貓頭鷹的深夜翻譯:核心JAVA併發(二)

前言

上一篇文章請參考貓頭鷹的深夜翻譯:核心JAVA併發(一)java

安全發佈

發佈一個對象是指該對象的引用對當前的域以外也可見(好比,從getter方法中獲取一個引用)。要確保一個對象被安全的發佈(即在初始化完成以後發佈),可能須要使用同步。能夠經過如下方法實現安全的發佈:面試

  • 靜態初始化方法。只有一個線程可以初始化靜態變量由於該類的初始化是在一個排它鎖之下完成的。
class StaticInitializer {
  // Publishing an immutable object without additional initialization
  public static final Year year = Year.of(2017); 
  public static final Set<String> keywords;
  // Using static initializer to construct a complex object
  static {
    // Creating mutable set
    Set<String> keywordsSet = new HashSet<>(); 
    // Initializing state
    keywordsSet.add("java");
    keywordsSet.add("concurrency");
    // Making set unmodifiable 
    keywords = Collections.unmodifiableSet(keywordsSet); 
  }
}
  • volatile關鍵字。讀者線程老是能獲取最近的值,由於寫線程老是在後續的讀取以前進行
class Volatile {
  private volatile String state;
  void setState(String state) {
    this.state = state;
  }
  String getState() {
    return state; 
  }
}
  • Atomics。好比,AtomicInteger將一個值存儲爲volatile類型,因此這裏和volatile變量的規則相同
class Atomics {
  private final AtomicInteger state = new AtomicInteger();
  void initializeState(int state) {
    this.state.compareAndSet(0, state);
  }
  int getState() {
    return state.get();
  }
}
  • Final類型
class Final {
  private final String state;
  Final(String state) {
    this.state = state;
  }
  String getState() {
    return state;
  }
}
確保 this引用不會再初始化過程當中泄漏
class ThisEscapes {
 private final String name;
 ThisEscapes(String name) {
   Cache.putIntoCache(this);
   this.name = name;
 }
 String getName() { return name; }
}
class Cache {
 private static final Map<String, ThisEscapes> CACHE = new ConcurrentHashMap<>();
 static void putIntoCache(ThisEscapes thisEscapes) {
   // 'this' reference escaped before the object is fully constructed.
   CACHE.putIfAbsent(thisEscapes.getName(), thisEscapes);
 }
}
  • 正確同步的域
class Synchronization {
  private String state;
  synchronized String getState() {
    if (state == null)
      state = "Initial";
    return state;
  }
}

不變的對象

不變對象的一個很是棒的屬性時,他們是現成安全的,全部無需在其上進行同步。是一個對象成爲不變對象的要求爲:segmentfault

  • 全部的字段爲final類型
  • 全部字段能夠是可變對象或不可變對象,但不能越過對象的範圍,從而對象的狀態在構建後不能更改。
  • this引用在初始化期間不會泄露
  • 該類爲final類型,因此沒法在子類中修改其行爲

不變對象的例子:安全

// Marked as final - subclassing is forbidden
public final class Artist {
  // Immutable object, field is final
  private final String name; 
  // Collection of immutable objects, field is final
  private final List<Track> tracks; 
  public Artist(String name, List<Track> tracks) {
    this.name = name;
    // Defensive copy
    List<Track> copy = new ArrayList<>(tracks); 
    // Making mutable collection unmodifiable
    this.tracks = Collections.unmodifiableList(copy); 
    // 'this' is not passed to anywhere during construction
  }
  // Getters, equals, hashCode, toString
}
// Marked as final - subclassing is forbidden
public final class Track { 
  // Immutable object, field is final
  private final String title; 
  public Track(String title) {
    this.title = title;
  }
  // Getters, equals, hashCode, toString
}

Threads

java.lang.Thread類用來表示一個應用或是一個JVM現場。其代碼一般在某個進程類的上下文中執行。(使用Thread#currentThread來獲取當前線程自己)微信

線程的狀態和相應的描述:併發

NEW: 還未啓動
RUNNABLE: 啓動並運行
BLOCKED: 在控制器上等待 - 該線程正視圖獲取鎖並進入關鍵區域
WAITING: 等待另外一個線程執行特殊操做( notify/notifyAll, LockSupport#unpark)
TIMED_WAITING: 和WAITING相似,可是有超時設置
TERMINATED: 中止

Thread的方法和相應的描述:異步

start: 啓動一個 Thread實例而且執行 run()方法
join: 阻塞直到線程完成
interrupt: 中斷線程。若是該線程在響應終端的方法中阻塞着,則會在另外一個線程中拋出 InterruptedException,不然將會被設置爲中斷狀態。
stop,suspend,resume,destroy: 這些方法都已經失效了。

如何處理InterruptedException

  • 若是可能的話,清理資源並終止線程的運行
  • 聲明當前的方法會拋出InterruptedException
  • 若是一個方法並無被聲明拋出InterruptedException,應該使用Thread.currentThread().interrupt()將中斷標識回覆爲true,而後在該層拋出異常。將中斷標識設爲true很重要,它使得異常在能夠在更高的層次上進行處。

意料以外的異常處理

Threads能夠設置UncaughtExceptionHandler,它會在程序忽然中斷的時候收到通知。函數

Thread thread = new Thread(runnable);
thread.setUncaughtExceptionHandler((failedThread, exception) -> {
  logger.error("Caught unexpected exception in thread '{}'.",
      failedThread.getName(), exception);
});
thread.start();

生命力

死鎖

當多個線程在等待彼此釋放持有的資源,從而造成了資源佔有和等待的循環時,就產生了死鎖。可能產生死鎖的例子:this

class Account {
  private long amount;
  void plus(long amount) { this.amount += amount; }
  void minus(long amount) {
    if (this.amount < amount)
      throw new IllegalArgumentException();
    else
      this.amount -= amount;
  }
  static void transferWithDeadlock(long amount, Account first, Account second){
    synchronized (first) {
      synchronized (second) {
        first.minus(amount);
        second.plus(amount);
      }
    }
  }
}

死鎖可能會這樣產生:spa

  • 一個線程正視圖從第一個帳戶向第二個帳戶轉帳,而且已經得到了第一個帳戶的鎖
  • 與此同時,另外一個線程正視圖從第二個線程像第一個線程轉帳,而且已經得到了第二個帳戶的鎖

避免死鎖的方法有:

  • 順序加鎖 - 老是按相同的順序得到鎖
class Account {
  private long id;
  private long amount;
  // Some methods are omitted
  static void transferWithLockOrdering(long amount, Account first, Account second){
    boolean lockOnFirstAccountFirst = first.id < second.id;
    Account firstLock = lockOnFirstAccountFirst  ? first  : second;
    Account secondLock = lockOnFirstAccountFirst ? second : first;
    synchronized (firstLock) {
      synchronized (secondLock) {
        first.minus(amount);
        second.plus(amount);
      }
    }
  }
}
  • 會超時的鎖 - 不要無限的佔有鎖,應當釋放全部的鎖並從新嘗試獲取
class Account {
  private long amount;
  // Some methods are omitted
  static void transferWithTimeout(
      long amount, Account first, Account second, int retries, long timeoutMillis
  ) throws InterruptedException {
    for (int attempt = 0; attempt < retries; attempt++) {
      if (first.lock.tryLock(timeoutMillis, TimeUnit.MILLISECONDS))
      {
        try {
          if (second.lock.tryLock(timeoutMillis, TimeUnit.MILLISECONDS))
          {
            try {
              first.minus(amount);
              second.plus(amount);
            }
            finally {
              second.lock.unlock();
            }
          }
        }
        finally {
          first.lock.unlock();
        }
      }
    }
  }
}

活鎖和線程飢餓

當全部的線程都在協商對資源的訪問,或是預防死鎖,從而致使沒有一個線程真正在運行時,會產生活鎖。當一個線程長時間佔據一個鎖致使別的線程沒法進展時,會產生線程飢餓現象。

java.util.concurrent

線程池

線程池的核心接口是ExecutorServicejava.util.concurrent還提供了一個靜態工廠Executors,它包含建立具備最多見配置的線程池的工廠方法。

工廠方法以下:

newSingleThreadExecutor: 返回一個只有一個線程的 ExecutorService
newFixedThreadPool: 返回一個具備固定數目線程的 ExecutorService
newCachedThreadPool: 返回一個可變大小的線程池 ExecutorService
newSingleThreadScheduledExecutor: 返回只有一個線程的 ScheduledExecutorService
newScheduledThreadPool: 返回包含一組線程的 ScheduledExecutorService
newWorkStealingPool: 返回一個帶有並行級別的 ExecutorService
當調整線程池大小時,最好基於機器運行該應用時分配的邏輯內核數。能夠經過調用 Runtime.getRuntime().availableProcessors()來得到該值。

線程池的實現類

clipboard.png

任務經過ExecutorService#submitExecutorService#invokeAllExecutorService#invokeAny提交,它們對不一樣類型的任務有多種重載。

任務的功能性接口:

Runnable: 表示一個沒有返回值的任務
Callable: 表示一個包含返回值的計算。它還聲明能夠拋出原始異常,因此不須要對檢查異常進行包裝

Future

Future是對全部的異步計算的抽象。它表示這些計算的結果,在某些時候可用。大多數的ExecutorService方法都是用Future做爲返回值。它包含檢查當前future的狀態以及阻塞當前讀取操做直至結果能夠被讀取等方法。

ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<String> future = executorService.submit(() -> "result");
try {
  String result = future.get(1L, TimeUnit.SECONDS);
  System.out.println("Result is '" + result + "'.");
} 
catch (InterruptedException e) {
  Thread.currentThread().interrupt();
  throw new RuntimeException(e);
} 
catch (ExecutionException e) {
  throw new RuntimeException(e.getCause());
} 
catch (TimeoutException e) {
  throw new RuntimeException(e);
}
assert future.isDone();

Locks

Lock
java.util.concurrent.locks包中有一個標準的Lock接口,ReentrantLock實現複製了synchronized關鍵字的功能,同時提供了一些額外的功能,好比獲取當前鎖狀態的信息,非阻塞的tryBlock()方法,以及可中斷的鎖。下面是使用具體的ReentrantLock實例的例子:

class Counter {
  private final Lock lock = new ReentrantLock();
  private int value;
  int increment() {
    lock.lock();
    try {
      return ++value;
    } finally {
      lock.unlock();
    }
  }
}

ReadWriteLock
java.util.concurrent.locks包還包含了ReadWriteLock接口(以及ReentrantReadWriteLock實現),它被定義爲一組讀寫鎖,支持多個同步讀者和單一寫者。

class Statistic {
  private final ReadWriteLock lock = new ReentrantReadWriteLock();
  private int value;
  void increment() {
    lock.writeLock().lock();
    try {
      value++;
    } finally {
      lock.writeLock().unlock();
    }
  }
  int current() {
    lock.readLock().lock();
    try {
      return value;
    } finally {
      lock.readLock().unlock();
    }
  }
}

CountDownLatch
CountDownLatch經過一個數值初始化。線程會調用await()方法阻塞本身,等待計數值爲0後再繼續運行。其它的線程(或是同一個線程)調用countDown()來減小計數。一旦計數爲0後,該倒計時器便不能夠重複使用。用來在達到某個條件後,啓動一組未知數量的線程

CompletableFuture
CompletableFuture是異步計算的一個抽象。不一樣於Future,只能經過阻塞獲取結果,該類鼓勵註冊回調函數來建立一組任務,從而在獲得返回值或是出現異常時執行該任務。

clipboard.png
想要了解更多開發技術,面試教程以及互聯網公司內推,歡迎關注個人微信公衆號!將會不按期的發放福利哦~

相關文章
相關標籤/搜索