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

【JAVA ee初阶】多线程(3)

一、出现线程安全的原因

1.【根本原因】线程的调度执行时随机的(抢占式执行)->罪魁祸首

2.多个线程同时修改同一个变量

如果是一个线程修改一个变量 或者 多个线程读取同一个变量 或者 多个线程修改不同变量 这些都没事。

3.修改操作不是原子的!

像count++ 这样的修改,就是不是原子的修改。

但是像 = 这样的修改,就是属于原子的。(在Java针对内置类型进行 = ,或者针对引用 = 都是原子的)(*但是,在C++中就不一定了,还是不是原子的,就得具体问题具体分析了)

后序判定某个代码是否是线程安全的,需要结合这几点一起来判定。

*如果将全局变量count放入到main方法当中,就会出现报错。

把变量改成局部变量,编译直接过不了了:lambda表达式要想正确捕获变量,要求是final或者事实final(虽然没有加final关键字,但是代码中确实也没人修改)

写成成员变量在lambda中确实能够使用,此时不是走“变量捕获”语法,而是“内部类访问外部类成员”,本身就ok。

lambda本质上就是匿名 内部类(函数式接口)

内部类访问外部类成员,本来就可以实现。

*扩展String 

String是不可变对象,new好一个String对象本身就是不能修改的。设计成不可变对象,其中有一个理由,就是不可变对象天然就是线程安全的。

不可变对象,方便放到常量池中进行缓存。不可变对象,hasCode是固定值,也方便和哈希表进行结合,作为hash的Key

StringBuffer本身确实修改了,但是又通过其他路径(例如枷锁)解决线程安全问题。

StringBuilder是彻底的线程不安全。

*什么是原子的?一个事物是原子的,说明他就是不可拆分的最小单位。SQL中,事务就是把多个SQL打包成一个整体,执行的时候,要么全都执行完,要么一个都不执行。就不会出现执行一半的情况,这就成为原子性。

此处谈到的原子也是类似的含义,CPU执行指令的角度,如果是一条指令,对于CPU来说,要么就是执行完,要么就是不执行,不会出现“一个指令执行一般”这样的情况。CPU执行一条指令,这个行为就是原子的。

像count++这样的指令,对应到多条CPU指令,CPU执行过程中就可能执行一半,就调度走执行别人的指令了(这就不是原子的)

=这样的操作,也是对应到一条CPU指令(类似于MOVE)

那么如何将这些操作变为线程安全的呢?

核心思路:把修改操作变成原子的。

通过锁来实现。

关键字:synchronized通过这个关键字来实现使用锁。

对于锁这样的概念,涉及到两个核心操作:

1.加锁

2.解锁

Java就通过这一个关键字来表示这两种操作,进来就是加锁,出去就是解锁。sychronized()的()里面填写的是“锁对象”,真正用来枷锁的锁是谁?——>在Java中,任何一个对象都可以用来作为锁对象(引用类型,不能是内置类型)

加锁,就是把若干个操作“打包成一个原子”。不是说把这count++的三个指令变成一个指令了。也不是说,这三个指令就必须要一口气在cpu上执行完,不会触发调度。加锁会影响到其他的加锁线程,而且是加同一个锁的线程。

当两个线程,尝试竞争同一把锁,才会产生“阻塞”,如果是竞争不同的锁,就没有影响。

sychronized(锁对象),看锁对象是不是同一个对象。

锁竞争(Lock Contention)是指多个线程试图同时访问同一个临界区(即需要互斥访问的代码区域),因此它们之间产生了竞争。在任意时刻,只能有一个线程持有锁并进入临界区,其他试图进入临界区的线程必须等待。

如果是两个线程,一个加锁了另一个么有加锁,这样就不会产生阻塞。两个线程都加锁了,而且是同一个对象,才会产生锁竞争。

此处的加锁和解锁,也可以视为两个cpu指令。这两个操作使得这两个线程各自循环执行5w次。

整个程序按照如图所示的流程进行:

本来load add save 在两个线程中是穿插执行的,但是在引入锁之后,就变成了“串行执行”,不再穿插,最后输出结果也自然是1w次。

*要是两个锁对象不一样:不一样就不会产生阻塞,程序的执行也就不会出现上述的“禁止插队”这样的行为。

这里又一系列很复杂的逻辑这里也有一系列很复杂的逻辑

日常工作中,一般都是让加锁范围尽量的小

这样的话,可以并发执行的逻辑就更多,此时外部的逻辑通常是更复杂的。

此时这张图中,只有count++是串行的。

引入多线程,就是为了并发执行,就是为了充分利用cpu多核心资源。

多进程编程 和 多线程编程 就是在利用多核心的编程手法。

t1如果加上锁,t1就会不停地执行循环,直到把5w次都执行完,才会去释放锁。

t2只能阻塞等待,一直等到t1释放锁(t1的5w次循环都执行完了),t2才能继续执行

此时,这两个线程的循环时完全串行的,(也就是和一个线程执行是类似的了),这种写法,两个代码是完全串行化。

此时,并没有把多核心利用起来。

这种写法,在当前代码下,执行速度反而更快,主要是因为当前的任务很简单。

*这种情况下,t1和t2谁先拿到锁?

结论没有唯一性,假设t1先拿到锁,当t1循环完毕一次,下一次加锁可能是t1继续加上,也可能是t2加上(这里体现了随机调度)。类似于“数据库隔离级别”,隔离级别越高,并发程度越低,执行速度越慢。

synchronized使用方法

1.synchronized(锁对象){}

基础使用,常见使用。不是禁止调度,而是禁止其他线程插队。

2.修饰一个普通方法,就可以省略锁对象:

*此时,相当于针对this加锁,不是没有锁对象,而是把锁对象给省略掉了

等价于

上面这两种写法,实际上没有任何区别。锁对象,是啥对象不重要,重要的是,两个线程是否是针对同一个对象加锁。

3.synchronized修饰静态方法

此时认为synchronized是针对类对象进行加锁的

static修饰的方法,也叫做“类方法”,不是针对“实例”的方法,而是针对类的。在这个方法里,没有this。

Counter.class——>反射 程序运行时,能够拿到类/对象的一些相关属性。(这个类有哪些成员,都叫啥名字,都是啥类型,都是private/public,有啥方法,都叫啥名,参数列表是啥,是private/public,这个类继承的父类是谁,上实现了哪些interface.......)

通过类对象拿到上述信息。(*不考虑运行,看下代码就行,强调“运行时”的意思就是,不让你看代码,也能够获取到这里的信息)

.java编译生成.class 

.class被jvm加载到内存中,就得到了对应的“类对象”。

synchronized的特性

1.互斥性(前文已经提及)

2.可重入

如果一个线程,针对一把锁,连续加锁两次会发生死锁(deadlock)。(如图所示)

分析:初始情况下,假定locker是未加锁的状态,此时的synchronized就会加锁成功,继续向下执行。

第二次加锁的时候,这个锁已经是“被加锁”的状态了,如果这个锁已经被加锁了,尝试加锁操作,就会触发锁冲突/锁竞争,此时该线程就会阻塞等待,一直等到锁被释放。

走到第二个加锁的位置,触发阻塞,如何解除阻塞?得先释放第一个锁,如何释放第一个锁,得先把第二个锁加上,往下继续走。

BLOCKED状态不是“死锁”,而是因为锁产生的阻塞,这里所说的死锁指的是这个锁再也解不开了。

有的时候,死锁的现象不是特别明显,稍有不慎就会发生死锁现象。

但是,java中引入了可重入机制,有效地避免了上述的死锁情况。(注意,死锁有很多种体现形式,可重入只是能解决一个线程一把锁,加锁两次的情况,解决不了其他情况)同一个线程,针对同一把锁,连续加锁多次,不会触发死锁,此时这个锁就可以称为“可重入锁”。

http://www.xdnf.cn/news/188371.html

相关文章:

  • es+kibana---集群部署
  • MYOJ_1349:(洛谷P3951)[NOIP 2017 提高组] 小凯的疑惑(数学公式套用,两步搞定代码)
  • 快速上手QEMU:创建你的第一个虚拟机实例
  • 深入浅出限流算法(一):简单但有“坑”的固定窗口计数器
  • 大数据应用开发和项目实战
  • 第五章、SpringBoot与消息通信(三)
  • 线性代数——行列式⭐
  • 任意波形发生器——2路同步DA模拟量输出卡
  • 项目管理 - 1.Maven
  • [特殊字符] SpringCloud项目中使用OpenFeign进行微服务远程调用详解(含连接池与日志配置)
  • stm32week13
  • Swiper 在 Vue 中的使用指南
  • 02《小地图实时》Unity
  • 榕壹云信用租赁系统:基于ThinkPHP+MySQL+UniApp的全链路免押租赁解决方案
  • [ACTF2020 新生赛]Include [ACTF2020 新生赛]Exec
  • 基于ffmpeg的音视频编码
  • 电路研究9.3.2——合宙Air780EP中的AT开发指南:HTTP(S)-PDP的研究
  • 【图论 拓扑排序 bfs】P6037 Ryoku 的探索|普及+
  • SpeedyAutoLoot
  • DeepSeek+Dify之五工作流引用API案例
  • 在自动驾驶数据闭环中的特征工程应用
  • VSCode 查看文件的本地修改历史
  • 大模型(LLMs)加速篇
  • Ubuntu 20.04 上安装 最新版CMake 3.31.7 的详细步骤
  • MongoDB的增删改查操作
  • 如何搭建spark yarn模式的集群
  • vite项目tailwindcss4的使用
  • 检查IBM MQ SSL配置是否成功
  • 代码片段存储解决方案ByteStash
  • 每日算法-250428