懒加载成员变量时的线程安全问题

917 阅读3分钟

问题场景

  在平时写代码的时候,我们有时候经常会遇到一些懒加载的成员变量的场景,比如下面这个例子


public class Test {

    private static  Member member;

    public static Member getMember() {
        if (member == null) {
            member = new Member(30, "Jack");
        }

        return member;
    }

   
    public static class Member {

        private String name;

        private int age;

        public Member(int age, String name) {
            this.age = age;
            this.name = name;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }
    }
}

  需要注意的是new Member(30, "Jack");new一个对象出来这个动作一共是分三部分,如下:

  1. 申请一块内存空间
  2. 在这块内存空间上初始化Member对象
  3. 将member对象的引用指向目标内存空间

  上述三个步骤中,2和3有可能会发生指令重排序,发生指令重排之后,new 一个 Member对象出来的步骤就变成了1->3->2。假设Test.member还没有被初始化,且此时有两个线程同时访问Test.getMember方法,线程1先进入了if (member == null)里,去new一个Member对象,这个时候恰好执行完了,1,3两步骤,还没有执行步骤2。此时线程2进来了,开始判断if (member == null),此时member== null 为false,所以线程2不走到if逻辑里而是继续往下走并返回还没有初始化完的member对象,如果在线程1执行步骤2之前,线程2里用拿到的member对象去执行操作就会有问题。

复现

  没复现出来,日了狗了,求求你new对象的时候重排序吧,我透。

解决方式

  为了避免new对象时指令重排序造成的赖加载成员变量时的线程安全问题,第一个想到的解决方法,给member加上关键字volatile,防止member在通过new赋值时出现指令重排序:

方案1:

    private static volatile Member member;

    public static Member getMember() {
        if (member == null) {
            member = new Member(30, "Jack");
        }

        return member;
    }

但是这样子还是不够,多线程的情况下,会new出多个对象来,虽然不影响代码的运行,但是还是有一点点浪费内存啊。   在方案2的基础上进行优化,将getMember编程同步方法:

方案2:

    private static volatile Member member;

    public static synchronized Member getMember() {
        if (member == null) {
            member = new Member(30, "Jack");
        }

        return member;
    }
    

  这样子方案2确实解决了方案1中并发情况下可能会在堆中new出多个Member对象的问题,但是将整个懒加载的方法设置为同步方法,开销实在是太大了,即使在JDK8中,synchronized一开始不会马上是重型锁,而是从偏向锁->轻型锁->自旋锁->重型锁一步步提升锁的等级。   那么如何才能优化同步方法呢?------>缩小同步锁的粒度&double-check,代码优化如下:

方案3:


    private static volatile Member member;

    public static Member getMember() {
        if (member == null) {
            synchronized (Test.class) {
                if (member == null) { //Double-check,保证member还未被初始化
                    member = new Member(30, "Jack");
                }
            }
        }

        return member;
    }