JUC
一、基础知识
1.进程是资源分配的最小单位:包括:内存空间、cpu时间片分配
线程是调度的最小单位,能够使cpu资源充分利用。更轻量,上下文切换成本低。
进程: 用来加载指令、管理内存、管理IO。
一个线程就是一个指令流,以一定顺序交给CPU执行。
2.线程创建方式:
-
extends Thread(),然后重写run方法
-
implements Runnable, 重写run
-
implements Callable 重写call 然后call的返回类型是T,能上throws
-
线程池创建线程(常用):
ExcutorService pool = Executors.newFixedThreadPool(3);
pool.submit(new MyRunnable/ MyCallable并用Future<>接收)
3.runnable, callable区别:
-
runnable没返回值,不能抛异常
-
Callable 本身不能直接被线程 / 线程池执行,也没法自己把结果传出来,必须靠 Future/FutureTask 做「中间容器」:执行异步任务、存任务返回值、主线程主动取结果
-
callable有返回值,可抛异常。
为了让
Thread能够运行Callable,Java 设计者引入了FutureTask。FutureTask的“双重身份”:FutureTask实现了RunnableFuture接口,而RunnableFuture又继承了Runnable和Future。- 它是
Runnable:因为它实现了Runnable接口,所以它能被放进new Thread()的参数里。 - 它是
Future:它包含了你想要的结果(Callable的返回值)。
MyCallable mc = new Mycallable(); //FutureTask是Future接口的实现类(装Callable任务+返回值) FutureTask<String> ft = new FutrueTask<String>(mc); Thread t = new Thread(ft); //适配器模式,只认Runnable的run()方法 t.start(); //开启子线程,异步执行任务 String result = ft.get(); //主线程通过Future接口拿结果Thead: MyThread t = new MyThead(); t.start(); //调用start方法启动线程 Runnable: MyRunnable mr = new MyRunnable(); //创建Thread对象 Thread t = new Thread(mr); t.start(); - 它是
4.start()和run()方法区别:
- start()方法会通知jvm向操作系统创建一个子线程,调用run()方法并执行函数逻辑
- run()在当前线程同步执行,失去了并发性。
5.线程状态:
new、runnable、Blocked、waiting、timeWating、terminated
- new Thread()线程:新建态
- .start() 转换为 可运行态。
- 可运行态 synchronzied, 转换为 阻塞态
- 可运行态 在 synchronized调用lock.wait(),转换为Wating
- 可运行态 调用Thread.sleep(30)Thread 静态方法,转换为timeWating
- 线程执行结束, 变为终止态。
6.新建多个线程怎么保证按顺序执行?
• 如果已经新建了 3 个线程,想保证它们按顺序执行,常见做法有几种。
a.最简单:用 join()。
Thread t1 = new Thread(() -> {
System.out.println(“t1”);
});
等价:
// 传统匿名内部类写法
Thread t = new Thread(new Runnable() {
@Override public void run() { } });
Thread t2 = new Thread(() -> {
System.out.println(“t2”);
});
Thread t3 = new Thread(() -> {
System.out.println(“t3”);
});
t1.start();
t1.join();
t2.start();
t2.join();
t3.start();
t3.join();
执行顺序一定是:
t1 -> t2 -> t3
join() 也是一种阻塞等待。
t.join();
当前线程会停在这里,等待 t 线程执行结束。
例如:
Thread t = new Thread(() -> {
try {
Thread.sleep(3000);
System.out.println("子线程结束");
} catch (InterruptedException e) {
System.out.println("子线程被打断");
}
});
t.start();
System.out.println("主线程等待子线程");
t.join();
System.out.println("主线程继续执行");
执行顺序是:
主线程等待子线程
3 秒后
子线程结束
主线程继续执行
这里阻塞的是:
调用 join() 的主线程
不是 t 自己。
b. 如果要求 3 个线程都先启动,但执行内容按顺序,可以用 CountDownLatch:java.util.concurrent(JUC)并发包,JDK1.5 开始提供。
CountDownLatch latch12 = new CountDownLatch(1); // 一次性计数器=1
CountDownLatch latch23 = new CountDownLatch(1);
Thread t1 = new Thread(() -> {
System.out.println("t1");
latch12.countDown(); //计数器-1
});
Thread t2 = new Thread(() -> {
try {
latch12.await(); // 阻塞!等 latch12 计数器变为0
System.out.println("t2");
latch23.countDown();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread t3 = new Thread(() -> {
try {
latch23.await();
System.out.println("t3"); // 阻塞!等 latch23 计数器变为0
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
t1.start();
t2.start();
t3.start();
这里 3 个线程可以同时启动,但实际打印顺序一定是:
t1 -> t2 -> t3
**c.也可以用 ExecutorService 的单线程线程池:**newSingleThreadExecutor();
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> System.out.println("task1"));
executor.submit(() -> System.out.println("task2"));
executor.submit(() -> System.out.println("task3"));
executor.shutdown();
7.notify 和 notifyAll区别
notify: 随机唤醒一个wait线程
notifyAll()唤醒所有wait线程
8.wait 和sleep区别
- 共同点:wait(), wait(time), sleep(time)都能让线程放弃cpu使用权,进入阻塞态
-
- wait是Object成员方法,每个对象都有
- sleep是Tread的静态方法,Thread.sleep(30)
- 苏醒时机不同: wait(time), sleep(time)会在设置时间到期后醒来; wait(time)/ wait()可以被notify唤醒; wait()不唤醒就一直等待; 他们都能被打断唤醒t1.interrupt();。
- 锁特性不同: wait调用必须要获取wait对象的锁, sleep无限制。 wait方法执行会释放对象锁,允许其他线程获取对象锁。 sleep在synchronized执行,不会释放对象锁。(我放弃cpu, 其他还是用不了)
注意:被唤醒不代表马上执行。线程被唤醒后,还要重新竞争这把锁,拿到锁后才能继续从 wait() 后面执行。
Object lock = new Object();
Thread t1 = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("t1 wait 前");
lock.wait(); //释放锁,同时等待被唤醒
System.out.println("t1 wait 后");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock) {
System.out.println("t2 notify 前");
lock.notify(); //唤醒 t1
System.out.println("t2 notify 后");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2 synchronized 结束");
}
});
执行过程:
1. t1 进入 synchronized(lock),拿到 lock 锁
2. t1 执行 lock.wait()
3. t1 释放 lock 锁,进入等待状态
4. t2 进入 synchronized(lock),拿到 lock 锁
5. t2 执行 lock.notify(),唤醒 t1
6. 但是 t2 还没退出 synchronized,所以 lock 锁还在 t2 手里
7. t1 虽然醒了,但拿不到 lock 锁,所以不能继续执行
8. t2 sleep 3 秒,仍然没有释放 lock,因为还在 synchronized 里面
9. t2 退出 synchronized,释放 lock
10. t1 重新抢到 lock
11. t1 才能从 wait() 后面继续执行
9. 如何停止运行的线程?
不要强杀,用“协作式停止”:设置停止标记或调用 interrupt(),让线程自己退出
-
最常见是用 interrupt()。
Thread t = new Thread(() -> { while (!Thread.currentThread().isInterrupted()) { System.out.println("working..."); try { Thread.sleep(1000); } catch (InterruptedException e) { // sleep 被打断后,中断标记会被清除,所以这里重新设置 Thread.currentThread().interrupt(); } } System.out.println("线程退出"); }); t.start(); Thread.sleep(3000); // 请求线程停止 t.interrupt(); 关键点: t.interrupt(); 不是直接杀死线程,而是告诉线程: 你该停了 线程内部要配合检查: Thread.currentThread().isInterrupted() 如果线程在 sleep()、wait()、join() 中,interrupt() 会让它抛出 InterruptedException,从阻塞中醒来。 -
也可以用 volatile 标记:
class Worker implements Runnable {
private volatile boolean running = true; //标记
public void stop() {
running = false;
}
@Override
public void run() {
while (running) {
System.out.println("working...");
}
System.out.println("线程退出");
}
}
使用:
Worker worker = new Worker();
Thread t = new Thread(worker);
t.start();
Thread.sleep(3000);
worker.stop(); //停止线程
为什么要用 volatile?
因为一个线程改了 running = false,另一个线程要能马上看见。
不推荐使用:
thread.stop();
thread.suspend();
thread.resume();
这些方法已经废弃,因为它们可能导致锁没有正确释放、对象状态被破坏。
总结:
// 线程正在运行:用 volatile 标记或 interrupt 标记,让它自己退出
// 线程正在 sleep/wait/join:用 interrupt 让它醒来并退出
不要用 Thread.stop() 强杀
推荐写法通常是:
while (!Thread.currentThread().isInterrupted()) {
try {
// do work
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
关于 join 和 interrupt,要分清楚两个线程:
main 线程调用 t.join()
此时:
main 被阻塞
t 正在运行
// 如果你想让 main 不再等了,要打断的是 main:
mainThread.interrupt();
不是 t.interrupt()。
例子:
Thread mainThread = Thread.currentThread();
Thread t = new Thread(() -> {
try {
Thread.sleep(5000);
System.out.println("子线程结束");
} catch (InterruptedException e) {
System.out.println("子线程被打断");
}
});
Thread interrupter = new Thread(() -> {
try {
Thread.sleep(1000);
mainThread.interrupt(); // 让 main 不再等了
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
interrupter.start();
try {
t.join(); // main 在这里阻塞
System.out.println("main 等到了 t 结束"); // 主线程调用 t.join()被打断,不能执行到这
} catch (InterruptedException e) {
System.out.println("main 的 join 被打断");
}
输出可能是:
main 的 join 被打断
子线程结束
二、线程中并发安全
1.sychronized的底层原理

sychronized底层是: Monitor

当一个对象进入synchronized,会将其于Monitor关联
Monitor结构:

- 对象锁采用互斥的方法让同一时刻只有一个线程持有synchronized锁
- 底层由monitor实现,monitor是jvm级别的对象(c++实现),线程获取锁需要:让synchronized锁关联monitor。
- monitor内部有三个属性:owner、entrylist、waitset
- owner是关联锁获得的线程,只能关联一个; entrylist关联被阻塞的线程; waitset关联处于waiting状态的线程
锁升级
a.重量级锁
monitor实现的锁 属于重量级锁,你了解锁升级吗?
- monitor实现的锁属于重量级锁,涉及了用户态和内核态的切换、进程上下文切换,成本高,性能较低。
- JDK 1.6引入: 偏向锁 和 轻量级锁, 以解决没有多线程竞争或基本没有竞争(同一线程重复获取)的场景下因使用传统锁机制带来的性能开销问题。
当线程获取锁时,怎么让lock对象锁 与 monitor关联?

synchronized(lock) 不是“给对象加了一个独立的锁字段”,而是 JVM 把这个对象当成锁的载体。
而对象头里的 Mark Word,就是 JVM 用来记录这把锁状态的地方。
对象头 = JVM 管锁状态的地方
monitor = JVM 真正管理阻塞/唤醒的运行时结构
关系是这样的:
对象头里的 Mark Word
|
记录锁状态
|
竞争激烈 / 调用 wait() 时
|
膨胀成 monitor(ObjectMonitor)
“对象内存结构”不是废话,它是在解释**: 一个对象为什么能被 synchronized 锁住**
更具体:
- 没竞争时,JVM 可能只在对象头里记个轻量状态
- 有竞争时,锁会“膨胀”为 monitor
- wait/notify 必须依赖 monitor,所以只能在 synchronized 里调用

对象锁lock能被monitor关联的原因: 对象的对象头中的markWord中记录了monitor的地址
b.轻量级锁
在很多的情况下,在 Java 程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁是没必要的。因此 JVM 引入了轻量级锁的概念。
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块A
method2();
}
}
public static void method2() {
synchronized( obj ) { //锁重入,同一线程持有的锁,不存在锁竞争
// 同步块B
}
}

加锁流程
- 在线程对应栈帧中创建一个 Lock Record(锁记录结构),将其 obj 字段指向锁对象Object。
- 通过 CAS 指令将 Lock Record 的地址存储在对象头的 mark word 中,如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。
- 如果是当前线程已经持有该锁了,代表这是一次锁重入。设置 Lock Record 第一部分为 null,起到了一个重入计数器的作用。
- 如果 CAS 修改失败,说明发生了竞争,需要膨胀为重量级锁。
解锁过程
- 遍历线程栈,找到所有 obj 字段等于当前锁对象的 Lock Record。
- 如果 Lock Record 的 Mark Word 为 null,代表这是一次重入,将 obj 设置为 null 后 continue。
- 如果 Lock Record 的 Mark Word 不为 null,则利用 CAS 指令将对象头的 mark word 恢复成为无锁状态。如果失败则膨胀为重量级锁。
c.偏向锁
对CAS操作做优化, 多次重入只做一次CAS


Monitor 实现的锁属于重量级锁,你了解过锁升级吗?
Java 中的 synchronized 有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。
| 锁类型 | 描述 |
|---|---|
| 重量级锁 | 底层使用的 Monitor 实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。 |
| 轻量级锁 | 线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是 CAS 操作,保证原子性 |
| 偏向锁 | 一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个 CAS 操作,之后该线程再获取锁,只需要判断 mark word 中是否是自己的线程 id 即可,而不是开销相对较大的 CAS 命令 |
2. JMM(Java内存模型)
主内存:线程之间真正共享的数据存放处
工作内存:每个线程私有的“变量副本区”
共享内存:更泛的说法,在 Java 里通常可以近似理解成“主内存里那部分被多个线程共同访问的数据
a.主内存是什么
在 Java 内存模型(JMM)里,实例字段、静态字段、数组元素这类数据,按模型都存在于主内存。
可以把它理解成:
- 所有线程都能看到的“公共区”
- 线程要操作共享变量,最终都得和主内存打交道
例如:
static int count = 0;
static final Object obj = new Object();
这里的 count 和 obj 引用,从 JMM 角度看都属于主内存中的共享变量。
———
b.工作内存是什么
每个线程都有自己的工作内存。
线程不能每次都直接操作主内存变量,而是会先把主内存里的变量值拷贝一份到自己的工作内存,然后:
- 读取:先从工作内存读
- 修改:先改工作内存里的副本
- 某个时机再刷回主内存
所以会出现一个问题:
- 线程 A 改了 count
- 线程 B 可能还在用自己工作内存里的旧值
- 这就产生了可见性问题(synchronized, lock, volatile解决)
Java 线程之间通过主内存来实现共享。

标答:
- JMM是java内存模型,定义了共享内存中多线程程序 读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性。
- JMM把内存分为两块:一块是私有线程的工作内存,一块是所有线程的共享区域(主内存)
- 线程间隔离,通信需要先从主内存读取共享变量到自己的工作内存,操作完后同步回主内存。
3.CAS
“我认为这个值现在还是 A,如果真是 A,就把它改成 B;如果不是 A,说明别人动过了,我就失败或重试。”
能保证并发安全: “比较 + 修改”这个动作是CPU 级别的原子操作。


-
先读旧值 -> 计算新值 -> 提交时比较旧值有没有变 -> 没变就更新,变了就重试。
CAS 的典型特点
优点:
- 不用加互斥锁
- 性能通常比阻塞锁更高
- 适合低冲突场景
缺点:
- 高竞争时会一直自旋重试,浪费 CPU -> 一般会设置阈值
- 会有 ABA 问题(线程1读到a准备改为c, 然后线程2把改为b,又改回a。线程1CAS成功,把a改为c。CAS只能确保值没变, 无法这个值在期间有没有被别人动过/节点是不是还是原来那个语义上的对象) -> 加版本号解决(A, version=1)
- 只能保证一个共享变量的原子更新,复杂场景不够用
———
和 synchronized 的区别
- synchronized:悲观锁,先假设会冲突,先加锁再操作
- CAS:乐观锁,先假设不会冲突,提交时再检查
底层实现
依赖于一个UnSage类直接调用OS底层的CAS指令


4.volatile
修饰共享变量**(类的成员变量、类的静态变量**)后, 能:
- 保证线程间的可见性:
用volatile修饰共享变量,能够防止防止JIT等优化发生,让一个线程对共享变量可见。
- 禁止指令重排
会在读、写共享变量时加入不同的屏障, 防止指令重排。


5.什么是AQS
全称是 AbstractQueuedSynchronizer,即抽象队列同步器。它是构建锁或者其他同步组件的基础框架。
AQS 与 Synchronized 的区别
| synchronized | AQS |
|---|---|
| 关键字,C++ 语言实现 | Java 语言实现 |
| 悲观锁,自动释放锁 | 悲观锁,手动开启和关闭 |
| 锁竞争激烈时都是重量级锁,性能差 | 锁竞争激烈的情况下,提供了多种解决方案 |
AQS 常见的实现类
- ReentrantLock:阻塞式锁
- Semaphore:信号量
- CountDownLatch:倒计时锁


标答:
- AQS是多线程中的队列同步器。是一种锁机制,作为一个基础框架使用,像是ReentrantLock, Semaphore,CountDownLatch倒计时锁等基于此实现。
- AQS内部维护了FIFO的双向队列,存储排队的线程。
- 内部还有一个属性state,相当于是个资源,默认是0无锁状态,如果队列中的线程成功修改state=1,则当前线程就获取了资源。
- 保证多线程修改情况下的原子性:对state修改时用cas操作。

6.ReentrantLock
翻译过来是可重入锁,相对于synchronized锁 有以下特点:
- 可中断
- 可设置超时时间
- 可设置公平锁/非公平锁
- 支持多个条件变量
- 与synchronized一样,都支持重入
// 创建锁对象
ReentrantLock lock = new ReentrantLock();
try{
// 获取锁
lock.lock();
}finally{
lock.unlock();
}
}
实现原理
主要利用CAS + AQS队列实现。支持公平锁和非公平锁。
构造方法接收一个可选公平参数,默认非公平锁。设置为true时,为公平锁,否则为非公平锁。公平锁的效率往往没有非公平锁的效率高,在多线程访问情况下,非公平锁表现出较低的吞吐量。
ReentrantLock 本身更像对外接口, 真正同步控制核心在内部的 Sync(基于 AQS):
abstract static class Sync extends AbstractQueuedSynchronizer

标答:
- ReentrantLock表示可重入锁,调用lock方法获取锁之后,再次调用lock,不会再次阻塞。
- ReentrantLock主要利用CAS + AQS队列来实现
- 支持公平锁和非公平锁,在提供的构造器中:默认无参构造是:非公平锁,也可以传参true设置公平锁。
// 1. 基本加锁
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
System.out.println("do something");
} finally {
lock.unlock();
}
// 2. 可重入
// 同一个线程拿到锁后,可以再次拿同一把锁:
class Demo {
private final ReentrantLock lock = new ReentrantLock();
public void a() {
lock.lock();
try {
b();
} finally {
lock.unlock();
}
}
public void b() {
lock.lock();
try {
System.out.println("reentrant");
} finally {
lock.unlock();
}
}
}
// 这里 a() 持锁后调用 b(),不会死锁,因为它是可重入锁。
但要注意:
加锁几次,就要解锁几次
// ReentrantLock 可以配合 Condition 做线程等待/唤醒,类似 wait/notify, 但更灵活。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
class Demo {
private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private boolean ready = false;
public void awaitMethod() throws InterruptedException {
// 共享数据在并发场景下不能随便看、随便改,所以要先加锁
lock.lock();
try {
while (!ready) {
condition.await(); // 当前线程先放下锁,然后进入等待,直到别人唤醒它
}
System.out.println("继续执行");
} finally {
lock.unlock();
}
}
public void signalMethod() {
lock.lock();
try {
ready = true;
condition.signal(); // 唤醒一个线程
} finally {
lock.unlock();
}
}
}
和 synchronized 的区别
ReentrantLock:
- 需要手动加锁解锁
- 支持 tryLock()
- 支持可中断锁 lockInterruptibly()
- 支持 Condition做线程等待/唤醒
- 功能更灵活
synchronized:
- 写法简单
- JVM 自动释放锁
- 适合大多数简单同步场景
7.synchronized 和 Lock有什么区别 *
Lock 是接口: public interface Lock
常见实现类有:
- ReentrantLock
- ReentrantReadWriteLock.WriteLock
- ReentrantReadWriteLock.ReadLock
所以平时写的是:
Lock lock = new ReentrantLock();

8.死锁产生及排查
产生条件
一个线程需要同时获取多把锁,这时就容易发生死锁。
排查
出现死锁现象,可以用jdk自带工具:jps和jstack
- jps:输出JVM中运行的线程状态信息
- jstack: 查看Java进程内线程的堆栈信息
-
先用 jps 找 Java 进程 ID:
jps可能输出:
12345 DeadLockDemo
67890 Jps
2.然后用 jstack 查看线程栈:
jstack 12345
如果真的有死锁,jstack 通常会在末尾直接提示:
Found one Java-level deadlock:
- Jconsole: jdk/bin启动

- VisualVM: jdk/bin/jvisualvm.exe启动
9.ConcurrentHashMap
一种线程安全的高效Map集合
底层数据结构:
- JDK1.7底层采用分段的数组+链表实现
- JDK1.8采用的数据结构和HashMap1.8一样:数组+链表+红黑树

- 底层数据结构:
- JDK1.7底层采用分段的数组+链表实现
- JDK1.8采用的数据结构更HashMap1.8结构一样,数组、链表、红黑树
- 加锁方式:
- JDK1.7采用Segment分段锁,底层使用的是ReentrantLock
- JDK1.8采用CAS添加新节点,采用synchronized锁定链表或红黑二叉树的首节点,相对Segment分段锁粒度更细,性能更好。
10.导致并发程序出现问题的根本原因:
Java程序中怎么保证多线程的执行安全?
Java并发编程三大特性:
-
原子性:一个线程在cpu中操作不可暂停,也不可中断,要不执行完成,要不不执行
synchronized: 同步加锁; JUC中的lock: 加锁
-
内存可见性: 让一个线程对共享变量的修改对另一个线程可见
volatile, synchronized, lock
-
有序性: 处理器为提交效率,对输入代码优化不保证执行顺序
volatile

798

被折叠的 条评论
为什么被折叠?



