一.概念
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类