单例模式是 Java 中最简单的设计模式之一,它是指一个类在运行期间始终只有一个实例,我们就把它称之为单例模式。它不但被应用在实际的工作中,而且还是面试中最常考的题目之一。通过单例模式我们可以知道此人的编程风格,以及对于基础知识的掌握是否牢固。

我们本课时的面试题是,单例的实现方式有几种?它们有什么优缺点?

典型回答

单例的实现分为饿汉模式和懒汉模式。顾名思义,饿汉模式就好比他是一个饿汉,而且有一定的危机意识,他会提前把食物囤积好,以备饿了之后直接能吃到食物。对应到程序中指的是,在类加载时就会进行单例的初始化,以后访问时直接使用单例对象即可。

饿汉模式的实现代码如下:

public class Singleton {
// 声明私有对象
private static Singleton instance = new Singleton();
// 获取实例(单例对象)
public static Singleton getInstance() {
return instance;
}
private Singleton() {
}
// 方法
public void sayHi() {
System.out.println("Hi,Java.");
}
}
class SingletonTest {
public static void main(String[] args) {
// 调用单例对象
Singleton singleton = Singleton.getInstance();
// 调用方法
singleton.sayHi();
}
}

以上程序的执行结果为:

Hi,Java.

从上述结果可以看出,单例对象已经被成功获取到并顺利地执行了类中的方法。它的优点是线程安全,因为单例对象在类加载的时候就已经被初始化了,当调用单例对象时只是把早已经创建好的对象赋值给变量;它的缺点是可能会造成资源浪费,如果类加载了单例对象(对象被创建了),但是一直没有使用,这样就造成了资源的浪费。

懒汉模式也被称作为饱汉模式,顾名思义他比较懒,每次只有需要吃饭的时候,才出去找饭吃,而不是像饿汉那样早早把饭准备好。对应到程序中指的是,当每次需要使用实例时,再去创建获取实例,而不是在类加载时就将实例创建好。

懒汉模式的实现代码如下:

public class Singleton {
// 声明私有对象
private static Singleton instance;
// 获取实例(单例对象)
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
private Singleton() {
}
// 方法
public void sayHi() {
System.out.println("Hi,Java.");
}
}
class SingletonTest {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
singleton.sayHi();
}
}

以上程序的执行结果为:

Hi,Java.

从上述结果可以看出,单例对象已经被成功获取到并顺利地执行了类中的方法,它的优点是不会造成资源的浪费,因为在调用的时候才会创建被实例化对象;它的缺点在多线程环境下是非线程是安全的,比如多个线程同时执行到 if 判断处,此时判断结果都是未被初始化,那么这些线程就会同时创建 n 个实例,这样就会导致意外的情况发生。

考点分析

使用单例模式可以减少系统的内存开销,提高程序的运行效率,但是使用不当的话就会造成多线程下的并发问题。饿汉模式为最直接的实现单例模式的方法,但它可能会造成对系统资源的浪费,所以只有既能保证线程安全,又可以避免系统资源被浪费的回答才能彻底地征服面试官。

和此知识点相关的面试题还有以下这些:

  • 什么是双重检测锁?它是线程安全的吗?
  • 单例的还有其他实现方式吗?

知识扩展

双重检测锁

为了保证懒汉模式的线程安全我们最简单的做法就是给获取实例的方法上加上 synchronized(同步锁)修饰,如下代码所示:

public class Singleton {
// 声明私有对象
private static Singleton instance;
// 获取实例(单例对象)
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
private Singleton() {
}
// 类方法
public void sayHi() {
System.out.println("Hi,Java.");
}
}

这样虽然能让懒汉模式变成线程安全的,但由于整个方法都被 synchronized 所包围,因此增加了同步开销,降低了程序的执行效率。

于是为了改进程序的执行效率,我们将 synchronized 放入到方法中,以此来减少被同步锁所修饰的代码范围,实现代码如下:

public class Singleton {
// 声明私有对象
private static Singleton instance;
// 获取实例(单例对象)
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
private Singleton() {
}
// 类方法
public void sayHi() {
System.out.println("Hi,Java.");
}
}

细心的你可能会发现以上的代码也存在着非线程安全的问题。例如,当两个线程同时执行到「if (instance == null) { 」判断时,判断的结果都为 true,于是他们就排队都创建了新的对象,这显然不符合我们的预期。于是就诞生了大名鼎鼎的双重检测锁(Double Checked Lock,DCL),实现代码如下:

public class Singleton {
// 声明私有对象
private static Singleton instance;
// 获取实例(单例对象)
public static Singleton getInstance() {
// 第一次判断
if (instance == null) {
synchronized (Singleton.class) {
// 第二次判断
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
private Singleton() {
}
// 类方法
public void sayHi() {
System.out.println("Hi,Java.");
}
}

上述代码看似完美,其实隐藏着一个不容易被人发现的小问题,该问题就出在 new 对象这行代码上,也就是 instance = new Singleton() 这行代码。这行代码看似是一个原子操作,然而并不是,这行代码最终会被编译成多条汇编指令,它大致的执行流程为以下三个步骤:

  • 给对象实例分配内存空间;
  • 调用对象的构造方法、初始化成员字段;
  • 将 instance 对象指向分配的内存空间。

但由于 CPU 的优化会对执行指令进行重排序,也就说上面的执行流程的执行顺序有可能是 1-2-3,也有可能是 1-3-2。假如执行的顺序是 1-3-2,那么当 A 线程执行到步骤 3 时,切换至 B 线程了,而此时 B 线程判断 instance 对象已经指向了对应的内存空间,并非为 null 时就会直接进行返回,而此时因为没有执行步骤 2,因此得到的是一个未初始化完成的对象,这样就导致了问题的诞生。执行时间节点如下表所示:

时间点 线程 执行操作
t1 A instance = new Singleton() 的 1-3 步骤,待执行步骤 2
t2 B if (instance == null) { 判断结果为 false
t3 B 返回半初始的 instance 对象

为了解决此问题,我们可以使用关键字 volatile 来修饰 instance 对象,这样就可以防止 CPU 指令重排,从而完美地运行懒汉模式,实现代码如下:

public class Singleton {
// 声明私有对象
private volatile static Singleton instance;
// 获取实例(单例对象)
public static Singleton getInstance() {
// 第一次判断
if (instance == null) {
synchronized (Singleton.class) {
// 第二次判断
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
private Singleton() {
}
// 类方法
public void sayHi() {
System.out.println("Hi,Java.");
}
}

单例其他实现方式

除了以上的 6 种方式可以实现单例模式外,还可以使用静态内部类和枚举类来实现单例。静态内部类的实现代码如下:

public class Singleton {
// 静态内部类
private static class SingletonInstance {
private static final Singleton instance = new Singleton();
}
// 获取实例(单例对象)
public static Singleton getInstance() {
return SingletonInstance.instance;
}
private Singleton() {
}
// 类方法
public void sayHi() {
System.out.println("Hi,Java.");
}
}

从上述代码可以看出,静态内部类和饿汉方式有异曲同工之妙,它们都采用了类装载的机制来保证,当初始化实例时只有一个线程执行,从而保证了多线程下的安全操作。JVM 会在类初始化阶段(也就是类装载阶段)创建一个锁,该锁可以保证多个线程同步执行类初始化的工作,因此在多线程环境下,类加载机制依然是线程安全的。

但静态内部类和饿汉方式也有着细微的差别,饿汉方式是在程序启动时就会进行加载,因此可能造成资源的浪费;而静态内部类只有在调用 getInstance() 方法时,才会装载内部类从而完成实例的初始化工作,因此不会造成资源浪费的问题。由此可知,此方式也是较为推荐的单例实现方式。

单例的另一种实现方式为枚举,它也是《Effective Java》作者极力推荐地单例实现方式,因为枚举的实现方式不仅是线程安全的,而且只会装载一次,无论是序列化、反序列化、反射还是克隆都不会新创建对象。它的实现代码如下:

public class Singleton {
// 枚举类型是线程安全的,并且只会装载一次
private enum SingletonEnum {
INSTANCE;
// 声明单例对象
private final Singleton instance;
// 实例化
SingletonEnum() {
instance = new Singleton();
}
private Singleton getInstance() {
return instance;
}
}
// 获取实例(单例对象)
public static Singleton getInstance() {
return SingletonEnum.INSTANCE.getInstance();
}
private Singleton() {
}
// 类方法
public void sayHi() {
System.out.println("Hi,Java.");
}
}
class SingletonTest {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
singleton.sayHi();
}
}

以上程序的执行结果为:

Hi,Java.

小结

本课时我们讲了 8 种实现单例的方式,包括线程安全但可能会造成系统资源浪费的饿汉模式,以及懒汉模式和懒汉模式变种的 5 种实现方式。其中包含了两种双重检测锁的懒汉变种模式,还有最后两种线程安全且可以实现延迟加载的静态内部类的实现方式和枚举类的实现方式,其中比较推荐使用的是后两种单例模式的实现方式。

Java中的单例模式最全解析的更多相关文章

  1. Java并发原理层面:ReentrantLock中lock()、unlock()全解析

    一.前言 Java线程同步两种方式,synchronized关键字和Lock锁机制,其中,AQS队列就是Lock锁实现公平加锁的底层支持. 二.AQS源码对于lock.lock()的实现 2.1 AQ ...

  2. java中的单例模式与静态类

    单例模式与静态类(一个类,所有方法为静态方法)是另一个非常有趣的问题,在<Java中有关单例模式的面试问题>博文中露掉了,由于单例模式和静态类都具有良好的访问性,它们之间有许多相似之处,例 ...

  3. JAVA中的四种JSON解析方式详解

    JAVA中的四种JSON解析方式详解 我们在日常开发中少不了和JSON数据打交道,那么我们来看看JAVA中常用的JSON解析方式. 1.JSON官方 脱离框架使用 2.GSON 3.FastJSON ...

  4. Java中json的构造和解析

    什么是 Json? JSON(JvaScript Object Notation)(官网网站:http://www.json.org/)是 一种轻量级的数据交换格式.  易于人阅读和编写.同时也易于机 ...

  5. java中的单例模式与doublecheck

    转自: http://devbean.blog.51cto.com/448512/203501 在GoF的23种设计模式中,单例模式是比较简单的一种.然而,有时候越是简单的东西越容易出现问题.下面就单 ...

  6. Java基础学习(五)-- Java中常用的工具类、枚举、Java中的单例模式之详解

    Java中的常用类 1.Math : 位于java.lang包中 (1)Math.PI:返回一个最接近圆周率的 (2)Math.abs(-10):返回一个数的绝对值 (3)Math.cbrt(27): ...

  7. Java中的单例模式(Singleton Pattern in Java)

    Introduction 对于系统中的某个类来说,只有一个实例是很重要的,比如只有一个timer和ID Producer.又比如在服务器程序中,配置信息保留在一个文件中,这些配置信息由一个单例对象统一 ...

  8. Java中哪个JSON库的解析速度是最快的?

    JSON已经成为当前服务器与WEB应用之间数据传输的公认标准,不过正如许多我们所习以为常的事情一样,你会觉得这是理所当然的便不再深入思考 了.我们很少会去想用到的这些JSON库到底有什么不同,但事实上 ...

  9. 在Java中使用xpath对xml解析

    xpath是一门在xml文档中查找信息的语言.xpath用于在XML文档中通过元素和属性进行导航.它的返回值可能是节点,节点集合,文本,以及节点和文本的混合等.在学习本文档之前应该对XML的节点,元素 ...

随机推荐

  1. C语言知识点复习梳理

    C语言知识点复习梳理 C语言的知识点讲完了,接下来就是做一下整理与总结,然后就会进入其他知识的学习. 本文目录如下: 基础知识. 顺序程序设计. 数据类型. 标准输入输出. 进制转换. 选择结构. 循 ...

  2. Reverse for ‘password_reset_complete‘ not found. ‘password_reset_complete‘ is not a valid view funct

    关注公众号"轻松学编程"了解更多 原因 在使用xadmin与django 2版本以上修改密码时会报这个错,这是由于django修改密码成功后使用的是success_url参数,而x ...

  3. 2018-12-8 论文翻译+hdoj+git+python

    今天干的事不多,明天得把实验写了. 论文翻译了摘要.0.5h hdoj 五道水题.注意while(cin>>char&&char != '\n')没用.可用ch = cin ...

  4. MySQL中load data infile将文件中的数据批量导入数据库

    有时候我们需要将文件中的数据直接导入到数据库中,那么我们就可以使用load data infile,下面具体介绍使用方法. dao中的方法 @Autowired private JdbcTemplat ...

  5. 最全总结 | 聊聊 Python 办公自动化之 Word(上)

    1. 前言 日常自动化办公中,使用 Python 真的能做到事半功倍! 在上一个系列中,我们对 Python 操作 Excel 进行了一次全面总结 最全总结 | 聊聊 Python 办公自动化之 Ex ...

  6. html中创建并调用vue组件的几种方法

    最近在写项目的时候,总是遇到在html中使用vue.js的情况,且页面逻辑较多,之前的项目经验都是使用脚手架等已有的项目架构,使用.vue文件完成组价注册,及组件之间的调用,还没有过在html中创建组 ...

  7. jdk的动态代理和静态代理你还写不出来嘛???

    一.什么叫jdk的代理? 用另外一个对象去代理实际对象的操作 分为动态代理和静态代理二.先说说静态代理 从字面意思来看就是不会改变的,只可以代理某个固定对象的. 静态代理就是通过实现和目标对象实现的同 ...

  8. leetcode 30day--2

    202. Happy Number Write an algorithm to determine if a number n is "happy". A happy number ...

  9. Maximum Subarray(最大连续子串)

    1 class Solution { 2 public: 3 //动态规划,维护两个变量 local[i+1]=max(array[i],local[i]+array[i+1]) 4 int Find ...

  10. Apache Shiro (Shiro-550)(cve_2016_4437)远程代码执行 - 漏洞复现

    0x00 漏洞原理 Apache Shiro框架提供了记住密码的功能(RememberMe),用户登录成功后会生成经过加密并编码的cookie.在服务端对rememberMe的cookie值, 先ba ...