并发 2

线程池

线程池,是一种线程的使用模式,它为了降低线程使用中频繁的创建和销毁所带来的资源消耗与代价。 通过创建一定数量的线程,让他们时刻准备就绪等待新任务的到达,而任务执行结束之后再重新回来继续待命。

这就是线程池最核心的设计思路,「复用线程,平摊线程的创建与销毁的开销代价」。

相比于来一个任务创建一个线程的方式,使用线程池的优势体现在如下几点:

  • 避免了线程的重复创建与开销带来的资源消耗代价
  • 提升了任务响应速度,任务来了直接选一个线程执行而无需等待线程的创建
  • 线程的统一分配和管理,也方便统一的监控和调优

线程池的实现天生就实现了异步任务接口,允许你提交多个任务到线程池,线程池负责选用线程执行任务调度。

基本介绍

线程池相关接口的类图结构。

Executor、ExecutorService 以及 AbstractExecutorService 都是我们熟悉的,它们抽象了任务执行者的基本模型。

ThreadPoolExecutor 是对线程池概念的抽象,它天生实现了任务执行的相关接口,也就是说,线程池也是一个任务的执行者,允许你向其中提交多个任务,线程池将负责分配线程与调度任务。 至于 Schedule 线程池,它是扩展了基础的线程池实现,提供「计划调度」能力,定时调度任务,延时执行等。

线程池基本原理

ThreadPoolExecutor 的创建并不复杂,直接 new 就好,只不过构造函数有好久个重载,我们直接看最底层的那个,也就是参数最多的那个。

public ThreadPoolExecutor
(   int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler
)
复制代码
线程池容量问题

构造函数中有这么几个参数是用于配置线程池中线程容量与生命周期的:

  • corePoolSize
  • maximumPoolSize
  • keepAliveTime

corePoolSize 指定了线程池中的核心线程的个数,线程池新建线程的时候,如果当前线程总数小于corePoolSize,则新建的是核心线程,如果超过corePoolSize,则新建的是非核心线程;核心线程默认情况下会一直存活在线程池中,即使这个核心线程啥也不干(闲置状态);如果设置了 allowCoreThreadTimeOut 为 true,那么核心线程如果不干活(闲置状态)的话,超过一定时间(时长下面参数决定),就会被销毁掉。

maximumPoolSize 指定了线程池能够创建的最大线程数量。线程总数 = 核心线程数 + 非核心线程数。

keepAliveTime 是用于控制非核心线程最长空闲等待时间,如果一个非核心线程处理完任务后回到线程池待命,超过这个指定时长依然没有新任务的分配将导致线程被销毁。当设置了 allowCoreThreadTimeOut 为 true 时,对核心线程同样起作用。

任务阻塞问题

ThreadPoolExecutor 中有这么一个字段:private final BlockingQueue workQueue; 这个队列的作用很明显,当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务,常用的 workQueue 类型:

SynchronousQueue:这个队列接收到任务的时候,会直接提交给线程处理,而不保留它,如果所有线程都在工作怎么办?那就新建一个线程来处理这个任务!所以为了保证不出现 线程数达到了 maximumPoolSize 而不能新建线程 的错误,使用这个类型队列的时候,maximumPoolSize 一般指定成 Integer.MAX_VALUE,即无限大。

LinkedBlockingQueue:这个队列接收到任务的时候,如果当前线程数小于核心线程数,则新建线程(核心线程)处理任务;如果当前线程数等于核心线程数,则进入队列等待。由于这个队列没有最大值限制,即所有超过核心线程数的任务都将被添加到队列中,这也就导致了 maximumPoolSize 的设定失效,因为总线程数永远不会超过 corePoolSize。

ArrayBlockingQueue:可以限定队列的长度,接收到任务的时候,如果没有达到 corePoolSize 的值,则新建线程(核心线程)执行任务,如果达到了,则入队等候,如果队列已满,则新建线程(非核心线程)执行任务,又如果总线程数到了 maximumPoolSize,并且队列也满了,则发生错误。

DelayQueue:队列内元素必须实现 Delayed 接口,这就意味着你传进去的任务必须先实现 Delayed 接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务。

线程工厂

线程工厂 ThreadFactory 中只定义了一个方法 newThread,子类实现它并按照自己的需求创建一个线程返回。 例如 DefaultThreadFactory 实现的该方法将创建一个线程,名称格式: pool-<线程池编号>-thread-<线程编号>,设置线程的优先级为标准优先级,非守护线程等。

任务拒绝策略

构造函数中还有一个参数 handle 是必须传的,它将为 ThreadPoolExecutor 中的同名字段赋值。private volatile RejectedExecutionHandler handler; RejectedExecutionHandler 中定义了一个 rejectedExecution 用于描述一种任务拒绝策略。那么哪种情况下才会触发该方法的调用呢?

当线程池中的所有线程全部分配出去工作了,并且任务阻塞队列也阻塞满了,那么此时新提交的任务将触发任务拒绝策略
复制代码

而拒绝策略主要有以下四个子类实现,而它们都是定义在 ThreadPoolExecutor 的内部类,我们看一看都是哪四种策略:

  • AbortPolicy
  • CallerRunsPolicy
  • DiscardOldestPolicy
  • DiscardPolicy

AbortPolicy 是默认的拒绝策略,他的实现就是直接抛出 RejectedExecutionException 异常。

CallerRunsPolicy 暂停当前提交任务的线程返回,自己去执行自己提交过来的任务。

DiscardOldestPolicy 策略将从阻塞任务队列对头移除一个任务并将自己排到队列尾部等待调度执行。

DiscardPolicy 是一种佛系策略,方法体的实现为空,什么也不做,也即忽略当前任务的提交。

线程池执行策略

当一个任务要被添加进线程池时,有以下四种执行策略:

  • 线程数量未达到 corePoolSize,则新建一个线程(核心线程)执行任务。
  • 线程数量达到了 corePoolsSize,则将任务移入队列等待。
  • 队列已满,新建非核心线程执行任务。
  • 队列已满,总线程数又达到了 maximumPoolSize,就会由 RejectedExecutionHandler 抛出异常。

其流程图如下所示:

合理地配置线程池

需要针对具体情况而具体处理,不同的任务类别应采用不同规模的线程池,任务类别可划分为 CPU 密集型任务、IO 密集型任务和混合型任务。

CPU 密集型任务:线程池中线程个数应尽量少,推荐配置为 (CPU 核心数 + 1);

IO 密集型任务:由于 IO 操作速度远低于 CPU 速度,那么在运行这类任务时,CPU 绝大多数时间处于空闲状态,那么线程池可以配置尽量多些的线程,以提高 CPU 利用率,推荐配置为 (2 * CPU 核心数 + 1);

混合型任务:可以拆分为 CPU 密集型任务和 IO 密集型任务,当这两类任务执行时间相差无几时,通过拆分再执行的吞吐率高于串行执行的吞吐率,但若这两类任务执行时间有数据级的差距,那么没有拆分的意义。

常见的四类线程池

常见的四类线程池分别有 FixedThreadPool、SingleThreadExecutor、ScheduledThreadPool 和 CachedThreadPool,它们其实都是通过 ThreadPoolExecutor 创建的,其参数如下表所示:

源码

submit 方法有四个重载,分别允许你传入不同类型的任务,Runnable 或是 Callable。这个 RunnableFuture 类型我们之前说过,他只不过是同时继承了 Runnable 和 Future 接口,象征性的描述了「这是一个可监控的任务」。整个 submit 的核心逻辑在 execute 方法里面,也就是说 execute 方法才是真正向线程池提交任务的方法。

先看看 ThreadPoolExecutor 中定义几个重要的字段:

ctl 是一个原子变量,它用了一个 32 位的整型描述了两个重要信息。当前线程池运行状态(runState)和当前线程池中有效的线程个数(workCount)。

runState 占用高 3 比特位,workCount 占用低 29 比特位。

接着我们来看 execute 方法的实现:

红框部分:

如果当前线程池中的实际工作线程数还未达到配置的核心线程数量,那么将调用 addWorker 为当前任务创建一个新线程并启动执行。

addWorker 方法:

  1. 死循环中判断线程池状态是否正常,如果不正常被关闭了等,将直接返回 false
  2. 如果正常则 CAS 尝试为 workerCount 增加一,并创建一个新的线程调用 start 方法执行任务。

addWorker 方法的第二个参数用于指定线程池的上界。如果传的是 true,则说明使用 corePoolSize 作为上界,也就是此次为任务分配线程如果线程池中所有的工作线程数达到这个 corePoolSize 则将拒绝分配并返回添加失败。 如果传的是 false,则使用 maximumPoolSize 作为上界,道理是一样的。

蓝框部分:

从红框出来,你可以认为任务分配线程失败了,大概率是所有正常工作的线程数达到核心线程数量了。这部分做的事情就是:

  1. 如果线程池状态正常,就尝试将当前任务添加到任务阻塞队列上。
  2. 再一次检查线程池状态,如果异常了,将撤回刚才添加的任务并根据我们设定的拒绝策略予以拒绝。
  3. 如果发现线程池自上次检查后,所哟线程全部死亡,那么将创建一个空闲线程,适当的时候他会去从任务队列取我们刚刚添加的任务的。

黄框部分:

到达黄色部分必然说明线程池状态异常或是队列添加失败,大概率是因为队列满了无法再添加了。 此时再次调用 addWorker 方法,不过这次传入 false,意思是,我知道所有的核心线程都在忙并且任务队列也排满了,那么你就额外创建一个非核心线程来执行我的任务吧。 如果失败了,执行拒绝策略。

任务的提交到分配线程,甚至阻塞到任务队列这一系列过程:

一个任务过来,如果线程池中的线程数不足我们配置的核心线程数,那么会尝试创建新线程来执行任务,否则会优先把任务往阻塞队列上添加 如果阻塞队列上满员了,那么说明当前线程池中核心线程工作量有点大,将开始创建非核心线程共同执行任务,直到达到上限或是阻塞队列不再满员。

当一个线程的任务执行结束之后,他是如何去取下一个任务的?

线程池的内部定义了一个 Worker 内部类,这个类有两个字段,一个用于保存当前的任务,一个用于保存用于执行该任务的线程。

addWorker 中会调用线程的 start 方法,进而会执行 Worker 实例的 run 方法,这个 run 方法是这样的:

public void run() {
    runWorker(this);
}
复制代码
  1. 如果自己内部的任务是空,则尝试从阻塞队列上获取一个任务
  2. 执行任务
  3. 循环的执行 1和2 两个步骤,直到阻塞队列中没有任务可获取
  4. 调用 processWorkerExit 方法移除当前线程在线程池中的引用,也就相当于销毁了一个线程,因为不久后会被 GC 回收

第一个步骤从任务队列中取一个任务调用的是 getTask 方法。 这个方法设定了一个逻辑,如果线程池中正在工作的线程数大于设定的核心线程数,也就是说线程池中存在非核心线程,那么当前线程获取任务时,如果超过指定时长依然没有获取,就将返回跳过循环执行我们 runWorker 的第四个步骤,移除对该线程的引用。 反之,如果此时有效工作线程数少于规定的核心线程数,则认定当前线程是一个核心线程,于是对于获取任务失败的处理是「阻塞到条件队列上,等待其他线程唤醒」。 什么时候唤醒也很容易想到了,就是当任务队列有新任务添加时,会唤醒所有的核心线程,他们会去队列上取任务,没抢到的依然回去阻塞。

合理地配置线程池

需要针对具体情况而具体处理,不同的任务类别应采用不同规模的线程池,任务类别可划分为 CPU 密集型任务、IO 密集型任务和混合型任务。

CPU 密集型任务:线程池中线程个数应尽量少,推荐配置为 (CPU 核心数 + 1);

IO 密集型任务:由于 IO 操作速度远低于 CPU 速度,那么在运行这类任务时,CPU 绝大多数时间处于空闲状态,那么线程池可以配置尽量多些的线程,以提高 CPU 利用率,推荐配置为 (2 * CPU 核心数 + 1);

混合型任务:可以拆分为 CPU 密集型任务和 IO 密集型任务,当这两类任务执行时间相差无几时,通过拆分再执行的吞吐率高于串行执行的吞吐率,但若这两类任务执行时间有数据级的差距,那么没有拆分的意义。

线程池实例

单线程遍历文件目录

循环遍历

static ArrayList<File> list = new ArrayList<>();

public static void scanDir(String dirString) throws IOException {
    long start = System.currentTimeMillis();
    File dirFile = new File(dirString);
    list.add(dirFile);
    while (list.size() > 0) {
        File files[] = list.remove(0).listFiles();
        for (File file : files) {
            if (file.isDirectory()) {
                list.add(file);
            }
            else {
                if (isSymlink(file)) continue;
                System.out.print(file.getName());
                if (file.getAbsolutePath().endsWith("cpp")) {
                    System.out.print("这是C++文件");
                }
                System.out.println();
            }
        }
    }
    long end = System.currentTimeMillis();
    System.out.println("Time:" + (end - start));
}

public static boolean isSymlink(File file) throws IOException {
    File canon;
    if (file.getParent() == null) {
        canon = file;
    } else {
        File canonDir = file.getParentFile().getCanonicalFile();
        canon = new File(canonDir, file.getName());
    }
    return !canon.getCanonicalFile().equals(canon.getAbsoluteFile());
}
复制代码

递归遍历

static class FileSearcher1 implements Runnable {
    final List<File> result=new ArrayList<>();
    final File folder;
    //统计文件总数
    int fileCount;
    //任务启动时间
    final long startTime;
    long currentTime;
    long elapsedTime;

    public FileSearcher1(File folder) {
        this.folder = folder;
        this.startTime =System.currentTimeMillis();
        this.currentTime=System.currentTimeMillis();
    }

    @Override
    public void run() {
        System.out.println("开始扫描文件1-正常线程检索");
        startSearch(folder);
        elapsedTime=currentTime-startTime;
        System.out.println("最终结果-耗时:"+elapsedTime+" 当前共扫描文件:"+fileCount+"个"+",搜索结果:"+result.size()+"个");
    }

    /**
     * 正常线程检索
     * @param file
     */
    void startSearch(File file){
        if(file.isDirectory()){
            File[] files = file.listFiles();
            for(int i=0;i<files.length;i++){
                startSearch(files[i]);
            }
        } else if(file.getName().endsWith("gif")){
            //检索文件
            result.add(file);
        }
        fileCount++;
        if(System.currentTimeMillis()- currentTime >1000){
            currentTime =System.currentTimeMillis();
            elapsedTime=currentTime-startTime;
            System.out.println("耗时:"+(int)(elapsedTime/1000)+"秒"+" 当前共扫描文件:"+fileCount+"个");
        }
    }
}
复制代码

线程池实现

/**
 * 文件搜索器,第二种实现
 * 让一个线程,检索一个文件.当某个线程检索完文件时,再协助其他线程检索
 */
static class FileSearcher2 implements Runnable{
    final ThreadPoolExecutor executorService;
    final ExecutorCompletionService completionService;
    final File folder;
    //任务启动时间
    final long startTime;
    long currentTime;
    long elapsedTime;

    public FileSearcher2(File folder) {
        this.folder = folder;
        this.startTime =System.currentTimeMillis();
        this.currentTime=System.currentTimeMillis();
        //遍历线程池对象
        executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()+1);
        completionService =new ExecutorCompletionService(executorService);
    }

    @Override
    public void run() {
        //遍历用户目录下所有文件,但查找到指定文件
        final AtomicInteger searchCount = new AtomicInteger();
        //当前线程负责纷发需要检索目录
        completionService.submit(new FileSearchWorker(completionService,executorService,folder,searchCount));
        try {
            Future<SearchResult> result;
            int resultCount=0;
            int fileCount=0;
            final long l=System.currentTimeMillis();
            while(null!=(result = completionService.take())){
                SearchResult searchResult = result.get();
                fileCount+=searchResult.totalCount;
                resultCount+=searchResult.result.size();
                //当原子计数器为0时,代表当前检索任务完成,因为CompletionService并不能主动停止服务
                if(0==searchCount.get()){
                    executorService.shutdownNow();
                    break;
                }
            }
            System.out.println("最终结果-耗时:"+(System.currentTimeMillis()-l)+" 当前共扫描文件:"+fileCount+"个"+",搜索结果:"+resultCount+"个");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("扫描任务完成!");
    }

}

static class FileSearchWorker implements Callable<SearchResult> {
    final ExecutorCompletionService completionService;
    final ThreadPoolExecutor executorService;
    final AtomicInteger searchCount;
    final File folder;
    int fileCount;

    public FileSearchWorker(ExecutorCompletionService completionService,ThreadPoolExecutor executorService, File folder,AtomicInteger searchCount) {
        this.completionService=completionService;
        this.executorService=executorService;
        this.searchCount =searchCount;
        this.folder = folder;
    }

    @Override
    public SearchResult call() {
        //当前扫描计数
        this.searchCount.incrementAndGet();
        //开始扫描
        final List<File> result=new ArrayList<>();
        startSearch(folder,result);
        //扫描完成后,计数器减一,当减到0时,代表所有搜索任务都完成,这个设计是因为没找到CompletionService的结束机制
        this.searchCount.decrementAndGet();
        return new SearchResult(result,fileCount);
    }

    /**
     * 正常线程检索
     * @param file
     */
    void startSearch(File file,List<File> result){
        if(file.isDirectory()){
            File[] files = file.listFiles();
            for(int i=0;i<files.length;i++){
                if(executorService.getActiveCount()<executorService.getCorePoolSize()&&files[i].isDirectory()){
                    //当前线程池有未工作线程,启动新的工作任务,这个新的线程会抢占当前线程想要遍历的目录,而当前目录则跳过此目录继续遍历其他目录
                    //以此达到只要线程池有线程不工作,其他线程在遍历时检测到了,立即分配一个目录给其遍历的目的,尽可能提升线程间协同遍历
                    completionService.submit(new FileSearchWorker(completionService,executorService, files[i],searchCount));
                } else {
                    startSearch(files[i],result);
                }
            }
        } else if(file.getName().endsWith("gif")){
            //检索文件
            result.add(file);
        }
        fileCount++;
    }
}
复制代码
//  第三种实现为增加了线程间的同步协作检索,并试图通过职责化线程,让线程检索工作更具体
//----------------------------------------------------------------------------------
/**
 * * 增加改进思路,划分几种不同职责的工作对象
 * 1:一个前置探测工作,获取根目录下,如100个文件夹,后续所有分类工作,只为探测这100个文件夹,专门探测文件夹
 * 2:为持续遍历线程,好处为专心遍历,避免做其他线程工作分配以及代码检测
 * 3:某一个线程,它做任务分派,当检测到有线程空余时,指派任务
 */
static class FileSearcher3 implements Runnable {
    final ThreadPoolExecutor executorService;
    final ExecutorCompletionService completionService;
    final File folder;

    public FileSearcher3(File folder) {
        this.folder = folder;
        //遍历线程池对象,
        executorService = (ThreadPoolExecutor)
                Executors.newFixedThreadPool(4);
        completionService =new ExecutorCompletionService(executorService);
    }

    @Override
    public void run() {
        //第一步,默认检索出指定目录数
        SearchIndexResult indexResult = new Detector(folder, 2).call();
        //所有需要遍历的文件
        final List<File> folders=indexResult.files;
        int taskCount=folders.size();
        int resultCount=indexResult.result.result.size();
        int fileCount=indexResult.result.totalCount;
        folders.forEach(f-> completionService.submit(new FileSearchWorker2(f)));
        try {
            Future<SearchResult> result;
            final long l=System.currentTimeMillis();
            while(null!=(result = completionService.take())){
                SearchResult searchResult = result.get();
                fileCount+=searchResult.totalCount;
                resultCount+=searchResult.result.size();
                System.out.println("fileCount:"+fileCount+" resultCount:"+resultCount);
                //当原子计数器为0时,代表当前检索任务完成,因为CompletionService并不能主动停止服务
                if(0==--taskCount){
                    executorService.shutdownNow();
                    break;
                }
            }
            System.out.println("最终结果-耗时:"+(System.currentTimeMillis()-l)+" 当前共扫描文件:"+fileCount+"个"+",搜索结果:"+resultCount+"个");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("扫描任务完成!");

    }
}

static class FileSearchWorker2 implements Callable<SearchResult>{
    final ThreadPoolExecutor executorService;
    final ExecutorCompletionService completionService;
    final File folder;
    public FileSearchWorker2(File folder) {
        this.folder = folder;
        //遍历线程池对象
        executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(2);
        completionService =new ExecutorCompletionService(executorService);
    }

    @Override
    public SearchResult call(){
        //遍历用户目录下所有文件,但查找到指定文件
        final AtomicInteger searchCount = new AtomicInteger();
        //当前线程负责纷发需要检索目录
        completionService.submit(new FileSearchWorker(completionService,executorService,folder,searchCount));
        int fileCount=0;
        List<File> resultFiles=new ArrayList<>();
        try {
            Future<SearchResult> result;
            while(null!=(result = completionService.take())){
                SearchResult searchResult = result.get();
                fileCount+=searchResult.totalCount;
                resultFiles.addAll(searchResult.result);
                //当原子计数器为0时,代表当前检索任务完成,因为CompletionService并不能主动停止服务
                if(0==searchCount.get()){
                    executorService.shutdownNow();
                    break;
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        return new SearchResult(resultFiles,fileCount);
    }
}

/**
 * 前置工作探测对象,检索出层级深度为指定深度的文件结果
 */
static class Detector implements Callable<SearchIndexResult>{
    final File folder;
    final int maxDepth;
    int fileCount=0;

    public Detector(File folder,int maxDepth) {
        this.folder = folder;
        this.maxDepth = maxDepth;
    }
    @Override
    public SearchIndexResult call()  {
        long l = System.currentTimeMillis();
        final List<File> folders=new ArrayList<>();
        final List<File> result=new ArrayList<>();
        //检索文件
        searchFile(folder,folders,result,maxDepth,0);
        System.out.println("第一步检索结果:"+result.size()+" 检索任务:"+folders.size()+" 检索文件:"+fileCount+" time:"+(System.currentTimeMillis()-l));
        return new SearchIndexResult(new SearchResult(result, fileCount),folders);
    }

    /**
     * 检索文件
     * @param file 当前遍历目录
     * @param result 采集结果
     * @param maxDepth 最大检索深度
     * @param depth 检索文件深度
     */
    private void searchFile(File file,List<File> folders,List<File> result,int maxDepth,int depth){
        if(depth>maxDepth){
            return;
        } else {
            if(file.isDirectory()){
                //在深度上一级,添加需要检索目录
                if(maxDepth-1==depth){
                    folders.add(file);
                }
                File[] files = file.listFiles();
                for(File f:files){
                    searchFile(f,folders,result,maxDepth,depth+1);
                }
            } else if(file.getName().endsWith("gif")){
                //检索文件
                result.add(file);
            }
            fileCount++;
        }
    }
}

static class SearchResult{
    final List<File> result;
    final int totalCount;

    public SearchResult(List<File> result, int totalCount) {
        this.result = result;
        this.totalCount = totalCount;
    }
}

/**
 * 搜索索引结果
 */
static class SearchIndexResult{
    final SearchResult result;
    final List<File> files;

    public SearchIndexResult(SearchResult result, List<File> files) {
        this.result = result;
        this.files = files;
    }
}
复制代码

任务的执行结果

传统的 Future 模式

异步编程的一个好处是:

我只需要定义好任务,向 ExecutorService 中提交即可,而不用关心什么时候,什么线程在执行我们的任务。它会返回一个 Future 对象,我们通过他了解当前任务的执行细节。
复制代码

Future 接口中定义了以下一些方法:

public interface Future<V> {
    //取消执行当前任务
    boolean cancel(boolean mayInterruptIfRunning);
    //当前任务是否被取消了
    boolean isCancelled();
    //当前任务是否已经完成
    boolean isDone();
    //返回任务执行的返回结果,如果任务未完成
    //将阻塞在 Future 内部队列上等待
    V get()
    //新增超时限制
    V get(long timeout, TimeUnit unit)
}
复制代码

这五个方法,每一个都很重要,为我们监控任务的执行提供有力的支持。而我们的 ThreadPoolExecutor 使用的是 FutureTask 作为 Future 的实现类。

state 和它可取的这些值共同描述了当前任务的执行状态,是刚开始执行,还是正在执行中,还是正常结束,还是异常结束,还是被取消了,都由这个 state 来体现。

callable 代表当前正在执行的工作内容,这里说一下为什么只有 Callable 类型的任务,因为所有的 Runnable 类型任务都会被事先转换成 Callable 类型,我觉得主要是统一和抽象实现吧。 outcome 是任务执行结束的返回值,runner 是正在执行当前任务的线程,waiters 是一个简单的单链表,维护的是所有在任务执行结束之前尝试调用 get 方法获取执行结果的线程集合。当任务执行结束自当唤醒队列中所有的线程。 除此之外,还有一个稍显重要的方法,就是 run 方法,这个方法会在任务开始时由 ExecutorService 调用,这是一个很核心的方法。

  1. 如果任务已经开始将退出方法逻辑的执行
  2. 调度任务执行,调用 call 方法
  3. 调用成功将保存结果,异常则将保存异常信息
  4. 处理中断

这里需要额外去说一下,第三步中的 set 方法除了会将任务执行的返回结果设置到 FutureTask 的 outcome 字段上,还会调用 finishCompletion 方法完成任务的调用,尝试唤醒所有在等待任务执行结果的线程。

简单的应用示例:

我们向线程池提交了一个任务,这个任务的工作量不大,就是睡觉然后返回执行结果。而我们可以直接调用 get 方法去获取任务执行的结果,不过 get 方法是阻塞式的,一旦任务还未执行结束,当前线程将丢失 CPU 进而被阻塞到 Future 的内部队列上。 所以,推荐大家在 get 返回结果之前,先判断下目标任务是否已经执行结束,进而避免当前线程的阻塞唤醒所带来的代价。 到这里,相信你也一定看出来了,FutureTask 实现的 Future 的弊端在 get 方法,这个方法非异步,如果没有成功获取到任务的执行结果就将直接阻塞当前线程,以等待任务的执行完成。 但是,有一种情境,当我们向线程池中提交了很多任务,但是不清楚各个任务的执行效率,也就是不知道谁先执行结束,如果直接 get 某个未完成的任务,将导致当前线程阻塞等待。

那么我们能不能阻塞,直接获取已经执行结束的任务 Future,而未完成的任务不允许获取它的 Future?

使用 CompletionService

左半边的类我们已经在前面的文章中都涉猎了,唯独落下了 CompletionService 这个接口,我们当时说以后会分析它的,现在我们来看看这个接口会给我们带来哪些能力。 首先,从类的继承体系上来看,CompletionService 并不与我们的 Executor 产生任何直接关系,线程池的实现也没有继承该接口。 实际上来说,CompletionService 只是利用了 Executor 乃至线程池为自己提供任务的提交与执行能力,而自己不过额外的维护一个队列,保存着所有已经完成的任务的 Future,以至于我们可以直接在外部调用 take 方法直接获取已完成的任务返回结果,无需阻塞。

简单的应用示例:

==要求:使用多线程计算 1-10000 之间的总和==

==思路:分段计算,最后总和相加==

可能很多人会有疑问,这段代码其实也没什么特别的地方啊,我使用基本的线程池不一样也能实现吗? 但是,实际上并没有那么简单,因为你不能确定哪个任务完成了,哪个还没有,所以你至少需要写五个循环自旋等待。 而如果你的运气不好,第一个任务特别慢,即便后续的任务已经结束了,主线程也依然由于第一个任务的结果拿不到而阻塞,耽误了对其他已完成任务的返回结果处理。

CompletionService 实现子类内部结构开始:

字段 executor 是一个任务调度器,completionQueue 是一个阻塞队列。 也就是说,Completion 是完全依赖外部传入的 Executor 来实现任务的提交与执行的。而这个阻塞队列 completionQueue 就是保存的所有已经完成的任务 Future 对象。 除此之外,ExecutorCompletionService 还自定义了一个内部类 QueueingFuture,重写了 FutureTask 的 done 方法。

可能大家对这个 done 没什么印象,但是还记得我们说过的 finishCompletion 方法吗? FutureTask 抽象的描述了一个任务,当线程启动后将调用 FutureTask 内部的 run 方法执行任务的核心逻辑,并在执行的最后调用 finishCompletion 唤醒所有阻塞在自己队列上等待返回结果的线程。 而其中 finishCompletion 方法在结束前,会调用一个 done 方法,这个 done 方法在 FutureTask 中是空实现,没有任何的代码实现,表示并没有什么用。 但是我们的 QueueingFuture 充分利用这一点,重写了 done 方法,而逻辑就是将已结束的任务添加到我们在外部维护的一个新队列 completionQueue 中,供外部获取调用。

定时任务

Java 系统中主要有三种方式来实现定时任务:

  • Timer和TimerTask
  • ScheduledExecutorService

Timer和TimerTask

这种方式的定时任务主要用到两个类,Timer 和 TimerTask。其中,TimerTask 继承接口 Runnable,抽象的描述一种任务类型,我们只要重写实现它的 run 方法就可以实现自定义任务。 而 Timer 就是用于定时任务调度的核心类,demo 中我们调用其 schedule 并指定延时 1000 毫秒,所以上述代码会在一秒钟后完成打印操作,接着程序结束。

Timer 接口中,这两个字段是非常核心重要的:

TaskQueue 是一个队列,内部由动态数组实现的最小堆结构,换句话说,它是一个优先级队列。而优先级参考下一次执行时间,越快执行的越排在前面。

接着,这个 TimerThread 类其实是 Timer 的一个内部类,它继承了 Thread 并重写了其 run 方法,该线程实例将在构建 Timer 实例的时候被启动。 run 方法内部会循环的从队列中取任务,如果没有就阻塞自己,而当我们成功的向队列中添加了定时任务,也会尝试唤醒该线程。 我们也来看一下 Timer 的构造方法:

public Timer(String name) {
    thread.setName(name);
    thread.start();
}
复制代码

为内部线程设置线程名,并启动该线程。

Timer 中用于配置一个定时任务进任务队列的方法。

//在时刻 time 处执行任务
schedule(TimerTask task, Date time)

//延时 delay 毫秒后执行任务
schedule(TimerTask task, long delay)

//固定延时重复执行,firstTime为首次执行时间,
//往后没间隔 period 毫秒执行一次
schedule(TimerTask task, Date firstTime, long period)

//固定延时重复执行
//首次执行时间为当前时间延时 delay 毫秒
schedule(TimerTask task, long delay, long period)

//固定频率重复执行,每过 period 毫秒执行一次
scheduleAtFixedRate(TimerTask task, Date firstTime, long period)

//固定频率重复执行
scheduleAtFixedRate(TimerTask task, long delay, long period)
复制代码

==固定延时== VS ==固定频率==

固定延时:以任务的上一次 实际 执行时间做参考,往后延时 period 毫秒。

固定频率:任务的往后每一次执行时间都在任务提交的那一刻得到了确定,不论你上次任务是否意外延时了,定时定点执行下一次任务。

以当前时间为准,延时 delay 毫秒后第一次执行该任务,并且采取固定延时的方式,每隔 period 毫秒再次执行该任务。

方法需要传入三个参数,参数 task 代表的需要执行的任务体,TimerTask。 参数 time 描述了该任务下一次执行的时刻,计算机底层是以毫秒描述时刻的,所以这里转换为 long 类型来描述时刻。 参数 period 是固定延时的毫秒数。

  • 首先使用任务队列的内置对象锁,锁住个队列。
  • 接着再去锁住我们的 task,并修改其内部的一些属性字段值,nextExecutionTime 指明下一次任务执行时间,period 设置固定延时的毫秒数,修改 state 状态为计划中。
  • 然后将 task 添加到任务队列,其中 add 方法内部会进行最小堆重构,参考的就是 nextExecutionTime 字段的值,越小优先级越高。
  • 判断如果自己就是队列第一个任务,那么将唤醒 Timer 中阻塞了的任务线程。

当我们构造 Timer 实例的时候,就会启动该线程,该线程会在一个死循环中尝试从任务队列上获取任务,如果成功获取就执行该任务并在执行结束之后做一个判断。 如果 period 值为零,则说明这是一次普通任务,执行结束后将从队列首部移除该任务。 如果 period 为负值,则说明这是一次固定延时的任务,修改它下次执行时间 nextExecutionTime 为当前时间减去 period,重构任务队列。 如果 period 为正数,则说明这是一次固定频率的任务,修改它下次执行时间为 上次执行时间加上 period,并重构任务队列。

  • Timer 的背后只有一个线程,不管你有多少个任务,都只有一个工作线程,效率上必然是要打折扣的。
  • 限于单线程,如果第一个任务逻辑上死循环了,后续的任务一个都得不到执行。
  • 依然是由于单线程,任一任务抛出异常后,整个 Timer 就会结束,后续任务全部都无法执行。

ScheduledExecutorService

ScheduledExecutorService中定义的这四个接口方法和 Timer 中对应的方法几乎一样,只不过 Timer 的 scheduled 方法需要在外部传入一个 TimerTask 的抽象任务。 而我们的 ScheduledExecutorService 封装的更加细致了,随便你传 Runnable 或是 Callable,我会在内部给你做一层封装,封装一个类似 TimerTask 的抽象任务类(ScheduledFutureTask)。 然后传入线程池,启动线程去执行该任务,而我们的 ScheduledFutureTask 重写的 run 方法是这样的:

如果 periodic 为 true 则说明这是一个需要重复执行的任务,否则说明是一个一次性任务。 所以实际执行该任务的时候,需要分类,如果是普通的任务就直接调用 run 方法执行即可,否则在执行结束之后还需要重置下下一次执行时间。 整体来说,ScheduledExecutorService 区别于 Timer 的地方就在于前者依赖了线程池来执行任务,而任务本身会判断是什么类型的任务,需要重复执行的在任务执行结束后会被重新添加到任务队列。 而对于后者来说,它只依赖一个线程不停的去获取队列首部的任务并尝试执行它,无论是效率上、还是安全性上都比不上前者。

ThreadLocal

『ThreadLocal』营造了一种「线程本地变量」的概念,也就是说,同一个变量在每个线程的内部,都有一份副本,且相互之间具有不同的取值。

现在要对函数 D 中要打印的字符串进行动态的传入,那你是不是得修改每一个方法的形参列表,增加一个形参位,接着在函数 A 中的调用上传入一个参数过来?

这太繁琐了,我们使用 ThreadLocal 就可以简单解决这种「需求变更」的问题:

这一连串函数的调用必然是同一个线程调用的,那么我们只要在最开头存储下一个变量,无论当前线程调用了多少层函数,这个局部变量一直都存在。 这是 ThreadLocal 的一种使用场景,但有点低估它的价值了,ThreadLocal 最常用的使用场景是,在多线程并发情境下避免一些由于共享变量竞争访问导致的并发问题。 我们来看看广为大家诟病的 SimpleDateFormat,周所周知,这是个多线程不安全的类,我们再次回顾下以前的内容: SimpleDateFormat 是一个用于格式化日期和字符串的工具类,主要有两个核心方法,format 和 parse,前者用于将一个日期转换成指定格式的字符串,后者用于将一个指定格式的字符串转换成一个日期对象。 但是,这两个方法都不是线程安全的,format 方法倒还好,最多导致传入的 Date 格式化成错误的值,而 parse 将直接导致多种异常。原因很简单,他们公用了同一个局部变量。

format 方法的第一个行就是将传入的 Date 对象保存到父类 DateFormat 的字段 calendar 上,然后会在后面逻辑中读取这个 Date 实例并完成转换字符串的逻辑。 但是完全有可能在你设置完日期时间后,其他线程也执行 format 方法并覆盖了你的日期时间 calendar 中的值,这样你后续的转换字符串的动作基于的日期已经不再是传入的日期对象了,导致的最终结果就是错误将别人的日期 Date 转换成字符串并返回了。

明显的是构造的上一个线程传入的 Date 参数,也就是在格式化的过程中被别的线程覆盖了自己传入的 Date 导致的错误的格式化数据。 parse 方法的线程不安全就不带大家重现了,它更严重,因为方法内部会执行一个 clear 操作清空 calendar 字段保存的值,并且还是非线程安全式的清空,会导致某些其他线程发生转换异常的。

而我们简单的使用 ThreadLocal 就可以解决上述 format 的线程不安全问题:

ThreadLocal 的 set 方法将导致每个线程的内部都持有一个 SimpleDateFormat 的实例,自己用自己的,也就不存在因为共享变量而导致的数据一致性问题了。

基本原理

基本字段属性

除了 threadLocalHashCode 是一个常量,每当创建一个新的 ThreadLocal 实例的时候就会根据 nextHashCode 和 HASH_INCREMENT 去计算初始的赋值。 因为 nextHashCode 是静态的,是类共享的,所以,每创建一个 ThreadLocal 实例,它的 threadLocalHashCode 是前一个实例的基础上加固定常量 0x61c88647。 这个值经换算是一个斐波那契数,每次增量该常量可以分散 hash 值的分布,减少后续在 map 中定位保存数据时产生冲突。

内部类 ThreadLocalMap

ThreadLocalMap 的内部实现是很类似 HashMap 的内部实现的。

首先,Entry 这个类是 ThreadLocalMap 中定义的内部类,很简单,保存了两个主要内容,一个是 ThreadLocal 的局部变量,一个是 Object 类型的 value 值。 INITIAL_CAPACITY 指定了 table 的初始化容量,或者说是默认的数组初始化长度。 size 指定了 table 中实际有效的 Entry 数量。 threshold 是一个阈值的概念抽象,当 table 的 size 达到了这个阈值,就会触发一个动态扩容动作,扩容 table。 所以,对于 ThreadLocal 的一个不太恰当的理解是,它只是一个封装了 hashCode 的 key,这个 key 决定了我们的 value 该保存在 ThreadLocalMap 内部 table 的哪个位置。

这个 i 就是当前 Entry 要保存在 table 上的具体索引,它是如何计算的? 就是用我们的 key(ThreadLocal 实例)内部保存的 hashcode 取余 table 容量计算而来。 threshold 会被设置为 table 容量的三分之二。

set、get方法原理

Thread 类中的一个字段:

Thread 类中持有了两个 ThreadLocalMap 实例,两个实例稍有区别,inheritableThreadLocals 相比于 threadLocals 来说具有更大的特殊性。 区别在于,如果父线程(即创建自己的那个线程)使用了 inheritableThreadLocals 存储线程本地变量,那么本线程的创建过程中也会使用 inheritableThreadLocals 进行本地变量的存储并且将父线程中所有的本地变量进行一份拷贝,填充到自己的 inheritableThreadLocals 中。 具体怎么实现的大家可以自行去查看,jdk 中重新定义了一个 InheritableThreadLocal 类,继承的 ThreadLocal 并重写了其中的 getMap 方法,导致你外部的 get 操作会转而返回 inheritableThreadLocals 而不再是 threadLocals。 现在我们来看 ThreadLocal 的 set 方法:

set 方法还是很简单的,获取当前线程内部的 ThreadLocalMap 实例,如果不是空的就往里面增加一条记录,反之先初始化一个 map 再增加一条记录进去。

核心还是在 ThreadLocalMap 的 set 方法:

  • 根据 ThreadLocal 这个 key 计算出当前节点应该保存在 table 的哪个索引位置
  • 如果该位置上不是空,产生了 hash 冲突,被别的节点提前占有了。那么会将该节点保存在 i+1 的索引位置上
  • 如果该位置是空,那么将自己挂在这个位置上
  • 最后,如果添加结束后,发现 table 中有效节点数达到了阈值 threshold,那么将调用 rehash 方法进行一次扩容并转移数据的过程。

get 方法:

既然存是用的 ThreadLocal 实例作为 key,取自然也是根据该实例进行 get 了。

内存泄露

首先,ThreadLocal 确实是存在『内存泄漏』这个内存隐患的,但是一大堆人把源头指向 Entry 这个节点类。

很明显,我们 Entry 将 key 存储为『弱引用』,什么是弱引用这里不再赘述了,而将 value 存储为『强引用』,于是他们的内存结构就是这样的(盗了张图):

我们的 ThreadLocal 实例被创建在堆中,方法栈中存在一个对它的强引用,我们的 Entry 实例中存在一个对他的弱引用。 重点来了,有人就认为,一旦我在主程序中丢失了对该实例的强引用,或是赋空了该实例,那么 GC 会无视该实例存在着一个弱引用,而直接回收了该资源,以至于你永远无法访问到该 Entry 实例的 value 属性且无法回收它,所以导致的内存泄漏。

你换成强引用,会导致整个 Entry 实例都是无用数据,更大的内存泄漏。反而使用弱引用后,当你调用 get 方法的时候,会由于 key 为 null,执行清除逻辑,将 Entry 实例赋 null,最后由 GC 回收该内存资源。 但这始终不能解决 ThreadLocal 的内存泄漏问题,建议的做法是,当某个本地变量不用的时候,手动的调用 remove 方法进行移除。

信号量 Semaphore

它类似一个资源池(读者可以类比线程池),每个线程需要调用 acquire() 方法获取资源,然后才能执行,执行完后,需要 release 资源,让给其他的线程用。

大概大家也可以猜到,Semaphore 其实也是 AQS 中共享锁的使用,因为每个线程共享一个池嘛。

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

Semaphore 适用于什么样的使用场景呢,我们举个通俗的例子:

假如现在有一个停车场,里面有只十个停车位,当着十个停车位都被占用了,外面的车就不允许进入了,就必须在外面等着。出来一辆车才允许进去一辆车

这个场景不同于我们一般的并发场景,一般来说,我们的临界资源只能允许一个线程进行访问,其他线程都地等着。 但是,有一种场景是,临界资源允许多个线程同时访问,超过限定数量的外的线程得阻塞等待。

Semaphore 可以说是为上述这种场景而生的一个工具类:

你看,出来一个线程才允许进去一个线程,这就是 Semaphore。 semaphore 的内部原理其实你去看源码,你会发现和我们的 ReentrantLock 的实现是极其类似的,包括公平与非公平策略的支持,只不过,AQS 里面的 state 在前者的实现中,一般小于等于一(除非重入锁),而后者的 state 则小于等于十,记录的是剩余可用临界资源数量。 所以,semaphore 天生就存在一个问题,如果某个线程重入了临界区,可用临界资源的数量是否需要减少?

停车场一共十个停车位,一辆车进去并占有了一个停车位,过了一段时间,这个向管理员报告,我还要占用一个停车位,先不管他占两个干啥,此时的管理员会同意吗?

实际上,在 Java 这个管理员看来,已经进入临界区的线程是「老爷」,提出的要求都会优先满足,即便他自身占有的资源并没有释放。 所以,在 Semaphore 机制里,一个线程进入临界区之后占用掉所有的临界资源都是可能的。

构造方法:

public Semaphore(int permits) {
    sync = new NonfairSync(permits);
}

public Semaphore(int permits, boolean fair) {
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
复制代码

看 acquire 方法:

public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}
public void acquireUninterruptibly() {
    sync.acquireShared(1);
}
public void acquire(int permits) throws InterruptedException {
    if (permits < 0) throw new IllegalArgumentException();
    sync.acquireSharedInterruptibly(permits);
}
public void acquireUninterruptibly(int permits) {
    if (permits < 0) throw new IllegalArgumentException();
    sync.acquireShared(permits);
}
复制代码

我们接下来看不抛出 InterruptedException 异常的 acquireUninterruptibly() 方法吧:

public void acquireUninterruptibly() {
    sync.acquireShared(1);
}
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}
复制代码

Semaphore 分公平策略和非公平策略,我们对比一下两个 tryAcquireShared 方法:

// 公平策略:
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;
    }
}
// 非公平策略:
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;
    }
}
复制代码

我们再回到 acquireShared 方法,

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

由于 tryAcquireShared(arg) 返回小于 0 的时候,说明 state 已经小于 0 了(没资源了),此时 acquire 不能立马拿到资源,需要进入到阻塞队列等待,虽然贴了很多代码,不在乎多这点了:

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);
    }
}
复制代码

线程挂起后等待有资源被 release 出来。接下来,我们就要看 release 的方法了:

// 任务介绍,释放一个资源
public void release() {
    sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

protected final boolean tryReleaseShared(int releases) {
    for (;;) {
        int current = getState();
        int next = current + releases;
        // 溢出,当然,我们一般也不会用这么大的数
        if (next < current) // overflow
            throw new Error("Maximum permit count exceeded");
        if (compareAndSetState(current, next))
            return true;
    }
}
复制代码

tryReleaseShared 方法总是会返回 true,然后是 doReleaseShared,这个也是我们熟悉的方法了,我就贴下代码,不分析了,这个方法用于唤醒所有的等待线程:

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);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}
复制代码

倒计时门栓 CountDownLatch

CountDownLatch 这个类是比较典型的 AQS 的共享模式的使用,这是一个高频使用的类。latch 的中文意思是门栓、栅栏。

使用例子

我们看下 Doug Lea 在 java doc 中给出的例子,这个例子非常实用,我经常会写到这个代码。

假设我们有 N ( N > 0 ) 个任务,那么我们会用 N 来初始化一个 CountDownLatch,然后将这个 latch 的引用传递到各个线程中,在每个线程完成了任务后,调用 latch.countDown() 代表完成了一个任务。

调用 latch.await() 的方法的线程会阻塞,直到所有的任务完成。

class Driver2 { // ...
    void main() throws InterruptedException {
        CountDownLatch doneSignal = new CountDownLatch(N);
        Executor e = Executors.newFixedThreadPool(8);

        // 创建 N 个任务,提交给线程池来执行
        for (int i = 0; i < N; ++i) // create and start threads
            e.execute(new WorkerRunnable(doneSignal, i));

        // 等待所有的任务完成,这个方法才会返回
        doneSignal.await();           // wait for all to finish
    }
}

class WorkerRunnable implements Runnable {
    private final CountDownLatch doneSignal;
    private final int i;

    WorkerRunnable(CountDownLatch doneSignal, int i) {
        this.doneSignal = doneSignal;
        this.i = i;
    }

    public void run() {
        try {
            doWork(i);
            // 这个线程的任务完成了,调用 countDown 方法
            doneSignal.countDown();
        } catch (InterruptedException ex) {
        } // return;
    }

    void doWork() { ...}
}
复制代码

有这么一个常见的场景,我们一起来看看:

大家日常经常使用的拼多多,一件商品至少需要两到三人拼团,商家才会发货。

这里,我们不去研究它的商业模式,不管他是怎么实现盈利的,就这么一种场景,如果要用基本的并发 API 来实现,你可能会想到:

来一个线程阻塞一次,知道达到指定的数量后,全部唤醒

对,没错,CountDownLatch 内部就是这样实现的,轮子已经帮你造好了,我们来看看该怎么实现上述的模型案例:

多运行几次,你会发现结果不会错,拼团的人先后顺序可能不同,但商家一定是在三个人都准备好了之后才会发货。 除此之外,它还有更多的应用,比如百米赛跑,只有当所有运动员都准备好了之后,裁判员才会吹响哨子,等等等等。 实现原理也基本和显式锁类似,不同点依然在于对 state 的控制,CountDownLatch 只判断 state 是否等于零,不等于零就说明时机未到,阻塞当前线程。 而每一次的 countDown 方法调用都会减少一次倒计时资源,直至为零才唤醒阻塞的线程。

所以说 CountDownLatch 非常实用,我们常常会将一个比较大的任务进行拆分,然后开启多个线程来执行,等所有线程都执行完了以后,再往下执行其他操作。这里例子中,只有 main 线程调用了 await 方法。

我们再来看另一个例子,这个例子很典型,用了两个 CountDownLatch:

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
        // 因为这里 N == 1,所以,只要调用一次,那么所有的 await 方法都可以通过
        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() { ...}
}
复制代码

这个例子中,doneSignal 同第一个例子的使用,我们说说这里的 startSignal。N 个新开启的线程都调用了startSignal.await() 进行阻塞等待,它们阻塞在栅栏上,只有当条件满足的时候(startSignal.countDown()),它们才能同时通过这个栅栏,目的是让所有的线程站在一个起跑线上。

如果始终只有一个线程调用 await 方法等待任务完成,那么 CountDownLatch 就会简单很多,所以之后的源码分析读者一定要在脑海中构建出这么一个场景:有 m 个线程是做任务的,有 n 个线程在某个栅栏上等待这 m 个线程做完任务,直到所有 m 个任务完成后,n 个线程同时通过栅栏。

源码分析

构造方法,需要传入一个不小于 0 的整数:

public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}
// 老套路了,内部封装一个 Sync 类继承自 AQS
private static final class Sync extends AbstractQueuedSynchronizer {
    Sync(int count) {
        // 这样就 state == count 了
        setState(count);
    }
    ...
}
复制代码

AQS 里面的 state 是一个整数值,这边用一个 int count 参数其实初始化就是设置了这个值,所有调用了 await 方法的等待线程会挂起,然后有其他一些线程会做 state = state - 1 操作,当 state 减到 0 的同时,那个将 state 减为 0 的线程会负责唤醒 所有调用了 await 方法的线程。

对于 CountDownLatch,我们仅仅需要关心两个方法,一个是 countDown() 方法,另一个是 await() 方法。

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

我们用以下程序来分析源码,t1 和 t2 负责调用 countDown() 方法,t3 和 t4 调用 await 方法阻塞:

public class CountDownLatchDemo {

    public static void main(String[] args) {

        CountDownLatch latch = new CountDownLatch(2);

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException ignore) {
                }
                // 休息 5 秒后(模拟线程工作了 5 秒),调用 countDown()
                latch.countDown();
            }
        }, "t1");

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException ignore) {
                }
                // 休息 10 秒后(模拟线程工作了 10 秒),调用 countDown()
                latch.countDown();
            }
        }, "t2");

        t1.start();
        t2.start();

        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    // 阻塞,等待 state 减为 0
                    latch.await();
                    System.out.println("线程 t3 从 await 中返回了");
                } catch (InterruptedException e) {
                    System.out.println("线程 t3 await 被中断");
                    Thread.currentThread().interrupt();
                }
            }
        }, "t3");
        Thread t4 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    // 阻塞,等待 state 减为 0
                    latch.await();
                    System.out.println("线程 t4 从 await 中返回了");
                } catch (InterruptedException e) {
                    System.out.println("线程 t4 await 被中断");
                    Thread.currentThread().interrupt();
                }
            }
        }, "t4");

        t3.start();
        t4.start();
    }
}
复制代码

上述程序,大概在过了 10 秒左右的时候,会输出:

线程 t3 从 await 中返回了
线程 t4 从 await 中返回了
复制代码

这两条输出,顺序不是绝对的

后面的分析,我们假设 t3 先进入阻塞队列

接下来,我们按照流程一步一步走:先 await 等待,然后被唤醒,await 方法返回。

首先,我们来看 await() 方法,它代表线程阻塞,等待 state 的值减为 0。

public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    // 这也是老套路了,我在第二篇的中断那一节说过了
    if (Thread.interrupted())
        throw new InterruptedException();

    // t3 和 t4 调用 await 的时候,state 都大于 0(state 此时为 2)。
    // 也就是说,这个 if 返回 true,然后往里看
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}
// 只有当 state == 0 的时候,这个方法才会返回 1
protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}
复制代码

从方法名我们就可以看出,这个方法是获取共享锁,并且此方法是可中断的(中断的时候抛出 InterruptedException 退出这个方法)。

private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    // 1. 入队
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                // 同上,只要 state 不等于 0,那么这个方法返回 -1
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            // 2
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
复制代码

我们来仔细分析这个方法,线程 t3 经过第 1 步 addWaiter 入队以后,我们应该可以得到这个:

由于 tryAcquireShared 这个方法会返回 -1,所以 if (r >= 0) 这个分支不会进去。到 shouldParkAfterFailedAcquire 的时候,t3 将 head 的 waitStatus 值设置为 -1,如下:

然后进入到 parkAndCheckInterrupt 的时候,t3 挂起。

我们再分析 t4 入队,t4 会将前驱节点 t3 所在节点的 waitStatus 设置为 -1,t4 入队后,应该是这样的:

然后,t4 也挂起。接下来,t3 和 t4 就等待唤醒了。

接下来,我们来看唤醒的流程。为了让下面的示意图更丰富些,我们假设用 10 初始化 CountDownLatch。

首先,我们看 countDown() 方法:

public void countDown() {
    sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
    // 只有当 state 减为 0 的时候,tryReleaseShared 才返回 true
    // 否则只是简单的 state = state - 1 那么 countDown() 方法就结束了
    //    将 state 减到 0 的那个操作才是最复杂的,继续往下吧
    if (tryReleaseShared(arg)) {
        // 唤醒 await 的线程
        doReleaseShared();
        return true;
    }
    return false;
}
// 这个方法很简单,用自旋的方法实现 state 减 1
protected boolean tryReleaseShared(int releases) {
    for (;;) {
        int c = getState();
        if (c == 0)
            return false;
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}
复制代码

countDown 方法就是每次调用都将 state 值减 1,如果 state 减到 0 了,那么就调用下面的方法进行唤醒阻塞队列中的线程:

// 调用这个方法的时候,state == 0
// 这个方法先不要看所有的代码,按照思路往下到我写注释的地方,我们先跑通一个流程,其他的之后还会仔细分析
private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            // t3 入队的时候,已经将头节点的 waitStatus 设置为 Node.SIGNAL(-1) 了
            if (ws == Node.SIGNAL) {
                // 将 head 的 waitStatue 设置为 0
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                // 就是这里,唤醒 head 的后继节点,也就是阻塞队列中的第一个节点
                // 在这里,也就是唤醒 t3
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) // todo
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}
复制代码

一旦 t3 被唤醒后,我们继续回到 await 的这段代码,parkAndCheckInterrupt 返回,我们先不考虑中断的情况:

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); // 2. 这里是下一步
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                // 1. 唤醒后这个方法返回
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
复制代码

接下来,t3 会进到 setHeadAndPropagate(node, r) 这个方法,先把 head 给占了,然后唤醒队列中其他的线程:

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node);

    // 下面说的是,唤醒当前 node 之后的节点,即 t3 已经醒了,马上唤醒 t4
    // 类似的,如果 t4 后面还有 t5,那么 t4 醒了以后,马上将 t5 给唤醒了
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            // 又是这个方法,只是现在的 head 已经不是原来的空节点了,是 t3 的节点了
            doReleaseShared();
    }
}
复制代码

又回到这个方法了,那么接下来,我们好好分析 doReleaseShared 这个方法,我们根据流程,头节点 head 此时是 t3 节点了:

// 调用这个方法的时候,state == 0
private void doReleaseShared() {
    for (;;) {
        Node h = head;
        // 1. h == null: 说明阻塞队列为空
        // 2. h == tail: 说明头结点可能是刚刚初始化的头节点,
        //   或者是普通线程节点,但是此节点既然是头节点了,那么代表已经被唤醒了,阻塞队列没有其他节点了
        // 所以这两种情况不需要进行唤醒后继节点
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            // t4 将头节点(此时是 t3)的 waitStatus 设置为 Node.SIGNAL(-1) 了
            if (ws == Node.SIGNAL) {
                // 这里 CAS 失败的场景请看下面的解读
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                // 就是这里,唤醒 head 的后继节点,也就是阻塞队列中的第一个节点
                // 在这里,也就是唤醒 t4
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     // 这个 CAS 失败的场景是:执行到这里的时候,刚好有一个节点入队,入队会将这个 ws 设置为 -1
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        // 如果到这里的时候,前面唤醒的线程已经占领了 head,那么再循环
        // 否则,就是 head 没变,那么退出循环,
        // 退出循环是不是意味着阻塞队列中的其他节点就不唤醒了?当然不是,唤醒的线程之后还是会调用这个方法的
        if (h == head)                   // loop if head changed
            break;
    }
}
复制代码

我们分析下最后一个 if 语句,然后才能解释第一个 CAS 为什么可能会失败:

  • h == head:说明头节点还没有被刚刚用 unparkSuccessor 唤醒的线程(这里可以理解为 t4)占有,此时 break 退出循环。
  • h != head:头节点被刚刚唤醒的线程(这里可以理解为 t4)占有,那么这里重新进入下一轮循环,唤醒下一个节点(这里是 t4 )。我们知道,等到 t4 被唤醒后,其实是会主动唤醒 t5、t6、t7...,那为什么这里要进行下一个循环来唤醒 t5 呢?我觉得是出于吞吐量的考虑。 满足上面的 2 的场景,那么我们就能知道为什么上面的 CAS 操作 compareAndSetWaitStatus(h, Node.SIGNAL, 0) 会失败了?

因为当前进行 for 循环的线程到这里的时候,可能刚刚唤醒的线程 t4 也刚刚好到这里了,那么就有可能 CAS 失败了。

for 循环第一轮的时候会唤醒 t4,t4 醒后会将自己设置为头节点,如果在 t4 设置头节点后,for 循环才跑到 if (h == head),那么此时会返回 false,for 循环会进入下一轮。t4 唤醒后也会进入到这个方法里面,那么 for 循环第二轮和 t4 就有可能在这个 CAS 相遇,那么就只会有一个成功了。

循环屏障 CyclicBarrier

字面意思是“可重复使用的栅栏”或“周期性的栅栏”,总之不是用了一次就没用了的,CyclicBarrier 相比 CountDownLatch 来说,要简单很多,其源码没有什么高深的地方,它是 ReentrantLock 和 Condition 的组合使用。看如下示意图,CyclicBarrier 和 CountDownLatch 是不是很像,只是 CyclicBarrier 可以有不止一个栅栏,因为它的栅栏(Barrier)可以重复使用(Cyclic)。

CountDownLatch 基于 AQS 的共享模式的使用,而 CyclicBarrier 基于 Condition 来实现。

先用一张图来描绘下 CyclicBarrier 里面的一些概念,和它的基本使用流程:

考虑这么一个场景:

公寓的班车总是在公寓楼下装满一车人之后,出发并开到地铁站,接着再回来接下一班人。

这么一个场景,我们考虑该怎么实现:

CyclicBarrier 就像一个屏障,实例化的时候需要传入两个参数,第一个参数指定我们的屏障最多拦截多少个线程后就打开屏障,第二个参数指明最后一个到达屏障的线程需要额外做的操作。 一般而言,最后一个线程到达屏障后,屏障将会打开,释放前面所有的线程,并在最后重新关上屏障。 CyclicBarrier 只需要用到一个 await 就可以完成所有的功能,我们总结下该方法的实现逻辑:

首先,减少一次可用资源数量 如果可用资源数为零,则说明自己是最后一个线程,于是会执行我们传入的额外操作,唤醒所有已经到达在等待的线程,并重新开启一个屏障计数。 否则说明自己不是最后一个线程,于是将自身线程在一个循环当中阻塞到一个条件队列上

好了,看完 CyclicBarrier 你会发现,它真的很类似我们的倒计时门栓,下面我们就来阐述他俩的区别与联系。

第一个区别

倒计时门栓 CountDownLatch 一旦被打开后就不能再次合上,也是说只要被调用了足够次数的 countDown,await 方法就会失效,它是一次性的。 CyclicBarrier 是循环发生的,当最后一个线程到达屏障,会优先重置屏障计数,屏障再次开启拦截阻隔。

第二个区别

CountDownLatch 是计数器, 线程来一个就记一个,此期间不阻塞线程,当达到指定数量之后才会去唤醒外部等待的线程,也就是说外部是有一个乃至多个线程等待一个条件满足之后才能继续执行,而这个条件就是满足一定数量的线程,这样才能激活当前外部线程的继续执行。 CyclicBarrier 像一个栅栏,来一个线程阻塞一个,直到阻塞了指定数量的线程后,一次性全部激活,让他们同时执行,像一个百米冲刺一样。

然后我们开始源码分析:

public class CyclicBarrier {
    // 我们说了,CyclicBarrier 是可以重复使用的,我们把每次从开始使用到穿过栅栏当做"一代",或者"一个周期"
    private static class Generation {
        boolean broken = false;
    }

    /** The lock for guarding barrier entry */
    private final ReentrantLock lock = new ReentrantLock();

    // CyclicBarrier 是基于 Condition 的
    // Condition 是“条件”的意思,CyclicBarrier 的等待线程通过 barrier 的“条件”是大家都到了栅栏上
    private final Condition trip = lock.newCondition();

    // 参与的线程数
    private final int parties;

    // 如果设置了这个,代表越过栅栏之前,要执行相应的操作
    private final Runnable barrierCommand;

    // 当前所处的“代”
    private Generation generation = new Generation();

    // 还没有到栅栏的线程数,这个值初始为 parties,然后递减
    // 还没有到栅栏的线程数 = parties - 已经到栅栏的数量
    private int count;

    public CyclicBarrier(int parties, Runnable barrierAction) {
        if (parties <= 0) throw new IllegalArgumentException();
        this.parties = parties;
        this.count = parties;
        this.barrierCommand = barrierAction;
    }

    public CyclicBarrier(int parties) {
        this(parties, null);
    }
复制代码

先看怎么开启新的一代:

// 开启新的一代,当最后一个线程到达栅栏上的时候,调用这个方法来唤醒其他线程,同时初始化“下一代”
private void nextGeneration() {
    // 首先,需要唤醒所有的在栅栏上等待的线程
    trip.signalAll();
    // 更新 count 的值
    count = parties;
    // 重新生成“新一代”
    generation = new Generation();
}
复制代码

开启新的一代,类似于重新实例化一个 CyclicBarrier 实例

看看怎么打破一个栅栏:

private void breakBarrier() {
    // 设置状态 broken 为 true
    generation.broken = true;
    // 重置 count 为初始值 parties
    count = parties;
    // 唤醒所有已经在等待的线程
    trip.signalAll();
}
复制代码

这两个方法之后用得到,现在开始分析最重要的等待通过栅栏方法 await 方法:

// 不带超时机制
public int await() throws InterruptedException, BrokenBarrierException {
    try {
        return dowait(false, 0L);
    } catch (TimeoutException toe) {
        throw new Error(toe); // cannot happen
    }
}
// 带超时机制,如果超时抛出 TimeoutException 异常
public int await(long timeout, TimeUnit unit)
    throws InterruptedException,
           BrokenBarrierException,
           TimeoutException {
    return dowait(true, unit.toNanos(timeout));
}
复制代码

继续往里看:

private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
    final ReentrantLock lock = this.lock;
    // 先要获取到锁,然后在 finally 中要记得释放锁
    // 如果记得 Condition 部分的话,我们知道 condition 的 await() 会释放锁,被 signal() 唤醒的时候需要重新获取锁
    lock.lock();
    try {
        final Generation g = generation;
        // 检查栅栏是否被打破,如果被打破,抛出 BrokenBarrierException 异常
        if (g.broken)
            throw new BrokenBarrierException();
        // 检查中断状态,如果中断了,抛出 InterruptedException 异常
        if (Thread.interrupted()) {
            breakBarrier();
            throw new InterruptedException();
        }
        // index 是这个 await 方法的返回值
        // 注意到这里,这个是从 count 递减后得到的值
        int index = --count;

        // 如果等于 0,说明所有的线程都到栅栏上了,准备通过
        if (index == 0) {  // tripped
            boolean ranAction = false;
            try {
                // 如果在初始化的时候,指定了通过栅栏前需要执行的操作,在这里会得到执行
                final Runnable command = barrierCommand;
                if (command != null)
                    command.run();
                // 如果 ranAction 为 true,说明执行 command.run() 的时候,没有发生异常退出的情况
                ranAction = true;
                // 唤醒等待的线程,然后开启新的一代
                nextGeneration();
                return 0;
            } finally {
                if (!ranAction)
                    // 进到这里,说明执行指定操作的时候,发生了异常,那么需要打破栅栏
                    // 之前我们说了,打破栅栏意味着唤醒所有等待的线程,设置 broken 为 true,重置 count 为 parties
                    breakBarrier();
            }
        }

        // loop until tripped, broken, interrupted, or timed out
        // 如果是最后一个线程调用 await,那么上面就返回了
        // 下面的操作是给那些不是最后一个到达栅栏的线程执行的
        for (;;) {
            try {
                // 如果带有超时机制,调用带超时的 Condition 的 await 方法等待,直到最后一个线程调用 await
                if (!timed)
                    trip.await();
                else if (nanos > 0L)
                    nanos = trip.awaitNanos(nanos);
            } catch (InterruptedException ie) {
                // 如果到这里,说明等待的线程在 await(是 Condition 的 await)的时候被中断
                if (g == generation && ! g.broken) {
                    // 打破栅栏
                    breakBarrier();
                    // 打破栅栏后,重新抛出这个 InterruptedException 异常给外层调用的方法
                    throw ie;
                } else {
                    // 到这里,说明 g != generation, 说明新的一代已经产生,即最后一个线程 await 执行完成,
                    // 那么此时没有必要再抛出 InterruptedException 异常,记录下来这个中断信息即可
                    // 或者是栅栏已经被打破了,那么也不应该抛出 InterruptedException 异常,
                    // 而是之后抛出 BrokenBarrierException 异常
                    Thread.currentThread().interrupt();
                }
            }

              // 唤醒后,检查栅栏是否是“破的”
            if (g.broken)
                throw new BrokenBarrierException();

            // 这个 for 循环除了异常,就是要从这里退出了
            // 我们要清楚,最后一个线程在执行完指定任务(如果有的话),会调用 nextGeneration 来开启一个新的代
            // 然后释放掉锁,其他线程从 Condition 的 await 方法中得到锁并返回,然后到这里的时候,其实就会满足 g != generation 的
            // 那什么时候不满足呢?barrierCommand 执行过程中抛出了异常,那么会执行打破栅栏操作,
            // 设置 broken 为true,然后唤醒这些线程。这些线程会从上面的 if (g.broken) 这个分支抛 BrokenBarrierException 异常返回
            // 当然,还有最后一种可能,那就是 await 超时,此种情况不会从上面的 if 分支异常返回,也不会从这里返回,会执行后面的代码
            if (g != generation)
                return index;

            // 如果醒来发现超时了,打破栅栏,抛出异常
            if (timed && nanos <= 0L) {
                breakBarrier();
                throw new TimeoutException();
            }
        }
    } finally {
        lock.unlock();
    }
}
复制代码

首先,我们看看怎么得到有多少个线程到了栅栏上,处于等待状态:

public int getNumberWaiting() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return parties - count;
    } finally {
        lock.unlock();
    }
}
复制代码

判断一个栅栏是否被打破了,这个很简单,直接看 broken 的值即可:

public boolean isBroken() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return generation.broken;
    } finally {
        lock.unlock();
    }
}
复制代码

前面我们在说 await 的时候也几乎说清楚了,什么时候栅栏会被打破,总结如下:

  • 中断,我们说了,如果某个等待的线程发生了中断,那么会打破栅栏,同时抛出 InterruptedException 异常;
  • 超时,打破栅栏,同时抛出 TimeoutException 异常;
  • 指定执行的操作抛出了异常,这个我们前面也说过。

最后,我们来看看怎么重置一个栅栏:

public void reset() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        breakBarrier();   // break the current generation
        nextGeneration(); // start a new generation
    } finally {
        lock.unlock();
    }
}
复制代码

我们设想一下,如果初始化时,指定了线程 parties = 4,前面有 3 个线程调用了 await 等待,在第 4 个线程调用 await 之前,我们调用 reset 方法,那么会发生什么?

首先,打破栅栏,那意味着所有等待的线程(3个等待的线程)会唤醒,await 方法会通过抛出 BrokenBarrierException 异常返回。然后开启新的一代,重置了 count 和 generation,相当于一切归零了。

Phaser

启动 10 个线程执行任务,由于启动时间有先后,我们希望等到所有的线程都启动成功以后再开始执行,让每个线程在同一个起跑线上开始执行业务操作。

Phaser phaser = new Phaser();
// 1. 注册一个 party
phaser.register();

for (int i = 0; i < 10; i++) {

    phaser.register();

    executorService.submit(() -> {
        // 2. 每个线程到这里进行阻塞,等待所有线程到达栅栏
        phaser.arriveAndAwaitAdvance();

        // doWork()
    });
}
phaser.arriveAndAwaitAdvance();
复制代码

Phaser 比较灵活,它不需要在构造的时候指定固定数目的 parties,而 CountDownLatch 和 CyclicBarrier 需要在构造函数中明确指定一个数字。

我们可以看到,上面的代码总共执行了 11 次 phaser.register() ,可以把 11 理解为 CountDownLatch 中的 count 和 CyclicBarrier 中的 parties。

这样读者应该很容易理解 phaser.arriveAndAwaitAdvance() 了,这是一个阻塞方法,直到该方法被调用 11 次,所有的线程才能同时通过。

这里和 CyclicBarrier 是一个意思,凑齐了所有的线程,一起通过栅栏。

Phaser 也有周期的概念,一个周期定义为一个 phase,从 0 开始。

重要接口介绍

Phaser 还是有 parties 概念的,但是它不需要在构造函数中指定,而是可以很灵活地动态增减。

我们来看 3 个代码片段,看看 parties 是怎么来的。

1、首先是 Phaser 有一个带 parties 参数的构造方法:

public Phaser(int parties) {
    this(null, parties);
}
复制代码

2、register() 方法:

public int register() {
    return doRegister(1);
}
复制代码

这个方法会使得 parties 加 1

3、bulkRegister(int parties) 方法:

public int bulkRegister(int parties) {
    if (parties < 0)
        throw new IllegalArgumentException();
    if (parties == 0)
        return getPhase();
    return doRegister(parties);
}
复制代码

一次注册多个,这个方法会使得 parties 增加相应数值

parties 也可以减少,因为有些线程可能在执行过程中,不和大家玩了,会进行退出,调用 arriveAndDeregister() 即可,这个方法的名字已经说明了它的用途了。

再看一下这个图,phase-1 结束的时候,黑色的线程离开了大家,此时就只有 3 个 parties 了。

这里说一下 Phaser 的另一个概念 phase,它代表 Phaser 中的周期或者叫阶段,phase 从 0 开始,一直往上递增。

通过调用 arrive() 或 arriveAndDeregister() 来标记有一个成员到达了一个 phase 的栅栏,当所有的成员都到达栅栏以后,开启一个新的 phase。

1、arrive()

这个方法标记当前线程已经到达栅栏,但是该方法不会阻塞,注意,它不会阻塞。

大家要理解一点,party 本和线程是没有关系的,不能说一个线程代表一个 party,因为我们完全可以在一个线程中重复调用 arrive() 方法。

2、arriveAndDeregister()

和上面的方法一样,当前线程通过栅栏,非阻塞,但是它执行了 deregister 操作,意味着总的 parties 减 1。

3、arriveAndAwaitAdvance()

这个方法应该一目了然,就是等其他线程都到了栅栏上再一起通过,进入下一个 phase。

4、awaitAdvance(int phase)

这个方法需要指定 phase 参数,也就是说,当前线程会进行阻塞,直到指定的 phase 打开。

5、protected boolean onAdvance(int phase, registeredParties)

这个方法是 protected 的,所以它不是 phaser 提供的 API,从方法名字上也可以看出,它会在一个 phase 结束的时候被调用。

它的返回值代表是否应该终结(terminate)一个 phaser,之所以拿出来说,是因为我们经常会见到有人通过覆写该方法来自定义 phaser 的终结逻辑,如:

protected boolean onAdvance(int phase, int registeredParties) {
    return phase >= N || registeredParties == 0;
}
复制代码

1、我们可以通过 phaser.isTerminated() 来检测一个 phaser 实例是否已经终结了

2、当一个 phaser 实例被终结以后,register()、arrive() 等这些方法都没有什么意义了,大家可以玩一玩,观察它们的返回值,原本应该返回 phase 值的,但是这个时候会返回一个负数。

Phaser 的监控方法

介绍下几个用于返回当前 phaser 状态的方法:

getPhase():返回当前的 phase,前面说了,phase 从 0 开始计算,最大值是 Integer.MAX_VALUE,超过又从 0 开始

getRegisteredParties():当前有多少 parties,随着不断地有 register 和 deregister,这个值会发生变化

getArrivedParties():有多少个 party 已经到达当前 phase 的栅栏

getUnarrivedParties():还没有到达当前栅栏的 party 数

Phaser 的分层结构

Tiering 这个词本身就不好翻译,大家将就一下,要表达的意思就是,将多个 Phaser 实例构造成一棵树。

1、第一个问题来了,为什么要把多个 Phaser 实例构造成一棵树,解决什么问题?有什么优点?

Phaser 内部用一个 state 来管理状态变化,随着 parties 的增加,并发问题带来的性能影响会越来越严重。

/**
 * 0-15: unarrived
 * 16-31: parties,   所以一个 phaser 实例最大支持 2^16-1=65535 个 parties
 * 32-62: phase,     31 位,那么最大值是 Integer.MAX_VALUE,达到最大值后又从 0 开始
 * 63: terminated
 */
private volatile long state;
复制代码

通常我们在说 0-15 位这种,说的都是从低位开始的

state 的各种操作依赖于 CAS,典型的无锁操作,但是,在大量竞争的情况下,可能会造成很多的自旋。

而构造一棵树就是为了降低每个节点(每个 Phaser 实例)的 parties 的数量,从而有效降低单个 state 值的竞争。

2、第二个问题,它的结构是怎样的?

Phaser root = new Phaser(5);

Phaser n1 = new Phaser(root, 5);
Phaser n2 = new Phaser(root, 5);

Phaser m1 = new Phaser(n1, 5);
Phaser m2 = new Phaser(n1, 5);
Phaser m3 = new Phaser(n1, 5);

Phaser m4 = new Phaser(n2, 5);
复制代码

这棵树上有 7 个 phaser 实例,每个 phaser 实例在构造的时候,都指定了 parties 为 5,但是,对于每个拥有子节点的节点来说,每个子节点都是它的一个 party,我们可以通过 phaser.getRegisteredParties() 得到每个节点的 parties 数量:

m1、m2、m3、m4 的 parties 为 5

n1 的 parties 为 5 + 3,n2 的 parties 为 5 + 1

root 的 parties 为 5 + 2

在子节点注册第一个 party 的时候,这个时候会在父节点注册一个 party,注意这里说的是子节点添加第一个 party 的时候,而不是说实例构造的时候。

Phaser m5 = new Phaser(n2);
System.out.println("n2 parties: " + n2.getRegisteredParties());
m5.register();
System.out.println("n2 parties: " + n2.getRegisteredParties());
复制代码

第一行代码中构造了 m5 实例,但是此时它的 parties == 0,所以对于父节点 n2 来说,它的 parties 依然是 6,所以第二行代码输出 6。第三行代码注册了 m5 的第一个 party,显然,第四行代码会输出 7。

当子节点的 parties 降为 0 的时候,会从父节点中"剥离",我们在上面的基础上,再加两行代码:

m5.arriveAndDeregister();
System.out.println("n2 parties: " + n2.getRegisteredParties());
复制代码

由于 m5 之前只有一个 parties,所以一次 arriveAndDeregister() 就会使得它的 parties 变为 0,此时第二行代码输出父节点 n2 的 parties 为 6。

在非树的结构中,此时 m5 应该处于 terminated 状态,因为它的 parties 降为 0 了,不过在树的结构中,这个状态由 root 控制,所以我们依然可以执行 m5.register()...

3、每个 phaser 实例的 phase 周期有快有慢,怎么协调的?

在组织成树的这种结构中,每个 phaser 实例的 phase 已经不受自己控制了,由 root 来统一协调,也就是说,root 当前的 phase 是多少,每个 phaser 的 phase 就是多少。

那又有个问题,如果子节点的一个周期很快就结束了,要进入下一个周期怎么办?需要等!这个时候其实要等所有的节点都结束当前 phase,因为只有这样,root 节点才有可能结束当前 phase。

我觉得 Phaser 中的树结构我们要这么理解,我们要把整棵树当做一个 phaser 实例,每个节点只是辅助用于降低并发而存在,整棵树还是需要满足 Phaser 语义的。

4、这种树结构在什么场景下会比较实用?设置每个节点的 parties 为多少比较合适?

转载于:https://juejin.im/post/5cb14254f265da03594862fc

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值