当前位置: 首页 > news >正文

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 修改操作,不是原子的

2.4.3.1 锁的概念

2.4.3.2加锁:synchronized

2.4.3.3 synchronized的变种写法

2.4.3.4 可重入

2.4.3.5 监视器锁


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中解决线程安全问题,最主要的方案就是:加锁

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

2.4.3.1 锁的概念

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

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

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

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

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

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

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

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

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


2.4.3.2加锁:synchronized

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

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

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

加锁 lock();

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

解锁 unlock();

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

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

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

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

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

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

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

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

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

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

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

【注意】

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

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

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

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


2.4.3.3 synchronized的变种写法

在方法内加锁:

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

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

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

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

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

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

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

2.4.3.4 可重入

当我们使用锁写代码,一旦方法调用的层次比较深,搞不好就会出现这种情况(或者类似的):

分析上面这段代码:

1.第一次进行加锁(第一层),能够成功(锁没有人使用)

2.第二次进行加锁(第二层),此时的locker已经是被占用的状态,第二次加锁就会触发阻塞等待

要想解除阻塞,就必须往下执行

要想往下执行,就需要等到第一次的锁被释放

这样的问题,就被称为“死锁”(dead lock)

为了解决上述问题,Java的synchronized就引入了可重入的概念

当某个线程针对一个锁加锁成功之后

后续该线程再次针对这个锁进行加锁

不会触发阻塞,而是直接往下走

因为当前这把锁就是被这个线程持有

但是如果是其他线程尝试加锁,就会正常阻塞

也就是说,当我们执行上述代码时,并不会触发死锁,而是正常执行:

可重入锁的实现原理,关键在于让锁对象内部保存,当前是哪个线程持有这把锁

后续有线程针对这个锁加锁的时候,对比一下,持有锁的线程是否和当前要加锁的线程是同一个

当有多层加锁时,最外层的加锁和解锁才是有效的,内部锁直接放行

站在JVM的角度,看到多个 } 需要执行,JVM如何知道哪个 } 是真正需要解锁的那个:

先引入一个变量,计数器(0)

每次触发 { 的时候,把计数器++

每次触发 } 的时候,把计数器--

当计数器 --到0的时候,就是真正需要解锁的时候

2.4.3.5 监视器锁

监视器锁 minitor lock

JVM中采用的一个术语

使用锁的过程中抛出的一些异常,可能会看到 监视器锁 这样的报错信息


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

相关文章:

JavaEE-多线程初阶(3)

目录 1.线程的状态 1.1 NEW、RUNNABLE、TERMINATED 1.2 TIMED_WAITING 1.3 WAITING 1.4 BLOCKED 2.多线程带来的风险-线程安全&#xff08;重点&#xff09; 2.1 观察线程不安全的现象 2.2 分析产生该现象的原因 2.3 产生线程安全问题的原因 2.3.1 抢占式执行&#x…...

从入门到精通:如何在Vue项目中有效运用el-image-viewer

Element UI之el-image-viewer组件详解 引言 在现代 Web 应用中,高质量的用户体验是不可或缺的一环。Element UI 作为一款基于Vue.js 2.0 的桌面端组件库,以其丰富的组件集、良好的文档和支持赢得了广大开发者的好评。本文将深入探讨el-image-viewer组件,这是一个用于在网页…...

uniapp组件实现省市区三级联动选择

1.导入插件 先将uni-data-picker组件导入我们的HBuilder项目中&#xff0c;在DCloud插件市场搜索uni-data-picker 点击下载插件并导入到我们的项目中 2.组件调用 curLocation &#xff1a;获取到的当前位置&#xff08;省市区&#xff09; <uni-data-picker v-slot:defa…...

【C++】异常处理机制(对运行时错误的处理)

&#x1f308; 个人主页&#xff1a;谁在夜里看海. &#x1f525; 个人专栏&#xff1a;《C系列》《Linux系列》 ⛰️ 天高地阔&#xff0c;欲往观之。 目录 引言 1.编译器可以处理的错误 2.编译器不能处理的错误 3.传统的错误处理机制 assert终止程序 返回错误码 一、…...

C++ boost steady_timer使用介绍

文章目录 1. 引入必要的头文件2. 基本用法2.1 同步定时器解释:2.2 异步定时器解释:3. 异步定时器与回调函数4. 设置定时器的超时时间4.1 使用秒、毫秒、微秒4.2 修改定时器的到期时间5. 多次使用定时器6. 循环执行任务7. 错误处理总结:C++ Boost 库提供了 boost::asio::stea…...

JVM 由多个模块组成,每个模块负责特定的功能

Java虚拟机&#xff08;JVM, Java Virtual Machine&#xff09;是一个抽象的计算机&#xff0c;它提供了一个运行环境&#xff0c;使得Java字节码可以在不同的平台上执行。JVM 由多个模块组成&#xff0c;每个模块负责特定的功能。以下是 JVM 的主要模块及其功能&#xff1a; …...

ORACLE批量插入更新如何拆分大事务?

拆分大事务 一、批量插入更新二、拆分事务之前文章MYSQL批量插入更新如何拆分大事务?说明了Mysql如何拆分,本篇文章探讨Oracle或OceanBase批量插入更新拆分大事务的问题 一、批量插入更新 oracle批量插入更新可使用merge语法eg: merge test ausing test_tmp bon (a.id = b.id…...

kafka+zookeeper的搭建

kafka从2.8版本开始&#xff0c;就可以不用配置zookeeper了&#xff0c;但是也可以继续配置。我目前使用的kafka版本是kafka_2.12-3.0.0.tgz&#xff0c;其中前面的2.12表示是使用该版本的scala语言进行编写的&#xff0c;而后面的3.00才是kafka当前的版本。 通过百度网盘分享…...

Spark中的宽窄依赖

一、什么是依赖关系 这里通过一张图来解释&#xff1a; result_rdd是由tuple_rdd使用reduceByKey算子得到的&#xff0c; 而tuple_rdd是由word_rdd使用map算子得到的&#xff0c;word_rdd又是由input_rdd使用flatMap算子得到的。它们之间的关系就称为依赖关系&#xff01; 二…...

安装和运行开发微信小程序

下载HBuilder uniapp官网 uni-app官网 微信开发者工具 安装 微信小程序 微信小程序 官网 微信小程序 配置 运行 注意&#xff1a;运行前需要开启服务端口 如果运行看不到效果&#xff0c;设置下基础库选别的版本 配置...

地图框架之mapbox——(五)

今天主要学习mapbox中如何使用画笔&#xff01; 一、导入画笔依赖 <script src"https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-draw/v1.2.2/mapbox-gl-draw.js"></script> <link rel"stylesheet" href"https://api.mapbox…...

Hive 的数据类型

基本类型 整型 TINYINT: 1字节整数&#xff0c;范围从 -128 到 127。SMALLINT: 2字节整数&#xff0c;范围从 -32,768 到 32,767。INT: 4字节整数&#xff0c;范围从 -2,147,483,648 到 2,147,483,647。BIGINT: 8字节整数&#xff0c;范围从 -9,223,372,036,854,775,808 到 9…...

2024下半年软考考后估分,快来预约!

2024下半年软考这周末就要开考了&#xff01;考后大家最关心的&#xff0c;莫过于考试成绩。届时会为家更新回忆版真题及答案&#xff0c;现在就可以开始预约啦~ 因为是回忆版&#xff0c;老师做题也需要时间&#xff0c;答案会慢慢更新&#xff0c;大家耐心等待片刻&#xff…...

第8章 利用CSS制作导航菜单作业

1.利用CSS技术&#xff0c;结合链接和列表&#xff0c;设计并实现“山水之间”页面。 浏览效果如下&#xff1a; HTML代码如下&#xff1a; <!DOCTYPE html> <html><head><meta charset"utf-8" /><title>山水之间</title><…...

基于Spring Boot的船舶监造系统的设计与实现,LW+源码+讲解

摘要 近年来&#xff0c;信息化管理行业的不断兴起&#xff0c;使得人们的日常生活越来越离不开计算机和互联网技术。首先&#xff0c;根据收集到的用户需求分析&#xff0c;对设计系统有一个初步的认识与了解&#xff0c;确定船舶监造系统的总体功能模块。然后&#xff0c;详…...

linux强制修改mysql的root账号密码

在Linux环境下&#xff0c;如果您忘记了MySQL的root密码&#xff0c;可以通过以下步骤来强制修改root密码&#xff1a; 在执行这些步骤之前&#xff0c;请确保您有足够的权限来执行这些命令。 停止MySQL服务&#xff1a; systemctl stop mysql 启动MySQL的安全模式&#xff0c…...

CentOS系统查看CPU、内存、操作系统等信息

Linux系统提供了一系列命令可以用来查看系统硬件信息&#xff0c;如CPU的物理个数、核数、逻辑CPU数量、内存信息和操作系统版本。 查看物理CPU、核数和逻辑CPU 在多核、多线程的系统中&#xff0c;了解物理CPU个数、每个物理CPU的核数和逻辑CPU个数至关重要。超线程技术进一步…...

针对解决前后端BUG的个人笔记

1-IDEA Q&#xff1a;Required Java version 17 is not supported by SDK 1.8. The maximum supported Java version is 8. A: 我们只知道IDEA页面创建Spring项目&#xff0c;其实是访问spring initializr去创建项目。故我们可以通过阿里云国服去间接创建Spring项目。将https…...

5G时代已来:我们该如何迎接超高速网络?

内容概要 随着5G技术的普及&#xff0c;我们的生活似乎变得更加“科幻”了。想象一下&#xff0c;未来的智能家居将不仅仅是能够听你说“开灯”&#xff1b;它们可能会主动询问你今天心情如何&#xff0c;甚至会推荐你一杯“维他命C芒果榨汁”&#xff0c;帮助你抵御夏天的炎热…...

企业级-实现Redis封装层

作者&#xff1a;fyupeng 技术专栏&#xff1a;☞ https://github.com/fyupeng 项目地址&#xff1a;☞ https://github.com/fyupeng/distributed-blog-system-api 留给读者 封装 Redis 客户端Dao层、分布式锁等。 一、介绍 二、代码 DataInitialLoadRunner.java /*** Clas…...

网络六边形受到攻击

大家读完觉得有帮助记得关注和点赞&#xff01;&#xff01;&#xff01; 抽象 现代智能交通系统 &#xff08;ITS&#xff09; 的一个关键要求是能够以安全、可靠和匿名的方式从互联车辆和移动设备收集地理参考数据。Nexagon 协议建立在 IETF 定位器/ID 分离协议 &#xff08;…...

Ubuntu系统下交叉编译openssl

一、参考资料 OpenSSL&&libcurl库的交叉编译 - hesetone - 博客园 二、准备工作 1. 编译环境 宿主机&#xff1a;Ubuntu 20.04.6 LTSHost&#xff1a;ARM32位交叉编译器&#xff1a;arm-linux-gnueabihf-gcc-11.1.0 2. 设置交叉编译工具链 在交叉编译之前&#x…...

设计模式和设计原则回顾

设计模式和设计原则回顾 23种设计模式是设计原则的完美体现,设计原则设计原则是设计模式的理论基石, 设计模式 在经典的设计模式分类中(如《设计模式:可复用面向对象软件的基础》一书中),总共有23种设计模式,分为三大类: 一、创建型模式(5种) 1. 单例模式(Sing…...

Java 8 Stream API 入门到实践详解

一、告别 for 循环&#xff01; 传统痛点&#xff1a; Java 8 之前&#xff0c;集合操作离不开冗长的 for 循环和匿名类。例如&#xff0c;过滤列表中的偶数&#xff1a; List<Integer> list Arrays.asList(1, 2, 3, 4, 5); List<Integer> evens new ArrayList…...

【入坑系列】TiDB 强制索引在不同库下不生效问题

文章目录 背景SQL 优化情况线上SQL运行情况分析怀疑1:执行计划绑定问题?尝试:SHOW WARNINGS 查看警告探索 TiDB 的 USE_INDEX 写法Hint 不生效问题排查解决参考背景 项目中使用 TiDB 数据库,并对 SQL 进行优化了,添加了强制索引。 UAT 环境已经生效,但 PROD 环境强制索…...

江苏艾立泰跨国资源接力:废料变黄金的绿色供应链革命

在华东塑料包装行业面临限塑令深度调整的背景下&#xff0c;江苏艾立泰以一场跨国资源接力的创新实践&#xff0c;重新定义了绿色供应链的边界。 跨国回收网络&#xff1a;废料变黄金的全球棋局 艾立泰在欧洲、东南亚建立再生塑料回收点&#xff0c;将海外废弃包装箱通过标准…...

【SQL学习笔记1】增删改查+多表连接全解析(内附SQL免费在线练习工具)

可以使用Sqliteviz这个网站免费编写sql语句&#xff0c;它能够让用户直接在浏览器内练习SQL的语法&#xff0c;不需要安装任何软件。 链接如下&#xff1a; sqliteviz 注意&#xff1a; 在转写SQL语法时&#xff0c;关键字之间有一个特定的顺序&#xff0c;这个顺序会影响到…...

NFT模式:数字资产确权与链游经济系统构建

NFT模式&#xff1a;数字资产确权与链游经济系统构建 ——从技术架构到可持续生态的范式革命 一、确权技术革新&#xff1a;构建可信数字资产基石 1. 区块链底层架构的进化 跨链互操作协议&#xff1a;基于LayerZero协议实现以太坊、Solana等公链资产互通&#xff0c;通过零知…...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

今日学习:Spring线程池|并发修改异常|链路丢失|登录续期|VIP过期策略|数值类缓存

文章目录 优雅版线程池ThreadPoolTaskExecutor和ThreadPoolTaskExecutor的装饰器并发修改异常并发修改异常简介实现机制设计原因及意义 使用线程池造成的链路丢失问题线程池导致的链路丢失问题发生原因 常见解决方法更好的解决方法设计精妙之处 登录续期登录续期常见实现方式特…...