目 录CONTENT

文章目录

单例模式

小王同学
2023-12-23 / 0 评论 / 0 点赞 / 41 阅读 / 0 字

微信公众号:[新时代程序猿]

关注可了解更多的JAVA,PYTHON,ANDROID教程及开发技术。

问题或建议,请公众号留言

[如果你觉得文章对你有帮助,欢迎赞赏]

一、概述

设计模式刚开始真是个难搞的东西,大学学的时候一脸懵逼,没好好学,现在到公司后,复杂的业务逻辑难以招架,某个时候在网站上看到设计模式后恍然大雾,原来自己写的代码竟然是设计模式的某一种,所以下定决心,好好学习设计模式的思想。

在工作中难免出现bug,很多时候改一个bug出现另一个bug,是因为代码耦合问题严重,设计模式可以让我们的代码简洁明了,更重要的是可以使代码解耦,在修改代码的时候避免修改到老的业务逻辑,减少了很多看不见的“坑”。

本文主要从以下几个方面介绍单例模式:

  1. 单例模式是什么

  2. 实现单例模式的思路

  3. 单例模式的分类与实现(重点)

  4. 总结

1、单例模式是什么?

23 种设计模式可以分为三大类:

  • 创建型模式

  • 行为型模式

  • 结构型模式

单例模式属于创建型模式的一种,单例模式是最简单的设计模式之一:

单例模式只涉及一个类,确保在系统中一个类只有一个实例,并提供一个全局访问入口。许多时候整个系统只需要拥有一个全局对象,这样有利于我们协调系统整体的行为。

比如:大家都要喝水,但是没必要每人家里都打一口井是吧,通常的做法是整个村里打一个井就够了,大家都从这个井里面打水喝。

对应到我们计算机里面,像日志管理、打印机、数据库连接池、应用配置。

2.实现单例模式的思路

1. 构造私有:

如果要保证一个类不能多次被实例化,那么我肯定要阻止对象被new 出来,所以需要把类的所有构造方法私有化。

2.以静态方法返回实例。

因为外界就不能通过new来获得对象,所以我们要通过提供类的方法来让外界获取对象实例。

3.确保对象实例只有一个。

只对类进行一次实例化,以后都直接获取第一次实例化的对象。

/**

 * 单例模式案例

 */

public class Singleton {

	//确保对象实例只有一个。

 private static final Singleton singleton = new Singleton();

	//构造方法私有

 private Singleton() {

    }

 //以静态方法返回实例

 public static Singleton getInstance() {

 return singleton;

    }

}

这里类的实例在类初始化的时候已经生成,不再进行第二次实例化了,而外界只能通过Singleton.getInstance()方法来获取Singleton对象, 所以这样就保证整个系统只能获取一个类的对象实例。

3、单例模式的分类与实现

A、懒汉单例模式:在第一次调用的时候实例化本身,在并发环境下,可能出现多个本身对象。所以线程是不安全的

(太懒了,程序运行后不自动创建,只有等用到的时候才创建对象,如果用不到就不创建)

B、饿汉单例模式:在类初始化时,已经自行实例化一个静态对象,所以本身就是线程安全的

(只要程序一运行,我就把这个对象创建好,并且就创建一次,等用的时候拿来用就可以)

懒汉单例模式

public class Singleton {

	

	/**

	 * 该函数限制用户主动创建实例

	 */

	private Singleton() {}

	/**

	*刚开始不创建

	*/

	private static Singleton singleton = null;

	

	/**

	 * 获取Singleton实例(也叫静态工厂方法)

	 * @return Singleton

	 */

	public static Singleton getSingleton() {

		/* 当singleton为空时创建它,反之直接返回,保证唯一性 */

		if(singleton == null){

			singleton = new Singleton();

		}

		return singleton;

	}

	

}

懒汉模式在并发情况下可能引起的问题

懒汉模式解决了饿汉模式可能引起的资源浪费问题,因为这种模式只有在用户要使用的时候才会实例化对象。但是这种模式在并发情况下会出现创建多个对象的情况。

因为可能出现外界多人同时访问Singleton.getSingleton()方法,这里可能会出现因为并发问题导致类被实例化多次,所以懒汉模式需要加上锁synchronized (Singleton.class) 来控制类只允许被实例化一次。

线程安全的懒汉单例模式

在getSingleton()添加synchronized同步

public class Singleton {

	

/**

	 * 该函数限制用户主动创建实例

	 */

	private Singleton() {}

	private static Singleton singleton = null;

	/**

	 * 获取Singleton实例,也叫静态工厂方法

	 * @return Singleton

	 */

	public static synchronized Singleton getSingleton(){

		if(singleton==null){

			singleton=new Singleton();

		}

		return singleton;

	}

	

}

```

这种方式效率比较低,性能不是太好,不过也可以用,因为是对整个方法加上了线程同步,其实只要在new的时候考虑线程同步就行了,这种方法不推荐使用。

双重检查锁定(DCL)Double Check Lock

public class Singleton {

	/**

	 * 该函数限制用户主动创建实例

	 */

	private Singleton() {}

	private volatile static Singleton singleton = null;

	/**

	 * 获取Singleton实例,也叫静态工厂方法

	 * @return Singleton

	 */

	public static Singleton getInstance() {

		if (singleton == null) {

			synchronized (Singleton.class) {

				if (singleton == null) {

					singleton = new Singleton();

				}

			}

		}

		return singleton;

	}

}

DCL模式的优点就是,只有在对象需要被使用时才创建,第一次判断 INSTANCE == null为了避免非必要加锁,当第一次加载时才对实例进行加锁再实例化。这样既可以节约内存空间,又可以保证线程安全。但是,由于jvm存在乱序执行功能,DCL也会出现线程不安全的情况。具体分析如下:

singleton  = new SingleTon();

这个步骤,其实在jvm里面的执行分为三步:

  • 1.在堆内存开辟内存空间。

  • 2.在堆内存中实例化SingleTon里面的各个参数。

  • 3.把对象指向堆内存空间。

由于jvm存在乱序执行功能,所以可能在2还没执行时就先执行了3,如果此时再被切换到线程B上,由于执行了3,INSTANCE 已经非空了,会被直接拿出来用,这样的话,就会出现异常。这个就是著名的DCL失效问题。

不过在JDK1.5之后,官方也发现了这个问题,故而具体化了volatile,即在JDK1.6及以后,只要定义为

private volatile static SingleTon  INSTANCE = null;

就可解决DCL失效问题。

volatile确保INSTANCE每次均在主内存中读取,这样虽然会牺牲一点效率,但也无伤大雅。

volatile关键字及其作用(补充)

1 保证内存可见性

可见性是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。

也就是一个线程修改的结果,另一个线程马上就能看到。

2 禁止指令重排

指令重排序是JVM为了优化指令、提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。指令重排序包括编译器重排序和运行时重排序。

示例说明:

double r = 2.1; //(1) 

double pi = 3.14;//(2) 

double area = pi*r*r;//(3)

虽然代码语句的定义顺序为1->2->3,

但是计算顺序1->2->3与2->1->3对结果并无影响,

所以编译时和运行时可以根据需要对1、2语句进行重排序。

饿汉单例模式

public class Singleton {

	/**

	 * 该函数限制用户主动创建实例

	 */

	private Singleton() {}

	private static final Singleton singleton = new Singleton();

	/**

	 * 获取Singleton实例,也叫静态工厂方法

	 * @return Singleton

	 */

	public static Singleton getInstance() {

		return singleton;

	}

}

但是这种浪费资源,不太推荐,因为一上来不管你用不用都创建一个对象。

静态内部类实现:

静态内部类比双重检查锁定和在getInstance()方法上加同步都要好,实现了线程安全又避免了同步带来的性能影响

public class SingleTon{

  private SingleTon(){}

 

  private static class SingleTonHoler{

     private static SingleTon INSTANCE = new SingleTon();

 }

 

  public static SingleTon getInstance(){

    return SingleTonHoler.INSTANCE;

  }

}

静态内部类的优点是:

外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。即当SingleTon第一次被加载时,并不需要去加载SingleTonHoler,只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE,第一次调用getInstance()方法会导致虚拟机加载SingleTonHoler类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

那么,静态内部类又是如何实现线程安全的呢?首先,我们先了解下类的加载时机。

类加载时机:

在class文件中的描述信息都需要加载到jvm才能运行和使用。

jvm的类加载机制:jvm把描述类的数据从class文件中加载到内存,并对数据进行校验,转换解析和初始化,最终形成被jvm使用的Java类型。

生命周期:加载->验证->准备->解析->初始化->使用->卸载

加载到初始化都是在程序的与运行期间完成的。

验证,准备,解析也叫连接过程,Java的特性是依赖在运行期动态加载和动态连接。

Java类加载 会初始化的情况有且仅有以下五种:(也称为主动引用)

1. 遇到new(用new实例对象),getStatic(读取一个静态字段),putstatic(设置一个静态字段),invokeStatic(调用一个类的静态方法)这四条指令字节码命令时

1. 使用Java.lang.reflect包的方法对类进行反射调用时,如果此时类没有进行init,会先init。

1. 当初始化一个类时,如果其父类没有进行初始化,先初始化父类

1. jvm启动时,用户需要指定一个执行的主类(包含main的类)虚拟机会先执行这个类

1. 当使用JDK1.7的动态语言支持的时候,当java.lang.invoke.MethodHandler实例后的结果是REF-getStatic/REF_putstatic/REF_invokeStatic的句柄,并且这些句柄对应的类没初始化的话应该首先初始。

注意:除以上5种方法外,所有引用类的方法都不会触发初始化,称为被动引用。

类加载的五个过程

1.加载

  1. 通过类的全限定名来获取类的二进制字节流(用户可操作,自定义类加载器(实现通过一个类的全限定名获取类的二进制字节流的动作放在jvm外部实现的模块))

  2. 将这个字节流所代表的静态存储结构转化为在方法区的运行时数据结构。

  3. 在内存中生成代表这个类的Java.lang.class类对象,作为这个数据访问的入口。

注:数组类本身不是通过类加载器创建,而是有jvm直接创建

2.验证: 防止危害jvm安全,目的是确保class文件中字节流中包含的信息符合当前虚拟机的要求。主要有四个方面的验证:

1、文件格式验证,是否以魔数开始,版本信息是否为jvm接受,常量池中是否有不支持的类型。

2、元数据验证:进行语法分析,是否每个类都有父类,是否有语法错误,是否继承自final。

3、字节码验证:语义分析,通过数据流和控制流分析,确保语义是合法的。

4、符号引用验证:是否能找到对应的类。发生在讲符号引用转化为直接引用时,在解析中产生。

3.准备:正式为类变量分配内存,在方法区中分配

注意:static+ final修饰的变量在准备阶段之后就是用户指定的值。

4.解析:将符号引用转化为直接引用(可选择)包括类,接口, 字段,方法的解析。

5.初始化:真正执行Java程序中的代码(字节码),是执行类构造器的过程,对类的静态变量和代码块执行初始化工作。

  1. <clinit>()方法是由于编译器自动收集类中所有类变量赋值,静态语句块合并产生的,顺序是语句在源文件中的顺序。

  2. 1. <clinit>方法与类的构造器不同,他不需要显示的调用父类的构造方法,因为jvm会保证在子类的clinit方法执行之前,

  3. 1. 父类已经执行了。所以jvm执行的clinit一定是object类。

  4. 1. 如果一个类或者接口中没有静态语句或者静态块,则可以没有clinit方法。

1. jvm会保证类的clinit方法加锁。

注意:静态语句块中只能访问定义在静态语句之前的变量,不能访问语句之后的,但可以为后边的变量赋值。

我们再回头看下getInstance()方法,调用的是SingleTonHoler.INSTANCE,取的是SingleTonHoler里的INSTANCE对象,跟上面那个DCL方法不同的是,getInstance()方法并没有多次去new对象,故不管多少个线程去调用getInstance()方法,取的都是同一个INSTANCE对象,而不用去重新创建。

当getInstance()方法被调用时,SingleTonHoler才在SingleTon的运行时常量池里,把符号引用替换为直接引用,这时静态对象INSTANCE也真正被创建,然后再被getInstance()方法返回出去,这点同饿汉模式。那么INSTANCE在创建过程中又是如何保证线程安全的呢?在《深入理解JAVA虚拟机》中,有这么一句话:

虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程 去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行<clinit>()方法后,其他线程唤醒之后不会再次进入<clinit>()方法。同一个加载器下,一个类型只会初始化一次。),在实际应用中,这种阻塞往往是很隐蔽的。

故而,可以看出INSTANCE在创建过程中是线程安全的,所以说静态内部类形式的单例可保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

那么,是不是可以说静态内部类单例就是最完美的单例模式了呢?其实不然,静态内部类也有着一个致命的缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去,例如Context这种参数,所以,我们创建单例时,可以在静态内部类与DCL模式里自己斟酌。

通过枚举实现单例模式

在effective java(这本书真的很棒)中说道,最佳的单例实现模式就是枚举模式。利用枚举的特性,让JVM来帮我们保证线程安全和单一实例的问题。除此之外,写法还特别简单。

public enum Singleton {

    INSTANCE;

    public void doSomething() {

        System.out.println("doSomething");

    }

}

调用

public class Main {

    public static void main(String[] args) {

        Singleton.INSTANCE.doSomething();

    }

}

直接通过Singleton.INSTANCE.doSomething()的方式调用即可。方便、简洁又安全。

0
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区