一、简介
读写锁是一种特殊的自旋锁,它把对共享资源对访问者划分成了读者和写者,读者只对共享资源进行访问,写者则是对共享资源进行写操作。读写锁在ReentrantLock上进行了拓展使得该锁更适合读操作远远大于写操作对场景。一个读写锁同时只能存在一个写锁但是可以存在多个读锁,但不能同时存在写锁和读锁。
如果读写锁当前没有读者,也没有写者,那么写者可以立刻获的读写锁,否则必须自旋,直到没有任何的写锁或者读锁存在。如果读写锁没有写锁,那么读锁可以立马获取,否则必须等待写锁释放。(但是有一个例外,就是读写锁中的锁降级操作,当同一个线程获取写锁后,在写锁没有释放的情况下可以获取读锁再释放读锁这就是锁降级的一个过程)
二、简单示例
1 package cn.memedai; 2 3 import java.util.Random; 4 import java.util.concurrent.ExecutorService; 5 import java.util.concurrent.Executors; 6 import java.util.concurrent.locks.ReadWriteLock; 7 8 /** 9 * 读写锁Demo10 */11 public class ReentrantReadWriteLockDemo {12 13 class MyObject {14 private Object object;15 16 private ReadWriteLock lock = new java.util.concurrent.locks.ReentrantReadWriteLock();17 18 public void get() throws InterruptedException {19 lock.readLock().lock();//上读锁20 try {21 System.out.println(Thread.currentThread().getName() + "准备读取数据");22 Thread.sleep(new Random().nextInt(1000));23 System.out.println(Thread.currentThread().getName() + "读数据为:" + this.object);24 } finally {25 lock.readLock().unlock();26 }27 }28 29 public void put(Object object) throws InterruptedException {30 lock.writeLock().lock();31 try {32 System.out.println(Thread.currentThread().getName() + "准备写数据");33 Thread.sleep(new Random().nextInt(1000));34 this.object = object;35 System.out.println(Thread.currentThread().getName() + "写数据为" + this.object);36 } finally {37 lock.writeLock().unlock();38 }39 }40 }41 42 public static void main(String[] args) {43 final MyObject myObject = new ReentrantReadWriteLockDemo().new MyObject();44 ExecutorService executorService = Executors.newCachedThreadPool();45 for (int i = 0; i < 3; i++) {46 executorService.execute(new Runnable() {47 @Override48 public void run() {49 for (int j = 0; j < 3; j++) {50 51 try {52 myObject.put(new Random().nextInt(1000));//写操作53 } catch (InterruptedException e) {54 e.printStackTrace();55 }56 }57 }58 });59 }60 61 for (int i = 0; i < 3; i++) {62 executorService.execute(new Runnable() {63 @Override64 public void run() {65 for (int j = 0; j < 3; j++) {66 try {67 myObject.get();//多个线程读取操作68 } catch (InterruptedException e) {69 e.printStackTrace();70 }71 }72 }73 });74 }75 76 executorService.shutdown();77 }78 }
下面是代码运行结果的一种:
pool-1-thread-1准备写数据pool-1-thread-1写数据为513pool-1-thread-1准备写数据pool-1-thread-1写数据为173pool-1-thread-1准备写数据pool-1-thread-1写数据为487pool-1-thread-2准备写数据pool-1-thread-2写数据为89pool-1-thread-2准备写数据pool-1-thread-2写数据为814pool-1-thread-2准备写数据pool-1-thread-2写数据为1pool-1-thread-3准备写数据pool-1-thread-3写数据为701pool-1-thread-3准备写数据pool-1-thread-3写数据为503pool-1-thread-3准备写数据pool-1-thread-3写数据为694pool-1-thread-4准备读取数据pool-1-thread-5准备读取数据pool-1-thread-6准备读取数据pool-1-thread-4读数据为:694pool-1-thread-4准备读取数据pool-1-thread-4读数据为:694pool-1-thread-4准备读取数据pool-1-thread-6读数据为:694pool-1-thread-6准备读取数据pool-1-thread-5读数据为:694pool-1-thread-5准备读取数据pool-1-thread-6读数据为:694pool-1-thread-6准备读取数据pool-1-thread-4读数据为:694pool-1-thread-5读数据为:694pool-1-thread-5准备读取数据pool-1-thread-6读数据为:694pool-1-thread-5读数据为:694
从数据中也可以发现一开始读取的数据可能不一样,但是你会发现下面的时候线程4和线程5、线程6之间的读取的数据都是一样的,这就是共享读的特性。
三、实现原理
ReentrantReadWriteLock的基本原理和ReentrantLock没有很大的区别,只不过在ReentantLock的基础上拓展了两个不同类型的锁,读锁和写锁。首先可以看一下ReentrantReadWriteLock的内部结构:
内部维护了一个ReadLock和一个WriteLock,整个类的附加功能也就是通过这两个内部类实现的。
那么内部又是怎么实现这个读锁和写锁的呢。由于一个类既要维护读锁又要维护写锁,那么这两个锁的状态又是如何区分的。在ReentrantReadWriteLock对象内部维护了一个读写状态:
读写锁依赖自定义同步器实现同步功能,读写状态也就是同步器的同步状态。读写锁将整形变量切分成两部分,高16位表示读,低16位表示写:
读写锁的状态低16位为写锁,高16位为读锁
读写锁通过位运算计算各自的同步状态。假设当前同步状态的值为c,写状态就为c&0x0000FFFF,读状态为c >>> 16(无符号位补0右移16位)。当写状态增加1状态变为c+1,当读状态增加1时,状态编码就是c+(1 <<< 16)。
怎么维护读写状态的已经了解了,那么就可以开始了解具体怎么样实现的多个线程可以读,一个线程写的情况。
首先介绍的是ReadLock获取锁的过程
lock():获取读锁方法
1 public void lock() {2 sync.acquireShared(1);//自定义实现的获取锁方式3 }
acquireShared(int arg):这是一个获取共享锁的方法
1 protected final int tryAcquireShared(int unused) { 17 Thread current = Thread.currentThread();//获取当前线程18 int c = getState();//获取锁状态19 if (exclusiveCount(c) != 0 &&20 getExclusiveOwnerThread() != current)//如果获取锁的不是当前线程,并且由独占式锁的存在就不去获取,这里会发现必须同时满足两个条件才能判断其不能获取读锁这也会后面的锁降级做了准备21 return -1;22 int r = sharedCount(c);//获取当前共享资源的数量23 if (!readerShouldBlock() &&24 r < MAX_COUNT &&25 compareAndSetState(c, c + SHARED_UNIT)) {//代表可以获取读锁26 if (r == 0) {//如果当前没有线程获取读锁27 firstReader = current;//当前线程是第一个读锁获取者28 firstReaderHoldCount = 1;//在计数器上加129 } else if (firstReader == current) {30 firstReaderHoldCount++;//代表重入锁计数器累加31 } else { //内部定义的线程记录缓存32 HoldCounter rh = cachedHoldCounter;//HoldCounter主要是一个类用来记录线程已经线程获取锁的数量33 if (rh == null || rh.tid != current.getId())//如果不是当前线程34 cachedHoldCounter = rh = readHolds.get();//从每个线程的本地变量ThreadLocal中获取35 else if (rh.count == 0)//如果记录为0初始值设置36 readHolds.set(rh);//设置记录37 rh.count++;//自增38 }39 return 1;//返回1代表获取到了同步状态40 }41 return fullTryAcquireShared(current);//用来处理CAS设置状态失败的和tryAcquireShared非阻塞获取读锁失败的42 }
内部运用到了ThreadLocal线程本地对象,将每个线程获取锁的次数保存到每个线程内部,这样释放锁的时候就不会影响到其它的线程。
fullTryAcquireShared(Thread current):此方法用于处理在获取读锁过程中CAS设置状态失败的和非阻塞获取读锁失败的线程
1 final int fullTryAcquireShared(Thread current) { 2 //内部线程记录器 8 HoldCounter rh = null; 9 for (;;) {10 int c = getState();//同步状态11 if (exclusiveCount(c) != 0) {//代表存在独占锁12 if (getExclusiveOwnerThread() != current)//获取独占锁的线程不是当前线程返回失败13 return -1;16 } else if (readerShouldBlock()) {//判断读锁是否应该被阻塞18 if (firstReader == current) { 20 } else {21 if (rh == null) {//为null22 rh = cachedHoldCounter;//从缓存中进行获取23 if (rh == null || rh.tid != current.getId()) {24 rh = readHolds.get();//获取线程内部计数状态25 if (rh.count == 0)26 readHolds.remove();//移除27 }28 }29 if (rh.count == 0)//如果内部计数为0代表获取失败30 return -1;31 }32 }33 if (sharedCount(c) == MAX_COUNT)34 throw new Error("Maximum lock count exceeded");35 if (compareAndSetState(c, c + SHARED_UNIT)) {//CAS设置成功36 if (sharedCount(c) == 0) {37 firstReader = current;//代表为第一个获取读锁38 firstReaderHoldCount = 1;39 } else if (firstReader == current) {40 firstReaderHoldCount++;//重入锁41 } else {42 if (rh == null)43 rh = cachedHoldCounter;44 if (rh == null || rh.tid != current.getId())45 rh = readHolds.get();46 else if (rh.count == 0)47 readHolds.set(rh);48 rh.count++;49 cachedHoldCounter = rh; //将当前多少读锁记录下来50 }51 return 1;//返回获取同步状态成功52 }53 }54 }
分析完上面的方法可以总结一下获取读锁的过程:首先读写锁中读状态为所有线程获取读锁的次数,由于是可重入锁,又因为每个锁获取的读锁的次数由每个锁的本地变量ThreadLocal对象去保存因此增加了读取获取的流程难度,在每次获取读锁之前都会进行一次判断是否存在独占式写锁,如果存在独占式写锁就直接返回获取失败,进入同步队列中。如果当前没有写锁被获取,则线程可以获取读锁,由于共享锁的存在,每次获取都会判断线程的类型,以便每个线程获取同步状态的时候都在其对应的本地变量上进行自增操作。
lock(int arg):写锁的获取
public void lock() { sync.acquire(1);//AQS独占式获取锁 }
tryAcquire(int arg):独占式的获取写锁
1 protected final boolean tryAcquire(int acquires) { 13 Thread current = Thread.currentThread();//获取当前线程14 int c = getState();//获取同步状态值15 int w = exclusiveCount(c);//获取独占式资源值16 if (c != 0) {//已经有线程获取了 //代表已经存在读锁,或者当前线程不是获取到写锁的线程18 if (w == 0 || current != getExclusiveOwnerThread())19 return false;//获取失败20 if (w + exclusiveCount(acquires) > MAX_COUNT)21 throw new Error("Maximum lock count exceeded");22 //设置同步状态23 setState(c + acquires);24 return true;25 }26 if (writerShouldBlock() ||27 !compareAndSetState(c, c + acquires))//判断当前写锁线程是否应该阻塞,这里会有公平锁和非公平锁之间的区分28 return false;29 setExclusiveOwnerThread(current);//设置为当前线程30 return true;31 }
获取写锁相比获取读锁就简单了很多,在获取读锁之前只需要判断当前是否存在读锁,如果存在读锁那么获取失败,进而再判断获取写锁的线程是否为当前线程如果不是也就是失败否则就是重入锁在已有的状态值上进行自增
unlock():读锁释放
public void unlock() { sync.releaseShared(1);//AQS释放共享锁操作 }
tryReleaseShared(int arg):释放共享锁
1 protected final boolean tryReleaseShared(int unused) { 2 Thread current = Thread.currentThread();//获取当前线程 3 if (firstReader == current) {//如果当前线程就是获取读锁的线程 5 if (firstReaderHoldCount == 1)//如果此时获取资源为1 6 firstReader = null;//直接赋值null 7 else 8 firstReaderHoldCount--;//否则计数器自减 9 } else { //其他线程10 HoldCounter rh = cachedHoldCounter;//获取本地计数器11 if (rh == null || rh.tid != current.getId())12 rh = readHolds.get();13 int count = rh.count;14 if (count <= 1) {//代表只获取了一次15 readHolds.remove();16 if (count <= 0)17 throw unmatchedUnlockException();18 }19 --rh.count;20 }21 for (;;) {22 int c = getState();23 int nextc = c - SHARED_UNIT;24 if (compareAndSetState(c, nextc))28 return nextc == 0;//代表已经全部释放29 }30 }
释放锁的过程不难,但是有一个注意点,并不是释放一次就已经代表可以获取独占式写锁了,只有当同步状态的值为0的时候也就是代表既没有读锁存在也没有写锁存在才代表完全释放了读锁。
unlock():释放写锁
1 public void unlock() {2 sync.release(1);//释放独占式同步状态3 }
tryRelease(int arg):释放独占式写锁
1 protected final boolean tryRelease(int releases) { 2 if (!isHeldExclusively())//判断是否 3 throw new IllegalMonitorStateException(); 4 int nextc = getState() - releases;//同步状态值自减 5 boolean free = exclusiveCount(nextc) == 0;//如果状态值为0代表全部释放 6 if (free) 7 setExclusiveOwnerThread(null); 8 setState(nextc); 9 return free;10 }
写锁的释放相比读锁的释放简单很多,只需要判断当前的写锁是否全部释放完毕即可
四、读写锁之锁降级操作
什么是锁降级,锁降级就是从写锁降级成为读锁。在当前线程拥有写锁的情况下,再次获取到读锁,随后释放写锁的过程就是锁降级。这里可以举个例子:
1 public class CacheDemo { 3 private Mapcache = new HashMap (); 4 5 private ReadWriteLock rwl = new ReentrantReadWriteLock(); 6 public ReadLock rdl = rwl.readLock(); 7 public WriteLock wl = rwl.writeLock(); 8 9 public volatile boolean update = false;10 public void processData(){11 rdl.lock();//获取读锁12 if(!update){13 rdl.unlock();//释放读锁14 wl.lock();//获取写锁15 try{16 if(!update){17 update =true;18 }19 rdl.lock();//获取读锁20 finally{21 wl.unlock();//释放写锁22 }23 }24 try{25 }finally{26 rdl.unlock();//释放读锁27 } 29 }
五、总结
读写锁是在重入锁ReentrantLock基础上的一大改造,其通过在重入锁上维护一个读锁一个写锁实现的。对于ReentrantLock和ReentrantreadWriteLock的使用需要在开发者自己根据实际项目的情况而定。对于读写锁当读的操作远远大于写操作的时候会增加程序很高的并发量和吞吐量。虽说在高并发的情况下,读写锁的效率很高,但是同时又会存在一些问题,比如当读并发很高时读操作长时间占有锁,导致写锁长时间无法被获取而导致的线程饥饿问题,因此在JDK1.8中又在ReentrantReadWriteLock的基础上新增了一个读写并发锁StampLock。
================================================================================== 不管岁月里经历多少辛酸和艰难,告诉自己风雨本身就是一种内涵,努力的面对,不过就是一场命运的漂流,既然在路上,那么目的地必然也就是前方。
==================================================================================