大家通常知道,ThreadLocal类可以帮助我们实现线程的安全性,这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。从概念上看,我们把ThreadLocal<T>理解成一个包含了Map<Thread,T>的对象,其中Map的key用来标识不同的线程,而Map的value存放了特定该线程的某个值。但是ThreadLocal的实现并非如此,我们以这样的理解方式去使用ThreadLocal也并不能实现真正的线程安全。

  下面我们举一个例子进行说明,Number是拥有一个int型成员变量的类:

public class Number {

    private int num;

    public int getNum() {
return num;
} public void setNum(int num) {
this.num = num;
} @Override
public String toString() {
return "Number [num=" + num + "]";
} }

  NotSafeThread是一个实现了Runable接口的类,其中我们创建了一个ThreadLocal<Number>类型的变量value,用来存放不同线程的num值,接着我们用线程池的方式启动了5个线程,我们希望使用ThreadLocal类为5个不同的线程都存放一个Number类型的副本,根除对变量的共享,并且在调用ThreadLocal类的get()方法时,返回与线程关联的Number对象,而这些Number对象我们希望它们都能跟踪自己的计数值:

public class NotSafeThread implements Runnable {

    public static Number number = new Number();

    public static int i = 0;

    public void run() {
//每个线程计数加一
number.setNum(i++);
     //将其存储到ThreadLocal中
value.set(number);
//输出num值
System.out.println(value.get().getNum());
} public static ThreadLocal<Number> value = new ThreadLocal<Number>() {
}; public static void main(String[] args) {
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
newCachedThreadPool.execute(new NotSafeThread());
}
} }

  启动程序:输出结果

0
1
2
3
4

  看起来一切正常,每个线程好像都有自己关于Number的存储空间,但是我们简单的在输出前加一个延时:

public class NotSafeThread implements Runnable {

    public static Number number = new Number();

    public static int i = 0;

    public void run() {
//每个线程计数加一
number.setNum(i++);
//将其存储到ThreadLocal中
value.set(number);
//延时2秒
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
}
//输出num值
System.out.println(value.get().getNum());
} public static ThreadLocal<Number> value = new ThreadLocal<Number>() {
}; public static void main(String[] args) {
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
newCachedThreadPool.execute(new NotSafeThread());
}
} }

  运行程序,输出:

4
4
4
4
4

  为什么每个线程都输出4?难道他们没有独自保存自己的Number副本吗?为什么其他线程还是能够修改这个值?我们看一下ThreadLocal的源码:

    public void set(Object obj)
{
Thread thread = Thread.currentThread();//获取当前线程
ThreadLocalMap threadlocalmap = getMap(thread);
if(threadlocalmap != null)
threadlocalmap.set(this, obj);
else
createMap(thread, obj);
}

  其中getMap方法:

    ThreadLocal.ThreadLocalMap getMap(Thread thread)
{
return thread.inheritableThreadLocals;//返回的是thread的成员变量
}

  可以看到,这些特定于线程的值是保存在当前的Thread对象中,并非保存在ThreadLocal对象中。并且我们发现Thread对象中保存的是Object对象的一个引用,这样的话,当有其他线程对这个引用指向的对象做修改时,当前线程Thread对象中保存的值也会发生变化。这也就是为什么上面的程序为什么会输出一样的结果:5个线程中保存的是同一Number对象的引用,在线程睡眠2s的时候,其他线程将num变量进行了修改,因此它们最终输出的结果是相同的。

  那么,ThreadLocal的“为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。”这句话中的“独立的副本”,也就是我们理解的“线程本地存储”只能是每个线程所独有的对象并且不与其他线程进行共享,大概是这样的情况:

    public static ThreadLocal<Number> value = new ThreadLocal<Number>() {
public Number initialValue(){//为每个线程保存的值进行初始化操作
return new Number();
}
};

  或者

    public void run() {
value.set(new Number());
}

  好吧...这个时候估计你会说:那这个ThreadLocal有什么用嘛,每个线程都自己new一个对象使用,只有它自己使用这个对象而不进行共享,那么程序肯定是线程安全的咯。这样看起来我不使用ThreadLocal,在需要用某个对象的时候,直接new一个给本线程使用不就好咯。

  确实,ThreadLocal的使用不是为了能让多个线程共同使用某一对象,而是我有一个线程A,其中我需要用到某个对象o,这个对象o在这个线程A之内会被多处调用,而我不希望将这个对象o当作参数在多个方法之间传递,于是,我将这个对象o放到TheadLocal中,这样,在这个线程A之内的任何地方,只要线程A之中的方法不修改这个对象o,我都能取到同样的这个变量o。

  再举一个在实际中应用的例子,例如,我们有一个银行的BankDAO类和一个个人账户的PeopleDAO类,现在需要个人向银行进行转账,在PeopleDAO类中有一个账户减少的方法,BankDAO类中有一个账户增加的方法,那么这两个方法在调用的时候必须使用同一个Connection数据库连接对象,如果他们使用两个Connection对象,则会开启两段事务,可能出现个人账户减少而银行账户未增加的现象。使用同一个Connection对象的话,在应用程序中可能会设置为一个全局的数据库连接对象,从而避免在调用每个方法时都传递一个Connection对象。问题是当我们把Connection对象设置为全局变量时,你不能保证是否有其他线程会将这个Connection对象关闭,这样就会出现线程安全问题。解决办法就是在进行转账操作这个线程中,使用ThreadLocal中获取Connection对象,这样,在调用个人账户减少和银行账户增加的线程中,就能从ThreadLocal中取到同一个Connection对象,并且这个Connection对象为转账操作这个线程独有,不会被其他线程影响,保证了线程安全性。

  代码如下:

public class ConnectionHolder {

    public static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
}; public static Connection getConnection(){
Connection connection = connectionHolder.get();
if(null == connection){
connection = DriverManager.getConnection(DB_URL);
connectionHolder.set(connection);
}
return connection;
} }

  在框架中,我们需要将一个事务上下文(Transaction  Context)与某个执行中的线程关联起来。通过将事务上下文保存在静态的ThreaLocal对象中(这个上下文肯定是不与其他线程共享的),可以很容易地实现这个功能:当框架代码需要判断当前运行的是哪一个事务时,只需从这个ThreadLocal对象中读取事务上下文,避免了在调用每个方法时都需要传递执行上下文信息。

  

用ThreadLocal类实现线程安全的正确姿势的更多相关文章

  1. Android PermissionUtils:运行时权限工具类及申请权限的正确姿势

    Android PermissionUtils:运行时权限工具类及申请权限的正确姿势 ifadai 关注 2017.06.16 16:22* 字数 318 阅读 3637评论 1喜欢 6 Permis ...

  2. 线程系列5--java中的ThreadLocal类实现线程范围内的数据共享(二)

    ThreadLocal类可以理解成一个类似与map集合使用,以当前线程当做key 来使用,将线程氛围内需要共享的数据当做value,形成键值对的形式使用.ThreadLocal和线程同步机制都是为了解 ...

  3. 并发和多线程(二)--启动和中断线程(Interrupt)的正确姿势

    启动线程: 从一个最基本的面试题开始,启动线程到底是start()还是run()? Runnable runnable = () -> System.out.println(Thread.cur ...

  4. 深入理解java:2.4. 线程本地变量 java.lang.ThreadLocal类

    ThreadLocal,很多人都叫它做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多. 可能很多朋友都知道ThreadLocal为变量在每个线程中都创建了一个副本,那样每个线程可以访问自己内 ...

  5. Java线程与并发库高级应用-线程范围内共享数据ThreadLocal类

    1.线程范围内共享变量 1.1 前奏: 使用一个Map来实现线程范围内共享变量 public class ThreadScopeShareData { static Map<Thread, In ...

  6. ThreadLocal类及常用的线程安全类探究

    1.ThreadLocal类 ThreadLocal是Thread Local Variable的简称,意思是线程局部变量.作用是为每一个使用该变量的线程都提供一个该变量的副本,使每一个线程都能独立操 ...

  7. 线程变量---ThreadLocal类

    用处:保存线程的独立变量.对一个线程类(继承自Thread) 思想:如果一个资源会引起线程竞争,那就为每一个线程配置一个资源.相比于synchronized是一种空间换时间的策略 当使用ThreadL ...

  8. ThreadLocal类的理解

    首先,ThreadLocal 不是用来解决共享对象的多线程访问问题的,一般情况下,通过ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的.各 ...

  9. ThreadLocal类详解

    学习一个东西首先要知道为什么要引入它,就是我们能用它来干什么.所以我们先来看看ThreadLocal对我们到底有什么用,然后再来看看它的实现原理. ThreadLocal如果单纯从名字上来看像是“本地 ...

随机推荐

  1. Unity - Apk包的代码与资源提取

    最近在研究如何给Unity游戏进行加密,让别人不能轻易破解你的apk包,不过网上的加密方法都是有对应的破解方法~_~!!结果加密方法没找到好的,逆向工程倒会了不少.今天就来讲解如何提取一个没做任何保护 ...

  2. Hbuilder开发HTML5 APP之创建子页面

    折腾了好久,终于看明白怎么创建了: 1.创建个html5的mui页面,在其初始化方法中: mui.init({   subpages:[{    id:"list",   url: ...

  3. python获取当前时间的用法

    1.先导入库:import datetime 2.获取当前日期和时间:now_time = datetime.datetime.now() 3.格式化成我们想要的日期:strftime() 比如:“2 ...

  4. ABP理论学习之仓储

    返回总目录 本篇目录 IRepository接口 查询 插入 更新 删除 其他 关于异步方法 仓储实现 管理数据库连接 仓储的生命周期 仓储最佳实践 Martin Fowler对仓储的定义 位于领域层 ...

  5. SQL Server 查询分解

    标签:SQL SERVER/MSSQL SERVER/数据库/DBA/查询步骤 概述 查询步骤是很基础也挺重要的一部分,但是我还是在周围发现有些人虽然会语法,但是对于其中的步骤不是很清楚,这里就来分解 ...

  6. Appium的安装-MAC平台

    其实Appium的安装方式主要有两种: 1)自己安装配置nodejs的环境,然后通过npm进行appium的安装 2)直接下载官网提供的dmg进行安装,dmg里面已经有nodejs的环境和appium ...

  7. OpenGL学习资料汇总

    我学OpenGL的3D编程也有1.2个年头了,走了很多弯路,也算有点收获.现在整理出一些好用的资料如下. NeHe OpenGL教程中文版 地址(http://www.yakergong.net/ne ...

  8. 移动APP服务端设计开发注意要点

    2014年,移动APP的热度丝毫没有减退,怎么为您的移动端app设计良好的服务器端接口(API)呢? 下面谈谈我个人的一些想法. 2014年,移动APP的热度丝毫没有减退,并没有像桌面软件被WEB网站 ...

  9. 2013 duilib入门简明教程 -- 简单控件介绍 (12)

        前面的教程应该让大家对duilib的整体有所映像了,下面就来介绍下duilib具体控件的使用.     由于官方没有提供默认的控件样式,所以我就尽量使用win7或者XP自带的按钮样式了,虽然界 ...

  10. Android笔记——eclipse快捷键

    查看快捷键定义的地方 Window->Preferences->General->Keys. 编辑相关快捷键 1. [ALT+/] 自动提示方法 此快捷键为用户编辑的好帮手,能为用户 ...