Java併發之 volatile & synchronized & ThreadLocal 講解

Java 之 volatile & synchronized & ThreadLocal 講解

在併發編程中,基本上離不開這三個東西,如何實現多線程之間的數據共享,能夠用 volatile; 每一個線程維護本身的變量,則採用 ThreadLocal; 爲了保證方法or代碼塊的線程安全,就該 synchronized 上場。這裏將主要說明下這三個能夠怎麼用,以及內部的實現細節html

1. volatile

java編程語言容許線程訪問共享變量,爲了確保共享變量能被準確和一致的更新,線程應該確保經過排他鎖單獨得到這個變量。Java語言提供了volatile,在某些狀況下比鎖更加方便。若是一個字段被聲明成volatile,java線程內存模型確保全部線程看到這個變量的值是一致的。java

實現原理

處理器爲了提升處理速度,不直接和內存進行通信,而是先將系統內存的數據讀到內部緩存(L1,L2或其餘)後再進行操做,但操做完以後不知道什麼時候會寫到內存,若是對聲明瞭Volatile變量進行寫操做,JVM就會向處理器發送一條Lock前綴的指令,將這個變量所在緩存行的數據寫回到系統內存。可是就算寫回到內存,若是其餘處理器緩存的值仍是舊的,再執行計算操做就會有問題,因此在多處理器下,爲了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每一個處理器經過嗅探在總線上傳播的數據來檢查本身緩存的值是否是過時了,當處理器發現本身緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器要對這個數據進行修改操做的時候,會強制從新從系統內存裏把數據讀處處理器緩存裏 用一個圖簡單的說明上面的過程web

圖解

圖畫的通常般,簡單說一下算法

  1. cpu與內部緩存進行交互
  2. volatile生命的變量,操做完以後寫入內存(data -> data' 同時寫入內存)
  3. 其餘cpu緩存嗅探總線變更,並設置本身的data無效,使用時,從內存中獲取

輸入圖片說明

測試case

咱們有兩個線程, 線程B修改一個共享變量tag, 線程A一直循環幹模式, 當發現 tag 設置爲了 true 時, 則結束編程

private volatile boolean tag = false;

    @Test
    public void testVolatile() throws InterruptedException {
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                System.out.println("in A-------");
                while (!tag) {
                    System.out.print((i++) + ",");
                }
                System.out.println("\nout A-------");
            }
        });


        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("in B---------");
                tag = true;
                System.out.println("out B--------");
            }
        });

        threadA.start();
        Thread.sleep(1);
        threadB.start();;
    }

輸出爲:數組

in A-------
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,in B---------
96,
out A-------
out B--------

從上面的輸出能夠看出,當進入線程B以後,將 tag設置爲true, 對線程A而言,它很迅速的感知到了這個參數的變化, 並終止了循環; 若是將tag前面的volatile 關鍵字幹掉,下面是輸出,從最終的結果來看好像並無什麼區別,那這個東西到底有什麼用,該怎麼用?緩存

輸出結果安全

in A-------
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,in B---------
135,
out A-------
out B--------

一篇參考連接: http://blog.csdn.net/feier7501/article/details/20001083 (說明這篇博文中的case,本機jdk8並無復現....., 因此這是一個失敗的case)多線程

再看一個case,併發

public class TestVolatile {
    int a = 1;
    int b = 2;

    public void change(){
        a = 3;
        Thread.sleep(10);   // 人肉加長這個賦值的時間
        b = a;
    }

    public void print(){
        System.out.println("b="+b+";a="+a);
    }

    public static void main(String[] args) {
        while (true){
            final TestVolatile test = new TestVolatile();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                        test.change();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();

            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.print();
                }
            }).start();

        }
    }
}

從上面的代碼來看,正常來說,輸出1,2; 或者 3, 3, 而實際輸出卻並非這樣

...... 
b=2;a=1
b=2;a=1
b=3;a=3
b=3;a=3
b=3;a=1           <--------------------- 看這裏
b=3;a=3
b=2;a=1
b=3;a=3
b=3;a=3
......

使用建議:在兩個或者更多的線程訪問的成員變量上使用volatile。當要訪問的變量已在synchronized代碼塊中,或者爲常量時,沒必要使用。

2. synchronized

synchronized 同步代碼塊 or 同步方法, 加鎖, 簡單來說,當一塊被這個關鍵詞修飾時,那麼這塊在統一時刻,只能有一個線程進行訪問

一般來說,有三種使用方法,用來修飾成員方法, 靜態方法, 和代碼快,下面分別來寫個測試case

修飾靜態方法

public synchronized static void staticFunc() {
        System.out.println(Thread.currentThread().getName() + " in 1--->");
        try {
            System.out.println(Thread.currentThread().getName() + "-->synch staticFunc print");
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " out 1--->");
    }

    public synchronized static void staticFunc2() {
        System.out.println(Thread.currentThread().getName() + " in 2--->");
        try {
            System.out.println(Thread.currentThread().getName() + "-->synch staticFunc2 print");
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " out 2--->");
    }


    public static void main(String[] args) {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                SynchronizedTest.staticFunc();
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                SynchronizedTest.staticFunc2();
            }
        });

        thread1.start();
        thread2.start();
    }

輸出以下, 兩個同步修飾的靜態方法, 第一個線程使用其中的方法時,第二個線程即使調用第二個靜態方法,依然會被阻塞

Thread-0 in 1--->
Thread-0-->synch staticFunc print
Thread-0 out 1--->
Thread-1 in 2--->
Thread-1-->synch staticFunc2 print
Thread-1 out 2--->

將上面的 synchronized 修飾去掉, 看下輸出以下,也就是說,二者的調用是能夠並行的

Thread-1 in 2--->
Thread-0 in 1--->
Thread-1-->synch staticFunc2 print
Thread-0-->synch staticFunc print
Thread-1 out 2--->
Thread-0 out 1--->

修飾成員方法

在上面的例子中,稍稍改動便可

public class SynchronizedTest {

    public synchronized void staticFunc() {
        System.out.println(Thread.currentThread().getName() + " in 1--->");
        try {
            System.out.println(Thread.currentThread().getName() + "-->synch staticFunc print");
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " out 1--->");
    }

    public synchronized void staticFunc2() {
        System.out.println(Thread.currentThread().getName() + " in 2--->");
        try {
            System.out.println(Thread.currentThread().getName() + "-->synch staticFunc2 print");
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " out 2--->");
    }


    public static void main(String[] args) {
        SynchronizedTest synchronizedTest = new SynchronizedTest();

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronizedTest.staticFunc();
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronizedTest.staticFunc2();
            }
        });

        thread1.start();
        thread2.start();
    }

}

輸出以下:

Thread-0 in 1--->
Thread-0-->synch staticFunc print
Thread-0 out 1--->
Thread-1 in 2--->
Thread-1-->synch staticFunc2 print
Thread-1 out 2--->

成員方法和靜態方法的修飾區別是什麼 ?對上面的代碼,作一個簡單的修改, Thread1調用對象1的方法1, Thread3 調用對象2的方法1

public static void main(String[] args) {
        SynchronizedTest synchronizedTest = new SynchronizedTest();

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronizedTest.staticFunc();
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronizedTest.staticFunc2();
            }
        });


        SynchronizedTest synchronizedTest2 = new SynchronizedTest();
        Thread thread3 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronizedTest2.staticFunc2();
            }
        });

        thread1.start();
        thread2.start();
        thread3.start();
    }

輸出以下, 其中線程0 和線程1 保證有序, 可是與線程2就沒有什麼關係了;即這個鎖是針對對象的,這個也很容易理解,畢竟對象都不一樣了,對象的成員方法固然是相對獨立的

Thread-0 in 1--->
Thread-0-->synch staticFunc print
Thread-2 in 2--->
Thread-2-->synch staticFunc2 print
Thread-0 out 1--->
Thread-2 out 2--->
Thread-1 in 2--->
Thread-1-->synch staticFunc2 print
Thread-1 out 2--->

同步代碼塊

同步代碼塊的使用,就是將一塊代碼用大括號圈起來, 外面用 synchronized() 進行修飾,括號裏面就表示要加鎖的東西

public class SynchronizedTest {

    public void staticFunc() {
        System.out.println(Thread.currentThread().getName() + " in 1--->");
        try {
            synchronized (this) {
                System.out.println(Thread.currentThread().getName() + "-->synch staticFunc print");
                Thread.sleep(100);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " out 1--->");
    }

    public void staticFunc2() {
        System.out.println(Thread.currentThread().getName() + " in 2--->");
        try {
            synchronized (this) {
                System.out.println(Thread.currentThread().getName() + "-->synch staticFunc2 print");
                Thread.sleep(100);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " out 2--->");
    }


    public static void main(String[] args) {
        SynchronizedTest synchronizedTest = new SynchronizedTest();

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronizedTest.staticFunc();
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronizedTest.staticFunc2();
            }
        });

        thread1.start();
        thread2.start();
    }
}

輸出以下, 對這個說明一點, 若是在靜態方法中, 使用了同步代碼塊, 那麼括號裏面的能夠寫什麼 ? xx.class 便可

Thread-0 in 1--->
Thread-1 in 2--->
Thread-0-->synch staticFunc print
Thread-0 out 1--->
Thread-1-->synch staticFunc2 print
Thread-1 out 2--->

實現原理

源碼以下

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("Method 1 start");
        }
    }
}

輸入圖片說明

在加鎖的代碼塊, 多了一個 monitorenter , monitorexit

每一個對象有一個監視器鎖(monitor)。當monitor被佔用時就會處於鎖定狀態,線程執行monitorenter指令時嘗試獲取monitor的全部權,過程以下:

  1. 若是monitor的進入數爲0,則該線程進入monitor,而後將進入數設置爲1,該線程即爲monitor的全部者。
  2. 若是線程已經佔有該monitor,只是從新進入,則進入monitor的進入數加1.
  3. 若是其餘線程已經佔用了monitor,則該線程進入阻塞狀態,直到monitor的進入數爲0,再從新嘗試獲取monitor的全部權

執行monitorexit的線程必須是objectref所對應的monitor的全部者。

  1. 指令執行時,monitor的進入數減1
  2. 若是減1後進入數爲0,那線程退出monitor,再也不是這個monitor的全部者
  3. 其餘被這個monitor阻塞的線程能夠嘗試去獲取這個 monitor 的全部權

談到 synchronized 就不可避免的要說到鎖這個東西,基本上在網上能夠搜索到一大批的關於偏向鎖,輕量鎖,重量鎖的講解文檔,對這個東西基本上我也不太理解,多看幾篇博文以後,簡單的記錄一下

先拋一個結論: 輕量級鎖是爲了在線程交替執行同步塊時提升性能,而偏向鎖則是在只有一個線程執行同步塊時進一步提升性能

1. 偏向鎖

獲取過程

  • 判斷是否爲可偏向狀態
  • 是,則判斷線程ID是否指向當前線程
    • 是,即表示這個偏向鎖就是這個線程持有, 直接執行代碼塊
    • 否,經過CAS操做競爭鎖
      • 競爭成功, 則設置線程ID爲當前線程, 並執行代碼塊;
      • 競爭失敗,說明多線程競爭啦,問題嚴重了,當偏向鎖到達安全點時,將偏向鎖升級爲輕量鎖

釋放過程

  • 當偏向鎖遇到其餘線程嘗試競爭時,持有偏向鎖的線程會釋放,並升級爲輕量鎖
  • 到達安全點, 暫停擁有偏向鎖的線程,判斷鎖對象是否處於被鎖的狀態,撤銷偏向鎖後恢復到未鎖定(標誌位爲「01」)或輕量級鎖(標誌位爲「00」)的狀態。

2. 輕量鎖

「輕量級」是相對於使用操做系統互斥量來實現的傳統鎖而言的。可是,首先須要強調一點的是,輕量級鎖並非用來代替重量級鎖的,它的本意是在沒有多線程競爭的前提下,減小傳統的重量級鎖使用產生的性能消耗。在解釋輕量級鎖的執行過程以前,先明白一點,輕量級鎖所適應的場景是線程交替執行同步塊的狀況,若是存在同一時間訪問同一鎖的狀況,就會致使輕量級鎖膨脹爲重量級鎖

輕量鎖

3. 轉換

簡單來說,單線程時,使用偏向鎖,若是這個時候,又來了一個線程訪問這個代碼塊,那麼就要升級爲輕量鎖,若是這個線程在訪問代碼塊同時,又來了一個線程來訪問這個代碼塊,那麼就要升級爲重量鎖了。下面更多的顯示了這些變更時,標記位的隨之改變

輸入圖片說明

4. 經典案例

單例模式,懶加載的方式,就是一個典型的利用了 synchronized 的案例

public class SingleClz {
    private static final SingleClz instance;
    
    private SingleClz() {}
    
    public static SingleClz getINstance() {
        if(instance == null) {
            synchronized(SingleClz.class) {
                if(instance == null) {
                    instance = new SingleClz();
                }
            }
        }
        return instance;
    }
}

ThreadLocal

線程本地變量,每一個線程保存變量的副本,對副本的改動,對其餘的線程而言是透明的(即隔離的)

1. 使用姿式一覽

先來瞅一下,這個東西通常的使用姿式。一般要獲取線程變量, 直接調用 ParamsHolder.get()

public class ParamsHolder {
    private static final ThreadLocal<Params> PARAMS_INFO = new ThreadLocal<>();

    @ToString
    @Getter
    @Setter
    public static class Params {
        private String mk;
    }

    public static void setParams(Params params) {
        PARAMS_INFO.set(params);
    }

    public static void clear() {
        PARAMS_INFO.remove();
    }
    
    public static Params get() {
        return PARAMS_INFO.get();
    }
    
    
    public static void main(String[] args) {

        Thread child = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("child thread initial: " + ParamsHolder.get());
                ParamsHolder.setParams(new ParamsHolder.Params("thread"));
                System.out.println("child thread final: " + ParamsHolder.get());
            }
        });


        child.start();

        System.out.println("main thread initial: " + ParamsHolder.get());
        ParamsHolder.setParams(new ParamsHolder.Params("main"));
        System.out.println("main thread final: " + ParamsHolder.get());
    }
}

輸出結果

child thread initial: null
main thread initial: null
child thread final: ParamsHolder.Params(mk=thread)
main thread final: ParamsHolder.Params(mk=main)

2. 實現原理探究

直接看源碼中的兩個方法, get/set, 看下究竟是如何實現線程變量的

public void set(T value) {
   Thread t = Thread.currentThread();
   ThreadLocalMap map = getMap(t);
   if (map != null)
       map.set(this, value);
   else
       createMap(t, value);
}

public T get() {
   Thread t = Thread.currentThread();
   ThreadLocalMap map = getMap(t);
   if (map != null) {
       ThreadLocalMap.Entry e = map.getEntry(this);
       if (e != null) {
           @SuppressWarnings("unchecked")
           T result = (T)e.value;
           return result;
       }
   }
   return setInitialValue();
}

先看set方法, 邏輯是獲取當前線程對象, 獲取到線程對象中的 threadLocals 屬性, 這個屬性的解釋以下,簡單來說, 這個裏面的變量都是線程獨享的,徹底由線程本身hold住

ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class.

獲取的話主要是從 ThreadLocalMap 中,將存進去的參數撈出來,如今須要瞭解的就是這個對象的內部構造了, 裏面的有個table對象,維護了一個Entry的數組tableEntry的key爲ThreadLocal對象, value爲具體的值。

聚焦在 int i = key.threadLocalHashCode & (table.length - 1); 這一行,這個就是獲取Entry對象在table中索引值的主要邏輯,主要利用當前線程的hashCode值,假設出現兩個不一樣的線程,這個code值同樣,會如何?下面的getEntry()邏輯中對key值進行了判斷是否爲當前線程

//ThreadLocalMap.java
static class Entry extends WeakReference<ThreadLocal<?>> {
       /** The value associated with this ThreadLocal. */
       Object value;

       Entry(ThreadLocal<?> k, Object v) {
           super(k);
           value = v;
       }
   }
   
   /**
    * The table, resized as necessary.
    * table.length MUST always be a power of two.
    */
private Entry[] table;
   
private Entry getEntry(ThreadLocal<?> key) {
       int i = key.threadLocalHashCode & (table.length - 1);
       Entry e = table[i];
       if (e != null && e.get() == key)
           return e;
       else
           return getEntryAfterMiss(key, i, e);
   }

針對上面的邏輯,有兩個點有必要繼續研究下, hashCode 的計算方式, 爲何要和數組的長度進行與計算

做爲ThreadLocal實例的變量只有 threadLocalHashCode 這一個,nextHashCodeHASH_INCREMENT 是ThreadLocal類的靜態變量,實際上HASH_INCREMENT是一個常量,表示了連續分配的兩個ThreadLocal實例的threadLocalHashCode值的增量,而nextHashCode 的表示了即將分配的下一個ThreadLocal實例的threadLocalHashCode 的值

全部ThreadLocal對象共享一個AtomicInteger對象nextHashCode用於計算hashcode,一個新對象產生時它的hashcode就肯定了,算法是從0開始,以HASH_INCREMENT = 0x61c88647爲間隔遞增,這是ThreadLocal惟一須要同步的地方。根據hashcode定位桶的算法是將其與數組長度-1進行與操做

ThreadLocalMap的初始長度爲16,每次擴容都增加爲原來的2倍,即它的長度始終是2的n次方,上述算法中使用0x61c88647可讓hash的結果在2的n次方內儘量均勻分佈,減小衝突的機率

3. 線程池中使用ThreadLocal的注意事項

這裏主要的一個問題是線程複用時, 若是不清楚掉ThreadLocal 中的值,就會有可怕的事情發生, 先簡單的演示一下

private static final ThreadLocal<AtomicInteger> threadLocal =new ThreadLocal<AtomicInteger>() {

        @Override
        protected AtomicInteger initialValue() {
            return new AtomicInteger(0);
        }
    };


    static class Task implements Runnable {

        @Override
        public void run() {
            AtomicInteger s = threadLocal.get();
            int initial = s.getAndIncrement();
            // 指望初始爲0
            System.out.println(initial);
        }
    }


    public static void main(String[] args) {

        ExecutorService executor = Executors.newFixedThreadPool(2);
        executor.execute(new Task());
        executor.execute(new Task());
        executor.execute(new Task());
        executor.shutdown();
    }

輸出結果

0
0
1

說好的線程變量,這裏竟然沒有按照咱們預期的來玩,主要緣由就是線程複用了,而線程中的局部變量沒有清零,致使下一個使用這個線程的時候,這些局部變量也帶過來,致使沒有按照咱們的預期使用

這個最可能致使的一個超級嚴重的問題,就是web應用中的用戶串掉的問題,若是咱們將每一個用戶的信息保存在 ThreadLocal 中, 若是出現線程複用了,那麼問題就會致使明明是張三用戶,結果登陸顯示的是李四的賬號,這下就真的呵呵了

所以,強烈推薦,對於線程變量,一但不用了,就顯示的調用 remove()方法進行清楚

4. 經典case

SimpleDataFormate 是一個非線程安全的類,可使用 ThreadLocal 完成的線程安全的使用

public class ThreadLocalDateFormat {
    static ThreadLocal<DateFormat> sdf = new ThreadLocal<DateFormat>() {

        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public static String date2String(Date date) {
        return sdf.get().format(date);
    }

    public static Date string2Date(String str) throws ParseException {
        return sdf.get().parse(str);
    }
}

參考文檔:

  1. 聊聊併發(一)深刻分析Volatile的實現原理
  2. Java 併發編程:volatile的使用及其原理
  3. Synchronized及其實現原理
  4. Java併發編程:Synchronized底層優化(偏向鎖、輕量級鎖)
  5. 聊聊併發(二)Java SE1.6中的Synchronized
  6. 理解ThreadLocal / 計算機程序的思惟邏輯
  7. 【ThreadLocal】深刻JDK源碼之ThreadLocal類
相關文章
相關標籤/搜索