关于这一篇一直想做一个最后结尾,但是总不知道该写点什么东西比较好,最近打算把它完成
ThreadLocal
Threadlocal类,称为本地线程变量,或者本地线程储存。简单来说,即通过该类能为变量在每个线程中储存一个副本,在线程任何时候都能够使用该副本。
在这里我们模拟下ThreadLocal如何使用
public class demo {
//单例模式开启数据库连接
Connection con = null;
public static Connection openConnection() {
if(con == null) {
con = new Connection();
}
return con;
}
public static void closeConnection() {
if(con != null)
con.close();
}
}
我们能够看到,这段代码实现功能是模拟数据库连接池中获取一个连接的功能,对于单线程程序来说,其内容完全正确,不需要做任何改变。但如果多个线程进行使用,则可能造成非常麻烦的后果,首先这两个方法都是未同步的,当两个线程获取到同一个connection时,就会出现错误,当一个线程完成任务后,需要关闭数据库链接(或者归还到连接池中),而此时另一个线程正在进行数据操作,那么就可能造成数据丢失的问题,针对这种请客,之前来说我们有两种解决办法。
- 为这两个方法进行同步处理,保证锁的获取
- 为每一个线程都创建一个数据库连接,线程结束时,将连接关闭
两种方法代码如下:
public class demo {
//单例模式开启数据库连接
Connection con = null;
public static synchronized Connection openConnection() {
if(con == null) {
con = new Connection();
}
return con;
}
public static synchronized void closeConnection() {
if(con != null)
con.close();
}
}
public class demo {
//新建数据库连接对象
Connection con = new Connection();
//进行数据库操作
con.save();
//关闭数据库
con.close();
}
第二种方法看起来更适合我们使用,不会造成线程安全问题同时也不会出现多个线程等待的情况,但我们需要考虑,频繁的开启数据库连接的消耗是十分昂贵的,当高并发时,会造成服务器巨大压力,可能造成服务器崩溃。所以虽然看起来很美好,但是需要考虑的情况太过于复杂。
针对这种情况,Java中提供了ThreadLocal类来让我们处理此类问题,ThreadLocal能为我们每一个线程中都创建一个该变量的副本,在我们使用中,各个线程操作各自的副本进行使用,互不干扰,这也就解决了线程安全的问题,同时我们也不需要进行同步处理,也不会降低效率。
但需要我们注意的是,虽然ThreadLocal能够解决这类问题,但是其代价是需要消耗更大的内存空间来存放各个线程中储存的变量副本,所以说,在我们使用该类处理问题时,同样要考虑清楚内存需求。
ThreadLocal使用方法
在ThreadLocal中,有get,set,remove,initialValue这几个方法。get方法为获取到当前线程中的变量副本,set为设置当前线程中变量副本,remove为移除当前线程中变量副本,而initialValue为延迟加载的方法。
其中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();
}
首先是创建了一个线程t获取到当前线程,然后通过内部的getMap方法获得了一个map,通过查找我们能够看到,这个Map集合是一个ThreadLocal的集合,源码如下。
static class Entry extends WeakReference<ThreadLocal<?>>
而在这里,我们需要看一下getMap方法的源代码是如何实现的
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
方法中只有一个返回值,返回了一个叫threadLocals的对象,继续找到它的源码
ThreadLocal.ThreadLocalMap threadLocals = null;
显而易见,这是一个ThreadLocalMap的引用,继续来看该Map如何进行实行,对于参数如何处理。
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;
}
}
可以看到,其继承了WeakReference,设置键值对,方法很简单。
现在我们只需要看最后一个方法 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;
}
实际上非常简单,如果map不为空,则设置键值对,如果map为空,则创建map。
到这里其功能就很明显了,首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。
初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。
然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。
然后我们实现一段代码,来测试一下ThreadLocal在不同线程中所获得的副本究竟是否相同
package DEMO;
public class DEMO {
ThreadLocal<Integer> intLocal = new ThreadLocal<>();
ThreadLocal<String> strLocal = new ThreadLocal<>();
public void set() {
intLocal.set((int) Thread.currentThread().getId());
strLocal.set(Thread.currentThread().getName());
}
public int getInt() {
return intLocal.get();
}
public String getStr() {
return strLocal.get();
}
public static void main(String[] args) throws InterruptedException {
DEMO test = new DEMO();
test.set();
System.out.println(test.getInt());
System.out.println(test.getStr());
Thread testThread = new Thread() {
public void run() {
test.set();
System.out.println(test.getInt());
System.out.println(test.getStr());
}
};
testThread.start();
testThread.join();
System.out.println(test.getInt());
System.out.println(test.getStr());
}
}
控制台打印结果
1
main
12
Thread-0
1
main
能够看到结果,在main线程和我们定义的另一个线程中,所获得的副本值不同。
拿图来解释,我们将不同的副本存入ThreadLocal中的时候,实际上是将这些数据按照键值对的方式存入Threadlocals这个集合中,集合的键即为我们当前线程,集合的值为我们所set的数据。需要注意的是,当我们使Threadlocal时,必须先进行set然后再进行get方法,否则会出现空指针异常。
ThreadLocal应用场景
针对一般的共享变量,我们没必要使用ThreadLocal,而且使用后就会发生类似于缓存一致性的问题,这个变量在不同线程中保存有不同的副本,值不尽相同,导致获取到的数据为废数据。
使用Threadlocal时,一般为数据库连接,session管理等等。