1、到底什麼叫“底層原理”?本章研究的內容是什麼?
1.1 從Java代碼到CPU指令
①最開始,我們編寫的Java代碼,是*.java文件
②在編譯(javac命令)後,從剛纔的*.java文件會變出一個新的Java字節碼文件(*.class)
③JVM會執行剛纔生成的字節碼文件(*.class),並把字節碼文件轉化爲機器指令
④機器指令可以直接在CPU上運行,也就是最終的程序執行
1.2 JVM實現會帶來不同的“翻譯”,不同的CPU平臺的機器指令又千差萬別,無法保證併發安全的效果一致
1.3 因此引入內存模型:轉換過程的規範、原則
2、三兄弟:JVM內存結構 VS Java內存模型 VS Java對象模型
整體方向:
- JVM內存結構,和Java虛擬機的運行時區域有關。如堆和棧
- Java內存模型,和Java的併發編程有關
- Java對象模型,和Java對象在虛擬機中的表現形式有關
2.1 JVM內存結構
- 堆(heap):是運行時數據區中佔用最大的。存儲對象的實例
- 虛擬機棧/Java棧(VM stack):保存各個基本類型、對象引用
- 方法區(method):存放static的靜態變量/類/常量,以及永久引用
- 本地方法棧:存放與本地方法(native)相關的
- 程序計數器
2.2 Java對象模型
- Java對象自身的存儲模型
- JVM會給這個類創建一個instanceKlass保存在方法區,用來在JVM層表示該Java類。
- 當我們在Java代碼中,使用new創建一個對象的時候,JVM會創建一個instanceOopDesc對象,這個對象中包含了對象頭以及實例數據。
3、JMM是什麼
3.1 爲什麼需要JMM
- C語言不存在內存模型的概念
- 依賴處理器,不同處理器結果不一樣
- 無法保證併發安全
- 需要一個標準,讓多線程運行的結果可預期
3.2 JMM是規範
- Java Memory Model
- JMM是一組規範,需要各個JVM的實現來遵守JMM規範,以便於開發者可以利用這些規範,更方便地開發多線程程序
- 如果沒有這樣的一個JMM內存模型來規範,那麼很可能經過了不同JVM的不同規則的重排序之後,導致不同的虛擬機上運行的結果不一樣,那是很大的問題
3.3 JMM是工具類和關鍵字的原理
- volatile、synchronized、Lock等的原理都是JMM
- 如果沒有JMM,那就需要我們自己指定什麼時候用內存柵欄等,那是相當麻煩的,幸好有了JMM,讓我們只需要用同步工具和關鍵字就可以開發併發程序
3.4 最重要的3點內容:重排序、可見性、原子性爲什麼需要JMM
4、重排序
4.1 重排序的代碼案例
/**
* OutOfOrderExecution
*
* @author venlenter
* @Description: 演示重排序的現象
* “直到達到某個條件才停止”,測試小概率事件
* @since unknown, 2020-05-20
*/
public class OutOfOrderExecution {
private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
Thread one = new Thread(new Runnable() {
@Override
public void run() {
a = 1;
x = b;
}
});
Thread two = new Thread(new Runnable() {
@Override
public void run() {
b = 1;
y = a;
}
});
one.start();
two.start();
one.join();
two.join();
System.out.println("x = " + x + ", y = " + y);
}
}
4.2 重排序分析
這4行代碼的執行順序決定了最終x和y的結果,一共有3種情況:
- 1、 a=1;x=b(0);b=1;y=a(1),最終結果是x=0,y=1
Thread-one執行完後,Thread-two再執行
- 2、 b=1;y=a(0);a=1;x=b(1),最終結果是x=1,y=0
Thread-two執行完後,Thread-one再執行
- 3、 b=1;a=1;x=b(1);y=a(1),最終結果是x=1,y=1
/**
* OutOfOrderExecution
*
* @author venlenter
* @Description: 演示重排序的現象
* “直到達到某個條件才停止”,測試小概率事件
* @since unknown, 2020-05-20
*/
public class OutOfOrderExecution {
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;
CountDownLatch latch = new CountDownLatch(1);
Thread one = new Thread(new Runnable() {
@Override
public void run() {
try {
latch.countDown();
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
a = 1;
x = b;
}
});
Thread two = new Thread(new Runnable() {
@Override
public void run() {
try {
latch.countDown();
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
b = 1;
y = a;
}
});
one.start();
two.start();
latch.countDown();
one.join();
two.join();
String result = "第" + i + "次(" + x + "," + y + ")";
System.out.println(result);
if (x == 1 && y == 1) {
break;
}
}
}
}
//輸出結果
...
第230次(0,1)
第231次(1,1)
- 重排序出現的情況(x=0,y=0)
Thread-1
a = 1;
x = b;
——————————————————————————
Thread-2
b = 1;
y = a;
y=a; #Thread2的2行代碼被重排序了,同時中間插入了Threa1
a=1;
x=b;
b=1
4.3 什麼是重排序:
在線程1內部的兩行代碼的【實際執行順序】和代碼在【Java文件中的順序】不一致,代碼指令並不是嚴格按照代碼語句順序執行的,它們的順序被改變了,這就是重排序,種類被顛倒的是y=a和b=1這2行語句
4.4 重排序的好處:提高處理速度
- 對比重排序前後的指令優化
4.5 重排序的3種情況:編譯器優化、CPU指令重排、內存的“重排序”
- 編譯器優化:包括JVM,JIT編譯器等
- CPU指令重排:就算編譯器不發生重排,CPU也可能對指令進行重排
- 內存的“重排序”:線程A的修改線程B卻看不到,引出可見性問題
5、可見性
5.1 案例:演示什麼是可見性問題
(1)案例一
- 代碼演示
/**
* FieldVisibility
*
* @author venlenter
* @Description: 演示可見性帶來的問題
* @since unknown, 2020-05-23
*/
public class FieldVisibility {
int a = 1;
int b = 2;
private void change() {
a = 3;
b = a;
}
private void print() {
System.out.println("b=" + b + ";a=" + a);
}
public static void main(String[] args) {
while (true) {
FieldVisibility test = new FieldVisibility();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.change();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.print();
}
}).start();
}
}
}
//輸出可能結果
b=2;a=1(println線程先執行)
b=3;a=3(先執行完全部change,再執行println)
b=2;a=3(change執行a=3後,切換到println線程)
第四種情況:
b=3;a=1(出現了可見性問題--概率比較小,但確實發生了):
沒給b加volatile,那麼有可能出現a=1,b=3.因爲a雖然被修改了,但是其他線程不可見,而b恰好其他線程可見,這就造成了b=3,a=1
(2)案例二
- 代碼
public class FieldVisibility {
int x = 0;
public void writeThread() {
x = 1;
}
public void readerThread() {
int r2 = x;
}
}
- 案例二分析:
- 可見性問題出現問題原因:
①主內存中原x=0,線程1和線程2分別讀取了x=0
②線程1在工作內存中賦值x=1,但還沒有寫入到主內存
③此時線程2的本地內存中x還是0,所以導致了可見性問題
(3)用volatile解決問題
- 代碼
/**
* FieldVisibility
*
* @author venlenter
* @Description: 演示可見性帶來的問題
* @since unknown, 2020-05-23
*/
public class FieldVisibility {
//分別對變量加volatile
volatile int a = 1;
volatile int b = 2;
private void change() {
a = 3;
b = a;
}
private void print() {
System.out.println("b=" + b + ";a=" + a);
}
public static void main(String[] args) {
while (true) {
FieldVisibility test = new FieldVisibility();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.change();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.print();
}
}).start();
}
}
}
- 分析
①使用了volatile,線程1在工作內存中修改了x=1後,會強制flush到主內存
②當線程2要讀取x/使用舊的x的時候,會判斷x爲失效,同時重新從主內存中讀取進來,則x爲新的1
5.2、爲什麼會有可見性問題
RAM是主內存
registers是寄存器
core假設是多核CPU
- CPU讀取寄存器registers中緩存——>registers讀取L1 cache級緩存——>L2——>L3——>主內存RAM
CPU有多級緩存,導致讀的數據過期
- 高速緩存的容量比主內存小,但是速度僅次於寄存器,所以在CPU和主內存之間就多了Cache層
- 線程間的對於共享變量的可見性問題不是直接由多核引起的,而是由多緩存引起的
- 如果所有的核心(core)都只用一個緩存,那麼也就不存在內存可見性問題了
- 每個核心都會將自己需要的數據讀到獨佔緩存(工作內存)中,數據修改後也是寫入到獨佔緩存中,然後等待刷入到主存中。所以會導致有些核心讀取的值是一個過期的值
5.3、JMM的抽象:主內存和本地內存
5.3.1 什麼是主內存和本地內存
- Java作爲高級語言,屏蔽了這些底層細節,用JMM定義了一套讀寫內存數據的【規範】,雖然我們【不再需要關心一級緩存和二級緩存】的問題,但是,JMM抽象了主內存和本地內存的概念
- 這裏說的本地內存【並不是真的是一塊給每個線程分配的內存】,而是JMM的一個抽象,是對於寄存器、一級緩存、二級緩存等的【抽象】
- 以上面core、regisiters的那張圖來說:(registers、L1、L2是線程的本地內存)(L3、RAM是線程共享的)
5.3.2 主內存和本地內存的關係
JMM有以下規定:
- 【所有的變量】都存儲在【主】內存中,同時【每個線程】也有自己【獨立】的【工作內存】,工作內存中的變量內容是主內存中的【拷貝】
- 線程【不能直接讀寫主內存中】的變量,而是隻能【操作自己工作內存】中的變量,然後再【同步】到主內存中
- 【主內存】是【多個線程共享】的,但【線程間不共享工作內存】,如果線程間需要【通信】,必須藉助【主內存中轉】來完成
所有的【共享變量存在於主內存】中,每個【線程有自己的本地內存】,而且【線程讀寫共享數據也是通過本地內存交換】的,所以才導致了【可見性問題】
5.4、Happens-Before原則
- 什麼是happens-before:(解決可見性問題)在時間上,動作A發生在動作B之前,B保證能看見A,這就是happens-before
- Happens-Before原則有哪裏?
1. 單線程規則
- 同個線程(同個工作內存)內,前面修改的變量對後面的操作是可見的。但不影響重排序
- 例如
int a = 1;
int b = 2;
private void change() {
a = 3;
b = a;
}
在同個線程執行change()的時候,a=3先執行,則後面b=a肯定爲3,因爲這裏使用到的是同個工作內存,a=3和b=3都是在該線程的同個工作內存中執行的,所以可見
但如果發生了重排序,也就是b=a先執行了,這是允許的,這裏b=a=1初始值
2. 鎖操作(synchronized和Lock)
3. volatile變量
- 理解:只要TheadA volatile變量是已經寫入了,那麼ThreadB讀取就肯定可以讀取到最新的結果
volatile int a = 1;
volatile int b = 2;
private void change() {
a = 3;
b = a;
}
private void print() {
System.out.println("b=" + b + ";a=" + a);
}
如果change()先執行,由於a、b被volatile,volatile也會防止重排序導致錯誤結果,所以a=3肯定在b=a之前執行。
所以當change()先執行完後,再執行print(),肯定可以得到正確的結果,不會出現change線程和print線程穿插導致變量值錯亂的問題
4. 線程啓動
- ThreadB執行的時候,可以看到ThreadA之前的操作
5. 線程join
- 在ThreadA(例如主線程爲main()方法)中執行了ThreadB.join,則ThreadA會等待ThreadB執行完畢後,才執行下面的statement1的操作邏輯
- 當ThreadB執行完畢後,下面的statement1也可以看到statement1的變化
6. 傳遞性:如果hb(A,B)而且hb(B,C),那麼可以推出hb(A,C)
- 假設背景爲main主線程中有ThreadA、ThreadB、ThreadC的執行,如果ThreadA和ThreadB遵循happen-before原則,ThreadB和ThreadC也遵循happens-before,則可以推出hb(A,C)
7. 中斷:一個線程被其他線程interrupt時,那麼檢測中斷(isInterrupted)或者拋出InterruptedException一定能看到
8. 構造方法:對象構造方法的最後一行指令happens-before於finalize()方法的第一行指令
- finalize()已不推薦使用
9. 工具類的Happens-Before原則
- (1)線程安全的容器get一定能看到在此之前的put等存入動作
如線程安全的ConcurrentHashMap的get和put
- (2)CountDownLatch
CountDownLatch latch = new CountDownLatch(1);
Thread one = new Thread(new Runnable() {
@Override
public void run() {
try {
latch.countDown();
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
a = 1;
x = b;
}
});
one.start();
latch.countDown();
當執行CountDownLatch的countDown(),Thread one才能從await中喚醒,繼續執行下面的a=1;x=b
- (3)Semaphore:類似CountDownLatch
- (4)Future:可以去後臺執行,並拿到一個線程執行結果的類。Future的get是拿到Future的執行結果,get對於之前的執行結果是可見的(不用過多關注,默認保證的)
- (5)線程池:我們會向線程池提交許多任務,然後在提交的任務中,每個任務都可以看到在提交之前的所有的執行結果(不用過多關注,默認保證的)
- (6)CyclicBarrier:CountDownLatch
①CyclicBarrier cyclicBarrier1 = new CyclicBarrier(1);
②cyclicBarrier1.await();
③xxx
④cyclicBarrier1.reset(); //當執行了reset後,才能從②await中喚起,繼續執行③的代碼
案例:happens-before演示
- happens-before有一個原則是:如果A是對volatile變量的寫操作,B是對同一個變量的讀操作,那麼hb(A,B)
- 分析這四種情況:
int a = 1;
int b = 2;
private void change() {
a = 3;
b = a;
}
private void print() {
System.out.println("b=" + b + ";a=" + a);
}
//輸出可能結果
b=2;a=3(change執行a=3後,切換到println線程)
b=2;a=1(println線程先執行)
b=3;a=3(先執行完全部change,再執行println)
第四種:b=3;a=1(出現了可見性問題--概率比較小,但確實發生了):
沒給b加volatile,那麼有可能出現a=1,b=3.因爲a雖然被修改了,但是其他線程不可見,而b恰好其他線程可見,這就造成了b=3,a=1
- 改進:之前是對a、b都加了volatile,實際上在該場景,只要對b加volatile就可以了
int a = 1;
volatile int b = 2;
private void change() {
a = 3;
b = a;
}
private void print() {
System.out.println("b=" + b + ";a=" + a);
}
- 近朱者赤:給b加了volatile,不僅b被影響,也可以實現輕量級同步
- b之前的寫入(對應代碼b=a)對讀取b後的代碼(print b)都可見,所以在writerThread裏對a的賦值,一定會對readerThread裏的讀取可見,所以這裏的【a即使不加volatile,只要b讀到的是3,就可以由happens-before原則保證了print a讀到的也都是3而不可能讀到1】
5.5、volatile關鍵字
5.5.1 volatile是什麼
- volatile是一種【同步機制】,比synchronized或者Lock相關類【更輕量】,因爲使用volatile並不會發生【上下文切換】等開銷很大的行爲
- 如果一個變量被修飾成volatile,那麼JVM就知道了這個變量可能【會被併發修改】(JVM就會做一些相關邏輯,如禁止重排序)
- 開銷小,相應的能力也小,雖然說volatile是用來同步地保證線程安全的,但volatile無法保證synchronized那樣的【原子保護】,volatile僅在【很有限的場景】下才能發揮作用
5.5.2 volatile的適用場合
(1)不適用a++
/**
* NoVolatile
*
* @author venlenter
* @Description: 不適用於volatile的場景
* @since unknown, 2020-05-26
*/
public class NoVolatile implements Runnable {
volatile int a;
AtomicInteger realA = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Runnable r = new NoVolatile();
Thread thread1 = new Thread(r);
Thread thread2 = new Thread(r);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(((NoVolatile) r).a);
System.out.println(((NoVolatile) r).realA);
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
a++;
realA.incrementAndGet();
}
}
}
//輸出結果
19855
20000
(2)適用場景:volatile的變量,不依賴之前的值。如果依賴之前的值如a++(先讀a,再+),就會有問題。如果只是對變量進行覆蓋賦值(不依賴之前的值),則適用
(3)適用場景1:boolean flag,如果一個共享變量自始自終只【被各個線程賦值】,而沒有其他的操作(對比、取值),那麼就可以用volatile來代替synchronized或者代替原子變量,因爲賦值自身是有原子性的,而volatile又保證了可見性,所以就足以保證線程安全1
(4)適用場景2:作爲刷新之前變量的觸發器
Map configOptions;
char[] configText;
volatile boolean initialized = false;
//Thread A
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText, configOptions);
initialized = true;
//Thread B
## 當在ThreadA中initialized設置爲true,則在ThreadB中就跳過while,同時因爲volatile的happens-before,則在ThreadA的initialized賦值操作前的configOptions肯定已經初始化完畢了
while (!initialized) {
sleep();
}
//use configOptions
5.5.3 volatile的作用:可見性、禁止重排序
(1)可見性:讀volatile變量時會去【主內存讀取最新值】,寫一個volatile屬性會【立即刷入到主內存】
(2)禁止指令【重排序】優化:解決單例雙重鎖亂序問題
5.5.4 volatile和synchronized的關係?
- volatile可看做是【輕量版的synchronized】:如果一個共享變量自始至終【只被各個線程賦值】,而沒有其他的操作(讀值),那麼就可以用volatile來代替synchronized或者代替原子變量,因爲【賦值自身是有原子性的,而volatile又保證了可見性】,所以就足以保證線程安全
5.5.5 學以致用:用volatile修正重排序問題
- OutOfOrderExecution類加了volatile後,用於不會出現(0,0)的情況了
/**
* OutOfOrderExecution
*
* @author venlenter
* @Description: 演示重排序的現象
* “直到達到某個條件才停止”,測試小概率事件
* @since unknown, 2020-05-20
*/
public class OutOfOrderExecution {
//加上volatile,則不會出現(0,0)了
private volatile static int x = 0, y = 0;
private volatile 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;
CountDownLatch latch = new CountDownLatch(1);
Thread one = new Thread(new Runnable() {
@Override
public void run() {
try {
latch.countDown();
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
a = 1;
x = b;
}
});
Thread two = new Thread(new Runnable() {
@Override
public void run() {
try {
latch.countDown();
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
b = 1;
y = a;
}
});
one.start();
two.start();
latch.countDown();
one.join();
two.join();
String result = "第" + i + "次(" + x + "," + y + ")";
System.out.println(result);
if (x == 0 && y == 0) {
break;
}
}
}
}
5.5.6 volatile小結
- 1、volatile修飾符【適用於以下場景】:某個屬性被多個線程共享,其中一個線程修改了此屬性,其他線程可以立即得到修改後的值,比如【boolean flag】;或者作爲【觸發器】,實現輕量級同步
- 2、volatile屬性的書寫操作都是【無鎖】的,它不能替代synchronized,因爲它沒有提供【原子性】和【互斥性】。因爲無鎖,不需要花費時間在獲取鎖和釋放鎖上,所以說它是【低成本】的
- 3、volatile只能作用於【屬性】。使用volatile修飾屬性,該屬性就不會被指令重排序
- 4、volatile提供了【可見性】,任何一個線程對其的修改將立馬對其他線程可見。volatile屬性不會被線程緩存,始終【從主存中讀取】
- 5、volatile提供了【happens-before】保證,對volatile變量v的寫入操作-【happens-before】於-所有其他線程後續對v的讀操作
- 6、volatile可以【使得long和double的賦值是原子】的
5.6、能保證可見性的措施
- 除了volatile可以讓變量保證可見性外,【synchronized、Lock、併發集合、Thread.join()和Thread.start()】等都可以保證可見性
- 具體看happens-before原則的規定
5.7、昇華:對synchronized可見性的正確理解
- synchronized不僅保證了原子性,還保證了【可見性】
- synchronized不僅讓被保護的代碼安全,還讓其之前的代碼執行結果可見
6、原子性
6.1、什麼是原子性
- 一系列操作,要麼全部執行成功,要麼全部不執行,不會出現執行一半的情況,是不可分割的
- 銀行轉賬問題(A轉賬給B):A先減100,B再加100
- i++不是原子性的
- 用synchronized實現原子性
6.2、Java中的原子操作有哪些?
- 除long和double之外的【基本類型】(int,byte,boolean,short,char,float)的賦值操作
- 所有引用【reference的賦值操作】,不管是32位的機器還是64位的機器
- java.concurrent.Atomic.* 包中所有類的原子操作
6.3、long和double的原子性
- 問題描述:官方文檔、對於64位的值的寫入,可以分爲兩個32位的操作進行寫入、讀取錯誤、使用volatile解決 https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.7
- 結論:在32位上的JVM上,long和double的操作不是原子的,但在64位的JVM上是原子的
- 實際開發中:商用Java虛擬機中不會出現
6.4、原子操作+原子操作!=原子操作
- 簡單地把原子操作組合在一起,並不能保證整體依然具有原子性
- 全同步的HashMap也不能完全安全(多個synchronized方法操作組合在一起,就不是原子的了)
7、面試常見問題
7.1 JMM應用實例:單例模式8種寫法、單例和併發的關係(真實面試超高頻考點)
(1)單例模式的作用和使用場景
單例模式的作用
- 爲什麼需要單例模式:節省內存和計算、保證結果正確、方便管理
單例模式適用場景
- 無狀態的工具類:比如日誌工具類,不管是在哪裏適用,我們需要的只是它幫我們記錄日誌信息,除此之外,並不需要在它的實例對象上存儲任何狀態,這個時候我們就只需要一個實例對象即可
- 全局信息類:比如我們在一個類上記錄網站的訪問次數,我們不希望有的訪問被記錄在對象A上,有的卻記錄在對象B上,這時候我們就讓這個類成爲單例
(2)單例模式的8種寫法
1、餓漢式(靜態常量)[可用]
/**
* Singleton1
*
* @author venlenter
* @Description: 餓漢式(靜態常量)(可用)
* @since unknown, 2020-06-02
*/
public class Singleton1 {
//類加載時就完成了初始化
private final static Singleton1 INSTANCE = new Singleton1();
private Singleton1() {
}
public static Singleton1 getInstance() {
return INSTANCE;
}
}
2、餓漢式(靜態代碼塊)[可用]
/**
* Singleton2
*
* @author venlenter
* @Description: 餓漢式(靜態代碼塊)(可用)
* @since unknown, 2020-06-02
*/
public class Singleton2 {
//類加載時就完成了初始化
private final static Singleton2 INSTANCE;
static {
INSTANCE = new Singleton2();
}
private Singleton2() {
}
public static Singleton2 getInstance() {
return INSTANCE;
}
}
3、懶漢式(線程不安全)[不可用]
/**
* Singleton3
*
* @author venlenter
* @Description: 懶漢式(線程不安全)
* @since unknown, 2020-06-03
*/
public class Singleton3 {
private static Singleton3 instance;
private Singleton3() {
}
public static Singleton3 getInstance() {
//如果2個線程同時執行到這一行,則會執行2次new Singleton3(),創建了多個實例
if (instance == null) {
instance = new Singleton3();
}
return instance;
}
}
4、懶漢式(線程安全,同步方法)[不推薦用]
/**
* Singleton4
*
* @author venlenter
* @Description: 懶漢式(線程安全)(不推薦)
* @since unknown, 2020-06-03
*/
public class Singleton4 {
private static Singleton4 instance;
private Singleton4() {
}
//synchronized,多個線程執行會阻塞等待
public synchronized static Singleton4 getInstance() {
if (instance == null) {
instance = new Singleton4();
}
return instance;
}
}
5、懶漢式(線程不安全,同步代碼塊)[不可用]
/**
* Singleton5
*
* @author venlenter
* @Description: 懶漢式(線程不安全,同步代碼塊)(不可用)
* @since unknown, 2020-06-03
*/
public class Singleton5 {
private static Singleton5 instance;
private Singleton5() {
}
public static Singleton5 getInstance() {
//2個線程同時進入這裏,則A線程synchronized執行完後,B線程又執行一次synchronized初始化
//本質還是執行了2次初始化,線程不安全
if (instance == null) {
synchronized (Singleton5.class) {
instance = new Singleton5();
}
}
return instance;
}
}
6、雙重檢查[推薦用]
/**
* Singleton6
*
* @author venlenter
* @Description: 雙重檢查(推薦使用)
* @since unknown, 2020-06-03
*/
public class Singleton6 {
private volatile static Singleton6 instance;
private Singleton6() {
}
public static Singleton6 getInstance() {
if (instance == null) {
synchronized (Singleton6.class) {
if (instance == null) {
instance = new Singleton6();
}
}
}
return instance;
}
}
- 優點:線程安全;延遲加載;效率較高
- 爲什麼要double-check
①線程安全
②單check行不行?:不行多個線程同時執行到instance==null,雖然有synchronized,但也會執行了2次初始化
③直接在方法加synchronized呢?:性能問題,多個線程排隊等待
- 爲什麼要用volatile
①新建對象實際上有3個步驟:創建空對象、空對象內初始化、賦值
②重排序會帶來nullpointexception
③防止重排序
7、靜態內部類[推薦使用]
/**
* Singleton7
*
* @author venlenter
* @Description: 靜態內部類方式,可用
* @since unknown, 2020-06-03
*/
public class Singleton7 {
private Singleton7() {
}
//JVM加載Singleton7類的時候,不會初始化內部類變量,達到了懶加載
private static class SingletonInstance {
private static final Singleton7 instance = new Singleton7();
}
public static Singleton7 getInstance() {
//只有當調用到的是,纔會進行加載
return SingletonInstance.instance;
}
}
8、枚舉[推薦用]
/**
* Singleton8
*
* @author venlenter
* @Description: 枚舉單例
* @since unknown, 2020-06-04
*/
public enum Singleton8 {
INSTANCE;
public void whatever() {
}
}
//調用
Singleton8.INSTANCE.whatever();
(3)不同寫法對比
- 餓漢:簡單,但是沒有lazy loading,直接就初始化創建了一些對象,而這些對象可能是不需要的
- 懶漢:寫法複雜,同時有線程安全問題
- 靜態內部類:可用
- 雙重檢查:同時做到了線程安全和懶加載
- 枚舉:最好
(4)用哪種單例的實現方案最好?
《Effective Java》中表明:使用枚舉實現單例的方法雖然還沒有廣泛採用,但單元素的枚舉類型已經成爲實現Singleton的最佳方法
- 寫法簡單
- 線程安全有保障
- 避免反序列化破壞單例
(5)各種寫法的適用場景
- 最好的方法是利用【枚舉】,因爲還可以防止反序列化重新創建新的對象
- 非線程同步的方法不能使用
- 如果程序一開始要加載的資源太多,那麼就應該使用【懶加載】
- 餓漢式如果是對象的創建需要配置文件就不適用(假設對象的創建需要調用一個前置方法去獲取配置,但因爲餓漢式,對象被提前創建,而沒有將對應的前置方法數據賦值進去,造成創建的對象是一個空對象)
- 懶加載雖然好,但是靜態內部類這種方式會引入編程複雜性
7.2 講一講什麼是Java內存模型
- JMM是什麼?:一組規範
- 最重要的3點內容:重排序、可見性、原子性
- 可見性內容從主內存和本地內存、Happens-before原則、volatile
- 原子性:實現原子性的方法、單例模式
7.3 volatile和synchronized的異同?
- volatile可以算是輕量版的synchronized,開銷小,適用場合相對就少一點:如果一個共享變量至始至終只被各個線程賦值,而沒有其他的操作,那麼就可以用volatile來代替
7.4 什麼是原子操作?Java中有哪些原子操作?生成對象的過程是不是原子操作?
- 什麼是原子操作:要麼全部執行,要麼全部不執行
- Java中有哪裏原子操作
除long和double之外的【基本類型】(int,byte,boolean,short,char,float)的賦值操作
所有引用【reference的賦值操作】,不管是32位的機器還是64位的機器
java.concurrent.Atomic.* 包中所有類的原子操作
- 生成對象的過程是不是原子操作:是多步操作,無法保證原子操作
①新建一個空的Person對象
②執行Person的構造函數
③把這個對象的地址指向p
7.5 什麼是內存可見性?
7.6 64位的double和long寫入的時候是原子的嗎
- 32位上不是原子的,64位上是原子的,一般不需要我們考慮
8、總結:Java內存模型————底層原理
- 什麼叫“底層原理”
- 三兄弟:JVM內存結構 VS Java內存模型 VS Java對象模型
- JMM是什麼
- 重排序
- 可見性
- 原子性
筆記來源:慕課網悟空老師視頻《Java併發核心知識體系精講》