1. 锁的设计思路
1.1 独享锁、共享锁
独享锁:该锁一次只能被一个线程持有
共享锁:该锁可以被多个线程持有
应用:
synchronize、ReetrantLock、WriteLock是独享锁ReadLock是共享锁
1.2 乐观锁、悲观锁
乐观锁:
- 认为读多写少,并发写的概率低。读取数据时不加锁,更新数据时取出当前版本号和上一次数据的版本号对比,如果一致则成功更新,不一致则失败重试(重新读取、对比、写入)
- 应用:Java中的CAS,传入期待值和更新值,如果当前值和期待值一致,则成功更新,不一致则重新读取、对比、更新
悲观锁:
- 认为读少写多,并发写的概率高。读写数据时都会加锁,得到锁的线程才能执行,得不到锁的线程阻塞等待
- 应用:Java中的
synchronize、基于AQS的ReetrantLock(先CAS尝试获取锁,获取不到再升级为悲观锁)
2. 锁的实现
2.1 自旋锁、自适应自旋锁
自旋锁:
- 线程获取不到锁时执行一段无意义的空转循环,不立刻挂起,如果持有锁的线程很快释放,则不需要切换和阻塞就可以获得锁
- 缺点:如果持有锁的线程占用时间太长,则会浪费CPU资源
自适应自旋锁:自旋锁可以通过设置自旋时间或自旋次数优化。JDK1.6中可以设置自旋次数。JDK1.7后不固定自旋次数,由JVM控制,自旋次数由上一个获得锁的线程的自旋时间和持有者锁的状态决定
2.2 阻塞锁、公平锁、非公平锁
阻塞锁:只有获得锁的线程能够执行,竞争锁失败的线程会进入阻塞状态,进入等待队列
公平锁:获取锁按照先到先得的顺序,从等待队列中按序取出,新来的线程进入队尾
非公平锁:每次释放锁时,全部线程都会同时开始竞争锁,可能出现后到的线程先获得锁的情况
3. 锁优化
3.1 锁粗化
问题:对同一个锁不停请求、同步和释放,造成资源浪费
解决:将多个连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成一个范围更大的锁
3.2 锁消除
问题:对于没有线程安全问题的对象,加锁是不必要的,造成性能浪费
解决:编译时通过代码逃逸技术,判断一段代码中堆上的数据不会逃逸出当前线程,则认为这段代码是安全的,消除无用锁
3.3 锁分离
互不影响的操作可以根据功能分离,如读写锁,实现读读不互斥、读写互斥、写写互斥
3.4 锁升级(JDK1.6后Synchronize的优化)
对象头MarkWord存储结构
3.4.1 重量级锁
加锁:线程尝试获取锁,得到则占有锁,其他未竞争到锁的线程进入阻塞状态等待
实现原理:通过对象内部的Monitor实现,依赖于操作系统的MutexLock
缺点:线程从运行到阻塞等待,需要切换用户态和内核态,线程切换成本高
3.4.2 轻量级锁
加锁:线程CAS尝试获取锁,得到则修改对象头和方法栈帧中的轻量锁指针,其他线程自旋等待,如果长时间未得到锁就升级为重量级锁
解锁:CAS解锁,将Displace Mark Word替换回到对象头,如果替换成功则代表无竞争,解锁成功;如果替换失败则说明有竞争,锁升级为重量级锁,此时解锁的同时唤醒被阻塞的线程,之后按照重量级锁规则重新竞争
实现原理:通过对象头中Mark Word的锁记录指针和锁标志位实现
缺点:在没有多线程竞争的场景可用,在多线程竞争场景如果代码块执行缓慢,则会在线程切换成本上增加CAS自旋成本,性能比重量级锁更差
3.4.3 偏向锁
加锁:线程CAS尝试获取锁,得到锁时在对象头和栈帧中的锁记录里存储偏向锁的线程ID,之后该线程再进入和退出该同步代码块时不需要CAS,只用检查对象头中的偏向锁线程ID即可。如果线程ID与当前线程一致,则代表已有偏向锁,继续执行;如果线程ID不一致,则再检查当前锁是否为偏向锁,如果不是偏向锁则代表是更高级的锁,升级使用CAS竞争轻量级锁,如果是偏向锁则尝试CAS将偏向锁线程ID改为当前线程
解锁:只有当其他线程竞争时才释放锁。释放时,首先暂停拥有偏向锁的线程,再检查线程是否存活,如果存活则继续执行,重置对象头和栈帧的锁记录;如果不存活则将对象头设置为无锁状态
优点:全程只需要在获取锁时执行一次CAS
缺点:一旦出现多线程竞争的情况就需要撤销偏向锁,撤销偏向锁的性能成本必须小于节省的CAS性能成本
对比轻量级锁:轻量级锁是为了在线程交替执行同步代码块时提高性能,偏向锁是只有一个线程执行同步代码块时提高性能
3.4.4 对比
| 锁 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 偏向锁 | 加锁和解锁不需要额外的消耗,只使用一次CAS | 如果线程间存在锁竞争,会有额外的锁撤销的消耗 | 只有一个线程访问同步块 |
| 轻量级锁 | 线程不阻塞,减少线程切换成本,提高性能 | 如果始终得不到锁竞争的线程,自旋会消耗CPU,锁升级的开销大于重量级锁 | 追求响应时间,同步块执行时间短 |
| 重量级锁 | 线程竞争不自旋,不消耗CPU | 线程阻塞,切换线程成本高 | 线程竞争激烈,同步块执行时间较长 |