转载

Java并发编程学习系列八:单例模式

介绍

什么是单例模式?

通俗的讲,就是在应用程序中只需要某个类保留唯一一个实例对象,不希望有更多的实例。单例模式是 Java 设计模式中最简单的设计模式之一,在应用程序中经常被用到。

应用场景

单例模式的应用场景有很多,比如线程池、日志对象、缓存、数据库连接池、计算机系统设备管理器等等。这些常常都设计成全局唯一的,方便集中管理,也节省系统的开销。

实现方式

实现单例模式要注意以下三点:

1、单例类只能有一个实例,不能从其他对象中 new 出来, 即构造器用 private 修饰。

2、单例类必须自己创建自己的唯一实例,需要实现一个方法提供这个实例。

3、单例类必须能给其他对象提供这一实例。

接下来我们讲讲在 Java 中如何实现单例模式 :

(1)饿汉式

饿汉式,顾名思义指的是在类加载的时候就初始化好对象,不管有没有用到。绝对线程安全,在线程还没出现以前就是实例化了,不可能存在访问安全问题。

Spring 中 IOC 容器 ApplicationContext 就是典型的饿汉式单例。

public class Singleton {

    private final static Singleton singleton = new Singleton();

    private Singleton(){}

    public static Singleton getInstance(){
        return singleton;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            Singleton singleton1 = Singleton.getInstance();   //获取都是同一个对象
            System.out.println(singleton1.hashCode());      
        }
    }
}
复制代码

还有另外一种写法,利用静态代码块的机制:

public class Singleton {
    // 1. 私有化构造器
    private Singleton(){}

    // 2. 实例变量
    private static final Singleton instance;

    // 3. 在静态代码块中实例化
    static {
        instance = new Singleton();
    }

    // 4. 提供获取实例方法
    public static Singleton getInstance(){
        return instance;
    }
}
复制代码

(2)懒汉式

懒汉式和饿汉式相对,指的在程序加载时不初始化对象,什么时候被引用什么时候才初始化对象,即在第一次使用的时候才去初始化对象,可以避免内存浪费。注意在获取实例的 getInstance()方法前加上了 synchronized 关键字,这是为了保证线程安全,避免多线程同一时刻获取对象时造成生成了多个实例。

public class Singleton {

    private static Singleton singleton = null;

    private Singleton(){}

    public synchronized static Singleton getInstance(){
        if(singleton == null){   // 1
            singleton = new Singleton(); // 2
        }
        return singleton;
    }

}
复制代码

(3)双重检查锁

双重检查锁是在懒汉式基础上演变过来的,当分析懒汉式代码时,你会发现只有在第一次调用获取实例方法时才需要同步。因为仅步骤2处的代码需要同步,但只有第一次调用才执行此行,后面的其他调用没有执行此行,但都付出了同步的代价。因此为避免在实例已经创建的情况下每次获取实例都加锁取,提高效率,双重检查锁应运而生。

为什么要二次检查?分析双重检查锁代码,多线程并发情况下, 第一个线程执行完 synchronized 的代码块后,后面的线程仍然需要对 singleton 进行第二次检查,即步骤3,避免重复实例化对象。所以需要对实例对象做两次检查。

既然 synchronized 能保证有序性,为什么还要加 volatile?多线程情况下,synchronized 关键字能够起到同步的作用,保证每次只有一个线程能够操作。这样一来对于其内部就相当于单线程操作,但是不会影响其内部的指令重排。我们知道 volatile 可以禁止指令重排,步骤4是属于复合操作指令,首先是实例化对象,然后才是写操作,关于实例化对象其实分为以下三个步骤:

​ (1)分配内存空间。

​ (2)初始化对象。

​ (3)将内存空间的地址赋值给对应的引用。

但是由于操作系统可以对指令进行重排序,所以上面的过程也可能会变成如下过程:

​ (1)分配内存空间。

​ (2)将内存空间的地址赋值给对应的引用。

​ (3)初始化对象

如果是这个流程,多线程环境下就可能将一个未初始化的对象引用暴露出来,从而导致不可预料的结果。因此,为了防止这个过程的重排序,我们需要将变量设置为 volatile 类型的变量。volatile 变量的写操作,会保证之前的所有指令一定会在 volatile 写操作之前完成,那么 instance = new SingleTon() 这个复合操作指令一定是对象创建完成再进行赋值。

public class Singleton {

    private volatile static Singleton singleton = null;

    private Singleton(){}

    public static Singleton getInstance(){
        if(singleton == null){ // 步骤1
            synchronized (Singleton.class){ // 步骤2
                if(singleton == null){ // 步骤3
                    singleton = new Singleton(); // 步骤4
                }
            }
        }
        return singleton;
    }

}
复制代码

(4)静态内部类

这种方式能达到双检锁方式一样的功效,但实现更为简单。这种和饿汉式比较,在类加载时,singleton实例并没有被初始化,需要显示调用getInstance()方法才会转载SingleHolder类,从而初始化singleton实例,所以达到了延时加载的效果。此方法在实际使用中用的最多,推荐此种写法。

public class Singleton {

    private static class SingleHolder{
        private static Singleton singleton = new Singleton();
    }

    private Singleton(){ }

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

}
复制代码

(5)枚举

这种方式巧妙的应用了枚举的特点,构造器本身私有,写法简单,自动支持序列化机制,防止多次实例化,获取实例可以通过 Singleton.INSTANCE 来访问。

public enum Singleton {

    INSTANCE;
}
复制代码

参考文献

java 在同步锁内外判断两次,有什么用处?

Java设计模式(一)—— 单例模式

设计模式 - 单例模式(详解)看看和你理解的是否一样?

设计模式 - 单例模式之多线程调试与破坏单例

原文  https://juejin.im/post/5f01e264e51d4534b66d425d
正文到此结束
Loading...