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()處於的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()會恢復第一個執行wait()的線程。其他的不動。
調用notifyAll()會按照後進先出(LIFO)的原則恢復線程。
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 < 3; 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()");
}
}
}
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
//19:35:51 AThread0 before wait()
//19:35:51 AThread1 before wait()
//19:35:51 AThread2 before wait()
//19:35:51 BThread before notify()
//19:35:56 BThread after notify()
//19:35:56 AThread0 after wait()
//調用notifyAll
//19:39:51 AThread0 before wait()
//19:39:51 AThread2 before wait()
//19:39:51 AThread1 before wait()
//19:39:51 BThread before notify()
//19:39:56 BThread after notify()
//19:39:56 AThread1 after wait()
//19:39:56 AThread2 after wait()
//19:39: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。如果沒有這種阻塞方法?那就什麼都不會做。下面是兩種標準用法
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();
}
}
...
}