单例模式绝对没有你想象的那么简单!不服来战!
Posted 程序编织梦想
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单例模式绝对没有你想象的那么简单!不服来战!相关的知识,希望对你有一定的参考价值。
一、前言
单例模式(Singleton Pattern)是 Java 中最常用的设计模式之一,同时也是面试的重灾区。有些人可能觉的单例模式很简单,没有什么难的。其实不然,因为牵扯到线程安全的问题,所以单例模式绝对能体现出你的功底。不信接着往下看。
二、单例模式详解
单例模式大体分为二种写法:饿汉式和懒汉式。
1.饿汉式
这种方式最简单,所以我们先把这种方式介绍一下,代码如下:
public class Singleton
private static Singleton instance = new Singleton();
private Singleton ()
public static Singleton getInstance()
return instance;
这种方式优点就是效率高、写法简单并且是线程安全的。但是缺点就是在类加载的时候就要初始化,浪费内存。
饿汉式单例天生就是线程安全的。饿汉式在类加载过程中就会初始化,因为类在加载过程中会加锁,所以线程安全。
2.懒汉式
懒汉式的特点是在第一次调用的时候才初始化,避免浪费内存。懒汉式是面试重灾区,为了让大家了解每一种写法存在的问题,我们从简单到复杂一步步写。
2.1 懒汉式初级写法(线程不安全):
public class Singleton
private static Singleton instance;
private Singleton ()
public static Singleton getInstance()
if (instance == null) // 问题出在这里,导致线程不安全。
instance = new Singleton();
return instance;
这个写法之所以不安全,是因为当有多个线程同时进入 if (singleton2 == null) … 语句块的时候,该单例类有可能会创建出多个实例,违背单例模式的初衷,因此,传统的懒汉式单例是非线程安全的。
2.2 懒汉中级写法(线程安全)
既然上面的写法线程不安全,那么我们在getInstance()方法上加一把锁,代码如下:
// 线程安全的懒汉式单例
public class Singleton
private static Singleton singleton;
private Singleton()
// 使用 synchronized 修饰,临界资源的同步互斥访问
public static synchronized Singleton getSingleton()
if (singleton2 == null)
singleton2 = new Singleton2();
return singleton2;
该实现与上面2.1传统懒汉式单例的实现唯一的差别就在于:是否使用 synchronized 修饰 getSingleton()方法。若使用,就保证了对临界资源的同步互斥访问,也就保证了单例。
这种写法乍一看没问题,但是这种实现方式的运行效率会很低,因为我们把整个getSingleton()加锁,同步块的作用域有点大,而且锁的粒度有点粗,所以我们继续升级写法。
2.3 双重校验锁写法(线程不安全)
public class Singleton
private static Singleton singleton;
private Singleton ()
public static Singleton getSingleton()
if (singleton == null)
synchronized (Singleton.class)
if (singleton == null)
singleton = new Singleton(); //位置1:问题出在这里
return singleton;
这种写法也是有问题的。为什么呢?问题出在singleton = new Singleton();这条语句上。
singleton = new Singleton();这条语句在创建对象的过程中会分成3个步骤,如下:
memory = allocate(); // 步骤1.分配对象的内存空间
ctorInstance(memory); // 步骤2.初始化对象
sInstance = memory; // 步骤3.设置sInstance指向刚分配的内存地址
JVM在执行的过程中步骤2和步骤3可能会发生指令重排序,重排后执行顺序如下:
memory = allocate(); // 步骤1.分配对象的内存空间
sInstance = memory; // 步骤2.设置sInstance指向刚分配的内存地址,此时对象还没有被初始化
ctorInstance(memory); // 步骤3.初始化对象
这种指令重排序在单线程下不会有问题,但是在并发情况下就会出现问题,如下图:
我觉的这张图我画的很明白了,大家应该是可以看懂的。因为并发的原因线程2访问到的是一个还未初始化的对象。这种不安全的因素是极难复现的,但是理论上还是存在线程不安全的因素。所以我们还要继续改进。
2.4 volatile修饰写法(线程安全)
public class Singleton
private volatile static Singleton singleton;
private Singleton ()
public static Singleton getSingleton()
if (singleton == null)
synchronized (Singleton.class)
if (singleton == null)
singleton = new Singleton();
return singleton;
这个方式和上面双重锁检查写法唯一的区别就是加volatile来修饰singleton。
volatile要仔细解释起来篇幅就大了,本章主要介绍单例,所以由于篇幅的问题,这里只是简单介绍一下volatile关键字。volatile有三大作用:
1.可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
2.原子性:一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行、
3.有序性:被volatile修饰的变量不会发生指令重排序。
2.5 静态内部类写法(线程安全)
public class Singleton
private static class SingletonHolder
private static final Singleton INSTANCE = new Singleton();
private Singleton ()
public static final Singleton getInstance()
return SingletonHolder.INSTANCE;
我们上面提到过,类的加载机制是线程安全的。这种方式能达到双检锁方式一样的功效,但实现更简单,对静态域使用延迟初始化。
结尾
好了,本章就讲到这里吧。怎么样,单例模式是不是没你想象的那么简单。
有什么问题留言,也可以去我的微信公众号留言。
大家帮忙微信关注我的微信公众号,每天提供优质java知识,并领取很多视频学习资料。
扫二维码关注公众号【Java程序员的奋斗路】可领取如下:
1.学习资料: 1T视频教程(大约有100多个视频):涵盖Javaweb前后端教学视频、机器学习/人工智能教学视频、Linux系统教程视频、雅思考试视频教程,android.等
2.项目源码:20个JavaWeb项目源码。
以上是关于单例模式绝对没有你想象的那么简单!不服来战!的主要内容,如果未能解决你的问题,请参考以下文章