Java深入探索線程安全和線程通信的特性
一、線程安全(重點(diǎn))
1、線程安全概念
在多線程的情況下,需要考慮多個(gè)線程并行并發(fā)執(zhí)行:此時(shí)多個(gè)線程之間的代碼是隨機(jī)執(zhí)行的。如果多線程環(huán)境下代碼的運(yùn)行結(jié)果是符合我們的預(yù)期的,即在單線程情況下應(yīng)該的結(jié)果,則這個(gè)程序是線程安全的。
2、產(chǎn)生線程不安全的情況
多個(gè)線程共享變量的操作:
- 都是讀操作時(shí),使用值進(jìn)行判斷,打印等操作(不存在線程安全問題)
- 存在線程進(jìn)行了寫操作(存在線程安全問題)
3、線程不安全的原因
(1)原子性:多行指令是最小的執(zhí)行單位(不可拆分),就具有原子性。如果不滿足原子性,則存在線程安全問題。
一條java語(yǔ)句不一定是原子的:
- 如n++、n--,是有三步組成:從內(nèi)存讀取到cpu寄存器、修改數(shù)據(jù)、再寫回到cpu;
- Object o=new Object(),也涉及到三個(gè)步驟:申請(qǐng)內(nèi)存、初始化對(duì)象、賦值。
以上列舉的雖然只是一條java語(yǔ)句,但是其不具有原子性。
(2)可見性:一個(gè)線程對(duì)共享變量的修改,能夠及時(shí)的被其他線程看見。多個(gè)線程并發(fā)并行執(zhí)行,使用各自的工作內(nèi)存,互相之間不可見(不具有可見性)。
Java內(nèi)存模型(JMM):
目的是屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,以實(shí)現(xiàn)Java程序在各種平臺(tái)下都能達(dá)到一致的并發(fā)效果。
- 線程之間的共享變量存在主內(nèi)存;
- 每一個(gè)線程都有自己的工作內(nèi)存;
- 當(dāng)程序要讀取一個(gè)共享變量時(shí),會(huì)先將變量從主內(nèi)存拷貝到工作內(nèi)存,再?gòu)墓ぷ鲀?nèi)存讀取數(shù)據(jù);
- 當(dāng)程序要修改一個(gè)共享變量時(shí),會(huì)先修改工作內(nèi)存中的副本,再同步到主內(nèi)存。
(3)代碼順序性/有序性:
代碼重排序:
比如,以下代碼:
1、去前臺(tái)取U盤,
2、回教室寫一會(huì)作業(yè),
3、去前臺(tái)取快遞
在單線程的情況下,JVM、cpu會(huì)對(duì)其進(jìn)行優(yōu)化,1→3→2,這樣會(huì)提高效率,這樣就叫做代碼重排序。
代碼重排序的前提是:保證邏輯不發(fā)生改變。
4、如何解決線程不安全問題
設(shè)計(jì)多線程代碼原則:滿足線程安全的前提下,盡可能地提高效率
(1)對(duì)共享變量的寫操作,可以加鎖來(lái)保證線程安全:
Java中加鎖的兩種方式:
- synchronized關(guān)鍵字:申請(qǐng)對(duì)給定的Java對(duì)象,對(duì)象頭加鎖;
- Lock:是一個(gè)鎖的接口,它的實(shí)現(xiàn)類提供了鎖這樣的對(duì)象,可以調(diào)用方法來(lái)加鎖/釋放鎖。
對(duì)共享變量的寫操作,不依賴任何共享變量,也可以使用volatile關(guān)鍵字來(lái)保證線程安全。
(2)對(duì)共享變量的讀操作,使用volatile關(guān)鍵字就可以保證線程安全
volatile關(guān)鍵字:修飾變量,變量的讀操作,本身就保證了原子性,volatile的作用是保證可見性和有序性,這樣就可以保證線程安全。
二、synchronized關(guān)鍵字
synchronized本質(zhì)上是修飾指定對(duì)象的對(duì)象頭去。使用角度來(lái)看,synchronized必須搭配一個(gè)具體的對(duì)象來(lái)使用。
1、使用
(1)修飾普通方法:鎖TestDemo對(duì)象
//方法一 public class TestDemo { public synchronized void methond() { } } //方法二 public class TestDemo { public void methond() { synchronized(this){ } } }
(2)修飾靜態(tài)方法:鎖TestDemo對(duì)象
//方法一 public class TestDemo { public synchronized static void method() { } } //方法二 public class TestDemo { public static void method() { synchronized(TestDemo.class){ } } }
2、特性
(1)互斥
synchronized會(huì)起到同步互斥的作用,某個(gè)線程執(zhí)行到某個(gè)對(duì)象的synchronized中時(shí),如果其他線程也執(zhí)行到同一個(gè)對(duì)象synchronized時(shí)會(huì)阻塞等待。
- 進(jìn)入synchronized修飾的代碼塊,相當(dāng)于加鎖;
- 退出synchronized代碼塊,相當(dāng)于釋放鎖。
互斥可以滿足原子性,
(2)刷新內(nèi)存
synchronized結(jié)束釋放鎖,會(huì)把工作內(nèi)存中的數(shù)據(jù)刷新到主存中;其他線程申請(qǐng)鎖時(shí),獲取的始終是最新的數(shù)據(jù)。(滿足可見性)。
(3)有序性
某個(gè)線程執(zhí)行一段同步代碼,不管如何重排序,過(guò)程中不可能有其他線程執(zhí)行的指令,這樣多個(gè)線程執(zhí)行同步代碼,就滿足一定的順序。
(4)可重入
同一個(gè)線程,可以多次申請(qǐng)同一個(gè)對(duì)象鎖(可重入)
三、volatile關(guān)鍵字
修飾某個(gè)變量(實(shí)例變量,靜態(tài)變量)
1、保證可見性
代碼在寫入volatilt修飾的變量時(shí):
- 改變線程工作內(nèi)存中volatile變量副本的值
- 將改變后的副本的值從工作內(nèi)存中刷新到主存中
代碼在讀取volatile修飾的變量時(shí):
- 從主存中讀取volatile變量的最新值到工作內(nèi)存中
- 從工作內(nèi)存中讀取副本值
2、禁止指令重排序
建立內(nèi)存屏障,保證代碼有序性。
3、不保證原子性
synchronized和volatile有著本質(zhì)區(qū)別。synchronized可以保證原子性,volatile保證的是內(nèi)存的可見性。
只能在共享變量的讀操作以及常量賦值操作時(shí)使用(這些操作本身就具有原子性)
四、wait和notify(線程間的通信)
線程通信:線程間通信,就是一個(gè)線程以通知的方式,喚醒某些等待的線程(或者讓當(dāng)前線程等待),這樣就可以讓線程通過(guò)通信的方式具有一定的順序性。
1、wait()方法
wait做的事情:
- 使當(dāng)前執(zhí)行代碼的線程進(jìn)入等待狀態(tài)(把線程放到等待隊(duì)列中)
- 釋放當(dāng)前的鎖
- 滿足一定條件時(shí),重新嘗試獲取這個(gè)鎖
wait結(jié)束等待的條件:
- 其他線程調(diào)用該對(duì)象的notify方法
- wait等待時(shí)間超時(shí)(wait提供了一個(gè)帶一個(gè)參數(shù)的方法,可以指定等待時(shí)間)
- 其他線程調(diào)用該等待線程的interrupted方法,導(dǎo)致wait拋出InterruptedException異常
wait要搭配synchronized來(lái)使用。脫離synchronized使用wait會(huì)直接拋異常。
如下:
Object object = new Object(); synchronized (object) { object.wait(); } //這種情況下線程會(huì)一直等待下去,這個(gè)時(shí)候需要使用notify來(lái)喚醒
2、notify()和notifyAll()方法
notify方法只是喚醒某一個(gè)等待的線程,使用notifyAll方法可以一次性喚醒所有的等待線程。
- notify()也是在同步方法中調(diào)用,用來(lái)通知其他等待的線程,對(duì)其發(fā)出通知notify,并使它們重新獲取該對(duì)象的對(duì)象鎖;
- 如果有多個(gè)線程在等待,則有線程調(diào)度器隨機(jī)挑選出一個(gè)處于等待狀態(tài)的線程;
- notify()方法執(zhí)行后,當(dāng)前線程不會(huì)立馬釋放該對(duì)象鎖,要等到當(dāng)前線程將程序執(zhí)行完,退出同步代碼塊后才會(huì)釋放對(duì)象鎖。
【注】
雖然notifyAll()同時(shí)喚醒所有處于等待狀態(tài)的線程,但是這些線程需要競(jìng)爭(zhēng)鎖。所以并不是同時(shí)執(zhí)行,仍然是有先后順序的執(zhí)行。
3、wait和sleep的對(duì)比
一個(gè)是用于線程之間的通信,一個(gè)是讓線程阻塞一段時(shí)間。
- wait需要搭配synchrionzed使用,sleep不用
- wait是Object的方法,sleep是Thread的靜態(tài)方法
五、線程和進(jìn)程的比較
1、線程的優(yōu)點(diǎn)
- 創(chuàng)建線程的代價(jià)比創(chuàng)建進(jìn)程小得多
- 與進(jìn)程切換相比,線程切換需要操作系統(tǒng)做的事少得多
- 線程占用的資源比進(jìn)程少
- 能充分利用多個(gè)處理器,提高效率
- 在等待I/O操作結(jié)束的同時(shí),程序可執(zhí)行其他的計(jì)算任務(wù)
- I/O密集型操作,為了提高性能,將I/O操作重疊。線程可以同時(shí)等待不同的I/O操作
- 計(jì)算密集型應(yīng)用,為了能在多處理器系統(tǒng)上運(yùn)行,將計(jì)算分解到多個(gè)線程中實(shí)現(xiàn)
2、線程和進(jìn)程的區(qū)別
- 進(jìn)程是進(jìn)行資源分配的最小單位,線程是程序執(zhí)行的最小單位
- 進(jìn)程有自己的內(nèi)存地址空間,線程只獨(dú)享指令流執(zhí)行的必要資源,如寄存器和棧
- 同一個(gè)進(jìn)程的各線程之間共享內(nèi)存和文件資源,可以不通過(guò)內(nèi)核進(jìn)行直接通信
- 線程的創(chuàng)建、切換、銷毀效率更高
到此這篇關(guān)于Java深入探索線程安全和線程通信的特性的文章就介紹到這了,更多相關(guān)Java線程安全內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Jvisualvm監(jiān)控遠(yuǎn)程SpringBoot項(xiàng)目的過(guò)程詳解
這篇文章主要介紹了Jvisualvm監(jiān)控遠(yuǎn)程SpringBoot項(xiàng)目,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-04-04解決mybatis使用char類型字段查詢oracle數(shù)據(jù)庫(kù)時(shí)結(jié)果返回null問題
這篇文章主要介紹了mybatis使用char類型字段查詢oracle數(shù)據(jù)庫(kù)時(shí)結(jié)果返回null問題的解決方法,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-06-06web 容器的設(shè)計(jì)如何實(shí)現(xiàn)
這篇文章主要介紹了web 容器的設(shè)計(jì)如何實(shí)現(xiàn)的相關(guān)資料,本文旨在介紹如何設(shè)計(jì)一個(gè)web容器,只探討實(shí)現(xiàn)的思路,并不涉及過(guò)多的具體實(shí)現(xiàn)。把它分解劃分成若干模塊和組件,每個(gè)組件模塊負(fù)責(zé)不同的功能,需要的朋友可以參考下2016-12-12java 取交集方法retainAll的實(shí)現(xiàn)
這篇文章主要介紹了java 取交集方法retainAll的實(shí)現(xiàn)操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06Spring?Security放行的接口Knife4j靜態(tài)資源的問題小結(jié)
這篇文章主要介紹了Spring?Security使用Knife4j靜態(tài)資源的問題小結(jié),項(xiàng)目中使用?Spring?Security?做身份認(rèn)證和授權(quán),使用?Knife4j?做接口調(diào)試,需要?Spring?Security?放行的接口記錄在?RequestMatcherConstant?類中,感興趣的朋友跟隨小編一起看看吧2024-02-02詳解Spring Boot中使用@Scheduled創(chuàng)建定時(shí)任務(wù)
本篇文章中主要介紹了Spring Boot中使用@Scheduled創(chuàng)建定時(shí)任務(wù),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-03-03