为了更好的支持并发程序,JDK内部提供了多种锁。本文总结4种锁。html
使用:并发
synchronized本质上就2种锁:高并发
1.锁同步代码块性能
2.锁方法ui
可用object.wait() object.notify()来操做线程等待唤醒spa
原理:synchronized细节的描述传送门:jdk源码剖析三:锁Synchronized 线程
性能和建议:JDK6以后,在并发量不是特别大的状况下,性能中等且稳定。建议新手使用。设计
使用:ReentrantLock是Lock接口的实现类。Lock接口的核心方法是lock(),unlock(),tryLock()。可用Condition来操做线程:code
如上图,await()和object.wait()相似,singal()和object.notify()相似,singalAll()和object.notifyAll()相似orm
原理:核心类AbstractQueuedSynchronizer,经过构造一个基于阻塞的CLH队列容纳全部的阻塞线程,而对该队列的操做均经过Lock-Free(CAS)操做,但对已经得到锁的线程而言,ReentrantLock实现了偏向锁的功能。
性能和建议:性能中等,建议须要手动操做线程时使用。
使用:ReentrantReadWriteLock是ReadWriteLock接口的实现类。ReadWriteLock接口的核心方法是readLock(),writeLock()。实现了并发读、互斥写。但读锁会阻塞写锁,是悲观锁的策略。
原理:类图以下:
JDK1.8下,如图ReentrantReadWriteLock有5个静态方法:
性能和建议:适用于读多写少的状况。性能较高。
UnsupportedOperationException
异常。应用:
使用:
StampedLock控制锁有三种模式(排它写,悲观读,乐观读),一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字做为票据stamp,它用相应的锁状态表示并控制访问。下面是JDK1.8源码自带的示例:
1 public class StampedLockDemo { 2 //一个点的x,y坐标 3 private double x,y; 4 private final StampedLock sl = new StampedLock(); 5 6 //【写锁(排它锁)】 7 void move(double deltaX,double deltaY) {// an exclusively locked method 8 /**stampedLock调用writeLock和unlockWrite时候都会致使stampedLock的stamp值的变化 9 * 即每次+1,直到加到最大值,而后从0从新开始 10 **/ 11 long stamp =sl.writeLock(); //写锁 12 try { 13 x +=deltaX; 14 y +=deltaY; 15 } finally { 16 sl.unlockWrite(stamp);//释放写锁 17 } 18 } 19 20 //【乐观读锁】 21 double distanceFromOrigin() { // A read-only method 22 /** 23 * tryOptimisticRead是一个乐观的读,使用这种锁的读不阻塞写 24 * 每次读的时候获得一个当前的stamp值(相似时间戳的做用) 25 */ 26 long stamp = sl.tryOptimisticRead(); 27 //这里就是读操做,读取x和y,由于读取x时,y可能被写了新的值,因此下面须要判断 28 double currentX = x, currentY = y; 29 /**若是读取的时候发生了写,则stampedLock的stamp属性值会变化,此时须要重读, 30 * 再重读的时候须要加读锁(而且重读时使用的应当是悲观的读锁,即阻塞写的读锁) 31 * 固然重读的时候还可使用tryOptimisticRead,此时须要结合循环了,即相似CAS方式 32 * 读锁又从新返回一个stampe值*/ 33 if (!sl.validate(stamp)) {//若是验证失败(读以前已发生写) 34 stamp = sl.readLock(); //悲观读锁 35 try { 36 currentX = x; 37 currentY = y; 38 }finally{ 39 sl.unlockRead(stamp);//释放读锁 40 } 41 } 42 //读锁验证成功后执行计算,即读的时候没有发生写 43 return Math.sqrt(currentX *currentX + currentY *currentY); 44 } 45 46 //【悲观读锁】 47 void moveIfAtOrigin(double newX, double newY) { // upgrade 48 // 读锁(这里可用乐观锁替代) 49 long stamp = sl.readLock(); 50 try { 51 //循环,检查当前状态是否符合 52 while (x == 0.0 && y == 0.0) { 53 /** 54 * 转换当前读戳为写戳,即上写锁 55 * 1.写锁戳,直接返回写锁戳 56 * 2.读锁戳且写锁可得到,则释放读锁,返回写锁戳 57 * 3.乐观读戳,当当即可用时返回写锁戳 58 * 4.其余状况返回0 59 */ 60 long ws = sl.tryConvertToWriteLock(stamp); 61 //若是写锁成功 62 if (ws != 0L) { 63 stamp = ws;// 替换票据为写锁 64 x = newX;//修改数据 65 y = newY; 66 break; 67 } 68 //转换为写锁失败 69 else { 70 //释放读锁 71 sl.unlockRead(stamp); 72 //获取写锁(必要状况下阻塞一直到获取写锁成功) 73 stamp = sl.writeLock(); 74 } 75 } 76 } finally { 77 //释放锁(多是读/写锁) 78 sl.unlock(stamp); 79 } 80 } 81 }
原理:
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以后才有,当高并发下且读远大于写时,因为能够乐观读,性能极高!
4种锁,最稳定是内置synchronized锁(并非彻底被替代),当并发量大且读远大于写的状况下最快的的是StampedLock锁(乐观读。近似于无锁)。建议你们采用。
====================
参考:《JAVA高并发程序设计》