单例模式
定义:
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
实现方式:“饿汉式” and “懒汉式”
“饿汉式”:在类加载的时候就实例化对象,通俗来说就是:不管这个对象你要不要用,我都先实例化出来,要用的时候直接取就行
“懒汉式”:也就是我们常说的延迟加载。通俗来说就是:只要你要用这个对象,我才去实例化这个对象,然后在取来用
通过定义我们就知道这两种方式的差异了,“饿汉式”在不采取同步的方式的时候就是线程安全的,而“懒汉汉”则需要采取同步的措施来保证线程安全,当然也可以不采取同步的措施,例如采用枚举类来实现单例模式,这个接下来细说。
优点:
- 1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例。
- 2、避免对资源的多重占用(比如写文件操作)。
缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
具体实现:也就是上代码啦!!!
1、“饿汉式”
1 // 饿汉式 demo1 2 public class Singleton1 { 3 4 // 必须私有化构造函数 5 private Singleton1(){} 6 7 // 先初始化对象 8 private static Singleton1 singleton1 = new Singleton1(); 9 10 // 直接返回对象引用 11 public static Singleton1 getSingleton1(){ 12 return singleton1; 13 } 14 15 }
优点:简单、线程安全
缺点:如果单例使用过多,首次加载占用内存比较多,而且如果很多对象不常用,造成资源的浪费
2、“懒汉式”(简单粗暴方式:synchronized 方法)
1 public class Singleton2 { 2 3 private static Singleton2 singleton2; 4 5 private Singleton2() {} 6 7 // 使用 synchronized 保证线程安全 8 public static synchronized Singleton2 getInstance() { 9 // 判断是否已经实例化对象,没有则进行实例化 10 if (singleton2 == null){ 11 singleton2 = new Singleton2(); 12 } 13 return singleton2; 14 } 15 16 }
注意: getInstance方法中使用了Synchronied来保证线程安全,实现方式比较简单,但是这样锁的粒度的太大,高并发的情况下,会导致效率不高(可能会存在多个线程在等着拿到锁进入getInstance方法)
3、“懒汉式”(双重检验式:synchronized代码块 + 双重判断 + volatile)(常见的写法)
1 public class Singleton3 { 2 3 private volatile static Singleton3 instance; 4 5 private Singleton3() {} 6 7 public static Singleton3 getInstance() { 8 // 第一次先判断引用是否为空 9 if (instance == null) { 10 // 再次判断,在同步代码块里再次检验(应付高并发) 11 synchronized (Singleton3.class) { 12 if (instance == null) { 13 instance = new Singleton3(); 14 } 15 } 16 } 17 return instance; 18 } 19 20 }
此处引用: https://blog.csdn.net/da_kao_la/article/details/89076575 博主对单例双重检验实现方式的一段解释,如下:
双重检测方式是兼备高并发效率和延迟加载的优点。
我们需要注意instance属性要加volatile关键字,这是因为Java对象的实例化过程不是原子操作,具体来说,实例化过程大致包含如下步骤:
- 开辟内存
- 对象的初始化(默认初始化和动态块)
- 执行对象的构造方法
- 将栈区的引用指向堆区的内存空间
其中2,3,4步可能发生重排,4可能在发生在2,3之前。因此,可能线程A获得了线程B尚未初始化完成的实例就返回了。volatile关键字其中一个作用就是禁止编译器的指令重排(编译器做指令重排的目的是单线程条件下重排指令可以在不改变程序执行结果的条件下优化运行速度)。在Java 5之前,由于编译器对volatile关键字的实现并不能完全屏蔽编译器的指令重排,因此双重检测锁的单例模式在多线程环境下偶尔会出现上述问题。Java 5及其以后版本,应该没有这个问题了。
4、“懒汉式”(内部类实现)
1 public class Singleton4 { 2 3 private static class Singleton4InnerClass { 4 private static final Singleton4 instance = new Singleton4(); 5 } 6 7 private Singleton4() { 8 } 9 10 public static Singleton4 getInstance() { 11 return Singleton4InnerClass.instance; 12 } 13 14 }
这种方式实现也是线程安全的,不是基于锁的方式来实现,而是基于ClassLoader机制来实现线程安全,静态内部类的类加载过程是线程安全的,所以在静态内部类的属性中实例化外部类的对象这样可以保证线程安全以及高并发的效率
5、枚举类实现(据说是单例模式的最佳实现)
1 public enum Singleton5 { 2 3 INSTANCE; 4 5 public void doSomething() { 6 System.out.println("doSomething"); 7 } 8 9 }
备注:枚举类的构造方法的权限修饰符默认是private;
枚举类明确了构造方法限制为私有,在我们访问枚举实例时会执行构造方法,同时每个枚举实例都是static final类型的,也就表明只能被实例化一次。在调用构造方法时,我们的单例被实例化。 也就是说,因为enum中的实例被保证只会被实例化一次,所以我们的INSTANCE也被保证实例化一次。
参考链接:
https://www.runoob.com/design-pattern/singleton-pattern.html