java读写锁原理及源码分析(一)

(一) 公平性

  • 非公平锁(默认)
    这个和独占锁的非公平性一样,由于读线程之间没有锁竞争,所以读操作没有公平性和非公平性,写操作时,由于写操作可能立即获取到锁,所以会推迟一个或多个读操作或者写操作。因此非公平锁的吞吐量要高于公平锁。
  • 公平锁
    利用AQS的CLH队列,释放当前保持的锁(读锁或者写锁)时,优先为等待时间最长的那个写线程分配写入锁,当前前提是写线程的等待时间要比所有读线程的等待时间要长。同样一个线程持有写入锁或者有一个写线程已经在等待了,那么试图获取公平锁的(非重入)所有线程(包括读写线程)都将被阻塞,直到最先的写线程释放锁。如果读线程的等待时间比写线程的等待时间还有长,那么一旦上一个写线程释放锁,这一组读线程将获取锁。

(二) 重入性

  • 读写锁允许读线程和写线程按照请求锁的顺序重新获取读取锁或者写入锁。当然了只有写线程释放了锁,读线程才能获取重入锁。

  • 写线程获取写入锁后可以再次获取读取锁,但是读线程获取读取锁后却不能获取写入锁。

  • 另外读写锁最多支持65535个递归写入锁和65535个递归读取锁。

(三) 锁降级

  • 写线程获取写入锁后可以获取读取锁,然后释放写入锁,这样就从写入锁变成了读取锁,从而实现锁降级的特性。

(四) 锁升级

  • 读取锁是不能直接升级为写入锁的。因为获取一个写入锁需要释放所有读取锁,所以如果有两个读取锁视图获取写入锁而都不释放读取锁时就会发生死锁。

(五) 锁获取中断

  • 读取锁和写入锁都支持获取锁期间被中断。这个和独占锁一致。

(六) 条件变量

  • 写入锁提供了条件变量(Condition)的支持,这个和独占锁一致,但是读取锁却不允许获取条件变量,将得到一个UnsupportedOperationException异常。

(七) 重入数

  • 读取锁和写入锁的数量最大分别只能是65535(包括重入数)。

(八) 监测

  • 此类支持一些确定是保持锁还是争用锁的方法。这些方法设计用于监视系统状态,而不是同步控制。

  • 首先我们看到这个类似乎有两把锁:readLock/writeLock。

  • 但是事实上在ReentrantReadWriteLock里锁的实现是靠内部类java.util.concurrent.locks.ReentrantReadWriteLock.Sync完成的。这个类看起来比较眼熟,它是AQS的一个子类,这中类似的结构在CountDownLatch、ReentrantLock、Semaphore里面都存在。

  • 在ReentrantReadWriteLock里面的锁主体就是一个Sync,也就是上面提到的FairSync或者NonfairSync,所以说实际上只有一个
    锁,只是在获取读取锁和写入锁的方式上不一样而已。

public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable public static class ReadLock implements Lock, java.io.Serializable public static class WriteLock implements Lock, java.io.Serializable /** Inner class providing readlock */ private final ReentrantReadWriteLock.ReadLock readerLock;

/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;

/** Performs all synchronization mechanics */
private final Sync sync;

public ReentrantReadWriteLock.WriteLock writeLock() {               
       return writerLock; 
}
public ReentrantReadWriteLock.ReadLock  readLock()  {
       return readerLock; 
}
public ReentrantReadWriteLock() {
       this(false);
}
public ReentrantReadWriteLock(boolean fair) {
   sync = (fair)? new FairSync() : new NonfairSync();
   readerLock = new ReadLock(this);
   writerLock = new WriteLock(this);
}
  • 很明显获取锁就是直接返回了对应内部类的读锁和写锁,而这两把锁在读写锁实例创建的时候进行了初始化。那这两把锁又是怎么实现锁操作,又怎样协作的呢?
public void lock() {//write Lock
    sync.acquire(1);
}
public void lock() {//read Lock
    sync.acquireShared(1);
}
  • 这里和正常的锁操作一样,都是调用同步器的acquire(独占锁)和acquireShared(共享锁)操作的,我们来看一下具体的内部实现。
public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){
            selfInterrupt();
        }         
}
public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0){
            doAcquireShared(arg);
        }          
}
  • acquire和acquireShared仍然是调用AQS父类的操作,先尝试,然后不行进入队列阻塞等待,具体可参考之前锁机制的部分的描述,我们重点来看下前面第一步尝试部分不同的地方。首先我们来看写锁部分。
protected final boolean tryAcquire(int acquires) {
      Thread current = Thread.currentThread();
      int c = getState();
      int w = exclusiveCount(c);
      if (c != 0) {
     // (Note: if c != 0 and w == 0 then shared count != 0)
         if (w == 0 ||current!=getExclusiveOwnerThread()){
               return false;
         }

         if (w + exclusiveCount(acquires) > MAX_COUNT){
            throw new Error("Maximum lock count exceeded");
         }            
      }
           if((w==0&&writerShouldBlock(current))
           ||!compareAndSetState(c,c+acquires)){
}
           return false;
      setExclusiveOwnerThread(current);
      return true;
}
  • 这段代码首先取当前锁的个数,然后取写锁的个数w,这段取写锁个数的地方有点奇怪。在AQS部分我们讲到AQS中有一个state字段(int类型,32位)用来描述有多少线程获持有锁。在独占锁的时代这个值通常是0或者1(如果是重入的就是重入的次数),在共享锁的时代就是持有锁的数量。上面我们提到,ReadWriteLock的读、写锁是相关但是又不一致的,所以需要两个数来描述读锁(共享锁)和写锁(独占锁)的数量。显然现在一个state就不够用了。于是在ReentrantReadWrilteLock里面将这个字段一分为二,高位16位表示共享锁的数量,低位16位表示独占锁的数量(或者重入数量)。2^16-1=65535,这就是上节中提到的为什么共享锁和独占锁的数量最大只能是65535的原因了。
static final int SHARED_SHIFT   = 16;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
static int exclusiveCount(int c) { 
    return c & EXCLUSIVE_MASK; 
}

了解了之后我们再来看这段代码,就好理解多了。既然写锁是低16位,那就取低16位的最大值与当前的c做与运算,高16位和0与运算后是0,那剩下的就是低位运算的值,就全是写锁持有线程的数目了!

  • 在取到写锁线程的数目后,首先判断是否已经有线程持有了锁,如果已经有线程持有了锁(c!=0),则看当前写锁线程的数目,如果写线程数(w)为0(那么读线程数就不为0)或者独占锁线程(持有锁的线程)不是当前线程就返回失败,如果写入锁的数量(其实是重入数)大于65535就抛出一个Error异常。
  • 如果当且写线程数为0(那么读线程也应该为0,因为上面已经处理c!=0的情况),并且当前线程需要阻塞那么就返回失败;如果通过CAS增加写线程数失败也返回失败。
  • 如果c=0,w=0或者c>0,w>0(重入),则设置当前线程或锁的拥有者,返回成功!
  • 整个流程还是比较简单的,但是有人会有疑问了,第2步中,既然写线程为0,读线程也为0,为啥还要判断是否需要阻塞的呢?这一点就涉及到公平锁和不公平锁的实现:
final boolean writerShouldBlock(Thread current) {
     // only proceed if queue is empty or current thread at head
     return !isFirst(current);
}
final boolean writerShouldBlock(Thread current) {
     return false; // writers can always barge
}
  • 公平锁只有头结点才不会阻塞,非公平锁则无阻塞。
    原文作者:java锁
    原文地址: https://blog.csdn.net/wuhao13601400547/article/details/52673325
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞