java并发包追问集

324 阅读7分钟

java并发包追问集

package java.util.concurrent

1. lock包

  • ReenTrantLock
  • ReenTrantWriteReadLock

2. atomic包

  • AtomicInteger
  • LongAdder

3. 并发辅助类

  • CountDownLatch (AKA:发令枪\倒数锁)
    • 概览

      • 一起看看源码的注释
        /**
        * A synchronization aid that allows one or more threads to wait until
        * a set of operations being performed in other threads completes.
        *
        * @since 1.5
        * @author Doug Lea
         */
        public class CountDownLatch {
        }
        

      翻译如下:它是一个同步工具类,允许一个或多个线程一直等待,直到其他线程运行完成后再执行。
      通过描述,可以清晰的看出,CountDownLatch的两种使用场景:

      • 场景1:让多个线程等待
      • 场景2:和让单个线程等待。
    • 用法

      • 场景1:让多个线程等待:模拟并发,让并发线程一起执行

        package com.zly.concurrent;
        
        import java.util.concurrent.CountDownLatch;
        
        public class useCountDownLatch {
            public static void main(String[] args) throws Exception{
                useCountDownLatch useCountDownLatch = new useCountDownLatch();
                useCountDownLatch.multiThreadWait();
            }
        
            public void multiThreadWait() throws Exception{
                CountDownLatch countDownLatch = new CountDownLatch(1);
        
                int i = 0;
                do{
                    new Thread(() -> {
                        try {
                        	//运动员都阻塞在这,等待发令
                            countDownLatch.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("currentThread start run");
                    }).start();
                    i ++;
                }while (i < 5);
        
                System.out.println("before run");
                Thread.sleep(2000);// 裁判准备发令
                countDownLatch.countDown();// 发令枪:执行发令
            }
        }
        

        运行结果:

          before run
          currentThread start run
          currentThread start run
          currentThread start run
          currentThread start run
          currentThread start run
        
      • 场景2:让单个线程(比如主线程)等待:多个线程(任务)完成后,进行汇总合并

        public void singleThreadWait() throws Exception{
        CountDownLatch countDownLatch = new CountDownLatch(5);
        for (int i = 1; i <= 5; i++) {
        	final int index = i;
        	new Thread(() -> {
            	try {
                	Thread.sleep(1000 + ThreadLocalRandom.current().nextInt(1000));
                	System.out.println("finish:" + index + Thread.currentThread().getName());
                  	countDownLatch.countDown();
              	} catch (InterruptedException e) {
                  	e.printStackTrace();
              	}
          	}).start();
        	}
        
            countDownLatch.await();// 主线程在阻塞,当计数器==0,就唤醒主线程往下执行。
            System.out.println("主线程:在所有任务运行完成后,进行结果汇总");
        }
        

        运行结果:

          finish:1Thread-5
          finish:5Thread-9
          finish:4Thread-8
          finish:3Thread-7
          finish:2Thread-6
          主线程:在所有任务运行完成后,进行结果汇总
        
        • 比如springBoot项目启动时,在spring线程处理完前,不希望主线程停止
        @SpringBootApplication
        public class Main {
        
            private static final Logger logger = LoggerFactory.getLogger(Main.class);
        
            private static CountDownLatch closeLatch = new CountDownLatch(1);
        
            public static void main(String[] args) throws InterruptedException {
                new SpringApplicationBuilder()
                    .sources(Main.class)
                    .web(false)
                    .run(args);
                logger.info("admin service start ok.");
                closeLatch.await();
            }
        
        
    • 思路

      • CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量;
        调用await()方法的线程会被阻塞,直到计数器 减到 0 的时候,才能继续往下执行;

      • 调用了await()进行阻塞等待的线程,它们阻塞在Latch门闩/栅栏上;只有当条件满足的时候(countDown() N次,将计数减为0),它们才能同时通过这个栅栏;以此能够实现,让所有的线程站在一个起跑线上。

    • 原理

      • 底层基于 AbstractQueuedSynchronizer 实现,CountDownLatch 构造函数中指定的count直接赋给AQS的state;
      • 每次countDown()则都是release(1)减1,最后减到0时unpark阻塞线程;这一步是由最后一个执行countdown方法的线程执行的。
      • 而调用await()方法时,当前线程就会判断state属性是否为0,如果为0,则继续往下执行
      • 如果不为0,则使当前线程进入等待状态,直到某个线程将state属性置为0,其就会唤醒在await()方法中等待的线程。
    • 对比

      • vs CyclicBarrier
        CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:
        CountDownLatch一般用于一个或多个线程,等待其他线程执行完任务后,再才执行
        CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行
        另外,CountDownLatch是减计数,计数减为0后不能重用;而CyclicBarrier是加计数,可置0后复用。
      • vs Thread.join
        CountDownLatch的作用就是允许一个或多个线程等待其他线程完成操作,看起来有点类似join() 方法,但其提供了比 join() 更加灵活的API。
        CountDownLatch可以手动控制在n个线程里调用n次countDown()方法使计数器进行减一操作,也可以在一个线程里调用n次执行减一操作。
        而 join() 的实现原理是不停检查join线程是否存活,如果 join 线程存活则让当前线程永远等待。所以两者之间相对来说还是CountDownLatch使用起来较为灵活。
  • Semphare(AKA:信号量)
  • CyclicBarrier(AKA:环形栅栏)
    • 环形指的是,当所有等待线程都被释放以后,CyclicBarrier可以被重用

      • CyclicBarrier 的计数器有自动重置的功能,当减到 0 的时候,会自动重置你设置的初始值,自动复原。这个功能用起来实在是太方便了。
    • 栅栏指的是,锁住多个线程,让大家一起等待

    • 概览
      源码注释如下:

      
      /* A synchronization aid that allows a set of threads to all wait foreach other to reach 
      a common barrier point.  CyclicBarriers areuseful in programs involving a fixed sized 
      party of threads thatmust occasionally wait for each other. The barrier is called
      <em>cyclic</em> because it can be re-used after the waiting threads are released.
      */
      
    • 用法

      • 回调
        CyclicBarrier 的第二个构造方法:
        CyclicBarrier(int parties, Runnable barrierAction);
        
        barrierAction 可以指定一个善后处理的task,在所有人都到达屏障点时,来执行;
        • CyclicBarrier 的回调函数,可以指定一个线程池来运行,相当于异步完成;
        • 如果不指定线程池,默认在最后一个执行await()的线程执行,相当于同步完成。
      
         package com.zly.concurrent;
      
         import java.util.concurrent.CyclicBarrier;
      
         public class useCyclicBarrier {
             static int state = 1;
             static int total = 5;
             public static void main(String[] args) {
      
      
               CyclicBarrier cyclicBarrier = new CyclicBarrier(total, new Runnable() {
                   @Override
                   public void run() {
                       System.out.println("完成第" + state + "阶段!!------");
                       state ++;
                   }
               });
               for(int i = 0; i< total; i++){
                   new Thread( () -> {
                       String name = Thread.currentThread().getName();
                       try {
                           //1. 上班开早会
                           System.out.println("上班开早会~" + name);
      
                           cyclicBarrier.await();
      
                           //集合准备工作
      
                           //2.开始工作
                           System.out.println("工作ing~" + name);
                           //集合准备吃饭
                           cyclicBarrier.await();
                           //3.吃饭
                           System.out.println("吃饭啦!!" + name);
                       }catch (Exception e){
                           e.printStackTrace();
                       }
                   }).start();
               }
           }
       }
      

      运行结果:

         上班开早会~Thread-0
         上班开早会~Thread-3
         上班开早会~Thread-2
         上班开早会~Thread-1
         上班开早会~Thread-4
         完成第1阶段!!------
         工作ing~Thread-4
         工作ing~Thread-2
         工作ing~Thread-1
         工作ing~Thread-0
         工作ing~Thread-3
         完成第2阶段!!------
         吃饭啦!!Thread-3
         吃饭啦!!Thread-4
         吃饭啦!!Thread-2
         吃饭啦!!Thread-0
         吃饭啦!!Thread-1
      
    • 原理
      基于ReentrantLock和Condition

      /** The lock for guarding barrier entry */
      private final ReentrantLock lock = new ReentrantLock();
      /** Condition to wait on until tripped */
      private final Condition trip = lock.newCondition();
      /** The number of parties */
      private final int parties;
      /* The command to run when tripped */
      private final Runnable barrierCommand;
      /** The current generation */
      private Generation generation = new Generation();
      

      核心逻辑是在dowait()里,使用ReentrantLock上锁,每await()一次, --count; 直到count==0, 触发更新换代逻辑

      private int dowait(boolean timed, long nanos){
      
          //省略
          int index = --count;
          if (index == 0) {  // tripped
              boolean ranAction = false;
              try {
                  final Runnable command = barrierCommand;
                  if (command != null)
                      command.run();
                  ranAction = true;
                  nextGeneration();
                  return 0;
              } finally {
                  if (!ranAction)
                      breakBarrier();
              }
          }
          //省略
      }
      

      更新换代逻辑

      private void nextGeneration() {
          // signal completion of last generation
          trip.signalAll();
          // set up next generation
          count = parties;
          generation = new Generation();
      }
      
    • 屏障破坏 BrokenBarrierException
      某些情况下,屏障会被破坏,等在屏障的线程都会被释放,详见CyclicBarrier的克星—BrokenBarrierException

4.Task任务

  • Callable接口
    • 为了解决Thread 和 Runnable没有结果返回的问题
    • 通过共享变量或者线程通信的方式倒是可以间接获取执行结果,但是我的水平,怕是要996解bug。还好已经被Doug Lea教授封装了
    • 可以将Runnble用RunnableAdapter适配器适配成Callable对象
  • Future接口
    • 概述
      Future是是调用方与异步执行方之间沟通的桥梁。
      是为了配合Callable/Runnable而产生的,Callable既然有返回值,那么返回什么?什么时候返回?这些都由Future作为句柄,放在上层线程的栈中,供其自己决定。
    • 和Runnable, Callable对比
      • Runnable接口是jdk1.0自带的,java.lang包下的
      • Callable和Future接口都是java.util.concurrent包下的,是Doug Lea教授在jdk1.5对Runnable功能的增强
  • RunnableFuture
  • FutureTask
    也是Doug Lea教授在jdk1.5编写的, 可用来包装Runnable和Future image.png
    FutureTask实现了RunnableFuture接口,同时具有Runnable、Future的能力,即既可以作为Future得到Callable的返回值,又可以作为一个Runnable。

5.Executor(执行器)

  • Executor接口 Executor 接口是线程池中最高级的接口, 该接口中只有一个方法,抽象核心动作,即【执行任务】

    void execute(Runnable command);
    

    注意,只支持最基础的Runnable

  • ExecutorService接口 (extend Executor)
    因为Executor的 execute()方法,虽然是极简的设计,但是也往往不能满足我们的需求,比如:

    • 需要任务的返回结果
    • 批量提交任务 方法如下:
      image.png
    • 3个submit()方法,分别处理Callable, Runnable, 以及任务是Runnable也想获得返回值的情况
    • 两个invokeAll()方法,都接受集合形式的Callable, 区别是1个方法可以传超时时间
      • 返回集合内的所有任务的Future
    • 两个invokeAny()方法,也是有1个可以传超时时间
      • 只返回一个任务的结果(不是Future),这个结果随机,可能是第一个任务结果,可能是中间的一个任务结果,也可能是最后一个任务的结果
  • AbstractExecutorService 实现了ExecutorService接口

    • 原理 todo
  • Executors

  • CompletionService

  • Fork-join

  • Phaser

  • Exchanger

  • ThreadFactory

参考资料

1.CountDownLatch的两种常用场景

2.CyclicBarrier多任务协同的利器