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<Thread, Integer> map = new HashMap<Thread, Integer>(); 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<People> threadLocal = new ThreadLocal<People>(); 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<Integer> seqNum = new ThreadLocal<Integer>() { 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<T>版本。
概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
Spring使用ThreadLocal解决线程安全问题我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,因为有状态的Bean就可以在多线程中共享了。
一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程,如图9‑2所示:
同一线程贯通三层这样你就可以根据需要,将一些非线程安全的变量以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<Connection> connThreadLocal = new ThreadLocal<Connection>(); 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<Connection> connectionHolder = new ThreadLocal<Connection>() { 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 }
总结: