Java并发(二):锁

106 阅读5分钟

1. 锁的设计思路

1.1 独享锁、共享锁

独享锁:该锁一次只能被一个线程持有

共享锁:该锁可以被多个线程持有

应用:

  • synchronizeReetrantLockWriteLock是独享锁
  • 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存储结构

16270c8c27c4815e~tplv-t2oaga2asx-jj-mark_3024_0_0_0_q75.png

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线程阻塞,切换线程成本高线程竞争激烈,同步块执行时间较长