多线程并发下的单例模式

定义:

单例模式是设计模式中最简单的形式之一。这一模式的目的是使得类的一个对象成为系统中的唯一实例。

下面通过代码分析下java中,各种单例模式写法的优缺点。

1、饿汉模式

示例1.1

public class Singleton {
    private Singleton() {}
    private static Object INSTANCE = new Object();
    public static Object getInstance() {
        return INSTANCE;
    }
}

在类生命周期的【初始化】阶段进行生成单例对象(类的初始化阶段会对静态变量赋值),当执行类初始化的阶段是需要先获得锁才能进行初始化操作,而且一个class类只进行初始化一次。类初始化阶段是线程安全的,JVM保证类初始化只执行一次。这样可以确保只生成一个对象。

类声明周期分为:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸御(Unloading)。 类的生命周期不明白的请查看:JVM 类加载机制深入浅出

类加载后不一定马上执行初始化阶段。当遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。

  1. new 创建对象操作
  2. getstatic 访问类的静态变量操作
  3. putstatic 给类的静态变量赋值操作
  4. invokestatic 调用静态方法操作

这个饿汉模式中,不会出现new、invokestatic和putstatic指令,外面的类只能调用 getInstance()静态方法,由此推断,此单例模式也是延迟加载对象的,只有第一次调用getInstance()静态方法,才会触发他的初始化阶段,才会创建单例对象。

其实这个例子应该是懒汉模式,只有在第一次使用的时候才加载

下面这个【示例1.2】不是延迟加载单例对象

示例1.2

public class Singleton {
    private Singleton() {}
    private static  int count=0;
    private static Object INSTANCE = new Object();
    public static Object getInstance() {
        return INSTANCE;
    }
}

当程序先调用Singleton1中的count属性时(getstatic 或putstatic 指令),就会执行类的【初始化】阶段,会生成单例对象,而不是调用getInstance()静态方法才生成单例对象。

示例1.3 (静态内部类实现方式)

public class Singleton {
    private Singleton() {}
    private static  int count=0;
    private static class SingletonHolder{
        private static final Object INSTANCE = new Object();
    }
    public static Object getInstance(){
        return SingletonHolder.INSTANCE;
    }
}

使用内部类SingletonHolder来防止【示例1.2】出现的问题,防止其它的变量的干扰,导致提前触发类声明周期中的【初始化】阶段来创建INSTANCE 实例。 Effective Java中推荐的单例写法

2、懒汉模式

示例2.1

public class Singleton{
    private Singleton() {   }
    private static Object INSTANCE = null;
    public static Object getInstance() {
        if(INSTANCE == null){
            INSTANCE = new Object();
        }
        return INSTANCE;
    }
}

每次创建INSTANCE 的时候先判断是否null,如果为null则new一个,否则就直接返回INSTANCE 。当多线程工作的时候,如果有多个线程同时运行到if (INSTANCE == null),都判断为null,那么两个线程就各自会创建一个实例。这样就会创建多一个实例,这样就不是单例了。

下面的【示例2.2】加上synchronized 改进多线程并发引起的问题

示例2.2 (synchronized 实现方式)

public class Singleton {
    private Singleton() {   }
    private static Object INSTANCE = null;
    public synchronized static Object getInstance() {
        if(INSTANCE == null){
            INSTANCE = new Object();
        }
        return INSTANCE;
    }
}

虽然synchronized 能解决多线程同时并发引起的问题,但是每次访问该方法都需要获得锁,性能大大降低。其实只要创建INSTANCE 实例后就不需要加锁的,直接获取该对象就ok。

示例2.3 (双重检查实现方式)

public class Singleton {
    private Singleton() {   }
    private static Object INSTANCE = null;
    public static Object getInstance() {
        if(INSTANCE == null){
            synchronized(Singleton3.class){
                if(INSTANCE == null){
                    INSTANCE = new Object();
                }
            }
        }
        return INSTANCE;
    }
}

这个版本的代码看起来有点复杂,注意其中有两次if (instance == null)的判断,这个叫做『双重检查 Double-Check』。

第一个if (instance == null),其实是为了解决【示例2.2】中的效率问题,只有instance为null的时候,才进入synchronized的代码段——这样在对象创建后就不会在进入同步代码块了。 第二个if (instance == null),则是跟【示例2.2】一样,是为了防止可能出现多个实例的情况。

从代码层面看似完美,效率问题也解决了。但实际还是有问题,在并发环境下可能会出现instance为null的情况。下面我们来分析下为什么会出现此问题。

原子操作

INSTANCE = new Object();不是原子操作。 在JVM中会拆分成3个步骤 1、分配对象的内存空间 2、初始化对象 3、设置INSTANCE 指向刚分配的内存地址

指令重排

指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。 可以参考:java内存模型

【2、初始化对象和 3、设置INSTANCE 指向刚分配的内存地址】这两个操作可能发生重排序。 如下图:

指令重排序

从图中可以看出A2和A3的重排序,将导致线程 B在B1处判断出instance不为空,线程B接下来将访问instance引用的对象。此时,线程B将会访 问到一个还未初始化的对象。

示例2.4 (基于volatile的解决方案)

public class Singleton {
    private Singleton() {}
    private static volatile Object INSTANCE = null;
    public static Object getInstance() {
        if(INSTANCE == null){
            synchronized(Singleton.class){
                if(INSTANCE == null){
                    INSTANCE = new Object();
                }
            }
        }
        return INSTANCE;
    }
}

声明对象的引用为volatile后,【2、初始化对象和 3、设置INSTANCE 指向刚分配的内存地址】之间的重排序,在多线程环境中将会被禁止。

volatile的重排序规则

从图表中可以看出volatile可以确保,volatile变量读写顺序,可以保证一个线程写volatile变量完成后(创建完对象后),其它线程才能读取该volatile变量,相当于给这个创建实例的构造上了一把锁。这样,在它的赋值完成之前,就不用会调用读操作。

示例2.5 (枚举实现方式)

public enum Singleton6 {
    INSTANCE;
    public String getInfo(String s){
        s = "hello " + s;
        System.out.println(s);
        return s;
    }
    public static void main(String[] args) {
        String s = INSTANCE.getInfo("aa");
        System.out.println(s);
    }
}

这种写法在功能上与共有域方法相近,但是它更简洁,无偿地提供了序列化机制,绝对防止对此实例化,即使是在面对复杂的序列化或者反射攻击的时候。虽然这中方法还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。


本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Fish

android文件存储

为了输出数据,要把list中存储的写到一个txt文件里,就顺手学了一下 文件存储的方法,说是学,其实又是百度之后复制粘贴。不过学到了一个关于java中的一个知识...

2329
来自专栏Java帮帮-微信公众号-技术文章全总结

【数据库】MySQL进阶七、limit用法& varchar类型排序

【数据库】MySQL进阶七、 limit用法与varchar排序 limit用法 limit是mysql的语法 select * from table limi...

3526
来自专栏Hadoop数据仓库

HAWQ技术解析(十) —— 过程语言

        HAWQ支持用户自定义函数(user-defined functions,UDF),还支持给HAWQ内部的函数起别名。编写UDF的语言可以是SQ...

4765
来自专栏IT可乐

Java设计模式之(一)------单例模式

1、什么是单例模式?   采取一定的办法保证在整个软件系统中,单例模式确保对于某个类只能存在一个实例。有如下三个特点:   ①、单例类只能有一个实例   ②、单...

2227
来自专栏GIS讲堂

数据库 连接(JOIN)

连接运算中有两种最为重要的连接,一种是等值连接(Equijoin),另一种是自然连接(Nature Join):等值连接是从关系R和S中的笛卡尔积中选取A,B属...

2213
来自专栏禁心尽力

数据库设计之数据库,数据表和字段等的命名总结

数据库命名规则: 根据项目的实际意思来命名。 数据表命名规则: 1.数据表的命名大部分都是以名词的复数形式并且都为小写; 2.尽量使用前缀"table_"; 3...

2535
来自专栏博客园

设计模式学习-单例模式

但是这么做不感觉有问题吗?假如这个类我们并不使用或在程序启动很久以后我们才使用,那么这个对象的预创建不就很浪费吗?并且如果这个对象的创建需要很大的资源,那......

783
来自专栏CodingToDie

Schema与数据类型优化(一)

Schema与数据类型优化 Table of Contents 1. 选择优化的数据类型 1.1. 整数类型 1.2. 实数类型 1. 选择优化的数据类型 My...

3252
来自专栏JavaEdge

为什么java中用枚举实现单例模式会更好代码简洁

代码简洁 这是迄今为止最大的优点,如果你曾经在Java5之前写过单例模式代码,那么你会知道即使是使用双检锁你有时候也会返回不止一个实例对象。虽然这种问题通过...

5774
来自专栏技术碎碎念

sql server T-SQL 基础

SQL语言按照用途可以分为如下3类: ①DDL(Data Definition Language)  数据定义语言:    定义修改和删除数据库、表、索引和视图...

2956

扫码关注云+社区

领取腾讯云代金券