设计模式之单例模式

单例模式(Singleton Pattern)

引言

单例模式顾名思义,只生成单一实例的一种设计模式。这一个过程反应到设计领域就是,要求一个类只能生成一个对象,所有对象对它的依赖都是相同的。

怎么实现呢? 对象产生是通过new关键字完成的(当然也有其他方式,比如对象复制、反射等)

怎么控制每个类只产生一个对象呢? 创建一个对象是通过构造函数,那么我们把构造函数设置为private私有访问权限,就可以达到这一目的(反射仍然还是可以破解)。

一、定义

​ Ensure a class has only one instance, and provide a global point of accessto it.(确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。)

img

二、使用场景

● 要求生成唯一序列号的环境;

● 在整个项目中需要一个共享访问点或共享数据,例如一个 Web 页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保是线程安全的;

● 创建一个对象需要消耗的资源过多,如要访问 IO 和数据库等资源;

● 需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式 (当然,也可以直接声明为 static 的方式)。

三、实现

1.饿汉式

​ 该模式的特点是类加载时就生成了单例。代码如下:

public class HungryMan {
    //static修饰,加载HungryMan类时,就创建了实例对象
    //final修饰,该属性不能在被赋值
    public static final HungryMan hungryMan = new HungryMan();
    
    private HungryMan(){}
    
    public static HungryMan getInstance(){
        return hungryMan;
    }
}

2. 懒汉式

​ 该模式的特点是类加载时没有生成单例,只有当第一次调用 getlnstance 方法时才去创建这个单例。代码如下:

public class LazyMan {
    public static LazyMan lazyMan = null;

    //构造方法私有
    private LazyMan(){}

    public static LazyMan getInstance(){
        if(lazyMan == null) lazyMan = new LazyMan();
        return lazyMan;
    }
}

创造两个实例,判断两个实例是否为同一实例

    public static void main(String[] args) {
        LazyMan lm1 = LazyMan.getInstance();
        LazyMan lm2 = LazyMan.getInstance();
        System.out.println(lm1==lm2);
    }

结果

true

​ 该方式在低并发的情况下尚不会出现问题,若系统压力增大,并发量增加时则可能 在内存中出现多个实例,破坏了最初的预期。

为什么会出现这种情况呢?如一个线程A执行 到singleton = new Singleton(),但还没有获得对象(对象初始化是需要时间的),第二个线程 B也在执行,执行到singleton == null)判断,那么线程B获得判断条件也是为真,于是继续 运行下去,线程A获得了一个对象,线程B也获得了一个对象,在内存中就出现两个对象

代码如下:

public class LazyMan {
    public static LazyMan lazyMan = null;
    private static int count;
    //构造方法私有
    private LazyMan(){
        System.out.println("Singleton 私有的构造方法被实例化 " + (++count) + " 次。");
    }

    public static LazyMan getInstance() {
        if(lazyMan == null) {
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }
}

执行

 public static void main(String[] args) {
        //Runnable runnable = LazyMan::getInstance;
        Runnable runnable = ()->{
            System.out.println(Thread.currentThread().getName()+"执行");
            LazyMan.getInstance();
        };
        for (int i = 0; i < 2; i++) {
            new Thread(runnable).start();
        }
    }

结果

Thread-0执行
Thread-1执行
Singleton 私有的构造方法被实例化 2 次。
Singleton 私有的构造方法被实例化 1 次。

分析:

​ 两个线程是并发执行的,很有可能在Thread-0进入 if(lazyMan == null)代码块中还没有实例化或者实例化还没有初始化赋值的时候,lazyMan仍然为空,此时切换到Thread-1执行,完成初始化之后,再切换回Thread-0执行完初始化过程,因此这种方式是线程不安全的。我们可以通过给变量加volatile修饰,使用synchronized加锁。

3. 加锁(synchronized)

第一种:我们可以对方法加锁。

​ 这种方式对静态方法加锁,虽然解决了并发问题,但效率很低。因为,只有当第一次调用 getInstance() 时才需要同步创建对象,创建之后再次调用 getInstance() 时就只是简单的返回成员变量,而这里是无需同步的,所以没必要对整个方法加锁。

public class LazyMan {
    public static LazyMan lazyMan = null;
    private LazyMan(){}
    public static synchronized LazyMan getInstance() {
        if(lazyMan == null) {
            lazyMan = new LazyMan();
        }      
        return lazyMan;
    }
}

第二种:对方法内的部分代码加锁

​ 这种方式同样有问题,因为计算机存在指令重排序。并发情况下,还是有可能创建出多个对象。

public class LazyMan {
    public static LazyMan lazyMan = null;
    private LazyMan(){}
    public static LazyMan getInstance() {
        synchronized(LazyMan.class){
            if(lazyMan == null) {
                lazyMan = new LazyMan();
            }
        }
        return lazyMan;
	}
}

​ 计算机在执行程序时候,为了提高代码、指令的执行效率,编译器和处理器会对指令进行重新排序,一般分为编译器对于指令的重新排序、指令并行之间的优化、以及内存指令的优化。

解释:

public static void main(String[] args) {
    Object o = new Object();
}
#编译
javac test.java
#反编译,再输出
javap -v test.class >  test.txt

img

​ 查看txt文件,我们可以看到class文件被编译成jvm指令。code中就是创建对象的步骤。不熟悉的同学可以学习一下jvm创建对象的过程:

检查类是否加载—>类加载—>堆中开辟空间,赋默认值(对象默认null)—>初始化(init)—>连接引用

img

0: new           #2            //实例化:堆中开辟空间,赋默认值null
3: dup
4: invokespecial #1            //执行init方法,初始化,此时不为null
7: astore_1					   //对象引用 连接 局部变量表中局部变量(变量名‘o’)
8: return

指令未重排序,则不会出现问题。可如果重排序将 4 和 7颠倒,先执行astore_1 后执行invokespecial,就可能创建出两个对象。

7: astore_1	
4: invokespecial #1            //执行init方法,初始化,此时不为null

​ 加volatile则可禁止指令重排序,避免这个问题,所以一般来说volatile会和synchronized搭配使用。

public class LazyMan {
    public static volatile LazyMan lazyMan = null;
    private LazyMan(){}
    public static LazyMan getInstance() {
        synchronized(LazyMan.class){
            if(lazyMan == null) {
                lazyMan = new LazyMan();
            }
        }       
        return lazyMan;
    }
}

​ 这里还可以进行优化。值得优化的地方就在于 synchronized 代码块这里。每个线程进来,不管三七二十一,都要先进入同步代码块再说,如果说现在 INSTANCE 已经不为null了,那么,此时当一个线程进来,先获得锁,然后才会执行 if 判断。我们知道加锁是非常影响效率的,所以,如果 INSTANCE 已经不为null,是不是就可以先判断,再进入 synchronized 代码块。

4. 双重检查加锁

public class LazyMan {
    public static volatile LazyMan lazyMan = null;
    private LazyMan(){}
    public static LazyMan getInstance() {
        if(lazyMan == null){
            synchronized(LazyMan.class){
                if(lazyMan == null) {
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }
}

5. 静态内部类

public class StaticInternalClasses {
    public static class InternalClass{
        public static StaticInternalClasses singleton = new StaticInternalClasses();
    }
    private StaticInternalClasses(){}

    public static StaticInternalClasses getInstance(){
        return InternalClass.singleton;
    }
}

6. 枚举

​ 枚举类其实就是一个使用final修饰的类,继承了java.lang.enum枚举的实例都是static修饰的,那么如果枚举只有一个对象,可以作为单例模式的一个实现方式。

public enum EnumSingleton {
    instance;
}

四、优点

● 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。

● 由于单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要 比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存的方式来解决(在Java EE中采用单例模式时需要注意JVM 垃圾回收机制)。

● 单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在 内存中,避免对同一个资源文件的同时写操作。

● 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单 例类,负责所有数据表的映射处理。

五、缺点

● 单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途 径可以实现。单例模式为什么不能增加接口呢?因为接口对单例模式是没有任何意义的,它 要求“自行实例化”,并且提供单一实例、接口或抽象类是不可能被实例化的。当然,在特殊 情况下,单例模式可以实现接口、被继承等,需要在系统开发中根据环境判断。

● 单例模式对测试是不利的。在并行开发环境中,如果单例模式没有完成,是不能进行 测试的,没有接口也不能使用mock的方式虚拟一个对象。

● 单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心它是否是单例的,是不是要单例取决于环境,单例模式把“要单例”和业务逻辑融合在一个类中。

六、总结

​ 单例模式是23个模式中比较简单的模式,应用也非常广泛,如在Spring中,每个Bean默 认就是单例的,这样做的优点是Spring容器可以管理这些Bean的生命期,决定什么时候创建 出来,什么时候销毁,销毁的时候要如何处理,等等。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值