抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

所谓单例模式,简单来说,就是在整个应用中保证只有一个类的实例存在。就像是Java Web中的application,也就是提供了一个全局变量,用处相当广泛,比如保存全局数据,实现全局性的操作等。

在单例模式的实现过程中,需要注意以下三点:

  • 单例类的构造函数为私有
  • 提供一个自身的静态私有成员变量
  • 提供一个公有的静态工厂方法

单例模式优点

  • 提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它,并为设计及开发团队提供了共享的概念。
  • 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能 。
  • 允许可变数目的实例。我们可以基于单例模式进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例 。

单例模式缺点

  • 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难 。
  • 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
  • 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致对象状态的丢失。

单例模式示例

最简单实现(饿汉)

外部使用者如果需要使用SingletonClass的实例,只能通过getInstance()方法,并且它的构造方法是private的,这样就保证了只能有一个对象存在。

缺点:

  • 资源浪费:无论这个类是否被使用,都会创建一个instance对象
public class Singleton{
    private static final Singleton singleton = new Singleton();
    public static Singleton getInstance(){
        return singleton;
    }
    private Singleton(){
    }
}

性能优化–lazy loaded(懒汉)

不推荐使用该模式

对上述的饿汉进行优化,加入懒加载的实现逻辑。在系统调用getInstance方法时才初始化instance。

缺点:

  • 非线程安全
public class SingletonClass { 
  private static SingletonClass instance = null; 

  public static SingletonClass getInstance() { 
    if(instance == null) { 
      instance = new SingletonClass(); 
    } 
    return instance; 
  }

  private SingletonClass() { 

  }
}

线程安全的懒汉模式

在讨论线程安全时,顺便分析下为什么会产生线程安全问题:

  • 线程A希望使用SingletonClass,调用getInstance()方法。因为是第一次调用,A就发现instance是null的,于是它开始创建实例,就在这个时候,CPU发生时间片切换。
  • 线程B开始执行,它要使用SingletonClass,调用getInstance()方法,同样检测到instance是null——注意,这是在A检测完之后切换的,也就是说A并没有来得及创建对象——因此B开始创建。
  • B创建完成后,切换到A继续执行,因为它已经检测完了,所以A不会再检测一遍,它会直接创建对象。这样,线程A和B各自拥有一个SingletonClass的对象——单例失败。

解决办法:加锁(synchronized )。

只要getInstance()加上同步锁,一个线程必须等待另外一个线程创建完后才能使用这个方法,这就保证了单例的唯一性。

缺点:

  • 如果存在很多类同时调用或多次调用getInstance方法,则会导致程序响应速度很慢
public class SingletonClass{
    private static SingletonClass instance = null;
    public synchronized static SingletonClass getInstance(){
        if(instance == null){
            instance = new SingletonClass();
        }
        return instance;
    }
    private SingletonClass(){

    } 
}

双重检查锁定(double–checked–locking)

在上述代码中,我们解决了懒汉模式的线程安全问题,但是在getInstance方法加锁的方法很笨重,在一定情况下会产生性能问题,所以此处我们再对上面的方式进行优化。虽然getInstance方法存在多次调用,但在单例模式中instance的初始化方法却只会调用一次。

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

    } 
}

并发编程–有序性

并发编程中,我们通常会遇到以下三个问题:

  • 原子性:一个操作或多个操作要么全部执行完成且执行过程不被中断,要么就不执行 。
  • 可见性:当多个线程同时访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值 。
  • 有序性:程序执行的顺序按照代码的先后顺序执行。

基于上诉代码不难发现其均违背了有序性

初始化Singleton和将对象地址赋给instance字段的顺序是不确定的。在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance,此时instance不是null,但取到的对象还未真正初始化,程序就会出错。

在Java里面,可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述volatile关键字)。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

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

    } 
}

枚举

使用枚举除了线程安全和防止反射调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。因此,《Effective Java》作者推荐使用的方法。

public enum Singleton{  
    instance;  
    public void whateverMethod(){}      
} 

静态内部类

SingletonClass没有static的属性,因此并不会被初始化。直到调用getInstance()的时候,会首先加载SingletonClassInstance类,这个类有一个static的SingletonClass实例,因此需要调用SingletonClass的构造方法,然后getInstance()将把这个内部类的instance返回给使用者。由于这个instance是static的,因此并不会构造多次。

由于SingletonClassInstance是私有静态内部类,所以不会被其他类知道,同样,static语义也要求不会有多个实例存在。并且,JSL规范定义,类的构造必须是原子性的,非并发的,因此不需要加同步块。同样,由于这个构造是并发的,所以getInstance()也并不需要加同步。

public class SingletonClass { 
  private static class SingletonClassInstance { 
    private static final SingletonClass instance = new SingletonClass(); 
  } 

  public static SingletonClass getInstance() { 
    return SingletonClassInstance.instance; 
  } 

  private SingletonClass() { 

  } 
}

补充:被破坏的单例模式

单例模式真的能够实现实例的唯一性吗?

答案是否定的,反射、序列化均可破坏单例模式。

反射

在第二次实例化的时候,抛出异常

public class SingletonClass { 
  private static boolean isInstanced = false;

  private static class SingletonClassInstance { 
    private static final SingletonClass instance = new SingletonClass(); 
  } 

  public static SingletonClass getInstance() { 
    return SingletonClassInstance.instance; 
  } 

  private SingletonClass() { 
        if(!instanced){
            instanced = true;
        }else{
            throw new Exception("duplicate instance create error!" + SingletonClass.class.getName());  
        }
  } 
}

序列化

查看ObjectInputStream源码可发现以下一段代码:

try {
    obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
    throw (IOException) new InvalidClassException(desc.forClass().getName(),"unable to create instance").initCause(ex);
}

序列化会通过反射调用无参数的构造方法创建一个新的对象。

防止序列化破坏单例模式

只要在Singleton类中定义readResolve就可以解决该问题:

public class SingletonClass { 
  private static class SingletonClassInstance { 
    private static final SingletonClass instance = new SingletonClass(); 
  } 

  public static SingletonClass getInstance() { 
    return SingletonClassInstance.instance; 
  } 

  private SingletonClass() { 

  } 

  private Object readResolve(){
      return SingletonClassInstance.instance;
  }
}

具体原理请查阅http://www.hollischuang.com/archives/1144


参考资料

http://www.hollischuang.com/archives/1144

https://blog.csdn.net/jlh912008548/article/details/79448124

评论