IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> ThreadLocal使用及原理解析 -> 正文阅读

[Java知识库]ThreadLocal使用及原理解析

ThreadLocal作用和原理

我们知道Java多线程会出现安全问题主要原因是因为多线程同时访问一个共享数据,从而我们解决多线程问题的思路主要有2个:

1.给共享数据加锁

2.避免多线程操作同一共享数据

而思路1是我们平时比较常用的一种方式,但是既然是加锁就必然会有一些性能方面的问题,比如线程等待。

所以今天我们讲讲思路2,但是思路2并不能适用于所有线程安全问题,因为在很多具体业务场景下必须让多线程访问同一数据,所以思路2适用于可以将共享数据变为线程私有变量的场景,例如Android中Handler的实现中。

Android中的Handler中实现线程–Looper一对一关系使用了ThreadLocal。

首先讲一下ThreadLocal的原理,ThreadLocal是一个通过空间换时间的多线程并发问题的解决工具,它给每个线程提供了一个变量副本,实现了共享变量在多个线程间的隔离,比起synchronized通过加锁实现线程安全 ThreadLocal 的效率更高是一种无锁编程的实现。

首先我们以一个例子为切入点来看ThreadLocal的内部原理:

public class ThreadLocalTest {

    private static ExecutorService service = Executors.newCachedThreadPool();

    public static void main(String[] args) {

        ThreadLocal<Boolean> threadLocal = new ThreadLocal<>();

        threadLocal.set(Boolean.TRUE);

        for (int i=0;i<100;i++){
            service.execute(() -> {
                threadLocal.set(Boolean.FALSE);
                System.out.println("子线程设置为:" + threadLocal.get());
            });
        }
        try {
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("主线程设置为:" + threadLocal.get());
        service.shutdown();
    }
}

我们可以看到输出为:

子线程设置为:false
    ...
子线程设置为:false
主线程设置为:true

我们在主线程中将threadLocal的值设置为true,然后开了一百个子线程来将它的值改为false,为了更好的复现线程不安全的情况,我们特地还给主线程休眠了3S,这时候到最后输出的主线程的threadLocal的值依然是true。这就说明了,子线程中的设置并没有影响到主线程的值,其实各个线程中操作的都是各自线程对应的threadLocal的值。

如果我们将以上代码中ThreadLocal类型改为AtomicBoolean那么最终的输出结果将会是

子线程设置为:false
    ...
子线程设置为:false
主线程设置为:false

其实ThreadLocal就是这么回事,就等于我们在每个线程中都创建了一个布尔值。

我们假设有一个场景,每个线程都会做各自线程相关的操作,最后要将该线程里的操作写入每个线程中id作为名称的文件中。

public class ThreadLocalTest {
    private static ExecutorService service = Executors.newCachedThreadPool();

    public void main() {

        ThreadLocal<Integer> taskId = new ThreadLocal<>();

        taskId.set(1);

        service.execute(() -> {
            taskId.set(2);
            System.out.println("这个线程做了一堆A操作,然后生成一个以" + taskId.get() + "命名的文件中。。。");
        });
        service.execute(() -> {
            taskId.set(3);
            System.out.println("这个线程做了一堆B操作,然后生成一个以" + taskId.get() + "命名的文件中。。。");
        });
        service.execute(() -> {
            taskId.set(4);
            System.out.println("这个线程做了一堆C操作,然后生成一个以" + taskId.get() + "命名的文件中。。。");
        });

        System.out.println("主线程中的taskId值:" + taskId.get());
        service.shutdown();
    }
}

那我们这样写的话就符合标准,最终输出结果:

这个线程做了一堆A操作,然后生成一个以2命名的文件中。。。
这个线程做了一堆B操作,然后生成一个以3命名的文件中。。。
主线程中的taskId值:1
这个线程做了一堆C操作,然后生成一个以4命名的文件中。。。

当然这个操作我们完全可以不用ThreadLocal来实现,直接在每个线程内部都生成一个类型为int的taskId即可.

public class ThreadLocalTest {
    private static ExecutorService service = Executors.newCachedThreadPool();

    public void main() {

        int taskId = 1;

        service.execute(() -> {
            int taskId2 = 2;
            System.out.println("这个线程做了一堆A操作,然后生成一个以" + taskId2 + "命名的文件中。。。");
        });
        service.execute(() -> {
            int taskId3 = 3;
            System.out.println("这个线程做了一堆B操作,然后生成一个以" + taskId3 + "命名的文件中。。。");
        });
        service.execute(() -> {
            int taskId4 = 4;
            System.out.println("这个线程做了一堆C操作,然后生成一个以" + taskId4 + "命名的文件中。。。");
        });
        System.out.println("主线程中的taskId值:" + taskId);
        service.shutdown();
    }
}

以上两段代码的作用是一模一样的,但是你认为哪个更为优雅?

所以看懂这两个区别之后,很明显我们能知道,其实在使用ThreadLocal的时候,各个线程为生成一个该变量在本线程中的副本,后续本线程中的所有操作获取到的值都为这个副本,避免了开发过程中诸多麻烦比如每个id的命名还不一样,看起来又长又臭。

具体来说,如果有某些需求中要求的是各个线程都需要某一数据,而线程之间又不互相干预,那么使用ThreadLocal无疑是一种绝佳选择。

ThreadLocal原理

我们从ThreadLocal的源码来分析一下为什么ThreadLocal能保证多个线程操作同一共享变量时使用ThreadLocal类型都会生成一份该线程对应的副本从而保证线程安全的。

ThreadLocal主要我们使用时的方法有这几个:

protected T initialValue()

private void set(ThreadLocal<?> key, Object value)

public T get()

public void remove()

从最简单的先看:

protected T initialValue() {
    return null;
}

够简单吧,其实在ThreadLocal内部它没有什么意义,它的作用是开发者在创建ThreadLocal的时候复写它使用的。为的是当ThreadLocal未set()时调用get()返回默认值使用的。

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

看下get()方法:

public T get() {
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取当前线程对应的ThreadLocalMap,每个Thread都保存了一个ThreadLocalMap,这样就实现了Thread与ThreadLocalMap的一一对应
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //获取本ThreadLocal对应的在本线程中存储的值
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            //取值
            T result = (T)e.value;
            //返回正确结果
            return result;
        }
    }
    //说明该线程未调用过set()方法设置值,则执行初始化操作获取要保存的value并生成一个Thread对应的ThreadLocalMap
    return setInitialValue();
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

private T setInitialValue() {
    //调用initialValue()初始化值,如果未复写则是null
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        //设置值
        map.set(this, value);
    else
        //如果Thread对应的ThreadLocalMap仍为空,则创建该Thread对应的ThreadLocalMap并写值
        createMap(t, value);
    return value;
}

Thread类中有一个ThreadLocalMap对象存储的就是当前线程对应的ThreadLocalMap,ThreadLocalMap中有一个Entry类型的数组,它的作用就是以ThreadLocal为key,以要存储的值为value存储数据。

现在可以看出来的是,Thread与TreadLocalMap一一对应了。

调用get()做了几个事情:

1.获取当前线程并调用getMap()获取当前线程对应的ThreadLocalMap

2.如果线程对应的ThreadLocalMap不为空则获取该ThreadLocal对应key的value

3.如果2为空则初始化value并创建Thread对应的ThreadLocalMap并写入值

接着set():

public void set(T value) {
    Thread t = Thread.currentThread();
    //获取当前操作线程所对应的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    if (map != null)
        //取到了,就把所需要保存的值存入map中
        map.set(this, value);
    else
        //没获取到则为当前线程创建一个map并把要保存的值以ThreadLocal为key存进去
        createMap(t, value);
}

ThreadLocalMap中的set(ThreadLocal<?> key, Object value):

private void set(ThreadLocal<?> key, Object value) {

    
    //其实ThreadLocalMap是一个Entry类型的数组结构
    Entry[] tab = table;
    int len = tab.length;
    //用key的hashcode与数组长度-1做&操作来寻址,找出应该存入的位置i,其实与数组长度-1做&是为了将hashcode做截断操作,以保证这个i一定在数组的长度范围以内。
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    //将ThreadLocal为key,value为值得Entry存入tab[i]中
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        //存储完数据再清除掉key被回收掉的无用项之后将数据规整并判断数组长度是否达到阈值,达到了就扩容
        rehash();
}

private void rehash() {
    expungeStaleEntries();

    // Use lower threshold for doubling to avoid hysteresis
    //当数组利用率大于等于3/4的时候,进行扩容
    if (size >= threshold - threshold / 4)
        resize();
}

private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    //每次扩容都是原长度*2
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }
    //将新的长度设置给threshold以便下次判断是否需要扩容
    setThreshold(newLen);
    size = count;
    table = newTab;
}

在存值得过程中其实主要思路就是

1.如果该Thread对应的ThreadLocalMap不存在则创建,如已存在则执行存值

2.如key出现hash冲突则使用开放定址法进行往后寻址找到合适的位置存入

3.存值完毕之后清除掉key已经被回收的无用项并判断是否需要扩容,如需要则进行扩容,判断扩容的阈值为原长度的3/4,每次扩容都是原长度*2,我们看下初始长度是多少:

private static final int INITIAL_CAPACITY = 16;

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

ThreadLocalMap的初始长度为16

内存泄漏问题

上面的Entry类型为:

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

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

是一个继承自ThreadLocal类型的弱引用的类,内部的key值为弱引用,那么当key被回收的时候key为null,value依然被强引用着导致无法回收,所以这时就会出现内存泄漏。

当然我们发现每次get(),set()等操作都会遍历数组中的无用项并回收,所以我们如果有调用过get()或者set()就不会内存泄漏。但是最好的方案是使用完之后手动执行一下remove()方法保证把不需要的值释放掉。

早前不理解的问题

在早期阅读源码的时候我很不理解多个线程操作同一个ThreadLocal对象内部不加锁,那如何保证多个线程创建或者读取到的是该线程对应的ThreadLocalMap呢?
例如:

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    //如果某个线程执行到这里的时候线程执行权被其他线程拿走,那再回来的时候这个map还是刚才操作的线程的map吗?
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

后来我看了深入Java虚拟机之后发现这并不是个问题:

虽然多个线程同时操作了一个公共的ThreadLocal变量,但是并不是给ThreadLocal实例做读写,而是调用其方法。

map是get()的局部变量,而局部变量是保存在get()方法栈帧的局部变量表中的,所以这是线程私有的数据并不会被其他线程共享,所以当其他线程获取到执行权走到这里的时候操作的是自身的map对象,不会影响到上一个线程的map。

如果get()方法中的map对象是成员变量的话,那么多线程操作的时候就会出现线程不安全的问题。

参考了以下几个文章:
https://blog.csdn.net/weixin_43314519/article/details/108188298
https://www.jianshu.com/p/1a5d288bdaee

https://www.freesion.com/article/4836870638/

jvm变量的存储位置这篇文章也大概说了一些变量的存储位置,可供参考,但是最好还是推荐去读一读《深入Java虚拟机》这本书:
https://blog.csdn.net/shanchahua123456/article/details/79605433

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-09-13 09:08:16  更:2021-09-13 09:10:07 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 17:01:57-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码