文档章节

【每周一讲】Java的ThreadLocal

ulyn
 ulyn
发布于 2015/08/16 23:49
字数 2374
阅读 140
收藏 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
粉丝 57
博文 16
码字总数 18070
作品 1
厦门
程序员
私信 提问
使用ANT生成证书的时候出错

我想使用Ant生成CA证书,在做好build.xml,写好./bin/cli.xml 执行脚本时出现 [java] Initializing CA [java] Generating rootCA keystore: [java] CA name: AdminRootCA [java] SuperAdmin CN......

fringe-liu
2012/06/26
440
2
【目录导航】JAVA零基础进阶之路

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

MFrank
06/21
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
访问不了http://localhost:8080/ecommerce和https://localhost:8443/webtools?

1.ofbiz版本是:ofbiz12 2.根据这篇博文(http://blog.csdn.net/xiaoliouc/article/details/10223095?reload#html),前面都没有问题,但是在最后一步: 1)执行ant load-demo,成功创建了数...

对岸
2014/11/12
3.2K
2
Ubuntu 13.10 64位下安装配置 JDK 7

第一步:下载 jdk-7u45-linux-x64.tar.gz wget -c http://download.oracle.com/otn-pub/java/jdk/7u45-b18/jdk-7u45-linux-x64.tar.gz 或者直接下载:http://download.oracle.com/otn-pub/ja......

姚君
2014/03/24
0
0

没有更多内容

加载失败,请刷新页面

加载更多

EOS官方钱包keosd

EOS官方钱包的名称是keosd,它负责管理你的私钥,并且帮你进行交易的签名。 不过不幸的是,keosd钱包对普通用户并不友好,它是一个命令行程序,目前还没有像以太坊的mist那样的图形化界面,而...

汇智网教程
今天
23
0
ArrayList的实现原理以及实现线程安全

一、ArrayList概述 ArrayList是基于数组实现的,是一个动态的数字,可以自动扩容。 ArrayList不是线程安全的,效率比较高,只能用于单线程的环境中,在多线程环境中可以使用Collections.syn...

一看就喷亏的小猿
今天
25
0
Netty 备录 (一)

入职新公司不久,修修补补1个月的bug,来了点实战性的技术---基于netty即时通信 还好之前对socket有所使用及了解,入手netty应该不是很难吧,好吧,的确有点难,刚看这玩意的时候,可能都不知道哪里...

_大侠__
昨天
33
0
Django简单介绍和用户访问流程

Python下有许多款不同的 Web 框架。Django是重量级选手中最有代表性的一位。许多成功的网站和APP都基于Django。 Django是一个开放源代码的Web应用框架,由Python写成。 Django遵守BSD版权,初...

枫叶云
昨天
41
0
Spring Cloud Stream消费失败后的处理策略(四):重新入队(RabbitMQ)

应用场景 之前我们已经通过《Spring Cloud Stream消费失败后的处理策略(一):自动重试》一文介绍了Spring Cloud Stream默认的消息重试功能。本文将介绍RabbitMQ的binder提供的另外一种重试...

程序猿DD
昨天
22
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部