共计 17205 个字符,预计需要花费 44 分钟才能阅读完成。
上篇文章对并发的实践根底进行了回顾,次要是为什么应用多线程、多线程会引发什么问题及引发的起因,和怎么应用 Java 中的多线程去解决这些问题。
正所谓,知其然知其所以然,这是学习一个常识遵循的准则。
举荐读者后行查看并发编程的理论知识,以便能够丝滑入戏。
并发编程系列之一并发实践根底
本篇文章重点在于 Java 中怎么去应用多线程,和多线程的一些相干概念和操作,及怎么优化多线程。
在 Java 中每个对象都有其生命周期,线程同样不例外,也有其生命周期。
一、线程生命周期
线程的几种状态转换
1、新建(New)
新创建了一个线程对象,但还没有调用 start()办法。
2、就绪
当线程对象调用了 start()办法后,该线程就进入就绪状态。处于就绪状态的线程位于线程队列中,此时它只是具备了运行的条件,是否取得 CPU 的使用权并开始运行,还须要期待零碎的调度。
3、运行(Runnable)
如果处于就绪状态的线程取得了 CPU 的使用权,并开始执行 run()办法中的线程执行体,则该线程处于运行状态。
一个线程启动后,它可能不会始终处于运行状态,当运行状态的线程应用完零碎调配的工夫后,零碎就会剥夺该线程占用的 CPU 资源,让其余线程取得执行的机会。须要留神的是,
只有处于就绪状态的线程才可能转换到运行状态。
4、阻塞(Blocking)
期待获取一个排它锁,如果其线程开释了锁就会完结此状态。
①无限期期待(Waiting)
期待其它线程显式地唤醒,否则不会被调配 CPU 工夫片。
进入办法 | 退出办法 |
---|---|
没有设置 Timeout 参数的 Object.wait() 办法 | Object.notify() / Object.notifyAll() |
没有设置 Timeout 参数的 Thread.join() 办法 | 被调用的线程执行结束 |
LockSupport.park() 办法 | – |
②限期期待(Timed Waiting)
无需期待其它线程显式地唤醒,在肯定工夫之后会被零碎主动唤醒。
调用 Thread.sleep() 办法使线程进入限期期待状态时,经常用“使一个线程睡眠”进行形容。
调用 Object.wait() 办法使线程进入限期期待或者无限期期待时,经常用“挂起一个线程”进行形容。
睡眠和挂起是用来形容行为,而阻塞和期待用来形容状态。
阻塞和期待的区别在于,阻塞是被动的,它是在期待获取一个排它锁。而期待是被动的,通过调用 Thread.sleep() 和 Object.wait() 等办法进入。
进入办法 | 退出办法 |
---|---|
Thread.sleep() 办法 | 工夫完结 |
设置了 Timeout 参数的 Object.wait() 办法 | 工夫完结 / Object.notify() / Object.notifyAll() |
设置了 Timeout 参数的 Thread.join() 办法 | 工夫完结 / 被调用的线程执行结束 |
LockSupport.parkNanos() 办法 | – |
LockSupport.parkUntil() 办法 | – |
5、死亡(Terminated)
如果线程调用 stop()办法或 nun()办法失常执行结束,或者线程抛出一个未捕捉的异样 (Exception) 谬误(Error),线程就进入死亡状态。一旦进入死亡状态,线程将不再领有运行的资格,也不能再转换到其余状态。
了解线程的五种状态,在调用多线程的办法时,能分明的晓得以后处于哪个状态。
咱们举一个简略的实例来阐明每个状态。
public class MyThread extends Thread {
// 运行状态
public void run() {// ...}
public static void main(String[] args) {MyThread mt = new MyThread(); //1、新建状态
mt.start(); // 就绪状态}
}
在线程管制章节有一些办法,如 sleep()\join()办法,这些办法会让线程处于阻塞状态。
理解了线程的生成周期当前,接下来咱们就须要把握在 Java 中怎么应用多线程。
在 Java 中有三种形式实现多线程。
二、创立线程的三种形式
有三种应用线程的办法:
- 实现 Runnable 接口;
- 实现 Callable 接口;
- 继承 Thread 类。
实现 Runnable 和 Callable 接口的类只能当做一个能够在线程中运行的工作,不是真正意义上的线程,因而最初还须要通过 Thread 来调用。能够说工作是通过线程驱动从而执行的。
1、实现 Runnable 接口
须要实现 run() 办法。
通过 Thread 调用 start() 办法来启动线程。
public class MyRunnable implements Runnable {public void run() {// 须要执行多线程的业务逻辑}
}
public static void main(String[] args) {MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();}
2、实现 Callable 接口
与 Runnable 相比,Callable 能够有返回值,返回值通过 FutureTask 进行封装。
public class MyCallable implements Callable<Integer> {public Integer call() {return 123;}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
3、继承 Thread 类
同样也是须要实现 run() 办法,因为 Thread 类也实现了 Runable 接口。
当调用 start() 办法启动一个线程时,虚构机会将该线程放入就绪队列中期待被调度,当一个线程被调度时会执行该线程的 run() 办法。
public class MyThread extends Thread {public void run() {// ...}
}
public static void main(String[] args) {MyThread mt = new MyThread();
mt.start();}
4、实现接口 VS 继承 Thread
实现接口会更好一些,因为:
- Java 不反对多重继承,因而继承了 Thread 类就无奈继承其它类,然而能够实现多个接口;
- 类可能只要求可执行就行,继承整个 Thread 类开销过大。
三、线程管制
线程在应用过程中能对其灵便的管制,蕴含线程睡眠和线程退让等。
在学习线程的一些管制办法前,有一个必须要理解的前置常识,在线程中分为守护过程和非守护过程。
1、Daemon
守护线程是程序运行时在后盾提供服务的线程,不属于程序中不可或缺的局部。
当所有非守护线程完结时,程序也就终止,同时会杀死所有守护线程。
垃圾回收线程就是一个经典的守护线程,当咱们的程序中不再有任何运行的 Thread, 程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是 JVM 上仅剩的线程时,垃圾回收线程会主动来到。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
main() 属于非守护线程。
非守护线程能够转换为守护过程。
应用 setDaemon() 办法将一个线程设置为守护线程。
public static void main(String[] args) {Thread thread = new Thread(new MyRunnable());
thread.setDaemon(true);
}
2、sleep()
Thread.sleep(millisec) 办法会休眠以后正在执行的线程,millisec 单位为毫秒。
sleep() 可能会抛出 InterruptedException,因为异样不能跨线程流传回 main() 中,因而必须在本地进行解决。线程中抛出的其它异样也同样须要在本地进行解决。
public void run() {
try {Thread.sleep(3000);
} catch (InterruptedException e) {e.printStackTrace();
}
}
3、yield()
对静态方法 Thread.yield() 的调用申明了以后线程曾经实现了生命周期中最重要的局部,能够切换给其它线程来执行。该办法只是对线程调度器的一个倡议,而且也只是倡议具备雷同优先级的其它线程能够运行。
public void run() {Thread.yield();
}
4、join()
一旦这个线程执行了这个办法,只有这个线程处于死亡状态其余线程能力执行。
public class MyThread extends Thread {
11
12 public MyThread() {13}
14
15 public MyThread(String name) {16 super(name);
17 }
18
19 @Override
20 public void run() {21 for (int i = 0; i < 10; i++) {22 System.out.println(getName() + ":" + i);
23 }
24 }
25
26 public static void main(String[] args) {
27 // 1. 创立 MyThread 类的对象
28 MyThread myThread1 = new MyThread("线程 1");
29 MyThread myThread2 = new MyThread("线程 2");
30 MyThread myThread3 = new MyThread("线程 3");
31
32 // 2. 启动线程
33 myThread1.start();
34 try {
35 // 期待 myThread1 线程死亡, 只有当该线程死亡之后能力继续执行其它线程
36 myThread1.join();
37 } catch (InterruptedException e) {38 e.printStackTrace();
39 }
40 myThread2.start();
41 myThread3.start();
42
43 }
44 }
5、wait()\notify()
wait\notify\notifyAll 操作都是属于 Object 类提供的办法,即所有的对象都具备该办法,他们是的一对的,调用的时候不能离开呦。
wait(): 调用 wait 办法的线程,以后持有锁的该线程期待,直至该对象的另一个持锁线程调用 notify/notifyAll 操作。
wait(long timeOut)、wait(long timeOut,int nanos)
线程状态转换是,当 wait 被唤醒或超时,并不是间接进入到运行或者就绪状态,而是先进入到 Block 状态,抢锁胜利后,能力进入到可运行状态。
wait 办法在调用进入阻塞之前会开释锁,而 sleep 或 join 是不会开释锁的
notify(): 告诉持有该对象锁的所有线程中的的随便一个线程被唤醒
notifyAll(): 告诉持有该对象锁的所有线程被同时唤醒
咱们形象的做一个比喻:
如果把多线程比喻成一个运动员,跑道就是 CPU 每次只能容许一个运动员进入跑道,运动员的后勤保障就是守护过程,通过 setDaemon()办法,运动员就转业为了后勤人员。
执行 sleep()就是提前设定一个工夫,让运动员劳动会。wait()办法是运动员无限期的睡着,直到教练杀进去一脚踹醒(执行 notify 办法)运动员才会唤醒。
yield()会把跑道让给别的运动员。
join()办法会让运动员领有最高的跑道权限,我不跑完,谁都不能进来。
四、线程同步
Java 容许并发管制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不精确,相互之间产生抵触,因而退出同步锁以防止在该线程没有实现操作之前,被其余线程的调用,从而保障了该变量的唯一性和准确性。
Java 提供了两种锁机制来管制多个线程对共享资源的互斥拜访,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock。
1、synchronized
①. 同步一个代码块
public void func() {synchronized (this) {// ...}
}
它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步。
对于以下代码,应用 ExecutorService 执行了两个线程,因为调用的是同一个对象的同步代码块,因而这两个线程会进行同步,当一个线程进入同步语句块时,另一个线程就必须期待。
public class SynchronizedExample {public void func1() {synchronized (this) {for (int i = 0; i < 10; i++) {System.out.print(i + " ");
}
}
}
}
public static void main(String[] args) {SynchronizedExample e1 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> e1.func1());
executorService.execute(() -> e1.func1());
}
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
对于以下代码,两个线程调用了不同对象的同步代码块,因而这两个线程就不须要同步。从输入后果能够看出,两个线程穿插执行。
public static void main(String[] args) {SynchronizedExample e1 = new SynchronizedExample();
SynchronizedExample e2 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> e1.func1());
executorService.execute(() -> e2.func1());
}
0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
②. 同步一个办法
public synchronized void func () {// ...}
它和同步代码块一样,作用于同一个对象。
③. 同步一个类
public void func() {synchronized (SynchronizedExample.class) {// ...}
}
作用于整个类,也就是说两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步。
public class SynchronizedExample {public void func2() {synchronized (SynchronizedExample.class) {for (int i = 0; i < 10; i++) {System.out.print(i + " ");
}
}
}
}
public static void main(String[] args) {SynchronizedExample e1 = new SynchronizedExample();
SynchronizedExample e2 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> e1.func2());
executorService.execute(() -> e2.func2());
}
④. 同步一个静态方法
public synchronized static void fun() {// ...}
作用于整个类。
2、ReentrantLock
ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁。
public class LockExample {private Lock lock = new ReentrantLock();
public void func() {lock.lock();
try {for (int i = 0; i < 10; i++) {System.out.print(i + " ");
}
} finally {lock.unlock(); // 确保开释锁,从而防止产生死锁。}
}
}
public static void main(String[] args) {LockExample lockExample = new LockExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> lockExample.func());
executorService.execute(() -> lockExample.func());
}
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
3、比拟
①. 锁的实现 **
synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
②. 性能
新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。
③. 期待可中断
当持有锁的线程长期不开释锁的时候,正在期待的线程能够抉择放弃期待,改为解决其余事件。
ReentrantLock 可中断,而 synchronized 不行。
④. 偏心锁
偏心锁是指多个线程在期待同一个锁时,必须依照申请锁的工夫程序来顺次取得锁。
synchronized 中的锁是非偏心的,ReentrantLock 默认状况下也是非偏心的,然而也能够是偏心的。
⑤. 锁绑定多个条件
一个 ReentrantLock 能够同时绑定多个 Condition 对象。
4、应用抉择
除非须要应用 ReentrantLock 的高级性能,否则优先应用 synchronized。
这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地反对它,而 ReentrantLock 不是所有的 JDK 版本都反对。
并且应用 synchronized 不必放心没有开释锁而导致死锁问题,因为 JVM 会确保锁的开释。
如果并发的线程数量很多,并且每个线程都是执行一个工夫很短的工作就完结了,这样频繁创立线程就会大大降低零碎的效率,因为频繁创立线程和销毁线程须要工夫。
线程池就利用而生。
五、线程池
线程池围绕着一个外围的类 java.uitl.concurrent.ThreadPoolExecutor,咱们将它作为一个切入点揭开线程池的面纱。
1、外围线程类
java.uitl.concurrent.ThreadPoolExecutor 类是线程池中最外围的一个类,因而如果要透彻地理解 Java 中的线程池,必须先理解这个类。上面咱们来看一下 ThreadPoolExecutor 类的具体实现源码。
在 ThreadPoolExecutor 类中有四个构造方法。
其中三个最终都是调用了上面这个构造方法,限于篇幅就不在贴其余三个源码了,读者能够进行求证。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
上面解释下一下结构器中各个参数的含意:
- 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 种动态属性:
TimeUnit.DAYS; // 天 TimeUnit.HOURS; // 小时 TimeUnit.MINUTES; // 分钟 TimeUnit.SECONDS; // 秒 TimeUnit.MILLISECONDS; // 毫秒 TimeUnit.MICROSECONDS; // 奥妙 TimeUnit.NANOSECONDS; // 纳秒
-
workQueue:一个阻塞队列,用来存储期待执行的工作,这个参数的抉择也很重要,会对线程池的运行过程产生重大影响,一般来说,察看传入的 workQueue 都是默认,即最大可增加 Integer.MAX_VALUE 个工作,所有在应用过程中要防止应用默认线程池。这里的阻塞队列有以下几种抉择:
ArrayBlockingQueue; LinkedBlockingQueue; SynchronousQueue; ArrayBlockingQueue 和 PriorityBlockingQueue 应用较少,个别应用 LinkedBlockingQueue 和 Synchronous。线程池的排队策略与 BlockingQueue 无关。
- threadFactory:线程工厂,次要用来创立线程;
- handler:示意当回绝解决工作时的策略,有以下四种取值:
ThreadPoolExecutor.AbortPolicy: 抛弃工作并抛出 RejectedExecutionException 异样。ThreadPoolExecutor.DiscardPolicy:也是抛弃工作,然而不抛出异样。ThreadPoolExecutor.DiscardOldestPolicy:抛弃队列最后面的工作,而后从新尝试执行工作(反复此过程)ThreadPoolExecutor.CallerRunsPolicy:由调用线程解决该工作
以上对结构的七个参数进行了介绍,那么这些参数是怎么起作用的呢,咱们接着看线程池的执行流程。
2、线程执行流程
- 当线程池小于 corePoolSize 时,新提交工作将创立一个新线程执行工作,即便此时线程池中存在闲暇线程。
- 当线程池达到 corePoolSize 时,新提交工作将被放入 workQueue 中,期待线程池中任务调度执行
- 当 workQueue 已满,且 maximumPoolSize>corePoolSize 时,新提交工作会创立新线程执行工作
- 当提交工作数超过 maximumPoolSize 时,新提交工作由 RejectedExecutionHandler 解决
- 当线程池中超过 corePoolSize 线程,闲暇工夫达到 keepAliveTime 时,开释闲暇线程
- 当设置 allowCoreThreadTimeOut(true)时,该参数默认 false,线程池中 corePoolSize 线程闲暇工夫达到 keepAliveTime 也将敞开
3、四种线程池及应用场景
Java 通过 Executors 提供四种线程池,别离为
- newSingleThreadExecutor 创立一个单线程化的线程池,它只会用惟一的工作线程来执行工作,保障所有工作依照指定程序 (FIFO, LIFO, 优先级) 执行。
- newFixedThreadPool 创立一个定长线程池,可控制线程最大并发数,超出的线程会在队列中期待。
- newScheduledThreadPool 创立一个可定期或者延时执行工作的定长线程池,反对定时及周期性工作执行。
- newCachedThreadPool 创立一个可缓存线程池,如果线程池长度超过解决须要,可灵便回收闲暇线程,若无可回收,则新建线程。
newCachedThreadPool:
- 底层:返回 ThreadPoolExecutor 实例,corePoolSize 为 0;maximumPoolSize 为 Integer.MAX_VALUE;keepAliveTime 为 60L;工夫单位 TimeUnit.SECONDS;workQueue 为 SynchronousQueue(同步队列)
- 艰深:当有新工作到来,则插入到 SynchronousQueue 中,因为 SynchronousQueue 是同步队列,因而会在池中寻找可用线程来执行,若有能够线程则执行,若没有可用线程则创立一个线程来执行该工作;若池中线程闲暇工夫超过指定工夫,则该线程会被销毁。
-
实用:执行很多短期的异步工作
/** * 1. 创立一个可缓存的线程池。如果线程池的大小超过了解决工作所须要的线程,那么就会回收局部闲暇(60 秒不执行工作)的线程 <br> * 2. 当工作数减少时,此线程池又能够智能的增加新线程来解决工作 <br> * 3. 此线程池不会对线程池大小做限度,线程池大小齐全依赖于操作系统(或者说 JVM)可能创立的最大线程大小 <br> */ public static void cacheThreadPool() {ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); for (int i = 1; i <= 10; i++) { final int ii = i; try {Thread.sleep(ii * 1); } catch (InterruptedException e) {e.printStackTrace(); } cachedThreadPool.execute(()->out.println("线程名称:" + Thread.currentThread().getName() + ",执行" + ii)); } } -----output------ 线程名称:pool-1-thread-1,执行 1 线程名称:pool-1-thread-1,执行 2 线程名称:pool-1-thread-1,执行 3 线程名称:pool-1-thread-1,执行 4 线程名称:pool-1-thread-1,执行 5 线程名称:pool-1-thread-1,执行 6 线程名称:pool-1-thread-1,执行 7 线程名称:pool-1-thread-1,执行 8 线程名称:pool-1-thread-1,执行 9 线程名称:pool-1-thread-1,执行 10
newFixedThreadPool:
- 底层:返回 ThreadPoolExecutor 实例,接管参数为所设定线程数量 n,corePoolSize 和 maximumPoolSize 均为 n;keepAliveTime 为 0L;工夫单位 TimeUnit.MILLISECONDS;WorkQueue 为:new LinkedBlockingQueue<Runnable>() 无界阻塞队列
- 艰深:创立可包容固定数量线程的池子,每个线程的存活工夫是有限的,当池子满了就不再增加线程了;如果池中的所有线程均在忙碌状态,对于新工作会进入阻塞队列中(无界的阻塞队列)
-
实用:执行长期工作
/** * 1. 创立固定大小的线程池。每次提交一个工作就创立一个线程,直到线程达到线程池的最大大小 <br> * 2. 线程池的大小一旦达到最大值就会放弃不变,如果某个线程因为执行异样而完结,那么线程池会补充一个新线程 <br> * 3. 因为线程池大小为 3,每个工作输入 index 后 sleep 2 秒,所以每两秒打印 3 个数字,和线程名称 <br> */ public static void fixTheadPoolTest() {ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3); for (int i = 0; i < 10; i++) { final int ii = i; fixedThreadPool.execute(() -> {out.println("线程名称:" + Thread.currentThread().getName() + ",执行" + ii); try {Thread.sleep(2000); } catch (InterruptedException e) {e.printStackTrace(); } }); } } ------output------- 线程名称:pool-1-thread-3,执行 2 线程名称:pool-1-thread-1,执行 0 线程名称:pool-1-thread-2,执行 3 线程名称:pool-1-thread-3,执行 4 线程名称:pool-1-thread-1,执行 5 线程名称:pool-1-thread-2,执行 6 线程名称:pool-1-thread-3,执行 7 线程名称:pool-1-thread-1,执行 8 线程名称:pool-1-thread-3,执行 9
newSingleThreadExecutor:
- 底层:FinalizableDelegatedExecutorService 包装的 ThreadPoolExecutor 实例,corePoolSize 为 1;maximumPoolSize 为 1;keepAliveTime 为 0L;工夫单位 TimeUnit.MILLISECONDS;workQueue 为:new LinkedBlockingQueue<Runnable>() 无解阻塞队列
- 艰深:创立只有一个线程的线程池,当该线程正忙碌时,对于新工作会进入阻塞队列中(无界的阻塞队列)
- 实用:按程序执行工作的场景
/** * 创立一个单线程化的线程池,它只会用惟一的工作线程来执行工作,保障所有工作依照指定程序 (FIFO, LIFO, 优先级) 执行
*/
public static void singleTheadPoolTest() {ExecutorService pool = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int ii = i;
pool.execute(() -> out.println(Thread.currentThread().getName() + "=>" + ii));
}
}
-----output-------
线程名称:pool-1-thread-1,执行 0
线程名称:pool-1-thread-1,执行 1
线程名称:pool-1-thread-1,执行 2
线程名称:pool-1-thread-1,执行 3
线程名称:pool-1-thread-1,执行 4
线程名称:pool-1-thread-1,执行 5
线程名称:pool-1-thread-1,执行 6
线程名称:pool-1-thread-1,执行 7
线程名称:pool-1-thread-1,执行 8
线程名称:pool-1-thread-1,执行 9
NewScheduledThreadPool:
- 底层:创立 ScheduledThreadPoolExecutor 实例,该对象继承了 ThreadPoolExecutor,corePoolSize 为传递来的参数,maximumPoolSize 为 Integer.MAX_VALUE;keepAliveTime 为 0;工夫单位 TimeUnit.NANOSECONDS;workQueue 为:new DelayedWorkQueue() 一个按超时工夫升序排序的队列
- 艰深:创立一个固定大小的线程池,线程池内线程存活工夫无限度,线程池能够反对定时及周期性工作执行,如果所有线程均处于忙碌状态,对于新工作会进入 DelayedWorkQueue 队列中,这是一种依照超时工夫排序的队列构造
- 实用:执行周期性工作
/**
* 创立一个定长线程池,反对定时及周期性工作执行。提早执行
*/
public static void sceduleThreadPool() {ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
Runnable r1 = () -> out.println("线程名称:" + Thread.currentThread().getName() + ",执行:3 秒后执行");
scheduledThreadPool.schedule(r1, 3, TimeUnit.SECONDS);
Runnable r2 = () -> out.println("线程名称:" + Thread.currentThread().getName() + ",执行: 提早 2 秒后每 3 秒执行一次");
scheduledThreadPool.scheduleAtFixedRate(r2, 2, 3, TimeUnit.SECONDS);
Runnable r3 = () -> out.println("线程名称:" + Thread.currentThread().getName() + ",执行: 一般工作");
for (int i = 0; i < 5; i++) {scheduledThreadPool.execute(r3);
}
}
----output------
线程名称:pool-1-thread-1,执行: 一般工作
线程名称:pool-1-thread-5,执行: 一般工作
线程名称:pool-1-thread-4,执行: 一般工作
线程名称:pool-1-thread-3,执行: 一般工作
线程名称:pool-1-thread-2,执行: 一般工作
线程名称:pool-1-thread-1,执行: 提早 2 秒后每 3 秒执行一次
线程名称:pool-1-thread-5,执行:3 秒后执行
线程名称:pool-1-thread-4,执行: 提早 2 秒后每 3 秒执行一次
线程名称:pool-1-thread-4,执行: 提早 2 秒后每 3 秒执行一次
线程名称:pool-1-thread-4,执行: 提早 2 秒后每 3 秒执行一次
线程名称:pool-1-thread-4,执行: 提早 2 秒后每 3 秒执行一次
5、应用实例
在 ThreadPoolTaskExecutor 的原理章节中,有一系列的办法,如果咱们手动调用这些线程池办法实现办法是极其简单的。
①、在 java 中的应用
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+"执行结束");
}
}
从执行后果能够看出,当线程池中线程的数目大于 5 时,便将工作放入工作缓存队列外面,当工作缓存队列满了之后,便创立新的线程。如果下面程序中,将 for 循环中改成执行 20 个工作,就会抛出工作回绝异样了。
不过在 java doc 中,并不提倡咱们间接应用 ThreadPoolExecutor,而是应用 Executors 类中提供的几个静态方法来创立线程池:
Executors.newCachedThreadPool(); // 创立一个缓冲池,缓冲池容量大小为 Integer.MAX_VALUE
Executors.newSingleThreadExecutor(); // 创立容量为 1 的缓冲池
Executors.newFixedThreadPool(int); // 创立固定容量大小的缓冲池
从它们的具体实现来看,它们实际上也是调用了 ThreadPoolExecutor,只不过参数都已配置好了。
newFixedThreadPool 创立的线程池 corePoolSize 和 maximumPoolSize 值是相等的,它应用的 LinkedBlockingQueue;
newSingleThreadExecutor 将 corePoolSize 和 maximumPoolSize 都设置为 1,也应用的 LinkedBlockingQueue;
newCachedThreadPool 将 corePoolSize 设置为 0,将 maximumPoolSize 设置为 Integer.MAX_VALUE,应用的 SynchronousQueue,也就是说来了工作就创立线程运行,当线程闲暇超过 60 秒,就销毁线程。
理论中,如果 Executors 提供的三个静态方法能满足要求,就尽量应用它提供的三个办法,因为本人去手动配置 ThreadPoolExecutor 的参数有点麻烦,要依据理论工作的类型和数量来进行配置。
另外,如果 ThreadPoolExecutor 达不到要求,能够本人继承 ThreadPoolExecutor 类进行重写。
②、在 Spring 中应用
以下为 Java 线程池在 Spring 中的应用,ThreadPoolTaskExecutor 一个对象注入到 Spring 的容器中。
/**
* 线程池配置
*
* @author tcy
**/
@Configuration
public class ThreadPoolConfig {
// 外围线程池大小
private final int corePoolSize = 50;
// 最大可创立的线程数
private final int maxPoolSize = 200;
// 队列最大长度
private final int queueCapacity = 1000;
// 线程池保护线程所容许的闲暇工夫
private final int keepAliveSeconds = 300;
@Bean(name = "threadPoolTaskExecutor")
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setMaxPoolSize(maxPoolSize);
executor.setCorePoolSize(corePoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setKeepAliveSeconds(keepAliveSeconds);
// 线程池对回绝工作 (无线程可用) 的解决策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
在办法或者类上加 @Async 注解,表明该办法或类为多线程办法,Spirng 外部会主动调用多线程的回绝策略、线程初始化等办法。