首页 > 代码库 > 聊聊高并发(十五)实现一个简单的读-写锁(共享-排他锁)
聊聊高并发(十五)实现一个简单的读-写锁(共享-排他锁)
读写锁是数据库中很常见的锁,又叫共享-排他锁,S锁和X锁。读写锁在大量读少量写的情况下有很高的效率优势。
读写锁是基于普通的互斥锁构建出来的更复杂的锁,它有两个基本特点:
1. 当任一线程持有读锁或写锁时,不允许其他线程再持有写锁
2. 当任一线程持有写锁时,不允许其他线程再持有读锁
也就是说,写锁是排他的,只要有一个线程持有写锁,就不允许其他线程再上锁。读锁是共享的,可以有多个线程持有读锁,但不允许同时持有写锁。
读锁和写锁还存在一个锁升级的问题,比如一个线程先持有了读锁,想升级成写锁,这时候基本的读写锁不能支持锁升级,有可能造成死锁,需要更新锁的操作,后面会细说更新锁。这篇只涉及到如何实现一个基本的读-写锁。
首先是一个非公平的读-写锁实现。
1. 使用一个volatile boolean write标志位来表示是否有线程持有写锁
2. 使用一个volatile 计数器来记录持有读锁的个数
3. 使用两个条件来表示条件谓词: existReadCondition, existWriteCondition
4. 需要一个内部锁来进行线程的同步,这里使用非公平的显式锁ReentrantLock
5. 当有线程持有写锁时,设置write = true,让其他线程在existWriteCondition上等待
6. 当有线程持有读锁时,让需要获得写锁的线程在existReadCondition等待,需要读锁的线程可以获得读锁,并设置读锁计数器加1
7. 释放写锁时,需要唤醒在existWriteCondition上等待的线程
8. 释放读锁时,设置读锁计数器减1,当读锁计数器等于0时表示当前没有读锁存在,唤醒在existReadCondition条件等待的线程
9. 值得注意的是这个简单的读-写锁实现,在同一时刻,不管是读锁还是写锁都需要竞争内部锁,也就是说即使是多个读锁并发,同一时刻也只有一个线程能拿到内部锁,只是这个锁的持有时间很短,只设置一个计数器就释放了。
读写锁的接口如下:
package com.zc.lock; public interface ReadWriteLock { public Lock readLock(); public Lock writeLock(); }
非公平的读-写锁实现
package com.zc.lock; import java.util.concurrent.locks.ReentrantLock; /** * 读写锁的特点有两个: * 1. 当读锁和写锁上锁时,不允许有写者锁上锁 * 2. 当写锁上锁时,不允许有读者锁和写者锁上锁 * 也就是说写锁是排他锁,同时只能有一个写锁 * 读锁是共享锁,可以有多个读锁存在 * **/ public class SimpleReadWriteLock implements ReadWriteLock{ private final java.util.concurrent.locks.Lock lock; private final java.util.concurrent.locks.Condition existReadCondition; private final java.util.concurrent.locks.Condition existWriteCondition; private final Lock readLock; private final Lock writeLock; private volatile boolean write; private volatile int readCount; public SimpleReadWriteLock(){ lock = new ReentrantLock(); readLock = new ReadLock(); writeLock = new WriteLock(); existReadCondition = lock.newCondition(); existWriteCondition = lock.newCondition(); write = false; readCount = 0; } private class ReadLock implements Lock{ @Override public void lock() { lock.lock(); try{ while(write){ try { existWriteCondition.await(); } catch (InterruptedException e) { throw new RuntimeException("Interrupted"); } } readCount ++; }finally{ lock.unlock(); } } @Override public void unlock() { lock.lock(); try{ readCount --; if(readCount == 0){ existReadCondition.signalAll(); } }finally{ lock.unlock(); } } } private class WriteLock implements Lock{ @Override public void lock() { lock.lock(); try{ while(readCount > 0){ try { existReadCondition.await(); } catch (InterruptedException e) { throw new RuntimeException("Interrupted"); } } while(write){ try { existWriteCondition.await(); } catch (InterruptedException e) { throw new RuntimeException("Interrupted"); } } write = true; }finally{ lock.unlock(); } } @Override public void unlock() { lock.lock(); try{ write = false; existWriteCondition.signalAll(); }finally{ lock.unlock(); } } } @Override public Lock readLock() { return readLock; } @Override public Lock writeLock() { return writeLock; } }
非公平的读-写锁在大量读存在的情况下有饥饿的问题,写锁在readCount > 0这个条件谓词下等待,也就是说一旦有读锁存在,写操作就要等待,而读操作一般都是远高于写操作的,并且没有先来先服务的公平性,后来的读操作很可能快于写操作获得锁。写锁有可能长时间的等待锁而不能获取锁。
下面实现一个公平的读-写锁:
1. 使用公平的显式锁作为内部锁,保证先来先服务的公平性
2. 使用一个readAccquired和readReleased计数器来记录获取读锁和释放读锁的线程个数
3. 当readAccquired == readReleased时表示读锁已经全部释放,可以获得写锁
4. 最大的改进在这里,获取写锁时分为两步,第一步当没有写锁存在时,就设置write标志为true表示要获取写锁。当write为true时,后来的读锁就一直等待existWriteConditiont条件释放,而不能增加readAccquire计数器。当之前已经获得的读锁都释放后,写锁获得锁。只有等待写锁释放后,后续的读锁才能继续操作。
它的改进主要是在readAccquired == readRelease 条件谓词等待,而不是readCount > 0条件谓词等待。readAccquired的数量即肯定小于readCount,而且只要之前没有写锁存在,就优先让写锁获取。能够保证写锁快速获得锁
package com.zc.lock; import java.util.concurrent.locks.ReentrantLock; /** * 公平的读写锁,优先让写锁能更快地获得锁,写锁尝试获得锁时,读锁会进入等待,让写锁先获得锁 * * **/ public class FairReadWriteLock implements ReadWriteLock{ private final java.util.concurrent.locks.Lock lock; private final java.util.concurrent.locks.Condition existReadCondition; private final java.util.concurrent.locks.Condition existWriteCondition; private final Lock readLock; private final Lock writeLock; private volatile boolean write; private volatile int readAccquired; private volatile int readReleased; public FairReadWriteLock(){ lock = new ReentrantLock(true); existReadCondition = lock.newCondition(); existWriteCondition = lock.newCondition(); readLock = new ReadLock(); writeLock = new WriteLock(); write = false; readAccquired = 0; readReleased = 0; } private class ReadLock implements Lock{ @Override public void lock() { lock.lock(); try{ while(write){ try { existWriteCondition.await(); } catch (InterruptedException e) { throw new RuntimeException("Interrupted"); } } readAccquired ++; }finally{ lock.unlock(); } } @Override public void unlock() { lock.lock(); try{ readReleased ++; if(readReleased == readAccquired){ existReadCondition.signalAll(); } }finally{ lock.unlock(); } } } private class WriteLock implements Lock{ @Override public void lock() { lock.lock(); try{ while(write){ try { existWriteCondition.await(); } catch (InterruptedException e) { throw new RuntimeException("Interrupted"); } } // 让新加入的读锁不能增加readAccquired write = true; while(readAccquired != readAccquired){ try { existReadCondition.await(); } catch (InterruptedException e) { throw new RuntimeException("Interrupted"); } } }finally{ lock.unlock(); } } @Override public void unlock() { lock.lock(); try{ write = false; existWriteCondition.signalAll(); }finally{ lock.unlock(); } } } @Override public Lock readLock() { return readLock; } @Override public Lock writeLock() { return writeLock; } }
聊聊高并发(十五)实现一个简单的读-写锁(共享-排他锁)