发布对象:使一个对象能被当前范围外的代码所使用。
与之对应的一个问题是 对象逸出
对象逸出是一种错误的发布对象方式,当一个对象还没有构造完时,就被其他线程所见。常见于多线程之中。
错误的发布对象:
私有成员变量在对象的公有方法中被修改。当其他线程访问该私有变量时可能得到不正确的值。
例如:
private String[] states = {"a", "b", "c"}; public String[] getStates() { return states; } public static void main(String[] args) { UnsafePublish unsafePublish = new UnsafePublish(); log.info("{}", Arrays.toString(unsafePublish.getStates())); unsafePublish.getStates()[0] = "d"; log.info("{}", Arrays.toString(unsafePublish.getStates()));
对象逸出的例子:
在这个内部类中,有对封装对象的私有成员变量的引用。在对象没有被正确完成构造之前,它就会被发布。不安全的因素就是在构造函数中显示的启动了一个线程,不管是显示还是隐式的启动,都会造成this引用的逸出,新线程在对象完成构造之前就看到了。
在构造函数中不要直接构造对象,其实就是创建了一个线程,存在上述的逸出风险,
如果要在构造函数中创建线程,应该用一个专有的start或初始化的方法来统一启动线程。可以采用 工厂方法 和私有构造函数解决。
总之在对象未完成之前不能将其发布,是安全发布的准则。否则其他线程可能会看到旧值,
public class Escape { private int thisCanBeEscape = 0; public Escape () { new InnerClass(); } private class InnerClass { public InnerClass() { log.info("{}", Escape.this.thisCanBeEscape); // 存在逸出风险 } } public static void main(String[] args) { new Escape(); } }
如何安全的发布的?
1、在静态初始化函数中初始化一个对象的引用
2、将对象的引用保存在volatile类型域中
3、将对象的引用保存在某个正确构造的final类型域中。
4、将对象的引用保存咋一个y由锁保护的域中。
下面是一个用静态工厂方法线程不安全的例子
这种方式称为懒汉模式 :
单例实例在第一次使用时进行创建
该段代码在单线程中运行是没有问题的,但是在多线程中会存在问题,例如两个线程都开始访问这个方法时就会出现该实例被实例化两次,如果实例化过程中有逻辑运算则返回不同的值。
// 私有构造函数 private SingletonExample1() { } // 单例对象 private static SingletonExample1 instance = null; // 静态的工厂方法 public static SingletonExample1 getInstance() { if (instance == null) { instance = new SingletonExample1(); } return instance; }
那么如何保证在多线程中保证呢?
饿汉模式:
单例实例在类装载时进行创建.
因此使用饿汉模式的时候需要注意
1、构造私有函数的时候没有太多的处理。
2、这个类被加载后肯定会被使用。
3、如果使用静态域和静态代码块初一定要注意 静态域和静态代码块的顺序.(静态域在静态代码块之前)
// 私有构造函数 private SingletonExample2() { } // 单例对象 private static SingletonExample2 instance = new SingletonExample2(); // 静态的工厂方法 public static SingletonExample2 getInstance() { return instance; } 上面的方法也可以用过静态带啊模块来实现实例化 // 私有构造函数 private SingletonExample6() { } // 单例对象 private static SingletonExample6 instance = null; static { instance = new SingletonExample6(); } // 静态的工厂方法 public static SingletonExample6 getInstance() { return instance; } public static void main(String[] args) { System.out.println(getInstance().hashCode()); System.out.println(getInstance().hashCode()); }
深究一下 懒汉模式时候是否可以实现线程安全呢?
使用synchronized关键字。
该方法 同一时间内只允许一个线程访问,是可以保证线程安全的,但是牺牲了效能 ,竞争激烈的时候会有很多线程wait
// 私有构造函数 private SingletonExample3() { } // 单例对象 private static SingletonExample3 instance = null; // 静态的工厂方法 public static synchronized SingletonExample3 getInstance() { if (instance == null) { instance = new SingletonExample3(); } return instance; }
synchronized修饰方法所造成的的效能问题是否可以再优化呢?
但是可以的,可以将synchronized下沉到方法实现里面。代码如下:在判断完之后单独锁定该类(也就是开始说到如何保证线程安全的第四点)通常称其为 双重同步锁单例模式。
这个类是线程安全的吗?
并不是,
instance = new SingletonExample4();
当执行完上面这行代码时需要三步:
// 1、memory = allocate() 分配对象的内存空间
// 2、ctorInstance() 初始化对象
// 3、instance = memory 设置instance指向刚分配的内存
在单线程下上面逻辑是线程安全的,但是多线程下会发生指令重排
// JVM和cpu优化,发生了指令重排
// 1、memory = allocate() 分配对象的内存空间
// 3、instance = memory 设置instance指向刚分配的内存
// 2、ctorInstance() 初始化对象
在指令重排后发生的对象未完成初始化的动作时,却被直接return。
// 私有构造函数 private SingletonExample4() { } // 单例对象 private static SingletonExample4 instance = null; // 静态的工厂方法 public static SingletonExample4 getInstance() { if (instance == null) { // 双重检测机制 // B synchronized (SingletonExample4.class) { // 同步锁 if (instance == null) { instance = new SingletonExample4(); // A - 3 } } } return instance; }
如何避免其指令重排呢?
使用关键字volatile 和双重检查机制可以禁止指令重排。
// 私有构造函数 private SingletonExample5() { } // 1、memory = allocate() 分配对象的内存空间 // 2、ctorInstance() 初始化对象 // 3、instance = memory 设置instance指向刚分配的内存 // 单例对象 volatile + 双重检测机制 -> 禁止指令重排 private volatile static SingletonExample5 instance = null; // 静态的工厂方法 public static SingletonExample5 getInstance() { if (instance == null) { // 双重检测机制 // B synchronized (SingletonExample5.class) { // 同步锁 if (instance == null) { instance = new SingletonExample5(); // A - 3 } } } return instance; }