1. 什么是AQS
?
1.1. 概述
全称是AbstractQueuedSynchronizer
,是阻塞式锁和相关的同步器工具的框架,它是构建锁或者其他同步组件的基础框架
AQS
与Synchronized
的区别
synchronized | AQS |
---|---|
关键字,c++ 语言实现 | java 语言实现 |
悲观锁,自动释放锁 | 悲观锁,手动开启和关闭 |
锁竞争激烈都是重量级锁,性能差 | 锁竞争激烈的情况下,提供了多种解决方案 |
AQS
常见的实现类
-
ReentrantLock
阻塞式锁 -
Semaphore
信号量 -
CountDownLatch
倒计时锁
1.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
,它默认就是非公平锁,新的线程与队列中的线程共同来抢资源
2. ReentrantLock
的实现原理
2.1. 概述
ReentrantLock
翻译过来是可重入锁,相对于synchronized
它具备以下特点:
-
可中断
-
可以设置超时时间
-
可以设置公平锁
-
支持多个条件变量
-
与
synchronized
一样,都支持重入
2.2. 实现原理
ReentrantLock
主要利用CAS+AQS
队列来实现。它支持公平锁和非公平锁,两者的实现类似
构造方法接受一个可选的公平参数(默认非公平锁),当设置为true
时,表示公平锁,否则为非公平锁。公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。
查看ReentrantLock
源码中的构造方法:
提供了两个构造方法,不带参数的默认为非公平
如果使用带参数的构造函数,并且传的值为true
,则是公平锁
其中NonfairSync
和FairSync
这两个类父类都是Sync
而Sync
的父类是AQS
,所以可以得出ReentrantLock
底层主要实现就是基于AQS
来实现的
工作流程
-
线程来抢锁后使用
cas
的方式修改state
状态,修改状态成功为1,则让exclusiveOwnerThread
属性指向当前线程,获取锁成功 -
假如修改状态失败,则会进入双向队列中等待,
head
指向双向队列头部,tail
指向双向队列尾部 -
当
exclusiveOwnerThread
为null
的时候,则会唤醒在双向队列中等待的线程 -
公平锁则体现在按照先后顺序获取锁,非公平体现在不在排队的线程也可以抢锁
3. synchronized
和Lock
有什么区别 ?
参考回答
- 语法层面
synchronized
是关键字,源码在jvm
中,用c++
语言实现Lock
是接口,源码由jdk
提供,用java
语言实现- 使用
synchronized
时,退出同步代码块锁会自动释放,而使用Lock
时,需要手动调用unlock
方法释放锁
- 功能层面
- 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
Lock
提供了许多synchronized
不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量Lock
有适合不同场景的实现,如ReentrantLock
,ReentrantReadWriteLock
- 性能层面
- 在没有竞争时,
synchronized
做了很多优化,如偏向锁、轻量级锁,性能不赖 - 在竞争激烈时,
Lock
的实现通常会提供更好的性能
- 在没有竞争时,
4. 死锁产生的条件是什么?
死锁:一个线程需要同时获取多把锁,这时就容易发生死锁
例如:
t1
线程获得A对象锁,接下来想获取B
对象的锁
t2
线程获得B
对象锁,接下来想获取A
对象的锁
代码如下:
package com.dcxuexi.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
的锁。
5. 如何进行死锁诊断?
当程序出现了死锁现象,我们可以使用jdk
自带的工具:jps
和jstack
步骤如下:
第一:查看运行的线程
第二:使用jstack
查看线程运行的情况,下图是截图的关键信息
运行命令:jstack -l 46032
其他解决工具,可视化工具
jconsole
用于对jvm
的内存,线程,类 的监控,是一个基于jmx
的GUI
性能监控工具
打开方式:java
安装目录bin
目录下 直接启动jconsole.exe
就行
VisualVM
:故障处理工具
能够监控线程,内存情况,查看方法的CPU
时间和内存中的对 象,已被GC
的对象,反向查看分配的堆栈
打开方式:java
安装目录bin
目录下 直接启动jvisualvm.exe
就行
6. ConcurrentHashMap
ConcurrentHashMap
是一种线程安全的高效Map
集合
底层数据结构:
-
JDK1.7
底层采用分段的数组+链表实现 -
JDK1.8
采用的数据结构跟HashMap1.8
的结构一样,数组+链表/红黑二叉树。
6.1. JDK1.7
中concurrentHashMap
数据结构
- 提供了一个
segment
数组,在初始化ConcurrentHashMap
的时候可以指定数组的长度,默认是16,一旦初始化之后中间不可扩容- 在每个
segment
中都可以挂一个HashEntry
数组,数组里面可以存储具体的元素,HashEntry
数组是可以扩容的- 在
HashEntry
存储的数组中存储的元素,如果发生冲突,则可以挂单向链表
存储流程
- 先去计算
key
的hash
值,然后确定segment
数组下标 - 再通过
hash
值确定hashEntry
数组中的下标存储数据 - 在进行操作数据的之前,会先判断当前
segment
对应下标位置是否有线程进行操作,为了线程安全使用的是ReentrantLock
进行加锁,如果获取锁是被会使用cas
自旋锁进行尝试
6.2. JDK1.8
中concurrentHashMap
在JDK1.8
中,放弃了Segment
臃肿的设计,数据结构跟HashMap
的数据结构是一样的:数组+红黑树+链表
采用CAS + Synchronized
来保证并发安全进行实现
-
CAS
控制数组节点的添加 -
synchronized
只锁定当前链表或红黑二叉树的首节点,只要hash
不冲突,就不会产生并发的问题 , 效率得到提升
7. 导致并发程序出现问题的根本原因是什么?
Java
并发编程三大特性
-
原子性
-
可见性
-
有序性
(1)原子性
一个线程在CPU
中操作不可暂停,也不可中断,要不执行完成,要不不执行
比如,如下代码能保证原子性吗?
以上代码会出现超卖或者是一张票卖给同一个人,执行并不是原子性的
解决方案:
1.synchronized
:同步加锁
2.JUC
里面的lock
:加锁
(2)内存可见性
内存可见性:让一个线程对共享变量的修改对另一个线程可见
比如,以下代码不能保证内存可见性
解决方案:
-
synchronized
-
volatile
(推荐) -
LOCK
(3)有序性
指令重排:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的
还是之前的例子,如下代码:
解决方案:
- volatile