Fork me on GitHub

读写锁

1. 前言

如果使用 synchronized 关键字,那么无论是读请求,还是写请求,都是串行化,十分影响性能,而读请求本身是可以并行的。互联网中大部分请求都是读请求,如果能允许并行读,写的时候使用串行,效率会更高。

2. 读写分离锁

有一点值得注意的是,只要涉及到写操作,就一定要加锁,串行化。

2.1 先定义ReadWriteLock类

首先我们定义几个变量,用来记录处于各个状态的线程数量。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 正在读的线程数
private int readingNumber = 0;

// 等待读的线程数
private int waitingReadNumber = 0;

// 正在写的线程数 最多为1
private int writingNumber = 0;

// 等待写的线程数
private int waitingWriteNumber = 0;

// 偏向锁
private boolean preferWriteFlag;

其中值得注意的是 preferWriteFlag ,这个变量是为了满足我们自定义读写偏向性的需求。比如,如果我们更希望写优先,那么设置 preferWriteFlag 为 true 。在读加锁的时候,判断如果该值为 true,并且有部分写线程正处于等待中,那么读线程先 wait ,让写线程先执行。

我示例中的代码是写优先,当等待写线程数大于 1 时,读线程先进入等待,让写线程先执行。具体代码如2.1.1。

2.1.1 读加锁

在读加锁的时候,要判断是否有线程正在写,如果有,则要先等待。如果没有,则正在读线程数加 1。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public synchronized void readLock() throws InterruptedException {
// 1 等待读线程数加1
waitingReadNumber++;
try {
// 2.0 如果有线程正在写,先等着 如果更偏向于写优先,并且有部分写线程在等待,也等着
while (writingNumber > 0 || (preferWriteFlag && waitingWriteNumber > 1)) {
this.wait();
}
// 2.1 没有线程在写了,开始读,正在读线程数加1
readingNumber++;
} finally {
// 3 等待读线程数减1
waitingReadNumber--;
}
}

2.1.2 读解锁

解锁比较简单,正在读线程数减 1 ,然后唤醒所有线程。

1
2
3
4
public synchronized void readUnlock() {
readingNumber--;
this.notifyAll();
}

2.1.3 写加锁

写加锁和读加锁类似,只不过判断条件不同。如果有正在读的线程或者正在写的线程,则等待。

1
2
3
4
5
6
7
8
9
10
11
public synchronized void writeLock() throws InterruptedException {
waitingWriteNumber++;
try {
while (readingNumber > 0 || writingNumber > 0) {
this.wait();
}
writingNumber++;
} finally {
waitingWriteNumber--;
}
}

2.1.4 写解锁

和读解锁类似。

1
2
3
4
public synchronized void writeUnlock() {
writingNumber --;
this.notifyAll();
}

2.1.5 提供设置preferWriteFlag的构造函数

我默认是写优先,但是提供构造函数可以修改。构造函数如下:

1
2
3
4
5
6
7
public ReadWriteLock() {
this(true);
}

public ReadWriteLock(boolean preferWriteFlag) {
this.preferWriteFlag = preferWriteFlag;
}

如构造函数代码所示,默认空构造函数设置为 true ,提供非空构造函数修改 preferWriteFlag 的值。

2.2 写共享数据类

读写锁的目标就是为了同时操作同一个共享数据,所以我们定义一个共享数据类。

我们定义共享数据为字符数组,然后引入读写锁类,提供读方法和写方法即可。具体代码如下:

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
public class ShareData {

private final char[] buffer;

private final ReadWriteLock readWriteLock = new ReadWriteLock();

public ShareData(int size) {
buffer = new char[size];
for (int i = 0; i < size; i++) {
buffer[i] = '#';
}

}

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

private char[] doRead() {
char[] newBuffer = new char[buffer.length];
for (int i = 0; i < buffer.length; i++) {
newBuffer[i] = buffer[i];
try {
Thread.sleep(100);
} catch (InterruptedException e) {

}
}
return newBuffer;
}

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

private void doWrite(char c) {
for (int i = 0; i < buffer.length; i++) {
buffer[i] = c;
try {
Thread.sleep(200);
} catch (InterruptedException e) {
}
}
}

}

代码比较简单,主要是 read 和 write 两个方法,每个方法执行都是先加锁,再解锁。为了能够看到测试效果,执行中线程会 sleep 一定的时间(如果不 sleep,控制台打印太快,看不清)。

注意: read 方法读的数据时副本。构造方法中设置了默认的数据为 ##### 。

2.3 写读线程和写线程类

无论读线程类还是写线程类,都要引入共享数据类作为变量,写线程类还要定义数据变量。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ReadThread extends Thread {

private final ShareData shareData;

public ReadThread(ShareData shareData) {
this.shareData = shareData;
}

@Override
public void run() {
try {
while (true) {
char[] c = shareData.read();
System.out.println(Thread.currentThread().getName() + " read " + String.valueOf(c) + " from share data");
}
} catch (InterruptedException e) {
}
}
}
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
public class WriteThread extends Thread {

private final ShareData shareData;

private final String writeChar;

private int index = 0;

public WriteThread(ShareData shareData, String writeChar) {
this.shareData = shareData;
this.writeChar = writeChar;
}

@Override
public void run() {
try {
while (true) {
char c = nextChar();
shareData.write(c);
System.out.println("write " + c + " to share data");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}

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

}

2.4 测试类

在测试类中读线程多一些,写线程少一些,也比较符合实际情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ReadWriteTest {


public static void main(String[] args) {
ShareData shareData = new ShareData(20);

new ReadThread(shareData).start();
new ReadThread(shareData).start();
new ReadThread(shareData).start();
new ReadThread(shareData).start();
new ReadThread(shareData).start();
new ReadThread(shareData).start();
new ReadThread(shareData).start();
new ReadThread(shareData).start();
new ReadThread(shareData).start();

new WriteThread(shareData,"abcdefg").start();
new WriteThread(shareData,"hijklmn").start();

}


}

最终执行结果如下:

Thread-8 read #################### from share data
Thread-7 read #################### from share data
Thread-3 read #################### from share data
Thread-2 read #################### from share data
Thread-5 read #################### from share data
Thread-4 read #################### from share data
Thread-6 read #################### from share data
Thread-1 read #################### from share data
Thread-0 read #################### from share data
write a to share data
write h to share data
Thread-0 read hhhhhhhhhhhhhhhhhhhh from share data
Thread-2 read hhhhhhhhhhhhhhhhhhhh from share data
Thread-6 read hhhhhhhhhhhhhhhhhhhh from share data
Thread-1 read hhhhhhhhhhhhhhhhhhhh from share data
Thread-4 read hhhhhhhhhhhhhhhhhhhh from share data
Thread-5 read hhhhhhhhhhhhhhhhhhhh from share data
Thread-3 read hhhhhhhhhhhhhhhhhhhh from share data
write i to share data
Thread-3 read iiiiiiiiiiiiiiiiiiii from share data
Thread-1 read iiiiiiiiiiiiiiiiiiii from share data
Thread-4 read iiiiiiiiiiiiiiiiiiii from share data
Thread-5 read iiiiiiiiiiiiiiiiiiii from share data
write b to share data
Thread-4 read bbbbbbbbbbbbbbbbbbbb from share data
Thread-5 read bbbbbbbbbbbbbbbbbbbb from share data

分布还是比较均匀的。

3. JDK锁对比

synchronized : 随着 JDK 版本不断迭代,效率逐渐提高;

StampedLock:JDK1.8 引入的一个锁,有兴趣可以自己了解下;

ReadWriteLock:要正确使用,读多写少性能好。

有文章对这三种锁在各种情况下进行性能的对比,综合情况下,StampedLock 表现最优。所以,为图方便,可以无脑使用 StampedLock 。

本文标题:读写锁

原始链接:https://zhaoxiaofa.com/2019/02/15/读写锁分离/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。