Thread 線程共享-ThreadLocal 原理分析、OOM問題及線程安全問題分析

  • 線程共享
  1. synchronized 關鍵字 控制方法或代碼塊同一時刻只有一個線程訪問。鎖必須是一個Object對象,並且多線程只有鎖同一個對象時才能保證同一時刻只有一個線程訪問代碼塊
  2. volatile 關鍵字,多用於變量上。多線程訪問的變量會拿一份副本到線程內存中,以後的操作都是操作的這個副本。如果想每個線程訪問的變量都是最新的,需要在變量前面添加volatile關鍵字。volatile只能保證每次獲取的變量都是最新的,不能保證變量線程安全性。volatile一般用於只有一個修改,多個讀取的業務。

以上2個關鍵字用的比較頻繁,資料也很多,這裏不做詳細講解。

  • ThreadLocal 源碼分析

ThreadLocal 源碼set方法:

  1. 首先獲取當前線程,然後調用getMap(),傳遞參數當前線程t
  2. getMap 方法中可知,獲取的ThreadLocalMap其實是Thread類中的變量,我們查看Thread類
    ThreadLocal.ThreadLocalMap threadLocals = null; 可知,Thread在初始化的時候會對變量threadLocals賦值
  3. ThreadLocalMap 類型中有個Entry[] 數組變量,並且Entry類型是繼承WeakReference<ThreadLocal<?>>  是一個弱引用類型,爲什麼這裏需要設置成弱引用類型?後續OOM中會詳細講解。這裏爲什麼是Entry數組,因爲線程中用到的ThreadLocal可以有多個,比如定義2個ThreadLocal<Integer>,ThreadLcoal<String> 等等。Entry實例對象是以ThreadLocal<?>爲key,以?爲Value,存儲數據,類似Map。
  4. ThreadLocal get()方法同樣也是通過當前線程獲取線程變量ThreadLocalMap,是通過Entry數組獲取到的Value

從以上分析可得,ThreadLocal其實只是個工具類,實際存儲數據還是當前線程實例變量中,每個線程實例都只保存自己的數據,因此多線程中獲取到數據都只是當前線程保存的數據。

 

  • ThreadLocal OOM問題

案例:通過線程池(最多5個線程),創建線程;每個線程都完成模擬5MB的圖片數據保存到內存中;設置VM的堆最大內存100MB ,運行程序通過Jvisualvm.exe查看JVM中的內存使用情況。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @ClassName ThreadLocalOOM
 * @Description 使用ThreadLocal<?>內存泄漏問題分析;每個線程保存部分圖片(byte數據)到內存中
 * @Author zyk
 * @Date 2019/9/26
 * @Version 1.0
 **/
//設置VM -Xmx100m
public class ThreadLocalOOM {
    //ThreadLocal變量,用來訪問每個線程自己的圖片數據入口
    private static ThreadLocal<byte[]> threadPic=new ThreadLocal<>();
    //線程數量
    private static int threadCount=600;
    //線程池
    private static ExecutorService pool=new ThreadPoolExecutor(5,5,
            60L, TimeUnit.SECONDS, new LinkedBlockingQueue());

    //定義線程
    private static class ThreadOOM extends Thread{
        @Override
        public void run() {
            //保存自己圖片到內存中
            threadSaveSelfData();
        }
    }
    //每個線程保存自己的圖片數據
    private static void threadSaveSelfData(){
        System.out.println("==================="+Thread.currentThread().getName()+"--開始保存圖片========================");
        byte[] pictures=new byte[1024*1024*5];//5MB數據
        //代碼1
        threadPic.set(pictures);
        //代碼2
        threadPic.remove();
    }
    public static void main(String[] args) {
        for(int i=0;i<threadCount;i++){
            pool.execute(new ThreadOOM());

            //暫停一下,給出查看 jvisualvm.exe 時間
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("================main線程結束======================");
    }
}

1、把threadSaveSelfData()方法中的 代碼1和代碼2都註釋掉,運行情況如下:

可見堆內存在10MB-25MB左右,當前最多5個線程,每個線程創建5MB的數據,因此最多佔用內存25MB。線程運行完結束,垃圾進行回收。

2、把threadSaveSelfData()方法中的 代碼1打開,代碼2註釋掉,運行情況如下:

JVM堆內存在25MB-60MB左右,我們知道當前JVM應該最多是25MB,堆內存爲什麼會超過25MB,又爲什麼不一直增大,到達最大內存,甚至出現OOM異常呢?帶着這些問題我們分析一下當前程序在棧和堆上創建的對象是怎麼的,這裏只拿出一個線程做對象創建過程說明,如下圖所示:

我們系統中定義了靜態變量,static ThreadLocal<byte[]> threadPic 後續線程池創建的所有線程都通過這個變量獲取當前線程中的數據。threadPic會在堆中創建出內存空間,並且地址會賦值給棧上threadPic棧針的引用。我們每調用一次 pool.execute(new ThreadOOM()); 都會創建出1個線程,並且線程會在堆上創建出響應的內存空間,這裏標註爲圖中的淺紅色空間。線程實例內部會有個threadLocals變量,變量值是個ThreadLocalMap實例,這個實例內部有個table變量(其實是個Entry[]數組實例)。在程序中“代碼1”位置 threadPic.set(pictures) 其實是把其中1個Entry實例賦值,其中key爲ThreadLocal<?>類型,這裏也就是threadPic變量的引用,Value爲 new byte[1024*1024*5] 我們創建出來的5MB數據。

上圖中的箭頭1,2,3和4都是實線,代表強引用。箭頭5爲虛線,代表弱引用 WeakReference。從源代碼中我們知道了Entry繼承WeakReference<ThreadLocal<?>>,set方法把ThreadLocal<?>設置到了Entry的key中,所以Entry中key對靜態變量threadPic的引用是弱引用。

堆中內存爲什麼會超過25MB?

由於我們線程池Pool一直在創建線程,最多5個線程同時運行,每個線程佔用堆空間5MB,5個線程總共佔用堆空間25MB。之所以多餘25MB空間是因爲線程死亡後,創建出來的5MB內存沒有回收造成的。但又爲什麼沒有回收呢?這就是JVM中弱引用的的原因,JVM中弱引用的變量內存會在下次垃圾回收時進行回收。當一個線程死亡後,箭頭2和3所引用的內存地址都應該回收,但是JVM中可達性分析會發現還有個弱引用,引用某個Entry中的Key,所以導致這個Entry不能被回收。這裏也就是爲什麼Entry的key採用弱引用,而不是強引用。如果強引用就肯定會出現OOM問題,引用threadPic是不會回收的(靜態的,後續線程都會使用)

又爲什麼內存不會一直增大,甚至OOM異常呢?

在ThreadLocal的set()方法中會有調用 replaceStaleEntry() 方法,這個方法會判斷是否有弱引用的Entry,如果有則會清空,但這個方法並不是每次都調用,頻率低。由於線程池Pool一直在創建線程執行set()方法,會有replaceStaleEntry方法調用,所以也不會有太多的Entry停留在內存中。這也就是內存爲什麼不一直增大,但是如果程序中調用set()方法的頻率不高就有可能出現大量內存泄漏。所以推薦ThreadLocal中有set方法被調用,當線程結束的時候最後調用remove方法

 

3、把threadSaveSelfData()方法中的 代碼1和代碼2都打開,運行情況如下:

可見這種情況和第一種情況是一樣的效果,線程運行完畢,資源就會回收,不會出現內存浪費問題。

結論:使用ThreadLocal set()和remove()方法一定要同時都出現,不要認爲線程結束了,資源就會釋放。如果此時大量線程結束,每個線程又佔用大量內存就有可能導致內存泄漏

 

  • ThreadLocal 線程安全問題

ThreadLocal其實就是在線程實例內存中有個變量集合存儲數據,存儲的數據並不是數據的副本,對於引用類型還是引用地址,當引用變量發生改變了,TheadLocal中get方法獲取的數據也同樣發生改變。如果這個變量多個線程同時訪問,就有可能造成線程安全問題。

解決方法有2種:

1,線程創建時,線程變量作爲實例變量保存到線程內存中,只有這個線程可以訪問。

2,創建ThreadLocal<?>實例是提供initalValue,每個線程保存自己的初始值,並只修改自己的數據,其他線程不能夠訪問。

以下是ThreadLocal中線程安全代碼案例:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @ClassName ThreadLocalSafety
 * @Description ThreadLocal<?>中的線程安全
 * @Author zyk
 * @Date 2019/9/26
 * @Version 1.0
 **/
public class ThreadLocalSafety {
    //定義Student對象,初始Age爲1
    private static Student stu=new Student(1);
    private static ThreadLocal<Student> threadLocal=new ThreadLocal<>();


    private static ExecutorService pool=new ThreadPoolExecutor(5,5,60L,
            TimeUnit.SECONDS,new LinkedBlockingQueue<>());

    public static void main(String[] args) {
        for (int i=0;i<5;i++) {
            pool.execute(new Runnable() {
                @Override
                public void run() {
                    //當前線程獲取stu的age+1,然後採用ThreadLocal保存stu
                    stu.setAge(stu.getAge()+1);
                    threadLocal.set(stu);

                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "--stu.age= " + threadLocal.get().getAge());
                }
            });
        }
        //執行結果:並不是我們在線程中修改後的值
        //有的時候是:pool-1-thread-4--stu.age= 5
        //有的時候是:pool-1-thread-4--stu.age= 6
    }

    private static class Student{
        public Student(int a){
            this.age=a;
        }
        private int age;

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }
    }
}

通過多次執行,查看執行結果會出現多種情況(不唯一),這就是出現了線程安全問題。在程序設計時,儘量避免。

以下是通過ThreadLocal的initialValue() 解決線程安全問題的代碼案例:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @ClassName ThreadLocalSafety
 * @Description ThreadLocal<?>中的線程安全
 * @Author zyk
 * @Date 2019/9/26
 * @Version 1.0
 **/
public class ThreadLocalSafety {
    //定義Student對象,初始Age爲1
    private static Student stu=new Student(1);
    private static ThreadLocal<Student> threadLocal=new ThreadLocal<Student>(){
        @Override
        protected Student initialValue() {
            return new Student(1);
        }
    };

    private static ExecutorService pool=new ThreadPoolExecutor(5,5,60L,
            TimeUnit.SECONDS,new LinkedBlockingQueue<>());

    public static void main(String[] args) {
        for (int i=0;i<5;i++) {
            pool.execute(new Runnable() {
                @Override
                public void run() {
                    Student student = threadLocal.get();
                    student.setAge(student.getAge()+1);
                    threadLocal.set(student);

                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "--stu.age= " + threadLocal.get().getAge());
                }
            });
        }
    }

    private static class Student{
        public Student(int a){
            this.age=a;
        }
        private int age;

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }
    }
}

多次執行,運行結果一致:線程是安全的

pool-1-thread-2--stu.age= 2
pool-1-thread-3--stu.age= 2
pool-1-thread-1--stu.age= 2
pool-1-thread-4--stu.age= 2
pool-1-thread-5--stu.age= 2

 

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