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