Java多線程之搞定最後一公裡詳解
緒論
上期介紹瞭多線程的概念、優勢、創建方法以及幾個常用的關鍵字。有瞭之前的基礎過後,我們來討論討論線程安全問題以及其他線程進階知識。
一:線程安全問題
1.1 提出問題
首先,給大傢看一下這個代碼:
public class yy1 { private static class Counter { private long n = 0; public void increment() { n++; } public void decrement() { n--; } public long value() { return n; } } public static void main(String[] args) throws InterruptedException { final int COUNT = 1000_0000; Counter counter = new Counter(); Thread thread = new Thread(() -> { for (int i = 0; i < COUNT; i++) { counter.increment(); } }, "李四"); thread.start(); for (int i = 0; i < COUNT; i++) { counter.decrement(); } thread.join(); // 期望最終結果應該是 0 System.out.println(counter.value()); } }
大傢看結果:
大傢觀察下是否適用多線程的現象是否一致?同時嘗試思考下為什麼會有這樣的現象發生呢?
想給出一個線程安全的確切定義是復雜的,但我們可以這樣認為:
如果多線程環境下代碼運行的結果是符合我們預期的,即在單線程環境應該的結果,則說這個程序是線程安全的。
1.2 不安全的原因
1.2.1 原子性
舉個簡單的例子,當我i們買票的時候,如果車站剩餘票數大於0,就可以買。反之,買完一張票後,車站的票數也會自動減一。假設出現這種情況,兩個人同時來買票,隻剩最後一張票,前面那個人把最後一張票買瞭,但是短時間內票數還沒減一也就是清零,這時另外一個人看到還有一張票,於是提交訂單,但是其實已經沒有多餘的票瞭,那麼問題就來瞭。這時我們引入原子性:
我們把一段代碼想象成一個房間,每個線程就是要進入這個房間的人。如果沒有任何機制保證, A 進入房間之後,還 沒有出來; B 是不是也可以進入房間,打斷 A 在房間裡的隱私。這個就是不具備原子性的。 那我們應該如何解決這個問題呢?是不是隻要給房間加一把鎖, A 進去就把門鎖上,其他人是不是就進不來瞭。這樣 就保證瞭這段代碼的原子性瞭。 有時也把這個現象叫做同步互斥,表示操作是互相排斥的。 不保證原子性, 如果一個線程正在對一個變量操作,中途其他線程插入進來瞭,如果這個操作被打斷瞭,結果就可能是錯誤的。
1.2.2 代碼“優化”
一段代碼是這樣的:
1. 去前臺取下 U 盤
2. 去教室寫 10 分鐘作業
3. 去前臺取下快遞
如果是在單線程情況下, JVM 、 CPU 指令集會對其進行優化,比如,按 1->3->2 的方式執行,也是沒問題,可以少跑 一次前臺。這種叫做指令重排序。 剛才那個例子中,單線程情況是沒問題的,優化是正確的,但在多線程場景下就有問題瞭,什麼問題呢。可能快遞是 在你寫作業的10 分鐘內被另一個線程放過來的,或者被人變過瞭,如果指令重排序瞭,代碼就會是錯誤的。
二:如何解決線程不安全的問題
2.1 通過synchronized關鍵字
synchronized 的底層是使用操作系統的 mutex lock 實現的。 當線程釋放鎖時, JMM 會把該線程對應的工作內存中的共享變量刷新到主內存中 當線程獲取鎖時, JMM 會把該線程對應的本地內存置為無效。從而使得被監視器保護的臨界區代碼必須從主內 存中讀取共享變量 synchronized 用的鎖是存在 Java對象頭裡的。 synchronized 同步快對同一條線程來說是可重入的,不會出現自己把自己鎖死的問題; 同步塊在已進入的線程執行完之前,會阻塞後面其他線程的進入。
鎖的 SynchronizedDemo 對象
public class SynchronizedDemo { public synchronized static void methond() { } public static void main(String[] args) { method(); // 進入方法會鎖 SynchronizedDemo.class 指向對象中的鎖;出方法會釋放 SynchronizedDemo.class 指向的對象中的鎖 } }
鎖的 SynchronizedDemo 類的對象
public class SynchronizedDemo { public synchronized static void methond() { } public static void main(String[] args) { method(); // 進入方法會鎖 SynchronizedDemo.class 指向對象中的鎖;出方法會釋放 SynchronizedDemo.class 指向的對象中的鎖 } }
明確鎖的對象
public class SynchronizedDemo { public synchronized static void methond() { } public static void main(String[] args) { method(); // 進入方法會鎖 SynchronizedDemo.class 指向對象中的鎖;出方法會釋放 SynchronizedDemo.class 指向的對象中的鎖 } }
public class SynchronizedDemo { public void methond() { // 進入代碼塊會鎖 SynchronizedDemo.class 指向對象中的鎖;出代碼塊會釋放 SynchronizedDemo.class 指向的對象中的鎖 synchronized (SynchronizedDemo.class) { } } public static void main(String[] args) { SynchronizedDemo demo = new SynchronizedDemo(); demo.method(); } }
2.2 volatile
這裡提一下volatile:
首先,被volatile關鍵字修飾的變量,編譯器與運行時都會註意到這個變量是共享的,因此不會將該變量上的操作與其他內存操作一起重排序。volatile變量不會被緩存在寄存器或者對其他處理器不可見的地方,因此在讀取volatile類型的變量時總會返回最新寫入的值。
在訪問volatile變量時不會執行加鎖操作,因此也就不會使執行線程阻塞,因此volatile變量是一種比sychronized關鍵字更輕量級的同步機制。當對非 volatile 變量進行讀寫的時候,每個線程先從內存拷貝變量到CPU緩存中。如果計算機有多個CPU,每個線程可能在不同的CPU上被處理,這意味著每個線程可以拷貝到不同的 CPU cache 中。而聲明變量是 volatile 的,JVM 保證瞭每次讀變量都從內存中讀,跳過 CPU cache 這一步
三:wait和notify關鍵字
3.1 wait方法
其實 wait() 方法就是使線程停止運行。
1. 方法 wait() 的作用是使當前執行代碼的線程進行等待, wait() 方法是 Object 類的方法,該方法是用來將當前線程 置入 “ 預執行隊列 ” 中,並且在 wait() 所在的代碼處停止執行,直到接到通知或被中斷為止。
2. wait() 方法隻能在同步方法中或同步塊中調用。如果調用 wait() 時,沒有持有適當的鎖,會拋出異常。
3. wait() 方法執行後,當前線程釋放鎖,線程與其它線程競爭重新獲取鎖。
public static void main(String[] args) throws InterruptedException { Object object = new Object(); synchronized (object) { System.out.println(" 等待中 ..."); object.wait(); System.out.println(" 等待已過 ..."); } System.out.println("main 方法結束 ..."); }
這樣在執行到 object.wait() 之後就一直等待下去,那麼程序肯定不能一直這麼等待下去瞭。這個時候就需要使用到瞭 另外一個方法喚醒的方法 notify() 。
3.2 notify方法
notify 方法就是使停止的線程繼續運行。
- 1. 方法 notify() 也要在同步方法或同步塊中調用,該方法是用來通知那些可能等待該對象的對象鎖的其它線程,對 其發出通知 notify ,並使它們重新獲取該對象的對象鎖。如果有多個線程等待,則有線程規劃器隨機挑選出一個 呈 wait 狀態的線程。
- 2. 在 notify() 方法後,當前線程不會馬上釋放該對象鎖,要等到執行 notify() 方法的線程將程序執行完,也就是退出 同步代碼塊之後才會釋放對象鎖。
class MyThread implements Runnable { private boolean flag; private Object obj; public MyThread(boolean flag, Object obj) { super(); this.flag = flag; this.obj = obj; } public void waitMethod() { synchronized (obj) { try { while (true) { System.out.println("wait()方法開始.. " + Thread.currentThread().getName()); obj.wait(); System.out.println("wait()方法結束.. " + Thread.currentThread().getName()); return; } } catch (Exception e) { e.printStackTrace(); } } } public void notifyMethod() { synchronized (obj) { try { System.out.println("notifyAll()方法開始.. " + Thread.currentThread().getName()); obj.notifyAll(); System.out.println("notifyAll()方法結束.. " + Thread.currentThread().getName()); } catch (Exception e) { e.printStackTrace(); } } } @Override public void run() { if (flag) { this.waitMethod(); } else { this.notifyMethod(); } } } public class TestThread { public static void main(String[] args) throws InterruptedException { Object object = new Object(); MyThread waitThread1 = new MyThread(true, object); MyThread waitThread2 = new MyThread(true, object); MyThread waitThread3 = new MyThread(true, object); MyThread notifyThread = new MyThread(false, object); Thread thread1 = new Thread(waitThread1, "wait線程A"); Thread thread2 = new Thread(waitThread2, "wait線程B"); Thread thread3 = new Thread(waitThread3, "wait線程C"); Thread thread4 = new Thread(notifyThread, "notify線程"); thread1.start(); thread2.start(); thread3.start(); Thread.sleep(1000); thread4.start(); System.out.println("main方法結束!!"); } }
從結果上來看第一個線程執行的是一個 waitMethod 方法,該方法裡面有個死循環並且使用瞭 wait 方法進入等待狀態 將釋放鎖,如果這個線程不被喚醒的話將會一直等待下去,這個時候第二個線程執行的是 notifyMethod 方法,該方 法裡面執行瞭一個喚醒線程的操作,並且一直將 notify 的同步代碼塊執行完畢之後才會釋放鎖然後繼續執行 wait 結束 打印語句。 註意: wait , notify 必須使用在 synchronized 同步方法或者代碼塊內。
3.3 wait和sleep對比(面試常考)
其實理論上 wait 和 sleep 完全是沒有可比性的,因為一個是用於線程之間的通信的,一個是讓線程阻塞一段時間, 唯一的相同點就是都可以讓線程放棄執行一段時間。用生活中的例子說的話就是婚禮時會吃糖,和傢裡自己吃糖之間 有差別。說白瞭放棄線程執行隻是 wait 的一小段現象。 當然為瞭面試的目的,我們還是總結下:
- 1. wait 之前需要請求鎖,而 wait 執行時會先釋放鎖,等被喚醒時再重新請求鎖。這個鎖是 wait 對像上的 monitor
- lock
- 2. sleep 是無視鎖的存在的,即之前請求的鎖不會釋放,沒有鎖也不會請求。
- 3. wait 是 Object 的方法
- 4. sleep 是 Thread 的靜態方法
四:多線程案例
4.1 餓漢模式單線程
class Singleton { private static Singleton instance = new Singleton(); private Singleton() {} public static Singleton getInstance() { return instance; }
4.2 懶漢模式單線程
class Singleton { private static Singleton instance = null; private Singleton() {} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
4.3 懶漢模式多線程低性能版
class Singleton { private static Singleton instance = null; private Singleton() {} public synchronized static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
4.4懶漢模式-多線程版-二次判斷-性能高
class Singleton { private static volatile Singleton instance = null; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
總結
多線程的部分暫時分享到這裡,但其實還有很多沒有沒有涉及 ,等日後深刻理解後再來分享,碼文不易,多謝大傢支持,感激不盡!
到此這篇關於Java多線程之搞定最後一公裡詳解的文章就介紹到這瞭,更多相關Java 多線程內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- Java多線程Thread類的使用及註意事項
- 為什麼Java單例模式一定要加 volatile
- Java單例模式的8種寫法(推薦)
- Java中Volatile關鍵字能保證原子性嗎
- 一個例子帶你看懂Java中synchronized關鍵字到底怎麼用