JUC基础07——读写锁

23 阅读6分钟

读写锁

先说说独占锁共享锁互斥锁

简述

  • 独占锁(写锁):只能被一个线程所持有。例如:ReentrantLockSynchronized 都是独占锁。当一个线程获得独占锁后,其他任何尝试获取该锁的线程都会被阻塞,直到这把锁被释放。独占锁主要用于需要独占某个资源的情况。比如:当一个线程正在进行写操作时,就需要确保没有其他线程能够同时进行读或者写操作

  • 共享锁(读锁):可以被多个线程所持有。当一个线程获得共享锁后,其他线程也可以获得共享锁,不会造成阻塞。这种锁主要用于需要允许多个线程同时访问某个资源的情况。比如:当多个线程需要读取同一份数据时,它们可以同时获得共享锁进行读取

  • 互斥锁:在某一个时刻只允许一个线程去访问资源,它会在任何情况下阻止其他线程对资源的访问。互斥锁主要用于严格控制并发的情况。比如:当一个线程在进行敏感操作时,需要确保没有其他线程能干扰。

为什么会有 读锁写锁

读锁(共享锁)允许多个线程同时读取共享资源,而写锁(独占锁)只允许一个线程写入共享资源。

在并发的场景下,如果我们使用独占锁 进行资源访问控制,每次都只能有一个线程进行访问,当存在读写分离的需求,在读的时候需要并发的访问,读锁并不会造成数据不一致的问题,可以多个线程共享读,此时在只有独占锁的情况下就会导致并发访问效率低下。

所以为了满足并发量,读取共享资源应该可以同时进行,但是如果一个线程想去写共享资源,就需要确保没有其他线程可以对该资源同时进行读或者写操作。

像之前我们使用的 ReentrantLock 或者 Synchronized 都是独占锁,而系统的大多数场景都是读多写少,使用独占锁必然导致效率低下,因此就有了读写锁 ReentrantReadWriteLock

ReentrantReadWriteLock 简述

ReentrantReadWriteLock 位于 java.util.concurrent.locks 包下。它实现了ReadWriteLock接口,提供了一种可重入的读写锁。这种锁允许多个线程同时读取共享资源,而在写操作时只允许一个线程进行操作。这种锁可以提高并发性能和吞吐量,适用于读操作频繁而写操作较少的场景。

ReentrantReadWriteLock 有两个内部类: ReadLockWriteLock

  • ReadLock:读锁(共享锁),允许多个线程同时读取共享资源
  • WriteLock:写锁(独占锁),只允许一个线程写入字眼

ReentrantReadWriteLock 具有以下特点:

  1. 可重入性:ReentranReadWriteLock 支持重入,即读线程获取读锁之后能够再次获取读锁;写线程获取写锁之后能够再次获取写锁,同时也可以获取读锁。这种特性可以避免死锁

  2. 公平性:ReentrantReadWriteLock 支持公平和非公平两种模式。在公平模式下,锁的获取按照先后顺序进行,先请求锁的线程先获取锁,后请求锁的线程等待先获取锁的线程释放锁之后再获取。在非公平模式下,锁的获取不保证按照先后顺序进行,可能出现后请求锁的线程先获取锁的情况。

  3. 分离读锁和写锁:ReentranReadWriteLock 分离了读锁和写锁,通过分离读锁和写锁,使得并发性比一般的排他锁有了较大的提升:同一时间可以允许多个线程同时访问,在写线程获取到锁时,所有读线程和线程都会被阻塞

  4. 支持状态管理:ReentranReadWriteLock 支持状态管理,可以通过 getState() 方法获取当前状态,也可以通过 setState() 方法设置当前状态。泽中特性可以用于实现复杂的业务逻辑

ReentrantReadWriteLock 使用

不加锁案例:实现一个读写缓存的操作,看看没有有加锁的时候,会出现什么情况

代码:

package com.avgrado.demo.thread;


import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
 * @ClassName ReadWriteLockDemo
 * @Description TODO
 * @Author 半晨烟宇
 */
class MyCache{
   volatile Map cacheMap =  new HashMap<>();

   /**
    * 定义写入资源方法
    */
   public void put(String key,Object value){
       System.out.println(Thread.currentThread().getName()+"\t 正在写入");

       try {
           //模拟网络拥堵延迟0.3秒
           TimeUnit.MILLISECONDS.sleep(300);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
       cacheMap.put(key,value);
       System.out.println(Thread.currentThread().getName()+"\t 写入完成");
   }
   
    /**
    * 定义读取资源方法
    */
   public void get(String key){
       System.out.println(Thread.currentThread().getName()+"\t 正在读取");

       try {
           //模拟查询缓慢,延迟0.5秒
           TimeUnit.MILLISECONDS.sleep(500);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }

       Object value = cacheMap.get(key);
       System.out.println(Thread.currentThread().getName()+"\t 读取完成:"+value);
   }
}

public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache cache = new MyCache();
        //模拟5个线程写
        for(int i =1;i<=5;i++){
            final int tempInt = i;
            new Thread(()->{
                cache.put(tempInt+"",tempInt);
            },String.valueOf(i)+"号线程").start();
        }

        //模拟5个线程读
        for(int j=1;j<=5;j++){
            final int tmpInt = j;
            new Thread(()->{
                cache.get(tmpInt+"");
            },String.valueOf(j)+"号线程").start();
        }
    }
}

执行结果:

((I0WKK8~R_L1~22XKWMD.png 从结果看出:我们可以看到,在写入的时候,写操作都全部完成就被其它线程打断了,这就造成了,还没写完,其它线程又开始读,这样就造成结果不一致

解决办法

上面的代码是没有加锁的,这样就会造成线程在进行写入操作的时候,被其它线程频繁打断,从而不具备原子性。针对这种情况,使用读写锁ReentranReadWriteLock解决,保证写操作是独占,读操作共享

代码:

package com.avgrado.demo.thread;


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

class MyCache{
   volatile Map map =  new HashMap<>();

   final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    /**
     * 定义写入资源方法
     */
    public void put(String key, Object value) {

        // 创建一个写锁
        readWriteLock.writeLock().lock();

        try {

            System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key);

            try {
                // 模拟网络拥堵,延迟0.3秒
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            map.put(key, value);

            System.out.println(Thread.currentThread().getName() + "\t 写入完成");

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 写锁 释放
            readWriteLock.writeLock().unlock();
        }
    }

    /**
     * 定义读取资源方法
     */
    public void get(String key) {

        // 读锁
        readWriteLock.readLock().lock();
        try {

            System.out.println(Thread.currentThread().getName() + "\t 正在读取:");

            try {
                // 模拟网络拥堵,延迟0.3秒
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            Object value = map.get(key);

            System.out.println(Thread.currentThread().getName() + "\t 读取完成:" + value);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 读锁释放
            readWriteLock.readLock().unlock();
        }
    }
}
/**
 * @ClassName ReadWriteLockDemo
 * @Description TODO
 * @Author gongchen
 * @Date 2023-10-30 11:22
 */
public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache cache = new MyCache();
        //模拟5个线程写
        for(int i =1;i<=5;i++){
            final int tempInt = i;
            new Thread(()->{
                cache.put(tempInt+"",tempInt);
            },String.valueOf(i)+"号线程").start();
        }

        //模拟5个线程读
        for(int j=1;j<=5;j++){
            final int tmpInt = j;
            new Thread(()->{
                cache.get(tmpInt+"");
            },String.valueOf(j)+"号线程").start();
        }
    }
}

执行结果:

image.png

从运行结果我们可以看出,写入操作是一个一个线程进行执行的,并且中间不会被打断,而读操作的时候,是同时5个线程进入,然后并发读取操作