本文仅讲解单例模式中的饿汉式和懒汉式(双检索)
先来简单的说说什么是单例模式。所谓单例就是在系统中只有一个该类的实例,或者说类只能被创建一次。单例模式(Singleton),也是23种设计模式中常见的一种。
优点:
1、在单例模式中,单例对象的类必须保证只有一个实例存在,这就说明这种模式能一定程度的减少内存开支。
2、配置的读取,现在市场上较为常见的场景,将配置信息放在某个配置文件中,这些配置数据由一个单例对象统一读取。采用单例模式,即可减少系统的性能开销。
缺点:
1、单例模式既只能被实例化一次,没有抽象层,那么它想要扩展,就没有其他的途径(代码无bug的情况下,下文会提到)
2、单例类的职责过重,在一定程度上违背了“单一职责原则”。
顾名思义,饿汉式,饿极了的汉子,有饭就吃,不会规规矩矩的等到饭点才去食堂,所以在加载初始化的时候就已经被创建出实例了。
先上代码(饿汉式):
import java.io.Serializable;
/**
* @ClassName Singleton
* @Descption TODO
* @ClassName lk
* Version 1.0
**/
public class Singleton {
private static final Singleton INST = new Singleton();
//构造方法
private Singleton(){
System.out.println("我是个私有的静态方法,目的是不被随便调用,从而创建实例对象");
}
public static Singleton getInstance(){
return INST;
}
//我用来测试
public static void testfun(){
System.out.println("我是用来debug测试饿汉式的方法");
}
}
饿汉式测试
public class EhanTest {
public static void main(String[] args) {
Singleton.testfun();
//可以根据输出的对象判断是否是同一实例
System.out.println(Singleton.getInstance());
System.out.println(Singleton.getInstance());
}
}
读完了代码,那么上述代码是否真的只能被创建一次呢?
其实不然,接着往下看:
(1)、反射机制
Class<?> clazz = Singleton.class;
//根据类型获取类的构造方法
Constructor<?> constructor = clazz.getDeclaredConstructor();
//让私有的构造方法可以被使用
constructor.setAccessible(true);
//创建个新对象
constructor.newInstance();
既然根据反射机制可以破坏我的单例模式,那我能否预防这种场景发生呢?当然可以
import java.io.Serializable;
/**
* @ClassName Singleton
* @Descption TODO
* @ClassName lk
* Version 1.0
**/
public class Singleton {
private static final Singleton INST = new Singleton();
//构造方法
private Singleton(){
if (INST != null){
throw new RuntimeException("我已心有所属,不能再次被创建");
}
System.out.println("我是个私有的静态方法,目的是不被随便调用,从而创建实例对象");
}
public static Singleton getInstance(){
return INST;
}
//我用来测试
public static void testfun(){
System.out.println("我是用来debug测试饿汉式的方法");
}
}
在构造方法中直接判断是否已经被创建即可
(2)、反序列化
这种场景需要你的单例模式实现序列化接口,还是较为常见
ByteArrayOutputStream os = new ByteArrayOutputStream();
ObjectOutputStream obj = new ObjectOutputStream(os);
//对象转换为字节流
obj.write(singleton);
ObjectInputStream bs = new ObjectInputStream(new ByteArrayInputStream(os.toByteArray()));
//还原对象
bs.readObject();
public Object readResolve(){
return INST;
}
3、Unsafe()方法
unsafe是JDK提供的类,可以根据类型来创建实例,并且和反序列化相同,都是不走构造方法,这种场景目前不知道有什么方法可以预防,有了解的大神可以评论下,我也学习学习,哈哈哈
懒汉两式之第一式:
import java.io.Serializable;
/**
* @ClassName Singleton
* @Descption TODO
* @ClassName lk
* Version 1.0
**/
public class Singleton implements Serializable {
private static Singleton INST = null;
//构造方法
private Singleton(){
System.out.println("我是个私有的静态方法,目的是不被随便调用,从而创建实例对象");
}
public static Singleton getInstance(){
if (INST == null){
INST = new Singleton();
}
return INST;
}
//我用来测试
public static void testfun(){
System.out.println("我是用来debug测试懒汉式的方法");
}
}
上述饿汉式中可以被破坏,从而创建新的对象,那么懒汉式会不会也会被创建多个多个实例,从而破坏单例模式。
public static Singleton getInstance(){
if (INST == null){// 1 判断
INST = new Singleton();//2 创建对象
}
return INST;
}
public static synchronized Singleton getInstance(){
if (INST == null){
INST = new Singleton();
}
return INST;
}
重点来了:懒汉式(双检索)
这种实际就是对懒汉式做了优化,即在获取锁的之前加了判断
import java.io.Serializable;
/**
* @ClassName Singleton
* @Descption TODO
* Version 1.0
**/
public class Singleton implements Serializable {
// 此处的volatile不可忽略
private static volatile Singleton INST = null;
//构造方法
private Singleton(){
System.out.println("我是个私有的静态方法,目的是不被随便调用,从而创建实例对象");
}
public static Singleton getInstance(){
if (INST == null){
synchronized(Singleton.class){
if (null==INST){
INST = new Singleton();
}
}
}
return INST;
}
//我用来测试
public static void testfun(){
System.out.println("我是用来debug测试懒汉式的方法");
}
}
下面对双检索详细讲解
public static Singleton getInstance(){
if (INST == null){// 1 判断
synchronized(Singleton.class){ //2 获取锁
if (null==INST){ // 3 判断
INST = new Singleton(); // 创建对象
}
}
}
return INST;
}
代码中一个关键词,不知是否有注意到:volatile
volatile 有两个作用:
上述代码中volatile 的作用用到了有序性,防止指令重排序。又来接着学吧!
众所周知,创建对象总共分几步?三步!
哈哈哈哈哈,实际差不多,这样更好理解!
问题来了:
public static Singleton getInstance(){
if (INST == null){// 1 判断
synchronized(Singleton.class){ //2 获取锁
if (null==INST){ // 3 判断
INST = new Singleton(); // 创建对象
}
}
}
return INST;
}
如果创建对象的三步骤经过CPU排序之后最终执行的顺序是
1、需要分配内存空间
2、静态变量的赋值
3、调用构造方法(成员变量的初始化赋值)
那么多线程环境下,线程1正在创建对象,刚好执行完2静态变量的赋值,注意,此时静态变量不为null,线程2执行到了1 判断的时候,直接跳过了锁资源这一步,直接返回,构造方法还未执行,拿到的实例有啥用呢?乱七八糟的问题随之而来。
但是当静态变量有了volatile之后,便不会有这种场景,因为volatile会在静态变量赋值语句之后加入内存屏障,阻止了CPU指令排序,让构造方法这步操作越过屏障,在静态变量赋值之后执行,简单的来讲,它的作用就是可以让静态变量在最后一步才执行。
作为技术型文章,没有华丽的语言和生动的文字,有的仅仅是数行代码和对其枯燥的描述,希望本文能对即将开始写作的你,有所帮助。
本文为作者原创文章,未经原创作者同意不得转载