多线程相关概念

进程与线程

一个应用程序,既可以有多个进程,也可以有多个线程,一个进程可以包含多个线程,每个线程相互独立

  • 进程(Process):是一个运行程序的实例
  • 线程(Thread):是程序中的一个执行单元

拿浏览器举例,打开一个浏览器,浏览器是一个进程,浏览器中可以打开很多标签页,每个标签页都是这个浏览器进程的子进程,每个子进程中可以有多个线程来协同完成页面的加载和渲染,比如图片、CSS 和 JS 文件等都是线程来做的

  • 多进程模式:每个进程只有一个线程
  • 多线程模式:一个进程有多个线程
  • 多进程+多线程模式:多个进程,每个进程有多个线程

单核CPU和多核CPU

单核CPU和多核CPU是指计算机处理器的核心数量不同。

  • 单核CPU:单核CPU拥有一个物理处理核心,它只能同时执行一个线程。在单核CPU上运行的多个线程实际上是通过CPU调度算法快速切换执行的,由于处理速度非常快,会给用户造成一种貌似同时执行的错觉。但因为只有一个物理核心,所以在同一时间点上只能处理一个线程的指令。
  • 多核CPU:多核CPU拥有多个物理处理核心,可以同时执行多个线程。每个核心都独立地进行指令执行,因此可以同时运行多个线程,提高了系统的并发性能。多核CPU可以根据具体的负载情况将多个线程分配到不同的核心上执行,从而提高整体的处理能力。

并行和并发

单核CPU只能实现并发,多核CPU既可以实现并行又可以实现并发

  • 并行:同一时刻,多个任务同时执行,每个任务在不同的处理单元上独立执行
  • 并发:同一时刻,多个任务交替执行,所有任务在同一个处理单元上执行

多线程的使用

Thread类

Thread类是Java中用于创建和操作线程的核心类之一,提供了多种方法来管理线程的生命周期、优先级、状态等属性,常用方法如下

方法名介绍
run()线程执行体,在线程被调度时执行的操作,单独调用不会启动新线程
start()启动当前线程,使其进入就绪状态并等待CPU调度,底层调用当前线程的run()方法
sellp(long millis)睡眠,使当前线程暂停指定的时间(以毫秒为单位)
yield()线程的礼让,让出当前CPU执行权,让优先级相同或更高的线程先执行,但礼让的时间不确定,所以不一定礼让成功
join()线程的插队,等待插入的线程执行完毕后,再继续执行当前线程
interrupt()中断线程,不是停止,一般用于中断正在休眠的线程
currentThread()静态方法,返回执行当前代码的线程
getName()获取当前线程的名称
setName(String name)设置当前线程名称
getPriority()获取线程优先级,默认为5,取值范围为1~10
setPriority(int newPriority)设置线程的优先级,取值范围为1~10,数字越大表示优先级越高
getState()获取线程状态
isAlive()判断该线程是否仍在运行
stop()已过时,当执行此方法时,强制结束当前线程

线程的创建

方式一:继承于Thread类(重点)

创建步骤

  1. 创建一个继承于Thread类的子类
  2. 重写Thread类的run()
  3. 创建Thread类的子类对象
  4. 通过此子类对象调用start()启动线程

代码实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// (1)通过继承于Thread类,重写Thread类的run()方法,来开发线程
class MyThread extends Thread {
// (2)重写Thread类的run(),写上自己的逻辑
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
// currentThread():静态方法,返回执行当前代码的线程
System.out.println(i +"线程名:" + Thread.currentThread().getName() + " 优先级:" + getPriority());
// 让线程休眠1秒
Thread.sleep(1000);
// 如果i等于5,则退出线程
if (i == 5) {
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) {
// (3)创建Thread类的子类MyThread的对象,可以当做线程使用
MyThread myThread = new MyThread();
// (4)通过此子类对象调用start()启动线程
myThread.start();
}
}

方式二:实现Runnable接口(重点)

创建步骤

  1. 创建一个实现Runnable接口的实现类
  2. 实现类重写Runnable接口中的抽象方法run()
  3. 创建Runnable接口实现类的对象
  4. 创建Thread类的对象,将实现类的对象作为参数,传递到Thread类的构造器中
  5. 通过Thread类的对象调用start()启动线程

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// (1)通过实现Runnable接口,重写run()方法,来开发线程
class MyThread implements Runnable {
// (2)重写Runnable接口的run()方法,写上自己的逻辑
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
// currentThread():静态方法,返回执行当前代码的线程
System.out.println(i +"线程名:" + Thread.currentThread().getName());
// 让线程休眠1秒
Thread.sleep(1000);
// 如果i等于5,则退出线程
if (i == 5) {
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) {
// (3)创建Runnable接口实现类的对象
MyThread myThread = new MyThread();
// (4)创建Thread类的对象,将实现类的对象作为参数,传递到Thread类的构造器中
Thread thread = new Thread(myThread);
// (5)通过Thread类的对象调用start()启动线程
thread.start();
}
}

方式三:实现Callable接口(了解)

创建步骤

  1. 创建一个实现Callable接口的实现类
  2. 实现call方法,将此线程需要执行的操作声明在call()中
  3. 创建Callable接口实现类的对象
  4. 创建FutureTask的对象,将Callable接口实现类的对象作为参数传递到FutureTask构造器中
  5. 创建Thread对象,将FutureTask的对象作为参数传递到Thread类的构造器中,并调用start
  6. 获取Callable中call方法的返回值

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// (1)创建一个实现Callable接口的实现类
class MyThread implements Callable {
// (2)实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() {
System.out.println(Thread.currentThread().getName() + "执行任务...");
return "call()方法返回值";
}

public static void main(String[] args) throws ExecutionException, InterruptedException {
// (3)创建Callable接口实现类的对象
MyThread myThread = new MyThread();

// (4)创建FutureTask的对象,将Callable接口实现类的对象,作为参数传递到FutureTask构造器中
FutureTask futureTask = new FutureTask(myThread);

// (5)将FutureTask的对象,作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
Thread thread = new Thread(futureTask);
thread.start();

// (6)获取Callable中call方法的返回值
// get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值
Object result = futureTask.get();
System.out.println("线程返回的结果为:" + result);
}
}

三种创建方式的对比

使用实现Runnable接口创建线程的方式比继承Thread类更推荐一些,因为Java是单继承的语言,通过实现接口可以更灵活地扩展其他类或实现其他接口。如果你只需要简单地启动一个线程,并不需要获得执行结果或向上抛出异常,那么继承Thread类或实现Runnable接口都是可以的。如果你需要获得线程的执行结果,或者需要向上抛出异常,那么实现Callable接口创建线程是更好的选择。

  • 继承 Thread 类创建线程:简单直观,但 Java 语言是单继承的,如果继承了 Thread 类,那就不能再继承其他类了。
  • 实现Runnable接口创建线程:可以解决单继承的问题,同时继承其他类或实现其他接口,但不能向上抛出异常,不能获得线程的执行结果。
  • 实现Callable接口创建线程:可以解决以上问题,可以向上抛出异常,可以通过返回值来获取任务的执行结果。
特性继承Thread类实现Runnable接口实现Callable接口
是否可以有返回值和异常××
是否重写run方法
是否支持多线程共享数据
是否可以继承其他类××
是否支持Lambda×
是否支持启动线程start()new Thread().start()Executors.newFixedThreadPool等方式
实现方式的灵活性不够灵活,需要继承更加灵活,可以顶替其他类的实例最灵活,可以按需实现

线程的终止(stop)

如果一个线程已经完成了它的任务,或者不再需要运行,或者需要停止执行,那么我们就需要终止该线程,以便回收资源和避免资源的浪费,终止方式有正常结束、异常结束和强制结束等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 通过继承于Thread类,重写Thread类的run()方法,来开发线程
class MyThread extends Thread {
private int sum = 0;
private boolean loop = true;

public void setLoop(boolean loop) {
this.loop = loop;
}

// 重写Thread类的run(),写上自己的逻辑
@Override
public void run() {
while (loop) {
try {
// currentThread():静态方法,返回执行当前代码的线程
System.out.println(++sum + "线程名:" + Thread.currentThread().getName() + " 优先级:" + getPriority());
// 让线程休眠1秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) throws InterruptedException {
// 创建Thread类的子类MyThread的对象,可以当做线程使用
MyThread myThread = new MyThread();
// 调用start()启动线程
myThread.start();

// stop():已过时,当执行此方法时,强制结束当前线程
// myThread.stop();

// 休眠5秒
Thread.sleep(1000 * 5);

// 设置false,使用变量停止线程
myThread.setLoop(false);
}
}

线程的中断(interrupt)

线程的中断通过 interrupt() 方法实现,用来通知线程停止正在进行的工作,会抛出InterruptedException异常并转移到运行状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 通过继承于Thread类,重写Thread类的run()方法,来开发线程
class MyThread extends Thread {
private int sum = 0;

// 重写Thread类的run(),写上自己的逻辑
@Override
public void run() {
while (true) {
try {
// currentThread():静态方法,返回执行当前代码的线程
System.out.println(++sum + "线程名:" + Thread.currentThread().getName() + " 优先级:" + getPriority());
// 让线程休眠1秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("线程被中断了");
}
}
}

public static void main(String[] args) throws InterruptedException {
//创建Thread类的子类MyThread1的对象,可以当做线程使用
MyThread myThread = new MyThread();
// 调用start()启动线程
myThread.start();
// 睡眠5秒
Thread.sleep(5000);
// interrupt():中断线程,不是停止,一般用于中断正在休眠的线程
myThread.interrupt();
}
}

线程的调度

线程的优先级

(1)高优先级的线程要抢占低优先级线程的CPU的执行权,但是只是从概率上讲,高优先级的线程高概率被执行,并不意外着只有当高优先级的线程执行完以后,低优先级的线程才执行

优先级
MIN_PRIORITY1
NORM_PRIORITY5,默认优先级
MAX_PRIORITY10

(2)相关方法

方法简介
getPriority()获取线程优先级
setPriority(int newPriority)设置线程的优先级

线程的礼让(yield)

yield方法用于暂停当前线程,让出当前CPU执行权,让优先级相同或更高的线程先执行,但礼让的时间不确定,所以不一定礼让成功

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 通过继承于Thread类,重写Thread类的run()方法,来开发线程
class MyThread extends Thread {
// 重写Thread类的run(),写上自己的逻辑
@Override
public void run() {
for (int i = 0; i < 6; i++) {
System.out.println(i + "子线程------");
}
}

public static void main(String[] args) throws InterruptedException {
//创建Thread类的子类MyThread1的对象,可以当做线程使用
MyThread myThread = new MyThread();
// 启动线程
myThread.start();
// 设置子线程插队
for (int i = 0; i < 6; i++) {
System.out.println("--------主线程" + i);
if (i == 3) {
System.out.println("=================礼让子线程================");
// 调用Thread的静态方法yield(),设置线程礼让,让主线程暂停执行,将 CPU 时间片让给其他正在运行的线程
Thread.yield();
}
}
}
}

线程的插队(join)

创建三个线程A、B、C,使用join()方法来让它们按照顺序执行,每个线程执行完毕后,都会插队主线程,阻塞主线程的执行,等待主线程唤醒并继续执行下一个线程,直到所有线程都执行完毕才会执行主线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 通过继承于Thread类,重写Thread类的run()方法,来开发线程
class MyThread extends Thread {

private String name;

public MyThread(String name) {
this.name = name;
}

// 重写Thread类的run(),写上自己的逻辑
@Override
public void run() {
try {
System.out.println("线程" + name + "开始执行...");
Thread.sleep(1000);// 让线程睡眠1秒,模拟执行任务
System.out.println("线程" + name + "执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public static void main(String[] args) throws InterruptedException {
MyThread threadA = new MyThread("A");
MyThread threadB = new MyThread("B");
MyThread threadC = new MyThread("C");

threadA.start();
System.out.println(">>>线程" + threadA.name + "开始插队主线程<<<");
threadA.join(); // A 执行完成后才会轮到 B
System.out.println("线程" + threadA.name + "插队主线程结束");

threadB.start();
System.out.println(">>>线程" + threadB.name + "开始插队主线程<<<");
threadB.join(); // B 执行完成后才会轮到 C
System.out.println("线程" + threadB.name + "插队主线程结束");

threadC.start();
System.out.println(">>>线程" + threadC.name + "开始插队主线程<<<");
threadC.join();// C 执行完成后才会轮到 主线程
System.out.println("线程" + threadC.name + "插队主线程结束");

System.out.println("主线程任务开始执行...");
System.out.println("主线程任务执行完毕");
}
}

线程的分类

用户线程setDeamon(false)

用户线程也叫工作线程,指程序中创建的普通线程,即使所有用户线程都结束了,程序仍会继续执行,不会影响 JVM 的退出,直到所有非守护线程都执行完毕,用户线程可以通过调用 Thread 类的 setDaemon(false) 方法,将其设置为非守护线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 通过继承于Thread类,重写Thread类的run()方法,来开发线程
class MyThread extends Thread {
// 重写Thread类的run(),写上自己的逻辑
@Override
public void run() {
// 无限循环
for (; ; ) {
try {
System.out.println("子线程------");
// 让线程休眠1秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) throws InterruptedException {
// 创建Thread类的子类MyThread的对象,可以当做线程使用
MyThread myThread = new MyThread();

// 设置线程为非守护线程(默认即为非守护线程)
myThread.setDaemon(false);

// 启动线程
myThread.start();

// 主线程循环输出
for (int i = 0; i <= 3; i++) {
System.out.println("--------主线程" + i);
// 让当前线程(即主线程)休眠1秒
Thread.sleep(1000);
}
}
}

守护线程setDeamon(true)

作用是为其他线程提供服务,无论其他线程是否执行完毕,只要守护线程的任务执行完毕,JVM 就会自动退出,常见的守护线程:垃圾回收机制、异常处理线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 通过继承于Thread类,重写Thread类的run()方法,来开发线程
class MyThread extends Thread {
// 重写Thread类的run(),写上自己的逻辑
@Override
public void run() {
// 无限循环
for (; ; ) {
try {
System.out.println("子线程------");
// 让线程休眠1秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) throws InterruptedException {
// 创建Thread类的子类MyThread的对象,可以当做线程使用
MyThread myThread = new MyThread();

// 将该线程设置为守护线程,当主线程结束时,守护线程自动结束(设置守护进程 应在启动线程之前)
myThread.setDaemon(true);

// 启动线程
myThread.start();

// 主线程循环输出
for (int i = 0; i <= 3; i++) {
System.out.println("--------主线程" + i);
// 让当前线程(即主线程)休眠1秒
Thread.sleep(1000);
}
}
}

线程的生命周期

线程的生命周期是指线程从创建到终止的整个过程,可以分为以下几个阶段:

  1. 新建(New):当一个 Thread 类型的实例被创建(new),处于新建状态,此时该线程还没有被启动
  2. 就绪(Runnable):新建状态的线程调用 start() 方法后,线程进入就绪状态,等待系统分配 CPU 使用权,此时线程有执行资格,但是没有执行权
  3. 运行(Running):就绪的线程被调度并获得CPU资源时,便进入运行状态,开始执行 run() 方法中的操作和功能,此时线程有执行资格,同时也有执行权
  4. 阻塞(Blocked):当线程因为某些原因被阻塞时,例如线程在等待获取锁时,如果锁已经被其他线程占用,会等待其他线程释放锁,进入阻塞状态,此时线程没有执行资格,也没有执行权
  5. 等待(Waiting):线程调用了 Object.wait()、Thread.join() 等方法时,将进入等待状态,等待其他线程调用相应的 notify()、notifyAll() 方法唤醒或等待Thread.join()插队完毕,如果线程在等待一定时间后,如果还没有接收到唤醒信号,就会一直处于等待状态,如果线程使用 interrupt() 方法中断,它会抛出InterruptedException异常并转移到运行状态,此时线程没有执行资格,也没有执行权
  6. 限时等待(Timed Waiting):线程调用了Thread.sleep()、Object.wait(timeout)、Thread.join(timeout)等方法后,将进入限时等待状态,等调用的时间到达或者等待其他线程调用相应的唤醒方法,如果线程使用 interrupt() 方法中断,会抛出InterruptedException异常并转移到运行状态,此时线程没有执行资格,也没有执行权
  7. 终止(Terminated):当线程执行完毕(run() 方法 / call() 方法执行完毕)、发生了异常而导致线程停止、调用Thread.stop()方法,线程将进入终止状态,此时线程死亡,变成垃圾,不能再重复使用

线程安全性问题

线程安全问题简介

Java中的线程安全性问题是指在多线程环境下,多个线程同时访问共享资源时可能导致的数据不一致或者意外行为的情况。

相关概念

  • 原子性:原子性指的是一个操作是不可中断的,要么全部执行完成,要么完全不执行。
  • 可见性:可见性指的是当一个线程对共享变量进行修改后,其他线程能够立即看到修改的结果。
  • 有序性:有序性指的是程序执行的顺序与代码的编写顺序是一致的。

常见的线程安全性问题

  • 数据竞争问题(Race Condition):多个线程并发访问和修改共享数据时,一个线程修改或访问了另一个线程未修改完毕的数据,可能导致数据的不一致、数据丢失或造成错误。

  • 线程死锁问题(Deadlock):多个线程相互等待对方释放持有的资源,但不肯相让,导致的互相等待(死锁)

  • 内存可见性问题:当一个线程修改了共享变量的值后,其他线程无法立即看到最新值的情况

数据竞争问题

模拟数据竞争问题

  1. 创建MyThread的实例myThread,并创建两个线程thread1和thread2。
  2. 启动两个线程,每个线程在run()方法中执行10000次counter = counter + 1的操作,即将counter累加1。
  3. 调用thread1.join()thread2.join()方法,等待两个子线程执行完成
  4. 执行代码,计数器的结果应该是20000,但是发现输出最终的counter值会有偏差,出现了数据丢失问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 通过实现Runnable,重写run()方法,来开发线程
class MyThread implements Runnable {
private static int counter = 0;

// 重写接口的run()方法,编写线程的执行逻辑
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
counter = counter + 1;
}
}

public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
Thread thread1 = new Thread(myThread);
Thread thread2 = new Thread(myThread);
thread1.start();
thread2.start();
thread1.join();// 插队主线程,等待线程1完成
thread2.join();// 插队主线程,等待线程2完成
System.out.println("累加器的值: " + counter);
}
}

解决方案

  • 使用互斥锁(Mutex)或 synchronized 关键字来保护共享数据的访问,在同一时间只允许一个线程访问。
  • 使用原子操作类(Atomic Class)来执行共享数据的非阻塞更新。
  • 使用线程安全的数据结构,如 ConcurrentHashMap,它提供了内部一致性保证。

线程死锁问题

死锁产生的原因?

  • 互斥条件:资源不能被同时占用,即在某一时刻只能由一个进程使用
  • 请求与保持条件:进程已经保持至少一个资源,并且正在等待获取其他的资源,但是这些资源可能被其他进程占用
  • 不可剥夺条件:进程已经获得的资源,在未使用完之前,不能被其他进程强制抢占
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

模拟线程死锁

  1. 定义两个线程 “线程A” 和 “线程B”,每个线程都需要获取资源 A 和资源 B。

  2. 线程 A 获取了资源 A 后,需要获取资源 B

  3. 线程 B 已经获取了资源 B,需要获取资源 A

  4. 两个线程就陷入了互相等待的状态,导致死锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 通过继承于Thread类,重写Thread类的run()方法,来开发线程
class MyThread extends Thread {
private static final Object resourceA = new Object();
private static final Object resourceB = new Object();

public void run() {
String currentThreadName = Thread.currentThread().getName();
if (currentThreadName.equals("线程A")) {
synchronized (resourceA) {
System.out.println(currentThreadName + " 获取了资源A");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(currentThreadName + " 需要资源B");
synchronized (resourceB) {
System.out.println(currentThreadName + " 获取了资源B");
}
}
} else if (currentThreadName.equals("线程B")) {
synchronized (resourceB) {
System.out.println(currentThreadName + " 获取了资源B");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(currentThreadName + " 需要资源A");
synchronized (resourceA) {
System.out.println(currentThreadName + " 获取了资源A");
}
}
}
}

// 模拟线程死锁
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
Thread thread1 = new Thread(myThread, "线程A");
Thread thread2 = new Thread(myThread, "线程B");
thread1.start();
thread2.start();
// 睡眠三秒,让线程有足够的时间去获取资源
Thread.sleep(3000);
// 线程状态(BLOCKED)
System.out.println("线程A状态:" + thread1.getState());
System.out.println("线程B状态:" + thread2.getState());
}
}

解决方案

  • 加锁顺序:确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。
  • 超时机制:当请求资源的等待时间超过一定阈值时,放弃请求并进行回退策略。
  • 死锁检测:JDK提供了两种方式来给我们检测死锁位置,图形化工具JConsole和命令行工具Jstack

内存可见性问题

模拟内存可见性问题

  • 线程1通过循环检测flag是否为0,如果是0则继续循环,直到flag的值被线程2更新为非0,循环结束后输出提示信息。
  • 线程2使用Scanner类从控制台接收一个整数,并将其赋值给flag,从而修改flag的值不为0。
  • 线程1永远无法检测到线程2对counter.flag的修改,导致死循环。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class MyThread {
public static void main(String[] args) {
// 线程1:检测flag是否为0,循环结束后输出提示信息
Thread t1 = new Thread(() -> {
while (Counter.flag == 0) {
// 空循环
}
System.out.println("线程一循环结束,flag:" + Counter.flag);
});
// 线程2:从控制台输入一个整数赋值给flag
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个整数:");
Counter.flag = scanner.nextInt();
System.out.println("修改完毕,flag:" + Counter.flag);
});
t1.start();// 启动线程一
t2.start();// 启动线程二
}

// Counter类,用于保存共享变量flag
static class Counter {
static int flag = 0; // 确保线程对其进行读取和写入操作时的可见性
}
}

解决方案

  • 使用 volatile 关键字修饰共享变量volatile关键字修饰的变量可以确保线程对其进行读取和写入操作时的可见性。当一个线程修改了volatile修饰的变量的值时,该变量的新值会立即被其他线程看到,避免了线程间的数据不一致问题。
  • 使用 synchronized 或 Lock 来加锁:进入临界区前将共享变量刷新到主内存,退出临界区后从主内存更新缓存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class MyThread {
public static void main(String[] args) {
// 线程1:检测flag是否为0,循环结束后输出提示信息
Thread t1 = new Thread(() -> {
while (Counter.flag == 0) {
// 空循环
}
System.out.println("线程一循环结束,flag:" + Counter.flag);
});
// 线程2:从控制台输入一个整数赋值给flag
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个整数:");
Counter.flag = scanner.nextInt();
System.out.println("修改完毕,flag:" + Counter.flag);
});
t1.start();// 启动线程一
t2.start();// 启动线程二
}

// Counter类,用于保存共享变量flag
static class Counter {
volatile static int flag = 0; // 确保线程对其进行读取和写入操作时的可见性
}
}

解决线程安全性问题的方案

在多线程编程中,当多个线程同时访问和修改同一个变量时,可能会导致数据竞争和不一致的问题。为了解决这个线程安全问题,可以采用两种常见的解决方案:

  • 时间换空间:通过使用同步机制(如synchronized关键字或Lock对象),确保在同一时间只有一个线程可以访问共享变量。当一个线程正在访问共享变量时,其他线程需要等待,从而避免了并发访问导致的线程安全问题。这种方式以时间换取了空间,但可能会引入线程竞争和上下文切换的开销。
  • 空间换时间:通过使用ThreadLocal将共享变量复制多份,每个线程都拥有自己独立的副本。这样,各个线程之间相互独立,彼此的操作不会相互干扰,避免了数据竞争和不一致的问题。虽然这种方式增加了内存消耗,但提高了程序的并发性能。

线程同步

线程同步的概念

线程同步是一种多线程编程的机制,通常用于协调多个线程之间对共享资源的访问和操作,以避免数据竞争和其他并发问题的发生

线程同步的优点

  • 避免数据竞争和并发问题:多线程环境下,如果多个线程同时访问共享资源而没有同步机制,就会导致数据错误或程序崩溃,线程同步技术能够有效避免这种情况的发生,确保共享资源的完整性。
  • 确保共享资源的完整性:线程同步技术可以保证多个线程对共享资源的访问和操作是有序的,并且只能有一个线程进行,避免了不正确的并发访问,确保了共享资源的完整性。
  • 解决并发问题:线程同步技术能够有效解决多线程环境下的并发问题,确保程序的正确性。

线程同步的缺点

  • 导致程序性能下降:因为只能有一个线程进行同步代码的访问和操作,其他线程需要等待,所以会导致执行效率下降,影响程序的性能。
  • 可能引起死锁:如果同步代码中出现了交叉等待的情况,就可能形成死锁,导致程序无法继续执行,甚至崩溃。
  • 容易出错:线程同步涉及到复杂的并发控制问题,需要精确控制线程的执行顺序和状态,容易出现各种锁定、竞争等问题,需要仔细考虑和测试。

线程同步方式一:使用synchronized关键字

(1)使用synchronized关键字可以实现线程同步,从而确保同一时间,只有一个线程可以访问被保护的代码块或方法

(2)根据synchronized关键字位置不同分为同步代码块和同步方法

1
2
3
4
5
// 同步方法
修饰符 synchronized 返回值 方法名(方法形参){}

// 同步代码块
synchronized(同步监视器){需要被同步的代码}

(3)同步方法和同步代码块代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class MyThread extends Thread {

@Override
public void run() {

}

// 方法中加入synchronized关键字,就是同步方法,该线程完成操作,其他线程才能对该内存地址进行操作
// 非静态的同步方法,此时互斥锁在 this对象
public synchronized void test1() {
System.out.println("线程同步---");
}

// 静态的同步方法,此时互斥锁在 当前类本身(MyThread.class)
public static synchronized void test2() {
System.out.println("线程同步---");
}

// 非静态的同步方法,此时互斥锁在 this对象
public void test3() {
// 同步代码块
synchronized (this) {
System.out.println("线程同步---");
}

}

// 静态的同步方法,此时互斥锁在 当前类本身(MyThread.class)
public static void test4() {
// 同步代码块
synchronized (MyThread.class) {
System.out.println("线程同步---");
}

}

}

线程同步方式二:使用Lock接口

(1)除了使用关键字synchronized外,Java还提供了Lock接口来实现线程同步

(2)Lock接口是Java提供的一种线程同步机制,可以实现对共享资源的互斥访问,但Lock接口不能直接实例化,一般使用实现了Lock接口的ReentrantLock类来创建锁对象

(3)ReentrantLock是一个可重入锁,支持公平锁和非公平锁,可以控制多个线程对共享资源的访问,类中的常用方法如下

方法名描述
lock()尝试获取锁,如果锁已经被其他线程获取,则当前线程会阻塞。如果当前线程已经持有锁,则可以重入获取锁,而不会被阻塞。
tryLock()尝试获取锁,如果锁没有被其他线程获取,则获取锁并返回true;否则返回false,不会阻塞当前线程。
unlock()释放锁,与lock()方法对应使用。只有当前线程获得锁后才能释放锁,否则会抛出IllegalMonitorStateException异常。
newCondition()创建一个Condition对象,该对象可以用于实现更加复杂的线程同步逻辑,比如多个线程协作完成某项任务。
isFair()判断当前锁是否公平锁。
isHeldByCurrentThread()判断当前线程是否持有锁。
getHoldCount()获取当前线程在已经获取锁的情况下重入的次数,即获取锁的次数。
getQueueLength()获取等待锁的线程数。
isLocked()判断当前锁是否被任意一个线程获取。

(4)使用ReentrantLock类实现线程同步案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 通过实现Runnable,重写run()方法,来开发线程
class MyThread implements Runnable {
// 实例化锁
private Lock lock = new ReentrantLock();

// 重写接口的run()方法,写上自己的逻辑
@Override
public void run() {
while (true) {
// 使用lock锁住关键代码段,只有一个线程能够进入
lock.lock();
try {
// 执行操作
System.out.println("线程同步---");
} finally {
// 释放锁
lock.unlock();
}
}
}
}

synchronized与ReentrantLock的异同

使用优先顺序:ReentrantLock -> 同步代码块 -> 同步方法

特性ReentrantLocksynchronized
是否可重入是,同一个线程可以多次加锁是,同一个线程可多次进入同步代码块或方法
是否公平锁可以选择公平锁或非公平锁默认为非公平锁
锁的粒度可以设置不同粒度的锁,提高并发性能锁整个方法或代码块
锁的获取方式tryLock() 方法可以尝试非阻塞地获取锁只能等待锁释放
等待通知机制可以通过 Condition 接口实现复杂的线程间通信
中断处理支持不支持
可轮询的锁请求支持不支持
使用方式需要手动获取和释放锁,需要在 finally 块中确保锁被正确释放自动获取和释放锁

volatile关键字

volatile关键字是一种轻量级的同步机制,使用volatile修饰的变量对所有线程可见,即一个线程修改了该变量的值,其他线程能够立即看到最新的值。

  1. 为了提高程序的运行效率,编译器会对经常访问的变量进行缓存优化,将其缓存在寄存器或高速缓存中。当程序读取这些变量时,可以直接从缓存中获取值,而不需要每次都去访问内存,从而提高程序的执行效率。
  2. 然而,在多线程环境下,由于每个线程都有自己的缓存,当一个线程修改了变量的值时,其他线程可能仍然使用旧的缓存值,导致数据不一致。为了解决这个问题,可以使用volatile关键字修饰需要共享的变量。
  3. 使用volatile修饰的变量,编译器不会对该变量进行缓存优化,每次访问时都直接从主内存中读取值或写入值。当一个线程修改了volatile修饰的变量,其他线程立即能够看到最新的值,从而避免了数据不一致的问题。
  4. 需要注意的是,volatile关键字只保证变量的可读性和可写性,并不能保证对volatile变量的操作是原子性。如果需要保证多个操作的原子性,仍然需要使用锁或其他的同步机制。

ThreadLocal线程本地变量

ThreadLocal简介

(1)ThreadLocal是Java中的一个类,用于在多线程环境下实现线程局部变量。它提供了一种机制,使得每个线程都可以拥有自己独立的变量副本,而不会与其他线程共享。该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量

(2)ThreadLocal的主要作用是在多线程场景下,将数据与线程进行绑定,使得每个线程都拥有自己独立的数据副本,互不干扰,解决共享变量的线程安全问题。

(3)一般都会将ThreadLocal声明成一个静态字段,同时初始化

1
private static ThreadLocal<Object> threadLocal = new ThreadLocal<>();

常用方法

方法名描述
get()获取当前线程持有的变量的值。如果没有设置过变量,则返回初始值(null)或通过initialValue()方法指定的初始值。
set(T value)设置当前线程持有的变量的值为指定的值。
remove()删除当前线程持有的变量。相当于将该变量从当前线程的ThreadLocalMap中移除。
initialValue()返回初始化的变量值。该方法的默认实现返回null,可以通过继承ThreadLocal并重写该方法来指定自定义的初始化值。
withInitial(Supplier<? extends T> supplier)返回一个新的ThreadLocal对象,并将其初始值设置为由给定的Supplier提供的值。该方法可以方便地通过lambda表达式或方法引用指定初始化值。
getMap(Thread t)返回给定线程t关联的ThreadLocalMap对象。ThreadLocalMap用于存储每个线程对应的ThreadLocal变量。
createMap(Thread t, T firstValue)创建一个新的ThreadLocalMap对象,并将给定线程t关联到该Map。同时将初始值firstValue设置为该线程的ThreadLocal变量的值。

售票案例

线程不安全的售票

假设有3个售票员同时卖票,一共有100张票。每个售票员会执行一个循环,每次从总票数中减去1,并输出当前售票员的编号和剩余票数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 通过实现Runnable,重写run()方法,来开发线程
public class TicketSeller implements Runnable {
// 定义一百张票
private int num = 100; // 总票数

// 重写接口的run()方法,编写线程的执行逻辑
@Override
public void run() {
while (true) {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + " 卖出了第 " + num + " 张票,剩余 " + --num + " 张票");
} else {
break;
}
}
}

public static void main(String[] args) {
TicketSeller seller = new TicketSeller();
new Thread(seller, "售票员1").start();
new Thread(seller, "售票员2").start();
new Thread(seller, "售票员3").start();
}
}

由于多个线程同时对共享资源进行修改,并没有进行同步控制,可能出现以下的线程不安全现象

  1. 出现重复售票:由于多个线程同时访问tickets变量并修改它,可能导致多个线程同时卖出同一张票,从而造成售票的重复。
  2. 出现负数售票:由于多个线程同时访问tickets变量并修改它,在最后几张票的情况下,一个线程将tickets减为1时,另一个线程仍然有可能减为0,进而导致tickets变为负数,从而造成程序异常。
  3. 票数不准确:由于多个线程同时访问tickets变量并修改它,可能导致票数不准确。例如,有时候可能只卖出了99张票,或者卖出了101张票。

使用synchronized关键字解决售票问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 通过实现Runnable,重写run()方法,来开发线程
public class TicketSeller implements Runnable {
// 定义一百张票
private int num = 100; // 总票数

// 重写接口的run()方法,写上自己的逻辑
@Override
public void run() {
while (true) {
// 同步控制代码块
synchronized (this) {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + " 卖出了第 " + num + " 张票,剩余 " + --num + " 张票");
} else {
break;
}
}
}
}

public static void main(String[] args) {
TicketSeller seller = new TicketSeller();
new Thread(seller, "售票员1").start();
new Thread(seller, "售票员2").start();
new Thread(seller, "售票员3").start();
}
}

使用Lock接口解决售票问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 通过实现Runnable,重写run()方法,来开发线程
public class TicketSeller implements Runnable {
// 定义一百张票
private int num = 100; // 总票数
private Lock lock = new ReentrantLock(); // 创建一个ReentrantLock对象

// 重写接口的run()方法,写上自己的逻辑
@Override
public void run() {
while (true) {
while (true) {
lock.lock(); // 获取锁
try {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + " 卖出了第 " + num + " 张票,剩余 " + --num + " 张票");
} else {
break;
}
} finally {
lock.unlock(); // 释放锁
}
}
}
}

public static void main(String[] args) {
TicketSeller seller = new TicketSeller();
new Thread(seller, "售票员1").start();
new Thread(seller, "售票员2").start();
new Thread(seller, "售票员3").start();
}
}

线程的阻塞

常见阻塞方式

阻塞状态简介
synchronized同步阻塞当多个线程同时访问同一个对象的 synchronized 代码块或方法时,只有一个线程获得了锁
其他线程需要等待该锁被释放后才能执行,这个过程称为 synchronized 阻塞
wait()等待通知阻塞运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态
直到另一个线程调用该对象的 notify() 或 notifyAll() 方法来通知它继续执行,这个过程称为等待通知阻塞
sleep()睡眠阻塞当线程调用 Thread.sleep() 方法时,会进入休眠状态
等待指定的时间后才能继续执行,这个过程称为 sleep 阻塞
join()线程插队阻塞当一个线程调用另一个线程的 join() 方法时,它将被阻塞,直到被等待线程执行完毕为止

相关方法

wait()、notify()、notifyAll()方法定义在Object类中,必须使用在同步代码块或同步方法中,否则会出现异常

方法名描述状态
sleep()使当前线程休眠指定的时间(单位毫秒),进入 TIMED_WAITING 状态。阻塞
wait()当前线程进入阻塞状态并释放锁,等待某个对象使用notify()或notifyAll()方法唤醒,进入WAITING状态阻塞
notify()唤醒被wait()的一个线程,使其进入可运行状态,但不释放对象锁
如果有多个线程被wait(),则任意唤醒其中一个线程。
非阻塞
notifyAll()唤醒被wait()的所有线程,使它们进入可运行状态,但不释放对象锁。非阻塞
yield()暂停当前线程的执行,并放弃 CPU 的时间片,让其他线程有机会执行。非阻塞
park()暂停当前线程的执行,并使其进入 WAITING 状态,等待 unpark() 方法的唤醒。阻塞
join()等待目标线程执行结束,并且将目标线程的返回值传递给调用 join() 的线程。阻塞

sellp()和wait()异同

sellp()和wait()一旦执行,都可以使当前的线程进入阻塞状态,但是存在一些区别

特征sleep() 方法wait() 方法
所处位置Thread类中Object 类中
调用方式sleep() 方法是静态的,可以在任何地方被调用必须在 synchronized 块或方法中被调用
等待时间指定时间后自动唤醒需要另外线程调用 notify() 或 notifyAll() 才能被唤醒
线程状态进入 TIMED_WAITING 状态进入 WAITING 状态
锁释放不释放持有的锁释放持有的锁
异常处理不会抛出 InterruptedException 异常会抛出 InterruptedException 异常

synchronized同步阻塞

由于线程在执行 synchronized 块时会占用锁,所以当 Thread A 获取到锁并开始执行 synchronized 块时,Thread B 需要等待 Thread A 释放锁之后才能获取锁并执行 synchronized 块,这样就会出现同步阻塞的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 通过实现Runnable,重写run()方法,来开发线程
public class SynchronizedExample implements Runnable {
private static int count = 0;

// 重写接口的run()方法,编写线程的执行逻辑
@Override
public void run() {
synchronized (SynchronizedExample.class) {
System.out.println(Thread.currentThread().getName() + " 开始执行任务");
for (int i = 0; i < 5; i++) {
count++;// 对共享资源count进行加1操作
System.out.println(Thread.currentThread().getName() + " 执行了第" + (i + 1) + "次操作,count=" + count);
}
System.out.println(Thread.currentThread().getName() + " 执行完毕");
}
}

public static void main(String[] args) {
SynchronizedExample example = new SynchronizedExample();
Thread thread1 = new Thread(example, "线程A");
Thread thread2 = new Thread(example, "线程B");
thread1.start();
thread2.start();
}
}

wait()等待通知阻塞

  1. 创建WaitNotifyExample实例example,构造两个线程thread1和thread2,并且启动这两个线程。
  2. 当启动线程后,线程A和线程B都进入了run方法,并在synchronized块中使用 lock.wait() 进入等待通知状态
  3. 主线程睡眠一秒钟后,调用 lock.notifyAll() 方法通知所有等待的线程唤醒
  4. 被唤醒后的线程输出收到通知的信息并继续执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 通过实现Runnable,重写run()方法,来开发线程
public class WaitNotifyExample implements Runnable {
private static Object lock = new Object();

// 重写接口的run()方法,编写线程的执行逻辑
@Override
public void run() {
try {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "获取了锁");
System.out.println(Thread.currentThread().getName() + "等待通知...");

// 使用 lock.wait() 进入等待状态并释放锁
lock.wait();

// 被唤醒后,继续执行任务
System.out.println(Thread.currentThread().getName() + "被唤醒,继续执行...");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public static void main(String[] args) throws InterruptedException {
WaitNotifyExample example = new WaitNotifyExample();
Thread thread1 = new Thread(example, "线程A");
Thread thread2 = new Thread(example, "线程B");
thread1.start();
thread2.start();

// 主线程等待 1 秒钟,等待线程进入等待状态
Thread.sleep(1000);

// 主线程获取锁
synchronized (lock) {
System.out.println("开始唤醒等待中的线程");
lock.notifyAll();// 使用 lock.notifyAll() 方法通知所有等待的线程唤醒
}
}
}

sleep()睡眠阻塞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 通过实现Runnable,重写run()方法,来开发线程
public class SleepExample implements Runnable {
private Object lock = new Object();

// 重写接口的run()方法,编写线程的执行逻辑
@Override
public void run() {
synchronized (lock) {
try {
System.out.println(Thread.currentThread().getName() + " 获取了锁");

// 线程休眠3秒钟
System.out.println(Thread.currentThread().getName() + " 开始休眠");
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + " 休眠结束");

// 休眠结束后继续执行任务
System.out.println(Thread.currentThread().getName() + " 执行任务");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) {
SleepExample example = new SleepExample();
Thread thread = new Thread(example, "线程A");
thread.start();
}
}

join()线程插队阻塞

  1. 创建一个 JoinExample 的实例,构造两个线程thread1和thread2,并且启动这两个线程
  2. 主线程调用 thread2.start() 使线程A进入就绪状态
  3. 主线程调用 thread1.join() 方法,将主线程阻塞
  4. 线程A开始执行
  5. 线程A执行结束后,主线程执行thread2.start()使线程B进入就绪状态
  6. 主线程输出 “主线程继续执行” 的信息,因为线程B是在主线程中等待的,只有在主线程所有任务执行完成之后才会开始执行
  7. 线程B开始执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 通过实现Runnable,重写run()方法,来开发线程
public class JoinExample implements Runnable {
// 重写接口的run()方法,编写线程的执行逻辑
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " 开始执行任务");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " 执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public static void main(String[] args) throws InterruptedException {
JoinExample example = new JoinExample();
Thread thread1 = new Thread(example, "线程A");
Thread thread2 = new Thread(example, "线程B");

// 启动线程A
thread1.start();

// 通过join方法阻塞主线程,等待线程1执行结束后再继续执行
thread1.join();

// 启动线程B
thread2.start();
System.out.println("主线程继续执行");
}
}

线程池

什么是线程池?

线程池是指预先创建一定数量的线程,放置到一个池中,等待调用任务,任务完成后,该线程并不会被销毁,而是重新放回线程池中等待下一次调用

为什么使用线程池?

在高并发情况下,需要频繁地创建线程和销毁线程,对性能影响很大。有了线程池,可以提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁,实现重复利用,提高系统性能和效率,以下是线程池的优点:

  • 降低系统消耗:重复利用已经创建的线程降低线程创建和销毁造成的资源消耗
  • 提高响应速度:当任务到达时,任务不需要等到线程创建就可以立即执行
  • 提供线程管理:可以通过设置合理分配、调优、监控

线程池的常见相关接口和类

Executors线程池工厂类

Executors 是 Java 中提供的线程池工厂类,它包含一些静态方法用于创建不同类型的线程池。

Executors 是一个类而不是接口,提供了一些静态工厂方法来创建不同类型的线程池。

Executors类中方法简介
newFixedThreadPool(nThreads)创建固定大小的线程池,线程池中的线程数量固定为 nThreads
newSingleThreadExecutor()创建只有一个线程的线程池。
newCachedThreadPool()创建可缓存的线程池,线程池的大小可以根据需要自动调整。
newScheduledThreadPool(int corePoolSize)创建定时执行任务的线程池。
newSingleThreadScheduledExecutor()创建只有一个线程的定时执行任务的线程池。
newWorkStealingPool()创建工作窃取线程池,Java 8 中新增的方法。

Executor接口

定义了执行任务的方法,是线程池的顶层接口。

ExecutorService接口

继承自 Executor 接口,定义了一些管理线程池的方法,如提交任务、关闭线程池等。常用的实现类有 ThreadPoolExecutorScheduledThreadPoolExecutor

方法描述
execute(Runnable command)执行给定的任务
submit(Runnable task)提交一个可运行的任务,并返回一个表示任务结果的 Future 对象
submit(Callable<T> task)提交一个可调用的任务,并返回一个表示任务结果的 Future 对象
invokeAll(Collection<? extends Callable<T>> tasks)执行给定的任务集合,并返回一个包含所有任务执行结果的 Future 对象列表
invokeAny(Collection<? extends Callable<T>> tasks)执行给定的任务集合,并返回其中任意一个任务的执行结果,无法保证返回的是哪个任务的结果
boolean isShutdown()判断线程池是否已经调用了 shutdown() 方法
isTerminated()判断线程池是否已经完全终止
shutdown()优雅地关闭线程池,等待已提交的任务执行完成后关闭
shutdownNow()立即关闭线程池,尝试取消所有正在执行的任务,并丢弃等待执行的任务
awaitTermination(long timeout, TimeUnit unit)等待线程池终止

ThreadPoolExecutor类

实现了 ExecutorService 接口,是线程池的核心实现类。用于创建和管理线程池的具体实现类,提供了丰富的功能和灵活的配置选项,构造方法如下:

1
2
3
4
5
6
7
8
9
public ThreadPoolExecutor(
int corePoolSize, // 线程池的核心线程数,即线程池中同时可以执行的线程数量。
int maximumPoolSize, // 线程池的最大线程数,即线程池中最多可以创建的线程数量。
long keepAliveTime, // 空闲线程的存活时间。
TimeUnit unit, // 空闲线程存活时间的单位。
BlockingQueue<Runnable> workQueue, // 用于存放待执行任务的阻塞队列。
ThreadFactory threadFactory, // 用于创建新线程的工厂。
RejectedExecutionHandler handler // 线程池的饱和策略,即当线程池和阻塞队列都满了之后,如何处理新提交的任务。
) { ... }
参数描述简介
corePoolSize线程池的核心线程数线程池中始终保持存活的线程数
maximumPoolSize线程池的最大线程数线程池中允许的最大线程数
keepAliveTime空闲线程的存活时间当线程池中的线程数量超过 corePoolSize 且没有任务可执行时
多余的空闲线程会根据 keepAliveTime 进行存活时间的判断
如果超过指定时间仍然没有任务可执行,则会被回收。
unitkeepAliveTime 参数的时间单位可选秒(TimeUnit.SECONDS)、毫秒(TimeUnit.MILLISECONDS)等
workQueue线程池中的任务队列存储等待执行的任务的阻塞队列,一般可选如下
ArrayBlockingQueue:基于数组的有界阻塞队列
LinkedBlockingQueue:基于链表的阻塞队列
SynchronizedQueue:一个不存储元素的阻塞队列
PriorityBlockingQueue:一个具有优先级的阻塞队列
threadFactory创建新线程的工厂用于创建新线程的工厂类,不指定时会使用默认的线程工厂来创建线程
handler拒绝策略当线程池无法接受新任务时的处理策略,可选的策略如下
AbortPolicy:直接抛出异常
CallerRunsPolicy:调用者所在的线程来运行任务
DiscardPolicy:丢弃队列里最近的一个任务,并执行当前任务
DiscardOldestPolicy:不处理,直接丢掉

ScheduledExecutorService接口

继承自 ExecutorService 接口,定义了一些定时执行任务的方法。常用的实现类有 ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor类

ScheduledThreadPoolExecutor类继承了 ThreadPoolExecutor 类,在其基础上增加了对定时任务的支持,可以按照固定延迟或固定频率执行任务。

方法签名描述
schedule(Runnable command, long delay, TimeUnit unit)在给定的延迟时间后执行任务。
schedule(Callable<V> callable, long delay, TimeUnit unit)在给定的延迟时间后执行可调用任务,并返回表示任务结果的 Future 对象。
scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)以固定的速率执行任务,从指定的初始延迟开始,然后以固定的时间间隔重复执行。
scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)以固定的延迟执行任务,从指定的初始延迟开始,然后在每次执行完成之后等待固定的时间间隔。
setContinueExistingPeriodicTasksAfterShutdownPolicy(boolean value)设置在调用 shutdown 后是否继续执行现有周期性任务的策略。如果为 true,则会继续执行现有任务;如果为 false,则会取消现有任务。
setExecuteExistingDelayedTasksAfterShutdownPolicy(boolean value)设置在调用 shutdown 后是否继续执行现有延迟任务的策略。如果为 true,则会继续执行现有任务;如果为 false,则会取消现有任务。
setRemoveOnCancelPolicy(boolean value)设置在任务被取消时是否从工作队列中移除该任务的策略。如果为 true,则会从队列中移除;如果为 false,则会保留在队列中等待执行。
shutdown()平滑关闭线程池,不再接受新任务,但会等待已提交的任务完成执行。
shutdownNow()立即关闭线程池,尝试取消所有运行中的任务,并返回等待执行的任务列表。
isShutdown()判断线程池是否已调用了 shutdown 方法。
isTerminated()判断线程池是否已完全终止。
awaitTermination(long timeout, TimeUnit unit)阻塞等待线程池终止,直到超时或线程池终止。

如何创建线程池?

线程池的创建方式共包含七种(其中六种是通过 Executors 创建的,一种是通过ThreadPoolExecutor 创建的)根据阿里巴巴的Java技术手册(编码规约),不推荐使用Executors工具类去创建线程池,而是推荐直接使用ThreadPoolExecutor的方式进行创建。

通过Executors线程池工厂类创建线程池

创建线程池可以通过 java.util.concurrent.Executors 类中提供的静态方法来完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建固定大小的线程池,线程池中的线程数量为3
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
// 创建只有一个线程的线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// 创建可缓存的线程池
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 创建定时执行任务的线程池,线程池中的线程数量为3
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
// 创建只有一个线程的定时执行任务的线程池
ScheduledExecutorService singleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();
// 创建工作窃取线程池
ExecutorService workStealingPool = Executors.newWorkStealingPool();

// 向线程池中添加任务并执行
fixedThreadPool.execute(() -> System.out.println("线程" + Thread.currentThread().getName() + "正在执行任务"));
singleThreadExecutor.execute(() -> System.out.println("线程" + Thread.currentThread().getName() + "正在执行任务"));
cachedThreadPool.execute(() -> System.out.println("线程" + Thread.currentThread().getName() + "正在执行任务"));
scheduledThreadPool.schedule(() -> System.out.println("线程" + Thread.currentThread().getName() + "正在执行任务"), 3, TimeUnit.SECONDS);
singleThreadScheduledExecutor.schedule(() -> System.out.println("线程" + Thread.currentThread().getName() + "正在执行任务"), 3, TimeUnit.SECONDS);
workStealingPool.execute(() -> System.out.println("线程" + Thread.currentThread().getName() + "正在执行任务"));

// 关闭线程池,不再接收新的任务,并等待已经提交的任务执行完毕
fixedThreadPool.shutdown();
singleThreadExecutor.shutdown();
cachedThreadPool.shutdown();
scheduledThreadPool.shutdown();
singleThreadScheduledExecutor.shutdown();
workStealingPool.shutdown();
}
}

通过ThreadPoolExecutor创建线程池

除了使用Executors类提供的方法外,我们还可以直接使用ThreadPoolExecutor类来创建线程池,这样可以更加灵活地配置线程池的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建线程池,核心线程数为2,最大线程数为5,等待队列容量为10
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
2, // 核心线程数
5, // 最大线程数
1, // 线程空闲时间
TimeUnit.MINUTES, // 空闲时间单位
new ArrayBlockingQueue<>(10), // 等待队列
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略(当线程池和等待队列都满了时,由提交任务的线程来执行该任务)
);

// 向线程池中添加任务并执行
for (int i = 0; i < 10; i++) {
final int taskId = i;
threadPool.execute(() -> System.out.println("任务" + taskId + "正在执行,线程" + Thread.currentThread().getName()));
}

// 关闭线程池,不再接收新的任务,并等待已经提交的任务执行完毕
threadPool.shutdown();
}
}

线程池的执行流程

  1. 创建线程池:首先,创建一个线程池对象。可以使用 java.util.concurrent.Executors 类提供的静态方法来创建不同类型的线程池,如 newFixedThreadPool()newCachedThreadPool() 等。
  2. 提交任务:通过调用线程池对象的 submit()execute() 方法来提交任务。任务可以是实现了 Runnable 接口或者 Callable 接口的对象。
  3. 任务接收:线程池接收到任务后,会根据线程池的状态和配置来确定如何处理任务。如果线程池中的线程数小于核心线程数,会创建新的线程来执行任务;如果线程池中的线程数已达到核心线程数,任务会被放入任务队列等待执行;如果任务队列已满且线程池中的线程数未达到最大线程数,则会创建新的线程来执行任务;如果线程池中的线程数已达到最大线程数,且任务队列已满,根据拒绝策略来处理任务(如抛出异常、丢弃任务等)。
  4. 任务执行:线程池中的线程从任务队列中取出任务,执行任务的逻辑。执行的方式取决于具体的任务类型,可以是 Runnable 对象的 run() 方法或 Callable 对象的 call() 方法。
  5. 结果返回(仅适用于 Callable 任务):如果任务是 Callable 类型的,并且需要返回结果,线程执行任务后会将结果返回。
  6. 线程回收:任务执行完毕后,线程池中的线程可能会被回收。具体回收的条件取决于线程池的配置,例如空闲时间超过一定阈值、线程池关闭等。
  7. 关闭线程池:当不再需要线程池时,可以调用 shutdown() 方法请求关闭线程池。这会停止接受新的任务,并等待线程池中正在执行的任务执行完毕。然后可以选择调用 awaitTermination() 方法等待线程池中的任务执行完毕,或者直接终止尚未完成的任务。