大佬,能给我讲讲什么是读写锁和锁的升级吗?
故事开端
在一个阳光灿烂的早晨,熊某突然从别人的口中听到读写锁和锁的升级这个名词,脑海中第一反应就是——“What‘s this???”,锁还能分读写?锁还能升级?我的天,怎么感觉在升级打怪一样。
于是熊某带着满头的疑虑去请求公司的技术大佬,这位技术大佬很耐心的给我说:”熊兄弟,并发编程没你想得那么简单的,下面让我一一为你道来。”于是,担起小板凳,拿着扇子马上开讲!
何为“读写锁”?
读写锁是一个很多地方都使用的技术,基本上实现读写锁都要遵循三个原则:
- 允许多个线程同时读一个共享变量
- 只能有一个线程对共享变量执行写操作
- 当一个线程执行写操作的时候其它线程不能读共享变量
从以上三个原则可以得出,写锁和读锁是互斥的,写的时候不能读,写只能一个写,读可以多个读。
为什么要用读锁
这时候熊某略为不解,就问技术老大:“既然读锁能够运行多个线程访问共享变量,那直接不要锁就好了,为什么还要那么麻烦弄个读锁?“
技术老大耐心讲解:”熊兄弟这问题提的不错,为什么需要读锁?上面讲述的三个原则的第三点是答案问题的。’当一个线程执行写操作的时候其它线程不能读共享变量‘,就是说当有线程对共享变量进行更新的时候,其它线程是不能进行读操作的,只有当写操作完成后才能进行读,确保读到的数据是最新的。如果不加读锁,当数据更新的时候有其它线程还在读,就会出现读到的不是最新数据的情况“。
熊某:“哦!!!!”
读写锁的实现
“说了这么多,能给我看看是怎么实现的吗?”,熊某说到。
“没问题,上代码!”,技术大佬道
public class ReadWriteTest{
private Map<String, Object> map = new HashMap<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.writeLock();
Object read(String key){
readLock.lock();
System.out.println("-------获得读锁");
try {
return map.get(key);
}finally {
System.out.println("-------释放读锁");
readLock.unlock();
}
}
void write(String key, Object value){
writeLock.lock();
System.out.println("-------获得写锁");
try {
map.put(key, value);
}finally {
System.out.println("-------释放写锁");
writeLock.unlock();
}
}
public static void main(String[] args) {
ReadWriteTest readWriteTest = new ReadWriteTest();
readWriteTest.write("test", "熊小哥好帅!");
System.out.println(readWriteTest.read("test"));
}
}
输出结果是:
-———获得写锁
-———释放写锁
-———获得读锁
-———释放读锁
熊小哥好帅!
代码这么看的话看不出什么,既然是并发编程就要加多几个线程来测试啦,下面是改良版:
public class ReadWriteTest {
private Map<String, Object> map = new HashMap<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.writeLock();
Object read(String key) {
System.out.println(Thread.currentThread().getName() + "-------尝试获得读锁");
while (!readLock.tryLock())
System.out.println(Thread.currentThread().getName() + "-------获得读锁失败");
if (readLock.tryLock()) {
System.out.println(Thread.currentThread().getName() + "-------获得读锁成功");
try {
return map.get(key);
} finally {
System.out.println(Thread.currentThread().getName() + "-------释放读锁");
readLock.unlock();
}
}
return null;
}
void write(String key, Object value) {
System.out.println(Thread.currentThread().getName() + "-------尝试获得写锁");
if (writeLock.tryLock()) {
System.out.println(Thread.currentThread().getName() + "-------获得写锁成功");
try {
map.put(key, value);
} finally {
System.out.println(Thread.currentThread().getName() + "-------释放写锁");
writeLock.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + "-------获得写锁失败");
}
}
public static void main(String[] args) throws InterruptedException {
ReadWriteTest readWriteTest = new ReadWriteTest();
Thread thread1 = new Thread(
() -> {
readWriteTest.write("test", "熊小哥好帅");
});
Thread thread2 = new Thread(
() -> {
readWriteTest.read("test");
});
Thread thread3 = new Thread(
() -> {
readWriteTest.write("test", "熊小哥好帅");
});
Thread thread4 = new Thread(
() -> {
readWriteTest.read("test");
});
thread1.setName("线程A");
thread2.setName("线程B");
thread3.setName("线程C");
thread4.setName("线程D");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
这边为了得到加锁释放成功的结果,就用了tryLock(),运行结果为:
线程A———-尝试获得写锁
线程D———-尝试获得读锁
线程C———-尝试获得写锁
线程C———-获得写锁失败
线程B———-尝试获得读锁
。。。。。。。。省略了无数次的获得读锁失败
线程A———-释放写锁
线程D———-获得读锁失败
线程B———-获得读锁失败
线程D———-获得读锁成功
线程D———-释放读锁
线程B———-获得读锁成功
线程B———-释放读锁
线程AC负责获取写锁,线程BD负责获取读锁,看结果可以知道,当线程A获得写锁后,线程BD是不能获取读锁的,线程C也不能获取写锁。所以后面线程BD获取的数据是线程A修改后的数据。接下来我们看看多个线程能不能同时获取读锁,稍微改改测试代码:
public static void main(String[] args) throws InterruptedException {
ReadWriteTest readWriteTest = new ReadWriteTest();
Thread thread2 = new Thread(
() -> {
readWriteTest.read("test");
});
Thread thread3 = new Thread(
() -> {
readWriteTest.read("test");
});
Thread thread4 = new Thread(
() -> {
readWriteTest.read("test");
});
thread2.setName("线程B");
thread3.setName("线程C");
thread4.setName("线程D");
thread2.start();
thread3.start();
thread4.start();
}
看看结果:
线程C———-尝试获得读锁
线程D———-尝试获得读锁
线程B———-尝试获得读锁
线程C———-获得读锁成功
线程C———-释放读锁
线程D———-获得读锁成功
线程B———-获得读锁成功
线程B———-释放读锁
线程D———-释放读锁
可以看到,多个线程可以同时获得读锁来访问变量。
值得注意的是,读写锁也是可重入锁。
锁的升级
“读写锁的使用我基本明白,但是锁的升级呢?”
“熊兄弟莫要心急,本大佬马上给你解释。”
常说的锁升级,其实指的是从读锁升级为写锁,我们用上面的代码说下,这边改造下read()方法,为了方便,删除多余的代码:
void read(String key) {
// 这里上读锁
if (readLock.tryLock()) {
try {
if(Objects.isNull(map.get(key))){
try{
// 这里上写锁
writeLock.lock();
map.put(key, "123");
}finally {
// 这里释放写锁
writeLock.unlock();
}
}
map.get(key);
} finally {
// 这里释放读锁
readLock.unlock();
}
}
}
执行一下,发现程序永久没执行完。这是咋回事?
正当熊某有这个问题时,技术大佬心领神会的说出了答案,“读写锁(ReadWriteLock)是不支持锁的升级,因为当读锁不释放的时候,获取写锁的线程只能永久等待,虽然不支持锁的升级,但是锁的降级还是支持的(降级指的是从写锁转换成读锁),你看看代码”:
void read(String key) {
// 这里上读锁
if (readLock.tryLock()) {
if(Objects.isNull(map.get(key))){
// 发现为空释放读锁,然后上写锁
readLock.unlock();
writeLock.lock();
// 再检测下是否有数据,避免重复存放数据,耗费资源
try{
if(Objects.isNull(map.get(key))){
map.put(key, "熊小哥好帅");
}
// 这里降级为读锁
readLock.lock();
}finally {
writeLock.unlock();
}
// 获取数据并释放读锁
System.out.println(map.get(key));
readLock.unlock();
}
}
}
执行一下测试代码:
public static void main(String[] args) throws InterruptedException {
ReadWriteTest readWriteTest = new ReadWriteTest();
Thread thread2 = new Thread(
() -> {
readWriteTest.read("test");
});
thread2.setName("线程B");
thread2.start();
}
看结果为:熊小哥好帅
结尾
“oh,我都懂了,我应该要怎么感谢技术大佬您呢?”熊某兴奋地叫道。
“今晚带我去吃海底捞!!”,技术大佬不客气的喊道。
“好吧好吧!”
最后说一句,各位喜欢的话可以点个赞哦。
还没有评论,来说两句吧...