本帖最后由 小江哥 于 2017-12-5 13:18 编辑
ThreadLocal 要编写一个多线程安全(Thread-safe)的程序是困难的,为了让线程共享资源,必须小心地对共享资源进行同步,同步带来一定的效能延迟,而另一方面,在处理同步的时候,又要注意对象的锁定与释放,避免产生死结,种种因素都使得编写多线程程序变得困难。 那么我们可以尝试从另一个角度来思考多线程共享资源的问题,既然共享资源这么困难,那么就干脆不要共享,何不为每个线程创造一个资源的复本。将每一个线程存取数据的行为加以隔离,实现的方法就是给予每个线程一个特定空间来保管该线程所独享的资源。 1.概念:ThreadLocal类用来提供线程内部的局部变量。这些变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量,ThreadLocal实例通常来说都是private static类型。 总结:ThreadLocal不是为了解决多线程访问共享变量,而是为每个线程创建一个单独的变量副本,提供了保持对象的方法和避免参数传递的复杂性。 2.实现原理:ThreadLocal可以看做是一个容器,容器里面存放着属于当前线程的变量。ThreadLocal类提供了四个对外开放的接口方法,这也是用户操作ThreadLocal类的基本方法:
1)void set(Object value)设置当前线程的线程局部变量的值。
2)public Object get()该方法返回当前线程所对应的线程局部变量。
3) public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
4) protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次,ThreadLocal中的缺省实现直接返回一个null。
可以通过上述的几个方法实现ThreadLocal中变量的访问,数据设置,初始化以及删除局部变量,那ThreadLocal内部是如何为每一个线程维护变量副本的呢? 其实在ThreadLocal类中有一个静态内部类ThreadLocalMap(其类似于Map),用键值对的形式存储每一个线程的变量副本,ThreadLocalMap中元素的key为当前ThreadLocal对象,而value对应线程的变量副本,每个线程可能存在多个ThreadLocal。 3.案例分析:在今天的分析中我们以SimpleDateFormat为例,做线程局部变量的分析,使用threadlocal来解决SimpleDateFormat引起的线程安全问题。先来看SimpleDateFormat线程不安全的原因:SimpleDateFormat(下面简称sdf)类内部有一个Calendar对象引用,它用来储存和这个sdf相关的日期信息,例如sdf.parse(dateStr), sdf.format(date) 诸如此类的方法参数传入的日期相关String, Date等等, 都是交友Calendar引用来储存的.这样就会导致一个问题,如果你的sdf是个static的, 那么多个thread 之间就会共享这个sdf, 同时也是共享这个Calendar引用, 并且, 观察 sdf.parse() 方法,你会发现有如下的调用: [Java] 纯文本查看 复制代码 Date parse() {
calendar.clear(); // 清理calendar
... // 执行一些操作, 设置 calendar 的日期什么的
calendar.getTime(); // 获取calendar的时间
} 这里会导致的问题就是, 如果线程A 调用了 sdf.parse(), 并且进行了calendar.clear()后还未执行calendar.getTime()的时候,线程B又调用了sdf.parse(), 这时候线程B也执行了sdf.clear()方法, 这样就导致线程A的的calendar数据被清空了(实际上A,B的同时被清空了). 又或者当 A 执行了calendar.clear() 后被挂起, 这时候B 开始调用sdf.parse()并顺利结束, 这样 A 的 calendar内存储的的date 变成了后来B设置的calendar的date,这就造成了多线程访问同一变量造成了不同步的问题。下面我们在代码中重现这个错误:
[Java] 纯文本查看 复制代码 public class Demo01 {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static class ParseDate implements Runnable {
private int i;
public ParseDate(int i) {
super();
this.i = i;
}
@Override
public void run() {
try {
Date parse = sdf.parse("2017-11-17 15:30:" + i % 60);
System.out.println(i + "=========== " + parse);
} catch (ParseException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
newFixedThreadPool.execute(new ParseDate(i));
}
newFixedThreadPool.shutdown();
}
} 查看控制台我们发现报了错误: [Java] 纯文本查看 复制代码 944=========== Fri Nov 17 15:30:44 GMT+08:00 2017
950=========== Fri Nov 17 15:30:50 GMT+08:00 2017
Exception in thread "pool-1-thread-137" java.lang.NumberFormatException: For input string: ""
951=========== Fri Nov 17 15:30:51 GMT+08:00 2017
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:453)
at java.lang.Long.parseLong(Long.java:483)952=========== Fri Nov 17 15:30:52 GMT+08:00 2017
at java.text.DigitList.getLong(DigitList.java:194)
at java.text.DecimalFormat.parse(DecimalFormat.java:1316)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2088)
953=========== Fri Nov 17 15:30:53 GMT+08:00 2017 而这个错误的原因就是上面我们分析的,多线程同时使用simpleDateFormat这个对象造成的。 在之前我们的多线程访问同一对象的时候我们的解决方案是使用synchronized关键字将多线程访问的不安全的对象或方法包裹起来使同一时间只有一个线程可以访问,具体实现如下: [Java] 纯文本查看 复制代码 synchronized (sdf) {
try {
Date parse = sdf.parse("2017-11-17 15:30:" + i % 60);
System.out.println(i + "=========== " + parse);
} catch (ParseException e) {
e.printStackTrace();
}
} 这样确实可以解决多线程使用simpleDateFormat这个对象的问题,但是今天我们使用的是另外一种解决方案也就是使用threadlocal来解决这个问题。既然多线程都要访问这个对象,而这个对象是不安全的,那么我们完全可以给每个线程都存放一个这样的对象,threadlocal就是起存贮这个对象的作用。具体实现如下: [Java] 纯文本查看 复制代码 public class Demo02 {
static ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<SimpleDateFormat>();
public static class ParseDate implements Runnable {
private int i;
public ParseDate(int i) {
super();
this.i = i;
}
@Override
public void run() {
if (tl.get() == null) {
tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
try {
Date parse = tl.get().parse("2017-11-17 15:30:" + i % 60);
System.out.println(i + " =========== " + parse);
} catch (ParseException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
newFixedThreadPool.execute(new ParseDate(i));
}
newFixedThreadPool.shutdown();
}
} 在这里我们要强调一下,每一个threadlocal里面存贮的都是一个独立的作用完全一样的对象,而不仅仅是一个引用。我们看下面代码: [Java] 纯文本查看 复制代码 public class Demo03 {
static ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<SimpleDateFormat>();
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static class ParseDate implements Runnable {
private int i;
public ParseDate(int i) {
super();
this.i = i;
}
@Override
public void run() {
if (tl.get() == null) {
tl.set(sdf);
}
try {
Date parse = tl.get().parse("2017-11-17 15:30:" + i % 60);
System.out.println(i + " =========== " + parse);
} catch (ParseException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
newFixedThreadPool.execute(new ParseDate(i));
}
newFixedThreadPool.shutdown();
}
} 总体和demo02没有太大区别,但是demo03却报错了,错误跟demo01一模一样,这是为什么呢?这就是因为threadlocal存贮的必须是一个独立的具有相同功能的对象可以称之为复本,也就是说必须一致,而demo03中存储的是一个引用,存贮的实际上是同一个对象,这就没有解决我们之前所说的线程问题,所以就会报出和demo01一样的错误。经过案列分析,你有没有对threadlocal有一个更深层的认识了呢?接下来就是介绍它优缺点的时候了。 4.优点与缺点:ThreadLocal使用场合主要解决多线程中数据因并发产生不一致问题。ThreadLocal为每个线程的中并发访问的数据提供一个副本,通过访问副本来运行业务,这样的结果是耗费了内存,大大减少了线程同步所带来性能消耗,也减少了线程并发控制的复杂度。 ThreadLocal的使用比synchronized要简单得多。 ThreadLocal和Synchonized都用于解决多线程并发访问。但是ThreadLocal与synchronized有本质的区别。 a. synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。 b. ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。 Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。 当然ThreadLocal并不能替代synchronized,它们处理不同的问题域。Synchronized用于实现同步机制,比ThreadLocal更加复杂
校区捷报
众览群雄,唯我杭城独秀—一贴汇总杭州校区所有就业薪资
|