JUC(八)ReentrantLock 源码解析

Lock简介

Lock 接口实现类提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的 Condition (Condition实现类ConditonObject来实现线程的通知/与唤醒机制,关于Condition后期会进行介绍)对象。

锁是用于控制多线程访问共享资源的工具。通常,锁提供对共享资源的独占访问:一次只有一个线程可以获取锁,对共享资源的所有访问都需要首先获取锁。但是,一些锁可以允许同时访问共享资源,例如ReadWriteLock

虽然使用关键字synchronized修饰的方法或代码块,会使得在监视器模式(ObjectMonitor)下编程变得非常容易(通过synchronized块或者方法所提供的隐式获取释放锁的便捷性)。虽然这种方式简化了锁的管理,但是某些情况下,还是建议采用Lock接口(及其相关子类)提供的显示的锁的获取和释放。例如,针对一个场景,手把手进行锁获取和释放,先获得锁A,然后再获取锁B,当锁B获得后,释放锁A同时获取锁C,当锁C获得后,再释放B同时获取锁D,以此类推。这种场景下, synchronized关键字就不那么容易实现了,而Lock接口的实现类允许锁在不同的作用范围内获取和释放,并允许以任何顺序获取和释放多个锁。

Lock接口中的方法

关于Lock接口中涉及到的方法具体如下:(建议直接在PC端查看,手机上有可能看的不是很清楚)

lock_method.png

 

从上表中,我们就可以得出使用Lock接口实现的锁机制与使用传统的synchronized的区别

 

  1. 尝试非阻塞地获取锁:当线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁。
  2. 能被中断的获取锁:与synchronized不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常会被抛出,同时锁也会被释放。
  3. 超时获取锁:在指定的截止时间之前获取锁,如果截止时间到了任然无法获取到锁,则返回。

Lock简单使用与注意事项

其中Lock的使用方式也很简单,具体代码如下所示:

Lock lock = ....;具体实现类
lock.lock();
try {
} finally {
lock.unlock();//建议在finally中释放锁
}
复制代码

当锁定和解锁发生在不同的范围时,一定要注意确保在持有锁时执行的所有代码都受到try-finally或try-catch的保护,以确保在必要时释放锁。不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常,异常抛出的同时,也会导致锁无故释放(因为一旦发生异常,就会走finally语句,如果这个异常(可能是用户自定义异常,用户可以自己处理)需要线程1来处理,但是接着执行了lock.unlock()语句导致了锁的释放。那么其他线程就可以操作共享资源。有可能破坏程序的执行结果)。

Lock相关实现类实现锁机制

为了使用Lock接口实现相关锁功能时,会涉及以下类和接口,这里还是把上篇文章提到的UML图展示出来:

 

lock.png

 

 

上图中,

  1. 绿色部分为:其中ReentrantLock(重入锁)、WriteLock、ReadLock都是Lock的实现类。Segment为ReentrantLock的子类(在后续文章,ConcurrentHashMap的讲解中我们会提及)。 ReentrantReadWriteLock (读写锁)的实现使用了WriteLock与ReadLock类。
  2. 紫色部分为:其中AbstractQueuedSynchronizerAbstractQueuedLongSynchronizer都为AbstractOwnableSynchronizer的子类,该两个类中都维护了一个同步队列,用于线程的并发执行。在该两个类中拥有名为ConditionObject(为Conditon的实现类)的内部类,只是其内部实现不同。在ConditionObject内部维护了一个等待队列,用于控制线程的等待与唤醒。

基本代码结构

在了解了Lock相关实现类实现锁机制后,这里给实现该锁机制的大致代码结构(根据不同需求,部分方法实现可能不一样,这里只是一个参考,并不是样本代码)。具体代码如下所示:

class LockImpl implements Lock {

    private final sync mSync = new sync();
    @Override
    public void lock() {
        mSync.acquire(1);
    }
    @Override
    public void lockInterruptibly() throws InterruptedException {
        mSync.acquireInterruptibly(1);
    }

    @Override
    public boolean tryLock() {
        return mSync.tryAcquire(1);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return mSync.tryAcquireNanos(1, unit.toNanos(time));
    }

    @Override
    public void unlock() {
        mSync.release(1);
    }

    @Override
    public Condition newCondition() {
        return mSync.newCondition();
    }
    
	 //这里也可以继承AbstractQueuedLongSynchronizer
    private static class sync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean isHeldExclusively() {...}
        @Override
        protected boolean tryAcquire(int arg) {...}
        @Override
        protected boolean tryRelease(int arg) {...}
        @Override
        protected int tryAcquireShared(int arg) {...}
        @Override
        protected boolean tryReleaseShared(int arg) {...}
        final ConditionObject newCondition() {...}
    }
}
复制代码

从代码中我们可以看出,在整个Lock接口下实现的锁机制中,AQS(这里我们将AbstractQueuedSynchronizer 或AbstractQueuedLongSynchronizer统称为AQS)是实现锁的关键,整个锁的实现是在Lock类的实现类中聚合AQS来实现的,从代码层面上来说,Lock接口(及其实现类)是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节。AQS与Condition才是真正的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。

总结

  1. Lock接口(及其实现类)相比synchronized有如下优点:
  • 锁的释放与获取不在是隐式的,允许锁在不同的作用范围内获取和释放`,并允许以任何顺序获取和释放多个锁。
  • 能被中断的获取锁,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常会被抛出,同时锁也会被释放
  • 超时获取锁:在指定的截止时间之前获取锁,如果截止时间到了任然无法获取到锁,则返回。
  1. 在使用Lock的时候注意,一定要确保必要时释放锁
  2. 在整个Lock接口下实现的锁机制中,AQS(上文进行了统称)与Condition才是真正的实现者。

 

一、前言

  在分析了AbstractQueuedSynchronier源码后,接着分析ReentrantLock源码,其实在AbstractQueuedSynchronizer的分析中,已经提到过ReentrantLock,ReentrantLock表示下面具体分析ReentrantLock源码。

二、ReentrantLock数据结构

  ReentrantLock的底层是借助AbstractQueuedSynchronizer实现,所以其数据结构依附于AbstractQueuedSynchronizer的数据结构,关于AQS的数据结构,在前一篇已经介绍过,不再累赘。

三、ReentrantLock源码分析

  3.1 类的继承关系 

public class ReentrantLock implements Lock, java.io.Serializable

  说明:ReentrantLock实现了Lock接口,Lock接口中定义了lock与unlock相关操作,并且还存在newCondition方法,表示生成一个条件。

  3.2 类的内部类

  ReentrantLock总共有三个内部类,并且三个内部类是紧密相关的,下面先看三个类的关系。

  说明:ReentrantLock类内部总共存在Sync、NonfairSync、FairSync三个类,NonfairSync与FairSync类继承自Sync类,Sync类继承自AbstractQueuedSynchronizer抽象类。下面逐个进行分析。

  1. Sync类

  Sync类的源码如下  

 View Code

  说明:Sync类存在如下方法和作用如下。

  2. NonfairSync类

  NonfairSync类继承了Sync类,表示采用非公平策略获取锁,其实现了Sync类中抽象的lock方法,源码如下。

 View Code

  说明:从lock方法的源码可知,每一次都尝试获取锁,而并不会按照公平等待的原则进行等待,让等待时间最久的线程获得锁。

  3. FairSyn类

  FairSync类也继承了Sync类,表示采用公平策略获取锁,其实现了Sync类中的抽象lock方法,源码如下。 

 View Code

  说明:跟踪lock方法的源码可知,当资源空闲时,它总是会先判断sync队列(AbstractQueuedSynchronizer中的数据结构)是否有等待时间更长的线程,如果存在,则将该线程加入到等待队列的尾部,实现了公平获取原则。其中,FairSync类的lock的方法调用如下,只给出了主要的方法。

  说明:可以看出只要资源被其他线程占用,该线程就会添加到sync queue中的尾部,而不会先尝试获取资源。这也是和Nonfair最大的区别,Nonfair每一次都会尝试去获取资源,如果此时该资源恰好被释放,则会被当前线程获取,这就造成了不公平的现象,当获取不成功,再加入队列尾部。

  3.3 类的属性

复制代码

public class ReentrantLock implements Lock, java.io.Serializable {
    // 序列号
    private static final long serialVersionUID = 7373984872572414699L;    
    // 同步队列
    private final Sync sync;
}

复制代码

  说明:ReentrantLock类的sync非常重要,对ReentrantLock类的操作大部分都直接转化为对Sync和AbstractQueuedSynchronizer类的操作。

  3.4 类的构造函数

  1. ReentrantLock()型构造函数  

 View Code

  说明:可以看到默认是采用的非公平策略获取锁。

  2. ReentrantLock(boolean)型构造函数

 View Code

  说明:可以传递参数确定采用公平策略或者是非公平策略,参数为true表示公平策略,否则,采用非公平策略。

  3.5 核心函数分析

  通过分析ReentrantLock的源码,可知对其操作都转化为对Sync对象的操作,由于Sync继承了AQS,所以基本上都可以转化为对AQS的操作。如将ReentrantLock的lock函数转化为对Sync的lock函数的调用,而具体会根据采用的策略(如公平策略或者非公平策略)的不同而调用到Sync的不同子类。

  所以可知,在ReentrantLock的背后,是AQS对其服务提供了支持,由于之前我们分析AQS的核心源码,遂不再累赘。下面还是通过例子来更进一步分析源码。

四、示例分析

  4.1 公平锁 

复制代码

package com.hust.grid.leesf.abstractqueuedsynchronizer;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class MyThread extends Thread {
    private Lock lock;
    public MyThread(String name, Lock lock) {
        super(name);
        this.lock = lock;
    }
    
    public void run () {
        lock.lock();
        try {
            System.out.println(Thread.currentThread() + " running");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } finally {
            lock.unlock();
        }
    }
}

public class AbstractQueuedSynchonizerDemo {
    public static void main(String[] args) throws InterruptedException {
        Lock lock = new ReentrantLock(true);
        
        MyThread t1 = new MyThread("t1", lock);        
        MyThread t2 = new MyThread("t2", lock);
        MyThread t3 = new MyThread("t3", lock);
        t1.start();
        t2.start();    
        t3.start();
    }
}

复制代码

  运行结果(某一次): 

Thread[t1,5,main] running
Thread[t2,5,main] running
Thread[t3,5,main] running

  说明:该示例使用的是公平策略,由结果可知,可能会存在如下一种时序。

  说明:首先,t1线程的lock操作 -> t2线程的lock操作 -> t3线程的lock操作 -> t1线程的unlock操作 -> t2线程的unlock操作 -> t3线程的unlock操作。根据这个时序图来进一步分析源码的工作流程。

  ① t1线程执行lock.lock,下图给出了方法调用中的主要方法。

  说明:由调用流程可知,t1线程成功获取了资源,可以继续执行。

  ② t2线程执行lock.lock,下图给出了方法调用中的主要方法。

  

  说明:由上图可知,最后的结果是t2线程会被禁止,因为调用了LockSupport.park。

  ③ t3线程执行lock.lock,下图给出了方法调用中的主要方法。

  说明:由上图可知,最后的结果是t3线程会被禁止,因为调用了LockSupport.park。

  ④ t1线程调用了lock.unlock,下图给出了方法调用中的主要方法。

  说明:如上图所示,最后,head的状态会变为0,t2线程会被unpark,即t2线程可以继续运行。此时t3线程还是被禁止。

  ⑤ t2获得cpu资源,继续运行,由于t2之前被park了,现在需要恢复之前的状态,下图给出了方法调用中的主要方法。

  说明:在setHead函数中会将head设置为之前head的下一个结点,并且将pre域与thread域都设置为null,在acquireQueued返回之前,sync queue就只有两个结点了。

  ⑥ t2执行lock.unlock,下图给出了方法调用中的主要方法。

  说明:由上图可知,最终unpark t3线程,让t3线程可以继续运行。

  ⑦ t3线程获取cpu资源,恢复之前的状态,继续运行。

  说明:最终达到的状态是sync queue中只剩下了一个结点,并且该节点除了状态为0外,其余均为null。

  ⑧ t3执行lock.unlock,下图给出了方法调用中的主要方法。

  说明:最后的状态和之前的状态是一样的,队列中有一个空节点,头结点为尾节点均指向它。

  使用公平策略和Condition的情况可以参考上一篇关于AQS的源码示例分析部分,不再累赘。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值