Java并发之ReentrantLock 与 synchronized

1,098 阅读5分钟

ReentrantLock

ReentrantLock直译为重入锁,又称为递归锁。

    是指在同一个线程中,外部方法获得锁之后,内层的递归方法依然可以获取该锁
    倘若锁不具备可重入性,那么我们在第二次获取锁的时候就会造成死锁

ReentrantLock的实现是基于AQS的,实现了锁机制和重入机制

ReentrantLock在底层有两种实现方式,分别是公平锁(FairSync)和非公平锁(NonfairSync)

public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

我们在实例化ReentrantLock对象的时候,可以给他传一个boolean类型的变量,如果什么都不传,那么默认生成非公平锁。

公平锁

我们先来看一看公平锁的具体实现

这是公平锁的简易执行流程

ReentrantLock之公平锁.png

是不是看起来特别简单,所以真正的底层实现也不是很难

//这是FairSync的lock方法,用于获取锁,它在这里直接调用了AQS的加锁方法
final void lock() {
            acquire(1);
        }

acquire的实现.png 这里我们看一下AQS的acquire方法。

public final void acquire(int arg) {
   if (!tryAcquire(arg) &&
       acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
       selfInterrupt();
}

这里先尝试获取锁,如果获取失败,就会把当前线程加入到线程等待队列,而这里的acquireQueued()和addWaiter()方法分别用于请求入作和封装为Node结点,最后执行selfInterrupt()方法,来中断线程

非公平锁

我们再来看一看NonfairSync(非公平锁)的实现

它的简易流程大概如图:

ReentrantLock之非公平锁.png

同样的,我们看一看NonfairSync的lock方法

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

我们可以看到先使用CAS的操作,将当前锁的状态由0改为1,如果该锁的状态不为0,那么和将和公平锁的执行流程一样,直接去调用AQS的acquire方法,如果修改成功的话,证明没有其他线程持有该锁,当前线程可以直接获取该锁。

公平锁和非公平锁

公平锁直接去调用AQS的acquire方法,而非公平锁先去验证当前锁的状态,倘若为0,修改为1之后,去执行setExculsiveOwnerThread()方法,也就是当前线程持有该锁

ReentrantLock的重入性

说到ReentrantLock的重入性之前我们不妨先了解一下互斥锁和自旋锁

互斥锁:通过阻塞线程来进行加锁,中断阻塞来进行解锁。

自旋锁:线程保持运行状态,用一个循环体不停地判断某个标识的状态来确定加锁还是解锁

在上面者两种锁当中,如果出现了一种情况,就是说,如果A线程给资源B加锁了,然后A线程中还有一个子方法C,需要使用资源B,此时,会出现死锁,因为C在给B加锁的时候,会发现B已经被加锁了,此时会导致C阻塞,而可重入锁就是解决的这个问题!

在ReentrantLock中式如何处理重入性的呢?

我们其实很容易想到该怎么实现重入这样的特性,类比我们生活中,就比如我们要从家门口到我们自己的卧室,我们需要先打开大门的锁,进入家里,然后打开客厅的门锁,进入客厅,最后要打开卧室的门锁,进入卧室,这样我们的目的就达成了。

同样的,我们出门就像我们释放锁的过程,我们需要先出卧室然后锁上卧室的门,然后出客厅,锁客厅的门,最后,出大门,锁上大门。

而在ReentrantLock中是通过tryAcqurie()方法完成重入功能的

这里我们以非公平锁的tryAcqurie()方法为例

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

ReentrantLock的可重入性实现.png 与之对应的既然有加锁,那肯定就有释放锁,我们可以看tryRelease()方法

同样的这里我们以非公平锁的tryRelease()方法为例

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

ReentrantLock可重入锁释放.png

synchronized

使用

  • 修饰在方法上

    作用:进入该方法前,需要先获得当前实例的锁
    
  • 修饰在静态方法上

    作用:进入该方法前,需要先获得当前类对象的锁
    
  • 修饰在代码块上

    作用:指定加锁对象,进入该代码块前需要先获取该对象的锁
    

在我们使用synchronized的时候,我们需要知道,synchronized修饰的方法,无论执行成功或者异常退出都会释放掉该锁

原理

在synchronized关键字修饰过的方法或者代码块上,在经过编译之后会在它们前后分别生成monitorentermonitorexit这两个指令,我们来细细道来这两个指令到底是什么意思,什么用途 这两个指令其实就是操作的一个monitor计数器(监听器)对象

monitorenter

当执行这行指令的时候,会查看monitor计数的值,

  • 当monitor的值为0的时候,证明目前还没有被获取,那么这个线程会获取锁,然后monitor的值+1,然后其他线程就不能再获取锁了
  • 如果该线程已经拿到了该锁,然后又重入了这把锁,那么这个monitor的值就会累加,一直累加
  • 当monitor的值大于0那么证明,这把锁已经被别的线程获取了,等待锁的释放

monitorexit

该指令其实就是释放锁的过程,而该过程也十分简单,就是将monitor的值依次减一直到monitor的值为0

优缺点

synchronized是非公平锁对象,这样可能导致,新的进程一来就获得了锁,而等待很久的进程迟迟无法获得锁,这样有利于提高性能,但是却可能导致进程饥饿现象

不过synchronized使用起来非常方便直接在方法上添加该关键字就可以了

ReentrantLock完全可以替代synchronized,synchronized在加锁和释放锁的时候是固定的,而ReentrantLock在加锁和释放锁就十分的灵活,并且synchronized只能使用非公平锁,而ReentrantLock可以灵活的使用非公平锁和公平锁!