七、ReentrantReadWriteLock

一、基础概念

  • 基于AQS那一套“框架”实现的
  • ReentrantLock是一个纯互斥锁,那么在读多写少的情况下,就会很慢。效率非常的低。
  • 使用基本:
    • 读读操作共享
    • 写写操作互斥
    • 读写操作互斥
    • 写读操作互斥
      • 单个线程获得写锁后,是可以再次获得读锁的
      • 当前线程获得读锁后,是不能获得写锁的
  • state是一个int,在ReentrantLock中是表示状态的。在ReentrantReadWriteLock中也是表示状态的,但是将这个int分了,高16位表示读锁状态,低16表示写锁状态
  • 锁重入问题:
    • 写锁:因为写操作和其他操作是互斥的,代表同一时间,只有一个线程持有着写锁,只要锁重入,就对低位+1即可。而且锁重入的限制,从原来的2^31 - 1,变为了2 ^ 16 -1。变短了
    • 读锁:读锁重入怎么玩:读锁的重入不能仿照写锁的方式,因为写锁属于互斥锁,同一时间只会有一个线程持有写锁,但是读锁是共享锁,同一时间会有多个线程持有读锁。所以每个获取到读锁的线程,记录锁重入的方式都是基于自己的ThreadLocal存储锁重入次数。
  • 每个读操作的线程,在获取读锁时,都需要开辟一个ThreadLocal。读写锁为了优化这个事情,做了两手操作:
    • 第一个拿到读锁的线程,不用ThreadLocal记录重入次数,在读写锁内有有一个firstRead记录重入次数
    • 还记录了最后一个拿到读锁的线程的重入次数,交给cachedHoldCounter属性标识,可以避免频繁的在锁重入时,从TL中获取

二、使用

public class Test {

    static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    static ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
    static ReentrantReadWriteLock.ReadLock readLock = lock.readLock();

    static LinkedList<String> list = new LinkedList<>();

    public static void readData () {
        readLock.lock();
        try {
            for (int i = 0; i < 200; i ++) {
                Thread thread = Thread.currentThread();
                System.out.println("当前线程" + thread.getName() + "消费:" + list.get(i));
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            readLock.unlock();
        }
    }

    public static void producerData () {
        writeLock.lock();
        try {
            Thread thread = Thread.currentThread();
            for (int i = 0; i < 100; i ++) {
                String data = thread.getName() + i;
                list.addLast(data);
            }
            System.out.println("当前线程" + thread.getName() + "生产:" + list.size());
        } finally {
            writeLock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread3 = new Thread(Test::producerData);
        thread3.setName("写1");
        Thread thread4 = new Thread(Test::producerData);
        thread4.setName("写2");
        thread3.start();
        thread4.start();

        Thread.sleep(500);
        Thread thread1 = new Thread(Test::readData);
        thread1.setName("读1");
        Thread thread2 = new Thread(Test::readData);
        thread2.setName("读2");
        thread1.start();
        thread2.start();
    }
}

三、源码(jdk1.8)

写锁的加锁

public void lock() {
    sync.acquire(1);
}
public final void acquire(int arg) {
    // 尝试获取锁资源(看一下,能否以CAS的方式将state 从0 ~ 1,改成功,拿锁成功)
    // 成功走人
    // 不成功执行下面方法
    if (!tryAcquire(arg) &&
        // addWaiter:将当前没按到锁资源的,封装成Node,排到AQS里
        // acquireQueued:当前排队的能否竞争锁资源,不能挂起线程阻塞
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

tryAcquire

static final int SHARED_SHIFT   = 16;
//  00000000  00000001  00000000  00000000
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
//  00000000  00000000  11111111  11111111
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
//  00000000  00000000  11111111  11111111
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

// 只拿到表示读锁的高16位。
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
// 只拿到表示写锁的低16位。
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

protected final boolean tryAcquire(int acquires) {
    // 获取当前线程
    Thread current = Thread.currentThread();
    // 拿到AQS的状态
    int c = getState();
    // 获取写锁的状态
    int w = exclusiveCount(c);
    // 当有锁,不确定是写锁还是读锁
    // 这一步其实是判断是不是锁重入
    if (c != 0) {
        // 如果写锁==0,那就证明有读锁,直接返回false不能抢锁
        // 如果写锁!=0,且当前线程不是拿锁的线程,直接返回false不能抢锁
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        // 锁重入了,判断重入次数是不是超过最大值
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // 重入锁,state + 1
        setState(c + acquires);
        return true;
    }
    // writerShouldBlock方法,有共享锁和非共享锁。共享锁判断非(是不是head的next或者队列是空的)。非共享锁直接返回false。
    if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
        return false;
    // 抢锁成功了,那么就将AQS中的锁线程设置为当前线程
    setExclusiveOwnerThread(current);
    return true;
}

final boolean writerShouldBlock() {
    return hasQueuedPredecessors();
}

final boolean writerShouldBlock() {
    return false; // writers can always barge
}

addWaiter和acquireQueued

与ReentrantLock中的一样

写锁的释放锁

public final boolean release(int arg) {
    // 只有tryRealse是读写锁重新实现的方法,其他的和ReentrantLock一致
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
protected final boolean tryRelease(int releases) {
    // 判断AQS中拿锁的线程是否为当前线程
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // state - 1
    int nextc = getState() - releases;
    // 判断低16位的写锁的state == 0。其实就是看有没有将写锁全部释放干净
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        // 写锁释放干净了,那么就将AQS中的拿锁线程设置为null
        setExclusiveOwnerThread(null);
    // 更新state
    setState(nextc);
    return free;
}

读锁的加锁

public void lock() {
    sync.acquireShared(1);
}
public final void acquireShared(int arg) {
    // tryAcquireShared,尝试获取锁资源,获取到返回1,没获取到返回-1
    if (tryAcquireShared(arg) < 0)
        // doAcquireShared 前面没拿到锁,这边需要排队~
        doAcquireShared(arg);
}

tryAcquireShared

protected final int tryAcquireShared(int unused) {
    // 获取当前线程
    Thread current = Thread.currentThread();
    // 获取AQS的state
    int c = getState();
    // 如果有写锁,且AQS的拿锁线程不是当前线程,就不能抢锁直接返回-1
    if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
        return -1;
    // 获取共享锁的state
    int r = sharedCount(c);
    // readerShouldBlock方法:共享锁执行hasQueuedPredecessors方法,非共享锁判断head的next节点是不是写锁(不能直接抢的原因是:写操作的时候,不能有读操作,如果每一个读操作上来都能直接抢锁,写操作还活不活了)
    // 判断读锁的数量
    // cas的方式抢锁
    if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
        // 抢锁成功了
        // 如果是第一次抢锁成功
        if (r == 0) {
            // 第一个拿到锁资源的线程,用first存储。提升效率,不用频繁的从TL中获取
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            // 我是锁重入,我就是第一个拿到读锁的线程,直接对firstReaderHoldCount++记录重入的次数
            firstReaderHoldCount++;
        } else {
            // 不是第一个拿到锁资源的
            // 先拿到cachedHoldCounter,最后一个线程的重入次数
            HoldCounter rh = cachedHoldCounter;
            // 之前的如果不存在或者说不是当前线程
            if (rh == null || rh.tid != getThreadId(current))
                // 将当前线程的重入次数设置给cachedHoldCounter
                cachedHoldCounter = rh = readHolds.get();
            // 之前拿过,现在是0,那么就将tl直接设置就行了
            else if (rh.count == 0)
                readHolds.set(rh);
            // 重入次数+1,
            // 第一个:可能是第一次拿
            // 第二个:可能是重入操作
            rh.count++;
        }
        return 1;
    }
    return fullTryAcquireShared(current);
}
// 通过tryAcquireShared没拿到锁资源,也没返回-1,就走这
final int fullTryAcquireShared(Thread current) {
    HoldCounter rh = null;
    for (;;) {
        // 获取AQS的state
        int c = getState();
        // 如果有写锁,且AQS的拿锁线程不是当前线程,就不能抢锁直接返回-1
        if (exclusiveCount(c) != 0) {
            if (getExclusiveOwnerThread() != current)
                return -1;
        // readerShouldBlock方法:共享锁执行hasQueuedPredecessors方法,非共享锁判断head的next节点是不是写锁(不能直接抢的原因是:写操作的时候,不能有读操作,如果每一个读操作上来都能直接抢锁,写操作还活不活了)
        } else if (readerShouldBlock()) {
            // 进入到这个方法里面,就证明了因为各种原因,不能抢锁。这个代码块里面的内容是为了阻塞前做准备哦
            if (firstReader == current) {
                
            } else {
                if (rh == null) {
                    rh = cachedHoldCounter;
                    // 之前的如果不存在或者说不是当前线程
                    if (rh == null || rh.tid != getThreadId(current)) {
                        // 获取当前线程的锁重入次数
                        rh = readHolds.get();
                        // 如果等于0,那么就证明你是第一次来
                        if (rh.count == 0)
                            // 将我的TL中的值移除掉,不移除会造成内存泄漏
                            readHolds.remove();
                    }
                }
                // 如果我的次数是0,绝对不是重入操作!
                if (rh.count == 0)
                    // 返回-1,等待阻塞吧!
                    return -1;
            }
        }
        // 判断读锁的数量是不是超过最大值
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // 抢锁
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            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;
        }
    }
}

doAcquireShared

// 没有拿到锁准备挂起了
private void doAcquireShared(int arg) {
    // 构建一个共享锁的Node并加入到链表的末尾
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // 获取上一个节点
            final Node p = node.predecessor();
            if (p == head) {
                // 如果我的上一个是head,尝试再次获取锁资源
                int r = tryAcquireShared(arg);
                // 判断是否拿锁成功,如果成功唤醒后面的后面AQS读锁线程。
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            // 能否挂起当前线程,需要保证我前面Node的状态为-1,才能执行后面操作
            if (shouldParkAfterFailedAcquire(p, node) &&
                //LockSupport.park挂起~~
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

读锁的释放锁

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();
    // 判断当前线程是不是第一个获取读锁的线程
    if (firstReader == current) {
        // 判断第一个读锁线程的重入次数是不是1,如果是1证明只拿了一次,如果不是就-1即可
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--;
    } else {
        // 获取最后一个读锁的线程信息
        HoldCounter rh = cachedHoldCounter;
        // 之前的如果不存在或者说不是当前线程
        if (rh == null || rh.tid != getThreadId(current))
            // 获取当前线程TL
            rh = readHolds.get();
        int count = rh.count;
        if (count <= 1) {
            // 如果小于等于1,就证明这个线程的读锁用完了,直接removeTL,防止内存泄漏
            readHolds.remove();
            if (count <= 0)
                throw unmatchedUnlockException();
        }
        // 将锁的数量-1
        --rh.count;
    }
    for (;;) {
        // 获取当前AQS锁state
        int c = getState();
        // state - -1
        int nextc = c - SHARED_UNIT;
        // CAS方式设置state
        if (compareAndSetState(c, nextc))
            // 判断接来下是不是要唤醒后续的线程了
            return nextc == 0;
    }
}

doReleaseShared

// 唤醒head后面的所有的读操作,直到有写操作或者队列没有任务了
private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            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)                   // loop if head changed
            break;
    }
}

四、总结流程(个人总结,可能有误)

写锁的加锁

  • tryAcquire
    • 判断是不是写锁的重入锁 ,如果是的话,就直接state+1,抢锁成功了
    • 判断还能不能抢一次锁:公平锁调用hasQueuedPredecessors方法,非公平锁则直接抢
  • 抢锁失败则调用addWaiter和acquireQueued方法,跟ReentrantLock一样的

写锁的释放锁

  • 判断AQS中拿锁的线程是不是当前线程,不是则直接报错
  • 查看写锁的state -1 后是否等于0:如果是0,那么就将AQS中的锁线程设置为null。将state刷新
    • 如果state==0的话,获取head,不为null且状态不是0,那么就从后往前,找一个距离head最近的且状态是0或者-1的Node。unpark唤醒它。

读锁的加锁

  1. 判断写锁是否存在,如果存在且AQS的拿锁线程不是当前线程,则返回-1
  2. 判断是否可以抢锁:公平锁调用hasQueuedPredecessors方法;非公平锁则判断head的next是不是读锁,如果是读锁则不能抢。防止大量的读锁,导致写锁饥饿
    • 如果可以且抢锁成功:
      • 如果是第一个抢锁的线程,则将线程的信息赋值给firstReader和firstReaderHoldCount。这算是一个优化:对于第一个频繁读的线程,就不用不停的去tl中获取重入信息了
      • 如果不是第一个抢锁的线程,则看最后一个cachedHoldCounter,判断是不是自己或者cachedHoldCounter是不是null
        • 如果成立:则将自己的线程的信息重入次数等赋值给cachedHoldCounter
        • 如果不成立:则直接用cachedHoldCounter
        • 最后将重入次数加1
      • 返回1
    • 如果抢锁失败(起一个死循环):
      • 判断写锁是否存在,如果存在且AQS的拿锁线程不是当前线程,则返回-1
      • 判断:公平锁调用hasQueuedPredecessors方法;非公平锁则判断head的next是不是读锁,如果是读锁则不能抢。防止大量的读锁,导致写锁饥饿
        • 如果不能抢锁的话,后面的代码都是为阻塞做准备。获取当前线程重入次数,如果是0,则将tl删除,否则会内存泄漏。
        • 返回-1
      • 判断共享锁数量是不是超限
      • 尝试在抢一次锁:
        • 如果是第一个抢锁的线程,则将线程的信息赋值给firstReader和firstReaderHoldCount。这算是一个优化:对于第一个频繁读的线程,就不用不停的去tl中获取重入信息了
        • 如果不是第一个抢锁的线程,则看最后一个cachedHoldCounter,判断是不是自己或者cachedHoldCounter是不是null
          • 如果成立:则将自己的线程的信息重入次数等赋值给cachedHoldCounter
          • 如果不成立:则直接用cachedHoldCounter
          • 最后将重入次数加1
        • 返回1
  3. 抢锁失败则需要排队了
    • 构建一个共享锁的Node并加入到链表的末尾
    • 起一个死循环
      • 判断当前Node的pre == head,如果是尝试抢锁,流程如上
        • 抢锁成功:将当前Node变为head,thread设置为null,然后将以前的head中的next设置为null。并且唤醒后面的读锁。终止循环
        • 抢锁失败:判断当前Node的前一个Node状态
          • 是-1,如果是-1就直接挂起当前线程
          • 是1,从当前Node往前找,直到找到一个Node状态是小于等于0的,将当前节点挂在它的后面。流程就是addWaiter中将Node挂在tail的流程
          • 是-2,-3,0,那么cas将这个节点的状态设置为-1。
        • 最后将当前线程park挂起,等待唤醒。

读锁的释放锁

  • 判断是不是第一个抢锁的线程
    • 如果是:则更新firstReaderHoldCount
    • 如果不是:判断是不是最后一个抢锁的线程,如果是则更新cachedHoldCounter。如果不是则更新自己的TL,并且重入次小于等于0的话,就将TL移除掉
  • 起一个死循环:将以前的state-1。CAS更新共享锁的state
  • 共享锁的次数如果等于0,则:唤醒head后面的所有的读操作,直到有写操作或者队列没有任务了
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值