concurrentHashMap 使用的是分段式锁(分段式锁就是Segment)
在迭代的过程中,ConcurrentHashMap仅仅锁定map的某个部分,而Hashtable则会锁定整个map。
HashTable的迭代器是强一致性的,而ConcurrentHashMap是弱一致的。 ConcurrentHashMap的get,clear,iterator 都是弱一致性的 如果要求强一致性,那么必须使用Collections.synchronizedMap()方法。
也就是说API上保证get操作一定能看到已完成的put操作。已完成的put操作肯定在get读取count之前对count做了写入操作。因此,也就是我们第一个轨迹分析的情况。
jdk1.7
ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组
JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,
并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap,
虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本
put的流程现在已经分析完了,你可以从中发现,他在并发处理中使用的是乐观锁,当有冲突的时候才进行并发处理,而且流程步骤很清晰,但是细节设计的很复杂,毕竟多线程的场景也复杂。
其实可以看出JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树,相对而言,总结如下思考:
JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)
JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了
JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档
JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock,我觉得有以下几点:
因为粒度降低了,在相对而言的低粒度加锁方式,synchronized并不比ReentrantLock差,在粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了
JVM的开发团队从来都没有放弃synchronized,而且基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然
在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存,虽然不是瓶颈,但是也是一个选择依据
锁的分类:
可重入锁(ReenTrantLock) 同一个线程可以多次拿到这个锁, 每拿到一次 state +1; 释放锁的时候 state=0;
公平锁 : 所有的线程按照先来先得的规则,获取线程
非公平锁 : 当a线程释放锁,处于等待状态的b现在 还没有唤醒,c线程刚好进来抢先获取到了锁
乐观锁: 每次默认没有并发,当有并发情况突发时,再采取并发措施
悲观锁: 每次默认都会有并发,每次处理时都会采取并发措施
Node: 字节 包含本身存储的数据 和 下一个节点的数据 (链表结构)
entry: key-value 的形式存储数据,map的数据类型就是一个数组里面包含多个entry,entry的key和value 根据一定的算法得到的hash值作为数组的索引
CAS: 原子操作; 可以是一个步骤也可以是多个步骤,其顺序不能被打乱,不会有并发.
CopyOnWriteArrayList 高效率的读取,通常 读写是互斥的, 读的时候写要等待 写的时候读要等待, 但是这个类的读不需要等待,写的时候是copy一份副本,写完之后将副本代替原来的数据,所以读的效率很高,不需要等待.
concurrentLinkedQueue 可以理解成最高效的List poll(),获取元素没有就返回null
BlockingQueue: 数据共享通道 poll(),获取元素没有就返回null take()获取元素没有的话线程等待,等待有数据了继续执行
ArrayBlockingQueue:固定长度存储数据
LinkedBlockingQueue:长度不固定的数据存储
synchronized 组合使用的 方法 waiter() notify() notifyAll()
在迭代的过程中,ConcurrentHashMap仅仅锁定map的某个部分,而Hashtable则会锁定整个map。
HashTable的迭代器是强一致性的,而ConcurrentHashMap是弱一致的。 ConcurrentHashMap的get,clear,iterator 都是弱一致性的 如果要求强一致性,那么必须使用Collections.synchronizedMap()方法。
也就是说API上保证get操作一定能看到已完成的put操作。已完成的put操作肯定在get读取count之前对count做了写入操作。因此,也就是我们第一个轨迹分析的情况。
jdk1.7
ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组
JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,
并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap,
虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本
put的流程现在已经分析完了,你可以从中发现,他在并发处理中使用的是乐观锁,当有冲突的时候才进行并发处理,而且流程步骤很清晰,但是细节设计的很复杂,毕竟多线程的场景也复杂。
其实可以看出JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树,相对而言,总结如下思考:
JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)
JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了
JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档
JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock,我觉得有以下几点:
因为粒度降低了,在相对而言的低粒度加锁方式,synchronized并不比ReentrantLock差,在粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了
JVM的开发团队从来都没有放弃synchronized,而且基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然
在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存,虽然不是瓶颈,但是也是一个选择依据
锁的分类:
可重入锁(ReenTrantLock) 同一个线程可以多次拿到这个锁, 每拿到一次 state +1; 释放锁的时候 state=0;
公平锁 : 所有的线程按照先来先得的规则,获取线程
非公平锁 : 当a线程释放锁,处于等待状态的b现在 还没有唤醒,c线程刚好进来抢先获取到了锁
乐观锁: 每次默认没有并发,当有并发情况突发时,再采取并发措施
悲观锁: 每次默认都会有并发,每次处理时都会采取并发措施
Node: 字节 包含本身存储的数据 和 下一个节点的数据 (链表结构)
entry: key-value 的形式存储数据,map的数据类型就是一个数组里面包含多个entry,entry的key和value 根据一定的算法得到的hash值作为数组的索引
CAS: 原子操作; 可以是一个步骤也可以是多个步骤,其顺序不能被打乱,不会有并发.
CopyOnWriteArrayList 高效率的读取,通常 读写是互斥的, 读的时候写要等待 写的时候读要等待, 但是这个类的读不需要等待,写的时候是copy一份副本,写完之后将副本代替原来的数据,所以读的效率很高,不需要等待.
concurrentLinkedQueue 可以理解成最高效的List poll(),获取元素没有就返回null
BlockingQueue: 数据共享通道 poll(),获取元素没有就返回null take()获取元素没有的话线程等待,等待有数据了继续执行
ArrayBlockingQueue:固定长度存储数据
LinkedBlockingQueue:长度不固定的数据存储
synchronized 组合使用的 方法 waiter() notify() notifyAll()
ReentrantLock 组合使用 awaiter(),signal()
java.util.concurrent.atomic 包下面的所有的类的操作都是 原子性的 AtomicInteger 可以用来计数