線程安全性

1.定義

當多個線程訪問某個類時,不管運行時環境採用何種調度方式或者這些進程將如何交替執行,並且在主調代碼中不需要任何額外的同步和協同,這個類都能夠表現出正確的行爲,那麼就稱這個類爲線程安全的類;

 

2.線程安全的體現

併發程序正確地執行,必須要保證原子性、可見性以及有序性。只要有一個沒有被保證,就有可能會導致程序運行不正確。

原子性:一個操作或多個操作要麼全部執行完成且執行過程不被中斷,要麼就不執行。

可見性:當多個線程同時訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。

有序性:程序執行的順序按照代碼的先後順序執行。

對於單線程,在執行代碼時jvm會進行指令重排序,處理器爲了提高效率,可以對輸入代碼進行優化,它不保證程序中各個語句的執行先後順序同代碼中的順序一致,但是它會保證保存最終執行結果和代碼順序執行的結果是一致的。

3.原子性詳解

3.1 Atomic包(在Atomic包中都是使用CAS來保證原子性)

    代碼示例:

@Slf4j
@ThreadSafe
public class AtomicExample1 {

    // 請求總數
    public static int clientTotal = 5000;

    // 同時併發執行的線程數
    public static int threadTotal = 200;

    public static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count.get());
    }

    private static void add() {
        count.incrementAndGet();
        // count.getAndIncrement();
    }
}

我們可以通過簡單的改造將原來count的類型由int修改爲AtomicInteger這個類就由線程不安全變成了線程安全。

3.2AtomicInteger源碼解析

 public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    1.  在源碼中我們可以看到incrementAndGet使用了一個unsage的類,
        然後調用的是unsafe.getAndAddInt方法,接下來我們看一下這個
        方法的源碼;
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
         其實這個方法在每次執行的時候都會去判斷當前的值與底層的值是否一致,
         如果一致纔會執行加1的操作,如果不一致則重新循環取值然後接着判斷進行
         加1運算;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
        compareAndSwapInt這個方法的核心就是CAS的核心;
        return var5;
    }
    2. 在這個方法中主體是通過一個do while語句來進行實現的,核心的邏輯  
       是compareAndSwapInt這個方法,接下來我們看一下這個方法的源碼;
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
    3. 可以看到這是一個被native修飾的方法;

類似的,我們可以去理解AtomicLong以及LongAddr

//AtomicLong
@Slf4j
@ThreadSafe
public class AtomicExample2 {

    // 請求總數
    public static int clientTotal = 5000;

    // 同時併發執行的線程數
    public static int threadTotal = 200;

    public static AtomicLong count = new AtomicLong(0);

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count.get());
    }

    private static void add() {
        count.incrementAndGet();
        // count.getAndIncrement();
    }
}
//LongAddr
@Slf4j
@ThreadSafe
public class AtomicExample3 {

    // 請求總數
    public static int clientTotal = 5000;

    // 同時併發執行的線程數
    public static int threadTotal = 200;

    public static LongAdder count = new LongAdder();

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count);
    }

    private static void add() {
        count.increment();
    }
}

其實二者差別不大,LongAddr是jdk1.8新增加的,這裏簡單比較一下

AtomicInteger中,底層其實就是一個死循環內進行循環的比較和運算的不斷嘗試修改目標值,如果競爭不激烈一般都是能成功的,但是如果競爭激烈的情況下,就會容易修改失敗,從而浪費資源降低性能。

對於64位的(Long或double)操作JVM允許拆分成兩個32位的操作,LongAdder的思想是將value拆分成數組,然後進行運算,通過這樣的操作可以很大程度上提升性能。

總結:Atomic包中的大部分類原理都差不多,核心就是藉助CAS的思想來保證原子性。

3.3 鎖

Java中常用的鎖以兩種形式體現,一種是用synchronized修飾,另外一種就是Lock。

首先,synchronized是一個關鍵字,它是同步鎖,它修飾的對象有四種。

① 修飾代碼塊:大括號括起來的代碼,作用於調用對象;

② 修飾方法: 整個方法,作用於調用對象;

③ 修飾靜態方法: 整個靜態方法,作用於所有對象;

④ 修飾類: 括號括起來的部分,作用於所有對象;


@Slf4j
public class SynchronizedExample1 {

    // 修飾一個代碼塊
    public void test1(int j) {
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                log.info("test1 {} - {}", j, i);
            }
        }
    }

    // 修飾一個方法(synchronized修飾的方法不能被繼承)
    public synchronized void test2(int j) {
        for (int i = 0; i < 10; i++) {
            log.info("test2 {} - {}", j, i);
        }
    }

    public static void main(String[] args) {
        SynchronizedExample1 example1 = new SynchronizedExample1();
        SynchronizedExample1 example2 = new SynchronizedExample1();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> {
            example1.test2(1);
        });
        executorService.execute(() -> {
            example2.test2(2);
        });
    }
}

 

@Slf4j
public class SynchronizedExample2 {

    // 修飾一個類
    public static void test1(int j) {
        synchronized (SynchronizedExample2.class) {
            for (int i = 0; i < 10; i++) {
                log.info("test1 {} - {}", j, i);
            }
        }
    }

    // 修飾一個靜態方法
    public static synchronized void test2(int j) {
        for (int i = 0; i < 10; i++) {
            log.info("test2 {} - {}", j, i);
        }
    }

    public static void main(String[] args) {
        SynchronizedExample2 example1 = new SynchronizedExample2();
        SynchronizedExample2 example2 = new SynchronizedExample2();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> {
            example1.test1(1);
        });
        executorService.execute(() -> {
            example2.test1(2);
        });
    }
}

 如果使用synchronized進行修飾的類或者方法,那麼當前列或者方法只能有一個線程去執行,會導致其他的線程阻塞,synchronized是不可中斷的鎖,適合競爭不激烈的情況使用,可讀性較好。

Java中的鎖簡單說一下。首先是J.U.C中的鎖,J.U.C的核心的鎖就是ReentrantLock,ReentrantLock既屬於鎖,它的核心也是Lock與unlock。其次就是上面的synchronized。

3.4 對比

synchronized:不可中斷的鎖,適合競爭不激烈的情況,可讀性較好

lock:可中斷的鎖,多樣化同步,競爭激烈時能維持常態。

Atomic:競爭激烈時能維持常態,比Lock性能好,只能同步一個值。

ReentrantLock與synchronized的區別

  1. 可重入性:ReentrantLock從字面上理解就是可重入鎖,其實synchronized關鍵字修飾的鎖也是可重入的,在這個點上,區別不大,他們都是線程進入一次,鎖的計數器就自增1,所以需要等待鎖的計數器爲0時,才能釋放鎖。
  2. 鎖的實現:synchronized關鍵字是依賴JVM實現的。而ReentrantLock是JDK實現的。他們之間的本質區別類似於操作系統實現與自己寫代碼實現。
  3. 性能的區別:在synchronized關鍵字優化之前,它的性能比ReentrantLock差了很多,但自從synchronized引入了偏向鎖,輕量級鎖之後,他們的性能就差不多了。而synchronized更加簡潔了。
  4. 功能的區別:第一個方面,synchronized的適用,方便簡潔,並且由編譯器來保證加鎖與釋放。ReentrantLock需要手工加鎖和釋放,爲了避免忘記,需要在finally中添加釋放的操作,第二個方面是鎖的細粒度與靈活度很明顯,ReentrantLock會優於synchronized。

並不是說哪個鎖比哪個鎖一定好。根據不同的場景選擇合適的鎖來完成相應的工作纔是最好的。

4.可見性

可見性:是一個線程對主內存的修改可以及時的被其他線程觀察到。

說起可見性我們就需要說一下什麼時候不可見,下面我們就簡單說一下導致不可見的常見原因:

① 線程交叉執行;

② 重排序結合線程交叉執行;

③ 共享變量更新後的值沒有在工作內存與主內存間及時更新;

 

可見性:synchronized實現

JMM關於synchronized的兩條規定:

  1. 線程解鎖前,必須把共享變量的最新值刷新到主內存;
  2. 線程加鎖時,將清空工作內存中共享變量的值,從而使用共享變量時需要從主內存中重新讀取最新的值(注意:加鎖與解鎖是同一把鎖);

可見性: volatile實現

通過加入內存屏障和禁止重排序優化來實現;

  1. 對volatile變量寫操作時,會在寫操作後加入一條store屏障指令,將本地內存中的共享變量值刷新到主內存;
  2. 對volatile變量讀操作時,會在讀操作前加入一條load屏障指令,從主內存中讀取共享變量;
  3. 使用volatile關鍵字優化計數器

@Slf4j
@NotThreadSafe
public class CountExample4 {

    // 請求總數
    public static int clientTotal = 5000;

    // 同時併發執行的線程數
    public static int threadTotal = 200;

    public static volatile int count = 0;

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count);
    }

    private static void add() {
        count++;
        // 1、count
        // 2、+1
        // 3、count
    }
}

 

當我們使用volatile優化代碼後,這個類仍然不是線程安全的,這是爲什麼呢?我們來分析一下,當執行count++操作的時候,第一步先從主存中獲取最新的值,第二步執行+1操作,第三步將新值從新寫入主存;那麼此時問題就來了,如果同時有兩個線程執行,那麼他們獲取的都是最新的值,但是當他們執行+1以後的寫入時,寫入的值是相同的,就會丟失一次操作;

這也從另一個方面證明了volatile不具備原子性。

5.有序性

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

我們可以通過volatile synchronized lock來保證線程的有序性;首先來看一個happens-before原則

happens-before原則:

① 程序次序規則 : 一個線程內,按照代碼順序,書寫在前面的操作先行發生於書寫在後面的操作;

② 鎖定規則 : 一個unlock操作先行發生於後面對同一個鎖的lock操作;

③ volatile變量規則 : 對一個變量的寫操作先行發生於後面對這個變量的讀操作;

④ 傳遞規則 : 如果操作A先行發生於操作B,操作B先行發生於操作C,那麼可以得出操作A先行發生於操作C;

⑤ 線程啓動規則 : Thread對象的start()方法先行發生於此線程的每一個動作;

⑥ 線程中斷規則 : 對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生;

⑦ 線程終結規則 : 線程中所有的操作都先行發生於線程的終止檢測,我們可以通過Thread.join()方法結束 Thread.isAlive()的返回值手段檢測到線程已經終止執行;

⑧ 對象終結規則 : 一個對象的初始化完成先行發生於它的finalize()方法的開始;

如果線程執行的結果不能通過happens-before原則推導出來,那麼就不能保證他們的有序性,虛擬機就可以隨意對他們進行重排序。

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章