多线程之线程局部变量ThreadLocal及原理
2017-07-16 20:50
441 查看
一、线程局部变量ThreadLocal
ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。既然是只有当前线程可以访问的数据,自然是线程安全的。主要方法:
initialValue()方法可以重写,它默认是返回null。
下面来看一个例子:
public class ThreadLocalTest { private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static class ParseDate implements Runnable{ int i = 0; public ParseDate(int i){this.i = i;} @Override public void run() { try{ Date t = sdf.parse("2017-07-16 10:34:" + i % 60); System.out.println(i + ":" + t); }catch (ParseException e){ e.printStackTrace(); } } } public static void main(String[] args) { ExecutorService es = Executors.newFixedThreadPool(10); for(int i = 0;i<10;i++){ es.execute(new ParseDate(i)); } es.shutdown(); } }执行上面的程序可以回得到下面的异常:
因为SimpleDateFormat.parse方法并不是线程安全的,因此在线程池中共享这个对象必然导致错误。
一种可行的方法就是加锁:
public class ThreadLocalTest { private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static class ParseDate implements Runnable{ int i = 0; public ParseDate(int i){this.i = i;} private static ReentrantLock lock = new ReentrantLock(); @Override public void run() { try{ lock.lock(); Date t = sdf.parse("2017-07-16 10:34:" + i % 60); System.out.println(i + ":" + t); lock.unlock(); }catch (ParseException e){ e.printStackTrace(); } } } public static void main(String[] args) { ExecutorService es = Executors.newFixedThreadPool(10); for(int i = 0;i<10;i++){ es.execute(new ParseDate(i)); } es.shutdown(); } }运行结果:
我们也可以用ThreadLocal为每一个线程都产生一个SimpleDateFormat对象实例:
public class ThreadLocalTest { static ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<>(); public static class ParseDate implements Runnable{ int i = 0; public ParseDate(int i){this.i = i;} @Override public void run() { try{ if(tl.get() == null){ tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); } Date t = tl.get().parse("2017-07-16 10:34:" + i % 60); System.out.println(i + ":" + t); }catch (ParseException e){ e.printStackTrace(); } } } public static void main(String[] args) { ExecutorService es = Executors.newFixedThreadPool(10); for(int i = 0;i<10;i++){ es.execute(new ParseDate(i)); } es.shutdown(); } }运行结果也是OK的。
这里要注意的是:需要自己为每个线程分配不同的SimpleDateFormat对象,ThreadLocal只是起到了简单的容器的作用。如果在应用上为每一个线程分配了相同的对象实例,那么ThreadLocal也不能保证线程安全。
看到这里可能你会问:上面这个例子,把ThreadLocal换成,直接在ParseDate中创建一个成员变量Private SimpleDateFormat sdf = new SimpleDateFormat(“yyyy-MM-dd HH:mm:dd”)不也可以达到同样的效果。当然这样效果跟ThreadLocal是一样的,ThreadLocal只是提供了一个容器,容纳这些需要在每个线程上都互不干扰的变量的副本。
二、ThreadLocal源码分析
下面我们来分析下ThreadLocal的源码,看看是怎么保证这些对象只被当前线程所访问。首先我们要先了解ThreadLocal中的一个静态内部类:ThreadLocalMap
ThreadLocalMap是一个类似HashMap的东西,更准确的说是WeakHashMap。
进一步查看ThreadLocalMap的实现,可以看到它由一系列的Entry构成:
static class Entry extends WeakReference<ThreadLocal> { Object value; Entry(ThreadLocal k, Object v) { super(k); value = v; } }可以看到ThreadLocal的实现使用了弱引用。为什么要使用弱引用呢?
先看看WeakRefercence的特点:
WeakReference是Java语言规范中为了区别直接的对象引用(程序中通过构造函数声明出来的对象引用)而定义的另外一种引用关系。WeakReference标志性的特点是:reference实例不会影响到被应用对象的GC回收行为(即只要对象被除WeakReference对象之外所有的对象解除引用后,该对象便可以被GC回收),只不过在被对象回收之后,reference实例想获得被应用的对象时程序会返回null。
ThreadLocalMap中的每个Entry都引用了ThreadLocal实例,如果ThreadLocal实例是强引用,那么即使把ThreadLocal的实例设为null,但这个实例在ThreadLocalMap中还有引用,导致无法被GC回收。声明为WeakReference的话,ThreadLocal实例在ThreadLocalMap中的引用就为弱引用,那么把ThreadLocal实例设为null后,它就可以被GC回收了。当然,如果使用完ThreadLocal实例的话,最好是用threadLocal.remove()来代替threadLocal
= null。
主要看ThreadLocal的set()和get()方法。
首先我们要知道每个Thread实例都有一个ThreadLocalMap类型的成员变量:
ThreadLocal.ThreadLocalMap threadLocals = null;
set()方法:
public void set(T value) { Thread t = Thread.currentThread(); //拿到当前线程 ThreadLocalMap map = getMap(t); //拿到当前线程t的那个ThreadLocalMap类型的成员变量 if (map != null) map.set(this, value); //map不为null,就把键为该threadLocal的entry的值设置为value else createMap(t, value);//map为null,就为当前线程的那个成员变量new一个ThreadLocalMap并加入一个键为该threadLocal,值为value的Entry。 }
get()方法:
public T get() { Thread t = Thread.currentThread(); //拿到当前线程 ThreadLocalMap map = getMap(t); //拿到当前线程的那个ThreadLocalMap类型的成员变量 if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this);//map不为null,取出键为该threadLocal的Entry对象 if (e != null) return (T)e.value; //存在这个Entry对象,就返回它的值 } return setInitialValue(); //map为null,就为当前线程的那个成员变量new一个ThreadLocalMap并加入一个键为该threadLocal,值为初始值的Entry }
总结一下:
1、变量的副本是通过ThreadLocalMap来存储,键为ThreadLocal实例(每个线程可以有多个ThreadLocal实例),值为变量的值。
2、每个线程都有一个ThreadLocalMap类型的threadLocals 变量,实际也就存储在这。
3、一般要在get()之前先set(),否则会抛出空指针异常,除非重写initialValue方法。
相关文章推荐
- ThreadLocal维护线程局部 变量或线程局部对象
- ThreadLocal变量存储的实际原理与线程安全原因
- (三) Java多线程详解之线程范围内共享变量及ThreadLocal类使用
- java多线程学习之通过ThreadLocal实现每个线程拥有自己的变量
- 多线程,为每个线程维护变量副本的ThreadLocal,ThreadLocal会造成内存泄漏吗?
- java学习——ThreadLocal 线程局部 (thread-local) 变量的使用
- 【Android应用源码分析】Java多线程:线程本地变量ThreadLocal源码分析
- 多线程 : ThreadLocal 实现线程间共享变量隔离例子
- Java多线程——线程范围内共享变量和ThreadLocal
- 【Java多线程与并发库】05 线程范围内共享变量ThreadLocal
- 多线程,为每个线程维护变量副本的ThreadLocal,ThreadLocal会造成内存泄漏吗?
- posix多线程有感--线程高级编程(条件变量属性)
- java多线程模式ThreadLocal原理简述及其使用详解
- 多线程系列八:线程安全、Java内存模型(JMM)、底层实现原理
- 【Python】[进程和线程]多进程,多线程,ThreadLocal,进程VS.线程,分布式进程
- 多线程——线程范围内变量的共享
- ThreadLocal---线程本地变量
- 多线程(三) 实现线程范围内模块之间共享数据及线程间数据独立(ThreadLocal)
- ThreadLocal实现线程范围的共享变量
- Java并发(六):线程本地变量ThreadLocal、再聊线程池