ReentrantReadWriteLock底层源码

声明:本文为作者原创,如若转发,请指明转发地址

1、ReentranReadWriteLock示例

当读操作远远高于写操作时,这时候使用 读写锁 让 读-读 可以并发,读-写和写-写互斥,提高性能。
提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法

读锁不是独占式锁,即同一时刻该锁可以被多个读线程获取也就是一种共享式锁。现共享式同步组件的同步语义需要通过重写AQS的tryAcquireShared方法和tryReleaseShared方法。

@Slf4j(topic = "c.TestReadWriteLock")
public class TestReadWriteLock {
    public static void main(String[] args) throws InterruptedException {
        DataContainer dataContainer = new DataContainer();
        new Thread(() -> {
            dataContainer.read();
        }, "t1").start();

        new Thread(() -> {
            dataContainer.read();
        }, "t2").start();
    }
}

@Slf4j(topic = "c.DataContainer")
class DataContainer {
    private Object data;
    private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
    private ReentrantReadWriteLock.ReadLock r = rw.readLock();
    private ReentrantReadWriteLock.WriteLock w = rw.writeLock();

    public Object read() {
        log.debug("获取读锁...");
        r.lock();
        try {
            log.debug("读取");
            sleep(1);
            return data;
        } finally {
            log.debug("释放读锁...");
            r.unlock();
        }
    }

    public void write() {
        log.debug("获取写锁...");
        w.lock();
        try {
            log.debug("写入");
            sleep(1);
        } finally {
            log.debug("释放写锁...");
            w.unlock();
        }
    }
}

结果:读锁没有释放时,其他线程就可以获取读锁

09:29:07.134 c.DataContainer [t2] - 获取读锁...
09:29:07.134 c.DataContainer [t1] - 获取读锁...
09:29:07.134 c.DataContainer [t1] - 读取
09:29:07.134 c.DataContainer [t2] - 读取
09:29:08.146 c.DataContainer [t1] - 释放读锁...
09:29:08.146 c.DataContainer [t2] - 释放读锁...

2、ReentrantReadWriteLock底层原理

读写锁用的是同一个 Sycn 同步器,因此等待队列、state 等也是同一个

1、t1 w.lock,t2 r.lock

ReentrantReadWriteLock类结构:
在这里插入图片描述

1、写锁上锁流程

1、acquire(arg)方法

public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
    //内部类 WriteLock类
    public static class WriteLock implements Lock, java.io.Serializable {
    	private final Sync sync;
        //通过WriteLock类对象w调用该方法
        public void lock() {
            sync.acquire(1);
        } 
    }
}

2、tryAcquire(arg)方法

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer{
     public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
}

在这里插入图片描述

由于t1线程第第一个获取锁的线程,因此 t1 成功上锁,流程与 ReentrantLock 加锁相比没有特殊之处,不同是写锁状态占了 state 的低 16 位,而读锁使用的是 state 的高 16 位

在这里插入图片描述

protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    //1、获取写锁当前的同步状态,即锁状态的低16位
    int c = getState();
    //2、获取写锁获取的次数
    int w = exclusiveCount(c);
    //如果写锁状态state!=0,说明写锁已经被其他线程获取
    if (c != 0) {
        //如果获取写锁的线程不是当前线程,获取写锁失败
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        //写锁计数超过低 16 位, 报异常
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        //写锁重入, 获得锁成功
        setState(c + acquires);
        return true;
    }
    //判断写锁是否应该阻塞
    if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
        //获取锁失败
        return false;
    //获取锁成功,设置当前线程为独占线层
    setExclusiveOwnerThread(current);
    return true;
}

这里需要注意一点:int w = exclusiveCount(c);表示写锁获取的次数

/**
    该方法是获取读锁被获取的次数,是将同步状态(int c)右移16次,即取同步状态的高16位,
    结论:同步状态的高16位用来表示读锁被获取的次数。
*/
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
/**
	将同步状态(state为int类型)与0x0000FFFF相与,即获取同步状态的低16位,即写锁被获取的次数,
	结论:同步状态的低16位用来表示写锁的获取次数。
*/
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

在这里插入图片描述

总结:同步状态的低16位用来表示写锁的获取次数,同步状态的高16位用来表示读锁的获取状态。

当写锁已经被其他线程获取,就返回false,继续执行下面的逻辑。否则,获取锁成功并支持可重入锁,更新获取锁的次数。

3、writerShouldBlock()方法

该方法时Sync类中的抽象方法,有公平锁和非公平锁两种实现方式:
在这里插入图片描述

对于非公平锁:

static final class NonfairSync extends Sync {
    //对于非公平锁总是返回false,不需要阻塞
    final boolean writerShouldBlock() {
        return false; 
    }
}

对于公平锁:

static final class FairSync extends Sync {
    //对于公平锁,需要判断
    final boolean writerShouldBlock() {
        return hasQueuedPredecessors();
    }
}

2、读锁上锁流程

1、acquireShared(arg)方法

public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
	public static class ReadLock implements Lock, java.io.Serializable {
        //...
        //调用读锁的lock()方法
        public void lock() {
            sync.acquireShared(1);
        }
}

2、tryAcquireShared(arg)方法

t2 执行 r.lock,这时进入读锁的 sync.acquireShared(1) 流程,首先会进入 tryAcquireShared 流程。如果有写锁占据并且获取写锁的线程不是当前线程,那么 tryAcquireShared 返回 -1 表示失败

在这里插入图片描述

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer {
    public final void acquireShared(int arg) {
        //tryAcquireShared 返回负数, 表示获取读锁失败
        if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
    }
}

在这里插入图片描述

protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    /**
    	就是说如果其他线程已经获取了写锁,并且获取写锁的线程不是当前线程,那么读锁就会获取失败
    	这句话说明当前线程获取写锁之后仍然能够获取读锁
    */
    if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
        //获取锁失败
        return -1;
   
    int r = sharedCount(c);
    //读锁不该阻塞(如果老二是写锁,读锁该阻塞) && 读锁被获取的次数小于读锁计数 && 尝试加锁成功 
    if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)){
        //获取锁成功
        return 1;
    }
    
    //上面CAS获取读锁失败后,尝试循环获取
    return fullTryAcquireShared(current);
}

3、readerShouldBlock()方法

这个方法对于公平锁和非公平锁的实现是不同的,也就导致了ReentrantReadWriteLock()对于公平和非公平的两种不同实现:

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
    abstract static class Sync extends AbstractQueuedSynchronizer {
        abstract boolean readerShouldBlock();
        abstract boolean writerShouldBlock();
}

对于readerShouldBlock()这个抽象方法,公平锁和非公平锁都有实现,具体实现有所不同。

对于非公平锁:

static final class NonfairSync extends Sync {
	//...
    final boolean readerShouldBlock() {
/**
	看 AQS 队列中第一个节点是否是写锁,true 则该阻塞, false 则不阻塞:
  	由于非公平的竞争,并且读锁可以共享,所以可能会出现源源不断的读,使得写锁永远竞争不到,然后出现饿死的现象
    通过这个策略,当一个写锁出现在头结点后面的时候,会立刻阻塞所有还未获取读锁的其他线程,让步给写线程先执行
*/
        return apparentlyFirstQueuedIsExclusive();
    }
}

对于公平锁:

static final class FairSync extends Sync {
	//...
    final boolean readerShouldBlock() {
    	//对于公平锁来说,如果有前驱(也就是非头结点),都会进行等待,不允许竞争锁
        return hasQueuedPredecessors();
    }
}

如果获取读锁获取失败,就会继续执行下面的doAcquireShared(arg)方法:想象成acquireQueued()方法

4、doAcquireShared(arg)方法

如果t2线程获取锁失败,这时会进入doAcquireShared(1) 流程,首先也是调用 addWaiter 添加节点,不同之处在于节点被设置为Node.SHARED 模式而非 Node.EXCLUSIVE 模式,注意此时 t2 仍处于活跃状态 。

在这里插入图片描述

在该方法中,t2 会看看自己的节点是不是老二,如果是,还会再次调用 tryAcquireShared(1) 来尝试获取锁,如果获取锁没成功,在 doAcquireShared 内 for (;😉 循环一次,把前驱节点的 waitStatus 改为 -1,再 for (;😉 循环一次尝试 tryAcquireShared(1) 如果还不成功,那么在 parkAndCheckInterrupt() 处 park

在这里插入图片描述

注意:如果t2线程执行tryAcquireShared(arg)方法获取锁失败,那么总共会在doAcquireShared(arg)方法中执行3次doAcquireShared(1) 方法获取锁,如果还没有成功,就会进入阻塞状态

private void doAcquireShared(int arg) {
    //将当前线程关联到一个 Node 对象上, 模式为共享模式,加入到同步队列的队尾
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        //死循环,CAS自旋的方式尝试获取锁
        for (;;) {
            //获取当前节点的前驱节点
            final Node p = node.predecessor();
            //t2 会看看自己的节点是不是老二,如果是,还会再次调用 tryAcquireShared(1) 来尝试获取锁
            if (p == head) {
                int r = tryAcquireShared(arg);
                //如果获取锁成功
                if (r >= 0) {
                    //设置新的head节点,并(唤醒 AQS 中下一个 Share 节点)
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            //获取锁失败后是否应该被阻塞,如果需要阻塞,就调用 parkAndCheckInterrupt()方法阻塞当前线程
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

2、t3 r.lock, t4 w.lock

这种状态下,假设又有 t3 加读锁和 t4 加写锁,这期间 t1 仍然持有锁,就变成了下面的样子

在这里插入图片描述

3、t1.unlock

1、写锁释放流程及读锁加锁流程

1、release()方法

public static class WriteLock implements Lock, java.io.Serializable {
    public void unlock() {
        sync.release(1);
    }
}

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    public final boolean release(int arg) {
        //调用tryrelease()方法尝试释放锁
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                //如果头结点不为null并且waitStatus!=0 ,唤醒等待队列中下一个线程unpark()
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
}

2、tryRelease()方法

在这里插入图片描述

protected final boolean tryRelease(int releases) {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    //因为可重入的原因, 写锁计数为 0, 才算释放成功
    int nextc = getState() - releases;
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        //如果释放锁成功,就将加锁线程设置为null
        setExclusiveOwnerThread(null);
    //如果写锁计数不为0,更新写锁计数
    setState(nextc);
    return free;
}

这时会走到写锁的 sync.release(1) 流程,调用 sync.tryRelease(1) 成功,变成下面的样子 :

在这里插入图片描述

3、unparkSuccessor ()方法

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        //将当前线程的节点状态置0
        compareAndSetWaitStatus(node, ws, 0);
    
	//找到下一个需要唤醒的结点s
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        //如果该节点已经取消获取锁,那就从队尾开始向前找,找到第一个ws<=0的节点,并赋值给s
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    //调用unpark()方法,唤醒正在阻塞的线程
    if (s != null)
        LockSupport.unpark(s.thread);
}

接下来执行唤醒流程sync.unparkSuccessor,即让老二恢复运行:

在这里插入图片描述

4、doAcquireShared()方法

这时 t2 在doAcquireSharedparkAndCheckInterrupt()处恢复运行,这回再来一次 for (;😉 执行 tryAcquireShared 成功则让读锁计数加一

在这里插入图片描述

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) {
                //2、继续尝试获取锁资源,让读锁计数加1
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    //3、唤醒下一个线程
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
           	//1、t2线程在这儿被唤醒,就会继续指向一次for循环
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

5、setHeadAndPropagate (node, 1)方法

这时 t2 已经恢复运行,接下来 t2 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点

在这里插入图片描述

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head;
    setHead(node);//head指向自己
     //如果锁计数>0,就继续唤醒下面的线程
    if (propagate > 0 || h == null || h.waitStatus < 0) {
        Node s = node.next;
        //检查下一个节点是否是 shared,如果是将 head 的状态从 -1 改为 0 并唤醒老二
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

事情还没完,在setHeadAndPropagate方法内还会检查下一个节点是否是 shared,如果是则调用
doReleaseShared() 将 head 的状态从 -1 改为 0 并唤醒老二,这时 t3 在 doAcquireShared
parkAndCheckInterrupt() 处恢复运行

在这里插入图片描述

这回再来一次 for (;😉 执行 tryAcquireShared 成功则让读锁计数加一

在这里插入图片描述

这时 t3 已经恢复运行,接下来 t3 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点

在这里插入图片描述

下一个节点不是 shared 了,因此不会继续唤醒 t4 所在节点

4、t2 r.unlock,t3 r.unlock

1、读锁释放流程与写锁加锁流程

1、releaseShared(int arg)方法

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

2、tryReleaseShared(int unused)方法

t2 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,但由于计数还不为零

在这里插入图片描述

protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    //省略不必要的代码...
    //
    for (;;) {
        int c = getState();
        //释放读锁
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            //读锁的计数不会影响其它获取读锁线程, 但会影响其它获取写锁线程
            //计数为 0 才是真正释放
            return nextc == 0;
    }
}

3、doReleaseShared()方法

t3 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,这回计数为零了,进入
doReleaseShared() 将头节点从 -1 改为 0 并唤醒老二,即

在这里插入图片描述

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            //如果头结点的waitStatus=Node.SIGNAL,就将其通过CAS改为0
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            
                //唤醒下一个线程
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;               
        }
        if (h == head)                   
            break;
    }
}

之后 t4 在 acquireQueued 中 parkAndCheckInterrupt 处恢复运行,再次 for (;😉 这次自己是老二,并且没有其他竞争,tryAcquire(1) 成功,修改头结点,流程结束

在这里插入图片描述

3、锁降级

由上面的源码可以看出,线程在获取读锁时,如果state!=0,那么会先判断获取写锁的线程是不是当前线程,也就是说一个线程在获取写锁后,还可以获取读锁,当写锁释放后,就降级为读锁了。
在这里插入图片描述

4、不可以锁升级

在这里插入图片描述

在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章