三军可夺帅也,匹夫不可夺志也。———《论语》
上一篇讲到同步容器类的潜在问题,可以通过两个方法解决。
- 可以通过客户端加锁解决。
- 可以使用并发容器类来解决问题。
客户端加锁的方法我们已经知道,所以,这一篇介绍一下并发容器类原理,看它是如何解决这些问题的。
下面看下并发容器的框架图:
我们从上图可以看到,它们分为五大类:Map, List, Set,Collection,Queue, 同步容器类都是从这五大基类继承而来,它们都是线程安全的。
同步容器类实现线程安全性的方式是所有方法都持有一个锁,并发容器则不同,这里主要讲介绍几个并发容器类来说明。
ConcurrentHashMap
它并不是每个方法都在同一个锁上实现同步使得每次只能有一个线程访问容器。而是使用一种粒度更细的加锁机制来实现更大程度的共享,这种机制称为分段锁。在这种机制中,任意数量的读取线程可以并发访问 Map,执行读取操作的线程和执行写入操作的线程可以并发的访问 Map,并且一定数量的写入线程可以并发的修改 Map。所以在并发环境中,它实现更高的吞吐量,在单线程环境中损失非常小的性能。
由于 ConcurrentHashMap 不能加锁来执行独占访问,因此我们无法通过客户端加锁来创建新的原子操作。然而,一些常见的复合操作已经实现了。包括如下接口:
当你需要这些额外的原子操作时,那么你可以考虑使用它。
CopyOnWriteArrayList
写时复制容器,可以理解为向容器添加一个元素时,先将当前的容器进行复制,生成一个新的容器,然后在向新的容器添加元素,之后再将原容器的引用指向新的容器。
好处是,对 CopyOnWrite 容器可以不用加锁进行并发的读,因为此时不会添加任何元素。CopyOnWrite 是一种读写分离的思想,读操作和写操作的是不同的容器。
它可以用于替代同步 List,但是每次在修改容器时都会复制底层数组。比如 add(),set() 等操作时,需要一定的开销,特别是当容器规模较大时,它比较适用于读多写少的并发场景。
ArrayBlockingQueue
它是数组实现的线程安全的有界的阻塞队列(FIFO),支持多任务并发操作。它内部通过互斥锁保护竞争资源,实现了多线程对竞争资源的互斥访问;有界是指数组的长度是固定的;阻塞是指当竞争资源已经某线程获取时,其他要获取该资源的线程需要阻塞等待。
它的核心函数都是通过可重入锁 ReentrantLock 来确保线程同步的。包括 put(),offer(),take(),poll() 等。同时,还包含两个条件 Condition(notEmpty, notFull),加入元素是,如果队列已满则必须等待;取出元素时,如果队列为空则必须等待。
总结
并发容器是针对多个线程并发访问设计的,通过并发容器来代替同步容器,可以极大的提高伸缩性并降低风险。并发容器提供的迭代不会抛出 ConcurrentModificationException,因此在迭代过程中不需要对容器加锁。另外,并发容器只能保证数据的最终一致性,不能保证实时一致性。换句话说,容器被修改后的数据并不保证能够实时的反应到迭代器的遍历。
本文完结,如果觉得有帮助,请关注我,谢谢!
参考
- 《Java并发编程实战》
- http://www.cnblogs.com/leesf456/p/5428630.html