文档章节

【每周一讲】Java的ThreadLocal

ulyn
 ulyn
发布于 2015/08/16 23:49
字数 2374
阅读 137
收藏 8

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
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
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

没有更多内容

加载失败,请刷新页面

加载更多

设计模式之 明确责任 观察者模式 状态模式 责任链模式

观察者模式是任务分发的一种模式。 如果认为我们设计的系统的各个模块(或子系统)的最终目的是完成共同任务,那么这个任务如何分配到多个模块的就是我们遇到的第一个问题。简单设计场合我们...

backbye
22分钟前
2
0
14-利用思维导图梳理JavaSE-大汇总

14-利用思维导图梳理JavaSE-Java基础知识大汇总 主要内容 1.对象入门 2.一切都是对象 3.程序流程控制 4.初始化和消除 5.权限访问控制 6.复用类 7.多态 8.接口与抽象类 9.内部类 10.容器 11.异...

飞鱼说编程
57分钟前
6
0
利用Lombok编写优雅的spring依赖注入代码,去掉繁人的@Autowired

大家平时使用spring依赖注入,都是怎么写的? @Servicepublic class OrderService { @Autowired private UserService userService;} 是不是很熟悉的感觉?但是呢 如果你用...

HeyS1
今天
26
0
IBATIS 写BLOB字段遇到的问题

1、 首先遇到的配置问题,通过设置typeHandler 来支持写入。接下来由此引出了事务的问题。 <typeHandler jdbcType="BLOB" javaType="[B" callback="org.springframework.orm.ibatis.support....

echo-neo
今天
1
0
37. Sudoku Solver

Description tags: backtrack,hash table difficulty: hard Write a program to solve a Sudoku puzzle by filling the empty cells.A sudoku solution must satisfy all of the following......

52iSilence7
今天
1
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部