线程间通信
1. 单线程间通信
wait和notify
wait和notify方法并不是Thread特有的方法,而是Object中的方法,也就是JDK中的每一个类到拥有这两个方法。
wait
public final void wait() throws InterruptedException
public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException
- wait方法的两个重载方法都将用调用native wait(long timeout)方法,wait()方法等价于wait(0),0代表永不超时。
- Object的wait(long timeout)方法会导致当前线程进入阻塞,直到有其他线程调用了Object的notify()或者notifyAll方法才能将其唤醒,或者阻塞到达了timeout时间自动唤醒。
- wait方法必须拥有该对象的monitor,也就是wait方法必须在同步方法中调用。
- 当前线程执行了该对象的wait方法之后,将会放弃对monitor的所有权并且进入与该对象关联的wait set中,也就是说一旦线程执行了某个object的wait方法之后,他就会释放对对该对象monitor的所有权,其他线程也就有机会继续争抢该monitor的所有权。
notify
public final native void notify();
- 唤醒单个正在执行该对象wait方法的线程。
- 如果有某个线程由于执行该对象的wait方法而进入阻塞则会被唤醒,如果没有则会忽略。
- 被唤醒的线程需要重新获取对该对象所关联monitor的lock才能继续执行。
- 如果有任何线程正在等待该对象,则选择其中一个被唤醒。该选择是任意的,并由实施自行决定。
注意事项:
-
wait方法是可中断方法,当前线程一旦调用了wait方法进入阻塞状态,其他线程是可以使用interrupt方法将其打断的。可中断方法被打断后悔收到中断异常InterruptedException,同时interrupt标识也会被擦除。
-
线程执行了某个对象的wait方法以后,会加入与之对应的wait set中,每一个对象的monitor都有一个与之关联的wait set。
-
当线程进入wait set之后,notify方法可以将其唤醒,也就是从wait set中弹出,同时中断wait中的线程也会将其唤醒。
-
必须在同步方法中使用wait和notify方法,因为执行wait和notify的前提条件是必须持有同步方法的monitor的所有权,运行下面任何一个方法都会抛出非法的monitor状态异常IllgalMonitorStateException:
private void waitTest() {
try {
this.wait();
} catch(InterruptedException){
e.printStackTrace();
}
}
private void notifyTest(){
this.notify();
} -
同步代码的monitor必须与执行wait notify方法的对象一致,简单来说就是用那个对象的monitor进行同步,就只能用哪个对象进行wait和notify操作。
public class WaitNotifyTest {
private final Object MUTEX = new Object();
private synchronized void testWait() {
try {
MUTEX.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private synchronized void testNotify() {
MUTEX.notify();
}
}
以上方法运行之后会报
Exception in thread "main" java.lang.IllegalMonitorStateException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at com.spring.zcl.study.springbootstudy.thread.thread10.WaitNotifyTest.testWait1(WaitNotifyTest.java:29)
at com.spring.zcl.study.springbootstudy.thread.thread10.WaitNotifyTest.main(WaitNotifyTest.java:41)
修改如下即可:
private void testWait() {
synchronized(MUTEX) {
try {
MUTEX.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void testNotify() {
synchronized (MUTEX){
MUTEX.notify();
}
}
原因:上述方法中synchronized在方法上,那么monitor引用的是this,而wait和notify方法使用的却是MUTEX的方法,虽然是在同步方法中执行了wait和notify,但是并未以获取MUTEX的monitor为前提。
wait和sleep
- wait和sleep方法都可以是线程进入阻塞状态。
- wait和sleep方法均是可中断方法,被中断后都会收到中断异常。
- wait是Object方法,sleep是Thread特有的方法。
- wait方法的执行必须在同步方法中进行,而sleep则不需要。
- 线程在同步方法中执行sleep方法时,并不会释放monitor的锁,而wait方法则会释放monitor的锁。
- sleep方法短暂休眠之后会主动退出阻塞,而wait方法(没有指定wait时间)则需要被其他线程中断后才能退出阻塞。
2. 多线程间通信
线程休息室 wait set
在虚拟机规范中存在一个wait set(wait set 又被称为线程休息室)的概念,至于该wait set是怎样的数据结构,JDK官方并没有给出明确的定义,不同厂家的JDK有着不同的实现方式,甚至相同的JDK厂家不同的版本也存在着差异,但是不管怎样,线程调用了某个对象的wait方法之后都会被加入与该对象monitor关联的wait set中,并且释放monitor的所有权。
若干个线程调用wait方法之后被加入与monitor关联的wait set中,待另外一个线程调用该monitor的notify方法之后,其中一个线程会从wait set中弹出,至于随机还是先进先出弹出,虚拟机没有强制要求。
notify更不需要考虑哪个线程会被弹出,以为wait set中的所有wait线程将会被弹出。
synchronized缺陷
- 无法控制阻塞时长
- synchronized同步的线程不可被中断
改造自定义代码:
package com.spring.zcl.study.springbootstudy.thread.Lock;
import java.util.List;
import java.util.concurrent.TimeoutException;
/**
* @Author: zcl
* @Date: 2022-01-10 14:52
*/
public interface Lock {
/**
* this is synchronized use wait and notify implements
* @throws InterruptedException 中断异常
*/
void lock() throws InterruptedException;
/**
* this is synchronized use wait and notify implements
* contains time control
* @param mills time mills
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
void lock(long mills) throws InterruptedException, TimeoutException;
/**
* 解锁
*/
void unlock();
/**
* 获取当前阻塞线程
* @return 阻塞线程集合
*/
List<Thread> getBlockedThreads();
}
实现:
package com.spring.zcl.study.springbootstudy.thread.Lock;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeoutException;
/**
* @Author: zcl
* @Date: 2022-01-10 14:54
*/
public class BooleanLock implements Lock{
private Thread currentThread;
private boolean locked = false;
private final List<Thread> blockedList = new ArrayList<>();
@Override
public void lock() throws InterruptedException {
synchronized (this) {
while (locked) {
blockedList.add(Thread.currentThread());
this.wait();
}
blockedList.remove(Thread.currentThread());
this.locked = true;
this.currentThread = Thread.currentThread();
}
}
@Override
public void lock(long mills) throws InterruptedException, TimeoutException {
synchronized (this) {
if (mills <= 0) {
this.lock();
} else {
long remainingMills = mills;
long endMills = System.currentTimeMillis() + remainingMills;
while (locked) {
if (remainingMills <= 0) {
throw new TimeoutException("can not get the lock during " + mills);
}
if (!blockedList.contains(Thread.currentThread())) {
blockedList.add(Thread.currentThread());
}
this.wait(remainingMills);
remainingMills = endMills - System.currentTimeMillis();
}
blockedList.remove(Thread.currentThread());
this.locked = true;
this.currentThread = Thread.currentThread();
}
}
}
@Override
public void unlock() {
synchronized (this) {
if (currentThread == Thread.currentThread()) {
this.locked = false;
Optional.of(Thread.currentThread().getName() + " release the lock.")
.ifPresent(System.out::println);
this.notifyAll();
}
}
}
@Override
public List<Thread> getBlockedThreads() {
return Collections.unmodifiableList(blockedList);
}
}
测试类:
package com.spring.zcl.study.springbootstudy.thread.Lock;
import java.sql.Time;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
/**
* @Author: zcl
* @Date: 2022-01-10 15:09
*/
public class RunLock {
private final Lock lock = new BooleanLock();
try {
lock.lock();
int randomInt = ThreadLocalRandom.current().nextInt(10);
System.out.println(Thread.currentThread() + " get the lock");
TimeUnit.SECONDS.sleep(randomInt);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
test2();
}
public static void test1(){
RunLock runLock = new RunLock();
IntStream.range(0, 10).mapToObj(i -> new Thread(runLock::syncMethod)).forEach(Thread::start);
}
public static void test2() throws InterruptedException {
RunLock runLock = new RunLock();
new Thread(runLock::syncMethod, "Thread1").start();
TimeUnit.SECONDS.sleep(1);
Thread thread = new Thread(runLock::syncMethod, "Thread2");
thread.start();
TimeUnit.SECONDS.sleep(2);
thread.interrupt();
}
}
以上代码仍存在问题,就是某个线程被中断,那么它将有可能还处于blockList中,该问题修复如下:
synchronized (this) {
while (locked) {
final Thread tempThread = Thread.currentThread();
try {
if (!blockedList.contains(tempThread)) {
blockedList.add(Thread.currentThread());
}
this.wait();
} catch (InterruptedException e) {
blockedList.remove(Thread.currentThread());
throw e;
}
}
blockedList.remove(Thread.currentThread());
this.locked = true;
this.currentThread = Thread.currentThread();
}
public void lock(long mills) throws InterruptedException, TimeoutException {
synchronized (this) {
if (mills <= 0) {
this.lock();
} else {
long remainingMills = mills;
long endMills = System.currentTimeMillis() + remainingMills;
while (locked) {
if (remainingMills <= 0) {
throw new TimeoutException("can not get the lock during " + mills);
}
final Thread tempThread = Thread.currentThread();
try {
if (!blockedList.contains(tempThread)) {
blockedList.add(tempThread);
}
this.wait(remainingMills);
} catch (InterruptedException e) {
blockedList.remove(tempThread);
throw e;
}
remainingMills = endMills - System.currentTimeMillis();
}
blockedList.remove(Thread.currentThread());
this.locked = true;
this.currentThread = Thread.currentThread();
}
}
}
学习自:
《Java高并发编程详解——多线程与架构设计》 汪文君