单例模式与多线程

本文的知识点非常重要,通过单例模式与多线程技术相结合,在这个过程中能发现很多以前未考虑过的情况,一些不良的程序设计方法如果应用在商业项目中,将会遇到非常大的麻烦。本文的案例也将充分说明,线程与某些技术相结合时要考虑的事情有很多。在学习本文时只需要考虑一件事情,那就是:如何使单例模式遇到多线程是安全的、正确的。

在标准的23个设计模式中,单例设计模式在应用中是比较常见的。但在常规的该模式教学资料介绍中,多数并没有结合多线程技术作为参考,这就造成在使用多线程技术的单例模式时会出现一些意想不到的情况,这样的代码如果在生产环境中出现异常,有可能造成灾难性的后果。本文将介绍单例模式结合多线程技术在使用时的相关知识。

1. 立即加载/“饿汉模式”

什么是立即加载?立即加载就是使用类的时候已经将对象创建完毕,常见的实现办法就是直接new实例化。而立即加载从中文的语境来看,有“着急”、“急迫”的含义,所以也称为“饿汉模式”。
立即加载/“饿汉模式”是在调用方法前,实例已经被创建了,来看一下实现代码。

创建测试用的项目,创建类MyObject.java代码如下:

public class MyObject {

    // 立即加载方式==饿汉模式
    private static MyObject myObject = new MyObject();

    private MyObject() {
    }

    public static MyObject getInstance() {
        // 此代码版本为立即加载
        // 此版本代码的缺点是不能有其它实例变量
        // 因为getInstance()方法没有同步
        // 所以有可能出现非线程安全问题
        return myObject;
    }

}

创建线程类MyThread.java代码如下:

import test.MyObject;

public class MyThread extends Thread {

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

}

创建运行类Run.java代码如下:

import extthread.MyThread;

public class Run {

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

        t1.start();
        t2.start();
        t3.start();

    }

}

程序运行后的结果如下:

1031548957
1031548957
1031548957

控制台打印的hashCode是同一个只,说明对象是同一个,也就实现了立即加载型单例设计模式。

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

什么是延迟加载?延迟加载就是在调用get()方法时实例才被创建,常见的实现办法就是在get()方法中进行new实例化。而延迟加载从中文的语境来看,是“缓慢”、“不急迫”的含义,所以也称为“懒汉模式”。

2.1 延迟加载/“懒汉模式”解析

延迟加载/“懒汉模式”是在调用方法时实例才被创建。一起来看一下实现代码。

创建类MyObject.java代码如下:

public class MyObject {

    private static MyObject myObject;

    private MyObject() {
    }

    public static MyObject getInstance() {
        // 延迟加载
        if (myObject != null) {
        } else {
            myObject = new MyObject();
        }
        return myObject;
    }

}

创建线程类MyThread.java代码如下:

import test.MyObject;

public class MyThread extends Thread {

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

}

创建运行类Run.java代码如下:

import extthread.MyThread;

public class Run {

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.start();
    }

}

程序运行后的效果如下:

1031548957

此实验虽然取得一个对象的实例,但如果是在多线程的环境中,就会出现去除多个实例的情况,与单例模式的初衷是相背离的。

2.2 延迟加载/“懒汉模式”的缺点

前面两个实验虽然使用“立即加载”和“延迟加载”实现了单例设计模式,但在多线程的环境中,前面“延迟加载”示例中的代码完全就是错误的,根本补鞥呢实现保持单例的状态。来看一下如何在多线程环境中结合“错误的单例模式”创建出“多例”。
创建类MyObject.java代码如下:

public class MyObject {

    private static MyObject myObject;

    private MyObject() {
    }

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

}

创建线程类MyThread.java代码如下:

import test.MyObject;

public class MyThread extends Thread {

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

}

创建运行类Run.java代码如下:

import extthread.MyThread;

public class Run {

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

        t1.start();
        t2.start();
        t3.start();

    }

}

程序运行后的效果如下:

186668687
903341402
1770731361

控制台打印出了3中hashCode,说明创建了3个对象,并不是单例的,这就是“错误的单例模式”。如何解决呢?先看一下解决方案。

2.3 延迟加载/“懒汉模式”的解决方案

2.3.1 声明synchronized关键字

既然多个线程可以同时进入getInstance()方法,那么只需要对getInstance()方法声明synchronized关键字即可。

创建类MyObject.java代码如下:

public class MyObject {

    private static MyObject myObject;

    private MyObject() {
    }

    // 设置同步方法效率太低了
    // 整个方法被上锁
    synchronized public static MyObject getInstance() {
        try {
            if (myObject != null) {
            } else {
                // 模拟在创建对象之前做一些准备性的工作
                Thread.sleep(3000);
                myObject = new MyObject();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myObject;
    }

}

创建线程类MyThread.java代码如下:

import test.MyObject;

public class MyThread extends Thread {

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

}

创建运行类Run.java代码如下:

import extthread.MyThread;

public class Run {

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

        t1.start();
        t2.start();
        t3.start();

    }

}

程序运行后的结果如下:

531436928
531436928
531436928

此方法加入同步synchronized关键字得到相同实例的对象,但此种方法的运行效率非常低下,是同步运行的,下一个线程想要取得对象,则必须等上一个线程释放锁之后,才可以继续执行。

2.3.2 尝试同步代码块

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

创建类MyObject.java代码如下:

public class MyObject {

    private static MyObject myObject;

    private MyObject() {
    }

    public static MyObject getInstance() {
        try {
            // 此种写法等同于:
            // synchronized public static MyObject getInstance()
            // 的写法,效率一样很低,全部代码被上锁
            synchronized (MyObject.class) {
                if (myObject != null) {
                } else {
                    // 模拟在创建对象之前做一些准备性的工作
                    Thread.sleep(3000);

                    myObject = new MyObject();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myObject;
    }

}

创建线程类MyThread.java代码如下:

import test.MyObject;

public class MyThread extends Thread {

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

}

创建运行类Run.java代码如下:

import extthread.MyThread;

public class Run {

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

        t1.start();
        t2.start();
        t3.start();

        // 此版本代码虽然是正确的
        // 但public static MyObject getInstance()方法
        // 中的全部代码都是同步的了,这样做有损效率
    }

}

程序运行后的结果如下:

903341402
903341402
903341402

此方法加入同步synchronized语句块得到相同实例的对象,但此种方法的运行效率也是非常低的,和synchronized同步方法一样是同步运行的。继续更改代码尝试解决这个缺点。

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

同步代码块可以针对某些重要的代码进行单独的同步,而其他的代码则不需要同步。这样在运行时,效率完全可以得到大幅提升。

创建MyObject.java代码如下:

public class MyObject {

    private static MyObject myObject;

    private MyObject() {
    }

    public static MyObject getInstance() {
        try {
            if (myObject != null) {
            } else {
                // 模拟在创建对象之前做一些准备性的工作
                Thread.sleep(3000);
                // 使用synchronized (MyObject.class)
                // 虽然部分代码被上锁
                // 但还是有非线程安全问题
                synchronized (MyObject.class) {
                    myObject = new MyObject();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myObject;
    }

}

创建线程类MyThread.java代码如下:

import test.MyObject;

public class MyThread extends Thread {

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

}

创建运行类Run.java代码如下:

import extthread.MyThread;

public class Run {

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

        t1.start();
        t2.start();
        t3.start();

    }

}

程序运行后的结果如下:

903341402
1770731361
1684444739

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

2.3.4 使用DCL双检查锁机制

在最后的步骤中,使用的是DCL双检查锁机制来实现多线程环境中的延迟加载单例设计模式。

创建类MyObject.java代码如下:

public class MyObject {

    private volatile static MyObject myObject;

    private MyObject() {
    }

    // 使用双检测机制来解决问题
    // 即保证了不需要同步代码的异步
    // 又保证了单例的效果
    public static MyObject getInstance() {
        try {
            if (myObject != null) {
            } else {
                // 模拟在创建对象之前做一些准备性的工作
                Thread.sleep(3000);
                synchronized (MyObject.class) {
                    if (myObject == null) {
                        myObject = new MyObject();
                    }
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myObject;
    }
    // 此版本的代码称为:
    // 双重检查Double-Check Locking

}

创建线程类MyThread.java代码如下:

import test.MyObject;

public class MyThread extends Thread {

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

}

创建运行类Run.java代码如下:

import extthread.MyThread;

public class Run {

    public static void main(String[] args) {
        
        
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

        t1.start();
        t2.start();
        t3.start();

    }

}

程序运行后的结果如下:

186668687
186668687
186668687

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

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

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

创建类MyObject.java代码如下:

public class MyObject {

    // 内部类方式
    private static class MyObjectHandler {
        private static MyObject myObject = new MyObject();
    }

    private MyObject() {
    }

    public static MyObject getInstance() {
        return MyObjectHandler.myObject;
    }

}

创建线程类MyThread.java代码如下:

import test.MyObject;

public class MyThread extends Thread {

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

}

创建运行类Run.java代码如下:

import extthread.MyThread;

public class Run {

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

        t1.start();
        t2.start();
        t3.start();

    }

}

程序运行后的结果如下:

531436928
531436928
531436928

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

静态内置类可以达到线程安全问题,但如果遇到序列化对象时,使用默认的方式运行得到的结果还是多例的。

创建MyObject.java代码如下:

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

public class MyObject implements Serializable {

    private static final long serialVersionUID = 888L;

    // 内部类方式
    private static class MyObjectHandler {
        private static final MyObject myObject = new MyObject();
    }

    private MyObject() {
    }

    public static MyObject getInstance() {
        return MyObjectHandler.myObject;
    }

    protected Object readResolve() throws ObjectStreamException {
        System.out.println("调用了readResolve方法!");
        return MyObjectHandler.myObject;
    }

}

创建业务类SaveAndRead.java代码如下:

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;

import test.MyObject;

public class SaveAndRead {

    public static void main(String[] args) {
        try {
            MyObject myObject = MyObject.getInstance();
            FileOutputStream fosRef = new FileOutputStream(new File(
                    "myObjectFile.txt"));
            ObjectOutputStream oosRef = new ObjectOutputStream(fosRef);
            oosRef.writeObject(myObject);
            oosRef.close();
            fosRef.close();
            System.out.println(myObject.hashCode());
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        try {
            FileInputStream fisRef = new FileInputStream(new File(
                    "myObjectFile.txt"));
            ObjectInputStream iosRef = new ObjectInputStream(fisRef);
            MyObject myObject = (MyObject) iosRef.readObject();
            iosRef.close();
            fisRef.close();
            System.out.println(myObject.hashCode());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }

}

程序运行后的效果如下:

1836019240
81628611

解决办法就是在反序列化中使用readResolve()方法。
去掉如下代码的注释:

    protected Object readResolve() throws ObjectStreamException {
        System.out.println("调用了readResolve方法!");
        return MyObjectHandler.myObject;
    }

程序运行后的结果如下:

1836019240
调用了readResolve方法!
1836019240

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

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

创建MyObject.java代码如下:

public class MyObject {

    private static MyObject instance = null;

    private MyObject() {
    }

    static {
        instance = new MyObject();
    }

    public static MyObject getInstance() {
        return instance;
    }

}

创建线程类MyThread.java代码如下:

import test.MyObject;

public class MyThread extends Thread {

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

创建运行类Run.java代码如下:

import extthread.MyThread;

public class Run {

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

        t1.start();
        t2.start();
        t3.start();

    }

}

程序运行后的结果如下:

186668687
186668687
186668687
186668687
186668687
186668687
186668687
186668687
186668687
186668687
186668687
186668687
186668687
186668687
186668687

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

枚举enum和静态代码块的特性相似,在使用枚举类时,构造方法会被自动调用,也可以应用其这个特性实现单例设计模式。

创建类MyObject.java代码如下:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public enum MyObject {
    connectionFactory;

    private Connection connection;

    private MyObject() {
        try {
            System.out.println("调用了MyObject的构造");
            String url = "jdbc:mysql://localhost:3306/test";
            String username = "root";
            String password = "root";
            String driverName = "com.mysql.jdbc.Driver";
            Class.forName(driverName);
            connection = DriverManager.getConnection(url, username, password);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public Connection getConnection() {
        return connection;
    }
}

创建线程类MyThread.java代码如下:

import test.MyObject;

public class MyThread extends Thread {

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

创建运行类Run.java代码如下:

import extthread.MyThread;

public class Run {

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

        t1.start();
        t2.start();
        t3.start();

    }
}

程序运行后的结果如下:

调用了MyObject的构造
482793510
482793510
482793510
482793510
482793510
482793510
482793510
482793510
482793510
482793510
482793510
482793510
482793510
482793510
482793510

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

前面一节将枚举类进行暴露,违反了“职责单一原则”,在本节中进行完善。

更改类MyObject.java代码如下:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class MyObject {

    public enum MyEnumSingleton {
        connectionFactory;

        private Connection connection;

        private MyEnumSingleton() {
            try {
                System.out.println("创建MyObject对象");
                String url = "jdbc:mysql://localhost:3306/test";
                String username = "root";
                String password = "root";
                String driverName = "com.mysql.jdbc.Driver";
                Class.forName(driverName);
                connection = DriverManager.getConnection(url, username,
                        password);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

        public Connection getConnection() {
            return connection;
        }
    }

    public static Connection getConnection() {
        return MyEnumSingleton.connectionFactory.getConnection();
    }

}

更改MyThread.java类代码如下:

import test.MyObject;

public class MyThread extends Thread {

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

程序运行的结果如下:

创建MyObject对象
2007882269
2007882269
2007882269
2007882269
2007882269
2007882269
2007882269
2007882269
2007882269
2007882269
2007882269
2007882269
2007882269
2007882269
2007882269
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容