第三章 对象的共享

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();
    }
}
this引用逸出

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测试

  看下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方法

  回过来看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;
    }
}    
Entry

    这是弱引用的子类,指向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 可变对象

  可变对象必须安全的发布,并且每次修改需要使用同步

  

人生就像蒲公英,看似自由,其实身不由己。
原文地址:https://www.cnblogs.com/walker993/p/14586754.html