3.1 可见性
一个线程修改了变量保证对其他线程可见
3.1.1 失效数据
读取的数据的旧值。
3.1.2 非原子的64位操作
非volatile类型的long和double变量,将64位的操作分成两个32位,此种情况是线程不安全的。
3.1.3 加锁与可见性
通过加锁,线程对变量的修改,对下个获取锁的线程来说是立即可见的。
3.1.4 Volatile变量
对Volatile变量的读取总是从主内存中获取最新值。仅保证可见性,不保证原子性。所使用的场景有限,需要满足:
1. 对变量的写不依赖当前值,或者只有一个线程更新变量
2. 不跟其他变量参与不变性条件
3. 访问变量不需要加锁,否则加锁直接搞定
3.2 发布和逸出
发布对象:让某个对象可以被外部访问
对象逸出:不应该被发布的对象意外发布了
3.2.1 this引用逸出
对象还没有完全实例化,它的this引用就发布了。
1. 在构造函数中启动线程
2. 在构造函数中调用可改写的实例方法
public class ThisEscape { private String name; public ThisEscape(List<Runnable> source) { //通过内部类拿到外部类ThisEscape的this引用 source.add(() -> System.out.println(this.name)); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } name = "name"; System.out.println("初始化完毕"); } public static void main(String[] args) { List<Runnable> source = new ArrayList<>(); //拿到this引用 new Thread(() -> new ThisEscape(source)).start(); //访问name属性,这时还没有初始化完成 new Thread(source.get(0)).start(); } }
3.3 线程封闭
只在单线程内访问/修改数据。
3.3.2 栈封闭
局部变量维护在线程独有的栈空间,当局部变量没有逸出时,是线程安全的
3.3.3 ThreadLocal类
为每个线程单独维护一个变量副本,核心思想是将变量存储到线程内部。
public class ThreadLocalTest implements Runnable { private static final ThreadLocal<Integer> THREAD_LOCAL = new ThreadLocal<>(); @Override public void run() { THREAD_LOCAL.set(new Random().nextInt(100)); } public static void main(String[] args) throws InterruptedException { ThreadLocalTest myThreadLocal = new ThreadLocalTest(); new Thread(myThreadLocal).start(); Thread.sleep(100); System.out.println(JSON.toJSONString(myThreadLocal.getThreadLocal())); } public ThreadLocal<Integer> getThreadLocal() { return THREAD_LOCAL; } }
看下ThreadLocal#set方法,ThreadLocalMap是ThreadLocal中定义的静态内部类,每个Thread都有一个自己的ThreadLocalMap,那么怎么实现变量的线程隔离呢?很显然就是将这个变量放在这个ThreadLocalMap中,每个线程就能访问自己的专属变量了。
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
回过来看ThreadLocal#set方法,首先拿到当前线程,拿这个线程内部的ThreadLocalMap属性,若不为空,将ThreadLocal变量作为key,需要专享的值作为value,存入ThreadLocalMap中。
ThreadLocalMap造成的内存泄漏问题,前提知识:弱引用,被弱引用引用的对象,一旦发生GC,不管引用关系这个对象直接被回收,注意不是弱引用被回收。
1. 为什么会造成内存泄漏?
ThreadLocalMap内部保存变量副本的结构:Entry[]
static class Entry extends WeakReference<ThreadLocal<?>> { //保存变量副本 Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
这是弱引用的子类,指向ThreadLocal变量。当发生GC时,ThreadLocal变量被回收了,但是问题来了,只要线程没有被回收,value对象是不会被回收的。因为无法再通过ThreadLocal访问到value,造成了内存泄漏。ThreadLocalMap和HashMap挺像,但是解决哈希冲突的方法不一样,一个使用开放地址,一个使用链式地址
2. 内存泄漏一定会发生吗?
上面说了,只要线程被回收,ThreadLocalMap对象就会被回收,Value对象自然也会被回收。所以一般发生在线程不会顺利回收的情况,比如线程池中线程是复用的。
3. 真的无法处理了吗?
有三种应对方案:remove()、set()、get()、设置ThreadLocal变量为static
remove是主动避免,使用完ThreadLocal后需要手动释放value。
设置static防止只剩ThreadLocalMap的虚引用而被回收,这种情况必须使用remove来回收,否则会造成内存泄漏。设置为static,使得所有实例共享一个ThreadLocal变量,不会影响使用,因为变量的副本是存在每个线程自身的ThreadLocalMap中的。
set和get是ThreadLocalMap自身的修复机制。每次取值赋值后会额外检查一下,删除无效的entry。
1. 插入的key是新key,没有发生冲突,插入后会向后遍历额外检查一下是否有要删除的entry
1.1 cleanSomeSlots方法
向后遍历log2(n)次,如果扫描过程中遇到了脏entry,会延长扫描log2(n)次。首次扫描n=已插入元素的个数,延长n=数组大小
1.2 expungeStaleEntry方法
首先删除当前位置的脏entry,然后向后遍历,找到脏entry删除,直到遇到entry为null的情况。返回entry为null的index。
结合1.1和1.2解释一下删除的思路:外层负责向后寻找脏entry,内层负责删除。内层同时删除当前脏entry到下一个空entry这段距离的脏entry,然后交由上层继续寻找脏entry。外层每查到一个脏entry会延长查找次数。
1.3 replaceStaleEntry方法
这个方法两个目标,一个是尽可能找到最早的脏entry位置,作为清除的开始位置。另一个是找到合适的位置放入新entry,入参的这个位置是一个脏entry,也要一并清除。
(1)将检查点初始化为入参脏entry的位子。
(2)与之前不同的是,首先会前向查找,直到遇到空entry。这个过程中如果遇到脏entry,将这个位置设置为检查点。
(3)然后后向查找,直到遇到空entry,在这个过程中
1. 如果查找到相同的key,则覆盖原有的值,并交换两者的顺序。
如果之前没有设置过检查点,将当前位置设置为检查点,并执行cleanSomeSlots(expungeStaleEntry(检查点), len)。
因为这个时候新的值已经放好了,最早的脏entry位子也找到了,直接从该位置expungeStaleEntry,再遍历cleanSomeSlots
2. 如果找到脏entry,并且之前没有设置过检查点,将当前位置设置为检查点
(4)后向查找没有找到相同的key,那么替换入参位置的脏entry。
(5)如果检查点位变动了,那么执行cleanSomeSlots(expungeStaleEntry(检查点), len),开始清除脏entry。
源码分析请参考:https://www.jianshu.com/p/dde92ec37bd1
4. 为什么要使用弱引用?
网上看到一种说法,如果使用强引用,Thread不回收的情况,除非手动删除,ThreadLocal是无法回收的。
其实说到这里,跟value的内存泄漏是一样的情况,一般使用ThreadLocal也是推荐主动删除的,虽然ThreadLocalMap使用set和get方法做了兜底吧。
3.4 不变性
不可变对象:对象创建后其属性不可修改。不可变对象一定是线程安全的。必须满足一下三点:
1. 创建后状态不能修改
2. 对象的所有域都是final类型的
3. 对象创建期间没有this引用逸出
注意第一点,不可变对象的属性可以是可变对象的引用,只要不提供改变这个属性的方法,则是安全的。
不可变的对象引用:引用被final修饰
3.4.1 Final域
被final修饰的基本数据类型,初始化后不能改变值。被final修饰的对象引用,初始化后不能改变指向的对象。
3.4.2 使用Volatile类型发布不可变对象
在访问和更新多个相关变量时出现竞态条件问题,考虑将这些变量保存在不可变对象中,类似于快照,并使用volatile保证可见性。因此当一个线程获取到这个对象后,不用担心其中的属性会被其他线程修改。
举个例子,查找订单数据,如果缓存不存在,那么查找数据库并更新到缓存。
import java.util.function.Function; import java.util.stream.IntStream; public class QueryOrder { private static volatile OrderCache cache = new OrderCache(null, null); /** * 不可变对象,初始化后无法改变属性,是线程安全的 */ static class OrderCache { //查询条件 private final Integer orderNo; //查询结果 private final Object order; public OrderCache(Integer orderNo, Object order) { this.orderNo = orderNo; this.order = order; } public Object getOrderCache(Integer orderNo) { if (this.orderNo == null || !this.orderNo.equals(orderNo)) { return null; } return order; } } public static void main(String[] args) { //订单号 Integer orderNo = 1024; //定义查找订单的函数 Function<Object, Object> function = (Object i) -> { //先从缓存中获取 Object result = cache.getOrderCache(orderNo); if (result == null) { //缓存中没有则查找库 cache = new OrderCache(orderNo, "订单数据"); System.out.println("线程" + Thread.currentThread().getId() + "查找了数据库并获取了订单数据" + "[" + cache.order + "]"); } else { System.out.println("线程" + Thread.currentThread().getId() + "获取了订单缓存" + "[" + result + "]"); } return cache; }; //并发调用9次 long count = IntStream.range(1, 10).boxed().parallel().map(function).count(); } }
代码的运行结果
上述的代码是线程安全的,在并发场景下都得到了正确的结果。但是在高并发场景下千万别这样做,从结果看,仍然有两次访问数据库的操作,因为查找数据库并缓存这中间存在时差,所有线程这个时候看到的缓存都是空的,都会进行查找并缓存的动作。
3.5 安全发布
3.5.1 不正确的发布
对象的引用和属性没有同时对外可见。
3.5.2 不可变对象和初始化安全性
3.5.3 安全发布的常用模式
保证引用和状态同时对其他线程可见
1. 在静态初始化函数中初始化一个对象引用
2. 将对象引用保存在volatile类型的域或者AtomicReference对象中
3. 将对象引用保存在某个正确构造对象的final域中
4. 将对象的引用保存到一个由锁保护的域中
3.5.4 事实不可变对象
虽然状态是可变的,但一旦发布后不会再更改,比如一条数据的创建时间。这样的对象也可以看成是不可变对象
3.5.5 可变对象
可变对象必须安全的发布,并且每次修改需要使用同步