关于java:JAVA并发编程读写锁ReentrantReadWriteLock的实现分析

3次阅读

共计 2551 个字符,预计需要花费 7 分钟才能阅读完成。

一、简介

读写锁在同一时刻容许多个读线程拜访,然而在写线程拜访时,所有的读线程和其余写线程均被阻塞 。读写锁保护了两把锁, 一把读锁和一把写锁。获取读写锁可分为上面两种状况:

  • 同一线程:该线程获取读锁后,可能再次获取读锁,但不能获取写锁。该线程获取写锁后,可能再次获取写锁,也能够再获取读锁。
  • 不同线程:A 线程获取读锁后,B 线程能够再次获取读锁,不能够获取写锁。A 线程获取写锁后,B 线程无奈获取读锁和写锁。

二、读写锁示例

public class Cache {static Map<String, Object> map = new HashMap<String, Object>();
    static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    static Lock r = rwl.readLock();
    static Lock w = rwl.writeLock();
    public static final Object get(String key) {r.lock();
        try {return map.get(key);
        } finally {r.unlock();
        }
    }
    public static final Object put(String key, Object value) {w.lock();
        try {return map.put(key, value);
        } finally {w.unlock();
        }
    }
}

上述示例中,Cache 组合一个非线程平安的 HashMap 作为缓存的实现,同时应用读写锁来保障 Cache 是线程平安的。

三、读写锁的实现剖析

3.1 读写状态的设计

回忆 AQS 的实现,同步状态示意锁被获取的次数,而读写锁的自定义同步器须要在同步状态(一个整型变量)上保护多个读线程和一个写线程的状态,该状态的设计成为设计读写锁的要害

如果在一个整型变量上保护多种状态,就须要“按位切割应用 ”这个变量,读写锁将变量切分成了两局部, 高 16 位示意读,低 16 位示意写,划分形式如下图。

上图同步状态示意一个线程曾经获取了写锁,且重进入了两次,同时也间断获取了两次读锁。读写锁时如何确定读和写的状态?答案是通过位运算。假如以后同步状态为 S,写状态等于 S & 0x0000FFFF(将高 16 位抹去)读状态等于 S >>> 16(无符号补 0 右移 16 位)。当写状态减少 1 时,等于 S + 1,当读状态减少 1 时,等于 S + (1 << 16),也就是 S + 0x00010000。

3.2 写锁的获取与开释

写锁是一个反对重入的排它锁。如果以后线程曾经获取了写锁,则减少写状态。如果以后线程在获取写锁时,读锁曾经被获取(读状态不为 0)或者该线程不是曾经获取写锁的线程,则以后线程进入期待状态。代码如下

protected final boolean tryAcquire(int acquires) {Thread current = Thread.currentThread();
    int c = getState();
    int w = exclusiveCount(c);
    if (c != 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;
}

如果存在读锁(即便只有以后线程获取了读锁也不行 ),则写锁不能被获取,起因在于:读写锁要确保写锁的操作对读锁可见,因而只有期待其余读线程都开释了读锁,写锁能力被以后线程获取, 而写锁一旦获取,其余读写线程的后续拜访均被阻塞

3.3 读锁的获取与开释

读锁是一个反对重入的共享锁,它可能被多个线程同时获取,在写状态为 0 时,读锁总会被胜利获取,而所做的也只是减少读状态。如果以后线程在获取读锁时,写锁已被其余线程获取,则进入期待状态。

protected final int tryAcquireShared(int unused) {for (;;) {int c = getState();
        int nextc = c + (1 << 16);
        if (nextc < c) 
            throw new Error("Maximum lock count exceeded");
        if (exclusiveCount(c) != 0 && owner != Thread.currentThread())
            return -1;
        if (compareAndSetState(c, nextc))
            return 1;
    }
}

如果其余线程曾经获取了写锁,则获取读锁失败,进入期待状态。
读锁的每次开释均缩小读状态,缩小的值是 1 << 16。

3.4 锁降级

锁降级指的是 写锁降级成读锁 。锁降级是持有了写锁之后,在获取到读锁,随后开释之前领有的写锁,那么只剩下读锁,这个过程是锁降级( 读写锁不反对锁降级)。代码如下

public void processData() {readLock.lock();
    if (!update) {
        // 必须先开释读锁
        readLock.unlock();
        // 锁降级从写锁获取开始
        writeLock.lock();
        try {if (!update) {update = true;}
            readLock.lock();} finally {writeLock.unlock();
        }
        // 锁降级实现,写锁降级为读锁
    }
    try {// 应用 update} finally {readLock.unlock();
    }
}

下面代码中锁降级中,读锁的获取是否必要?是必要的,因为批改 update 之后,后续还要应用到 update,所以为了避免其余线程批改 update,所以须要加读锁

四、总结

个别状况下,读写锁的性能优于 ReentrantLock,因为大多数场景读是远大于写的,所以在读多于写的状况下,读写锁可能提供更好的并发性和吞吐量。

正文完
 0