多线程情况下,怎么结合当前资源(硬件资源,如cpu,内存,IO等,缓层服务、软件负载等)进行有效控制,一般如何考虑和规划这方面的设计?
第1个问题我的博客上回复了,第2个问题能否说下具体是控制什么?
ThreadLocal类是个什么东西的确不容易解释。要深入理解ThreadLocal类,还是得从为什么有这个类说起。
打个比方说。两个小孩玩一台遥控小汽车玩具,一个时刻只能有一个小孩操控遥控器,另外一个小孩只能等待,弄不好两个小孩还会为抢遥控器的控制权而打架!因此,共享是好的,但是有时也会产生一些问题。于是,我们容易想到一个解决由共享导致的麻烦,那就是不共享——给两个小孩给咱买一台同一型号的遥控小汽车,让它们各自玩各自的!
回到多线程编程领域,多线程编程中共享变量(数据)往往导致要加锁,而锁又会导致等待以及上下文切换、死锁等开销和问题。因此,有时候不共享是最好的。这就是引入ThreadLocal的原因。
多数情况下,我们访问一个变量值是通过使用相应的变量名进行的。我们可以把ThreadLocal类的一个实例看做变量名,通过这个变量名我们可以获得一个变量值,这个变量值同时还与具体的线程相关联。也就是说,特定线程与特定这样的变量名的组合决定了一个特定的变量值。也就是说,假设Java中有这样一个关键字 thread_specific,它可以用来修饰某个变量。这样的变量一旦被多个线程访问,各个线程所得到的变量值总是属于该线程所特有的那一份,彼此之间互不干扰。这个假设的关键字所起到的作用正是ThreadLocal类所要实现的效果。
private thread_specific SimpleDateFormat threadSpecificSdf=new SimpleDateFormat("MMddHHmmss");
形象地说ThreadLocal类可以这样理解:每个线程都持有一个其特有(私有)的一个储物柜。一个储物柜可以有多个储物箱,每个储物箱中存放的东西就是变量值。每个线程只能访问自己的储物柜而不能访问别的线程的拥有储物柜。并且,每个储物箱都有一把钥匙(Key),一把钥匙只能开一个储物箱。一把钥匙就是一个ThreadLocal实例。因此,我们就可以看到下面的这种决定关系:
{线程对象(储物柜),ThreadLocal实例(储物箱钥匙)}→变量值(储物箱中存放的东西)
例如,
{thread1,threadLocalA}→String1
{thread1,threadLocalB}→String2
{thread2,threadLocalC}→String3
{thread2,threadLocalD}→String4
《Java多线程编程实战指南(设计模式篇)》第10章讲解的设计模式的实现使用了ThreadLocal类。这一章的“模式评价与实现考量”一节总结了ThreadLocal类的4种典型应用场景。书中有详细的结束和示例代码,这里我简单列举下。
场景一:需要使用非线程安全对象,但是又不希望引入锁。
这个典型的例子就是在多线程环境中在不加锁的情况下保证对SimpleDateFormat类使用的线程安全。如下代码所示:
public class SimpleDateFormatExample {
// 注意这里!SimpleDateFormat是非线程安全,这意味着直接在多个线程间共享它是有问题的。
private static ThreadLocal<SimpleDateFormat> tlSdf = new ThreadLocal<SimpleDateFormat>() {
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("MMddHHmmss");
};
};
public void someOper(Date date) {
String ts = tlSdf.get().format(date);
System.out.println(ts);
}
}
场景二:需要使用线程安全对象,但是希望避免其使用的锁的开销和相关问题。
比如,随机数生成器类Random是个线程安全的对象,这是因为它内部使用锁。虽然我们可以在多个线程间共享Random实例而不会导致线程安全问题,但是这涉及锁的开销。如果想避免这种开销,那么一个好的方法是每个线程只使用一个Random实例来生成随机数。在JDK7中引入的类java.util.concurrent.ThreadLocalRandom体现的正是这种思想。《Java多线程编程实战指南(设计模式篇)》第10章所举的实战案例就是这种应用场景,大家可以参考下。
场景三:隐式参数传递。
一个ThreadLocal类的实例可以被同一个线程内的不同方法(可以跨类)使用。具体实现通常借助单例(Singleton)模式。场景四:特定于线程的单例(Singleton)模式。
传统的单例模式实际上是保证对于某个类,一个JVM下的一个ClassLoader下最多只有一个实例。而借助ThreadLocal我们可以实现对于某个类,每个线程可以拥有该类最多一个实例。
ThreadLocal类使用时需要特别注意以下两点:
1、ThreadLocal类的实例通常设置为某个类的静态变量。
即通常的使用格式是:
private static ThreadLocal<XXX> tlVar=new ThreadLocal<XXX>() {
protected XXX initialValue() {
return new XXX();
};
};
这是因为:一个ThreadLocal实例就对应一个线程特有的变量值,如果把ThreadLocal作为某个类的实例变量,由于一个类可以有多个实例,那么就会有多个ThreadLocal实例被创建。即便是对于一个线程而言,这多个ThreadLocal实例就对应了多个该线程特有的变量值。而这通常不是我们所需要。如果我们需要为同一线程创建不同的该线程特有的变量值,那应该创建不同名字的ThreadLocal实例。例如:
private static ThreadLocal<XXX> tlVarA=new ThreadLocal<XXX>() {
protected XXX initialValue() {
return new XXX();
};
};
private static ThreadLocal<YYY> tlVarB=new ThreadLocal<YYY>() {
protected YYY initialValue() {
return new YYY();
};
};
2、在线程池环境下(如Web应用服务器下),使用ThreadLocal可能导致内存泄漏
这种内存泄漏的原因分析可以从Class(也是一个对象)及负责加载其的ClassLoader之间的关系、JDK对ThreadLocal的具体实现以及Web应用服务器加载Web应用程序的原理入手。分析起来需要花费不是篇幅,《Java多线程编程实战指南(设计模式篇)》10章有详细的分析和配图。
在此基础上,我们可以给出相应的解决方案。详情参考《Java多线程编程实战指南(设计模式篇)》10章。