前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java设计模式(一)-单例模式

Java设计模式(一)-单例模式

作者头像
joshua317
发布2021-11-24 14:26:29
7590
发布2021-11-24 14:26:29
举报
文章被收录于专栏:技术博文技术博文

一、单例模式介绍

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

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。类的构造函数是私有的,并且具有自身的静态实例。单例类一般情况只想内部保留一个实例对象,所以会选择将构造函数声明为私有的,这才使得单例类无法被继承。

1.1 单例模式的核心要素

(1)单例类只能有一个实例。

(2)单例类必须自己创建自己的唯一实例。

(3)单例类必须给所有其他对象提供这一实例。

1.2 单例模式的应用及实现说明

(1)意图: 保证一个类仅有一个实例,并提供一个访问它的全局访问点。

(2)主要解决: 一个全局使用的类频繁地创建与销毁。

(3)何时使用: 控制实例数目,节省系统资源的时候。

(4)如何解决: 判断系统是否已经有这个单例,如果有则返回,如果没有则创建。

(5)关键代码: 构造函数是私有的;提供一个获得该实例的对外方法。

1.3 单例模式实现的思路

(1)一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称);

(2)当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;

(3)同时我们还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。

1.4 单例模式优缺点

优点:

(1)在内存里只有一个实例,所以能够节约系统资源,减少了内存的开销,尤其是频繁的创建和销毁实例,可以提高系统效率,同时也能够严格控制客户对它的访问。

(2)避免对资源的多重占用。

缺点:

也正是因为系统中只有一个实例,这样就导致了单例类的职责过重,违背了“单一职责原则”,同时也没有抽象类,这样扩展起来有一定的困难。

1.5 单例模式线程安全问题

单例模式 在多线程的应用场合下必须小心使用。如果当唯一实例尚未创建时,有两个线程同时调用创建方法,那么它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例,这样就有两个实例被构造出来,从而违反了 单例模式 中实例唯一的原则。 解决这个问题的办法是 为类是否已经实例化的变量提供一个互斥锁 (虽然这样会降低效率)。

二、单例模式应用场景

(1)无状态工具类:这种工具类不需要记录状态,只保证正确的应用就行,可以通过单例模式来定义。比如项目中用于读取配置文件的类

(2)数据共享:即多个不相关的两个线程或者进程之间实现通信。因为是一个实例,如果它的属性或者变量值被修改,所有引用都是同时修改的,当然需要 volatile 来定义变量。比如网站的计数器。

(3)日志应用:通常应用会向日志文件写日志信息,为了实时向文件写,通常会使用单例模式,保证有一个实例持有文件,然后进行操作。

(4)数据库连接池:数据库连接是一种数据库资源,使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,通过单例模式来维护,就可以大大降低这种损耗。

(5)Web应用的配置对象:读取文件需要消耗时间,如果读取大文件,消耗的时间和资源更久,所以通过单例模式可以大大降低消耗。

(6)生产唯一序列号:当系统需要持续不断地产生唯一序列号时,为避免频繁创建对象,所以通过单例模式可以大大降低消耗。

(7)Spring中,每个Bean默认都是单例的,这样便于Spring容器进行管理

三、单例模式实现方式

常见的单例模式实现方式有五种:饿汉式懒汉式双重检测锁式静态内部类式枚举单例。而在这五种方式中饿汉式懒汉式又最为常见。

3.1 单例模式实现一:饿汉式

饿汉式:线程安全,调用效率高,但是不能延时加载。由于该模式在加载类的时候对象就已经创建了,所以加载类的速度比较慢,但是获取对象的速度比较快,且是线程安全的。这种方式比较常用,但容易产生垃圾对象。优点就是没有加锁,执行效率会提高。缺点是类加载时就初始化,浪费内存。它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到延迟加载的效果。

代码语言:javascript
复制
/**
 * 单例模式实现一-饿汉式
 */
class Singleton1 {
    /**
     * 1.直接创建对象,定义静态属性,类初始化时,立即加载这个对象
     */
    public static Singleton1 instance = new Singleton1();
    /**
     * 2.私有化构造函数
     */
    private Singleton1()
    {
        System.out.println("单例模式实现一-饿汉式");
    }

    /**
     * 3.提供对外方法,返回对象实例
     * @return Singleton1
     */
    public static Singleton1 getInstance()
    {
        return instance;
    }

    /**
     * 两数相加之和方法
     * @param a
     * @param b
     * @return int
     */
    public int add(int a, int b)
    {
        return a + b;
    }
}

3.2 单例模式实现二:懒汉式

懒汉式实现又分为线程不安全和线程安全

3.2.1 线程不安全

这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。这种方式延迟加载很明显,所以加载类比较快,但是对象的获取速度相对较慢,且线程不安全,在多线程不能正常工作。

代码语言:javascript
复制
/**
 * 单例模式实现二-懒汉式,线程不安全
 */
class Singleton2 {
    /**
     * 1.定义静态属性
     */
    private static Singleton2 instance;

    /**
     * 2.私有化构造函数
     */
    private Singleton2()
    {
        System.out.println("单例模式实现二-懒汉式,线程不安全");
    }

    /**
     * 3.提供对外方法,返回对象实例,运行时加载对象
     * @return Singleton2
     */
    public static Singleton2 getInstance()
    {
        if (instance == null) {
            instance = new Singleton2();
        }
        return instance;
    }

    /**
     * 两数相加之和方法
     * @param a
     * @param b
     * @return
     */
    public int add(int a, int b)
    {
        return a + b;
    }
}
3.2.2 线程安全

所以如果想要线程安全的话可以加上synchronized关键字,但是这样会付出惨重的效率代价。

这种方式具备很好的延迟加载,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。 优点是第一次调用才初始化,避免内存浪费。缺点是必须加锁 synchronized 才能保证单例,但加锁会影响效率。

代码语言:javascript
复制
/**
 * 单例模式实现二-懒汉式,线程安全
 */
class Singleton3 {
    /**
     * 1.定义静态属性
     */
    private static Singleton3 instance;

    /**
     * 2.私有化构造函数
     */
    private Singleton3()
    {
        System.out.println("单例模式实现二-懒汉式,线程安全");
    }

    /**
     * 3.提供对外方法,返回对象实例,运行时加载对象
     * @return Singleton3
     */
    public static synchronized Singleton3 getInstance()
    {
        if (instance == null) {
            instance = new Singleton3();
        }
        return instance;
    }

    /**
     * 两数相加之和方法
     * @param a
     * @param b
     * @return
     */
    public int add(int a, int b)
    {
        return a + b;
    }
}

3.3 单例模式实现三:双重校验锁

双检锁/双重校验锁(DCL,即 double-checked locking),这种方式采用双锁机制,安全且在多线程情况下能保持高性能。

代码语言:javascript
复制
/**
 * 单例模式实现三-双重校验锁
 */
class Singleton4 {
    /**
     * 1.定义静态属性
     */
    private volatile static Singleton4 instance;

    /**
     * 2.私有化构造函数
     */
    private Singleton4()
    {
        System.out.println("单例模式实现三-双重校验锁");
    }

    /**
     * 3.提供对外方法,返回对象实例,运行时加载对象
     * @return Singleton4
     */
    public static synchronized Singleton4 getInstance()
    {
        if (instance == null) {
            synchronized (Singleton4.class) {
                if (instance == null) {
                    instance = new Singleton4();
                }
            }
        }
        return instance;
    }

    /**
     * 两数相加之和方法
     * @param a
     * @param b
     * @return
     */
    public int add(int a, int b)
    {
        return a + b;
    }
}

双重校验锁拓展:

(1)为什么加了同步锁之后还需要二次判空? 因为如果不二次判空那么有可能会出现以下情况:

Thread 1

Thread 2

第一次判定instance为空

第一次判定instance为空

获取锁

等待1释放锁

初始化instance

-

获取到锁,初始化instance

这样的话instance就会被初始化两次,所以在获取到锁后还需要进行二次判空。

(2)为什么要使用volatile关键字? 因为java初始化时有可能会进行指令重排

指令重排: 一般而言初始化操作并不是一个原子操作,而是分为三步:

  1. 在堆中开辟对象所需空间,分配地址;
  2. 根据类加载的初始化顺序进行初始化;
  3. 将内存地址返回给栈中的引用变量。

由于 Java 内存模型允许“无序写入”,有些编译器因为性能原因,可能会把上述步骤中的 2 和 3 进行重排序,顺序就成了

  1. 在堆中开辟对象所需空间,分配地址;
  2. 将内存地址返回给栈中的引用变量(此时变量已不在为null,但是变量却并没有初始化完成);
  3. 根据类加载的初始化顺序进行初始化。

所以就可能会出现以下情况:

Thread 1

Thread 2

第一次检测, instance 为空

获取锁

再次检测, instance 为空

在堆中分配内存空间

instance 指向分配的内存空间

第一次检测,instance不为空

访问 instance(此时对象还未初始化完成)

加入volatile关键字修饰之后,会禁用指令重排,这样就保证了线程同步。

3.4 单例模式实现四:登记式/静态内部类

这种方式能达到双重校验锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双重校验锁方式。这种方式只适用于静态域的情况,双重校验锁方式可在实例域需要延迟初始化时使用。 这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟第饿汉式不同的是:饿汉式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到延迟加载效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比饿汉式就显得很合理。

代码语言:javascript
复制
/**
 * 单例模式实现四-登记式/静态内部类
 */
class Singleton5 {
    /**
     * 1.显式装载 SingletonHolder类
     */
    private static class SingletonHolder {
        private static final Singleton5 instance = new Singleton5();
    }

    /**
     * 2.私有化构造函数
     */
    private Singleton5()
    {
        System.out.println("单例模式实现四-登记式/静态内部类");
    }

    /**
     * 3.提供对外方法,返回对象实例,运行时加载对象
     * @return Singleton5
     */
    public static synchronized Singleton5 getInstance()
    {
        return SingletonHolder.instance;
    }

    /**
     * 两数相加之和方法
     * @param a
     * @param b
     * @return
     */
    public int add(int a, int b)
    {
        return a + b;
    }
}

3.5 单例模式实现五:枚举

这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。利用枚举的特性,让JVM来帮我们保证线程安全和单一实例的问题。除此之外,写法还特别简单、更简洁,自动支持序列化机制,绝对防止多次实例化。这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。

代码语言:javascript
复制
/**
 * 单例模式实现五-枚举
 */
enum Singleton6 {
    INSTANCE;

    private Singleton6() {
        System.out.println("单例模式实现五-枚举");
    }

    /**
     * 两数相加之和方法
     * @param a
     * @param b
     * @return
     */
    public int add(int a, int b)
    {
        return a + b;
    }
}

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

注意:一般情况下,不建议使用懒汉方式,建议使用饿汉方式。只有在要明确实现延迟加载效果时,才会使用登记方式。如果涉及到反序列化创建对象时,可以尝试使用枚举方式。如果有其他特殊的需求,可以考虑使用双重校验锁方式。

四、完整demo

代码语言:javascript
复制
package com.joshua317;

public class Main {

    public static void main(String[] args) {
        Singleton1 instance0 = Singleton1.getInstance();
        Singleton1 instance1 = Singleton1.getInstance();

        Singleton2 instance2 = Singleton2.getInstance();
        Singleton3 instance3 = Singleton3.getInstance();
        Singleton4 instance4 = Singleton4.getInstance();
        Singleton5 instance5 = Singleton5.getInstance();
        Singleton6 instance6 = Singleton6.INSTANCE;


        int add1 = instance1.add(1, 2);
        int add2 = instance2.add(1, 2);
        int add3 = instance3.add(1, 2);
        int add4 = instance4.add(1, 2);
        int add5 = instance5.add(1, 2);

        System.out.println("add1的值为:" + add1);
        System.out.println("add2的值为:" + add2);
        System.out.println("add3的值为:" + add3);
        System.out.println("add4的值为:" + add4);
        System.out.println("add5的值为:" + add5);
        //单例模式-枚举实现调用
        System.out.println("add6的值为:" + instance6.add(1,2));
        System.out.println("add6的值为:" + Singleton6.INSTANCE.add(1,2));

        System.out.println("instance0和instance1是否相等:" + (instance0 == instance1));
    }
}

/**
 * 单例模式实现一-饿汉式
 */
class Singleton1 {
    /**
     * 1.直接创建对象,定义静态属性,类初始化时,立即加载这个对象
     */
    public static Singleton1 instance = new Singleton1();
    /**
     * 2.私有化构造函数
     */
    private Singleton1()
    {
        System.out.println("单例模式实现一-饿汉式");
    }

    /**
     * 3.提供对外方法,返回对象实例
     * @return Singleton1
     */
    public static Singleton1 getInstance()
    {
        return instance;
    }

    /**
     * 两数相加之和方法
     * @param a
     * @param b
     * @return int
     */
    public int add(int a, int b)
    {
        return a + b;
    }
}

/**
 * 单例模式实现二-懒汉式,线程不安全
 */
class Singleton2 {
    /**
     * 1.定义静态属性
     */
    private static Singleton2 instance;

    /**
     * 2.私有化构造函数
     */
    private Singleton2()
    {
        System.out.println("单例模式实现二-懒汉式,线程不安全");
    }

    /**
     * 3.提供对外方法,返回对象实例,运行时加载对象
     * @return Singleton2
     */
    public static Singleton2 getInstance()
    {
        if (instance == null) {
            instance = new Singleton2();
        }
        return instance;
    }

    /**
     * 两数相加之和方法
     * @param a
     * @param b
     * @return
     */
    public int add(int a, int b)
    {
        return a + b;
    }
}

/**
 * 单例模式实现二-懒汉式,线程安全
 */
class Singleton3 {
    /**
     * 1.定义静态属性
     */
    private static Singleton3 instance;

    /**
     * 2.私有化构造函数
     */
    private Singleton3()
    {
        System.out.println("单例模式实现二-懒汉式,线程安全");
    }

    /**
     * 3.提供对外方法,返回对象实例,运行时加载对象
     * @return Singleton3
     */
    public static synchronized Singleton3 getInstance()
    {
        if (instance == null) {
            instance = new Singleton3();
        }
        return instance;
    }

    /**
     * 两数相加之和方法
     * @param a
     * @param b
     * @return
     */
    public int add(int a, int b)
    {
        return a + b;
    }
}

/**
 * 单例模式实现三-双重校验锁
 */
class Singleton4 {
    /**
     * 1.定义静态属性
     */
    private volatile static Singleton4 instance;

    /**
     * 2.私有化构造函数
     */
    private Singleton4()
    {
        System.out.println("单例模式实现三-双重校验锁");
    }

    /**
     * 3.提供对外方法,返回对象实例,运行时加载对象
     * @return Singleton4
     */
    public static synchronized Singleton4 getInstance()
    {
        if (instance == null) {
            synchronized (Singleton4.class) {
                if (instance == null) {
                    instance = new Singleton4();
                }
            }
        }
        return instance;
    }

    /**
     * 两数相加之和方法
     * @param a
     * @param b
     * @return
     */
    public int add(int a, int b)
    {
        return a + b;
    }
}

/**
 * 单例模式实现四-登记式/静态内部类
 */
class Singleton5 {
    /**
     * 1.显式装载 SingletonHolder类
     */
    private static class SingletonHolder {
        private static final Singleton5 instance = new Singleton5();
    }

    /**
     * 2.私有化构造函数
     */
    private Singleton5()
    {
        System.out.println("单例模式实现四-登记式/静态内部类");
    }

    /**
     * 3.提供对外方法,返回对象实例,运行时加载对象
     * @return Singleton5
     */
    public static synchronized Singleton5 getInstance()
    {
        return SingletonHolder.instance;
    }

    /**
     * 两数相加之和方法
     * @param a
     * @param b
     * @return
     */
    public int add(int a, int b)
    {
        return a + b;
    }
}

/**
 * 单例模式实现五-枚举
 */
enum Singleton6 {
    INSTANCE;

    private Singleton6() {
        System.out.println("单例模式实现五-枚举");
    }

    /**
     * 两数相加之和方法
     * @param a
     * @param b
     * @return
     */
    public int add(int a, int b)
    {
        return a + b;
    }
}
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021-11-22 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.1 单例模式的核心要素
  • 1.2 单例模式的应用及实现说明
  • 1.3 单例模式实现的思路
  • 1.4 单例模式优缺点
  • 1.5 单例模式线程安全问题
  • 二、单例模式应用场景
  • 三、单例模式实现方式
    • 3.1 单例模式实现一:饿汉式
      • 3.2 单例模式实现二:懒汉式
        • 3.2.1 线程不安全
        • 3.2.2 线程安全
      • 3.3 单例模式实现三:双重校验锁
        • 3.4 单例模式实现四:登记式/静态内部类
          • 3.5 单例模式实现五:枚举
          • 四、完整demo
          相关产品与服务
          文件存储
          文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档