JAVA 多线程与高并发学习笔记(十六)——高并发设计模式

本部分介绍几种在高并发场景中常用的设计模式:线程安全的单例模式、ForkJoin模式、生产者——消费者模式、Master-Worker模式和Future模式。

线程安全的单例模式

单例模式很常见,一般用于全局对象管理,比如XML读写实例、系统配置实例、任务调度实例、数据库连接池实例等。

从饿汉式单例到懒汉式单例

按照单例对象初始化的时机,单例模式分为懒汉式和饿汉式,懒汉式单例对象在类被加载时就直接被初始化。

public class Singleton1 {
    // 简单的懒汉单例模式
    private Singleton1() {} // 私有构造器

    private static final Singleton1 single = new Singleton1();
    public static Singleton1 getInstance() {
        return single;
    }
}

饿汉式单例模式的优点是足够简单、安全,缺点是单例对象在类被加载时实例就创建,很多时候类被加载时并不需要初始化单例。

懒汉式单例在使用的时候才进行初始化。

public class ASingleton {

    static ASingleton instance; // 静态成员
    // 私有构造器
    private ASingleton() {}

    static ASingleton getInstance() {
        if(instance == null) {
            instance = new ASingleton();
        }
        return instance;
    }
}

这两种单例模式的问题是都不是线程安全的。

使用内置锁保护懒汉式单例

可以为getInstance()方法添加synchronized内置锁进行单利获取同步。

public class ASingleton {

    static ASingleton instance; // 静态成员
    // 私有构造器
    private ASingleton() {}

    static synchronized ASingleton getInstance() {
        if(instance == null) {
            instance = new ASingleton();
        }
        return instance;
    }
}

使用synchronized后每次调用都要同步,争用激烈的场景下,内置锁会升级为重量级锁,开销大,性能差,所以高并发下不推荐使用。

双重检查锁单例模式

实际上,单例模式的加锁操作只有单例在第一次创建的时候才用到,之后的单例获取操作没必要再加锁。所以,可以先判断单例对象是否已经被初始化,如果没有,加锁后再初始化,这种模式叫做双重检查锁(Double Checked Locking)单例模式。

public class ASingleton {

    static ASingleton instance; // 静态成员
    // 私有构造器
    private ASingleton() {}

    static synchronized ASingleton getInstance() {
        if(instance == null) { // 检查1
            synchronized (ASingleton.class) {
                if(instance == null) { // 检查2
                    instance = new ASingleton();
                }
            }
        }
        return instance;
    }
}

双重检查锁单例模式主要包括三步:

  1. 检查对象是否被初始化,如果已经被初始化,就立即返回单例对象,此次检查无需锁进行同步。
  2. 如果单例没有被初始化,就试图进入临界区进行初始化操作,此时采取获取锁。
  3. 进入临界区后,再次检查单例对象是否已经被初始化,如果没有就初始化一个实例。

需要二次检查的原因是在多线程场景下有可能多个线程通过了第一次检查。

双重检查不仅避免了单例对象返回初始化,而且除了初始化的时候需要现加锁外,后续的所有调用都不需要加锁而直接返回单例,从而提升了获取单例时的性能。

使用双重检查锁+volatile

上述的双重检查锁单例模式还有问题,看一下初始化代码:

instance = new Singleton();

这行代码转换成汇编指令后,大致会细分成三个:

  1. 分配一块内存M。
  2. 在内存M上初始化Singleton对象。
  3. M的地址赋值给instance变量。

编译器、CPU都可能对没有内存屏障、数据依赖关系的操作进行重排序,上述的三个指令优化后可能变成了:

  1. 分配一块内存M。
  2. M的地址赋值给instance变量。
  3. 在内存M上初始化Singleton对象。

这样有可能导致isntance未初始化就被其他线程调用,得到一个未初始化的对象。

因此需要通过volatile禁止指令重排。

public class ASingleton {

    static volatile ASingleton instance; // 静态成员
    // 私有构造器
    private ASingleton() {}

    static synchronized ASingleton getInstance() {
        if(instance == null) { // 检查1
            synchronized (ASingleton.class) {
                if(instance == null) { // 检查2
                    instance = new ASingleton();
                }
            }
        }
        return instance;
    }
}

使用静态内部类实现懒汉式单例模式

双重检查锁+volatile的方式能实现高性能、线程安全的单例模式,但是该实现的底层原理比较复杂,写法繁琐。另一种简单的单例模式实现是使用静态内部类实例懒汉式单例模式。

public class Singleton {
    // 静态内部类
    private static class LazyHolder {
        // 通过final保证初始化时的线程安全
        private static final Singleton INSTANCE = new Singleton();
    }
    // 私有构造器
    private Singleton() {}
    // 获取单例的方法
    public static final Singleton getInstance() {
        // 返回内部类的静态、最终成员
        return LazyHolder.INSTANCE;
    }
}

Master-Worker模式

Master-Worker模式是一种常见的高并发模式,核心思想是分而治之,将任务调度和执行分离,调度任务的角色为Master,Master负责接收、分配任务和合并任务结果,Worker负责执行任务。

master-worker.png

Master-Worker模式的参考实现

Master-Worker模式理解起来不难,但实现起来有很多要点需要注意。下面看一个简单的累加求和的任务使用Master-Worker模式实现。

Master的参考代码


public class Master {

    // 所有Worker的集合
    private HashMap> workers = new HashMap<>();
    // 任务的集合
    private LinkedBlockingDeque taskQueue = new LinkedBlockingDeque<>();
    // 任务c处理结果集合
    private Map resultMap = new ConcurrentHashMap<>();
    // Master的任务调度线程
    private Thread thread = null;

    private AtomicLong sum = new AtomicLong(0);

    public Master(int workerCount) {
        // 每个Worker对象都需要持有queue的引用,用于领任务与提交结果
        for(int i = 0; i < workerCount; i++) {
            Worker worker = new Worker<>();
            workers.put("子节点:" + i, worker);
        }
        thread = new Thread(() -> this.execute());
        thread.start();
    }

    // 提交任务
    public void submit(T task) {
        taskQueue.add(task);
    }

    private void resultCallBack(Object o) {
        Task task = (Task)o;
        String taskName = "Worker:" + task.getWorkerId()
                + "-" + "Task:" + task.getId();
        R result = task.getResult();
        resultMap.put(taskName, result);
        sum.getAndAdd((Integer)result); // 和的累加
    }

    // 启动所有的子任务
    private void execute() {
        for(;;) {
            for(Map.Entry> entry : workers.entrySet()) {
                T task = null;
                try {
                    task = this.taskQueue.take(); // 获取任务
                    Worker worker = entry.getValue(); // 获取节点
                    worker.submit(task, this::resultCallBack); // 分配任务
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 获取最终给结果
    public void printResult() {
        System.out.println("---------sum is : " + sum.get());
        for(Map.Entry entry : resultMap.entrySet()) {
            String taskName = entry.getKey();
            System.out.println(taskName + ":" + entry.getValue());
        }
    }


}

Master负责接收客户端提交的任务,然后通过阻塞队列对任务进行缓存。Master所拥有的线程作为阻塞队列的消费者,不断从阻塞队列获取任务并轮流分给Worker。

Worker的参考代码


public class Worker {

    // 接受任务的阻塞队列
    private LinkedBlockingDeque taskQueue = new LinkedBlockingDeque<>();
    // Worker 的编号
    static AtomicInteger index = new AtomicInteger(1);
    private int workerId;
    private Thread thread = null;
    public Worker() {
        this.workerId = index.getAndIncrement();
        thread = new Thread(() -> this.run());
        thread.start();
    }

    // 轮询执行任务
    public void run() {
        // 轮询启动所有的子任务
        for(;;) {
            try {
                // 从阻塞队列中提取任务
                T task = this.taskQueue.take();
                task.setWorkerId(workerId);
                task.execute();
            } catch(InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    // 接收任务列异步队列
    public void submit(T task, Consumer action) {
        task.resultAction = action; // 设置回调
        try {
            this.taskQueue.put(task);
        } catch(InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Worker接收Master分配的任务,同样也通过阻塞队列对局部任务进行缓存。Worker所拥有的线程作为局部任务的阻塞队列的消费者,不断从阻塞队列获取任务并执行,执行完成后回调Master传递过来的回调函数。

异步任务类


public class Task {

    static AtomicInteger index = new AtomicInteger(1);
    // 任务的回调函数
    public Consumer> resultAction;
    // 任务的id
    private int id;
    private int workerId;
    // 计算结果
    R result = null;

    public Task() {
        this.id = index.getAndIncrement();
    }

    public void execute() {
        this.result = this.doExecute();
        // 执行回调函数
        resultAction.accept(this);
    }

    // 由子类实现
    protected R doExecute() {
        return null;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getWorkerId() {
        return workerId;
    }

    public void setWorkerId(int workerId) {
        this.workerId = workerId;
    }

    public R getResult() {
        return result;
    }

}

异步任务类在执行子任务的 doExecute 方法后,回调一下Master传递过来的回调函数,将执行完成后的任务进行回调。

测试用例

下面测试一下。


public class MasterWorkerTest {
    // 简单任务
    static class SimpleTask extends Task {
        @Override
        protected Integer doExecute() {

            System.out.println("task " + getId() + " is done ");
            return getId();
        }
    }

    @Test
    public void testMasterWorker() {
        // 创建master,包含4个Worker,并启动Master的执行线程
        Master master = new Master<>(4);

        ScheduledExecutorService pool1 = Executors.newScheduledThreadPool(1);

        pool1.scheduleAtFixedRate(() -> master.submit(
                new SimpleTask()
        ), 0,1, TimeUnit.SECONDS);

        ScheduledExecutorService pool2 = Executors.newScheduledThreadPool(1);
        pool2.scheduleAtFixedRate(() -> master.printResult(),0, 2, TimeUnit.SECONDS);

        try {
            Thread.sleep(10000);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

执行测试用例,结果如下:

---------sum is : 0
task 1 is done 
task 2 is done 
task 3 is done 
---------sum is : 6
Worker:1-Task:3:3
Worker:3-Task:1:1
Worker:2-Task:2:2
task 4 is done 
task 5 is done 
---------sum is : 15
Worker:4-Task:4:4
Worker:3-Task:5:5
Worker:1-Task:3:3
Worker:3-Task:1:1
Worker:2-Task:2:2
task 6 is done 
task 7 is done 
---------sum is : 28
Worker:1-Task:7:7
Worker:2-Task:6:6
Worker:4-Task:4:4
Worker:3-Task:5:5
Worker:1-Task:3:3
Worker:3-Task:1:1
Worker:2-Task:2:2
task 8 is done 
task 9 is done 
---------sum is : 36
Worker:1-Task:7:7
Worker:2-Task:6:6
Worker:4-Task:8:8
Worker:3-Task:9:9
Worker:4-Task:4:4
Worker:3-Task:5:5
Worker:1-Task:3:3
Worker:3-Task:1:1
Worker:2-Task:2:2
task 10 is done 
task 11 is done 
---------sum is : 66
Worker:1-Task:7:7
Worker:2-Task:6:6
Worker:4-Task:8:8
Worker:2-Task:10:10
Worker:3-Task:9:9
Worker:4-Task:4:4
Worker:1-Task:11:11
Worker:3-Task:5:5
Worker:1-Task:3:3
Worker:3-Task:1:1
Worker:2-Task:2:2

Process finished with exit code 0

Netty 中 Master-Worker 模式的实现

高性能传输模式 Reactor 模式就是 Master-Worker 模式在传输领域的一种应用。基于Java的NIO技术,Netty设计了一套优秀的、高性能Reactor(反应器)模式的具体实现。

在Netty中,EventLoop反应器内部都有一个线程负责Java NIO选择器的事件轮询,然后进行对应的事件分发,事件分发的目标就是Netty的Handler处理程序(含用户定义的业务处理逻辑)。

Netty服务器程序中需要设置两个EventLoopGroup轮询组,一个组负责连接的监听和接收,一个组负责IO传输事件和轮询与分发,两个轮询组的职责具体如下:

  1. 负责新连接的监听和接收的EventLoopGroup轮询组中的反应器完成查询通道的新连接IO事件查询,这些反应器有点像负责招工的包工头,因此被称为BOSS。

  2. 另一个轮询组中的反应器完成查询所有子通道的IO事件,并且执行对应的Handler处理程序完成IO处理,例如数据的输入和输出,这个轮询组被称为Worker。

Nginx 中Master-Worker模式的实现

Nginx 在启动后会以daemon方式在后台运行,它的后台进程有两类:一类称为Master进程(管理进程),另一个类称为Worker进程(工作进程)。

nginx.png

Nginx 的 Master 进程主要负责调度 Worker 进程,比如加载配置,启动工作进程,接收来自外界的信号,向各Worker进程发送信号,监控Worker进程的运行状态等。Master进程负责创建监听套接口,交由Worker进程进行连接监听。

Worker进程主要用来处理网络时间,当一个Worker进程在介绍一条连接通道之后,就开始读取请求,解析请求,处理请求,处理完成产生的数据后,再返回给客户端,最后断开连接通道。

ForkJoin模式

ForkJoin模式也是“分而治之”思想的一种应用,不过它没有Master角色,ForkJoin模式将大的任务分割成小的任务,一直到任务的规模足够小,可以使用很简单、直接的方式来完成。

ForkJoin模式的原理

ForkJoin模式先把一个大任务分解成许多个独立的子任务,然后开启多个线程并行区处理这些子任务。有可能子任务还是很大而且需要进一步分解,最终得到足够小的任务。

forkjoin.png

ForkJoin框架

JUC包提供了一套ForkJoin框架的实现,具体以ForkJoinPool线程池的形式提供,并且该线程池在 Java 8 的 Lambda 并行流框架中充当着底层框架的角色。ForkJoin框架包含如下组件:

  1. ForkJoinPool:执行任务的线程池,继承了AbstractExectutorService 类。

  2. ForkJoinWorkerThread:执行任务的工作线程。每个线程都维护着一个内部队列,用于存放“内部任务”,该类继承了Thread类。

  3. ForkJoinTask:用于ForkJoinPool的任务抽象类,实现了Future接口。

  4. RecursiveTask:带返回结果的递归执行任务,是ForkJoinTask的子类,在子任务带返回结果时使用。

  5. RecursiveAction:不返回结果的递归执行任务,是ForkJoinTask的子类,在子任务不带返回结果时使用

日常使用一般通过继承 RecursiveTask 或 RecursiveAction 来实现自定义的任务类,自定义任务类需要实现这些子类的 compute 方法,该方法的执行流程一般如下:

    if 任务足够小
        直接返回结果
    else
        分割成N个任务
        依次调用每个子任务的fork方法执行子任务
        依次调用每个子任务的join方法,等待子任务完成,然后合并执行结果。

ForkJoin框架使用实战

可递归执行的异步任务类AccumulateTask


public class AccmulateTask extends RecursiveTask {

    private static final int THRESHOLD = 2;
    // 累加的起始编号
    private int start;
    // 累加的结束编号
    private int end;

    public AccmulateTask(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        int sum = 0;
        // 判断任务的规模,若规模小则可以直接计算
        boolean canCompute = (end - start) <= THRESHOLD;
        // 如果任务已经足够小,则可以直接计算
        if(canCompute) {
            //直接结算并返回结果,Recursive结束
            for(int i = start; i <= end; i++) {
                sum += i;
            }
            System.out.println("执行任务,计算" + start + "到" +
                    end + "的和,结果是:" + sum);
        } else {
            System.out.println("切割任务:将" + start + "到" + end + "的和一分为二");
            int middle = (start + end) / 2;
            // 切割成两个子任务
            AccmulateTask lTask = new AccmulateTask(start, middle);
            AccmulateTask rTask = new AccmulateTask(middle + 1, end);
            // 依次调用每个子任务的fork方法执行子任务
            lTask.fork();
            rTask.fork();
            // 等待子任务完成,依次调用每个子任务的join方法合并执行结果
            int leftResult = lTask.join();
            int rightResult = rTask.join();
            // 合并子任务执行结果
            sum = leftResult + rightResult;
        }
        return sum;
    }
}

使用ForkJoinPool调度AccumulateTask

使用ForkJoinPool调度AccumulateTask的示例代码如下:


public class AccmulateTask extends RecursiveTask {

    private static final int THRESHOLD = 2;
    // 累加的起始编号
    private int start;
    // 累加的结束编号
    private int end;

    public AccmulateTask(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        int sum = 0;
        // 判断任务的规模,若规模小则可以直接计算
        boolean canCompute = (end - start) <= THRESHOLD;
        // 如果任务已经足够小,则可以直接计算
        if(canCompute) {
            //直接结算并返回结果,Recursive结束
            for(int i = start; i <= end; i++) {
                sum += i;
            }
            System.out.println("执行任务,计算" + start + "到" +
                    end + "的和,结果是:" + sum);
        } else {
            System.out.println("切割任务:将" + start + "到" + end + "的和一分为二");
            int middle = (start + end) / 2;
            // 切割成两个子任务
            AccmulateTask lTask = new AccmulateTask(start, middle);
            AccmulateTask rTask = new AccmulateTask(middle + 1, end);
            // 依次调用每个子任务的fork方法执行子任务
            lTask.fork();
            rTask.fork();
            // 等待子任务完成,依次调用每个子任务的join方法合并执行结果
            int leftResult = lTask.join();
            int rightResult = rTask.join();
            // 合并子任务执行结果
            sum = leftResult + rightResult;
        }
        return sum;
    }
}

执行测试用例,运行结果如下:


切割任务:将1到100的和一分为二
切割任务:将1到50的和一分为二
切割任务:将1到25的和一分为二
切割任务:将1到13的和一分为二
切割任务:将1到7的和一分为二
切割任务:将1到4的和一分为二
执行任务,计算1到2的和,结果是:3
执行任务,计算3到4的和,结果是:7
执行任务,计算5到7的和,结果是:18
切割任务:将8到13的和一分为二
执行任务,计算8到10的和,结果是:27
执行任务,计算11到13的和,结果是:36
切割任务:将14到25的和一分为二
切割任务:将14到19的和一分为二
执行任务,计算14到16的和,结果是:45
执行任务,计算17到19的和,结果是:54
切割任务:将20到25的和一分为二
执行任务,计算20到22的和,结果是:63
执行任务,计算23到25的和,结果是:72
切割任务:将26到50的和一分为二
切割任务:将26到38的和一分为二
切割任务:将26到32的和一分为二
切割任务:将26到29的和一分为二
执行任务,计算26到27的和,结果是:53
执行任务,计算28到29的和,结果是:57
执行任务,计算30到32的和,结果是:93
切割任务:将33到38的和一分为二
执行任务,计算33到35的和,结果是:102
执行任务,计算36到38的和,结果是:111
切割任务:将39到50的和一分为二
切割任务:将39到44的和一分为二
执行任务,计算39到41的和,结果是:120
执行任务,计算42到44的和,结果是:129
切割任务:将45到50的和一分为二
执行任务,计算45到47的和,结果是:138
执行任务,计算48到50的和,结果是:147
切割任务:将51到100的和一分为二
切割任务:将51到75的和一分为二
切割任务:将51到63的和一分为二
切割任务:将51到57的和一分为二
切割任务:将51到54的和一分为二
执行任务,计算51到52的和,结果是:103
执行任务,计算53到54的和,结果是:107
执行任务,计算55到57的和,结果是:168
切割任务:将58到63的和一分为二
执行任务,计算58到60的和,结果是:177
执行任务,计算61到63的和,结果是:186
切割任务:将64到75的和一分为二
切割任务:将64到69的和一分为二
执行任务,计算64到66的和,结果是:195
执行任务,计算67到69的和,结果是:204
切割任务:将70到75的和一分为二
执行任务,计算70到72的和,结果是:213
执行任务,计算73到75的和,结果是:222
切割任务:将76到100的和一分为二
切割任务:将76到88的和一分为二
切割任务:将76到82的和一分为二
切割任务:将76到79的和一分为二
执行任务,计算76到77的和,结果是:153
执行任务,计算78到79的和,结果是:157
执行任务,计算80到82的和,结果是:243
切割任务:将83到88的和一分为二
执行任务,计算83到85的和,结果是:252
执行任务,计算86到88的和,结果是:261
切割任务:将89到100的和一分为二
切割任务:将89到94的和一分为二
执行任务,计算89到91的和,结果是:270
执行任务,计算92到94的和,结果是:279
切割任务:将95到100的和一分为二
执行任务,计算95到97的和,结果是:288
执行任务,计算98到100的和,结果是:297
最终的计算结果:5050

ForkJoin框架的核心API

ForkJoin框架的核心是ForkJoinPool线程池。该线程池使用一个无锁的栈来管理空闲线程。如果一个工作线程暂时取不到可用的任务,则可能被挂起,而挂起的线程将被压入由ForkJoinPool维护的栈中,待有新任务到来时,再从栈中唤醒这些线程。

ForkJoinPool构造器

public ForkJoinPool(int parallelism,
                        ForkJoinWorkerThreadFactory factory,
                        UncaughtExceptionHandler handler,
                        boolean asyncMode) {
    this(checkParallelism(parallelism),
            checkFactory(factory),
            handler,
            asyncMode ? FIFO_QUEUE : LIFO_QUEUE,
            "ForkJoinPool-" + nextPoolId() + "-worker-");
    checkPermission();
}

以上构造器4个参数具体如下:

  • parallelism:可并行级别。此参数决定框架内并行执行的线程数量。并行的每一个任务都会有一个线程处理,该属性不是框架中最大的线程数量,和框架可存在的线程数量并不是绝对关联的。

  • factory:线程创建工厂。ForkJoin框架创建一个新的线程时,同样会用到线程创建工厂。只不过这个线程工厂不再需要实现ThreadFactory接口,而是需要实现ForkJoinWorkerThreadFactory接口,它是一个函数式接口,只需要实现一个名为newThread的方法。ForkJoin框架有一个默认的接口实现DefaultForkJoinWorkerThreadFactory。

  • handler:异常捕获处理程序。当执行的任务中出现异常,并从任务中被抛出时,就会被handler捕获。

  • asyncMode:异步模式。该参数表示任务是否为异步模式,默认为false。asyncMode为true,表示子任务的执行组训FIFO顺序,并且子任务不能被合并;如果asyncMode为false,就表示子任务的执行遵循LIFO顺序,子任务可以被合并。asyncMode异步模式仅指任务的调度方式。

ForkJoinPool无参数的、默认的构造器如下:

public ForkJoinPool() {
    this(Math.min(MAX_CAP, Runtime.getRuntime().availableProcessors()),
            defaultForkJoinWorkerThreadFactory, null, false);
}

该构造器的parallelism值为CPU核数,factory值为defaultForkJoinWorkerThreadFactory默认的线程工厂,异常捕获处理程序handler值为null,表示不会进行异常处理,异步模式asyncMode值为false,使用LIFO,可以合并子任务的模式。

ForkJoinPool的common通用池

调用 ForkJoinPool.commonPool 方法可以获取该 ForkJoin 线程池,该线程池通过 makeCommonPool 来构造,具体代码如下:

private static ForkJoinPool makeCommonPool() {
    int parallelism = -1;
    ForkJoinWorkerThreadFactory factory = null;
    UncaughtExceptionHandler handler = null;
    try {  // ignore exceptions in accessing/parsing properties
        String pp = System.getProperty
            ("java.util.concurrent.ForkJoinPool.common.parallelism");
        String fp = System.getProperty
            ("java.util.concurrent.ForkJoinPool.common.threadFactory");
        String hp = System.getProperty
            ("java.util.concurrent.ForkJoinPool.common.exceptionHandler");
        if (pp != null)
            parallelism = Integer.parseInt(pp);
        if (fp != null)
            factory = ((ForkJoinWorkerThreadFactory)ClassLoader.
                        getSystemClassLoader().loadClass(fp).newInstance());
        if (hp != null)
            handler = ((UncaughtExceptionHandler)ClassLoader.
                        getSystemClassLoader().loadClass(hp).newInstance());
    } catch (Exception ignore) {
    }
    if (factory == null) {
        if (System.getSecurityManager() == null)
            factory = defaultForkJoinWorkerThreadFactory;
        else // use security-managed default
            factory = new InnocuousForkJoinWorkerThreadFactory();
    }
    if (parallelism < 0 && // default 1 less than #cores
        (parallelism = Runtime.getRuntime().availableProcessors() - 1) <= 0)
        parallelism = 1;
    if (parallelism > MAX_CAP)
        parallelism = MAX_CAP;
    return new ForkJoinPool(parallelism, factory, handler, LIFO_QUEUE,
                            "ForkJoinPool.commonPool-worker-");
}

使用common池的优点是可以通过指定系统属性的方式定义“并行度、线程工厂和异常处理类”,并且common池使用的是同步模式,也就是说可以支持任务合并。

通过系统属性指定parallelism值的示例:

System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "8");

向ForkJoinPool线程池提交任务的方式

可以向ForkJoinPool线程池提交一下两类任务:

  1. 外部任务(External/Submissions Task)提交。向ForkJoinPool 提交外部任务有三种方式:
    • 调用invoke方法,该方法提交任务后线程会等待,等到任务计算完毕返回结果。
    • 调用execute方法提交一个任务来异步执行,无返回结果。
    • 调用submit方法提交一个任务,并且会返回一个ForkJoinTask实例,之后适当的时候可通过ForkJoinTask实例获取执行结果。
  2. 子任务(Worker Task)提交。由任务实例的fork方法完成。当任务被分割之后,内部会调用ForkJoinPool.WorkQueue.push方法直接把任务放到内部队列汇总等待被执行。

工作窃取算法

ForkJoinPool线程池的任务分为“外部任务”和“内部任务”,两种任务的存放位置不同:

  • 外部任务放在 ForkJoinPool 的全局队列中。
  • 子任务会作为“内部任务”放到内部队列中,ForkJoinPool池中的每个线程都维护着一个内部队列,用于存放这些“内部任务”。

由于ForkJoinPool线程池通常有多个工作线程,与之相对应的就会有多个任务队列,这就会出现任务分配不均衡的问题。工作窃取算法就是帮忙将任务从繁忙的线程分摊给空闲的线程。

工作窃取算法的核心思想是,工作线程自己的活干完了之后,会去看看其它线程有没有没完成的任务,如果有就拿过来帮忙。工作窃取算法的主要逻辑,每个线程拥有一个双端队列,用于存放要执行的任务,当自己的队列没有任务时,可以从其它线程的任务队列中获取一个任务继续执行。

forkjoin-steal.png

为了避免在任务窃取中发生线程安全问题,一种简单的优化方法是:在线程自己的本地队列采用LIFO策略,窃取其它任务队列的任务时采用FIFO策略。简单来说,就是获取自己队列的任务从头开始,窃取其它队列的任务从尾开始。

ForkJoin框架的原理

ForkJoin框架的核心原理大致如下:

  1. ForkJoin框架的线程池ForkJoinPool的任务分为“外部任务”和“内部任务”。
  2. “外部任务”放在ForkJoinPool的全局队列中。
  3. ForkJoinPool池中的每个线程都维护着一个任务队列,用于存放“内部任务”,线程切割任务得到的子任务会作为“内部任务”放到内部队列中。
  4. 当工作线程想要拿到子任务的计算结果时,先判断子任务有没有完成,如果没有完成,在判断子任务有没有被其他线程“窃取”,如果子任务没有被窃取,则有本线程来完成;一旦子任务被窃取了,就去执行本线程“内部队列”的其它任务,或者扫描其它的任务队列并窃取任务。
  5. 当工作线程完成其“内部任务”,处于空闲状态时,就会扫描其它的任务队列窃取任务,尽可能不会阻塞等待。

工作窃取算法的优点如下:

  1. 线程不会因为等待某个子任务的执行或者没有内部任务要执行而被阻塞等待、挂起,而是会扫描所有队列窃取任务,直到所有队列都为空时才会被挂起。
  2. ForkJoin框架为每个线程维护者一个内部任务队列以及一个全局的任务队列,而且任务队列都是双向队列,可从首尾两端来获取任务,极大减少竞争的可能性,提高并行的性能。

生产者-消费者模式

生产者-消费者模式是一个经典的多线程设计模式,它为多线程间的协作提供了良好的解决方案,是高并发编程过程汇总常用的一种设计模式。

产生数据的模块可以成为生产者,消费数据的模块可以称为消费者。还需要一个数据缓冲区作为生产者和消费者之间的中介,使他们解耦。

producer-consumer.png

Future 模式

Future模式的核心思想是异步调用。它不会立即返回我们需要的数据,而是返回一个契约(或异步任务),将来凭借这个契约(或异步任务)获取结果。

下面是一个客户端调服务端的示例。实现了Future模式的客户端在调用服务端得到返回结果后并不急于对其进行处理而是调用其他业务逻辑,充分利用等待时间,这就是Future模式的核心所在。在完成其他业务处理后,最后再使用返回比较慢的Future数据。这样在整个调用中就不存在无谓的等待,充分利用所有的时间片,从而提高了系统响应速度。

future.png

在实现上,Future模式和异步回调模式既有区别,又有俩你洗。Java的Future实现类并没有支持异步回调,任然需要主动获取耗时任务的结果;而Java 8 中的CompletableFuture组件实现了异步回调模式。

你可能感兴趣的:(JAVA 多线程与高并发学习笔记(十六)——高并发设计模式)