Java多线程

线程简介

多任务

即同一时间多项事项,比如边吃饭边玩手机,边开车边打电话边打点滴。

多线程

有时一条路无法满足所有车辆的需求,有的车直走,有的车逆行,有的几辆车运行在一条道路上,容易导致冲突和堵塞,效率极低!为了提高利用效率,充分利用道路,于是我们可以加宽加多车道,这便是多线程。

普通方法调用和多线程

我们单线程Java程序运行的方式是,首先主线程下来,调用一个方法,然后执行它,执行完后,再继续往下执行程序。这种方式只有主线程一条路径。

单线程

对于多线程的Java程序运行的方式是,主线程走主线程的,子线程走子线程的,互不冲突,同时运行,效率更高。

多线程

程序、进程、线程

简单来说,在操作系统中运行的一个程序就是进程,比如你平常用的QQ、播放器、游戏、IDE等等都算是一个进程。比如你看视频,相当于启动了一个进程,然后你可以看到图像、声音和字幕,这三个项目是同时进行的,互不影响。它们是播放器进程下的一个个的线程。

Process(进程)与Thread(线程)

说起进程,就不得不说下程序。程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。而进程则是执行程序的一次执行过程,它是一个动态的概念。是系统资源分配的单位。

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

哪怕进程什么都不干,都有一个main线程,主线程。java还有一个GC线程,用于回收清理内存垃圾。程序是静态的,程序跑起来变成了进程,进程里面分为若干个线程,真正跑起来的是线程,程序只是系统分配的一个单位,比如说分配一个进程所处的位置,启动程序,程序里面运行多个线程。

注意:很多多线程是模拟出来的,真正的多线程是指有多个cpu,即多核,如服务器。如果是模拟出来的多线程,即在一个cpu的情况下,在同一个时间点,cpu只能执行一个代码,因为切換的很快,所以就有同时执行的错误,导致线程不安全。

本章核心

  1. 线程就是独立的执行路径;
    • 比如说main线程,它就走main线程,不会去走其他线程。gc线程就执行清理垃圾的线程,互不干扰。
  2. 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程;
  3. main()称之为主线程,为系统的入口,用于执行整个程序;
  4. 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为的干预的。
    • 不能人为干预,由CPU来调度,CPU允许你运行,线程才能运行。
  5. 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
    • 比如说,我们有一万个人来强一百张票,这时就需要加一个并发控制,如果没加,那么一万个人他们都发现有一百张票,都一哄而上,抢完发现票数变成了-9900,这就非常不安全。所以通过加入并发控制,让这些人排队取票,票取完了就完了。
  6. 线程会带来额外的开销,如cpu调度时间,并发控制开销。
    • CPU调度需要时间会带来额外的开销,并发控制也会,比如排队则增加了等候时间。
  7. 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致

线程

线程创建三种方式

继承Thread

Thread实现了Runnable接口,Runnable是最重要的。Callable(不常用,但是也很重要需要了解)

创建线程的三种方式

通过继承Thread类,Thread类也继承了Runnable接口,有相应的实现方式,比如说run()。我们可以直接继承Thread类后重写run方法,在run方法里重写我们要多线程执行的程序,最后调用Thread类的start方法运行即可。

此外,我们还可以查看JDK帮助文档: Thread类的介绍。

线程执行的优先级我们可以设置,但主要还是看CPU的调度,CPU想调谁就调谁。

继承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
package com.everweekup.Thread;

// 创建线程方式一:集成Thread类,重写run方法,调用start开启线程
// 总结: 线程开启不一定立即执行,由CPU调度执行
public class TestThreadDemo01 extends Thread{
@Override
public void run() {
// run方法线程体
for (int i = 0; i < 20; i++) {
System.out.println("子:我在看代码---" + i);
}
}

public static void main(String[] args) {
// 创建一个线程对象
TestThreadDemo01 testThread1 = new TestThreadDemo01();
// 调用start方法开启线程
testThread1.start();

// main线程,主线程
for (int i = 0; i < 20; i++) {
System.out.println("主: 我在学习多线程---" + i);
}
}
}

输出结果:

我们可以发现,为什么主线程的先输出呢?存不存在子线程先输出的情况呢?

经过多次尝试,如加大子线程迭代次数最后结果都是主线程的先输出,这可能是因为启动一个线程需要一定的时间,因为CPU先是在运行一个主线程main,发现有子线程后要进行调度,需要一定的时间去启动子线程。

案例1 网络图片下载

这里我们来做一个网络图片下载的案例,下图是案例的实现概要步骤。

我们先去下载一个已经写好的jar包commons-io-2.6.jar,这个是apache开源的一个jar包。

Apache Commons IO 2.8.0 (requires Java 8)

image-20210411083605388

下载好后解压,拿出commons-io-2.8.0.jar、commons-io-2.8.0-sources.jar这两个包,在项目根目录下创建一个lib的package,放到里面去,之后add as libariry。

Commons IO

是处理IO的工具类包,对java.io进行扩展,提供了更加方便的IO操作

java io操作是开发中比较常用的技术,但是如果每次都使用原生的IO流来操作会显得比较繁琐,Common IO 是一个工具库,用来帮助开发IO功能。

添加jar包参考链接

多线程下载网络图片资源代码实现:

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

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;

// 练习Thread,实现一个多线程下载图片
public class TestThreadDemo02 extends Thread{
private String url; // 网络图片地址
private String name; // 保存的文件名

public TestThreadDemo02(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) {
TestThreadDemo02 t1 = new TestThreadDemo02("https://www.kuangstudy.com/assert/images/xiaok.png", "1.png");
TestThreadDemo02 t2 = new TestThreadDemo02("https://www.kuangstudy.com/assert/images/coursebg.jpg", "2.jpg");
TestThreadDemo02 t3 = new TestThreadDemo02("https://kuangstudy.oss-cn-beijing.aliyuncs.com/bbs/2021/04/10/kuangstudyb5c74883-172e-4f5a-9d6c-7967735b6689.png", "3.png");

// 先执行t1
t1.start();
// 最后是t2
t2.start();
// 接着执行t3
t3.start();
}
}

// 下载器
class WebDownloader{
// 下载方法
public void downloader(String url, String name){
try {
// 调用apache的FileUtils工具类, 把网页上的一个地址变成一个文件
FileUtils.copyURLToFile(new URL(url), new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO异常,downloader方法出现问题");
}
}
}

实现Runnable接口

通过实现Runnable接口类来实现多线程是比较推荐的,因为Java单继承的局限性,实现Runnable接口有很好的灵活性。下图是简要的代码逻辑:

实现Runnable接口后,最终还是要通过Thread类的start方法去启动线程,因为Thread类有对Runnable方法的实现,可以调用Thread启动线程。

Runnable练习代码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
25
26
package com.everweekup.Thread;

// 创建线程方式2: 实现runnable接口,重写run方法,执行线程需要丢入runnable接口的实现类
public class TestThreadDemo03 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接口的实现类对象
TestThreadDemo03 testThread3 = new TestThreadDemo03();
// 创建线程对象,通过线程对象来启动我们的线程,代理
// Thread thread3 = new Thread(testThread3);
// thread3.start();
new Thread(testThread3).start();

// main线程,主线程
for (int i = 0; i < 20; i++) {
System.out.println("主: 我在学习多线程---" + i);
}
}
}

通过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
package com.everweekup.Thread;

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;

// 练习Thread,实现一个多线程下载图片
public class TestThreadDemo02 implements Runnable{
private String url; // 网络图片地址
private String name; // 保存的文件名

public TestThreadDemo02(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) {
TestThreadDemo02 t1 = new TestThreadDemo02("https://www.kuangstudy.com/assert/images/xiaok.png", "1.png");
TestThreadDemo02 t2 = new TestThreadDemo02("https://www.kuangstudy.com/assert/images/coursebg.jpg", "2.jpg");
TestThreadDemo02 t3 = new TestThreadDemo02("https://kuangstudy.oss-cn-beijing.aliyuncs.com/bbs/2021/04/10/kuangstudyb5c74883-172e-4f5a-9d6c-7967735b6689.png", "3.png");

new Thread(t1).start();
new Thread(t2).start();
new Thread(t3).start();

// 先执行t1
// t1.start();
// 最后是t2
// t2.start();
// 接着执行t3
// t3.start();

}

}

// 下载器
class WebDownloader{
// 下载方法
public void downloader(String url, String name){
try {
// 调用apache的FileUtils工具类, 把网页上的一个地址变成一个文件
FileUtils.copyURLToFile(new URL(url), new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO异常,downloader方法出现问题");
}
}
}
案例2 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
package com.everweekup.Thread.Runnable;

// 多个线程操作一个对象
// 买火车票例子

// 发现问题,出现多个线程操作同一个资源,线程不安全,数据紊乱
public class TestThreadDemo04 implements Runnable{
// 票数
private int tickNums = 10;

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

// 模拟延时,避免线程太快,一个人抢完了所有票
try {
Thread.sleep(200); // 延时0.2s
} catch (InterruptedException e) {
e.printStackTrace();
}

// Thread.currentThread()获得当前线程
System.out.println(Thread.currentThread().getName()+"拿到了" + tickNums-- + "票");
}
}

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

// 线程可以起名字
new Thread(ticket, "小米").start();
new Thread(ticket, "小明").start();
new Thread(ticket, "小红").start();

}
}

结果:出现了线程不安全的情况,同样的票多个线程都抢到了(即多个线程访问了不属于自己的资源),甚至出现-1。 这就是多线程编程会出现的一些资源抢占等问题。

总结

image-20210411093207492

案例3 龟兔赛跑

代码逻辑:

  1. 首先来个赛道距离,然后要离终点越来越近
  2. 判断比赛是否结束
  3. 打印出胜利者
  4. 龟兔赛跑开始
  5. 故事中是乌龟赢的,兔子需要睡党,所以我们来模拟兔子睡觉
  6. 终于,乌亀嬴得比赛

龟兔赛跑代码实现:

由于调用Thread.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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package com.everweekup.Thread;

// 龟兔赛跑案例
public class RabbitTurtleRace implements Runnable{
private static String winner;

@Override
public void run() {

for (int i = 0; i <= 50; i++) {
// 模拟兔子休息,每10步休息一会
if (Thread.currentThread().getName().equals("Rabbit") && (i%15 == 0)){
for (int i1 = 0; i1 < 5; i1++) {
System.out.print(""); // sleep
}
// 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 >= 50){
winner= Thread.currentThread().getName();
System.out.println("winner is " + winner);
}
}
return false;
}

public static void main(String[] args) {
RabbitTurtleRace race = new RabbitTurtleRace();

new Thread(race, "Turtle").start();
new Thread(race, "Rabbit").start();
}
}

结果

实现Callable接口

  1. 实现 Callable接口,需要返回值券型
  2. 重写call方法,需要抛出异常
  3. 创建目标对象
  4. 创建执行服务: Executorservice ser= Executors. newFixedThreadPool(1); // 1代表单线程
  5. 提交执行: Future< Boolean> result1= ser. submit(t1);
  6. 获取结果: boolean r1= result1.get();
  7. 关闭服务:ser. 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.everweekup.Thread.Callable;

import com.everweekup.Thread.ExtendsThread.TestThreadDemo02;
import org.apache.commons.io.FileUtils;

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

// 线程创建的方式三: 实现callable接口
/*
1.callable可以定义返回值
2.callable可以抛出异常
*/
public class TestCallableThread implements Callable<Boolean> {
private String url; // 网络图片地址
private String name; // 保存的文件名

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


// 下载图片线程的执行体
// Call方法拥有返回值
@Override
public Boolean call() {
WebDownloader webDownloader = new WebDownloader();
webDownloader.downloader(url, name);
System.out.println("下载的文件名为:"+name);

return true;
}

public static void main(String[] args) throws ExecutionException, InterruptedException {
TestCallableThread t1 = new TestCallableThread("https://www.kuangstudy.com/assert/images/xiaok.png", "1.png");
TestCallableThread t2 = new TestCallableThread("https://www.kuangstudy.com/assert/images/coursebg.jpg", "2.jpg");
TestCallableThread t3 = new TestCallableThread("https://kuangstudy.oss-cn-beijing.aliyuncs.com/bbs/2021/04/10/kuangstudyb5c74883-172e-4f5a-9d6c-7967735b6689.png", "3.png");

// 创建执行服务
// nThreads: 创建3个线程
ExecutorService ser = Executors.newFixedThreadPool(3);

// 提交执行
Future<Boolean> r1 = ser.submit(t1);
Future<Boolean> r2 = ser.submit(t2);
Future<Boolean> r3 = ser.submit(t3);

// 获取结果
boolean rs1 = r1.get();
boolean rs2 = r2.get();
boolean rs3 = r3.get();

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

}

// 下载器
class WebDownloader{
// 下载方法
public void downloader(String url, String name){
try {
// 调用apache的FileUtils工具类, 把网页上的一个地址变成一个文件
FileUtils.copyURLToFile(new URL(url), new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO异常,downloader方法出现问题");
}
}
}

Lambda表达式

lambda简化了代码,是一种函数式的编程思想。使用lambda表达式能够避免内部类定义过多,是我们的代码更加简洁。lambda实质属于函数式编程的一种概念。

lambda表达式使用的前提是接口为函数式接口: 定义一个函数式接口 ,即 只有一个抽象方法的接口

(params) -> expression[表达式]

表达式由变量、操作符和方法调用构成并返回一个值结果,构造过程需要遵守Java语言语法规则。

new Thread(() -> System.out.println("labmda~")).statr;:这里是给Thread里面传入一个lambda表达式,代替Runnable的接口实现

(params) -> statement[语句]

语句大致相当于自然语言中的句子。语句构成一个完整的执行单元。下面的这些表达式只要是以分号;结尾都可以认为是一个语句

(params) -> {statements}

代码块是指花括号{}括起来的0个或多个语句,花括号可以用在有语句存在的地方。

1
2
3
lambda = (a) -> {
System.out.println("I Love You Thress Thounds -->" + a);
};

我们先前定义的对象都是使用过一次后就没有再用了,相当于让代码看起了很臃肿,多了些没有太多意义的代码。使用lambda能够:

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

在学习lambda之前,需要理解 Functional Interface(函数式接口),这是学习Java8 lambda表达式的关键所在。

函数式接口的定义:任何接口,如果只包含唯一一个抽象方法,那么他就是一个函数式接口。

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

对于函数式接口,我们可以通过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.everweekup.Lambda;
/*
推导lambda表达式
*/

public class LambdaTest01 {

// 3.静态内部类
static class Like2 implements ILike{
@Override
public void lambda() {
System.out.println("i like lambda2 !");
}
}

public static void main(String[] args) {
// new一个接口的实现对象
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.jdk8出来了,进一步简化-->lambda
// 避免了过多的内部类
like = () -> {
System.out.println("i like lambda5 !");
};
like.lambda ();

}
}
// 1.定义一个函数式接口 : 只有一个抽象方法的接口
interface ILike{
// 自动抽象,隐式申明
// 接口中的所有定义都是抽象的 public abstract,方法默认用public abstract修饰
void lambda ();
}

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

练习代码:

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

public class LambdaTestParameters01 {

//2.静态外部类
static class Love2 implements ILove{
@Override
public void love(int a) {
System.out.println("I Love You Thress Thounds -->" + a);
}
}

public static void main(String[] args) {
// 3.局部类
class Love3 implements ILove{
@Override
public void love(int a) {
System.out.println("I Love You Thress Thounds -->" + a);
}
}


ILove love = new Love();
love.love(1);

love = new Love2();
love.love(2);

love = new Love3();
love.love(3);

// 4.匿名内部类
love = new ILove(){
@Override
public void love(int a) {
System.out.println("I Love You Thress Thounds -->" + a);
}
};
love.love(4);

// 5.lambda
// 前面已经将love引用指向了ILove对象
love = (int a) -> {
System.out.println("I Love You Thress Thounds -->" + a);
};
love.love(5); // I Love You Thress Thounds -->5

// lambda简化1,去掉参数类型
love = (a) -> {
System.out.println("I Love You Thress Thounds -->" + a);
};
love.love(6);

// lambda简化2,去掉括号
love = a -> {
System.out.println("I Love You Thress Thounds -->" + a);
};
love.love(7);

// lambda简化3,去掉花括号
love = a -> System.out.println("I Love You Thress Thounds -->" + a);
love.love(8); // I Love You Thress Thounds -->8
/*
总结: 1.因为代码里只有一行,所以可以去掉花括号,将lambda简化成一行
2.如果有多行,则lambda表达式用花括号括起来
3.lambda表达式使用的前提是接口为函数式接口 // 定义一个函数式接口 : 只有一个抽象方法的接口
4.lambda表达式中多个参数可以都去掉参数类型,要去掉就都去掉
*/
// 需要修改接口的参数个数
// love = (a) -> {
// System.out.println("I Love You Thress Thounds -->" + a);
// System.out.println("I Love You Too" + b);
// };


}
}

interface ILove{
void love(int a);
}

// 1.
class Love implements ILove{
@Override
public void love(int a) {
System.out.println("I Love You Thress Thounds -->" + a);
}
}

class Love1 implements ILove{
@Override
public void love(int a) {
System.out.println("I Love You Thress Thounds -->" + a);
}
}

静态代理

何为静态代理?我们先来理解理解代理。代理,顾名思义,就是找个人或公司以你的名义,帮你做事。比如你要结婚了,结婚大大小小那么多事,又要办酒席,又要找场地,又要发通知等等,这么多事情,一个人做实在是忙不过来,不过我们可以找别人帮忙代理做一些事情。比如婚庆公司,帮你处理结婚相关的事宜,比如订场地,办酒席等。

映射到Java对象里,”你”是一个对象,”婚庆公司”也是一个对象,而结婚这个事项,可以看成一个”接口”,在这接口定义结婚的相关规则,然后你和婚庆公司实现这个“结婚”接口。具体我们来看一下代码~

代码理解静态代理:

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
package com.everweekup.Thread.StaticProxy;

/*
静态代理模式总结
1.真实对象和代理对象都要实现同一个接口;
2.代理对象要代理真实角色;
好处:
1.代理对象能够做很多真实对象做不了的事情
2.真实对象专注做自己的事情

*/

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

// 原始模式是自己直接结婚,不找代理,很low
// You you = new You();
// you.HappyMarry();

// WeddingCompany weddingcompany = new WeddingCompany(new You());
// weddingcompany.HappyMarry();
new WeddingCompany(new You()).HappyMarry();
// 和上面的WeddingCompany对比,可以发现两者很相似,都是代理,Thread实现的是Runnable接口,代理的是括号里的真实的Runnable接口
new Thread(() -> System.out.println("I Love You")).start();
}

}

interface Marry{

void HappyMarry();
}
// 真实角色
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 before(){
System.out.println("结婚之前布置现场");
}

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

线程状态

线程五个状态

阻塞状态是由线程运行状态进入的,不会从就绪状态过去,现场阻塞拓展内容 :

  • sleep():占用资源睡觉,可以限制等待时间;
  • wait():不占用资源,可以限制等待时间;
  • join():加入、合并或插队,这个方法会阻塞当前线程到另一个线程完成后才继续运行;
  • IO阻塞,如write()或read()方法,通过操作系统调用;

上面的方法和start方法类似,不是说调用了就能立即执行,要看cpu的调度。

线程方法

停止线程

停止线程不推荐使用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
42
package com.everweekup.Thread.ThreadState;

// 测试stop线程
// 1. 建议线程正常停止--->利用次数,不建议死循环
// 2. 建议使用标志位---> 设置标志位置
// 3. 不要使用stop、destory等过时的,JDK不建议使用的方法

public class TestStop implements Runnable{
// 1. 设置一个标志位
private boolean flag = true;

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

// 设置一个公开的方法停止线程, 对外提供调用,用于转换标志位,停止线程
public void stop(){
this.flag = false;
}

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

new Thread(testStop).start();

// 主线程和线程都在跑,主线程900时,停止线程
for (int i = 0; i < 1000; i++) {
System.out.println("main" + i);

if(i==900){
// 调用stop方法切换标志位
testStop.stop();
System.out.println("线程该停止了");
}
}
}

}

线程休眠

  • 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
38
39
package com.everweekup.Thread.ThreadState;

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

public class TestCount implements Runnable{

public static void main(String[] args) {
try {
tenCount();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

// 模拟倒计时
public static void tenCount() throws InterruptedException {
int num =10;
// 打印当前系统时间
Date starttime = new Date(System.currentTimeMillis()); // 获取当前系统时间

while (true){
// 每一s停一下
Thread.sleep(1000);
// System.out.println(num--);
System.out.println(new SimpleDateFormat("HH:mm:ss").format(starttime));
starttime = new Date(System.currentTimeMillis()); // 更新当前时间

if(num <= 0){
break;
}
}
}

@Override
public void run() {

}
}

线程礼让

线程礼让,即让当前正在执行的线程暂停但不阻塞,将线程从运行状态转为就绪状态。让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
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.everweekup.Thread.ThreadYield;

// 测试礼让线程,礼让不一定成功,看cpu调度
public class TestYield {
public static void main(String[] args) {
MyYield myYield = new MyYield();

new Thread(myYield, "a").start();
new Thread(myYield, "b").start();
/*
礼让成功
aThread Start Run
bThread Start Run
bThread Stop Run
aThread Stop Run
礼让失败
aThread Start Run
aThread Stop Run
bThread Start Run
bThread Stop Run
*/
}
}

class MyYield implements Runnable{
// 礼让成功 start start
// 礼让失败 start stop start
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"Thread Start Run");

Thread.yield(); // 礼让

System.out.println(Thread.currentThread().getName()+"Thread Stop Run");
}

}

线程强制执行 Join

join合并现场,等待此线程执行完后再执行其他线程,其他现场处于阻塞状态,可以现象成插队。

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
package com.everweekup.Thread.ThreadJoin;

// 测试join方法,想象成插队
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 < 1000; i++) {
if(i == 200){
thread.join(); //插队,此时让线程vip执行,执行完后主线程才能继续
}
System.out.println("main" + i);
}
}
}

观测线程状态

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.everweekup.Thread.ThreadState;

// 测试线程状态
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 (InterruptedException 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); // Runnable

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

}

// Exception in thread "main" java.lang.IllegalThreadStateException
thread.start(); // 会报错,死亡之后的线程不会再启动
}
}

线程优先级

线程优先级高,cpu不一定先调用。但是java里面可以通过分配较多的资源来实现线程优先调用的概率。比如你买彩票,1张和100张,哪个中将概率大?

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

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

  • Thread. MIN PRIORITY =1
  • Thread. MAX PRIORITY= 10
  • Thread. NORM PRIORITY =5

优先级低只是意味着获得调度的概率低。并不是优先级低就不会被调用了,这都是看CPU的调度

使用以下方式改变或获取优先级

  • getPriority().setPriority(int xxx)
    • 优先级的设定建议在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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package com.everweekup.Thread.Priority;

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

// MyPriority myPriority = new MyPriority();
Thread thread1 = new Thread(
() -> System.out.println(Thread.currentThread().getName() + "---->" + Thread.currentThread().getPriority())
);
Thread thread2 = new Thread(
() -> System.out.println(Thread.currentThread().getName() + "---->" + Thread.currentThread().getPriority())
);
Thread thread3 = new Thread(
() -> System.out.println(Thread.currentThread().getName() + "---->" + Thread.currentThread().getPriority())
);
Thread thread4 = new Thread(
() -> System.out.println(Thread.currentThread().getName() + "---->" + Thread.currentThread().getPriority())
);
Thread thread5 = new Thread(
() -> System.out.println(Thread.currentThread().getName() + "---->" + Thread.currentThread().getPriority())
);
Thread thread6 = new Thread(
() -> System.out.println(Thread.currentThread().getName() + "---->" + Thread.currentThread().getPriority())
);



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

thread2.setPriority(1);
thread2.start();

thread3.setPriority(2);
thread3.start();

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

// thread5.setPriority(-1); // 报错
// thread5.start();
//
// thread6.setPriority(11); // 报错
// thread5.start();

/*
线程不一定优先级别高的会先跑,但大多数时候是这样
main---->5
Thread-0---->5
Thread-1---->1
Thread-2---->2
Thread-3---->10
*/

}

}

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

守护线程

main是用户线程,gc是守护线程(后端监控垃圾回收,等待机制)

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

这里来看一个守护现场的代码:

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
package com.everweekup.Thread.ThreadState;

// 测试守护线程
// 上帝守护你
public class TestDameon {
public static void main(String[] args) {
God god = new God();
You you = new You();

Thread thread = new Thread(god);
thread.setDaemon(true);

thread.start(); // 上帝守护线程启动

Thread thread1 = new Thread(you);
thread1.start(); // 你得线程结束后,守护线程会强制结束,但是还会打印一些数据,因为jvm关闭需要时间
}
}

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


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

关于线程状态可以参考的博客推荐:https://www.cnblogs.com/lifegoeson/p/13516019.html

线程同步(重点)—线程安全

为了实现并发,我们会设置多线程操作同一个资源,但是多个线程操作同一个资源经常会出现诸如线程不安全等问题。比如之前得抢票问题,就会出现线程不安全的问题。这时,我们就要用到线程同步,通过队列访问一个资源,实现等待机制,来解决这个问题。

现实生活中,我们会遇到”同一个资源,多个人都想使用”的问题,比如,食堂排队打饭,每个人都想吃饭,最天然的解决办法就是,排队。一个个来。

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

队列和锁

举个例子,就像你排队上厕所,为了更加舒适和安全,排队到你后,进入厕所你会把门给锁上。线程也会这样做,为了保证线程同步的安全,当一个线程正在使用一个资源时,会把这个资源给上锁,避免出现安全问题。

由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.everweekup.Thread.SyncThread;

// 不安全的买票
// 为什么会出现-1:每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
public class UnsaveTicket {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();

new Thread(buyTicket, "小明").start();
new Thread(buyTicket, "小红").start();
new Thread(buyTicket, "小张").start();

}
}

class BuyTicket implements Runnable {
// 票
private int ticketNums = 10;
private boolean flag = true;

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

private void buy() throws InterruptedException {
// 判断是否有 票
if(this.ticketNums <= 0){
this.flag = false;
return;
}
Thread.sleep(100);

// 买票
System.out.println(Thread.currentThread().getName() + "拿到" + ticketNums--);
}
}

线程不安全的银行:

两个人眼里看到主内存有100万,都是可以取的,由于内存是各自的互不干扰,所以两个都取了,导致出现负数。

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
package com.everweekup.Thread.SyncThread;

// 不安全的银行
// 两个人去银行取钱
public class UnsaveBank {
public static void main(String[] args) {
Account account = new Account(100, "结婚基金");

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

you.start();
girl.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;
this.nowMoney = nowMoney;

}

// 取钱
@Override
public void run(){
// 判断有没钱
if(account.money - drawingMoney < 0){
System.out.println(Thread.currentThread().getName() + "钱不够了,取不了");
return;
}
// sleep放大线程不安全
try {
Thread.sleep(1000); // 延时1s
} catch (InterruptedException e) {
e.printStackTrace();
}

// 卡内余额
account.money = account.money - drawingMoney ;
// 你手里的钱
nowMoney = drawingMoney + nowMoney;

System.out.println(account.name + "余额为: " + account.money);
// Thread.currentThread().getName() == this.getName();
System.out.println(this.getName() + "手里的钱有: " + nowMoney);


}

}

线程不安全的集合:

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
package com.everweekup.Thread.SyncThread;

import java.util.ArrayList;
import java.util.List;

// 线程不安全的集合
public class UnsafeList {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
list.add(Thread.currentThread().getName());
}).start();
}
try {
Thread.sleep(3000); // 9999 : 说明线程是不安全的,有一个线程覆盖掉了一个线程.
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println(list.size());


}
}

同步方法和同步块

同步方法

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

同步方法:public synchronized void method(int args){}

synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞。方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。

缺陷:若将一个大的方法申明为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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.everweekup.Thread.ThreadLock;

// 不安全的买票
// 为什么会出现-1:每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
public class UnsafeTicket2 {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();

new Thread(buyTicket, "小明").start();
new Thread(buyTicket, "小红").start();
new Thread(buyTicket, "小张").start();

}
}

class BuyTicket implements Runnable {
// 票
private int ticketNums = 10;
private boolean flag = true;


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

// synchronized同步方法,实现一把锁,锁的是对象本身,this
private synchronized void buy() throws InterruptedException {
// 判断是否有 票
if(this.ticketNums <= 0){
this.flag = false;
return;
}
// Thread.sleep(100);

// 买票
System.out.println(Thread.currentThread().getName() + "拿到" + this.ticketNums--);
}
}

同步块

同步块:synchronized(Obj){}

Obj称之为同步监视器

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

同步监视器的执行过程

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

要监视的对象是增删改查的对象。syn锁会引发死锁问题。jdk5新增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
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
package com.everweekup.Thread.ThreadLock.SyncThread;

// 不安全的银行
// 两个人去银行取钱
public class UnsaveBank {
public static void main(String[] args) {
Account account = new Account(100, "结婚基金");

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

you.start();
girl.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;
this.nowMoney = nowMoney;

}

// 取钱
// synchronized 同步的是this.本身,无用
@Override
public void run(){
// 锁的对象是变化的量,需要增删改
// 如果一开始只对run进行锁,synchronized (this),锁的是对象本身,无法控制accout,仍然会出现不安全的问题
synchronized (account){ // 对accout进行锁
// 判断有没钱
if(account.money - drawingMoney < 0){
System.out.println(Thread.currentThread().getName() + "钱不够了,取不了");
return;
}
// sleep放大线程不安全
try {
Thread.sleep(1000); // 延时1s
} catch (InterruptedException e) {
e.printStackTrace();
}

// 卡内余额
account.money = account.money - drawingMoney ;
// 你手里的钱
nowMoney = drawingMoney + nowMoney;

System.out.println(account.name + "余额为: " + account.money);
// Thread.currentThread().getName() == this.getName();
System.out.println(this.getName() + "手里的钱有: " + nowMoney);
}
}

}

补充JUC并发包+线程安全

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.everweekup.Thread.SyncThread;

import java.util.concurrent.CopyOnWriteArrayList;

// 测试JUC安全类型的集合
// JUC是java的并发包
public class TestJUC {
public static void main(String[] args) {
CopyOnWriteArrayList list = new CopyOnWriteArrayList<String>(); // JUC里的本身就是安全的线程
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
list.add(Thread.currentThread().getName());
}).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());

}
}

死锁

多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。

java代码模拟死锁:

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
package com.everweekup.Thread.DeadLock;

// 死锁: 多个线程互相抱着对方需要的资源,然后形成僵持
public class DeadLockTest {
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){ // 1s钟后获得镜子的锁
System.out.println(this.girlName + "获得镜子的锁");
}
}
}else{
synchronized (mirror){ // 获得口红的锁
System.out.println(this.girlName + "获得镜子的锁");
Thread.sleep(1000);

synchronized(lipstick){ // 1s钟后获得镜子的锁
System.out.println(this.girlName + "获得口红的锁");
}
}
}
}
}

修正方法:不要让两个人同时抱一把锁

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
package com.everweekup.Thread.DeadLock;

// 死锁: 多个线程互相抱着对方需要的资源,然后形成僵持
public class DeadLockTest {
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){ // 1s钟后获得镜子的锁
System.out.println(this.girlName + "获得镜子的锁");
}
}else{
synchronized (mirror){ // 获得口红的锁
System.out.println(this.girlName + "获得镜子的锁");
Thread.sleep(1000);
}
synchronized(lipstick){ // 1s钟后获得镜子的锁
System.out.println(this.girlName + "获得口红的锁");
}
}
}
}

死锁避免方法

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

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

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

从JDK5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当

java. util. concurrent. locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象

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

ReetranLock是可重入锁。

synchronized与Lock的对比

  • Lock是显式锁(手动开启和关洞锁,別忘记关闭锁) synchronized是隐式锁,出了作用域自动释放
  • Lock只有代码块锁, synchronized有代码块锁和方法锁
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
  • 优先使用顺序
    • Lock>同步代码块(已经进入了方法体,分配了相应资源)>同步方法(在方法体之外)

线程通信问题

之前我们学习的多线程编程,每个线程都是独立运行,一旦某个线程达到某个条件,就停止程序,没有涉及到线程之间的协作通信问题。

生产者消费者模式

生产者和消费者模式是一个问题,不是23种设计模式之一。

这是一个线程同步向题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。

  • 对于生产者,没有生产产品之前,要通知消费者等待,而生产了产品之后,又需要马上通知消费者消费
  • 对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费。
  • 在生产者消费者问题中,仅有 synchronized是不够的
    • synchronized可阻止并发更新同一个共享资源,实现了同步
    • synchronized不能用来实现不同线程之间的消息传递(通信)

思路分析

  • 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费
  • 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止
  • 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待直到仓库中再次放入产品为止

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

  • wait()
    • 表示线程一直等待,直到其他线程通知,与 sleep不同,会释放锁,slep是抱着锁睡觉;
  • notify()
    • 唤醒一个处于等待状态的线程
  • notifyAll()
    • 唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度

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

解决方式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
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.everweekup.Thread.ThreadCommunicateMethod;

// 测试生产者消费者模型--->利用缓冲区解决---管程法
// 生产者、消费者、产品、缓冲区
public class TestPC {
public static void main(String[] args) {
Container container = new Container();

new Producer(container).start();
new Consumer(container).start();
}

}
// 生产者
class Producer extends Thread {
Container container;

public Producer(Container container){
this.container = container;
}
// 生产
@Override
public void run(){
for (int i = 0; i < 100; i++) {
container.push(new Product(i));
System.out.println("生产了" + i + "只鸡");
// test
// try {
// Thread.sleep(100);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
}
}

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

public Consumer(Container container){
this.container = container;
}
// 消费
@Override
public void run(){
for (int i = 0; i < 100; i++) {
System.out.println("消费了--->" + container.consume().id + "只鸡");
// test
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

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

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


// 缓冲区
class Container{
// 需要一个容器大小
Product[] products = new Product[10];
// 容器计数器
int count = 0;

// 生产者放入产品
public synchronized void push(Product product){
// 如果容器满了需要等待消费者消费
if(count == products.length){
// 如果容器满了,通知消费者消费,生产等待
try { // 让生产者等待
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

// 生产
products[count] = product;
count++; // 可以通知消费者消费

// 通知消费
this.notifyAll();
}
public synchronized Product consume(){
// 判断能否消费
if(count == 0){
// 等待消费者生产,消费者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

// 如果可以消费
count--; // 拿初始例子来看,因为生产者生产后count是1,对应的product索引是0
Product product = products[count];
// count--;
// 消费完,通知生产者生产
this.notifyAll();

return product;
}
}

解决方式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
package com.everweekup.Thread.ThreadCommunicateMethod;

// 测试生产者和消费者问题2:信号灯法,标志位解决
public class TestPCSignalLightMethod {
public static void main(String[] args) {
TV tv = new TV();

new Actor(tv).start();
new Audience(tv).start();
}
}

// 生产者 -- >演员
class Actor extends Thread{
TV tv;
public Actor(TV tv){
this.tv = tv;
}

@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i%2 == 0){
this.tv.play("<快乐大本营播放中>");
}else{
this.tv.play("抖音: 记录美好生活");
}
// // test
// try {
// Thread.sleep(100);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
}
}

// 消费者--> 观众
class Audience extends Thread{
TV tv;
public Audience(TV tv){
this.tv = tv;
}

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

// 产品 ---> 节目
class TV{
String voice;
boolean flag = true;

// 出演节目
// 演员表演的时候观众等待 T
public synchronized void play(String voice){
// 观众观看时,演员等待
if(!this.flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("演员表演了" + voice);
// 通知观众观看
this.voice = voice;
this.flag = !this.flag;
this.notifyAll(); // 唤醒

}

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



}
}

高级主题

使用线程池

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

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

好处:

  • 提高响应速度(减少了创建新线程的时间)
  • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  • 便于线程管理(…)
    • corePoolsize:核心池的大小
    • maximumPoolSize:最大线程数
    • keepAliveTime:线程没有任务时最多保持多长时间后会终止

JDK5.0起提供了线程池相关API: ExecutorService和 Executors

Executorservice:真正的线程池接口。常见子类 ThreadPoolExecutor

  • void execute( Runnable command): 执行任务命令,没有返回值,一般用来执行 Runnable
  • Future submit( Callabletask): 执行任务,有返回值,一般用来执行 Callable
  • void shutdown():关闭连接池

Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

callable提交用submmit,runabble提交线程池用execute。

线程池代码:

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.everweekup.Thread.ThreadPool;

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

// 测试线程池
public class TestPool {
public static void main(String[] args) {
// 创建服务,创建线程池
// newFixedThreadPool参数表示线程池大小
ExecutorService service = Executors.newFixedThreadPool(10);
MyThread myThread = new MyThread();

// 执行
service.execute(myThread);
service.execute(myThread);
service.execute(myThread);
service.execute(myThread);

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

}
}

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

参考连接

https://www.kuangstudy.com/