您的位置:首页 > 编程语言 > Java开发

Java并发编程:深入剖析ThreadLocal

2017-05-08 22:37 459 查看

一. 对ThreadLocal的理解

import java.util.Date;
import java.util.concurrent.TimeUnit;

class UnsafeTask implements Runnable{
private Date startDate;

@Override
public void run() {
startDate = new Date();
System.out.printf("Starting Thread: %s : %s\n",Thread. currentThread().getId(),startDate);
try {
//            TimeUnit.SECONDS.sleep((int)Math.rint(Math.random()*10));
TimeUnit.SECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("Thread Finished: %s : %s\n",Thread. currentThread().getId(),startDate);
}
}
public class Test {
public static void main(String[] args) {
UnsafeTask task = new UnsafeTask();
for (int i = 0; i < 5; i++){
Thread thread = new Thread(task);
thread.start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}


每个线程有着不同的开始时间,但是全部都有相同的结束时间。
Starting Thread: 11 : Mon May 08 22:20:43 CST 2017
Starting Thread: 12 : Mon May 08 22:20:45 CST 2017
Starting Thread: 13 : Mon May 08 22:20:47 CST 2017
Starting Thread: 14 : Mon May 08 22:20:49 CST 2017
Starting Thread: 15 : Mon May 08 22:20:51 CST 2017
Thread Finished: 11 : Mon May 08 22:20:51 CST 2017
Thread Finished: 12 : Mon May 08 22:20:51 CST 2017
Thread Finished: 13 : Mon May 08 22:20:51 CST 2017
Thread Finished: 14 : Mon May 08 22:20:51 CST 2017
Thread Finished: 15 : Mon May 08 22:20:51 CST 2017


使用本地线程变量机制来解决这个问题:

import java.util.Date;
import java.util.concurrent.TimeUnit;

class UnsafeTask implements Runnable{
private static ThreadLocal<Date> startDate= new ThreadLocal<Date>() {
protected Date initialValue(){
return new Date();
}
};

@Override
public void run() {
System.out.printf("Starting Thread: %s : %s\n",Thread. currentThread().getId(),startDate.get());
try {
//            TimeUnit.SECONDS.sleep((int)Math.rint(Math.random()*10));
TimeUnit.SECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("Thread Finished: %s : %s\n",Thread. currentThread().getId(),startDate.get());
}
}
public class Test {
public static void main(String[] args) {
UnsafeTask task = new UnsafeTask();
for (int i = 0; i < 5; i++){
Thread thread = new Thread(task);
thread.start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}


Starting Thread: 11 : Mon May 08 22:26:55 CST 2017
Starting Thread: 12 : Mon May 08 22:26:57 CST 2017
Starting Thread: 13 : Mon May 08 22:26:59 CST 2017
Starting Thread: 14 : Mon May 08 22:27:01 CST 2017
Starting Thread: 15 : Mon May 08 22:27:03 CST 2017
Thread Finished: 11 : Mon May 08 22:26:55 CST 2017
Thread Finished: 12 : Mon May 08 22:26:57 CST 2017
Thread Finished: 13 : Mon May 08 22:26:59 CST 2017
Thread Finished: 14 : Mon May 08 22:27:01 CST 2017
Thread Finished: 15 : Mon May 08 22:27:03 CST 2017


本地线程变量为每个使用这些变量的线程储存属性值。可以用 get() 方法读取值和使用 set() 方法改变值。 如果第一次你访问本地线程变量的值,如果没有值给当前的线程对象,那么本地线程变量会调用 initialValue() 方法来设置值给线程并返回初始值。

本地线程类还提供 remove() 方法,删除存储在线程本地变量里的值。

Java 并发 API 包括 InheritableThreadLocal 类提供线程创建线程的值的遗传性 。如果线程A有一个本地线程变量,然后它创建了另一个线程B,那么线程B将有与A相同的本地线程变量值。 你可以覆盖 childValue() 方法来初始子线程的本地线程变量的值。 它接收父线程的本地线程变量作为参数。

二.深入解析ThreadLocal类

ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多。可能很多朋友都知道ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

先了解一下ThreadLocal类提供的几个方法:

public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }


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

首先我们来看一下ThreadLocal类是如何为每个线程创建一个变量的副本的。

先看下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); //获取当前线程的ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); //获取<key,value>键值对,注意这里获取键值对传进去的是this,而不是当前线程t
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}


首先看一下getMap方法中做了什么:

ThreadLocalMap getMap(Thread t) {
return t.threadLocals; //说明当前线程t里面有ThreadLocalMap的变量
}


那么我们继续取Thread类中取看一下成员变量threadLocals是什么:

/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;


实际上就是一个ThreadLocalMap,这个类型是ThreadLocal类的一个内部类,我们继续取看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;
}
}


然后再继续看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;
}


如果想在set之前使用get,就必须重写initialValue方法!

protected T initialValue() {
return null;
}


/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}


至此,ThreadLocal是如何为每个线程创建变量的副本的:

列首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。

初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。

然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。

public class Test {
ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
ThreadLocal<String> stringLocal = new ThreadLocal<String>();

public void set() {
longLocal.set(Thread.currentThread().getId());
stringLocal.set(Thread.currentThread().getName());
}

public long getLong() {
return longLocal.get();
}

public String getString() {
return stringLocal.get();
}

public static void main(String[] args) throws InterruptedException {
final Test test = new Test();

test.set();
System.out.println(test.getLong());
System.out.println(test.getString());

Thread thread1 = new Thread(){
public void run() {
test.set();
System.out.println(test.getLong());
System.out.println(test.getString());
};
};
thread1.start();
thread1.join();

System.out.println(test.getLong());
System.out.println(test.getString());
}
}


从这段代码的输出结果可以看出,在main线程中和thread1线程中,longLocal保存的副本值和stringLocal保存的副本值都不一样。最后一次在main线程再次打印副本值是为了证明在main线程中和thread1线程中的副本值确实是不同的。

总结一下:

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

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

在进行get之前,必须先set,否则会报空指针异常。

public class Test {
ThreadLocal<Long> longLocal = new ThreadLocal<Long>(){
protected Long initialValue() {
return Thread.currentThread().getId();
};
};
ThreadLocal<String> stringLocal = new ThreadLocal<String>(){;
protected String initialValue() {
return Thread.currentThread().getName();
};
};

public void set() {
longLocal.set(Thread.currentThread().getId());
stringLocal.set(Thread.currentThread().getName());
}

public long getLong() {
return longLocal.get();
}

public String getString() {
return stringLocal.get();
}

public static void main(String[] args) throws InterruptedException {
final Test test = new Test();

test.set();
System.out.println(test.getLong());
System.out.println(test.getString());

Thread thread1 = new Thread(){
public void run() {
test.set();
System.out.println(test.getLong());
System.out.println(test.getString());
};
};
thread1.start();
thread1.join();

System.out.println(test.getLong());
System.out.println(test.getString());
}
}


三.ThreadLocal的应用场景

最常见的ThreadLocal使用场景为 用来解决 数据库连接、Session管理等。

首先,ThreadLocal 不是用来解决共享对象的多线程访问问题的,一般情况下,通过ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。各个线程中访问的是不同的对象。

另外,说ThreadLocal使得各线程能够保持各自独立的一个对象,并不是通过ThreadLocal.set()来实现的,而是通过每个线程中的new 对象 的操作来创建的对象,每个线程创建一个,不是什么对象的拷贝或副本。通过ThreadLocal.set()将这个新创建的对象的引用保存到各线程的自己的一个map中,每个线程都有这样一个map,执行ThreadLocal.get()时,各线程从自己的map中取出放进去的对象,因此取出来的是各自自己线程中的对象,ThreadLocal实例是作为map的key来使用的。

如果ThreadLocal.set()进去的东西本来就是多个线程共享的同一个对象,那么多个线程的ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题。

下面来看一个hibernate中典型的ThreadLocal的应用:

private static final ThreadLocal threadSession = new ThreadLocal();

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


可以看到在getSession()方法中,首先判断当前线程中有没有放进去session,如果还没有,那么通过sessionFactory().openSession()来创建一个session,再将session set到线程中,实际是放到当前线程的ThreadLocalMap这个map中,这时,对于这个session的唯一引用就是当前线程中的那个ThreadLocalMap(下面会讲到),而threadSession作为这个值的key,要取得这个session可以通过threadSession.get()来得到,里面执行的操作实际是先取得当前线程中的ThreadLocalMap,然后将threadSession作为key将对应的值取出。这个session相当于线程的私有变量,而不是public的。

显然,其他线程中是取不到这个session的,他们也只能取到自己的ThreadLocalMap中的东西。要是session是多个线程共享使用的,那还不乱套了。

试想如果不用ThreadLocal怎么来实现呢?可能就要在action中创建session,然后把session一个个传到service和dao中,这可够麻烦的。或者可以自己定义一个静态的map,将当前thread作为key,创建的session作为值,put到map中,应该也行,这也是一般人的想法,但事实上,ThreadLocal的实现刚好相反,它是在每个线程中有一个map,而将ThreadLocal实例作为key,这样每个map中的项数很少,而且当线程销毁时相应的东西也一起销毁了

总之,ThreadLocal不是用来解决对象共享访问问题的,而主要是提供了保持对象的方法和避免参数传递的方便的对象访问方式。归纳了两点:

1、每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。

2、将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。

当然如果要把本来线程共享的对象通过ThreadLocal.set()放到线程中也可以,可以实现避免参数传递的访问方式,但是要注意get()到的是那同一个共享对象,并发访问问题要靠其他手段来解决。但一般来说线程共享的对象通过设置为某类的静态变量就可以实现方便的访问了,似乎没必要放到线程中。

ThreadLocal的应用场合,我觉得最适合的是按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: