1.为什么使用ThreadLocal
在说作用之前先看一段通过单例模式(懒汉:需要时才创建。相反的是饿汉模式,类一加载就创建)获取数据库连接池对象的代码
public class Test1 {
private Test1() {};
private static Connection conn;
/**
* 获得数据库连接对象
*/
public static Connection getConnection() throws SQLException {
if(conn == null) { ---------语句1
conn = DriverManager.getConnection("", "", "");
}
return conn;
}
/**
* 关闭连接对象
*/
public static void closeConnection() throws SQLException {
if(conn != null) {
conn.close();
}
}
}
在对数据库进行操作的时候,通过单例模式系统只保留一个实例减少资源的开销。获取数据库连接需要时间和额外的开销,使用单例模式,除了第一次创建外,其余都是使用现有的对象,减少创建的时间。但是使用以上代码在多线程环境下,会出现线程安全的问题。如:(1)当线程A运行到语句一的时候,if语句判断为true。而此时线程A分配的时间刚好执行完。此时线程B分配到时间,也刚好执行语句1,由于线程A判断为true后还未获取连接对象,时间片就执行完毕。所以此处线程B判断结果也是为true。这就会导致线程A和线程B都会获取数据库连接。单例模式也失去了意义。(2)前面一种假设可能不会导致程序运行的失败。但是当某个线程通过getConnection获取到数据库连接之后,另一个线程则执行closeConnection()方法关闭连接,就会造成程序的运行异常。
针对上面的两种情况,可以将getConnection方法和closeConnection方法都加上syncronized修饰,并且在使用conn对象的逻辑代码中加上同步锁,就可以避免。但是这样又带来一个问题,就是同一时刻,只能又一个线程对conn对象进行操作,其他需要conn对象的线程将处于阻塞状态,这样会导致效率的低下。
这时就可以使用ThreadLocal来解决多线程环境下,共享对象的线程安全问题,并减少解决线程安全而带来的效率低下问题。
2.什么是ThreadLocal
当使用ThreadLocal维护变量时,会为每个使用该变量的线程维护一个该变量的副本,副本是独立的。但是此时会带来一个问题,就是每个线程都有一个该变量的副本,那相对而言使用ThreadLocal会比不用ThreadLocal额外占用内存。
3.ThreadLocal类中常用的方法
(1) set(T value)
public void set(T value) {
Thread t = Thread.currentThread(); //获取当前线程
ThreadLocalMap map = getMap(t); //ThreadLocalMap是ThreaLocal中的一个内部类,getMap()方法为获取当前线程的threadLocals属 性,TheadLocalMap中维护一个key-value类型的数组。
if (map != null)
map.set(this, value); //若map已经存在,以线程对象为key,存储的值为value存入map的key-value数组中。下标通过key的hash值计 算得到
else
createMap(t, value); //若map不存在,创建ThreadLocalMap并存入
}
(2)V get(K k)
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); //获取线程中的threadLocals属性
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); //根据当前线程对象获得value
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue(); //当线程中的threadLocals对象未初始化,调用该方法,该方法的第一行调用protected T initialValue()方 法用于子类重写(使用者重写)获得value的方法。而后初始化线程的threadLocals属性,创建ThreadLocal Map对象。
}
(3)remove()
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
移除ThreadLocalMap中的变量副本。
总结:每个Thread线程类中都有一个threadLocals属性,该属性是ThreadLocalMap类型,但该类型不是map的子类!!当线程通过ThreadLocal的set方法存储变量的副本时,是将该副本存储到Thread当前线程对象的theadLocals属性中,而每个线程对象都有各自的threadLocals属性,这保证了多线程下共享变量的线程安全问题。
4.ThreadLocal的实验
public class Test {
ThreadLocal<String> local = new ThreadLocal<String>();
public void set() {
local.set(Thread.currentThread().getName());
}
public void get() {
System.out.println(local.get());
}
public static void main(String[] args) throws InterruptedException {
Test t = new Test();
t.set(); //实质是将变量副本存储到main线程的threadlocals属性中
t.get();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
t.set(); //实质是将变量副本存储到thread线程的threadlocals属性中
t.get();
}
});
thread.start();
thread.join();
t.get();
}
}
输出结果为:
5.改写 1 中的单例子模式获取数据库连接对象的方法
private Test1() {};
private static ThreadLocal<Connection> local = new ThreadLocal<>();
public static Connection conn;
/**
* 获得数据库连接对象
*/
public static Connection getConnection() throws SQLException {
if(conn == null) {
conn = DriverManager.getConnection("", "", "");
}
if(null == local.get()) {
local.set(conn);
}
return local.get();
}
/**
* 关闭连接对象
*/
public static void closeConnection() throws SQLException {
if(local.get() != null) {
local.get().close();
}
}