線程的建立和啓動:java
1.定義Thread類的子類,並重寫該類的run()方法,該run()方法的方法體就表明了線程須要完成的任務,所以把run()方法稱爲線程執行體安全
2.建立Thread子類的實例,即建立了線程的對象多線程
3.調用線程對象的start()方法來啓動該線程併發
1 public class FirstThread extends Thread{ 2 private int i; 3 //重寫run()方法,run()方法的方法體就是線程的執行體 4 public void run(){ 5 for(; i < 100; i++){ 6 //當線程類繼承Thread類時,直接使用this便可獲取當前線程 7 //Thread對象的getName9)返回當前線程的名字 8 //所以能夠直接調用getName()方法返回當前線程的名字 9 System.out.println(getName() + " " + i); 10 } 11 } 12 13 public static void main(String[] args){ 14 for(int i = 0; i < 100; i++){ 15 //調用Thread的currentThread()方法獲取當前線程 16 System.out.println(Thread.currentThread().getName() + " " + i); 17 if(i == 20){ 18 //建立並啓動第一個線程 19 new FirstThread().start(); 20 //建立並啓動第二個線程 21 new FirstThread().start(); 22 } 23 } 24 } 25 }
使用繼承Thread類的方法來建立線程類時,多個線程之間沒法共享線程類的實例變量。ide
實現Runnable接口建立線程類:函數
1.定義Runnable接口的實現類,並重寫該接口的run()方法,該run()方法體一樣是該線程的線程執行體。工具
2.建立Runnable實現類的實例,並以此實例做爲Thread的target來建立Thread對象,該Thread對象纔是真正的線程對象this
//建立Runnable實現類的對象spa
SecondThread st = new SecondThread();線程
//以Runnable實現類的對象做爲Thread的target來建立Thread對象,即線程對象
new Thread(st);
也能夠在建立Thread線程的時候爲該Thread對象指定一個名字:
new Thread(st, "新線程1");
Runnable對象僅僅做爲Thread對象的target,Runnable實現類裏包含的run()方法僅做爲線程執行體。而實際的線程對象依然是Thread實例,只是該Thread線程負責執行其
target的run()方法。
3.調用線程對象的start()方法來啓動該線程
1 public class SecondThread implements Runnable{ 2 private int i; 3 //run()方法一樣是線程執行體 4 public void run(){ 5 for(; i < 100; i++){ 6 //當線程類實現Runnable接口時 7 //若想獲取當前線程,只能用Thread.currentThread()方法 8 //所以能夠直接調用getName()方法返回當前線程的名字 9 System.out.println(Thread.currentThread().getName() + " " + i); 10 } 11 } 12 13 public static void main(String[] args){ 14 for(int i = 0; i < 100; i++){ 15 //調用Thread的currentThread()方法獲取當前線程 16 System.out.println(Thread.currentThread().getName() + " " + i); 17 if(i == 20){ 18 SecondThread st = new SecondThread(); 19 //經過new Thread(target, name)方法建立新線程 20 new Thread(st, "新線程1").start(); 21 new Thread(st, "新線程2").start(); 22 } 23 } 24 } 25 }
經過實現Runnable接口來得到當前線程對象,必須使用Thread.currentThread()方法。
Runnable接口只包含一個抽象方法,因此是函數式接口;Callable接口也是函數式接口。
從上面運行結果能夠看出,新線程1和新線程2的兩個i值沒有重複,這說明採用Runnable接口的方式建立的多線程能夠共享線程類的實例變量。這是由於在這種方式下,程
序所建立額Runnable對象只是線程的target,而多個線程能夠共享同一個target,因此多線程能夠共享同一個線程的target類的實例變量。
使用Callable和Future建立線程:
Callable接口提供了一個call()方法,能夠做爲線程執行體,但call()方法比run()方法更強大:
1.call()方法有返回值
2.call()方法能夠聲明拋出異常
能夠徹底提供一個Callable對象做爲Thread的target,而該線程的線程執行體就是該Callable對象的call()方法
Java5提供了Future接口來表明Callable接口裏call()方法的返回值,併爲接口Future提供了一個FutureTask實現類,該實現類實現了Future接口,並實現了Runnable接口——可
以做爲Thread類的target。
Callable接口有泛型限制,Callable接口裏的泛型形參類型與call()方法返回值類型相同。且Callable接口是函數式接口。
建立並啓動有返回值的線程的步驟以下:
1.建立Callable接口的實現類,並實現call()方法,該call()方法將做爲線程執行體,且該call()方法由返回值,在建立Callable實現類的實例。
2.使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了該Callable對象的call()方法的返回值
3.使用FutureTask對象做爲Thread對象的target建立並啓動新線程
4.調用FutureTask對象的get()方法來得到子線程執行結束後的返回值
1 import java.util.concurrent.FutureTask; 2 import java.util.concurrent.Callable; 3 4 public class ThirdThread{ 5 public static void main(String[] args){ 6 //先使用Lambda表達式建立Callable<Integer>對象 7 //使用FutureTask來包裝Callable對象 8 FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>) () -> { 9 int i = 0; 10 for( ; i < 100; i++){ 11 System.out.println(Thread.currentThread().getName() + " 的循環變量i的值:" + i); 12 } 13 //call()方法能夠有返回值 14 return i; 15 }); 16 for(int i = 0; i < 100; i++){ 17 System.out.println(Thread.currentThread().getName() + " 的循環變量i的值:" + i); 18 if(i == 20){ 19 //實質仍是以Callable對象來建立並啓動線程 20 new Thread(task, "有返回值的線程").start(); 21 } 22 } 23 24 try{ 25 //獲取線程返回值 26 System.out.println("子線程的返回值:" + task.get()); 27 }catch(Exception ex){ 28 ex.printStackTrace(); 29 } 30 } 31 }
1 public class InvokeRun extends Thread{ 2 private int i; 3 //重寫run()方法,run()方法的方法體就是線程執行體 4 public void run(){ 5 for( ; i < 100; i++){ 6 //直接調用run()方法時,Thread的this.getName()返回的是該對象的名字 7 //而不是當前線程的名字 8 //使用Thread.currentThread().getName()老是獲取當前線程的名字 9 System.out.println(Thread.currentThread().getName() + " " + i); 10 } 11 } 12 13 public static void main(String[] args){ 14 for(int i = 0; i < 100; i++){ 15 //調用Thread的currentThread()方法獲取當前線程 16 System.out.println(Thread.currentThread().getName() + " " + i); 17 if( i == 20){ 18 //直接調用線程對象的run()方法 19 //系統會把線程對象當成普通對象,把run()方法當成普通方法 20 //因此下面兩行代碼並不會啓動兩個線程,而是一次執行兩個run()方法 21 new InvokeRun().run(); 22 new InvokeRun().run(); 23 } 24 } 25 } 26 }
1 public class StartDead extends Thread{ 2 private int i; 3 //重寫run()方法,run()方法的方法體就是線程執行體 4 public void run(){ 5 for( ; i < 100; i++){ 6 System.out.println(getName() + " " + i); 7 } 8 } 9 10 public static void main(String[] args){ 11 //建立線程對象 12 StartDead sd = new StartDead(); 13 for(int i = 0; i < 300; i++){ 14 //調用Thread的currentThread()方法獲取當前線程 15 System.out.println(Thread.currentThread().getName() + " " + i); 16 if( i == 20){ 17 //啓動線程 18 sd.start(); 19 //判斷啓動後線程的isAlive()值,輸出true 20 System.out.println(sd.isAlive()); 21 } 22 //當線程處於新建、死亡兩種狀態時,isAlive()方法返回false 23 //當i > 20時,該線程確定已經啓動過了,若sd.isAlive()爲假時 24 //那麼就是死亡狀態了 25 if(i > 20 && !sd.isAlive()){ 26 //試圖再次啓動該線程 27 sd.start(); 28 } 29 } 30 } 31 }
不要對處於死亡狀態的線程調用start()方法,程序只能對新建狀態的線程調用start()方法,對新建線程調用兩次start()方法也是錯誤的。
控制線程:
join線程:
Thread提供了讓一個線程等待另外一個線程完成的方法——join()方法。
當在某個程序執行流中調用其餘線程的join()方法時,調用線程將被阻塞,直到被join()方法加入的join線程執行完爲止。
join()方法一般由使用線程的程序調用,以將大問題劃分紅許多小問題,每一個小問題分配一個線程。當全部小問題獲得處理後,再調用主線程進一步操做:
1 public class JoinThread extends Thread{ 2 //提供一個有參數的構造器,用於設置該線程的名字 3 public JoinThread(String name){ 4 super(name); 5 } 6 //重寫run()方法,定義線程執行體 7 public void run(){ 8 for(int i = 0; i < 100; i++){ 9 System.out.println(getName() + " " + i); 10 } 11 } 12 13 public static void main(String[] args) throws Exception{ 14 //啓動子線程 15 new JoinThread("新線程").start(); 16 for(int i = 0; i < 100; i++){ 17 18 if(i == 20){ 19 JoinThread jt = new JoinThread("被Join的線程"); 20 jt.start(); 21 //main線程調用了jt線程的join()方法,main線程必須等jt執行結束纔會向下執行 22 jt.join(); 23 } 24 System.out.println(Thread.currentThread().getName() + " " + i); 25 } 26 } 27 }
當主線程的循環變量i等於20時,啓動了名爲「被Join的線程」的線程,該線程不會和main線程併發執行,main必須等該線程執行結束後才能夠向下執行。
後臺線程:
在後臺運行,它的任務是爲其餘的線程提供服務,這種線程被稱爲「後臺線程」(Daemon Thread),JVM的垃圾回收線程就是典型的後臺線程。
後臺線程有個特徵:若全部的前臺線程都死亡,後臺線程會自動死亡。
調用Thread對象的setDaemon(true)方法可將指定線程設置成後臺線程。
1 public class DaemonThread extends Thread{ 2 //定義後臺線程的線程執行體與普通線程沒有任何區別 3 public void run(){ 4 for(int i = 0; i < 1000; i++){ 5 System.out.println(getName() + " " + i); 6 } 7 } 8 9 public static void main(String[] args){ 10 DaemonThread t = new DaemonThread(); 11 //將此線程設置成後臺線程 12 t.setDaemon(true); 13 //啓動後臺線程 14 t.start(); 15 for(int i = 0; i < 10; i++){ 16 System.out.println(Thread.currentThread().getName() + " " + i); 17 } 18 //------程序執行到此處,前臺線程(main線程)結束------ 19 //後臺線程也應該隨之結束 20 } 21 }
從程序中能夠看出主線程默認是前臺線程,前臺線程建立的子線程默認是前臺線程,後臺線程建立的子線程默認是後臺線程。
setDaemon(true)必須在start()方法以前調用。
線程睡眠:
讓當前正在執行的線程暫停一段時間,並進入阻塞狀態,可經過調用Thread類的靜態sleep()方法來實現。
1 import java.util.Date; 2 3 public class SleepTest{ 4 public static void main(String[] args) throws Exception{ 5 for(int i = 0; i < 10; i++){ 6 System.out.println("當前時間:" + new Date()); 7 //調用sleep()方法讓當前線程暫停1s 8 Thread.sleep(1000); 9 } 10 } 11 }
讓線程睡眠1s。
線程讓步:
yield()方法是一個和sleep()方法有點類似的方法,yield()也是Thread類提供的一個靜態方法。可讓當前正在執行的線程暫停,但不會阻塞該線程,只是將該線程轉入就緒狀態
實際上,當某個線程調用了yield()方法暫停以後,只有優先級和當前線程相同,或優先級比當前線程更高的處於就緒狀態的線程纔會得到執行的機會。
1 public class YieldTest extends Thread{ 2 public YieldTest(String name){ 3 super(name); 4 } 5 //定義run()方法做爲線程執行體 6 public void run(){ 7 for(int i = 0; i < 50; i++){ 8 System.out.println(getName() + " " + i); 9 //當i等於20時,使用yield()方法讓當前進程讓步 10 if(i == 20){ 11 Thread.yield(); 12 } 13 } 14 } 15 16 public static void main(String[] args){ 17 //啓動兩個併發線程 18 YieldTest yt1 = new YieldTest("高級"); 19 //將yt1線程設置成最高優先級 20 yt1.setPriority(Thread.MAX_PRIORITY); 21 yt1.start(); 22 YieldTest yt2 = new YieldTest("低級"); 23 //將yt2線程設置成最低級優先級 24 yt2.setPriority(Thread.MIN_PRIORITY); 25 yt2.start(); 26 } 27 }
因爲電腦是多CPU,因此yield()方法不明顯。
改變線程的優先級:
setPriority()方法用來改變線程的優先級:
1 public class PriorityTest extends Thread{ 2 //定義一個有參數的構造器,用於建立線程時指定name 3 public PriorityTest(String name){ 4 super(name); 5 } 6 public void run(){ 7 for(int i = 0; i < 50; i++){ 8 System.out.println(getName() + ",其優先級是:" + getPriority() + ",循環變量的值爲:" + i); 9 } 10 } 11 12 public static void main(String[] args){ 13 //改變主線程的優先級 14 Thread.currentThread().setPriority(6); 15 for(int i = 0; i < 30; i++){ 16 if(i == 10){ 17 PriorityTest low = new PriorityTest("低級"); 18 low.start(); 19 System.out.println("建立之初的優先級:" + low.getPriority()); 20 //設置該線程爲最低優先級 21 low.setPriority(Thread.MIN_PRIORITY); 22 } 23 if(i == 20){ 24 PriorityTest high = new PriorityTest("高級"); 25 high.start(); 26 System.out.println("建立之初的優先級:" + high.getPriority()); 27 //設置該線程爲最高優先級 28 high.setPriority(Thread.MAX_PRIORITY); 29 } 30 } 31 } 32 }
改變了主線程的優先級爲6,因此主線程所建立的子線程的優先級默認都是6。
線程同步:
線程安全問題:
有一個經典的問題——銀行取錢問題:
1.用戶輸入帳號、密碼,系統判斷用戶的帳號、密碼是否匹配。
2.用戶輸入取款金額
3.系統判斷帳戶餘額是否大於取款金額
4.若餘額大於取款金額,則取款成功;小於取款金額,則取款失敗
1 public class Account 2 { 3 // 封裝帳戶編號、帳戶餘額的兩個成員變量 4 private String accountNo; 5 private double balance; 6 public Account(){} 7 // 構造器 8 public Account(String accountNo , double balance) 9 { 10 this.accountNo = accountNo; 11 this.balance = balance; 12 } 13 // 此處省略了accountNo和balance的setter和getter方法 14 15 // accountNo的setter和getter方法 16 public void setAccountNo(String accountNo) 17 { 18 this.accountNo = accountNo; 19 } 20 public String getAccountNo() 21 { 22 return this.accountNo; 23 } 24 25 // balance的setter和getter方法 26 public void setBalance(double balance) 27 { 28 this.balance = balance; 29 } 30 public double getBalance() 31 { 32 return this.balance; 33 } 34 35 // 下面兩個方法根據accountNo來重寫hashCode()和equals()方法 36 public int hashCode() 37 { 38 return accountNo.hashCode(); 39 } 40 public boolean equals(Object obj) 41 { 42 if(this == obj) 43 return true; 44 if (obj != null && obj.getClass() == Account.class) 45 { 46 Account target = (Account)obj; 47 return target.getAccountNo().equals(accountNo); 48 } 49 return false; 50 } 51 }
1 public class DrawThread extends Thread 2 { 3 // 模擬用戶帳戶 4 private Account account; 5 // 當前取錢線程所但願取的錢數 6 private double drawAmount; 7 public DrawThread(String name , Account account 8 , double drawAmount) 9 { 10 super(name); 11 this.account = account; 12 this.drawAmount = drawAmount; 13 } 14 // 當多條線程修改同一個共享數據時,將涉及數據安全問題。 15 public void run() 16 { 17 // 帳戶餘額大於取錢數目 18 if (account.getBalance() >= drawAmount) 19 { 20 // 吐出鈔票 21 System.out.println(getName() 22 + "取錢成功!吐出鈔票:" + drawAmount); 23 24 try 25 { 26 Thread.sleep(1); 27 } 28 catch (InterruptedException ex) 29 { 30 ex.printStackTrace(); 31 } 32 33 // 修改餘額 34 account.setBalance(account.getBalance() - drawAmount); 35 System.out.println("\t餘額爲: " + account.getBalance()); 36 } 37 else 38 { 39 System.out.println(getName() + "取錢失敗!餘額不足!"); 40 } 41 } 42 }
1 public class DrawTest 2 { 3 public static void main(String[] args) 4 { 5 // 建立一個帳戶 6 Account acct = new Account("1234567" , 1000); 7 // 模擬兩個線程對同一個帳戶取錢 8 new DrawThread("甲" , acct , 800).start(); 9 new DrawThread("乙" , acct , 800).start(); 10 } 11 }
上面結果都是錯誤的,因此這就是線程的安全問題。
同步代碼塊:
上面代碼出錯的主要緣由是在執行run()方法的時候同一個線程沒有一次執行完畢,致使餘額老是大於800。
爲了解決上面出現的問題,Java的多線程支持引入同步監視器,使用同步件事器的通用方法就是同步代碼塊。格式以下:
synchronized(obj){
...
//此處的代碼就是同步代碼塊
}
上面格式中synchronized括號中的obj就是同步監視器,含義是:線程開始執行同步代碼塊以前,必須先得到對同步監視器的鎖定。
任什麼時候刻只能有一個線程能夠得到對同步監視器的鎖定,當同步代碼塊執行完成後,該線程會釋放對該同步監視器的鎖定。
同步監視器的目的:阻止兩個線程對同一個共享資源進行併發訪問,所以一般推薦使用可能被併發訪問的共享資源充當同步監視器,對於上面的取錢模擬程序,應考慮帳戶做爲
同步監視器:
1 public class DrawThread extends Thread 2 { 3 // 模擬用戶帳戶 4 private Account account; 5 // 當前取錢線程所但願取的錢數 6 private double drawAmount; 7 public DrawThread(String name , Account account 8 , double drawAmount) 9 { 10 super(name); 11 this.account = account; 12 this.drawAmount = drawAmount; 13 } 14 // 當多條線程修改同一個共享數據時,將涉及數據安全問題。 15 public void run() 16 { 17 // 使用account做爲同步監視器,任何線程進入下面同步代碼塊以前, 18 // 必須先得到對account帳戶的鎖定——其餘線程沒法得到鎖,也就沒法修改它 19 // 這種作法符合:「加鎖 → 修改 → 釋放鎖」的邏輯 20 synchronized (account) 21 { 22 // 帳戶餘額大於取錢數目 23 if (account.getBalance() >= drawAmount) 24 { 25 // 吐出鈔票 26 System.out.println(getName() 27 + "取錢成功!吐出鈔票:" + drawAmount); 28 try 29 { 30 Thread.sleep(1); 31 } 32 catch (InterruptedException ex) 33 { 34 ex.printStackTrace(); 35 } 36 // 修改餘額 37 account.setBalance(account.getBalance() - drawAmount); 38 System.out.println("\t餘額爲: " + account.getBalance()); 39 } 40 else 41 { 42 System.out.println(getName() + "取錢失敗!餘額不足!"); 43 } 44 } 45 // 同步代碼塊結束,該線程釋放同步鎖 46 } 47 }
上面程序使用synchronized將run()方法體修改爲同步代碼塊,該同步代碼塊的同步監視器是account對象。
同步方法:
就是使用synchronized關鍵字修飾某個方法。對於synchronized修飾的實例方法(非static方法)而言,無需顯式指定同步監視器,同步方法的同步監視器是this,也就是調用該
方法的對象。
1 public class Account 2 { 3 // 封裝帳戶編號、帳戶餘額兩個成員變量 4 private String accountNo; 5 private double balance; 6 public Account(){} 7 // 構造器 8 public Account(String accountNo , double balance) 9 { 10 this.accountNo = accountNo; 11 this.balance = balance; 12 } 13 14 // accountNo的setter和getter方法 15 public void setAccountNo(String accountNo) 16 { 17 this.accountNo = accountNo; 18 } 19 public String getAccountNo() 20 { 21 return this.accountNo; 22 } 23 // 所以帳戶餘額不容許隨便修改,因此只爲balance提供getter方法, 24 public double getBalance() 25 { 26 return this.balance; 27 } 28 29 // 提供一個線程安全draw()方法來完成取錢操做 30 public synchronized void draw(double drawAmount) 31 { 32 // 帳戶餘額大於取錢數目 33 if (balance >= drawAmount) 34 { 35 // 吐出鈔票 36 System.out.println(Thread.currentThread().getName() 37 + "取錢成功!吐出鈔票:" + drawAmount); 38 try 39 { 40 Thread.sleep(1); 41 } 42 catch (InterruptedException ex) 43 { 44 ex.printStackTrace(); 45 } 46 // 修改餘額 47 balance -= drawAmount; 48 System.out.println("\t餘額爲: " + balance); 49 } 50 else 51 { 52 System.out.println(Thread.currentThread().getName() 53 + "取錢失敗!餘額不足!"); 54 } 55 } 56 57 // 下面兩個方法根據accountNo來重寫hashCode()和equals()方法 58 public int hashCode() 59 { 60 return accountNo.hashCode(); 61 } 62 public boolean equals(Object obj) 63 { 64 if(this == obj) 65 return true; 66 if (obj !=null 67 && obj.getClass() == Account.class) 68 { 69 Account target = (Account)obj; 70 return target.getAccountNo().equals(accountNo); 71 } 72 return false; 73 } 74 }
1 public class DrawThread extends Thread 2 { 3 // 模擬用戶帳戶 4 private Account account; 5 // 當前取錢線程所但願取的錢數 6 private double drawAmount; 7 public DrawThread(String name , Account account 8 , double drawAmount) 9 { 10 super(name); 11 this.account = account; 12 this.drawAmount = drawAmount; 13 } 14 // 當多條線程修改同一個共享數據時,將涉及數據安全問題。 15 public void run() 16 { 17 // 直接調用account對象的draw方法來執行取錢 18 // 同步方法的同步監視器是this,this表明調用draw()方法的對象。 19 // 也就是說:線程進入draw()方法以前,必須先對account對象的加鎖。 20 account.draw(drawAmount); 21 } 22 }
1 public class DrawTest 2 { 3 public static void main(String[] args) 4 { 5 // 建立一個帳戶 6 Account acct = new Account("1234567" , 1000); 7 // 模擬兩個線程對同一個帳戶取錢 8 new DrawThread("甲" , acct , 800).start(); 9 new DrawThread("乙" , acct , 800).start(); 10 } 11 }
同步鎖:
java5提供了更強大的線程同步機制——經過顯式定義同步鎖對象來實現同步。
1 import java.util.concurrent.locks.*; 2 3 public class Account 4 { 5 // 定義鎖對象 6 private final ReentrantLock lock = new ReentrantLock(); 7 // 封裝帳戶編號、帳戶餘額的兩個成員變量 8 private String accountNo; 9 private double balance; 10 public Account(){} 11 // 構造器 12 public Account(String accountNo , double balance) 13 { 14 this.accountNo = accountNo; 15 this.balance = balance; 16 } 17 18 // accountNo的setter和getter方法 19 public void setAccountNo(String accountNo) 20 { 21 this.accountNo = accountNo; 22 } 23 public String getAccountNo() 24 { 25 return this.accountNo; 26 } 27 // 所以帳戶餘額不容許隨便修改,因此只爲balance提供getter方法, 28 public double getBalance() 29 { 30 return this.balance; 31 } 32 33 // 提供一個線程安全draw()方法來完成取錢操做 34 public void draw(double drawAmount) 35 { 36 // 加鎖 37 lock.lock(); 38 try 39 { 40 // 帳戶餘額大於取錢數目 41 if (balance >= drawAmount) 42 { 43 // 吐出鈔票 44 System.out.println(Thread.currentThread().getName() 45 + "取錢成功!吐出鈔票:" + drawAmount); 46 try 47 { 48 Thread.sleep(1); 49 } 50 catch (InterruptedException ex) 51 { 52 ex.printStackTrace(); 53 } 54 // 修改餘額 55 balance -= drawAmount; 56 System.out.println("\t餘額爲: " + balance); 57 } 58 else 59 { 60 System.out.println(Thread.currentThread().getName() 61 + "取錢失敗!餘額不足!"); 62 } 63 } 64 finally 65 { 66 // 修改完成,釋放鎖 67 lock.unlock(); 68 } 69 } 70 71 // 下面兩個方法根據accountNo來重寫hashCode()和equals()方法 72 public int hashCode() 73 { 74 return accountNo.hashCode(); 75 } 76 public boolean equals(Object obj) 77 { 78 if(this == obj) 79 return true; 80 if (obj !=null 81 && obj.getClass() == Account.class) 82 { 83 Account target = (Account)obj; 84 return target.getAccountNo().equals(accountNo); 85 } 86 return false; 87 } 88 }
在Account類中定義ReentrantLock對象,在draw()方法中上鎖,執行draw()方法後釋放鎖。
死鎖:
死鎖的狀況很容易發生,如在系統中出現多個同步監聽器:
1 class A 2 { 3 public synchronized void foo( B b ) 4 { 5 System.out.println("當前線程名: " + Thread.currentThread().getName() 6 + " 進入了A實例的foo()方法" ); // ① 7 try 8 { 9 Thread.sleep(200); 10 } 11 catch (InterruptedException ex) 12 { 13 ex.printStackTrace(); 14 } 15 System.out.println("當前線程名: " + Thread.currentThread().getName() 16 + " 企圖調用B實例的last()方法"); // ③ 17 b.last(); 18 } 19 public synchronized void last() 20 { 21 System.out.println("進入了A類的last()方法內部"); 22 } 23 } 24 class B 25 { 26 public synchronized void bar( A a ) 27 { 28 System.out.println("當前線程名: " + Thread.currentThread().getName() 29 + " 進入了B實例的bar()方法" ); // ② 30 try 31 { 32 Thread.sleep(200); 33 } 34 catch (InterruptedException ex) 35 { 36 ex.printStackTrace(); 37 } 38 System.out.println("當前線程名: " + Thread.currentThread().getName() 39 + " 企圖調用A實例的last()方法"); // ④ 40 a.last(); 41 } 42 public synchronized void last() 43 { 44 System.out.println("進入了B類的last()方法內部"); 45 } 46 } 47 public class DeadLock implements Runnable 48 { 49 A a = new A(); 50 B b = new B(); 51 public void init() 52 { 53 Thread.currentThread().setName("主線程"); 54 // 調用a對象的foo方法 55 a.foo(b); 56 System.out.println("進入了主線程以後"); 57 } 58 public void run() 59 { 60 Thread.currentThread().setName("副線程"); 61 // 調用b對象的bar方法 62 b.bar(a); 63 System.out.println("進入了副線程以後"); 64 } 65 public static void main(String[] args) 66 { 67 DeadLock dl = new DeadLock(); 68 // 以dl爲target啓動新線程 69 new Thread(dl).start(); 70 // 調用init()方法 71 dl.init(); 72 } 73 }
線程通訊:
1 public class Account 2 { 3 // 封裝帳戶編號、帳戶餘額的兩個成員變量 4 private String accountNo; 5 private double balance; 6 // 標識帳戶中是否已有存款的旗標 7 private boolean flag = false; 8 9 public Account(){} 10 // 構造器 11 public Account(String accountNo , double balance) 12 { 13 this.accountNo = accountNo; 14 this.balance = balance; 15 } 16 17 // accountNo的setter和getter方法 18 public void setAccountNo(String accountNo) 19 { 20 this.accountNo = accountNo; 21 } 22 public String getAccountNo() 23 { 24 return this.accountNo; 25 } 26 // 所以帳戶餘額不容許隨便修改,因此只爲balance提供getter方法, 27 public double getBalance() 28 { 29 return this.balance; 30 } 31 32 public synchronized void draw(double drawAmount) 33 { 34 try 35 { 36 // 若是flag爲假,代表帳戶中尚未人存錢進去,取錢方法阻塞 37 if (!flag) 38 { 39 wait(); 40 } 41 else 42 { 43 // 執行取錢 44 System.out.println(Thread.currentThread().getName() 45 + " 取錢:" + drawAmount); 46 balance -= drawAmount; 47 System.out.println("帳戶餘額爲:" + balance); 48 // 將標識帳戶是否已有存款的旗標設爲false。 49 flag = false; 50 // 喚醒其餘線程 51 notifyAll(); 52 } 53 } 54 catch (InterruptedException ex) 55 { 56 ex.printStackTrace(); 57 } 58 } 59 public synchronized void deposit(double depositAmount) 60 { 61 try 62 { 63 // 若是flag爲真,代表帳戶中已有人存錢進去,則存錢方法阻塞 64 if (flag) //① 65 { 66 wait(); 67 } 68 else 69 { 70 // 執行存款 71 System.out.println(Thread.currentThread().getName() 72 + " 存款:" + depositAmount); 73 balance += depositAmount; 74 System.out.println("帳戶餘額爲:" + balance); 75 // 將表示帳戶是否已有存款的旗標設爲true 76 flag = true; 77 // 喚醒其餘線程 78 notifyAll(); 79 } 80 } 81 catch (InterruptedException ex) 82 { 83 ex.printStackTrace(); 84 } 85 } 86 87 // 下面兩個方法根據accountNo來重寫hashCode()和equals()方法 88 public int hashCode() 89 { 90 return accountNo.hashCode(); 91 } 92 public boolean equals(Object obj) 93 { 94 if(this == obj) 95 return true; 96 if (obj !=null 97 && obj.getClass() == Account.class) 98 { 99 Account target = (Account)obj; 100 return target.getAccountNo().equals(accountNo); 101 } 102 return false; 103 } 104 }
1 public class DrawThread extends Thread 2 { 3 // 模擬用戶帳戶 4 private Account account; 5 // 當前取錢線程所但願取的錢數 6 private double drawAmount; 7 public DrawThread(String name , Account account 8 , double drawAmount) 9 { 10 super(name); 11 this.account = account; 12 this.drawAmount = drawAmount; 13 } 14 // 重複100次執行取錢操做 15 public void run() 16 { 17 for (int i = 0 ; i < 100 ; i++ ) 18 { 19 account.draw(drawAmount); 20 } 21 } 22 }
1 public class DepositThread extends Thread 2 { 3 // 模擬用戶帳戶 4 private Account account; 5 // 當前取錢線程所但願存款的錢數 6 private double depositAmount; 7 public DepositThread(String name , Account account 8 , double depositAmount) 9 { 10 super(name); 11 this.account = account; 12 this.depositAmount = depositAmount; 13 } 14 // 重複100次執行存款操做 15 public void run() 16 { 17 for (int i = 0 ; i < 100 ; i++ ) 18 { 19 account.deposit(depositAmount); 20 } 21 } 22 }
1 public class DrawTest 2 { 3 public static void main(String[] args) 4 { 5 // 建立一個帳戶 6 Account acct = new Account("1234567" , 0); 7 new DrawThread("取錢者" , acct , 800).start(); 8 new DepositThread("存款者甲" , acct , 800).start(); 9 new DepositThread("存款者乙" , acct , 800).start(); 10 new DepositThread("存款者丙" , acct , 800).start(); 11 } 12 }
上面程序表明存款者和取款者,存款者將錢存入帳戶喚醒取款者,取款者從帳戶取出錢後喚醒全部線程。取款者是隨機的。程序使用synchronized方法,flag標誌位,wait()
方法、notify()方法、notifyAll()方法進行同步操做。
使用Condition控制線程通訊:
Condition和Lock對象協做,實現上面程序的功能。這時候Lock替代了同步方法或同步代碼塊,Condition替代了同步監視器的功能。
Condition實例被綁定在一個Lock對象上。要得到特定Lock實例的Condition實例,調用Lock對象的newCondition()方法便可。
修改上面程序的Account代碼:
1 import java.util.concurrent.*; 2 import java.util.concurrent.locks.*; 3 4 public class Account 5 { 6 // 顯式定義Lock對象 7 private final Lock lock = new ReentrantLock(); 8 // 得到指定Lock對象對應的Condition 9 private final Condition cond = lock.newCondition(); 10 // 封裝帳戶編號、帳戶餘額的兩個成員變量 11 private String accountNo; 12 private double balance; 13 // 標識帳戶中是否已有存款的旗標 14 private boolean flag = false; 15 16 public Account(){} 17 // 構造器 18 public Account(String accountNo , double balance) 19 { 20 this.accountNo = accountNo; 21 this.balance = balance; 22 } 23 24 // accountNo的setter和getter方法 25 public void setAccountNo(String accountNo) 26 { 27 this.accountNo = accountNo; 28 } 29 public String getAccountNo() 30 { 31 return this.accountNo; 32 } 33 // 所以帳戶餘額不容許隨便修改,因此只爲balance提供getter方法, 34 public double getBalance() 35 { 36 return this.balance; 37 } 38 39 public void draw(double drawAmount) 40 { 41 // 加鎖 42 lock.lock(); 43 try 44 { 45 // 若是flag爲假,代表帳戶中尚未人存錢進去,取錢方法阻塞 46 if (!flag) 47 { 48 cond.await(); 49 } 50 else 51 { 52 // 執行取錢 53 System.out.println(Thread.currentThread().getName() 54 + " 取錢:" + drawAmount); 55 balance -= drawAmount; 56 System.out.println("帳戶餘額爲:" + balance); 57 // 將標識帳戶是否已有存款的旗標設爲false。 58 flag = false; 59 // 喚醒其餘線程 60 cond.signalAll(); 61 } 62 } 63 catch (InterruptedException ex) 64 { 65 ex.printStackTrace(); 66 } 67 // 使用finally塊來釋放鎖 68 finally 69 { 70 lock.unlock(); 71 } 72 } 73 public void deposit(double depositAmount) 74 { 75 lock.lock(); 76 try 77 { 78 // 若是flag爲真,代表帳戶中已有人存錢進去,則存錢方法阻塞 79 if (flag) // ① 80 { 81 cond.await(); 82 } 83 else 84 { 85 // 執行存款 86 System.out.println(Thread.currentThread().getName() 87 + " 存款:" + depositAmount); 88 balance += depositAmount; 89 System.out.println("帳戶餘額爲:" + balance); 90 // 將表示帳戶是否已有存款的旗標設爲true 91 flag = true; 92 // 喚醒其餘線程 93 cond.signalAll(); 94 } 95 } 96 catch (InterruptedException ex) 97 { 98 ex.printStackTrace(); 99 } 100 // 使用finally塊來釋放鎖 101 finally 102 { 103 lock.unlock(); 104 } 105 } 106 107 // 下面兩個方法根據accountNo來重寫hashCode()和equals()方法 108 public int hashCode() 109 { 110 return accountNo.hashCode(); 111 } 112 public boolean equals(Object obj) 113 { 114 if(this == obj) 115 return true; 116 if (obj !=null 117 && obj.getClass() == Account.class) 118 { 119 Account target = (Account)obj; 120 return target.getAccountNo().equals(accountNo); 121 } 122 return false; 123 } 124 }
程序經過上鎖和Condition控制實現存款和取款
使用阻塞隊列(BlockingQueue)控制線程通訊:
BlockingQueue是Queue的子接口,是線程同步的工具。BlockingQueue具備一個特徵:當生產者線程試圖向BlockingQueue中放入元素時,若該隊列已滿,則該線程被阻塞;
當消費者線程試圖從BlockingQueue中取出元素時,若該隊列已空,則該線程被阻塞。
1 import java.util.concurrent.*; 2 3 class Producer extends Thread 4 { 5 private BlockingQueue<String> bq; 6 public Producer(BlockingQueue<String> bq) 7 { 8 this.bq = bq; 9 } 10 public void run() 11 { 12 String[] strArr = new String[] 13 { 14 "Java", 15 "Struts", 16 "Spring" 17 }; 18 for (int i = 0 ; i < 99 ; i++ ) 19 { 20 System.out.println(getName() + "生產者準備生產集合元素!"); 21 try 22 { 23 Thread.sleep(200); 24 // 嘗試放入元素,若是隊列已滿,線程被阻塞 25 bq.put(strArr[i % 3]); 26 } 27 catch (Exception ex){ex.printStackTrace();} 28 System.out.println(getName() + "生產完成:" + bq); 29 } 30 } 31 } 32 class Consumer extends Thread 33 { 34 private BlockingQueue<String> bq; 35 public Consumer(BlockingQueue<String> bq) 36 { 37 this.bq = bq; 38 } 39 public void run() 40 { 41 while(true) 42 { 43 System.out.println(getName() + "消費者準備消費集合元素!"); 44 try 45 { 46 Thread.sleep(200); 47 // 嘗試取出元素,若是隊列已空,線程被阻塞 48 bq.take(); 49 } 50 catch (Exception ex){ex.printStackTrace();} 51 System.out.println(getName() + "消費完成:" + bq); 52 } 53 } 54 } 55 public class BlockingQueueTest2 56 { 57 public static void main(String[] args) 58 { 59 // 建立一個容量爲1的BlockingQueue 60 BlockingQueue<String> bq = new ArrayBlockingQueue<>(1); 61 // 啓動3條生產者線程 62 new Producer(bq).start(); 63 new Producer(bq).start(); 64 new Producer(bq).start(); 65 // 啓動一條消費者線程 66 new Consumer(bq).start(); 67 } 68 }
線程組和未處理的異常:
Java使用ThreadGroup來表示線程組,能夠對一批線程進行分類管理,Java容許程序直接對線程組進行控制。
1 class MyThread extends Thread 2 { 3 // 提供指定線程名的構造器 4 public MyThread(String name) 5 { 6 super(name); 7 } 8 // 提供指定線程名、線程組的構造器 9 public MyThread(ThreadGroup group , String name) 10 { 11 super(group, name); 12 } 13 public void run() 14 { 15 for (int i = 0; i < 20 ; i++ ) 16 { 17 System.out.println(getName() + " 線程的i變量" + i); 18 } 19 } 20 } 21 public class ThreadGroupTest 22 { 23 public static void main(String[] args) 24 { 25 // 獲取主線程所在的線程組,這是全部線程默認的線程組 26 ThreadGroup mainGroup = Thread.currentThread().getThreadGroup(); 27 System.out.println("主線程組的名字:" 28 + mainGroup.getName()); 29 System.out.println("主線程組是不是後臺線程組:" 30 + mainGroup.isDaemon()); 31 new MyThread("主線程組的線程").start(); 32 ThreadGroup tg = new ThreadGroup("新線程組"); 33 tg.setDaemon(true); 34 System.out.println("tg線程組是不是後臺線程組:" 35 + tg.isDaemon()); 36 MyThread tt = new MyThread(tg , "tg組的線程甲"); 37 tt.start(); 38 new MyThread(tg , "tg組的線程乙").start(); 39 } 40 }
1 // 定義本身的異常處理器 2 class MyExHandler implements Thread.UncaughtExceptionHandler 3 { 4 // 實現uncaughtException方法,該方法將處理線程的未處理異常 5 public void uncaughtException(Thread t, Throwable e) 6 { 7 System.out.println(t + " 線程出現了異常:" + e); 8 } 9 } 10 public class ExHandler 11 { 12 public static void main(String[] args) 13 { 14 // 設置主線程的異常處理器 15 Thread.currentThread().setUncaughtExceptionHandler 16 (new MyExHandler()); 17 int a = 5 / 0; // ① 18 System.out.println("程序正常結束!"); 19 } 20 }