Lock锁<二> _ 重入锁/读写锁

目录

一、重入锁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 - 在周末 - 博客园

优化这段代码 #include <iostream> #include <thread> #include <chrono> #include <mutex> #include <semaphore.h> using namespace std; // shared data resource int shared_data = 0; // semaphores for synchronization sem_t mutex, rw_mutex; // number of readers int num_readers = 0; // reader function void reader(int id) { while (true) { // acquire mutex to update the number of readers sem_wait(&mutex); num_readers++; if (num_readers == 1) { // if this is the first reader, acquire the rw_mutex sem_wait(&rw_mutex); } sem_post(&mutex); // read the shared data cout << "Reader " << id << " read shared data: " << shared_data << endl; // release mutex sem_wait(&mutex); num_readers--; if (num_readers == 0) { // if this is the last reader, release the rw_mutex sem_post(&rw_mutex); } sem_post(&mutex); // sleep for a random amount of time this_thread::sleep_for(chrono::milliseconds(rand() % 1000)); } } // writer function void writer(int id) { while (true) { // acquire the rw_mutex sem_wait(&rw_mutex); // write to the shared data shared_data++; cout << "Writer " << id << " wrote to shared data: " << shared_data << endl; // release the rw_mutex sem_post(&rw_mutex); // sleep for a random amount of time this_thread::sleep_for(chrono::milliseconds(rand() % 1000)); } } int main() { // initialize semaphores sem_init(&mutex, 0, 1); sem_init(&rw_mutex, 0, 1); // create reader threads thread readers[8]; for (int i = 0; i < 8; i++) { readers[i] = thread(reader, i); } // create writer threads thread writers[2]; for (int i = 0; i < 2; i++) { writers[i] = thread(writer, i); } // join threads for (int i = 0; i < 8; i++) { readers[i].join(); } for (int i = 0; i < 2; i++) { writers[i].join(); } // destroy semaphores sem_destroy(&mutex); sem_destroy(&rw_mutex); return 0; }
最新发布
05-25
这段代码是使用信号量实现读写锁的示例,但是存在一些需要优化的地方: 1. 不支持优先级反转:在这个实现中,读者和写者都使用了同一个互斥信号量(mutex)来保证对num_readers的互斥访问。但是,如果一个写者在等待互斥信号量(mutex)时,一个读者持续地获取并释放互斥信号量(mutex),那么这个写者可能会一直等待下去,从而导致优先级反转的问题。为了避免这个问题,可以使用两个互斥,一个用于读者,一个用于写者。 2. 读者数量计数器(num_readers)不需要互斥访问:在这个实现中,num_readers是一个全局变量,需要使用互斥信号量(mutex)来保证互斥访问。但是,实际上,这个变量只需要在读者线程中使用,因此不需要使用互斥信号量(mutex)来保护它。 3. 读者线程可能会饿死:在这个实现中,如果有一个写者在等待互斥信号量(rw_mutex),那么所有的读者都不能访问共享数据,这可能会导致读者线程饿死。为了避免这个问题,可以使用条件变量来实现读者线程的优先级。 下面是优化后的代码: ``` #include <iostream> #include <thread> #include <chrono> #include <mutex> #include <condition_variable> #include <atomic> #include <vector> using namespace std; // shared data resource atomic<int> shared_data(0); // mutexes for synchronization mutex read_mutex, write_mutex; // condition variables for synchronization condition_variable read_cond, write_cond; // number of readers int num_readers = 0; // reader function void reader(int id) { while (true) { // acquire the read_mutex to update the number of readers unique_lock<mutex> lock(read_mutex); num_readers++; if (num_readers == 1) { // if this is the first reader, acquire the write_mutex write_mutex.lock(); } lock.unlock(); // read the shared data cout << "Reader " << id << " read shared data: " << shared_data << endl; // acquire the read_mutex to update the number of readers lock.lock(); num_readers--; if (num_readers == 0) { // if this is the last reader, release the write_mutex write_mutex.unlock(); } lock.unlock(); // sleep for a random amount of time this_thread::sleep_for(chrono::milliseconds(rand() % 1000)); } } // writer function void writer(int id) { while (true) { // acquire the write_mutex write_mutex.lock(); // write to the shared data shared_data++; cout << "Writer " << id << " wrote to shared data: " << shared_data << endl; // release the write_mutex write_mutex.unlock(); // sleep for a random amount of time this_thread::sleep_for(chrono::milliseconds(rand() % 1000)); } } int main() { // create reader threads vector<thread> readers(8); for (int i = 0; i < 8; i++) { readers[i] = thread(reader, i); } // create writer threads vector<thread> writers(2); for (int i = 0; i < 2; i++) { writers[i] = thread(writer, i); } // join threads for (int i = 0; i < 8; i++) { readers[i].join(); } for (int i = 0; i < 2; i++) { writers[i].join(); } return 0; } ``` 在这个优化后的代码中,我们使用了两个互斥和两个条件变量来实现读写锁。其中,read_mutex和read_cond用于保护和唤醒读者线程,write_mutex和write_cond用于保护和唤醒写者线程。通过这种方式,我们避免了优先级反转和读者线程饿死的问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值