系列博客目录
文章目录
- 系列博客目录
- 前言
- 1.线程的基础知识
- 1.1 线程和进程的区别?难2频3
- 面试文稿
- 1.2 并行和并发有什么区别? 难1频1
- 面试文稿
- 1.3 创建线程的四种方式 难2频4
- 面试文稿
- 1.4 runnable 和 callable 有什么区别 难2频3
- 面试文稿
- 1.5 线程的 run()和 start()有什么区别?难2频2
- 面试文稿
- 1.6 线程包括哪些状态,状态之间是如何变化的? 难3频率4
- 总结
- 面试文稿
- 1.7 新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?难2频3
- 面试文稿
- 1.8 notify()和 notifyAll()有什么区别? 2 2
- 面试文稿
- 1.9 在 java 中 wait 和 sleep 方法的不同?3 3
- 面试文稿
- 1.10 如何停止一个正在运行的线程? 2 2
- 面试文稿
- 2.线程中并发锁
- 2.1 讲一下synchronized关键字的底层原理? 5 3
- 2.1.1 基本使用
- 2.1.2 Monitor
- 面试文稿
- 2.2 synchronized关键字的底层原理-进阶 Monitor实现的锁属于重量级锁,你了解过锁升级吗?
- 2.2.1 对象的内存结构
- 2.2.2 MarkWord
- 2.2.3 再说Monitor重量级锁
- 2.2.4 轻量级锁
- 2.2.5 偏向锁
- 面试文稿
- 2.3 你谈谈 JMM(Java 内存模型) 3 3
- 面试文稿
- 2.4 CAS 你知道吗?
- 2.4.1 概述及基本工作流程
- 2.4.2 CAS 底层实现
- 2.4.3 乐观锁和悲观锁是什么?(区别)
- 面试文稿
- 2.5 请谈谈你对 volatile 的理解 3 3
- 2.5.1 保证线程间的可见性
- 2.5.2 禁止进行指令重排序
- 面试文稿
- 什么是AQS?
- 2.6.1 概述
- 2.6.2 工作机制
- 面试文稿
- 2.7 ReentrantLock的实现原理 4 3
- 2.7.1 概述
- 2.7.2 实现原理
- 工作流程
- 面试文稿
- 2.8 synchronized和Lock有什么区别 ? 4 4
- 面试文稿
- 2.9 死锁产生的条件是什么? 4 3
- 面试文稿
- 2.10 如何进行死锁诊断? 3 3
- 面试文稿
- 2.11 ConcurrentHashMap(问到线程安全的时候会被顺便问到) 3 4
- 面试文稿
- 2.12 导致并发程序出现问题的根本原因是什么
- 面试文稿
前言
线程是个难点,但是面试官很爱问。分为四部分:线程的基础知识(简单好回答),线程中并发安全(这个以及之后都难起来了),线程池,使用场景。
1.线程的基础知识
1.1 线程和进程的区别?难2频3
程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的。
当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
多实例进程就是能被打开多次,单实例进程就是只能被打开一次。
一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行。一个进程之内可以分为一到多个线程。
面试文稿
- 进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
- 不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)
1.2 并行和并发有什么区别? 难1频1
单核CPU
- 单核CPU下线程实际还是串行执行的
- 操作系统中有一个组件叫做任务调度器,将cpu的时间片(windows下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于cpu在线程间(时间片很短)的切换非常快,人类感觉是同时运行的 。
- 总结为一句话就是: 微观串行,宏观并行
一般会将这种线程轮流使用CPU的做法称为并发(concurrent)
多核CPU
每个核(core)都可以调度运行线程,这时候线程可以是并行的。
并发(concurrent)是同一时间应对(dealing with)多件事情的能力
并行(parallel)是同一时间动手做(doing)多件事情的能力
举例:
- 家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这多件事,这时就是并发
- 家庭主妇雇了个保姆,她们一起这些事,这时既有并发,也有并行(这时会产生竞争,例如锅只有一口,一个人用锅时,另一个人就得等待)
- 雇了3个保姆,一个专做饭、一个专打扫卫生、一个专喂奶,互不干扰,这时是并行
面试文稿
- 现在都是多核CPU,在多核CPU下
- 并发是同一时间应对多件事情的能力,多个线程轮流使用一个或多个CPU
- 并行是同一时间动手做多件事情的能力,4核CPU同时执行4个线程
1.3 创建线程的四种方式 难2频4
共有四种方式可以创建线程,分别是:继承Thread类、实现runnable接口、实现Callable接口、线程池创建线程
详细创建方式参考下面代码:
public class MyThread extends Thread {
@Override
public void run() {//这里是线程默认要运行的代码
System.out.println("MyThread...run...");
}
public static void main(String[] args) {
// 创建MyThread对象
MyThread t1 = new MyThread() ;
MyThread t2 = new MyThread() ;
// 调用start方法启动线程
t1.start();
t2.start();
}
}
public class MyRunnable implements Runnable{
@Override
public void run() {//无法抛异常,加上throw exception会直接报错。 可以加try,catch
System.out.println("MyRunnable...run...");
}
public static void main(String[] args) {
// 创建MyRunnable对象
MyRunnable mr = new MyRunnable() ;
// 创建Thread对象
Thread t1 = new Thread(mr) ;//用Thread包装MyRunnable的对象。
Thread t2 = new Thread(mr) ;
// 调用start方法启动线程
t1.start();
t2.start();
}
}
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {//注意这里的返回值类型要与上面的泛型一致,可以看出,callable是有返回值的。
System.out.println("MyCallable...call...");
return "OK";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建MyCallable对象
MyCallable mc = new MyCallable() ;
// 创建F 包装了Callable的对象。
FutureTask<String> ft = new FutureTask<String>(mc) ;
// 创建Thread对象
Thread t1 = new Thread(ft) ;
Thread t2 = new Thread(ft) ;
// 调用start方法启动线程
t1.start();
// 调用ft的get方法获取执行结果
String result = ft.get();
// 输出
System.out.println(result);
}
}
public class MyExecutors implements Runnable{
@Override
public void run() {
System.out.println("MyRunnable...run...");
}
public static void main(String[] args) {
// 创建线程池对象
ExecutorService threadPool = Executors.newFixedThreadPool(3);
threadPool.submit(new MyExecutors()) ;
// 关闭线程池
threadPool.shutdown();
}
}
面试文稿
在java中一共有四种常见的创建方式,分别是:继承Thread类、实现runnable接口、实现Callable接口、线程池创建线程。通常情况下,我们项目中都会采用线程池的方式创建线程。
1.4 runnable 和 callable 有什么区别 难2频3
面试文稿
- Runnable 接口run方法没有返回值;Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
- Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
- Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化(可以加try,catch),不能继续上抛
1.5 线程的 run()和 start()有什么区别?难2频2
Thread t1 = new Thread( name:"t1"){
@0verride
public void run(){ system.out.println("running....");
}
t1.run();
t1.run();
t1.run();可以,相当于调用普通方法。
t1.start()
t1.start();不可以,一个线程只能开启一次。
面试文稿
start(): 用来启动线程,通过该线程调用run方法,执行run方法中所定义的逻辑代码。start方法只能被调用一次。
run(): 封装了要被线程执行的代码,可以被调用多次。
1.6 线程包括哪些状态,状态之间是如何变化的? 难3频率4
线程的状态可以参考JDK中的Thread类中的枚举State
public enum State {
/**
* 尚未启动的线程的线程状态
*/
NEW,
/**
* 可运行线程的线程状态。处于可运行状态的线程正在 Java 虚拟机中执行,但它可能正在等待来自
* 操作系统的其他资源,例如处理器。
*/
RUNNABLE,
/**
* 线程阻塞等待监视器锁的线程状态。处于阻塞状态的线程正在等待监视器锁进入同步块/方法或在调
* 用Object.wait后重新进入同步块/方法。
*/
BLOCKED,
/**
* 等待线程的线程状态。由于调用以下方法之一,线程处于等待状态:
* Object.wait没有超时
* 没有超时的Thread.join
* LockSupport.park
* 处于等待状态的线程正在等待另一个线程执行特定操作。
* 例如,一个对对象调用Object.wait()的线程正在等待另一个线程对该对象调用Object.notify()
* 或Object.notifyAll() 。已调用Thread.join()的线程正在等待指定线程终止。
*/
WAITING,
/**
* 具有指定等待时间的等待线程的线程状态。由于以指定的正等待时间调用以下方法之一,线程处于定 * 时等待状态:
* Thread.sleep
* Object.wait超时
* Thread.join超时
* LockSupport.parkNanos
* LockSupport.parkUntil
* </ul>
*/
TIMED_WAITING,
/**
* 已终止线程的线程状态。线程已完成执行
*/
TERMINATED;
}
public enum State {
//尚未启动的线程的线程状态
NEW,
//可运行线程的线程状态。
RUNNABLE.
//线程阻塞等待监视器锁的线程状态。
BLOCKED
//等待线程的线程状态
WAITING.
//具有指定等待时间的等待线程的线程状态
TIMED_WAITING,
//已终止线程的线程状态。线程已完成执行
TERMINATED;
}
代码如上,一般流程如下,包含新建状态和死亡状态。
就绪和运行是可执行状态。
分别是
- 新建
- 当一个线程对象被创建,但还未调用 start 方法时处于新建状态
- 此时未与操作系统底层线程关联
- 可运行
- 调用了 start 方法,就会由新建进入可运行
- 此时与底层线程关联,由操作系统调度执行
- 终结
- 线程内代码已经执行完毕,由可运行进入终结
- 此时会取消与底层线程关联
- 阻塞
- 当获取锁失败后,由可运行进入 Monitor 的阻塞队列阻塞,此时不占用 cpu 时间
- 当持锁线程释放锁时,会按照一定规则唤醒阻塞队列中的阻塞线程,唤醒后的线程进入可运行状态
- 等待
- 当获取锁成功后,但由于条件不满足,调用了 wait() 方法,此时从可运行状态释放锁进入 Monitor 等待集合等待,同样不占用 cpu 时间,进入等待状态
- 当其它持锁线程调用 notify() 或 notifyAll() 方法,会按照一定规则唤醒等待集合中的等待线程,恢复为可运行状态
- 有时限等待
- 当获取锁成功后,但由于条件不满足,调用了 wait(long) 方法,此时从可运行状态释放锁进入 Monitor 等待集合进行有时限等待,同样不占用 cpu 时间
- 当其它持锁线程调用 notify() 或 notifyAll() 方法,会按照一定规则唤醒等待集合中的有时限等待线程,恢复为可运行状态,并重新去竞争锁
- 如果等待超时,也会从有时限等待状态恢复为可运行状态,并重新去竞争锁
- 还有一种情况是调用 sleep(long) 方法也会从可运行状态进入有时限等待状态,但与 Monitor 无关,不需要主动唤醒,超时时间到自然恢复为可运行状态
总结
- 线程包括哪些状态
新建(NEW)、可运行(RUNNABLE)、阻塞(BLOCKED)等待(WAITING)时间等待(TIMED_WALTING)、终止(TERMINATED) - 线程状态之间是如何变化的
创建线程对象是新建状态
调用了start()方法转变为可执行状态
线程获取到了CPU的执行权,执行结束是终止状态
在可执行状态的过程中,如果没有获取CPU的执行权,可能会切换其他状态如果没有获取锁(synchronized或lock)进入阻塞状态,获得锁再切换为可执行状态
如果线程调用了wait()方法进入等待状态,其他线程调用notify()唤醒后可切换为可执行状态
如果线程调用了sleep(50)方法,进入计时等待状态,到时间后可切换为可执行状态
面试文稿
在JDK中的Thread类中的枚举State里面定义了6中线程的状态分别是:新建、可运行、终结、阻塞、等待和有时限等待六种。
关于线程的状态切换情况比较多。我分别介绍一下。
当一个线程对象被创建,但还未调用 start 方法时处于新建状态,调用了 start 方法,就会由新建进入可运行状态。如果线程内代码已经执行完毕,由可运行进入终止状态。当然这些是一个线程正常执行情况。
如果线程获取锁失败后,会进入阻塞状态,由可运行进入 Monitor 的阻塞队列阻塞,只有当持锁线程释放锁时,会按照一定规则唤醒阻塞队列中的阻塞线程,唤醒后的线程进入可运行状态。
如果线程获取锁成功后,但由于条件不满足,调用了 wait() 方法,此时从可运行状态释放锁进入等待状态,当其它持锁线程调用 notify() 或 notifyAll() 方法,会恢复为可运行状态。
还有一种情况是调用 sleep(long) 方法也会从可运行状态进入计时(有时限)等待状态,不需要主动唤醒,超时时间到自然恢复为可运行状态。
1.7 新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?难2频3
有很多方式,一下是虽简单方便的方式。
在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程A中等待(或者寻找)另一个线程B,另外一个线程B完成(或者没有被运行)后该线程A才能继续执行。
代码举例:
为了确保三个线程的顺序你应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成。
public class JoinTest {
public static void main(String[] args) {
// 创建线程对象
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");
}) ;
// 启动线程 (顺序无所谓)。
t1.start();
t2.start();
t3.start();
}
}
面试文稿
嗯~~,我思考一下 (适当的思考或想一下属于正常情况,脱口而出反而太假[背诵痕迹])
可以这么做,在多线程中有多种方法让线程按特定顺序执行,可以用线程类的join()方法在一个线程中检测另一个线程,另外一个线程完成该线程继续执行。
比如说:
使用join方法,T3调用T2,T2调用T1,这样就能确保T1就会先完成而T3最后完成
1.8 notify()和 notifyAll()有什么区别? 2 2
public class WaitNotify {
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() ->{
synchronized (lock){
System.out.println(Thread.currentThread().getName() + "...waiting");
try{
lock.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "...被唤醒了");
}
},"t1");
Thread t2 = new Thread(() ->{
synchronized (lock){
System.out.println(Thread.currentThread().getName() + "...waiting");
try{
lock.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "...被唤醒了");
}
},"t2");
t1.start();
t2.start();
Thread.sleep(2000);
synchronized (lock){
lock.notify();//下面第二个结果是改为notifyAll()
}
}
}
t1...waiting
t2...waiting
t1...被唤醒了
t1...waiting
t2...waiting
t1...被唤醒了
t2...被唤醒了
面试文稿
notifyAll:唤醒所有wait的线程
notify:只随机唤醒一个 wait 线程
1.9 在 java 中 wait 和 sleep 方法的不同?3 3
public class WaitSleepCase {
static final Object LOCK = new Object();
public static void main(String[] args) throws InterruptedException {
sleeping();
}
private static void illegalWait() throws InterruptedException {
synchronized(LOCK){
LOCK.wait();//这时候illegalWait()被main函数调用的话,不会报错,注意要在synchronized(LOCK)的情况下使用wati()。
}
}
private static void waiting() throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (LOCK) {//与下面的synchronized (LOCK)是同一把锁,也就是只有一个synchronized (LOCK)代码块可以被执行
try {
get("t").debug("waiting...");
LOCK.wait(5000L);//wait释放锁,主线程才会获得锁。并且wait在5000L后会继续往下执行。
} catch (InterruptedException e) {
get("t").debug("interrupted...");
e.printStackTrace();
}
}
}, "t1");
t1.start();
Thread.sleep(100);
synchronized (LOCK) {
main.debug("other...");
}
}
private static void sleeping() throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (LOCK) {
try {
get("t").debug("sleeping...");
Thread.sleep(5000L);//sleep不会释放锁,主线程才会获得锁。sleep在5s后运行完毕,才会释放锁。
} catch (InterruptedException e) {
get("t").debug("interrupted...");
e.printStackTrace();
}
}
}, "t1");
t1.start();
Thread.sleep(100);
synchronized (LOCK) {
main.debug("other...");
}
}
}
面试文稿
共同点
- wait() ,wait(long即等待时间) 和 sleep(long即等待时间) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态
不同点
- 方法归属不同
- sleep(long) 是 Thread 的静态方法
- 而 wait(),wait(long) 都是 Object 的成员方法,每个对象都有
- 醒来时机不同
- 执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来
- wait(long) 和 wait() 还可以被 notify 唤醒,wait() 如果不唤醒就一直等下去,sleep(long)必须有时间,不会一直睡下去
- 它们都可以被打断唤醒
- 锁特性不同(重点)
- wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制
- wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用),而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了)
1.10 如何停止一个正在运行的线程? 2 2
第一种方式:使用退出标志,使线程正常退出。
public class MyInterrupt1 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对象
MyInterrupt1 t1 = new MyInterrupt1() ;
t1.start();
// 主线程休眠6秒
Thread.sleep(6000);
// 更改标记为true
t1.flag = true ;
}
}
输出如下
MyThread...run... 一开始进入while循环,输出后,睡3s,到下一次循环
MyThread...run...输出完后,继续睡3s,程序运行至少6s,此时flag被改,while条件不满足了,线程运行完毕
第二种方式:使用stop方法强行终止,已废除,不推荐
public class MyInterrupt2 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对象
MyInterrupt2 t1 = new MyInterrupt2() ;
t1.start();
// 主线程休眠2秒
Thread.sleep(6000);
// 调用stop方法
t1.stop();
}
}
第三种方法:
使用interrupt方法中断线程
- 打断阻塞的线程(sleep,wait,join)的线程,线程会抛出InterruptedException异常
- 打断正常的线程,可以根据打断状态来标记是否退出线程
public class MyInterrupt3 {
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) {//如果被打断了就会调用这个if里的代码
System.out.println("打断状态:"+interrupted);
break;
}
}
}, "t2");
t2.start();
Thread.sleep(500);
t2.interrupt();
}
}
面试文稿
有三种方式可以停止线程(实际开发推荐第1、3种)
- 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
- 使用stop方法强行终止(不推荐,方法已作废)
- 使用interrupt方法中断线程
- 打断阻塞的线程(sleep,wait,join)的线程,线程会抛出InterruptedException异常
- 打断正常的线程,可以根据打断状态来标记是否退出线程
2.线程中并发锁
2.1 讲一下synchronized关键字的底层原理? 5 3
2.1.1 基本使用
如下抢票的代码,如果不加锁,就会出现超卖或者一张票卖给多个人,Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。
public class TicketDemo {
static Object lock = new Object();
int ticketNum = 10;
public synchronized void getTicket() {
synchronized (this) {
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 的底层。
2.1.2 Monitor
- monitorenter 上锁开始的地方
- monitorexit 解锁的地方
- 其中被monitorenter和monitorexit包围住的指令就是上锁的代码
- 有两个monitorexit的原因,第二个monitorexit是为了防止锁住的代码抛异常后不能及时释放锁
在使用了synchornized代码块时需要指定一个对象,所以synchornized也被称为对象锁。
monitor主要就是跟这个对象产生关联,如下图
Monitor内部具体的存储结构:
- Owner:存储当前获取锁的线程的,只能有一个线程可以获取
- EntryList:关联没有抢到锁的线程,处于Blocked状态的线程
- WaitSet:关联调用了wait方法的线程,处于Waiting状态的线程
具体的流程:
- 代码进入synchorized代码块,先让lock(对象锁)关联的monitor,去判断Owner是否有线程持有这个锁。
- 如果没有线程持有(也就是Owner为null),则让当前线程持有,表示该线程获取锁成功
- 如果有线程持有,则让当前线程进入entryList进行阻塞,如果Owner持有的线程已经释放了锁,在EntryList中的线程去竞争锁的持有权(非公平)
- 如果代码块中调用了wait()方法,则会进去WaitSet中进行等待
面试文稿
Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】。
Synchronized底层使用的JVM级别中的Monitor 来决定当前线程是否获得了锁,如果某一个线程获得了锁,在没有释放锁之前,其他线程是不能或得到锁的。Synchronized 属于悲观锁。
Synchronized因为需要依赖于JVM级别的Monitor ,相对性能也比较低。
monitor对象存在于每个Java对象的对象头中,Synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因
monitor内部维护了三个变量
- WaitSet:保存处于Waiting状态的线程
- EntryList:保存处于Blocked状态的线程
- Owner:持有锁的线程
只有一个线程获取到的标志就是在monitor中设置成功了Owner,一个monitor中只能有一个Owner。
在上锁的过程中,如果有其他线程也来抢锁,则进入EntryList 进行阻塞,当获得锁的线程执行完了,释放了锁,就会唤醒EntryList 中等待的线程竞争锁,竞争的时候是非公平的。
(GPT:悲观锁:假设会发生冲突,通过加锁来确保资源的独占。乐观锁:假设不会发生冲突,线程在操作资源时不加锁,而是执行操作后检查是否发生冲突(通常使用版本号、时间戳等机制来检查)
2.2 synchronized关键字的底层原理-进阶 Monitor实现的锁属于重量级锁,你了解过锁升级吗?
- Monitor实现的锁属于重量级锁,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。(如果一个锁,只有一个进程来重复获取释放它,那样的话,上下锁成本太高,解决方法如下)。
- 在JDK 1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。
之前的时候我们默认对象锁管理moniter,那是如何实现的呢?(实际上还是与java的对象有关)
2.2.1 对象的内存结构
在HotSpot虚拟机(java一般都是用这个虚拟机)中,对象在内存中存储的布局可分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充。
我们需要重点分析MarkWord对象头。
2.2.2 MarkWord
- hashcode:25位的对象标识Hash码
- age:对象分代年龄占4位
- biased_lock:偏向锁标识,占1位 ,0表示没有开始偏向锁,1表示开启了偏向锁
- thread:持有偏向锁的线程ID,占23位
- epoch:偏向时间戳,占2位
- ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针,占30位
- ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针,占30位
我们可以通过lock的标识,来判断是哪一种锁的等级
- 后三位是001表示无锁
- 后三位是101表示偏向锁
- 后两位是00表示轻量级锁
- 后两位是10表示重量级锁
2.2.3 再说Monitor重量级锁
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针。
简单说就是:每个对象的对象头都可以设置monoitor的指针,让对象与monitor产生关联
2.2.4 轻量级锁
在很多的情况下,在Java程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁是没必要的。(如下方代码,如果一个线程调用了method1方法,method1方法又调用了method2方法,对锁进行了重入,这时候由于是一个线程,不存在竞争关系,所以没有必要用重量级锁)因此JVM引入了轻量级锁的概念。
static final Object obj = new Object();
public static void method1() {
synchronized (obj) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized (obj) {
// 同步块 B
}
}
一开始Object对象的内容是这样的,还没有上锁。
1.在线程栈中创建一个Lock Record,将其obj字段指向锁对象,并且存储锁定对象的MarkWord。
2.如下图所示,通过CAS指令将Lock Record的地址存储在对象头的mark word中(数据进行交换),如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。
3.如果是当前线程已经持有该锁了,代表这是一次锁重入。(每次都要进行CAS操作,但是重入的时候不用真正修改,只要是添加一次记录,并且)设置Lock Record第一部分为null,起到了一个重入计数器的作用。
4.如果CAS修改失败,说明发生了竞争,需要膨胀为重量级锁。
解锁过程
1.遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。
2.如果Lock Record的Mark Word为null,代表这是一次重入,将obj设置为null后continue。(也就是删除了一次记录)
3.如果Lock Record的 Mark Word不为null,则利用CAS指令将对象头的mark word恢复成为无锁状态(再交换一次数据)。如果失败则膨胀为重量级锁。(GPT:如果在 CAS 操作前,其他线程已经修改了这个对象的 Mark Word(比如已经将其修改为重量级锁的标志),则 CAS 操作会失败。)
2.2.5 偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现
这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。
static final Object obj = new Object();
public static void m1() {
synchronized (obj) {
// 同步块 A
m2();
}
}
public static void m2() {
synchronized (obj) {
// 同步块 B
m3();
}
}
public static void m3() {
synchronized (obj) {
}
}
加锁的流程
1.在线程栈中创建一个Lock Record,将其obj字段指向锁对象。
2.通过CAS指令将Lock Record的线程id存储在对象头的mark word中,同时也设置偏向锁的标识为101,如果对象处于无锁状态则修改成功,代表该线程获得了偏向锁。
3.如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。与轻量级锁不同的时,这里不会再次进行cas操作,只是判断对象头中的线程id是否是自己,因为缺少了cas操作,性能相对轻量级锁更好一些。
面试文稿
Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。
重量级锁:底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
轻量级锁:线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性。
偏向锁:一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令。
一旦锁发生了竞争,都会升级为重量级锁。
2.3 你谈谈 JMM(Java 内存模型) 3 3
JMM(Java Memory Model)Java内存模型,是java虚拟机规范中所定义的一种内存模型。
Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。
特点:
- 所有的共享变量都存储于主内存(计算机的RAM)这里所说的变量指的是实例变量和类变量。不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。
- 每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本,只能被线程自己本身所调用,不存在安全问题。
- 线程对变量的所有的操作(读,写)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存完成。
总结:
你谈谈 JMM(Java内存模型)
- JMM(ava Memory Model)Java内存模型,定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性
- JMM把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)
- 线程跟线程之间是相互隔离,线程跟线程交互需要通过主内存
面试文稿
Java内存模型是Java虚拟机规范中定义的一种非常重要的内存模型。它的主要作用是描述Java程序中线程共享变量的访问规则,以及这些变量在JVM中是如何被存储和读取的,涉及到一些底层的细节。
这个模型有几个核心的特点。首先,所有的共享变量,包括实例变量和类变量,都被存储在主内存中,也就是计算机的RAM。需要注意的是,局部变量并不包含在内,因为它们是线程私有的,所以不存在竞争问题。
其次,每个线程都有自己的工作内存,这里保留了线程所使用的变量的工作副本。这意味着,线程对变量的所有操作,无论是读还是写,都必须在自己的工作内存中完成,而不能直接读写主内存中的变量。
最后,不同线程之间不能直接访问对方工作内存中的变量。如果线程间需要传递变量的值,那么这个过程必须通过主内存来完成。
2.4 CAS 你知道吗?
2.4.1 概述及基本工作流程
CAS的全称是: Compare And Swap(比较再交换),它体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性。
在JUC( java.util.concurrent )包下实现的很多类都用到了CAS操作
- AbstractQueuedSynchronizer(AQS框架)
- AtomicXXX类
例子:
我们还是基于刚才学习过的JMM内存模型进行说明
- 线程1与线程2都从主内存中获取变量int a = 100,同时放到各个线程的工作内存中
一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当旧的预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。如果CAS操作失败,通过自旋的方式等待并再次尝试,直到成功
- 线程1操作:V:int a = 100,A:int a = 100,B:修改后的值:int a = 101 (a++)
- 线程1拿A的值与主内存V的值进行比较,判断是否相等
- 如果相等,则把B的值101更新到主内存中
- 线程2操作:V:int a = 100,A:int a = 100,B:修改后的值:int a = 99(a - -)
- 线程2拿A的值与主内存V的值进行比较,判断是否相等(目前不相等,因为线程1已更新V的值101)
- 不相等,则线程2更新失败
开始执行自旋锁操作,即不断拿去新的共享变量,更改后,对比旧值,看看是否可以把自己更改后的值放到内存中。
- 自旋锁操作
- 因为没有加锁,所以线程不会陷入阻塞,效率较高
- 如果竞争激烈,重试频繁发生,效率会受影响
需要不断尝试获取共享内存V中最新的值,然后再在新的值的基础上进行更新操作,如果失败就继续尝试获取新的值,直到更新成功
2.4.2 CAS 底层实现
CAS 底层依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令(如下图所示)。
都是native修饰的方法,由系统提供的接口执行,并非java代码实现,一般的思路也都是自旋锁实现
在java中比较常见使用有很多,比如ReentrantLock和Atomic开头的线程安全类,都调用了Unsafe中的方法
- ReentrantLock中的一段CAS代码
当前值对应前面的 内存总中的值V,期望的值对应A,更行后的值对应B。
2.4.3 乐观锁和悲观锁是什么?(区别)
面试官可能根据CAS是乐观锁,来问什么是乐观悲观锁。
- CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
- synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
面试文稿
CAS的全称是: Compare And Swap(比较再交换);它体现的一种乐观锁的思想,在无锁状态下保证线程操作数据的原子性。
- CAS使用到的地方很多:AQS框架(Abstract Queued Synchronizer)、AtomicXXX类
- 在操作共享变量的时候使用的自旋锁,效率上更高一些
- CAS的底层是调用的Unsafe类中的方法,都是操作系统提供的,其他语言实现
2.5 请谈谈你对 volatile 的理解 3 3
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
2.5.1 保证线程间的可见性
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。用 volatile 修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见。
举个例子如下:有三个线程。
线程1修改了变量之后,线程2是可以读到的,为什么线程3不行呢?
问题分析:主要是因为在JVM虚拟机中有一个JT(即时编译器)给代码做了优化。
解决方案:
第一:在程序运行的时候加入vm参数-Xint表示禁用即时编辑器,不推荐,得不偿失(其他代码还要使用)
第二:在修饰stop变量的时候加上volatile,表示当前代码禁用了即时编辑器,问题就可以解决,
代码如下:static volatile boolean stop = false;
效果如下。
2.5.2 禁止进行指令重排序
用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果
在去获取上面的结果的时候,有可能会出现4种情况
情况一:先执行actor2获取结果—>0,0(正常)
情况二:先执行actor1中的第一行代码,然后执行actor2获取结果—>0,1(正常)
情况三:先执行actor1中所有代码,然后执行actor2获取结果—>1,1(正常)
情况四:先执行actor1中第二行代码,然后执行actor2获取结果—>1,0(发生了指令重排序,影响结果)
解决方案
在变量上添加volatile,禁止指令重排序,则可以解决问题
屏障添加的示意图
注意尖刺的方向。
- 写操作加的屏障是阻止上方其它写操作越过屏障排到volatile变量写之下
- 读操作加的屏障是阻止下方其它读操作越过屏障排到volatile变量读之上
其他补充
我们上面的解决方案是把volatile加在了int y这个变量上,我们能不能把它加在int x这个变量上呢?
下面代码使用volatile修饰了x变量
屏障添加的示意图
这样显然是不行的,主要是因为下面两个原则:
- 写操作加的屏障是阻止上方其它写操作越过屏障排到volatile变量写之下
- 读操作加的屏障是阻止下方其它读操作越过屏障排到volatile变量读之上
所以,现在我们就可以总结一个volatile使用的小妙招: - 写变量让volatile修饰的变量的在代码最后位置
- 读变量让volatile修饰的变量的在代码最开始位置
面试文稿
volatile 是一个关键字,可以修饰类的成员变量、类的静态成员变量,主要有两个功能
第一:保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。
第二: 禁止进行指令重排序,可以保证代码执行有序性。底层实现原理是,添加了一个内存屏障,通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化
什么是AQS?
2.6.1 概述
全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架,它是构建锁或者其他同步组件的基础框架 。其实就是用作锁。
AQS与Synchronized的区别
AQS常见的实现类
- ReentrantLock 阻塞式锁
- Semaphore 信号量
- CountDownLatch 倒计时锁
2.6.2 工作机制
- 在AQS中维护了一个使用了volatile修饰的state属性来表示资源的状态,0表示无锁,1表示有锁
- 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList
- 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet
- 线程0来了以后,去尝试修改state属性,如果发现state属性是0,就修改state状态为1,表示线程0抢锁成功
- 线程1和线程2也会先尝试修改state属性,发现state的值已经是1了,有其他线程持有锁,它们都会到FIFO队列中进行等待,
- FIFO是一个双向队列,head属性表示头结点,tail表示尾结点
如果多个线程共同去抢这个资源是如何保证原子性的呢?
在去修改state状态的时候,使用的cas自旋锁来保证原子性,确保只能有一个线程修改成功,修改失败的线程将会进入FIFO队列中等待
AQS是公平锁吗,还是非公平锁?
- 新的线程与队列中的线程共同来抢资源,是非公平锁
- 新的线程到队列中等待,只让队列中的head线程获取锁,是公平锁
比较典型的AQS实现类ReentrantLock,它默认就是非公平锁,新的线程与队列中的线程共同来抢资源。
面试文稿
什么是AQS?
- 是多线程中的队列同步器。是一种锁机制,它是做为一个基础框架使用的,像ReentrantLock、Semaphore都是基于AQS实现的
- AQS内部维护了一个先进先出的双向队列,队列中存储排队的线程。
- 在AQS内部还有一个属性state,这个state就相当于是一个资源,默认是0(无锁状态),如果队列中的有一个线程修改成功了state为1,则当前线程就相等于获取了资源。
- 在对state修改的时候使用的cas操作,保证多个线程修改的情况下原子性。
2.7 ReentrantLock的实现原理 4 3
2.7.1 概述
ReentrantLock翻译过来是可重入锁,相对于synchronized它具备以下特点:
- 可中断
- 可以设置超时时间
- 可以设置公平锁
- 支持多个条件变量
- 与synchronized一样,都支持重入
2.7.2 实现原理
ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似。
构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。
查看ReentrantLock源码中的构造方法:
提供了两个构造方法,不带参数的默认为非公平。
如果使用带参数的构造函数,并且传的值为true,则是公平锁。
其中NonfairSync和FairSync这两个类父类都是Sync,而Sync的父类是AQS(如下图所示),所以可以得出ReentrantLock底层主要实现就是基于AQS来实现的。
工作流程
- 线程来抢锁后使用cas的方式修改state状态,修改状态成功为1,则让exclusiveOwnerThread属性指向当前线程,获取锁成功。
- 假如修改状态失败,则会进入双向队列中等待,head指向双向队列头部,tail指向双向队列尾部
- 当exclusiveOwnerThread为null的时候,则会唤醒在双向队列中等待的线程
- 公平锁则体现在按照先后顺序获取锁,非公平体现在不在排队的线程也可以抢锁。
面试文稿
ReentrantLock是一个可重入锁: 一个线程调用 lock 方法获取了锁之后,再次调用 lock,是不会再阻塞,内部直接增加重入次数就行了,标识这个线程已经重复获取一把锁而不需要等待锁的释放。
ReentrantLock是属于juc包下的类,跟synchronized一样,都是悲观锁。属于api层面的锁,通过lock()用来获取锁,unlock()释放锁。
它的底层实现原理主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似。
构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。公平锁的效率往往没有非公平锁的效率高。
2.8 synchronized和Lock有什么区别 ? 4 4
要对两个所有充分的认识,才能回答好。
第一,语法层面
- synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现,退出同步代码块锁会自动释放
- Lock 是接口,源码由 jdk 提供,用 java 语言实现,需要手动调用 unlock 方法释放锁
第二,功能层面
- 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
- Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态、公平锁、可(在等待过程中被)打断、可超时、多条件变量,同时Lock 可以实现不同的场景,如 ReentrantLock, ReentrantReadWriteLock。
/**
* ClassName: LockInterruptiblyDemo
* Package: PACKAGE_NAME
* Description:
*
* @Author 醒了就刷牙
* @Create 2024/12/21 12:23
* @Version 1.0
*/
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.*;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockTest {
//创建锁对象
static ReentrantLock lock=new ReentrantLock();
//条件1
static Condition c1 =lock.newCondition();
static Condition c2 =lock.newCondition();
public static void main(String[] args) throws InterruptedException {
// lockInterrupt();
time0utLock();
}
public static void time0utLock()throws InterruptedException {
Thread t1 = new Thread(() -> {
//设置trylock的时间使其一直尝试,如果不设置,就是尝试一下,不满足的话直接执行获取不到锁的代码。
try {
if(!lock.tryLock(3000, TimeUnit.SECONDS)){
System.out.println("t1-获取锁失败");
return;
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
System.out.println("t1线程-获得了锁");
}finally {
lock.unlock();
}
},"t1");
lock.lock();
System.out.println("主线程获得了锁");
t1.start();
try {
Thread.sleep(4000);
}finally {
lock.unlock();
}
}
public static void lockInterrupt()throws InterruptedException {
Thread t1=new Thread(()->{
try {
//开启可中断的锁
lock.lockInterruptibly();
}catch(InterruptedException e){
e.printStackTrace();
System.out.println("等待的过程中被打断");
return;
}
try {
System.out.println(Thread.currentThread().getName()+",获得了锁");
} finally {
lock.unlock();
}
},"t1");
lock.lock();
System.out.println("主线程获得了锁");
t1.start();
try {
Thread.sleep(1000);
t1.interrupt();
System.out.println("主线程执行打断");
}finally {
lock.unlock();
}
}
}
多条件变量 就是 一个线程中使用 c1.await()进入等待,等其他进程调用c1.signal()唤醒它,
一个c1.signal只能唤醒一个由于c1进入等待的进程,signalAll()可以唤醒所有。
第三,性能层面
- 在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖。
- 在竞争激烈时,Lock 的实现通常会提供更好的性能。
统合来看,需要根据不同的场景来选择不同的锁的使用。
面试文稿
第一,语法层面
- synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现,退出同步代码块锁会自动释放
- Lock 是接口,源码由 jdk 提供,用 java 语言实现,需要手动调用 unlock 方法释放锁
第二,功能层面
- 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
- Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量,同时Lock 可以实现不同的场景,如 ReentrantLock, ReentrantReadWriteLock
第三,性能层面
- 在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖
- 在竞争激烈时,Lock 的实现通常会提供更好的性能
统合来看,需要根据不同的场景来选择不同的锁的使用。
2.9 死锁产生的条件是什么? 4 3
死锁:一个线程需要同时获取多把锁,这时就容易发生死锁
例如:
t1 线程获得A对象锁,接下来想获取B对象的锁
t2 线程获得B对象锁,接下来想获取A对象的锁
package com.itheima.basic;
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("lock A");
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (B) {
System.out.println("lock B");
System.out.println("操作...");
}
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (B) {
System.out.println("lock B");
try {
sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (A) {
System.out.println("lock A");
System.out.println("操作...");
}
}
}, "t2");
t1.start();
t2.start();
}
}
控制台输出结果
此时程序并没有结束,这种现象就是死锁现象…线程t1持有A的锁等待获取B锁,线程t2持有B的锁等待获取A的锁。
面试文稿
嗯,是这样的,一个线程需要同时获取多把锁,这时就容易发生死锁,举个例子来说:
t1 线程获得A对象锁,接下来想获取B对象的锁
t2 线程获得B对象锁,接下来想获取A对象的锁
这个时候t1线程和t2线程都在互相等待对方的锁,就产生了死锁。
2.10 如何进行死锁诊断? 3 3
当程序出现了死锁现象,我们可以使用jdk自带的工具:jps和 jstack
步骤如下:
第一:查看运行的线程
第二:使用jstack查看线程运行的情况,下图是截图的关键信息
运行命令:jstack -l 46032
其他解决工具,可视化工具
- jconsole
用于对jvm的内存,线程,类 的监控,是一个基于 jmx 的 GUI 性能监控工具
打开方式:java 安装目录 bin目录下 直接启动 jconsole.exe 就行
- VisualVM:故障处理工具
能够监控线程,内存情况,查看方法的CPU时间和内存中的对 象,已被GC的对象,反向查看分配的堆栈
打开方式:java 安装目录 bin目录下 直接启动 jvisualvm.exe就行
面试文稿
- 死锁产生的条件是什么?
- 一个线程需要同时获取多把锁,这时就容易发生死锁
- 如何进行死锁诊断?
- 当程序出现了死锁现象,我们可以使用jdk自带的工具:jps和jstack
- jps:输出JVM中运行的进程状态信息
- jstack:查看java进程内线程的堆栈信息,查看日志,检查是否有死锁如果有死锁现象,需要查看具体代码分析后,可修复
- 可视化工具jconsole、VisualVM也可以检查死锁问题
2.11 ConcurrentHashMap(问到线程安全的时候会被顺便问到) 3 4
ConcurrentHashMap 是一种线程安全的高效Map集合
底层数据结构:
- JDK1.7底层采用分段的数组+链表实现
- JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。
(1) JDK1.7中concurrentHashMap
数据结构
- 提供了一个segment数组,在初始化ConcurrentHashMap 的时候可以指定数组的长度,默认是16,一旦初始化之后中间不可扩容
- 在每个segment中都可以挂一个HashEntry数组,数组里面可以存储具体的元素,HashEntry数组是可以扩容的
- 在HashEntry存储的数组中存储的元素,如果发生冲突,则可以挂单向链表
存储流程
- 先去计算key的hash值,然后确定segment数组下标
- 再通过hash值确定hashEntry数组中的下标存储数据
- 在进行操作数据的之前,会先判断当前segment对应下标位置是否有线程进行操作,为了线程安全使用的是ReentrantLock进行加锁,如果获取锁是被会使用cas自旋锁进行尝试
(2) JDK1.8中concurrentHashMap
在JDK1.8中,放弃了Segment臃肿的设计,数据结构跟HashMap的数据结构是一样的:数组+红黑树+链表
采用 CAS + Synchronized来保证并发安全进行实现
- CAS控制数组节点的添加
- synchronized只锁定当前链表或红黑二叉树的首节点,只要hash不冲突,就不会产生并发的问题 , 效率得到提升
面试文稿
ConcurrentHashMap 是一种线程安全的高效Map集合,jdk1.7和1.8也做了很多调整。
- JDK1.7的底层采用是分段的数组+链表 实现
- JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。
在jdk1.7中 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一 种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构 的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修 改时,必须首先获得对应的 Segment的锁。
Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元 素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁
在jdk1.8中的ConcurrentHashMap 做了较大的优化,性能提升了不少。首先是它的数据结构与jdk1.8的hashMap数据结构完全一致。其次是放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保 证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲 突,就不会产生并发 , 效率得到提升
2.12 导致并发程序出现问题的根本原因是什么
面试文稿
Java并发编程有三大核心特性,分别是原子性、可见性和有序性。
首先,原子性指的是一个线程在CPU中的操作是不可暂停也不可中断的,要么执行完成,要么不执行。比如,一些简单的操作如赋值可能是原子的,但复合操作如自增就不是原子的。为了保证原子性,我们可以使用synchronized关键字或JUC里面的Lock来进行加锁。
其次,可见性是指让一个线程对共享变量的修改对另一个线程可见。由于线程可能在自己的工作内存中缓存共享变量的副本,因此一个线程对共享变量的修改可能不会立即反映在其他线程的工作内存中。为了解决这个问题,我们可以使用synchronized关键字、volatile关键字或Lock来确保可见性。
最后,有序性是指处理器为了提高程序运行效率,可能会对输入代码进行优化,导致程序中各个语句的执行先后顺序与代码中的顺序不一致。虽然处理器会保证程序最终执行结果与代码顺序执行的结果一致,但在某些情况下我们可能需要确保特定的执行顺序。为了解决这个问题,我们可以使用volatile关键字来禁止指令重排。