单例设计模式

单例模式是什么

        单例设计模式( Singleton Pattern)是一种创建型设计模式,它保证类只能有一个实例,并提供一个全局访问点。 
        在单例模式中,一般采用“懒加载”的方式来创建类实例,即只有在第一次访问时才会创建实例,此后每次访问都使用同一实例。

在实现单例模式时,需要注意以下几点: 
         1.私有化构造方法,以防止外部实例化;
         2.提供一个私有静态变量来存储实例;
         3.提供一个公共静态方法来获取实例,该方法一般是线程安全的;
         4.如果需要支持序列化和反序列化,要实现 SERIALIZABLE接口和 readResolve方法。

为什么要有单例模式

        从开发者本身来考虑的。比如日志类、配置类、连接池等,如果是一样的配置文件且不是单例的,就浪费了很多资源,而且也不知道是依哪一个为准。

        当我们在应用中遇到功能性冲突的时候就需要使用单例模式。

懒汉式单例和饿汉式单例优缺点

1、时间和空间

        懒汉式是典型的时间换空间,也就是每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。当然,如果一直没有人使用的话,那就不会创建实例,则节约内存空间。

        饿汉式是典型的空间换时间,当类装载的时候就会创建类实例,不管你用不用,先创建出来,然后每次调用的时候,就不需要再判断了,节省了运行时间。

2、线程安全

(1):饿汉式线程安全,静态修饰符修饰的属性,jvm类加载机制保证对象只会被创建一次

public class Singleton02 {

    // 三部曲

    //1.私有构造方法,不让外界进行创建对象。

    private Singleton02() {

    }

    //2.静态变量实例

    private static Singleton02 singleton = new Singleton02();

    
    //3.通过提供一个静态方法来获取我们的单例

    //该单例模式是饿汉式,及加载该类字节码的时候该单例就已经被创建。

    public static Singleton02 getInstance() {

        return singleton;

    }

}

(2):懒汉式


public class Singleton2 {
    // 1:私有的构造函数
    private Singleton2() {

    }
    // 2:静态变量
    private static Singleton2 instance;

    // 3:静态方法
    public static Singleton2 getInstance() {
        // 4:读取instance的值
        if (instance == null) {
            // 5: 实例化instance
            instance = new Singleton2();
        }
        return instance;
    }

}

        对于以上代码注释部分,如果此时有AB两个线程,线程A执行到4处,读取了instance为null,然后cpu就被线程B抢去了,此时,线程A还没有对instance进行实例化。因此,线程B读取instance时仍然为null,于是,它对instance进行实例化了。然后,cpu就被线程A抢去了。此时,线程A由于已经读取了instance的值并且认为它为null,所以,再次对instance进行实例化。所以,线程A和线程B返回的不是同一个实例。

(3):懒汉式加在方法前面加synchronized修饰

public class Singleton2 {

    private Singleton2() {

    }

    private static Singleton2 instance;

    public static synchronized Singleton2 getInstance() {

        if (instance == null) {

            instance = new Singleton2();
        }

        return instance;
    }

}

         这种线程安全的解决方式,假如有100个线程同时执行,那么,每次去执行getInstance方法时都要先获得锁再去执行方法体,如果没有锁,就要等待,耗时长,变成了串行处理。因此,尝试其他更好的处理方式。

(4)懒汉式加同步代码块,减少锁的颗粒大小

public class Singleton2 {

    private Singleton2() {

    }

    private static Singleton2 instance;

    public static Singleton2 getInstance() {

        if (instance == null) {

            synchronized (Singleton2.class) {

                instance = new Singleton2();

            }

        }
        return instance;
    }

}

        只有第一次instance为null的时候,才去创建实例,而判断instance是否为null是读的操作,不可能存在线程安全问题,因此,我们只需要对创建实例的代码进行同步代码块的处理,也就是所谓的对可能出现线程安全的代码进行同步代码块的处理。

        但是,这样处理其实还是有问题,同样的原理,线程A和线程B,线程A读取instance值为null,此时cpu被线程B抢去了,线程B再来判断instance值为null,于是,它开始执行同步代码块中的代码,对instance进行实例化。此时,线程A获得cpu,由于线程A之前已经判断instance值为null,于是开始执行它后面的同步代码块代码。它也会去对instance进行实例化。这样就导致了还是会创建两个不一样的实例。

(5)懒汉式双重检查加锁机制

        同步代码块中instance实例化之前进行判断,如果instance为null,才对其进行实例化。这样,就能保证instance只会实例化一次了。

public class Singleton2 {

    private Singleton2() {

    }

    private static Singleton2 instance;

    public static Singleton2 getInstance() {
        
        if (instance == null) {
            
            synchronized (Singleton2.class) {
                
                if (instance == null) {
                    
                    instance = new Singleton2();
                }
                
            }
        }
        return instance;
    }

}

再次分析上面的场景:

        线程A和线程B,线程A读取instance值为null,此时cpu被线程B抢去了,线程B再来判断instance值为null。于是,它开始执行同步代码块代码,对instance进行了实例化。这是线程A获得cpu执行权,当线程A去执行同步代码块中的代码时,它再去判断instance的值,由于线程B执行完后已经将这个共享资源instance实例化了,所以instance不再为null,所以,线程A就不会再次实行实例化代码了。

(5):懒汉式双重检查加锁机制+volatile关键字禁止指令重排序

        双重检查加锁并不代码百分百一定没有线程安全问题了。因为,这里会涉及到一个指令重排序问题。instance = new Singleton2()其实可以分为下面的步骤:

        1.申请一块内存空间;

        2.在这块空间里实例化对象;

        3.instance的引用指向这块空间地址;

指令重排序存在的问题是:

        对于以上步骤,指令重排序很有可能不是按上面123步骤依次执行的。比如,先执行1申请一块内存空间,然后执行3步骤,instance的引用去指向刚刚申请的内存空间地址,那么,当它再去执行2步骤,判断instance时,由于instance已经指向了某一地址,它就不会再为null了,因此,也就不会实例化对象了。这就是所谓的指令重排序安全问题。那么,如何解决这个问题呢?

加上volatile关键字,因为volatile可以禁止指令重排序。volatile参考文章

public class Singleton2 {

    private Singleton2() {

    }

    private static volatile Singleton2 instance;

    public static Singleton2 getInstance() {
        if (instance == null) {
            synchronized (Singleton2.class) {
                if (instance == null) {

                    instance = new Singleton2();
                }
            }
        }
        return instance;
    }

}

(6):静态内部类(线程安全,调用效率高,可以延时加载) 

public class Singleton {
    private static class LazyHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    
    private Singleton() {
    }      //私有的构造方法

    /**
     * 获取单例对象的方法
     *
     * @return Singleton
     */
    public Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
}

  从外部无法访问静态内部类LazyHolder,只有当调用Singleton.getInstance方法的时候,才能得到单例对象INSTANCE。
    INSTANCE对象初始化的时机并不是在单例类Singleton被加载的时候,而是在调用getInstance方法,使得静态内部类LazyHolder被加载的时候。因此这种实现方式是利用classloader的加载机制来实现懒加载,并保证构建单例的线程安全。

(7):枚举类(线程安全,调用效率高,不能延时加载,可以天然防止反射和序列化的调用)

    静态内部类的方法虽然很好,但是存在着单例共同的问题:无法防止利用反射来重复构建对象。


//获得构造器
Constructor con = Singleton.class.getDeclaredConstructor();
//设置为可访问
con.setAccessible(true);
//构造两个不同的对象
Singleton singleton1 = (Singleton)con.newInstance();
Singleton singleton2 = (Singleton)con.newInstance();
//验证是否是不同对象
System.out.println(singleton1.equals(singleton2));

  枚举:本身是个类,且是静态类,就是一个单例的,默认被final修改类,不能被继承,枚举中只有ToString没有被final修饰,枚举是自己内部实例化对象,这种其实也是一种饿汉式。优点:代码简单,防止序列化。


public enum EnumSingleton {

    INSTANCE;
  
  // 单例的成员变量
    private Object data;
    // 单例的成员方法
    // 调用方式是 EnumSingleton.getInstance().getData()
    public Object getData() {
      return data;
    }
    public void setData(Object data) {
      this.data = data;
    }
  
  // 通过 getInstance() 获取单例对象
  // 调用方式是 EnumSingleton.getInstance()
    public static EnumSingleton getInstance(){
      return INSTANCE;
    }
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值