如何正确使用ThreadLocal

本文详细介绍了Java中ThreadLocal类的使用方法及其在解决线程安全问题上的优势。通过示例代码展示了如何利用ThreadLocal为每个线程创建独立的变量副本,并避免了内存泄漏的风险。


转载于开源中国  原贴链接 https://my.oschina.net/u/580483/blog/113654

以及关于ThreadLocal内存泄漏的另一篇文章   https://my.oschina.net/xpbug/blog/113444#comment-list


首先看下Java6API文档中对它的描述(中文版):

该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。 例如,以下类生成对每个线程唯一的局部标识符。线程 ID 是在第一次调用 UniqueThreadIdGenerator.getCurrentThreadId() 时分配的,在后续调用中不会更改。

import java.util.concurrent.atomic.AtomicInteger;

public class UniqueThreadIdGenerator {

     private static final AtomicInteger uniqueId = new AtomicInteger(0);

     private static final ThreadLocal < Integer > uniqueNum = 
         new ThreadLocal < Integer > () {
             @Override protected Integer initialValue() {
                 return uniqueId.getAndIncrement();
         }
     };
 
     public static int getCurrentThreadId() {
         return uniqueId.get();
     }
 } // UniqueThreadIdGenerator


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

下面是我写的有关ThreadLocal内存回收的测试代码:

package misty.threadlocal;

import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * Created with IntelliJ IDEA.
 * User: Misty
 * Date: 13-3-14
 * Time: 上午8:59
 */
public class ThreadLocalTest {
	private final static ThreadLocal<My50MB> tl = new ThreadLocal<My50MB>() {
		@Override
		protected My50MB initialValue() {
			System.out.println(new Date() + " - init thread local value. in " + Thread.currentThread());
			return new My50MB();
		}
	};

	public static void main(String[] args) throws InterruptedException {
		Thread t = new Thread(new Runnable() {
			@Override
			public void run() {
				My50MB m = tl.get();
				// do anything with m
			}
		}, "one");
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				My50MB m = tl.get();
				// do anything with m
				try {
					TimeUnit.SECONDS.sleep(1);
				} catch (InterruptedException e) {
					// ignore
				}
			}
		}, "two");
		t.start();
		t2.start();
		t.join();
		System.gc();
		System.out.println(new Date() + " - GC");
		t2.join();
		System.gc();
		System.out.println(new Date() + " - GC");
		TimeUnit.MILLISECONDS.sleep(100);
	}

	public static class My50MB {
		private String name = Thread.currentThread().getName();
		private byte[] a = new byte[1024 * 1024 * 50];

		@Override
		protected void finalize() throws Throwable {
			System.out.println(new Date() + " - My 50 MB finalized. in " + name);
			super.finalize();
		}
	}
}
//输出
//Thu Mar 14 09:29:14 CST 2013 - init thread local value. in Thread[two,5,main]
//Thu Mar 14 09:29:14 CST 2013 - init thread local value. in Thread[one,5,main]
//Thu Mar 14 09:29:14 CST 2013 - GC
//Thu Mar 14 09:29:14 CST 2013 - My 50 MB finalized. in one
//Thu Mar 14 09:29:15 CST 2013 - GC
//Thu Mar 14 09:29:15 CST 2013 - My 50 MB finalized. in two 

那么ThreadLocal有何用呢?

很多时候我们会创建一些静态域来保存全局对象,那么这个对象就可能被任意线程访问到,如果它是线程安全的,这当然没什么说的。然而大部分情况下它不是线程安全的(或者无法保证它是线程安全的),尤其是当这个对象的类是由我们自己(或身边的同事)创建的(很多开发人员对线程的知识都是一知半解,更何况线程安全)。

这时候我们就需要为每个线程都创建一个对象的副本。我们当然可以用ConcurrentMap<Thread, Object>来保存这些对象,但问题是当一个线程结束的时候我们如何删除这个线程的对象副本呢?

ThreadLocal为我们做了一切。首先我们声明一个全局的ThreadLocal对象(final static,没错,我很喜欢final),当我们创建一个新线程并调用threadLocal.get时,threadLocal会调用initialValue方法初始化一个对象并返回,以后无论何时我们在这个线程中调用get方法,都将得到同一个对象(除非期间set过)。而如果我们在另一个线程中调用get,将的到另一个对象,而且始终会得到这个对象。

当一个线程结束了,ThreadLocal就会释放跟这个线程关联的对象,这不需要我们关心,反正一切都悄悄地发生了。

(以上叙述只关乎线程,而不关乎get和set是在哪个方法中调用的。以前有很多不理解线程的同学总是问我这个方法是哪个线程,那个方法是哪个线程,我不知如何回答。)

下面我们从实际角度出发。

假如我们需要在多线程环境下生成大量随机数,我们不会希望每次都创建一个Random对象,然后丢弃:

public void test1() {
	Random r = new Random();
	int i = r.nextInt();
	//use i
}
最常见的做法是创建一个全局Random对象:
public final static Random rand = new Random();

public void test2() {
	int i = rand.nextInt();
	// use i
}

 幸运的是Random类的线程安全的,我们可以肆无忌惮的在多个线程中使用它。

然而在另一个需求中,我们就没那么幸运了——日期格式化DataFormat——我们每天都在跟日期格式化打交道,同样很多人是这么做的:

//第一种,效率低下
public void test1() {
	DateFormat format = new SimpleDateFormat("yyyy-MM-dd");
	System.out.println(format.format(new Date()));
}
//第二种,单线程没问题,多线程等着哭吧
private final static DateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd");
public void test2() {
	System.out.println(FORMAT.format(new Date()));
}
//第三种,这是我自己加的,两年的工作中没见过这种写法,因为身边的同事会用synchronized关键字的都屈指可数
//这种方式依旧是效率低下(可能还不及第一种,没具体测试过)
public void test3() {
    synchronized (FORMAT) {
        System.out.println(FORMAT.format(new Date()));
    }
}
DateFormat的API写得很清楚:
同步 日期格式不是同步的。建议为每个线程创建独立的格式实例。如果多个线程同时访问一个格式,则它必须保持外部同步。


保持外部同步已经在上述第三种方法中试过了,那么我们就为每个线程创建独立实例吧。

public final static ThreadLocal<DateFormat> TL_FORMAT = new ThreadLocal<DateFormat>() {
	@Override
	protected DateFormat initialValue() {
		return new SimpleDateFormat("yyyy-MM-dd");
	}
};
public void test4() {
	DateFormat df = TL_FORMAT.get();
	System.out.println(df.format(new Date()));
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值