文章目录
- 1. 线程与进程
- 2. 并发与并行
- 3. 同步与异步
- 4. 为什么用多线程 & 多线程如何实现
- 5. 多线程可能带来的问题
- 6. 线程的生命周期和状态
- 7. 上下文切换
- 8. 线程死锁
- 9. sleep和wait方法对比
- 10. wait方法为什么不定义在thread中;sleep定义在thread类中
- 11. run和join方法
- 12. JMM (Java内存模型)
- 13. volatile关键字
- 14. synchronized关键字
- 15. ThreadLocal
- 16. 线程池
- 17. AQS
- 18. Atomic原子类
- 19. 乐观锁和悲观锁
- 20. ReentrantReadWriteLock与StampedLock
- 21. CountDownLatch
- 22. CyclicBarrier
- 23. Semaphore
- 24. Future类
- 25. 锁体系
1. 线程与进程
1.1 进程
- 进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
- 在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。
1.2 线程
- 线程与进程相似,但线程是一个比进程更小的执行单位。
- 一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,
- 所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此线程也被称为轻量级进程。
总结
1.3 区别与联系
1.4 程序计数器为什么是私有的
- 作用
字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。 - 原因
需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置
1.5 虚拟机栈和本地方法栈为什么是私有的
- 作用
虚拟机栈 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。 - 原因
为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的
1.6 堆和方法区
堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
1.7 线程有几种状态
- 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
- 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。 - 阻塞(BLOCKED):表示线程阻塞于锁。
- 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
- 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
- 终止(TERMINATED):表示该线程已经执行完毕。
1.8 进程有几种状态
- 创建状态:进程在创建时需要申请一个空白PCB,向其中填写控制和管理进程的信息,完成资源分配。如果创建工作无法完成,比如资源无法满足,就无法被调度运行,把此时进程所处状态称为创建状态
- 就绪状态:进程已经准备好,已分配到所需资源,只要分配到CPU就能够立即运行
- 执行状态:进程处于就绪状态被调度后,进程进入执行状态
- 阻塞状态:正在执行的进程由于某些事件(I/O请求,申请缓存区失败)而暂时无法运行,进程受到阻塞。在满足请求时进入就绪状态等待系统调用
- 终止状态:进程结束,或出现错误,或被系统终止,进入终止状态。无法再执行
1.9 进程间的通信方式
管道、 消息队列、信号量、信号、共享内存、套接字
1.10 java实现线程同步
1.synchronized关键字
2.使用重入锁reentrantLock()
3.使用局部变量ThreadLocal实现线程同步
4.使用阻塞队列实现线程同步
5.使用原子变量实现线程同步
2. 并发与并行
并发:两个及两个以上的作业在同一 时间段 内执行。(一个处理器处理多个任务,按时间片轮流处理多个任务)
并行:两个及两个以上的作业在同一 时刻 执行。(单位时间多个处理器同时处理多个任务)
参考连接
并发例子
3. 同步与异步
同步 : 发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。
异步 :调用在发出之后,不用等待返回结果,该调用直接返回。
4. 为什么用多线程 & 多线程如何实现
- 从计算机底层来说: 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
- 从当代互联网发展趋势来说: 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能
- 单核时代: 在单核时代多线程主要是为了提高单进程利用 CPU 和 IO 系统的效率
- 多核时代: 多核时代多线程主要是为了提高进程利用多核 CPU 的能力
4.1 实现多线程
资料
在Java中,多线程主要的实现方式有四种:继承Thread类、实现Runnable接口、实现Callable接口通过FutureTask包装器来创建Thread线程、使用ExecutorService、Callable、Future实现有返回结果的多线程。
4.2 Runnable 和 Callable 有什么区别?
Runnable 接口中的 run() 方法的返回值是 void,它做的事情只是纯粹地去执行 run() 方法中的代码而已;
Callable 接口中的 call() 方法是有返回值的,是一个泛型,和 Future、FutureTask 配合可以用来获取异步执行的结果。
5. 多线程可能带来的问题
并发编程并不总是能提高程序运行速度的 内存泄漏、死锁、线程不安全等等
6. 线程的生命周期和状态
7. 上下文切换
线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所说到过的程序计数器,栈信息等。当出现如下情况的时候,线程会从占用 CPU 状态中退出。
线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 上下文切换。
8. 线程死锁
8.1 定义
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
8.2 死锁四大条件
8.3 如何预防与避免死锁
- 预防:破坏死锁的必要条件
- 破坏请求与保持条件:一次性申请所有的资源
- 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源
- 破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
- 避免
避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。
安全状态 指的是系统能够按照某种线程推进顺序(P1、P2、P3…Pn)来为每个线程分配所需资源,直到满足每个线程对资源的最大需求,使每个线程都可顺利完成。称 <P1、P2、P3…Pn> 序列为安全序列。
8.4 银行家算法
每一个新进程进入系统时,必须声明需要每种资源的最大数目,其数目不能超过系统所拥有的的资源总量。当进程请求一组资源时,系统必须首先确定是否有足够的资源分配给该进程,若有,再进一步计算在将这些资源分配给进程后,是否会使系统处于不安全状态如果不会才将资源分配给它,否则让进程等待
8.5 死锁代码demo
public class DeadLockDemo {
private static Object resource1 = new Object();
private static Object resource2 = new Object();
// /**
// * 出现了死锁
// * @param args
// */
//
// public static void main(String[] args) {
// new Thread(()->{
// synchronized (resource1){
// System.out.println(Thread.currentThread()+"get resource1");
// try{
// Thread.sleep(1000);//休眠保证线程2拿到资源2
// }catch (InterruptedException e){
// e.printStackTrace();
// }
// System.out.println(Thread.currentThread()+"waiting get resource2");
// synchronized (resource2){
// System.out.println(Thread.currentThread()+"get resource2");
// }
// }
// },"线程1").start();
// new Thread(()->{
// synchronized (resource2){
// System.out.println(Thread.currentThread()+"get resource2");
// try{
// Thread.sleep(1000);//休眠保证线程1获得资源1
// }catch (InterruptedException e){
// e.printStackTrace();
// }
// System.out.println(Thread.currentThread()+"waiting get resource1");
// synchronized (resource1){
// System.out.println(Thread.currentThread()+"get resource1");
// }
// }
// },"线程2").start();
// }
/**
* 解决死锁:按顺序获取资源
* @param args
*/
public static void main(String[] args) {
new Thread(()->{
synchronized (resource1){
System.out.println(Thread.currentThread()+"get resource1");
try{
Thread.sleep(1000);//休眠保证线程2拿到资源2
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread()+"waiting get resource2");
synchronized (resource2){
System.out.println(Thread.currentThread()+"get resource2");
}
}
},"线程1").start();
new Thread(()->{
synchronized (resource1){
System.out.println(Thread.currentThread()+"get resource1");
try{
Thread.sleep(1000);//休眠保证线程1获得资源1
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread()+"waiting get resource2");
synchronized (resource2){
System.out.println(Thread.currentThread()+"get resource2");
}
}
},"线程2").start();
}
}
9. sleep和wait方法对比
- 相同点:
都可以暂停线程的执行 - 区别
锁 sleep()方法没有释放锁, wait释放了锁
用处 sleep用于暂停执行,wait用于线程交互
自动苏醒 sleep方法使用后,线程会自动苏醒;wait方法,线程不会苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法。
所属类 wait是Object类的本地方法;sleep是thread类的静态本地方法
10. wait方法为什么不定义在thread中;sleep定义在thread类中
wait() 是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。每个对象(Object)都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入 WAITING 状态,自然是要操作对应的对象(Object)而非当前的线程(Thread)。
因为 sleep() 是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。
11. run和join方法
11.1 可以直接调用thread中的run方法吗 / run和start方法的对比
new 一个 Thread,线程进入了新建状态。调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 但是,直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结: 调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。
11.2 join()的作用
join()是 Thread 类中的一个方法,当我们需要让线程按照自己指定的顺序执行的时候,就可以利用这个方法。「Thread.join()方法表示调用此方法的线程被阻塞,仅当该方法完成以后,才能继续运行」。
添加链接描述
12. JMM (Java内存模型)
0. 面试如何答
-
了解JMM吗?
Java 定义的并发编程相关的一组规范,除了抽象了线程和主内存之间的关系(稍微解释一下,参考后边)之外,其还规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,(简单提一下happens-before原则)其主要目的是为了简化多线程编程,增强程序可移植性的。 -
happens-before原则
整体定义(前一个操作对后一个操作具有可见性) + 两个整体原则 + 五个小原则(说多少是多少) -
并发编程三大特性
原子、可见、有序
12.1 CPU缓存模型 (引出JMM的前置知识)
-
为什么需要CPU高速缓存
CPU 缓存则是为了解决 CPU 处理速度和内存处理速度不对等的问题
CPU Cache 缓存的是内存数据用于解决 CPU 处理速度和内存不匹配的问题,内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题。 -
CPU Cache的工作方式
先复制一份数据到 CPU Cache 中,当 CPU 需要用到的时候就可以直接从 CPU Cache 中读取数据,当运算完成后,再将运算得到的数据写回 Main Memory 中
-
内存缓存不一致问题
比如我执行一个 i++ 操作的话,如果两个线程同时执行的话,假设两个线程从 CPU Cache 中读取的 i=1,两个线程做了 1++ 运算完之后再写回 Main Memory 之后 i=2,而正确结果应该是 i=3。 -
解决内存缓存不一致
对于CPU: CPU 为了解决内存缓存不一致性问题可以通过制定缓存一致协议(比如 MESI 协议)或者其他手段来解决。
对于操作系统: 操作系统通过 内存模型(Memory Model) 定义一系列规范来解决 内存缓存不一致性问题。
12.2 指令重排序 (引出JMM的前置知识)
简单来说就是系统在执行代码的时候并不一定是按照你写的代码的顺序依次执行。
- 常见的指令重排序
编译器优化重排 :编译器(包括 JVM、JIT 编译器等)在不改变单线程程序语义的前提下,重新安排语句的执行顺序。
指令并行重排 :现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序 - java源代码的指令重排序
Java 源代码会经历 编译器优化重排 —> 指令并行重排 —> 内存系统重排
指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致
12.3 为什么需要JMM(面试答这个就好) ***
对于 Java 来说,你可以把 JMM 看作是 Java 定义的并发编程相关的一组规范,除了抽象了线程和主内存之间的关系之外,其还规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的。
这是因为并发编程下,像 CPU 多级缓存和指令重排这类设计可能会导致程序运行出现一些问题。就比如说我们上面提到的指令重排序就可能会让多线程程序的执行出现问题,为此,JMM 抽象了 happens-before 原则(后文会详细介绍到)来解决这个指令重排序问题。JMM 说白了就是定义了一些规范来解决这些问题,开发者可以利用这些规范更方便地开发多线程程序。
12.4 JMM是如何抽象线程和主内存之间的关系
- 自己的话总结
- 实例对象 主内存
- 线程1和2互相不能访问各自的本地内存
- 线程1如果要修改主内存的对象,会先复制一份到自己的本地内存,再进行修改
- 主内存
所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量) - 本地内存
每个线程都有一个私有的本地内存来存储共享变量的副本,并且,每个线程只能访问自己的本地内存,无法访问其他线程的本地内存。本地内存是 JMM 抽象出来的一个概念,存储了主内存中的共享变量副本。 - JMM抽象示意图
- 线程1和线程2通信的过程
线程 1 把本地内存中修改过的共享变量副本的值同步到主内存中去。
线程 2 到主存中读取对应的共享变量的值。
- 存在的问题
线程 1 和线程 2 分别对同一个共享变量进行操作,一个执行修改,一个执行读取。线程 2 读取到的是线程 1 修改之前的值还是修改后的值并不确定,都有可能,因为线程 1 和线程 2 都是先将共享变量从主内存拷贝到对应线程的工作内存中。
- 解决
关于主内存与工作内存直接的具体交互协议,即一个变量如何从主内存拷贝到工作内存,如何从工作内存同步到主内存之间的实现细节,Java 内存模型定义八种同步操作.
12.5 Java内存区域和JMM有何区别
JVM 内存结构和 Java 虚拟机的运行时区域相关,定义了 JVM 在运行时如何分区存储程序数据,就比如说堆主要用于存放对象实例。
Java 内存模型和 Java 的并发编程相关,抽象了线程和主内存之间的关系就比如说线程之间的共享变量必须存储在主内存中,规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的。
12.6 happens-before原则
12.6.1 意义
happens-before 原则表达的意义其实并不是一个操作发生在另外一个操作的前面,虽然这从程序员的角度上来说也并无大碍。更准确地来说,它更想表达的意义是前一个操作的结果对于后一个操作是可见的,无论这两个操作是否在同一个线程里。
12.6.2 happens-before常见规则
8条,重点理解下面五个
- 程序顺序规则
一个线程内,按照代码顺序,书写在前面的操作 happens-before 于书写在后面的操作; - 解锁规则
加了锁,必须先释放锁,另外一个线程才能获取该锁 - volatile 变量规则
对 volatile 变量的写操作的结果对于发生于其后的任何操作都是可见的。 - 传递规则
如果 A happens-before B,且 B happens-before C,那么 A happens-before C; - 启动规则
Thread 对象的 start()方法 happens-before 于此线程的每一个动作
12.6.3 happens-before和JMM关系
12.7 并发编程三个重要特性
- 原子性
一次操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么都不执行。
在 Java 中,可以借助synchronized 、各种 Lock 以及各种原子类实现原子性。 - 可见性
当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。
在 Java 中,可以借助synchronized 、volatile 以及各种 Lock 实现可见性。 - 有序性
由于指令重排序问题,代码的执行顺序未必就是编写代码时候的顺序。
在 Java 中,volatile 关键字可以禁止指令进行重排序优化。
12.8 总结
你可以把 JMM 看作是 Java 定义的并发编程相关的一组规范,除了抽象了线程和主内存之间的关系之外,其还规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的。
JSR 133 引入了 happens-before 这个概念来描述两个操作之间的内存可见性
13. volatile关键字
13.1 保证可见性
可见性是指当一个线程修改了共享变量的值后,其他线程能够立即看到最新的值。
volatile 关键字可以保证变量的可见性,如果我们将变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
13.2 如何禁止指令重排序
如果我们将变量声明为 volatile ,在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。
- 内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)
是一种 CPU 指令,用来禁止处理器指令发生重排序(像屏障一样),从而保障指令执行的有序性。另外,为了达到屏障的效果,它也会使处理器写入、读取值之前,将主内存的值写入高速缓存,清空无效队列,从而保障变量的可见性。
13.3 能否保证原子性
volatile 关键字能保证变量的可见性,但不能保证对变量的操作是原子性的。
演示代码略
13.4 实现原理
- 可见性
内存不可见是如何引起的?lock指令
volatile关键字通过使用内存屏障的机制来实现可见性。内存屏障会强制刷新缓存并保证读写操作的顺序性,从而保证变量的可见性。
具体来说,当一个线程对volatile变量进行写操作时,会在写操作之后插入写屏障,将最新的值刷新到主内存中。当其他线程对该变量进行读操作时,会在读操作之前插入读屏障,从主内存中获取最新的值。
- 有序性
内存屏障
14. synchronized关键字
14.1 定义及作用
- 主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
- 发展历史
在 Java 早期版本中,synchronized 属于 重量级锁,效率低下。,在 Java 6 之后, synchronized 引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销,这些优化让 synchronized 锁的效率提升了很多。
14.2 如何使用
- 修饰实例方法
- 修饰静态方法
- 修饰代码块
- synchronized 关键字加到 static 静态方法和 synchronized(class) 代码块上都是是给 Class 类上锁;
- synchronized 关键字加到实例方法上是给对象实例上锁;
- 尽量不要使用 synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能
14.3 构造方法可以用 synchronized 修饰么?
不能,构造方法本身就属于线程安全的,不存在同步的构造方法一说。
14.4 synchronized 和 volatile 有什么区别?.
synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在!
- volatile 关键字是线程同步的轻量级实现,所以 volatile性能肯定比synchronized关键字要好 。但 volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块 。
- volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
- volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。
volatile 本质是在告诉 JVM当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
volatile 仅能使用在变量级别;synchronized 则可以使用在变量、方法、和类级别的。
volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。
volatile:可见性,有序性
synchronize:原子性,可见性
14.5 synchronized 和 ReentrantLock 有什么区别?
- 相同点: 两者都是可重入锁
- synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
- synchronized 是和 for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比 synchronized 更多更灵活的特性:等待可中断、可实现公平锁、可实现选择性通知(锁可以绑定多个条件)、性能已不是选择标准
- 补充概念
(1)可重入锁
指的是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果是不可重入锁的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。
(2)公平锁
锁被释放之后,先申请的线程/进程先得到锁。
(3)非公平锁
锁被释放之后,后申请的线程/进程可能会先获取到锁,是随机或者按照其他优先级排序的。
14.6 JDK1.6对synchronized做了哪些优化?为什么要优化
锁的升级
- 偏向锁:
偏向锁的“偏”就是偏心的偏,它的意思是会偏向于第一个获得它的线程,如果在接下来的执行中,该锁没有被其他线程获取,那么持有偏向锁的线程就不需要进行同步。 - 轻量级锁:
如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥操作的开销。但如果存在锁竞争,除了互斥量开销外,还会额外发生 CAS 操作,因此在有锁竞争的情况下,轻量级锁比传统的重量级锁更慢! - 重量级锁:
重量级锁的实现一般是通过操作系统的互斥量来实现的,当一个线程获取重量级锁时,会将该线程挂起,直到锁被释放。这种锁的性能比较低,因为每次加锁和释放锁都需要涉及到对操作系统的系统调用,会有较大的开销。
14.7 synchronized可重入锁的原理
每个锁关联一个线程持有者和一个计数器。当计数器为0时表示该锁没有被任何线程持有,那么任何线程都都可能获得该锁而调用相应方法。当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计为1。此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。当线程退出一个synchronized方法/块时,计数器会递减,如果计数器为0则释放该锁。
14.8 Synchronized原理(方法和代码块)
(1)当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
(2)执行被修饰的代码块时先执行monitorEnter指令
(3)monitor中有一个计数器,初始为0
(4)尝试获取monitor的锁,会先判断计数器的值是否为0
(a)如果为0,则上锁,对monitor的计数器+1(可以锁重入)
(b)如果不为0,且当前线程不是自己,进入block阻塞状态等待
(5)执行完代码退出方法后会执行monitorExit指令,monitor的计数器-1(锁重入时,多次-1,直到为0)
(6)其他处于block阻塞状态的线程尝试获取锁(非公平锁)
在执行同步代码块之前之后都有一个monitor字样,其中前面的是monitorenter,后面的是离开monitorexit,不难想象一个线程也执行同步代码块,首先要获取锁,而获取锁的过程就是monitorenter ,在执行完代码块之后,要释放锁,释放锁就是执行monitorexit指令。
- 为什么会有两个monitorexit呢?
这个主要是防止在同步代码块中线程因异常退出,而锁没有得到释放,这必然会造成死锁(等待的线程永远获取不到锁)。因此最后一个monitorexit是保证在异常情况下,锁也可以得到释放,避免死锁。仅有ACC_SYNCHRONIZED这么一个标志,该标记表明线程进入该方法时,需要monitorenter,退出该方法时需要monitorexit。
14.9 sychronized和lock区别
- 首先synchronized是Java内置关键字,在JVM层面,Lock是个Java类;
- synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
- synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
- 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
Synchornonized好文章
14.10 CAS 和 synchronized 的使用场景?
简单的来说 CAS 适用于写比较少的情况下(多读场景,冲突一般较少),synchronized 适用于写比较多的情况下(多写场景,冲突一般较多)。
1、对于资源竞争较少(线程冲突较轻)的情况,使用 synchronized 同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗 cpu 资源;而 CAS 基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
2、对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。
15. ThreadLocal
15.0 数据结构
ThreadLocalMap的key可以简单看成ThreadLocal
15.1 什么是threadlocal?应用场景(pdf)
- 定义
ThreadLocal对象可以提供线程局部变量,每个线程Thread拥有一份自己的副本变量,多个线程互不干扰。
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢?
JDK 中自带的ThreadLocal类正是为了解决这样的问题。 ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。 - 应用场景
保存线程上下文信息,在需要的地方获取
线程间数据隔离
数据库连接
15.2 原理
其中,虚线表示弱引用
threadLocal的key是弱引用(为什么 面试题)
每个线程都有一个ThreadLocalMap,每个ThreadLocalMap中保存所有的ThreadLocal,而所有的ThreadLocal只是一个引用并不保存值,threadlocal为ThreadLocalMap中的key
15.3 内存泄漏问题
- 定义
内存泄漏是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成内存空间的浪费称为内存泄漏。随着垃圾回收器活动的增加以及内存占用的不断增加,程序性能会逐渐表现出来下降,极端情况下,会引发OutOfMemoryError导致程序崩溃。
- 导致原因
ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。
这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,**value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。**使用完 ThreadLocal方法后 最好手动调用remove()方法
如果是调用了thread.set方法,那么就不会出现key为null
15.4 如何解决hash冲突
由于没有链表结构,不能像hashmap(HashMap中解决冲突的方法是在数组上构造一个链表结构,冲突的数据挂载到链表上,如果链表长度超过一定数量则会转化成红黑树)一样
采用的是线性探测的方法
15.5 如何实现线程安全
有待研究 threadLocal能叫线程安全吗?
看原理部分。总结: 每一个线程都有自己的threadlocalMap---- threadLocalMap 以threadLocal为key(弱引用),代码放入的值为value。对thread的操作实际就是对threadLocalMap操作
15.6 如何避免内存泄漏?
使用ThreadLocal时,一般建议将其声明为static final的,避免频繁创建ThreadLocal实例。
使用完threadlocal后 手动调用remove方法
15.7 为什么将key设置为弱引用
15.8 四种引用类型
强引用:我们常常 new 出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候
软引用:使用 SoftReference 修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收
弱引用:使用 WeakReference 修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收
虚引用:虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知。随时可以被回收
15.9 ThreadLocal应用场景
- 线程间数据隔离,各线程的 ThreadLocal 互不影响
- 方便同一个线程使用某一对象,避免不必要的参数传递
- 全链路追踪中的 traceId 或者流程引擎中上下文的传递一般采用 ThreadLocal
- Spring 事务管理器采用了 ThreadLocal
- Spring MVC 的 RequestContextHolder 的实现使用了 ThreadLocal
16. 线程池
16.1 好处
降低资源消耗、降低资源消耗、提高线程的可管理性
16.2 如何创建
方式一:通过构造方法实现
方式二:通过 Executor 框架的工具类 Executors 来实现
《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
(1)newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
(2)newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。如果希望在服务器上使用线程池,建议使用 newFixedThreadPool方法来创建线程池,这样能获得更好的性能。
(3) newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。
(4)newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
16.3 核心线程数和最大线程数
核心线程数:线程池的基本大小,即在没有任务需要执行的时候线程池的大小,并且只有在工作队列满了的情况下才会创建超出这个数量的线程。
最大线程数: 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
16.4 线程池的饱和策略(拒绝策略)
- AbortPolicy:直接抛出异常拒绝新任务
- CallerRunsPolicy:当线程池无法处理当前任务时,会将该任务交由提交任务的线程来执行
- DiscardPolicy:直接丢弃新任务
- DiscardOleddestPolicy:丢弃最早的未处理的任务请求
16.5 线程池的原理
16.6 设定线程池大小
如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。
- CPU 密集型任务(N+1)
- I/O 密集型任务(2N)
N为CPU 核心数 - 标准参考答案
01:一个计算为主的程序(CPU密集型程序),多线程跑的时候,可以充分利用起所有的 CPU 核心数,比如说 8 个核心的CPU ,开8 个线程的时候,可以同时跑 8 个线程的运算任务,此时是最大效率。但是如果线程远远超出 CPU 核心数量,反而会使得任务效率下降,因为频繁的切换线程也是要消耗时间的。因此对于 CPU 密集型的任务来说,线程数等于 CPU 数是最好的了。
02:如果是一个磁盘或网络为主的程序(IO密集型程序),一个线程处在 IO 等待的时候,另一个线程还可以在 CPU 里面跑,有时候 CPU 闲着没事干,所有的线程都在等着 IO,这时候他们就是同时的了,而单线程的话此时还是在一个一个等待的。我们都知道 IO 的速度比起 CPU 来是很慢的。此时线程数等于CPU核心数的两倍是最佳的。
17. AQS
17.1 定义
AbstractQueuedSynchronizer ,翻译过来的意思就是抽象队列同步器.这个类在 java.util.concurrent.locks 包下面。
维护了一个共享资源,然后使用队列保证线程排队获取资源的一个过程
AQS 就是一个抽象类,主要用来构建锁和同步器。
17.2 原理
- 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。
- 如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
17.3 AQS资源共享方式
AQS 定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和
Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)
17.4 常见同步工具类
- Semaphore(信号量)
synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,而Semaphore(信号量)可以用来控制同时访问特定资源的线程数量。 - CountDownLatch (倒计时器)
CountDownLatch 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。
CountDownLatch 是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 CountDownLatch 使用完毕后,它不能再次被使用。 - CyclicBarrier(循环栅栏)
CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是:让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
17.5 CLH队列
CLH 锁是对自旋锁的一种改进,是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系),暂时获取不到锁的线程将被加入到该队列中。AQS 将每条请求共享资源的线程封装成一个 CLH 队列锁的一个结点(Node)来实现锁的分配。在 CLH 队列锁中,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)。
18. Atomic原子类
18.1 定义
Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。原子类基于CAS算法实现线程安全,无需加锁,性能高效。
18.2 JUC中的四类原子类
- 基本类型
- 数组类型
- 引用类型
- 对象的属性修改类型
19. 乐观锁和悲观锁
19.1 悲观锁定义及应用
每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。
像 Java 中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
悲观锁通常多用于写多比较多的情况下**(多写场景)**,避免频繁失败和重试影响性能。
19.2 乐观锁定义及应用
乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。
19.3 乐观锁的实现
19.3.1 版本号限制
一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。
19.3.2 CAS
CAS的全称是 Compare-and-Swap,也就是比较并交换,是并发编程中一种常用的算法。它包含了三个参数:V,A,B。
CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。
其中,V表示要读写的内存位置,A表示旧的预期值,B表示新值
CAS指令执行时,当且仅当V的值等于预期值A时,才会将V的值设为B,如果V和A不同,说明可能是其他线程做了更新,那么当前线程就什么都不做,最后,CAS返回的是V的真实值。
而在多线程的情况下,当多个线程同时使用CAS操作一个变量时,只有一个会成功并更新值,其余线程均会失败,但失败的线程不会被挂起,而是不断的再次循环重试。正是基于这样的原理,CAS即使没有使用锁,也能发现其他线程对当前线程的干扰,从而进行及时的处理。
19.4 存在的问题(乐观锁)
- ABA问题(重点)
问题描述:
在多线程环境中,某个location(或可以理解为某内存地址指向的变量)会被一个线程连续重复读取两次,那么只要第一次读取的值和第二次读取的值一样,那么这个线程就会认为这个变量在两次读取时间间隔内没有发生任何变化;
这种判定方式有问题: 在多线程环境下,在两次读取的时间间隔内,其他线程很可能对这个值做了修改,然后又改回原值,这似乎给此时正在重复读取变量的线程造成了该内存变量没有发生变化的错觉。
解决方案:
利用版本号,给每个变量加一个版本号,每次更新的时候把版本号加1,这样即使从A - > B -> A,两个A的版本号也不一样。基于CAS的乐观锁就是这个实现原理
- 循环时间长开销大
CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。
- 只能保证一个共享变量的原子操作
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效
20. ReentrantReadWriteLock与StampedLock
- ReentrantReadWriteLock
是一个可重入的读写锁,既可以保证多个线程同时读的效率,同时又可以保证有写入操作时的线程安全。在读多写少的情况下,使用 ReentrantReadWriteLock 能够明显提升系统性能 - StampledLock
是 JDK 1.8 引入的性能更好的读写锁,同样适合读多写少的业务场景,可以作为 ReentrantReadWriteLock的替代品,性能更好。
21. CountDownLatch
21.1 CountDownLatch 有什么用?
CountDownLatch 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。
CountDownLatch 是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 CountDownLatch 使用完毕后,它不能再次被使用。
21.2 CountDownLatch 的原理是什么?
CountDownLatch 是共享锁的一种实现,它默认构造 AQS 的 state 值为 count。当线程使用 countDown() 方法时,其实使用了tryReleaseShared方法以 CAS 的操作来减少 state,直至 state 为 0。当调用 await() 方法的时候,如果 state 不为 0,那就证明任务还没有执行完毕,await() 方法就会一直阻塞,也就是说 await() 方法之后的语句不会被执行。然后,CountDownLatch 会自旋 CAS 判断 state = 0,如果 state = 0 的话,就会释放所有等待的线程,await() 方法之后的语句得到执行。
21.3 用过 CountDownLatch 么?什么场景下用的
CountDownLatch 的作用就是 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。之前在项目中,有一个使用多线程读取多个文件处理的场景,我用到了 CountDownLatch 。
具体场景是下面这样的:我们要读取处理 6 个文件,这 6 个任务都是没有执行顺序依赖的任务,但是我们需要返回给用户的时候将这几个文件的处理的结果进行统计整理。为此我们定义了一个线程池和 count 为 6 的CountDownLatch对象。使用线程池处理读取任务,每一个线程处理完之后就将 count-1,调用CountDownLatch对象的 await()方法,直到所有文件读取完之后,才会接着执行后面的逻辑。
22. CyclicBarrier
22.1 CyclicBarrier 有什么用?
CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是:让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
22.2 CyclicBarrier 的原理是什么?
CyclicBarrier 内部通过一个 count 变量作为计数器,count 的初始值为 parties 属性的初始化值,每当一个线程到了栅栏这里了,那么就将计数器减 1。如果 count 值为 0 了,表示这是这一代最后一个线程到达栅栏,就尝试执行我们构造方法中输入的任务。
23. Semaphore
23.1 Semaphore有什么用?
synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,而Semaphore(信号量)可以用来控制同时访问特定资源的线程数量。
Semaphore 的使用简单,我们这里假设有 N(N>5) 个线程来获取 Semaphore 中的共享资源,下面的代码表示同一时刻 N 个线程中只有 5 个线程能获取到共享资源,其他线程都会阻塞,只有获取到共享资源的线程才能执行。等到有线程释放了共享资源,其他阻塞的线程才能获取到。
// 初始共享资源数量
final Semaphore semaphore = new Semaphore(5);
// 获取1个许可
semaphore.acquire();
// 释放1个许可
semaphore.release();
Semaphore 通常用于那些资源有明确访问数量限制的场景比如限流(仅限于单机模式,实际项目中推荐使用 Redis +Lua 来做限流。
23.2 Semaphore原理
Semaphore 是共享锁的一种实现,它默认构造 AQS 的 state 值为 permits,你可以将 permits 的值理解为许可证的数量,只有拿到许可证的线程才能执行。调用semaphore.acquire() ,线程尝试获取许可证,如果 state >= 0 的话,则表示可以获取成功。如果获取成功的话,使用 CAS 操作去修改 state 的值 state=state-1。如果 state<0 的话,则表示许可证数量不足。此时会创建一个 Node 节点加入阻塞队列,挂起当前线程。
24. Future类
24.1 作用
Future 类是异步思想的典型运用,主要用在一些需要执行耗时任务的场景,避免程序一直原地等待耗时任务执行完成,执行效率太低。具体来说是这样的:当我们执行某一耗时的任务时,可以将这个耗时任务交给一个子线程去异步执行,同时我们可以干点其他事情,不用傻傻等待耗时任务执行完成。等我们的事情干完后,我们再通过 Future 类获取到耗时任务的执行结果。这样一来,程序的执行效率就明显提高了。
四大功能:
取消任务;
判断任务是否被取消;
判断任务是否已经执行完成;
获取任务执行结果。
24.2 Callable 和 Future 有什么关系?
FutureTask 提供了 Future 接口的基本实现,常用来封装 Callable 和 Runnable,具有取消任务、查看任务是否执行完成以及获取任务执行结果的方法
24.3 CompletableFuture 类有什么用?
Future 在实际使用过程中存在一些局限性比如不支持异步任务的编排组合、获取计算结果的 get() 方法为阻塞调用。
Java 8 才被引入CompletableFuture 类可以解决Future 的这些缺陷。CompletableFuture 除了提供了更为好用和强大的 Future 特性之外,还**提供了函数式编程、异步任务编排组合(可以将多个异步任务串联起来,组成一个完整的链式调用)**等能力。
25. 锁体系
25.1 互斥锁与自旋锁
- 互斥锁加锁失败后,线程释放CPU,给其他线程,线程阻塞,开销大(两次线程上下文切换的成本)
- 自旋锁加锁失败后,线程会忙等待,直到它拿到锁,永远不会放弃 CPU
- 使用场景
如果是多核处理器,如果预计线程等待锁的时间很短,短到比线程两次上下文切换时间要少的情况下,使用自旋锁是划算的。
如果是多核处理器,如果预计线程等待锁的时间较长,至少比两次线程上下文切换的时间要长,建议使用互斥量。
如果是单核处理器,一般建议不要使用自旋锁。在单核 CPU 上,需要抢占式的调度器(即通过时钟中断一个线程,运行其他线程)。否则,自旋锁在单 CPU 上无法使用,因为一个自旋的线程永远不会放弃 CPU。
25.2 乐观锁与悲观锁
- 乐观锁
认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新时会判断此期间数据是否被更新采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作
java 中的乐观锁基本通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败 - 悲观锁
写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁
Java 中的悲观锁就是Synchronized,AQS 框架下的锁则是先尝试 cas 乐观锁去获取锁,获取不到,才会转为悲观锁,如 RetreenLock
25.3 公平锁与非公平锁
25.4 可重入锁(递归锁)
任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞。
Java中的可重入锁: ReentrantLock、synchronized修饰的方法或代码段。
可重入锁的作用:避免死锁。
25.5 共享锁与独占锁
可以有多个线程获取读锁,以共享的方式持有锁。和乐观锁、读写锁同义。
Java中用到的共享锁: ReentrantReadWriteLock。
只能有一个线程获取锁,以独占的方式持有锁。和悲观锁、互斥锁同义。
Java中用到的独占锁: synchronized,ReentrantLock
25.6 读写锁
读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由 jvm 自己控制的。
读锁:允许多个线程获取读锁,同时访问同一个资源。
写锁:只允许一个线程获取写锁,不允许同时访问同一个资源。
Java中的读写锁:ReentrantReadWriteLock