疯狂java


您现在的位置: 疯狂软件 >> 新闻资讯 >> 正文

ThreadLocal类的实现用法


 

   

  ThreadLocal是什么呢?其实ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是threadlocalvariable(线程局部变量)。也许把它命名为ThreadLocalVar更加合适。线程局部变量(ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。

  从线程的角度看,每个线程都保持一个对其线程局部变量副本的隐式引用,只要线程是活动的并且 ThreadLocal 实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。

  通过ThreadLocal存取的数据,总是与当前线程相关,也就是说,JVM 为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境常出现的并发访问问题提供了一种隔离机制。

  ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单,在ThreadLocal类中有一个Map,用于存储每一个线程的变量的副本。

  概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

  ThreadLocal的接口方法

  ThreadLocal类接口很简单,只有4个方法,我们先来了解一下:

  void set(Object value)设置当前线程的线程局部变量的值。

  public Object get()该方法返回当前线程所对应的线程局部变量。

  public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。

  protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

  模拟实现ThreadLocal代码:

  复制代码

  1 public class ThreadShareData {

  2 static int num = 0;

  3

  4

  5 public static void main(String[] args) {

  6 Map

  7 for (int i = 0; i < 2; i++) {

  8 new Thread(new Runnable() {

  9 @Override

  10 public void run() {

  11 int num = new Random().nextInt();

  12 System.out.println(Thread.currentThread().getName()+":"+" get num: "+num);

  13 new A().get();

  14 new B().get();

  15 }

  16 }).start();

  17 }

  18 }

  19

  20 static class A{

  21 public void get(){

  22 System.out.println("A: "+Thread.currentThread().getName() +"get num: "+num);

  23 }

  24 }

  25

  26 static class B{

  27 public void get(){

  28 System.out.println("A: "+Thread.currentThread().getName() +"get num: "+num);

  29 }

  30 }

  31

  32 }

  复制代码

  ThreadLocal的经典用法:

  复制代码

  1 public class ThreadLocalShareData2 {

  2 static ThreadLocal threadLocal = new ThreadLocal ();

  3 public static void main(String[] args) {

  4 for (int i = 0; i < 2; i++) {

  5 new Thread(new Runnable() {

  6 @Override

  7 public void run() {

  8 int data = new Random().nextInt();

  9 People people = new People().getInstance();

  10 people.setName("name"+data);

  11 people.setAge(data);

  12 System.out.println(Thread.currentThread().getName()+" set name "+people.getName()+" set age "+people.getAge());

  13 new A().get();

  14 new B().get();

  15 }

  16 }).start();

  17 }

  18 }

  19

  20 static class A{

  21 public void get(){

  22 System.out.println("A: "+Thread.currentThread().getName() +"get name "+new People().getInstance().getName()+" get age "+new People().getInstance().getAge());

  23 }

  24 }

  25 static class B{

  26 public void get(){

  27 System.out.println("B: "+Thread.currentThread().getName() +"get name "+new People().getInstance().getName()+" get age "+new People().getInstance().getAge());

  28 }

  29 }

  30 static class People{

  31 private People(){

  32

  33 }

  34 public People getInstance(){

  35 People people = threadLocal.get();

  36 if(people == null){

  37 people = new People();

  38 threadLocal.set(people);

  39 }

  40 return people;

  41 }

  42 private int age;

  43 private String name;

  44 public int getAge() {

  45 return age;

  46 }

  47 public String getName() {

  48 return name;

  49 }

  50 public void setAge(int age) {

  51 this.age = age;

  52 }

  53 public void setName(String name) {

  54 this.name = name;

  55 }

  56 }

  57 }

  复制代码

  将ThreadLocal仿单例模式进行实现,更加面向对象。

  在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。

  复制代码

  1 public class TestNum {

  2 // ①通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值

  3 private static ThreadLocal seqNum = new ThreadLocal() {

  4 public Integer initialValue() {

  5 return 0;

  6 }

  7 };

  8

  9 // ②获取下一个序列值

  10 public int getNextNum() {

  11 seqNum.set(seqNum.get() + 1);

  12 return seqNum.get();

  13 }

  14

  15 public static void main(String[] args) {

  16 TestNum sn = new TestNum();

  17 // ③ 3个线程共享sn,各自产生序列号

  18 TestClient t1 = new TestClient(sn);

  19 TestClient t2 = new TestClient(sn);

  20 TestClient t3 = new TestClient(sn);

  21 t1.start();

  22 t2.start();

  23 t3.start();

  24 }

  25

  26 private static class TestClient extends Thread {

  27 private TestNum sn;

  28

  29 public TestClient(TestNum sn) {

  30 this.sn = sn;

  31 }

  32

  33 public void run() {

  34 for (int i = 0; i < 3; i++) {

  35 // ④每个线程打出3个序列值

  36 System.out.println("thread[" + Thread.currentThread().getName() + "] --> sn["

  37 + sn.getNextNum() + "]");

  38 }

  39 }

  40 }

  41 }

  复制代码

  我们发现每个线程所产生的序号虽然都共享同一个TestNum实例,但它们并没有发生相互干扰的情况,而是各自产生独立的序列号,这是因为我们通过ThreadLocal为每一个线程提供了单独的副本。

  Thread同步机制的比较:

  ThreadLocal和线程同步机制相比有什么优势呢?ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。

  在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。

  而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。

  由于ThreadLocal中可以持有任何类型的对象,低版本JDK所提供的get()返回的是Object对象,需要强制类型转换。但JDK 5.0通过泛型很好的解决了这个问题,在一定程度地简化ThreadLocal的使用,代码清单 9 2就使用了JDK 5.0新的ThreadLocal版本。

  概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

  Spring使用ThreadLocal解决线程安全问题我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,因为有状态的Bean就可以在多线程中共享了。

  一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程,如图9‑2所示:

  通通透透理解ThreadLocal

  同一线程贯通三层这样你就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有关联的对象引用到的都是同一个变量。

  下面的实例能够体现Spring对有状态Bean的改造思路:

  非线程安全代码:

  复制代码

  1 public class TestDao {

  2 private Connection conn;// ①一个非线程安全的变量

  3

  4 public void addTopic() throws SQLException {

  5 Statement stat = conn.createStatement();// ②引用非线程安全变量

  6 // …

  7 }

  8 }

  复制代码

  线程安全代码:

  复制代码

  1 public class TestDaoNew {

  2 // ①使用ThreadLocal保存Connection变量

  3 private static ThreadLocal connThreadLocal = new ThreadLocal();

  4

  5 public static Connection getConnection() {

  6 // ②如果connThreadLocal没有本线程对应的Connection创建一个新的Connection,

  7 // 并将其保存到线程本地变量中。

  8 if (connThreadLocal.get() == null) {

  9 Connection conn = getConnection();

  10 connThreadLocal.set(conn);

  11 return conn;

  12 } else {

  13 return connThreadLocal.get();// ③直接返回线程本地变量

  14 }

  15 }

  16

  17 public void addTopic() throws SQLException {

  18 // ④从ThreadLocal中获取线程对应的Connection

  19 Statement stat = getConnection().createStatement();

  20 }

  21 }

  复制代码

  不同的线程在使用TopicDao时,先判断connThreadLocal.get()是否是null,如果是null,则说明当前线程还没有对应的Connection对象,这时创建一个Connection对象并添加到本地线程变量中;如果不为null,则说明当前的线程已经拥有了Connection对象,直接使用就可以了。这样,就保证了不同的线程使用线程相关的Connection,而不会使用其它线程的Connection。因此,这个TopicDao就可以做到singleton共享了。

  当然,这个例子本身很粗糙,将Connection的ThreadLocal直接放在DAO只能做到本DAO的多个方法共享Connection时不发生线程安全问题,但无法和其它DAO共用同一个Connection,要做到同一事务多DAO共享同一Connection,必须在一个共同的外部类使用ThreadLocal保存Connection。

  复制代码

  1 public class ConnectionManager {

  2

  3 private static ThreadLocal connectionHolder = new ThreadLocal() {

  4 @Override

  5 protected Connection initialValue() {

  6 Connection conn = null;

  7 try {

  8 conn = DriverManager.getConnection(

  9 "jdbc:mysql://localhost:3306/test", "username",

  10 "password");

  11 } catch (SQLException e) {

  12 e.printStackTrace();

  13 }

  14 return conn;

  15 }

  16 };

  17

  18 public static Connection getConnection() {

  19 return connectionHolder.get();

  20 }

  21

  22 public static void setConnection(Connection conn) {

  23 connectionHolder.set(conn);

  24 }

  25 }

  复制代码

  还有一个经典实例,在HibernateUtil中,用于session的管理:

  复制代码

  1 public class HibernateUtil {

  2 private static Log log = LogFactory.getLog(HibernateUtil.class);

  3 private static final SessionFactory sessionFactory; //定义SessionFactory

  4

  5 static {

  6 try {

  7 // 通过默认配置文件hibernate.cfg.xml创建SessionFactory

  8 sessionFactory = new Configuration().configure().buildSessionFactory();

  9 } catch (Throwable ex) {

  10 log.error("初始化SessionFactory失败!", ex);

  11 throw new ExceptionInInitializerError(ex);

  12 }

  13 }

  14

  15 //创建线程局部变量session,用来保存Hibernate的Session

  16 public static final ThreadLocal session = new ThreadLocal();

  17

  18 /**

  19 * 获取当前线程中的Session

  20 * @return Session

  21 * @throws HibernateException

  22 */

  23 public static Session currentSession() throws HibernateException {

  24 Session s = (Session) session.get();

  25 // 如果Session还没有打开,则新开一个Session

  26 if (s == null) {

  27 s = sessionFactory.openSession();

  28 session.set(s); //将新开的Session保存到线程局部变量中

  29 }

  30 return s;

  31 }

  32

  33 public static void closeSession() throws HibernateException {

  34 //获取线程局部变量,并强制转换为Session类型

  35 Session s = (Session) session.get();

  36 session.set(null);

  37 if (s != null)

  38 s.close();

  39 }

  40 }

  复制代码

  总结:

  ThreadLocal使用场合主要解决多线程中数据数据因并发产生不一致问题。ThreadLocal为每个线程的中并发访问的数据提供一个副本,通过访问副本来运行业务,这样的结果是耗费了内存,单大大减少了线程同步所带来性能消耗,也减少了线程并发控制的复杂度。

  ThreadLocal不能使用原子类型,只能使用Object类型。ThreadLocal的使用比synchronized要简单得多。

  ThreadLocal和Synchonized都用于解决多线程并发访问。但是ThreadLocal与synchronized有本质的区别。synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而 Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。

  Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。

  当然ThreadLocal并不能替代synchronized,它们处理不同的问题域。Synchronized用于实现同步机制,比ThreadLocal更加复杂。