美文网首页Java
JAVA线程安全及性能的优化笔记(五)——ThreadLocal

JAVA线程安全及性能的优化笔记(五)——ThreadLocal

作者: Java_苏先生 | 来源:发表于2020-04-13 14:30 被阅读0次

    前期回顾

    JAVA线程安全及性能的优化笔记(四)——什么是线程安全?

    一、ThreadLocal原理

    1. 线程程序介绍

    早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。

    2. Threadlocal变量

    ThreadLocal很容易让人望文生义,想当然地认为是一个“本地线程”。其实,ThreadLocal并不是一个Thread,而是Thread的局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些。

    当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

    从线程的角度看,目标变量就像是线程的本地变量,这也是类名中“Local”所要表达的意思。

    线程局部变量并不是Java的新发明,很多语言(如IBM IBM XL FORTRAN)在语法层面就提供线程局部变量。在Java中没有提供在语言级支持,而是变相地通过ThreadLocal的类提供支持。

    所以,在Java中编写线程局部变量的代码相对来说要笨拙一些,因此造成线程局部变量没有在Java开发者中得到很好的普及。

    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。

    值得一提的是,在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal<T>。API方法也相应进行了调整,新版本的API方法分别是void set(T value)、T get()以及T initialValue()。

    ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单:在ThreadLocal类中定义了一个ThreadLocalMap,每一个Tread中都有一个该类型的变量——threadLocals——用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。

    二、基本概念

    为每一个使用该变量的线程都提供一个变量值的副本,使每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量。使用场景To keep state with a thread (user-id, transaction-id, logging-id) To cache objects which you need frequentlyThreadLocal类

    它主要由四个方法组成initialValue(),get(),set(T),remove(),其中值得注意的是initialValue(),该方法是一个protected的方法,显然是为了子类重写而特意实现的。该方法返回当前线程在该线程局部变量的初始值,这个方法是一个延迟调用方法,在一个线程第1次调用get()或者set(Object)时才执行,并且仅执行1次。ThreadLocal中的确实实现直接返回一个null:

    举例

    ThreadLocal的原理

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

      public class ThreadLocal
      {
          private Map values = Collections.synchronizedMap(new HashMap());
          public Object get()
          {
              Thread curThread = Thread.currentThread();
              Object o = values.get(curThread);
              if (o == null && !values.containsKey(curThread))
              {
                  o = initialValue();
                  values.put(curThread, o);
                  
            }
              return o;
              
        }
          public void set(Object newValue)
          {
              values.put(Thread.currentThread(), newValue);
              
        }
          public Object initialValue()
          {
              return null;
              
        }
          
    }
    

    使用方法

    ThreadLocal 的使用

    使用方法一:Hibernate的文档时看到了关于使ThreadLocal管理多线程访问的部分。具体代码如下

       public static final ThreadLocal session = new ThreadLocal();
       public static Session currentSession() {
           Session s = (Session)session.get();
           //open a new session,if this session has none
           if(s == null){
               s = sessionFactory.openSession();
               session.set(s);
              
        }
          return s;
          
    }
    

    我们逐行分析

    1. 初始化一个ThreadLocal对象,ThreadLocal有三个成员方法 get()、set()、initialvalue()。
    2. 如果不初始化initialvalue,则initialvalue返回null。
    3. session的get根据当前线程返回其对应的线程内部变量,也就是我们需要的net.sf.hibernate.Session(相当于对应每个数据库连接).多线程情况下共享数据库链接是不安全的。
    4. ThreadLocal保证了每个线程都有自己的s(数据库连接)。
    5. 如果是该线程初次访问,自然,s(数据库连接)会是null,接着创建一个Session,具体就是行6。
    6. 创建一个数据库连接实例 s
    7. 保存该数据库连接s到ThreadLocal中。
    8. 如果当前线程已经访问过数据库了,则从session中get()就可以获取该线程上次获取过的连接实例。

    使用方法二:当要给线程初始化一个特殊值时,需要自己实现ThreadLocal的子类并重写该方法,通常使用一个内部匿名类对ThreadLocal进行子类化,EasyDBO中创建jdbc连接上下文就是这样做的:

      public class JDBCContext{
          private static Logger logger = Logger.getLogger(JDBCContext.class);
          private DataSource ds;
          protected Connection connection;
          private Boolean isValid = true;
          private static ThreadLocal jdbcContext;
          private JDBCContext(DataSource ds){
              this.ds = ds;
              createConnection();
              
        }
          public static JDBCContext getJdbcContext(javax.sql.DataSource ds)
          {
              if(jdbcContext==null)jdbcContext=new JDBCContextThreadLocal(ds);
              JDBCContext context = (JDBCContext) jdbcContext.get();
              if (context == null) {
                  context = new JDBCContext(ds);
                  
            }
              return context;
              
        }
          private static class JDBCContextThreadLocal extends ThreadLocal {
              public javax.sql.DataSource ds;
              public JDBCContextThreadLocal(javax.sql.DataSource ds)
              {
                  this.ds=ds;
                  
            }
              protected synchronized Object initialValue() {
                  return new JDBCContext(ds);
                  
            }
              
        }
          
    }
    

    简单的实现版本

    代码清单1 SimpleThreadLocal

    public class SimpleThreadLocal {
          private Map valueMap = Collections.synchronizedMap(new HashMap());
          public void set(Object newValue) {
              valueMap.put(Thread.currentThread(), newValue);
            ①键为线程对象,值为本线程的变量副本
              
        }
          public Object get() {
              Thread currentThread = Thread.currentThread();
              Object o = valueMap.get(currentThread);
            ②返回本线程对应的变量
              if (o == null && !valueMap.containsKey(currentThread)) {
                ③如果在Map中不存在,放到Map
                  中保存起来。
                  o = initialValue();
                  valueMap.put(currentThread, o);
                  
            }
              return o;
              
        }
          public void remove() {
              valueMap.remove(Thread.currentThread());
              
        }
          public Object initialValue() {
              return null;
              
        }
          
    }
    

    虽然代码清单9 3这个ThreadLocal实现版本显得比较幼稚,但它和JDK所提供的ThreadLocal类在实现思路上是相近的。

    举例

    下面,我们通过一个具体的实例了解一下ThreadLocal的具体使用方法。

    代码清单2 SequenceNumber

    package com.baobaotao.basic;
      public class SequenceNumber {
          ①通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值
          private static ThreadLocal seqNum = new ThreadLocal(){
              public Integer initialValue(){
                  return 0;
                  
            }
              
        }
        ;
          ②获取下一个序列值
          public int getNextNum(){
              seqNum.set((Integer)seqNum.get()+1);
              return (Integer)seqNum.get();
              
        }
          public static void main(String[] args)
          {
              SequenceNumber sn = new SequenceNumber();
              ③ 3个线程共享sn,各自产生序列号
              TestClient t1 = new TestClient(sn);
              TestClient t2 = new TestClient(sn);
              TestClient t3 = new TestClient(sn);
              t1.start();
              t2.start();
              t3.start();
              
        }
          private static class TestClient extends Thread
          {
              private SequenceNumber sn;
              public TestClient(SequenceNumber sn) {
                  this. sn = sn;
                  
            }
              public void run()
              {
                  for (int i = 0; i < 3; i++) {
                    ④每个线程打出3个序列值
                      System.out.println("thread["+Thread.currentThread().getName()+
                      "] sn["+sn.getNextNum()+"]");
                      
                }
                  
            }
              
        }
          
    }
    

    分析

    通常我们通过匿名内部类的方式定义ThreadLocal的子类,提供初始的变量值,如例子中①处所示。TestClient线程产生一组序列号,在③处,我们生成3个TestClient,它们共享同一个SequenceNumber实例。运行以上代码,在控制台上输出以下的结果:

      thread[Thread-2] sn[1]
      thread[Thread-0] sn[1]
      thread[Thread-1] sn[1]
      thread[Thread-2] sn[2]
      thread[Thread-0] sn[2]
      thread[Thread-1] sn[2]
      thread[Thread-2] sn[3]
      thread[Thread-0] sn[3]
      thread[Thread-1] sn[3]
    

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

    说明

    在Java的多线程编程中,为保证多个线程对共享变量的安全访问,通常会使用synchronized来保证同一时刻只有一个线程对共享变量进行操作。但在有些情况下,synchronized不能保证多线程对共享变量的正确读写。例如类有一个类变量,该类变量会被多个类方法读写,当多线程操作该类的实例对象时,如果线程对类变量有读取、写入操作就会发生类变量读写错误,即便是在类方法前加上synchronized也无效,因为同一个线程在两次调用方法之间时锁是被释放的,这时其它线程可以访问对象的类方法,读取或修改类变量。这种情况下可以将类变量放到ThreadLocal类型的对象中,使变量在每个线程中都有独立拷贝,不会出现一个线程读取变量时而被另一个线程修改的现象。

    下面举例说明:

      public class QuerySvc {
          private String sql;
          private static ThreadLocal sqlHolder = new ThreadLocal();
          public QuerySvc() {
              
        }
          public void execute() {
              System.out.println("Thread " + Thread.currentThread().getId() +" Sql is " + sql);
              System.out.println("Thread " + Thread.currentThread().getId() +" Thread Local variable Sql is " + sqlHolder.get());
              
        }
          public String getSql() {
              return sql;
              
        }
          public void setSql(String sql) {
              this.sql = sql;
              sqlHolder.set(sql);
              
        }
          
    }
    

    三、多线程访问

    为了说明多线程访问对于类变量和ThreadLocal变量的影响,QuerySvc中分别设置了类变量sql和ThreadLocal变量,使用时先创建 QuerySvc的一个实例对象,然后产生多个线程,分别设置不同的sql实例对象,然后再调用execute方法,读取sql的值,看是否是set方法中写入的值。这种场景类似web应用中多个请求线程携带不同查询条件对一个servlet实例的访问,然后servlet调用业务对象,并传入不同查询条件,最后要保证每个请求得到的结果是对应的查询条件的结果。

    使用QuerySvc的工作线程如下:

    public class Work extends Thread {
          private QuerySvc querySvc;
          private String sql;
          public Work(QuerySvc querySvc,String sql) {
              this.querySvc = querySvc;
              this.sql = sql;
              
        }
          public void run() {
              querySvc.setSql(sql);
              querySvc.execute();
              
        }
          
    }
    

    运行线程代码如下:

      QuerySvc qs = new QuerySvc();
      for (int k=0; k<10; k++)
      String sql = "Select * from table where id =" + k;
      new Work(qs,sql).start();
      
    }
    

    先创建一个QuerySvc实例对象,然后创建若干线程来调用QuerySvc的set和execute方法,每个线程传入的sql都不一样,从运行结果可以看出sql变量中值不能保证在execute中值和set设置的值一样,在 web应用中就表现为一个用户查询的结果不是自己的查询条件返回的结果,而是另一个用户查询条件的结果;而ThreadLocal中的值总是和set中设置的值一样,这样通过使用ThreadLocal获得了线程安全性。

    如果一个对象要被多个线程访问,而该对象存在类变量被不同类方法读写,为获得线程安全,可以用ThreadLocal来替代类变量。

    四、Thread同步机制的比较

    说明

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

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

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

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

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

    五、Spring使用ThreadLocal解决线程安全问题

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

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

    图1同一线程贯通三层

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

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

    代码清单3 TopicDao:非线程安全

      public class TopicDao {
          private Connection conn;
        ①一个非线程安全的变量
          public void addTopic(){
              Statement stat = conn.createStatement();
            ②引用非线程安全变量
              …
              
        }
          
    }
    

    由于①处的conn是成员变量,因为addTopic()方法是非线程安全的,必须在使用时创建一个新TopicDao实例(非singleton)。下面使用ThreadLocal对conn这个非线程安全的“状态”进行改造:

    代码清单4 TopicDao:线程安全

      import java.sql.Connection;
      import java.sql.Statement;
      public class TopicDao {
          ①使用ThreadLocal保存Connection变量
          private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>();
          public static Connection getConnection(){
              ②如果connThreadLocal没有本线程对应的Connection创建一个新的Connection,
              并将其保存到线程本地变量中。
              if (connThreadLocal.get() == null) {
                  Connection conn = ConnectionManager.getConnection();
                  connThreadLocal.set(conn);
                  return conn;
                  
            } else{
                  return connThreadLocal.get();
                ③直接返回线程本地变量
                  
            }
              
        }
          public void addTopic() {
              ④从ThreadLocal中获取线程对应的Connection
              Statement stat = getConnection().createStatement();
              
        }
          
    }
    

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

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

    小结

    解决方法

    ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。

    六、ThreadLocal线程局部变量

    1. 什么是线程局部变量

    什么是线程局部变量(thread-local variable)?轻松使用线程: 不共享有时是最好的
    ThreadLocal 类是悄悄地出现在 Java 平台版本 1.2 中的。虽然支持线程局部变量早就是许多线程工具(例如 Posix pthreads 工具)的一部分,但 Java Threads API 的最初设计却没有这项有用的功能。而且,最初的实现也相当低效。由于这些原因, ThreadLocal 极少受到关注,但对简化线程安全并发程序的开发来说,它却是很方便的。在 轻松使用线程的第 3 部分,Java 软件顾问 Brian Goetz 研究了 ThreadLocal 并提供了一些使用技巧。

    编写线程安全类是困难的。它不但要求仔细分析在什么条件可以对变量进行读写,而且要求仔细分析其它类能如何使用某个类。 有时,要在不影响类的功能、易用性或性能的情况下使类成为线程安全的是很困难的。有些类保留从一个方法调用到下一个方法调用的状态信息,要在实践中使这样 的类成为线程安全的是困难的。

    管理非线程安全类的使用比试图使类成为线程安全的要更容易些。非线程安全类通常可以安全地在多线程程序中使用,只要您能确保一个线程所用的类的实例不被其它线程使用。例如,JDBC Connection 类是非线程安全的 — 两个线程不能在小粒度级上安全地共享一个 Connection — 但如果每个线程都有它自己的 Connection ,那么多个线程就可以同时安全地进行数据库操作。

    不使用 ThreadLocal 为每个线程维护一个单独的 JDBC 连接(或任何其它对象)当然是可能的;Thread API 给了我们把对象和线程联系起来所需的所有工具。而 ThreadLocal 则使我们能更容易地把线程和它的每线程(per-thread)数据成功地联系起来。

    2. 什么是线程局部变量(thread-local variable)?

    线程局部变量高效地为每个使用它的线程提供单独的线程局部变量值的副本。每个线程只能看到与自己相联系的值,而不知道别的线程可 能正在使用或修改它们自己的副本。一些编译器(例如 Microsoft Visual C++ 编译器或 IBM XL FORTRAN 编译器)用存储类别修饰符(像 static 或 volatile )把对线程局部变量的支持集成到了其语言中。Java 编译器对线程局部变量不提供特别的语言支持;相反地,它用 ThreadLocal 类实现这些支持, 核心 Thread 类中有这个类的特别支持。

    因为线程局部变量是通过一个类来实现的,而不是作为 Java 语言本身的一部分,所以 Java 语言线程局部变量的使用语法比内建线程局部变量语言的使用语法要笨拙一些。要创建一个线程局部变量,请实例化类 ThreadLocal 的一个对象。 ThreadLocal 类的行为与 java.lang.ref 中的各种 Reference 类的行为很相似; ThreadLocal 类充当存储或检索一个值时的间接句柄。清单 1 显示了 ThreadLocal 接口。

    清单 1. ThreadLocal 接口

    public class ThreadLocal {
        public Object get();
        public void set(Object newValue);
        public Object initialValue();
    }
    

    get() 访问器检索变量的当前线程的值; set() 访问器修改当前线程的值。 initialValue() 方法是可选的,如果线程未使用过某个变量,那么您可以用这个方法来设置这个变量的初始值;它允许延迟初始化。用一个示例实现来说明 ThreadLocal 的工作方式是最好的方法。清单 2 显示了 ThreadLocal 的一个实现方式。它不是一个特别好的实现(虽然它与最初实现非常相似),所以很可能性能不佳,但它清楚地说明了 ThreadLocal 的工作方式。

    清单 2. ThreadLocal 的糟糕实现

    public class ThreadLocal {
        private Map values = Collections.synchronizedMap(new HashMap());
        public Object get() {
            Thread curThread = Thread.currentThread();
            Object o = values.get(curThread);
            if (o == null && !values.containsKey(curThread)) {
                o = initialValue();
                values.put(curThread, o);
            }
            return o;
        }
        public void set(Object newValue) {
            values.put(Thread.currentThread(), newValue);
        }
        public Object initialValue() {
            return null;
        }
    }
    

    这个实现的性能不会很好,因为每个 get() 和 set() 操作都需要 values 映射表上的同步,而且如果多个线程同时访问同一个 ThreadLocal ,那么将发生争用。此外,这个实现也是不切实际的,因为用 Thread 对象做 values 映射表中的关键字将导致无法在线程退出后对 Thread 进行垃圾回收,而且也无法对死线程的 ThreadLocal 的特定于线程的值进行垃圾回收。

    3. 用 ThreadLocal 实现每线程 Singleton

    线程局部变量常被用来描绘有状态“单子”(Singleton) 或线程安全的共享对象,或者是通过把不安全的整个变量封装进 ThreadLocal ,或者是通过把对象的特定于线程的状态封装进 ThreadLocal 。例如,在与数据库有紧密联系的应用程序中,程序的很多方法可能都需要访问数据库。在系统的每个方法中都包含一个 Connection 作为参数是不方便的 — 用“单子”来访问连接可能是一个虽然更粗糙,但却方便得多的技术。然而,多个线程不能安全地共享一个 JDBC Connection 。如清单 3 所示,通过使用“单子”中的 ThreadLocal ,我们就能让我们的程序中的任何类容易地获取每线程 Connection 的一个引用。这样,我们可以认为 ThreadLocal 允许我们创建 每线程单子。

    清单 3. 把一个 JDBC 连接存储到一个每线程 Singleton 中

    public class ConnectionDispenser {
        private static class ThreadLocalConnection extends ThreadLocal {
            public Object initialValue() {
                return DriverManager.getConnection(ConfigurationSingleton.getDbUrl());
            }
        }
        private ThreadLocalConnection conn = new ThreadLocalConnection();
        public static Connection getConnection() {
            return (Connection) conn.get();
        }
    }
    

    任何创建的花费比使用的花费相对昂贵些的有状态或非线程安全的对象,例如 JDBC Connection 或正则表达式匹配器,都是可以使用每线程单子(singleton)技术的好地方。当然,在类似这样的地方,您可以使用其它技术,例如用池,来安全地管理 共享访问。然而,从可伸缩性角度看,即使是用池也存在一些潜在缺陷。因为池实现必须使用同步,以维护池数据结构的完整性,如果所有线程使用同一个池,那么 在有很多线程频繁地对池进行访问的系统中,程序性能将因争用而降低。

    4. 用 ThreadLocal 简化调试日志纪录

    其它适合使用 ThreadLocal 但用池却不能成为很好的替代技术的应用程序包括存储或累积每线程上下文信息以备稍后检索之用这样的应用程序。例如,假设您想创建一个用于管理多线程应用程序调试信息的工具。您可以用如清单 4 所示的 DebugLogger 类作为线程局部容器来累积调试信息。在一个工作单元的开头,您清空容器,而当一个错误出现时,您查询该容器以检索这个工作单元迄今为止生成的所有调试信息。

    清单 4. 用 ThreadLocal 管理每线程调试日志

    public class DebugLogger {
        private static class ThreadLocalList extends ThreadLocal {
            public Object initialValue() {
                return new ArrayList();
            }
            public List getList() {
                return (List) super.get();
            }
        }
        private ThreadLocalList list = new ThreadLocalList();
        private static String[] stringArray = new String[0];
        public void clear() {
            list.getList().clear();
        }
        public void put(String text) {
            list.getList().add(text);
        }
        public String[] get() {
            return list.getList().toArray(stringArray);
        }
    }
    

    在您的代码中,您可以调用 DebugLogger.put() 来保存您的程序正在做什么的信息,而且,稍后如果有必要(例如发生了一个错误),您能够容易地检索与某个特定线程相关的调试信息。 与简单地把所有信息转储到一个日志文件,然后努力找出哪个日志记录来自哪个线程(还要担心线程争用日志纪录对象)相比,这种技术简便得多,也有效得多。

    ThreadLocal 在基于 servlet 的应用程序或工作单元是一个整体请求的任何多线程应用程序服务器中也是很有用的,因为在处理请求的整个过程中将要用到单个线程。您可以通过前面讲述的每线程单子技术用 ThreadLocal 变量来存储各种每请求(per-request)上下文信息。

    5. ThreadLocal 的线程安全性稍差的堂兄弟,InheritableThreadLocal

    ThreadLocal 类有一个亲戚,InheritableThreadLocal,它以相似的方式工作,但适用于种类完全不同的应用程序。创建一个线程时如果保存了所有 InheritableThreadLocal 对象的值,那么这些值也将自动传递给子线程。如果一个子线程调用 InheritableThreadLocal 的 get() ,那么它将与它的父线程看到同一个对象。为保护线程安全性,您应该只对不可变对象(一旦创建,其状态就永远不会被改变的对象)使用 InheritableThreadLocal ,因为对象被多个线程共享。 InheritableThreadLocal 很合适用于把数据从父线程传到子线程,例如用户标识(user id)或事务标识(transaction id),但不能是有状态对象,例如 JDBC Connection 。

    七、ThreadLocal 的性能

    虽然线程局部变量早已赫赫有名并被包括 Posix pthreads 规范在内的很多线程框架支持,但最初的 Java 线程设计中却省略了它,只是在 Java 平台的版本 1.2 中才添加上去。在很多方面, ThreadLocal 仍在发展之中;在版本 1.3 中它被重写,版本 1.4 中又重写了一次,两次都专门是为了性能问题。

    在 JDK 1.2 中, ThreadLocal 的实现方式与清单 2 中的方式非常相似,除了用同步 WeakHashMap 代替 HashMap 来存储 values 之外。(以一些额外的性能开销为代价,使用 WeakHashMap 解决了无法对 Thread 对象进行垃圾回收的问题。)不用说, ThreadLocal 的性能是相当差的。

    Java 平台版本 1.3 提供的 ThreadLocal 版本已经尽量更好了;它不使用任何同步,从而不存在可伸缩性问题,而且它也不使用弱引用。相反地,人们通过给 Thread 添加一个实例变量(该变量用于保存当前线程的从线程局部变量到它的值的映射的 HashMap )来修改 Thread 类以支持 ThreadLocal 。因为检索或设置一个线程局部变量的过程不涉及对可能被另一个线程读写的数据的读写操作,所以您可以不用任何同步就实现 ThreadLocal.get() 和 set() 。而且,因为每线程值的引用被存储在自已的 Thread 对象中,所以当对 Thread 进行垃圾回收时,也能对该 Thread 的每线程值进行垃圾回收。

    不幸的是,即使有了这些改进,Java 1.3 中的 ThreadLocal 的性能仍然出奇地慢。据我的粗略测量,在双处理器 Linux 系统上的 Sun 1.3 JDK 中进行 ThreadLocal.get() 操作,所耗费的时间大约是无争用同步的两倍。性能这么差的原因是 Thread.currentThread() 方法的花费非常大,占了 ThreadLocal.get() 运行时间的三分之二还多。虽然有这些缺点,JDK 1.3 ThreadLocal.get() 仍然比争用同步快得多,所以如果在任何存在严重争用的地方(可能是有非常多的线程,或者同步块被频繁地执行,或者同步块很大), ThreadLocal 可能仍然要高效得多。

    在 Java 平台的最新版本,即版本 1.4b2 中, ThreadLocal 和 Thread.currentThread() 的性能都有了很大提高。有了这些提高, ThreadLocal 应该比其它技术,如用池,更快。由于它比其它技术更简单,也更不易出错,人们最终将发现它是避免线程间出现不希望的交互的有效途径。

    八、ThreadLocal 的好处

    ThreadLocal 能带来很多好处。它常常是把有状态类描绘成线程安全的,或者封装非线程安全类以使它们能够在多线程环境中安全地使用的最容易的方式。使用 ThreadLocal 使我们可以绕过为实现线程安全而对何时需要同步进行判断的复杂过程,而且因为它不需要任何同步,所以也改善了可伸缩性。除简单之外,用 ThreadLocal 存储每线程单子或每线程上下文信息在归档方面还有一个颇有价值好处 — 通过使用 ThreadLocal ,存储在 ThreadLocal 中的对象都是 不被线程共享的是清晰的,从而简化了判断一个类是否线程安全的工作。

    未完待续...

    相关文章

      网友评论

        本文标题:JAVA线程安全及性能的优化笔记(五)——ThreadLocal

        本文链接:https://www.haomeiwen.com/subject/qxwgmhtx.html