并发锁之二:ReentrantReadWriteLock读写锁

一、简介

  读写锁是一种特殊的自旋锁,它把对共享资源对访问者划分成了读者和写者,读者只对共享资源进行访问,写者则是对共享资源进行写操作。读写锁在ReentrantLock上进行了拓展使得该锁更适合读操作远远大于写操作对场景。一个读写锁同时只能存在一个写锁但是可以存在多个读锁,但不能同时存在写锁和读锁。

  如果读写锁当前没有读者,也没有写者,那么写者可以立刻获的读写锁,否则必须自旋,直到没有任何的写锁或者读锁存在。如果读写锁没有写锁,那么读锁可以立马获取,否则必须等待写锁释放。

二、简单示例

   缓存系统:你要取数据,需调用我的public Object getData(String key)方法,我要检查我内部有没有这个数据,如果有就直接返回,如果没有,就从数据库中查找这个数,查到后将这个数据存入我内部的存储器中,下次再有人来要这个数据,我就直接返回这个数不用再到数据库中找了

 1 /**
 2  * 设计一个缓存系统*/
 8 public class CacheDemo {
 9   
10   private Map<String, Object> cache = new HashMap<String, Object>();
11   
12   public static void main(String[] args) {
13     String key = "name";
14     CacheDemo cacheDemo = new CacheDemo();
15     System.out.println(cacheDemo.getData(key)); //从数据库获取数据
16     System.out.println(cacheDemo.getData(key)); //从缓存获取数据
17     System.out.println(cacheDemo.getData(key)); //从缓存获取数据
18   }
19   
20   private ReadWriteLock rwl = new ReentrantReadWriteLock();
   public ReadLock rdl = rwl.readLock();
   public WriteLock wl = rwl.writeLock();
21 22 public Object getData(String key) { 23 rdl.lock(); //上读锁 24 Object value = null; 25 try { 26 value = cache.get(key); //先查询内部存储器中有没有要的值 27 if (value == null) { //如果没有,就去数据库中查询,并将查到的结果存入内部存储器中 28 //释放读锁、上写锁 29 rdl.unlock(); 30 wl.lock(); 31 try { 32 if (value == null) { //再次进行判断,防止多个写线程堵在这个地方重复写 33 System.out.println("read data from database"); 34 value = "张三"; 35 cache.put(key, value); 36 } 37 } finally { 38 //设置完成 释放写锁 39 wl.unlock(); 40 } 41 //恢复读写状态 42 rdl.lock(); 43 }else{ 44 System.out.println("read data from cache"); 45 } 46 } finally { 47 rdl.unlock(); //释放读锁 48 } 49 return value; 50 } 51 }

对于缓存系统而言,尤其是热门数据很少改变的情况下,使用ReentrantReadWriteLock读写锁可以大大的增加程序的并发性。

三、实现原理

   ReentrantReadWriteLock的基本原理和ReentrantLock没有很大的区别,只不过在ReentantLock的基础上拓展了两个不同类型的锁,读锁和写锁。首先可以看一下ReentrantReadWriteLock的内部结构:

内部维护了一个ReadLock和一个WriteLock,整个类的附加功能也就是通过这两个内部类实现的。

那么内部又是怎么实现这个读锁和写锁的呢。由于一个类既要维护读锁又要维护写锁,那么这两个锁的状态又是如何区分的。在ReentrantReadWriteLock对象内部维护了一个读写状态:

 读写锁依赖自定义同步器实现同步功能,读写状态也就是同步器的同步状态。读写锁将整形变量切分成两部分,高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 }

  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;
14                     // else we hold the exclusive lock; blocking here
15                     // would cause deadlock.
16                 } else if (readerShouldBlock()) {//判断读锁是否应该被阻塞
17                     // Make sure we're not acquiring read lock reentrantly
18                     if (firstReader == current) {
19                         // assert firstReaderHoldCount > 0;
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的使用需要在开发者自己根据实际项目的情况而定。对于读写锁当读的操作远远大于写操作的时候会增加程序很高的并发量和吞吐量。

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

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


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

猜你喜欢

转载自www.cnblogs.com/wait-pigblog/p/9350569.html