之前学习单例模式都是看别人是如何实现的, 今天就自己写一下实现单例模式的代码, 在这里分享一下
饿汉模式
饿汉模式其实就是大部分人最常使用的一种单例模式
public class SingletonTest {
private final static SingletonTest singletonTest = new SingletonTest();
private SingletonTest() {}
public static SingletonTest getInstance() {
return singletonTest;
}
}
很清晰的可以看见, 这个singletonTest对象在类加载的时候就已经实例化了, 并且我们对构造器方法使用了private修饰, 使得外部无法再次使用构造器将该类实例化, 保证了单例模式, 这种实现方式的缺点就是, 只要这个类一经加载, 那么这个类的实例化对象就会存在, 如果后面我们不使用的话, 就会造成空间的浪费, 所以有下面的懒汉模式
懒汉模式
懒汉模式的意思就是, 这个类只有在使用到的时候才会实例化, 所以我们需要在获取类的方法中做文章, 需要保证他只被实例化一次
这里讲两种实现的方法
1. 同步锁
public class SingletonTest {
private static SingletonTest singletonTest = null;
private SingletonTest() {}
public synchronized static SingletonTest getInstance() {
if(null == singletonTest) {
singletonTest = new SingletonTest();
}
return singletonTest;
}
}
这种方式很明显会造成资源的浪费, 每次使用get方法的时候都需要对锁资源进行竞争, 但是由于对类的实例化只需要进行一次, 所以其实只需要在第一次实例化的时候加锁就行了, 这就有了下面的双检锁
2. 双检锁
public class SingletonTest {
private volatile static SingletonTest singletonTest = null;
private SingletonTest() {}
public static SingletonTest getInstance() {
if(null == singletonTest) {
synchronized (SingletonTest.class) {
if(null == singletonTest) {
singletonTest = new SingletonTest();
}
}
}
return singletonTest;
}
}
在get方法中有两个if判断, 外部的if主要是用来防止资源浪费的, 因为后续进入的线程只需判断是否已经被实例化过了而不需要进行锁的竞争, 所以其实外部的if并不影响我们的单例模式的实现, 重点是内部的判空, 通过synchronized关键字来对整个SingletonTest类上锁, 保证只有一个线程进入语句块, 然后判断当前实例是否为空来实现单例模式.
双检锁最为重要的一点就是对singletonTest加了一个volatile关键字来修饰.
volatile关键字的作用有两点:
- 使用volatile关键字修饰的变量, 一个线程修改了这个变量之后, 其他线程工作区域内的该变量会失效, 从而强制其他线程从主存中重新获取该变量的值
- 禁止指令重排序
现在很多文章都是说双检锁中volatile是禁止指令重排序作用, 可我认为这有点滑稽
我们知道一个对象T被实例化了需要经过以下三步:
1.给对象T分配内存
2.调用构造函数
3.将T对象指向1中所分配的内存空间
但是由于指令重排序的原因, 其中第二第三步可能会被重排序, 因为在重排序之后结果是不变的, 这就导致了先将T指向了一块内存空间,那么这个T==null的结果就是false, 在多线程的情况下, 其他线程这时候可能正在进行null == singletonTest的判断, 所以这时这个判断结果是true, 会返回一个还没有初始化的singletonTest对象, 会导致抛出异常.
上面的就是其他文章认为在双检锁的情况下会发生的重排序情况, 可是我们可以看见, 在双检锁中synchronized锁定的是整个class, 所以不可能存在我正在分配对象空间的时候还有其他线程可以进入这个类
其次, 假设类中的对象还未实例化, 多个线程进入到第一个if中, 只有一个线程抢到了锁, 其他线程被阻塞, 这时的volatile意义其实就是第一点, 保证可见性, 因为其他线程获取锁之后, 进入if判断, 发现现在的对象已经被实例化了, 就直接return 保证了单例模式
单例模式的应用场景
单例模式主要应用于在业务逻辑上限定不能存在多实例的情况, 例如: 网站中的在线人数计数器, 在默认状态下spring中的bean都是单例模式的, 因为我们的确不需要创建两个相同的controller或者service来实现某种功能. 我们并不会在controller或者service里面写一些没有被static或者final修饰的变量, 如果有这种需求, 那一定不能使用单例模式