1. 线程池
1.1 JDK中的线程池
一、线程池:提供了一个线程队列,队列中保存着所有等待状态的线程。避免了创建与销毁额外开销,提高了响应的速度。
二、线程池的体系结构:
java.util.concurrent.Executor : 负责线程的使用与调度的根接口
|–ExecutorService 子接口: 线程池的主要接口
|–ThreadPoolExecutor 线程池的实现类
|–ScheduledExecutorService 子接口:负责线程的调度
|–ScheduledThreadPoolExecutor :继承 ThreadPoolExecutor, 实现 ScheduledExecutorService
三、工具类 : Executors
- newFixedThreadPool(int nThreads) 创建一个固定数量线程的线程池,如果在所有线程都处于活动状态时提交了额外的任务,他们将在队列中等待,直到线程可用为止。
- newSingleThreadExecutor() 创建一个单例的线程池,也就是说池中就一个线程。
- newCachedThreadPool() 创建一个可缓存线程池,如果没有线程可用,会创建一个线程的线程来取代他,再放入池中。如果有线程60s钟不工作,就销魂。
- newSingleThreadScheduledExecutor() 创建一个单线程的线程池,此线程池的的线程可以定时周期性的运行任务。注意坑点:使用这种方法,如果出现异常,会导致无法正常的运行任务。所以,个人建议,使用这种方式的时候,run方法里面的代码可以加上异常处理逻辑。
- newScheduledThreadPool(int corePoolSize) 创建一个固定大小的线程池。此线程池支持定时以及周期性执行任务的需求。
- newWorkStealingPool() 创建一个工作窃取式线程池,每个工作线程有自己的任务队列,当前完成自己本地的队列的任务时,会自动去全局队列里面获取任务来工作,或者去”偷“其他线程的队列里面的任务。它实现的是ForkJoinPool 类(基于Fork/Join框架)
常用的线程池模型:
如上图所示,工作队列由主线程和工作者线程共享,主线程将任务放进工作队列,工作者线程从工作队列中取出任务执行。共享工作队列的操作需在互斥量的保护下安全进行,主线程将任务放进工作队列时若检测到当前待执行的工作数目小于工作者线程总数,则需使用条件变量唤醒可能处于等待状态的工作者线程。当然,还有其他地方可能也会使用到互斥量和条件变量,不再赘述。
无锁化线程池模型
将共享工作队列加以拆分成每工作线程一个工作队列的方式。
Fork/Join框架
1.采用“工作窃取”模式(work-stealing):
当执行新的任务时它可以将其拆分分成更小的任务执行,并将小任务加到线程队列中,然后再从一个随机线程的队列中偷一个并把它放在自己的队列中。
2.相对于一般的线程池实现,fork/join框架的优势体现在对其中包含的任务的处理方式上.在一般的线程池中,如果一个线程正在执行的任务由于某些原因无法继续运行,那么该线程会处于等待状态。而在fork/join框架实现中,如果某个子问题由于等待另外一个子问题的完成而无法继续运行。那么处理该子问题的线程会主动寻找其他尚未运行的子问题来执行.这种方式减少了线程的等待时间,提高了性能。
Fork/Join框架核心算法—工作窃取算法简单介绍
工作窃取算法是指某个线程从其他队列里窃取任务来执行。那么,为什么需要使用工作窃取算法呢?假如我们需要做一个比较大的任务,可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应。比如A线程负责处理A队列里的任务。但是,有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是他就去其他线程的队列里窃取一个任务来执行。而在这时他们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。
工作窃取的运行流程如下图所示。
工作窃取算法的优点:充分利用线程进行并行计算,减少了线程间的竞争。
工作窃取算法的缺点:在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且该算法会消耗了更多的系统资源,比如创建多个线程和多个双端队列。
1.2 线程池的7参数
1.corePoolSize : 线程池维护线程的最少数量,哪怕是空闲的。
**2.maximumPoolSize :**线程池维护线程的最大数量。一个任务被提交到线程池后,首先会缓存到工作队列(后面会介绍)中,如果工作队列满了,则会创建一个新线程,然后从工作队列中的取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize来指定。
3.keepAliveTime : 线程池维护线程所允许的空闲时间。一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定。
4.unit : 线程池维护线程所允许的空闲时间的单位,keepAliveTime的计量单位。
5.workQueue : 线程池所使用的缓冲队列,缓冲队列的长度决定了能够缓冲的最大数量。
新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。主要使用了以下三种工作队列:
a)ArrayBlockingQueue
基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
b)LinkedBlockingQuene
基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
c)SynchronousQuene
一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
d)DelayedWorkQueue
一个具有优先级的延迟阻塞队列。
6.ThreadFactory:创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等
7.handler : 线程池对拒绝任务的处理策略。在 ThreadPoolExecutor 里面定义了 4 种 handler 策略,分别是
a)CallerRunsPolicy :这个策略重试添加当前的任务,他会自动重复调用 execute() 方法,直到成功。
b)AbortPolicy :对拒绝任务抛弃处理,并且抛出异常。
c)DiscardPolicy :对拒绝任务直接无声抛弃,没有异常信息。
d)DiscardOldestPolicy :对拒绝任务不抛弃,而是抛弃队列里面等待最久的一个任务,然后把拒绝任务加到队列。
1.3非阻塞队列与阻塞队列
1.ArrayDeque, (数组双端队列)
2.PriorityQueue, (优先级队列)
3.ConcurrentLinkedQueue, (基于链表的并发队列)
4.DelayQueue, (延期阻塞队列)(阻塞队列实现了BlockingQueue接口)
5.ArrayBlockingQueue, (基于数组的并发阻塞队列)
6.LinkedBlockingQueue, (基于链表的FIFO阻塞队列)
7.LinkedBlockingDeque, (基于链表的FIFO双端阻塞队列)
8.PriorityBlockingQueue, (带优先级的无界阻塞队列)
9.SynchronousQueue (并发同步阻塞队列)
阻塞队列和非阻塞队列的区别:
阻塞队列与普通队列的区别在于,当队列是空的时,从队列中获取元素的操作将会被阻塞,或者当队列是满时,往队列里添加元素的操作会被阻塞。试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。同样,试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程使队列重新变得空闲起来,如从队列中移除一个或者多个元素,或者完全清空队列。
阻塞队列可以阻塞,非阻塞队列不能阻塞,只能使用队列wait(),notify()进行队列消息传送。而阻塞队列当队列里面没有值时,会阻塞直到有值输入。输入也一样,当队列满的时候,会阻塞,直到队列不为空。
以下五个方法,在阻塞与非阻塞队列中均可使用。对于非阻塞队列,一般情况下建议使用offer、poll和peek三个方法,不建议使用add和remove方法。因为使用offer、poll和peek三个方法可以通过返回值判断操作成功与否,而使用add和remove方法却不能达到这样的效果。注意,非阻塞队列中的方法都没有进行同步措施。
a)add(E e):将元素e插入到队列末尾,如果插入成功,则返回true;如果插入失败(即队列已满),则会抛出异常;
b)remove():移除队首元素,若移除成功,则返回true;如果移除失败(队列为空),则会抛出异常;
c)offer(E e):将元素e插入到队列末尾,如果插入成功,则返回true;如果插入失败(即队列已满),则返回false;
d)poll():移除并获取队首元素,若成功,则返回队首元素;否则返回null;
e)peek():获取队首元素,若成功,则返回队首元素;否则返回null。
阻塞队列包括了非阻塞队列中的大部分方法,上面列举的5个方法在阻塞队列中都存在,但是要注意这5个方法在阻塞队列中都进行了同步措施。
除此之外,阻塞队列提供了另外4个非常有用的方法:
a)put(E e):put方法用来向队尾存入元素,如果队列满,则等待;
b)take():take方法用来从队首取元素,如果队列为空,则等待
c)offer(E e,long timeout, TimeUnit unit):offer方法用来向队尾存入元素,如果队列满,则等待一定的时间,当时间期限达到时,如果还没有插入成功,则返回false;否则返回true;
d)poll(long timeout, TimeUnit unit): poll方法用来从队首取元素,如果队列空,则等待一定的时间,当时间期限达到时,如果取到,则返回null;否则返回取得的元素;
关于阻塞队列和非阻塞队列的一个经典的使用场景就是生产者和消费者模型,思考一下,按照上面的对两种队列的描述,其实现有何差异呢?
很明显,阻塞队列不需要外力干扰,只要生产者和消费者使用的是同一个队列,生产者生产完数据,就会自动阻塞在那里,等待消费者消费,即队列只要不为空,生产中能和就会一直生产数据,
反观非阻塞队列,生产者生产完毕数据,由于是非阻塞的,其他任何线程都有可能参与到消费队列数据的可能,同时由于非阻塞,线程就有终止的风险,就不能像阻塞队列那样一直运转,就需要借助非阻塞队列的wait()和notify()方法,
1.4 非阻塞队列实现生产者和消费者
*
*/
public class Test1 {
public static void main(String[] args) {
Test1 t = new Test1();
Producer p = t.new Producer();
Consumer c = t.new Consumer();
p.start();
c.start();
}
private int queueSize = 10;
private PriorityQueue<Integer> queue = new PriorityQueue<Integer>();
//对于生产者来说,一旦队列填满了,就需要等待,即wait(),一旦消费者消费了,队列存在容量了,则生产者线程被notify(),就继续生产
class Producer extends Thread{
@Override
public void run() {
produce();
}
public void produce(){
while(true){
synchronized (queue) {
while(queue.size() == queueSize){
System.out.println("队列满了,需要等待");
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.offer(1);
queue.notify(); //生产者生产出了数据就通知消费者线程取出数据
System.out.println("生产者向队列中添加了一个元素,队列的剩余空间是:" + (10 - queue.size()));
}
}
}
}
//对于消费者来说,如果队列为空,则需要等待,否则,从队列中取出一条数据
class Consumer extends Thread{
@Override
public void run() {
consume();
}
public void consume(){
while(true){
synchronized (queue) {
while(queue.size() == 0){
System.out.println("队列为空,请等一会儿再消费");
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.poll();
//消费者取出数据后,就需要唤醒生产者线程,可以进行数据放入了
queue.notify();
System.out.println("消费者从队列中取出一个元素,队列中剩余的元素个数是:" + (queue.size()));
}
}
}
}
}
1.5 阻塞队列实现生产者和消费者
/**
* 阻塞队列实现生产者和消费者
*
*/
public class Test2 {
public static void main(String[] args) {
Test2 t = new Test2();
Producer p = t.new Producer();
Consumer c = t.new Consumer();
p.start();
c.start();
}
private int queueSize = 10;
private ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(queueSize);
// 生产者
class Producer extends Thread {
@Override
public void run() {
producer();
}
private void producer() {
while (true) {
try {
queue.put(1);
System.out.println("生产者向队列中添加了一个元素,队列的剩余空间是:" + (10 - queue.size()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 生产者
class Consumer extends Thread {
@Override
public void run() {
consumer();
}
private void consumer() {
while (true) {
try {
queue.take();
System.out.println("消费者从队列中取出一个元素,队列中剩余的元素个数是:" + (queue.size()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
1.6 Disruptor框架无锁实现生产者和消费者
Disruptor框架详细学习:http://ifeve.com/disruptor/
Disruptor是一个高性能的异步处理框架,一个轻量级的JMS,和JDK中的BlockingQueue有相似处,但是它的处理速度非常快,获得2011年程序框架创新大奖,号称“一个线程一秒钟可以处理600W个订单”。
Disruptor论文中讲述一个实验,一个计数器循环自增5亿次
场景1:单线程无锁时,程序耗时300ms
场景2:单线程有锁,程序需要耗时10000ms
场景3:双线程有锁,耗时224000ms
简而言之就是多线程锁竞争导致的上下文切换时间成本远远大于了线程持有锁的性能损耗
比较disruptor和阻塞队列
BlockQueue是悲观锁的一种体现,读写线程都假设存在冲突, 多线程并发场景下,性能很差
disruptor根本就不用锁,取而代之-CAS,严格意义上说仍然是使用锁, 因为CAS本质上也是一种乐观锁,CAS 比较适宜持有锁的时间较短的并发场景(自增、简单更新),CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
乐观锁:每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
悲观锁:这种线程一旦得到锁,其他需要锁的线程就挂起的情况就是悲观锁
disruptor核心数据结构-Ringbuffer 是一个环形数组
添加依赖
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.2.1</version>
</dependency>
创建数据实体类Data
/**
* 定义事件Data通过Disruptor 进行交换的数据类型。
*
*/
public class Data {
private Long value;
public Long getValue() {
return value;
}
public void setValue(Long value) {
this.value = value;
}
}
创建数据工厂
public class DataFactory implements EventFactory<Data> {
@Override
public Data newInstance() {
return new Data();
}
}
创建消费者
import com.lmax.disruptor.WorkHandler;
public class Consumer implements WorkHandler<Data> {
@Override
public void onEvent(Data data) throws Exception {
System.out.println(Thread.currentThread().getName()+"---"+data.getValue());
}
创建生产者
public class Producer {
//队列
private final RingBuffer<Data> ringBuffer;
public Producer(RingBuffer<Data> dataRingBuffer) {
this.ringBuffer = dataRingBuffer;
}
/**
* 插入数据
* @parambyteBuffer
*/
public void pushData(ByteBuffer byteBuffer) {
// 1.ringBuffer 事件队列 下一个槽
long sequence = ringBuffer.next();
Long data = null;
try {
// 2.取出空的事件队列
Data data1 = ringBuffer.get(sequence);
data = byteBuffer.getLong(0);
// 3.获取事件队列传递的数据
data1.setValue(data);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} finally {
System.out.println("生产者准备发送数据");
// 4.发布事件
ringBuffer.publish(sequence);
}
}
}
测试类
public class Test {
public static void main(String[] args) throws InterruptedException {
// 1.创建一个可缓存的线程 提供线程来出发Consumer 的事件处理
ExecutorService executor = Executors.newCachedThreadPool();
// 2.创建工厂
EventFactory<Data> DataFactory = new DataFactory();
// 3.创建ringBuffer 大小
int ringBufferSize = 1024 * 1024; // ringBufferSize大小一定要是2的N次方
/**
* 其中策略有几种:
* 1. BlockingWaitStrategy:阻塞策略,最节省CPU,但是高并发条件下性能最糟糕
* 2 SleepingWaitStrategy:在循环中无限等待,处理数据会产生高延迟,对生产线程影响小,场景:异步日志
* 3. YieldingWaitStrategy:低延迟场合,使用必须保证剩余的消费者线程的逻辑CPU
* 4. BusySpinWaitStrategy:消费者线程会尽最大努力疯狂的监控缓冲区变化。
*/
// 4.创建Disruptor
Disruptor<Data> disruptor = new Disruptor<Data>(DataFactory, ringBufferSize, executor,
ProducerType.SINGLE, new YieldingWaitStrategy());
// 5.连接消费端方法
// disruptor.handleEventsWith(new Consumer());
// 创建10个消费者来处理同一个生产者发的消息(这10个消费者不重复消费消息)
Consumer[] consumers = new Consumer[10];
for (int i = 0; i < consumers.length; i++) {
consumers[i] = new Consumer();
}
disruptor.handleEventsWithWorkerPool(consumers);
// 6.启动
disruptor.start();
// 7.创建RingBuffer容器
RingBuffer<Data> ringBuffer = disruptor.getRingBuffer();
// 8.创建生产者
Producer producer = new Producer(ringBuffer);
// 9.指定缓冲区大小
ByteBuffer byteBuffer = ByteBuffer.allocate(8);
for (int i = 1; i <= 100; i++) {
byteBuffer.putLong(0, i);
producer.pushData(byteBuffer);
}
// 10.关闭disruptor和executor
disruptor.shutdown();
executor.shutdown();
}
}
根据Disruptor框架的官方报告,Distruptor框架的性能比BlockingQueue队列至少高一个数量级。
1.7 线程池使用示例
第一种:newFixedThreadPool(int nThreads) 创建一个固定数量线程的线程池,如果在所有线程都处于活动状态时提交了额外的任务,他们将在队列中等待,直到线程可用为止。
/**
* 因为线程池大小为3,每个任务输出index后sleep 2秒,所以每两秒打印3个数字。
* 定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()。可参考PreloadDataCache。
*/
@Test
public void fixedThreadPool() {
// ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
// ExecutorService fixedThreadPool =new ThreadPoolExecutor(3, 3,
// 0L, TimeUnit.MILLISECONDS,
// new LinkedBlockingQueue<Runnable>());
ExecutorService fixedThreadPool =new ThreadPoolExecutor(3, 3,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
// ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
// Executors.defaultThreadFactory(), defaultHandler);
// return new ThreadPoolExecutor(3, 3,
// 0L, TimeUnit.MILLISECONDS,
// new LinkedBlockingQueue<Runnable>());
for (long i = 0; i < 10; i++) {
final long index = i;
fixedThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
log.info(Thread.currentThread().getName() + ":" + index);
Thread.sleep(5000);
} catch (InterruptedException e) {
log.error(e.getMessage());
}
}
});
}
try {
Thread.sleep(30000);
} catch (InterruptedException e) {
log.error(e.getMessage());
}
}
第二种:newCachedThreadPool() 创建一个可缓存线程池,如果没有线程可用,会创建一个线程的线程来取代他,再放入池中。如果有线程60s钟不工作,就销毁。
/**
* 线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。
*/
@Test
public void cachedThreadPool() {
// ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
ExecutorService cachedThreadPool = new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
for (long i = 0; i < 1000000; i++) {
final long index = i;
cachedThreadPool.execute(new Runnable() {
@Override
public void run() {
log.info(Thread.currentThread().getName() + ":" + index);
try {
Thread.sleep(index * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
try {
Thread.sleep(30000);
} catch (InterruptedException e) {
log.error(e.getMessage());
}
}
第三种:newSingleThreadExecutor() 创建一个单例的线程池,也就是说池中就一个线程。
/**
* 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
*/
@Test
public void newSingleThreadExecutor() {
// ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
ExecutorService singleThreadExecutor =new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
for (long i = 0; i < 10; i++) {
final long index = i;
singleThreadExecutor.execute(new Runnable() {
@Override
public void run() {
try {
log.info(Thread.currentThread().getName() + ":" + index);
Thread.sleep(2000);
} catch (Exception e) {
log.error(e.getMessage());
}
}
});
}
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
log.error(e.getMessage());
}
}
第四种:newScheduledThreadPool(int corePoolSize) 创建一个固定大小的线程池。此线程池支持定时以及周期性执行任务的需求。
/**
* 创建一个定长线程池,支持定时及周期性任务执行
* 表示延迟3秒执行。
*/
@Test
public void newScheduledThreadPool() {
//ScheduledThreadPoolExecutor
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(1);
// scheduledThreadPool.schedule(new Runnable() {
//
// @Override
// public void run() {
// log.info(Thread.currentThread().getName() + ":delay 5 seconds");
// }
// }, 5, TimeUnit.SECONDS);
/**
* 是以上一个任务开始的时间计时,period时间过去后,检测上一个任务是否执行完毕,如果上一个任务执行完毕,则当前任务立即执行,如果上一个任务没有执行完毕,则需要等上一个任务执行完毕后立即执行。
* scheduledThreadPool.scheduleAtFixedRate()
*/
scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
log.info(Thread.currentThread().getName() + ":delay 1 seconds, and excute every 1 seconds");
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, 1, 1, TimeUnit.SECONDS);
/**
* 是以上一个任务结束时开始计时,period时间过去后,立即执行。
* scheduledThreadPool.scheduleWithFixedDelay()
*/
scheduledThreadPool.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
log.info(Thread.currentThread().getName() + ":delay 1 seconds, and excute every 3 seconds");
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, 1, 6, TimeUnit.SECONDS);
//
//
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
log.error(e.getMessage());
}
}
第五种:newWorkStealingPool() 创建一个工作窃取式线程池,每个工作线程有自己的任务队列,当前完成自己本地的队列的任务时,会自动去全局队列里面获取任务来工作,或者去”偷“其他线程的队列里面的任务。它实现的是ForkJoinPool 类(基于Fork/Join框架)
/**
* 线程池类ForkJoinPool的扩展,能够合理的使用CPU,进行并行运行任务
* 每个工作线程有自己的任务队列,当前完成自己本地的队列的任务时,会自动去全局队列里面获取任务来工作,或者去”偷“其他线程的队列里面的任务。
* 创建时如果不设置任何参数,则以当前机器处理器个数作为线程个数,此线程池会并行处理任务,不能保证执行顺序。
*
*/
@Test
public void newWorkStealingPool() throws IOException, InterruptedException, ExecutionException {
// CPU 核数
System.out.println(Runtime.getRuntime().availableProcessors());
ForkJoinPool fp = (ForkJoinPool) Executors.newWorkStealingPool();
double t1 = System.currentTimeMillis();
double sum = 0;
for (double i=0; i<=5000000000d; i++)
sum += i;
double t2 = System.currentTimeMillis();
System.out.println("final sum = " + sum);
System.out.println("TimeUsage with 1个thread: " + (t2 - t1));
CountSumOfIntegers task = new CountSumOfIntegers(1, 5000000000d);
Future<Double> result = fp.submit(task);
System.out.println("waiting .....");
System.out.println("final sum = " + result.get());
System.out.println("TimeUsage with 6个thread: " + (System.currentTimeMillis() - t2));
// 因为work stealing 是deamon线程,即后台线程,精灵线程,守护线程
// 所以当main方法结束时, 此方法虽然还在后台运行,但是无输出
// 可以通过对主线程阻塞解决
// System.in.read();
}
/**
* 定义一个可分解的的任务类,继承了RecursiveAction抽象类
* 必须实现它的compute方法
*/
class CountSumOfIntegers extends RecursiveTask<Double>{
/**
*
*/
private static final double serialVersionUID = 1d;
private static final double THREADHOLD = 1000000000d;
private double start, end;
public CountSumOfIntegers(double start, double end) {
this.start = start;
this.end = end;
}
@Override
protected Double compute() {
double sum = 0;
if (end - start <= THREADHOLD) {
for (double i=start; i<=end; i++)
sum += i;
} else {
double mid = start + (end - start) / 2;
CountSumOfIntegers lt = new CountSumOfIntegers(start, mid);
CountSumOfIntegers rt = new CountSumOfIntegers(mid+1, end);
lt.fork();
rt.fork();
double leftsum = lt.join();
double rightsum = rt.join();
sum = leftsum + rightsum;
}
System.out.println(Thread.currentThread().getName()+"求和结果:"+sum);
return sum;
}
}
1.8 线程池任务流程总结
提交任务后,线程池先判断线程数是否达到了核心线程数(corePoolSize)。如果未达到线程数,则创建核心线程处理任务;否则,就执行下一步;
接着线程池判断任务队列是否满了。如果没满,则将任务添加到任务队列中;否则,执行下一步;
接着因为任务队列满了,线程池就判断线程数是否达到了最大线程数。如果未达到,则创建非核心线程处理任务;否则,就执行RejectedExecutionHandler
这里需要注意的是:在刚刚创建ThreadPoolExecutor的时候,线程并不会立即启动,而是要等到有任务提交时才会启动,除非调用了prestartCoreThread/prestartAllCoreThreads事先启动核心线程。再考虑到keepAliveTime和allowCoreThreadTimeOut超时参数的影响,所以没有任务需要执行的时候,线程池的大小不一定是corePoolSize。
corePoolSize:核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;
keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
通俗解释 :keepAliveTime的jdk中的解释为:当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。说的让人感觉比较模糊,总结一下大概意思为:比如说线程池中最大的线程数为50,而其中只有40个线程任务在跑,相当于有10个空闲线程,这10个空闲线程不能让他一直在开着,因为线程的存在也会特别耗资源的,所有就需要设置一个这个空闲线程的存活时间。