农场主的黑科技.

并发操作合集-2.状态转换方法:wait,notify,sleep,join,yield

字数统计: 3.1k阅读时长: 19 min
2018/11/01 Share

🍤 并发操作合集系列 目录

🍕 并发操作合集系列 源代码

这篇文章中会详细介绍那些切换线程状态的常用方法(wait,notify,sleep,join,yield)。

线程的状态和常用方法

还是这张图。本章中介绍的方法与线程状态的关系如下图所示

1541035245337

wait() 和 notify() 方法

首先,这两个方法是做什么的:

  • Object.wait() – 挂起一个线程
  • Object.notify() – 唤醒一个线程

wait()使当前线程阻塞,前提是 必须先获得锁,所以通常在synchronized 同步代码块里使用 wait(),notify/notifyAll() 方法。

当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。此时的等待状态和sleep造成的等待状态稍有不同,下一章会对此进行解释。

当 notify/notifyAll() 被执行时候,才会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized 代码块的代码。如果中途遇到wait() ,将再次释放锁。

也就是说,notify/notifyAll() 的执行只是唤醒沉睡的线程,而不会立即释放锁,此时被唤醒的等待线状态的线程将会变为阻塞状态(BLOCKED)位于对象的等待锁定池,直到notify释放锁并获取该锁。所以在编程中,尽量在使用了notify/notifyAll() 后立即退出临界区,以唤醒其他线程 。

另外wait() 需要被try catch包围,中断也可以使wait等待的线程唤醒。

notify 和 notifyAll的区别 : notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。notifyAll 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll 方法。

示例:发送者 - 接收者同步问题

假如有这样的需求:

  • 发送者会向接收者发送数据包
  • 在发送者完成发送之前,接收者无法处理数据包
  • 同样,发送者不得尝试发送另一个数据包,直到接收者已处理完上一个数据包

创建一个Data类来实现这些数据包的操作

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
public class Data {
private String packet;

// True if receiver should wait
// False if sender should wait
private boolean transfer = true;

public synchronized void send(String packet) {
while (!transfer) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Log.error("Thread interrupted", e);
}
}
transfer = false;

this.packet = packet;
notifyAll();
}

public synchronized String receive() {
while (transfer) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Log.error("Thread interrupted", e);
}
}
transfer = true;

notifyAll();
return packet;
}
}

那么为什么要用while而不是if包住wait()?

因为这里的notifyAll()唤醒的是所有处于等待状态的线程,也就是说被唤醒的线程可能并不符合条件判断,所以应该用while循环再次进行判断,如果不满足将继续调用wait。

下面是Sender类和Receiver类,它们将对Data类进行同步操作:

Sender:

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
public class Sender implements Runnable {
private Data data;

// standard constructors

public void run() {
String packets[] = {
"First packet",
"Second packet",
"Third packet",
"Fourth packet",
"End"
};

for (String packet : packets) {
data.send(packet);

// Thread.sleep() to mimic heavy server-side processing
try {
Thread.sleep(ThreadLocalRandom.current().nextInt(1000, 5000));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Log.error("Thread interrupted", e);
}
}
}
}

Receiver:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Receiver implements Runnable {
private Data load;

// standard constructors

public void run() {
for(String receivedMessage = load.receive();
!"End".equals(receivedMessage);
receivedMessage = load.receive()) {

System.out.println(receivedMessage);

// ...
try {
Thread.sleep(ThreadLocalRandom.current().nextInt(1000, 5000));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Log.error("Thread interrupted", e);
}
}
}
}

写一个客户端程序来启动Sender和Receiver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {
Data data = new Data();
Thread sender = new Thread(new Sender(data));
Thread receiver = new Thread(new Receiver(data));

sender.start();
receiver.start();
}
-------------------------------------------------
输出:
First packet
Second packet
Third packet
Fourth packet

wait()和sleep()方法

我们首先要知道sleep()方法是属于Thread类中的。而wait()方法,则是属于Object类中的。

sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态。也就是说,在调用sleep()方法的过程中,线程不会释放对象锁。

而当调用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
public class Test {

public static void main(String[] args) throws InterruptedException {

//测试Wait会释放锁
new Thread(new WaitTread()).start();
Thread.sleep(1000);
new Thread(new SleepThread()).start();

//测试sleep不会释放锁
// new Thread(new SleepThread()).start();
// Thread.sleep(1000);
// new Thread(new WaitTread()).start();

}
}

class WaitTread implements Runnable {

@Override
public void run() {
synchronized (Test.class) {
System.out.println("进入WaitTread 当前时间 : " + LocalTime.now());
//调用wait()方法,线程会放弃对象锁,进入等待此对象的等待锁定池
try {
Test.class.wait(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("WaitTread结束了 当前时间 : " + LocalTime.now());
}
}
}

class SleepThread implements Runnable {

@Override
public void run() {
synchronized (Test.class) {
System.out.println("进入SleepThread 当前时间 : " + LocalTime.now());


//在调用sleep()方法的过程中,线程不会释放对象锁。
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("SleepThread结束了 当前时间 : " + LocalTime.now());
}
}
}

上面的例子中,sleep和wait会分别暂停5秒,线程启动之间间隔1秒。

修改WaitTread和SleepThread的启动顺序,输出结果会截然不同:

1
2
3
4
5
6
7
8
9
10
//测试sleep不会释放锁
new Thread(new SleepThread()).start();
Thread.sleep(1000);
new Thread(new WaitTread()).start();
-------------------------------------------------
输出
进入SleepThread 当前时间 : 21:59:49.747
SleepThread结束了 当前时间 : 21:59:54.747
进入WaitTread 当前时间 : 21:59:54.748
WaitTread结束了 当前时间 : 21:59:59.748

很显然,由于sleep不释放锁,所以直到sleep线程执行结束后wait线程才获取到锁。wait线程在被启动的4秒后才开始执行。

如果把调用顺序反过来会如何:

1
2
3
4
5
6
7
8
9
10
//测试wait会释放锁
new Thread(new WaitTread()).start();
Thread.sleep(1000);
new Thread(new SleepThread()).start();
-------------------------------------------------
输出
进入WaitTread 当前时间 : 21:53:56.563
进入SleepThread 当前时间 : 21:53:57.492
SleepThread结束了 当前时间 : 21:54:02.492
WaitTread结束了 当前时间 : 21:54:02.493

在WaitTread调用wait()之后SleepThread获取到了锁,说明wait过程中会释放锁。值得注意的是,WaitTread从开始到结束花了整整6秒时间,而我们设置的wait的时限是5秒,是和SleepThread几乎同时结束的。原因也很简单,wait经过5秒后唤醒,此时该线程处于等待锁定池,而此时的锁还在sleep线程手中,因此直到sleep线程执行完毕释放锁之后,wait线程才能继续执行。

join()方法

join()方法的作用

thread.join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。

它的重载:

  • t.join() : 调用join方法,等待线程t执行完毕
  • t.join(1000) : 等待 t 线程,等待时间是1000毫秒。

使用join时需要注意的几点

调用join时需要注意以下几点:

  • 只能用于已启动的线程
    如果线程被生成了,但还未被起动,调用它的 join() 方法是没有作用的,将直接继续向下执行。在源码中用isAlive()来进行判断。
  • join是基于wait实现的
    通过wait(timeout)来让当前线程变为等待状态,这里的timeout是目标线程的执行时间。从而实现让当前线程成为等待状态,直到目标线程执行完毕。
  • 调用join时,必须先获取目标线程的锁
    既然基于wait,那么join方法肯定是在synchronized中,这是因为wait必须是在有锁的地方才能使用。从源码上来看也确实如此。也就是说在调用join时我们必须获得目标线程t的锁,否则会一直被阻塞。

详细可以参考JDK源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;

if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}

if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}

示例代码

为了理解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
28
29
30
31
32
33
34
35
36
public class TestJoin implements Runnable {

public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new TestJoin());
long start = System.currentTimeMillis();
t.start();
Thread.sleep(1000); //为了让线程调度器能调度到t
t.join(2000);
System.out.println("t.join()调用结束,耗时(ms):" + (System.currentTimeMillis() - start));
System.out.println(" Main 结束了");
}

@Override
public void run() {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("sleeping" + i);
}

System.out.println("TestJoin 结束了");
}
}
-----------------------------------------------------
输出
sleeping0
sleeping1
t.join()调用结束,耗时(ms):3000
Main 结束了
sleeping2
sleeping3
sleeping4
TestJoin 结束了

可以看出mian线程在调用join()后变为等待状态,让给线程两秒后继续执行main线程。

如果不启动线程呢?把start()注释掉

1
2
3
4
5
6
7
//        t.start();
Thread.sleep(1000);
t.join(2000);
-------------------------------------------------------
输出
t.join()调用结束,耗时(ms):1000
Main 结束了

果然join是不起作用的。

我们再通过synchronized块让Main线程获取不到锁,从而测试”调用join时,必须先获取目标线程的锁”。

把run()改成以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    @Override
public void run() {
synchronized (Thread.currentThread()) {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("sleeping" + i);
}
System.out.println("TestJoin 结束了");
}
}
-------------------------------------------------------
输出
sleeping0
sleeping1
sleeping2
sleeping3
sleeping4
TestJoin 结束了
t.join()调用结束,耗时(ms):5002
Main 结束了

main线程因为获取不到锁,所以一直处于阻塞状态,直到线程t执行完毕。

yield()方法

yield()的作用

yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。

但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。所以,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。

但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。yield()的使用场景很少,如果不是有特别的需求,尽量避免使用。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ThreadYield {
public static void main(String[] args) {
Runnable r = () -> {
int counter = 0;
while (counter < 2) {
System.out.println(Thread.currentThread().getName());
counter++;
Thread.yield();
}
};

new Thread(r).start();
new Thread(r).start();
}
}

运行结果可能是

1
2
3
4
Thread-0
Thread-1
Thread-1
Thread-0

也可能是

1
2
3
4
Thread-0
Thread-0
Thread-1
Thread-1

Reference

Java多线程学习之wait、notify/notifyAll 详解

wait and notify() Methods in Java

java中的sleep()和wait()的区别

Difference Between Wait and Sleep in Java

Java中join()方法的理解

java中yield()方法如何使用

Brief Introduction to Java Thread.yield()

CATALOG
  1. 1. 线程的状态和常用方法
  2. 2. wait() 和 notify() 方法
    1. 2.1. 示例:发送者 - 接收者同步问题
  3. 3. wait()和sleep()方法
    1. 3.1. 示例代码
  4. 4. join()方法
    1. 4.1. join()方法的作用
    2. 4.2. 使用join时需要注意的几点
    3. 4.3. 示例代码
  5. 5. yield()方法
    1. 5.1. yield()的作用
    2. 5.2. 示例代码
  6. 6. Reference