单例模式(一)

一、定义

保证一个类仅有一个实例,并提供一个全局访问点
应用场景:想确保任何情况下都只有一个实例。例如;线程池、数据库连接池等

二、优缺点

优点:
1、在内存中只有一个实例,减少内存开销。特别是需要频繁进行创建销毁的对象,而且创建和销毁时的性能无法优化
2、避免对资源的多重占用
3、设置全局访问点,严格控制访问
缺点:
没有接口,扩展困难

三、重点

1、私有构造器
2、线程安全
3、延迟加载
4、序列化和反序列化安全
5、反射,防止反射攻击
关于这些重点,在下面的演示代码过程会讲到

四、分类

懒汉式
饿汉式

五、懒汉式单例演进过程

(1) 简单的懒汉式单例模式

//单例对象类
public class LazySingleton {
    private static LazySingleton lazySingleton = null;
    //私有构造器,禁止从外部创建对象
    private LazySingleton(){

    }
    //提供访问点,供外部获取单例对象
    //外部只能通过类来获取对象,所以这里需要是静态方法
    public static LazySingleton getInstance(){
        if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

//测试类
public class Test {
    public static void main(String[] args) {
        LazySingleton lazySingleton = LazySingleton.getInstance();
        System.out.println(lazySingleton);
    }
}

启动测试类,可正常获取单例对象。
但是当前实现方式是仅支持单线程的,当出现多线程访问的时候,会有问题,我们可以通过多线程debug的方式来查看。
先修改代码为多线程访问:

public class T implements Runnable{
    @Override
    public void run() {
        LazySingleton lazySingleton = LazySingleton.getInstance();
        System.out.println(lazySingleton);
    }
}

//测试类
public class Test {
    public static void main(String[] args) {
//        LazySingleton lazySingleton = LazySingleton.getInstance();
//        System.out.println(lazySingleton);

        Thread thread1 = new Thread(new T());
        Thread thread2 = new Thread(new T());
        thread1.start();
        thread2.start();
    }
}

ps:多线程debug
右击你打得断点,选择thread
在这里插入图片描述
运行之后,可以查看当前断点在哪个线程
在这里插入图片描述

好了,言归正传,我们继续验证线程不安全的问题:
让两个线程全都走到 lazySingleton = new LazySingleton();然后放开一个线程,他会创建一个对象,再放开另一个线程,会创建第二个对象。这时就不是单例了,但是这种情况不是一定会出现的,是有一定几率了,还要看代码逻辑的复杂程度和cpu的分配运行情况,但是我们应该解决线程不安全的问题。

(2) 使用synchronized修饰,解决多线程安全问题
修改LazySingleton的访问出口的方法,给该方法上锁,就解决了多线程问题。如下:

public class LazySingleton {
    private static LazySingleton lazySingleton = null;
    private LazySingleton(){

    }
    public synchronized static LazySingleton getInstance(){
        if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

再进行多线程debug,会发现问题解决了。
但是由于synchronized修饰静态方法时,会将整个类的class文件上锁,所以性能会受影响。

(3)DoubleCheckLazySingleton
所以,我们考虑,只给创建对象的代码上锁

public class DoubleCheckLazySingleton {
    private static DoubleCheckLazySingleton doubleCheckLazySingleton = null;
    private DoubleCheckLazySingleton(){

    }

    public static DoubleCheckLazySingleton getInstance(){
        if(doubleCheckLazySingleton == null){
            synchronized (DoubleCheckLazySingleton.class){
                if(doubleCheckLazySingleton == null){
                    doubleCheckLazySingleton = new DoubleCheckLazySingleton();
                }
            }
        }
        return doubleCheckLazySingleton;
    }
}

public class T implements Runnable{
    @Override
    public void run() {
        DoubleCheckLazySingleton doubleCheckLazySingleton = DoubleCheckLazySingleton.getInstance();
        System.out.println(doubleCheckLazySingleton);
    }

这样的代码看起来没有问题,但是事实上是有问题的,问题出现在创建对象的过程

   doubleCheckLazySingleton = new DoubleCheckLazySingleton();
   这句代码看起来只有一句,但实际上他背后做了三件事情
   1.分配内存给这个对象
   2.初始化对象
   3.doubleCheckLazySingleton指向这个内存地址

但是这里的2、3是会发生重排序
Java规范,在单线程内,允许不会影响执行结果的冲排序,在单线程时,2和3重排序,对执行结果是没有影响的。但是多线程时,会导致线程不安全。
单线程时:
在这里插入图片描述
多线程时:
在这里插入图片描述
所以,可能会出现线程安全问题。
解决办法两种,1-禁止重排序 2-重排序不被另一个线程看见
1-禁止重排序 volatile实现线程安全的延迟初始化,重排序会被禁止
2-重排序不被另一个线程看见 使用静态内部类,在静态内部类中创建对象
代码改动如下:

public class DoubleCheckLazySingleton {
//仅仅是在这里加了volatile关键字
    private volatile static DoubleCheckLazySingleton doubleCheckLazySingleton = null;
    private DoubleCheckLazySingleton(){

    }

    public static DoubleCheckLazySingleton getInstance(){
        if(doubleCheckLazySingleton == null){
            synchronized (DoubleCheckLazySingleton.class){
                if(doubleCheckLazySingleton == null){
                    doubleCheckLazySingleton = new DoubleCheckLazySingleton();
                }
            }
        }
        return doubleCheckLazySingleton;
    }
}

经测试,没有问题。

(4)静态内部类方式

public class StaticInnerClassSingleton {
//静态内部类
    private static class InnerClass{
        private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
    }
    //访问出口
    public static StaticInnerClassSingleton getinstance(){
        return InnerClass.staticInnerClassSingleton;
    }
    //这里切记,需要将构造器声明为私有
    private StaticInnerClassSingleton() {
    }
}

public class T implements Runnable{
    @Override
    public void run() {
        StaticInnerClassSingleton staticInnerClassSingleton = StaticInnerClassSingleton.getinstance();
       System.out.println(staticInnerClassSingleton);
    }

这里使用静态内部类,其实是是用了类初始化锁,实现只有一个线程创建对象的目的。
在这里插入图片描述

六、饿汉式

饿汉式的写法比较简单,在类加载的时候就完成实例化,不会有多线程安全问题。

public class HungrySingleton {
//final修饰的变量,必须在类加载完成时完成赋值。当然这里也可以不用声明为final,看情况
  private static final HungrySingleton hungrySingleton = new HungrySingleton();
    private HungrySingleton() {
    }
    public static HungrySingleton getIinstance(){
        return hungrySingleton;
    }
}

public class T implements Runnable{
    @Override
    public void run() {
        HungrySingleton hungrySingleton = HungrySingleton.getInstance();
        System.out.println(hungrySingleton);
    }
}

//final修饰的变量赋值,有两种写法
第一种,像上面那样,直接new

private static final HungrySingleton hungrySingleton = new HungrySingleton();

第二种,我们还可以把创建过程写在静态代码块里,如下

 private static final HungrySingleton hungrySingleton ;
 static{
 hungrySingleton = new HungrySingleton();
 }

七、序列化和反序列化对单例的破坏

//HungrySingleton实现序列化Serializable
public class HungrySingleton implements Serializable{
    private static final HungrySingleton hungrySingleton = new HungrySingleton();
    private HungrySingleton() {
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}
//测试序列化和反序列化
public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        HungrySingleton hungrySingleton = HungrySingleton.getInstance();
        
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("hungry_singleton"));
        oos.writeObject(hungrySingleton);
        
        File file = new File("hungry_singleton");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        HungrySingleton newHungrySingleton = (HungrySingleton) ois.readObject();
        
        System.out.println(hungrySingleton);
        System.out.println(newHungrySingleton);
        System.out.println(hungrySingleton == newHungrySingleton);

    }
}

执行,会发现序列化之前和反序列化之后不是一个对象
查看源码发现
ObjectInputStream 的 readObject方法里面:
会先判断是否实现来序列化接口,如果实现了,会创建一个实例对象,然后判断是不是有readResolve方法
有的话会通过反射执行该方法返回对象,我们需要在这个方法里面返回单例对象
修改HungrySingleton如下

public class HungrySingleton implements Serializable{
    private static final HungrySingleton hungrySingleton = new HungrySingleton();
    private HungrySingleton() {
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
//新增这个方法,方法名固定
    private Object readResolve(){
        return hungrySingleton;
    }
}

八、反射攻击解决方案及原理分析

虽然我们声明了私有的构造器。但事实上,我们可以通过反射打开私有构造器的权限,来创建实例。以上面的饿汉式为例,看代码

//测试类
public class Test {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //单例获取对象
        HungrySingleton hungrySingleton = HungrySingleton.getInstance();
        //反射获取对象
        Class hungrySingletonClass =  HungrySingleton.class;
        Constructor constructor = hungrySingletonClass.getDeclaredConstructor();
        //打开私有构造器权限
        constructor.setAccessible(true);
        HungrySingleton hungrySingleton1 = (HungrySingleton) constructor.newInstance();

        System.out.println(hungrySingleton);
        System.out.println(hungrySingleton1);
        System.out.println(hungrySingleton == hungrySingleton1);

    }
}

运行会发现,我们能够获取到两个不同的实例。
那我们要如何改进,防止反射攻击呢。
修改私有构造器,代码如下:

public class HungrySingleton implements Serializable{
    private static final HungrySingleton hungrySingleton = new HungrySingleton();
    private HungrySingleton() {
    //在这里新增判断就可以防止多次创建实例
        if(hungrySingleton != null){
            throw new RuntimeException("单例对象不可以通过反射获取");
        }
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
    private Object readResolve(){
        return hungrySingleton;
    }
}

上面讲到的静态内部类方式和饿汉式是一样的道理,同样的修改方式就可以防止反射攻击,因为他们都是在类加载的时候就创建了单例对象。

下面我们再以懒汉式为例,看一下这样做行不行(简单懒汉式和doublecheck是一样的情况,因为他们都不是在类加载的时候创建对象),
演示代码:

public class Test {

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //单例获取对象
        LazySingleton lazySingleton = LazySingleton.getInstance();
        //反射获取对象
        Class lazySingletonClass =  LazySingleton.class;
        Constructor constructor = lazySingletonClass.getDeclaredConstructor();
        //打开私有构造器权限
        constructor.setAccessible(true);
        LazySingleton lazySingleton1 = (LazySingleton) constructor.newInstance();
        
        System.out.println(lazySingleton);
        System.out.println(lazySingleton1);
        System.out.println(lazySingleton == lazySingleton1);
    }
}

public class LazySingleton {
    private static LazySingleton lazySingleton = null;
    private LazySingleton(){
        if(lazySingleton != null){
            throw new RuntimeException("不能通过反射获取单例");
        }

    }
    public synchronized static LazySingleton getInstance(){
        if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

运行发现,可以防止反射攻击。
但是当我们把单例创建对象和通过反射创建对象的顺序反过来,你就会发现,其实是没有防止的,还是会创建两个实例。这样的话,假如多线程访问,单例一个线程,反射一个线程,就可能会出现创建两个实例的情况,如果是反射先去实例化这个对象,那就不止一个了。
(那上面的饿汉式和静态内部类的方式,交换顺序就不会有这种问题吗?是的,他们不存在这样的问题,因为他们都是在类初始化的时候就去创建类单例对象。)
那这种懒汉式的反射攻击可以防止吗,答案是不能,没有办法的,我们尝试去在私有构造器里做一些复杂的判断逻辑也是不行的,因为反射都可以拿到并随意修改,达到创建对象的目的。尝试代码:

public class LazySingleton {
    private static LazySingleton lazySingleton = null;
    //用标记位标识是否已经创建了单例
    private static boolean flag = true;
    private LazySingleton(){
    //如果创建过了,就抛出异常
        if(flag){
        //会发现我们这里的逻辑无论多么复杂,反射都能够获取并修改标记位,从而创建对象
            flag = false;
        }else{
            throw new RuntimeException("不能通过反射获取单例"); 
        }
    }

    //public static LazySingleton getInstance(){
    //线程安全问题
    public synchronized static LazySingleton getInstance(){
        if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

所以,简单懒汉式和doublecheck的反射攻击没法避免。
后续将介绍一种既可以避免序列化对单例破坏又可以防止反射攻击的一种单例模式。下篇继续…

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值