Java併發控制機制詳解

Frodo發表於2016-08-25

在一般性開發中,筆者經常看到很多同學在對待java併發開發模型中只會使用一些基礎的方法。比如Volatile,synchronized。像Lock和atomic這類高階併發包很多人並不經常使用。我想大部分原因都是來之於對原理的不屬性導致的。在繁忙的開發工作中,又有誰會很準確的把握和使用正確的併發模型呢?

所以最近基於這個思想,本人打算把併發控制機制這部分整理成一篇文章。既是對自己掌握知識的一個回憶,也是希望這篇講到的類容能幫助到大部分開發者。

並行程式開發不可避免地要涉及多執行緒、多工的協作和資料共享等問題。在JDK中,提供了多種途徑實現多執行緒間的併發控制。比如常用的:內部鎖、重入鎖、讀寫鎖和訊號量。

Java記憶體模型

在java中,每一個執行緒有一塊工作記憶體區,其中存放著被所有執行緒共享的主記憶體中的變數的值的拷貝。當執行緒執行時,它在自己的工作記憶體中操作這些變數。

為了存取一個共享的變數,一個執行緒通常先獲取鎖定並且清除它的工作記憶體區,這保證該共享變數從所有執行緒的共享記憶體區正確地裝入到執行緒的工作記憶體區,當執行緒解鎖時保證該工作記憶體區中變數的值協會到共享記憶體中。

當一個執行緒使用某一個變數時,不論程式是否正確地使用執行緒同步操作,它獲取的值一定是由它本身或者其他執行緒儲存到變數中的值。例如,如果兩個執行緒把不同的值或者物件引用儲存到同一個共享變數中,那麼該變數的值要麼是這個執行緒的,要麼是那個執行緒的,共享變數的值不會是由兩個執行緒的引用值組合而成。

一個變數時Java程式可以存取的一個地址,它不僅包括基本型別變數、引用型別變數,而且還包括陣列型別變數。儲存在主記憶體區的變數可以被所有執行緒共享,但是一個執行緒存取另一個執行緒的引數或者區域性變數時不可能的,所以開發人員不必擔心區域性變數的執行緒安全問題。

volatile變數–多執行緒間可見

由於每個執行緒都有自己的工作記憶體區,因此當一個執行緒改變自己的工作記憶體中的資料時,對其他執行緒來說,可能是不可見的。為此,可以使用volatile關鍵字破事所有執行緒軍讀寫記憶體中的變數,從而使得volatile變數在多執行緒間可見。

宣告為volatile的變數可以做到如下保證:

1、其他執行緒對變數的修改,可以及時反應在當前執行緒中;
2、確保當前執行緒對volatile變數的修改,能及時寫回到共享記憶體中,並被其他執行緒所見;
3、使用volatile宣告的變數,編譯器會保證其有序性。

同步關鍵字synchronized

同步關鍵字synchronized是Java語言中最為常用的同步方法之一。在JDK早期版本中,synchronized的效能並不是太好,值適合於鎖競爭不是特別激烈的場合。在JDK6中,synchronized和非公平鎖的差距已經縮小。更為重要的是,synchronized更為簡潔明瞭,程式碼可讀性和維護性比較好。

鎖定一個物件的方法:

public synchronized void method(){}

當method()方法被呼叫時,呼叫執行緒首先必須獲得當前物件所,若當前物件鎖被其他執行緒持有,這呼叫執行緒會等待,犯法結束後,物件鎖會被釋放,以上方法等價於下面的寫法:

public void method(){
synchronized(this){
// do something …
}
}

其次,使用synchronized還可以構造同步塊,與同步方法相比,同步塊可以更為精確控制同步程式碼範圍。一個小的同步程式碼非常有離與鎖的快進快出,從而使系統擁有更高的吞吐量。

public void method(Object o){
// before
synchronized(o){
// do something ...
}
// after
}

synchronized也可以用於static函式:

public synchronized static void method(){}

這個地方一定要注意,synchronized的鎖是加在當前Class物件上,因此,所有對該方法的呼叫,都必須獲得Class物件的鎖。

雖然synchronized可以保證物件或者程式碼段的執行緒安全,但是僅使用synchronized還是不足以控制擁有複雜邏輯的執行緒互動。為了實現多執行緒間的互動,還需要使用Object物件的wait()和notify()方法。

典型用法:

synchronized(obj){
    while(<?>){
        obj.wait();
        // 收到通知後,繼續執行。
    }
}

在使用wait()方法前,需要獲得物件鎖。在wait()方法執行時,當前執行緒或釋放obj的獨佔鎖,供其他執行緒使用。

當等待在obj上執行緒收到obj.notify()時,它就能重新獲得obj的獨佔鎖,並繼續執行。注意了,notify()方法是隨機喚起等待在當前物件的某一個執行緒。

下面是一個阻塞佇列的實現:

public class BlockQueue{
 private List list = new ArrayList();

 public synchronized Object pop() throws InterruptedException{
 while (list.size()==0){
 this.wait();
 }
 if (list.size()>0){
 return list.remove(0);
 } else{
 return null;
 }
 }

 public synchronized Object put(Object obj){
 list.add(obj);
 this.notify();
 }

}

synchronized配合wait()、notify()應該是Java開發者必須掌握的基本技能。

Reentrantlock重入鎖

Reentrantlock稱為重入鎖。它比synchronized擁有更加強大的功能,它可以中斷、可定時。在高併發的情況下,它比synchronized有明顯的效能優勢。

Reentrantlock提供了公平和非公平兩種鎖。公平鎖是對鎖的獲取是先進先出,而非公平鎖是可以插隊的。當然從效能上分析,非公平鎖的效能要好得多。因此,在無特殊需要,應該優選非公平鎖,但是synchronized提供鎖業不是絕對公平的。Reentrantlock在構造的時候可以指定鎖是否公平。

在使用重入鎖時,一定要在程式最後釋放鎖。一般釋放鎖的程式碼要寫在finally裡。否則,如果程式出現異常,Loack就永遠無法釋放了。synchronized的鎖是JVM最後自動釋放的。

經典使用方式如下:

try {
 if (lock.tryLock(5, TimeUnit.SECONDS)) { //如果已經被lock,嘗試等待5s,看是否可以獲得鎖,如果5s後仍然無法獲得鎖則返回false繼續執行
 // lock.lockInterruptibly();可以響應中斷事件
 try { 
 //操作
 } finally {
 lock.unlock();
 }
 }
} catch (InterruptedException e) {
 e.printStackTrace(); //當前執行緒被中斷時(interrupt),會拋InterruptedException 
}

Reentrantlock提供了非常豐富的鎖控制功能,靈活應用這些控制方法,可以提高應用程式的效能。不過這裡並非是極力推薦使用Reentrantlock。重入鎖算是JDK中提供的高階開發工具。

ReadWriteLock讀寫鎖

讀寫分離是一種非常常見的資料處理思想。在sql中應該算是必須用到的技術。ReadWriteLock是在JDK5中提供的讀寫分離鎖。讀寫分離鎖可以有效地幫助減少鎖競爭,以提升系統效能。讀寫分離使用場景主要是如果在系統中,讀操作次數遠遠大於寫操作。使用方式如下:

private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private Lock readLock = readWriteLock.readLock();
private Lock writeLock = readWriteLock.writeLock();
public Object handleRead() throws InterruptedException {
    try {
        readLock.lock();
        Thread.sleep(1000);
        return value;
    }finally{
        readLock.unlock();
    }
}
public Object handleRead() throws InterruptedException {
    try {
        writeLock.lock();
        Thread.sleep(1000);
        return value;
    }finally{
        writeLock.unlock();
    }
}

Condition物件

Conditiond物件用於協調多執行緒間的複雜協作。主要與鎖相關聯。通過Lock介面中的newCondition()方法可以生成一個與Lock繫結的Condition例項。Condition物件和鎖的關係就如用Object.wait()、Object.notify()兩個函式以及synchronized關鍵字一樣。

這裡可以把ArrayBlockingQueue的原始碼摘出來看一下:

public class ArrayBlockingQueue extends AbstractQueue
implements BlockingQueue, java.io.Serializable {
/** Main lock guarding all access */
final ReentrantLock lock;
/** Condition for waiting takes */
private final Condition notEmpty;
/** Condition for waiting puts */
private final Condition notFull;

public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0)
        throw new IllegalArgumentException();
    this.items = new Object[capacity];
    lock = new ReentrantLock(fair); 
    notEmpty = lock.newCondition(); // 生成與Lock繫結的Condition
    notFull =  lock.newCondition();
}

public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == items.length)
            notFull.await();
        insert(e);
    } finally {
        lock.unlock();
    }
}

private void insert(E x) {
    items[putIndex] = x;
    putIndex = inc(putIndex);
    ++count;
    notEmpty.signal(); // 通知
}

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0) // 如果佇列為空
            notEmpty.await();  // 則消費者佇列要等待一個非空的訊號
        return extract();
    } finally {
        lock.unlock();
    }
}

private E extract() {
    final Object[] items = this.items;
    E x = this.<E>cast(items[takeIndex]);
    items[takeIndex] = null;
    takeIndex = inc(takeIndex);
    --count;
    notFull.signal(); // 通知put() 執行緒佇列已有空閒空間
    return x;
}

// other code
}

Semaphore訊號量

訊號量為多執行緒協作提供了更為強大的控制方法。訊號量是對鎖的擴充套件。無論是內部鎖synchronized還是重入鎖ReentrantLock,一次都允許一個執行緒訪問一個資源,而訊號量卻可以指定多個執行緒同時訪問某一個資源。從建構函式可以看出:

public Semaphore(int permits) {}
public Semaphore(int permits, boolean fair){} // 可以指定是否公平

permits指定了訊號量的准入書,也就是同時能申請多少個許可。當每個執行緒每次只申請一個許可時,這就相當於指定了同時有多少個執行緒可以訪問某一個資源。這裡羅列一下主要方法的使用:

  •  public void acquire() throws InterruptedException {} //嘗試獲得一個准入的許可。若無法獲得,則執行緒會等待,知道有執行緒釋放一個許可或者當前執行緒被中斷。
  • public void acquireUninterruptibly(){} // 類似於acquire(),但是不會響應中斷。
  • public boolean tryAcquire(){} // 嘗試獲取,如果成功則為true,否則false。這個方法不會等待,立即返回。
  • public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException {} // 嘗試等待多長時間
  • public void release() //用於在現場訪問資源結束後,釋放一個許可,以使其他等待許可的執行緒可以進行資源訪問。

下面來看一下JDK文件中提供使用訊號量的例項。這個例項很好的解釋瞭如何通過訊號量控制資源訪問。

public class Pool {
private static final int MAX_AVAILABLE = 100;
private final Semaphore available = new Semaphore(MAX_AVAILABLE, true);
public Object getItem() throws InterruptedException {
    available.acquire();
    // 申請一個許可
    // 同時只能有100個執行緒進入取得可用項,
    // 超過100個則需要等待
    return getNextAvailableItem();
}

public void putItem(Object x) {
    // 將給定項放回池內,標記為未被使用
    if (markAsUnused(x)) {
        available.release();
        // 新增了一個可用項,釋放一個許可,請求資源的執行緒被啟用一個
    }
}

// 僅作示例參考,非真實資料
protected Object[] items = new Object[MAX_AVAILABLE]; // 用於物件池複用物件
protected boolean[] used = new boolean[MAX_AVAILABLE]; // 標記作用

protected synchronized Object getNextAvailableItem() {
    for (int i = 0; i < MAX_AVAILABLE; ++i) {
        if (!used[i]) {
            used[i] = true;
            return items[i];
        }
    }
    return null;
}

protected synchronized boolean markAsUnused(Object item) {
    for (int i = 0; i < MAX_AVAILABLE; ++i) {
        if (item == items[i]) {
            if (used[i]) {
                used[i] = false;
                return true;
            } else {
                return false;
            }
        }
    }
    return false;
}
}

此例項簡單實現了一個物件池,物件池最大容量為100。因此,當同時有100個物件請求時,物件池就會出現資源短缺,未能獲得資源的執行緒就需要等待。當某個執行緒使用物件完畢後,就需要將物件返回給物件池。此時,由於可用資源增加,因此,可以啟用一個等待該資源的執行緒。

ThreadLocal執行緒區域性變數

在剛開始接觸ThreadLocal,筆者很難理解這個執行緒區域性變數的使用場景。當現在回過頭去看,ThreadLocal是一種多執行緒間併發訪問變數的解決方案。與synchronized等加鎖的方式不同,ThreadLocal完全不提供鎖,而使用了以空間換時間的手段,為每個執行緒提供變數的獨立副本,以保障執行緒安全,因此它不是一種資料共享的解決方案。

ThreadLocal是解決執行緒安全問題一個很好的思路,ThreadLocal類中有一個Map,用於儲存每一個執行緒的變數副本,Map中元素的鍵為執行緒物件,而值對應執行緒的變數副本,由於Key值不可重複,每一個“執行緒物件”對應執行緒的“變數副本”,而到達了執行緒安全。

特別值得注意的地方,從效能上說,ThreadLocal並不具有絕對的又是,在併發量不是很高時,也行加鎖的效能會更好。但作為一套與鎖完全無關的執行緒安全解決方案,在高併發量或者所競爭激烈的場合,使用ThreadLocal可以在一定程度上減少鎖競爭。

下面是一個ThreadLocal的簡單使用:

public class TestNum {
 // 通過匿名內部類覆蓋ThreadLocal的initialValue()方法,指定初始值
 private static ThreadLocal seqNum = new ThreadLocal() {
 public Integer initialValue() {
 return 0;
 }
 };
 // 獲取下一個序列值
 public int getNextNum() {
 seqNum.set(seqNum.get() + 1);
 return seqNum.get();
}public static void main(String[] args) {
 TestNum sn = new TestNum();
 //3個執行緒共享sn,各自產生序列號
 TestClient t1 = new TestClient(sn);
 TestClient t2 = new TestClient(sn);
 TestClient t3 = new TestClient(sn);
 t1.start();
 t2.start();
 t3.start();
 }
private static class TestClient extends Thread {
 private TestNum sn;
public TestClient(TestNum sn) {
 this.sn = sn;
 }
public void run() {
 for (int i = 0; i < 3; i++) {
 // 每個執行緒打出3個序列值
 System.out.println("thread[" + Thread.currentThread().getName() + "] --> sn["
 + sn.getNextNum() + "]");
 }
 }
 }
 }

輸出結果:

thread[Thread-0] –> sn[1]
thread[Thread-1] –> sn[1]
thread[Thread-2] –> sn[1]
thread[Thread-1] –> sn[2]
thread[Thread-0] –> sn[2]
thread[Thread-1] –> sn[3]
thread[Thread-2] –> sn[2]
thread[Thread-0] –> sn[3]
thread[Thread-2] –> sn[3]

輸出的結果資訊可以發現每個執行緒所產生的序號雖然都共享同一個TestNum例項,但它們並沒有發生相互干擾的情況,而是各自產生獨立的序列號,這是因為ThreadLocal為每一個執行緒提供了單獨的副本。

鎖的效能和優化

“鎖”是最常用的同步方法之一。在平常開發中,經常能看到很多同學直接把鎖加很大一段程式碼上。還有的同學只會用一種鎖方式解決所有共享問題。顯然這樣的編碼是讓人無法接受的。特別的在高併發的環境下,激烈的鎖競爭會導致程式的效能下降德更加明顯。因此合理使用鎖對程式的效能直接相關。

1、執行緒的開銷

在多核情況下,使用多執行緒可以明顯提高系統的效能。但是在實際情況中,使用多執行緒的方式會額外增加系統的開銷。相對於單核系統任務本身的資源消耗外,多執行緒應用還需要維護額外多執行緒特有的資訊。比如,執行緒本身的後設資料,執行緒排程,執行緒上下文的切換等。

2、減小鎖持有時間 

在使用鎖進行併發控制的程式中,當鎖發生競爭時,單個執行緒對鎖的持有時間與系統效能有著直接的關係。如果執行緒持有鎖的時間很長,那麼相對地,鎖的競爭程度也就越激烈。因此,在程式開發過程中,應該儘可能地減少對某個鎖的佔有時間,以減少執行緒間互斥的可能。比如下面這一段程式碼:

public synchronized void syncMehod(){
beforeMethod();
mutexMethod();
afterMethod();
}

此例項如果只有mutexMethod()方法是有同步需要的,而在beforeMethod(),和afterMethod()並不需要做同步控制。如果beforeMethod(),和afterMethod()分別是重量級的方法,則會花費較長的CPU時間。在這個時候,如果併發量較大時,使用這種同步方案會導致等待執行緒大量增加。因為當前執行的執行緒只有在執行完所有任務後,才會釋放鎖。

下面是優化後的方案,只在必要的時候進行同步,這樣就能明顯減少執行緒持有鎖的時間,提高系統的吞吐量。程式碼如下:

public void syncMehod(){
beforeMethod();
synchronized(this){
mutexMethod();
}
afterMethod();
}

3、減少鎖粒度

減小鎖粒度也是一種削弱多執行緒鎖競爭的一種有效手段,這種技術典型的使用場景就是ConcurrentHashMap這個類。在普通的HashMap中每當對集合進行add()操作或者get()操作時,總是獲得集合物件的鎖。這種操作完全是一種同步行為,因為鎖是在整個集合物件上的,因此,在高併發時,激烈的鎖競爭會影響到系統的吞吐量。

如果看過原始碼的同學應該知道HashMap是陣列+連結串列的方式做實現的。ConcurrentHashMap在HashMap的基礎上將整個HashMap分成若干個段(Segment),每個段都是一個子HashMap。如果需要在增加一個新的表項,並不是將這個HashMap加鎖,二十搜線根據hashcode得到該表項應該被存放在哪個段中,然後對該段加鎖,並完成put()操作。這樣,在多執行緒環境中,如果多個執行緒同時進行寫入操作,只要被寫入的項不存在同一個段中,那麼執行緒間便可以做到真正的並行。具體的實現希望讀者自己花點時間讀一讀ConcurrentHashMap這個類的原始碼,這裡就不再做過多描述了。

4、鎖分離 

在前面提起過ReadWriteLock讀寫鎖,那麼讀寫分離的延伸就是鎖的分離。同樣可以在JDK中找到鎖分離的原始碼LinkedBlockingQueue。

public class LinkedBlockingQueue extends AbstractQueue
implements BlockingQueue, java.io.Serializable {
/* Lock held by take, poll, etc /
private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();

/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();

/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();

public E take() throws InterruptedException {
    E x;
    int c = -1;
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lockInterruptibly(); // 不能有兩個執行緒同時讀取資料
    try {
        while (count.get() == 0) { // 如果當前沒有可用資料,一直等待put()的通知
            notEmpty.await();
        }
        x = dequeue(); // 從頭部移除一項
        c = count.getAndDecrement(); // size減1
        if (c > 1)
            notEmpty.signal(); // 通知其他take()操作
    } finally {
        takeLock.unlock(); // 釋放鎖
    }
    if (c == capacity)
        signalNotFull(); // 通知put()操作,已有空餘空間
    return x;
}

public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    // Note: convention in all put/take/etc is to preset local var
    // holding count negative to indicate failure unless set.
    int c = -1;
    Node<E> node = new Node(e);
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    putLock.lockInterruptibly(); // 不能有兩個執行緒同時put資料
    try {
        /*
         * Note that count is used in wait guard even though it is
         * not protected by lock. This works because count can
         * only decrease at this point (all other puts are shut
         * out by lock), and we (or some other waiting put) are
         * signalled if it ever changes from capacity. Similarly
         * for all other uses of count in other wait guards.
         */
        while (count.get() == capacity) { // 佇列滿了 則等待
            notFull.await();
        }
        enqueue(node); // 加入佇列
        c = count.getAndIncrement();// size加1
        if (c + 1 < capacity)
            notFull.signal(); // 如果有足夠空間,通知其他執行緒
    } finally {
        putLock.unlock();// 釋放鎖
    }
    if (c == 0)
        signalNotEmpty();// 插入成功後,通知take()操作讀取資料
}

// other code     
}

這裡需要說明一下的就是,take()和put()函式是相互獨立的,它們之間不存在鎖競爭關係。只需要在take()和put()各自方法內部分別對takeLock和putLock發生競爭。從而,削弱了鎖競爭的可能性。

5、鎖粗化

上面說到的減小鎖時間和粒度,這樣做就是為了滿足每個執行緒持有鎖的時間儘量短。但是,在粒度上應該把握一個度,如果對用一個鎖不停地進行請求、同步和釋放,其本身也會消耗系統寶貴的資源,反而加大了系統開銷。

我們需要知道的是,虛擬機器在遇到一連串連續的對同一鎖不斷進行請求和釋放的操作時,便會把所有的鎖操作整合成對鎖的一次請求,從而減少對鎖的請求同步次數,這樣的操作叫做鎖的粗化。下面是一段整合例項演示:

public void syncMehod(){
synchronized(lock){
method1();
}
synchronized(lock){
method2();
}
}

JVM整合後的形式:

public void syncMehod(){
synchronized(lock){
method1();
method2();
}
}

因此,這樣的整合給我們開發人員對鎖粒度的把握給出了很好的演示作用。

無鎖的平行計算

上面花了很大篇幅在說鎖的事情,同時也提到過鎖是會帶來一定的上下文切換的額外資源開銷,在高併發時,”鎖“的激烈競爭可能會成為系統瓶頸。因此,這裡可以使用一種非阻塞同步方法。這種無鎖方式依然能保證資料和程式在高併發環境下保持多執行緒間的一致性。

1、非阻塞同步/無鎖
非阻塞同步方式其實在前面的ThreadLocal中已經有所體現,每個執行緒擁有各自獨立的變數副本,因此在平行計算時,無需相互等待。這裡筆者主要推薦一種更為重要的、基於比較並交換(Compare And Swap)CAS演算法的無鎖併發控制方法。

CAS演算法的過程:它包含3個引數CAS(V,E,N)。V表示要更新的變數,E表示預期值,N表示新值。僅當V值等於E值時,才會將V的值設為N,如果V值和E值不同,則說明已經有其他執行緒做了更新,則當前執行緒什麼都不做。最後CAS返回當前V的真實值。CAS操作時抱著樂觀的態度進行的,它總是認為自己可以成功完成操作。當多個執行緒同時使用CAS操作一個變數時,只有一個會勝出,併成功更新,其餘俊輝失敗。失敗的執行緒不會被掛起,僅是被告知失敗,並且允許再次嘗試,當然也允許失敗的執行緒放棄操作。基於這樣的原理,CAS操作及時沒有鎖,也可以發現其他執行緒對當前執行緒的干擾,並且進行恰當的處理。

2、原子量操作

JDK的java.util.concurrent.atomic包提供了使用無鎖演算法實現的原子操作類,程式碼內部主要使用了底層native程式碼的實現。有興趣的同學可以繼續跟蹤一下native層面的程式碼。這裡就不貼表層的程式碼實現了。

下面主要以一個例子來展示普通同步方法和無鎖同步的效能差距:

public class TestAtomic {
private static final int MAX_THREADS = 3;
private static final int TASK_COUNT = 3;
private static final int TARGET_COUNT = 100 * 10000;
private AtomicInteger acount = new AtomicInteger(0);
private int count = 0;
synchronized int inc() {
    return ++count;
}

synchronized int getCount() {
    return count;
}

public class SyncThread implements Runnable {
    String name;
    long startTime;
    TestAtomic out;

    public SyncThread(TestAtomic o, long startTime) {
        this.out = o;
        this.startTime = startTime;
    }

    @Override
    public void run() {
        int v = out.inc();
        while (v < TARGET_COUNT) {
            v = out.inc();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("SyncThread spend:" + (endTime - startTime) + "ms" + ", v=" + v);
    }
}

public class AtomicThread implements Runnable {
    String name;
    long startTime;

    public AtomicThread(long startTime) {
        this.startTime = startTime;
    }

    @Override
    public void run() {
        int v = acount.incrementAndGet();
        while (v < TARGET_COUNT) {
            v = acount.incrementAndGet();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("AtomicThread spend:" + (endTime - startTime) + "ms" + ", v=" + v);
    }
}

@Test
public void testSync() throws InterruptedException {
    ExecutorService exe = Executors.newFixedThreadPool(MAX_THREADS);
    long startTime = System.currentTimeMillis();
    SyncThread sync = new SyncThread(this, startTime);
    for (int i = 0; i < TASK_COUNT; i++) {
        exe.submit(sync);
    }
    Thread.sleep(10000);
}

@Test
public void testAtomic() throws InterruptedException {
    ExecutorService exe = Executors.newFixedThreadPool(MAX_THREADS);
    long startTime = System.currentTimeMillis();
    AtomicThread atomic = new AtomicThread(startTime);
    for (int i = 0; i < TASK_COUNT; i++) {
        exe.submit(atomic);
    }
    Thread.sleep(10000);
}
}

測試結果如下:

testSync():
SyncThread spend:201ms, v=1000002
SyncThread spend:201ms, v=1000000
SyncThread spend:201ms, v=1000001
testAtomic():
AtomicThread spend:43ms, v=1000000
AtomicThread spend:44ms, v=1000001
AtomicThread spend:46ms, v=1000002

相信這樣的測試結果將內部鎖和非阻塞同步演算法的效能差異體現的非常明顯。因此筆者更推薦直接視同atomic下的這個原子類。

結束語

終於把想表達的這些東西整理完成了,其實還有一些想CountDownLatch這樣的類沒有講到。不過上面的所講到的絕對是併發程式設計中的核心。也許有些讀者朋友能在網上看到很多這樣的知識點,但是個人還是覺得知識只有在對比的基礎上才能找到它合適的使用場景。因此,這也是筆者整理這篇文章的原因,也希望這篇文章能幫到更多的同學。

相關文章