Java多线程变量共享与隔离
文章目錄
- 線程相關(guān)
- 線程的相關(guān)API
- 線程的調(diào)度
- 線程的優(yōu)先級(jí)
- 方法和變量的線程安全問題
- 靜態(tài)方法
- 非靜態(tài)方法
- 靜態(tài)變量
- 實(shí)例變量
- 局部變量
- 變量共享
- 共享變量線程安全問題
- 可見性
- 可見性舉例
- 共享變量可見性的實(shí)現(xiàn)
- synchronized
- volatile
- synchronized和volatile比較
- volatile適用情況
- 特殊操作會(huì)從主內(nèi)存中拉取值
- 變量隔離
- ThreadLocal
- 使用ThreadLocal的好處
- ThreadLocal主要方法
- ThreadLocal源碼分析
- ThreadLocal注意事項(xiàng)
- 注意事項(xiàng)
線程相關(guān)
線程的相關(guān)API
線程的調(diào)度
調(diào)度策略:
Java的調(diào)度方法:
線程的優(yōu)先級(jí)
等級(jí):
MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5
方法:
getPriority():返回線程優(yōu)先級(jí)
setPriority(int newPriority):改變線程的優(yōu)先級(jí)
注意事項(xiàng):高優(yōu)先級(jí)的線程要搶占低優(yōu)先級(jí)的線程的cpu的執(zhí)行權(quán)。但是僅是從概率上來說的,高優(yōu)先級(jí)的線程更有可能被執(zhí)行。并不意味著只有高優(yōu)先級(jí)的線程執(zhí)行完以后,低優(yōu)先級(jí)的線程才執(zhí)行。
方法和變量的線程安全問題
靜態(tài)方法
與靜態(tài)成員變量一樣,屬于類本身,在類裝載的時(shí)候被裝載到內(nèi)存(Memory),不自動(dòng)進(jìn)行銷毀,會(huì)一直存在于內(nèi)存中,直到JVM關(guān)閉。
非靜態(tài)方法
又叫實(shí)例化方法,屬于實(shí)例對(duì)象,實(shí)例化后才會(huì)分配內(nèi)存,必須通過類的實(shí)例來引用。不會(huì)常駐內(nèi)存,當(dāng)實(shí)例對(duì)象被JVM 回收之后,也跟著消失。
靜態(tài)變量
線程非安全,靜態(tài)變量即類變量,位于方法區(qū),為所有該類下的對(duì)象共享,共享一份內(nèi)存,一旦靜態(tài)變量被修改,其他對(duì)象均對(duì)修改可見,故線程非安全。
實(shí)例變量
實(shí)例變量為對(duì)象實(shí)例私有,在虛擬機(jī)的堆中分配,若在系統(tǒng)中只存在一個(gè)此對(duì)象的實(shí)例,在多線程環(huán)境下,“猶如”靜態(tài)變量那樣,被某個(gè)線程修改后,其他線程對(duì)修改均可見,故線程非安全;如果每個(gè)線程執(zhí)行都是在不同的對(duì)象中,那對(duì)象與對(duì)象之間的實(shí)例變量的修改將互不影響,故線程安全。
局部變量
線程安全,每個(gè)線程執(zhí)行時(shí)將會(huì)把局部變量放在各自棧幀的工作內(nèi)存中,線程間不共享,故不存在線程安全問題。
變量共享
共享變量線程安全問題
可見性
如果一個(gè)線程對(duì)共享變量值的修改,能夠及時(shí)的被其他線程看到,叫做共享變量的可見性。如果一個(gè)變量同時(shí)在多個(gè)線程的工作內(nèi)存中存在副本,那么這個(gè)變量就叫共享變量。
Java多線程里對(duì)于共享變量的操作往往需要考慮進(jìn)行一定的同步互斥操作,原來是因?yàn)镴ava內(nèi)存模型導(dǎo)致的共享內(nèi)存對(duì)于線程不可見。
Java 內(nèi)存模型規(guī)定,將所有的變量都存放在主內(nèi)存中。
多個(gè)線程同時(shí)對(duì)主內(nèi)存的一個(gè)共享變量進(jìn)行讀取和修改時(shí),首先會(huì)讀取這個(gè)變量到自己的工作內(nèi)存中成為一個(gè)副本,對(duì)這個(gè)副本進(jìn)行改動(dòng)之后,再更新回主內(nèi)存中變量所在的地方。
由于CPU時(shí)間片是以線程為最小單位,所以這里的工作內(nèi)存實(shí)際上就是指的物理緩存,CPU運(yùn)算時(shí)獲取數(shù)據(jù)的地方;而主內(nèi)存也就是指的是內(nèi)存,也就是原始的共享變量存放的位置。
一個(gè)線程A對(duì)共享變量1的修改對(duì)線程B可見,需要經(jīng)過下列步驟:
要實(shí)現(xiàn)共享變量的可見性必須保證下列兩點(diǎn):
可見性舉例
一個(gè)雙核 CPU 系統(tǒng)架構(gòu),每個(gè)核有自己的控制器和運(yùn)算器,其中控制器包含一組寄存器和操作控制器,運(yùn)算器執(zhí)行算術(shù)邏輔運(yùn)算。CPU的每個(gè)核都有自己的一級(jí)緩存,在有些架構(gòu)里面還有一個(gè)所有CPU都共享的二級(jí)緩存。
1、線程A首先獲取共享變量X的值,由于兩級(jí)Cache都沒有命中,所以加載主內(nèi)存中X的值,假如為0。然后把X=0的值緩存到兩級(jí)緩存,線程A修改X的值為1,然后將其寫入兩級(jí)Cache,并且刷新到主內(nèi)存。線程A操作完畢后,線程A所在的CPU的兩級(jí)Cache內(nèi)和主內(nèi)存里面的X的值都是l。
2、線程B獲取X的值,首先一級(jí)緩存沒有命中,然后看二級(jí)緩存,二級(jí)緩存命中了,所以返回X=1;到這里一切都是正常的,因?yàn)檫@時(shí)候主內(nèi)存中也是X=l。然后線程B修改X的值為2,并將其存放到線程2所在的一級(jí)Cache和共享二級(jí)Cache中,最后更新主內(nèi)存中X的值為2,到這里一切都是好的。
3、線程A這次又需要修改X的值,獲取時(shí)一級(jí)緩存命中,并且X=l這里問題就出現(xiàn)了,明明線程B已經(jīng)把X的值修改為2,為何線程A獲取的還是l呢?這就是共享變量的內(nèi)存不可見問題,也就是線程B寫入的值對(duì)線程A不可見。
共享變量可見性的實(shí)現(xiàn)
Java中可以通過synchronized、volatile、java concurrent類來實(shí)現(xiàn)共享變量的可見性。
synchronized
使用synchronized可以保證原子性(synchronized代碼塊內(nèi)容要么不執(zhí)行,要執(zhí)行就保證全部執(zhí)行完畢)和可見性,修改后的代碼為在write和read方法上加synchronized關(guān)鍵字。
JMM關(guān)于Synchronized的兩條規(guī)定:
synchronized 實(shí)際上是對(duì)訪問修改共享變量的代碼塊進(jìn)行加互斥鎖,多個(gè)線程對(duì)synchronized代碼塊的訪問時(shí),某一時(shí)刻僅僅有一個(gè)線程在訪問和修改代碼塊中的內(nèi)容(加鎖),其他所有的線程等待該線程離開代碼塊時(shí)(釋放鎖)才有機(jī)會(huì)進(jìn)入synchronized代碼塊。
某一個(gè)線程進(jìn)入synchronized代碼塊前后,執(zhí)行過程入如下:
隨后,其他代碼在進(jìn)入synchronized代碼塊的時(shí)候,所讀取到的工作內(nèi)存上共享變量的值都是上一個(gè)線程修改后的最新值。
注意,synchronized加鎖后用到的變量才會(huì)從主內(nèi)存拉取、才會(huì)修改后刷新回主內(nèi)存。
舉例說明:
該代碼程序不會(huì)進(jìn)入死循環(huán),分析執(zhí)行過程:
從主內(nèi)存拷貝共享變量最新的值到工作內(nèi)存成為副本(該步驟省略,因?yàn)樗锩鏇]有代碼塊(不知道是否可以這么理解));
執(zhí)行代碼(該步驟省略,因?yàn)闆]有代碼);
將修改后的副本的值刷新會(huì)主內(nèi)存中(該步驟省略,因?yàn)闆]有代碼);
特別情況:
在上個(gè)例子的基礎(chǔ)上加上一段代碼:System.out.println(isStop),查看打印日志:
沒有達(dá)到自己預(yù)期的效果:結(jié)束前一次貌似應(yīng)該打印true;
上面代碼其實(shí)有兩種結(jié)果:
具體原因:
System.out.println(boolean x)方法其實(shí)也含有一個(gè)synchronized鎖。
結(jié)果1出現(xiàn)的情況:where時(shí),isStop=false,第一個(gè)鎖o對(duì)象鎖清空工作內(nèi)存后重新從內(nèi)存得到isStop=true(幾率小)
結(jié)果2出現(xiàn)的情況:where時(shí),isStop=false,第一個(gè)鎖o對(duì)象鎖清空工作內(nèi)存后重新從內(nèi)存得到isStop=false,println得到的參數(shù)值為false(值傳遞),println的鎖PrintStream對(duì)象鎖清空工作內(nèi)存,再次where時(shí),isStop=true(幾率大)。
volatile
volatile變量每次被線程訪問時(shí),都強(qiáng)迫線程從主內(nèi)存中重讀該變量的最新值,而當(dāng)該變量發(fā)生修改變化時(shí),也會(huì)強(qiáng)迫線程將最新的值刷新回主內(nèi)存。這樣一來,不同的線程都能及時(shí)的看到該變量的最新值。
但是volatile不能保證變量更改的原子性
比如number++,這個(gè)操作實(shí)際上是三個(gè)操作的集合(讀取number,number加1,將新的值寫回number),volatile只能保證每一 步的操作對(duì)所有線程是可見的,但是假如兩個(gè)線程都需要執(zhí)行number++,那么這一共6個(gè)操作集合,之間是可能會(huì)交叉執(zhí)行的,那么最后導(dǎo)致number 的結(jié)果可能會(huì)不是所期望的。
舉例說明:
程序不會(huì)進(jìn)入死循環(huán),原因:
isStop是被volatile修飾的,所有每次while時(shí)都是從主內(nèi)存中獲取isStop的值,當(dāng)子線程2秒鐘后修改了isStop的值為true,并刷新進(jìn)了住內(nèi)存,此后while從主內(nèi)存中獲取的isStop值為true,結(jié)束循環(huán)。
AtomicInteger:一個(gè)提供原子操作的Integer的類。在Java語言中,++i和i++操作并不是線程安全的,在使用的時(shí)候,不可避免的會(huì)用到synchronized關(guān)鍵字。而AtomicInteger則通過一種線程安全的加減操作接口。
synchronized和volatile比較
volatile不需要同步操作,所以效率更高,不會(huì)阻塞線程,但是適用情況比較窄
volatile讀變量相當(dāng)于加鎖(即進(jìn)入synchronized代碼塊),而寫變量相當(dāng)于解鎖(退出synchronized代碼塊)
synchronized既能保證共享變量可見性,也可以保證鎖內(nèi)操作的原子性;volatile只能保證可見性
volatile適用情況
對(duì)變量的寫入操作不依賴當(dāng)前值
比如自增自減、number = number + 5等(不滿足)
當(dāng)前volatile變量不依賴于別的volatile變量
比如 volatile_var > volatile_var2這個(gè)不等式(不滿足)
特殊操作會(huì)從主內(nèi)存中拉取值
以上操作會(huì)從主內(nèi)存拉去值到工作內(nèi)存,所有如果在上面的例子的weile循環(huán)中有這些操作,不會(huì)造成死循環(huán)。
變量隔離
多線程之間就是因?yàn)閿?shù)據(jù)共享在多個(gè)線程才導(dǎo)致了線程不安全,這就要求線程間的數(shù)據(jù)需要隔離,從根本上解決了線程安全問題。
ThreadLocal
提供線程局部變量;一個(gè)線程局部變量在多個(gè)線程中,分別有獨(dú)立的值(副本)。
使用ThreadLocal的好處
ThreadLocal主要方法
主要是initialValue、set、get、remove這幾個(gè)方法
- initialValue方法返回當(dāng)前線程對(duì)應(yīng)的“初始值”,這是一個(gè)延遲加載的方法,只有在調(diào)用get的時(shí)候,才會(huì)觸發(fā)。
- 當(dāng)線程第一次使用get方法訪問變量時(shí),將調(diào)用initialValue方法,除非線程先前調(diào)用了set方法,在這種情況下,不會(huì)為線程調(diào)用本initialValue方法。
- 通常,每個(gè)線程最多調(diào)用一次initialValue()方法,但如果已經(jīng)調(diào)用了一次remove()后,再調(diào)用get(),則可以再次調(diào)用initialValue(),相當(dāng)于第一次調(diào)用get()。
- 如果不重寫initialValue()方法,這個(gè)方法會(huì)返回null。一般使用匿名內(nèi)部類的方法來重寫initialValue()方法,以便在后續(xù)使用中可以初始化副本對(duì)象。
ThreadLocal源碼分析
Thread、ThreadLocal、ThreadLocalMap三者的關(guān)系:
每個(gè)Thread對(duì)象都有一個(gè)ThreadLocalMap,每個(gè)ThreadLocalMap可以存儲(chǔ)多個(gè)ThreadLocal。
get方法
get方法是先取出當(dāng)前線程的ThreadLocalMap,然后調(diào)用map.getEntry方法,把本ThreadLocal的引用作為參數(shù)傳入,取出map中屬于本ThreadLocal的value。
注意:這個(gè)map以及map中的key和value都是保存在線程中ThreadLocalMap的,而不是保存在ThreadLocal中
getMap方法:
獲取到當(dāng)前線程內(nèi)的ThreadLocalMap對(duì)象。每個(gè)線程內(nèi)都有ThreadLocalMap對(duì)象,名為threadLocals,初始值為null。
set方法
把當(dāng)前線程需要全局共享的value傳入
initialValue方法
這個(gè)方法沒有默認(rèn)實(shí)現(xiàn),如果要用initialValue方法,需要自己實(shí)現(xiàn),通常使用匿名內(nèi)部類的方式實(shí)現(xiàn)
remove方法
刪除對(duì)應(yīng)這個(gè)線程的值。
ThreadLocalMap類
ThreadLocalMap類,也就是Thread.threadLocals。
ThreadLocalMap 類是每個(gè)線程Thread類里面的變量,但ThreadLocalMap這個(gè)靜態(tài)內(nèi)部類定義在ThreadLocal類中,其中發(fā)現(xiàn)這一行代碼
private Entry[] table;里面最重要的是一個(gè)鍵值對(duì)數(shù)組Entry[] table,可以認(rèn)為是一個(gè)map,鍵值對(duì):
- 鍵:這個(gè)ThreadLocal
- 值:實(shí)際需要的成員變量,比如User或者SimpleDateFormat對(duì)象
這個(gè)思路和HashMap一樣,那么我們可以把它想象成HashMap來分析,但是實(shí)現(xiàn)上略有不同。
比如處理沖突方式不同,HashMap采用鏈地址法,而ThreadLocalMap采用的是線性探測(cè)法,也就是如果發(fā)生沖突,就繼續(xù)找下一個(gè)空位置,而不是用鏈表拉鏈
通過源碼分析可以看出,setInitialValue和直接set最后都是利用map.set()方法來設(shè)置值,最后都會(huì)對(duì)應(yīng)到ThreadLocalMap的一個(gè)Entry
ThreadLocal注意事項(xiàng)
1.ThreadLocal內(nèi)存泄漏問題
ThreadLocalMap中的Entry繼承自 WeakReference,是弱引用,ThreadLocal可能出現(xiàn)Value泄漏!
什么是內(nèi)存泄漏:某個(gè)對(duì)象不再有用,但是占用的內(nèi)存卻不能被回收
弱引用:通過WeakReference類實(shí)現(xiàn)的,在GC的時(shí)候,不管內(nèi)存空間足不足都會(huì)回收這個(gè)對(duì)象,適用于內(nèi)存敏感的緩存,ThreadLocal中的key就用到了弱引用,有利于內(nèi)存回收。
強(qiáng)引用:我們平日里面的用到的new了一個(gè)對(duì)象就是強(qiáng)引用,例如 Object obj = new Object();當(dāng)JVM的內(nèi)存空間不足時(shí),寧愿拋出OutOfMemoryError使得程序異常終止也不愿意回收具有強(qiáng)引用的存活著的對(duì)象。
ThreadLocalMap 的每個(gè) Entry 都是一個(gè)對(duì)key的弱引用,同時(shí),每個(gè) Entry 都包含了一個(gè)對(duì)value的強(qiáng)引用,如下:
static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k); // key值給WeakReference處理value = v; // value直接用變量保存,是強(qiáng)引用}}正常情況下,當(dāng)線程終止,保存在ThreadLocalMap里的value會(huì)被垃圾回收,因?yàn)闆]有任何強(qiáng)引用了。但如果線程不終止(比如線程需要保持很久),那么key對(duì)應(yīng)的value就不能被回收,因?yàn)橛幸韵碌恼{(diào)用鏈:
Thread---->ThreadLocalMap---->Entry(key為null,弱引用被回收)---->value
因?yàn)関alue和Thread之間還存在這個(gè)強(qiáng)引用鏈路,所以導(dǎo)致value無法回收,就可能會(huì)出現(xiàn)OOM。
JDK已經(jīng)考慮到了這個(gè)問題,所以在set, remove, rehash方法中會(huì)掃描key為null的Entry,并把對(duì)應(yīng)的value設(shè)置為null,這樣value對(duì)象就可以被回收。比如rehash里面調(diào)用resize,如果key回收了,那么value也設(shè)置為null,斷開強(qiáng)引用鏈路,便于垃圾回收。
private void resize() {......省略代碼ThreadLocal<?> k = e.get();if (k == null) {e.value = null; // Help the GC} ......但是如果一個(gè)ThreadLocal不被使用,那么實(shí)際上set, remove, rehash方法也不會(huì)被調(diào)用,如果同時(shí)線程又不停止,那么調(diào)用鏈就一直存在,那么就導(dǎo)致了value的內(nèi)存泄漏。
ThreadLocal如何避免內(nèi)存泄漏
及時(shí)調(diào)用remove方法,就會(huì)刪除對(duì)應(yīng)的Entry對(duì)象,可以避免內(nèi)存remove泄漏,所以使用完ThreadLocal之后,應(yīng)該調(diào)用remove方法。
比如攔截器獲取到用戶信息,用戶信息存在ThreadLocalMap中,線程請(qǐng)求結(jié)束之前攔住它,并用remove清除User對(duì)象,這樣就能穩(wěn)妥的保證不會(huì)內(nèi)存泄漏。
注意事項(xiàng)
如果在每個(gè)線程中ThreadLocal.set()進(jìn)去的東西本來就是多線程共享的同一個(gè)對(duì)象,比如static對(duì)象,那么多個(gè)線程的ThreadLocal.get()取得的還是這個(gè)共享對(duì)象本身,還是有并發(fā)訪問問題。
如果可以不使用ThreadLocal就能解決問題,那么不要強(qiáng)行使用,在任務(wù)數(shù)很少的時(shí)候,可以通過在局部變量中新建對(duì)象解決。
在Spring中,如果可以使用RequestContextHolder,那么就不需要自己維護(hù)ThreadLocal,因?yàn)樽约嚎赡軙?huì)忘記調(diào)用remove()方法等,造成內(nèi)存泄漏。
參考文章:
Java多線程里共享變量線程安全問題的原因
Java多線程共享變量控制
Java多線程超詳解
ThreadLocal詳解
總結(jié)
以上是生活随笔為你收集整理的Java多线程变量共享与隔离的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: VW 80000-2021版,最新发行,
- 下一篇: java多线程嵌套_Java多线程进阶