Java - 单例模式与多线程

单例模式大家并不陌生,分为饿汉式和懒汉式等。

线程安全的饿汉式单例

饿汉式单例在类第一次加载的时候就完成了初始化,上代码:

public class MyObject {
    private static MyObject myObject = new MyObject();
    public static MyObject getInstance(){
        return myObject;
    }
}

下面来验证饿汉式单例的线程安全性:

public class MyThread extends Thread{
    public void run() {
        System.out.println(MyObject.getInstance().hashCode());
    }
}
public class Test {
    public static void main(String[] args) throws Exception {
        Thread t1 = new MyThread();
        Thread t2 = new MyThread();
        Thread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

输出:

763347431
763347431
763347431

三次输出 hashCode 是同一个值,说明饿汉式单例天生就是线程安全的。

结论:饿汉式单例在类第一次加载的时候完成初始化,而且是线程安全的。

非线程安全的懒汉式单例

懒汉式单例为延迟加载,调用 getInstance() 时才被创建,上代码:

public class MyObject {
    private static MyObject myObject = null;    public static MyObject getInstance(){
        if(myObject == null){
            myObject = new MyObject();
        }
        return myObject;
    }
}

这种情况下肯定是非线程安全的。因为判断对象为空和创建对象是一个原子性操作,多线程访问产生竞态条件。(竞态条件:多线程并发中,正确的结果取决于多个线程的执行顺序)

下面做一下验证:

public class MyThread extends Thread{
    public void run() {
        System.out.println(MyObject.getInstance().hashCode());
    }
}
public class Test {
    public static void main(String[] args) throws Exception {
        Thread t1 = new MyThread();
        Thread t2 = new MyThread();
        Thread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

输出:

2146509683
810456228
1996534722

hashcode 输出不同,很明显懒汉式单例不具有线程安全性。

结论:懒汉式单例在需要该实例的时候才会进行初始化(仅初始化一次),但却是非线程安全的。在单例类数量少的情况下,这样一个延迟加载和饿汉式单例相比在性能上又没有明显的差距,所以我一般不会选择这种初级的懒汉式单例。

基于 volatile 的 DCL 方案为懒汉式单例提供线程安全性

在懒汉式单例的基础上,我们做一些优化使其具有线程安全的特性。

第一次尝试:

为 getInstance() 加上 synchronized 关键字

public class MyObject {
    private static MyObject myObject = null;    synchronized public static MyObject getInstance(){
        if(myObject == null){
            myObject = new MyObject();
        }
        return myObject;
    }
}

输出

2000544445
2000544445
2000544445

得到的是单例,不过如果多个线程频繁调用 getInstance() 的话将会导致程序执行效率非常低下:下个线程想要取得对象锁,必须等到上一个线程释放锁之后才可以继续执行。

第二次尝试:

同步代码块

public class MyObject {
    private static MyObject myObject = null;    public static MyObject getInstance() {
        synchronized (MyObject.class) {
            if (myObject == null) {
                myObject = new MyObject();
            }

        }
        return myObject;
    }
}

输出

512965639
512965639
512965639

然而这种方法等同于上一种尝试,唯一区别就是多线程下,上一次尝试中线程被堵在门口一米处进不来,而这一次尝试中线程被堵在门口半米处进不来。

第三次尝试:

继续缩小同步的代码范围

public class MyObject {
    private static MyObject myObject = null;    public static MyObject getInstance() {
        if (myObject == null) {            synchronized (MyObject.class) {
                myObject = new MyObject();
            }
        }
        return myObject;
    }
}

输出

1175759956
2000544445
2146509683

相比前面几次尝试,执行效率会有显著提升,但是无法得到单例。因为破坏了原子性。

第四次尝试:

下面的代码使用了 DCL(双检查锁机制 double check lock )

public class MyObject {
    private static MyObject myObject = null;    public static MyObject getInstance() {
        if (myObject == null) {            synchronized (MyObject.class) {
                if (myObject == null) {
                    myObject = new MyObject();
                }
            }
        }
        return myObject;
    }
}

输出:

1320194849
1320194849
1320194849

然而此时就是线程安全了吗?

No~ 不过我给不出反例推到,大家有经验的请指教一下~

第五次尝试:

我们还需要为“单例”加上 volatile 关键字。

public class MyObject {
    volatile private static MyObject myObject = null;
    public static MyObject getInstance() {
        if (myObject == null) {
            synchronized (MyObject.class) {
                if (myObject == null) {
                    myObject = new MyObject();
                }
            }
        }
        return myObject;
    }
}

此时才算是线程安全。

回头来看,“单例” 为何必须用 volatile 修饰呢 ?

因为关键字 volatile 能够禁止指令重排序。

Example - “单例” 在没有 volatile 修饰的情况下,线程A 和线程B 都是第一次调用该单例方法,线程A 先执行构造方法;然而该构造方法是一个非原子操作,编译后生成多条指令,由于指令重排序,可能会先执行赋值操作(实际是在内存中开辟一片存储对象的区域后直接返回内存的引用),之后 myObject 便不为空了,但是实际的初始化操作却还没有执行,如果就在此时线程B 进入,就会看到一个不为空的但是不完整(没有完成初始化)的对象。而 volatile 关键字能够禁止指令重排序优化,从而安全的实现单例。

结论:基于 volatile的 DCL方案能够保留延迟加载的特性,同时赋予了该单例线程安全性。

静态内部类实现单例模式

public class MyObject {
    private static class Holder{
        private static MyObject myObject = new MyObject();
    }

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

怎样保证的线程安全? 会不会因为多个线程同时调用 getInstance() 出现指令重排导致初始化不完全呢?

JVM 在加载 class 之后就会进行类的初始化,在类的初始化期间,JVM 会去获取一个初始化锁,这个锁可以同步多个线程对同一个类的初始化,因此指令重排还是可能发生的,但是并不影响获得初始化锁的下一个线程,因为下一个线程进来的时候,上个线程已经完成了类的初始化。

从书上搞了个图过来:

结论:静态内部类实现的单例模式能够保证线程安全,同时具有延迟加载特性。而且代码够简洁哦。

时间: 2024-10-06 08:04:37

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

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

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

java单例模式,多线程下实现

单例模式 必备条件: 1:private的构造方法. 2:private static 对象保存该类实例. 3:static方法返回该类实例. (一)饿汉模式 /** * 单例模式 * 1:线程安全实现 * 2:浪费内存 * @author 祥少 * */public class SingletonTest { //final关键字,导致一旦被初始化,一直占用该段内存(即使你不使用)    public static final SingletonTest singletonTest = new

设计模式——单例模式(Java)——考虑多线程环境下的线程安全问题

设计模式--单例模式(Java)--考虑多线程环境下的线程安全问题 一:单例模式概念 单例模式是一种常用的软件设计模式.在它的核心结构中只包含一个被称为单例的特殊类.通过单例模式可以保证系统中一个类只有一个实例 二:单例模式的实现方式 特别注意,在多线程环境下,需要对获取对象实例的方法加对象锁(synchronized) 方式一:(懒汉式)程序执行过程中需要这个类的对象,再实例化这个类的对象 步骤: 1.定义静态私有对象 2.构造方法私有化保证在类的外部无法实例化该类的对象 3.定义对外开放的静

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

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

单例模式与多线程

概述 关于一般单例模式的创建和分析在我的另一篇博客<Java设计模式--单件模式>中有详细说明.只是在上篇博客中的单例是针对于单线程的操作,而对于多线程却并不适用,本文就从单例模式与多线程安全的角度出发,讲解单例模式在多线程中应该如何被使用. 版权说明 著作权归作者所有. 商业转载请联系作者获得授权,非商业转载请注明出处. 本文作者:Coding-Naga 发表日期: 2016年4月6日 本文链接:http://blog.csdn.net/lemon_tree12138/article/det

【深入】java 单例模式(转)

[深入]java 单例模式 关于单例模式的文章,其实网上早就已经泛滥了.但一个小小的单例,里面却是有着许多的变化.网上的文章大多也是提到了其中的一个或几个点,很少有比较全面且脉络清晰的文章,于是,我便萌生了写这篇文章的念头.企图把这个单例说透,说深入.但愿我不会做的太差. 首先来看一个典型的实现: 1 /** 2 * 基础的单例模式,Lazy模式,非线程安全 3 * 优点:lazy,初次使用时实例化单例,避免资源浪费 4 * 缺点:1.lazy,如果实例初始化非常耗时,初始使用时,可能造成性能问

JAVA单例模式的实践

单例模式是JAVA设计模式中最常用.最重要的设计模式之一. 最简单的写法是: public class TestSingleton { private static String ourInstance = null; //私有化构造器 private TestSingleton() { } //提供静态方法给外部访问 public static String getOurInstance(){ if(ourInstance == null){ ourInstance="单例模式赋值成功!!!&

Java 单例模式探讨

以下是我再次研究单例(Java 单例模式缺点)时在网上收集的资料,相信你们看完就对单例完全掌握了 Java单例模式应该是看起来以及用起来简单的一种设计模式,但是就实现方式以及原理来说,也并不浅显哦. 总结一下我所知道的单例模式实现方式: 1.预先加载法 Java代码 class S1 { private S1() { System.out.println("ok1"); } private static S1 instance = new S1(); public static S1

你所知道的Java单例模式并不是单例模式

当我们搜索单例模式的时候,能看到很多例子,什么懒汉式.饿汉式,大概如下: public class Singleton { private static Singleton instance=null; private Singleton(){ System.out.println("Singleton..init..."); } public static Singleton getInstance(){ if(instance==null){ instance=new Single