1. 程式人生 > >理解多執行緒 (篇一)

理解多執行緒 (篇一)

執行緒:通常每一個任務稱為一個執行緒(Thread),他是執行緒控制的簡稱,執行緒是CPU最小的執行單元,也可以理解為是一個程式裡面不同的執行路徑。每個執行緒都有獨立的執行棧和程式計數器(PC)。Java虛擬機器一般預設有兩個執行緒,一個是主執行緒main,另一個是垃圾回收執行緒。

既然CPU同一時間只能處理一個執行緒,那為什麼我們平時在電腦上一邊聽歌一邊看電影呢,那是因為CPU等概率的在各個執行緒中相互切換的頻率特別快,快到我們感官無法感知。執行緒排程的細節依賴於作業系統的服務,

執行緒的執行狀態

執行緒的三種建立方式:

One:

  • 寫一個類繼承Thread類,實現run()方法,然後使用該類物件的start方法來啟動執行緒。(一般很少用這種方式,除非Thread類寫的方法不滿足你的需求)

Two:

  • 寫一個類來實現Runnable介面,實現run()方法,建立該類物件並作為引數傳遞給Thread類,並呼叫start方法開始啟動執行緒。

Three:

  • 建立一個匿名內部類物件,呼叫start方法來開啟執行緒。

例:

package net.csdn.qf.thread;
/** 
 * @author 北冥有熊
 *  2018年11月6日
 */
public class ThreadTest {
	public static void main(String[] args) {
		//第一種方式啟動
		new Demo().start(); //建立Demo物件並呼叫start方法啟動執行緒
		//第二種方式啟動
		Demo1 demo1 = new Demo1(); //建立Demo1物件
		new Thread(demo1).start(); //將物件作為引數傳遞給Thread,並呼叫start方法開始啟動執行緒
		//第三種方式啟動
		new Thread(new Runnable() { //直接使用匿名內部類物件來作為構造引數來建立執行緒
			
			@Override
			public void run() {
				// TODO Auto-generated method stub
				for (int i=1; i<=50; i++) {
					System.out.println("第3種執行緒執行--->"+i+"(3)");
				}
			}
		}).start();
	}
}
//第一種類
class Demo extends Thread{
	@Override
	public void run() {
		// TODO Auto-generated method stub
		for (int i=1; i<=50; i++) {
			System.out.println("第1種執行緒執行--->"+i+"(1)");
		}
	}
}
//第二種類
class Demo1 implements Runnable{
	@Override
	public void run() {
		// TODO Auto-generated method stub
		for (int i=1; i<=50; i++) {
			System.out.println("第2種執行緒執行--->"+i+"(2)");
		}
	}
}


獲取執行緒物件及名稱

currentThread:獲取當前執行緒物件
currentThread.getName: 獲取當前執行緒的名字
currentThread.getId:獲取當前執行緒的Id

下面以買票為例:

package net.csdn.qf.thread;
/**
 * @author 北冥有熊
 *  2018年11月6日
 */
public class Test01 {
	public static void main(String[] args) {
		Ticket ticket = new Ticket();
		new Thread(ticket,"小明").start();
		new Thread(ticket,"郭靖").start();
		new Thread(ticket,"黃蓉").start();
		new Thread(ticket,"楊康").start();
	}
}
class Ticket implements Runnable{
	int num = 100;
	@Override
	public void run() {
		while(num>=1) {
			if(num>=1) {
				try {
					Thread.sleep(50);//睡眠500ms
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				Thread a = Thread.currentThread();//建立當前執行緒物件
				System.out.println("當前執行緒物件為: "+a.getName()+"   賣出的票號是--->"+num);//輸出當前執行緒的名字
				num--;
			}
		}
	}
	
}

結果是:

當前執行緒物件為: 小明   賣出的票號是--->20                                                  
當前執行緒物件為: 郭靖   賣出的票號是--->20      當前執行緒物件為: 郭靖   賣出的票號是--->8
當前執行緒物件為: 楊康   賣出的票號是--->20      當前執行緒物件為: 小明   賣出的票號是--->8
當前執行緒物件為: 黃蓉   賣出的票號是--->20      當前執行緒物件為: 黃蓉   賣出的票號是--->8
當前執行緒物件為: 郭靖   賣出的票號是--->16      當前執行緒物件為: 小明   賣出的票號是--->4
當前執行緒物件為: 黃蓉   賣出的票號是--->16      當前執行緒物件為: 楊康   賣出的票號是--->4
當前執行緒物件為: 小明   賣出的票號是--->16      當前執行緒物件為: 小明   賣出的票號是--->0
當前執行緒物件為: 楊康   賣出的票號是--->16      當前執行緒物件為: 楊康   賣出的票號是--->0
當前執行緒物件為: 郭靖   賣出的票號是--->12      當前執行緒物件為: 郭靖   賣出的票號是--->0 
當前執行緒物件為: 楊康   賣出的票號是--->12      當前執行緒物件為: 郭靖   賣出的票號是--->-1
當前執行緒物件為: 黃蓉   賣出的票號是--->12                                               
當前執行緒物件為: 小明   賣出的票號是--->12

很明顯,不同執行緒物件賣出了重複的票甚至還出現負數票,這在日常的買票系統中是絕對不能允許的。

出現復票和負票的原因:

復值原因:當執行到輸出 當前執行緒執行物件的時候,一個執行緒物件(黃蓉)執行完輸出語句後,還未執行num--語句時,第二個執行緒物件(郭靖)緊接著也執行到了該輸出語句,這就導致了出現復票的原因。

負值原因:當num==0時,一個執行緒物件(黃蓉)執行完輸出語句後,再執行完num-- 語句,第二個執行緒物件(郭靖)緊接著也執行到了輸出語句,這時先執行的執行緒物件(黃蓉)因為已經執行了num自減語句,此時num已經變為-1,緊跟第一個執行緒物件其後的第二個執行緒物件輸出的是 num== -1,這就是出現負值的原因。

多執行緒安全問題

當多個執行緒同時操作同一個共享資料時,操作資料包括判斷,修改,同一個執行緒在沒有處理完資料的時候別的執行緒參與了資料運算,導致資料發生異常。

synchronized:

在Java中,synchronized關鍵字是用來控制執行緒同步的,就是在多執行緒的環境下,控制synchronized程式碼段不被多個執行緒同時執行。synchronized既可以加在一段程式碼上,也可以加在方法上。

同步程式碼塊:將一次只希望一個執行緒物件處理的程式碼塊寫在synchronized(this){  }的方法體內 ,此處this表示當前物件,意思是隻允許當前this物件執行方法體內的程式碼塊,表示一個通行證(加鎖)。對於static的synchronized方法,因為沒有this物件,因此鎖的就是這個類的Class物件,如:synchronized(XXX.class){  程式碼塊 }。

同步方法:使用synchronized修飾方法,在呼叫該方法前,需要獲得內建鎖(java每個物件都有一個內建鎖),否則就處於阻塞狀態。例如:public synchronized void save(){//內容}。

下面以多執行緒物件在銀行取款為例,解釋說明同步程式碼塊與同步方法。

package net.csdn.qf.test;
/**
 * @author 北冥有熊
 *  2018年11月7日
 */
public class Test {
	public static void main(String[] args) {
		Bank bank = new Bank();//銀行物件
		Account account = new Account(bank); //賬戶物件
		new Thread(account,"郭靖").start();
		new Thread(account,"黃蓉").start();
		new Thread(account,"楊康").start();
	}
}
//銀行存款
class Bank{
	int money = 1000;
}
//賬戶
class Account implements Runnable{
	static Bank bank = null;
	public Account(Bank bank) {
		Account.bank = bank;
	}
	@Override
	public void run() {
		// TODO Auto-generated method stub
		while(bank.money>=100) {
			synchronized (this) { //同步程式碼塊,加鎖只允許當前this訪問。
                                              //此時,所有被this鎖住的同步程式碼塊以及同步方法都被同步鎖住。
				if(bank.money>=100) {
					try {
						Thread.sleep(500);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					String name = Thread.currentThread().getName(); //建立當前執行緒物件
					System.out.println("run當前執行緒名:"+name+"  餘額--->"+bank.money);
					bank.money-=100; //每次取100
				}
			}
			
			tes();//呼叫靜態同步方法tes(),因為上述同步程式碼塊鎖住的是this,
                            //而靜態方法沒有this,所以tes方法並沒有被鎖住,任然出現執行緒安全問題。
		}
	}
	public static synchronized void tes() { //靜態同步方法,因為沒有this,所以只能被Account.class鎖住。  
		if(bank.money>=100) {
			try {
				Thread.sleep(500);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			String name = Thread.currentThread().getName(); //建立當前執行緒物件
			System.out.println("tes當前執行緒名:"+name+"  餘額--->"+bank.money);
			bank.money-=100; //每次取100
		}
	}
	
}

因為synchronized(this)沒有鎖住tes()方法,故結果仍有重複值,當將this換為Account.class鎖住後,執行緒安全問題得到解決。

            用this鎖住                          用Account.class鎖住

run當前執行緒名:郭靖  餘額--->1000           run當前執行緒名:郭靖  餘額--->1000
tes當前執行緒名:郭靖  餘額--->900            tes當前執行緒名:郭靖  餘額--->900
run當前執行緒名:黃蓉  餘額--->900            run當前執行緒名:郭靖  餘額--->800
tes當前執行緒名:黃蓉  餘額--->700            run當前執行緒名:黃蓉  餘額--->700
run當前執行緒名:楊康  餘額--->700            tes當前執行緒名:黃蓉  餘額--->600
run當前執行緒名:黃蓉  餘額--->500            run當前執行緒名:楊康  餘額--->500
tes當前執行緒名:楊康  餘額--->500            tes當前執行緒名:楊康  餘額--->400
run當前執行緒名:郭靖  餘額--->300            run當前執行緒名:楊康  餘額--->300
tes當前執行緒名:黃蓉  餘額--->300            tes當前執行緒名:楊康  餘額--->200
run當前執行緒名:楊康  餘額--->100            run當前執行緒名:黃蓉  餘額--->100
tes當前執行緒名:郭靖  餘額--->100            

擴充套件(面試題):

大家都知道,對於單例餓漢模式來說,當程式開始執行時,常量池中已經建立了一個不可變物件,因此不用擔心多執行緒安全問題。但對於懶漢單例模式而言,就沒有那麼好了。。。

例:

package net.csdn.qf.test;
/**
 * @author 北冥有熊
 *  2018年11月7日
 */
public class Test01 {
	public static void main(String[] args) {
		int num = 0;
		while(num<=50) {
			new Thread(new Runnable() {
				
				@Override
				public void run() {
					//EHan.getEHan(); //餓漢模式,因為物件在常量池中共享,所以共享
                                                        //不會出現多執行緒安全問題
					LHan.getLHan(); //懶漢模式,出現多執行緒安全問題
				}
			}).start();
			num++;
		}
	}
}
//餓漢單例
class EHan{
	private static final EHan E_Han = new EHan();
	private EHan() {
		System.out.println("建立了餓漢物件");
	}
	public static EHan getEHan() {
		return E_Han;
	}
}
//懶漢單例
class LHan{
	private static LHan lanHan = null;
	private LHan() {
		System.out.println("建立了懶漢物件");
	}
	public static LHan getLHan() {
		if(lanHan==null) {
			lanHan = new LHan();
		}
		return lanHan;
	}
}

結果:

      餓漢模式                      懶漢模式
 
    建立了餓漢物件                建立了懶漢物件
                                建立了懶漢物件
                                建立了懶漢物件
                                建立了懶漢物件
                              

原因還是一樣,在懶漢模式下進入if判斷語句時,當建立物件語句還沒有執行完,其餘在外面等的三個也已經判斷了if(nanhan==null),因此建立了多個物件,解決辦法就是加上同步程式碼塊。面試題的重點來了.......

加上同步程式碼塊後,雖然多執行緒安全問題解決了,但也緊接著出現另一個問題,執行緒阻塞。當執行緒達到一定數量的時候,第一個想成通過同步程式碼塊建立物件後,在其後面的執行緒都會判斷兩次(第一次:判斷鎖;第一次:判斷物件是否為空),執行緒數量多的時候就會出現阻塞。

解決辦法:雙重判斷來減少比較次數。

class LHan{
	private static LHan lanHan = null;
	private LHan() {
		System.out.println("建立了懶漢物件");
	}
	public static LHan getLHan() {
		if(lanHan==null) { //雙重判斷,解決執行緒阻塞
			synchronized (LHan.class) { //加鎖
				if(lanHan==null) {
					lanHan = new LHan();
				}
			}
		}
		return lanHan;
	}
}

儘管在平時處理多執行緒安全問題中基本都用餓漢單例模式,懶漢單例模式被忽略,但這卻是一道檢查基本功的面試題,要理解哦。到時候面試官問你時,可別說我沒有說哦!
  
更多內容見篇二!