java中实现单例模式的两种方式
- 饿汉模式
- 懒汉模式
- 单例模式的应用场景
之前学习单例模式都是看别人是如何实现的, 今天就自己写一下实现单例模式的代码, 在这里分享一下
饿汉模式
饿汉模式其实就是大部分人最常使用的一种单例模式
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关键字修饰的变量, 一个线程修改了这个变量之后, 其他线程工作区域内的该变量会失效, 从而强制其他线程从主存中重新获取该变量的值
- 禁止指令重排序
instance = new Singleton(); // 第10行
// 可以分解为以下三个步骤
1 memory=allocate();// 分配内存 相当于c的malloc
2 ctorInstanc(memory) //初始化对象
3 s=memory //设置s指向刚分配的地址
// 上述三个步骤可能会被重排序为 1-3-2,也就是:
1 memory=allocate();// 分配内存 相当于c的malloc
3 s=memory //设置s指向刚分配的地址
2 ctorInstanc(memory) //初始化对象
而一旦假设发生了这样的重排序,比如线程A在第10行执行了步骤1和步骤3,但是步骤2还没有执行完。这个时候线程B执行到了第7行,它会判定instance不为空,然后直接返回了一个未初始化完成的instance!
单例模式的应用场景
单例模式主要应用于在业务逻辑上限定不能存在多实例的情况, 例如: 网站中的在线人数计数器, 在默认状态下spring中的bean都是单例模式的, 因为我们的确不需要创建两个相同的controller或者service来实现某种功能. 我们并不会在controller或者service里面写一些没有被static或者final修饰的变量, 如果有这种需求, 那一定不能使用单例模式