设计模式第一章——单例模式

首先,在谈设计模式之前,要知道一下设计模式的七大原则。
七大设计原则

  • 【开闭原则】是总纲,它告诉我们要【对扩展开放,对修改关闭】;
  • 【里氏替换原则】告诉我们【不要破坏继承体系】;
  • 【依赖倒置原则】告诉我们要【面向接口编程】;
  • 【单一职责原则】告诉我们实现【类】要【职责单一】;
  • 【接口隔离原则】告诉我们在设计【接口】的时候要【精简单一】;
  • 【迪米特法则】告诉我们要【降低耦合度】;
  • 【合成复用原则】告诉我们要【优先使用组合或者聚合关系复用,少用继承关系复用】。

单例模式

首先问一下自己,单例模式是什么?**
简单点的回答就是在整个应用范围内,同一个类只能有一个实例对象存在。

接下来我们就通过代码来逐个讲解单例的实现。

单例模式的实现有两种方式: 饿汉式 和 懒汉式。

- 饿汉式单例:

/**
 * Created by Senliang-Ying on 2020/2/16.
 *
 * 饿汉式单例模式
 * 特点:类初始化的时候就创建好。但是浪费内存空间,
 * 没有线程安全问题,因为JVM在类初始化的时候是互斥的。
 */
public class HPerson {

    //1. 私有化构造方法
    private HPerson(){}


    //2. 静态成员变量
    private static  HPerson hPerson = new HPerson();

    //3. 对外提供方法,获取类对象。(调用私有构造方法,获取类对象)
    public  HPerson gethPerson(){
        return hPerson;
    }

    //4. 普通类方法
    public void sayHelle(){
        System.out.println("我是饿汉式单例");
    }

}

饿汉式单例在性能和空间上有一些缺点,这是需要注意的。一般我们推荐使用懒汉式单例。

懒汉式单例:

懒汉式单例有许多实现方式,接下来我会写6个实现方式,逐步递进,从线程安全的问题开始切入,最后再从反射攻击和序列化攻击进行完善。

01

/**
 * Created by Senliang-Ying on 2020/2/16.
 *
 * 懒汉式单例01 (非线程安全的)
 *
 * 饿汉式单例精髓就是延迟加载,当你需要的时候再创建这个类对象。
 *
 * 懒汉式设计模式,有一种叫法:延迟加载
 *
 *
 * 懒汉式单例模式步骤:
 * 		1:构造私有
 * 		2:定义私有静态成员变量,先不初始化
 * 		3:定义公开静态方法,获取本身对象
 * 			有对象就返回已有对象
 * 			没有对象,再去创建
 *
 * 线程安全问题,判断依据:
 * 		1:是否存在多线程	是
 * 		2:是否有共享数据	是
 * 		3:是否存在非原子性操作
 *
 *
 */
public class LPerson01 {


    //  1.私有化构造函数
    private LPerson01(){}

    //  2. 定义私有静态成员变量 ,不初始化
    private static LPerson01 lPerson01;

    //  3.对外获取对象的接口,内部判断是否为空
    public LPerson01 lPerson01Factory(){
        if(lPerson01 == null){
            lPerson01 = new LPerson01();
        }
        return lPerson01;
    }

}

02


/**
 * Created by Senliang-Ying on 2020/2/16.
 *
 * 懒汉式单例02
 * 在01的基础上在方法上进行了加锁
 * 特点:线程安全,但是除首次创建以外,都进行了多余的加锁,性能不好
 *
 */
public class LPerson02 {


    //  1.私有化构造函数
    private LPerson02(){}

    //  2. 定义私有静态成员变量 ,不初始化
    private static LPerson02 lPerson02;

    //  3.对外获取对象的接口,内部判断是否为空,加锁
    public synchronized  LPerson02 lPerson01Factory(){
        if(lPerson02 == null){
            lPerson02 = new LPerson02();
        }
        return lPerson02;
    }

}

03 双重检查锁(双重校验锁)方式实现


/**
 * Created by Senliang-Ying on 2020/2/16.
 *
 * 懒汉式单例03
 * 在02的基础上进行了双重检查
 * 特点:似乎解决了之前提到的问题,将synchronized关键字加在了内部,也就是说当调用的时候是不需要加锁的,
 * 只有在instance为null,并创建对象的时候才需要加锁,性能有一定的提升。
 *
 * 但是,由于指令重排序的存在,这样的情况,还是有可能有问题的。看下面的情况:
 * 	在Java指令中创建对象和赋值操作是分开进行的,也就是说instance = new Singleton();语句是分两步执行的。
 *  但是JVM并不保证这两个操作的先后顺序,也就是说有可能JVM会为新的Singleton实例分配空间,然后直接赋值给instance成员,
 *  然后再去初始化这个Singleton实例。
 *
 *  这样就可能出错了,我们以A、B两个线程为例:
 *
 * 		a> A、B线程同时进入了第一个if判断
 *
 * 		b> A首先进入synchronized块,由于instance为null,所以它执行instance = new Singleton();
 *
 * 		c> 由于JVM内部的优化机制(指令重排序),JVM先画出了一些分配给Singleton实例的空白内存,
 * 	     	并赋值给instance成员(注意此时JVM没有开始初始化这个实例),然后A离开了synchronized块。
 *
 * 		d> B进入synchronized块,由于instance此时不是null,因此它马上离开了synchronized块并将结果返回给调用该方法的程序。
 *
 * 		e> 此时B线程打算使用Singleton实例,却发现它没有被初始化,于是错误发生了。
 *
 *
 */
public class LPerson03 {


    //  1.私有化构造函数
    private LPerson03(){}

    //  2. 定义私有静态成员变量 ,不初始化
    private static LPerson03 lPerson03;

    //  3.对外获取对象的接口,内部判断是否为空,双重检查锁,内部加锁
    public LPerson03 lPerson01Factory(){
        if(lPerson03 == null){
            synchronized(LPerson03.class){
                if(lPerson03 == null)
                    lPerson03 = new LPerson03();
            }
        }
        return lPerson03;
    }

}

04

package singleton;

/**
 * Created by Senliang-Ying on 2020/2/16.
 *
 * 懒汉式单例04
 * 在03的基础上运用了 volatile 修饰静态成员变量,阻止指令重排序。
 * 特点:volatile关键字 基本满足所有要求了。但是还是可以通过反射或者序列化的方式攻击
 *
 */
public class LPerson04 {


    //  1.私有化构造函数
    private LPerson04(){}

    //  2. 定义私有静态成员变量 ,不初始化
    private static  volatile  LPerson04 lPerson04;

    //  3.对外获取对象的接口,内部判断是否为空,双重检查锁,内部加锁
    public synchronized LPerson04 lPerson01Factory(){
        if(lPerson04 == null){
            synchronized(LPerson04.class){
                if(lPerson04 == null)
                    lPerson04 = new LPerson04();
            }
        }
        return lPerson04;
    }

}

05 静态内部类方式

package singleton;

/**
 * Created by Senliang-Ying on 2020/2/16.
 *
 * 懒汉式单例05
 * 通过内部类来实现
 * 特点:线程安全,效率也高,但是可以通过序列化攻击
 * JVM在类加载的时候,是互斥的,所以可以由此保证线程安全问题
 */
public class LPerson05 {


    //  1.私有化构造函数
    private LPerson05(){}

    //  2. 定义静态内部类
    private static class LPerson05Factory{
        private static LPerson05 lPerson05 = new LPerson05();
    }

   //   3. 对外接口提供获取实例的入口。
    public  static LPerson05 getInstance(){
        return LPerson05Factory.lPerson05;
    }

}

06 枚举方式实现

package singleton;

/**
 * Created by Senliang-Ying on 2020/2/16.
 *
 * 懒汉式单例06 (最简单最安全)
 * 枚举方法实现
 * 特点:基本满足所有要求,而且还不容易被攻击。
 *
 */
public enum LPerson06 {
    //    1. 类声明
    LPerson06("枚举单例");

    private String name;

    private LPerson06(String name){
       this.name = name;
    }

    //    2. 类的内部方法
    public void sayHello() {
        System.out.println("这是通过枚举实现的单例");
    }


    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

至于反射和序列化是如何攻击或者说获取实例的,我们简单写一下,可粗略看一眼。

//** 反射攻击
			public class SingletonAttack {
				public static void main(String[] args) throws Exception {
					reflectionAttack();
				}

				public static void reflectionAttack() throws Exception {
					//1,通过反射,获取单例类的私有构造器
					Constructor constructor = DoubleCheckLockSingleton.class.getDeclaredConstructor();
					//2,设置私有成员的暴力破解
					constructor.setAccessible(true);
					//3,通过反射去创建单例类的多个不同的实例
					DoubleCheckLockSingleton s1 = (DoubleCheckLockSingleton)constructor.newInstance();
					DoubleCheckLockSingleton s2 = (DoubleCheckLockSingleton)constructor.newInstance();
					
					s1.tellEveryone();
					s2.tellEveryone();
					System.out.println(s1 == s2);
				}
			}
		
//** 序列化攻击
			public class SingletonAttack {
				public static void main(String[] args) throws Exception {
					serializationAttack();
				}
				  /**
					* 通过将单例实例写入文件,然后再通过文件读取创建
					*/
				public static void serializationAttack() throws Exception {
					//01 读取文件,用对象序列化流去对对象进行操作
					ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("serFile"));
					//02 通过单例代码获取一个对象
					DoubleCheckLockSingleton s1 = DoubleCheckLockSingleton.getInstance();
					//03 将单例对象,通过序列化流,序列化到文件中
					outputStream.writeObject(s1);
					
					//04 通过序列化流,将文件中序列化的对象信息读取到内存中
					ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(new File("serFile")));
					//05 通过序列化流,去创建对象
					DoubleCheckLockSingleton s2 = (DoubleCheckLockSingleton)inputStream.readObject();
					
					s1.tellEveryone();
					s2.tellEveryone();
					
					System.out.println(s1 == s2);
				}
			}

防止序列化攻击也可以通过在单例类中重写readResolve方法实现:

private Object readResolve() {
  return instance;
}

最后再补充一些关于线程安全的内容:

  • 并发编程的三大特性

  • 原子性
    *** 狭义上指的是CPU操作指令必须是原子操作
    *** 广义上指的是字节码指令是原子操作
    *** 如何保证原子性呢?加锁(synchronize、Lock)

  • 有序性
    *** 狭义上指的是CPU操作指令是有序执行的
    *** 广义上指的是字节码指令是有序执行的
    *** 指令重排序(JIT即时编译器的优化策略)
    ** happend-before六大原则
    ** 两行代码之后的操作,执行结果不存在影响,就可以发生指令重排序(JMM课程)

     				int i =10;
     				boolean a = false;
     				
     				i ++ ; // 操作1
     				a = true; // 操作2
    

3.可见性
*** 在多核(CPU)时代,内存的可见性是一个很常见的并发问题。
*** 可见性的解决需要使用到volatile关键字

其他信息:

  • 对象在JVM中的创建步骤 Student student = new Student();

    • 1.new:开辟JVM堆中的内存空间
    • 2.将内存空间初始化(指的就是对象的成员变量初始化为0值)
    • 3.将内存空间地址(引用地址)赋值给引用类型的变量
    • 结论:在new对象的时候,JIT即时编译器会根据运行情况,对对象创建的过程进行指令重排序(132)
  • 线程执行代码的时候需要通过竞争CPU时间片去执行

  • volatile关键字

    • 作用一:禁止被它修饰的变量发生指令重排操作。是通过内存屏障去完成的禁止指令重排序。
    • 作用二:简单理解是禁止CPU缓存使用,其实是被volatile关键字修饰的变量,在修改之前,都需要将最新CPU缓存中的数据刷新到主内存中。

以上内容为开课吧学习总结。
附上自己代码的git地址:https://github.com/ying105525/ysl-design-pattern.git

你可能感兴趣的:(#,设计模式,JAVA架构)