热门标签 | HotTags
当前位置:  开发笔记 > 后端 > 正文

【多线程学习笔记】单例模式延迟加载/懒汉模式的解决方案:DCL双重检测锁+单例模式的其他实现方案

文章目录立即加载“饿汉模式”延迟加载“懒汉模式”延迟加载“懒汉模式”的解决方案(1)声明synchronized关键字(2)


文章目录

  • 立即加载/“饿汉模式”
  • 延迟加载/“懒汉模式”
  • 延迟加载/“懒汉模式”的解决方案
    • (1)声明synchronized关键字
    • (2)尝试同步代码块
    • (3)针对某些重要的代码进行单独的同步
    • (4)使用DCL双检查锁机制
  • 使用静态内置类实现单例模式
  • 序列化与反序列化的单例模式实现
  • 使用static代码块实现单例模式




立即加载/“饿汉模式”

什么是立即加载?立即加载就是使用类的时候已经将对象创建完毕,常见的实现办法就是直接new实例化。而立即加载从中文的语境来看,有“着急”、“急迫”的含义,所以也称为“饿汉模式”。 立即加载/“饿汉模式”是在调用方法前,实例已经被创建了,来看一下实现代码。 创建类MyObject.java代码如下:

package test;
public class MyObject {// 立即加载方式==饿汉模式private static MyObject myObject = new MyObject();private MyObject(){}public static MyObject getInstance() {// 此代码版本为立即加载// 此版本代码的缺点是不能有其他实例变量// 因为getInstance()方法没有同步// 所以有可能出现非线程安全问题return myObject;}

}
创建线程类MyThread.java代码如下:

package extthread;
import test.MyObject;
public class MyThread extends Thread {@Overridepublic void run() {System.out.println(MyObject.getInstance().hashCode());}
}

创建运行类Run.java代码如下:

package test.run;
import extthread.MyThread;
public class Run {public static void main(String[] args) {MyThread t1 = new MyThread();MyThread t2 = new MyThread();MyThread t3 = new MyThread();t1.start();t2.start();t3.start();}

控制台打印的hashCode是同一个值,说明对象是同一个,也就实现了立即加载型单例设计模式。


延迟加载/“懒汉模式”

什么是延迟加载?延迟加载就是在调用get()方法时实例才被创建,常见的实现办法就是在get()方法中进行new实例化。而延迟加载从中文的语境来看,是“缓慢”、“不急迫”的含义,所以也称为“懒汉模式”。 1.延迟加载/“懒汉模式”解析 延迟加载/“懒汉模式”是在调用方法时实例才被创建。一起来看一下实现代码。 创建测试用的项目singleton_1,创建类MyObject.java代码如下: package test;
public class MyObject {
private static MyObject myObject;
private MyObject() {
}
public static MyObject getInstance() {
// 延迟加载
if (myObject != null) {
} else {
myObject = new MyObject();
}
return myObject;
}
} 创建线程类MyThread.java代码如下:
package extthread;
import test.MyObject;
public class MyThread extends Thread {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}
创建运行类Run.java代码如下:
package test.run;
import extthread.MyThread;
public class Run {
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start();
MyThread t2 = new MyThread();
t2.start();
}
}
结果:
在这里插入图片描述

前面两个实验虽然使用“立即加载”和“延迟加载”实现了单例设计模式,但在多线程的环境中,前面“延迟加载”示例中的代码完全就是错误的,根本不能实现保持单例的状态。
来看一下如何在多线程环境中结合“错误的单例模式”创建出“多例”。

package test;
public class MyObject {private static MyObject myObject;private MyObject() {}public static MyObject getInstance() {try {
if (myObject != null) {} else {// 模拟在创建对象之前做一些准备性的工作Thread.sleep(3000);myObject = new MyObject();}} catch (InterruptedException e) {e.printStackTrace();}return myObject;}}

创建线程类MyThread.java代码如下:

package extthread;
import test.MyObject;
public class MyThread extends Thread {@Overridepublic void run() {System.out.println(MyObject.getInstance().hashCode());}
}

创建运行类Run.java代码如下:

package test.run;
import extthread.MyThread;
public class Run {public static void main(String[] args) {MyThread t1 = new MyThread();MyThread t2 = new MyThread();MyThread t3 = new MyThread();t1.start();t2.start();t3.start();}

}
在这里插入图片描述


延迟加载/“懒汉模式”的解决方案


(1)声明synchronized关键字

既然多个线程可以同时进入getInstance()方法,那么只需要对getInstance()方法声明synchronized关键字即可。

package test;
public class MyObject {private static MyObject myObject;private MyObject() {}// 设置同步方法效率太低了// 整个方法被上锁synchronized public static MyObject getInstance() {try {if (myObject != null) {} else {// 模拟在创建对象之前做一些准备性的工作Thread.sleep(3000);myObject = new MyObject();}} catch (InterruptedException e) {e.printStackTrace();}return myObject;}
}

此方法加入同步synchronized关键字得到相同实例的对象,但此种方法的运行效率非常低下,是同步运行的,下一个线程想要取得对象,则必须等上一个线程释放锁之后,才可以继续执行。


(2)尝试同步代码块

同步方法是对方法的整体进行持锁

public class MyObject {private static MyObject myObject;private MyObject() {}public static MyObject getInstance() {try {// 此种写法等同于:// synchronized public static MyObject getInstance()// 的写法,效率一样很低,全部代码被上锁synchronized (MyObject.class) {if (myObject != null) {} else {// 模拟在创建对象之前做一些准备性的工作Thread.sleep(3000);myObject = new MyObject();}}} catch (InterruptedException e) {e.printStackTrace();}return myObject;}

}


(3)针对某些重要的代码进行单独的同步

同步代码块可以针对某些重要的代码进行单独的同步,而其他的代码则不需要同步。这样在运行时,效率完全可以得到大幅提升。 修改上面代码:

;public class MyObject {private static MyObject myObject;private MyObject() {}public static MyObject getInstance() {try { if (myObject != null) {} else {// 模拟在创建对象之前做一些准备性的工作Thread.sleep(3000);// 使用synchronized (MyObject.class)// 虽然部分代码被上锁// 但还是有非线程安全问题synchronized (MyObject.class) {myObject = new MyObject();}}} catch (InterruptedException e) {e.printStackTrace();}return myObject;}

此方法使同步synchronized语句块,只对实例化对象的关键代码进行同步,从语句的结构上来讲,运行的效率的确得到了提升。**但如果是遇到多线程的情况下还是无法解决得到同一个实例对象的结果。
**到底如何解决“懒汉模式”遇到多线程的情况呢?


(4)使用DCL双检查锁机制

在最后的步骤中,使用的是DCL双检查锁机制来实现多线程环境中的延迟加载单例设计模式。

public class MyObject {private volatile static MyObject myObject;private MyObject() {}// 使用双检测机制来解决问题,既保证了不需要同步代码的异步执行性// 又保证了单例的效果public static MyObject getInstance() {try {if (myObject != null) {} else {// 模拟在创建对象之前做一些准备性的工作Thread.sleep(3000);synchronized (MyObject.class) {if (myObject == null) {myObject = new MyObject();}}}} catch (InterruptedException e) {e.printStackTrace();}return myObject;}// 此版本的代码称为双重检查Double-Check Locking
}

结果:
在这里插入图片描述


使用静态内置类实现单例模式

DCL可以解决多线程单例模式的非线程安全问题。当然,使用其他的办法也能达到同样的效果。
创建类MyObject.java代码如下:

public class MyObject {// 内部类方式private static class MyObjectHandler {private static MyObject myObject = new MyObject();}private MyObject() {}public static MyObject getInstance() {return MyObjectHandler.myObject;}
}

线程类:

package extthread;
import test.MyObject;
public class MyThread extends Thread {@Overridepublic void run() {System.out.println(MyObject.getInstance().hashCode());}
}

运行类和上面的一样
结果是三个hashcode一样。


序列化与反序列化的单例模式实现

静态内置类可以达到线程安全问题,但如果遇到序列化对象时,使用默认的方式运行得到的结果还是多例的。

import java.io.ObjectStreamException;
import java.io.Serializable;
public class MyObject implements Serializable {private static final long serialVersionUID = 888L;// 内部类方式private static class MyObjectHandler {private static final MyObject myObject = new MyObject();}private MyObject() {
}public static MyObject getInstance() {return MyObjectHandler.myObject;}// protected Object readResolve() throws ObjectStreamException {// System.out.println("调用了readResolve方法!");// return MyObjectHandler.myObject;// }}

创建业务类SaveAndRead.java代码如下:

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import test.MyObject;
public class SaveAndRead {public static void main(String[] args) {try {MyObject myObject = MyObject.getInstance();FileOutputStream fosRef = new FileOutputStream(new File("myObjectFile.txt"));ObjectOutputStream oosRef = new ObjectOutputStream(fosRef);oosRef.writeObject(myObject);//在这里进行序列化oosRef.close();fosRef.close();System.out.println(myObject.hashCode());} catch (FileNotFoundException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}try {FileInputStream fisRef = new FileInputStream(new File("myObjectFile.txt"));ObjectInputStream iosRef = new ObjectInputStream(fisRef);MyObject myObject = (MyObject) iosRef.readObject();//进行反序列化iosRef.close();fisRef.close();System.out.println(myObject.hashCode());} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} catch (ClassNotFoundException e) {e.printStackTrace();}}

在这里插入图片描述

}解决办法就是在反序列化中使用readResolve()方法。 去掉如下代码的注释:

protected Object readResolve() throws ObjectStreamException {System.out.println("调用了readResolve方法!");return MyObjectHandler.myObject;
}

在这里插入图片描述
当JVM从内存中反序列化地"组装"一个新对象时,就会自动调用这个 readResolve方法来返回我们指定好的对象了, 单例规则也就得到了保证.


使用static代码块实现单例模式

静态代码块中的代码在使用类的时候就已经执行了,所以可以应用静态代码块的这个特性来实现单例设计模式。

public class MyObject {private static MyObject instance = null;private MyObject() {}static {instance = new MyObject();}public static MyObject getInstance() {return instance;}
}

推荐阅读
  • 2023年京东Android面试真题解析与经验分享
    本文由一位拥有6年Android开发经验的工程师撰写,详细解析了京东面试中常见的技术问题。涵盖引用传递、Handler机制、ListView优化、多线程控制及ANR处理等核心知识点。 ... [详细]
  • 优化ListView性能
    本文深入探讨了如何通过多种技术手段优化ListView的性能,包括视图复用、ViewHolder模式、分批加载数据、图片优化及内存管理等。这些方法能够显著提升应用的响应速度和用户体验。 ... [详细]
  • 并发编程:深入理解设计原理与优化
    本文探讨了并发编程中的关键设计原则,特别是Java内存模型(JMM)的happens-before规则及其对多线程编程的影响。文章详细介绍了DCL双重检查锁定模式的问题及解决方案,并总结了不同处理器和内存模型之间的关系,旨在为程序员提供更深入的理解和最佳实践。 ... [详细]
  • 在多线程编程环境中,线程之间共享全局变量可能导致数据竞争和不一致性。为了解决这一问题,Linux提供了线程局部存储(TLS),使每个线程可以拥有独立的变量副本,确保线程间的数据隔离与安全。 ... [详细]
  • 本文探讨了在Java多线程环境下,如何确保具有相同key值的线程能够互斥执行并按顺序输出结果。通过优化代码结构和使用线程安全的数据结构,我们解决了线程同步问题,并实现了预期的并发行为。 ... [详细]
  • 本文总结了Java程序设计第一周的学习内容,涵盖语言基础、编译解释过程及基本数据类型等核心知识点。 ... [详细]
  • Java 类成员初始化顺序与数组创建
    本文探讨了Java中类成员的初始化顺序、静态引入、可变参数以及finalize方法的应用。通过具体的代码示例,详细解释了这些概念及其在实际编程中的使用。 ... [详细]
  • 1:有如下一段程序:packagea.b.c;publicclassTest{privatestaticinti0;publicintgetNext(){return ... [详细]
  • 深入理解Tornado模板系统
    本文详细介绍了Tornado框架中模板系统的使用方法。Tornado自带的轻量级、高效且灵活的模板语言位于tornado.template模块,支持嵌入Python代码片段,帮助开发者快速构建动态网页。 ... [详细]
  • 本文介绍如何利用动态规划算法解决经典的0-1背包问题。通过具体实例和代码实现,详细解释了在给定容量的背包中选择若干物品以最大化总价值的过程。 ... [详细]
  • 本文介绍了Java并发库中的阻塞队列(BlockingQueue)及其典型应用场景。通过具体实例,展示了如何利用LinkedBlockingQueue实现线程间高效、安全的数据传递,并结合线程池和原子类优化性能。 ... [详细]
  • 主要用了2个类来实现的,话不多说,直接看运行结果,然后在奉上源代码1.Index.javaimportjava.awt.Color;im ... [详细]
  • 使用 Azure Service Principal 和 Microsoft Graph API 获取 AAD 用户列表
    本文介绍了一段通用代码示例,该代码不仅能够操作 Azure Active Directory (AAD),还可以通过 Azure Service Principal 的授权访问和管理 Azure 订阅资源。Azure 的架构可以分为两个层级:AAD 和 Subscription。 ... [详细]
  • 在前两篇文章中,我们探讨了 ControllerDescriptor 和 ActionDescriptor 这两个描述对象,分别对应控制器和操作方法。本文将基于 MVC3 源码进一步分析 ParameterDescriptor,即用于描述 Action 方法参数的对象,并详细介绍其工作原理。 ... [详细]
  • Android 渐变圆环加载控件实现
    本文介绍了如何在 Android 中创建一个自定义的渐变圆环加载控件,该控件已在多个知名应用中使用。我们将详细探讨其工作原理和实现方法。 ... [详细]
author-avatar
吴国伟60942
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有