文章目录
- 1、读写锁
- 2、读写锁的体验
- 3、读写锁的特点
- 4、锁的演变
- 5、读写锁的降级
- 6、复习:悲观锁和乐观锁
1、读写锁
JUC下的锁包的ReadWriteLock接口,以及其实现类ReentrantReadWriteLock
- ReadWriteLock 维护了一对相关的锁,即读锁和写锁,使得并发和吞吐相比一般的排他锁有了很大提升
- 读锁属于共享锁
- 写锁属于独占锁
- 相比前面的ReentrantLock适用于一般场合,ReadWriteLock 适用于读多写少的场景
关于ReadWriteLock接口的两个方法:
- 返回用于读的锁
Lock readLock()
- 返回用于写的锁
Lock writeLock()
2、读写锁的体验
先看没有读写锁时,开多个线程对同一个资源进行读和写:
/资源类
class MyCache{
//map模拟redis
private volatile Map<String,Object> map = new HashMap<>();
//写
public void put(String key,Object value){
System.out.println(Thread.currentThread().getName() + "线程正在进行写操作==>" + key);
//暂停一会儿
try {
TimeUnit.MICROSECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
//放数据
map.put(key,value);
System.out.println(Thread.currentThread().getName() + "线程写完了==>" + key);
}
//取
public Object get(String key){
Object result = null;
System.out.println(Thread.currentThread().getName() + "线程正在进行读操作-->" + key);
//暂停一会儿
try {
TimeUnit.MICROSECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
result = map.get(key);
System.out.println(Thread.currentThread().getName() + "线程读完了-->" + key);
return result;
}
}
创建5个线程来读,5个线程来写:
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
//创建5个线程来写数据
for (int i = 1; i < 6; i++) {
final int num = i; //临时变量,直接put一个变量i报错
new Thread(() -> {
myCache.put(num +"",num+"");
},String.valueOf(i)).start();
}
//创建5个线程来读数据
for (int i = 1; i < 6; i++) {
final int num = i;
new Thread(() -> {
myCache.get(num +"");
},String.valueOf(i)).start();
}
}
}
运行发现:没写完就开始读,此时肯定读不到
加入读锁和写锁:
//资源类
class MyCache{
//map模拟redis
private volatile Map<String,Object> map = new HashMap<>();
//创建读写锁的对象
private ReadWriteLock rwLock = new ReentrantReadWriteLock();
//写
public void put(String key,Object value){
//添加写锁
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "线程正在进行写操作==>" + key);
//暂停一会儿,别瞬间写完
TimeUnit.MICROSECONDS.sleep(300);
//放数据
map.put(key,value);
System.out.println(Thread.currentThread().getName() + "线程写完了==>" + key);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放写锁
rwLock.writeLock().unlock();
}
}
//取
public Object get(String key){
//添加读锁
rwLock.readLock().lock();
Object result = null;
try {
System.out.println(Thread.currentThread().getName() + "线程正在进行读操作-->" + key);
//暂停一会儿,别读完太快,以证明读锁确实可以共享
TimeUnit.MICROSECONDS.sleep(300);
result = map.get(key);
System.out.println(Thread.currentThread().getName() + "线程读完了-->" + key);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
rwLock.readLock().unlock();
}
return result;
}
}
和上面一样,再执行main:创建5个线程来读,5个线程来写:
3、读写锁的特点
- 读读共享:允许多个线程同时对同一个资源进行读,不用等前面的线程释放读锁,后面的线程就能获取到读锁,并执行加了读锁的代码
- 读写互斥:一个线程获取了读锁,未释放前,不允许另一个线程同时来获取写锁进行写操作
- 写写互斥:不允许多个线程对同一个资源进行写,必须等到前面线程释放写的锁,后面的线程才能获取到写锁并执行加了写锁的代码
写个demo开两个线程去获取读锁和写锁,调试验证下,这里两个线程都不释放自己获取到的读锁或者写锁:
public class ReadWriteDemo2 {
public static void main(String[] args) {
//可重入读写锁
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
//读锁
ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
//写锁
ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
//获取读锁
readLock.lock();
System.out.println("reading....");
new Thread(() -> {
//另一线程获取写锁
writeLock.lock();
System.out.println("write....");
}).start();
//释放写锁
//writeLock.unlock();
//释放读锁
//readLock.unlock();
}
}
前面线程先获取写锁,另一线程去获取读锁:
public class ReadWriteDemo2 {
public static void main(String[] args) {
//可重入读写锁
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
//读锁
ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
//写锁
ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
//获取写锁
writeLock.lock();
System.out.println("write....");
new Thread(() -> {
//另一线程获取读锁
readLock.lock();
System.out.println("reading....");
}).start();
//释放写锁
//writeLock.unlock();
//释放读锁
//readLock.unlock();
}
}
同时获取读锁:
public class ReadWriteDemo2 {
public static void main(String[] args) {
//可重入读写锁
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
//读锁
ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
//写锁
ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
//获取读锁
readLock.lock();
System.out.println("reading....");
new Thread(() -> {
//另一线程也获取读锁
readLock.lock();
System.out.println("reading....");
}).start();
//释放写锁
//writeLock.unlock();
//释放读锁
//readLock.unlock();
}
}
同时获取写锁:
public class ReadWriteDemo2 {
public static void main(String[] args) {
//可重入读写锁
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
//读锁
ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
//写锁
ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
//获取写锁
writeLock.lock();
System.out.println("writing....");
new Thread(() -> {
//另一线程也获取读锁
writeLock.lock();
System.out.println("writing....");
}).start();
//释放写锁
//writeLock.unlock();
//释放读锁
//readLock.unlock();
}
}
4、锁的演变
无锁 ⇒ 独占锁 ⇒ 读写锁
5、读写锁的降级
前面提到,不同线程下,读读共享,读写互斥,写写互斥。
而同一线程中,在持有写锁未解锁的情况下,可以获取读锁。按照如下步骤:
在同一个线程中,写锁就被过渡降级到了读锁,读写锁的降级,其目的是为了解决,持有写锁时,其他线程无法获得读锁,影响性能。
public class ReadWriteDemo {
public static void main(String[] args) {
//可重入读写锁
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
//读锁
ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
//写锁
ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
//锁降级
//1.获取写锁
writeLock.lock();
System.out.println("write....");
//2.获取读锁
readLock.lock();
System.out.println("reading....");
//3.释放写锁
writeLock.unlock();
//4.释放读锁
readLock.unlock();
}
}
如果写锁被释放时,执行读锁的线程非常多,而需要执行写锁的线程非常少,则会导致读锁一直被使用不被释放,从而造成写线程无法获取写锁,造成写线程一直等待获取,造成线程“饥饿”。这个就像某一站地铁,上来100个人,下1个人,结果车一停(类比锁一降级),100个人往进涌(类比其他线程可以获取读锁了),把地铁门堵到发车(好多线程,读锁半天没有全部释放完),导致这一个下车的人也愣是没下去(类比少数其他想获取写锁的线程半天获取不到,因为不同线程,读写互斥)。
补充:
可能会有个疑问,既然释放写锁,干嘛非要手里纂一个读锁后才释放写锁,为何不:
持有写锁 -> 释放写锁 -> 持有读锁 -> 释放读锁
这样的坑在于,你释放完写锁,被另一线程T拿到并写了些数据,等你再拿到读锁时(不是你一释放写锁就一定能给自己拿到读锁,不同线程,读写互斥!),读到的已经是被修改了N手的数据。降级是,你什么时候想要读锁,你就什么时候获取读锁(因为写锁在你手里,你主动的),而如果你先释放写锁,想再获取读锁,那就不是想要就能立马拿到的了。
6、复习:悲观锁和乐观锁
最后,梳理下其他相关的锁。
悲观锁
心态悲观,认为每次操作都会去修改数据,因此,次次操作前都上锁,即共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。如Java中的synchronized、ReentrantLock就属于悲观锁的范畴(独占锁)。
乐观锁
总是假设好的方向,即认为每次操作都不会修改数据,因此也不上锁,只是在更新时会去判断一下有没人在这期间更新过这个数据,这个"判断",可以使用版本号机制或者CAS算法。版本号即多维护个字段:
# version=version+1
# where xx=#{xx} and version=#{version}
update table set x=x+1, version=version+1 where id=#{id} and version=#{version};
CAS算法,CAS 即compare and swap,比较与交换,是一种无锁算法,实现了不用锁的情况下进行多线程变量同步,也称非阻塞同步。其实现思路是一种自旋的思想,即不断的重试(这同时也是乐观锁的一个缺点,长时间不成功并重试CPU开销变大)。CAS算法的三个数:
- 需要读写的内存值 V
- 进行比较的值 A
- 拟写入的新值 B
当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。
ABA问题:
如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。
根据悲观锁与乐观锁的特点,可以知道:
- 悲观锁适用于多写少读的场景
- 乐观锁适用于多读少写的场景