单例模式
今天来总结一下程序员非常熟悉的singledog模式,哦不,是singleton(单例)模式。单例模式可以让程序猿孤独终老,专心写代码,因为在单例模式中,程序员不能随意new对象。
言归正传,单例模式属于创建型模式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。显著的特征是构造方法私有,避免其它类实例化它的对象,在同一个虚拟机内,单例模式的实例只能通过它提供的getInstance()方法访问。
简单来说就是要符合以下三点:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
优点:解决一个全局使用的类频繁地创建与销毁。在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例,避免对资源的多重占用。
缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
单例模式主要有三种:懒汉模式、饿汉模式和登记式,主要关注使用较多的懒汉模式和饿汉模式。这两者的主要区别:饿汉模式在类初始化时,已自行实例化;懒汉模式在第一次调用的时候实例化。
- 饿汉模式
按照单例模式的特征,构造方法私有,避免其它类实例化它的对象;单例类必须创建自己的唯一实例,并提供getInstance()方法给其它类访问该实例。
public class Singleton1 {
private Singleton1 instance = new Singleton1();
private Singleton1(){}
public Singleton1 getInstance(){
return instance;
}
}
这种模式优点是线程安全,缺点是类加载的时候已经实例化对象,造成资源浪费。怎么去优化呢?懒汉模式应运而生。
- 懒汉模式V1.0
为避免类加载就实例化对象,把实例对象放进getInstance()方法内,而且是只有满足该实例对象还是空的时候才实例化。
public class LazySingleton1 {
private LazySingleton1 instance;
private LazySingleton1 () {}
public LazySingleton1 getInstance(){
if(instance == null){
instance = new LazySingleton1();
}
return instance;
}
}
这种方式避免了饿汉式类加载的时候就已实例化造成资源浪费的缺点,但同时也丧失了线程的安全性。如果不同的线程都获取到了instance=null,则不同的线程都会实例对象,这会破坏单例模式。
关于解决线程不安全的问题,很容易联想到加锁。即以下的V1.1。
V1.1
public class LazySingleton2 {
private LazySingleton2 instance;
private LazySingleton2 () {}
public synchronized LazySingleton2 getInstance(){
if(instance == null){
instance = new LazySingleton2();
}
return instance;
}
}
给方法getInstance()加上synchronize修饰符之后,解决了线程安全的问题,但随之而来的是性能上的大打折扣。因为不同的线程运行到getInstance()方法的时候,都需要排队等候。
鉴于只有instance==null条件成立才需要进行实例化,可以考虑把锁加在判断后的语句块,这样一来运行到getInstance方法后,先进行判断,如果条件不满足,就可以直接执行后续代码,无需等候进入同步代码块。这就是双重校验锁的单例模式,即下面的V1.2版本。
V1.2
public class LazySingleton3 {
private LazySingleton3 instance;
private LazySingleton3 () {}
public LazySingleton3 getInstance(){
if(instance == null){
/*如果你知道一个实例,那么你可以通过实例的“getClass()”方法获得该对象的类型类,
如果你知道一个类型,那么你可以使用“.class”的方法获得该类型的类型类。*/
synchronized (LazySingleton3.class){
if(instance == null){
instance = new LazySingleton3();
}
}
}
return instance;
}
}
双重校验锁的模式,既保证了线程安全,又提高了性能。其实这个方法还是有实例出不止一个实例的几率,但是有双重判断,几率很低。
3.Effective Java中提到的实现方式:
3.1使用内部类:利用了ClassLoader来保证了同步,同时又能让开发者控制类加载的时机。从内部看是一个饿汉式的单例,但是从外部看来,又的确是懒汉式的实现。
public class LazySingletonV2 {
private LazySingletonV2(){
}
static class SingletonHolder{
private static final LazySingletonV2 instance = new LazySingletonV2();
}
public static LazySingletonV2 getInstance(){
return SingletonHolder.instance;
}
}
这种写法从内部类来看是一个饿汉模式,在SignletonHolder初始化的时候,会由类加载器来保证同步,使得instance是一个真实例。同时,由于SingletonHolder是一个内部类,只在外部类的Singleton的getInstance()中被使用,所以它被加载的时机也就是在getInstance()方法第一次被调用的时候。
3.2使用枚举的方式:利用创建枚举的实例是线程安全的,避免同步问题。
public enum SingletonEnum {
INSTANCE;
public void functon(){
//do someting
}
}
这种写法在功能上与共有域方法相近,但是它更简洁,无偿地提供了序列化机制,绝对防止对此实例化,即使是在面对复杂的序列化或者反射攻击的时候。虽然这中方法还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。以上的作者对这种实现方式的评价。
文章的最后来解决历史遗留问题,上一篇文章提到利用反射可以破坏单例模式。首先使用getDeclaredConstructor(Class... parameterTypes):获取"某个构造方法"可以是私有的,或受保护、默认、公有; 返回Constructor。然后调用setAccessible(true)方法使得私有的方法也可以调用。
其实也是可以通过异常来实现防止反射破坏单例模式的,话不多说,上代码
public class LazySingletonV3 {
private static boolean initialized = false;
private LazySingletonV3(){
synchronized (LazySingletonV3.class){
if(initialized == false){
initialized = !initialized;
} else {
throw new RuntimeException("单例已被破坏!");
}
}
}
static class SingletonHolder {
private static final LazySingletonV3 instance = new LazySingletonV3();
}
public static LazySingletonV3 getInstance(){
return SingletonHolder.instance;
}
}
本文参考博文:https://www.jianshu.com/p/eb30a388c5fc