ThreadLoacl、InheritableThreadLocal学习

如果写过多线程的代码,你肯定考虑过线程安全问题,更进一步你可能还考虑在在线程安全的前提下性能的问题。大多数情况下大家用来解决线程安全问题都会使用同步,比如用synchron或者concurrent包提供的各种锁,当然这些都能解决问题。但有多线程做同步一定会涉及到资源争抢和等待的问题。java中各种同步方法都是提供一种准入机制,JVM会调用系统同步原语来保证临界区任意时刻只能有一个线程进入,那必然其他线程都得等待了,性能的瓶颈就在这同步上了。
  解决问题最好的方式是啥?当然是避免问题的发生了。ThreadLocal就是用这样一种方式提升性能的。ThreadLocal也不是万金油,它也只能在多线程之间数据相互独立的情况下使用,如果是多线程间的数据同步,还得使用某个同步的方式

要理解ThreadLocal的工作原理,还需要了解Thread,ThreadLocalMap

每一个ThreadLocal只能存储一个值,一个ThreadLocal可以被多个线程记录,一个线程Thread也可以记录多个ThreadLocal,他们通过ThreadLocalMap来关联。


ThreadLocal

1. 先从ThreadLocal提供的几个公共方法入口来分析它的使用原理

get方法

    public T get() {
        Thread t = Thread.currentThread();
        // 获取当前线程对象保存的ThreadLocalMap,存储在线程对象的成员变量threadLocals中
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 关于ThreadLocalMap的相关操作,另外写在其他地方
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 如果当前线程对象的threadLocals尚未创建,则设置初始值
        return setInitialValue();
    }

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

    private T setInitialValue() {
        // 获取默认的初始值,如未重写该方法,默认为null
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }


    /**
     * initialValue方法,默认返回null,如果希望在未进行set操作之前,调用get方法时能返回一个非
     * null初始值,那么必须创建一个ThreadLocal的子类对象并重写该方法。
     */
    protected T initialValue() {
        return null;
    }

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

set方法

    public void set(T value) {
        Thread t = Thread.currentThread();
        // 可以发现,实际的赋值是对当前线程绑定的ThreadLocalMap进行赋值
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

remove方法

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

可以发现,get,set ,remove方法中的逻辑都十分简单,但都涉及到了许多对ThreadLocalMap的操作,实际上,我认为ThreadLoacl的核心就是ThreadLocalMap,ThreadLoacl为每个线程存储的值实际上就是通过 对每个线程对象中的ThreadLocalMap 进行赋值,所以即使在多线程环境下,我们对同一个ThreadLoacl对象进行set,get操作,实际上操作的是每个线程下不同的ThreadLocalMap对象,所以不存在线程安全问题。所以我们重点关注一下ThreadLocalMap


Thread

每一个线程对象中都要一个ThreadLocalMap成员变量,初始值为null,其初始化由ThreadLocal完成

// 这个map通过ThreadLocal类,保留了与此线程相关的ThreadLocal值。
ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap 简单介绍

ThreadLocalMap 是 ThreadLocal  的一个静态成员内部类,且访问权限为默认,并且没有提供任何公有方法,这就意味着除了在ThreadLocal中,其他地方是无法对其进行操作的。另外,它没有实现Map接口,emmm 这就意味它不是一个标准的map了

核心成员  private Entry[] table;

这个EntryThreadLocalMap的一个静态内部类,可以简单理解为一个键值对,键始终为ThreadLocal实例对象,值为该ThreadLocal对象存储的值。该类继承了WeakReference(不了解),当key为null时,意味着该key已经无效,意味着这个entry已经可以从table这个数组中移除。

通过继承至父类的get()方法,可以获取key

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

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

构造方法

需要提出来的是,每个Entry在table数组中的位置的计算方式比较特殊(具体方式看代码),用这种方式,不同的key可能会得出相同的下标i,如何解决这种冲突,参考博文https://blog.csdn.net/xindoo/article/details/88777833#commentBox

        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            // 构造一个初始容量的Entry数组
            table = new Entry[INITIAL_CAPACITY];
            // 计算出每个元素在数组中的位置
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            // 数组扩容相关,比较常见,不关心
            setThreshold(INITIAL_CAPACITY);
        }

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();
        }

从set方法中我们就可以看出ThreadLocalMap和HashMap,TreeMap的设计不同之处。首先也是对Key求hash值做定位,但当遇到hash冲突的时候,它的选择不是开链,而是调用nextIndex往后移动,直到遇见某个entry为null或者其key和要插入的key一样。同时,插入的过程也会调用replaceStaleEntry对Map做清理,清理过程比较复杂,稍后介绍。插入后,如果size大于阀值,也会对整个map做扩容操作。

remove方法

        private void remove(ThreadLocal<?> key) {
            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)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

get方法

        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            // 理想情况,直接命中
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                // 说明对应entry不存在
                if (k == null)
                    // 如果键为null,说明这个entry已经无效了,该方法用于删除那些无用的entry,不关心具体实现
                    expungeStaleEntry(i);
                else
                    // 这里说明发生了下标冲突,k != key两个不同的键产生了相同的下标
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

因为刚刚说到ThreadLocalMap处理key冲突的方式是往后移,直到有空闲的位置。这样虽然实现简单,但查的时候问题就来了,首先根据参数key的hash值算出目标entry的位置

1. 如果目标位置entry为null,意味着不存在该键key对应的entry

2. 如果目标位置entry存在,但其键k与作为参数的key不等,这就意味着发生了下标冲突,插入过程中,位置被后移,或者确实从未插入,不存在该键key对应的entry

所以得往后遍历,直到找到或者遍历到某个空Entry。如果你仔细想想可能就会发现问题,如果只是遍历到遇到null,而不是遍历整个tab,可能会漏掉。比如下面这个例子。

  | 0 | 1 | 2 | 3 | 5 | 6 | 7 |  
  |   | a | b | c | d | e |   |     

开始的时候,tab状态是这样的,现在我要插入一个h,其hashcode恰好是1,然而a已经在那了,按插入逻辑,h只能插到7的位置了,插入后如下。

  | 0 | 1 | 2 | 3 | 5 | 6 | 7 |  
  |   | a | b | c | d | e | h |     

后来,我把c删掉,变成了下面这样。如果我现在想查h,按照上面getEntry的逻辑,是不是遍历到3就停了,所以找不到h了? getEntry的逻辑表面确实是这样,但实际上getEntryAfterMiss、remove、get时都会直接或者间接调用expungeStaleEntry会对表里的数据做整理。expungeStaleEntry()除了利用弱引用的特性对tab中Entry做清理外,还会对之前Hash冲突导致后移的Entry重新安放位置。所以不可能出现下面这种tab排放的。

  | 0 | 1 | 2 | 3 | 5 | 6 | 7 |  
  |   | a | b |   | d | e | h |     
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

还有set中调用的replaceStaleEntry(),代码很长,其实也是保证key失效的Entry被清理,Hash冲突的key能放回正确的位置。

        private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            // Back up to check for prior stale entry in current run.
            // We clean out whole runs at a time to avoid continual
            // incremental rehashing due to garbage collector freeing
            // up refs in bunches (i.e., whenever the collector runs).
            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

            // Find either the key or trailing null slot of run, whichever
            // occurs first
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                // If we find key, then we need to swap it
                // with the stale entry to maintain hash table order.
                // The newly stale slot, or any other stale slot
                // encountered above it, can then be sent to expungeStaleEntry
                // to remove or rehash all of the other entries in run.
                if (k == key) {
                    e.value = value;

                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // Start expunge at preceding stale entry if it exists
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                // If we didn't find stale entry on backward scan, the
                // first stale entry seen while scanning for key is the
                // first still present in the run.
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // If key not found, put new entry in stale slot
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // If there are any other stale entries in run, expunge them
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

看这么多复杂的代码,最后看个简单的resize(),ThreadLocalMap的resize相较于HashMap的简单多了,就是新建一个长度为当前2倍的tab,然后把当前tab中的每个entry重新计算index再插入新tab。

        private void resize() {
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            int newLen = oldLen * 2;
            Entry[] newTab = new Entry[newLen];
            int count = 0;

            for (Entry e : oldTab) {
                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++;
                    }
                }
            }

            setThreshold(newLen);
            size = count;
            table = newTab;
        }

使用场景

  1. 数据库连接
  2. Cache
  3. 线程池

参考资料

  1. ThreadLocal源码
  2. 简单理解ThreadLocal原理和适用场景,多数据源下ThreadLocal的应用

测试代码

package threadLocal;

/**
 * @author LiFeng
 * @create 2019-11-05 上午 11:08
 * @describe
 */
public class TestLocal {
	public static void main(String[] args) {
		ThreadLocal<String> t = new ThreadLocal();
		String s = t.get();
		System.out.println(s);

		// 重写initialValue的方法,未set直接调用get也能获得一个初始值
		ThreadLocal<String> t2 = new ThreadLocal<String>() {
			@Override
			protected String initialValue() {
				return "lifeng";
			}
		};

		s = t2.get();
		System.out.println(s);

		t2.remove();
		s = t2.get();
		System.out.println(s);

		t2.set("hello world");
		s = t2.get();
		System.out.println(s);

		Thread thread = new Thread(new Runnable() {
			@Override
			public void run() {
				System.out.println("thread1:" + t.get());
				System.out.println("thread1:" + t2.get());
				try {
					Thread.sleep(5000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		});

		thread.start();
		System.out.println("wait...");
	}

}

测试结果如图

因为无法在外部获取thread对象中的ThreadLocalMap,所以我通过debug的方式观察了一下。符合预期


 InheritableThreadLocal

InheritableThreadLocal 继承自 ThreadLocal,但是仅仅是重写了3个方法,可以看到并没有什么复杂的操作。它的特性是,子线程可以继承父线程的初始值,那么它是如何实现的呢? 对于后两个方法非常简单,用处也在上文提到过。稍后介绍childValue方法的用途。

其实,这个要联系到Thread类的核心构造方法,这个我在另一篇博文中详细介绍过,这里简单说一下。

对于这个参数,默认为true,仅有一个构造方法可以设定其值为false,

我们平常创建线程时,绝大多数时候是不会调用此方法的,所以inheritThreadLocals这个参数通常为true,它的作用是

简单说,就是利用父线程中的ThreadLocal.ThreadLocalMap inheritableThreadLocals属性来构建自己的inheritableThreadLocals

可以认为这是一次copy操作。这种构建是一次性的,之后父线程修改其inheritableThreadLocals中的值,子线程并不受其影响。

这里我们也看到了childValue方法的调用,在ThreadLocal方法中未被重写时,它是这样的,这是一种典型的模板方法模式,定义

了构造ThreadLocalMap的步骤,但将其中某个步骤交由其子类去具体实现。

对于childValue方法,我们也可以进一步重写,以达到值继承并进行修改的效果。写了一个简单的demo如下。

测试代码:

package threadLocal;

/**
 * @author LiFeng
 * @create 2019-11-06 上午 10:35
 * @describe
 */
public class TestInheritable {

	public static void main(String[] args) {
		InheritableThreadLocal<String> iht = new InheritableThreadLocal<>();
		iht.set("first value");
		System.out.println("main get: " + iht.get());

		InheritableThreadLocal<String> iht2 = new InheritableThreadLocal<String>() {
			@Override
			protected String childValue(String parentValue) {
				return parentValue + "取得父线程中的值并加以补充修改";
			}
		};
		iht2.set("iht2");
		System.out.println("main get: " + iht2.get());

		new Thread(new Runnable() {
			@Override
			public void run() {
				System.out.println("thread-01 get: " + iht.get());
				System.out.println("thread-01 get: " + iht2.get());
				try {
					Thread.sleep(2000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println("thread-01 get: " + iht.get());
			}
		}, "thread-01").start();


		// 模拟在main线程中将iht中的值进行修改,发现在thread-01线程中并不会受影响
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		iht.set("second value");
		System.out.println("main get: " + iht.get());


	}

}

 

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值