单例模式,其实很简单,就是保持一个类仅有一个实例,并提供一个访问它的全局访问点。
简单来说,就是保证一个类在全局的范围内,只能有一个实例化的对象。一个最好的解决办法是,让这个类自身负责保存它的唯一实例,这个类可以保证没有其他实例可以被创建,并且提供一个访问该实例的方法。
单例模式有什么好处呢?它可以控制客户如何去访问、何时去访问这个全局唯一的实例,也就是对唯一实例的受控访问。
单例模式有很多种写法,下面一一进行介绍
首先是饿汉式。这是最简单的一种方式。就是在类里面声明一个自身的静态对象,然后在声明时就创建它。
private static Singleton s = new Singleton();
这样的好处是,不用判断什么时候来创建这个对象,只要类被初始化,对象就创建了。但坏处就是,可能会造成资源浪费。这个对象可能并没有被使用到,但也被创建了。
然后是普通的懒汉式,它的特点是,在需要用到的时候才创建这个对象。
private static Singleton s = null;
public static Singleton getInstance() {
if(null == s) {
s = new Singleton();
}
return s;
}
懒汉式解决了资源浪费的问题,但在多线程环境下,无法保证安全性,因为多个线程可能同时访问到判断对象是否为空的代码,这个时候还是会创建多个对象。但在单线程环境下,这样做是安全的。
我们为了解决懒汉式的多线程不安全问题,可以采取加锁的方式
private static Singleton s = null;
public synchronized static Singleton getInstance() {
if (null == s) {
s = new Singleton();
}
return s;
}
这样,给方法加上锁,就可以保证这个方法同时只能被一个线程访问到,就保证了线程的安全性。但问题又来了,加锁是一个比较耗费资源的操作,这样每一次访问都要进行加锁,势必会影响效率。然后我们又有了双重检查锁的写法
private volatile static Singleton s = null;
public static Singleton getInstance() {
if(null == s) {
synchronized(Singleton.class) {
if(null == s) {
s = new Singleton();
}
}
}
return s;
}
这段代码有几个比较有意思的点。首先是变量加上了volatile。这个关键字是忽略编译器的优化,保证每一次都是重新读取这个变量的值。因为编译器很可能会对一段代码进行优化,比如把值放到寄存器里面。这样如果其他线程修改了这个值,又没有及时更新寄存器,那么就会造成错误。然后是方法头部,没有了synchronized关键字,这样就避免了每一次进入这个方法都要加锁。然后是方法体,可以看到先对对象做了一次判空操作,然后加锁,在加锁块的内部,又对对象进行了一次判空操作。这是为什么呢?因为多个线程很可能同时访问到最外层的判空,如果加锁块内部不再次进行判断的话,那么即使一个线程持有了锁,创建了对象,那当这个线程释放锁,另一个线程继续执行的时候,又会执行创建对象的操作,这样就不是线程安全的了。所以双重检查锁避免了这样的问题。
最后是利用内部类来进行实现
private static class Holder {
private static Singleton s = new Singleton();
}
public static Singleton getInstance(){
return Holder.s;
}
这样写的好处是,它拥有了懒汉式的优点,在真正调用getInstance的时候才会加载静态内部类,而且加载时是线程安全的。
单例模式虽然简单,但如果深入研究,却也有这么多种不同的实现,嗯,好好学习。
相关demo可以参考我的gitee仓库
https://gitee.com/akitsuki-kouzou/DesignPatternDemo