加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 编程开发 > Java > 正文

为什么juc下的集合类是线程安全的

发布时间:2020-12-15 02:20:40 所属栏目:Java 来源:网络整理
导读:1. JUC 简介 在 Java 5.0 提供了? java.util.concurrent (简称JUC)包,在此包中增加了在并发编程中很常用的工具类,用于定义类似于线程的自定义子系统,包括线程池,异步 IO 和轻量级任务框架;还提供了设计用于多线程上下文中的 Collection 实现等。 2.并发容器

1. JUC 简介

  在 Java 5.0 提供了?java.util.concurrent(简称JUC)包,在此包中增加了在并发编程中很常用的工具类,用于定义类似于线程的自定义子系统,包括线程池,异步 IO 和轻量级任务框架;还提供了设计用于多线程上下文中的 Collection 实现等。

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是支持高并发的。
(02) CopyOnWriteArraySet相当于线程安全的HashSet,它继承于AbstractSet类。CopyOnWriteArraySet内部包含一个CopyOnWriteArrayList对象,它是通过CopyOnWriteArrayList实现的,CopyOnWriteArraySet操作原理与CopyOnWriteArrayList类似,只是Set是无序不可重复,List是有序可重复。

CopyOnWriteArrayList。

1. CopyOnWriteArrayList实现了List接口,因此它是一个队列。

2. CopyOnWriteArrayList包含了成员lock。每一个CopyOnWriteArrayList都和一个互斥锁lock绑定,通过lock,实现了对CopyOnWriteArrayList的互斥访问。
3. CopyOnWriteArrayList包含了成员array数组,这说明CopyOnWriteArrayList本质上通过数组实现的。
下面从“动态数组”和“线程安全”两个方面进一步对CopyOnWriteArrayList的原理进行说明。
1.?CopyOnWriteArrayList的“动态数组”机制?--?它内部有个“volatile数组”(array)来保持数据。在“添加/修改/删除”数据时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给“volatile数组”。这就是它叫做CopyOnWriteArrayList的原因!CopyOnWriteArrayList就是通过这种方式实现的动态数组;不过正由于它在“添加/修改/删除”数据时,都会新建数组,所以涉及到修改数据的操作,CopyOnWriteArrayList效率很低;但是单单只是进行遍历查找的话,效率比较高。

2.?CopyOnWriteArrayList的“线程安全”机制?--?是通过volatile和互斥锁来实现的。(01) CopyOnWriteArrayList是通过“volatile数组”来保存数据的。一个线程读取volatile数组时,总能看到其它线程对该volatile变量最后的写入;就这样,通过volatile提供了“读取到的数据总是最新的”这个机制的保证。
(02) CopyOnWriteArrayList通过互斥锁来保护数据。在“添加/修改/删除”数据时,会先“获取互斥锁”,再修改完毕之后,先将数据更新到“volatile数组”中,然后再“释放互斥锁”;这样,就达到了保护数据的目的。?

我们来看看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是通过“锁分段”来实现的,它支持并发。
(02) ConcurrentSkipListMap是线程安全的有序的哈希表(相当于线程安全的TreeMap); 它继承于AbstractMap类,并且实现ConcurrentNavigableMap接口。ConcurrentSkipListMap是通过“跳表”来实现的,它支持并发。
(03) ConcurrentSkipListSet是线程安全的有序的集合(相当于线程安全的TreeSet);它继承于AbstractSet,并实现了NavigableSet接口。ConcurrentSkipListSet是通过ConcurrentSkipListMap实现的,它也支持并发。

ConcurrentHashMap。

1、ConcurrentHashMap是线程安全的哈希表,它是通过“锁分段”来实现的。ConcurrentHashMap中包括了“Segment(锁分段)数组”,每个Segment就是一个哈希表,而且也是可重入的互斥锁。第一,Segment是哈希表表现在,Segment包含了“HashEntry数组”,而“HashEntry数组”中的每一个HashEntry元素是一个单向链表。即Segment是通过链式哈希表。第二,Segment是可重入的互斥锁表现在,Segment继承于ReentrantLock,而ReentrantLock就是可重入的互斥锁。
对于ConcurrentHashMap的添加,删除操作,在操作开始前,线程都会获取Segment的互斥锁;操作完毕之后,才会释放。而对于读取操作,它是通过volatile去实现的,HashEntry数组是volatile类型的,而volatile能保证“即对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入”,即我们总能读到其它线程写入HashEntry之后的值。 以上这些方式,就是ConcurrentHashMap线程安全的实现原理。

(01) put()根据key获取对应的哈希值,再根据哈希值找到对应的Segment片段。如果Segment片段不存在,则新增一个Segment。
(02) 将key-value键值对添加到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;
}

?

说明:
put()的作用是将key-value键值对插入到“当前Segment对应的HashEntry中”,在插入前它会获取Segment对应的互斥锁,插入后会释放锁。具体的插入过程如下:
(01) 首先根据“hash值”获取“当前Segment的HashEntry数组对象”中的“HashEntry节点”,每个HashEntry节点都是一个单向链表。
(02) 接着,遍历HashEntry链表。
? ? ? ?若在遍历HashEntry链表时,找到与“要key-value键值对”对应的节点,即“要插入的key-value键值对”的key已经存在于HashEntry链表中。则根据onlyIfAbsent进行判断,若onlyIfAbsent为true,即“当要插入的key不存在时才插入”,则不进行插入,直接返回;否则,用新的value值覆盖原始的value值,然后再返回。
? ? ? ?若在遍历HashEntry链表时,没有找到与“要key-value键值对”对应的节点。当node!=null时,即在scanAndLockForPut()获取锁时,已经新建了key-value对应的HashEntry节点,则”将HashEntry添加到Segment中“;否则,新建key-value对应的HashEntry节点,然后再“将HashEntry添加到Segment中”。 在”将HashEntry添加到Segment中“前,会判断是否需要rehash。如果在添加key-value键值之后,容量会超过阈值,并且HashEntry数组的长度没有超过限制,则进行rehash;否则,直接通过setEntryAt()将key-value键值对添加到Segment中。

?

2、之前是分段锁的思想,通过采用分段锁Segment减少热点域来提高并发效率。1.8之后利用CAS+Synchronized来保证并发更新的安全,底层采用数组+链表+红黑树的存储结构。

    • CAS(Compare-And-Swap) 算法是硬件对于并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于
      管理对共享数据的并发访问;
    • CAS 是一种无锁的非阻塞算法的实现;
    • CAS 包含了三个操作数:
      • 需要读写的内存值: V
      • 进行比较的预估值: A
      • 拟写入的更新值: B
      • 当且仅当 V == A 时,V = B,否则,将不做任何操作;

这里来看看ConcurrentHashMap的put源码:

 
 

public V put(K key,V value) {
return putVal(key,false);
}

final V putVal(K key,boolean onlyIfAbsent) {
//和hashMap不同的是,concurrentHashMap的key和value都不允许为null
//concurrenthashmap它们是用于多线程的,并发的 ,如果map.get(key)得到了null,
// 不能判断到底是映射的value是null,还是因为没有找到对应的key而为空,
// 而用于单线程状态的hashmap却可以用containKey(key) 去判断到底是否包含了这个null。
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n,i,fh;
//如果是第一次put,进行初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//根据(tab.length - 1) & hash 计算目标节点在数组中的位置
else if ((f = tabAt(tab,i = (n - 1) & hash)) == null) {
//如果为空,则通过cas 添加一个新建一个头节点
if (casTabAt(tab,null,
new Node<K,V>(hash,null)))
break; // no lock when adding to empty bin
}
//hash为-1 说明是一个forwarding nodes节点,表明正在扩容
else if ((fh = f.hash) == MOVED)
//帮助扩容
tab = helpTransfer(tab,f);
else {
V oldVal = null;
//对上面计算出来的节点进行加锁
synchronized?(f) {
//这里判断下有没有线程对数组进行了修改
if (tabAt(tab,i) == f) {
//这里如果hash值是大于等于0的说明是链表
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
//如果找到了目标节点,那么进行值替换
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
//如果循环到链表结尾还没发现,那么进行插入操作
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,
value,null);
break;
}
}
}
//如果是树
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
//如果链表数量大于TREEIFY_THRESHOLD(8),开始执行转换树
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab,i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//进行元素数量统计,和决定是否扩容
addCount(1L,binCount);
return null;
}

?ConcurrentHashMap 是设计为非阻塞的。就是一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。

在更新时会局部锁住某部分数据,但不会把整个表都锁住。同步读取操作则是完全非阻塞的。好处是在保证合理的同步前提下,效率很高。

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读