【设计模式】-单例模式

1、单例模式

概念:
java单例模式是一种常见的设计模式单例模式分三种:懒汉式单例、饿汉式单例、登记式单例三种。
单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。

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

–以上来自百度百科
下面主要介绍懒汉式、饿汉式两种

1、懒汉式单例

所谓懒汉式单例,重点突出的是懒,也就是它不会主动的去创建实例。只有调用的时候,才会创建。

/**
* 单例模式 - 懒汉式  
*/
public class Singleton {
    private static Singleton singleton;
    
    private Singleton() {
    }
    
    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

但是上面这种方式创建出来的实例,在多线程情况下是不安全的。当一个线程获取实例的时候。判断singleton == null为true,于是执行singleton = new Singleton();但是对象实例化也是需要花时间的。如果在对象还没有创建成功的时候,此时又来了一个线程,执行if (singleton == null) 结果发现实例还不存在。于是又执行了一次singleton = new Singleton(); 这样就产生了两个实例对象了。所以肯定是不行的。
我们可以使用下面的方法进行验证。发现确实可以产生多个实例。
在这里插入图片描述
在这里插入图片描述


为了解决上面这种懒汉式线程不安全的情况。可以采用下面三种方式来解决

1.1、增加synchronized 关键字【不推荐使用】
/**
 * 单例模式 - 懒汉式 
 * 使用synchronized关键字保证线程安全
 * 对方法加锁
 */
public class Singleton {
    private static Singleton singleton;

    private Singleton() {
    }

    public static synchronized Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}
// 对代码块加锁,与上面等效
public class Singleton {
    private static Singleton singleton;

    private Singleton() {
    }

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

1.2、双检锁(Double Check Lock双端检锁机制)【推荐使用】
public class Singleton {
    private Singleton() {
    }

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

1.3、静态内部类【推荐使用】
/**
 * 采用静态内部类的方法获取单例
 */
class Singleton {
    /**
     * 静态内部类
     */
    private static class getSingleton {
        private static final Singleton singleton = new Singleton();
    }

    private Singleton() {
    }

    public static Singleton getInstance() {
        return getSingleton.singleton;
    }

}

先说结论,性能:静态内部类>双检锁>synchronized 。但是面试中最常问的还是双检锁

现在我们来分析上面三种,是如何保证线程安全的,同时为了保证线程安全而带来的性能影响。

1)首先第一种。加synchronized 关键字这个很好理解。对getInstance方法进行加锁。保证同一时间只有一个线程可以获取到该方法的控制权。其他线程只能等待该线程执行完才能继续执行。也就是说同一时间下只允许一个线程执行对象的判断和实例化的动作。虽然保证了线程安全。但缺点也是显而易见。并发性能太差,每次都需要先获取锁才能进行其他操作。


这种对方法或者方法内的代码块 直接加锁的方式,会导致无论这个对象存在不存在。都需要等待上一个线程执行。其他线程才能获取锁继续执行。可是如果对象实例已经存在了,能不能让其他线程直接返回已存在的实例,而不是不分青红皂白全去排队,争抢锁资源呢。

于是就引出了双检锁机制。由于需要两次判空,且对类对象加锁,该写法也被称为:Double Check(双重校验) + Lock(加锁) 即我们所说的双检锁。

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

2)我们可以看一下这个代码。当一个线程执行getInstance方法时,先判断对象实例是否存在,如果对象存在,不为空,那我们就可以直接返回该实例。而不是所有线程全都等待着去抢锁资源。这样可以很大程度上解决线程性能问题。


关于第四行。为什么又有一次判空操作呢,明明第二行已经执行了判空操作。这第四行还有必要吗? 答案是肯定的且二者的目的也是不同的。如果对象不存在时,多个线程执行第二行,很有可能都会进入到if方法体中,虽然同一时刻只有一个线程可以获取锁,但是进来的线程已经都在队列中了。如果不加第四行的判断,进来的线程迟早都会获取到锁资源,进而实例化一个新的对象。

  • 第二行的检查操作,是为了防止所有线程都去排队获取锁资源,用于提升性能。
  • 第四行的检查操作,是为了防止所有获取到锁资源的线程都重新实例化一个对象。用于保证线程安全。

    这里还有一个关键点,需要使用volatile关键字修饰一下静态成员变量。防止指令重排序

3)静态内部类能实现单例模式主要采用了JVM加载类的两个特性:

  • 静态内部类在类加载的时候不会被初始化,只有被调用的时候,才会被初始化。这样延迟加载的方式其实就是懒汉式。
  • JVM的类加载机制,已经处理了异步加锁问题,即便是高并发情况下,也不会产生线程安全问题。保证创建单例时的并发安全性。

2、饿汉式单例

/**
 * 饿汉式  线程安全
 */
class Singleton {

    private static Singleton singleton = new Singleton();

    private Singleton() {
    }

    public Singleton getSingleton() {
        return singleton;
    }
}

饿汉式,在类创建的同时就已经创建好一个静态的对象,并且以后都不需要重新创建,所以不存在线程不安全的问题。

3、登记式单例

原理:维护一组单例类的实例,将这些实例存放在一个单例Map中,对于已经登记过的实例,则从Map直接返回,对于没有登记的,则先登记,然后返回。
代码就不放了。我用的比较少,感兴趣的可以去搜一下。

完美的单例模式
上面几种,在日常开发中,已经够用了。但是称不上最完美。虽然都保证了对象的唯一性和线程安全。
我们知道,创建一个对象,除了使用new之外,还可以通过克隆、反射和反序列化

为了防止通过上面三种方式创建对象来保证对象的唯一性。可以采用枚举的方式。而且通过枚举实现的单例模式。也被公认为实现单例的最佳途径
简单的枚举单例示例如下:

/**
 * 枚举单例
 */
public enum EnumSingleton {
    INSTANCE;

    public void doSomething() {
        System.out.println("执行枚举单例方法");
    }
}

JVM能保证枚举类型不能被反射并且构造函数只被执行一次。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值