目录
4.2. 如何选择synchronized和reentrantlock
1. 目标
基于源码,简要介绍concurrent包的Lock以及主要实现类,对比synchronized关键字等。
2. JDK版本
JDK1.8
3. interface Lock
3.1. what
Lock是控制多线程访问共享资源的工具。通常其实现应该是排他的,即同一时刻只有一个线程可访问共享资源。然而,有些实现是允许并发访问共享资源的,例如ReadWriteLock。
3.2. 差异对比
与synchronized(方法和变量)相比,差异如下
- lock作为接口,具有多种实现,可以进行自定义
- 请求lock时是否允许中断/超时,依据具体实现的特性而定,如执行特性,顺序保证,或其他实现特性。
- 所有的特性都应该doc写明。
- lock的实现将提供更多的行为和语义
- 加锁行为:尝试加锁trylock、可中断加锁、超时加锁
- 其他可提供的:顺序保证、是否可重入、是否共享、死锁检查等
- 允许更加灵活的结构,例如具有不同的属性,具有多个关联的condition对象等
- 获取锁与释放锁的范围scope和顺序order差异
- synchronized提供访问[与对象关联的隐示监视器锁]的能力。
其要求锁的获取与释放必须发生在块结构中:
1当获取多个锁后,释放必须是反序的;
2获取和释放必须在同一个范围scope内; - synchronized的范围规则使得编程更简单,可以规避使用lock引起的错误。
但是这种范围规则的限制,正是lock发挥[提供更灵活的使用方式]机会。
例如在并发遍历访问数据时,需要一个一个或是链式的加锁:
1 lock a, lock b
2 unlock a, lock c
3 unlock b, lock d
Lock的实现之所以满足这种场景,是因为lock的获取与释放没有范围scope和顺序order限制。
- synchronized提供访问[与对象关联的隐示监视器锁]的能力。
synchronized会引起线程上下文切换的资源浪费关于这点我看了jvm的源码,发现objectMonitor内部实现用的也是 自旋+cas+park,所以无法证明 关键字比lock造成更多的线程上下文切换。- 使用方式差异
- synchronized用于方法声明或变量
synchronized void m1(){//todo}
synchronized(变量){//todo} - lock丢弃了结构化加锁,从而引起锁无法自动释放,所以需要进行unlock。
lock使用习惯:使用try-catch-finally
lock.lock();
try{
//TODO
}finally{
lock.unlock();
}
- synchronized用于方法声明或变量
- 实现方式差异
- synchronized是使用对象的隐示监视器锁
- lock需自行实现锁机制,基于AQS,unsafe.cas
4. ReentrantLock可重入排他锁
4.1. 比synchronized的优势
与synchronized具有相同行为和语义的重入排他锁,但是提供了更多的行为。
- lock 尝试一次获取锁,成功则返回,否则内部循环获取,且不会中断,因为中断被内部处理
- trylock 尝试一次获取锁,返回成功或失败
- lockInterruptibly 尝试一次获取锁,成功则返回,否则循环获取,且可中断,因为中断则抛出异常
- unlock
- getHoldCount 当前线程重入次数
- hasQueuedThread 参数线程是否在等待该锁
- hasQueuedThreads 是否有线程在等待锁
- isLocked 锁是否被占用
4.2. 如何选择synchronized和reentrantlock
- 从行为和语义多样性、使用灵活性、具体场景要求考虑
4.3. 参数true,所谓的公平锁
- 倾向于将锁分配给等待时间较长的线程,但是不保证确切的访问顺序。
- 多个线程访问公平锁时性能很低(与默认的非公平锁相比),具有很小的好处,即较少饥饿线程。
- 不保证线程调度的公平性
- 与其他没有执行的或没有成功获取锁的线程相比,已经获取锁的线程可多次重复获取;
- 不可预期的发生取消、中断、超时,先来的线程可能没拿到锁,但后来的线程可能拿到锁。
- trylock是忽略公平性参数的,只要锁是可获取的,他就会返回
4.4. 锁实现
- 继承AQS,且使用state来表示获取锁的重入次数;使用cas操作state
- acquire方法
-
tryacquire 成功则返回 循环获取锁
-
- trylock会立即尝试占用锁,如果成功则占用,否则立即返回,不会进入等待队列。
- 锁获取流程
- 非公平锁 NonfairSync
-
lock方法 //差异一 if CASstate(0,1) 表示获取锁 else acquire(1) tryacquire方法 //差异二 if state == 0 if CASstate(0,x) 获取锁 else if 当前线程==持有锁的线程 进行重入操作
-
- 公平锁 FairSync
-
lock方法 //差异一 acquire(1) tryacquire方法 //差异二 if state == 0 if 等待队列中无其他节点 && CASstate(0,x) 获取锁 else if 当前线程==持有锁的线程 进行重入操作
-
- 非公平锁 NonfairSync
5. ReentrantReadWriteLock
与重入锁具有类似的语义
仍然使用aqs的state作为占用计数,但是:
使用高16位表示读锁占用,计数运算为state cas c+65536(1<<16 即为第17位为1,低16为0),state>>16表示读锁占用个数。
使用低16位标示写锁占用,计数运算为state cas c+A,state低16位即为写锁占用次数。
这样就可使用state&65535((1<<16)-1 即低16位全为1)来判断是否写占用。
特性
- 获取的顺序性
- 不是利用reader或writer的请求顺序,而是提供一种可选的公平性。
- 非公平模式
- 默认选择
- 非公平锁会被不断的竞争,不断的cas,虽然会不定期的延缓读或写线程,但是比公平锁的性能高。
- 公平模式
- 使用类似到达顺序的规则,即检查等待队列
- 当一个lock被释放时
- 有个写线程等待的时间最长,则其获得锁
- 有一组读线程的等待时间比写线程等待时间长,则该组读线程获得锁
- 当读线程lock时,需等待之前的写线程获得锁并释放;当写线程lock时,需等待之前的读线程获得锁并释放。
- 当读写线程进行trylock时 ,会立即尝试占用,如果成功则获得锁,否则立即返回。
- 重入性
- 写线程可以重复请求写锁,在获得写锁时也可获取读锁;
- 读线程可以重复请求读锁,但必须等待写锁释放,且获得读锁后不可能获取写锁;
- 锁降级
- 顺序为:获取写锁后,写操作,然后再获得读锁,然后释放写锁,读操作,然后释放读锁。
- 目的:写之后,我就想立马使用,不想再去等待读锁。
- 只允许从写到读,但是无法从读到写锁。
- 写锁是提供condition的,而读锁不提供且会抛异常。
- 使用举例
- 使用降级特性,写后立即读;
- 改善非线程安全的集合的并发性,如读时获取读锁,写时获取写锁。
6. 如何选择Lock和同步器
同步关键字的jvm实现已经修改,不应把导致更多上线文切换资源浪费作为选择依据。
应考虑两者的差异、使用场景,参见上文,如表现形式、行为与语义、用法。
7. 自旋锁
在AQS类中可以看到自旋锁的实现方式,基本操作包括 循环(参与获得锁)、node状态对比、cas(获取锁)、park(停止线程调度) 、try-catch等。AQS依赖自旋锁,以实现行为多样性,让Lock具有更多的特性,所以自旋锁是java lock的具体实现基础。个人理解,讨论为什么使用自旋锁,其就是要讨论lock的特性。