Java併發編程:線程安全和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可以在一定程度上減少鎖競爭。

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