文章目录
- [0. 前言](#0. 前言)
- [1. 线程基础](#1. 线程基础)
-
- [1.1 线程和进程](#1.1 线程和进程)
-
- [1.1.1 进程](#1.1.1 进程)
- [1.1.2 线程](#1.1.2 线程)
- [1.1.3 进程和线程的区别](#1.1.3 进程和线程的区别)
- [1.2 并行和并发](#1.2 并行和并发)
-
- [1.2.1 单核 CPU 的情况](#1.2.1 单核 CPU 的情况)
- [1.2.2 多核 CPU 的情况](#1.2.2 多核 CPU 的情况)
- [1.2.3 并行和并发的区别](#1.2.3 并行和并发的区别)
- [1.3 线程创建的方式](#1.3 线程创建的方式)
-
- [1.3.1 继承 Thread 类,重写 run 方法](#1.3.1 继承 Thread 类,重写 run 方法)
- [1.3.2 实现 Runnable 接口,重写 run 方法](#1.3.2 实现 Runnable 接口,重写 run 方法)
- [1.3.3 实现 Callable 接口,重写 call 方法](#1.3.3 实现 Callable 接口,重写 call 方法)
- [1.3.4 通过线程池创建线程(项目中的使用方式)](#1.3.4 通过线程池创建线程(项目中的使用方式))
- [1.3.5 Runnable 接口与 Callable 接口有什么区别](#1.3.5 Runnable 接口与 Callable 接口有什么区别)
- [1.3.6 在启动线程的时候,可以使用 run 方法吗,run 方法和 start 方法有什么区别](#1.3.6 在启动线程的时候,可以使用 run 方法吗,run 方法和 start 方法有什么区别)
- [1.4 线程包括哪些状态,状态之间是如何转换的](#1.4 线程包括哪些状态,状态之间是如何转换的)
-
- [1.4.1 线程的状态](#1.4.1 线程的状态)
- [1.4.2 线程状态之间是如何转换的](#1.4.2 线程状态之间是如何转换的)
- [1.5 新建三个线程,如何保证它们按照顺序执行](#1.5 新建三个线程,如何保证它们按照顺序执行)
- [1.6 notify 方法和 notifyAll 方法有什么区别](#1.6 notify 方法和 notifyAll 方法有什么区别)
- [1.7 wait 方法和 sleep 方法的区别](#1.7 wait 方法和 sleep 方法的区别)
-
- [1.7.1 共同点](#1.7.1 共同点)
- [1.7.2 不同点](#1.7.2 不同点)
- [1.8 如何停止一个正在运行的线程](#1.8 如何停止一个正在运行的线程)
- [2. 线程安全](#2. 线程安全)
-
- [2.1 synchronized 关键字的底层原理](#2.1 synchronized 关键字的底层原理)
- [2.2 synchronized 关键字的底层原理-进阶](#2.2 synchronized 关键字的底层原理-进阶)
- [2.3 Java 的内存模型](#2.3 Java 的内存模型)
- [2.4 CAS](#2.4 CAS)
- [2.5 volatile 关键字](#2.5 volatile 关键字)
-
- [2.5.1 保证共享变量在线程间的可见性](#2.5.1 保证共享变量在线程间的可见性)
- [2.5.2 禁止指令重排序](#2.5.2 禁止指令重排序)
- [2.6 AQS](#2.6 AQS)
-
- [2.6.1 AQS 与 synchronized 的区别](#2.6.1 AQS 与 synchronized 的区别)
- [2.6.2 AQS 的常见实现类](#2.6.2 AQS 的常见实现类)
- [2.6.3 AQS 的基本工作机制](#2.6.3 AQS 的基本工作机制)
- [2.7 ReentrantLock 的底层实现原理](#2.7 ReentrantLock 的底层实现原理)
- [2.8 synchronized 和 Lock 有什么区别](#2.8 synchronized 和 Lock 有什么区别)
-
- [2.8.1 语法层面](#2.8.1 语法层面)
- [2.8.2 功能层面](#2.8.2 功能层面)
- [2.8.3 性能层面](#2.8.3 性能层面)
- [2.9 死锁产生的条件及排查方案](#2.9 死锁产生的条件及排查方案)
- [2.10 ConcurrentHashMap](#2.10 ConcurrentHashMap)
- [2.11 导致并发程序出现问题的根本原因](#2.11 导致并发程序出现问题的根本原因)
-
- [2.11.1 原子性](#2.11.1 原子性)
- [2.11.2 可见性](#2.11.2 可见性)
- [2.11.3 有序性](#2.11.3 有序性)
0. 前言
相信说到多线程,很多同学都麻了,因为我们在学习过程中基本上没有使用过多线程,而且在项目开发的过程中好像也没怎么用到多线程,但面试官是真爱问,我们必须要研究一下
与多线程相关的面试题一般都有两个标签:高频、难以回答
与多线程相关的面试题大概分为四类
- 第一类:线程的基础知识,涉及到很多面试题,但相对来说比较好回答
- 第二类:线程中的并发安全,这一类的内容被问到的概率非常高,并且不太好回答,大部分都是跟锁的内容有关
- 第三类:线程池,一般在项目中使用多线程的话,都会配合线程池一起使用
- 第四类:使用场景,这个是最让人头疼的,前三类问题死记硬背可能还没啥问题,但是遇到场景问题可能就毫无思路了,万一面试官一上来就问你们在项目中哪里用到线程池,像这些场景题,因为大家没有经历过,项目中根本没用过,不太好回答,不过没关系,这次我们一起来搞定这些问题
其实学习多线程相关的知识,不仅仅是为了应付面试,更是为了个人技能水平的提升,万一以后项目中真要使用到多线程,也能快速上手
线程池和使用场景部分可以查看我的另一篇博文:Java面试篇(线程池相关专题)
1. 线程基础
1.1 线程和进程
1.1.1 进程
程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU ,将数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备,进程就是用来加载指令、管理内存、管理 IO 的
简单地来说,当一个程序被运行,从磁盘加载这个程序的代码至内存,就开启了一个进程
进程也可细分为多实例进程和单实例进程
1.1.2 线程
一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
一个进程之内可以分为一到多个线程
1.1.3 进程和线程的区别
- 进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
- 不同的进程使用不同的内存空间,进程下的所有线程可以共享进程的内存空间
- 线程更轻量,线程上下文切换的成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)
1.2 并行和并发
1.2.1 单核 CPU 的情况
单核 CPU 下线程实际还是串行执行的
操作系统中有一个组件叫做任务调度器,将 CPU 的时间片(Windows 操作系统下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 CPU 在线程间的切换非常快(时间片很短),人类感觉是同时运行的
总结为一句话就是:微观上是串行的,宏观上是并行的
一般会将这种线程轮流使用 CPU 的做法称为并发(concurrent)
1.2.2 多核 CPU 的情况
每个核(core)都可以调度运行线程,这时候线程可以是并行的
1.2.3 并行和并发的区别
并发(concurrent)是同一时间应对(dealing with)多件事情的能力
并行(parallel)是同一时间动手做(doing)多件事情的能力
举一个例子方便大家理解:
- 家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这多件事,这时就是并发
- 家庭主妇雇了个保姆,她们一起做这些事,这时既有并发,也有并行(这时会产生竞争,例如锅只有一口,一个人用锅时,另一个人就得等待)
- 家庭主妇雇了 3 个保姆,一个专做饭、一个专打扫卫生、一个专喂奶,互不干扰,这时是并行
1.3 线程创建的方式
共有四种方式可以创建线程,分别是:
- 继承 Thread 类,重写 run 方法
- 实现 Runnable 接口,重写 run 方法
- 实现 Callable 接口,重写 call 方法
- 通过线程池创建线程(项目中的使用方式)
1.3.1 继承 Thread 类,重写 run 方法
java
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("MyThread is running");
}
}
java
import cn.edu.scau.thread.MyThread;
public class ThreadDemo {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start();
t2.start();
}
}
1.3.2 实现 Runnable 接口,重写 run 方法
java
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("MyRunnable is running");
}
}
java
import cn.edu.scau.runnable.MyRunnable;
public class RunnableDemo {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread t1 = new Thread(myRunnable);
Thread t2 = new Thread(myRunnable);
t1.start();
t2.start();
}
}
1.3.3 实现 Callable 接口,重写 call 方法
java
import java.util.concurrent.Callable;
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("MyCallable is calling");
return "ok";
}
}
java
import cn.edu.scau.callable.MyCallable;
import java.util.concurrent.FutureTask;
public class CallableDemo {
public static void main(String[] args) {
MyCallable myCallable = new MyCallable();
FutureTask<String> stringFutureTask = new FutureTask<>(myCallable);
Thread thread = new Thread(stringFutureTask);
thread.start();
String result;
try {
result = stringFutureTask.get();
System.out.println("result = " + result);
} catch (Exception exception) {
exception.printStackTrace();
}
}
}
1.3.4 通过线程池创建线程(项目中的使用方式)
java
package cn.edu.scau.threadpool;
public class MyExecutor implements Runnable {
@Override
public void run() {
System.out.println("MyExecutor is running");
}
}
java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorDemo {
public static void main(String[] args) {
// 创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(3);
// 提交任务
executorService.submit(new MyExecutor());
// 关闭线程池
executorService.shutdown();
}
}
1.3.5 Runnable 接口与 Callable 接口有什么区别
- Runnable 接口的 run 方法没有返回值
- Callable 接口 call 方法有返回值,是个泛型,和 Future、FutureTask 配合可以用来获取异步执行的结果
- Callable 接口的 call 方法允许抛出异常,而 Runnable 接口的 run 方法的异常只能在内部处理(使用 try-catch 代码块),不能继续上抛
1.3.6 在启动线程的时候,可以使用 run 方法吗,run 方法和 start 方法有什么区别
- start 方法:用来启动线程,通过该线程调用 run 方法执行 run 方法中所定义的逻辑代码,start 方法只能被调用一次,如果多次调用 start 方法会抛出异常(
IllegalThreadStateException
) - run 方法:封装了要被线程执行的代码,可以被调用多次
- 调用 run 方法使用的是原线程,而调用 start 方法会另开一个线程
下面是一个例子
java
public class MyRunnable implements Runnable {
@Override
public void run() {
System.err.println("currentThread = " + Thread.currentThread().getName());
System.out.println("MyRunnable is running");
}
}
java
import cn.edu.scau.runnable.MyRunnable;
public class RunnableDemo {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread t1 = new Thread(myRunnable);
Thread t2 = new Thread(myRunnable);
t1.run();
t1.run();
t1.start();
t1.start();
t2.start();
}
}
输出结果如下
1.4 线程包括哪些状态,状态之间是如何转换的
1.4.1 线程的状态
线程的状态可以参考 JDK 中 Thread 类中的枚举类 State,主要有以下六个状态:
- NEW:新建
- RUNNABLE:可运行
- BLOCKED:阻塞
- WAITING:等待
- TIMED_WAITING:计时等待
- TERMINATED:终止
java
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW,
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called {@code Object.wait()}
* on an object is waiting for another thread to call
* {@code Object.notify()} or {@code Object.notifyAll()} on
* that object. A thread that has called {@code Thread.join()}
* is waiting for a specified thread to terminate.
*/
WAITING,
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;
}
1.4.2 线程状态之间是如何转换的
- 创建线程对象是新建状态
- 调用了 start 方法后转变为可执行状态
- 线程获取到了 CPU 的执行权,且执行结束,是终止状态
- 在可执行状态的过程中,如果没有获取 CPU 的执行权,可能会切换其他状态
- 如果没有获取锁(synchronized 或 lock )进入阻塞状态,获得锁再切换为可执行状态
- 如果线程调用了wait 方法进入等待状态,其他线程调用 notify 方法唤醒线程后可切换为可执行状态
- 如果线程调用了 sleep 方法,进入计时等待状态,到时间后可切换为可执行状态
1.5 新建三个线程,如何保证它们按照顺序执行
要保证新建的三个线程按照顺序执行,可以使用线程中的 join 方法,以下是一个示例
java
public class JoinTest {
public static void main(String[] args) throws InterruptedException {
// 创建线程对象
Thread t1 = new Thread(() -> System.out.println("t1"));
Thread t2 = new Thread(() -> {
try {
t1.join(); // 加入线程t1,只有t1线程执行完毕以后,再次执行该线程
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2");
});
Thread t3 = new Thread(() -> {
try {
t2.join(); // 加入线程t2,只有t2线程执行完毕以后,再次执行该线程
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t3");
});
// 启动线程
t3.start();
t2.start();
t1.start();
}
}
1.6 notify 方法和 notifyAll 方法有什么区别
- notifyAIL 方法:唤醒所有处于 WAITING 状态的线程
- notify 方法:随机唤醒一个处于 WAITING 状态的线程
1.7 wait 方法和 sleep 方法的区别
1.7.1 共同点
wait()、wait(long) 和 sleep(long) 方法的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态
1.7.2 不同点
方法归属不同:
- sleep(long)是 Thread 的静态方法
- 而 wait()、wait(long) 都是 Object 类的方法,每个对象都有
醒来时机不同:
- 执行 sleep(long) 和 wait(long) 方法的线程都会在等待相应毫秒后醒来,wait(long) 和 wait() 可以被 notify 唤醒,wait() 如果不唤醒就会一直等下去
- 它们都可以被打断唤醒
锁特性不同(重点):
- wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制
- wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(放弃 CPU 的执行权,但其它线程可以用)
- sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(放弃 CPU 的执行权,但其它线程也用不了)
1.8 如何停止一个正在运行的线程
有三种方式可以停止线程:
- 使用退出标志,使线程正常退出,也就是当 run 方法完成后线程终止
- 使用 stop 方法强行终止(不推荐,方法已废用)
- 使用 interrupt 方法中断线程
- 打断阻塞的线程(sleep、wait、join),线程会抛出
InterruptedException
异常 - 打断正常的线程,可以根据打断状态来标记是否退出线程
- 打断阻塞的线程(sleep、wait、join),线程会抛出
使用退出标志的示例
java
public class InterruptDemo01 extends Thread {
volatile boolean flag = false; // 线程执行的退出标记
@Override
public void run() {
while (!flag) {
System.out.println("MyThread...run...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
// 创建MyThread对象
InterruptDemo01 t1 = new InterruptDemo01();
t1.start();
// 主线程休眠6秒
Thread.sleep(6000);
// 更改标记为true
t1.flag = true;
}
}
使用 stop 方法的示例
java
public class InterruptDemo02 extends Thread {
volatile boolean flag = false; // 线程执行的退出标记
@Override
public void run() {
while (!flag) {
System.out.println("MyThread...run...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
// 创建MyThread对象
InterruptDemo02 t1 = new InterruptDemo02();
t1.start();
// 主线程休眠6秒
Thread.sleep(6000);
// 调用stop方法
t1.stop();
}
}
使用 interrupt 方法的示例
java
public class InterruptDemo03 {
public static void main(String[] args) throws InterruptedException {
// 1.打断阻塞的线程
// Thread t1 = new Thread(() -> {
// System.out.println("t1 正在运行...");
// try {
// Thread.sleep(5000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }, "t1");
// t1.start();
// Thread.sleep(500);
// t1.interrupt();
// System.out.println(t1.isInterrupted());
// 2.打断正常的线程
Thread t2 = new Thread(() -> {
while (true) {
Thread current = Thread.currentThread();
boolean interrupted = current.isInterrupted();
if (interrupted) {
System.out.println("打断状态:" + interrupted);
break;
}
}
}, "t2");
t2.start();
Thread.sleep(500);
t2.interrupt();
}
}
2. 线程安全
2.1 synchronized 关键字的底层原理
我们先来回忆一下 synchronized 关键字的基本使用场景------卖票
java
public class TicketDemo {
private static final Object lock = new Object();
private int ticketNum = 10;
public synchronized void getTicket() {
synchronized (lock) {
if (ticketNum <= 0) {
return;
}
System.out.println(Thread.currentThread().getName() + "抢到一张票,剩余:" + ticketNum);
// 非原子性操作
ticketNum--;
}
}
public static void main(String[] args) {
TicketDemo ticketDemo = new TicketDemo();
for (int i = 0; i < 20; i++) {
new Thread(ticketDemo::getTicket).start();
}
}
}
synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就进入阻塞状态
在了解 synchronized 关键字的底层原理前,我们需要先了解一下 Monitor
Monitor 被翻译为监视器,由 JVM 提供,C++ 语言实现,Monitor 的大致结构如下
下面来看一个使用 synchronized 关键字的例子
- Owner:存储当前获取锁的线程的,只能有一个线程可以获取
- EntryList:关联没有抢到锁的线程,处于 Blocked 状态的线程
- WaitSet:关联调用了 wait 方法的线程,处于 WAITING 状态的线程
注意:EntryList 中的线程并不是按照先来后到的顺序获取 Owner 的,谁抢到了 Owner ,谁就拿到了锁
总结:
- synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】
- synchronized 的底层由 Monitor 实现的,Monitor 是 JVM 级别的对象(由C++实现),线程获得锁需要使用对象(锁)关联 Monitor
- 在 Monitor 内部有三个属性,分别是 Owner、EntryList、WaitSet
- Owner 关联的是获得锁的线程,并且只能关联一个线程
- Entrylist 关联的是处于阻塞状态的线程
- Waitset 关联的是处于 WAITING 状态的线程
2.2 synchronized 关键字的底层原理-进阶
面试官可能会问:Monitor 实现的锁属于重量级锁,你了解过锁升级吗
利用 Monitor 实现的锁属于重量级锁,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低
该部分的内容比较难懂,可观看视频:synchronized 关键字的底层原理-进阶
2.3 Java 的内存模型
JMM:Java Memory Model,Java内存模型,定义了共享内存中多线程程序读写操作的规则,通过这些规则来规范对内存的读写操作,从而保证指令的正确性
总结:
- JMM(Java Memory Model),Java内存模型,定义了共享内存中多线程程序读写操作的规则,通过这些规则来规范对内存的读写操作从而保证指令的正确性
- JMM 把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)
- 线程跟线程之间是相互隔离,线程跟线程之间交互需要通过主内存
2.4 CAS
CAS:Compare And Swap(先比较再交换),它体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性,很多框架的底层都采用了 CAS 的思想
JUC(java.util.concurrent)包下也有很多类都用到了 CAS 操作
- AbstractQueuedSynchronizer(AQS框架)
- AtomicXXX 类
下面是 CAS 的一个示例
一个当前内存值 V、旧的预期值 A、即将更新的值 B,当且仅当旧的预期值 A 和内存值 V 相同时,将内存值修改为 B 并返回 true,否则什么都不做,并返回 false
如果 CAS 操作失败,通过自旋的方式等待并再次尝试,直到成功
那什么是自旋呢,自旋可以理解为一个死循环
因为自旋没有加锁,所以线程不会陷入阻塞,效率较高。如果竞争激烈,重试频繁发生,效率会受影响
CAS 底层依赖于 Unsafe 类,利用 Unsafe 类直接调用操作系统底层的 CAS 指令,以下是 Unsafe 类的部分源码
JUC 包下的 ReentrantLock 类也采用了 CAS 思想,以下是 ReentrantLock 类中的 compareAndSetState 方法的源码(其中 U 是一个 Unsafe 类的实例)
- CAS 是基于乐观锁的思想:乐观地估计,不怕别的线程来修改共享变量,就算其它线程改了也没关系,吃亏点再重试
- synchronized 是基于悲观锁的思想:悲观地估计,得防着其它线程来修改共享变量,上了锁之后其它线程都无法修改,修改完后解开锁,其它线程才有机会修改
2.5 volatile 关键字
一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 关键字修饰之后,那么就具备了两层语义:
- 保证共享变量在线程间的可见性
- 禁止进行指令重排序
2.5.1 保证共享变量在线程间的可见性
用 volatile 修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见
我们来看以下代码
java
public class ForeverLoop {
static boolean stop = false;
public static void main(String[] args) {
new Thread(() -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
stop = true;
System.out.println(Thread.currentThread().getName() + ":modify stop to true...");
}, "t1").start();
new Thread(() -> {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + stop);
}, "t2").start();
new Thread(() -> {
int i = 0;
while (!stop) {
i++;
}
System.out.println("stopped... c:" + i);
}, "t3").start();
}
}
大家猜一下,线程 t3 中的死循环会停止吗,我们来看一下控制台的输出
可以看到,线程 t3 中的死循环不会停止(而且 Java 程序也没有结束运行)
为什么线程 t3 中的死循环不会停止呢,明明线程 t1 成功修改了 stop 变量,而且线程 t2 也成功打印了 stop 变量修改后的值
其实是因为 JVM 虚拟机中的 JIT(Just-In-Time,即时编译器)对代码做了优化,优化后的代码大概如下
有两种解决方案:
- 在程序运行的时候添加 VM 参数 -Xint ,禁用即时编译器,但是不推荐,因为其它代码需要使用 JIT
- 在修饰变量的时候加上 volatile 关键字,告诉 JIT 不要对 volatile 修饰的变量做优化
2.5.2 禁止指令重排序
该部分内容晦涩难懂,请观看视频:禁止指令重排序
用 volatile 修饰的共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果
volatile使用技巧:
- 写变量时让 volatile 修饰的变量的在代码最后位置
- 读变量时让 volatile 修饰的变量的在代码最开始位置
2.6 AQS
AQS:AbstractQueuedSynchronizer,抽象队列同步器,是构建锁或者其他同步组件的基础框架
2.6.1 AQS 与 synchronized 的区别
synchronized | AQS |
---|---|
关键字,由 C++ 语言实现 | 由 Java 语言实现 |
悲观锁,能够自动释放锁 | 悲观锁,需要手动开启和关闭 |
锁竞争激烈都是重量级锁,性能差 | 锁竞争激烈的情况下,提供了多种解决方案 |
2.6.2 AQS 的常见实现类
- ReentrantLock:阻塞式锁
- Semaphore:信号量
- CountDownLatch:倒计时锁
2.6.3 AQS 的基本工作机制
AQS 内部维护了一个先进先出的双向队列,队列中存储的是排队的线程
如果多个线程共同去抢 state 资源,如何保证原子性呢?其实也是采用了 CAS 的思想,某个线程一抢到 state 资源就将 state 的值设置为 1
面试官可能此时又会问了,AQS 是公平锁还是非公平锁?其实 AQS 可以实现公平锁,也可以实现非公平锁,AQS 的不同实现类有不同的方案
那什么是公平锁和非公平锁呢,我们来看一个例子
线程 0 目前拿到了锁,当线程 0 刚好释放锁的时候,另一个线程 5 刚好也来了
- 如果新来的线程 5 跟等待队列中的线程争夺锁,就是非公平锁
- 如果新来的线程 5 加入等待队列中,成为等待队列的最后一个元素,就是非公平锁
2.7 ReentrantLock 的底层实现原理
在这里插入图片描述
ReentrantLock 翻译过来是可重入锁,相对于 synchronized ,它具备以下特点:
- 可中断
- 可以设置超时时间
- 可以设置公平锁
- 支持多个条件变量
- 与 synchronized 一样,都支持重入
ReentrantLock 的基本使用方法
ReentrantLock 主要利用 CAS + AQS 队列来实现,支持公平锁和非公平锁,ReentrantLock 的构造方法接受一个可选的公平参数,默认非公平锁,我们可以查看 ReentrantLock 类的源码(其中 sync 是 Sync 类的实例,Sync 类继承自 AbstractQueuedSynchronizer 类)
当设置为true时,表示公平锁,否则为非公平锁,公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量
ReentrantLock 的实现原理
- 线程来抢锁后使用 CAS 的方式修改 state 状态,修改成功则让 exclusiveOwnerThread 属性指向当前线程,获取锁成功
- 假如修改状态失败,则会进入双向队列中等待,head 指向双向队列头部,tail 指向双向队列尾部
- 当 exclusiveOwnerThread 为 null 的时候,则会唤醒在双向队列中等待的线程
- 公平锁则体现在按照先后顺序获取锁,非公平体现在不在排队的线程也可以抢锁
2.8 synchronized 和 Lock 有什么区别
2.8.1 语法层面
- synchronized 是关键字,源码在 JVM 中,由 C++ 语言实现
- Lock 是接口,源码由 JDK 提供,由 Java 语言实现
- 使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁
2.8.2 功能层面
- 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
- Lock 提供了许多 synchronized 不具备的功能,例如公平锁、可打断、可超时、多条件变量
- Lock 有适合不同场景的实现类,如 ReentrantLock, ReentrantReadWriteLock(读写锁)
注意:使用 lock 方法的锁不是一个可打断的锁,如果想使用可打断的锁,需要使用 lockInterruptibly 方法
2.8.3 性能层面
- 在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能算差
- 在竞争激烈时,Lock类 的实现通常会提供更好的性能
以下是一个示例
java
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockTest {
// 创建锁对象
static final ReentrantLock REENTRANT_LOCK = new ReentrantLock();
static Condition firstCondition = REENTRANT_LOCK.newCondition();
static Condition secondCondition = REENTRANT_LOCK.newCondition();
public static void main(String[] args) throws InterruptedException {
// 可打断
lockInterrupt();
// 可超时
// timeOutLock();
// 多条件变量
// conditionTest();
}
/**
* 多条件变量
*/
public static void conditionTest() {
new Thread(() -> {
REENTRANT_LOCK.lock();
try {
firstCondition.await();
System.out.println(Thread.currentThread().getName() + ",acquire lock...");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
REENTRANT_LOCK.unlock();
}
}, "t1").start();
new Thread(() -> {
REENTRANT_LOCK.lock();
try {
firstCondition.await();
System.out.println(Thread.currentThread().getName() + ",acquire lock...");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
REENTRANT_LOCK.unlock();
}
}, "t2").start();
new Thread(() -> {
REENTRANT_LOCK.lock();
try {
// 唤醒firstCondition条件的线程
firstCondition.signalAll();
// 唤醒secondCondition条件的线程
// secondCondition.signal();
System.out.println(Thread.currentThread().getName() + ",acquire lock...");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
REENTRANT_LOCK.unlock();
}
}, "t3").start();
}
/**
* 锁超时
*
* @throws InterruptedException interruptedException
*/
public static void timeOutLock() throws InterruptedException {
Thread t1 = new Thread(() -> {
// 尝试获取锁,如果获取锁成功,返回true,否则返回false
try {
if (!REENTRANT_LOCK.tryLock(2, TimeUnit.SECONDS)) {
System.out.println("t1-获取锁失败");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
System.out.println("t1线程-获得了锁");
} finally {
REENTRANT_LOCK.unlock();
}
}, "t1");
REENTRANT_LOCK.lock();
System.out.println("主线程获得了锁");
t1.start();
try {
Thread.sleep(3000);
} finally {
REENTRANT_LOCK.unlock();
}
}
/**
* 可打断
*
* @throws InterruptedException interruptedException
*/
public static void lockInterrupt() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
// 开启可中断的锁
REENTRANT_LOCK.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("等待的过程中被打断");
return;
}
try {
System.out.println(Thread.currentThread().getName() + ",获得了锁");
} finally {
REENTRANT_LOCK.unlock();
}
}, "t1");
REENTRANT_LOCK.lock();
System.out.println("主线程获得了锁");
t1.start();
try {
Thread.sleep(1000);
t1.interrupt();
System.out.println("执行打断");
} finally {
REENTRANT_LOCK.unlock();
}
}
}
2.9 死锁产生的条件及排查方案
一个线程需要同时获取多把锁,就容易发生死锁,我们运行以下示例代码
java
package cn.edu.scau.deadlock;
import static java.lang.Thread.sleep;
public class Deadlock {
public static void main(String[] args) {
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(() -> {
synchronized (A) {
System.out.println(Thread.currentThread().getName() + "-lock A");
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (B) {
System.out.println(Thread.currentThread().getName() + "-lock B");
System.out.println(Thread.currentThread().getName() + "-操作...");
}
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (B) {
System.out.println(Thread.currentThread().getName() + "-lock B");
try {
sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (A) {
System.out.println(Thread.currentThread().getName() + "-lock A");
System.out.println(Thread.currentThread().getName() + "-操作...");
}
}
}, "t2");
t1.start();
t2.start();
}
}
可以发现,程序并没有结束,线程 t1 持有 A 的锁等待获取 B 锁,线程 t2 持有 B 的锁等待获取 A 的锁,这种现象就是死锁现象
那么该如何进行死锁诊断呢,当程序出现了死锁现象,我们可以使用 JDK 自带的工具:jps 和 jstack
- jps:输出 JVM 中运行的进程状态信息
- jstack:查看 Java 进程内线程的堆栈信息
更方便的排查方案是利用 jconsole ,以下是利用 jconsole 查看死锁情况的页面
具体的诊断方案可以参考我的另一篇博文:Java面试篇(JVM相关专题) 的 8.3 JVM 调优的工具
章节
2.10 ConcurrentHashMap
ConcurrentHashMap 是一种线程安全的高效 Map 集合
底层数据结构:
- JDK1.7 底层采用分段的数组 + 链表实现
- JDK1.8 采用的数据结构跟 HashMap 1.8 的结构一样,数组 + 链表 / 红黑二叉树
JDK1.7 中的 ConcurrentHashMap
JDK1.8 中的 ConcurrentHashMap
在 JDK 1.8 中,放弃了 Segment 的臃肿设计,ConcurrentHashMap 数据结构跟 HashMap 的数据结构是一样的:数组 + 红黑树 + 链表,采用 CAS + Synchronized 来保证并发安全进行实现
- CAS 控制数组节点的添加
- synchronized 只锁定当前链表或红黑二叉树的首节点,只要 hash 不冲突,就不会产生并发的问题,效率得到提升
2.11 导致并发程序出现问题的根本原因
Java 并发编程的三大特性:
- 原子性
- 可见性
- 有序性
2.11.1 原子性
一个线程在 CPU 中的操作不可暂停,也不可中断,要么执行完成,要么不执行
2.11.2 可见性
让一个线程对共享变量的修改对另一个线程可见
2.11.3 有序性
指令重排:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序与代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的
阅读完本文后可学习下一个篇章:Java面试篇(线程池相关专题)