老馬的JVM筆記(八)(完)----線程安全與鎖優化

8.1 線程安全

“當多個線程訪問一個對象時,如果不用考慮這些線程在運行時環境下的調度和交替執行,也不需要額外的同步,或者在調用方進行任何其他的協調操作,調用這個對象的行爲都可以獲得正確的結果,那這個對象就是線程安全的。”

這裏作者把Java的線程安全分爲五個等級:

1.不可變:

不可變的對象一定是線程安全的,前提是在構建過程未逃逸。final修飾的對象就是不可變對象。final修飾基本類型就不可變,修飾對象的話對象不可重新初始化,只能調用其中方法,個人理解是內存起始不可變。

2.絕對線程安全:

難。

但作者給的示例代碼已經失效了,科技在進步。只會不停地跑下去,還是安全的。

    while(true){
            for(int i = 0; i < 10; i++){
                vector.add(i);
            }

            Thread removeThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int i = 0; i < vector.size(); i++) vector.remove(i);
                }
            });

            Thread printThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int i = 0; i < vector.size(); i++) System.out.println(vector.get(i));
                }
            });

            removeThread.start();
            printThread.start();

            System.out.println(" current have " + Thread.activeCount() );

            while(Thread.activeCount() > 20) System.out.println(" already have " + Thread.activeCount() );
        }

3.相對線程安全:

通常意義的線程安全,不需要額外加入同步限制。Java的Vector,HashTable都是線程安全的。

4.線程兼容:

對象本身不是線程安全的,在加入同步手段後可以安全地使用。基本都是這樣的。

5.線程對立:

無論加入什麼措施都無法多線程併發。Thread的suspend()和resume()方法就是線程對立的。

8.2 線程安全的實現方法

互斥同步(Mutual Exclusion & Synchronization),阻塞式同步,是一種保證一個對象同一時間只在一個線程中被使用的方法。利用互斥(臨界區、互斥量、信號量)來實現同步。synchronized在java中是一個重量級操作,在有必要的時候才使用。

除了synchronized,還有ReentrantLock可以實現同步。基於同步的基礎上,重入鎖加入三中功能:等待可中斷,持有鎖的線程長時間不釋放鎖,等待的線程可以放棄等待,去做其他事情;公平鎖,多個線程同時等待一個鎖時,按照申請鎖的時間依次獲得鎖,synchronized不行;鎖綁定多個條件,一個ReentrantLock可以綁定多個Condition對象,使用newCondition()就可以添加,不像synchronized每增加一個條件都要添加一個鎖。

非阻塞同步與互斥同步不同,互斥同步在發生鎖時,會立刻處於等待階段,阻塞狀態。非阻塞同步是一種樂觀鎖,先操作,再看該對象是否被多線程競爭,如果被競爭,則採取補救措施(重試),否則就繼續。

無同步方案指一些天生線程安全的方法,不需要同步來保護。可重入代碼(Reentrant Code),也叫純代碼(Pure Code),可以在任何地方被打斷,且重入後不會出現錯誤。線程本地存儲(Thread Local Storage),如果把有競爭的變量都統一放在一個線程中,則無需同步。

8.3 鎖優化

8.3.1 自旋鎖與自適應自旋

由於線程的總調度要交給內核線程,所以線程阻塞的時候的掛起和恢復操作都要轉入內核態來完成,是非常效率低的。且大多數情況等待時間較短,沒必要切換狀態。爲了減少這種狀態切換,可以讓線程執行一個忙自旋,讓線程在等待時“原地踏步”,這個技術是自旋鎖。自旋等待避免了狀態切換,但會佔CPU時間,在等待時間較短時,是非常划算的。因此自旋鎖的自旋時間需要有限制,不能沒完沒了地原地踏步。

自適應自旋指自旋的次數根據上次在同一個鎖上的自旋時間決定,如果上一次成功獲得鎖,且持有鎖的線程正在運行,代表本次自旋是值得的,時間長一些也可以;如果某個鎖自旋很少成功過,則本次可能不選擇自旋。

8.3.2 鎖消除

鎖消除指自動消除一些“沒必要”的鎖。如果一個對象被同步,但檢測到不存在對其數據的競爭,那本次的鎖時無必要的,可以消除。如果一段代碼的所有數據都不會逃逸,則他們是線程私有的,很安全,沒必要上鎖。然而代碼不上鎖不代表運行時無同步,有時Java的內在類會包裝一些同步措施,在這個技術下可以被摘除。

8.3.3 鎖粗化

通常來說,爲了節省阻塞時間,同步範圍越小越好。然而一個對象反反覆覆被同步,且某程序塊都會被反覆競爭,則可以將同步塊的範圍擴大。

8.3.4 輕量級鎖

虛擬機的對象的佈局中對象頭(Object Head)分爲兩個部分,第一個部分用於存儲對象自身的運行時數據,如HashCode,Generation GC Age等,官方稱爲“Mark Word”;另一部分用於存儲指向方法區對象類型數據的指針。爲了節省空間,Mark Word的空間不是固定的,是可伸縮可複用的。

簡單來說,輕量級鎖的原理就是在當前線程中建立一塊Lock Record空間,存儲同步對象的Mark Word,然後讓該對象的Mark Word指向Lock Record中的指針,這樣線程就有了該對象的鎖。如果更新失敗,虛擬機檢查對象的Mark Word是否指向該線程的棧幀,如果有鎖,則該線程擁有了該對象的鎖,否則該對象在被其他線程鎖定了。如果存在兩條線程以上的線程競爭同一個鎖,則輕量級鎖要升級到重量鎖,此時Mark Word中存儲重量級鎖(互斥量)的指針,等待鎖的線程進入等待狀態。

在解鎖中,對象的Mark Word指向棧幀中Lock Record中的指針,將兩指針調換,則解除鎖定。爲啥叫輕量級鎖?因爲很多時候雖然同步了,但不一定有競爭。但真的有了競爭,輕量級鎖使用了CAS來加鎖解鎖,速度倒慢了。所以也不可能取代重量鎖。

8.3.5 偏向鎖 

在無競爭的情況下,直接消除整個同步。偏向鎖的意思是鎖會偏向第一個獲取鎖的線程,在他的執行過程中,如果沒有別的線程競爭此對象,則不同步。偏向鎖提高了無競爭,且有同步的程序性能。

 

至此,本書結束。其實就編程而言,並無太多實用之處,更適合一些硬核的底層設計人員。但開卷有益,也許今日覺得無用的東西,在哪天突然見到了實用性,就會知道回頭看看,也是不錯的。

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