今天,我们在学习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,反序列化要注意的就是这一点。