一、Java 线程、进程、多线程概述

在 Java 中,进程(Process)线程(Thread) 是操作系统进行资源管理和任务调度的重要概念,特别是在高并发和多任务处理场景下,多线程编程尤为重要。

1. 进程(Process)

1.1 进程的定义

进程是操作系统中运行的一个独立程序实例,每个进程都有自己独立的 地址空间资源(如内存、文件、网络连接等),并且相互隔离(简单地讲就是一个正在运行的应用软件就是一个进程)。

1.2 进程的特点

  • 独立性:一个进程的崩溃不会影响其他进程。
  • 资源分配单位:每个进程都有自己的 内存空间、文件描述符 等资源。
  • 通信复杂:进程间通信(IPC,如管道、消息队列、共享内存等)比线程间通信复杂。
  • 系统开销大:创建或销毁进程的开销远高于线程。

2. 线程(Thread)

2.1 线程的定义

线程是 进程内部的执行单元一个进程可以包含多个线程,这些线程共享进程的内存和资源,但每个线程有自己独立的栈空间和寄存器。(比如:快餐店就是一个进程,而店内的不同员工(点餐员、厨师、服务员)就相当于多个线程,它们可以同时进行不同的任务。)

2.2 线程的特点

  • 轻量级:相比进程,线程创建和销毁的开销更小。
  • 共享资源:同一进程的多个线程共享 内存、文件句柄等 资源,因此线程间通信更容易,但也可能引发线程安全问题
  • 并行执行:多个线程可以同时运行,提高程序的并发能力。

3. 多线程(Multithreading)

3.1 什么是多线程?

多线程指的是一个进程内同时运行多个线程,这些线程共享内存,但各自有独立的执行路径。

3.2 多线程的优点

  • 提高程序效率:多个线程可以并发执行,提高 CPU 利用率。
  • 减少资源消耗:多个线程共享内存,而多个进程需要单独的内存空间。
  • 提升用户体验:如 UI 线程 + 后台线程,避免界面卡顿。

3.3 线程的生命周期

Java 线程的生命周期主要包括:

  1. 新建(New)Thread t = new Thread();
  2. 就绪(Runnable)t.start();
  3. 运行(Running):CPU 调度执行 run() 方法。
  4. 阻塞(Blocked/Waiting)
    • sleep(time):让线程休眠
    • join():等待另一个线程完成
    • wait():等待某个条件
  5. 终止(Terminated):线程执行完毕,或 stop()(不推荐)结束。

二、并发和并行

1. 并发

并发:在同一个时间段内,有多个指令在单个CPU上交替执行。

  • 多个任务在同一时间段内交替执行,但不一定同时执行。

  • 适用于单核 CPU,通过任务切换让多个任务看起来是“同时”进行的。

2. 并行

并行:在同一个时刻,有多个指令在多个CPU上同时执行。

  • 多个任务真正同时执行,需要多核 CPU 支持,每个任务运行在不同的 CPU 核心上。

  • 适用于多核处理器,每个线程可以真正并行运行,不会互相等待。

三、多线程的实现方式

1. 继承Thread类的方式

步骤:

(1)自己定义一个类继承Thread类

(2)重写run()方法

(3)创建自定义的类对象,并调用**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
package com.ryan.thread;

public class ThreadDemo {
public static void main(String[] args) {
// 创建两个线程对象
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();

// 此处可以给两个线程起名
myThread1.setName("线程1");
myThread2.setName("线程2");

// 启动线程
myThread1.start();
myThread2.start();

}
}

// 自定义一个线程继承Thread类
class MyThread extends Thread {
@Override
public void run() {
// 书写线程要执行的代码
for (int i = 0; i < 100; i++) {
System.out.println(this.getName() + " Hello");
}
}
}

2. 实现Runnable接口的方式

步骤:

(1)自己定义一个类实现Runnable接口

(2)重写run()方法

(3)创建自定义的类对象

(4)创建一个Thread类的对象,传入自定义的类对象后,开启线程

示例:

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
package com.ryan.thread;

public class ThreadDemo2 {
public static void main(String[] args) {
// (3)创建自定义的类对象
MyRun mr = new MyRun();

// (4)创建一个Thread类的对象,传入自定义的类对象
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);

// 给线程定义一个名字
t1.setName("线程1");
t2.setName("线程2");

// 开启线程
t1.start();
t2.start();
}
}

// (1)自己定义一个类实现Runnable接口
class MyRun implements Runnable {

// (2)重写run()方法
@Override
public void run() {
// 书写线程要执行的代码
for (int i = 0; i < 100; i++) {
// 获取当前的线程对象
Thread t = Thread.currentThread();
System.out.println(t.getName() + " Hello");
// 或者 :System.out.println(Thread.currentThread().getName() + " Hello");
}
}
}

3. 利用Callable接口和Future接口方式

特点:可以获取到多线程运行的结果

步骤:

(1)创建一个类MyCallable实现Callable接口

(2)重写call()方法(是有返回值的,表示多线程运行的结果)

(3)创建MyCallable类的对象(表示多线程要执行的任务)

(4)创建FutureTask类的对象(FutureTask是Future接口的实现类)(作用是管理多线程运行的结果)

(5)创建Thread类的对象,并启动线程(表示线程)

示例:

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
package com.ryan.thread;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadDemo3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// (3)创建MyCallable类的对象
MyCallable mc = new MyCallable();
// (4)创建FutureTask类的对象
FutureTask<Integer> ft = new FutureTask<>(mc);
// (5)创建Thread类的对象,并启动线程
Thread t1 = new Thread(ft);
t1.start();
// 获取多线程运行的结果
Integer result = ft.get();
System.out.println(result);
}
}

// (1)创建一个类MyCallable实现Callable接口
class MyCallable implements Callable<Integer> {
// 重写call()
@Override
public Integer call() throws Exception {
// 求1~100之间的和
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
return sum;
}
}

4. 总结

实现方式 优势 适用场景 备注
继承 Thread 结构简单,直接调用 start() 启动线程 适用于简单的多线程任务 受限于 Java 单继承,不推荐
实现 Runnable 接口 可以继承其他类,线程任务独立 适用于大多数多线程任务 推荐使用
实现 Callable 接口 可以返回结果,可以抛出异常 适用于需要返回结果的任务 需结合 FutureTask 使用

四、多线程中常用的成员方法

方法名称 说明
String getName() 返回此线程的名称
void setName(String name) 设置线程的名字(构造方法也可以设置名字)
static Thread currentThread() 获取当前线程的对象
static void sleep(long time) 让线程休眠指定的时间,单位为毫秒
setPriority(int newPriority) 设置线程的优先级
final int getPriority() 获取线程的优先级
final void setDaemon(boolen on) 设置守护线程
public static void yield() 礼让线程/出让线程
public static void join() 插入线程/插队线程

第一部分

getName()和setName()细节:

  1. 如果没有给线程设置名字,线程是有默认的名字的。格式为:Thread-X(X:表示序号,从0开始)。
  2. 如果要给线程设置名字,可以用setName()方法进行设置,也可以用构造方法进行设置(需要在自定义类中调用父类Thread类的构造方法)。

currentThread()细节:

  1. 哪个线程执行到这个方法,此时获取的就是哪条线程的对象。
  2. 当JVM启动之后,会自动的启动多条线程,其中有一条线程就叫做main线程,作用是调用main方法,并执行其中的代码。在以前,我们写的所有代码,其实都是运行在main线程中的。

sleep()细节:

  1. 哪条线程执行到这个方法,哪条线程就会在这里停留对应的时间。
  2. 方法的参数:表示睡眠时间,单位是毫秒(1s = 1000ms)
  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
38
39
40
41
42
package com.ryan.thread;

public class ThreadDemo4 {
public static void main(String[] args) {
// 1.setName() 2.构造方法也可以设置名字
/* MyThreadTwo t1 = new MyThreadTwo("飞机");
MyThreadTwo t2 = new MyThreadTwo("坦克");*/

// 开启线程
/* t1.start();
t2.start();*/

// 不开启任何线程时,得到调用main方法的线程"main"
Thread t = Thread.currentThread();
String name = t.getName();
System.out.println(name); // 输出结果:main
}
}

class MyThreadTwo extends Thread {
// 想要使用构造方法来设置名字,就要调用父类Thread的构造方法
public MyThreadTwo() {
}

public MyThreadTwo(String name) {
super(name);
}

@Override
public void run() {
for (int i = 0; i < 100; i++) {
// 每打印一次休眠1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(this.getName() + "#" + i);
}
}
}

第二部分 - 线程优先级

线程的调度

1. 抢占式调度(java使用的就是这个):表示多个线程在抢占CPU的执行权,CPU在什么时候执行哪个线程是不确定的,执行多长时间也是不确定的。体现了随机性。

2. 非抢占式调度:表示所有的线程轮流地执行,执行的时间也是差不多的。

3. 线程的优先级越大,抢到CPU的执行权的概率也就越大(不代表百分百能抢到)。

4. 在java中,线程优先级分为10档,最小的是1,最大的是10,默认为5。

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
package com.ryan.thread;

public class ThreadDemo5 {
public static void main(String[] args) {
// 线程优先级
// 创建线程要执行的参数对象
MyThreadThree mt = new MyThreadThree();
// 创建线程对象,调用Thread(Runnabel o, String name)设置线程名
Thread t1 = new Thread(mt, "飞机");
Thread t2 = new Thread(mt, "坦克");

// 1. 未设置线程优先级时,默认为5
System.out.println(t1.getPriority());
System.out.println(t2.getPriority());
// 2. 获取main线程的优先级,也是默认5
System.out.println(Thread.currentThread().getPriority());

// 3. 设置线程优先级
t1.setPriority(1);
t2.setPriority(10);
// 启动线程
t1.start();
t2.start();
}
}
class MyThreadThree implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "#" + i);
}
}
}

第三部分 - 守护线程

final void setDaemon(boolen on)细节:

  1. 当其它的非守护线程执行完毕后,守护线程会陆续结束。
  2. 通俗解释就是,当非守护线程执行结束后,守护线程就没有必要继续执行了,所以也会跟着结束。
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
package com.ryan.thread;

public class ThreaDemo6 {
public static void main(String[] args) {
Thread1 t1 = new Thread1();
Thread2 t2 = new Thread2();
t1.setName("女神");
t2.setName("备胎");
// 将t2设置为守护线程
t2.setDaemon(true);

t1.start();
t2.start();
}
}
class Thread1 extends Thread {
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(this.getName() + "#" + i);
}
}
}

class Thread2 extends Thread {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println(this.getName() + "#" + i);
}
}
}

第四部分 - 礼让线程和插队线程

public static void yield()

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
package com.ryan.thread;

public class ThreadDemo7 {
public static void main(String[] args) {
thread3 t1 = new thread3();
thread3 t2 = new thread3();

t1.setName("飞机");
t2.setName("坦克");

t1.start();
t2.start();
}
}

class thread3 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(this.getName() + "#" +i);
}
// 出让当前CPU的执行权
// 结果是尽可能的让飞机和坦克两个线程打印的结果均匀一点,但不能保证百分百均匀。
Thread.yield();
}
}

public static void 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
package com.ryan.thread;

public class ThradDemo8 {
public static void main(String[] args) throws InterruptedException {
thread4 t = new thread4();
t.setName("土豆");
t.start();
/*
线程t:土豆
将线程t插入到当前线程之前
当前线程:main线程
*/
t.join();
// 执行在main线程中的代码
for (int i = 0; i < 10; i++) {
System.out.println("main线程" + i);
}
}
}

class thread4 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(this.getName() + "#" +i);
}
}
}

五、线程的生命周期

线程执行全过程图

img

六、线程安全的问题

需求:

某电影院目前正在上映国产大片,共有100张票,而它有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
38
39
40
41
package com.ryan.thread;

public class ThreadDemo9 {
public static void main(String[] args) {
thread5 t1 = new thread5();
thread5 t2 = new thread5();
thread5 t3 = new thread5();

// 设置窗口名
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");

// 启动线程
t1.start();
t2.start();
t3.start();

}
}

class thread5 extends Thread {
// 将ticket变量设置为静态的,以保证三个窗口卖的是100张票(用一个ticket,否则会卖出300张)
static int ticket = 0;
@Override
public void run() {
while(true) {
if (ticket < 100) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(this.getName() + "在卖第" + ticket + "张票");
} else {
break;
}
}
}
}

出现的问题:

  1. 相同的票出现了多次。
  2. 出现了超出范围的票。

同步代码块 synchronized

作用:把操作共享数据的代码锁起来(当有线程在操作该代码块时,其它线程必须等待该线程执行完毕才能抢夺CPU执行权)。

格式:

1
2
3
synchronized (锁) {
操作共享数据的代码
}

特点

  1. 锁默认是打开的,有一个线程进去后,锁自动关闭。
  2. 里面的代码全部执行完毕,线程出来,锁自动打开。

改进后的写法

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
package com.ryan.thread;

public class ThreadDemo9 {
public static void main(String[] args) {
thread5 t1 = new thread5();
thread5 t2 = new thread5();
thread5 t3 = new thread5();

// 设置窗口名
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");

// 启动线程
t1.start();
t2.start();
t3.start();

}
}

class thread5 extends Thread {
// 将ticket变量设置为静态的,以保证三个窗口卖的是100张票(用一个ticket,否则会卖出300张)
static int ticket = 0;

// 锁对象,一定要是唯一的
static Object obj = new Object();

@Override
public void run() {
while(true) {
synchronized (obj) {
if (ticket < 100) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(this.getName() + "在卖第" + ticket + "张票");
} else {
break;
}
}
}
}
}

细节:

  1. 不能将synchronized (锁对象)放在循环while (true)外面,否则就相当于线程1卖完所有票后,其它线程才开始卖票,但是票已经卖完了。
  2. synchronized (锁对象)中的锁对象一定要是唯一的,所以一般都是将锁对象写成当前类的字节码文件。如上例,可更改为:sychronized (thread5.class)

同步方法 synchronized

就是将synchronized关键字加到方法上。

格式

1
修饰符 synchronized 返回值类型 方法名(方法参数) {...}

特点

  1. 同步方法是锁住方法里面所有的代码
  2. 锁对象不能自己指定:

(1)若当前为非静态方法:则锁对象为:this

(2)若当前为静态方法:则锁对象为:当前类的字节码文件对象*(当前类名.class)

技巧

当不知道应该将哪些代码写入同步方法中时,可以先写同步代码块,之后再将同步代码块中的代码抽取成同步方法

同步代码块版:

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
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.ryan.thread;

public class ThreadDemo10 {
public static void main(String[] args) {
// 1. 创建MyRunnable1类对象
MyRunnable1 mr = new MyRunnable1();
// 2. 将MyRunnable1类对象当作参数传入Thread构造方法中,创建线程对象
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
Thread t3 = new Thread(mr);
// 给线程设置名字
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
// 4. 启动线程
t1.start();
t2.start();
t3.start();
}
}

class MyRunnable1 implements Runnable {
// 定义共享的数据ticket
/*
因为创建的类是实现Runnable接口的,
在主程序中只会创建一个对象,并且作为一个参数让线程去执行的。
所以ticket就不需要定义为静态的static
*/
int ticket = 0;

@Override
public void run() {
/*
1. 循环
2. 同步代码块(再转为同步方法)
3. 判断共享数据是否到了末尾,如果到了末尾...
4. 判断共享数据是否到了末尾,如果没到末尾...
*/
while (true) {
synchronized (MyRunnable1.class) {
if (ticket == 100) {
break;
} else {
ticket++;
/*
这里不能直接使用this.getName()来获取线程名,
因为this指向的是Runnable对象,而不是Thread对象。
在Runnable接口中并没有提供方法来获取线程的名字。
要获取线程的名字,必须通过Thread.currentThread().getName()来获取当前线程的名字。
*/
System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票");
}
}
// 为了提高性能,一般把线程睡眠放在同步代码块之外。
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}

}
}

同步方法版:

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
51
52
53
54
package com.ryan.thread;

public class ThreadDemo10 {
public static void main(String[] args) {
// 1. 创建MyRunnable1类对象
MyRunnable1 mr = new MyRunnable1();
// 2. 将MyRunnable1类对象当作参数传入Thread构造方法中,创建线程对象
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
Thread t3 = new Thread(mr);
// 给线程设置名字
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
// 4. 启动线程
t1.start();
t2.start();
t3.start();
}
}

class MyRunnable1 implements Runnable {
int ticket = 0;

@Override
public void run() {
while (true) {
if (method()) {
break;
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}

}
/*
在idea中,选中需要设置为方法的代码,按快捷键 ctrl +alt + m即可。
抽出为方法后,加入synchronized关键字定义为同步方法。
此时方法是非静态的,锁对象为:this
而这里的this就是在main方法中创建的mr,mr是唯一的。
*/
private synchronized boolean method() {
if (ticket == 100) {
return true;
} else {
ticket++;
System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票");
}
return false;
}
}

idea中将选中的代码定义为一个方法的快捷键:ctrl + alt + m

拓展 StringBuilder和StringBuffer

根据java API帮助文档,可以看出StringBuilder和StringBuffer的方法都是一样的。但是StringBuilder是线程不安全的StringBuffer是线程安全的(在源码中StringBuilder的所有方放都是普通的方法,而StringBuffer的所有方法都加了synchronized关键字,都是同步方法)。

选择

当代码都是单线程的,不需要考虑多线程的情况时,选择StringBuilder

如果是多线程环境下,需要考虑到线程安全问题,则选择StringBuffer

Lock锁

  1. 虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁。为了更清晰的表达如何加锁和释放锁,JDK5之后提供了一个新的锁对象Lock

  2. Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。

  3. Lock中提供了获得锁和释放锁的方法:

(1)void lock(); 获得锁

(2)void unLock(); 释放锁

手动上锁,手动释放锁。

  1. Lock是接口不能直接实例化,可以采用它的实现类ReentrantLock来实例化

ReentrantLock的构造方法:

ReentrantLock():创建一个ReentrankLock的实例。

示例

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
51
52
53
54
package com.ryan.thread;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ThreadDemo10 {
public static void main(String[] args) {
// 1. 创建MyRunnable1类对象
MyRunnable1 mr = new MyRunnable1();
// 2. 将MyRunnable1类对象当作参数传入Thread构造方法中,创建线程对象
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
Thread t3 = new Thread(mr);
// 给线程设置名字
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
// 4. 启动线程
t1.start();
t2.start();
t3.start();
}
}

class MyRunnable1 implements Runnable {
int ticket = 0;
// 如果自定义类使用的是继承Thread类的方式,则需要加static
Lock lock = new ReentrantLock();

@Override
public void run() {
while (true) {
lock.lock();
/*
使用try-catch-finally,
并将判断放入try中,
最后不论代码执行得如何,都要保证关闭锁。
*/
try {
if (ticket == 100) {
break;
} else {
ticket++;
System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 保证关闭锁
lock.unlock();
}
}
}
}

七、死锁

1. 什么是死锁?

死锁是指两个或多个线程相互等待对方释放资源,导致程序永久阻塞的一种情况。发生死锁的典型场景是:

  • 线程A持有资源X,想获取资源Y;
  • 线程B持有资源Y,想获取资源X;
  • 线程A和B都不释放自己已占有的资源,导致程序进入无限等待状态。

2. 死锁产生的四个必要条件

死锁的产生必须同时满足以下四个条件

  1. 互斥条件(Mutual Exclusion)
    • 资源一次只能被一个线程占用,其他线程必须等待。
  2. 占有并等待(Hold and Wait)
    • 线程已经持有了至少一个资源,并且正在等待获取其他被占用的资源。
  3. 非抢占条件(No Preemption)
    • 线程已经持有的资源不能被其他线程强行抢占,只能由线程自己释放。
  4. 循环等待条件(Circular Wait)
    • 存在一个线程等待的循环链,每个线程都在等待下一个线程所持有的资源。

3. 如何避免死锁

要避免死锁,需要破坏死锁产生的四个必要条件之一。以下是常见的避免死锁的方法:

  1. 破坏占有并等待条件
  • 让线程一次性获取所有需要的资源,而不是分多次获取。
  • 例如,使用一个全局锁来保护所有资源的获取。
  1. 破坏非抢占条件
  • 允许线程释放已经持有的资源,如果它无法获取其他资源。
  • 例如,使用 tryLock() 方法尝试获取锁,如果失败则释放已持有的锁。
  1. 破坏循环等待条件
  • 对资源进行排序,要求线程按照固定的顺序获取资源。
  • 例如,在上面的示例中,可以要求所有线程先获取 lock1,再获取 lock2
  1. 使用超时机制
  • 在获取锁时设置超时时间,如果超时则放弃并释放已持有的锁。
  • 例如,使用 ReentrantLocktryLock(long timeout, TimeUnit unit) 方法。

八、生产者和消费者(等待唤醒机制)

生产者-消费者模式(Producer-Consumer Pattern)是一个十分经典的多线程写作的模式。

1. 什么是生产者-消费者模式?

生产者-消费者模式是一种线程间协作(Inter-thread Cooperation)机制,主要用于解决多线程环境下的生产和消费同步问题

  • 生产者(Producer):负责生产数据,并将数据放入缓冲区(队列)。
  • 消费者(Consumer):从缓冲区获取数据并进行处理。
  • 缓冲区(共享队列):用于存储生产者生产的数据,消费者从中取数据。

2. 为什么要使用生产者-消费者模式?

  • 解耦生产与消费:生产者和消费者不直接交互,而是通过缓冲区进行通信,使它们可以独立扩展。
  • 提高系统吞吐量:多个生产者和消费者可以并行工作,提高整体处理能力。
  • 解决线程同步问题:适用于多线程环境下的任务协调。

3. 生产者和消费者(常见方法)

方法名称 说明
void wait() 当前线程等待,直到被其它线程唤醒
void notify() 随机唤醒单个线程
void notifyAll() 唤醒所有线程

示例图

img

代码实现

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
package com.ryan.thread;

public class ThreadDemo11 {
public static void main(String[] args) {
/**
* 需求:完成生产者和消费者(等待唤醒机制)的代码
* 实现线程轮流交替执行的效果
*/
Cook c = new Cook();
Foodie f = new Foodie();
c.setName("厨师");
f.setName("吃货");
c.start();
f.start();

}
}

class Cook extends Thread {
// 生产者:厨师

@Override
public void run() {
/**
* 1. 循环
* 2. 同步代码块
* 3. 判断共享数据是否到了末尾(到了末尾)
* 4. 判断共享数据是否到了末尾(没到了末尾,执行核心逻辑)
*/
while (true) {
synchronized (Desk.lock) {
if (Desk.count == 0) {
break;
} else {
if (Desk.foodFlag == 1) {
// 如果有,就等待
try {
Desk.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
// 如果没有,就制作食物
System.out.println("厨师做了一碗面条");
// 修改桌子上的食物状态
Desk.foodFlag = 1;
// 叫醒等待的消费者开吃
Desk.lock.notifyAll();
}
}
}
}
}
}

class Foodie extends Thread {
// 消费者:吃货

@Override
public void run() {
/**
* 1. 循环
* 2. 同步代码块
* 3. 判断共享数据是否到了末尾(到了末尾)
* 4. 判断共享数据是否到了末尾(没到了末尾,执行核心逻辑)
*/
while (true) {
synchronized (Desk.lock) {
if (Desk.count == 0) {
break;
} else {
// 先判断桌子上是否有面条
if (Desk.foodFlag == 0) {
// 如果没有,就等待
try {
// 让当前线程跟锁对象进行绑定
// 做等待操作。
Desk.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
// 如果有,就把面条总数-1
Desk.count--;
// 然后开吃
System.out.println("吃货正在吃面条,还能再吃" + Desk.count + "碗");
// 吃完之后,唤醒厨师继续做
// 唤醒所有绑定了锁对象的线程
Desk.lock.notifyAll();
// 修改桌子的状态
Desk.foodFlag = 0;
}
}
}
}

}
}

class Desk {
/**
* 桌子作用:控制生产者和消费者的执行
*/

// 是否有面条 0:没有面条 1:有面条
public static int foodFlag = 0;

// 面条总数
public static int count = 10;

// 锁对象
public static Object lock = new Object();
}


4. 等待唤醒机制(阻塞队列方式实现)

示例图

img

阻塞队列的继承结构

阻塞队列实现了四个接口:iterable, Collection, Queue, BlockingQueue

两个实现类:

  1. ArrayBlockingQueue:底层是数组,有界(创建对象的时候需要指定长度)。
  2. LinkedBlockingQueue:底层是链表,无界但不是真正的无界,最大为int的最大值。

代码实现

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package com.ryan.thread;

import java.util.concurrent.ArrayBlockingQueue;

public class ThreadDemo12 {
public static void main(String[] args) {
/**
* 需求:利用阻塞队列完成生产者和消费者(等待唤醒机制)
* 细节:
* 生产者和消费者必须使用同一个阻塞队列
*/

// 为保证生成者和消费者使用同一个阻塞队列,
// 1. 选择在测试类main当中创建阻塞队列对象
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
// 2. 创建线程对象,并把阻塞队列传递进去
Cook1 c = new Cook1(queue);
Foodie1 f = new Foodie1(queue);
c.start();
f.start();

}
}

class Cook1 extends Thread {
// 创建一个成员变量,只定义不给值
ArrayBlockingQueue<String> queue;

// 当在测试类中创建对象时,传入测试类中创建的唯一的阻塞队列
public Cook1(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}

@Override
public void run() {
while (true) {
// 不断地把面条放到阻塞队列中
try {
// 这里不需要synchronized上锁
// 因为在put()方法底层中,已经使用lock锁对象的方式上锁和释放锁了
queue.put("面条");
System.out.println("厨师放了一碗面条");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

class Foodie1 extends Thread {
ArrayBlockingQueue<String> queue;

public Foodie1(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}

@Override
public void run() {
while (true) {
// 不断地从阻塞队列中获取面条
try {
// take()方法底层也是有锁的
// 我们就不需要自己在外面加锁了,
// 否则容易导致锁的嵌套(死锁)
String food = queue.take();
System.out.println(food);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

九、多线程的6种状态

示例图

img

java中没有运行状态,因为在线程抢到CPU执行权后,线程就会交给操作系统进行处理,而JVM就不管了。

6种状态

新建状态(NEW) –> 创建线程对象

就绪状态(RUNNABLE)–> start方法

阻塞状态(BLOCKED)–> 无法获得锁对象

等待状态(WAITING)–> wait方法

计时等待(TIMED_WAITING)–> sleep方法

结束状态(TERMINATED)–> 全部代码运行完毕

十、线程池

我们使用之前多线程的写法的弊端:用到线程的时候就创建,用完后线程消失,这样会浪费系统资源。

线程池

  1. 创建一个池子,池子中是空的
  2. 提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还给池子下回再次提交任务时,不需要创建新的线程,直接复用已有的线程即可。
  3. 但是如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待。

代码实现步骤

  1. 创建线程池
  2. 提交任务(提交任务时,线程池底层会创建线程或者复用 已经存在的线程,这些代码不需要我们写,我们负责的就是提交任务)。
  3. 所有任务全部执行完毕,关闭线程池(实际开发中线程池是不会关闭的,因为服务器一般是24小时都在运行)。

线程池代码实现

Executors:线程池的工具类通过调用方法返回不同类型的线程池对象。

方法名称 说明
public static ExecutorService newCachedThreadPool() 创建一个没有上限的线程池
public static ExecutorService newFixedThreadPool(int nThreads) 创建一个有上限的线程池
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
package com.ryan.threadpool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MyThreadPoolDemo {
public static void main(String[] args) {

// 1. 创建线程池对象
ExecutorService pool1 = Executors.newCachedThreadPool();

ExecutorService pool2 = Executors.newFixedThreadPool(3);

// 2. 提交任务
pool1.submit(new MyRunnable());

/**
* 虽然创建了四个线程,但是实际线程池只给了3个线程
*/
pool2.submit(new MyRunnable());
pool2.submit(new MyRunnable());
pool2.submit(new MyRunnable());
pool2.submit(new MyRunnable());

// 3. 销毁线程池,一般不销毁
// pool1.shutdown();

}
}

class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + "---" + i);
}
}
}

十一、自定义线程池

Java 提供了 ThreadPoolExecutor 作为核心的线程池实现,但有时我们需要自定义线程池来满足特定需求,比如:

  • 限制最大线程数,防止过载。
  • 自定义拒绝策略,比如日志记录或降级处理。
  • 监控线程池状态,提高可观测性。

使用ThreadPoolExecutor类

ThreadPoolExecutor中有七个参数。

参数解析

参数 说明
核心线程数量 不能小于0
线程池中最大线程的数量 最大数量>=核心线程数量
空闲时间(值) 不能小于0
空闲时间(单位) TimeUnit指定
阻塞队列(任务队列) 不能为null
创建线程的方式(创建线程工厂) 不能为null
要执行的任务过多时的任务拒绝策略 不能为null

任务拒绝策略

任务拒绝策略 说明
ThreadPoolExecutor.AbortPolicy (默认策略):丢弃任务并抛出RejectedExecutionException异常
ThreadPoolExecutor.DiscardPolicy 丢弃任务,但是不抛出异常,这是不推荐的做法
ThreadPoolExecutor.DiscardOldestPolicy 抛弃队列中等待最久的任务,然后把当前任务加入队列中
ThreadPoolExecutor.CallerRunsPolicy 调用任务的run()方法绕过线程池直接执行

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.ryan.threadpool;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class MyThreadPoolDemo1 {
public static void main(String[] args) {
// 创建自定义线程池对象
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3, // 核心线程数量
6, // 线程池中最大线程的数量
60, // 空闲时间(值)
TimeUnit.SECONDS, // 空闲时间(单位)
new ArrayBlockingQueue<>(3), // 阻塞队列(任务队列)
Executors.defaultThreadFactory(), // 创建线程工厂
new ThreadPoolExecutor.AbortPolicy() // 任务拒绝策略
);

// 剩下就是和之前的一样,提交任务...
}
}

小结

步骤

  1. 创建一个空的线程池
  2. 有任务提交时,线程池会创建线程去执行任务,执行完毕归还线程

不断提交任务,会有以下三个临界点

  1. 当核心线程满时,再提交任务就会排队
  2. 当核心线程满,队伍满时,会创建临时线程
  3. 当核心线程满,队伍满,临时线程满时,会触发任务拒绝策略