java并发笔记之闭锁源码解读

本文详细解析了Java并发工具类CountDownLatch的构造器、await()和countDown()方法,以及FutureTask的构造、run()、get()和cancel()方法。CountDownLatch利用AQS实现等待队列,确保线程安全地同步执行。FutureTask则提供了对异步计算结果的管理和取消功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

1、CountDownLatch

1.1、类关系:

1.2、大体叙述

1.3、构造器

1.4、await()方法

1.5、countDown()方法:

1.6、另:

2、FutureTask

2.1、类关系:

2.2、大体介绍

2.3、构造器

2.4、Run()方法

2.5、get()方法

2.6、cancel()方法


1、CountDownLatch

 

 

 

 

1.1、类关系:

Sync为CountDownLatch的内部。

AbstractQueuedSynchronizer的是Sync的父类

 

 

 

 

 

1.2、大体叙述

关于CountDownLatch主要方法有两个:await()   and   countDown()。还有构造器以及内部类syn以及父类,由于他的内部是继承自AQS,AbstractQueueSynchronizer。利用了AQS内部状态state变量记录等待队列的长度,

至于CountDownLatch的节点状态则有AQS的Node节点提供。

 

 

 

 

 

 

 

1.3、构造器

CountDownLatch的构造器只有一个,必须带有参数,用以构造Sync的父类中的state变量,以定义状态

 

 

 

 

1.4、await()方法

这个方法主要用以线程请求等待,并且加入等待队列的,等待队列又由Sync的父类AbstractQueuedSynchronizer提供,AbstractQueuedSynchronizer中自定义节点Node,双向链表形式实现等待队列

 

这里请求中断

 

 

随后的tryAcquireShared在父类中是个抽象方法,由sync自己实现。getState()是我们一开始构造器传入的参数,通常会给1或者线程数量,因此通常都是不为0的,-1返回,获取共享状态成功,执行上图中doAcquireSharedInterrupt方法,请求共享中断

 

 

 

首先,在线程中断前,将当前线程加入等待队列,Node.SHARED是申请一个空节点做哨兵,关于加入等待队列的细节,请接着往下看

 

 

 

加入等待队列ing:

首先做出currentThread()   ->  哨兵空节点,以随后的cas中空指针异常。然后,我们预中断节点的prev指向等待队列中的尾节点,再以cas去吧尾节点的next指针指向自己,cas是这句

compareAndSetTail(pred, node)

关于cas我们也能从中知道,正如efftive java中所指示的那样,尽量把需要同步的原子代码范围缩小,以提高吞吐量,包括内置锁,重入锁,cas等等等

 

 

 

加入等待队列ing:

若是cas失败那会怎么样?会自旋,一直去cas添加到尾节点。这点由方法enq保证,一下是cas自旋源码。

 

 

 

 

当前线程在未中断前加入了等待队列,此时,不难想到,随后该是中断了。

这里需要从后往前看,try中第一个if是去确定队列中的所有节点都已停靠完毕,于是状态false,返回。若是未知原因导致false没有成功,那么失败检查会让我们cancelAcquice(node)取消当前线程的入队等待。而第二个if则是去修改线程node的状态后,中断。

 

这里从第二个if看

shouldParkAfterFailedAcquire,1、如果它本身就是等待状态SIGNAL,-1的,那么很ok。2、如果是>0的,目前大于0只有一种情况,取消等待了即为1,do while从等待队列把所有前置,并且连着的取消等待节点全部从等待队列中移除,这句话有点难以理解,实际上就是一种优化,我看看你前面的是不是也是取消等待的,是的话,接着看你前面的前面,直到非等待节点为止,将中间这一块扣除。3、0既是默认,那么很正常的cas设置SIGNAL标记,表示线程以停靠。然后返回false是为了让之前的for循环中自旋确认。

而parkAndCheckInterrupt()很简单,禁用并且检查当前线程是否已经中断,没中断的话,我在中断一下。

 

 

 

再看第一个if

这里就是个检查

final Node p = node.predecessor();

后面的tryAcquireShared,我们前面解释过,还是个检查,对CountDownLatch的一个检查

int r = tryAcquireShared(arg);
if(r>=0)

至于setHeadAndPropagate()方法,注释也表明了

 

总结:

await()方法,是用以讲当前线程停靠进CountDownLatch的等待队列中,并且设置装载了线程的Node容器的状态。然后每次停靠都要确认队列中线程状态如何,以确保安全。

 

 

 

 

 

 

1.5、countDown()方法:

见名知意,这里是要激活了,讲await()方法中断停靠的线程全部激活释放,让其继续运行。1是状态参数。

然后,尝试释放,循环释放

tryReleaseShared()方法比较简单,他也是父类的抽象方法,由sync覆写实现。自旋cas去改变我们初始化时候的状态值state,将其设置为0,注意这里cas的实现导致只有一个线程能拿到true,因此也只有一个线程去执行“释放”

然后是doReleaseShared()方法,这里可以很清楚的看到,如果节点是停靠状态,那么cas设置成默认0,然后unpark去取消阻塞状态,成功后又会从0变为PROPAGATE:-3状态,结束。这段代码不难理解,值得一看的也就是unparkSuccessor()方法。

 

 

 

unparkSuccessor(),这里

if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

还需要检查cas设置一遍线程状态为默认0值,影响不大,然后,这里

if (t.waitStatus <= 0)
                    s = t;

把所有0值,一直向前找到最后一个状态变为默认值的节点,最后LockSupport.unpark()唤醒s节点的线程

 

 

关于LockSupport.unpark()方法:注释提到,唤醒线程,给他cpu执行权,不再阻塞。

 

结论:countDown就是唤醒等待队列中所有线程,并且状态值无法为外部用户改变,他先将自己状态改变才去唤醒,而唤醒过程是自tail往前prev找,找到最靠前那个唤醒,以此顺序唤醒,以停靠顺序依次唤醒。

 

 

 

 

 

1.6、另:

还有一点未讲,看到这里,我们源码已经看结束,再回过头来不禁有个疑问,形如:

class Driver { // ...
  void main() throws InterruptedException {
    CountDownLatch startSignal = new CountDownLatch(1);
    CountDownLatch doneSignal = new CountDownLatch(N);

    for (int i = 0; i < N; ++i) // create and start threads
      new Thread(new Worker(startSignal, doneSignal)).start();

    doSomethingElse();            // don't let run yet
    startSignal.countDown();      // let all threads proceed
    doSomethingElse();
    doneSignal.await();           // wait for all to finish
  }
}
class Worker implements Runnable {
  private final CountDownLatch startSignal;
  private final CountDownLatch doneSignal;
  Worker(CountDownLatch startSignal, CountDownLatch doneSignal) {
    this.startSignal = startSignal;
    this.doneSignal = doneSignal;
  }
  public void run() {
    try {
      startSignal.await();
      doWork();
      doneSignal.countDown();
    } catch (InterruptedException ex) {} // return;
  }
  void doWork() { ... }
}}

的开始门结束门代码,那么我们主线程和次线程,哪个线程先运行到countDown不定,那么我们岂不是无法保证全部线程都进入等待队列了之后才能去唤醒了?这违背闭锁原理。原因如下:

为了方便讲述,我们的初始化传入状态值开始边制定了等待队列的容量,必须等到起了以后才能去release。简图如下:构造器代码就不再讲述了,这里是由countDown去判断的,而await传入伪数值1,实际到最后也是不管的,个人认为,算是一个设计上的失误,导致代码并没有那么好看。

最后这里最关键:

每次-1知道为0时,才会触发unpark给予cpu执行权的非阻塞操作。

 

 

 

 

 

 

 

 

2、FutureTask

 

 

2.1、类关系:

FutureTask是实现了RunnableFuture接口的

而接口RunnableFuture则是继承自Runnable和Future接口的

而关于这两个接口主要方法如下所示:

 

 

根据接口方法名一目了然

 

 

 

 

 

 

 

 

2.2、大体介绍

关于FutureTask的大体叙述,可通过下列注释明白。a cancellable asynchronous computation,这一句话已经说的很明白了,一种可取消的一部计算。并且,关于取消,Once the computation has completed,the computation cannot be restarted or cancelled,一旦计算完成,那么就不能重启或者取消计算。除非使用保护方法runAndReset。

然后是关于FutureTask中状态叙述:

new -> 计算中 -> 正常

new -> 计算中 -> 异常

new -> 取消

new -> 中断中 -> 已中断

这里讨论翻译没有任何意义,比起completing完成中,我更愿意叫他计算中。

 

对于FutureTask的的方法有很多,但大多是私有的,提供对外访问的方法很少,其中最重要的,也就是get方法和run方法,还有cancel()方法。其他辅助方法isCancelled和isDone方法见名知意,无非是查看状态值,看看处于哪个阶段。而:

run()方法:不难猜到,它是用来运行线程run方法的,并且监控方法执行状态,是进行中还是异常退出或是中断了

get()方法:用来得到线程运行结果的。按注释的用语,也即是得到计算结果

cancel()方法:取消计算

后续会对这三个方法做源码分析

 

 

 

 

首先,我们需要明确,FutureTask是很明确,简单清晰易懂的,因为他不同于CountDownLatch一样,他仅仅支持一个Task一个线程,而CountDownLatch更倾向于相当于一个容器,里面很多操作可能是多线程并发的操作,而Task可以简单的看成单线程操作,所以他里面的CAS更多的并非是去考虑并发安全性,而是真正意义上的安全,防止的是程序执行期间的恶意修改内存,导致程序崩溃,譬如反射,OE等等等

 

 

 

 

 

 

 

2.3、构造器

构造器有俩,分别支持了callable和runnable,并且初始化时,都会标记该“未来任务”为new新建。

这里需要注意一下,传入Runnable会转成callable,这个的实现是由Executors中提供的Runnable和Callable适配器类实现,这个适配器思想蛮实用的,值得借鉴。而由于Runnable是无返回值的,这里需要手动指定一个result。

 

 

 

 

 

 

 

2.4、Run()方法

他是整个类中的核心方法。

 

if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;

一重检查,防止线程重复运行。然后没什么好说的,状态检查,以及执行Callable线程。然后完成后,set状态正常完成或者是指定异常,这里两段套路都是一样的。

注意,这里显示获取结果写入变量outcome,再修改状态变量到正常结束或是异常,咋一看没什么问题,但是稍有经验的人都会注意到一个问题,那就是,如果这里发生指令重排怎么办,他不是原子的,结果获取和最终状态改变并没有必然性,如果发生状态优先于结果写入,那么就是大问题。这是不是jdk的一个bug?发现这个问题,可以认为是对计算机有了一定理解的程序员了。但是,我们注意到,putOrderedInt,这个unsafe的方法,其实他是会设置内存屏障的,也就是说putOrderedInt方法保证先改变结果后改变状态的语义。

最后再来看看finishCompletion会干什么?取消等待链表的阻塞状态,置空当前callable。一些原子操作都不难理解,只是这里突然冒出个WaitNode和waitersOffset有点突兀。这里暂且压下这个疑问

 

 

 

 

 

 

 

 

 

 

 

2.5、get()方法

首先,这个方法是用于获取构造器传入的Callable的执行结果的。

由上图不难看出,主要根据状态返回报告,而下图也给出report的源码,获取run中提到的outcome结果,当然也仅仅是正常结果下的outcome,如果是new -> completing -> exceptioned那么直接到最后一句,抛出给get处理。

这些都没有什么好介绍的,稍微关键的是

if (s <= COMPLETING &&
            (s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
            throw new TimeoutException();

当callable还未处理完成时会怎么处理?来自awiatDone方法如下:博主已经把每行注释清楚了

/**
     * Awaits completion or aborts on interrupt or timeout.
     *
     * @param timed true if use timed waits
     * @param nanos time to wait, if timed
     * @return state upon completion
     */
    private int awaitDone(boolean timed, long nanos)
        throws InterruptedException {
        final long deadline = timed ? System.nanoTime() + nanos : 0L;
        WaitNode q = null;
        boolean queued = false;
        //自旋
        for (;;) {
            //进入get的线程中断了,那么就从等待队列中移除
            if (Thread.interrupted()) {
                removeWaiter(q);
                throw new InterruptedException();
            }

            int s = state;
            //构造器传入的callable执行完成,不管是何种状态的的完成,只要有了结果,那么返回
            if (s > COMPLETING) {
                if (q != null)
                    q.thread = null;
                return s;
            }
            //callable还在执行中时,让出cpu执行权
            else if (s == COMPLETING) // cannot time out yet
                Thread.yield();
            //这里状态已经被以上两种情况用完了,只剩下new,那么加入等待节点
            else if (q == null)
                q = new WaitNode();
            //后续都是自旋中,加入等待节点的当前线程的操作
            //这一行的原子操作请看文章接下来讲解
            else if (!queued)
                queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                     q.next = waiters, q);
            //get有两个,一个是无参,代表即时get得到结果,一个时有参传入TimeUtil,多久以后尝试get得到计算结果,这里是为了第二种get准备的
            else if (timed) {
                nanos = deadline - System.nanoTime();
                if (nanos <= 0L) {
                    removeWaiter(q);
                    return state;
                }
                LockSupport.parkNanos(this, nanos);
            }
            //自旋得不到结果时,都已经加入等待节点了,那么让他转入阻塞,阻止资源无故开销
            else
                LockSupport.park(this);
        }
    }

看到这里一目了然,包括看run时候,留下的WaitNode伏笔,也在这里得到解释,那么关于加入等待队列怎么说?

WaitNode是FutureTask的一个内部类,初始化时获得当前线程存入,awaitDone里面的new WaitNode,便是如此,随后的waitoffset会记录FutureTask等待链表尾巴到了那里,if(!queued)随后的原子操作,会把WaitNode的初始化节点加入等待队列

 

 

 

 

 

 

 

 

2.6、cancel()方法

先上图,随后逐句解释

文章最初介绍过,无法重启或取消计算完成后计算,实际上那段描述并不准确,为什么呢?我们在解析run()时有看到源码,if(Completing)之后才是得到结果,再设置为Completed或是excetion,不管代码怎样接近,都可能有秒级间隔,虽然new状态占了绝大部分实际,并且真正的callable的执行期间也是在new状态下执行的,所以注释并不完全,这里保证了,只要你是非new状态,只要我futuretask没有拿到结果,那么都视为没有取消成功的。同时,只要你是new状态,即使是callable计算完成,也可以取消成功。

然后就是

UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
                  mayInterruptIfRunning ? INTERRUPTING : CANCELLED)

根据传入参数决定new转换为什么状态,中断?或是被取消?ture让其中断,false让其取消,之后的代码都不用看了,如果中断,那么就让进入get的线程真正的中断。

finishCompletion我们之前也是看到过的,这个方法蛮重要的,因为get自旋时会加入等待队列并挂起,这个方法总在finally处的保证,能让随后从等待节点中unpark并且移除等待队列,他在run最后会运行,在cancel最后也会运行。从而保证了线程不会死在FutureTask内部,而是很多情况都能激活它。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值