Java併發(4)- synchronized與CAS

引言

上一篇文章中咱們說過,volatile經過lock指令保證了可見性、有序性以及「部分」原子性。但在大部分併發問題中,都須要保證操做的原子性,volatile並不具備該功能,這時就須要經過其餘手段來達到線程安全的目的,在Java編程中,咱們能夠經過鎖、synchronized關鍵字,以及CAS操做來達到線程安全的目的。java

synchronized

在Java的併發編程中,保證線程同步最爲程序員所熟悉的就是synchronized關鍵字,synchronized關鍵字最爲方便的地方是他不須要顯示的管理鎖的釋放,極大減小了編程出錯的機率。程序員

在Java1.5及之前的版本中,synchronized並非同步最好的選擇,因爲併發時頻繁的阻塞和喚醒線程,會浪費許多資源在線程狀態的切換上,致使了synchronized的併發效率在某些狀況下不如ReentrantLock。在Java1.6的版本中,對synchronized進行了許多優化,極大的提升了synchronized的性能。只要synchronized能知足使用環境,建議使用synchronized而不使用ReentrantLock。編程

synchronized的三種使用方式

  1. 修飾實例方法,爲當前實例加鎖,進入同步方法前要得到當前實例的鎖。
  2. 修飾靜態方法,爲當前類對象加鎖,進入同步方法前要得到當前類對象的鎖。
  3. 修飾代碼塊,指定加鎖對象,對給定對象加鎖,進入同步代碼塊前要得到給定對象的鎖。

這三種使用方式你們應該都很熟悉,有一個要注意的地方是對靜態方法的修飾能夠和實例方法的修飾同時使用,不會阻塞,由於一個是修飾的Class類,一個是修飾的實例對象。下面的例子能夠說明這一點:安全

public class SynchronizedTest {

    public static synchronized void StaticSyncTest() {

        for (int i = 0; i < 3; i++) {
            System.out.println("StaticSyncTest");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }

    public synchronized void NonStaticSyncTest() {

        for (int i = 0; i < 3; i++) {
            System.out.println("NonStaticSyncTest");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
}

public static void main(String[] args) throws InterruptedException {

    SynchronizedTest synchronizedTest = new SynchronizedTest();
    new Thread(new Runnable() {
        @Override
        public void run() {
            SynchronizedTest.StaticSyncTest();
        }
    }).start();
    new Thread(new Runnable() {
        @Override
        public void run() {
            synchronizedTest.NonStaticSyncTest();
        }
    }).start();
}

//StaticSyncTest
//NonStaticSyncTest
//StaticSyncTest
//NonStaticSyncTest
//StaticSyncTest
//NonStaticSyncTest

代碼中咱們開啓了兩個線程分別鎖定靜態方法和實例方法,從打印的輸出結果中咱們能夠看到,這兩個線程鎖定的是不一樣對象,能夠併發執行。併發

synchronized的底層原理

咱們看一段synchronized關鍵字通過編譯後的字節碼:ide

if (null == instance) {   
    synchronized (DoubleCheck.class) {
        if (null == instance) {   
            instance = new DoubleCheck();   
        }
    }
}



能夠看到synchronized關鍵字在同步代碼塊先後加入了monitorenter和monitorexit這兩個指令。monitorenter指令會獲取鎖對象,若是獲取到了鎖對象,就將鎖計數器加1,未獲取到則會阻塞當前線程。monitorexit指令會釋放鎖對象,同時將鎖計數器減1。性能

JDK1.6對synchronized的優化

JDK1.6對對synchronized的優化主要體如今引入了「偏向鎖」和「輕量級鎖」的概念,同時synchronized的鎖只可升級,不可降級:
優化

這裏我不打算詳細講解每種鎖的實現,想了解的能夠參照《深刻理解Java虛擬機》,只簡單說下本身的理解。this

偏向鎖的思想是指若是一個線程得到了鎖,那麼就從無鎖模式進入偏向模式,這一步是經過CAS操做來作的,進入偏向模式的線程每一次訪問這個鎖的同步代碼塊時都不須要再進行同步操做,除非有其餘線程訪問這個鎖。線程

偏向鎖提升的是那些帶同步但無競爭的代碼的性能,也就是說若是你的同步代碼塊很長時間都是同一個線程訪問,偏向鎖就會提升效率,由於他減小了重複獲取鎖和釋放鎖產生的性能消耗。若是你的同步代碼塊會頻繁的在多個線程之間訪問,可使用參數-XX:-UseBiasedLocking來禁止偏向鎖產生,避免在多個鎖狀態之間切換。

偏向鎖優化了只有一個線程進入同步代碼塊的狀況,當多個線程訪問鎖時偏向鎖就升級爲了輕量級鎖。

輕量級鎖的思想是當多個線程進入同步代碼塊後,多個線程未發生競爭時一直保持輕量級鎖,經過CAS來獲取鎖。若是發生競爭,首先會採用CAS自旋操做來獲取鎖,自旋在極短期內發生,有固定的自旋次數,一旦自旋獲取失敗,則升級爲重量級鎖。

輕量級鎖優化了多個線程進入同步代碼塊的狀況,多個線程未發生競爭時,能夠經過CAS獲取鎖,減小鎖狀態切換。當多個線程發生競爭時,不是直接阻塞線程,而是經過CAS自旋來嘗試獲取鎖,減小了阻塞線程的機率,這樣就提升了synchronized鎖的性能。

synchronized的等待喚醒機制

synchronized的等待喚醒是經過notify/notifyAll和wait三個方法來實現的,這三個方法的執行都必須在同步代碼塊或同步方法中進行,不然將會報錯。

wait方法的做用是使當前執行代碼的線程進行等待,notify/notifyAll相同,都是通知等待的代碼繼續執行,notify只通知任一個正在等待的線程,notifyAll通知全部正在等待的線程。wait方法跟sleep不同,他會釋放當前同步代碼塊的鎖,notify在通知任一等待的線程時不會釋放鎖,只有在當前同步代碼塊執行完成以後纔會釋放鎖。下面的代碼能夠說明這一點:

public static void main(String[] args) throws InterruptedException {
    waitThread();
    notifyThread();
}

private static Object lockObject = new Object();
    
private static void waitThread() {
    
    Thread watiThread = new Thread(new Runnable() {
        
        @Override
        public void run() {
            
            synchronized (lockObject) {
                System.out.println(Thread.currentThread().getName() + "wait-before");
                
                try {
                    TimeUnit.SECONDS.sleep(2);
                    lockObject.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                System.out.println(Thread.currentThread().getName() + "after-wait");
            }
            
        }
    },"waitthread");
    watiThread.start();
}

private static void notifyThread() {
    
    Thread watiThread = new Thread(new Runnable() {
        
        @Override
        public void run() {
            
            synchronized (lockObject) {
                System.out.println(Thread.currentThread().getName() + "notify-before");
                
                lockObject.notify();
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } 
                
                System.out.println(Thread.currentThread().getName() + "after-notify");
            }
            
        }
    },"notifythread");
    watiThread.start();
}

//waitthreadwait-before
//notifythreadnotify-before
//notifythreadafter-notify
//waitthreadafter-wait

代碼中notify線程通知以後wait線程並無立刻啓動,還須要notity線程執行完同步代碼塊釋放鎖以後wait線程纔開始執行。

CAS

在synchronized的優化過程當中咱們看到大量使用了CAS操做,CAS全稱Compare And Set(或Compare And Swap),CAS包含三個操做數:內存位置(V)、原值(A)、新值(B)。簡單來講CAS操做就是一個虛擬機實現的原子操做,這個原子操做的功能就是將舊值(A)替換爲新值(B),若是舊值(A)未被改變,則替換成功,若是舊值(A)已經被改變則替換失敗。

能夠經過AtomicInteger類的自增代碼來講明這個問題,當不使用同步時下面這段代碼不少時候不能獲得預期值10000,由於noncasi[0]++不是原子操做。

private static void IntegerTest() throws InterruptedException {

    final Integer[] noncasi = new Integer[]{ 0 };

    for (int i = 0; i < 10; i++) {
        Thread thread = new Thread(new Runnable() {

            @Override
            public void run() {
                for (int j = 0; j < 1000; j++) {
                    noncasi[0]++;
                }
            }
        });
        thread.start();
    }
    
    while (Thread.activeCount() > 2) {
        Thread.sleep(10);
    }
    System.out.println(noncasi[0]);
}

//7889

當使用AtomicInteger的getAndIncrement方法來實現自增以後至關於將casi.getAndIncrement()操做變成了原子操做:

private static void AtomicIntegerTest() throws InterruptedException {

    AtomicInteger casi = new AtomicInteger();
    casi.set(0);

    for (int i = 0; i < 10; i++) {
        Thread thread = new Thread(new Runnable() {

            @Override
            public void run() {
                for (int j = 0; j < 1000; j++) {
                    casi.getAndIncrement();
                }
            }
        });
        thread.start();
    }
    while (Thread.activeCount() > 2) {
        Thread.sleep(10);
    }
    System.out.println(casi.get());
}

//10000

固然也能夠經過synchronized關鍵字來達到目的,但CAS操做不須要加鎖解鎖以及切換線程狀態,效率更高。

再來看看casi.getAndIncrement()具體作了什麼,在JDK1.8以前getAndIncrement是這樣實現的(相似incrementAndGet):

private volatile int value;

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}

經過compareAndSet將變量自增,若是自增成功則完成操做,若是自增不成功,則自旋進行下一次自增,因爲value變量是volatile修飾的,經過volatile的可見性,每次get()都能獲取到最新值,這樣就保證了自增操做每次自旋必定次數以後必定會成功。

JDK1.8中則直接將getAndAddInt方法直接封裝成了原子性的操做,更加方便使用。

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

CAS操做是實現Java併發包的基石,他理解起來比較簡單但同時也很是重要。Java併發包就是在CAS操做和volatile基礎上創建的,下圖中列舉了J.U.C包中的部分類支撐圖:

相關文章
相關標籤/搜索