多线程学习篇之ReentrantLock(相关实现细节)

本文参考:
https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/synchronized.md
https://www.javadoop.com/post/AbstractQueuedSynchronizer-2
java并发编程艺术

本篇关于细节上的源码就不仔细探究了,以后再来搞把

java5以后提供了Lock接口以及相关实现类实现锁的功能,提供了与synchronized关键字类似的同步功能,
synchronized关键字是是依赖于 JVM 实现的,而Lock接口相关实现类依赖于API实现(AQS)。

AQS解析看这篇文章 https://blog.csdn.net/wmh1152151276/article/details/89398854

本文重点介绍ReentrantLock(可重入锁)实现类。
正确使用方式

Lock lock = new ReentrantLock();
lock.lock();
try {
    // 业务逻辑
} finally {
     lock.unlock();
}

注意两点

  1. 在finally块中释放锁,目的是保证在获取到锁之后,最终能够被释放。
  2. 不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常,异常抛出的同时,也会导致锁无故释放。

synchronized 和 Lock实现类ReentrantLock的对比

  1. 使用synchronized会自动隐式的获取和释放锁,简化了同步的管理
    ReenTrantLock需要显式的获取和释放锁,缺少了便捷性,但是拥有了锁获取与释放的可操作性和灵活性

  2. 两者都是可重入锁,并且默认都是非公平锁,ReenTrantLock还提供了公平锁,可在构造方法中指定

  3. Lock接口中提供了 synchronized 不具备的主要几个特性 (相当于ReentrantLock和synchronized对比)

    • 获取锁过程中可被中断 通过lock.lockInterruptibly()来实现,当获取到锁或因为获取锁被阻塞时,可响应中断抛出异常。
    • 尝试非阻塞的获取锁并可以超时获取 通过boolean tryLock()实现非阻塞的获取锁,获取成功或者获取失败都直接返回结果,不会阻塞等待再次获取。通过boolean tryLock(time, unit) 指定因为获取锁失败而等待的时间
  4. ReentrantLock提供Condition来实现和wait notify类似的等待通知功能。 使用condition可以实现有选择的通知,在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的。

这里列出lock接口的Api 然后说明ReentrantLock实现这些功能的细节

public interface Lock {
    //获取锁,调用该方法将会获取锁,当锁获取后,从该方法返回
    void lock();
    //可中断地获取锁,和lock()方法的不同之处在于该方法会响应中断,即在锁的获取过程中可以中断当前线程 
    void lockInterruptibly() throws InterruptedException;
    //尝试非阻塞的获取锁,调用该方法后会立刻返回,如果能够获取则返回true,否则返回false 
    boolean tryLock();
    //超时地获取锁 1、当前线程在超时时间内成功获取锁。2、当前线程在超时时间内被中断。3、超时时间结束返回false。 
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    //释放锁 
    void unlock();
    //获取等待通知组件
    Condition newCondition();
}

ReentrantLock重入锁 作为一个同步组件,它当然也是用了AQS作为实现。
它的内部不仅定义了一个Sync静态内部类继承自AQS
还另外定义了2个继承Sync的内部类 一个作为公平锁实现一个作为非公平锁实现
在这里插入图片描述
在构造函数中会创建对应的实现,如果不指定公不公平,默认是不公平的

以默认的非公平实现为例,
当我们 Lock lock = new ReentrantLock()之后 会创建一个NonfairSync的内部类对象
在这里插入图片描述
当我们调用lock.lock()时 其实就是调用了NonfairSync里的lock方法
在这里插入图片描述
下面是源码

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

因为是非公平的
所以首先会利用CAS直接试图修改同步状态为1 如果成功,设置获取锁线程为当前线程
如果失败才会和其他同步组件一样调用AQS中的acquire()模板方法
acquire()模板方法的内容在AQS解析中说到了这里不说了,反正就是会调用子类重写的tryAcquire(int acquires)


   protected final boolean tryAcquire(int acquires) {
       return nonfairTryAcquire(acquires);
   }

这里不多说直接看nonfairTryAcquire()

 final boolean nonfairTryAcquire(int acquires) {
       final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
        	// 这里判断同步是否为0 然后试图修改为1 获取锁
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        // 如果当前当前线程就是之前获取锁的线程 那么可以直接再次获得锁然后将同步状态 + 1
        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;
    }

注意了可重入的性质就是在这里实现的:
加了一层判断获取锁的线程是否为当前线程 如果是就直接获取并将同步状态 + 1

并且我们可以看到非公平锁进行了2次CAS操作设置同步状态,

我们可以对比看一下公平锁 公平锁的实现靠的是fairSync内部类
公平锁设置同步状态的操作直接放在了tryAcquire()方法里,没有和非公平锁一样另外定义一个方法

final void lock() {
	// 这里没有进行cas 直接调用acquire
    acquire(1);
  }
protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
            // 这里是重点 比非公平锁多了一个判断:是否有线程在等待
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
             // 如果当前当前线程就是之前获取锁的线程 那么可以直接再次获得锁然后将同步状态 + 1
            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
这个方法判断此时的同步队列里是否有节点在阻塞等待,如果有则不抢了。
这是实现公平的关键。

总结:公平锁和非公平锁只有两处不同:

  1. 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
  2. 非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire
    方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS
    抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。

公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。

相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。

获取可响应中断锁实现

  //可中断地获取锁,和lock()方法的不同之处在于该方法会响应中断,即在锁的获取过程中可以中断当前线程 
  void lockInterruptibly() throws InterruptedException;

 public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

获取锁可中断靠的是这2个方法,当检测到中断时直接抛出异常

对比AQS中解析那篇文章中不响应中断的处理方法就可以发现不同,不响应中断只是会判断有没有中断过然后重新设置中断状态,仍常会照常去竞争锁资源,而这里响应中断,直接抛出异常。

 public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
        // 抛出异常
            throw new InterruptedException();
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }
private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    // 直接抛出异常
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

非阻塞获取锁

public boolean tryLock() {
    return sync.nonfairTryAcquire(1);
}

很简单 可以看出只会尝试一次设置同步状态,成功或失败都直接返回结果。
失败之后不会构造节点加入同步队列进入阻塞状态。

对比lock

final void lock() {
            acquire(1);
        }
  public final void acquire(int arg) {
  // 在这里tryAcquire 也是调用了nonfairTryAcquire
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    

失败会构造节点加入同步队列

Condition实现看这篇文章:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值