首先,在谈设计模式之前,要知道一下设计模式的七大原则。
七大设计原则
首先问一下自己,单例模式是什么?**
简单点的回答就是在整个应用范围内,同一个类只能有一个实例对象存在。
接下来我们就通过代码来逐个讲解单例的实现。
单例模式的实现有两种方式: 饿汉式 和 懒汉式。
- 饿汉式单例:
/**
* 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();
线程执行代码的时候需要通过竞争CPU时间片去执行
volatile关键字
以上内容为开课吧学习总结。
附上自己代码的git地址:https://github.com/ying105525/ysl-design-pattern.git