• Home
  • About
    • 谢志平 photo

      谢志平

      程序员

    • Learn More
    • Email
    • Github
  • Posts
    • All Posts
    • All Tags
  • Projects

单例模式与线程安全

09 Oct 2018

Reading time ~6 minutes

目录

  1. 立即加载/“恶汉模式”
  2. 延迟加载/“懒汉模式”

    2.1 方法中声明synchronized关键字

    2.2 尝试同步代码块

    2.3 针对某些重要的代码进行单独的同步

    2.4 使用DCL双检查锁机制

  3. 使用静态内部类实现单例模式
  4. 序列化与反序列化的单例模式实现
  5. 使用static代码块实现单例模式
  6. 使用enum枚举数据类型实现单例模式
  7. 完善使用enum枚举实现单例模式

前言

单例模式是为确保一个类只有一个实例,并为整个系统提供一个全局访问点的一种模式方法。本文将介绍几种线程安全的单例模式实现。

1 立即加载/“恶汉模式”

立即加载就是使用类的时候就已经将对象创建完毕

public class MySingleton {

    private static MySingleton instance = new MySingleton();

    private MySingleton(){}

    public static MySingleton getInstance() {
        return instance;
    }

}

以上是单例的饿汉式实现,我们来看看饿汉式在多线程下的执行情况,给出一段多线程的执行代码:

public class MyThread extends Thread{

    @Override
    public void run() {
        System.out.println(MySingleton.getInstance().hashCode());
    }

    public static void main(String[] args) {

        MyThread[] mts = new MyThread[10];
        for(int i = 0 ; i < mts.length ; i++){
            mts[i] = new MyThread();
        }

        for (int j = 0; j < mts.length; j++) {
            mts[j].start();
        }
    }
}

以上代码的执行结果为:

1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954

控制台打印的hashcode是同一个值,说明对象是同一个,也就是实现了立即加载型线程安全的单例模式。

2 延迟加载/“懒汉模式”

延迟加载就是在调用getInstance()方法时实例才被创建。

public class MySingleton {

    private static MySingleton instance = null;

    private MySingleton(){}

    public static MySingleton getInstance() {
        if(instance == null){//懒汉式
            instance = new MySingleton();
        }
        return instance;
    }
}

以上代码是懒汉模式的单例模式实现,但是在多线程环境下,该单例模式是非线程安全的,即可能在某个时刻线程同时创建了多个不同的实例。为了看到效果,我们对代码进行了模拟改造:

public class MySingleton {

    private static MySingleton instance = null;

    private MySingleton(){}

    public static MySingleton getInstance() {
        try {
            if(instance != null){//懒汉式
            }else{
                //模拟在创建对象之前做一些准备性工作
                Thread.sleep(300);
                instance = new MySingleton();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return instance;
    }
}

这里假设在创建实例前有一些准备性的耗时工作要处理,多线程调用:

public class MyThread extends Thread{

    @Override
    public void run() {
        System.out.println(MySingleton.getInstance().hashCode());
    }

    public static void main(String[] args) {
        MyThread[] mts = new MyThread[10];
        for(int i = 0 ; i < mts.length ; i++){
            mts[i] = new MyThread();
        }

        for (int j = 0; j < mts.length; j++) {
            mts[j].start();
        }
    }
}

执行结果如下:

1210420568
1210420568
1935123450
1718900954
1481297610
1863264879
369539795
1210420568
1210420568
602269801

从控制台输出可以看到,在多线程环境下该类创建了不同的对象,并不是单例的,即非线程安全的单例模式。

下面来看延迟加载/“懒汉模式”的线程安全解决方案

2.1 方法中声明synchronized关键字

既然多个线程可以同时进入getInstance()方法,那么只需要对getInstance()方法声明synchronized关键字进行锁同步即可,保证在同个时刻只有一个线程进入到getInstance()方法。

public class MySingleton {

    private static MySingleton instance = null;

    private MySingleton(){}

    public synchronized static MySingleton getInstance() {
        try {
            if(instance != null){//懒汉式
            }else{
                //创建实例之前可能会有一些准备性的耗时工作
                Thread.sleep(300);
                instance = new MySingleton();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return instance;
    }
}

此时在多线程环境下进行验证,输出结果为:

1689058373
1689058373
1689058373
1689058373
1689058373
1689058373
1689058373
1689058373
1689058373
1689058373

控制台输出结果显示,对getInstance()方法加synchronized关键字进行锁同步可以实现线程安全的单例模式实现。但是这种方法的运行效率非常低下,是同步运行的,下一个线程想要获得对象,则必须等上一个线程释放锁之后,才可以继续执行。

2.2 尝试同步代码块

同步方法是对方法的整体进行持锁,这对运行效率来讲是不利的。现在看看同步代码块能否解决?

public class MySingleton {

    private static MySingleton instance = null;

    private MySingleton(){}

    public static MySingleton getInstance() {
        try {
            synchronized (MySingleton.class) {
                if(instance != null){//懒汉式
                }else{
                    //创建实例之前可能会有一些准备性的耗时工作
                    Thread.sleep(300);
                    instance = new MySingleton();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return instance;
    }
}

在多线程验证环境下,其输出结果为:

1689058373
1689058373
1689058373
1689058373
1689058373
1689058373
1689058373
1689058373
1689058373
1689058373

从控制台输出可看,对代码块进行同步可以实现线程安全,但是这种效率也是非常低的,和synchronized方法一样也是同步运行的,其实锁住了整个代码块。

2.3 针对某些重要的代码进行单独的同步

针对某些重要的代码进行单独的同步,而不是全部进行同步,可以极大的提高执行效率。

public class MySingleton {

    private static MySingleton instance = null;

    private MySingleton(){}

    public static MySingleton getInstance() {
        try {
            if(instance != null){//懒汉式

            }else{
                //创建实例之前可能会有一些准备性的耗时工作
                Thread.sleep(300);
                synchronized (MySingleton.class) {
                    instance = new MySingleton();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return instance;
    }
}

同样在多线程验证环境下,其输出结果为:

1481297610
397630378
1863264879
1210420568
1935123450
369539795
590202901
1718900954
1689058373
602269801

此方法使用同步synchronized语句块,只对实例化对象的关键代码进行同步,从语句的结构上来讲,运行的效率的确得到了提升。但如果是遇到多线程的情况下,还是无法解决得到同一个实例对象的结果。

2.4 使用DCL双检查锁机制

DCL(Double-Check Lock)双检查锁机制即提高了执行效率,又保证了线程安全。

public class MySingleton {

    //使用volatile关键字保其可见性
    volatile private static MySingleton instance = null;

    private MySingleton(){
    
    }

    public static MySingleton getInstance() {
        try {
            if(instance != null){ //懒汉式,Check1

            }else{
                //创建实例之前可能会有一些准备性的耗时工作
                Thread.sleep(300);
                synchronized (MySingleton.class) {
                    if(instance == null){ //Check2
                        instance = new MySingleton();
                    }
                }
            }
        } catch (InterruptedException e) {
        e.printStackTrace();
        }
        return instance;
    }
}

同样在多线程验证环境下,其输出结果为:

369539795
369539795
369539795
369539795
369539795
369539795
369539795
369539795
369539795
369539795

使用双检查锁功能,成功解决了“懒汉模式”遇到多线程的问题。DCL也是大多数多线程结合单例模式使用的解决方法。

3 使用静态内置类实现单例模式

DCL可以解决多线程单例模式的非线程安全问题,当然,使用其他的办法也可以达到同样的效果。

public class MySingleton {
    //内部类
    private static class MySingletonHandler{
        private static final MySingleton instance = new MySingleton();
    }

    private MySingleton(){
    
    }

    public static MySingleton getInstance() {
        return MySingletonHandler.instance;
    }
}

同样在多线程验证环境下,其输出结果为:

1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954

使用静态内置类实现单例模式为线程安全的

4 序列化与反序列化的单例模式实现

静态内部类虽然保证了单例在多线程并发下的线程安全性,但是在遇到序列化对象时,默认的方式运行得到的结果就是多例的。代码实现如下:

import java.io.Serializable;

public class MySingleton implements Serializable {

    private static final long serialVersionUID = 1L;

    //内部类
    private static class MySingletonHandler{
        private static MySingleton instance = new MySingleton();
    }

    private MySingleton(){
    }

    public static MySingleton getInstance() {
        return MySingletonHandler.instance;
    }
}

序列化与反序列化测试代码:

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class SaveAndReadForSingleton {

    public static void main(String[] args) {
        MySingleton singleton = MySingleton.getInstance();

        File file = new File("MySingleton.txt");

        try {
            FileOutputStream fos = new FileOutputStream(file);
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(singleton);
            fos.close();
            oos.close();
            System.out.println(singleton.hashCode());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            FileInputStream fis = new FileInputStream(file);
            ObjectInputStream ois = new ObjectInputStream(fis);
            MySingleton rSingleton = (MySingleton) ois.readObject();
            fis.close();
            ois.close();
            System.out.println(rSingleton.hashCode());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

运行以上代码,得到的结果如下:

865113938
1442407170

从结果中我们发现,序列号对象的hashCode和反序列化后得到的对象的hashCode值不一样,说明反序列化后返回的对象是重新实例化的,单例被破坏了。那怎么来解决这一问题呢?

解决办法就是在反序列化的过程中使用readResolve()方法,单例实现的代码如下:

import java.io.ObjectStreamException;
import java.io.Serializable;

public class MySingleton implements Serializable {

    private static final long serialVersionUID = 1L;

    //内部类
    private static class MySingletonHandler{
        private static MySingleton instance = new MySingleton();
    }

    private MySingleton(){
    }

    public static MySingleton getInstance() {
        return MySingletonHandler.instance;
    }

    //该方法在反序列化时会被调用,该方法不是接口定义的方法,有点儿约定俗成的感觉
    protected Object readResolve() throws ObjectStreamException {
        System.out.println("调用了readResolve方法!");
        return MySingletonHandler.instance;
    }
}

再次运行上面的测试代码,得到的结果如下:

865113938
调用了readResolve方法!
865113938

从运行结果可知,添加readResolve方法后反序列化后得到的实例和序列化前的是同一个实例,单个实例得到了保证。

5 使用static代码块实现单例模式

静态代码块中的代码在使用类的时候就已经执行了,所以可以应用静态代码块的这个特性来实现单例模式。

public class MySingleton{

    private static MySingleton instance = null;

    private MySingleton(){
    }

    static{
        instance = new MySingleton();
    }

    public static MySingleton getInstance() {
        return instance;
    }
}

测试代码如下:

public class MyThread extends Thread{

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(MySingleton.getInstance().hashCode());
        }
    }

    public static void main(String[] args) {

        MyThread[] mts = new MyThread[3];
        for(int i = 0 ; i < mts.length ; i++){
            mts[i] = new MyThread();
        }

        for (int j = 0; j < mts.length; j++) {
            mts[j].start();
        }
    }
}

执行结果如下:

1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954

从运行结果看,单例的线程安全性得到了保证。

6 使用enum枚举数据类型实现单例模式

枚举enum和静态代码块的特性相似,在使用枚举时,构造方法会被自动调用,利用这一特性也可以实现单例。

public enum EnumFactory{
    
    singletonFactory;
    private MySingleton instance;
        
    private EnumFactory(){//枚举类的构造方法在类加载是被实例化
        instance = new MySingleton();
    }
    
    public MySingleton getInstance(){
        return instance;
    }
                                                                                        
}
                                                
class MySingleton{//需要获实现单例的类,比如数据库连接Connection
    public MySingleton(){
    }
}

测试代码如下:

public class MyThread extends Thread{

    @Override
    public void run() {
        System.out.println(EnumFactory.singletonFactory.getInstance().hashCode());
    }

    public static void main(String[] args) {

        MyThread[] mts = new MyThread[10];
        for(int i = 0 ; i < mts.length ; i++){
            mts[i] = new MyThread();
        }

        for (int j = 0; j < mts.length; j++) {
            mts[j].start();
        }
    }
}

以上代码的执行结果为:

1481297610
1481297610
1481297610
1481297610
1481297610
1481297610
1481297610
1481297610
1481297610
1481297610

行结果表明单例得到了保证,但是这样写枚举类被完全暴露了,违反了“职责单一原则”,那我们来看看怎么进行改造呢

7 完善使用enum枚举实现单例模式

不暴露枚举类实现细节的封装代码如下:

public class ClassFactory{

    private enum MyEnumSingleton{
        singletonFactory;

        private MySingleton instance;

        private MyEnumSingleton(){//枚举类的构造方法在类加载是被实例化
            instance = new MySingleton();
        }

        public MySingleton getInstance(){
            return instance;
        }
    }

    public static MySingleton getInstance(){
        return MyEnumSingleton.singletonFactory.getInstance();
    }
}

class MySingleton{//需要获实现单例的类,比如数据库连接Connection
    public MySingleton(){
    }
}

验证单例模式的代码如下:

public class MyThread extends Thread{

    @Override
    public void run() {
        System.out.println(ClassFactory.getInstance().hashCode());
    }

    public static void main(String[] args) {

        MyThread[] mts = new MyThread[10];
        for(int i = 0 ; i < mts.length ; i++){
            mts[i] = new MyThread();
        }

        for (int j = 0; j < mts.length; j++) {
            mts[j].start();
        }
    }
}

以上代码的执行结果为:

1935123450
1935123450
1935123450
1935123450
1935123450
1935123450
1935123450
1935123450
1935123450
1935123450

验证结果表明,完善后的单例实现更为合理。



单例模式线程安全 Share Tweet +1