Java并发的多种实现

以Thread、线程池、ForkJoinPool和Actor框架看java并发实现。

原生Thread

原生线程的并发难度在于理解力和想像力,可以做的很多,也可以很少。借鉴一些已有的工具,或许更能开阔思路。

    new Thread(()->{
            //to do
    }).start();

Executors工具类

作为Java并发方面的领军人物Doug Lea的杰作,Executors单从应用场景的角度就给出了很多解决方案。创建一个固定线程数量的线程池,没有空闲线程,又有新任务加入时,则任务会被放入队列,直到有可用线程。

Executors.newFixedThreadPool(10);

根据需要创建新线程的线程池,并能够重用以前构造的线程。在执行许多短暂异步任务时,可以明显提高性能。调用execute时,如果有可重用的线程则重用,没有空闲线程可重用,则创建新线程。60秒未使用的线程,则会从缓存中删除。

Executors.newCachedThreadPool();

创建一个线程池,给定一个延迟时间或指定一个定时周期,空闲线程会被保留在池中。

Executors.newScheduledThreadPool(10);

创建一个单线程,任务会被放入队列中,当执行任务出现异常,导致线程不能正常返回,则创建一个新线程继续执行任务。

Executors.newSingleThreadExecutor();

创建一个工作窃取线程池,线程间相互协作,共同完成任务,所以每个线程所领取的到任务数不固定。

Executors.newWorkStealingPool();

ForkJoinPool任务调度框架

ForkJoinPool在使用时唯一需要做的是,告诉它如何分解任务,如:有0~1000的数字,在多线程下如果想让每个线程领到10个数字,这时,就需要在一个线程拿到数字的开始与结束的区间时做出判断,看这个区间的数字个数是否大于10,如果大于10就再分解一次。这种任务分解与线程调度的场景,ForkJoinPool有专门的抽象类去定义,只需要重写任务分解逻辑,就可以很好的工作。

ForkJoinTask有很多子类,由这些子类去定义分解方式。针对不同的场景选择不同的子类,然后进行逻辑重写,会是一个很好的选择。示例中采用递归的方式分解任务,使用RecursiveAction作为分解规范。

@Override      //使用jdk7的api,后续版本的jdk允许这个方法有返回值
protected void compute() {
            if(et - st < unit){     //判断区间包含的数字个数是否大于规定的值
                c.call(st, et);       //拿到想要的数据后扔给业务处理逻辑
            }else {
                int middle =(st+et)/2;      //对任务进行分解
                TaskUnit l = new TaskUnit(st, middle,c);   //把任务一分为二,变成两个任务
                TaskUnit r = new TaskUnit(middle, et,c); 
                l.fork();    //把任务交由线程调度去执行
                r.fork();
                l.join();
                r.join();
            }
        }
}

以上是使用ForkJoinPool框架进行此类任务分解的核心,重点在于对任务的分解,以及何时对业务逻辑进行调用。ForkJoinPool完整示例

Actor编程模型

Actor模型由Carl Hewitt在几十年前提出,在网络中作为高性能并行处理的一种方式 - 当时还没有这种环境。如今,硬件和基础设施能力已经赶上,传统的面向对象编程(OOP)模型,无法完全构建具有苛刻要求的分布式系统。

因为OOP的核心是封装,规定对象的内部数据,不能直接从外部访问,只能通过调用一组特定的方法来修改,以保护其封装数据的不变性及操作安全。

消息传递的使用避免了锁定和阻塞,actor不是方法间的调用,而是互相发送消息。发送消息不会导致调用方,与被调用方处在同一个线程中。actor可以连续发送而不会阻塞。因此,它可以在相同的时间内完成更多。

对于对象,当方法返回时,它释放对执行线程的控制。在这方面,actor的行为与对象非常相似,它们在处理完消息时,对消息作出反应并返回执行。通过这种方式,actor实现了如我们所想的对象调用。

传递消息和调用方法之间的一个重要区别是,消息没有返回值,通过发送消息,actor将工作委托给另一个actor。正如我们在调用堆栈时那样,如果期望一个返回值,则发送方会被阻塞,或者和被调用的actor在同一线程上。相反,actor接收者在回复的消息中带着返回结果。更多参见actor架构与示例