Java併發程式設計系列(一)-執行緒的基本使用
最近在學習java併發程式設計基礎.一切從簡,以能理解概念為主.
併發程式設計肯定繞不過執行緒.這是最基礎的.
那麼就從在java中,如何使用執行緒開始.
繼承Thread類
繼承Thread類,重寫run方法,new出物件,呼叫start方法.
在新啟的執行緒裡執行的就是重寫的run方法.
1 /** 2 * 整合Thread類 實現run() 3 */ 4 public class C1 extends Thread { 5 6 @Override 7 public void run() { 8 try { 9 Thread.sleep(100);10 } catch (InterruptedException e) { 11 e.printStackTrace(); 12 } 13 System.out.println(Thread.currentThread().getName() + " run()"); 14 } 15 16 public static void main(String[] args) { 17 C1 c1 = new C1(); 18 c1.start(); 19 } 20 }
run方法裡先睡100毫秒,然後列印當前執行緒名稱+run()
執行結果:
實現Runnable介面
實現Runnable介面run方法
1 /** 2 * 實現Runnable介面run() 3 */ 4 public class C2 implements Runnable { 5 6 @Override 7 public void run() { 8 try { 9 Thread.sleep(100); 10 } catch (InterruptedException e) { 11 e.printStackTrace(); 12 }13 System.out.println(Thread.currentThread().getName() + " run()"); 14 } 15 16 public static void main(String[] args) { 17 C2 c2 = new C2(); 18 new Thread(c2, "thread0").start(); 19 } 20 }
執行結果:
Lambda表示式
Lambda表示式本質上還是實現了Runnable的run方法,但是寫起來相當的方便.注:java8或以上版本才支援
java中Lambda表示式的語法:
1.(parameters) -> expression
2.(parameters) ->{ statements; }
3.物件名::方法名 注:方法名後不加小括號
1 /** 2 * Lambda表示式 java8或以上版本 3 */ 4 public class C3 { 5 6 public void foo() { 7 try { 8 Thread.sleep(100); 9 } catch (InterruptedException e) { 10 e.printStackTrace(); 11 } 12 System.out.println(Thread.currentThread().getName() + " foo()"); 13 } 14 15 public static void main(String[] args) { 16 C3 c3 = new C3(); 17 new Thread(() -> c3.foo()).start(); 18 new Thread(() -> { 19 c3.foo(); 20 }).start(); 21 new Thread(c3::foo).start(); 22 } 23 }
執行結果:
可以看到三種寫法是等價的.執行無引數的方法,用第三種雙冒號的方式最簡單,有引數的可以用第一種方式,執行多行的程式碼片段只能用第二種方式.
start() run() join()
首先看一段程式碼吧
1 /** 2 * start() run() join() 3 */ 4 public class C4 { 5 6 public void foo() { 7 try { 8 Thread.sleep(100); 9 } catch (InterruptedException e) { 10 e.printStackTrace(); 11 } 12 System.out.println(Thread.currentThread().getName() + " foo()"); 13 } 14 15 public static void main(String[] args) { 16 C4 c4 = new C4(); 17 //一個執行緒不能同時執行多次start() 否側會丟擲IllegalThreadStateException 18 Thread tStart=new Thread(c4::foo); 19 tStart.start(); 20 tStart.start(); 21 //同時執行多次start()沒丟擲IllegalThreadStateException 因為不在同一執行緒內 是new出來的 22 new Thread(c4::foo).start(); 23 new Thread(c4::foo).start(); 24 25 try { 26 Thread.sleep(1000); 27 } catch (InterruptedException e) { 28 e.printStackTrace(); 29 } 30 System.out.println("---分割線---"); 31 //run() 呼叫的就是重寫的那個run 所以沒有開啟執行緒 是在主執行緒裡執行的 32 new Thread(c4::foo).run(); 33 34 System.out.println("---分割線---"); 35 SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss:SSS"); 36 Thread tJoin = new Thread(c4::foo); 37 tJoin.start(); 38 System.out.println(simpleDateFormat.format(new Date())); 39 try { 40 //阻塞程式碼 等待執行緒執行完 41 tJoin.join(); 42 } catch (InterruptedException e) { 43 e.printStackTrace(); 44 } 45 System.out.println(simpleDateFormat.format(new Date())); 46 } 47 }
start()
先看main方法裡的前5行,在同一時間兩次呼叫同一個執行緒的start().
執行結果:
丟擲了IllegalThreadStateException非法的執行緒狀態異常.也就是說在同一執行緒裡,不能同時多次執行同一段程式碼.這很好理解了,同時多次執行同一段程式碼應該用多個執行緒.
註釋掉tStart相關程式碼,繼續執行
執行結果:
第一部分結果說明了同時多次執行同一段程式碼應該用多個執行緒.
start方法開啟新執行緒,見原始碼(刪掉源註釋):
1 public synchronized void start() { 2 //判斷執行緒狀態 丟擲IllegalThreadStateException 3 if (threadStatus != 0) 4 throw new IllegalThreadStateException(); 5 6 group.add(this); 7 8 boolean started = false; 9 try { 10 //這裡開啟 11 start0(); 12 started = true; 13 } finally { 14 try { 15 if (!started) { 16 group.threadStartFailed(this); 17 } 18 } catch (Throwable ignore) { 19 } 20 } 21 }
可以看到呼叫start首先會判斷執行緒狀態,不是0的話,丟擲非法的執行緒狀態異常.
真正開啟執行緒是在start0()這裡.
轉到定義可以看到start呼叫了本地方法,也就是真正開啟執行緒是c語言寫的.那麼什麼引數都沒有.底層這麼知道開啟執行緒呼叫什麼方法呢.
最上面,Thread類有個靜態建構函式是類在例項化的時候,呼叫registerNatives本地方法,將自己註冊進去的.
run()
第二部分是呼叫的run方法,顯示的執行緒名是主執行緒.就是說呼叫run方法並不會開啟新執行緒.
看看到底是怎麼回事.
導航到原始碼可以看到,Thread類是實現了Runnable介面的.重寫了run方法.
如果用繼承Thread類的方式寫執行緒類,要重寫run方法.先不管原始碼重寫的run是什麼,那麼你的重寫會覆蓋掉原始碼重寫的run.這種情況下,new出自己的執行緒類,然後呼叫run,當然和執行緒沒有任何關係,就是在主執行緒裡呼叫了另一個類的普通的方法.
如果用實現Runnable介面的方式寫執行緒類,那麼會new一個這個介面的例項,傳到new出的Thread物件裡.繼續往下傳到init方法,最終賦值到target變數上
結合上面原始碼重寫的run,那麼它還是呼叫了你傳過來的Runnable例項的run方法.
join()
join就是阻塞程式碼,等待執行緒執行完後再開繼續始執行當前程式碼
可以看到第三部分結果,第一次輸出時間和第二次輸出時間相差了109毫秒(執行緒內睡了100毫秒).如果不用join阻塞,時間一般相差的會很小,當然具體是多少也不一定,得看機器當時的執行情況.用join相差多少毫秒也不一定,但至少一定不會小於100毫秒.(可自行多次嘗試).