多线程理解(六) 线程封闭

一、什么是线程封闭:就是把对象封闭到一个线程里,只有这一个线程能够看到此对象。那么就算是这个对象不是线程安全的也不会出现不安全的问题,这就是线程封闭。

二、实现线程封闭的方法:

        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方法时才调用。

  1. 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;

  2. 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)了。

猜你喜欢

转载自blog.csdn.net/linjiaen20/article/details/81212176