文档章节

【每周一讲】Java的ThreadLocal

ulyn
 ulyn
发布于 2015/08/16 23:49
字数 2374
阅读 131
收藏 7
点赞 0
评论 0

1、初识她

    她到底是谁? 姑且先看个例子。

public interface Counter {

    /**
     * 获取下一个序列值
     * @return
     */
    int getNextNum();
}
public class SimpleCounter implements Counter {

    private int i = 0;

    @Override
    public int getNextNum() {
        return i++;
    }

}
public class TestCounter extends Thread {

    private Counter counter;

    public TestCounter(Counter counter) {
        this.counter = counter;
    }

    public void run() {
        for (int i = 0; i < 3; i++) {
            // ④每个线程打出3个序列值
            System.out.println("thread[" + Thread.currentThread().getName() + "] --> count["
                    + counter.getNextNum() + "]");
        }
    }

    /**
     * 测试多个线程处理共享对象的情况
     * 使用ThreadLocal的方式,每个线程拥有各自独立的数据拷贝。即时同一个对象
     * @param args
     */
    public static void main(String[] args) {
        Counter counter = new SimpleCounter();
//        Counter counter = new ThreadLocalCounter();
        // ③ 3个线程共享counter,各自产生序列号
        TestCounter t1 = new TestCounter(counter);
        TestCounter t2 = new TestCounter(counter);
        TestCounter t3 = new TestCounter(counter);
        t1.start();
        t2.start();
        t3.start();
    }
}

    对多线程了解的亲,肯定一眼识之,多线程操作同一个对象变量。结果:

thread[Thread-1] --> count[1]
thread[Thread-2] --> count[2]
thread[Thread-0] --> count[0]
thread[Thread-0] --> count[5]
thread[Thread-0] --> count[6]
thread[Thread-2] --> count[4]
thread[Thread-1] --> count[3]
thread[Thread-1] --> count[8]
thread[Thread-2] --> count[7]

    我想让它每个线程都不互相影响,于是,ThreadLocal姑娘出现了。

public class ThreadLocalCounter implements Counter {

    // ①通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值
    private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() {
        public Integer initialValue() {
            return 0;
        }
    };

    // ②获取下一个序列值
    public int getNextNum() {
        int i = seqNum.get();
        seqNum.set(i + 1);
        return i;
    }

}

    是的,打开main方法的

Counter counter = new ThreadLocalCounter();

    运行,可以看到结果:

thread[Thread-2] --> count[0]
thread[Thread-1] --> count[0]
thread[Thread-0] --> count[0]
thread[Thread-1] --> count[1]
thread[Thread-2] --> count[1]
thread[Thread-1] --> count[2]
thread[Thread-0] --> count[1]
thread[Thread-2] --> count[2]
thread[Thread-0] --> count[2]

    完全变了,这同样是操作同一个对象呢,但是每个线程都是从0开始累加到2。

2、小窥她

2.1 概述

    声明:以下大部分文字都是摘抄整理。

    线程同步是进行多线程编程时所必须考虑的一个问题。之所以要进行同步,是因为多个线程需要访问共享资源,典型的是共享内存数据。如果能为每个线程提供一份需要共享的数据的copy,那么对该数据的访问也就没有必要进行同步了。

    Thread Local Storage(TLS),就是能够达到这个目的的一个多线程设计模式。顾名思义,就是“线程本地数据”,指每个线程拥有各自独立的数据拷贝。

    Java类库中的ThreadLocal类就是该模式的一个实现,JDK1.6源码描述了她

/**
 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * <tt>get</tt> or <tt>set</tt> method) has its own, independently initialized
 * copy of the variable.  <tt>ThreadLocal</tt> instances are typically private
 * static fields in classes that wish to associate state with a thread (e.g.,
 * a user ID or Transaction ID).

    大致意思:ThreadLocal类型的变量不同于普通变量,每个访问它的线程都有一份各自独立初始化的copy,对它的访问是通过get/set方法实现的。ThreadLocal实例典型情况下是类的private static字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。 

    API表达了下面几种观点:

  • ThreadLocal不是线程,是线程的一个变量,你可以先简单理解为线程类的属性变量。

  • ThreadLocal 在类中通常定义为静态类变量。

  •  每个线程有自己的一个ThreadLocal,它是变量的一个“拷贝”,修改它不影响其他线程。 

    既然定义为类变量,为何为每个线程维护一个副本(姑且成为“拷贝”容易理解),让每个线程独立访问?多线程编程的经验告诉我们,对于线程共享资源(你可以理解为属性),资源是否被所有线程共享,也就是说这个资源被一个线程修改是否影响另一个线程的运行,如果影响我们需要使用synchronized同步,让线程顺序访问。ThreadLocal适用于资源共享但不需要维护状态的情况,也就是一个线程对资源的修改,不影响另一个线程的运行;这种设计是“空间换时间”,synchronized顺序执行是“时间换取空间”。

    有人说:ThreadLocal不是用来解决对象共享访问问题的,而主要是提供了保持对象的方法和避免参数传递的方便的对象访问方式。

    我认为:开头例子中,她还真是多个线程访问同一个对象seqNum,只是很巧妙的又给每个线程返回了“独立初始化的copy”,使得线程对它的访问都是相互独立的。确实,致使她能以方便地对象访问方式来保持对象的方法和避免参数传递,我们常用的Hibernate的session,执行状态环境的上下文Context也都是用到了她。

2.2 ThreadLocal方法

  • T get()  返回此线程局部变量的当前线程副本中的值。

  • protected T initialValue()  返回此线程局部变量的当前线程的“初始值”。

  • void remove()  移除此线程局部变量当前线程的值。(ps:当线程销毁时,它也会被销毁)

  • void set(T value)  将此线程局部变量的当前线程副本中的值设置为指定值。


2.3 深入源码

    网络上太多了,主要贴出主要代码:

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();
    }

    另外,有兴趣的,请自己思考

  • 既然是线程安全,为什么要再定义一个ThreadLocalMap,而不直接使用HashMap

  • ThreadLocalMap.Entry extends WeakReference<ThreadLocal>,这个这个....

3、拥抱她

    既然她提供了当前线程的一份可以访问的数据,我们就可以很方便的在同一个线程中,执行的不同方法中取得到她,避免了参数的传递。紧紧拥抱她吧,我们自己写个上下文。

public class CurrentContext {
    private int i = 1;
    private boolean isEnabled = true;

    public int getI() {
        return i;
    }

    public void setI(int i) {
        this.i = i;
    }

    public boolean isEnabled() {
        return isEnabled;
    }

    public void setEnabled(boolean isEnabled) {
        this.isEnabled = isEnabled;
    }

    private static ThreadLocal<CurrentContext> threadLocal = new ThreadLocal<CurrentContext>();

    public static CurrentContext get(){
        return threadLocal.get();
    }
    public static void remove(){threadLocal.remove();}

    public void addContext(){threadLocal.set(this);}
}
public class TestTransferContext {

    public static void main(String[] args) {
        CurrentContext context = new CurrentContext();
        System.out.println(CurrentContext.get());
        context.addContext();
        System.out.println(CurrentContext.get());
        doSomething();
        System.out.println(String.format("i=%s,isEnabled=%s"
                ,CurrentContext.get().getI(),CurrentContext.get().isEnabled()));
    }

    private static void doSomething() {
        System.out.println(String.format("i=%s,isEnabled=%s"
                ,CurrentContext.get().getI(),CurrentContext.get().isEnabled()));
        CurrentContext.get().setEnabled(false);
        CurrentContext.get().setI(2);
    }
}

    最终,可以看到,doSomething中并没有进行参数传递,但是我们确实是可以取得main中放进去的上下文对象。

null
com.sunsharing.ulyn.test.context.CurrentContext@173a10f
i=1,isEnabled=true
i=2,isEnabled=false

4、遇上线程掉进池

    理想很美好,现实却是坑坑洼洼。天气太热,4线程君都想跳进池中游泳。这泳池只能容纳两个线程君。前两个线程君进池的时候,管理员说池中要有肥皂,给了他们每人一块。他们俩舒服走人了,后面两位线程池君继续进来,管理员却告诉他们,不能再给你们了。不公平不公平!!!为什么?好吧,就让代码模拟下。

public class TestThreadPool {

    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(2);
        for(int i=0;i<4;i++){
            service.execute(new Service(i));
        }
    }

    private static class Service implements Runnable{
        private final int i;

        public Service(int i) {
            this.i = i;
        }

        @Override
        public void run() {
            if(CurrentContext.get()!=null){
                System.out.println(i + " getContext="+CurrentContext.get());
            }else{
                CurrentContext currentContext = new CurrentContext();
                currentContext.addContext();
                System.out.println(i + " addContext="+currentContext);
            }
        }
    }
}

    执行结果:

0 addContext=com.sunsharing.ulyn.test.context.CurrentContext@863399
1 addContext=com.sunsharing.ulyn.test.context.CurrentContext@a59698
2 getContext=com.sunsharing.ulyn.test.context.CurrentContext@863399
3 getContext=com.sunsharing.ulyn.test.context.CurrentContext@a59698

    看吧,池中已经有肥皂了,你们还想要,没门!

    其实,使用ThreadLocal非常容易掉的坑就是如此,尤其javaweb开发,web容器都是线程池管理,稍不留神就把前一次操作的数据留了下来,反而成为了脏数据。更严重的是,脏数据却成为了活靶子,狠狠的把以前的数据库数据删除了。这是一次惨痛的经历。

    还记得那次,由于某些原因,数据库不能用事务。那不行我就自己做下回滚,把新增的删除掉。不可能每次新增就去手动写一次记录吧,这时候就想到利用上下文,动手就做。

public class InsertRecordContext {

    public static final ThreadLocal threadSession = new ThreadLocal();
    public static InsertRecordContext get(){
        return (InsertRecordContext)threadSession.get();
    }
    public static void remove(){threadSession.remove();}

    public void addContext(){threadSession.set(this);}

    private List<DaoBeanWrapper> recordList = new ArrayList<DaoBeanWrapper>();

    public List<DaoBeanWrapper> getRecordList() {
        return recordList;
    }

    public void addInsertDaoBeanWrapper(DaoBeanWrapper daoBeanWrapper) {
        recordList.add(daoBeanWrapper);
    }
    /**
     * 会滚当前会话事务,不支持事务的数据库可以用啊,请一定一定要在finally里面 clearSession啊
     * 并且记得在开头resetSession啊
     */
    public static void currentSessionRollback(){
        InsertRecordContext context = InsertRecordContext.get();
        if(context!=null){
            List<DaoBeanWrapper> rec = context.getRecordList();
            for(DaoBeanWrapper daoBeanWrapper : rec){
                daoBeanWrapper.getJdbcDao().del(daoBeanWrapper.getClazz());
            }
        }
    }
    public static void resetSession(){
        InsertRecordContext.remove();
    }
    public static void clearSession(){
        InsertRecordContext.remove();
    }
    private static InsertRecordContext getCurrentSession(){
        InsertRecordContext context = InsertRecordContext.get();
        if(context == null){
            context = new InsertRecordContext();
            context.addContext();;
        }
        return context;
    }
}

    每次新增的时候,往上下文中增加这么一条新增的记录

InsertRecordContext session = getCurrentSession();
session.addInsertDaoBeanWrapper(new DaoBeanWrapper(this,t));

    当发现异常时候,执行回滚。

try{
    //doSomething...
}catch(Exception e){
            InsertRecordContext.currentSessionRollback();
        }

    总以为很美好,假如,前N次都是正常的,后来出现一次异常,因为线程未及时清理上下文数据,那么currentSessionRollback删除的可不只是这次add进去上下文的list。finally你却会发现它有多重要

finally {
            InsertRecordContext.clearSession();
        }

    请记住,使用了ThreadLocal,请及时让她自身清洁remove。finally 你会发现,她很漂亮!


© 著作权归作者所有

共有 人打赏支持
ulyn
粉丝 56
博文 16
码字总数 18070
作品 1
厦门
程序员
JDK、JRE、JVM三者间的关系

  JDK(Java Development Kit)是针对Java开发员的产品,是整个Java的核心,包括了Java运行环境JRE、Java工具和Java基础类库。Java Runtime Environment(JRE)是运行JAVA程序所必须的环境...

张德德
2014/02/28
0
0
JDK 1.7 基本概念和目录结构

参考资料: http://blog.csdn.net/kindazrael/article/details/7270673 http://docs.oracle.com/javase/7/docs/index.html JDK and JRE File Structure http://docs.oracle.com/javase/7/doc......

jack688
06/26
0
0
一句话讲清楚什么是JavaEE

Java技术不仅是一门编程语言而且是一个平台。同时Java语言是一门有着特定语法和风格的高级的面向对象的语言,Java平台是Java语言编写的特定应用程序运行的环境。Java平台有很多种,很多的Jav...

qq58edeba279279
06/26
0
0
My java——JVM(java 虚拟机)一

JVM是Java Virtual Machine(Java虚拟机)的缩写。一般我们在学习java中会用到很多缩写名称,如JRE、JDK、SDK、JAVA SE、JAVA EE、JAVA ME、JAVA FX、还有j2se、j2ee、javaee5,我勒个去!多...

tngou
2013/03/13
0
2
linux下的jdk环境变量配置

新安装的ubuntu不能以root用户登录,可以这样做 sudo passwd root //然后设置密码 su //输入密码登录 ubuntu 新建简单文本 touch hello.sh //新建文件 hello.sh vim hello.sh // 打开hello文...

苏云飞
2015/11/18
0
0
【目录导航】JAVA零基础进阶之路

【JAVA零基础入门系列】(已完结)导航目录 Day1 开发环境搭建 Day2 Java集成开发环境IDEA Day3 Java基本数据类型 Day4 变量与常量 Day5 Java中的运算符 Day6 Java字符串 Day7 Java输入与输出...

MFrank
06/21
0
0
ThreadLocal in Java - Example Program and Tutorial

ThreadLocal in Java is another way to achieve thread-safety apart from writing immutable classes. If you have been writing multi-threaded or concurrent code in Java then you mus......

perfectspr
2014/12/10
0
0
JVM基础:深入学习JVM堆与JVM栈

以前堆是干啥栈是干啥都知道,就是没连在一起想想。感觉讲的不错的一篇儿~~JVM栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;JVM堆解决的是数据存储的问题,即数据怎么放、放在...

李星
2014/06/04
0
0
JVM -verbose参数详解(转)

转自:http://www.javaranger.com/archives/367 java -verbose[:class|gc|jni] 在输出设备上显示虚拟机运行信息。 1.java -verbose:class 在程序运行的时候有多少类被加载!你可以用verbose...

巴顿
2014/12/04
0
0
ubuntu install sun-java

Ubuntu安装Java环境如何安装 1. 从http://java.sun.com/下载jdk的bin文件,将下载 的jdk1.6.0_16.bin文件放到/usr/lib/jvm/java中 然后在终端执行代码: sudo chmod u+x /usr/lib/jvm/java/jd...

eric_zhang
2011/10/13
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

java集合元素的默认大小

当底层实现涉及到扩容时,容器或重新分配一段更大的连续内存(如果是离散分配则不需要重新分配,离散分配都是插入新元素时动态分配内存),要将容器原来的数据全部复制到新的内存上,这无疑使...

竹叶青出于蓝
1分钟前
0
0
Java快速开发平台,JEECG 3.7.7闪电版本发布,增加多套主流UI代码生成器模板

JEECG 3.7.7 闪电版本发布,提供5套主流UI代码生成器模板 导读 ⊙平台性能优化,速度闪电般提升 ⊙提供5套新的主流UI代码生成器模板(Bootstrap表单+BootstrapTable列表\ ElementUI列表表单)...

Jeecg
4分钟前
0
0
export 和 module.export 的区别

在浏览器端 js 里面,为了解决各模块变量冲突等问题,往往借助于 js 的闭包把左右模块相关的代码都包装在一个匿名函数里。而 Nodejs 编写模块相当的自由,开发者只需要关注 require,exports,...

孟飞阳
6分钟前
0
0
技术教育的兴起

技术教育的兴起 作者: 阮一峰 1、 有一年,我在台湾环岛旅行。 花莲的海边,我遇到一对台湾青年夫妻,带着女儿在海滩上玩。我们聊了起来。 当时,我还在高校当老师。他们问我,是否觉得台湾...

吕伯文
6分钟前
0
0
Linux服务器下的HTTP抓包分析

说到抓包分析,最简单的办法莫过于在客户端直接安装一个Wireshark或者Fiddler了,但是有时候由于客户端开发人员(可能是第三方)知识欠缺或者其它一些原因,无法顺利的在客户端进行抓包分析,...

mylxsw
11分钟前
0
0
mybatis3-javaapi

sqlSessionFactoryBuilder->sqlSessionFactory->sqlSession<-rowbound<-resultHandler myBatis uses a Java enumeration wrapper for transaction isolation levels, called TransactionIsol......

writeademo
14分钟前
0
0
Java NIO:浅析I/O模型

也许很多朋友在学习NIO的时候都会感觉有点吃力,对里面的很多概念都感觉不是那么明朗。在进入Java NIO编程之前,我们今天先来讨论一些比较基础的知识:I/O模型。下面本文先从同步和异步的概念...

yzbty23
15分钟前
0
0
了解iOS消息推送一文就够:史上最全iOS Push技术详解

本文作者:陈裕发, 腾讯系统测试工程师,由腾讯WeTest整理发表。 1、引言 开发iOS系统中的Push推送,通常有以下3种情况: 1)在线Push:比如QQ、微信等IM界面处于前台时,聊天消息和指令都会...

JackJiang-
16分钟前
0
0
Mysql汉子转拼音

update t_app_city SET CITY_NAME_BEGIN = ELT(INTERVAL(CONV(HEX(LEFT(CONVERT(CITY_NAME USING gbk),1)),16,10), 0xB0A1,0xB0C5,0xB2C1,0xB4EE,0xB6EA,0xB7A2,0xB8C1,0xB9FE,0xBBF7, 0xBFA......

尘叙缘
18分钟前
0
0
大数据构建智慧城市“新引擎”,加速推进新旧动能转换

——“大数据与智慧城市”技术交流分享会——济南站召开 7月13日,“大数据携手智慧城市,助力山东新旧动能转换”技术交流分享会——济南站在山东信息通信技术研究院会议室成功举办,此次会议...

左手的倒影
20分钟前
2
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部