ThreadLocal,你掌握了吗?

1,129 阅读8分钟

ThreadLocal

ThreadLocal是为了解决多线程程序间并发问题的一种方法,它采用 “以空间换时间” 的方式,为每一个线程提供一份共享变量的副本,从而实现同时访问共享变量而互不干扰。

ThreadLocal的使用

/**
 * @Author GJY
 * @Date 2021/6/28 8:41
 * @Version 1.0
 */
public class ThreadLocalDemo1 {

    private ThreadLocal<Integer> threadLocal1 = ThreadLocal.withInitial(()->3);
    private ThreadLocal<Integer> threadLocal2 = ThreadLocal.withInitial(()->4);

    //设置一个信号量,同一时刻只能有一个线程执行
    private Semaphore  semaphore = new Semaphore(1);

    //内部类
    private class ThreadLocalInnerClass implements Runnable{
        @Override
        public void run() {
            try {
                Thread.sleep(2000);
                //获取许可
                semaphore.acquire();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //此时,没有修改ThreadLocal值前,线程中ThreadLocalMap中的Entry键值对为 < threadLocal1对象 , 3 >、< threadLocal2对象 , 4 >
            //不同的ThreadLocal对象及其对应的value,在一个线程的ThreadLocalMap中以不同的Entry对象存储
            //从ThreadLocal中获取值
            Integer value = threadLocal1.get();
            System.out.println(Thread.currentThread().getName()+"未改变ThreadLocal1前的值为:"+value);

            //改变threadLocal的值(随机数)
            threadLocal1.set(new Random().nextInt());
            threadLocal2.set(new Random().nextInt());
            //输出改变之后threadLocal中的值
            System.out.println(Thread.currentThread().getName()+"改变后ThreadLocal的值为:"+threadLocal1.get());
            System.out.println(Thread.currentThread().getName()+"改变后ThreadLocal的值为:"+threadLocal2.get());
            //释放信号量
            semaphore.release();
            //清除ThreadLocal
            //线程池中的线程是循环使用的
            //如果不清除ThreadLocal,就可能会造成下一个任务进入线程池后线程执行任务时获取到的是上次保存的ThreadLocal
            //因为清除后,ThreadLocal中的ThreadLocalMap对象的Entry就为null,那么线程中的ThreadLocalMap对象就为null,
            // 在另一个任务进入该线程后第一次调用ThreadLocal的get方法时
            //就会触发初始化setInitialValue(),获取ThreadLocal的初始值
            threadLocal1.remove();
            System.out.println("执行完毕");
        }
    }

    public static void main(String[] args) {
        //创建一个线程池
        ExecutorService executor = Executors.newFixedThreadPool(2);
        ThreadLocalDemo1 thread = new ThreadLocalDemo1();
        executor.execute(thread.new ThreadLocalInnerClass());
        executor.execute(thread.new ThreadLocalInnerClass());
        //关闭线程池
        executor.shutdown();

        /**
         * 程序输出:
         * pool-1-thread-2未改变ThreadLocal前的值为:3
         * pool-1-thread-2改变后ThreadLocal的值为:132549042
         * pool-1-thread-1未改变ThreadLocal前的值为:3
         * pool-1-thread-1改变后ThreadLocal的值为:572861240
         */
    }
}

ThreadLocal的结构

  • 每个Thread线程都有一个自己的ThreadLocalMap。ThreadLocalMap其实就是Thead类里面的一个属性,它在 Thread 类中定义,初始值为null,每个线程维护一个彼此独立的ThreadLocalMap。 ThreadLocalMap是ThreadLocal类中的一个静态内部类,维护了以ThreadLocal对象为key、需要存储的数据为value(键值对)的Entry数组。即ThreadLocalMap的中的Entry数组中的每一个键值对中的key为ThreadLocal对象的一个弱引用(对象引用是存在栈中的,而对象是存在堆中的),value为共享变量的一个副本,这个变量副本只在当前线程内起作用。

image.png

ThreadLocal内存泄漏问题

  • 什么是内存泄漏? 内存泄漏是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等后果,内存泄漏的堆积终将导致内存溢出。

  • 内存溢出 没有足够的内存供申请者使用。

  • JAVA 四种引用类型 (1)强引用。 在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。

(2)软引用。 软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。

(3)弱引用。 弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。

(3)虚引用。 虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。

  • 通常在使用线程池时,线程执行完后不被回收,而是进行线程的复用,会被保留在线程池中。所以,Thread有一个强引用指向ThreadLocalMap对象,Entry的键是一个弱引用,指向的是ThreadLocal对象。那么,当ThreadLocal对象不再有任何引用指向它的时候,JVM垃圾回收器就会在下一次GC的时候将ThreadLocal对象回收掉,这样就会导致Entry的键值对的键变为null,当key变为null,那么原本所对应的value再也无法通过key来获取到,那么这个Entry就变为无效的。因为value无法被回收,而一直存在内存中,就很容易导致内存泄漏。另一个问题是如果当前线程执行完相关代码后,很可能会被重新放入线程池中,如果ThreadLocal没有被清除,该线程执行其他代码时,会把上一次的状态带进去。

  • 解决办法:在使用ThreadLocal的set()方法设置值后,要使用ThreadLocal的remove()方法删除对应的Entry。

深入源码

/*
 * JDK1.8
*/
public class ThreadLocal<T> {

     //ThreadLocalMap是ThreadLocal的静态内部类
     static class ThreadLocalMap {

            //Entry是ThreadLocalMap的静态内部类
            static class Entry extends WeakReference<ThreadLocal<?>> {
                //ThreadLocal对应的值
                Object value;
                //Entry构造方法,ThreadLocal是key,value是ThreadLocal对应的值
                Entry(ThreadLocal<?> k, Object v) {
                    //调用父类构造方法,key是ThreadLocal
                    //所以key是一个指向ThreadLocal的弱引用
                    super(k);
                    //设置value
                    value = v;
                }
            }

            //ThreadLocalMap的属性

            //Entry数组的初始容量
            private static final int INITIAL_CAPACITY = 16;

            //Entry数组,长度必须是2的整数次幂
            private Entry[] table;

            //Entry中的元素个数
            private int size = 0;

            //Entry扩容阈值,默认为0
            private int threshold;
            
            略..

     }

     //ThreadLocal的set()方法,设置ThreadLocal对象的值
     public void set(T value) {
         //获取当前线程
         Thread t = Thread.currentThread();
         //获取当前线程的ThreadLocalMap对象
         ThreadLocalMap map = getMap(t);
         if (map != null){
             //如果当前线程ThreadLocalMap不为空
             //则调用ThreadLocalMap的set()方法,将key-value保存到ThreadLocalMap中
             map.set(this, value);
         }
         else{
             //否则,创建ThreadLocalMap
             createMap(t, value);
         }
     }

     //ThreadLocal的成员方法createMap(Thread t, T firstValue)创建ThreadLocalMap
     void createMap(Thread t, T firstValue) {
         //调用ThreadLocalMap的构造方法创建ThreadLocalMap对象并赋值给当前线程的ThreadLocalMap
         t.threadLocals = new ThreadLocalMap(this, firstValue);
     }


     //ThreadLocal的get()方法
     public T get() {
         //获取当前线程
         Thread t = Thread.currentThread();
         //获取当前线程的ThreadLocalMap对象
         ThreadLocalMap map = getMap(t);
         if (map != null) {
             //根据当前的ThreadLocal对象调用ThreadLocalMap的getEntry()方法获取Entry对象
             ThreadLocalMap.Entry e = map.getEntry(this);
             if (e != null) {
                 //Entry非空,即以ThreadLocal对象为key,ThreadLocal对象的值为value的Entry存在
                 @SuppressWarnings("unchecked")
                 //获取Entry对应的值
                 T result = (T)e.value;
                 return result;
             }
         }
         //如果ThreadLocalMap对象为null,则进行初始化
         return setInitialValue();
     }

     private T setInitialValue() {
         //获取初始值,返回null
         T value = initialValue();
         //获取当前线程
         Thread t = Thread.currentThread();
         //获取当前线程的ThreadLocalMap对象
         ThreadLocalMap map = getMap(t);
         if (map != null)
             //如果map不为null,就在ThreadLocalMap中设置键值对
             map.set(this, value);
         else
            //如果ThreadLocalMap为null,则创建ThreadLocalMap
             createMap(t, value);
         return value;
     }
     
     //ThreadLocal的remove()方法
     public void remove() {
          ThreadLocalMap m = getMap(Thread.currentThread());
          if (m != null)
              //如果ThreadLocalMap不为空
              //就调用ThreadLocal的remove()的重载方法remove(ThreadLocal<?> key)
              m.remove(this);
     }

     //ThreadLocal的remove()的重载方法remove(ThreadLocal<?> key)
     private void remove(ThreadLocal<?> key) {
         //将Entry数组赋值给tab
         Entry[] tab = table;
         //获取Entry数组的长度
         int len = tab.length;
         int i = key.threadLocalHashCode & (len-1);
         for (Entry e = tab[i];
              e != null;
              e = tab[i = nextIndex(i, len)]) {
             if (e.get() == key) {
                 e.clear();
                 expungeStaleEntry(i);
                 return;
             }
         }
     }

}

ThreadLocal应用

  1. 解决SimpleDateFormat多线程下线程安全问题

    • SimpleDateFormat多线程下出现的线程安全问题。 例子:
/**
     * @Author GJY
     * @Date 2021/6/28 20:45
     * @Version 1.0
     */
    public class ThreadLocalSimpleDateFormat {
        //共享资源,多线程下simpleDateFormat实例会被多个线程 共享
        private static SimpleDateFormat  sdf = new SimpleDateFormat("yyyy-MM-dd");
        
        public static void main(String[] args) {
            ExecutorService threadPool = Executors.newFixedThreadPool(20);
            for (int i = 0; i < 15; i++) {
                threadPool.execute(()->{
                    for (int i1 = 0; i1 < 10; i1++) {
                        try {
                            //出现线程安全问题
                            System.out.println(Thread.currentThread().getName()+":"+sdf.parse("2021-6-28"));
                        } catch (ParseException e) {
                            e.printStackTrace();
                        }
                    }
                });
            }

            threadPool.shutdown();

        }
    }

原因:SimpleDateFormat转换日期是通过Calendar对象来操作的,SimpleDateFormat是DateFormat这个抽象类的子类,当我们使用simpleDateFormat.parse(String source),首先这个parse方法是SimpleDateFormat的父类DateFormat中的parse(String source)方法,在DateFormat的parse(String source)方法中,再次调用了DateFormat中parse(String source)的重载方法parse(String, ParsePosition)方法来格式化日期,这个重载方法是抽象的,这个重载方法具体由其子类 SimpaleDateFormat 实现的。

代码如下:

  • DateFormat 类
public abstract class DateFormat extends Format {
     //parse重载方法1
     public Date parse(String source) throws ParseException{
        ParsePosition pos = new ParsePosition(0);
        Date result = parse(source, pos);
        if (pos.index == 0)
            throw new ParseException("Unparseable date: \"" + source + "\"" ,
                pos.errorIndex);
        return result;
     }
     //parse重载方法2,是一个抽象方法,由其子类SimpleDateFormat实现
     public abstract Date parse(String source, ParsePosition pos);
}

  • SimpleDateFormat类
//该方法时实现其父类DateFormat中的抽象方法parse(String text, ParsePosition pos)
//最终实现日期转换实在这里实现的
//在这个方法中调用了CalenderBuilder的establish来进行解析,这个方法中又调用了 Calender的clear方法来重置 Calender 实例的属性。
//如果此时线程A执行Calender的clear,且没有设置新值,线程B也进入parse方法用到了SimpleDateFormat对象中的calendar对象,此时就会产生线程安全问题。
@Override
    public Date parse(String text, ParsePosition pos)
    {
        checkNegativeNumberExpression();

        int start = pos.index;
        int oldStart = start;
        int textLength = text.length();

        boolean[] ambiguousYear = {false};

        CalendarBuilder calb = new CalendarBuilder();

        for (int i = 0; i < compiledPattern.length; ) {
            int tag = compiledPattern[i] >>> 8;
            int count = compiledPattern[i++] & 0xff;
            if (count == 255) {
                count = compiledPattern[i++] << 16;
                count |= compiledPattern[i++];
            }

            switch (tag) {
            case TAG_QUOTE_ASCII_CHAR:
                if (start >= textLength || text.charAt(start) != (char)count) {
                    pos.index = oldStart;
                    pos.errorIndex = start;
                    return null;
                }
                start++;
                break;

            case TAG_QUOTE_CHARS:
                while (count-- > 0) {
                    if (start >= textLength || text.charAt(start) != compiledPattern[i++]) {
                        pos.index = oldStart;
                        pos.errorIndex = start;
                        return null;
                    }
                    start++;
                }
                break;

            default:
                
                boolean obeyCount = false;

                boolean useFollowingMinusSignAsDelimiter = false;

                if (i < compiledPattern.length) {
                    int nextTag = compiledPattern[i] >>> 8;
                    if (!(nextTag == TAG_QUOTE_ASCII_CHAR ||
                          nextTag == TAG_QUOTE_CHARS)) {
                        obeyCount = true;
                    }

                    if (hasFollowingMinusSign &&
                        (nextTag == TAG_QUOTE_ASCII_CHAR ||
                         nextTag == TAG_QUOTE_CHARS)) {
                        int c;
                        if (nextTag == TAG_QUOTE_ASCII_CHAR) {
                            c = compiledPattern[i] & 0xff;
                        } else {
                            c = compiledPattern[i+1];
                        }

                        if (c == minusSign) {
                            useFollowingMinusSignAsDelimiter = true;
                        }
                    }
                }
                start = subParse(text, start, tag, count, obeyCount,
                                 ambiguousYear, pos,
                                 useFollowingMinusSignAsDelimiter, calb);
                if (start < 0) {
                    pos.index = oldStart;
                    return null;
                }
            }
        }

        pos.index = start;

        Date parsedDate;
        try {
            parsedDate = calb.establish(calendar).getTime();
            // If the year value is ambiguous,
            // then the two-digit year == the default start year
            if (ambiguousYear[0]) {
                if (parsedDate.before(defaultCenturyStart)) {
                    parsedDate = calb.addYear(100).establish(calendar).getTime();
                }
            }
        }
    
        catch (IllegalArgumentException e) {
            pos.errorIndex = start;
            pos.index = oldStart;
            return null;
        }

        return parsedDate;
    }
  • 使用ThreadLocal解决SimpleDateFormat线程安全问题
/**
 * @Author GJY
 * @Date 2021/6/28 20:45
 * @Version 1.0
 */
public class ThreadLocalSimpleDateFormat {
    private static SimpleDateFormat  sdf = new SimpleDateFormat("yyyy-MM-dd");

    private static ThreadLocal tl = new ThreadLocal<SimpleDateFormat>().withInitial(()->new SimpleDateFormat("yyyy-MM-dd"));


    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(20);
        for (int i = 0; i < 15; i++) {
            threadPool.execute(()->{
                for (int i1 = 0; i1 < 10; i1++) {
                    try {
                        //出现线程安全问题
                        //System.out.println(Thread.currentThread().getName()+":"+sdf.parse("2021-6-28"));

                        //使用ThreadLocal解决
                        //这样的话每一个线程拿到的都是共享资源SimpleDateFormat对象的副本,
                        //各个线程之间对各自副本的操作,不会相互影响
                        SimpleDateFormat spl = (SimpleDateFormat)tl.get();
                        System.out.println(Thread.currentThread().getName()+":"+spl.parse("2021-6-28"));
                        tl.remove();
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }
                }
            });
        }

        threadPool.shutdown();

    }
}
  1. 解决数据库连接问题
  • 如果请求中的一个事务涉及多个数据库操作,但这些数据库操作中的Connection不能从连接池中获得,因为如果是从连接池获得的话,两个数据库操作使到了两个Connection,这时就可能出现不一致,这样是没法完成一个事务的。因此,可以使用ThreadLocal来保证一个事务中的所有数据库操作使用的是同一个数据库连接,当开始一个事务时,会将Connection与ThreadLocal进行绑定,同一个事务的多个数据库操作每次都从ThreadLocal中获取Collection,这样一来,就可以保证他们获取的都是同一个Collection,从而实现控制事务的独立性。因为如果同一个事务中的所有数据库操作所使用的数据库连接是从ThreadLocal中获取的,那么这些数据库操作所使用的Connection都是同一个,并且是当前线程(事务)所独享的。

  • 示例:

public class DBUtil {
    private static final String DRIVER = "com.mysql.cj.jdbc.Driver";
    private static final String USER = "root";
    private static final String PWD = "root";
    private static final String URL = "jdbc:mysql://localhost:3306/blog?serverTimezone = Asia/Shanghai";

    //定义一个数据库连接
    private static Connection conn = null;
    //保证在同一个线程里对数据库的多个操作使用的是同一个数据库连接,以此来控制事务的独立性
    private static ThreadLocal<Connection> connContainer = new ThreadLocal<Connection>();
    //获取连接
    public synchronized static Connection getConnection() {
        //从ThreadLocal中获取连接对象
        conn = connContainer.get();
        try {
            if(conn == null) {
                //如果数据库连接为null,就重新设置
                Class.forName(DRIVER);
                conn = DriverManager.getConnection(URL, USER, PWD);
                //将连接对象设置到ThreadLocalMap中对应的Entry中
                connContainer.set(conn);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return conn;
    }
    //关闭连接
    public static void closeConnection() {
        if(conn != null) {
            try {
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        System.out.println(getConnection());
    }
}
  • 多个数据库操作
public class Dao {
    public void insert() {
        //获取连接
        Connection conn = DBUtil.getConnection();
        System.out.println("插入操作->" + Thread.currentThread().getName() + conn);
    }
    public void delete() {
        //获取连接
        Connection conn = DBUtil.getConnection();
        System.out.println("删除操作->" + Thread.currentThread().getName() + conn);
    }
    public void update() {
        //获取连接
        Connection conn = DBUtil.getConnection();
        System.out.println("更新操作->" + Thread.currentThread().getName() + conn);
    }
    public void select() {
        //获取连接
        Connection conn = DBUtil.getConnection();
        System.out.println("查询操作->" + Thread.currentThread().getName() + conn);
    }
}
  • 测试
public class Test {
    public static void main(String[] args) {

        for (int i = 0; i < 3; i++) {
            new Thread(new Runnable() {
                Dao dao = new Dao();
                @Override
                public void run() {
                    dao.insert();
                    dao.delete();
                    dao.update();
                    dao.select();
                }
            }).start();
        }
    }
}

ThreadLocal与Synchronized的区别:

synchronized的原理:同步机制采用“以时间换空间”的方式,只提供了一份变量,让不同的线程排队访问。侧重点:多个线程之间访问资源的同步

ThreadLocal:ThreadLocal采用“以空间换时间”的方式,为每一个线程都提供了一份变量副本,从而实现同时访问而互相不干扰。 侧重点:多线程让每一个线程之间的数据相互隔离