线程间的协作(线程通信)

线程的状态

Java中线程中状态可分为五种:New(新建状态),Runnable(就绪状态),Running(运行状态),Blocked(阻塞状态),Dead(死亡状态)。

线程中各种状态的转换关系如下图:

wait/notify/notifyAll

这三个方法都是Object上的方法, 只有获取到了所调用对象的monitor锁才能进行调用。是什么意思呢,举个例子:

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

public Object lock = new Object();

public void testWait() {
synchronized (lock) {
try {
lock.wait(1000);
System.out.println("end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final WaitT waitT = new WaitT();
new Thread(() -> {
waitT.testWait();
}).start();
}
}

上述例子中,只有获取到了lock对象的monitor锁(通过synchronize)才能调用lock.wait(注意,这里是通过lock对象才能调用wait,因为是获取的lock对象的锁)

wait

JDK中一共提供了三种wait方法:

  1. wait()方法的作用是将当前运行的线程挂起(即让其进入阻塞状态),直到notify或notifyAll方法来唤醒线程.

  2. wait(long timeout),该方法与wait()方法类似,唯一的区别就是在指定时间内,如果没有notify或notifAll方法的唤醒,也会自动唤醒

  3. 至于wait(long timeout,long nanos),本意在于更精确的控制调度时间

wait方法的使用必须在同步的范围内(获得monitor的锁),否则就会抛出IllegalMonitorStateException异常,wait方法的作用就是阻塞当前线程等待notify/notifyAll
方法的唤醒,或等待超时后自动唤醒。

notify/notifyAll

既然wait方式是通过对象的monitor对象来实现的,所以只要在同一对象上去调用notify/notifyAll方法,就可以唤醒对应对象monitor上等待的线程了。notify和notifyAll
的区别在于前者只能唤醒monitor上的一个线程,对其他线程没有影响,而n6otifyAll则唤醒所有的线程

sleep/join/yield

这三个方法是Thread上的方法

sleep

sleep方法的作用是让当前线程暂停指定的时间(毫秒),sleep方法是最简单的方法,在上述的例子中也用到过,比较容易理解。唯一需要注意的是其与wait方法的区别。最简单的区别是,wait方法依赖于同步,而sleep方法可以直接调用。

而更深层次的区别在于sleep方法只是暂时让出CPU的执行权,并不释放锁。而wait方法则会释放锁。通过sleep方法实现的暂停,程序是顺序进入同步块的,只有当上一个线程执行完成的时候,下一个线程才能进入同步方法,sleep暂停期间一直持有monitor对象锁,其他线程是不能进入的.

这个又怎么理解呢,还是用具体的例子来说明:

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

Object lock = new Object();

public void sleepSyn() {
synchronized (lock) {
try {
System.out.println(Thread.currentThread().getName() + " sleep start");
Thread.sleep(3000L);
System.out.println(Thread.currentThread().getName() + " sleep end");
} catch (InterruptedException e) {}
}
}

public static void main(String[] args) {
SleepT sleepT = new SleepT();
new Thread( () -> sleepT.sleepSyn(), "thread1").start();
sleepT.sleepSyn();
}
}

不管是那个线程先进入sleepSyn, 都会把threadName sleep start | threadName sleep end打印完成后,才会让下一个线程访问,也就是说当持有对象锁的时候,sleep期间是不会释放的

join

join方法的作用是父线程等待子线程执行完成后再执行,换句话说就是将异步执行的线程合并为同步的线程。

join也有三种调用方式:

1
2
3
void join()
void join(long millis)
void join(long millis, int nanos)

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
public final void join() throws InterruptedException {
join(0);
}

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呢,这个问题最开始困扰了我很久。我的理解是:

发起join调用的线程等待join的线程执行完了之后才会执行

有一些绕口,还是用一个例子来理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class JoinT {

public void print() {
System.out.println("sun thread run");
}

public static void main(String[] args) {
System.out.println("main thread start");
JoinT joinT = new JoinT();
Thread sunThread = new Thread(() -> joinT.print());
sunThread.start();
try {
sunThread.join();
} catch (InterruptedException e) {}
System.out.println("main thread end");
}
}

main thread 会等待 sunThread 执行完了之后在执行。从join的源码可以看到,发起join调用实际上等于获取了sunThread的monitor锁后,调用了sunThread.wait,需要等待sunThread.notify(notifyAll)才能唤醒。但是我们并没有看到唤醒的代码。这是因为当 Thread 退出后(Thread.exit), cpp的源码会自动执行 Thread.notifyAll。所以就能理解,为什么join线程执行完成后,调用join的线程会被唤醒执行

yield

yield方法的作用是暂停当前线程,以便其他线程有机会执行,不过不能指定暂停的时间,并且也不能保证当前线程马上停止。yield方法只是将Running状态转变为Runnable状态

yield是一种线程让步,暂时让出时间片,但是下一次时间片同样有机会抢占

参考资料


线程间的协作(线程通信)
https://haobin.work/2018/04/25/并发/线程间的协作/
作者
Leo Hao
发布于
2018年4月25日
许可协议