Java 并发基础

ThreadLocal

每一个线程都有自己的专属本地变量,通过空间换时间的方式避免并发下线程安全问题

  • 完整的一次请求处理,于唯一一个线程中执行,可以通过 ThreadLocal 共享数据
  • ThreadLocal.set 方法是将值存储到 Thread 线程本身的 ThreadLocalMap 里面

原理

public class ThreadLocal<T> {
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// 存放数据
private Entry[] table;
}
}

整体结构图

img

引用关系图

img

Q&A

Entry的key为什么设计成弱引用?

ThreadLocal 变量生命周期结束后,ThreadLocal 对象就可以被回收;

ThreadLocal为什么会导致内存泄露,如何解决?

虽然 get、set 或 remove 方法会回收 key 为 null 的 value 值,但是如果没有调用这些方法,Entry 和ThreadLocalMap 将会长期存在下去,会导致内存泄露;

使用完 ThreadLocal 对象之后,调用 remove 方法;

ThreadLocal 是如何定位数据的?

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

如果有冲突就通过线性探测再散列,直到找到空 bin;

其他Hash冲突解决方法:开放寻址法(再散列)、拉链法

ThreadLocal 是如何扩容的?

父子线程如何共享数据?

InheritableThreadLocal:初始化时会拷贝一份父线程中 ThreadLocal 值,到子线程 InheritableThreadLocal 中;

ThreadLocal 作为成员变量时,为什么定义成 static 更好?

将 ThreadLocal 定义为 static 可以确保所有线程都访问同一个 ThreadLocal 实例,但它们各自存储的数据是独立的;

确保它们的生命周期与线程的生命周期一致,而不是与类的实例的生命周期一致;

使用 InheritableThreadLocal 时,如果父线程中重新set值,在子线程中能够正确的获取修改后的新值吗?

不会影响到已经存在的子线程中 InheritableThreadLocal 的值,子线程将保持它在创建时从父线程中继承的原始值;

JMM

  • 抽象了 happens-before 原则来解决这个指令重排序问题,保证多线程环境下数据的一致性和可见性。

  • 抽象了线程和主内存之间的关系,提供一套内存模型以屏蔽系统差异

概念

CPU 高速缓存:为解决 CPU 处理速度和内存不匹配的问题;

指令重排序【编译器优化重排 —> 指令并行重排 —> 内存系统重排】:指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致;

内存屏障可以禁止处理器指令发生重排序,从而保障指令执行的有序性。此外,还能保证指令执行的可见性。

happens-before 原则

前一个操作的结果对于后一个操作是可见的,无论这两个操作是否在同一个线程里

  • 只要不改变程序的执行结果,编译器和处理器怎么进行重排序优化都行;
  • 会改变程序执行结果的重排序,JMM 要求编译器和处理器必须禁止这种重排序;

并发的三大特性

原子性:一次操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么都不执行

  • synchronized、各种 Lock 以及各种原子类;

可见性:当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值

  • synchronized、Volatile 以及各种 Lock 实现可见性;

有序性:代码的执行顺序未必就是编写代码时候的顺序

  • Volatile 关键字:保证变量的可见性和代码执行的有序性,但无法在多线程读写变量时保证操作原子性;

  • 内存屏障:Unsafe 类的 fullFence() 可以避免代码重排序;