Java中的单例模式最全解析
单例模式是 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中的单例模式最全解析的更多相关文章
- Java并发原理层面:ReentrantLock中lock()、unlock()全解析
一.前言 Java线程同步两种方式,synchronized关键字和Lock锁机制,其中,AQS队列就是Lock锁实现公平加锁的底层支持. 二.AQS源码对于lock.lock()的实现 2.1 AQ ...
- java中的单例模式与静态类
单例模式与静态类(一个类,所有方法为静态方法)是另一个非常有趣的问题,在<Java中有关单例模式的面试问题>博文中露掉了,由于单例模式和静态类都具有良好的访问性,它们之间有许多相似之处,例 ...
- JAVA中的四种JSON解析方式详解
JAVA中的四种JSON解析方式详解 我们在日常开发中少不了和JSON数据打交道,那么我们来看看JAVA中常用的JSON解析方式. 1.JSON官方 脱离框架使用 2.GSON 3.FastJSON ...
- Java中json的构造和解析
什么是 Json? JSON(JvaScript Object Notation)(官网网站:http://www.json.org/)是 一种轻量级的数据交换格式. 易于人阅读和编写.同时也易于机 ...
- java中的单例模式与doublecheck
转自: http://devbean.blog.51cto.com/448512/203501 在GoF的23种设计模式中,单例模式是比较简单的一种.然而,有时候越是简单的东西越容易出现问题.下面就单 ...
- Java基础学习(五)-- Java中常用的工具类、枚举、Java中的单例模式之详解
Java中的常用类 1.Math : 位于java.lang包中 (1)Math.PI:返回一个最接近圆周率的 (2)Math.abs(-10):返回一个数的绝对值 (3)Math.cbrt(27): ...
- Java中的单例模式(Singleton Pattern in Java)
Introduction 对于系统中的某个类来说,只有一个实例是很重要的,比如只有一个timer和ID Producer.又比如在服务器程序中,配置信息保留在一个文件中,这些配置信息由一个单例对象统一 ...
- Java中哪个JSON库的解析速度是最快的?
JSON已经成为当前服务器与WEB应用之间数据传输的公认标准,不过正如许多我们所习以为常的事情一样,你会觉得这是理所当然的便不再深入思考 了.我们很少会去想用到的这些JSON库到底有什么不同,但事实上 ...
- 在Java中使用xpath对xml解析
xpath是一门在xml文档中查找信息的语言.xpath用于在XML文档中通过元素和属性进行导航.它的返回值可能是节点,节点集合,文本,以及节点和文本的混合等.在学习本文档之前应该对XML的节点,元素 ...
随机推荐
- WIN32之消息队列
0x01. 什么是消息? 当我们点击鼠标的时候,或者当我们按下键盘的时候,操作系统都要把这些动作记录下来,存储到结构体中,这个结构体就是 消息 比如我们点击运行程序,是通过消息队列获取,通过explo ...
- 【转】Getting an Image on the Screen
FROM:http://lazyfoo.net/tutorials/SDL/02_getting_an_image_on_the_screen/index.php Getting an Image o ...
- Java到处运行的基础之 Class 文件
Java 实现一次编译到处运行的基础,来源于 Java 虚拟机屏蔽了操作系统的底层细节.使用 class 文件存储编译后的源程序,使得 Java 程序的编译与操作系统解耦.正是因为 Java clas ...
- nextInt()和nextLine()连用报错
当nextInt(),next(),nextDouble(),nextFloat()方法与nextLine()连用并放在nextLine()前面时,会出现下面的错误: Exception in thr ...
- 全排列算法--递归实现(Java)
求一个n阶行列式,一个比较简单的方法就是使用全排列的方法,那么简述以下全排列算法的递归实现. 首先举一个简单的例子说明算法的原理,既然是递归,首先说明一下出口条件.以[1, 2]为例 首先展示一下主要 ...
- Cocos Creator与VS Code整合代码提示问题
Cocos Creator与VS Code整合开发配置 在Cocos Creator中依次点击下面框中的菜单 VS Code工作流 配置Cocos Creator的默认编辑器 Cocos Creato ...
- Firefox威武 尚译威武!
有感于鹰文文档看起来麻烦,期待有一款即时翻译的工具,在windows上还可以,但是上班用的是ubuntu,所以就想到要找一个firefox插件,还真找到了,但是不是插件,是一个书签,那就是尚译!尚哥威 ...
- 04 . Vue组件注册,数据交互,调试工具及组件插槽介绍及使用
vue组件 组件(Component)是 Vue.js 最强大的功能之一. 组件可以扩展 HTML 元素,封装可重用的代码. 组件系统让我们可以用独立可复用的小组件来构建大型应用,几乎任意类型的应用的 ...
- gdb调试入门(下)
GDB调试主要包括: 1.查看运行时数据 2.程序错误 3.gdb调试逻辑错误 4.gdb调试段错误 5.core文件调试 一.查看运行时数据 1.print 查看变量值 2.ptype 变量: 查看 ...
- C/C++宏替换详解
目录 1. 基本形式 2. 宏展开中的陷阱 3. #undef 4. 宏参数.# 和 ## 1. 基本形式 #define name replacement_text 通常情况下,#define 指令 ...