ReentrantReadWriteLock 源码学习

是什么

读写锁在同一时刻可以允许多个读线程访问,但是写线程操作时,所有的读线程和其他写线程均被阻塞。

使用例子

public class LockTest {
    private static ReadWriteLock lock = new ReentrantReadWriteLock();
     /**
     * 读取资源
     */
    public void read() throws InterruptedException {
        lock.readLock().lock();
        try{
           readSomething();
        }finally {
            lock.readLock().unlock();
        }
    }
    /**
     * 修改资源
     */
    public void write() throws InterruptedException {
        lock.writeLock().lock();
        try{
            writeSomething();
        }finally {
            lock.writeLock().unlock();
        }
    }
}

对比于排他锁

排他锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,其他线程会被阻塞,对于读多写少的场景并发性高。

其他方案(等待/通知)

等待/通知。在Java 5之前没有读写锁时候,当写操作开始时,所有晚于写操作的读操作均会进入等待状态,写操作完成并进行通知之后,所有等待的读操作才能继续执行。写操作需要通过synchronized进行同步。

继承结构

继承了ReadWriteLock接口,接口中只有两个方法
在这里插入图片描述

接口结构

在这里插入图片描述

  • Lock readLock();

返回用于读取的锁。

  • Lock writeLock();

返回用于写入的锁。

内部结构

按照惯例,先看一下内部结构,大概有个意识。
内部聚合了WriteLock,ReadLock。又都聚合了Sync(继承AQS)。
其中又有FairSync,NonfairSync 公平锁与非公平锁,都继承与Sync。
内部持有两把锁(读锁与写锁),并且根据Sync来实现功能,其中又分为公平锁方式与非公平锁方式。
在这里插入图片描述

/** 提供读锁的内部类 */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** 提供写锁的内部类 */
private final ReentrantReadWriteLock.WriteLock writerLock;
/** 执行所有同步机制 */
final Sync sync;

Sync

获取写锁(独占锁)

AQS中被volatile修饰的代表锁状态的变量,在读写锁中被拆成了两半,高16位代表读锁的获取情况,低16位代表写锁的获取情况
在这里插入图片描述
1.获取写锁的状态
2.如果锁被其他线程获取,那么返回false
3.锁没有被获取,询问是否需要阻塞,如果需要阻塞,返回false,入队寻找时机休眠
4.不需要阻塞则尝试CAS获取锁

protected final boolean tryAcquire(int acquires) {
    
    Thread current = Thread.currentThread();
    int c = getState();
    //获取写锁的状态(同步状态值的低16位)
    int w = exclusiveCount(c);

    //1.锁被获取的情况(可能读锁,也能写锁)
    if (c != 0) {
        //如果写锁的状态为0,那么直接返回false
        if (w == 0
            //判断一下持有锁的线程,是否是当前线程,(需要考虑锁的重入)
            || current != getExclusiveOwnerThread())
            return false;
        //重入次数过多,抛出异常,(因为状态state只有一半表示写锁的状态)
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        //锁重入
        setState(c + acquires);
        return true;
    }

    //2.锁没有被获取,询问是否需要阻塞(公平锁还是非公平锁处理方式不一致,也就是在这里体现出公平与非公平锁)。
    //writerShouldBlock公平锁与非公平锁的判断情况不一致。后续分析具体差别
    if (writerShouldBlock() ||
        //假设writerShouldBlock返回false,不需要阻塞。就会去尝试CAS修改同步状态。这里的c必定是0,acquires是1
        //CAS成功说明没有线程竞争,那么代表锁的状态c的值,从0->1,成功获取到锁
        !compareAndSetState(c, c + acquires))
        //线程需要进入同步队列,或者CAS失败,需要返回false,让线程进入同步队列(AQS模板方法)
        return false;
    
    //3.成功获取到锁,设置持有锁的线程为当前线程
    setExclusiveOwnerThread(current);
    return true;
}

**获取写锁的状态(同步状态值的低16位)**在读写锁中,将同步状态值一分为二,高十六位为读的状态,低16位为写锁的状态。

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

static final int EXCLUSIVE_MASK = (1 << 16) - 1;
1左移动16位,再减1
1: 0000 0000 0000 0000 0000 0000 0000 0001 1
左移29位: 0000 0000 0000 0001 0000 0000 0000 0000
减1: 0000 0000 0000 0000 1111 1111 1111 1111
任何数&EXCLUSIVE_MASK,舍弃高16位,保留低16位,得到写锁被线程获取的状态

是否需要进入同步队列的判断(公平锁/非公平锁的体现)

队头节点等于当前线程允许直接CAS
队头节点是当前线程需要进入AQS的同步队列

final boolean writerShouldBlock() {
    Node t = tail; 
    Node h = head;
    Node s;
    if(h != t){
        //false: 头节点后继不为null,并且后继节点等于当前线程(队头)
        //true: 头节点后继不为null或者后继节点不等于当前节点(不在队头)
        return (s = h.next) == null || s.thread != Thread.currentThread()
    } else {
        //队列都空了,允许直接CAS
        return false;
    }
}

非公平锁永远都允许先直接CAS,而不用入同步队列

final boolean writerShouldBlock() {
    return false;
}

不允许直接CAS,或者CAS失败进入同步队列等待

tryAcquire竞争锁失败了,线程构造成节点进入AQS的同步队列中。
后续情况在ReentrantLock中分析过了,不再赘述

public final void acquire(long arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

释放写锁

1.将同步状态值减1
2.返回锁是否全部释放完毕

protected final boolean tryRelease(int releases) {
    //只有持有锁的线程才能释放锁,否则抛出错误
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    //状态减一
    int nextc = getState() - releases;
    //判断锁是否全部释放完毕(可重入锁可以多次获取锁)
    //低16位表示写锁的状态
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        //清除锁的被持有线程
        setExclusiveOwnerThread(null);
    setState(nextc);
    //只有全部释放完才会返回true
    return free;
}

获取读锁(共享锁)

tryAcquireShared在AQS也是未实现的方法,留给子类去实现。典型的模板设计模式

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

1.如果写锁被其他线程获取,直接返回-1,乖乖排队去。
2.判断是否需要阻塞,如果需要阻塞直接进入到fullTryAcquireShared方法,这个方法后面再看有对阻塞做出处理
3.不需要阻塞就一直尝试CAS,CAS成功做一些记录工作。失败进入入fullTryAcquireShared方法

protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    //1.写锁被其他线程获取了,直接返回-1,未获得锁
    if (exclusiveCount(c) != 0 &&
        //并且保证线程的拥有者不是自身,后续会讲解到锁降级的情况,要考虑到
        getExclusiveOwnerThread() != current)
        return -1;
    //2.运行到这儿说明没有线程获取写锁,或者拥有锁的人是自身
    //获取读锁的数量
    int r = sharedCount(c);
    
    //(公平锁还是非公平锁处理方式不一致,也就是在这里体现出公平与非公平锁),假设false执行CAS
    if (!readerShouldBlock() &&
        //获取读锁的线程数量是否超过最大值
        r < MAX_COUNT &&
        //CAS设置同步状态值,+ SHARED_UNIT是因为高16位才表示读锁的个数
        compareAndSetState(c, c + SHARED_UNIT)) {
        //设置同步状态值成功,做一些记录工作(暂时不用去看)
        if (r == 0) {
            // firstReader是把读锁状态从0变成1的那个线程(第一个获取读锁的线程)
            firstReader = current;
            firstReaderHoldCount = 1;
        //当前线程是第一次获取读锁的线程重入时,直接使用firstReaderHoldCount进行++
        } else if (firstReader == current) {
            firstReaderHoldCount++;
        } else { //从ThreadLocal中获取当前线程重入读锁的次数,然后自增下。
            //缓存的上一个线程的HoldCounter
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                //不是同一个线程,从线程的ThreadLocal中取
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                //同一个线程,将缓存中的HoldCounter设置到线程的ThreadLocal
                readHolds.set(rh);
            //获取到读锁+1(rh是保存在各自线程中的一份副本,其他线程不可访问,只有自身线程可以访问)
            //这个else中做的东西其实就是讲各自线程获取锁的次数记录到ThreadLocal中
            rh.count++;
        }
        //竞争到读锁,返回true,不需要进入AQS的同步队列
        return 1;
    }
    //3.补偿循环尝试
    return fullTryAcquireShared(current);
}

公平锁获取读锁

与获取写锁一致

final boolean readerShouldBlock() {
    Node t = tail; 
    Node h = head;
    Node s;
    if(h != t){
        //false: 头节点后继不为null,并且后继节点等于当前线程(队头)
        //true: 头节点后继不为null或者后继节点不等于当前节点(不在队头)
        return (s = h.next) == null || s.thread != Thread.currentThread()
    } else {
        //队列都空了,允许直接CAS
        return false;
    }
}

非公平锁获取读锁

如果同步队列队头节点为写线程的话,那么就需要进入同步队列。
为了避免写线程无限期地饿死
试想一下,写线程在同步队列中等待,被持有的是读锁,持续的有读线程都可以获取读锁,那么写线程将很难获得写多,最终造成饥饿

final boolean readerShouldBlock() {
    Node h, s;
    //true:需要入同步队列,头队列非空,队头节点为写节点
    //false:不需要入同步队列  队列为空,或者队头节点为读节点
    return 
        //头结点不等于null
        (h = head) != null &&
        //后继节点不为null
        (s = h.next)  != null &&
        //后继节点不是读节点
        !s.isShared() &&
        //后继节点线程不为空
        s.thread != null;
}

fullTryAcquireShared

在什么情况下会进入到fullTryAcquireShared

  1. 目前没有线程获取写锁(有的话直接进入同步队列排队了)
  2. 或者CAS失败了。其上情况并不能确定线程一定没有拥有获取锁的资格。(有可能是重入锁的情况或者有其他读线程在竞争CAS)
    fullTryAcquireShared作为一个补偿方法
    方法中代码中对需要阻塞的情况做了处理如果是CAS失败,则会通过循环进行CAS的尝试。
final int fullTryAcquireShared(Thread current) {
   
    HoldCounter rh = null;
    for (;;) {
        int c = getState();
         //写锁被其他线程获取了,直接返回-1,表示失败(代码一致)
        if (exclusiveCount(c) != 0) {
            if (getExclusiveOwnerThread() != current)
                return -1;
                
        //对于需要进入同步队列的处理(公平锁非公平锁处理不一致)
        } else if (readerShouldBlock()) {
            // 如果这个线程是第一个获取读锁的线程,线程一定属于重入,直接放行进入循环CAS
            //与上述代码一致,不再赘述
            if (firstReader == current) {
                
            } else {
                if (rh == null) {
                    rh = cachedHoldCounter;
                    //先看本地缓存是否为Null,或者与当前线程id不一致
                    //进行一个ThreadLocal初始化
                    if (rh == null || rh.tid != getThreadId(current)) {
                        rh = readHolds.get();
                        if (rh.count == 0)
                            readHolds.remove();
                    }
                }
                //线程并不是重入线程,并且已经判断需要进入同步队列
                if (rh.count == 0)
                    return -1;
            }
        }
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
            
        //运行到这一步说明不需要进入AQS的同步队列,或者是进行锁的重入
        if (compareAndSetState(c, c + SHARED_UNIT)) {
             //CAS成功做一些记录
            if (sharedCount(c) == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                firstReaderHoldCount++;
            } else {
                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;
        }
    }
}

读锁的获取相对会比较麻烦

  1. 先判断是否有线程获得写锁,如果有那么直接进入同步队列,寻找休眠的机会线程休眠
  2. 如果写锁没有被获取,根据锁的类型判断线程是否应该被阻塞
  3. 如果不需要阻塞的话,则自旋CAS设置读锁的状态,并且将线程获取读锁的情况记录在ThreadLocal中

释放读锁(共享锁)

protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    //第一个获取读锁的线程
    if (firstReader == current) {
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--;
    } else {
        //将线程中存在ThreadLocal减一处理
        HoldCounter rh = cachedHoldCounter;
        if (rh == null || rh.tid != getThreadId(current))
            rh = readHolds.get();
        int count = rh.count;
        if (count <= 1) {
            readHolds.remove();
            if (count <= 0)
                throw unmatchedUnlockException();
        }
        --rh.count;
    }
    //循环CAS修改同步值状态
    for (;;) {
        int c = getState();
        //高16位代表读锁的状态
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            //==0也没有线程获取写锁
            return nextc == 0;
    }
}

结构

在这里插入图片描述

  • static final class HoldCounter

用来记录线程已经获取了多少次读锁的内部类

  • static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter>

ThreadLocal,并且泛型指定位HoldCounter,初始化值为0。

以上两个内部类,用来实现读锁的可重入功能,如果值为0那么说明线程在之前没有获取读锁,反之线程已经是获取了读锁的并且没有释放。

核心属性

  • private transient ThreadLocalHoldCounter readHolds;

ThreadLocal。当前线程持有的重入读锁数目。每重入一次就会++。每释放一次–,当线程的hold count 变为0时被移除。

  • private transient HoldCounter cachedHoldCounter;

相对于当前最后一个获取读锁的线程

  • private transient Thread firstReader = null;
    private transient int firstReaderHoldCount;

firstReader 是第一个获取了读锁的线程(没有释放锁)。firstReaderHoldCount 是 firstReader 的 hold count(即重入次数)

WriteLock 写锁

方法代理到Sync获取独占锁,释放独占锁。按照AQS的模板方法,如果获取锁失败,那么进入同步队列,等待唤醒。
AQS释放锁,唤醒同步队列中的后继线程。

  1. lock() 加锁
public void lock() {
    sync.acquire(1);
}
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
  1. unlock() 释放锁
public void unlock() {
    sync.release(1);
}
//AQS释放锁
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

ReadLock 读锁

其行为都是代理到Sync的,获取共享锁,释放独占锁。

  1. lock() 加锁
public void lock() {
    sync.acquireShared(1);
}
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}
//以共享的不间断模式进行获取
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) {
                //Sync实现的以为共享的方式获取锁
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    //设置队列的头部,并检查后继者是否为共享模式(读锁),是的话也进行唤醒
                    setHeadAndPropagate(node, r);
                    p.next = null; 
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
  1. unlock() 释放锁
public void unlock() {
    sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        //唤醒后继线程,检查后继者是否为共享模式(读锁),是的话也进行唤醒
        doReleaseShared();
        return true;
    }
    return false;
}

锁降级

锁降级指的是写锁降级成为读锁。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。

public void processData() {
    // 锁降级从写锁获取到开始
    writeLock.lock();
    try {
        write();
        readLock.lock();
    } finally {
        writeLock.unlock();
    }
    // 锁降级完成,写锁降级为读锁
    try {
        read();
    } finally {
            readLock.unlock();
    }
}

锁降级中读锁的获取是否必要呢?答案是必要的。主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值