Java多線程學習筆記
作者:Grey
原文地址:Java多線程學習筆記
什麼是程序,進程和線程?
- 程序是計算機的可執行文件
- 進程是計算機資源分配的基本單位
- 線程是資源調度執行的基本單位
- 一個程序裏面不同的執行路徑
- 多個線程共享進程中的資源
線程和進程的關係
線程就是輕量級進程,是程序執行的最小單位。
多進程的方式也可以實現並發,為什麼我們要使用多線程?
- 共享資源在線程間的通信比較容易。
- 線程開銷更小。
進程和線程的區別?
- 進程是一個獨立的運行環境,而線程是在進程中執行的一個任務。他們兩個本質的區別是是否單獨佔有內存地址空間及其它系統資源(比如I/O)。
- 進程單獨佔有一定的內存地址空間,所以進程間存在內存隔離,數據是分開的,數據共享複雜但是同步簡單,各個進程之間互不干擾;而線程共享所屬進程佔有的內存地址空間和資源,數據共享簡單,但是同步複雜。
- 進程單獨佔有一定的內存地址空間,一個進程出現問題不會影響其他進程,不影響主程序的穩定性,可靠性高;一個線程崩潰可能影響整個程序的穩定性,可靠性較低。
- 進程單獨佔有一定的內存地址空間,進程的創建和銷毀不僅需要保存寄存器和棧信息,還需要資源的分配回收以及頁調度,開銷較大;線程只需要保存寄存器和棧信息,開銷較小。
- 進程是操作系統進行資源分配的基本單位,而線程是操作系統進行調度的基本單位,即CPU分配時間的單位。
什麼是線程切換?
從底層角度上看,CPU主要由如下三部分組成,分別是:
- ALU: 計算單元
- Registers: 寄存器組
- PC:存儲到底執行到哪條指令
T1線程在執行的時候,將T1線程的指令放在PC,數據放在Registers,假設此時要切換成T2線程,T1線程的指令和數據放cache,然後把T2線程的指令放PC,數據放Registers,執行T2線程即可。
以上的整個過程是通過操作系統來調度的,且線程的調度是要消耗資源的,所以,線程不是設置越多越好。
單核CPU設定多線程是否有意義?
有意義,因為線程的操作中可能有不消耗CPU的操作,比如:等待網絡的傳輸,或者線程sleep,此時就可以讓出CPU去執行其他線程。可以充分利用CPU資源。
- CPU密集型
- IO密集型
線程數量是不是設置地越大越好?
不是,因為線程切換要消耗資源。
示例:
單線程和多線程來累加1億個數。-> CountSum.java
工作線程數(線程池中線程數量)設多少合適?
-
和CPU的核數有關
-
最好是通過壓測來評估。通過profiler性能分析工具jProfiler,或者Arthas
-
公式
N = Ncpu * Ucpu * (1 + W/C)
其中:
-
Ncpu是處理器的核的數目,可以通過Runtime.getRuntime().availableProcessors() 得到
-
Ucpu是期望的CPU利用率(該值應該介於0和1之間)
-
W/C是等待時間和計算時間的比率。
Java中創建線程的方式
- 繼承Thread類,重寫run方法
- 實現Runnable接口,實現run方法,這比方式1更好,因為一個類實現了Runnable以後,還可以繼承其他類
- 使用lambda表達式
- 通過線程池創建
- 通過Callable/Future創建(需要返回值的時候)
具體示例可見:HelloThread.java
線程狀態
- NEW
線程剛剛創建,還沒有啟動
即:剛剛New Thread的時候,還沒有調用start方法時候,就是這個狀態
- RUNNABLE
可運行狀態,由線程調度器可以安排執行,包括以下兩種情況:
- READY
- RUNNING
READY和RUNNING通過yield來切換
- WAITING
等待被喚醒
- TIMED_WAITING
隔一段時間後自動喚醒
- BLOCKED
被阻塞,正在等待鎖
只有在synchronized的時候在會進入BLOCKED狀態
- TERMINATED
線程執行完畢後,是這個狀態
線程狀態切換
線程基本操作
sleep
當前線程睡一段時間
yield
這是一個靜態方法,一旦執行,它會使當前線程讓出一下CPU。但要注意,讓出CPU並不表示當前線程不執行了。當前線程在讓出CPU後,還會進行CPU資源的爭奪,但是是否能夠再次被分配到就不一定了。
join
等待另外一個線程的結束,當前線程才會運行
public class ThreadBasicOperation {
static volatile int sum = 0;
public static void main(String[] args) throws Exception {
Thread t = new Thread(()->{
for (int i = 1; i <= 100; i++) {
sum += i;
}
});
t.start();
// join 方法表示主線程願意等待子線程執行完畢後才繼續執行
// 如果不使用join方法,那麼sum輸出的可能是一個很小的值,因為還沒等子線程
// 執行完畢後,主線程就已經執行了打印sum的操作
t.join();
System.out.println(sum);
}
}
示例代碼:ThreadBasicOperation.java
interrupt
- interrupt()
打斷某個線程(設置標誌位)
- isInterrupted()
查詢某線程是否被打斷過(查詢標誌位)
- static interrupted
查詢當前線程是否被打斷過,並重置打斷標誌位
示例代碼:ThreadInterrupt.java
如何結束一個線程
不推薦的方式
- stop方法
- suspend/resume方法
以上兩種方式都不建議使用, 因為會產生數據不一致的問題,因為會釋放所有的鎖。
優雅的方式
如果不依賴循環的具體次數或者中間狀態, 可以通過設置標誌位的方式來控制
public class ThreadFinished {
private static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
// 推薦方式:設置標誌位
Thread t3 = new Thread(() -> {
long i = 0L;
while (flag) {
i++;
}
System.out.println("count sum i = " + i);
});
t3.start();
TimeUnit.SECONDS.sleep(1);
flag = false;
}
}
如果要依賴循環的具體次數或者中間狀態, 則可以用interrupt方式
public class ThreadFinished {
public static void main(String[] args) throws InterruptedException {
// 推薦方式:使用interrupt
Thread t4 = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
}
System.out.println("t4 end");
});
t4.start();
TimeUnit.SECONDS.sleep(1);
t4.interrupt();
}
}
示例代碼: ThreadFinished.java
並發編程的三大特性
可見性
每個線程會保存一份拷貝到線程本地緩存,使用volatile,可以保持線程之間數據可見性。
如下示例: ThreadVisible.java
public class ThreadVisible {
static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(flag) {
// 如果這裡調用了System.out.println()
// 會無論flag有沒有加volatile,數據都會同步
// 因為System.out.println()背後調用的synchronized
// System.out.println();
}
System.out.println("t end");
});
t.start();
TimeUnit.SECONDS.sleep(3);
flag = false;
// volatile修飾引用變量
new Thread(a::m,"t2").start();
TimeUnit.SECONDS.sleep(2);
a.flag = false;
// 阻塞主線程,防止主線程直接執行完畢,看不到效果
new Scanner(System.in).next();
}
private static volatile A a = new A();
static class A {
boolean flag = true;
void m() {
System.out.println("m start");
while(flag){}
System.out.println("m end");
}
}
}
代碼說明:
- 如在上述代碼的死循環中增加了System.out.println(), 則會強制同步flag的值,無論flag本身有沒有加volatile。
- 如果volatile修飾一個引用對象,如果對象的屬性(成員變量)發生了改變,volatile不能保證其他線程可以觀察到該變化。
關於三級緩存
如上圖,內存讀出的數據會在L3,L2,L1上都存一份。所謂線程數據的可見性,指的就是內存中的某個數據,假如第一個CPU的一個核讀取到了,和其他的核讀取到這個數據之間的可見性。
在從內存中讀取數據的時候,根據的是程序局部性的原理,按塊來讀取,這樣可以提高效率,充分發揮總線CPU針腳等一次性讀取更多數據的能力。
所以這裡引入了一個緩存行的概念,目前一個緩存行多用64個位元組來表示。
如何來驗證CPU讀取緩存行這件事,我們可以通過一個示例來說明:
public class CacheLinePadding {
public static T[] arr = new T[2];
static {
arr[0] = new T();
arr[1] = new T();
}
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> {
for (long i = 0; i < 1000_0000L; i++) {
arr[0].x = i;
}
});
Thread t2 = new Thread(() -> {
for (long i = 0; i < 1000_0000L; i++) {
arr[1].x = i;
}
});
final long start = System.nanoTime();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println((System.nanoTime() - start) / 100_0000);
}
private static class Padding {
public volatile long p1, p2, p3, p4, p5, p6, p7;
}
private static class T /**extends Padding*/ {
public volatile long x = 0L;
}
}
說明:以上代碼,T這個類extends Padding與否,會影響整個流程的執行時間,如果繼承了,會減少執行時間,因為繼承Padding後,arr[0]和arr[1]一定不在同一個緩存行裏面,所以不需要同步數據,速度就更快一些了。
jdk1.8增加了一個註解:@Contended,標註了以後,不會在同一緩存行, 僅適用於jdk1.8
還需要增加jvm參數
-XX:-RestrictContended
CPU為每個緩存行標記四種狀態(使用兩位)
- Exclusive
- Invalid
- Shared
- Modified
有序性
為什麼會出現亂序執行呢?因為CPU為了提高效率,可能在執行某些指令的時候,不按順序執行(指令前後沒有依賴關係的時候)
亂序存在的條件是:不影響單線程的最終一致性(as – if – serial)
驗證亂序執行的程序示例 DisOrder.java:
public class DisOrder {
private static int x = 0, y = 0;
private static int a = 0, b = 0;
// 以下程序可能會執行比較長的時間
public static void main(String[] args) throws InterruptedException {
int i = 0;
for (;;) {
i++;
x = 0;
y = 0;
a = 0;
b = 0;
Thread one = new Thread(() -> {
// 由於線程one先啟動,下面這句話讓它等一等線程two. 讀着可根據自己電腦的實際性能適當調整等待時間.
shortWait(100000);
a = 1;
x = b;
});
Thread other = new Thread(() -> {
b = 1;
y = a;
});
one.start();
other.start();
one.join();
other.join();
String result = "第" + i + "次 (" + x + "," + y + ")";
if (x == 0 && y == 0) {
// 出現這個分支,說明指令出現了重排
// 否則不可能 x和y同時都為0
System.err.println(result);
break;
} else {
// System.out.println(result);
}
}
}
public static void shortWait(long interval) {
long start = System.nanoTime();
long end;
do {
end = System.nanoTime();
} while (start + interval >= end);
}
}
如上示例,如果指令不出現亂序,那麼x和y不可能同時為0,通過執行這個程序可以驗證出來,在我本機測試的結果是:
執行到第1425295次 出現了x和y同時為0的情況。
原子性
程序的原子性是指整個程序中的所有操作,要麼全部完成,要麼全部失敗,不可能滯留在中間某個環節;在多個線程一起執行的時候,一個操作一旦開始,就不會被其他線程所打斷。
一個示例:
class T {
m = 9;
}
對象T在創建過程中,背後其實是包含了多條執行語句的,由於有CPU亂序執行的情況,所以極有可能會在初始化過程中生成以一個半初始化對象t,這個t的m等於0(還沒有來得及做賦值操作)
所以,不要在某個類的構造方法中啟動一個線程,這樣會導致this對象逸出,因為這個類的對象可能還來不及執行初始化操作,就啟動了一個線程,導致了異常情況。
volatile一方面可以保證線程數據之間的可見性,另外一方面,也可以防止類似這樣的指令重排,所以
所以,單例模式中,DCL方式的單例一定要加volatile修飾:
public class Singleton6 {
private volatile static Singleton6 INSTANCE;
private Singleton6() {
}
public static Singleton6 getInstance() {
if (INSTANCE == null) {
synchronized (Singleton6.class) {
if (INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Singleton6();
}
}
}
return INSTANCE;
}
}
具體可以參考設計模式學習筆記 中單例模式的說明。
CAS
比較與交換的意思
舉個例子:
內存有個值是3,如果用Java通過多線程去訪問這個數,每個線程都要把這個值+1。
之前是需要加鎖,即synchronized關鍵字來控制。但是JUC的包出現後,有了CAS操作,可以不需要加鎖來處理,流程是:
第一個線程:把3拿過來,線程本地區域做計算+1,然後把4寫回去,
第二個線程:也把3這個數拿過來,線程本地區域做計算+3後,在回寫回去的時候,會做一次比較,如果原來的值還是3,那麼說明這個值之前沒有被打擾過,就可以把4寫回去,如果這個值變了,假設變為了4,那麼說明這個值已經被其他線程修改過了,那麼第二個線程需要重新執行一次,即把最新的4拿過來繼續計算,回寫回去的時候,繼續做比較,如果內存中的值依然是4,說明沒有其他線程處理過,第二個線程就可以把5回寫回去了。
流程圖如下:
ABA問題
CAS會出現一個ABA的問題,即在一個線程回寫值的時候,其他線程其實動過那個原始值,只不過其他線程操作後這個值依然是原始值。
如何來解決ABA問題呢?
我們可以通過版本號或者時間戳來控制,比如數據原始的版本是1.0,處理後,我們把這個數據的版本改成變成2.0版本, 時間戳來控制也一樣,
以Java為例,AtomicStampedReference這個類,它內部不僅維護了對象值,還維護了一個時間戳。
當AtomicStampedReference對應的數值被修改時,除了更新數據本身外,還必須要更新時間戳。
當AtomicStampedReference設置對象值時,對象值以及時間戳都必須滿足期望值,寫入才會成功。
因此,即使對象值被反覆讀寫,寫回原值,只要時間戳發生變化,就能防止不恰當的寫入。
CAS的底層實現
Unsafe.cpp–>Atom::cmpxchg–>Atomic_linux_x86_inline.hpp–>調用了彙編的LOCK_IF_MP方法
Multiple_processor
lock cmpxchg
雖然cmpxchg指令不是原子的,但是加了lock指令後,則cmpxhg被上鎖,不允許被打斷。 在單核CPU中,無須加lock,在多核CPU中,必須加lock,可以參考stackoverflow上的這個回答:
is-x86-cmpxchg-atomic-if-so-why-does-it-need-lock
使用CAS好處
jdk早期是重量級別鎖 ,通過0x80中斷 進行用戶態和內核態轉換,所以效率比較低,有了CAS操作,大大提升了效率。
對象的內存布局(Hotspot實現)
使用jol查看一個對象的內存布局
我們可以通過jol包來查看一下某個對象的內存布局
引入jol依賴
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.15</version>
</dependency>
示例代碼(ObjectModel.java)
public class ObjectModel {
public static void main(String[] args) {
T o = new T();
String s = ClassLayout.parseInstance(o).toPrintable();
System.out.println(s);
}
}
class T{
}
配置VM參數,開啟指針壓縮
-XX:+UseCompressedClassPointers
運行結果如下:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
8 4 (object header: class) 0x00067248
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
其中8個位元組的markword
4個位元組的類型指針,可以找到T.class
這裡一共是12個位元組, 由於位元組數務必是8的整數倍,所以補上4個位元組,共16個位元組
我們修改一下T這個類
class T{
public int a = 3;
public long b = 3l;
}
再次執行,可以看到結果是
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
8 4 (object header: class) 0x00067248
12 4 int T.a 3
16 8 long T.b 3
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
其中多了4位表示int這個成員變量,多了8位表示long這個成員變量, 相加等於24,正好是8的整數倍,不需要補齊。
內存布局詳細說明
使用synchronized就是修改了對象的markword信息,markword中還記錄了GC信息,Hashcode信息
鎖升級過程
偏向鎖
synchronized代碼段多數時間是一個線程在運行,誰先來,這個就偏向誰,用當前線程標記一下。
輕量級鎖(自旋鎖,無鎖)
偏向鎖撤銷,然後競爭,每個線程在自己線程棧中存一個LR(lock record)鎖記錄
偏向鎖和輕量級鎖都是用戶空間完成的,重量級鎖需要向操作系統申請。
兩個線程爭搶的方式將lock record的指針,指針指向哪個線程的LR,哪個線程就拿到鎖,另外的線程用CAS的方式繼續競爭
重量級鎖
JVM的ObjectMonitor去操作系統申請。
如果發生異常,synchronized會自動釋放鎖
interpreteRuntime.cpp –> monitorenter
鎖重入
synchronized是可重入鎖
可重入次數必須記錄,因為解鎖需要對應可重入次數的記錄
偏向鎖:記錄在線程棧中,每重入一次,LR+1,備份原來的markword
輕量級鎖:類似偏向鎖
重量級鎖:記錄在ObjectMonitor的一個字段中
自旋鎖什麼時候升級為重量級鎖?
- 有線程超過十次自旋
- -XX:PreBlockSpin(jdk1.6之前)
- 自旋的線程超過CPU核數一半
- jdk1.6 以後,JVM自己控制
為什麼有偏向鎖啟動和偏向鎖未啟動?
未啟動:普通對象001
已啟動:匿名偏向101
為什麼有自旋鎖還需要重量級鎖?
因為自旋會佔用CPU時間,消耗CPU資源,如果自旋的線程多,CPU資源會被消耗,所以會升級成重量級鎖(隊列)例如:ObjectMonitor裏面的WaitSet,重量級鎖會把線程都丟到WaitSet中凍結, 不需要消耗CPU資源
偏向鎖是否一定比自旋鎖效率高?
明確知道多線程的情況下,不一定。
因為偏向鎖在多線程情況下,會涉及到鎖撤銷,這個時候直接使用自旋鎖,JVM啟動過程,會有很多線程競爭,比如啟動的時候,肯定是多線程的,所以默認情況,啟動時候不打開偏向鎖,過一段時間再打開。
有一個參數可以配置:BiasedLockingStartupDelay默認是4s鍾
偏向鎖狀態下,調用了wait方法,直接升級成重量級鎖
一個線程拿20個對象進行加鎖,批量鎖的重偏向(20個對象),批量鎖撤銷(變成輕量級鎖)(40個對象), 通過Epoch中的值和對應的類對象裏面記錄的值比較。
synchronized
鎖定對象
public class SynchronizedObject implements Runnable {
static SynchronizedObject instance = new SynchronizedObject();
final Object object = new Object();
static volatile int i = 0;
@Override
public void run() {
for (int j = 0; j < 1000000; j++) {
// 任何線程要執行下面的代碼,必須先拿到object的鎖
synchronized (object) {
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
鎖定方法
- 鎖定靜態方法相當於鎖定當前類
public class SynchronizedStatic implements Runnable {
static SynchronizedStatic instance = new SynchronizedStatic();
static volatile int i = 0;
@Override
public void run() {
increase();
}
// 相當於synchronized(SynchronizedStatic.class)
synchronized static void increase() {
for (int j = 0; j < 1000000; j++) {
i++;
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
- 鎖定非靜態方法相當於鎖定該對象的實例或synchronized(this)
public class SynchronizedMethod implements Runnable {
static SynchronizedMethod instance = new SynchronizedMethod();
static volatile int i = 0;
@Override
public void run() {
increase();
}
void increase() {
for (int j = 0; j < 1000000; j++) {
// 任何線程要執行下面的代碼,必須先拿到object的鎖
synchronized (this) {
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
臟讀
public class DirtyRead {
String name;
double balance;
public static void main(String[] args) {
DirtyRead a = new DirtyRead();
Thread thread = new Thread(() -> a.set("zhangsan", 100.0));
thread.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(a.getBalance("zhangsan"));
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(a.getBalance("zhangsan"));
}
public synchronized void set(String name, double balance) {
this.name = name;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.balance = balance;
}
// 如果get方法不加synchronized關鍵字,就會出現臟讀情況
public /*synchronized*/ double getBalance(String name) {
return this.balance;
}
}
其中的getBalance方法,如果不加synchronized,就會產生臟讀的問題。
可重入鎖
一個同步方法可以調用另外一個同步方法,
一個線程已經擁有某個對象的鎖,再次申請的時候仍然會得到該對象的鎖(可重入鎖)
子類synchronized,如果調用父類的synchronize方法:super.method(),如果不可重入,直接就會死鎖。
public class SynchronizedReentry implements Runnable {
public static void main(String[] args) throws IOException {
SynchronizedReentry myRun = new SynchronizedReentry();
Thread thread = new Thread(myRun, "t1");
Thread thread2 = new Thread(myRun, "t2");
thread.start();
thread2.start();
System.in.read();
}
synchronized void m1(String content) {
System.out.println(this);
System.out.println("m1 get content is " + content);
m2(content);
}
synchronized void m2(String content) {
System.out.println(this);
System.out.println("m2 get content is " + content);
}
@Override
public void run() {
m1(Thread.currentThread().getName());
}
}
程序在執行過程中,如果出現異常,默認情況鎖會被釋放 ,所以,在並發處理的過程中,有異常要多加小心,不然可能會發生不一致的情況。比如,在一個web app處理過程中,多個servlet線程共同訪問同一個資源,這時如果異常處理不合適, 在第一個線程中拋出異常,其他線程就會進入同步代碼區,有可能會訪問到異常產生時的數據。因此要非常小心的處理同步業務邏輯中的異常。
示例見:
SynchronizedException.java
synchronized的底層實現
在早期的JDK使用的是OS的重量級鎖
後來的改進鎖升級的概念:
synchronized (Object)
- markword 記錄這個線程ID (使用偏向鎖)
- 如果線程爭用:升級為 自旋鎖
- 10次自旋以後,升級為重量級鎖 – OS
所以:
- 執行時間短(加鎖代碼),線程數少,用自旋
- 執行時間長,線程數多,用系統鎖
synchronized不能鎖定String常量,Integer,Long等基礎類型
見示例:
SynchronizedBasicType.java
如何模擬死鎖
public class DeadLock implements Runnable {
int flag = 1;
static Object o1 = new Object();
static Object o2 = new Object();
public static void main(String[] args) {
DeadLock lock = new DeadLock();
DeadLock lock2 = new DeadLock();
lock.flag = 1;
lock2.flag = 0;
Thread t1 = new Thread(lock);
Thread t2 = new Thread(lock2);
t1.start();
t2.start();
}
@Override
public void run() {
System.out.println("flag = " + flag);
if (flag == 1) {
synchronized (o2) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println("1");
}
}
}
if (flag == 0) {
synchronized (o1) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("0");
}
}
}
}
}
volatile
- 保持線程之間的可見性(不保證操作的原子性),依賴這個MESI協議
- 防止指令重排序,CPU的load fence和store fence原語支持
CPU原來執行指令一步一步執行,現在是流水線執行,編譯以後可能會產生指令的重排序,這樣可以提高性能
DCL為什麼一定要加volatile?
DCL示例:
public class Singleton6 {
private volatile static Singleton6 INSTANCE;
private Singleton6() {
}
public static Singleton6 getInstance() {
if (INSTANCE == null) {
synchronized (Singleton6.class) {
if (INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Singleton6();
}
}
}
return INSTANCE;
}
}
在New對象的時候,編譯完實際上是分了三步
- 對象申請內存,成員變量會被賦初始值
- 成員變量設為真實值
- 成員變量賦給對象
指令重排序可能會導致2和3進行指令重排,導致下一個線程拿到一個半初始化的對象,導致單例被破壞。所以DCL必須加Volitile