java设计模式之单例模式

Posted J_Newbie

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java设计模式之单例模式相关的知识,希望对你有一定的参考价值。

定义:

单例模式指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点,属于创建型设计模式

应用场景:

  1. 需要频繁的创建一些类,使用单例模式可以降低系统的内存压力,减少GC

  2. 某些类创建实例时占用资源比较多,或实例化耗时较长,且经常使用。

  3. 频繁访问数据库或者文件的对象

  4. 对于一些控制硬件级别的操作,或者从系统上来讲应当是单一控制逻辑的操作,如果有多个实例,则系统会完全乱套

UML类图:

通用写法:

public class Client
  public static void main(String[] args)
    Singleton.getInstance();
  
  static class Singleton()
    private static final Singleton instance = new Singleton();
    
    private Singleton();
    
    public static Singleton getInstance()
      return instance;
    
  

使用单例模式解决实际问题:

饿汉式单例写法的弊端: 

上面的通用型写法就是标准的饿汉式写法。饿汉式单例写法在类加载的时候立即初始化,并且创建单例对象。他绝对线程安全,在线程还没有出现之前就实例化了,不可能存在访问安全问题。饿汉式还有一种写法,代码如下:

//饿汉式静态块单例模式
public class Singleton
  private static final Singleton singleton;
  static
    singleton = new Singleton();
  
  private Singleton();
  
  public static Singleton getInstance()
    return singleton;
  

这种写法的静态块机制,非常简单也容易理解。饿汉式单例写法适用于单例对象比较少的情况,这样写可以保证绝对的线程安全。执行效率比较高。但是它的缺点明显,就是所有对象都会在类加载的时候创建实例化,这样一来,系统中就会有大量的实例化单例对象存在,而且单例对象的数量也不确定,则系统初始化时会造成大量的内存浪费,从而导致系统内存不可控。也就是说,不管对象用不用,都会占用空间,浪费了内存。

懒汉式写法:

//懒汉式单例在外部需要使用的时候才会进行实例化
public class LazySingleton
  private LazySingleton()
  //静态块,公共内存区域
  private static LazySingleton lazySingleton = null;
  
  public static LazySingleton getInstance()
    if(lazySingleton == null)
      lazySingleton = new LazySingleton();
    
    return lazySingleton;
  

但这样写又带来一个新的问题,如果在多线程环境下,则会出现线程安全问题。我们来模拟一下,编写线程类ExectorThread

public class ExectorThread implements Runnable
  @Override
  public void run()
    LazySingleton lazySingleton = LazySingleton.getInstance();
    System.out.println(Thread.currentThread().getName() + ":" + lazySingleton);
  

客户端测试代码:

public class LazySingletonTest
  public static void main(String[] args)
    Thread t1 = new Thread(new ExectorThread());
    Thread t2 = new Thread(new ExectorThread());
    t1.start();
    t2.start();
    System.out.println("End");
  

运行多次出现的结果:

End
Thread-0:com.design.pattern.singleton.LazySingleton@92734f7
Thread-1:com.design.pattern.singleton.LazySingleton@6a2ff6fb

这意味着上面的单例模式存在着线程安全的隐患。那么结果是怎么产生的。我们分析一下:

如果两个线程在同一时间进入getInstance()方法,则会同时满足if(lazySingleton == null)的条件,创建两个对象。如果两个线程继续执行往下代码,则又可能后执行线程的结果覆盖先执行的线程的结果,如果打印发生在覆盖之前,则最终的结果就是一致的。如果发生在打印发生在覆盖之后,就会得到两个不同的结果。

当然也有可能没有发生并发,正常运行。

如何优化代码,使得懒汉式单例写法在线程环境下是安全的呢?来看下面代码,给getInstance()方法加上synchronized关键字,使这个方法变成线程同步方法。

public class LazySingleton
  private LazySingleton()
  //静态块,公共内存区域
  private static LazySingleton lazySingleton = null;
  
  public synchronized static LazySingleton getInstance()
    if(lazySingleton == null)
      lazySingleton = new LazySingleton();
    
    return lazySingleton;
  

这样就解决了线程安全的问题。

双重检查锁单例写法:

上面通过给方法加synchronized的方法解决线程问题,但是在线程数据剧增的情况下,用synchronized加锁,则会导致程序性能的大幅下降。所以引入了双重检查的方式

public class Singleton
  private volatile static Singleton instance;
  private Singleton();
  public static Singleton getInstance()
    synchronized(Singleton.class)
      if(instance == null)
        instance = new Singleton();
      
    
    return instance;
  

这样的写法其实与在方法上加synchronized并无差异,还是会造成大规模的阻塞。我们把判断条件往上提一级

public class LazyDoubleCheckSingleton
  private volatile static LazyDoubleCheckSingleton instance;
  private LazyDoubleCheckSingleton()
  public static LazyDoubleCheckSingleton getInstance()
    if(instance == null)
      synchronized(LazyDoubleCheckSingleton.class)
        instance = new LazyDoubleCheckSingleton();
      
    
    return instance;
  

运行代码后,还是会存在线程安全问题。这是为什么呐,其实如果两个线程同一时间都满足if(instance == null)条件,则两个线程都会执行 synchronized块中的代码,因此,还是会创建两次,再优化一下代码

public class LazyDoubleCheckSingleton
  private volatile static LazyDoubleCheckSingleton instance;
  private LazyDoubleCheckSingleton()
  public static LazyDoubleCheckSingleton getInstance()
    //检查是否要阻塞
    if(instance == null)
      synchronized(LazyDoubleCheckSingleton.class)
        //检查是否要创建实例
        if(instance == null)
          instance = new LazyDoubleCheckSingleton();
          //指令重排序的问题
        
      
    
    return instance;
  

当第一个线程调用getInstance()方法时,第二个线程也可以调用,当第一个线程执行到synchronized时会上锁。第二个线程就会变成MONITOR状态,出现阻塞。此时,阻塞并不是基于整个LazyDoubleCheckSingleton类的阻塞,而是在getInstance()方法内部的阻塞,只要逻辑不太复杂,对于调用者而言感觉不到。

双重检查锁单例写法虽然解决了线程安全的问题和性能问题,但是只要用到synchronized关键字就总是要上锁,对程序的性能还是存在一定的影响。我们从类初始化的角度考虑下面代码,采用静态内部类的方式:

//这种形式兼顾了饿汉式单例写法的内存浪给问题和synchronized的性能问题
//完美的屏蔽了这两个缺点
public class LazyStaticInnerClassSingleton
  //使用LazyInnerClassGeneral的时候,默认会初始化内部类
  //如果没使用,则内部类是不加载的
  private LazyStaticInnerClassSingleton();
  //每一个关键字都不是多余的,static是为了使单例模式的空间共享,保证这个写法不会被重写,重载
  private static LazyStaticInnerClassSingleton getInstance()
    //返回结果前,一定会先加载内部类
    return LazyHolder.INSTANCE;
  
  //利用了java本身的语法特点,默认不加载内部类
  private static class LazyHolder
    private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
  

这种方式兼顾了饿汉式单例写法的内存浪费问题和synchronized的性能问题,内部类一定要在方法调用之前被初始化,巧妙的避免了线程安全问题。

还原反射破坏单例模式的事故现场:

上面的单例模式的构造方法除了加上private关键字,没有坐任何处理,我们如果用反射调用起构造方法,在调用getInstance方法,应该会有两个不同的实例。客户端代码如下:以LazyStaticInnerClassSingleton为例:

public static void main(String[] args)
  try
    Class<?> clazz = LazyStaticInnerClassSingleton.class;
    Constructor c = clazz.getDeclaredConstructor(null);
    c.setAccessible(true);
    Object o1 = c.newInstance();
    Object o2 = c.newInstance();
    //调用了两次构造方法,相当于new了两次,犯了原则性错误
    System.out.println(o1 == o2);
  catch(Exception e)
    e.printStackTrace();
  

运行结果

false

我们做一次优化,我们在其构造方法中做一些限制,一旦出现多次重复创建,则直接抛出异常,优化后代码如下:

//这种形式兼顾了饿汉式单例写法的内存浪给问题和synchronized的性能问题
//完美的屏蔽了这两个缺点
public class LazyStaticInnerClassSingleton
  //使用LazyInnerClassGeneral的时候,默认会初始化内部类
  //如果没使用,则内部类是不加载的
  private LazyStaticInnerClassSingleton()
    if(LazyHolder.INSTANCE != null)
      throw new RuntimeException("不允许创建多个实例");
    
  ;
  //每一个关键字都不是多余的,static是为了使单例模式的空间共享,保证这个写法不会被重写,重载
  private static LazyStaticInnerClassSingleton getInstance()
    //返回结果前,一定会先加载内部类
    return LazyHolder.INSTANCE;
  
  //利用了java本身的语法特点,默认不加载内部类
  private static class LazyHolder
    private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
  

测试结果如下:

java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at com.design.pattern.singleton.Test.main(Test.java:11)
Caused by: java.lang.RuntimeException: 不允许创建多个实例
	at com.design.pattern.singleton.LazyStaticInnerClassSingleton.<init>(LazyStaticInnerClassSingleton.java:10)
	... 5 more

但是会抛出异常,不够优雅。

更加优雅的枚举式单例写法:

枚举式单例的标准写法,创建EnumSingleton类:

public enum EnumSingleton
  INSTANCE;
  private Object data;
  public Object getData()
    return data;
  
  public void setData(Object data)
    this.data = data;
  
  public static EnumSingleton getInstance()
    return INSTANCE;
  

客户端测试代码:

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

public class TestEnumSingleton
  public static void main(String[] args)
    try
      EnumSingleton instance1 = null;
      EnumSingleton instance2 = EnumSingleton.getInstance();
      instance2.setData(new Object());

      FileOutputStream fos = new FileOutputStream("EnumSingleton.obj");
      ObjectOutputStream oos = new ObjectOutputStream(fos);
      oos.writeObject(instance2);
      oos.flush();
      oos.close();

      FileInputStream fis = new FileInputStream("EnumSingleton.obj");
      ObjectInputStream ois = new ObjectInputStream(fis);
      instance1= (EnumSingleton) ois.readObject();
      ois.close();
      System.out.println(instance1.getData());
      System.out.println(instance2.getData());
      System.out.println(instance1.getData() == instance2.getData());
    catch(Exception e)
      e.printStackTrace();
    
  

测试结果:

java.lang.Object@6acbcfc0
java.lang.Object@6acbcfc0
true

还原反序列化破坏单例模式的事故现场:

一个单例对象创建好后,有时候需要讲对象序列化然后写入磁盘,当下次使用时再从磁盘中读取对象并进行反序列化,将其转化为内存对象。反序列化的对象会重新分配内存,即重新创建。如果序列化的目标为单例对象,则违背了单例模式的初衷,相当于破坏了单例模式:如下代码“

//反序列化破坏了单例模式
public class SeriableSingleton implements Serializable
  //序列化就是把内存中的状态通过转换成字节码的形式
  //从而转换成一个I/O流,写入其他的地方
  //内存中的状态会被永久保存下来
  
  //反序列化就是将已经持久化的字节码内容转换为I/O流
  //通过I/O流的读取,进而将读取的内容转换成为Java对象
  //在转换过程中会重新创建对象
  public final static SeriableSingleton INSTANCE = new SeriableSingleton();
  private SeriableSingleton()
  
  public static SeriableSingleton getInstance()
    return INSTANCE;
  

客户端测试代码:

package com.design.pattern.singleton;

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

public class SeriableSingletonTest 
    public static void main(String[] args) 
        SeriableSingleton s1 = null;
        SeriableSingleton s2 = SeriableSingleton.getInstance();

        FileOutputStream fos = null;
        try 
            fos = new FileOutputStream("SeriableSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();

            FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (SeriableSingleton) ois.readObject();

            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1 == s2);
        catch (Exception e)
            e.printStackTrace();
        
    

测试结果:

com.design.pattern.singleton.SeriableSingleton@7cd84586
com.design.pattern.singleton.SeriableSingleton@5a07e868
false

从运行结果可以知道,反序列化后的对象和手动创建的对象是不一致的,被实例化了两次,违背了单例模式的设计初衷。要想解决,其实增加一个readResolve()方法即可:代码如下:

//反序列化破坏了单例模式
public class SeriableSingleton implements Serializable
  //序列化就是把内存中的状态通过转换成字节码的形式
  //从而转换成一个I/O流,写入其他的地方
  //内存中的状态会被永久保存下来
  
  //反序列化就是将已经持久化的字节码内容转换为I/O流
  //通过I/O流的读取,进而将读取的内容转换成为Java对象
  //在转换过程中会重新创建对象
  public final static SeriableSingleton INSTANCE = new SeriableSingleton();
  private SeriableSingleton()
  
  public static SeriableSingleton getInstance()
    return INSTANCE;
  
  private Object readResolve()
    return INSTANCE;
  

结果如下:

com.design.pattern.singleton.SeriableSingleton@5a07e868
com.design.pattern.singleton.SeriableSingleton@5a07e868
true

从JDK源码来看,虽然增加readResolve()方法返回实例解决了单例模式被破坏的问题,但实际上单例对象被实例化了两次,只不过新创建的对象没有返回而已。

容器式单例写法:

public class ContainerSingleton
  private ContainerSingleton()
  private static Map<String,Object> ioc = new ConcurrentHashMap<String,Object>();
 	public static Object getBean(String className)
    synchronized(ioc)
      if(!ioc.containsKey(className))
        Object obj = null;
        try
          obj = Class.forName(className).newInstance();
        catch(Exception e)
          e.printStackTrace();
        
        return obj;
      else
        return ioc.get(className);
      
    
  

容器式单例写法适用于需要大量创建单例对象的场景,便于管理,但它是非线程安全的。

ThreadLocal单例详解

线程单例实现ThreadLocal,不能保证其创建的对象是全局唯一的,但能保证在单个线程中是唯一的,是线程安全的。看下面代码:

package com.design.pattern.singleton;

public class ThreadLocalSingleton 
    private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance =
            new ThreadLocal<ThreadLocalSingleton>()
                @Override
                protected ThreadLocalSingleton initialValue() 
                    return new ThreadLocalSingleton();
                
            ;
    private ThreadLocalSingleton();
    
    public static ThreadLocalSingleton getInstance()
        return threadLocalInstance.get();
    

测试代码:

package com.design.pattern.singleton;

public class ThreadLocalSingletonTest 
    public static void main(String[] args) 
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println("-----------------");
        Thread t1 = new Thread(new ExectorThread());
        Thread t2 = new Thread(new ExectorThread());
        t1.start();
        t2.start();
    

结果如下:

com.design.pattern.singleton.ThreadLocalSingleton@135fbaa4
com.design.pattern.singleton.ThreadLocalSingleton@135fbaa4
com.design.pattern.singleton.ThreadLocalSingleton@135fbaa4
com.design.pattern.singleton.ThreadLocalSingleton@135fbaa4
com.design.pattern.singleton.ThreadLocalSingleton@135fbaa4
-----------------
Thread-1:com.design.pattern.singleton.LazySingleton@33e329ce
Thread-0:com.design.pattern.singleton.LazySingleton@33e329ce

由测试可以知道,在主线程中无论调用多少次,获取的实例都是同一个,都在两个字线程中分别获取了不同的实例。我们知道单例模式为了达到线程安全的目的,会给方法上锁,以时间换空间。ThreadLocal将多有的对象放在ThreadLocalMap中,为每个线程都提供一个对象,实际上是以空间换时间来实现线程隔离的。

单例的优点:

  1. 单例模式可以保证内存里只有一个实例,减少了内存的开销

  2. 可以避免对资源的多重占用

  3. 单例模式设置全局访问点,可以优化和共享资源

单例模式缺点:

  1. 单例模式一般没有接口,扩展困难,如果要扩展,则出了修改原来的代码,没有第二种途径,违背开闭原则

  2. 在并发测试中,单例模式不利于代码的调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。

  3. 单例模式的功能代码通常写在一个类中,如果功能设计的不合理,则很容易违背单一职责原则

以上是关于java设计模式之单例模式的主要内容,如果未能解决你的问题,请参考以下文章

JAVA设计模式之单例模式

JAVA设计模式之单例模式(转)

JAVA设计模式之单例模式

java设计模式之单例模式

Java设计模式之单例模式

《JAVA与模式》之单例模式