Java并发编程:ThreadLocal的使用以及实现原理解析

前言

前面的文章里,我们学习了有关锁的使用,锁的机制是保证同一时刻只能有一个线程访问临界区的资源,也就是通过控制资源的手段来保证线程安全,这固然是一种有效的手段,但程序的运行效率也因此大大降低。那么,有没有更好的方式呢?答案是有的,既然锁是严格控制资源的方式来保证线程安全,那我们可以反其道而行之,增加更多资源,保证每个线程都能得到所需对象,各自为营,互不影响,从而达到线程安全的目的,而ThreadLocal便是采用这样的思路。

ThreadLocal实例

ThreadLocal翻译成中文的话大概可以说是:线程局部变量,也就是只有当前线程能够访问。它的设计作用是为每一个使用该变量的线程都提供一个变量值的副本,每个线程都是改变自己的副本并且不会和其他线程的副本冲突,这样一来,从线程的角度来看,就好像每个线程都拥有了该变量。

下面是一个简单的实例:

public class ThreadLocalDemo {

    static ThreadLocal<Integer> local = new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public static class MyRunnable implements Runnable{

        @Override
        public void run() {
            for (int i = 0;i<3;i++){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                int value = local.get();
                System.out.println(Thread.currentThread().getName() + ":" + value);
                local.set(value + 1);
            }
        }
    }

    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
    }
}

上面的代码不难理解,首先是定义了一个名为 local 的ThreadLocal变量,并初识变量的值为0,然后是定义了一个实现Runnable接口的内部类,在其run方法中对local 的值做读取和加1的操作,最后是main方法中开启两个线程来运行内部类实例。

以上就是代码的大概逻辑,运行main函数后,程序的输出结果如下:

Thread-0:0
Thread-1:0
Thread-1:1
Thread-0:1
Thread-1:2
Thread-0:2

从结果可以看出,虽然两个线程都共用一个Runnable实例,但两个线程中所展示的ThreadLocal的数据值并不会相互影响,也就是说这种情况下的local 变量保存的数据相当于是线程安全的,只能被当前线程访问。

ThreadLocal实现原理

那么ThreadLocal内部是怎么保证对象是线程私有的呢?毫无疑问,答案需要从源码中查找。回顾前面的代码,可以发现其中调用了ThreadLocal的两个方法setget,我们就从这两个方法入手。

先看 set() 的源码:

public void set(T value) {
    Thread t = Thread.currentThread();
    // 获取线程的ThreadLocalMap,返回map
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        //map为空,创建
        createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

set的代码逻辑比较简单,主要是把值设置到当前线程的一个ThreadLocalMap对象中,而ThreadLocalMap可以理解成一个Map,它是定义在Thread类中内部的成员,初始化是为null,

ThreadLocal.ThreadLocalMap threadLocals = null;

不过,与常见的Map实现类,如HashMap之类的不同的是,ThreadLocalMap中的Entry是继承于WeakReference类的,保持了对 “键” 的弱引用和对 “值” 的强引用,这是类的源码:

static class ThreadLocalMap {

    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }

    //省略剩下的源码
    ....................
}

从源码中中可以看出,Entry构造函数中的参数 k 就是ThreadLocal实例,调用super(k) 表明对 k 是弱引用,使用弱引用的原因在于,当没有强引用指向 ThreadLocal 实例时,它可被回收,从而避免内存泄露,那么为何需要防止内存泄露呢?原因下面会说到。

接着说set方法的逻辑,当调用set方法时,其实是将数据写入threadLocals这个Map对象中,这个Map的key为ThreadLocal当前对象,value就是我们存入的值。而threadLocals本身能保存多个ThreadLocal对象,相当于一个ThreadLocal集合。

接着看 get() 的源码:

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //设置初识值到ThreadLocal中并返回
    return setInitialValue();
}
private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

get方法的逻辑也是比较简单的,就是直接获取当前线程的ThreadLocalMap对象,如果该对象不为空就返回它的value值,否则就把初始值设置到ThreadLocal中并返回。

看到这,我们大概就能明白为什么ThreadLocal能实现线程私有的原理了,其实就是每个线程都维护着一个ThreadLocal的容器,这个容器就是ThreadLocalMap,可以保存多个ThreadLocal对象。而调用ThreadLocal的set或get方法其实就是对当前线程的ThreadLocal变量操作,与其他线程是分开的,所以才能保证线程私有,也就不存在线程安全的问题了。

然而,该方案虽然能保证线程私有,但却会占用大量的内存,因为每个线程都维护着一个Map,当访问某个ThreadLocal变量后,线程会在自己的Map内维护该ThreadLocal变量与具体实现的映射,如果这些映射一直存在,就表明ThreadLocal 存在引用的情况,那么系统GC就无法回收这些变量,可能会造成内存泄露。

针对这种情况,上面所说的ThreadLocalMap中Entry的弱引用就起作用了。

TheadLocal与同步机制的区别

最后,总结一下ThreadLocal和同步机制之间的区别吧。

实现机制:

同步机制采用了“以时间换空间”的方式,控制资源保证同一时刻只能有一个线程访问。

ThreadLocal采用了“以空间换时间”的方式,为每一个线程都提供一份变量的副本,从而实现同时访问而互不影响,但因为每个线程都维护着一份副本,对内存空间的占用会增加。

数据共享:

同步机制是对公共资源做控制访问的方式来保证线程安全,但资源仍是共享状态,可用于线程间的通信;

ThreadLocal是每个线程都有自己的资源(变量)副本,互相之间不影响,也就不存在共享的说法了。

原文地址:https://www.cnblogs.com/yeya/p/10212501.html

时间: 2024-10-27 18:34:42

Java并发编程:ThreadLocal的使用以及实现原理解析的相关文章

Java 并发编程:volatile的使用及其原理

Java并发编程系列[未完]: Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) Java 并发编程:线程间的协作(wait/notify/sleep/yield/join) Java 并发编程:volatile的使用及其原理 一.volatile的作用 在<a href="http://www.cnblogs.com/paddix/p/5374810.html">&

Java并发编程(六):volatile关键字解析(转载)

原文转载自:http://www.cnblogs.com/dolphin0520/p/3920373.html volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在Java 5之后,volatile关键字才得以重获生机. volatile关键字虽然从字面上理解起来比较简单,但是要用好不是一件容易的事情.由于volatile关键字是与Java的内存模型有关的,因此在讲述volatile关键之前,我

【Java并发编程】15、ReentrantLock实现原理深入探究

原文已经写得非常详细了,直接把大神的文章转发过来了  https://www.cnblogs.com/xrq730/p/4979021.html 前言 这篇文章被归到Java基础分类中,其实真的一点都不基础.网上写ReentrantLock的使用.ReentrantLock和synchronized的区别的文章很多,研究ReentrantLock并且能讲清楚ReentrantLock的原理的文章很少,本文就来研究一下ReentrantLock的实现原理.研究ReentrantLock的实现原理需

【Java并发编程】22、Exchanger源码解析

Exchanger是双向的数据传输,2个线程在一个同步点,交换数据.先到的线程会等待第二个线程执行exchangeSynchronousQueue,是2个线程之间单向的数据传输,一个put,一个take. 先举个例子说明一下如何使用 public class ExchangerDemo { public static void main(String[] args) { Exchanger<List<Integer>> exchanger = new Exchanger<&g

JAVA并发编程:volatile的使用及其原理

一.volatile的使用 1.防止重排序 在并发环境下实现单例模式,我们通常可以采用双重检查加锁(DCL)的方式来现实: public class Singleton { public static volatile Singleton singleton; private Singleton() {} public static Singleton getInstance() { if(singleton == null) { synchronized (singleton) { if(si

Java并发编程 Volatile关键字解析

volatile关键字的两层语义 一旦一个共享变量(类的成员变量.类的静态成员变量)被volatile修饰之后,那么就具备了两层语义: 1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的. 2)禁止进行指令重排序. 根据volatile的语义,我们可以看到,volatile主要针对的是并发三要素(原子性,可见性和有序性)中的后两者有实际优化作用. 可见性: 线程本身并不直接与主内存进行数据的交互,而是通过线程的工作内存来完成相应的操作.

Java并发指南开篇:Java并发编程学习大纲

Java并发编程一直是Java程序员必须懂但又是很难懂的技术内容. 这里不仅仅是指使用简单的多线程编程,或者使用juc的某个类.当然这些都是并发编程的基本知识,除了使用这些工具以外,Java并发编程中涉及到的技术原理十分丰富.为了更好地把并发知识形成一个体系,也鉴于本人没有能力写出这类文章,于是参考几位并发编程专家的博客和书籍,做一个简单的整理. 一:并发基础和多线程 首先需要学习的就是并发的基础知识,什么是并发,为什么要并发,多线程的概念,线程安全的概念等. 然后学会使用Java中的Threa

Java并发编程的艺术下载 &#155949;

下载地址: http://www.gqylpy.com/di/11 <Java并发编程的艺术>PDF高清完整版-下载 内容简介 并发编程领域的扛鼎之作,作者是阿里和1号店的资深Java技术专家,对并发编程有非常深入的研究,<Java并发编程的艺术>是他们多年一线开发经验的结晶.本书的部分内容在出版早期发表在Java并发编程网和InfoQ等技术社区,得到了非常高的评价.它选取了Java并发编程中核心的技术进行讲解,从JDK源码.JVM.CPU等多角度全面剖析和讲解了Java并发编程的

7、Java并发编程:深入剖析ThreadLocal

Java并发编程:深入剖析ThreadLocal 想必很多朋友对ThreadLocal并不陌生,今天我们就来一起探讨下ThreadLocal的使用方法和实现原理.首先,本文先谈一下对ThreadLocal的理解,然后根据ThreadLocal类的源码分析了其实现原理和使用需要注意的地方,最后给出了两个应用场景. 以下是本文目录大纲: 一.对ThreadLocal的理解 二.深入解析ThreadLocal类 三.ThreadLocal的应用场景 若有不正之处请多多谅解,并欢迎批评指正. 请尊重作者

Java并发编程:深入剖析ThreadLocal

Java并发编程:深入剖析ThreadLocal 想必很多朋友对ThreadLocal并不陌生,今天我们就来一起探讨下ThreadLocal的使用方法和实现原理.首先,本文先谈一下对ThreadLocal的理解,然后根据ThreadLocal类的源码分析了其实现原理和使用需要注意的地方,最后给出了两个应用场景. 以下是本文目录大纲: 一.对ThreadLocal的理解 二.深入解析ThreadLocal类 三.ThreadLocal的应用场景 若有不正之处请多多谅解,并欢迎批评指正. 请尊重作者