Java多執行緒(4):ThreadLocal

2022-10-27 09:01:40

您好,我是湘王,這是我的部落格園,歡迎您來,歡迎您再來~

 

為了提高CPU的利用率,工程師們創造了多執行緒。但是執行緒們說:要有光!(為了減少執行緒建立(T1啟動)和銷燬(T3切換)的時間),於是工程師們又接著創造了執行緒池ThreadPool。就這樣就可以了嗎——不,工程師們並不滿足於此,他們不把自己創造出來的執行緒給扒個底朝天決不罷手。

有了執行緒關鍵字解決執行緒安全問題,有了執行緒池解決效率問題,那還有什麼問題是可以需要被解決的呢——還真被這幫瘋子攻城獅給找到了!

當多個執行緒共用同一個資源的時候,為了保證執行緒安全,有時不得不給資源加鎖,例如使用Synchronized關鍵字實現同步鎖。這本質上其實是一種時間換空間的搞法——用單一資源讓不同的執行緒依次存取,從而實現內容安全可控。就像這樣:

 

 

 

但是,可以不可以反過來,將資源拷貝成多份副本的形式來同時存取,達到一種空間換時間的效果呢?當然可以,就像這樣:

 

 

 

而這,就是ThreadLocal最核心的思想。

 

但這種方式在很多應用級開發的場景中用得真心不多,而且有些公司還禁止使用ThreadLocal,因為它搞不好還會帶來一些負面影響。

其實,從拷貝若干副本這種功能來看,ThreadLocal是實現了線上程內部儲存資料的能力的,而且相互之間還能通訊。就像這樣:

 

 

 

還是以程式碼的形式來解讀一下ThreadLocal。有一個資源類Resource:

/**
 * 資源類
 *
 * @author 湘王
 */
public class Resource {
    private String name;
    private String value;

    public Resource(String name, String value) {
        super();
        this.name = name;
        this.value = value;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }
}

 

分別有ResuorceUtils1、ResuorceUtils2ResuorceUtils3分別以不同的方式來連線資源,那麼看看效率如何。

/**
 * 連線資源工具類,通過靜態方式獲得連線
 *
 * @author 湘王
 */
public class ResourceUtils1 {
    // 定義一個靜態連線資源
    private static Resource resource = null;
    // 獲取連線資源
    public static Resource getResource() {
        if(resource == null) {
            resource = new Resource("xiangwang", "123456");
        }
        return resource;
    }

    // 關閉連線資源
    public static void closeResource() {
        if(resource != null) {
            resource = null;
        }
    }
}



/**
 * 連線資源工具類,通過範例化方式獲得連線
 *
 * @author 湘王
 */
public class ResourceUtils2 {
    // 定義一個連線資源
    private Resource resource = null;
    // 獲取連線資源
    public Resource getResource() {
        if(resource == null) {
            resource = new Resource("xiangwang", "123456");
        }
        return resource;
    }

    // 關閉連線資源
    public void closeResource() {
        if(resource != null) {
            resource = null;
        }
    }
}



/**
 * 連線資源工具類,通過執行緒中的static Connection的副本方式獲得連線
 *
 * @author 湘王
 */
public class ResourceUtils3 {
    // 定義一個靜態連線資源
    private static Resource resource = null;
    private static ThreadLocal<Resource> resourceContainer = new ThreadLocal<Resource>();
    // 獲取連線資源
    public static Resource getResource() {
        synchronized(ResourceManager.class) {
            resource = resourceContainer.get();
            if(resource == null) {
                resource = new Resource("xiangwang", "123456");
                resourceContainer.set(resource);
            }
            return resource;
        }
    }

    // 關閉連線資源
    public static void closeResource() {
        if(resource != null) {
            resource = null;
            resourceContainer.remove();
        }
    }
}



/**
 * 連線資源管理類
 *
 * @author 湘王
 */
public class ResourceManager {
    public void insert() {
        // 獲取連線
        // System.out.println("Dao.insert()-->" + Thread.currentThread().getName() + ResourceUtils1.getResource());
        // Resource resource = new ResourceUtils2().getResource();
        Resource resource = ResourceUtils3.getResource();
        System.out.println("Dao.insert()-->" + Thread.currentThread().getName() + resource);
    }

    public void delete() {
        // 獲取連線
        // System.out.println("Dao.delete()-->" + Thread.currentThread().getName() + ResourceUtils1.getResource());
        // Resource resource = new ResourceUtils2().getResource();
        Resource resource = ResourceUtils3.getResource();
        System.out.println("Dao.delete()-->" + Thread.currentThread().getName() + resource);
    }

    public void update() {
        // 獲取連線
        // System.out.println("Dao.update()-->" + Thread.currentThread().getName() + ResourceUtils1.getResource());
        // Resource resource = new ResourceUtils2().getResource();
        Resource resource = ResourceUtils3.getResource();
        System.out.println("Dao.update()-->" + Thread.currentThread().getName() + resource);
    }

    public void select() {
        // 獲取連線
        // System.out.println("Dao.select()-->" + Thread.currentThread().getName() + ResourceUtils1.getResource());
        // Resource resource = new ResourceUtils2().getResource();
        Resource resource = ResourceUtils3.getResource();
        System.out.println("Dao.select()-->" + Thread.currentThread().getName() + resource);
    }

    public void close() {
        ResourceUtils3.closeResource();
    }

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            new Thread(new Runnable() {
                ResourceManager rm = new ResourceManager();
                @Override
                public void run() {
                    rm.insert();
                    rm.delete();
                    rm.update();
                    rm.select();
                    rm.close();
                }
            }).start();
        }
    }
}

 

執行ResourceManager類中的main()方法後,可以清楚地看到:

第一種靜態方式:大部分資源都能複用,但毫無規律;

第二種範例方式:即使是同一個執行緒,資源範例也不一樣;

第三種ThreadLocal靜態方式:相同的執行緒有相同的範例。

結論是:ThreadLocal實現了執行緒的資源複用。

 

也可以通過畫圖的方式來看清楚三者之間的不同:

這是靜態方式下的資源管理:

 

 

這是範例方式下的資源管理:

 

 

 

這是ThreadLocal靜態方式下的資源管理:

 

 

 

理解了之後,再來看一個資料傳遞的例子,也就是ThreadLocal實現執行緒間通訊的例子:

/**
 * 資料傳遞
 *
 * @author 湘王
 */
public class DataDeliver {
    static class Data1 {
        public void process() {
            Resource resource = new Resource("xiangwang", "123456");
            //將物件儲存到ThreadLocal
            ResourceContextHolder.holder.set(resource);
            new Data2().process();
        }
    }

    static class Data2 {
        public void process() {
            Resource resource = ResourceContextHolder.holder.get();
            System.out.println("Data2拿到資料: " + resource.getName());
            new Data3().process();
        }
    }

    static class Data3 {
        public void process() {
            Resource resource = ResourceContextHolder.holder.get();
            System.out.println("Data3拿到資料: " + resource.getName());
        }
    }

    static class ResourceContextHolder {
        public static ThreadLocal<Resource> holder = new ThreadLocal<>();
    }

    public static void main(String[] args) {
        new Data1().process();
    }
}

 

執行程式碼之後,可以看到Data1的資料都被Data2Data3拿到了,就像這樣:

 

 

 

ThreadLocal在實際應用級開發中較少使用,因為容易造成OOM:

1、由於ThreadLocal是一個弱參照(WeakReference<ThreadLocal<?>>),因此會很容易被GC回收;

2、ThreadLocalMap的生命週期和Thread相同,這就會造成當key=null時,value卻還存在,造成記憶體漏失。所以,使用完ThreadLocal後需要顯式呼叫remove操作(但很多碼農不知道這一點)。

 

 


 

 

感謝您的大駕光臨!諮詢技術、產品、運營和管理相關問題,請關注後留言。歡迎騷擾,不勝榮幸~