• 使用ThreadLocal解决多个线程共享变量的线程安全问题
  • 发布于 2个月前
  • 575 热度
    0 评论
  • 雾月
  • 0 粉丝 27 篇博客
  •   
一、ThreadLocal 简介
1.  ThreadLocal 是什么?
ThreadLocal 字面意思是本地线程,其实更准确来说是线程局部变量,线程类 Thread 有个变量叫做 threadLocals,其类型就是ThreadLocal.ThreadLocalMap 类型,他其实不是一个 Map 类型,但可以暂时理解它是一个Map,键为 ThreadLocal 对象,值就是要存入的value。

2.  ThreadLocal 作用
ThreadLocal 就是用于线程间的数据隔离的。ThreadLocal 提供线程内部的局部变量,在本线程内随时随地可取,隔离其他线程,获取保存的值时非常方便,ThreadLocal 为变量在每个线程中都创建了一个副本,每个线程就可以很方便的访问自己内部的副本变量。

3. ThreadLocal的两大使用场景
每个线程需要一个独享的对象(通常是指工具类对象),每个线程内有自己的实例副本,不与其他线程共享;
每个线程内需要一个变量作为全局共用(当前线程内全局共用),可以让不同的方法直接使用,避免传递参数的麻烦;
总之,就是解决多个线程的共享变量的线程安全问题;

二、使用场景案例
1. 每个线程需要一个独享的对象
下面的案例是关于 SimpleDateFormat 工具类,在多线程共享时的线程安全问题
线程不安全的代码:
public class MyThreadLocal {
    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
    public static String date(int seconds) {
        Date date = new Date(1000 * seconds);
        return sdf.format(date);
    }

    public static void main(String[] args) {
        // 堆代码 duidaima.com
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    String date = date(finalI);
                    System.out.println(date);
                }
            });
        }
        executorService.shutdown();
    }
}
这里为了提高性能,所以将 SimpleDateFormat 作为 static 属性,多线程共享,但是这样就会出现安全问题,打印结果如下:

由结果可以看出,打印出了两个相同的时间,说明发生了运行结果错误,问题代码就发生在sdf.format(date),这行代码不是线程安全的。

解决方案有两个:使用同步锁 synchronized 和使用 ThreadLocal 解决。
(1)同步锁 synchronized
public class MyThreadLocal {

    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

    public static String date(int seconds) {
        Date date = new Date(1000 * seconds);
        String s = null;
        synchronized (MyThreadLocal.class) {
            s = sdf.format(date);
        }
//        Date date = new Date(1000 * seconds);
        return s;
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    String date = date(finalI);
                    System.out.println(date);
                }
            });
        }
        executorService.shutdown();
    }
}
(2) 用ThreadLocal解决
public class MyThreadLocal1 {

//    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        private static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));

    public static String date(int seconds) {
        Date date = new Date(1000 * seconds);
        SimpleDateFormat sdf = simpleDateFormatThreadLocal.get();
        return sdf.format(date);
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    String date = date(finalI);
                    System.out.println(date);
                }
            });
        }
        executorService.shutdown();
    }
}
打印结果:

两个方案都可以解决线程安全问题,但是synchronized加锁的方法,由于同一时刻只有一个线程执行,所以效率低下;ThreadLocal方法在多线程并行的情况下,由于每个线程内都有自己独享的对象,也不会有线程安全问题。

2. 每个线程内需要一个变量作为全局共用
在应用开发中,有些参数需要被线程内许多方法使用,如权限管理,很多的方法都需要验证当前线程用户的身份信息
案例内容:一个系统中,user对象需要在很多server中进行使用
方案1 将user作为参数层层传递,从service1->service2->service3以此类推。这样会导致代码冗余且难以维护
方案2 定义一个全局的static 的user,想要拿的时候直接获取。但这是一种错误的方案!!因为我们现在的场景是多用户的系统,每个线程对应着不同的用户,每个线程的user是不同的

方案3 定义一个UserMap,每次访问从Map中获取用户的信息,多线程访问下加锁或者使用ConcurrentHashMap,但是对性能有影响
方案4 利用ThreadLocal,不需要锁,不影响性能。ThreadLocal 主打的就是同一个线程内不同方法间的共享。

所以优选选择方案4,代码演示如下:
/**
 * 避免传递参数的麻烦
 * ThreadLocalan案例2
 * @ 堆代码 duidaima.com
 * @since 1.0.0
 */
public class ThreadLocalNormalUsage06 {
    public static void main(String[] args) {
        new Service1().process();
    }
}

class Service1 {
    public void process() {
        User user = new User("周星驰");
        UserContextHolder.holder.set(user);
        new Service2().process();
    }
}

class Service2 {
    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("service2:" + user.name);
        UserContextHolder.holder.remove();
        UserContextHolder.holder.set(new User("古天乐"));
        new Service3().process();
    }
}

class Service3 {
    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("service3:" + user.name);
    }
}

class UserContextHolder {
    public static ThreadLocal<User> holder
            = new ThreadLocal<>();
}

class User {
    String name;
    public User(String name) {
        this.name = name;
    }
}
打印结果:

通过以上两个案例,我们可以了解到 ThreadLocal 的两个作用:
.让对象在线程之间隔离;

.在任何方法中都可以直接获取到对象;


3. ThreadLocal 的两个使用方式
上面两个使用场景中,ThreadLocal 的初始化方式也是分为两种:
场景1:initialValue 如果在ThreadLocal第一次get的时候把对象给初始化时使用,对象的初始化时机受控制
场景2:set 如果需要保存到ThreadLocal的对象的生成时机不由我们随意控制,我们用set方法放进去,再用get方法取出来;
4. ThreadLocal的好处
.线程安全
.不需要加锁,执行效率高
.更高效的利用内存,节省开销
.相比于每个任务都新建一个SimpleDateFormat,显然用ThreadLocal可以节省内存和开销
.避免传参的繁琐操作

无论是场景一的工具类,还是场景二的用户名,都可以在任务地方直接通过ThreadLocal拿到,再也不需要在方法的形参中再定义传入相同的参数。ThreadLocal使代码耦合度更低,更优雅。

三、ThreadLocal 原理
1. ThreadLocal 与 Thread 的关系

每一个Thread里面都有一个ThreadLocalMap类型的threadlocals成员变量,它可以存储很多的ThreadLocal对象,因为一个线程可能有多个ThreadLocal对象,其中对象引用名称作为key;
ThreadLocalMap:也就是Thread.threadLocals,是Thread里的一个成员变量,里面最重要的是一个键值对数组Entry[] table,可以认为是一个map,键值对;键:这个ThreadLocal;值:实际需要的成员变量;
2. ThreadLocal  源码
(1) initialValue() 方法源码
/**
     * Returns the current thread's "initial value" for this
     * thread-local variable.  This method will be invoked the first
     * time a thread accesses the variable with the {@link #get}
     * method, unless the thread previously invoked the {@link #set}
     * method, in which case the {@code initialValue} method will not
     * be invoked for the thread.  Normally, this method is invoked at
     * most once per thread, but it may be invoked again in case of
     * subsequent invocations of {@link #remove} followed by {@link #get}.
     *
     * <p>This implementation simply returns {@code null}; if the
     * programmer desires thread-local variables to have an initial
     * value other than {@code null}, {@code ThreadLocal} must be
     * subclassed, and this method overridden.  Typically, an
     * anonymous inner class will be used.
     *
     * @return the initial value for this thread-local
     */
    protected T initialValue() {
        return null;
    }
该方法返回当前线程对应的初始值,使用了延迟加载,当调用get()方法是才会触发
当第一次使用get()方法时会调用此方法,如果调用前用set()方法设置了值就不会调用
当调用remove()方法后再次调用get()方法依然会调用initialize
如果不重写initialValue方法,直接调用get()会返回null
(2) get 方法的实现:
    /**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
方法里面第一行获取当前线程,然后通过 getMap(t) 方法获取 ThreadLocal.ThreadLocalMap,所有的变量数据都存在该 map,map 的具体类型是一个 Entry 数组。然后接着下面获取到 Entry 键值对,注意这里获取 Entry 时参数传进去的是 this,即 ThreadLocal 实例,而不是当前线程 t。如果获取成功,则返回 value 值。

如果 map 为空,则调用 setInitialValue 方法返回一个初始 value,其实这个默认初始 value 为 null。

(3) 接着来看一下 getMap 方法做了什么:
/* ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
在 getMap 中,是调用当期线程 t,返回当前线程t中的一个成员变量 threadLocals,类型为 ThreadLocal.ThreadLocalMap。就是上面提到的每一个线程都自带一个 ThreadLocalMap 类型的成员变量。

(4) 继续来看 ThreadLocalMap 的实现:
static class ThreadLocalMap {
        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
ThreadLocalMap 是 ThreadLocal 的一个静态内部类,其内部主要是一个 Entry 数组存储数据(并不是一个 map 类型)。

ThreadLocalMap 的 Entry 继承了 WeakReference,用来实现弱引用,被弱引用关联的对象(其实就是 ThreadLocal 对象)只能生存到下一次垃圾收集发生之前,并且使用 ThreadLocal 对象的 HashCode 的散列值计算得出的 Entry 数组的下标 i,这里不同对象可能存在相同的下标 i,对此 set() 方法处理逻辑是:下标加一,直到第一个要插入的位置为空。

(5) set()方法
/**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
为这个线程设置一个新值

(6) remove()方法
/**
     * Removes the current thread's value for this thread-local
     * variable.  If this thread-local variable is subsequently
     * {@linkplain #get read} by the current thread, its value will be
     * reinitialized by invoking its {@link #initialValue} method,
     * unless its value is {@linkplain #set set} by the current thread
     * in the interim.  This may result in multiple invocations of the
     * {@code initialValue} method in the current thread.
     *
     * @since 1.5
     */
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
删除线程中对应的值,remove()方法也是在ThreadlocalMap中进行操作,传入当前ThreadLocal对象的引用,删除map中的value的值,不是删除整个ThreadLocalMap对象,而是根据this(也就是当前ThreadLocal对象)来删除对应的threadLocal对象

四、ThreadLocal 注意点
1. 在使用 ThreadLocal时的注意事项
.最后一次使用之后应该手动的调用remove()方法,防止内存泄露
.如果可以不使用ThreadLocal就解决问题,不要强行使用(如:任务数很少时)
.优先使用框架的支持,而不是自己创造,例如在Spring中,如果可以使用 RequestContextHolder,那么就不需要自己去维护ThreadLocal,因为自己可能会忘记调用remove方法,造成内存泄漏;
2  ThreadLocal  为什么会发生内存泄露?
内存泄漏:某个对象不再有用,但是占用的内存不能被回收;
源码:
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
我们知道,ThreadLocal 是基于 ThreadLocalMap 实现的,这个 Map 的 Entry 继承了 WeakReference,而 Entry 对象中的 key 使用了 WeakReference 封装,也就是说 Entry 中的 key 是一个弱引用类型,而弱引用类型只能存活在下次 GC 之前。

如果一个线程调用 ThreadLocal 的 set 设置变量,当前 ThreadLocalMap 则新增一条记录,但发生一次垃圾回收,此时 key 值被回收,而 value 值依然存在内存中,如果线程一直存在(比如在线程池中),那么 value 值将一直被引用,不能被回收。因为存在一条引用链的关系:Thread-->ThreadLocalMap-->Entry-->Value。造成内存泄漏,甚至有可能造成内存溢出OOM。

如何避免内存泄漏:当使用完了对应的ThreadLocal,主动调用remove方法删除。

3. 空指针异常问题
代码演示:
public class ThreadLocalNPE {

   ThreadLocal<Long> tl =  new ThreadLocal();

   public void set(){
       tl.set(Thread.currentThread().getId());
   }
   public long get(){
       return tl.get();
   }

    public static void main(String[] args) {
        ThreadLocalNPE item = new ThreadLocalNPE();
        System.out.println(item.get());

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                item.set();
                System.out.println(item.get());
            }
        });
        thread.start();
    }
}
打印结果:

这里的,get()方法出现了NPE异常,那为什么呢?

ThreadLocal在实例化时是指定存储的包装类型 Long (ThreadLocaltl =  new ThreadLocal()), 而演示代码中的 get() 方法返回的是基本类型 long,那么他在执行 initialValue() 时返回的是 Long,然后自动拆箱,转为 long 基本类型,这里就出现了错误,因为在返回Long 类型时就是null了,对 null进行拆箱返回基本类型,就会出现空指针这异常!

通过修改get()方法的返回值 ,从long —> Long,就可以解决问题
用户评论