《JAVA併發編程實戰》——線程安全

引論

想要學習併發編程,線程安全必須是要知道的,先給出一個線程不安全的例子吧:

package com.mmall.concurrency;

import com.mmall.concurrency.annoations.NotThreadSafe;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

@NotThreadSafe
public class ConcurrencyTest {
    // 請求總數
    public static int clientTotal = 5000;
    // 同時併發執行的線程數
    public static int threadTotal = 200;
    public static 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) {
                    e.printStackTrace();
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        System.out.println("count:"+count);

    }
    private static void add() {
        count++;
    }
}

這個代碼的意思很簡單,就是先定義5000個請求,200個線程同時執行。請求的操作是對count++。所以預期的結果應該是5000個請求,count++ 5000次,所以輸出結果應該爲5000.在main方法中寫的是創建線程池-連接線程池-操作-釋放線程。

但是實際運行結果每次都不一樣,每次的結果都小於5000,這個說明這個類是線程不安全的。
運行結果

//第一次
count:4996
//第一次
count:4998
//第三次
count:4991

定義

那什麼事線程安全呢?

書中的定義:

當多個線程訪問一個類時,如果不考慮這些線程在運行時環境下的調度和交替執行,並且不需要額外的同步以及在調用方代碼不必作其他協調,這個類的行爲依然正確,那麼就稱這個類是線程安全的

視頻給出的定義:

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

其實仔細一看,兩個定義差不多,所謂的線程安全,都是對類而言的,如果多個線程同時訪問這個類,這個類響應的結果都和預期的一樣,那麼說明這個線程是安全。

線程的安全性體現在三個方面:原子性、可見性、有序性

  • 原子性:提供了互斥訪問,同一時刻只能有一個線程來對它進行操作。
  • 可見性:一個線程對主內存的修改可以及時的被其他線程看到
  • 有序性:一個線程觀察其他線程中的指令執行順序,由於指令重排序的存在,該觀察結果一般雜亂無序。

原子性

JDK中atomic包
原子性是怎麼實現的呢,在jdk中有一個Atomic包是保存可以確保線程的安全性
看下面的例子:

package com.mmall.concurrency.example.count;

import com.mmall.concurrency.annoations.ThreadSafe;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
@ThreadSafe
public class CountExample2 {

    // 請求總數
    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();
        System.out.println(count.get());
    }

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

其實可以發現和文中最開始給出的例子想要做的事是一樣的,只不過這裏的count是AtomicInteger 類型的,也就是atomic包下的封裝類。

public static AtomicInteger count = new AtomicInteger(0);

然後在count++ 操作時變成

count.incrementAndGet();

這個很有意思,這個纔是確保這個類是線程安全的關鍵,可以看一下源碼

public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

可以看到,調用了unsafe.getAndAddInt(this, valueOffset, 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;
    }

可以看到getAndAddInt方法的具體實現,var1爲當前的對象,var2是當前count的值,var4是要加值,這裏每次都是加1,所以var4的值爲1.
var5的值是取主內存中當前count值,compareAndSwapInt()方法的關鍵是,如果從主內存中取出的當前count值var5與傳入的當前count值var2不一致,那麼就會重新取主內存count的值var5,只有當主內存的count值和當前count值相同時,纔會將執行var5=varchat+var4操作。最後返回var5的值,這樣就確保了每次在執行加一操作的時候得到的當前的count值都是最新的。

可以看到真正的核心就是compareAndSwapInt()方法啦,這就是是automic確保線程安全的CAS方法的具體實現。

另外再拓展一點:automic包下的AtomicLong類。看類名可以知道和AtomicInteger類一樣,只是不同數據對應不同數據類型。但是在java8中新增一種類LongAdder.這個類和AtomicLong實現的功能是一樣的,那爲什麼要在java8中新增一個LongAdder來替代AtomicLong呢。

剛剛上面講了CAS的具體實現是通過循環一直判斷,只有當值一樣時候纔會執行後續操作,低併發的時候還好,但是高併發的時候就會導致循環的次數增多,總而導致性能降低,使用LongAdder類,在低併發的時候和AtomicLong保持一直,但是在高併發的時候通過分散將壓力分散到各個節點,總而提高高併發時的性能(具體的源碼我也沒有看,所以也是聽被人說的這個,有興趣的同學可以自己研究下)。但是LongAdder有個致命的缺點,就是雖然提高了性能,但是有的時候結果會出現偏差(通過離散模型統計的,在統計的時候,如果有併發更新,就可能會出現誤差),導致如果需要結果是準確無誤且唯一的時候最好使用AtomicLong。

在提一下atomic包下atomicStampReference類,這個類是解決CAS的ABA問題(是指一個線程操作某個值的時候,其他線程先修改了一次,然後又修改成原來的值。這樣當前線程就會認爲這個沒有任何操作),但是實際上這個值是進行了2次操作的,值是把值改回去了,那怎麼解決ABA問題呢,atomicStampReference類在線程操作某個值的時候,不僅會判斷值,還會判斷當前的版本號是否一致。一致才能進行下一步操作。線程在進行寫或修改操作時會進行版本號加一。這樣就能規避掉ABA問題了。

在看一下AtomicBoolean類。這個多線程調用類類讓方法執行一次,也就是說比如5000個線程同時訪問,但是隻會有一個線程執行這個方法,其他的線程都不執行。
看一下代碼:

package com.mmall.concurrency.example.atomic;

import com.mmall.concurrency.annoations.ThreadSafe;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;
@ThreadSafe
public class AtomicExample6 {
    private static AtomicBoolean isHappened = new AtomicBoolean(false);
    // 請求總數
    public static int clientTotal = 5000;
    // 同時併發執行的線程數
    public static int threadTotal = 200;
    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();
                    test();
                    semaphore.release();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
       System.out.println(isHappened.get());
    }
    private static void test() {
        if (isHappened.compareAndSet(false, true)) {
           System.out.println("execute");
        }
    }
}

結果:表明test()方法只執行了一次的。

execute
true

Synchronized
synchronized是java關鍵字,相信大家都不會陌生,synchronized也可以確保線程的原子性。synchronized修飾的對象有四種:代碼塊、方法、靜態方法、類。

  • 修飾代碼塊:大掛號掛起來的代碼,作用於調用的對象
  • 修飾方法:作用於調用的對象
  • 修飾靜態方法:作用於所有對象
  • 修飾類:掛號掛起來的部分,所用於所有對象
    還是最開始給出的線程不安全的代碼,給add()加上synchronized關鍵字就可以保證線程安全了。
private synchronized static void add() {
        count++;
    }

在這裏插入圖片描述

可見性

導致共享變量在線程間不可見的原因:

  • 線程交叉執行
  • 重排序結合線程交叉執行
  • 共享變量更新後的值沒有在工作內存與主內存及時更新
  • 在這裏插入圖片描述
  • 在這裏插入圖片描述
    還是最開始的代碼,將 public static int count = 0;用volalite關鍵字修飾
public static int volalite count = 0;

得到結果並不是我們想要的5000,而是比5000小,count儘管用volalite關鍵字修飾了,但是還是線程不安全的,因爲count++,實際上是三步操作,取當前count值,當前count值加1,將當前count值寫入內存,volalite關鍵字僅僅保障了第一步取當前count值是同步的,但是後面兩部操作導致了線程不安全。

有序性

java內存模型中,允許編譯器和處理器對指令重排序,但是重排序的過程不會影響到單線程的執行,但是會影響多線程併發執行的正確性。
java中通過volalite、synchronized、Lock可以保證線程的有序性。

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