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毫秒.(可自行多次嘗試).
Java並發編程系列(一)-線程的基本使用