推薦:Java并發程式設計彙總
Thread和Object中的重要方法詳解
原文位址
多線程學習(二)——Thread和Object類中的重要方法詳解
方法概覽
類 | 方法名 | 簡介 |
Thread | sleep | 線程休眠一段時間 |
join | 等待其他線程執行完畢 | |
yield | 放棄已獲得的CPU資源 | |
currentThread | 擷取目前執行線程的引用 | |
start | 啟動線程 | |
interrupt | 中斷線程 | |
stop, suspend, resume | 已廢棄 | |
Object | wait / notify / notifyAll | 讓線程暫時休息/喚醒 |
wait / notify / notifyAll 方法
作用
wait
讓線程休息,進入阻塞階段,執行完
wait
方法會釋放
monitor
鎖。
四種被喚醒的情況:
- 另一個線程調用這個對象的
方法,且剛好被喚醒的是本線程。notify()
- 另一個線程調用這個對象的
方法。notifyAll()
- 過了
規定的逾時時間,如果傳入的是wait(long timeout)
,則表示永久等待。0
- 線程自身調用了
(遇到中斷時,會抛出interrupt()
并釋放InterruptedException
鎖)。monitor
特點
- 需要在
關鍵字修飾的代碼塊或方法中執行。synchronized
- 執行
、wait
、notify
都必須先擁有該notifyAll
。monitor
-
隻能喚醒其中一個線程。notify
- 多個鎖的情況下,隻會釋放目前的鎖。
- 都屬于
類Object
代碼示範
-
:線程場景
睡了,線程1
去喚醒。2
-
:執行順序
進入同步代碼塊,執行了Thread1
方法後,線程釋放了鎖; 此時wait()
獲得鎖,執行了Thread2
方法喚醒了notify()
,執行完 Thread1
同步代碼塊中的所有語句後,Thread2
繼續執行 Thread1
後的代碼。wait()
public class WaitNotify {
public static Object object = new Object();
static class Thread1 extends Thread {
@Override
public void run() {
synchronized (object) {
System.out.println(Thread.currentThread().getName() + " 開始執行");
try {
// 在同步代碼塊中執行了 wait() 方法後,釋放了鎖
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 擷取到了monitor鎖");
}
}
}
// 喚醒 Thread1
static class Thread2 extends Thread {
@Override
public void run() {
synchronized (object) {
object.notify();
System.out.println(Thread.currentThread().getName() + " 調用了notify");
}
}
}
// 讓Thread1先進入wait狀态,再被Thread2喚醒
public static void main(String[] args) throws InterruptedException {
Thread1 thread1 = new Thread1();
Thread2 thread2 = new Thread2();
thread1.start();
Thread.sleep(200);
thread2.start();
}
}
notify與notifyAll的差別
-
:線程場景
和線程1
被阻塞,線程2
喚醒它們。3
-
:調用notifyAll
喚醒全部線程(notifyAll
的調用順序不一定能保證線程的執行順序,是以線程start
需要等待一會兒再3
)。start
public class NotifyAndNotifyAll implements Runnable{
private static final Object resource = new Object();
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new NotifyAndNotifyAll();
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
Thread thread3 = new Thread(() -> {
synchronized (resource) {
resource.notifyAll();
System.out.println(Thread.currentThread().getName() + " 看我叫醒這兩個豬");
}
});
thread1.start();
thread2.start();
Thread.sleep(200);
thread3.start();
}
@Override
public void run() {
synchronized (resource) {
System.out.println(Thread.currentThread().getName() + " 得到了鎖");
try {
System.out.println(Thread.currentThread().getName() + " 困了,休息會兒");
resource.wait();
System.out.println(Thread.currentThread().getName() + " 醒啦,一起來happy");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
-
:notify
隻能喚醒其中一個線程。notify
證明wait隻會釋放目前的鎖
-
:線程場景
持有兩把鎖并釋放其中一把,線程1
能拿到幾把鎖。2
public class ReleaseOwnMonitor {
private static volatile Object resourceA = new Object();
private static volatile Object resourceB = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (resourceA) {
System.out.println(Thread.currentThread().getName() + " 得到了resourceA鎖");
synchronized (resourceB) {
System.out.println(Thread.currentThread().getName() + " 得到了resourceB鎖");
try {
System.out.println(Thread.currentThread().getName() + " 釋放了resourceA鎖");
resourceA.wait();
System.out.println(Thread.currentThread().getName() + " 後續happy");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resourceA) {
System.out.println(Thread.currentThread().getName() + " 表示收到了老鐵釋放的resourceA了");
System.out.println(Thread.currentThread().getName() + " 心想:那麼resourceB還在它手上嗎,拿到了我再吱聲");
synchronized (resourceB) {
System.out.println(Thread.currentThread().getName() + " 我拿到resourceB啦");
}
}
}).start();
}
}
根據控制台輸出,我們可以看到線程
2
隻拿到了線程
1
釋放的目前的鎖
A
,鎖
B
沒有被釋放導緻線程
2
一直在等待鎖的過程中。
sleep 方法
作用
隻想讓線程在預期的時間執行,其他時間不占用CPU資源。
特點
與
wait
不同,執行
wait
會釋放鎖,而執行
sleep
的時候不釋放鎖(
synchronized
和
lock
),等
sleep
設定的時間到了以後(正常結束)才會釋放鎖。
代碼示範
-
方法不釋放鎖。sleep
public class SleepDontReleaseMonitor implements Runnable{
public static void main(String[] args) {
SleepDontReleaseMonitor target = new SleepDontReleaseMonitor();
new Thread(target).start();
new Thread(target).start();
}
@Override
public void run() {
sync();
}
private synchronized void sync() {
System.out.println(Thread.currentThread().getName() + " 擷取到了monitor");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 退出了同步代碼塊");
}
}
Thread-0 擷取到了monitor
Thread-0 退出了同步代碼塊
Thread-1 擷取到了monitor
Thread-1 退出了同步代碼塊
public class SleepDontReleaseLock implements Runnable{
private static final Lock LOCK = new ReentrantLock();
public static void main(String[] args) {
SleepDontReleaseLock target = new SleepDontReleaseLock();
new Thread(target).start();
new Thread(target).start();
}
@Override
public void run() {
LOCK.lock();
System.out.println(Thread.currentThread().getName() + " 擷取到了鎖");
try {
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + " 醒了");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
LOCK.unlock();
}
}
}
Thread-1 擷取到了鎖
Thread-1 醒了
Thread-0 擷取到了鎖
Thread-0 醒了
-
方法響應中斷,線程中斷會抛出sleep
,随即會清除中斷狀态。InterruptedException
public class SleepInterrupted implements Runnable{
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new SleepInterrupted());
thread.start();
Thread.sleep(8000);
thread.interrupt();
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(new Date());
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
System.out.println("遇到了中斷");
e.printStackTrace();
}
}
}
}
TimeUnit上面的代碼中,我們使用了
TimeUnit.SECONDS.sleep(long)
進行休眠,它幫我們進行了時間的機關換算。
而它的
sleep
方法本質也是
Thread.sleep
,隻不過當它的逾時時間
< 0
時,不作操作,而
Thread.sleep
的設定的時間如果
< 0
,則會
throw new IllegalArgumentException("timeout value is negative")
。
join 方法
作用
新的線程加入,需要等待它執行完畢再出發,例如
main
線程等待子線程
thread1
執行完畢。
代碼示範
-
方法的用法,如果沒有調用 join
方法,兩條輸出語句join
和如廁
是緊接着輸出的,而調用了 上完出來
方法,join
線程就會等待它們執行完畢再執行。main
public class Join {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 我花了 2 秒上完了廁所");
});
Thread thread2 = new Thread(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 我花了 3 秒上完了廁所");
});
thread1.start();
thread2.start();
System.out.println(Thread.currentThread().getName() + " 小夥伴要如廁,我在門口開始等候它們");
thread1.join();
thread2.join();
System.out.println(Thread.currentThread().getName() + " 它們終于上完出來啦");
}
}
main 小夥伴要如廁,我在門口開始等候它們
Thread-0 我花了 2 秒上完了廁所
Thread-1 我花了 3 秒上完了廁所
main 它們終于上完出來啦
- 當主線程中斷時,子線程也需要中斷。
public class JoinInterrupted {
public static void main(String[] args) {
Thread mianThread = Thread.currentThread();
Thread thread1 = new Thread(() -> {
try {
mianThread.interrupt();
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + " 我執行完畢了");
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 我也中斷了");
}
});
thread1.start();
System.out.println(Thread.currentThread().getName() + " 等待子線程搞完");
try {
// 主線程加入 Thread1
thread1.join();
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 被中斷了");
// 将主線程的中斷傳遞給子線程,不然會不一緻
thread1.interrupt();
}
System.out.println(Thread.currentThread().getName() + " 子線程執行完畢了");
}
}
main 等待子線程搞完
main 被中斷了
main 子線程執行完畢了
Thread-0 我也中斷了
-
期間,線程是 join
狀态。Waiting
public class JoinState {
public static void main(String[] args) throws InterruptedException {
Thread mainThread = Thread.currentThread();
Thread thread = new Thread(() -> {
try {
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + " 我想知道此時主線程的狀态 : " + mainThread.getState());
System.out.println(Thread.currentThread().getName() + " 運作結束");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
System.out.println(Thread.currentThread().getName() + " 等待子線程運作完畢");
thread.join();
System.out.println(Thread.currentThread().getName() + " 子線程運作完畢");
}
}
main 等待子線程運作完畢
Thread-0 我想知道此時主線程的狀态 : WAITING
Thread-0 運作結束
main 子線程運作完畢
源碼
join
内部調用了
wait()
方法,
wait(0)
表示一直處于休眠直至被喚醒。然而我們并沒有看到喚醒的方法,這是因為
Thread
類
run
方法執行完畢後,會進行自動喚醒。底層
c/c++
代碼中線上程退出後,會執行
lock.notify_all(thread)
喚醒。
void JavaThread::exit(booldestory_vm, ExitTypeexit_type);
static void ensure_join(JavaThread*thread) {
Handle threadObj(thread, thread -> threadObj());
ObjectLocker lock(threadObj, thread);
thread -> clear_pending_exception();
java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);
java_lang_Thread::set_thread(threadObj(), NULL);
lock.notify_all(thread);
thread -> clear_pending_exception();
}
join
源碼:
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;
}
}
}
CountDownLatch
/
CyclicBarrier
是
join
的等價類 (感興趣的小夥伴可以參考另外資料了解)。
yeild 方法
作用
釋放自己的CPU時間片,但不會釋放鎖,也不會陷入阻塞,線程狀态依然是
Runnable
狀态。
與
sleep
的差別,
yield
隻是暫時把CPU排程權讓給别的線程,但該線程還是能立刻處于競争狀态被再次排程,
sleep
的話,會被認為已經阻塞了。
問題
- 為什麼線程通信的方法
, wait()
和 notify()
被定義在notifyAll()
類中?而Object
定義在sleep()
Thread
類?
因為
, wait()
和 notify()
是鎖級别操作,而鎖是屬于對象的,每一個對象的對象頭中都包含幾位用于儲存目前鎖的狀态的預留,是以鎖是綁定于某一個對象,而不是線程。notifyAll()
-
/wait
與notify
sleep
的異同點?
相同:都會使線程進入阻塞狀态,可以響應中斷。
不同:
/wait
必須在同步方法中執行,防止死鎖和永久等待,而notify
不需要;sleep
釋放鎖,wait
不釋放鎖;sleep
必須指定參數,sleep
可不傳參;所屬的類不同,前者屬于wait
類,後者屬于Object
類。Thread
使用
wait
/
notify
實作生産者消費者設計模式,生産者往隊列中存放資料,如果隊列滿了會阻塞;消費者從隊列擷取資料,如果隊列空了也會阻塞。
如果隊列中有了資料,生産者會通知消費者去擷取資料;同樣如果隊列中資料不滿,消費者會通知生産者生産資料。
public class ConsumerProducer {
public static void main(String[] args) {
DataStorage storage = new DataStorage();
Producer producer = new Producer(storage);
Consumer consumer = new Consumer(storage);
new Thread(producer).start();
new Thread(consumer).start();
}
}
class DataStorage {
private int maxSize;
private LinkedList<Date> storage;
// 初始化存儲隊列
public DataStorage() {
this.maxSize = 10;
this.storage = new LinkedList<>();
}
// 生産資料
public synchronized void put() {
// 如果隊列中已滿,則進入等待狀态
while (storage.size() == maxSize) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
storage.add(new Date());
System.out.println("生産者生産資料了,倉庫中有 " + storage.size() + " 條資料");
// 通知消費者
notify();
}
// 消費資料
public synchronized void get() {
// 同理,如果隊列中沒有資料,則進入等待資料的狀态
while (storage.size() == 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 擷取并删除隊列中資料
System.out.println("消費者取到 " + storage.poll() + " ; 倉庫還剩 " + storage.size() + " 條資料");
// 消費後必然會有空閑容量,通知生産者
notify();
}
}
兩個線程交替列印 0~100 的奇偶數
僅通過加鎖
synchronized
,分别判斷奇、偶性進行輸出。
該方法雖然能得到正确的輸出,但是效率很低。兩個線程同時在競争鎖,如果同一個線程一直搶到鎖,另一個線程就會一直等待無法接着輸出,這會經曆多餘的
while
循環(隻不過不符合輸出要求不會列印罷了)。
public class PrintOddEvenAlternately {
private static Object lock = new Object();
private static int num;
public static void main(String[] args) throws InterruptedException {
new Thread(new AlternateRunner(), "OddThread").start();
Thread.sleep(100);
new Thread(new AlternateRunner(), "EvenThread").start();
}
private static class AlternateRunner implements Runnable {
@Override
public void run() {
while (num <= 100) {
synchronized (lock) {
// int類型變量初始化值為 0,第一次會是偶數線程列印 0
System.out.println(Thread.currentThread().getName() + " : " + num++);
// 偶(奇)線程在列印一次後,喚醒對方執行列印
lock.notify();
// 這裡必須再進行一次判斷再進入wait
// 否則如果直接進入休眠,而正好100輸出後通過num++變成了101,另外個線程就無法進入while循環進行喚醒
// 那麼線程将一直處于等待狀态,程式無法停止運作
if (num <= 100) {
try {
// 進入等待狀态,釋放鎖給對方
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}