java进阶5 -「ThreadLocal」

一 神么是ThreadLocal

ThreadLocal很多地方叫做线程本地变量, 也有线程本地存储的叫法, 它为变量在每个线程中创建一个副本, 每个线程可以访问自己内部的副本变量

ThreadLocal不是用于解决共享变量的问题的,不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制

如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用 get()set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。

  

比如数据库连接的例子:

class ConnectionManager {
    private static Connection connect = null;
   
    public static Connection openConnection() {
        if(connect == null){
            connect = DriverManager.getConnection();
        }
        return connect;
    }
     
    public static void closeConnection() {
        if(connect!=null)
            connect.close();
    }
}

 

上面的代码在多线程中使用有线程安全问题,因为connect是共享变量,而两个方法使用共享变量的方法都没有进行同步.

如果两个方法都进行同步处理,而且在在调用connect的地方进行了同步处理确实可以解决这个问题,但是这样会大大影响程序执行的效率,因为一个线程在使用connect进行数据库操作的时候,其他线程只有等待.

那么事实上,这里是不需要将connect变量共享的,因为一个线程不关心其他线程是否对这个connect进行了修改.

 

 

那么既然不需要共享,就可以不用static的,用实例的方式在每个需要使用数据库连接的地方调用,使用后再释放这个连接,如下

class ConnectionManager {
    private  Connection connect = null;
     
    public Connection openConnection() {
        if(connect == null){
            connect = DriverManager.getConnection();
        }
        return connect;
    }
     
    public void closeConnection() {
        if(connect!=null)
            connect.close();
    }
}
 
class Dao{
    public void insert() {
        ConnectionManager connectionManager = new ConnectionManager();
        Connection connection = connectionManager.openConnection();
         
        //使用connection进行操作
         
        connectionManager.closeConnection();
    }
}

但是这样频繁开闭数据库连接,服务器压力大,并且严重影响程序执行性能.那么这种情况下使用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为每个线程创建变量的副本的方法:

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

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

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

 

 

 三 ThreadLocal示例

下面简单的例子说明通过ThreadLocal能达到在每个线程中创建变量副本的效果

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(); //anchor
        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());
    }
}

输出结果:

1
main
9
Thread-0
1
main

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

 

 

总结一下:

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

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

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

 如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。

因为在上面的代码分析过程中,我们发现如果没有先set的话,即在map中查找不到对应的存储,则会通过调用setInitialValue方法返回i, 而在setInitialValue方法中,有一个语句是T value = initialValue(), 而默认情况下,initialValue方法返回的是null。

 

看上面的anchor,也就是test.set(),不写会报NullPointerException,而用下面的写法不写set方法也可

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

 

 

  

 四 ThreadLocal应用场景

最常见的使用场景是用来解决数据库连接,session管理等

private static ThreadLocal<Connection> connectionHolder
= new ThreadLocal<Connection>() {
public Connection initialValue() {
    return DriverManager.getConnection(DB_URL);
}
};
 
public static Connection getConnection() {
return connectionHolder.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;
}

 

 

五 ThreadLocal 内存泄露问题是怎么导致的?

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。

这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()get()remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后最好手动调用remove()方法

posted @ 2015-08-31 17:25  balfish  阅读(217)  评论(0编辑  收藏  举报