原文出处: 黄勇

ThreadLocal,直译为“线程本地”或“本地线程”,如果你真的这么认为,那就错了!其实,它就是一个容器,用于存放线程的局部变量,我认为应该叫做 ThreadLocalVariable(线程局部变量)才对,真不理解为什么当初 Sun 公司的工程师这样命名。

早在 JDK 1.2 的时代,java.lang.ThreadLocal 就诞生了,它是为了解决多线程并发问题而设计的,只不过设计得有些难用,所以至今没有得到广泛使用。其实它还是挺有用的,不相信的话,我们一起来看看这个例子吧。

一个序列号生成器的程序,可能同时会有多个线程并发访问它,要保证每个线程得到的序列号都是自增的,而不能相互干扰。

先定义一个接口:

1
2
3
4
public interface Sequence {
 
    int getNumber();
}

每次调用 getNumber() 方法可获取一个序列号,下次再调用时,序列号会自增。

再做一个线程类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ClientThread extends Thread {
 
    private Sequence sequence;
 
    public ClientThread(Sequence sequence) {
        this.sequence = sequence;
    }
 
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName() + " => " + sequence.getNumber());
        }
    }
}

在线程中连续输出三次线程名与其对应的序列号。

我们先不用 ThreadLocal,来做一个实现类吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class SequenceA implements Sequence {
 
    private static int number = 0;
 
    public int getNumber() {
        number = number + 1;
        return number;
    }
 
    public static void main(String[] args) {
        Sequence sequence = new SequenceA();
 
        ClientThread thread1 = new ClientThread(sequence);
        ClientThread thread2 = new ClientThread(sequence);
        ClientThread thread3 = new ClientThread(sequence);
 
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

序列号初始值是0,在 main() 方法中模拟了三个线程,运行后结果如下:

Thread-0 => 1
Thread-0 => 2
Thread-0 => 3
Thread-2 => 4
Thread-2 => 5
Thread-2 => 6
Thread-1 => 7
Thread-1 => 8
Thread-1 => 9

由于线程启动顺序是随机的,所以并不是0、1、2这样的顺序,这个好理解。为什么当 Thread-0 输出了1、2、3之后,而 Thread-2 却输出了4、5、6呢?线程之间竟然共享了 static 变量!这就是所谓的“非线程安全”问题了。

那么如何来保证“线程安全”呢?对应于这个案例,就是说不同的线程可拥有自己的 static 变量,如何实现呢?下面看看另外一个实现吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class SequenceB implements Sequence {
 
    private static ThreadLocal<Integer> numberContainer = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };
 
    public int getNumber() {
        numberContainer.set(numberContainer.get() + 1);
        return numberContainer.get();
    }
 
    public static void main(String[] args) {
        Sequence sequence = new SequenceB();
 
        ClientThread thread1 = new ClientThread(sequence);
        ClientThread thread2 = new ClientThread(sequence);
        ClientThread thread3 = new ClientThread(sequence);
 
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

通过 ThreadLocal 封装了一个 Integer 类型的 numberContainer 静态成员变量,并且初始值是0。再看 getNumber() 方法,首先从 numberContainer 中 get 出当前的值,加1,随后 set 到 numberContainer 中,最后将 numberContainer 中 get 出当前的值并返回。

是不是很恶心?但是很强大!确实稍微饶了一下,我们不妨把 ThreadLocal 看成是一个容器,这样理解就简单了。所以,这里故意用 Container 这个单词作为后缀来命名 ThreadLocal 变量。

运行结果如何呢?看看吧。

Thread-0 => 1
Thread-0 => 2
Thread-0 => 3
Thread-2 => 1
Thread-2 => 2
Thread-2 => 3
Thread-1 => 1
Thread-1 => 2
Thread-1 => 3

每个线程相互独立了,同样是 static 变量,对于不同的线程而言,它没有被共享,而是每个线程各一份,这样也就保证了线程安全。 也就是说,TheadLocal 为每一个线程提供了一个独立的副本!

搞清楚 ThreadLocal 的原理之后,有必要总结一下 ThreadLocal 的 API,其实很简单。

  1. public void set(T value):将值放入线程局部变量中
  2. public T get():从线程局部变量中获取值
  3. public void remove():从线程局部变量中移除值(有助于 JVM 垃圾回收)
  4. protected T initialValue():返回线程局部变量中的初始值(默认为 null)

为什么 initialValue() 方法是 protected 的呢?就是为了提醒程序员们,这个方法是要你们来实现的,请给这个线程局部变量一个初始值吧。

了解了原理与这些 API,其实想想 ThreadLocal 里面不就是封装了一个 Map 吗?自己都可以写一个 ThreadLocal 了,尝试一下吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class MyThreadLocal<T> {
 
    private Map<Thread, T> container = Collections.synchronizedMap(new HashMap<Thread, T>());
 
    public void set(T value) {
        container.put(Thread.currentThread(), value);
    }
 
    public T get() {
        Thread thread = Thread.currentThread();
        T value = container.get(thread);
        if (value == null && !container.containsKey(thread)) {
            value = initialValue();
            container.put(thread, value);
        }
        return value;
    }
 
    public void remove() {
        container.remove(Thread.currentThread());
    }
 
    protected T initialValue() {
        return null;
    }
}

以上完全山寨了一个 ThreadLocal,其中中定义了一个同步 Map(为什么要这样?请读者自行思考),代码应该非常容易读懂。

下面用这 MyThreadLocal 再来实现一把看看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class SequenceC implements Sequence {
 
    private static MyThreadLocal<Integer> numberContainer = new MyThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };
 
    public int getNumber() {
        numberContainer.set(numberContainer.get() + 1);
        return numberContainer.get();
    }
 
    public static void main(String[] args) {
        Sequence sequence = new SequenceC();
 
        ClientThread thread1 = new ClientThread(sequence);
        ClientThread thread2 = new ClientThread(sequence);
        ClientThread thread3 = new ClientThread(sequence);
 
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

以上代码其实就是将 ThreadLocal 替换成了 MyThreadLocal,仅此而已,运行效果和之前的一样,也是正确的。

其实 ThreadLocal 可以单独成为一种设计模式,就看你怎么看了。

ThreadLocal 具体有哪些使用案例呢?

我想首先要说的就是:通过 ThreadLocal 存放 JDBC Connection,以达到事务控制的能力。

如何实现呢?下回分解!

ThreadLocal 那点事儿(续集)

注意:当您在一个类中使用了 static 成员变量的时候,一定要多问问自己,这个 static 成员变量需要考虑“线程安全”吗?(也就是说,多个线程需要独享自己的 static 成员变量吗?)如果需要考虑,那就请用 ThreadLocal 吧!

ThreadLocal 那点事儿的更多相关文章

  1. ThreadLocal 那点事儿(续集)

    本篇是<ThreadLocal 那点事儿>的续集,如果您没看上一篇,就就有点亏了.如果您错过了这一篇,那亏得就更大了. 还是保持我一贯的 Style,用一个 Demo 来说话吧.用户提出一 ...

  2. ThreadLocal使用场景案例

    本篇是<ThreadLocal 那点事儿>的续集,如果您没看上一篇,就就有点亏了.如果您错过了这一篇,那亏得就更大了. 还是保持我一贯的 Style,用一个 Demo 来说话吧.用户提出一 ...

  3. 架构探险——第二章(为web应用添加业务功能)

    第二章不使用框架完成了自己的Web应用. 重点: 服务层的完善优化过程,思路 在看这一段的时候引起了无数次的共鸣.相信大家在开始接触Java Web的时候,都做过类似的封装和优化. 第一版 在Serv ...

  4. 关于 SimpleDateFormat 的非线程安全问题及其解决方案

    一直以来都是直接用SimpleDateFormat开发的,没想着考虑线程安全的问题,特记录下来(摘抄的): 1.问题: 先来看一段可能引起错误的代码: package test.date; impor ...

  5. Java_基础_02_ThreadLocal

    二.参考资料 1.ThreadLocal 那点事儿 2.彻底理解ThreadLocal

  6. ThreadLocal,Lock的事儿

    ThreadLocal作用 防止线程间的干扰 public interface Sequence { int getNumber(); } public class ClientThread exte ...

  7. ThreadLocal 工作原理、部分源码分析

    1.大概去哪里看 ThreadLocal 其根本实现方法,是在Thread里面,有一个ThreadLocal.ThreadLocalMap属性 ThreadLocal.ThreadLocalMap t ...

  8. Java 线程 — ThreadLocal

    ThreadLocal 先来看看ThreadLocal的注释: This class provides** thread-local variables**. These variables diff ...

  9. 详细领悟ThreadLocal变量

    关于对ThreadLocal变量的理解,我今天查看一下午的博客,自己也写了demo来测试来看自己的理解到底是不是那么回事.从看到博客引出不解,到仔细查看ThreadLocal源码(JDK1.8),我觉 ...

随机推荐

  1. 如何提高android串口kernel log等级

    在 /device/qcom/common/rootdir/etc/init.qcom.rc write /proc/sys/kernel/printk  "6 6 1 7" 第一 ...

  2. Spring 框架 详解 (四)------IOC装配Bean(注解方式)

    Spring的注解装配Bean Spring2.5 引入使用注解去定义Bean @Component  描述Spring框架中Bean Spring的框架中提供了与@Component注解等效的三个注 ...

  3. The Pilots Brothers' refrigerator

    2965 he Pilots Brothers' refrigerator Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 1 ...

  4. Cheatsheet: 2013 06.01 ~ 06.22

    .NET Git for Visual Studio and .NET developers How to download multiple files concurrently using Web ...

  5. Create Hierarchical Tree To Control Records In Oracle Forms

    Download Source Code Providing an example form for creating hierarchical trees in Oracle Forms to co ...

  6. DJANGO基础学习之转义总结:escape,autoescape,safe,mark_safe

    何谓转义?就是把html语言的关键字过滤掉.例如,<div>就是html的关键字,如果要在html页面上呈现<div>,其源代码就必须是<div> PS:转义其实就 ...

  7. OGNL调用静态方法和属性

    ognl的全名是 Object-Graph Navigation Language 表示的是图对象导航语言...我觉得它最厉害的一点是,通过"."来实现对象的导航...下面看他他的 ...

  8. CVE-2015-7547

    危险漏洞补丁修复通知 漏洞编号 漏洞编号为CVE-2015-7547 漏洞说明: Google安全团队近日发现glibc存在的溢出漏洞. glibc的DNS客户端解析器中存在基于栈的缓冲区溢出漏洞.当 ...

  9. ABAP DESCRIBE语句

    声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将 ...

  10. 关于gitlab的一个问题解决

    这两天折腾一个关于gitlab的问题,搞得比较焦头烂额.不过经过折腾,最终还是成功了. 当面对着一个问题,并且看到还没被解决的时候,感觉很不舒服: 努力折腾之后,解决之后,也会身体轻松.或许工程师天生 ...