赞
踩
本章路线总纲
无锁——>独占锁——>读写锁——>邮戳锁
类图:
读写锁的演变情况:
读写锁说明
演变
读写锁意义和特点
可重入、读写兼顾
结论:一体两面,读写互斥,读读共享,读没有完成的时候其他线程写锁无法获得
ReentrantReadWriteLock的缺点:
1. 锁饥饿问题:
2. 锁降级:
- public class ReentrantReadWriteLockDemo {
- public static void main(String[] args) {
- MyCache cache = new MyCache();
-
- //开启10个线程,写入数据
- for (int i = 1; i <= 10; i++) {
- int finalI = i;
- new Thread(() -> {
- cache.write(finalI + "", finalI + "");
- }, String.valueOf(i)).start();
- }
-
- //开启10个线程,读取数据
- for (int i = 1; i <= 10; i++) {
- int finalI = i;
- new Thread(() -> {
- cache.read(finalI + "");
- }, String.valueOf(i)).start();
- }
- }
- }
-
- //模拟一个缓存资源类,有读写两种功能
- class MyCache {
-
- HashMap<String, String> map = new HashMap<>();
-
- ReentrantLock lock = new ReentrantLock();
-
- //读写都加锁
- public void write(String key, String value) {
- lock.lock();
- try {
- System.out.println(Thread.currentThread().getName() + "线程开始写入数据...");
- //延迟500ms模拟业务耗时,同时可以看出读写不能共同执行 (因为运行结果是先打印一个线程写入,再打印对应线程写入完成)
- TimeUnit.MILLISECONDS.sleep(500);
- map.put(key, value);
- System.out.println(Thread.currentThread().getName() + "线程完成写入数据!");
- } catch (InterruptedException e) {
- e.printStackTrace();
- }finally {
- lock.unlock();
- }
- }
-
- public void read(String key) {
- lock.lock();
- try {
- System.out.println(Thread.currentThread().getName() + "线程开始读取数据...");
- String val = map.get(key);
- TimeUnit.MILLISECONDS.sleep(200);
- System.out.println(Thread.currentThread().getName() + "线程读取到的数据是:\t" + val);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }finally {
- lock.unlock();
- }
- }
- }
- 运行结果:
- 1线程开始写入数据...
- 1线程完成写入数据!
- 2线程开始写入数据...
- 2线程完成写入数据!
- 3线程开始写入数据...
- 3线程完成写入数据!
- 4线程开始写入数据...
- 4线程完成写入数据!
- 5线程开始写入数据...
- 5线程完成写入数据!
- 6线程开始写入数据...
- 6线程完成写入数据!
- 7线程开始写入数据...
- 7线程完成写入数据!
- 9线程开始写入数据...
- 9线程完成写入数据!
- 8线程开始写入数据...
- 8线程完成写入数据!
- 10线程开始写入数据...
- 10线程完成写入数据!
- 1线程开始读取数据...
- 1线程读取到的数据是: 1
- 2线程开始读取数据...
- 2线程读取到的数据是: 2
- 3线程开始读取数据...
- 3线程读取到的数据是: 3
- 4线程开始读取数据...
- 4线程读取到的数据是: 4
- 5线程开始读取数据...
- 5线程读取到的数据是: 5
- 6线程开始读取数据...
- 6线程读取到的数据是: 6
- 7线程开始读取数据...
- 7线程读取到的数据是: 7
- 8线程开始读取数据...
- 8线程读取到的数据是: 8
- 9线程开始读取数据...
- 9线程读取到的数据是: 9
- 10线程开始读取数据...
- 10线程读取到的数据是: 10
说明:可以看出,开始写入/读取和完成写入/读取,都是成对出现的。这说明这写入/读取期间,其他线程不能执行写入/读取。读写/读读/写写都互斥了。
问题:我们希望的情况应该是,读写/写写都互斥,但读读可以并发读取。从而引出了读写锁(对写独占,对读共享)
- public class ReentrantReadWriteLockDemo {
- public static void main(String[] args) {
- MyCache cache = new MyCache();
-
- //开启10个线程,写入数据
- for (int i = 1; i <= 10; i++) {
- int finalI = i;
- new Thread(() -> {
- cache.write(finalI + "", finalI + "");
- }, String.valueOf(i)).start();
- }
-
- //开启10个线程,读取数据
- for (int i = 1; i <= 10; i++) {
- int finalI = i;
- new Thread(() -> {
- cache.read(finalI + "");
- }, String.valueOf(i)).start();
- }
- }
- }
-
- //模拟一个缓存资源类,有读写两种功能
- class MyCache {
-
- HashMap<String, String> map = new HashMap<>();
-
- ReentrantLock lock = new ReentrantLock();
-
- ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
-
- //读写都加锁
- public void write(String key, String value) {
- rwLock.writeLock().lock();
- try {
- System.out.println(Thread.currentThread().getName() + "线程开始写入数据...");
- //延迟500ms模拟业务耗时,同时可以看出读写不能共同执行 (因为运行结果是先打印一个线程写入,再打印对应线程写入完成)
- TimeUnit.MILLISECONDS.sleep(500);
- map.put(key, value);
- System.out.println(Thread.currentThread().getName() + "线程完成写入数据!");
- } catch (InterruptedException e) {
- e.printStackTrace();
- } finally {
- rwLock.writeLock().unlock();
- }
- }
-
- public void read(String key) {
- rwLock.readLock().lock();
- try {
- System.out.println(Thread.currentThread().getName() + "线程开始读取数据...");
- String val = map.get(key);
- TimeUnit.MILLISECONDS.sleep(200);
- System.out.println(Thread.currentThread().getName() + "线程读取到的数据是:\t" + val);
- } catch (InterruptedException e) {
- e.printStackTrace();
- } finally {
- rwLock.readLock().unlock();
- }
- }
- }
- 运行结果:
- 1线程开始写入数据...
- 1线程完成写入数据!
- 2线程开始写入数据...
- 2线程完成写入数据!
- 3线程开始写入数据...
- 3线程完成写入数据!
- 4线程开始写入数据...
- 4线程完成写入数据!
- 5线程开始写入数据...
- 5线程完成写入数据!
- 6线程开始写入数据...
- 6线程完成写入数据!
- 7线程开始写入数据...
- 7线程完成写入数据!
- 8线程开始写入数据...
- 8线程完成写入数据!
- 9线程开始写入数据...
- 9线程完成写入数据!
- 10线程开始写入数据...
- 10线程完成写入数据!
- 1线程开始读取数据...
- 9线程开始读取数据...
- 7线程开始读取数据...
- 6线程开始读取数据...
- 5线程开始读取数据...
- 3线程开始读取数据...
- 4线程开始读取数据...
- 2线程开始读取数据...
- 10线程开始读取数据...
- 8线程开始读取数据...
- 10线程读取到的数据是:10
- 4线程读取到的数据是: 4
- 2线程读取到的数据是: 2
- 8线程读取到的数据是: 8
- 3线程读取到的数据是: 3
- 7线程读取到的数据是: 7
- 6线程读取到的数据是: 6
- 5线程读取到的数据是: 5
- 1线程读取到的数据是: 1
- 9线程读取到的数据是: 9
说明:可以看出,所有写操作还是跟之前一样,全部互斥。但读操作可以并发读取。
结论
使用ReadWriteLock实现读写操作,一体两面,读写互斥,读读共享,但是读没有完成时候其它线程写锁无法获取
ReentrantReadwriteLock锁降级:
ReentrantReadwriteLock的特性:
写锁降级成为读锁
总之:
why?要有这么个特性?
----后面解释,大概目的:锁降级是为了让当前线程感知到数据的变化,目的是保证数据的可见性
重入还允许通过获取写入锁定,然后读取锁然后释放写锁从写锁到读取锁,但是从读锁升级到写锁是不可能的
锁降级的目的:锁降级是为了让当前线程感知到数据的变化,目的是保证数据可见性
样例1
锁降级:获取写锁 ——> 获取读锁 ——> 释放写锁 ——> 释放读锁 ✔ 可以完成
- import java.util.concurrent.locks.ReentrantReadWriteLock;
-
- public class LockDownGradingDemo {
-
- public static void main(String[] args) {
- ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
-
- ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
- ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
-
- // 例一:正常两个A、B线程
- // new Thread(() -> {
- // readLock.lock();
- // System.out.println("---A线程读取---");
- // readLock.unlock();
- // }, "A").start();
- //
- // new Thread(() -> {
- // writeLock.lock();
- // System.out.println("---B线程写入---");
- // writeLock.unlock();
- // }, "B").start();
-
-
- // 例二:only one 同一个线程
- writeLock.lock();
- System.out.println("---写入---");
- // 一些其它的业务操作...
-
- readLock.lock();
- System.out.println("---读取---");
- // 一些其它的业务操作...
-
- writeLock.unlock();
- readLock.unlock();
- }
-
- }
-
- 输出结果:
- ---写入---
- ---读取---
说明:
样例2
锁降级:获取读锁 ——> 获取写锁 ——> 释放读锁 ——> 释放写锁 X 不可以完成
- import java.util.concurrent.locks.ReentrantReadWriteLock;
-
- public class LockDownGradingDemo2 {
-
- public static void main(String[] args) {
- ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
-
- ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
- ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
-
- // 例二:only one 同一个线程
- readLock.lock();
- System.out.println("---读取---");
- // 一些其它的业务操作...
-
- writeLock.lock();
- System.out.println("---写入---");
- // 一些其它的业务操作...
-
- readLock.unlock(); // 这个位置和下面那个位置效果一样
-
- writeLock.unlock();
- // readLock.unlock();
- }
-
- }
-
-
- 输出结果:
- ---读取---
- // ...程序未结束
说明:
1、2例子对比小结:
写锁和读锁是互斥的(这里的互斥是指线程间的互斥,当前线程可以获取到写锁又获取到读锁,但是获取到了读锁不能继续获取写锁),这是因为读写锁要保持写操作的可见性。因为,如果允许读锁在被获取的情况下对写锁的获取,那么正在运行的其他读线程无法感知到当前写线程的操作
因此,分析读写锁ReentrantReadWriteLock,会发现它有个潜在的问题:
- * <p><b>Sample usages</b>. Here is a code sketch showing how to perform
- * lock downgrading after updating a cache (exception handling is
- * particularly tricky when handling multiple locks in a non-nested
- * fashion):
- *
- * <pre> {@code
- * class CachedData {
- * Object data;
- * volatile boolean cacheValid;
- * final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
- *
- * void processCachedData() {
- * rwl.readLock().lock();// 1
- * if (!cacheValid) {
- * // Must release read lock before acquiring write lock
- * rwl.readLock().unlock();// 2
- * rwl.writeLock().lock();// 3
- * try {
- * // Recheck state because another thread might have
- * // acquired write lock and changed state before we did.
- * if (!cacheValid) {
- * data = ...//在此做一些写操作
- * cacheValid = true;
- * }
- * // Downgrade by acquiring read lock before releasing write lock
- * rwl.readLock().lock();// 4
- * } finally {
- * rwl.writeLock().unlock(); // 5 Unlock write, still hold read
- * }
- * }
- *
- * try {
- * use(data);
- * } finally {
- * rwl.readLock().unlock();// 6
- * }
- * }
- * }}</pre>
代码解读:
这里只有锁降级才能保证,同一个线程我先执行写操作,再继续读我刚刚写的数据。在整个线程执行业务的过程中,一直是加锁(不是写锁就是读锁)状态,没有出现空档期,因此整个操作保证了原子性。
如果违背锁降级的步骤,如果违背锁降级的步骤, 如果违背锁降级的步骤
StampedLock是JDK1.8中新增的一个读写锁,也是对JDK1.5中的读写锁ReentrantReadWriteLock的优化
stamp 代表了锁的状态。当stamp返回零时,表示线程获取锁失败,并且当释放锁或者转换锁的时候,都要传入最初获取的stamp值。
锁饥饿问题:
如何解决锁饥饿问题:
- import java.util.concurrent.TimeUnit;
- import java.util.concurrent.locks.StampedLock;
-
- 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 second continue...");
- for (int i = 0; i < 4; i++) {
- try {
- TimeUnit.SECONDS.sleep(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(Thread.currentThread().getName() + "\t" + "正在读取中");
- }
-
- try {
- int result = number;
- System.out.println(Thread.currentThread().getName() + "\t" + "获得成员变量值result: " + result);
- System.out.println("写线程没有修改成功,读锁时候写锁无法介入,传统的读写互斥");
- } finally {
- stampedLock.unlockRead(stamp);
- }
-
- }
-
- 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();
-
- try {
- TimeUnit.SECONDS.sleep(4);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
-
- System.out.println(Thread.currentThread().getName() + "\t" + "number:" + number);
- }
-
- }
-
- 输出结果:
- readThread come in readLock code Block, 4 second continue...
- readThread 正在读取中
- writeThread come in
- readThread 正在读取中
- readThread 正在读取中
- readThread 正在读取中
- readThread 获得成员变量值result: 37
- 写线程没有修改成功,读锁时候写锁无法介入,传统的读写互斥
- writeThread 写线程准备修改
- writeThread 写线程结束修改
- main number:50
这和之前的读写锁ReentrantLock使用类似,但邮戳锁不可重入
- import java.util.concurrent.TimeUnit;
- import java.util.concurrent.locks.StampedLock;
-
- public class StampedLockDemo2 {
-
- 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.tryOptimisticRead();
-
- int result = number;
-
- // public boolean validate(long stamp)方法官方解释
- // 如果自发出给定标记后未完全获取锁,则返回true。如果标记为零,则始终返回false。如果图章代表当前持有的锁,则始终返回true。 使用未从tryOptimisticRead()获取的值或此锁定的锁定方法调用此方法没有定义的效果或结果
- System.out.println("4秒前 stampedLock.validate方法值(true 无修改 false有修改)" + "\t" + stampedLock.validate(stamp));
-
- // 故意间隔4秒钟,很乐观认为读取中没有其它线程修改过number值,具体靠判断
- 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 + "秒后,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) {
- StampedLockDemo2 resource = new StampedLockDemo2();
- new Thread(resource::read, "readThread").start();
-
- // 暂停2秒线程,演示读过程可以写介入
- // 如果暂停>4秒线程,这样读过程没有写介入,此时输出finally value:37
- 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();
- }
-
- }
-
- 输出结果:
- 4秒前 stampedLock.validate方法值(true 无修改 false有修改) true
- readThread 正在读取....0秒后,stampedLock.validate方法值(true 无修改 false有修改) true
- writeThread come in
- writeThread 写线程准备修改
- writeThread 写线程结束修改
- readThread 正在读取....1秒后,stampedLock.validate方法值(true 无修改 false有修改) false
- readThread 正在读取....2秒后,stampedLock.validate方法值(true 无修改 false有修改) false
- readThread 正在读取....3秒后,stampedLock.validate方法值(true 无修改 false有修改) false
- 有人修改----------有写操作
- 从乐观读升级为悲观读
- 重新悲观读后result:50
- readThread finally value: 50
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。