Java程序編譯到運行須要通過將.java後綴的文件經過javac命令編譯成.class文件(此時與平臺無關),而後將對應的.class文件轉化成機器碼並執行,可是因爲不一樣平臺的JVM會帶來不一樣的「翻譯」,因此咱們在Java層寫的各類Lock,其實最終依賴的是JVM的具體實現和CPU指令,才能幫助咱們達到線程安全的效果。java
下面介紹面試
在使用new指令建立一個對象的時候,JVM會建立一個instanceOopDesc對象,這個對象中包含了對象頭以及實例數據編程
C/C++語言它們不存在內存模型的概念,它們依賴於處理器,不一樣的處理器處理的結果不一樣,也就沒法保證併發安全。因此此時須要一個標準,讓多線程的運行結果可預期。緩存
JMM是一組規範,要求JVM依照規範來實現,從而讓咱們更好的開發多線程程序。若是沒有了JMM規範,那麼不一樣的虛擬機可能會進行不一樣的重排序,這樣就會致使不一樣的虛擬機上運行的結果不一樣,這也就引法了問題。安全
JMM除了是規範仍是工具類和關鍵字的原理,咱們常見的
volatile
、synchronized
以及Lock
等的原理都是JMM。若是沒有JMM,那就須要咱們本身指定何時須要內存柵欄(工做內存與主內存之間的拷貝同步)等,這樣就很麻煩,由於有了JMM,因此咱們只須要使用關鍵字就能夠開發併發程序了。bash
第一種執行狀況多線程
/**
* 演示重排序的現象
* 「直到達到某個條件才中止」,測試小几率事件
*/
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);
}
}
複製代碼
第二種執行狀況併發
/**
* 演示重排序的現象
* 「直到達到某個條件才中止」,測試小几率事件
*/
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;
}
});
two.start();
one.start();
one.join();
two.join();
System.out.println("x = " + x + ", y = " + y);
}
}
複製代碼
第三種執行狀況
/**
* 演示重排序的現象
* 「直到達到某個條件才中止」,測試小几率事件
*/
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 {
CountDownLatch latch = new CountDownLatch(1);
Thread one = new Thread(new Runnable() {
@Override
public void run() {
try {
latch.await(); //進行等待
} catch (InterruptedException e) {
e.printStackTrace();
}
a = 1;
x = b;
}
});
Thread two = new Thread(new Runnable() {
@Override
public void run() {
try {
latch.await(); //進行等待
} catch (InterruptedException e) {
e.printStackTrace();
}
b = 1;
y = a;
}
});
one.start();
two.start();
latch.countDown(); //統一開始執行
one.join();
two.join();
System.out.println("x = " + x + ", y = " + y);
}
}
複製代碼
對第三種狀況的優化
/**
* 演示重排序的現象
* 「直到達到某個條件才中止」,測試小几率事件
*/
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.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
a = 1;
x = b;
}
});
Thread two = new Thread(new Runnable() {
@Override
public void run() {
try {
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 + ")";
if (x == 1 && y == 1) {
System.out.println(result);
return; //知足條件後退出循環
} else {
System.out.println(result);
}
}
}
}
複製代碼
總結:代碼的執行順序決定了執行的結果
只需將上面的結束條件改成x == 0 && y == 0
便可 app
出現這種狀況是由於重排序發生了,代碼的執行順序有可能爲ide
y = a;
a = 1;
x = b;
b = 1;
複製代碼
線程1中代碼的執行順序與Java代碼不一致,代碼的執行順序並非按照指令執行的,它們的執行順序被改變了,這就是重排序。
對比下圖能夠發現若是進行重排序能夠減小關於變量a
的執行指令,若是在程序中個存在大量的相似狀況,也就提升了處理速度。
好比存在變量a和b,若是將對a的操做連續執行效率更高的話,就可能發生重排序來提升執行效率。
CPU重排和編譯器重排相似,就算編譯器不重排CPU也會進行重排,它們都是打亂執行順序達到優化的目的。
內存中的重排序並不是真正的重排序,由於內存中有緩存的存在,在JMM中表現爲本地內存和主內存,若是線程1修改了變量a的值尚未來得及寫入到主存,此時線程2因爲可見性的緣由沒法知道線程1對變量進行了修改,因此會使程序表現出亂序行爲。
演示代碼當一個線程執行寫操做時,另一個線程沒法看見此時被更改的值。就像下圖所示當線程1從主存中讀取變量x,並將x的值設置爲1,可是此時線程1並無將x的值寫回主存,因此線程2就沒法得知x的值已經改變了。
/**
* 演示可見性帶來的問題
*/
public class FieldVisibility {
int a = 1;
int b = 2;
public static void main(String[] args) {
while (true) {
FieldVisibility test = new FieldVisibility();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.change();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.print();
}
}).start();
}
}
private void print() {
System.out.println("b = " + b + "; a = " + a);
}
private void change() {
a = 3;
b = a;
}
}
複製代碼
四種狀況
a = 3; b = 3;
a = 1; b = 2;
a = 3; b = 2;
a = 1; b = 3; //發生可見性問題
複製代碼
/**
* 演示可見性帶來的問題
*/
public class FieldVisibility {
//解決可見性問題
volatile int a = 1;
volatile int b = 2;
public static void main(String[] args) {
while (true) {
FieldVisibility test = new FieldVisibility();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.change();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.print();
}
}).start();
}
}
private void print() {
System.out.println("b = " + b + "; a = " + a);
}
private void change() {
a = 3;
b = a;
}
}
複製代碼
volatile怎麼解決可見性問題
當線程1讀取到x並將值更新爲1後會刷回主存,當線程2再次讀取x時就會從主存中加載,這樣就不會引起可見性的問題。
Java做爲高級語言,屏蔽了這些底層細節,用JMM定義了這些讀寫內存數據的規範,雖然咱們再也不須要關心一級緩存和二級緩存的問題,可是JMM抽象了主內存和本地內存的概念。
這裏說的本地內存並非真正的爲每一個線程分配一塊內存,而是JMM的抽象,是對寄存器、一級緩存、二級緩存的抽象。
主內存和本地內存的關係
JMM有如下規定
總結:線程操做數據必須從主內存中讀取數據,而後在本身的工做內存中進行操做,操做完成後再寫回主內存,由於讀寫須要時間因此就會引起可見性的問題
在單線程狀況下,後面的語句必定能看到前面的語句作了什麼
加鎖以後能看到解鎖以前的所有操做
被volatile
修飾的變量只要執行了寫操做,就必定會被讀取到
調用start()
方法可讓子線程中全部語句看到啓動以前的結果
join()
後的語句能看到等待以前的全部操做
好比第一行代碼運行後第二行會看到,第二行運行後第三行會看到,從中能夠推斷出第一行代碼運行完第三行就會看到。
若是一個線程被interrupt()
時,那麼isInterrupt()
或者InterruptException
必定能看到。
對象構造方法的最後一行語句happens-before於finalize()
的第一行語句
volatile
是一種同步機制,相對synchronized
和Lock
更輕量,不會帶來上下文切換等重大開銷。若是一個變量被volatile
修飾,那麼JVM就知道這個變量可能會被併發修改。雖然volatile
的開銷小,可是它的能力也小,相對於synchronized
來講volatile
沒法保證原子性。
/**
* 不適用volatile的場景
*/
public class NoVolatile implements Runnable {
volatile int a;
AtomicInteger realA = new AtomicInteger();
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
a++;
realA.incrementAndGet();
}
}
public static void main(String[] args) throws InterruptedException {
NoVolatile noVolatile = new NoVolatile();
Thread thread1 = new Thread(noVolatile);
Thread thread2 = new Thread(noVolatile);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(noVolatile.a);
System.out.println(noVolatile.realA.get());
}
}
複製代碼
/**
* volatile的適用場景
*/
public class UseVolatile implements Runnable {
volatile boolean done = false;
AtomicInteger realA = new AtomicInteger();
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
setDone();
realA.incrementAndGet();
}
}
private void setDone() {
done = true;
}
public static void main(String[] args) throws InterruptedException {
UseVolatile noVolatile = new UseVolatile();
Thread thread1 = new Thread(noVolatile);
Thread thread2 = new Thread(noVolatile);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(noVolatile.done);
System.out.println(noVolatile.realA.get());
}
}
複製代碼
注意:賦值操做原本是原子操做,因此對volatile
修飾的變量進行賦值能夠保證線程安全,可是若是不是直接賦值則沒法保證,請看下面的例子
/**
* 不適用volatile的場景
*/
public class NoVolatile2 implements Runnable {
volatile boolean done = false;
AtomicInteger realA = new AtomicInteger();
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
flipDone();
realA.incrementAndGet();
}
}
private void flipDone() {
done = !done;
}
public static void main(String[] args) throws InterruptedException {
NoVolatile2 noVolatile = new NoVolatile2();
Thread thread1 = new Thread(noVolatile);
Thread thread2 = new Thread(noVolatile);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(noVolatile.done);
System.out.println(noVolatile.realA.get());
}
}
複製代碼
上來初始值爲false
,因此執行偶數次結果應該爲false
,但是執行了20000次以後結果倒是true
,從中即可以看出volatile
在此狀況下不適用
/**
* 演示可見性帶來的問題
*/
public class FieldVisibility {
int a = 1;
int abc = 1;
int abcd = 1;
volatile int b = 2;
public static void main(String[] args) {
while (true) {
FieldVisibility test = new FieldVisibility();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.change();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.print();
}
}).start();
}
}
private void print() {
if (b == 0) {
System.out.println("b = " + b + "; a = " + a);
}
}
private void change() {
abc = 7;
abcd = 70;
a = 3;
b = 0;
}
}
複製代碼
在這裏b==0
做爲觸發的條件,由於在change()
方法中最後一句將b設置爲0,因此依照happens-before原則在b=0之前的操做都是可見的,從而達到了觸發器的做用
volatile
修飾的變量須要使本地緩存失效,而後從主存中讀取新值,寫一個volatile
變量後會當即刷回主存volatile
是輕量級的synchronized
,當在多線程環境下只作賦值操做時可使用volatile
代替synchronized
,由於賦值操做自身保證原子性,而使用volatile
又能保證可見性,因此能夠實現線程安全。
boolean flag;
,或者做爲觸發器實現輕量級同步。synchronized
是由於它沒法提供原子性和互斥性,由於無鎖,它也不會在獲取鎖和釋放鎖上有開銷,因此說它是低成本的。除了volatile能夠保證可見性以外,synchronized、Lock、併發集合、Thread.join()和Thread。start()均可以保證可見性(具體看happens-before原則)。
一系列操做要麼所有成功,要麼所有失敗,不會出現只執行一半的狀況,是不可分割的。
對於64位值的寫入,能夠分爲兩個32位操做進行寫入,因此可能會致使64位的值發生錯亂,針對這種狀況能夠添加volatile進行解決。在32位的JVM上它們不是原子的,而在64位的JVM上倒是原子的。
簡單的把原子操做組合在一塊兒,並不能保證總體依然具備原子性,好比說去銀行取兩次錢,這兩次取錢都是原子操做,可是中途銀行卡可能會被女友借走,這樣就形成了兩次取錢的中斷。
/**
* 餓漢式(靜態常量)(可用)
*/
public class Singleton1 {
private static final Singleton1 INSTANCE = new Singleton1();
private Singleton1(){
}
public Singleton1 getInstance(){
return INSTANCE;
}
}
複製代碼
/**
* 餓漢式(靜態代碼塊) (可用)
*/
public class Singleton2 {
private static Singleton2 INSTANCE;
static {
INSTANCE = new Singleton2();
}
private Singleton2(){}
public static Singleton2 getInstance(){
return INSTANCE;
}
}
複製代碼
/**
* 懶漢式(線程不安全) (不可用)
*/
public class Singleton3 {
private static Singleton3 INSTANCE;
private Singleton3(){}
public static Singleton3 getInstance(){
if (INSTANCE == null){
INSTANCE = new Singleton3();
}
return INSTANCE;
}
}
複製代碼
由於這樣寫在多線程狀況下有可能線程1進入了if (INSTANCE == null)
但還沒來得及建立,此時線程2進入if (INSTANCE == null)
,這樣就形成了重複的建立,破壞了單例。
/**
* 懶漢式(線程安全,同步方法) (不推薦用)
*/
public class Singleton4 {
private static Singleton4 INSTANCE;
private Singleton4(){}
public synchronized static Singleton4 getInstance(){
if (INSTANCE == null){
INSTANCE = new Singleton4();
}
return INSTANCE;
}
}
複製代碼
由於添加了synchronized
關鍵字,因此能夠保證同一時刻只有一個線程能進入方法也就保證了線程安全。可是因爲添加了synchronized
也會對性能產生影響
/**
* 懶漢式(線程不安全,同步代碼塊) (不可用)
*/
public class Singleton5 {
private static Singleton5 INSTANCE;
private Singleton5() {
}
public static Singleton5 getInstance() {
if (INSTANCE == null) {
synchronized (Singleton5.class) {
INSTANCE = new Singleton5();
}
}
return INSTANCE;
}
}
複製代碼
這樣寫看似可行,但是實際上卻不能夠。由於只要INSTANCE
爲空就會進入判斷,不管裏面加不加同步遲早都會再次建立,因此這樣會致使實例被屢次建立
/**
* 雙重檢查(推薦面試使用)
*/
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行不行?
由於若是不進行第二次檢查不管添不添加同步都會對實例進行建立,這樣就會建立多個實例,是線程不安全的
若是把synchronized
添加在方法上能夠嗎?
若是添加在方法上是能夠的,可是這樣會形成性能問題
爲何必定要加volatile
由於新建對象不是原子操做,它須要通過建立空對象、調用構造方法、將地址分配給引用這三個步驟,這樣可能會進行重排序,因此就可能出現空指針異常,針對這個問題能夠添加volatile
關鍵字來解決
/**
* 靜態內部類式,可用
*/
public class Singleton7 {
private Singleton7() {
}
private static class InnerClass{
//不會對內部靜態實例進行初始化
private static Singleton7 INSTANCE = new Singleton7();
}
public static Singleton7 getInstance(){
return InnerClass.INSTANCE;
}
}
複製代碼
靜態內部類方式是一種「懶漢」的方式,在最初對類加載時不會加載內部類的靜態實例
/**
* 枚舉單例
*/
public enum Singleton8 {
INSTANCE;
}
複製代碼