ReentrantReadWriteLock和StampedLock
Java并发编程一:并发基础必知
Java并发编程二:Java中线程
Java并发编程三:volatile使用
Java并发编程四:synchronized和lock
Java并发编程五:Atomic原子类
Java并发编程六:并发队列
ReentrantReadWriteLock
是一种基于lock的读写锁,在使用ReentrantLock时,它保证当前只有一个线程获取锁,但是有时候我们实际应用中会出现读多写少的场景,读于读之间都是读取同样的数据,如果使用ReentrantLock反而效率会低下,使用ReentrantReadWriteLock会很高效,它可以实现多个读锁同时进行,但是读与写和写于写互斥,只能有一个写锁线程在进行。
public class ReentrantReadWriteLockDemo {
static class MyDemo{
// 实例化读写锁 默认非公平
private ReentrantReadWriteLock lock=new ReentrantReadWriteLock();
// 模拟共享资源
private int number;
public void put(int number) {
// 写锁加锁
lock.writeLock().lock();
try {
Thread.sleep(500);
this.number=number;
System.out.println(Thread.currentThread().getName()+":写入了"+number);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 写锁释放锁
lock.writeLock().unlock();
}
}
public int get() {
// 读写 加锁
lock.readLock().lock();
try {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":读取了"+number);
return number;
} finally {
// 读锁解锁
lock.readLock().unlock();
}
}
}
public static void main(String[] args) {
MyDemo myDemo = new MyDemo();
// 三个写线程
for (int i = 0; i < 3; i++) {
new Thread(()-> myDemo.put(new Random().nextInt(100)),"写锁"+i).start();
}
// 十个读线程
for (int i = 0; i < 10; i++) {
new Thread(()-> myDemo.get(),"读锁"+i).start();
}
}
}
其中一次输出结果为:
写锁2:写入了96
读锁0:读取了96
写锁1:写入了27
读锁4:读取了27
读锁6:读取了27
读锁5:读取了27
读锁3:读取了27
读锁7:读取了27
读锁1:读取了27
读锁2:读取了27
写锁0:写入了66
读锁9:读取了66
读锁8:读取了66
StampedLock
它是Jdk在1.8提供的一种读写锁,相比较ReentrantReadWriteLock性能更好,因为ReentrantReadWriteLock在读写之间是互斥的,使用的是一种悲观策略,在读线程特别多的情况下,会造成写线程处于饥饿状态,虽然可以在初始化的时候设置为true指定为公平,但是吞吐量又下去了,而StampedLock是提供了一种乐观策略,更好的实现读写分离,并且吞吐量不会下降。
三种锁
- 写锁writeLock:
是一个独占锁写锁,当一个线程获得该锁后,其他请求读锁或者写锁的线程阻塞, 获取成功后,会返回一个stamp(凭据)变量来表示该锁的版本,在释放锁时调用unlockWrite方法传递stamp参数。提供了非阻塞式获取锁tryWriteLock。 - 悲观读锁readLock:
是一个共享读锁,在没有线程获取写锁情况下,多个线程可以获取该锁。如果有写锁获取,那么其他线程请求读锁会被阻塞。悲观读锁会认为其他线程可能要对自己操作的数据进行修改,所以需要先对数据进行加锁,这是在读少写多的情况下考虑的。请求该锁成功后会返回一个stamp值,在释放锁时调用unlockRead方法传递stamp参数。提供了非阻塞式获取锁方法tryWriteLock。 - 乐观读锁tryOptimisticRead:
相对比悲观读锁,在操作数据前并没有通过CAS设置锁的状态,如果没有线程获取写锁,则返回一个非0的stamp变量,获取该stamp后在操作数据前还需要调用validate方法来判断期间是否有线程获取了写锁,如果是返回值为0则有线程获取写锁,如果不是0则可以使用stamp变量的锁来操作数据。由于tryOptimisticRead并没有修改锁状态,所以不需要释放锁。这是读多写少的情况下考虑的,不涉及CAS操作,所以效率较高,在保证数据一致性上需要复制一份要操作的变量到方法栈中,并且在操作数据时可能其他写线程已经修改了数据,而我们操作的是方法栈里面的数据,也就是一个快照,所以最多返回的不是最新的数据,但是一致性得到了保证。
StampedLock支持三种锁在一定情况下进行相互转换。例如long tryConvertToWriteLock(long stamp)期望把stamp标示的锁升级为写锁,这个函数会在下面几种情况下返回一个有效的stamp(也就是晋升写锁成功):
- 如果当前锁已经是写锁,直接返回stamp。
- 如果当前是读锁,没有其他线程是读锁模式,返回一个写锁stamp。
- 如果当前是乐观读锁,并且没有线程获取写锁,返回一个stamp。
注意点
由于StampedLock是读写锁都是不可冲入锁,所以在获取锁后释放锁前不再调用获取锁操作,避免造成线程的阻塞,当多个线程同时尝试获取写锁和读锁,是随机性的,没有一定的规则,并且该锁不是实现Lock或ReadWriteLock接口,而是在其内部自己维护了一个双向队列。
案例说明
下面一个例子是官方的提供的一个二维点的的例子:
package com.smart.home.ThreadTest;
import java.util.concurrent.locks.StampedLock;
class Point {
// 内部定义表示坐标点
private double x, y;
//定义了StampedLock锁,
private final StampedLock s1 = new StampedLock();
// 写锁
public void move(double deltaX, double deltaY) {
// 获得写锁 凭据
long stamp = s1.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
// 释放写锁
s1.unlockWrite(stamp);
}
}
// 乐观锁读
public double distanceFormOrigin() {
//尝试乐观读 返回stamp凭证
long stamp = s1.tryOptimisticRead();
//读取x和y的值,这时候我们并不确定x和y是否是一致的 需要下一步再次判断
double currentX = x, currentY = y;
/**
* 判断stamp在读过程发生期间被修改过,如果没有被修改,则这次读取有效,直接return
* 如果stamp被修改过,则有可能其他线程改写了数据,会出现脏读,可以使用死循环使用乐观锁读,直到成功
* 也可以使用锁的级别,将乐观锁变为悲观锁
*/
if (!s1.validate(stamp))
// 使用悲观锁读 如果有写线程那么该线程会挂起
stamp = s1.readLock();
try {
currentX = x;
currentY = y;
} finally {
// 释放读锁
s1.unlockRead(stamp);
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
// 读锁转为写锁
public void moveIfAtOrigin(double newX, double newY) {
// 读锁加锁 可以使用乐观读锁替代
long stamp = s1.readLock();
try {
// 如果当前是原点 则修改
while (x == 0.0 && y == 0.0) {
// 尝试升级为写锁
long ws = s1.tryConvertToWriteLock(stamp);
// 升级成功 更新stamp凭据 设置坐标值 退出循环
if (ws != 0L) {
stamp = ws;
x = newX;
y = newY;
break;
} else {
// 升级失败 释放读锁,重新获取写锁,循环重试
s1.unlockRead(stamp);
stamp = s1.writeLock();
}
}
} finally {
// 释放锁
s1.unlock(stamp);
}
}
}
使用乐观锁读可以避免写锁处于饥饿状态,增加吞吐量,但是使用乐观锁读也是很容易犯错误的,在使用上必须保证以下顺序。
//乐观读 返回stamp凭证
long stamp = lock.tryOptimisticRead();
// 复制变量到方法栈
CopyVariablesMethodStack();
// 校验stamp凭证
if (!lock.validate(stamp)){
// 获取读锁
stamp = lock.readLock();
try {
// 复制变量到方法栈
CopyVariablesMethodStack();
} finally {
// 释放读锁
lock.unlockRead(stamp);
}
}
// 操作操作复制到方法栈中的变量
ManipulateVariablesCopeMethodStack();