一、volatile關鍵字與內存可見性
1、測試沒有 volatile關鍵字的demo
public class VolatileTest1 {
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
new Thread(threadDemo).start();
while(true){
if(threadDemo.isFlag()){
System.out.println("----主線程讀到flag爲true----");
break;
}
}
}
}
class ThreadDemo implements Runnable {
private boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
}
flag = true;
System.out.println("子線程修改了值flag=" + isFlag());
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
在子線程中將線程的共享變量 flag的值修改成了 true時,但是主線程在條件判斷時讀到的flag一直是false,所以while循環不會停止跳出,程序不會終止。這是由於內存的可見性導致的。
2、內存可見性(Memory Visibility)
內存可見性(Memory Visibility)其實是指共享變量在不同線程之間的可見性。
- 共享變量:如果一個變量在多個線程的工作內存中都存在副本,那麼這個變量就是這幾個線程的共享變量,即通常稱這種被多個線程訪問的變量爲共享變量。
- 可見性:指當某個線程正在使用共享變量並對共享變量的值做了修改時,能夠及時的被其他線程看到共享變量的變化。
內存可見性與Java內存模型有關係
所有的變量都存儲在主內存中(操作系統給進程分配的內存空間),而每個線程都有自己獨立的工作內存,裏面保存該線程使用到的變量的副本。
注意:線程對共享變量的所有操作都必須在自己的工作內存(working memory,是cache和寄存器的一個抽象,並不是內存中的某個部分)。不同線程之間,當前線程無法直接訪問其他線程的工作內存中的變量,線程間變量值得傳遞需要通過主內存來完成。
緩存一致性協議。最出名的就是Intel 的MESI協議,MESI協議保證了每個緩存中使用的共享變量的副本是一致的。它核心的思想是:當CPU寫數據時,如果發現操作的變量是共享變量,即在其他CPU中也存在該變量的副本,會發出信號通知其他CPU將該變量的緩存行置爲無效狀態,因此當其他CPU需要讀取這個變量時,發現自己緩存中緩存該變量的緩存行是無效的,那麼它就會從內存重新讀取。
解決共享變量的內存可見問題的方式有很多
1、synchronized實現可見性
JMM(Java內存模型)關於synchronized的兩條規定:
線程解鎖前,必須把共享變量的最新值刷新到主內存中,
線程加鎖時,將清空工作內存中共享變量的值,從而使用共享變量時需要從主內存中重新讀取最新的值。
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
new Thread(threadDemo).start();
while(true){
// main線程加鎖
synchronized(threadDemo){
if(threadDemo.isFlag()){
System.out.println("----主線程讀到flag爲true----");
break;
}
}
}
}
2、volatile關鍵字實現可見性
對於多線程, volatile不具備“互斥性”,不能保證變量狀態的“原子性操作”。
使用 volatile 關鍵字用來確保將變量的更新操作通知到其他線程。
某個線程的工作內存中修改了共享變量的值並會刷新到主內存中,同時其他線程已經讀取的共享變量副本就會失效,需要讀數據時就會再次去主內存中讀取新的共享變量的值,從而達到共享變量內存可見。
// 共享變量用 volatile修飾即可
private volatile boolean flag = false;
synchronized 和 volatile比較
synchronized具備“互斥性”,既能保證可見性,又能保證原子性,volatile不具備“互斥性”,只能保證可見性,不能保證原子性。
volatile不需要加鎖,比synchronized更輕量級,不會阻塞線程,效率更高。如果能用 volatile解決問題,應儘量使用volatile,因爲它的效率比synchronized更高。
二、原子性
原子性:一次操作,要麼全部執行成功,要麼全部執行失敗。一個很經典的例子就是銀行賬戶轉賬問題。
1、一個實例demo
public class AtomicTest {
public static void main(String[] args) {
AtomicDemo atomicDemo = new AtomicDemo(0);
for (int i = 0; i < 10; i++) {
new Thread(atomicDemo).start();
}
}
}
class AtomicDemo implements Runnable{
//線程共享變量
private volatile int number;
public AtomicDemo(int number) {
this.number = number;
}
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + ",number=" + ++number);
}
}
運行結果會發現可能會在不同的線程中,看到相同的數值,這是由於 volatile關鍵字保證了操作的內存可見性,但是 volatile不能保證操作的原子性。
自增操作不是原子性操作,它包括讀取變量的原始值、進行加1操作、寫入工作內存。而且volatile也無法保證對變量的任何操作都是原子性的。
2、解決原子性操作問題--JUC
java.util.concurrent.atomic 原子操作類包裏面提供了一組原子變量類。封裝了一系列常用的數據類型對應的封裝類,
Java.util.concurrent.atomic 中實現的原子操作類可以分成4組:
標量類:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
數組類:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
更新器類:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
複合變量類:AtomicMarkableReference,AtomicStampedReference
這些類都保證了兩點:
1)類裏的變量都用了volatile保證內存是可見的
2)使用了一個算法CAS(Compare And Swap),保證對這些數據的操作具有原子性
public class AtomicTest1 {
public static void main(String[] args) throws InterruptedException {
//線程共享變量
AtomicInteger atomicInteger = new AtomicInteger(0);
AtomicDemo1 atomicDemo = new AtomicDemo1(atomicInteger);
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(atomicDemo);
thread.start();
}
}
}
class AtomicDemo1 implements Runnable{
private AtomicInteger atomicInteger = null;
public AtomicDemo1(AtomicInteger atomicInteger) {
this.atomicInteger = atomicInteger;
}
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + ",atomicInteger=" + atomicInteger.incrementAndGet());
}
}
參考文章: