多線程進階與源碼分析--synchronized與volatile實踐場景(二)

上篇文章主要講了怎麼創建合理的線程池的方式、線程同步,外加部分問題定位的方法,對於簡單的業務開發是可以勝任,這僅僅是入門。

       多線程帶來的問題是順序性與競爭問題,這個問題的產生於多個線程同時訪問一個或多個共享變量引起的,而現代多核處理器在設計上增加了許多一二三級緩存,每個CPU有自己的緩存,在執行之前會先拿到要操作值的副本,拷貝到高速緩存區,然後處理器直接從高速緩存區獲取信息進行計算,如果這個過程中處理器A與處理器B都從主內存中緩存一個數值i=1,然後倆處理器都對其進行+1操作,這個時候會出現雖然做了兩次加1,但結果卻是2的問題,synchronized能解決這個問題,synchronized可以修飾方法、靜態方法、代碼塊,本質上就是通過加鎖與解鎖的方式保證當前代碼塊內只有一個線程在執行,我們知道synchronized有性能問題,顯而易見,一個方法或代碼塊只有一個線程能跑確實存在一定的性能問題,這個問題在jdk1.6之後又做了些優化,加了偏向鎖、輕量級鎖、重量級鎖等概念,隨着線程競爭的規模而從左向右進行膨脹,但是還是存在性能問題,原因是java中的鎖都是對象鎖,什麼意思?首先大家知道java中基礎單位是對象,我們都知道Java中所有的對象都有一個基類是Object,它有它最通用的數據結構,我們來看下Java對象頭的數據結構:

20151217151455512

       ok,存儲結構裏面貌似好多跟鎖相關的,那我們可以推測出,java中所有的加鎖、競爭鎖的操作實際都是競爭的同一個對象的鎖,然後一個對象可能有多個方法或多個同步代碼塊,而且很有可能存在這些方法並不是相關的,方法A與方法B,方法A佔用了鎖,我方法B就得阻塞,等待獲取鎖,嚴重的性能問題,我們通過下面的代碼來做驗證(當然實際上是每個對象都有個monitor監視器,其實就是鎖,本例是靜態方法,靜態方法鎖的是Class類本身,如果是對象的話,鎖的是每個new出來的對象):

/**
 * 驗證同步靜態方法 是否鎖對象
 * @author zhengxun
 * @version 2017-11-12
 */
public class StaticSynchronized {

    public synchronized static void test1() throws InterruptedException {
        System.out.println(new Date() + "synchronized static test1");
        Thread.sleep(10000);
    }
    public synchronized static void test2() throws InterruptedException {
        System.out.println(new Date() +"synchronized static test2");
     Thread.sleep(10000); 
    }
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    StaticSynchronized.test1();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    StaticSynchronized.test2();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

這是運行結果:

	Tue Nov 14 00:00:07 CST 2017synchronized static test1
	Tue Nov 14 00:00:17 CST 2017synchronized static test2

        如猜想,因此一個對象中慎用多個同步代碼塊。

        若使用sychronized關鍵詞:

1、儘量減少佔用鎖的時間,比如代碼塊的代碼內容儘量少,一段代碼中如果可以就用多個代碼塊來代替整個代碼塊;

2、儘量減少阻塞的放生,從設計上將synchronized分佈到不同的對象中,減少一個對象中的synchronized整體數量;

      儘可能的多釋放鎖,比如使用wait()方法+循環重試,允許其他線程獲取鎖。

這兩段代碼摘自Netty,第一段代碼只在需要做鎖的地方加同步代碼塊,第二段代碼即使用wait()方法的方式,當然爲了避免過多的佔用cpu資源我們在重試的時候可以加一個上限,一旦超過上線則直接拋異常,如第三段代碼。

image

image

 

synchronized (this) {
            while (!isDone()) {
                incWaiters();
                try {
                    wait();
                } finally {
                    decWaiters();
                }
            }
        }
private void incWaiters() {
    if (waiters == Short.MAX_VALUE) {//超過32767則拋出異常
        throw new IllegalStateException("too many waiters: " + this);
    }
    ++waiters;
    }

        前面提到偏向鎖、輕量級鎖、重量級鎖,這是synchronized的內部優化,即如何降低資源消耗一些策略,我們來講下什麼情況下對應什麼樣的鎖。

偏向鎖是第一個階段,也是效率最高的,條件:不存在線程競爭的場景,意思是雖然你加了synchronized關鍵詞,但是通常都只有一個線程會訪問,ok,那這個之後資源消耗最低,僅僅通過判斷對象頭的偏向鎖是否指向當前線程,如果是,那就獲取到鎖。如果存在線程競爭,會帶來鎖撤銷的消耗(偏向鎖默認開啓,可通過jvm參數關閉)

輕量級鎖是第二個階段,效率次等,條件:多個線程交替進入synchronized代碼塊,意思是偏向鎖裏面指向的不是當前線程,那麼就會升級到輕量級鎖。輕量級鎖是在沒有線程競爭鎖的情況下,如果存在一個線程已經獲取到鎖,那麼第二個線程會使用CAS自旋嘗試獲取鎖,會帶來一定的cpu開銷。

重量級鎖是最後一個階段,效率最差,條件是:多個線程同時進入synchronized代碼塊,就是線程存在競爭鎖的場景,這時沒有獲取到鎖的線程只能阻塞,當持有鎖的線程釋放鎖之後會喚醒阻塞的線程,進行來一輪的鎖競爭,如果線程過多的話,響應時效無法保證。

需要強調的是偏向鎖與輕量級鎖都不是對象鎖,雖然做了不少優化,但是多線程競爭條件下還是會很慢,我們大致猜想一種場景,線程之間鎖競爭比較少或線程數量比較少,可適當使用synchronized,另外如果要用,遵循以上提到的兩點,當然新手如果用到鎖還是直接用synchronized,等出現性能問題再選用其他的鎖。

volatile又是什麼場景用呢?

        第一volatile只保證可見性、順序性(禁止CPU重排序優化),不保證原子性,像前面提到的多個線程操作同一個變量,就算加上voatile也不起作用。如下代碼(驗證volatile的可見性與非原子性):

/**
 * 驗證volatile的可見性與非原子性
 *
 * @author zhengxun
 * @version 2017-11-14
 */
public class VisibilityForVolatile {
    private static volatile int non_atomic = 0;
    public static void main(String[] args) {
        try {
            final CountDownLatch countDownLatchForAtomicity = new CountDownLatch(1);
            for (int i = 0; i < 10; i++) {//驗證非原子性
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        for (int j = 0; j < 10000; j++) {
                            non_atomic++;
                        }
                        try {
                            countDownLatchForAtomicity.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }).start();
            }
            final CountDownLatch countDownLatchForVisibility = new CountDownLatch(1);
            final Visibility visibilityThread = new Visibility(countDownLatchForVisibility);
            new Thread(visibilityThread).start();
            countDownLatchForVisibility.await();
            System.out.println("驗證可見性:" + visibilityThread.getVisibility());
            countDownLatchForAtomicity.countDown();
            Thread.sleep(1000);
            System.out.println("驗證非原子性:" + non_atomic);
//            Thread.sleep(Integer.MAX_VALUE);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

class Visibility implements Runnable {
    private CountDownLatch countDownLatchForVisibility;
    public Visibility(CountDownLatch countDownLatchForVisibility) {
        this.countDownLatchForVisibility = countDownLatchForVisibility;
    }
    private Boolean visibility = true;
//    private volatile Boolean visibility = true;
    @Override
    public void run() {
        visibility = false;
        countDownLatchForVisibility.countDown();
    }
    public Boolean getVisibility() {
        return visibility;
    }
    public void setVisibility(Boolean visibility) {
        this.visibility = visibility;
    }
}

輸出結果:

        程序本應輸出10000,結果輸出92453,證明其是非原子性的,

        可見性,方面子線程已修改爲false,有時輸出卻還是true,而加上volatile卻絕對不會出現這種情況(實際上很多次可見性不好驗證,大部分情況下不加volatile也能具有可見性,後續再找機會驗證這個問題)

驗證可見性:true
   驗證非原子性:92453

 

        根據其特性,總結,volatile最適合的場景是一個線程寫,其他線程讀的場景,如果有多個線程寫共享變量,還是直接用鎖來的合適。

        還有中奇妙的思路,volatile既然不支持原子性,那麼jdk中有其他的類支持,比如AtomicLongFieldUpdater原子操作類,volatile修飾的屬性如果修改對其他線程是可見的,AtomicLongFieldUpdater又能保證原子性,但是這種也存在一定的性能問題,這些原子操作類都使用的CAS來保證原子操作的,如果存在大量線程,CPU自旋的時間就會過長,同樣會影響性能,所以這種最好能在有限的線程池中操作。

/**
 * A reflection-based utility that enables atomic updates to
 * designated {@code volatile long} fields of designated classes.
 * This class is designed for use in atomic data structures in which
 * several fields of the same node are independently subject to atomic
 * updates.
 *
 * <p>Note that the guarantees of the {@code compareAndSet}
 * method in this class are weaker than in other atomic classes.
 * Because this class cannot ensure that all uses of the field
 * are appropriate for purposes of atomic access, it can
 * guarantee atomicity only with respect to other invocations of
 * {@code compareAndSet} and {@code set} on the same updater.
 *
 * @since 1.5
 * @author Doug Lea
 * @param <T> The type of the object holding the updatable field
 */
public abstract class AtomicLongFieldUpdater<T> {

        AtomicLongFieldUpdater這個原子操作類我們需要學習,源碼也寫的很清楚了,是專門爲volatile修飾的屬性設計的,內部使用CAS自旋保證修改正確,其底層也使用了unsafe的compareAndSet()

        如下代碼,volatile不加鎖形式的原子性實現:主要利用AtomicLongFieldUpdater的特性實現。

/**
 * volatile 修飾詞,原子性代碼實現
 * @author zhengxun
 * @version 2017-11-14
 */
public class VolatileAndCAS {

    private volatile long atomic = 0L;
    private AtomicLongFieldUpdater<VolatileAndCAS> atomicInteger = AtomicLongFieldUpdater.newUpdater(VolatileAndCAS.class, "atomic");
    public void increment(CountDownLatch countDownLatch) {
        long oldValue = atomic;
        long newValue = atomic + 1;
        while (!atomicInteger.compareAndSet(this, oldValue, newValue)) {
            System.out.println("重試中==========atomicInteger:" + atomicInteger
                    + "===oldValue:" + atomic + "====newValue:" + newValue);
            oldValue = atomic;
            newValue = oldValue + 1;
        }
        countDownLatch.countDown();
    }

    public long getAtomic() {
        return atomic;
    }
}

class TestVolatile {
    public static void main(final String[] args) {
        final VolatileAndCAS volatileAndCAS = new VolatileAndCAS();
        try {
            final CountDownLatch countDownLatch = new CountDownLatch(500);
            for (int i = 0; i < 5; i++) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        for (int j = 0; j < 100; j++) {
                            volatileAndCAS.increment(countDownLatch);
                        }
                    }
                }).start();
            }
            countDownLatch.await();
            System.out.println("=======" + volatileAndCAS.getAtomic());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

        結果:數值爲500,是爲原子性,中間有兩次重試,還可以接受。

重試中==========atomicInteger:java.util.concurrent.atomic.AtomicLongFieldUpdater$CASUpdater@1c015d7a===oldValue:287====newValue:160
重試中==========atomicInteger:java.util.concurrent.atomic.AtomicLongFieldUpdater$CASUpdater@1c015d7a===oldValue:354====newValue:260
=======500

 

        以上主要以偏代碼、實踐層面來解析synerchronized、volatile的使用場景,網絡上有更多關於其底層實現的文章:

        http://www.cnblogs.com/dennyzhangdd/p/6734638.html

        http://www.jianshu.com/u/90ab66c248e6

本篇也參考了李林峯撰寫的《多線程併發編程在netty中的應用分析》

 

中間有不對的地方還請海涵,指正。

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