ThreadLocal原理与使用

1、ThreadLocal 是什么?

在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。

ThreadLocal并不是一个Thread,而是Thread的局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些。

在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal。API方法也相应进行了调整,新版本的API方法分别是void set(T value)、T get()以及T initialValue()。

  • their normal counterparts in that each thread that accesses one (via its
  • {@code get} or {@code set} method) has its own, independently initialized
  • copy of the variable. {@code ThreadLocal} instances are typically private
  • static fields in classes that wish to associate state with a thread (e.g.,
  • a user ID or Transaction ID)

它大致的实现思路是怎样的?

Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap,ThreadLocalMap有自己的独立实现。每个线程在往某个ThreadLocal里塞值的时候,都会往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。

2、ThreadLocal 的作用?

ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。

ThreadLocal的应用场景?

在Java的多线程编程中,为保证多个线程对共享变量的安全访问,通常会使用synchronized来保证同一时刻只有一个线程对共享变量进行操作。这种情况下可以将类变量放到ThreadLocal类型的对象中,使变量在每个线程中都有独立拷贝,不会出现一个线程读取变量时而被另一个线程修改的现象。最常见的ThreadLocal使用场景为用来解决数据库连接、Session管理等。在下面会例举几个场景。

为什么要弱引用

如果不问不答为什么是这样的定义形式,为什么要用弱引用,等于没读懂源码。
因为如果这里使用普通的key-value形式来定义存储结构,实质上就会造成节点的生命周期与线程强绑定,只要线程没有销毁,那么节点在GC分析中一直处于可达状态,没办法被回收,而程序本身也无法判断是否可以清理节点。弱引用是Java中四档引用的第三档,比软引用更加弱一些,如果一个对象没有强引用链可达,那么一般活不过下一次GC。当某个ThreadLocal已经没有强引用可达,则随着它被垃圾回收,在ThreadLocalMap里对应的Entry的键值会失效,这为ThreadLocalMap本身的垃圾清理提供了便利。

3、ThreadLocal的内部原理

我们从源码中了解ThreadLocal的原理,下面来看一下具体ThreadLocal是如何实现的。

ThreadLocal类中提供了几个方法:

1.public T get() { }

2.public void set(T value) { }

3.public void remove() { }

4.protected T initialValue(){ }

get()方法是用来获取ThreadLocal在当前线程中保存的变量副本,set()用来设置当前线程中变量的副本,remove()用来移除当前线程中变量的副本,initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法,下面会详细说明。

3.1 先看下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();
}

第一句是取得当前线程,然后通过getMap(t)方法获取到一个map,map的类型为ThreadLocalMap。然后接着下面获取到<key,value>键值对,注意这里获取键值对传进去的是 this,而不是当前线程t。 如果获取成功,则返回value值。如果map为空,则调用setInitialValue方法返回value。

看看getMap(t)做了些什么

 ThreadLocalMap getMap(Thread t) {
 	return t.threadLocals;
 }

在getMap中,是调用当期线程t,返回当前线程t中的一个成员变量threadLocals。 那么我们继续取Thread类中取看一下成员变量threadLocals是什么?继续查看源码

/**
 * 这个是一个内部类
 */
 static class ThreadLocalMap {

	 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类的一个内部类,我们继续取看ThreadLocalMap的实现。

再看setInitialValue()方法

private T setInitialValue() {
	T value = initialValue();
	Thread t = Thread.currentThread();
	ThreadLocalMap map = getMap(t);
	if (map != null)
	    map.set(this, value);
	else
	    createMap(t, value);
	return value;
}

setInitialValue()很容易理解,就是如果map不为空,就设置键值对,为空,再创建Map,看一下createMap的实现。

 void createMap(Thread t, T firstValue) {
 	t.threadLocals = new ThreadLocalMap(this, firstValue);
 }

4、实例

注意:如果使用ThreadLocal时,在进行get之前,必须先set,否则会报空指针异常

public class ThreadLocalExsample {

    ThreadLocal<Long> longLocal = new ThreadLocal<>();
    public void set() {
        longLocal.set(Thread.currentThread().getId());
    }
    public long getLong() {
        return longLocal.get();
    }
 	public static void main(String[] args) {
        ThreadLocalExsample test = new ThreadLocalExsample();
        //注意:没有set之前,直接get,报null异常了
        System.out.println("-------threadLocal value-------" + test.getLong());
    }
}

ThreadLocal的应用场景# 数据库连接

public Connection initialValue() {
	return DriverManager.getConnection(DB_URL);
};  

public static Connection getConnection() {  
 	return connectionHolder.get();
} 

ThreadLocal的应用场景# Session管理

public static Session getSession() throws InfrastructureException {  
	Session s = (Session) threadSession.get();
	try {
		if (s == null) {
			s = getSessionFactory().openSession();
			threadSession.set(s);
		}
	} catch (HibernateException ex) {
		throw new InfrastructureException(ex);
	}
	return s;
}

ThreadLocal的应用场景# 多线程

public class ThreadLocalExsample {
    /**
     * 创建了一个MyRunnable实例,并将该实例作为参数传递给两个线程。两个线程分别执行run()方法,
     * 并且都在ThreadLocal实例上保存了不同的值。如果它们访问的不是ThreadLocal对象并且调用的set()方法被同步了,
     * 则第二个线程会覆盖掉第一个线程设置的值。但是,由于它们访问的是一个ThreadLocal对象,
     * 因此这两个线程都无法看到对方保存的值。也就是说,它们存取的是两个不同的值。
     */
    public static class MyRunnable implements Runnable {
        /**
         * 例化了一个ThreadLocal对象。我们只需要实例化对象一次,并且也不需要知道它是被哪个线程实例化。
         * 虽然所有的线程都能访问到这个ThreadLocal实例,但是每个线程却只能访问到自己通过调用ThreadLocal的
         * set()方法设置的值。即使是两个不同的线程在同一个ThreadLocal对象上设置了不同的值,
         * 他们仍然无法访问到对方的值。
         */
        private final ThreadLocal<Integer> threadLocal1 = new ThreadLocal<>();
        private final ThreadLocal<BigDecimal> threadLocal2 = new ThreadLocal<>();
        private final ThreadLocal<Integer> threadLocal3 = new ThreadLocal<>();


        @Override
        public void run() {
            //一旦创建了一个ThreadLocal变量,你可以通过如下代码设置某个需要保存的值
            threadLocal1.set((int) (Math.random() * 100D));
            threadLocal2.set(BigDecimal.TEN);
            System.out.println(Thread.currentThread().getId() + "-------赋值成功-------");
            //threadLocal.set(BigDecimal.ONE);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
            }
            //可以通过下面方法读取保存在ThreadLocal变量中的值
            System.out.println(Thread.currentThread().getId() + "-------threadLocal value-------" + threadLocal1.get());
            System.out.println(Thread.currentThread().getId() + "-------threadLocal value-------" + threadLocal2.get());
            System.out.println(Thread.currentThread().getId() + "-------threadLocal value-------" + threadLocal3.get());

        }
    }

    public static void main(String[] args) {
        MyRunnable sharedRunnableInstance = new MyRunnable();
        Thread thread1 = new Thread(sharedRunnableInstance);
        Thread thread2 = new Thread(sharedRunnableInstance);
        thread1.start();
        thread2.start();
    }
}

运行结果:

20-------赋值成功-------
21-------赋值成功-------
20-------threadLocal value-------31
21-------threadLocal value-------50
20-------threadLocal value-------10
21-------threadLocal value-------10
21-------threadLocal value-------null
20-------threadLocal value-------null

得出结论:

ThreadLocal 中 set 和 get 操作的都是对应线程的 table数组,因此在不同的线程中访问同一个 ThreadLocal 对象的 set 和 get 进行存取数据是不会相互干扰的。

5、总结

在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。 初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。 然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。

实际的通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中的;

为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象,因为每个线程中可有多个threadLocal变量,就像上面代码中的longLocal和stringLocal;

在进行get之前,必须先set,否则会报空指针异常;如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。 因为在上面的代码分析过程中,我们发现如果没有先set的话,即在map中查找不到对应的存储,则会通过调用setInitialValue方法返回i,而在setInitialValue方法中,有一个语句是T value = initialValue(), 而默认情况下,initialValue方法返回的是null。

进一步理解可参考:ThreadLocal源码解读

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值