一個程序在運行起來時,會轉換為進程,通常含有多個線程。
通常情況下,一個進程中的比較耗時的操作(如長循環、文件上傳下載、網絡資源獲取等),往往會采用多線程來解決。
比如,現實生活中,銀行取錢問題、火車票多個窗口售票問題等,通常會涉及並發問題,從而需要用到多線程技術。
當進程中有多個並發線程進入一個重要數據的代碼塊時,在修改數據的過程中,很有可能引發線程安全問題,從而造成數據異常。例如,正常邏輯下,同一個編號的火車票只能售出一次,卻由於線程安全問題而被多次售出,從而引起實際業務異常。
接下來,我以售票問題,來演示多線程問題中對核心數據保護的重要性。我們先來看不對多線程數據進行保護時會引發什么樣的狀況。
/** * 售票問題 */ public class Test1 { static int tickets=10; class SellTickets implements Runnable{ @Override public void run() { // 未加同步時,產生臟數據 while(tickets>0){ System.out.println(Thread.currentThread().getName()+" -->售出第 "+tickets+" 張票"); tickets--; try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } if(tickets<=0){ System.out.println(Thread.currentThread().getName()+" -->售票結束!"); } } } public static void main(String[] args) { SellTickets sell=new Test1().new SellTickets(); Thread t1=new Thread(sell, "1號窗口"); Thread t2=new Thread(sell, "2號窗口"); Thread t3=new Thread(sell, "3號窗口"); Thread t4=new Thread(sell, "4號窗口"); t1.start(); t2.start(); t3.start(); t4.start(); } }
上述代碼運行后,效果如下:
1號窗口 -->售出第 10 張票 3號窗口 -->售出第 10 張票 2號窗口 -->售出第 10 張票 4號窗口 -->售出第 10 張票 3號窗口 -->售出第 6 張票 2號窗口 -->售出第 6 張票 1號窗口 -->售出第 5 張票 4號窗口 -->售出第 3 張票 3號窗口 -->售出第 2 張票 2號窗口 -->售出第 2 張票 1號窗口 -->售出第 2 張票 4號窗口 -->售票結束! 3號窗口 -->售票結束! 1號窗口 -->售票結束! 2號窗口 -->售票結束!
上述運行結果中,第10張票被售出多次,顯然不符合實際應用中的邏輯。由於多線程調度中的不確定性,讀者在演示上述代碼時,可能會取得不同的運行結果。
為了解決上述臟數據的問題,我為大家介紹3種使用比較普遍的三種同步方式。
第一種,同步代碼塊。
有synchronized關鍵字修飾的語句塊,即為同步代碼塊。同步代碼塊會被JVM自動加上內置鎖,從而實現同步。
我們來看代碼:
/** * 售票問題 * @author 李章勇 * */ public class Test2 { static int tickets=10; class SellTickets implements Runnable{ @Override public void run() { //同步代碼塊 while(tickets>0){ synchronized(this){ if(tickets<=0){ break; } System.out.println(Thread.currentThread().getName()+" -->售出第 "+tickets+" 張票"); tickets--; } try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } if(tickets<=0){ System.out.println(Thread.currentThread().getName()+" -->售票結束!"); } } } public static void main(String[] args) { SellTickets sell=new Test2().new SellTickets(); Thread t1=new Thread(sell, "1號窗口"); Thread t2=new Thread(sell, "2號窗口"); Thread t3=new Thread(sell, "3號窗口"); Thread t4=new Thread(sell, "4號窗口"); t1.start(); t2.start(); t3.start(); t4.start(); } }
上述代碼運行結果:
1號窗口 -->售出第 10 張票 3號窗口 -->售出第 9 張票 4號窗口 -->售出第 8 張票 2號窗口 -->售出第 7 張票 3號窗口 -->售出第 6 張票 4號窗口 -->售出第 5 張票 2號窗口 -->售出第 4 張票 1號窗口 -->售出第 3 張票 4號窗口 -->售出第 2 張票 3號窗口 -->售出第 1 張票 1號窗口 -->售票結束! 2號窗口 -->售票結束! 4號窗口 -->售票結束! 3號窗口 -->售票結束!
通過運行結果可知,上述運行結果正常。
第二種,同步方法 。
即有synchronized關鍵字修飾的方法。由於java的每個對象都有一個內置鎖,當用此關鍵字修飾方法時,內置鎖會保護整個方法。在調用該方法前,需要獲得內置鎖,否則就處於阻塞狀態。
我們來看代碼:
/** * 售票問題 * @author 李章勇 * */ public class Test3 { static int tickets=10; class SellTickets implements Runnable{ @Override public void run() { //同步方法 while(tickets>0){ synMethod(); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } if(tickets<=0){ System.out.println(Thread.currentThread().getName()+" -->售票結束!"); } } //同步方法 synchronized void synMethod(){ synchronized(this){ if(tickets<=0){ return; } System.out.println(Thread.currentThread().getName()+" -->售出第 "+tickets+" 張票"); tickets--; } } } public static void main(String[] args) { SellTickets sell=new Test3().new SellTickets(); Thread t1=new Thread(sell, "1號窗口"); Thread t2=new Thread(sell, "2號窗口"); Thread t3=new Thread(sell, "3號窗口"); Thread t4=new Thread(sell, "4號窗口"); t1.start(); t2.start(); t3.start(); t4.start(); } }
上述代碼運行結果:
1號窗口 -->售出第 10 張票 4號窗口 -->售出第 9 張票 3號窗口 -->售出第 8 張票 2號窗口 -->售出第 7 張票 1號窗口 -->售出第 6 張票 2號窗口 -->售出第 5 張票 4號窗口 -->售出第 4 張票 3號窗口 -->售出第 3 張票 4號窗口 -->售出第 2 張票 3號窗口 -->售出第 1 張票 1號窗口 -->售票結束! 4號窗口 -->售票結束! 2號窗口 -->售票結束! 3號窗口 -->售票結束!
上述代碼運行結果也正常。
第三種,Lock鎖機制。
通過創建Lock對象,采用lock()加鎖,采用unlock()解鎖,來保護指定代碼塊。我們看如下代碼:
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * 售票問題 * @author 李章勇 * */ public class Test4 { static int tickets=10; class SellTickets implements Runnable{ Lock lock=new ReentrantLock(); @Override public void run() { //Lock鎖機制 while(tickets>0){ try{ lock.lock(); if(tickets<=0){ break; } System.out.println(Thread.currentThread().getName()+" -->售出第 "+tickets+" 張票"); tickets--; }finally{ lock.unlock(); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } if(tickets<=0){ System.out.println(Thread.currentThread().getName()+" -->售票結束!"); } } } public static void main(String[] args) { SellTickets sell=new Test4().new SellTickets(); Thread t1=new Thread(sell, "1號窗口"); Thread t2=new Thread(sell, "2號窗口"); Thread t3=new Thread(sell, "3號窗口"); Thread t4=new Thread(sell, "4號窗口"); t1.start(); t2.start(); t3.start(); t4.start(); } }
運行結果如下:
1號窗口 -->售出第 10 張票 2號窗口 -->售出第 9 張票 3號窗口 -->售出第 8 張票 4號窗口 -->售出第 7 張票 1號窗口 -->售出第 6 張票 4號窗口 -->售出第 5 張票 2號窗口 -->售出第 4 張票 3號窗口 -->售出第 3 張票 1號窗口 -->售出第 2 張票 2號窗口 -->售出第 1 張票 3號窗口 -->售票結束! 1號窗口 -->售票結束! 2號窗口 -->售票結束! 4號窗口 -->售票結束!
最后總結:
由於synchronized是在JVM層面實現的,因此系統可以監控鎖的釋放與否;而ReentrantLock是使用代碼實現的,系統無法自動釋放鎖,需要在代碼中的finally子句中顯式釋放鎖lock.unlock()。
另外,在並發量比較小的情況下,使用synchronized是個不錯的選擇;但是在並發量比較高的情況下,其性能下降會很嚴重,此時ReentrantLock是個不錯的方案。
補充:
在使用synchronized 代碼塊時,可以與wait()、notify()、nitifyAll()一起使用,從而進一步實現線程的通信。
其中,wait()方法會釋放占有的對象鎖,當前線程進入等待池,釋放cpu,而其他正在等待的線程即可搶占此鎖,獲得鎖的線程即可運行程序;線程的sleep()方法則表示,當前線程會休眠一段時間,休眠期間,會暫時釋放cpu,但並不釋放對象鎖,也就是說,在休眠期間,其他線程依然無法進入被同步保護的代碼內部,當前線程休眠結束時,會重新獲得cpu執行權,從而執行被同步保護的代碼。
wait()和sleep()最大的不同在於wait()會釋放對象鎖,而sleep()不會釋放對象鎖。
notify()方法會喚醒因為調用對象的wait()而處於等待狀態的線程,從而使得該線程有機會獲取對象鎖。調用notify()后,當前線程並不會立即釋放鎖,而是繼續執行當前代碼,直到synchronized中的代碼全部執行完畢,才會釋放對象鎖。JVM會在等待的線程中調度一個線程去獲得對象鎖,執行代碼。
需要注意的是,wait()和notify()必須在synchronized代碼塊中調用。
notifyAll()是喚醒所有等待的線程。
接下來,我們通過下一個程序,使得兩個線程交替打印“A”和“B”各10次。請見下述代碼:
public class Test5 { static final Object obj=new Object(); //一個子線程 static class ThreadA implements Runnable{ @Override public void run() { int count=10; while(count>0){ synchronized(Test5.obj){ System.out.println("A-->"+count); count--; Test5.obj.notify(); try { Test5.obj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } //另一個子線程 static class ThreadB implements Runnable{ @Override public void run() { int count=10; while(count>0){ synchronized(Test5.obj){ System.out.println("B-->"+count); count--; Test5.obj.notify(); try { Test5.obj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } public static void main(String[] args) { new Thread(new ThreadA()).start(); new Thread(new ThreadB()).start(); } }
顯示結果如下:
A-->10 B-->10 A-->9 B-->9 A-->8 B-->8 A-->7 B-->7 A-->6 B-->6 A-->5 B-->5 A-->4 B-->4 A-->3 B-->3 A-->2 B-->2 A-->1 B-->1