多线程学习
一、多线程概念
1.1、多线程定义
- 程序:是为完成特定任务,用某种语言编写的一组指令的集合,即指一段静态的代码,静态对象。
- 进程:是正在运行的程序
- 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位
- 动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的
- 并发性:任何进程都可以同其他进程一起并发执行
- 线程:是进程中的单个顺序控制流,是一条执行路径
- 单线程:一个进程如果只有一条执行路径,则称为单线程程序
- 多线程:一个进程如果有多条执行路径,则称为多线程程序
- 并行:在同一时刻,有多个指令在多个CPU上同时执行。
- 并发:在同一时刻,有多个指令在单个CPU上交替执行。
1.2、多线程的调度方式
- 分时调度模式:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
- 抢占调度模式:优先让优先级较高的线程使用 CPU,如果优先级相同,那么会随机选择一个。
- Java 使用的就是抢占式调度模型,线程的执行是随机的。
- 线程默认优先级是 5,优先级范围是 1-10。
- 当运行的线程都是守护线程时,JVM 将退出。
二、实现多线程的多种方式
2.1、继承 Thred 类
方法名 说明
void run() 在线程开启后,此方法将被调用执行
void start() 使此线程开始执行,Java虚拟机会调用run方法()
- 实现步骤
- 定义一个类MyThread继承Thread类
- 在MyThread类中重写run()方法
- 创建MyThread类的对象
- 启动线程
- 代码演示
public class MyThread extends Thread {
@Override
public void run() {
for(int i=0; i<100; i++) {
System.out.println(i);
}
}
}
public class MyThreadDemo {
public static void main(String[] args) {
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
// my1.run();
// my2.run();
//void start() 导致此线程开始执行; Java虚拟机调用此线程的run方法
my1.start();
my2.start();
}
}
- 注意:
- 重写 run()方法:因为 run()是用来封装被线程执行的代码。
- run():封装被线程执行的代码,直接调用,相当于普通方法的调用。
- start():启动线程,然后由 JVM 调动此线程的 run()方法。
2.2、实现 Runnable 接口
- Thread构造方法
方法名 说明
Thread(Runnable target) 分配一个新的Thread对象
Thread(Runnable target, String name) 分配一个新的Thread对象和名称
- 实现步骤
- 定义一个类MyRunnable实现Runnable接口
- 在MyRunnable类中重写run()方法
- 创建MyRunnable类的对象
- 创建Thread类的对象,把MyRunnable对象作为构造方法的参数
- 启动线程
- 代码演示
public class MyRunnable implements Runnable {
@Override
public void run() {
for(int i=0; i<100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
public class MyRunnableDemo {
public static void main(String[] args) {
//创建MyRunnable类的对象
MyRunnable my = new MyRunnable();
//创建Thread类的对象,把MyRunnable对象作为构造方法的参数
//Thread(Runnable target)
// Thread t1 = new Thread(my);
// Thread t2 = new Thread(my);
//Thread(Runnable target, String name)
Thread t1 = new Thread(my,"坦克");
Thread t2 = new Thread(my,"飞机");
//启动线程
t1.start();
t2.start();
}
}
2.3、实现 Callable 接口
- 方法介绍
方法名 说明
V call() 计算结果,如果无法计算结果,则抛出一个异常
FutureTask(Callable callable) 创建一个 FutureTask,一旦运行就执行给定的 Callable
V get() 如有必要,等待计算完成,然后获取其结果
- 实现步骤
- 定义一个类MyCallable实现Callable接口
- 在MyCallable类中重写call()方法
- 创建MyCallable类的对象
- 创建Future的实现类FutureTask对象,把MyCallable对象作为构造方法的参数
- 创建Thread类的对象,把FutureTask对象作为构造方法的参数
- 启动线程
- 再调用get方法,就可以获取线程结束之后的结果。
- 代码演示
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println("跟女孩表白" + i);
}
//返回值就表示线程运行完毕之后的结果
return "答应";
}
}
public class Demo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//线程开启之后需要执行里面的call方法
MyCallable mc = new MyCallable();
//Thread t1 = new Thread(mc);
//可以获取线程执行完毕之后的结果.也可以作为参数传递给Thread对象
FutureTask<String> ft = new FutureTask<>(mc);
//创建线程对象
Thread t1 = new Thread(ft);
String s = ft.get();
//开启线程
t1.start();
//String s = ft.get();
System.out.println(s);
}
}
2.4、使用线程池
- 线程池介绍
Executors 工具类 线程池的工厂类,用于创建并返回不同类型的线程池
Executors.newCachedThreadPool() 创建一个可根据需要创建新线程的线程池
Executors.newFixedThreadPool(n) 创建一个可重用固定线程数的线程池
Executors.newSingleThreadExecutor() 创建一个只有一个线程的线程池
Executors.newScheduledThreadPool(n 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行
- 实现步骤
- 创建实现 runnable 或 callable 接口方式的对象
- 创建 executorservice 线程池
- 将创建好的实现了 runnable 接口的对象 放入 executorservice 对象的 execute 方法中执行
- 关闭线程池
- 代码演示
public class NumberThread implements Runnable{
@Override
public void run() {
for(int i = 0;i<=100;i++){
if (i % 2 ==0 )
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
public class NumberThread1 implements Runnable{
@Override
public void run() {
for(int i = 0;i<100; i++){
if(i%2==1){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args){
//创建固定线程个数为十个的线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
//new一个Runnable接口的对象
NumberThread number = new NumberThread();
NumberThread1 number1 = new NumberThread1();
//执行线程,最多十个
executorService.execute(number1);
executorService.execute(number);//适合适用于Runnable
//executorService.submit();//适合使用于Callable
//关闭线程池
executorService.shutdown();
}
}
2.5、不同实现方式对比
1、三种不同实现方式对比
-
实现Runable、Callable接口
- 好处:扩展性强,实现该接口的同时还可以继承其他的类
- 缺点: 编程相对复杂,不能直接使用Thread类中的方法
-
继承 Thread 类
- 好处: 编程比较简单,可以直接使用Thread类中的方法
- 缺点: 可以扩展性较差,不能再继承其他的类
2、runnable 和 callable 有什么区别
-
Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
-
Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息。
-
注:Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
三、Thred 中常见的方法
3.1、设置和获取线程名称
方法介绍
方法名 说明
void setName(String name) 将此线程的名称更改为等于参数name
String getName() 返回此线程的名称
Thread currentThread() 返回对当前正在执行的线程对象的引用
- 代码演示
public class MyThread extends Thread {
public MyThread() {}
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName()+":"+i);
}
}
}
public class MyThreadDemo {
public static void main(String[] args) {
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
//void setName(String name):将此线程的名称更改为等于参数 name
my1.setName("高铁");
my2.setName("飞机");
//Thread(String name)
MyThread my1 = new MyThread("高铁");
MyThread my2 = new MyThread("飞机");
my1.start();
my2.start();
//static Thread currentThread() 返回对当前正在执行的线程对象的引用
System.out.println(Thread.currentThread().getName());
}
}
- 3.2、线程休眠
相关方法
方法名 说明
static void sleep(long millis) 使当前正在执行的线程停留(暂停执行)指定的毫秒数
- 代码演示
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "---" + i);
}
}
}
public class Demo {
public static void main(String[] args) throws InterruptedException {
/*System.out.println("睡觉前");
Thread.sleep(3000);
System.out.println("睡醒了");*/
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
t1.start();
t2.start();
}
}
3.3、线程优先级
- 线程调度
- 两种调度方式
- 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
- 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些
- Java使用的是抢占式调度模型
- 随机性
假如计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令。所以说多线程程序的执行是有随机性,因为谁抢到CPU的使用权是不一定的
- 两种调度方式
- 优先级相关方法
方法名 说明
final int getPriority() 返回此线程的优先级
final void setPriority(int newPriority) 更改此线程的优先级线程默认优先级是5;线程优先级的范围是:1-10
- 代码演示
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "---" + i);
}
return "线程执行完毕了";
}
}
public class Demo {
public static void main(String[] args) {
//优先级: 1 - 10 默认值:5
MyCallable mc = new MyCallable();
FutureTask<String> ft = new FutureTask<>(mc);
Thread t1 = new Thread(ft);
t1.setName("飞机");
t1.setPriority(10);
//System.out.println(t1.getPriority());//5
t1.start();
MyCallable mc2 = new MyCallable();
FutureTask<String> ft2 = new FutureTask<>(mc2);
Thread t2 = new Thread(ft2);
t2.setName("坦克");
t2.setPriority(1);
//System.out.println(t2.getPriority());//5
t2.start();
}
}
3.4、守护线程
相关方法
方法名 说明
void setDaemon(boolean on) 将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出
- 代码演示
public class MyThread1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName() + "---" + i);
}
}
}
public class MyThread2 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "---" + i);
}
}
}
public class Demo {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
MyThread2 t2 = new MyThread2();
t1.setName("女神");
t2.setName("备胎");
//把第二个线程设置为守护线程
//当普通线程执行完之后,那么守护线程也没有继续运行下去的必要了.
t2.setDaemon(true);
t1.start();
t2.start();
}
}
四、线程同步
4.1、线程安全问题
1、线程安全问题定义
- 当多个线程对同一个对象的实例变量,做写(修改)的操作时,可能会受到其他线程的干扰,发生线程安全的问题。
2、安全问题出现的条件
- 是多线程环境
- 有共享数据
- 有多条语句操作共享数据
3、安全问题的特性
- 原子性(Atomic):
- 不可分割,访问(读,写)某个共享变量的时候,从其他线程来看,该操作要么已经执行完毕,要么尚未发生。其他线程看不到当前操作的中间结果。 访问同一组共享变量的原子操作是不能够交错的,如现实生活中从ATM取款。
java中有两种方式实现原子性:
1.锁 :锁具有排他性,可以保证共享变量某一时刻只能被一个线程访问。
2.CAS指令 :直接在硬件层次上实现,看做是一个硬件锁。
- 可见性(visbility):
- 在多线程环境中,一个线程对某个共享变量更新之后,后续其他的线程可能无法立即读到这个更新的结果。
- 如果一个线程对共享变量更新之后,后续访问该变量的其他线程可以读到更新的结果,称这个线程对共享变量的更新对其他线程可见。否则称这个线程对共享变量的更新对其他线程不可见。
- 多线程程序因为可见性问题可能会导致其他线程读取到旧数据(脏数据)。
- 有序性(Ordering):
- 是指在什么情况下一个处理器上运行的一个线程所执行的 内存访问操作在另外一个处理器运行的其他线程来看是乱序的(Out of Order)
- 乱序: 是指内存访问操作的顺序看起来发生了变化。
4.2、线程同步
1、锁得概念
- 线程安全问题的产生前提是多个线程并发访问共享数据,** 将多个线程对数据得并发访问转为串行访问,即一个共享数据一次只能被一个线程访问,锁就是用这种思路来保护线程安全的。 **
- 一个线程只能有锁的时候才能对共享数据进行访问,访问结束后必须释放锁。
- 锁具有排他性 ,即一个锁只能被一个线程持有,这种锁被称为 互斥锁 。
- 锁的作用 :锁可以实现对共享数据的安全访问,保证线程的 原子性 、可见性 、 有序性 。
2、 synchronized锁
- synchronized 修饰代码块:同步代码块
- 被修饰的代码被称为同步代码块,其作用域是大括号 { } 括起来的代码,作用的对象是调用这个代码块的对象。
- synchronized修饰一个方法:同步方法
- 被修饰的方法称为同步方法,其作用域是整个方法,作用的对象是调用这个方法的对象。
- synchronized修饰一个静态方法:
- 其作用域是整个静态方法,作用的对象是这个类的所有对象。
- synchronized修饰一个类:同步类
- 其作用域是 synchronized 后面括号括起来的部分,作用的对象的这个类的所有对象。
- 线程出现异常会自动释放锁
3、 volatile关键字
- volatile可以保证内存可见性且禁止重排序,可以强制线程从公共内存种读取变量的值,而不是从工作内存种读取。
- 内存可见性:指的是线程之间的可见性,当一个线程修改共享变量是,另一个线程可以读取到这个修改后的值。
- synchronized 关键字不仅可以保证可加性,同时也保证了原子性(互斥性)。
- volatile和 synchronized比较
- volatile只能修饰变量,而 synchronized可以修饰方法、代码块、类。
- 多线程访问 volatile变量不会发生阻塞,而 synchronized可能会阻塞。
- volatile能保证数据的可见性,不能保证原子性,synchronized都可以保证,会同步数据。
- volatile解决的是变量在多个线程之间的可见性,synchronized解决的是多个线程之间访问公共资源的同步性。
4、 Lock锁
- 代码演示
package com.example.paoduantui.Thread;
import java.util.concurrent.locks.ReentrantLock;
class Window implements Runnable{
private int ticket = 100;//定义一百张票 //1.实例化锁 ReentrantLock 可重入锁 // Lock :一个接口 private Lock lock = new ReentrantLock();
@Override public void run() {
while (true) {
//2.调用锁定方法lock lock.lock();
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售出第" + ticket + "张票");
ticket--;
} else {
break;
}
}
}
}
public class LockTest {
public static void main(String[] args){
Window w= new Window();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口1");
t3.setName("窗口1");
t1.start();
t2.start();
t3.start();
}
}
- synchronized和 Lock锁比较
- synchronized当相应代码执行完之后,会自动释放锁;Lock 需要用户去手动释放锁。
- synchronized是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
- ReentrantLock 构造参数,true--公平锁 、false--非公平锁
5、CAS 逻辑锁
CAS的全称是:比较并交换(Compare And Swap)。在CAS中,有这样三个值:
- V:要更新的变量(var)
- E:预期值(expected)
- N:新值(new)
比较并交换的过程如下:
判断V是否等于E,如果等于,将V的值设置为N;如果不等,说明已经有其它线程更新了V,则当前线程放弃更新,什么都不做。
- ABA 问题:
- 有一个共享变量 count =0,A线程对count的值修改为10,B线程对count修改为20,C线程对count修改为10; 如果当前线程看到count变量的值为10,我们是否认为count变量的值没有被其他线程更新呢??这种结果是否能接受??
- ABA 解决方案:
- 可以为共享变量引入一个修订号(时间戳),每次修改共享变量时,相应的修订号就会增加1。
6、公平/非公平锁
- 公平锁 :指的是多个线程按照申请锁的顺序来获取锁,线程直接进入队列种排队,队列种的第一个线程才能获得锁。
- 优点:等待线程不会等待超长时间
- 缺点:整体吞吐效率相对非公平锁要低。
- 非公平锁:指的是多个线程加锁时直接尝试获取锁,获取不到才会到队尾等待。但如果此时锁刚好可用,那么这个线程无需阻塞可以直接获取到锁。
- 优点:整体吞吐率高、减少唤起线程开销。
- 缺点:等待队列中的线程可能会饿死,或者等很久才会获得锁。
7、死锁
- 死锁产生必要条件
- 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
- 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
- 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
- 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
- 避免死锁
- 加锁顺序
- 加锁时限
- 死锁检测
五、线程通信
5.1、等待通知机制实现
- Object 类中的 wait()方法,可以使当前执行代码的线程等待,暂停执行。直到接收到通知或者终端位置。
- wait()方法只能在同步代码块中由锁对象调用。
- 调用wait()方法后,当前线程会释放锁。、
- Object 类的 notify()可以唤醒线程。
- 改方法也必须在同步代码块中由锁对象调用。
- 没有锁对象调用 wait()/notify()会抛出异常。
- 如果有多个等待的线程,notify()方法只能唤醒其中的一个,并不会立即释放锁对象。
- 一般将 notify()方法放在同步代码块的最后。
- 代码演示
package se.high.thread.wait;
/** * @author 结构化思维wz * 用notify唤醒等待的线程 */public class Test01 {
public static void main(String[] args) {
String lock = "wzjiayou"; //定义一个字符串作为锁对象 Thread t1 = new Thread(new Runnable() {
@Override public void run() {
synchronized (lock){
System.out.println("线程1开始等待-->"+System.currentTimeMillis());
try {
lock.wait(); //线程等待 } catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1结束等待-->"+System.currentTimeMillis());
}
}
});
/** * 定义线程2,用来唤醒线程1 */ Thread t2 = new Thread(new Runnable() {
@Override public void run() {
//notify需要在同步代码块中由锁对象调用 synchronized (lock){
System.out.println("线程2开始唤醒"+System.currentTimeMillis());
lock.notify();
System.out.println("线程2结束唤醒"+System.currentTimeMillis());
}
}
});
t1.start(); //开启t1线程,main线程谁3秒,确保t1等待 try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
- notify()/notifyAll()
- notify():一次只能唤醒一个线程。
- notifyAll(): 可以唤醒所有线程。
- wait(long)
- 如果在参数指定时间内没有被唤醒,超时后会自动唤醒。
- sleep() 和 wait()
- 相同点:
- 一旦执行方法以后,都会使得当前的进程进入阻塞状态
- 相同点:
- 不同点:
- 两个方法声明的位置不同,Thread类中声明sleep,Object类中声明wait。
- 调用的要求不同,sleep可以在任何需要的场景下调用,wait必须使用在同步代码块或者同步方法中
- 关于是否释放同步监视器,如果两个方法都使用在同步代码块或同步方法中,sleep不会释放,wait会释放
六、线程池
6.1、线程池概念
1、线程池定义
- 线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。
- 线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。
- 如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。
- 如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。
- 超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。
2、使用线程池原因:
- 创建/销毁线程需要消耗系统资源,线程池可以 复用已创建的线程。
- 控制并发的数量,并发数量过多,可能会导致资源消耗过多,从而造成服务器崩溃。
- 可以对线程做统一管理。
6.2、四种常见的线程
- 常用线程池
Executors 工具类 线程池的工厂类,用于创建并返回不同类型的线程池
Executors.newCachedThreadPool() 创建一个可根据需要创建新线程的线程池
Executors.newFixedThreadPool(n) 创建一个可重用固定线程数的线程池
Executors.newSingleThreadExecutor() 创建一个只有一个线程的线程池
Executors.newScheduledThreadPool(n) 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行
6.3、线程池参数解析
- Java中的线程池顶层接口是Executor接口,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;
}
- int corePoolSize :该线程池中核心线程数最大值
- 核心线程:线程池中有两类线程,核心线程和非核心线程。核心线程默认情况下会一直存在于线程池中,即使这个核心线程什么都不干(铁饭碗),而非核心线程如果长时间的闲置,就会被销毁(临时工)。
- int maximumPoolSize :该线程池中线程总数最大值 。
- 该值等于核心线程数量 + 非核心线程数量。
- long keepAliveTime:非核心线程闲置超时时长。
- 非核心线程如果处于闲置状态超过该值,就会被销毁。如果设置allowCoreThreadTimeOut(true),则会也作用于核心线程。
- TimeUnit unit:keepAliveTime的单位。
- TimeUnit是一个枚举类型 ,包括以下属性:
- NANOSECONDS : 1微毫秒 = 1微秒 / 1000 ; MICROSECONDS :1微秒 = 1毫秒 / 1000 ;MILLISECONDS : 1毫秒 = 1秒 /1000 ;SECONDS : 秒; MINUTES : 分 ;HOURS : 小时 ;DAYS : 天
- TimeUnit是一个枚举类型 ,包括以下属性:
- BlockingQueue workQueue:阻塞队列,维护着等待执行的Runnable任务对象。
- 常用的几个阻塞队列:
- LinkedBlockingQueue:链式阻塞队列,底层数据结构是链表,默认大小是Integer.MAX_VALUE,也可以指定大小。
- ArrayBlockingQueue:数组阻塞队列,底层数据结构是数组,需要指定队列的大小。
- SynchronousQueue:同步队列,内部容量为0,每个put操作必须等待一个take操作,反之亦然。
- DelayQueue:延迟队列,该队列中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素 。
- 常用的几个阻塞队列:
- ThreadFactory threadFactory:创建线程的工厂 ,用于批量创建线程,统一在创建线程时设置一些参数,如是否守护线程、线程的优先级等。如果不指定,会新建一个默认的线程工厂。
- RejectedExecutionHandler handler:拒绝处理策略,线程数量大于最大线程数就会采用拒绝处理策略,四种拒绝处理的策略为 :
- ThreadPoolExecutor.AbortPolicy:默认拒绝处理策略,丢弃任务并抛出RejectedExecutionException异常。
- ThreadPoolExecutor.DiscardPolicy:丢弃新来的任务,但是不抛出异常。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列头部(最旧的)的任务,然后重新尝试执行程序(如果再次失败,重复此过程)。
- ThreadPoolExecutor.CallerRunsPolicy:由调用主线程处理该任务。
6.4、线程池主要的任务处理流程
- 线程总数量 < corePoolSize(核心线程),无论线程是否空闲,都会新建一个核心线程执行任务(让核心线程数量快速达到corePoolSize,在核心线程数量 < corePoolSize时)。注意,这一步需要获得全局锁。
- 线程总数量 >= corePoolSize时,新来的线程任务会进入任务队列中等待,然后空闲的核心线程会依次去缓存队列中取任务来执行(体现了线程复用)。
- 当缓存队列满了,说明这个时候任务已经多到爆棚,需要一些“临时工”来执行这些任务了。于是会创建非核心线程去执行这个任务。注意,这一步需要获得全局锁。
- 缓存队列满了, 且总线程数达到了maximumPoolSize,则会采取上面提到的拒绝策略进行处理。