首页 > 代码库 > 聊聊高并发(十五)实现一个简单的读-写锁(共享-排他锁)

聊聊高并发(十五)实现一个简单的读-写锁(共享-排他锁)

读写锁是数据库中很常见的锁,又叫共享-排他锁,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;
	}

}


聊聊高并发(十五)实现一个简单的读-写锁(共享-排他锁)