1 线程
1.1 相关方法
| 方法 | 作用 | 备注 |
|---|---|---|
| getId() | 获取线程的ID | 1)某线程执行完毕后,其ID可能会被后续新的线程使用 2)重启JVM后,同一个线程的ID可能不一样 |
| setPriority() | 设置线程的优先级,取值范围是[1, 10] | 优先级越高,得到CPU调度的概率越高 |
| setDaemon(true) | 设置线程为守护线程 | 1)守护线程不能单独运行,如果所有用户线程执行结束,则JVM会关闭,守护线程也将结束 2)该方法需要在 start()之前执行才有效 |
| interrupt() | 给线程设置一个中断标志 | 1)并不是真正中断线程,而是打上了一个标志 2)可通过 isInterrupted()判断该标志来手动退出,如下例子3)该方法会中断 wait()方法,见章节4.1的例1 |
// 通过判断中断标志来手动退出
public class MyThead extends Thread {
@Override
public void run() {
super.run();
for (int i = 0; i < 100000; i++) {
System.out.println("子线程:" + i);
if (this.isInterrupted()) {
return;
}
}
}
}
public class Test {
public static void main(String[] args) {
MyThead t1 = new MyThead();
t1.start();
for (int i = 0; i < 10; i++) {
System.out.println("main线程:" + i);
}
t1.interrupt();
}
}
1.2 生命周期
可通过getState()获取所在状态:
-
NEW:新建状态,还未执行
start()方法 -
RUNNABLE:可运行状态,实际上包含READY和RUNNING两个状态。当执行
start()方法后,变为READY,获取CPU执行权后,则是RUNNING。当调用Thread.yield()后,由RUNNING变为READY。 -
BLOCKED:阻塞状态,如等待IO操作或者申请其他线程的独占资源时,都会变成阻塞状态,此时不占用CPU资源。当IO结束或声请到资源后,变为RUNNABLE
-
WAITING:等待状态,当执行了
Object.wait()方法或者其他线程.join()方法,则该线程变为WAITING状态。当执行Object.notify()或者加入的线程执行完毕,变为RUNNABLE -
TIMED_WAITING:与WAITING相似,但它是带有计时的等待状态,即不会无限等待。
-
TERMINATED:终止状态,线程运行结束
1.3 多线程的风险
-
线程安全问题:没有正确处理并发访问逻辑,使得出现脏数据、丢失数据更新等问题
-
线程活性问题:由于程序缺陷或资源缺陷,导致线程一直处于非RUNNABLE状态。常见的有以下几种
- 死锁:类似于鹬蚌相争,谁也不让谁
- 活锁:指线程一直处于运行状态,但是其任务一直无法进展的一种活性故障。产生活锁的线程一直在做无用功,如小猫一直追着自己的尾巴咬但总是咬不到
- 饥饿:线程一直无法获得资源来运行。比如在CPU繁忙的情况下,优先级低的线程执行的概率低,就可能发生线程“饥饿”
-
上下文切换:CPU从一个线程切换到另一个线程执行,需要耗费时间
2 线程安全
2.1 原子性
对变量的操作不可分割
- 线程访问(读、写)某个共享变量时,对于其他线程来说,这个操作要么执行完毕,要么未执行,其他线程无法看到中间结果
- 访问共享变量的原子操作,不能交错执行
在Java中有两种方式实现原子性:
- 使用锁:保证共享变量在某一时刻只能被一个线程访问
- 使用处理器的CAS指令
2.2 可见性
在多线程环境中,一个线程对某个共享变量进行更新后,其他线程能够立刻看到这个新的值
2.3 有序性
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
可通过volatile、synchronized、lock保证有序性
2.4 Java内存模型
Java内存模型(即Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范。与JVM的内存划分不是同一个概念。
JMM规定:
- 每个线程的共享变量存储在主内存中
- 每个线程都有自己的工作内存。工作内存是一个抽象的概念,并非真实存在。涵盖写缓冲器、寄存器等
- 每个线程访问变量时,都要从主内存中拷贝一份副本到自己的工作内存里,操作完成后再将变量刷写回主内存。这个过程其他线程是不可见的
3 线程同步
线程同步机制是一套用于协调线程之间的数据访问的机制。该机制可以保障线程安全。Java平台提供同步机制的有:锁、volatile、final、static等等。
3.1 volatile
volatile关键字的作用:使变量在多个线程之间可见。线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存;当其他线程读取该共享变量,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。
由于该关键字不保证原子性,所以只有当新值不依赖当前值时,才可以使用volatile;否则如果依赖当前值,则是 取值、计算、写值 三步操作,这三步操作不是原子性的,而volatile不保证原子性。例子如下:
public class Test {
public static void main(String[] args) {
MyInt myInt = new MyInt();
// 开启子线程,无限循环
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 子线程开始运行...");
while (myInt.getVal() == 0) {
}
System.out.println(Thread.currentThread().getName() + " 子线程结束运行...");
}).start();
// main线程休眠1秒后修改myInt的值
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
myInt.setVal(1);
System.out.println(Thread.currentThread().getName() + ": val is " + myInt.getVal());
}
private static final class MyInt {
// 使用volatile关键字,子线程会看到main线程修改后的最新值,因此会结束循环
// 若没有该关键字,则子线程不会结束
private volatile int val;
public int getVal() {
return val;
}
public void setVal(int val) {
this.val = val;
}
}
}
volatile与synchronized的比较:
- volatile是线程同步的轻量级实现,性能比后者好
- volatile只能修饰变量,后者可以修饰代码块、方法
- 多线程访问volatile变量不会发送阻塞,而后者会阻塞
- volatile能保证可见性、有序性;但不能保证原子性(例子如下)。synchronized则均能保证这三个特性
public class Test {
public static void main(String[] args) {
MyInt myInt = new MyInt();
int end = 100000;
// 开启两个子线程,每个线程自增100000次
for (int i = 0; i < 2; i++) {
new Thread(() -> {
for (int j = 0; j < end; j++) {
myInt.increment();
}
}).start();
}
// main线程休眠2秒,保证子线程跑完
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 此时值不等于200000,就是因为volatile不保证原子性
System.out.println(myInt.getVal());
}
private static final class MyInt {
private volatile int val;
public int getVal() {
return val;
}
public void increment() {
// 分取值、计算、写值三步操作,非原子操作
val++;
}
}
}
3.2 CAS
CAS(Compare And Swap),含三个操作数:内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值;否则,放弃本次操作或者重试。下面是基于CAS实现的一个计数器:
public class CASCounter {
private volatile long val;
public long incrementAndGet() {
long oldVal;
long newVal;
do {
oldVal = val;
newVal = oldVal + 1;
} while (!compareAndSwap(oldVal, newVal));
return newVal;
}
private boolean compareAndSwap(long expectVal, long newVal) {
// 这里使用同步代码块是模拟处理器提供的CAS指令
synchronized (this) {
if (val == expectVal) {
val = newVal;
return true;
}
return false;
}
}
public static void main(String[] args) {
CASCounter counter = new CASCounter();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
System.out.println(Thread.currentThread().getName() + ": " + counter.incrementAndGet());
}
}).start();
}
}
}
ABA问题:当进行compareAndSwap操作时,此时当前值看似等于期望值,但实际上是已经被更新过的了,即ABA中前一个A和后一个A的时间戳(或者版本号)不同,但是当前线程会认为该值没有被改过,这种情况能否被接受?
3.3 锁
将多个线程对共享数据的访问改为串行访问,即一个共享数据一次只能被一个线程访问。锁就是利用这个思路来保障线程安全,即保障了原子性、可见性、有序性。Java的锁有两种:内部锁(synchronized关键字)、显示锁(java.util.concurrent.locks.Lock接口的实现类)
- 一个线程只有在拥有锁(许可证)的情况下才能访问共享变量
- 一个锁只能被一个线程拥有,称为排他锁或互斥锁
- 线程访问结束后释放锁,运行过程中出现异常也会释放锁
锁的其他概念:
- 可重入性:一个线程在拥有锁的时候能否再次申请该锁,若能则是可重入锁。
void methodA() {
申请A锁;
methodB();
释放A锁;
}
void methodB() {
申请A锁; // 如果这里也能申请成功,就是可重入锁
doSomething;
释放A锁;
}
- 根据锁的争用与调度机制可分:公平锁和非公平锁
- 锁的粒度:锁保护的共享数据的大小。若锁保护的共享数据量大,则称该锁的粒度粗;反之粒度小。粒度过粗会导致申请锁时进行不必要的等待,过小则会增加锁调度的开销
3.3.1 synchronized
Java中每个对象都能充当一个内部锁,这种锁也被称为监视器(Monitor),是一种排他锁。synchronized有以下3种使用场景
- 修饰代码块。进入同步代码块时,会清空工作内存,从主内存读取最新值;退出同步代码块时,则会把工作内存的值写回主内存
synchronized (对象锁) {
// 同步代码块
}
public class Test {
public static void main(String[] args) {
final Object mutex = new Object();
// 此时第一个线程打印完后,第二个线程才会开始打印。不会出现交错打印的情况
new Thread(() -> {
synchronized (mutex) {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}).start();
new Thread(() -> {
synchronized (mutex) {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}).start();
}
}
- 修饰实例方法,此时锁是this
- 修饰静态方法,此时锁是运行时类,即XXX.class
3.3.2 Lock
java.util.concurrent.locks.Lock接口的常用方法有:
| 方法 | 作用 | 备注 |
|---|---|---|
| lock() | 等待获取锁 | |
| unlock() | 释放锁 | 通常放在try/finally中来获取、释放锁 |
| lockInterruptibly() | 等待获取锁 | 在等待的过程中如果被中断了(调用线程的interrupt()方法),则抛异常结束 |
| tryLock(long, TimeUnit) | 等待获取锁 | 在指定等待时间中,如果锁没有被其他线程持有,且当前线程也没被中断,才能获取锁,并返回true |
| newCondition() | 返回Condition对象 | Condition对象也能实现等待通知机制,见章节4.1的例2 |
Lock与synchronized的区别:
- 对于synchronized内部锁来说,如果一个线程在等待锁,则只有两种结果:要么获得锁继续执行,要么继续等待。
- 而Lock则提供了另外一种可能:在等待锁的过程中,可以根据需求中断对锁的等待,只要把
lock()方法改为lockInterruptibly()方法即可。表示如果在等待锁的过程中,被中断了(调用线程的interrupt()方法),则抛异常结束。合理使用lockInterruptibly()方法可解决死锁问题。
Lock接口的常用实现类:ReentrantLock,该类的常用方法有:
| 方法 | 作用 | 备注 |
|---|---|---|
| getHoldCount() | 返回当前线程调用lock()的次数 | |
| getQueueLength() | 返回正在等待获取锁的线程预估数量 | 是一个预估值,不保证准确 |
| getWaitQueueLength(Condition) | 返回正在等待指定Condition对象的线程预估数量 | |
| hasQueuedThread(Thread) | 查询指定线程是否在等待锁 | |
| hasQueuedThreads() | 查询是否有线程在等待锁 | |
| hasWaiters(Condition) | 查询是否有线程在等待指定Condition对象 | |
| isHeldByCurrentThread() | 判断该锁是不是被当前线程持有 | 常用于释放锁之前的判断 |
| isLocked() | 判断该锁是否被线程持有 |
public class LockTest {
private static final Lock lock = new ReentrantLock();
public static void method1() {
try {
// 先获取锁,通常用try/finally包住
lock.lock();
// 这中间的代码就相当于同步代码块
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
} finally {
// 最后释放锁
lock.unlock();
}
}
public static void main(String[] args) {
Runnable runnable = LockTest::method1;
new Thread(runnable).start();
new Thread(runnable).start();
new Thread(runnable).start();
}
}
3.3.3 公平锁和非公平锁
多数情况下,锁的申请都是非公平的。当多个线程在申请同一个锁时,只是从阻塞队列中随机取一个线程获取锁,这是非公平的。而公平锁则按照时间顺序保证先到先得,从而避免线程饥饿问题。synchronizedned是非公平锁;ReentrantLock则提供了一个构造方法可以指定为公平锁,但需要维护一个有序队列,性能较低。
3.3.4 ReadWriteLock
synchronizedned内部锁和ReentrantLock都是排他锁,同一时间只允许一个线程持有,以此保证线程安全,但是执行效率较低。而读写锁是一种改进的共享/排他锁,允许多个线程同时读取共享数据,但每次只允许一个线程修改数据。通过读锁和写锁来实现:线程读取数据前必须先持有读锁,读锁可以被多个线程持有,即读锁是共享的;线程在修改数据前必须先持有写锁,写锁是排他的,当线程持有写锁时,其他线程无法获取任何锁,不管是读还是写。保证了在读取数据期间,数据不会被修改。
| 其他线程能否获取读锁 | 其他线程能否获取写锁 | |
|---|---|---|
| 某线程获取读锁 | YES | NO |
| 某线程获取写锁 | NO | NO |
java.util.concurrent.locks.ReadWriteLock接口定义了两个方法:readLock()返回一个读锁、writeLock()返回一个写锁。注意:这两个方法返回的是同一个锁的两个不同角色,而不是两个不同的锁。常用实现类:ReentrantReadWriteLock
4 线程通信
4.1 等待通知机制
在多线程编程中,A线程的运行条件可能并不满足,此时可以将A线程暂停,等待其他线程更新这个条件,直到A线程满足运行条件后再将它唤醒。
相关的实现方法如下:
| 方法 | 作用 | 备注 |
|---|---|---|
| Object.wait() | 使得当前线程暂停,直到被唤醒为止 | 1)只能在同步代码块中由锁对象调用 2)调用方法后,当前线程会立即释放锁 3) interrupt()方法会中断线程的wait()方法 |
| Object.notify() | 可以唤醒一个处于wait状态的线程 | 1)只能在同步代码块中由锁对象调用 2)如果由多个等待的线程,只能随机唤醒其中一个 3)调用方法后,需要执行完同步代码块的内容才会释放锁 4)被唤醒的线程需要重新获得锁后才能继续往下执行 |
| Object.notifyAll() | 可以唤醒全部处于wait状态线程 | |
| Condition.await() | 使得当前线程暂停 | 1)Condition对象通过Lock.newCondition()方法获取2)调用 await()/signal()前需要获取对应的Lock锁3)可创建多个Condition对象来绑定线程,达到唤醒指定线程的效果,更为灵活 |
| Condition.signal() | 可以唤醒一个线程 |
例1:interrupt()方法会中断线程的wait()方法
public class WaitTest {
public static void main(String[] args) {
final Object mutex = new Object();
Thread t1 = new Thread(() -> {
synchronized (mutex) {
try {
System.out.println("开始wait...");
mutex.wait();
System.out.println("结束wait...");
} catch (InterruptedException e) {
System.out.println("wait()方法被中断");
}
}
});
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 此时会中断子线程的wait()方法
t1.interrupt();
}
}
例2:使用Condition对象实现等待通知机制
public class ConditionTest {
public static void main(String[] args) {
final Lock lock = new ReentrantLock();
final Condition condition0 = lock.newCondition();
final Condition condition1 = lock.newCondition();
new Thread(() -> {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "获得锁后开始等待...");
condition0.await();
System.out.println(Thread.currentThread().getName() + "被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}).start();
new Thread(() -> {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "获得锁后开始等待...");
condition1.await();
System.out.println(Thread.currentThread().getName() + "被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}).start();
try {
Thread.sleep(1000);
// main线程获得锁后,将子线程唤醒
lock.lock();
// 调用的是condition0的方法,达到只唤醒线程0的效果
condition0.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
4.2 生产者消费者模型
- 编写产品类
public class Product {
private int num;
public Product(int num) {
this.num = num;
}
public synchronized void addProduct() {
if (num < 10) {
num++;
System.out.println(Thread.currentThread().getName() + "添加产品,现有" + num + "个");
notifyAll();
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void delProduct() {
if (num > 0) {
num--;
System.out.println(Thread.currentThread().getName() + "减少产品,现有" + num + "个");
notifyAll();
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 编写生产者线程
public class ProducerThread extends Thread {
private final Product product;
public ProducerThread(Product product) {
this.product = product;
}
@Override
public void run() {
super.run();
while (true) {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
product.addProduct();
}
}
}
- 编写消费者线程
public class ConsumerThread extends Thread {
private final Product product;
public ConsumerThread(Product product) {
this.product = product;
}
@Override
public void run() {
super.run();
while (true) {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
product.delProduct();
}
}
}
- 测试类
public class Test {
public static void main(String[] args) {
Product product = new Product(10);
ProducerThread t0 = new ProducerThread(product);
ConsumerThread t1 = new ConsumerThread(product);
ConsumerThread t2 = new ConsumerThread(product);
t0.start();
t1.start();
t2.start();
}
}
5 ThreadLocal
ThreadLocal主要是为每个线程绑定自己的值,通过ThreadLocalMap实现,即每个线程都有自己的ThreadLocalMap。这个Map的key是ThreadLocal变量实例,value则是set()进去的值。如下例子所示:
public class Test {
// 定义ThreadLocal变量时,一般加上static关键字
static ThreadLocal<String> name = new ThreadLocal<>();
static ThreadLocal<String> phone = new ThreadLocal<>();
static ThreadLocal<Integer> age = new ThreadLocal<>();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
name.set("小明");
phone.set("123xxxx6666");
age.set(20);
System.out.println(Thread.currentThread().getName() + "_" + name.get());
System.out.println(Thread.currentThread().getName() + "_" + phone.get());
System.out.println(Thread.currentThread().getName() + "_" + age.get());
});
Thread t2 = new Thread(() -> {
name.set("小红");
phone.set("123xxxx9999");
age.set(18);
System.out.println(Thread.currentThread().getName() + "_" + name.get());
System.out.println(Thread.currentThread().getName() + "_" + phone.get());
System.out.println(Thread.currentThread().getName() + "_" + age.get());
});
t1.start();
t2.start();
}
}
6 线程管理
6.1 线程组
类似于计算机中使用文件夹来管理文件。在线程组中,可以定义一组相似(相关)的线程,也可以定义子线程组。创建线程时可定义线程组,若不指定则默认属于父线程所在的线程组。现在的开发已经不常用线程组,一般会将相关的线程放在一个数组或集合中。
6.2 处理线程异常
在线程运行时,如果有受检异常则需要进行捕获处理,如果有运行时异常,则可以通过设置未捕获异常处理器(UncaughtExceptionHandler)来处理,当线程出现运行时异常后,则会调用设置好的Handler来处理异常。
例1:设置全局的异常处理器
public class ExceptionHandlerTest {
public static void main(String[] args) {
// 设置全局的异常处理器,所有线程都按照以下逻辑处理异常
Thread.setDefaultUncaughtExceptionHandler((thread, exception) -> {
System.out.println(thread.getName() + "线程抛出异常:" + exception.getMessage());
});
// main线程出现异常,然后被上述处理器处理
int i = 10 / 0;
}
}
例2:为某个线程设置单独的异常处理器
public class ExceptionHandlerTest {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "开始运行...");
int i = 10 / 0;
});
// 为t1线程设置单独的异常处理器
t1.setUncaughtExceptionHandler((thread, exception) -> {
System.out.println(thread.getName() + "线程抛出异常:" + exception.getMessage());
});
t1.start();
}
}
6.3 注入Hook子线程
JVM退出的时候会执行Hook线程。基于这个特性,我们可以在程序启动时创建一个.lock文件,用于校验程序是否启动,在程序结束时执行的Hook线程中删除该文件,以此防止程序重复启动。除外,Hook线程也常用于做资源释放的操作。注意:Hook线程只有在程序正常退出时才会执行,强制关闭(如kill -9)则不会执行Hook线程。
public class HookTest {
private static final String FILE_PATH = "E:\\tmp.lock";
public static void main(String[] args) {
// 程序运行时,检查lock文件是否存在
File file = new File(FILE_PATH);
if (file.exists()) {
// 如果已经存在则退出
throw new RuntimeException("程序已启动!");
} else {
// 不存在则创建
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
// 注入一个Hook线程,用于删除lock文件
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("程序即将退出,将执行Hook线程");
file.delete();
}));
// 模拟程序运行
try {
System.out.println("程序正在运行...");
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
6.4 线程池
上文的例子中都是通过new Thread()的方式来创建线程,这样创建的线程对象在执行完run()方法后会被GC回收。而线程的创建、调度、销毁都有一定的开销,这会导致整个应用的性能降低,所以我们需要线程池来有效使用线程。
线程池内部维护一定数量的工作线程,开发人员将任务作为一个对象提交给线程池,线程池则把这些任务缓存在工作队列中,然后工作线程不断从队列中取出任务来执行。在开发中,一般使用第三方提供的线程池,或使用java.util.concurrent.Executors的相关方法创建线程池。
6.4.1 基本使用
例1:提交普通任务
public class ThreadPoolTest {
public static void main(String[] args) {
// 创建线程池,有5个线程
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
// 向线程池提交10个任务
for (int i = 0; i < 10; i++) {
fixedThreadPool.execute(() -> {
System.out.println(Thread.currentThread().getId() + "执行任务");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
}
例2:提交计划任务
public class ThreadPoolTest {
public static void main(String[] args) {
// 创建具有调度功能的线程池
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(10);
// 提交一个任务,2秒后执行
executorService.schedule(() -> {
System.out.println(Thread.currentThread().getId() + "执行任务");
}, 2, TimeUnit.SECONDS);
// 提交定时执行的任务,3秒后,每隔5秒就执行一次
executorService.scheduleAtFixedRate(() -> {
System.out.println(Thread.currentThread().getId() + "执行定时任务");
}, 3, 5, TimeUnit.SECONDS);
}
}
6.4.2 各个参数
以ThreadPoolExecutor的构造方法为例,各个参数的含义如下
-
corePoolSize:核心线程数量
参考公式:线程大小 = CPU数量 * 目标CPU的使用率 * (1 + 等待时间与计算时间的比)
-
maximumPoolSize:最大线程数量
-
keepAliveTime:当线程数量超过核心线程数量时,多余线程的存活时长
-
unit:上述keepAliveTime的单位
-
workQueue:任务队列,任务会提交到该队列等待执行
-
threadFactory:线程工厂,用于创建线程
-
handler:拒绝策略,当任务过多来不及处理时使用策略拒绝
6.4.3 任务队列
其中任务队列是由阻塞队列(BlockingQueue)实现,一般有以下几种:
- 直接提交队列:如SynchronousQueue。该队列没有容量,提交给线程池的任务不会被真实保存,而是将新的任务交给线程执行。如果没有空闲线程则尝试创建新的线程,当线程数量达到maximumPoolSize后执行拒绝策略。
- 有界任务队列:如ArrayBlockingQueue。该队列有具体容量,处理新任务的逻辑如下图所示
-
无界任务队列:如LinkedBlockingQueue。有新任务时,当线程数小于corePoolSize则创建新线程来执行任务,否则把任务加入队列。默认情况下该队列大小为Integer.MAX_VALUE,所以被视为是无界的。
-
任务优先队列:如PriorityBlockingQueue。与无界队列相似,只不过由于任务具有优先级,所以不是按照先进先出的顺序来执行任务。
6.4.4 拒绝策略
JDK提供了4种拒绝策略
- AbortPolicy:直接抛出RejectedExecutionException异常(默认策略)
- CallerRunsPolicy:只要线程池不关闭,则使用调用者线程来执行当前新任务
- DiscardPolicy:直接丢弃任务
- DiscardOldestPolicy:将队列中最老的任务丢弃,再次尝试提交新任务
6.4.5 扩展线程池
线程池ThreadPoolExecutor提供了以下方法来让我们增强线程池功能:
| 方法 | 作用 | 备注 |
|---|---|---|
| beforeExecute(Thread t, Runnable r) | 执行任务前调用该方法 | |
| afterExecute(Runnable r, Throwable t) | 任务结束后/异常退出后执行该方法 | |
| terminated() | 线程池关闭后执行该方法 |
public class ThreadPoolTest {
private static final class MyTask implements Runnable {
private final String name;
public MyTask(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println("执行任务" + name + "中...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
// 自定义线程池,重写beforeExecute、afterExecute等方法来增强功能
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5,
0, TimeUnit.SECONDS, new LinkedBlockingQueue<>()) {
@Override
protected void beforeExecute(Thread t, Runnable r) {
System.out.println("线程" + t.getId() + "即将执行" + ((MyTask) r).name);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
System.out.println(((MyTask) r).name + "执行结束");
}
@Override
protected void terminated() {
System.out.println("线程池退出...");
}
};
// 提交5个任务
for (int i = 0; i < 5; i++) {
threadPoolExecutor.execute(new MyTask("task-" + i));
}
// 关闭线程池:不再接收新的任务,已接收的任务仍会继续执行
threadPoolExecutor.shutdown();
}
}