Java并发:安全发布对象

一.概念

1.什么是 发布对象?
使一个对象能够被当前范围之外的代码所访问,我们就称发布了这个对象。

下面例子解释何为对象发布:

public class Test{
    
    

  private String[] states={
    
    "c","a","r"};
  //这个方法对外发布了此类的states对象
  public String[] getStates(){
    
    
        return states;
   }
   
public static void main(String[] args){
    
    
      Test test=new Test();
      test.getStates()[0]="b";//外部可以得到Test的私有对象域且对它进行修改
      System.out.println(Arrays.toString(test.getStates()));
  }
}

显然这个例子的发布方式不是安全的,试想,如果有多个线程同时得到states对象,且对它进行修改,由于线程之间的执行是并发的,我们写的代码执行后很可能不能得到我们想要的结果。

2.什么是对象逸出?
它是一种错误的发布方式,当一个对象还没有被构造完成时,就让它可以被其他线程看见

下面的例子展示了何为对象逸出:

public class Test {
    
    
  private int number=0;
  public Test(){
    
    
      new InnerClass();
  }
  private class Innerclass{
    
    
       public InnerClass(){
    
    
            System.out.println(Test.this.number);//这里调用的是还没有初始化完全的类,this溢出了,不安全
        }
  }
  public static void main(String[] args){
    
    
          new Test();
   }
}

二.安全的发布对象

1.我们可以遵循下面原则来安全的发布对象:

1)在静态初始化函数中初始化一个对象引用
2)将对象的引用保存到volatile类型域或者AtomicReference对象中
3)将对象的引用保存到某个正确构造对象的final类型域中
4)将对象的引用保存到一个由锁保护的域中

2.下面我们以单例模式为例学习:
单例模式: 保证一个类只被初始化一次

1)懒汉模式:单例实例在第一次使用时才进行创建
一般我们是这样写的:

public class SinglentonExa{
    
    
   private SingletonExa(){
    
    }//构造函数私有保证实例只能由此类的方法产生

   private static SingletonExa instance=null;//单例对象
   //静态工厂方法
   public static SingletonExa getInstance(){
    
    
       if(instance==null){
    
    
             instance=new SingletonExa(); 
       }
       return instance;
  }
}

很明显,在单线程的环境下它也许是安全的,但在多线程的环境下,试想如果有两个线程第一次同时进入了静态工厂方法中,那么这个类就会被实例化两次,不安全。

我们可以看到关键在于静态工厂方法,如果我们可以赋予这个方法原子性,那么多线程下就安全了

public class SinglentonExa{
    
    
   private SingletonExa(){
    
    }//构造函数私有保证实例只能由此类的方法产生

   private static SingletonExa instance=null;//单例对象
   //加锁的静态工厂方法
   public static synchronized SingletonExa getInstance(){
    
    
       if(instance==null){
    
    
             instance=new SingletonExa(); 
       }
       return instance;
  }
}

是否这样就是完美的吗?我们试想,上面的做法相当于让线程排队进入工厂方法中,这样做是不是违背了并发的思想?

那能不能让线程既能并发进入工厂方法获得对象,又能保证线程安全的方法呢?
也许我们可以在工厂方法内部进行加锁:

public class SinglentonExa{
    
    
   private SingletonExa(){
    
    }//构造函数私有保证实例只能由此类的方法产生

   private static SingletonExa instance=null;//单例对象
   //内部加锁的静态工厂方法
   public static SingletonExa getInstance(){
    
    
       if(instance==null){
    
    
           synchronized (SingletonExa.class){
    
    
                  if(instance==null) 
                    instance=new SingletonExa(); 
             }
       }
       return instance;
  }
}

这个做法叫双重检测机制,这样做确实解决了多线程进入问题,但也引发了另一个问题导致不安全:
我们要从CPU的指令说起,当一个线程执行instance=new SingletonExa();时有如下步骤:
a.分配对象的内存空间 memory=allocate();
b.初始化对象 ctorInstance();
c.设置instance指向刚分配的内存 instance=memory;

但是 在多线程情况下,由于CPU和JVM的优化上面的步骤可能发生重排序:
a.分配对象的内存空间 memory=allocate();
b.设置instance指向刚分配的内存 instance=memory;
c.初始化对象 ctorInstance();
这就可能导致在多线程同时进入时,有的线程得到的可能是未被初始化的内存空间。

如何解决呢?我们在JMM中学到了volatile可以禁止重排序,于是有:

public class SinglentonExa{
    
    
   private SingletonExa(){
    
    }//构造函数私有保证实例只能由此类的方法产生

   private volatile static SingletonExa instance=null;//单例对象 遵循规则2
   //内部加锁的静态工厂方法
   public static SingletonExa getInstance(){
    
    
       if(instance==null){
    
    
           synchronized (SingletonExa.class){
    
    //遵循规则4
                  if(instance==null) 
                    instance=new SingletonExa(); 
             }
       }
       return instance;
  }
}

2)饿汉模式:单例实例在类装载的时候就进行创建
一般的写法是:

public class SingletonExa{
    
    
 private SingletonExa(){
    
    }//构造函数私有
 
 private static SingletonExa instance=new SingletonExa();//遵循规则1

 public static SingletonExa getInstance(){
    
    
    return instance;
  }
}

这样做虽然简单,但如果构造方法存在很多处理,那么类的加载会很慢,如果我们不用这个类,也会造成资源浪费。

3)推荐做法:枚举

public class SingletonExa{
    
    

 private SingletonExa(){
    
    }//构造函数私有
 
 public static SingletonExa getInstance(){
    
    
    return Singleton.INSTANCE.getInstance();
  }
  
  private enum Singleton{
    
    
    INSTANCE;
    private SingletonExa singleton;
    //JVM保证此方法只被执行一次
    Singleton(){
    
    
       singleton=new SingletonExa();
   }
   public SingletonExa getInstance(){
    
    
       return singleton;
  }
 }
 
}

这样做既能保证多线程同时进入而又不影响其安全性。

总结:在编写单例模式的线程安全的发布时,我们可以用懒汉模式的双重检测机制,饿汉模式,枚举方式,其中枚举方式最为方便安全。

3.不可变类
不可变类发布的对象称为不可变对象,不可变对象都是安全的。

1)不可变对象需要满足下面条件:

a.对象创建后其状态不可修改
b.对象的所有域都是final类型
c.在对象创建期间没有逸出

再学习不可变类之前我们需要了解下final关键字:

a.final修饰的类不可被继承,类中所有成员方法会被隐式地指定为final方法
b.final修饰的方法不可被覆盖
c.final修饰的基本数据变量不可被修改,对象引用不可被修改,但允许修改对象属性

关于如何创建不可变类我们可以参考String类

猜你喜欢

转载自blog.csdn.net/c1776167012/article/details/109020816