Bootstrap

第17章 读写锁分离设计模式(Java高并发编程详解:多线程与系统设计)

1.场景描述

对资源的访问一般包括两种类型的动作——读和写(更新、删除、增加等资源会发生变化的动作),如果多个线程在某个时刻都在进行资源的读操作,虽然有资源的竞争,但是这种竞争不足以引起数据不一致的情况发生,那么这个时候直接采用排他的方式加锁,就显得有些简单粗暴了。表1将两个线程对资源的访问动作进行了枚举,除了多线程在同一时间都进行读操作时不会引起冲突之外,其余的情况都会导致访问的冲突,需要对资源进行同步处理。

2.读写分离程序设计

2.1 接口定义

读写锁的类图

1.Lock接口定义
public interface Lock {
    // 获取显示锁, 没有获得锁的线程将被堵塞
    void lock() throws InterruptedException;

    // 释放获取的锁
    void unlock();
}

Lock接口定义了锁的基本操作, 加锁和解锁, 显式锁的操作强烈建议与try finally语句块一起使用,加锁和解锁说明如下。

  • lock() :当前线程尝试获得锁的拥有权, 在此期间有可能进入阻塞。
  • unlock() :释放锁, 其主要目的就是为了减少reader或者writer的数量。
2.ReadWriteLock接口定义

ReadWrite Lock虽然名字中有lock, 但是它并不是lock, 它主要是用于创建read lock和write lock的, 并且提供了查询功能用于查询当前有多少个reader和writer以及waiting中的writer, 根据我们在前文中的分析, 如果reader的个数大于0, 那就意味着writer的个数等于0, 反之writer的个数大于0(事实上writer最多只能为1) , 则reader的个数等于0,由于读和写,写和写之间都存在着冲突,因此这样的数字关系也就不奇怪了。

readLock() :该方法主要用来获得一个Read Lock。
writeLock() :同read Lock类似, 该方法用来获得Write Lock。
getWriting Writers) :获取当前有多少个线程正在进行写的操作, 最多是1个。
getWaiting Writers() :获取当前有多少个线程由于获得写锁而导致阻塞。
getReading Readers() :获取当前有多少个线程正在进行读的操作。

2.2程序实现

1.ReadWriteLockImpl

相对于Lock, ReadWrite Lock Impl更像是一个工厂类, 可以通过它创建不同类型的锁,我们将ReadWrite Lock Impl设计为包可见的类, 其主要目的是不想对外暴露更多的细节,在ReadWrite Lock Impl中还定义了非常多的包可见方法, 代码所示

public class ReadWriteLockImpl implements ReadWriteLock{

    // 定义对象锁
    private final Object MUTEX = new Object();

    // 当前有多少个线程正在写入
    private int writingWriters = 0;

    // 当前有多少个线程正在等待写入
    private int waitingWriters = 0;

    // 当前有多少个线程正在read
    private int readingReaders = 0;

    // read 和 write 的偏好设置
    private boolean preferWriter;

    // 默认情况下preferWrite为true
    public ReadWriteLockImpl() {
        this(true);
    }

    // 构造ReadWriteLockImpl并且传入preferWriter
    public ReadWriteLockImpl(boolean preferWriter) {
        this.preferWriter = preferWriter;
    }


    public Object getMUTEX() {
        return MUTEX;
    }

    public int getWaitingWriters() {
        return waitingWriters;
    }

    public boolean getPreferWriter() {
        return preferWriter;
    }

    // 创建读锁
    @Override
    public Lock readLock() {
        return new ReadLock(this);
    }

    // 创建写锁
    @Override
    public Lock writeLock() {
        return new WriteLock(this);
    }

    // 使写线程的数量增加
    void incrementWritingWriters() {
        this.writingWriters++;
    }

    // 使等待写入的线程数量增加
    void incrementWaitingWriters() {
        this.waitingWriters++;
    }


    // 使读线程的数量增加
    void incrementReadingReaders() {
        this.readingReaders++;
    }


    // 使写线程的数量减少
    void decrementWritingWriters() {
        this.writingWriters--;
    }


    // 使等待获取写入锁的数量减一
    void decrementWaitingWriters() {
        this.waitingWriters--;
    }

    // 使读取线程的数量减少
    void descementReadingReaders() {
        this.readingReaders--;
    }

    @Override
    public int getWritingWriters() {
        return this.writingWriters;
    }


    @Override
    public int getReadingReaders() {
        return this.readingReaders;
    }

    void changePrefer(boolean preferWriter) {
        this.preferWriter = preferWriter;
    }
}

虽然我们在开发一个读写锁,但是在实现的内部也需要一个锁进行数据同步以及线程之间的通信, 其中MUTEX的作用就在于此, 而prefer Writer的作用在于控制倾向性, 一般来说读写锁非常适用于读多写少的场景, 如果prefer Writer为false, 很多读线程都在读数据,那么写线程将会很难得到写的机会。

2.ReadLock

读锁是Lock的实现, 同样将其设计成包可见以透明其实现细节, 让使用者只用专注于对接口的调用,代码如所示

public class ReadLock implements Lock{

    private final ReadWriteLockImpl readWriteLock;

    ReadLock(ReadWriteLockImpl readWriteLock) {
        this.readWriteLock = readWriteLock;
    }


    @Override
    public void lock() throws InterruptedException {
        // 使用Mutex 作为 锁
        synchronized (readWriteLock.getMUTEX()) {
            // 若此时有线程在进行写操作,或者有写线程在等待并且偏向写锁的标识为
            // true时,就会无法获得读锁,只能被挂起
            while(readWriteLock.getWritingWriters() > 0
            || (readWriteLock.getPreferWriter() && readWriteLock.getWritingWriters() > 0 )) {
                readWriteLock.getMUTEX().wait();
            }
            readWriteLock.incrementReadingReaders();
        }
    }

    @Override
    public void unlock() {
        // 使用Mutex作为锁,并且进行同步
        synchronized (readWriteLock.getMUTEX()) {
            // 释放锁的过程就是使得当前reading的数量减一
            // 将perferWriter设置为true,可以使得writer线程获得更多的机会
            // 通知唤醒与Mutex关联monitor waitset中的线程
            readWriteLock.descementReadingReaders();
            readWriteLock.changePrefer(true);
            readWriteLock.getMUTEX().notifyAll();
        }

    }
}
  • 当没有任何线程对数据进行写操作的时候,读线程才有可能获得锁的拥有权,当然除此之外,为了公平起见,如果当前有很多线程正在等待获得写锁的拥有权,同样读线程将会进入Mutex的wait set中, reading Reader的数量将增加。
  • 读线程释放锁, 这意味着reader的数量将减少一个, 同时唤醒wait中的线程, reader唤醒的基本上都是由于获取写锁而进入阻塞的线程,为了提高写锁获得锁的机会,需要将prefer Writer修改为true
3.WriteLock

写锁是Lock的实现, 同样将其设计成包可见以透明其实现细节, 让使用者只用专注于对接口的调用,由于写-写冲突的存在,同一时间只能由一个线程获得锁的拥有权,代码所示。

public class WriteLock implements Lock{

    private final ReadWriteLockImpl readWriteLock;

    public WriteLock(ReadWriteLockImpl readWriteLock) {
        this.readWriteLock = readWriteLock;
    }


    @Override
    public void lock() throws InterruptedException {
        synchronized (readWriteLock.getMUTEX()) {
            try {
                // 首先使等待获取写入锁的数字加一
                readWriteLock.incrementWritingWriters();
                // 如果此时有其他线程正在进行读操作,或者写操作,那么当前线程将被挂起
                while(readWriteLock.getReadingReaders() > 0
                || readWriteLock.getWritingWriters() > 0) {
                    readWriteLock.getMUTEX().wait();
                }
            } finally {
                // 成功获取到了写入锁,使得等待获取写入锁的计数减一
                this.readWriteLock.decrementWaitingWriters();
            }
            // 将正在写入的线程数量加一
            readWriteLock.incrementWritingWriters();
        }
    }

    @Override
    public void unlock() {
        synchronized (readWriteLock.getMUTEX() ) {
            // 减少正在写入锁的线程计数器
            readWriteLock.decrementWritingWriters();
            // 将偏好状态修改为false, 可以使得读锁被最快速的获得
            readWriteLock.changePrefer(false);
            // 通知唤醒其他在Mutex monitor waitset中的线程
            readWriteLock.getMUTEX().notifyAll();
        }
    }
}

  • 当有线程在进行读操作或者写操作的时候,若当前线程试图获得锁,则其将会进入MUTEX的wait set中而阻塞, 同时增加waiting Writer和writing Writer的数量, 但是当线程从wait set中被激活的时候waiting Writer将很快被减少。
  • 写释放锁, 意味着writer的数量减少, 事实上变成了0, 同时唤醒wait中的线程,并将prefer Writer修改为false, 以提高读线程获得锁的机会。

3.读写锁的使用

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class ShareData {
    // 定义共享数据(资源)
    private final List<Character> container = new ArrayList<>();

    //构造ReadWriteLock
    private final ReadWriteLock readWriteLock = ReadWriteLock.readWriteLock();

    // 创建读取锁
    private final Lock readLock = readWriteLock.readLock();

    // 创建写入锁
    private final Lock writeLock = readWriteLock.writeLock();
    private final int length;

    public ShareData(int length) {
        this.length = length;
        for(int i = 0; i < length; i++) {
            container.add(i, 'c');
        }
    }

    public char[] read() throws InterruptedException {
        try {
           // 首先使用读锁进行lock
           readLock.lock();
           char[] newBuffer = new char[length];
           for(int i = 0; i < length; i++) {
               newBuffer[i] = container.get(i);
           }
           slowly();
           return newBuffer;
        } finally {
            // 当操作结束之后,将锁释放
            readLock.unlock();
        }
    }

    public void write(char c) throws InterruptedException {
        try {
            //使用写锁进行lock
            writeLock.lock();
            for(int i = 0; i < length; i++ ) {
                this.container.add(i, c);
            }

            slowly();

        }finally {
            // 当所有的操作都完成之后,对写锁进行释放
            writeLock.unlock();
        }
    }

    // 简单模拟操作的耗时
    private void slowly() {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

ShareData中涉及了对数据的读写操作, 因此它是需要进行线程同步控制的。首先, 创建一个ReadWrite Lock工厂类, 然后用该工厂分别创建ReadLock和WriteLock的实例, 在read方法中使用Read Lock对其进行加锁, 而在write方法中则使用WriteLock, 的程序则是关于对ShareData的使用。

public class ReadWriteLockTest {
     this is the example for read write lock
    private final static String text = "this";

    public static void main(String[] args) {
        // 定义共享数据
        final ShareData shareData = new ShareData(50);
        // 创建两个线程进行数据写操作
        for(int i = 0; i < 2; i++) {
            new Thread(
                    () -> {
                        for(int index = 0; index < text.length(); index++ ) {
                            try {
                                char c= text.charAt(index);
                                shareData.write(c);
                                System.out.println(Thread.currentThread() + " write " + c);
                            }catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
            ).start();

            // 创建10个线程进行数据读操作
            for(int i1 = 0; i1 < 10; i1++ ) {
                new Thread( ()-> {
                    while(true) {
                        try {
                            System.out.println(Thread.currentThread() + " read " + new String(shareData.read()));
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }).start();
            }

        }
    }
}

;