【Java併發】Daemon、Interrupt、Wait/Notify、Join、ThreadLocal、Pipe
Daemon、Interrupt、Wait/Notify、Join、ThreadLocal、Pipe
關於執行緒
現代作業系統在執行一個程式時,會為其建立一個程序。現代作業系統排程的最小單元是執行緒,也叫輕量級程序。
在一個程序裡可以建立多個執行緒,這些執行緒都擁有各自的計數器、堆疊和區域性變數等,並且能夠訪問共享的記憶體變數。
Daemon
daemon執行緒是一種支援型執行緒,當一個Java虛擬機器不存在非daemon執行緒的時候,Java虛擬機器就會退出。
一個生動的例子
public class DaemonExample {
public static void main(String[] args) {
Thread thread = new Thread(new DaemonRunner(), "DaemonRunner");
thread.setDaemon(true);
thread.start();
}
static class DaemonRunner implements Runnable {
@Override
public void run() {
try {
TimeUnit. SECONDS.sleep(10);
} catch (Exception e) {
} finally {
System.out.println("DaemonThread finally run.");
}
}
}
}
執行Daemon程式,可以看到終端沒有任何輸出。
Interrupt
中斷可以理解為執行緒的一個標識位屬性,它表示一個執行中的程式是否被其他程式進行了中斷操作。中斷好比其他執行緒對該執行緒打了個招呼。其他執行緒通過呼叫該執行緒的interrupt()方法對其進行中斷操作。
執行緒通過方法isInterrupted()來進行判斷是否被中斷,也可以呼叫靜態方法Thread.interrupted()對當前執行緒的中斷標識位進行復位。
如果該執行緒處於終結狀態,即使該執行緒被中斷過,呼叫該執行緒物件的isInterrupted()時依舊會返回flase。
許多宣告丟擲InterruptedException的方法在丟擲InterruptedException之前,Java虛擬機器會先將該執行緒的中斷標識位清除,此時呼叫isInterrupted()方法將會放回false。
一個生動的例子
public class InterruptExample {
public static void main(String[] args) throws InterruptedException {
Thread sleepThread = new Thread(new SleepRunner(), "SleepThread");
sleepThread.setDaemon(true);
Thread busyThread = new Thread(new BusyRnner(), "BusyThread");
busyThread.setDaemon(true);
sleepThread.start();
busyThread.start();
TimeUnit.SECONDS.sleep(5);
sleepThread.interrupt();
busyThread.interrupt();
System.out.println("BusyThread interrupted is " + busyThread.isInterrupted());
SleepUtils.second(30);
}
static class SleepRunner implements Runnable {
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread() + " sleep...");
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
System.out.println("SleepThread interrupted is " + Thread.currentThread().isInterrupted());
System.out.println(Thread.currentThread() + " interrupted.");
}
}
}
}
static class BusyRnner implements Runnable {
@Override
public void run() {
while (true) {
}
}
}
}
可以看到,丟擲InterruptException的執行緒SleepThread,其中斷標誌位為false,而一直忙碌運作的執行緒BusyThread,中斷標識位為true。
Wait/Notify
相關方法如下:
方法名稱 | 描述 |
---|---|
notify() | 通知一個在物件上等待的執行緒,試其從wait()方法放回 |
notifyAll() | 通知所有等待在該物件上的執行緒 |
wait() | 進入WAITING狀態,只有等待另外執行緒的通知或被中斷才會放回 |
wait(long) | 超時等待一段時間,如果沒有通知就返回 |
wait(long, int) | 對超時時間更細粒度的控制,可以達到納秒 |
一個執行緒A呼叫了物件O的wait方法進入等待狀態,而另一個執行緒B呼叫了物件O的notify()或notifyAll()方法,執行緒A收到通知後從物件O的wait()方法返回,繼續執行後續操作。
一個生動的例子
public class WaitNotifyExample {
static boolean flag = true;
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread waitThread = new Thread(new Wait(), "WaitThread");
waitThread.start();
TimeUnit.SECONDS.sleep(2);
Thread notifyThread = new Thread(new Notify(), "NotifyThread");
notifyThread.start();
}
static class Wait implements Runnable {
@Override
public void run() {
synchronized (lock) {
while (flag) {
try {
System.out.println(Thread.currentThread() + " flag is true. wait @ "
+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
lock.wait();
} catch (InterruptedException e) {
}
}
System.out.println(Thread.currentThread() + " flag is flase. running @ "
+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
}
}
}
static class Notify implements Runnable {
@Override
public void run() {
synchronized (lock) {
System.out.println(Thread.currentThread() + " hold lock. notify @ "
+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
lock.notifyAll();
flag = false;
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
}
}
synchronized (lock) {
System.out.println(Thread.currentThread() + " hold lock again. sleep @ "
+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
}
}
}
}
}
- 使用wait()、notify()和notifyAll時需要先對呼叫物件加鎖。
- 呼叫wait()方法後,執行緒狀態由running變為waiting,釋放鎖並放置到等待佇列中。
- 從wait()方法返回的前提是獲得了呼叫物件的鎖。
執行過程如下圖所示:
等待/通知的經典範式
等待方遵循如下原則:
- 獲取物件的鎖
- 如果條件不滿足,呼叫wait()方法,被通知後仍要檢查條件
- 條件滿足則執行對應的邏輯
對應的虛擬碼如下:
synchronized(物件) {
while(條件不滿足) {
物件.wait();
}
對應的處理邏輯
}
通知方遵循如下原則:
- 獲得物件的鎖
- 改變條件
- 通知物件上的執行緒
對應的虛擬碼如下:
synchronized(物件) {
改變條件
物件.notify(); / 物件.notifyAll();
}
Join
如果一個執行緒A執行了thread.join()方法語句,其含義是:當前執行緒A等待thread執行緒終止之後才從thread.join()返回。
執行緒Thread除了提供join()方法之外,還提供了join(long)和join(long, int)兩個具備超時特性的方法。
一個生動的例子
public class JoinExample {
public static void main(String[] args) throws InterruptedException {
Thread previous = Thread.currentThread();
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new Domino(previous), String.valueOf(i));
thread.start();
previous = thread;
}
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + " terminate.");
}
static class Domino implements Runnable {
private Thread thread;
public Domino(Thread thread) {
this.thread = thread;
}
@Override
public void run() {
try {
thread.join();
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + " terminate.");
}
}
}
輸出如下:
main terminate.
0 terminate.
1 terminate.
2 terminate.
3 terminate.
4 terminate.
5 terminate.
6 terminate.
7 terminate.
8 terminate.
9 terminate.
可以看出每個執行緒終止的前提是前驅執行緒的終止,每個執行緒等待前驅執行緒終止後,才從join()方法返回。
ThreadLocal
執行緒本地儲存為使用相同變數的每個不同的執行緒都建立不同的儲存。因此,如果你有5個執行緒都要使用變數x所表示的物件,那麼執行緒本地儲存會生成5個用於x不同的儲存塊。
一個生動的例子
public class ThreadLocalExample {
public static final ThreadLocal<Integer> X_THREADLOCAL = new ThreadLocal<>();
public static void increment() {
X_THREADLOCAL.set(0);
for (int i = 0; i < 5; i++) {
int value = X_THREADLOCAL.get();
System.out.println(Thread.currentThread().getName() + " : " + value);
X_THREADLOCAL.set(value + 1);
}
}
public static void main(String[] args) throws InterruptedException {
ThreadLocalExample.increment();
new Thread(new StaticThreadLocal(), "staticThreadLocal-1").start();
new Thread(new StaticThreadLocal(), "StaticThreadLocal-2").start();
}
static class StaticThreadLocal implements Runnable {
@Override
public void run() {
ThreadLocalExample.increment();
}
}
}
執行這個程式時,可以看到每個單獨的執行緒都被分配了自己的儲存,因為它們每個都需要跟蹤自己的計數值,即便只有一個X_THREADLOCAL物件。
Pipe
管道輸入/輸出流和普通的檔案輸入/輸出流或者網路輸入/輸出流不同之處在於,它們主要用於執行緒之間的資料傳輸,傳輸的媒介是記憶體。
管道輸入/輸出主要包括了4種具體實現:PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前兩種面向位元組,後兩種面向字元。
一個生動的例子
package top.leagle.artofconcurrency.chapter4;
import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;
import java.util.concurrent.TimeUnit;
public class PipeExample {
public static void main(String[] args) throws IOException {
Sender sender = new Sender();
Receiver receiver = new Receiver(sender);
Thread senderThread = new Thread(sender, "SenderThrad");
Thread receiverThread = new Thread(receiver, "ReceiverThread");
senderThread.start();
receiverThread.start();
}
static class Sender implements Runnable {
private PipedWriter out = new PipedWriter();
@Override
public void run() {
try {
for (char c = 'A'; c <= 'C'; c++) {
out.write(c);
TimeUnit.SECONDS.sleep(1);
}
} catch (IOException e) {
System.out.println(e + " Sender write exception");
} catch (InterruptedException e) {
System.out.println(e + " Sender sleep interrupted");
}
}
public PipedWriter getPipedWriter() {
return out;
}
}
static class Receiver implements Runnable {
private PipedReader in;
public Receiver(Sender sender) throws IOException {
in = new PipedReader(sender.getPipedWriter());
}
@Override
public void run() {
try {
while (true) {
System.out.println("Read " + (char) in.read());
}
} catch (IOException e) {
System.out.println(e + " Receiver read exception");
}
}
}
}
參考
- Java併發程式設計的藝術[書籍]