设计模式之单例模式

单例模式是一种创建型模式。它保证一个类仅有一个实例,并对外提供一个可以全局访问的方法。

优点:

在内存中只有一个对象,节省内存空间;

避免频繁的创建销毁对象,可以提高性能;

避免对共享资源的多重占用,简化访问;

为整个系统提供一个全局访问点。

缺点:

不适用于变化频繁的对象;

滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;

如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,这可能会导致对象状态的丢失;

特点:

  • 类构造器私有化
  • 持有自己类的引用
  • 对外提供获取实例的静态方法

常见的几种写法:饿汉式、懒汉式、懒汉式同步锁、双重校验锁、静态内部类

饿汉式

优点:没有线程安全问题存在

缺点:浪费内存

public class SimpleSingleton {

    //1.创建对象  持有自己类的引用
    private static final SimpleSingleton mSimpleSingleton = new SimpleSingleton();

    //2.私有化构造器
    private SimpleSingleton() {
    }

    //3.对外提供获取实例的静态方法
    public static SimpleSingleton getInstance() {
        return mSimpleSingleton;
    }

饿汉式-变种  通过静态代码块实现

public class SimpleSingleton {

    //1.创建对象  持有自己类的引用
    private static final SimpleSingleton mSimpleSingleton;

    //2.静态代码块的方式实例化mSimpleSingleton对象
    static {
        mSimpleSingleton = new SimpleSingleton();
    }

    //3.对外提供获取实例的静态方法
    public static SimpleSingleton getInstance() {
        return mSimpleSingleton;
    }
    
}

懒汉式

优点:没有内存空间浪费的问题

缺点:非线程安全(如果控制不好,实际上不是单例模式)

public class SimpleSingleton {

    private static SimpleSingleton sSimpleSingleton;

    private SimpleSingleton() {

    }

    /**
     * 用的时候才去检查有没有实例,如果有则返回,没有则新建
     *
     * @return
     */
    public static SimpleSingleton getInstance() {
        if (sSimpleSingleton == null) {
            sSimpleSingleton = new SimpleSingleton();
        }
        return sSimpleSingleton;
    }
}

存在问题:假如有多个线程中都调用了getInstance方法,那么都走到 if (sSimpleSingleton == null) 判断时,可能同时成立,因为sSimpleSingleton 初始化时默认值是null。这样会导致多个线程中同时创建sSimpleSingleton 对象,即sSimpleSingleton 对象被创建了多次,违背了只创建一个sSimpleSingleton 对象的初衷。

解决方法:synchronized关键字

synchronized:
Java语言的关键字,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。然而,当一个线程访问object的一个加锁代码块时,另一个线程仍然可以访问该object中的非加锁代码块。

public class SimpleSingleton {

    private static SimpleSingleton sSimpleSingleton;

    private SimpleSingleton() {

    }
    /**
     * 问题是:假如有多个线程中都调用了getInstance方法,那么都走到 if (sSimpleSingleton == null) 判断时,
     * 能同时成立,因为INSTANCE初始化时默认值是null。这样会导致多个线程中同时创建INSTANCE对象,
     * 即sSimpleSingleton对象被创建了多次,违背了只创建一个sSimpleSingleton对象的初衷。
     * 
     * 解决方法:synchronized关键字
     *
     * 在getInstance方法上加synchronized关键字,保证在并发的情况下,只有一个线程能创建INSTANCE对象的实例
     * 
     * 使用synchronized关键字会消耗getInstance方法的性能,我们应该判断当sSimpleSingleton为空时才加锁,如果不为空不应该加锁,需要直接返回
     */
    public static synchronized SimpleSingleton getInstance() {
        if (sSimpleSingleton== null) {
            sSimpleSingleton= new SimpleSingleton();
        }
        return sSimpleSingleton;
    }
}

虽然使用了关键字synchronized ,但是synchronized关键字会消耗getInstance方法的性能,我们应该判断当sSimpleSingleton为空时才加锁,如果不为空不应该加锁,需要直接返回,这就需要双重校验锁了。

双重校验锁

顾名思义会检查两次:在加锁之前检查一次是否为空,加锁之后再检查一次是否为空。

public class SimpleSingleton {

   private static SimpleSingleton mSimpleSingleton;

    private SimpleSingleton() {

    }

    /**
     * 双重校验锁:
     * 是为了防止在多线程并发的情况下,只会实例化一个对象。
     * 
     * 比如:线程a和线程b同时调用getInstance方法,假如同时判断mSimpleSingleton都为空,这时会同时进行抢锁。
     * 
     * 假如线程a先抢到锁,开始执行synchronized关键字包含的代码,此时线程b处于等待状态。
     * 
     * 线程a创建完新实例了,释放锁了,此时线程b拿到锁,进入synchronized关键字包含的代码,如果没有再判断一次mSimpleSingleton是否为空,则可能会重复创建实例。
     * 
     * 所以需要在synchronized前后两次判断。
     */
    public static SimpleSingleton getInstance() {
        if (mSimpleSingleton == null) { 
            synchronized (SimpleSingleton.class) {
                if (mSimpleSingleton == null) {
                    mSimpleSingleton = new SimpleSingleton();
                }
            }
        }
        return mSimpleSingleton;
    }
    
}

在这段代码中我希望执行的顺序是1、2、3、4、5

    public static SimpleSingleton getInstance() {
        if (mSimpleSingleton == null) { //1
            synchronized (SimpleSingleton.class) {//2
                if (mSimpleSingleton == null) {//3
                    mSimpleSingleton = new SimpleSingleton();//4
                }
            }
        }
        return mSimpleSingleton;//5
    }

重排之后的顺序可能就变成了:1、3、2、4、5,这样在多线程的情况下同样会创建多次实例。

所以这就使用到volatile关键字

volatile关键字

volatile是一个类型修饰符(type specifier)。它是被设计用来修饰被不同线程访问和修改的变量。确保本条指令不会因编译器的优化而省略,且要求每次直接读值。

保证了各线程对singleton静态实例域修改的可见性。

public class SimpleSingleton {

   private volatile static SimpleSingleton INSTANCE;

    private SimpleSingleton() {

    }
    
    /**
     * 双重校验锁的机制既保证了线程安全,又比直接上锁提高了执行效率,还节省了内存空间
     *
     * @return
     */
    public static SimpleSingleton getInstance() {
        if (INSTANCE == null) {
            synchronized (SimpleSingleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new SimpleSingleton();
                }
            }
        }
        return INSTANCE;
    }
}

关于锁的介绍
锁提供的两种特性:互斥(mutual exclusion) 和可见性(visibility):

(1)互斥(mutual exclusion):互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据;

(2)可见性(visibility):简单来说就是一个线程修改了变量,其他线程可以立即知道。保证可见性的方法:volatile,synchronized,final(一旦初始化完成其他线程就可见)。

 静态内部类

顾名思义是通过静态的内部类来实现单例模式的。
在SimpleSingleton类中定义了一个静态的内部类Inner。在SimpleSingleton类的getInstance方法中,返回的是内部类Inner的实例SINGLETON对象。
只有在程序第一次调用getInstance方法时,虚拟机才加载Inner并实例化SINGLETON对象。
java内部机制保证了,只有一个线程可以获得对象锁,其他的线程必须等待,保证对象的唯一性。


步骤:
1.创建构造器
2.创建静态内部类(内部创建对象)
3.getInstance方法实现(引用内部类)

public class SimpleSingleton {

    private SimpleSingleton() {

    }

    /**
     * 解决办法:需要在无参构造方式中判断,如果非空,则抛出异常了。
     * 这样解决之后,还存在问题  “ 反序列化漏洞”
     * 反序列化漏洞:众所周知,java中的类通过实现Serializable接口,可以实现序列化。我们可以把类的对象先保存到内存,或者某个文件当中。后面在某个时刻,再恢复成原始对象。
     *
     * @return
     */
    private SimpleSingleton() {
        if (InnerClass.SINGLETON != null) {
            throw new RuntimeException("单例模式下不支持重复实例化");
        }
    }

    public static SimpleSingleton getInstance() {
        return InnerClass.SINGLETON;
    }

    private static class InnerClass {
        private static final SimpleSingleton SINGLETON = new SimpleSingleton();
    }

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值