【并发设计模式】聊聊线程本地存储模式如何实现的线程安全

前面两篇文章,通过两阶段终止的模式进行优雅关闭线程,利用数据不变性的方式保证数据安全,以及基于COW的模式,保证读数据的安全。本篇我们来简述下如果利用线程本地存储的方式保证线程安全。

首先一个大前提就是并发问题,其实就是多个线程之间读写共享数据,那么COW是通过将数据读和写分离。而从不共享数据的角度看,那么每个线程都存储一份数据。那么就不会存在线程安全。也就是说线程T1维护一个变量i 自己操作,而线程T2也维护一个变量i。 T1对i 操作,不会影响到T2的i值。

Java中就是通过ThreadLocal的方式实现。

实现原理

具体的工作原理就是线程Thread持有ThreadLocalMap变量,而ThreadLocalMap其实是ThreadLocal中的一个静态内部类。而ThreadLocalMap其实就是数组结构,key对应的线程this。value对应的是线程设置的值。
在这里插入图片描述


public class Thread implements Runnable {
   
    /**
     * ThreadLocal 的 ThreadLocalMap 是线程的一个属性,所以在多线程环境下 threadLocals 是线程安全的
     */
    ThreadLocal.ThreadLocalMap threadLocals = null;

}

public class ThreadLocal<T> {
   
	    public T get() {
   
        // 返回当前 ThreadLocal 所在的线程
        Thread t = Thread.currentThread();
        // 从线程中拿到 ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
   
            // 从 map 中拿到 entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            // 如果不为空,读取当前 ThreadLocal 中保存的值
            if (e != null) {
   
                @SuppressWarnings("unchecked")
                T result = (T) e.value;
                return result;
            }
        }
        // 若 map 为空,则对当前线程的 ThreadLocal 进行初始化,最后返回当前的 ThreadLocal 对象关联的初值,即 value
        return setInitialValue();
    }

	/**
     * 初始化 ThreadLocalMap,并存储键值对 <key, value>,最后返回 value
     *
     * @return value
     */
    private T setInitialValue() {
   
        // 获取为 ThreadLocal 对象设置关联的初值
        T value = initialValue();
        Thread t = Thread.currentThread();
        // 返回当前线程 t 持有的 map
        ThreadLocalMap map = getMap(t);
        if (map != null) {
   
            map.set(this, value);
        } else {
   
            // 为当前线程初始化 map,并存储键值对 <t, value>
            createMap(t, value);
        }
        return value;
    }

   /**
     * 为当前 ThreadLocal 对象关联 value 值
     *
     * @param value 要存储在此线程的线程副本的值
     */
    public void set(T value) {
   
        // 返回当前 ThreadLocal 所在的线程
        Thread t = Thread.currentThread();
        // 返回当前线程持有的map
        ThreadLocalMap map = getMap(t);
        if (map != null) {
   
            // 如果 ThreadLocalMap 不为空,则直接存储<ThreadLocal, T>键值对
            map.set(this, value);
        } else {
   
            // 否则,需要为当前线程初始化 ThreadLocalMap,并存储键值对 <this, firstValue>
            createMap(t, value);
        }
    }

    /**
     * 清理当前 ThreadLocal 对象关联的键值对
     */
    public void remove() {
   
        // 返回当前线程持有的 map
        ThreadLocalMap m = getMap(Thread.currentThread());
        if (m != null) {
   
            // 从 map 中清理当前 ThreadLocal 对象关联的键值对
            m.remove(this);
        }
    }

    /**
     * 返回当前线程 thread 持有的 ThreadLocalMap
     *
     * @param t 当前线程
     * @return ThreadLocalMap
     */
    ThreadLocalMap getMap(Thread t) {
   
        return t.threadLocals;
    }

在这里插入图片描述
所以通过以上的方式可以保证每个线程内部保存一个Map数组,但是对应的key确实一个软引用,具体的介绍,另一篇文章有详细介绍就不说了总体上就是

【Java并发】从simpleDateFormart聊聊threadlocal原理机制

  • 对象的强软弱虚引用
  • threadlocal的原理
  • 对象内存泄漏

实际应用

因为threadlocal是和线程绑定的,所以可以很自然的就采用在线程级别做一些事情。

1.切换数据库
比如我们在切换数据的时候,就可以通过threadlocal进行操作。
比如当前默认就是从库,但是想要从主库切到从库上,就可以进行通过threadlocal进行使用。

        DynamicDataSourceHolder.setDataSourceTypeMaster();
        boolean updateResult;
        try {
   
            xxxxx // 业务代码
        } finally {
   
            DynamicDataSourceHolder.clearDataSourceType();
        }
public final class DynamicDataSourceHolder {
   
    private static ThreadLocal<String> threadLocal = new ThreadLocal();
    public static int dataSourceMasterSize;
    public static int dataSourceSlaveSize;
    private static Random random = new Random();

    private DynamicDataSourceHolder() {
   
    }

    public static String getDataSourceType() {
   
        if (null == threadLocal.get()) {
   
            setDataSourceTypeSlave();
        }

        return (String)threadLocal.get();
    }

    public static void setDataSourceTypeMaster() {
   
        threadLocal.set("MASTER");
    }

    public static void setDataSourceTypeSlave() {
   
        int randomSlave = random.nextInt(dataSourceSlaveSize);
        threadLocal.set("SLAVE" + (randomSlave + 1));
    }

    public static void clearDataSourceType() {
   
        threadLocal.remove();
    }
}

通过这种方式,可以很方便的进行切换数据库。

2.第二种场景
那就是比如我们需要针对线程级别进行添加整个链路的相关信息,或者存储相关数据。或者通过AOP注入的方式,前后执行一些方法。

好了今天比较简单,就到这里。

推荐阅读

保姆级教学,22张图揭开ThreadLocal

相关推荐

  1. 线上下文设计模式

    2023-12-30 13:14:02       31 阅读
  2. 线安全单例模式

    2023-12-30 13:14:02       21 阅读

最近更新

  1. TCP协议是安全的吗?

    2023-12-30 13:14:02       17 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2023-12-30 13:14:02       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2023-12-30 13:14:02       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2023-12-30 13:14:02       18 阅读

热门阅读

  1. day 31 贪心(1)

    2023-12-30 13:14:02       41 阅读
  2. Dockerfile文件介绍

    2023-12-30 13:14:02       40 阅读
  3. nginx配置文件

    2023-12-30 13:14:02       36 阅读
  4. MFC:如何将JPEG等图片显示到对话框客户区

    2023-12-30 13:14:02       32 阅读
  5. journalctl命令学习

    2023-12-30 13:14:02       37 阅读
  6. 第一篇 设计模式引论 - 探索软件设计的智慧结晶

    2023-12-30 13:14:02       34 阅读
  7. 二、计算机软件及其使用-电子表格软件Excel 2016

    2023-12-30 13:14:02       40 阅读