Java【多线程】(3)单例模式与线程安全


目录

1.前言

2.正文

2.1线程安全类

2.2杂谈(介绍几个概念)

2.2.1内存可见性

2.2.2指令重排序

2.2.3线程饥饿

1. 什么是线程饥饿?

2. 线程饥饿的常见原因

2.2.4区分wait和sleep

2.4单例模式

2.4.1饿汉模式

2.4.2懒汉模式

2.4.2指令重排序与线程安全

3.小结


1.前言

哈喽大家好吖,今天继续给大家分享Java中多线程的学习,今天主要先给上文做个收尾以及讲解单例模式,那么废话不多说,让我们开始吧。

2.正文

2.1线程安全类

先再重新回顾一个概念,到底如何判断会涉及线程安全问题,凡是该方法涉及到修改数据的操作,而且没有内部进行加锁操作,这样就会导致线程安全问题,那么接下来就来详细介绍线程安全类以及线程不安全类:

常见的线程安全类

  1. 集合框架

    • Vector(同步方法)

    • Hashtable(同步方法)

    • CopyOnWriteArrayList(写时复制)

    • BlockingQueue 实现类(如 ArrayBlockingQueue,用于生产者-消费者模式)

  2. 原子类

    • AtomicIntegerAtomicLong

    • AtomicReferenceAtomicBoolean

  3. 工具类

    • String(不涉及修改)

    • StringBuffer(同步方法,线程安全版的 StringBuilder

    • Collections.synchronizedList()(包装非线程安全集合,如 ArrayList


常见的线程不安全类

  1. 集合框架(集合类本身没有进行任何加锁限制)

    • ArrayListLinkedList

    • HashMapHashSet

    • StringBuilder(非同步的字符序列操作)

  2. 日期格式化类

    • SimpleDateFormat(内部状态可变)

  3. 其他工具类

    • Random(共享种子可能导致竞争)

上述集合中,有的虽然有synchronized,但不推荐使用,因为加锁这个事情,是有代价的,一旦在代码中使用了锁,意味着代码可能会因为锁的竞争,产生阻塞,这样程序执行的效率会大打折扣,一旦造成线程阻塞,从cpu中调度走,啥时候才能回来执行就未知了。

2.2杂谈(介绍几个概念)

2.2.1内存可见性

内存可见性也是造成线程安全问题的原因之一,我们先附上一个代码:

import java.util.Scanner;public class test {public static int flag = 0;public static void main(String[] args) {Thread t1 = new Thread(()->{while(flag == 0){}System.out.println("t1线程结束");});Thread t2 = new Thread(()->{Scanner scanner = new Scanner(System.in);System.out.println("请输入flag值:");flag = scanner.nextInt();});t1.start();t2.start();}
}

我们尝试运行一下,结果发现:

我们修改了flag值,结果发现t1线程没有像我们预期的会结束线程,一个线程读取,一个线程修改,修改线程的值,并没有被线程读到,这就是内存可见性问题。

讲一下为什么:

研究 JDK 的大佬们,就希望通过让编译器 & JVM 对程序员写的代码,自动的进行优化
本来写的代码是进行 xxxxx,编译器/VM 会在你原有逻辑不变的前提下, 对你的代码进行调整
使程序效率更高。

编译器,虽然声称优化操作,是能够保证逻辑不变,尤其是在多线程的程序中,编译器的判断可能出现失误.可能导致编译器的优化,使优化后的逻辑,和优化前的逻辑出现细节上的偏差。

于是原因就显而易见了:

  1. 硬件架构影响

    • CPU缓存:每个线程可能在自己的CPU缓存中操作变量,而非直接读写主内存。

    • 指令重排序:编译器和处理器可能优化指令顺序以提高性能,导致代码执行顺序与预期不一致。

  2. Java内存模型(JMM)抽象

    • JMM规定所有变量存储在主内存,线程通过本地内存(缓存副本)操作变量。

    • 线程间通信需通过主内存完成,本地内存更新若未同步到主内存,其他线程无法感知变化。

于是上述代码我们这样稍作修改就可以了:

import java.util.Scanner;public class test {public static int flag = 0;public static void main(String[] args) {Thread t1 = new Thread(()->{while(flag == 0){try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("t1线程结束");});Thread t2 = new Thread(()->{Scanner scanner = new Scanner(System.in);System.out.println("请输入flag值:");flag = scanner.nextInt();});t1.start();t2.start();}
}

结果如我们所愿。

那么我们不能一遇到内存可见性问题就选择sleep,那样会影响程序执行效率,所以说接下来我们引入一个关键字来解决这个问题:

volatile 是 Java 提供的一种轻量级的同步机制,主要解决 内存可见性 和 指令重排序 问题,但 不保证原子性。确保一个线程对 volatile 变量的修改对其他线程立即可见。

  • 问题根源:线程操作变量时可能使用本地缓存(如 CPU 缓存),而非直接读写主内存。

  • volatile 的解决:强制所有读写操作直接操作主内存,绕过线程本地缓存。

import java.util.Scanner;public class test {public volatile static int flag = 0;public static void main(String[] args) {Thread t1 = new Thread(()->{while(flag == 0){}System.out.println("t1线程结束");});Thread t2 = new Thread(()->{Scanner scanner = new Scanner(System.in);System.out.println("请输入flag值:");flag = scanner.nextInt();});t1.start();t2.start();}
}

结束!

2.2.2指令重排序

还记得上文提到的volatile关键字吗,里面的讲解提到了一个指令重排序的问题,那么这个问题是什么意思呢?这里先简单提及下,讲到单例模式时会有详细讲解:

指令重排序是指在不改变单线程程序执行结果的前提下,编译器或处理器通过调整指令的执行顺序来优化性能。


编译器进行指令重排序的原因:

  • 提高CPU利用率:减少流水线停顿,避免等待慢操作(如内存访问,上文提及)。

  • 优化缓存效率:通过局部性原理提高缓存命中率。

  • 并行执行指令:现代CPU的多级流水线和多核心架构需要指令级并行。


我们为什么之前没有遇到这个问题呢,因为我们在Java初阶的学习过程中大部分都是单线程环境下,只有在多线程环境下才会受到指令重排序的影响。

  • 可见性问题:一个线程的修改对另一个线程不可见。

  • 有序性问题:代码的实际执行顺序与预期不一致。

2.2.3线程饥饿

什么是线程饥饿呢?

1. 什么是线程饥饿?

线程饥饿指在多线程环境下,某个或某些线程长期无法获得所需的资源(如CPU时间片、锁、I/O等),导致其任务无法正常执行的现象。饥饿的线程可能永远等待,或执行进度远慢于其他线程。

关键特征

  • 非全局阻塞(其他线程仍正常运行)。

  • 由资源分配策略或调度机制引起。

  • 可能伴随优先级反转、资源竞争等问题。


2. 线程饥饿的常见原因

原因说明示例
高优先级线程抢占高优先级线程始终优先获得CPU时间片,低优先级线程长期无法执行。线程优先级设置不合理(如Java中setPriority(10)抢占低优先级线程)。
非公平锁竞争锁的获取策略不公平,某些线程始终竞争失败。synchronized关键字导致某些线程饥饿。
资源独占某个线程长期持有共享资源(如数据库连接、文件句柄),其他线程无法获取。未合理释放资源(如忘记关闭锁或未用try-finally块)。
任务调度策略缺陷任务队列设计不合理(如固定顺序的任务分配)。线程池使用无界队列或固定顺序提交任务。
通过合理设计资源分配策略和使用同步工具,可有效减少线程饥饿的发生,保障多线程程序的稳定性和性能。

2.2.4区分wait和sleep

在讲解单例模式前,再最后区分一下wait和sleep:

wait有等待时间,sleep也有等待时间,wait可以使用notify提前唤醒,sleep也可以使用Interrupt提前唤醒。


wait 和 sleep 最主要的区别,在于针对锁的操作.

  1. wait 必须要搭配锁.先加锁, 才能用 wait. sleep 不需要.
  2. 2)如果都是在 synchronized 内部使用, wait 会释放锁.sleep 不会释放锁~

2.4单例模式

单例模式是一种常用的软件设计模式,用于确保某个类只有一个实例,并且提供一个全局访问点。其中饿汉模式和懒汉模式是其中最经典的两种单例模式。

2.4.1饿汉模式

饿汉式单例在类加载时就创建实例,这种方式可以保证线程安全,但是实例的创建是立即进行的,可能会浪费资源。


 

class Singleton {private static Singleton instance = new Singleton(100);public static Singleton getInstance() {return instance;}private Singleton() {}private Singleton(int n) {}
}public class test2 {public static void main(String[] args) {Singleton t1 = Singleton.getInstance();Singleton t2 = Singleton.getInstance();System.out.println(t1 == t2);}
}

为了判断该代码仅创建了一个实例,我们创建t1和t2来判断是一个实例还是两个:

可以发现是一个实例。

2.4.2懒汉模式

懒和饿是相对的,一个是在程序一启动就创建好示例,另一个是尽可能晚的创建实例,以达到节省效率的目的。

懒汉式单例的特点是在需要的时候才创建实例,这种实现方式可以延迟实例的创建,节省资源。但是,如果多个线程同时访问getInstance方法,可能会导致多个实例的创建,因此需要进行同步处理。


class SingletonLazy{public static SingletonLazy instance = null;public static SingletonLazy getInstance(){if(instance == null){instance = new SingletonLazy();}return instance;}private SingletonLazy(){}
}
public class test3 {public static void main(String[] args) {SingletonLazy s1 = SingletonLazy.getInstance();SingletonLazy s2 = SingletonLazy.getInstance();System.out.println(s1 == s2);// SingletonLazy s3 = new SingletonLazy();}
}

 

仿照着饿汉模式,我们仿佛就把正确的代码写出来了,但这里要抛出一个很重要的问题:这样的代码是否是线程安全的呢?

2.4.2指令重排序与线程安全

第一个饿汉模式,在getinstance方法中只涉及到return的读操作,不涉及到线程安全问题。然而懒汉模式的getinstance方法

创建实例时可能涉及到多线程的修改操作,并且一个if语句加上与一个创建示例的语句,这样就违背了原子性的原则。

在多线程环境下,如果有多个线程同时调用 getInstance() 方法,可能会在检查 instance == null 后,多个线程都进入 if 块并创建新的实例。这是因为多个线程可能在同一时间检查到 instancenull,从而都执行 new SingletonLazy(),导致创建多个实例。


所以我们就希望通过修改代码,使其避免上述问题。


但又有新的问题出现了,在多线程情况下,加锁会互相阻塞,影响执行效率,所以我们再进行修改:

此处最外层的if语句即为判断该实例是否已被创建,如果该实例以及被创建,就不需要进行获得锁操作,提升程序执行效率。


这样总会没问题了吧,不其实还有,有没有可能出现内存重排序问题呢,稳妥起见我们加上volatile

到这里就要呼应上文了,那指令重排序呢,现在就要讲了:

在创建实例时,要经过下面几个步骤:

  1. 申请内存空间
  2. 在空间上构造对象(初始化)
  3. 内存空间的首地址,赋值给引用变量

正常来说,这三个步骤按照 123这样的顺序来执行的,但是在指令重排序下,可能成为132 这样的顺序单线程环境下,123 还是 132 其实无所谓~~如果是13 2 这样的顺序执行,多线程环境下,可能会出现 bug !!

如果先进行1,3,那么很有可能出现尝试赋值时在对一个“未初始化的对象”进行操作,于是乎在这里,volatile也起到了解决指令重排序的问题。接下来就是正确的完整代码:
 

class SingletonLazy{private static volatile SingletonLazy instance = null;private static Object locker = new Object();public static SingletonLazy getInstance() {if (instance == null) {synchronized (locker) {if (instance == null) {instance = new SingletonLazy();}}}return instance;}
}
public class test3 {public static void main(String[] args) {SingletonLazy s1 = SingletonLazy.getInstance();SingletonLazy s2 = SingletonLazy.getInstance();System.out.println(s1 == s2);// SingletonLazy s3 = new SingletonLazy();}
}

大功告成!

3.小结

今天的分享到这里就结束了,喜欢的小伙伴点点赞点点关注,你的支持就是对我最大的鼓励,大家加油!

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

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

相关文章

4g串口发短信踩坑

这短短的4行有三种发送方式 1 勾选新行 2 不选新行 3 不选新行,再勾选16进制,完美解决 推荐网站AIR780 MINI LTE 4G全网通模块 — wiki

STM32 ——系统架构

3个被动单元 SRAM 存储程序运行时用到的变量 Flash(内部闪存存储器) 存储下载的程序 程序执行时用到的常量 桥接1和桥接2 AHB到APB的桥(AHBtoAPBx) 桥1 通过APB2总线连接到APB2上的外设。 高速外设,最高72MHz。 桥2 通过…

离散化和树状数组

离散化 #include<bits/stdc.h> using namespace std; using lllong long; const int N3e59; ll a[N]; struct Q {ll a,b; }Add[N],Que[N];//用结构体存储数值对 vector<ll>X; ll getIdx(ll x)//得到离散化数组下标 {return lower_bound(X.begin(),X.end(),x)-X.beg…

序列化和反序列化(Linux)

1 序列化和反序列化 write和read实质是拷贝函数 1.1序列化和反序列化的概述&#xff1a; 2网络版计算器 2.1代码实现 先把日志拷贝过来 2.1.1必须先要有网络功能 先把 TcpServer.hpp编写号 #pragma once #include <cstdint>#include "Socket.hpp" #inclu…

关于ngx-datatable no data empty message自定义模板解决方案

背景&#xff1a;由于ngx-dataable插件默认没有数据时显示的文案是no data to display&#xff0c;且没有任何样式。这里希望通过自定义模板来实现。但目前github中有一个案例是通过设置代码&#xff1a; https://swimlane.github.io/ngx-datatable/empty** <ngx-datatable…

opencv 图片颜色+轮廓识别

目标图片&#xff1a; 1 简单识别图片中出现颜色最多的 import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException;public class SimpleImageColorRecognizer implements ImageColorRecogniz…

文件系统调用(上) ─── linux第17课

目录 linux 中man 2和man 3的区别 文件内容介绍 C语言文件接口 示例: 输出信息到显示器&#xff0c;你有哪些方法 总结: 系统文件I/O 文件类的系统调用接口介绍 示例 open 函数具体使用哪个,和具体应用场景相关&#xff0c; write read close lseek ,类比C文件相关接…

vue3-element-admin 前后端本地启动联调

一、后端环境准备 1.1、下载地址 gitee 下载地址 1.2、环境要求 JDK 17 1.3、项目启动 克隆项目 git clone https://gitee.com/youlaiorg/youlai-boot.git数据库初始化 执行 youlai_boot.sql 脚本完成数据库创建、表结构和基础数据的初始化。 修改配置 application-dev.y…

证券行业SCA开源风险治理实践

近日&#xff0c;悬镜安全成功中标国内领先的证券公司海通证券软件成分分析工具采购项目&#xff0c;中标产品为源鉴SCA开源威胁管控平台。 海通证券作为国内领先的证券公司之一&#xff0c;当下已基本建成涵盖证券期货经纪、投行、自营、资产管理、私募股权投资、另类投资、融…

JVM内存结构笔记(上)

文章目录 前言运行时数据区域1.程序计数器定义特点总结 2.虚拟机栈2.1 定义局部变量表 ★操作数栈动态链接方法返回地址(方法出口) 2.2 栈内存溢出演示栈内存溢出 java.lang.StackOverflowError 2.3问题辨析1. 垃圾回收是否涉及栈内存&#xff1f;2. 栈内存分配越大越好吗&…

使用 Miniforge3 管理 Python 环境的详细指南(基于最新实践和时效性信息,截至 2025 年)

以下是使用 Miniforge3 管理 Python 环境的详细指南&#xff08;基于最新实践和时效性信息&#xff0c;截至 2025 年&#xff09;&#xff1a; 一、Miniforge3 简介 Miniforge3 是一个轻量级 Conda 环境管理工具&#xff0c;默认使用 conda-forge 软件源&#xff08;社区维护的…

【python|二分|leetcode441】一题搞清楚二分区间问题---闭区间、左闭右开、左开右闭、全开区间

every blog every motto: Although the world is full of suffering&#xff0c; it is full also of the overcoming of it 0. 前言 一题搞清楚二分区间问题—闭区间、左闭右开、左开右闭、全开区间 0.1 题目&#xff1a;Problem: 441. 排列硬币 你总共有 n 枚硬币&#x…

【NLP 34、实践 ⑧ 基于faq知识库和文本匹配算法进行意图识别】

目录 一、demo1_similarity_function.py 二、demo2_bm25.py 三、基于faq知识库和文本匹配算法的意图识别 1.初始化 2.加载BM25模型 3.加载Word2Vec模型 4.文本向量化 5.加载知识库 6.查询方法 7.模型测试 正是江南好时节&#xff0c;落花时节又逢君 —— 25.3.7 一、demo1_sim…

机器人交互系统 部署构建

环境要求 Ubuntu 20.04 或更高版本ROS Noetic 或兼容版本Python 3.8 安装步骤 1. 安装ROS环境&#xff08;如未安装&#xff09; sudo apt update sudo apt install ros-noetic-desktop-full source /opt/ros/noetic/setup.bash2. 创建工作空间并克隆代码 mkdir -p ~/code…

每日一题——两数相加

两数相加 问题描述问题分析解题思路代码实现代码解析注意事项示例运行总结 问题描述 给定两个非空链表&#xff0c;表示两个非负整数。链表中的每个节点存储一个数字&#xff0c;数字的存储顺序为逆序&#xff08;即个位在链表头部&#xff09;。要求将这两个数字相加&#xff…

ResNet50深度解析:原理、结构与PyTorch实现

ResNet50深度解析&#xff1a;原理、结构与PyTorch实现 1. 引言 ResNet&#xff08;残差网络&#xff09;是深度学习领域的一项重大突破&#xff0c;它巧妙解决了深层神经网络训练中的梯度消失/爆炸问题&#xff0c;使得构建和训练更深的网络成为可能。作为计算机视觉领域的里…

政安晨【零基础玩转各类开源AI项目】Wan 2.1 本地部署,基于ComfyUI运行,最强文生视频 图生视频,一键生成高质量影片

政安晨的个人主页&#xff1a;政安晨 欢迎 &#x1f44d;点赞✍评论⭐收藏 希望政安晨的博客能够对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff01; 目录 下载项目 创建虚拟环境 安装项目依赖 尝试运行 依次下载模型 完成 我们今天要使…

每日一题----------String 和StringBuffer和StringBuiler重点

本质&#xff1a;是一个char字符数组存储字符串 总结&#xff1a; 1.如果字符串存在大量的修改操作&#xff0c;一般使用StringBuffer或者StringBuilder。 2.如果字符串存在大量的修改操作&#xff0c;并且单线程的情况&#xff0c;使用StringBuilder。 3.如果字符串存在大…

35.HarmonyOS NEXT Layout布局组件系统详解(二):AutoRow行组件实现原理

HarmonyOS NEXT Layout布局组件系统详解&#xff08;二&#xff09;&#xff1a;AutoRow行组件实现原理 文章目录 HarmonyOS NEXT Layout布局组件系统详解&#xff08;二&#xff09;&#xff1a;AutoRow行组件实现原理1. AutoRow组件概述2. AutoRow组件接口定义3. AutoRow组件…

Java 集合框架大师课:集合框架源码解剖室(五)

&#x1f525;Java 集合框架大师课&#xff1a;集合框架源码解剖室&#xff08;五&#xff09; &#x1f4a3; 警告&#xff1a;本章包含大量 裸码级硬核分析&#xff0c;建议搭配咖啡因饮料阅读&#xff01;☕️ 第一章 ArrayList 的扩容玄学 1.1 动态扩容核心代码大卸八块 …