单例模式
概述
-
保证一个类仅有一个实例,并提供一个全局的访问点
-
类型: 创建型
-
场景: 想确保任何情况下都绝对只有一个实例
-
优点:在内存中只有一个实例,减少了内存的开销,避免了对资源的多重占用,设置全局的访问点,严格控制访问。
-
缺点:扩展比较困难
注意点:
- 私有构造器 - 防止被主动创建
- 线程安全 - 这个很好理解,所有线程共享一个对象,就会存在线程安全的问题。
- 延迟加载 - 用到的时候才去创建
创建的方式
懒汉式
普通写法 - 线程不安全
public class LazySingleton {
private static LazySingleton lazySingleton;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
- 线程不安全
- 这个很明显当两个线程同时进入getInstance方法的时候,都会判断lazySingleton对象为空,从而创建两个lazySingleton对象,显然不符合我们单例模式的规则。
同步方法 - 线程安全
public class LazySingleton {
private static LazySingleton lazySingleton;
private LazySingleton() {
}
public synchronized static LazySingleton getInstance() {
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
- 改成了同步方法,虽然解决了线程安全的问题,但是每次获取单例对象都要加锁,效率明细降低。显然是不可取的。
双重校验
public class DoubleCheckSingleton {
private static DoubleCheckSingleton doubleCheckSingleton;
private DoubleCheckSingleton(){
}
public static DoubleCheckSingleton getInstance(){
if(doubleCheckSingleton == null){ //01
synchronized (DoubleCheckSingleton.class){ //02
if(doubleCheckSingleton ==null){ //03
doubleCheckSingleton = new DoubleCheckSingleton(); // 04
}
}
}
return doubleCheckSingleton;
}
}
-
同步块放到getInstance方法中进行加锁,这样避免了每次获取都要进行获取锁和释放锁的动作,只是第一次需要获取锁进行创建对象。
-
在同步块还需要再一次判断是否为空。原因是如果第一次有两个线程同时进入线程A和线程B,如果线程A抢到锁的资源,进行创建对象实例。线程B在获取到锁如果不加判断的话还会继续创建对象的实例,这样也不满足单例模式的原则。
-
缺点: 会存在获取对象成功后,调用对象的相关属性或者方法空指针异常。
-
原因如下:
- 主要的原因是我们在创建 doubleCheckSingleton对象的时候,代码04行不是原则操作,这里的原子操作可以理解为new的过程中会被cpu调度打断。
- 还有一个原因是JVM的重排序优化。
-
基于上面两个原因导致了空指针,我们可以先画个图在结合代码进行分析。
- 重排序的规则在单线程中在不影响最终结果的基础上是允许打乱顺序执行的,使用02 和 03 是允许调换位置进行执行的 。
双重检查 -升级版
- 看到上面的讲述大家很自然的想到了,如果解决了重排序的问题,那么就不会存在空指针异常的问题。代码如下所示:
public class DoubleCheckSingleton {
private volatile static DoubleCheckSingleton doubleCheckSingleton;
private DoubleCheckSingleton() {
}
public static DoubleCheckSingleton getInstance() {
if (doubleCheckSingleton == null) {
synchronized (DoubleCheckSingleton.class) {
if (doubleCheckSingleton == null) {
doubleCheckSingleton = new DoubleCheckSingleton();
}
}
}
return doubleCheckSingleton;
}
}
- 在生命变量的时候添加一个volatile关键字。在这里就不对volatile关键字做解释了。就是禁止重排序。严格按照01 02 03的顺序进行执行。
静态内部类的方式 - 懒汉单例
- 先不说原理,直接上代码;
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton() {
}
public static StaticInnerClassSingleton getInstance() {
return InnerClass.staticInnerClassSingleton;
}
private static class InnerClass {
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
}
- 代码是不是很简单。原理如下所示:
- 我们在了解原理之前要对对象的加载,初始化动作很清晰,可以参考 java虚拟机类加载深入理解和阶段分析
- 这里涉及到的知识点有对类的主动使用导致其初始化,初始化的过程会为类的静态变量赋值在这里就是创建staticInnerClassSingleton对象。
- 而类的初始化动作只会发生一次,非常符合我们的单例模式这个场景。
- 感慨一句:不管你在学什么东西,知识点都是相同的。加油!
枚举
- 因为枚举在java里面就是单例的,这个是我们java的特性,我们可以利用这个特性来完成我们单例模式的编写。
public class Singleton07 {
private Singleton07() {
}
public static Singleton07 getInstance(){
return Single.INSTANCE.getInstance();
}
private enum Single {
INSTANCE;
private final Singleton07 singleton07;
Single() {
singleton07 = new Singleton07();
}
public Singleton07 getInstance() {
return singleton07;
}
}
}
饿汉式
- 懒汉和饿汉的主要区别就是延迟加载。
- 懒汉就是在程序需要的时候进行创建
- 饿汉而是在类进行初始化的时候就进行了创建,可能一直没有被程序所使用
简单的写法
public class HungrySingleton {
private static final HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
-
线程安全的:因为在类加载的过程中进行创建,只会创建一次。
-
如果文章有帮助到你欢迎关注微信公众号《后端学长》