无锁 - > 独占锁 - > 读写锁 - > 邮戳锁
1、面试题
-
你知道Java里面有哪些锁?
-
你说你用过读写锁,锁饥饿问题是什么?有没有比读写锁更快的锁?
-
StampedLock
知道吗?(邮戳锁/票据锁) -
ReentrantReadWriteLock
有锁降级机制,你知道吗? -
。 。 。。。
2、ReentrantReadWriteLock
定义
- 一个资源能够被 \color{blue}一个资源能够被 一个资源能够被 多个读线程 \color{red}多个读线程 多个读线程 访问,或者被 \color{blue}访问,或者被 访问,或者被 一个写线程 \color{red}一个写线程 一个写线程 访问,但是不能同时存在读写线程 \color{blue}访问,但是不能同时存在读写线程 访问,但是不能同时存在读写线程
2.1、意义与特点
读写互斥,OK
读读可以共享,多线程并发可以访问,大面积的可以容许多个线程来读取
读多写少的时候,读写锁good
缺点
-
写锁饥饿问题(大量读锁,少量写锁,导致写锁一直等待饿死)
-
注意,锁降级
-
它只允许读读共存,而读写和写写依然是互斥的 \color{red}它只允许读读共存,而读写和写写依然是互斥的 它只允许读读共存,而读写和写写依然是互斥的,大多实际场景是 " 读 / 读 " 线程间并不存在互斥关系 \color{red}"读/读"线程间并不存在互斥关系 "读/读"线程间并不存在互斥关系,
- 只有"读/写"线程或"写/′写"线程间的操作需要互斥的。
- 因此引入ReentrantReadWriteLock。
-
一个ReentrantReadWriteLock同时只能存在一个写锁但是可以存在多个读锁,
- 但不能同时存在写锁和读锁(切菜还是拍蒜选一个)。
- 也即 一个资源可以被多个读操作访问 或一个写操作访问 \color{blue}一个资源可以被多个读操作访问\quad \quad 或一个写操作访问 一个资源可以被多个读操作访问或一个写操作访问,但两者不能同时进行。
-
只有在读多写少情景之下,读写锁才具有较高的性能体现。
2.2、code演示
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(finalI + "", finalI + "");
}).start();
new Thread(() -> {
myResource.read(finalI + "");
}).start();
}
/*
普通锁
Thread-0 正在写入
Thread-0 完成写入
Thread-1 正在读取
Thread-1 完成读取 0
Thread-2 正在写入
Thread-2 完成写入
Thread-4 正在写入
Thread-4 完成写入
Thread-3 正在读取
Thread-3 完成读取 1
Thread-5 正在读取
Thread-5 完成读取 2
...
读写锁
Thread-0 正在写入
Thread-0 完成写入
Thread-2 正在写入
Thread-2 完成写入
Thread-1 正在读取
Thread-3 正在读取
Thread-1 完成读取 0
Thread-3 完成读取 1
*/
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i <= 3; i++) {
int finalI = i;
new Thread(() -> {
myResource.write(finalI + "", finalI + "");
}, "新写锁线程->" + i).start();
}
/*
....
Thread-19 正在读取
Thread-19 完成读取 9
Thread-20 正在写入
Thread-20 完成写入
Thread-21 正在读取
Thread-21 完成读取 10
新写锁线程->0 正在写入
新写锁线程->0 完成写入
新写锁线程->1 正在写入
新写锁线程->1 完成写入
新写锁线程->2 正在写入
新写锁线程->2 完成写入
新写锁线程->3 正在写入
新写锁线程->3 完成写入
*/
}
}
/**
* 资源类
*/
class MyResource {
Map<String, String> map = new HashMap<>();
Lock lock = new ReentrantLock();
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
public void write(String key, String value) {
//rwLock.writeLock().lock();
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t" + "正在写入");
map.put(key, value);
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t" + "完成写入");
} finally {
lock.unlock();
//rwLock.writeLock().unlock();
}
}
public void read(String key) {
rwLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t" + "正在读取");
String res = map.get(key);
/*try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
//读锁没有完成之前,写锁没法获得
try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
System.out.println(Thread.currentThread().getName() + "\t" + "完成读取 " + res);
} finally {
rwLock.readLock().unlock();
}
}
}
2.3、结论
一体两面、读写互斥、读读共享、读没有完成时候其它线程写锁无法获得
3、 R e e n t r a n t R e a d W r i t e L o c k 降级 \color{green}ReentrantReadWriteLock 降级 ReentrantReadWriteLock降级
3.1、关于锁降级的说明
ReentrantReadWiteLock锁降级:
- 将写入锁降级为读锁(类似Linux文件读写权限理解,就像写权限要高于读权限一样), 锁的严苛程度变强叫做升级,反之叫做降级。 \color{red}锁的严苛程度变强叫做升级,反之叫做降级。 锁的严苛程度变强叫做升级,反之叫做降级。
特性 | 说明 |
---|---|
公平性选择 | 支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平 |
重进入 | 该锁支持重进入,以读写线程为例:读线程在获取了读锁之后,能够再次获取读锁。而写线程在获取了写锁之后能够再次获取写锁,同时也可以获取读锁 |
锁降级 ! \color{red}! ! | 遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁 |
写锁的降级,降级成为了读锁 \color{blue}写锁的降级,降级成为了读锁 写锁的降级,降级成为了读锁
-
如果同一个线程持有了写锁,在没有释放写锁的情况下,它还可以继续获得读锁。 \color{red}如果同一个线程持有了写锁,在没有释放写锁的情况下,它还可以继续获得读锁。 如果同一个线程持有了写锁,在没有释放写锁的情况下,它还可以继续获得读锁。这就是写锁的降级,降级成为了读锁。
-
规则惯例,
- 先获取写锁,然后获取读锁,再释放写锁的次序。 \color{red}先获取写锁,然后获取读锁,再释放写锁的 次序。 先获取写锁,然后获取读锁,再释放写锁的次序。
-
如果释放了写锁,那么就完全转换为读锁。
w h y ? 要有这么个特性 ? − − − 后面解释,设计思想见《 O r a c l e 公司 R e e n t r a n t W r i t e R e a d L o c k 源码总结》 \color{blue}why?要有这么个特性?---后面解释,设计思想见《Oracle公司ReentrantWriteReadLock源码总结》 why?要有这么个特性?−−−后面解释,设计思想见《Oracle公司ReentrantWriteReadLock源码总结》
3.2、读写锁降级演示
3.2.1、可以降级
锁降级:
- 遵循获取写锁→再获取读锁→再释放写锁的次序,写锁能够降级成为读锁。
如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁。
J a v a 8 官网说明 \color{red}Java8官网说明 Java8官网说明
重入还允许通过获取写入锁定,然后读取锁,然后释放写锁从写锁到读取锁 , \color{red}重入还允许通过获取写入锁定,然后读取锁,然后释放写锁从写锁到读取锁, 重入还允许通过获取写入锁定,然后读取锁,然后释放写锁从写锁到读取锁,
但是,从读锁定升级到写锁是不可能的。 \color{red}但是,从读锁定升级到写锁是不可能的。 但是,从读锁定升级到写锁是不可能的。
锁降级是为了让当前线程感知到数据的变化,目的是保证数据可见性
3.2.2、code
获取写锁→再获取读锁→再释放写锁的次序,写锁能够降级成为读锁。
//若当这个线程写完的时候,另一个线程立马抢走了写锁,他就得不到他自己刚刚改的数据了
public class LockDownGradingDemo {
public static void main(String[] args) {
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
//only one 同一个线程
writeLock.lock(); //获得写锁
System.out.println("----写入");
/**
* biz
* ....
*/
//写完以后马上去读锁
readLock.lock(); //获得读锁
System.out.println("----读取");
/**
* biz
* ....
*/
writeLock.unlock(); //释放写锁
readLock.unlock(); //释放读锁
}
}
先读锁再写锁,则写锁永远等不到读锁释放
读没有完成时候写锁无法获得锁,必须要等着读锁读完后才有机会写
public class LockDownGradingDemo {
public static void main(String[] args) {
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
//only one 同一个线程
readLock.lock(); //获得读锁
System.out.println("----读取");
writeLock.lock(); //获得写锁
System.out.println("----写入");
/**
* biz
* ....
*/
writeLock.unlock(); //释放写锁
readLock.unlock(); //释放读锁
}
}
结论
如果有线程在读,那么写线程是无法获取写锁的,只能等待读锁释放再获取写锁,是悲观锁的策略 \color{red}如果有线程在读,那么写线程是无法获取写锁的,只能等待读锁释放再获取写锁,是悲观锁的策略 如果有线程在读,那么写线程是无法获取写锁的,只能等待读锁释放再获取写锁,是悲观锁的策略
3.2.3、不可锁升级
线程获取读锁是 不能 \color{red}不能 不能直接升级为写入锁的
在 R e e n t r a n t R e a d W r i t e L o c k 中,当读锁被使用时,如果有线程尝试获取写锁,该写线程会被阻塞。 \color{red}在ReentrantReadWriteLock中,当读锁被使用时,如果有线程尝试获取写锁,该写线程会被阻塞。 在ReentrantReadWriteLock中,当读锁被使用时,如果有线程尝试获取写锁,该写线程会被阻塞。
所以,需要释放所有读锁,才可获取写锁, \color{red}所以,需要释放所有读锁,才可获取写锁, 所以,需要释放所有读锁,才可获取写锁,
3.3、写锁和读锁互斥
写锁和读锁是互斥的 \color{blue}写锁和读锁是互斥的 写锁和读锁是互斥的
- (这里的互斥是指 线程间的互斥 \color{red}线程间的互斥 线程间的互斥,当前线程可以获取到写锁又获取到读锁,但是获取到了读锁不能继续获取写锁),这是因为读写锁要 保持写操作的可见性 \color{red}保持写操作的可见性 保持写操作的可见性。
因为,如果允许读锁在被获取的情况下对写锁的获取,那么正在运行的其他读线程无法感知到当前写线程的操作。
因此, 分析读写锁 R e e n t r a n t R e a d W r i t e L o c k \color{blue}分析读写锁ReentrantReadWriteLock 分析读写锁ReentrantReadWriteLock,会发现它有个潜在的问题:
- 读锁结束,写锁有望;写锁独占,读写全堵;
如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,见前面Case《code演示LockDownGradingDemo》
即ReentrantReadWriteLock
读的过程中不允许写
\color{red}读的过程中不允许写
读的过程中不允许写,
只有等待线程都释放了读锁,当前线程才能获取
\color{blue}只有等待线程都释放了读锁,当前线程才能获取
只有等待线程都释放了读锁,当前线程才能获取
写锁
\color{red}写锁
写锁,
也就是写入必须等待
\color{red}也就是写入必须等待
也就是写入必须等待,这是一种
悲观的读锁
‾
\color{red} \underline{悲观的读锁}
悲观的读锁,o(T…T)o,人家还在读着那,你先别去写,省的数据乱。
3.3.1、 后续 S t a m p e d L o c k \color{blue}后续StampedLock 后续StampedLock
分析 S t a m p e d L o c k ( 后面详细讲解 ) \color{blue}分析StampedLock(后面详细讲解) 分析StampedLock(后面详细讲解),会发现它改进之处在于:
读的过程中也允许获取写锁介入 ( 相当牛 B ,读和写两个操作也让你“共享” ( 注意引号 ) ) \color{red}读的过程中也允许获取写锁介入(相当牛B,读和写两个操作也让你“共享”(注意引号)) 读的过程中也允许获取写锁介入(相当牛B,读和写两个操作也让你“共享”(注意引号)),这样会导致我们读的数据就可能不一致
所以,需要额外的方法来判断 读的过程中是否有写入 \color{red}读的过程中是否有写入 读的过程中是否有写入,这是一种乐观的读锁,o(n_n)O哈哈~。
显然乐观锁的并发效率更高,但一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行。
4、为什么要锁降级
4.1、ReentrantWriteReadLock源码总结
4.1.1、CachedData
锁降级
\color{red}锁降级
锁降级下面的示例代码摘自ReentrantWriteReadLock源码中:
ReentrantWiteReadLock支持锁降级,遵循按照获取写锁,获取读锁再释放写锁的次序,
写锁能够降级成为读锁 \color{red}写锁能够降级成为读锁 写锁能够降级成为读锁,不支持锁升级。
class CachedData {
//被缓存的具体对象
Object data;
//当前对象是否可用,使用volatile来保证可见性
volatile boolean cacheValid;
//今天的主角,ReentrantReadWriteLock
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
//业务处理逻辑
void processCachedData() {
//要读取数据时,先加读锁,如果加成功,说明此时没有人在并发写
rwl.readLock().lock();
//拿到读锁后,判断当前对象是否有效
if (!cacheValid) {
// Must release read lock before acquiring write lock
//这里的处理非常经典,当你持有读锁之后,不能直接获取写锁,
//因为写锁是独占锁,如果直接获取写锁,那代码就在这里死锁了
//所以必须要先释放读锁,然后手动获取写锁
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
// Recheck state because another thread might have
// acquired write lock and changed state before we did.
//经典处理之二,在独占锁内部要处理数据时,一定要做二次校验
//因为可能同时有多个线程全都在获取写锁,
//当时线程1释放写锁之后,线程2马上获取到写锁,此时如果不做二次校验那可能就导致某些操作做了多次
if (!cacheValid) {
data = ...
//当缓存对象更新成功后,重置标记为true
cacheValid = true;
}
// Downgrade by acquiring read lock before releasing write lock
//这里有一个非常神奇的锁降级操作,所谓降级是说当你持有写锁后,可以再次获取读锁
//这里之所以要获取一次写锁是为了防止当前线程释放写锁之后,其他线程马上获取到写锁,改变缓存对象
//因为读写互斥,所以有了这个读锁之后,在读锁释放之前,别的线程是无法修改缓存对象的
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock(); // Unlock write, still hold read
}
}
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
}
-
代码中声明了一个
volatile
类型的cacheValid
变量,保证其可见性。 -
首先获取读锁,如果
cache
不可用,则释放读锁。 -
获取写锁,在更改数据之前,再检查一次
cacheValid
的值(双重检查),然后修改数据,将cacheVald
置为true
, 然后在释放写锁前 立刻抢夺获取读锁 \color{blue}然后在释放写锁前 \color{green}立刻抢夺获取读锁 然后在释放写锁前立刻抢夺获取读锁; -
此时,
cache
中数据可用,处理cache
中数据,最后释放读锁。这个过程就是一个完整的锁降级的过程,目的是保证数据可见性。
总结 \color{red}总结 总结:
- 一句话,同一个线程自己持有写锁时再去拿读锁,其本质相当于重入。
如果违背锁降级的步骤,如果违背锁降级的步骤,如果违背锁降级的步骤 \color{red}如果违背锁降级的步骤,如果违背锁降级的步骤,如果违背锁降级的步骤 如果违背锁降级的步骤,如果违背锁降级的步骤,如果违背锁降级的步骤
- 如果当前的线程
C
在修改完cache
中的数据后,没有获取读锁而是直接释放了写锁, - 那么假设此时另一个线程
D
获取了写锁并修改了数据,那么C
线程无法感知到数据己被修改,则数据出现错误。
如果遵循锁降级的步骤 \color{green}如果遵循锁降级的步骤 如果遵循锁降级的步骤
-
线程
C
在释放写锁之前获取读锁,那么线程D
在获取写锁时将被阻塞,直到线程C
完成数据处理过程,释放读锁。 -
这样可以保证返回的数据是这次更新的数据,该机制是专门为了缓存设计的。
4.1.2、RWDictionary
Doug Lea
给出的第二个demo
,一个并发容器的demo
。并发容器我们一般都是直接使用ConcurrentHashMap
的,但是我们可以使用非并发安全的容器+ReentrantReadWriteLock
来组合出一个并发容器。如果这个并发容器的读的频率>写的频率,那这个效率还是不错的
class RWDictionary {
//原来非并发安全的容器
private final Map<String, Data> m = new TreeMap<String, Data>();
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
public Data get(String key) {
//读数据,上读锁
r.lock();
try { return m.get(key); }
finally { r.unlock(); }
}
public String[] allKeys() {
//读数据,上读锁
r.lock();
try { return m.keySet().toArray(); }
finally { r.unlock(); }
}
public Data put(String key, Data value) {
//写数据,上写锁
w.lock();
try { return m.put(key, value); }
finally { w.unlock(); }
}
public void clear() {
//写数据,上写锁
w.lock();
try { m.clear(); }
finally { w.unlock(); }
}
}
5、面试题
有没有比读写锁更快的锁
6、邮戳锁StampedLock
6.1、是什么
StampedLock是JDK1.8中新增的一个读写锁,
也是对JDK1.5中的读写锁ReentrantReadWriteLock的优化。
邮戳锁(也叫票据锁)
stamp(戳记,long类型)
- 代表了锁的状态。当stamp返回零时,表示线程获取锁失败。
- 并且,当释放锁或者转换锁的时候,都要传入最初获取的stamp值。
6.2、锁饥饿问题
ReentrantReadWriteLock实现了读写分离,但是一旦读操作比较多的时候,想要获取写锁就变得比较困难了,
假如当前1000个线程,999个读,1个写,有可能999个读取线程长时间抢到了锁,那1个写线程就悲剧了
因为当前有可能会一直存在读锁,而无法获得写锁 \color{red}因为当前有可能会一直存在读锁,而无法获得写锁 因为当前有可能会一直存在读锁,而无法获得写锁,根本没机会写,o(TT)o
6.2.1、如何缓解锁饥饿问题
使用"公平"策略可以一定程度上缓解这个问题
new ReentrantReadWriteLock(true)
但是"公平"策略是以牺牲系统吞吐量为代价的
6.2.2、 S t a m p e d L o c k 登场 \color{red}StampedLock登场 StampedLock登场
R
e
e
n
t
r
a
n
t
R
e
a
d
W
r
i
t
e
L
o
c
k
\color{blue}ReentrantReadWriteLock
ReentrantReadWriteLock
允许多个线程间时读,但是只允许一个线程写,在线程获取到写锁的时候,其他写操作和读操作都会处于阻塞状态,读锁和写锁也是互斥的,所以在读的时候是不允许写的,
读写锁比传统的synchronized速度要快很多,
原因就是在于
R
e
e
n
t
r
a
n
t
R
e
a
d
W
r
i
t
e
L
o
c
k
\color{blue}ReentrantReadWriteLock
ReentrantReadWriteLock支持
读并发,读读可以共享
\color{red}读并发,读读可以共享
读并发,读读可以共享
S
t
a
m
p
e
d
L
o
c
k
横空出世
\color{blue}StampedLock横空出世
StampedLock横空出世
ReentrantReadWriteLock的读锁被占用的时候,其他线程尝试获取写锁的时候会被阻塞。
但是,StampedLock采取乐观获取锁后,其他线程尝试获取写锁时 不会被阻塞 \color{green}不会被阻塞 不会被阻塞,这其实是对读锁的优化,所以, 在获取乐观读锁后,还需要对结果进行校验。 \color{red}在获取乐观读锁后,还需要对结果进行校验。 在获取乐观读锁后,还需要对结果进行校验。
6.3、StampedLock的特点
-
所有 获取锁 \color{red}获取锁 获取锁的方法,都返回一个邮戳(Stamp),
- Stamp为0表示获取失败,
- 其余都表示成功;
-
所有 释放锁 \color{red}释放锁 释放锁的方法,都需要一个邮戳(Stamp),
- 这个Stamp必须是和成功获取锁时得到的Stamp一致;
-
StampedLock 是不可重入的,危险 \color{red}是不可重入的,危险 是不可重入的,危险(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)
- StampedLock有三种访问模式
- ①Reading (读模式悲观):
- 功能和ReentrantReadWriteLock的读锁类似
- ②Writing(写模式):
- 功能和ReentrantReadWriteLock的写锁类似
- ③Optimistic reading(乐观读模式):
- 用邮戳来判断其他写线程是否修改过数据
- 无锁机制,类似于数据库中的乐观锁,
支持读写并发, 很乐观认为读取时没人修改,假如被修改再实现升级为悲观读模式 \color{blue}很乐观认为读取时没人修改,假如被修改再实现升级为悲观读模式 很乐观认为读取时没人修改,假如被修改再实现升级为悲观读模式
- ①Reading (读模式悲观):
6.4、code演示
读的过程中也允许获取写锁介入
code
public class StampedLockDemo {
static int number = 37;
static StampedLock stampedLock = new StampedLock();
public void write() {
long stamp = stampedLock.writeLock();
System.out.println(Thread.currentThread().getName() + "\t" + "写线程准备修改");
try {
number = number + 13;
} finally {
stampedLock.unlockWrite(stamp);
}
System.out.println(Thread.currentThread().getName() + "\t" + "写线程结束修改");
}
/**
* 悲观读,读没有完成的时候写锁是无法获得锁
*/
public void read() {
long stamp = stampedLock.readLock();
System.out.println(Thread.currentThread().getName() + "\t" + "come in readLock code block, 4 seconds continue..");
for (int i = 0; i < 4; i++) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("正在读取中....");
}
try {
int res = number;
System.out.println(Thread.currentThread().getName() + "\t" + "获得成员变量值result: " + res);
System.out.println("写现场没有修改成功,读锁时候写锁无法介入,传统的读写互斥");
} finally {
stampedLock.unlock(stamp);
}
}
/**
* 乐观读,读的过程中也允许获取写锁介入,说白了就是不加锁,用邮戳来判断其他写线程是否修改过数据
*/
public void tryOptimisticRead() {
long stamp = stampedLock.tryOptimisticRead();
int result = number;
//故意间隔4秒,很乐观认为读取中没有其他线程修改过number值,具体靠判断
System.out.println("4秒前 stampedLock.validate 方法值(true无修改, false有修改)" + "\t" + stampedLock.validate(stamp));
for (int i = 0; i < 4; i++) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t" + "正在读取..." + i + "s后 " +
"stampedLock.validate 方法值(true无修改, false有修改)" + "\t" + stampedLock.validate(stamp));
}
//若有修改
if (!stampedLock.validate(stamp)) {
System.out.println("有人修改过------有写操作");
stamp = stampedLock.readLock();
try {
System.out.println("从乐观锁 升级为悲观读");
result = number;
System.out.println("重新悲观读后 result:" + result);
} finally {
stampedLock.unlockRead(stamp);
}
}
System.out.println(Thread.currentThread().getName() + "\t" + "finally value: " + result);
}
public static void main(String[] args) {
StampedLockDemo resource = new StampedLockDemo();
/*传统版
new Thread(() -> {
resource.read();
}, "readThread").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t" + "come in");
resource.write();
}, "writeThread").start();
*/
new Thread(() -> {
resource.tryOptimisticRead();
}, "tryOptimisticRead").start();
//暂停2秒钟线程,读过程可以介入, 演示
try {
TimeUnit.SECONDS.sleep(2); //
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t" + "come in");
resource.write();
}, "writeThread").start();
}
}
传统
正在读取中....
writeThread come in
正在读取中....
正在读取中....
正在读取中....
readThread 获得成员变量值result: 37
写现场没有修改成功,读锁时候写锁无法介入,传统的读写互斥
writeThread 写线程准备修改
writeThread 写线程结束修改
-----------------------------------------------
只暂停2秒主线程,就是让一个线程在读的过程中,另一个线程进去写,validate判断为 = false
则从 乐观锁升级为悲观锁(手动加锁)
4秒前 stampedLock.validate 方法值(true无修改, false有修改) true
tryOptimisticRead 正在读取...0s后 stampedLock.validate 方法值(true无修改, false有修改) true
writeThread come in
writeThread 写线程准备修改
writeThread 写线程结束修改
tryOptimisticRead 正在读取...1s后 stampedLock.validate 方法值(true无修改, false有修改) false
tryOptimisticRead 正在读取...2s后 stampedLock.validate 方法值(true无修改, false有修改) false
tryOptimisticRead 正在读取...3s后 stampedLock.validate 方法值(true无修改, false有修改) false
有人修改过------有写操作
从乐观锁 升级为悲观读
重新悲观读后 result:50
tryOptimisticRead finally value: 50
---------------------------------------------
若设置暂停为6秒主线程 ,一个线程读完后,在去让另一个线程写
4秒前 stampedLock.validate 方法值(true无修改, false有修改) true
tryOptimisticRead 正在读取...0s后 stampedLock.validate 方法值(true无修改, false有修改) true
tryOptimisticRead 正在读取...1s后 stampedLock.validate 方法值(true无修改, false有修改) true
tryOptimisticRead 正在读取...2s后 stampedLock.validate 方法值(true无修改, false有修改) true
tryOptimisticRead 正在读取...3s后 stampedLock.validate 方法值(true无修改, false有修改) true
tryOptimisticRead finally value: 37
writeThread come in
writeThread 写线程准备修改
writeThread 写线程结束修改
public class StampedLock implements java.io.Serializable {
//..........
public boolean validate(long stamp) {
U.loadFence();
return (stamp & SBITS) == (state & SBITS);
}
}
7、StampedLock的缺点
-
StampedLock
不支持重入,没有Re开头 -
StampedLock
的悲观读锁和写锁都不支持条件变量(Condition
),这个也需要注意。 -
如果线程阻塞在
StampedLock
的readLock()
或者writeLock()
上时,此时调用该阻塞线程的interrupt()
方法,会导致CPU
飙升。所以,使用 StampedLock 一定不要调用中断操作,如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly() 和写锁 writeLockInterruptibly()。- 若中断的话,当前线阻塞了,其他线程会一直循环,导致
CPU
空转。
- 若中断的话,当前线阻塞了,其他线程会一直循环,导致