Bootstrap

Java多线程学习笔记

JMM内存模型

由CPU内存模型到JMM内存模型

先从CPU内存模型说起:
在这里插入图片描述

cpu缓存分为L1-L3级缓存,因为cpu内存与主内存之间的数据IO相对较慢。

在这里插入图片描述

《并发编程的艺术》P9各种名词,什么缓存命中,写命中都是针对缓存行而言。
P10的优化实际上也是针对缓存行的优化,防止伪共享的发生。

由此引出JMM模型的设计
一个Java程序是一个进程,一个进程内可以有很多线程。
在这里插入图片描述

线程操作的是工作内存中的副本,处理完毕后再刷入主内存中。

JMM真正的原子操作

在这里插入图片描述

可以理解都是与硬件有关的操作

在这里插入图片描述

实际上同步的过程就是需要让线程1去主动寻找主内存的最新结果,再次执行read。

JMM内存模型竞态条件原因

JMM内存模型引发竟态条件的原因;

  1. 线程本地内存与主内存数据不一致。
  2. 处理器的重排序(指令级、内存级):Java单线程遵守的是as if serial语义,在保证最终结果一致的情况下,可以发生指令级、内存级的重排序(核心:同一时间做同一类事务)。多线程下就会发生顺序与代码不一致的现象改变运行结果。
  3. Java的猜测机制:重排序缓冲提前计算好结果,如果真的执行了这一步,就会将提前计算的结果赋值(《并发编程的艺术》P31)

volatile、synchronize、CAS的原子性、可见性、有序性

本节可以先跳过,阅读下文后再读本节。

并发的三个性质:可见性、原子性、有序性
https://blog.csdn.net/u011521203/article/details/80186218
https://juejin.im/post/5bfa580fe51d4566f358b137#heading-9
https://www.cnblogs.com/dolphin0520/p/3920373.html
文章从内存模型概念 -》并发编程的三个性质:原子性、可见性、有序性-》内存模型-》volatile++不具备原子性的实例

volatile的happensbefore依靠缓存锁定 + 嗅探 + MSEI搞定 + 内存屏障搞定
volatile解决了单个volatile变量的可见性(MESI + 嗅探) + 原子性(缓存锁定保证volatile的最新值一定能被其他线程所见,但是无法保证volatile++的原子性) + 写读的有序性=synchronize(内存屏障)。
要在多线程中安全使用volatile变量(原子性),需同时满足:

  1. 对变量的写入操作不依赖其当前值或者能确保只有一个线程修改变量的值: 不满足:number++,count=count*5 满足:boolean变量,记录温度变化的变量等。赋值操作是可以保证原子性的。
    例子:A、B线程同一时刻更新volatile int count = 2;moment1 : A、B同时看到count = 2,A、B在不使用CAS操作的时候同时执行count++;最终更新进去的结果是count = 3。
    所以只有一种办法实现volatile的原子性就是加cas。将三步操作中的第一步操作拿出来当前值,在做set时候比较一下当前值是否失效。
    在这里插入图片描述

  2. 该变量没有包含在具有其他变量的不变式中: 不满足: 不变式 low < up

CAS目前不能保证多线程对共享变量(volatile除外)的可见性,必须配合volatile才能保证可见性
有序性上通过屏障搞定,原子性是ok的,能保证刷回内存,但是不能通知其他线程去主内存中读取最新值所以说可见性有问题。《并发编程的艺术》P54:CAS+volatile的读写可以实现线程间的通信。
https://blog.csdn.net/weixin_43461932/article/details/106911984?utm_medium=distribute.pc_aggpage_search_result.none-task-blog-2aggregatepagefirst_rank_v2~rank_aggregation-1-106911984.pc_agg_rank_aggregation&utm_term=cas+%E5%8F%AF%E8%A7%81%E6%80%A7&spm=1000.2123.3001.4430

并发编程的艺术:P49
synchorize:保证同步语句块内变量可见性和一段代码的原子性(lock与unlock原语),synchronize防止重排序保证了同步语句块之间的有序性
synchronized的两条可见性规定:

  1. 线程解锁前,必须把共享变量的最新值刷新到主内存中
  2. 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值

final的内存语义
final域的内存语义核心:保证final变量在使用时一定是经过正确初始化的,所以防止final在构造过程中的重排序,避免读引用对象以及通过引用对象读final域之间的重排序。这也是通过内存屏障实现的。
但是要小心this溢出的问题

Happens-before规则:P65.其中也有CAS的规则

内存屏障与synchronize、volatile的总结:
总结《并发编程的艺术》3、12章节,介绍了几种屏障类型以及synchronized、volatile实例与屏障是如何应用的
https://www.jianshu.com/p/43af2cc32f90

Volatile + CAS原理理解

总结

实际上无论CAS、Volatile地层都是调用汇编的lock进行实现,CAS与volatile都是Java进行的一种供开发者调用的上层封装。volatile只是封装了lock,cas封装了lock cmpxchgq命令,都是原子命令。这两个汇编会引发两件事情:缓存行锁定/总线锁定(与处理器有关) + 嗅探(缓存行状态遵循MSEI协议),通过二者保证了单个volatile变量的可见性与原子性。
Java本身增强了volatile与CAS的内存语义,volatile的写-读有着synchronize的内存语义,即写之前的变量内容可以保证可见性、有序性与原子性。做法是借助内存屏障,插入了Load save barrier等屏障,或者Ifense等,这也与平台有关。
因此,volatile与CAS本身都不会引起上下文切换,不会引起线程状态的变化。只会引起缓存行锁定或者总线锁定,是比锁代价小得多的并发编程保证。

具体解释

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

针对不同平台内存屏障有不同的实现:
在这里插入图片描述

缓存一致性协议

在这里插入图片描述

CAS + volatile实例:Atomic

Atomic的底层实现:volatile + CAS
incrementAndGet的实现思路
在这里插入图片描述

if的判断解释:

  1. 成功的话直接退出
  2. 失败的话:
    其他线程在进行增加,当前oldValue与newValue都失效,需要重新赋值

CAS的问题

ABA问题是要看业务是否容忍,如果可以容忍可以不解决,如果需要解决的话,使用版本号的思路解决

这个提供了带版本号的实现

CAS + Volatile 实例:并发包的理解

并发包的底层也是Volatile + CAS,与atomic之间的区别在于并发包的各种锁,没有无限while true自旋,而是使用了LockSupport工具,让线程实现休眠。并发包的优势也是在于locksupport等的使用,通过维护两个队列,完成线程的休眠与唤醒,替我们做了非常多线程休眠唤醒的事情。
并发包比Synchronize轻量的原因就在于通过两个队列 + 支持定时唤醒等能力,替我们一定程度上优化了线程之间的竞争。synchronize暴力地让所有线程等待与竞争,并发包则是通过唤醒队列中合适的node,让不满足条件的node迅速判断并且休眠指定时间,减少了线程竞争,也减少了上下文切换的次数

CAS减少线程切换原因

为什么说CAS可以减少线程切换:
一个误区,并发编程肯定就是会涉及到线程的切换的,CAS并不是说就会没有上下文切换,只是CAS本身不会主动引起上下文切换。
当线程进行CAS算法更新的时候,如果发现不是期望的数据,那么他会进入循环进行更新,或者时间片用完后切换线程,状态改为runable状态。而在本次时间片内,存在本线程多次CAS更新后更新成功,从而减少了线程的切换。不会主动让thread.wait等让出时间片,但是会存在时间片耗尽而切换线程退出的情况。但是已经大大减少了上下文切换的次数,比起synchronize而言温和得多。synchronize会暴力的阻塞其他所有的线程,引入其他需要锁的线程直接block,引起大量线程的竞争,如果当前的线程时间片耗尽会带着锁一起退出时间片,进一步也导致其他需要锁的线程都阻塞block着等待争抢锁。
有锁并发在没有竞争到锁的情况下会进行阻塞,造成了线程提前进行线程切换。无锁并发则只有时间片用完后才进行线程切换。
参考: https://blog.csdn.net/qq_24672657/article/details/102731489

关于上下文切换:
https://www.cnblogs.com/sevencutekk/p/11534140.html
在线程状态变化期间都会引起上下文切换,只是CAS大大减少了上下文切换的次数,参见下帖:
https://blog.csdn.net/qq_24672657/article/details/102731489

Java锁的分类

在这里插入图片描述
在这里插入图片描述

Synchronize

1.6 -》 1.8锁的优化

1.6之前的重量锁的架构逻辑:

在这里插入图片描述https://blog.csdn.net/zmh458/article/details/93053867
四种状态详解

轻量级锁的逻辑: 在这里插入图片描述

由上可知,为什么CAS又叫做自旋锁

1.8之后的锁优化:引入了锁膨胀过程,尽可能用轻量级锁去同步
在这里插入图片描述

Synchronize的轻量锁底层就是Java头+CAS:思想就是,认为可以在非常有限的时间内自旋获取到CPU时间片,得到执行

1.6之后的synchronize,重量级锁的架构如上,通过monitor对象进行监控,维持着一个synchronize队列以及一个wait队列
轻量级锁则是CAS实现

这些都写在Java头中

偏向锁:线程ID,只要还是这个线程就没必要升级锁,单线程运行

偏向锁到轻量级:轻度竞争,多个线程都是自旋等待获取到锁。如果线程很多,这些线程全部在自旋,消耗CPU,反而造成性能损失。就放入队列中管理

锁的实际存储结构

锁的实际存储结构:

在这里插入图片描述
在这里插入图片描述

锁的底层

打印对象组成结构的插件:
在这里插入图片描述

打印对象的组成结构以及各个锁:

在这里插入图片描述
在这里插入图片描述
前2行对需要反过来看(第二行右到左,第一行右到左),对应64为虚拟机的一整行
第三行存储的是指向元数据的指针
最后一行是为了对齐8个字节(字节长度能够被8整除),再分配空间
在这里插入图片描述

64位处理器,那么一行也就是内存条里一行也就是8个字节
为了方便寻址,牺牲了一定存储空间,换取寻址的方便,让一个对象占据的空间是从一个内存行行首到行尾。

如果不补充,那么对象就会在内存行里分布非常乱

回到打印锁的程序
userTmp是无状态
Java 4秒后启用偏向锁,所有对象里面的标志位会变成偏向锁的标志位,都会启动偏向锁
user加锁后,偏向锁记录下当前执行同步的线程的ID
释放锁之后,user偏向锁记录是不会释放的,依旧记录的是刚刚执行同步的线程的ID
偏向锁是不会释放线程的,认为下次可能还是这个线程来获取偏向锁

代码第二部分
在这里插入图片描述

开始有新的线程来竞争锁,偏向锁 -》 轻量级锁
在这里插入图片描述

再引入一个新的线程,上一个线程在持有锁期间休眠,这个时候新线程就一直在申请获取新的锁,这个时候锁升级为重量级锁,最后指针指向的位置就是monitor的地址

为什么2个线程就升级为重量级锁?当前只要有新线程加入竞争,先得到轻量级锁,然后开始自旋,自旋10次仍旧为获得锁,那么就升级为重量级锁。
当前jdk仍在不断优化,其中一个优化方向就是轻量级锁-》重量级锁,当前2个线程就升级为重量级锁是有问题的。
实际上的思想就是避免长时间自旋占据CPU时间片。

其他未获取到锁的对象,对象头是如何存储的?

锁的优化实例:分段思想

LongAdder:分段CAS的思想
在这里插入图片描述

其他关于线程的内容

使用线程池的好处

降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行。
提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。可以通过合理设置线程池的几个参数,corepoolsize、maxpoolsize、任务队列这些参数,实现对任务的管理

线程状态解读:

https://blog.csdn.net/xingjing1226/article/details/81977129
在这里插入图片描述

只有runnable的才能去争夺抢占时间片,只有拿到锁的才能进入到runnable状态

只有running时才会占用cpu时间片,其他状态都会出让cpu时间片。
线程的资源有不少,但应该包含CPU资源和锁资源这两类。
sleep(long mills):让出CPU资源,但是不会释放锁资源。
wait():让出CPU资源和锁资源。
https://blog.csdn.net/wm5920/article/details/89067205

在这里插入图片描述

还有两种waiting状态,又可以分为:有时间的waiting与无时间的waiting。都会进入等待队列

Synchronize有自己的锁池队列与等待队列。调用lock.wait()
并发包也有自己的锁池队列与等待队列。调用condition.wait()

不同状态对中断的响应

https://www.cnblogs.com/yangming1996/p/7612653.html
waiting状态会对中断有直接的响应,抛出异常。
其他对于中断完全由业务自己去判断。
大多数抛出InterruptException异常的方法,会在抛出之前将中断标志清除,所以是否要继续传递下去需要业务自己判断。

如何正确的终止线程:volatile + interrupted

安全编程规范,规则5.9

并发编程的挑战

《并发编程实战》1.3节:安全性(是否能够正确同步,保护资源);活跃性(死锁产生);性能问题(良好并发能保证业务吞吐量,而不良好并发导致大量线程阻塞;频繁上下文切换导致CPU资源浪费在线程调度上,浪费在线程退出的保存进入的加载上,导致大量的总线事务,降低CPU的处理性能),对JVM本身优化的阻碍(阻碍了处理器的重排序)等等。
加几个例子。

已使用 Microsoft OneNote 2013 创建。

;