JavaJava 杂谈Java服务器端编程

唯一实例与中心化——单例模式

2019-05-13  本文已影响0人  RunAlgorithm

1. 定义

单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。

一句话总结,就是唯一实例,中心化

现实世界的模型:

单例模式需要满足以下的特点:

2. 设计

单例类的类图可以简单表示为: 单例模式

如果正确地设计单列,需要关注两个点:

因为单例类需要自己实例化自身,并且要确保在多线程环境下不会产生多个实例,而且并发下性能到达最优。

根据实例化的时机,分为两种:

2.1. 懒汉:错误方式

为了实现懒汉,延迟初始化单例,我们把实例化的过程推迟到第一次访问单例上。于是就有了这样的代码

public class Singleton {
    private static Singleton instance;
    private Singleton (){}
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

看上去很美好,实则有一个很大的隐患,多线程调用 getInstance 方法会产生多实例。

简单分析一下,假设 A 线程和 B 线程同时调用

为了确保能够延迟初始化,并且做到线程安全,下面会开始介绍

2.2. 懒汉:方法直接加同步(不要用)

最简单粗暴的方式,获取实例的方法直接上锁:

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

确实线程安全了。

但是 synchronized 是互斥锁,悲观锁,getInstance 被调用频繁的情况下性能低下。

这样吧,我们把锁的粒度

2.3. 懒汉:双重检查锁定(推荐,记得 volatile)

双重检查锁定,本质是对锁的一个粒度优化。

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

只有判断实例为空,才会进入同步代码。实例存在的话就直接返回了。

所以只有在创建的那一刹那,并且有多线程并发,会有一个互斥等待的过程。

因为进入同步代码,有可能其他线程已经实例化完毕,所以还要再检查一下是否已经实例化(判空),这就是双重检查。

因为 JVM 的指令优化,指令重排序现象会导致对象延迟初始化,所以其他线程读到实例不为空的时候,可能还没初始化。

所以需要加个 volatile 关键字,禁止重排序优化。

2.4. 懒汉:静态内部类(非常推荐)

这是一个比较机智的做法,利用 JVM 类加载机制,来延迟初始化对象实例

public class Singleton {    
    private static class LazyHolder {    
       private static final Singleton INSTANCE = new Singleton();    
    }    
    private Singleton (){}    
    public static Singleton getInstance() {    
       return LazyHolder.INSTANCE;    
    }
}

什么时候会进行类加载?

这里的静态内部类,外部的 Singleton 加载的时候并不会引起它的加载。因为虽然是它的内部静态类,但编译成字节码文件后是两个单独的类。

在调用 getInstance 方法后,静态内部类被主动调用,触发类加载流程。

而类加载流程是天然同步的,我们可以从源码上看到:

 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
 {
    synchronized (getClassLoadingLock(name)) {
        ...
        return c;
    }
}

我们也就不需要再进行加锁了。

2.5. 饿汉:静态工厂方法

在单例类加载的时候,马上实例化单例对象。

public class Singleton {  
    private Singleton() {}  
    private static final Singleton singleton = new Singleton();  
    public static Singleton getInstance() {  
        return singleton;  
    }  
}  

始终是线程安全的。

缺点是,及时没有调用该单例,也会在类使用的时候一开始就创建好了,在一些对性能要求高的场景会消耗性能。(这个在客户端场景比较常见)

2.6. 枚举实现

public enum Singleton {
    INSTANCE;
    private Singleton() {
    }
}

创建枚举默认是线程安全的

3. 应用

什么时候需要使用单例?

控制实例的访问,所有的访问必须在单一实例上进行。

控制资源的使用,和线程池、连接池等配合使用,资源型单例,避免创建多个池导致资源的浪费。

控制对象的创建,不需要重复创建的对象,和工厂模式配合,比如实现工厂实例的唯一。

在客户端环境,比如 Android 开发,因为对应用启动的性能要求高,不希望应用一加载马上进行单例实例化,所以对懒汉模式应用较多。

3.1. JDK:Runtime

Runtime 可以获取 JVM 运行环境的信息。

public class Runtime {
    private static Runtime currentRuntime = new Runtime();
    
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}
    
    ...
}

4. 特点

4.1. 优势

4.2. 缺点

4.3. 注意事项

上一篇下一篇

猜你喜欢

热点阅读