一.Java內存模型
學習Java內存模型的思路:
1.Java 內存模型是什麼
2.解決什麼問題及具體是怎麼解決的
3.內存劃分
4.垃圾回收機制
5.內存泄露分析
背景:
CPU 和緩存一致性(可見性):CPU 和主存之間增加緩存,在多線程場景下會存在緩存一致性問題,原因是多核多線程
處理器優化(原子性):處理器可能會對輸入代碼進行亂序執行處理
指令重排(有序性):編譯器指令重排
這三個問題會導致併發變成中線程對數據操作的不一致性問題。
怎麼解決:
java內存模型就是解決以上問題提供的一種思路或解決方案
1.Java 內存模型是什麼
Java 內存模型(Java Memory Model,JMM)就是一種符合內存模型規範的,屏蔽了各種硬件和操作系統的訪問差異的,保證了 Java 程序在各種平臺下對內存的訪問都能保證效果一致的機制及規範
一句話總結:內存模型就是合理操作內存的一種規範,目的是保證併發編程場景中的原子性、可見性和有序性。
具體規範:
1.1: 內存分爲:主內存+線程內存(工作內存),所有變量都存儲在主內存,每個線程都有自己的工作內存
1.2: 線程內存中保存了該線程操作的變量的主內存副本,線程對變量的所有操作都必須在工作內存中進行,不能直接讀寫主內存。
1.3: 不同線程之間不能訪問對方內存中的變量.線程間變量的傳遞需要自己的工作內存和主內存之間進行數據同步
具體實現:
1.1: 關鍵字:Volatile、Synchronized、Final、Concurren等
其實這些就是 Java 內存模型封裝了底層的實現後提供給程序員使用的一些關鍵字,解決併發編程原子性、有序性和一致性的問題
2.爲什麼要有 Java 內存模型,Java 內存模型解決了什麼問題等
原子性:爲了保證原子性,提供了兩個高級的字節碼指令 Monitorenter 和 Monitorexit,對應Synchronized
可見性:被其修飾的變量在被修改後可以立即同步到主內存。被其修飾的變量在每次使用之前都從主內存刷新,對應volatile(除了 Volatile,Java 中的 Synchronized 和 Final 兩個關鍵字也可以實現可見性)
有序性:可以使用 Synchronized 和 Volatile 來保證多線程之間操作的有序性,實現方式有所區別:Volatile 關鍵字會禁止指令重排。Synchronized 關鍵字保證同一時刻只允許一條線程操作。
3.內存劃分
虛擬機棧,堆,方法區,程序計數器,本地方法棧
線程私有:虛擬機棧,本地方法棧,程序計數器 這三塊是不進行垃圾回收的,他們的生命週期是同線程同步的
線程共享:堆,方法區 這兩塊內存是需要GC的
4.垃圾回收機制
虛擬機把堆內存劃分成新生代(Young Generation)、老年代(Old Generation)和永久代(Permanent Generation)3個區域
爲什麼要分代:提高GC效率
GC算法:
1.引用計數算法:給對象添加一個引用計數器,每當有一個地方引用它時,計數器值就加1
缺點:無法判斷對象之間相互循環引用
2.可達性分析算法
2.1:什麼是GC Roots
Java虛擬機中所有引用的根對象,具體那些對象可以作爲GC ROOT,如下:
- Class 由System Class Loader/Boot Class Loader加載的類對象,這些對象不會被回收。需要注意的是其它的Class Loader實例加載的類對象不一定是GC root,除非這個類對象恰好是其它形式的GC root;
- Thread 線程,激活狀態的線程;
- Stack Local 棧中的對象。每個線程都會分配一個棧,棧中的局部變量或者參數都是GC root,因爲它們的引用隨時可能被用到;
- JNI Local JNI中的局部變量和參數引用的對象;可能在JNI中定義的,也可能在虛擬機中定義
- JNI Global JNI中的全局變量引用的對象;同上
- Monitor Used 用於保證同步的對象,例如wait(),notify()中使用的對象、鎖等。
- Held by JVM JVM持有的對象。JVM爲了特殊用途保留的對象,它與JVM的具體實現有關。比如有System Class Loader, 一些Exceptions對象,和一些其它的Class Loader。對於這些類,JVM也沒有過多的信息。
3.從GC Roots垃圾回收機制分析內存泄露問題
垃圾回收器不會回收GC Roots以及那些被它們間接引用的對象,這是造成內存泄露的根本原因。
分析內存泄露時從該點出發,找到引用鏈上的那些對象是否存在一直被GC ROOTS持有。消除這些一直被持有的對象,做到隨用隨創建,用完就回收的原則。
GC ROOTS參考:
內存泄露分析參考:
https://cloud.tencent.com/developer/article/1334692
二.volatile與synchronized區別
volatile
當一個變量定義爲volatile後,它將具備兩種特性:1. 可見性,2. 禁止指令重排序。
即:線程A對volatile變量a做了修改,其它線程是能拿到該變量a的最新值。避免了線程安全問題。
我們在寫線程安全的單例模式時,使用volatile修飾單例變量:
volatile作用:1.volatile有序性保證了指令不被重排
2.volatile可見性,保證了行程a對單例變量的修改對其它進程是安全可見的
public class SafeDoubleCheckedLocking {
//volatile作用:1.volatile有序性保證了指令不被重排
2.volatile可見性,保證了行程a對單例變量的修改對其它進程是安全可見的
private volatile static Instance instance;
public static Instance getInstance() {
if (instance == null) {//第一次判空是爲了縮小鎖的範圍,提高效率,競爭鎖是非常重的操作
synchronized (SafeDoubleCheckedLocking.class) {
if (instance == null)//第一次判空是爲確保只初始化一次
instance = new Instance(); // instance爲volatile }
}
return instance;
}
}
}
synchronized
synchronized同步體現在:一次只允許一個線程執行代碼塊。synchronized使整個線程內存與“主”內存同步。
int i3;
synchronized int geti3() {return i3;}
執行geti3()會執行以下操作:
1、線程從當前對象獲取鎖。
2、線程內存刷新所有的變量,也就是說,它的所有變量都有效地從“主”內存讀取。
3、執行代碼塊。在本例中,這意味着將返回值設置爲i3的當前值,i3可能剛剛從“主”內存中重置。
4、對變量的任何更改通常會被寫到“主”內存中,但是對於geti3(),我們沒有更改。
5、線程釋放這個對象在監視器上的鎖。
參考文章:https://www.cnblogs.com/tf-Y/p/5266710.html
http://developer.51cto.com/art/201807/579744.htm
https://www.cnblogs.com/nexiyi/p/java_memory_model_and_thread.html
注意:volatile關鍵字並不能保證線程安全,它只能保證當前線程需要該變量的值時能夠獲得最新的值,而不能保證線程修改的安全性。
三.線程安全
1.什麼是線程安全問題
網上有各種描述,每個人的理解略有差異。
自己理解的線程安全是:線程安全問題出現的前提是多線程。經常用來描繪一段代碼。指在併發的情況之下,該代碼經過多線程使用,線程的調度順序不影響任何結果
2.爲什麼會有線程安全問題
線程安全問題產生的根本原因是:共享數據存在被併發修改的可能,即一個線程讀取時,允許另一個線程修改。
從java的內存模型角度分析:java的內存模型:每個線程都有自己的私有內存空間(線程內存),進程有共享內存(主內存),共享數據是存儲在主內存,同時共享數據也會在每個操作它的線程中有一份拷貝。線程修改共享數據時會從主內存中獲取一份拷貝到自己的私有內存空間,操作完後會同步到主內存。這樣會導致,線程ThreadA修改完共享數據後還沒有完全同步到主內存,導致其它線程讀取共享數據的時候,沒有讀取到最新修改值,出現了線程安全問題。不安全體現在:線程讀取到的共享數據有可能不是最新的。該內存模型會導致天生就導致了線程安全問題。
線程安全產生的條件:多線程 修改 同一個對象
注意:多線程讀取一個對象時不會發生線程安全問題。因爲該對象的值是不變的。一定是被修改了,對其它線程的訪問造成了影響。
3 Java中怎麼解決線程安全問題
解決線程安全問題時,最直接的是找出產生線程安全問題的原因和條件,然後消除掉這些條件。解題思路:
1.“廢除”共享變量
因爲多線程對共享變量同時操作,導致出現了訪問不一致的問題,所以可以考慮將全局共享變量改成局部變量或者變成線程私有變量
如:ThreadLocal,ThreadLocal存儲的是每個線程私有的數據,各個線程間獨立操作,互不影響,這種處理方式是以犧牲空間換取訪問安全的一種方式。這種方式雖然解決了線程安全問題,但是也阻斷了線程間的通信。即:兩個線程各玩各的。
1.什麼是ThreadLocal
/**
* Implements a thread-local storage, that is, a variable for which each thread
* has its own value. All threads share the same {@code ThreadLocal} object,
* but each sees a different value when accessing it, and changes made by one
* thread do not affect the other threads. The implementation supports
* {@code null} values.
*
* @see java.lang.Thread
* @author Bob Lee
*/
官方定義:1.每個線程都一份自己的私有的變量拷貝,對該變量的修改只對該線程可見
2.所有線程共享一個ThreadLocal 對象,該對象是全局的
2.ThreadLocal有什麼特點
/**
* This class provides thread-local variables. These variables differ from
* their normal counterparts in that each thread that accesses one (via its
* {@code get} or {@code set} method) has its own, independently initialized
* copy of the variable. {@code ThreadLocal} instances are typically private
* static fields in classes that wish to associate state with a thread (e.g.,
* a user ID or Transaction ID).
*
* <p>For example, the class below generates unique identifiers local to each
* thread.
* A thread's id is assigned the first time it invokes {@code ThreadId.get()}
* and remains unchanged on subsequent calls.
* <pre>
* import java.util.concurrent.atomic.AtomicInteger;
*
* public class ThreadId {
* // Atomic integer containing the next thread ID to be assigned
* private static final AtomicInteger nextId = new AtomicInteger(0);
*
* // Thread local variable containing each thread's ID
* private static final ThreadLocal<Integer> threadId =
* new ThreadLocal<Integer>() {
* @Override protected Integer initialValue() {
* return nextId.getAndIncrement();
* }
* };
*
* // Returns the current thread's unique ID, assigning it if necessary
* public static int get() {
* return threadId.get();
* }
* }
* </pre>
* <p>Each thread holds an implicit reference to its copy of a thread-local
* variable as long as the thread is alive and the {@code ThreadLocal}
* instance is accessible; after a thread goes away, all of its copies of
* thread-local instances are subject to garbage collection (unless other
* references to these copies exist).
*
* @author Josh Bloch and Doug Lea
* @since 1.2
*/
3.Android中使用ThreadLocal的場景學習分析
消息分發機制中,每個線程都有自己唯一的Looper對象。而這個Looper對象就是線程獨有,正符合ThreadLocal使用特點。
線程,ThreadLocal對象,ThreadLocalMap之間的關係:
一個線程有唯一ThreadLocalMap對象,線程是通過ThreadLocal.get()獲取ThreadLocalMap。
ThreadLocalMap存儲的元素是:key:ThreadLocal對象爲key 線程變量爲value.
總結:一個線程僅有一個ThreadLocalMap對象,可以有很多個threadlocal對象,這也是ThreadLocal設計的初衷,就是維護線程私有數據的。
參考:https://www.jianshu.com/p/98b68c97df9b
2.同步機制
如果共享變量無法轉化成局部變量或線程私有變量,那可以通過同步的機制強制線程對共享變量"獨佔訪問"