ReentrantLock(可重入锁)

518 阅读3分钟

ReentrantLock具有重入性,也就是说线程可以对它已经加锁的ReentrantLock再次加锁,ReentrantLock对象会持有维持一个计算器来追踪lock方法的嵌套调用,线程在每次调用lock()加锁后,必须显示调用unlock()来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。

特性

ReentrantLock可以实现公平锁。

公平锁是指当锁可用时,在锁上等待时间最长的线程将获得锁的使用权。而非公平锁则随机分配这种使用权。和synchronized一样,默认的ReentrantLock实现是非公平锁,因为相比公平锁,非公平锁性能更好。当然公平锁能防止饥饿,某些情况下也很有用。在创建ReentrantLock的时候通过传进参数true创建公平锁,如果传入的是false或没传参数则创建的是非公平锁。

ReentrantLock lock = new ReentrantLock(true);

ReentrantLock可响应中断

当使用synchronized实现锁时,阻塞在锁上的线程除非获得锁否则将一直等待下去,也就是说这种无限等待获取锁的行为无法被中断。而ReentrantLock给我们提供了一个可以响应中断的获取锁的方法lockInterruptibly()。该方法可以用来解决死锁问题。

获取锁时限时等待

ReentrantLock还给我们提供了获取锁限时等待的方法tryLock(),可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。我们可以使用该方法配合失败重试机制来更好的解决死锁问题。

结合Condition实现等待通知机制

使用synchronized结合Object上的wait和notify方法可以实现线程间的等待通知机制。ReentrantLock结合Condition接口同样可以实现这个功能。而且相比前者使用起来更清晰也更简单。

public class Account {

    private final Lock mLock = new ReentrantLock();
    
    private final Condition mCondition = mLock.newCondition();

    private String mAccountNo;
    /**
     * 余额
     */
    private double mBalance;

    private boolean mFlag;

    public Account(String accountNo, double balance) {
        mAccountNo = accountNo;
        mBalance = balance;
    }

    public double getBalance() {
        return mBalance;
    }

    /**
     * 取钱
     *
     * @param drawAmount
     */
    public void draw(double drawAmount) {
        mLock.lock();
        try {
            if (!mFlag) {
                mCondition.wait();
            } else {
                System.out.println(Thread.currentThread().getName() + "取钱:" + drawAmount);
                mBalance -= drawAmount;
                System.out.println(mAccountNo + "账户余额为:" + mBalance);
                mFlag = false;
                mCondition.signalAll();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            mLock.unlock();
        }
    }

    /**
     * 存钱
     *
     * @param depositAmount
     */
    public void deposit(double depositAmount) {
        mLock.lock();
        try {
            if (mFlag) {
                mCondition.wait();
            } else {
                System.out.println(Thread.currentThread().getName() + "存钱:" + depositAmount);
                mBalance += depositAmount;
                System.out.println(mAccountNo + "账户余额为:" + mBalance);
                mFlag = true;
                mCondition.signalAll();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            mLock.unlock();
        }
    }
}

ReentrantLock主要利用CAS+AQS队列来实现。

CAS

Compare and Swap,比较并交换。CAS有3个操作数:内存值V、预期值A、要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”该操作是一个原子操作,被广泛的应用在Java的底层实现中。Java并发包(java.util.concurrent)中大量使用了CAS操作。在Java中,CAS主要是由sun.misc.Unsafe这个类通过JNI调用CPU底层指令实现。

AQS

AbstractQueuedSynchronizer,是一个用于构建锁和同步容器的框架。事实上concurrent包内许多类都是基于AQS构建,例如ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock,FutureTask等。AQS解决了在实现同步容器时设计的大量细节问题。

AQS使用一个FIFO的队列表示排队等待锁的线程,队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。其他的节点与等待线程关联,每个节点维护一个等待状态waitStatus。