Java并发 -- Lock
管程
- 并发领域的两大核心问题:互斥 + 同步
- 互斥:同一时刻只允许一个线程访问共享资源
- 同步:线程之间的通信和协作
- JUC通过Lock和Condition两个接口实现管程,其中Lock用于解决互斥问题,而Condition用于解决同步问题
再造管程的理由
- Java语言对管程的原生实现:synchronized
- 在Java 1.5中,synchronized的性能不如JUC中的Lock,在Java 1.6中,synchronized做了很多的性能优化
- 再造管程的核心理由:synchronized无法破坏不可抢占条件(死锁的条件之一)
- synchronized在申请资源的时候,如果申请不到,线程直接进入阻塞状态,也不会释放线程已经占有的资源
- 更合理的情况:占用部分资源的线程如果进一步申请其它资源的时,如果申请不到,可以主动释放它所占有的资源
- 解决方案
- 能够响应中断
- synchronized:持有锁A的线程在尝试获取锁B失败,进入阻塞状态,如果发生死锁,将没有机会唤醒阻塞线程
- 如果处于阻塞状态的线程能够响应中断信号,那阻塞线程就有机会释放曾经持有的锁A
- 支持超时
- 如果线程在一段时间内没有获得锁,不是进入阻塞状态,而是返回一个错误
- 那么该线程也有机会释放曾经持有的锁
- 非阻塞地获取锁
- 如果尝试获取锁失败,不是进入阻塞状态,而是直接返回,那么该线程也有机会释放曾经持有的锁
- 能够响应中断
1 | // java.util.concurrent.locks.Lock接口 |
保证可见性
1 | public class Counter { |
1 | // ReentrantLock的伪代码 |
- Java多线程的可见性是通过Happens-Before规则来保证的
- synchronized的可见性保证:synchronized的解锁Happens-Before于后续对这个锁的加锁
- JUC中Lock的可见性保证:_利用了volatile相关的Happens-Before规则_
- ReentrantLock内部持有一个volatile的成员变量state,加锁和解锁时都会读写state
- 执行value++之前,执行lock,会读写volatile变量state
- 执行value++之后,执行unlock,会读写volatile变量state
- 相关的Happens-Before规则
- 顺序性规则
- 对于线程T1,
value++Happens-Beforeunlock() - 对于线程T2,
lock()Happens-Before读取value
- 对于线程T1,
- volatile变量规则
- 对于线程T1,unlock()会执行
state=1 - 对于线程T2,lock()会先读取state
- volatile变量的写操作 Happens-Before volatile变量的读操作
- 因此线程T1的unlock Happens-Before 线程T2的lock,与synchronized非常类似
- 对于线程T1,unlock()会执行
- 传递性规则:线程T1的value++ Happens-Before 线程T2的lock()
- 顺序性规则
可重入锁
1 | public class X { |
- 可重入锁:线程可以_重复获取同一把锁_
- 执行路径:addOne -> get,在执行到2时,如果锁是可重入的,那么线程会再次加锁成功,否则会被阻塞
公平锁和非公平锁
1 | // java.util.concurrent.locks.ReentrantLock |
- 在管程模型中,每把锁都对应着一个_入口等待队列_
- 如果一个线程没有获得锁,就会进入入口等待队列,当有线程释放锁的时候,需要从入口等待队列中唤醒一个等待的线程
- 唤醒策略:如果是公平锁,唤醒等待时间最长的线程,如果是非公平锁,随机唤醒
锁的最佳实践
- 永远只在更新对象的成员变量时加锁
- 永远只在访问可变的成员变量时加锁
- 永远不在调用其它对象的方法时加锁,因为调用其它对象的方法是不安全的(对其它对象的方法不了解)
- 可能有Thread.sleep(),也有可能有慢IO,这会严重影响性能
- 甚至还会加锁,这有可能导致死锁
- 减少锁的持有时间
- 减少锁粒度
参考资料
All articles on this blog are licensed under CC BY-NC-SA 4.0 unless otherwise stated.










