单例模式——8种实现方式

前言

在一些场景中,咱们但愿建立的对象在整个软件系统只保存一份实例,如线程池, 日志对象、缓存等。建立并保存对象单一主要有两个做用:节省系统资源;防止多个对象产生冲突。**单例模式(Singleton Pattern)**就能够确保只有一个实例对象会被建立。今天,咱们重点聊聊单例模式的8种实现方式(java语言)java

private修饰符

咱们都知道,能够经过 new 的方式建立对象。若是类对new方式建立对象不加以约束的话,就不能保证系统只建立一个对象。private修饰类的构造方法,就能够确保该类不能任意建立对象。缓存

1、饿汉式(静态常量)

饿汉式实现单例模式的原理:利用静态常量在类加载时生成全局惟一实例特性安全

具体代码markdown

// 单例模式实现1,饿汉式(静态常量)
public class Singleton1 {
    // 类加载时,实例化对象
    private static Singleton1 instance = new Singleton1();

    public static Singleton1 getInstance() {
        return instance;
    }

    private Singleton1() {
        System.out.println("单例模式实现1,饿汉式(静态常量)");
    }

    public static void main(String[] args) {
        System.out.println("开始演示静态常量方式建立单例对象:");
        Singleton1 instance1 = Singleton1.getInstance();
        Singleton1 instance2 = Singleton1.getInstance();
        System.out.println(instance1 == instance2);
    }
}
// 运行main方法,结果以下:
单例模式实现1,饿汉式(静态变量)
开始演示静态变量方式建立单例对象:      
true
复制代码

从运行结果(输出打印的1,2行顺序)能够看出,咱们想要获取的实例对象在真正获取以前已经实例化。(静态常量在类加载过程当中赋值)多线程

这也是饿汉式实现单例模式很差的一点:不能懒加载。jvm

2、饿汉式(静态代码块)

基本与上面的实现方式同样,只是语法有点区别,静态代码块替换静态变量直接赋值。oop

// 单例模式实现2,饿汉式(静态代码块)
public class Singleton2 {

    static {
        instance = new Singleton2();
    }

    private static Singleton2 instance;

    private Singleton2() {
        System.out.println("单例模式实现2,饿汉式(静态代码块)");
    }

    public static Singleton2 getInstance() {
        return instance;
    }

    public static void main(String[] args) {
        System.out.println("开始演示静态代码块方式建立单例对象:");
        Singleton2 instance1 = Singleton2.getInstance();
        Singleton2 instance2 = Singleton2.getInstance();
        System.out.println(instance1 == instance2);
    }
}
// 运行main方法,结果以下:
单例模式实现2,饿汉式(静态代码块)
开始演示静态代码块方式建立单例对象:
true
复制代码
3、懒汉式(常规写法,线程不安全)

上面的两种写法,都是不支持懒加载的。接下来的几种方式,都是懒加载的方式。首先看看最简单的一种实现方式优化

// 单例模式实现3,懒汉式(常规写法,线程不安全)
public class Singleton3 {
    private static Singleton3 instance;

    private Singleton3() {
        System.out.println("单例模式实现3,懒汉式(常规写法,线程不安全)。当前线程:" + Thread.currentThread().getName());
    }

    public static Singleton3 getInstance() {
        if (instance == null) {
            instance = new Singleton3();
        }
        return instance;
    }

    public static void main(String[] args) {
        System.out.println("开始演示常规懒加载方式建立单例对象:");
        Singleton3 instance1 = Singleton3.getInstance();
        Singleton3 instance2 = Singleton3.getInstance();
        System.out.println(instance1 == instance2);
    }
}
// 运行结果
开始演示常规懒加载方式建立单例对象:
单例模式实现3,懒汉式(常规写法,线程不安全)。当前线程:main
true
复制代码

从运行结果来看,这种方式彷佛没有问题。即实现了懒加载,又保证了对象单一。spa

咱们换种演示方式,修改main方法:线程

public static void main(String[] args) {
        System.out.println("开始演示常规懒加载方式建立单例对象:");
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                Singleton3.getInstance();
            }).start();
        }
    }
    // 运行结果(有可能须要多运行几回,才会出现相似效果)
    开始演示常规懒加载方式建立单例对象:
	单例模式实现3,懒汉式(常规写法,线程不安全)。当前线程:Thread-1
	单例模式实现3,懒汉式(常规写法,线程不安全)。当前线程:Thread-0
复制代码

从运行结果能够看出,这种单例模式的实现方式是线程不安全的,在多线程环境下,有可能会建立多个实例。

4、懒汉式(同步方法,线程安全)

方式三建立单例对象,线程不安全的缘由是:当instance在完成实例化以前,多个线程同时判断if (instance == null)结果都为true,致使这些线程都往下继续执行建立实例对象。简单粗暴的解决方式,在getInstance方法加锁(用synchronized关键字修饰方法)。具体代码:

// 单例模式实现4,懒汉式(同步方法,线程安全)
public class Singleton4 {
    private static Singleton4 instance;

    private Singleton4() {
        System.out.println("单例模式实现4,懒汉式(同步方法,线程安全)。当前线程:" + Thread.currentThread().getName());
    }

    public static synchronized Singleton4 getInstance() {
        if (instance == null) {
            instance = new Singleton4();
        }
        return instance;
    }

    public static void main(String[] args) {
        System.out.println("开始演示懒加载-同步方法方式建立单例对象:");
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                Singleton4.getInstance();
            }).start();
        }
    }
}
// 运行结果
开始演示懒加载-同步方法方式建立单例对象:
单例模式实现4,懒汉式(同步方法,线程安全)。当前线程:Thread-1
复制代码

这种方式,虽然解决了线程安全问题,可是每次获取实例对象时,都须要加锁,这大大影响了系统运行效率。接下来的实现方式,将逐步优化线程安全下懒加载效率低的问题。

5、懒汉式(同步代码块)

在静态方法加锁,锁粒度太大,形成资源浪费。所以,咱们尝试把锁粒度缩小,在代码块加锁。

示例代码:

public class Singleton5 {
    private static Singleton5 instance;

    private Singleton5() {
        System.out.println("单例模式实现5,懒汉式(同步代码块,线程安全)。当前线程:" + Thread.currentThread().getName());
    }

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

    public static void main(String[] args) {
        System.out.println("开始演示懒加载-同步代码块方式建立单例对象:");

        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                Singleton5.getInstance();
            }).start();
        }
    }
}
// 运行结果(有可能须要多运行几回,才会出现相似效果):
开始演示懒加载-同步代码块方式建立单例对象:
单例模式实现5,懒汉式(同步代码块,线程安全)。当前线程:Thread-0
单例模式实现5,懒汉式(同步代码块,线程安全)。当前线程:Thread-1
复制代码

从运行结果来看,这种实现方式也是线程不安全的。缘由分析:

关键代码

if (instance == null) {                     // 第1行
            synchronized (Singleton5.class){        // 第2行
                instance = new Singleton5();        // 第3行
            }
        }
复制代码

虽然在2行加上了锁,但这只保证了同一时刻,只有一个线程能够执行第3行代码。在第3行代码执行前,不一样的线程仍是能够判断if是true,而后执行到第2行,等待有锁的线程释放锁,得到锁以后继续建立对象。

若要线程安全,改造以下:

public static Singleton5 getInstance() {
        synchronized (Singleton5.class) {
            if (instance == null) {
                instance = new Singleton5();
            }
        }
        return instance;
    }
复制代码

然而,这种实现方式与第四种效果一致,锁的粒度是整个getInstance方法。

6、懒汉式(双重检查)

前面三种懒加载实现单例的方式,都有各自的不足,不是线程不安全就是获取单例效率低。线程不安全的地方在于已有线程建立实例,继续建立实例。效率低的地方在于,已经建立好实例,还加锁获取实例。而双重检查就避免了这两种问题。

示例代码

// 懒汉式(双重检查)
public class Singleton6 {

    private static volatile Singleton6 instance;

    private Singleton6() {
        System.out.println("单例模式实现6,懒汉式(双重检查)。当前线程:" + Thread.currentThread().getName());
    }

    public static Singleton6 getInstance() {
        if (instance == null) {
            System.out.println("尝试建立实例...");
            synchronized (Singleton6.class){
                if (instance == null) {
                    instance = new Singleton6();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        System.out.println("开始演示懒加载-双重检查方式建立单例对象:");

        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                Singleton6.getInstance();
            }).start();
        }
    }
}
// 运行结果
开始演示懒加载-双重检查方式建立单例对象:
尝试建立实例...
单例模式实现6,懒汉式(双重检查)。当前线程:Thread-0
复制代码

特别注意一点,咱们静态变量用了「volatile」关键词修饰,为何要用volatile修饰呢,能够参考文章:《双重检查锁定与延迟初始化》

7、静态内部类

利用静态内部类的方式,咱们也能够实现线程安全的单例模式

示例代码

// 单例模式实现7,静态内部类
public class Singleton7 {

    private Singleton7(){
        System.out.println("单例模式实现7静态内部类。当前线程:" + Thread.currentThread().getName());
    }

    private static class InstanceHolder{
        private static Singleton7 instance = new Singleton7();
    }

    public static Singleton7 getInstance() {
        return InstanceHolder.instance;
    }

    public static void main(String[] args) {
        System.out.println("开始演示静态内部类建立单例对象:");
        Singleton7 instance1 = Singleton7.getInstance();
        Singleton7 instance2 = Singleton7.getInstance();
        System.out.println(instance1 == instance2);
    }
}
// 运行结果
开始演示静态内部类建立单例对象:
单例模式实现7静态内部类。当前线程:main
true
复制代码

JVM 帮助咱们保证了内部类建立的线程安全性

8、枚举方式

枚举在jvm里是自然的单例,因此利用枚举实现单例也是线程安全的。《 Effective Java》这本书就提倡用枚举的方式建立单例对象

示例代码

public enum Singleton8 {

    INSTANCE();

    Singleton8(){
        System.out.println("单例模式实现8,枚举方式");
    }
}
复制代码
总结

单例模式的实现方式有多种,保证线程安全和运行效率状况下(文中的第三种方式线程不安全,第4、五种方式效率低)),各类实现方式的实际效果差异并不大,选择本身顺手的实现方式就能够!而「懒加载」和「双重检查」思想,在咱们开发中常用到的,但愿你们好好理解这两种思想。

相关文章
相关标签/搜索