--设计模式--五种实现单例模式的方式

15 篇文章 0 订阅
2 篇文章 0 订阅

■ 章节目录

■ 前言

  • 由于涉及到挺多关于线程这一块的知识,如果自己线程相关知识不是很清楚的话,可以去看看这篇文章噢👉:关于线程你想知道的都在这

■ 什么是单例模式?

单例模式(Singleton Pattern)是 Java中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

注意
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。

再介绍

单例模式
意图保证一个类仅有一个实例,并提供一个访问它的全局访问点
主要解决一个全局使用的类频繁地创建与销毁。
何时使用当您想控制实例数目,节省系统资源的时候。
如何解决判断系统是否已经有这个单例,如果有则返回,如果没有则创建
关键代码构造函数是私有的。

① 应用场景实例

1、Windows的Task Manager(任务管理器)就是很典型的单例模式(这个很熟悉吧),想想看,是不是呢,你能打开两个windows task manager吗? 不信你自己试试看哦~
2、windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
3、网站的计数器,一般也是采用单例模式实现,否则难以同步。
4、应用程序的日志应用,一般都是用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
5、数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。
6、多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。
7、J2EE中的ServletContext,ServletContextConfig等;在Spring中,每个Bean默认是单例;Spring中的ApplicationContext、数据库连接池等

② 模式优点

  • 在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就 防止其它对象对自己的实例化,确保所有的对象都访问一个实例
  • 避免对共享资源的多重占用。(比如写文件操作)。
  • 提供了对唯一实例的受控访问
  • 由于在系统内存中只存在一个对象,因此可以节约系统资源,当需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能

③ 模式缺点

  • 不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
  • 由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。

④ 注意事项

  • 使用时不能用反射模式创建单例,否则会实例化一个新的对象 。
  • 使用懒单例模式时注意线程安全问题

■ 单例模式的五种实现方式

饿汉式

以空间换时间,没有实现延迟加载,线程安全

懒汉式

以时间换空间,实现了延迟加载,加了synchronized后线程安全

双重检测锁式

实现了延迟加载和线程安全,使用双重检查加锁的方法,使得性能不会有太大的影响,但会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高,线程安全

静态内部类式

线程安全,调用效率高。可以延时加载

使用枚举

线程安全、调用效率高,不能延时加载

饿汉式代码实现

//线程1用来实例对象
class Thread1 implements Runnable{

    @Override
    public void run() {
        for (int i=0;i<3;i++){
            Singleton_Hungry instance1 = Singleton_Hungry.getInstance();
            Singleton_Hungry instance2 = Singleton_Hungry.getInstance();
            System.out.println("我是Thread1,他们地址返回"+(instance1 == instance2));
        }
    }
}
//线程2再用来实例对象
class Thread2 implements Runnable{

    @Override
    public void run() {
        for (int i=0;i<3;i++){
            Singleton_Hungry instance1 = Singleton_Hungry.getInstance();
            Singleton_Hungry instance2 = Singleton_Hungry.getInstance();
            System.out.println("我是Thread2,他们地址返回"+(instance1 == instance2));
        }
    }
}
public class HungryMan {
    public static void main(String[] args) {
        // 启动两个线程,线程1,2都实例Singleton_Hungry10次,实例Singleton_Hungry10次
        Thread thread = new Thread(new Thread1());
        Thread thread1 = new Thread(new Thread2());
        thread.start();
        thread1.start();

        // 再测试单例模式
        Singleton_Hungry instance1 = Singleton_Hungry.getInstance();
        Singleton_Hungry instance2 = Singleton_Hungry.getInstance();
        // 两者的地址相等,对外是同一个对象
        System.out.println("我是main,他们地址返回"+(instance1 == instance2));
    }
}
class Singleton_Hungry{
    private static Singleton_Hungry instance = new Singleton_Hungry();

    // 私有构造函数,使得在其它方法上不能被调用,必须通过内部方法进行实例化
    private Singleton_Hungry(){
        System.out.println("我是饿汉模式");
    }

    /* 提供一个对外方法,通过此方法可以获得该类的一个实例
       这里不需要加synchronized,因为创建对象时,此类是在类加载器中加载的,具备天然的线程安全
	   而且不加synchronized可以提高效率
    * */
    public static /*synchronized*/ Singleton_Hungry getInstance(){
        return instance;
    }
}

打印结果:

我是饿汉模式
我是Thread1,他们地址返回true
我是Thread2,他们地址返回true
我是Thread2,他们地址返回true
我是Thread2,他们地址返回true
我是main,他们地址返回true
我是Thread1,他们地址返回true
我是Thread1,他们地址返回true

从结果可以看出,不管是在哪个位置,只要Singleton_Hungry被创建了,不管被创建几次,都是只有一个实例。
注意

  • 这种方式的缺点是如果这个对象没有被调用,就会浪费内存

懒加载代码实现

暂时没用线程,线程在下面聊到

在这里插入代码片public class Lazy_load {
    public static void main(String[] args) {
        // 测试单例模式
        Lazy_model instance1 = Lazy_model.getInstance();
        Lazy_model instance2 = Lazy_model.getInstance();
        // 两者的地址相等,对外是同一个对象
        System.out.println("我是main,他们地址返回"+(instance1 == instance2));
    }
}
class Lazy_model {
    private static Lazy_model instance;

    // 私有构造函数,使得在其它方法上不能被调用,必须通过内部方法进行实例化
    private Lazy_model() {
        System.out.println("我是懒加载模式");
    }

    /*先不加同步锁synchronized也可以达到效果,在本程序中
    * */
    public static /*synchronized*/ Lazy_model getInstance() {
        if (instance==null){
            instance = new Lazy_model();
        }
        return instance;
    }
}

打印结果如下:

我是懒加载模式
我是main,他们地址返回true

对比饿汉模式,实际上只改了这块:
在这里插入图片描述
乍一看好像懒加载和饿汉其实一样,怎么才突出这个字呢?来,我们对两个模式的代码进行修改,我都在其中增加了print()打印这个函数:
在这里插入图片描述
在这里插入图片描述
然后打印结果分别为:
饿汉

我是饿汉模式
我被打印了
我被打印了
我是main,他们地址返回true

懒加载

我被打印了
我是懒加载模式
我被打印了
我是main,他们地址返回true

我们看到在饿汉模式下,明明在return前执行的print()方法,居然在构造函数打印语句的后边,所以这就是懒加载,只有真正要的时候才会调用构造函数进行初始化,判断变量是否为null,为null在创建对象。但这种方式的问题是多线程环境中线程不安全,一个线程判断为null创建对象时,还没初始化时,另一个线程判断也为null,此时就会创建多个实例,例如下面这个代码👇:

// 线程3用来创建Lazy_model实例
class Thread3 extends Thread{
    @Override
    public void run() {
        Lazy_model instance1 = Lazy_model.getInstance();
        System.out.println("在线程中的地址:"+instance1.toString());
    }
}
public class Lazy_load {
    public static void main(String[] args) {
        // 启动线程,同时对Lazy_model进行实例化
        Thread t1 = new Thread3();
        Thread t2 = new Thread3();
        t1.start();
        t2.start();

        // 测试单例模式
        Lazy_model instance1 = Lazy_model.getInstance();
        Lazy_model instance2 = Lazy_model.getInstance();
        // 两者的地址相等,对外是同一个对象
        System.out.println("得到两个地址分别为:"+'\n'+instance1.toString() +'\n'+instance2.toString());
    }
}
class Lazy_model {
    private static Lazy_model instance;

    // 私有构造函数,使得在其它方法上不能被调用,必须通过内部方法进行实例化
    private Lazy_model() {
    }
    private static void print(){
        System.out.println("我被打印了");
    }

    /*没加同步锁synchronized
    * */
    public static /*synchronized*/ Lazy_model getInstance() {
//        print();
        if (instance==null){
            instance = new Lazy_model();
        }
        return instance;
    }
}

我们看看打印结果:

在线程中的地址:Lazy_model@7510bd5c
得到两个地址分别为:
Lazy_model@4554617c
Lazy_model@4554617c

在线程中的地址:Lazy_model@7aa3aa20

可以看到,没加synchronized打印出来的地址不一样!!!
我们看看加了synchronized(同步锁)后的代码和结果:
在这里插入图片描述

在线程中的地址:Lazy_model@4554617c
得到两个地址分别为:
Lazy_model@4554617c
Lazy_model@4554617c
在线程中的地址:Lazy_model@4554617c

可以看到,地址都是一样的!!!所以要使用懒加载一定要加上synchronized修饰!!
注意

  • 使用被synchronized修饰的方法后,多个线程同时访问这个方法时会阻塞,性能很低
    所以要来介绍第三种方式…

双重检测锁代码实现

class Thread4 extends Thread{
    @Override
    public void run() {
        DoubleCheckImpl doubleCheck = DoubleCheckImpl.getInstance();
        // 打印实例地址
        System.out.println(doubleCheck.toString());
    }
}
public class DoubleCheck {
    public static void main(String[] args) {
        //启动了三个线程
        Thread t1 = new Thread4();
        Thread t2 = new Thread4();
        Thread t3 = new Thread4();
        t1.start();
        t2.start();
        t3.start();
    }
}
class DoubleCheckImpl{

    /* 添加了volatile关键字,被volatile关键字修饰的变量,如果值发生了变更,其他线程立马可见,避免出现脏读的现象
       这里不加也可以,不过volatile变量通常能够减少同步的性能开销
    * */
    private volatile static DoubleCheckImpl instance;

    private DoubleCheckImpl(){

    }
    // 公共可调用方法
    public static DoubleCheckImpl getInstance(){
        // 首先判断是否有创建实例,有可能会有线程同时通过这个instance==null
        if (instance==null){
            // 然后给下面这个方法上锁,将同步内容放到下边if下
            synchronized (DoubleCheckImpl.class){
                // 然后这里就变成了上边通行过来的两个线程在这排队,先是A线程通过
                if (instance==null){
                    //A 线程里的instance没有实例,为null,故执行下边的实例方法
                    instance = new DoubleCheckImpl();
                }
                /*因为instance已经被A线程实例了,这时instance不是null了,
                所以还排在上边的线程B再在判断instance是否为null时就是false了
                * */
            }
        }
        return instance;
    }
}

打印结果:
DoubleCheckImpl@3d3e384b
DoubleCheckImpl@3d3e384b
DoubleCheckImpl@3d3e384b

三个地址一样!大家留意一下我注释的内容,写的挺详细的应该嘻嘻^^
在这里插入图片描述
在这里插入图片描述
如果不进行双重检测会怎么样呢?
在这里插入图片描述
打印结果:

DoubleCheckImpl@3d6f32ba
DoubleCheckImpl@3c494859
DoubleCheckImpl@3c494859

果然,有线程一起通过了第一个if判断,然后就实例了两个出来…
注意

  • 将对象声明为volatitle后,重排序在多线程环境中将会被禁止,所以使得线程的执行结果顺序不会改变,不会出行线程访问到的是一个还未初始化的对象(使用volatile的好处)。

静态内部类代码实现(也是懒加载的一种方式)

class Thread5 extends Thread{
    @Override
    public void run() {
        TestInner testInner = TestInner.getInstance();
        System.out.println("在线程中的地址:"+testInner.toString());
    }
}
public class StaticInner {
    public static void main(String[] args) {
        // 有三个线程同时实例
        Thread t1 = new Thread5();
        Thread t2 = new Thread5();
        Thread t3 = new Thread5();

        t1.start();
        t2.start();
        t3.start();

//        TestInner testInner = TestInner.getInstance();
//        TestInner testInner1 = TestInner.getInstance();
//        System.out.println(testInner==testInner1);
    }
}

class TestInner{
    // 声明静态内部类,类中有一个获取实例的属性testInner
    private static class InstanceClass{
        private static TestInner testInner = new TestInner();
    }
    private TestInner(){

    }
    // 测试是否为像懒加载模式那样延迟加载,发现下面代码先执行,所以是懒加载的一种
    private static void print(){
        System.out.println("我被打印了");
    }

    public static /*synchronized*/ TestInner getInstance(){
        print();
        // 返回静态内部类中的testInner属性
        return InstanceClass.testInner;
    }
}

打印结果:

我被打印了
我被打印了
我被打印了
在线程中的地址:TestInner@70d192f2
在线程中的地址:TestInner@70d192f2
在线程中的地址:TestInner@70d192f2

三个线程的地址都相同,说明没有实例化多个。
注意

  • 只有真正调用getInstance(),才会加载静态内部类,所以说是懒加载的一种方式
  • 兼备了并发高效调用延迟加载的优势。
  • 似乎静态内部类看起来已经是最完美的方法了,其实不是,可能还存在反射攻击或者反序列化攻击

枚举代码实现

class Thread6 extends Thread{
    @Override
    public void run() {
        Singleton singleton1 = Singleton.INSTANCE;
        System.out.println(singleton1);
    }
}
public class EnumImpl {
    public static void main(String[] args) {

		// 启动三个线程
        Thread t1 = new Thread6();
        Thread t2 = new Thread6();
        Thread t3 = new Thread6();
        t1.start();
        t2.start();
        t3.start();

//        Singleton singleton1 = Singleton.INSTANCE;
//        Singleton singleton2 = Singleton.INSTANCE;
//        System.out.println(singleton1==singleton2);
    }
}

enum Singleton{
    INSTANCE("实例",1);
    private int code;
    private String name;

    public int getCode(){
        return code;
    }
    public String getName(){
        return name;
    }

    Singleton(String name, int code){
        this.name = name;
        this.code = code;
    }
}

运行结果:

INSTANCE
INSTANCE
INSTANCE

♦ 总结

用一个表格来概括他们的优缺点吧👇:

优点缺点
饿汉式简单方便 ,线程安全静态对象在类加载时就要生成,会降低应用的启动速度存在反射攻击的风险
懒加载延迟加载,可以提高应用的启动速度线程不安全,多线程下需要加synchronized锁,降低性能
双重检测锁懒加载,线程安全,效率高代码比较复杂
静态内部类实现简单,懒加载,线程安全存在反射攻击的风险
枚举线程安全,不用担心序列化和反射问题枚举占用的内存会多一些没有延迟加载

所以根据自己的需求,如是否需要延迟加载,是否需要提示性能,是否需要防范反射攻击等来选择适合的单例模式。

——————————————————————————————————————————————————————————————
一起加油吧❤!!!

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值