Java多线程

一、介绍

在了解线程之前,还需要简单了解进程的概念。简单的来说就是一心多用

在生活之中,我们常常可以一心多用。我可以一边打游戏,一边放着音乐听听歌,甚至可以再泡个脚。没错,这也可以理解成我的多线程生活。

而在计算机之中,也有以上同时进行的任务,这就可以叫做多线程,例如

  • 进程:比如说电脑上开着游戏,音乐等其他多款应用。这每一个应用姑且可以算作一个进程

  • 线程:往往一款游戏,有伤害计算,有数据上传,图像音乐等等的步骤,这每个执行的细项也可以理解成一个线程

所以总结来看,进程是一个应用运行的过程,可以包含多个线程运行,但至少必须要有一个线程,这样才能撑得起这是个进程。

线程是cpu对某个资源的调度计算的通道,这条通道下,cpu可以执行某些任务的调度。

在java中,我们从Main方法运行,所以称其为主线程

除了主线程外,java还有一个后台线程在默默地工作着,这就是GC线程,也就是垃圾回收所处的线程

二、Java线程的实现

1)继承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
37
package com.banmoon.mode;

/**
* 实现多线程方式
* 1、继承类Thread
* 2、实现其run方法
* 3、创建该对象,调用start方法
*/
public class ExtendsMode{

public static void main(String[] args) {
ExtendsModeA modeA = new ExtendsModeA();
ExtendsModeB modeB = new ExtendsModeB();
modeA.start();
modeB.start();
for (int i = 0; i < 1000; i++)
System.out.println("=========== 主线程 ===========");
}
}

class ExtendsModeA extends Thread{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("=========== 线程A ===========");
}
}
}

class ExtendsModeB extends Thread{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("=========== 线程B ===========");
}
}
}

启动后就会发现,原本应该最后打印的主线程,居然夹杂在线程A和线程B之中了。

这也就是说这几条线程是交替执行的,计算机实际上不能做到真正的并发,但它的线程之间的切换人为感知不出来,所以就给人一种并发的错觉。

那一条线程优先执行,这和CPU的调度有关,后续会讲到。

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

/**
* 实现多线程方式
* 1、实现接口Runnable
* 2、构造Thread对象,将Runnable实现对象作为参数
* 3、调用Thread对象的start方法
*/
public class RunnableMode {

public static void main(String[] args) {
Thread modeA = new Thread(new RunnableModeA());
Thread modeB = new Thread(new RunnableModeB());
modeA.start();
modeB.start();
for (int i = 0; i < 1000; i++)
System.out.println("=========== 主线程 ===========");
}
}

class RunnableModeA implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("=========== 线程A ===========");
}
}
}

class RunnableModeB implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("=========== 线程B ===========");
}
}
}

执行后效果与上方一致,打印的信息都是穿插打印的

由于Java只支持单继承,为了使得线程实现更具有灵活性,推荐使用Runnable接口方式

此外,Runnable还有Lanmbda的简写方式

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

/**
* 实现多线程方式
* 1、实现接口Runnable,Lambda简写方式
*/
public class RunnableModeByLambda {

public static void main(String[] args) {
new Thread(() -> {
for (int i = 0; i < 1000; i++)
System.out.println("=========== 线程A ===========");
}).start();
new Thread(() -> {
for (int i = 0; i < 1000; i++)
System.out.println("=========== 线程B ===========");
}).start();
for (int i = 0; i < 1000; i++)
System.out.println("=========== 主线程 ===========");
}
}

3)实现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
package com.banmoon.mode;

import java.util.concurrent.*;

/**
* 实现多线程方式
* 1、实现Callable接口,了解
* 2、需要定义返回值类型
* 3、创建执行服务线程池,来进行执行
*/
public class CallableMode {

public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService service = Executors.newFixedThreadPool(2);
Future<String> resultA = service.submit(new CallableModeA());
Future<String> resultB = service.submit(new CallableModeB());
System.out.println("结果A:" + resultA.get());
System.out.println("结果B:" + resultB.get());
for (int i = 0; i < 1000; i++)
System.out.println("=========== 主线程 ===========");
service.shutdown();
}

}

class CallableModeA implements Callable<String>{
@Override
public String call() throws Exception {
String str = "=========== 线程A ===========";
for (int i = 0; i < 1000; i++)
System.out.println(str);
return str;
}
}

class CallableModeB implements Callable<String>{
@Override
public String call() throws Exception {
String str = "=========== 线程B ===========";
for (int i = 0; i < 1000; i++)
System.out.println(str);
return str;
}
}

这里使用到了一个执行服务工具类Executors,它可以创建线程池,后续会讲到

三、线程状态及方法

1)状态

image-20211205001704675

其实,jdk中还有一个线程状态的枚举Thread.State

和上图有些不同,但不影响理解,只是少了个就绪的状态

2)方法

Thread方法 是否静态 说明
setPriority 设置线程的优先级,优先级高的更有机会优先被CPU调度,但这个不是绝对
sleep 让当前所处的线程进行休眠,可以用来模拟网络延迟,放大同步问题
join 插队,等待正在运行的线程终止
yield 暂停当前的线程,执行其他的线程,让CPU选择再次进行选择调度

1、setPriority

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.banmoon.status;

public class ThreadPriorityMethods {

public static void main(String[] args) {
Thread threadA = new Thread(new MyThread(), "线程A");
Thread threadB = new Thread(new MyThread(), "线程B");
Thread threadC = new Thread(new MyThread(), "线程C");
threadA.setPriority(9);
threadB.setPriority(5);
threadC.setPriority(1);
threadC.start();
threadB.start();
threadA.start();
}

}

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

2、sleep

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.banmoon.status;

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

public class ThreadSleepMethods {

public static void main(String[] args) {
new Thread(() -> {
int i = 0;
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
while (i++<10){
String dateStr = sdf.format(new Date());
System.out.println(dateStr + "============ 线程A ============");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}

3、join

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.banmoon.status;

public class ThreadJoinMethods {

public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
for (int i = 0; i < 200; i++) {
System.out.println("============= 线程A"+ i +" =============");
}
});
thread.start();
for (int i = 0; i < 1000; i++) {
if(i==100)
thread.join();
System.out.println("============= 主线程"+ i +" =============");
}
}

}

4、yield

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.banmoon.status;

public class ThreadYieldMethods {

public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
if(i==200)
Thread.yield();
System.out.println("============= 线程A"+ i +" =============");
}
});
thread.start();
for (int i = 0; i < 1000; i++) {
if(i==500)
Thread.yield();
System.out.println("============= 主线程"+ i +" =============");
}
}
}

四、synchronized关键字

1)并发数据问题

看下列代码,总共有10张票,创建3个线程,每个线程都去取票,直到票数小于0,则退出

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.banmoon.sync;

/**
* 不安全的买票服务
*/
public class TicketServer implements Runnable{

private int ticketNum = 10;

/**
* 取票
*/
@Override
public void run() {

// 获取当前线程名
String name = Thread.currentThread().getName();
while (true){
if(ticketNum<=0)
return;
System.out.println(name + ":取到了第" + ticketNum + "张票");
// 模拟网络延迟
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticketNum--;
}
}

public static void main(String[] args) {
TicketServer ticketServer = new TicketServer();
new Thread(ticketServer, "A").start();
new Thread(ticketServer, "B").start();
new Thread(ticketServer, "C").start();
}
}

预期将会是,10,9,8,7…直到取完票,但真实的结果是

image-20211204181756933

我执行了很多遍,结果远远没有出现我预期的模样。

这是因为cpu调度线程太快了,当取票完成,但票数还没有减一的时候,其他的线程读取了没有减票前的票数,所以导致出现的问题,此类问题被称为并发问题,也称线程安全问题

2)synchronized介绍

此关键字保证了访问同个资源时出现的并发问题。

他的工作原理是对指定对象进行加锁,对此锁表示占有。导致其他线程进不来,可以查看下面示例的代码

synchronized对取票检票进行限制,这把锁就是ticketServer。当A线程获取锁,进入代码执行时,其他线程必须进行等待,直到A线程完成逻辑释放锁后,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.banmoon.sync;

public class SyncTicketServer implements Runnable{

private int ticketNum = 10;

/**
* 取票
*/
@Override
public void run() {
// 获取当前线程名
String name = Thread.currentThread().getName();
while (true){
// 注意,锁住的是this对象哦,也就是32行创建出来的ticketServer
synchronized (this){
if(ticketNum<=0)
return;
System.out.println(name + ":取到了第" + ticketNum + "张票");
// 模拟网络延迟
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticketNum--;
}
}
}

public static void main(String[] args) {
SyncTicketServer ticketServer = new SyncTicketServer();
new Thread(ticketServer, "A").start();
new Thread(ticketServer, "B").start();
new Thread(ticketServer, "C").start();
}
}

执行结果

image-20211205164432798

3)死锁

诚然,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
47
48
package com.banmoon.sync;

public class DeadLock {

public static void main(String[] args) {
Thread threadA = new Thread(new ThreadA(), "ThreadA");
Thread threadB = new Thread(new ThreadB(), "ThreadB");
threadA.start();
threadB.start();
}

}

class ThreadA implements Runnable{

@Override
public void run() {
synchronized (ThreadA.class){
System.out.println(Thread.currentThread().getName() + "已持有锁:ThreadA");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (ThreadB.class){
System.out.println(Thread.currentThread().getName() + "已持有锁:ThreadA");
}
}
}
}

class ThreadB implements Runnable{

@Override
public void run() {
synchronized (ThreadB.class){
System.out.println(Thread.currentThread().getName() + "已持有锁:ThreadB");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (ThreadA.class){
System.out.println(Thread.currentThread().getName() + "已持有锁:ThreadA");
}
}
}
}

真实的情况远比上述要复杂的多,但死锁的基本概念就是如此。

4)不同的修饰位置

我们现在知道,synchronized锁住的是对象,也就是获取到了对象的锁,但处于不同的修饰位置,获取哪个对象的锁也是不一致的。

简单可以分为下面几个修饰位置

1、修饰this时

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.banmoon.sync;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;

/**
* sync修饰对象时
*/
public class SyncDemo1{

private void printOne(){
synchronized (this){
try {
Thread.sleep(2000);
String format = StrUtil.format("{}:当前this对象:{},时间:{}", Thread.currentThread().getName(), this, DateUtil.now());
System.out.println(format);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

private void printTwo(){
synchronized (this){
String format = StrUtil.format("{}:当前this对象:{},时间:{}", Thread.currentThread().getName(), this, DateUtil.now());
System.out.println(format);
}
}

public static void main(String[] args) {
SyncDemo1 syncDemo1 = new SyncDemo1();
new Thread(() -> {
syncDemo1.printOne();
}, "线程A").start();
new Thread(() -> {
syncDemo1.printTwo();
}, "线程B").start();
// System.out.println(syncDemo1);// 指的就是31行创建出来的对象
}

}

image-20211206231143698

如上,因为只创建一个实例,所以他们锁定的只是this

如果不信,可以再new SyncDemo1(),让线程B去调用这个实例对象的printTwo(),保证你看到不同的结果

2、修饰XXX.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
package com.banmoon.sync;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;

/**
* 当修饰class对象时
*/
public class SyncDemo2 {

private void printOne(){
synchronized (SyncDemo2.class){
try {
Thread.sleep(2000);
String format = StrUtil.format("{}:时间:{}", Thread.currentThread().getName(), DateUtil.now());
System.out.println(format);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

private void printTwo(){
synchronized (SyncDemo2.class){
String format = StrUtil.format("{}:时间:{}", Thread.currentThread().getName(), DateUtil.now());
System.out.println(format);
}
}

public static void main(String[] args) {
SyncDemo2 syncDemo2 = new SyncDemo2();
new Thread(() -> {
syncDemo2.printOne();
}, "线程A").start();
new Thread(() -> {
syncDemo2.printTwo();
}, "线程B").start();
// System.out.println(SyncDemo2.class);// class对象具有唯一性
}
}

这个结果与上面一致,因为锁住的都是同一个对象

3、修饰成员方法时

在这里我做了对比,判断修饰this和修饰成员方法时,锁住的对象是否是同一个

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.banmoon.sync;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;

/**
* 修饰普通方法,锁住的到底是什么
*/
public class SyncDemo3 {

private synchronized void printOne(){
try {
Thread.sleep(2000);
String format = StrUtil.format("{}:时间:{}", Thread.currentThread().getName(), DateUtil.now());
System.out.println(format);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

private void printTwo(){
synchronized ("Don't write that"){
// synchronized (this){// 测试锁住的是否是this
String format = StrUtil.format("{}:时间:{}", Thread.currentThread().getName(), DateUtil.now());
System.out.println(format);
}
}

public static void main(String[] args) {
SyncDemo3 syncDemo3 = new SyncDemo3();
new Thread(() -> {
syncDemo3.printOne();
}, "线程A").start();
new Thread(() -> {
syncDemo3.printTwo();
}, "线程B").start();
}

}

当23行放开进行使用,会发现线程B会被线程A卡住,说明修饰成员方法时,获取的就是当前对象的锁

4、修饰静态方法时

在这里进行了对比,判断修饰静态方法、修饰this、修饰当前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
package com.banmoon.sync;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;

public class SyncDemo4 {

private static synchronized void printOne(){
try {
Thread.sleep(2000);
String format = StrUtil.format("{}:时间:{}", Thread.currentThread().getName(), DateUtil.now());
System.out.println(format);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

private void printTwo(){
synchronized (this){
// synchronized (SyncDemo4.class){// 判断获取的是否是SyncDemo4.class对象
String format = StrUtil.format("{}:时间:{}", Thread.currentThread().getName(), DateUtil.now());
System.out.println(format);
}
}

public static void main(String[] args) {
SyncDemo4 syncDemo4 = new SyncDemo4();
new Thread(() -> {
syncDemo4.printOne();
}, "线程A").start();
new Thread(() -> {
syncDemo4.printTwo();
}, "线程B").start();
}
}

如果线程B马上打印,那说明获取的不是同一把锁。要是线程B被线程A卡住了,那说明确实是一把锁了

总结

  • 修饰代码块时:锁住的是括号中的对象
    • this:指向的是当前对象,也就是实现重写run方法的类实例化出来的对象
    • XXX.class:就是这个class对象,我比较喜欢使用,因为class对象具有唯一性
  • 修饰方法时:
    • 成员方法:成员方法所在的类所创建出来的对象,也就是谁调用了这个方法,获取的就是谁的锁
    • 静态方法:当前方法所在类的class对象,XXX.class

注意:不要修饰什么乱七八糟的对象,比如字符串对象,就像上面写的Don't write that。有时候,自己都分不清两个是不是同一个对象,还敢乱写在代码中。

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

public class SyncException {

public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (SyncException.class){
for (int i = 0; i < 20; i++) {
if(i==10)
i = 1/0;// 异常
System.out.println("线程A:" + i);
}
}
}).start();

Thread.sleep(3000);
synchronized (SyncException.class){
for (int i = 0; i < 10; i++) {
System.out.println("主线程");
}
}
}

}

如下运行结果,线程中出现异常时,当前持有的锁会立即释放。所以一定要准确的捕获异常,可以试试将异常捕获,保证线程的安全。

image-20211208223225143

五、线程通信

在上述synchronized的代码案例中,线程获取了锁后,都是一条路走到黑的,除了异常没捕获的那次。

线程通信,主要是线程在获取锁后,主动将锁放弃,让其他线程也来喝喝汤,cpu大哥觉得你很懂事,cpu很欣慰。

1)主要方法

由于synchronized获取的是对象的锁,所以有关线程之间的阻塞唤醒,都来自Object

方法名 功能
public final void wait() 释放当前锁,本线程进入睡眠,从运行状态进入阻塞状态
需要等待其他线程唤醒
public final native void wait(long timeout) 释放当前锁,本线程进入睡眠,从运行状态进入阻塞状态,
一段时间后本线程自动醒来
public final native void notify() 唤醒其他任意一个线程,将它从阻塞状态拉回到就绪状态
public final native void notifyAll() 唤醒其他所有线程,将它们从阻塞状态拉回到就绪状态

image-20211208230101275

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

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;

public class Demo1 {

public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (Demo1.class){
for (int i = 1; i <= 10; i++) {
try {
System.out.println(StrUtil.format("线程A:{},时间:{}", i, DateUtil.now()));
if(i==5){
System.out.println("睡一会,释放锁");
Demo1.class.wait();
}
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();

// 等待3秒
Thread.sleep(5000);

System.out.println("主线程:唤醒");
Demo1.class.notify();
synchronized (Demo1.class){
System.out.println("主线程:唤醒");
Demo1.class.notify();
}
}

}

执行结果,主线程唤醒后,线程A继续走他未走完的路
image-20211208223918311

大家可以放开上述代码的第29,30行,运行后惊奇的发现居然出了异常

image-20211208224223077

这个异常是什么原因呢,在执行wait()、notify()、notifyAll()方法时,必须要持有锁。而且唤醒还一定要持有相同对象的锁,也就是使用synchronized获取同样的对象的锁,并使用该对象进行唤醒其实很好理解,一个持有锁的线程,怎么可能会被没有持有同样锁的线程唤醒呢

3)生产者消费者模式

通过一个中间容器,来设置该容器的最大容量作为生产者线程的结束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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
package com.banmoon.wait;

import cn.hutool.core.util.StrUtil;

import java.util.LinkedList;
import java.util.Queue;

public class Demo2 {

public static void main(String[] args) throws InterruptedException {
Container container = new Container();
new Thread(new Consumer(container)).start();
Thread.sleep(1000);
new Thread(new Producer(container)).start();
}

}

/**
* 生产者
*/
class Producer implements Runnable{
private Container container;

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

@Override
public void run() {
int i = 0;
while (true){
try {
container.put(i);
System.out.println(StrUtil.format("生产者:生产了{},当前数量:{}", i, container.queue.size()));
i++;
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

/**
* 消费者
*/
class Consumer implements Runnable{
private Container container;

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

@Override
public void run() {
while (true){
try {
Integer i = container.get();
System.out.println("消费者:消费了" + i);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

/**
* 容器
*/
class Container{
public Queue<Integer> queue;
public int MAX_SIZE = 10;

public Container() {
this.queue = new LinkedList<>();
}

public synchronized Integer get(){
try {
if(queue.size()==0){
notifyAll();
wait();
}
return this.queue.poll();
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}

public synchronized void put(Integer integer){
try {
if(queue.size()>=MAX_SIZE){
notifyAll();
wait();
}
this.queue.add(integer);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

image-20211209211134749

4)信号灯法

设置一个标志位flag,来控制线程之间的通信状态。简单改造上面的生产消费者,使通过flag来进行通信。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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
package com.banmoon.wait;

import cn.hutool.core.date.DateUtil;

public class Demo3 {

public static void main(String[] args) {
Demo3Flag flag = new Demo3Flag();
new Thread(new Demo3Producer(flag)).start();
new Thread(new Demo3Consumer(flag)).start();
}
}

class Demo3Producer implements Runnable{

private Demo3Flag flag;

public Demo3Producer(Demo3Flag flag) {
this.flag = flag;
}

@Override
public void run() {
for (int i = 0; i < 100; i++){
flag.production();
}
}
}

class Demo3Consumer implements Runnable{

private Demo3Flag flag;

public Demo3Consumer(Demo3Flag flag) {
this.flag = flag;
}

@Override
public void run() {
for (int i = 0; i < 100; i++){
flag.consumption();
}
}
}

class Demo3Flag{

// 标志位 true:生成,false:消费
private boolean flag;

public Demo3Flag() {
this.flag = true;
}

public void setFlag(boolean flag) {
this.flag = flag;
}

public synchronized void production(){
if(!this.flag){
try {
wait();
} catch (InterruptedException interruptedException) {
interruptedException.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ":正在生产..." + DateUtil.now());
// 生产完成,标志位设置为可以消费
this.flag = false;
notifyAll();
}

public synchronized void consumption(){
if(this.flag){
try {
wait();
} catch (InterruptedException interruptedException) {
interruptedException.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ":正在消费..." + DateUtil.now());
// 消费完成,标志位设置为可以继续生产
this.flag = true;
notifyAll();
}
}

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

/**
* 虚假唤醒,问题演示
*/
public class Demo4 {

public static void main(String[] args) {
MyDemo4 myDemo4 = new MyDemo4();
new Thread(() -> {
for(int i = 0; i < 10; i++) myDemo4.increment();
}, "线程A").start();
new Thread(() -> {
for(int i = 0; i < 10; i++) myDemo4.decrement();
}, "线程B").start();
new Thread(() -> {
for(int i = 0; i < 10; i++) myDemo4.increment();
}, "线程C").start();
new Thread(() -> {
for(int i = 0; i < 10; i++) myDemo4.decrement();
}, "线程D").start();

}

}

class MyDemo4{

private int number = 0;

public synchronized void increment(){
try {
if(number==1)
wait();
number++;
System.out.println(Thread.currentThread().getName() + ":" + number);
notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public synchronized void decrement(){
try {
if(number==0)
wait();
number--;
System.out.println(Thread.currentThread().getName() + ":" + number);
notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

}

正常预想的结果,ABCD四个线程使得number在0和1之间反复横跳,但实际上的结果,却大大出乎我所料

image-20211214231422847

这个原因很好解释,因为wait方法有个特性,在摔倒就在哪里爬起来,然后继续向前走。简单的描述下出现问题的步骤,

  1. A线程,number+1

  2. C线程,判断后进行wait

  3. A线程,判断后进行wait

  4. B线程,number-1,唤醒其他线程,此时A,C被唤醒

  5. A线程,number+1

  6. C线程,number+1

  7. 一直持续下去…

好的,到第6步就已经出现问题了,记住上面说的,在哪里摔倒就在哪里爬起来。

第二步时,C线程在判断完成后进入等待,直到第六步被CPU调度,因为判断已经完成,所以直接进入了number+1的逻辑。

像上述这种现象被称为虚假唤醒

解决虚假唤醒

既然是由于被唤醒后没有判断导致,所以我们这里只需要将if改为while,让线程唤醒后的第一件事就是判断条件

修改为while后的执行结果

image-20211214233353115

六、经典笔试题

思考一下如何用3条线程循环输出ABC

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

/**
* 使用线程循环输出ABC
*/
public class Question1 {

public static void main(String[] args) throws InterruptedException {
Object a = new Object();
Object b = new Object();
Object c = new Object();
ABC A = new ABC("A", c, a);
ABC B = new ABC("B", a, b);
ABC C = new ABC("C", b, c);

new Thread(A).start();
Thread.sleep(10);// 保证初始ABC的启动顺序
new Thread(B).start();
Thread.sleep(10);
new Thread(C).start();
Thread.sleep(10);
}

}

class ABC implements Runnable{

private String name;
// 下一个获取的对象
private Object prev;
// 当前对象
private Object self;

public ABC(String name, Object prev, Object self) {
this.name = name;
this.prev = prev;
this.self = self;
}

@Override
public void run() {
while (true){
// 获取上个对象的锁
synchronized (prev) {
// 获取当前对象的锁
synchronized (self) {
System.out.print(name);
// 唤醒当前对象锁,也就是下个线程的上个对象锁
self.notifyAll();
}
try {
Thread.sleep(50);
// 释放上个对象的锁
prev.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

七、最后

多线程使用不是难点,而多线程的锁是面试的常客,synchronized关键字锁住的对象这一知识点必须要掌握。

关于其他什么轻量级锁,总量级锁,读写锁,重入锁等等的概念,后续会出个单章来进行理解。

关于本文出现的代码示例,已提交至码云,只看文章不懂时,一定要敲代码进行理解。

我是半月,祝你幸福!