本文目錄:
1.等待、喚醒機制的原理
2.Lock和Condition
3.單生產者單消費者模式
4.使用Lock和Condition實現單生產單消費模式
5.多生產多消費模式(單面包)
6.多生產多消費模式
生產者消費者模式是多執行緒中最為常見的模式:生產者執行緒(一個或多個)生成麵包放進籃子裡(集合或陣列),同時,消費者執行緒(一個或多個)從籃子裡(集合或陣列)取出麵包消耗。雖然它們任務不同,但處理的資源是相同的,這體現的是一種執行緒間通訊方式。
本文將先說明單生產者單消費者的情況,之後再說明多生產者多消費者模式的情況。還會分別使用wait()/nofity()/nofityAll()
機制、lock()/unlock()
機制實現這兩種模式。
在開始介紹模式之前,先解釋下wait()、notify()和notifyAll()
方法的用法細節以及改進的lock()/unlock()、await()/signal()/signalAll()
的用法。
1.等待、喚醒機制的原理
wait()、notify()和notifyAll()
分別表示讓執行緒進入睡眠、喚醒睡眠執行緒以及喚醒所有睡眠的執行緒。但是,物件是哪個執行緒呢?另外,在API文件中描述這三個方法都必須在有效監視器(可理解為持有鎖)的前提下使用。這三個方法和鎖有什麼關係呢?
以同步程式碼塊synchronized(obj){}
或同步函式為例,在它們的程式碼結構中可以使用wait()、notify()以及notifyAll()
,因為它們都持有鎖。
對於下面的兩個同步程式碼塊來說,分別使用的是鎖obj1和鎖obj2,其中執行緒1、執行緒2執行的是obj1對應的同步程式碼,執行緒3、執行緒4執行的是obj2對應的同步程式碼。
class MyLock implements Runnable {
public int flag = 0;
Object obj1 = new Object();
Object obj2 = new Object();
public void run(){
while(true){
if(flag%2=0){
synchronized(obj1){ //執行緒t1和t2執行此同步任務
//try{obj1.wait();}catch(InterruptedException i){}
//obj1.notify()
//obj1.notifyAll()
}
} else {
synchronized(obj2){ //執行緒t3和t4執行此同步任務
//try{obj2.wait();}catch(InterruptedException i){}
//obj2.notify()
//obj2.notifyAll()
}
}
}
}
}
class Demo {
public static void main(String[] args){
MyLock ml = new MyLock();
Thread t1 = new Thread(ml);
Thread t2 = new Thread(ml);
Thread t3 = new Thread(ml);
Thread t4 = new Thread(ml);
t1.start();
t2.start();
try{Thread.sleep(1)}catch(InterruptedException i){};
ml.flag++;
t3.start();
t4.start();
}
}
當t1開始執行到wait()
時,它將進入睡眠狀態,但卻不是一般的睡眠,而是在一個被obj1標識的執行緒池中睡眠(實際上是監視器對應執行緒池,只不過此時的監視器和鎖是繫結在一起的)。當t2開始執行,它發現鎖obj1被其他執行緒持有,它將進入睡眠態,這次睡眠是因為鎖資源等待而非wait()
進入的睡眠。因為t2已經判斷過它要申請的是obj1鎖,因此它也會進入obj1這個執行緒池睡眠,而不是普通的睡眠。同理t3和t4,這兩個執行緒會進入obj2執行緒池睡眠。
當某個執行緒執行到notify()
時,這個notify()
將 隨機 喚醒它 所屬鎖對應執行緒池 中的 任意一個 執行緒。例如,obj1.notify()
將喚醒obj1執行緒池中任意一個睡眠的執行緒(當然,如果沒有睡眠執行緒則什麼也不做)。同理notifyAll()
則是喚醒所屬鎖對應執行緒池中所有睡眠的執行緒。
必須要搞清楚的是"對應鎖",因為在呼叫wait()、notify()和notifyAll()
時都必須明確指定鎖。例如,obj1.wait()
。如果省略了所屬鎖,則表示的是this這個物件,也就是說,只有在非靜態的同步函式中才能省略這三個方法的字首。
簡而言之,當使用了同步,就使用了鎖,執行緒也就有了歸屬,它的所有依據都由所屬鎖來決定。例如,執行緒同步時,判斷鎖是否空閒以決定是否執行後面的程式碼,亦決定是否去特定的執行緒池中睡眠,當喚醒時也只會喚醒所屬鎖對應執行緒池中的執行緒。
這幾個方法在應用上,一般在一次任務中,wait()
和notify()/notifyAll()
是成對出現且擇一執行的。換句話說,就是這一輪原子性同步執行過程中,要麼執行wait()進入睡眠,要麼執行notify()
喚醒執行緒池中的睡眠執行緒。要如何實現擇一執行,可以考慮使用標記的方式來作為判斷依據。參考後文的例子。
2.Lock和Condition
wait()系列的三個方法侷限性很大,因為無論是睡眠還是喚醒的動作,都完全和鎖耦合在一起了。例如,鎖obj1關聯的執行緒只能喚醒obj1執行緒池中的執行緒,而無法喚醒鎖obj2關聯的執行緒;再例如,在原來synchronized同步時,鎖是在開始同步時隱式地自動獲取的,且是在執行完一整個任務後,又隱式地自動釋放鎖,也就是說獲取鎖和釋放鎖的動作無法人為控制。
從JDK 1.5開始,java提供了java.util.concurrent.locks包,這個包中提供了Lock介面、Condition介面和ReadWriteLock介面,前兩個介面將鎖和監視器方法(睡眠、喚醒操作)解耦了。其中Lock介面只提供鎖,通過鎖方法newConditon()
可以生成一個或多個與該鎖關聯的監視器,每個監視器都有自己的睡眠、喚醒方法。也就是說Lock替代了synchronized方法和同步程式碼塊的使用,Condition替代了Object監視器方法的使用。
如下圖:
當某執行緒執行condition1.await()
時,該執行緒將進入condition1監視器對應的執行緒池睡眠,當執行condition1.signal()
時,將隨機喚醒condition1執行緒池中的任意一個執行緒,當執行condition1.signalAll()
時,將喚醒condition1執行緒池中的所有執行緒。同理,對於condition2監視器也是一樣的。
即使有多個監視器,但只要它們關聯的是同一個鎖物件,就可以跨監視器操作對方執行緒。例如condition1中的執行緒可以執行condition2.signal()
來喚醒condition2執行緒池中的某個執行緒。
要使用這種鎖、監視器的關聯方式,參考如下步驟:
import java.util.concurrent.locks.*;
Lock l = new ReentrantLock();
Condition con1 = l.newCondition();
condition con2 = l.newCondition();
l.lock();
try{
//包含await()、signal()或signalAll()的程式碼段...
} finally {
l.unlock(); //由於程式碼段可能異常,但unlock()是必須執行的,所以必須使用try,且將unlock()放進finally段
}
具體用法見後文關於Lock、condition的示例程式碼。
3.單生產者單消費者模式
一個生產者執行緒,一個消費者執行緒,生產者每生產一個麵包放進盤子裡,消費者從盤子裡取出麵包進行消費。其中生產者判斷是否繼續生產的依據是盤子裡沒有面包,而消費者判斷是否消費的依據是盤子裡有面包。由於這個模式中,盤子一直只放一個麵包,因此可以把盤子省略掉,生產者和消費者直接手把手地交遞麵包即可。
首先需要描述這三個類,一是多執行緒共同操作的資源(此處即麵包),二是生產者,三是消費者。在下面的例子中,我把生產麵包和消費麵包的方法分別封裝到了生產者和消費者類中,如果把它們封裝在麵包類中則更容易理解。
//描述資源:麵包的名稱和編號,由編號決定麵包的號碼
class Bread {
public String name;
public int count = 1;
public boolean flag = false; //該標記為wait()和notify()提供判斷標記
}
//生產者和消費者先後處理的麵包資源是同一個,要確保這一點,
//可以按單例模式來設計麵包類,也可以將同一個麵包物件通過構造方法傳遞給生產者和消費者,此處使用後一種方式。
//描述生產者
class Producer implements Runnable {
private Bread b; //生產者的成員:它要處理的資源
Producer(Bread b){
this.b = b;
}
//提供生產麵包的方法
public void produce(String name){
b.name = name + b.count;
b.count++;
}
public void run(){
while(true){
synchronized(Bread.class){ //使用Bread.class作為鎖標識,使得生產者和消費者的同步程式碼塊可以使用同一個鎖
if(b.flag){ //wait()必須在同步程式碼塊內部,不僅因為必須持有鎖才能睡眠,而且對鎖這個資源的判斷會出現混亂
try{Bread.class.wait();}catch(InterruptedException i){}
}
produce("麵包");
System.out.println(Thread.currentThread().getName()+"----生產者------"+b.name);
try{Thread.sleep(10);}catch(InterruptedException i){}
b.flag = true; //標記的切換也必須在保持同步
Bread.class.notify(); //notify()也必須同步,否則鎖都已經釋放了,就無法做喚醒動作
//ps:一次同步任務中,wait()和notify()應當只能其中一個執行,否則對方執行緒會混亂
}
}
}
}
//描述消費者
class Consumer implements Runnable {
private Bread b; //消費者的成員:它要處理的資源
Consumer(Bread b){
this.b = b;
}
//提供消費麵包的方法
public String consume(){
return b.name;
}
public void run(){
while(true){
synchronized(Bread.class){
if(!b.flag){
try{Bread.class.wait();}catch(InterruptedException i){}
}
System.out.println(Thread.currentThread().getName()+"----消費者-------------"+consume());
try{Thread.sleep(10);}catch(InterruptedException i){}
b.flag = false;
Bread.class.notify();
}
}
}
}
public class ProduceConsume_1{
public static void main(String[] args) {
//1.建立資源物件
Bread b = new Bread();
//2.建立生產者和消費者物件,將同一個麵包物件傳遞給生產者和消費者
Producer pro = new Producer(b);
Consumer con = new Consumer(b);
//3.建立執行緒物件
Thread pro_t = new Thread(pro);
Thread con_t = new Thread(con);
pro_t.start();
con_t.start();
}
}
最後的執行結果應當生產一個、消費一個,如此不斷迴圈。如下:
Thread-0----生產者------麵包1
Thread-1----消費者-------------麵包1
Thread-0----生產者------麵包2
Thread-1----消費者-------------麵包2
Thread-0----生產者------麵包3
Thread-1----消費者-------------麵包3
Thread-0----生產者------麵包4
Thread-1----消費者-------------麵包4
Thread-0----生產者------麵包5
Thread-1----消費者-------------麵包5
Thread-0----生產者------麵包6
Thread-1----消費者-------------麵包6
4.使用Lock和Condition實現單生產單消費模式
程式碼如下:
import java.util.concurrent.locks.*;
class Bread {
public String name;
public int count = 1;
public boolean flag = false;
//為生產者和消費者提供同一個鎖物件以及同一個Condition物件
public static Lock lock = new ReentrantLock();
public static Condition condition = lock.newCondition();
}
class Producer implements Runnable {
private Bread b;
Producer(Bread b){
this.b = b;
}
public void produce(String name){
b.name = name + b.count;
b.count++;
}
public void run(){
while(true){
//使用Bread.lock來鎖住資源
Bread.lock.lock();
try{
if(b.flag){
try{Bread.condition.await();}catch(InterruptedException i){}
}
produce("麵包");
System.out.println(Thread.currentThread().getName()+"----生產者------"+b.name);
try{Thread.sleep(10);}catch(InterruptedException i){}
b.flag = true;
Bread.condition.signal();
} finally {
Bread.lock.unlock();
}
}
}
}
class Consumer implements Runnable {
private Bread b;
Consumer(Bread b){
this.b = b;
}
public String consume(){
return b.name;
}
public void run(){
while(true){
//使用Bread.lock來鎖住資源
Bread.lock.lock();
try{
if(!b.flag){
try{Bread.condition.await();}catch(InterruptedException i){}
}
System.out.println(Thread.currentThread().getName()+"----消費者-------------"+consume());
try{Thread.sleep(10);}catch(InterruptedException i){}
b.flag = false;
Bread.condition.signal();
} finally {
Bread.lock.unlock();
}
}
}
}
public class ProduceConsume_1{
public static void main(String[] args) {
//1.建立資源物件
Bread b = new Bread();
//2.建立生產者和消費者物件,將同一個麵包物件傳遞給生產者和消費者
Producer pro = new Producer(b);
Consumer con = new Consumer(b);
//3.建立執行緒物件
Thread pro_t = new Thread(pro);
Thread con_t = new Thread(con);
pro_t.start();
con_t.start();
}
}
5.多生產多消費模式(單面包)
這裡先說明多生產者多消費者,但同一個時刻最多隻能有一個麵包的模式,這個模式在實際中可能是不理想的,但為了引出後面真實的多生產多消費模式,我覺得有必要在這裡解釋這種模式,並且分析這種模式以及如何從單生產單消費的程式碼演變而來。
如下圖:
從單生產單消費到多生產多消費,因為多執行緒安全問題和死鎖問題,所以有兩個方面的問題需要考慮:
- 對於某一方來說,如何讓多執行緒達到和單執行緒同樣的生產或消費能力?也就是說,如何讓多執行緒看上去就是單執行緒。多執行緒和單執行緒最大的區別在於多執行緒安全問題,因此,只要保證多執行緒執行的任務能夠同步即可。
- 第1個問題考慮的是某一方多執行緒的問題,第2個問題考慮的是兩方如何能和諧配合完成生產消費問題。也就是如何保證生產方和消費方一方活動的同時另一方睡眠。只需在某一方執行完同步任務時,喚醒另一方即可。
其實從單執行緒到多執行緒,就兩個問題需要考慮:不同步和死鎖。(1)當生產方和消費方都出現了多執行緒,可以將生產方的多執行緒看成一個執行緒整體、消費方的多執行緒也看成一個整體,這解決的是執行緒安全問題。(2)再將生產方整體和消費方整體兩方結合起來看成多執行緒,來解決死鎖問題,而java中解決死鎖的方式就是喚醒對方或喚醒所有。
問題是如何保證某一方的多執行緒之間同步?以多執行緒執行單消費方的程式碼為例進行分析。
while(true){
synchronized(Bread.class){
if(!b.flag){
try{Bread.class.wait();}catch(InterruptedException i){}
}
System.out.println(Thread.currentThread().getName()+"----消費者-------------"+consume());
try{Thread.sleep(10);}catch(InterruptedException i){}
b.flag = false;
Bread.class.notify();
}
}
假設消費執行緒1消費完一個麵包後喚醒了消費執行緒2,並繼續迴圈,判斷if(!flag)
,它將wait,於是鎖被釋放。假設CPU正好選中了消費執行緒2,那麼消費執行緒2也將進入wait。當生產方生產了一個麵包後,假設喚醒了消費執行緒1,它將從wait語句處繼續向下消費剛生產完的麵包,假設正好再次喚醒了消費執行緒2,當消費執行緒2被CPU選中後,消費執行緒2也將從wait語句處向下消費,消費的也是剛才生產的麵包,問題再此出現了,連續喚醒的消費執行緒1和2消費的是同一個麵包,也就是說麵包被重複消費了。這又是多執行緒不同步問題。
說了一大段,其實將視線放大後分析就很簡單了,只要某一方的2個或多個執行緒都因為判斷b.flag而wait,那麼這兩個或多個執行緒有可能會被連續喚醒而繼續向下生產或消費。這造成了多執行緒不同步問題。
不安全的問題就出在同一方的多個執行緒在連續喚醒後繼續向下生產或消費。這是if語句引起的,如果能夠讓wait的執行緒在喚醒後還回頭判斷b.flag是否為true,就能讓其決定是否繼續wait還是向下生產或消費。
可以將if語句替換為while語句來滿足要求。這樣一來,無論某一方的多個執行緒是否被連續喚醒,它們都將回頭判斷b.flag。
while(true){
synchronized(Bread.class){
while(!b.flag){
try{Bread.class.wait();}catch(InterruptedException i){}
}
System.out.println(Thread.currentThread().getName()+"----消費者-------------"+consume());
try{Thread.sleep(10);}catch(InterruptedException i){}
b.flag = false;
Bread.class.notify();
}
}
解決了第一個多執行緒安全的問題,但會出現死鎖問題。這很容易分析,將生產方看作一個整體,將消費方也看作一個整體,當生產方執行緒都wait了(生產方的執行緒被連續喚醒時會出現該方執行緒全部wait),消費方也都wait了,死鎖就出現了。其實放大了看,將生產方、消費方分別看作一個執行緒,這兩個執行緒組成多執行緒,當某一方wait後無法喚醒另一方,另一方也一定會wait,於是就死鎖了。
對於雙方死鎖的問題,只要保證能喚醒對方,而非本方連續喚醒就能解決。使用notifyAll()或signalAll()
即可,也可以通過signal()
喚醒對方執行緒解決,見下面的第二段程式碼。
根據上面的分析,將單生產、單消費模式的程式碼改進一下,就可以變為多生產多消費單面包模式。、
//程式碼段1
class Bread {
public String name;
public int count = 1;
public boolean flag = false;
}
//描述生產者
class Producer implements Runnable {
private Bread b;
Producer(Bread b){
this.b = b;
}
public void produce(String name){
b.name = name + b.count;
b.count++;
}
public void run(){
while(true){
synchronized(Bread.class){
while(b.flag){
try{Bread.class.wait();}catch(InterruptedException i){}
}
produce("麵包");
System.out.println(Thread.currentThread().getName()+"----生產者------"+b.name);
try{Thread.sleep(10);}catch(InterruptedException i){}
b.flag = true;
Bread.class.notifyAll();
}
}
}
}
//描述消費者
class Consumer implements Runnable {
private Bread b;
Consumer(Bread b){
this.b = b;
}
public String consume(){
return b.name;
}
public void run(){
while(true){
synchronized(Bread.class){
while(!b.flag){
try{Bread.class.wait();}catch(InterruptedException i){}
}
System.out.println(Thread.currentThread().getName()+"----消費者-------------"+consume());
try{Thread.sleep(10);}catch(InterruptedException i){}
b.flag = false;
Bread.class.notifyAll();
}
}
}
}
public class ProduceConsume_5 {
public static void main(String[] args) {
//1.建立資源物件
Bread b = new Bread();
//2.建立生產者和消費者物件
Producer pro = new Producer(b);
Consumer con = new Consumer(b);
//3.建立執行緒物件
Thread pro_t1 = new Thread(pro); //生產執行緒1
Thread pro_t2 = new Thread(pro); //生產執行緒2
Thread con_t1 = new Thread(con); //消費執行緒1
Thread con_t2 = new Thread(con); //消費執行緒2
pro_t1.start();
pro_t2.start();
con_t1.start();
con_t2.start();
}
}
以下是採用Lock和Conditon重構後的程式碼,使用的是signal()
喚醒對方執行緒的方法。
//程式碼段2
import java.util.concurrent.locks.*;
class Bread {
public String name;
public int count = 1;
public boolean flag = false;
public static Lock lock = new ReentrantLock();
public static Condition pro_con = lock.newCondition();
public static Condition con_con = lock.newCondition();
}
//描述生產者
class Producer implements Runnable {
private Bread b;
Producer(Bread b){
this.b = b;
}
public void produce(String name){
b.name = name + b.count;
b.count++;
}
public void run(){
while(true){
Bread.lock.lock();
try{
while(b.flag){
try{Bread.pro_con.await();}catch(InterruptedException i){}
}
produce("麵包");
System.out.println(Thread.currentThread().getName()+"----生產者------"+b.name);
try{Thread.sleep(10);}catch(InterruptedException i){}
b.flag = true;
Bread.con_con.signal(); //喚醒的是consumer執行緒
} finally {
Bread.lock.unlock();
}
}
}
}
//描述消費者
class Consumer implements Runnable {
private Bread b;
Consumer(Bread b){
this.b = b;
}
public String consume(){
return b.name;
}
public void run(){
while(true){
Bread.lock.lock();
try{
while(!b.flag){
try{Bread.con_con.await();}catch(InterruptedException i){}
}
System.out.println(Thread.currentThread().getName()+"----消費者-------------"+consume());
try{Thread.sleep(10);}catch(InterruptedException i){}
b.flag = false;
Bread.pro_con.signal(); //喚醒的是producer執行緒
} finally {
Bread.lock.unlock();
}
}
}
}
public class ProduceConsume_6 {
public static void main(String[] args) {
//1.建立資源物件
Bread b = new Bread();
//2.建立生產者和消費者物件
Producer pro = new Producer(b);
Consumer con = new Consumer(b);
//3.建立執行緒物件
Thread pro_t1 = new Thread(pro);
Thread pro_t2 = new Thread(pro);
Thread con_t1 = new Thread(con);
Thread con_t2 = new Thread(con);
pro_t1.start();
pro_t2.start();
con_t1.start();
con_t2.start();
}
}
關於多生產、多消費問題做個總結:
- (1).解決某一方多執行緒不同步的方案是使用while(flag)來判斷是否wait;
- (2).解決雙方死鎖問題的方案是喚醒對方,可以使用notifyAll(),signalAll()或對方監視器的signal()方法。
6.多生產多消費模式
有多個生產者執行緒,多個消費者執行緒,生產者將生產的麵包放進籃子(集合或陣列)裡,消費者從籃子裡取出麵包。生產者判斷繼續生產的依據是籃子已經滿了,消費者判斷繼續消費的依據是籃子是否空了。此外,當消費者取出麵包後,對應的位置又空了,生產者可以回頭從籃子的起始位置繼續生產,這可以通過重置籃子的指標來實現。
在這個模式裡,除了描述生產者、消費者、麵包,還需要描述籃子這個容器。假設使用陣列作為容器,生產者每生產一個,生產指標向後移位,消費者每消費一個,消費指標向後移位。
程式碼如下:可參考API-->Condition類中給出的示例程式碼
import java.util.concurrent.locks.*;
class Basket {
private Bread[] arr;
//the size of basket
Basket(int size){
arr = new Bread[size];
}
//the pointer of in and out
private int in_ptr,out_ptr;
//how many breads left in basket
private int left;
private Lock lock = new ReentrantLock();
private Condition full = lock.newCondition();
private Condition empty = lock.newCondition();
//bread into basket
public void in(){
lock.lock();
try{
while(left == arr.length){
try{full.await();} catch (InterruptedException i) {i.printStackTrace();}
}
arr[in_ptr] = new Bread("MianBao",Producer.num++);
System.out.println("Put the bread: "+arr[in_ptr].getName()+"------into basket["+in_ptr+"]");
left++;
if(++in_ptr == arr.length){in_ptr = 0;}
empty.signal();
} finally {
lock.unlock();
}
}
//bread out from basket
public Bread out(){
lock.lock();
try{
while(left == 0){
try{empty.await();} catch (InterruptedException i) {i.printStackTrace();}
}
Bread out_bread = arr[out_ptr];
System.out.println("Get the bread: "+out_bread.getName()+"-----------from basket["+out_ptr+"]");
left--;
if(++out_ptr == arr.length){out_ptr = 0;}
full.signal();
return out_bread;
} finally {
lock.unlock();
}
}
}
class Bread {
private String name;
Bread(String name,int num){
this.name = name + num;
}
public String getName(){
return this.name;
}
}
class Producer implements Runnable {
private Basket basket;
public static int num = 1; //the first number for Bread's name
Producer(Basket b){
this.basket = b;
}
public void run(){
while(true) {
basket.in();
try{Thread.sleep(10);}catch(InterruptedException i){}
}
}
}
class Consumer implements Runnable {
private Basket basket;
private Bread i_get;
Consumer(Basket b){
this.basket = b;
}
public void run(){
while(true){
i_get = basket.out();
try{Thread.sleep(10);}catch(InterruptedException i){}
}
}
}
public class ProduceConsume_7 {
public static void main(String[] args) {
Basket b = new Basket(20); // the basket size = 20
Producer pro = new Producer(b);
Consumer con = new Consumer(b);
Thread pro_t1 = new Thread(pro);
Thread pro_t2 = new Thread(pro);
Thread con_t1 = new Thread(con);
Thread con_t2 = new Thread(con);
Thread con_t3 = new Thread(con);
pro_t1.start();
pro_t2.start();
con_t1.start();
con_t2.start();
con_t3.start();
}
}
這裡涉及了消費者、生產者、麵包和籃子,其中麵包和籃子是多執行緒共同操作的資源,生產者執行緒生產麵包放進籃子,消費者執行緒從籃子中取出麵包。理想的程式碼是將生產任務和消費任務都封裝在資源類中,因為麵包是籃子容器的元素,所以不適合封裝到麵包類中,而且封裝到籃子中,能更方便地操作容器。
注意,一定要將所有涉及資源操作的程式碼都放進鎖的內部,否則會產生多執行緒不同步問題。例如,在Producer類中定義了生產麵包的方法produce(),然後將其作為放進籃子的方法basket.in()的引數,即basket.in(producer()),這是錯誤的行為,因為produce()是在鎖的外部執行後才傳遞給in()方法的。
注:若您覺得這篇文章還不錯請點選右下角推薦,您的支援能激發作者更大的寫作熱情,非常感謝!