Java锁详解之ReentrantLock

在这里插入图片描述

写在前面

ReentrantLock继承了AbstractQueuedSynchronizer(简称AQS),AQS用到了模板模式这种设计模式,所以阅读AQS和ReentrantLock的源码时,最好对模板设计模式有一定了解,这里我就讲AQS了。

ReentrantLock是除了synchronized用得较多的一种锁。ReentrantLock也属于重入锁,后面接着就会提到它的重入锁实现原理。

ReentrantLock的功能要比内部锁synchronized更多,如指定锁等待时间的方法tryLock(long time,TimeUnit unit)、中断锁的方法lockInterruptibly()、没获取锁直接返回的方法tryLock()。

所以,什么时候选择ReentrantLock呢?

一般是synchronized 不满足需求时,才选择ReentrantLock,比如实现立即返回、可中断、conditon机制时

ReentrantLock的重要方法

方法说明
lock()获取锁。如果锁已经被占用,则等待
tryLock()尝试获取锁,拿到锁返回true,没拿到返回false,并立即返回
tryLock(long time, TimeUnit unit)在指定时间内会等待获取锁,如果一直拿不到就返回false,并立即返回。在等待过程中可以进行中断
lockInterruptibley()获取锁。如果线程interrupted了,则跟着抛出异常中断
unLock()释放锁
newCondition()创建一个与此 Lock 实例一起使用的 Condition 实例。

ReentrantLock使用示例

 private ReentrantLock lock = new ReentrantLock();
 
 public void run() {
	 // 加锁 
       if(lock.tryLock()){
            System.out.println(Thread.currentThread().getName()+" 拿到锁");
            try {
                Thread.sleep(3000);
                // doSomething...
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                lock.unlock();// 释放锁
            }
	    } else{
	        System.out.println(Thread.currentThread().getName()+" 获取锁失败");
	    }
 }

ReentrantLock的公平和非公平锁

公平锁:公平锁讲究先来先到,线程在获取锁时,会先看这个锁的等待队列中是否有线程在等待,如果有,则当前线程就会直接进入等待队列中,而不是直接去抢占锁。

ReentrantLock fairLock = new ReentrantLock(true); // 初始化一个公平锁

非公平锁:不管是否有等待队列,先直接尝试获取锁,如果拿到锁,则立刻占有锁对象;如果未拿到,则自动排到队尾等待。

ReentrantLock fairLock = new ReentrantLock(); // 初始化一个非公平锁
ReentrantLock fairLock = new ReentrantLock(false); // 初始化一个非公平锁

非公平锁的性能比公平锁的性能好很多,所以ReentrantLock默认是非公平锁

为什么非公平锁性能好?


//公平锁静态内部类
static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                // hasQueuedPredecessors返回true,说明当前节点不是头节点或队列不为空,此时直接加在队列后面
                // hasQueuedPredecessors前面有个非符号,此时不再走&&后面的CAS操作。
                // hasQueuedPredecessors返回false,说明当前节点是头节点或队列为空,
                // 说明只有当前线程在竞争锁,此时可以进行compareAndSetState操作。
                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;
        }
 }
 
// 非公平锁静态内部类
static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
        
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires); // 是Sync的方法
        }
 }

// Sync的方法
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的公平锁和非公平锁的源代码你就会发现,公平锁直接走的父类AbstractQueuedSynchronizer的acquire方法,而非公平锁是先作CAS操作。非公平锁这样做的优点是
1.如果直接拿到了锁,就避免了维护node链表队列
2.如果直接拿到了锁,就避免了线程休眠和唤醒的上下文切换

ReentrantLock的重入锁

ReentrantLock它是怎么实现重入锁的呢?
截取前面小节的部分代码:

     else if (current == getExclusiveOwnerThread()) {
           int nextc = c + acquires;
           if (nextc < 0) // overflow
               throw new Error("Maximum lock count exceeded");
           setState(nextc);
           return true;
       }

从代码可以看出,来获取锁的线程如果是当前占有锁的线程,则直接将nextc+1。而且从if (nextc < 0)知道,可重入的次数是int的最大值。刚入门的同学可能不知道为什么会是int的最大的值,这是因为一个int值在做不限制累加,到了最大值2147483647时会溢出变成负数,这个在大学计算机相关课程应该会讲到。

ReentrantLock的Condition机制

ReentrantLock还提供了Condition机制来进行复杂的线程控制功能。

Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition。

ArrayBlockingQueue的实现就依靠了Condition机制。如下核心代码。

下面代码中Condition机制的核心原理就是:当前线程被哪个condtion阻塞(调用await),就会加到当前condition的阻塞队列里

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 数组阻塞队列
 *
 * @param <E>
 */
class ArrayBlockingQueueDemon<E> {

    final ReentrantLock lock;

    /**
     * put元素时被阻塞的条件
     */
    final Condition putCondition;

    /**
     * take数据时被阻塞的条件
     */
    final Condition takeCondition;

    /**
     * 放元素的队列
     */
    final Object[] items;

    /**
     * take下一个元素的索引下标
     */
    int takeIndex;

    /**
     * put下一个元素的索引下标
     */
    int putIndex;
    
    /**
     * 队列中元素个数
     */
    int count;

    /**
     * 构造方法
     *
     * @param capacity 允许队列
     * @param fair     是否创建公平锁
     */
    public ArrayBlockingQueueDemon(int capacity, boolean fair) {
        if (capacity <= 0) {
            throw new IllegalArgumentException();
        }
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);
        takeCondition = lock.newCondition();
        putCondition = lock.newCondition();
    }

    public void put(E e) throws InterruptedException {
        if (e == null) {
            throw new NullPointerException();
        }
        lock.lockInterruptibly();
        try {
            // 队列到达初始化上限时,不再允许向队列放数据,放数据的线程要等待
            // 此刻,当前线程会被添加到putCondition的阻塞队列里
            while (count == items.length) {
                putCondition.await();
            }
            // 入队
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }

    public E take() throws InterruptedException {
        lock.lockInterruptibly();
        try {
            // 当队列没有数据时,拿数据的线程等待
            // 此该,当前线程会被添加到takeCondition的阻塞队列里
            while (count == 0) {
                takeCondition.await();
            }
            // 出队并返回
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

    private void enqueue(E x) {
        items[putIndex] = x;
        ++putIndex;
        if (putIndex == items.length) {
            putIndex = 0;
        }
        count++;
        // 队列里增加元素了,可以唤醒取元素的线程了
        takeCondition.signal();
    }

    private E dequeue() {
        E x = (E) items[takeIndex];
        items[takeIndex] = null;
        ++takeIndex;
        if (takeIndex == items.length) {
            takeIndex = 0;
        }
        count--;
        // 队列元素减少了,腾出了位置,可唤醒放元素的线程了
        putCondition.signal();
        return x;
    }

}

注意

  1. Condition在使用之前,一定要先获取监视器。即调用Condition的await()和signal()方法的代码,都必须在lock.lock()和lock.unlock之间。

  2. Conditon中的await()对应Object的wait(),Condition中的signal()对应Object的notify(),Condition中的signalAll()对应Object的notifyAll()。

ReentrantLock与synchronized(内部锁)性能比较

在资源竞争不激烈的情形下,ReentrantLock性能稍微比synchronized差一点。但是当同步非常激烈的时候,synchronized的性能会下降好几十倍。而ReentrantLock不会有太大的性能波动。

在写同步的时候,优先考虑synchronized,毕竟synchronized更简单,总得来说性能被优化得还不错。ReentrantLock比较复杂,写得不好,还可能会给程序性能带来大问题。

Lock与synchronized区别

  1. Lock锁在抛异常时,不会自动释放锁,需要在finally里手动释放。synchronized会手动释放锁
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页