1.FutureTask
首先我们拿一段简单的 Demo 来看一下效果:
public class FutureTaskDemo {
public static void main(String[] args) throws InterruptedException, ExecutionException{
Callable<String> c=new Callable<String>() {
@Override
public String call() throws Exception {
// TODO Auto-generated method stub
System.out.println("模拟业务执行");
Thread.sleep(2000L);
System.out.println("模拟业务结束,准备返回数据...");
return "Hello World";
}
};
FutureTask<String> ft=new FutureTask<>(c);
Thread t=new Thread(ft);
t.start();
System.out.println(ft.get());
}
}
从 Demo 中我们可以看到 FutureTask 实例可以放在线程中执行,那么 FutureTask 类一定是实现了 Runnable 接口或者是继承了 Thread 方法;另外在创建 FutureTask 的时候构造函数传递进去了一个 Callable 接口的实例对象,Callable 接口提供了一个 call 方法,我们可以把主要的业务逻辑放在 call 方法里面;最后可以通过 FutureTask 实例对象获得线程的执行结果(这个返回结果的过程是通过 FutureTask 的 get 方法获得,而 get 方法是一个阻塞返回结果的方法)。所以从上面的 Demo 可以看到的是,FutureTask 是一个可以返回线程执行结果的、实现了 Runnable 接口的类。
接下来根据上面的描述,我们来手写一个 FutureTask:就叫 MyFutureTask 吧。首先 MyFutureTask 需要实现 Runnable 接口,并且需要实现里面的 run 方法;然后有一个 Callable 接口的实例字段来存储主要的逻辑,并且将 Callable 实例的 call 方法放到 run 方法中执行,这样就可以在线程中执行我们的业务逻辑了;result 用来返回 Callable 的执行结果,而提供返回执行结果,就必要要等待业务逻辑执行完才可以正确返回结果,因此还需要一个表示 MyFutureTask 执行状态的字段 state;如果有线程需要获取 MyFutureTask 实例的执行结果,但是当前 MyFutureTask 并没有执行结束,即 MyFutureTask 的状态 state 并没有变成自定义的 end 状态,因此线程需要有一个阻塞的过程,阻塞那么就自然而然想到要有阻塞队列来存储所有等待结果的线程,在结果出来的时候通知他们;最后想要阻塞或者锁住一个线程的执行,我们有几种方法:wait/notify(配合同步关键字),LockSupport 的 park/unpark(JDK 源码中大量使用的方式),Lock 的 lock/unlock。
public class MyFutureTask<T> implements Runnable {
Callable<T> callable;
// callable执行结果
T result;
// task执行状态
String state = "new";
/**
* 存储正在等待结果的消费者
*/
LinkedBlockingQueue<Thread> waiters = new LinkedBlockingQueue<>();
public MyFutureTask(Callable<T> callable) {
this.callable = callable;
}
@Override
public void run() {
try {
result = callable.call();
} catch (Exception e) {
e.printStackTrace();
} finally {
state = "end";
}
// unpark通知消费者
System.out.println(Thread.currentThread().getName() + " 生产者执行结束,通知消费者");
while (true) {
Thread waiter = waiters.poll();
if (waiter == null) {
break;
}
LockSupport.unpark(waiter);
}
}
// park / unpark
public T get() throws Exception {
Thread mainThread = Thread.currentThread();
waiters.add(mainThread); // 塞入等待的集合中
// 判断状态
System.out.println(Thread.currentThread().getName() + " 消费者进入等待");
while (!"end".equals(state)) {
LockSupport.park(mainThread);
}
return result;
}
}
最后我们上源代码来解读一下:首先看一下字段信息,有 callable 对象,有 waiters 阻塞线程链表,有 outcome 表示执行结果的对象
有状态 state并且有七种状态,状态之间的转换关系如下
接下来是 run 方法:
最后是获取结果的 get 方法:
调用的 awaitDone 方法是在等待 FutureTask 的 run 方法执行结束,注意划线的几个地方,第418行,将线程放入阻塞链表;第 426 和429 行将当前线程阻塞住。
2.ForkJoin
转自链接:https://www.cnblogs.com/cjsblog/p/9078341.html
从JDK1.7开始,Java提供Fork/Join框架用于并行执行任务,它的思想就是讲一个大任务分割成若干小任务,最终汇总每个小任务的结果得到这个大任务的结果。
这种思想和MapReduce很像(input --> split --> map --> reduce --> output)
主要有两步:
- 第一、任务切分;
- 第二、结果合并
它的模型大致是这样的:线程池中的每个线程都有自己的工作队列(PS:这一点和ThreadPoolExecutor不同,ThreadPoolExecutor是所有线程公用一个工作队列,所有线程都从这个工作队列中取任务),当自己队列中的任务都完成以后,会从其它线程的工作队列中偷一个任务执行,这样可以充分利用资源。
工作窃取(work-stealing)
工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行。工作窃取的运行流程图如下:
那么为什么需要使用工作窃取算法呢?
假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务。但是有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。
工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。
API介绍
ForkJoinPool
An ExecutorService for running ForkJoinTasks.
A ForkJoinPool differs from other kinds of ExecutorService mainly by virtue of employing work-stealing: all threads in the pool attempt to find and execute tasks submitted to the pool and/or created by other active tasks (eventually blocking waiting for work if none exist). This enables efficient processing when most tasks spawn other subtasks (as do most ForkJoinTasks), as well as when many small tasks are submitted to the pool from external clients. Especially when setting asyncMode to true in constructors, ForkJoinPools may also be appropriate for use with event-style tasks that are never joined.
ForkJoinPool与其它的ExecutorService区别主要在于它使用“工作窃取”:线程池中的所有线程都企图找到并执行提交给线程池的任务。当大量的任务产生子任务的时候,或者同时当有许多小任务被提交到线程池中的时候,这种处理是非常高效的。特别的,当在构造方法中设置asyncMode为true的时候这种处理更加高效。
ForkJoinTask
ForkJoinTask代表运行在ForkJoinPool中的任务。
主要方法:
- fork() 在当前线程运行的线程池中安排一个异步执行。简单的理解就是再创建一个子任务。
- join() 当任务完成的时候返回计算结果。
- invoke() 开始执行任务,如果必要,等待计算完成。
子类:
- RecursiveAction 一个递归无结果的ForkJoinTask(没有返回值)
- RecursiveTask 一个递归有结果的ForkJoinTask(有返回值)
ForkJoinWorkerThread
A thread managed by a ForkJoinPool, which executes ForkJoinTasks.
ForkJoinWorkerThread代表ForkJoinPool线程池中的一个执行任务的线程。
代码分析
接下来,简略的看一下关键代码来加深对Fork/Join的理解。
ForkJoinPool
WorkQueue是一个ForkJoinPool中的内部类,它是线程池中线程的工作队列的一个封装,支持任务窃取。
什么叫线程的任务窃取呢?就是说你和你的一个伙伴一起吃水果,你的那份吃完了,他那份没吃完,那你就偷偷的拿了他的一些水果吃了。存在执行2个任务的子线程,这里要讲成存在A,B两个个WorkQueue在执行任务,A的任务执行完了,B的任务没执行完,那么A的WorkQueue就从B的WorkQueue的ForkJoinTask数组中拿走了一部分尾部的任务来执行,可以合理的提高运行和计算效率。
submit()
可以看到:
- 同样是提交任务,submit会返回ForkJoinTask,而execute不会
- 任务提交给线程池以后,会将这个任务加入到当前提交者的任务队列中。
前面我们说过,每个线程都有一个WorkQueue,而WorkQueue中有执行任务的线程(ForkJoinWorkerThread owner),还有这个线程需要处理的任务(ForkJoinTask<?>[] array)。那么这个新提交的任务就是加到array中。
ForkJoinWorkerThread
从代码中我们可以清楚地看到,ForkJoinWorkThread持有ForkJoinPool和ForkJoinPool.WorkQueue的引用,以表明该线程属于哪个线程池,它的工作队列是哪个
ForkJoinTask
fork()
可以看到,如果是ForkJoinWorkerThread运行过程中fork(),则直接加入到它的工作队列中,否则,重新提交任务。
join()和invoke()
可以看到它们都会等待计算完成
图形化处理过程
接下來是一个 Demo 的演示,计算斐波拉切数列的值:
public class RecursiveTaskDemo {
private static class Fibonacci extends RecursiveTask<Integer> {
final int n;
public Fibonacci(int n) {
this.n = n;
}
@Override
protected Integer compute() {
if (n <= 1) {
return n;
}else {
//斐波拉切数列 1,1,2,3,5,8,13...
//满足这样的关系:f(n)=f(n-1)+f(n-2)
Fibonacci f1 = new Fibonacci(n - 1);
f1.fork();
Fibonacci f2 = new Fibonacci(n - 2);
f2.fork();
return f2.join() + f1.join();
}
}
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
ForkJoinPool pool = new ForkJoinPool();
Future<Integer> future = pool.submit(new Fibonacci(10));
System.out.println(future.get());
pool.shutdown();
}
}
3.CountDownLatch
先看一下 CountDownLatch 的类结构图:可以看到 CountDownLatch 也是基于 AQS 实现的。CountDownLatch 的作用是计数器,即在创建对象的时候指定计数的值,然后后面通过 countDown() 方法修改这个计数值,直到计数值变成 0 。
再看一下 CountDownLatch 里面的 AQS 子类是如何实现的:这里其实是把构造函数中传递过来的 count 作为 Sync 的 state 属性值,而 CountDownLatch 的 await() 方法就是通过判断 tryAcquireShared 是否返回正数来判断计数器是否更新到 0 从而决定是否需要阻塞当前线程;而 countDown() 方法就是不断的在更新 state 的值直到将其更新到 0。
附上一段 Demo:
public class CountDownLatchDemo {
static ExecutorService threadPool=Executors.newCachedThreadPool();
public static void main(String[] args){
MyCountDownLatch2 cdl=new MyCountDownLatch2(2);
ArrayList<String> al=new ArrayList<String>();
long currentTime=System.currentTimeMillis();
threadPool.submit(()->{
try{
Thread.sleep(2000L);
al.add("111");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
cdl.countDown();
}
});
threadPool.submit(()->{
try{
Thread.sleep(3000L);
al.add("222");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
cdl.countDown();
}
});
cdl.await();
System.out.println("程序经过:"+ (System.currentTimeMillis()-currentTime) +"s 获取到结果");
threadPool.shutdown();
}
}
4.CyclicBarrier
我们以一段 Demo 来呈现 CyclicBarrier 的作用:以下代码中定义了一个 CyclicBarrier 的实例 barrier,并且传递了两个参数,一个是 4,表示需要达到的线程等待数量;另外一个是一个 Runnable 的匿名内部类,用来表示当达到 4 个线程处于等待的条件后执行的方法。barrier 的 await() 方法就是用来阻塞当前线程,等到一定数量后执行 barrier 的 run() 方法。
//满足多少个线程执行 barrier 的 await 方法后,才会执行 barrier 定义的方法
//await
//barrierAction 就是定义 CyclicBarrier 实例时传递的 Runnable 对象
public class CyclicBarrierDemo {
public static void main(String[] args) throws InterruptedException {
LinkedBlockingQueue<String> sqls = new LinkedBlockingQueue<>();
// 任务1+2+3...1000 拆分为100个任务(1+..10, 11+20) -> 100线程去处理。
// 每当有4个线程处于await状态的时候,则会触发barrierAction执行
CyclicBarrier barrier = new CyclicBarrier(4, new Runnable() {
@Override
public void run() {
// 这是每满足4次数据库操作,就触发一次批量执行
System.out.println("有4个线程执行了,开始批量插入: " + Thread.currentThread());
for (int i = 0; i < 4; i++) {
System.out.println(sqls.poll());
}
}
});
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
sqls.add("data - " + Thread.currentThread()); // 缓存起来
Thread.sleep(1000L); // 模拟数据库操作耗时
barrier.await(); // 等待栅栏打开,有4个线程都执行到这段代码的时候,才会继续往下执行
System.out.println(Thread.currentThread() + "插入完毕");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
Thread.sleep(2000);
}
}
5.Semaphore
还是以一段 Demo 来了解一下 Semaphore 的功能:我们可以把信号量的概念理解成洗澡堂里面的储物柜,如果储物柜都被占用了,那么其他需要洗澡的人就需要等待洗澡堂里面的人出来。在下面的 Demo 中我们定义了一个信号量并且设定它的储物柜数量为 5,也就是说当前最多只允许 5 个人同时在里面洗澡,每当有人进来洗澡的时候都会调用 acquire 方法尝试获取一个储物柜的钥匙,如果获取不到则无法进去洗澡,直到有人洗澡结束通过 release 方法把钥匙交回给前台。信号量也可以理解成是一种资源,这个资源是可重复使用的资源,但是资源的数量有限,每次想要获得资源需要调用 acquire 方法, 使用完释放资源需要调用 release 方法。
public class SemaphoreDemo {
public static void main(String[] args) {
SemaphoreDemo semaphoreTest = new SemaphoreDemo();
int N = 9; // 客人数量
Semaphore semaphore = new Semaphore(5); // 手牌数量,限制请求数量
for (int i = 0; i < N; i++) {
String vipNo = "vip-00" + i;
new Thread(() -> {
try {
semaphore.acquire(); // 获取令牌
semaphoreTest.service(vipNo);
semaphore.release(); // 释放令牌
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
// 限流 控制5个线程 同时访问
public void service(String vipNo) throws InterruptedException {
System.out.println("楼上出来迎接贵宾一位,贵宾编号" + vipNo + ",...");
Thread.sleep(new Random().nextInt(3000));
System.out.println("欢送贵宾出门,贵宾编号" + vipNo);
}
}
接下来在代码里面看下具体实现过程:首先看一下 Semaphore 的定义,可以看到,依然是使用的 AQS 机制来实现的。
我们具体看一下 acquire 方法的实现:
如果 tryAcquireShared 方法返回的值小于 0,那么就将当前线程加入到等待队列中:
死循环加上 CAS 形成自旋锁,即一个线程在尝试做 CAS 的时候,如果成功了就返回结果,如果失败了就不断的尝试。如果经过计算,发现 remaining 即剩余的资源数量小于 0 那么就返回给上一层调用方法从而使当前线程进入等待队列。
既然这么多场合下面都使用到了 AQS 机制,接下来呈现一个简化版的 AQS 实现 MyAbstractQueueSynchronizer:
/**
* 自定义的抽象队列同步器
* 提供了 acquire,release 的实现,主要业务逻辑 tryAcquire,tryRelease 留着给子类实现
*
*/
public class MyAbstractQueueSynchronizer {
public volatile AtomicInteger state=new AtomicInteger(0);
public AtomicInteger getState() {
return state;
}
public void setState(AtomicInteger state) {
this.state = state;
}
//原子引用类,可以提供 CAS 操作
public AtomicReference<Thread> owner=new AtomicReference<>();
//等待队列
public LinkedBlockingQueue<Thread> waiters=new LinkedBlockingQueue<>();
public boolean tryAcquire(){
throw new UnsupportedOperationException();
}
public void acquire() {
// TODO Auto-generated method stub
//使用 while 的原因是不断的尝试去获取锁,如果没有获取到锁的线程就挂起,等待下一轮争抢;如果获取到锁的线程就跳出循环,继续执行业务逻辑
while(!tryAcquire()){
//如果没有获取到锁资源,那么就将当前线程挂起
waiters.offer(Thread.currentThread());
LockSupport.park();
//当原先拥有锁资源线程释放锁之后,所有在等待队列中的线程都被唤醒,唤醒之后需要将等待队列中的自己移除掉,从而进行下一轮的争抢
waiters.remove(Thread.currentThread());
}
}
public boolean tryRelease(){
throw new UnsupportedOperationException();
}
public void release() {
// TODO Auto-generated method stub
//为了保证非锁的拥有者也可以释放锁,这里使用 CAS 机制,如果 owner 的期望值跟当前线程不一样,那么就释放不成功
//释放锁之后,还需要通知所有在等待队列中的线程开始新一轮的争抢锁
if(tryRelease()){
Iterator<Thread> iter=waiters.iterator();
while(iter.hasNext()){
Thread next=iter.next();
LockSupport.unpark(next);
}
}
}
public int tryAcquireShared(){
throw new UnsupportedOperationException();
}
//共享锁的实现其实就是比对锁资源的个数还剩下多少
public void acquireShared(){
boolean addQueue=true; //表示是否要添加到等待队列中
while(tryAcquireShared()<0){ //如果没有获取到的可用资源,就放到等待队列中并继续尝试获取锁,如果依旧没有获取到就挂起
if(addQueue){
waiters.offer(Thread.currentThread());
addQueue=false;
}else{
LockSupport.park();
}
}
//当线程获取到锁之后,就将自己从等待队列中移除
waiters.remove(Thread.currentThread());
}
public boolean tryReleaseShared(){
throw new UnsupportedOperationException();
}
public void releaseShared() {
// TODO Auto-generated method stub
//为了保证非锁的拥有者也可以释放锁,这里使用 CAS 机制,如果 owner 的期望值跟当前线程不一样,那么就释放不成功
//释放锁之后,还需要通知所有在等待队列中的线程开始新一轮的争抢锁
if(tryReleaseShared()){
Iterator<Thread> iter=waiters.iterator();
while(iter.hasNext()){
Thread next=iter.next();
LockSupport.unpark(next);
}
}
}
}
以上如有不足和理解错误之处,还请各位大神指教,谢谢。