JavaEE-多线程初阶(3)

目录

1.线程的状态

1.1 NEW、RUNNABLE、TERMINATED

1.2 TIMED_WAITING

1.3 WAITING

1.4 BLOCKED

2.多线程带来的风险-线程安全(重点)

2.1 观察线程不安全的现象

2.2 分析产生该现象的原因

2.3 产生线程安全问题的原因

2.3.1 抢占式执行(根本)

2.3.2 多个线程同时修改同一个变量

2.3.3 修改操作,不是原子的

2.3.4 内容可见性问题

2.3.5 指令从排序

2.4 如何解决线程安全问题

2.4.1 抢占式执行

2.4.2 多个线程同时修改同一个变量

2.4.3 修改操作,不是原子的

解决方案

加锁:synchronized

synchronized的变种写法



1.线程的状态

线程的所有状态如下:

NEW: 安排了⼯作, 还未开始⾏动
RUNNABLE: 可⼯作的. ⼜可以分成正在⼯作中和即将开始⼯作.
BLOCKED: 这⼏个都表⽰排队等着其他事情
WAITING: 这⼏个都表⽰排队等着其他事情
TIMED_WAITING: 这⼏个都表⽰排队等着其他事情
TERMINATED: ⼯作完成了.

1.1 NEW、RUNNABLE、TERMINATED

对于这三个状态的解释:

NEW:new了Thread对象,还没start

RUNNABLE :就绪状态,可以分成以下两种情况(正在工作或者即将开始工作):

(1)线程正在cpu上执行
(2)线程随时可以去cpu上执行

TERMINATED:内核中的线程已经结束了,但是Thread对象还在

    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(()->{
            System.out.println("线程t1开始执行...");
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("t1线程结束...");
        });
        System.out.println(t1.getState());
        t1.start();
        Thread.sleep(0,500);
        System.out.println(t1.getState());
        t1.join();
        System.out.println(t1.getState());
    }

执行结果:

1.2 TIMED_WAITING

指定时间的阻塞

• 线程阻塞(不参与 cpu 调度,不继续执行了)

阻塞的时间是有上限的

Thread.sleep(时间);会进入到TIMED_WAITING状态

另外,join(时间)也会进入到TIMED_WAITING状态:

1.3 WAITING

死等,没有超时时间的阻塞等待

1.4 BLOCKED

也是一种阻塞,比较特殊,是由于锁导致的阻塞

2.多线程带来的风险-线程安全(重点)

2.1 观察线程不安全的现象

案例:

创建一个静态变量count=0;

在main方法里创建两个线程t1和t2

两个线程内分别循环五万次count++操作

最后打印出count的值:

public class Demo10 {
    public static int count=0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

按照道理来说,count进行了总共十万次count++操作,打印出来的值应该是十万,实际上却并非如此,执行代码:

这样的代码很明显是有bug的,实际执行效果与预期效果不符合就叫做bug。

这样的问题,是多线程并发执行引发的问题。

如果调换代码的执行程序,把t1线程和t2线程的并行改成串行(一个执行完了再执行另一个)

结果又变成正确的了:

执行结果:

很明显,当前的bug是由于多线程的并发执行代码引起的bug

这样的bug,就称为“线程安全问题”,或者叫做“线程不安全”

反之,如果一个代码在多线程并发执行的环境下也不会出现类似上述的bug

这样的代码就称为“线程安全”

2.2 分析产生该现象的原因

站在 CPU 的角度来看count++,这个操作看起来是一行代码,实际上对应到三个CPU指令:

1.load,把内存中的值(count变量)读取到寄存器中

2.add,把指定的寄存器中的值进行+1操作(结果还是在这个寄存器中)

3.save,把寄存器中的值,写回到内存中

CPU执行这三条指令的过程中,随时有可能触发线程的调度切换(如下所示):

指令1 指令2 指令3 线程切走

指令1  指令2  线程切走  指令3

指令1 线程切走 指令2 指令3

指令1 线程切走 指令2 线程切走 指令3

但是由于操作系统的调度是随机的,不可预期的

执行任何一个指令的过程中都有可能触发上述的“线程切换”的操作

也就是说,随机调度(或者叫抢占式执行)就是线程安全问题的罪魁祸首。

t1线程和t2线程的执行过程可能会出现如下情况(只举了个别例子,可能还有别的情况):

对于执行结果:打 √ 的为正确结果,× 为错误结果。

对于其中某种正确情况的具体执行过程如下:

对于其中某种错误情况的具体执行过程如下:

可以看到:

t1线程t2线程分别进行了一次count++操作,理论上count的值应为2,实际上由于随机调度,t1的寄存器和t2的寄存器都读取了内存的初始值0,最终执行完两次count++后count的值为1

由于线程执行的过程中有可能(大概率)会发生上述的错误情况,因此代码的执行结果总是<=100000


如果降低单个线程的循环次数,会发现能运行出正确的结果:

运行结果:

出现这种现象的原因:

事实上,线程安全问题依旧存在,只不过概率变低了。

单个线程执行的count++次数会影响到这个线程的运行时间,即运行50次比运行50000次要快得多

因为运行的太快了,很可能在t2.start()执行之前,t1线程就执行完了

等到后续t2线程的执行,就变成了串行执行

2.3 产生线程安全问题的原因

2.3.1 抢占式执行(根本)

抢占式执行:操作系统对于线程的调度是随机的

抢占式执行策略

最初诞生于多任务操作系统的时候,是非常重大的发明

后世的操作系统,都是一脉相承

2.3.2 多个线程同时修改同一个变量

如果是一个线程,修改一个变量---没问题

如果是多个线程不是同时修改同一个变量---没问题

如果是多个线程,修改不同变量---没问题(不会出现中间结果相互覆盖的情况)

如果是多个线程读取同一个变量---没问题(该变量不可修改)

2.3.3 修改操作,不是原子的

原子:

如果一个操作只是对应到一个cpu指令,那么就可以认为它是原子的

cpu就不会出现“一条指令执行到一半”这样的情况

反之,如果一个操作对应到多个cpu指令,那么它就不是原子的,例如:

++

--

+=

-=

........

2.3.4 内容可见性问题

后续再讨论

2.3.5 指令从排序

后续再讨论

2.4 如何解决线程安全问题

2.4.1 抢占式执行

抢占式执行是操作系统的底层设定,程序员无法左右。

2.4.2 多个线程同时修改同一个变量

和代码的结构直接相关

可以调整代码结构,规避一些线程不安全的代码

但是这样的方案不够通用

Java中有个东西:

String 就是采用了“不可变”特性,确保线程安全

2.4.3 修改操作,不是原子的

解决方案

Java中解决线程安全问题,最主要的方案就是:加锁

通过加锁操作,让不是原子的操作,打包成一个原子的操作

计算机中的锁,和生活中的锁,是同样的概念:互斥/排他

把锁“锁上”称为“加锁”

把锁“解开”称为“解锁”

一旦把锁加上了,其他人要想加锁,就得阻塞等待

对于上述例子的count++操作,它并非是原子的,此时就可以使用锁,把刚才不是原子的count++

包裹起来,在count++之前,先加锁,然后进行count++,计算完毕之后,再解锁

(此时在执行三步走的过程中,其他线程就没法“插队”了)

加锁操作,不是把线程锁死到cpu上,禁止这个线程被调度走

而是禁止其他线程重新加这个锁,避免其他线程的操作在当前线程执行的过程中“插队”


加锁:synchronized

加锁 / 解锁 本身是操作系统提供的api

很多编程语言都对这样的api进行了封装

大多数的封装风格,都是采取两个函数:

加锁 lock();

//执行一些要保护起来的逻辑

解锁 unlock();

Java中使用 synchronized 这样的关键字,搭配代码块来实现类似的效果:

synchronized(){ //进入代码块,相当于加锁

//执行一些要保护起来的逻辑

}出了代码块,相当于解锁

使用synchronized对上述案例进行加锁:

可以看到括号内需要填入参数,应该填入什么呢?

填写的是,用来加锁的对象。要 加锁 / 解锁,前提是得先有一个锁

在Java中任何一个对象都可以用作“锁”

这个对象的类型是什么不重要

重要的是,是否有多个线程针对这同一个对象加锁(竞争同一把锁)

再次执行代码,线程安全问题得到了解决:

【注意】

两个线程,针对同一个对象加锁,才会产生互斥效果

(一个线程加上了锁,另一个线程就得阻塞等待,等到第一个线程释放锁,才有机会)

如果是不同的锁对象,此时不会有互斥效果,线程安全问题并没有得到解决:

代码执行结果,依旧存在线程安全问题:


synchronized的变种写法

可以使用 synchronized 修饰方法

这种写法跟之前的写法产生的效果是一样的,执行代码:

修饰方法的写法还能继续变形:使用synchronized修饰方法,就相当于是针对this进行加锁

这种写法跟上面的效果也是一样的。

StringBuffer、Vector这些类里面有些方法就是带有synchronized的:

因此,StringBuffer是线程安全的,而StringBuilder线程不安全。

而方法之中,还有一个特殊情况,static修饰的方法不存在this

此时,synchronized修饰static方法相当于针对类对象加锁:


如果哪里有疑问的话欢迎来评论区指出和讨论,如果觉得文章有价值的话就请给我点个关注还有免费的收藏和赞吧,谢谢大家

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

江协科技STM32学习- P35 硬件I2C读写MPU6050

&#x1f680;write in front&#x1f680; &#x1f50e;大家好&#xff0c;我是黄桃罐头&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流 &#x1f381;欢迎各位→点赞&#x1f44d; 收藏⭐️ 留言&#x1f4dd;​…

学习虚幻C++开发日志——定时器

官方文档&#xff1a;虚幻引擎中的Gameplay定时器 | 虚幻引擎 5.5 文档 | Epic Developer Community | Epic Developer Community 定时器 安排在经过一定延迟或一段时间结束后要执行的操作。例如&#xff0c;您可能希望玩家在获取某个能力提升道具后变得无懈可击&#xff0c;…

【简道云 -注册/登录安全分析报告】

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 暴力破解密码&#xff0c;造成用户信息泄露短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造成亏损无底洞…

【表格解决问题】EXCEL行数过多,WPS如何按逐行分别打印多个纸张中

1 问题描述 如图&#xff1a;我的表格行数太多了。打印在一张纸上有点不太好看 2 解决方式 Step01&#xff1a;先选中你需要打印的部分&#xff0c;找到【页面】->【打印区域】->【设置打印区域】 Step02&#xff1a;先选中一行&#xff0c;找到【插入分页符】 Step0…

提高交换网络可靠性之链路聚合

转载请注明出处 该实验为链路聚合的配置实验。 1.改名&#xff0c;分别将交换机1和交换机2改名为S1&#xff0c;S2&#xff0c;然后查看S1&#xff0c;S2的STP信息。以交换机1为例&#x1f447;。 2.交换机S1&#xff0c;S2上创建聚合端口&#xff0c;将端口加入聚合端口。以S…

SpringMVC笔记 一万字

此笔记来自于B站尚硅谷 文章目录 一、SpringMVC 简介1、什么是MVC2、什么是SpringMVC3、SpringMVC的特点 二、HelloWorld1、开发环境2、创建maven工程a>添加web模块b>打包方式&#xff1a;warc>引入依赖 3、配置web.xmla>默认配置方式b>扩展配置方式 4、创建请求…

【Hive sql面试题】找出连续活跃3天及以上的用户

表数据如下&#xff1a; 要求&#xff1a;求出连续活跃三天及以上的用户 建表语句和插入数据如下&#xff1a; create table t_useractive(uid string,dt string );insert into t_useractive values(A,2023-10-01 10:10:20),(A,2023-10-02 10:10:20),(A,2023-10-03 10:16…

livp是什么格式文件?这几款软件可以轻松处理!

今天&#xff0c;我们要探讨的是一种可能相对陌生但又颇具特色的文件格式——LIVP。它通常与某些特定的软件或设备相关联&#xff0c;比如某些品牌的相机或视频编辑软件。LIVP文件往往包含了丰富的图像或视频信息&#xff0c;以及与之相关的元数据&#xff08;如拍摄时间、地点…

贪心算法---java---黑马

贪心算法 1)Greedy algorithm 称之为贪心算法或者贪婪算法&#xff0c;核心思想是 将寻找最优解的问题分为若干个步骤每一步骤都采用贪心原则&#xff0c;选取当前最优解因为未考虑所有可能&#xff0c;局部最优的堆叠不一定得到最终解最优 贪心算法例子 Dijkstra while …

基于vue框架的的留守儿童帮扶管理系统c2691(程序+源码+数据库+调试部署+开发环境)系统界面在最后面。

系统程序文件列表 项目功能&#xff1a;留守儿童,帮扶活动,申请记录,帮扶机构,帮扶进度,帮扶人,申请加入记录,参与帮扶记录,地区信息 开题报告内容 基于Vue框架的留守儿童帮扶管理系统开题报告 一、研究背景与意义 在现代化进程中&#xff0c;随着城乡经济差异的不断扩大&a…

MySQL数据库迁移到DM8数据库

1. 达梦新建zsaqks库 2. 打开DM数据迁移工具 3. 新建工程 4. 迁移 - 右击 - 新建迁移 下一步 5. 选择迁移方式 6. MySQL数据源 请输入MySQL数据库信息 7. DM数据库目的 请输入达梦数据库信息 8. 迁移选项 保持对象名大小写(勾选) 9. 指定模式 指定是从数据源复制对象。 10.…

关于电脑蓝屏的那些解决方案--总有一款适合你

目录 背景内存检测硬盘检测拆机除尘上硅脂查看蓝屏日志--计算机管理1796事件进入bios启用安全启动状态创建转储期间出错失败蓝屏crystaldiskinfo查找BitLocker 恢复密钥关闭cpu-c步骤一&#xff1a;进入BIOS设置步骤二&#xff1a;找到CPU C-state设置步骤三&#xff1a;关闭CP…

HTML 语法规范——代码注释、缩进与格式、标签与属性、字符编码等

文章目录 一、代码注释1.1 使用注释的主要目的1.2 使用建议二、标签的使用2.1 开始标签和结束标签2.2 自闭合标签2.3 标签的嵌套2.4 标签的有效性三、属性四、缩进与格式4.1 一致的缩进4.2 元素单独占用一行4.3 嵌套元素的缩进4.4 避免冗长的行五、字符编码六、小结在开发 HTML…

项目一:使用 Spring + SpringMVC + Mybatis + lombok 实现网络五子棋

一&#xff1a;系统展示: 二&#xff1a;约定前后端接口 2.1 登陆 登陆请求&#xff1a; GET /login HTTP/1.1 Content-Type: application/x-www-form-urlencodedusernamezhangsan&password123登陆响应&#xff1a; 正常对象&#xff1a;正常对象会在数据库中存储&…

从 vue 源码看问题 — vue 初始化都做了什么事?

前言 最近想要对 Vue2 源码进行学习&#xff0c;主要目的就是为了后面在学习 Vue3 源码时&#xff0c;可以有一个更好的对比和理解&#xff0c;所以这个系列暂时不会涉及到 Vue3 的内容&#xff0c;但是 Vue3 的核心模块和 Vue2 是一致的&#xff0c;只是在实现上改变了方式、…

如何在BSV区块链上实现可验证AI

​​发表时间&#xff1a;2024年10月2日 nChain的顶尖专家们已经找到并成功测试了一种方法&#xff1a;通过区块链技术来验证AI&#xff08;人工智能&#xff09;系统的输出结果。这种方法可以确保AI模型既按照规范运行&#xff0c;避免严重错误&#xff0c;遵守诸如公平、透明…

MATLAB——矩阵操作

内容源于b站清风数学建模 数学建模清风老师《MATLAB教程新手入门篇》https://www.bilibili.com/video/BV1dN4y1Q7Kt/ 目录 1.MATLAB中的向量 1.1向量创建方法 1.2向量元素的引用 1.3向量元素修改和删除 2.MATLAB矩阵操作 2.1矩阵创建方法 2.2矩阵元素的引用 2.3矩阵…

SQL基础—2

1.左外连接查询&#xff08;left join on&#xff09; A - A∩B 左外连接查询两张表条件都满足的数据&#xff0c;以及左边表(A表)存在的数据(以左边表为主查询表)。 A - A∩B (A和A交B)。 示例&#xff1a;使用左外连接将dept表作为主查询表&#xff0c;查询员工编号、员工姓…

【Java并发】乐观锁、悲观锁、CAS、版本号机制

前言 在现代计算机系统中&#xff0c;处理并发操作时&#xff0c;锁机制是至关重要的。本文将介绍乐观锁、悲观锁以及CAS&#xff08;Compare and Swap&#xff09;这三种常见的并发控制技术&#xff0c;帮助理解它们的原理和应用场景。 1.悲观锁 1.1 定义 悲观锁是一种在访…

【优选算法】——二分查找!

目录 1、二分查找 2、在排序数组中查找元素的第一个和最后一个位置 3、搜索插入位置 4、x的平方根 5、山脉数组的封顶索引 6、寻找峰值 7、寻找旋转排序数组中的最小值 8、点名 9、完结散花 1、二分查找 给定一个 n 个元素有序的&#xff08;升序&#xff09;整型数组…