Java多線程之搞定最后一公里詳解
緒論
上期介紹了多線程的概念、優(yōu)勢、創(chuàng)建方法以及幾個常用的關(guān)鍵字。有了之前的基礎(chǔ)過后,我們來討論討論線程安全問題以及其他線程進(jìn)階知識。
一:線程安全問題
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(); // 期望最終結(jié)果應(yīng)該是 0 System.out.println(counter.value()); } }
大家看結(jié)果:
大家觀察下是否適用多線程的現(xiàn)象是否一致?同時嘗試思考下為什么會有這樣的現(xiàn)象發(fā)生呢?
想給出一個線程安全的確切定義是復(fù)雜的,但我們可以這樣認(rèn)為:
如果多線程環(huán)境下代碼運行的結(jié)果是符合我們預(yù)期的,即在單線程環(huán)境應(yīng)該的結(jié)果,則說這個程序是線程安全的。
1.2 不安全的原因
1.2.1 原子性
舉個簡單的例子,當(dāng)我i們買票的時候,如果車站剩余票數(shù)大于0,就可以買。反之,買完一張票后,車站的票數(shù)也會自動減一。假設(shè)出現(xiàn)這種情況,兩個人同時來買票,只剩最后一張票,前面那個人把最后一張票買了,但是短時間內(nèi)票數(shù)還沒減一也就是清零,這時另外一個人看到還有一張票,于是提交訂單,但是其實已經(jīng)沒有多余的票了,那么問題就來了。這時我們引入原子性:
我們把一段代碼想象成一個房間,每個線程就是要進(jìn)入這個房間的人。如果沒有任何機制保證, A 進(jìn)入房間之后,還 沒有出來; B 是不是也可以進(jìn)入房間,打斷 A 在房間里的隱私。這個就是不具備原子性的。 那我們應(yīng)該如何解決這個問題呢?是不是只要給房間加一把鎖, A 進(jìn)去就把門鎖上,其他人是不是就進(jìn)不來了。這樣 就保證了這段代碼的原子性了。 有時也把這個現(xiàn)象叫做同步互斥,表示操作是互相排斥的。 不保證原子性, 如果一個線程正在對一個變量操作,中途其他線程插入進(jìn)來了,如果這個操作被打斷了,結(jié)果就可能是錯誤的。
1.2.2 代碼“優(yōu)化”
一段代碼是這樣的:
1. 去前臺取下 U 盤
2. 去教室寫 10 分鐘作業(yè)
3. 去前臺取下快遞
如果是在單線程情況下, JVM 、 CPU 指令集會對其進(jìn)行優(yōu)化,比如,按 1->3->2 的方式執(zhí)行,也是沒問題,可以少跑 一次前臺。這種叫做指令重排序。 剛才那個例子中,單線程情況是沒問題的,優(yōu)化是正確的,但在多線程場景下就有問題了,什么問題呢。可能快遞是 在你寫作業(yè)的10 分鐘內(nèi)被另一個線程放過來的,或者被人變過了,如果指令重排序了,代碼就會是錯誤的。
二:如何解決線程不安全的問題
2.1 通過synchronized關(guān)鍵字
synchronized 的底層是使用操作系統(tǒng)的 mutex lock 實現(xiàn)的。 當(dāng)線程釋放鎖時, JMM 會把該線程對應(yīng)的工作內(nèi)存中的共享變量刷新到主內(nèi)存中 當(dāng)線程獲取鎖時, JMM 會把該線程對應(yīng)的本地內(nèi)存置為無效。從而使得被監(jiān)視器保護(hù)的臨界區(qū)代碼必須從主內(nèi) 存中讀取共享變量 synchronized 用的鎖是存在 Java對象頭里的。 synchronized 同步快對同一條線程來說是可重入的,不會出現(xiàn)自己把自己鎖死的問題; 同步塊在已進(jìn)入的線程執(zhí)行完之前,會阻塞后面其他線程的進(jìn)入。
鎖的 SynchronizedDemo 對象
public class SynchronizedDemo { public synchronized static void methond() { } public static void main(String[] args) { method(); // 進(jìn)入方法會鎖 SynchronizedDemo.class 指向?qū)ο笾械逆i;出方法會釋放 SynchronizedDemo.class 指向的對象中的鎖 } }
鎖的 SynchronizedDemo 類的對象
public class SynchronizedDemo { public synchronized static void methond() { } public static void main(String[] args) { method(); // 進(jìn)入方法會鎖 SynchronizedDemo.class 指向?qū)ο笾械逆i;出方法會釋放 SynchronizedDemo.class 指向的對象中的鎖 } }
明確鎖的對象
public class SynchronizedDemo { public synchronized static void methond() { } public static void main(String[] args) { method(); // 進(jìn)入方法會鎖 SynchronizedDemo.class 指向?qū)ο笾械逆i;出方法會釋放 SynchronizedDemo.class 指向的對象中的鎖 } }
public class SynchronizedDemo { public void methond() { // 進(jìn)入代碼塊會鎖 SynchronizedDemo.class 指向?qū)ο笾械逆i;出代碼塊會釋放 SynchronizedDemo.class 指向的對象中的鎖 synchronized (SynchronizedDemo.class) { } } public static void main(String[] args) { SynchronizedDemo demo = new SynchronizedDemo(); demo.method(); } }
2.2 volatile
這里提一下volatile:
首先,被volatile關(guān)鍵字修飾的變量,編譯器與運行時都會注意到這個變量是共享的,因此不會將該變量上的操作與其他內(nèi)存操作一起重排序。volatile變量不會被緩存在寄存器或者對其他處理器不可見的地方,因此在讀取volatile類型的變量時總會返回最新寫入的值。
在訪問volatile變量時不會執(zhí)行加鎖操作,因此也就不會使執(zhí)行線程阻塞,因此volatile變量是一種比sychronized關(guān)鍵字更輕量級的同步機制。當(dāng)對非 volatile 變量進(jìn)行讀寫的時候,每個線程先從內(nèi)存拷貝變量到CPU緩存中。如果計算機有多個CPU,每個線程可能在不同的CPU上被處理,這意味著每個線程可以拷貝到不同的 CPU cache 中。而聲明變量是 volatile 的,JVM 保證了每次讀變量都從內(nèi)存中讀,跳過 CPU cache 這一步
三:wait和notify關(guān)鍵字
3.1 wait方法
其實 wait() 方法就是使線程停止運行。
1. 方法 wait() 的作用是使當(dāng)前執(zhí)行代碼的線程進(jìn)行等待, wait() 方法是 Object 類的方法,該方法是用來將當(dāng)前線程 置入 “ 預(yù)執(zhí)行隊列 ” 中,并且在 wait() 所在的代碼處停止執(zhí)行,直到接到通知或被中斷為止。
2. wait() 方法只能在同步方法中或同步塊中調(diào)用。如果調(diào)用 wait() 時,沒有持有適當(dāng)?shù)逆i,會拋出異常。
3. wait() 方法執(zhí)行后,當(dāng)前線程釋放鎖,線程與其它線程競爭重新獲取鎖。
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 方法結(jié)束 ..."); }
這樣在執(zhí)行到 object.wait() 之后就一直等待下去,那么程序肯定不能一直這么等待下去了。這個時候就需要使用到了 另外一個方法喚醒的方法 notify() 。
3.2 notify方法
notify 方法就是使停止的線程繼續(xù)運行。
- 1. 方法 notify() 也要在同步方法或同步塊中調(diào)用,該方法是用來通知那些可能等待該對象的對象鎖的其它線程,對 其發(fā)出通知 notify ,并使它們重新獲取該對象的對象鎖。如果有多個線程等待,則有線程規(guī)劃器隨機挑選出一個 呈 wait 狀態(tài)的線程。
- 2. 在 notify() 方法后,當(dāng)前線程不會馬上釋放該對象鎖,要等到執(zhí)行 notify() 方法的線程將程序執(zhí)行完,也就是退出 同步代碼塊之后才會釋放對象鎖。
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()方法結(jié)束.. " + 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()方法結(jié)束.. " + 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方法結(jié)束!!"); } }
從結(jié)果上來看第一個線程執(zhí)行的是一個 waitMethod 方法,該方法里面有個死循環(huán)并且使用了 wait 方法進(jìn)入等待狀態(tài) 將釋放鎖,如果這個線程不被喚醒的話將會一直等待下去,這個時候第二個線程執(zhí)行的是 notifyMethod 方法,該方 法里面執(zhí)行了一個喚醒線程的操作,并且一直將 notify 的同步代碼塊執(zhí)行完畢之后才會釋放鎖然后繼續(xù)執(zhí)行 wait 結(jié)束 打印語句。 注意: wait , notify 必須使用在 synchronized 同步方法或者代碼塊內(nèi)。
3.3 wait和sleep對比(面試??迹?/h3>
其實理論上 wait 和 sleep 完全是沒有可比性的,因為一個是用于線程之間的通信的,一個是讓線程阻塞一段時間, 唯一的相同點就是都可以讓線程放棄執(zhí)行一段時間。用生活中的例子說的話就是婚禮時會吃糖,和家里自己吃糖之間 有差別。說白了放棄線程執(zhí)行只是 wait 的一小段現(xiàn)象。 當(dāng)然為了面試的目的,我們還是總結(jié)下:
- 1. wait 之前需要請求鎖,而 wait 執(zhí)行時會先釋放鎖,等被喚醒時再重新請求鎖。這個鎖是 wait 對像上的 monitor
- lock
- 2. sleep 是無視鎖的存在的,即之前請求的鎖不會釋放,沒有鎖也不會請求。
- 3. wait 是 Object 的方法
- 4. sleep 是 Thread 的靜態(tài)方法
四:多線程案例
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; } }
總結(jié)
多線程的部分暫時分享到這里,但其實還有很多沒有沒有涉及 ,等日后深刻理解后再來分享,碼文不易,多謝大家支持,感激不盡!
到此這篇關(guān)于Java多線程之搞定最后一公里詳解的文章就介紹到這了,更多相關(guān)Java 多線程內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot如何優(yōu)雅地使用Swagger2
這篇文章主要介紹了SpringBoot如何優(yōu)雅地使用Swagger2,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-07-07Mybatis-plus中IService接口的基本使用步驟
Mybatis-plus是一個Mybatis的增強工具,它提供了很多便捷的方法來簡化開發(fā),IService是Mybatis-plus提供的通用service接口,封裝了常用的數(shù)據(jù)庫操作方法,包括增刪改查等,下面這篇文章主要給大家介紹了關(guān)于Mybatis-plus中IService接口的基本使用步驟,需要的朋友可以參考下2023-06-06Spring中@Autowired和@Qualifier注解的3個知識點小結(jié)
這篇文章主要介紹了Spring中@Autowired和@Qualifier注解的3個知識點小結(jié),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-09-09Java并發(fā)編程之關(guān)鍵字volatile知識總結(jié)
今天帶大家學(xué)習(xí)java的相關(guān)知識,文章圍繞著Java關(guān)鍵字volatile展開,文中有非常詳細(xì)的知識總結(jié),需要的朋友可以參考下2021-06-06Java深入了解數(shù)據(jù)結(jié)構(gòu)之優(yōu)先級隊列(堆)
普通的隊列是一種先進(jìn)先出的數(shù)據(jù)結(jié)構(gòu),元素在隊列尾追加,而從隊列頭刪除。在優(yōu)先隊列中,元素被賦予優(yōu)先級。當(dāng)訪問元素時,具有最高優(yōu)先級的元素最先刪除。優(yōu)先隊列具有最高級先出 (first in, largest out)的行為特征。通常采用堆數(shù)據(jù)結(jié)構(gòu)來實現(xiàn)2022-01-01Spring Boot2.0實現(xiàn)靜態(tài)資源版本控制詳解
這篇文章主要給大家介紹了關(guān)于Spring Boot2.0實現(xiàn)靜態(tài)資源版本控制的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-11-11mybatis mapper互相引用resultMap啟動出錯的解決
這篇文章主要介紹了mybatis mapper互相引用resultMap啟動出錯的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-08-08