1. 锁的分类
① 乐观锁与悲观锁
- 悲观锁:
- 对共享数据进行访问时,悲观锁总是认为一定会有其他线程修改数据。如果不加锁,肯定会出问题。
- 因此,悲观锁无论是否出现共享数据的争用,在访问数据时都会先加锁。
- Java中同步互斥都是采用这种悲观的并发策略,synchronized关键字和Lock接口的实现类都是悲观锁。
- 乐观锁:
- 对共享数据进行访问时,乐观锁总是认为不会有其他线程修改数据修改数据。
- 于是直接执行操作,只是在更时检查数据是否已经被其他线程修改。
- 如果没有被修改,则操作执行成功;否则,添加其他补偿措施。
- 常见的补偿措施是不断尝试,直到成功。
- Java中的非阻塞同步都是采用这种乐观的并发策略,乐观锁在Java中是通过使用无锁编程来实现,最常使用的CAS操作。
- 比如,线程安全的原子类的自增操作,就是通过循环的CAS操作实现的。
② 独占锁和共享锁
- 独占锁:
- 又叫排它锁,同一个锁对象,同一时刻只允许一个线程获取到锁。
- 如果线程T对数据A加上独占锁后,其他线程不能对该数据再加任何类型的锁(包括独占锁和共享锁),自己可以对数据进行读操作或者写操作。
- 独占锁允许线程对数据进行读写操作。
- Java中的 synchronized关键字、Mutex、ReentrantLock、
ReentrantReadWriteLock
中写锁,都是独占锁。 - 共享锁:
- 同一个所对象,同一时刻允许多个线程获取到锁。
- 线程T对数据A加上共享锁,则其他线程只能对数据A加共享锁,不能加独占锁。
- 共享锁只允许对数据进行读操作。
- java中
ReentrantReadWriteLock
中读锁是共享锁。 ReentrantReadWriteLock
读写锁的获取:
- 同步状态不为0,如果有其他线程获取到读锁或者当前线程不是持有写锁的线程,则获取写锁失败进入阻塞状态;否则,当前线程是持有写锁的线程,直接通过
setState()
方法增加写状态。 - 同步状态为0,直接通过
compareAndSetState()
方法实现写状态的CAS增加,并将当前线程设置为持有写锁的线程。 - 如果有其他线程获取到了写锁,则获取读锁失败进入阻塞状态。
- 如果写锁未被获取或者该线程为持有写锁的线程,则获取读锁成功,通过
compareAndSetState()
方法实现读状态的CAS增加
- 独占锁和共享锁都是通过AQS实现的,
tryAcquire()
或者tryAcquireShared()
方法支持独占式或者共享式的获取同步状态。
③ 公平锁和非公平锁
- 公平锁:
- 当锁被释放,按照阻塞的先后顺序获取锁,即同步队列头节点中的线程将获取锁。
- 公平锁可以保证锁的获取按照FIFO原则,但需要进行大量的线程切换,导致吞吐率较低。
- 非公平锁:
- 当锁被释放,所有阻塞的线程都可以争抢获取锁的资格,可能导致先阻塞的线程最后获取锁。
- 非公平锁虽然可能造成线程饥饿,但极少进行线程的切换,保证了更大的吞吐量。
- Java中ReentrantLock和ReentrantReadWriteLock支持公平和非公平访问,而synchronized关键字只支持非公平访问。
- 公平与非公平可以通过构造函数的
fair
参数进行指定,默认是false
,即默认为非公平的获取锁。 - 公平和非公平都是依靠AQS实现的,公平使用
FairSync
同步器,非公平使用NoFairSync
同步器。
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
④ 可重入锁和非可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。下面用示例代码来进行分析:
- 可重入锁:
- 已经获取锁的线程再次获取该锁而不被锁所阻塞,需要解决线程再次获取锁和锁的最终释放两个问题。
- 可重入锁可以一定程度的避免死锁。
- 非可重入锁:
- 已经获取锁的线程再次获取该锁,会因为需要等待自身释放锁而被阻塞。
- 非可重入锁容易造成当前线程死锁,从而使整个队列中线程永久阻塞。
- Java中的synchronized关键字、ReentrantLock锁和ReentrantReadWriteLock锁都支持重进入,其中
ReentrantReadWriteLock
的读锁是支持重进入的共享锁,写锁是支持重进入的独占锁。
⑤ 无锁VS偏向锁VS轻量级锁VS重量级锁
synchronized
关键字实现同步的基础是每个对象都是一个锁,它依靠对象头存储锁。- 无锁、偏向锁、轻量级锁、重量级锁都是专门针对
synchronized
关键字设计的、级别从低到高的4种状态。 - 注意: 锁状态只能升级,不能降级。
- 对象头中的第一个字宽叫做
Mark Word
,用于存储对象的hashCode、分代年龄、锁等信息。 - 其中最后2 bit的标志位,用于标记锁的状态。根据标志位的不同,可以有如下几种状态:
无锁
- 不对资源进行锁定,所有的线程都可以访问并修改同一资源,但同一时刻只有一个线程能修改成功。
- 无锁的修改操作依靠循环实现: 如果没有争用,修改成功并退出循环;否则,循环尝试修改操作,直到成功。
- 无锁无法全面代替有锁,但在某些场景下具有非常高的性能。
- 无锁的经典实现: CAS操作。
偏向锁
- 出现的原因:
- 在无竞争的情况下,同一线程可能多次进入同一个同步块,即多次获取同一个锁。
- 如果进入和退出同步块都使用CAS操作来加锁和解锁,则会消耗一定的资源。
- 于是通过CAS操作将线程ID存储到
Mark Word
中,线程再次进入或退出同步块时,直接检查Mark Word
中是否存储指向当前线程的偏向锁。如果存储了,则直接进入或退出同步块。
- 偏向锁可以在无竞争的情况下,尽量减少不必要的轻量级锁执行路径。轻量级锁的加锁和解锁都需要CAS操作,而偏向锁只有将线程ID存储到
Mark Word
中时才执行一次CAS操作。 - 偏向锁的释放:
- 当有其他线程竞争偏向锁时,持有偏向锁的线程会释放锁偏向锁。
- 释放时,会根据锁对象是否处于锁定状态而恢复到不同的状态。
- 如果锁对象处于未锁定状态,撤销偏向后恢复到无锁的状态(
0 + 01
);如果锁对象处于锁定状态,撤销偏向后恢复到轻量级锁的状态(00
)。
- 偏向锁在JDK1.6及以后,默认是启用的,即
-XX:+UseBiasedLocking
。可以通过-XX:-UseBiasedLocking
关闭偏向锁。
轻量级锁
- 多个线程竞争同步资源时,没有获取到资源的线程自旋等待锁的释放。
- 加锁过程:
- 线程进入同步块时,如果同步对象处于无锁状态(
0 + 01
),JVM 首先在当前线程的栈帧中开辟一块叫做锁记录(Lock Record
)的空间,用于存储同步对象的Mark Word
的拷贝。这个拷贝加了一个前缀,叫Displaced Mark Word
。 - 然后通过CAS操作将同步对象的
Mark Word
更新为指向Lock Record
的指针,并将Lock Record
里的owner指针指向同步对象的Mark Word
。 - 如果这个更新动作成功,则当前线程拥有了该对象的锁,
Mark Word
中的标志位更新为00
,表示对象处于轻量级锁定状态。 - 如果更新动作失败,JVM首先会检查同步对象的
Mark Word
是否指向当前线程的栈帧。如果是,说明当前线程已经持有了该对象的锁,可以直接进入同步块继续执行;否则,说明存在多线程竞争锁。
- 轻量级锁升级为重量级锁:
- 若当前只有一个线程在等待,则通过自旋进行等待。自旋超过一定的次数,轻量级锁升级为重量级锁。
- 若一个线程持有锁,一个线程自旋等待锁,又有第三个线程想要获取锁,轻量级锁升级为重量级锁。
- 锁的释放:
- 通过CAS操作,将
Lock Record
中的Displaced Mark Word
与对象中的Mark Word
进行替换。 - 替换成功,同步状态完成;替换失败,说明有其他线程尝试获取过该锁,释放锁的同时需要唤醒被挂起的线程。
重量级锁
- 多线程竞争同步资源时,没有获取到资源的线程阻塞等待锁的释放。
- 轻量级锁升级为重量级锁,锁的标志位变成
10
,Mark Word
中存储的是指向重量级锁的指针。 - 所有等待锁的线程都会进入阻塞状态。
⑥ 自旋锁与自适应自旋锁
- 自旋锁:
- 阻塞或唤醒一个线程都需要从用户态切换到内核态去完成,会对性能造成很大影响。
- 有时一个线程持有锁的时间很短,如果在很短的时间内让后续获取锁的线程都进入阻塞态,这是很不值得。
- 可以让后续线程持有
CPU
时间等待一会,这个等待需要执行忙循环(自旋) 来实现。 - 自旋等待的时间由自旋次数来衡量,默认为10,可以使用
-XX:PreBlockSpin
来进行设置。 - 如果在自旋等待中,持有锁的线程释放该锁,当前线程可以不必阻塞直接获取同步资源。
- 如果超过自旋次数仍未获取成功,则使用传统的方法将其阻塞。
- 自旋锁的实现原理: 循环的CAS操作
- 自旋锁的缺点:
- 自旋锁虽然避免了线程的切换开销,但是会占用
CPU
时间。 - 如果每个等待获取锁的线程总是自旋规定的次数,却又没有等到锁的释放,这样就白白浪费了CPU时间。
- 自旋锁在
JDK1.4.2
中引入,默认是关闭的;在JDK1.6
中变成默认开启,并为了解决自旋锁中浪费CPU
资源的问题,而引入了自适应自旋锁。 - 自适应自旋锁:
- 自适应意味着自旋的次数不再固定,而是根据上一次在同一个锁自旋的次数和锁的拥有者的状态来决定。
- 如果在同一个锁对象上自旋刚刚成功获取过锁,并持有锁的线程处于运行状态,则可以认为这一次自旋也很可能成功,允许它自旋更长的时间。
- 如果在一个锁上,自旋很少成功,则下一次可以省略自旋过程,直接阻塞线程,避免浪费处理器资源。
2. 锁的有关问题总结
1. Java中的乐观锁与悲观锁
- 基础: 什么是乐观锁,什么是悲观锁?二者的典型代表(非阻塞同步和互斥同步)
- 进阶: CAS如何实现原子更新的;JUC包中的原子类如何实现线程安全的自增。
2. 一个线程怎么判断自己是否可以获得共享资源的锁?
- Lock的实现类,线程在调用 lock()方法获取锁时,
lock()
方法会调用AQS中的模板方法,模板方法会调用AQS中的可重写方法。在可重写方法中,会通过判断同步状态,决定是否可以获取共享资源的锁。 - 深入举例:
ReentrantLock
的公平和非公平获取锁:同步状态为0和不为0的情况。ReentrantReadWriteLock
的写锁获取: 同步状态不为0和为0的情况。ReentrantReadWriteLock
的读锁的获取: 不是持有写锁的线程,获取失败;其他情况获取成功。
3. 共享资源state何时增加何时减少?对于synchronized和lock有什么区别?
- 加锁state的值加1,解锁state的值减1,
state = 0
表示锁被释放。 synchronized
和lock都支持重进入:lock依靠AQS中的同步状态,synchronized
依靠对象头中Mark Word的锁标志位处于可偏向状态(1 + 01
)。
4. 对于++i操作,如何不使用锁进行同步,保证其线程安全?
- JUC包中的原子类的自增运算(incrementAndGet()或者compareAndSet()),实质: 循环的CAS操作。
5. CAS操作的实现机制?
- 基础: 如何实现原子更新的、Java中对CAS的支持
- 进阶:
- 如何使用CAS操作实现非同步互斥,以原子类的自增运算为例。
- CAS操作存在的问题:ABA问题、循环时间过长消耗大、只能保证一个共享变量的原子操作。
6. 公平锁与非公平锁
- 基础: 什么是公平锁,什么是非公平锁。
- 进阶:
synchronized
只支持非公平访问,ReentrantLock
和ReentrantReadWriteLock
支持公平与非公平访问。- 以
ReentrantLock
为例,讲解如何支持公平与非公平的:构造函数中指定fair参数,对应的AQS为FairSync
或者NonfairSync
,公平访问要求没有前驱节点。
7. lock ,sychronized,volatile的区别
- lock的特性: 非阻塞获取锁、可中断获取锁、超时获取锁;lock接口的几种方法;常见的Lock的实现类。
synchronized
的同步基础,如何实现同步。volatile
保证变量在线程间的可见性和禁止指令重排序。只适合运算的结果不依赖当前变量值得情况,无法解决线程同步。- 总结: 以
sychronized
和ReentrantLock
为例,二者的异同:实现、性能、是否可中断、公平性支持、是否可绑定多个对象。
- lock和synchronized的区别:
- 实现: lock由jdk支持,
synchronized
由JVM支持;导致前者需要显式的获取、释放锁;后者由JVM隐式完成。 - 性能:
synchronized
进行了很多的优化,目前二者的性能大致相同。 - lock的特性: 支持非阻塞获取(
tryLock
)、中断获取(lockinterruptibly
)、超时获取(带参数的tryLock),而synchronized
不支持。 - 二者的实现机制:
synchronized
依靠进入和退出monitor对象,实现同步方法或同步代码块;lock依靠AQS实现锁的获取和释放。 - 线程间的协作:
synchronized
依靠Object的监视器方法wait()
、notify()/notifyAll()
,实现等待/通知模式;lock接口的实现类,依靠绑定的condition对象的await()
、signal()/signalAll()
,实现等待/通知模式。
8. 谈一下Java中的锁
- 锁是通过互斥同步实现Java线程安全的一种手段,JVM以
synchronized
关键字提供锁功能,JDK1.5及以后
提供依靠Lock及其实现类提供锁功能。 - 讲解方法一:
synchronized
关键字:实现同步的基础,锁存储的位置、如何实现同步。[ 如何保证原子性、可见性、顺序性 ]- Lock接口: 如何借助AQS实现锁;
ReentrantLock
的重进入、公平与非公平;ReentrantReadWriteLock
的写锁/读锁的获取。 synchronized
与ReentrantLock
的比较与选择- 锁的优化:自旋锁与自适应自旋锁、锁消除、锁粗化、偏向锁、轻量级锁
- 讲解方法二:
- 独占锁与共享锁、公平锁与非公平锁、可重进入锁与非可重进入锁、乐观锁与悲观锁、自旋锁与自适应自旋锁、无锁
VS
偏向锁VS
轻量级锁VS
重量级锁,它们的含义、具体的实例等
9. synchronized关键字和ReentrantLock
- 基础:
synchronized
的同步基础、锁存储的位置、如何实现同步;ReentrantLock
的特性重进入、公平与非公平 - 进阶: 二者的比较与选择
10. 共享锁与独占锁
- 独占锁: 一个线程持有锁,其他线程不能再获取锁,无论执行读操作还是写操作
- 共享锁: 一个线程持有读锁,其他线程可以在获取读锁,不能再获取写锁
11. 单例模式如何解决高并发?
- DCL(双重锁检测)的单例模式: 将
instance
定义为volatile
,使用synchronized
再次进行instance
为null
的判断。 - 重点: 两次null判断的意义,添加volatile关键字的意义。
12. AQS和condition对象的关系
ConditionObject
是AQS的内部类,一个AQS不仅拥有自身的同步队列,还拥有ConditionObject
的等待队列;而且,一个AQS可以绑定多个condition对象。- 调用condition对象的
await()
方法时,要求当前线程已经获取到锁。即该线程处于同步队列的头节点中。接着当前线程释放该并将自己封装成新的节点加入到等待队列中,进入等待状态;从AQS的角度看,调用await()
方法时,节点从同步队列的头节点移动到等待队列的尾节点。 - 调用condition对象的
signal()
方法时,会唤醒处于等待队列中等待最久的节点,即等待队列的头节点;线程想从await()
方法返回,必须获取到锁,因此线程又会封装成同步节点,加入到同步队列中。从AQS的角度看,调用signal()
方法时,节点从等待队列的头节点移动到同步队列的尾节点。
13. lock接口的实现
- 定义lock接口的实现类,在其中创建AQS的子类(静态内部类)作为同步器;
- 实现AQS的子类: 调用更改同步状态的三种核心方法,实现AQS中提供的可重写方法。
- 实现lock接口中的方法: 调用同步器中的模板方法,实现lock接口的
lock()
、unlock()
等方法。