Java 多线程基础(二)

简介

在上篇 Java 多线程基础(一) 我们提到了一些线程的常用方法,这篇我们具体看看其中一些方法的使用以及方法的区别,让我们在工作中更好的使用。

wait 方法与 notify 方法

Object 类中定义了 wait 方法和 notify 方法,wait 方法的作用是让当前线程进入等待状态,将当前线程置入 预执行队列,会在 wait 方法所在代码处停止执行,直到被通知或者被中断,在调用 wait 方法之前,线程必须获取该对象的锁,因此只能在同步方法或者同步代码块中调用 wait 方法,并且该方法会释放当前线程锁持有的锁。notify 方法是唤醒在当前对象上等待的单个线程,如果有多个线程等待,那么线程调度器会挑出一个 wait 的线程,对其发出 notify ,并使它等待获取该对象的对象锁,这意味着,即使收到了通知,线程也不会立即获取到对象锁,必须等待 notify 方法的线程释放锁才可以。和 wait 方法一样,notify 方法也只能在同步方法或者同步代码块中调用。它还有个相似的方法 notifyAll,它的作用是唤醒在当前对象上等待的所有线程

下面通过一个生产者消费者来说明 wait 方法和 notify 方法的使用:

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
/**
* @author mghio
* @date: 2019-12-14
* @version: 1.0
* @description: 线程 wait() 和 notify() 方法使用示例
* @since JDK 1.8
*/
public class ThreadWaitAndNotifyDemo {

public static void main(String[] args) {
Producer producer = new Producer();
producer.start();
new Consumer("Consumer One", producer).start();
new Consumer("Consumer Two", producer).start();
new Consumer("Consumer Three", producer).start();
new Consumer("Consumer Four", producer).start();
}

static class Producer extends Thread {

List<String> messageList = new ArrayList<>(2);

@Override
public void run() {
try {
while (true) {
Thread.sleep(2000);
synchronized (messageList) {
String message = String.format("producer message [create time:%s]", LocalDateTime.now());
messageList.add(message);
System.out.println("Producer " + getName() + " producer a msg: " + message);
messageList.notify();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}

String getMessage() {
synchronized (messageList) {
if (messageList.size() == 0) {
try {
messageList.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return messageList.remove(0);
}
}
}

static class Consumer extends Thread {

private Producer producer;

public Consumer(String name, Producer producer) {
super(name);
this.producer = producer;
}

@Override
public void run() {
while (true) {
String message = producer.getMessage();
System.out.println("Consumer " + getName() + " get a msg: " + message);
}
}
}
}

输出结果如下:

1
2
3
4
5
6
7
8
Producer Thread-0 producer a msg: producer message [create time:2019-12-14T22:45:42.319]
Consumer Consumer One get a msg: producer message [create time:2019-12-14T22:45:42.319]
Producer Thread-0 producer a msg: producer message [create time:2019-12-14T22:45:44.324]
Consumer Consumer Two get a msg: producer message [create time:2019-12-14T22:45:44.324]
Producer Thread-0 producer a msg: producer message [create time:2019-12-14T22:45:46.325]
Consumer Consumer Three get a msg: producer message [create time:2019-12-14T22:45:46.325]
Producer Thread-0 producer a msg: producer message [create time:2019-12-14T22:45:48.328]
Consumer Consumer Four get a msg: producer message [create time:2019-12-14T22:45:48.328]

消费者线程循环调用生产者的 getMessage 方法获取消息,如果消息列表 messageList 为空,则调用消息列表的 wait 方法让线程进入等待状态,生产者每隔 2 秒生成消息并放入消息列表 messageList 中,放入成功后调用 notify 方法唤醒一个处于 wait 状态的线程去消费消息,需要注意的是,在调用 waitnotify 方法时必须要先获得该对象的锁,上面的示例中是在 synchronized 代码块中调用的。

sleep 方法

waitnotify 方法不同,sleep 方法定义在 Thread 类中,从方法名也可以知道,这个方法的作用就是让当前线程休眠,即调用该方法后当前线程会从运行状态(Running)状态进入到阻塞(休眠)状态(Blocked),同时该方法必须指定休眠的时间,当前线程的休眠时间会大于或者等于这个指定的休眠时间。当线程重新被唤醒时,线程会由阻塞状态(Blocked)变成就绪状态(Runnable),然后等待 CPU 的调度执行。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
/**
* @author mghio
* @date: 2019-12-14
* @version: 1.0
* @description: 线程 sleep() 方法使用示例
* @since JDK 1.8
*/
public class ThreadSleepDemo {

private static Object object = new Object();

public static void main(String[] args) {
MyThread myThreadOne = new MyThread("t1");
MyThread myThreadTwo = new MyThread("t2");
myThreadOne.start();
myThreadTwo.start();
}

static class MyThread extends Thread {

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

@Override
public void run() {
synchronized (object) {
try {
for (int i = 0; i < 5; i++) {
System.out.println(String.format("%s: %d", this.getName(), i));
if (i % 2 == 0) {
Thread.sleep(2000);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

输出结果如下:

1
2
3
4
5
6
7
8
9
10
t1: 0
t1: 1
t1: 2
t1: 3
t1: 4
t2: 0
t2: 1
t2: 2
t2: 3
t2: 4

我们启动了两个线程 t1t2,两个线程的 run 方法引用了同一个对象 object 的同步锁(synchronized (object)),虽然在第一个线程 t1 中当 i 被 2 整除时会调用 Thread.sleep(2000) 让当前线程休眠 2 s,但是此时线程 t2 也不会得到 cpu 的执行权去执行,因为 t1 线程调用 sleep 方法并没有释放object所持有的同步锁。如果我们注释掉 synchronized (object) 后再次执行该程序,线程 t1t2 是可以交替执行的,注释之后的输出结果如下:

1
2
3
4
5
6
7
8
9
10
t2: 0
t1: 0
t1: 1
t2: 1
t1: 2
t2: 2
t2: 3
t1: 3
t2: 4
t1: 4

yield 方法

yield 方法定义在 Thread 类中,是线程特有的方法。此方法的主要作用是让步,它会使当前线程从运行状态(Running)变为就绪状态(Runnable),从而让其他具有同样优先级的处于就绪状态的线程获取到 CPU 执行权(PS: CPU 会从众多的处于就绪状态的线程里选择,也就是说,当前也就是刚刚的那个线程还是有可能会被再次执行到的,并不是说一定会执行其他线程而该线程在下一次中不会执行到),但是,也并不能保证在当前线程调用 yield 之后,其它哪些具有相同优先级的线程就一定能获得执行权,也有可能是当前线程又进入到运行状态(Running)继续运行。yield 方法的示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* @author mghio
* @date: 2019-12-14
* @version: 1.0
* @description: 线程 yield() 方法使用示例
* @since JDK 1.8
*/
public class ThreadYieldDemo {

public static void main(String[] args) {
MyThread myThreadOne = new MyThread("t1");
MyThread myThreadTwo = new MyThread("t2");
myThreadOne.start();
myThreadTwo.start();
}

static class MyThread extends Thread {

MyThread(String name) {
super(name);
}

@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(String.format("%s [%d] ---> %d", this.getName(), this.getPriority(), i));
if (i % 2 == 0) {
yield();
}
}
}
}
}

输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
t1 [5] ---> 0
t2 [5] ---> 0
t1 [5] ---> 1
t1 [5] ---> 2
t1 [5] ---> 3
t1 [5] ---> 4
t1 [5] ---> 5
t1 [5] ---> 6
t1 [5] ---> 7
t1 [5] ---> 8
t1 [5] ---> 9
t2 [5] ---> 1
t2 [5] ---> 2
t2 [5] ---> 3
t2 [5] ---> 4
t2 [5] ---> 5
t2 [5] ---> 6
t2 [5] ---> 7
t2 [5] ---> 8
t2 [5] ---> 9

从以上输出结果可以看出,线程 t1 中的变量 i 在被 2 整除的时候,并没有切换到线程 t2 去执行,这也验证了我们上文说的,yield 方法虽然可以让线程由运行状态变成就绪状态,但是,它不一定会让其它线程获取 CPU 执行权从而进入到运行状态,即使这个其它线程和当前具有相同的优先级,yield 方法不会释放锁(证明方法只需将上面这个示例的 run 方法里面加上 synchronized (obj) 即可,此时 t2 线程会等到线程 t1 执行完毕后才会执行)。

join 方法

在有些场景中我们需要在子线程去执行一些耗时的任务,但是我们的主线程又必须等待子线程执行完毕之后才能结束,那么此时就可以使用 join 方法了,该方法定义在 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
38
39
/**
* @author mghio
* @date: 2019-12-15
* @version: 1.0
* @description: 线程 join() 方法使用示例
* @since JDK 1.8
*/
public class ThreadJoinDemo {

public static void main(String[] args) {
try {
MyThread myThread = new MyThread("t1");
myThread.start();
myThread.join();
System.out.println(String.format("%s ---> %s finish", LocalDateTime.now(), Thread.currentThread().getName()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}

static class MyThread extends Thread {

MyThread(String name) {
super(name);
}

@Override
public void run() {
System.out.println(String.format("%s ---> %s start", LocalDateTime.now(), this.getName()));
// 模拟耗时操作
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(String.format("%s ---> %s finish", LocalDateTime.now(), this.getName()));
}
}
}

输出结果如下:

1
2
3
2019-12-15T00:22:55.971 ---> t1 start
2019-12-15T00:22:57.984 ---> t1 finish
2019-12-15T00:22:57.985 ---> main finish

在主线程 main 中通过 new MyThread("t1") 新建线程 t1。 接着,通过 t1.start() 启动线程 t1,在执行 t1.join()之后, 主线程会进入阻塞状态等待 t1 运行结束。子线程 t1 结束之后,会唤醒主线程,主线程重新获取 CPU 执行权,主线程继续往下运行。在使用了 join 方法之后主线程会等待子线程结束之后才会结束。

总结

以上是线程一些常用的方法介绍和具体使用知识总结。

-------------本文结束感谢您的阅读-------------
mghio wechat
微信公众号「mghio」
赏作者☕️