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

 

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