1 并发编程的源头——原子性、可见性、有序性

线程安全:当多个线程同时访问某个类或对象时,每个线程访问得到都是预期的正确的结果,那么这个类就是线程安全的。

随着计算机的发展和迭代更新,发生了一下优化:

  • CPU出现了多核,增加了高速缓存(三级缓存),均衡与内存的速度差异 ----> 导致可见性问题
  • 操作系统增加了进程、线程、以及分时复用CPU,均衡CPU与I/O设备的速度差异 ----> 进程切换导致了原子性问题
  • 编译程序优化了指令的执行顺序(CPU层面、JVM层面指令优化重排序),使得能够更加合理的利用缓存 ----> 导致有序性问题

CPU缓存

ios线程安全性_缓存

  

伪共享

CPU缓存是由多个缓存行组成的,缓存行是CPU和内存之间交互的最小单元,每个缓存行大小是64个字节,每次CPU读取数据以块为单位64个字节,一次读取一块数据(猜想上下位的数据也是需要的),避免多次交互,一个缓存行可以缓存多个数据(比如:X, Y, Z三个数据),当多个线程情况下,如果线程A修改数据X,线程B需要修改数据Y,但是都处于同一个缓存行,这时候会存在缓存行竞争,如果线程A获取到缓存行,那么线程B缓存行失效,修改数据会失败,如果线程B竞争到缓存行,线程A修改数据会失败,这样会多次请求失败,影响性能,这就是伪共享问题。

对齐填充(Padding)

针对伪共享,所以需要通过对齐填充(不够64字节用0填充)来解决这个问题,保证一个缓存行只读取一个数据,避免多个线程竞争同一个缓存行带来性能问题。

Java8提供@Contended

除了对字段进行填充之外,还有一个比较清爽的方法,那就是对需要避免陷入伪共享的字段进行注解,这个注解暗示JVM应当将字段放入不同的缓存行,这也正是JEP142的相关内容。Java8中JEP引入了@Contended注解,被这个注解修饰的字段必须和其他字段放在不同的位置,避免出现伪共享,前提是需要通过JVM参数-XX:-RestrictContended开启此功能。

下面的代码就是将x和y置于不同的缓存行。@Contended注解将y移动到远离对象头部的地方,以避免和x一起被加载到同一个缓存行。


public class Test {
    int x;
    @Contended
    int y;
}


Idea中配置开启@Contented:

1) idea工具栏中 Help --> Edit Custom Vm Options
2) -XX参数中 boolean类型, -XX:+RestrictContended为该属性设置为true,-XX:-RestrictContended为属性设置为false;
3) 上述参数为JVM虚拟机启动时使用;

ios线程安全性_缓存_02

1.1 缓存一致性问题

ios线程安全性_java_03

1.1.1 总线锁、缓存锁

如果多核CPU不同核心之间的数据不一致,可以通过缓存锁或总线锁来解决一致性问题;但是总线锁范围太大,CPU性能下降,所以通过缓存锁来控制锁的力度,降低锁的范围,既保证数据一致,又提升性能。而Java中的volatile关键字就是告诉jvm调用Lock指令添加缓存锁。

默认优先添加缓存锁,如果CPU不支持缓存锁,才使用总线锁。因为添加总线锁会阻塞其他CPU核心的运行,那么多核CPU的作用就失效了。

1.1.2 MESI协议

缓存锁:遵守缓存一致性协议(MESI、MOSI)

MESI表示缓存的四种状态(Modify、Exclusive、Shared、Invalidate),MES状态的缓存行数据都可以读取,而 I 状态的缓存行数据失效,只能从内存读取。

M(Modify): 一个缓存行对数据进行了修改,那么状态变成M;
E(Exclusive): 独占,如果当前缓存行的数据是独有的,其他缓存行没有,则是E;
S(Shared): 两个缓存行都缓存了A=1的数据,那么则是共享的;
I(Invalidate): 如果一个缓存行对数据A进行了修改,那么就让同时缓存数据A的其他缓存行失效,状态就变成Invalidate,需要重新从内存加载数据。

下面网址工具可以模拟MESI流程:VivioJS MESI

ios线程安全性_java_04


ios线程安全性_重排序_05


1.2 CPU层面如何导致指令重排序

1.2.1 内存屏障

ios线程安全性_重排序_06


如上图 CPU 0 执行了一次写操作,但是此时 CPU 0 的 local cache 中没有这个数据。

于是 CPU 0 发送了一个 Invalidate 消息,其他所有的 CPU 在收到这个 Invalidate 消息之后,需要将自己 CPU local cache 中的该数据从 cache 中清除,并且发送消息 acknowledge 告知 CPU 0。

CPU 0 在收到所有 CPU 发送的 ack 消息后会将数据写入到自己的 local cache 中。这里就产生了性能问题:当 CPU 0 在等待其他 CPU 的 ack 消息时是处于停滞的(stall)状态,大部分的时间都是在等待消息。为了提高性能就引入的 Store Buffer。

1.2.2 Store Buffer

ios线程安全性_重排序_07


store buffer 的目的是让 CPU 不再操作之前进行漫长的等待时间,而是将数据先写入到 store buffer 中,CPU 无需等待可以继续执行其他指令,等到 CPU 收到了 ack 消息后,再从 store buffer 中将数据写入到 local cache 中。有了 store buffer 之后性能提高了许多,但常言道:“有一利必有一弊。”store buffer 虽然提高了性能但是却引入了新的问题,那就是缓存一致性问题。

案例代码


int a = 0;
function() {
    a = 1;
    b = a + 1;
    assert(b == 2); //false
}
// CPU指令重排序后,b的值是1
b = a + 1;
a = 1;


ios线程安全性_重排序_08

用上图来描述 CPU 将a=1和b=a+1指令重排序的大概流程,具体步骤如下:

  • 1) CPU 0 执行 a = 1。
  • 2) CPU 0 local cache 中没有 a ,发生 cache miss 。
  • 3) CPU 0 发送 read invalidate 消息获取 a ,同时让其他 CPU local cache 中的 a 被清除。
  • 4) CPU 0 把需要写入 a 的值 1 放入到 store buffer 。
  • 5) CPU 1 收到了 read invalidate 消息,回应了 read response 和 acknowledge 消息,把自己 local cache 中的 a 清除了。
  • 但是CPU 0接收其他CPU的 ack 确认消息是需要消耗性能有延迟的,所有CPU层面会对指令做优化重排序,先执行后面的指令。
  • 6) CPU 0 执行 b = a + 1 。
  • 7) CPU 0 收到了 read response 消息得到了 a 的值是 0 。
  • 8) CPU 0 从 cache line 中读取了 a 值为 0 。
  • 9) CPU 0 执行 a + 1 , 并写入 b ,b 被 CPU 0 独占所以直接写入 cache line , 这时候 b 的值为 1。
  • 10) CPU 0 将 store buffer 中 a 的值写入到 cache line , a 变为 1。
  • 11) CPU 0 执行 assert(b == 2) , 程序报错。

导致这个问题是因为 CPU 对内存进行操作的时候,顺序和程序代码指令顺序不一致。在写操作完成之前就先执行了读操作。另一个原因是在同一个 CPU 中同一个数据存在不一致的情况 , 在 store buffer 中是最新的数据, 然而却从 cache line 中去取旧的数据。为了解决在同一个 CPU 的 store buffer 和 cache 之间数据不一致的问题,引入了 Store Forwarding。

1.2.3 Store Forwarding

ios线程安全性_java_09


store forwarding 的目的是当CPU执行读操作时,会从 store buffer 和 cache 中读取数据,当 store buffer 中有数据,CPU直接从store buffer取最新的数据,不再从cache line中取数据,这样就解决了同一个 CPU 中数据不一致的问题。但是由于 Memory Ordering 引起的问题还没有解决。

1.2.4 Memory Ordering


a = 0; b = 0;
void fun1() {
    a = 1;
    b = 1;
}

void fun2() {
    while(b == 1) { // true
        assert(a == 1); // false
        break;
    }
}


ios线程安全性_ios线程安全性_10


假设 CPU 0 执行 fun1() , CPU 1 执行 fun2() , a 变量在 CPU 1 cache 中 , b 变量在 CPU 0 cache 中。 上述代码的执行序列可能会如下:

  • 1) CPU 0执行a=1的赋值操作,由于a不在local cache中,因此,CPU 0将a=1的值放到store buffer中之后,发送了read invalidate命令到总线上去让其他CPU cache中的a值invalidate, 这个时候可能会有延迟。
  • 2) CPU 1执行 while (b == 1) 循环,由于b不在CPU 1的cache中,因此,CPU发送一个read message到总线上,看看是否可以从其他cpu的local cache中或者memory中获取数据。
  • 3) CPU 0继续执行b=1的赋值语句,由于b就在自己的local cache中(cacheline处于modified状态或者exclusive状态),因此CPU0可以直接操作将新的值1写入cache line。
  • 4) CPU 0收到了read message,将最新的b值”1“回送给CPU 1,同时将b cache line的状态设定为shared。
  • 5) CPU 1收到了来自CPU 0的read response消息,将b变量的最新值”1“值写入自己的cache line,状态修改为shared。
  • 6) 由于b值等于1了,因此CPU 1进入while (b == 1)的循环,继续执行。
  • 7) CPU 1执行assert(a == 1),这时候CPU 1的local cache中还是旧的a值,因此assert(a == 1)失败。
  • 8) CPU 1收到了来自CPU 0的read invalidate消息,以a变量的值进行回应,同时清空自己的cacheline。
  • 9) CPU 0收到了read response和invalidate ack的消息之后,将store buffer中的a的最新值”1“数据写入cacheline。

产生问题的原因是 CPU 0 对 a 的写操作还没有执行完,但是 CPU 1 对 a 的读操作已经执行了。毕竟CPU并不知道哪些变量有相关性,这些变量是如何相关的。不过CPU设计者可以间接提供一些工具让软件工程师来控制这些相关性。这些工具就是 memory barrier 指令。要想程序正常运行,必须增加一些 memory barrier 的操作。

1.2.5 内存屏障


a = 0; b = 0;
void fun1() {
    a = 1;
    smp_mb(); //内存屏障
    b = 1;
}

void fun2() {
    while(b == 1) { // true
        assert(a == 1); // false
        break;
    }
}


由于Memory Ordering问题,引入内存屏障,在两个指令之间加入内存屏障smp_mb(),保证在执行后续store操作之前,首先flush store buffer(也就是先将之前的值写入到cacheline中)。smp_mb() 操作主要是为了让数据在local cache中的操作顺序是符合程序执行的顺序的。为了达到这个目标有两种方法:方法一就是让CPU stall,直到完成了清空了store buffer(也就是把store buffer中的数据写入cacheline了)。方法二是让CPU可以继续运行,不过需要在store buffer中记录数据的顺序,在将store buffer的数据更新到cacheline的操作中,严格按照顺序执行,即便是后来的store buffer数据对应的cacheline已经ready,也不能执行操作,要等前面的store buffer值写到cacheline之后才操作。增加smp_mb() 之后,操作顺序如下:

  • 1) CPU 0执行a=1的赋值操作,由于a不在local cache中,因此,CPU 0将a=1的值放到store buffer中之后,发送了read invalidate命令到总线上去让其他CPU cache中的a值invalidate, 这个时候可能会有延迟。
  • 2) CPU 1执行 while (b == 1) 循环,由于b不在CPU 1的cache中,因此,CPU发送一个read message到总线上,看看是否可以从其他cpu的local cache中或者memory中获取数据。
  • 3) CPU 0执行smp_mb()函数,给目前store buffer中的所有数据做一个标记(后面我们称之marked entries)。在这个例子中,store buffer中只有一个marked entry就是“a=1”。
  • 4) CPU 0继续执行b=1的赋值语句,虽然b就在自己的local cache中(cacheline处于modified状态或者exclusive状态),不过在store buffer中有marked entry,因此CPU0并没有直接操作将新的值1写入cache line,取而代之是b的新值”1“被写入store buffer排队等待执行,当然是unmarked状态。
  • 5) CPU 0收到了read message,将b值”0“回送给CPU 1(最新的b值”1“还在store buffer中),同时将b cache line的状态设定为shared。
  • 6) CPU 1收到了来自CPU 0的read response消息,将b值(”0“)写入自己的cache line,状态修改为shared。
  • 7) 完成了总线transaction之后,CPU 1可以load b到寄存器中了(local cache line中已经有b值了),当然,这时候b仍然等于0,因此不满足条件。
  • 8) CPU 1收到了来自CPU 0的read invalidate消息,以a变量的值进行回应,同时清空自己的cacheline。
  • 9) CPU 0收到了read response和invalidate ack的消息之后,将store buffer中的a的最新值”1“数据写入cacheline,并将状态改为modified。
  • 10) 完成了第9步的marked entry之后,store buffer的b值("1")也可以写入到cache line,不过b对应的cache line状态是shared,因为CPU 1中也有b的值("0").
  • 11) CPU 0发送read invalidate命令到总线上去让CPU 1 cache中的b值invalidate。
  • 12) CPU 1收到invalidate消息,清空自己的b cacheline,并回送acknowledgement给CPU 0。
  • 13) CPU 1继续执行while (b == 1),由于此时b不在自己的local cache中,因此 CPU 1发送read消息,请求获取b的数据。
  • 14) CPU 0收到acknowledgement消息,将b对应的cacheline修改成exclusive状态,这时候,CPU 0终于可以将b的新值1写入cacheline。
  • 15) CPU 0收到read消息,将b的新值1回送给CPU 1,同时将其local cache中b对应的cacheline状态修改为shared。
  • 16) CPU 1获取来自CPU 0的b的新值"1",将其放入cacheline中。
  • 17) 此时b值等于1了,因此CPU 1满足while (b == 1)的判断,继续执行。
  • 18) CPU 1执行assert(a == 1),因为前面CPU 1已经清空了a对应的cacheline,所以这时候a值没有在自己的cacheline中,需要通过cache一致性协议从CPU 0那里获得,这时候获取的是a的最新值(“1”),因此assert成功。

简单来说在加入屏障之后的写操作必须等待屏障之前的写操作完成才可以执行,读操作则不受该屏障的影响。

但是由异步执行改成了顺序执行写操作,导致了CPU的停顿,并且需要将写操作的数据放入store buffer中mark,而每个CPU的store buffer不能太大,容量有限, 所以卡顿会导致CPU的store buffer被迅速填满,从而CPU进入等待状态。为了解决这个问题引入了 invalidate queues 可以缓解这个状况,store buffer之所以很容易被填充满,主要是其他CPU回应invalidate acknowledge比较慢,如果能够加快这个过程,让store buffer尽快进入cacheline,那么也就不会那么容易填满了。

invalidate acknowledge不能尽快回复的主要原因是invalidate cacheline的操作没有那么快完成,特别是cache比较繁忙的时候。因此CPU可以缓存这些invalidate message(放入Invalidate Queues),然后直接回应acknowledge,表示自己已经收到请求,随后会慢慢处理。当然,再慢也要有一个度,例如对a变量cacheline的invalidate处理必须在该CPU发送任何关于a变量对应cacheline的操作到bus之前完成。所以 Invalidate Queues 不能根本解决这个问题,只是缓解这个问题。

ios线程安全性_数据_11


注:内存屏障部分内容参考【https://zhuanlan.zhihu.com/p/460780133】,写的非常好

从上面可以看出内存屏障是可以处理指令重排序问题,但是针对不同CPU不同操作系统,屏障指令是不同的,但是JVM是跨平台,需要针对不同操作系统都实现对应的指令,所有JVM定义了线程与内存之间的交易方式,提供了对外的可见性方案Java内存模式(JMM)。

2 Java内存模型JMM

Java内存模型是一种抽象结构,它提供了合理的禁用缓存和禁止重排序的方法来解决可见性、有序性问题。

Java内存模型将硬件层面带来的可见性、有序性问题抽取到了Java应用层面,通过指令关键字来调取CPU提供的内存屏障或者禁用缓存指令从而解决上面的问题。

JMM提供的可见性、有序性解决方案:

  • volatile关键字:可以解决可见性、有序性问题
  • synchronized关键字:加锁
  • final关键字:通过final域的规则保证
  • Happens-Before原则:前面的必须在后面的之前发生或执行

2.1 volatile

将volatile关键字加在前,可以解决可见性、有序性问题
volatile底层调用了Lock指令,Lock指令有以下作用:

  • 将当前处理器缓存行的数据写入到系统内存
  • 这个写回内存的操作会使得在其他CPU中缓存了该内存地址的数据无效

CPU层面提供了内存屏障:

  • Store Barrier:强制所有存储屏障之前的存储指令,都在该指令屏障之前被执行,并且把存储缓冲区的数据都刷到CPU缓存
  • Load Barrier:强制所有load屏障之后的load指令,都在该load屏障之后被执行,并且一直等到load缓冲区被该CPU读完才能执行后面的load指令
  • Full Barrier:复合了Store和Load屏障的功能

Java层面也提供了内存屏障:

  • LoadLoad Barriers:Load1 | LoadLoad | Load2  --> 确保load1在load2之前加载
  • StoreStore Barriers:Store1 | StoreStore | Store2 --> 确保store1在store2之前存储
  • LoadStore Barriers:Load1 | LoadStore | Store2 --> 确保load1加载数据在store2存储数据之前
  • StoreLoad Barriers:Store1 | StoreLoad | Load2 --> 确保store1存储数据并刷新到内存必须在load2加载数据之前

本质上来说,volatile是通过内存屏障来防止指令冲排序,以及禁用CPU高速缓存来解决可见性问题。

Lock指令,本意是禁止高速缓存解决可见性问题,但实际上是一种内存屏障的功能,JMM采用Lock指令作为内存屏障来解决可见性问题。

ios线程安全性_数据_12


注:空白代表代表允许重排序,NO代表不允许重排序。 


// 这两行代码对应的指令是不允许重排序的
volatile int a = 0;
int b = 1;


2.2 synchronized

修饰普通方法:锁的是当前实例对象,多个实例对象之间不影响
修饰静态方法:锁的是当前类的Class对象
修饰代码块:锁的是括号里配置的对象,同步代码块可以实现以上两种,并且更加灵活

jdk1.6之后对synchronized做了一下优化,减少了性能开销:

  • 自适应自旋锁
  • 引入偏向锁、轻量级锁
  • 锁消除、锁粗化

ios线程安全性_重排序_13


默认是偏向锁,即假设只有一个线程来抢占资源,则偏向线程A,实际开发中是不存在的,所以默认延迟开启;

如果发现有其他线程B来获取资源,发现资源已被线程A抢占,则升级锁为轻量级锁,升级锁不阻塞,会通过自旋锁的方式来判断资源是否被释放;

自旋锁是通过循环的方式来判断锁是否被释放;

如果经过几次判断后,资源依然没有被释放,则升级锁为重量级锁,等待资源释放然后再获取,重量级锁是阻塞的;

2.3 final

final与线程安全有什么关系呢?
对于final域,编译器与处理器必须遵守两个重排序规则:

  • 在构造函数中对一个final修饰的变量写入,与随后把这个被构造对象的引用赋值给另一个引用变量,这两个操作之间不能重排序
  • 初次读一个包含final域的对象的引用,与随后初次读这个final域的值,这两个操作不能重排序

对final域写入操作的重排序规则:

  • JMM禁止编译器把对final修饰的变量写的操作重排序到构造函数之外
  • 编译器会在对final修饰的变量写入之后,构造函数return之前,插入StoreStore屏障,这个屏障禁止处理器把对final域的写操作重排序到构造函数之外

2.4 Happens-Before规则

Happens-Before是一种可见性规则,表示前面一个操作的结果对后续操作是可见的。

Java中有6中本身满足Happens-Before规则的规则:

  • 程序顺序规则:单线程中代码必须从前往后顺序执行
  • 监视器锁规则:synchronized锁,一个线程对锁的释放操作必然happens-before后期对它的加锁操作。如果线程1获得锁并修改了值A然后释放锁,那么线程2获得锁后拿到的A的值必然是线程1修改之后的结果。
  • volatile变量规则:对于volatile修饰的变量的写,happens-before任意后续对这个变量的读
  • 传递性:a happens-before b, b happens-before c, 那么a happens-before c
  • start()规则:如果A线程执行了ThreadB.start(),那么start()前的操作happens-before于ThreadB中的任意其他操作
  • join()规则:join()方法happens-before与后面的操作,因为join()方法会阻塞主线程

2.5 原子类Automic——无锁工具

和synchronized、Lock一样,可以解决原子性问题,只是Automic是无锁的,通过while循环实现。

Amomic实现原理:Unsafe类,CAS(compareAndSwapInt()方法)


AtomicInteger atomicInteger = new AtomicInteger(0);
atomicInteger.incrementAndGet();
atomicInteger.get();


2.6 ThreadLocal的使用和原理

线程安全的一种解决方案,类似于线程的副本,多线程之间彼此隔离,互不影响.

每个线程当中都有一个ThreadLocalMap,ThreadLocalMap当中有一个默认16长度的Entry数组存储数据。

2.6.1 基本使用

案例:创建5个线程分别对num加5,5个线程打印的结果并不是期待的5,而是线程之间互相有影响。


public class ThreadLocalDemo {
    private static int num = 0;
    public static void main(String[] args) {
        Thread[] threads = new Thread[5];
        for (int i = 0; i < threads.length; i++) {
            new Thread(()->{
                num += 5;
                System.out.println(Thread.currentThread().getName() + "的值为:" + num);
            }).start();
        }
    }
}


打印结果如下:


Thread-0的值为:10
Thread-2的值为:15
Thread-1的值为:10
Thread-4的值为:20
Thread-3的值为:25


这时候通过ThreadLocal来对值进行操作


public class ThreadLocalDemo {
    private static ThreadLocal<Integer> local = new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };
    public static void main(String[] args) {
        Thread[] threads = new Thread[5];
        for (int i = 0; i < threads.length; i++) {
            new Thread(()->{
                int num = local.get();
                local.set(num += 5);
                System.out.println(Thread.currentThread().getName() + "的值为:" + local.get());
            }).start();
        }
    }
}


打印结果如下:


Thread-0的值为:5
Thread-1的值为:5
Thread-2的值为:5
Thread-3的值为:5
Thread-4的值为:5


SimpleDateFormat常见错误

多线程情况下使用同一个SimpleDateFormat对象去parse日期字符串,会报错

java.lang.NumberFormatException: multiple points

解决办法是通过ThreadLocal进行线程隔离

public class SimpleDateFormatExample {
    private static ThreadLocal<DateFormat> dateFormatThreadLocal = new ThreadLocal<>();
    private static DateFormat getDateFormat() {
        DateFormat dateFormat = dateFormatThreadLocal.get();
        if (null == dateFormat) {
            dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            dateFormatThreadLocal.set(dateFormat);
        }
        return dateFormat;
    }

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

    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            executor.execute(()->{
                parse("2022-07-03 22:58:58");
            });
        }
    }
}

InheritableThreadLocal父子线程传递

通过InheritableThreadLocal,可以实现父子线程之间数据的传递,父线程修改的结果对子线程可见

public class InheritableThreadLocalExample {
    public static void main(String[] args) throws Exception {
        InheritableThreadLocal<String> itl= new InheritableThreadLocal<>();
        Thread t1 = new Thread(()->{
            itl.set("Thread1 value");
            // Thread1修改ThreadLocal的值,当前线程可以拿到新值
            System.out.println(Thread.currentThread().getName() + " value: " + itl.get());
            Thread t2 = new Thread(()->{
                // 通过InheritableThreadLocal,子线程Thread2也可以拿到新值
                // 如果使用ThreadLocal,此处子线程无法拿到Thread1修改的值
                System.out.println(Thread.currentThread().getName() + " value: " + itl.get());
            });
            t2.start();
        });
        t1.start();
    }
}

2.6.2 强引用与弱引用

强引用:强引用会导致对象无法被回收


public class StrongReferenceDemo {
    private static Object obj = new Object();
    public static void main(String[] args) {
        Object strongRef = obj; //强引用
        obj = null;
        System.gc();
        System.out.println(strongRef); //输出结果发现对象依然存在:java.lang.Object@2b193f2d
    }
}


弱引用:对象不会因为被引用而无法回收


public class WeakReferenceDemo {
    private static Object obj = new Object();
    public static void main(String[] args) {
        WeakReference<Object> weakReference = new WeakReference<>(obj);
        obj = null;
        System.gc();
        System.out.println(weakReference.get()); //输出结果:null
    }
}


2.6.3 源码分析

通过下面源码可以看出,ThreadLocal的结构是针对每个Thread都有一个ThreadLocalMap来存储当前线程的数据,而Map的key就是ThreadLocal对象,因为一个Thread可以有多个ThreadLocal。

get / set/ remove方法


public class ThreadLocal<T> {
    ThreadLocal.ThreadLocalMap threadLocals = null;

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

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

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

    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 void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
}


ThreadLocalMap

通过下面源码可以看出,ThreadLocalMap<ThreadLocal, value>的key是一个弱引用,这么做的目的是:如果一个线程已经被销毁,那么ThreadLocalMap对应的key也是null,代表数据是脏数据,可以对其进行销毁清除。


public class ThreadLocal<T> {

    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

        private void set(ThreadLocal<?> key, Object value) {
            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中多次用到0x61c88647来计算下标,这个数是Integer有符号整数的0.618倍,既黄金比例,斐波拉契数列。 使用这个比例,可以使key在数组上被更均匀的分散。


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

private static final int HASH_INCREMENT = 0x61c88647;
private static AtomicInteger nextHashCode = new AtomicInteger();
private final int threadLocalHashCode = nextHashCode();

private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}


2.6.4 ThreadLocalMap的key为什么是弱引用,value为什么是强引用?

强引用:强引用会导致对象无法被回收;
弱引用:就算对象被弱引用,也会被回收;

ThreadLocalMap的key使用弱引用是为了避免内存泄漏,通过弱引用,如果ThreadLocal被销毁,发现ThreadLocalMap的key为null,而Entry不会null,那么就会对脏数据进行清除,减少内存泄漏,如果使用强引用,那么ThreadLocal将无法被销毁。

ThreadLocalMap的value不设置为弱引用,是因为不清楚这个Value除了map的引用还是否还存在其他引用,如果不存在其他引用,当GC的时候就会直接将这个Value干掉了,而此时我们的ThreadLocal可能还处于使用期间,就会造成Value为null的错误,所以将其设置为强引用。

所以为了避免内存泄漏,将key设置为了弱引用,一旦发现key为null而value不为null的数据,就会直接清除。

2.6.5 如何解决内存泄漏问题?

调用remove方法清除对象。

3 安全发布对象

对象的发布:使一个对象能够被当前范围之外的代码所使用。

不安全发布的示例:私有属性,被其他外部调用修改


public class TestDemo {
    private String[] priAttr = {"aaa", "bbb", "ccc", "ddd"};
    public String[] getPriAttrs() {
        return priAttr;
    }
    public static void main(String[] args) throws Exception {
        TestDemo testDemo = new TestDemo();
        String[] priAttr = testDemo.getPriAttrs();
        priAttr[0] = "111"; // 结果变成了{"111", "bbb", "ccc", "ddd"};
    }
}


对象溢出、逃逸

一种错误的发布,当一个对象还没有构造完成时,就使它被其他线程可见。被构造对象obj = this导致引用逃逸。

安全发布对象的4种方法:

  • 在静态初始化函数中初始化一个对象引用
  • 通过volatile关键字
  • 添加final关键字,将对象的引用保存到构造对象的final域中
  • 通过synchronized加锁