多线程之美7一ReentrantReadWriteLock源码分析

目录

前言

在多线程环境下,为了保证线程安全, 我们通常会对共享资源加锁操作,我们常用Synchronized关键字或者ReentrantLock 来实现,这两者加锁方式都是排他锁,即同一时刻最多允许一个线程操作,然而大多数场景中对共享资源读多于写,那么存在线程安全问题的是写操作(修改,添加,删除),我们是否应该考虑将读和写两个分开,只要运用合理,并发性能是不是可以提高,吞吐量增大呢? ReentrantReadWriteLock已经为我们实现了这种机制,我们一起来看它是怎样实现的吧!

1、读写锁的一些概念

在查看可重入读写锁的源码前,有几个概念需要先知道,对于后面理解源码很有帮助。

1、ReentrantReadWriteLock 内部 Sync类依然是继承AQS实现的,因此同步状态字段 state,依然表示对锁资源的占用情况。那么如何实现一个 int类型的state 同时来表示读写锁两种状态的占用情况呢? 这里实现非常巧妙,将4个字节的int类型, 32位拆分为2部分,高16位表示读锁的占用情况,低16位表示写锁的占用情况,这样读写锁互不影响,相互独立;也因此读写锁的最大值是2^16-1 = 65535,不能超过16位,下面源码有体现。

state值表示如图所示:

2、读锁是共享锁,只要不超过最大值,可多个线程同时获取; 写锁是排他锁,同一时刻最多允许一个线程获取。

写锁与其他锁都互斥,含写写互斥,写读互斥,读写互斥。

3、state可同时表示读写锁的状态,state的高16位表示获取读锁的线程数,读锁支持可重入,即一个线程也可多次获取读锁,怎么维护每个读锁线程的重入次数的? 每个线程有一个计数器 HoldCounter,用ThreadLocal来存放每个线程的计数器;state的低16位表示写锁的同步状态,因为写锁是排他锁,这里就不能表示获取写锁的线程数了,只能表示写锁的重入次数,获取写锁的线程可多次重复获取写锁(支持重入)。

读锁的计数器的实现原理如下:

可见ThreadLocalHoldCounter继承 ThreadLocal,每个获取读锁的线程是通过其本地变量来存储自己的计数器,来统计获取读锁的重入次数。ThreadLocal原理解析

    static final class ThreadLocalHoldCounter
            extends ThreadLocal<HoldCounter> {
          //重写了ThreadLocal的initialValue方法
            public HoldCounter initialValue() {
                return new HoldCounter();
            }
        }

4、state的高16位需要记录获取读锁的线程数,每增加一个线程获取读锁,在state的高16执行加1操作,即state+2^16,写锁增加重入次数,直接 state+1即可。

5、锁降级:获取写锁的线程,可以再次获取到读锁,即写锁降级为读锁。

? 读锁可以升级为写锁吗? 不可以,因为存在线程安全问题,试想获取读锁的线程有多个,其中一个线程升级为写锁,对临界区资源进行操作,比如修改了某个值,对其他已经获取读锁的线程不可见,出现线程安全问题。

代码演示:

1、读写状态

AQS(AbstractQueuedSynchronizer的简称)中同步状态字段 private volatile int state, int类型,4个字节,32位,拆分为高16位表示读状态,低16位表示写状态,如下定义了一些常量,实现获取读写锁的数量。

ReentrantReadWriteLock部分代码如下:

   //分隔位数,16位
     static final int SHARED_SHIFT   = 16;
   //读锁加1的数量,1左位移16位, (16)0x10000  = (2)1000000000000000= (10) 65536
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
  //读写锁的最大数量, (16)0xFFFFFFFF =(2)1111111111111111 =(10)65535
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
  //写锁的掩码,用于计算写锁重入次数时,将state的高16全部置为0, 等于(2)1111111111111111
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
   //获取读锁数,表示当前有多少个线程获取到读锁
   static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
  //获取写锁重入次数(不等于0表示有线程持有独占锁,大于1,表示写锁有重入)
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

分别看一下获取读写锁数量的方法。

获取占用读锁的线程数,代码如下:

 static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }

传入的c为 state,state 无符号右移16位,抹去低16位值,左边补0

示例图如下:

获取写锁的值的方法

  static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

与运算,将高16全部置为0,低16值代表写锁的值,&运算,相同为1,不同为0,得到低16位写锁值。

示例图如下:

2、三个锁概念

  1. int c =getState() ,获取state的值,代表同步锁状态,该值包含读写两个锁的同步状态
  2. int w = exclusiveCount(c); w代表写锁的同步状态,通过c获取到写锁的状态值
  3. int r = sharedCount(c); r 代表读锁的同步状态,通过c获取到读锁的状态值

以下分析三种情况下state,r, w 的值及代表的含义:

  • 1、一个线程获取到写锁:

state =1, w =1, r =0

获取写锁加1操作就比较简单了,因为写锁是独占锁,与正常的ReentrantLock获取锁实现一样,占用state的低16位表示,不用看state的高16,左边补16位0。获取写锁一次,直接 c+1;

  • 2、一个线程获取到读锁:

state =65536, w= 0, r=1

c初始为0 ,获取读锁,则读锁数量+1,执行 c + SHARED_UNIT, SHARED_UNIT = (2)1000000000000000 = (10)65536,括号内表示进制,SHARED_UNIT是每次读锁加1的数值。

如下图所示: 在获取读锁数量 r时,将state的低16位抹去,r=1,而state此时的值= 2^16 =65536,state的实际值可能会很大,但其实分别拆分读写锁的值不一定大,只是读锁值表示在高位,会造成state值很大。

  • 3、一个线程获取到写锁,又获取到读锁情况(锁降级):

state = 65537,w=1, r=1

state二进制表示: 00000000 00000001 00000000 00000001

锁降级代码演示如下:

package readwritelock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
 * @author zdd
 * 2019/12/30  上午
 * Description: 锁降级测试
 */
public class ReadWriteLockTest {
    static Integer shareVar = 0;
    public static void main(String[] args) {
        ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
        //1,首先获取写锁
        rw.writeLock().lock();
        //2.修改共享变量值
        shareVar = 10 ;
        //3.再获取读锁
        rw.readLock().lock();
        System.out.println("读取变量值 shareVar:"+ shareVar);
        //4.释放写锁
        rw.writeLock().unlock();
        //5.释放读锁
        rw.readLock().unlock();
    }
}

2、类结构和构造方法

ReentrantReadWriteLock 类中有ReadLock和WriteLock,分别对应读锁和写锁,而读写锁又分为公平方式和非公平方式获取锁。

简略类图结构如下:

构造方法如下:根据传入参数设置公平或者非公平获取锁方式,默认是非公平方式

  public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }

3、写锁

由于写锁是独占锁,由于写锁是独占锁,获取写锁的方式在AQS中已经说过了,详见AQS源代码分析, 只是每个子类的尝试获取锁方式不同,所以ReentrantReadWriteLock类获取写锁过程就看一下尝试获取锁方法的源码。

3.1、尝试获取锁

tryAcquire(int acquires),获取锁失败则加入同步队列中等待获取锁,源代码如下:

 protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
           //1,获取同步状态state的值,注意该值可表示读写锁的同步状态
            int c = getState();
          //2,获取写锁状态,低16位的值
            int w = exclusiveCount(c);
          //3,如果同步锁状态不为0,有线程已经获取到了锁
            if (c != 0) {
        //4,w==0则表示写锁为0,那么一定有线程获取了读锁,需要等待,读写互斥
 //current != getExclusiveOwnerThread() 当前线程不等于已经获取到写锁的线程,则也需等待其释放,写写互斥
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
       //5,此时再次获取锁,判断锁重入次数是否超过最大限定次数
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                //更新写锁重入次数
                setState(c + acquires);
                return true;
            }
      //6,代码执行这,一定是c==0,同步锁空闲情况
     //writerShouldBlock该方法是基于公平锁和非公平锁2种方式的体现
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
       //获取到锁,设置独占锁为当前写锁线程
            setExclusiveOwnerThread(current);
            return true;
        }

写锁是否应该阻塞等待

  • 1、 非公平锁方式
  final boolean writerShouldBlock() {
          //直接返回false
            return false; // writers can always barge
        }
  • 2、公平锁方式

需要判断同步队列中是否还有其他线程在挂起等待,如存在应该按照入队顺序获取锁

  final boolean writerShouldBlock() {
            return hasQueuedPredecessors();
        }
 public final boolean hasQueuedPredecessors() {
   //1.获取同步队列头,尾节点
        Node t = tail;
        Node h = head;
        Node s;
  // h !=t 同步队列不为空
  // 队列中还有其他线程在等待锁,则返回true
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

3.2、释放写锁

unlock方法释放锁

public void unlock() {
    sync.release(1);
}

可见,调用内部类Sync的release方法,Sync继承AQS

public final boolean release(int arg) {
    if (tryRelease(arg)) {
       //1,释放锁成功
        Node h = head;
        if (h != null && h.waitStatus != 0)
        //2.唤醒同步队列中等待线程
            unparkSuccessor(h);
        return true;
    }
    return false;
}

核心在尝试释放锁方法上,看看写锁的释放锁方法tryRelease

   protected final boolean tryRelease(int releases) {
           //1,判断当前线程是否持有当前锁
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            //2,同步状态 - 需要释放的写锁同步值
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
              //3,free ==true,完全释放写锁,将当前获取独占锁线程置空
                setExclusiveOwnerThread(null);
           //4,更新state值
            setState(nextc);
            return free;
        }

注: 在释放写锁占用次数时, state的高16的读锁有值也不影响,减去releases,首先减去的state低位的数,而且在释放写锁时,state的低16位的值一定>=1,不存在减少读锁的值情况。

   int nextc = getState() - releases;
   boolean free = exclusiveCount(nextc) == 0;

也可改写为如下面代码

//1,获取state值
int c = getState();
//2,获取写锁的值
int w= exclusiveCount(c);
int remain = w- releases;
boolean free = remain== 0;

4、读锁

4.1、获取读锁

读锁调用lock方法加锁,实际调用Sync的acquireShared方法

  public void lock() {
            sync.acquireShared(1);
        }

走进acquireShared,获取共享锁方法

 public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

尝试获取锁tryAcquireShared,如果返回值<0, 表示获取读锁失败

主要执行步骤:

1、首先判断是否存在其他线程在占用写锁,有需要挂起等待;

2、在不用阻塞等待,且读锁值没有超过最大值,cas更新成功了state的值,可以获取到读锁,还会做以下事:

? a. 第一个获取读锁的,直接记录线程对象和其重入获取读锁的次数

? b. 非第一个获取读锁的,则获取缓存计数器(cachedHoldCounter),其记录上一次获取读锁的线程,如果是同一个线程,则直接更新其计数器的重入次数,如果缓存计数器为空或缓存计数器的线程不是当前获取读锁的线程,则从当前线程本地变量中获取自己的计数器,更新计数器的值

  protected final int tryAcquireShared(int unused) {
          //1,获取当前线程对象
            Thread current = Thread.currentThread();
          //2,获取同步锁的值
            int c = getState();
         /*3,exclusiveCount(c) != 0 计算写锁的同步状态,不等于0,说明有写锁已经获取到同步锁,
          *需要判断当前线程是否等于获取写锁线程,
          *是,可以允许再次获取读锁,这里涉及到锁降级问题,写锁可以降为读锁
          *否则不让获取,写读互斥
         */
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
       //4,获取读锁同步状态
            int r = sharedCount(c);
    /**
      *此处3个判断条件
      * 1.是否应该阻塞等待,这里也是基于公平锁和非公平获取锁实现
      * 2.读锁同步状态值是超过最大值,即限制获取读锁的最大线程数
      * 3.cas更新读锁同步状态是否成功
      */
      if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
        //可以获取到读锁
         //r==0表示是第一个获取读锁的线程
                if (r == 0) {
                    firstReader = current;
                   //记录第一个线程读锁的重入次数
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                  //是第一个获取读锁线程,锁重入,锁重入次数+1
                    firstReaderHoldCount++;
                } else {
               // 已有其他线程获取到读锁
        /*
         *1,获取缓存记录的计数器,计数器是用来统计每一个获取读锁线程的重入次数的,
         *由每个线程的ThreadLocal,即线程内的副本存储,相互独立;
         *此处也不是放入缓存,在有多个线程同时获取读锁情况,
         *用一个变量记录上一个获取读锁的线程的计数器,可能考虑多次获取读锁线程大概率是同一个线程情况,
         *这样做是可提高执行效率
          */
                    HoldCounter rh = cachedHoldCounter;
          // rh==null,第一个获取读锁,rh没有值
   // 或者计数器存储的上一次线程的id与当前线程不等, 即不是相同一个线程,
  //那么就获取当前线程内部的计数器,并赋值给cachedHoldCounter变量,这样可以让下一次获取读锁线程获取比较了
       if (rh == null || rh.tid != getThreadId(current))
              cachedHoldCounter = rh = readHolds.get();
         else if (rh.count == 0)
  /*进入该条件,我理解是在线程获取读锁再释放后,同一线程再次获取读锁情况,
   * 缓存计数器会记录上一个线程计数器,因为线程释放读锁后,count=0,
   * 这里重新将计数器放入线程内部中,
   * 因为线程在使用完线程内部变量后会防止内存泄漏,会执行remove,释放本地存储的计数器。
   */
                readHolds.set(rh);
        //计数器+1
         rh.count++;
              }
                return 1;
            }
       //上面3个条件没有同时满足,没有成功获取到读锁,开始无限循环尝试去获取读锁
            return fullTryAcquireShared(current);
        }

无限循环尝试获取共享锁 fullTryAcquireShared方法

主要执行步骤:

1、 如果有其他线程获取到了写锁,写读互斥,应该去挂起等待;

2、如果可以获取读锁,判断是否应该阻塞等待,在公平获取锁方式中,同步队列中有其他线程在等待,则应该去排队按照FIFO顺序获取锁,非公平获取锁方式,可以直接去竞争获取锁。

3、可以获取锁,则尝试cas更新state的值,更新成功,获取到锁。

  final int fullTryAcquireShared(Thread current){
            HoldCounter rh = null;
        //无限循环
            for (;;) {
              //获取同步锁状态
                int c = getState();
               //判断写锁值不为0,且不是当前线程,不可获取读锁
                if (exclusiveCount(c) != 0) {
                    if (getExclusiveOwnerThread() != current)
                        return -1;
                } else if (readerShouldBlock()) {
               //没有线程获取到写锁情况,公平获取锁情况,
               //同步队列中有其他线程等待锁,该方法主要是在需要排队等待,计数器重入次数==0情况,清除计数器
                    if (firstReader == current) {
               //此处firstReader !=null, 则第1个获取读锁的线程还没释放锁,可允许该线程继续重入获取锁
               //计数器count一定>0
                    } else {
                        if (rh == null) {
                            rh = cachedHoldCounter;
                            if (rh == null || rh.tid != getThreadId(current)) {
                                rh = readHolds.get();
                                if (rh.count == 0)
                                  //清除计数器
                                    readHolds.remove();
                            }
                        }
              // 为什么rh.count == 0就不让线程获取到锁了,基于公平获取锁方式,去同步队列中等待
                        if (rh.count == 0)
                            return -1;
                    }
                }
                //获取读锁线程超过最大限制值 65535
                if (sharedCount(c) == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
               // cas执行读锁值+1
                if (compareAndSetState(c, c + SHARED_UNIT)) {
                    if (sharedCount(c) == 0) {
                      //1,第一个获取读锁
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                      //2,第一个获取读锁重入
                        firstReaderHoldCount++;
                    } else {
                      //3,非第一个线程获取读锁,存在多个线程获取读锁
                        if (rh == null)
                            rh = cachedHoldCounter;
                        if (rh == null || rh.tid != getThreadId(current))
                            rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                      //缓存计数器变量记录此次获取读锁线程的计数器
                        cachedHoldCounter = rh; // cache for release
                    }
                    return 1;
                }
            }
        }

tryAcquireShared 返回< 0, 获取锁失败,执行 doAcquireShared

在获取读锁失败后,执行以下步骤:

1、将节点加入同步队列中

2、如果前置节点是头节点,将再次尝试获取锁,如果成功,设置当前节点为head节点,并根据tryAcquireShared方法的返回值r判断是否需要继续唤醒后继节点,如果 r大于0,需要继续唤醒后继节点,r=0不需要唤醒后继节点。

3、如果前置节点不是头节点,则在队列中找到安全位置,设置前置节点 ws=SIGNAL, 挂起等待。

private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                   //如果前继节点是头节点,再次尝试获取共享锁
                    int r = tryAcquireShared(arg);
                   //r>=0,表示获取到锁,
                   //r=0,表示不需要唤醒后继节点
                   //r>0,需要继续唤醒后继节点
                    if (r >= 0) {
                       //该方法实现2个步骤
                       //1,设置当前节点为头节点
                       //2,r>0情况会继续唤醒后继节点
                        setHeadAndPropagate(node, r);
                      //旧的头节点移出队列
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

setHeadAndPropagate 该方法是与独占锁获取锁的区别之处,获取到锁后,设置为头结点还需要继续传播下去。

private void setHeadAndPropagate(Node node, int propagate) {
   //记录是的旧的头节点
   Node h = head; // Record old head for check
   //设置当前获取到锁节点为头节点
    setHead(node);
   //propagate >0,表示还需要继续唤醒后继节点
   //旧的头节点和新头节点为空,或者ws<0,满足条件之一,尝试去唤醒后继节点
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
       //后继节点为空或者是共享节点(获取读锁的线程)
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

doReleaseShared 方法较难理解,在释放锁中也有调用,留着后面一起分析。

4.2、释放读锁

public void unlock() {
    sync.releaseShared(1);
}

AQS中释放共锁方法releaseShared

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

看一下读写锁具体实现tryReleaseShared 的方法

protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
  //1,更新或者移出线程内部计数器的值
    if (firstReader == current) {
        //当前线程是第一个获取读锁的线程
        if (firstReaderHoldCount == 1)
          //直接置空
            firstReader = null;
        else
          //该线程获取读锁重入多次,计数器-1
            firstReaderHoldCount--;
    } else {
        HoldCounter rh = cachedHoldCounter;
        if (rh == null || rh.tid != getThreadId(current))
            rh = readHolds.get();
        int count = rh.count;
        if (count <= 1) {
          //非第一个获取读锁线程,避免ThreadLocal内存泄漏,移出计数器
            readHolds.remove();
            if (count <= 0)
             //此处是调用释放锁次数比获取锁次数还多情况,直接抛异常
                throw unmatchedUnlockException();
        }
        --rh.count;
    }
   //2,循环cas更新同步锁的值
    for (;;) {
        int c = getState();
        //读锁同步状态-1
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            // Releasing the read lock has no effect on readers,
            // but it may allow waiting writers to proceed if
            // both read and write locks are now free.
          //返回完全释放读锁,读锁值是否==0,完全释放,等待写锁线程可获取
            return nextc == 0;
    }
}

tryReleaseShared 返回true情况,表示完全释放读锁,执行doReleaseShared,那就需要唤醒同步队列中等待的其他线程

在读写锁中存在几种情况

情况一、如果当前获取锁的线程占用的是写锁,则后来无论是获取读锁还写锁的线程都会被阻塞在同步队列中,

同步队列是FIFO队列,在占用写锁的释放后,node1获取读锁,因读锁是共享的,继续唤醒后一个共享节点。

如上图,在node1获取到读锁时,会调用doReleaseShared方法,继续唤醒下一个共享节点node2,可以持续将唤醒动作传递下去,如果node2后面还存在几个等待获取读锁的线程,这些线程是由谁唤醒的?是其前置节点,还是第一个获取读锁的节点? 应该是第1个获取锁的节点,这里即node1, 由下代码可见,在无限循环中,只有头节点没有变化时,即再没其他节点获取到锁后,才会跳出循环。

private void doReleaseShared() {
    for (;;) {
      //获取同步队列中头节点
        Node h = head;
      //同步队列中节点不为空,且节点数至少2个
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            //1,表示后继节点需要被唤醒
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
               //唤醒后继节点
                unparkSuccessor(h);
            }
           //2,后继节点暂时不需要唤醒,设置节点 ws = -3, 确保后面可以继续传递下去
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        //如果头节点发生变化,表示已经有其他线程获取到锁了,需要重新循环,确保可以将唤醒动作传递下去。
        if (h == head)                   // loop if head changed
            break;
    }
}

5、思考

1、在非公平获取锁方式下,是否存在等待获取写锁的线程始终获取不到锁,每次都被后来获取读锁的线程抢先,造成饥饿现象?

存在这种情况,从获取读锁源码中看出,如果第一个线程获取到读锁正在执行情况下,第二个等待获取写锁的线程在同步队列中挂起等待,在第一个线程没有释放读锁情况下,又陆续来了线程获取读锁,因为读锁是共享的,线程都可以获取到读锁,始终是在读锁没有释放完毕加入获取读锁的线程,那么等待获取写锁的线程是始终拿不到写锁,导致饥饿。为什么默认还是非公平模式?因为减少线程的上下文切换,保证更大的吞吐量。

6、总结

1、读写锁可支持公平和非公平两种方式获取锁。

2、支持锁降级,写锁可降级为读锁,但读锁不可升级为写锁。

3、大多数场景是读多于写的,所以ReentrantReadWriteLock 比 ReentrantLock(排他锁)有更好的并发性能和吞吐量。

4、读写锁中读锁和写锁都支持锁重入。

5、在获取Condition对象实现阻塞唤醒机制,ReentrantReadWriteLock.WriteLock 重写了 newCondition方法,ReadLock不支持,即读锁不支持与Condition配合使用,使用阻塞唤醒机制。

原文地址:https://www.cnblogs.com/flydashpig/p/12121953.html

时间: 2024-09-27 12:22:56

多线程之美7一ReentrantReadWriteLock源码分析的相关文章

多线程之美5一 AbstractQueuedSynchronizer源码分析&lt;一&gt;

AQS的源码分析 目录结构 1.什么是CAS ? 2.同步器类结构 3.CLH同步队列 4.AQS中静态内部类Node 5.方法分析 ? 5.1.acquire(int arg ) ? 5.2.release(int arg) 释放锁 6.总结 前言 在多线程环境下,我们一般会对临界区资源(共享资源)进行加锁,释放锁,保证同一时刻最多只有一个线程(独占模式),就如去公共厕所里,在使用一个小房间时会加锁避免自己在使用的时候,别人突然闯进来一样,引起不必要的麻烦,在使用完后,再打开锁,其他人才可使用

Java显式锁学习总结之五:ReentrantReadWriteLock源码分析

概述 我们在介绍AbstractQueuedSynchronizer的时候介绍过,AQS支持独占式同步状态获取/释放.共享式同步状态获取/释放两种模式,对应的典型应用分别是ReentrantLock和Semaphore,AQS还可以混合两种模式使用,读写锁ReentrantReadWriteLock就是如此. 设想以下情景:我们在系统中有一个多线程访问的缓存,多个线程都可以对缓存进行读或写操作,但是读操作远远多于写操作,要求写操作要线程安全,且写操作执行完成要求对当前的所有读操作马上可见. 分析

ReentrantReadWriteLock源码分析(一)

此处源码分析,主要是基于读锁,非公平机制,JDK1.8. 问题: 1.ReentrantReadWriteLock是如何创建读锁与写锁? 2.读锁与写锁的区别是什么? 3.锁的重入次数与获取锁的线程数分别是用哪种方式记录的? 4.当队列中出现多个共享模式的线程节点连续排列时,那么当第一个共享模式的线程拿到锁之后,后面的共享线程节点怎么获取锁? 一.创建ReadLock. ReentrantReadWriteLock rrw = new ReentrantReadWriteLock(); publ

ReentrantReadWriteLock源码分析

概述 ReentrantReadWriteLock维护了一对相关的锁,它们分别是共享readLock和独占writeLock.关于共享读锁和排他写锁的概念其实很好理解.所谓共享读锁就是一个线程读的时候,其它线程也可以来读(共享),但是不能来写.排他写锁是指一个线程在写的时候,其它线程不能来写或读(排他).除了这个特点之外,ReentrantReadWriteLock还有一个特点就是可重入的.它和ReentrantLock一样都是支持Condition的.而且ReentrantReadWerite

java并发锁ReentrantReadWriteLock读写锁源码分析

1.ReentrantReadWriterLock基础 所谓读写锁,是对访问资源共享锁和排斥锁,一般的重入性语义为 如果对资源加了写锁,其他线程无法再获得写锁与读锁,但是持有写锁的线程,可以对资源加读锁(锁降级):如果一个线程对资源加了读锁,其他线程可以继续加读锁. java.util.concurrent.locks中关于多写锁的接口:ReadWriteLock public interface ReadWriteLock { /** * Returns the lock used for r

Python之美[从菜鸟到高手]--浅拷贝、深拷贝完全解读(copy源码分析)

可悲的我一直以为copy模块是用C写的,有时候需要深入了解deepcopy,文档描述的实在太简单,还是不知所云. 比如说最近看sqlmap源码中AttribDict的_deepcopy__有些疑惑, def __deepcopy__(self, memo): retVal = self.__class__() memo[id(self)] = retVal for attr in dir(self): if not attr.startswith('_'): value = getattr(se

Java多线程 -- JUC包源码分析11 -- ThreadPoolExecutor源码分析

在JUC包中,线程池部分本身有很多组件,可以说是前面所分析的各种技术的一个综合应用.从本文开始,将综合前面的知识,逐个分析线程池的各个组件. -Executor/Executors -ThreadPoolExecutor使用介绍 -ThreadPoolExecutor实现原理 –ThreadPoolExecutor的中断与优雅关闭 shutdown + awaitTermination –shutdown的一个误区 Executor/Executors Executor是线程池框架最基本的几个接

干货:Java多线程详解(内附源码)

线程是程序执行的最小单元,多线程是指程序同一时间可以有多个执行单元运行(这个与你的CPU核心有关). 在java中开启一个新线程非常简单,创建一个Thread对象,然后调用它的start方法,一个新线程就开启了. 那么执行代码放在那里呢?有两种方式:1. 创建Thread对象时,复写它的run方法,把执行代码放在run方法里.2. 创建Thread对象时,给它传递一个Runnable对象,把执行代码放在Runnable对象的run方法里. 如果多线程操作的是不同资源,线程之间不会相互影响,不会产

[源码分析]读写锁ReentrantReadWriteLock

一.简介 读写锁. 读锁之间是共享的. 写锁是独占的. 首先声明一点: 我在分析源码的时候, 把jdk源码复制出来进行中文的注释, 有时还进行编译调试什么的, 为了避免和jdk原生的类混淆, 我在类前面加了"My". 比如把ReentrantLock改名为了MyReentrantLock, 在源码分析的章节里, 我基本不会对源码进行修改, 所以请忽视这个"My"即可. 1. ReentrantReadWriteLock类里的字段 unsafe在这里是用来给TID_O