普通互斥锁会把读取也串行化。如果读取多、修改少,可以用读写锁让多个读者同时进入,写入时再独占。

读锁保护读取,写锁保护修改

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteCacheDemo {
    private final Map<String, String> data = new HashMap<>();
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public String get(String key) {
        lock.readLock().lock();
        try {
            return data.get(key);
        } finally {
            lock.readLock().unlock();
        }
    }

    public void put(String key, String value) {
        lock.writeLock().lock();
        try {
            data.put(key, value);
        } finally {
            lock.writeLock().unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReadWriteCacheDemo cache = new ReadWriteCacheDemo();
        Thread writer = new Thread(() -> cache.put("language", "Java"));
        writer.start();
        writer.join();
        System.out.println(cache.get("language"));
    }
}

结果为 Java。多个 get 可以共同持有读锁,但 put 要等待读者退出。这里返回的是不可变字符串;如果返回 Map 本身或可变 value,让外部在锁外修改,就绕过了同步规则。

不要在读锁里升级

线程已经持有读锁,再尝试获取写锁,会等待读锁释放,而自己的读锁又不会释放,可能把自己卡住。不能用这种方式完成读锁升级。

需要修改时,先释放读锁,再取得写锁,并重新检查条件。释放与重新加锁之间可能有其他写者进来,前一次读取结果不能继续当成最新事实。

写锁持有者可以获取读锁,然后释放写锁,实现降级:

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class LockDowngradeDemo {
    public static void main(String[] args) {
        ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
        int value;
        lock.writeLock().lock();
        try {
            value = 42;
            lock.readLock().lock();
        } finally {
            lock.writeLock().unlock();
        }
        try {
            System.out.println(value);
        } finally {
            lock.readLock().unlock();
        }
    }
}

示例用局部值突出获取和释放的顺序。应用到共享数据时,先取得读锁再释放写锁,就能保证后续受读锁保护的使用阶段不会被其他写者插入修改。如果先释放写锁再加读锁,这个连续性就丢了。

state 如何表示两类锁

JDK 8 的实现把 AQS 的 int 状态拆成两部分:高 16 位表示读锁的总体持有计数,低 16 位表示写锁重入计数。还需要额外记录每个线程自己的读锁重入次数,不能只知道总数。

写锁支持 Condition,读锁不支持。公平与非公平策略也会影响新读者、新写者是否可以进入,不是只要“当前没有写锁”就永远让所有新读者抢入。

读多也要测量

读锁的获取和释放同样有维护共享状态的成本。读取内容非常短、竞争不高时,读写锁未必胜过普通互斥锁。如果只是单键访问,ConcurrentHashMap 也可能更适合。

读写锁更适合需要保护一组共享数据、读取可以并行且有一定工作量的情况。选择依据是实际临界区和负载,而不只是“读多写少”四个字。

最后修改:2018 年 05 月 27 日
如果觉得我的文章对你有用,请随意赞赏