J.U.C 和 锁
线程安全是指多线程环境下,程序的执行结果不会受线程执行的先后顺序影响。
2.1.1 什么是AQS?
AbstractQueuedSynchronizer 多线程同步器,是JUC包中多个组件的底层实现。
AQS提供了两种锁机制,分别是:排他锁和共享锁。
排他锁 | 共享锁 | |
---|---|---|
定义 | 同一时刻只允许一个线程访问共享资源 | 读锁,同一时刻允许多个线程同时获得锁资源 |
举例 | ReentrantLock | CountDownLatch, Semaphore |
2.1.2 如何理解AQS的实现原理?
- 一个volatile修饰的state变量,作为一个竞态条件
- 用双向链表维护FIFO线程等待队列
具体原理:多线程对state共享变量进行修改来实现竞态条件,竞争失败的线程加入FIFO并阻塞,抢占到竞态资源的线程释放资源后,后续线程按照FIFO顺序唤醒。
2.1.3 AQS为什么要使用双向链表?
- 避免链表中存在异常线程导致无法唤醒后续线程
- 移除中断的线程时效率更高
- 避免阻塞和唤醒的开销,更容易判断前置节点是不是head节点
2.1.4 什么是CAS?
CAS是Java中Unsafe类提供的一种原子操作,它的全称是Compare And Swap,即比较并交换。
比如 compareAndSwapInt() 方法,入参有当前对象实例,成员变量state在内存中的偏移量,预期值0,期望更改后的值1。
CAS的底层实现中,在多核CPU环境下,会增加一个Lock指令对缓存或总线程加锁,从而保证比较并替换这两个指令的原子性。
2.1.5 什么是乐观锁,什么是悲观锁?
乐观锁是指在在操作数据时认为别的现成不会同时修改数据,因此不会上锁,但是在更新时会判断在此期间别的线程有没有更新过这个数据。
反之,悲观锁每次操作数据的时候都认为别的线程也会同时修改数据,所以每次操作都会上锁,这样别的线程想拿到这个数据就会阻塞,直到它拿到锁。
乐观锁适用于写少读多,悲观锁适用于写多读少。
乐观锁实现:数据库提供的write_condition机制,原子变量类。
悲观锁实现:行锁,表锁,读锁,写锁,synchronized, ReentrantLock。
2.1.6 什么条件下会产生死锁,如何避免死锁?
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象。 产生死锁的四个必要条件:
- 互斥条件,共享资源a和b每次只能被一个进程使用–不能破坏的条件
- 请求和保持条件;–可以破坏:首次执行时一次性申请所有的资源
- 不可抢占条件;–可以破坏:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源
- 循环等待。–可以破坏:按序申请资源
2.1.7 synchronized 和 Lock 的区别是什么?
synchronized | Lock | |
---|---|---|
特性 | Java内置的同步关键字 | J.U.C包下的一个接口,有多个实现类,比如ReentrantLock |
用法 | 可以同步对象,方法,代码块 | lock(), unlock()–通常写在finally中 |
性能 | 悲观锁 | 乐观锁 |
用途 | 非公平 | 公平/非公平 |
2.1.8 什么是可重入锁,它的作用是什么?
在运行的某个方法或者代码片段,因为抢占资源或者中断等原因,导致方法或者代码片段的运行中断,等待中断程序执行结束后,重新进入这个方法运行,并且结果不会受到影响,那这个方法就是可重入的。
一个线程如果抢占到了互斥锁,在锁释放之前再去竞争同一把锁,不需要等待,只需要记录重入次数。
大部分锁是可重入的,比如synchronized, ReentrantLock。
不可重入锁,比如读写锁StampedLock。
作用:避免死锁。
2.1.9 ReentrantLock 的实现原理是什么?
ReentrantLock 是一种可重入的排他锁。它底层实现的关键技术有
- 锁竞争,通过互斥变量,使用CAS机制实现。没有竞争到锁的线程,使用了AQS 队列同步器存储。
- 公平性和非公平
- 重入性
2.1.10 ReentrantLock 是如何实现锁的公平性和非公平性的?
默认采用了非公平锁。
公平锁的实现方式是,线程在竞争锁资源的时候判断AQS同步队列中有没有等待的线程,如果有,就加入队尾等待。
非公平锁的实现方式是,不管队列中是否有线程在等待,都先尝试抢占资源,如果抢不到,再加到队尾。
2.1.11 说说你对行锁、间隙锁、临建锁的理解?
这些是MySQL中InnoDB引擎下解决事务隔离性的一系列排他锁。
行锁:避免其他事务对这一行数据进行修改。
select * from table where id = 1 for update;
间隙锁:锁定一个索引区间。基于索引的范围查询,无论是否是唯一索引,都会自动触发间隙锁。比如基于between的范围查询,会锁定左边界和右边界之间的间隙。
主要是为了解决同一事务中两次查询不同的幻读问题。但可能产生死锁问题。
临建锁:行锁和间隙锁的组合。
select * from table where id between 5 and 7 for update;
2.1.12 如何理解java中令人眼花缭乱的各种并发锁?
根据不同情况使用不同的锁
1. 某个线程是否锁住同步资源
要锁住同步资源(写多读少),使用悲观锁,不锁住(写少读多),就用乐观锁。
2. 多个线程是否共享一把锁
共享锁,比如读锁,ReentrantReadWriteLock。 排他锁,synchronized, Lock。
3. 多个线程竞争时是否要排队
公平锁:Lock lock = new ReentrantLock(true);
非公平锁: Lock lock = new ReentrantLock(false);
4. 一个线程的多个流程是否能获取同一把锁
可重入锁,比如synchronized, ReentrantLock。
不可重入锁。
5. 某个线程锁住同步资源失败,该线程是否阻塞
如果希望线程不阻塞,可以使用自旋锁或者自适应自旋锁。
自旋锁是指线程再没有获得锁时不是被挂起,而是执行一个忙循环,不断的尝试获取锁。目的是减少线程被挂起的概率。不适合锁占用时间长的并发情况。AtomicInteger 就是自旋锁的实现。
6. 线程竞争同步资源时,细节流程是否发生变化
无锁(乐观锁)-> 偏向锁(第一线访问锁的线程不需要重复获取锁) -> 轻量级锁(自旋) -> 重量级锁(互斥锁)
7. 锁再设计和锁优化
分段锁:ConcurrentHashMap 1.7 底层使用分段锁Segment。
锁粗化:如果多个线程对同一个对象进行多次加锁,那么可以将多个锁合并为一个锁。 锁消除
2.1.13 阻塞队列被异步消费,怎么保持顺序?
- 阻塞队列本身符合FIFO队列特性。
- 阻塞队列中,使用了condition条件等待来维护两个等待队列。
- 一个是队列为空的时候存储被阻塞的消费者。
- 一个是队列满的时候存储被阻塞的生产者。
- 阻塞队列的消费过程,有两种情况
- 阻塞队列中已经包含了很多任务,启动多个消费者去消费任务,它的有序性是由加锁实现
- 如果由多个消费者线程因为阻塞队列中没有任务而被阻塞,那么它们是按照FIFO的顺序存储到 condition 条件等待队列中的。当阻塞队列中有任务要处理的时候,这些被阻塞的消费者线程会按照FIFO的顺序被唤醒。
2.1.14 基于数组的阻塞队列ArrayBlockingQueue的实现原理是什么?
阻塞队列(BlockingQueue)在队列的基础上,增加了两个附加操作:队列为空时,获取元素的线程会等待队列变为非空;当队列满时,存储元素的线程会等待队列可用。
阻塞队列可以非常容易地实现生产-消费者模型,其中用到两种关键技术:队列的存储,以及线程的阻塞和唤醒。
ArrayBlockingQueue 是基于数组结构的阻塞队列,循环数组。
线程的阻塞和唤醒用到了 ReentrantLock 和 condition 。