前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Java设计模式之单例模式

Java设计模式之单例模式

作者头像
CoderJed
发布2018-09-13 10:46:40
发布2018-09-13 10:46:40
39200
代码可运行
举报
文章被收录于专栏:Jed的技术阶梯Jed的技术阶梯
运行总次数:0
代码可运行

这个模式是很有意思,而且比较简单,但是我还是要说因为它使用的是如此的广泛,如此的有人缘,单例就是单一、独苗的意思,那什么是独一份呢?你的思维是独一份,除此之外还有什么不能山寨的呢?我们举个比较难复制的对象:皇帝,中国的历史上很少出现两个皇帝并存的时期,是有,但不多,那我们就认为皇帝是个单例模式,在这个场景中,有皇帝,有大臣,大臣是天天要上朝参见皇帝的,今天参拜的皇帝应该和昨天、前天的一样(过渡期的不考虑,别找茬哦),大臣磕完头,抬头一看,嗨,还是昨天那个皇帝,单例模式,绝对的单例模式,先看类图:

然后我们看程序实现,先定一个皇帝:

代码语言:javascript
代码运行次数:0
运行
复制
/**
 * @Description: 
 * 中国的历史上一般都是一个朝代一个皇帝,有两个皇帝的话,必然要PK出一个皇帝出来
 */
public class Emperor {

    // 定义一个皇帝放在那里,然后给这个皇帝名字
    private static Emperor emperor = null;
    
    private Emperor() {
        // 世俗和道德约束你,目的就是不让你产生第二个皇帝
    }
    
    public static Emperor getInstance() {
        // /如果皇帝还没有定义,那就定一个
        if(emperor == null) {
            emperor = new Emperor();
        }
        return emperor;
    }
    
    // 皇帝叫什么名字呀
    public static void emperorInfo() {
        System.out.println("吾乃康熙大帝...");
    }
    
}

然后定义大臣:

代码语言:javascript
代码运行次数:0
运行
复制
/**
 * @Description: 
 * 大臣是天天要面见皇帝,今天见的皇帝和昨天的,前天不一样那就出问题了!
 */
public class Minister {

    public static void main(String[] args) {
        // 第一天
        Emperor emperor1 = Emperor.getInstance();
        emperor1.emperorInfo();
        
        // 第二天
        Emperor emperor2 = Emperor.getInstance();
        emperor2.emperorInfo();
        
        // 第二天
        Emperor emperor3 = Emperor.getInstance();
        emperor3.emperorInfo();
        
        // 三天见的皇帝都是同一个人,荣幸吧!
        
    }
    
}

结果:
吾乃康熙大帝...
吾乃康熙大帝...
吾乃康熙大帝...

看到没,大臣天天见到的都是同一个皇帝,不会产生错乱情况,反正都是一个皇帝,是好是坏就这一个,只要提到皇帝,大家都知道指的是谁,清晰,而又明确。问题是这是通常情况,还有个例的,如同一个时期同一个朝代有两个皇帝,怎么办? 看看这篇文章吧:Java设计模式之多例模式

单例模式很简单,就是在构造函数中多了加一个构造函数,访问权限是private的就可以了,这个模式是简单,但是简单中透着风险,风险?什么风险?在一个B/S项目中,每个HTTPRequest请求到J2EE的容器上后都创建了一个线程,每个线程都要创建同一个单例对象,怎么办?我们写一个通用的单例程序,然后分析一下:

我们来看黄色的那一部分,假如现在有两个线程A和线程B,线程A执行到singletonPattern = new SingletonPattern(),正在申请内存分配,可能需要0.001微秒,就在这0.001微秒之内,线程B执行到if(singletonPattern == null),你说这个时候这个判断条件是true还是false?是true,那然后呢?线程B也往下走,于是乎就在内存中就有两个SingletonPattern的实例了,这就出问题了。如果你这个单例是去拿一个序列号或者创建一个信号资源的时候,会导致业务逻辑混乱!数据一致性校验失败!最重要的是你从代码上还看不出什么问题,这才是最要命的!因为这种情况基本上你是重现不了的,不寒而栗吧,那怎么修改?有很多种方案,我就说一种,能简单的、彻底解决这个问题的方案:

代码语言:javascript
代码运行次数:0
运行
复制
public class SingletonPattern {

    private static final SingletonPattern singletonPattern = new SingletonPattern();

    private SingletonPattern() {
    
    }

    public synchronized static SingletonPattern getInstance() {
        return singletonPattern;
    }
}

直接new一个对象传递给类的成员变量singletonPattern,你要的时候getInstance()直接返回给你,解决问题!

以上内容原书为:

《您的设计模式》 作者:CBF4LIFE

作者提到,有多种实现单例模式的方案,接下来,我来介绍着几种方案,并进行比较

饿汉式单例模式

代码语言:javascript
代码运行次数:0
运行
复制
package com.cbf4life.singleton;

/**
 * @Description: 
 * 饿汉式单例模式
 * 饿汉式单例模式中,static变量会在类装载时初始化,此时也不会涉及多个线程对象访问该对象的问题
 * 虚拟机保证只会装载一次该类,肯定不会发生并发访问的问题,因此可以省略synchronized关键字
 * 问题:如果只是加载了本类,而没有调用getInstance()方法,那么这个单例对象存在就没有意义,造成资源浪费
 */
public class SingletonPattern {

    private SingletonPattern() {
        
    }
    
    private static SingletonPattern instance = new SingletonPattern();
    
    public static SingletonPattern getInstance() {
        return instance;
    }
}

懒汉式单例模式

代码语言:javascript
代码运行次数:0
运行
复制
/**
 * @Description: 
 * 懒汉式单例模式
 * 优点:lazy load!延迟加载,懒加载!真正用的时候才加载
 * 问题:资源利用率高了,但是,每次调用getInstance()方法都要同步,并发效率较低!
 */
public class SingletonPattern {

    private SingletonPattern() {
        
    }
    
    private static SingletonPattern instance;
    
    public static synchronized SingletonPattern getInstance() {
        if(instance == null) {
            instance = new SingletonPattern();
        }
        return instance;
    }

}

双重检测锁式单例模式

代码语言:javascript
代码运行次数:0
运行
复制
package com.cbf4life.singleton;

/**
 * @Description: 
 * 双重检测锁式单例模式
 * 优点:这个模式将同步内容写到if()内部,提高了执行的效率
 * 不必每次获取对象都进行同步,只有第一次才同步,创建以后就没有必要了
 * 问题:由于编译器优化和JVM底层内部模型原因,偶尔会出现问题。不建议使用
 */
public class SingletonPattern {

    private SingletonPattern() {
        
    }
    
    private static SingletonPattern instance = null;
    
    public static SingletonPattern getInstance() {
        if(instance == null) {
            SingletonPattern sp;
            synchronized (SingletonPattern.class) {
                sp = instance;
                if(sp == null) {
                    synchronized (SingletonPattern.class) {
                        if(sp == null) {
                            sp = new SingletonPattern();
                        }
                    }
                    instance = sp;
                }
            }
        }
        return instance;
    }
}

静态内部类式单例模式

代码语言:javascript
代码运行次数:0
运行
复制
package com.cbf4life.singleton;

/**
 * @Description: 
 * 静态内部类式单例模式
 * 优点:外部类没有static属性,不会像饿汉式那样立即加载
 * 只有真正调用getInstance()方法,才会加载静态内部类,加载类时是线程安全的
 * instance是static final类型,保证了内存中只有这样一个实例存在而且只能被赋值一次
 * 从而保证了线程安全性,兼顾了并发高效调用和延迟加载的优势
 */
public class SingletonPattern {

    private SingletonPattern() {
        
    }
    
    private static SingletonPattern instance = null;
    
    private static class SingletonPatternInstance {
        private static final SingletonPattern instance = new SingletonPattern();
    }
    
    public static SingletonPattern getInstance() {
        return SingletonPatternInstance.instance;
    }
    
}

枚举单例

代码语言:javascript
代码运行次数:0
运行
复制
/**
 * @Description: 
 * 枚举单例
 * 优点:实现简单,枚举本身就是单例模式
 * 由JVM从根本上提供保障,避免发射和序列化得漏洞!
 * 问题:不能延迟加载
 */
public enum SingletonPattern {

    // 定义一个枚举的元素,它就代表了SingletonPattern的一个实例
    INSTANCE;
    
    public static void main(String[] args) {
        // 需要的时候直接调用就可以
        SingletonPattern sp = SingletonPattern7.INSTANCE;
        // 其他操作...
    }
}

几种单例模式的比较

  • 饿汉式(线程安全,调用效率高。但是,不能延时加载)。
  • 懒汉式(线程安全,调用效率不高。但是,可以延时加载)。
  • 双重检测锁式(由于JVM底层内部模型原因,偶尔会出问题,不建议使用)。
  • 静态内部类式(线程安全,调用效率高。可以延时加载)。
  • 枚举单例(线程安全,调用效率高。不能延时加载,可以天然防止反射和反序列化的漏洞)。

如何选用

单例对象占用资源少,不需要延时加载:枚举式好于饿汉式

单例对象占用资源多,需要延时加载:静态内部类式好于懒汉式

扩展

我们说到,除了枚举单例模式外,其他的模式都可以被发射和反序列化破解,那么是怎么破解的?看下面的代码

代码语言:javascript
代码运行次数:0
运行
复制
package com.jed.singleton;

import java.lang.reflect.Constructor;

/**
 * @Description: 
 * 测试反射破解单例模式
 */
public class Demo {

    private Demo() {
        
    }
    
    private static Demo instance = new Demo();
    
    public static Demo getInstance() {
        
        return instance;
    }
    
    public static void main(String[] args) throws Exception {
        Demo d1 = Demo.getInstance();
        Demo d2 = Demo.getInstance();
        System.out.println(d1 == d2);// true
        
        // 通过反射的方法破解单例模式
        Class<Demo> cla = (Class<Demo>) Class.forName("com.jed.singleton.Demo");
        // 获得无参构造器
        Constructor<Demo> constructor = cla.getDeclaredConstructor(null);
        // 设置跳过权限检查,可以访问私有成员
        constructor.setAccessible(true);
        
        Demo d3 = constructor.newInstance();
        Demo d4 = constructor.newInstance();
        System.out.println(d3 == d4);// false
    }
    
}

解决办法,改写私有的构造方法
private Demo() {
    if(instance != null) {
        throw new RuntimeException("instance存在,不允许再调用构造器!");
    }
}
代码语言:javascript
代码运行次数:0
运行
复制
package com.jed.singleton;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

/**
 * @Description: 
 * 测试反序列化破解单例模式
 * @author Jed
 * @date 2018年1月8日
 */
public class Demo implements Serializable {

    private static Demo instance ;
    
    private Demo() {
        
    }
    
    public static synchronized Demo getInstance() {
        if(instance == null) {
            instance = new Demo();
        }
        return instance;
    }
    
    public static void main(String[] args) throws Exception {
        Demo d1 = Demo.getInstance();
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("E:/object.txt"));
        out.writeObject(d1);
        out.close();
        
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("E:/object.txt"));
        Demo d2 = (Demo) in.readObject();
        System.out.println(d1 == d2);// false
        in.close();
    }
}

解决办法,在类中添加readResolve()这个方法
反序列化直接调用readResolve()返回指定的对象,不需要再创建新对象
public Object readResolve() {
    return instance;
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018.01.08 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 饿汉式单例模式
  • 懒汉式单例模式
  • 双重检测锁式单例模式
  • 静态内部类式单例模式
  • 枚举单例
  • 几种单例模式的比较
  • 如何选用
  • 扩展
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档