如何在線程之間共享資源?

介紹

涉及IO讀/寫時,多線程能夠提升應用程序的性能。不幸的是,共享資源(共享變量)在每一個CPU緩存中可能有不一樣的版本。結果是應用程序的行爲不可預測。Java提供了synchronized關鍵字來保持CPU緩存中共享資源的一致性。不幸的是,synchronized關鍵詞減慢了應用程序的速度。html

我使用JMH做爲微型基準測試AverageTime模式,這意味着基準測試結果是每一個測試用例的平均運行時間,較低的輸出效果更好。您能夠在此連接找到更多關於微基準的信息。java

爲何同步減速應用程序?

當線程被鎖定並開始執行同步塊中的指令時,全部其餘線程將被阻塞並變爲空閒狀態。這些線程的執行上下文(CPU緩存,指令集,堆棧指針...)將被存儲,其餘活動線程的執行上下文將被恢復以恢復計算。它被稱爲上下文切換,須要系統的大量工做。任務計劃程序也必須運行以選擇將加載哪一個線程。緩存

易變關鍵字

volatile關鍵字只是作了一些事情:告訴CPU從主內存中讀取資源的值,而不是從CPU的緩存中讀取資源的值; 在每次後續讀取該字段以前,都會發生對易失性字段的寫入。 易失性永遠不會有比同步更高volatile的開銷,synchronized若是synchronized塊只有一個操做,它將具備相同的開銷。安全

volatile若是隻有一個寫入線程,關鍵字能夠很好地工做。若是有2個或更多的寫入線程,就會出現競爭條件:全部寫入線程獲取最新版本的變量,在本身的CPU上修改值,而後寫入主內存。結果是內存中的數據只是一個線程的輸出,其餘線程的修改被覆蓋。數據結構

包java.util.concurrent

Doug Lea在建立和改進這個軟件包時作了很棒的工做。這個包有不少用於管理線程的工具,還包含一些線程安全的數據結構。那些數據結構也可使用synchronizedvolatile可是以一種複雜的方式,你能夠從編寫你本身的代碼中得到更好的性能。多線程

ConcurrentHashMap「遵循與」相同的功能規範Hashtable「,併爲您提供線程安全的優點。oracle

 

public class TestHashMap {

  @Benchmark
  @BenchmarkMode(Mode.AverageTime)
  @OutputTimeUnit(TimeUnit.NANOSECONDS)
  @Threads(10)
  public void concurrentHashMap(BenchMarkState state){
      Integer temp;
      for(int i = 0; i < 100000; i++){
          temp = Integer.valueOf(i);
          state.chm.put(temp,temp);
      }
  }

  @Benchmark
  @BenchmarkMode(Mode.AverageTime)
  @OutputTimeUnit(TimeUnit.NANOSECONDS)
  @Threads(10)
  public void hashMap(BenchMarkState state){
      Integer temp;
      for(int i = 0; i < 100000; i++){
          temp = Integer.valueOf(i);
          synchronized (state.LOCK_1) {
              state.hm.put(temp,temp);
          }            
      }
  }

  @State(Scope.Benchmark)
  public static class BenchMarkState {
      @Setup(Level.Trial)
      public void doSetup() {
          hm = new HashMap<>(100000);
          chm = new ConcurrentHashMap<>(100000);
      }
      @TearDown(Level.Trial)
      public void doTearDown() {
          hm = new HashMap<>(100000);
          chm = new ConcurrentHashMap<>(100000);
      }
      public HashMap<Integer, Integer> hm = new HashMap<>(100000);
      public ConcurrentHashMap<Integer, Integer> chm = new ConcurrentHashMap<>(100000);
      public final Object LOCK_1 = new Object();
  }

隱藏   複製代碼分佈式

Benchmark                      Mode  Cnt         Score        Error  Units
TestHashMap.concurrentHashMap  avgt  200  10740649.930 ± 351589.110  ns/op
TestHashMap.hashMap            avgt  200  60661584.668 ± 758157.651  ns/op

AtomicInteger和其餘相似的類使用volatileUnsafe.compareAndSwapIntAtomicInteger能夠稱爲忙等待,這意味着一個線程老是檢查條件執行。這個線程什麼都不作,可是任務調度程序不能檢測到這個檢查而且認爲這個線程很忙,因此任務調度程序不能把CPU拿到另外一個準備執行的線程。若是在幾個CPU時鐘以後條件能夠歸檔,則忙等待效果很好。微服務

 

public class TestCAS {

  @Benchmark
  @BenchmarkMode(Mode.AverageTime)
  @OutputTimeUnit(TimeUnit.NANOSECONDS)
  @Threads(10)
  public void atomic(BenchMarkState state){
    while(state.atomic.get() < 100000)
      while(true){
        int temp = state.atomic.get();
        if(temp >= 100000 || state.atomic.compareAndSet(temp, temp + 1))
          break;
      }
  }

  @Benchmark
  @BenchmarkMode(Mode.AverageTime)
  @OutputTimeUnit(TimeUnit.NANOSECONDS)
  @Threads(10)
  public void integer(BenchMarkState state){
    while(state.integer < 100000){
      synchronized (state.LOCK) {
        if(state.integer < 100000)
          state.integer += 1;
      }
    }
  }

  @State(Scope.Benchmark)
  public static class BenchMarkState {
    @Setup(Level.Trial)
    public void doSetup() {
      atomic.set(0);
      integer = 0;
    }

    public Object LOCK = new Object();
    public AtomicInteger atomic = new AtomicInteger(0);
    public Integer integer = new Integer(0);
  }

 

Benchmark        Mode  Cnt   Score   Error  Units
TestCAS.atomic   avgt  200  10.053 ± 0.985  ns/op
TestCAS.integer  avgt  200  12.666 ± 1.145  ns/op

Lock具備更多靈活的功能synchronized,您可使用tryLock()特定的時間等待或確保最長的等待線程得到公平選項的鎖定。但synchronized關鍵字能夠保證執行順序和數據新鮮度,源代碼synchronized也很簡單。Lock工具

@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Threads(10)
public void lock(BenchMarkState state){
  while(state.intLock < 100000){
      state.lock.lock();
      if(state.intLock < 100000)
          state.intLock++;
      state.lock.unlock();
  }
}
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Threads(10)
public void synchonized(BenchMarkState state){
  while(state.intSync < 100000){
    synchronized (state.LOCK) {
      if(state.intSync < 100000)
        state.intSync += 1;
    }
  }
}

 

Benchmark             Mode  Cnt  Score   Error  Units
TestLock.lock         avgt  200  1.960 ± 0.074  ns/op
TestLock.synchonized  avgt  200  2.394 ± 0.047  ns/op

不變的對象

這個想法很簡單,若是一個對象從不改變值,它是線程安全的。可是有一個問題,每次你想改變一些值時你必須建立一個新的對象,所以GC過熱。

結論

synchronized關鍵字之間共享線程之間的資源很容易,但它可能會致使世界各地的等待和放慢您的應用程序。其餘簡單的技術也能夠歸檔線程安全,但速度比synchronized

​針對上面的技術我特地整理了一下,有不少技術不是靠幾句話能講清楚,因此乾脆找朋友錄製了一些視頻,不少問題其實答案很簡單,可是背後的思考和邏輯不簡單,要作到知其然還要知其因此然。若是想學習Java工程化、高性能及分佈式、深刻淺出。微服務、Spring,MyBatis,Netty源碼分析的朋友能夠加個人Java進階羣:744642380,羣裏有阿里大牛直播講解技術,以及Java大型互聯網技術的視頻免費分享給你們。

相關文章
相關標籤/搜索