为什么juc下的集合类是线程安全的
1. JUC 简介 在 Java 5.0 提供了? 2.并发容器类我们都知道在java包下的集合大多是线程不安全的,而Vector,stack,hashtable是线程安全的,它们的线程安全是依靠synchronized,效率低。虽然可以通过Collections工具类中的方法获取java集合包对应的同步类,但是这些同步类的并发效率并不是很高。为了更好的支持高并发任务,并发大师Doug Lea在JUC(java.util.concurrent)包中添加了java集合包中单线程类的对应的支持高并发的类。 例如,ArrayList对应的高并发类是CopyOnWriteArrayList,HashMap对应的高并发类是ConcurrentHashMap,等等。 JUC包在添加”java集合包“对应的高并发类时,为了保持API接口的一致性,使用了”Java集合包“中的框架。例如,CopyOnWriteArrayList实现了“Java集合包”中的List接口,ConcurrentHashMap继承了“java集合包”中的AbstractMap类等等,是的我们便于理解。 ? List和Set JUC集合包中的List和Set实现类包括: CopyOnWriteArrayList,CopyOnWriteArraySet和ConcurrentSkipListSet。CopyOnWriteArrayList 和 CopyOnWriteArraySet的框架如下图所示: (01) CopyOnWriteArrayList相当于线程安全的ArrayList,它实现了List接口。CopyOnWriteArrayList是支持高并发的。 CopyOnWriteArrayList。 1. CopyOnWriteArrayList实现了List接口,因此它是一个队列。 2. CopyOnWriteArrayList包含了成员lock。每一个CopyOnWriteArrayList都和一个互斥锁lock绑定,通过lock,实现了对CopyOnWriteArrayList的互斥访问。 我们来看看CopyOnWriteArrayList的核心的源码: ? public boolean add(E e) { final ReentrantLock lock = this.lock; // 获取“锁” lock.lock(); try { // 获取原始”volatile数组“中的数据和数据长度。 Object[] elements = getArray(); int len = elements.length; // 新建一个数组newElements,并将原始数据拷贝到newElements中; // newElements数组的长度=“原始数组的长度”+1 Object[] newElements = Arrays.copyOf(elements,len + 1); // 将“新增加的元素”保存到newElements中。 newElements[len] = e; // 将newElements赋值给”volatile数组“。 setArray(newElements); return true; } finally { // 释放“锁” lock.unlock(); } } 说明:?第一,在”添加操作“开始前,获取独占锁(lock),若此时有需要线程要获取锁,则必须等待;在操作完毕后,释放独占锁(lock),此时其它线程才能获取锁。通过独占锁,来防止多线程同时修改数据!lock的定义如下:transient final ReentrantLock lock = new ReentrantLock(); ? ? ? ? 第二,操作完毕时,会通过setArray()来更新”volatile数组“。而且,前面我们提过”即对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入“;这样,每次添加元素之后,其它线程都能看到新添加的元素。 ?由于ReentrantLock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,ReentrantLock类提供了一些高级功能: 1.等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。 2.公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁 3.锁绑定多个条件,一个ReentrantLock对象可以同时绑定多个对象。ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。 简单来说,ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。 ? Map JUC集合包中Map的实现类包括: ConcurrentHashMap和ConcurrentSkipListMap。它们的框架如下图所示: (01) ConcurrentHashMap是线程安全的哈希表(相当于线程安全的HashMap);它继承于AbstractMap类,并且实现ConcurrentMap接口。ConcurrentHashMap是通过“锁分段”来实现的,它支持并发。 ConcurrentHashMap。 1、ConcurrentHashMap是线程安全的哈希表,它是通过“锁分段”来实现的。ConcurrentHashMap中包括了“Segment(锁分段)数组”,每个Segment就是一个哈希表,而且也是可重入的互斥锁。第一,Segment是哈希表表现在,Segment包含了“HashEntry数组”,而“HashEntry数组”中的每一个HashEntry元素是一个单向链表。即Segment是通过链式哈希表。第二,Segment是可重入的互斥锁表现在,Segment继承于ReentrantLock,而ReentrantLock就是可重入的互斥锁。 (01) put()根据key获取对应的哈希值,再根据哈希值找到对应的Segment片段。如果Segment片段不存在,则新增一个Segment。 final V put(K key,int hash,V value,boolean onlyIfAbsent) { // tryLock()获取锁,成功返回true,失败返回false。 // 获取锁失败的话,则通过scanAndLockForPut()获取锁,并返回”要插入的key-value“对应的”HashEntry链表“。 HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key,hash,value); V oldValue; try { // tab代表”当前Segment中的HashEntry数组“ HashEntry<K,V>[] tab = table; // 根据”hash值“获取”HashEntry数组中对应的HashEntry链表“ int index = (tab.length - 1) & hash; HashEntry<K,V> first = entryAt(tab,index); for (HashEntry<K,V> e = first;;) { // 如果”HashEntry链表中的当前HashEntry节点“不为null, if (e != null) { K k; // 当”要插入的key-value键值对“已经存在于”HashEntry链表中“时,先保存原有的值。 // 若”onlyIfAbsent“为true,即”要插入的key不存在时才插入”,则直接退出; // 否则,用新的value值覆盖原有的原有的值。 if ((k = e.key) == key || (e.hash == hash && key.equals(k))) { oldValue = e.value; if (!onlyIfAbsent) { e.value = value; ++modCount; } break; } e = e.next; } else { // 如果node非空,则将first设置为“node的下一个节点”。 // 否则,新建HashEntry链表 if (node != null) node.setNext(first); else node = new HashEntry<K,V>(hash,key,value,first); int c = count + 1; // 如果添加key-value键值对之后,Segment中的元素超过阈值(并且,HashEntry数组的长度没超过限制),则rehash; // 否则,直接添加key-value键值对。 if (c > threshold && tab.length < MAXIMUM_CAPACITY) rehash(node); else setEntryAt(tab,index,node); ++modCount; count = c; oldValue = null; break; } } } finally { // 释放锁 unlock(); } return oldValue; } ? 说明: ? 2、之前是分段锁的思想,通过采用分段锁Segment减少热点域来提高并发效率。1.8之后利用CAS+Synchronized来保证并发更新的安全,底层采用数组+链表+红黑树的存储结构。
这里来看看ConcurrentHashMap的put源码: public V put(K key,V value) { ?ConcurrentHashMap 是设计为非阻塞的。就是一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。 在更新时会局部锁住某部分数据,但不会把整个表都锁住。同步读取操作则是完全非阻塞的。好处是在保证合理的同步前提下,效率很高。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |