1. 程式人生 > >帶你走進多執行緒的世界(多執行緒實現方式)

帶你走進多執行緒的世界(多執行緒實現方式)

做效能測試的同學使用最多的就是LoadRunner和Jemter工具了吧,能夠使用洪荒之力模擬多使用者同時請求伺服器,來觀察伺服器端的負載情況並定位效能瓶頸,聽上去挺高大上的。無論任何效能工具,核心原理都離不開多執行緒。如何實現多執行緒?如何定位異常狀態的執行緒找到效能瓶頸呢?別急,開始我們的多執行緒之旅吧~

什麼是多執行緒?

舉個簡單的例子,比如你去一家餐館吃飯,餐館只有一個服務員,那麼這個服務員給你點菜的時候,別的人就得等著。但如果這個餐廳有3個服務員A,B,C,那麼同一時刻就可以給3個顧客(甲乙丙)去點菜,每個顧客點了不同的2道菜。我們把餐館理解成一個程序,服務員A,B,C理解為3個執行緒,後廚做菜的廚師是cpu(假設是單核的,一個cpu)。

從A,B,C 三個服務員同時接待 3個顧客(甲乙丙)這個表象看執行緒是同步,併發執行的,但是廚師在做菜的過程中是有先後之分的,廚師會把甲乙丙三個人的菜分開來做,做完甲的菜,立刻開始做乙的菜,乙的菜可能需要時間蒸的時候,會去做丙的菜,就這樣不停的切換做著甲乙丙三個顧客的菜,而在甲乙丙顧客看來他們桌子上都有菜吃,誤以為是同事做出來的。但嚴格意義上講,同一時刻只有一個執行緒執行,但是使用者會覺得是多個執行緒同時執行的。

Java多執行緒實現

java多執行緒實現主要有三種方式:繼承thread類,實現runnable介面,使用ExecutorService、Callable、Future實現有返回結果的多執行緒。其中前兩種方式執行緒執行完後都沒有返回值,只有最後一種是帶返回值的。這裡我們只談前兩種。

1、繼承Thread類實現多執行緒

我們先看一個例子:

package thread;

public class Thread_1 extends Thread{
	
	private String name;

	public Thread_1(){
		
	}
	
	public Thread_1(String name){
		this.name = name ; 
		
	}

	public void run(){
		for (int i = 0;i<5 ; i++){
			System.out.println(name + "執行" +i);
		}
	}
	
	public static void main(String[] args){
		Thread_1 h1 = new Thread_1("A");
		Thread_1 h2 = new Thread_1("B");
		h1.run();
		h2.run();
		
	}
}

我們看一下執行結果:

A執行0

A執行1

A執行2

A執行3

A執行4

B執行0

B執行1

B執行2

B執行3

B執行4

我們會發現這些都是順序執行的,並沒有多執行緒執行,為什麼呢?因為我們直接呼叫了run方法,啟動執行緒唯一的方法是通過Thread類呼叫start()方法,start()方法是一個native方法,即本地作業系統方法,它將啟動一個新執行緒,並執行run()方法,我們通過自己的類直接extend Thread,並複寫run()方法,就可以啟動新執行緒並執行自己定義的run()方法。

如果看一些start的原始碼就會更容易理解:

public synchronized void start() {
        /**
     * This method is not invoked for the main method thread or "system"
     * group threads created/set up by the VM. Any new functionality added
     * to this method in the future may have to also be added to the VM.
     *
     * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0 || this != me)
            throw new IllegalThreadStateException();
        group.add(this);
        start0();
        if (stopBeforeStart) {
        stop0(throwableFromStop);
    }
}
private native void start0();

我們看最後,說明此處呼叫的start0(),這個方法用了native關鍵字。

2、通過實現Runnable介面

Thread本質上也是實現了Runnable介面的一個例項,如果自己的類已經extends另一個類,就無法直接繼承thread類,必須實現一個Runnable介面。

我們在看一個小例子:

package thread;

public class runnable_2 implements Runnable{
	
	private String name ; 
	public runnable_2(){
		
	}

	public runnable_2(String name){
		this.name = name;
	}
	@Override
	public void run() {
		// TODO Auto-generated method stub
		for(int i =0;i <5 ;i++){
			System.out.println(name +"執行"+i);
		}
		
	}
	public static void main(String[] args){
		runnable_2 h1 = new runnable_2("執行緒A");
		Thread demo = new Thread(h1);
		runnable_2 h2 = new runnable_2("執行緒B");
		Thread demo1 = new Thread(h2);
		demo.start();
		demo1.start();
	}

}

執行結果:

執行緒A執行0

執行緒B執行0

執行緒B執行1

執行緒A執行1

執行緒B執行2

執行緒A執行2

執行緒B執行3

執行緒A執行3

執行緒B執行4

執行緒A執行4

我們是選擇thread 類還是實現runnable介面呢?

其實thread類也是實現Runnable介面的:

class Thread implements Runnable {
    //…
public void run() {
        if (target != null) {
             target.run();
        }
        }
}

Thread和Runnable的區別?

如果一個類繼承Thread,則不適合資源共享,如果實現了Runnable介面,則很容易資源共享。

看下面的例子:

package thread;
/**
 * 繼承Thread類,不能資源共享
 * @author shangwei
 *
 */
public class thread_share_3 extends Thread{
		private int count = 5;
		public void run(){
			for(int i=0;i<10;i++){
				if(count >0){
					System.out.println(Thread.currentThread().getName()+"="+count--);
				}
			}
		}
		public static void main(String[] args){
			thread_share_3 h1 = new thread_share_3();
			thread_share_3 h2 = new thread_share_3();
			thread_share_3 h3 = new thread_share_3();
			h1.start();
			h2.start();
			h3.start();
			
		}

}


執行介面

Thread-1=5

Thread-3=5

Thread-2=5

Thread-3=4

Thread-1=4

Thread-3=3

Thread-2=4

Thread-2=3

Thread-3=2

Thread-1=3

Thread-3=1

Thread-2=2

Thread-1=2

Thread-1=1

Thread-2=1

我們把count看成是一個需要共享的資源,比如我們的搶票系統,系統裡有5張票,3個人去搶票,實際上每個人都搶了5張票。一共15張票。

我們換成Runnale介面試一下:
package thread;

public class runnable_share_4 implements Runnable {
	
	private int count = 5;
	
	public static void main(String[] args){
		runnable_share_4 h1 = new runnable_share_4();
		Thread t1 = new Thread(h1,"1號視窗");
		t1.start();
		Thread t2 = new Thread(h1,"2號視窗");
		t2.start();
		Thread t3 = new Thread(h1,"3號視窗");
		t3.start();
		
	}

	@Override
	public void run() {
		for(int i=0;i<10;i++){
			if(count >0){
				System.out.println("count"+"正在賣"+count--);
			}
		}
		// TODO Auto-generated method stub
		
	}

}



執行結果:

count正在賣5

count正在賣3

count正在賣4

count正在賣1

count正在賣2

這裡我們看到3個執行緒都共享了同一個例項,實現了資源的共享,3個執行緒搶5張票。
那麼為什麼Thread類不能共享同一個例項呢?我們試試唄。
</pre><pre code_snippet_id="1856515" snippet_file_name="blog_20160829_6_1221599" name="code" class="java" style="font-size: 14px;">package thread;

public class Thread_1 extends Thread{
	
	private String name;

	public Thread_1(){
		
	}
	
	public Thread_1(String name){
		this.name = name ; 
		
	}

	public void run(){
		for (int i = 0;i<5 ; i++){
			System.out.println(name + "執行" +i);
		}
	}
	
	public static void main(String[] args){
		Thread_1 h1 = new Thread_1("A");
		Thread_1 h2 = new Thread_1("B");
		//h1.run();
		//h2.run();
		h1.start();
		h1.start();
		
		
	}
}

在main方法裡,我們對同一個例項都start()開啟執行緒,看會出現什麼情況呢?

執行結果:

A執行0Exception in thread "main"

A執行1

A執行2

A執行3

A執行4

java.lang.IllegalThreadStateException

at java.lang.Thread.start(Thread.java:671)

at thread.Thread_1.main(Thread_1.java:28)

總結一下吧:

實現Runnable介面比繼承Thread類所具有的優勢:

1):適合多個相同的程式程式碼的執行緒去處理同一個資源

2):可以避免java中的單繼承的限制

3):增加程式的健壯性,程式碼可以被多個執行緒共享,程式碼和資料獨立。

執行緒排程 執行緒有5種基本狀態: 1、新建狀態(new):當建立執行緒物件後,就進入了新建狀態,比如:Thread t  = new Thread(); 2、 就緒狀態(runnable):當執行緒物件被建立後,其他執行緒呼叫了該執行緒的start()方法,該執行緒就進入了佇列,說明此執行緒做好了準備,隨時等待cpu排程執行。 3、執行狀態(running)當cpu時間片分給就緒狀態的執行緒時,該執行緒就真正的執行起來,進入了執行狀態。 4、阻塞狀態(blocked)阻塞狀態是因為執行緒因為某種原因放棄了cpu的使用權,暫時停止執行,直到執行緒進入就緒狀態,才有機會執行。 阻塞狀態分為3種情況: 等待阻塞:執行狀態中的執行緒執行wait()方法,使本執行緒進入到等待阻塞狀態;
同步阻塞 -- 執行緒在獲取synchronized同步鎖失敗(因為鎖被其它執行緒所佔用),它會進入同步阻塞狀態;
其他阻塞 -- 通過呼叫執行緒的sleep()或join()或發出了I/O請求時,執行緒會進入到阻塞狀態。當sleep()狀態超時、join()等待執行緒終止或者超時、或者I/O處理完畢時,執行緒重新轉入就緒狀態。
5、死亡狀態(Dead):執行緒執行完了或者因異常退出了run()方法,該執行緒結束生命週期。

處於阻塞狀態的執行緒往往存在效能瓶頸,也是我們需要特別關注的。如果是因為競爭同步鎖引發的死鎖或者的響應時間的延長,需要找到這些處於blocked狀態的執行緒在等待什麼資源,通常情況下是資料庫連線或者是日誌的輸出。