1. 程式人生 > >高併發程式設計:執行緒安全和ThreadLocal

高併發程式設計:執行緒安全和ThreadLocal

執行緒安全的概念:當多個執行緒訪問某一個類(物件或方法)時,這個類始終都能表現出正確的行為,那麼這個類(物件或方法)就是執行緒安全的。

執行緒安全

說的可能比較抽象,下面就以一個簡單的例子來看看什麼是執行緒安全問題。

public class MyThread implements Runnable {
    private int number = 5;

    @Override
    public void run() {
        number--;
        System.out.println("執行緒 : " + Thread.currentThread().getName() + "獲取到了公共資源,number = " + number);
    }

    public static void main(String[] args) {
        MyThread mt = new MyThread();
        Thread t1 = new Thread(mt, "t1");
        Thread t2 = new Thread(mt, "t2");
        Thread t3 = new Thread(mt, "t3");
        Thread t4 = new Thread(mt, "t4");
        Thread t5 = new Thread(mt, "t5");

        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
    }
}

Java中定一個執行緒有兩種方式:一是繼承Thread方法,二是實現Runnable介面,MyThread使用的是實現Runnable的方式來定義一個執行緒類。該類中有一個類變數number,初始值是5。在我new出的5個執行緒開啟start()方法的時候,執行緒執行到run方法就把number減一次。程式碼在控制檯的輸出結果如下:

執行緒 : t1獲取到了公共資源,number = 3
執行緒 : t3獲取到了公共資源,number = 2
執行緒 : t2獲取到了公共資源,number = 3
執行緒 : t4獲取到了公共資源,number = 1
執行緒 : t5獲取到了公共資源,number = 0

再次執行,得到以下結果:

執行緒 : t2獲取到了公共資源,number = 3
執行緒 : t1獲取到了公共資源,number = 3
執行緒 : t3獲取到了公共資源,number = 2
執行緒 : t4獲取到了公共資源,number = 1
執行緒 : t5獲取到了公共資源,number = 0

從上面兩個輸出結果可以看出,先執行到那個執行緒是不確定的,而number的值更為奇怪,並不是按照5到0依次遞減的。已第一次執行結果為例子,究竟是什麼原因導致了程式出現數據不一致問題的可能性?下面給出了一個可能的情景,如圖所示:
在這裡插入圖片描述
程式碼中建立了5個執行緒,t1執行緒啟動做number–操作時,這時候t3執行緒搶佔到CPU的執行權,t1中斷,t3啟動,這時候number的值等於4,t3執行緒在number等於4的基礎上做number–操作,當t3執行完number–操作時,t1又搶到了CPU的執行權,於是對number進行輸出,此時的number等於3,輸出結束之後t3搶到了CPU執行權,於是t3也對number進行列印輸出,於是t3執行緒輸出的結果也是等於3。
這是多執行緒程式中的一個普遍問題,稱為競爭狀態,如果一個類的物件在多執行緒程式中沒有導致競爭狀態,則稱這樣的類為執行緒安全的。上訴的MyThread類不是執行緒安全的。解決的辦法是給程式碼加鎖,加鎖的關鍵字為synchronized,synchronized可以在任意物件及方法上加鎖,而加鎖的這段程式碼稱為“互斥區”或“臨界區”

public class MyThread implements Runnable {
    private int number = 5;

    @Override
    public synchronized void run() {
        number--;
        System.out.println("執行緒 : " + Thread.currentThread().getName() + "獲取到了公共資源,number = " + number);
    }

    public static void main(String[] args) {
        MyThread mt = new MyThread();
        Thread t1 = new Thread(mt, "t1");
        Thread t2 = new Thread(mt, "t2");
        Thread t3 = new Thread(mt, "t3");
        Thread t4 = new Thread(mt, "t4");
        Thread t5 = new Thread(mt, "t5");

        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
    }
}

本例為一個執行緒安全的執行緒類,無論執行多少次,都是同樣的輸出結果:

執行緒 : t1獲取到了公共資源,number = 4
執行緒 : t2獲取到了公共資源,number = 3
執行緒 : t4獲取到了公共資源,number = 2
執行緒 : t3獲取到了公共資源,number = 1
執行緒 : t5獲取到了公共資源,number = 0

當多個執行緒訪問myThread的run方法時,以排隊的方式進行處理,這裡的排隊是按照CPU分配的先後順序給定的,而不是按照程式碼的先後順序或者執行緒的啟動先後順序來執行的。一個執行緒想要執行synchronized修改的方法裡面的程式碼,首先是嘗試獲得鎖,如果拿到鎖,執行synchronized程式碼體內容;拿不到鎖,這個執行緒就會不斷的嘗試獲得這把鎖,直到拿到為止。而且多個執行緒會同時去競爭這把鎖,也就是會有鎖競爭問題。

ThreadLocal

ThreadLocal是執行緒區域性變數,是一種多執行緒間併發訪問量的解決方案。與其synchronized等加鎖的方式不同,ThreadLocal完全不提供鎖,而使用以空間換時間的手段,為每個執行緒提供變數的獨立副本,以保障執行緒安全。

public class UseThreadLocal {

    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public void setThreadLocal(String value) {
        threadLocal.set(value);
    }

    public String getThreadLocal(){
        return threadLocal.get();
    }

    public static void main(String[] args) {

        UseThreadLocal utl = new UseThreadLocal();

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                utl.setThreadLocal("張三");
                System.err.println("當前t1執行緒拿到的值 : " + utl.getThreadLocal());
            }
        }, "t1");


        Thread t2 = new Thread(new Runnable() {

            @Override
            public void run() {
                utl.setThreadLocal("李四");
                System.err.println("當前t2執行緒拿到的值 : " + utl.getThreadLocal());
            }
        }, "t2");

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t1.start();
        t2.start();
        System.err.println("主執行緒拿到的值 : " + utl.getThreadLocal());
    }
}

上述程式碼建立了3個執行緒,執行緒1向ThreadLocal裡面設定值"張三",執行緒2向ThreadLocal裡面設定值"李四"。程式的程式碼輸出如下:

當前t1執行緒拿到的值 : 張三
當前t2執行緒拿到的值 : 李四
主執行緒拿到的值 : null

從程式的輸出可以看出,每個執行緒只能打印出本執行緒設定的變數值。該程式存在一個共享變數threadLocal,當t1向threadLocal設定“張三”之後,取出的值自然是“張三”,接下來t2執行緒向threadLocal設定值“李四”之後,取出來的值自然是“李四”。有的同學可能會有疑問,說t2也許將t1之前設定的值覆蓋掉了,那麼請看主執行緒的輸出,其結果為null,主執行緒取出的結果為空。這說明了用了ThreadLocal裡面的值只存在與執行緒的區域性變數,對其他執行緒具有不可見性。
那麼ThreadLocal是如何實現其功能的?閱讀其原始碼發現它用到了ThreadLocalMap,該類和HashMap一樣是鍵值對的一種資料結構,值得注意的是雖然該類和HashMap功能類似,當時該類並沒有繼續自Map。

		private Entry[] table;

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

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

ThreadLocal的set方法原始碼

    public void set(T value) {
        Thread t = Thread.currentThread(); //獲取當前執行緒
        ThreadLocalMap map = getMap(t); /以當前執行緒作為key獲得map容器
        if (map != null)//判斷map是否為空
            map.set(this, value); //非空則把當前執行緒作為key,當前value作為值放進map裡面
        else
            createMap(t, value);//為空則建立map
    }

下面我們再來看ThreadLocal應用場景的另一個例子,任務的同時提交。

public class MessageHolder {

    private List<String> messages = new ArrayList<>();

    private static final ThreadLocal<MessageHolder> holder = new ThreadLocal<MessageHolder>(){
        @Override
        protected MessageHolder initialValue() {
            return new MessageHolder();
        }
    };

    public static void add(String value) {
        holder.get().messages.add(value);
    }

    /**
     * 清空list,並返回刪掉的list裡面的值
     * @return
     */
    public static List<String> clear() {
        List<String> list = holder.get().messages;
        holder.remove();
        return list;
    }

    public static void main(String[] args) {
        MessageHolder.add("A");
        MessageHolder.add("B");
        List<String> cleared = MessageHolder.clear(); //已經被清除的list

        System.out.println("被清空掉的元素:" + cleared);
    }
}

MessageHolder類定義了add和clear方法,add方法是新增元素,clear是清空元素的方法,並返回被清楚的list集合。應用場景如下圖,funtion1可能return 1,2,function2可能返回3,4,function3返回5,6,而之前的做法可能是對這三個function()累加的程式碼段進行加鎖,這樣造成A執行緒在訪問的時候B執行緒只能處於等待,只有當這三個方法都執行完畢,向前端返回1,2,3,4,5,6的時候,A執行緒釋放索,B執行緒才能繼續使用,這樣系統解決併發性就很低。

在這裡插入圖片描述
從效能上說,ThreadLocal不具有絕對的優勢,在併發不是很高的時候,加鎖的效能會更好,但作為一套與鎖完全無關的執行緒安全解決方案,在高併發量或者競爭激烈的場景,使用ThreadLocal可以在一定程度上減少鎖競爭。