JUC并发集合总结

ConcurrentLinkedQueue

线程安全的支持高并发的队列,使用链表实现。非阻塞,无锁,无界。该队列也不允许空元素,而且size方法并不是常量,其需要遍历链表,此时并发修改链表会造成统计size不正确。同样,bulk操作和equal以及toArray方法不保证原子性。

代码实现:

public class ConcurrentLinkedQueueTest {

    public static void main(String[] args) {

        final ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();

        // 往队列中执行添加的任务
        Runnable offerTask = () -> {
            String threadName = Thread.currentThread().getName();
            for (int i = 0; i < 10000; i++) {
                queue.offer(threadName + i);
            }
        };

        // 往队列中执行移除的任务
        Runnable pollTask = () -> {
            for (int i = 0; i < 10000; i++) {
                queue.poll();
            }
        };

        Thread[] threads = new Thread[100];

        // 100个offerTask线程
        for (int i = 0; i < 100; i++) {
            threads[i] = new Thread(offerTask);
            threads[i].start();
        }

        // 主线程等待生产线程执行完毕
        for (int i = 0; i < 100; i++) {
            try {
                threads[i].join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("当前队列size:" + queue.size());
        Assert.assertEquals(10000 * 100, queue.size());

        // 100个pollTask线程
        for (int i = 0; i < 100; i++) {
            threads[i] = new Thread(pollTask);
            threads[i].start();
        }

        // 主线程等待消费线程执行完毕
        for (int i = 0; i < 100; i++) {
            try {
                threads[i].join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("当前队列size:" + queue.size());
        Assert.assertEquals(0, queue.size());
    }
}

ConcurrentLinkedDeque

并发队列ConcurrentLinkedDeque,这是一个非阻塞,无锁,无界,线程安全双端操作的队列。简单说就是ConcurrentLinkedQueue的升级版,在JDK7之后才提供。该队列也不允许空元素,而且size方法并不是常量,其需要遍历链表,此时并发修改链表会造成统计size不正确。同样,bulk操作和equal以及toArray方法不保证原子性。

主要API介绍

  1. 不抛异常,会移除
    pollFirst()pollLast():返回并移除队列中第一个元素和最后一个元素,如果队列为空,返回null
  2. 不抛异常,不会移除
    peek()peekFirst()peekLast():这些方法将分别返回列表的第一个和最后一个元素。它们不会从列表删除返回的元素。如果列表为空,这些方法将返回null值。
  3. 抛异常,会移除
    remove()removeFirst()removeLast():这些方法将分别返回列表的第一个和最后一个元素。它们将从列表删除返回的元素。如果列表为空,这些方法将抛出NoSuchElementExcpetion异常。
  4. 抛异常,不会移除
    getFirst()getLast():这些方法将分别返回列表的第一个和最后一个元素。它们不会从列表删除返回的元素。如果列表为空,这些方法将抛出NoSuchElementExcpetion异常。

代码实现

public class AddTask implements Runnable {

    private ConcurrentLinkedDeque<String> deque;

    public AddTask(ConcurrentLinkedDeque<String> deque) {
        this.deque = deque;
    }

    // 在列表中存储10000个正在执行任务的线程的名称和一个数字的字符串。
    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        for(int i = 0; i < 10000; i++) {
            deque.add(name + ": Element " + i);
        }
    }
}
public class PollTask implements Runnable {
    private ConcurrentLinkedDeque<String> deque;

    public PollTask(ConcurrentLinkedDeque<String> deque) {
        this.deque = deque;
    }

    // 从列表中取出10000个元素(在一个循环5000次的循环中,每次取出2个元素)。
    @Override
    public void run() {
        for(int i = 0; i < 5000; i++) {
            // 返回并移除队列中第一个元素,如果队列为空,返回null
            deque.pollFirst();
            // 返回并队列中最后一个元素,如果队列为空,返回null
            deque.pollLast();
        }
    }
}
public class Main {

    public static void main(String[] args) {

        final ConcurrentLinkedDeque<String> deque = new ConcurrentLinkedDeque<>();
        Thread[] threads = new Thread[100];

        // 创建100个AddTask,并启动线程,每一个task都会在list中加入10000个元素,即最终应该是1000000个元素
        for(int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(new AddTask(deque));
            threads[i].start();
        }
        System.out.printf("Main: %d AddTask threads have been launched\n", threads.length);

        // 让Main线程等待所有AddTask线程执行完毕后才能继续执行
        for(int i = 0; i < threads.length; i++) {
            try {
                threads[i].join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.printf("Main: Size of the List: %d\n", deque.size());

        // 创建100个PollTask,并启动线程,每一个task会从list中取出10000个元素,即list最终应剩余0个元素
        for(int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(new PollTask(deque));
            threads[i].start();
        }
        System.out.printf("Main: %d PollTask threads have been launched\n", threads.length);

        // 让Main线程等待所有PollTask线程执行完毕后才能继续执行
        for(int i = 0; i < threads.length; i++) {
            try {
                threads[i].join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.printf("Main: Size of the List: %d\n", deque.size());
    }
}

DelayQueue

此队列中的元素必须实现Delayed接口,其实也是一个阻塞队列。

代码实现

// 事件类
public class Event implements Delayed {
    // 到期时间
    private Date startDate;

    public Event(Date startDate) {
        this.startDate = startDate;
    }

    // 此方法返回此延迟对象剩余多少时间到期,根据给定的TmeUnit表示
    @Override
    public long getDelay(TimeUnit unit) {
        Date now = new Date();
        long diff = startDate.getTime() - now.getTime();
        return unit.convert(diff, TimeUnit.MILLISECONDS);
    }

    // 可以理解为,延迟队列根据此方法的返回值,排列延迟对象在延迟队列中的位置
    // 这里表达的意思是: 距离到期时间越远的延迟对象,放到延迟队列的越后面
    @Override
    public int compareTo(Delayed o) {
        long result = this.getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS);
        if (result < 0) {
            return -1;
        } else if (result > 0) {
            return 1;
        } else {
            return 0;
        }
    }
}
// 任务类
public class Task implements Runnable {

    private int id;
    private DelayQueue<Event> queue;

    public Task(int id, DelayQueue<Event> queue) {
        this.id = id;
        this.queue = queue;
    }

    @Override
    public void run() {
        Date now = new Date();
        Date delay = new Date();
        delay.setTime(now.getTime() + (id * 4000));
        System.out.printf("Thread %s: %s\n", id, delay);

        for (int i = 0; i < 100; i++) {
            Event event = new Event(delay);
            queue.add(event);
        }
    }
}
// 主类
/*
    DelayedQueue类是Java API提供的一种有趣的数据结构,并且你可以用在并发应用程序中。
    在这个类中,你可以存储带有激活日期的元素。方法返回或抽取队列的元素将忽略未到期的数据元素。它们对这些方法来说是看不见的。
    为了获取这种行为,你想要存储到DelayedQueue类中的元素必须实现Delayed接口。这个接口允许你处理延迟对象,
    所以你将实现存储在DelayedQueue对象的激活日期,这个激活时期将作为对象的剩余时间,直到激活日期到来。
    这个接口强制实现以下两种方法:
    1.compareTo(Delayed o):Delayed接口继承Comparable接口。
        如果执行这个方法的对象的延期小于作为参数传入的对象时,该方法返回一个小于0的值。
        如果执行这个方法的对象的延期大于作为参数传入的对象时,该方法返回一个大于0的值。
        如果这两个对象有相同的延期,该方法返回0。
    2.getDelay(TimeUnit unit):该方法返回与此对象相关的剩余延迟时间,以给定的时间单位表示。
*/
public class Main {

    public static void main(String[] args) throws InterruptedException {

        // 一个共享延迟队列
        DelayQueue<Event> queue = new DelayQueue<>();
        Thread threads[] = new Thread[5];

        // 5个线程
        for (int i = 0; i < threads.length; i++) {
            Task task = new Task(i + 1, queue);
            threads[i] = new Thread(task);
        }

        // 启动这5个线程,每个线程往延迟队列中放入100个Event对象,并指定Event对象的StartDate
        for (int i = 0; i < threads.length; i++) {
            threads[i].start();
        }

        // 等待线程执行完毕
        for (int i = 0; i < threads.length; i++) {
            threads[i].join();
        }
        System.out.println(queue.size());
        // 当延迟队列中有数据时,每500毫秒从延迟队列中
        do {
            int counter = 0;
            Event event;
            do {
                // 只有当延迟对象到期后才能从queue中取到
                // 注意: 这里如果使用take方法获取,如果获取不到会阻塞.而poll方法获取不到会返回空
                event = queue.poll();
                if (event != null) counter++;
            } while (event != null);
            System.out.printf("At %s you have read %d events\n", new Date(), counter);
            TimeUnit.MILLISECONDS.sleep(500);
        } while (queue.size() > 0);
    }
}

ArrayBlockingQueue

有界阻塞队列,数组实现。

代码实现

public class ArrayBlockingQueueTest {

    public static void main(String[] args) {

        // 用于记录生产总量
        AtomicInteger putCount = new AtomicInteger(0);
        // 用于记录消费总量
        AtomicInteger takeCount = new AtomicInteger(0);

        final ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(10);

        // putTask负责生产数据放入queue
        Runnable putTask = () -> {
            String threadName = Thread.currentThread().getName();
            for(int i = 0; i < 10000; i++) {
                try {
                    // 向queue中放入数据。当queue中的元素数量已满,则阻塞等待,等到有人消费掉,再向queue中放入数据
                    queue.put(threadName + i);
                    putCount.incrementAndGet();
                    System.out.println(threadName + ":" + queue.size());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        // takeTask负责从queue消费数据
        Runnable takeTask = () -> {
            String threadName = Thread.currentThread().getName();
            for(int i = 0; i < 10000; i++) {
                try {
                    // 从queue中取出数据。当queue中没有元素时,则阻塞等待,等到有人生产出数据放入queue时再进行消费
                    queue.take();
                    takeCount.incrementAndGet();
                    System.out.println(threadName + ":" + queue.size());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        Thread[] putThreads = new Thread[10];
        Thread[] takeThreads = new Thread[10];

        // 启动10个生产者线程和10个消费者线程
        for(int i = 0; i < 10; i++) {
            Thread putThread = new Thread(putTask, "producer" + i);
            putThreads[i] = putThread;
            putThread.start();
            Thread takeThread = new Thread(takeTask, "consumer" + i);
            takeThreads[i] = takeThread;
            takeThread.start();
        }

        // 等待生产者线程和消费者线程执行完毕
        for(int i = 0; i < 10; i++) {
            try {
                putThreads[i].join();
                takeThreads[i].join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 输出生产总量和消费总量
        System.out.println(putCount.get());
        System.out.println(takeCount.get());
    }
}

LinkedBlockingQueue

无界阻塞队列(也可以指定有界),链表实现。和LinkedBlockingDeque差不多,一个单向、一个双向。

LinkedBlockingDeque

无界阻塞队列(也可以指定有界),链表实现。和LinkedBlockingQueue差不多,一个单向、一个双向。

主要API介绍

  1. 获取:获取到并移除/获取不到就阻塞
    takeFirst()takeLast():这些方法分别返回队列的第一个和最后一个元素。它们从队列删除返回的元素。如果队列为空,这些方法将阻塞线程,直到队列有元素。

  2. 获取:获取到并移除/获取不到返回null
    poll()pollFirst()pollLast():这些方法分别返回队列的第一个和最后一个元素。它们从队列删除返回的元素。如果队列为空,这些方法将返回null值。

  3. 获取:获取到但不移除/获取不到抛异常
    getFirst()getLast():这些方法分别返回队列的第一个和最后一个元素。它们不会从队列删除返回的元素。如果队列为空,这些方法将抛出NoSuchElementExcpetion异常。

  4. 获取:获取到但不移除/获取不到返回null
    peek()peekFirst(),和peekLast():这些方法分别返回队列的第一个和最后一个元素。它们不会从队列删除返回的元素。如果队列为空,这些方法将返回null值。

  5. 添加:超过容量的添加会抛出异常
    add()addFirst()addLast():这些方法分别在第一个位置和最后一个位置上添加元素(add是在最后添加)。如果队列已满(你已使用固定大小创建它),这些方法将抛出IllegalStateException异常。

代码实现

public class Client implements Runnable {

    private LinkedBlockingDeque<String> deque;

    public Client(LinkedBlockingDeque<String> deque) {
        this.deque = deque;
    }

    @Override
    public void run() {
        // 每2秒往队列中放入5个字符串,重复3次
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 5; j++) {
                StringBuilder request = new StringBuilder();
                request.append(i);
                request.append(":");
                request.append(j);
                try {
                    deque.put(request.toString());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.printf("Client: %s at %s.\n", request, new Date());
            }
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.printf("Client: End.\n");
    }
}
public class Main {

    /*
        Client类使用put()方法添加字符串到队列中。
        如果队列已满(因为你已使用固定大小来创建它),这个方法阻塞线程的执行,直到队列有可用空间。
        
        Main类使用take()方法从队列中获取字符串,
        如果队列为空,这个方法将阻塞线程的执行,直到队列中有元素。

        在这个例子中,使用LinkedBlockingDeque类的这两个方法,
        如果它们在阻塞时被中断,将抛出InterruptedException异常。
        所以,你必须包含必要的代码来捕捉这个异常。
    */
    public static void main(String[] args) throws InterruptedException {

        // 创建阻塞双端队列
        LinkedBlockingDeque<String> deque = new LinkedBlockingDeque<>(3);
        // Client每2秒会往这个双端队列中放入5个字符串,重复3次
        Thread thread = new Thread(new Client(deque));
        // 启动线程
        thread.start();

        // 每300毫秒从队列中取走3个元素,重复5次
        for (int i = 0; i < 5; i++) {
            for (int j = 0; j < 3; j++) {
                String request = deque.take();
                System.out.printf("Main: Request: %s at %s. Size:%d\n", request, new Date(), deque.size());
            }
            TimeUnit.MILLISECONDS.sleep(300);
        }

        System.out.printf("Main: End of the program.\n");
    }
}

PriorityBlockingQueue

优先级队列,所有存储在PriorityBlockingQueue的元素必须实现Comparable接口。在使用add方法加入元素时,并不会进行排序,而是在第一次调用take或poll等方法时才会将队列元素排序(根据compareTo方法作升序排列),然后取出优先级最高的元素。

代码实现

public class Event implements Comparable<Event> {

    // 用来存储已创建事件的线程数。
    private int thread;

    // 用来存储事件的优先级。
    private int priority;

    public Event(int thread, int priority) {
        this.thread = thread;
        this.priority = priority;
    }

    public int getThread() {
        return thread;
    }

    public int getPriority() {
        return priority;
    }

    // 实现compareTo()方法。它接收Event作为参数,并且比较当前事件与参数的优先级。
    // 如果当前事件的优先级更大,则返回-1,如果这两个优先级相等,则返回0,如果当前事件的优先级更小,则返回1。
    // 注意,这与大多数Comparator.compareTo()的实现是相反的。
    // 即当前对象的优先级越高,越应该排在队列的最前方,可以理解为队列中的排序是根据此方法作升序排列的
    @Override
    public int compareTo(Event e) {
        if (this.priority > e.getPriority()) {
            return -1;
        } else if (this.priority < e.getPriority()) {
            return 1;
        } else {
            return 0;
        }
    }
}

SynchronousQueue

同步阻塞队列,直接队列中add会抛异常,除非此时有一个线程正调用take向此队列索取元素,那么队列会将add进来的元素直接交给正在take的线程。

代码实现

public class SynchronousQueueTest {

    public static void main(String[] args) {

        // 可以理解为这个阻塞队列的大小为0,生产者想要往这个队列中放数据是放不进的,
        // 除非此时有线程在从此队列中取数据,那么此队列会将生产者会生产出的数据直接交给消费者

        // 当没有消费者取数据时,各类添加操作的行为:
        //     put      阻塞
        //     offer    返回false
        //     add      抛出异常

        final SynchronousQueue<Integer> queue = new SynchronousQueue<>();

        // 生产者任务
        new Thread(() -> {
            String threadName = Thread.currentThread().getName();
            for (int i = 0; i < 100; i++) {
                try {
                    queue.put(i);
                    // 永远为0
                    System.out.println(threadName + ":" + queue.size());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        // 消费者任务
        new Thread(() -> {
            String threadName = Thread.currentThread().getName();
            for (int i = 0; i < 100; i++) {
                try {
                    queue.take();
                    // 永远为0
                    System.out.println(threadName + ":" + queue.size());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

LinkedTransferQueue

TransferQueue也具有SynchronousQueue的所有功能,但是TransferQueue的功能更强大。

主要API介绍:

  1. transfer(E e)
    若当前存在一个正在等待获取的消费者线程,即立刻将e移交之;否则将元素e插入到队列尾部,并且当前线程进入阻塞状态,直到有消费者线程取走该元素。
  2. tryTransfer(E e)
    若当前存在一个正在等待获取的消费者线程,则该方法会即刻转移e,并返回true;若不存在则返回false,但是并不会将e插入到队列中。这个方法不会阻塞当前线程,要么快速返回true,要么快速返回false。
  3. hasWaitingConsumer()getWaitingConsumerCount()
    用来判断当前正在等待消费的消费者线程个数。
  4. tryTransfer(E e, long timeout, TimeUnit unit)
    若当前存在一个正在等待获取的消费者线程,会立即传输给它; 否则将元素e插入到队列尾部,并且等待被消费者线程获取消费掉。若在指定的时间内元素e无法被消费者线程获取,则返回false,同时该元素从队列中移除。

代码实现

public class TransferQueueTest1 {

    public static void main(String[] args) throws InterruptedException {
        final TransferQueue<String> queue = new LinkedTransferQueue<>();

        // 1
        new Thread(() -> {
            try {
                // 向空队列中获取数据,这里会阻塞
                System.out.println(Thread.currentThread().getName() + " : queue.take() = " + queue.take());
                System.out.println(Thread.currentThread().getName() + " : queue.size() = " + queue.size());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        TimeUnit.SECONDS.sleep(1);

        // 2
        new Thread(() -> {
            try {
                // transfer(E e):
                // 若当前存在一个正在等待获取的消费者线程,即立刻将元素移交给消费者线程,
                // 否则将元素插入到队列尾部,并且当前线程进入阻塞状态,直到有消费者线程取走该元素

                // A元素直接移交成功,因为在此之前有一个消费者线程正在等待获取
                queue.transfer("A");

                // B元素会移交不成功,所以会添加到队列尾部
                queue.transfer("B");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        TimeUnit.SECONDS.sleep(3);

        // 这里会获取到B元素,B元素在之前无法直接移交,会放到队列中,这里使用take方法直接获取到
        System.out.println(Thread.currentThread().getName() + " : queue.take() = " + queue.take());

        // 此时队列容量为0,队列中所有元素均被取走
        System.out.println(Thread.currentThread().getName() + " : queue.size() = " + queue.size());
    }
}
public class TransferQueueTest2 {

    public static void main(String[] args) throws InterruptedException {
        final TransferQueue<String> queue = new LinkedTransferQueue<>();

        // 1
        new Thread(() -> {
            try {
                // 向空队列中获取数据,这里会阻塞
                System.out.println(Thread.currentThread().getName()
                        + " : queue.take() = " + queue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        TimeUnit.SECONDS.sleep(1);

        // 2
        new Thread(() -> {
            // tryTransfer(E e):
            // 若当前存在一个正在等待获取的消费者线程,则该方法会即刻转移e,并返回true;
            // 若不存在则返回false,但是并不会将e插入到队列中。
            // 这个方法不会阻塞当前线程,要么快速返回true,要么快速返回false。

            // A元素直接移交成功,因为在此之前有一个消费者线程正在等待获取
            System.out.println(Thread.currentThread().getName() +
                    " : queue.tryTransfer(\"A\") = " + queue.tryTransfer("A"));

            // B元素会移交不成功,直接返回false,也不会将B放入队列中
            System.out.println(Thread.currentThread().getName() +
                    " : queue.tryTransfer(\"B\") = " + queue.tryTransfer("B"));
        }).start();

        TimeUnit.SECONDS.sleep(1);

        // 此时队列大小为0,因为tryTransfer不会将未成功移交的元素放入队列
        System.out.println(Thread.currentThread().getName() + " : queue.size() = " + queue.size());
    }
}

ConcurrentSkipListMap

ConcurrentSkipListMap是ConcurrentNavigableMap的实现类,在内部实现中,它使用Skip List来存储数据。Skip List是基于并行列表的数据结构,它允许我们获取类似二叉树的效率。使用它,你可以得到一个排序的数据结构,这比排序数列使用更短的访问时间来插入、搜索和删除元素。

主要API介绍

  1. headMap(K toKey):K是参数化ConcurrentSkipListMap对象的Key值的类。返回此映射的部分视图,其键值小于 toKey。
  2. tailMap(K fromKey):K是参数化ConcurrentSkipListMap对象的Key值的类。返回此映射的部分视图,其键大于等于 fromKey。
  3. putIfAbsent(K key, V Value):如果key不存在map中,则这个方法插入指定的key和value。
  4. pollLastEntry():这个方法返回并删除map中最后一个元素的Map.Entry对象。
  5. replace(K key, V Value):如果这个key存在map中,则这个方法将指定key的value替换成新的value。

代码实现

public class Main {
    public static void main(String[] args) throws InterruptedException {

        ConcurrentSkipListMap<String, Contact> map = new ConcurrentSkipListMap<>();

        Thread threads[] = new Thread[25];
        int counter = 0;

        // 创建并启动25个线程,每个线程的任务就是往map中放入1000条数据
        // key:A1001  value:new Contact(A, 1001)
        for (char i = 'A'; i < 'Z'; i++) {
            // Task (ConcurrentSkipListMap<String, Contact> map, String id)
            Task task = new Task(map, String.valueOf(i));
            threads[counter] = new Thread(task);
            threads[counter].start();
            counter++;
        }

        // 等待线程创建完毕
        for (int i = 0; i < 25; i++) {
            threads[i].join();
        }

        // 输出map当前容量,即25000
        System.out.printf("Main: Size of the map: %d\n", map.size());
        Map.Entry<String, Contact> element;
        Contact contact;

        // 获取第一个Entry
        element = map.firstEntry();
        contact = element.getValue();
        System.out.printf("Main: First Entry: %s: %s\n", contact.getName(), contact.getPhone());

        // 获取最后一个Entry
        element = map.lastEntry();
        contact = element.getValue();
        System.out.printf("Main: Last Entry: %s: %s\n", contact.getName(), contact.getPhone());

        // 获取key为A1996-B1002之间的数据作为子map,实际上取出的是A1996-B1001,即包左不包右的原则
        System.out.printf("Main: Submap from A1996 to B1002: \n");
        ConcurrentNavigableMap<String, Contact> submap = map.subMap("A1996", "B1002");

        do {
            // 获取并移除第一个Entry
            element = submap.pollFirstEntry();
            if (element != null) {
                contact = element.getValue();
                System.out.printf("%s: %s\n", contact.getName(), contact.getPhone());
            }
        } while (element != null);

        // 输出map当前容量,即24994
        System.out.printf("Main: Size of the map: %d\n", map.size());
    }

}

ConcurrentHashMap

以下内容的原文地址:ConcurrentHashMap总结
并发编程实践中,ConcurrentHashMap是一个经常被使用的数据结构,相比于Hashtable以及Collections.synchronizedMap(),ConcurrentHashMap在线程安全的基础上提供了更好的写并发能力,但同时降低了对读一致性的要求。ConcurrentHashMap的设计与实现非常精巧,大量的利用了volatile,final,CAS等lock-free技术来减少锁竞争对于性能的影响。ConcurrentHashMap在JDK6,7,8中实现都不同。

JDK6与JDK7中的实现

ConcurrentHashMap采用了分段锁的设计,只有在同一个分段内才存在竞态关系,不同的分段锁之间没有锁竞争。相比于对整个Map加锁的设计,分段锁大大的提高了高并发环境下的处理能力。但同时,由于不是对整个Map加锁,导致一些需要扫描整个Map的方法(如size(), containsValue())需要使用特殊的实现,另外一些方法(如clear())甚至放弃了对一致性的要求(ConcurrentHashMap是弱一致性的,具体请查看ConcurrentHashMap能完全替代HashTable吗?)。

JDK8中的实现

ConcurrentHashMap在JDK8中进行了巨大改动,很需要通过源码来再次学习下Doug Lea的实现方法。
它摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用CAS算法。它沿用了与它同时期的HashMap版本的思想,底层依然由“数组”+链表+红黑树的方式思想(JDK7与JDK8中HashMap的实现),但是为了做到并发,又增加了很多辅助的类,例如TreeBin,Traverser等对象内部类。

代码实现

public class ConcurrentHashMapTest {

    public static long execute(Map<String, String> map) {
        Random random = new Random();
        Thread[] threads = new Thread[100];

        long start = System.currentTimeMillis();

        for (int i = 0; i < threads.length; i++) {
            final int x = i;
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    map.put(String.valueOf(random.nextInt(100000)), x + "" + j);
                }
            });
        }

        for (int i = 0; i < 100; i++) {
            threads[i].start();
        }

        for (int i = 0; i < 100; i++) {
            try {
                threads[i].join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        return System.currentTimeMillis() - start;
    }

    public static void main(String[] args) {

        Map<String, String> hashtable = new Hashtable<>();
        Map<String, String> concurrentHashMap = new ConcurrentHashMap<>();
        Map<String, String> concurrentSkipListMap = new ConcurrentSkipListMap<>();

        long total = 0;
        for (int i = 0; i < 10; i++) {
            total += execute(hashtable);
        }
        System.out.println("hashtable: " + total); // 3942

        total = 0;
        for (int i = 0; i < 10; i++) {
            total += execute(concurrentHashMap);
        }
        System.out.println("concurrentHashMap: " + total); // 1258

        total = 0;
        for (int i = 0; i < 10; i++) {
            total += execute(concurrentSkipListMap);
        }
        System.out.println("concurrentSkipListMap: " + total); // 2525
    }
}

CopyOnWrite

以下内容的原文地址:聊聊并发-Java中的Copy-On-Write容器

Copy-On-Write简称COW,是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器非常有用,可以在非常多的并发场景中使用到。

什么是CopyOnWrite容器?

CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

实现原理

以下是CopyOnWriteArrayList的源码,可以发现在添加的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来。读的时候不需要加锁,如果读的时候有多个线程正在向list添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的list。

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

public E get(int index) {
    return get(getArray(), index);
}

代码实现

public class CopyOnWriteArrayListTest {

    public static void main(String[] args) {
        final List<String> list =
//                Collections.synchronizedList(new ArrayList<>());
//                new Vector<>();
                // 并发写时,CopyOnWriteArrayList性能远不如synchronizedList和Vector
                new CopyOnWriteArrayList<>();
        Random random = new Random();

        // 创建100个线程,每个线程的任务就是往list中放入1000条数据
        Thread[] threads = new Thread[100];
        for (int i = 0; i < threads.length; i++) {
            Runnable task = () -> {
                for (int j = 0; j < 1000; j++) {
                    list.add("a" + random.nextInt(10000));
                }
            };
            threads[i] = new Thread(task);
        }

        long start = System.currentTimeMillis();
        // 启动所有线程
        for (Thread thread : Arrays.asList(threads)) {
            thread.start();
        }
        // 等待这些线程执行完毕
        for (Thread thread : Arrays.asList(threads)) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(System.currentTimeMillis() - start);

        System.out.println(list.size());
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容