1 如何创建线程
1) 实现Runnable接口
- 实现run()方法
1new Thread(MyRunnable()).start();
2)继承Thread类
- 重写run()方法
3)Callable接口&&FutureTask
- 实现Callable call()方法,使用FutureTask包装Callable对象,通过Thread启动
1FutrueTask<ReturnType> task = new FutrueTask<>(MyCallable());
2Thread(task).start
3
4ResultType res = task.get(); // 这里阻塞
4)使用线程池
- 通过ExecutorService提交Runnable或者Callable任务
不同方法对比
1Runnable vs Callable
2Callable:可以返回结果,可以抛出异常
3
4线程池的优势:避免重复创建和销毁线程,减少这部分重复带来的开销;
5ThreadPool => (FixedThreadPool, CachedThreadPool, ScheduledThreadPool)
6
7虚拟线程:虚拟线程创建和切换开销更低
8Thread.startVirtualThread()
ThreadPool实践
XXXThreadPool.submit(task…)
1FixedThreadPool: 固定池中线程数量 => 适合1.执行较长任务;2.控制并发度
2
3CachedThreadPool:池中线程数量不固定,根据需要动态创建线程,空的被回收,少了多创建;
4⭐适用于任务执行时间短且任务数量不确定的场景;
5
6ScheduledThreadPool:适用于需要定时执行或周期性执行任务的场景。
1// 定时任务线程池
2ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
3// For 延迟任务
4scheduledThreadPool.schedule(() -> {
5 // 任务逻辑
6}, 10, TimeUnit.SECONDS); // 延迟10秒执行
7
8// For周期执行任务
9scheduledThreadPool.scheduleAtFixedRate(() -> {
10 // 任务逻辑
11}, 0, 1, TimeUnit.SECONDS); // 每隔1秒执行一次
Thread.sleep(0) <= 主动让出CPU控制权
thread.join() <= 等待该线程完成
2 创建线程池
1) Executors工厂类
- Executors.newFixedThreadPool(10) //
2)ThreadPoolExecutor直接创建线程池
1new ThreadPoolExecutor(corePoolSize, maximumPoolSize, KeepAliveTime,TimeUnit,new LinkedBlockingQueue<>())
2
3LinkedBlockingQueue: 当提交的任务多于线程数时,会将多余的暂时挂起
线程池相关参数解释
- corePoolSize:核心线程数,即线程池中始终保存的线程数量;
- maximumPoolSize:最大线程数,线程池中允许的最大线程数量;
- KeepAliveTime:线程空闲时间,非核心线程空闲超过这个时间会被销毁;
- workQueue:工作队列,存放待执行的任务;
- threadFactory:线程工厂,用于创建新线程;
- rejectedExecutionHandler:任务拒绝处理器,当前任务无法执行时的处理策略。
工作队列
- SynchronousQueue:不存储任务,直接提交任务给线程;
- LinkedBlockingQueue:链表结构的阻塞队列,大小无限;
- ArrayBlockingQueue:数据结构的阻塞队列;
- PriorityBlockingQueue:带优先级的无界阻塞队列。/ praɪˈɒrəti /
线程池的拒绝策略
队列满且无空闲线程的添加任务的情况。
- AbortPolicy:抛异常;
- CallerRunsPolicy:由提交线程本身执行;
- DiscardOldestPolicy:删除最早提交的任务;
- DiscardPolicy:直接丢弃当前任务。
3 线程安全的集合
- 加锁
- 内置锁的 线程安全的容器
- 原子类
- 自己独享的资源:局部变量+ThreadLocal
- 无锁CAS
1concurrentHashMap: 同步HashMap<K->V>
2数据结构=> 使用数组 + 链表 + 红黑树的结构;
3线程安全=> CAS保证操作的原子性
4
5eg:线程不安全的操作:
6// 非线程安全的操作
7int value = map.get("key"); // 这里,如果有其他线程对key->value进行了修改!,这里的value是过期了的
8map.put("key", value + 1);
9// ⭐保证原子性
10map.compute("key", (k, v) -> v + 1); // 一次完成
11
12// 如果是方法中创建的Map,每个线程专享,不会有线程间安全问题
13// 如果是类中,且多个线程共享同一个Class实例,则可能出现线程安全问题
14Method中创建:安全
15Class-成员变量:多个线程可能共享同一个map实例,需要保证线程安全
16Class-静态变量:所有线程共享同一个 map 实例 ,需要保证线程安全
17
18BlockingQueue: -数组实现
19LinkedBlockingQueue: 线程安全的阻塞队列-链表实现
4 线程同步
多线程情况下,避免共享资源同时访问,引发数据不一致。=> 加锁,限制访问。
JAVA中,常见的同步方式:
synchronized 同步 // 仅限一个线程访问. 由 JVM 负责管理锁的获取和释放. 非公平锁
生产-消费 模拟
1Main:
2// 共享队列,用于生产者和消费者之间的数据传递
3Queue<Integer> queue = new LinkedList<>(); // or BlockingQueue
4int maxSize = 5; // 队列的最大容量
5
6// 创建生产者和消费者线程
7Thread producer = new Thread(new Producer(queue, maxSize), "Producer");
8Thread consumer = new Thread(new Consumer(queue), "Consumer");
9
10// 启动线程
11producer.start();
12consumer.start();
1Producer: 实现Runnable interface
2@Override
3public void run() {
4 int i = 0;
5 while (true) {
6 synchronized (queue) { // 对队列加锁
7 // 如果队列已满,生产者等待
8 while (queue.size() == maxSize) {
9 try {
10 System.out.println("队列已满,生产者等待...");
11 queue.wait(); // 释放锁,进入等待状态
12 } catch (InterruptedException e) {
13 e.printStackTrace();
14 }
15 }
16
17 // 生产数据并添加到队列
18 System.out.println("生产者生产数据: " + i);
19 queue.add(i++);
20
21 // 通知消费者可以消费了
22 queue.notifyAll();
23
24 // 模拟生产耗时
25 try {
26 Thread.sleep(500);
27 } catch (InterruptedException e) {
28 e.printStackTrace();
29 }
30 }
31 }
32}
1Consumer:实现Runnable接口
2@Override
3public void run() {
4 while (true) {
5 synchronized (queue) { // 对队列加锁
6 // 如果队列为空,消费者等待
7 while (queue.isEmpty()) {
8 try {
9 System.out.println("队列为空,消费者等待...");
10 queue.wait(); // 释放锁,进入等待状态
11 } catch (InterruptedException e) {
12 e.printStackTrace();
13 }
14 }
15
16 // 消费数据
17 int value = queue.poll();
18 System.out.println("消费者消费数据: " + value);
19
20 // 通知生产者可以生产了
21 queue.notifyAll();
22
23 // 模拟消费耗时
24 try {
25 Thread.sleep(1000);
26 } catch (InterruptedException e) {
27 e.printStackTrace();
28 }
29 }
30 }
31}
⭐⭐⭐
synchronized(临界区对象) <= 锁住临界区
while(临界区不可用) => 重试 => .wait() 当前线程进入等待状态让出CPU,并且释放锁
处理完成=> .notifyAll() // 通知等待状态的线程
synchronized:通过 Object.wait() 和 Object.notify()/notifyAll() 进行线程间通信
ReentrantLock 可重入锁 Re en trant lock. 需要手动加锁和释放锁
1lock.lock();
2try {
3 // 临界区代码
4} finally {
5 lock.unlock();
6}
ReentrantLock:使用 Condition 对象更灵活地控制线程等待和唤醒:
1Condition condition = lock.newCondition();
2lock.lock();
3try {
4 condition.await(); // 线程等待
5 condition.signal(); // 唤醒单个等待的线程
6 condition.signalAll(); // 唤醒所有等待的线程
7} finally {
8 lock.unlock();
9}
案例
1// 共享队列,用于生产者和消费者之间的数据传递
2Queue<Integer> queue = new LinkedList<>();
3// 创建 ReentrantLock 和 Condition
4Lock lock = new ReentrantLock();
5Condition notFull = lock.newCondition(); // 队列未满的条件
6Condition notEmpty = lock.newCondition(); // 队列非空的条件
7
8Producer:
9while(true) {
10 lock.lock()
11 try{
12 //
13 while(queue.size == maxSize) {
14 System.out.println("队列已满,生产者等待...");
15 notFull.await(); // 等待队列未满的条件
16 }
17
18 queue.add(?)
19 notEmpty.signalAll()
20
21 // ... 生产耗时
22
23 }finally{
24 lock.unlock()
25 }
26}
27
28Consumer:
29while(true) {
30 lock.lock()
31 try {
32 while(queue.size == 0) {
33 sout("没东西消费")
34 notEmpty.await()
35 }
36
37 queue.poll()
38 notFull.signalAll()
39
40 // ...消费时长
41 }finally{
42 lock.unlock()
43 }
44}
⭐⭐⭐ 关键用法:
Lock lock = new ReentrantLock(); Condition notFull = lock.newCondition(); // 队列未满的条件 Condition notEmpty = lock.newCondition(); // 队列非空的条件
lock.lock && lock.unlock
notFull.await(), notFull.signal(), notFull.signalAll()
5 线程安全
保证多线程,乱序执行时候,无论怎么执行,都可以得到预期结果
=> 通过 线程同步 (悲观锁)
通过Synchronized 和 ReentrantLock ˈsɪŋkrənaɪzd & riːˈentrənt, lɒk 实现线程同步
=> 通过原子操作类
AtomicInteger əˈtɒmɪk 原子, 原子整数
⭐⭐⭐扩展CAS(乐观锁):(Compare And Swap,比较并交换)是一种无锁并发编程技术,常用于实现原子操作。它的基本原理是:先比较,再交换,即只有当变量的当前值等于预期值时,才会更新,否则重试。
1// 当前值, 预期值, 新值
2boolean compareAndSwap(V, E, N) {
3 if (V == E) { // 比较当前值是否等于预期值
4 V = N; // 如果相等,更新为新值
5 return true; // 操作成功
6 }
7 return false; // 操作失败
8}
例子:最佳实践案例=> 线程安全的计数器
1private AtomicInteger value = new AtomicInteger(0);
2
3// 线程安全的递增方法
4public void increment() {
5 int oldValue;
6 int newValue;
7 do {
8 oldValue = value.get(); // 获取当前值, 先获取修改值
9 newValue = oldValue + 1; // 计算新值
10 } while (!value.compareAndSet(oldValue, newValue)); // CAS 操作
11}
12// CAS compare and swap <=> 访问到修改期间,没有其他线程进行修改的话,就可以执行
13value.compareAndSet(oldValue, newValue) => oldvalue == value.get() ? value = newValue :nothing
检查第一次得到的旧值与修改时的值是否一致,判断是否被动过,没动过再改
=> 线程安全的容器:concurrentHashMap or copyonWriteArrayList❌ 不懂
=> 局部变量,线程专享
=> ThreadLocal, 线程本地资源,线程专享
6 线程生命周期
初始(资源) => 可运行(CPU队列) => 运行 => 终止
阻塞&等待

7 线程通信
多线程间的协同工作
1)**共享变量:**访问共享内存变量来交换信息;
2)同步机制: 阻塞&唤醒
synchronized() => wait => notify¬ifyAll (Object中的方法)
ReentrantLock.lock => condition.await => condition.signal&signalAll
BlockingQueue => queue.put() 满则阻塞 => queue.take() 空则阻塞
1synchronized (lock) {
2 while (conditionNotMet) {
3 lock.wait(); // 释放锁,进入等待状态
4 }
5 // 执行操作
6 lock.notify(); // 唤醒等待的线程
7}
1Lock lock = new ReentrantLock();
2Condition condition = lock.newCondition();
3
4lock.lock();
5try {
6 while (conditionNotMet) {
7 condition.await(); // 等待
8 }
9 // 执行操作
10 condition.signal(); // 唤醒等待的线程
11} finally {
12 lock.unlock();
13}
1BlockingQueue<String> queue = new LinkedBlockingQueue<>(10);
2
3// 生产者
4new Thread(() -> {
5 try {
6 queue.put("Data");
7 } catch (InterruptedException e) {
8 Thread.currentThread().interrupt();
9 }
10}).start();
11
12// 消费者
13new Thread(() -> {
14 try {
15 String data = queue.take();
16 } catch (InterruptedException e) {
17 Thread.currentThread().interrupt();
18 }
19}).start();
8 线程池
池化技术,预先创建并管理一组线程,避免线程重复创建和销毁带来的开销
关键配置:核心线程数,最大线程数,空间存活时间,工作队列,拒绝策略
=> 提交任务才会创建线程,或者设置preStartAllCoreThreads
=> 核心线程满了不会创建线程,而是把多余的任务放到工作队列中,等待执行
=> 核心线程满载且工作队列放不下了,才会新增线程执行提交的任务(<最大线程数)
=> 工作队列满了+已最大线程数了 => 拒绝策略?新任务
=> 线程空闲时间超过指定时间 且有多余的非核心线程 => 释放非核心线程
工作队列:
LinkedBlockingQueue 无界队列,链式
ArrayBlockingQueue 有界队列,数组
PriorityBlockingQueue 带有优先级的无界阻塞队列
线程池类型:
1FixedThreadPool: 固定线程数
2CachedThreadPool:变化,动态新建
3SingleThreadPool:单线程的池子
4ScheduledThreadPool:定时任务的池子
shutdown与shutdownNow的区别
shutdown:提醒关闭,会把已提交的任务执行完毕
shutdownNow:强制停止
⭐核心线程数、最大线程数、空闲线程最大存活时间、阻塞队列、拒绝策略
Threads: [⭐⭐⭐⏱️⏱️⏱️]
Task: [🏷️🏷️🏷️🏷️🏷️🏷️]
满了拒绝
Java线程池的线程数设置
CPU密集型:CPU核心数+1。 这样刚好满载,并且当有线程下线,马上有替补上场
IO密集型:CPU核心*2. 满载情况,流水线形式,刚好CPU核心的线程等待IO,下一批线程补上。
Java中的线程池拒绝策略
当线程满载 + 队列满载
- 抛出异常
- 交付调用者线程执行提交的任务
- 删除最早提交的任务
- 丢弃当前提交的任务
- 自定义拒绝策略(根据重要性吧,排序)
线程池内部线程异常怎么办
评论
-
执行方法是execute时,可以看到堆栈异常,移除异常线程并补充一个新的线程
-
执行方法是submit时,可以调用Future.get, 捕捉异常,但不移除和补充线程
面试鸭
- 线程内部手动try-catch
线程安全的集合
Vector:动态数组
HashTable:线程安全的Hash表
ConcurrentHashmap
CopyOnWriteArrayList
BlockingQueue
LinkedBlockingQueue
9 并发工具类
-
ConcurrentHashMap:线程安全的HashMap,多线程修改临界区时加锁或者其他方法,=>安全
-
AtomicInterger:əˈtɒmɪk 线程安全的整型 compareAndSet CAS方法,原子类型
-
Semaphore 信号量:acquire() and release()
-
BlockingQueue:阻塞队列-通信容器 queue.put() and queue.take()
-
CyclicBarrier:循环屏障 barrier.await
-
CountDownlatch:计时器 latch.countDown() latch.await()
497 ReentrantLock实现
=> 可重入锁,允许同一个线程多次获取同一把锁的锁机制,避免线程因为重复获取锁而导致死锁
案例:1. 递归调用中的锁保护
1# 非公平锁
2final boolean nonfairTryAcquire(int acquires) {
3 final Thread current = Thread.currentThread();
4 int c = getState();
5 if (c == 0) { // 锁未被占用
6 if (compareAndSetState(0, acquires)) { // CAS 尝试获取锁
7 setExclusiveOwnerThread(current); // 设置当前线程为独占线程
8 return true;
9 }
10 } else if (current == getExclusiveOwnerThread()) { // ⭐⭐⭐锁已被当前线程占用(重入)
11 int nextc = c + acquires; // 增加重入次数
12 if (nextc < 0) throw new Error("Maximum lock count exceeded");
13 setState(nextc); // 更新状态
14 return true;
15 }
16 return false; // 获取锁失败
17}
基于AQS实现的一个可重入锁,支持公平和非公平两种方式
内部依靠一个State变量和两个等待队列:同步队列和等待队列
利用CAS修改state来争夺锁
-
争抢不到锁就入AQS 等待队列进行等待,AQS 等待队列是一个双向队列
-
抢到锁但是条件condition不满足则入条件队列(每个condition维护一个),单向链表
是否公平锁 => 线程获取锁 是加入同步队列尾部还是直接利用CAS争夺锁
ReentrantLock 是基于 AQS 实现的可重入独占锁,支持公平锁和非公平锁两种模式。其核心是通过 AQS 的状态管理(state)和等待队列来实现线程的阻塞和唤醒。非公平锁的性能通常优于公平锁,但公平锁可以避免线程饥饿问题。ReentrantLock 提供了比 synchronized 更灵活的锁操作,是 Java 并发编程中的重要工具
492 Synchronized实现
依赖于JVM的监视器锁+对象头
当synchronized修饰在方法或者代码块上时,会对特定的对象或者类加锁,确保只有一个线程能运行加锁的代码块;
- synchronized修饰方法:方法的标志位会增加一个ACC_SYNCHRONIZED标志,检查标志再获取锁,这部分进行同步控制
- synchronized修饰代码块:会在代码块前后插入monitorenter和monitorexit字节码指令,上锁+解锁
synchronized是可重入锁
491 Synchronized和ReentrantLock
**Synchronized:**基于JVM实现
ReentrantLock:基于AQS实现
Synchronized 锁升级
无锁阶段:最初(没有对象拿这个锁),多个线程可能并不需要同步访问共享资源,因此可能不存在显式的锁。这是性能最高的阶段,因为不存在锁的开销。
偏向锁阶段:当某个线程多次访问共享资源时(单一线程重复访问),JVM会认为这个线程将来还会继续访问该共享资源,因此会使用偏向锁。意味着被锁定的对象会标记上一个线程的ID,这样其他线程就无法获取到这个锁,只有这个锁可以无竞争的访问资源。
轻量级锁阶段:如果某个线程尝试获取一个被偏向锁锁定的对象,那么就会进入轻量级锁阶段(多个线程交替,不同时竞争)。此时,JVM会使用CAS操作来尝试获取锁。如果CAS成功,那么就得到锁,否则,进入重量级锁阶段。
重量级锁阶段:当轻量级锁阶段的CAS失败(有线程CAS失败****,同时竞争******),升级为重量级锁。意味着拿不到锁的线程会进入阻塞状态,需要进行上下文切换,导致性能下降。
495 什么是Java中的锁自适应自旋
思想:在锁竞争比较少的情况下,线程进入等待状态前,先执行一段自旋操作竞争锁,而不是直接进入等待状态。这样可以减少线程上下文环境切换带来的性能开销。
- 自旋:竞争锁失败后,线程会自旋一段时间争抢锁。反复检查
- 自适应:动态调整自旋的次数。基于之前的自旋结果,上次很快拿到锁,那么自旋久一点,否则自旋少一些。
11373 Volatile 和 synchronized
Volatile 修饰字段-变量:每次读写内存中的变量,保证变量的修改线程及时可见性
synchronized加锁,保证原子性,同一时间仅该线程占有该共享资源。
496 如何优化Java中锁的使用?
锁粒度(分段) or 无锁(CAS)
- 减少锁的粒度:
- 减少加锁的范围,减少锁的持续时间
- 使用更细粒度的锁:提高并发度
- hashTable:
- 通过方法上添加synchronized实现锁的安全,仅一个线程,性能较差
- concurrentHashMap:
- 通过CAS+synchronized实现线程安全,允许多个线程同时读写,性能更高
- hashTable:
- 减少锁的使用
- 通过无锁编程、CAS操作和原子类来避免锁的使用,减少锁带来的性能损失
- 通过减少共享资源的使用,避免对临界区的竞争。(本地变量+线程本地变量)
扩展:
- 独占锁:写操作多的场景,仅允许一个线程持有锁
- 读写锁:允许多个线程并发读,但写的时候需要上锁,适合读多写少的场景
- 乐观锁和悲观锁:悲观锁每次都加锁;乐观锁假设没有冲突-CAS或版本号实现
499 读写锁
允许多个线程同时读操作,但是**写操作需要加锁(**单个线程)。
=> 读写+写写操纵是互斥操作;⭐适合读多写少的情况
可以利用ReadWriteLock和ReentrantReadWriteLock实现
1# 代码示例
2
3ReentrantReadWriteLock lock = new ReentrantReadWriteLock()
4Lock readLock = lock.readLock()
5lock writeLock = lock.writeLock()
6
7// 1. 判断写锁(读写互斥)2. 判断读锁,第一个和后续
8readLock.lock()
9try{
10 ...
11} finally {
12 readLock.unlock()
13}
14
15// 1. 有读锁或写锁且写锁不是当前线程持有,则失败
16// 2. 根据公平性策略(公平锁或非公平锁)决定是否需要阻塞
17// 3. CAS 更新状态
18// 4. 设置写锁持有者 <= 设置可重入
19writeLock.lock() // 互斥 写写互斥
20try{
21 ...
22} finally {
23 writeLock.unlock()
24}
501 Java JMM java内存模型
java memory model
用于描述线程何时从主内存中读取数据、何时把数据写回主存中
屏蔽各操作系统之间的硬件差异,描述多个线程环境中的变量如何在内存中储存和传递

JMM核心目标
- 可见性:确保某个线程的修改,其他线程及时可见。 使用volatile关键字强制线程每次读写都直接从主内存中获取新值
- 有序性:指线程执行操作的顺序,JMM允许某些指令通过指令重拍提高性能,且保证线程内的操作顺序不会被破坏,通过
happens-before关系保证跨线程的有序性。 - **原子性:**指操作不可分割,线程不会在执行过程中被中断。
Why JMM:
操作系统有自己的内存模型,但JAVA是跨平台实现的,因此需要自己设计一套内存模型屏蔽各操作系统之间的差异。JMM描述了多线程环境下,如何在不通过的线程之间共享变量以及变量的操作顺序。
主内存和工作内存:
- 主内存:JAVA堆内存的一部分,所有的实例变量、静态变量和数组元素都存储在主内存中;
- 工作内存:每个线程都有自己的工作内存,工作内存存储了主内存中的变量副本,线程的所有操作都在工作内存中进行。
- 线程之间的变量,必须经过主内存进行传递
506 Why ThreadLocal
每个线程自己独享的独立变量副本,避免多个线程间的变量共享和竞争,解决线程安全问题。
每个线程维护一个ThreadLocalMap 用于存储线程独立的变量副本,ThreadLocalMap以ThreadLocal实例为键,不同线程通过自己(线程独立的)ThreadLocal身份获取各自的变量副本。
避免同一个ThreadLocalMap的竞争
应用场景
- 用户上下文管理
- 数据库连接管理
517 Java中wait、notify和notifyALL
这三个方法都是Object对象定义的方法,用于线程之间的通信,且需要在Synchronized修饰内使用
- wait => 线程进入等待状态,释放锁
- notify => 唤醒一个在等待的线程
- notifyALL => 唤醒所有等待的线程
518 死锁 及 避免
- 条件互斥:独享资源
- 占有且等待:不放手,等别人放弃
- 不可抢占:文明
- 循环等待: A=>B=>C=>A
避免:
- 按序申请 => 锁获取的顺序相同,这样就可以在前面卡住
- 超时等待时间=>释放手中资源和锁
519 volatile关键字
主要的作用还是保证变量的可见性
- 可见性:每次读取变量值都从内存读取最新的值。修改了volatile变量的值,该值会被立刻刷新回主内存中,及时让其他线程可见。
6304 如何知晓子线程是否执行完毕?
- ThreadObj.join() 会等待对应子线程执行完毕,套娃,线程A<=>线程B…
- FutureTask+Callable futrue.get() 拿到线程执行完成的结果
- 回调机制:完成后,调用回调函数通知主线程,异步了
481 Semaphore 信号量
ˈseməfɔːr
主要作用就是确保 只有指定数量的线程能够访问资源,限制同时访问特定资源的线程数量
基本概念
-
许可 permits: 可以访问资源的线程数量。
-
Acquire:尝试获取许可;
-
release:释放许可。
-
公平:按照请求顺序获取许可,防止线程饥饿
-
非公平:可以提高性能。
常见用法:
1Semaphore semaphore = new Semaphore(10); // 允许最多10个线程访问
2semaphore.acquire(); // 失败会阻塞不会往下执行了
3try {
4 // 访问共享资源
5} finally {
6 semaphore.release();
7}
8
9Semaphore semaphore = new Semaphore(5); // 允许最多5个线程同时执行任务
10for (int i = 0; i < 10; i++) {
11 new Thread(() -> {
12 try {
13 semaphore.acquire(); // 阻塞
14 // 执行任务
15 } catch (InterruptedException e) {
16 Thread.currentThread().interrupt();
17 } finally {
18 semaphore.release();
19 }
20 }).start();
21}
482 CyclicBarrier
ˈsaɪklɪk ˈbæriə(r) 循环障碍 // 可以重用,当所有线程到达屏障后,刷新
允许一组线程在执行某个任务相互等待,直到所有线程都达到了Barrier屏障后才能继续执行 // 直接全卡住
- 屏障:调用barrier.await() 阻塞,等待所有线程都到达屏障;
- 线程数量:预指定的,当所有线程到达屏障,所有线程才被唤醒;
- 重用性:可以被重用。
1# 示例
2static CyclicBarrier cyclicbarrier;
3cyclicbarrier = new CyclicBarrier(parties=10, () -> Sout("全部准备就绪"))
4
5for(i ...)
6 new Thread(() -> {
7 Thread.sleep(i * 3000 ms)
8 sout(i+"玩家准备完成");
9 cyclicbarrier.await() // ⭐等待全部完成
10 sout(i+"玩家进入游戏")
11 })
483 CountDownLatch
倒计时门闩锁 - 不可复用
作用:使某线程等待其他线程执行完一组操作完成。每当其他线程完成一个操作,计数器–,到达0则等待的ALL线程会被唤醒
主要功能:
- 等待事件完成:await();
- 递减计数器:latch.countdown();
- 线程同步:当计数器变为0,唤醒线程。
1CountDownLatch latch = new CountDownLatch(3)
2
3for(int i = 1; i <= 3; i++) { // 异步
4 new Thread(() -> {
5 Thread,sleep(i*3000)
6 sout(i+"???")
7 latch.countDown(); // 这里模拟递减计数器
8 }).start()
9}
10
11sout("wait all thread finish")
12latch.await() // 主线程阻塞等待
13sout("all thread finish")
// 并行计算结果的汇总
487 如何控制多个线程的执行顺序呢?
- synchronized + awit + notify, A => B => C 多组锁
- ReentrantLock + condition 多组
- Thread.join, 逐步等待
- CountDownLatch,等待其他的线程countdown
- semaphore,限制异步为同步顺序
==
- synchronized 等待&唤醒
1public void methodA() {
2 synchronized (lock) { // 锁的是 lock
3 lock.wait(); // wait() 也是在 lock 上调用 ✅, 释放锁,并进入等待状态
4 }
5}
6
7public void methodB() {
8 synchronized (lock) { // 锁的是 lock
9 lock.notify(); // notify() 也是在 lock 上调用 ✅
10 }
11}
- reentrantLock + condition
- 信号量
- Thread中的Join()会进行等待线程执行完毕,套娃
- 单线程池,有序提交任务,顺序执行
488 Java阻塞队列
-
ArrayBlockingQueue
-
LinkedBlockingQueue
-
PriorBlockingQueue praɪə(r)
-
delayQueue: 当队列中元素延迟时间到了,才能取出来
489 原子类
- AtomicInterger əˈtɒmɪk ˈɪntɪdʒə(r)
- AtomicLong
- AtomicBoolean
- AtomicStampedReference stæmpt
1.get
2.compareAndSet
3.getAndIncrement ˈɪŋkrəmənt
提问
CAS
Compare and swap
比较内存中的值 是否与 之前的预期值(之前拿到的旧值) 相等
相等 => 将该变量的值设置为新值
=> 判断从之前的访问到现在的修改,中间变量的值是否变动过,没变过=>可以修改
优势:
无锁并发 + CAS是原子性的(线程安全)
缺点:
- ABA问题,如果变量值 从 A=>B=>A,CAS无法检测到这种变化
- 自旋(重复)开销:导致CPU资源浪费,因为一直比较,直到能够修改为止
- 单变量限制:仅支持修改单变量
1public class SpinLock {
2 private final AtomicBoolean lock = new AtomicBoolean(false);
3
4 public void lock() {
5 while (!lock.compareAndSet(false, true)) {
6 // 自旋等待
7 }
8 }
9
10 public void unlock() {
11 lock.set(false);
12 }
13
14 } // 短时间自旋,避免线程上下文切换的开销
ABA问题:引入版本号或者时间戳,每次更新变量的同时更新版本号,从而依靠版本号判断变量是否被调整过。
做法:
1private AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(0, 0); // 初始值:版本号 == 0:0
2
3public void updateValue(int expected, int newValue) {
4 int[] stampHolder = new int[1];
5 Integer currentValue = atomicStampedReference.get(stampHolder);
6 int currentStamp = stampHolder[0];
7
8 //
9 boolean updated = atomicStampedReference.compareAndSet(expected, newValue, currentStamp, currentStamp + 1);
10 if (updated) {
11 System.out.println("Value updated to " + newValue);
12 } else {
13 System.out.println("Update failed");
14 }
15}
AtomicStampedReference:<ObjRef => stampedref>
尝试更新值和版本号
boolean updated = atomicStampedReference.compareAndSet(expected, newValue, currentStamp, currentStamp + 1);
期望的当前值,更新值,期望的版本号,更新的版本号; 当期望的两个值相同才更新
辅助理解
1expected, newValue <= ready
2
3// 获取当前引用值和版本号
4int[] stampHolder = new int[1];
5
6// 因为Java只能返回一个返回值,将多个结果修改数组的形式间接返回
7Integer currentValue = atomicStampedRef.get(stampHolder);
8int currentStamp = stampHolder[0]; // ⭐⭐⭐
9
10"当前值: " + currentValue
11版本号: " + currentStamp
12
13atomicStampedReference.compareAndSet(expected, newValue, currentStamp, currentStamp + 1);
在CAS基础上,多判断一个版本号,检查变量是否修改过,解决ABA问题
// ##
自旋锁 => 获取锁失败,不会阻塞,而是重复尝试获取锁 ❗非公平:饥饿;性能问题:对同一变量高并发进行CAS
1public class SpinLock {
2 private final AtomicBoolean lock = new AtomicBoolean(false);
3
4 public void lock() {
5 while (!lock.compareAndSet(false, true)) {
6 // 自旋等待
7 }
8 }
9
10 public void unlock() {
11 lock.set(false);
12 }
13
14 }
针对自旋锁=>CLH 基于队列的自旋锁

CAS之前节点的完成状态
AQS对CLH的改造

阻塞和通知
❌❌❌AQS
Abstract Queued Synchronizer 是同步器的基础框架, 起到抽象、封装的作用,将一些排队、入队、加锁、中断方法提供出来,具体加锁时机、入队时机等需要实现类自己控制。
- (volatile申明)state状态,可以通过CAS无锁并发方式竞争锁
AQS支持
- 独占模式:只有一个线程可以执行,互斥锁;
- 共享模式:多个线程可以同时执行,例如信号量;
当一个线程获取锁失败时,AQS将其插入等待队列中,并阻塞线程,直到同步状态可用。
使用AQS实现一个独占锁
1
2public class SimpleLock {
3 private static class Sync extends AbstractQueuedSynchronizer {
4 @Override
5 protected boolean tryAcquire(int acquires) {
6 if (compareAndSetState(0, 1)) {
7 setExclusiveOwnerThread(Thread.currentThread());
8 return true;
9 }
10 return false;
11 }
12
13 @Override
14 protected boolean tryRelease(int releases) {
15 if (getState() == 0) throw new IllegalMonitorStateException();
16 setExclusiveOwnerThread(null);
17 setState(0);
18 return true;
19 }
20
21 @Override
22 protected boolean isHeldExclusively() {
23 return getState() == 1;
24 }
25
26 final ConditionObject newCondition() {
27 return new ConditionObject();
28 }
29 }
30
31 private final Sync sync = new Sync();
32
33 public void lock() {
34 sync.acquire(1);
35 }
36
37 public void unlock() {
38 sync.release(1);
39 }
40
41 public boolean isLocked() {
42 return sync.isHeldExclusively();
43 }
44
45 public Condition newCondition() {
46 return sync.newCondition();
47 }
48}
1# 基于AQS的独占锁
2tryAcquire: => CAS(原子,比较且设置)检查是否锁可以使用 => 可以则设置当前线程为独占模式 true=>else false
3
4tryRelease: => getState => 如果不为1则异常 => else 关闭独占模式并释放锁(state设置为0)
5
6private final Sync sync = new Sync() // 作为锁对象
7lock: sync.acquire(1)
8unlock: sync.release(1)
AQS和CAS两者可以经常一起使用,例如在ReentrantLock中,CAS用于实现锁的获取和释放操作,而AQS则管理锁的状态和等待队列。
==
美团技术团队
1// **************************Synchronized的使用方式**************************
2// 1.用于代码块
3synchronized (this) {}
4// 2.用于对象
5synchronized (object) {}
6// 3.用于方法
7public synchronized void test () {}
8// 4.可重入
9for (int i = 0; i < 100; i++) {
10 synchronized (this) {}
11}
12
13// **************************ReentrantLock的使用方式**************************
14public void test () throw Exception {
15 // 1.初始化选择公平锁、非公平锁
16 ReentrantLock lock = new ReentrantLock(true);
17 // 2.可用于代码块
18 lock.lock();
19 try {
20 try {
21 // 3.支持多种加锁方式,比较灵活; 具有可重入特性
22 if(lock.tryLock(100, TimeUnit.MILLISECONDS)){ }
23 } finally {
24 // 4.手动释放锁
25 lock.unlock()
26 }
27 } finally {
28 lock.unlock();
29 }
30}
非公平锁
1// 非公平锁
2static final class NonfairSync extends Sync {
3 ...
4 final void lock() {
5 if (compareAndSetState(0, 1))
6 setExclusiveOwnerThread(Thread.currentThread());
7 else
8 acquire(1);
9 }
10 ...
11}
- 若通过CAS设置变量State(同步状态)成功,也就是获取锁成功,则将当前线程设置为独占线程。
- 若通过CAS设置变量State(同步状态)失败,也就是获取锁失败,则进入Acquire方法进行后续处理。
某个线程获取锁失败的后续流程是什么呢?有以下两种可能:
(1) 将当前线程获锁结果设置为失败,获取锁流程结束。这种设计会极大降低系统的并发度,并不满足我们实际的需求。所以就需要下面这种流程,也就是AQS框架的处理流程。
(2) 存在某种排队等候机制,线程继续等待,仍然保留获取锁的可能,获取锁流程仍在继续。
