ThreadLocal使用及原理
阅读原文时间:2021年04月20日阅读:1

1.ThreadLocal的简单使用

从ThreadLocal的名字上可以看到,这是个线程的局部变量。也就是说只有当前线程可以访问,既然是只有当前线程池可以访问的数据自然是线程安全的。

package thread;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantLock;

public class ThreadLocalDemo {

    private static ThreadLocal<SimpleDateFormat> t1 = new ThreadLocal<SimpleDateFormat>();

//如果不想要下面的t1.set则可以通过这种方式构造ThreadLocal.
//    private static ThreadLocal<SimpleDateFormat> t1 = new ThreadLocal<SimpleDateFormat>() {
//        public SimpleDateFormat initialValue() {
//            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//        }
//    };
    private static final SimpleDateFormat sm = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    private static ReentrantLock lock = new ReentrantLock();

    public static class ParseDate implements Runnable {

        int i = 0;

        public ParseDate(int i) {
            this.i = i;
        }

        // @Override
        // public void run() {
        // try {
        // lock.lock();
        // Date date = sm.parse("2018-8-12 17:30:" + i + "");
        // System.out.println(i + "--" + date);
        // lock.unlock();
        // Thread.sleep(1000);
        // } catch (ParseException e) {
        // // TODO Auto-generated catch block
        // e.printStackTrace();
        // } catch (InterruptedException e) {
        // // TODO Auto-generated catch block
        // e.printStackTrace();
        // }
        // }

        // @Override
        // public void run() {
        // try {
        // synchronized (ParseDate.class) {
        // Date date = sm.parse("2018-8-12 17:30:"+i+"");
        // System.out.println(i+"--"+date);
        // }
        // Thread.sleep(1000);
        // } catch (ParseException e) {
        // // TODO Auto-generated catch block
        // e.printStackTrace();
        // } catch (InterruptedException e) {
        // // TODO Auto-generated catch block
        // e.printStackTrace();
        // }
        // }

        @Override
        public void run() {
            if (t1.get() == null)
                t1.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

            try {
                Date date = t1.get().parse("2018-8-12 17:30:" + i + "");
                System.out.println(i + "--" + date);
                Thread.sleep(1000);
            } catch (ParseException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

    }

    public static void main(String[] args) {
        ExecutorService es = Executors.newFixedThreadPool(10);

        for (int i = 0; i < 30; i++)
            es.execute(new ParseDate(i));
    }

}

可以看到输出结果也是正确地。这里我有三种方式保证线程的安全性,锁和重入锁还有就是ThreadLocal。在线程ThreadLocal中if (t1.get() == null)
                t1.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

这两行代码的含义是,如果当前线程不持有SimpleDateFormat对象的实例则新建一个并将其设置到当前线程中。如果有则直接使用。从这里也可以看到为每个线程人手分配一个对象的工作并不是由ThreadLocal来完成的而是在应用层保证的。如果在应用上为每一个线程分配相同的对象实例,那么ThreadLocal也不能保证线程安全。(注意:为每个线程分配不同的对象,需要在应用层保证。ThreadLocal只是起到简单容器的作用。)

2.ThreadLocal的实现原理

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

首先它会获取当前线程对象,然后通过getMap()拿到线程ThreadLocalMap,然后将值设入ThreadLocalMap中。(ThreadLocalMap可以简单的理解为一个Map,它是定义在Thread内部的成员。)

设置到ThreadLocal中的数据,也是写入了threadLocals这个Map中。其中key为ThreadLocal当前对象,value则是我们需要的值这里是:new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")。而threadLocals本身就保存了当前自己所在线程的所有局部变量,也就是一个ThreadLocal变量的集合。

get()操作:

 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();
    }

它也是先获取当前线程的ThreadLocalMap对象,然后通过将自己作为key取得内部的实际数据。

因为这些变量是维护在Thread类内部的,所以这意味着只要线程不退出,对象的引用将一直存在。而当线程退出时,Thread类会进行清理工作:

因此如果使用线程池,这就意味着当前线程未必会退出(比如固定大小的线程池。)如果这样将一些太大的对象设置到ThreadLocal中(实际会保存在线程持有的threadLocals Map中)。可能会使系统出现内存泄露的可能。

此时如果希望及时的回收对象,最好使用ThreadLocal.remove()方法将这个变量移除。或者可以直接通过将ThreadLocal对象设为null的方式,让系统自行垃圾回收。

ThreadLocal的使用场景一般是共享对象对于竞争的处理容易引起性能的损失,这时候可以考虑使用ThreadLocal为每个线程分配单独的对象。