ThreadLocal详解(AtomicInteger相关)

  • 注意, 本文是以熟悉HashMap源码为前提下开讲, 如果不熟悉HashMap的数据结构, 可以先行了解HashMap再来理解本文.
  • 本文以JDK1.7进行讲解.

在学习Handler机制的过程中, 了解到Looper的成员属性消息队列MessageQueue是通过ThreadLocal实现了每个线程只有一个Looper对象, 每个Looper对象只有一个MessageQueue对象.
实际上, 通常我们把ThreadLocal作为线程隔离的工具使用.
没接触过Android的Handler机制的朋友, 下面讲述的内容不会影响你理解ThreadLocal源码.

分析源码要有的放矢, 带着问题去分析ThreadLocal源码:
首先提出问题: 为什么ThreadLocal可以作为线程隔离的工具?
问题提出后, 开始分析ThreadLocal, 列出常用的两个成员方法:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

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. ThreadLocal#set(T value):
  1. Thread t = Thread.currentThread(); 获取当前线程
  2. ThreadLocalMap map = getMap(t);获取当前线程的对应的ThreadLocalMap对象, getMap(t)方法实现为:
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
  1. 当map !=null; 或map == null;分别执行map.set(this, value); 和 createMap(t, value);方法, 第一次执行set(T value)方法时本地变量map肯定为null, 所以会执行createMap(t, value);
/**
 * 此方法其实就是浓缩了HashMap精髓的方法
 */
private void set(ThreadLocal key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    //这里出现了threadLocalHashCode
    int i = key.threadLocalHashCode & (len-1);//计算数组下标
	//如果在数组table中以下标为i的元素的链表中能找到(方法传进来的)以key为成员属性的Entry对象, 则把该Entry对象的value成员属性修改为(方法传进来的)value, 并return返回本方法
    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;
        }
    }
    //如果for循环中没有找以key为成员属性的Entry对象, 则新创建一个Entry对象, 并放到table数组中
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

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

ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    //threadLocalHashCode又出现了
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    //ThreadLocal对象作为key, firstValue作为value保存在Entry中
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

可以看出执行了createMap(Thread t, T firstValue)后, 当前Thread对应的ThreadLocalMap对象threadLocals不再为null.
在这个方法中也可以清楚的看出, ThreadLocalMap是以ThreadLocal对象作为key保存传进来的值.
值得注意的是, 此方法如果在同一个线程中调用多次, 后面的对象会替换前面设置的对象.

2. 分析ThreadLocal#get()方法:
本方法很简单:
  • List item当在get()方法执行前如果先执行了set(T value)方法后, 本地变量map不再为null, 所以, 会获取到set(T value)方法传进来的value值.
  • 如果没有先执行set()方法, 那么get()方法最终会执行setInitialValue()方法, 最终返回一个null值.

综合ThreadLocal#set(T value)和ThreadLocal#get()两个方法的分析: 我们可以知道:

为什么ThreadLocal可以作为线程隔离的工具:
- 每次set的时候, 都获取所在线程的对象Thread. - 每个Thread只对应唯一一个ThreadLocalMap对象. - 通过这个ThreadLocalMap对象保存set(T value)传递进来的值. - 通过这个ThreadLocalMap对象获取对应的value值. - 以上只保证了一个线程对应的一个ThreadLocalMap对象, 保证一个线程中某个类只有一个实例, 但不保证一个线程中该类一直是同一个实例. - 所以如果我们需要某个线程只有唯一一个不变的实例, 需要在设计上做做文章. 例如我们定义一个类,一个线程中只有一个该类的对象:
/**
 * 自定义Looper
 */
public class Looper {
    /**
     * 静态
     */
    final static ThreadLocal<Looper> testThreadLocal = new ThreadLocal<>();

    /**
     * 不允许new
     */
    private Looper() {

    }

    /**
     * 通过此方法创建本类实例
     */
    public static void prepare() {
        //在set之前先get
        if (testThreadLocal.get() != null) {
            throw new RuntimeException("每个线程只能调用一次prepare()方法");
        }
        testThreadLocal.set(new Looper());
    }

    /**
     * 通过此方法获取本类实例
     *
     * @return
     */
    public static Looper myLooper() {
        return testThreadLocal.get();
    }
}

以上设计可以保证一个Thread对应一个ThreadLocal, 也保证了一个Thread只对应某个类的实例.

PS:
前面提到ThreadLocal吸收了HashMap的精髓, 是一种特殊的HashMap, 而HashMap中, 存在计算hash问题, 那么ThreadLocal的hash值呢? 这就要分析ThreadLocal的一个普通成员threadLocalHashCode及相关三个static成员:
private final int threadLocalHashCode = nextHashCode();

private static AtomicInteger nextHashCode = new AtomicInteger();

private static final int HASH_INCREMENT = 0x61c88647;

private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}
  • 要记住一点: static成员属于类, 不属于对象, 所以, 他们是唯一的, 只加载(初始化)一次.
  • nextHashCode 这个AtomicInteger实例, 它有一个成员属性value, 如果使用无参构造函数创建, 所以这里它的初始默认值是0.
  • threadLocalHashCode 相当于ThreadLocal的hashCode, ThreadLocal就靠这个值实现线程隔离的.
  • 每次new ThreadLocal 对象出来后, threadLocalHashCode 都会重新赋值.
  • 追踪代码可以看到其调用了AtomicInteger类的方法:
public final int getAndAdd(int delta) {
    for (;;) {
        //获取原来的值, 最初默认值为0(重点)
        int current = get();//获取AtomicInteger#value
        //设置期望值
        int next = current + delta;
        //如果current == AtomicInteger#value, 则赋值AtomicInteger#value = next
        if (compareAndSet(current, next))
            return current;//返回原AtoomicInteger#value值
    }
}

方法分析:

  1. getAndAdd(int delta)方法作用就是死循环获取一个值赋值给ThreadLocal#threadLocalHashCode.
  2. 因为ThreadLocal的成员属性nextHashCode是static, 所以所有ThreadLocal对象都共享同一个AtomicInteger对象, 这个AtomicInteger对象的value初始默认值为0, for循环中:
    1. 首先获取value的值, 赋值current保存起来;
    2. 然后设置一个期望 next = current + delta;
    3. 然后通过调用compareAndSet方法进行比较和交换.
    4. 如果这个过程中, value没有被改变, 当执行compareAndSet(current, next)时current等于value;
    5. 那么赋值value, value = next;返回current并赋值给ThreadLocal#threadLocalHashCode;
    6. 此时, 该ThreadLocal对象的成员属性threadLocalHashCode值为0;
    7. 如果重新创建ThreadLocal对象, 因为所有ThreadLocal对象共用一个AtomicInteger对象, 那么这个新创建的ThreadLocal对象的threadLocalHashCode成员属性又会继续执行上述方法. 而唯一不一样的地方在于: 该AtomicInteger对象的value的值为上一次交换后的值(重点), 以此类推!
    8. 上述过程, 保证了不管有多少个ThreadLocal对象, 他们的threadLocalHashCode都是不一样!

getAndAdd(int delta)最后会调用compareAndset()方法:

public final boolean compareAndSet(int expect, int update) {
	return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

最终会调用Unsafe类的compareAndSwapInt()方法, 通过原子操作实现了CAS(Compare And Swap)操作,compareAndSwapInt()方法跟踪到最后, 最底层基于汇编语言实现。 跟系统(CPU指令集)挂钩, 这里就不深究了.
编程中, 设定"原子"代表最小的单位,所以原子操作可以看做最小的执行单位,该操作在执行完毕前不会被任何其他任务或事件打断。

AtomicInteger类相关测试:

/**
 * Created by tgvincent on 2019/7/8.
 * 并发编程, 测试{@link java.util.concurrent.atomic.AtomicInteger}
 */
public class AtomicIntegerTest {
    //volatile让num对线程可见(即变化可被线程感知)
    private volatile int num = 10;

    public static void main(String[] args) {
        AtomicIntegerTest test = new AtomicIntegerTest();
        int temp = test.num;
        System.out.println("test.num: " + test.num);
        System.out.println("temp: " + temp);
        AtomicInteger atomicInteger = new AtomicInteger(test.num);
        new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                test.num++;
            }
        }).start();

        try {
            Thread.sleep(3000);//休眠3秒确保上述线程执行完毕
            System.out.println("=================After thread execution=================");
            System.out.println("test.num: " + test.num);
            System.out.println("temp: " + temp);
            boolean cas = atomicInteger.compareAndSet(test.num, 999);
            int result = atomicInteger.get();
            /**
             * test.num 已经变为1010, != 10, 所以, {@link AtomicInteger#compareAndSet(int, int)}
             * 不会发生交换, 所以{@link AtomicInteger#get()} 还是原来的值10
             */
            System.out.println("is swap?: " + cas +", result: "+ result);
            /**
             * temp为10, == 10, 所以, {@link AtomicInteger#compareAndSet(int, int)}
             * 发生交换, 交换之后, {@link AtomicInteger#value}为999,
             * 所以{@link AtomicInteger#get()} 获取的值为999
             */
            cas = atomicInteger.compareAndSet(temp, 999);
            result = atomicInteger.get();
            System.out.println("is swap?: " + cas +", result: "+ result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

测试结果:

test.num: 10
temp: 10
=================After thread execution=================
test.num: 1010
temp: 10
is swap?: false, result: 10
is swap?: true, result: 999

补充问题: 为什么是0x61c88647? 以下为网上搜索的资料:

  • 生成hash code间隙为这个魔数,可以让生成出来的值或者说ThreadLocal的ID较为均匀地分布在2的幂大小的数组中。
  • 可以看出,它是在上一个被构造出的ThreadLocal的ID/threadLocalHashCode的基础上加上一个魔数0x61c88647的。
  • 这个魔数的选取与斐波那契散列有关,0x61c88647对应的十进制为1640531527。
  • 斐波那契散列的乘数可以用(long) ((1L << 31) * (Math.sqrt(5) - 1))可以得到2654435769,如果把这个值给转为带符号的int,则会得到-1640531527。换句话说(1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1))得到的结果就是1640531527也就是0x61c88647。
  • 通过理论与实践,当我们用0x61c88647作为魔数累加为每个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂取模,得到的结果分布很均匀。
  • ThreadLocalMap使用的是线性探测法,均匀分布的好处在于很快就能探测到下一个临近的可用slot,从而保证效率。为了优化效率。
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值