ReentrantLock
:是可重入的独占锁,所以在同一时刻只有一个线程可以获取到该锁,其他想要获取该锁的线程会被阻塞,放入到该锁的AQS阻塞队列。
AQS
:队列同步器(AbstractQueuedSynchronizer)是用来构建锁和其他同步组件的基础框架,他使用一个int成员变量表示同步状态,通过内置的FIFO来完成资源获取线程的排队工作。同步器使用的主要方法是继承,子类通过继承同步器并实现他的抽象方法来管理状态,在此过程中避免不了状态的改变:getState(),setState(int newState),和compareAndSetState(int expect,int update)
Segment
:ConcurrentHashMap为了提高本身的并发能力,在内部采用了一个叫做Segment的结构,一个Segment其实就是一个类Hash Table的结构,Segment内部维护了一个链表数组
jdk7:
数据结构
:ReentrantLock+Segment+Hash Entry,一个Segment中包含一个HashEntry数组,每个HashEntry又是一个链表结构
元素查询
: 二次 hash,第一次 Hash定位到Segment,第二次定位到元素所在的链表的头部
锁
:Segment分段锁,Segment继承了ReentrantLock,锁定操作的Segment,其他的Segment不受影响,并发度为Segment个数,可以通过构造函数指定,数组扩容不会影响其他的Segment。get方法无需加锁,volatile保证
jdk8:
数据结构
:synchronized+CAS+Node+红黑树,Node的val和next都用volatile修饰,保证可见性查找,替换,赋值操作都使用CAS
锁
:锁链表的head节点,不影响.其他元素的读写,锁粒度更细,效率高,扩容时,阻塞所有的读写操作、并非扩容
读操作无锁:Node的val和next使用volatile修饰,读写程序对该变量互相可见.数组用volatile修饰,保证扩容时被读线程感知
为什么自动化加锁释放会到导致效率低下?
因为在多线程环境下当你锁住map,进行put的操作时,其他想要读写的线程都会被阻塞,所以不推荐使用HashTable(全表锁);HashTable之所以性能插是因为每个方法都加上了synchronized。相当于一把锁,锁住了整个数组资源,而ConcurrentHashMap用到了分段锁,每把锁只锁数组中的一段内容。
在实际的应用中,散列表一般的应用场景是:除了少数插入操作和删除操作外,绝大多数都是读取操作,而且读操作在大多数时候都是成功的。正是基于这个前提,ConcurrentHashMap针对读操作做了大量的优化。通过 HashEntry 对象的不变性和用 volatile 型变量协调线程间的内存可见性,使得 大多数时候,读操作不需要加锁就可以正确获得值。这个特性使得 ConcurrentHashMap 的并发性能在分离锁的基础上又有了近一步的提高。
ConcurrentHashMap 是一个并发散列映射表的实现,它允许完全并发的读取,并且支持给定数量的并发更新。相比于 HashTable 和用同步包装器包装的 HashMap(Collections.synchronizedMap(new HashMap())),ConcurrentHashMap 拥有更高的并发性。在 HashTable 和由同步包装器包装的 HashMap中,使用一个全局的锁来同步不同线程间的并发访问。同一时间点,只能有一个线程持有锁,也就是说在同一时间点,只能有一个线程能访问容器。这虽然保证多线程间的安全并发访问,但同时也导致对容器的访问变成串行化的了。
ConcurrentHashMap 的高并发性主要来自于三个方面:
- 用分离锁实现多个线程间的更深层次的共享访问。
- 用 HashEntery 对象的不变性来降低执行读操作的线程在遍历链表期间对加锁的需求。
- 通过对同一个 Volatile 变量的写 / 读访问,协调不同线程间读 / 写操作的内存可见性。
使用分离锁,减小了请求 同一个锁的频率。
通过 HashEntery 对象的不变性及对同一个 Volatile 变量的读 / 写来协调内存可见性,使得 读操作大多数时候不需要加锁就能成功获取到需要的值。由于散列映射表在实际应用中大多数操作都是成功的 读操作,所以 2 和 3既可以减少请求同一个锁的频率,也可以有效减少持有锁的时间。通过减小请求同一个锁的频率和尽量减少持有锁的时间 ,使得ConcurrentHashMap 的并发性相对于 HashTable 和用同步包装器包装的 HashMap有了质的提高。