网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
package com.lizba.pattern.singleton;
/**
*
* 单例模式的简单使用示例
*
*
* @Author: Liziba
* @Date: 2021/6/27 14:44
*/
public class SingletonDemo {
private static final SingletonDemo singleton = new SingletonDemo();
private SingletonDemo() {}
public static SingletonDemo getInstance() {
return singleton;
}
}
单例模式的UML类图:
3、单例模式的优缺点和使用场景
在很多技术的学习和选择的前置条件我想可能都会是这两个问题为先驱,所以把本该放置到最后去总结的两个点,优先提到了最前面,也正是出于这个原因。单例模式的编码说简单确实简单,编码一个最优的单例或者如何更好的适用当前场景,对编码人员有一定的要求,如下主要分析其优缺点和使用场景,便于编码人员的选择。
3.1 单例模式的优点
- 只创建一个实例对象,减少内存开销,同时也减少了GC
- 只创建一个示例对象,通过全局唯一的访问点,可以避免对资源的多重占用,例如文件的对同一个文件的写
- 通过这个全局访问点为抓手,可以对资源的访问做优化处理,例如访问点中预置缓存
3.2 单例模式的缺点
- 单例模式的功能一般写于一个类中,这回导致业务逻辑耦合,如果设计不合理,是违背单一职责原则
- 单例模式一般不实现接口,使其扩展困难,是违背开闭原则
- 单例模式只存在一个对象,在并发测试中不利于调试
3.3 使用典型场景
- 日志类,对于日志的记录等操作,我们通常使用单例模式
- 数据库连接和访问的管理
- 文件资源访问管理
- 生成唯一的序列号
- 系统全局唯一的访问端点,例如系统访问请求计数器
- 对象的创建需要大量的开销或者对象会被频繁调用可以考虑通过单例模式来优化替换
3.4 源码中的使用
说三道四不如看看Java最佳规范JDK中对于单例模式的一些使用
- java.lang.Runtime
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {}
// …
}
- java.awt.GraphicsEnvironment
public abstract class GraphicsEnvironment {
protected GraphicsEnvironment() {
}
public static synchronized GraphicsEnvironment getLocalGraphicsEnvironment() {
if (localEnv == null) {
localEnv = createGE();
}
return localEnv;
}
// …
}
- java.awt.Desktop
public class Desktop {
private Desktop() {
peer = Toolkit.getDefaultToolkit().createDesktopPeer(this);
}
public static synchronized Desktop getDesktop(){
if (GraphicsEnvironment.isHeadless()) throw new HeadlessException();
if (!Desktop.isDesktopSupported()) {
throw new UnsupportedOperationException("Desktop API is not " +
“supported on the current platform”);
}
sun.awt.AppContext context = sun.awt.AppContext.getAppContext();
Desktop desktop = (Desktop)context.get(Desktop.class);
if (desktop == null) {
desktop = new Desktop();
context.put(Desktop.class, desktop);
}
return desktop;
}
}
除了在jdk源码中对于单例模式有不少的使用之外,我们最常见的Spring框架中,每个Bean默认就是单例的,这样做的原因在于Spring容器可以管理Bean的生命周期。修改Spring中Bean的单例属性,只需要设置为Prototype类型即可,这样Bean的生命周期将不再有Spring容器跟踪。
4、饿汉式单例
饿汉式单例主要体现在一个饿字,也就是说在使用这个对象之前,在类加载的时候就立即初始化,创建其实例对象。这样做的好处是线程安全、使用时速度更快。饿汉式单例的写法如下:
写法一:
package com.lizba.pattern.singleton.hungry;
/**
*
* 饿汉式单例写法一
*
*
* @Author: Liziba
* @Date: 2021/6/27 15:46
*/
public class HungrySingletonDemo1 {
private static final HungrySingletonDemo1 HUNGRY_SINGLETON_DEMO_1 = new HungrySingletonDemo1();
private HungrySingletonDemo1() {}
public static HungrySingletonDemo1 getInstance() {
return HUNGRY_SINGLETON_DEMO_1;
}
}
写法二:
package com.lizba.pattern.singleton.hungry;
/**
*
* 饿汉式单例写法二(静态代码块)
*
*
* @Author: Liziba
* @Date: 2021/6/27 15:49
*/
public class HungrySingletonDemo2 {
private static final HungrySingletonDemo2 HUNGRY_SINGLETON_DEMO_2;
static {
HUNGRY_SINGLETON_DEMO_2 = new HungrySingletonDemo2();
}
public static HungrySingletonDemo2 getInstance() {
return HUNGRY_SINGLETON_DEMO_2;
}
}
饿汉式单例可以保证线程安全,执行效率高,同时编码简单容易理解。但是饿汉式单例只适用于单例对象减少的情况,如果大量编写饿汉式单例不仅会给系统启动带来负担,也可能会导致内存的浪费,比如创建的单例对象在程序运行过程中并未使用。因此这对内存浪费这个问题,我们衍生出了懒汉式单例。
5、懒汉式单例
懒汉式单例主要体现在一个懒字,也就是说类加载的时候我懒得实例化,等你需要用了再说吧!接下来的懒汉式单例中我通过一步步的优化和推翻来演进如何编写一个优秀的单例。
5.1 普通懒汉式单例
package com.lizba.pattern.singleton.lazy;
/**
*
* 简单懒汉式单例示例代码1
*
*
* @Author: Liziba
* @Date: 2021/6/27 16:01
*/
public class LazySingletonDemo1 {
private static LazySingletonDemo1 LAZY_SINGLETON = null;
private LazySingletonDemo1() {
}
public static LazySingletonDemo1 getInstance() {
if (LAZY_SINGLETON == null) {
LAZY_SINGLETON = new LazySingletonDemo1();
}
return LAZY_SINGLETON;
}
}
这是一个非常普通的懒汉式单例模式的写法,相信很多初学者会编码成这样,但其实这是一种错误的线程不安全的写法。我们通过一个断点测试来证明其不安全,我采用的是IDEA编写代码,通过设置断点为Thread模式来使得线程1和线程2同时满足LAZY_SINGLETON == null。
测试代码
package com.lizba.pattern.singleton.lazy;
/**
*
* 测试懒汉式单例1的线程不安全
*
*
* @Author: Liziba
* @Date: 2021/6/27 16:05
*/
public class LazySingletonTest {
public static void main(String[] args) {
new Thread(() -> run(), “Thread-1”).start();
new Thread(() -> run(), “Thread-2”).start();
System.out.println(“End of test…”);
}
public static void run() {
LazySingletonDemo1 lad = LazySingletonDemo1.getInstance();
System.out.println(Thread.currentThread().getName() + " : " + lad);
}
}
断点设置如下图,邮件打在LAZY_SINGLETON == null的断点上,选择Suspend为Thread,然后点击Done即可。
调试过程,我们执行测试代码,使其两个线程均执行到LAZY_SINGLETON == null,然后F8使得线程1进入if语句;切换至线程2F8进入if语句,此时我们的目的便达到了(在实际的并发使用场景中,这种情况是非常可能出现的)
此时线程1和线程2中的对象并不是同一个对象,所以这种单例编码方式是不正确的。
5.2 同步懒汉式单例
关于上述的单例模式编码,部分读者会想到通过同步原语synchronized加在getInstance()方法上来解决,此时代码如下所示:
package com.lizba.pattern.singleton.lazy;
/**
*
* synchronized修饰方法getInstance()的懒汉式单例
*
*
* @Author: Liziba
* @Date: 2021/6/27 16:37
*/
public class LazySingletonDemo2 {
private static LazySingletonDemo2 LAZY_SINGLETON = null;
private LazySingletonDemo2() {
}
// synchronized修饰getInstance()方法
public static synchronized LazySingletonDemo2 getInstance() {
if (LAZY_SINGLETON == null) {
LAZY_SINGLETON = new LazySingletonDemo2();
}
return LAZY_SINGLETON;
}
}
如上通过给getInstance()加上synchronized关键字,使得同步方法中的代码块线程安全了,但是随之而来的是在大量并发调用该方法的性能问题,大量的线程会阻塞在这个方法上,所以有些我们通过缩小锁锁住的范围来尽可能的提升其并发性能。代码衍生为如下所示:
package com.lizba.pattern.singleton.lazy;
/**
*
* synchronized修饰方法代码块
*
*
* @Author: Liziba
* @Date: 2021/6/27 16:53
*/
public class LazySingletonDemo3 {
private static LazySingletonDemo3 LAZY_SINGLETON = null;
private LazySingletonDemo3() {
}
public static LazySingletonDemo3 getInstance() {
synchronized (LazySingletonDemo3.class) {
if (LAZY_SINGLETON == null) {
LAZY_SINGLETON = new LazySingletonDemo3();
}
return LAZY_SINGLETON;
}
}
}
如上这种锁住代码块的方式,虽然缩小了锁同步的范围,但是并未本质上的改变每个线程均需要阻塞这个问题,因此我们可以前置if(LAZY_SINGLETON == null)这个判断,使得部分线程判断对象被实例化后无需加锁,直接返回即可,其代码如下所示:
package com.lizba.pattern.singleton.lazy;
/**
*
* if(LAZY_SINGLETON == null)前置于同步代码块,单例模式示例4
*
*
* @Author: Liziba
* @Date: 2021/6/27 16:56
*/
public class LazySingletonDemo4 {
private static LazySingletonDemo4 LAZY_SINGLETON = null;
private LazySingletonDemo4() {
}
public static LazySingletonDemo4 getInstance() {
// 前置if判断
if (LAZY_SINGLETON == null) {
synchronized (LazySingletonDemo4.class) {
LAZY_SINGLETON = new LazySingletonDemo4();
}
}
return LAZY_SINGLETON;
}
}
如上代码看似兼顾了性能和同步对象的实例化,但是这里的同步语言并不能保证对象只实例化一次,它只能保证每次只有一个线程在实例化这个对象。试想一下,如果线程1和线程2均执行到代码21行,此时线程1获得锁继续执行,实例化对象LazySingletonDemo4,当线程1退出锁后,线程2获取到锁,线程2也会对LazySingletonDemo4进行实例化,这种情况也可以用上面的断点法测试出来。所以这种情况是错误的,但是只有小小的改动一下即可,改动后的方式如下所示。
5.3 双重检查锁懒汉式单例
双重检查锁看名字高大上,其实就是在上面的LazySingletonDemo4多个if判断而已,理解为双重检查+锁可能更明了,其代码如下所示:
package com.lizba.pattern.singleton.lazy;
/**
*
* 双重检查锁
*
*
* @Author: Liziba
* @Date: 2021/6/27 17:04
*/
public class LazySingletonDemo5 {
private static LazySingletonDemo5 LAZY_SINGLETON = null;
private LazySingletonDemo5() {
}
public static LazySingletonDemo5 getInstance() {
// 检查1
if (LAZY_SINGLETON == null) {
synchronized (LazySingletonDemo5.class) {
// 检查2(这就是双重检查)
if (LAZY_SINGLETON == null) {
LAZY_SINGLETON = new LazySingletonDemo5();
}
}
}
return LAZY_SINGLETON;
}
}
双重检查锁看似完全正确,其实还存在一个指令重排序问题,至于什么是指令重排序,在我的并发编程专题中有详细的讲述,这里不再细讲;简单来说,就是编译器和处理器会针对编译后的指令进行重新排序,这种重排序对应编译器和处理器来说是会带来一定的性能提升的,但是对于编写代码的程序员来说,如果不正确的使用同步语义,将会导致非预期结果出现。在上述示例中第24行代码LAZY_SINGLETON = new LazySingletonDemo5(),在编译器编译后生成的字节码会有三条指令如下所示:
memory = allocate(); // 1:分配对象的内存空间
ctorInstance(memory); // 2:初始化对象
instance = memory; // 3:设置instance指向刚分配的内存地址
这三条指令中,指令2和指令3可能会出现重排序,也就是说对象的初始化会被后置到将分配的地址指向对象的引用这个指令的后面;这种重排序,假设是在线程1执行中发生了,此时线程2执行到第20行 if (LAZY_SINGLETON == null),此时LAZY_SINGLETON 并不为null,程序会直接返回LAZY_SINGLETON对象,但是此时的对象是一个实例化不完全的对象,这种情况是不允许存在的。
其解决办法是通过volatile关键字借助其内存语义来禁止指令重排序,这样2和3指令之间的重排序将会被禁止,这涉及到JMM规范,需要的请查看我的并发专题系列文章。其改造代码如下所示:
package com.lizba.pattern.singleton.lazy;
/**
*
* volatile修饰 LAZY_SINGLETON禁止指令重排序
*
*
* @Author: Liziba
* @Date: 2021/6/27 17:12
*/
public class LazySingletonDemo6 {
// volatile修饰LAZY_SINGLETON反正指令重排序
private static volatile LazySingletonDemo6 LAZY_SINGLETON = null;
private LazySingletonDemo6() {
}
public static LazySingletonDemo6 getInstance() {
// 检查1
if (LAZY_SINGLETON == null) {
synchronized (LazySingletonDemo6.class) {
// 检查2(这就是双重检查)
if (LAZY_SINGLETON == null) {
LAZY_SINGLETON = new LazySingletonDemo6();
}
}
}
return LAZY_SINGLETON;
}
}
上述演进过程,其实已经编写一个比较完整的懒汉式单例的示例代码,但是介于语言的一些特性,也就是反射这个无事不为的操作,能破坏这种场景。我们先来看看怎么破坏的,其破坏示例代码如下:
package com.lizba.pattern.singleton.lazy;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
*
* 破坏双重检查锁单例
*
*
* @Author: Liziba
* @Date: 2021/6/27 17:34
*/
public class DoubleCheckTest {
public static void main(String[] args) {
Class singletonClass = LazySingletonDemo6.class;
try {
Constructor c = singletonClass.getDeclaredConstructor(null);
// 破坏private访问权限
c.setAccessible(true);
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
tring[] args) {
Class singletonClass = LazySingletonDemo6.class;
try {
Constructor c = singletonClass.getDeclaredConstructor(null);
// 破坏private访问权限
c.setAccessible(true);
[外链图片转存中…(img-XOTbHZ15-1715306604782)]
[外链图片转存中…(img-2V16zp7k-1715306604782)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!