1 基本概念
1.程序、进程和线程
-
程序(Program):
程序是指一组指令的集合,它描述了计算机执行特定任务的步骤和操作。程序是静态的,它只是存储在磁盘或内存中的一组代码和数据,不会直接执行。当我们运行一个程序时,操作系统会加载程序到内存中,并为其创建一个进程来执行。 -
进程(Process):
进程是指正在运行中的程序的实例。进程是动态的,它是计算机中资源分配和管理的基本单位。每个进程都有自己独立的内存空间、执行状态和系统资源,进程之间相互隔离,互不干扰。一个进程可以包含多个线程,可以同时执行多个任务。每个进程都有一个唯一的进程ID(PID)用于标识和管理。 -
线程(Thread):
线程是进程中的执行单元,它是比进程更细小的单位。一个进程可以包含多个线程,共享进程的资源和内存空间。线程是并发执行的,一个进程中的多个线程可以同时执行不同的任务。由于线程共享进程的资源,因此线程之间的通信和数据共享更加高效。
2.单核CPU和多核CPU
-
单核CPU:
单核CPU只有一个处理器核心,它一次只能处理一个任务或指令。在单核CPU中,所有的计算和处理都是顺序进行的,即每个指令按照严格的顺序依次执行。单核CPU无法同时处理多个任务,因此在多任务或多线程情况下,任务会按照时间片轮转的方式切换执行。 -
多核CPU:
多核CPU具有多个处理器核心,每个核心都可以独立地执行任务或指令。多核CPU允许多个任务或线程同时运行在不同的核心上,实现真正的并行处理。多核CPU可以提高计算机的整体性能和效率,特别是在多任务处理和多线程应用程序中表现更好。
一个Java应用程序java.exe,至少有三个线程: main()主线程, gc()垃圾回收线程以及异常处理线程。
3.并行和并发
-
并行:
并行指的是多个任务在同一时刻同时执行,即多个CPU同时执行多个任务。在并行执行中,每个任务互不干扰,彼此之间没有依赖关系,可以同时进行而不会相互阻塞。比如:多个人同时做不同的事。 -
并发:
并发指的是多个任务在同一时间段内交替执行,任务之间可能有时间片轮转或上下文切换,每个任务执行一小段时间后,切换到另一个任务继续执行。在并发执行中,多个任务交替运行,看起来好像同时进行,但实际上是通过快速切换来实现的。简单的说就是一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。
2 线程的创建和启动
2.1 继承Thread类的方式
- 定义子类继承Thread类。
- 类中重写Thread类中的run方法。
- 创建Thread子类对象,即创建了线程对象。
- 调用线程对象start方法:启动线程,调用run方法。
示例:
public class MyThread extends Thread {
@Override
public void run() {
// 线程要执行的代码逻辑
for (int i = 0; i <= 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" +i);
}
}
public static void main(String[] args) {
// 创建线程对象
MyThread thread = new MyThread();
// 启动线程
thread.start();
}
}
上述示例中,MyThread
类继承自Thread
类,并重写了run()
方法,然后通过创建MyThread
对象并调用start()
方法来启动线程。线程启动后,会执行run()
方法中的代码逻辑,并输出线程的执行结果。
2.2 实现Runnable接口的方式
- 定义子类,实现Runnable接口。
- 类中重写Runnable接口中的run方法。
- 通过Thread类含参构造器创建线程对象。
- 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
- 调用Thread类的start方法:开启线程, 调用Runnable子类接口的run方法。
示例:
public class MyRunnable implements Runnable {
@Override
public void run() {
// 线程要执行的代码逻辑
for (int i = 0; i <= 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + "\t" +i);
}
}
public static void main(String[] args) {
//方式1Runable方式
// 创建Runnable接口的实现类对象
MyRunnable myRunnable = new MyRunnable();
// 创建Thread对象,并将Runnable接口的实现类对象作为参数
Thread thread = new Thread(myRunnable);
// 启动线程
thread.start();
//方式2:匿名内部类
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
}
}).start();
}
}
上述示例中,MyRunnable
类实现了Runnable
接口,并重写了run()
方法。然后通过创建MyRunnable
对象,再将该对象作为参数创建Thread
对象,并调用start()
方法来启动线程。线程启动后,会执行run()
方法中的代码逻辑,并输出线程的执行结果。
3 Thread常见方法
构造函数:
- Thread(): 创建新的Thread对象
- Thread(String threadname): 创建线程并指定线程实例名
- Thread(Runnable target): 指定创建线程的目标对象,它实现了Runnable接口中的run方法
- Thread(Runnable target, String name): 创建新的Thread对象
方法:
变量和类型 | 方法 | 描述 |
---|---|---|
void | start() | 启动线程,并执行对象的run()方法 |
void | run() | 线程在被调度时执行的操作 |
String | getName() | 返回线程的名称 |
void | setName(String name) | 设置该线程名称 |
static Thread | currentThread() | 返回对当前正在执行的线程对象的引用。在Thread子类中就是this,通常用于主线程和Runnable实现类 |
static void | yield() | 线程让步, 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程, 若队列中没有同优先级的线程,忽略此方法 |
void | join() | 当某个程序执行流中调用其他线程的 join() 方法时, 调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止 |
static void | sleep() | (指定时间:毫秒) 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重新排队 |
void | stop() | 强制线程生命期结束,不推荐使用 ,已过时 |
boolean | isAlive() | 返回boolean,判断线程是否还活着 |
void | interrupt() | 中断此线程 |
static boolean | interrupted() | 测试当前线程是否已被中断 |
boolean | isInterrupted() | 测试此线程是否已被中断 |
3.1 sleep
指定线程休眠的时间,单位毫秒,让出cpu时间片,其他线程可以抢占cpu时间片
示例:
public class MyTask implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
3.2 线程优先级
线程的优先级等级:
- MAX_PRIORITY: 10
- MIN _PRIORITY: 1
- NORM_PRIORITY: 5
示例:
public static void main(String[] args) {
MyTask myTask = new MyTask();
Thread thread1 = new Thread(myTask, "t1");
thread1.setPriority(Thread.MIN_PRIORITY);
thread1.start();
Thread thread2 = new Thread(myTask, "t2");
thread2.setPriority(Thread.MAX_PRIORITY);
thread2.start();
}
结果:
3.3 守护线程
- 其他线程都执行结束,守护线程自动结束
- 守护启动子线程,也是守护线程
- 守护线程的语法
thread.setDaemon(true)
设置守护线程
示例:
public class Test2 {
public static void main(String[] args) {
MyThread myThread1 = new MyThread();
myThread1.setDaemon(true);
myThread1.start(); //守护线程, gc线程,jvm线程结束gc会自动结束
JOptionPane.showMessageDialog(null, "是否确认向下执行...."); //主线程进入IO阻塞
System.out.println("main over");
}
}
class MyThread extends Thread {
@Override
public void run() {
while (true) {
System.out.println("a");
}
}
}
结果:
守护线程自动结束
若注释掉myThread1.setDaemon(true);
结果:
不会结束循环
3.4 join
当某个程序执行流中调用其他线程的 join() 方法时, 调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止。也可以理解为插队。
示例:
public class Test{
/**
* CountDownLatch:可以实现相同的效果
* @param args
*/
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
t1.start();
try {
t1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + " main over");
}
}
有join的结果:
没有join的结果:
3.5 线程退出
3.5.1 stop
强制线程生命期结束,不推荐使用。不管线程正在执行的任务,直接退出,可能丢失数据。
示例:
public class Test {
public static void main(String[] args) {
Thread t1 = new Thread(new MyTask());
t1.start();
Scanner in = new Scanner(System.in);
System.out.println("输入1/0:0表示退出");
int i = in.nextInt(); ///主线程进入IO阻塞
if (i == 0) {
t1.stop();
}
System.out.println("main over");
}
static class MyTask implements Runnable {
@Override
public void run() {
while (true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
没有stop的结果:
有stop的结果:
3.5.2 中断信号interrupt
- void interrupt():发送中断信号(true)
如果线程在阻塞状态,比如sleep(),join(),wait(),这时接收到中断信号会抛出一个异常InterruptException,同时中断信号清除(false)
只是发送信号,不会对线程产生影响 - static boolean interrupted():得到中断信号(true),然后把中断信号设置成false
- boolean isInterrupted():得到中断信号,不会清除中断信号
示例:
public class Test{
public static void main(String[] args) {
Thread t1 = new Thread(new MyTask());
t1.start();
JOptionPane.showMessageDialog(null, "是否确认向下执行...."); //主线程进入IO阻塞
t1.interrupt(); //发送中断信号给t1
System.out.println("main over");
}
static class MyTask implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("a");
try {
Thread.sleep(500000);
} catch (InterruptedException e) {
System.out.println("bbbbbbbbbbbbbbb");
e.printStackTrace();
Thread.currentThread().interrupt(); //再次发送中断信号,中断信号发给阻塞线程,抛出Interrupt异常,中断信号清除
}
//得到中断信号,优雅的退出
if (Thread.interrupted()) {
break;
}
}
}
}
}
结果:
4 线程原理
4.1 线程栈帧
线程栈帧(Thread Stack Frame)是指线程在执行方法时,每个方法都会创建一个栈帧,用于保存方法的局部变量、操作数栈、返回值等信息。线程栈帧是线程独立的,每个线程都有自己的栈帧,用于执行方法的调用和返回。
线程栈帧通常包含以下几个重要部分:
- 局部变量区:用于存储方法内部的局部变量,包括基本数据类型和对象引用。
- 操作数栈:用于执行方法时的计算和操作,临时保存操作数和中间结果。
- 方法返回地址:记录方法调用结束后需要返回的位置,通常是调用该方法的下一条指令。
- 动态链接:记录方法调用所在的类和方法,用于动态链接到正确的方法。
每当一个方法被调用时,Java虚拟机都会创建一个新的栈帧,并将其压入当前线程的线程栈中。方法执行结束后,栈帧会被弹出,控制权回到上一层的栈帧。整个过程就像是一种方法的递归调用和返回。
线程栈帧的大小在运行时是动态变化的,取决于方法的嵌套深度和方法内部局部变量的大小。如果线程栈空间不足,可能会导致栈溢出(StackOverflowError)错误。因此,合理地设置线程栈大小是很重要的,可以通过虚拟机参数来进行配置。
4.2 线程的调度与时间片
线程的调度
- 指操作系统对多个线程之间的执行顺序进行安排和分配CPU时间的过程。操作系统采用线程调度算法来决定哪个线程应该被执行,并分配给它一定的CPU时间片(也称为时间量),在这个时间片内执行线程的任务。
时间片
- 它表示为每个线程分配的连续时间段。在一个时间片内,一个线程可以在CPU上执行,执行完成后,操作系统会将线程挂起,并将CPU资源分配给其他等待执行的线程。如果一个线程在时间片结束时还没有完成任务,操作系统会将它暂时挂起,等待下一次调度时再继续执行。时间片的大小通常是固定的,比如每个线程分配10毫秒的时间片。
线程的调度模型:分时调度模型、抢占式调度模型。
- 分时调度模型
系统平均分配 CPU 的时间片,所有线程轮流占用 CPU。分时调度模型在时间片调度的分配上,所有线程平等。 - 抢占式调度模型
系统按照线程优先级分配 CPU 时间片。优先级高的线程,优先分配 CPU 时间片;如果所有的就绪线程的优先级相同,那么会随机选择一个;优先级高的线程获取的 CPU 时间片相对多一些。
由于目前大部分操作系统都是使用抢占式调度模型进行线程调度。 Java 的线程管理和调度是委托给了操作系统完成的,与之相对应, Java 的线程调度也是使用抢占式调度模型。
4.3 线程的生命周期状态
Java 中的线程的生命周期分为 6 种状态:
1. NEW 状态
通过 new Thread(…)已经创建线程,但尚未调用 start()启动线程,该线程处于 NEW(新建)状态。
2.RUNNABLE 状态
Java 把就绪(Ready)和执行(Running)两种状态合并为一种状态:可执行(RUNNABLE)状态(或者可运行状态)。调用了线程的 start()实例方法后,线程就处于就绪状态;此线程获取到 CPU 时间片后,开始执行 run( )方法中的业务代码,线程处于执行状态。
(1)就绪状态
就绪状态仅仅表示线程具备运行资格,如果没有被操作系统的调度程序挑选中,线程就永远是就绪状
态;当前线程进入就绪状态的条件,大致包括以下几种:
- 调用线程的 start()方法,此线程进入就绪状态。
- 当前线程的执行时间片用完。
- 线程睡眠(sleep)操作结束。
- 对其他线程合入(join)操作结束。
- 等待用户输入结束。
- 线程争抢到对象锁(Object Monitor)。
- 当前线程调用了 yield 方法出让 CPU 执行权限。
(2)执行状态
线程调度程序从就绪状态的线程中选择一个线程,作为当前线程时线程所处的状态。这也是线程进入执行状态的唯一方式。
3.BLOCKED 状态
处于阻塞(BLOCKED)状态的线程并不会占用 CPU 资源,以下情况会让线程进入阻塞状态:
(1)线程等待获取锁
等待获取一个锁,而该锁被其他线程持有,则该线程进入阻塞状态。当其他线程释放了该锁,并且线
程调度器允许该线程持有该锁时,该线程退出阻塞状态。
(2)IO 阻塞
线程发起了一个阻塞式 IO 操作后,如果不具备 IO 操作的条件,线程会进入阻塞状态。 IO 包括磁盘
IO、 网络 IO 等。 IO 阻塞的一个简单例子:线程等待用户输入内容后继续执行。
4.WAITING 状态
处于 WAITING(无限期等待)状态的线程不会被分配 CPU 时间片,需要被其他线程显式地唤醒,才会进入就绪状态。线程调用以下 3 种方法,会让自己进入无限等待状态:
- Object.wait() 方法,对应的唤醒方式为: Object.notify() / Object.notifyAll()。
- Thread.join() 方法,对应的唤醒方式为:被合入的线程执行完毕。
- LockSupport.park() 方法,对应的唤醒方式为: LockSupport.unpark(Thread)。
5.TIMED_WAITING 状态
处于 TIMED_WAITING(限时等待)状态的线程不会被分配 CPU 时间片,如果指定时间之内没有被唤醒,限时等待的线程会被系统自动唤醒,进入就绪状态。以下 3 个方法会让线程进入限时等待状态:
- Thread.sleep(time) 方法,对应的唤醒方式为: sleep 睡眠时间结束。
- Object.wait(time) 方 法 , 对 应 的 唤 醒 方 式 为 : 调 用 Object.notify() /Object.notifyAll()去主
动唤醒,或者限时结束。 - LockSupport.parkNanos(time)/parkUntil(time) 方法,对应的唤醒方式为:线程调用配套的LockSupport.unpark(Thread)方法结束,或者线程停止(park)时限结束。
进入 BLOCKED 状态、 WAITING 状态、 TIMED_WAITING 状态的线程都会让出 CPU 的使用权;另外,等待或者阻塞状态的线程被唤醒后,进入 Ready 状态,需要重新获取时间片才能接着运行。
6.TERMINATED 状态
线程结束任务之后,将会正常进入 TERMINATED(死亡)状态;或者说在线程执行过程中发生了异常(而没有被处理),也会导致线程进入死亡状态。
与之对应的操作系统线程五种状态:
新建:创建新的进程
就绪:进程已经获得除CPU时间片以外的任何资源,一旦获得cpu时间片就能立马执行。
执行:处于就绪队列中的进程获得了时间片运行进程。
阻塞:进程时间片用完进入阻塞队列中等待唤醒。
终止:进程执行完毕。
4.4 线程执行原理和线程自增i++
自增类
public class Plus {
private int amount = 0;
public void selfPlus() {
synchronized (this){ //加锁
amount ++;
}
}
public int getAmount() {
return amount;
}
}
自增任务类
方式一:实现Runnable接口
public class PlusTask implements Runnable {
private Plus plus;
public PlusTask() {
}
public PlusTask(Plus plus) {
this.plus = plus;
}
@Override
public void run() {
for (int i = 0; i < 100000000; i++) {
plus.selfPlus();
}
}
}
方式二:继承Thread
public class MyThread extends Thread {
Plus plus;
public MyThread() {
}
public MyThread(Plus plus) {
this.plus = plus;
}
@Override
public void run() {
for (int i = 0; i < 100000000; i++) {
plus.selfPlus();
}
}
}
测试类:
方式一(实现Runnable接口)
public class Test {
public static void main(String[] args) throws InterruptedException {
Plus plus = new Plus();
PlusTask plusTask = new PlusTask(plus);
Thread t1 = new Thread(plusTask);
Thread t2 = new Thread(plusTask);
Thread t3 = new Thread(plusTask);
Thread t4 = new Thread(plusTask);
t1.start();
t2.start();
t3.start();
t4.start();
t1.join();
t2.join();
t3.join();
t4.join();
System.out.println("自增后="+plus.getAmount());
}
}
方式二(继承Thread)
public class Test{
public static void main(String[] args) throws InterruptedException {
Plus plus = new Plus();
MyThread t1 = new MyThread(plus);
MyThread t2 = new MyThread(plus);
MyThread t3 = new MyThread(plus);
MyThread t4 = new MyThread(plus);
t1.start();
t2.start();
t3.start();
t4.start();
t1.join();
t2.join();
t3.join();
t4.join();
System.out.println("自增后="+plus.getAmount());
}
}
方式三(CountDownLatch)
public class Test {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(10);
Plus plus = new Plus();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000000; i++) {
plus.selfPlus();
}
countDownLatch.countDown();//10->1
}
}).start();
}
countDownLatch.await();//阻塞到0
System.out.println("自增后="+plus.getAmount());
}
浅画了一个内存分析图:
加锁前的结果:
加锁后的结果:
5 线程同步
线程同步是为了解决多线程并发执行时可能出现的线程安全问题。在多线程环境中,多个线程可能同时访问共享的资源,如变量、对象、文件等,如果没有合适的同步机制,可能会导致以下问题:
- 竞争条件:多个线程对同一个资源进行读写操作时,由于操作顺序不确定,可能会产生意想不到的结果。
- 数据不一致:多个线程同时修改共享变量时,可能会导致数据不一致,即出现脏数据。
- 死锁:多个线程相互等待对方释放资源,导致程序无法继续执行。
为了避免上述问题,需要在多线程程序中使用线程同步机制。常见的线程同步机制包括:
- synchronized关键字:用于修饰方法或代码块,保证同一时间只有一个线程可以执行synchronized修饰的方法或代码块。
- ReentrantLock:可重入锁,与synchronized类似,但提供更多的灵活性和功能。
- AtomicInteger:原子整数,用于实现原子操作,避免竞争条件。
- Semaphore:信号量,用于控制同时访问某个资源的线程数量。
- CountDownLatch:倒计时门闩,用于让一组线程等待某个事件发生。
- CyclicBarrier:循环栅栏,用于让一组线程相互等待,直到所有线程都到达栅栏位置。
5.1 多窗口买票
Ticket类
public class Ticket {
private int count=100;
public int getCount() {
return count;
}
public int outTicket(){
return this.count--;
}
}
WindowTask类
public class WindowTask implements Runnable{
private Ticket ticket;
public WindowTask() {
}
public WindowTask(Ticket ticket) {
this.ticket = ticket;
}
@Override
public void run() {
while (true){
if(ticket.getCount() <= 0){
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+ "\t" +ticket.outTicket());
}
}
}
测试类
public class Test {
public static void main(String[] args) {
Ticket ticket = new Ticket();
WindowTask windowTask = new WindowTask(ticket);
Thread t1 = new Thread(windowTask,"窗口1");
Thread t2 = new Thread(windowTask,"窗口2");
Thread t3 = new Thread(windowTask,"窗口3");
Thread t4 = new Thread(windowTask,"窗口4");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
结果:
以上示例中实现了一个简单的多线程售票系统,有四个窗口同时售票。在WindowTask
类中的if (ticket.getCount() <= 0)
检查和ticket.outTicket()
方法的调用之间,由于没有任何同步机制,可能会导致多个线程进入if块
并执行减操作,导致一张票被卖多次。如上图所示,窗口1、2、3、4都卖了13。
超卖问题的原因:
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
解决办法:
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
5.2 synchronized关键字
synchronized 是 Java 中用于实现线程同步的关键字。它可以用于修饰方法或代码块,确保同一时间只有一个线程可以执行被 synchronized 修饰的方法或代码块,从而避免多个线程对共享资源的并发访问导致的竞争条件和线程安全问题。
语法:
1.修饰代码块
使用 synchronized 修饰的代码块称为同步代码块,可以指定锁对象,确保同一时间只有一个线程可以进入该代码块执行。
public void f1() {
// 非同步代码
synchronized (共享对象) {
// 同步代码块,只有一个线程可以进入
// ...
}
// 非同步代码
}
自增1亿次案例:
public void run() {
synchronized (plus) { //对共享对象加锁
for (int i = 0; i < 100000000; i++) {
plus.selfPlus();
}
}
}
2.修饰方法
当一个方法被 synchronized 修饰时,同一时间只有一个线程可以执行该方法。其他线程需要等待锁的释放才能继续执行该方法。
public synchronized void f2() {
// 该方法只能由一个线程执行
}
实例方法加锁:相当于synchronized(this)
public class SynDemo {
public static void main(String[] args) {
Car a = new Car();
Thread t1 = new Thread1(a);
Thread t2 = new Thread2(a);
t1.start();
t2.start();
}
}
class Thread1 extends Thread {
private Car a;
public Thread1(Car a) {
this.a = a;
}
public void run() {
a.fun1();
}
}
class Thread2 extends Thread {
private Car a;
public Thread2(Car a) {
this.a = a;
}
public void run() {
a.fun2();
}
}
class Car {
public synchronized void fun1() {
//synchronized (this) {
System.out.println("开始打蜡");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("打蜡结束");
//}
}
public void fun2() {
synchronized (this) {
try {
Thread.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("开始抛光");
System.out.println("抛光结束");
}
}
}
静态方法加锁:相当于synchronized(类.class)
public class SynDemo {
public static void main(String[] args) {
Car a = new Car();
Thread t1 = new Thread1(a);
Thread t2 = new Thread2(a);
t1.start();
t2.start();
}
}
class Thread1 extends Thread {
private Car a;
public Thread1(Car a) {
this.a = a;
}
public void run() {
a.fun1();
}
}
class Thread2 extends Thread {
private Car a;
public Thread2(Car a) {
this.a = a;
}
public void run() {
a.fun2();
}
}
class Car {
public synchronized static void fun1() {
System.out.println("开始打蜡");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("打蜡结束");
}
public static void fun2() {
synchronized (Car.class) {
try {
Thread.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("开始抛光");
System.out.println("抛光结束");
}
}
}
5.3 死锁
死锁是多线程编程中的一种特殊情况,当两个或多个线程互相持有对方需要的资源,并且同时又在等待对方释放资源时,就会发生死锁。由于每个线程都在等待对方释放资源,导致这些线程都无法继续执行,从而陷入了僵局,这种状态被称为死锁。
比如一个系统一共四个进程,进程之间的依赖关系如下图所示。
很容易看出发生了死锁。
示例两个线程发生死锁:
public class DeadLock {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (String.class) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (Integer.class) {
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (Integer.class) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (String.class) {
}
}
}
});
t1.start();
t2.start();
System.out.println("死锁...");
}
}
在上述示例中,有两个线程t1和t2,它们分别持有不同的锁,并且彼此等待对方释放锁,导致死锁的发生。
5.4 CountDownLatch
juc的公共锁
- 构造函数new CountDownLatch(perms 10):授权数量
- countDown():授权数量-1
- await():执行后线程进入阻塞,知道授权数量=0
示例:
自增类:
public class Plus {
private int count = 0;
public void selfPlus() {
count ++;
}
public int getCount() {
return count;
}
}
MyTask 类实现Runnable 接口
public class MyTask implements Runnable {
Plus plus;
CountDownLatch countDownLatch;
public MyTask() {
}
public MyTask(Plus plus, CountDownLatch countDownLatch) {
this.plus = plus;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
synchronized (plus) {
for (int i = 0; i < 100000000; i++) {
plus.selfPlus();
}
countDownLatch.countDown(); //-1
}
}
}
测试:
public class Test{
public static void main(String[] args) {
Plus plus = new Plus();
CountDownLatch countDownLatch = new CountDownLatch(4);
MyTask task = new MyTask(plus, countDownLatch);
for (int i = 0; i < 4; i++) {
new Thread(task).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("自增后=" + plus.getCount());
}
}
匿名内部类写法
public class Test {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(4);
Plus plus = new Plus();
for (int i = 0; i < 4; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000000; i++) {
plus.selfPlus();
}
countDownLatch.countDown();//4->1
}
}).start();
}
countDownLatch.await();//阻塞到0
System.out.println("自增后="+plus.getCount());
}
6 线程通讯
6.1 等待唤醒机制
线程的等待唤醒机制:
- EntryList:双向队列:等待锁的队列
- Owner:正在执行的线程
- WaitSet:双向队列
在Java中,线程通讯等待唤醒机制通常使用以下三个方法来实现:
- wait(): 当一个线程调用了对象的wait()方法时,它会释放对象的锁,并进入等待状态,直到其他线程调用了该对象的notify()或notifyAll()方法来唤醒它。
- notify(): 当一个线程调用了对象的notify()方法时,它会随机唤醒一个正在等待该对象的线程,使其从等待状态转变为可运行状态。被唤醒的线程会尝试重新获取对象的锁,一旦成功,它会从wait()方法返回,并继续执行。
- notifyAll(): 当一个线程调用了对象的notifyAll()方法时,它会唤醒所有正在等待该对象的线程,使它们从等待状态转变为可运行状态。被唤醒的线程会竞争获取对象的锁,只有一个线程能够成功获取锁并继续执行,其他线程会继续等待。
这种等待唤醒机制可以用于实现线程之间的协作和同步,例如生产者-消费者模型、任务队列等场景。通过合理地使用wait()、notify()和notifyAll()方法,可以有效地避免线程之间的竞争和资源浪费,提高程序的效率和性能。但需要注意的是,使用等待唤醒机制时,必须在同步代码块中调用这些方法,否则会抛出IllegalMonitorStateException异常。
6.2 生产者消费者问题
1.共享对象 (包子)
public class BaoZi {
private String pi;
private String xian;
private boolean tag = false;
public String getPi() {
return pi;
}
public void setPi(String pi) {
this.pi = pi;
}
public String getXian() {
return xian;
}
public void setXian(String xian) {
this.xian = xian;
}
public boolean isTag() {
return tag;
}
public void setTag(boolean tag) {
this.tag = tag;
}
@Override
public String toString() {
return "BaoZi{" +
"pi='" + pi + '\'' +
", xian='" + xian + '\'' +
", tag=" + tag +
'}';
}
}
2.生产者(包子铺线程)
public class BaoZiPu implements Runnable{
private BaoZi baoZi;
public BaoZiPu() {
}
public BaoZiPu(BaoZi baoZi) {
this.baoZi = baoZi;
}
@Override
public void run() {
int count = 0;
while (true) {
synchronized (baoZi) {
if (baoZi.isTag()) {
try {
baoZi.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(count % 2 == 0){
baoZi.setPi("面皮");
baoZi.setXian("鸡蛋韭菜");
}else {
baoZi.setPi("面皮");
baoZi.setXian("三鲜");
}
count++;
System.out.println("包子已经生产完毕," + baoZi);
System.out.println("吃货来吃包子吧");
baoZi.setTag(true);
baoZi.notify();
}
}
}
}
3.消费者(吃货)
public class Chi implements Runnable{
private BaoZi baoZi;
public Chi() {
}
public Chi(BaoZi baoZi) {
this.baoZi = baoZi;
}
@Override
public void run() {
while (true){
synchronized (baoZi){
if(!baoZi.isTag()){
try {
baoZi.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("吃货正在吃 " + baoZi);
baoZi.setTag(false);
baoZi.notify();
}
}
}
}
4.测试类
public class Test {
public static void main(String[] args) {
BaoZi baoZi = new BaoZi();
BaoZiPu baoZiPu = new BaoZiPu(baoZi);
Chi chi = new Chi(baoZi);
Thread t1 = new Thread(baoZiPu);
Thread t2 = new Thread(chi);
t1.start();
t2.start();
}
}
运行结果:
包子已经生产完毕,BaoZi{pi='面皮', xian='鸡蛋韭菜', tag=false}
吃货来吃包子吧
吃货正在吃 BaoZi{pi='面皮', xian='鸡蛋韭菜', tag=true}
包子已经生产完毕,BaoZi{pi='面皮', xian='三鲜', tag=false}
吃货来吃包子吧
吃货正在吃 BaoZi{pi='面皮', xian='三鲜', tag=true}
...
7 JUC显示锁
JUC 就是 java.util .concurrent 工具包的简称,该工具包是从 JDK 1.5 开始加入到JDK,用于完成高并发、处理多线程的一个工具包。
- 内置锁:synchronized
- 显示锁:JUC锁
7.1 Lock接口
Java 对象锁还存在性能问题。在竞争稍微激烈的情况下, Java 对象锁会膨胀为重量级锁(基于操作系统的 Mutex Lock 实现),而重量级锁的线程阻塞和唤醒操作,需要进程在内核态和用户态之间来回切换,导致其性能非常低。JUC显示锁因为是纯粹Java语言实现,避免了这些问题,同时JUC显示锁具备了对象锁不具备的高级特性r如限时抢锁、可中断抢锁、多个等待队列。
显式锁不再作为 Java 内置特性来实现,而是作为 Java 语言可编程特性来实现
java.util.concurrent.locks.Lock接口的主要抽象方法如下
方 法 | 描 述 |
---|---|
void lock() | 抢锁。 成功则向下运行,失败则阻塞抢锁线程 |
void lockInterruptibly() | 可中断抢锁,当前线程在抢锁的过程中可以响应中断信号 |
boolean tryLock() | 尝试抢锁, 线程为非阻塞模式,在调用 tryLock 方法后立即返回。抢锁成功返回 true, 抢锁失败返回 false |
boolean tryLock(long time, TimeUnit unit) | 限时抢锁,到达超时时间返回 false。并且限时抢锁方法也可以响应中断信号 |
void unlock() | 释放锁 |
Condition newCondition() | 获取与显式锁绑定的 Condition 对象,用于“等待-通知”方式的线程间通信 |
从 Lock 提供的接口方法可以看出, 显式锁至少比 Java 内置锁多了以下优势:
- 可中断获取锁
使用 synchronized 关键字获取锁的时候,如果线程没有获取到被阻塞,阻塞期间该线程是不响应中断信号(interrupt)的;而使用 Lock.lockInterruptibly( )方法获取锁时,如果线程被中断,线程将抛出中断异常。 - 可非阻塞获取锁
使用 synchronized 关键字获取锁时,如果没有成功获取,线程只有被阻塞;而使用Lock.tryLock( )方法获取锁时,如果没有获取成功,线程也不会被阻塞,而是直接返回 false。 - 可限时抢锁
使用 Lock.tryLock(long time, TimeUnit unit)方法, 显式锁可以设置限定抢占锁的超时时间。而在用synchronized 关键字获取锁时,如果不能抢到锁,线程只能无限制阻塞。
7.2 ReentrantLock的基本用法
ReentrantLock 是 Java 中的一个独占锁(也称为互斥锁)实现类,它提供了与关键字 synchronized 相似的功能,但具有更强大的特性和灵活性。ReentrantLock 支持可重入性,即同一线程可以多次获得同一个锁,而不会发生死锁。
示例:
public class ReenterLockTest implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
public static int i = 0;
public void run() {
lock.lock();
lock.lock();
try {
for (int j = 0; j < 10000000; j++) {
i++;
}
} finally {
lock.unlock();
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ReenterLockTest r1 = new ReenterLockTest();
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r1);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
在这个示例中,ReentrantLock 被用来保护临界区,它允许同一个线程在获取锁之后多次获取锁(例如,在 run 方法中)。在结束临界区操作后,必须确保释放相同次数的锁,以避免死锁。通过使用 ReentrantLock,你可以更灵活地控制锁的获取和释放,以及提供更多的线程同步控制选项。
7.3 读写锁
读写锁的内部包含了两把锁:一把是为读锁,是一种共享锁;一把写锁,是一种独占锁。读写锁适用于读多写少的并发情况。
- 读、读共享
- 读、写互斥
- 写、写互斥
示例:
public class ReadWriteLockDemo {
static CountDownLatch countDownLatch;
private static Lock lock = new ReentrantLock();
private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private static Lock readLock = readWriteLock.readLock();
private static Lock writeLock = readWriteLock.writeLock();
private int value;
public Object handleRead(Lock lock) throws InterruptedException {
lock.lock(); //阻塞
try {
Thread.sleep(1000);
System.out.println("read success");
countDownLatch.countDown();
return value;
} finally {
lock.unlock();
}
}
public void handleWrite(Lock lock, int index) throws InterruptedException {
lock.lock(); //阻塞
try {
Thread.sleep(1000);
value = index;
System.out.println("write success");
countDownLatch.countDown();
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
final ReadWriteLockDemo demo = new ReadWriteLockDemo();
Runnable readRunnable = new Runnable() {
public void run() {
try {
demo.handleRead(readLock);
//demo.handleRead(lock); 可重入锁较慢
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Runnable writeRunnable = new Runnable() {
public void run() {
try {
demo.handleWrite(writeLock, new Random().nextInt());
//demo.handleWrite(lock, new Random().nextInt()); 可重入锁较慢
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
countDownLatch = new CountDownLatch(20);
long t1 = System.currentTimeMillis(); //1970-1-1 0:0:0:0到现在的毫秒
for (int i = 0; i < 18; i++) {
new Thread(readRunnable).start();
}
for (int i = 18; i < 20; i++) {
new Thread(writeRunnable).start();
}
countDownLatch.await();
long t2 = System.currentTimeMillis(); //1970-1-1 0:0:0:0到现在的毫秒
System.out.println(t2 - t1); //期待20s
}
}
代码展示了一个使用读写锁(ReadWriteLock)的例子。读写锁允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。
在以上示例中,有两种任务类型:读任务(readRunnable)和写任务(writeRunnable)。创建了一个 ReadWriteLockDemo 实例,然后使用读写锁来保护共享资源 value。读任务和写任务在获取锁后进行相应的操作,然后释放锁。
值得注意的是,写锁是独占的,这意味着只有一个线程能够获取写锁并执行写操作。而读锁是共享的,多个线程可以同时获取读锁并执行读操作。
在示例中,通过比较使用读写锁和普通可重入锁(ReentrantLock)的执行时间,可以看到读写锁在多读少写的场景下可以提供更好的性能。这是因为读写锁允许多个线程同时读取,而普通可重入锁在同一时刻只允许一个线程执行。所以在读多写少的情况下,使用读写锁可以更好地利用线程的并发性。
7.4 Semaphore
Semaphore 是一个是许可管理器,可以用来控制在同一时刻访问共享资源的线程数量,Semaphore 维护了一组虚拟许可,其数量可以通过构造函数的参数指定。线程在访问共享资源前,必须使用 Semaphore 的 acquire 方法获得许可,如果许可数量为 0,该线程则一直阻塞。线程访问完成资源后,必须使用 Semaphore 的 release 方法去释放许可。
1.Semaphore 的主要方法
- Semaphore(permits):构造一个 Semaphore 实例,初始化其管理的许可数量为 permits 参数值。
- Semaphore(permits,fair):是否以公平模式(fair 参数是否为 true)进行许可的发放。
- availablePermits( ):获取 Semaphore 对象可用的许可数量。
- acquire( ):尝试获取 1 个许可。而当前线程被中断,则会抛出 InterruptedException 异常并终止阻塞
- acquire(permits):尝试去阻塞的获取 permits 个许可
- acquierUninterruptibly( ):阻塞的过程不可中断,直到成功获取 1 许可。
- acquireUninterruptibly(permits):获取 permits 个许可,阻塞的过程不可中断
- tryAcquire():非阻塞获取1个许可
- tryAcquire(permits):非阻塞获取 permits 个许可。
- tryAcquire(timeout,TimeUnit):限时获取许可
- release( ):释放 1 个可用的许可
- release(permits):释放 permits 个可用的许可。
- drainPermits( ):当前线程获得剩余的所有可用许可。
- hasQueuedThreads( ):判断当前 Semaphore 对象上是否存在正在等待许可的线程。
- getQueueLength( ):获取当前 Semaphore 对象上正在等待许可的线程数量。
2.示例
假设有 10 个人在银行办理业务,只有 2 个工作窗口,使用 Semaphore 模拟银行排队,代码如下:
public class SemaphoreTest {
public static void main(String[] args) throws InterruptedException {
//线程池,用于多线程模拟测试
final CountDownLatch countDownLatch = new CountDownLatch(10);
//创建信号量,含有2个许可
final Semaphore semaphore = new Semaphore(2);
AtomicInteger index = new AtomicInteger(0);
//创建Runnable可执行实例
Runnable r = () ->
{
try
{
//抢占一个许可
semaphore.acquire(1);
//模拟业务操作: 处理排队业务
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(new Date()) + ", 受理处理中...,服务号: " + index.incrementAndGet());
Thread.sleep(1000);
//释放一个信号
semaphore.release(1);
} catch (Exception e)
{
e.printStackTrace();
}
countDownLatch.countDown();
};
//创建10条线程
Thread[] tArray = new Thread[10];
for (int i = 0; i < 10; i++)
{
tArray[i] = new Thread(r, "线程" + i);
}
//启动10条线程
for (int i = 0; i < 10; i++)
{
tArray[i].start();
}
countDownLatch.await();
}
}
以上示例演示了使用 Semaphore 实现并发访问控制的功能。在这个示例中,创建了一个包含 2 个许可的信号量 semaphore,然后创建了 10 个线程模拟排队等待服务。
每个线程通过调用 semaphore.acquire(1) 方法来获取一个许可,表示开始处理排队业务。处理业务过程中,线程会睡眠 1 秒,模拟实际业务操作。然后通过 semaphore.release(1) 方法释放许可,表示该业务处理结束,其他线程可以继续获取许可并开始处理。
这样就实现了同时只允许两个线程并发执行业务操作,其他线程需要等待前面的线程释放许可后才能继续执行。
8 线程池原理与实战
Java 线程需要 JVM 和 操作系统配合完成大量的工作:
- 必须为线程堆栈分配和初始化大量内存块,其中包含至少 1M 的栈内存。
- 需要进行系统调用,以便在操作系统中创建和注册本地线程。
频繁创建和销毁线程非常低效。通过使用线程池可以提升性能,方便线程的管理。
8.1 JUC 的线程池架构
JUC 就是 java.util .concurrent 工具包的简称,该工具包是从 JDK 1.5 开始加入到JDK,用于完成高并发、处理多线程的一个工具包。
JUC线程池架构如图所示:
1.Executor
它是 Java 异步目标任务的“执行者”接口,其目标是来执行目标任务。“执行者” Executor提供了 execute()接口来执行已提交的 Runnable 执行目标实例。 Executor 作为执行者的角色,存在的目的是“任务提交者”与“任务执行者”分离开来的机制。它只包含一个函数式方法:
void execute(Runnable command)
2.ExecutorService
ExecutorService 继承于 Executor。它是 Java 异步目标任务的“执行者服务“接口,它对外提供异步任务的接收服务, ExecutorService 提供了“接收异步任务、并转交给执行者”的方法,如submit 系列方法、 invoke 系列方法等等。具体如下:
//向线程池提交单个异步任务
Future submit(Callable task);
//向线程池提交批量异步任务
List<Future> invokeAll(Collection<? extends Callable> tasks) throws InterruptedException;
3.AbstractExecutorService
AbstractExecutorService 是一个抽象类 , 它实现了 ExecutorService 接口 。AbstractExecutorService 存在的目的是为 ExecutorService 中的接口提供了默认实现。
4.ThreadPoolExecutor
ThreadPoolExecutor 就是“线程池”实现类,它继承于 AbstractExecutorService 抽象类。 ThreadPoolExecutor 是 JUC 线程池的核心实现类。线程的创建和终止需要很大的开销,线程池中预先提供了指定数量的可重用线程,所以使用线程池会节省系统资源,并且每个线程池都维护了一些基础的数据统计,方便线程的管理和监控。
5. ScheduledExecutorService
ScheduledExecutorService 是一个接口,它继承于于 ExecutorService。它是一个可以完成“延时”“周期性”任务的调度线程池接口,其功能和 Timer/TimerTask 类似。
6. ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor 继承于ThreadPoolExecutor,它提供了 ScheduledExecutorService 线程池接口中“延时执行”和“周期执行”等抽象调度方法的具体实现。
ScheduledThreadPoolExecutor 类似于 Timer,但是在高并发程序 ScheduledThreadPoolExecutor 的性能要优于 Timer。
7. Executors
Executors 是个静态工厂类 ,它通过静态工厂方法返回 ExecutorService、ScheduledExecutorService 等线程池实例对象,这些静态工厂方法可以理解为一些快捷的创建线程池的方法。
8.2 Executors创建线程池
方法名 | 功能简介 |
---|---|
newSingleThreadExecutor() | 创建只有一个线程的线程池 |
newFixedThreadPool(int nThreads) | 创建固定大小的线程池 |
newCachedThreadPool() | 创建一个不限制线程数量的线程池,任何提交的任务都将立即执行,但是空闲线程会得到及时回收 |
newScheduledThreadPool() | 创建一个可定期或者延时执行任务的线程池 |
1.newSingleThreadExecutor() 创建单线程线程池
该方法用于创建一个“单线程化线程池”,也就是只有一条线程的线程池,所创建的线程池用唯一的工作线程来执行任务,使用此方法创建的线程池,能保证所有任务按照指定顺序(如FIFO)执行。
特点:
- 单线程化的线程池中的任务,是按照提交的次序,顺序执行的。
- 池中的唯一线程的存活时间是无限的。
- 当池中的唯一线程正繁忙时,新提交的任务实例会进入内部的阻塞队列(无界的阻塞队列)。
示例:
public class ThreadPoolDemo {
public static final int SLEEP_GAP = 500;
public static final int MAX_TURN = 5;
CountDownLatch countDownLatch = new CountDownLatch(MAX_TURN);
//异步的执行目标类
public static class TargetTask implements Runnable {
CountDownLatch countDownLatch;
static AtomicInteger taskNo = new AtomicInteger(1);
protected String taskName;
public TargetTask() {
taskName = "task-" + taskNo.get();
taskNo.incrementAndGet();
}
public TargetTask(CountDownLatch countDownLatch) {
this();
this.countDownLatch = countDownLatch;
}
public void run() {
System.out.println("任务:" + taskName + " doing");
try {
Thread.sleep(SLEEP_GAP);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(taskName + " 运行结束.");
countDownLatch.countDown();
}
@Override
public String toString() {
return "TargetTask{" + taskName + '}';
}
}
//测试用例:只有一条线程的线程池
@Test
public void testSingleThreadExecutor() {
ExecutorService pool = Executors.newSingleThreadExecutor();
for (int i = 0; i < MAX_TURN; i++) {
pool.execute(new TargetTask(countDownLatch));
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//关闭线程池
pool.shutdown();
}
}
运行结果:
任务:task-1 doing
task-1 运行结束.
任务:task-2 doing
task-2 运行结束.
任务:task-3 doing
task-3 运行结束.
任务:task-4 doing
task-4 运行结束.
任务:task-5 doing
task-5 运行结束.
Process finished with exit code 0
2.newFixedThreadPool (int nThreads) 创建固定数量的线程池
该方法用于创建一个“固定数量的线程池”,其唯一的参数用于设置池中线程的“固定数量”。
特点:
- 如果线程数没有达到“固定数量”,则每次提交一个任务池内就创建一个新线程,直到线程达到线程池的固定的数量。
- 线程池的大小一旦达到“固定数量”就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
- 在接收异步任务的执行目标实例时,如果池中的所有线程均在繁忙状态,对于新任务会进入阻塞队列中(无界的阻塞队列)。
示例:
//有3条线程固定大小的线程池
@Test
public void testNewFixedThreadPool() throws InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(3);
for (int i = 0; i < MAX_TURN; i++) {
pool.execute(new TargetTask(countDownLatch));
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//关闭线程池
pool.shutdown();
}
运行结果:
任务:task-1 doing
任务:task-3 doing
任务:task-2 doing
task-1 运行结束.
task-2 运行结束.
task-3 运行结束.
任务:task-5 doing
任务:task-4 doing
task-5 运行结束.
task-4 运行结束.
Process finished with exit code 0
3.newCachedThreadPool() 创建可缓存线程池
创建一个不限制线程数量的线程池,任何提交的任务都将立即执行,但是空闲线程会得到及时回收。
特点:
- 在接收新的异步任务 target 执行目标实例时,如果池内所有线程繁忙,此线程池会添加新线程来处理任务。
- 此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小
- 如果部分线程空闲,也就是存量线程的数量超过了处理任务数量,那么就会回收空闲(60 秒不执行任务)线程。
- “可缓存线程池”的弊端:线程池没有最大线程数量限制,如果大量的异步任务执行目标实例同时提交,可能导致创线程过多会而导致资源耗尽。
//线程数没有限制
@Test
public void testNewCachedThreadPool() throws InterruptedException {
ExecutorService pool = Executors.newCachedThreadPool();
for (int i = 0; i < MAX_TURN; i++) {
pool.execute(new TargetTask(countDownLatch));
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//关闭线程池
pool.shutdown();
}
运行结果:
任务:task-1 doing
任务:task-2 doing
任务:task-5 doing
任务:task-3 doing
任务:task-4 doing
task-1 运行结束.
task-2 运行结束.
task-5 运行结束.
task-3 运行结束.
task-4 运行结束.
Process finished with exit code 0
4.newScheduledThreadPool() 创建可调度线程池
该方法用于创建一个可调度线程池,具有延时和周期性任务的调度功能的ScheduledExecutorService 类型的线程池。
示例:
//可调度线程池
@Test
public void testNewScheduledThreadPool() {
ScheduledExecutorService scheduled = Executors.newScheduledThreadPool(2);
for (int i = 0; i < 2; i++) {
scheduled.scheduleAtFixedRate(new TargetTask(countDownLatch),
0, 5000, TimeUnit.MILLISECONDS);
}
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//关闭线程池
scheduled.shutdown();
}
scheduleAtFixedRate 方法的定义如下:
public ScheduledFuture<?> scheduleAtFixedRate(
Runnable command, //异步任务 target 执行目标实例;
long initialDelay, //首次执行延时;
long period, //两次开始执行最小间隔时间;
TimeUnit unit //所设置的时间的计时单位,如 TimeUnit.SECONDS 常量;
);
运行结果:
任务:task-2 doing
任务:task-1 doing
task-1 运行结束.
task-2 运行结束.
时隔5s...
任务:task-1 doing
任务:task-2 doing
task-1 运行结束.
task-2 运行结束.
Process finished with exit code 130
8.3 线程池的标准创建方式
大部分企业的开发规范,都会禁止使用快捷线程池,要求通过标准构造器 ThreadPoolExecutor 去构造工作线程池。
阿里规约:
ThreadPoolExecutor 定义:
// 使用标准构造器,构造一个普通的线程池
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数,即使线程空闲(Idle),也不会回收;
int maximumPoolSize, // 最大线程数;
long keepAliveTime, TimeUnit unit, // 线程最大空闲(Idle)时长
BlockingQueue workQueue, // 任务的排队队列
ThreadFactory threadFactory, // 新线程的产生方式
RejectedExecutionHandler handler) // 拒绝策略
-
corePoolSize和maximumPoolSize
线程池规则如下:
(1)在线程池接收到的新任务,并且当前工作线程数少于corePoolSize时,即使其他工作线程处于空闲状态,也会创建一个新线程来处理该请求,直到线程数达到 corePoolSize。
(2)当前工作线程数多于 corePoolSize 数量,但小于 maximumPoolSize 数量,则仅当任务排队已满时,才会创建新线程。 通过设置 corePoolSize 和 maximumPoolSize 相同,可以创建一个固定大小的线程池。
(3)当 maximumPoolSize 被设置为无界值(如 Integer.MAX_VALUE)时,线程池可以接收任意数量的并发任务。
(4)corePoolSize 和 maximumPoolSize 不仅能在线程池构造时设置,也可以使用setCorePoolSize 和 setMaximumPoolSize 两个方法进行动态更改。 -
BlockingQueue
BlockingQueue(阻塞队列)的实例用于暂时接收到的异步任务,如果线程池的核心线程都在忙,则所接收到的目标任务,缓存在阻塞队列中。 -
keepAliveTime
默认情况下, Idle 超时策略仅适用于存在超过 corePoolSize 线程的情况。 但是如果调用了allowCoreThreadTimeOut(boolean)方法,并且传入了参数 true,则 keepAliveTime 参数所设置的 Idle超时策略也将被应用于核心线程。
8.4 线程池的任务调度流程
线程池的任务调度流程如下:
(1)当前工作线程数小于核心线程池数量,执行器总是优先创建一个任务线程,而不是从线程队列中取一个空闲线程。
(2)线程池中任务数大于核心线程池数,任务将被加入到阻塞队列中,一直到阻塞队列满。在核心线程池数量已经用完、阻塞队列没有满的场景下,线程池不是为新任务创建一个新线程。
(3)当完成一个任务的执行时,执行器总是优先从阻塞队列中取下一个任务,并开始其执行,一直到阻塞队列为空,其中所有的缓存任务被取光。
(4)在核心线程池数量已经用完、阻塞队列也已经满了的场景下,如果线程池接收到新的任务,将会为新任务创建一个线程(非核心线程),并且立即开始执行新任务。
(5)在核心线程都用完、阻塞队列已满的情况下,一直会创建新线程去执行新任务,直到池内的线程总数超出 maximumPoolSize。如果线程池的线程总数超时 maximumPoolSize,则线程池会拒绝接收任务,当新任务过来时,会为新任务执行拒绝策略。
下面是一个线程池配置示例:
@Test
public void testThreadPoolExecutor() {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, //corePoolSize
4, //maximumPoolSize
100, //keepAliveTime
TimeUnit.SECONDS, //unit
new LinkedBlockingDeque<>(2), //workQueue
new ThreadPoolExecutor.DiscardOldestPolicy());
for (int i = 0; i < 7; i++) {
final int taskIndex = i;
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " taskIndex = " + taskIndex );
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
executor.execute(task);
}
while (true) {
//每隔 1 秒,输出线程池的工作任务数量、总计的任务数量
System.out.printf("- activeCount: %d - taskCount: %d\r\n", executor.getActiveCount(), executor.getTaskCount());
ThreadUtil.sleepSeconds(1);
}
}
运行结果:
main taskIndex = 6
pool-1-thread-1 taskIndex = 0
pool-1-thread-2 taskIndex = 1
pool-1-thread-3 taskIndex = 4
pool-1-thread-4 taskIndex = 5
pool-1-thread-1 taskIndex = 2
pool-1-thread-3 taskIndex = 3
- activeCount: 3 - taskCount: 5
- activeCount: 2 - taskCount: 6
- activeCount: 2 - taskCount: 6
- activeCount: 2 - taskCount: 6
- activeCount: 2 - taskCount: 6
- activeCount: 2 - taskCount: 6
- activeCount: 2 - taskCount: 6
- activeCount: 2 - taskCount: 6
- activeCount: 2 - taskCount: 6
- activeCount: 2 - taskCount: 6
- activeCount: 0 - taskCount: 6
- activeCount: 0 - taskCount: 6
- activeCount: 0 - taskCount: 6
- activeCount: 0 - taskCount: 6
- activeCount: 0 - taskCount: 6
- activeCount: 0 - taskCount: 6
Process finished with exit code 130
8.5 线程池的拒绝策略
RejectedExecutionHandler 是拒绝策略的接口, JUC 为该接口提供了以下几种实现:
- AbortPolicy:拒绝策略
- DiscardPolicy:抛弃策略
- DiscardOldestPolicy:抛弃最老任务策略
- CallerRunsPolicy:调用者执行策略
- 自定义策略
JUC 线程池拒绝策略的接口与类之间的关系图
(1) AbortPolicy
使用该策略时,如果线程池队列满了则新任务被拒绝,并且会抛出 RejectedExecutionException异常。该策略是线程池的默认的拒绝策略。
(2) DiscardPolicy
该策略是 AbortPolicy 的 Silent(安静)版本,如果线程池队列满了,新任务会直接被丢掉,并且不会有任何异常抛出。
(3) DiscardOldestPolicy
抛弃最老任务策略,也就是说如果队列满了,会将最早进入队列的任务抛弃,从队列中腾出空间,再尝试加入队列。因为队列是队尾进队头出,队头元素是最老的,所以每次都是移除对头元素后再尝试入队。
(4) CallerRunsPolicy
调用者执行策略。在新任务被添加到线程池时,如果添加失败,那么提交任务线程会自己去执行该任务,不会使用线程池中的线程去执行新任务。
在以上的四种内置策略中,线程池默认的拒绝策略为 AbortPolicy,如果提交的任务被拒绝,线程池抛出 RejectedExecutionException 异常,该异常是非受检异常(运行时异常),很容易忘记捕获。如果关心任务被拒绝的事件,需要在提交任务时捕获 RejectedExecutionException 异常。
(5)自定义策略
如果以上拒绝策略都不符合需求,则可自定义一个拒绝策略,实现 RejectedExecutionHandler接口的 rejectedExecution 方法即可。
8.5 线程池状态
线程池的 5 种状态,具体如下:
(1) RUNNING:线程池创建之后的初始状态,这种状态下可以执行任务。
(2)SHUTDOWN:该状态下线程池不再接受新任务,但是会将工作队列中的任务执行完毕。
(3) STOP:该状态下线程池不再接受新任务,也不会处理工作队列中的剩余任务,并且将会中断所有工作线程。
(4) TIDYING:该状态下所有任务都已终止或者处理完成,将会执行terminated( )钩子方法。
(5) TERMINATED:执行完terminated( )钩子方法之后的状态。
线程池的 5 种状态定义在 ThreadPoolExecutor 类中,具体代码如下:
public class ThreadPoolExecutor extends AbstractExecutorService {
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
…
}
线程池的状态转换规则为:
(1)线程池创建之后状态为 RUNNING。
(2)执行线程池的 shutdown 实例方法,会使线程池状态从 RUNNING 转变为 SHUTDOWN。
(3)执行线程池的 shutdownNow 实例方法,会使线程池状态从 RUNNING 转变为 STOP。
(4)当线程池处于 SHUTDOWN 状态,执行器 shutdownNow 方法,会将其状态转变为 STOP状态。
(5)等待线程池的所有工作线程停止,工作队列清空之后,线程池状态会从 STOP 转变为TIDYING。
(6)执行完 terminated( ) 钩子方法之后,线程池状态从 TIDYING 转变为 TERMINATED 。