一. 概述
锁的演变过程
无锁->独占锁->读写锁->邮戳锁
1. 无锁:多线程抢占,数据混乱,无序
2. ReentrantLock、synchronized:有序,数据一致性,每次只能来一个线程,不管什么操作
缺点:读操作效率低
3. ReentrantReadWriteLock:读写互斥,读读可以共享,提升大面积的共享性能,同时多人读。
缺点:
1. 锁饥饿问题,写线程获得不到锁
2. 读的过程中,如果没有释放,写线程不可以获得锁;必须读完后,才能有机会写
4. StampedLock:读的过程中,也允许写
二. ReentrantLock
独占锁,读写都是一致性,每次only one一个,程序正确,但是每次只有一个读,读效率不高
代码示例
读写分离,写写分离,读读分离,每次一个线程执行完后,才能执行下一个线程。
class MyResource01{
Map<String,String> map = new HashMap<>();
//独占锁,读写都是一致性,每次only one一个,程序正确,但是每次只有一个读,读效率不高
Lock lock = new ReentrantLock();
public void write(String key,String value){
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+" 正在写");
map.put(key, value);
TimeUnit.MILLISECONDS.sleep(500);
System.out.println(Thread.currentThread().getName()+" 完成写");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void read(String key){
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+" 正在读");
String res = map.get(key);
System.out.println(Thread.currentThread().getName()+" 完成读,res: "+res);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class RwLockDemo01 {
public static void main(String[] args) {
MyResource01 myResource = new MyResource01();
for (int i = 0; i < 10; i++) {
String k = i+"";
new Thread(()->{
myResource.write(k,k);
},String.valueOf(i)).start();
}
for (int i = 0; i < 10; i++) {
String k = i+"";
new Thread(()->{
myResource.read(k);
},String.valueOf(i)).start();
}
}
}
三. ReentrantReadWriteLock
1. 读写锁的定义
读写锁定义为一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程。
2. 读写锁的特点
ReentrantReadWriteLock并不是真正意义上的读写分离,它只允许读读共存,而读写和写写依然是互斥的。大多实际场景是“读/读”线程间并不存在互斥关系,只有"读/写"线程或"写/写"线程间的操作需要互斥的,因此引入ReentrantReadWriteLock。
一个ReentrantReadWriteLock同时只能存在一个写锁但是可以存在多个读锁,但不能同时存在写锁和读锁。也即一个资源可以被多个读操作访问或一个写操作访问,但两者不能同时进行。
只有在读多写少情境之下,读写锁才具有较高的性能体现。
3. 代码示例
读写分离,写写分离,读读共存。如下面所示,线程2读的过程中,允许3,5,8共读
class MyResource02 {
Map<String, String> map = new HashMap<>();
//读写锁,允许并发读
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
public void write(String key, String value) {
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " 正在写");
map.put(key, value);
TimeUnit.MILLISECONDS.sleep(500);
System.out.println(Thread.currentThread().getName() + " 完成写");
} catch (Exception e) {
e.printStackTrace();
} finally {
rwLock.writeLock().unlock();
}
}
public void read(String key) {
rwLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " 正在读");
String res = map.get(key);
System.out.println(Thread.currentThread().getName() + " 完成读,res: " + res);
} catch (Exception e) {
e.printStackTrace();
} finally {
rwLock.readLock().unlock();
}
}
}
public class RwLockDemo02 {
public static void main(String[] args) {
MyResource02 myResource = new MyResource02();
for (int i = 0; i < 10; i++) {
String k = i + "";
new Thread(() -> {
myResource.write(k, k);
}, String.valueOf(i)).start();
}
for (int i = 0; i < 10; i++) {
String k = i + "";
new Thread(() -> {
myResource.read(k);
}, String.valueOf(i)).start();
}
}
}
4. 锁降级(写锁->读锁)
锁的严苛程度变强叫做升级,反之叫做降级
锁降级的规则:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁。
Java8官网说明,重入还允许通过获取写入锁定,然后读取锁然后释放写锁。从写锁降为读取锁, 但是,从读锁定升级到写锁是不可能的。
锁降级的目的:锁降级是为了让当前线程感知到数据的变化,目的是保证数据可见性(写后立即可以读,把写锁降为读锁,保证其他写进程无法进来写,没有读完,写不进去)
不可升级:在ReentrantReadWriteLock中,当读锁被使用时,如果有线程尝试获取写锁,该写线程会被阻塞。所以,需要释放所有读锁,才可获取写锁
代码示例
先获取写锁,再获取读锁,此时写锁降为了读锁
public class LockDownGradingDemo {
public static void main(String[] args) {
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
//有且只有一个线程main,来验证锁降级策略要求
writeLock.lock();
System.out.println("-------write");
readLock.lock();
System.out.println("-------read");
writeLock.unlock();
readLock.unlock();
}
}
反过来,读后写,如果有线程在读,那么写线程是无法获取写锁的,是悲观锁的策略。在ReentrantReadWriteLock中,当读锁被使用时,如果有线程尝试获取写锁,该写线程会被阻塞。所以,需要释放所有读锁,才可获取写锁。
public class LockDownGradingDemo {
public static void main(String[] args) {
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
//反过来,读后写
//如果有线程在读,那么写线程是无法获取写锁的,是悲观锁的策略
//在ReentrantReadWriteLock中,当读锁被使用时,如果有线程尝试获取写锁,该写线程会被阻塞。所以,需要释放所有读锁,才可获取写锁,
readLock.lock();
System.out.println("-------read");
writeLock.lock();
System.out.println("-------write");
readLock.unlock();
writeLock.unlock();
}
}
5. 写锁和读锁互斥
这里的互斥是指线程间的互斥,当前线程可以获取到写锁又获取到读锁,但是获取到了读锁不能继续获取写锁,这是因为读写锁要保持写操作的可见性。 因为,如果允许读锁在被获取的情况下对写锁的获取,那么正在运行的其他读线程无法感知到当前写线程的操作。
如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即ReadWriteLock读的过程中不允许写,只有等待线程都释放了读锁,当前线程才能获取写锁,也就是写入必须等待,这是一种悲观的读锁。
6. Oracle公司ReentrantWriteReadLock源码总结
1. 代码中声明了一个volatile类型的cacheValid变量,保证其可见性。
2. 首先获取读锁,如果cache不可用,则释放读锁,获取写锁,在更改数据之前,再检查一次cacheValid的值,然后修改数据,将cacheValid置为true,然后在释放写锁前获取读锁;此时,cache中数据可用,处理cache中数据,最后释放读锁。这个过程就是一个完整的锁降级的过程,目的是保证数据可见性。
如果违背锁降级的步骤
如果当前的线程C在修改完cache中的数据后,没有获取读锁而是直接释放了写锁,那么假设此时另一个线程D获取了写锁并修改了数据,那么C线程无法感知到数据已被修改,则数据出现错误。
如果遵循锁降级的步骤
线程C在释放写锁之前获取读锁,那么线程D在获取写锁时将被阻塞,直到线程C完成数据处理过程,释放读锁。这样可以保证返回的数据是这次更新的数据,该机制是专门为了缓存设计的。
四. StampedLock
1. 有没有比读写锁更快的锁?
StampedLock
2. 解决锁饥饿问题:
1.使用“公平"策略可以一定程度上缓解这个问题,new ReentrantReadWriteLock(true);但是“公平”策略是以牺牲系统吞吐量为代价的
2.StampedLock
3. StampedLock的定义
StampedLock是JDK1.8中新增的一个读写锁,也是对JDK1.5中的读写锁ReentrantReadWriteLock的优化,邮戳锁也叫票据锁。
stamp (戳记,long类型) 代表了锁的状态。当stamp返回零时,表示线程获取锁失败。并且,当释放锁或者转换锁的时候,都要传入最初获取的stamp值。
所有获取锁的方法,都返回一个邮戳(Stamp) ,Stamp为零表示获取失败,其余都表示成功
所有释放锁的方法,都需要一个邮戳(Stamp) ,这个Stamp必须是和成功获取锁时得到的Stamp一致。
StampedLock是不可重入的,危险(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)
4. StampedLock的改进
StampedLock改进之处在于读的过程中也允许获取写锁介入,这样会导致我们读的数据就可能不一致!所以,需要额外的方法来判断读的过程中是否有写入,这是一种乐观的读锁 ,显然乐观锁的并发效率更高,但一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行。
ReentrantReadWriteLock的读锁被占用的时候,其他线程尝试获取写锁的时候会被阻塞。
但是,StampedLock采取乐观获取锁后,其他线程尝试获取写锁时不会被阻塞,这其实是对读锁的优化,所以,在获取乐观读锁后,还需要对结果进行校验。
5. StampedLock的三种访问模式
①Reading (读模式) :功能和ReentrantReadWriteLock的读锁类似
②Writing (写模式) : 功能和ReentrantReadWriteLock的写锁类似
③Optimistic reading (乐观读模式) :无锁机制,类似于数据库中的乐观锁,支持读写并发,很乐观认为读取时没人修改,假如被修改再实现升级为悲观读模式
6. 代码示例
定义写方法,悲观读和乐观读,比较并分析悲观读、乐观读成功,乐观读失败的情况。
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()+" -----写线程准备");
try {
number = number + 13;
} catch (Exception e) {
e.printStackTrace();
} finally {
stampedLock.unlockWrite(stamp);
}
System.out.println(Thread.currentThread().getName()+" -----写线程完毕");
}
//悲观读
public void read(){
long stamp = stampedLock.readLock();
System.out.println(Thread.currentThread().getName()+" 读线程进入,读取4秒");
for (int i = 0; i < 4; i++) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-------正在读取");
}
try {
int res = number;
System.out.println(Thread.currentThread().getName()+" 获取成员变量值res:"+res);
System.out.println("写线程没有修改值,因stampedLock.readLock()读的时候,不可以写,读写互斥");
} catch (Exception e) {
e.printStackTrace();
} finally {
stampedLock.unlockRead(stamp);
}
}
//乐观读
public void optimisticRead(){
long stamp = stampedLock.tryOptimisticRead();
//先把数据获取一次
int res = number;
//间隔4秒,很乐观的认为没有其他线程修改过number值,但实际情况靠判断。
System.out.println("4秒前,stampedLock.validate值(ture无修改,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()+"-------正在读取,"+i+"秒后stampedLock.validate值(ture无修改,false有修改)"+"\t"+stampedLock.validate(stamp));
}
if(!stampedLock.validate(stamp)){
System.out.println("有人动过——存在写操作");
//从乐观读切换到普通的悲观读
stamp = stampedLock.readLock();
try {
System.out.println("从乐观读升级为悲观读,并重新获取数据");
//重新获取数据
res = number;
System.out.println("悲观读锁通过获取到的成员变量值res:"+res);
} catch (Exception e) {
e.printStackTrace();
}finally {
stampedLock.unlockRead(stamp);
}
}
System.out.println(Thread.currentThread().getName()+" 最终获取res:"+res);
}
}
悲观读,与ReentrantReadWriteLock一致,读完才能写
public static void main(String[] args) {
StampedLockDemo resource = new StampedLockDemo();
//1.悲观读,与ReentrantReadWriteLock一致,读完才能写
new Thread(()->{
resource.read();
},"read").start();
new Thread(()->{
resource.write();
},"write").start();
}
乐观读,成功,其他线程尝试获取写锁时不会被阻塞,对读锁优化
public static void main(String[] args) {
StampedLockDemo resource = new StampedLockDemo();
//2. 乐观读,成功
new Thread(()->{
resource.optimisticRead();
},"optimisticRead").start();
//6秒后,tryOptimisticRead()成功
try {
TimeUnit.SECONDS.sleep(6);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
resource.write();
},"write").start();
}
乐观读,失败,转为悲观读,重新读一次。读的过程中也允许获取写锁介入
public static void main(String[] args) {
StampedLockDemo resource = new StampedLockDemo();
//乐观读,失败,转为悲观读,重新读一次
new Thread(()->{
resource.optimisticRead();
},"optimisticRead").start();
//2秒后,写线程介入
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
resource.write();
},"write").start();
}
7. StampedLock的缺点
1. StampedLock不支持重入,没有Re开头
2. StampedLock的悲观读锁和写锁都不支持条件变量(Condition),这个也需要注意。
3. 使用StampedLock一定不要调用中断操作,即不要调用interrupt()方法