今天,我们在学习java时,老师给我们讲了关于单例在java中使用的两种方法。通过在网上查询资料,我对单例有了更深刻的了解。

单例模式,是一种常用的软件设计模式,是设计模式中最简单的形式之一。在他的核心结构中只包含一个被称为单例的特殊类。此模式的目的是使得类的一个对象成为系统的唯一实例。即一个类只有一个对象实例。在现实生活中有很多事物都需要用到单例模式。例如:打印机,一个系统中可以存在多个大一任务,但是只能由一个正在工作的任务。单例模式的要点有三个;一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。

单例模式的思路:首先私有化构造方法,其次对外提供一个方法可以返回该类的实例。

要实现这一点,可以从客户端对其进行实例化开始。因此需要用一种只允许生成对象类的唯一实例的机制,"阻止"所有想要生成对象的访问。使用工厂方法来限制实例化过程。这个方法应该是静态方法(类方法),因为让类的实例去生成另一个唯一实例毫无意义。

 

单例的优点:

 

    实例控制:会阻止其他对象实例化其自己的单例对象的副本,从而确保所有对象都访问唯一实例。

    灵活性:类控制了实例化过程,所以类可以灵活更改实例化过程。

 

缺点:

    开销:虽然数量很少,但如果每次对象请求引用时都要检查是否存在类的实例,将仍然需要一些开销。可以通过使用静态初始化解决此问题。

    可能的开发开发混淆:使用单例对象(尤其在类库中定义的对象)时,开发人员必须记住自己不能使用new关键字实例化对象。因为可能无法访问库源代码,因此应用程序开发人员可能会意外发现自己无法直接实例化此类。

    对象生存期:不能解决删除单个对象的问题。在提供内存管理的语言中(例如基于.NET Framework的语言),只有单例类能够导致实例被取消分配,因为它包含对该实例的私有引用。在某些语言中(如 C++),其他类可以删除对象实例,但这样会导致单例类中出现悬浮引用。

 

在java中的使用方法

第一种:最体现技术的单例---懒汉式,常用模式

 

懒汉式即实现延迟加载的单例,为上述饿汉式的优化形式。而因其仍需要进一步优化,往往成为面试考点。

 

public class Singleton {  
    private static Singleton INSTANCE;  
    private Singleton (){}  
    public static Singleton getInstance() {  
      if (INSTANCE == null) {  
         INSTANCE = new Singleton();  
 
     }  
     return INSTANCE;  
 
    }  
}

这种写法就轻松实现了单例的懒加载,只有调用了getInstance方法才会初始化。但是这样的写法出现了新的问题--线程不安全。当多个线程调用getInstance方法时,可能会创建多个实例,因此需要对其进行同步。

 

如何使其线程安全呢?简单,加个synchronized关键字就行了

 

public static synchronized Singleton getInstance() { 
    if (INSTANCE == null) {  
        INSTANCE = new Singleton();  
    }  
    return INSTANCE;  
}

可是...这样又出现了性能问题,简单粗暴的同步整个方法,导致同一时间内只有一个线程能够调用getInstance方法。

因为仅仅需要对初始化部分的代码进行同步,所以再次进行优化:

public static Singleton getSingleton() {  
    if (INSTANCE == null) { // 第一次检查  
        synchronized (Singleton.class) {  
            if (INSTANCE == null) { // 第二次检查 
                INSTANCE = new Singleton();  
            }  
        }  
    }  
    return INSTANCE ;  
}

执行两次检测很有必要:当多线程调用时,如果多个线程同时执行完了第一次检查,其中一个进入同步代码块创建了实例,后面的线程因第二次检测不会创建新实例。

这段代码看起来很完美,很可惜,它是有问题。主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。

给 instance 分配内存

调用 Singleton 的构造函数来初始化成员变量

将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)

 

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

我们只需要将 instance 变量声明成 volatile 就可以了。

public class Singleton {  
    private volatile static Singleton INSTANCE; //声明成 volatile  
    private Singleton (){}              
    public static Singleton getSingleton() { 
           if (INSTANCE == null) {         
             synchronized (Singleton.class) {  
                   if (INSTANCE == null) {        
                           INSTANCE = new Singleton();         
                   }  
                  return INSTANCE;  
              }           }    
}

 

至此,这样的懒汉式才是没有问题的懒汉式。

第二种:最简单的单例---饿汉式

public class Singleton {  
    private static final Singleton INSTANCE = new Singleton();  
    // 私有化构造函数  
    private Singleton(){}  
    public static Singleton getInstance(){  
        return INSTANCE;  
    }  
}

这种单例的写法最简单,但是缺点是一旦类被加载,单例就会初始化,没有实现懒加载。而且当实现了Serializable接口后,反序列化时单例会被破坏。

实现Serializable接口需要重写readResolve,才能保证其反序列化依旧是单例:

 

public class Singleton implements Serializable {  
    private static final Singleton INSTANCE = new Singleton();  
    // 私有化构造函数  
    private Singleton(){}  
    public static Singleton getInstance(){  
        return INSTANCE;  
    }  
    /** 
     * 如果实现了Serializable, 必须重写这个方法 
     */  
    private Object readResolve() throws ObjectStreamException {  
        return INSTANCE;  
    }  
}

OK,反序列化要注意的就是这一点。