一個程序在運行起來的時候會轉換成進程,通常含有多個線程。
通常情況下,一個進程中的比較耗時的操作(如長循環、文件上傳下載、網絡資源獲取等),往往會采用多線程來解決。
比如顯示生活中,銀行取錢問題、火車票多個售票窗口的問題,通常會涉及到並發的問題,從而需要多線程的技術。
當進程中有多個並發線程進入一個重要數據的代碼塊時,在修改數據的過程中,很有可能引發線程安全問題,從而造成數據異常。例如,正常邏輯下,同一個編號的火車票只能售出一次,卻由於線程安全問題而被多次售出,從而引起實際業務異常。
現在我們就以售票問題來演示線程安全的問題
1, 在不對多線程數據進行保護的情況下會引發的狀況
public class ThreadUnSecurity { 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(1000); } catch (InterruptedException e) { e.printStackTrace(); } } if (tickets <= 0) { System.out.println(Thread.currentThread().getName()+"--->售票結束!"); } } } public static void main(String[] args) { SellTickets sell = new ThreadUnSecurity().new SellTickets(); Thread thread1 = new Thread(sell, "1號窗口"); Thread thread2 = new Thread(sell, "2號窗口"); Thread thread3 = new Thread(sell, "3號窗口"); Thread thread4 = new Thread(sell, "4號窗口"); thread1.start(); thread2.start(); thread3.start(); thread4.start(); } }
上述代碼運行的結果:
1號窗口--->售出第: 10 票 3號窗口--->售出第: 10 票 2號窗口--->售出第: 10 票 4號窗口--->售出第: 10 票 2號窗口--->售出第: 6 票 1號窗口--->售出第: 5 票 3號窗口--->售出第: 4 票 4號窗口--->售出第: 3 票 2號窗口--->售出第: 2 票 4號窗口--->售出第: 1 票 1號窗口--->售出第: 1 票 3號窗口--->售票結束! 2號窗口--->售票結束! 1號窗口--->售票結束! 4號窗口--->售票結束!
我們可以看出同一張票在不對票數進行保護時會出現同一張票會被出售多次!由於線程調度中的不確定性,讀者在演示上述代碼時,出現的運行結果會有不同。
第一種實現線程安全的方式
同步代碼塊
package com.bpan.spring.beans.thread; import com.sun.org.apache.regexp.internal.recompile; public class ThreadSynchronizedSecurity { static int tickets = 10; class SellTickets implements Runnable{ @Override public void run() { // 同步代碼塊 while(tickets > 0) { synchronized (this) { // System.out.println(this.getClass().getName().toString()); if (tickets <= 0) { return; } 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 ThreadSynchronizedSecurity().new SellTickets(); Thread thread1 = new Thread(sell, "1號窗口"); Thread thread2 = new Thread(sell, "2號窗口"); Thread thread3 = new Thread(sell, "3號窗口"); Thread thread4 = new Thread(sell, "4號窗口"); thread1.start(); thread2.start(); thread3.start(); thread4.start(); } }
輸出結果讀者可自行調試,不會出現同一張票被出售多次的情況。
第二種 方式
同步方法
package com.bpan.spring.beans.thread; public class ThreadSynchroniazedMethodSecurity { static int tickets = 10; class SellTickets implements Runnable{ @Override public void run() { //同步方法 while (tickets > 0) { synMethod(); try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block 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 ThreadSynchroniazedMethodSecurity().new SellTickets(); Thread thread1 = new Thread(sell, "1號窗口"); Thread thread2 = new Thread(sell, "2號窗口"); Thread thread3 = new Thread(sell, "3號窗口"); Thread thread4 = new Thread(sell, "4號窗口"); thread1.start(); thread2.start(); thread3.start(); thread4.start(); } }
讀者可自行調試上述代碼的運行結果
第三種 方式
Lock鎖機制, 通過創建Lock對象,采用lock()加鎖,unlock()解鎖,來保護指定的代碼塊
package com.bpan.spring.beans.thread; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class ThreadLockSecurity { 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) { return; } System.out.println(Thread.currentThread().getName()+"--->售出第: "+tickets+" 票"); tickets--; } catch (Exception e1) { // TODO Auto-generated catch block e1.printStackTrace(); }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 ThreadLockSecurity().new SellTickets(); Thread thread1 = new Thread(sell, "1號窗口"); Thread thread2 = new Thread(sell, "2號窗口"); Thread thread3 = new Thread(sell, "3號窗口"); Thread thread4 = new Thread(sell, "4號窗口"); thread1.start(); thread2.start(); thread3.start(); thread4.start(); } }
最后總結:
由於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()是喚醒所有等待的線程。
下面是示例代碼,
package com.bpan.spring.beans.thread; public class ThreadDemo { static final Object obj = new Object(); //第一個子線程 static class ThreadA implements Runnable{ @Override public void run() { int count = 10; while(count > 0) { synchronized (ThreadDemo.obj) { System.out.println("A-----"+count); count--; synchronized (ThreadDemo.obj) { //notify()方法會喚醒因為調用對象的wait()而處於等待狀態的線程,從而使得該線程有機會獲取對象鎖。 //調用notify()后,當前線程並不會立即釋放鎖,而是繼續執行當前代碼,直到synchronized中的代碼全部執行完畢, ThreadDemo.obj.notify(); try { ThreadDemo.obj.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } } } static class ThreadB implements Runnable{ @Override public void run() { int count = 10; while(count > 0) { synchronized (ThreadDemo.obj) { System.out.println("B-----"+count); count--; synchronized (ThreadDemo.obj) { //notify()方法會喚醒因為調用對象的wait()而處於等待狀態的線程,從而使得該線程有機會獲取對象鎖。 //調用notify()后,當前線程並不會立即釋放鎖,而是繼續執行當前代碼,直到synchronized中的代碼全部執行完畢, ThreadDemo.obj.notify(); try { ThreadDemo.obj.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } } } public static void main(String[] args) { new Thread(new ThreadA()).start(); new Thread(new ThreadB()).start(); } }
參考地址:https://www.cnblogs.com/lizhangyong/p/8029287.html