併發和並行的含義
併發(concurrency):指在同一時刻只能有一條指令執行,但多個進程指令被快速的輪換執行,使得在宏觀上具有多個進程同時執行的效果,但在微觀上並不是同時執行的,只是把時間分成若干段,使多個進程快速交替的執行。
併發的三大特性
可見性:當一個線程修改了共享變量的值,其他線程能夠看到修改的值。
有序性:即程序執行的順序按照代碼的先後順序執行
原子性:一個或多個操作,要麼全部執行且在執行過程中不被任何因素打斷,要麼全部不執行。不採用任何的原子性保障的自主操作都不是原子性的。
Chapter 17. Threads and Locks (oracle.com)
例一,可見性問題的深入分析
public class VisibilityTest {
private boolean flag = true;
public void refresh() {
flag = false;
System.out.println(Thread.currentThread().getName() + "修改flag");
}
public void load() {
System.out.println(Thread.currentThread().getName() + "開始執行.....");
int i = 0;
while (flag) {
i++;
}
System.out.println(Thread.currentThread().getName() + "跳出循環: i=" + i);
}
public static void main(String[] args) throws InterruptedException {
VisibilityTest test = new VisibilityTest();
// 線程threadA模擬數據加載場景
Thread threadA = new Thread(() -> test.load(), "threadA");
threadA.start();
// 讓threadA執行一會兒
Thread.sleep(1000);
// 線程threadB通過flag控制threadA的執行時間
Thread threadB = new Thread(() -> test.refresh(), "threadB");
threadB.start();
}
}
結果
threadA開始執行.....
threadB修改flag
由於可見性原因, 程序不會中斷
- lock(鎖定):作用於主內存的變量,把一個變量標識爲一條線程獨佔狀態。
- unlock(解鎖):作用於主內存變量,把一個處於鎖定狀態的變量釋放出來,釋放後的變量纔可以被其他線程鎖定。
- read(讀取):作用於主內存變量,把一個變量值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用
- load(載入):作用於工作內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中。
- use(使用):作用於工作內存的變量,把工作內存中的一個變量值傳遞給執行引擎,每當虛擬機遇到一個需要使用變量的值的字節碼指令時將會執行這個操作。
- assign(賦值):作用於工作內存的變量,它把一個從執行引擎接收到的值賦值給工作內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作。
- store(存儲):作用於工作內存的變量,把工作內存中的一個變量的值傳送到主內存中,以便隨後的write的操作。
- write(寫入):作用於主內存的變量,它把store操作從工作內存中一個變量的值傳送到主內存的變量中。
代碼例一中問題的分析
當線程A啓動後,會從主存中經過 read、load 操作將變量加載到線程本地內存中。當線程B修改flag變量後,由於線程A執行的代碼是while(flag) ,所以本地內存中的變量副本緩存不會失效,拿不到最新數據最終導致不可見的問題。
在這個程序示例中,我們嘗試用幾種不同的方法解決可見性問題,然後分享一下以下幾種方法爲什麼能解決可見性問題。
public class VisibilityTest {
// storeLoad JVM內存屏障 ----> (彙編層面指令) lock; addl $0,0(%%rsp)
// lock前綴指令不是內存屏障的指令,但是有內存屏障的效果 緩存失效
// private volatile boolean flag = true;
private boolean flag = true;
// private int count = 0;
private Integer count = 0;
public void refresh() {
flag = false;
System.out.println(Thread.currentThread().getName() + "修改flag:" + flag);
}
public void load() {
System.out.println(Thread.currentThread().getName() + "開始執行.....");
while (flag) {
//TODO 業務邏輯
count++;
// 第一種, 內存屏障
UnsafeFactory.getUnsafe().storeFence();
// 第二種, 釋放時間片, 上下文切換加載上下文, 緩存失效重新從主存加載
Thread.yield();
// 第三種, println中用到了synchronized, 能夠跳出循環,內存屏障
System.out.println(count);
// 第四種, unpark也是用到了內存屏障
LockSupport.unpark(Thread.currentThread());
// 第五種, 使用一個方法模擬運行時間在1ms時發現本地方法副本緩存會失效, 當1000納秒時不會失效
shortWait(1000000); //1ms
shortWait(1000);
// 第五種, sleep方法中也用到了內存屏障
try {
Thread.sleep(1); //內存屏障
} catch (InterruptedException e) {
e.printStackTrace();
}
// 第六種, 當count爲final修飾時, 也可以解決可見性問題, 原因未知
//總結: Java中可見性如何保證? 方式歸類有兩種:
//1. jvm層面 storeLoad內存屏障 ===> x86 lock替代了mfence
//2. 上下文切換 Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "跳出循環: count=" + count);
}
public static void main(String[] args) throws InterruptedException {
VisibilityTest2 test = new VisibilityTest2();
// 線程threadA模擬數據加載場景
Thread threadA = new Thread(() -> test.load(), "threadA");
threadA.start();
// 讓threadA執行一會兒
Thread.sleep(1000);
// 線程threadB通過flag控制threadA的執行時間
Thread threadB = new Thread(() -> test.refresh(), "threadB");
threadB.start();
}
public static void shortWait(long interval) {
long start = System.nanoTime();
long end;
do {
end = System.nanoTime();
} while (start + interval >= end);
}
}
在linux系統x86中的實現
orderAccess_linux_x86.inline.hpp
如果是多核心處理器時,在x86的架構中內存屏障的實現方式使用lock前綴代替,因爲效率高
inline void OrderAccess::storeload() { fence(); }
inline void OrderAccess::fence() {
if (os::is_MP()) {
// always use locked addl since mfence is sometimes expensive
#ifdef AMD64
__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
__asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
}
}
void TemplateTable::volatile_barrier(Assembler::Membar_mask_bitsorder_constraint) {
// Helper function to insert a is‐volatile test and memory barrier
// 出入內存屏障
if (os::is_MP()) { // Not needed on single CPU
__ membar(order_constraint);
}
}
// 負責執行putfield或putstatic指令
void TemplateTable::putfield_or_static(int byte_no, bool is_static, RewriteControl rc) {
// ...
// Check for volatile store
__ testl(rdx, rdx);
__ jcc(Assembler::zero, notVolatile);
putfield_or_static_helper(byte_no, is_static, rc, obj, off, flags);
volatile_barrier(Assembler::Membar_mask_bits(Assembler::StoreLoad |Assembler::StoreStore));
__ jmp(Done);
__ bind(notVolatile);
putfield_or_static_helper(byte_no, is_static, rc, obj, off, flags);
__ bind(Done);
}
// Serializes memory and blows flags
void membar(Membar_mask_bits order_constraint) {
// We only have to handle StoreLoad
// x86平臺只需要處理StoreLoad
if (order_constraint & StoreLoad) {
int offset = ‐VM_Version::L1_line_size();
if (offset < ‐128) {
offset = ‐128;
}
// 下面這兩句插入了一條lock前綴指令: lock addl $0, $0(%rsp)
lock(); // lock前綴指令
addl(Address(rsp, offset), 0); // addl $0, $0(%rsp)
}
}
JMM內存屏障插入策略
1. 在每個volatile寫操作的前面插入一個StoreStore屏障
2. 在每個volatile寫操作的後面插入一個StoreLoad屏障
3. 在每個volatile讀操作的後面插入一個LoadLoad屏障
4. 在每個volatile讀操作的後面插入一個LoadStore屏障
JSR133規範
x86處理器不會對讀-讀、讀-寫和寫-寫操作做重排序, 會省略掉這3種操作類型對應的內存屏障。僅會對寫-讀操作做重排序,所以volatile寫-讀操作只需要在volatile寫後插入StoreLoad屏障
lock前綴指令的作用
1. 確保後續指令執行的原子性。在Pentium及之前的處理器中,帶有lock前綴的指令在執行期間會鎖住總線,使得其它處理器暫時無法通過總線訪問內存,很顯然,這個開銷很
大。在新的處理器中,Intel使用緩存鎖定來保證指令執行的原子性,緩存鎖定將大大降低lock前綴指令的執行開銷。
2. LOCK前綴指令具有類似於內存屏障的功能,禁止該指令與前面和後面的讀寫指令重排序。
3. LOCK前綴指令會等待它之前所有的指令完成、並且所有緩衝的寫操作寫回內存(也就是將store buffer中的內容寫入內存)之後纔開始執行,並且根據緩存一致性協議,刷新
store buffer的操作會導致其他cache中的副本失效。
‐XX:+UnlockDiagnosticVMOptions ‐XX:+PrintAssembly ‐Xcomp