Java并发编程之ThreadLocal探讨

ThreadLocal这部分内容在正常开发过程中可能用的很少,但是却是线程操作不可缺少的,尤其是在线程间通过Handler通信是重要的一环,这一篇我就帮着大家一起分析一下ThreadLocal的使用和内部原理。

ThreadLocal是什么

ThreadLocal 是一个关于创建线程局部变量的类。ThreadLocal 可以把一个对象保存在指定的线程中,对象保存后,只能在指定线程中获取保存的数据,对于其他线程来说则无法获取到数据。

ThreadLocal使用

ThreadLocal使用非常简单,通常情况下我们只需要关心内部的set()get()方法即可。

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "ThreadLocalTest";
    
    private ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        // 主线程中通过set一个对象
        stringThreadLocal.set("MainThread");
        // 主线程通过TheadLocal获取该对象, 结果为“MainThread”
        Log.d(TAG, "MainThread's stringThreadLocal=" + stringThreadLocal.get());
        
        new Thread("Thread#1") {
            @Override
            public void run() {
                // 子线程中获取该对象,结果为null
                Log.d(TAG, "Thread#1's stringThreadLocal=" + stringThreadLocal.get());
            }
        }.start();
    }
}

ThreadLocal也可以设置一个默认值,在获取的时候如果有set值就会取set值,没有就取默认值。

private ThreadLocal<Boolean> booleanThreadLocal = new ThreadLocal<Boolean>(){
    @Override
    protected Boolean initialValue() {
        return false;
    }
};

ThreadLocal还有一个remove,移除保存的对象。

Handler中Looper和ThreadLocal的关系

我们知道在子线程中使用Handler构建消息体Looper必不可少:

class MyThread extends Thread {
 
        @Override
        public void run() {
            super.run();
 
            // 准备消息循环体
            Looper.prepare();
            // 构建Handler实例
            handler = new Handler(){
                @Override
                public void handleMessage(Message msg) {
                    super.handleMessage(msg);
                    System.out.println( "threadName--" + Thread.currentThread().getName() + "messageWhat-"+ msg.what );
                }
            };
            // 执行looper循环
            Looper.loop();
        }
    }

在Looper内部定义了一个静态的ThreadLocal,在prepare()过程中专门用来存储Looper实例,并保证一个线程只能有一个Looper实例:

// sThreadLocal.get() will return null unless you've called prepare().
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
}

接下来在Handler初始化的时候通过ThreadLocal.get获取到该Looper,拿到Looper的MessageQueue,接着Handler可以执行sendMessage,消息就存储在MessageQueue里面。

最后Looper.looper()的背后执行就是把MessageQueue里的消息进行遍历然后分发的。

ThreadLocal原理剖析

每一个线程都维护了一个ThreadLocalMap对象,而ThreadLocal存的值就在ThreadLocalMap中(key为ThreadLocal本身,value为当前值),这是是为什么ThreadLocal只能在某个线程中存取其他线程访问不了的原因,一个线程可以拥有多个ThreadLocal(一个线程可以有多个变量存储),但是一个ThreadLocal只能负责一个线程。

ThreadLocalMap是ThreadLocal的静态内部类,ThreadLocal总共七百多行代码,而ThreadLocalMap就占了四五百行,ThreadLocalMap和ThreadLocal是相互依偎的,将二者结合在一起是设计模式的单一职责原则。

ThreadLocal 的 public 方法,只有三个:set、get、remove。

set方法

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
  1. 获取当前线程
  2. 得到当前线程所拥有的ThreadLocalMap
  3. 存值,key为当前ThreadLocal本身,value是所有存的对象。

ThreadLocalMap是如何set呢?来看一下ThreadLocalMap源码:

/**
 * Set the value associated with key.
 *
 * @param key the thread local object
 * @param value the value to be 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();
}

ThreadLocalMap存值的背后是Entry数组的存值,数组每一个节点即Entry本身作为key(ThreadLocal),弱引用类型,Entry内部维护了一个变量即用来指向ThreadLocal存的那个值。

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

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

数组在存的过程中通过Key(ThreadLocal)的hash作为数组的下标索引i找到目标的Entry,Entry已存在则替换value值,如果不存在,则创建一个新Entry。

Entry为什么是弱引用类型

如果Entry不是弱引用类型,实质上就会造成节点的生命周期与线程强绑定,只要线程没有销毁,那么节点在GC分析中一直处于可达状态,没办法被回收。

虽然Entry本身是弱引用,但内部维护的值value是强引用,value引用链不断开则Entry不会回收,一旦value引用链断开了,那么Entry本身也就很容易就回收了。

Entry环形数组

ThreadLocalMap维护了Entry环形数组,Entry数组的环形结构来自set方法中的nextIndex函数:

private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
            }
  1. 传入index值加一,返回坐标。
  2. 当index值长度超过数组长度后,会直接返回0,又回到了数组头部,完成了环形结构。

优点就是:

  1. 长度固定,下标永不越界。
  2. 空间利用率高,能够大大的节省内存的开销,能够充分的利用数组的空间。

get方法

分析完上边的set方法,get方法就简单多了,先看源码:

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null)
            return (T)e.value;
    }
    return setInitialValue();
}
  1. 找到当前线程
  2. 找到线程所拥有的ThreadLocalMap
  3. 通过Key(当前的ThreadLocal)找到目标Entry
  4. Entry的value即为get的值

map.getEntry背后是通过Key(当前的ThreadLocal)的Hash作为Entry数组的某个坐标,找到目标Entry。

remove方法

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

主要看背后ThreadLocalMap.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;
        }
    }
}

remove方法也很简单,通过Key(当前的ThreadLocal)的Hash作为Entry数组的某个坐标,找到目标Entry,直接clear(弱引用内部固有方法)。

Entry数组扩容

通过rehash()方法进行判断扩容:

/**
 * Re-pack and/or re-size the table. First scan the entire
 * table removing stale entries. If this doesn't sufficiently
 * shrink the size of the table, double the table size.
 */
private void rehash() {
    expungeStaleEntries();

    // Use lower threshold for doubling to avoid hysteresis
    if (size >= threshold - threshold / 4)
        resize();
}

扩容条件:数组中含有是实例数大于等于threshold 的四分之三(threshold为数组长度的 三分之二)。

resize方法是实际扩容算法:

/**
 * Double the capacity of the table.
 */
private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    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++;
            }
        }
    }

    setThreshold(newLen);
    size = count;
    table = newTab;
}
  1. 扩容的核心是数组的替换和复制。
  2. 新数组大小为老数组的两倍。
  3. 复制,算出index值,向扩容数组中存储,如果该节点冲突,向后找到为null的节点,然后存储。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值