Read-Write Lock模式:读写锁案例

Read-Write Lock模式

生活小case

课堂上老师讲解Java的并发知识,黑板上记录了许多干货。干货太多黑板没有空间了,老师要擦掉板书再继续写新的内容,然而学生都还没看完,不让老师擦掉。
这时,老师等待大家看完,再擦掉重新写。这个思想就是Read-Write Lock模式。

概念

Read-Write Lock模式,即:读取和写入操作分开考虑,在执行读取操作之前线程必须获取用于读取的锁,同理写入之前也需获取用于写入的锁。

  • 线程读取操作时实例状态不变,故可多个线程同时读取,但不可写入;
  • 线程写入操作时会改变实例状态,故在此期间,其他线程不可读取或写入;

这样将针对写入和读取的互斥处理操作分开考虑,能够提高程序的性能。

适用场景

  • 适合读取操作繁重时;
  • 适合读取频率比写入频率高时;

案例

场景

假设有一个Data类可以进行数据的读写,在多线程环境下对其进行读写操作。

实现

主要模块

Read-Write Lock模式案例图

类名 说明
Main 测试主类
Data 可以读写的类
WriterThread 表示写入线程的类
ReaderThread 表示读取线程的类
ReadWriteLock 提供读写锁的类

代码

  • Main函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public static void main(String[] args) {
    // 启动6个读取线程和2个写入线程
    // 结果打印ReadThread读取的内容,当WriteThread写入时,输出会暂停一下。
    // reads日志中右边的10个字符是相同的,因为即使有线程冲突,程序的安全性是可以保证,否则会多个字符交叉在一起显示
    Data data = new Data(10);
    new ReadThread(data).start();
    new ReadThread(data).start();
    new ReadThread(data).start();
    new ReadThread(data).start();
    new ReadThread(data).start();
    new ReadThread(data).start();

    new WriteThread(data, "1234567890").start();
    new WriteThread(data, "abcdefghijk").start();
    }
  • Data数据操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    package com.bubble.demo.read_write_lock;


    import java.util.Arrays;

    /**
    * 用于数据读写
    *
    * @author wu gang
    * date: 2021-10-08 18:50
    **/
    public class Data {

    /**
    * 实际读写对象的数组
    */
    private final char[] buffer;
    /**
    * 读写锁
    */
    private final ReadWriteLock lock = new ReadWriteLock();

    /**
    * 基于size初始化buffer数组,默认用字符*作为初始值来填充
    *
    * @param size 数组长度
    */
    public Data(int size) {
    this.buffer = new char[size];
    Arrays.fill(buffer, '*');
    }

    public char[] read() throws InterruptedException {
    lock.readLock();
    try {
    return doRead();
    } finally {
    lock.readUnLock();
    }
    }

    private char[] doRead() {
    char[] newBuf = new char[buffer.length];
    System.arraycopy(buffer, 0, newBuf, 0, buffer.length);
    slowly();
    return newBuf;
    }

    public void write(char c) throws InterruptedException {
    lock.writeLock();
    try {
    doWrite(c);
    } finally {
    lock.writeUnLock();
    }
    }

    private void doWrite(char c) {
    for (int i = 0; i < buffer.length; i++) {
    buffer[i] = c;
    // 假定写入操作比读取操作耗时久,每写入一个字符就sleep下
    slowly();
    }
    }

    /**
    * 模拟耗时操作
    */
    private void slowly() {
    try {
    Thread.sleep(50);
    } catch (InterruptedException ignored) {
    }
    }

    }
  • 读取线程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    package com.bubble.demo.read_write_lock;

    /**
    * 对Data实例执行读取操作的线程
    *
    * @author wu gang
    * date: 2021-10-08 19:23
    **/
    public class ReadThread extends Thread {

    private final Data data;

    public ReadThread(Data data) {
    this.data = data;
    }

    @Override
    public void run() {
    try {
    while (true) {
    char[] readBuf = data.read();
    System.out.printf("%s reads: %s%n", Thread.currentThread().getName(), String.valueOf(readBuf));
    }
    } catch (InterruptedException ignored) {
    }
    }

    }
  • 写入线程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    package com.bubble.demo.read_write_lock;

    import java.util.Random;

    /**
    * 对Data实例执行写入操作的线程(无互斥处理相关代码)
    *
    * @author wu gang
    * date: 2021-10-08 19:23
    **/
    public class WriteThread extends Thread {

    private static final Random RANDOM = new Random();
    private final Data data;
    /**
    * 程序逐个取出该字符串中的字符,并写入到Data实例中
    */
    private final String filter;
    private int index = 0;

    public WriteThread(Data data, String filter) {
    this.data = data;
    this.filter = filter;
    }

    @Override
    public void run() {
    try {
    while (true) {
    char c = nextChar();
    data.write(c);
    Thread.sleep(3000);
    }
    } catch (InterruptedException ignored) {
    }
    }

    private char nextChar() {
    char c = filter.charAt(index);
    index++;
    if (index >= filter.length()) {
    index = 0;
    }
    return c;
    }

    }
  • 自定义读写锁

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    package com.bubble.demo.read_write_lock;

    /**
    * 读写锁:
    * 防止下面的两种冲突:
    * - 读取和写入的冲突(read-write conflict)
    * - 写入和写入的冲突(write-write conflict)
    * 由于读取不涉及实例状态的变化,读取和读取之间无冲突。
    *
    * 实现时主要考虑下面两种情况:
    * 1、当线程想要获取读锁时:
    * - 如果有线程正在执行写入,则等待,避免read-write conflict;
    * - 如果有线程正在执行读取,则无需等待;
    * 2、当线程想要获取写锁时:
    * - 如果有线程正在执行写入,则等待,避免write-write conflict;
    * - 如果有线程正在执行读取,则等待,避免read-write conflict;
    *
    * @author wu gang
    * date: 2021-10-08 18:52
    **/
    public class ReadWriteLock {

    /**
    * 实际正在读取中的线程个数。
    * 即执行了readLock但未执行readUnLock的线程个数,值一定大于0
    */
    private int readingReaders = 0;
    /**
    * 正在等待写入的线程个数。
    * 即执行到writeLock之后,使用wait执行等待的线程的个数
    */
    private int waitingWriters = 0;
    /**
    * 实际正在写入中的线程个数。
    * 即执行了writeLock但未执行writeUnLock的线程个数,值只能是0或1。
    */
    private int writeWriters = 0;

    /**
    * 若写入优先则为true,否则读取优先。
    * 不降低线程的生存性,去掉不影响结果的输出。
    * 让ReadThread和WriteThread轮流优先执行,
    * 就像实际生活中车前进方向的信号灯和人行方向的信号灯轮流变为红灯一样。
    */
    private boolean preferWriter = true;


    public synchronized void readLock() throws InterruptedException {
    // 读时,如果有线程正在写入,避免冲突,wait
    while (writeWriters > 0 || (preferWriter && waitingWriters > 0)) {
    wait();
    }
    // 实际正在读取的线程个数加1
    readingReaders++;
    }

    public synchronized void readUnLock() {
    // 实际正在读取的线程个数减1
    readingReaders--;
    preferWriter = true;
    notifyAll();
    }

    public synchronized void writeLock() throws InterruptedException {
    // 正在等待的线程数量加1
    waitingWriters++;
    try {
    // 写时,如果有线程正在读或写入,避免冲突,wait
    while (readingReaders > 0 || writeWriters > 0) {
    wait();
    }
    } finally {
    // 正在等待的线程数量减1
    waitingWriters--;
    }
    // 实际正在写入的线程数量加1
    writeWriters++;
    }

    public synchronized void writeUnLock() {
    // 实际正在写入的线程数量减1
    writeWriters--;
    preferWriter = false;
    notifyAll();
    }

    }

结果输出

  • 当reader角色正在读取,writer角色正在等待的情形
    当reader角色正在读取,writer角色正在等待的情形

  • 当一个writer角色正在写入,reader和其他writer角色正在等待的情形
    当一个writer角色正在写入,reader和其他writer角色正在等待的情形

  • 正常加读写锁输出
    结果打印ReadThread读取的内容,当WriteThread写入时,输出会暂停一下。reads日志中右边的10个字符是相同的,因为即使有线程冲突,程序的安全性是可以保证,否则会多个字符交叉在一起显示。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    Thread-1 reads: **********
    Thread-0 reads: **********
    Thread-3 reads: **********
    Thread-2 reads: **********
    Thread-5 reads: **********
    Thread-4 reads: **********
    Thread-2 reads: aaaaaaaaaa
    Thread-4 reads: aaaaaaaaaa
    Thread-1 reads: aaaaaaaaaa
    Thread-5 reads: aaaaaaaaaa
    Thread-0 reads: aaaaaaaaaa
    Thread-3 reads: aaaaaaaaaa
    Thread-0 reads: 1111111111
    Thread-4 reads: 1111111111
    Thread-4 reads: 1111111111
    Thread-3 reads: 1111111111
    Thread-5 reads: 1111111111
    Thread-1 reads: 1111111111
    Thread-4 reads: 1111111111
    Thread-0 reads: 1111111111
    Thread-2 reads: 1111111111
    Thread-3 reads: 1111111111
    Thread-1 reads: 1111111111
    Thread-5 reads: 1111111111
    Thread-4 reads: 1111111111
    Thread-5 reads: 1111111111
    Thread-1 reads: 1111111111
    Thread-3 reads: 1111111111
    ...
  • 将读写锁换为synchronized关键字
    耗时会更久

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
Thread-0 reads: **********
Thread-5 reads: 1111111111
Thread-4 reads: 1111111111
Thread-3 reads: 1111111111
Thread-2 reads: 1111111111
Thread-1 reads: 1111111111
Thread-1 reads: 1111111111
Thread-1 reads: 1111111111
Thread-1 reads: 1111111111
Thread-1 reads: 1111111111
Thread-1 reads: 1111111111
Thread-2 reads: 1111111111
Thread-3 reads: 1111111111
Thread-4 reads: 1111111111
Thread-4 reads: 1111111111
Thread-5 reads: 1111111111
Thread-0 reads: 1111111111
Thread-5 reads: 1111111111
Thread-4 reads: 1111111111
Thread-3 reads: 1111111111
Thread-2 reads: 1111111111
Thread-2 reads: 1111111111
Thread-1 reads: 1111111111
Thread-2 reads: 1111111111
Thread-3 reads: 1111111111
Thread-4 reads: 1111111111
Thread-5 reads: 1111111111
Thread-0 reads: 1111111111
Thread-5 reads: 1111111111
Thread-4 reads: 1111111111
Thread-3 reads: 1111111111
Thread-3 reads: 1111111111
Thread-2 reads: 1111111111
Thread-1 reads: 1111111111
Thread-2 reads: 1111111111
Thread-3 reads: 1111111111
Thread-4 reads: 1111111111
Thread-5 reads: 1111111111
Thread-5 reads: 1111111111
Thread-0 reads: 1111111111
Thread-5 reads: 1111111111
Thread-4 reads: 1111111111
Thread-3 reads: 1111111111
Thread-2 reads: 1111111111
Thread-1 reads: 1111111111
Thread-2 reads: 1111111111
Thread-3 reads: 1111111111
Thread-4 reads: 1111111111
Thread-5 reads: 1111111111
Thread-0 reads: 1111111111
Thread-5 reads: 1111111111
Thread-4 reads: bbbbbbbbbb
Thread-3 reads: bbbbbbbbbb
Thread-2 reads: bbbbbbbbbb
Thread-1 reads: bbbbbbbbbb
Thread-2 reads: bbbbbbbbbb
Thread-3 reads: bbbbbbbbbb
Thread-4 reads: bbbbbbbbbb
Thread-5 reads: 2222222222
Thread-0 reads: 2222222222
Thread-5 reads: 2222222222
Thread-4 reads: 2222222222
...
  • 不加锁(去掉Data类中read和write方法中的锁),非线程安全的输出:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    Thread-1 reads: **********
    Thread-5 reads: **********
    Thread-2 reads: **********
    Thread-4 reads: **********
    Thread-3 reads: **********
    Thread-0 reads: **********
    Thread-5 reads: aa********
    Thread-2 reads: aa********
    Thread-4 reads: aa********
    Thread-1 reads: aa********
    Thread-3 reads: aa********
    Thread-0 reads: aa********
    Thread-2 reads: aaa*******
    Thread-1 reads: aaa*******
    Thread-4 reads: aaa*******
    Thread-5 reads: aaa*******
    Thread-3 reads: aaa*******
    Thread-0 reads: aaa*******
    Thread-2 reads: aaa1******
    Thread-4 reads: aaa1******
    Thread-1 reads: aaa1******
    Thread-5 reads: aaa1******
    Thread-3 reads: aaa1******
    Thread-0 reads: aaa1******
    Thread-2 reads: aaa1a*****
    Thread-1 reads: aaa1a*****
    Thread-4 reads: aaa1a*****
    Thread-5 reads: aaa1a*****
    Thread-3 reads: aaa1a*****
    Thread-4 reads: aaa1aaa***
    Thread-5 reads: aaa1aaa***
    Thread-3 reads: aaa1aaa***
    Thread-0 reads: aaa1aaa***
    Thread-1 reads: aaa1aaa***
    Thread-2 reads: aaa1aaaa**
    Thread-4 reads: aaa1aaaa**
    Thread-5 reads: aaa1aaaa**
    Thread-3 reads: aaa1aaaa**
    Thread-0 reads: aaa1aaaa**
    Thread-1 reads: aaa1aaaa**
    Thread-2 reads: aaa1aaaa1*
    Thread-4 reads: aaa1aaaa1*
    Thread-5 reads: aaa1aaaa1*
    Thread-3 reads: aaa1aaaa1*
    Thread-0 reads: aaa1aaaa1*
    Thread-1 reads: aaa1aaaa1a
    Thread-2 reads: aaa1aaaa1a
    Thread-4 reads: aaa1aaaa1a
    Thread-3 reads: aaa1aaaa1a
    Thread-0 reads: aaa1aaaa1a
    Thread-1 reads: aaa1aaaa1a
    ...

扩展

juc包下的读写锁:ReentrantReadWriteLock

java.util.concurrent.locks包下提供了ReadWriteLock接口ReentrantReadWriteLock实现类,来提供Read-Write Lock模式的读写锁功能。

特征

ReentrantReadWriteLock类的主要特征有:

1、公平性

在创建ReentrantReadWriteLock类的实例时,可以选择锁的获取顺序是否为公平(fair)的。
如果创建的实例是公平的,那么等待时间久的线程可以优先获取锁

2、可重入性

ReentrantReadWriteLock类的锁是可重入的(Reentrant)。
可重入锁:是指以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。

可参考:JAVA可重入锁与不可重入锁

  • synchronized和ReentrantReadWriteLock都是可重入锁;
  • 意义防止死锁

    如:子类覆写了父类的synchonized方法,然后调用父类中的方法,此时如果没有可重入的锁,那么这段代码将产生死锁。

  • 实现原理

    为每一个锁关联一个请求计数器和一个占有它的线程。当计数为0时,即锁未被占有,线程请求时JVM将其记录为锁的占有者并将请求数记为1。如果同一个线程再次请求这个锁,计数器递增。
    每次占用线程退出同步块时,计数器的值将递减,直到计数器为0,锁就会被释放。

  • 一个线程执行synchronized同步代码时,再次重入该锁过程中,如果抛出异常,会释放锁

    可参考:线程执行SYNCHRONIZED同步代码块时再次重入该锁过程中抛异常,是否会释放锁

3、锁降级

ReentrantReadWriteLock类可以按照下面的顺序将用于写入的锁降级为用于读取的锁

获取用于写入的锁 -> 获取用于读取的锁 -> 释放用于写入的锁。

注意:用于读取的锁不可以降级为用于写入的锁。

4、便捷方法

提供了一些便捷方法:

  • 获取等待中的线程个数的方法:getQueueLength;
  • 检查是否获取了用于写入的锁的方法:isWriteLocked;
案例

使用juc包下的读写锁RenentrantReadWriteLock来实现Data类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
package com.bubble.demo.read_write_lock;

import java.util.Arrays;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
* 基于juc下的ReentrantReadWriteLock读写锁来实现
*
* @author wu gang
* date: 2021-10-09 16:09
**/
public class DataByJUC {

/**
* 实际读写对象的数组
*/
private final char[] buffer;
/**
* 读写锁
*/
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.writeLock();

/**
* 基于size初始化buffer数组,默认用字符*作为初始值来填充
*
* @param size 数组长度
*/
public DataByJUC(int size) {
this.buffer = new char[size];
Arrays.fill(buffer, '*');
}

public char[] read() throws InterruptedException {
readLock.lock();
try {
return doRead();
} finally {
readLock.unlock();
}
}

private char[] doRead() {
char[] newBuf = new char[buffer.length];
System.arraycopy(buffer, 0, newBuf, 0, buffer.length);
slowly();
return newBuf;
}

public void write(char c) throws InterruptedException {
writeLock.lock();
try {
doWrite(c);
} finally {
writeLock.unlock();
}
}

private void doWrite(char c) {
for (int i = 0; i < buffer.length; i++) {
buffer[i] = c;
// 假定写入操作比读取操作耗时久,每写入一个字符就sleep下
slowly();
}
}

/**
* 模拟耗时操作
*/
private void slowly() {
try {
Thread.sleep(50);
} catch (InterruptedException ignored) {
}
}

}

评论