0%

Java多线程之StampedLock

这里就JUC包中的StampedLock做相关介绍

abstract.jpeg

概述

在读多写少的场景下,非常适合使用ReentrantReadWriteLock读写锁。但其也存在一定的弊端,其有可能导致写线程饥饿。为此JDK 8中提供了StampedLock类,其是一个非公平的读写锁。其与ReentrantReadWriteLock相比,不仅提供了传统意义上的悲观读锁和写锁,最大的区别是其还为读操作提供了乐观锁的方法——即所谓的乐观读锁。当然其也有弊端,无论是悲观读锁还是写锁,均不支持条件变量Condition;然后从类名也可以看到其是不可重入锁。需要注意的是,虽然一个线程可以多次获取悲观读锁,但究其原因是因为悲观读锁是共享锁。实际实践中,可以直接通过writeLock、readLock等阻塞式 或 tryWriteLock、tryReadLock等非阻塞式的方式获取锁,也可通过ReadLockView读锁视图、WriteLockView写锁视图、ReadWriteLockView读写锁视图来进行相应锁的操作

基本实践

读锁、写锁

这里就基本的悲观读锁、写锁的使用进行实践,示例如下所示

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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class StampedLockTest1 {

private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");

private static ExecutorService threadPool = Executors.newFixedThreadPool(10);

private static StampedLock stampedLock = new StampedLock();

private static Integer count;

/**
* 测试: 读锁为共享锁
*/
@Test
public void test1() {
System.out.println("\n---------------------- Test 1 ----------------------");

count = 100;
for(int i=1; i<5; i++) {
Runnable runnable = new ReadTask("Task"+i);
threadPool.execute( runnable );
}

// 主线程等待所有任务执行完毕
try{ Thread.sleep( 10*1000 ); } catch (Exception e) {}
}

/**
* 测试: 写锁为独占锁
*/
@Test
public void test2() {
System.out.println("\n---------------------- Test 2 ----------------------");

count = 200;
for(int i=1; i<5; i++) {
Runnable runnable = new WriteTask("Task"+i);
threadPool.execute( runnable );
}

// 主线程等待所有任务执行完毕
try{ Thread.sleep( 10*1000 ); } catch (Exception e) {}
}

/**
* 测试: 读写互斥
*/
@Test
public void test3() {
System.out.println("\n---------------------- Test 3 ----------------------");

count = 300;
for(int i=1; i<9; i++) {
Runnable task = null;
Boolean isReadTask = RandomUtils.nextBoolean();
if( isReadTask ) {
task = new ReadTask2("读任务 #"+i);
} else {
task = new WriteTask2("写任务 #"+i);
}
threadPool.execute( task );
}
// 主线程等待所有任务执行完毕
try{ Thread.sleep( 20*1000 ); } catch (Exception e) {}
}

/**
* 打印信息
* @param msg
*/
public static void info(String msg) {
String time = formatter.format(LocalTime.now());
String log = "["+time+"] " + msg;
System.out.println(log);
}

@AllArgsConstructor
private static class ReadTask implements Runnable {

private String taskName;

@Override
public void run() {
Integer localData = null;
long stamp = stampedLock.readLock();
try{
info(taskName + ": 成功获取读锁, stamp: " + stamp);
localData = count;
} catch (Exception e) {
info(taskName+"Happen Exception");
} finally {
info(taskName + ": 释放读锁, stamp: "+stamp+", localData: "+localData);
stampedLock.unlockRead(stamp);
}
}
}

@AllArgsConstructor
private static class WriteTask implements Runnable {

private String taskName;

@Override
public void run() {
long stamp = stampedLock.writeLock();
try {
info(taskName + ": 成功获取写锁, stamp: " + stamp);
count++;
} catch (Exception e) {
info(taskName+"Happen Exception");
} finally {
info(taskName + ": 释放写锁, stamp: "+stamp+", count: " + count);
stampedLock.unlockWrite(stamp);
}
}
}

@AllArgsConstructor
private static class ReadTask2 implements Runnable {

private String taskName;

@Override
public void run() {
Integer localData = null;
Lock readLock = stampedLock.asReadLock();
readLock.lock();
try{
info(taskName + ": 成功获取读锁");
localData = count;
} catch (Exception e) {
info(taskName+"Happen Exception");
} finally {
info(taskName + ": 释放读锁, localData: "+localData+"\n");
readLock.unlock();
}
}
}

@AllArgsConstructor
private static class WriteTask2 implements Runnable {

private String taskName;

@Override
public void run() {
Lock writeLock = stampedLock.asWriteLock();
writeLock.lock();
try {
info(taskName + ": 成功获取写锁");
count++;
} catch (Exception e) {
info(taskName+"Happen Exception");
} finally {
info(taskName + ": 释放写锁, count: " + count +"\n");
writeLock.unlock();
}
}
}
}

测试结果如下所示,符合预期

figure 1.jpeg

可以看到StampedLock获取锁、释放锁都需要相应的stamp值。为此也可以通过相应的视图类进行操作,如上述代码的test3所示。其相应测试结果所示。可以看到悲观读锁是一个共享锁,而写锁则是一个互斥锁

figure 2.jpeg

乐观读锁

可通过tryOptimisticRead获取一个stamp,即所谓的乐观读锁。然后在完成读操作后,通过validate方法对stamp进行检查。由于读过程通常是非原子性的,故需要判断是否存在其他线程在此期间获取到了写锁,对数据进行了修改。造成当前线程读取的数据状态不一致(部分为修改前的,部分为修改后的)。如果在当前线程进行读的过程中发生了修改更新,则检查结果为false。这时再获取悲观读锁进行重读。事实上,由于乐观读锁并没有锁。故其一方面不会阻塞写线程获取写锁,也不需要在结束后释放该锁。示例代码如下所示

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
88
89
90
91
92
93
94
public class StampedLockTest2 {

private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");

private static ExecutorService threadPool = Executors.newFixedThreadPool(10);

private static StampedLock stampedLock = new StampedLock();

private static Integer count;

/**
* 测试: 乐观读锁
*/
@Test
public void test1() {
count = 500;

threadPool.execute( new ReadTask("读任务 #1",0) );
threadPool.execute( new ReadTask("读任务 #2",0) );
threadPool.execute( new ReadTask("读任务 #33",2*1000) );
threadPool.execute( new ReadTask("读任务 #44",2*1000) );

try{ Thread.sleep( 1000 ); } catch (Exception e) {}
threadPool.execute( new WriteTask("写任务 #55") );

// 主线程等待所有任务执行完毕
try{ Thread.sleep( 20*1000 ); } catch (Exception e) {}
}

/**
* 打印信息
* @param msg
*/
public static void info(String msg) {
String time = formatter.format(LocalTime.now());
String log = "["+time+"] " + msg;
System.out.println(log);
}

@AllArgsConstructor
private static class ReadTask implements Runnable {

private String taskName;

private Integer sleepTime;

@Override
public void run() {
long stamp = stampedLock.tryOptimisticRead();
info(taskName + ": 成功获取乐观读锁, stamp: "+stamp);
// 读取数据
Integer localData = count;
// 模拟业务耗时
try{ Thread.sleep(sleepTime); } catch (Exception e) {}
info(taskName + ":localData: "+localData);

// 检查在获取乐观锁后, 是否被写锁获得过
if( !stampedLock.validate(stamp) ) {
info(taskName+": 数据被其他线程修改需重读");
stamp = stampedLock.readLock();
info(taskName + ": 成功获取读锁, stamp: " + stamp);
try{
// 模拟业务耗时
try{ Thread.sleep(500); } catch (Exception e) {}
} catch (Exception e) {
info(taskName+"Happen Exception");
} finally {
info(taskName + ": 释放读锁, stamp: "+stamp+", count: " + count+"\n");
stampedLock.unlockRead(stamp);
}
}
}
}

@AllArgsConstructor
private static class WriteTask implements Runnable {

private String taskName;

@Override
public void run() {
long stamp = stampedLock.writeLock();
try {
info(taskName + ": 成功获取写锁, stamp: " + stamp);
count++;
} catch (Exception e) {
info(taskName+"Happen Exception");
} finally {
info(taskName + ": 释放写锁, stamp: "+stamp+", count: " + count +"\n");
stampedLock.unlockWrite(stamp);
}
}
}
}

测试结果如下所示

figure 3.jpeg

参考文献

  1. Java并发编程之美 翟陆续、薛宾田著
请我喝杯咖啡捏~

欢迎关注我的微信公众号:青灯抽丝