目录

-黑马JUC学习笔记-上

目录

黑马JUC学习笔记-上

1.进程与线程

1.进程与线程的概念

https://i-blog.csdnimg.cn/img_convert/4a2d9e17f0aaaf070b94bb21007f2d0a.png

https://i-blog.csdnimg.cn/img_convert/dfb7c9e2b8efcdd08ed9f25e60433126.png

2.并行和并发

并发

操作系统的任务调度器可以在同一时间应对多件事情

时间片轮转

https://i-blog.csdnimg.cn/img_convert/0ec13b02481abf5413a69113d28a98b6.png

每个线程都有一个时间片 在一个时间片内如果线程未执行完毕会被暂停 然后将下一条将要执行的指令存在程序计数器上(类似于存档)然后切换到下一个线程 继续执行 避免后续的线程等待时间过长

https://i-blog.csdnimg.cn/img_convert/9f5b7f44bd9d80b79494b9e1164e5350.png

并行

https://i-blog.csdnimg.cn/img_convert/6f91a78cc75264ed429d01ae5e06227e.png

https://i-blog.csdnimg.cn/img_convert/c278143711d9b0b70fa21197514f01aa.png

https://i-blog.csdnimg.cn/img_convert/356034c550e028bef3145253f422ac1c.png

https://i-blog.csdnimg.cn/img_convert/bce888b0d4b403ce4affb08215f19725.png

3.线程

1.创建线程

1.普通创建方式

https://i-blog.csdnimg.cn/img_convert/4e81d22be4c7b60e3d9576441f49c7af.png

第一种直接创建线程

第二种先创建线程 然后newthread对象导入任务

2.使用lambda表达式

https://i-blog.csdnimg.cn/img_convert/d11835929618ac4a60640032470c0b7f.png

这里其实使用了匿名内部类 (匿名内部类必须继承一个类 Runnable)

https://i-blog.csdnimg.cn/img_convert/702c85722f004233d9305adbc0575b1d.png

3.使用FutureTask

https://i-blog.csdnimg.cn/img_convert/838b1eff150fb27ae09e8e237802d389.png

继承了Runnable接口

并且多了一个future接口 可以用来返回对象 在线程之间传递结果

https://i-blog.csdnimg.cn/img_convert/ac16ec3774d875e1d11f4fdc2ab7b960.png

callable和runnable类似

callable可以抛出异常

https://i-blog.csdnimg.cn/img_convert/f50b14e484fac084059479d156294584.png

callable和runnable类似 futuretask也实现了runnable接口

因此callable传入futuretask

task传入thread

2.Tread和Runnable的关系

在test2中使用了Thread t=new Thread(Runnable) 传入任务的方式

在跟进源码后得知他们都会调用thread的run方法 在执行run方法前会判断是否传入任务对象

https://i-blog.csdnimg.cn/img_convert/b1fd452acfdb033c3dec3511e1aba0c3.png

https://i-blog.csdnimg.cn/img_convert/4e84596d60cef5c146ba303f08d5501c.png

https://i-blog.csdnimg.cn/img_convert/2db19701b84bc626cd7b75970d461c93.png

3.线程运行原理

1.单线程

https://i-blog.csdnimg.cn/img_convert/e3d90c3bd345fb3793a9808162d8621d.png

1.首先线程运行时先进行类加载 将TestFrams加载到JVM中 放入方法区

2.为main线程分配栈内存 cpu将时间片分配给main

3.JVM创建String对象 放入堆中 在main的栈帧中引用string对象 将变量存储到局部变量表

4.调用methord1方法 为方法分配栈帧 并且将变量存储到局部变量表

5.不断执行方法区中的代码 对变量进行操作

https://i-blog.csdnimg.cn/img_convert/4622d3e6175f712961516f11001efb14.png

2.多线程

栈帧是以线程为单位的 堆中创建的对象才会被线程共享 这样是高并发会应发数据库各种问题的原因

栈帧与线程

在Java中,每个线程都有自己的虚拟机栈(VM Stack),每当一个方法被调用时,就会创建一个新的栈帧(Stack Frame)并将其压入当前线程的栈中。栈帧存储了方法执行期间所需的信息,比如局部变量、操作数栈等。因为这些栈是线程私有的,所以栈帧中的数据默认情况下是不会被其他线程访问到的,这就保证了线程的安全性。

堆中对象的共享

而堆(Heap)是所有线程共享的一块内存区域,用于存放对象实例。如果多个线程需要访问同一个对象,那么就存在数据竞争的可能性,除非有适当的同步机制来保护对这些共享资源的访问。这确实是导致并发问题的一个原因,例如竞态条件(Race Condition)、死锁(Deadlock)等。

3.上下文切换

https://i-blog.csdnimg.cn/img_convert/20dfd1ff8b55da2357b60aef7c5327c2.png

上下文切换是有性能消耗的 这在JVM的学习中也解释过 同一个问题使用多个线程和单线程 有可能单线程更快 因为需要用到程序计数器 上下文切换 性能会有一定的下降

4.常见方法

https://i-blog.csdnimg.cn/img_convert/c67d7a01d2f7d147a143717ee8b7d8de.png

https://i-blog.csdnimg.cn/img_convert/bb47545f3095939c76e321b79362dcbe.png

1.start | run

start启动线程 run是执行的方法

https://i-blog.csdnimg.cn/img_convert/48e2721f04a5c8dd86901df793209099.png

为什么要start 如果不start线程 那么会直接由主线程执行

https://i-blog.csdnimg.cn/img_convert/7203ee7c2cad7c23214387b992f183fd.png

start线程就可以调用新的线程 用以实现异步的目的

https://i-blog.csdnimg.cn/img_convert/7f823f1664d723a642d4db36884414bc.png

如果二次启用就会报异常

https://i-blog.csdnimg.cn/img_convert/69c72b609a9eb5909a5e15476cfcc2a9.png

2.sleep | yield

https://i-blog.csdnimg.cn/img_convert/119d2ca682b5eb4ecd124d55ab7572b3.png

Runnable VS TimedWaiting 就绪状态和阻塞状态对比

就绪状态是主动等待时间片 而阻塞状态是被迫无法接受到时间片

程序优先级

https://i-blog.csdnimg.cn/img_convert/ec4da893cb408734ef8c178ccae745d4.png

https://i-blog.csdnimg.cn/img_convert/fac1e326e5f1a24d2da3cca42a2ae850.png

3.Join

join底层是Wait

等待某个线程运行结束

异步转同步

https://i-blog.csdnimg.cn/img_convert/5a56c9c690d45bafefdda3635ab06d2d.png

限时同步

https://i-blog.csdnimg.cn/img_convert/ea960822889cc3b85b9573ddceb8a632.png

4.Interrupt

https://i-blog.csdnimg.cn/img_convert/0824600baeaec01bab8a51a546851390.png

sleep,wait和join被打断后都会将打断标记清空

https://i-blog.csdnimg.cn/img_convert/6b1393c2af799e854ffb841a50d93bda.png

被打断的线程并不会停止运行 可以通过自己选择是否停止

被吵醒了也可以装睡)

https://i-blog.csdnimg.cn/img_convert/dc49731e9f3f2ba0f0d1688f3a88cb81.png

5.Park

https://i-blog.csdnimg.cn/img_convert/d9d777a2973418c6b69137e419181d8c.png

守护线程

https://i-blog.csdnimg.cn/img_convert/b77357d58cb515a195c941eaec6d038b.png

https://i-blog.csdnimg.cn/img_convert/6197b84f2250b89b2e1913666a96474e.png

5.线程状态

五种状态

操作系统层面

https://i-blog.csdnimg.cn/img_convert/14080042fab779a813b2207102819de9.png

初始状态 : 刚刚创建了线程对象 刚刚new出来的

可运行状态 :也叫就绪状态 standby 时刻准备着接受任务 接受cpu的调度

运行状态 :获取了cpu的时间片 当时间片用完就会从运行态转为可运行态

阻塞状态

如果调用了阻塞API 例如:BIO读写文件 这时候该线程实际不会用到CPU 会导致上下文切换加入阻塞状态 等BIO操作完毕 会由操作系统幻想阻塞线程 转入可运行状态

终止状态 :表示线程已经执行完毕生命周期结束

六种状态

https://i-blog.csdnimg.cn/img_convert/255e51df2fb7db36a7dbaf36ac7d8317.png

https://i-blog.csdnimg.cn/img_convert/9ced9038033aeaf28ab2267b219d20a4.png

异步的思想真的和我以前理解Redis并发问题的时候想的一样原来他叫统筹方法!

其实感觉又和拓扑排序有关系

https://i-blog.csdnimg.cn/img_convert/3c84d13026e83bc5bb624d27bc0c7bf2.png

4.共享模型知管程

1.共享带来的问题

并发安全问题产生的原因 就是缺乏原子性 i++这种代码 也分为好多条指令

本质原因是多个线程访问了共享资源

https://i-blog.csdnimg.cn/img_convert/1739208e2595732f847375b06ce1c64b.png

https://i-blog.csdnimg.cn/img_convert/b4bf165aebc30bda3e20b1330f387d29.png

https://i-blog.csdnimg.cn/img_convert/ffb80d0a69e7d9470068bec039041d3f.png

2.Synchronized

对象锁

https://i-blog.csdnimg.cn/img_convert/00ea6ff668944236521e29e96afe14c7.png

语法

https://i-blog.csdnimg.cn/img_convert/3b69f144d232ca0629afa8b3128c4d6c.png

https://i-blog.csdnimg.cn/img_convert/b846f81b8635252616b32f7da7f181ed.png

操作系统是独立于锁之外的

https://i-blog.csdnimg.cn/img_convert/ca0105a296a8bde6f90c645030561111.png

https://i-blog.csdnimg.cn/img_convert/fcd08fe0d78093fa40d5b5aff19c7e25.png

整体流程:

1.线程2首先获取锁 由于此时没人获取锁 因此线程2获取锁对象成功

2.执行指令集 此时操作系统依然会进行调度 在时间片用完之后进行上下文切换

3.线程1拿到时间片 尝试获取锁对象 但是此时锁对象依然在线程2手中 线程1获取锁失败 进入阻塞状态

然后进行上下文切换

4.线程2由于手中持有锁对象 依然拥有对静态资源的访问权限 进行写入-1操作 执行完成后 释放锁 并且唤醒刚刚加入阻塞状态的线程1 将锁交给线程1

https://i-blog.csdnimg.cn/img_convert/1970a5dcf499fd00d5320506eab92e15.png

q1:强调原子性 但是会消耗性能

q2:如果需要保护一块共享空间 那么所有对这块资源操作的线程都必须只能进入一个房间(持有同一把锁)

q3:不可以 不加锁就根本不会尝试获取锁 那么上下文切换的时候就不会被阻塞 直接修改资源

优化

采用面向对象的思想将需要保护的共享变量放入一个类

https://i-blog.csdnimg.cn/img_convert/b73c5101d1cddab03ae981be18cb0ed8.png

将逻辑封装

https://i-blog.csdnimg.cn/img_convert/ffa9f68886d6d89608e02520f9eb4a11.png

Synchronized加在方法上

加载方法上 相当于锁住了成员方法 但锁的依然是对象

https://i-blog.csdnimg.cn/img_convert/e68f5d564de4f2a058a302715e287083.png

线程八锁

https://i-blog.csdnimg.cn/img_convert/48b6251ac78a6d5bb54138e816318c0e.png

  1. 为什么这样写可以“锁住对象”?

    synchronized 方法本质:

    当你在实例方法(如 a() 和 b())前加 synchronized 时,等同于:

java

复制

public void a() {

synchronized(this) { // 锁的是当前对象实例(n1)

// 方法体

}

}

锁对象就是 this(即调用方法的对象实例 n1)。

两个线程操作同一个对象(n1):

线程1调用 n1.a(),会先获取 n1 的锁。

线程2调用 n1.b(),也需要获取 n1 的锁,但此时锁已被线程1持有,所以线程2必须等待。

最终效果:a() 和 b() 不会同时执行,保证互斥。

  1. this 指的是什么?

    this 是当前对象实例:

    在 a() 和 b() 中,this 都指向 Number 类的实例 n1。

两个同步方法共享同一把锁(n1 对象本身的锁)。

如果创建多个 Number 对象(如 n1、n2),每个对象的锁是独立的,不会互相干扰。

对于普通同步方法,锁的是当前实例对象,对于静态方法,锁的是类的class对象

https://i-blog.csdnimg.cn/img_convert/c81a0b08c1f8703b062bf93164d59c5a.png

a锁的是当前类对象class

b锁的是实例对象thi

n1.a锁了number

n1.b锁了n1

https://i-blog.csdnimg.cn/img_convert/230cf588c8fcae0670e68096bbc867e0.png

首先,静态方法(Static Methods)是属于类本身的,而不是类的某个具体实例。这意味着即使没有创建类的对象,也可以直接通过类名调用静态方法。

首先,静态方法(Static Methods)是属于类本身的,而不是类的某个具体实例。这意味着即使没有创建类的对象,也可以直接通过类名调用静态方法。

静态方法在类加载时就被分配内存,且在整个应用程序生命周期中只存在一份。这对于频繁使用的工具方法来说更加高效,因为它们不需要随着对象的创建和销毁而重复分配内存。

  • 静态方法

    • 不能访问实例变量或实例方法 ,强制开发者分离无关状态的操作。
    • 例如, StringUtils.isEmpty(str) 不依赖任何对象状态,仅处理输入参数。
  • 实例方法

    • 可自由访问实例变量和其他实例方法,支持对象状态的封装和操作。
    • 例如, Car.startEngine() 需要检查燃油量(实例变量)并触发引擎行为。

3.线程安全问题分析

1.成员变量和静态变量是否线程安全

1.如果没有被共享:安全

局部变量会在各自线程虚拟机栈中创建栈帧

https://i-blog.csdnimg.cn/img_convert/fd251038a92eb8c46ec5260f2be1b084.png

2.如果被共享

https://i-blog.csdnimg.cn/img_convert/675ad95fea4206388a0b299a3c954540.png

https://i-blog.csdnimg.cn/img_convert/83eba1aa82315f779f82eaf92a56422d.png

如果只有读操作:安全

如果有读写操作:不安全

2.局部变量是否线程安全

https://i-blog.csdnimg.cn/img_convert/e9d9769ea7000291bf9357627ccbe27f.png

创建子类访问父类中的局部变量资源 儿子坑爹 这也是访问修饰符的存在意义 避免子类覆盖父类的方法

1.局部变量是线程安全的

2.局部变量引用的对象则未必:

如果对象没有逃离方法的作用范围 :安全

如果对象逃离方法的作用范围 :不安全

4.常见线程安全类

https://i-blog.csdnimg.cn/img_convert/58addcac0b61dcbcfe8852fbaa6ea88e.png

https://i-blog.csdnimg.cn/img_convert/933901b62559a55b96d0e35434fff86d.png

https://i-blog.csdnimg.cn/img_convert/0432d890c0716d9090142f90315aca48.png

与前面的例子读写分离时发生的线程安全一致

https://i-blog.csdnimg.cn/img_convert/67bfa5bb3f1a48c86fe553b00a9e8cad.png

5.不可变类线程安全性

https://i-blog.csdnimg.cn/img_convert/25dffc05e2daa6e5213bce347329d00f.png

String:

subString 每次都会创建新的字符串(不会覆盖) 因此不会对原有的字符串进行改变

https://i-blog.csdnimg.cn/img_convert/44298654021fbcf17e155bb4b8e366a4.png

HashMap不是线程安全的 HashTable线程安全

String 是不可变量 线程安全

Date可变 不是线程安全

https://i-blog.csdnimg.cn/img_convert/80da8dbe994eb22e5849f9b44a3604c0.png

解释:虽然start是private的,但是可以通过调用before来实现对start的修改,又因为它是成员变量,不是局部变量,生命周期是从类的创建到消亡,因此在堆内存中只有一份。所以不是线程安全的 这个start是单例的z

总结…无状态(没有成员变量),不可变(成员变量不被修改)就是对应没有共享变量,没有写操作

https://i-blog.csdnimg.cn/img_convert/f7fa5de5d429c13959a109e5c4b59fd4.png

这里的dao只有一份 导致userdao的connectiopn(成员方法只有一份)因此会被共享

6.Synchronized底层

1.Monitor

https://i-blog.csdnimg.cn/img_convert/e50e587cb6a8704b17cf01417ae754ff.png

https://i-blog.csdnimg.cn/img_convert/a8c62841d7bb936f1c76d7b058b9dc47.png

https://i-blog.csdnimg.cn/img_convert/d1b1a10c63536e935788f82206f04e8d.png

当对象获得锁后 会将 markword的标记值更改 markword就是专门记录标记的区域

https://i-blog.csdnimg.cn/img_convert/3416513332d2e5dff005743981f62189.png

https://i-blog.csdnimg.cn/img_convert/8fc04b525402faa6e0dcff511689dec4.png

并且把操作系统中的monitor的值传入markword中记录

由于没有其他人获得锁 因此获取锁成功 成为owner

上锁流程:

1.线程首先尝试获取锁 将obj对象与操作系统monitor相关联

2.使用指针将OBJ对象中的markWord指向monitor

3.然后操作系统的owner就指向Thread2

此时如果thread1 尝试获取obj锁:

1.检查obj是否关联了monitor 锁

2.检查owner是否关联了线程

3.失败 将thread1加入EntryList (阻塞队列)并且进入阻塞状态

当thread2执行完后就会唤醒队列中等待的线程 (此时竞争是非公平的)将owner交给线程

7.轻量级锁

1.上锁流程

https://i-blog.csdnimg.cn/img_convert/210ea464f6dcca18693d1af253acbaab.png

1.在每个线程的栈帧中存储锁记录LockRecord(JVM层面)的信息

ObjectReference记录对象地址

https://i-blog.csdnimg.cn/img_convert/cdb51cde27f51b37254972b380961ddc.png

2.将锁记录的数据 和锁对象markword的数据交换 表示加锁

https://i-blog.csdnimg.cn/img_convert/f8184953ef986716e25bf1171cee9e60.png

3.成功交换如下

https://i-blog.csdnimg.cn/img_convert/fac19825792a58a3290732c4d00a2698.png

4.如果失败分为两种情况:

进入锁膨胀阶段

如果是自己执行加锁 即—锁重入 那么会再添加一条LockRecord作为重入的条数

https://i-blog.csdnimg.cn/img_convert/3fb06192d06d5e2de9d46d6ff52ac666.png

5.解锁过程:

https://i-blog.csdnimg.cn/img_convert/52df7e2fd8efc5b4e929fec9884957f3.png

2.锁膨胀

1.获取锁失败 事态升级! 变为重量级锁

https://i-blog.csdnimg.cn/img_convert/6c04f4a47582264ca1a503fcdbb7bc24.png

2.修改Markword 锁升级 改为monitor锁

并且线程1进入阻塞 但是此时线程1还是轻量级锁 类似商场扶梯没人慢慢走 有人就加速

https://i-blog.csdnimg.cn/img_convert/bcea4f9332f57cfe6aa2311312cffe8c.png

3.解锁流程改变 线程0还原地址的时候失败

进入重量级锁解锁流程

https://i-blog.csdnimg.cn/img_convert/7e0d6a555ec9f8e5c0140a9581e49dbe.png

3.自旋优化

为了避免上下文切换(消耗性能) 那么就选择多等一会 (等不到公交就多等一会 毕竟已经等了一段时间了 实在不行再走回家)

https://i-blog.csdnimg.cn/img_convert/16cf011b40829a630eecb0a5b6066c79.png

https://i-blog.csdnimg.cn/img_convert/125c9dfeacdd764e649117055bc852ae.png

单核CPU同一时间只能运行一个线程。如果有线程在自旋等待锁,而锁被另一个线程持有,但那个线程无法运行,因为CPU被自旋的线程占用了。这样会导致持有锁的线程得不到执行,无法释放锁,自旋的线程就会一直空转,无法继续下去,形成死锁吗?或者说,这种情况会导致系统无法调度,因为自旋的线程占用了CPU,其他线程无法运行,所以锁无法释放,从而自旋的线程永远等不到锁,导致CPU空转,浪费资源。

等了也是拜登!

4.偏向锁

https://i-blog.csdnimg.cn/img_convert/5af34e684791a28dcdc8fd10af647c11.png

为什么要比较 获得是不是自己的锁?

并发安全主要还是因为多个线程操作共享资源

如果是自己线程获得了锁 那就不用竞争锁 自己总不会撞自己的车吧

偏向状态

https://i-blog.csdnimg.cn/img_convert/4e3be23c65c27415fc85d217ee6430d4.png

偏向锁的设计初衷是为了减少无竞争情况下的同步开销。当一个线程访问同步块时,会在对象头和栈帧的锁记录中存储线程ID,之后该线程进入和退出同步块时不需要进行CAS操作,只需检查线程ID是否一致。这样可以减少轻量级锁使用CAS带来的开销。然后是偏向锁与轻量级锁的关系。轻量级锁是通过CAS操作来尝试获取锁,适用于线程交替执行的情况,但如果有竞争,会升级为重量级锁。而偏向锁是在无竞争的情况下,直接偏向第一个访问的线程,省去了后续的同步操作。当出现另一个线程尝试获取锁时,偏向锁会撤销,升级为轻量级锁或重量级锁。因此,偏向锁可以看作是轻量级锁的一个优化,只有在无竞争时才有效,有竞争时则退化为轻量级锁。

如果可偏向的对象 调用了hashcode就会取消偏向锁 因为对象头只能存32位 hashcode占用了偏向锁的位置

为什么重量级锁可以直接调用?

因为重量级锁是存在monitor的 而偏向锁是存在对象头中

一、偏向锁的特点

  1. 适用场景

    单线程重复访问同步代码块:若一个同步代码块始终被同一个线程访问,偏向锁会直接记录该线程ID,后续无需任何同步操作。

无实际竞争:在代码逻辑上存在“同步”,但运行时没有多线程竞争。

  1. 核心机制

    对象头标记:在对象头的 Mark Word 中记录偏向线程ID,以及偏向时间戳(epoch)。

免同步:一旦偏向锁生效,线程进入同步块时无需任何 CAS 操作,只需检查线程ID是否匹配。

延迟启用:JVM 默认在程序启动后几秒(约 4 秒)才启用偏向锁,以避免初始阶段的锁撤销开销。

  1. 优缺点

    优点:无竞争时性能极高(几乎无开销)。

缺点:

锁撤销(Revoke)成本高:当其他线程尝试获取锁时,需撤销偏向锁并升级为轻量级锁。

不适合高竞争场景:频繁竞争会导致反复锁撤销,反而降低性能。

二、轻量级锁的特点

  1. 适用场景

    低竞争的多线程交替执行:线程交替访问同步代码块,但不会同时竞争锁(即不存在锁膨胀为重量级锁的条件)。

  2. 核心机制

    CAS 操作:通过 CAS 操作将对象头的 Mark Word 替换为指向线程栈中锁记录(Lock Record)的指针。

自旋优化:若 CAS 失败,线程会短暂自旋重试(避免直接升级为重量级锁)。

锁记录:在栈帧中存储锁对象的原始 Mark Word 和锁状态。

  1. 优缺点

    优点:低竞争时性能优于重量级锁(无需操作系统介入)。

缺点:

自旋消耗 CPU:若竞争激烈,自旋会导致 CPU 空转。

不适用于长临界区:长时间占用锁会触发膨胀为重量级锁。

三、偏向锁与轻量级锁的关系

  1. 锁升级流程

    无竞争时:对象初始为无锁状态 → 第一次访问时升级为偏向锁。

出现竞争时:

偏向锁 → 轻量级锁:当第二个线程尝试获取偏向锁,JVM 会撤销偏向锁,升级为轻量级锁(通过 CAS 竞争)。

轻量级锁 → 重量级锁:若自旋失败或竞争加剧,轻量级锁膨胀为重量级锁(依赖操作系统互斥量)。

  1. 性能对比

    特性 偏向锁 轻量级锁 重量级锁

    适用场景 单线程重复访问 低竞争的多线程交替访问 高竞争、长临界区

    同步开销 几乎无开销 CAS 自旋 操作系统互斥量(线程阻塞)

    竞争处理 直接升级为轻量级锁 自旋失败后升级为重量级锁 无升级

    典型应用 单例模式、工具类 短暂并发操作(如计数器) 数据库连接池、复杂事务

偏向状态的撤销
1.调用hashcode

https://i-blog.csdnimg.cn/img_convert/7e2c7259c345c46ce6984ddf47d1a770.png

2.其他线程使用对象

https://i-blog.csdnimg.cn/img_convert/f8fb2097086ec88020ac2c6c446d6dba.png

其他的线程加入争抢锁对象 偏向锁升级为轻量级锁

https://i-blog.csdnimg.cn/img_convert/559433226eff14c2dea5c052fe6972e5.png

批量重偏向

偏向锁会加在第一个尝试获取锁的线程对象 但是如果多次有其他线程来尝试获取锁 那么jvm就会认为第一次的偏向是错的 批量的冲偏向到其他线程

https://i-blog.csdnimg.cn/img_convert/8826b7e926ae50bc6ddb764a680bf96f.png

批量撤销

https://i-blog.csdnimg.cn/img_convert/a231a71d1952f3c5d9c353684f14d904.png

5.锁消除

逃逸分析

https://i-blog.csdnimg.cn/img_convert/09ae23268db3326227da0957d192ec8a.png

8.Wait-Notify

https://i-blog.csdnimg.cn/img_convert/6c6b1c49d122a59979c65f46bab40a19.png

https://i-blog.csdnimg.cn/img_convert/ed4fa84b4e88f55460343c52b2c725bb.png

sleep和wait的区别

https://i-blog.csdnimg.cn/img_convert/bf48e76b14badb6cd09b416d212b8a35.png

1.方法继承类不同

2.wait需要获得锁的使用权

3.wait会释放锁

https://i-blog.csdnimg.cn/img_convert/678b913ea1957fae43ea654e1cb7277e.png

9.Park-UnPark

https://i-blog.csdnimg.cn/img_convert/37ea14924726d91d137e676a59578f39.png

https://i-blog.csdnimg.cn/img_convert/d1a84cb3da8de7fc12bab991abc88fe1.png

为什么先执行unpark再park也可以继续执行?

Park和Unpark的原理

https://i-blog.csdnimg.cn/img_convert/a048dd8daa46364693b3fc368ed12971.png

https://i-blog.csdnimg.cn/img_convert/6f379badae917a002ee034b94534cc07.png

先park后unpark:

thread进入parker对象(区域) park就是观察counter(看看有没有干粮)如果parker为1就继续运行没有就进入condition(休息)并且把counter设置为0

https://i-blog.csdnimg.cn/img_convert/2392e297726a7bdfab28e8d5c59bb443.png

unpark后将counter设置为1(补充干粮) 唤醒thread 恢复运行后 将counter设置为0

https://i-blog.csdnimg.cn/img_convert/db63182f28f4d4b0058559e6aa3b5d59.png

10.重新理解线程状态

https://i-blog.csdnimg.cn/img_convert/7c15abfc7dea35fd5394533efcc39d4e.png

https://i-blog.csdnimg.cn/img_convert/f870b0315ac55850576f0c24ce3e3d35.png

https://i-blog.csdnimg.cn/img_convert/caec9c5e1cd6bf869902e4d13f9b414c.png

https://i-blog.csdnimg.cn/img_convert/46a4ea609c7ea2c174fa3ba1c3daf55a.png

11.多把锁

可以采用多把锁 提高并发度

https://i-blog.csdnimg.cn/img_convert/7b1a37f437efacc8f7b41fae8b286232.png

1.线程活跃性

1.死锁

https://i-blog.csdnimg.cn/img_convert/0d17ebf87f945f77d30d6ce054e0e457.png

定位死锁:

使用jps查看pid

使用jstack查看线程状态

https://i-blog.csdnimg.cn/img_convert/f3412141fb5d6ff04c2875c68d05d58e.png

https://i-blog.csdnimg.cn/img_convert/fb38543a2307b22c655f2df6900b9702.png

2.哲学家就餐问题

https://i-blog.csdnimg.cn/img_convert/f2f5deec65b0a5515e4b575dd77ea668.png

3.活锁

https://i-blog.csdnimg.cn/img_convert/6b9352cae0e1965e44cdb5c170f515f1.png

死锁:两个线程互相持有一个锁并且等待对方释放锁 —对峙

活锁:两个线程没有阻塞 由于改变了对方的结束条件 —僵持

可以增加睡眠时间 让线程交错 来避免活锁

https://i-blog.csdnimg.cn/img_convert/78182d18d9a8386b811cf7328d9343f6.png

https://i-blog.csdnimg.cn/img_convert/14b988849f73473cde6c8632e419e136.png

12.ReentranLock

  • Condition
    ReentrantLock 结合使用,可以实现类似 Object 监视器方法(如 wait() , notify() , 和 notifyAll() )的功能。每个 Condition 实例都与一个 ReentrantLock 实例关联,允许线程在满足某些条件前等待,或者在条件满足时通知其他线程。

继承 ReentrantLock 的目的

当你的类继承了 ReentrantLock ,你实际上是让这个类拥有了锁的能力,使得你可以在这个类中方便地使用锁来保护临界区代码,避免并发访问的问题。此外,你可以通过调用 newCondition() 方法来创建一个或多个 Condition 对象,这些对象依赖于该锁进行操作。

但是,继承 ReentrantLock 并不意味着“自动获得锁”或者“condition就自动获得了锁”。实际上,你需要显式地调用 lock.lock() 来获取锁,并在操作完成后调用 lock.unlock() 来释放锁。对于 Condition ,同样需要先获取锁才能调用它的 await()signal() 方法。

https://i-blog.csdnimg.cn/img_convert/df9f8d9dae97a37890c4e0e3ead10f3d.png

https://i-blog.csdnimg.cn/img_convert/f7d8a7bb541a4895d42ccbc950b4e568.png

1.可重入

@Slf4j
public class demo8 {
    private static ReentrantLock reentrantLock = new ReentrantLock();

    public static void main(String[] args) {
        reentrantLock.lock();
        try {
            log.debug("enter main");
            m1();
        }finally {
            reentrantLock.unlock();
        }
    }
    public static void m1(){
        reentrantLock.lock();
        try {
            log.debug("enter m1");
            m2();
        }finally {
            reentrantLock.unlock();
        }
    }public static void m2(){
        reentrantLock.lock();
        try {
            log.debug("enter m2");
        }finally {
            reentrantLock.unlock();
        }
    }
}

2.可打断(被动取消等待)

使用lockInterruptibly()

如果直接使用lock方法是无法被打断的 防止死锁的一种机制

https://i-blog.csdnimg.cn/img_convert/1eb016f0f2e3ac86bc9f8be8b95d3e9a.png

3.锁超时(主动取消等待)

https://i-blog.csdnimg.cn/img_convert/eb275f2512ba9c858a26a8d8841a864c.png

4.公平锁

5.条件变量

https://i-blog.csdnimg.cn/img_convert/929da89a8ad218721bccea0597829cc0.png

@Slf4j
public class demo9 {
    static  final ReentrantLock lock=new ReentrantLock();
    static final Condition condition = lock.newCondition();
    static  boolean t2Runned=false;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            lock.lock();
            try{
                while (!t2Runned){
                    try {
                        condition.await();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                log.debug("1");
            }finally {
                lock.unlock();
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            lock.lock();
            try{
                log.debug("2");
                t2Runned=true;
                condition.signal();
            }finally {
                lock.unlock();
            }

        }, "t2");
        t1.start();
        t2.start();


    }
}

13.小结

在JVM层面实现的Monitor是使用Synchronized 的锁

而在Java层面实现的是ReentranLock

https://i-blog.csdnimg.cn/img_convert/ce909624e0475751c6596efb3d017b36.png

5.共享模型之内存(JMM JAVA内存模型)

https://i-blog.csdnimg.cn/img_convert/1b27ab774fe08783d942cbe4be9e5c09.png

不会停下来

https://i-blog.csdnimg.cn/img_convert/76bdcb0d2fa143397b9423ecdc97044b.png

不走static 直接将run缓存下来 类似中间层

https://i-blog.csdnimg.cn/img_convert/ada5266d0a323ededbf409d09efdee23.png

其实也是数据不一致带来的问题

1.可见性

Volatile

更加轻量级 在解决可见性的问题

易变关键字 修饰成员变量和静态变量 用于避免线程从工作缓存中查找变量的值

必须到主存上查找变量的值

https://i-blog.csdnimg.cn/img_convert/6b3dfd878c2fd1e74bacb7adef538aab.png

volatile static boolean run=true;

public static void main(String[] args) throws InterruptedException {
    new Thread(()->{
        while (run){

        }
    }).start();

    Thread.sleep(1000);
    System.out.println("停止");
    run=true;
}

可见性与原子性

https://i-blog.csdnimg.cn/img_convert/54d3255361a93f35d009fc52b7a3a99b.png

Synchronized既可以保证原子也可以保证可见 但是比较消耗性能

2.Volatile原理

指令重排序的目的主要是提高运行效率

https://i-blog.csdnimg.cn/img_convert/8ee4e2dd79759f3819fae15b006aa534.png

https://i-blog.csdnimg.cn/img_convert/ee935c0ebb1efff24bafe23c48ce3833.png

可以使用Volatile来禁止指令重排序

1.如何保证可见性

通过读屏障和写屏障

写屏障:在写屏障之前的所有对共享变量的改动都会同步到主存之中 防止指令跑到屏障之后

读屏障: 在读屏障之前对共享变量的读取都会访问主存 防止指令跑到屏障之前

https://i-blog.csdnimg.cn/img_convert/5914211bde00d646c5cbadcfabea656e.png

https://i-blog.csdnimg.cn/img_convert/079bf6793f542a8ecd0c0f8430e7c274.png

2.如何保证有序性

volatile不能解决原子性问题 即指令的交错执行

Volatile解决的是单线程的指令重排的问题 但是如果是多线程就难免交错

https://i-blog.csdnimg.cn/img_convert/fff3cb1bfba52360b41792c5b63db16c.png

总结: Volatile是用来解决有序性和可见性的

3.DCL

Double Check Locking

https://i-blog.csdnimg.cn/img_convert/1cdaa5f75e57abcc28a33d1b80e29737.png

这样加锁的方法有什么问题:

性能受限:在每次调用getInstance都会加入同步代码块 但其实只需要在第一次加入代码块的时候保护起来 然后更改instance就可以了

解决:

https://i-blog.csdnimg.cn/img_convert/7891b43996ad3820bfa8117932a94eb6.png

但是这样操作任然是存在问题的:并发问题 指令重排

https://i-blog.csdnimg.cn/img_convert/616e1cc2201c5f78cb3b0695064a66b3.png

1.首先先行的线程率先对instance变量进行赋值

2.而由于在同步代码块外面还有一次对instance的引用 就导致instance的暴露

其他线程获取到了instance

3.由于在率先的线程中已经对instance进行了赋值 就可以直接进行后续的代码执行

变量暴露问题

    • 假设线程 A 执行到 INSTANCE = new Singleton(); 时,由于指令重排,它可能先将未初始化的对象引用赋值给 INSTANCE ,然后才开始初始化对象。
    • 如果此时另一个线程 B 访问 getInstance() 方法,并且刚好跳过了同步块(因为 INSTANCE 已经不为 null ),那么 B 可能会拿到一个尚未完全初始化的对象引用。

真正解决:

采用Volatile 组织指令重排

https://i-blog.csdnimg.cn/img_convert/797e63254ae64cc08abc12fed6840574.png

读屏障(Acquire Barrier)和写屏障(Release Barrier)的设计是为了确保内存操作的顺序性,以维护程序在并发环境下的正确性。理解为什么读屏障要保证指令在屏障之前完成而写屏障确保指令在屏障之后完成,需要从它们各自解决的问题出发。

读屏障(Acquire Barrier)
  • 目的 :确保所有后续的读操作不会看到任何在读屏障之前未完成的操作结果。换句话说,它确保了在此之前的所有读取操作都已完成,并且任何共享变量的更新都能被当前线程看到。
  • 原因 :当一个线程试图获取某个锁或进入临界区时,读屏障确保该线程能看到所有由其他线程释放该锁之前所做的更改。这样可以避免由于乱序执行导致的数据不一致问题。例如,在一个多线程环境中,如果线程A修改了一些共享数据并释放了一个锁,那么随后获得该锁的线程B必须能够看到线程A所做的所有修改。
写屏障(Release Barrier)

目的:确保所有在此之前的写操作都已完成,之后的操作可以看到这些变化。这保证了对于共享资源的所有修改在这个点上都是可见的。

  • 原因 :当一个线程准备释放一个锁或者发布(publish)一个对象到另一个线程时,写屏障确保在此之前的所有写操作都已对外可见。这意味着任何后续的读操作(可能发生在另一个线程中)都能够看到这些修改。例如,如果线程A对共享数据进行了修改,并通过某种方式通知了线程B(比如解锁或发送信号),则需要确保所有这些修改都已经完成并且是可见的,以便线程B能够看到最新的状态。

总结来说,读屏障和写屏障分别用于不同的同步场景,前者关注的是确保读取操作前的所有相关操作都已完成,后者则确保写入操作的结果对后续操作(特别是其他线程的操作)是可见的。这种机制是保障多线程环境下数据一致性和程序正确性的基础。

https://i-blog.csdnimg.cn/img_convert/bec74e9c2be5d5650e51e623fc38e1b5.png

说白了

读屏障保证读取的一定是最新数据

写屏障保证写操作一定全部执行完

这样就解决了上面构造方法未执行完毕就读取的问题

4.Happens-Before

几个对线程可见性的方法

https://i-blog.csdnimg.cn/img_convert/f579438a7252bc023714670a5d998f83.png

https://i-blog.csdnimg.cn/img_convert/e6aa613f0c69b1f9be0a4c3ae035fc6c.png

https://i-blog.csdnimg.cn/img_convert/a0a4210b3023d398d31dbc13fac7ebc7.png

https://i-blog.csdnimg.cn/img_convert/62c3a55b1a4820edddbbc3f0f3b39541.png

小结

https://i-blog.csdnimg.cn/img_convert/193161a9c565a5a8b6b64ec40e7e02b1.png

1.为什么加Final

避免子类覆盖父类方法破坏单例

2.序列化怎么防止破坏单例 (序列化会创建新的对象)

https://i-blog.csdnimg.cn/img_convert/5b65320ace5861d2c69b46afb637b56a.png

返回自己原先的对象

3.为什么需要设置为私有 能否防止反射

不能防止

4.可以 因为这样对象是在类加载时就创建的 没有并发安全

https://i-blog.csdnimg.cn/img_convert/7a447bebbb1926c8d24c7f70c4bda828.png

https://i-blog.csdnimg.cn/img_convert/cd9fb1e3ba8cb4335dbc80e789ccb64a.png

重新认识:

为什么要DOUBLECHECK?

可以自行模拟

1.在多线程情况下 线程1率先判断 发现instance为空 加入Synchronize 获得锁 执行同步代码块

2.线程2在线程1释放锁之前尝试获取锁 失败 进入阻塞队列

3.线程1 释放锁 线程2获得锁 假设没有第二条判断 此时就会又一次的新建一个对象

https://i-blog.csdnimg.cn/img_convert/4e4d66f7b04dc9b6589f72e7c6565bd5.png

6.共享模型之无锁

1.CAS与Volatile

package org.example.JUC;

import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicInteger;

public class demo15 {
    public static void main(String[] args) {
        Account account=new AccountCas(10000);
        Account.demo(account);
    }

}
class AccountCas implements Account{
    private AtomicInteger balance;

    public AccountCas(int balance){
        this.balance=new AtomicInteger(balance);
    }
    @Override
    public Integer getBalance() {
        return balance.get();
    }

    @Override
    public void withDraw(Integer ammount) {
        while (true){
            int pre=balance.get();
            int next=pre-ammount;
            if (balance.compareAndSet(pre,next)){
                break;
            }
        }
    }
}
class AccountSyn implements Account{

    private Integer balance;
    public AccountSyn(Integer balance){
        this.balance=balance;
    }
    @Override
    public Integer getBalance() {
        return this.balance;
    }

    @Override
    public void withDraw(Integer ammount) {
        synchronized (this){
            this.balance-=ammount;
        }
    }
}
interface Account{

    Integer getBalance();
    void withDraw(Integer ammount);
    static void demo(Account account){
        ArrayList<Thread> ts = new ArrayList<>();
        for(int i=0;i<1000;i++){
            ts.add(new Thread(()->{
                account.withDraw(10);
            }));

        }
        long start=System.nanoTime();
        ts.forEach(Thread::start);;
        ts.forEach(t->{
            try {
                t.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        long  end=System.nanoTime();
        System.out.println(account.getBalance()+"        cost:"+(end-start)/1000_000+"ms");
    }
}

https://i-blog.csdnimg.cn/img_convert/ba86affdbb0d11c20b2b9506ec32b5ab.png

因为cas需要数据的实时性因此需要volatile来保证他的数据最新

Volatile

https://i-blog.csdnimg.cn/img_convert/3898e1d1e2ec99289d942599a3ef8d71.png

https://i-blog.csdnimg.cn/img_convert/fd9b78b5199fc4e1a73bd4653a8e0fa7.png

CAS特点

CAS操作执行时,首先检查内存位置V处的值是否等于预期值A。如果是,则将内存位置V更新为新值B,并返回成功;如果不是,则不执行任何操作,并返回失败。整个过程必须是原子的,这意味着一旦一个线程开始执行CAS操作,在它完成之前,不会有其他线程能够修改内存位置V的值。

适用于无锁并发 特别是在线程数少 CPU多的情况下

https://i-blog.csdnimg.cn/img_convert/bcd9804acb01a6f8d27e18dc7ef1a6bd.png

2.原子整数

https://i-blog.csdnimg.cn/img_convert/2bfbce644592387c1d34b8eea5da43ff.png

3.原子引用

ABA问题

Cas无法判断共享变量是否被修改过

无法感知其他线程对变量的操作

https://i-blog.csdnimg.cn/img_convert/a936bdf04a31737f3f53a6cead7bbc93.png

解决:

AtomicStampedReference

版本号法

https://i-blog.csdnimg.cn/img_convert/bd56b3f598f892532b78aa552495b0a9.png

https://i-blog.csdnimg.cn/img_convert/924e550d6100513c1d8b1a7e124ad023.png

AtomicMarkableReference

https://i-blog.csdnimg.cn/img_convert/7c21ad18c70dceccc7120294ef704fdc.png

https://i-blog.csdnimg.cn/img_convert/a080a7ef1f73f7b542db6b66f0fbe6b2.png

原子数组

函数式编程

https://i-blog.csdnimg.cn/img_convert/6d935371801876a4044383a026718e0f.png

https://i-blog.csdnimg.cn/img_convert/b0110af68c2ecef76dc89d55066f4d32.png