Java 设计模式(一)-单例模式
什么是单例模式
- 单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
- 这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
实现单例模式的思路
-
构造方法私有化
保证系统中只有一个对象的实例,那么我们就需要去防止该对象一不小心被 new 出来,因此我们需要把构造方法私有化
-
创建静态方法获取实例
外界不能通过 new 来获得对象实例了,然而我们需要获取一个实例,那么我们就需要提供一个可供外界访问的返回对象实例的静态方法
-
确保对象实例只有一个
只对对象进行一次实例化,以后的获取都是获取第一次创建的对象实例
几种单例模式的区别
-
饿汉模式
- 饿汉模式是指,我先把对象创建好,等你需要的时候再来拿就可以了
public class Singleton1 { // 无论该对象是否被使用,均会创建,绝大数情况加浪费性能 private static Singleton1 singleton = new Singleton_1(); private static Singleton1() { } public Singleton1 getSingleton() { return singleton; } }
- 这种模式是最为简单的,而且天生线程安全,但是有一个缺陷在于有资源浪费的嫌疑,不论你什么时候使用,我在类加载的时候就一次性创建了
-
懒汉模式
- 因为饿汉模式存在浪费资源的嫌疑,懒汉模式应运而生
- 懒汉模式的意思是,我先不创建类的对象实例,等你需要的时候我再创建
public class Singleton2 { private static Singleton2 singleton = null; private Singleton2() {} // 在调用的时候再去创建对象 // 在并发情况下无法保证项目中只有一个Singleton_2对象,违背单例模式的初衷 public static Singleton2 getSingleton() { if (singleton == null) { // 线程1 singleton = new Singleton2(); // 线程 2 } return singleton; } }
- 该对象实例只有在执行获取方法的时候才会去创建、解决了饿汉模式的资源浪费的缺陷
- 但是上述代码在并发情况下会出现创建多个对象的情况,例如,此时 singleton 为 null,线程 1 与线程 2 均会通过判断,进行对象实例化,因此线程 1 与线程 2 会各自创建一个对象
-
synchronized 同步锁
- 因为可能出现外界多人同时访问 getSingleton()方法,可能会出现因为并发问题导致类被实例化多次,我们可以给获取对象方法加上锁 synchronized 来控制该方法在同一时刻只有一个线程能进入,来保证该对象只会被实例化一次
public class Singleton3 { private static Singleton3 singleton = null; private Singleton3() { } // 这是最简单的解决方案,使用synchronized加锁来解决 // 可以有多个线程进入getSingleton()方法,但只会有一个线程进入if判断,其他线程等待 public static Singleton3 getSingleton() { synchronized (Singleton3.class) { if (singleton == null) { singleton = new Singleton3(); } } return singleton; } }
- 我们通过加锁的方式保证了单例模式的安全性,但因为给获取对象的方法进行了加锁,多个线程只有一个能进行判断,其他进程进入阻塞状态,等待上个进程执行结束后才能进行判断,影响性能
-
双重检验锁
-
我们可以使用双重检验锁的形式来优化
public class Singleton4 { private static Singleton4 singleton = null; private Singleton_3() { } public static Singleton4 getSingleton() { // 先进行判断,为null再进入同步代码块 if (singleton == null) { synchronized (Singleton_3.class) { if (singleton == null) { singleton = new Singleton_3(); } } } return singleton; } }
-
但是 DCL 也存在有缺陷,由于 jvm 的指令重排序,DCL 偶尔也会存在失效的情况
-
创建一个对象这个操作并不是一个原子性的操作,jvm 会生成三个指令
- 指令 1:给对象分配内存空间
- 指令 2:调用构造器,初始化对象属性
- 指令 3:将对象引用指向内存地址
-
java 编译器可能会对指令进行优化,可能会把指令执行顺序变为
- 执行指令 1:分配对象空间
- 执行指令 3:将对象引用执行内存地址
- 执行指令 2:调用构造器,初始化对象属性
-
我们来分析重排序后可能存在的问题
- 在线程 1 执行到重排序后的第二步之后,然后 cpu 正好切换到线程 2 工作,且线程 2 也正好进行了 getSingleton 操作,此时对象处于创建了引用且分配了内存空间但未进行初始化的状态,那么线程 2 在执行 **if (singleton == null)**判断的时候,线程 2 发现对象不为 null,那么线程 2 就获取到了一个没有进行初始化的对象
-
所以在这种情况下,双重检验锁的方式会出现 DCL 失效的问题。
-
优化双重检验锁以及双重检验锁失效的问题非本文重点,有兴趣可参考双重检验锁失效”的问题说明以及相关优化以及翻译版本
-
-
静态内部类
- 当外部类内被访问时,并不会加载内部类,我们可以通过这一特性来进行设计单例
public class Singleton4 { private Singleton4() { } // 定义静态内部类 private static class BuildSingleton4 { // 加载外部类时不会执行,只有加载 BuildSingleton4 时才会执行 private static Singleton4 singleton = new Singleton4(); } public Singleton4 getSingleton() { //当内部类第一次被访问时,创建对象实例 return BuildSingleton4.singleton; } }
- 当外部内被访问时,并不会加载内部类,所以只要不访问 BuildSingleton4 这个内部类,private static Singleton4 singleton = new Singleton4()不会被执行,只有当 Singleton4.getSingleton被调用时访问内部类的属性,此时才会将对象进行实例化,这就相当于实现懒加载的效果,这样既解决了恶汉模式下可能造成资源浪费的问题,也避免了了懒汉模式下的并发问题。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于