一、简单的懒汉式单例
public class LazyType {
private static LazyType lazyType;
private LazyType(){
}
public static LazyType getInstance(){
if (lazyType == null){
lazyType = new LazyType();
return lazyType;
}
return lazyType;
}
}
测试类
public class TestLazyTon {
public static void main(String[] args) {
LazyType lazyType = LazyType.getInstance();
LazyType lazyType1 = LazyType.getInstance();
System.out.println(lazyType);
System.out.println(lazyType1);
}
}
输出结果:
看似正常,实则在多线程情况下会出现问题
在构造方法中加入一行打印输出代码
在测试类中创建多线程来调用getInstance()方法
public class TestLazyTon {
public static void main(String[] args) {
// LazyType lazyType = LazyType.getInstance();
// LazyType lazyType1 = LazyType.getInstance();
//
// System.out.println(lazyType);
// System.out.println(lazyType1);
for (short i = 0;i < 20;i++){
new Thread(()->{
LazyType.getInstance();
}).start();
}
}
}
输出结果
证明以上设计在多线程环境下不安全。
二、双重锁判断
双重检测锁可以解决以上问题。
public class LazyType {
private static LazyType lazyType;
private LazyType(){
System.out.println("LazyType构造方法执行");
}
public static LazyType getInstance(){
if (lazyType == null){
//锁定当前线程操作的对象
synchronized (LazyType.class){
//同一时刻只会有一个线程执行该代码
if (lazyType == null){
lazyType = new LazyType();
return lazyType;
}
}
}
return lazyType;
}
}
此时再执行main方法便只会创建一个对象,但在极端情况下还是会出问题。在执行LazyType的构造方法时,由于其不是一个原子性操作,会分为一下三步:
1.在内存中给即将要创建的对象分配内存空间(内存地址)
2.创建对象
3.让第2步创建的对象执行第1步的内存地址。
但由于程序在执行的过程中可能会发生指令重排,也就是可能会按照2,1,3或者1,3,2的步骤去创建对象,假设A线程已分配内存空间,且指向了内存地址,但还没有实例对象,也就是1,3,2的步骤,那么此时B线程进入getInstance()方法在判断lazyType == null时发现lazyType不为null,但此时并没有真正创建对象,所以B线程可能产生异常。为了避免此现象,则要使用volatile关键字修饰lazyType避免问题。
public class LazyType {
private volatile static LazyType lazyType;
private LazyType(){
System.out.println("LazyType构造方法执行");
}
public static LazyType getInstance(){
if (lazyType == null){
//锁定当前线程操作的对象
synchronized (LazyType.class){
//同一时刻只会有一个线程执行该代码
if (lazyType == null){
lazyType = new LazyType();
return lazyType;
}
}
}
return lazyType;
}
}
以上方式在用反射的方式获取对象时依然是有问题的。
public class TestLazyTon {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
// LazyType lazyType = LazyType.getInstance();
// LazyType lazyType1 = LazyType.getInstance();
//
// System.out.println(lazyType);
// System.out.println(lazyType1);
// for (short i = 0;i < 20;i++){
// new Thread(()->{
// LazyType.getInstance();
// }).start();
// }
Constructor<LazyType> declaredConstructor = LazyType.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true); // 虽然构造方法私有,但只要将该方法通过反射设置为可访问的就可以实例对象
LazyType lazyType = declaredConstructor.newInstance();
LazyType lazyType1 = declaredConstructor.newInstance();
System.out.println(lazyType);
System.out.println(lazyType1);
}
}
输出结果
这样即便是双重检测在使用反射的情况下也是有问题的。
三、构造器异常
可以通过一个全局变量和在构造器中抛出异常的方式来解决以上问题。
public class LazyType {
private volatile static LazyType lazyType;
private static boolean flag = false;
private LazyType(){
if (lazyType == null && !flag){
flag = true;
System.out.println("layZon构造方法执行");
}else {
throw new RuntimeException("已经实例化对象");
}
}
public static LazyType getInstance(){
if (lazyType == null){
//锁定当前线程操作的对象
synchronized (LazyType.class){
//同一时刻只会有一个线程执行该代码
if (lazyType == null){
lazyType = new LazyType();
return lazyType;
}
}
}
return lazyType;
}
}
此时在运行测试程序,则只会执行一次构造方法,在执行第二次的时候抛出了异常。
但是如果将flag属性通过反射设置为accessible以后,以上操作依然是不安全的。
public class TestLazyTon {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
// LazyType lazyType = LazyType.getInstance();
// LazyType lazyType1 = LazyType.getInstance();
//
// System.out.println(lazyType);
// System.out.println(lazyType1);
// for (short i = 0;i < 20;i++){
// new Thread(()->{
// LazyType.getInstance();
// }).start();
// }
Constructor<LazyType> declaredConstructor = LazyType.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true); // 虽然构造方法私有,但只要将该方法通过反射设置为可访问的就可以实例对象
LazyType lazyType = declaredConstructor.newInstance();
//在执行完第一次构造方法以后,获取flag属性并设置访问权限
Field flag = LazyType.class.getDeclaredField("flag");
flag.setAccessible(true);
//然后将flag属性的值从true在置为false
flag.set(lazyType,false);
LazyType lazyType1 = declaredConstructor.newInstance();
System.out.println(lazyType);
System.out.println(lazyType1);
}
}
输出结果:
可以发现构造方法又执行了两次。