概述
一、前言:
- 单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证并发环境下中,应用该模式的一个类只有一个实例。即一个类只有一个对象实例。
- 常见的单例模式:
饿汉模式: 在程序启动时即创建对象实例。
懒汉模式:仅当程序中使用到改对象时,才回去创建对象。
二、单例模式实例:
1. 饿汉模式,程序启动,对象实例被创建 【不推荐】:
/**
* @Des: 饿汉模式
*/
public class SingleTest01 {
// 静态变量系统启动就会被加载
private static SingleTest01 singleTest = new SingleTest01();
// 私有化构造方方
private SingleTest01(){
System.out.println("SingleTest01 init ");
}
// 返回对象
public static SingleTest01 newSingleTest(){
return singleTest;
}
}
优点:
- 代码实现简单,利用类加载机制保证线程安全。
缺点:
- 在程序启动时,就已经完成实例化,如果对象没有使用,会造成内存浪费。
- 如果对象在启动时存在一些耗时操作,会影响到我们程序启动时间
2、 懒汉模式, 当程序用到该实例时去创建对象,使用 synchronized 对获取方法进行加锁,实现并发安全【不推荐】:
/**
* @Des: 懒汉模式
*/
public class SingleTest02 {
// 静态变量保存对象
private static SingleTest02 singleTest = null;
// 私有化构造方法
private SingleTest02(){
System.out.println("SingleTest02 init ");
}
// synchronized 修饰方法保证并发安全
public static synchronized SingleTest02 newSingleTest() {
if (singleTest == null){
singleTest = new SingleTest02();
}
return singleTest;
}
}
优点:
- 就是饿汉模式的缺点
缺点:
- 效率太低,加锁的细粒度太大,其实仅仅在第一次创建对象时需要加锁,实例化完成之后,获取的时候完全没有必要加锁。
三、单例模式 – 懒汉模式优化:
第一种优化方案,缩小锁粒度:
- 使用 volatile 保证线程可见性
- 对象为被实例化时,通过代码快进行加锁,双重检验保证最终结果单例。
/**
* @Des: 懒汉模式
*/
public class SingleTest03 implements Serializable {
private static final long serialVersionUID = 1L;
public String name;
// 静态变量保存对象, volatile 保证每个线程读取最新的数据
private static volatile SingleTest03 singleTest = null;
// 私有化构造方法
private SingleTest03(){
System.out.println("SingleTest03 init ");
}
// 获取实例对象
public static SingleTest03 newSingleTest() {
if (singleTest == null){
// singleTest 为空,表示没有初始化,将当前类加锁。
synchronized(SingleTest03.class){
// 双重校验,避免第一个if之后有多个线程在等待。
if (singleTest == null){
singleTest = new SingleTest03();
}
}
}
return singleTest;
}
}
第二种优化方案,使用静态内部类:
- 利用类加载机制,保证实例化对象时仅有一个线程,内部类仅在使用时会初始化静态属性,实现了懒加载,效率高, 和第一中优化方案差不多,代码如下:
/**
* @Des: 懒汉模式
*/
public class SingleTest04 {
private static class SingletonInstance{
private final static SingleTest04 SINGLETON = new SingleTest04();
}
private SingleTest04(){
System.out.println("SingleTest04 init ");
}
// 获取实例对象
public static SingleTest04 newSingleTest() {
return SingletonInstance.SINGLETON;
}
}
四、目前推荐的单例的创建方式:
- 上面的方式虽然都实现了单例模式,各有各自的优缺点。但是他们都有一个公共的 缺点,无法防止暴力创建对象,例如: 反射、序列化、克隆。
- 在 《Effective Java》作者的Josh Bloch提倡我们使用枚举的方式来创建单例对象,使用非常简单。
关于枚举的博文: https://blog.csdn.net/zhangyong01245/article/details/103322007 - 代码示例:
/**
* @Des: 枚举实现单例
*/
public enum SingletonEnum {
// Singleton的单例对象,枚举被加载时,由JVM创建,线程安全
INSTANCE;
// 单例对象中的方法
public void print(){
System.out.println(this.hashCode());
}
}
测试类:
class Test{
public static void main(String[] args) {
for (int i =0; i<10;i++){
new Thread(new Runnable() {
public void run() {
SingletonEnum singleton = SingletonEnum.INSTANCE;
singleton.print();
}
}).start();
}
}
}
打印结果:
重点:
上述的 饿汉模式 和 懒汉模式 真的就可以保证单例吗?
下面我们用 反射 和 序列化 对SingleTest01测试一下:
1、反射破坏单例:
反射生成新的对象代码如下:
public class TestReflection {
@SneakyThrows
public static void main(String[] args) {
// 获取 Test01 Test03 无参构造方式
Constructor<SingleTest01> constructorTest01 = SingleTest01.class.getDeclaredConstructor();
// 设置为可访问
constructorTest01.setAccessible(true);
// 各自单例对象
SingleTest01 singleTest01 = SingleTest01.newSingleTest();
// 通过无参构造新的对象
SingleTest01 newSingleTest01 = constructorTest01.newInstance();
// 校验两个对象是否相等
System.out.println("singleTest01 是否等于 newSingleTest01 :" + (singleTest01 == newSingleTest01));
}
}
执行结果如下:
如结果所示, 我们的单例被 反射破坏了, 应对反射破坏单例的方式,我可以改造一下 SingleTest01 构造方法,改造后如下:
private SingleTest01(){
if (singleTest != null){
throw new RuntimeException("单例对象不允许调用构造方法!!");
}
System.out.println("SingleTest01 init ");
}
再次调用反射破坏单例执行结果如下:
2、序列化破坏单例:
如果我们将一个对象 序列化 到文件 然后再从文件读入到内存中,会生成一个新的对象破坏单例吗, ok,我们以SingleTest03 【类必须要实现序列化, SingleTest03代码中已经实现】试一下:
import lombok.SneakyThrows;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class TestSerializable {
@SneakyThrows
public static void main(String[] args) {
SingleTest03 singleTest03 = SingleTest03.newSingleTest();
// 写入到文件
FileOutputStream outFile = new FileOutputStream("SingleTest03");
ObjectOutputStream outObject = new ObjectOutputStream(outFile);
outObject.writeObject(singleTest03);
// 从文件中读取对象
FileInputStream fileIn = new FileInputStream("SingleTest03");
ObjectInputStream objIn = new ObjectInputStream(fileIn);
SingleTest03 fileSingleTest01 = (SingleTest03) objIn.readObject();
System.out.println("singleTest03 是否等于 fileSingleTest01: " + (singleTest03 == fileSingleTest01));
}
}
执行结果, 如下, 单例模式再次被序列化与反序列化破坏
这… 单例模式再次被破坏,我们先看一下 java.io.Serializable, 注释里面有行代码:
意思: 反序列时执行序列化对象里面的这个方法获得新的对象,而不是从反序列化文件中获取
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
给 SingleTest03 加入此方法后:
public class SingleTest03 implements Serializable {
private static final long serialVersionUID = 1L;
...........
Object readResolve() throws ObjectStreamException {
return SingleTest03.newSingleTest();
}
}
再次执行结果如下:
我们可以看到,及时是反序列化生成的对象不再是从文件中获取的, 而是从readResolve() 拿到的 【写到文件中“张三” 的对象没有加载出来】
为什么? 为什么 readResolve() 方法有如此大的 能力 ~
我们从代码一步步跟进去:
1、文件输入流开始读取对象
2、调用 ObjectInputStream 中 readObject0 构建对象
3、在 readObject0()中进入 readOrdinaryObject() 中
4、在 readOrdinaryObject() 中判断是否有 readResolve() 方法,并反射调用该方法,生成最终的对象。
最后:
枚举对于上述的 反射、序列化是如何处理,并保证真正的单例 ,可以看一下我的下一遍文章,关于枚举的讲解
java能力-枚举浅析: https://blog.csdn.net/zhangyong01245/article/details/119841606
最后
以上就是聪明电源为你收集整理的创建型设计模式(一)-单例模式简述及优化的全部内容,希望文章能够帮你解决创建型设计模式(一)-单例模式简述及优化所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复