并发锁之二:ReentrantReadWriteLock读写锁


一、简介

  读写锁是一种特殊的自旋锁,它把对共享资源对访问者划分成了读者和写者,读者只对共享资源进行访问,写者则是对共享资源进行写操作。读写锁在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  * 读写锁Demo 10  */
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  @Override 48                 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  @Override 64                 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写数据为513 pool-1-thread-1准备写数据 pool-1-thread-1写数据为173 pool-1-thread-1准备写数据 pool-1-thread-1写数据为487 pool-1-thread-2准备写数据 pool-1-thread-2写数据为89 pool-1-thread-2准备写数据 pool-1-thread-2写数据为814 pool-1-thread-2准备写数据 pool-1-thread-2写数据为1 pool-1-thread-3准备写数据 pool-1-thread-3写数据为701 pool-1-thread-3准备写数据 pool-1-thread-3写数据为503 pool-1-thread-3准备写数据 pool-1-thread-3写数据为694 pool-1-thread-4准备读取数据 pool-1-thread-5准备读取数据 pool-1-thread-6准备读取数据 pool-1-thread-4读数据为:694 pool-1-thread-4准备读取数据 pool-1-thread-4读数据为:694 pool-1-thread-4准备读取数据 pool-1-thread-6读数据为:694 pool-1-thread-6准备读取数据 pool-1-thread-5读数据为:694 pool-1-thread-5准备读取数据 pool-1-thread-6读数据为:694 pool-1-thread-6准备读取数据 pool-1-thread-4读数据为:694 pool-1-thread-5读数据为:694 pool-1-thread-5准备读取数据 pool-1-thread-6读数据为:694 pool-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;//在计数器上加1 29                 } 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) {//为null 22 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 Map<String, Object> cache = new HashMap<String, Object>();  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。

 

 

 

================================================================================== 

不管岁月里经历多少辛酸和艰难,告诉自己风雨本身就是一种内涵,努力的面对,不过就是一场命运的漂流,既然在路上,那么目的地必然也就是前方。


==================================================================================


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM