上篇文章咱們介紹了 synchronized 這個關鍵字,經過它能夠基本實現線程間在臨界區對臨界資源正確的訪問與修改。可是,它依賴一個 Java 對象內置鎖,某個時刻只能由一個線程佔有該鎖,其餘試圖佔有的線程都得阻塞在對象的阻塞隊列上。java
但實際上還有一種狀況也是存在的,若是某個線程得到了鎖但在執行過程當中因爲某些條件的缺失,好比數據庫查詢的資源還未到來,磁盤讀取指令的數據未返回等,這種狀況下,讓線程依然佔有 CPU 等待是一種資源上的浪費。git
因此,每一個對象上也存在一個等待隊列,這個隊列上阻塞了全部得到鎖並處於運行期間缺失某些條件的線程,因此整個對象的鎖與隊列情況是這樣的。github
Entry Set 中阻塞了全部試圖得到當前對象鎖而失敗的線程,Wait Set 中阻塞了全部在得到鎖運行期間因爲缺失某些條件而交出 CPU 的線程集合。數據庫
而當某個現場稱等待的條件知足了,就會被移除等待隊列進入阻塞隊列從新競爭鎖資源。bash
Object 類中有幾個方法咱們雖然不常使用,可是確實線程協做的核心方法,咱們經過這幾個方法控制線程間協做。微信
public final native void wait(long timeout)
public final void wait()
public final native void notify();
public final native void notify();
複製代碼
wait 類方法用於阻塞當前線程,將當前線程掛載進 Wait Set 隊列,notify 類方法用於釋放一個或多個處於等待隊列中的線程。框架
因此,這兩個方法主要是操做對象的等待隊列,也便是將那些得到鎖可是運行期間缺少繼續執行的條件的線程阻塞和釋放的操做。dom
可是有一個前提你們須要注意,wait 和 notify 操做的是對象內置鎖的等待隊列,也就是說,必須在得到對象內置鎖的前提下才能阻塞和釋放等待隊列上的線程。簡單來講,這兩個方法的只能在 synchronized 修飾的代碼塊內部進行調用。ide
下面咱們看一段代碼:工具
public class Test {
private static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(){
@Override
public void run(){
synchronized (lock){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread thread2 = new Thread(){
@Override
public void run(){
synchronized (lock){
System.out.println("hello");
}
}
};
thread1.start();
thread2.start();
Thread.sleep(2000);
System.out.println(thread1.getState());
System.out.println(thread2.getState());
}
}
複製代碼
運行結果:
能夠看到,程序是沒有正常結束的,也就是說,有線程還未正常退出。線程一優先啓動於線程二,因此它將先得到 lock 鎖,接着調用 wait 方法將本身阻塞在 lock 對象的等待隊列上,並釋放鎖交出 CPU。
線程二啓動時可能因爲線程一依然佔有鎖而阻塞,但當線程一釋放鎖之後,線程二將得到鎖並執行打印語句,隨後同步方法結束並釋放鎖。
此時,線程一依然阻塞在 lock 對象的等待隊列上,因此整個程序沒有正常退出。
演示這麼一段程序的意義是什麼呢?就是想告訴你們,雖然阻塞隊列和等待隊列上的線程都不能獲得 CPU 正常執行指令,可是它們卻屬於兩種不一樣的狀態,阻塞隊列上的線程在得知鎖已經釋放後將公平競爭鎖資源,而等待隊列上的線程則必須有其餘線程經過調用 notify 方法通知並移出等待隊列進入阻塞隊列,從新競爭鎖資源。
一、sleep 方法
sleep 方法用於阻塞當前線程指定時長,線程狀態隨即變成 TIMED_WAITING,但區別於 wait 方法。二者都是讓出 CPU,可是 sleep 方法不會釋放當前持有的鎖。
也就是說,sleep 方法不是用於線程間同步協做的方法,它只是讓線程暫時交出 CPU,暫停運行一段時間,時間到了將由系統調度分配 CPU 繼續執行。
二、join 方法
join 方法用於實現兩個線程之間相互等待的一個操做,看段代碼:
public void testJoin() throws InterruptedException {
Thread thread = new Thread(){
@Override
public void run(){
for (int i=0; i<1000; i++)
System.out.println(i);
}
};
thread.start();
thread.join();
System.out.println("main thread finished.....");
}
複製代碼
拋開 join 方法不談,main 線程中的打印方法必定是先執行的,而實際上這段程序會在線程 thread 執行完成以後才執行主線程的打印方法。
實現機理區別於 sleep 方法,咱們一塊兒看看:
方法的核心就是調用 wait(delay) 阻塞當前線程,當線程被喚醒計算從進入方法到當前時間共通過了多久。
接着比較 millis 和 這個 now,若是 millis 小於 now 說明,說明等待時間已經到了,能夠退出方法返回了。不然則說明線程提早被喚醒,須要繼續等待。
須要注意的是,既然是調用的 wait 方法,那麼等待的線程必然是須要釋放持有的當前對象內置鎖的,這區別於 sleep 方法。
下面咱們寫一個頗有意思的代碼,實現操做系統中的生產者消費者模型,藉助咱們的 wait 和 notify 方法。
生產者不停生產產品到倉庫中直到倉庫滿,消費者不停的從倉庫中取出產品直到倉庫爲空。若是生產者發現倉庫已經滿了,就不能繼續生產產品,而消費者若是發現倉庫爲空,就不能從倉庫中取出產品。
public class Repository {
private List<Integer> list = new ArrayList<>();
private int limit = 10; //設置倉庫容量上限
public synchronized void addGoods(int count) throws InterruptedException {
while(list.size() == limit){
//達到倉庫上限,不能繼續生產
wait();
}
list.add(count);
System.out.println("生產者生產產品:" + count);
//通知全部的消費者
notifyAll();
}
public synchronized void removeGoods() throws InterruptedException {
while(list.size() <= 0){
//倉庫中沒有產品
wait();
}
int res = list.get(0);
list.remove(0);
System.out.println("消費者消費產品:" + res);
//通知全部的生產者
notifyAll();
}
}
複製代碼
寫一個倉庫類,該類提供兩個方法供外部調用,一個是往倉庫放產品,若是倉庫滿了則阻塞到倉庫對象的等待隊列上,一個是從倉庫中取出產品,若是倉庫爲空則阻塞在倉庫的等待隊列上。
public class Producer extends Thread{
Repository repository = null;
public Producer(Repository p){
this.repository = p;
}
@Override
public void run(){
int count = 1;
while(true){
try {
Thread.sleep((long) (Math.random() * 500));
repository.addGoods(count++);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
複製代碼
定義一個生產者類,生產者隨機的向倉庫添加產品。若是沒有能成功的添加,會被阻塞在循環裏。
public class Customer extends Thread{
Repository repository = null;
public Customer(Repository p){
this.repository = p;
}
@Override
public void run(){
while(true){
try {
Thread.sleep((long) (Math.random() * 500));
repository.removeGoods();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
複製代碼
定義一個消費者類,消費者類隨機的從倉庫中取一個產品。若是沒有成功的取出一個產品,一樣會被阻塞在循環裏。
public void testProducerAndCustomer() {
Repository repository = new Repository();
Thread producer = new Producer(repository);
Thread consumer = new Customer(repository);
producer.start();
consumer.start();
producer.join();
consumer.join();
System.out.println("main thread finished..");
}
複製代碼
主線程啓動這兩個線程,程序運行的狀況大體是這樣的:
生產者生產產品:1
消費者消費產品:1
生產者生產產品:2
消費者消費產品:2
生產者生產產品:3
消費者消費產品:3
。。。。。
。。。。。
消費者消費產品:17
生產者生產產品:21
消費者消費產品:18
生產者生產產品:22
消費者消費產品:19
生產者生產產品:23
消費者消費產品:20
生產者生產產品:24
生產者生產產品:25
生產者生產產品:26
消費者消費產品:21
生產者生產產品:27
生產者生產產品:28
消費者消費產品:22
消費者消費產品:23
生產者生產產品:29
生產者生產產品:30
。。。。。。
。。。。。。
複製代碼
仔細觀察,你會發現,消費者者永遠不會消費一個不存在的產品,消費的必定是生產者生產的產品。剛開始多是生產者生產一個產品,消費者消費一個產品,而一旦消費者線程執行的速度超過了生產者,必然會因爲倉庫容量爲空而被阻塞。
生產者線程的執行速度能夠超過消費者線程,而消費者線程的執行速度若是一直超過生產者就會致使倉庫容量爲空而導致本身被阻塞。
總結一下,synchronized 修飾的代碼塊是直接使用的對象內置鎖的阻塞隊列,線程獲取不到鎖天然被阻塞在該隊列上,而 wait/notify 則是咱們手動的控制等待隊列的入隊和出隊操做。但本質上都是利用的對象內置鎖的兩個隊列。
這兩篇文章介紹的是利用 Java 提供給咱們的對象中的內置鎖來完成基本的線程間同步操做,這部分知識是後續介紹的各類同步工具,集合類框架等實現的底層原理。
文章中的全部代碼、圖片、文件都雲存儲在個人 GitHub 上:
歡迎關注微信公衆號:OneJavaCoder,全部文章都將同步在公衆號上。