多线程一直Java开发中的难点,也是面试中的常客,趁着还有时间,打算巩固一下JUC方面知识,我想机会随处可见,但始终都是留给有准备的人的,希望我们都能加油!!!
沉下去,再浮上来
,我想我们会变的不一样的。
一个非常喜欢的女孩子拍的照片
作者:次辣条吗
我们开发中应该能够遇到这样的一种情况,对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是当一个写者线程在写这些共享资源时,就不允许其他线程进行访问。
针对这种场景,Java的并发包下提供了读写锁 ReadWriteLock
(接口) | ReentrantReadWriteLock
(实现类)。
读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。我们将读操作相关的锁,称为读锁,因为可以共享读,我们也称为“共享锁”,将写操作相关的锁,称为写锁、排他锁、独占锁。每次可以多个线程的读者进行读访问,但是一次只能由一个写者线程进行写操作,即写操作是独占式的。
读写锁适合于对数据结构的读次数比写次数多得多的情况. 因为, 读模式锁定时可以共享, 以写模式锁住时意味着独占, 所以读写锁又叫共享-独占锁。
public interface ReadWriteLock {
// 读锁
Lock readLock();
// 写锁
Lock writeLock();
}
ReentrantReadWriteLock这个得自己去看哈,这里给出一个整体架构哈😁。
public class ReentrantReadWriteLock implements ReadWriteLock,
java.io.Serializable {
/** 读锁 */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** 写锁 */
private final ReentrantReadWriteLock.WriteLock writerLock;
final Sync sync;
/** 使用默认(非公平)的排序属性创建一个新的
ReentrantReadWriteLock */
public ReentrantReadWriteLock() {
this(false);
}
/** 使用给定的公平策略创建一个新的 ReentrantReadWriteLock */
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
/** 返回用于写入操作的锁 */
public ReentrantReadWriteLock.WriteLock writeLock() { return
writerLock; }
/** 返回用于读取操作的锁 */
public ReentrantReadWriteLock.ReadLock readLock() { return
readerLock; }
abstract static class Sync extends AbstractQueuedSynchronizer {}
static final class NonfairSync extends Sync {}
static final class FairSync extends Sync {}
public static class ReadLock implements Lock, java.io.Serializable {}
public static class WriteLock implements Lock, java.io.Serializable {}
}
🛫公平选择性:
🛬可重入
读锁和写锁都支持线程重进入。但是写锁可以获得读锁,读锁不能获得写锁。因为读锁是共享的,写锁是独占式的。
💺锁降级
遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为 读锁。
🚤支持中断锁的获取
在读锁和写锁的获取过程中支持中断
🛸监控
提供一些辅助方法,例如hasQueuedThreads
方法查询是否有线程正在等待获取读锁或写锁、isWriteLocked
方法查询写锁是否被任何线程持有等等
一个特别简单的案例哈。
场景: 使用 ReentrantReadWriteLock 对一个 hashmap 进行读和写操作
package com.crush.juc06;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
//资源类
class ReentrantReadWriteLockDemo{
//创建 map 集合
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;
}
public static void main(String[] args) {
ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();
for (int i = 1; i <= 5; i++) {
final int number = i;
new Thread(() -> {
demo.put(String.valueOf(number), number);
}, String.valueOf(i)).start();
}
for (int i = 1; i <= 5; i++) {
final int number = i;
new Thread(() -> {
demo.get(String.valueOf(number));
}, String.valueOf(i)).start();
}
}
}
/**
5正在进行写操作5
5写完了5
4正在进行写操作4
4写完了4
3正在进行写操作3
3写完了3
2正在进行写操作2
2写完了2
1正在进行写操作1
1写完了1
1正在取数据1
4正在取数据4
3正在取数据3
5正在取数据5
2正在取数据2
1取完数据了1
4取完数据了4
2取完数据了2
5取完数据了5
3取完数据了3
*/
写是唯一的,而读的时候是共享的。
ReentrantReadWriteLock
和Synchonized、ReentrantLock
比较起来有哪些区别呢?或者有哪些优势呢?
当然ReentrantReadWriteLock优势是有,但是也存在一些缺陷,容易造成锁饥饿,因为如果是读线程先拿到锁的话,并且后续有很多读线程,但只有一个写线程,很有可能这个写线程拿不到锁,它可能要等到所有读线程读完才能进入,就可能会造成一种一直读,没有写的现象。
锁降级的意思就是写锁降级为读锁。而读锁是不可以升级为写锁的。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程,最后释放读锁的过程。
编程模型:
简单的代码:
/**
* @Author: crush
* @Date: 2021-08-21 9:04
* version 1.0
*/
public class ReadWriteLockDemo2 {
public static void main(String[] args) {
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
// 获取读锁
ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
// 获取写锁
ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
//1、 获取到写锁
writeLock.lock();
System.out.println("获取到了写锁");
//2、 继续获取到写锁
readLock.lock();
System.out.println("继续获取到读锁");
//3、释放写锁
writeLock.unlock();
//4、 释放读锁
readLock.unlock();
}
}
/**
* 获取到了写锁
* 继续获取到读锁
*/
也许大家觉得看不出什么,但是如果将获取读锁那一行代码调到获取写锁上方去,可能结果就完全不一样拉。
public class ReadWriteLockDemo2 {
public static void main(String[] args) {
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
// 获取读锁
ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
// 获取写锁
ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
//1、 获取到读锁
readLock.lock();
System.out.println("获取到了读锁");
writeLock.lock();
System.out.println("继续获取到写锁");
writeLock.unlock();
readLock.unlock();
// 释放写锁
}
}
为什么会出现上面这一幕呢?
上面就一普通案例,看完确实会有点迷,这只是做个简单证明,下面才是正文哈。😁
对于数据比较敏感, 需要在对数据修改以后, 获取到修改后的值, 并进行接下来的其它操作
我们来看个比较实在的案例:
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class CacheDemo {
/**
* 缓存器,这里假设需要存储1000左右个缓存对象,按照默认的负载因子0.75,则容量=750,大概估计每一个节点链表长度为5个
* 那么数组长度大概为:150,又有雨设置map大小一般为2的指数,则最近的数字为:128
*/
private Map<String, Object> map = new HashMap<>(128);
private ReadWriteLock rwl = new ReentrantReadWriteLock();
private Lock writeLock=rwl.writeLock();
private Lock readLock=rwl.readLock();
public static void main(String[] args) {
}
public Object get(String id) {
Object value = null;
readLock.lock();//首先开启读锁,从缓存中去取
try {
//如果缓存中没有 释放读锁,上写锁
if (map.get(id) == null) {
readLock.unlock();
writeLock.lock();
try {
//防止多写线程重复查询赋值
if (value == null) {
//此时可以去数据库中查找,这里简单的模拟一下
value = "redis-value";
}
//加读锁降级写锁,不明白的可以查看上面锁降级的原理与保持读取数据原子性的讲解
readLock.lock();
} finally {
//释放写锁
writeLock.unlock();
}
}
} finally {
//最后释放读锁
readLock.unlock();
}
return value;
}
}
如果不使用锁降级功能,如先释放写锁,然后获得读锁,在这个获取读锁的过程中,可能会有其他线程竞争到写锁 或者是更新数据 则获得的数据是其他线程更新的数据,可能会造成数据的污染,即产生脏读的问题。
锁降级中读锁的获取是否必要呢?
答案是必要的。主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁, 假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。
最近又开始了JUC的学习,感觉Java内容真的很多,但是为了能够走的更远,还是觉得应该需要打牢一下基础。
最近在持续更新中,如果你觉得对你有所帮助,也感兴趣的话,关注我吧,让我们一起学习,一起讨论吧。
你好,我是博主宁在春
,Java学习路上的一颗小小的种子,也希望有一天能扎根长成苍天大树。
希望与君共勉
😁
我们:待别时相见时,都已有所成。
参考: