java thread源码分析_Java 源码分析-ThreadLocal

今晚重新看了一下Android 消息传递的机制,其中发现了每一个Looper的对象放在了一个ThreadLocal对象里面,当时就非常的好奇,ThreadLocal这个类是怎么实现的。于是结合了Java 8的源代码和网上的资料对这个类有了一定的理解,在此记录一下,如果有错误之处,请各位指正!

本文参考资料:

1.深入分析 ThreadLocal 内存泄漏问题

2.【Java 并发】详解 ThreadLocal

3.周志明老师的《深入理解Java虚拟机》

其实,大家可以看看上面两篇文章,上面的两篇文章比我写的好。我这里写的都是我自己对ThreadLocal的理解!哈哈!

1.初识ThreadLocal

这里就不对ThreadLocal类的基本使用进行展开了,我们直接从源码入手,来理解ThreadLocal实现的原理。

(1).set方法

当我们在使用ThreadLocal类时,通常会使用set方法来给当前的线程设置一个变量。我们来看看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);

}

set方法的代码非常短,至少比我之前看的其他代码短得多。

首先我们是获取的是当前的线程,然后调用getMap方法来获得TreadLocalMap对象,再来看看getMap方法获取的是什么:

ThreadLocalMap getMap(Thread t) {

return t.threadLocals;

}

哎呀,这个也太简单了吧。这里简单的介绍一下threadLocals表示的含义,threadLocals其实就是Thread类的一个成员变量,也就是说每个Thread对象里面都有一个这个变量,这样就能保证这个变量在线程之间是保持独立,线程之间不会相互的影响。

回到我们的set方法里面来,然后调用map的set方法来进行赋值。请注意这里,第一个参数传入的是this,也就是说是当前的ThreadLocal,而这个参数是干嘛的呢?就是key。也就是说,这里是ThreadLocal对象来作为key的。

map的set方法又在干嘛呢?这里我不解释,待会在讲解ThreadLocalMap类时在详细解释set方法的作用。

(2).get方法

在使用ThreadLocal类时,get方法也是不可避免的,通常我们调用get方法来获取在ThreadLocal里面保存的变量。

public T get() {

Thread t = Thread.currentThread();

ThreadLocalMap map = getMap(t);

if (map != null) {

ThreadLocalMap.Entry e = map.getEntry(this);

if (e != null) {

@SuppressWarnings("unchecked")

T result = (T)e.value;

return result;

}

}

return setInitialValue();

}

get方法也是非常的简单,最终还是到了ThreadLocalMap里面去了。看来不得不去ThreadLocalMap是一个什么东西了。

2.ThreadLocalMap

(1).成员变量

在理解ThreadLocalMap之前,我们还是来看看这类的里面有那些成员变量:

//默认的容量

private static final int INITIAL_CAPACITY = 16;

//Entry数组,用来保存每个键值对的

private Entry[] table;

//map的size

private int size = 0;

//阈值,用来扩容的

private int threshold; // Default to 0

这个成员变量也是非常的少,之前在看HashMap源代码的时候,那家伙!!!

(2).Entry

在理解代码之前,我们必须还说Entry这个东西:

static class Entry extends WeakReference> {

/** The value associated with this ThreadLocal. */

Object value;

Entry(ThreadLocal> k, Object v) {

super(k);

value = v;

}

}

Entry的封装也是非常简单,继承了WeakReference类,表示是一个弱引用。这里弱引用是什么东西呢?其实这个就能扯到JVM的GC那一块了,由于不是本文的重点,所以就不讲解(其实是自己太菜了!!!!)。这里就简单的介绍一下Java中的四种引用,摘抄自周志明老师的《深入理解Java虚拟机》。

1.强引用:在程序代码中普遍存在的,类似 Object obj = new Object()。这类的引用便是强应用。只要强引用存在,GC是永远不会回收该引用的对象。

2.软引用:用来描述一些有用但是并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收对象范围之中,进行第二次的回收。如果这次回收还没有足够的内存的话,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。

3.弱引用:也是用来描述非必须的对象,但是他的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次GC发生之前。当GC工作时,无论当前内存是否足够,都会回收掉纸杯弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。

4.虚引用:也称为幽灵引用或者幻影引用,它是最弱的一种引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被GC回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。

回到Entry里面来,我们需要注意的是key被弱引用关联了。这就是因为这一点,出现了ThreadLocal的内存泄露问题。详细请看:深入分析 ThreadLocal 内存泄漏问题。

(3).set方法

准备的差不多了,我们开始研究ThreadLocalMap的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方法的目的,我们都知道,就是将一个键值对成功保存在Entry数组里面,而保存的index不是指定,而是通过key的hashCode来计算的。将一个key的hashCode通过一个hash函数得到一个index值,而这个值就是这个键值对在数组里面的位置。

但是理想是美好的,现实是残酷的,当两个不同的key而产生相同的index时,就出现了hash碰撞。这种情况下,通常来说有两种解决的办法:1.开放地址法;2.链地址法。由于这两种方法在数据结构中是比较重要的内容,大家应该都学过,所以这里就不解释这两种方法的区别。

在ThreadLocalMap里面使用的是开放地址法,也就是说当出现了哈希碰撞时,会从当前的位置往后找一个为null的位置来保存键值对。

接下来详细的分析一下set方法的代码,首先是:

int i = key.threadLocalHashCode & (len-1);

可能有些老哥在看到这段代码时,就有点懵逼了,这特么是什么鬼东西?如果我说这个其实就是取模运算,老哥们会不会what fuck?

我们详细的解释一下这段代码。当len为2的n次方时,key.threadLocalHashCode & (len-1)就相当于是一个取模运算;例如,当len为16时,上面的代码相当于就是对16取模。这个是为什么呢?因为16的二进制是:10000,当减1变成15时,二进制变为:1111。这样len - 1与任何一个数字进行与运算时,最终剩下来的都是那个数字的低4位,而低4位就是对16取模的结果。如果还不懂的话,看图:

38f196b35a42

如图,相当于是20%16的结果。但是,需要注意的是,len必须是2的n次方,这样才能保证-1之后,低位全为1。这个也是Map为什么是2倍扩容的原因之一。

通过上一步的hash操作,算是找到一个index来存储我们键值对,但是必须考虑到hash碰撞的情况。其中当hash碰撞了之后,是通过这个方法来获取下一个index位置:

i = nextIndex(i, len)

/**

* Increment i modulo len.

*/

private static int nextIndex(int i, int len) {

return ((i + 1 < len) ? i + 1 : 0);

}

当获取的inedx对应的数组中为null时,表示这个位置没有被占据。如果当前key已经在Entry数组中里面存在,直接替换值即可:

if (k == key) {

e.value = value;

return;

}

当时k为null这种情况怎么里面?这个就得引出之前需要注意的Entry中key为WeakReference关联,也就是说,在Entry数组里面,每个Entry对象的key可能随时都会被GC回收,从而导致k为null,由于Entry是一个对象,虽然Entry里面的key被回收了,但是key对应value并没有回收,从而导致这个value不可能再被get到。由于value是被一个强引用关联,除非value所在的Entry对象被回收,value才会被回收,由于这个Entry在数组中有一个强引用,所以除非收到将数组的相应位置置为nulll,否则这个强引用会一直存在。

由于以上原因,导致Entry对象不能回收,从而导致value内存泄露。

卧了个槽,怎么去分析内存了,跑题了跑题了。回到主题,根据上面的解释,我们知道了k为null是因为相应k被释放导致的,此时为了防止内存泄露,会去清理垃圾对象。如下:

if (k == null) {

//将旧的对象替换程新的Entry对象,并且清理垃圾对象

replaceStaleEntry(key, value, i);

return;

}

当然如果找到了空位置的话,就占了。

tab[i] = new Entry(key, value);

int sz = ++size;

if (!cleanSomeSlots(i, sz) && sz >= threshold)

rehash();

添加数据之后,必不可少的就是判断是否达到了扩容的条件。

这个就是整个set的过程。这里,简单的总结一下。

1.在set方法里面,将key的hashCode对len进行取模运算来获取index。这里需要注意的是len必须是2的n次方。

2.如果发生了哈希碰撞了,set方法采用的是开发地址方法来解决的。

3.在进行开放地址方法时,有可能会出现key被回收的情况,这里会可能会导致内存泄露的问题。官方的手段是,对key为null的Entry进行清理。

(3).getEntry方法

看完了set方法,我们再来看看get方法。在ThreadLocal的get方法里面,是通过调用getEntry来获取一个Entry的

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

}

从这段代码里面,我们可以看出来,如果index能获取得到的Entry的key与想要找到的key是一样的话,那么直接就返回,否则的话,就通过开放地址法来循环遍历寻找。

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;

if (k == null)

expungeStaleEntry(i);

else

i = nextIndex(i, len);

e = tab[i];

}

return null;

}

同时我们发现, 在getEntryAfterMiss方法里面是通过循环遍历找一个Entry。

3.总结

这个ThreadLocal感觉也不是很难,可能是自己太菜了吧,很多的问题都没有发现。在这里对ThreadLocal做一个总结。

1. ThreadLocal实现线程的局部变量是通过Thread的一个ThreadLocalMap成员变量,因为每个线程对象都持有自己的ThreadLocalMap对象,所以线程之间不会有影响的。

2.ThreadLocalMap使用的存储结构与普通的Map结构非常相似,只是ThreadLocalMap使用的开放地址法来解决哈希碰撞的。

3.ThreadLocalMap的key使用的是WeakReference对象关联,所以会出现key为null,但是value不能被释放的情况。官方在每次的set和get方法里面,会对Entry进行清理。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值