单例模式与多线程

概述

关于一般单例模式的创建和分析在我的另一篇博客《Java设计模式——单件模式》中有详细说明。只是在上篇博客中的单例是针对于单线程的操作,而对于多线程却并不适用,本文就从单例模式与多线程安全的角度出发,讲解单例模式在多线程中应该如何被使用。


版权说明

著作权归作者所有。

商业转载请联系作者获得授权,非商业转载请注明出处。

本文作者:Coding-Naga

发表日期: 2016年4月6日

本文链接:http://blog.csdn.net/lemon_tree12138/article/details/51074383

来源:CSDN

更多内容:分类 >> 并发与多线程


一般情况下的单例模式的创建

首先我们基于单例模式来编写一个Student的类。如下:

Student.java

public class Student {

    private static Student student = null;

    private Student() {
    }

    public static Student getInstance() {
        if (student == null) {
            System.out.println("线程" + Thread.currentThread() + "进入,student = " + student);
            student = new Student();
        }
        return student;
    }
}

我们将创建学生类的任务交给一个 Runnable 去完成。

CreateRunnable.java

public class Createable implements Runnable {

    @Override
    public void run() {
        Student student = Student.getInstance();
        System.out.println("学生类被创建:" + student);
        System.out.println("Hashcode:" + student.hashCode());
    }

}

如下是测试代码:

Client.java

public class Client {

    public static void main(String[] args) {
        Thread thread1 = new Thread(new Createable());
        Thread thread2 = new Thread(new Createable());
        thread1.start();
        thread2.start();
    }
}

运行结果

线程Thread[Thread-0,5,main]进入,student = null
线程Thread[Thread-1,5,main]进入,student = null
学生类被创建:org.naga.demo.thread.singleton.Student@1dbaf954
学生类被创建:org.naga.demo.thread.singleton.Student@6df90bbf
Hashcode:1845038015
Hashcode:498792788

从上面程序的运行结果来看,很明显这里创建了两个不同的对象。这与单例模式的定义相悖了。因为在多线程环境下,很明显 getInstance() 方法不能保证原子性,所以这种方法在多线程下是不安全的。


基于 synchronized 的同步解决方案

在一般情况下的单例模式的创建中,我们知道那是一种不安全的创建对象的方案。那么就很容易想到用多线程同步的方法来解决,就是使用关键字 synchronized 来实现同步策略。使用 synchronized 之后的代码及运行结果如下:

Student.java

public synchronized static Student getInstance() {
        if (student == null) {
            System.out.println("线程" + Thread.currentThread() + "进入,student = " + student);
            student = new Student();
        }
        return student;
    }

运行结果

线程Thread[Thread-0,5,main]进入,student = null
学生类被创建:org.naga.demo.thread.singleton.Student@1dbaf954
Hashcode:498792788
学生类被创建:org.naga.demo.thread.singleton.Student@1dbaf954
Hashcode:498792788

从运行结果上可以看出,这里的同步策略是有效的,Thread-0 和 Thread-1 创建的是同一个对象。而关于 synchronized 关键字的详细说明请参见《Java多线程之synchronized和volatile的比较》一文。

不过,对于系统而言,synchronized 同步策略的实现其实是一项性能开销非常大的操作。这可能是 synchronized 需要对对象加锁的缘故。


基于双重检查锁定的解决方案

方案分析及测试

上面说到 synchronized 同步策略对性能开销比较大,对于可能存在大量的 getInstance() 方法调用时,对于系统而言可能就会难以负荷或运行缓慢。这里想到的方法就是减少对 synchronized 关键字的调用。也就是下面要说的双重检查锁定。

Student.java

public class Student {
    ... ...
    public static Student getInstance() {
        System.out.println("线程" + Thread.currentThread() + "进入,student = " + student);
        if (student == null) {
            synchronized(Student.class) {
                if (student == null) {
                    student = new Student();
                }
            }
        }
        return student;
    }
}

运行结果-1

线程Thread[Thread-0,5,main]进入,student = null
学生类被创建:org.naga.demo.thread.singleton.Student@386f4317
学生类被创建:org.naga.demo.thread.singleton.Student@386f4317
Hashcode:946815767
Hashcode:946815767

运行结果-2

线程Thread[Thread-1,5,main]进入,student = null
学生类被创建:org.naga.demo.thread.singleton.Student@1dbaf954
Hashcode:498792788
线程Thread[Thread-0,5,main]进入,student = org.naga.demo.thread.singleton.Student@1dbaf954
学生类被创建:org.naga.demo.thread.singleton.Student@1dbaf954
Hashcode:498792788

这里的结果是没有问题的。只是你可能会有疑问,为什么这里采用双重检查锁定?之前我们不是已经对 student 对象进行了判空操作了么,这里怎么还要进行第二次判空?其实在理解了多线程执行的过程,这个问题也就很好回答了。假定有两个线程 T-0 和 T-1,它们现在同时到达第一个 if (student == null) 判空操作,那么这两个线程都可以进入到 if (student == null) 的内部,因为在此之前对象的访问还没有被锁定;这个时候,如果 T-0 获得了锁,并对对象进行初始化操作,结束后释放锁;然后 T-1 获得了 T-0 释放的锁,如果这里不进行第二次判空操作的话,那么 T-1 也会创建一个对象,这个对象与 T-0 创建的是两个完全不同的对象。而如果这里我们进行了第二次判空操作,那么 T-1 得到的对象不为空,就不会再次创建新的对象了。这个方案设计得十分巧妙,既解决了同步带来的性能开销,又保证了单例模式的构建。

存在的问题

对于这一小节,我本人还没有找到一个可以正确测试的方法。这里所作的逻辑说明是来自于《Java 并发编程的艺术》一书。如果你有好的验证方法,欢迎以评论的方式与我交流,共同进步。

这里介绍的双重检查锁定的方案,这的确是一个很巧妙的设计。不过也存在一些细微的问题,这个问题就在于 student = new Student(); 这句代码。对于通过 new 创建对象的过程可以分解成以下3行伪代码。

memory = allocate();    // 1: 分配对象的内存空间
ctorInstance(memory);   // 2: 初始化对象
instance = memory;      // 3: 设置 instance 指向刚分配的内存地址

而这里的2、3两个步骤可以被重排序,重排序的结果就像下面的这样:

memory = allocate();    // 1: 分配对象的内存空间
instance = memory;      // 2: 设置 instance 指向刚分配的内存地址
                        // 这时,memory处的对象还没有被初始化
ctorInstance(memory);   // 3: 初始化对象

因为这个重排序的过程,所以这里就有一个问题了。假设有一个线程 T-0 当前执行到上面重排序后伪代码的第2步完成,第3步还没开始时,有一个线程 T-1 进来了,要进行第一次 if (student == null) 判断。因为这里 instance 已经被指向了 memory 分配的地址了。所以,这时 T-1 判断的对象是一个未被初始化的对象。这样就出现了下面这样的输出了。

线程Thread[Thread-1,5,main]进入,student = null
学生类被创建:org.naga.demo.thread.singleton.Student@1dbaf954
Hashcode:498792788
线程Thread[Thread-0,5,main]进入,student = org.naga.demo.thread.singleton.Student@1dbaf954
学生类被创建:org.naga.demo.thread.singleton.Student@1dbaf954
Hashcode:498792788

尽管如此,我们还是不能直接就说出了问题,因为这里也有可能 T-1 就是在 T-0 对对象创建完成之后才进来的。这里还是看看最佳的实践方案吧。


基于 volatile 的解决方案

上面介绍了双重检查锁定存在的一些弊端,不过我们还是有办法解决的。只要对 student 对象进行 volatile 关键字修饰即可。

Student.java

public class Student {
    private volatile static Student student = null;
    ... ...
}

运行结果

线程Thread[Thread-0,5,main]进入,student = null
学生类被创建:org.naga.demo.thread.singleton.Student@6df90bbf
学生类被创建:org.naga.demo.thread.singleton.Student@6df90bbf
Hashcode:1845038015
Hashcode:1845038015

这样就保证了多线程之间,对共享变量的可见性。


基于类初始化的解决方案

在类的初始化阶段(即在Class被加载之后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获得一个锁。这个锁可以同步多个线程对同一个类的同步初始化。

Student.java

public class Student {

    private Student() {
    }

    private static class StudentHolder {
        private final static Student instance = new Student();
    }

    public static Student getInstance() {
        return StudentHolder.instance;
    }
}

运行结果

学生类被创建:org.naga.demo.thread.singleton.Student@386f4317
学生类被创建:org.naga.demo.thread.singleton.Student@386f4317
Hashcode:946815767
Hashcode:946815767

Ref

  • 《Java 多线程编程核心技术》
  • 《Java 并发编程的艺术》

时间: 2024-10-09 07:04:33

单例模式与多线程的相关文章

单例模式在多线程中的使用情况

废话不多说,直接上代码: class MyThreadScopeData{ private MyThreadScopeData(){} private static MyThreadScopeData instance; //单例设计模式 public static MyThreadScopeData getInstance(){ if(instance ==null){ instance = new MyThreadScopeData(); } return instance; } } 上述代

单例模式在多线程环境下的lazy模式为什么要加两个if(instance==null)

刚才在看阿寻的博客”C#设计模式学习笔记-单例模式“时,发现了评论里有几个人在问单例模式在多线程环境下为什么lazy模式要加两个if进行判断,评论中的一个哥们剑过不留痕,给他们写了一个demo来告诉他们为什么. 我看了一下这个demo,确实说明了这个问题,但我认为不够直观,呵呵,于是我就稍微的改了一下. 这是剑过不留痕的demo using System; using System.Threading; namespace SingletonPattern { class Program { s

Java多线程核心技术(五)单例模式与多线程

本文只需要考虑一件事:如何使单例模式遇到多线程是安全的.正确的 1.立即加载 / "饿汉模式" 什么是立即加载?立即加载就是使用类的时候已经将对象创建完毕,常见的实现办法就是直接 new 实例化. public class MyObject { private static MyObject myObject = new MyObject(); public MyObject(){ } public static MyObject getInstance(){ return myObj

如何保证单例模式在多线程中的线程安全性

对大数据.分布式.高并发等知识的学习必须要有多线程的基础.这里讨论一下如何在多线程的情况下设计单例模式.在23中设计模式中单例模式是比较常见的,在非多线程的情况下写单例模式,考虑的东西会很少,但是如果将多线程和单例模式结合起来,考虑的事情就变多了,如果使用不当(特别是在生成环境中)就会造成严重的后果.所以如何使单例模式在多线程中是安全的显得尤为重要,下面介绍各个方式的优缺点以及可用性: 1.立即加载(饿汉模式) 立即加载模式就是在调用getInstance()方法前,实例就被创建了,例: pub

浅淡java单例模式结合多线程测试

本人才疏学浅,正好利用博客这个平台整理下思路 使用单例模式简单来说生成对象时属性都一样,即你new一百次,通过方法得到的结果都一样(比如获取静态资源文件,工具类等). 所以就没必要生成多个对象浪费服务器内存,他和静态类又不同,因为单例本质也是对象系统,长期不使用,也会给cg清除.但是静态类不同,静态类的成员变量和有静态方法会在程序的整个生命周期存在,比如在服务器内在中加载后服务器不关,就会一直存在,同理的有servlet的ServletContext对象和jsp的application对象 单例

彻头彻尾理解单例模式与多线程

摘要: 本文首先概述了单例模式产生动机,揭示了单例模式的本质和应用场景.紧接着,我们给出了单例模式在单线程环境下的两种经典实现:饿汉式 和 懒汉式,但是饿汉式是线程安全的,而懒汉式是非线程安全的.在多线程环境下,我们特别介绍了五种方式来在多线程环境下创建线程安全的单例,使用 synchronized方法.synchronized块.静态内部类.双重检查模式 和 ThreadLocal 实现懒汉式单例,并总结出实现效率高且线程安全的单例所需要注意的事项. 版权声明: 本文原创作者:书呆子Rico

单例模式在多线程下的多种实现模式

单例模式是23种设计模式中比较常见的设计模式,又因为其代码量精简,所以经常会被用在在面试中测试面试者的能力. 初级的单例模式很简单 实现两个要求 1构造方法私有化 2对外提供静态的,公开的获取对象的方法 所以:初级单例模式如下 public class Singelton {private Singelton(){} private static Singelton sin=null;public static Singelton getSingelton(){           if(sin

单例模式 和 多线程

饿汉模式又称为立即加载模式,含以上就是非常急 也就是在使用类的时候已经将对象创建完毕 package lock; public class EhanSingleton { /*饿汉加载模式/立即加载模式*/ //初始化构造函数 private EhanSingleton(){ } private static EhanSingleton ehan = new EhanSingleton(); public static EhanSingleton getInstance(){ try { Thr

6 单例模式及其多线程问题

一.单例模式 单例模式可以保证一个类仅有一个实例,这个模式应该更简单工厂一样常用了吧,但对我来说,以前都是瞎用,这是第一次深度学习单例模式. 最简单的单例模式代码是这样的: class Singleton { private static Singleton _instance; private Singleton() { } public static Singleton GetInstance() { if (_instance == null) _instance = new Single