新版Java面试专题视频教程——多线程篇①
- = = = = = = = = = = = = = = = = Java多线程相关面试题 = = = = = = = = = = = = = = = =
- 0. 问题汇总
- 0.1 线程的基础知识
- 0.2 线程中并发安全
- 1.线程的基础知识
- 1.1 线程和进程的区别?
- 1.2 并行和并发有什么区别?
- 1.3 创建线程的四种方式
- 1.4 runnable 和 callable 有什么区别
- 1.5 线程的 run()和 start()有什么区别?
- 1.6 线程包括哪些状态,状态之间是如何变化的
- 1.7 新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?
- 1.8 notify()和 notifyAll()有什么区别?
- 1.9 在 java 中 wait 和 sleep 方法的不同?
- 1.10 如何停止一个正在运行的线程?
- 2.线程中并发锁
- 2.1 讲一下synchronized关键字的底层原理?
- 2.1.1 基本使用
- 2.1.2 Monitor
- 2.2 synchronized关键字的底层原理-进阶
- 2.2.1 对象的内存结构
- 2.2.2 MarkWord
- 2.2.3 再说Monitor重量级锁
- 2.2.4 轻量级锁
- 2.2.5 偏向锁
- 2.2.6 Monitor实现的锁属于重量级锁,你了解过锁升级吗?——参考回答
- 2.3你谈谈 JMM(Java 内存模型)
- 2.4 CAS 你知道吗?
- 2.4.1 概述及基本工作流程
- 2.4.2 CAS 底层实现
- 2.4.3 乐观锁和悲观锁的区别
- 2.5 请谈谈你对 volatile 关键字的理解
- 2.5.1 保证线程间的可见性
- 2.5.2 禁止进行指令重排序
- 2.6 什么是AQS?
- 2.6.1 概述
- 2.6.2 基本工作机制
- 2.5 ReentrantLock的实现原理
- 2.5.1 概述
- 2.5.2 实现原理
- 2.6 synchronized和Lock有什么区别 ?
- 2.7 死锁产生的条件是什么?
- 2.8 如何进行死锁诊断?
- 2.10 ConcurrentHashMap
- 2.11 导致并发程序出现问题的根本原因是什么
= = = = = = = = = = = = = = = = Java多线程相关面试题 = = = = = = = = = = = = = = = =
0. 问题汇总
0.1 线程的基础知识
线程与进程的区别
并行与并发的区别
线程创建的方式有哪些
runnable和callable有什么区别
线程包括哪些状态
状态之间是如何变化的
在java中wait和sleep方法的不同
新建三个线程,如何保证它们按顺序执行
notify和notifyAll有什么区别
线程的run()和start()有什么区别
如何停止一个正在运行的线程
0.2 线程中并发安全
synchronized关键字的底层原理
你谈谈JMM (Java 内存模型)
CAS你知道吗
什么是AQS
ReentrantLock的实现原理
synchronized和Lock有什么区别
死锁产生的条件是什么
如何进行死锁诊断
请谈谈你对volatile的理解
聊一下ConcurrentHashMap
导致并发程序出现问题的根本原因是什么
1.线程的基础知识
1.1 线程和进程的区别?
难易程度:☆☆
出现频率:☆☆☆
程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理IO的。
当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
一个进程之内可以分为一到多个线程。
一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。
在 windows 中进程是不活动的,只是作为线程的容器
二者对比
-
进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
-
不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
-
线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)
1.2 并行和并发有什么区别?
难易程度:☆ ☆
出现频率:☆ ☆
多核CPU
并行和并发有什么区别?
- 并发(concurrent) 是同一时间应对(dealing with)多件事情的能力
- 并行(parallel) 是同一时间动手做(doing) 多件事情的能力
举例:
家庭主妇做饭、打扫卫生、给孩子喂奶,她 一个人轮流交替做这多件事,这时就是并发
家庭主妇雇了个保姆,她们一起这些事,这时既有并发,也有并行(这时会产生竞争,例如(并发)锅只有一口,一个人用锅时,另一个人就得等待)
雇了3个保姆,一个专做饭、一个专打扫卫生、一个专喂奶,互不干扰,这时是并行
1.3 创建线程的四种方式
难易程度:☆ ☆
出现频率:☆ ☆ ☆ ☆
参考回答:
共有四种方式可以创建线程,分别是:
继承Thread类、实现runnable接口、实现Callable接口、线程池创建线程(项目中常用)
详细创建方式参考下面代码:
① 继承Thread类
public class MyThread extends Thread {@Overridepublic 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();}
}
② 实现runnable接口
public class MyRunnable implements Runnable{@Overridepublic void run() {System.out.println("MyRunnable...run...");}public static void main(String[] args) {// 创建MyRunnable对象MyRunnable mr = new MyRunnable() ;// 创建Thread对象Thread t1 = new Thread(mr) ;Thread t2 = new Thread(mr) ;// 调用start方法启动线程t1.start();t2.start();}
}
③ 实现Callable接口 适用于:当前线程执行完 需要返回结果
public class MyCallable implements Callable<String> {@Overridepublic String call() throws Exception {//返回值与泛型相同System.out.println("MyCallable...call...");return "OK";}public static void main(String[] args) throws ExecutionException, InterruptedException {// 创建MyCallable对象MyCallable mc = new MyCallable() ;// 创建 FutureTask 包装mycallableFutureTask<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{@Overridepublic void run() {System.out.println("MyRunnable...run...");}public static void main(String[] args) {// 创建线程池对象ExecutorService threadPool = Executors.newFixedThreadPool(3);threadPool.submit(new MyExecutors()) ;//提交任务// 关闭线程池threadPool.shutdown();}
}
1.4 runnable 和 callable 有什么区别
难易程度:☆☆
出现频率:☆☆☆
参考回答:
Runnable 接口run方法没有返回值;Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
Callable接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
Callable接口的call()方法允许throw抛出异常;而Runnable接口的run()方法的异常只能在内部消化(try catch),不能继续上抛
1.5 线程的 run()和 start()有什么区别?
难易程度:☆☆
出现频率:☆☆
start(): 用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次。
run(): 封装了要被线程执行的代码,普通方法当然 可以被调用多次。
1.6 线程包括哪些状态,状态之间是如何变化的
难易程度:☆☆☆
出现频率:☆☆☆☆
线程的状态可以参考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;
}
状态之间是如何变化的
分别是
-
新建
-
当一个线程对象被创建,但还未调用 start 方法时处于新建状态
-
此时未与操作系统底层线程关联
-
-
可运行
-
调用了 start 方法,就会由新建进入可运行
-
此时与底层线程关联,由操作系统调度执行
-
-
终结
-
线程内代码已经执行完毕,由可运行进入终结
-
此时会取消与底层线程关联
-
-
阻塞
-
当获取锁失败后,由可运行进入 Monitor 的阻塞队列阻塞,此时不占用 cpu 时间
-
当持锁线程释放锁时,会按照一定规则唤醒阻塞队列中的阻塞线程,唤醒后的线程进入可运行状态
-
-
等待
-
当获取锁成功后,但由于条件不满足,调用了 wait() 方法,此时从可运行状态释放锁进入 Monitor 等待集合等待,同样不占用 cpu 时间
-
当其它持锁线程调用 notify() 或 notifyAll() 方法,会按照一定规则唤醒等待集合中的等待线程,恢复为可运行状态
-
-
有时限等待
-
当获取锁成功后,但由于条件不满足,调用了 wait(long) 方法,此时从可运行状态释放锁进入 Monitor 等待集合进行有时限等待,同样不占用 cpu 时间
-
当其它持锁线程调用 notify() 或 notifyAll() 方法,会按照一定规则唤醒等待集合中的有时限等待线程,恢复为可运行状态,并重新去竞争锁
-
如果等待超时,也会从有时限等待状态恢复为可运行状态,并重新去竞争锁
-
还有一种情况是调用 sleep(long) 方法也会从可运行状态进入有时限等待状态,
但与 Monitor 无关,不需要主动唤醒,超时时间到自然恢复为可运行状态
1.7 新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?
难易程度:☆☆
出现频率:☆☆☆
在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。
代码举例:
为了确保三个线程的顺序你应该先启动最后一个(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();}
}
1.8 notify()和 notifyAll()有什么区别?
难易程度:☆☆
出现频率:☆☆
notifyAll:唤醒所有wait的线程
notify:只 随机
唤醒一个wait 线程
package com.itheima.basic;
public class WaitNotify {static boolean flag = false;static Object lock = new Object();public static void main(String[] args) {Thread t1 = new Thread(() -> {synchronized (lock){while (!flag){System.out.println(Thread.currentThread().getName()+"...wating...");try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName()+"...flag is true");}});Thread t2 = new Thread(() -> {synchronized (lock){while (!flag){System.out.println(Thread.currentThread().getName()+"...wating...");try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName()+"...flag is true");}});Thread t3 = new Thread(() -> {synchronized (lock) {System.out.println(Thread.currentThread().getName() + " hold lock");lock.notifyAll();flag = true;try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}});t1.start();t2.start();t3.start();}
}
1.9 在 java 中 wait 和 sleep 方法的不同?
难易程度:☆☆☆
出现频率:☆☆☆
参考回答:
共同点
- wait() ,wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态
不同点
- 方法归属不同
- sleep(long) 是 Thread 的静态方法
- 而 wait(),wait(long) 都是 Object 的成员方法,每个对象都有
- 醒来时机不同
- 执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来
- wait(long) 和 wait() 还可以被 notify 唤醒,wait() 如果不唤醒就一直等下去
- 它们都可以被打断唤醒
- 锁特性不同(重点)
- wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制
- wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用)
- 而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了)
代码示例:
public class WaitSleepCase {static final Object LOCK = new Object();public static void main(String[] args) throws InterruptedException {sleeping();}private static void illegalWait() throws InterruptedException {LOCK.wait();//没和synchronized配合使用 直接报错}private static void illegalWait() throws InterruptedException {synchronized(LOCK){LOCK.wait();}}
//wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用)private static void waiting() throws InterruptedException {Thread t1 = new Thread(() -> {synchronized (LOCK) {try {get("t").debug("waiting...");LOCK.wait(5000L);//5秒之后 从阻塞跳出 可运行状态} catch (InterruptedException e) {get("t").debug("interrupted...");e.printStackTrace();}}}, "t1");t1.start();Thread.sleep(100);synchronized (LOCK) {main.debug("other...");}}
//而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了)private static void sleeping() throws InterruptedException {Thread t1 = new Thread(() -> {synchronized (LOCK) {try {get("t").debug("sleeping...");Thread.sleep(5000L);} catch (InterruptedException e) {get("t").debug("interrupted...");e.printStackTrace();}}}, "t1");t1.start();Thread.sleep(100);synchronized (LOCK) {main.debug("other...");}}
}
1.10 如何停止一个正在运行的线程?
难易程度:☆☆
出现频率:☆☆
参考回答:
有三种方式可以停止线程
- 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
- 使用stop方法强行终止(不推荐,方法已作废)
- 使用interrupt方法中断线程
代码参考如下:
① 使用退出标志,使线程正常退出。
public class MyInterrupt1 extends Thread {volatile boolean flag = false ; // 线程执行的退出标记@Overridepublic 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);//6秒之后 正好打印两次 改flag 退出// 更改标记为truet1.flag = true ;}
}
在这段代码中,主线程创建了一个名为 t1
的 MyInterrupt1
线程,并启动了它。然后主线程休眠了6秒钟。在这6秒钟内,MyInterrupt1
线程会一直循环执行打印语句 System.out.println("MyThread...run...");
,每次循环之间休眠3秒钟。
当主线程休眠6秒钟后,主线程将 t1
线程的 flag
属性设置为 true
,这样在 MyInterrupt1
线程的下一次循环迭代时,由于 flag
被设置为 true
,循环条件 while(!flag)
将不再满足,MyInterrupt1
线程退出循环并终止。
因此,在主线程休眠结束之后,会再打印一次 "MyThread...run..."
,然后 MyInterrupt1
线程结束。这就解释了为什么会打印两次 "MyThread...run..."
。
② 使用stop方法强行终止(不推荐)
public class MyInterrupt2 extends Thread {volatile boolean flag = false ; // 线程执行的退出标记@Overridepublic 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方法中断线程。
package com.itheima.basic;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) {//默认为false 调用interrupt之后改为trueSystem.out.println("打断状态:"+interrupted);break;}}}, "t2");t2.start();Thread.sleep(500);t2.interrupt();}
}
2.线程中并发锁
2.1 讲一下synchronized关键字的底层原理?
难易程度:☆☆☆☆☆
出现频率:☆☆☆
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();}}
}
2.1.2 Monitor
Monitor 被翻译为监视器,是由jvm提供,c++语言实现
在代码中想要体现monitor需要借助javap命令查看clsss的字节码,比如以下代码:
public class SyncTest {static final Object lock = new Object();static int counter = 0;public static void main(String[] args) {synchronized (lock) {counter++;}}
}
找到这个类的class文件,在class文件目录下执行
javap -v SyncTest.class,反编译效果如下:
monitorenter 上锁开始的地方
monitorexit 解锁的地方
其中被monitorenter和monitorexit 包围住的指令就是上锁的代码
有两个monitorexit的原因, 第二个monitorexit是为了防止锁住的代码抛异常后不能及时释放锁
在使用了synchornized代码块时需要指定一个对象,所以synchornized也被称为对象锁
monitor主要就是跟这个对象产生关联,如下图
Monitor内部具体的存储结构:
-
Owner:存储当前获取锁的线程的,只能有一个线程可以获取
-
EntryList:关联没有抢到锁(Owner为null)的线程,处于Blocked状态的线程
-
WaitSet:关联调用了wait方法的线程,处于Waiting状态的线程
具体的流程:
-
代码进入synchorized代码块,先让lock(对象锁)关联monitor,然后判断Owner是否有线程持有
-
如果没有线程持有(Owner为null),则让当前线程持有,表示该线程获取锁成功
-
如果有线程持有,则让当前线程进入entryList进行阻塞,如果Owner持有的线程已经释放了锁,在EntryList中的线程去竞争锁的持有权(非公平)
-
如果代码块中调用了wait()方法,则会进去WaitSet中进行等待
参考回答:
-
Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】
-
它的底层由monitor实现的,monitor是jvm级别的对象( C++实现),线程获得锁需要使用对象(锁)关联monitor
-
在monitor内部有三个属性,分别是owner、entrylist、waitset
-
其中owner是关联的获得锁的线程,并且只能关联一个线程;entrylist关联的是处于阻塞状态的线程;waitset关联的是处于Waiting状态的线程
2.2 synchronized关键字的底层原理-进阶
Monitor实现的锁属于重量级锁(性能低),你了解过锁升级吗?
- Monitor实现的锁属于重量级锁,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
- 在JDK 1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。
2.2.1 对象的内存结构
在HotSpot虚拟机中,对象在内存中存储的布局可分为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 对象的指针
简单说就是:每个对象的对象头都可以设置monitor的指针,让对象与monitor产生关联
2.2.4 轻量级锁
在很多的情况下,在Java程序运行时,同步块中的代码(同一线程持有锁)都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁是没必要的。因此JVM引入了轻量级锁的概念。
static final Object obj = new Object();public static void method1() {synchronized (obj) {// 同步块 Amethod2();}
}public static void method2() {synchronized (obj) {// 同步块 B}
}
加锁的流程
1.在线程执行时线程栈中创建一个Lock Record锁记录,将其obj字段指向锁对象。
2.通过CAS指令将Lock Record的地址存储在对象头的****mark word(hashcode)中(数据进行交换),如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。
3.如果是当前线程已经持有该锁了,代表这是一次锁重入(比如method1调用2)。
设置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恢复成为无锁状态。如果失败则膨胀为重量级锁。
2.2.5 偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。
以后只要不发生竞争,这个对象就归该线程所有
static final Object obj = new Object();public static void m1() {synchronized (obj) {// 同步块 Am2();}
}public static void m2() {synchronized (obj) {// 同步块 Bm3();}
}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操作,性能相对轻量级锁更好一些
解锁流程参考轻量级锁
2.2.6 Monitor实现的锁属于重量级锁,你了解过锁升级吗?——参考回答
Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,
分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。
X | 描述 |
---|---|
重量级锁 | 底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。 |
轻量级锁 | 线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性 |
偏向锁 | 一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令 |
一旦锁发生了竞争,都会升级为重量级锁
2.3你谈谈 JMM(Java 内存模型)
难易程度:☆☆☆
出现频率:☆☆☆
JMM(Java Memory Model)Java内存模型,是java虚拟机规范中所定义的一种内存模型。
Java内存模型(Java Memory Model)描述了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 = 101,A:int a = 100,B:修改后的值:int a = 99(a–)
-
线程2拿A的值与主内存V的值进行比较,判断是否相等(目前不相等,因为线程1已更新V的值99)
-
不相等,则线程2更新失败
-
自旋锁****操作
-
因为没有加锁,所以线程不会陷入阻塞,效率较高
-
如果竞争激烈,重试频繁发生,效率会受影响
自旋:需要不断尝试获取共享内存V中最新的值,然后再在新的值的基础上进行更新操作,如果失败就继续尝试获取新的值,直到更新成功
2.4.2 CAS 底层实现
CAS 底层依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令
都是native修饰的方法,由系统提供的接口执行,并非java代码实现,一般的思路也都是自旋锁实现
在java中比较常见使用有很多,
比如ReentrantLock和Atomic开头的线程安全类,都调用了Unsafe中的方法
- ReentrantLock中的一段CAS代码
2.4.3 乐观锁和悲观锁的区别
- CAS 是基于乐观锁的思想:最乐观的估计,
不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试(自旋)呗。
- synchronized 是基于悲观锁的思想:最悲观的估计,
得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
2.5 请谈谈你对 volatile 关键字的理解
难易程度:☆☆☆
出现频率:☆☆☆
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
2.5.1 保证线程间的可见性
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,防止编译器优化发生,这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。
一个典型的例子:永不停止的循环
package com.itheima.basic;
// 可见性例子
// -Xint
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("modify stop to true...");}).start();foo();}static void foo() {int i = 0;while (!stop) {i++;}System.out.println("stopped... c:"+ i);}
}
当执行上述代码的时候,发现foo()方法中的循环是结束不了的,也就说读取不到共享变量的值结束循环
主要是因为在JVM虚拟机中有一个JIT(即时编辑器)给代码做了优化。
上述代码
while (!stop) { i++; }
在很短的时间内,这个代码执行的 次数太多了,当达到了一个阈值,JIT就会优化此代码,如下:
while (true) { i++; }
当把 代码优化成这样子以后,及时stop变量改变为了false也依然停止不了循环
解决方案:
第一:
在程序运行的时候加入vm参数-Xint表示禁用即时编辑器,不推荐,得不偿失(其他程序还要使用)
第二:
在修饰stop变量的时候加上volatile,表示当前代码禁用了即时编辑器,问题就可以解决,代码如下:
static volatile boolean stop = false;
2.5.2 禁止进行指令重排序
用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果
注解 @Actor
保证方法内的代码在同一个线程下执行
引入多线程测试工具进行测试大量线程 出现上述四种可能结果
import org. openjdk.jcstress…
解决方案
在变量上添加volatile,禁止指令重排序,则可以解决问题
屏障添加的示意图
-
写操作加的屏障是阻止上方其它写操作越过屏障排到volatile变量写之下
-
读操作加的屏障是阻止下方其它读操作越过屏障排到volatile变量读之上
其他补充
我们上面的解决方案是把volatile加在了int y这个变量上,我们能不能把它加在int x这个变量上呢?
下面代码使用volatile修饰了x变量
屏障添加的示意图
这样显然是不行的,主要是因为下面两个原则:
-
写操作加的屏障是阻止上方其它写操作越过屏障排到volatile变量写之下
-
读操作加的屏障是阻止下方其它读操作越过屏障排到volatile变量读之上
-
两个都加volatile 行不行? 行但是 阻止指令重排序 性能会降低
所以,现在我们就可以总结一个volatile使用的小妙招:
-
写变量让volatile修饰的变量的在代码最后位置
-
读变量让volatile修饰的变量的在代码最开始位置
2.6 什么是AQS?
难易程度:☆☆☆
出现频率:☆☆☆
2.6.1 概述
全称是 AbstractQueuedSynchronizer,
是阻塞式锁和相关的同步器工具的框架,它是构建锁或者其他同步组件的基础框架
简单说,JUC提供的锁机制
AQS与Synchronized的区别
synchronized | AQS |
---|---|
关键字,c++ 语言实现 | java 语言实现 |
悲观锁,自动释放锁 | 悲观锁,手动开启和关闭 |
锁竞争激烈都是重量级锁,性能差 | 锁竞争激烈的情况下,提供了多种解决方案 |
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表示尾结点
0线程结束,把state改成0,唤醒head
如果多个线程共同去抢这个资源是如何保证原子性的呢?
在去修改state状态的时候,使用的cas自旋锁来保证原子性,确保只能有一个线程修改成功,修改失败的线程将会进入FIFO队列中等待
AQS是公平锁吗,还是非公平锁?
-
新的线程与队列中的线程共同来抢资源,是非公平锁
-
新的线程到队列中最后等待,只让队列中的head线程获取锁,是公平锁
比较典型的AQS实现类ReentrantLock,
它 默认就是非公平锁,新的线程与队列中的线程共同来抢资源
2.5 ReentrantLock的实现原理
难易程度:☆☆☆☆
出现频率:☆☆☆
2.5.1 概述
ReentrantLock翻译过来是可重入锁,相对于synchronized它具备以下特点:
-
可中断,synchronized不能中断
-
可以设置超时时间
-
可以设置公平/非公平 锁,synchronized只有非公平
-
支持多个条件变量
-
与synchronized一样,都支持重入
不释放锁会死锁
2.5.2 实现原理
ReentrantLock主要利用 CAS+AQS队列(底层完全是AQS) 来实现。
它支持公平锁和非公平锁,两者的实现类似
构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。
查看ReentrantLock源码中的构造方法:
提供了两个构造方法,不带参数的默认为非公平
如果使用带参数的构造函数,并且传的值为true,则是公平锁
其中NonfairSync和FairSync这两个类父类都是Sync
而Sync的父类是AQS,所以可以得出ReentrantLock底层主要实现就是基于AQS来实现的
工作流程
-
线程来抢锁后使用cas的方式修改state状态,修改状态成功为1,则让exclusiveOwnerThread属性指向当前线程,获取锁成功
-
假如修改状态失败,则会进入双向队列中等待,head指向双向队列头部,tail指向双向队列尾部
-
当exclusiveOwnerThread为null的时候,则会唤醒在双向队列中等待的线程
-
公平锁则体现在按照先后顺序获取锁,非公平体现在不在排队的线程****也可以抢锁
2.6 synchronized和Lock有什么区别 ?
难易程度:☆☆☆☆
出现频率:☆☆☆☆
参考回答
- 语法层面
- synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现
- Lock 是接口,源码由 jdk 提供,用 java 语言实现
- 使用 synchronized 时,退出同步代码块锁会自动释放, 而使用 Lock 时,需要手动调用 unlock 方法释放锁
- 功能层面
- 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
- Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量
- Lock 有适合不同场景的实现,如 ReentrantLock, ReentrantReadWriteLock
- 性能层面
- 在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖
- 在竞争激烈时,Lock 的实现通常会提供更好的性能
2.7 死锁产生的条件是什么?
难易程度:☆☆☆☆
出现频率:☆☆☆
死锁:一个线程需要同时获取多把锁,这时就容易发生死锁
例如:
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的锁。
2.8 如何进行死锁诊断?
难易程度:☆☆☆
出现频率:☆☆☆
当程序出现了死锁现象,我们可以使用jdk自带的工具:jps和 jstack
- jps:输出JVM中运行的进程状态信息
- jstack:查看java进程内线程的堆栈信息
步骤如下:
第一:查看运行的线程 Terminal输入jps
第二:使用jstack查看线程运行的情况,下图是截图的关键信息
运行命令:jstack -l 46032
其他解决工具,可视化工具
- jconsole
用于对jvm的内存,线程,类的监控,是一个基于 jmx 的 GUI 性能监控工具
打开方式:javajdk 安装目录 bin目录下 直接启动 jconsole.exe 就行
- VisualVM:故障处理工具
能够监控线程,内存情况,查看方法的CPU时间和内存中的对象,已被GC的对象,反向查看分配的堆栈
打开方式:java 安装目录 bin目录下 直接启动 jvisualvm.exe就行
2.10 ConcurrentHashMap
难易程度:☆☆☆
出现频率:☆☆☆☆
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不冲突,就不会产生并发的问题 , 效率得到提升
2.11 导致并发程序出现问题的根本原因是什么
难易程度:☆☆☆
出现频率:☆☆☆
Java并发编程三大特性
-
原子性
-
可见性
-
有序性
(1)原子性
一个线程在CPU中操作不可暂停,也不可中断,要不执行完成,要不不执行
比如,如下代码能保证原子性吗?
以上代码会出现超卖或者是一张票卖给同一个人,执行并不是原子性的
解决方案:
1.synchronized:同步加锁
2.JUC里面的lock:加锁
(2)内存可见性
内存可见性:让一个线程对共享变量的修改对另一个线程可见
比如,以下代码不能保证内存可见性
解决方案:
-
synchronized
-
volatile(推荐)
-
LOCK
(3)有序性
指令重排:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的
还是之前的例子,如下代码:
解决方案:
- volatile