封面来源:碧蓝航线 铁血、音符 & 誓言 活动CG

本文视频参考地址:【狂神说Java】多线程详解

1. 线程简介

1.1 多任务

什么叫多任务?

其实现实生活中就有很多这样的例子,比如:边吃饭边玩手机的低头族、又开车又打电话还挂点滴的疯狂司机,以及边玩手机边撇条的“厕所困难户”。当然这些行为都是不好的!

现实中这样的例子有很多,看起来是多个任务都在做,实质上我们的大脑在同一时间依旧只做了一件事情。

1.2 普通方法调用和多线程

普通方法调用和多线程

1.3 程序、进程、线程

说起进程(Process),就不得不说说 程序 。程序是指指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。

进程 则是执行程序的一次执行过程,它是一个动态的概念,是系统资源分配的单位。

通常在一个进程中可以包含若干个 线程 (Thread),当然一个进程中至少有一个线程,不然没有存在的意义。线程是 CPU 调度和执行的单位。

注意: 许多多线程都是模拟出来的,真正意义上的多线程是指有多个 CPU,即多核,比如服务器。如果是模拟出来的多线程,即在一个 CPU 的情况下,在同一个时间点,CPU 只能执行一个代码(时间片轮转调度了解一下?),因为切换得很快,所以就有了同时执行的错觉。

1.4 核心概念

线程是独立的执行路径。

在程序运行时,即使没有自己创建线程,后台也会有多个线程,比如主线程、GC 线程等。

main() 称之为主线程,为系统的入口,用于执行整个程序。

在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统密切相关的,先后顺序是不可以人为干预的。

对同一份资源进行操作时,会存在资源抢夺的问题,因此需要加入并发控制。

线程会带来额外的开销,如 CPU 调度时间、并发控制开销等。

每个线程在自己的工作内存交互,内存控制不当会造成数据不一致。

2. 线程的创建

2.1 继承 Thread 类

编写步骤

1、自定义线程类继承 Thread 类

2、重写 run() 方法,编写线程执行体

3、创建线程对象,调用 start() 方法启动线程

但需要注意的是:线程开启不一定立即执行,是由 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
27
28
29
30
31
32
package com.yang.demo01;

/**
* @author 默烦 2020/10/5
*/
/**
* 创建线程方式一
* 1. 继承 Thread 类,
* 2. 重写 run() 方法,
* 3. 调用 start() 开启线程
*/
public class TestThread1 extends Thread{

@Override
public void run() {
// run 方法线程体
for (int i = 0; i < 20; i++) {
System.out.println("我在吃饭---" + i);
}
}

public static void main(String[] args) {
// main 线程,主线程
// 创建线程对象
TestThread1 testThread1 = new TestThread1();
// 调用 start() 方法开启线程
testThread1.start();
for (int i = 0; i < 20; i++) {
System.out.println("我在玩游戏---" + i);
}
}
}

运行上述代码后,我们可以发现每次的运行结果不同(如果发现相同,请把循环次数调大,比如 1000)。

练习 - 批量下载网络图片

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
/**
* @author 默烦 2020/10/5
*/
// 练习 Thread ,实现多线程同步下载图片
public class TestThread2 extends Thread{
public String url; // 网络图片地址
public String name; // 保存的文件名

public TestThread2(String url, String name) {
this.url = url;
this.name = name;
}

@Override
public void run() {
WebDownloader webDownloader = new WebDownloader();
webDownloader.downloader(url, name);
System.out.println("下载的文件名为: " + name);
}

public static void main(String[] args) {
TestThread2 t1 = new TestThread2("http://i0.hdslb.com/bfs/archive/a8cb2b3c23aec9d944784bf87ab55242a8700b7e.jpg", "1.jpg");
TestThread2 t2 = new TestThread2("http://i1.hdslb.com/bfs/archive/856f9b8c817bc98b53e2ce642d88b5b520b0ed3b.jpg", "2.jpg");
TestThread2 t3 = new TestThread2("http://i2.hdslb.com/bfs/archive/d9be812df14c44f1b554b055f3d5bd184bd62a3e.jpg", "3.jpg");

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


// 下载器
class WebDownloader {
// 下载方法
public void downloader(String url, String name) {
try {
FileUtils.copyURLToFile(new URL(url), new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO 异常,下载器出现问题!");
}
}
}

上述下载的图片是 B 站的封面图片,感觉以我现在的知识储备,似乎可以做一个根据网址或 bv \ av 号获取 B 站视频封面的小工具?(挖个坑)

2.2 实现 Runnable 接口

编写步骤

1、自定义线程类实现 Runnable 接口

2、 实现 run() 方法,编写线程执行体

3、创建线程对象,调用 start() 方法启动线程

因为 Java 单继承的局限性,推荐使用 Runnable 接口来创建线程。

代码实现

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
package com.yang.demo01;

/**
* @author 默烦 2020/10/6
*/
// 创建线程方式二,实现 Runnable 接口
public class TestThread3 implements Runnable{

@Override
public void run() {
// run 方法线程体
for (int i = 0; i < 20; i++) {
System.out.println("我在吃饭---" + i);
}
}

public static void main(String[] args) {
// 创建 Runnable 接口的实现类对象
TestThread3 testThread3 = new TestThread3();
// 创建线程对象,通过线程对象来开启线程,代理
// Thread thread = new Thread(testThread3);
// thread.start();
new Thread(testThread3).start();


for (int i = 0; i < 20; i++) {
System.out.println("我在玩游戏---" + i);
}
}
}

小结

继承 Thread 类:

  • 子类继承 Thread 类具备多线程能力
  • 启动线程:子类对象.start()
  • 不建议使用:避免 OOP 单继承局限性

实现 Runnable 接口:

  • 实现接口 Runnable 接口具有多线程能力
  • 启动线程:传入目标对象 + Thread对象.start()
  • 推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用

2.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
package com.yang.demo01;

/**
* @author 默烦 2020/10/6
*/
// 多个线程同时操作同一个对象
// 模拟买票
public class TestThread4 implements Runnable {

private int ticketNums = 10; // 票数


@Override
public void run() {
while (true) {
if (ticketNums <= 0) break;

// 模拟延时
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"--->拿到了第"+(ticketNums--)+"张票");
}
}

public static void main(String[] args) {
TestThread4 ticket = new TestThread4();

// 输出时出现问题:
// 多个线程操作同一个资源的情况下,线程不安全,数据紊乱
new Thread(ticket, "默烦").start();
new Thread(ticket, "Mofan").start();
new Thread(ticket, "黄牛党").start();
}
}

运行上述代码后,在控制台可能看到如下情况:

  • 某一张票同时被几个人购买
  • 某人购买到第 0 张或第 -1 张票

这种情况就是在多个线程操作同一个对象(资源)的情况下,导致线程不安全,进而数据紊乱。

2.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
51
52
package com.yang.demo01;

/**
* @author 默烦 2020/10/6
*/
// 模拟龟兔赛跑
public class Race implements Runnable {

private static String winner; // 胜利者

@Override
public void run() {
for (int i = 0; i <= 100; i++) {
// 模拟兔子睡觉
if (Thread.currentThread().getName().equals("兔子") && (i % 10 == 0)) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

// 判断比赛是否结束
boolean flag = gameOver(i);
// 如果比赛结束
if (flag){
break;
}
System.out.println(Thread.currentThread().getName() + "--->跑了" + i + "步");
}
}

// 判断是否完成比赛
private boolean gameOver(int steps) {
// 判断是否有胜利者
if (winner != null) {
return true;
}
if (steps >= 100) {
winner = Thread.currentThread().getName();
System.out.println("winner is " + winner);
return true;
}
return false;
}

public static void main(String[] args) {
Race race = new Race();
new Thread(race, "兔子").start();
new Thread(race, "乌龟").start();
}
}

2.5 实现 Callable 接口

PS:当前只做了解即可。

编写步骤

1、实现 Callable 接口,需要返回值类型

2、重写 call() 方法,需要抛出异常

3、创建目标对象

4、创建执行服务

1
ExecutorService service = Executors.newFixedThreadPool(1);

5、提交执行

1
Future<Boolean> result1 = service.submit(t1);

6、获取结果

1
boolean r1 = result1.get();

7、关闭服务

1
service.shutdownNow();

演示:利用 Callable 改造图片下载案例

代码实现

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
package com.yang.demo02;

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.*;

/**
* @author 默烦 2020/10/6
*/
// 线程创建方式三:实现 Callable 接口

/**
* Callable 的好处
* 1. 可以定义返回值
* 2. 可以抛出异常
*/
public class TestCallable implements Callable<Boolean> {
public String url; // 网络图片地址
public String name; // 保存的文件名

public TestCallable(String url, String name) {
this.url = url;
this.name = name;
}


@Override
public Boolean call() throws Exception {
WebDownloader webDownloader = new WebDownloader();
webDownloader.downloader(url, name);
System.out.println("下载的文件名为: " + name);
return true;
}

public static void main(String[] args) throws ExecutionException, InterruptedException {
TestCallable t1 = new TestCallable("http://i0.hdslb.com/bfs/archive/a8cb2b3c23aec9d944784bf87ab55242a8700b7e.jpg", "1.jpg");
TestCallable t2 = new TestCallable("http://i1.hdslb.com/bfs/archive/856f9b8c817bc98b53e2ce642d88b5b520b0ed3b.jpg", "2.jpg");
TestCallable t3 = new TestCallable("http://i2.hdslb.com/bfs/archive/d9be812df14c44f1b554b055f3d5bd184bd62a3e.jpg", "3.jpg");

// 创建执行服务
ExecutorService service = Executors.newFixedThreadPool(3);
// 提交执行
Future<Boolean> result1 = service.submit(t1);
Future<Boolean> result2 = service.submit(t2);
Future<Boolean> result3 = service.submit(t3);
// 获取结果
boolean r1 = result1.get();
boolean r2 = result2.get();
boolean r3 = result3.get();

System.out.println(r1);
System.out.println(r2);
System.out.println(r3);

// 关闭服务
service.shutdownNow();
}
}

// 下载器
class WebDownloader {
// 下载方法
public void downloader(String url, String name) {
try {
FileUtils.copyURLToFile(new URL(url), new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO 异常,下载器出现问题!");
}
}
}

3. Lambda 表达式

3.1 基本含义

λ 是希腊字母表中排序第十一位的字母,英文名称为 Lambda

Lambda 表达式可以避免匿名内部类定义过多,其实质属于函数式编程的概念:

1
2
3
(params) -> expression [表达式]
(params) -> statement [语句]
(params) -> {statement}

比如:

1
new Thread(() -> System.out.println("多线程学习......")).start();

为什么要使用 Lambda 表达式

  • 避免匿名内部类定义过多
  • 可以让代码看起来更简洁
  • 去掉了一堆没有意义的代码,只留下核心的逻辑

也许你会说,看了 Lambda 表达式后,不但不觉得简洁,反而觉得更乱了,而且还看不懂了。

那是因为我们没有使用习惯,用得多了,看习惯了,自然就好了。

3.2 函数式接口

理解 Functional Interface(函数式接口)是学习 Java 8 Lambda 表达式的关键所在。

函数式接口的定义:

任何接口,如果只包含唯一一个抽象方法,那么它就是函数式接口,比如:

1
2
3
public interface Runnable {
public abstract void run();
}

对于函数式接口,我们可以通过 Lambda 表达式来创建该接口的对象。

3.3 Lambda 的演化

Lambda 表达式针对的是函数式接口,函数式接口也是一种接口,只不过比较特殊。

因此,函数式接口也可以有实现类。如果想要使用一个函数式接口,就需要新开一个类,这个类作为函数式接口的 实现类 ,再对这个实现类实例化,然后使用实例化得到的对象调用方法。

但是这样需要新开一个类,为了不新开一个类可以使用当前类中编写一个 静态嵌套类 ,这个静态嵌套类也需要实现接口,后续的使用就和最开始的一样。

在当前类编写静态嵌套类会“污染”当前类,这个时候可以在方法内部编写 局部内部类

还可以对局部内部类进行简化,将其书写成 匿名内部类

对于函数式接口来说,这个接口只有唯一的一个抽象方法,因此后续使用这个接口时,接口的名称、抽象方法名都不是那么重要,可以将它们省略。

在 JDK 8 中引入了新特性 —— Lambda 表达式 ,使用 Lambda 表达式后可以让函数式接口的使用变得简洁,也避免了匿名内部类定义过多。

演化代码

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
package com.yang.lambda;

/**
* @author 默烦 2020/10/6
*/
// lambda 表达式推导
public class TestLambda1 {

// 3. 静态嵌套类
static class Like2 implements ILike {

@Override
public void lambda() {
System.out.println("I LIKE Lambda2!");
}
}

public static void main(String[] args) {
ILike like = new Like();
like.lambda();

like = new Like2();
like.lambda();

// 4. 局部内部类
class Like3 implements ILike {
@Override
public void lambda() {
System.out.println("I LIKE Lambda3!");
}
}

like = new Like3();
like.lambda();

// 5. 匿名内部类
// 没有类的名称,必须借助接口或父类
like = new ILike() {
@Override
public void lambda() {
System.out.println("I LIKE Lambda4!");
}
};
like.lambda();


// 6. 用 Lambda 进行简化
like = () -> {
System.out.println("I LIKE Lambda5!");
};
like.lambda();

}
}

// 1. 定义一个函数式接口
interface ILike {
void lambda();
}

// 2. 实现类
class Like implements ILike {
@Override
public void lambda() {
System.out.println("I LIKE Lambda!");
}
}

Lambda 表达式的简化

Lambda 表达式在一定条件下还可以进一步简化,让代码变得精简。

比如:

  • 参数类型可以进行简化,但如果有多个参数,必须加上包裹参数的括号
  • 当参数只有一个时,包裹参数的括号也可以进行简化
  • 方法执行语句只有一句时,还可以将大括号进行简化
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
package com.yang.lambda;

/**
* @author 默烦 2020/10/6
*/
public class TestLambda2 {

public static void main(String[] args) {

ILove love1 = (int a) ->{
System.out.println("I Love You --> " + a);
};

// 简化1:参数类型简化
ILove love2 = (a) -> {
System.out.println("I Love You --> " + a);
};

// 简化2:简化括号
ILove love3= a -> {
System.out.println("I Love You --> " + a);
};

// 简化3:简化大括号
ILove love4 = a -> System.out.println("I Love You --> " + a);
/**
* Lambda 表达式内执行语句只有一行时,可以去掉大括号
* 前提是接口为函数式接口
* 如果接口的抽象方法有多个参数,可以去掉参数的类型,但必须加上括号
*/
love4.love(2);
}
}

interface ILove {
void love(int a);
}

如果函数式接口中的抽象方法有返回值,只需要在 {} 包裹的函数体内 return 即可。

4. 静态代理

为什么多线程讲的好好的,突然讲到 Lambda 表达式,然后又讲到静态代理呢?

那肯定是因为多线程里面涉及了这两个知识点嘛~ 😜

创建线程的第二种方式,使用实现 Runnable 接口的方式就和动态代理一样。

我们可以看看下述以婚庆公司为例子的静态代理:

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

/**
* @author 默烦 2020/10/6
*/
public class StaticProxy {

// 创建线程的方式与案例的对比
public static void main(String[] args) {

// Lambda 表达式写法
// new Thread(()->System.out.println("线程1")).start();
// new WeddingCompany(()-> System.out.println("哥要结婚了")).HappyMarry();

// 常见写法
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程2");
}
});
thread.start();

WeddingCompany weddingCompany = new WeddingCompany(new You());
weddingCompany.HappyMarry();
}
}

interface Marry {
void HappyMarry();
}

// 真实角色 You 去结婚
class You implements Marry {

@Override
public void HappyMarry() {
System.out.println("你要去结婚啦!");
}
}

// 代理角色 帮助你结婚
class WeddingCompany implements Marry {

// 代理角色代理谁? 被代理的目标
private Marry target;

public WeddingCompany(Marry target) {
this.target = target;
}

@Override
public void HappyMarry() {
before();
this.target.HappyMarry(); // 真实对象调用
after();
}

private void after() {
System.out.println("结婚之后,收尾款");
}

private void before() {
System.out.println("结婚之前,布置现场");
}
}

根据 main() 方法中的对比可以很容易地看出:使用实现 Runnable 接口创建线程的方式就是一种静态代理!

总结

1、真实角色和代理对象需要实现同一个接口

2、代理对象要代理真实对象

这样做的好处:可以让代理对象做很多真实对象做不了的事,真实对象可以专注于自己的事

5. 线程状态

5.1 状态概述

相关概述

线程状态图解

Thread t = new Thread(); ,这行代码可以创建一个线程,线程对象一旦创建就进入到新生状态。

当调用 start() 方法时,线程立即进入就绪状态,但这并不意味着线程立即被调度执行。

当线程被调度执行后,线程立即进入运行状态,线程才真正执行线程体里的代码块。

当调用 sleep()wait() 或同步锁定时,线程进入阻塞状态,进入阻塞状态后,代码就不会往下执行。阻塞事件解除后,重新进入就绪状态,等待 CPU 调度执行。

线程中断或结束,一旦进入死亡状态,就不能再次启动。

线程方法

方法 说明
setPriority (int newPriority) 更改线程的优先级
static void sleep (long millis) 在指定的毫秒数内让当前正在执行的线程休眠
void join () 等待该线程终止
static void yield () 暂停当前正在执行的线程对象,并执行其他线程
void interrupt () 中断线程,不建议使用
boolean isAlive () 测试线程是否处于活动状态

5.2 线程停止

JDK 官方已不推荐使用 stop()destory() 来停止一个线程。

建议使用一个标志位进行终止变量,当 flag == 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
36
37
38
39
40
41
package com.state;

/**
* @author 默烦 2020/10/6
*/

/**
* 测试 stop
*/
public class TestStop implements Runnable {

// 1. 设置一个标志位
private boolean flag = true;

@Override
public void run() {
int i = 0;
while (flag) {
System.out.println("run......Thread " + i++);
}
}

// 2. 设置一个公开的方法停止线程,变换标志位
public void stop() {
this.flag = false;
}

public static void main(String[] args) {
TestStop testStop = new TestStop();
new Thread(testStop).start();

for (int i = 0; i < 1000; i++) {
System.out.println("main " + i);
if (i == 900) {
// 调用 stop() 切换标志位,让线程停止
testStop.stop();
System.out.println("线程停止了!");
}
}
}
}

运行后可能会看不到结果,可以多运行几次试试,毕竟我们不能更改 CPU 的调度。

5.3 线程休眠

sleep(休眠时间)指定当前线程阻塞的毫秒数;

sleep 存在异常 InterruptedException

sleep 时间达到后,线程就进入就绪状态;

sleep 可以模拟网络延时,倒计时等;

每一个对象都有一个锁,而 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
31
32
33
34
35
36
37
package com.state;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
* @author 默烦 2020/10/8
*/
// 模拟时间
public class TestSleep2 {

public static void main(String[] args) {
Date startTime = new Date(System.currentTimeMillis());
while (true) {
try {
Thread.sleep(1000);
System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
startTime = new Date(System.currentTimeMillis()); // 更新当前时间
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

// 模拟倒计时方法
public static void tenDown() throws InterruptedException {
int num = 10;

while (true) {
Thread.sleep(1000);
System.out.println(num--);
if (num <= 0) {
break;
}
}
}
}

5.4 线程礼让

礼让(yield)线程,让当前正在执行的线程暂停,但不阻塞。

让线程从运行状态转为就绪状态。

让 CPU 重新调度,但是礼让不一定成功,“需要看 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
package com.state;

/**
* @author 默烦 2020/10/8
*/
// 测试礼让线程
// 礼让不一定成功
public class TestYield {
public static void main(String[] args) {
MyYield myYield = new MyYield();
new Thread(myYield, "a").start();
new Thread(myYield, "b").start();
}
}

class MyYield implements Runnable {

@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 线程开始执行......");
Thread.yield(); // 礼让
System.out.println(Thread.currentThread().getName() + " 线程停止执行......");
}
}

5.5 线程强制执行

Join 合并线程,等当前线程执行完后,再执行其他线程,其他线程阻塞,可以想象成插队。

与休眠方法 sleep() 一样,join() 方法存在异常 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
package com.state;

/**
* @author 默烦 2020/10/8
*/
public class TestJoin implements Runnable{

@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("线程 VIP 来了!" + i);
}
}

public static void main(String[] args) throws InterruptedException{
// 启动我们自己的线程
TestJoin testJoin = new TestJoin();
Thread thread = new Thread(testJoin);

thread.start();

// 主线程
for (int i = 0; i < 500; i++) {
if (i == 200) {
thread.join(); // 爷来了,靠边!
}
System.out.println("main" + i);
}
}
}

5.6 线程状态观测

Thread.State

线程状态。线程可以处于以下状态之一:

NEW:尚未启动的线程处于此状态

RUNNABLE:在 Java 虚拟机中执行的线程处于此状态

BLOCKED:被阻塞等待监视器锁定的线程处于此状态

WAITING:正在等待另一个线程执行特定动作的线程处于此状态

TIMED_WAITING:正在等待另一个线程执行动作达到指定等待时间的线程处于此状态

TERMINATED:已退出的线程处于此状态

一个线程可以在给定时间点处于一个状态,这些状态是不反应任何操作系统线程状态的虚拟机状态。

测试代码

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.state;

/**
* @author 默烦 2020/10/8
*/
// 观察测试线程状态
public class TestState {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println("///////");
});

// 观察状态
Thread.State state = thread.getState();
System.out.println(state); // NEW

// 观察启动后
thread.start();
state = thread.getState();
System.out.println(state);

// 只要线程不终止,就一直输出状态
while (state != Thread.State.TERMINATED) {
Thread.sleep(100);
state = thread.getState();
System.out.println(state); // 输出线程状态
}
}
}

5.7 线程优先级

Java 提供一个线程调度器来监控程序启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程执行。

线程的优先级用数字表示,范围从 0 ~ 10:

  • Thread.MIN_PRIORITY = 1
  • Thread.NORM_PRIORITY = 5
  • Thread.MAX_PRIORITY = 10

使用以下方式可以改变或获取优先级:getPriority()setPriority(int xxx)

但并不是优先级高的线程就一定会被调用。

优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用了,这所有都会看 CPU 的调度,甚至可能会出现性能倒置(优先级低的线程先被调用,优先级高的线程反而在等待)。

在设置优先级时,优先级的设定建议在 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package com.state;

/**
* @author 默烦 2020/10/8
*/
public class TestPriority{
public static void main(String[] args) {
// 主线程默认优先级
System.out.println(Thread.currentThread().getName()
+ "-->"
+ Thread.currentThread().getPriority());

MyPriority myPriority = new MyPriority();
Thread t1 = new Thread(myPriority);
Thread t2 = new Thread(myPriority);
Thread t3 = new Thread(myPriority);
Thread t4 = new Thread(myPriority);
Thread t5 = new Thread(myPriority);
Thread t6 = new Thread(myPriority);

// 设置优先级再启动
t1.start();

t2.setPriority(1);
t2.start();

t3.setPriority(4);
t3.start();

t4.setPriority(Thread.MAX_PRIORITY); // 10
t4.start();

t5.setPriority(8);
t5.start();

t6.setPriority(7);
t6.start();
}
}

class MyPriority implements Runnable {

@Override
public void run() {
System.out.println(Thread.currentThread().getName()
+ "-->"
+ Thread.currentThread().getPriority());
}
}

5.8 守护线程

守护(daemon)线程。线程分为用户线程守护线程

虚拟机必须确保用户线程执行完毕,虚拟机不用等待守护线程执行完毕,比如:后台记录操作日志、监控内存、垃圾回收等待…

测试代码

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

/**
* @author 默烦 2020/10/8
*/
public class TestDaemon {

public static void main(String[] args) {
God god = new God();
You you = new You();

Thread thread = new Thread(god);
thread.setDaemon(true); // 默认 false 表示用户线程
thread.start();

new Thread(you).start(); // 用户线程启动
}

}


// God
class God implements Runnable{
@Override
public void run() {
while (true) {
System.out.println("上帝保佑着你!\uD83D\uDE04");
}
}
}


// You

class You implements Runnable {
@Override
public void run() {
for (int i = 0; i < 36500; i++) {
System.out.println("一生都开心得活着");
}
System.out.println("====== GoodBye World ======");
}
}

6. 线程同步

6.1 线程同步机制

线程同步

需要在多个线程操作同一个资源时进行线程同步。

并发:同一个对象多个线程 同时操作。比如:抢火车票、两个银行同时取钱…

现实生活中,我们经常会遇到“同一个资源,多个人想去使用”的问题。比如,食堂排队时,每个人都想要打饭,最简单的解决办法就是排队,一个一个来!

处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。这时候我们就需要线程同步,线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用。

队列和锁

要解决线程同步的安全性,就需要用到队列和锁。

每个对象都有一个锁,这些对象操作同一个资源时,会将锁交给这个资源,当前对象操作完那个资源后,资源就将锁还给对象,操作完资源的对象“撤退”,下一个对象再继续(类似于上厕所)。

线程同步问题

由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制 synchronized ,当一个线程获得对象的排它锁,就会独占资源,而其他线程必须等待,使用后释放锁即可。但会存在以下问题:

  • 一个线程持有锁会导致其他所有需要此锁的线程挂起
  • 在多线程竞争下,加锁和释放锁会导致比较多的上下文切换和调度延时,这会引起性能问题
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题

6.2 线程不安全案例

2.3 初始并发 中,编写了一个买票的经典案例,运行那个案例后,在控制台可能会看到某人购买到第 0 张或第 -1 张票的情况,这种情况就是线程不安全的一种案例。

我们将买票的逻辑抽出来,重新编写一个 buy() 方法,然后在重写的 run() 方法中调用 buy() 方法:

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
package com.yang.syn;

/**
* @author 默烦 2020/10/8
*/
// 不安全的买票
// 运行结果不对,存在负数
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket station = new BuyTicket();

new Thread(station, "mofan").start();
new Thread(station, "默烦").start();
new Thread(station, "You").start();
}
}

class BuyTicket implements Runnable{
private int ticketNums = 10; // 票数
boolean flag = true; // 外部停止标志

@Override
public void run() {
while (flag) {
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

private void buy() throws InterruptedException {
if (ticketNums <= 0) {
flag = false;
return;
}
// 模拟延时
Thread.sleep(100);
// 买票
System.out.println(Thread.currentThread().getName() + "拿到" + ticketNums--);
}
}

除此之外,我们还可以编写一个银行取钱的案例,在这个案例中,线程也是不安全的:

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.yang.syn;

/**
* @author 默烦 2020/10/8
*/
// 不安全的取钱
public class UnsafeBank {
public static void main(String[] args) {
// 账户
Account account = new Account(100, "结婚基金");

Drawing you = new Drawing(account, 50, "你");
Drawing girlFriend = new Drawing(account, 100, "girlFriend");

you.start();
girlFriend.start();

}
}

class Account {
int money; // 余额
String name; // 卡名

public Account(int money, String name) {
this.money = money;
this.name = name;
}
}

// 银行:模拟取款
class Drawing extends Thread {
Account account;
int drawingMoney; // 取了多少钱
int nowMoney; // 身上的钱

public Drawing(Account account, int drawingMoney, String name) {
super(name);
this.account = account;
this.drawingMoney = drawingMoney;
}

@Override
public void run() {
// 判断是否有钱
if (account.money - drawingMoney < 0) {
System.out.println(Thread.currentThread().getName() + " 钱不够,取不了");
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 卡内余额
account.money = account.money - drawingMoney;
// 手中的钱
nowMoney = nowMoney + drawingMoney;
System.out.println(account.name + " 余额为:" + account.money);
// this.getName() 等价于 Thread.currentThread().getName()
System.out.println(this.getName() + " 手里的钱:" + nowMoney);
}
}

运行上述代码后,控制台输出的“余额为”可能会是一个负数,这就是线程不安全的表现。

在 JDK 源码动态数组 ArrayList 类中的顶部注释有这样一句话:

1
Note that this implementation is not synchronized.

简单来说,JDK 提供的 ArrayList 没有实现同步,ArrayList 是线程不安全的。我们可以通过一些代码来进行验证:

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

import java.util.ArrayList;

/**
* @author 默烦 2020/10/8
*/
// 线程不安全的集合
public class UnsafeList {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(() ->{
list.add(Thread.currentThread().getName());
}).start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}

运行后,可以在控制台看到数组的长度没有达到 10000,这是由于在某一时刻多个线程向同一个位置插入了多个数据,最终导致数组长度没有达到 10000。因此,JDK 提供的 ArrayList 是线程不安全的。

6.3 同步方法与同步块

我们可以通过 private 关键字来保证数据对象只能被方法访问 ,所以我们需要针对方法提出一套机制 , 这套机制就是 synchronized 关键字 ,它包括两种用法:synchronized 方法和 synchronized 块。

同步方法

同步方法 :

1
public synchronized void method(int args) {} 

synchronized 方法控制对“对象” 的访问,每个对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞。

方法一旦执行,就独占该锁,直到该方法返回才会释放锁,然后后面被阻塞的线程才能获得这个锁,并继续执行。但这有一个很大的缺陷:将一个大的方法申明为 synchronized 会影响效率。

同步方法弊端

一个方法中可能存在只读的代码,也可能存在修改数据的代码,方法里面需要修改数据的代码才需要锁,只读的代码是不用锁的。如果使用同步方法,就是将整个方法锁了,锁得太多,就会浪费资源。

比如,在 6.2 线程不安全案例 中给 buy() 方法添加一个 synchronized 后,让这个方法变成一个同步方法。再次运行时,就不会出现线程不安全的情况。

但是在 6.2 线程不安全案例 中银行取钱的案例中,给重写的 run() 方法加上 synchronized 后,并不能使线程变成安全的。为什么会这样呢?

介绍一下 synchronized 关键字的应用方式。 👇

synchronized 关键字

参考链接:【Java并发编程之深入理解】Synchronized的使用

Java 中每一个对象都可以作为锁,这是 synchronized 实现同步的基础:

  1. 普通同步方法(实例方法),锁是当前实例对象 ,进入同步代码前要获得当前实例的锁;
  2. 静态同步方法,锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁;
  3. 同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

synchronized 关键字锁住的是:同步操作的对象,也就是共享资源。

在银行取钱的案例中出现的是第一种情况,创建了两个不同的对象,因此最终的运行结果仍然没有改变,依旧是线程不安全的。要想规避这种情况,就要使用同步块来解决。

同步块

1
synchronized (Obj) { }

Obj 称之为 同步监视器Obj 可以是任何对象,但是推荐使用共享资源作为同步监视器。同步方法中无需指定同步监视器 , 因为同步方法的同步监视器就是 this , 就是这个对象本身 , 或者是 class。(反射中进行了讲解)

同步监视器的执行过程:

  1. 第一个线程访问,锁定同步监视器,执行其中代码;
  2. 第二个线程访问,发现同步监视器被锁定,无法访问;
  3. 第一个线程访问完毕,解锁同步监视器;
  4. 第二个线程访问,发现同步监视器没有锁,然后锁定并访问

因此,针对银行取钱的案例中,使用同步块后如下(只针对重写的 run() 方法进行修改):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public void run() {
// 锁的对象是变化的量,是需要增删改的对象
synchronized (account) {
// 判断是否有钱
if (account.money - drawingMoney < 0) {
System.out.println(Thread.currentThread().getName() + " 钱不够,取不了");
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 卡内余额
account.money = account.money - drawingMoney;
// 手中的钱
nowMoney = nowMoney + drawingMoney;
System.out.println(account.name + " 余额为:" + account.money);
// this.getName() 等价于 Thread.currentThread().getName()
System.out.println(this.getName() + " 手里的钱:" + nowMoney);

}
}

在银行取钱的案例中,账户中的钱会被多个人取出,因此需要将共享资源(账户中的钱)锁住,就可以保证线程安全了。

针对 JDK 默认提供的非线程安全动态数组 ArrayList,使用同步块就可以解决非线程安全:

1
2
3
4
5
6
7
8
for (int i = 0; i < 10000; i++) {
new Thread(() ->{
// 使用同步块解决
synchronized (list) {
list.add(Thread.currentThread().getName());
}
}).start();
}

6.4 CopyOnWriteArrayList

在 JDK 5.0 中引入了一个新的包,名为 java.util.concurrent (简称 JUC),在这个包中提供了在并发编程中很常用到的工具类。

前文说到,JDK 提供了 ArrayList 是非线程安全的,在并发环境下使用会出现数据异常,为此,JUC 中提供了一个相似的类,名为 CopyOnWriteArrayList ,它也是一个动态数组,但它是线程安全的。

我们可以测试一下:

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.yang.syn;

import java.util.concurrent.CopyOnWriteArrayList;

/**
* @author 默烦 2020/10/8
*/
public class TestJUC {
public static void main(String[] args) {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}

控制台最后输出的结果一直是 10000,并没有出现数据异常的情况。

关于 JUC 的其他内容先挖个坑,以后慢慢填~ 😏

6.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
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
package com.yang.syn;

import javax.lang.model.element.VariableElement;

/**
* @author 默烦 2020/10/8
*/
// 死锁:多个线程互相抱着对方需要的资源,然后僵持
public class DeadLock {
public static void main(String[] args) {
MakeUp g1 = new MakeUp(0, "灰姑娘");
MakeUp g2 = new MakeUp(1, "白雪公主");

g1.start();
g2.start();
}
}

// 口红
class Lipstick {

}


// 镜子
class Mirror {

}

// 化妆
class MakeUp extends Thread {

// 用 static 保证资源只有一份
static Lipstick lipstick = new Lipstick();
static Mirror mirror = new Mirror();

int choice; // 选择
String girlName; // 使用化妆品的人

public MakeUp(int choice, String girlName) {
this.choice = choice;
this.girlName = girlName;
}

@Override
public void run() {
// 化妆
try {
makeUp();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

// 化妆,互相持有对方的锁,就是需要拿到对方的资源
private void makeUp() throws InterruptedException {
if (choice == 0) {
synchronized (lipstick) { // 获得口红的锁
System.out.println(this.girlName + " 获得口红的锁");
Thread.sleep(1000);
synchronized (mirror) { // 一秒中后获得镜子的锁
System.out.println(this.girlName + " 获得镜子的锁");
}
}
} else {
synchronized (mirror) { // 获得镜子的锁
System.out.println(this.girlName + " 获得镜子的锁");
Thread.sleep(2000);
synchronized (lipstick) { // 两秒中后获得口红的锁
System.out.println(this.girlName + " 获得口红的锁");
}
}
}
}
}

运行上述代码后,我们可以发现控制台卡死了,一直处于:

1
2
白雪公主 获得镜子的锁
灰姑娘 获得口红的锁

除非手动结束,否则会一直处于这种状态,当然输出先后可能有所交换。

这是因为白雪公主想要口红的锁,但是灰姑娘又抱着口红的锁;灰姑娘想要镜子的锁,但是白雪公主又抱着镜子的锁,两者互相僵持,谁都不放手,最终死锁。

想要解决也很简单,只需要:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 化妆,互相持有对方的锁,就是需要拿到对方的资源
private void makeUp() throws InterruptedException {
if (choice == 0) {
synchronized (lipstick) { // 获得口红的锁
System.out.println(this.girlName + " 获得口红的锁");
Thread.sleep(1000);
}
synchronized (mirror) { // 一秒中后获得镜子的锁
System.out.println(this.girlName + " 获得镜子的锁");
}
} else {
synchronized (mirror) { // 获得镜子的锁
System.out.println(this.girlName + " 获得镜子的锁");
Thread.sleep(2000);
}
synchronized (lipstick) { // 两秒中后获得口红的锁
System.out.println(this.girlName + " 获得口红的锁");
}
}
}

死锁避免方式

产生死锁的四个必要条件:

  1. 互斥条件:一个资源每次只能被一个进程使用;
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放;
  3. 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺;
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

上面列出了死锁的四个必要条件,我们只要想办法破其中的任意一个或多个条件就可以避免死锁发生。

6.6 Lock

从 JDK 5.0 开始,Java 提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用 Lock 对象充当。
java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应先获得 Lock 对象。

ReentrantLock (可重入锁)类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是 ReentrantLock,可以显式加锁、释放锁。

测试代码

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.yang.advanced;


import java.util.concurrent.locks.ReentrantLock;

/**
* @author 默烦 2020/10/9
*/
public class TestLock {
public static void main(String[] args) {
TestLock2 testLock2 = new TestLock2();
new Thread(testLock2).start();
new Thread(testLock2).start();
new Thread(testLock2).start();
}

}

class TestLock2 implements Runnable {
int ticketNums = 10;

// 定义 lock 锁
private final ReentrantLock lock = new ReentrantLock();

@Override
public void run() {
while (true) {
try {
lock.lock(); // 开锁
if (ticketNums > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticketNums--);
} else {
break;
}
} finally {
// 解锁
lock.unlock();
}

}
}
}

运行上述代码后,不会出现数据异常情况,证明我们自定义显式加锁是没有问题的! 😎

synchronized 与 Lock 的对比

Lock 是显式锁(手动开启和关闭锁,别忘记关闭锁),而 synchronized 是隐式锁,出了作用域自动释放。

Lock 只有代码块锁,synchronized 有代码块锁和方法锁。

使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好,并且具有更好的扩展性(提供更多的子类)。

优先使用顺序:Lock > 同步代码块(已经进入了方法体,分配了相应资源)> 同步方法(在方法体之外)

7. 线程协作

7.1 生产者消费者问题

问题描述

应用场景:生产者和消费者问题。

1、假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费。

2、如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止。

3、如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止。

生产者与消费者

问题分析

这是一个线程同步问题,生产者和消费者共享同一个资源,但这又不仅仅是一个线程同步问题,因为生产者和消费者之间相互依赖,互为条件。

对于生产者,在没有生产产品之前,要通知消费者等待,而生产了产品之后,又需要马上通知消费者消费。对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费。

在生产者消费者问题中,仅使用 synchronized 是不够的。synchronized 可阻止并发更新同一个共享资源,这实现了同步,但 synchronized 不能用来实现不同线程之间的消息传递 (通信)。

线程通信方法

Java 内提供了几个方法解决线程之间的通信问题:

方法名 作用
wait() 线程一直等待,直到其他线程通知 ,与 sleep() 不同,它会释放锁
wait(long timeout) 指定等待的毫秒数
notify() 唤醒一个处于等待状态的线程
notifyAll() 唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度

注意: 这些方法均是 Object 类的方法,都 只能在同步方法或者同步代码块中使用,否则会抛出异常IllegalMonitorStateException

至于为什么是 Objext 类的方法,其实很简单。因为每个对象都有一个锁,因此定义在所有类的“祖宗类” Object 类中。

这三个方法的调用者必须是同步代码块或同步方法中的同步监视器。比如,同步代码块中的就是 synchronized(this) {} 中的 this,这个 this 就是同步监视器,它可以换成任何一个唯一的对象。也就是说,如果使用 this,这三个方法的调用者必须都是 this(在同步代码块中 this 可以省略)。

wait 和 notify 的理解

wait():一旦执行此方法,当前线程就会进入阻塞状态,并释放同步监视器(锁)。

notify():一旦执行此方法,就会唤醒被 wait 的一个线程。如果有多个线程被 wait,会唤醒优先级高的线程。

notifyAll():一旦执行此方法,就会唤醒所有被 wait 的线程。

假设需要使用两个线程打印 1 ~ 100,线程 A 和线程 B 交替打印。那么则有以下思路:

在同步块中编写打印方法,一进入同步块就先使用 notify() 方法,然后进行打印,打印完后使用 wait() 方法。

理解一下:由于打印的方法在同步块中,一次只能有一个线程进入,假设线程 A 先进入,然后执行 notify() 方法,只不过现在没有被 wait 的方法,就继续执行到打印,执行完打印后,执行 wait() 方法,使当前线程进入阻塞状态,并释放锁。这个时候线程 B 拿到锁,进入同步块中执行,先执行 notify() 方法,唤醒一个线程,由于只有 A 和 B 线程,所以会唤醒 A 线程,但由于锁还在线程 B 手里,因此线程 B 继续执行,线程 A 先不执行。线程 B 执行打印,执行完后,执行 wait() 方法,使线程 B 进入阻塞状态并释放锁以便线程 A 可以继续执行。线程 A 刚被线程 B 唤醒,线程 A 执行 notify() 方法唤醒线程 B,线程 A 打印,执行 wait() 方法,进入阻塞状态。按照这个方式轮流打印,直到打印完。

解决方法

1、管程法

2、信号灯法

3、使用线程池

7.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
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
package com.yang.advanced;

/**
* @author 默烦 2020/10/9
*/
// 管程法
public class TestPC {
public static void main(String[] args) {
SynContainer container = new SynContainer();
new Producer(container).start();
new Consumer(container).start();
}
}

// 生产者
class Producer extends Thread {
SynContainer container;

public Producer(SynContainer container) {
this.container = container;
}

// 生产方法
@Override
public void run() {
for (int i = 0; i < 100; i++) {
container.push(new Chicken(i));
System.out.println("生产了" + i + "只鸡");
}
}
}

// 消费者
class Consumer extends Thread {
SynContainer container;

public Consumer(SynContainer container) {
this.container = container;
}

// 消费

@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("消费了-->" + container.pop().id + "只鸡");
}
}
}

// 产品
class Chicken {
int id; // 产品编号

public Chicken(int id) {
this.id = id;
}
}

// 缓冲区
class SynContainer {
// 需要一个容器的大小
Chicken[] chickens = new Chicken[10];
// 容器计数器
int count = 0;

// 生产者放入产品
public synchronized void push(Chicken chicken) {
// 如果容器满了,就需要等待消费者消费
if (count == chickens.length) {
// 通知消费者消费,生产等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果没有满,就丢入产品
chickens[count] = chicken;
count++;
// 通知消费者消费
this.notifyAll();
}

// 消费者消费产品
public synchronized Chicken pop() {
// 判断能否消费
if (count == 0) {
// 等待生产者生产,消费者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果可以消费
count--;
Chicken chicken = chickens[count];

// 吃完了,通知生产者生产
this.notifyAll();
return chicken;
}

}

7.3 信号灯法

所谓信号灯法,就是设置一个标志位。不多 BB ,直接整代码:

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
package com.yang.advanced;

/**
* @author 默烦 2020/10/9
*/
// 信号灯法
public class TestPC2 {
public static void main(String[] args) {
TV tv = new TV();
new Player(tv).start();
new Audience(tv).start();
}
}

// 生产者 --> 演员
class Player extends Thread {
TV tv;

public Player(TV tv) {
this.tv = tv;
}

@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i % 2 == 0) {
this.tv.play("Hop");
} else {
this.tv.play("抖音,记录美好生活~");
}
}
}
}

// 消费者 --> 观众
class Audience extends Thread {
TV tv;

public Audience(TV tv) {
this.tv = tv;
}

@Override
public void run() {
for (int i = 0; i < 20; i++) {
tv.watch();
}
}
}

// 产品 --> 节目
class TV {
// 演员表演时,观众等待 T
// 观众观看时,演员等待 F
String voice; // 表演的节目
boolean flag = true;

// 表演
public synchronized void play(String voice) {
if (!flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("演员表演了:" + voice);
// 通知观众观看
this.notifyAll(); // 通知唤醒
this.voice = voice;
this.flag = !this.flag;
}

// 观看
public synchronized void watch() {
System.out.println("观众观看了:" + voice);
if (flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("观众观看了:" + voice);
// 通知演员表演
this.notifyAll();
this.flag = !this.flag;
}
}

7.4 线程池

基本概念

背景:当经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。

思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。这样可以避免频繁创建销毁,从而实现重复利用。这类似生活中的公共交通工具。

好处:

  • 提高响应速度(减少了创建新线程的时间)
  • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  • 便于线程管理

corePoolSize:核心池的大小

maximumPoolSize:最大线程数

keepAliveTime:线程没有任务时最多保持多长时间后会终止

使用方法

JDK 5.0 起提供了线程池相关API:ExecutorServiceExecutors

ExecutorService :真正的线程池接口,常见子类 ThreadPoolExecutor

void execute(Runnable command) :执行任务 / 命令,没有返回值,一般用来执行 Runnable

<T> Future<T> submit(Callable<T> task) :执行任务,有返回值,一般又来执行 Callable

void shutdown() :关闭连接池

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
package com.yang.advanced;

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

/**
* @author 默烦 2020/10/9
*/
public class TestPool {
public static void main(String[] args) {
// 创建服务,创建线程池
ExecutorService service = Executors.newFixedThreadPool(10);

// 执行
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());

// 关闭连接
service.shutdown();
}
}

class MyThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}

Java 多线程基础完