“这是我参与8月更文挑战的第14天,活动详情查看:8月更文挑战”
上一篇:设计模式-工厂模式学习之旅
一、单例模式定义
单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式是创建型模式。单例模式在现实生活中应用也非常广泛,例如,公司CEO、部门经理等。J2EE标准中的ServletContext、ServletContextConfig等,Spring框架应用中的ApplicationContext、数据库的连接池等也都是单例模式。
二、饿汉式单例模式
饿汉式单例模式在类加载的时候就立即初始化,并且创建单例对象。它绝对线程安全,在线程还没出现以前就实例化了,不可能存在访问安全问题。
接下来我们看下饿汉式单例的标准代码:
public class HungrySingleton {
/**
* 加载机制:
* 先静态,后动态
* 先属性,后方法
* 从上而下
*/
private static final HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return HUNGRY_SINGLETON;
}
}
还有另外一种写法,利用静态代码块的机制:
public class HungryStaticSingleton {
private static final HungryStaticSingleton HUNGRY_STATIC_SINGLETON;
static {
HUNGRY_STATIC_SINGLETON = new HungryStaticSingleton();
}
private HungryStaticSingleton() {
}
public static HungryStaticSingleton getInstance() {
return HUNGRY_STATIC_SINGLETON;
}
}
这两种写法都非常简单,也非常好理解,饿汉式单例模式适用于单例对象较少的情况。这样写可以保证绝对线程安全、执行效率比较高。但是它的缺点也很明显,就是所有对象类加载的时候就实例化。这样一来,如果系统中有大批量的单例对象存在,那系统初始化时就会导致大量的内存浪费。也就是说,不管对象用与不用都占着空间,浪费了内存,有可能 “站着茅坑不拉屎”。
三、懒汉式单例模式
为了解决饿汉式单例可能带来的内存浪费问题,于是就出现了懒汉式单例的写法,懒汉式单例模式的特点是,单例对象是在被使用时才会初始化,下面看懒汉式单例模式的简单实现。
public class LazySimpleSingleton {
private LazySimpleSingleton() {
}
private static LazySimpleSingleton instance;
public static LazySimpleSingleton getInstance() {
if (instance == null) {
instance = new LazySimpleSingleton();
}
return instance;
}
}
但这样写又带来一个新的问题,如果在多线程环境下,就会出现线程安全问题(不唯一)。简单模拟下:
public class LazySimpleSingletonTest {
@Test
public void test() {
new Thread(new MyThread()).start();
new Thread(new MyThread()).start();
System.out.println("end");
}
class MyThread implements Runnable {
@Override
public void run() {
LazySimpleSingleton singleton = LazySimpleSingleton.getInstance();
System.out.println(Thread.currentThread().getName() + ": " + singleton);
}
}
}
可以利用idea的线程调试模式,干预多线程的执行流程:
那么,我们如何来优化代码,使得如何来优化代码,使得懒汉式单例模式在多线程环境下安全昵?来看下面的代码,给getInstance()方法上加synchronized关键字,使这个方法变成线程同步方法:
public class LazySimpleSingleton {
private LazySimpleSingleton() {
}
private static LazySimpleSingleton instance;
public static synchronized LazySimpleSingleton getInstance() {
if (instance == null) {
instance = new LazySimpleSingleton();
}
return instance;
}
}
线程安全的问题解决了。但是,用synchronized加锁时,在线程数量比较多的情况下,则会导致大批线程阻塞,从而导致程序性能大幅下降。那么,有没有一种更好的方式,既能兼顾线程安全又能提升程序性能昵?答案是肯定的,我们来看双重检查锁(DCL)
的单例模式:
public class LazyDoubleCheckSingleton {
private LazyDoubleCheckSingleton() {
}
//使用volatile禁止指令重排序
private static volatile LazyDoubleCheckSingleton instance;
public static LazyDoubleCheckSingleton getInstance() {
if (instance == null) {
synchronized (LazyDoubleCheckSingleton.class) {
if (instance == null) {
instance = new LazyDoubleCheckSingleton();
/**
* 1。分配内存给这个对象
* 2。初始化对象
* 3。设置lazy指向刚分配的内存地址
* 4。初始化访问对象
*/
}
}
}
return instance;
}
}
当第一个线程调用getInstance()方法时,第二个线程也可以调用。当第一个线程执行到synchronized时会上锁,第二个线程就会出现阻塞。此时阻塞是在getInstance()方法内部的阻塞,只要逻辑不太复杂,对于调用者而言感知不到。
DCL单例模式在好多框架源码里都被使用过,看过源码的同学知道哈!!!
但是,用到synchronized关键字总归要上锁,对程序性能还是存在一定影响的。难道就真的没有更好的方案妈?当然后,我们可以从类初始化的角度来考虑,采用静态内部类的方式。
四、静态内部类单例模式
/**
* 这种形式兼顾饿汉式单例模式的内存浪费问题和懒汉式synchronized加锁的性能问题,完美的屏蔽了这两个缺点
*/
public class LazyInnerClassSingleton {
/**
* 使用LazyInnerClassSingleton的时候,默认会初始化内部类LazyHolder,如果没使用,则内部类是不加载的
*/
private LazyInnerClassSingleton() {
}
/**
* 每一个关键字都不是多余的,static是为了使单例的空间共享,final保证这个方法不会被覆盖,重载
*/
public static final LazyInnerClassSingleton getInstance() {
return LazyHolder.LAZY_INNER_CLASS_SINGLETON;
}
//默认不加载
private static class LazyHolder {
private static final LazyInnerClassSingleton LAZY_INNER_CLASS_SINGLETON = new LazyInnerClassSingleton();
}
}
这种方法兼顾了饿汉式单例模式的内存浪费问题和懒汉式单例synchronized加锁的性能问题。内部类在方法调用时才会初始化,巧妙了避免了线程安全问题。
五、破坏单例行为
1. 反射破坏单例
静态内部类单例模式由于这种方式比较简单,但是,金无足赤,人无完人。这种写法真的完美了吗?
public class LazyInnerClassSingletonTest {
@Test
public void test() {
try {
//在很无聊的情况下,进行破坏
Class clazz = LazyInnerClassSingleton.class;
//通过反射获取私有构造方法
Constructor constructor = clazz.getDeclaredConstructor(null);
constructor.setAccessible(true);
//暴力初始化
//调用了两次构造方法,相当于new了两次,犯了原则性错误
Object o1 = constructor.newInstance();
Object o2 = constructor.newInstance();
System.out.println(o1 == o2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
答案:false
显然,创建了两个不同的实例,那怎么办昵?我们来做一次优化,现在我们在其构造方法中做一些限制,一旦出现多次重复创建,则直接抛出异常,来看优化后的代码:
private LazyInnerClassSingleton() {
if (LazyHolder.LAZY_INNER_CLASS_SINGLETON != null) {
throw new RuntimeException("不允许创建多个实例~~~");
}
}
至此,自认为史上最牛的单例模式的实现方式便大功告成。
2. 序列化破坏单例
静态内部类单例模式构造函数限制虽然能限制住反射,但又被序列化给破了!!!
一个单例对象创建好后,有时候需要将对象序列化然后写入磁盘,下次使用时再从磁盘中读取对象并进行反序列化,将其转化为内存对象。反序列化后的对象会重新分配内存,即重新创建。如果序列化的目标对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例,来看一段代码:
@Test
public void test2() {
try {
LazyInnerClassSingleton s1 = LazyInnerClassSingleton.getInstance();
FileOutputStream fos = new FileOutputStream("serializableSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s1);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("serializableSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
LazyInnerClassSingleton s2 = (LazyInnerClassSingleton) ois.readObject();
ois.close();
System.out.println("s1=" + s1);
System.out.println("s2=" + s2);
System.out.println(s1 == s2);
} catch (Exception e) {
e.printStackTrace();
}
}
从运行结果可以看出,反序列化后的对象和手动创建的对象是不一致的,实例化了两次,违背了单例模式的设计初衷。那如何保证在序列化情况下也能够实现单例模式昵?其实很简单,只需要增加readResole()方法即可。来看优化后的代码:
public class LazyInnerClassSingleton implements Serializable {
private static final long serialVersionUID = -8484501356898167924L;
private LazyInnerClassSingleton() {
if (LazyHolder.LAZY_INNER_CLASS_SINGLETON != null) {
throw new RuntimeException("不允许创建多个实例~~~");
}
}
public static final LazyInnerClassSingleton getInstance() {
return LazyHolder.LAZY_INNER_CLASS_SINGLETON;
}
private static class LazyHolder {
private static final LazyInnerClassSingleton LAZY_INNER_CLASS_SINGLETON = new LazyInnerClassSingleton();
}
// 重点代码!!!
private Object readResolve() {
return LazyHolder.LAZY_INNER_CLASS_SINGLETON;
}
}
总算解决了,为了单例不被破坏,真是煞费苦心啊!!!
想知道具体原因,大家可以阅读ObjectInputStream的readObject()方法。
六、枚举式单例模式
public enum EnumSingleton {
INSTANCE;
public static EnumSingleton getInstance() {
return INSTANCE;
}
}
其实枚举式单例,虽然写法优雅,但是也会有一些问题。因为它在类加载之时就将所有的对象初始化放在类内存中,这其实和饿汉式并无差异,不适合大量创建单例对象的场景。
七、总结
单例模式可以保证内存里只有一个实例,减少了内存的开销,可以避免对资源的重复占用。
欢迎大家关注微信公众号(MarkZoe)互相学习、互相交流。