Java 多线程并发编程详解及CompletionService、Future使用技巧

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ZYC88888/article/details/87706725

知识体系图:

1、线程是什么?

线程是进程中独立运行的子任务。

2、创建线程的方式

方式一:将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法

方式二:声明实现 Runnable 接口的类。该类然后实现 run 方法

推荐方式二,因为接口方式比继承方式更灵活,也减少程序间的耦合。

3、获取当前线程信息?

Thread.currentThread()

4、线程的分类

线程分为守护线程、用户线程。线程初始化默认为用户线程。

setDaemon(true) 将该线程标记为守护线程或用户线程。

特性:设置守护线程,会作为进程的守护者,如果进程内没有其他非守护线程,那么守护线程也会被销毁,即使可能线程内没有运行结束。

5、线程间的关系?

某线程a 中启动另外一个线程 t,那么我们称 线程 t是 线程a 的一个子线程,而 线程a 是 线程t 的 父线程。

最典型的就是我们在main方法中 启动 一个 线程去执行。其中main方法隐含的main线程为父线程。

6、线程API一览:如何启动、停止、暂停、恢复线程?

(1)start() 使线程处于就绪状态,Java虚拟机会调用该线程的run方法;

(2)stop() 停止线程,已过时,存在不安全性:

一是可能请理性的工作得不得完成;

二是可能对锁定的对象进行“解锁”,导致数据不同步不一致的情况。

推荐 使用 interrupt() +抛异常 中断线程。

(3)suspend() 暂停线程,已过时。

resume() 恢复线程,已过时。

suspend 与resume 不建议使用,存在缺陷:

一是可能独占同步对象;

二是导致数据不一致。

(4)yield() 放弃当前线程的CPU资源。放弃时间不确认,也有可能刚刚放弃又获得CPU资源。

(5)t.join() 等待该线程t 销毁终止。

7、synchronized关键字用法

一 原子性(互斥性):实现多线程的同步机制,使得锁内代码的运行必需先获得对应的锁,运行完后自动释放对应的锁。

二 内存可见性:在同一锁情况下,synchronized锁内代码保证变量的可见性。

三 可重入性:当一个线程获取一个对象的锁,再次请求该对象的锁时是可以再次获取该对象的锁的。

如果在synchronized锁内发生异常,锁会被释放。

总结:

(1)synchronized方法 与 synchronized(this) 代码块 锁定的都是当前对象,不同的只是同步代码的范围

(2)synchronized (非this对象x) 将对象x本身作为“对象监视器”:

a、多个线程同时执行 synchronized(x) 代码块,呈现同步效果。

b、当其他线程同时执行对象x里面的 synchronized方法时,呈现同步效果。

c、当其他线程同时执行对象x里面的 synchronized(this)方法时,呈现同步效果。

(3)静态synchronized方法 与 synchronized(calss)代码块 锁定的都是Class锁。Class 锁与 对象锁 不是同一个锁,两者同时使用情况可能呈异步效果。

(4)尽量不使用 synchronized(string),是因为string的实际锁为string的常量池对象,多个值相同的string对象可能持有同一个锁。

8、volatile关键字用法

一 内存可见性:保证变量的可见性,线程在每次使用变量的时候,都会读取变量修改后的最的值。

二 不保证原子性。

9、线程间的通信方式

线程间通信的方式主要为共享内存、线程同步。

线程同步除了synchronized互斥同步外,也可以使用wait/notify实现等待、通知的机制。

(1)wait/notify属于Object类的方法,但wait和notify方法调用,必须获取对象的对象级别锁,即synchronized同步方法或同步块中使用。

(2)wait()方法:在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,或者其他某个线程中断当前线程,导致当前线程一直阻塞等待。等同wait(0)方法。

wait(long timeout) 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量前,导致当前线程等待。 单位为毫秒。

void wait(long timeout, int nanos) 与 wait(long timeout) 不同的是增加了额外的纳秒级别,更精细的等待时间控制。

(3)notfiy方法:唤醒在此对象监视器上等待的单个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。

(4)notifyAll方法:唤醒在此对象监视器上等待的所有线程。

需要:wait被执行后,会自动释放锁,而notify被执行后,锁没有立刻释放,由synchronized同步块结束时释放。

应用场景:简单的生产、消费问题。

1

2

3

4

5

6

7

8

synchronized (lock) {//获取到对象锁lock

    try {

        lock.wait();//等待通信信号, 释放对象锁lock

    } catch (InterruptedException e) {

        e.printStackTrace();

    }

    //接到通信信号

}

1

2

3

4

5

synchronized (lock) {//获取到对象锁lock

    lock.notify();//通知并唤醒某个正等待的线程

    //其他操作

}

//释放对象锁lock

10、ThreadLocal与InheritableThreadLocal

让每个线程都有自己独立的共享变量,有两种方式:

一 该实例变量封存在线程类内部;如果该实例变量(非static)是引用类型,存在可能逸出的情况。

二 就是使用ThreadLocal在任意地方构建变量,即使是静态的(static)。具有很好的隔离性。

(1)重写initialValue()方法: 初始化ThreadLocal变量,解决get()返回null问题(

(2)InheritableThreadLocal 子线程可以读取父线程的值,但反之不行

11、ReentrantLock的使用

一个简单的示例:

1

2

3

4

5

6

7

8

9

private java.util.concurrent.locks.Lock lock = new ReentrantLock();

public void method() {

    try {

        lock.lock();

        //获取到锁lock,同步块

    } finally {

        lock.unlock();//释放锁lock

    }

}

ReentrantLock 比 synchronized 功能更强大,主要体现:

(1)ReentrantLock 具有公平策略的选择。

(2)ReentrantLock 可以在获取锁的时候,可有条件性地获取,可以设置等待时间,很有效地避免死锁。

如 tryLock() 和 tryLock(long timeout, TimeUnit unit)

(3)ReentrantLock 可以获取锁的各种信息,用于监控锁的各种状态。

(4)ReentrantLock 可以灵活实现多路通知,即Condition的运用。

————————————————————————————–

一、公平锁与非公平锁

ReentrantLock 默认是非公平锁,允许线程“抢占插队”获取锁。公平锁则是线程依照请求的顺序获取锁,近似FIFO的策略方式。

二、锁的使用:

(1)lock() 阻塞式地获取锁,只有在获取到锁后才处理interrupt信息

(2)lockInterruptibly() 阻塞式地获取锁,立即处理interrupt信息,并抛出异常

(3)tryLock() 尝试获取锁,不管成功失败,都立即返回true、false,注意的是即使已将此锁设置为使用公平排序策略,tryLock()仍然可以打开公平性去插队抢占。如果希望遵守此锁的公平设置,则使用 tryLock(0, TimeUnit.SECONDS),它几乎是等效的(也检测中断)。

(4)tryLock(long timeout, TimeUnit unit)在timeout时间内阻塞式地获取锁,成功返回true,超时返回false,同时立即处理interrupt信息,并抛出异常。

如果想使用一个允许闯入公平锁的定时 tryLock,那么可以将定时形式和不定时形式组合在一起:

if (lock.tryLock() || lock.tryLock(timeout, unit) ) { … }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

private java.util.concurrent.locks.ReentrantLock lock = new ReentrantLock();

public void testMethod() {

    try {

        if (lock.tryLock(1, TimeUnit.SECONDS)) {

            //获取到锁lock,同步块

        } else {

            //没有获取到锁lock

        }

    } catch (InterruptedException e) {

        e.printStackTrace();

    } finally {

        if (lock.isHeldByCurrentThread())//如果当前线程持有锁lock,则释放锁lock

            lock.unlock();

    }

}

}

三、条件Condition的使用

条件Condition可以由锁lock来创建,实现多路通知的机制。

具有await、signal、signalAll的方法,与wait/notify类似,需要在获取锁后方能调用。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

private final java.util.concurrent.locks.Lock lock = new ReentrantLock();

private final java.util.concurrent.locks.Condition condition = lock.newCondition();

public void await() {

    try {

        lock.lock();

        //获取到锁lock

        condition.await();//等待condition通信信号,释放condition锁

        //接到condition通信

    } catch (InterruptedException e) {

        e.printStackTrace();

    } finally {

        lock.unlock();//释放对象锁lock

    }

}

12、ReentrantReadWriteLock的使用

ReentrantReadWriteLock是对ReentrantLock 更进一步的扩展,实现了读锁readLock()(共享锁)和写锁writeLock()(独占锁),实现读写分离。读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。

读锁示例:

1

2

3

4

5

6

7

8

9

private final java.util.concurrent.locks.ReadWriteLock lock = new ReentrantReadWriteLock();

public void method() {

    try {

        lock.readLock().lock();

        //获取到读锁readLock,同步块

    } finally {

        lock.readLock().unlock();//释放读锁readLock

    }

}

写锁示例:

1

2

3

4

5

6

7

8

9

private final java.util.concurrent.locks.ReadWriteLock lock = new ReentrantReadWriteLock();

public void method() {

    try {

        lock.writeLock().lock();

        //获取到写锁writeLock,同步块

    } finally {

        lock.writeLock().unlock();//释放写锁writeLock

    }

}

13、同步容器与异步容器概览

(1)同步容器

包括两部分:

一个是早期JDK的Vector、Hashtable;

一个是它们的同系容器,JDK1.2加入的同步包装类,使用Collections.synchronizedXxx工厂方法创建。

1

Map<String, Integer> hashmapSync = Collections.synchronizedMap(new HashMap<String, Integer>());

同步容器都是线程安全的,一次只有一个线程访问容器的状态。

但在某些场景下可能需要加锁来保护复合操作。

复合类操作如:新增、删除、迭代、跳转以及条件运算。

这些复合操作在多线程并发的修改容器时,可能会表现出意外的行为,

最经典的便是ConcurrentModificationException,

原因是当容器迭代的过程中,被并发的修改了内容,这是由于早期迭代器设计的时候并没有考虑并发修改的问题。

其底层的机制无非就是用传统的synchronized关键字对每个公用的方法都进行同步,使得每次只能有一个线程访问容器的状态。这很明显不满足我们今天互联网时代高并发的需求,在保证线程安全的同时,也必须有足够好的性能。

(2)并发容器

与Collections.synchronizedXxx()同步容器等相比,util.concurrent中引入的并发容器主要解决了两个问题:

1)根据具体场景进行设计,尽量避免synchronized,提供并发性。

2)定义了一些并发安全的复合操作,并且保证并发环境下的迭代操作不会出错。

util.concurrent中容器在迭代时,可以不封装在synchronized中,可以保证不抛异常,但是未必每次看到的都是”最新的、当前的”数据。

1

Map<String, Integer> concurrentHashMap = new ConcurrentHashMap<String, Integer>()

ConcurrentHashMap 替代同步的Map即(Collections.synchronized(new HashMap()))。众所周知,HashMap是根据散列值分段存储的,同步Map在同步的时候会锁住整个Map,而ConcurrentHashMap在设计存储的时候引入了段落Segment定义,同步的时候只需要锁住根据散列值锁住了散列值所在的段落即可,大幅度提升了性能。ConcurrentHashMap也增加了对常用复合操作的支持,比如”若没有则添加”:putIfAbsent(),替换:replace()。这2个操作都是原子操作。注意的是ConcurrentHashMap 弱化了size()和isEmpty()方法,并发情况尽量少用,避免导致可能的加锁(当然也可能不加锁获得值,如果map数量没有变化的话)。

CopyOnWriteArrayList和CopyOnWriteArraySet分别代替List和Set,主要是在遍历操作为主的情况下来代替同步的List和同步的Set,这也就是上面所述的思路:迭代过程要保证不出错,除了加锁,另外一种方法就是”克隆”容器对象。—缺点也明显,占有内存,且数据最终一致,但数据实时不一定一致,一般用于读多写少的并发场景。

ConcurrentSkipListMap可以在高效并发中替代SoredMap(例如用Collections.synchronzedMap包装的TreeMap)。

ConcurrentSkipListSet可以在高效并发中替代SoredSet(例如用Collections.synchronzedSet包装的TreeMap)。

ConcurrentLinkedQuerue是一个先进先出的队列。它是非阻塞队列。注意尽量用isEmpty,而不是size();

14、CountDownLatch闭锁的使用

CountDownLatch是一个同步辅助类。

通常运用场景:

(1)作为启动信号:将计数 1 初始化的 CountDownLatch 用作一个简单的开/关锁存器,或入口。

通俗描述:田径赛跑运动员等待(每位运动员为一个线程,都在await())的”发令枪”,当发令枪countDown(),喊0的时候,所有运动员跳过await()起跑线并发跑起来了。

(2)作为结束信号:在通过调用 countDown() 的线程打开入口前,所有调用 await 的线程都一直在入口处等待。用 N 初始化的 CountDownLatch 可以使一个线程在 N 个线程完成某项操作之前一直等待,或者使其在某项操作完成 N 次之前一直等待。

通俗描述:某裁判,在终点等待所有运动员都跑完,每个运动员跑完就计数一次(countDown())当为0时,就可以往下继续统计第一人到最后一个撞线的时间。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

public long timeTasks(int nThreads, final Runnable task) throws InterruptedException {

    /**

     *一个启动信号,在 driver 为继续执行 worker 做好准备之前,它会阻止所有的 worker 继续执行。

     */

    final CountDownLatch startSignal = new CountDownLatch(1);

    /**

     * 一个完成信号,它允许 driver 在完成所有 worker 之前一直等待。

     */

    final CountDownLatch doneSignal = new CountDownLatch(nThreads);

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

        Thread t = new Thread() {

            public void run() {

                try {

                    startSignal.await();/** 阻塞于此,一直到startSignal计数为0,再往下执行 */

                    try {

                        task.run();

                    } finally {

                        doneSignal.countDown();/** doneSignal 计数减一,直到最后一个线程结束 */

                    }

                } catch (InterruptedException ignored) {

                }

            }

        };

        t.start();

    }

    long start = System.currentTimeMillis();

    startSignal.countDown();/** doneSignal 计数减一,为0,所有task开始并发执行run */

    doneSignal.await();/** 阻塞于此,一直到doneSignal计数为0,再往下执行 */

    long end = System.currentTimeMillis();

    return end - start;

}

public static void main(String[] args) throws InterruptedException {

    final Runnable task = new Runnable() {

        @Override

        public void run() {

            try {

                Thread.sleep((long) (Math.random() * 1000));

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

            System.out.println(Thread.currentThread().getName() + " end");

        }

    };

    long time = new CountDownLatchTest().timeTasks(10, task);

    System.out.println("耗时:" + time + "ms");

}

更多的api:

boolean await(long timeout, TimeUnit unit) 使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断或超出了指定的等待时间。

15、CyclicBarrier关卡的使用

CyclicBarrier是一个同步辅助类。

CyclicBarrier让一个线程达到屏障时被阻塞,直到最后一个线程达到屏障时,屏障才会开门,所有被屏障拦截的线程才会继续执行

CyclicBarrier(int parties, Runnable barrierAction)构造函数,用于在所有线程都到达屏障后优先执行barrierAction的run()方法

使用场景:

可以用于多线程计算以后,最后使用合并计算结果的场景;

通俗描述:某裁判,在终点(await()阻塞处)等待所有运动员都跑完,所有人都跑完就可以做吃炸鸡啤酒(barrierAction),但是只要一个人没跑完就都不能吃炸鸡啤酒,当然也没规定他们同时跑(当然也可以,一起使用CountDownLatch)。

————————————————————————————–

CyclicBarrier与CountDownLatch的区别:

CountDownLatch强调的是一个线程等待多个线程完成某件事,只能用一次,无法重置;

CyclicBarrier强调的是多个线程互相等待完成,才去做某个事情,可以重置。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

public static class WorkerThread implements Runnable {

    private final CyclicBarrier cyclicBarrier;

    public WorkerThread(CyclicBarrier cyclicBarrier) {

        this.cyclicBarrier = cyclicBarrier;

    }

    @Override

    public void run() {

        try {

            System.out.println(Thread.currentThread().getName() + " pre-working");

            /**

             * 线程在这里等待,直到所有线程都到达barrier。

             */

            cyclicBarrier.await();

            System.out.println(Thread.currentThread().getName() + " working");

        } catch (Exception e) {

            e.printStackTrace();

        }

    }

}

public static void main(String[] args) {

    int THREAD_NUM = 5;

    final CyclicBarrier cyclicBarrier = new CyclicBarrier(THREAD_NUM, new Runnable() {

        /**

         * 当所有线程到达barrier时执行

         */

        @Override

        public void run() {

            System.out.println("--------------Inside Barrier--------------");

        }

    });

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

        new Thread(new WorkerThread(cyclicBarrier)).start();

    }

}

更多api:

int await(long timeout, TimeUnit unit) 在所有参与者都已经在此屏障上调用 await 方法之前将一直等待,或者超出了指定的等待时间。

16、Semaphore信号量的使用

Semaphore信号量是一个计数信号量。

可以认为,Semaphore维护一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。

通俗描述:某个车库只有N个车位,车主们要泊车,请向车库保安处阻塞 acquire()等待获取许可证,当获得许可证,车主们才可以去泊车。当某个车主离开车位的时候,交还许可证release() ,从而其他阻塞等待的车主有机会获得许可证。

另外:

Semaphore 默认是非公平策略,允许线程“抢占插队”获取许可证。公平策略则是线程依照请求的顺序获取许可证,近似FIFO的策略方式。

17、Executors框架(线程池)的使用

(1)线程池是什么?

线程池是一种多线程的处理方式,利用已有线程对象继续服务新的任务(按照一定的执行策略),而不是频繁地创建销毁线程对象,由此提供服务的吞吐能力,减少CPU的闲置时间。具体组成部分包括:

a、线程池管理器(ThreadPool)用于创建和管理线程池,包括创建线程池、销毁线程池,添加新任务。

b、工作线程(Worker)线程池中的线程,闲置的时候处于等待状态,可以循环回收利用。

c、任务接口(Task)每个任务必须实现的接口类,为工作线程提供调用,主要规定了任务的入口、任务完成的收尾工作、任务的状态。

d、等待队列(Queue)存放等待处理的任务,提供缓冲机制。

(2)Executors框架常见的执行策略

Executors框架提供了一些便利的执行策略。

1

java.util.concurrent.ExecutorService service = java.util.concurrent.Executors.newFixedThreadPool(100);

- newSingleThreadExecutor:创建一个单线程的线程池。
这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
- newFixedThreadPool:创建固定大小的线程池。
每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
- newCachedThreadPool:创建一个可缓存的线程池。
如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
- newScheduledThreadPool:创建一个大小无限的线程池。
此线程池支持定时以及周期性执行任务的需求。
- newSingleThreadScheduledExecutor:创建一个单线程的线程池。
此线程池支持定时以及周期性执行任务的需求。

(3)ExecutorService线程池管理

ExecutorService的生命周期有3个状态:运行、关闭(shutting down)、停止。

提交任务submit(xxx)扩展了基本方法 Executor.execute(java.lang.Runnable)。

<T> Future<T> submit(Callable<T> task) 提交一个返回值的任务用于执行,返回一个表示任务的未决结果的 Future。

Future<?> submit(Runnable task) 提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。

<T> Future<T> submit(Runnable task, T result) 提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。

shutdown() 启动一次顺序关闭,执行以前提交的任务,但不接受新任务。

List<Runnable> shutdownNow() 试图停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表。

一个简单的示例:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

public static void main(String[] args) {

    ExecutorService executorService = Executors.newFixedThreadPool(10);

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

        executorService.submit(new Runnable() {

            @Override

            public void run() {

                System.out.println("哈哈");

            }

        });

    }

    /**

     * 如果不再需要新任务,请适当关闭executorService并拒绝新任务

     */

    executorService.shutdown();

}

(3)ThreadPoolExecutor机制

ThreadPoolExecutor为Executors的线程池内部实现类。

构造函数详解:

ThreadPoolExecutor线程池管理机制:

1.当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。

2.当线程池达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行

3.当workQueue已满,且maximumPoolSize>corePoolSize时,新提交任务会创建新线程执行任务

4.当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理

5.当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程

6.当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭

一个简单的示例:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

public static void main(String[] args) {

    java.util.concurrent.ThreadPoolExecutor threadPoolExecutor =

        new ThreadPoolExecutor(10, //corePoolSize 核心线程数

                    100, //maximumPoolSize 最大线程数

                    30, //keepAliveTime 线程池中超过corePoolSize数目的空闲线程最大存活时间;

                        // TimeUnit keepAliveTime时间单位

                    TimeUnit.SECONDS,

                        //workQueue 阻塞任务队列

                     new LinkedBlockingQueue<Runnable>(1000),

                        //threadFactory 新建线程的工厂

                    Executors.defaultThreadFactory(),

                        //RejectedExecutionHandler当提交任务数超过maxmumPoolSize+workQueue之和时,

                        // 任务会交给RejectedExecutionHandler来处理

                    new ThreadPoolExecutor.AbortPolicy()                       

    );

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

        threadPoolExecutor.submit(new Runnable() {

            @Override

            public void run() {

                System.out.println("哈哈");

            }

        });

    }

    /**

     * 如果不再需要新任务,请适当关闭threadPoolExecutor并拒绝新任务

     */

    threadPoolExecutor.shutdown();

}

18、可携带结果的任务Callable 和 Future / FutureTask

(1)为解决Runnable接口不能返回一个值或受检查的异常,可以采用Callable接口实现一个任务。

1

2

3

4

5

6

7

8

9

public interface Callable<V> { 

    /**

     * Computes a result, or throws an exception if unable to do so.

     *

     * @return computed result

     * @throws Exception if unable to compute a result

     */ 

    V call() throws Exception; 

}

(2)Future表示异步计算的结果,可以对于具体的Runnable或者Callable任务进行查询是否完成,查询是否取消,获取执行结果,取消任务等操作。

V get() throws InterruptedException, ExecutionException 如有必要,等待计算完成,然后获取其结果。

V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException 如有必要,最多等待为使计算完成所给定的时间之后,获取其结果(如果结果可用)。

(3)FutureTask

FutureTask则是一个RunnableFuture<V>,而RunnableFuture实现了Runnbale又实现了Futrue<V>这两个接口。

简单示例一:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

public static void main(String[] args) throws InterruptedException {

        FutureTask<Integer> future = new FutureTask<Integer>(new Callable<Integer>() {

            @Override

            public Integer call() throws Exception {

                // 返回一个值或受检查的异常

                //throw new Exception();

                return new Random().nextInt(100);

            }

        });

        new Thread(future).start();;

        /**

         * 模拟其他业务逻辑

         */

        Thread.sleep(1000);

        //Integer result = future.get(0, TimeUnit.SECONDS);

        Integer result = null;

        try {

            result = future.get();

        } catch (ExecutionException e) {

            e.printStackTrace();

        }

        System.out.println("result========" + result);

    }

简单示例二,采用Executors:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

public static void main(String[] args) throws InterruptedException {

        java.util.concurrent.ExecutorService threadPoolExecutor =

                java.util.concurrent.Executors.newCachedThreadPool();

        Future<Integer> future = threadPoolExecutor.submit(

                    new Callable<Integer>() {

            @Override

            public Integer call() throws Exception {

                // 返回一个值或受检查的异常

                //throw new Exception();

                return new Random().nextInt(100);

            }

        });

        /**

         * 如果不再需要新任务,请适当关闭threadPoolExecutor并拒绝新任务

         */

        threadPoolExecutor.shutdown();

        /**

         * 模拟其他业务逻辑

         */

        Thread.sleep(1000);

        //Integer result = future.get(0, TimeUnit.SECONDS);

        Integer result = null;

        try {

            result = future.get();

        } catch (ExecutionException e) {

            e.printStackTrace();

        }

        System.out.println("result========" + result);

    }

简单示例三,采用Executors+CompletionService:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

static class MyCallable implements Callable<Integer> {

    private final int i;

    public MyCallable(int i) {

        super();

        this.i = i;

    }

    @Override

    public Integer call() throws Exception {

        // 返回一个值或受检查的异常

        //throw new Exception();

        return new Integer(i);

    }

}

public static void main(String[] args) throws InterruptedException {

    java.util.concurrent.ExecutorService threadPoolExecutor = 

              java.util.concurrent.Executors.newCachedThreadPool();

    java.util.concurrent.CompletionService<Integer> completionService =  

              new java.util.concurrent.ExecutorCompletionService<Integer>(threadPoolExecutor);

    final int threadNum = 10;

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

        completionService.submit(new MyCallable(i + 1));

    }

    /**

     * 如果不再需要新任务,请适当关闭threadPoolExecutor并拒绝新任务

     */

    threadPoolExecutor.shutdown();

    /**

     * 模拟其他业务逻辑

     */

    Thread.sleep(2000);

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

        try {

            System.out.println("result========" + completionService.take().get());

        } catch (ExecutionException e) {

            e.printStackTrace();

        }

    }

}

注意的是提交到CompletionService中的Future是按照完成的顺序排列的,而不是按照添加的顺序排列的。

19、Atomic系列-原子变量类

其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。实际上是借助硬件的相关指令来实现的,不会阻塞线程(或者说只是在硬件级别上阻塞了)。其中的类可以分成4组

基本类:AtomicInteger、AtomicLong、AtomicBoolean;

引用类型:AtomicReference、AtomicStampedRerence、AtomicMarkableReference;–AtomicStampedReference 或者 AtomicMarkableReference 解决线程并发中,导致的ABA问题

数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray —数组长度固定不可变,但保证数组上每个元素的操作绝对安全的

属性原子修改器(Updater):AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater

Updater使用限制:

限制1:操作的目标不能是static类型,前面说到unsafe的已经可以猜测到它提取的是非static类型的属性偏移量,如果是static类型在获取时如果没有使用对应的方法是会报错的,而这个Updater并没有使用对应的方法。

限制2:操作的目标不能是final类型的,因为final根本没法修改。

限制3:必须是volatile类型的数据,也就是数据本身是读一致的。

限制4:属性必须对当前的Updater所在的区域是可见的,也就是private如果不是当前类肯定是不可见的,protected如果不存在父子关系也是不可见的,default如果不是在同一个package下也是不可见的。

简单示例:

1

2

3

4

5

static class A {

    volatile int intValue = 100;

}

private AtomicIntegerFieldUpdater<A> atomicIntegerFieldUpdater

        = AtomicIntegerFieldUpdater.newUpdater(A.class, "intValue");

20、总结

什么叫线程安全?

线程安全就是每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的。

线程安全就是说多线程访问同一代码,不会产生不确定的结果。

线程安全问题多是由全局变量和静态变量引起的,当多个线程对共享数据只执行读操作,不执行写操作时,一般是线程安全的;当多个线程都执行写操作时,需要考虑线程同步来解决线程安全问题。

什么叫线程同步?

多个线程操作一个资源的情况下,导致资源数据前后不一致。这样就需要协调线程的调度,即线程同步。 解决多个线程使用共通资源的方法是:线程操作资源时独占资源,其他线程不能访问资源。使用锁可以保证在某一代码段上只有一条线程访问共用资源。

有两种方式实现线程同步:

1、synchronized

2、同步锁(Lock)

什么叫线程通信?

有时候线程之间需要协作和通信。

有两种方式实现线程通信:

1、synchronized 实现内存可见性,满足线程共享变量

2、wait/notify\notifyAll(synchronized同步方法或同步块中使用) 实现内存可见性,及生产消费模式的相互唤醒机制

3、同步锁(Lock)的Condition(await\signal\signalAll)

4、管道,实现数据的共享,满足读写模式 

更多Demo:https://git.oschina.net/svenaugustus/MyJavaMultithreadingLab

补充线程池详解

我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?

在Java中可以通过线程池来达到这样的效果。今天我们就来详细讲解一下Java的线程池,首先我们从最核心的ThreadPoolExecutor类中的方法讲起,然后再讲述它的实现原理,接着给出了它的使用示例,最后讨论了一下如何合理配置线程池的大小。

以下是本文的目录大纲:

一.Java中的ThreadPoolExecutor类

二.深入剖析线程池实现原理

三.使用示例

四.如何合理配置线程池的大小

若有不正之处请多多谅解,并欢迎批评指正。

一.Java中的ThreadPoolExecutor类

java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类,因此如果要透彻地了解Java中的线程池,必须先了解这个类。下面我们来看一下ThreadPoolExecutor类的具体实现源码。

在ThreadPoolExecutor类中提供了四个构造方法:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

public class ThreadPoolExecutor extends AbstractExecutorService {

    .....

    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,

            BlockingQueue<Runnable> workQueue);

    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,

            BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);

    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,

            BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);

    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,

        BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);

    ...

}

从上面的代码可以得知,ThreadPoolExecutor继承了AbstractExecutorService类,并提供了四个构造器,事实上,通过观察每个构造器的源码具体实现,发现前面三个构造器都是调用的第四个构造器进行的初始化工作。

下面解释下一下构造器中各个参数的含义:

  • corePoolSize:核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
  • maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;
  • keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
  • unit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:

1

2

3

4

5

6

7

TimeUnit.DAYS;               //天

TimeUnit.HOURS;             //小时

TimeUnit.MINUTES;           //分钟

TimeUnit.SECONDS;           //秒

TimeUnit.MILLISECONDS;      //毫秒

TimeUnit.MICROSECONDS;      //微妙

TimeUnit.NANOSECONDS;       //纳秒

  • workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:

1

2

3

ArrayBlockingQueue;

LinkedBlockingQueue;

SynchronousQueue;

ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关。

  • threadFactory:线程工厂,主要用来创建线程;
  • handler:表示当拒绝处理任务时的策略,有以下四种取值:

1

2

3

4

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。

ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。

ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)

ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

具体参数的配置与线程池的关系将在下一节讲述。

从上面给出的ThreadPoolExecutor类的代码可以知道,ThreadPoolExecutor继承了AbstractExecutorService,我们来看一下AbstractExecutorService的实现:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

public abstract class AbstractExecutorService implements ExecutorService {

    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) { };

    protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) { };

    public Future<?> submit(Runnable task) {};

    public <T> Future<T> submit(Runnable task, T result) { };

    public <T> Future<T> submit(Callable<T> task) { };

    private <T> T doInvokeAny(Collection<? extends Callable<T>> tasks,

                            boolean timed, long nanos)

        throws InterruptedException, ExecutionException, TimeoutException {

    };

    public <T> T invokeAny(Collection<? extends Callable<T>> tasks)

        throws InterruptedException, ExecutionException {

    };

    public <T> T invokeAny(Collection<? extends Callable<T>> tasks,

                           long timeout, TimeUnit unit)

        throws InterruptedException, ExecutionException, TimeoutException {

    };

    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)

        throws InterruptedException {

    };

    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,

                                         long timeout, TimeUnit unit)

        throws InterruptedException {

    };

}

AbstractExecutorService是一个抽象类,它实现了ExecutorService接口。

我们接着看ExecutorService接口的实现:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

public interface ExecutorService extends Executor {

    void shutdown();

    boolean isShutdown();

    boolean isTerminated();

    boolean awaitTermination(long timeout, TimeUnit unit)

        throws InterruptedException;

    <T> Future<T> submit(Callable<T> task);

    <T> Future<T> submit(Runnable task, T result);

    Future<?> submit(Runnable task);

    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)

        throws InterruptedException;

    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,

                                  long timeout, TimeUnit unit)

        throws InterruptedException;

    <T> T invokeAny(Collection<? extends Callable<T>> tasks)

        throws InterruptedException, ExecutionException;

    <T> T invokeAny(Collection<? extends Callable<T>> tasks,

                    long timeout, TimeUnit unit)

        throws InterruptedException, ExecutionException, TimeoutException;

}

而ExecutorService又是继承了Executor接口,我们看一下Executor接口的实现:

1

2

3

public interface Executor {

    void execute(Runnable command);

}

到这里,大家应该明白了ThreadPoolExecutor、AbstractExecutorService、ExecutorService和Executor几个之间的关系了。

Executor是一个顶层接口,在它里面只声明了一个方法execute(Runnable),返回值为void,参数为Runnable类型,从字面意思可以理解,就是用来执行传进去的任务的;

然后ExecutorService接口继承了Executor接口,并声明了一些方法:submit、invokeAll、invokeAny以及shutDown等;

抽象类AbstractExecutorService实现了ExecutorService接口,基本实现了ExecutorService中声明的所有方法;

然后ThreadPoolExecutor继承了类AbstractExecutorService。

在ThreadPoolExecutor类中有几个非常重要的方法:

1

2

3

4

execute()

submit()

shutdown()

shutdownNow()

execute()方法实际上是Executor中声明的方法,在ThreadPoolExecutor进行了具体的实现,这个方法是ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行。

submit()方法是在ExecutorService中声明的方法,在AbstractExecutorService就已经有了具体的实现,在ThreadPoolExecutor中并没有对其进行重写,这个方法也是用来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行的结果,去看submit()方法的实现,会发现它实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果(Future相关内容将在下一篇讲述)。

shutdown()和shutdownNow()是用来关闭线程池的。

还有很多其他的方法:

比如:getQueue() 、getPoolSize() 、getActiveCount()、getCompletedTaskCount()等获取与线程池相关属性的方法,有兴趣的朋友可以自行查阅API。

二.深入剖析线程池实现原理

在上一节我们从宏观上介绍了ThreadPoolExecutor,下面我们来深入解析一下线程池的具体实现原理,将从下面几个方面讲解:

1.线程池状态

  2.任务的执行

  3.线程池中的线程初始化

  4.任务缓存队列及排队策略

  5.任务拒绝策略

  6.线程池的关闭

  7.线程池容量的动态调整

1.线程池状态

在ThreadPoolExecutor中定义了一个volatile变量,另外定义了几个static final变量表示线程池的各个状态:

1

2

3

4

5

volatile int runState;

static final int RUNNING    = 0;

static final int SHUTDOWN   = 1;

static final int STOP       = 2;

static final int TERMINATED = 3;

runState表示当前线程池的状态,它是一个volatile变量用来保证线程之间的可见性;

下面的几个static final变量表示runState可能的几个取值。

当创建线程池后,初始时,线程池处于RUNNING状态;

如果调用了shutdown()方法,则线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕;

如果调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务;

当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。

2.任务的执行

在了解将任务提交给线程池到任务执行完毕整个过程之前,我们先来看一下ThreadPoolExecutor类中其他的一些比较重要成员变量:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

private final BlockingQueue<Runnable> workQueue;              //任务缓存队列,用来存放等待执行的任务

private final ReentrantLock mainLock = new ReentrantLock();   //线程池的主要状态锁,对线程池状态(比如线程池大小

                                                              //、runState等)的改变都要使用这个锁

private final HashSet<Worker> workers = new HashSet<Worker>();  //用来存放工作集

private volatile long  keepAliveTime;    //线程存货时间  

private volatile boolean allowCoreThreadTimeOut;   //是否允许为核心线程设置存活时间

private volatile int   corePoolSize;     //核心池的大小(即线程池中的线程数目大于这个参数时,提交的任务会被放进任务缓存队列)

private volatile int   maximumPoolSize;   //线程池最大能容忍的线程数

private volatile int   poolSize;       //线程池中当前的线程数

private volatile RejectedExecutionHandler handler; //任务拒绝策略

private volatile ThreadFactory threadFactory;   //线程工厂,用来创建线程

private int largestPoolSize;   //用来记录线程池中曾经出现过的最大线程数

private long completedTaskCount;   //用来记录已经执行完毕的任务个数

每个变量的作用都已经标明出来了,这里要重点解释一下corePoolSize、maximumPoolSize、largestPoolSize三个变量。

corePoolSize在很多地方被翻译成核心池大小,其实我的理解这个就是线程池的大小。举个简单的例子:

假如有一个工厂,工厂里面有10个工人,每个工人同时只能做一件任务。

因此只要当10个工人中有工人是空闲的,来了任务就分配给空闲的工人做;

当10个工人都有任务在做时,如果还来了任务,就把任务进行排队等待;

如果说新任务数目增长的速度远远大于工人做任务的速度,那么此时工厂主管可能会想补救措施,比如重新招4个临时工人进来;

然后就将任务也分配给这4个临时工人做;

如果说着14个工人做任务的速度还是不够,此时工厂主管可能就要考虑不再接收新的任务或者抛弃前面的一些任务了。

当这14个工人当中有人空闲时,而新任务增长的速度又比较缓慢,工厂主管可能就考虑辞掉4个临时工了,只保持原来的10个工人,毕竟请额外的工人是要花钱的。

这个例子中的corePoolSize就是10,而maximumPoolSize就是14(10+4)。

也就是说corePoolSize就是线程池大小,maximumPoolSize在我看来是线程池的一种补救措施,即任务量突然过大时的一种补救措施。

不过为了方便理解,在本文后面还是将corePoolSize翻译成核心池大小。

largestPoolSize只是一个用来起记录作用的变量,用来记录线程池中曾经有过的最大线程数目,跟线程池的容量没有任何关系。

下面我们进入正题,看一下任务从提交到最终执行完毕经历了哪些过程。

在ThreadPoolExecutor类中,最核心的任务提交方法是execute()方法,虽然通过submit也可以提交任务,但是实际上submit方法里面最终调用的还是execute()方法,所以我们只需要研究execute()方法的实现原理即可:

1

2

3

4

5

6

7

8

9

10

11

12

public void execute(Runnable command) {

    if (command == null)

        throw new NullPointerException();

    if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {

        if (runState == RUNNING && workQueue.offer(command)) {

            if (runState != RUNNING || poolSize == 0)

                ensureQueuedTaskHandled(command);

        }

        else if (!addIfUnderMaximumPoolSize(command))

            reject(command); // is shutdown or saturated

    }

}

上面的代码可能看起来不是那么容易理解,下面我们一句一句解释:

首先,判断提交的任务command是否为null,若是null,则抛出空指针异常;

接着是这句,这句要好好理解一下:

1

if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command))

由于是或条件运算符,所以先计算前半部分的值,如果线程池中当前线程数不小于核心池大小,那么就会直接进入下面的if语句块了。

如果线程池中当前线程数小于核心池大小,则接着执行后半部分,也就是执行

1

addIfUnderCorePoolSize(command)

如果执行完addIfUnderCorePoolSize这个方法返回false,则继续执行下面的if语句块,否则整个方法就直接执行完毕了。

如果执行完addIfUnderCorePoolSize这个方法返回false,然后接着判断:

1

if (runState == RUNNING && workQueue.offer(command))

如果当前线程池处于RUNNING状态,则将任务放入任务缓存队列;如果当前线程池不处于RUNNING状态或者任务放入缓存队列失败,则执行:

1

addIfUnderMaximumPoolSize(command)

如果执行addIfUnderMaximumPoolSize方法失败,则执行reject()方法进行任务拒绝处理。

回到前面:

1

if (runState == RUNNING && workQueue.offer(command))

这句的执行,如果说当前线程池处于RUNNING状态且将任务放入任务缓存队列成功,则继续进行判断:

1

if (runState != RUNNING || poolSize == 0)

这句判断是为了防止在将此任务添加进任务缓存队列的同时其他线程突然调用shutdown或者shutdownNow方法关闭了线程池的一种应急措施。如果是这样就执行:

1

ensureQueuedTaskHandled(command)

进行应急处理,从名字可以看出是保证 添加到任务缓存队列中的任务得到处理。

我们接着看2个关键方法的实现:addIfUnderCorePoolSize和addIfUnderMaximumPoolSize:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

private boolean addIfUnderCorePoolSize(Runnable firstTask) {

    Thread t = null;

    final ReentrantLock mainLock = this.mainLock;

    mainLock.lock();

    try {

        if (poolSize < corePoolSize && runState == RUNNING)

            t = addThread(firstTask);        //创建线程去执行firstTask任务  

        } finally {

        mainLock.unlock();

    }

    if (t == null)

        return false;

    t.start();

    return true;

}

这个是addIfUnderCorePoolSize方法的具体实现,从名字可以看出它的意图就是当低于核心吃大小时执行的方法。下面看其具体实现,首先获取到锁,因为这地方涉及到线程池状态的变化,先通过if语句判断当前线程池中的线程数目是否小于核心池大小,有朋友也许会有疑问:前面在execute()方法中不是已经判断过了吗,只有线程池当前线程数目小于核心池大小才会执行addIfUnderCorePoolSize方法的,为何这地方还要继续判断?原因很简单,前面的判断过程中并没有加锁,因此可能在execute方法判断的时候poolSize小于corePoolSize,而判断完之后,在其他线程中又向线程池提交了任务,就可能导致poolSize不小于corePoolSize了,所以需要在这个地方继续判断。然后接着判断线程池的状态是否为RUNNING,原因也很简单,因为有可能在其他线程中调用了shutdown或者shutdownNow方法。然后就是执行

1

t = addThread(firstTask);

这个方法也非常关键,传进去的参数为提交的任务,返回值为Thread类型。然后接着在下面判断t是否为空,为空则表明创建线程失败(即poolSize>=corePoolSize或者runState不等于RUNNING),否则调用t.start()方法启动线程。

我们来看一下addThread方法的实现:

1

2

3

4

5

6

7

8

9

10

11

12

private Thread addThread(Runnable firstTask) {

    Worker w = new Worker(firstTask);

    Thread t = threadFactory.newThread(w);  //创建一个线程,执行任务  

    if (t != null) {

        w.thread = t;            //将创建的线程的引用赋值为w的成员变量      

        workers.add(w);

        int nt = ++poolSize;     //当前线程数加1      

        if (nt > largestPoolSize)

            largestPoolSize = nt;

    }

    return t;

}

在addThread方法中,首先用提交的任务创建了一个Worker对象,然后调用线程工厂threadFactory创建了一个新的线程t,然后将线程t的引用赋值给了Worker对象的成员变量thread,接着通过workers.add(w)将Worker对象添加到工作集当中。

下面我们看一下Worker类的实现:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

private final class Worker implements Runnable {

    private final ReentrantLock runLock = new ReentrantLock();

    private Runnable firstTask;

    volatile long completedTasks;

    Thread thread;

    Worker(Runnable firstTask) {

        this.firstTask = firstTask;

    }

    boolean isActive() {

        return runLock.isLocked();

    }

    void interruptIfIdle() {

        final ReentrantLock runLock = this.runLock;

        if (runLock.tryLock()) {

            try {

        if (thread != Thread.currentThread())

        thread.interrupt();

            } finally {

                runLock.unlock();

            }

        }

    }

    void interruptNow() {

        thread.interrupt();

    }

    private void runTask(Runnable task) {

        final ReentrantLock runLock = this.runLock;

        runLock.lock();

        try {

            if (runState < STOP &&

                Thread.interrupted() &&

                runState >= STOP)

            boolean ran = false;

            beforeExecute(thread, task);   //beforeExecute方法是ThreadPoolExecutor类的一个方法,没有具体实现,用户可以根据

            //自己需要重载这个方法和后面的afterExecute方法来进行一些统计信息,比如某个任务的执行时间等          

            try {

                task.run();

                ran = true;

                afterExecute(task, null);

                ++completedTasks;

            } catch (RuntimeException ex) {

                if (!ran)

                    afterExecute(task, ex);

                throw ex;

            }

        } finally {

            runLock.unlock();

        }

    }

    public void run() {

        try {

            Runnable task = firstTask;

            firstTask = null;

            while (task != null || (task = getTask()) != null) {

                runTask(task);

                task = null;

            }

        } finally {

            workerDone(this);   //当任务队列中没有任务时,进行清理工作      

        }

    }

}

它实际上实现了Runnable接口,因此上面的Thread t = threadFactory.newThread(w);效果跟下面这句的效果基本一样:

1

Thread t = new Thread(w);

相当于传进去了一个Runnable任务,在线程t中执行这个Runnable。

既然Worker实现了Runnable接口,那么自然最核心的方法便是run()方法了:

1

2

3

4

5

6

7

8

9

10

11

12

public void run() {

    try {

        Runnable task = firstTask;

        firstTask = null;

        while (task != null || (task = getTask()) != null) {

            runTask(task);

            task = null;

        }

    } finally {

        workerDone(this);

    }

}

从run方法的实现可以看出,它首先执行的是通过构造器传进来的任务firstTask,在调用runTask()执行完firstTask之后,在while循环里面不断通过getTask()去取新的任务来执行,那么去哪里取呢?自然是从任务缓存队列里面去取,getTask是ThreadPoolExecutor类中的方法,并不是Worker类中的方法,下面是getTask方法的实现:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

Runnable getTask() {

    for (;;) {

        try {

            int state = runState;

            if (state > SHUTDOWN)

                return null;

            Runnable r;

            if (state == SHUTDOWN)  // Help drain queue

                r = workQueue.poll();

            else if (poolSize > corePoolSize || allowCoreThreadTimeOut) //如果线程数大于核心池大小或者允许为核心池线程设置空闲时间,

                //则通过poll取任务,若等待一定的时间取不到任务,则返回null

                r = workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS);

            else

                r = workQueue.take();

            if (r != null)

                return r;

            if (workerCanExit()) {    //如果没取到任务,即r为null,则判断当前的worker是否可以退出

                if (runState >= SHUTDOWN) // Wake up others

                    interruptIdleWorkers();   //中断处于空闲状态的worker

                return null;

            }

            // Else retry

        } catch (InterruptedException ie) {

            // On interruption, re-check runState

        }

    }

}

在getTask中,先判断当前线程池状态,如果runState大于SHUTDOWN(即为STOP或者TERMINATED),则直接返回null。

如果runState为SHUTDOWN或者RUNNING,则从任务缓存队列取任务。

如果当前线程池的线程数大于核心池大小corePoolSize或者允许为核心池中的线程设置空闲存活时间,则调用poll(time,timeUnit)来取任务,这个方法会等待一定的时间,如果取不到任务就返回null。

然后判断取到的任务r是否为null,为null则通过调用workerCanExit()方法来判断当前worker是否可以退出,我们看一下workerCanExit()的实现:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

private boolean workerCanExit() {

    final ReentrantLock mainLock = this.mainLock;

    mainLock.lock();

    boolean canExit;

    //如果runState大于等于STOP,或者任务缓存队列为空了

    //或者  允许为核心池线程设置空闲存活时间并且线程池中的线程数目大于1

    try {

        canExit = runState >= STOP ||

            workQueue.isEmpty() ||

            (allowCoreThreadTimeOut &&

             poolSize > Math.max(1, corePoolSize));

    } finally {

        mainLock.unlock();

    }

    return canExit;

}

也就是说如果线程池处于STOP状态、或者任务队列已为空或者允许为核心池线程设置空闲存活时间并且线程数大于1时,允许worker退出。如果允许worker退出,则调用interruptIdleWorkers()中断处于空闲状态的worker,我们看一下interruptIdleWorkers()的实现:

1

2

3

4

5

6

7

8

9

10

void interruptIdleWorkers() {

    final ReentrantLock mainLock = this.mainLock;

    mainLock.lock();

    try {

        for (Worker w : workers)  //实际上调用的是worker的interruptIfIdle()方法

            w.interruptIfIdle();

    } finally {

        mainLock.unlock();

    }

}

从实现可以看出,它实际上调用的是worker的interruptIfIdle()方法,在worker的interruptIfIdle()方法中:

1

2

3

4

5

6

7

8

9

10

11

12

void interruptIfIdle() {

    final ReentrantLock runLock = this.runLock;

    if (runLock.tryLock()) {    //注意这里,是调用tryLock()来获取锁的,因为如果当前worker正在执行任务,锁已经被获取了,是无法获取到锁的

                                //如果成功获取了锁,说明当前worker处于空闲状态

        try {

    if (thread != Thread.currentThread()) 

    thread.interrupt();

        } finally {

            runLock.unlock();

        }

    }

}

这里有一个非常巧妙的设计方式,假如我们来设计线程池,可能会有一个任务分派线程,当发现有线程空闲时,就从任务缓存队列中取一个任务交给空闲线程执行。但是在这里,并没有采用这样的方式,因为这样会要额外地对任务分派线程进行管理,无形地会增加难度和复杂度,这里直接让执行完任务的线程去任务缓存队列里面取任务来执行。

我们再看addIfUnderMaximumPoolSize方法的实现,这个方法的实现思想和addIfUnderCorePoolSize方法的实现思想非常相似,唯一的区别在于addIfUnderMaximumPoolSize方法是在线程池中的线程数达到了核心池大小并且往任务队列中添加任务失败的情况下执行的:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

private boolean addIfUnderMaximumPoolSize(Runnable firstTask) {

    Thread t = null;

    final ReentrantLock mainLock = this.mainLock;

    mainLock.lock();

    try {

        if (poolSize < maximumPoolSize && runState == RUNNING)

            t = addThread(firstTask);

    } finally {

        mainLock.unlock();

    }

    if (t == null)

        return false;

    t.start();

    return true;

}

看到没有,其实它和addIfUnderCorePoolSize方法的实现基本一模一样,只是if语句判断条件中的poolSize < maximumPoolSize不同而已。

到这里,大部分朋友应该对任务提交给线程池之后到被执行的整个过程有了一个基本的了解,下面总结一下:

1)首先,要清楚corePoolSize和maximumPoolSize的含义;

2)其次,要知道Worker是用来起到什么作用的;

3)要知道任务提交给线程池之后的处理策略,这里总结一下主要有4点:

  • 如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;
  • 如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;
  • 如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理;
  • 如果线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。

3.线程池中的线程初始化

默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。

在实际中如果需要线程池创建之后立即创建线程,可以通过以下两个方法办到:

  • prestartCoreThread():初始化一个核心线程;
  • prestartAllCoreThreads():初始化所有核心线程

下面是这2个方法的实现:

1

2

3

4

5

6

7

8

9

10

public boolean prestartCoreThread() {

    return addIfUnderCorePoolSize(null); //注意传进去的参数是null

}

public int prestartAllCoreThreads() {

    int n = 0;

    while (addIfUnderCorePoolSize(null))//注意传进去的参数是null

        ++n;

    return n;

}

注意上面传进去的参数是null,根据第2小节的分析可知如果传进去的参数为null,则最后执行线程会阻塞在getTask方法中的

1

r = workQueue.take();

即等待任务队列中有任务。

4.任务缓存队列及排队策略

在前面我们多次提到了任务缓存队列,即workQueue,它用来存放等待执行的任务。

workQueue的类型为BlockingQueue<Runnable>,通常可以取下面三种类型:

1)ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;

2)LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;

3)synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。

5.任务拒绝策略

当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:

1

2

3

4

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。

ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。

ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)

ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

6.线程池的关闭

ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown()和shutdownNow(),其中:

  • shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
  • shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务

7.线程池容量的动态调整

ThreadPoolExecutor提供了动态调整线程池容量大小的方法:setCorePoolSize()和setMaximumPoolSize(),

  • setCorePoolSize:设置核心池大小
  • setMaximumPoolSize:设置线程池最大能创建的线程数目大小

当上述参数从小变大时,ThreadPoolExecutor进行线程赋值,还可能立即创建新的线程来执行任务。

三.使用示例

前面我们讨论了关于线程池的实现原理,这一节我们来看一下它的具体使用:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

public class Test {

     public static void main(String[] args) {  

         ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS,

                 new ArrayBlockingQueue<Runnable>(5));

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

             MyTask myTask = new MyTask(i);

             executor.execute(myTask);

             System.out.println("线程池中线程数目:"+executor.getPoolSize()+",队列中等待执行的任务数目:"+

             executor.getQueue().size()+",已执行玩别的任务数目:"+executor.getCompletedTaskCount());

         }

         executor.shutdown();

     }

}

class MyTask implements Runnable {

    private int taskNum;

    public MyTask(int num) {

        this.taskNum = num;

    }

    @Override

    public void run() {

        System.out.println("正在执行task "+taskNum);

        try {

            Thread.currentThread().sleep(4000);

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

        System.out.println("task "+taskNum+"执行完毕");

    }

}

执行结果:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

正在执行task 0

线程池中线程数目:1,队列中等待执行的任务数目:0,已执行玩别的任务数目:0

线程池中线程数目:2,队列中等待执行的任务数目:0,已执行玩别的任务数目:0

正在执行task 1

线程池中线程数目:3,队列中等待执行的任务数目:0,已执行玩别的任务数目:0

正在执行task 2

线程池中线程数目:4,队列中等待执行的任务数目:0,已执行玩别的任务数目:0

正在执行task 3

线程池中线程数目:5,队列中等待执行的任务数目:0,已执行玩别的任务数目:0

正在执行task 4

线程池中线程数目:5,队列中等待执行的任务数目:1,已执行玩别的任务数目:0

线程池中线程数目:5,队列中等待执行的任务数目:2,已执行玩别的任务数目:0

线程池中线程数目:5,队列中等待执行的任务数目:3,已执行玩别的任务数目:0

线程池中线程数目:5,队列中等待执行的任务数目:4,已执行玩别的任务数目:0

线程池中线程数目:5,队列中等待执行的任务数目:5,已执行玩别的任务数目:0

线程池中线程数目:6,队列中等待执行的任务数目:5,已执行玩别的任务数目:0

正在执行task 10

线程池中线程数目:7,队列中等待执行的任务数目:5,已执行玩别的任务数目:0

正在执行task 11

线程池中线程数目:8,队列中等待执行的任务数目:5,已执行玩别的任务数目:0

正在执行task 12

线程池中线程数目:9,队列中等待执行的任务数目:5,已执行玩别的任务数目:0

正在执行task 13

线程池中线程数目:10,队列中等待执行的任务数目:5,已执行玩别的任务数目:0

正在执行task 14

task 3执行完毕

task 0执行完毕

task 2执行完毕

task 1执行完毕

正在执行task 8

正在执行task 7

正在执行task 6

正在执行task 5

task 4执行完毕

task 10执行完毕

task 11执行完毕

task 13执行完毕

task 12执行完毕

正在执行task 9

task 14执行完毕

task 8执行完毕

task 5执行完毕

task 7执行完毕

task 6执行完毕

task 9执行完毕

从执行结果可以看出,当线程池中线程的数目大于5时,便将任务放入任务缓存队列里面,当任务缓存队列满了之后,便创建新的线程。如果上面程序中,将for循环中改成执行20个任务,就会抛出任务拒绝异常了。

不过在java doc中,并不提倡我们直接使用ThreadPoolExecutor,而是使用Executors类中提供的几个静态方法来创建线程池:

1

2

3

Executors.newCachedThreadPool();        //创建一个缓冲池,缓冲池容量大小为Integer.MAX_VALUE

Executors.newSingleThreadExecutor();   //创建容量为1的缓冲池

Executors.newFixedThreadPool(int);    //创建固定容量大小的缓冲池

下面是这三个静态方法的具体实现;

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

public static ExecutorService newFixedThreadPool(int nThreads) {

    return new ThreadPoolExecutor(nThreads, nThreads,

                                  0L, TimeUnit.MILLISECONDS,

                                  new LinkedBlockingQueue<Runnable>());

}

public static ExecutorService newSingleThreadExecutor() {

    return new FinalizableDelegatedExecutorService

        (new ThreadPoolExecutor(1, 1,

                                0L, TimeUnit.MILLISECONDS,

                                new LinkedBlockingQueue<Runnable>()));

}

public static ExecutorService newCachedThreadPool() {

    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,

                                  60L, TimeUnit.SECONDS,

                                  new SynchronousQueue<Runnable>());

}

从它们的具体实现来看,它们实际上也是调用了ThreadPoolExecutor,只不过参数都已配置好了。

newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的LinkedBlockingQueue;

newSingleThreadExecutor将corePoolSize和maximumPoolSize都设置为1,也使用的LinkedBlockingQueue;

newCachedThreadPool将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用的SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。

实际中,如果Executors提供的三个静态方法能满足要求,就尽量使用它提供的三个方法,因为自己去手动配置ThreadPoolExecutor的参数有点麻烦,要根据实际任务的类型和数量来进行配置。

另外,如果ThreadPoolExecutor达不到要求,可以自己继承ThreadPoolExecutor类进行重写。

四.如何合理配置线程池的大小

本节来讨论一个比较重要的话题:如何合理配置线程池大小,仅供参考。

一般需要根据任务的类型来配置线程池大小:

如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1

如果是IO密集型任务,参考值可以设置为2*NCPU

当然,这只是一个参考值,具体的设置还需要根据实际情况进行调整,比如可以先将线程池大小设置为参考值,再观察任务运行情况和系统负载、资源利用率来进行适当调整。

三、CompletionService和ExecutorCompletionService详解 

  1. CompletionService接口定义了一组任务管理接口:
  • submit() - 提交任务
  • take() - 获取任务结果
  • poll() - 获取任务结果
  1. ExecutorCompletionService类是CompletionService接口的实现
  • ExecutorCompletionService内部管理者一个已完成任务的阻塞队列
  • ExecutorCompletionService引用了一个Executor, 用来执行任务
  • submit()方法最终会委托给内部的executor去执行任务
  • take/poll方法的工作都委托给内部的已完成任务阻塞队列
  • 如果阻塞队列中有已完成的任务, take方法就返回任务的结果, 否则阻塞等待任务完成
  • poll与take方法不同, poll有两个版本:
    • 无参的poll方法 --- 如果完成队列中有数据就返回, 否则返回null
    • 有参数的poll方法 --- 如果完成队列中有数据就直接返回, 否则等待指定的时间, 到时间后如果还是没有数据就返回null
    • ExecutorCompletionService主要用与管理异步任务 (有结果的任务, 任务完成后要处理结果)
  1. 关于CompletionService和ExecutorCompletionService的类图如下:

CompletionService.png

下面是简单的用法:

package com.example.concurrent;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class Main {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
//        case1();
//        case2();
        case3();
    }


    /**
     * <一>
     * 1. 用List收集任务结果 (List记录每个submit返回的Future)
     * 2. 循环查看结果, Future不一定完成, 如果没有完成, 那么调用get会租塞
     * 3. 如果排在前面的任务没有完成, 那么就会阻塞, 这样后面已经完成的任务就没法获得结果了, 导致了不必要的等待时间.
     *    更为严重的是: 第一个任务如果几个小时或永远完成不了, 而后面的任务几秒钟就完成了, 那么后面的任务的结果都将得不到处理
     *
     * 导致: 已完成的任务可能得不到及时处理
     */
    private static void case1() throws ExecutionException, InterruptedException {
        final Random random = new Random();
        ExecutorService service = Executors.newFixedThreadPool(10);
        List<Future<String>> taskResultHolder = new ArrayList<>();
        for(int i=0; i<50; i++) {
            //搜集任务结果
            taskResultHolder.add(service.submit(new Callable<String>() {
                public String call() throws Exception {
                    Thread.sleep(random.nextInt(5000));
                    return Thread.currentThread().getName();
                }
            }));
        }
        // 处理任务结果
        int count = 0;
        System.out.println("handle result begin");
        for(Future<String> future : taskResultHolder) {
            System.out.println(future.get());
            count++;
        }
        System.out.println("handle result end");
        System.out.println(count + " task done !");

        //关闭线程池
        service.shutdown();
    }

    /**
     * <二> 只对第一种情况进行的改进
     *      1. 查看任务是否完成, 如果完成, 就获取任务的结果, 让后重任务列表中删除任务.
     *      2. 如果任务未完成, 就跳过此任务, 继续查看下一个任务结果.
     *      3. 如果到了任务列表末端, 那么就从新回到任务列表开始, 然后继续从第一步开始执行
     *
     *      这样就可以及时处理已完成任务的结果了
     */
    private static void case2() throws ExecutionException, InterruptedException {
        final Random random = new Random();
        ExecutorService service = Executors.newFixedThreadPool(10);
        List<Future<String>> results = new ArrayList<>();

        for(int i=0; i<50; i++) {
            Callable<String> task = new Callable<String>() {
                public String call() throws Exception {
                    Thread.sleep(random.nextInt(5000)); //模拟耗时操作
                    return Thread.currentThread().getName();
                }
            };
            Future<String> future = service.submit(task);
            results.add(future); // 搜集任务结果
        }

        int count = 0;
        //自旋, 获取结果
        System.out.println("handle result begin");
        for(int i=0; i<results.size(); i++) {
            Future<String> taskHolder = results.get(i);

            if(taskHolder.isDone()) { //任务完成
                String result = taskHolder.get(); //获取结果, 进行某些操作
                System.out.println("result: " + result);
                results.remove(taskHolder);
                i--;

                count++; //完成的任务的计数器
            }

            //回到列表开头, 从新获取结果
            if(i == results.size() - 1) i = -1;
        }
        System.out.println("handle result end");
        System.out.println(count + " task done !");

        //线程池使用完必须关闭
        service.shutdown();
    }


    /**
     * <三> 使用ExecutorCompletionService管理异步任务
     * 1. Java中的ExecutorCompletionService<V>本身有管理任务队列的功能
     *    i. ExecutorCompletionService内部维护列一个队列, 用于管理已完成的任务
     *    ii. 内部还维护列一个Executor, 可以执行任务
     *
     * 2. ExecutorCompletionService内部维护了一个BlockingQueue, 只有完成的任务才被加入到队列中
     *
     * 3. 任务一完成就加入到内置管理队列中, 如果队列中的数据为空时, 调用take()就会阻塞 (等待任务完成)
     *    i. 关于完成任务是如何加入到完成队列中的, 请参考ExecutorCompletionService的内部类QueueingFuture的done()方法
     *
     * 4. ExecutorCompletionService的take/poll方法是对BlockingQueue对应的方法的封装, 关于BlockingQueue的take/poll方法:
     *    i. take()方法, 如果队列中有数据, 就返回数据, 否则就一直阻塞;
     *    ii. poll()方法: 如果有值就返回, 否则返回null
     *    iii. poll(long timeout, TimeUnit unit)方法: 如果有值就返回, 否则等待指定的时间; 如果时间到了如果有值, 就返回值, 否则返回null
     *
     * 解决了已完成任务得不到及时处理的问题
     */
    static void case3() throws InterruptedException, ExecutionException {
        Random random = new Random();

        ExecutorService service = Executors.newFixedThreadPool(10);
        ExecutorCompletionService<String> completionService = new ExecutorCompletionService<String>(service);

        for(int i=0; i<50; i++) {
            completionService.submit(new Callable<String>() {
                @Override
                public String call() throws Exception {
                    Thread.sleep(random.nextInt(5000));
                    return Thread.currentThread().getName();
                }
            });
        }

        int completionTask = 0;
        while(completionTask < 50) {
            //如果完成队列中没有数据, 则阻塞; 否则返回队列中的数据
            Future<String> resultHolder = completionService.take();
            System.out.println("result: " + resultHolder.get());
            completionTask++;
        }

        System.out.println(completionTask + " task done !");

        //ExecutorService使用完一定要关闭 (回收资源, 否则系统资源耗尽! .... 呵呵...)
        service.shutdown();
    }
}

那咩, ExecutorCompletionService是如何执行任务, 又是如何将任务的结果存储到完成队列中的呢?

  1. ExecutorCompletionService在submit任务时, 会创建一个QueueingFuture, 然后将创建的QueueingFuture丢给executor, 让executor完成任务的执行工作
  2. QueueingFuture继承与FutureTask类, 而FutureTask实现了两个接口Runnable和Future.
  • Runnable一般表示要执行的任务的过程, 而Future则表述执行任务的 结果 (或者说是任务的一个句柄, 可获取结果, 取消任务等).
  • 因此FutureTask就是一个有结果可期待的任务. FutureTask实现了run方法, 我们指定此方法一般是在在工作线程(不是submit线程) 执行的, 可以看看源码:
public void run() {
      if (state != NEW ||
          !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                       null, Thread.currentThread()))
          return;
      try {
          Callable<V> c = callable;
          if (c != null && state == NEW) {
              V result;
              boolean ran;
              try {
                  result = c.call();
                  ran = true;
              } catch (Throwable ex) {
                  result = null;
                  ran = false;
                  setException(ex);
              }
              if (ran)
                  set(result);
          }
      } finally {
          // runner must be non-null until state is settled to
          // prevent concurrent calls to run()
          runner = null;
          // state must be re-read after nulling runner to prevent
          // leaked interrupts
          int s = state;
          if (s >= INTERRUPTING)
              handlePossibleCancellationInterrupt(s);
      }
  }
  • FutureTask构造的时候需要一个Callable<V>参数, Callable表示一个任务的执行过程, 在run方法中恰好调用了Callable.call(), 也就是任务工作在工作线程中执行.
  • 那么任务执行完了会返回结果, 这个结果是要在submit线程(就是提交任务的线程)中使用的, 那么如何让submit线程可以反问到呢? 答案也是在FutureTask类中, 我们可以看到run方法中执行任务(Callable.call())获取结果后, 会掉用一个set()方法, set方法源码如下:
protected void set(V v) {
      if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
          outcome = v;
          UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
          finishCompletion();
      }
  }
  • set() 将获取的结果存储到FuturnTask的一个outcome字段中, 这个过程是同步的, 所以其他线程稍后访问是可以读取到值的

  • ExecutorCompletionService中的完成队列中正好存储的是FuturnTask的子类, 当然可以调用FutureTask的get方法, FutureTask的get方法就是获取outcome值 (get()方法中调用了report()方法, report中返回了outcome字段).

  • FuturnTask中委托的任务执行完成后, 会掉一个done()方法, 这个方法是个空方法, 而其子类QueueingFuture重写了此方法, 如下:

protected void done() { 
    completionQueue.add(task); 
}

正式在此方法中把执行完的任务放置到完成队列中的!!

然后我们就可以在submit线程中从完成队列中取出任务句柄, 获取任务结果了!!!

至此, ExecutorCompletionService原理已经解析完毕 !!

欲知后事如何, 切听下回分解 !!
==> Future/FutureTask源码分析

四、Future/FutureTask源码分析

一. 关于Future/FutureTask

Future/FutureTask/ExecutorService相关类图:

FutureTask.png

0x01: Future

下面是Future的Doc文档

A {@code Future} represents the result of an asynchronous computation. Methods are provided to check if the computation is complete, to wait for its completion, and to retrieve the result of the computation. The result can only be retrieved using method {@code get} when the computation has completed, blocking if necessary until it is ready. Cancellation is performed by the {@code cancel} method. Additional methods are provided to determine if the task completed normally or was cancelled. Once a computation has completed, the computation cannot be cancelled. If you would like to use a {@code Future} for the sake of cancellability but not provide a usable result, you can declare types of the form {@code Future<?>} and return {@code null} as a result of the underlying task.

简单的说, Future表示一个异步计算的结果. 异步计算是在其他线程进行的, 因此异步计算的结果, 有可能有值, 也有可能没有值. 于是, Future就提供了一些方法来处理这种未知状态:

a. isDone() 异步任务是否完成, 即否有结果

b. get() 获取异步任务结果, 如果异步任务未完成, 此方法会一直阻塞, 直到异步方法完成 或 任务被取消 (调用了cancel()方法)

c. cancel() 取消异步任务, 如果异步任务已经完成, 那么取消失败(即cancel()方法返回false)

d. isCancelled() 查询异步任务是否已被取消

简单用法:

interface ArchiveSearcher { 
    String search(String target); 
}

class App {

  ExecutorService executor = ...
  ArchiveSearcher searcher = ...

  void showSearch(final String target) throws InterruptedException {
    Future<String> future = executor.submit(new Callable<String>() {
        public String call() {
            return searcher.search(target);
        }});

    displayOtherThings(); // do other things while searching
    try {
      displayText(future.get()); // use future
    } catch (ExecutionException ex) { 
        cleanup(); 
        return; 
    }
  }

}

用Future主要是为了获取异步计算的结果. 例如在Android中, 你会把网络请求放在子线程中去执行, 而请求的结果会拿到UI线程中来使用. 这时候就可以使用Future. 关键代码如下:

interface NetworkService {
    ModelXxx requestXxx();
}

// 注意: 这是一个阻塞的方法, 不能在UI线程中直接调用
public ModelXxx xx() {
    final NetworkService networkService = ...
    ExecutorService executorService = ...

    Future<ModelXxx> future = executorService.submit(new Callable<ModelXxx>() {
        @Override
        public ModelXxx call() throws Exception {
            return networkService.requestXxx();
        }
    });

    ModelXxx modelXxx = future.get(); //此处可能阻塞
}

void bindDataToUi(ModelXxx model) {
    // do something with model ...
}

Future使用起来并不方便, 因为异步任务什么时完成我们并不知晓, 除非你用isDone()方法去查询是否完成. 如果要异步任务完成后主动通知我们, 那么该如何做呢? 这个问题留到后面说.

0x02: FutureTask

下面是FutureTask的Doc文档:

A cancellable asynchronous computation. This class provides a base implementation of {@link Future}, with methods to start and cancel a computation, query to see if the computation is complete, and retrieve the result of the computation. The result can only be retrieved when the computation has completed; the {@code get} methods will block if the computation has not yet completed. Once the computation has completed, the computation cannot be restarted or cancelled (unless the computation is invoked using {@link #runAndReset}).

a. 简单来说: FutureTask就是一个可取消的异步任务.

b. 把FutureTask的名字拆开来看, FutureTask是Future(异步任务结果)和Task(异步任务)的集合. 因此, 我们可以直接把FutureTask扔给ExecutorService去执行, 然后又可以获取计算结果 (Future的特性FutureTask都有).

c. FutureTask的基本用法和步骤如下:

public void someBiz() {
        // 1. init services
        final String serviceUrl = ...
        final NetworkService networkService = ...
        final ExecutorService executorService = ...

        // 2. make task
        FutureTask<String> futureTask = new FutureTask<String>(new Callable<String>() {
            @Override
            public String call() throws Exception {
                return networkService.request(serviceUrl);
            }
        });
        
        // 3. submit task
        executorService.submit(futureTask);
        
        // 4. get result
        String result = futureTask.get();
        
        // 5. do something with result
        // ...
    }

d. 使用FutureTask执行异步任务有个问题: 就是异步任务执行完了并不会通知调用方. 但是, FutureTask已经有支持异步任务执行完毕就立刻通知调用方的基础. FutureTask有一个done() 方法, 此方法即是异步任务调用完毕的回调方法, 这个方法执行时已经可以获取到结果数据了. 我们可以扩展FutureTask并重写done()来支持异步任务执行完成后的回调通知.

二. Future/FutureTask实现解析

a. Future的创建

先看看Future是如何产生的(java.util.concurrent.AbstractExecutorService类中):

public <T> Future<T> submit(Callable<T> task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<T> ftask = newTaskFor(task);
    execute(ftask);
    return ftask;
}

protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
    return new FutureTask<T>(callable);
}

当我们提交一个异步任务 (Callable<T>) 时, 会调用submit()方法, submit()方法内部会调用newTask()方法创建一个FutureTask. 我们已经知道, FutureTask是Future的实现, 又是Runnable的实现. 因此, 它既可以执行又可以获取结果.

b. FutureTask的执行逻辑

创建FutureTask时, 会把我们提交的任务 (Callbable)传递给FutureTask. 其实FutureTask执行时, 会委托给传进来的Callable. 基本逻辑如下:

public class FutureTask<V> implements RunnableFuture<V> {
    // 用来存储 "用户提供的有实在业务逻辑的" 任务
    private Callable<V> callable;
    
    // 用来保存异步计算的结果
    private Object outcome;

    public FutureTask(Callable<V> callable) {
        if (callable == null) throw new NullPointerException();
        // 保存外部传来的任务, 待会在run()方法中调用
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }

    public void run() {
        // 省略其他代码 ...

        Callable<V> c = callable;

        //FutureTak的执行逻辑委托给用户提供的真正任务
        V result = c.call(); 

        // 设置异步任务结果
        set(result);
    }

    // 其他代码省略 ...
}

c. FutureTask是如何保存计算结果的

可以看到, FutureTask实现了RunnableFuture接口, 此接口是Runnable接口和Future接口的结合. 自然FutureTask是要被当成任务来在线程中执行的( 线程内部执行的一般都是Runnable ). FutureTask内部用了一个Object成员outcome来存储异步任务的结果. run() 方法调用用户传过来的Callbable的call()方法并产生一个运算结果, 此时会调用set()方法来将计算结果存储到成员outcome中. 下面就来看看FutureTask是如何将计算结果设置到outcome成员中的:

protected void set(V v) {
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        outcome = v;
        UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
        finishCompletion();
    }
}

非常简单, 直接赋值就好了. 需要注意的是, 赋值之前有一个任务状态的切换, 这个切换会同步. 因此设置完后, 其他线程就可以获取到结果了.

d. 任务执行完的收尾工作 (任务完成的回调, 资源回收等)

我们注意到, 赋值后会调用一个finishCompletion()方法, , 那么就来看看此方法:

private void finishCompletion() {
        // 省略无关代码 ... 
        
        done();
        callable = null;        // to reduce footprint
    }

其实就是做一些收尾工作, 将callbable置null等 (callable中可能会持有一些资源).
此方法里面还调用了done()方法, 此方法其实就是异步任务执行完的回调, 下面来看看此方法:

protected void done() { }

空的实现!! 且方法为protected, 这就是为扩展而存在的方法.
回到前面提的问题: **异步任务执行完毕后如何主动通知调用者? **
其实我们只要扩展FutureTask, 再给扩展对象设置一个回调对象, 然后重写done()方法, 在done()方法内调用回调对象就可以了.
具体我们可以来看看Guava的com.google.common.util.concurrent.ListenableFutureTask:

public class ListenableFutureTask<V> extends FutureTask<V> implements ListenableFuture<V> {

  //存储异步任务完成后的回调以及回调所在的Executor
  private final ExecutionList executionList = new ExecutionList(); 

  // 其他代码省略 ...

  //添加异步任务完成后的回调和回调所在的Executor
  @Override
  public void addListener(Runnable listener, Executor exec) {
    executionList.add(listener, exec);
  }

  @Override
  protected void done() {
    // 异步任务完成后回调
    executionList.execute();
  }
}

需要注意的是FutureTaskdone()方法是在Worker线程中执行的, 一般我们获取结果是在其他线程, 因此需要把计算结果挪到指定的线程中去. 因此不仅需要指定任务完成的回调, 还需要指定任务完成的回调所在的线程.

e. FutureTask计算结果同步问题

既然FutureTask是在Worker线程中执行的, 那么其他线程获取计算结果, 就会存在同步问题. 那么FutureTask是如何来同步计算结果的呢?

jdk1.8 FutureTask源码中, 保存计算结果的成员变量声明是这样的:
private Object outcome; // non-volatile, protected by state reads/writes
可以看到后面的说明, 此变量是non-volatile的, 同步是用state字段的读写来保证的. 由于用了不公开的API, 逻辑也比较复杂, 具体就不多说了, 自己看源码吧 --

三. 自定义FutureTask

a. 下面是对FutureTask的简单扩展 (获取网络图片)

import android.support.annotation.NonNull;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicReference;

public class ListenableFuture<V> extends FutureTask<V> {

    AtomicReference<Listener> mListenerRef;

    public ListenableFuture(@NonNull Callable<V> callable) {
        super(callable);
        mListenerRef = new AtomicReference<>();
    }

    public ListenableFuture(@NonNull Runnable runnable, V result) {
        super(runnable, result);
    }


    @Override
    protected void done() {
        final Listener<V> listener = mListenerRef.get();
        if(listener != null) {
            try {
                onSuccess(listener, get());
            } catch (InterruptedException e) {
                onFailed(listener, e);
            } catch (ExecutionException e) {
                onFailed(listener, e);
            }
            mListenerRef.set(null);
        }
    }

    private void onSuccess(final Listener<V> listener, final V result) {
        MainHandler.post(new Runnable() {
            @Override
            public void run() {
                listener.onSuccess(result);
            }
        });
    }

    private void onFailed(final Listener<V> listener, final Throwable e) {
        MainHandler.post(new Runnable() {
            @Override
            public void run() {
                listener.onFailed(e);
            }
        });
    }

    public void setListener(Listener<V> listener) {
        mListenerRef.compareAndSet(mListenerRef.get(), listener);
    }

    public interface Listener<V> {
        void onSuccess(V v);
        void onFailed(Throwable e);
    }
}

工具类MainHandler.java

import android.os.Handler;
import android.os.Looper;

public final class MainHandler {

    private static Handler mainHandler;

    private MainHandler() {
        //no instance
    }

    public static void post(Runnable task) {
        getMainHandler().post(task);
    }

    public static Handler getMainHandler() {
        if(mainHandler == null) {
            mainHandler = new Handler(Looper.getMainLooper());
        }
        return mainHandler;
    }
}

ListenableFuture的使用方法如下:

import com.stone.demo.R;

import android.Manifest;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.ImageView;
import android.widget.Toast;

import java.io.InputStream;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class MainActivity extends AppCompatActivity {
    ExecutorService service;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_future_main);

        service = Executors.newFixedThreadPool(4);

        findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                loadImage();
            }
        });
        checkPermission();
    }

    private void checkPermission() {
        int result = ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE);
        if(!(result == PackageManager.PERMISSION_GRANTED)) {
            ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0x10);
        }
    }


    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if(requestCode == 0x10) {
            if(!(grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
                Toast.makeText(this, "您拒绝了访问磁盘", Toast.LENGTH_SHORT).show();
            }
        }
    }

    private void loadImage() {
        ListenableFuture<Bitmap> future = new ListenableFuture<Bitmap>(new Callable<Bitmap>() {
            @Override
            public Bitmap call() throws Exception {
                OkHttpClient client = new OkHttpClient();
                Request request = new Request.Builder().url("http://img06.tooopen.com/images/20161214/tooopen_sy_190570171299.jpg").build();
                Call call = client.newCall(request);
                Response response = call.execute();
                InputStream inputStream = null;
                if(response != null && response.body() != null && (inputStream = response.body().byteStream()) != null) {
                    Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
                    inputStream.close();
                    return bitmap;
                }
                return null;
            }
        });

        future.setListener(new ListenableFuture.Listener<Bitmap>() {
            @Override
            public void onSuccess(Bitmap bitmap) {
                if(bitmap != null) {
                    ImageView.class.cast(findViewById(R.id.img)).setImageBitmap(bitmap);
                } else {
                    Toast.makeText(MainActivity.this, "图片加载失败", Toast.LENGTH_SHORT).show();
                }
            }

            @Override
            public void onFailed(Throwable e) {
                Toast.makeText(MainActivity.this, "图片加载失败", Toast.LENGTH_SHORT).show();
            }
        });

        service.submit(future);
    }
}

布局文件非常简单, 上面一个Button, 下面一个ImageView, 就不贴出来了。

参考资料:

http://ifeve.com/java-threadpool/

http://blog.163.com/among_1985/blog/static/275005232012618849266/

http://developer.51cto.com/art/201203/321885.htm

http://blog.csdn.net/java2000_wl/article/details/22097059

http://blog.csdn.net/cutesource/article/details/6061229

http://blog.csdn.net/xieyuooo/article/details/8718741

《JDK API 1.6》

猜你喜欢

转载自blog.csdn.net/ZYC88888/article/details/87706725