【设计模式】单例模式,逐步优化单例模式,并使用反射破解单例模式

什么是单例模式

单例:只允许创建一个该类的对象

实现单例三个步骤

  1. 私有化构造方法
  2. 在类内部创建一个对象
  3. 在类中添加一个公开的方法,返回单例对象

饿汉式

类加载时创建,线程安全

  • 优点:线程安全

  • 缺点:生命周期长,浪费空间

public class SingleTon {
    // 1 私有化构造方法
    private SingleTon() {
    }
    // 2 在类内部创建一个对象
    private static final SingleTon instance = new SingleTon();
    // 3 在类中添加一个公开的方法,返回该对象
    public static SingleTon getInstance() {
        return instance;
    }
}

懒汉式

使用时创建

  • 优点:生命周期短,节省空间
  • 缺点:线程不安全,需要加同步
public class SingleTon2 {
    // 1 私有化构造方法
    private SingleTon2() {
    }
    // 2 在类内部创建一个对象
    private static SingleTon2 instance;
    // 3 通过公开方法返回该对象
    public static SingleTon2 getInstance() {
        if (instance == null) {
            instance = new SingleTon2();
        }
        return instance;
    }
}

测试

public class TestSingleTon {
    public static void main(String[] args) {
        new Thread(() -> {
            for(int i = 0; i < 10; i++) {
                System.out.println(SingleTon2.getInstance().hashCode());
            }
        }).start();
    }
}

结果

出现哈希值不相同的对象,说明创建了多个对象,程序出错

解决

在多线程下,需要使用同步方法使每个线程同步执行

public class SingleTon2 {
    // 1 私有化构造方法
    private SingleTon2() {
    }
    // 2 在类内部创建一个对象
    private static SingleTon2 instance;

    // 3 通过公开方法返回该对象
    public static SingleTon2 getInstance() {
        synchronized (SingleTon2.class) {
            if (instance == null) {
                instance = new SingleTon2();
            }
            return instance;
        }
    }
}

添加synchronized后,就不会创建多个对象了,但是synchronized是重量级锁,需要调用操作系统的内核态,所以效率低

那么如何解决呢?

假如共有100个线程,第一个线程已经创建了对象,那么剩下99个线程无需再去获取锁了,所以应该在获取锁的外层再加一次判断:如果此对象为空,则再进行后续操作,如果对象不为空,说明已经实例化了对象,则无需再进行后续操作,提高了效率

此方法也就是:单例模式中的双重检测加锁机制

public class SingleTon2 {
    // 1 私有化构造方法
    private SingleTon2() {
    }
    // 2 在类内部创建一个对象
    private static SingleTon2 instance;

    // 3 通过公开方法返回该对象
    public static SingleTon2 getInstance() {
        if (instance == null) {
            synchronized (SingleTon2.class) {
                if (instance == null) {
                    instance = new SingleTon2();
                }
            }
        }
        return instance;
    }
}

但是在new对象的时候,在JVM中分为4步:(1)new (2)dup (3)invokespecial (4)astore,第三步和第四步在编译时可能被JVM优化,交换顺序执行:1、2、4、3,在单线程中没有问题,但是在多线程中就会出现问题

那么为了解决指令重排问题,需要在声明对象的时候,添加volatile关键字,就可以防止指令重排

private volatile static SingleTon2 instance;

那么现在就一定是单例模式了吗?

使用反射来创建对象,测试一下

public class TestSingleTon {
    public static void main(String[] args) throws Exception {
        Class<?> aClass = Class.forName("com.robot.reflect.pattern.SingleTon2");
        Constructor<?> con = aClass.getDeclaredConstructor();
        con.setAccessible(true);
        System.out.println(con.newInstance().hashCode());
        System.out.println(con.newInstance().hashCode());
        System.out.println(con.newInstance().hashCode());
    }
}

结果

1625635731
1580066828
491044090

三个对象的哈希值并不同,说明创建了三个对象

此方式也就是使用反射来破解单例模式

那么如何来防止破解呢?

添加一个boolean型变量flag,当第一次创建对象后,flag=true,在构造方法中判断,如果flag=true,说明对象已经创建过了,抛出异常

public class SingleTon2 {
    private static boolean flag = false;
    // 1 私有化构造方法
    private SingleTon2() {
        if (flag) {
            throw new RuntimeException("不能反射破解");
        }
    }
    // 2 在类内部创建一个对象
    private volatile static SingleTon2 instance;

    // 3 通过公开方法返回该对象
    public static SingleTon2 getInstance() {
        if (instance == null) {
            synchronized (SingleTon2.class) {
                if (instance == null) {
                    instance = new SingleTon2();
                    flag = true;
                }
            }
        }
        return instance;
    }
}

测试

需要先正常创建一个对象,然后使用反射来破解

public class TestSingleTon {
    public static void main(String[] args) throws Exception {
        // 先正常创建一次
        SingleTon2.getInstance();
        // 破解
        Class<?> aClass = Class.forName("com.robot.reflect.pattern.SingleTon2");
        Constructor<?> con = aClass.getDeclaredConstructor();
        con.setAccessible(true);
        System.out.println(con.newInstance().hashCode());
        System.out.println(con.newInstance().hashCode());
        System.out.println(con.newInstance().hashCode());
    }
}

结果

Exception in thread "main" java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at com.robot.reflect.pattern.TestSingleTon.main(TestSingleTon.java:19)
Caused by: java.lang.RuntimeException: 不能反射破解
	at com.robot.reflect.pattern.SingleTon2.<init>(SingleTon2.java:12)
	... 5 more

抛出异常,说明反射破解失败

既然是通过变量来标识是否创建了对象,而通过反射也可以修改属性变量的值,所以并没有完全的阻挡反射破解单例模式

所以如何既高效,又安全的实现单例模式呢?

答案是使用静态内部类的方式来实现

静态内部类方式

此方式也属于懒汉式,在使用的时候才会创建

优点:

  • 安全:使用类加载保证线程安全
  • 生命周期短、节省空间:静态内部类只有在使用的时候才会创建
public class TestSingleTon3 {
    private TestSingleTon3() {
    }
    private static class Holder {
        private static final TestSingleTon3 instance = new TestSingleTon3();
    }
    public static TestSingleTon3 getInstance() {
        return Holder.instance;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值