问题场景
在平时写代码的时候,我们有时候经常会遇到一些懒加载的成员变量的场景,比如下面这个例子
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;
}