1. 程式人生 > >Java線程安全:可見性,原子性,有序性

Java線程安全:可見性,原子性,有序性

處理器 ida 保持 jdk1 沒有 alt pri service example

Java線程安全

可見性,原子性,有序性

Java內存模型(JMM)

Java內存模型(Java Memory Model)描述了Java程序中各種變量(線程共享變量)的訪問規則,以及在JVM中將變量存儲到內存和從內存中讀取變量這樣的底層細節。

  • 所有的變量都存儲在主內存中。
  • 每個線程都有自己獨立的工作內存,裏面保持該線程使用到的變量副本。
    技術分享圖片

  • 線程對共享變量的所有操作都必須在自己的工作內存中進行,不能直接從主內存中進行讀寫。

  • 不同線程之間無法直接訪問其他線程工作內存的變量,所以線程間變量值的傳遞需要通過主內存來完成。

如何實現可見性

要實現共享變量的可見性,必須保證兩點:

  • 線程修改後的共享變量值能夠及時從工作內存刷到主內存中。
  • 其他線程能夠及時把共享變量的最新值從主內存更新到自己的工作內存中。

synchronized實現可見性

JMM關於synchronized的兩條規定:

  • 線程解鎖前,必須把共享變量的最新值刷新到主內存中。
  • 線程加鎖時,將清空工作內存中共享變量的值,從而使用共享變量時候需要從主內存中重新讀取最新值。
線程執行互斥代碼的過程:
  • 獲取互斥鎖
  • 清空工作內存
  • 從主內存拷貝變量最新值到工作內存中
  • 執行代碼
  • 將更改後的共享變量的值刷新到主內存中
  • 釋放互斥鎖
Synchronized的使用
  • 修飾代碼快:{}裏的代碼塊,作用與調用對象
  • 修飾方法:整個方法,作用於調用的對象
  • 修飾靜態的方法:整個靜態方法,作用於所有對象
  • 修飾類:{}括起來的部分,作用於所有對象
public class SyncExample {

    //修飾代碼快
    public void test1(String name){
        synchronized (this){
            for (int i = 0; i < 10; i++){
                System.out.println(name + ":" + i);
            }
        }
    }

    //修飾方法
    public synchronized void test2(String name){
        for (int i = 0; i < 10; i++){
            System.out.println(name + ":" +i);
        }
    }

    public static void main(String[] args) {
        final SyncExample sync1 = new SyncExample();
        SyncExample sync2 = new SyncExample();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> sync1.test1("sync1"));
        executorService.execute(() -> sync2.test1("sync2"));

    }
}
//結果
sync2:0
sync1:0
sync2:1
sync1:1
sync2:2
sync1:2
sync1:3
sync2:3
sync1:4
sync2:4
sync1:5
sync2:5
sync1:6
sync2:6
sync1:7
sync2:7
sync1:8
sync2:8
sync1:9
sync2:9

sync1和sync2的test1交替執行,說明同步代碼塊作用在當前對象,不同對象調用互不影響。

當換成test2()時,結果和上相同,說明synchronized修飾一個方法時,作用和同步代碼塊相同,都是作用當前對象。

所以,如果一個方法中是一個完整的同步代碼快,它和synchronized修飾一個方法是等同的。

當修飾一個類時

  public static void test3(){
        synchronized (SyncExample.class){
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread() + "test3 :" + i + " ");
            }
        }
    }

Thread[pool-1-thread-1,5,main]test3 :0 
Thread[pool-1-thread-1,5,main]test3 :1 
Thread[pool-1-thread-1,5,main]test3 :2 
Thread[pool-1-thread-1,5,main]test3 :3 
Thread[pool-1-thread-1,5,main]test3 :4 
Thread[pool-1-thread-2,5,main]test3 :0 
Thread[pool-1-thread-2,5,main]test3 :1 
Thread[pool-1-thread-2,5,main]test3 :2 
Thread[pool-1-thread-2,5,main]test3 :3 
Thread[pool-1-thread-2,5,main]test3 :4 

從結果上可以看出,修飾一個類時,作用於這個類的所有對象。

volatile 實現可見性

volatile通過加入內存屏障和禁止重排序優化來實現可見性

  • 對volatile變量執行寫操作時,會在寫操作後加入一條store屏障指令
  • 對volatile變量執行讀操作時,會在讀操作前加入一條load屏障指令

volatile變量在每次被線程訪問時,都強迫從主內存中讀取該變量的值,而當發生變化時,會強迫線程將最新的值刷新到主內存。

  • 線程寫volatile變量的過程
  1. 改變線程工作內存中volatile變量副本的值。
  2. 將改變後的副本的值重工作內存刷新到主內存
  • 線程度volatile變量的過程
  1. 將主內存中讀取volatile變量的最新值到工作內存中。
  2. 從工作內存中讀取volatile變量的副本。

volatile不能保證原子性

private int number = 0;
number ++;

number ++ 不是原子操作,可以分為三步
1.讀取number的值,2.將number值+1,3.寫入最新的numnber的值

public class TestAtomicDemo {

    public static void main(String[] args) {
        AtomicDemo demo = new AtomicDemo();
        for (int i = 0; i < 10; i ++){
            new Thread(demo).start();
        }
    }

}

class AtomicDemo implements Runnable{

    private volatile int serialNumber = 0;

    public void run() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() +":"+getSerialNumber());
    }

    public int getSerialNumber(){
        return serialNumber ++;
    }
}


Thread-0:0
Thread-1:1
Thread-3:3
Thread-2:2
Thread-5:4
Thread-4:5
Thread-6:7
Thread-7:7
Thread-8:6
Thread-9:8

可以看到Thread6和Thread7的執行結果相同。

原子性

原子性:提供了互斥訪問,同一時刻,只能由一個線程對它進行操作。

jdk1.5後java.util.concurrent.atomic包下面提供可常用的原子變量。CAS(Compare-And-Swap)算法保證數據原子性。

private volatile AtomicInteger serialNumber = new AtomicInteger(0);

 public int getSerialNumber(){
        return serialNumber.incrementAndGet();
    }

AtomicInteger的incrementAndGet()方法是如何保證原子性的呢?

private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
    private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");

    private volatile int value;
    /**
     * Atomically increments the current value,
     * with memory effects as specified by {@link VarHandle#getAndAdd}.
     *
     * <p>Equivalent to {@code addAndGet(1)}.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        return U.getAndAddInt(this, VALUE, 1) + 1;
    }

從源碼(我的是JDK10)中可以看到AtomicInteger使用了一個Unsafe類的getAndAddInt方法。

/**
     * Atomically adds the given value to the current value of a field
     * or array element within the given object {@code o}
     * at the given {@code offset}.
     *
     * @param o object/array to update the field/element in
     * @param offset field/element offset
     * @param delta the value to add
     * @return the previous value
     * @since 1.8
     */
    @HotSpotIntrinsicCandidate
    public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
            v = getIntVolatile(o, offset);
        } while (!weakCompareAndSetInt(o, offset, v, v + delta));
        return v;
    }

getIntVolatile方法,如果沒有其他線程來處理Object o,這個方法返回只v就等於offset。
weakCompareAndSetInt方法就是CAS算法核心
offset為內存值,v是預估值,v+delta為更新值,當且僅當offset==v,才會+1操作。

有序性

Java內存模型中,允許編譯器和處理器對指令進行重排序,但是重排序的過程不會影響單線程的執行結果,卻會影響到多線程並發執行的正確性。

volatile, synchronized,Lock,可以保證有序性

##### happens-before原則

  1. 程序次序規則:一個線程內,按照代碼順序,書寫在前的操作先於後的操作。Java虛擬機只會對沒有數據依賴的代碼進行重排序。
  2. 鎖定規則,一個unLock操作先行發生於後面對同一個鎖的lock操作。
  3. volatile變量規則:對一個變量的寫操作先行發生於後面對這個變量的讀操作。
  4. 傳遞規則:提現了happens-before原則具有傳遞性,即A happens-before B , B happens-before C,那麽A happens-before C
  5. 線程啟動規則:假定線程A在執行過程中,通過執行ThreadB.start()來啟動線程B,那麽線程A對共享變量的修改在接下來線程B開始執行後確保對線程B可見。
  6. 線程終結規則:假定線程A在執行的過程中,通過制定ThreadB.join()等待線程B終止,那麽線程B在終止之前對共享變量的修改在線程A等待返回後可見。
  7. 對象終結規則:一個對象的初始化完成先行發生於它的finalize()方法的開始。

Java線程安全:可見性,原子性,有序性