同步中的四种锁synchronized、ReentrantLock、ReentrantReadWriteLock、StampedLock
2017-06-01 18:38
381 查看
为了更好的支持并发程序,JDK内部提供了多种锁。本文总结4种锁。
synchronized本质上就2种锁:
1.锁同步代码块
2.锁方法
可用object.wait() object.notify()来操作线程等待唤醒
原理:synchronized细节的描述传送门:jdk源码剖析三:锁Synchronized
性能和建议:JDK6之后,在并发量不是特别大的情况下,性能中等且稳定。建议新手使用。
如上图,await()和object.wait()类似,singal()和object.notify()类似,singalAll()和object.notifyAll()类似
原理:核心类AbstractQueuedSynchronizer,通过构造一个基于阻塞的CLH队列容纳所有的阻塞线程,而对该队列的操作均通过Lock-Free(CAS)操作,但对已经获得锁的线程而言,ReentrantLock实现了偏向锁的功能。
性能和建议:性能中等,建议需要手动操作线程时使用。
原理:类图如下:
JDK1.8下,如图ReentrantReadWriteLock有5个静态方法:
Sync:继承于经典的AbstractQueuedSynchronizer(传说中的AQS),是一个抽象类,包含2个抽象方法readerShouldBlock();writerShouldBlock()
FairSync和NonfairSync:继承于Sync,分别实现了公平/非公平锁。
ReadLock和WriteLock:都是Lock实现类,分别实现了读、写锁。ReadLock是共享的,而WriteLock是独占的。于是Sync类覆盖了AQS中独占和共享模式的抽象方法(tryAcquire/tryAcquireShared等),用同一个等待队列来维护读/写排队线程,而用一个32位int state标示和记录读/写锁重入次数--Doug Lea把状态的高16位用作读锁,记录所有读锁重入次数之和,低16位用作写锁,记录写锁重入次数。所以无论是读锁还是写锁最多只能被持有65535次。
性能和建议:适用于读多写少的情况。性能较高。
公平性
非公平锁(默认),为了防止写线程饿死,规则是:当等待队列头部结点是独占模式(即要获取写锁的线程)时,只有获取独占锁线程可以抢占,而试图获取共享锁的线程必须进入队列阻塞;当队列头部结点是共享模式(即要获取读锁的线程)时,试图获取独占和共享锁的线程都可以抢占。
公平锁,利用AQS的等待队列,线程按照FIFO的顺序获取锁,因此不存在写线程一直等待的问题。
重入性:读写锁均是可重入的,读/写锁重入次数保存在了32位int state的高/低16位中。而单个读线程的重入次数,则记录在ThreadLocalHoldCounter类型的readHolds里。
锁降级:写线程获取写入锁后可以获取读取锁,然后释放写入锁,这样就从写入锁变成了读取锁,从而实现锁降级。
锁获取中断:读取锁和写入锁都支持获取锁期间被中断。
条件变量:写锁提供了条件变量(Condition)的支持,这个和独占锁ReentrantLock一致,但是读锁却不允许,调用readLock().newCondition()会抛出
应用:
StampedLock控制锁有三种模式(排它写,悲观读,乐观读),一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字作为票据stamp,它用相应的锁状态表示并控制访问。下面是JDK1.8源码自带的示例:
原理:
StampedLockd的内部实现是基于CLH锁的,一种自旋锁,保证没有饥饿且FIFO。
CLH锁原理:锁维护着一个等待线程队列,所有申请锁且失败的线程都记录在队列。一个节点代表一个线程,保存着一个标记位locked,用以判断当前线程是否已经释放锁。当一个线程试图获取锁时,从队列尾节点作为前序节点,循环判断所有的前序节点是否已经成功释放锁。
如上图所示,StampedLockd源码中的WNote就是等待链表队列,每一个WNode标识一个等待线程,whead为CLH队列头,wtail为CLH队列尾,state为锁的状态。long型即64位,倒数第八位标识写锁状态,如果为1,标识写锁占用!下面围绕这个state来讲述锁操作。
首先是常量标识:
WBIT=1000 0000(即-128)
RBIT =0111 1111(即127)
SBIT =1000 0000(后7位表示当前正在读取的线程数量,清0)
1.乐观读
tryOptimisticRead():如果当前没有写锁占用,返回state(后7位清0,即清0读线程数),如果有写锁,返回0,即失败。
2.校验stamp
校验这个戳是否有效validate():比较当前stamp和发生乐观锁得到的stamp比较,不一致则失败。
3.悲观读
乐观锁失败后锁升级为readLock():尝试state+1,用于统计读线程的数量,如果失败,进入acquireRead()进行自旋,通过CAS获取锁。如果自旋失败,入CLH队列,然后再自旋,如果成功获得读锁,激活cowait队列中的读线程Unsafe.unpark(),最终依然失败,Unsafe().park()挂起当前线程。
4.排它写
writeLock():典型的cas操作,如果STATE等于s,设置写锁位为1(s+WBIT)。acquireWrite跟acquireRead逻辑类似,先自旋尝试、加入等待队列、直至最终Unsafe.park()挂起线程。
5.释放锁
unlockWrite():释放锁与加锁动作相反。将写标记位清零,如果state溢出,则退回到初始值;
性能和建议:JDK8之后才有,当高并发下且读远大于写时,由于可以乐观读,性能极高!
====================
参考:《JAVA高并发程序设计》
1.synchronized同步锁
使用:synchronized本质上就2种锁:
1.锁同步代码块
2.锁方法
可用object.wait() object.notify()来操作线程等待唤醒
原理:synchronized细节的描述传送门:jdk源码剖析三:锁Synchronized
性能和建议:JDK6之后,在并发量不是特别大的情况下,性能中等且稳定。建议新手使用。
2.ReentrantLock可重入锁(Lock接口)
使用:ReentrantLock是Lock接口的实现类。Lock接口的核心方法是lock(),unlock(),tryLock()。可用Condition来操作线程:如上图,await()和object.wait()类似,singal()和object.notify()类似,singalAll()和object.notifyAll()类似
原理:核心类AbstractQueuedSynchronizer,通过构造一个基于阻塞的CLH队列容纳所有的阻塞线程,而对该队列的操作均通过Lock-Free(CAS)操作,但对已经获得锁的线程而言,ReentrantLock实现了偏向锁的功能。
性能和建议:性能中等,建议需要手动操作线程时使用。
3.ReentrantReadWriteLock可重入读写锁(ReadWriteLock接口)
使用:ReentrantReadWriteLock是ReadWriteLock接口的实现类。ReadWriteLock接口的核心方法是readLock(),writeLock()。实现了并发读、互斥写。但读锁会阻塞写锁,是悲观锁的策略。原理:类图如下:
JDK1.8下,如图ReentrantReadWriteLock有5个静态方法:
Sync:继承于经典的AbstractQueuedSynchronizer(传说中的AQS),是一个抽象类,包含2个抽象方法readerShouldBlock();writerShouldBlock()
FairSync和NonfairSync:继承于Sync,分别实现了公平/非公平锁。
ReadLock和WriteLock:都是Lock实现类,分别实现了读、写锁。ReadLock是共享的,而WriteLock是独占的。于是Sync类覆盖了AQS中独占和共享模式的抽象方法(tryAcquire/tryAcquireShared等),用同一个等待队列来维护读/写排队线程,而用一个32位int state标示和记录读/写锁重入次数--Doug Lea把状态的高16位用作读锁,记录所有读锁重入次数之和,低16位用作写锁,记录写锁重入次数。所以无论是读锁还是写锁最多只能被持有65535次。
性能和建议:适用于读多写少的情况。性能较高。
公平性
非公平锁(默认),为了防止写线程饿死,规则是:当等待队列头部结点是独占模式(即要获取写锁的线程)时,只有获取独占锁线程可以抢占,而试图获取共享锁的线程必须进入队列阻塞;当队列头部结点是共享模式(即要获取读锁的线程)时,试图获取独占和共享锁的线程都可以抢占。
公平锁,利用AQS的等待队列,线程按照FIFO的顺序获取锁,因此不存在写线程一直等待的问题。
重入性:读写锁均是可重入的,读/写锁重入次数保存在了32位int state的高/低16位中。而单个读线程的重入次数,则记录在ThreadLocalHoldCounter类型的readHolds里。
锁降级:写线程获取写入锁后可以获取读取锁,然后释放写入锁,这样就从写入锁变成了读取锁,从而实现锁降级。
锁获取中断:读取锁和写入锁都支持获取锁期间被中断。
条件变量:写锁提供了条件变量(Condition)的支持,这个和独占锁ReentrantLock一致,但是读锁却不允许,调用readLock().newCondition()会抛出
UnsupportedOperationException异常。
应用:
4.StampedLock戳锁
使用:StampedLock控制锁有三种模式(排它写,悲观读,乐观读),一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字作为票据stamp,它用相应的锁状态表示并控制访问。下面是JDK1.8源码自带的示例:
public class StampedLockDemo { //一个点的x,y坐标 private double x,y; private final StampedLock sl = new StampedLock(); //【写锁(排它锁)】 void move(double deltaX,double deltaY) {// an exclusively locked method /**stampedLock调用writeLock和unlockWrite时候都会导致stampedLock的stamp值的变化 * 即每次+1,直到加到最大值,然后从0重新开始 **/ long stamp =sl.writeLock(); //写锁 try { x +=deltaX; y +=deltaY; } finally { sl.unlockWrite(stamp);//释放写锁 } } //【乐观读锁】 double distanceFromOrigin() { // A read-only method /** * tryOptimisticRead是一个乐观的读,使用这种锁的读不阻塞写 * 每次读的时候得到一个当前的stamp值(类似时间戳的作用) */ long stamp = sl.tryOptimisticRead(); //这里就是读操作,读取x和y,因为读取x时,y可能被写了新的值,所以下面需要判断 double currentX = x, currentY = y; /**如果读取的时候发生了写,则stampedLock的stamp属性值会变化,此时需要重读, * 再重读的时候需要加读锁(并且重读时使用的应当是悲观的读锁,即阻塞写的读锁) * 当然重读的时候还可以使用tryOptimisticRead,此时需要结合循环了,即类似CAS方式 * 读锁又重新返回一个stampe值*/ if (!sl.validate(stamp)) {//如果验证失败(读之前已发生写) stamp = sl.readLock(); //悲观读锁 try { currentX = x; currentY = y; }finally{ sl.unlockRead(stamp);//释放读锁 } } //读锁验证成功后执行计算,即读的时候没有发生写 return Math.sqrt(currentX *currentX + currentY *currentY); } //【悲观读锁】 void moveIfAtOrigin(double newX, double newY) { // upgrade // 读锁(这里可用乐观锁替代) long stamp = sl.readLock(); try { //循环,检查当前状态是否符合 while (x == 0.0 && y == 0.0) { /** * 转换当前读戳为写戳,即上写锁 * 1.写锁戳,直接返回写锁戳 * 2.读锁戳且写锁可获得,则释放读锁,返回写锁戳 * 3.乐观读戳,当立即可用时返回写锁戳 * 4.其他情况返回0 */ long ws = sl.tryConvertToWriteLock(stamp); //如果写锁成功 if (ws != 0L) { stamp = ws;// 替换票据为写锁 x = newX;//修改数据 y = newY; break; } //转换为写锁失败 else { //释放读锁 sl.unlockRead(stamp); //获取写锁(必要情况下阻塞一直到获取写锁成功) stamp = sl.writeLock(); } } } finally { //释放锁(可能是读/写锁) sl.unlock(stamp); } } }
原理:
StampedLockd的内部实现是基于CLH锁的,一种自旋锁,保证没有饥饿且FIFO。
CLH锁原理:锁维护着一个等待线程队列,所有申请锁且失败的线程都记录在队列。一个节点代表一个线程,保存着一个标记位locked,用以判断当前线程是否已经释放锁。当一个线程试图获取锁时,从队列尾节点作为前序节点,循环判断所有的前序节点是否已经成功释放锁。
如上图所示,StampedLockd源码中的WNote就是等待链表队列,每一个WNode标识一个等待线程,whead为CLH队列头,wtail为CLH队列尾,state为锁的状态。long型即64位,倒数第八位标识写锁状态,如果为1,标识写锁占用!下面围绕这个state来讲述锁操作。
首先是常量标识:
WBIT=1000 0000(即-128)
RBIT =0111 1111(即127)
SBIT =1000 0000(后7位表示当前正在读取的线程数量,清0)
1.乐观读
tryOptimisticRead():如果当前没有写锁占用,返回state(后7位清0,即清0读线程数),如果有写锁,返回0,即失败。
2.校验stamp
校验这个戳是否有效validate():比较当前stamp和发生乐观锁得到的stamp比较,不一致则失败。
3.悲观读
乐观锁失败后锁升级为readLock():尝试state+1,用于统计读线程的数量,如果失败,进入acquireRead()进行自旋,通过CAS获取锁。如果自旋失败,入CLH队列,然后再自旋,如果成功获得读锁,激活cowait队列中的读线程Unsafe.unpark(),最终依然失败,Unsafe().park()挂起当前线程。
4.排它写
writeLock():典型的cas操作,如果STATE等于s,设置写锁位为1(s+WBIT)。acquireWrite跟acquireRead逻辑类似,先自旋尝试、加入等待队列、直至最终Unsafe.park()挂起线程。
5.释放锁
unlockWrite():释放锁与加锁动作相反。将写标记位清零,如果state溢出,则退回到初始值;
性能和建议:JDK8之后才有,当高并发下且读远大于写时,由于可以乐观读,性能极高!
5.总结
4种锁,最稳定是内置synchronized锁(并不是完全被替代),当并发量大且读远大于写的情况下最快的的是StampedLock锁(乐观读。近似于无锁)。建议大家采用。====================
参考:《JAVA高并发程序设计》
相关文章推荐
- 同步中的四种锁synchronized、ReentrantLock、ReentrantReadWriteLock、StampedLock
- Java多线程synchronized、ReentrantLock、ReentrantReadWriteLock 和StampedLock 的对比
- (转载)StampedLock、ReadWriteLock以及synchronized的比较
- Java:多线程,线程同步,同步锁(Lock)的使用(ReentrantLock、ReentrantReadWriteLock)
- ReentrantReadWriteLock和两种同步方式的对比
- JDK1.8 StampedLock: 解决ReentrantReadWriteLock在读多写少情况下,写线程饥饿问题
- 高并发第十一弹:J.U.C -AQS(AbstractQueuedSynchronizer) 组件:Lock,ReentrantLock,ReentrantReadWriteLock,StampedLock
- ReentrantReadWriteLock & ReentrantLock & Synchronized 区别
- java锁:synchronized、ReadWriteLock、ReentrantReadWriteLock*
- 关于synchronized以及ReetrantLock、ReentrantReadWriteLock的选择
- Synchronized与ReentrantReadWriteLock性能比较
- synchronized、Lock、ReentrantLock、ReadWriteLock区别,threadlocal怎么解决线程同步问题
- java多线程基础---synchronized与ReentrantReadWriteLock的介绍和比较
- Lock、ReentrantLock、synchronized、ReentrantReadWriteLock使用
- Android并发编程之如何使用ReentrantReadWriteLock替代synchronized来提高程序的效率
- ReentrantReadWriteLock 与 synchronized 性能差别
- java多线程基础---synchronized与ReentrantReadWriteLock的介绍与比较
- Java多线程——锁(Synchronized、Lock、ReentrantLock、ReadWriteLock、ReentrantReadWriteLock)
- Android并发编程之如何使用ReentrantReadWriteLock替代synchronized来提高程序的效率
- Java线程之锁对象高效同步(ReentrantLock/ReentrantReadWriteLock)