JDK1.7的分段锁
数据结构:数组(大数组 Segment 和小数组 HashEntry)+链表
将整个哈希表划分为多个 Segment(段),每个 Segment 是一个独立的哈希表(类似 HashTable),默认16个Segment。
访问某个键值对时,先根据哈希值高位确定对应的 Segment,再对该 Segment 加锁。其他 Segment 不受影响,允许并发操作。
JDK1.8的volatile+CAS + synchronized
数据结构:数组+链表+红黑数
与 HashMap 类似,链表长度超过阈值(默认 8)转红黑树,低于阈值(默认 6)转回链表
添加元素putVal时,判断容器是否为空
1.空:数组尚未初始化,用volatile 修饰的sizeCtl变量作为初始化状态标志,通过加 CAS 操作修改sizeCtl的值,只有第一个将sizeCtl从0改为-1的线程能执行初始化,其他线程让出CPU。
if (tab == null || (n = tab.length) == 0)
tab = initTable(); // 初始化数组
U.compareAndSwapInt(this, SIZECTL, sc, -1)
2.不为空:则根据存储的元素计算目标桶是否为空。
2.1桶为空:则利用 CAS 设置该节点,tabAt()保证读取最新值,casTabAt()原子性插入新节点。
if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break;
}
2.2不为空:已有链表或红黑树,则使用 synchronized对桶头节点加锁,然后,遍历桶中的数据,插入新节点或更新现有节点,最后判断是否需要转为红黑树。
volatile:
保证了变量的可见性,即一个线程修改了volatile变量,其他线程能立即看到变化
CAS:
无锁算法,当初始化或者插入节点时,通过CAS来确保只有一个线程能成功修改状态,比如初始化数组或者在某个桶中插入第一个节点。如果多个线程同时尝试,CAS会保证只有一个成功,其他线程则重试或采取其他策略。
synchronized:
用于同步代码块,确保同一时间只有一个线程执行该代码块。当桶中已经有节点时,ConcurrentHashMap会使用synchronized锁定该桶的头节点,然后进行链表或红黑树的插入操作。这样可以避免多个线程同时修改同一个桶,保证数据一致性。
可重入锁
同一个线程可以多次获取同一把锁,而不会导致死锁。即同一个线程可以重复进入被自己持有的锁保护的代码区域。
想象一个场景:
- 你有一个保险箱(共享资源),你(线程)用钥匙(锁)打开了它(获取锁)。
- 在保险箱里,你发现一张纸条写着:“打开第二个抽屉需要同一把钥匙”。
- 可重入锁:你直接用已有的钥匙打开第二个抽屉(无需重新申请钥匙)。
- 不可重入锁:你被要求归还钥匙后才能再次申请,但钥匙在你手里,导致死锁(自己卡住自己)。
可重入锁解决了 “线程因重复获取自己已持有的锁而阻塞自己” 的问题。
ReentrantLock和synchronized都是可重入的。
ReentrantLock:
显示可重入互斥锁,可中断锁获取、公平锁策略、条件变量(Condition)等
特性 说明 可重入性 同一线程可重复获取锁(通过计数器实现,避免自死锁) 公平性可选 支持公平锁(先申请先获取)和非公平锁(允许插队,默认策略) 可中断锁获取 线程在等待锁时可响应中断( lockInterruptibly()
)超时尝试获取锁 支持设定超时时间尝试获取锁( tryLock(long timeout, TimeUnit unit)
)条件变量 通过 newCondition()
创建多个等待队列,实现精细的线程唤醒控制
公平锁和非公平锁
公平锁:
按照线程请求锁的顺序来分配锁,也就是先到先得
非公平锁:
允许插队,后来的线程有可能先获取到锁
对比维度 | 公平锁 | 非公平锁 |
---|---|---|
公平性 | 严格按请求顺序分配锁 | 允许插队,新线程可能优先获得锁 |
吞吐量 | 较低(维护队列需要额外开销) | 较高(减少线程切换,允许插队) |
饥饿问题 | 不会发生线程饥饿 | 可能发生线程饥饿(某些线程长期得不到锁) |
适用场景 | 需要严格顺序的场景(如支付系统、票务系统) | 大多数高并发场景(如缓存访问、计数器) |
实现复杂度 | 高(需维护队列) | 低(直接竞争) |
在业务允许的情况下,优先使用非公平锁以提升系统吞吐量。
悲观锁和乐观锁
悲观锁:先加锁,再操作
认为并发操作一定会发生冲突,因此每次访问数据时都会加锁,比如synchronized和ReentrantLock。
举个例子:出门时锁门(默认有小偷)
乐观锁:先操作,提交时再检查冲突
认为并发操作很少发生冲突,只在提交操作时检查是否冲突,比如CAS操作,数据库的乐观锁和Java中的Atomic类。
举个例子:
1.购物车结算时才检查库存(默认没人抢购)
2.或者在网上订票,系统显示还有1个座位,你点击预订,系统会先让你填写信息,然后提交的时候检查是否还有座位。如果有,预订成功;如果没有,提示你重新选择
正在学习中,如有错误,大家一定要指出来!!!