Java并发编程序列之JUC中的ReentrantLock与ReentrantReadWriteLock

368 阅读4分钟

Java并发编程序列之JUC中的ReentrantLock-ReentrantReadWriteLock

Hello,大家好,之前两篇关于JUC中AQS的介绍算上大致把AQS是什么,自定义Lock怎么实现讲清楚了,这一篇就来说一说JDK中自带的常用的Lock接口的实现,ReentrantLock和ReentrantReadWriteLock,让大家在实战代码中可以用得到,文章结构:

  1. ReentrantLock之可重入性,公平性。
  2. ReentrantReadWriteLock详解
  3. LockSupport详解。

1. ReentrantLock之可重入性,公平性

ReentrantLock是一个独占的可重入锁,独占性我就不多说了,前面已经给大家说过,就是只能一个线程获取到锁,像之前给大家自定义过的OnlyOneLock,可重入性这里再说一说,前面实现的OnlyOneLock,大家可以看到,当一个线程拿到锁时,也就是把底层AQS的state改为1时,包括自己在内的任何线程都无法再获取到锁,这就是不可重入!反之,可重入的意思就是,如果一个线程获取到了锁,那么这个线程可以再次获取到锁,让我们看看ReentrantLock的可重入性是怎么实现的:

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //说白了,就是在这里!判断下获取锁的线程是否为当前线程,如果是,直接让state+1 
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

可以看到,代码走到else if 里判断下获取锁的线程是否为当前线程,如果是,直接让state+1,获取锁成功。

然后说下公平性:默认情况下JDK自带的Lock都是非公平的,效率高一些。所谓的公平性就是指,谁先请求锁,谁肯定先得到锁。前面和大家说过,AQS底层维护了一个FIFO的队列,本身就具备FIFO特性的公平性,但是大家要知道,当头结点刚释放锁时候,假如这时候有一个线程调用的lock方法想要获取锁,那么他相对于队列中的线程就是不公平的。因为他有可能直接获取到锁。所以这就是不公平的,那么何为公平呢,其实很简单,只需要在AQS的tryAcquire实现的时候判断一个队列中是否有等待的线程,有的话就自觉去排队就可以了。具体见代码:

protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
            //可以看到这里,先判断了hasQueuedPredecessors队列中有没有线程已经再排队了。
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

!hasQueuedPredecessors()判断了队列中有没有线程已经再排队了。

好了,其实没什么东西,这就给大家说完了可重入性和公平性。列下知识点:

  1. ReentrantLock 是一个可重入的,独占的,公平性可以根据参数选择的Lock.
  2. 默认为非公平锁,效率高,减了线程切换。

2. ReentrantReadWriteLock详解

这个Lock是工作中最常用的,在了解了ReentrantLock后,这个其实贼简单,我先说下结论:(以下知识点都是针对的ReentrantReadWriteLock)

  1. 内部维护了两个Lock,一个ReadLock,一个WriteLock。
  2. 读锁是一个可重入的共享锁。写锁是一个可重入的独占锁。
  3. 内部维护时讲state变量分为高16位和低16位来操作。
  4. 读锁与写锁相互排斥 。比如在获取读锁时如果已经有写锁了,阻塞。反之亦然。

这部分代码我就不Show了,其实知道结论就行了,因为原理前面已经说清楚了,只是在tryAcquire时根据state变量分高低16位维护两个锁罢了。

3. LockSupport详解

前面说到,当调用Lock时,内部调用AQS的acquire等方法,返回false时,会加入队列并阻塞线程。外在的含义就是没有获取到锁。那么这个阻塞线程到底是怎么实现的呢?其实就是一个工具类。LockSupport. 大家记住,JUC中所有关于线程阻塞和唤醒都是这个工具类来底层做到的。废话少说,直接列下API就可以了。

大家可以看到,有park和unpark两类方法。阻塞和唤醒线程,相比synchronized关键字的monitorenter字节码指令来讲效率会好很多,为什么呢?因为monitorenter指令加上去之后线程切换上去之后发现没有进入又直接切换走。而LockSupport的park方法则是将线程休息,此时线程处于WAITING状态。CPU是不会切换上去的。这就大大减少了线程的上下文切换。希望大家明白。为什么JUC效率上比synchronized好,虽然比较复杂,但是功能也强大很多。Over ,Have a good day .