鎖的優化及注意事項

多線程存在的效率問題:各線程的元數據,上下文切換,線程調度等

1.有助於提高鎖性能的幾點建議

1.1 減少鎖持有的時間

只在需要的代碼段加鎖,不要將不必要的內容加到同步代碼方法或同步代碼塊中。

1.2 減小鎖粒度

和1.1的方法的差不多,縮小鎖定對象的範圍,從而降低鎖衝突的可能性。

1.3 用讀寫分離鎖來替換獨佔鎖

1.4 鎖分離

與讀寫鎖的思路類似,對於一個對象,不使用獨佔鎖,而是依據操作功能的不同,對鎖進行分離。如LinkedBlockingQueue中的take和put方法實現了取數據和讀數據的分離。

1.5 鎖粗化

虛擬機再遇到一連串連續地對同一個鎖不斷進行請求和釋放的操作時,便會把所有的鎖操作整合成對鎖的一次請求,從而減少對鎖的請求同步的次數。我們自己寫程序時也應該注意這點。但要注意和1.2的區別。

2. Java虛擬機對鎖優化所做的努力

下面都是jdk對鎖所做的優化,當對象偏向鎖的標誌位(可偏向)爲1時,可以將鎖設置爲偏向鎖,偏向鎖適合只有一個線程訪問的情景,輕量級鎖的使用代價低於輕量級鎖。如果對象的偏向鎖的標誌位爲0時,不可以將鎖設置爲偏向鎖,那麼這時可以嘗試將其設置爲輕量級鎖,輕量級鎖適合多個線程交替競爭鎖,如果多個線程爭奪鎖,鎖會膨脹爲重量級鎖。

2.1 鎖偏向

如果一個線程獲得了鎖,那麼鎖就進入偏向模式。當這個線程再次請求時,無需再做任何同步操作,可以節省大量有關鎖申請的操作,從而提高程序性能。適合於幾乎沒有鎖競爭的場合。

2.2 輕量級鎖

https://www.cnblogs.com/paddix/p/5405678.html

https://blog.csdn.net/lengxiao1993/article/details/81568130

2.3 自旋鎖

虛擬機讓當前線程做幾個空循環,再經過若干次循環後,如果可以得到鎖,那麼就順利進入臨界區,否則就掛起線程。

2.4 鎖消除

鎖消除時一種更徹底的優化。Java虛擬機再JIT編譯時,通過對運行上下文的掃描,去除不可能存在共享資源的競爭的鎖。如StringBuffer,Vector等的使用過程中可能會出現。鎖消除要保證不會出現逃逸分析纔可以進行。

3 ThreadLocal

除了控制資源的訪問外,還可以通過增加資源來保證對象的線程安全。鎖是前者的思路,ThreadLcal是後者的思路。ThreadLocal會爲每一個線程提供一個獨立的變量副本,從而隔離了多個線程對數據的訪問衝突。在ThreadLocal類中有一個Map,用於存儲每一個線程的變量副本,Map中元素的鍵爲線程對象,而值對應線程的變量副本。

3.1 ThreadLocal是什麼

ThreadLocal源碼中該類的介紹:該類提供線程局部變量。這些變量與它們的正常變量不同,因爲每個訪問一個線程的變量都有它自己的、獨立的初始化拷貝。實例通常是希望將狀態與線程關聯的類中的私有字段(例如,*用戶ID或事務ID)。

3.2 ThreadLocal原理

https://www.cnblogs.com/ldq2016/p/9041856.html 這個博客講得不錯,結合《實戰java高併發程序設計》的這一小節的內容,以及自己的一些實踐,應該可以理解ThreadLocal。

(1)每個線程爲自己的ThreadLocal變量賦值

定義一個ThreadLocal對象,當一個線程要使用這個對象時,可以使用set方法給這個對象賦值,如果不賦值的話,而定義這個對象的時候有沒有定義initialValue()方法賦予初值,這個對象的值可能爲null,使用時就會出現空指針異常。所以在使用ThreadLoacl對象時,如果所有線程都是相同的值,那要在定義這個ThreadLocal變量時,給這個對象利用initialValue()方法賦予初值。如果每個線程這個變量的值不同,那要利用set()方法賦值,最好也利用initValue()方法賦值,防止空指針異常錯誤的出現。

《 實戰java高併發程序設計》中的例子,

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadLocalDemo {
    private static ThreadLocal<SimpleDateFormat> t1 = new ThreadLocal<SimpleDateFormat>();

    public static class ParseDate implements Runnable{
        int i=0;
        public ParseDate(int i){this.i=i;}

        public void run() {
            try{
                if(t1.get()==null){
                    t1.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
                }
                Date t =t1.get().parse("2015-03-29 19:29:"+i%60);
                System.out.println(i+":"+t);
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String []args) throws InterruptedException {
        ExecutorService es = Executors.newFixedThreadPool(10);
        for(int i=0;i<1000;i++){
            es.execute(new ParseDate(i));
        }
        Thread.sleep(2000);
    }
}

每個線程在使用ThreadLocal對象t1時,要利用 t1.get() == null 來判斷t1是否爲空,如果爲空,給它賦值。如果嘗試將

                if(t1.get()==null){
                    t1.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
                }

這段代碼刪掉,會報空指針異常的錯誤。

而如果在定義t1時,設置初值,就不會報錯了

private static ThreadLocal<SimpleDateFormat> t1 = new ThreadLocal<SimpleDateFormat>(){
    protected SimpleDateFormat initialValue(){
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    }
};

這裏我有一個疑問,同樣是使用t1,爲什麼有的線程爲空,有的不爲空,不太懂,但是自己在使用時注意初始化和set就好了。 

(2)線程取得ThreadLocal變量中的值

利用get()方法

 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();
    }

從代碼中可以看出,get方法首先嚐試從ThreadLocalMap中獲取這個線程對應的值,當爲空時,嘗試獲取定義這個對象時的初始值,如果還爲空,就返回null了。

(3)關於ThreadLocal變量GC

ThreadLocal變量中可能維護了很多線程的value,在一個線程中所以確定不用這個變量的時候,可以使用remove()方法將這個變量移除中的value移除,加快GC,防止內存泄漏。也可以使用 “ThreadLcal對象名” = null 加快將這個變量設爲null,加快GC。remove源碼如下:

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

 

4. 無鎖

鎖是一種悲觀的策略,認爲有多個線程會同時訪問同一資源,所以有多個線程訪問臨界區時,只能有一個線程訪問臨界去資源,其他線程只能阻塞。而無鎖是一種樂觀的策略,假設有衝突,也不需要阻塞,而是通過CAS技術不斷自旋嘗試,直到成功或者放棄。典型的應用是原子類和一些併發容器。 

5. 鎖的優化及注意事項

5.1 死鎖是什麼

5.2 哲學家問題的實例代碼

public class DeadLock extends Thread {
    protected Object tool;
    static Object fork1 = new Object();
    static Object fork2 = new Object();

    public DeadLock(Object obj){
        this.tool = obj;
        if(tool == fork1){
            this.setName("哲學家A");
        }
        if(tool == fork2){
            this.setName("哲學家B");
        }
    }


    public void run() {
        if(tool == fork1){
            synchronized (fork1){
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (fork2){
                    System.out.println("哲學家A開始喫飯了");
                }
            }
        }
        if(tool == fork2){
            synchronized (fork2){
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (fork1){
                    System.out.println("哲學家B開始喫飯了");
                }
            }
        }
    }

    public static void main(String []args) throws InterruptedException {
        DeadLock 哲學家A = new DeadLock(fork1);
        DeadLock 哲學家B = new DeadLock(fork2);
        哲學家A.start();
        哲學家B.start();
        Thread.sleep(1000);
     }
}

5.3 死鎖檢測方法

 

結果:

5.3 死鎖避免的方法:

1.使用無鎖的方法

2.限制線程獲得鎖的順序 

3.超時檢測

4.死鎖檢測

https://blog.csdn.net/ls5718/article/details/51896159

5.4 死鎖解除

釋放資源或者撤銷線程 

 

 

 

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