目录
一、如何保证线程安全?
二、线程 vs 进程
1、线程的优点
2、进程和线程的区别
三、常见的锁策略
1、乐观锁 vs 悲观锁
2、读写锁
3、重量级锁 vs 轻量级锁
4、自旋锁(Spin Lock)
5、公平锁 vs 非公平锁
6、可重入锁 vs 不可重入锁
面试葵花宝典
1、你是怎么理解乐观锁和悲观锁的,具体怎么实现?
2、介绍下读写锁?
3、什么是自旋锁,为什么要使用自旋锁策略,缺点是什么?
4、synchronized 是可重入锁么?
5、CAS?
1)什么是CAS?
2)CAS 是怎么实现的
3)CAS有哪些应用?
4)CAS 的 ABA 问题
5)高频面试题
6)Synchronized 原理
7) 偏向锁 -->? 轻量级锁 --> 重量级锁
7)锁消除 ?锁粗化?
8)Callable 接口
?9)JUC(java.util.concurrent)的常见类
10)原子类
11)线程池
12)信号量
?13)CountDownLatch
14)相关面试题
?15)线程安全的集合类
16)多线程环境使用 ArrayList?
?17)多线程环境使用队列
18)多线程环境使用哈希表
19)相关面试题
20)死锁?
21)如何避免死锁?
22)其他常见问题
一、如何保证线程安全?
1、使用没有共享资源的模型
2、使用共享资源只读不写的模型
1)不需要写共享资源
2)使用不可变对象
3、直面线程安全(重点)
? ? ? ? 1)保证原子性
? ? ? ? 2)保证顺序
? ? ? ? 3)? ?保证内存可见性
二、线程 vs 进程
1、线程的优点
1)创建一个新的线程的代价要比创建一个新的进程小
2)与进程间切换相比,线程间的切换需要操作系统做的操作小得多
3)线程占用资源比进程少
4)能充分利用多处理器的可并行数量
5)在等待 I/O 操作结束的同时,程序可以执行其他任务
6)计算密集型应用,将计算分解到多个线程中实现
2、进程和线程的区别
1)进程是系统进行资源分配的最小单位,线程是程序执行的最小单位
2)进程有自己的内存地址空间,线程只独享指令流执行的必要资源,如寄存器和栈
3)由于同一进程的各线程间共享内存和文件资源,可以不通过内核进行通信
4)线程的创建,切换及终止效率更高
三、常见的锁策略

1、乐观锁 vs 悲观锁
悲观锁:
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次拿数据的时候都上锁,这样别人想拿到这个数据就会阻塞直到拿到锁
乐观锁:
假设数据一般情况下不会发生冲突,所以在数据提交更新的时候,才会正式对数据是否产生并发进行检测,如果发现并冲突了,则让返回用户错误的信息,让用户决定处理

synchronized 初始使用乐观锁策略,当发现锁竞争比较频繁的时候,就会自动切换悲观锁策略
?乐观锁的一个重要功能?就是要检测出数据是否发生冲突,我们可以引入一个”版本号“来解决


?
2、读写锁
多线程之间,数据的读取方式之间不会产生线程安全问题,但数据的写入方式互相之间以及和读者之间都需要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗,所以读写锁因此而生。
读写锁(readers-writer lock) 在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。
一个线程对于数据的访问,主要存在两种操作:读数据和写数据
1)两个线程都只是读一个数据,此时并没有线程安全问题,直接并发的读取即可
2)两个线程都要写一个数据,此时会出现线程安全问题
3)一个线程读另外一个线程写,也有线程安全问题
读写锁就是把读操作和写操作区分对待。JAVA 标准库中提供了 ReentrantReadWriteLock 类,实现了读写锁

其中,
?读写锁 特别适合于”频繁读,不频繁写“的场景中
比如一个教务系统

synchronized 不是读写锁
3、重量级锁 vs 轻量级锁
锁的核心特性”原子性“,这样的机制追根溯源是 CPU 这样的硬件设备提供的
1)CPU 提供了”原子操作指令“
2)操作系统基于CPU的原子指令,实现了 mutex 互斥锁
3)JVM基于操作系统提供的互斥锁,实现了 synchronized 和 ReentrantLock 等关键字和类
?
重量级锁:加锁机制重度依赖 OS 提供的 mutex
?1)大量的内核态用户态切换
2)很容易引发线程的调度
轻量级锁:加锁机制尽可能不用 mutex,而是尽量在用户态代码完成,实在搞不定再用mutex
1)少量的内核态用户态切换
2)不太容易引发线程调度

4、自旋锁(Spin Lock)
?按之前的方式,线程在抢锁失败后进入阻塞状态,放弃CPU,需要很久才能被再次调度
但实际上,大部分情况下,虽然当前抢锁失败,但过不了多久,锁就会被释放。没必要放弃这个CPU,这个时候就可以使用自旋锁来处理这样的问题
自旋锁伪代码:
while (抢锁(lock)== 失败) {}
如果获取锁失败,立即再尝试获取锁,无限循环,直到获取锁为止,第一次获取锁失败,第二次的尝试会在极短的时间内到来
一旦锁被其他线程释放,就能第一时间获取到锁,

自旋锁是一种典型的 轻量级锁 的实现方式
优点: 没有放弃CPU,不涉及线程阻塞和调度,一旦锁被释放,就能第一时间获取到锁
缺点:如果锁被其他线程持有的时间比较久,那么就会持续的消耗CPU资源(而挂起等待的时候是不消耗CPU的)
synchronized 中的轻量级锁策略大概率是通过自旋锁的方式实现的
5、公平锁 vs 非公平锁
?

1) 操作系统内部的线程调度可以视为随机的,如果不做任何额外的限制,锁就是非公平锁,如果要想实现公平锁,就需要依赖额外的数据结构,来记录线程们的先后顺序
2)公平锁和非公平锁没有好坏之分,关键看适用场景
Synchronized 是非公平锁
6、可重入锁 vs 不可重入锁
可重入锁的字面意思是”可以重新进入的锁“,即允许同一个线程多次获取同一把锁。
比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫递归锁)
Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的
而LInux提供的mutex是不可重入锁

面试葵花宝典
1、你是怎么理解乐观锁和悲观锁的,具体怎么实现?

2、介绍下读写锁?

3、什么是自旋锁,为什么要使用自旋锁策略,缺点是什么?

4、synchronized 是可重入锁么?

5、CAS?
1)什么是CAS?
CAS:全称Compare and swap,字面意思:”比较并交换:,一个CAS涉及到以下操作:

?CAS伪代码
下面写的代码不是原子的,真实的 CAS 是一个原子的硬件指令完成的,这个伪代码只是辅助理解CAS 的工作流程

两种典型的不是“原子性”的代码
1)check and set (if 判定然后设定值)
2)read and update(i++)
当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号
CAS 可以视为一种乐观锁(或者理解成 CAS 是乐观锁的一种实现方式)
2)CAS 是怎么实现的
?针对不同的操作系统,JVM用到了不同的CAS实现原理,简单来讲:
1)java 的 CAS 利用的是 unsafe 这个类提供的 CAS 操作
2)unsafe 的CAS 依赖了的是 JVM 针对不同的操作系统实现的 Atomic
简而言之,硬件给予支持,软件层面才能做到
3)CAS有哪些应用?
1)实现原子类

这里边的 getAndIncrement 相当于 i++ 操作?
伪代码实现:

?

?

2)实现自旋锁
基于 CAS 实现更灵活的锁,获取到更多的控制权

4)CAS 的 ABA 问题
1)什么是 ABA 问题?
ABA 问题:
假设存在两个线程 t1 和 t2 ,有一个共享变量 num,初始值为 A
接下来,线程 t1 想使用 CAS 把值改为 Z,那么就需要
1、先读取 num 的值,记录到 oldNum 变量中
2、使用 CAS 判定当前 num 的值是否为 A,如果为 A,就修改为 Z


?2)ABA 问题引来的 BUG
大部分情况下,t2 线程这样的一个反复横跳改动,对于 t1 是否修改 num,是没有影响的,但是不排除一些特殊情况


3)解决 ABA 问题的方案
给要修改的值,引入版本号,在 CAS? 比较数据当前值和旧值之前,也要比较版本号是否符合预期。
?
5)高频面试题
1)讲解下你自己理解的 CAS 原理
CAS 全称 Compare and swap ,“比较并交换”,相当于通过一个原子的操作,同时完成“读取内存,比较是否相等,修改内存”这三个步骤,本质上需要 CPU 指令的支撑
2)ABA 问题怎么解决?
给需要修改的数据引入版本号,在 CAS 比较数据当前值和旧值的同时,也要比较版本号是否符合预期,如果发现版本号一致就真正执行修改操作,并且让版本号自增。如果发现版本号不一致(数据一定被修改过),操作失败。
6)Synchronized 原理
1) 基本特点
?结合上面的锁策略,我们可以总结出,Synchronized 具有以下特性
1、开始是乐观锁,如果频繁的锁冲突,就转换为悲观锁
2、开始是轻量级锁,如果锁被持有的时间过长,就转换为重量级锁
3、实现轻量级锁的时候大概率用到的自旋锁策略
4、是一种不公平锁
5、是一种可重入锁
6、不是读写锁
2)加锁工作过程
JVM 将 synchronized 锁分为 无锁,偏向锁,轻量级锁,重量级锁状态。会根据情况,依次进行升级

7) 偏向锁 -->? 轻量级锁 --> 重量级锁
1、偏向锁
第一个尝试加锁的过程,优先进入偏向锁状态


2、轻量级锁
随着其他线程进入竞争,偏向锁状态被消除,进入轻量级锁状态(自适应的自旋锁)
此处的轻量级锁就是通过 CAS 来实现

3、重量级锁
如果竞争进一步激烈,自旋不能快速获取到锁的状态,就会膨胀为重量级锁
此处的重量级锁就是指内核提供的 mutex

7)锁消除 ?锁粗化?
1、锁消除
编辑器 + JVM 判断锁是否可消除,如果可以,就直接消除

2、锁粗化?
?一段逻辑中如果多次出现加锁解锁,编译器 + JVM 会自动进行锁的粗化


由此可见,synchronized 的策略是比较复杂的,在背后做了很多事情,目的为了让程序员哪怕什么也不懂,也不至于写出特别慢的程序
8)Callable 接口
1、Callable 的用法
Callable 是一个 interface,相当于把线程封装了一个“返回值”,方便程序员借助多线程的方式计算结果
?
public class Result {
static class Res {
public int sum;
public Object lock = new Object();
}
public static void main(String[] args) throws InterruptedException {
Res res = new Res();
Thread t = new Thread(new Runnable() {
@Override
public void run() {
int sum = 0;
for (int i = 0; i < 1000; i ++) {
sum++;
}
synchronized (res.lock) {
res.sum = sum;
res.lock.notify();
}
}
});
t.start();
synchronized (res.lock) {
while (res.sum == 0) {
res.lock.wait();
}
System.out.println(res.sum);
}
}
}


import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Result {
public static void main(String[] args) throws InterruptedException, ExecutionException {
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int res = 0;
for (int i = 0; i < 1000; i ++) {
res++;
}
return res;
}
};
FutureTask<Integer> futureTask = new FutureTask(callable);
Thread t = new Thread(futureTask);
t.start();
int res = futureTask.get();
System.out.println(res);
}
}
?
相关面试题
?9)JUC(java.util.concurrent)的常见类
ReentrantLock
可重入互斥锁,和 synchronized 定位类似,都是用来实现互斥效果,保证线程安全


ReentrantLock 和 synchronized 的区别


?如何选择使用那个锁?

10)原子类
?原子类内部用的是 CAS 实现,所以性能要比加锁实现 i++ 高很多。原子类有以下几个:

11)线程池
虽然创建销毁线程比创建销毁进程更轻量,但是在频繁创建销毁线程的时候还是会比较低效
线程池就是为了解决频繁创建销毁线程的问题,如果某个线程不再使用了,并不是真正把线程释放,而是放到一个“池子”中,下次如果需要用到线程就直接从池子中取,不必通过系统来创建。
 ?

?
12)信号量
信号量,可以用来表示“可用资源的个数”,本质上就是一个计数器
?
public class Worker {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(4);
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
System.out.println("我要申请资源了!");
semaphore.acquire();
System.out.println("我申请到资源了");
Thread.sleep(1000);
System.out.println("我释放资源了!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
for (int i = 0; i < 10; i ++) {
Thread t = new Thread(runnable);
t.start();
}
}
}
?13)CountDownLatch
同时等待 N 个任务执行结束


14)相关面试题
1、线程同步的方式
synochronized , ReentrantLock ,Semaphore 等都可以用于线程同步
2、为什么有了 synchronized 还需要 juc 下的 lock?

3、AtomicInteger 的实现原理是什么?
?
4、信号量听说过?之前都用在那些场景下?
?
5、解释一下 ThreadPoolExecutor 构造方法的参数含义
参考:
(306条消息) 面试问我线程池?还好我早有应对_yan扬的博客-CSDN博客 https://blog.csdn.net/qq_59539549/article/details/125014470?spm=1001.2014.3001.5501
?15)线程安全的集合类
原来的集合类,大部分都不是线程安全的

16)多线程环境使用 ArrayList?
1、自己使用同步机制 (synchronized 或者 ReentrantLock)
2、Collections.synchronizedList(new ArrayList);
?3、使用 CopyOnWriteArrayList

?17)多线程环境使用队列

18)多线程环境使用哈希表
HashMap 本身不是线程安全的

?
?

19)相关面试题
1、ConcurrentHashMap 的读是否要加锁,为什么?
读操作没有加锁,目的是为了进一步降低锁冲突的概率,为了保证读到刚修改的数据,搭配了 volatile 关键字
2、介绍下 ConcurrentHashMap 的锁分段技术?

3、ConcurrentHashMap 在jdk1.8 做了那些优化?
?4、Hashtable 和 HashMap ,ConcurrentHashMap 之间的区别?

20)死锁?
?1、死锁是什么?
死锁的情形:多个线程同时被阻塞,他们中的一个或多个全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止


?

?死锁是一种很严重地 BUG !导致一个程序卡死,无法正常工作
21)如何避免死锁?
死锁产生地四个必要条件:
1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放
3、请求和保持,即当资源请求者在请求其他资源的同时保持对原有资源地占有
4、循环等待:即存在一个等待队列:p1 占有 p2 的资源, p2 占有 p3 的资源,p3 占有 p1 的资源,这样就形成了一个等待环路。
?
?破坏循环等待
最常用的一种死锁阻止技术就是”锁排序“,假设有 N 个线程尝试 同时获取 M 把锁,就可以针对 M 把锁进行编号(1,2,3....M)
N 个线程尝试获取锁的时候,都按照固定的编号由小到大顺序来获取锁,这样就可以避免环路等待


22)其他常见问题
1、谈谈 volatile 关键字的用法?
?2、java多线程如何实现数据共享?

3、java 创建线程池的接口是什么?参数 LinkedBlockingQueue 的作用是什么?

4、java 线程共有几种状态?状态之间如何切换?

5、在多线程下,如果对一个数进行叠加,该怎么做?

6、Servlet 是否是线程安全的?
?7、Thread 和 Runable 的区别和联系?
?8、多次 start 一个线程会怎么样?

9、有 synchronized 两个方法,两个线程分别同时用这个方法,请问会发生什么?

10、进程和线程的区别?

|