写在前面

本篇文章学习自:B站视频竹子爱熊猫-掘金文章

JMM

包含主内存和工作内存

JMM中,主内存属于线程共享区,从某个程度上讲,主存应该包括了堆和方法区。

工作内存则属于线程私有区,从某个程度上讲,应该包括程序计数器、虚拟机栈以及本地方法栈。

其实就是为了保证线程安全,让不同的线程对同一个资源的修改保证线程安全性,具体做法是拷贝工作副本到工作内存,修改完之后再刷新会主内存供其他线程查看最新的资源内容


下方内容取自本站链接-多线程面试题

JMM(Java Memory Model)Java内存模型,是java虚拟机规范中所定义的一种内存模型。

Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。

image-20230504181638237

特点:

  1. 所有的共享变量都存储于主内存(计算机的RAM)这里所说的变量指的是实例变量和类变量。不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。

  2. 每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。

  3. 线程对变量的所有的操作(读,写)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存完成。

🔴🟡🟢总结:

  • JMM(Java Memory Model)Java内存模型,定义了共享内存多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性。
  • JMM把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)
  • 线程跟线程之间是相互隔离,线程跟线程交互需要通过主内存

线程池

详见本站链接

线程状态

image-20240602170410365

volatile

两个作用:

  1. 保证对共享变量的可见性:这个是通过强制刷新缓存实现的
  2. 禁止指令重排序:这个是通过设置内存屏障实现的,即保证了有序性

内存屏障

基本概念

内存屏障: 是一种 屏障指令,它使得 CPU 或编译器 对 屏障指令的前 和后 所发出的内存操作 执行一个排序的约束。 也叫内存栅栏 或 栅栏指令

内存屏障的能力

  1. 阻止屏障两边的指令重排序
  2. 写数据的时候 加了屏障的话,强制把写缓冲区的数据刷回到主内存中
  3. 读数据的时候 加了屏障的话,让工作内存/CPU高速缓存当中缓存的数据失效,重新到主内存中获取新的数据

基本分类

  1. 读屏障: Load Barrier 在读指令之前插入读屏障,让工作内存/CPU 高速缓存当中缓存的数据失效,重新到主内存中获取新的数据
  2. 写屏障: Store Barrier : 在写指令之后插入写屏障,强制把写缓冲区的数据刷回到主内存中

重排序和内存屏障

  1. 重排序可能会给程序带来问题,因此,有些时候,我们希望告诉JVM,这里不需要排序

    JVM 本身为了保证可见性:

  2. 对于编译器的重排序,JMM 会根据重排序的规则,禁止特定类型的编译器重排序

  3. 对于处理器的重排序,Java 编译器在生成指令序列的适当位置,插入内存屏障指令,来禁止特定类型的处理器排序

JMM的内存屏障

LoadLoad Barriers : Loadl; LoadLoad; Load2+
禁止重排序:访问 Load2 的读取操作一定 不会重排到 Lad1之前
保证 Load2 在读取的时候,自己缓存内到相应数据失效,Load2 会去主内存中获取最新的数据

LoadStore Barriers :Load1; LoadStore; Store2+
禁止重排序:一定是 Load1 读取数据完成后,才能让 Store2 及其之后的写出操作的数据,被其它线程看到。

StoreStore Barriers :Store1; StoreStore; Store2
禁止重排序:一定是 Store1的数据写出到主内存完成后,才能让Store2及其之后的写出操作的数据,被其它线程看到。
保证Store1 指令写出去的数据,会强制被刷新回主内存中

StoreLoad Barriers :Storel;StoreLoad; Load2
禁止重排序:一定是 Store1的数据写出到主内存完成后,才能让Load2 来读取数据
同时保证: 强制把写缓冲区的数据刷回到主内存中
让工作内存/CPU 高速缓存当中缓存的数据失效,重新到主内存中获取新的数据

为什么说 StoreLoadBarriers 是最重的?

重: 就是跟内存交互次数多,交互延迟较大、消耗资源较多。

扩展

这些屏障指令并不是处理器真实的执行指令,他们只是JMM 定义出来的跨平台的指令。

因为不同硬件实现内存屏障的方式并不相同,JMM 为了屏蔽这种底层硬件平台的不同,抽象出了这些内存屏障指令,在运行的时候,由JVM 来为不同的平台生成相应的机器码。

这些内存屏障指令,在不同的硬件平台上,可能会做一些优化,从而只支持部分的JMM的内存屏障指令。

在x86 机器上,就只有 StoreLoadBarricrs 是有效的,其它的都不支持,被替换成 nop,也就是空操作。

synchronized

底层由monitor实现

monitor是线程私有的数据结构,包含下面几个关键属性:

  1. _owner:当前持有monitor的线程
  2. _WaitSet:包含了所有在这个 monitor 上调用 wait() 方法后被阻塞的线程。当 notify()notifyAll() 方法被调用时,线程将从 WaitSet 中被唤醒并尝试重新获得 monitor
  3. _EntryList:包含了所有试图进入这个 monitor 的同步块或同步方法但由于 monitor 已经被另一个线程持有而被阻塞的线程。这些线程在 monitor 被释放后将有机会重新竞争该 monitor
  4. _recursions:重入次数:用于跟踪当前持有该 monitor 的线程已经重新进入(递归进入)同步块或同步方法的次数。
  5. _count:用于跟踪 monitor 被持有的次数。每当一个线程获得 monitor 时,计数器增加;每当线程释放 monitor 时,计数器减少。当计数器为零时,表示 monitor 未被任何线程持有。

⭐⭐⭐获取锁的流程:

获取锁的流程

重入锁的流程:不需要再获得锁,只是增加重入次数recursions而已;

重入锁的流程

获取不到锁:就在entryList进行等待

获取不到锁

⭐⭐⭐竞争锁:entrylist的线程和waitset的线程都可以一起竞争锁

重新竞争锁

锁的升级流程

偏向锁升级为轻量级锁: 使用CAS替换markword的线程id

偏向锁升级为轻量级锁

轻量级锁升级为重量级锁:在线程栈的空间去创建Lock Record,然后从Mark word里面把锁的数据复制过来

轻量级锁升级为重量级锁

CAS

比较并交换,是一种乐观锁的概念,

  • V:需要操作的共享变量
  • E:预期值
  • N:新值

如果V值等于E值,则将N值赋值给V。反则如果V值不等于E值,则此时说明在当前线程写回之前有其他线程对V做了更改,那么当前线程什么都不做。

操作系统的原语用于支持原子性

ABA问题

一个线程A在查看的时候,线程B先改了值,线程C又改了回去,导致线程A看到的是一样的值,这就是ABA问题。正常来说只是值变了对普通的业务没多大影响,但是还是需要解决。

解决方式:

  • AtomicStampedReference:时间戳控制,能够完全解决
  • AtomicMarkableReference:维护boolean值控制,不能完全杜绝

UnSafe类

能直接操作指针,内存,不安全但是可塑性强

原子包Automic

这些包下的类都是原子性的操作,比如AutoInteger这些

  • 基本类型原子操作类
  • 引用类型原子操作类
  • 数组类型原子操作类
  • 属性更新原子操作类

ReentrantLock

基于AQS实现的ReetrantLock也是属于悲观锁类型的实现,相比于synchronized,他的区别在于他是显示的,即需要手动上锁,手动释放的,而synchronized是隐式的。

Lock锁提供了很多synchronized锁不具备的特性,如下:

  • ①获取锁中断操作(synchronized关键字是不支持获取锁中断的);
  • ②非阻塞式获取锁机制;
  • ③超时中断获取锁机制;
  • ④多条件等待唤醒机制Condition等。

AQS

Condition

Semaphore

CountDownLatch

ThreadLocal

并发容器

阻塞队列容器

写时复制容器

锁分段容器

ForkJoin

基本概念

有了ThreadPoolExecutor为什么还要有这个ForkJoinPool?

因为在某些情况下ForkJoinPool性能要更好,比如在处理单个超大的任务时,此时如果没有别的任务进来,那么整个系统就等着这一个线程慢慢地处理这个任务,而ForkJoinPool基于分治的思想和工作窃取思想,将一个大任务拆分成多个子任务,多个线程去一起并行执行子任务,最后将子任务的结果收集归并到最终的总的任务的结果集中返回。

工作窃取思想

其实就是已经执行完自己任务的线程,会去帮助那些执行的慢的线程,一起更快地完成任务。

比如某人为了早下班去帮同事干活,这样才能让项目尽早结束。。

执行者线程、任务实体以及线程池