当前位置: 首页 > news >正文

Java中单例设计模式之最佳实践举例

单例模式(Singleton)是“四人组”(GoF)设计模式中的一种,归类于创建型模式。从定义上来看,它似乎是非常简单的设计模式,但是当去实现它时,却又带来了很多实现方面的担忧。单例模式的实现一直是开发者之间的一个富有争议的话题。在这里,我们将了解单例设计模式的原则,用不同的方式来实现单例模式以及一些使用上的最佳实践。

单例模式
单例模式限制了类的实例,并确保在Java虚拟机中有且仅有一个类的实例对象的存在。这个单例类必须提供一个全局的访问点来获得这个类的实例。单例模式一般用于日志类(logging),驱动程序对象,缓存以及线程池(thread pool)中。

单例设计模式也用于其它的设计模式中,如抽象工厂(Abstract Factory)模式,创建者模式(Builder),原型模式(Prototype),门面模式(Facade)等等。单例设计模式也用于核心Java类中,例如java.lang.Runtime,java.awt.Desktop.

Java中的单例模式
当我们去实现单例模式时,有各种不同的方法,但所有的方法中都不外乎以下几点。

私有构造函数,限制从其它类中来进行实例化。
同一个类的私有静态变量,它的实例对象只有唯一一个。
公有的静态方法,返回该类的实例对象,这是从外部获得这个类的实例对象的一个全局访问点。
在下面,我们将会学习单例模式实现的不同方法和它的设计与实现。
1.饿汉式初始化
2.静态块初始化
3.懒汉式初始化
4.单例模式的线程安全问题
5.Bill Pugh式单例模式实现
6.反射技术对单例模式的破坏
7.枚举单例模式
8.单例模式与序列化

饿汉式初始化
在饿汉式初始化中,单例模式的实例对象在类加载的时候就创建了,这是创建单例模式类的最简单的方法,但它有一个缺陷,就是当客户端可能不使用它的时候,实例对象还是会被创建。

下面是静态初始化单例类的实现。

EagerInitializedSingleton.java
[java] view plaincopyprint?
package com.journaldev.singleton; 
  
public class EagerInitializedSingleton { 
      
    private static final EagerInitializedSingleton instance = new EagerInitializedSingleton(); 
      
    //private constructor to avoid client applications to use constructor 
    private EagerInitializedSingleton(){} 
  
    public static EagerInitializedSingleton getInstance(){ 
        return instance; 
    } 

如果你的单例类不使用大量的资源,可以使用这种方式。但是在大多数情况下,单例类是为一些比如文件系统,数据库连接资源等而创建的。这时我们应该避免它的实例化,除非客户端调用getInstance方法。此外,这种方式的实例化不能提供任何用于异常处理的选项。

静态块初始化
静态块(Static block)的初始化与饿汉式初始化相似,不同之处在于类的实例化提供了异常处理(exception handling)的选项。

StaticBlockSingleton.java
[java] view plaincopyprint?
package com.journaldev.singleton; 
  
public class StaticBlockSingleton { 
  
    private static StaticBlockSingleton instance; 
      
    private StaticBlockSingleton(){} 
      
    //static block initialization for exception handling 
    static{ 
        try{ 
            instance = new StaticBlockSingleton(); 
        }catch(Exception e){ 
            throw new RuntimeException("Exception occured in creating singleton instance"); 
        } 
    } 
      
    public static StaticBlockSingleton getInstance(){ 
        return instance; 
    } 

前面两种方式的初始化都是在使用之前就已经创建了实例对象,并不是使用单例设计模式的最佳方式。因此在下面的部分中,我们将学习怎样创建懒汉式的单例类。

知识扩展:Java static

懒汉式初始化
懒汉式初始化实现了在全局的访问方法中去创建单例模式类的实例对象。下面是用这种方式创建单例类的代码。

LazyInitializedSingleton.java
[java] view plaincopyprint?
package com.journaldev.singleton; 
  
public class LazyInitializedSingleton { 
  
    private static LazyInitializedSingleton instance; 
      
    private LazyInitializedSingleton(){} 
      
    public static LazyInitializedSingleton getInstance(){ 
        if(instance == null){ 
            instance = new LazyInitializedSingleton(); 
        } 
        return instance; 
    } 

上面这种实现方法在单线程中工作的很好,但是在多线程的环境中,如果多个线程是在同一时间访问的话,它可能就会导致问题了。它将破坏单例模式,而且两个线程会得到不同的单例类的实例对象。在下面的部分中,我们将用不同的方式来创建线程安全的单例类。

单例模式的线程安全问题
创建一个线程安全的单例类的一个更简单的方法是使用全局同步访问方法,以便每次只有一个线程可以执行这个方法,一般这种方法的实现如下类所示。

ThreadSafeSingleton.java
[java] view plaincopyprint?
package com.journaldev.singleton; 
  
public class ThreadSafeSingleton { 
  
    private static ThreadSafeSingleton instance; 
      
    private ThreadSafeSingleton(){} 
      
    public static synchronized ThreadSafeSingleton getInstance(){ 
        if(instance == null){ 
            instance = new ThreadSafeSingleton(); 
        } 
        return instance; 
    } 
      

上面这种方式工作的很好而且是线程安全的,但是它也由于同步方法的成本而降低了程序的性能,尽管我们只需要它在少数几个线程中可以创建单独的实例。为了避免每次都产生这种额外的开销,我们使用双重检测锁(double checked locking)原则。在这种方法中,同步块放在if条件中进行检查,以确保只有唯一一个单例类的实例对象被创建。

下面的代码片段提供了双重检测锁原则的实现。

[java] view plaincopyprint?
public static ThreadSafeSingleton getInstanceUsingDoubleLocking(){ 
    if(instance == null){ 
        synchronized (ThreadSafeSingleton.class) { 
            if(instance == null){ 
                instance = new ThreadSafeSingleton(); 
            } 
        } 
    } 
    return instance; 

知识扩展:Thread Safe Singleton Class

Bill Pugh式单例模式实现
在Java5之前,Java的内存模式有很多的问题,当多个线程同时去获得单例类的实例对象时,在某些情形下,以上那些方法的访问可能都会失败。因此Bill Pugh想出了一个不同的方法来创建单例类,即通过使用静态内部类。它的实现方式如下:

BillPughSingletong.java

[java] view plaincopyprint?
package com.journaldev.singleton; 
  
public class BillPughSingleton { 
  
    private BillPughSingleton(){} 
      
    private static class SingletonHelper{ 
        private static final BillPughSingleton INSTANCE = new BillPughSingleton(); 
    } 
      
    public static BillPughSingleton getInstance(){ 
        return SingletonHelper.INSTANCE; 
    } 

注意是在私有静态内部类中包含了单例类的实例对象。但单例类被加载时,SingletonHelper类并没有被加载到内存中去,仅仅当有人去调用getInstance方法时,这个类才被加载,单例类对象才被创建。
这种单例模式类创建的方法使用的最广泛,因为它不需要额外的同步锁开销,而且也很容易理解和实现。

知识扩展:Java Nested Classes

反射技术对单例模式的破坏
反射可以用来摧毁上面所有的单例实现方法,让我们来看看下面这个例子吧。

ReflectionSingletonTest.java


[java] view plaincopyprint?
package com.journaldev.singleton; 
 
import java.lang.reflect.Constructor; 
public class ReflectionSingletonTest { 
  
    public static void main(String[] args) { 
        EagerInitializedSingleton instanceOne = EagerInitializedSingleton.getInstance(); 
        EagerInitializedSingleton instanceTwo = null; 
        try { 
            Constructor[] constructors = EagerInitializedSingleton.class.getDeclaredConstructors(); 
            for (Constructor constructor : constructors) { 
                //Below code will destroy the singleton pattern 
                constructor.setAccessible(true); 
                instanceTwo = (EagerInitializedSingleton) constructor.newInstance(); 
                break; 
            } 
        } catch (Exception e) { 
            e.printStackTrace(); 
        } 
        System.out.println(instanceOne.hashCode()); 
        System.out.println(instanceTwo.hashCode()); 
    } 
  

当你运行这个测试类的时候,你会发现两个实例对象的哈希值不一样,可见单例模式已经被破坏了。反射是一种很强大,并且在框架中如Spring和Hibernate中使用的很多的技术。更多请查阅Java Reflection Tutorial.

枚举单例模式
为了克服上面反射的那种情况,Joshua Bloch建议使用枚举来实现单例设计模式,因为Java能确保任何枚举值在Java程序中仅被实例化一次。由于Java的枚举(Java Enum)是全局访问的,所以在单例模式中,使用起来可能不太灵活,比如,它不支持懒汉式初始化。

EnumSingleton.java

[java] view plaincopyprint?
package com.journaldev.singleton; 
  
public enum EnumSingleton { 
  
    INSTANCE; 
      
    public static void doSomething(){ 
        //do something 
    } 

知识扩展:Java Enum

单例模式与序列化
有时在分布式重生之大文豪系统中,我们需要在单例类中实现序列化的接口,以便我们能将它的状态储存在文件系统中,而在以后的某个时间点,我们又能将其从文件系统中恢复进行使用。下面是一个很小的实现了序列化接口的单例类。

SerializedSingleton.java


[java] view plaincopyprint?
package com.journaldev.singleton; 
  
import java.io.Serializable; 
  
public class SerializedSingleton implements Serializable{ 
  
    private static final long serialVersionUID = -7604766932017737115L; 
  
    private SerializedSingleton(){} 
      
    private static class SingletonHelper{ 
        private static final SerializedSingleton instance = new SerializedSingleton(); 
    } 
      
    public static SerializedSingleton getInstance(){ 
        return SingletonHelper.instance; 
    } 
      

这种序列化单例类的一个问题就是当我们反序列化它的时候,它会创建一个新的实例对象。看下面这段简单的程序。

SingletonSerializedTest.java


[java] view plaincopyprint?
package com.journaldev.singleton; 
  
import java.io.FileInputStream; 
import java.io.FileNotFoundException; 
import java.io.FileOutputStream; 
import java.io.IOException; 
import java.io.ObjectInput; 
import java.io.ObjectInputStream; 
import java.io.ObjectOutput; 
import java.io.ObjectOutputStream; 
  
public class SingletonSerializedTest { 
  
    public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException { 
        SerializedSingleton instanceOne = SerializedSingleton.getInstance(); 
        ObjectOutput out = new ObjectOutputStream(new FileOutputStream( 
                "filename.ser")); 
        out.writeObject(instanceOne); 
        out.close(); 
          
        //deserailize from file to object 
        ObjectInput in = new ObjectInputStream(new FileInputStream( 
                "filename.ser")); 
        SerializedSingleton instanceTwo = (SerializedSingleton) in.readObject(); 
        in.close(); 
          
        System.out.println("instanceOne hashCode="+instanceOne.hashCode()); 
        System.out.println("instanceTwo hashCode="+instanceTwo.hashCode()); 
          
    } 
  

上面程序的输出结果为:

[java] view plaincopyprint?
instanceOne hashCode=2011117821 
instanceTwo hashCode=109647522 
所以它破坏了单例模式,为了克服这个问题,我们只需要提供一个实现了的readResolve()方法。
[java] view plaincopyprint?
protected Object readResolve() { 
    return getInstance(); 

这样之后,你会发现在测试代码中两个实例对象的哈希值是一样的啦。
知识扩展:Java Serialization and  Java Deserialization.

希望这篇文章能够帮助你抓住单例设计模式的要点。

转载于:https://www.cnblogs.com/jiangye/p/3505990.html

相关文章:

  • Redkale 入门教程 01 -- Hello Word!
  • iOS sqlite 使用事务操作数据库
  • 【队列】【P2827】【NOIP2016D2T3】蚯蚓
  • java中Xml、json之间的相互转换
  • 新概念书店无非内容电商线下变体,西西弗终难逃被资本吞并命运?
  • android应用activity中调出输入法后界面调整问题的解决
  • watch深度监测
  • PHP-学习大规模高并发Web系统架构及开发推荐书籍
  • [caffe(二)]Python加载训练caffe模型并进行测试1
  • 【转】ini载入保存类,操作INI配置文件方便的很
  • PostgreSQL 连接的问题
  • 珍爱之礼 美妙感受
  • Python Flask-Mail环境变量配置
  • 内表生成XML简单实例
  • nginx基础
  • “大数据应用场景”之隔壁老王(连载四)
  • 【JavaScript】通过闭包创建具有私有属性的实例对象
  • Apache Pulsar 2.1 重磅发布
  • Fabric架构演变之路
  • Git的一些常用操作
  • Javascript基础之Array数组API
  • Java知识点总结(JavaIO-打印流)
  • Node项目之评分系统(二)- 数据库设计
  • PhantomJS 安装
  • python 学习笔记 - Queue Pipes,进程间通讯
  • Spring Cloud(3) - 服务治理: Spring Cloud Eureka
  • 从零开始学习部署
  • 简单易用的leetcode开发测试工具(npm)
  • 如何编写一个可升级的智能合约
  • 责任链模式的两种实现
  • Play Store发现SimBad恶意软件,1.5亿Android用户成受害者 ...
  • Semaphore
  • ​香农与信息论三大定律
  • (10)Linux冯诺依曼结构操作系统的再次理解
  • (力扣记录)235. 二叉搜索树的最近公共祖先
  • (十五)使用Nexus创建Maven私服
  • (五)Python 垃圾回收机制
  • .NET 4 并行(多核)“.NET研究”编程系列之二 从Task开始
  • .net core webapi Startup 注入ConfigurePrimaryHttpMessageHandler
  • .NET Core 将实体类转换为 SQL(ORM 映射)
  • .NET Framework 和 .NET Core 在默认情况下垃圾回收(GC)机制的不同(局部变量部分)
  • .NET/C# 中你可以在代码中写多个 Main 函数,然后按需要随时切换
  • .net6Api后台+uniapp导出Excel
  • .net打印*三角形
  • .pyc文件是什么?
  • /usr/bin/perl:bad interpreter:No such file or directory 的解决办法
  • :“Failed to access IIS metabase”解决方法
  • @拔赤:Web前端开发十日谈
  • []串口通信 零星笔记
  • [2017][note]基于空间交叉相位调制的两个连续波在few layer铋Bi中的全光switch——
  • [C++数据结构](31)哈夫曼树,哈夫曼编码与解码
  • [IE6 only]关于Flash/Flex,返回数据产生流错误Error #2032的解决方式
  • [InnoDB系列] -- SHOW INNODB STATUS 探秘
  • [Leetcode] 寻找数组的中心索引
  • [luoguP3159] [CQOI2012]交换棋子(最小费用最大流)