线程安全实例分析
一、变量的线程安全分析
成员变量和静态变量是否线程安全?
● 如果它们没有共享,则线程安全
● 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
—— 如果只有读操作,则线程安全
—— 如果有读写操作,则这段代码是临界区,需要考虑线程安全
局部变量是否线程安全?
● 局部变量是线程安全的
● 但局部变量引用的对象则未必
—— 如果该对象没有逃离方法的作用访问,它是线程安全的
—— 如果该对象逃离方法(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坐标有哪些输入方式吗?本…...
浏览器访问 AWS ECS 上部署的 Docker 容器(监听 80 端口)
✅ 一、ECS 服务配置 Dockerfile 确保监听 80 端口 EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]或 EXPOSE 80 CMD ["python3", "-m", "http.server", "80"]任务定义(Task Definition&…...

iOS 26 携众系统重磅更新,但“苹果智能”仍与国行无缘
美国西海岸的夏天,再次被苹果点燃。一年一度的全球开发者大会 WWDC25 如期而至,这不仅是开发者的盛宴,更是全球数亿苹果用户翘首以盼的科技春晚。今年,苹果依旧为我们带来了全家桶式的系统更新,包括 iOS 26、iPadOS 26…...

Zustand 状态管理库:极简而强大的解决方案
Zustand 是一个轻量级、快速和可扩展的状态管理库,特别适合 React 应用。它以简洁的 API 和高效的性能解决了 Redux 等状态管理方案中的繁琐问题。 核心优势对比 基本使用指南 1. 创建 Store // store.js import create from zustandconst useStore create((set)…...
线程与协程
1. 线程与协程 1.1. “函数调用级别”的切换、上下文切换 1. 函数调用级别的切换 “函数调用级别的切换”是指:像函数调用/返回一样轻量地完成任务切换。 举例说明: 当你在程序中写一个函数调用: funcA() 然后 funcA 执行完后返回&…...
FastAPI 教程:从入门到实践
FastAPI 是一个现代、快速(高性能)的 Web 框架,用于构建 API,支持 Python 3.6。它基于标准 Python 类型提示,易于学习且功能强大。以下是一个完整的 FastAPI 入门教程,涵盖从环境搭建到创建并运行一个简单的…...
linux 错误码总结
1,错误码的概念与作用 在Linux系统中,错误码是系统调用或库函数在执行失败时返回的特定数值,用于指示具体的错误类型。这些错误码通过全局变量errno来存储和传递,errno由操作系统维护,保存最近一次发生的错误信息。值得注意的是,errno的值在每次系统调用或函数调用失败时…...

视频字幕质量评估的大规模细粒度基准
大家读完觉得有帮助记得关注和点赞!!! 摘要 视频字幕在文本到视频生成任务中起着至关重要的作用,因为它们的质量直接影响所生成视频的语义连贯性和视觉保真度。尽管大型视觉-语言模型(VLMs)在字幕生成方面…...

论文浅尝 | 基于判别指令微调生成式大语言模型的知识图谱补全方法(ISWC2024)
笔记整理:刘治强,浙江大学硕士生,研究方向为知识图谱表示学习,大语言模型 论文链接:http://arxiv.org/abs/2407.16127 发表会议:ISWC 2024 1. 动机 传统的知识图谱补全(KGC)模型通过…...

【Zephyr 系列 10】实战项目:打造一个蓝牙传感器终端 + 网关系统(完整架构与全栈实现)
🧠关键词:Zephyr、BLE、终端、网关、广播、连接、传感器、数据采集、低功耗、系统集成 📌目标读者:希望基于 Zephyr 构建 BLE 系统架构、实现终端与网关协作、具备产品交付能力的开发者 📊篇幅字数:约 5200 字 ✨ 项目总览 在物联网实际项目中,**“终端 + 网关”**是…...

IT供电系统绝缘监测及故障定位解决方案
随着新能源的快速发展,光伏电站、储能系统及充电设备已广泛应用于现代能源网络。在光伏领域,IT供电系统凭借其持续供电性好、安全性高等优势成为光伏首选,但在长期运行中,例如老化、潮湿、隐裂、机械损伤等问题会影响光伏板绝缘层…...