首页 > 代码库 > 深入理解ThreadLocal

深入理解ThreadLocal

一. ThreadLocal是什么?

ThreadLocal,顾名思义,它不是一个线程,而是线程的一个本地化对象。当工作于多线程中的对象使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程分配一个独立的初始化变量副本。所以每一个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。从线程的角度看,这个变量就像是线程的本地变量,这也是类名中“Local”所要表达的意思。 


下面我们看一个例子:

public class ThreadLocalTest1 {
	private static Index num = new Index();  

	// 创建一个Index类型的本地变量 
	private static ThreadLocal<Index> local = new ThreadLocal<Index>() {
		@Override
		protected Index initialValue() {
			return num; // 注意这里,返回的是一个已经定义好的对象num,而不是new Index() --> 返回的不是副本
		}
	};

	public static void main(String[] args) throws InterruptedException {
		Thread[] threads = new Thread[5];
		for (int i = 0; i < 5; i++) { 
			threads[i] = new Thread(new Runnable() {
				@Override
				public void run() {
					// 取出当前线程的本地变量,并累加5次
					Index index = local.get();
					for (int i = 0; i < 5; i++) {
						index.increase();
					}
					
					// 重新设置累加后的本地变量
					local.set(index);
					System.out.println(Thread.currentThread().getName() + " : " + index.num);
				}
			}, "Thread-" + i);
		}
		
		for (Thread thread : threads) {
			thread.start();
		}
	}

	static class Index {
		int num;
		public void increase() {
			num++;
		}
	}
}
运行结果如下:

Thread-0 : 5
Thread-1 : 10
Thread-3 : 15
Thread-2 : 20
Thread-4 : 25
我们会发现,每一次运行的结果都不一样,为什么会出现这样的情况?

上面的代码中,我们通过覆盖initialValue函数来给我们的ThreadLocal提供初始值,每个线程都会获取这个初始值的一个副本,而现在我们的初始值是一个定义好的对象,num是这个对象的引用。换句话说我们的初始值是一个引用,线程的引用副本指向的是同一个对象。如下图:


把方法改造一下:创建对象的副本而不是对象应用的副本

// 创建一个Index类型的本地变量 
private static ThreadLocal<Index> local = new ThreadLocal<Index>() {
	@Override
	protected Index initialValue() {
		return new Index(); // 注意这里
	}
};


来看另外一个例子:

public class ThreadLocalTest2 {

	// 创建一个Integer型的线程本地变量
	public static final ThreadLocal<Integer> local = new ThreadLocal<Integer>() {
		@Override
		protected Integer initialValue() {
			return 0;
		}
	};

	public static void main(String[] args) throws InterruptedException {
		Thread[] threads = new Thread[5];
		for (int i = 0; i < 5; i++) { 
			threads[i] = new Thread(new Runnable() {
				@Override
				public void run() {
					// 获取当前线程的本地变量,然后累加5次
					int num = local.get();
					for (int i = 0; i < 5; i++) {
						num++;
					}
					
					// 重新设置累加后的本地变量
					local.set(num);
					System.out.println(Thread.currentThread().getName() + " : " + local.get());
				}
			}, "Thread-" + i);
		}

		for (Thread thread : threads) {
			thread.start();
		}
	}
}
运行结果如下:

Thread-4 : 5
Thread-1 : 5
Thread-2 : 5
Thread-0 : 5
Thread-3 : 5
我们可以看到,每个线程累加后的结果都是5,各个线程处理自己的本地变量值,线程之间互不影响。


二. ThreadLocal的接口方法

ThreadLocal从代码看其实很简单,每个线程调用set方法时,相当于往内部的Map中增加一条记录,key是各自的线程,value是各自线程调用set放进去的值。

public 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:
      public class SimpleThreadLocal {
      	private Map map = Collections.synchronizedMap(new HashMap());
      
      	public void set(Object newValue) {
      		// 键为线程对象,值为本线程的变量副本
      		map.put(Thread.currentThread(), newValue);
      	}
      
      	public Object get() {
      		Thread currentThread = Thread.currentThread();
      
      		// 返回本线程对应的变量
      		Object obj = map.get(currentThread); 
      
      		// 如果在Map中不存在,放到Map中保存起来
      		if (obj == null && !map.containsKey(currentThread)) {
      			obj = initialValue();
      			map.put(currentThread, obj);
      		}
      		return obj;
      	}
      
      	public void remove() {
      		map.remove(Thread.currentThread());
      	}
      
      	public Object initialValue() {
      		return null;
      	}
      }

      三. ThreadLocal与同步机制的比较

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

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

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

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

    四. 实际应用

    1. hibernate中使用:

    public class HibernateUtil {
    	private static Log log = LogFactory.getLog(HibernateUtil.class);
    	
    	// 定义SessionFactory
    	private static final SessionFactory sessionFactory; 
    
    	// 创建线程局部变量session,用来保存Hibernate的Session
    	public static final ThreadLocal local = new ThreadLocal();  
    	
    	static {
    		try {
    			// 通过默认配置文件hibernate.cfg.xml创建SessionFactory
    			sessionFactory = new Configuration().configure().buildSessionFactory();
    		} catch (Throwable ex) {
    			log.error("初始化SessionFactory失败!", ex);
    			throw new ExceptionInInitializerError(ex);
    		}
    	}
    
    
    	/**
    	 * 获取当前线程中的Session
    	 * @return Session
    	 * @throws HibernateException
    	 */
    	public static Session currentSession() throws HibernateException {
    		Session session = (Session) local.get();  
    		// 如果Session还没有打开,则新开一个Session
    		if (session == null) {
    			session = sessionFactory.openSession();
    			local.set(session); // 将新开的Session保存到线程局部变量中
    		}
    		return session;
    	}
    
    	public static void closeSession() throws HibernateException {
    		// 获取线程局部变量,并强制转换为Session类型
    		Session session = (Session) local.get();
    		local.set(null);
    		if (session != null)
    			session.close();
    	}
    }

    2. 事务控制中使用:

    将会另写博文记录

五. 参考文章:
1. http://stamen.iteye.com/blog/1535120
2. http://my.oschina.net/clopopo/blog/149368
3. http://lavasoft.blog.51cto.com/62575/51926/




深入理解ThreadLocal