Java多线程之StampedLock
这里就JUC包中的StampedLock做相关介绍
概述
在读多写少的场景下,非常适合使用ReentrantReadWriteLock读写锁。但其也存在一定的弊端,其有可能导致写线程饥饿。为此JDK 8中提供了StampedLock类,其是一个非公平的读写锁。其与ReentrantReadWriteLock相比,不仅提供了传统意义上的悲观读锁和写锁,最大的区别是其还为读操作提供了乐观锁的方法——即所谓的乐观读锁。当然其也有弊端,无论是悲观读锁还是写锁,均不支持条件变量Condition;然后从类名也可以看到其是不可重入锁。需要注意的是,虽然一个线程可以多次获取悲观读锁,但究其原因是因为悲观读锁是共享锁。实际实践中,可以直接通过writeLock、readLock等阻塞式 或 tryWriteLock、tryReadLock等非阻塞式的方式获取锁,也可通过ReadLockView读锁视图、WriteLockView写锁视图、ReadWriteLockView读写锁视图来进行相应锁的操作
基本实践
读锁、写锁
这里就基本的悲观读锁、写锁的使用进行实践,示例如下所示
@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();
}
}
}
}
测试结果如下所示,符合预期
可以看到StampedLock获取锁、释放锁都需要相应的stamp值。为此也可以通过相应的视图类进行操作,如上述代码的test3所示。其相应测试结果所示。可以看到悲观读锁是一个共享锁,而写锁则是一个互斥锁
乐观读锁
可通过tryOptimisticRead获取一个stamp,即所谓的乐观读锁。然后在完成读操作后,通过validate方法对stamp进行检查。由于读过程通常是非原子性的,故需要判断是否存在其他线程在此期间获取到了写锁,对数据进行了修改。造成当前线程读取的数据状态不一致(部分为修改前的,部分为修改后的)。如果在当前线程进行读的过程中发生了修改更新,则检查结果为false。这时再获取悲观读锁进行重读。事实上,由于乐观读锁并没有锁。故其一方面不会阻塞写线程获取写锁,也不需要在结束后释放该锁。示例代码如下所示
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);
}
}
}
}
测试结果如下所示
参考文献
Java并发编程之美 翟陆续、薛宾田著