Java多线程技术五——单例模式与多线程

1 概述

        本章的知识点非常重要。在单例模式与多线程技术相结合的过程中,我们能发现很多以前从未考虑过的问题。这些不良的程序设计如果应用在商业项目中将会带来非常大的麻烦。本章的案例也充分说明,线程与某些技术相结合中,我们要考虑的事情会更多。在学习本章的过程中,我们只需要考虑一件事情,那就是:如果使单例模式与多线程结合时是安全、正确的。

2 单例模式与多线程

        在标准的23个设计模式中,单例模式在应用中是比较常见的。但多数常规的该模式教学资料并没有结合多线程技术进行介绍,这就造成在使用结合多线程的单例模式时会出现一些意外。

3 立即加载/饿汉模式

        立即加载指的是,使用类的时候已经将对象创建完毕。常见的实现办法就是new实例化,也被称为“饿汉模式”。

public class MyObject {//立即加载方法 == 饿汉模式private static MyObject object = new MyObject();private MyObject(){}public static MyObject getInstance(){return object;}}
public class MyThread extends Thread{@Overridepublic void run(){System.out.println(MyObject.getInstance().hashCode());}
}
public class Run1 {public static void main(String[] args) {MyThread t1 = new MyThread();MyThread t2 = new MyThread();MyThread t3 = new MyThread();t1.start();t2.start();t3.start();}
}

66f314561cdc42398fc9b5d22a3d957d.png

        控制台打印的hashcode是同一个值,说明对象是一个,也就实现了立即加载型单例模式。此代码为立即加载模式,缺点是不能有其他实例变量,因为getInstance()方法没有同步,所以有可能出现非线程安全问题。

4 延迟加载/懒汉模式

        延迟加载就是调用get()方法时,实例才被创建。常见的实现办法就是在get()方法中进行new实例化,也被称为“懒汉模式”。

4.1 延迟加载解析

        先看下面一段代码。

public class MyObject {private static MyObject object;public MyObject() {}public static MyObject getInstance(){if(object == null){object = new MyObject();}return object;}
}
public class MyThread extends Thread{@Overridepublic void  run(){System.out.println(MyObject.getInstance().hashCode());}
}
public class Run1 {public static void main(String[] args) {MyThread t1 = new MyThread();MyThread t2 = new MyThread();MyThread t3 = new MyThread();t1.start();t2.start();t3.start();}
}

ff9b69c3702340c9b27709e4fa80bd70.png

 4.2 延迟加载的缺点

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

public class MyObject {private static MyObject object;public MyObject() {}public static MyObject getInstance(){try {if(object == null){//模拟在创建对象之前做一些准备工作Thread.sleep(3000);object = new MyObject();}}catch (InterruptedException e){e.printStackTrace();}return object;}
}
public class MyThread extends Thread{@Overridepublic void run(){System.out.println(MyObject.getInstance().hashCode());}
}
public class Run1 {public static void main(String[] args) {MyThread t1 = new MyThread();MyThread t2 = new MyThread();MyThread t3 = new MyThread();t1.start();t2.start();t3.start();}
}

f197c607d54c47b6a385151a86f3ed4c.png

        控制台打印3个不同的hashCode,说明创建了3个对象,并不是单例的。这就是“错误的单例模式”,如何解决呢?

4.3 延迟加载的解决方案 

        (1)声明synchronzied关键字

        既然多个线程可以同时进入getInstance()方法,只需要对getInstance()方法声明synchronzied关键字即可。修改MyObject.java类。

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

326e93f5dd0049f0a4c616c06910e615.png

        此方法在加入同步synchronzied关键字后得到相同实例的对象,但运行效率很低。下一个线程想要取得 对象,必须等待上一个线程释放完锁之后,才可以执行。那换成同步代码块可以解决吗?

(2)尝试同步代码块

        修改MyObject.java类。

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

         此方法加入同步synchronzied语句块后得到相同实例对象,但运行效率也非常低,和synchronzied同步方法一样是同步运行的。下面继续更改代码,尝试解决这个问题。

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

修改MyObject.java类。

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

d18820343cba46039770f296235eadcc.png

        此方法使同步synchronzied语句块只对实例化对象的关键代码进行同步。从语句的结构上讲,运行效率却是得到了提升,但遇到多线程的情况还是无法得到同一个实例对象。

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

public class MyObject {private  volatile static MyObject object;public MyObject() {}public static MyObject getInstance(){try {if(object == null){Thread.sleep(2000);synchronized (MyObject.class){if(object == null){object = new MyObject();}}}}catch (InterruptedException e){e.printStackTrace();}return object;}
}

 使用volatile修改变量object,使该变量在多个线程间可见,另外禁止 object = new MyObject()代码重排序。object = new MyObject()包含3个步骤:

        1、memory = allocate();//分配对象的内存空间

        2、ctorInstance(memory);//初始化对象

        3、object = memory;//设置instance指向刚分配的内存地址

JIT编译器有可能将这三个步骤重新排序。

        1、memory = allocate();//分配对象的内存空间

        2、object = memory;//设置instance指向刚分配的内存地址

        3、ctorInstance(memory);//初始化对象

这时,构造方法虽然还没有执行,但object对象已具有内存地址,即值不是null。当访问object对象中的值时,是当前声明数据类型的默认值。

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

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

     

public class Run1 {public static void main(String[] args) {MyThread t1 = new MyThread();MyThread t2 = new MyThread();MyThread t3 = new MyThread();t1.start();t2.start();t3.start();}
}

e5c10ed8b5de48eaa67856539d801ca8.png  

        可见,使用DCL双检查锁成功解决了懒汉模式下的多线程问题。DCL也是大多数多线程结合单例模式使用的解决方案。

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

        DCL可以解决多线程单例模式的非线程安全问题。还可以使用其他办法达到同样的效果。

public class MyObject {private static class MyObjectHandler{private static MyObject object = new MyObject();}public MyObject() {}public static MyObject getInstance(){return MyObjectHandler.object;}
}

 

public class MyThread extends Thread{@Overridepublic void run(){System.out.println(MyObject.getInstance().hashCode());}
}
public class Run1 {public static void main(String[] args) {MyThread t1 = new MyThread();MyThread t2 = new MyThread();MyThread t3 = new MyThread();t1.start();t2.start();t3.start();}
}

c90168a09bc040ebb2ed202c4c4cad01.png

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

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

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

 

public class MyThread extends Thread{@Overridepublic void run(){for (int i = 0; i < 5; i++) {System.out.println(MyObject.getInstance().hashCode());}}
}
public class Run1 {public static void main(String[] args) {MyThread t1 = new MyThread();MyThread t2 = new MyThread();MyThread t3 = new MyThread();t1.start();t2.start();t3.start();}
}

6197e85b5484485eb782a1342aabf123.png

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/298818.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

微信公众号怎么开评论功能?

为什么公众号没有留言功能&#xff1f;2018年2月12日之后直到现在&#xff0c;新注册公众号的运营者会发现一个问题&#xff1a;无论是个人还是企业的公众号&#xff0c;在后台都找不到留言功能了。这对公众号来说绝对是一个极差的体验&#xff0c;少了一个这么重要的功能&…

阶段七-GitEE

Git&#xff1a;版本控制软件 Git的优点 1.1 协同修改 多人并行不悖的修改服务器端的同一个文件。 1.2 数据备份 不仅保存目录和文件的当前状态&#xff0c;还能够保存每一个提交过的历史状态。 1.3 版本管理 在保存每一个版本的文件信息的时候要做到不保存重复数据&…

C语言--直接插入排序【排序算法|图文详解】

一.直接插入排序介绍&#x1f357; 直接插入排序又叫简单插入排序&#xff0c;是一种简单直观的排序算法&#xff0c;它通过构建有序序列&#xff0c;对于未排序的数据&#xff0c;在已排序序列中从后向前扫描&#xff0c;找到相应位置并插入。 算法描述&#xff1a; 假设要排序…

Linux:线程优先级设置

目录 一、背景二、调整普通线程的优先级通过系统命令通过Linux C代码 三、调整实时线程的优先级通过系统命令通过Linux C代码 四、参考资料&#xff08;建议一定要阅读&#xff09; 在操作系统中&#xff0c;线程优先级决定了线程在 CPU 调度时的重要性。较高优先级的线程会在竞…

docker-compose部署kafka

docker-compose.yml配置 version: "3" services:kafka:image: bitnami/kafka:latestports:- 7050:7050environment:- KAFKA_ENABLE_KRAFTyes- KAFKA_CFG_PROCESS_ROLESbroker,controller- KAFKA_CFG_CONTROLLER_LISTENER_NAMESCONTROLLER- KAFKA_CFG_LISTENERSPLAIN…

usb设备驱动程序(一)

代码&#xff1a; #include <linux/atomic.h> #include <linux/kernel.h> #include <linux/list.h> #include <linux/module.h> #include <linux/slab.h> #include <linux/usb.h> #include <linux/videodev2.h> #include <linux…

巅峰画师Midjourney:新时代的独角兽

介绍 AI绘画领域中&#xff0c;Midjourney处于绝对地位&#xff0c;并且一年时间就登顶。 Midjourney是一家独立的AI研究实验室,探索新的思维媒介,拓展人类的想象力。 它由一个小型的自筹资金团队组成,专注于设计、人类基础设施和AI。 在AI绘画领域,Midjourney取得了非常突出…

2023年国赛高教杯数学建模E题黄河水沙监测数据分析解题全过程文档及程序

2023年国赛高教杯数学建模 E题 黄河水沙监测数据分析 原题再现 黄河是中华民族的母亲河。研究黄河水沙通量的变化规律对沿黄流域的环境治理、气候变化和人民生活的影响&#xff0c;以及对优化黄河流域水资源分配、协调人地关系、调水调沙、防洪减灾等方面都具有重要的理论指导…

PyTorch深度学习实战(27)——变分自编码器(Variational Autoencoder, VAE)

PyTorch深度学习实战&#xff08;27&#xff09;——变分自编码器 0. 前言1. 变分自编码器1.1 自编码器的局限性1.2 VAE 工作原理1.3 VAE 构建策略1.4 KL 散度1.5 重参数化技巧 2. 构建 VAE小结系列链接 0. 前言 变分自编码器 (Variational Autoencoder, VAE) 是一种生成模型&…

测试C#使用AForge从摄像头获取图片

百度“C# 摄像头”关键词&#xff0c;从搜索结果来看&#xff0c;使用OpenCV、AForge、window动态链接库获取摄像头数据的居多&#xff0c;本文学习基于Aforge.net连接摄像头并从摄像头获取图片的基本方法。   AForge相关包&#xff08;尤其是相关的控件&#xff09;主要针对…

【Mathematical Model】Python拟合一元一/二次方程(线性回归)

Python中可以使用多种库进行拟合方程&#xff0c;其中最常用的是NumPy和SciPy。NumPy是一个用于处理数组和矩阵的库&#xff0c;而SciPy则提供了大量的科学计算函数&#xff0c;包括拟合算法。 1 一元一次方程拟合 需要注意的是我们这里的方程需要我们自己定义好&#xff0c;然…

OpenCV-Python(9):图像基础操作

目录 学习目标 获取图像像素并修改像素值 获取图像属性 图像ROI 拆分及合并图像通道 图像边缘扩充 学习目标 获取像素值并修改获取图像的属性(信息)图像的ROI获取图像通道拆分及合并图像扩边 获取图像像素并修改像素值 几乎所有这些操作与Numpy 的关系要比与OpenCV 的…