JAVA多线程进阶篇 13、JUC并发容器

文章目录

  • 1. 同步容器
    • 1.1 同步容器存在性能问题
    • 1.2 同步容器依旧存在安全问题
    • 1.3 ConcurrentModificationException
  • 2. 并发容器
    • 2.1 ConcurrentHashMap
    • 2.2 CopyOnWriteArrayList
    • 2.3 BlockingQueue
      • 2.3.1 ArrayBlockingQueue
      • 2.3.2 LinkedBlockingQueue
      • 2.3.3 PriorityBlockingQueue
      • 2.3.4 SynchronousQueue
      • 2.3.5 DelayQueue
  • 总结

在Java的集合容器框架中,主要有四大类别:List、Set、Queue、Map。

List、Set、Queue接口分别继承了Collection接口,分别代表数组、集合和队列这三大类容器。Map本身是一个接口。

但是,由于缺少安全机制,像ArrayList、LinkedList、HashMap这些容器都是非线程安全的。

1. 同步容器

java语言为此设计了同步容器。内置了Vector、Stack、HashTable等同步容器。同步容器中的方法采用了synchronized进行了同步。

也可以使用Collections工具类将不安全的容器封装了同步容器。

        List<String> list = new ArrayList<>();
        Set<String> set = new HashSet<>();
        Map<String,String> map = new HashMap();

        List<String> list1 = Collections.synchronizedList(list);
        Set<String> set1 = Collections.synchronizedSet(set);
        Map<String,String> map1 = Collections.synchronizedMap(map);

1.1 同步容器存在性能问题

同步容器中的大量的方法采用了synchronized进行了同步,在并发环境中都会争抢这一把锁,同时只能有一个线程对容器进行访问,这必然会影响到执行性能。

1.2 同步容器依旧存在安全问题

public class ListConcurrentTest2 {

    static Vector<Integer> vector = new Vector<Integer>();

    public static void main(String[] args) {

        for (int i = 0; i < 1000; i++) {
            vector.add(1);
        }

        Thread thread1 = new Thread() {
            public void run() {
                for (int i = 0; i < vector.size(); i++)
                    vector.remove(-1);
            }
        };

        Thread thread2 = new Thread() {
            public void run() {
                for (int i = 0; i < vector.size(); i++)
                    vector.get(i);
            }
        };
        thread1.start();
        thread2.start();
    }
}

运行结果,由于线程删除了vector的元素,导致thread2 遍历时,下标元素被删除不存在,发生越界错误。

Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException: -1
	at java.util.Vector.elementData(Vector.java:737)
	at java.util.Vector.remove(Vector.java:835)
	at com.concurrent.juc.ch13.ListConcurrentTest2$1.run(ListConcurrentTest2.java:26)

Process finished with exit code 0

1.3 ConcurrentModificationException

在对Vector等容器并发地进行迭代修改时,会报ConcurrentModificationException异常,但是在并发容器中不会出现这个问题。 比如:

public class ListConcurrentTest3 {
    public static void main(String[] args)  {
        ArrayList<Integer> list = new ArrayList<Integer>();
        list.add(1);
        list.add(2);
        list.add(3);

        Iterator<Integer> iterator = list.iterator();
        while(iterator.hasNext()){
            Integer integer = iterator.next();
            list.remove(integer);
        }
    }
}

此时,由于iterator在迭代时修改了list内容,报错

Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at com.concurrent.juc.ch13.ListConcurrentTest3.main(ListConcurrentTest3.java:22)

2. 并发容器

同步容器将所有对容器状态的访问都串行化了,以保证了线程的安全性,但是安全隐患仍然存在,而且严重降低了并发性,当多个线程竞争容器时,性能急剧下降。
因此Java5.0开始针对多线程并发访问设计,提供了并发性能较好的并发容器,有以下优点。

  • 根据具体场景进行设计,尽量避免synchronized,提供并发性。
  • 定义了一些并发安全的复合操作,并且保证并发环境下的迭代操作不会出错。

2.1 ConcurrentHashMap

ConcurrentHashMap可以做到读取数据不加锁,对数据分桶,在内部采用了一个叫做Segment数组,Segment元素指向一个Map。ConcurrentHashMap需要两次Hash操作,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部,因此,这一种结构的带来的副作用是Hash的过程要比普通的HashMap要长,但是带来的好处是写操作的时候可以只对元素所在的Segment进行加锁即可,不会影响到其他的Segment。
ConcurrentHashMap的源码值得深入研究,可以参考《JAVA多线程源码篇》

2.2 CopyOnWriteArrayList

Copy-On-Write是一种用于程序设计中的优化策略。其基本思路是,在读时共享数据,但是当有线程要修改元素时会将内部数组拷贝一份,对拷贝数组进行修改,拷贝完成后再更正链接指向。JUC容器里有CopyOnWriteArrayList和CopyOnWriteArraySet,可以在非常多的并发场景中使用到。

2.3 BlockingQueue

BlockingQueue常用于生产者消费者场景。队列满时,生产者会阻塞,队列空时,消费者会阻塞。可以方便地实现线程协同工作。

  • 插入方法
    • add(o) 如果无法立即执行,抛出异常。
    • offer(o) 返回boolean,表示是否成功操作。
    • offer(o, timeout, timeunit) 带超时参数的阻塞方法。
    • put(o) 阻塞方法,如果队列满将阻塞。
  • 删除方法
    • remove(o) 如果无法立即执行,抛出异常。
    • poll(o) 返回boolean,表示是否成功操作。
    • poll(o, timeout, timeunit) 带超时参数的阻塞方法。
    • take(o) 阻塞方法,如果队列空将阻塞。

2.3.1 ArrayBlockingQueue

ArrayBlockingQueue 是一个有界的阻塞队列,其内部实现是将对象放到一个数组里。有界也就意味着,它不能够存储无限多数量的元素。它有一个同一时间能够存储元素数量的上限。

2.3.2 LinkedBlockingQueue

LinkedBlockingQueue 内部以一个链式结构(链接节点)对其元素进行存储。如果需要的话,这一链式结构可以选择一个上限。如果没有定义上限,将使用 Integer.MAX_VALUE 作为上限。

2.3.3 PriorityBlockingQueue

PriorityBlockingQueue 是一个无界的并发队列。它使用了和类 java.util.PriorityQueue 一样的排序规则。你无法向这个队列中插入 null 值。所有插入到 PriorityBlockingQueue 的元素必须实现 java.lang.Comparable 接口。

2.3.4 SynchronousQueue

SynchronousQueue 是一个特殊的队列,它的内部同时只能够容纳单个元素。如果该队列已有一元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队列中抽走。

2.3.5 DelayQueue

DelayQueue 对元素进行持有直到一个特定的延迟到期。在每个元素的 getDelay() 方法返回的值的时间段之后才释放掉该元素。如果返回的是 0 或者负值,延迟将被认为过期,该元素将会在 DelayQueue 的下一次 take 被调用的时候被释放掉。

总结

Java5.0提供了并发性能较好的并发容器,根据具体场景进行设计,尽量避免synchronized,提供并发性。 定义了一些并发安全的复合操作,并且保证并发环境下的迭代操作不会出错。

多线程系列在github上有一个开源项目,主要是本系列博客的实验代码。

https://github.com/forestnlp/concurrentlab

如果您对软件开发、机器学习、深度学习有兴趣请关注本博客,将持续推出Java、软件架构、深度学习相关专栏。

您的支持是对我最大的鼓励。

你可能感兴趣的:(JAVA多线程进阶篇,java,开发语言,后端)