07. 線程的安全性分析

線程的使用帶來了很是多的便利同時,也給咱們帶來了不少困擾java

當多個線程訪問某個對象時,無論運行時環境採用何種調度方式或者這些線程將如何交替執行,而且在主調代碼中不須要任何額外的同步或者協同,這個類都能表現出正確的行爲,那麼就稱這個類是線程安全的數組

截圖

1、線程安全問題的本質

一、原子性緩存

二、可見性安全

三、有序性架構

截圖

一、CPU 增長了告訴緩存,均衡與內存的速度差別app

二、操做系統增長進程、線程、以及分時複用 CPU,均衡 CPU 與 I/O 設備的速度差別函數

三、編譯程序優化指令的執行順序,使得可以更加合理的利用緩存工具

截圖

截圖

public class AtomicDemo{
    public static int count=0;
    public static void incr(){
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count++; //count++ (只會由一個線程來執行)
    }
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            new Thread(AtomicDemo::incr).start();
        }
        Thread.sleep(4000);
        System.out.println("result:"+count);
    }
}

截圖

一、Java的內存模型

JMM優化

​ Java內存模型是一種抽象結構,它提供了合理的禁用緩存以及禁止重排序的方法來解決可見性,有序性問題this

截圖

截圖

截圖

二、可見性,有序性的解決方案

一、Volatile、synchronized、final 關鍵字

二、Happens-Before 原則

1)synchronized

鎖的範圍

​ 一、對於普通同步方法,鎖是當前實例對象。

​ 二、對於靜態同步方法,鎖是當前類的Class對象。

​ 三、對於同步方法塊,鎖是 Synchonized 括號裏配置的對象。

//對象鎖(同一對象生效),鎖的是this
public class SyncDemo {
    //對象鎖
    public synchronized void demo(){}
    public static void main(String[] args) {
        SyncDemo syncDemo1 = new SyncDemo();
        SyncDemo syncDemo2 = new SyncDemo();
        //沒法實現兩個線程的互斥
        new Thread(()->{
            syncDemo.demo();
        }).start();
        new Thread(()->{ 
//            syncDemo1.demo();
            syncDemo.demo();//BLOCKED狀態
        }).start();
    }
}
//靜態同步方法(類級別的鎖),鎖的是 SyncDemo.class
public class SyncDemo {
    //靜態同步方法,
    public synchronized static void demo(){

    }
    public static void main(String[] args) {
        SyncDemo syncDemo = new SyncDemo();
        SyncDemo syncDemo1 = new SyncDemo();
        //若是synchronized爲靜態方法,那麼下面兩個線程會互存在斥
        new Thread(()->{
            syncDemo.demo();
        }).start();
        new Thread(()->{
            syncDemo1.demo();
        }).start();
    }
}
//同步方法塊,能夠是對象鎖,也能夠是類級別鎖,可是範圍是可控的。
public class SyncDemo {
    
    public void demo(){
        //TODO
        synchronized (this){//對象鎖
        }
        //TODO
    }
    public void demo1(){
        //TODO
        synchronized (SyncDemo.class){//類級別的鎖
        }
        //TODO
    }
    
}

一、以上出現的鎖就只有兩把,一個是對象鎖,一個是類級別鎖。

二、synchronized方法塊中鎖的範圍是可控的

截圖

synchronized 的優化

​ 一、自適應自旋鎖

​ 二、引入偏向鎖、輕量級鎖

​ 三、鎖消除、鎖粗化

2)volatile

volatile 是能夠用來解決可見性和有序性問題的

Lock指令的做用

​ 一、將當前處理器緩存行的數據寫回到系統內存

​ 二、這個寫回內存的操做會使在其餘CPU裏緩存了該內存地址的數據無效

什麼狀況下須要用到volatile

​ 當存在多個線程對同一個共享變量進行修改的時候,須要增長volatile,保證數據修改的實時可見

public class VisableDemo {
    //volatile解決[可見性]
    public volatile static boolean stop=false;
    
    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            int i=0;
            while(!stop){
                i++;
            }
            System.out.println("result:"+i);
        });
        thread.start();
        System.out.println("begin start thread");
        Thread.sleep(1000);
        stop=true; //主線程中修改stop的值
    }
}
//volatile 解決 [有序性]
value = 3;
void exeToCPU0(){
    value = 10;
    isFinsh = true;
}
void exeToCPU1(){
   	if(isFinsh){
        assert value == 10;
    }
}
/*
CPU層面的內存屏障,(X86架構裏面提供了三種內存屏障)
一、Store Barrier[寫],強制全部在 store 屏障指令以前的 store 指令,都在該 store 屏障指令執行以前被執行,並把 store 緩衝區的數據都刷到 CPU 緩存
二、Load Barrier[讀],強制全部在 load 屏障指令以後的 load 指令,都在該 load 屏障指令執行以後被執行,而且一直等到 load 緩衝區被該 CPU 讀完才能執行以後 load 指令
三、Full Barrier[全],複合了 load 和 store 屏障的功能
*/
value = 3;
void exeToCPU0(){
    value = 10;
    storebarrier();//寫屏障,讓value 與 isFinsh 沒法指令重排
    isFinsh = true;
}
void exeToCPU1(){
   	if(isFinsh){
        loadbarrier();//讀屏障,確保value必定是10[最新值]
        assert value == 10;
    }
}

截圖

本質上來講:volatile 其實是 經過內存屏障來防止指令重排序 以及 禁止 cpu 高速緩存來解決可見性問題

#Lock 指令,它本意上是禁止高速緩存解決可見性問題,但實際上在這裏,它表示的是一種內存屏障的功能。也就是說針對當前的硬件環境, JMM 層面採用 Lock 指令做爲內存屏障來解決可見性問題。

3)final 關鍵字

final 在 Java中是一個保留的關鍵字,能夠聲明成員變量、方法、類以及本地變量。一旦你將引用聲明作 final,你將不能改變這個引用了

final域 與 線程安全 有什麼關係?

對於 final 域,編譯器 和 處理器 要遵照兩個重排序規則

​ 1)在構造函數內對一個 final 域的寫入,與隨後把這個被構造對象的引用賦值給一個引用變量,這兩個操做之間不能重排序。

​ 2)初次讀一個包含 final 域的對象的引用,與隨後初次讀這個final域,這兩個操做之間不能重排序。

//寫 final 域得重排序規則
public class FinalExample{
    int i;	//普通變量
    final int j;	//final變量
    static FinalExample obj;
    public FinalExample(){	//構造函數
        i = 1;	//寫普通域
        j = 2;	//寫 final 域
    }
    public static void writer(){//寫線程 A 執行
        obj = new FinalExample();
    }
    public static void reader(){	//讀線程 B 執行
        FinalExample object = obj;	//都對象引用
        int a = object.i;	//讀普通域
        int b = object.j;	//讀 fianl 域
    }
}

寫final域重排序規則

​ 一、JMM禁止編譯器把final域的寫重排序到構造函數以外。

​ 二、編譯器會在 final 域的寫以後,構造函數 return 以前,插入一個 StoreStore 屏障。這個屏障禁止處理器把final域的寫重排序到構造函數以外

截圖

讀域的重排序規則

​ 在一個線程中,初次讀對象引用與初次讀該對象包含的 final 域,JMM 禁止處理器重排序這兩個操做,編譯器會在讀final域操做的前面插入一個 LoadLoad 屏障。

截圖

截圖

截圖

三、Happens-Before模型

什麼是Happens-Before?

​ Happens-Before 是一種可見性規則,它表達的含義是前面一個操做的結果對後續操做是可見的。

​ A Happens-Before B ➡ A 得執行結果對 B 可見

6種 Happens-Before 規則 [ 天生知足可見性規則 ]

​ 一、程序順序規則

​ 二、監視器鎖規則

​ 三、Volatile 變量規則

​ 四、傳遞性

​ 五、start() 規則

​ 六、Join() 規則

1)程序順序規則

/*
as-if-serial 
	單線程中無論怎麼重排序,獲得的執行結果是不能改變的
	編譯器/處理器都要遵照,所以不會對存在數據依賴關係的語句進行重排序
*/
class VolatileExample{
    int x = 0;
    volatile boolean v = false;
    public void write(){
        x = 42;
        v = true;
    }
    public void reader(){
        if(v == true){
            //這裏x會是多少?
            
        }
    }
}

2)監視器鎖規則

對一個鎖的解鎖 Happens-Before 於後續對這個鎖的枷鎖

synchronized(this){//此處自動枷鎖
    //x 是共享變量,初始值 = 10
    if(this.x < 12){
        this.x = 12;
    }
}//此處自動解鎖

3)volatile 變量規則

對一個 volatile 域的寫,Happens-Before 於任意後續對這個 volatile 域的讀。

其實是經過內存屏障來實現的

4)傳遞性規則

若是 A happens-before B,且 B happens-before C,那麼 A happens-before C

class VolatileExample{
    int x = 0;
    volatile boolean v = false;
    public void write(){
        x = 42;
        v = true;
    }
    public void reader(){
        if(v == true){
            //這裏x會是多少?
            
        }
    }
}

5)start() 規則

若是線程A執行操做ThreadB.start()(啓動線程B),那麼A線程的ThreadB.start()操做happens-before於線程B中的任意操做

Thread B = new Thread(()->{
   //主線程調用 B.start()以前
   //全部對共享變量的修改,此處皆可見
   //此例中,var == 77
});
//此處對共享變量 var 修改
var = 77;
//主線程啓動子線程
B.start();

6)join() 規則

若是線程A執行操做 ThreadB.join() 併成功返回,那麼 線程B 中的任意操做 happens-before 於 線程A 從 ThreadB.join() 操做成功返回

Thread B = new Thread(()->{
    //此處對共享變量var 修改
    var = 66
});
//例如此處對共享變量修改
//則這個修改結果對線程B可見
//主線程啓動子線程
B.start();
B.join();
//子線程全部對共享變量的修改
//在主線程調用B.join()以後皆可見
//此例中,var == 66

四、原子類 Atomic

原子性問題的解決方案

​ synchronized、Lock

​ J.U.C 包下的 Atomic 類 [ 無鎖工具的典範 ]

import java.util.concurrent.atomic.AtomicInteger;
//atomic 保證原子性
public class AtomicDemo{
//    public static int count=0;
    private static AtomicInteger atomicInteger=new AtomicInteger(0);
    public static void incr(){
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
//        count++; //count++ (只會由一個線程來執行)
        atomicInteger.incrementAndGet();
    }
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            new Thread(AtomicDemo::incr).start();
        }
        Thread.sleep(4000);
//        System.out.println("result:"+count);
        System.out.println("result:"+atomicInteger.get());
    }
}

Atomic 實現原理

​ 一、Unsafe 類

​ 二、CAS

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

Atomic 分類

​ 一、原子更新基本類型

​ 二、原子更新數組

​ 三、原子更新引用類型

​ 四、原子更新字段類

五、ThreadLocal 的使用和原理

public class ThreadLocalDemo {

    private static Integer num=0;

    public static final ThreadLocal<Integer> local=new ThreadLocal<Integer>(){
        protected Integer initialValue(){
            return 0; //初始值
        }
    };
    public static final ThreadLocal<Integer> local1=new ThreadLocal<Integer>();
    public static void main(String[] args) {
        Thread[] threads=new Thread[5];
        //但願每一個線程都拿到的是0
        for (int i = 0; i < 5; i++) {
            threads[i]=new Thread(()->{
//                num+=5;
                int num=local.get(); //拿到初始值
                local1.get();
                num+=5;
                local.set(num);
                System.out.println(Thread.currentThread().getName()+"->"+num);
            },"Thread-"+i);
        }
        for(Thread thread:threads){
            thread.start();
        }
    }
}
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

截圖

2、如何安全發佈對象

一、發佈對象

發佈的意思是使一個對象可以被當前範圍以外的代碼所使用

public static HashSet<Person> persons;
public void init(){
    persons = new HashSet<Person>();
}
//不安全發佈
private String[] states = {'a','b','c','d'};
//發佈出去一個
public String[] getStates(){
    return states;
}
public static void main(String[] args){
    App unSafePub = new App();
    System.out.println("Init array is: "+Arrays.toString(unSafePub.getStates()));
    unSafePub.getStates()[0] = "Mic!";
    System.out.println("After modify.the array is: "+ Arrays.toString(unSafePub.getStates()));
    
}

二、對象溢出

也成爲對象的逃逸,一種錯誤的發佈,當一個對象尚未構造完成時,就使它被其餘線程所見

截圖

截圖

三、安全發佈對象

一、在靜態初始化函數中初始化一個對象引用

二、將對象的引用保存到volatile類型的域或者AtomicReference對象中(利用volatile happen-before規則)

三、將對象的引用保存到某個正確構造對象的final類型域中(初始化安全性)

四、將對象的引用保存到一個由鎖保護的域中(讀寫都上鎖)

//靜態初始化
public class StaticDemo {
    private StaticDemo(){}
    private static StaticDemo instance=new StaticDemo();
    public static StaticDemo getInstance(){
        return instance;
    }
}
//final域
public class FinalDemo {
    private final Map<String,String> states;
    public FinalDemo(){
        states=new HashMap<>();
        states.put("mic","mic");
    }
}
//volatile
public class VolatileSyncDemo {
    private VolatileSyncDemo(){}
     //DCL問題[加 volatile]
    private volatile static VolatileSyncDemo instance=null;
    public static VolatileSyncDemo getInstance(){
        if(instance==null){
            synchronized(VolatileSyncDemo.class) {
                if(instance==null) {
                    instance = new VolatileSyncDemo();
                }
            }
        }
        return instance;
    }
    /**
     * instance = new VolatileSyncDemo();
     * ->
     * 1. memory=allocate()
     * 2. 
     * 3. instance=memory
     *	  ctorInstance(memory)
     *
     * 1.3.2 (不完整實例)
     */
}
相關文章
相關標籤/搜索