threadlocal

应用场景

Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复,org.springframework.transaction.support.TransactionSynchronizationManager

private static final ThreadLocal<Map<Object, Object>> resources =
		new NamedThreadLocal<>("Transactional resources");

private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
		new NamedThreadLocal<>("Transaction synchronizations");

private static final ThreadLocal<String> currentTransactionName =
		new NamedThreadLocal<>("Current transaction name");

private static final ThreadLocal<Boolean> currentTransactionReadOnly =
		new NamedThreadLocal<>("Current transaction read-only status");

private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
		new NamedThreadLocal<>("Current transaction isolation level");

private static final ThreadLocal<Boolean> actualTransactionActive =
		new NamedThreadLocal<>("Actual transaction active");

原理解析
成员变量
public class Thread implements Runnable {
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    
    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
...
}

static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;

        /**
         * The number of entries in the table.
         */
        private int size = 0;

        /**
         * The next size value at which to resize.
         */
        private int threshold; // Default to 0
    ....
}
set方法
private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    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;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

为什么entry数组的大小,以及初始容量都必须是2的幂?

对于 firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); 以及很多源码里面都是使用 hashCode &(2^n -1) 来代替hashCode%2^n

使用位运算替代取模,提升计算效率。
为了使不同 hash 值发生碰撞的概率更小,尽可能促使元素在哈希表中均匀地散列。

为什么需要数组呢?没有了链表怎么解决Hash冲突呢

一个线程可以有多个TreadLocal来存放不同类型的对象的,但是他们都将放到你当前线程的ThreadLocalMap里,所以肯定要数组来存

HaseMap解决hash冲突是由链表和红黑树(jdk1.8)来解决的,ThreadLocalMap采用「线性探测」的方式解决hash冲突,
在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,int i = key.threadLocalHashCode & (len-1),
如果当前位置是空的,就初始化一个Entry对象放在位置i上,
如果位置i不为空,如果这个Entry对象的key正好是即将设置的key,那么就刷新Entry中的value,
如果位置i的不为空,而且key不等于entry,那就找下一个空位置,直到为空为止

ThreadLocal的实例以及其值存放在栈上

栈内存可以理解成线程的私有内存
ThreadLocal实例实际上也是被其创建的类持有(更顶端应该是被线程持有),而ThreadLocal的值其实也是被线程实例持有,它们都是位于堆上
内存泄漏
ThreadLoacal<Object> key = new ThreadLoacal()<> -- 强引用

Object value = new Object();
key.set(value) -- 弱引用

ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露,比如线程池复用

解决方式:代码最后使用remove

为什么ThreadLocalMap的key要设计成弱引用

key不设置成弱引用的话就会造成和entry中value一样内存泄漏的场景
SimpleDateFormat

SimpleDateFormat 其实是有状态的,它使用一个 Calendar 成员变量来保存状态

UnSafeDateUtil
public class UnSafeDateUtil {
    private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static String format(Date date) {
        return dateFormat.format(date);
    }

    public static Date parse(String dateStr) throws ParseException {
        return dateFormat.parse(dateStr);
    }

    public static void main(String[] args) {
        final CountDownLatch latch = new CountDownLatch(1);
        final String[] strs = new String[] {"2016-01-01 10:24:00", "2016-01-02 20:48:00", "2016-01-11 12:24:00"};
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    latch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                for (int i1 = 0; i1 < 10; i1++){
                    try {
                        System.out.println(Thread.currentThread().getName()+ "\t" + parse(strs[i1 % strs.length]));
                        Thread.sleep(100);
                    } catch (ParseException | InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
        latch.countDown();
    }
}

输出内容:
java.lang.NumberFormatException: For input string: "11011101EE.221101"
	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043)
	at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
SafeDateUtil
public class SafeDateUtil {
    /*private static ThreadLocal<SimpleDateFormat> local = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    public static String format(Date date) {
        return local.get().format(date);
    }

    public static Date parse(String dateStr) throws ParseException {
        return local.get().parse(dateStr);
    }*/

    private static ThreadLocal<SimpleDateFormat> local = new ThreadLocal<>();

    //延迟加载
    private static SimpleDateFormat getDateFormat() {
        SimpleDateFormat dateFormat = local.get();
        if (dateFormat == null) {
            dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            local.set(dateFormat);
        }
        return dateFormat;
    }

    public static String format(Date date) {
        return getDateFormat().format(date);
    }

    public static Date parse(String dateStr) throws ParseException {
        return getDateFormat().parse(dateStr);
    }


    public static void main(String[] args) {
        final CountDownLatch latch = new CountDownLatch(1);
        final String[] strs = new String[] {"2016-01-01 10:24:00", "2016-01-02 20:48:00", "2016-01-11 12:24:00"};
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    latch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                for (int i1 = 0; i1 < 10; i1++){
                    try {
                        System.out.println(Thread.currentThread().getName()+ "\t" + parse(strs[i1 % strs.length]));
                        Thread.sleep(100);
                    } catch (ParseException | InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
        latch.countDown();
    }
}
InheritableThreadLocal

InheritableThreadLocal提供了一种父子线程之间的数据共享机制,但是线程池的话,线程会复用,所以会存在问题,子线程只有在线程对象创建的时候才会把父线程inheritableThreadLocals中的数据复制到自己的inheritableThreadLocals中

public class InheritableThreadLocalDemo {

    static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        threadLocal.set("threadLocal主线程的值");
        Thread.sleep(100);
        new Thread(() -> System.out.println("子线程获取threadLocal的主线程值:" + threadLocal.get())).start();
        Thread.sleep(100);
        inheritableThreadLocal.set("inheritableThreadLocal主线程的值");
        new Thread(() -> System.out.println("子线程获取inheritableThreadLocal的主线程值:" + inheritableThreadLocal.get())).start();

    }
}

输出内容:
子线程获取threadLocal的主线程值:null
子线程获取inheritableThreadLocal的主线程值:inheritableThreadLocal主线程的值
FastThreadLocal

弥补ThreadLocal的内存泄漏的的不足,使用完 FastThreadLocal 之后不用 remove 了,因为在 FastThreadLocalRunnable 中已经加了移除逻辑,在线程运行完时会移除全部绑定在当前线程上的所有变量

FastThreadLocal 在大量频繁读写操作时效率要高于 ThreadLocal

public class FastThreadLocalTest {
    public static final int MAX = 100000;

    public static void main(String[] args) {
        new Thread(FastThreadLocalTest::threadLocal).start();
        new Thread(FastThreadLocalTest::fastThreadLocal).start();
    }

    private static void fastThreadLocal() {
        long start = System.currentTimeMillis();
        DefaultThreadFactory defaultThreadFactory = new DefaultThreadFactory(FastThreadLocalTest.class);

        FastThreadLocal[] fastThreadLocal = new FastThreadLocal[MAX];

        for (int i = 0; i < MAX; i++) {
            fastThreadLocal[i] = new FastThreadLocal<>();
        }

        Thread thread = defaultThreadFactory.newThread(() -> {
            for (int i = 0; i < MAX; i++) {
                fastThreadLocal[i].set("java: " + i);
            }

            System.out.println("fastThreadLocal set: " + (System.currentTimeMillis() - start));

            for (int i = 0; i < MAX; i++) {
                for (int j = 0; j < MAX; j++) {
                    fastThreadLocal[i].get();
                }
            }
        });
        thread.start();
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("fastThreadLocal total: " + (System.currentTimeMillis() - start));
    }

    private static void threadLocal() {
        long start = System.currentTimeMillis();
        ThreadLocal[] threadLocals = new ThreadLocal[MAX];

        for (int i = 0; i < MAX; i++) {
            threadLocals[i] = new ThreadLocal<>();
        }

        Thread thread = new Thread(() -> {
            for (int i = 0; i < MAX; i++) {
                threadLocals[i].set("java: " + i);
            }

            System.out.println("threadLocal set: " + (System.currentTimeMillis() - start));

            for (int i = 0; i < MAX; i++) {
                for (int j = 0; j < MAX; j++) {
                    threadLocals[i].get();
                }
            }
        });
        thread.start();
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("threadLocal total: " + (System.currentTimeMillis() - start));
    }

}

输出
threadLocal set: 85
fastThreadLocal set: 91
fastThreadLocal total: 121
threadLocal total: 16367


在大量读写面前,写操作的效率差不多,但读操作 FastThreadLocal 比 ThreadLocal 快的不是一个数量级,简直是秒杀 ThreadLocal

MAX值调到1000
threadLocal set: 4
threadLocal total: 14
fastThreadLocal set: 23
fastThreadLocal total: 32

区别不是很明显,但是threadLocal明显优势
transmittable-thread-local

解决线程池,池化过程中,父线程到子线程的值传递问题

原理
使用TtlRunnable/Ttlcallable包装了Runnable/Callable类

在TtlRunnable/Ttlcallable初始化时capture TransmittableThreadLocal变量
在run方法调用runnable.run()前进行replay,设置到当前线程ThreadLocal
在run方法调用runnable.run()后进行restore,上下文还原,也就是replay的反向操作

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

四美

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值