详解Java中如何正确书写单例模式
作者:又是火星人 发布时间:2022-06-20 01:35:00
单例模式算是设计模式中最容易理解,也是最容易手写代码的模式,但是其中涉及的知识点却一点也不少,所以经常作为面试题来考。一般单例都是五种写法:懒汉,饿汉,双重校验锁,静态内部类和枚举。为了记录学习过程的过程,这里整理了几种常见的单例写法,
青铜5:(Lazy-loaded,但线程不安全)
当被问到要实现一个单例模式时,很多人的第一反应是写出如下的代码,包括教科书上也是这样教我们的。
public class Singleton {
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这段代码简单明了,而且使用了延迟加载模式,但是线程不安全。多线程环境下调用 getInstance() 方法,可能会发生多个线程进入if语句的程序代码块。
懒汉式:synchronized(Lazy-loaded,线程安全,但不高效)
为了解决上面的问题,最简单的方法是将整个 getInstance() 方法设为同步(synchronized)。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
虽然做到了线程安全、延迟加载,但是它并不高效。因为在任何时候只能有一个线程调用 getInstance() 方法。但是synchronized操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。这种模式导致即使在单例创建完成后,每次依然只有一个线程可以访问getInstance()方法,会导致潜在的性能问题。这就引出了双重检验锁。
饿汉式:static final field(非Lazy-loaded)
这种方法非常简单,因为单例的实例被声明成 static final,在第一次加载类到内存中时就会初始化,所以创建实例对象是线程安全的(由JVM实现保证)。
public class Singleton{
//类加载时就初始化
private static final Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){ // Singleton with static factory
return instance;
}
}
它不是一种懒加载模式,instance会在加载类后一开始就被初始化,即使客户端没有调用 getInstance()方法。这会导致一些使用限制:譬如 Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。类似的方法还有:
public class Singleton{
public static final Singleton instance = new Singleton(); // Singleton with public final field
private Singleton(){}
}
// <Effective Java>第14页讲诉了两者的差异
双重检验锁 + volatile(Lazyload,线程安全,但晦涩)
双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法。程序员称其为双重检查锁,因为会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例对象了。
public static Singleton getSingleton() {
if (instance == null) { //Single Checked
synchronized (Singleton.class) {
if (instance == null) { //Double Checked
instance = new Singleton();
}
}
}
return instance ;
}
这段代码看起来很完美,很可惜它是有问题的。主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
给 instance 分配内存
调用 Singleton 的构造函数来初始化成员变量
将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
但是在 JVM 的JIT编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。为此,我们需要将 instance 变量声明成 volatile 。
public class Singleton {
private volatile static Singleton instance; //声明为 volatile
private Singleton(){}
public static Singleton getSingleton() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
但是特别注意在 Java 1.5 以前的版本使用了 volatile 的双检锁还是有问题的,这个问题在 Java 1.5 中才得以修复,所以在这之后才可以放心使用 volatile。
静态内部类:IoDH,initialization-on-demand holder
这个模式综合使用了Java的静态内部类和多线程缺省同步锁的知识,很巧妙地同时实现了延迟加载和线程安全。
public class Singleton {
private Singleton() {}
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() { // From wikipedia
return LazyHolder.INSTANCE;
}
}
静态内部类相当于其外部类的static部分,它的对象不依赖于外部类对象,因此可以直接创建。静态内部类只有在第一次被使用的时候才会被转载。
多线程缺省同步锁
大家都知道,在多线程开发中,为了解决并发问题,主要是通过使用synchronized来加互斥锁进行同步控制。但是在某些情况中,JVM已经隐含地为您执行了同步,这些情况下就不需要手动进行同步控制了。这些情况包括:
1.由静态初始化器(在静态字段上或static{}块中的初始化器)初始化数据时
2.访问final字段时
3.在创建线程之前创建对象时
4.线程可以看见它将要处理的对象时
枚举 Enum
从Java 1.5起,只需编写一个包含单个元素的枚举类型:
public enum Singleton {
INSTANCE;
}
这种方法在功能上与公有域方法相近,但是它更加简洁,无偿地提供了序列化机制,绝对防止多次实例化,即使是在面向复杂的序列化或者反射攻击的时候。虽然这种方法还没有广泛采用,但是单元素的枚举类型以及成为实现Singleton的最佳方法。
--------------------以下是几个细节存疑的实现方法---------------------
1.static final到底有哪些细节
2.static field处的赋值初始化到底和static代码块有先后吗?
3.静态内部类的单例模式到底怎么写
4.P50 in Java EE设计模式解析与应用中的例子真的有Lazyload效果吗?
来源:http://www.cnblogs.com/echo1937/p/6247050.html


猜你喜欢
- 本文简要分析了C#的回调机制。分享给大家供大家参考。具体分析如下:1.回调听起来高大上,实际上本质就是委托,回调是委托的一种应用,其本质就是
- 需求说明实际操作过程中,从D盘根目录下的ak.txt读取文件写入D盘根目录下的hello.txt文件内实现思路写两个方法,一个用于读取目标文
- 本文实例为大家分享了Android调用外置摄像头的具体代码,供大家参考,具体内容如下1、布局文件<?xml version="
- 什么是MD5?Message Digest Algorithm MD5(中文名为消息摘要算法第五版)为计算机安全领域广泛使用的一种散列函数,
- 这里以list为介绍:private static readonly T[] s_emptyArray = new T[0];public
- 报错信息:java.lang.Exception: org.apache.hadoop.mapreduce.task.reduce.Shuf
- using (TransactionScope tr = new TransactionScope()) {  
- 一、使用注解实现自定义映射关系当POJO属性名与数据库列名不一致时,需要自定义实体类和结果集的映射关系,在MyBatis注解开发中,使用 @
- 一、Future 接口当 call()方法完成时,结果必须存储在主线程已知的对象中,以便主线程可以知道该线程返回的结果。为此,可以使用 Fu
- 本篇博客要分享的一个UI效果——实现底部切换标签,想必大家在一些应用上面遇到过这种效果了,最典型的就是微信了,可以左右滑动切换页面,也可以点
- 这个问题属于非常初级的问题,但是对于初学不知道的人可能会比较头疼。C++ 中函数是不能直接返回一个数组的,但是数组其实就是指针,所以可以让函
- 本文实例讲述了C#序列化与反序列化的方法。分享给大家供大家参考。具体分析如下:把“对象”转换为“字节序列”的过程称为对象的序列化。 
- 一 前言springboot 额外的特色是提供了后台应用监控,可以通过 HTTP 或者 JMX的方式管理监控应用,本文主讲HTTP方式;其主
- 本文实例为大家分享了C#多线程Thread使用的示例代码,供大家参考,具体内容如下多线程:线程生命周期状态图:C#线程优先级(概率高低):基
- 1.概述Spring Boot Admin是一个Web应用程序,用于管理和监视Spring Boot应用程序。每个应用程序都被视为客户端,并
- 前言最近在学习Android开发,发现确实有太多东西需要去整理,去学习。慢慢来吧,任何东东的深入学习都是不简单的。今天稍微整理下Spanna
- java 中同步、异步、阻塞和非阻塞区别详解简单点说:阻塞就是干不完不准回来,一直处于等待中,直到事情处理完成才返回; 非阻塞就是你先干,我
- 字符串和列表学完,自己试着写了一个非常简单的Python名片管理系统。新萌尝试,大佬们不要喷。修改名片的功能我偷了个懒,因为我不知道怎么通过
- 本文为大家分享了maven环境变量配置的具体方法,供大家参考,具体内容如下一、maven环境配置1.解压apache-maven
- 网上有很多类似的文章,大多数还是继承listview来实现(主要是listview.addHeaderView()和listview.add