Java 并发编程 进阶 -- Java并发包中锁原理剖析(LockSupport抽象同步队列AQS独占锁ReentrantLock读写锁ReentrantReadWriteLock)

Posted CodeJiao

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 并发编程 进阶 -- Java并发包中锁原理剖析(LockSupport抽象同步队列AQS独占锁ReentrantLock读写锁ReentrantReadWriteLock)相关的知识,希望对你有一定的参考价值。

文章目录

1. Java并发包中锁原理剖析


1.1 LockSupport工具类

JDK中的rt.jar包里面的LockSupport是个工具类,它的主要作用是挂起和唤醒线程,该工具类是创建锁和其他同步类的基础。

AQS借助于两个类:Unsafe(提供CAS操作)和LockSupport(提供park/unpark操作)

LockSupport类与每个使用它的线程都会关联一个许可证,在默认情况下调用LockSupport类的方法的线程是不持有许可证的。LockSupport是使用Unsafe类实现的,下面介绍LockSupport中的几个主要函数。


1.1.1 void park()方法

如果调用park方法的线程已经拿到了与LockSupport关联的许可证,则调用LockSupport.park()时会马上返回,否则调用线程会被禁止参与线程的调度,也就是会被阻塞挂起。


在其他线程调用unpark(Thread thread)方法并且将当前线程作为参数时,调用park方法而被阻塞的线程会返回。另外,如果其他线程调用了阻塞线程的interrupt()方法,设置了中断标志或者线程被虚假唤醒,则阻塞线程也会返回。所以在调用park方法时最好也使用循环条件判断方式。

需要注意的是,因调用park()方法而被阻塞的线程被其他线程中断而返回时并不会抛出InterruptedException异常。


1.1.2 void unpark(Thread thread)方法

当一个线程调用unpark时,如果参数thread线程没有持有thread与LockSupport类关联的许可证,则让thread线程持有许可证。如果thread之前因调用park()而被挂起,则调用unpark后,该线程会被唤醒。如果thread之前没有调用park,则调用unpark方法后,再调用park方法,其会立刻返回。修改代码如下:

下面再来看一个例子以加深对park和unpark的理解。

    public static void main(String[] args) throws InterruptedException 
        Thread thread = new Thread(() -> 
            System.out.println("child thread begin park !");
            //调用park方法,挂起自己
            LockSupport.park();
            System.out.println("child thread unpark!");
        );

        //启动子线程
        thread.start();
        //主线程休眠1s
        Thread.sleep(1000);
        System.out.println("main thread begin unpark! ");
        //调用unpark方法让thread线程持有许可证,然后park方法返回
        LockSupport.unpark(thread);
    

运行结果:

上面代码首先创建了一个子线程thread,子线程启动后调用park方法,由于在默认情况下子线程没有持有许可证,因而它会把自己挂起。

主线程休眠1s是为了让主线程调用unpark方法前让子线程输出child thread beginpark!并阻塞。

主线程然后执行unpark方法,参数为子线程,这样做的目的是让子线程持有许可证,然后子线程调用的park方法就返回了。

park方法返回时不会告诉你因何种原因返回,所以调用者需要根据之前调用park方法的原因,再次检查条件是否满足,如果不满足则还需要再次调用park方法。

为了说明调用park方法后的线程被中断后会返回,我们修改上面的例子代码,具体代码如下:

运行结果:

在如上代码中,只有中断子线程,子线程才会运行结束,如果子线程不被中断,即使你调用unpark(thread)方法子线程也不会结束。


1.1.3 void parkNanos(long nanos)方法

和park方法类似,如果调用park方法的线程已经拿到了与LockSupport关联的许可证,则调用LockSupport.parkNanos(long nanos)方法后会马上返回。该方法的不同在于,如果没有拿到许可证,则调用线程会被挂起nanos时间后修改为自动返回。


1.1.4 park(Object blocker)方法

park方法还支持带有blocker参数的方法void park(Object blocker)方法,当线程在没有持有许可证的情况下调用park方法而被阻塞挂起时,这个blocker对象会被记录到该线程内部。

使用诊断工具可以观察线程被阻塞的原因,诊断工具是通过调用getBlocker(Thread)方法来获取blocker对象的,所以JDK推荐我们使用带有blocker参数的park方法,并且blocker被设置为this,这样当在打印线程堆栈排查问题时就能知道是哪个类被阻塞了。


1.1.5 void parkNanos(Object blocker, long nanos)方法

相比park(Object blocker)方法多了个超时时间。


1.1.6 void parkUntil(Object blocker, long deadline)方法


其中参数deadline的时间单位为ms,该时间是从1970年到现在某一个时间点的毫秒值。这个方法和parkNanos(Object blocker, long nanos)方法的区别是,后者是从当前算等待nanos秒时间,而前者是指定一个时间点,比如需要等到2017.12.11日12:00:00,则把这个时间点转换为从1970年到这个时间点的总毫秒数。


1.2 抽象同步队列AQS概述


1.2.1 AQS——锁的底层支持

AbstractQueuedSynchronizer抽象同步队列简称AQS,它是实现同步器的基础组件,并发包中锁的底层就是使用AQS实现的。

下面看下AQS的类图结构,如图所示:

由该图可以看到,AQS是一个FIFO的双向队列,其内部通过节点head和tail记录队首和队尾元素,队列元素的类型为Node。其中Node中的thread变量用来存放进入AQS队列里面的线程;Node节点内部的SHARED用来标记该线程是获取共享资源时被阻塞挂起后放入AQS队列的,EXCLUSIVE用来标记线程是获取独占资源时被挂起后放入AQS队列的;waitStatus记录当前线程等待状态,可以为CANCELLED(线程被取消了)、SIGNAL(线程需要被唤醒)、CONDITION(线程在条件队列里面等待)、PROPAGATE(释放共享资源时需要通知其他节点);prev记录当前节点的前驱节点,next记录当前节点的后继节点。

在AQS中维持了一个单一的状态信息state,可以通过getState、setState、compareAndSetState函数修改其值。对于ReentrantLock的实现来说,state可以用来表示当前线程获取锁的可重入次数;对于读写锁ReentrantReadWriteLock来说,state的高16位表示读状态,也就是获取该读锁的次数,低16位表示获取到写锁的线程的可重入次数;对于semaphore来说,state用来表示当前可用信号的个数;对于CountDownlatch来说,state用来表示计数器当前的值。

AQS有个内部类ConditionObject,用来结合锁实现线程同步。ConditionObject可以直接访问AQS对象内部的变量,比如state状态值和AQS队列。ConditionObject是条件变量每个条件变量对应一个条件队列(单向链表队列),其用来存放调用条件变量的await方法后被阻塞的线程,如类图所示,这个条件队列的头、尾元素分别为firstWaiter和lastWaiter。

对于AQS来说,线程同步的关键是对状态值state进行操作。根据state是否属于一个线程,操作state的方式分为独占方式和共享方式。

  • 在独占方式下获取和释放资源使用的方法为: void acquire(int arg)void acquireInterruptibly(int arg)boolean release(int arg)
  • 在共享方式下获取和释放资源的方法为: void acquireShared(int arg)void acquireSharedInterruptibly(int arg)boolean releaseShared(int arg)

使用独占方式获取的资源是与具体线程绑定的,就是说如果一个线程获取到了资源,就会标记是这个线程获取到了,其他线程再尝试操作state获取资源时会发现当前该资源不是自己持有的,就会在获取失败后被阻塞。比如独占锁ReentrantLock的实现,当一个线程获取了ReentrantLock的锁后,在AQS内部会首先使用CAS操作把state状态值从0变为1,然后设置当前锁的持有者为当前线程,当该线程再次获取锁时发现它就是锁的持有者,则会把状态值从1变为2,也就是设置可重入次数,而当另外一个线程获取锁时发现自己并不是该锁的持有者就会被放入AQS阻塞队列后挂起

对应共享方式的资源与具体线程是不相关的,当多个线程去请求资源时通过CAS方式竞争获取资源,当一个线程获取到了资源后,另外一个线程再次去获取时如果当前资源还能满足它的需要,则当前线程只需要使用CAS方式进行获取即可。比如Semaphore信号量,当一个线程通过acquire()方法获取信号量时,会首先看当前信号量个数是否满足需要,不满足则把当前线程放入AQS阻塞队列,如果满足则通过自旋CAS获取信号量。

在独占方式下,获取与释放资源的流程如下:

  1. 当一个线程调用acquire(int arg)方法获取独占资源时,会首先使用tryAcquire方法尝试获取资源,具体是设置状态变量state的值,成功则直接返回,失败则将当前线程封装为类型为Node.EXCLUSIVE的Node节点后插入到AQS阻塞队列的尾部,并调用LockSupport.park(this)方法挂起自己。
  2. 当一个线程调用release(int arg)方法时会尝试使用tryRelease操作释放资源,这里是设置状态变量state的值,然后调用LockSupport.unpark(thread)方法激活AQS队列里面被阻塞的一个线程(thread)。被激活的线程则使用tryAcquire尝试,看当前状态变量state的值是否能满足自己的需要,满足则该线程被激活,然后继续向下运行,否则还是会被放入AQS队列并被挂起。

需要注意的是,AQS类并没有提供可用的tryAcquire和tryRelease方法,正如AQS是锁阻塞和同步器的基础框架一样,tryAcquire和tryRelease需要由具体的子类来实现。子类在实现tryAcquire和tryRelease时要根据具体场景使用CAS算法尝试修改state状态值,成功则返回true,否则返回false。子类还需要定义,在调用acquire和release方法时state状态值的增减代表什么含义。

  • 比如继承自AQS实现的独占锁ReentrantLock,定义当status为0时表示锁空闲,为1时表示锁已经被占用。在重写tryAcquire时,在内部需要使用CAS算法查看当前state是否为0,如果为0则使用CAS设置为1,并设置当前锁的持有者为当前线程,而后返回true,如果CAS失败则返回false。
  • 比如继承自AQS实现的独占锁在实现tryRelease时,在内部需要使用CAS算法把当前state的值从1修改为0,并设置当前锁的持有者为null,然后返回true,如果CAS失败则返回false。

在共享方式下,获取与释放资源的流程如下:

  1. 当线程调用acquireShared(int arg)获取共享资源时,会首先使用tryAcquireShared尝试获取资源,具体是设置状态变量state的值,成功则直接返回,失败则将当前线程封装为类型为Node.SHARED的Node节点后插入到AQS阻塞队列的尾部,并使用LockSupport.park(this)方法挂起自己。
  2. 当一个线程调用releaseShared(int arg)时会尝试使用tryReleaseShared操作释放资源,这里是设置状态变量state的值,然后使用LockSupport.unpark(thread)激活AQS队列里面被阻塞的一个线程(thread)。被激活的线程则使用tryReleaseShared查看当前状态变量state的值是否能满足自己的需要,满足则该线程被激活,然后继续向下运行,否则还是会被放入AQS队列并被挂起。

同样需要注意的是,AQS类并没有提供可用的tryAcquireShared和tryReleaseShared方法,正如AQS是锁阻塞和同步器的基础框架一样,tryAcquireShared和tryReleaseShared需要由具体的子类来实现。子类在实现tryAcquireShared和tryReleaseShared时要根据具体场景使用CAS算法尝试修改state状态值,成功则返回true,否则返回false。

  • 比如继承自AQS实现的读写锁ReentrantReadWriteLock里面的读锁在重写tryAcquireShared时,首先查看写锁是否被其他线程持有,如果是则直接返回false,否则使用CAS递增state的高16位(在ReentrantReadWriteLock中,state的高16位为获取读锁的次数)。
  • 比如继承自AQS实现的读写锁ReentrantReadWriteLock里面的读锁在重写tryReleaseShared时,在内部需要使用CAS算法把当前state值的高16位减1,然后返回true,如果CAS失败则返回false。

基于AQS实现的锁除了需要重写上面介绍的方法外,还需要重写isHeldExclusively方法,来判断锁是被当前线程独占还是被共享。

AQS如何维护提供的队列,主要看入队操作:

入队操作当一个线程获取锁失败后该线程会被转换为Node节点,然后就会使用enq(final Node node)方法将该节点插入到AQS的阻塞队列

如上代码在第一次循环中,当要在AQS队列尾部插入元素时,AQS队列状态如图中(default)所示。也就是队列头、尾节点都指向null;当执行代码(1)后节点t指向了尾部节点,这时候队列状态如图中(I)所示。

这时候t为null,故执行代码(2),使用CAS算法设置一个哨兵节点为头节点,如果CAS设置成功,则让尾部节点也指向哨兵节点,这时候队列状态如图中(II)所示。

到现在为止只插入了一个哨兵节点,还需要插入node节点,所以在第二次循环后执行到代码(1),这时候队列状态如图(III)所示;然后执行代码(3)设置node的前驱节点为尾部节点,这时候队列状态如图中(IV)所示;然后通过CAS算法设置node节点为尾部节点,CAS成功后队列状态如图中(V)所示;CAS成功后再设置原来的尾部节点的后驱节点为node,这时候就完成了双向链表的插入,此时队列状态如图中(VI)所示。

说明:哨兵结点(头结点),哨兵节点通常位于链表头部,它的值没有任何意义,在一个有哨兵节点的链表中,从第二个节点开始才真正保存有意义的数据。作用是防止首节点为空时(first == null),出现无法指向下个节点情况。哨兵节点指向的下个单元为首节点。


1.2.2 AQS——条件变量的支持

条件变量这一概念源自于操作系统,设计它是为了解决等待同步需求,实现线程间协作通信的一种机制(通知 / 等待机制)

notify和wait,是配合synchronized内置锁实现线程间同步的基础设施一样,条件变量的signal和await方法也是用来配合锁(使用AQS实现的锁)实现线程间同步的基础设施。

它们的不同在于,synchronized同时只能与一个共享变量的notify或wait方法实现同步,而AQS的一个锁可以对应多个条件变量。

在调用共享变量的notify和wait方法前必须先获取该共享变量的内置锁,同理,在调用条件变量的signal和await方法前也必须先获取条件变量对应的锁。

我们先看一个关于条件变量的例子:

    public static void main(String[] args) throws InterruptedException 
        ReentrantLock lock = new ReentrantLock(); // (1)
        Condition condition = lock.newCondition(); //(2)
        lock.lock(); //(3)
        try 
            System.out.println("begin wait");
            condition.await(); //(4)
            System.out.println("end wait");
         catch (Exception e) 
            e.printStackTrace();
         finally 
            lock.unlock();//(5)
        
        lock.lock();//(6)
        try 
            System.out.println("begin signal");
            condition.signal(); // (7)
            System.out.println("end signal");
         catch (Exception e) 
            e.printStackTrace();
         finally 
            lock.unlock();//(8)
        
    
  • 代码(1)创建了一个独占锁ReentrantLock对象,ReentrantLock是基于AQS实现的锁。
  • 代码(2)使用创建的Lock对象的newCondition()方法创建了一个ConditionObject变量,这个变量就是Lock锁对应的一个条件变量。需要注意的是,一个Lock对象可以创建多个条件变量。
  • 代码(3)首先获取了独占锁,代码(4)则调用了条件变量的await()方法阻塞挂起了当前线程。当其他线程调用条件变量的signal方法时,被阻塞的线程才会从await处返回。需要注意的是,和调用Object的wait方法一样,如果在没有获取到锁前调用了条件变量的await方法则会抛出java.lang.IllegalMonitorStateException异常。
  • 代码(5)则释放了获取的锁。

在上面代码中,lock.newCondition()的作用其实是new了一个在AQS内部声明的ConditionObject对象,ConditionObject是AQS的内部类,可以访问AQS内部的变量(例如状态变量state)和方法。在每个条件变量内部都维护了一个条件队列,用来存放调用条件变量的await()方法时被阻塞的线程。注意这个条件队列和AQS队列不是一回事。

运行结果:

注意

  • 当线程调用条件变量的await()方法时(必须先调用锁的lock()方法获取锁),在内部会构造一个类型为Node.CONDITION的node节点,然后将该节点插入条件队列末尾,之后当前线程会释放获取的锁(也就是会操作锁对应的state变量的值),并被阻塞挂起。这时候如果有其他线程调用lock.lock()尝试获取锁,就会有一个线程获取到锁,如果获取到锁的线程调用了条件变量的await()方法,则该线程也会被放入条件变量的阻塞队列,然后释放获取到的锁,在await()方法处阻塞。
  • 其他线程调用条件变量的signal方法时(必须先调用锁的lock()方法获取锁),在内部会把条件队列里面队头的一个线程节点从条件队列里面移除并放入AQS的阻塞队列里面,然后激活这个线程。

需要注意的是,AQS只提供了ConditionObject的实现,并没有提供newCondition函数,该函数用来new一个ConditionObject对象。需要由AQS的子类来提供newCondition函数。

注意:当多个线程同时调用lock.lock()方法获取锁时,只有一个线程获取到了锁,其他线程会被转换为Node节点插入到lock锁对应的AQS阻塞队列里面,并做自旋CAS尝试获取锁。

  1. 如果获取到锁的线程调用了对应的条件变量的await()方法,则该线程会释放获取到的锁,并被转换为Node节点插入到条件变量对应的条件队列里面。
  2. 这时候因为调用lock.lock()方法被阻塞到AQS队列里面的一个线程会获取到被释放的锁,如果该线程也调用了条件变量的await()方法则该线程也会被放入条件变量的条件队列里面。
  3. 当另外一个线程调用条件变量的signal()或者signalAll()方法时,会把条件队列里面的一个或者全部Node节点移动到AQS的阻塞队列里面,等待时机获取锁。

总结如下:一个锁对应一个AQS阻塞队列,对应多个条件变量,每个条件变量有自己的一个条件队列。


1.3 独占锁ReentrantLock的原理


1.3.1 独占锁ReentrantLock的原理

ReentrantLock是可重入的独占锁,同时只能有一个线程可以获取该锁,其他获取该锁的线程会被阻塞而被放入该锁的AQS阻塞队列里面。首先看下ReentrantLock的类图以便对它的实现有个大致了解,如图所示。

从类图可以看到,ReentrantLock最终还是使用AQS来实现的,并且根据参数来决定其内部是一个公平还是非公平锁,默认是非公平锁

在这里,AQS的state状态值表示线程获取该锁的可重入次数,在默认情况下,state的值为0表示当前锁没有被任何线程持有。当一个线程第一次获取该锁时会尝试使用CAS设置state的值为1,如果CAS成功则当前线程获取了该锁,然后记录该锁的持有者为当前线程。在该线程没有释放锁的情况下第二次获取该锁后,状态值被设置为2,这就是可重入次数。在该线程释放该锁时,会尝试使用CAS让状态值减1,如果减1后状态值为0,则当前线程释放该锁。


1.3.2 获取锁

void lock() 方法:

当一个线程调用该方法时,说明该线程希望获取该锁。如果锁当前没有被其他线程占用并且当前线程之前没有获取过该锁,则当前线程会获取到该锁,然后设置当前锁的拥有者为当前线程,并设置AQS的状态值为1,然后直接返回。如果当前线程之前已经获取过该锁,则这次只是简单地把AQS的状态值加1后返回。如果该锁已经被其他线程持有,则调用该方法的线程会被放入AQS队列后阻塞挂起。

在如上代码中,ReentrantLock的lock()委托给了sync类,根据创建ReentrantLock构造函数选择sync的实现是NonfairSync还是FairSync,这个锁是一个非公平锁或者公平锁。这里先看sync的子类NonfairSync的情况,也就是非公平锁时。

在代码(1)中,因为默认AQS的状态值为0,所以第一个调用Lock的线程会通过CAS设置状态值为1, CAS成功则表示当前线程获取到了锁,然后setExclusiveOwnerThread设置该锁持有者是当前线程。如果这时候有其他线程调用lock方法企图获取该锁,CAS会失败,然后会调用AQS的acquire方法。注意,传递参数为1代表当前资源已经被加锁了。

void lockInterruptibly() 方法:

该方法与lock()方法类似,它的不同在于,它对中断进行响应,就是当前线程在调用该方法时,如果其他线程调用了当前线程的interrupt()方法,则当前线程会抛出InterruptedException异常,然后返回(释放了锁)。

boolean tryLock() 方法:

尝试获取锁,如果当前该锁没有被其他线程持有,则当前线程获取该锁并返回true,否则返回false。注意,该方法不会引起当前线程阻塞。

boolean tryLock(long timeout, TimeUnit unit) 方法:

尝试获取锁,与tryLock()的不同之处在于,它设置了超时时间,如果超时时间到没有获取到该锁则返回false。


1.3.3 释放锁

void unlock() 方法:

尝试释放锁,如果当前线程持有该锁,则调用该方法会让该线程对该线程持有的AQS状态值减1,如果减去1后当前状态值为0,则当前线程会释放该锁,否则仅仅减1而已。如果当前线程没有持有该锁而调用了该方法则会抛出IllegalMonitorStateException异常。


1.3.4 ReentrantLock实现一个简单的线程安全的list

如下代码通过在操作array元素前进行加锁保证同一时间只有一个线程可以对array数组进行修改和访问。

public class ReentrantLockList<T> 
    //线程不安全的list
    private final ArrayList<T> array = new ArrayList<T>();
    //独占锁
    private final ReentrantLock lock = new ReentrantLock();

    // 1. 添加元素
    public void add(T e) 
        lock.lock();
        try 
            array.add(e);
         finally 
            lock.unlock();
        
    

    // 2. 删除元素
    public void remove(T e) 
        lock.lock();
        try 
            array.remove(e);
         finally 
            lock.unlock();
        
    

    // 3. 获取数据
    public T get(int index) 
        lock.lock();
        try 
            return array.get(index);
         finally 
            lock.unlock();
        
    


1.3.5 小结

ReentrantLock的底层是使用AQS实现的可重入独占锁。在这里AQS状态值为0表示当前锁空闲,为大于等于1的值则说明该锁已经被占用。该锁内部有公平与非公平实现,默认情况下是非公平的实现。另外,由于该锁是独占锁,所以一个时刻只能有一个线程可以获取该锁。


1.4 读写锁ReentrantReadWriteLock的原理


1.4.1 读写锁ReentrantReadWriteLock的原理

解决线程安全问题使用ReentrantLock就可以,但是ReentrantLock是独占锁,某时只有一个线程可以获取该锁,而实际中会有写少读多的场景,显然ReentrantLock满足不了这个需求,所以ReentrantReadWriteLock应运而生。ReentrantReadWriteLock采用读写分离的策略,允许多个线程可以同时获取读锁。
读写锁的内部维护了一个ReadLock和一个WriteLock,它们依赖Sync实现具体功能。而Sync继承自AQS,并且也提供了公平和非公平的实现。下面只介绍非公平的读写锁实现。我们知道AQS中只维护了一个state状态,而ReentrantReadWriteLock则需要维护读状态和写状态,一个state怎么表示写和读两种状态呢?ReentrantReadWriteLock巧妙地使用state的高16位表示读状态,也就是获取到读锁的次数;使用低16位表示获取到写锁的线程的可重入次数


1.4.2 写锁的获取与释放

在ReentrantReadWriteLock中写锁使用WriteLock来实现。

void lock():

写锁是个独占锁,某时只有一个线程可以获取该锁。如果当前没有线程获取到读锁和写锁,则当前线程可以获取到写锁然后返回。如果当前已经有线程获取到读锁和写锁,则当前请求写锁的线程会被阻塞挂起。另外,写锁是可重入锁,如果当前线程已经获取了该锁,再次获取只是简单地把可重入次数加1后直接返回。

void lockInterruptibly():

类似于lock()方法,它的不同之处在于,它会对中断进行响应,也就是当其他线程调用了该线程的interrupt()方法中断了当前线程时,当前线程会抛出异常InterruptedException异常,然后返回(释放了锁)。

boolean tryLock():

尝试获取写锁,如果当前没有其他线程持有写锁或者读锁,则当前线程获取写锁会成功,然后返回true。如果当前已经有其他线程持有写锁或者读锁则该方法直接返回false,且当前线程并不会被阻塞。如果当前线程已经持有了该写锁则简单增加AQS的状态值后直接返回true。

boolean tryLock(long timeout, TimeUnit unit):

与tryAcquire()的不同之处在于,多了超时时间参数,如果尝试获取写锁失败则会把当前线程挂起指定时间,待超时时间到后当前线程被激活,如果还是没有获取到写锁则返回false。另外,该方法会对中断进行响应,也就是当其他线程调用了该线程的interrupt()方法中断了当前线程时,当前线程会抛出InterruptedException异常,然后返回(释放了锁)。

void unlock():

尝试释放锁,如果当前线程持有该锁,调用该方法会让该线程对该线程持有的AQS状态值减1,如果减去1后当前状态值为0则当前线程会释放该锁,否则仅仅减1而已。如果当前线程没有持有该锁而调用了该方法则会抛出IllegalMonitorStateException异常。


1.4.3 读锁的获取与释放

ReentrantReadWriteLock中的读锁是使用ReadLock来实现的。

void lock():

获取读锁,如果当前没有其他线程持有写锁,则当前线程可以获取读锁,AQS的状态值state的高16位的值会增加1,然后方法返回。否则如果其他一个线程持有写锁,则当前线程会被阻塞。

void lockInterruptibly():

类似于lock()方法,不同之处在于,该方法会对中断进行响应,也就是当其他线程调用了该线程的interrupt()方法中断了当前线程时,当前线程会抛出InterruptedException异常。

boolean tryLock():

尝试获取读锁,如果当前没有其他线程持有写锁,则当前线程获取读锁会成功,然后返回true。如果当前已经有其他线程持有写锁则该方法直接返回false,但当前线程并不会被阻塞。如果当前线程已经持有了该读锁则简单增加AQS的状态值高16位后直接返回true。

boolean tryLock(long timeout, TimeUnit unit):

与tryLock()的不同之处在于,多了超时时间参数,如果尝试获取读锁失败则会把当前线程挂起指定时间,待超时时间到后当前线程被激活,如果此时还没有获取到读锁则返回false。另外,该方法对中断响应,也就是当其他线程调用了该线程的interrupt()方法中断了当前线程时,当前线程会抛出InterruptedException异常。

void unlock():


如上代码具体释放锁的操作是委托给Sync类来做的,sync.releaseShared方法的代码如下:

其中tryReleaseShared的代码如下:

如以上代码所示,在无限循环里面,首先获取当前AQS状态值并将其保存到变量c,然后变量c被减去一个读计数单位后使用CAS操作更新AQS状态值,如果更新成功则查看当前AQS状态值是否为0,为0则说明当前已经没有读线程占用读锁,则tryReleaseShared返回true。然后会调用doReleaseShared方法释放一个由于获取写锁而被阻塞的线程,如果当前AQS状态值不为0,则说明当前还有其他线程持有了读锁,所以tryReleaseShared返回false。如果tryReleaseShared中的CAS更新AQS状态值失败,则自旋重试直到成功。


1.4.4 使用ReentrantReadWriteLock实现线程安全的list

前面介绍了如何使用ReentrantLock实现线程安全的list,但是由于ReentrantLock是独占锁,所以在读多写少的情况下性能很差。下面使用ReentrantReadWriteLock来改造它,代码如下:

public class ReentrantLockList<T> 
    //线程不安全的list
    private final ArrayList<T> array = new ArrayList<T>();
    //独占锁
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock readLock = lock.readLock();
    private final Lock writeLock = lock.writeLock();


    // 1. 添加元素
    public void add(T e) 
        writeLock.lock();
        try 
            array.add(e);
         finally 
            writeLock.unlock();
        
    

    // 2. 删除元素
    public void remove(T e) 
        writeLock.lock();
        try 
            array.remove(e);
         finally 
            writeLock.unlock();
        
    

    // 3. 获取数据
    public T get(int index) 
        readLock.lock();
        try 
            return array.get(index);
         finally 
            readLock.unlock();
        
    



1.4.5 小结

读写锁ReentrantReadWriteLock的底层是使用AQS实现的。ReentrantReadWriteLock巧妙地使用AQS的状态值的高16位表示获取到读锁的个数,低16位表示获取写锁的线程的可重入次数,并通过CAS对其进行操作实现了读写分离,这在读多写少的场景下比较适用。



以上是关于Java 并发编程 进阶 -- Java并发包中锁原理剖析(LockSupport抽象同步队列AQS独占锁ReentrantLock读写锁ReentrantReadWriteLock)的主要内容,如果未能解决你的问题,请参考以下文章

Java 并发编程 进阶 -- ThreadLocalRandom类原理剖析原子操作类原理剖析(AtomicLong)并发List原理剖析(CopyOnWriteArrayList)

Java 并发编程 进阶 -- ThreadLocalRandom类原理剖析原子操作类原理剖析(AtomicLong)并发List原理剖析(CopyOnWriteArrayList)

Java并发包中锁原理剖析

Java 并发编程实践基础 读书笔记: 第三章 使用 JDK 并发包构建程序

Java并发编程之美读书笔记5-Java并发包中ThreadLocalRandom类原理剖析

Java并发编程之美读书笔记5-Java并发包中ThreadLocalRandom类原理剖析