设计模式(8)单例模式(懒汉,双重检查,静态内部,饿汉)(防反射攻击)

单例模式

保证一个类只有一个实例,并且提供一个全局访问点

适用场景

确保任何情况下都绝对只有一个实例

单服务情况下网站计数器可以用单例,数据库连接池,数据库线程池

优点

在内存中只有一个实例减少了内存开销,在对象频繁需要创建销毁时,并且创建销毁的性能无法优化,这时候单例模式有很明显的优势

可以避免对资源的多重占用(such as :要对一个文件进行写操作,由于只有一个对象可以避免同时写操作)

设置了全局访问点严格控制访问(对外不能new出来对象,只能通过方法来创建单例对象)

缺点

没有接口,扩展困难(如果要扩展的话就得修改代码.)

重点

私有构造器

禁止从外部调用构造函数,创建对象

线程安全

延迟加载

使用到的时候在创建,需要延迟加载

序列化和反序列化安全

单例对象一旦序列化和反序列化,就会破坏单例 

反射

懒汉单例模式(代码笔记)

package com.sun.lazy;

public class LazySingLeton {
    //声明静态的单例对象
    //在初始化的时候没有创建,而是做延迟加载
    private static LazySingLeton lazySingLeton = null;
    //私有构造器 不让外部new
    private LazySingLeton(){

    }
    //得到实例
    //这种方式是线程不安全的
    //在单线程的时候这种写法是没有问题的
    //============================================
    //note:当第一个线程准备创建对象但是没有执行时,第二个线程去判断结果为true,
    //第二个线程也会在new一个对象,这就创建了两个对象,返回最后执行创建的对象
    //加上一个synchronized使方法变为同步方法,因为是static方法相当于锁是类的class文件
    //如果不是static方法相当于锁是堆内存中生成的一个对象
    //使用同步多线程后对象在第一个线程中已经生成,第二个线程将会直接返回生成的对象
    public /*synchronized*/ static LazySingLeton getInstance(){
        synchronized (LazySingLeton.class) {
            if (lazySingLeton == null) {
                lazySingLeton = new LazySingLeton();
            }
        }
        return lazySingLeton;
    }
}

由于 synchronized同步锁会耗费很多资源,并且同步静态方法等同于同步类文件,耗费更大,下面使用双重检查的方法来设计单例模式


双重检查(double check)

减少了性能开销,减少了 synchronized的锁定范围 

new对象的步骤

  1. 分配内存对象
  2. 初始化对象
  3. 设置对象指向刚分配的内存空间

注意:在进行到2和3的时候会经历重排序,他的顺序可能会被颠倒

在多线程的时候,线程1在运行到指向内存空间的时候线程2进行判断,结果不为null然后将还没有初始化的对象返回

package com.sun.doublecheck;

public class DoubleCheck {
    //使用volatile可以禁止重排序
    //在加了volatile后线程就都能看到共享内存的最新状态
    //会将当前内存缓存好的数据写回到系统内存,会使其他内存缓存了该数据无效,因为
    //无效,又会从共享内存同步数据(缓存一致性协议),多cpu可能会读到其他cpu过期的内存
    private volatile static DoubleCheck doubleCheck = null;
    private  DoubleCheck(){

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

上面的方法是通过禁止重排序来实现,也可以让其他线程看不到本线程的重排序 


基于类初始化的延迟加载(静态内部类单例)

在刚开始的时候jvm会初始化类,在执行初始化类的期间,jvm会获取一个锁(只有一个线程可以获得这个锁),这个锁可以同步多个线程对一个类的初始化,基于这个特性可以实现基于线程安全的静态内部类延迟初始化方案

非构造函数看不到;类的初始化

  1. 初始化一个类
  2. 执行这个类的静态初始化
package com.sun.staticInnerClass;

public class StaticInner {
    private static class InnerClass{
        private static StaticInner staticInner=new StaticInner();
    }
    public static StaticInner getInstance(){
        return InnerClass.staticInner;
    }
    //一定要声明私有构造器
    private StaticInner(){

    }
}

饿汉模式

在类加载的时候就完成实例化,这也是饿汉和懒汉最大的区别

因为在类加载的时候就加载好所以多线程下也没有问题

package com.sun.staticInnerClass;

public class StaticInner {
    private final static StaticInner s;
    static {
        s=new StaticInner();
    }
    private StaticInner(){

    }
    public static StaticInner getInstance(){
        return s;
    }
}

序列化和反序列化破坏单例模式

将对象序列化到文件中,在读出来是两个不同的对象

只需要在实现序列化的类中加上readResolve方法就行

反射攻击破坏单例

通过反射的方式,使用setaccessible将构造器置为true,这样可以实例化出对象

解决

这个方法只对静态单例有效 

package com.sun.staticInnerClass;

public class StaticInner {
    private final static StaticInner s;
    static {
        s=new StaticInner();
    }
    private StaticInner(){
        if (s!=null){
            throw new RuntimeException("单例禁止反射调用")
        }
    }
    public static StaticInner getInstance(){
        return s;
    }
}

可以通过改造构造器来禁止反射()

    //是true的时候代表可以实例化
    private boolean fl = true;
    private StaticInner(){
        if (fl){
            //第一次实例化后等于false
            fl=false;
        }else{
            throw RuntimeException("单例模式不能使用反射创建");
        }
    }

这个方法仍然可以通过反射来修改字段从而完成反射攻击

Enum单例

这种方式既可以防御反射,又可以不被序列化破坏

猜你喜欢

转载自blog.csdn.net/emptyee/article/details/87026583