上一篇中有提到并發(fā)的三大特性:原子性、可見性、有序性,這一篇就詳細(xì)來說一下這三大特性。
原子性:
Java原子性是指在多線程環(huán)境下,一段原子性的代碼執(zhí)行的時候是不會被打斷的,這段代碼要么全部完成,要么全部不完成,不會出現(xiàn)部分操作完成,部分操作沒有完成的情況。這段代碼可以是一行代碼也可以是多行代碼,一行代碼很多也不是原子性的,多行代碼加上鎖了它也可以是原子性的。所以說原子性和代碼的多少沒有關(guān)系。原子性其實指的是cpu執(zhí)行階段的原子性。
來點示例說明一下:(1)int i = 10; 這行就是原子性的,就是定義一個變量i并賦值。因為他在cpu里面執(zhí)行的時候就是一條指令。(2)long j = 13;這個就不是原子性的,因為long是64位長整型的,他在cpu執(zhí)行的時候是兩條指令,對高32位和低32位分別賦值。double也是如此,他也是64位的。(3)int i = 10; i++; 我們來看i++,在Java程序中它就是一行,但是在cpu級別其實是三條指令。第一條指令:獲取i的值;第二條指令:執(zhí)行i+1這個操作;第三條指令:把i+1這個結(jié)果賦值給i,所以i++就不是原子性操作了。
在Java中把多行語句加上鎖(通常用synchronized或者lock對象來實現(xiàn)一段代碼、一個方法、一個對象的加鎖來實現(xiàn)一段代碼的原子性),它就成了一個原子的了,從cpu的角度就是插入一個lock的指令,這段代碼被lock指令鎖住之后,只能一個線程執(zhí)行這段代碼了,其他線程就執(zhí)行不了了,只能等這個線程釋放掉鎖資源,其他線程獲取到這個鎖資源才能執(zhí)行這段代碼。
下面是一個使用Java原子類的代碼示例:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicDemo {
private static AtomicInteger atomicInteger = new AtomicInteger(0);
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
atomicInteger.incrementAndGet();
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
atomicInteger.incrementAndGet();
}
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最終結(jié)果: " + atomicInteger.get());
}
}
最終執(zhí)行的結(jié)果始終為:2000

在這個示例中,我們使用了AtomicInteger類來保證對變量atomicInteger的加操作是原子性的。在多線程環(huán)境下,兩個線程同時對atomicInteger進行加操作,但是由于AtomicInteger類的保證,最終的結(jié)果一定是2000。如果不用AtomicInteger,結(jié)果是無法保證的,可能不是2000??梢詤⒖忌弦黄蔷€程安全的情況。
可見性:
Java并發(fā)可見性是指在多線程的環(huán)境下,一個線程對共享變量的修改,能夠立即被其他線程看到。
在Java中每一個子線程會有一個單獨的工作內(nèi)存,主線程有一個主內(nèi)存,一個主內(nèi)存有多個工作內(nèi)存,主內(nèi)存中存放的是共享變量,雖然說是共享變量,從名字來看感覺好像是主線程和子線程共享的,這樣子線程就可以直接讀寫這個共享變量了,其實并不是,子線程是沒有辦法直接讀寫和操作這個共享變量的。實際上,子線程會在工作內(nèi)存中創(chuàng)建一個共享變量的副本,而子線程只能操作這個共享變量的副本,讀寫完畢之后再將這個變量副本回寫到主內(nèi)存中。
來一個示例說明一下:主內(nèi)存中有一個共享變量 i = 10; 有兩個子線程A、B,這兩個子線程都要操作共享變量i,這時線程A、B分別將共享變量i = 10 拷貝到自己的工作內(nèi)存中,這樣子線程A、B中就分別有了一個i = 10的變量副本。原來其實變量i只在主內(nèi)存中,現(xiàn)在變量i變成了三份了,主內(nèi)存、線程A工作內(nèi)存、線程B工作內(nèi)存各一份,如果線程A對變量i進行加1的操作,那么主線程中i = 11,而這時主線程和線程B中的i = 10;就出現(xiàn)了變量數(shù)據(jù)的值不一致了,這就是可見性的問題。
如何解決可見性的問題呢?
思路:線程A修改了i變量的副本值之后,馬上把修改后的這個值回寫到主內(nèi)存。線程B讀i變量副本的時候不再從自己的工作內(nèi)存去讀,而是從主內(nèi)存重新加載一遍i的副本到自己的工作內(nèi)存,相當(dāng)于重新從主線程讀變量i的值。
Java內(nèi)存模型中是通過內(nèi)存屏障來解決可見性的問題,內(nèi)存屏障其實就是cpu級別的一個指令,將這個指令插入到其他指令之間。比如說原來有兩個指令,在這兩個指令之間插入一個內(nèi)存屏障指令,這個內(nèi)存屏障的指令會對前后兩個指令做一些特殊的操作,它就可以解決這個可見性的問題。具體解決可見性問題使用的內(nèi)存屏障是兩個,一個是load屏障,一個是store屏障;load屏障:工作內(nèi)存從主內(nèi)存加載變量生成副本的指令。store屏障:將工作內(nèi)存變量的副本回寫到主內(nèi)存的指令。load屏障作用:比如說現(xiàn)在有一個執(zhí)行的指令A(yù),它要讀取一個共享變量的副本,現(xiàn)在在指令A(yù)前面插入一個load屏障指令,這時候指令A(yù)需要讀的變量副本就失效了,也就是說運動到load屏障指令的時候,它就會讓A需要用到的變量副本失效了,這時候指令A(yù)就讀不到變量副本,然后指令A(yù)就要從主內(nèi)存中再加載一次對應(yīng)的變量到自己的工作內(nèi)存,替換到當(dāng)前的工作內(nèi)存中的副本,然后才可以使用。store屏障作用:比如說現(xiàn)在有一個執(zhí)行的指令B,它要修改一個變量工作內(nèi)存副本的值,修改完成之后,我們在它之后插入一個store屏障指令,這個時候B修改過的那個變量的副本就會馬上回寫到主內(nèi)存,因為store屏障指令就是把它前變修改過的變量的值馬上回寫到主內(nèi)存。
下面是一個Java可見性的代碼示例:
public class VisibilityDemo {
private static volatile boolean flag = false;
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
flag = true;
System.out.println("t1 flag = true");
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
while (!flag) {
// 循環(huán)等待flag變?yōu)閠rue
}
System.out.println("t2 flag = true");
}
});
t1.start();
t2.start();
}
}
執(zhí)行結(jié)果:

在這個示例中,我們使用了一個volatile布爾變量flag。當(dāng)flag被修改為true時,會立即被其他線程看到。在第二個線程中,我們使用了一個while循環(huán)來等待flag變?yōu)閠rue。當(dāng)flag變?yōu)閠rue時,第二個線程會立即執(zhí)行下一步操作。因此,我們可以保證在flag變?yōu)閠rue時,所有線程都能夠看到這個修改,并作出相應(yīng)的反應(yīng)。
有序性:
Java并發(fā)有序性是指在多線程的環(huán)境下,程序執(zhí)行的順序按照代碼的先后順序執(zhí)行,禁止進行指令重排序??此评硭?dāng)然的事情,其實并不是這樣,指令重排序是 JVM為了優(yōu)化指令,提高程序運行效率,在不影響單線程程序執(zhí)行結(jié)果的前提下,盡可能地提高并行度。但是在多線程環(huán)境下,有些代碼的順序改變,有可能引發(fā)邏輯上的不正確。
在Java中,為了提高程序的運行效率,可能在編譯期和運行期會對代碼指令進行一定的優(yōu)化,不會百分之百的保證代碼的執(zhí)行順序嚴(yán)格按照編寫代碼中的順序執(zhí)行,但也不是隨意進行重排序,它會保證程序的最終運算結(jié)果是編碼時所期望的。這種情況被稱之為指令重排(Instruction Reordering)。
下面是一個Java可見性的代碼示例:
public class SequentialDemo {
private static int count = 0;
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (this) {
count++;
System.out.println("t1 count = " + count);
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (this) {
count++;
System.out.println("t2 count = " + count);
}
}
});
t1.start();
t2.start();
}
}
執(zhí)行結(jié)果:

在這個示例中,我們使用了一個靜態(tài)變量count。在第一個線程中,我們使用synchronized塊來保證對count的修改是原子的,不會被其他線程中斷。在第二個線程中,我們也使用了synchronized塊來保證對count的修改是原子的。由于我們使用了synchronized塊,因此可以保證在每個線程執(zhí)行完畢后,count的值都會自增1,從而保證了有序性。