目录
2、ReentrantReadWriteLock 可重入读写锁
2.2.2 ReentrantReadWriteLock锁演示
2.3 ReentrantReadWriteLock写锁饥饿问题
3.3.3 tryConvertToWriteLock 尝试锁转换
无锁—独占锁—读写锁—邮戳锁
1、面试题
1.用户读写锁吗,锁饥饿问题是什么?
2.有没有比读写锁更快的锁?(StampedLock锁知道吗?)
3.ReentrantReadWriteLock有锁降级机制,知道吗?
2、ReentrantReadWriteLock 可重入读写锁
其实还是实现的AQS
2.1 可重入读写锁定义
一个资源可以被多个读线程访问,或者被一个写线程访问。但是不能同时存在读写线程
它只允许读读共存,而读写和写写依然是互斥的,大多实际场景是"读/读"线程间并不存在互斥关系,只有"读/写"线程或“写/写”线程间的操作需要互斥的。因此引入ReentrantReadWriteLock。
简单的可以理解为读的时候不让写。(这也是导致锁饥饿问题的原因)
只有再读多写少的情景下,读写锁才具有较高的性能体现。
2.2 代码演示
2.2.1 ReentrantLock锁演示
package com.guigu.juc.aqs;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @Author: gzh
* @Date: 2022/8/29 14:26
*/
public class ReentrantReadWriteLockDemo {
public static void main(String[] args) {
MyResource myResource = new MyResource();
// 先写
for(int i = 0; i < 10;i++){
int finalI = i;
new Thread(() -> {
myResource.write(String.valueOf(finalI),String.valueOf(finalI));
},String.valueOf(i)).start();
}
// 再读
for(int i = 0; i < 10;i++){
int finalI = i;
new Thread(() -> {
myResource.read(String.valueOf(finalI));
},String.valueOf(i)).start();
}
}
}
class MyResource{
Map<String,String> map = new HashMap<>();
/** ReentrantLock */
Lock lock = new ReentrantLock();
/** 读写锁- 读读共享,读写互斥 */
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void write(String key,String val){
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "正在写入");
map.put(key,val);
TimeUnit.MILLISECONDS.sleep(500);
System.out.println(Thread.currentThread().getName() + "完成写入");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public String read(String key){
String result = null;
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "正在读取");
result = map.get(key);
TimeUnit.MILLISECONDS.sleep(200);
System.out.println(Thread.currentThread().getName() + "完成读取 " + result);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return result;
}
}
2.2.2 ReentrantReadWriteLock锁演示
/**
* 修改锁的使用
*/
public void write(String key,String val){
// lock.lock();
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "正在写入");
map.put(key,val);
TimeUnit.MILLISECONDS.sleep(500);
System.out.println(Thread.currentThread().getName() + "完成写入");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// lock.unlock();
readWriteLock.writeLock().unlock();
}
}
public String read(String key){
String result = null;
// lock.lock();
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "正在读取");
result = map.get(key);
TimeUnit.MILLISECONDS.sleep(200);
System.out.println(Thread.currentThread().getName() + "完成读取 " + result);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// lock.unlock();
readWriteLock.readLock().unlock();
}
return result;
}
2.3 ReentrantReadWriteLock写锁饥饿问题
package com.guigu.juc.aqs;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @Author: gzh
* @Date: 2022/8/29 14:26
*/
public class ReentrantReadWriteLockDemo {
public static void main(String[] args) {
MyResource myResource = new MyResource();
// 99个读
for(int i = 0; i < 99;i++){
int finalI = i;
new Thread(() -> {
myResource.read(String.valueOf(finalI));
},String.valueOf(i)).start();
}
// 一个写
new Thread(() -> {
myResource.write(String.valueOf(1),String.valueOf(1));
},"写线程").start();
}
}
class MyResource{
Map<String,String> map = new HashMap<>();
/** ReentrantLock */
Lock lock = new ReentrantLock();
/** 读写锁- 读读共享,读写互斥 */
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void write(String key,String val){
// lock.lock();
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "正在写入");
map.put(key,val);
TimeUnit.MILLISECONDS.sleep(500);
System.out.println(Thread.currentThread().getName() + "完成写入");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// lock.unlock();
readWriteLock.writeLock().unlock();
}
}
public String read(String key){
String result = null;
// lock.lock();
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "正在读取");
result = map.get(key);
// TimeUnit.MILLISECONDS.sleep(200);
// 将读的时间调长
TimeUnit.MILLISECONDS.sleep(2000);
System.out.println(Thread.currentThread().getName() + "完成读取 " + result);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// lock.unlock();
readWriteLock.readLock().unlock();
}
return result;
}
}
2.4 ReentrantReadWriteLock锁降级
将写入锁降级为读锁(类似Linux文件读写权限的理解,写的权限要远高于读权限一样),锁的严苛程度变强叫做升级,反之叫做降级。
写锁的降级,降级成为读锁:
1、如果同一个线程持有了写锁,再没有释放写锁的情况下,它还可以继续获得读锁。
2、规则惯例,先获取写锁,然后获取读锁,再释放写锁的次序。
3、如果释放了写锁,那么就完全转换为读锁。
2.4.1 支持锁降级
public static void main(String[] args) {
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
Lock readLock = readWriteLock.readLock();
Lock writeLock = readWriteLock.writeLock();
writeLock.lock();
try {
System.out.println("获取写锁");
readLock.lock();
System.out.println("获取读锁");
}finally {
readLock.unlock();
writeLock.unlock();
}
}
2.4.2 不支持锁升级
public static void main(String[] args) {
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
Lock readLock = readWriteLock.readLock();
Lock writeLock = readWriteLock.writeLock();
readLock.lock();
try {
System.out.println("获取读锁");
writeLock.lock();
System.out.println("获取写锁");
}finally {
readLock.unlock();
writeLock.unlock();
}
}
不支持锁升级的原因:
2.5 锁降级Oracle公司案例
3、StampedLock 邮戳锁
3.1 是什么
StampedLock 是JDK1.8中新增的读写锁,也是多1.5中的读写锁ReentrantReadWriteLock的优化。主要解决写锁饥饿问题。
3.2 特点
所有获取锁的方法,都返回一个邮戳(stamp),stamp为0表示获取失败,其余都表示成功。
所有释放锁的方法,都需要一个邮戳(stamp),这个Stamp必须和成功获取锁的stamp一直。
StampedLock是不可重入的,如果一个线程已经持有了写锁,再获取写锁的话会造成死锁。而且它的底层不是AQS。
三种模式:
1、独占写模式:writeLock方法获取共享状态时阻塞,如果成功获取锁,返回一个stamp,它可以作为参数被用在unlockWrite方法中以释放写锁。tryWriteLock的超时与非超时版本都被提供使用。当写锁被获取,那么没有读锁能够被获取,并且所有的乐观读锁验证都会失败。
2、悲观读模式:readLock方法可能会在获取共享状态时阻塞,如果成功获取锁,返回一个stamp,它可以作为参数被用在unlockRead方法中以释放读锁。tryReadLock的超时与非超时版本都被提供使用。
3、乐观读模式:tryOptimisticRead方法只有当写锁没有被获取时会返回一个非0的stamp。在获取这个stamp后直到调用validate方法这段时间,如果写锁没有被获取,那么validate方法将会返回true。(这个模式可以被认为是读锁的一个弱化版本,因为它的状态可能随时被写锁破坏。这个乐观模式的主要是为一些很短的只读代码块的使用设计,它可以降低竞争并且提高吞吐量。但是,它的使用本质上是很脆弱的。乐观读的代码区域应当只读取共享数据并将它们储存在局部变量中以待后来使用,当然在使用前要先验证这些数据是否过期,这可以使用前面提到的validate方法。在乐观读模式下的数据读取可能是非常不一致的过程,因此只有当你对数据的表示很熟悉并且重复调用validate方法来检查数据的一致性时使用此模式。例如,当先读取一个对象或者数组引用,然后访问它的字段、元素或者方法之一时上面的步骤都是需要的。)
这个类还提供了在三种模式之间转换的辅助方法。例如,tryConvertToWriteLock方法尝试"提升"一个模式,如果已经获取了读锁并且此时没有其他线程获取读锁,那么这个方法返回一个合法的写stamp。这些方法被设计来帮助减少以“重试为主”设计时发生的代码代码膨胀。
3.3 常用API
方法 | 说明 |
long writeLock() | 获取独占锁,如果该锁被另一个线程保持,则阻塞线程,直到拿到锁并返回邮戳。 |
void unlockWrite(long stamp) | 如果锁状态与给定的邮戳匹配,则释放独占锁。 |
long readLock() | 获取共享锁,如果该锁被另一个线程保持,则阻塞线程,直到拿到锁并返回邮戳。 |
void unlockRead(long stamp) | 如果锁状态与给定的邮戳匹配,则释放共享锁。 |
void unlock(long stamp) | 如果锁状态与给定的邮戳匹配,则释放锁的相应模式。 |
long tryOptimisticRead() | 如果当前未持有独占锁则返回当前锁版本作为邮戳,用于在以后验证状态,如果已持有独占锁(独占写)则返回0,用于乐观锁。 |
boolean validate(long stamp) | 如果自给定邮戳获取后未获取过独占锁(独占锁状态码未改变),则返回 true。 如果状态为 0,则始终返回 false。 |
long tryConvertToWriteLock(long stamp) | 验证当前锁版本和锁持有状态和给定的邮戳是否匹配,如果不匹配、邮戳的锁状态有误或当前持有多个共享锁则返回0。匹配时则分三种情况,当前未持有锁则获取独占锁,当前持有独占锁则不进行操作,当前仅持有一个共享锁则释放共享锁获取独占锁,最终返回独占锁的邮戳。 |
API说明测试
公用类
// 测试公用类
public class StampedLockDemo {
Integer number = -1;
StampedLock stampedLock = new StampedLock();
/**
* 共享读
* @return
*/
@SneakyThrows
public Integer read(){
Integer num;
long stamp = stampedLock.readLock();
try {
System.out.println(Thread.currentThread().getName() + "正在读");
TimeUnit.SECONDS.sleep(2);
num = number;
System.out.println(Thread.currentThread().getName() + "完成读 " + num);
}finally {
stampedLock.unlockRead(stamp);
}
return num;
}
/**
* 独占写
* @param num
*/
@SneakyThrows
public void write(Integer num){
long stamp = stampedLock.writeLock();
try {
System.out.println(Thread.currentThread().getName() + "正在写");
number = num;
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + "完成写");
}finally {
stampedLock.unlockWrite(stamp);
}
}
/**
* 乐观读
* @return
*/
@SneakyThrows
public Integer optimisticRead(){
// 尝试获取乐观读锁
long stamp = stampedLock.tryOptimisticRead();
Integer num = number;
System.out.println(Thread.currentThread().getName() + "正在乐观读 number:" + num);
TimeUnit.SECONDS.sleep(4);
// 检查在获取到读锁stamp后,锁有没被其他写线程抢占
if(!stampedLock.validate(stamp)){
// 如果被抢占则获取一个共享读锁(悲观获取)
stamp = stampedLock.readLock();
System.out.println(Thread.currentThread().getName() + "升级悲观读");
try {
num = number;
System.out.println(Thread.currentThread().getName() + "正在共享读(悲观读) number:" + num);
}finally {
stampedLock.unlockRead(stamp);
}
}
System.out.println(Thread.currentThread().getName() + "optimisticRead number:" + num);
return num;
}
}
3.3.1 测试独占锁和共享锁
效果与ReentrantReadWriteLock一致,写锁独占,读锁共享
public static void main(String[] args) {
StampedLockDemo demo = new StampedLockDemo();
// 十个写
for(int i = 0; i < 10; i++){
int finalI = i;
new Thread(() -> {
demo.write(finalI);
},"独占写线程" + i).start();
}
// 十个读
for(int i = 0; i < 10; i++){
new Thread(() -> {
demo.read();
},"共享读线程" + i).start();
}
}
3.3.2 乐观读
public static void main(String[] args) {
StampedLockDemo demo = new StampedLockDemo();
// 十个写
for(int i = 0; i < 10; i++){
int finalI = i;
new Thread(() -> {
demo.write(finalI);
},"独占写线程" + i).start();
}
// 三个读
for(int i = 0; i < 3; i++){
new Thread(() -> {
demo.optimisticRead();
},"乐观读线程" + i).start();
}
}
3.3.3 tryConvertToWriteLock 尝试锁转换
@SneakyThrows
public static void main(String[] args) {
StampedLockDemo demo = new StampedLockDemo();
long stamped = demo.stampedLock.tryOptimisticRead();
// 让其他线程持有独占锁
new Thread(() -> {
demo.write(10);
},"独占写线程").start();
TimeUnit.MILLISECONDS.sleep(1000);
long convert = demo.stampedLock.tryConvertToWriteLock(stamped);
try {
System.out.println(Thread.currentThread().getName() + "尝试转换WriteLock(0:失败,其他成功):" + convert);
}finally {
if(convert != 0){
demo.stampedLock.unlockWrite(convert);
}
}
}
3.4 官方案例解读
package com.guigu.juc.aqs;
import java.util.concurrent.locks.StampedLock;
/**
* @Author: gzh
* @Date: 2022/8/30 11:06
*/
public class Point {
/** 成员变量 */
private double x, y;
/** 锁实例 */
private final StampedLock sl = new StampedLock();
/** 排它锁-写锁(writeLock) */
void move(double deltaX, double deltaY) {
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
/**
* 一个只读方法
* 其中存在乐观读锁到悲观读锁的转换
* @return
*/
double distanceFromOrigin() {
// 尝试获取乐观读锁
long stamp = sl.tryOptimisticRead();
// 将全部变量拷贝到方法体栈内
double currentX = x, currentY = y;
// 检查在获取到读锁stamp后,锁有没被其他写线程抢占
if (!sl.validate(stamp)) {
// 如果被抢占则获取一个共享读锁(悲观获取)
stamp = sl.readLock();
try {
// 将全部变量拷贝到方法体栈内
currentX = x;
currentY = y;
} finally {
// 释放共享读锁
sl.unlockRead(stamp);
}
}
// 返回计算结果
return Math.sqrt(currentX * currentX + currentY * currentY);
}
/**
* 获取读锁,并尝试转换为写锁
* @param newX
* @param newY
*/
void moveIfAtOrigin(double newX, double newY) {
long stamp = sl.tryOptimisticRead();
try {
// 如果当前点在原点则移动
while (x == 0.0 && y == 0.0) {
// 尝试将获取的读锁升级为写锁:
// 返回0的情况 1、验证锁版本和持有锁版本不一致 2、多个线程持有共享读锁
long ws = sl.tryConvertToWriteLock(stamp);
// 升级成功,则更新stamp,并设置坐标值,然后退出循环
if (ws != 0L) {
stamp = ws;
x = newX;
y = newY;
break;
} else {
// 读锁升级写锁失败则释放读锁,显示获取独占写锁,然后循环重试
sl.unlockRead(stamp);
stamp = sl.writeLock();
}
}
} finally {
sl.unlock(stamp);
}
}
}