目录

一、重入锁ReentrantLock

1. 实现锁重进入

2. 获取公平锁与非公平锁的区别

二、读写锁ReentrantReadWriteLock

1. ReentrantReadWriteLock的API

2. 读写状态设计

3. 写锁的获取与释放

4. 读锁的获取与释放

5. 写锁降级

三、参考资料


一、重入锁ReentrantLock

        重入锁指线程获得锁后能够再次获取该锁而线程不被阻塞synchronized关键字隐式的支持重进入。java.util.concurrent.locks.ReentrantLock类是重入锁,实现Lock接口。ReentrantLock不仅支持锁的重入,而且还支持获取锁的公平和非公平选择。

        公平的获取锁是指等待时间最长的线程最先获取锁,即:锁按FIFO顺序获取。ReentrantLock默认是非公平锁,原因:公平锁虽然保证锁的获取按照FIFO原则,而代价是进行大量的线程切换;非公平性锁虽然可能造成线程“饥饿”,但极少的线程切换,保证其更大的吞吐量

1. 实现锁重进入

        以下代码是非公平锁的获取和释放,需要注意问题:

  • 线程再次获取锁:已获取锁的线程是否是当前线程,若是则再次成功获取锁;
  • 锁的最终释放:成功获取锁,state都要+1,即:state值表示当前被重复获取的次数,而释放锁时,则state自减,最后等于0时表示锁已经成功释放
/**
 * 非公平性获取锁
 */
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // state == 0,表示没有任何线程获得锁
    if (c == 0) {
        // 尝试获取锁
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // state != 0,说明已有线程获取锁
    // 判断当前线程 == 已获取锁的线程,则再次加锁,计数器+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;
}

/**
 * 非公平锁释放
 */
protected final boolean tryRelease(int releases) {
    // 每次释放,state自减
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // state == 0时,表示锁成功释放
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

2. 获取公平锁与非公平锁的区别

        公平锁按照FIFO队列的顺序获取锁,在非公平锁的基础上增加hasQueuedPredecessors()条件判定,即:同步队列的当前节点是否有前节点,若有返回true

  • 非公平锁的获取:compareAndSetState(0, acquires)
  • 公平锁的获取:!hasQueuedPredecessors() && compareAndSetState(0, acquires)
/**
 * 公平锁的获取
 */
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // state == 0,表示没有任何线程获得锁
    if (c == 0) {
        // 尝试获取锁
        // 公平获取锁 = 是否有前节点 + 非公平获取锁
        if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // state != 0,说明已有线程获取锁
    // 判断当前线程 == 已获取锁的线程,则再次加锁,计数器+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;
}

二、读写锁ReentrantReadWriteLock

        读写锁是指同一时刻允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一个读锁(共享锁)和一个写锁(排他锁)。同时读写锁也支持公平或非公平锁、锁重入、写锁降级

        java.util.concurrent.locks.ReentrantReadWriteLock读写锁实现ReadWriteLock接口,维护了一个读锁、一个写锁。注意读写锁的性能比排他锁的性能好,大多数场景下读多于写,这样多线程读提高了并发量和吞吐量。

1. ReentrantReadWriteLock的API

        注意:getReadLockCount(),获取当前读锁被获取的次数(共享读次数 + 重入锁次数);getReadHoldCount(),获取当前线程获取读锁的次数(一个线程的重入锁次数),存储在每个线程的ThreadLocal下

2. 读写状态设计

        同步器AQS维护一个int型的state变量,即:同步状态(private volatile int state) 。如下图所示,这个state同步状态“按位切割”:高16位,表示读锁;低16位,表示写锁

        根据state值划分能得出一个推论:state不等于0时,当写状态(state & 0x0000FFFF)等于0时,则读状态(state >>> 16)大于0,即读锁已被获取。

3. 写锁的获取与释放

        如以下代码是java.util.concurrent.locks.ReentrantReadWriteLock.Sync类中方法,定义了写锁的获取,也支持写锁重进入。当前线程已获取写锁,则增加写状态;如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者当前线程不是已获取写锁的线程,则当前线程进入等待状态

/**
 * ReentrantReadWriteLock写锁的获取
 * 该方法在java.util.concurrent.locks.ReentrantReadWriteLock.Sync继承了AQS
 */
protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    // 获取state值
    int c = getState();
    // 获取写状态
    int w = exclusiveCount(c);
    // state值 != 0
    if (c != 0) {
        /*
         * state值 != 0 且 写状态 == 0:表示已获取读锁
         *    或
         * 别的线程已获取写锁
         */
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // 重入写锁
        setState(c + acquires);
        return true;
    }
    if (writerShouldBlock() ||
            !compareAndSetState(c, c + acquires))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

        写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态为0
时表示写锁已被释放。

4. 读锁的获取与释放

        如以下代码是java.util.concurrent.locks.ReentrantReadWriteLock.Sync类中方法,定义了读锁的获取,也支持读锁重进入。如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态;如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠CAS保证)增加读状态,成功获取读锁。

        注意:其他线程没有持有写锁,但是当前线程已有写锁,那么读锁也可以成功获取,因为存在写锁降级的过程,详见下小节《写锁降级》

        注意:读状态是所有线程获取读锁次数的总和,由getReadLockCount()获取;而每个线程各自获取读锁的次数只能选择保存在ThreadLocal中,由getReadHoldCount()获取。

/**
 * ReentrantReadWriteLock读锁的获取
 * 该方法在java.util.concurrent.locks.ReentrantReadWriteLock.Sync继承了AQS
 */
protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    if (exclusiveCount(c) != 0 &&
            getExclusiveOwnerThread() != current)
        return -1;
    int r = sharedCount(c);
    if (!readerShouldBlock() &&
            r < MAX_COUNT &&
            compareAndSetState(c, c + SHARED_UNIT)) {
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            firstReaderHoldCount++;
        } else {
            ReentrantReadWriteLock.Sync.HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1;
    }
    return fullTryAcquireShared(current);
}

        读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的
值是(1<<16)。

5. 写锁降级

        写锁降级指的是写锁降级成为读锁。锁降级是指当前线程已拥有写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。但是当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称锁降级。同时不支持锁升级。下面是写锁降级的代码实例。

// 创建读写锁
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

// 获取写锁
Lock writeLock = readWriteLock.writeLock();
// 获取读锁
Lock readLock = readWriteLock.readLock();

@Test
public void writeLockTest(){
    // 写锁降级从写锁获取开始
    writeLock.lock();
    try {
        // 再次获取读锁
        readLock.lock();
    } finally {
        // 释放写锁
        writeLock.unlock();
    }
    // 写锁降级完成,降级为读锁
}

        锁降级中读锁的获取是否必要呢? 答案是必要的。主要是为了保证数据的可见性。如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程T获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。

三、参考资料

Lock锁<一> _ 基础_爱我所爱0505的博客-CSDN博客

通俗易懂的ReentrantLock

ReentrantLock基础知识_米兰的小铁匠z的博客-CSDN博客_reentrantlock

ReentrantReadWriteLock读写锁详解 - 平凡希 - 博客园

Java并发编程--ReentrantReadWriteLock - 在周末 - 博客园

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐