快捷搜索:  汽车  科技

javastream线程安全问题(Java集合系列-ConcurrentHashMap-线程安全的全面解析)

javastream线程安全问题(Java集合系列-ConcurrentHashMap-线程安全的全面解析)1:任何对volatile变量的写,都会立即从工作内存中刷新到主存中去。 2:任何对volatile变量的读,都会从主存中读取一份最新的数据。 大家目前只要理解上面的两段话就可以了,至于volatile的详细讲解,我会单独有一篇文章去分析,这牵涉到JMM(Java内存模型)的知识点,我们暂且放一放,你就理解成只要一个变量被volatile修饰,一个线程的修改,立刻会对其他线程可见,只是它不保证原子性特点。1:所有的线程通过CAS机制竞争更改sizeCtrl=-1.更改成功者初始化容器 2:只能有一个线程进行容器的初始化工作 3:其他线程检测到sizeCtrl=-1了,说明已经有线程正在初始化了,当前线程需要让出CPU 所以从上面的总结可以看出,ConcurrentHashMap是利用CAS机制修改sizeCtrl来保证只有一个线程才能初始化容器,而sizeCtrl是用volatile关键字

本人是工作7年的老程序员,在头条分享我对Java运用和源码、各种框架运用和源码的认识和理解,如果对您有所帮助,请持续关注。

声明:所有的文章都是自己工作之余一个字一个字码上去的,希望对学习Java的同学有所帮助,如果有理解不到位的地方,欢迎交流。

上一篇文章我对ConcurrentHashMap的扩容机制做了详细的说明,本篇文章来讲解第二个非常重要的内容,那就是ConcurrentHashMap是怎样保证多线程安全的。

本篇文章的主要内容如下:

1:CAS在ConcurrentHashMap中的运用 2:ConcurrentHashMap在初始化容器时怎样保证安全的 3:ConcurrentHashMap在添加元素时怎样保证线程安全的 4:ConcurrentHashMap在获取元素时怎样保证线程安全的 一、CAS在ConcurrentHashMap中的运用

首先看一下如下代码:

static final <K V> Node<K V> tabAt(Node<K V>[] tab int i) { return (Node<K V>)U.getObjectVolatile(tab ((long)i << ASHIFT) ABASE); } static final <K V> boolean casTabAt(Node<K V>[] tab int i Node<K V> c Node<K V> v) { return U.compareAndSwapObject(tab ((long)i << ASHIFT) ABASE c v); } static final <K V> void setTabAt(Node<K V>[] tab int i Node<K V> v) { U.putObjectVolatile(tab ((long)i << ASHIFT) ABASE v); }

上面的3个方法是保证不加锁的情况下,保证原子操作。

二、ConcurrentHashMap在初始化容器时怎样保证线程安全的:利用CAS机制保证线程安全的

我在讲解put方法时,已经说过了初始化容器方法initTab() 我总结如下:

1:所有的线程通过CAS机制竞争更改sizeCtrl=-1.更改成功者初始化容器 2:只能有一个线程进行容器的初始化工作 3:其他线程检测到sizeCtrl=-1了,说明已经有线程正在初始化了,当前线程需要让出CPU

所以从上面的总结可以看出,ConcurrentHashMap是利用CAS机制修改sizeCtrl来保证只有一个线程才能初始化容器,而sizeCtrl是用volatile关键字修饰的。我们回忆一下volatile的特点。

1:任何对volatile变量的写,都会立即从工作内存中刷新到主存中去。 2:任何对volatile变量的读,都会从主存中读取一份最新的数据。

大家目前只要理解上面的两段话就可以了,至于volatile的详细讲解,我会单独有一篇文章去分析,这牵涉到JMM(Java内存模型)的知识点,我们暂且放一放,你就理解成只要一个变量被volatile修饰,一个线程的修改,立刻会对其他线程可见,只是它不保证原子性特点。

initTab初始化底层容器的流程

总结一句话:初始化底层容器时利用CAS保证线程安全的

三、ConcurrentHashMap添加元素时怎样保证线程安全的:利用CAS synchronized实现的

1:ConcurrentHashMap使用乐观锁,只有冲突时才加锁并发处理,如果没有冲突,则利用CAS机制原子操作. 2:ConcurrentHashMap使用锁分离的方法,每一次加锁不是锁住整个数组,而只是锁住一个下标的数据。 说的大白话一点就是: 1:如果计算出的下标还没有值,则直接把(key value)封装的Node节点利用CAS机制存放在这个下标。 2:如果计算的下标已经有值了,并且不是在扩容期间,那说明就是向链表或者红黑树添加数据,此时利用synchronized进行加锁,而锁对象就是此下标第一个元素。

从上面的总结可以看出,ConcurrentHashMap并不是把底层的整个表都锁住,如果计算出的数组下标没有值,则利用CAS的机制保证线程的安全,如果此下标已经有值了,则利用synchronized锁进行保证线程的安全,而此时锁住的只是此下标,数组的其他下标并没有加锁,所以能够多个线程同时添加元素,只要多个线程计算的下标不同就可以并发添加,这大大的提高了元素添加的性能。

举一个例子:此时数组下标0没有值,下标3是一个链表,下标5只有一个值,此时线程1计算的下标为0,线程2和线程3计算的下标为3,线程4计算的下标为5 4个线程同时调用put方法。那么怎样保证线程安全的呢?

1:线程1计算的下标0,此下标没有值,则利用CAS直接添加 2:线程2和线程3计算的下标相同,都是3,锁对象就是tab[3] 那么两个线程谁获取了锁,谁就添加元素,没有获取到锁的则阻塞,直到获取锁。 3:线程4计算的下标5,锁对象就是tab[5]。那么它获取的锁和线程2/3不是同一个锁,线程4不会因为线程2获取了锁而阻塞。

所以从上面可知,不同的下标有不同的锁对象,一个下标被锁住了,不影响其他下标插入元素。

javastream线程安全问题(Java集合系列-ConcurrentHashMap-线程安全的全面解析)(1)

所以put的流程图如下:

javastream线程安全问题(Java集合系列-ConcurrentHashMap-线程安全的全面解析)(2)

四、ConcurrentHashMap在获取元素时怎样保证线程安全的:无锁化,关键字volatile

大家回过头来首先看一下底层数组和节点Node是怎样定义的:

//被volatile修饰 transient volatile Node<K V>[] table; ------------------------------------------------------------------------------------------------- //val和next被volatile修饰 static class Node<K V> implements Map.Entry<K V> { final int hash; final K key; volatile V val; volatile Node<K V> next; }

volatile修饰数组或者对象时,只是表示数组的每一个对象的地址具有可见性,一个线程修改,其他线程立刻可见,但是它并不保证对象中的成员变量具有可见性,所以在Node定义时,val和next都被volatile修饰,一个线程修改,其他线程立刻可见。所以在ConcurrentHashMap获取元素时并没有加锁,而是利用volatile修饰底层数组和Node的val和next保证线程安全的。

对ConcurrentHashMap线程安全总结如下:

1:在ConcurrentHashMap对元素修改时(增删改):利用CAS和锁分离原理保证线程安全的,只有在有冲突时才进行加锁。而加锁并不是锁住整个数组,而是只锁住指定下标的数据。 2:在获取元素时,并没有加锁,利用关键字volatile。

ConcurrentHashMap保证线程安全总结一句话:volatile CAS synchronized(锁分离技术)

从上面看出Doug Lea大神在高并发的功底令人膜拜,接下来一篇文章,我会介绍ConcurrentHashMap的最后一个知识点:计数,也就是size是怎样计算出来的呢?可能有人非常的疑惑,这不是非常的简单吗?其实在高并发下,size的计算并不是那么的容易,虽然有AtomicLong这种原子计数器,大家想一想在大并发下,CAS失败率非常的大,总是失败后在重试,效率不太高,所以ConcurrentHashMap并没有使用这个。而是使用和LongAdder机制完全一样的原理,下一篇文章我就详细讲解LongAdder,请持续关注。

猜您喜欢: