【每周一讲】Java的ThreadLocal

喜你入骨 提交于 2020-04-07 05:04:38

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


易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!