【每周一讲】Java的ThreadLocal

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
[email protected]
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 [email protected]9
1 [email protected]8
2 [email protected]9
3 [email protected]8

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

其实,使用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 你会发现,她很漂亮!

时间: 2024-10-14 05:16:38

【每周一讲】Java的ThreadLocal的相关文章

java concurrency: ThreadLocal及其实现机制

转载:http://shmilyaw-hotmail-com.iteye.com/blog/1703382 ThreadLocal概念 从字面上来理解ThreadLocal,感觉就是相当于线程本地的.我们都知道,每个线程在jvm的虚拟机里都分配有自己独立的空间,线程之间对于本地的空间是相互隔离的.那么ThreadLocal就应该是该线程空间里本地可以访问的数据了.ThreadLocal变量高效地为每个使用它的线程提供单独的线程局部变量值的副本.每个线程只能看到与自己相联系的值,而不知道别的线程可

Java之ThreadLocal原理分析

简介 早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路.使用这个工具类可以很简洁地编写出优美的多线程程序.当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本.从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思.所以,在Java中编写线程局部变量

深入研究java.lang.ThreadLocal类

深入研究java.lang.ThreadLocal类 一.概述 ThreadLocal是什么呢?其实ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是threadlocalvariable(线程局部变量).也许把它命名为ThreadLocalVar更加合适.线程局部变量(ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副

从Linux的errno到Java的ThreadLocal

Linux里的errno 在Linux下执行系统调用时,一般会有一个返回值表示成功或失败,但是这个值只说明了成功或失败,却没有说明是如何成功或失败的. errno就是为了解决这个问题的,系统调用会把错误号设置为errno,我们通过错误号就能知道失败的原因.还可以使用strerror打印出这个错误号对应的字符串说明.老版本的Linux需要加上extern int errno,现在直接引入<errno.h>就行了. errno示例: 现在的问题来了.假如我有两个线程,代码如下: errno是一个全

从零讲Java,给你一条清晰地学习道路!该学什么就学什么!

从零讲JAVA ,给你一条 清晰地学习道路!该学什么就学什么! 1.计算机基础: 1.1数据机构基础: 主要学习:1.向量,链表,栈,队列和堆,词典.熟悉2.树,二叉搜索树.熟悉3.图,有向图,无向图,基本概念4.二叉搜索A,B,C类熟练,9大排序熟悉.5.树的前中后,层次,之字,最短路.6.KMP等字符串算法. 1.2操作系统: 主要学习:1.进程,线程,进程线程区别.进程间通信2.进程调度算法理解3.存储,虚拟内存,分页分段,内存调度算法4.文件系统,链式,索引5.死锁:原因,避免,解除k6

java的ThreadLocal类的用法

java的ThreadLocal类的用法,ThreadLocal是一个支持泛型的类,用在多线程中用于防止并发冲突问题. 例如下面的一个例子,就是用于线程增加1,但是相互不冲突 package com.test.threadlocal; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; class Local { private static ThreadLocal<Integer

也讲Java NIO

也讲Java NIO 一点开场白 百度搜索java nio,前面的几个帖子总是从各种基础概念介绍起,通道.缓冲区.选择器- 然后看着看着就晕了,所以,经过一晚上的研究,我想从自己的理解讲讲nio. 一.单线程的通信 在没有nio之前,java妥妥的可以进行CS项目间的通信,来个最简单的例子.(懒得写,抄了段) server 端 package nio.nonio; import java.io.BufferedReader; import java.io.BufferedWriter; impo

吐哈社区 每周一讲

吐哈社区 每周一讲   公开课 第一讲  选择艺术  权衡 第二讲  创新和不创新的一墙之隔   ...期待哦

转!! Java中ThreadLocal的设计与使用

首先,ThreadLocal 不是用来解决共享对象的多线程访问问题的,一般情况下,通过ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的.各个线程中访问的是不同的对象. 另外,说ThreadLocal使得各线程能够保持各自独立的一个对象,并不是通过ThreadLocal.set()来实现的,而是通过每个线程中的new 对象 的操作来创建的对象,每个线程创建一个,不是什么对象的拷贝或副本.通过ThreadLocal.set()将这个新创建