我是靠谱客的博主 整齐小蝴蝶,最近开发中收集的这篇文章主要介绍单例模式_学习,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

一,概念

1.1)什么是单例模式:

        单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

        这种模式涉及到一个单一的类,该类负责自己创建自己的对象,同时确保永远只有这一个对象被创建这个类提供了一种外界访问其唯一对象的方式,外界可以直接访问自己创建好的自身对象不能实例化该类的对象

1.2)单例模式的意图:

                保证一个类仅有一个实例并提供一个访问它的全局访问点

二,单例模式的特性:

  •         1、单例就是该类只能返回⼀个实例,即,单例类只能有一个实例。
  •         2、单例类必须自己创建自己的唯一实例
  •         3、单例类必须给所有其他对象提供这一实例

三,单例代码的特点:

单例代码所具备的特点:

  •                 1.私有化的构造函数
  •                 2.私有的静态全局变量,最终指向自己创建的自身对象
  •                 3.供外界访问自己创建的自身对象的、公有的静态方法

四,关键代码:

  1.         判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
  2.         构造函数是私有的。

五,代码实现——5种方式

        首先,我们将创建一个 SingleObject 类。

        SingleObject 类有它的私有构造函数本身的一个静态全局实例

        SingleObject 类提供了一个静态方法,供外界获取它的这个静态全局实例

5.1)懒汉式,线程不安全

        描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。  

        特点:这种方式 lazy loading懒加载 很明显。

        缺陷在多线程环境下不能正常工作!懒汉式存在致命的问题:当有多个线程并行调用 getInstance() 的时候,就会创建多个实例。也就是说在多线程下不能正常工作。

public class Singleton {
//私有的静态全局变量
private static Singleton instance;
//私有的构造方法
private Singleton (){}
//供外界访问获取类对象的函数:
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

自己编写:

public class SingleObject01 {
//私有的构造函数:
private SingleObject01() {
}
//提供外界访问的全局变量:
private static SingleObject01 singleObject01;
//供外界访问的方法:
public static SingleObject01 getInstance() {
if (singleObject01 == null) {
singleObject01= new SingleObject01();
}
return singleObject01;
}
/**
* 分析:
*
这种懒汉式的严格来讲,在多线程访问的情况下,都不是单例模式了,
*
因为如果有多个线程同时访问,那么得到的将是多个SingleObject01对象。
*/
}

5.1)懒汉式--加锁优化,线程安全 

        描述:这种方式具备很好的 lazy loading懒加载效果,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步

        缺点:虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效。因为在任何时候只能有一个线程调用 getInstance() 方法,会严重影响效率。

public class Singleton {
//私有的静态全局变量
private static Singleton instance;
//私有的构造函数
private Singleton (){}
//加锁之后的供外界访问获取类对象的方法
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

自己编写:

public class SingleObject02 {
//私有构造函数:
private SingleObject02() {
}
//私有的静态全局变量:
private static SingleObject02 singleObject02;
//供外界访问上边全局变量的方法:
public static synchronized SingleObject02 getInstance() {
if (singleObject02 == null) {
singleObject02 = new SingleObject02();
}
return singleObject02;
}
}

注意:

        但是同步操作的代码其实只在第一次调用时才被需要用到,因为只有在第一次的时候才创建了单例实例对象时。

        对于第二次、第三次等以后的调用,直接返回第一次的时候实例化好的对象即可。这就引出了双重检验锁。

 5.2)双重检验锁

        是否多线程安全:

        实现难度:较复杂

        描述:这种方式采用双重检验锁机制,安全且在多线程情况下能保持高性能。

public class Singleton {
//私有的静态全局变量
private static Singleton singleton;
//私有的构造函数
private Singleton (){}
//供外界访问获取类对象的方法:
public static Singleton getSingleton() {
//双重检验锁:
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}

这段代码看起来很完美,很可惜,它是有问题。主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。

  1. 给 instance 分配内存
  2. 调用 Singleton 的构造函数来初始化成员变量
  3. 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,就会报错

我们只需要将 instance 变量声明成 volatile 就可以了。

有些人认为使用 volatile 的原因是可见性,也就是可以保证线程在本地不会存有 instance 的副本,每次都是去主内存中读取。但其实是不对的。使用 volatile 的主要原因是其另一个特性:禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。

public class Singleton {
//使用volatile关键字修饰的私有的静态全局变量
private volatile static Singleton singleton;
//私有的构造函数
private Singleton (){}
//供外界访问获取类对象的静态方法
public static Singleton getSingleton() {
//双重检验锁:
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}

自己编写:

public class SingleObject04 {
//私有的构造函数:
private SingleObject04() {
}
//私有的静态全局变量:
private static volatile SingleObject04 singleObject04;
//供外界获取该类对象的方法:
public static SingleObject04 getInstance() {
if (singleObject04 == null) {
synchronized (SingleObject04.class) {
if (singleObject04 == null) {
singleObject04 = new SingleObject04();
}else{
return singleObject04;
}
}
}
return singleObject04;
}
}

5.3)饿汉式

        描述:这种方式比较常用,但容易产生垃圾对象。

        优点:没有加锁,执行效率会提高。

        缺点:类加载时就初始化,浪费内存。

        它基于 classloader机制避免了多线程的同步问题,不过,类对象在类加载时就实例化,虽然导致类加载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类加载,如果有其他的方式导致了类加载、从而初始化了 类对象instance ,显然没有达到 lazy loading懒加载的效果。

public class Singleton {
//私有的静态全局变量,指向类加载时就实例化好的类对象;
private static Singleton instance = new Singleton();
//私有的构造函数
private Singleton (){}
//供外界访问获取类对象的静态方法:
public static Singleton getInstance() {
return instance;
}
}

自己编写:

public class SingleObject03 {
//私有构造函数
private SingleObject03() {
}
//私有的静态全局变量,饿汉式--直接提前实例化好
private static SingleObject03 singleObject03 = new SingleObject03();
//供外界访问的方法:
public static SingleObject03 getInstance() {
return singleObject03;
}
}

·

5.4)静态内部类

是否多线程安全:

实现难度:一般

描述:这种方式能达到双检锁方式一样的功效,但实现更简单。这种方法也是《Effective Java》上所推荐的。

这种写法由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,保证了线程安全问题,没有性能缺陷;

示例代码:

public class Singleton {
//静态内部类:
private static class SingletonHolder {
//供外界访问的静态常量:
private static final Singleton INSTANCE = new Singleton();
}
//私有的构造函数:
private Singleton (){}
//供外界访问获取类对象的方法:
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

自己编写:

public class SingleObject05 {
//私有的构造函数:
private SingleObject05() {
}
//将静态全局变量放到静态内部类里边:
private static class SingleObject05Inner{
private static final SingleObject05 SINGLE_OBJECT_05=new SingleObject05();
}
//供外界访问的方法:
public static SingleObject05 getInstance() {
return SingleObject05Inner.SINGLE_OBJECT_05;
}
}

·

为什么静态内部类能够实现单例模式,且保证了线程安全?

原文链接:

静态内部类为何能够实现单例?_小猪快跑22的博客-CSDN博客_为什么静态内部类可以实现单例

【JVM】为什么静态内部类实现单例模式是线程安全?_cafe-BABE的博客-CSDN博客_静态内部类为什么是线程安全的

静态内部类的优点是

        外部类加载时并不需要立即加载内部类,内部类不被加载则不会去创建实例对象,故而不占内存。

        具体来说当单例类第一次被加载时,并不需要去加载静态内部类,只有当getInstance()方法第一次被调用时,才会导致虚拟机加载静态内部类。

        这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例对象的实例化。

为什么可以保证线程安全?

       首先要了解类加载过程中的最后一个阶段:即类的初始化,类的初始化阶本质就是执行类构造器中的<clinit>方法

        <clinit>方法:这不是由程序员写的程序,而是根据代码由javac编译器生成的。<clinit>方法它是由类里面 所有的静态变量的赋值动作 和 静态代码块 组成的。

         JVM内部会保证一个类的<clinit>方法在多线程环境下被正确的加锁同步,也就是说如果多个线程同时去进行“类的初始化”,那么只有一个线程会去执行类的<clinit>方法,其他的线程都要阻塞等待,直到这个线程执行完<clinit>方法。

        然后执行完<clinit>方法后,其他线程唤醒,但是不会再进入<clinit>()方法。也就是说同一个加载器下,加载一个类的时候,只会执行一次<clinit>方法

        那么回到代码中,这里的静态变量的赋值操作实际上就是一个<clinit>方法中的代码,当我们执行getInstance方法的时候,才会导致静态内部类的加载(然后才会创建实例对象)

        而类加载的最后一步才会执行类的初始化,而且即使在多线程情况下,这个类的初始化的<clinit>代码也只会被执行一次,所以该类始终只会有一个实例对象

        那么再增加一句,之所以这里变量定义的时候不需要volatile,因为只有一个线程会执行具体的类的初始化代码<clinit>,也就是即使有指令重排序,因为根本没有第二个线程给你去影响,所以无所谓。

·

详情参见:

静态内部类的单例模式如何保证线程安全_Morning sunshine的博客-CSDN博客
 ·

5.5)枚举

        描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。

public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}

·

六,建议

        一般情况下,不建议使用第 1 种非线程安全的懒汉式单例和第 2 种线程安全的懒汉方式,建议使用第 3 种饿汉方式

        只有在明确必须要实现 lazy loading 懒加载效果时,才使用第四种双重检验锁方式第 5 种静态内部类方式。

        一般情况下直接使用饿汉式就好了,如果明确要求要懒加载(lazy initialization)会倾向于使用静态内部类,如果涉及到反序列化创建对象时会试着使用枚举的方式来实现单例。

PS:什么是 lazy loading 懒加载?

        懒加载其实就是延时加载,即当对象需要用到的时候再去加载。

        什么叫做需要用到的时候?比如说一个对象被创建出来就需要一笔内存开支,如果接下来就没有其他的操作,那可以认为这个对象创建得“过早”了。

        好处:内存占用小。

最后

以上就是整齐小蝴蝶为你收集整理的单例模式_学习的全部内容,希望文章能够帮你解决单例模式_学习所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(33)

评论列表共有 0 条评论

立即
投稿
返回
顶部