Java中的ReentrantReadWriteLock实现原理详解
作者:java架构师-太阳
介绍
读写锁
- 实现了接口ReadWriteLock
- 适合于读多写少的情况
- 支持公平锁和非公平锁
- 支持可冲入(进入读锁后可再进入读锁,进入写锁后可再进入写锁和读锁)
- 支持可冲入和公平与非公平
缺点
(1) 写锁饥饿问题,读的线程很多,写的线程抢占不到锁,就一直抢占不到锁,就饥饿
(2) 锁降级,获取写锁后又再次获取读锁(重入),释放了写锁之后就变成了读锁,就是锁降级
内部接口
Sync/ReadLock/WriteLock/FairSync/NonfairSync 是其内部类 下面图有问题,ReadWriteLock没有实现Lock
代码演示
public class Lock { public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10; i++) { Lock.put(i + "", i + ""); } } }).start(); new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10; i++) { Lock.get(i + ""); } } }).start(); } static Map<String, Object> map = new HashMap<String, Object>(); static ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); static java.util.concurrent.locks.Lock readLock = readWriteLock.readLock(); static java.util.concurrent.locks.Lock writeLock = readWriteLock.writeLock(); public static final Object get(String key) { readLock.lock(); try { System.out.println("正在做读的操作,key:" + key + "开始"); Thread.sleep(100); Object object = map.get(key); System.out.println("正在做读的操作,key:" + key + "结束"); System.out.println(); return object; } catch (InterruptedException e) { e.printStackTrace(); } finally { readLock.unlock(); } return key; } public static final Object put(String key, Object value) { writeLock.lock(); try { System.out.println("正在做写的操作,key:" + key + ",value:" + value + "开始"); Thread.sleep(100); Object object = map.put(key, value); System.out.println("正在做写的操作,key:" + key + ",value:" + value + "结束"); System.out.println(); return object; } catch (InterruptedException e) { e.printStackTrace(); } finally { writeLock.unlock(); } return value; } public static final void clear() { writeLock.lock(); try { map.clear(); } finally { writeLock.unlock(); } } }
class MyResource { Map<String,String> map = new HashMap<>(); Lock lock = new ReentrantLock(); ReadWriteLock rwLock = new ReentrantReadWriteLock(); public void write(String key ,String value) { rwLock.writeLock().lock(); try { System.out.println(Thread.currentThread().getName()+"\t"+"正在写入"); map.put(key,value); //暂停毫秒 try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"\t"+"完成写入"); }finally { rwLock.writeLock().unlock(); } } public void read(String key) { rwLock.readLock().lock(); try { System.out.println(Thread.currentThread().getName()+"\t"+"正在读取"); String result = map.get(key); // 暂停2000毫秒,演示读锁没有完成之前,写锁无法获得 try { TimeUnit.MILLISECONDS.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"\t"+"完成读取"+"\t"+result); }finally { rwLock.readLock().unlock(); } } } /** * @auther zzyy * @create 2022-04-08 18:18 */ public class ReentrantReadWriteLockDemo { public static void main(String[] args) { MyResource myResource = new MyResource(); for (int i = 1; i <=10; i++) { int finalI = i; new Thread(() -> { myResource.write(finalI +"", finalI +""); }, String.valueOf(i)).start(); } for (int i = 1; i <=10; i++) { int finalI = i; new Thread(() -> { myResource.read(finalI +""); },String.valueOf(i)).start(); } // 暂停几秒钟线程 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } // 在读锁没有完成时, 写锁不能写入 for (int i = 1; i <=3; i++) { int finalI = i; new Thread(() -> { myResource.write(finalI +"", finalI +""); },"新写锁线程->"+String.valueOf(i)).start(); } } }
实现原理
从表面来看,ReadLock和WriteLock是两把锁,实际上它只是同一把锁的两个视图而已。
什么叫两个视图呢?可以理解为是一把锁,线程分成两类:读线程和写线程。读线程和写线程之间不互斥(可以同时拿到这把锁),读线程之间不互斥,写线程之间互斥
从下面的构造方法也可以看出,readerLock和writerLock实际共用同一个sync对象。
sync对象同互斥锁一样,分为非公平和公平两种策略,并继承自AQS
public ReentrantReadWriteLock() { this(false); } public ReentrantReadWriteLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); // fair为false使用非公平锁,true使用公平锁 readerLock = new ReadLock(this); writerLock = new WriteLock(this); }
同互斥锁一样,读写锁也是用state变量来表示锁状态的。只是state变量在这里的含义和互斥锁完全不同。在内部类Sync中,对state变量进行了重新定义,如下所示:
abstract static class Sync extends AbstractQueuedSynchronizer { // ... static final int SHARED_SHIFT = 16; static final int SHARED_UNIT = (1 << SHARED_SHIFT); static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; // 持有读锁的线程的重入次数 static int sharedCount(int c) { return c >>> SHARED_SHIFT; } // 持有写锁的线程的重入次数 static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; } // ... }
把 state变量拆成两半,低16位用来记录写锁。但同一时间既然只能有一个线程写,为什么还需要16位呢?这是因为一个写线程可能多次重入。
例如,低16位的值等于5,表示一个写线程重入了5次 高16位,用来读锁。
如高16位的值等于5,既可以表示5个读线程都拿到了该锁;也可以表示一个读线程重入了5次
为什么要把一个int类型变量拆成两半,而不是用两个int型变量分别表示读锁和写锁的状态呢?因为无法用一次CAS同时操作两个int变量,所以用了一个int型的高16位和低16位分别表示读锁和写锁的状态
当state=0时,说明既没有线程持有读锁,也没有线程持有写锁;当state != 0时,要么有线程持有读锁,要么有线程持有写锁,两者不能同时成立,因为读和写互斥。
这时再进一步通过sharedCount(state)和exclusiveCount(state)判断到底是读线程还是写线程持有了该锁
到此这篇关于Java中的ReentrantReadWriteLock实现原理详解的文章就介绍到这了,更多相关ReentrantReadWriteLock原理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!