前言:
这是我参加工作之后,第一次开始写博客,我也不知道自己是怎么了,就突然想在网上沉淀点自己的东西,可能是平时在网上拿来主义太多,让我有点不太好意思吧。也可能是最近出去面试了几家公司,发现自己这两年都是在业务代码,对于底层的沉淀太少,虽然业务代码对我来说,没什么问题,但是只要涉及到底层的东西,我就会哑口无言,才让我痛下决心,一定要沉淀点属于自己的东西。不管怎么样,今天开始了这个博客,希望自己能够坚持住,保持自己的初心,持续更新技术和自己生活中的乐趣。共勉!!!
好了废话就不多说,开始下面的整体:
一. ReentrantLock介绍:
ReentrantLock是可重入锁,那么顾名思义就是支持重复进入的锁,该锁表示支持同一线程对资源的重复加锁,即当前线程获取该锁不会被阻塞。该锁实现了Lock接口的一个类,也是在实际工作中经常使用到的一个锁。
在java关键字synchronized隐式支持重入性,synchronized通过获取自增,释放自减的方式实现重入。与此同时,ReentrantLock还支持公平锁和非公平锁两种方式。那么,要想完完全全的弄懂ReentrantLock的话,主要也就是ReentrantLock同步语义的学习:
- 重入性的实现原理;
- 公平锁和非公平锁。
二. ReentrantLock实现原理:
ReentrantLock两种锁实现的底层都是继承了AbstractQueuedSynchronizer(AQS同步机制框架)和 final volatile status的同步状态机制。
//status的状态值:
SIGNAL: 当前节点的后继处于阻塞状态(or即将处于阻塞状态),因此当当前节点的线程发生了 release或者cancel后,应该使得其后继节点发生移动.为了避免竞争,acquire方法 必须先暗示他们需要一个signal,然后重复尝试原子的acquire操作,如果失败则进入阻塞状态。
CANCELLED:节点被撤销通常都是因为超时或者被中断.节点从来不会保存这个状态.特殊的是被撤销节点的线程不会再出现阻塞。
CONDITION:当前节点处于条件队列.它不会用作同步队列节点直到从条件队列被转移走,一旦被转移走,它的status值将会被设置为0(此值和status的其它值之间没有什么关系,但简化了同步机制)。
PROPAGATE:方法releaseShared的调用应该向其它节点传播这一消息.为了保证持续传播,这一状态值在doReleaseShared方法中被设置(设定操作只能由头节点完成),即使有其它操作穿插其中。
0: 非上述情况的其它情况。(锁最初的状态)
重进入是任意线程在获取到锁之后,能够再次获取锁而不被阻塞的现象,满足这个条件,那么程序必须实现两个条件:
1. 锁需要去识别获取锁的线程是否为当前占据的线程,如果是则再次获取成功。
2. 锁的最终释放,由于锁会被获取N次,只有锁被释放N次之后,该锁才会被释放。
ReentrantLock是通过组合自定义同步器实现锁的获取与释放,重入锁的默认构造方法是非公平锁机制,下面以非公平锁为例:
//默认构造方法里面实现的是:非公平锁机制
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//当C==0表示:当前锁出于默认状态,未被任何线程占用,当前线程可以获取该锁
if (c == 0) {
//status== 0,通过CAS竞争成功,将state的状态值设置为acquires。
if (compareAndSetState(0, acquires)) {
//设置锁的拥护者为当前线程,并返回true
setExclusiveOwnerThread(current);
return true;
}
}
//若被占用,检查当前线程是否为占用线程
else if (current == getExclusiveOwnerThread()) {
//如果是占用线程是当前线程,则不参与锁的竞争,并将当前的状态state设置为原状态加上acquires。
int nextc = c + acquires;
if (nextc < 0) // overflow 防溢出
throw new Error("Maximum lock count exceeded");
//再次占用锁,在原来的基础上加1,与前面说到的第二个条件吻合,释放的时候也需要释放相应的次数
setState(nextc);
return true;
}
return false;
}
该方法的逻辑首先判断该锁是否被获取过,没有获取就直接获取,并将锁的调用状态值设置为acquires(其实就是1),也就是记录需要释放锁一次;第二个if判断就充分表达出这个锁为什么是重入锁的原因请看,它首先判断当前获取锁的线程是否是当前的线程,如果是当前的线程,则获取次数在原来的基础上加上acquires,同理释放的时候的需要释放相应的次数。
下面再来看下非公平锁的释放方法:
//非同步锁的释放机制
protected final boolean tryRelease(int releases) {
//当前获取锁的线程获取数量减一
int c = getState() - releases;
//判断当前获取锁的线程是否是当前线程
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//当我们的获取数量和释放数量相同的时候,也就是C等于零的时候,设置当前获取锁的线程为空,也就是最初期状态
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
//否则设置当前获取锁的数量为减一,并返回false。
setState(c);
return free;
}
该方法的释放逻辑为:首先在原来的获取次数上减一,并判断当前获取锁的线程是否是当前线程,如果不是就直接抛出异常,如果是当前线程,判断当前获取锁的数量是否减到了0,如果是方法返回true,并将当前获取锁的线程设置为空,如果不是0,就在原来获取次数上减一并返回false。
从这里我们可以看出,非同步线程的释放锁机制,一定要是当前线程获取锁的数量为零,才能释放锁,这点非常重要。
我们再来看下非公平锁的类:
/**
* 非公平锁继承Sync
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* 获取锁的资源
*/
final void lock() {
//直接参与锁的竞争,假设当前线程获取锁的数量为0,获取锁之后,在原来的基础上加一
if (compareAndSetState(0, 1))
//并设置设置获取锁的线程为当前线程
setExclusiveOwnerThread(Thread.currentThread());
else
//失败则调用 AbstractQueuedSynchronizer.acquire()获取锁
acquire(1);
}
//实质上方法tryAcquire底层还是调用前面的nonfairTryAcquire()方法
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
关于方法acquire()方法源码分析,详见我的下篇博客(AQS源码分析)。
三. 公平锁和非公平锁
ReentrantLock重入锁支持两种方式,一个是公平锁和另外一种非公平锁;如果一个锁是公平锁,那么在锁的机制上则完全符合时间上绝对顺序,也就是FIFO(先进先出队列)。
//默认构造器方法是实现非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
当前ReentrantLock也提供了另外一个构造方法,也就是:
//传入一个boolean值,来判断构造方法实现锁的方式
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
下面我们来分析下公平锁的类源码:
/**
* 公平锁的实现机制
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
//这里的获取锁资源方法,与非公平锁不一样,直接调用了acquire(1)这个方法,而没有去立刻获取锁资源。
final void lock() {
acquire(1);
}
/**
*
* 尝试获取锁
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//与非公平锁不同的一点是新增了一个方法hasQueuedPredecessors()
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
从源码中观察可以发现,其实公平锁和非公平锁的实现机制是差不多的,只不过在获取锁资源上面多了一个方法,就是hasQueuedPredecessors()方法。
该方法的作用是:加入同步队列的当前节点是否有前驱节点的判断,如果有前驱节点,表明在队列中有线程比当前线程更早的请求获取锁资源,所以这里方法必然会返回false,需要等待前线程获取锁资源并释放锁资源之后,重复上面的步骤直到hasQueuedPredecessors()方法返回false之后,当前线程才有机会去获取锁资源。
由此我们可以得出:公平锁每次都是从同步队列线程的第一个子节点获取锁资源,而非公平锁则不一定是从同步队列线程的第一个子节点获取锁资源,有可能是刚刚释放锁资源的线程。
总结:
-
公平锁每次获取到锁为同步队列中的第一个节点,保证请求资源时间上的绝对顺序,而非公平锁有可能刚释放锁的线程下次继续获取该锁,则有可能导致其他线程永远无法获取到锁,造成“饥饿”现象。
-
公平锁为了保证时间上的绝对顺序,需要频繁的上下文切换,而非公平锁会降低一定的上下文切换,降低性能开销。因此,ReentrantLock默认选择的是非公平锁,则是为了减少一部分上下文切换,保证了系统更大的吞吐量。
方法用例:
class ReentrantLockDemo {
private final static Logger logger = LoggerFactory.getLogger(ReentrantLock.class);
//定义一个公平锁
private final static ReentrantLock lock1 = new ReentrantLock(true);
//定义一个非公平锁
private final static ReentrantLock lock2 = new ReentrantLock(false);
/**
* 非公平锁使用方法
*/
public void reentrantLockFalse(){
try{
//获取锁资源
lock2.lock();
for(int i =0 ; i< 3; i++){
System.out.println("非公平锁获取线程的名称为:" + Thread.currentThread().getName() + "非公平锁遍历次数为:" + i);
}
}catch (Exception e){
logger.error("非公平锁使用抛出异常 ReetrantLockFalse >> e = {}", ExceptionUtils.getMessage(e));
throw e;
} finally {
//释放锁资源
lock2.unlock();
}
}
/**
* 公平锁的使用方法
*/
public void reentrantLockTrue(){
try{
//获取锁资源
lock1.lock();
for(int i =0 ; i< 3; i++){
System.out.println("公平锁获取线程的名称为:" + Thread.currentThread().getName() + "非公平锁遍历次数为:" + i);
}
}catch (Exception e){
logger.error("公平锁使用抛出异常 ReetrantLockFalse >> e = {}", ExceptionUtils.getMessage(e));
throw e;
}finally {
//释放锁资源
lock1.unlock();
}
public static void main(String[] args) {
ReentrantLockDemo lockDemo = new ReentrantLockDemo();
//声明一个非公平线程锁
MethodA methodA1 = new MethodA(lockDemo);
MethodA methodA2 = new MethodA(lockDemo);
MethodA methodA3 = new MethodA(lockDemo);
//声明一个公平线程锁
MethodB methodB1 = new MethodB(lockDemo);
MethodB methodB2 = new MethodB(lockDemo);
MethodB methodB3 = new MethodB(lockDemo);
methodA1.start();
methodA2.start();
methodA3.start();
methodB1.start();
methodB2.start();
methodB3.start();
}
}
class MethodA extends Thread {
private ReentrantLockDemo lock;
public MethodA(ReentrantLockDemo lock){
this.lock = lock;
}
@Override
public void run() {
lock.reentrantLockFalse();
}
}
class MethodB extends Thread {
private ReentrantLockDemo lock;
public MethodB(ReentrantLockDemo lock){
this.lock = lock;
}
@Override
public void run() {
lock.reentrantLockTrue();
}
}
运行结果:
##非公平锁运行结果
非公平锁获取线程的名称为:Thread-0非公平锁遍历次数为:0
非公平锁获取线程的名称为:Thread-0非公平锁遍历次数为:1
非公平锁获取线程的名称为:Thread-0非公平锁遍历次数为:2
非公平锁获取线程的名称为:Thread-1非公平锁遍历次数为:0
非公平锁获取线程的名称为:Thread-1非公平锁遍历次数为:1
非公平锁获取线程的名称为:Thread-1非公平锁遍历次数为:2
非公平锁获取线程的名称为:Thread-2非公平锁遍历次数为:0
非公平锁获取线程的名称为:Thread-2非公平锁遍历次数为:1
非公平锁获取线程的名称为:Thread-2非公平锁遍历次数为:2
##公平锁运行结果
公平锁获取线程的名称为:Thread-3非公平锁遍历次数为:0
公平锁获取线程的名称为:Thread-3非公平锁遍历次数为:1
公平锁获取线程的名称为:Thread-3非公平锁遍历次数为:2
公平锁获取线程的名称为:Thread-4非公平锁遍历次数为:0
公平锁获取线程的名称为:Thread-4非公平锁遍历次数为:1
公平锁获取线程的名称为:Thread-4非公平锁遍历次数为:2
公平锁获取线程的名称为:Thread-5非公平锁遍历次数为:0
公平锁获取线程的名称为:Thread-5非公平锁遍历次数为:1
公平锁获取线程的名称为:Thread-5非公平锁遍历次数为:2
从上面的运行结果可以看出公平锁和非公平锁的区别,这里我就不做过多的描述,大家请看运行后的结果。
博客中某些语句借鉴于:
https://blog.csdn.net/u011521203/article/details/80186741
https://blog.csdn.net/blogs_broadcast/article/details/80723021
https://blog.csdn.net/caoxiaohong1005/article/details/80173470
以及书籍:java并发编程艺术