wait、notify和notifyAll
wait和notify(notifyAll)一般是成对搭配出现的,用来资源调控。wait用来将当前线程挂起,notify/notifyAll用来恢复线程。它是类Object的方法,也就是所有的对象都可以使用。一个简单的例子
public class WaitClassDemo {
private static SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
public static void main(String[] args) {
Object obj = new Object();
new AThread(obj).start();
new BThread(obj).start();
}
static class AThread extends Thread {
Object obj;
public AThread(Object obj) {
setName("AThread");
this.obj = obj;
}
@Override
public void run() {
synchronized (obj) {
System.out.println(sdf.format(new Date()) + " AThread before wait()");
try {
obj.wait(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sdf.format(new Date()) + " AThread after wait()");
}
}
}
static class BThread extends Thread {
Object obj;
public BThread(Object obj) {
setName("BThread");
this.obj = obj;
}
@Override
public void run() {
synchronized (obj) {
System.out.println(sdf.format(new Date()) + " BThread before notify()");
obj.notify();
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sdf.format(new Date()) + " BThread after notify()");
}
}
}
}
// 打印
// 14:22:34 AThread before wait()
// 14:22:34 BThread before notify()
// 14:22:39 BThread after notify()
// 14:22:39 AThread after wait()
1、wait/notify是需要需要获取对象锁的,也就是需要写在同步代码块或同步方法内部,可以理解为用synchronize包裹的。如果不用,编译会通过,但运行时会抛出java.lang.IllegalMonitorStateException。
2、wait/notify是针对某个对象,是类Object的方法,并且注意要保证synchronize、waite和notify3者都是针对同一个具体对象。比如上面的synchronize锁的是obj这个对象,wait和notify也是由的obj对象。
3、上面这个中wait()执行后该线程就处于阻塞阶段,并且把当前的锁给释放了。BThread得以继续。notify调用后,会立即轮转到wait()方法那吗?答案是不会,上面的例子显示,notify()需要把这个代码块的Thread.sleep(5000L)执行完,退出代码块后才轮转到wait()方法那。也很合理,毕竟同一时间里,只有一个线程能拿到锁执行synchronize包裹的代码里。
wait方法也有带参数版的,wait(long timeout)和wait(long timeout, int nanos),后者看了下源码,只是判断如果如果nanos>0,让timeout++。看来虚拟机时间还是没精确到纳秒的地步。
带参数的wait方法意思是等过了timeout毫秒后,就会尝试获得该锁。
但是如果此时锁在别的线程那里,wait(1000)处于的AThread是不能往下执行,下面例子中如果把BThread的注释打开,就是要等BThread走出synchronize块后才可以。
public class WaitClassDemo {
private static SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
public static void main(String[] args) {
Object obj = new Object();
new AThread(obj).start();
// new BThread(obj).start();
}
static class AThread extends Thread {
Object obj;
public AThread(Object obj) {
setName("AThread");
this.obj = obj;
}
@Override
public void run() {
synchronized (obj) {
System.out.println(sdf.format(new Date()) + " AThread before wait()");
try {
obj.wait(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sdf.format(new Date()) + " AThread after wait()");
}
}
}
static class BThread extends Thread {
Object obj;
public BThread(Object obj) {
setName("BThread");
this.obj = obj;
}
@Override
public void run() {
synchronized (obj) {
System.out.println(sdf.format(new Date()) + " BThread before");
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sdf.format(new Date()) + " BThread after");
}
}
}
}
// 注释BThread
// 19:03:43 AThread before wait()
// 19:03:44 AThread after wait()
// 不注释BThread
// 19:14:13 AThread before wait()
// 19:14:13 BThread before
// 19:14:18 BThread after
// 19:14:18 AThread after wait()
调用notify()会按照先进先出(FIFO)的原则恢复线程。
调用notifyAll()会按照后进先出(LIFO)的原则恢复线程。
ps:这种顺序可能是因为某个具体的JVM实现决定的,规范上应该是随机的唤醒顺序。
import java.text.SimpleDateFormat;
import java.util.Date;
public class WaitClassDemo {
private static SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
public static void main(String[] args) {
Object obj = new Object();
for (int i = 0; i < 5; i++) {
new AThread(i + "", obj).start();
}
new BThread(obj).start();
}
static class AThread extends Thread {
Object obj;
public AThread(String name, Object obj) {
setName("AThread" + name);
this.obj = obj;
}
@Override
public void run() {
synchronized (obj) {
System.out.println(sdf.format(new Date()) + " " + getName() + " before wait()");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sdf.format(new Date()) + " " + getName() + " after wait()");
obj.notify();
}
}
}
static class BThread extends Thread {
Object obj;
public BThread(Object obj) {
setName("BThread");
this.obj = obj;
}
@Override
public void run() {
synchronized (obj) {
System.out.println(sdf.format(new Date()) + " BThread before notify()");
obj.notify();
// obj.notifyAll();
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sdf.format(new Date()) + " BThread after notify()");
}
}
}
}
// 调用notify()
// 14:54:57 AThread0 before wait()
// 14:54:57 AThread3 before wait()
// 14:54:57 AThread4 before wait()
// 14:54:57 AThread2 before wait()
// 14:54:57 AThread1 before wait()
// 14:54:57 BThread before notify()
// 14:55:02 BThread after notify()
// 14:55:02 AThread0 after wait()
// 14:55:02 AThread3 after wait()
// 14:55:02 AThread4 after wait()
// 14:55:02 AThread2 after wait()
// 14:55:02 AThread1 after wait()
// 注释掉notify,只调用一次notifyAll()
// 14:56:51 AThread0 before wait()
// 14:56:51 AThread3 before wait()
// 14:56:51 AThread4 before wait()
// 14:56:51 AThread2 before wait()
// 14:56:51 AThread1 before wait()
// 14:56:51 BThread before notify()
// 14:56:56 BThread after notify()
// 14:56:56 AThread1 after wait()
// 14:56:56 AThread2 after wait()
// 14:56:56 AThread4 after wait()
// 14:56:56 AThread3 after wait()
// 14:56:56 AThread0 after wait()
下面是一个利用wait和notifyAll实现的生产者消费者队列。因为即使notifyAll调用了,也需要退出synchronize代码才会真正去唤醒另一个线程,所以notifyAll可以写在Queue的操作之前。
public class MainClass {
public static void main(String[] args) {
QueueBuffer q = new QueueBuffer(2);
for(int i=0; i<5; i++) {
Producer p = new Producer(q);
p.start();
}
for(int i=0; i<2; i++) {
Consumer c = new Consumer(q);
c.start();
}
}
static class QueueBuffer{
Queue<Integer> queue = new LinkedList<>();
int size;
AtomicInteger seq = new AtomicInteger();
public QueueBuffer(int size) {
this.size = size;
}
public synchronized void put() {
while (queue.size() == size) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
notifyAll();
int num = seq.getAndIncrement();
queue.offer(num);
System.out.println("producer --- " + num);
}
public synchronized int get() {
while (queue.size() == 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
notifyAll();
return queue.poll();
}
}
static class Producer extends Thread{
QueueBuffer q;
static AtomicInteger seq = new AtomicInteger();
public Producer(QueueBuffer q) {
this.q = q;
}
@Override
public void run() {
while (true) {
q.put();
}
}
}
static class Consumer extends Thread{
QueueBuffer q;
public Consumer(QueueBuffer q) {
this.q = q;
}
@Override
public void run() {
while (true) {
int num = q.get();
try {
sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("consumer --- " + num);
}
}
}
}
sleep
Thread.sleep(long)应该是我们最常用的,一般也知道sleep方法不会释放锁(如果写在synchronize里的话)。
所以跟wait的区别是
1、sleep是Thread类的方法,是「静态方法」。wait是Object类的方法,调用需要具体的对象。
2、sleep是不释放锁的,解除方法要么是timeout,或者interrupt一下让它抛出InterruptedException。wait是释放锁的,可以被notify/notifyAll恢复,同样也可以timeout或者interrupt。
3、sleep在哪里都可以调用,wait必须在同步方法或同步块里调用,并且同步的对象要跟wait的对象一样。
4、sleep作用只是线程的操作,用于短时间暂停线程,wait/notify可以用作线程间通信,达到资源调度的功能。
yield
yield方法也是Thread类的静态方法,会把当前线程从可运行状态变成就绪状态,之后会cpu会从众多就绪状态的线程中选择一个来执行。选线程是根据线程优先级顺序的,如果没有比当前线程更高优先级的就绪线程,完全有可能选回刚才执行yield方法的线程。
join
join也是Thread类方法,非静态,表示等待该线程结束,当前线程才继续执行。
public class JoinClassDemo {
private static SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
public static void main(String[] args) {
System.out.println(sdf.format(new Date()) + " MainThread entry");
JoinThread t = new JoinThread();
t.start();
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sdf.format(new Date()) + " MainThread exit");
}
static class JoinThread extends Thread {
@Override
public void run() {
System.out.println(sdf.format(new Date()) + " JoinThread entry");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sdf.format(new Date()) + " JoinThread exit");
}
}
}
// 17:24:18 MainThread entry
// 17:24:18 JoinThread entry
// 17:24:23 JoinThread exit
// 17:24:23 MainThread exit
线程中断
首先Thread有两个一个暂停方法suspend()和一个停止方法stop()。两个都已经已经@deprecated废弃了。suspend()暂停和resume()继续容易造成死锁,stop()具有固有的不安全性。具体可以看Java API的文档注释。
所以抛弃上面的方法后,一般我们会用以下几个方法退出线程。
1.设计标记位法
public class InterruptDemo {
private static SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
public static void main(String[] args) {
new InterruptThread().start();
}
static class InterruptThread extends Thread {
public boolean stopFlag = true;
@Override
public void run() {
while (stopFlag) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sdf.format(new Date()));
}
}
}
}
一个变量作为标记位,判断标记位以确定退出循环达到退出线程。
缺点就是如果代码并没有这种循环语句,或者线程被其他语句阻塞了,线程可能一直不会去检查标记位。
2.interrupt中断
public class Thread implements Runnable {
// 中断目标线程
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
// 返回目标线程的中断状态 static在这里理解为:只供当前线程使用这个方法
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
// 判断目标线程是否中断
public boolean isInterrupted() {
return isInterrupted(false);
}
private native boolean isInterrupted(boolean ClearInterrupted);
}
线程里有一个boolean类型的中断状态,是一个标记位,是存在Native层的。当使用Thread的interrupt()方法时,线程的中断状态会被设置为true。一些阻塞方法就会抛出一个异常InterruptedException。如果没有这种阻塞方法?那就什么都不会做。
isInterrupted() 和 interrupted() 比较一下,相同点是这两个都是返回一个boolean值,true表示中断,false表示未被中断。
不同点是interrupted()是静态方法,只能在当前线程调用,判断是true后会清除标记,也就是重置为false。isInterrupted()是实例方法,不会清除标记,所以可以多次判断。
下面是两种标准用法
public class InterruptDemo {
private static SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss:SSS");
public static void main(String[] args) {
InterruptThread t = new InterruptThread();
// Interrupt2Thread t = new Interrupt2Thread();
t.start();
try {
Thread.sleep(3500);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.interrupt();
}
static class InterruptThread extends Thread {
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 抛出InterruptedException后中断标志被清除
System.out.println(sdf.format(new Date()) + " catch " + isInterrupted());
return;
}
System.out.println(sdf.format(new Date()) + " " + isInterrupted());
}
}
}
static class Interrupt2Thread extends Thread {
@Override
public void run() {
while (!isInterrupted()) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 抛出InterruptedException后中断标志被清除
// 可以再次调用interrupt恢复中断
System.out.println(sdf.format(new Date()) + " catch " + isInterrupted());
interrupt();
}
System.out.println(sdf.format(new Date()) + " " + isInterrupted());
}
}
}
}
// 15:08:01:249 false
// 15:08:02:251 false
// 15:08:03:252 false
// 15:08:03:749 catch false
InterruptThread是在catch中直接return结束线程。Interrupt2Thread是catch中再次调用interrupt恢复中断状态,下次判断isInterrupted()中结束线程。
需要注意的点如下
①、线程不应该交给别的线程中断,应该由自己中断自己,过程中保证资源和变量已合理的处理了(该关的关,该释放的释放)。
②、所谓的interrupt线程中断,只是修改了一个标记位,需要我们判断标记位做后续的处理。如果catch代码块什么都不处理,会继续跑完剩下的代码。所以应该理解为『并不是中断,而是通知你应该自行中断了』
③、注意在Thread.sleep这些方法,抛出InterruptedException异常后会清除标记位状态。下图为文档说明
类似的方法有
Thread.sleep
Thread.join
Object.wait
BlockingQueue.put(e)和take() 这可以用于实现生产者消费者队列
3.使用FutureTask.cancel(true)或者使用线程池的shutdown()方法(比如ThreadPoolExecutor.shutdown)
《AsyncTask源码解析 从AsyncTask讲到线程池》中讲到了java1.5的java.util.concurrent包带来新的线程处理方式。比如说FutureTask和ExecutorService。
看FutureTask.cancel源码可以知道,所谓的cancel(true),内部也只是调用了interrupt()
public class FutureTask<V> implements RunnableFuture<V>
...
public boolean cancel(boolean mayInterruptIfRunning) {
if (!(state == NEW &&
UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
return false;
try { // in case call to interrupt throws exception
if (mayInterruptIfRunning) {
try {
Thread t = runner;
if (t != null)
t.interrupt();
} finally { // final state
UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
}
}
} finally {
finishCompletion();
}
return true;
}
...
}
ThreadPoolExecutor.shutdown方法也是一样
public class ThreadPoolExecutor extends AbstractExecutorService {
...
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(SHUTDOWN);
interruptIdleWorkers();
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
tryTerminate();
}
private void interruptIdleWorkers() {
interruptIdleWorkers(false);
}
private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers) {
Thread t = w.thread;
if (!t.isInterrupted() && w.tryLock()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
} finally {
w.unlock();
}
}
if (onlyOne)
break;
}
} finally {
mainLock.unlock();
}
}
...
}