• 并发工具类


    CountDownLatch

    CountDownLatch是一个同步工具类,它允许一个或者多个线程一直等待,知道其他线程的操作执行完毕再执行。

    CountDownLatch提供了两个方法,一个是countDown,一个是await,countDownLatch初始化的时候需要传入一个整数,在这个整数倒数到0之前,调用了await方法的程序都必须要等待,然后通过countDown来倒数。

    public static void main(String[] args) throws InterruptedException {
    
      final CountDownLatch countDownLatch = new CountDownLatch(4);
    
      new Thread(new Runnable() {
        @Override
        public void run() {
          System.out.println("" + Thread.currentThread().getName() + "-执行中");
          countDownLatch.countDown();
          System.out.println("" + Thread.currentThread().getName() + "-执行完毕");
        }
      }, "t1").start();
    
      new Thread(new Runnable() {
        @Override
        public void run() {
          System.out.println("" + Thread.currentThread().getName() + "-执行中");
          countDownLatch.countDown();
          System.out.println("" + Thread.currentThread().getName() + "-执行完毕");
        }
      }, "t2").start();
    
      new Thread(new Runnable() {
        @Override
        public void run() {
          System.out.println("" + Thread.currentThread().getName() + "-执行中");
          countDownLatch.countDown();
          System.out.println("" + Thread.currentThread().getName() + "-执行完毕");
        }
      }, "t3").start();
    
      countDownLatch.await();
      System.out.println("所有线程已经执行完毕");
    }
    

    从代码实现看,类似join的功能,但是比join更加灵活。CountDownLatch构造函数会接受一个int类型的参数作为计数器的初始值,当调用CountDownLatch的countDown方法时,这和计数器就会减一。

    模拟高并发场景

    static CountDownLatch countDownLatch = new CountDownLatch(1);
    public static void main(String[] args) {
    
      for (int i = 0; i < 1000; i++) {
        new Thread(new CountDownLatchDemo()).start();
      }
      countDownLatch.countDown();
    }
    
    @Override
    public void run() {
      try {
        countDownLatch.await();
      }catch (InterruptedException e){
        e.printStackTrace();
      }
      System.out.println("ThreadName:"+Thread.currentThread().getName());
    }
    

    总的来说,凡是涉及到需要指定某个任务再执行之前,要等到前置任务执行完毕之后才能执行的场景,都可以使用CountDownLatch。

    CountDownLatch源码解析

    对于countDownLatch,只要有await()方法和countDown()方法。

    countDown()方法每次调用都会将state减1,直到state的值为0;而await是一个阻塞方法,当state减为0的时候,await方法才会返回。await可以被多个线程调用。所有调用了await方法的线程阻塞在AQS的紫色队列来,条件满足(state==0),将线程从队列中一个个唤醒过来。

    acquireSharedInterruptibly

    countDownLatch也使用到AQS,在CountDownLatch内部写了一个Sync并且继承了AQS这个抽象类重写了AQS中的共享锁方法。如下代码,这块代码只要是判断当前线程是否获取到了共享锁;(在CountDownLatch中,使用的是共享锁机制,因为CountDownLatch并不需要实现互斥的特性)

    public final void acquireSharedInterruptibly(int arg)
      throws InterruptedException {
      if (Thread.interrupted())
        throw new InterruptedException();
      //state如果不等于0,说明当前线程需要加入带共享锁队列
      if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
    }
    
    protected int tryAcquireShared(int acquires) {
      return (getState() == 0) ? 1 : -1;
    }
    
    

    doAcquireSharedInterruptibly

    1.addWaiter设置为shared模式

    2.tryAcquire和tryAcquireShared的返回值不同,因此会多出一个判断过程

    3.在判断前驱结点是头结点后,调用了setHeadAndPropagate方法,而不是简单地更新了一下头结点

    private void doAcquireSharedInterruptibly(int arg)
      throws InterruptedE
      //创建一个共享模式的节点添加到队列中
      final Node node = addWaiter(Node.SHARED);
      boolean failed = true;
      try {
        for (;;) {
          final Node p = node.predecessor();
          if (p == head) {
            //判断尝试获得锁
            int r = tryAcquireShared(arg);
            //r>=0表示获取到了执行权限,这个时候state!=0,所以不会执行这段代码
            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);
      }
    }
    
    

    图解分析

    加入这个时候有3个线程调用了await方法,由于这个时候state的值还不为0,所以这三个线程都会加入到AQS队列中。并且三个线程都处于阻塞状态。

    CountDownLatch.countDown

    由于线程被await方法阻塞了,所以只有等到countDown方法使得state=0的时候才会被唤醒。

    1.只有当state减为0的时候,tryReleaseShared才会返回true,否则只是简单的state=state-1

    2.如果state=0,则调用doReleaseShared唤醒处于await状态下的线程

    public final boolean releaseShared(int arg) {
      if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
      }
      return false;
    }
    
    //用自旋的方式实现state减1
    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;
      }
    }
    

    AQS.doReleaseShared

    共享锁的释放和独占锁的释放有一定的差别,前面唤醒锁的逻辑和独占锁是一样的,先判断头结点是不是SIGNAL状态,如果是,则修改为0,并且唤醒头结点的而下一个节点

    private void doReleaseShared() {
      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);
          }
          //这个CAS失败的场景是:执行到这里的时候,刚好有一个节点入队,入队会将这个ws设置为-1
          else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
            continue;                // loop on failed CAS
        }
        //如果当到这里的时候,前面唤醒的线程已经占了了head,那么再循环
        //通过检查头节点是否改变了,如果改变了就继续循环
        if (h == head)                   // loop if head changed
          break;
      }
    }
    PROPAGATE:标识为PROPAGATE状态的节点,是共享模式下的节点状态,处于这个状态下的节点,会对县城内的唤醒进行传播
    

    h==head:说明头节点还没有被刚刚用unparkSuccessor唤醒的线程(这里可以理解为ThreadB)占有,是break退出循环。

    h!=head:头节点被刚刚唤醒的线程(这里可以理解为ThreadB)占有,那么这里重新进入下一轮玄幻,唤醒下一个节点(这里是ThreadB)。然后后面唤醒传递。。

    一旦ThreadA被唤醒,代码又回到了doAcquireSharedInterruptibly中来执行。如果当前state满足等于0的条件,则会执行setHeadAndPropagate方法

    if (p == head) {
      //判断尝试获得锁
      int r = tryAcquireShared(arg);
      if (r >= 0) {
        setHeadAndPropagate(node, r);
        p.next = null; // help GC
        failed = false;
        return;
      }
    }
    

    setHeadAndPropagate

    这个方法主要作用是把被唤醒的节点,设置成head节点。然后继续唤醒队列中的其他线程。

    由于现在队列有3个线程处于阻塞状态,一旦ThreadA被唤醒,并且设置为head之后,会继续唤醒后续的ThreadB。

    private void setHeadAndPropagate(Node node, int propagate) {
      Node h = head; // Record old head for check below
      setHead(node);
      if (propagate > 0 || h == null || h.waitStatus < 0 ||
          (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
          doReleaseShared();
      }
    }
    

    Semaphore

    ​ semaphore 也就是我们常说的信号灯,semaphore 可以控 制同时访问的线程个数,通过 acquire 获取一个许可,如 果没有就等待,通过 release 释放一个许可。有点类似限流 的作用。叫信号灯的原因也和他的用处有关,比如某商场 就 5 个停车位,每个停车位只能停一辆车,如果这个时候 来了 10 辆车,必须要等前面有空的车位才能进入。

    public class SemaphoreDemo {
    
        public static void main(String[] args) {
            Semaphore semaphore = new Semaphore(5);
            for (int i = 0; i < 10; i++) {
                new Car(i, semaphore).start();
            }
        }
    
        static class Car extends Thread {
            private int num;
            private Semaphore semaphore;
    
            public Car(int num, Semaphore semaphore) {
                this.num = num;
                this.semaphore = semaphore;
            }
    
            public void run() {
                try {
                    semaphore.acquire();
                    System.out.println("第" + num + "占用一个停车位");
                    TimeUnit.SECONDS.sleep(2);
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

    使用场景

    Semaphore比较常见的就是用来作限流操作。

    Semaphore源码分析

    从Semaphore的功能来看,我们可以猜测它的底层原理一定是基于AQS的共享锁。

    创建Semaphore实例的时候,需要一个参数permits,这个基本上可以确定是设置给AQS的state的,然后每个线程调用acquire的时候,执行state=state-1,release的时候执行state=state+1,当然,acquire的时候,如果state=0,说明没有资源了,需要等待其他的线程release。

    Semaphore分公平策略和非公平策略

    FairSync

    static final class FairSync extends Sync {
      private static final long serialVersionUID = 2014338818796000944L;
    
      FairSync(int permits) {
        super(permits);
      }
    
      protected int tryAcquireShared(int acquires) {
        for (;;) {
          //区别就在于是不是会先判断是否有线程在排队,然后才进行CAS键操作
          if (hasQueuedPredecessors())
            return -1;
          int available = getState();
          int remaining = available - acquires;
          if (remaining < 0 ||
              compareAndSetState(available, remaining))
            return remaining;
        }
      }
    }
    

    NoFairSync

    通过对别发现公平锁和非公平锁的区别就是在于是否多了一个hasQueuedPredecessors的判断

    static final class NonfairSync extends Sync {
      private static final long serialVersionUID = -2694183684443567898L;
    
      NonfairSync(int permits) {
        super(permits);
      }
    
      protected int tryAcquireShared(int acquires) {
        return nonfairTryAcquireShared(acquires);
      }
    }
    
    final int nonfairTryAcquireShared(int acquires) {
      for (;;) {
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
          return remaining;
      }
    }
    //都是基于共享锁来实现的
    

    CyclicBarrier

    CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。他要做的事情是,让一组线程到达一个屏障(也可以佳作同步点)时被阻塞,知道最后一个线程到达平衡住那个是,屏障才会开门,所有被屏障拦截的线程才会继续工作。CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier当前线程已经到达了屏障,然后当前线程被阻塞。

    使用场景

    当存在需要所有的子任务都完成时,才会执行主任务,这个时候就可以选择使用CyclicBarrier。

    案例

    DataImportThread

    public class DataImportThread extends Thread{
    
    
        private CyclicBarrier cyclicBarrier;
    
        private String path;
    
        public DataImportThread(CyclicBarrier cyclicBarrier,String path){
            this.cyclicBarrier = cyclicBarrier;
            this.path = path;
        }
    
        @Override
        public void run() {
            System.out.println("开始导入:"+path+"位置的数据");
            try {
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }
    

    CyclicBarrierDemo

    public class CyclicBarrierDemo extends Thread{
    
        @Override
        public void run() {
    
            System.out.println("开始进行数据分析");
        }
    
        public static void main(String[] args) {
            CyclicBarrier cyclicBarrier = new CyclicBarrier(3,new CyclicBarrierDemo());
            new Thread(new DataImportThread(cyclicBarrier,"file1")).start();
            new Thread(new DataImportThread(cyclicBarrier,"file2")).start();
            new Thread(new DataImportThread(cyclicBarrier,"file3")).start();
        }
    }
    

    注意点:

    1)对于制定计数值parties。若由于某种原因,没有足够的线程调用CyclicBarrier的await,则所有调用await的线程都会被阻塞。

    2)同样的CyclicBarrier也可以调用await(timeout, unit),设置超时时间,在设定时间内,如果没有足够线程到达,则解除阻塞状态,继续工作。

    3)通过reset重置计数,会使得进入await的线程出现BrokenBarrierExecption;

    4)如果采用是CyclicBarrier(int parteis, Runnable barrierAction)构造方法,执行barrierAction操作的是最后一个到达的线程。

    实现原理

    CyclicBarrier相比CountDownLatch来说,要简单很多,源码实现是基于ReentrantLock和Condition的组合使用。如下图,CyclicBarrier和CountDownLatch是不是很像,只是CyclicBarrier可以不止一个栅栏,因为他的栅栏(Barrier)可以重复使用。

    技术分享图片

  • 相关阅读:
    python入门19 异常及异常处理 异常捕获
    python入门18 继承和多态
    python入门17 类和对象
    python入门16 递归函数 高阶函数
    接口测试get请求url拼接函数(python)
    python入门15 函数
    ssh tunnel 三种模式
    k8s 容器的生命周期钩子
    对k8s service的一些理解
    windows下使用pyinstaller把python文件打包成exe可执行文件
  • 原文地址:https://www.cnblogs.com/snail-gao/p/13574495.html
Copyright © 2020-2023  润新知