Catalogue
- 1. ① 先说说join和get对比
- 2. JUC并发编程
- 2.1. 一、线程基础知识复习
- 2.2. 二、CompletableFuture
- 2.3. 三、说说Java“锁”事
- 2.4. 四、LockSupport与线程中断
- 2.5. 五、Java内存模型之JMM
- 2.6. 六、volatile与JMM
- 2.7. 七、CAS
- 2.8. 八、原子操作类之18罗汉增强
- 2.9. 九、聊聊ThreadLocal(线程局部变量)
- 3.
① 先说说join和get对比
JUC并发编程
JUC:
java.util.concurrent在并发编程中使用的工具包
一、线程基础知识复习
为什么学习并用好多线程极其重要?
硬件方面
在主频不再提高且cpu核数在不断增加的情况下,要想让程序更快就要用到并行或并发编程。
软件方面
充分利用多核处理器
提高程序性能,高并发系统
提高程序吞吐量,异步+回调等生产需求
弊端及问题
i++、集合类安全否线程锁问题
线程性能问题
1、从start一个线程说起
①Java线程理解以及openjdk中的实现
Java语言本身底层就是C++语言
②更加底层的C++源码解读
java线程是通过start的方法启动执行的,主要内容在native方法start0中;
openjdk的写JNI一般是一一对应的,Thread.java对应的就是Thread.c;
start0其实就是JVM_StartThread。此时查看源代码可以看到在jvm.h中找到了声明,jvm.cpp中有实现。


2、Java多线程相关概念
①1把锁
synchronized会在后面章节详细介绍。
②2个并
并发(concurrent)
是在同一实体上的多个事件
是在一台处理器上“同时”处理多个任务
同一时刻,其实是只有一个事件在发生
并行(parallel)
是在不同实体上的多个事件
是在多台处理器上同时处理多个任务
同一时刻,大家真的都在做事情,你做你的,我做我的,但是我们都在做
并发 vs 并行

③3个程
进程
简单的说,在系统中运行的一个应用程序就是一个进程,每一个进程都有它自己的内存空间和系统资源。
线程
也被称为轻量级进程,在同一个进程内会有一个或多个线程,是大多数操作新系统进行时序调度的基本单元。
管程
Monitor(监视器),也就是我们平时所说的锁
Monitor其实是一种同步机制,他的义务是保证(同一时间)只有一个线程可以访问被保护的数据和代码。
JVM中同步是基于进入和退出监视器(Monitor,管程对象)来实现的,每个对象实例都会有一个Monitor对象。
Monitor对象会和Java对象一同创建并销毁,它底层是由C++来实现的。
1
2
3
4
5Object o = new Object();
new Thread(() -> {
synchronized (o){
}
}, "t2").start();JVM第3版的描述

3、用户线程和守护线程
①Java线程分为用户线程和守护线程
一般情况下不做特别说明配置,默认都是用户线程。
用户线程(User Thread)
是系统的工作线程,它会完成这个程序需要完成的业务操作。
守护线程(Daemon Thread)
是一种特殊的线程为其它线程服务的, 在后台默默地完成一些系统性的服务,比如垃圾回收线程就是最典型的例子。
守护线程作为一个服务线程,没有服务对象就没有必要继续运行了,如果用户线程全部结束了,意味着程序需要完成的业务操作已经结束了,系统可以退出了。所以假如当系统只剩下守护线程的时候,java虚拟机会自动退出。
②线程的daemon属性
true表示是守护线程;false表示是用户线程

③code演示
1 | public class DaemonDemo { |

1 | public class DaemonDemo { |

④总结
如果用户线程全部结束意味着程序需要完成的业务操作已经结束了,守护线程随着JVM一同结束工作
thread.setDaemon(true)方法必须在thread.start()方法之前设置,否则报IllegalThreadStateException异常
二、CompletableFuture
1、Future 接口理论知识
Future接口可以为主线程开一个分支任务,专门为主线程处理耗时和费力的复杂业务。
Future接口定义了操作异步任务执行的一些方法,如获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务执行是否完毕等。它的一个实现类是 FutureTask


比如主线程让一个子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务后,主线程就去做其他事情了,忙其他事情或者先执行完,过了一会才去获取子任务的执行结果或变更的任务状态。
2、Future 接口常用实现类FutureTask异步任务
①Future接口能干什么?
Future是Java5新加的一个接口,它提供了一种异步并行计算的功能。
如果主线程需要执行一个很耗时的计算任务,我们就可以通过future把这个任务放到异步线程中执行。主线程继续处理其他任务或者先行结束,再通过Future获取计算结果。
目的:异步多线程任务执行且返回有结果,三个特点:多线程、有返回、异步任务
比较:
Runnable 接口:无返回值、不抛出异常
Callable接口:有返回值、抛出异常
Future接口和FutureTask实现类:多线程、有返回、异步任务
本源Future接口相关架构
FutureTask本身的构造函数又支持Callable和Runnable


示例代码:
1 | /** |

②Future编码实战和优缺点分析
优点:Future + 线程池异步多线程任务配合,能显著提高程序的执行效率。
缺点:
get()容易导致阻塞:一旦调用get()方法求结果,如果计算没有完成容易导致程序阻塞,所以一般建议放在程序后面。
isDone轮询:轮询的方式会耗费无谓的CPU资源,而且也不见得能及时地得到计算结果。如果想要异步获取结果,通常都会以轮询的方式去获取结果,尽量不要阻塞。
结论:Future对于结果的获取不是很友好,只能通过阻塞或轮询的方式得到任务的结果。
示例代码:
1 | /** |

1 | /** |

3、CompletableFuture 对Futured的改进
①CompletableFuture为什么出现
①get()方法在Future计算完成之前会一直处在阻塞状态下。②isDone()方法容易耗费CPU资源。③对于真正的异步处理我们希望是可以通过传入回调函数,在Future结束时自动调用该回调函数,这样,我们就不用等待结果。
阻塞的方式和异步编程的设计理念相违背,而轮询的方式会耗费无谓的CPU资源。 因此JDK8设计出CompleteFuture。
CompleteFuture提供了一种观察者模式类似的机制,可以让任务执行完成后通知监听的一方。
②CompletableFuture和CompletionStage源码分别介绍
CompletionStage:
代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另外一个阶段,有些类似Linux系统的管道分隔符传参数。
CompletionStage代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另外一个阶段
一个阶段的计算执行可以是一个Function、Consumer或者Runnable。比如:stage.thenApply(x -> square(x)).thenAccept(x -> System.out.print(x)).thenRun(() -> System.out.println())
一个阶段的执行可能是被单个阶段的完成触发,也可能是由多个阶段一起触发
CompletableFuture:

在Java8中,CompletableFuture提供了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,并且提供了函数式编程的能力,可以通过回调的方式处理计算结果,也提供了转换和组合CompletableFuture的方法。
它可能代表一个明确完成的Future,也有可能代表一个完成阶段(CompletionStage),它支持在计算完成以后触发一些函数或执行某些动作。
它实现了Future和CompletionStage接口
③核心的四个静态方法,来创建一个异步任务

核心的四个静态方法:public static CompletableFuture<Void> runAsync(Runnable runnable)、public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)、public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)、public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)
Executor executor参数说明没有指定 Executor 的方法,直接使用默认的 ForkJoinPool.commonPool() 作为它的线程池执行异步代码。
如果指定线程池,则使用我们自定义的或者特别指定的线程池执行异步代码
CompletableFuture的优点
异步任务结束时,会自动回调某个对象的方法;
主线程设置好回调后,不再关心异步任务的执行,异步任务之间可以顺序执行;
异步任务出错时,会自动回调某个对象的方法。
减少阻塞和轮询
从Java8开始引入了CompletableFuture,它是Future的功能增强版,减少阻塞和轮询可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法。
示例代码:
1 | /** |

4、案例精讲-从电商网站的比价需求说开去
① 先说说join和get对比
public T get() throws InterruptedException, ExecutionException
public T join()
get会抛出异常;join不会抛出异常。
②需求分析
需求说明
同上一款产品,同时搜索出同款产品在各大电商平台的售价;
同一款产品,同时搜索出本产品在同一个电商平台下,各个入驻卖家售价是多少
输出返回
出来结果希望是同款产品的在不同地方的价格清单列表,返回一个List
《mysql》 in jd price is 88.05
《mysql》 in dangdang price is 86.11
《mysql》 in taobao price is 90.43
解决方案,比对同一个商品在各个平台上的价格,要求获得一个清单列表,
step by step,按部就班,查完京东查淘宝,查完淘宝查天猫……
多线程异步任务同时查询。
③示例代码
1 | /** |

5、CompletableFuture 常用方法
①获得结果和触发计算
获取结果:
public T get():程序阻塞直至获取返回结果public T get(long timeout, TimeUnit unit):程序阻塞指定时间获取返回结果,若没有获取到则抛出TimeOutExceptionpublic T join():程序阻塞获取返回结果,但不抛出异常public T getNow(T valueIfAbsent)没有计算完成的情况下,给我一个替代结果;
立即获取结果不阻塞:调用时,如果计算完了,返回计算完成后的结果;如果没算完,返回设定的valuefAbsent值
主动触发计算:
public boolean complete(T value):是否打断get方法立即返回括号中的值
示例代码:
1 | /** |

②对计算结果进行处理
thenApply计算结果存在依赖关系,这两个线程串行化
由于存在依赖关系(当前步骤错,不走下一步),当前步骤有异常的话就叫停。
handle计算结果存在依赖关系,这两个线程串行化
有异常也可以往下一步走,根据带的异常参数可以进一步处理
示例代码:
1 | /** |

③对计算结果进行消费
thenAccept: 接收任务的处理结果,并消费处理,无返回结果
对比:
``thenRun(Runnable runnable)`
任务 A 执行完执行 B,并且 B 不需要 A 的结果
thenAccept(Consumer action)任务 A 执行完执行 B,B 需要 A 的结果,但是任务 B 无返回值
thenApply(Function fn)任务 A 执行完执行 B,B 需要 A 的结果,同时任务 B 有返回值
CompletableFuture和线程池说明:
以thenRun和thenRunAsync为例:
没有传入自定义线程池,都用默认线程池ForkJoinPool;
传入了一个自定义线程池
如果你执行第一个任务的时候,传入了一个自定义线程池:
调用thenRun方法执行第二个任务时,则第二个任务和第一个任务是共用同一个线程池。
调用thenRunAsync执行第二个任务时,则第一个任务使用的是你自己传入的线程池,第二个任务使用的是ForkJoin线程池
备注
有可能处理太快,系统优化切换原则,直接使用main线程处理
其它如:thenAccept和thenAcceptAsync,thenApply和thenApplyAsync等,它们之间的区别也是同理
1 | public static void main(String[] args) { |

1 | /** |

④对计算速度进行选用
applyToEither:谁快用谁
示例代码:
1 | /** |

⑤对计算结果进行合并
thenCombine:
两个
CompletionStage任务都完成后,最终能把两个任务的结果一起交给thenCombine来处理先完成的先等着,等待其它分支任务
示例代码:
1 | /** |

三、说说Java“锁”事
1、从轻松的乐观锁和悲观锁开讲
①悲观锁(狼性锁)
认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
synchronized关键字和Lock的实现类都是悲观锁
适合写操作多的场景,先加锁可以保证写操作时数据正确。
显式的锁定之后再操作同步资源。
②乐观锁(佛系锁)
概念:
认为自己在使用数据时不会有别的线程修改数据或资源,所以不会添加锁。
在Java中是通过使用无锁编程来实现,只是在更新数据的时候去判断,之前有没有别的线程更新了这个数据。
如果这个数据没有被更新,当前线程将自己修改的数据成功写入。
如果这个数据已经被其它线程更新,则根据不同的实现方式执行不同的操作,比如放弃修改、重试抢锁等等。
判断规则
版本号机制Version
最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
适合场景:
适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
乐观锁则直接去操作同步资源,是一种无锁算法,得之我幸不得我命。
乐观锁一般有两种实现方式:
采用Version版本号机制
CAS(Compare-and-Swap,即比较并替换)算法实现
2、通过8种情况演示锁运行案例,看看我们到底锁的是什么
①锁相关的8种案例演示code
1 | /** |
②synchronized有三种应用方式
作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁;(对象锁)
作用于代码块,对括号里配置的对象加锁
作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁。(类锁)
③从字节码角度分析synchronized实现
javap -C **.class 文件反编译synchronized同步代码块实现使用的是monitorenter和monitorexit指令
synchronized普通同步方法调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置。
如果设置了,执行线程会将先持有monitor锁,然后再执行方法,
最后在方法完成(无论是正常完成还是非正常完成)时释放monitor
synchronized静态同步方法ACC_STATIC,ACC_SYNCHRONIZED访问标志区分该方法是否静态同步方法。
④反编译synchronized锁的是什么
为什么任何一个对象都可以成为一个锁?
在HotSpot虚拟机中,monitor采用ObjectMonitor实现
每个对象天生都带着一个对象监视器
每一个被锁住的对象都会和Monitor关联起来
3、公平锁和非公平锁
①从ReentrantLock卖票demo演示公平和非公平现象
1 | /** |


②何为公平锁、非公平锁?
公平 锁:
是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买后来的人在队尾排着,这是公平的
Lock lock = new ReentrantLock(true); //true 表示公平锁,先来先得
非公平锁 :
是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转或者饥饿的状态(某个线程一直得不到锁)
Lock lock = new ReentrantLock(false); //false 表示非公平锁,后来的也可能先获得锁
Lock lock = new ReentrantLock(); //默认非公平锁
为什么会有公平锁/非公平锁的设计?为什么默认非公平?
恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。
所以非公平锁能更充分的利用CPU的时间片,尽量减少CPU空闲状态时间。
使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。
4、可重入锁(又名递归锁)
①说明
是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象是同一个对象),不会因为之前已经获取过还没释放而阻塞。
如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。
所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁
②“可重入锁”四个字分开来解释
可:可以。
重:再次
入:进入
锁:同步锁
进入什么:进入同步域(即同步代码块/方法或显式锁锁定的代码)
一句话:一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。自己可以获取自己的内部锁。
③可重入锁种类
隐式锁(即synchronized关键字使用的锁)默认是可重入锁
指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁。这样的锁就叫做可重入锁。
简单的来说就是:在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的
同步块:
1 | /** |

同步方法:
1 | /** |

Synchronized的重入实现机理
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。
显式锁(即Lock)也有ReentrantLock这样的可重入锁
1 | /** |

5、死锁及排查
①说明
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。

产生死锁主要原因:
系统资源不足
进程运行推进的顺序不合适
资源分配不当
②死锁案例
1 | /** |

③如何排查死锁
纯命令:jps -l 、jstack 进程编号

图形化:jconsole



6、写锁(独占锁)/读锁(共享锁)
见第13章
7、自旋锁SpinLock
见第7章
8、无锁→独占锁→读写锁→邮戳锁
见第13章
9、无锁→偏向锁→轻量锁→重量锁
见第11章
10、总结

四、LockSupport与线程中断
1、内容简介
LockSupport

线程中断机制

2、线程中断机制
①什么是中断机制
首先
一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止,自己来决定自己的命运。
所以 Thread.stop、Thread.suspend、Thread.resume都已经被废弃了。
其次
在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。
因此,Java提供了一种用于停止线程的协商机制一一中断,也即中断标识协商机制。
中断只是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。
若要中断一个线程,你需要手动调用该线程的interrupt方法,中该方法也仅仅是将线程对象的中断标识设成true。
接着你需要自己写代码不断地检测当前线程的标识位,如果为true,表示别的线程请求这条线程中断,
此时究竟该做什么需要你自己写代码实现。
每个线程对象中都有一个中断标识位,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断;
通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。
②中断的相关API方法之三大方法说明
| 方法 | 说明 |
|---|---|
public void interrupt() |
实例方法,Just to set the interrupt flag |
实例方法interrupt()仅仅是设置线程的中断状态为true,发起一个协商而不会立刻停止线程 |
|
public static boolean interrupted() |
静态方法,Thread.interrupted(); |
判断线程是否被中断并清除当前中断状态。 |
|
这个方法做了两件事: |
|
1、返回当前线程的中断状态,测试当前线程是否已被中断 |
|
2、将当前线程的中断状态清零并重新设为false,清除线程的中断状态 |
|
此方法有点不好理解,如果连续两次调用此方法,则第二次调用将返回false,因为连续调用两次的结果可能不一样 |
|
public boolean isInterrupted() |
实例方法, |
判断当前线程是否被中断(通过检查中断标志位) |
③相关问题
如何停止中断运行中的线程?
通过一个volatile变量实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33/**
* @Description TODO
* @Author xbb
* @Date 2026/7/23 8:53
*/
package com.atguigu.juc;
import java.util.concurrent.TimeUnit;
public class InterruptDemo {
static volatile boolean isStop = false;
public static void main(String[] args) {
new Thread(()->{
while (true){
if(isStop){
System.out.println(Thread.currentThread().getName() + " 获取到停止命令,程序停止...");
break;
}
System.out.println(Thread.currentThread().getName() + " hello volatile...");
}
},"t1").start();
try { TimeUnit.MICROSECONDS.sleep(1); }catch (Exception e){ e.printStackTrace(); }
new Thread(()->{
isStop = true;
}).start();
}
}
通过AtomicBoolean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34/**
* @Description TODO
* @Author xbb
* @Date 2026/7/23 8:53
*/
package com.atguigu.juc;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class InterruptDemo {
static AtomicBoolean atomicBoolean = new AtomicBoolean(false);
public static void main(String[] args) {
new Thread(()->{
while (true){
if(atomicBoolean.get()){
System.out.println(Thread.currentThread().getName() + " 获取到停止命令,程序停止...");
break;
}
System.out.println(Thread.currentThread().getName() + " hello atomicBoolean...");
}
},"t1").start();
try { TimeUnit.MILLISECONDS.sleep(1); }catch (Exception e){ e.printStackTrace(); }
new Thread(()->{
atomicBoolean.set(true);
}).start();
}
}
通过Thread类自带的中断api实例方法实现
在需要中断的线程中不断监听中断状态,一旦发生中断,就执行相应的中断处理业务逻辑stop线程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34/**
* @Description TODO
* @Author xbb
* @Date 2026/7/23 8:53
*/
package com.atguigu.juc;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class InterruptDemo {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName() + " 获取到停止命令,程序停止...");
break;
}
System.out.println(Thread.currentThread().getName() + " hello isInterrupted api...");
}
}, "t1");
t1.start();
try { TimeUnit.MILLISECONDS.sleep(1); }catch (Exception e){ e.printStackTrace(); }
new Thread(()->{
t1.interrupt();
}).start();
}
}
当前线程的中断标识为true,是不是线程就立刻停止?
实例方法interrupt()仅仅是设置线程的中断状态位设置为true,不会停止线程。
中断只是一种协商机制,修改中断标识位仅此而已,不是立刻stop打断。
静态方法Thread.interrupted(),谈谈你的理解
两者的区别是是否需要清除标识位。

④总结
线程中断相关的方法:
public void interrupt(),interrupt()方法是一个实例方法
它通知目标线程中断,也仅是设置目标线程的中断标志位为true。
public boolean isInterrupted(),isInterrupted()方法也是一个实例方法
它判断当前线程是否被中断(通过检查中断标志位)并获取中断标志
public static boolean interrupted(),Thread类的静态方法interrupted()
返回当前线程的中断状态真实值(boolean类型)后会将当前线程的中断状态设为false,此方法调用之后会清除当前线程的中断标志位的状态(将中断标志置为false了),返回当前值并清零置false
3、LockSupport是什么
用于创建锁和其他同步类的基本线程阻塞原语。
LockSupport中的park()和unpark()的作用分别是阻塞线程和解除阻塞线程。
4、线程等待唤醒机制
①3种让线程等待和唤醒的方法
方式1:使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程
方式2:使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程
方式3:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
②Object类中的wait和notify方法实现线程等待和唤醒
wait和notify方法必须要在同步块或者方法里面,且成对出现使用。
先wait后notify才ok。
1 | /** |

③Condition接口中的await后signal方法实现线程的等待和唤醒
Condition中的线程等待和唤醒方法,需要先获取锁。
一定要先await后signal,不能反了。
1 | /** |

④上述两个对象Object和Condition使用的限制条件
线程先要获得并持有锁,必须在锁块(synchronized或lock)中。
必须要先等待后唤醒,线程才能够被唤醒。
⑤LockSupport类中的park等待和unpark唤醒
阻塞:
park() / park(Object blocker)阻塞当前线程/阻塞传入的具体线程。permit许可证默认没有不能放行,所以一开始调用park()方法时,当前线程就会阻塞,直到别的线程给当前线程的发放permit,park()方法才会被唤醒。
唤醒:
unpark(Thread thread)唤醒处于阻塞状态的指定线程。调用unpark(thread)方法后,就会将thread线程的许可证permit发放,会自动唤醒park线程,即之前阻塞中的LockSupport.park()方法会立即返回。
1 | /** |

五、Java内存模型之JMM
1、计算机硬件存储体系

因为有这么多级的缓存(CPU和物理主内存的速度不一致),CPU的运行并不是直接操作内存而是先把内存里边的数据读到缓存,而内存的读和写操作的时候就会造成不一致的问题。

JVM规范中试图定义一种Java内存模型(Java Memory Model,简称JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。
2、Java内存模型 Java Memory Model
JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在它仅仅描述的是一组约定或规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式并决定一个线程对共享变量的写入何时以及如何变成对另一个线程可见,关键技术点都是围绕多线程的原子性、可见性和有序性展开的。
原则:
JMM的关键技术点都是围绕多线程的原子性、可见性和有序性展开的
能干嘛?
通过JMM来实现线程和主内存之间的抽象关系。
屏蔽各个硬件平台和操作系统的内存访问差异以实现让Java程序在各种平台下都能达到一致的内存访问效果。
3、JMM规范下,三大特性
可见性
是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道该变更,JMM规定了所有的变量都存储在主内存中。

系统主内存共享变量数据修改被写入的时机是不确定的,多线程并发下很可能出现“脏读”,所以每个线程都有自己的 工作内存。线程自己的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在线程自己的工作内存中进行,而不能够直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。


原子性
指一个操作是不可打断的,即多线程环境下,操作不能被其他线程干扰。
有序性
是什么
对于一个线程的执行代码而言,我们总是习惯性认为代码的执行总是从上到下,有序执行。但为了提升性能,编译器和处理器通常会对指令序列进行重新排序。Java规范规定JVM线程内部维持顺序化语义,即只要程序的最终结果与它顺序化执行的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。
优缺点
JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。但是,指令重排可以保证串行语义一致,但没有义务保证多线程间的语义也一致(即可能产生“脏读”),简单说,两行以上不相干的代码在执行的时候有可能先执行的不是第一条,不见得是从上到下顺序执行,执行顺序会被优化。
从源码到最终执行示例图

单线程环境里通常确保程序最终执行结果和代码顺序执行的结果一致。
处理器在进行重排序时必须要考虑指令之间的数据依赖性
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
4、JMM规范下,多线程对变量的读写过程
①读写过程
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到线程自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传递)必须通过主内存来完成,其简要访问过程如下图:

JMM定义了线程和主内存之间的抽象关系:
线程之间的共享变量存储在主内存中(从硬件角度来说就是内存条)
每个线程都有一个私有的本地工作内存,本地工作内存中存储了该线程用来读/写共享变量的副本(从硬件角度来说就是CPU的缓存,比如寄存器、L1、L2、L3缓存等)
②总结
我们定义的所有共享变量都储存在物理主内存中
每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)
线程对共享变量所有的操作都必须先在线程自己的工作内存中进行后写回主内存,不能直接从主内存中读写(不能越级)
不同线程之间也无法直接访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存来进行(同级不能相互访问)
5、JMM规范下,多线程先行发生原则之 happens-before
在JMM中,如果一个操作执行的结果需要对另一个操作可见性
或者 代码重排序,那么这两个操作之间必须存在 happens-before(先行发生)原则。逻辑上的先后关系
①x、y案例说明

②先行发生原则说明
如果Java内存模型中所有的有序性都依赖volatile和synchronized来完成,那么有很多操作都将会变得非常啰嗦, 但是我们在编写Java并发代码的时候并没有察觉到这一点。
我们没有时时、处处、次次,添加volatile和synchronized来完成程序,这是因为Java语言中JMM原则下 有一个“先行发生”(Happens-Before)的原则限制和规矩,给你立好了规矩!
这个原则非常重要: 它是判断数据是否存在竞争、线程是否安全的非常有用的手段。依赖这个原则,我们可以通过几条简单规则一揽子解决并发环境下两个操 作之间是否可能存在冲突的所有问题,而不需要陷入Java内存模型苦涩难懂的底层编译原理之中。
③happens-before总原则
如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不违法。
④happens-before之8条
次序规则
一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作。
锁定规则
一个unLock操作先行发生于后面((这里的“后面”是指时间上的先后))对同一个锁的lock操作;
volatile变量规则
对一个volatile变量的写操作先行发生于后面对这个变量的读操作, 前面的写对后面的读是可见的,这里的“后面”同样是指时间上的先后。
传递规则
如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
线程启动规则(Thread Start Rule)
Thread对象的start()方法先行发生于此线程的每一个动作
线程中断规则(Thread Interruption Rule)
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
可以通过Thread.interrupted()检测到是否发生中断
也就是说你要先调用interrupt()方法设置过中断标志位,我才能检测到中断发送
线程终止规则(Thread Termination Rule)
线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过isAlive()等手段检测线程是否已经终止执行。
对象终结规则(Finalizer Rule)
一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize(方法的开始。
⑤总结
在 Java 语言里面,Happens-Before 的语义本质上是一种 可见性。
A Happens-Before B 意味着 A 发生过的事情对 B 来说是可见的,无论 A 事件和 B 事件是否发生在同一个线程里。
JMM 的设计分为两部分:
一部分是面向我们程序员提供的,也就是 happens-before 规则,它通俗易懂的向我们程序员阐述了一个强内存模型,我们只要理解 happens-before 规则,就可以编写并发安全的程序了。
另一部分是针对 JVM 实现的,为了尽可能少的对编译器处理器做约束从而提高性能,JMM 在不影响程序执行结果的前提下对其不做要求,即允许优化重排序。我们只需要关注前者就好了,也就是理解 happens-before 规则即可,其它繁杂的内容有 JMM 规范结合操作系统给我们搞定,我们只写好代码即可。
六、volatile与JMM
1、被volatile修饰的变量有2大特点
①特点
可见性
有序性
有排序要求,有时需要禁重排。
②volatile的内存语义
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。
当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,重新回到主内存中读取最新共享变量。
所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取。
③volatile凭什么可以保证可见性和有序性?
内存屏障 Memory Barrier
2、内存屏障
①是什么?
内存屏障(也称内存栅栏,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可开始执行此点之后的操作),避免代码重排序。内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性(禁重排),但volatile无法保证原子性。内存屏障之前的所有写操作都要写到主内存,内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)。
写屏障(Store Memory Barrier):告诉处理器在写屏障之前将所有存储在缓存(store buffers)中的数据同步到主内存。也就是说当看到Store屏障指令,就必须把该指令之前所有写入指令执行完毕才能继续往下执行。
读屏障(Load Memory Barrier):处理器在读屏障之后的读操作,都在读屏障之后执行。也就是说在Load屏障指令之后就能够保证后面的读取数据指令一定能够读取到最新的数据。
因此重排序时,不允许把内存屏障之后的指令重排序到内存屏障之前。一句话:对一个volatile变量的写,先行发生于任意后续对这个volatile变量的读,也叫写后读。

②内存屏障分类
第一种分类:
读屏障(Load Barrier)
在读指令之前插入读屏障,让工作内存或CPU高速缓存当中的缓存数据失效,重新回到主内存中获取最新数据。
写屏障(Store Barrier)
在写指令之后插入写屏障,强制把写缓冲区的数据刷回到主内存中。
第二种分类:
| 屏障类型 | 指令示例 | 说明 |
|---|---|---|
| LoadLoad | Load1; LoadLoad; Load2 | 保证load1的读取操作在load2及后续读取操作之前执行 |
| StoreStore | Store1; StoreStore; Store2 | 在store2及其后的写操作执行前,保证store1的写操作已刷新到主内存 |
| LoadStore | Load1; LoadStore; Store2 | 在store2及其后的写操作执行前,保证load1的读操作已读取结束 |
| StoreLoad | Store1; StoreLoad; Load2 | 保证store1的写操作已刷新到主内存之后,load2及其后的读操作才能执行 |
注意:



3、volatile特性
①保证可见性
保证不同线程对某个变量完成操作后结果及时可见,即该共享变量一旦改变所有线程立即可见。
1 | /** |

1 | /** |

②没有原子性
1 | /** |


需要加锁:
1 | /** |

③指令禁重排

4、如何正确使用volatile
单一赋值可以,但是含复合运算赋值不可以(i++之类)
volatile int a = 10;、volatile boolean flage = false;状态标志,判断业务是否结束
开销较低的读,写锁策略

DCL双端锁的发布
七、CAS
1、原子类
java.util.concurrent.atomic (juc这个包下面的所有类)

2、使用CAS前后对比
①没有CAS之前
多线程环境不使用原子类保证线程安全i++(基本数据类型)

②使用CAS之后
多线程环境使用原子类保证线程安全i++(基本数据类型)

3、是什么
①说明
compare and swap的缩写,中文翻译成比较并交换,实现并发算法时常用到的一种技术。它包含三个操作数一一内存位置、预期原值及更新值。执行CAS操作的时候,将内存位置的值与预期原值比较:如果相匹配,那么处理器会自动将该位置值更新为新值,如果不匹配,处理器不做任何操作,多个线程同时执行CAS操作只有一个会成功。
②原理
CAS有3个操作数,位置内存值V,旧的预期值A,要修改的更新值B。当且仅当旧的预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做或重来,当它重来重试的这种行为成为—自旋!!

③代码示例
1 | /** |

4、CAS底层原理
CAS的全称为Compare-And-Swap,它是一条CPU并发原语。
它的功能是判断内存某个位置的值是否为预期值,如果是则更改新的值,这个过程是原子的。
AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
源码截图

CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
总结:
你只需要记住:CAS 是靠硬件实现的从而在硬件层面提升效率,最底层还是交给硬件来保证原子性和可见性
实现方式是基于硬件平台的汇编指令,在 intel 的 CPU 中 (X86 机器上),使用的是汇编指令 cmpxchg 指令。
核心思想就是:比较要更新变量的值 V 和预期值 E(compare),相等才会将 V 的值设为新值 N(swap)如果不相等自旋再来。
5、原子引用
1 | /** |

6、CAS与自旋锁
①是什么
自旋锁(spinlock)
CAS 是实现自旋锁的基础,CAS 利用 CPU 指令保证了操作的原子性,以达到锁的效果,至于自旋呢,看字面意思也很明白,自己旋转。是指尝试获取锁的线程不会立即阻塞,而是采用 循环的方式 去尝试获取锁,当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少线程上下文切换的消耗,缺点是 循环会消耗 CPU。
CAS 是实现自旋锁的基础,自旋翻译成人话就是循环,一般是用一个无限循环实现。这样一来,一个无限循环中,执行一个 CAS 操作,当操作成功返回 true 时,循环结束;当返回 false 时,接着执行循环,继续尝试 CAS 操作,直到返回 true。
②自己实现一个自旋锁SpinLockDemo
1 | /** |

7、CAS缺点
循环时间长开销很大
如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
引出来ABA问题
CAS会导致“ABA问题”。
CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
比如说一个线程1从内存位置V中取出A,这时候另一个线程2也从内存中取出A,并且线程2进行了一些操作将值变成了B,然后线程2又将V位置的数据变成A,这时候线程1进行CAS操作发现内存中仍然是A,预期OK,然后线程1操作成功。
尽管线程1的CAS操作成功,但是不代表这个过程就是没有问题。
解决方案:版本号时间戳原子引用
代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61/**
* @Description TODO
* @Author xbb
* @Date 2026/8/2 10:13
*/
package com.atguigu.juc;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
public class CASDemo {
public static void main(String[] args) {
User z3 = new User("zhangsan",23);
User l4 = new User("lisi",24);
AtomicStampedReference<User> atomicStampedReference = new AtomicStampedReference<>(z3,1);
System.out.println("原始值:\t" + atomicStampedReference.getReference() + "\t 版本号:" + atomicStampedReference.getStamp());
System.out.println(atomicStampedReference.compareAndSet(z3,l4,atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1) + "\t " + atomicStampedReference.getReference().toString() + "\t 版本号:"+atomicStampedReference.getStamp());
System.out.println(atomicStampedReference.compareAndSet(l4,z3,atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1) + "\t " + atomicStampedReference.getReference().toString() + "\t 版本号:"+atomicStampedReference.getStamp());
}
}
class User{
public String name;
public int age;
public User() {
}
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
八、原子操作类之18罗汉增强
1、是什么

2、再分类
①基本类型原子类
类
AtomicInteger、AtomicBoolean、AtomicLong常用API
public final int get() // 获取当前的值
public final int getAndSet(int newValue) // 获取当前的值,并设置新的值
public final int getAndIncrement() // 获取当前的值,并自增
public final int getAndDecrement() // 获取当前的值,并自减
public final int getAndAdd(int delta) // 获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) // 如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44/**
* @Description TODO
* @Author xbb
* @Date 2026/8/2 11:45
*/
package com.atguigu.juc;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicDemo {
static final int size = 10;
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(size);
NumberPlus numberPlus = new NumberPlus();
for (int i = 1; i<= size; i++){
new Thread(()->{
try {
for (int j = 1; j <= 1000; j++){
numberPlus.numberAddOne();
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
countDownLatch.countDown();
}
},String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println("number:"+numberPlus.atomicInteger.get());
}
}
class NumberPlus{
AtomicInteger atomicInteger = new AtomicInteger();
public void numberAddOne(){
atomicInteger.getAndIncrement();
}
}
②数组类型原子类
类
AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray代码示例
1
2
3
4
5
6
7
8
9
10public static void main(String[] args) throws InterruptedException {
AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[]{1,2,3,4,5});
System.out.println(atomicIntegerArray.get(0));
atomicIntegerArray.compareAndSet(0,1,2026);
System.out.println(atomicIntegerArray.get(0));
atomicIntegerArray.getAndIncrement(0);
System.out.println(atomicIntegerArray.get(0));
}
③引用类型原子类
类
AtomicReference、AtomicStampedReference、AtomicMarkableReferenceAtomicStampedReference(观看上章ABA问题的讲解)
AtomicMarkableReference
原子更新带有标记位的引用类型对象
解决是否修改过:它的定义就是将状态戳简化为true|false;类似一次性筷子,只要修改过了,状态位就会置为true。
代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public static void main(String[] args) throws InterruptedException {
AtomicMarkableReference atomicMarkableReference = new AtomicMarkableReference<>(1,false);
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "\t 获取到的默认标志位:"+atomicMarkableReference.isMarked()+"\t 初始值:"+atomicMarkableReference.getReference());
try { TimeUnit.SECONDS.sleep(1); }catch (Exception e){ e.printStackTrace(); }
boolean b = atomicMarkableReference.compareAndSet(1, 1000, atomicMarkableReference.isMarked(), !atomicMarkableReference.isMarked());
System.out.println(Thread.currentThread().getName() + "\t 更新结果:\t "+ b + "\t 更后的值:\t"+atomicMarkableReference.getReference()+"\t 更新后的标志位:\t "+ atomicMarkableReference.isMarked());
},"t1").start();
try { TimeUnit.MILLISECONDS.sleep(5); }catch (Exception e){ e.printStackTrace(); }
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "\t 获取到的默认标志位:"+atomicMarkableReference.isMarked()+"\t 初始值:"+atomicMarkableReference.getReference());
try { TimeUnit.SECONDS.sleep(2); }catch (Exception e){ e.printStackTrace(); }
boolean b = atomicMarkableReference.compareAndSet(1, 2000, atomicMarkableReference.isMarked(), !atomicMarkableReference.isMarked());
System.out.println(Thread.currentThread().getName() + "\t 更新结果:\t "+ b + "\t 更后的值:\t"+atomicMarkableReference.getReference()+"\t 更新后的标志位:\t "+ atomicMarkableReference.isMarked());
},"t2").start();
}
④对象的属性修改原子类
类
AtomicIntegerFieldUpdater:原子更新对象中int类型字段的值AtomicLongFieldUpdater:原子更新对象中Long类型字段的值AtomicReferenceFieldUpdater:原子更新引用类型字段的值
使用目的
以一种线程安全的方式操作非线程安全对象内的某些字段。
使用要求
更新的对象属性必须使用 public volatile 修饰符。
因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 newUpdater() 创建一个更新器,并且需要设置想要更新的类和属性。
代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49/**
* @Description TODO
* @Author xbb
* @Date 2026/8/2 11:45
*/
package com.atguigu.juc;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerArray;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import java.util.concurrent.atomic.AtomicMarkableReference;
public class AtomicDemo {
public static void main(String[] args) throws InterruptedException {
NumberPlus numberPlus = new NumberPlus();
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 1; i<= 10; i++){
new Thread(()->{
try {
for (int j = 1; j <= 1000; j++){
numberPlus.add(numberPlus);
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
countDownLatch.countDown();
}
},String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println("number:"+numberPlus.number);
}
}
class NumberPlus{
public volatile int number = 0;
AtomicIntegerFieldUpdater<NumberPlus> fieldUpdater = AtomicIntegerFieldUpdater.newUpdater(NumberPlus.class,"number");
public void add(NumberPlus numberPlus){
fieldUpdater.getAndIncrement(numberPlus);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50/**
* @Description TODO
* @Author xbb
* @Date 2026/8/2 11:45
*/
package com.atguigu.juc;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class AtomicDemo {
public static void main(String[] args) throws InterruptedException {
NumberPlus numberPlus = new NumberPlus();
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 1; i<= 10; i++){
new Thread(()->{
try {
numberPlus.init(numberPlus);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
countDownLatch.countDown();
}
},String.valueOf(i)).start();
}
countDownLatch.await();
}
}
class NumberPlus{
public volatile Boolean isInit = Boolean.FALSE;
AtomicReferenceFieldUpdater<NumberPlus,Boolean> fieldUpdater = AtomicReferenceFieldUpdater.newUpdater(NumberPlus.class,Boolean.class,"isInit");
public void init(NumberPlus numberPlus){
if(fieldUpdater.compareAndSet(numberPlus,Boolean.FALSE,Boolean.TRUE)){
System.out.println(Thread.currentThread().getName()+"\t 开始初始化操作...");
try { TimeUnit.SECONDS.sleep(2); }catch (Exception e){ e.printStackTrace(); }
System.out.println(Thread.currentThread().getName()+"\t 初始化操作完成");
}else{
System.out.println(Thread.currentThread().getName()+"\t 初始化操作已被其他线程完成...");
}
}
}
⑤原子操作增强类原理深度解析
类
DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdderAPI代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28/**
* @Description TODO
* @Author xbb
* @Date 2026/8/2 11:45
*/
package com.atguigu.juc;
import java.util.concurrent.atomic.LongAccumulator;
import java.util.concurrent.atomic.LongAdder;
public class AtomicDemo {
public static void main(String[] args) throws InterruptedException {
LongAdder longAdder = new LongAdder();
longAdder.increment();
longAdder.increment();
longAdder.increment();
System.out.println(longAdder.sum());
LongAccumulator longAccumulator = new LongAccumulator((x,y)->{
return x + y;
},8);
longAccumulator.accumulate(1);
longAccumulator.accumulate(6);
System.out.println(longAccumulator.get());
}
}
1 |  |

总结
LongAdder的核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点。
AtomicLong
线程安全,可允许一些性能损耗,要求高精度时可使用
保证精度,性能代价
AtomicLong是多个线程针对单个热点值value进行原子操作
LongAdder
当需要在高并发下有较好的性能表现,且对值的精确度要求不高时,可以使用
保证性能,精度代价
LongAdder是每个线程拥有自己的槽,各个线程一般只对自己槽中的那个值进行CAS操作。
九、聊聊ThreadLocal(线程局部变量)
1、ThreadLocal简介
①是什么
ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。 ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。
②能干嘛
实现每一个线程都有自己专属的本地变量副本(自己用自己的变量不麻烦别人,不和其他人共享,人人有份,人各一份)。
主要解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全问题,比如我们之前讲解的8锁案例,资源类是使用同一部手机,多个线程抢夺同一部手机使用,假如人手一份是不是天下太平??
③api介绍

④代码示例
1 | /** |

⑤总结
必须回收自定义的 ThreadLocal 变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代理中使用 try-finally 块进行回收(
house.saleMoney.remove();)。因为每个 Thread 内有自己的实例副本且该副本只由当前线程自己使用。既然其它 Thread 不可访问,那就不存在多线程间共享的问题。统一设置初始值,但是每个线程对这个值的修改都是各自线程互相独立的。
2、ThreadLocal源码分析
①Thread、ThreadLocal、ThreadLocalMap关系
Thread和ThreadLocal

ThreadLocal和ThreadLocalMap

三者的关系

ThreadLocalMap实际上就是一个以ThreadLocal实例为key,任意对象为value的Entry对象。
当我们为ThreadLocal变量赋值,实际上就是以当前ThreadLocal实例为key,值为value的Entry往这个ThreadLocalMap中存放。
②总结
JVM 内部维护了一个线程版的 Map<ThreadLocal, Value>(通过 ThreadLocal 对象的 set 方法,结果把 ThreadLocal 对象自己当做 key,放进了 ThreadLocalMap 中),每个线程要用到这个 T 的时候,用当前的线程去 Map 里面获取,通过这样让每个线程都拥有了自己独立的变量,人手一份,竞争条件被彻底消除,在并发模式下是绝对安全的变量。
注:
同一个线程中可以定义多个 ThreadLocal 变量

每一个线程对应自己的一个ThreadLocalMap。key就是ThreadLocal对象本身,value就是存进去的值。

3、ThreadLocal内存泄漏问题
①引用的整体架构

②四大引用

③为什么源代码用弱引用
当function01方法执行完毕后,栈帧销毁强引用 tl 也就没有了。但此时线程的ThreadLocalMap 里某个entry的key引用还指向这个对象;
若这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏;
若这个key引用是弱引用就大概率会减少内存泄漏的问题;
使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null。

④弱引用造成的问题
当我们为threadLocal变量赋值,实际上就是当前的Entry(threadLocal实例为key,值为value)往这个threadLocalMap中存放。Entry中的key是弱引用,当threadLocal外部强引用被置为null(tl=null),那么系统GC的时候,根据可达性分析,这个threadLocal实例就没有任何一条链路能够引用到它,这个ThreadLocal势必会被回收。这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value。如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreadLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
当然,如果当前thread运行结束,threadLocal,threadLocalMap,Entry没有引用链可达,在垃圾回收的时候都会被系统进行回收。
但在实际使用中我们有时候会用到线程池去维护我们的线程,比如在Executors.newFixedThreadPool()时创建线程的时候,为了复用线程是不会结束的,所以threadLocal内存泄漏就值得我们小心。
所以使用完之后记得手动remove掉不使用的key和value。
4、总结
ThreadLocal 并不解决线程间共享数据的问题
ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景
ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
每个线程持有一个只属于自己的专属Map并维护了ThreadLocal对象与具体实例的映射,该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题
ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题
都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法回收键为null的Entry对象的值(即为具体实例)以及Entry对象本身从而防止内存泄漏,属于安全加固的方法
十、Java对象内存布局和对象头
1、一般而言JDK8按照默认情况下,new一个对象占多少内存空间
①位置所在
JVM堆里 —> 新生区 —> 伊甸园区
②构成布局
看下面的对象在堆内存中布局
2、对象在堆内存中布局
①定义
对象的内存布局:
在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)


②对象在堆内存中的存储布局
对象头
对象标记 Mark Word

默认存储对象的HashCode、分代年龄和锁标志位等信息。这些信息都是与对象自身定义无关的数据,所以MarkWord被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间MarkWord里存储的数据会随着锁标志位的变化而变化.
在64位系统中,Mark Word占了8个字节,类型指针占了8个字节,一共是16个字节。
类元信息(又叫类型指针)
对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
对象头多大
在64位系统中,Mark Word占了8个字节,类型指针占了8个字节,一共是16个字节。
实例数据
存放类的属性(Field)数据信息,包括父类的属性信息。
对齐填充
虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐这部分内存按8字节补充对齐。
3、再说对象头的MarkWord

4、聊聊Object obj = new Object()
Object obj = new Object(),创建后一般是16字节(只有对象头)
GC年龄采用4位bit存储,最大为15.(JVM参数MaxTenuringThreshold参数默认值就是15),超过15就会从新生代到老年代了。
默认开启了压缩指针(
-xx:+UseCompressedClassPointers)。表示开启了类型指针的压缩,以节约空间。
十一、Synchronized与锁升级
1、本章路线总纲
synchronized锁优化的背景:
用锁能够实现数据的安全性,但是会带来性能下降。无锁能够基于线程并行提升程序性能,但是会带来安全性下降。

2、Synchronized的性能变化
在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock(系统互斥量)来实现的。
Java6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁。
3、Synchronized锁种类及升级步骤
锁指向:
偏向锁:MarkWord存储的是偏向的线程ID;
轻量锁:MarkWord存储的是指向线程栈中Lock Record的指针;
重量锁:MarkWord存储的是指向堆中的monitor对象的指针;
无锁:
初始状态,一个对象被实例化后,如果还没有被任何线程竞争锁,那么它就为无锁状态。
偏向锁:
在没有其他线程竞争的时候,一直偏向偏心当前线程,当前线程可以一直执行。
作用:当一段同步代码一直被同一个线程多次访问,由于只有一个线程那么该线程在后续访问时便会自动获得锁。
备注:
偏向锁会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。也即偏向锁在资源没有竞争情况下消除了同步语句,懒的连CAS操作都不做了,直接提高程序性能。
轻量级锁:
多线程竞争,但是任意时刻最多只有一个线程竞争,即不存在锁竞争太过激烈的情况,也就没有线程阻塞。
主要作用:
有线程来参与锁的竞争,但是获取锁的冲突时间极短。本质就是自旋锁CAS。
重量级锁:
有大量的线程参与锁的竞争,冲突性很高。
注意:
锁升级发生后,hashcode去哪了?
在无锁状态下,Mark Word中可以存储对象的identity hash code值。当对象的hashCode()方法第一次被调用时,JVM会生成对应的identity hash code值并将该值存储到Mark Word中。
对于偏向锁,在线程获取偏向锁时,会用Thread ID和epoch值覆盖identity hash code所在的位置。如果一个对象的hashCode()方法已经被调用过一次之后,这个对象不能被设置偏向锁。因为如果可以的,那Mark Word中的identity hash code必然会被偏向线程Id给覆盖。这就会造成同一个对象前后两次调用hashCode()方法得到的结果不一致。
升级为轻量级锁时,JVM会在当前线程的栈帧中创建一个锁记录(Lock Record)空间,用于存储锁对象的Mark Word拷贝,该拷贝中可以包含identity hash code,所以轻量级锁可以和identity hash code共存,哈希码和GC年龄自然保存在此,释放锁后会将这些信息写回到对象头。
升级为重量级锁后,Mark Word保存的重量级锁指针,代表重量级锁的ObjectMonitor类型有字段记录非加锁状态下的Mark Word,锁释放后也会将信息写回到对象头。
4、JIT编译器对锁的优化
JIT:Just In Time Compiler,一般翻译为即时编译器。
锁消除:
从JIT角度看相当于无视它,synchronized(o)不存在了,这个锁对象并没有被共用扩散到其他线程使用,极端的说法就是根本没有加这个锁对象的底层机器码,消除了锁的使用。

锁粗化:
假如方法中首尾相接,前后相邻的都是同一个锁对象,那么JIT编译器就会把这几个synchronized块合并成一个大块,加粗加大范围,一次申请锁使用即可,避免次次的申请和释放锁,提升了性能。

十二、AbstractQueuedSynchronizer之AQS
1、前置知识
公平锁和非公平锁
可重入锁
自旋思想
LockSupport
数据结构之双向链表
设计模式之模板设计模式
2、AQS入门级别理论知识
①是什么
抽象的队列同步器;AbstractQueuedSynchronizer 简称为 AQS
是用来实现锁或者其它同步器组件的公共基础部分的抽象实现,是重量级基础框架及整个JUC体系的基石,主要用于解决锁分配给“谁”的问题
整体就是一个抽象的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量表示持有锁的状态

②AQS为什么是JUC内容中最重要的基石

Java并发大神DougLee,提出统一规范并简化了锁的实现,将其抽象出来屏蔽了同步状态管理、同步队列的管理和维护、阻塞线程排队和通知、唤醒机制等,是一切锁和同步组件实现的—公共基础部分
③能干嘛
加锁会导致阻塞,有阻塞就需要排队,实现排队必然需要队列。
AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作将每条要抢占资源的线程封装成一个Node节点来实现锁的分配通过CAS完成对State值的修改。

④总结
AQS同步队列的基本结构:

十三、ReentrantLock、ReentrantReadWriteLock、StampedLock讲解
1、本章路线总纲
无锁 –> 独占锁 —-> 读写锁 —–> 邮戳锁
2、ReentrantReadWriteLock
①读写锁说明
读写锁定义:一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程。
它只允许读读共享,而读写和写写依然是互斥的,大多实际场景是“读/读”线程间并不存在互斥关系,只有“读/写”线程或“写/写”线程间的操作需要互斥的。因此引入ReentrantReadWriteLock。
一个ReentrantReadWriteLock同时只能存在一个写锁但是可以存在多个读锁,但不能同时存在写锁和读锁。也即一个资源可以被多个读操作访问 或 一个写操作访问,但两者不能同时进行。
只有在读多写少情景之下,读写锁才具有较高的性能体现。
②特点
可重入
读写兼顾
示例代码
结论
一体两面,读写互斥,读读共享,读没有完成的时候其他线程写锁无法获得。

从写锁 —> 读锁,ReentrantReadWriteLock可以锁降级
ReentrantReadWriteLock锁降级:将写入锁降级为读锁(类似Linux文件读写权限理解,就像写权限要高于读权限一样),锁的严苛程度变强叫做升级,反之叫做降级。
特性 说明 公平性选择 支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平 重进入 该锁支持重进入,以读写线程为例:读线程在获取了读锁之后,能够再次获取读锁。而写线程在获取了写锁之后能够再次获取写锁,同时也可以获取读锁 锁降级 遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁 写锁的降级,降级成为了读锁:
- 如果同一个线程持有写了锁,在没有释放写锁的情况下,它还可以继续获得读锁。这就是写锁的降级,降级成为了读锁。
- 规则惯例,先获取写锁,然后获取读锁,再释放写锁的次序。
- 如果释放了写锁,那么就完全转换为读锁。

读写锁之读写规矩,解释为什么要锁降级?
首先获取读锁,如果 cache 不可用,则释放读锁。获取写锁,在更改数据之前,再检查一次 cacheValid 的值,然后修改数据,将 cacheValid 置为 true,然后在释放写锁前立刻抢夺获取读锁;此时,cache 中数据可用,处理 cache 中数据,最后释放读锁。这个过程就是一个完整的锁降级的过程,目的是保证数据可见性。
如果当前的线程C在修改完cache中的数据后,没有获取读锁而是直接释放了写锁,那么假设此时 另一个线程D 获取了写锁并修改了数据,那么C线程无法感知到数据已被修改,则数据出现错误。
线程C在释放写锁之前获取读锁,那么线程D在获取写锁时将被阻塞,直到线程C完成数据处理过程,释放读锁。这样可以保证返回的数据是这次更新的数据,该机制是专门为了缓存设计的。
总结:一句话,同一个线程自己拥有写锁时再去拿读锁,其本质相当于重入。
3、有没有比读写锁更快的锁?
邮戳锁StampedLock

4、邮戳锁StampedLock
①是什么
StampedLock是JDK1.8中新增的一个读写锁,也是对JDK1.5中的读写锁ReentrantReadWriteLock的优化。
邮戳锁 也叫 票据锁
②它是由锁饥饿问题引出
锁饥饿问题:
ReentrantReadWriteLock实现了读写分离,但是一旦读操作比较多的时候,想要获取写锁就变得比较困难了,假如当前1000个线程,999个读,1个写,有可能999个读取线程长时间抢到了锁,那1个写线程就悲剧了
因为当前有可能会一直存在读锁,而无法获得写锁,根本没机会写,o(T__T)o
如何缓解锁饥饿问题?
使用公平策略可以一定程度上缓解这个问题(
new ReentrantReadWriteLock(true)),但是公平策略是以牺牲系统吞吐量作为代价的。StampedLock锁
ReentrantReadWriteLock的读锁被占用的时候,其他线程尝试获取写锁的时候会被阻塞。
但是,StampedLock采取乐观获取锁后,其他线程尝试获取写锁时不会被阻塞,这其实是对读锁的优化。所以,在获取乐观读锁后,还需要对结果进行校验。
③StampedLock的特点
所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为零表示获取失败,其余都表示成功;
所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致;
StampedLock是不可重入的,危险(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)
StampedLock有三种访问模式
①Reading(读模式悲观):功能和ReentrantReadWriteLock的读锁类似
②Writing(写模式):功能和ReentrantReadWriteLock的写锁类似
③Optimistic reading(乐观读模式):无锁机制,类似于数据库中的乐观锁,支持读写并发,很乐观认为读取时没人修改,假如被修改再实现升级为悲观读模式
④代码示例


⑤StampedLock的缺点
StampedLock 不支持重入,没有 Re 开头
StampedLock 的悲观读锁和写锁都不支持条件变量(Condition),这个也需要注意。
使用 StampedLock 一定不要调用中断操作,即不要调用 interrupt() 方法
十四、总结
CompletableFuture
“锁”事件
悲观锁
乐观锁
自旋锁
可重入锁(递归锁)
写锁(独占锁)/读锁(共享锁)
公平锁/非公平锁
死锁
偏向锁
轻量锁
重量锁
邮戳(票据)锁
JMM
synchronized及升级优化
锁的到底是什么
无锁 – 偏向锁 — 轻量锁 — 重量锁
java对象内存布局和对象头
CAS
CAS的底层原理
CAS问题 ABA问题
volatile
2个特性 (可见性、禁重排)
内存屏障
LockSupport和线程中断
- LockSupport.park 和 Object.wait 的区别
AbstractQueuedSynchronizer
是什么
出队入队Node
ThreadLocal
原子增强类Atomic
