帶你走進多執行緒的世界(多執行緒實現方式)
做效能測試的同學使用最多的就是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狀態的執行緒在等待什麼資源,通常情況下是資料庫連線或者是日誌的輸出。