并发编程学习笔记(9)----AQS的共享模式源码分析及CountDownLatch使用及原理 并发编程学习笔记(9)----AQS的共享模式源码分析及CountDownLatch使用及原理

1. AQS共享模式

  前面已经说过了AQS的原理及独享模式的源码分析,今天就来学习共享模式下的AQS的几个接口的源码。

  首先还是从顶级接口acquireShared()方法入手:

public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }

  与acquire()方法一样,tryAcquireShared()为自己是实现的对资源获取的接口,AQS对返回值的语义已经定义好了,小于0表示失败,0表示成功,但是没有剩余资源,大于0表示成功,且还有剩余资源,其他线程还可以去获取,所以这里的流程就是,先调用tryAcquireShared();当不能获取资源时,调用doAcquireShared()方法让线程进入等待队列。

  doAcquireShared(int)方法,该方法用于将当前线程放入到等待队列中等待,直到其他线程唤醒并成功获取到资源才开始执行。源码如下:

 private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED); boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head) { int r = tryAcquireShared(arg); if (r >= 0) { setHeadAndPropagate(node, r); p.next = null; // help GC if (interrupted) selfInterrupt(); failed = false; return; } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); }

  将当前线程加入到等待队列队尾,并返回当前线程所在的节点,标记是否成功,判断是否被中断,获取当前节点的前驱,如果前驱不为空,如果前驱等于头节点,则表示当前线程被唤醒,因为头节点是持有资源的线程,当前节点可能会被头节点唤醒,尝试去获取资源,r>=0,表示获取成功,将当前节点设置为头节点,如果还有资源可以尝试唤醒下一个等待线程。判断是够被中断过,如果中断过,则清除中断标记,shouldParkAfterFailedAcquire()判断线程状态是否可以等待并找一个能够被唤醒的点进入等待,等着被unpark()或interrupt(),parkAndCheckInterrupt()使线程被waiting。

  跟独占模式相比,这里是将selfInterrupt()放到了doAcquireShared()中,具体为啥我也不知道,但是这里的问题是,当第一个线程执行完之后释放资源,可能释放的资源只有3个,但是当前线程需要4个,而后面一个线程只需要2个,再后一个线程只需要1个,这种情况下,当前线程也是不会去唤醒后两个线程的,它会继续等待着其他的线程释放资源,独享模式下这样没问题,但是在共享模式下,多个线程可以同时执行,这样的策略会使得后面的两个线程会因为没被唤醒而没法执行,其实也算是问题,这里是cas严格保证了入队顺序和出对顺序,降低了并发,但是却是保证了安全的。

  setHeadAndPropagate(Node, int)是将当前线程设置为头节点,当资源还有剩余的情况下去唤醒其他资源。

private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below  setHead(node); /* * Try to signal next queued node if: * Propagation was indicated by caller, * or was recorded (as h.waitStatus either before * or after setHead) by a previous operation * (note: this uses sign-check of waitStatus because * PROPAGATE status may transition to SIGNAL.) * and * The next node is waiting in shared mode, * or we don't know, because it appears null * * The conservatism in both of these checks may cause * unnecessary wake-ups, but only when there are multiple * racing acquires/releases, so most need signals now or soon * anyway. */ if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; if (s == null || s.isShared()) doReleaseShared(); } }

  setHead将当前节点设置为头节点,当资源还有剩余的情况下,唤醒当前节点的相邻节点。

  共享模式的流程就是尝试获取资源,获取资源失败,则进入等待,与独享模式相比,共享只是多了在资源剩余的情况下去唤醒其他线程的操作而已。

  releaseShared()共享模式下释放共享资源的顶级入口,释放指定量的资源,如果成功释放且允许唤醒其他线程来获取资源,则它会唤醒队列里的其他等待线程来获取资源,源码:

 public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; }

  调用tryReleaseShared()尝试释放资源,这里的tryReleaseShared()也是自己实现的,成功,则调用doReleaseShared()唤醒后继节点,

扫描二维码关注公众号,回复: 6246199 查看本文章

  doReleaseShared()用于唤醒后继节点

  源码:

private void doReleaseShared() {
        /*
         * Ensure that a release propagates, even if there are other
         * in-progress acquires/releases.  This proceeds in the usual
         * way of trying to unparkSuccessor of head if it needs
         * signal. But if it does not, status is set to PROPAGATE to
         * ensure that upon release, propagation continues.
         * Additionally, we must loop in case a new node is added
         * while we are doing this. Also, unlike other uses of
         * unparkSuccessor, we need to know if CAS to reset status
         * fails, if so rechecking.
         */ for (;;) { Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases  unparkSuccessor(h); } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS  } if (h == head) // loop if head changed break; } }

  自旋,通过unparkSuccessor()唤醒后继节点,这样就释放掉了资源。

  以上就是关于AQS共享模式的源码的分析。

2. CountDownLatch的使用及原理

  CountDownLatch是jdk并发包中提供的负责并发编程的类,它也是AQS中共享模式的一种运用,利用它可以实现类似于计数器的功能,比如当以个线程需要等待其他几个线程的结果,但是其他几个线程又需要并行的执行时,就可以利用该类来实现,这里我们以一个求和的程序来举例该类的用法,

  2.1 使用方式

  存在一个文件中有如下数据

12,13,20,40
50,60,80,90
50,23,40
16,13

  我们需要每一行用一个线程来进行加法计算,当所有线程执行完成后,将所有线程计算结果做一次汇总,实现如下:

  计算每一行之和的代码:

public void calc (String line, int index, CountDownLatch countDownLatch) {
        String[] nus = line.split(",");
        int total = 0; for (String n : nus) { total += Integer.parseInt(n); } nums[index] = total; System.out.println(Thread.currentThread().getName() + " 执行计划任务..." + line + " 结果为:" + total); countDownLatch.countDown(); }

  每一个线程执行计算完毕后都会调用countDownLatch.countDown();使得当前运行线程减一

计算总和的代码:

 public void sum () {
        System.out.println(Thread.currentThread().getName() + "汇总线程开始执行...");
        int total = 0; for (int i = 0; i < nums.length; i++) { total += nums[i]; } System.out.println("总结果为:" + total); }

  计算每一行之和的调用方式和计算总和的调用方式:

package com.wangx.thread.t7;

import java.io.BufferedReader;
import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; public class AddMain { public static void main(String[] args) { //读取文件 final List<String> contents = readFile(); //初始化countDownLatch 有几个线程执行构造参数就传几个 final CountDownLatch countDownLatch = new CountDownLatch(contents.size()); int lineNum = contents.size(); final AddNumber addNumber = new AddNumber(lineNum); //多个线程同时执行 for (int i = 0; i < lineNum; i++) { final int k = i; new Thread(new Runnable() { @Override public void run() { addNumber.calc(contents.get(k), k, countDownLatch); } }).start(); } //等待着直到所有线程执行完之后执行下面的代码 try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } //执行汇总  addNumber.sum(); } private static List<String> readFile() { List<String> contents = new ArrayList<>(); String line = null; try { BufferedReader bufferedReader = new BufferedReader(new FileReader("D:\\JavaDev\\spring\\thread\\src\\com\\wangx\\thread\\t7\\numers.txt")); while ((line = bufferedReader.readLine()) != null) { contents.add(line); } } catch (Exception e) { e.printStackTrace(); } return contents; } }

  先初始化CountDownLatch实例,构造参数为需要并行执行的线程个数,调用await()方法等待,知道所有并行执行的线程执行完成。

  2.2 实现原理(源码分析)

   首先看初始化的构造方法:

 public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0"); this.sync = new Sync(count); }

  它实例化了一个内部同步器Sync,我们继续看Sync的构造:

 Sync(int count) {
            setState(count);
        }

  这里调用了AQS的方法,直接设置了AQS的状态,达到了初始化多少个资源的目的,因为AQS是提供一个原子的int类型state来维护状态的,我们的示例中初始化了contents.size()个资源,接下来看countDown()方法,

 public void countDown() {
        sync.releaseShared(1);
    }

  调用releaseShared()来释放资源,这是AQS原理的运用,我们这里主要看在CountDownLatch中tryReleaseShared()方法的实现:

  

protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero for (;;) { int c = getState(); if (c == 0) return false; int nextc = c-1; if (compareAndSetState(c, nextc)) return nextc == 0; } } }

  实现也很简单,状态为0,资源已经被释放,自旋,更改状态,示例当contents个线程调用countDown完毕之后nextc == 0;才会成立,此时的releaseShared()才回去唤醒其他的等待的线程,示例中是主线程在调用求和的方法。

  await()方法:

public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

  同样是调用内部同步器的方法来执行,

  acquireSharedInterruptibly():

 public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (tryAcquireShared(arg) < 0) doAcquireSharedInterruptibly(arg); }

  该方法的功能是获取当前获取资源,如果获取资源失败,则调用doAcquireSharedInterruptibly()将线程放入到等待队列中等待,

  doAcquireSharedInterruptibly()与doAcquireShared()相似,看源码:

 private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException { final Node node = addWaiter(Node.SHARED); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head) { int r = tryAcquireShared(arg); if (r >= 0) { setHeadAndPropagate(node, r); p.next = null; // help GC failed = false; return; } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } }

  两段代码都与前面共享模式时的doAcquireShared()方法功能相同,只是它是一个可中断的实现。

  这就是CountDownLatch的实现原理,先初始化资源状态,每个线程执行完成后将释放资源,更改状态,直到最后一个执行的线程释放完资源,此时状态为0时,尝试去唤醒等待的线程,也就是执行countdown·.await()的线程。

  这里就分享完了AQS的共享模式已经CountDownLatch的使用及原理,限于笔者水平有限,文中错误之处希望各位能够指出,谢谢!

原文 并发编程学习笔记(9)----AQS的共享模式源码分析及CountDownLatch使用及原理

1. AQS共享模式

  前面已经说过了AQS的原理及独享模式的源码分析,今天就来学习共享模式下的AQS的几个接口的源码。

  首先还是从顶级接口acquireShared()方法入手:

public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }

  与acquire()方法一样,tryAcquireShared()为自己是实现的对资源获取的接口,AQS对返回值的语义已经定义好了,小于0表示失败,0表示成功,但是没有剩余资源,大于0表示成功,且还有剩余资源,其他线程还可以去获取,所以这里的流程就是,先调用tryAcquireShared();当不能获取资源时,调用doAcquireShared()方法让线程进入等待队列。

  doAcquireShared(int)方法,该方法用于将当前线程放入到等待队列中等待,直到其他线程唤醒并成功获取到资源才开始执行。源码如下:

 private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED); boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head) { int r = tryAcquireShared(arg); if (r >= 0) { setHeadAndPropagate(node, r); p.next = null; // help GC if (interrupted) selfInterrupt(); failed = false; return; } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); }

  将当前线程加入到等待队列队尾,并返回当前线程所在的节点,标记是否成功,判断是否被中断,获取当前节点的前驱,如果前驱不为空,如果前驱等于头节点,则表示当前线程被唤醒,因为头节点是持有资源的线程,当前节点可能会被头节点唤醒,尝试去获取资源,r>=0,表示获取成功,将当前节点设置为头节点,如果还有资源可以尝试唤醒下一个等待线程。判断是够被中断过,如果中断过,则清除中断标记,shouldParkAfterFailedAcquire()判断线程状态是否可以等待并找一个能够被唤醒的点进入等待,等着被unpark()或interrupt(),parkAndCheckInterrupt()使线程被waiting。

  跟独占模式相比,这里是将selfInterrupt()放到了doAcquireShared()中,具体为啥我也不知道,但是这里的问题是,当第一个线程执行完之后释放资源,可能释放的资源只有3个,但是当前线程需要4个,而后面一个线程只需要2个,再后一个线程只需要1个,这种情况下,当前线程也是不会去唤醒后两个线程的,它会继续等待着其他的线程释放资源,独享模式下这样没问题,但是在共享模式下,多个线程可以同时执行,这样的策略会使得后面的两个线程会因为没被唤醒而没法执行,其实也算是问题,这里是cas严格保证了入队顺序和出对顺序,降低了并发,但是却是保证了安全的。

  setHeadAndPropagate(Node, int)是将当前线程设置为头节点,当资源还有剩余的情况下去唤醒其他资源。

private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below  setHead(node); /* * Try to signal next queued node if: * Propagation was indicated by caller, * or was recorded (as h.waitStatus either before * or after setHead) by a previous operation * (note: this uses sign-check of waitStatus because * PROPAGATE status may transition to SIGNAL.) * and * The next node is waiting in shared mode, * or we don't know, because it appears null * * The conservatism in both of these checks may cause * unnecessary wake-ups, but only when there are multiple * racing acquires/releases, so most need signals now or soon * anyway. */ if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; if (s == null || s.isShared()) doReleaseShared(); } }

  setHead将当前节点设置为头节点,当资源还有剩余的情况下,唤醒当前节点的相邻节点。

  共享模式的流程就是尝试获取资源,获取资源失败,则进入等待,与独享模式相比,共享只是多了在资源剩余的情况下去唤醒其他线程的操作而已。

  releaseShared()共享模式下释放共享资源的顶级入口,释放指定量的资源,如果成功释放且允许唤醒其他线程来获取资源,则它会唤醒队列里的其他等待线程来获取资源,源码:

 public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; }

  调用tryReleaseShared()尝试释放资源,这里的tryReleaseShared()也是自己实现的,成功,则调用doReleaseShared()唤醒后继节点,

  doReleaseShared()用于唤醒后继节点

  源码:

private void doReleaseShared() {
        /*
         * Ensure that a release propagates, even if there are other
         * in-progress acquires/releases.  This proceeds in the usual
         * way of trying to unparkSuccessor of head if it needs
         * signal. But if it does not, status is set to PROPAGATE to
         * ensure that upon release, propagation continues.
         * Additionally, we must loop in case a new node is added
         * while we are doing this. Also, unlike other uses of
         * unparkSuccessor, we need to know if CAS to reset status
         * fails, if so rechecking.
         */ for (;;) { Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases  unparkSuccessor(h); } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS  } if (h == head) // loop if head changed break; } }

  自旋,通过unparkSuccessor()唤醒后继节点,这样就释放掉了资源。

  以上就是关于AQS共享模式的源码的分析。

2. CountDownLatch的使用及原理

  CountDownLatch是jdk并发包中提供的负责并发编程的类,它也是AQS中共享模式的一种运用,利用它可以实现类似于计数器的功能,比如当以个线程需要等待其他几个线程的结果,但是其他几个线程又需要并行的执行时,就可以利用该类来实现,这里我们以一个求和的程序来举例该类的用法,

  2.1 使用方式

  存在一个文件中有如下数据

12,13,20,40
50,60,80,90
50,23,40
16,13

  我们需要每一行用一个线程来进行加法计算,当所有线程执行完成后,将所有线程计算结果做一次汇总,实现如下:

  计算每一行之和的代码:

public void calc (String line, int index, CountDownLatch countDownLatch) {
        String[] nus = line.split(",");
        int total = 0; for (String n : nus) { total += Integer.parseInt(n); } nums[index] = total; System.out.println(Thread.currentThread().getName() + " 执行计划任务..." + line + " 结果为:" + total); countDownLatch.countDown(); }

  每一个线程执行计算完毕后都会调用countDownLatch.countDown();使得当前运行线程减一

计算总和的代码:

 public void sum () {
        System.out.println(Thread.currentThread().getName() + "汇总线程开始执行...");
        int total = 0; for (int i = 0; i < nums.length; i++) { total += nums[i]; } System.out.println("总结果为:" + total); }

  计算每一行之和的调用方式和计算总和的调用方式:

package com.wangx.thread.t7;

import java.io.BufferedReader;
import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; public class AddMain { public static void main(String[] args) { //读取文件 final List<String> contents = readFile(); //初始化countDownLatch 有几个线程执行构造参数就传几个 final CountDownLatch countDownLatch = new CountDownLatch(contents.size()); int lineNum = contents.size(); final AddNumber addNumber = new AddNumber(lineNum); //多个线程同时执行 for (int i = 0; i < lineNum; i++) { final int k = i; new Thread(new Runnable() { @Override public void run() { addNumber.calc(contents.get(k), k, countDownLatch); } }).start(); } //等待着直到所有线程执行完之后执行下面的代码 try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } //执行汇总  addNumber.sum(); } private static List<String> readFile() { List<String> contents = new ArrayList<>(); String line = null; try { BufferedReader bufferedReader = new BufferedReader(new FileReader("D:\\JavaDev\\spring\\thread\\src\\com\\wangx\\thread\\t7\\numers.txt")); while ((line = bufferedReader.readLine()) != null) { contents.add(line); } } catch (Exception e) { e.printStackTrace(); } return contents; } }

  先初始化CountDownLatch实例,构造参数为需要并行执行的线程个数,调用await()方法等待,知道所有并行执行的线程执行完成。

  2.2 实现原理(源码分析)

   首先看初始化的构造方法:

 public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0"); this.sync = new Sync(count); }

  它实例化了一个内部同步器Sync,我们继续看Sync的构造:

 Sync(int count) {
            setState(count);
        }

  这里调用了AQS的方法,直接设置了AQS的状态,达到了初始化多少个资源的目的,因为AQS是提供一个原子的int类型state来维护状态的,我们的示例中初始化了contents.size()个资源,接下来看countDown()方法,

 public void countDown() {
        sync.releaseShared(1);
    }

  调用releaseShared()来释放资源,这是AQS原理的运用,我们这里主要看在CountDownLatch中tryReleaseShared()方法的实现:

  

protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero for (;;) { int c = getState(); if (c == 0) return false; int nextc = c-1; if (compareAndSetState(c, nextc)) return nextc == 0; } } }

  实现也很简单,状态为0,资源已经被释放,自旋,更改状态,示例当contents个线程调用countDown完毕之后nextc == 0;才会成立,此时的releaseShared()才回去唤醒其他的等待的线程,示例中是主线程在调用求和的方法。

  await()方法:

public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

  同样是调用内部同步器的方法来执行,

  acquireSharedInterruptibly():

 public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (tryAcquireShared(arg) < 0) doAcquireSharedInterruptibly(arg); }

  该方法的功能是获取当前获取资源,如果获取资源失败,则调用doAcquireSharedInterruptibly()将线程放入到等待队列中等待,

  doAcquireSharedInterruptibly()与doAcquireShared()相似,看源码:

 private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException { final Node node = addWaiter(Node.SHARED); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head) { int r = tryAcquireShared(arg); if (r >= 0) { setHeadAndPropagate(node, r); p.next = null; // help GC failed = false; return; } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } }

  两段代码都与前面共享模式时的doAcquireShared()方法功能相同,只是它是一个可中断的实现。

  这就是CountDownLatch的实现原理,先初始化资源状态,每个线程执行完成后将释放资源,更改状态,直到最后一个执行的线程释放完资源,此时状态为0时,尝试去唤醒等待的线程,也就是执行countdown·.await()的线程。

  这里就分享完了AQS的共享模式已经CountDownLatch的使用及原理,限于笔者水平有限,文中错误之处希望各位能够指出,谢谢!

猜你喜欢

转载自www.cnblogs.com/xiaoshen666/p/10868839.html