并发——读写锁初探

比特星途者
• 阅读 1870

适用场景

  • 读操作频发,写操作不频繁。
  • 两个线程同时读取同一个共享资源没有任何问题
  • 如果一个线程对共享资源进行写操作,此时就不能有其他线程对共享资源进行读写

条件分析

  • 写操作的优先级高于读操作,在读操作频繁的场景下,如果写操作没有高于读操作的优先级,就会导致写操作线程“饿死”的情况发生
  • 读操作触发条件:

    1. 没有线程正在执行写操作
    2. 没有线程在等待执行写操作
  • 写操作触发条件:没有线程正在执行读写操作

代码实现

public class ReadWriteLock {

  private int readers = 0;
  private int writers = 0;
  private int writeRequests = 0;

  public synchronized void lockRead() throws InterruptedException {

    while (writers > 0 || writeRequests > 0) {
      wait();
    }
    readers++;
  }

  public synchronized void unlockRead() {
    readers--;
    notifyAll();
  }

  public synchronized void lockWrite() throws InterruptedException {

    writeRequests++;
    while (readers > 0 || writers > 0) {
      wait();
    }

    writeRequests--;
    writers++;
  }

  public synchronized void unlockWrite() throws InterruptedException {

    writers--;
    notifyAll();
  }
}
ReadWriteLockl类中通过读锁、写锁以两个锁的状态控制线程的读、写操作:
writers表示当前正在使用写锁的线程数量;
writeRequests表示等待请求写锁的线程数量;
readers表示请求读锁的线程数量;
说明:
1.线程在获取读锁的时候,只要没有线程拥有写锁即writers==0同时没有线程请求写锁即writerRquests==0,那么线程就能成功获取读锁;
2.当一个线程想获取写锁的时候,会把写锁的请求数加1即writeRequests++,然后再尝试获取能否获取写锁,如果当前没有线程占用写锁即writers==0,那么此时就能成功获取写锁,同时writers++;如果wirters>0表示写锁此时被其他线程占用那么当前线程会被阻塞等待写锁释放时被唤醒。
3.写操作的优先级高于读操作的优先级体现在,线程请求读锁时会判断持有写锁的线程数和请求写锁的线程数,即while(writers > 0 || writeRequests > 0){wait();},而线程请求写锁时只需要判断持有写锁和读锁的线程数即可,即while(readers > 0 || writers > 0) {wait();}

锁重入

锁重入,是指同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。ReentrantLock 和synchronized 都是可重入锁,可重入锁最大的作用是避免死锁。
以自旋锁为例,如果自旋锁不是可重入锁的话,如果一个线程在第一次获取锁执行同步代码前提下,第二次再执行同步代码就产生了死锁。
以前面的代码为例:

  1. 此时有两个线程Thread1,Thread2
  2. Thread2在Thread1获取读锁以后请求写锁,readers=1、writers=0、writeRequests=1
  3. 若此时Thread1再次尝试获取同一个读锁,根据已有的代码writers > 0 || writeRequests > 0,因为Thread请求写锁的原因导致该条件成立,Thread1进入阻塞状态,死锁出现

读锁重入

public class ReadWriteLock{

 private Map<Thread, Integer> readingThreads = new HashMap<Thread, Integer>();
 private int writers = 0;
 private int writeRequests = 0;

 public synchronized void lockRead() throws InterruptedException{
   Thread callingThread = Thread.currentThread();
   while(! canGrantReadAccess(callingThread)){
     wait();
   }
   readingThreads.put(callingThread, (getAccessCount(callingThread) + 1));
 }

 public synchronized void unlockRead(){
   Thread callingThread = Thread.currentThread();
   int accessCount = getAccessCount(callingThread);
   if(accessCount == 1) {
    readingThreads.remove(callingThread);
   } else {
    readingThreads.put(callingThread, (accessCount -1));
   }
   notifyAll();
 }

 private boolean canGrantReadAccess(Thread callingThread){
   if(writers > 0) return false;
   if(isReader(callingThread) return true;
   if(writeRequests > 0) return false;
   return true;
 }

 private int getReadAccessCount(Thread callingThread){
   Integer accessCount = readingThreads.get(callingThread);
   if(accessCount == null) return 0;
   return accessCount.intValue();
}

 private boolean isReader(Thread callingThread){
   return readingThreads.get(callingThread) != null;
 }
}
读锁的可重入有两种情况:
1.当前程序中没有线程请求写锁(这种情况是几乎不存在)
2.当前程序中有线程请求写锁也有线程请求读锁,并且有线程已经得到了读锁

第二种情况是最常见的,因此我们需要知道哪些线程是持有读锁的

因此在代码中使用Map来存储已经持有读锁的线程和对应线程获取读锁的次数,通过Map就可以判断对应的线程是否持有读锁,调整之后的代码在原有判断"writeRequests >0"和"writers > 0"还加上了判断当前线程是否持有读锁的判断逻辑

写锁重入

public class ReadWriteLock{
 private Map<Thread, Integer> readingThreads = new HashMap<Thread, Integer>();
 private int writeAccesses = 0;
 private int writeRequests = 0;
 private Thread writingThread = null;

 public synchronized void lockWrite() throws InterruptedException{

   writeRequests++;
   Thread callingThread = Thread.currentThread();
   while(!canGrantWriteAccess(callingThread)){
    wait();
   }
   writeRequests--;
   writeAccesses++;
   writingThread = callingThread;
 }

 public synchronized void unlockWrite() throws InterruptedException{
   writeAccesses--;
   if(writeAccesses == 0){
     writingThread = null;
   }
   notifyAll();
 }

 private boolean canGrantWriteAccess(Thread callingThread){
   if(hasReaders()) return false;
   if(writingThread == null) return true;
   if(!isWriter(callingThread)) return false;
   return true;
 }

 private boolean hasReaders(){
   return readingThreads.size() > 0;
 }

 private boolean isWriter(Thread callingThread){
   return writingThread == callingThread;
 }
}
写锁重入,是在当前程序里有且只有一个线程持有写锁,如果写锁重入,说明当前程序中没有线程持有读锁,写锁重入只有持有写锁的线程才能重入,其他的线程就需要进入阻塞状态

读写锁完整代码

public class ReadWriteLock{

    private Map<Thread, Integer> readingThreads = new HashMap<Thread, Integer>();
    private int writeAccesses = 0;
    private int writeRequests = 0;
    private Thread writingThread = null;

    public synchronized void lockRead() throws InterruptedException{
     Thread callingThread = Thread.currentThread();
     while(! canGrantReadAccess(callingThread)){
         wait();
     }
     readingThreads.put(callingThread,(getReadAccessCount(callingThread) + 1));
    }

    private boolean canGrantReadAccess(Thread callingThread){
     #写锁降级到读锁的逻辑判断
     if(isWriter(callingThread)) return true;
     if(hasWriter()) return false;
     if(isReader(callingThread)) return true;
     if(hasWriteRequests()) return false;
     return true;
    }

    public synchronized void unlockRead(){
     Thread callingThread = Thread.currentThread();
     if(!isReader(callingThread)){
        throw new IllegalMonitorStateException(
             "Calling Thread does not" +
             " hold a read lock on this ReadWriteLock");
     }

     int accessCount = getReadAccessCount(callingThread);
     if(accessCount == 1){
         readingThreads.remove(callingThread);
     } else {
         readingThreads.put(callingThread, (accessCount -1));
     }
     notifyAll();
    }

    public synchronized void lockWrite() throws InterruptedException{
     writeRequests++;
     Thread callingThread = Thread.currentThread();
     while(!canGrantWriteAccess(callingThread)){
         wait();
     }
     writeRequests--;
     writeAccesses++;
     writingThread = callingThread;
    }

    public synchronized void unlockWrite() throws InterruptedException{
     if(!isWriter(Thread.currentThread()){
        throw new IllegalMonitorStateException(
         "Calling Thread does not" +
         " hold the write lock on this ReadWriteLock");
     }
     writeAccesses--;
     if(writeAccesses == 0){
         writingThread = null;
     }
     notifyAll();
    }

    private boolean canGrantWriteAccess(Thread callingThread){
     #读锁转换成写锁的逻辑判断
     if(isOnlyReader(callingThread)) return true;
     if(hasReaders()) return false;
     if(writingThread == null) return true;
     if(!isWriter(callingThread)) return false;
     return true;
    }

    private int getReadAccessCount(Thread callingThread){
     Integer accessCount = readingThreads.get(callingThread);
     if(accessCount == null) return 0;
     return accessCount.intValue();
    }

    private boolean hasReaders(){
     return readingThreads.size() > 0;
    }

    private boolean isReader(Thread callingThread){
     return readingThreads.get(callingThread) != null;
    }

    private boolean isOnlyReader(Thread callingThread){
     return readingThreads.size() == 1 && readingThreads.get(callingThread) != null;
    }

    private boolean hasWriter(){
     return writingThread != null;
    }

    private boolean isWriter(Thread callingThread){
     return writingThread == callingThread;
    }

    private boolean hasWriteRequests(){
     return this.writeRequests > 0;
    }
}

参考文献
http://ifeve.com/read-write-l...

点赞
收藏
评论区
推荐文章
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Stella981 Stella981
3年前
ConcurrentHashMap介绍
在进行结构性修改,如put/remove/replace时都需要进行加锁,但是读取并未加锁,并发情况下,由于内存不同步问题,会导致一个线程的写操作并不会立即对另一个线程可见。这里ConcurrentHashMap通过volatile变量的内存可见性特性来保证一个线程的写操作立即被其他线程可见,每个方法在一开始都会读取count这个变量,该变量就是一个vola
Wesley13 Wesley13
3年前
Java8 容器类详解
 ArrayListVectorCopyOnWriteArrayListLinkedListHashMapConcurrentHashMapLinkedHashMapLinkedBlockingQueuePriorityQueue使用场景随机访问ArrayList的线程安全版读多写少,写加锁,写操作在复制的
Wesley13 Wesley13
3年前
mysql 锁
第一章概述锁的分类:从对数据操作的粒度分表锁、行锁。从对数据的操作类型(读\\写)分读锁(共享锁)、写锁(排它锁)读锁(共享锁):针对同一份数据,多个读操作可以同时进行而不会互相影响。写锁(排它锁):当前写操作没完成前,它会阻断其他写锁和读锁。第二章 表锁(偏读)偏向MyISAM存储引擎,开销小,加
Wesley13 Wesley13
3年前
JAVA中 ReentrantReadWriteLock读写锁详系教程,包会
一、读写锁简介现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。 针对这种场景,JAVA的并发包提供了读写锁ReentrantReadW
Wesley13 Wesley13
3年前
Java中的读写锁ReadWriteLock
ReadWriteLock是JDK中的读写锁接口ReentrantReadWriteLock是ReadWriteLock的一种实现读写锁非常适合读多写少的场景。读写锁与互斥锁的一个重要区别是读写锁允许多个线程同时读共享变量,这是读写锁在读多写少的情况下性能较高的原因。读写锁的原则:多个线程可同时读共享变量只允许一
Stella981 Stella981
3年前
ReentrantReadWriteLock(读写锁)
ReentrantReadWriteLock是JDK5中提供的读写分离锁。读写分离锁可以有效的帮助减少锁的竞争,以此来提升系统的性能。用锁分离的机制来提升性能也非常好理解,比如线程A,B,C进行写操作,D,E,F进行读操作,如果使用ReentrantLock或者synchronized关键字,这些线程都是串行执行的,即每次都只有一个线程做操作。但是当D进行读
Stella981 Stella981
3年前
ReentrantReadWriteLock实现原理
  在java并发包java.util.concurrent中,除了重入锁ReentrantLock外,读写锁ReentrantReadWriteLock也很常用。在实际开发场景中,在使用共享资源时,可能读操作远远多于写操作。这种情况下,如果对这部分共享资源能够让多个线程读的时候不受阻塞,仅仅在写的时候保证安全性,这样效率会得到显著提升。读写锁Reentra
Stella981 Stella981
3年前
ReentrantReadWriteLock读写锁详解
一、读写锁简介现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。 针对这种场景,JAVA的并发包提供了读写锁ReentrantReadW
Wesley13 Wesley13
3年前
Java并发编程原理与实战十八:读写锁
ReadWriteLock也是一个接口,提供了readLock和writeLock两种锁的操作机制,一个资源可以被多个线程同时读,或者被一个线程写,但是不能同时存在读和写线程。基本规则:读读不互斥读写互斥写写互斥问题:既然读读不互斥,为何还要加读锁答:如果只是读,是不需要加锁的,加锁本身就有性能上的损耗如果读可以不是最新数据
Wesley13 Wesley13
3年前
4种常用Java线程锁的特点,性能比较及使用场景
多个线程同时对同一个对象进行读写操作,很容易会出现一些难以预料的问题。所以很多时候我们需要给代码块加锁,同一时刻只允许一个线程对某个对象进行操作。多线程之所以会容易引发一些难以发现的bug,很多时候是写代码的程序员对线程锁不熟悉或者干脆就没有在必要的地方给线程加锁导致的。本篇我想分享java多线程中的4种常见线程锁的特点、性能比较及使用场景。一、多线