线程池ThreadPoolExecutor源码分析二(ReentrantLock)

目录

一、概述

二、类结构

三. lock()方法

1.1 lock()源码分析


一、概述

可重入锁,锁如其名,可以重复进入:当一个线程事先拥有了锁之后,后续该线程还可以再次拥有该锁(实现方式是锁状态state + 1,每获取一次,state + 1,每释放一次,state - 1,当state=0(即初始状态)就认为该锁没有被任何线程占用,处于空闲状态,此时任何线程都可以来占用该锁,相应的,如果state > 0,则只允许占有该锁的线程修改state的值)。

二、类结构

ReentrantLock类内部,只有一个成员变量,就是Sync类,所有的方法都通过该类和该类的子类实现。而除了Sync变量,它存在三个内部类,一个是继承了上一章提到的AbstractQueuedSynchronizer抽象类的Sync抽象类,剩下两个分别是NonfairSync(不公平锁)和FairSync(公平锁)子类,俩个子类分别实现了获取锁的方式,区别在于,非公平锁,当节点尝试获取锁时,只需要判断当前锁有没有被占用,以及能不能获取到,如果可以,那么该节点就获取锁,其他节点此时就无法获取,而至于哪个节点能获取到锁,无法预判保证;而公平锁,在判断当前锁是否被占用以及能否被该节点获取之前,事先判断了当前节点是否是头部节点,即当前节点是否是第一个节点,如果不是,那么会直接返回false,退出竞争锁。

ReentrantLock类的大概结构如下:

    public class ReentrantLock {
        private final Sync sync;

        abstract static class Sync extends AbstractQueuedSynchronizer {
        }

        static final class NonfairSync extends Sync {
        }

        static final class FairSync extends Sync {
        }

        /*
         * 下面是lock()方法涉及到的部分重要成员变量,基本都是父类AbstractQueuedSynchronizer类中,以及该类的内部类Node中
         * int state 锁状态,0为初始状态,一旦被占有一次,值+1
         * Node head 头部节点,等待队列中第一个节点
         * Node tail 尾部节点,等待队列中最后一个节点
         * Thread exclusiveOwnerThread AbstractQueuedSynchronizer父类AbstractOwnableSynchronizer中的变量,保存锁的持有线程
         * Node prev AbstractQueuedSynchronizer静态内部类Node的成员变量,记录当前节点前一个节点的引用
         * Node next 记录当前节点后一个节点的引用
         * Thread thread 记录当前节点封装的线程
         */
    }

有了一个大概的了解之后,下面就开始分析该类主要使用的一个方法lock()

三. lock()方法

在分析源码之前,首先先大概叙述一下方法执行过程,有一个初步印象:

当使用ReentrantLock实例,调用lock()方法时,其实例通过调用内部类Sync的lock()方法实现,而Sync类的lock()方法是被声明为抽象的,即,该方法实际执行过程是其子类定义;在ReentrantLock类中,除了Sync,还有上面讲到的NonfairSync和FairSync俩个静态内部类,这两个类就提供了lock()方法的具体实现(分析主要以NonfairSync的实现为主,因为默认情况下是以NonfairSync为实现类,而且FairSync基本就是多了条件的NonfairSync,这两个类实现大致相同。)。

在NonfairSync中,lock()方法执行步骤分两种情况,第一种:获取到锁,然后结束方法;第二种:先尝试获取锁,如果获取到了,直接结束方法,如果没有获取到,则把当前线程封装为一个Node节点,再把该节点插入等待队列(实际上不存在List类型的队列,算一个虚拟队列,通过Node节点,来保存前一个节点和下一个节点的引用实现的类似LinkedList结构的队列),之后会进行一个死循环来尝试让头结点获取锁资源,如果在循环时获取到了锁,那么会标记该线程是否被其他线程打断或者因为外部因素被打断等,最后结束方法。

有了一个大概印象之后,下面再仔细分析源码,看看是如何实现的:

1.1 lock()源码分析

第一步,调用ReentrantLock的lock()方法,如:

        //实际上该变量应该是一个线程共享的变量,这里仅仅表明调用lock()的过程
        ReentrantLock reentrantLock = new ReentrantLock();
        reentrantLock.lock();
        // xxx 执行操作
        reentrantLock.unlock();

之后我们进入lock()方法,会看到,实际上是调用的sync.lock()

 而在Sync中,lock()方法被定义为抽象方法,由子类实现:

下面分析NonfairSync子类的实现过程:

 首先,compareAndSetState(int x, int y)方法,该方法由UnSafe类提供,原子操作,不可中断,该方法的实现是由上一章讲到的AbstractQueuedSynchronizer顶层抽象类中的Unsafe提供,Unsafe类主要提供各种变量改变的原子操作,通过方法名能够知晓,而state变量也是AbstractQueuedSynchronizer类中的成员变量,表明当前锁状态,0为初始状态;

如果compareAndSetState方法返回true,说明锁状态state被当前线程修改,那么当前线程应该持有该锁,所以设置当前线程为锁的拥有者setExclusiveOwnerThread方法,将当前线程赋值给最顶层抽象类AbstractQueuedSynchronizer的父类AbstractOwnableSynchronizer的成员变量exclusiveOwnerThread(Thread类型),作用就是保存锁的拥有者是哪个线程,以便后续判断使用。

如果state值不为0,说明已经被其他线程修改过,那么锁就已经被其他线程占有了,这时调用acquire(int x)方法,等待其他线程释放锁并去争夺锁,这里需要记一下调用acquire方法,参数为1;下面看看acquire方法,该方法由抽象类AbstractQueuedSynchronizer提供,如图:

 在acquire(int x)方法中,首先调用了一个方法tryAcquire(int x),跳转到子类NonfairSync中可以看到该方法的具体实现步骤,如下:

 当通过tryAcquire(int x)获取到锁之后,方法会因为不满足if条件直接结束方法并且当前线程获取到了锁,可以执行xxx.lock()之后的代码了;下面继续分析如果还不能获取到锁的后续操作:

首先看一看addWaiter(Node x)方法,该方法又抽象类AbstractQueuedSynchronizer提供,源码如下:

 

 

 在addWaiter(Node mode)方法内,

第一步,通过Node的一个构造方法,创建了一个新的Node节点,该构造方法旁注释表明,这个构造方法专门设计为addWaiter时使用,构造方法内,将Node类型的参数赋值给了nextWaiter变量,将当前线程赋值给了thread变量;而通过上面,可以知道,调用addWaiter(Node mode)方法时,传递的mode,是一个特殊值Node.EXCLUSIVE;在静态内部类Node中,可以知道,该静态常量值为null,所以执行到这里时,nextWaiter = null;thread = Thread.currentThread()

第二步,获取等待队列的尾部节点,即最后一个节点的引用;

第三步,判断尾部节点是否为null,因为初始状态,等待队列是不存在的,既没有头部节点,也没有尾部节点;如果存在尾部节点,则执行以下操作:1.将当前节点Node内表示该节点前一个节点的变量prev赋值为尾部节点的引用;2.通过unsafe类的原子操作,比较当前尾部节点是否是上面步骤中获取到的尾部节点(防止在执行过程中,其他节点先一步插入,导致尾部节点改变),如果是,那么修改尾部节点为当前节点,并将上一个尾部节点的next指向当前节点,最后返回当前节点的引用;这个步骤可以参考linkedlist插入,就是通过改变引用值,达到队列的插入实现。

如果不存在尾部节点,说明此时只有该节点需要等待锁被释放,那么调用enq(Node node)方法,将当前节点插入,形成队列,下面看一看enq(Node node)方法的源码:

 再进行插入节点操作中,会进入一个死循环,只有成功插入节点之后才会返回,否则会重复刷新获取到的尾部节点,最后尝试把当前插入的节点放入尾部节点位置;

在插入过程中,当等待队列没有被初始化,即此时不存在尾部节点(包括头部节点也不存在,源码里用的尾部节点进行判断),那么会初始化一个空的节点,最后将当前节点设置为尾部节点,插入到空的节点后方;如果存在队列,那么只需要把当前节点插入到尾部。

这个方法需要注意的是,该方法的返回值,它返回的不是当前节点,而是当前节点的前一个节点信息(这也说明为什么必须初始化一个头部节点,因为每次第一个等待节点到来,都是没有前一个节点的,所以必须初始化一个空的头部节点,否则会返回null);至于为什么返回前一个节点,通过后续步骤以及其他方法的分析是可以解释的,先放在这里暂且不论。

现在,已经将等待获取锁的线程封装成一个Node并且放入了等待队列中了,接下来就是执行最后一个方法acquireQueued(Node node, int args)

需要注意的是,这里该方法的第一个Node参数,是使用的addWaiter方法的返回值,该方法的返回值在上面已经叙述过了,返回的是当前节点,并不是刚刚说到的enq(Node node)方法的返回值,enq方法的返回值是当前节点的前一个节点。下面是该方法的源码:

 

 该方法内部,会在死循环中重复尝试去获取锁,获取锁的方法是上面已经分析过的tryAcquire(int x)方法,是由Sync的子类提供实现,公平锁和非公平锁的实现是不一致的,公平锁再每次获取锁之前都会判断当前节点是否为头结点,只有头结点才能会尝试去获取锁,如果不是头节点,会直接放弃竞争锁。而根据这里的源码,可以知道,如果当前节点的前一个节点不是头部节点,那么他也是不会去竞争锁的,那么是否存在这样一个疑问?既然插入队列的之后的节点,想要获取锁的前提必须是他的前一个节点为头部节点,那是不是公平锁和非公平锁其实没有区别?其实并不是,因为公平锁,无论何时,只要执行竞争锁资源,那么这行代码之前一定会判断当前节点是否是头部节点,而非公平锁只有在这个时候才会进行一个筛选判断,想一下下面的这种情况:

当前锁被thread1占有,等待节点中存在thread2,thread3,thread4,此时,新增一个thread5也来获取该锁,而当thread5线程要执行获取锁的代码时,thread1任务完成,释放掉了锁,而且,意外的是,因为操作系统的调度,恰好thread2,thread3,thread4的线程没有被cpu调度执行,而thread5就成了天选之子,它根本不需要进入等待队列,在之前的获取锁的过程中,就一帆风顺的得到了锁并开始执行任务了;在thread5获取锁之后,cpu又调度执行了thread2,thread3,thread4,但是当前锁已经又被重新占有了,虽然这三个线程都不知道锁的持有者已经换人了,但是他们还是只有继续循环等待;而对于公平锁来说,thread5在最开始获取锁时,会判断当前等待队列头部节点是不是他,如果不是,thread5也不会去竞争锁,即使他来的时机非常的巧妙。

所以,公平锁中,获取锁的节点一定是等待时间最久的节点,而非公平锁则不然,天选之子永远要快人一步。

关于shouldParkAfterFailedAcquire(Node p, Node node)方法,后面分析Condition条件等待时会同时分析到,这里只需要知道这个方法的含义,就是判断当前节点在获取锁失败后是否应该被阻塞;而后面的parkAndCheckInterrupt()方法,简单了解一下,源码如下:

park(object obj)方法,是会确确实实阻塞线程的,在上面分析的方法中,很多方法都是通过死循环反复调用判断,这时其实线程依然在占用cpu,而阻塞之后,是不会占有cpu资源的。

关于ReentractLock的lock()方法暂时分析到这里,后面会继续分析该类的unlock()方法以及平时和该类一起配合使用的Condition的相关操作,其实很多方法的执行步骤,是需要其他方法一起分析才能完全明白源码为什么需要那样操作。这里仅仅只分析了与lock()相关的主要步骤。

总结一下,调用ReentractLock的lock()方法时,存在两种实现,一种是非公平锁(该锁也是默认实现),一种是公平锁(在创建ReentractLock类时,调用相应构造方法),

public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

 公平锁在每次获取锁之前会判断当前线程封装的节点是否是头部节点,如果不是,直接返回false,退出锁竞争,等待节点会通过死循环来重复获取锁资源;

非公平锁获取锁只需要做基础的共有的判断,即锁是否被占有,如果没有就尝试去获取,如果被占有了,继续判断占有锁的线程是否是当前线程,如果是,那么重复占有,state状态+1,如果不是,则返回false,将封装的节点插入等待队列。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值