设计模式-------【单例设计模式】

本文深入解析单例模式的概念,对比静态类的区别,探讨其在多线程环境下的实现技巧,包括饿汉式、懒汉式及双重检查锁定等,并介绍静态内部类实现的线程安全单例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 什么是单例模式

单例模式指的是在应用整个生命周期内只能存在一个实例。

单例模式是一种被广泛使用的设计模式。他有很多好处,能够避免实例对象的重复创建,减少创建实例的系统开销,节省内存。

2. 单例模式和静态类的区别


首先理解一下什么是静态类,静态类就是一个类里面都是静态方法和静态field,构造器被private修饰,因此不能被实例化。Math类就是一个静态类。

知道了什么是静态类后,来说一下他们两者之间的区别:

1)首先单例模式会提供给你一个全局唯一的对象,静态类只是提供给你很多静态方法,这些方法不用创建对象,通过类就可以直接调用;

2)如果是一个非常重的对象,单例模式可以懒加载,静态类就无法做到;

如果你只是想使用一些工具方法,那么最好用静态类,静态类比单例类更快,因为静态的绑定是在编译期进行的。如果你要维护状态信息,或者访问资源时,应该选用“单例模式”。还可以这样说,当你需要面向对象的能力时(比如继承、多态)时,选用单例类,当你仅仅是提供一些方法时选用静态类。


3.如何实现单例模式

3.1------创建饿汉式单例

所谓饿汉模式就是立即加载,一般情况下再调用getInstancef方法之前就已经产生了实例,也就是在类加载的时候已经产生了。这种模式的缺点很明显,就是占用资源,当单例类很大的时候,其实我们是想使用的时候再产生实例。因此这种方式适合占用资源少,在初始化的时候就会被用到的类。

public class SingletonTest {
    //创建饿汉式单例模式
    private static SingletonTest singletonHungary = new SingletonTest();
    //构造方法私有化
    private  SingletonTest() {

    }
    public static SingletonTest getSingleton(){
        return singletonHungary;
    }
}

 3.2-------创建懒汉式单例

懒汉模式就是延迟加载,也叫懒加载。在程序需要用到的时候再创建实例,这样保证了内存不会被浪费。针对懒汉模式,这里给出了5种实现方式,有些实现方式是线程不安全的,也就是说在多线程并发的环境下可能出现资源同步问题。

首先第一种方式,在单线程下没问题,在多线程下就出现问题了。

用几个多线程跑下,得出每个单例对象的哈希值,可以看出有不重复的值,那么说明有不同的对象生成。

public class SingletonTest {
    //创建懒汉式单例模式
    private static SingletonTest singletonHungary ;
    //构造方法私有化
    private  SingletonTest() {

    }
    public static SingletonTest getSingleton(){
        if(singletonHungary==null){
            singletonHungary=new SingletonTest();
        }
        return singletonHungary;
    }
}

3.3那么怎样才能保证什么情况下单例模式在多线程情况下能够实现呢

 用基于JVM的关键字synchronized

public class SingletonTest {

    //创建懒汉式单例模式
    private static SingletonTest singletonHungary ;
    //构造方法私有化
    private  SingletonTest() {

    }
    public static SingletonTest getSingleton(){
        synchronized(SingletonTest.class){
            if(singletonHungary==null){  //代码一
                singletonHungary=new SingletonTest();
            }
        }
        return singletonHungary;
    }

}

 3.4,看起来代码没有问题,实际测你就会发现并不能实现单例。当A,B两线程同时访问到代码一出,它们对象都为空,都会创建对象。那么我们需要双重判断就可以解决

public class SingletonTest {

    //创建懒汉式单例模式
    private static SingletonTest singletonHungary ;
    //构造方法私有化
    private  SingletonTest() {

    }
    public static SingletonTest getSingleton(){
        if(singletonHungary==null){
            synchronized(SingletonTest.class){
                if(singletonHungary==null) {
                    singletonHungary = new SingletonTest();
                }
            }
        }
        return singletonHungary;
    }

}

 我们看到双重校验锁即实现了延迟加载,又解决了线程并发问题,同时还解决了执行效率问题,是否真的就万无一失了呢?

 这里要提到Java中的指令重排优化。所谓指令重排优化是指在不改变原语义的情况下,通过调整指令的执行顺序让程序运行的更快。JVM中并没有规定编译器优化相关的内容,也就是说JVM可以自由的进行指令重排序的优化。

private static volatile SingletonTest singletonHungary= null;

3.5 这个问题的关键就在于由于指令重排优化的存在,导致初始化Singleton和将对象地址赋给singletonHungary字段的顺序是不确定的。在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给singletonHungary字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance,取到的就是状态不正确的对象,程序就会出错。不过还好在JDK1.5及之后版本增加了volatile关键字。volatile的一个语义是禁止指令重排序优化,也就保证了singletonHungary变量被赋值的时候对象已经是初始化过的,从而避免了上面说到的问题。

 3.6,这种基于JVM的双重锁机制处理问题在我们工作中也常见,例如我们在多线程情况下导入一百万条订单中用户的信息,订单中用户有重复,会遇到这种操作:【if(user==null) 插入用户信息】那么双重锁的机制可以解决这个问题了。

 

4.静态类下的单例模式

public class SingletonTest {

        private SingletonTest() {

        }
        private static class SingletonInner {
            private static SingletonTest singletonStaticInner = new SingletonTest();
        }
        public static SingletonTest getInstance() {
            
            return SingletonInner.singletonStaticInner;
        }

}

可以看到使用这种方式我们没有显式的进行任何同步操作,那他是如何保证线程安全呢?和饿汉模式一样,是靠JVM保证类的静态成员只能被加载一次的特点,这样就从JVM层面保证了只会有一个实例对象。那么问题来了,这种方式和饿汉模式又有什么区别呢?不也是立即加载么?实则不然,加载一个类时,其内部类不会同时被加载。一个类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时发生。

可以说这种方式是实现单例模式的最优解。
 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值