JUC之读写锁问题

读写锁

读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。

读操作可以多个线程,写操作只能一个线程

Java并发包提供读写锁的实现是 ReentrantReadWriteLock

特性:

  1. 支持公平性和非公平的锁获取方式
  2. 支持重进入:以读写线程为例,当读线程获取读锁以后,还能再次获取读锁,而写线程在获取写锁时还未完全释放的时候还能再获取写锁以及也能获取读锁。
  3. 锁降级。写锁可以降级为读锁,但是读锁不能升级为写锁

锁降级的定义:

锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。

写锁可以降级为读锁顺序:获取写锁—-获取读锁——释放写锁——释放读锁。

其缺点:会造成锁饥饿问题 一直读,没有写操作。

资源与锁的三个状态:

  1. 无锁,多线程抢夺资源 乱
  2. 添加锁(Synchronized和ReentrantLock) 都是独占,读读、读写、写写都是独占,每次只能一个操作
  3. 读写锁,读读可以共享,提升性能,同时可以多人进行读操作

ReentrantReadWriteLock 目的就是:提高读操作的吞吐量 (可用于读多写少的情况下)

读写锁可重入的理解:

读锁的重入是允许多个申请读操作的线程,而写锁同时只能允许单个线程占有,该线程的写操作可以重入。

如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,也就是锁的降级。

如果一个线程同时占有了读锁和写锁,在完全释放了写锁,那么就转换为了读锁,以后写操作无法重入,如果写锁未完全释放时,写操作时可以重入的。

失败例子:

package com.RWLock;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

class MyCache{
    private volatile Map<String,Object> map = new HashMap<String,Object>();

    //写操作
    public void put(String key,Object value) throws InterruptedException {
        System.out.println(Thread.currentThread().getName()+"\t------写入数据"+key);
        TimeUnit.SECONDS.sleep(1);
        map.put(key,value);
        System.out.println(Thread.currentThread().getName()+"\t------写入完成"+key);
    }
    //读操作
    public void get(String key) throws InterruptedException {
        System.out.println(Thread.currentThread().getName()+"\t------读数据"+key);
        TimeUnit.SECONDS.sleep(1);
        map.get(key);
        System.out.println(Thread.currentThread().getName()+"\t------读取完成"+key);
    }
}


public class readWriteLockDemo {
    public static void main(String[] args) {
        MyCache myCache = new MyCache();
        //多个线程进行写操作
        for (int i = 1; i <= 5; i++) {
            int finalI = i;
            new Thread(()->{
                try {
                    myCache.put(finalI +"", finalI +"");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
        for (int i = 1; i <= 5; i++) {
            int finalI = i;
            new Thread(()->{
                try {
                    myCache.get(finalI+"");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

使用读写锁以后:

Cache组合一个非线程安全的HashMap作为缓存的实现,同时使用读写锁的读锁和写锁来保证Cache是线程安全的。在读操作get(String key)方法中,需要获取读锁,这使得并发访问该方法时不会被阻塞。写操作put(String key,Object value)方法和clear()方法,在更新HashMap时必须提前获取写锁,当获取写锁后,其他线程对于读锁和写锁的获取均被阻塞,而只有写锁被释放之后,其他读写操作才能继续。

package com.RWLock;

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 MyCache{
    private volatile Map<String,Object> map = new HashMap<String,Object>();
    //可重入的读写锁
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    //写操作
    public void put(String key,Object value) throws InterruptedException {
        try{
            readWriteLock.writeLock().lock(); //写锁
            System.out.println(Thread.currentThread().getName()+"\t------写入数据"+key);
            TimeUnit.SECONDS.sleep(1);
            map.put(key,value);
            System.out.println(Thread.currentThread().getName()+"\t------写入完成"+key);
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally{
            readWriteLock.writeLock().unlock();
        }

    }
    //读操作
    public void get(String key) {
        try {
            readWriteLock.readLock().lock();   //读锁
            System.out.println(Thread.currentThread().getName()+"\t------读数据"+key);
            TimeUnit.SECONDS.sleep(1);
            map.get(key);
            System.out.println(Thread.currentThread().getName()+"\t------读取完成"+key);
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally{
            readWriteLock.readLock().unlock();
        }


    }
}


public class readWriteLockDemo {
    public static void main(String[] args) {
        MyCache myCache = new MyCache();
        //多个线程进行写操作
        for (int i = 1; i <= 5; i++) {
            int finalI = i;
            new Thread(()->{
                try {
                    myCache.put(finalI +"", finalI +"");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
        for (int i = 1; i <= 5; i++) {
            int finalI = i;
            new Thread(()->{
                try {
                    myCache.get(finalI+"");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

读写锁的设计:依赖于同步器的同步状态实现的。

同步状态表示锁被一个线程重复获取的次数,而读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态,使得该状态的设计成为读写锁实现的关键

如果一个整型变量维护,按位切割,高16位为读状态,低16位为写状态。

读写锁是如何迅速确定读和写各自的状态呢?答案是通过位运算

写锁的获取和释放:

写锁是一个支持重进入的排他锁;

  1. 如果当前线程获取了写锁,则增加写状态,独占
  2. 如果当前线程(A)再获取锁时,读锁已经被获取或者该线程不是已经获取写锁的线程(个人理解:如果有线程获取了写锁,则其他读写线程的后续访问均被阻塞),则当前线程(A)进入等待状态。

获取读锁后不能获取写锁,但是获取写锁后可以获取读锁

读锁的获取和释放

读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问(或者写状态为0)时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态。

如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠CAS保证)增加读状态,成功获取读锁