【Java】多執行緒複習

TypantK發表於2019-02-23

目錄


0.Create a Thread

方法①

方法②(常用)

兩種建立方式區別

多執行緒記憶體示意圖

start和run方法的區別

1.The Status of Thread

2.Security Problems in Multithreading(多執行緒)

3.Four kinds of usage of Synchroized(四種修飾)

需要同步的地方

程式碼塊

方法

靜態方法

一個類

4.Multithreading Security Problems in (多執行緒安全問題)

                   Lazy/Hungry Man singleton pattern(單例餓漢/懶漢)

餓漢(沒有新物件,用getInstance來獲得)

懶漢(懶得建立新物件,用之前有的)

解決方法:

5.Consumer and Producer Problem(生產者-消費者問題)

背景前瞻

產生問題

等待喚醒機制(wait() / notify() / notifyAll())

多消費者、多生產者問題

6.Details in Multithreading

sleep()和wait()的異同點

執行緒如何停止

守護(後臺)執行緒

優先順序&執行緒組

join() & yield()

微妙的地方

 

 

0.Create a Thread


方法①

繼承Thread類,重寫run()方法,呼叫start方法開啟執行緒(也就是呼叫重寫的run方法)

 

 

方法②(常用)

實現Runnable介面,重寫run方法,將實現類作為引數傳遞給Thread物件,最後呼叫start方法開啟執行緒(也就是呼叫run方法)

Demo d = new Demo();    //Demo類實現了Runnable介面,但他還不是執行緒物件
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
t1.start();
t2.start();

Thread原始碼

class Thread{
    private Runnable target;

    Thread(Runnable target){
        this.target = target;
    }

    public void run(){
        if(target != null){
            target.run();
        }
    }
}

**所有的執行緒共享實現類的成員變數

比如Demo中有一個成員變數ticket=100,t1/t2的run方法都可以對其進行操作,而且數值會儲存

 

兩種建立方式區別

方法①:執行緒任務和執行緒物件耦合在一起,一旦建立了Thread物件,就代表建立了執行緒任務以及執行緒物件,也就是有執行緒任務就有物件了

方法②:執行緒分為兩部分:執行緒物件(Thread物件)+執行緒任務(實現Runnable介面的類),將執行緒任務和執行緒物件解耦,此方式更加物件導向,也就更常用

 

多執行緒記憶體示意圖

public static void main(String[] args){
    Demo d = new Demo();

    Thread t1 = new Thread(d);
    Thread t2 = new Thread(d);

    t1.run();    //由當前執行緒負責
    t2.start();    //由新建立的Thread-1負責
}

 

start和run方法的區別

呼叫run方法不開啟執行緒,僅是物件呼叫方法,由當前執行緒負責方法的執行;

呼叫start方法開啟執行緒,並讓jvm呼叫run方法在新開啟的執行緒中負責方法執行。

 

 

1.The Status of Thread


*參考地址:https://www.cnblogs.com/happy-coder/p/6587092.html

 

1. 新建狀態(New)         : 執行緒物件被建立後,就進入了新建狀態。例如,Thread thread = new Thread()。
2. 就緒狀態(Runnable): 也被稱為“可執行狀態”。執行緒物件被建立後,其它執行緒呼叫了該物件的start()方法,從而來啟動該執行緒。例如,thread.start()。處於就緒狀態的執行緒,隨時可能被CPU排程執行。
3. 執行狀態(Running) : 執行緒獲取CPU許可權進行執行。需要注意的是,執行緒只能從就緒狀態進入到執行狀態。
4. 阻塞狀態(Blocked)  : 阻塞狀態是執行緒因為某種原因放棄CPU使用權,暫時停止執行。直到執行緒進入就緒狀態,才有機會轉到執行狀態。阻塞的情況分三種:
    (01) 等待阻塞 -- 通過呼叫執行緒的wait()方法,讓執行緒等待某工作的完成。
    (02) 同步阻塞 -- 執行緒在獲取synchronized同步鎖失敗(因為鎖被其它執行緒所佔用),它會進入同步阻塞狀態。
    (03) 其他阻塞 -- 通過呼叫執行緒的sleep()或join()或發出了I/O請求時,執行緒會進入到阻塞狀態。當sleep()狀態超時、join()等待執行緒終止或者超時、或者I/O處理完畢時,執行緒重新轉入就緒狀態。
5. 死亡狀態(Dead)    : 執行緒執行完了或者因異常退出了run()方法,該執行緒結束生命週期。

 

 

2.Security Problems in Multithreading(多執行緒)


*根本原因也就是對共享資料操作

而要解決此多執行緒安全問題就用到了同步鎖

public void run(){
    Object obj = new Object();

    synchroized(obj){
        //需要同步的程式碼塊,對共享資料的操作
    }

}

線上程執行run方法之前,會要求得到物件obj,而obj只有一個。

只要其中一個執行緒A得到了此物件obj,即使CPU切換執行緒B來執行,執行緒B得不到物件obj也就不能執行下去。

而執行緒A在執行完程式碼塊之後,就會將物件obj返回,其他執行緒就可以得到物件obj了。

 

使用同步鎖弊端:降低了程式的效能(切換程式時得不到物件obj-->不做事)

 

可能出現的問題:同步中用的不是同一把鎖(物件obj不是同一個),導致同步失效

 

 

3.Four kinds of usage of Synchroized(四種修飾)


需要同步的地方

同步裡面一般都是迴圈,如果不是迴圈,瞬間執行完了就沒必要多執行緒了

一般都是同步操作共享資料的程式碼,所以用同步來修飾的地方既不能過大(可能變成單執行緒),也不能過小(執行緒安全問題)

 

程式碼塊

被修飾的程式碼塊稱為同步語句塊,其作用的範圍是大括號{}括起來的程式碼,作用的物件是呼叫這個程式碼塊的物件(this),可以在括號內指定鎖Synchroized(Obj obj){},鎖是obj

 

方法

被修飾的方法稱為同步方法,其作用的範圍是整個方法,作用的物件是呼叫這個方法的物件(this)

 

靜態方法

作用的範圍是整個靜態方法,作用的物件是這個類的所有物件(類.class這個物件)

 

一個類

其作用的範圍是synchronized後面括號括起來的部分,作用主的物件是這個類的所有物件(類.class這個物件)

 

 

4.Multithreading Security Problems in (多執行緒安全問題)

                   Lazy/Hungry Man singleton pattern(單例餓漢/懶漢)


餓漢(沒有新物件,用getInstance來獲得)

class Single{
    
    private static Single s = new Single();
    
    private Single();

    public static Single getInstance(){
        
            return s;
    }

}

 

懶漢(懶得建立新物件,用之前有的)

**多執行緒併發問題:執行緒0判斷完s=null,CPU切換執行權到執行緒1判斷s=null後建立物件s,切換到執行緒0之後又建立物件s

class Single{
    
    private static Single s = null;
    
    private Single();

    //加入synchronized解決多執行緒同步問題,但是影響了效率
    public static /*synchronized*/ Single getInstance(){
        if(s==null){
            s = new Single();
            return s;
        }
    }

}

 

解決方法:

加入synchronized(解決併發問題)

如上述程式碼註釋部分

 

雙重判斷,減少判斷鎖的次數(解決加同步後效率低下問題)

減少判斷鎖的次數:顧名思義,也就是減少執行synchronized()這一行或synchronized修飾的方法

雙重判斷最核心的地方:第一個執行緒建立完物件之後,其他執行緒不用再判斷鎖(因為在第一個判斷中就已經出去了)

class Single{
    
    private static Single s = null;
    
    private Single();

   //雙重判斷
    public static  Single getInstance(){
        if(s==null){
            synchronized(Single.class){
                if(s==null){
                    s = new Single();
                    return s;
                }
            }
        }else return s;
    }

}

 

 

5.Consumer and Producer Problem(生產者-消費者問題)


背景前瞻

生產者執行緒每生產一個資源,由消費者執行緒消費,然後生產者執行緒再生產……(一個生產者、一個消費者)

class Resource{
	private String name;
	
	public synchronized void set(String name) {
		
		this.name = name;
		System.out.println(Thread.currentThread().getName() + "生產了" + this.name);

	}
	
	public synchronized void out() {
		System.out.println(Thread.currentThread().getName() + "消費了" + this.name);

	}
}

class Customer implements Runnable{

	private Resource r ;
	
	Customer(Resource r){
		this.r = r;
	}
	
	
	public void run() {
		while(true) {
			r.set("麵包");
		}
	}
	
}

class Producer implements Runnable{

	private Resource r ;
	
	Producer(Resource r){
		this.r = r;
	}
	
	
	public void run() {
		while(true) {
			r.out();
		}
		
	}
	
}

public class PAndC {
	public static void main(String[] args) {
		Resource r = new Resource();
		Producer p1 = new Producer(r);
		Customer c1 = new Customer(r);
		
		Thread t1 = new Thread(p1);
		Thread t2 = new Thread(c1);
		
		t1.start();
		t2.start();
		
	}
}

 

產生問題

生產者生產多次,才到消費者消費多次

*解決:等待喚醒機制

通過flag標記,如果flag為真(消費者wait(),生產者執行並notify()),如果flag為假(生產者wait(),消費者執行並notify())

class Resource{
	private String name;
	private boolean flag = false;
	
	public synchronized void set(String name) {
		if(flag)try {wait();}catch(Exception e) {};
		this.name = name;
		System.out.println(Thread.currentThread().getName() + "生產了" + this.name);
		flag = true;
		notify();
	}
	
	public synchronized void out() {
		if(!flag)try {wait();}catch(Exception e) {};
		System.out.println(Thread.currentThread().getName() + "消費了" + this.name);
		flag = false;
		notify();
	}
}

 

 

等待喚醒機制(wait() / notify() / notifyAll())

**這三個方法必須使用在同步(synchronized)中,因為要標識這些方法所屬的鎖,通過鎖來判斷wait()或者notify()哪個執行緒池

wait():將呼叫此方法的執行緒臨時儲存到執行緒池中

notify():會喚醒執行緒池中任一 一個執行緒

notifyAll():會喚醒執行緒池中所有執行緒

 

*同一個鎖上的notify(),只能喚醒該鎖上的被wait()的執行緒

 

上述解決方法中,對資源物件(單例)鎖上,呼叫set的程式0,呼叫out的程式1分別臨時儲存到執行緒池中,因為只有一個資源物件,所以可以判斷是同一個執行緒池。

 

多消費者、多生產者問題

*問題:消費者喚醒的物件可能是消費者,也可能是生產者(我們需要的是喚醒不同方)

喚醒同方會導致生產兩次 or 消費兩次

*解決:while替代if的判斷(多消費者、生產者必備while)

 

*新問題:替代之後導致死迴圈。eg:生產者喚醒生產者之後,佔著資源,死迴圈(消費者還未將標識變換,本應是喚醒消費者的)

 

*解決:喚醒所有(notifyAll()),但是效率低,因為又喚醒了本方

 

*效率低解決辦法:jdk1.5之後提供的java.util.concurrent.locks代替同步

Lock介面:lock()獲得鎖、unlock()釋放鎖 用於代替synchronized

Condition介面:await(),singal(),singalAll() 用於代替wait()、notify()、notifyAll()

*同作業系統中的P/V操作是一個概念,Condition物件就是訊號量

class Resource{
	private String name;
	private boolean flag = false;    //判斷是否需要停止生產者或者消費者的標識
	private Lock lock = new ReentrantLock();
	private Condition p = lock.newCondition();	//生產
	private Condition c = lock.newCondition();	//消費
	
	public  void set(String name) {
		lock.lock();
		try {
			while(flag)try {p.await();}catch(Exception e) {};
			this.name = name;
			System.out.println(Thread.currentThread().getName() + "生產了" + this.name);
			flag = true;
			c.signal();
		}finally {
			lock.unlock();
		}
		
	}
	
	public  void out() {
		lock.lock();
		try {
			while(!flag)try {c.await();}catch(Exception e) {};
			System.out.println(Thread.currentThread().getName() + "消費了" + this.name);
			flag = false;
			p.signal();
		}finally {
			lock.unlock();
		}
		
	}
}

 

 

6.Details in Multithreading


 

sleep()和wait()的異同點

  • sleep()必須指定時間;wait()可以指定也可以不指定時間
  • sleep()時間到,執行緒處於臨時阻塞或執行狀態;wait()如果沒有時間,需要通過notify()或notifyAll()喚醒
  • sleep()不一定要定義在同步中;wait()必須定義在同步中
  • 定義在同步中時,執行緒執行sleep(),不會釋放鎖(其他執行緒執行不了);執行緒執行到wait(),會釋放鎖

 

 

執行緒如何停止

stop():過時,因為執行之後會釋放其擁有的所有鎖,會引發其他問題,所以被棄用

*interrupt():將執行緒的凍結狀態清除,讓執行緒恢復到正常執行狀態,因為是強制性的所以會自帶異常

**將notify()比喻作喚醒一個睡覺的人;interrupt()就是一棍子打醒睡覺的人,打出來的包就是異常。

執行緒結束:讓run()結束(執行緒處於凍結[wait()]狀態時無法判斷標識flag,沒人喚醒執行緒)

  • 方法①:run()中通常都是迴圈,線上程外面控制一個標識來控制迴圈就可以讓執行緒結束
  • 方法②:interrupt()打斷之後在catch(){...}中控制標識來控制迴圈來讓執行緒結束

 

守護(後臺)執行緒

和一般執行緒(前臺)一樣,在啟動執行緒之前需要(thread.setDaemon(true))就可以變成後臺執行緒

*特點:執行緒結束方式:

  • 執行完run()方法
  • 所有前臺執行緒執行完-->程式執行完

 

 

優先順序&執行緒組

預設優先順序5,數字表示1-10,明顯的優先順序1,5,10

serPriority(Thread.MAX_PRIORITY);

 

執行緒組:可以對多個同組的執行緒,進行統一的操作(比如interrupt())

 

 

join() & yield()

t1.join():主執行緒釋放執行權,讓其餘的執行緒搶奪執行權,只有在t1執行完之後主執行緒才能獲得執行權(處於凍結狀態)

yield():釋放CPU執行權,讓其他執行緒有機會獲取(自己也能再次獲取)。讓執行緒放緩,增加間隔性

 

 

微妙的地方

new Thread(new Runnable(){
      public void run(){
            System.out.println("runnable run");
      }
}){
      public void run(){
            System.out.println("subthread run");
      }
}.start();

new Thread() {.....}建立了Thread的子類重寫了父類的run方法

父類的run方法原本是:

class Thread{
    private Runnable r;
    
    Thread(Runnable r){
        this.r = r;
    }
    
    public void run(){
        if(r != null){
            r.run();
        }
    }

    public void start(){
        run();
    }

}

所以重寫了之後,最後輸出的結果的subthread run

相關文章