一、什么是线程封闭:就是把对象封闭到一个线程里,只有这一个线程能够看到此对象。那么就算是这个对象不是线程安全的也不会出现不安全的问题,这就是线程封闭。
二、实现线程封闭的方法:
1.Ad-hoc线程封闭
这是完全靠实现者控制的线程封闭,他的线程封闭完全靠实现者实现,是最糟糕的一种线程封闭。
2.栈封闭
栈封闭是我们最经常遇到的一种线程封闭。栈封闭就是指局部变量。多个线程访问同一个方法时,此方法的局部变量会被拷贝一份到我们的线程栈中。所以局部变量是不被多个线程所共享的,也就不会出现并发的问题。所以能用局部变量就不要用全局变量,全局变量容易引起并发问题。
3.ThreadLocal类
ThreadLocal为每一个使用该变量的线程都提供一个变量的副本,每一个线程都可以独立地改变自己的副本,而不会与其他副本冲突。
ThreadLocal中有一个Map,用于存储每一个线程的变量的副本。
对于多线程的资源问题,同步机制采取“时间换空间”的方式,而ThreadLocal采用“空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供一份变量,因此可以同时访问而不受影响。
举个栗子:
public class TestNum {
private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return 0;
}
};
public int getNextNum(){
seqNum.set(seqNum.get()+1);
return seqNum.get();
}
public static void main(String[] args) {
TestNum sn = new TestNum();
ThreadClient t1 = new ThreadClient(sn);
ThreadClient t2 = new ThreadClient(sn);
ThreadClient t3 = new ThreadClient(sn);
t1.start();
t2.start();
t3.start();
}
}
class ThreadClient extends Thread{
private TestNum sn ;
public ThreadClient(TestNum sn){
this.sn = sn;
}
@Override
public void run(){
for(int i = 0 ; i < 3 ; i++){
System.out.println("Thread: "+ Thread.currentThread().getName()
+ " sn: " + sn.getNextNum());
}
}
}
三、ThreadLocal源码:
ThreadLocal类的方法很简单,只有四个,分别为set,get,remove, initialValue,从字面上我们也能理解这些方法的作用。
public T get():返回当前线程所对应的局部变量
public void set(T arg0):设置当前线程局部变量的值
public void remove():将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK5.0新增的方法。注意,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
protected T initialValue(): 对当线程局部变量进行初始化,并返回该初始值。是protected 属性,显然是让子类进行对其覆盖重写的,只有第一次调用set和get方法时才调用。
- set方法,以下是set方法的源码:
public void set(T arg0) { Thread arg1 = Thread.currentThread(); ThreadLocal.ThreadLocalMap arg2 = this.getMap(arg1); if (arg2 != null) { arg2.set(this, arg0); } else { this.createMap(arg1, arg0); } }
从set方法中可以看到,首先获取当前线程:Thread arg1 = Thread.currentThread();
再获取当前线程的ThreadLocalMap:ThreadLocal.ThreadLocalMap arg2 = this.getMap(arg1);
判断ThreadLocalMap是否为空,不为空,则以键值对的形式设置值,key为this,value就是局部变量的副本,this是当前线程持有的ThreadLocal类实例化对象。
假如为空,则通过createMap方法创建。
我们看下getMap和createMap方法的源码:
ThreadLocal.ThreadLocalMap getMap(Thread arg0) {
return arg0.threadLocals;
}
void createMap(Thread arg0, T arg1) {
arg0.threadLocals=new ThreadLocal.ThreadLocalMap(this, arg1);
}
从代码上已经写的非常清楚,每个线程都有自己的局部变量的副本,该副本是存在ThreadLocalMap 中,其中键值就是ThreadLocal类实例化对象。也就是说每个线程都拥有自己的ThreadLocalMap,ThreadLocalMap保存的就是局部变量副本。我们看一下java.lang.Thread源码。
private static int threadInitNumber;
ThreadLocalMap threadLocals = null;
ThreadLocalMap inheritableThreadLocals = null;
- get方法,以下是get方法源码:
public T get() { Thread arg0 = Thread.currentThread(); ThreadLocal.ThreadLocalMap arg1 = this.getMap(arg0); if (arg1 != null) { ThreadLocal.ThreadLocalMap.Entry arg2 = arg1.getEntry(this); if (arg2 != null) { Object arg3 = arg2.value; return arg3; } } return this.setInitialValue(); }
从代码上看,前两步和set方法是一个样的,分别获取当前线程和当前线程的ThreadLocalMap,第三步判断ThreadLocalMap是否为空,不为空根据this键值获取value,为空调用setInitialValue()方法。
以下是setInitialValue方法代码:
private T setInitialValue() {
Object arg0 = this.initialValue();
Thread arg1 = Thread.currentThread();
ThreadLocal.ThreadLocalMap arg2 = this.getMap(arg1);
if (arg2 != null) {
arg2.set(this, arg0);
} else {
this.createMap(arg1, arg0);
}
return arg0;
}
在setInitialValue里调用了initialValue()方法,也就是子类要重写覆盖的方法,对应上面的例子的代码是:
protected Connection initialValue() {
Connection conn = null;
try {
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "username", "password");
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
然后获取当前线程和当前线程的ThreadLocalMap,ThreadLocalMap为空则调用createMap,否则调用set方法。
四、总结
ThreadLocalMap对象是以this指向的ThreadLocal对象为键进行查找的,这当然和前面set()方法的代码是相呼应的。
进一步地,我们可以创建不同的ThreadLocal实例来实现多个变量在不同线程间的访问隔离,为什么可以这么做?因为不同的ThreadLocal对象作为不同键,当然也可以在线程的ThreadLocalMap对象中设置不同的值了。通过ThreadLocal对象,在多线程中共享一个值和多个值的区别,就像你在一个HashMap对象中存储一个键值对和多个键值对一样,仅此而已。
也就说,每个线程都有一个ThreadLocalMap,该线程访问到某个局部变量,且该局部变量是用ThreadLocal类进行声明时,该线程就会new ThreadLocal(),然后将该ThreadLocal类的对象作为key值,所对应的局部变量作为value值保存到ThreadLocalMap中。当线程访问多个ThreadLocal类进行声明局部变量时,在ThreadLocalMap中就有多个键值对。而每个线程都有自己的ThreadLocalMap,从而达到隔离的目的了。
当某个线程终止后,该线程里的ThreadLocalMap也被回收了,所以完全不用担心内存泄漏的问题。
假如多线程访问的对象实例是单例的,或者说只能创建一个,那就老老实实的使用同步机制(synchronized)了。