并发编程

总结自 【狂神说Java】多线程详解

问题
程序、进程、线程的概念
创建线程类的三种方式的具体做法
Runnable 方式的特点
Callable 方式的特点
函数式接口的概念
lambda 表达式的应用场景及写法

基本概念

程序、进程、线程的概念


创建线程

创建线程类主要有三种方式,分别为自定义线程类继承 Thread 类或实现Runnable 接口或实现 Callable 接口。


Thread方式

  1. 自定义线程类继承 Thread 类。
  2. 重写 run() 方法,编写线程执行体。
  3. 创建线程实例对象,调用 start() 方法启动线程。

线程类对象开启的子线程开启后不一定立即执行,主线程与子线程的执行由系统 CPU 调度。

image-20220901181815217

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Thread1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("run方法在执行: " + i);
}
}

public static void main(String[] args) {
Thread1 t = new Thread1();
// 如果执行 t.run(); 则先执行 run 方法。而 start 才是多线程并行执行,即 main 主线程与 run 同时执行。
t.start();
for (int i = 0; i < 20; i++) {
System.out.println("main方法在执行: " + i);
}
}
}

Runnable方式

  1. 自定义线程类实现 Runnable 接口。
  2. 重写 run() 方法,编写线程执行体。
  3. 创建线程实例对象。
  4. 创建 Thread 类实例并传入线程类的实例,再以 Thread 类实例调用 start() 方法启动线程。

在 Thread 类实例构造器中传入线程类实例的方式,内部是 代理模式 (静态代理) 的实现。详细可参考「设计模式」中的「代理模式」。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 通过实现 Runnable 接口来创建线程类
public class Thread3 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("run方法在执行: " + i);
}
}

public static void main(String[] args) {
// Thread3 t3 = new Thread3(); // 创建线程类实例 t3
// Thread t = new Thread(t3); // 创建 Thread 类实例,传入 t3 (静态代理)
// t.start();
new Thread(new Thread3()).start(); // 可以简略为一行

for (int i = 0; i < 20; i++) {
System.out.println("main方法在执行: " + i);
}
}
}

相比 Thread 方式, Runnable 方式能够避免但继承局限性,方便同一个对象 (同一份资源) 被多个线程使用 (多个代理) 。推荐以 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
package com.yukiyama.thread.demo1;

import org.apache.commons.io.FileUtils;

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

// Runnable 方式
public class Thread2 implements Runnable{
private String url;
private String fileName;

public Thread2(String url, String fileName) {
this.url = url;
this.fileName = fileName;
}
// 用于下载图片的线程的执行体
@Override
public void run() {
WebDownloader downloader = new WebDownloader();
downloader.download(url, fileName);
System.out.println("下载了: " + fileName);
}

public static void main(String[] args) {
Thread2 t1 = new Thread2("https://tva1.sinaimg.cn/large/e6c9d24egy1h5r9vjrnvwj20e607oaaj.jpg", "1.jpg");
Thread2 t2 = new Thread2("https://docs.spring.io/spring-framework/docs/5.0.0.RC2/spring-framework-reference/images/spring-overview.png", "2.jpg");
Thread2 t3 = new Thread2("https://tva1.sinaimg.cn/large/e6c9d24egy1h5qfkb8vfej21w80r8dno.jpg", "3.jpg");

new Thread(t1).start();
new Thread(t2).start();
new Thread(t3).start();
}
}
class WebDownloader{
public void download(String url, String fileName) {
try {
FileUtils.copyURLToFile(new URL(url), new File(fileName));
} catch (IOException e) {
e.printStackTrace();
System.out.println("download方法未能正常执行");
}
}
}

Callable方式

  1. 自定义线程类实现 Callable 接口。
  2. 重写 call() 方法,编写线程执行体。与重写 run() 方法的不同点在于 call() 方法需要 抛出异常
  3. 创建线程实例对象。
  4. 创建执行器服务。
  5. 提交执行:。
  6. 获取结果:。
  7. 关闭服务。

该方式相比前两种方式的特点:

  • 重写方法 call()抛出异常且有返回值
  • 线程的执行需要 声明执行器服务 (线程池) 并以 submit() 方法提交。
  • submit() 具有返回值,返回一个 Future<T> 泛型对象,其中的泛型 T 就是 call() 方法的返回类型。通过该对象的 get() 方法可获取到实际的返回结果。
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.yukiyama.thread.demo2;

import org.apache.commons.io.FileUtils;

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

// Callable 方式
public class TestCallable implements Callable<Boolean> {
private String url;
private String fileName;

public TestCallable(String url, String fileName) {
this.url = url;
this.fileName = fileName;
}
// 用于下载图片的线程的执行体
@Override
public Boolean call() throws Exception {
WebDownloader downloader = new WebDownloader();
downloader.download(url, fileName);
System.out.println("下载了: " + fileName);
return true;
}

public static void main(String[] args) throws ExecutionException, InterruptedException {
TestCallable t1 = new TestCallable("https://tva1.sinaimg.cn/large/e6c9d24egy1h5r9vjrnvwj20e607oaaj.jpg", "1.jpg");
TestCallable t2 = new TestCallable("https://docs.spring.io/spring-framework/docs/5.0.0.RC2/spring-framework-reference/images/spring-overview.png", "2.jpg");
TestCallable t3 = new TestCallable("https://tva1.sinaimg.cn/large/e6c9d24egy1h5qfkb8vfej21w80r8dno.jpg", "3.jpg");

// 创建执行器服务
ExecutorService es = Executors.newFixedThreadPool(3);
// 提交
Future<Boolean> r1 = es.submit(t1);
Future<Boolean> r2 = es.submit(t2);
Future<Boolean> r3 = es.submit(t3);
// 获取结果
boolean res1 = r1.get();
boolean res2 = r2.get();
boolean res3 = r3.get();
// 关闭服务
es.shutdown();
}
}
// class WebDownloader{} 实现同前

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class TestLambda {

public static void main(String[] args) {
ILambda lambda = new Lambda1();
lambda.like(); // 外部类实现

lambda = new Lambda2();
lambda.like(); // 内部静态类实现

// 局部内部类
class Lambda3 implements ILambda{
@Override
public void like() {
System.out.println("局部内部类实现");
}
}

lambda = new Lambda3();
lambda.like(); // 局部内部类实现

// 匿名内部类
lambda = new ILambda() {
@Override
public void like() {
System.out.println("匿名内部类实现");
}
};
lambda.like();

// lambda 表达式实现匿名内部类
lambda = () -> System.out.println("lambda表达式实现匿名内部类");
lambda.like();
}

// 静态内部类
private static class Lambda2 implements ILambda{
@Override
public void like() {
System.out.println("内部静态类实现");
}
}
}

interface ILambda{
void like();
}

// 外部类
class Lambda1 implements ILambda{
@Override
public void like() {
System.out.println("外部类实现");
}
}

线程状态

参考1, 参考2, 参考3

操作系统线程状态

操作系统线程状态如下 。

状态 描述
创建状态 New 线程对象被创建后,就进入了新建状态。例如 Thread thread = new Thread()
就绪状态 Runnable 执行 start() 方法后,由 CPU 调度。
运行状态 Running 获得 CPU 时间分片执行该线程时。线程只能从就绪状态进入到运行状态。
阻塞状态 Blocked 等待用户输入,执行 sleep()wait() 方法或同步锁定等,阻塞解除后会重新进入就绪状态
1. 等待阻塞:调用 wait() 方法,则线程等待某工作完成。
2. 同步阻塞:线程获取 synchronized 同步锁失败 (锁被其它线程所占用) 时进入同步阻塞状态。
3. 其他阻塞:调用 sleep()join() 或发出 I/O 请求时也会进入阻塞状态。当 sleep() 状态超时、join() 等待线程终止或者超时、或者I/O 处理完毕时,线程重新转入就绪状态。
死亡状态 Dead 线程执行完了或者因异常退出了run() 方法,该线程结束生命周期。

image-20220901235450790


JVM 中的线程状态

JVM 提供一个线程调度器来调度线程。在 Thread 类中,枚举类型 State 标识线程状态,取值如下。

State 枚举值 描述
NEW 线程创建后还未启动。
RUNNABLE 可运行线程的状态。正在 Java 虚拟机中执行,但可能在等待操作系统的其他资源。
BLOCKED 处于阻塞状态的线程正在等待监控锁,以进入一个同步块/方法,或在调用 Object.wait 后重新进入一个同步块/方法。
TIMED_WAITING 具有指定等待时间的等待状态。例如调用如下方法时。
Thread.sleep
带时间的 Object.wait
带时间的 Thread.join
LockSupport.parkNanos
LockSupport.parkUntil
WAITING 线程正在等待另一个线程执行一个特定的动作。例如,一个在对象上调用了 Object.wait() 的线程正在等待另一个线程对该对象调用Object.notify()Object.notifyAll() 。一个调用了 Thread.join() 的线程正在等待一个指定的线程终止。
调用如下方法会进入该状态。
不带时间的 Object.wait
不带时间的 Thread.join
LockSupport.park
TERMINATED 死亡状态。

阻塞与等待的区别:阻塞由 JVM 调度器来决定唤醒自己,而不需要由另一个线程来显式唤醒自己,不响应中断。等待则需要另一个线程显式地唤醒自己。

image-20220902122233260

操作系统与 JVM 的线程状态对应关系。

操作系统 JVM
NEW NEW
RUNNABLE RUNNABLE
RUNNING RUNNABLE
BLOCKED BLOCKED / WAITING / TIMED_WAITING
DEAD TERMINATED

如下代码可观察部分线程状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TestState {

public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
try {
Thread.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
System.out.println(t.getState()); // NEW

t.start();
System.out.println(t.getState()); // RUNNABLE

while(t.getState() != Thread.State.TERMINATED){
System.out.println(t.getState()); // TIMED_WAITING
}
System.out.println(t.getState()); // TERMINATED
}
}

线程方法

如下是 Thread 类的常用方法。

方法 说明
setPriority(int newPriority) 设置线程优先级。
static void sleep(long millis) 对当前线程设置休眠时间,休眠期间不释放锁。
void join() 等待该线程终止 (可理解为插队) 。
static void yield() 暂停正在执行的线程,但不阻塞,CPU 重新调度
(可理解为礼让,礼让可能不成功,CPU 又调度了礼让的线程)。
void interreput() 中断线程,不推荐使用。
boolean isAlive() 测试线程是否死亡。
getPriority() 获取线程优先级。
setPriority(int newPriority) 设置线程优先级。
setDaemon() 设置线程是否为守护线程。
wait() 线程保持等待直到其他线程通知,与 sleep() 不同,该方法会释放锁。
wait(long timeout) 可指定等待时间。
notify() 唤醒一个处于等待状态的线程。
notifyAll() 唤醒同一个对象上所有调用 wait() 方法的线程,优先级高的线程优先调度。

线程停止

停止线程方法 stop()destroy() 已废弃 (@Deprecated) 。一般做法是通过判断手动设置的标志的值,例如 boolean 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
public class TestStop implements Runnable{
private boolean flag = true;
@Override
public void run() {
int i = 0;
while(flag){
System.out.println("TestStop线程运行中: " + i++);
}
}

public void stopThread(){
this.flag = false;
}

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

for (int i = 0; i < 500; i++) {
System.out.println("主线程运行中: " + i);
if(i == 400) {
t.stopThread();
System.out.println("===========停止线程==========");
}

}
}
}

线程休眠 (sleep)

  1. 静态方法 Thread.sleep(long millis) 可指定当前程序休眠时间,即使其进入阻塞状态。可用于模拟网络时延或倒计时等。
  2. sleep 方法需要抛出 InterruptedException 异常。
  3. 达到指定时间后线程进入就绪状态。
  4. 每个对象都有一把锁,sleep 不会释放锁。即当前线程持有对某个对象的锁,则即使调用 sleep 方法,其他线程也无法访问这个对象。

线程礼让 (yield)

  • 暂停当前执行的线程,但不进入阻塞状态,而是 进入就绪状态
  • CPU 重新调度,可能执行其他线程,也可能又调度到礼让的线程。

下面是观察 yield() 方法效果的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TestYield implements Runnable{
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println(name + "线程开始");
Thread.yield(); // 礼让,可注释此行观察效果
System.out.println(name + "线程结束");
}
public static void main(String[] args) {
TestYield t = new TestYield();
new Thread(t, "A").start();
new Thread(t, "B").start();
}
}

线程强制执行(join)

thread.join() 使 thread 线程抢占 CPU,其他线程处于阻塞状态,等待 thread 线程完成执行后再执行其他线程。类似插队。

下面是观察 join() 方法效果的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TestJoin extends Thread{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("插队线程执行中: " + i);
}
}

public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new TestJoin());
t.start();

for (int i = 0; i < 500; i++) {
if(i == 200) t.join();
System.out.println("主线程执行中: " + i);
}
}
}

线程优先级

  • JVM 线程调度器根据线程优先级决定线程的执行顺序。线程优先级类型为 int ,范围为 1~10 ,数字越大越优先。

  • 优先级高的线程被优先调度的概率高,但不保证一定比优先级低的线程先调度。

  • Thread 类提供了三个优先级常量:Thread.MIN_PRIORITY = 1, Thread.MAX_PRIORITY = 10, Thread.NORM_PRIORITY = 5

  • 使用 getPriority() 方法获取线程优先级,使用 setPriority(int newPriority) 来设置优先级。

  • 优先级的设置通常在 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
public class TestPriority{
public static void main(String[] args) {
System.out.println("主线程优先级为: " + Thread.currentThread().getPriority()); // 主线程优先级默认为 5
MyPriority p = new MyPriority();

Thread t1 = new Thread(p);
Thread t2 = new Thread(p);
Thread t3 = new Thread(p);
Thread t4 = new Thread(p);

t1.start(); // 未设置情况下默认为 5

t2.setPriority(Thread.MIN_PRIORITY); // 1
t2.start();

t3.setPriority(6);
t3.start();

t4.setPriority(Thread.MAX_PRIORITY); // 10
t4.start();
}
}
class MyPriority implements Runnable{
@Override
public void run() {
Thread cur = Thread.currentThread();
System.out.println(cur.getName() + "的优先级为:" + cur.getPriority());
}
}

守护线程

参考1, Why are daemons called daemons?

  • 线程分为 用户线程 (User Thread)守护线程 (Daemon Thread)
  • 守护线程指的是低优先级的为用户线程提供服务的后台线程。
  • 当所有用户线程结束后,JVM 会终止守护线程。
  • 常见的守护线程有记录操作日志、监控内存、垃圾回收等线程。
  • 使用 setDaemon(boolean flag) 来设置线程为是否为守护线程。使用 isDaemon()isDaemon() 来判断线程是否为守护线程。

如下代码演示如何将线程设置为守护线程,并观察用户线程结束后,守护线程也会被强制终止。

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
public class TestDaemon {
public static void main(String[] args) {
Thread user = new Thread(new UserThread());
Thread daemon = new Thread(new DaemonThread());
daemon.setDaemon(true);
daemon.start(); // 守护线程 run 方法内为死循环,但用户线程结束后它也会被终止
System.out.println("daemon 线程是守护线程:" + daemon.isDaemon());
user.start();
}
}

class UserThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("用户线程运行中");
}
System.out.println("========用户线程运行结束========");
}
}

class DaemonThread implements Runnable{
@Override
public void run() {
while(true) {
System.out.println("守护线程运行中");
}
}
}

线程安全

参考1

线程安全 是指有多个线程访问同一资源时,线程间依照某种方式使得访问结果总是正确的。相对地,若由于多线程竞争导致访问结果错误,则称为 线程不安全

下图为 Java 内存模型简图,描述了多线程执行的场景。线程A和线程B分别读写 主内存 中的 共享变量。每个线程都有各自的 工作内存。线程读写共享变量时,先将该变量拷贝到自身工作内存,对其操作后再将结果同步回主内存中。

image-20220902185227136

上述工作方式可能导致线程不安全。例如共享变量为数字10,线程A和线程B执行的操作是将该数字变量减1,当它们同时获取到共享变量时,均将10拷贝到自身线程工作内存中,执行减1操作后,又分别将9写回到主内存中。而正确的结果应该为8。

以下给出三个线程不安全的例子。

线程不安全例1: 模拟抢票

根据前面的叙述,本例三个线程共享一个 TicketService 实例中的类变量 (共享变量),可能导致某几人抢票后剩余票数一样或剩余负数张票的错误情形。

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
// 线程不安全:模拟抢票
public class UnsafeTicket {
public static void main(String[] args) {
TicketService service = new TicketService();
new Thread(service, "小白").start();
new Thread(service, "小黑").start();
new Thread(service, "小红").start();
}
}

class TicketService implements Runnable{
private int ticketsNum = 1000;
private boolean available = true;
@Override
public void run(){
while(available) buy();
}
private void buy(){
if(ticketsNum <= 0) available = false;
else{
try {
Thread.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "购票,库存剩余" + --ticketsNum + "张票");
}
}
}
1
2
3
4
5
6
7
8
9
10
// 某次执行的结果
...(部分输出略)
小白购票,库存剩余4张票
小黑购票,库存剩余4张票
小红购票,库存剩余3张票
小白购票,库存剩余2张票
小黑购票,库存剩余1张票
小红购票,库存剩余0张票
小白购票,库存剩余-1张票
小黑购票,库存剩余-2张票

线程不安全例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
// 线程不安全:模拟取款
public class TestWithdrawal {
public static void main(String[] args) {
Account account = new Account(100, "小白的账户");
System.out.println(account.name + "当前余额为" + account.money);
Withdrawal w1 = new Withdrawal(account, 50);
Withdrawal w2 = new Withdrawal(account, 100);
new Thread(w1, "小白").start();
new Thread(w2, "小黑").start();
}
}
class Account {
int money;
String name;
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
class Withdrawal implements Runnable{
private Account account;
private int withDrawingMoney;
private int nowMoney;

public Withdrawal(Account account, int withDrawingMoney) {
this.account = account;
this.withDrawingMoney = withDrawingMoney;
}

@Override
public void run() {
String user = Thread.currentThread().getName();
if(account.money - withDrawingMoney < 0){
System.out.println(user + "想取: " + withDrawingMoney + ", " + account.name + "余额不足");
} else {
System.out.println(user + "取走: " + withDrawingMoney);
account.money -= withDrawingMoney;
nowMoney += withDrawingMoney;
}
System.out.println(account.name + "余额为: " + account.money);
System.out.println(Thread.currentThread().getName() + "手里的钱为: " + nowMoney);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
1
2
3
4
5
6
7
8
// 某次输出的结果
小白的账户当前余额为100
小白取走: 50
小黑取走: 100
小白的账户余额为: -50
小白的账户余额为: 50
小黑手里的钱为: 100
小白手里的钱为: 50

线程不安全例3: 线程不安全的集合

下例输出的 list 大小可能不足 100。

1
2
3
4
5
6
7
8
9
10
11
12
// 线程不安全的集合
public class UnsafeList {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 100; i++) {
new Thread(() -> {
list.add(Thread.currentThread().getName());
}).start();
}
System.out.println(list.size());
}
}

线程同步

「线程同步」 是解决**「线程不安全」**问题的一种方法。

「同步」一词在生活中多指「同时」、「随即」之意,例如新闻联播在中央台播放,其他省级频道也同步播出。但在计算机领域的「同步」隐含着 非同时 的意味,例如本节要叙述的线程同步,指两个线程针对同一内存数据,其中一个线程完成该数据的操作之后,另一个线程在前一个线程操作的结果之上操作该数据,以保证数据的一致性。多个线程「串行」处理同一数据,一旦某个线程正在处理,其他线程就必须等待 (阻塞状态) 。


synchronized

Java 中可以通过 synchronized 关键字来实现同步。synchronized 关键字有两种用法:同步方法同步块

同步方式 描述
synchronized 方法 (同步方法) public synchronized void method() {}
调用该方法的对象即为锁 (关键字 this),同步方法执行后即独占该锁,完成执行后释放。
synchronized 块 (同步块) synchronized (obj) {}
obj 称作「同步监视器」,即为锁,可以是任何对象,通常以多线程竞争的共享资源充当。

对于「线程安全」中的三个例子,按如下方法修改后变为同步安全。

例1: buy 方法加上 synchronized 关键字即可。

例2: run 中如下涉及 account 修改的内容加入到 synchronized 块即可。

1
2
3
4
5
6
7
8
9
synchronized (account){
if(account.money - withDrawingMoney < 0){
System.out.println(user + "想取: " + withDrawingMoney + ", " + account.name + "余额不足");
} else {
System.out.println(user + "取走: " + withDrawingMoney);
account.money -= withDrawingMoney;
nowMoney += withDrawingMoney;
}
}

例3: for 中内容 加入到 synchronized 块即可。

1
2
3
4
5
6
7
for (int i = 0; i < 100; i++) {
synchronized (list){ // 加入同步块后变为线程安全
new Thread(() -> {
list.add(Thread.currentThread().getName());
}).start();
}
}

死锁

当线程 A 持有锁 X,线程 B 持有锁 Y,此时线程 A 在不放弃锁 X 的情况下想要获取锁 Y,同时线程 B 也想在不放弃锁 Y 的情况下获取锁 X,则线程 A, B 处于「死锁」状态。若无外界干涉,则此情况将永久僵持下去,其他等待 X 或 Y 的线程将永远处于阻塞状态。

如下代码是死锁的一个例子。

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
public class DeadLock {
public static void main(String[] args) {
new Thread(new Trade(0), "买家").start();
new Thread(new Trade(1), "卖家").start();
}
}
class Trade implements Runnable{
private static Money money = new Money();
private static Product product = new Product();
private int choice;

public Trade(int choice) {
this.choice = choice;
}

@Override
public void run() {
try {
trade();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public void trade() throws InterruptedException {
if(choice == 0){
synchronized (money){
System.out.println(Thread.currentThread().getName() + "持有钱");
Thread.sleep(1000);
synchronized (product){
System.out.println(Thread.currentThread().getName() + "持有产品");
}
}
} else {
synchronized (product){
System.out.println(Thread.currentThread().getName() + "持有产品");
Thread.sleep(1000);
synchronized (money){
System.out.println(Thread.currentThread().getName() + "持有钱");
}
}
}
}
}
class Money {}
class Product {}

上述死锁的解决办法是将内部的 synchronized 同步块提到外面,如下。

1
2
3
4
5
6
7
synchronized (money){
System.out.println(Thread.currentThread().getName() + "持有钱");
Thread.sleep(1000);
}
synchronized (product){
System.out.println(Thread.currentThread().getName() + "持有产品");
}

Lock

从 JDK 5.0 开始 Java 提供了更强大的线程同步机制,通过显式定义同步锁对象 java.util.concurrent.locks.Lock 来实现同步。Lock 是接口,通常使用其实现类 ReentrantLock (可重入锁) 及其相关方式来界定同步块。

如下是 ReentrantLock 的用法的实例。此例子类似「线程安全」中的模拟购票,三个线程同时消费 10 个资源,若不对 资源加锁,可能出现消费同一个资源的情况。

  • 在多线程类 User 中定义可重入锁。
  • 在需要加锁的区域前后分别执行 lock()unlock() 方法。通常同步块写到 try{} 内,而 unlock() 方法写到对应的 finally{} 中。
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
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TestReentrantLock {
public static void main(String[] args) {
User c = new User();
new Thread(c, "消费者A").start();
new Thread(c, "消费者B").start();
new Thread(c, "消费者C").start();
}
}
class User implements Runnable{
private int resource = 10;
private final Lock lock = new ReentrantLock();
@Override
public void run() {
try {
lock.lock();
while(resource > 0) {
System.out.println(Thread.currentThread().getName() + "消费后还剩" + --resource);
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} finally {
lock.unlock();
}
}
}

synchronized 关键字与 Lock 的比较。

  • synchronized 为隐式锁,出作用域时自动释放锁。Lock为显式锁,需手动开启和关闭 (lock(), unlock()) 。
  • synchronized有方法锁和代码块锁,Lock只有代码块锁。
  • 使用 Lock 时 JVM 调度效率更高,性能更好。
  • Lock 提供不同的实现类,具有更好的扩展性。

生产者消费者模式

参考1

同步锁在线程同步机制中可以保证并发更新同一资源时不出错,但未实现线程间通信,例如线程 A 使用完共享资源后,若能主动通知线程 B,则线程 B 只需要等待通知而无需不断尝试获取。生产者消费者模式 就是描述 线程间通信 或者说实现 并发协作 的模式,它所解决的问题也称作 有限缓冲问题

生产者和消费者之间共享同一个资源,二者互相依赖。

  • 对于生产者,不被允许生产时需等待,否则生产之后通知消费者消费。
  • 对于消费者,不被允许消费时需等待,否则消费之后通知生产者生产。

Java 中通过 wait(), notify(), notifyAll() 方法来实现线程间通信。

  • 这些方法均为 Object 类的方法。
  • 只能在同步方法或同步代码块中使用。否则抛出 IllegalMonitorStateException 异常。
方法 描述
wait() 线程保持等待直到其他线程通知,与 sleep() 不同,该方法会释放锁。
wait(long timeout) 可指定等待时间。
notify() 唤醒某一个正在等待当前锁的线程,具体唤醒哪一个是不确定的。
notifyAll() 唤醒正在等待当前锁的所有线程,优先级高的线程优先调度。

生产者消费这模式通常有两种实现方式。

  • 缓冲区法: 设置一个缓冲区,即产品容器。生产者生产的产品放入该容器,当容器满时,等待消费者消费。消费者从容器中消费产品,当容器空时等待生产者生产。
  • 信号量法: 设置一个 boolean 信号,当其为 false 时,生产者等待消费者消费,为 true 时生产产品,并将信号置为 false ,通知消费者消费。当信号为 true 时,消费者等待生产者生产,为 false 时消费产品,并将信号量置为 true,通知生产者生产。

如下为「缓冲区法」示例。

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
public class TestProducerConsumer {
public static void main(String[] args) {
Container container = new Container(10);
new Thread(new Producer(container)).start();
new Thread(new Consumer(container)).start();
}
}
class Producer implements Runnable{
private Container container;
public Producer(Container container){
this.container = container;
}
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println("生产编号为 " + i + " 的手机");
container.push(new Phone(i));
}
}
}
class Consumer implements Runnable{
private Container container;
public Consumer(Container container){
this.container = container;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("消费编号为" + container.pop().getId() + "的手机" );
}
}
}
class Phone {
private int id;
public Phone(int id){
this.id = id;
}
public int getId(){
return id;
}
}
class Container {
private Phone[] phones;
private int count = 0;
public Container(int size){
this.phones = new Phone[size];
}
public synchronized void push(Phone phone){
if(count == phones.length){
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
phones[count] = phone;
count++;
this.notifyAll();
}
public synchronized Phone pop(){
if(count == 0){
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
count--;
Phone phone = phones[count];
this.notifyAll();
return phone;
}
}

如下为「信号量法」示例。

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
public class TestSenderReceiver {
public static void main(String[] args) {
Data data = new Data();
new Thread(new Sender(data)).start();
new Thread(new Receiver(data)).start();
}
}
class Sender implements Runnable{
Data data;
public Sender(Data data){
this.data = data;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
data.send("packet" + i);
}
}
}
class Receiver implements Runnable{
Data data;
public Receiver(Data data){
this.data = data;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
data.receive();
}
}
}
class Data{
private String packet;
private boolean transfer = true; // true: receiver should wait
public synchronized void send(String packet){
while(!transfer){
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("发送了数据包" + packet);
this.packet = packet;
transfer = false;
this.notifyAll();
}
public synchronized String receive(){
while(transfer){
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("接收了数据包" + packet);
String returnPacket = this.packet;
transfer = true;
this.notifyAll();
return returnPacket;
}
}

线程池

本节总结自 4种Java线程池用法以及线程池的作用和优点,你都知道了没?


ThreadLocal