单例模式简介
所谓的单例模式即在整个系统中,某个类只存在一个实例对象,并且该类只提供一个取得其对象实例的方法。
单例模式特点
单例模式有三个重要的特点,分别是:
- 一个类只有一个实例
- 这个类必须自行创建实例
- 这个类必须向整个系统提供这个实例
单例模式的结构与实现
单例模式是设计模式中最简单的模式之一。通常,普通类的构造函数是公有的,外部类可以通过“new 构造函数()”来生成多个实例。但是,如果将类的构造函数设为私有的,外部类就无法调用该构造函数,也就无法生成多个实例。这时该类自身必须定义一个静态私有实例,并向外提供一个静态的公有函数用于创建或获取该静态私有实例。
单例模式的结构
单例模式的主要角色如下。
- 单例类:包含一个实例且能自行创建这个实例的类。
- 访问类:使用单例的类。
单例模式的实现
单例模式有八种写法,但是在工作中推荐使用的只有几种方式。
方式一:饿汉式(静态变量)
饿汉式静态变量方式实现如下:
public class SingletonTypeOne {public static void main(String[] args) {Singleton instance = Singleton.getInstance();Singleton instance1 = Singleton.getInstance();System.out.println(instance == instance1);}}class Singleton{//1.私有化构造函数private Singleton(){}//2.类的内部创建对象实例private final static Singleton instance = new Singleton();//3.提供一个共有的静态方法,返回实例对象public static Singleton getInstance(){return instance;}}
这种方式虽然实现简单,但功能完全能满足需求,唯一瑕疵的地方就在于Singleton一旦加载到虚拟机中就会被实例化,造成系统资源浪费。但是LZ认为这种浪费完全可以忽略不计,JDK中Runtime的实现单例方式即使用的这种方式。
推荐指数:☆☆☆☆
方式二:饿汉式(静态代码块)
饿汉式静态代码块实现如下:
public class SingletonType02 {public static void main(String[] args) {Singleton instance = Singleton.getInstance();Singleton instance1 = Singleton.getInstance();System.out.println(instance == instance1);}}class Singleton{//1.私有化构造函数private Singleton(){ }//2.类的内部创建对象实例private static Singleton instance ;//在静态代码块中创建单例对象static {instance = new Singleton();}//3.提供一个共有的静态方法,返回实例对象public static Singleton getInstance(){return instance;}}
这种方式和方式一几乎一样,只是把实例化放到了静态代码块中来完成的。
推荐指数:☆☆☆
方式三:懒汉式(线程不安全)
懒汉式实现单例方式如下:
/*** 饿汉式实现单例模式* 线程不安全*/public class SingletonType03 {public static void main(String[] args) {for (int i=0;i<100;i++){new Thread(()-> System.out.println(Singleton.getInstance().hashCode())).start();}}}class Singleton{private static Singleton instance;private Singleton(){}public static Singleton getInstance(){if(instance == null){instance = new Singleton();}return instance;}}
这种方式只能在单线程下保证创建的实例是唯一的,但是在多线程下是不能保证创建的实例是唯一的。原因是假如现在有线程A和线程B同时访问getInstance()方法,线程A在判断完if(instance == null)后,此时CPU调度将时间片分给了线程B,那么线程B也会进行if(instance == null)的判断,这时instance ==null成立,线程B将执行instance = new Singleton()创建一个实例,在线程B创建完实例后,CPU调用将时间片重新分配给了线程A,那么A也将执行instance = new Singleton()再次创建一个新的实例,此时线程A创建的实例和线程B创建的实例就是不一致的。
测试结果:
既然上面的方式是线程不安全的,那么我们是否可以通过加锁来保障线程安全呢?因此演变出方式四的实现单例。
推荐指数:不推荐
方式四:懒汉式(同步方法)
public class SingletonType04 {public static void main(String[] args) {for (int i=0;i<100;i++){new Thread(()-> System.out.println(Singleton.getInstance().hashCode())).start();}}}class Singleton{private static Singleton instance;private Singleton(){}public static synchronized Singleton getInstance(){if(instance == null){instance = new Singleton();}return instance;}}
这种方式是在getInstance方法上加synchronized关键词来保障线程安全,但是我们知道synchronized关键词会影响性能,虽然官方自JDK6以后通过适应性自旋锁、锁消除、锁粗化、偏向锁以及轻量级锁等一系列手段对synchronized做了优化,它的性能得到了非常大的提升,但是我们能不能通过其它手段来优化上面的代码呢?这就演出了懒汉式(同步代码块)的实现方式。
推荐指数:不推荐
方式五:饿汉式(同步代码块)
public class SingletonType05 {public static void main(String[] args) {for (int i=0;i<100;i++){new Thread(()-> System.out.println(Singleton.getInstance().hashCode())).start();}}}class Singleton{private static Singleton instance;private Singleton(){}public static Singleton getInstance(){if(instance == null){synchronized (Singleton.class) {instance = new Singleton();}}return instance;}}
这种方式虽然对程序就行了优化,但是在多线程下就变得不安全了,至于原因已经在前面解释过了,主要就是在if(instance == null)这句多个线程都可以进入。
测试结果如下:
基于上面方式五是线程不安全,演变成了双重检查机制来既保证了性能,又保障了先安全。
推荐指数:不推荐
方式六:双重检查
public class SingletonType06 {public static void main(String[] args) {for (int i=0;i<100;i++){new Thread(()-> System.out.println(Singleton.getInstance().hashCode())).start();}}}class Singleton{private static volatile Singleton instance;private Singleton(){}public static Singleton getInstance(){if(instance == null){synchronized(Singleton.class){if(instance == null){instance = new Singleton();}}}return instance;}}
双重检查机制有一个非常重要的点就是属性instance上必须要有volatile关键词修饰,如果没有volatile关键词修饰,那么双重检查会有一个致命的缺陷。原因是编译器和处理器为了优化程序性能会对指令序列重新排序。关于重排序的知识,有兴趣的同学可以看看LZ的这篇博客https://zzwzdx.cn/jmm-reordering/。好了言归正传,为什么不加volatile就会有缺陷呢?这是因为创建实例不是一个原子操作,即instance = new Singleton();这句话不是原子操作,虽然它就只有一句,但是在虚拟机中它分为了3个步骤,分别是:
memory = allocate(); // 1:分配对象的内存空间ctorInstance(memory); // 2:初始化对象instance = memory; // 3: 设置instance指向刚分配的内存地址
上面3行代码中,2和3可能被重排序,如果2和3被重排序, 在对象还未被初始化时,其它线程就有可能拿到了这个还未被初始化的对象去使用,导致了程序出现异常。volatile的作用就是禁止指令2和3重排序。
推荐指数:☆☆☆☆☆
方式七:静态内部类
public class SingletonType07 {public static void main(String[] args) {for (int i=0;i<100;i++){new Thread(()-> System.out.println(Singleton.getInstance().hashCode())).start();}}}class Singleton{private Singleton(){}//静态内部类,该类中有一个静态属性private static class SingletonHolder{private static final Singleton INSTANCE = new Singleton();}public static Singleton getInstance(){return SingletonHolder.INSTANCE;}}
静态内部类实现单例模式也是一个非常好的选择,它不仅保证了性能、线程安全和懒加载,而且它的实现也非常的简单。性能的保障是程序没有使用锁,懒加载的保障是INSTANCE实例是在静态内部类中创建的,Singleton类在被加载时,实例是不会被初始化的。线程安全的保障是通过JVM来完成的,JVM初始化类时只会初始化一次,因此SingletonHolder在被虚拟机进行初始化时INSTANCE也只会被初始化一次。
推荐指数:☆☆☆☆☆
方式八:枚举
public class SingleType08 {public static void main(String[] args) {for (int i=0;i<100;i++){new Thread(()-> System.out.println(Singleton.getInstance().hashCode())).start();}}}enum Singleton{INSTANCE;}
这是最后一种实现单例模式的方式,这种方式是Josh Bloch在《Effective Java》中提出的。Josh Bloch在《Effective Java》中表明”使用枚举实现单例的方法虽然还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法“。确实看到这里我们也知道使用枚举实现单例是多么的简洁。使用枚举还有一个最大的优势就是不能通过反序列化手段来破坏单例。这里至于为什Enum能保证线程安全和不能反序列化,后面LZ单独写一篇文章解释。
推荐指数:☆☆☆☆☆
总结
上面就是单例模式的全部内容,从上面可以看到,单例模式看起来简单但实际上呢?完全不是,单例模式要考虑的东西还是很多的,既要考虑性能还要考虑线程安全必要时还需要考虑反序列化的问题。至于最喜欢哪种方式,那就是萝卜白菜各有所爱了!
