线程安全实例分析
一、变量的线程安全分析
成员变量和静态变量是否线程安全?
● 如果它们没有共享,则线程安全
● 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
—— 如果只有读操作,则线程安全
—— 如果有读写操作,则这段代码是临界区,需要考虑线程安全
局部变量是否线程安全?
● 局部变量是线程安全的
● 但局部变量引用的对象则未必
—— 如果该对象没有逃离方法的作用访问,它是线程安全的
—— 如果该对象逃离方法(eg:使用return)的作用范围,需要考虑线程安全
1.1 线程安全分析-局部变量
public static void test1() {int i = 10;i++;
}
每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享
反编译后的二进制字节码:
public static void test1();descriptor: ()V
flags: ACC_PUBLIC, ACC_STATICCode:stack=1, locals=1, args_size=00: bipush 10 // 准备常数10赋值给i2: istore_0 // 赋值给i3: iinc 0, 1 // 在局部变量i的基础上自增 6: return // 方法运行结束返回LineNumberTable:line 10: 0line 11: 3line 12: 6LocalVariableTable:Start Length Slot Name Signature3 4 0 i I
每个方法调用时都会创建一个栈帧,每个线程有自己独立的栈和栈帧内存(局部变量会在栈帧中被创建多份)
如图:

若局部变量的为对象,则稍有不同
观察一个成员变量的例子
public class TestThreadSafe {// 创建两个线程(每个线程调用method1循环200次)static final int THREAD_NUMBER = 2;static final int LOOP_NUMBER = 200;public static void main(String[] args) {ThreadUnsafe test = new ThreadUnsafe();for (int i = 0; i < THREAD_NUMBER; i++) {new Thread(() -> {test.method1(LOOP_NUMBER);}, "Thread" + (i+1)).start();}}
}
class ThreadUnsafe {ArrayList<String> list = new ArrayList<>();public void method1(int loopNumber) {for (int i = 0; i < loopNumber; i++) {// { 临界区, 会产生竞态条件// method2()、method3()访问的为共享资源(多个线程执行时会发生指令交错)method2();method3();// } 临界区}}private void method2() {// 往集合中加一个元素list.add("1");}// 往集合中移除一个元素private void method3() {list.remove(0);}
}
多个线程执行时会发生指令交错会产生问题
运行结果:其中一种情况是线程1的method2()还未add,线程2的method3()尝试移除,此时集合为空就会报错

分析:
● 无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量
● method3 与 method2 分析相同

若将 list 修改为局部变量就不会存在上述问题
class ThreadSafe {public final void method1(int loopNumber) {ArrayList<String> list = new ArrayList<>();for (int i = 0; i < loopNumber; i++) {method2(list);method3(list);}}public void method2(ArrayList<String> list) {list.add("1");}private void method3(ArrayList<String> list) {System.out.println(1);list.remove(0);}
}
分析:
● list 是局部变量,每个线程调用时会创建其不同实例,没有共享
● 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象(均引用的为堆中的对象)
● method3 的参数分析与 method2 相同

1.2 线程安全分析-局部变量引用
方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题?
● 情况1:有其它线程调用 method2 和 method3
● 情况2:在 情况1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,即
class ThreadSafe {public final void method1(int loopNumber) {ArrayList<String> list = new ArrayList<>();for (int i = 0; i < loopNumber; i++) {method2(list);method3(list);}}private void method2(ArrayList<String> list) {list.add("1");}private void method3(ArrayList<String> list) {list.remove(0);}
}
// 添加子类继承ThreadSafe,在子类中覆盖/重写method3
class ThreadSafeSubClass extends ThreadSafe{@Overridepublic void method3(ArrayList<String> list) {// 重写后重启一个新的线程new Thread(() -> {list.remove(0);}).start();}
}
此时会带来线程安全问题,新的线程可以访问到共享变量
从这个例子可以看出 private 或 final 提供【安全】的意义所在,可以体会开闭原则中的【闭】(使用private修饰符避免子类改变覆盖其行为)
1.3 线程安全分析-常见类-组合调用
常见线程安全类
● String
● Integer
● StringBuffer
● Random
● Vector
● Hashtable
● java.util.concurrent 包下的类
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为
Hashtable table = new Hashtable(); // 查看源码,发现其底层被synchronized关键字修饰new Thread(()->{table.put("key", "value1");
}).start();new Thread(()->{table.put("key", "value2");
}).start();
● 它们的每个方法是原子的
● 但注意它们多个方法的组合不是原子的,见后面分析
线程安全类方法的组合
分析下面代码是否线程安全?
get()、put()底层均有synchronized修饰
Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {table.put("key", value);
}
将两个方法组合到一起使用就不是线程安全的,中间会受到线程上下文切换的影响,其只能保证每一个方法内部代码是原子的。要使其组合后仍可以保证原子性,还需在外层加以线程安全的保护!
eg:线程1、2均执行方法内的代码,线程1执行get(“key”) == null,还未执行完,线程发生上下文切换轮到线程2执行,线程2也执行到此处得到的get(“key”) == null,线程2发现为null后put(“key”, v2),完成后又切换为线程1,线程1又put(“key”, v1)。理论上判断为空时,我们只存放一个键值对,实际上put(“key”, value)被执行两次,导致后一个执行的put将前一个执行put的结果覆盖,不是我们锁预期的效果。

1.3 线程安全分析-常见类-不可见
不可变类线程安全
String、Integer 等都是不可变类,因为其内部的状态(属性)不可以改变,因此它们的方法都是线程安全的(只可读不可修改)
那么,String 有 replace,substring 等方法【可以】改变值,那么这些方法又是如何保证线程安
全的?(其没有改变字符串的值,而是创建了一个新的字符串对象对原有的字符串复制,里面包含截取后的结果)用新的对象实现对象的不可变效果
public class Immutable{private int value = 0;public Immutable(int value){this.value = value;}public int getValue(){return this.value;}
}
如果想增加一个增加的方法应该如何实现?
public class Immutable{private int value = 0;public Immutable(int value){this.value = value;}public int getValue(){return this.value;}public Immutable add(int v){return new Immutable(this.value + v);}
}
1.4 线程安全分析-实例分析
例1:
Servlet运行Tomcat环境下,只有一个实例(会被Tomcat多个线程所共享使用)
public class MyServlet extends HttpServlet {// 是否安全?/*Map不是线程安全的,线程安全的实现有HashTable,而HashMap并非线程安全,若多个请求线程访问同一个Servlet,有的存储内容而有的读取内容,会造成混乱*/Map<String,Object> map = new HashMap<>();// 是否安全?/*是线程安全的,字符串属于不可变量*/String S1 = "...";// 是否安全?(是)final String S2 = "...";// 是否安全?(不是)Date D1 = new Date();// 是否安全?/*final修饰后只能说明D2这个成员变量的引用值固定,而Date中的其它属性还可以可变的)*/final Date D2 = new Date();public void doGet(HttpServletRequest request, HttpServletResponse response) {// 使用上述变量}
}
例2:Servlet调用Service
public class MyServlet extends HttpServlet {// 是否安全?/*不是===>Servlet只有一份,而userService是Servlet的一个成员变量,因此也只有一份,会有多个线程共享使用*/private UserService userService = new UserServiceImpl();public void doGet(HttpServletRequest request, HttpServletResponse response) {userService.update(...);}
}
public class UserServiceImpl implements UserService {// 记录调用次数private int count = 0;public void update() {// ...count++;}
}
例3:
@Aspect
@Component
public class MyAspect {// 是否安全?private long start = 0L;@Before("execution(* *(..))")public void before() {start = System.nanoTime();}@After("execution(* *(..))")public void after() {long end = System.nanoTime();System.out.println("cost time:" + (end-start));}
}
spring中若未指定scope为非单例。默认为单例模式(需要被共享,其成员变量也需被共享,因此无论是执行复制操作还是下面执行减法运算,都会涉及到对象对成员变量的并发修改,会存在线程安全问题)
如何解决上述问题?
可以使用环绕通知(环绕通知可以将开始时间、结束时间变为环绕通知中的局部变量,此时便可保证线程安全)
例4:
public class MyServlet extends HttpServlet {// 是否安全private UserService userService = new UserServiceImpl();public void doGet(HttpServletRequest request, HttpServletResponse response) {userService.update(...);}
}
public class UserServiceImpl implements UserService {// 是否安全private UserDao userDao = new UserDaoImpl();public void update() {userDao.update();}
}
public class UserDaoImpl implements UserDao { public void update() {String sql = "update user set password = ? where username = ?";// 是否安全try (Connection conn = DriverManager.getConnection("","","")){// ...} catch (Exception e) {// ...}}
}
① Dao无成员变量,意味着即使有多个线程访问也不能修改它的属性、状态===>没有成员变量的类都是线程安全的
② Connection也是线程安全的,Connection属于方法内的局部变量,即使有多个线程访问,线程1创建的为Connection1而线程2创建的为Connection2,两者独立互不干扰
例5:
public class MyServlet extends HttpServlet {// 是否安全private UserService userService = new UserServiceImpl();public void doGet(HttpServletRequest request, HttpServletResponse response) {userService.update(...);}
}
public class UserServiceImpl implements UserService {// 是否安全private UserDao userDao = new UserDaoImpl();public void update() {userDao.update();}
}
public class UserDaoImpl implements UserDao {// 是否安全(不安全)/*Connection不为方法内的局部变量,而是做为Dao的成员变量(Dao只有一份会被多个线程共享,其内的共享变量也会被线程共享)*//*eg:线程1刚创建Connection还未使用,此时线程2close()*/private Connection conn = null;public void update() throws SQLException {String sql = "update user set password = ? where username = ?";conn = DriverManager.getConnection("","","");// ...conn.close();}
}
对于Connection这种对象应将其变为线程内私有的局部变量,而不是设置为共享的成员变量
例6:
public class MyServlet extends HttpServlet {// 是否安全private UserService userService = new UserServiceImpl();public void doGet(HttpServletRequest request, HttpServletResponse response) {userService.update(...);}
}
public class UserServiceImpl implements UserService { public void update() {UserDao userDao = new UserDaoImpl();userDao.update();}
}
public class UserDaoImpl implements UserDao {// 是否安全private Connection = null;public void update() throws SQLException {String sql = "update user set password = ? where username = ?";conn = DriverManager.getConnection("","","");// ...conn.close();}
}
UserDao在Service中作为方法内的局部变量存在,每一个线程调用时都会创建一个新的UserDa0对象,其内部的Connection也为新的。因此线程安全。
例7:
public abstract class Test {public void bar() {// 是否安全/*SimpleDateFormat虽然为方法内的局部变量,但其会暴露给其他线程(抽象方法其子类可能会产生一些不恰当的操作)*/SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");foo(sdf);}public abstract foo(SimpleDateFormat sdf);public static void main(String[] args) {new Test().bar();}
}
其中 foo 的行为是不确定的,可能导致不安全的发生,被称之为外星方法
public void foo(SimpleDateFormat sdf) {String dateStr = "1999-10-11 00:00:00";for (int i = 0; i < 20; i++) {new Thread(() -> {try {sdf.parse(dateStr);} catch (ParseException e) {e.printStackTrace();}}).start();}
}
不想向外暴露的变量可以使用final、private修饰,这样可以增强类的安全性
可以比较 JDK 中 String 类的实现:String类是不可变的,同时也是final的
private static Integer i = 0;public static void main(String[] args) throws InterruptedException {List<Thread> list = new ArrayList<>();for (int j = 0; j < 2; j++) {Thread thread = new Thread(() -> {for (int k = 0; k < 5000; k++) {synchronized (i) {i++;}}}, "" + j);list.add(thread);}list.stream().forEach(t -> t.start());list.stream().forEach(t -> {try {t.join();} catch (InterruptedException e) {e.printStackTrace();}});
为何将String类涉及为final?:若不使用final修饰,其子类也许可能覆盖掉String父类中的一些行为,导致线程不安全的发生(子类可能会破坏父类中某一方法的行为)
相关文章:
线程安全实例分析
一、变量的线程安全分析 成员变量和静态变量是否线程安全? ● 如果它们没有共享,则线程安全 ● 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况 —— 如果只有读操作,则线程安全 —— 如果有读写操作&am…...
Tomcat源码分析-启动分析(二) Catalina初始化
Bootstrap Tomcat运行是通过Bootstrap的main方法,在开发工具中,我们只需要运行Bootstrap的main方法,便可以启动tomcat进行代码调试和分析。Bootstrap是tomcat的入口,它会完成初始化ClassLoader,实例化Catalina以及loa…...
基础复习第二十二天 泛型的使用
泛型JDK1.5设计了泛型的概念。泛型即为“类型参数”,这个类型参数在声明它的类、接口或方法中,代表未知的通用的类型。例如:java.lang.Comparable接口和java.util.Comparator接口,是用于对象比较大小的规范接口,这两个…...
【C++进阶】三、二叉搜索树
目录 一、二叉搜索树 1.1 概念 1.2 二叉搜索树操作 二、二叉搜索树实现 2.1 框架总览 2.2 实现接口总览 2.2.1 构造函数 2.2.2 拷贝构造 2.2.3 赋值重载 2.2.4 析构函数 2.2.5 二叉搜索树的遍历 2.2.6 插入函数 2.2.7 查找函数 2.2.8 删除函数 2.3 二叉搜索数完整…...
电脑系统崩溃怎么修复教程
系统崩溃了怎么办? 如今的软件是越来越复杂、越来越庞大。由系统本身造成的崩溃即使是最简单的操作,比如关闭系统或者是对BIOS进行升级都可能会对PC合操作系统造成一定的影响。下面一起来看看电脑系统崩溃修复方法步骤。 工具/原料: 系统版本…...
语义分割数据标注案例分析
语义分割(Semantic Segmentation)是计算机视觉领域中的一种重要任务,它的目的是将图像中的每个像素分配到对应的语义类别中。简单来说,就是将一张图像分割成多个区域,并为每个像素指定一个标签,标识出它属于…...
回归预测 | MATLAB实现GRU(门控循环单元)多输入单输出(多指标评价)
回归预测 | MATLAB实现GRU(门控循环单元)多输入单输出(多指标评价) 文章目录 回归预测 | MATLAB实现GRU(门控循环单元)多输入单输出(多指标评价)预测效果基本介绍程序设计参考资料预测效果 基本介绍 GRU神经网络是LST...
驱动程序开发:Buildroot根文件系统构建并加载驱动文件xxx.ko测试
目录一、buildroot根文件系统简介二、buildroot下载三、buildroot构建根文件系统1、配置 buildroot①配置 Target options②配置 Toolchain③配置 System configuration④配置 Filesystem images⑤禁止编译 Linux 内核和 uboot2、 buildroot 下的 busybox 配置①修改 Makefile&…...
R+VIC模型融合实践技术应用及未来气候变化模型预测
目录 理论专题一:VIC模型的原理及特点 综合案例一:基于QGIS的VIC模型建模 理论专题二:VIC模型率定验证 综合案例二:基于R语言VIC参数率定和优化 理论专题三:遥感技术与未来气候变化 综合案例三:运用V…...
第六章.决策树(Decision Tree)—ID3算法,C4.5算法
第六章.决策树(Decision Tree) 6.1 ID3算法,C4.5算法 1.决策树适用的数据类型 比较适合分析离散数据,如果是连续数据要先转换成离散数据再做分析 2.信息熵 1).概念: 一条信息的信息量大小和它的不确定性有直接的关系,要搞清楚一件非常不确…...
springboot+pgbouncer+postgres数据库连接池集成方案及问题解决
期望通过每一次分享,让技术的门槛变低,落地更容易。 —— around 前言 旨在解决微服务项目全是连接池并影响数据库并发连接,作者的环境是基于sprongboot微服务连接postgres数据库,每个微服务的DAO层配置都使用了连接池技术。后续…...
Mysql 常用日期处理函数
Mysql 常用日期处理函数 1 建表语句 SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS 0; -- ---------------------------- -- Table structure for emp -- ---------------------------- DROP TABLE IF EXISTS emp; CREATE TABLE emp (EMPNO int(4) NOT NULL,ENAME varchar(10…...
Pod中容器的健康检查
健康检查 上篇文章中我们了解了Pod中容器的生命周期的两个钩子函数,PostStart与PreStop,其中PostStart是在容器创建后立即执行的,而PreStop这个钩子函数则是在容器终止之前执行的。除了上面这两个钩子函数以外,还有一项配置会影响…...
信贷系统学习总结(5)—— 简单的风控示例(含代码)
一、背景1.为什么要做风控?目前我们业务有使用到非常多的AI能力,如ocr识别、语音测评等,这些能力往往都比较费钱或者费资源,所以在产品层面也希望我们对用户的能力使用次数做一定的限制,因此风控是必须的!2.为什么要自己写风控?那么多开源的风控组件,为什么还要写呢?是不是想…...
Java知识复习(四)多线程、并发编程
1、进程、线程和程序 进程:进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的;在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程…...
一个9个月测试经验的人,居然在面试时跟我要18K,我都被他吓到了····
2月初我入职了深圳某家创业公司,刚入职还是很兴奋的,到公司一看我傻了,公司除了我一个测试,公司的开发人员就只有3个前端2个后端还有2个UI,在粗略了解公司的业务后才发现是一个从零开始的项目,目前啥都没有…...
zigbee与WIFI同频干扰问题
zigbee与WIFI同频干扰 为了降低Wifi信道与Zigbee信道的同频干扰问题,Zigbee联盟在《Zigbee Home Automation Public Application Profile》中推荐使用11,14,15,19,20,24,25这七个信道。 为什么呢,我们看一下Wifi和Zigbee的信道分布。 WiFi带宽对干扰的…...
git拉取指定的单个或多个文件或文件夹
直接上步骤 初始化仓库 git init拉取远程仓库信息,省略号为仓库地址 git remote add -f origin http://****.git开启 sparse clone git config core.sparsecheckout true配置需要拉取的文件夹 有一个指定一个,有多个指定多个,路径写对即可&a…...
不是,到底有多少种图片懒加载方式?
一、也是我最开始了解到的 js方法,利用滚动事件,判断当时的图片位置是否在可视框内,然后进行渲染。 弊端:代码冗杂,你还要去监听页面的滚动事件,这本身就是一个不建议监听的事件,即便是我们做了…...
CAD坐标有哪些输入方式?来看看这些CAD坐标输入方式!
在CAD设计过程中,有时需要通过已知坐标点来画图,有时又需要通过已知角度和距离来画图,在这种情况下,由于已知条件不同,所以便需要用不同的方式来定位点。那么,你知道CAD坐标有哪些输入方式吗?本…...
KubeSphere 容器平台高可用:环境搭建与可视化操作指南
Linux_k8s篇 欢迎来到Linux的世界,看笔记好好学多敲多打,每个人都是大神! 题目:KubeSphere 容器平台高可用:环境搭建与可视化操作指南 版本号: 1.0,0 作者: 老王要学习 日期: 2025.06.05 适用环境: Ubuntu22 文档说…...
变量 varablie 声明- Rust 变量 let mut 声明与 C/C++ 变量声明对比分析
一、变量声明设计:let 与 mut 的哲学解析 Rust 采用 let 声明变量并通过 mut 显式标记可变性,这种设计体现了语言的核心哲学。以下是深度解析: 1.1 设计理念剖析 安全优先原则:默认不可变强制开发者明确声明意图 let x 5; …...
云计算——弹性云计算器(ECS)
弹性云服务器:ECS 概述 云计算重构了ICT系统,云计算平台厂商推出使得厂家能够主要关注应用管理而非平台管理的云平台,包含如下主要概念。 ECS(Elastic Cloud Server):即弹性云服务器,是云计算…...
解决Ubuntu22.04 VMware失败的问题 ubuntu入门之二十八
现象1 打开VMware失败 Ubuntu升级之后打开VMware上报需要安装vmmon和vmnet,点击确认后如下提示 最终上报fail 解决方法 内核升级导致,需要在新内核下重新下载编译安装 查看版本 $ vmware -v VMware Workstation 17.5.1 build-23298084$ lsb_release…...
MVC 数据库
MVC 数据库 引言 在软件开发领域,Model-View-Controller(MVC)是一种流行的软件架构模式,它将应用程序分为三个核心组件:模型(Model)、视图(View)和控制器(Controller)。这种模式有助于提高代码的可维护性和可扩展性。本文将深入探讨MVC架构与数据库之间的关系,以…...
linux arm系统烧录
1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 (忘了有没有这步了 估计有) 刷机程序 和 镜像 就不提供了。要刷的时…...
C++中string流知识详解和示例
一、概览与类体系 C 提供三种基于内存字符串的流,定义在 <sstream> 中: std::istringstream:输入流,从已有字符串中读取并解析。std::ostringstream:输出流,向内部缓冲区写入内容,最终取…...
ArcGIS Pro制作水平横向图例+多级标注
今天介绍下载ArcGIS Pro中如何设置水平横向图例。 之前我们介绍了ArcGIS的横向图例制作:ArcGIS横向、多列图例、顺序重排、符号居中、批量更改图例符号等等(ArcGIS出图图例8大技巧),那这次我们看看ArcGIS Pro如何更加快捷的操作。…...
全志A40i android7.1 调试信息打印串口由uart0改为uart3
一,概述 1. 目的 将调试信息打印串口由uart0改为uart3。 2. 版本信息 Uboot版本:2014.07; Kernel版本:Linux-3.10; 二,Uboot 1. sys_config.fex改动 使能uart3(TX:PH00 RX:PH01),并让boo…...
OPenCV CUDA模块图像处理-----对图像执行 均值漂移滤波(Mean Shift Filtering)函数meanShiftFiltering()
操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 在 GPU 上对图像执行 均值漂移滤波(Mean Shift Filtering),用于图像分割或平滑处理。 该函数将输入图像中的…...
