您的位置:首页 > 其它

深入理解ThreadLocal

2014-09-09 14:22 549 查看


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,各个线程处理自己的本地变量值,线程之间互不影响。



TheadLocal的接口方法

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. 事务控制中使用:

将会另写博文记录: http://blog.csdn.net/zdp072/article/details/39214867


参考文章

1. http://stamen.iteye.com/blog/1535120 2. http://my.oschina.net/clopopo/blog/149368 3. http://lavasoft.blog.51cto.com/62575/51926/
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: