- 浏览: 40396 次
- 性别:
- 来自: 上海
文章分类
最新评论
Java 多线程同步问题的探究(五、你有我有全都有—— ThreadLocal如何解决并发安全性?)【更新重要补疑】
- 博客分类:
- thread
前面我们介绍了Java当中多个线程抢占一个共享资源的问题。但不论是同步还是重入锁,都不能实实在在的解决资源紧缺的情况,这些方案只是靠制定规则来约束线程的行为,让它们不再拼命的争抢,而不是真正从实质上解决他们对资源的需求。
在JDK 1.2当中,引入了java.lang.ThreadLocal。它为我们提供了一种全新的思路来解决线程并发的问题。但是他的名字难免让我们望文生义:本地线程?
什么是本地线程?
本地线程开玩笑的说:不要迷恋哥,哥只是个传说。
其实ThreadLocal并非Thread at Local,而是LocalVariable in a Thread。
根据WikiPedia上的介绍,ThreadLocal其实是源于一项多线程技术,叫做Thread Local
Storage,即线程本地存储技术。不仅仅是Java,在C++、C#、.NET、Python、Ruby、Perl等开发平台上,该技术都已经得以实
现。
当使用ThreadLocal
维护变量时,它会为每个使用该变量的线程提供独立的变量副本
。也就是说,他从根本上解决的是资源数量的问题,从而使得每个线
程持有相对独立的资源。这样,当多个线程进行工作的时候,它们不需要纠结于同步的问题,于是性能便大大提升。但资源的扩张带来的是更多的空间消
耗,ThreadLocal就是这样一种利用空间来换取时间的解决方案。
说了这么多,来看看如何正确使用ThreadLocal。
通过研究JDK文档,我们知道,ThreadLocal中有几个重要的方法:get()、set()、remove()、initailValue(),对应的含义分别是:
返回此线程局部变量的当前线程副本中的值、将此线程局部变量的当前线程副本中的值设置为指定值、移除此线程局部变量当前线程的值、返回此线程局部变量的当前线程的“初始值”。
还记得我们在第三篇的上半节引出的那个例子么?几个线程修改同一个Student对象中的age属性。为了保证这几个线程能够工作正常,我们需要对Student的对象进行同步。
下面我们对这个程序进行一点小小的改造,我们通过继承Thread来实现多线程:
package sky.cn.test4; import java.util.Random; public class ThreadDemo3 extends Thread { private ThreadLocal<Student> stuLocal = new ThreadLocal<Student>(); public ThreadDemo3(Student stu) { stuLocal.set(stu); } public static void main(String[] args) { Student stu = new Student(); ThreadDemo3 td31 = new ThreadDemo3(stu); ThreadDemo3 td32 = new ThreadDemo3(stu); ThreadDemo3 td33 = new ThreadDemo3(stu); td31.start(); td32.start(); td33.start(); } public void run() { accessStudent(); } public void accessStudent() { String currentThreadName = Thread.currentThread().getName(); System.out.println(currentThreadName + " is running!!"); Random random = new Random(); int age = random.nextInt(100); System.out.println("Thread " + currentThreadName + " set age to: " + age); Student student = stuLocal.get(); student.setAge(age); System.out.println("Thread " + currentThreadName + " first read age is: " + student.getAge()); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Thread " + currentThreadName + " second read age is: " + student.getAge()); } }
而get方法则是首先得到当前线程的ThreadLocalMap对象,然后,根据ThreadLocal对象自身,取出相应的value。当然,如果在 当前线程中取不到ThreadLocalMap对象,则尝试为当前线程创建ThreadLocalMap对象,并以ThreadLocal对象自身为 key,把initialValue()方法产生的对象作为value放入新创建的ThreadLocalMap中。
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) return (T)e.value; } return setInitialValue(); } private T setInitialValue() { T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; } protected T initialValue() { return null; }
这样,我们就明白上面的问题出在哪里:我们在main方法执行期间,试图在调用ThreadDemo3的构造器时向ThreadLocal置入 Student对象,而此时,以ThreadLocal对象为key,Student对象 为value的Map是被放入 当前的活动线程内的。也就是 Main线程 。而当我们的3个ThreadDemo3线程运行起来 以后,调用get()方法 ,都是试图从当前的活动线程中取 得 ThreadLocalMap对象,但当前的活动线程显然已经不是Main线程 了,于是,程序最终执行了ThreadLocal原生的 initialValue() 方法,返回了null。
讲到这里,我想不少朋友一定已经看出来了:ThreadLocal的initialValue()方法是需要被覆盖的。
于是,ThreadLocal的正确使用方法是:将ThreadLocal以内部类的形式进行继承,并覆盖原来的initialValue()方法,在这里产生可供线程拥有的本地变量值。
这样,我们就有了下面的正确例程:
package sky.cn.test4; import java.util.Random; public class ThreadDemo3 extends Thread { private ThreadLocal<Student> stuLocal = new ThreadLocal<Student>() { @Override protected Student initialValue() { return new Student(); } }; public ThreadDemo3() { } public static void main(String[] args) { ThreadDemo3 td31 = new ThreadDemo3(); ThreadDemo3 td32 = new ThreadDemo3(); ThreadDemo3 td33 = new ThreadDemo3(); td31.start(); td32.start(); td33.start(); } public void run() { accessStudent(); } public void accessStudent() { String currentThreadName = Thread.currentThread().getName(); System.out.println(currentThreadName + " is running!!"); Random random = new Random(); int age = random.nextInt(100); System.out.println("Thread " + currentThreadName + " set age to: " + age); Student student = stuLocal.get(); student.setAge(age); System.out.println("Thread " + currentThreadName + " first read age is: " + student.getAge()); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Thread " + currentThreadName + " second read age is: " + student.getAge()); } }
********** 补疑 ******************
有的童鞋可能会问:“你这个Demo根本没体现出来,每个线程里都有一个ThreadLocal对象;应该是一个ThreadLocal对象对应多个线程,你这变成了一对一,完全没体现出ThreadLocal的作用。”
那么我们来看一下如何用一个ThreadLocal对象来对应多个线程:
package sky.cn.test4; import java.util.Random; public class ThreadDemo3 implements Runnable { private ThreadLocal<Student> stuLocal = new ThreadLocal<Student>() { @Override protected Student initialValue() { return new Student(); } }; public ThreadDemo3() { } public static void main(String[] args) { ThreadDemo3 td3 = new ThreadDemo3(); Thread t1 = new Thread(td3); Thread t2 = new Thread(td3); Thread t3 = new Thread(td3); t1.start(); t2.start(); t3.start(); } public void run() { accessStudent(); } public void accessStudent() { String currentThreadName = Thread.currentThread().getName(); System.out.println(currentThreadName + " is running!!"); Random random = new Random(); int age = random.nextInt(100); System.out.println("Thread " + currentThreadName + " set age to: " + age); Student student = stuLocal.get(); student.setAge(age); System.out.println("Thread " + currentThreadName + " first read age is: " + student.getAge()); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Thread " + currentThreadName + " second read age is: " + student.getAge()); } }
这里,多个线程对象都使用同一个实现了Runnable接口的ThreadDemo3对象来构造
。这样,多个线程使用的ThreadLocal对象就是同一个
。结果仍然是正确的。但是仔细回想一下,这两种实现方案有什么不同呢?
答案其实很简单,并没有本质上的不同。对于第一种实现
,不同的线程对象当中ThreadLocalMap里面的KEY使用的是不同的
ThreadLocal对象
。而对于第二种实现
,不同的线程对象当中ThreadLocalMap里面的KEY是同一个ThreadLocal对象
。但是
从本质上讲,不同的线程对象都是利用其自身的ThreadLocalMap对象来对各自的Student对象进行封装,用ThreadLocal对象作为
该ThreadLocalMap的KEY。所以说,“ThreadLocal的思想精髓就是为每个线程创建独立的资源副本。”这句话并不应当被理解成:一
定要使用同一个ThreadLocal对象来对多个线程进行处理。因为真正用来封装变量的不是ThreadLocal。就算是你的程序中所有线程都共用同
一个ThreadLocal对象,而你真正封装到ThreadLocalMap中去的仍然是.hashCode()方法返回不同值的不同对象。就好比线程
就是房东,ThreadLocalMap就是房东的房子。房东通过ThreadLocal这个中介去和房子里的房客打交道,而房东不管要让房客住进去还是
搬出来,都首先要经过ThreadLocal这个中介。
所以提到ThreadLocal,我们不应当顾名思义的认为JDK里面提供ThreadLocal就是提供了一个用来封装本地线程存储的容器,它本身并没
有Map那样的容器功能。真正发挥作用的是ThreadLocalMap。也就是说,事实上,采用ThreadLocal来提高并发行,首先要理解,这不
是一种简单的对象封装,而是一套机制,而这套机制中的三个关键因素(Thread、ThreadLocal、ThreadLocalMap)之间的关系是
值得我们引起注意的。
**************** 补疑完毕 ***************************
可见,要正确使用ThreadLocal,必须注意以下几点:
1. 总是对ThreadLocal中的initialValue()方法进行覆盖 。
2. 当使用set()或get()方法时 牢记这两个方法是对当前活动线程中的ThreadLocalMap进行操作,一定要认清哪个是当前活动线程 !
3. 适当的使用泛型,可以减少不必要的类型转换以及可能由此产生的问题。
运行该程序,我们发现:程序的执行过程只需要5秒,而如果采用同步的方法,程序的执行结果相同,但执行时间需要15秒。以前是多个线程为了争取一个资源,不得不在同步规则的制约下互相谦让,浪费了一些时间。
现在,采用ThreadLocal机制以后,可用的资源多了,你有我有全都有,所以,每个线程都可以毫无顾忌的工作,自然就提高了并发性,线程安全也得以保证。
当今很多流行的开源框架也采用ThreadLocal机制来解决线程的并发问题。比如大名鼎鼎的 Struts 2.x 和 Spring 等。
把ThreadLocal这样的话题放在我们的同步机制探讨中似乎显得不是很合适。但是ThreadLocal的确为我们解决多线程的并发问题带来了全新
的思路。它为每个线程创建一个独立的资源副本
,从而将多个线程中的数据隔离开来
,避免了同步所产生的性能问题,是一种“以空间换时间
”的解决方案。
但这并不是说ThreadLocal就是包治百病的万能药了。如果实际的情况不允许我们为每个线程分配一个本地资源副本的话,同步还是非常有意义的。
转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
好了,本系列到此马上就要划上一个圆满的句号了。不知大家有什么意见和疑问没有。希望看到你们的留言。
下一讲中我们就来对之前的内容进行一个总结,顺便讨论一下被遗忘的volatile关键字。敬请期待。
来源: http://www.blogjava.net/zhangwei217245/archive/2010/04/24/317651.html#317694
发表评论
-
Java 多线程同步问题的探究(三、Lock来了,大家都让开【2. Fair or Unfair? It is a question...】)
2012-08-15 16:12 534让我们继续前面有关ReentrantLock的话题。 首先, ... -
Java 多线程同步问题的探究(四、协作,互斥下的协作——Java多线程协作(wait、notify、notifyAll))
2012-08-15 10:38 863Java监视器支持两种线程:互斥和协作 。 前面我 ... -
Java 多线程同步问题的探究(三、Lock来了,大家都让开【1. 认识重入锁】)
2012-08-13 16:44 837在上一节中, 我们已经了解了Java多线程编程中常用的关 ... -
Java 多线程同步问题的探究(二、给我一把锁,我能创造一个规矩)
2012-08-13 15:09 655在上一篇中,我们讲到 ... -
Java多线程同步问题的探究(一、线程的先来后到)
2012-08-13 14:46 431众所周知,在Java多线程编程中,一个非常重要的方面就是线程的 ... -
ThreadLocal源码读后感总结
2012-04-23 14:47 9541.关联类 ThreadLocal: 线程局 ...
相关推荐
java ThreadLocal多线程专属的变量源码java ThreadLocal多线程专属的变量源码java ThreadLocal多线程专属的变量源码java ThreadLocal多线程专属的变量源码java ThreadLocal多线程专属的变量源码java ThreadLocal多...
资源名称:Java多线程与并发库高级应用视频教程22集资源目录:【】01传统线程技术回顾【】02传统定时器技术回顾【】03传统线程互斥技术【】04传统线程同步通信技术【】04传统线程同步通信技术_分割纪录【】05线程...
java多线程安全性基础介绍 线程安全 正确性 什么是线程安全性 原子性 竞态条件 i++ 读i ++ 值写回i 可见性 JMM 由于cpu和内存加载速度的差距,在两者之间增加了多级缓存导致,内存并不能直接对cpu可见。 ...
解决Java多线程同步的方法是在需要同步的方法签名中加入synchronized关键字,...为解决多个线程对同一变量进行访问时可能发生的安全性问题,不仅可以采用同步机制,更可以通过JDK中加入的ThreadLocal来保证更好的并发性。
目录SimpleDateFormat诡异bug复现SimpleDateFormat诡异bug字符串日期转Date日期(parse)Date日期转String类型(format)SimpleDateFormat出现bug的原因如何解决SimpleDateFormat多线程安全问题局部变量使用...
目录一、背景介绍二、TestNG多线程详解2.1 TestNG多线程实现2.2 TestNG多线程效果演示三、ThreadLocal3.1 ThreadLocal概念3.2 具体实现 一、背景介绍 在使用Selenium+TestNG做WebUI自动化过程中,为了能够加快Web...
第1章 简介 1.1 并发简史 1.2 线程的优势 1.2.1 发挥多处理器的强大能力 1.2.2 建模的简单性 1.2.3 异步事件的简化处理 1.2.4 响应更灵敏的用户界面 1.3 线程带来的风险 ...附录A 并发性标注 参考文献
synchronized关键字不属于方法特征签名的一部分,所以可以在覆盖方法的时候加上去。也就是说,在父类的方法声明上可以没有synchronized关键字,而在子类覆盖该方法时加上synchronized关键字。 注意:使用...
【2018最新最详细】并发多线程教程,课程结构如下 1.并发编程的优缺点 2.线程的状态转换以及基本操作 3.java内存模型以及happens-before规则 4.彻底理解synchronized 5.彻底理解volatile 6.你以为你真的了解final吗...
主要介绍了Java ThreadLocal 线程安全问题解决方案的相关资料,需要的朋友可以参考下
Java线程:概念与原理 Java线程:创建与启动 Java线程:线程状态的转换 Java线程:线程的同步与锁 一、同步问题提出 二、同步和锁定 三、静态方法同步 四、如果线程不能不能获得锁会怎么样 五、何时需要同步...
早在 JDK 1.2 的时代,java.lang.ThreadLocal 就诞生了,它是为了解决多线程并发问题而设计的,只不过设计得有些难用,所以至今没有得到广泛使用。其实它还是挺有用的,不相信的话,我们一起来看看这个例子吧。 一个...
java并发编程 基础知识,守护线程与线程, 并行和并发有什么区别? 什么是上下文切换? 线程和进程区别 什么是线程和进程? 创建线程有哪几种方式?,如何避免线程死锁 线程的 run()和 start()有什么区别? 什么是 ...
前 言 第1章 简介 1.1 并发简史 1.2 线程的优势 1.2.1 发挥多处理器的强大能力 1.2.2 建模的简单性 1.2.3 异步事件的简化处理 1.2.4 响应更灵敏的用户界面 1.3 线程带来的风险 ...附录A 并发性标注
java 简单的ThreadLocal示例
本章内容 掌握同步代码块的使用 掌握同步方法的使用 理解线程死锁 掌握 ThreadLocal 类的使用 使用多线程模拟猴子采花 使用同步方法模拟购票 使用多线程模拟购物订单生成 使用 ThreadLocal 类模拟银行取款 Java高级...
ThreadLocal保证一个类的实例变量在各个线程中都有一份单独的拷贝, 从而不会影响其他线程中的实例变量
从Java字节码的角度看线程安全性问题.mp4 synchronized保证线程安全的原理(理论层面).mp4 synchronized保证线程安全的原理(jvm层面).mp4 单例问题与线程安全性深入解析.mp4 理解自旋锁,死锁与重入锁.mp4 深入...
详解java底层实现原理,ThreadLocal底层实现的数据结构,为什么不会导致内存泄露
主要介绍了Java中的线程同步与ThreadLocal无锁化线程封闭实现,Synchronized关键字与ThreadLocal变量的使用是Java中线程控制的基础,需要的朋友可以参考下