多线程(5):Java多线程中的并发容器
一、JDK提供的并发容器都有哪些
JDK提供的容器大多在 java.util.concurrent
包中。
- ConcurrentHashMap:线程安全的HashMap
- CopyOnWriteArrayList:线程安全的List。在读多写少的场合性能非常好,远远好于Vector
- ConcurrentLinkedQueue(非阻塞队列):线程安全的LinkedList。并发队列,非阻塞队列,使用链表实现
- BlockingQueue(阻塞队列):是一个接口,阻塞队列。通过链表、数组等方式实现,非常适合做数据共享的通道
- ConcurrentSkipListMap:调表的实现。是一个Map,使得调表的数据结构能快速查找
1.彻底搞懂ConcurrentHashMap(线程安全HashMap)
对读和写都能保证高性能:读操作时(几乎)不加锁,写操作时通过 锁分段技术 ,只对操作的段加锁而不影响客户端对其他段的访问。
(1)ConcurrentHashMap与Hashtable的区别
底层数据结构不同:
①JDK1.7的ConcurrentHashMap底层采用 分段数组+链表实现、JDK1.8采用数据结构跟HashMap一样的 数组+链表/红黑二叉树 实现。
②Hashtable和JDK1.8之前的HashMap一样都是 数组+链表 实现。
注意:数组是HashMap主体,链表则是为了解决哈希冲突而存在
实现线程安全方式不同:
①JDK1.7的ConcurrentHashMap(分段锁)对整个桶数组进行分段(Segment),每把锁只锁容器一部分数据,多线程访问不同段数据不会存在锁竞争、JDK1.8摒弃了Segment直接采用 数组+链表+红黑树 的数据结构实现,并发控制使用 synchronized 和CAS操作。
②Hashtable(同一把锁)只使用synchronized 来保证线程安全,效率低。
(2)ConcurrentHashMap线程安全具体实现方式 / 底层原理
JDK1.7
将数据分为一段段存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一段数据时,其他段数据也能被其他线程访问。
ConcurrentHashMap 由Segment数组结构(数组+链表)和HashEntry数组结构组成。Segment实现了ReentrantLock所以Segment是可重入锁,扮演锁的角色;HashEntry用于存储键值对数据。
JDK1.8
取消了Segment分段锁,采用CAS和synchronized来保证并发安全,数据结构和HashMap类似,采用数组+链表/红黑二叉树。
synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,提高了效率。
2.彻底搞懂 CopyOnWriteArraylist(线程安全List)
读操作完全不加锁,写入也不会阻塞读取操作,只有写入和写入需要同步(ReentrantReadWriteLock
读写锁是,读读共享、写写互斥、读写和写读互斥)
(1)CopyOnWriteArraylist底层原理
CopyOnWriteArraylist的所有可变操作(add、set等)都是通过创建底层数组的副本实现。
当List需要被修改时并不修改原有内容,而是对原有数据进行一次复制,将修改的内容写入副本,写完后再将副本替换原来数据。这样可保证写操作不会影响读操作。
读取操作:没有任何同步控制和锁操作,因为内部数组array不会发生修改,只会被另一个array替换,可以保证数据安全。
写入操作:add()方法在添加集合的时候加了锁,保证了同步,避免了所线程写的时候会copy出多个副本出来。
3.ConcurrentLinkedQueue(非阻塞队列)
底层原理:使用 链表 作为数据结构,主要使用CAS非阻塞算法实现线程安全,高性能、无锁
4.BlockingQueue(阻塞队列)
(1)底层原理:主要应用于“生产者——消费者”问题中,原因是其提供了可阻塞的插入和移除的方法。当队列已满 生产者 线程会被阻塞,直到队列未满;当队列为空 消费者 线程被阻塞,直到队列非空。
(它是一个接口,继承Queue,而Queue又继承Collention接口)
(2)BlockingQueue接口的3个实现类ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue
——ArrayBlockingQueue(底层是: 数组)
有界队列(FIFO)实现类,一旦创建容量不能改变。其并发控制采用可重入锁实现,不管插入还是读取都需要先获取到锁,当队列满时插入队列会阻塞,从空队列读取元素也会阻塞。不保证公平性。
——LinkedBlockingQueue(底层是:单项链表)
可以是有界队列也可以试无界队列(FIFO),为防止容量迅速增加一般在建立时会指定大小,默认大小是Integer.MAX_VALUE,相比ArrayBlockingQueue有更高的吞吐量。
——PriorityBlockingQueue
支持优先级的 无界队列。默认采用自然顺序排序,也可以通过自定义类实现compareTo()方法来指定元素排序规则,还可以初始化时通过构造器参数Comparator来指定排序规则。并发控制采用ReentrantLock可重入锁,只能指定初始大小,后面插入容量时空间不够会自动扩容。
(是PriorityQueue 的线程安全版本,不可以插入null,插入的对象必须是可比较大小的)
什么是有界队列和无界队列:
有界队列:有固定大小的队列
无界队列:没有设置固定大小的队列