Java编程读写锁详解

ReadWriteLock也是一个接口,提供了readLock和writeLock两种锁的操作机制,一个资源可以被多个线程同时读,或者被一个线程写,但是不能同时存在读和写线程。

基本规则: 读读不互斥 读写互斥 写写互斥

问题: 既然读读不互斥,为何还要加读锁

答: 如果只是读,是不需要加锁的,加锁本身就有性能上的损耗

如果读可以不是最新数据,也不需要加锁

如果读必须是最新数据,必须加读写锁

读写锁相较于互斥锁的优点仅仅是允许读读的并发,除此之外并无其他。

结论: 读写锁能够保证读取数据的 严格实时性,如果不需要这种 严格实时性,那么不需要加读写锁。

简单实现:

package readandwrite;

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class MyTest {
 private static ReentrantReadWriteLock rwl=new ReentrantReadWriteLock();
 private static double data=0;
 static class readClass implements Runnable{
  @Override
  public void run() {
   rwl.readLock().lock();
   System.out.println("读数据:"+data);
   rwl.readLock().unlock();
  }
 }

 static class writeClass implements Runnable{
  private double i;

  public writeClass(double i) {
   this.i = i;
  }

  @Override
  public void run() {
   rwl.writeLock().lock();
   data=i;
   System.out.println("写数据: "+data);
   rwl.writeLock().unlock();
  }

 }

 public static void main(String[] args) throws InterruptedException {
  ExecutorService pool=Executors.newCachedThreadPool();
  for(int i=0;i<10;i++){
   pool.submit(new readClass());
   pool.submit(new writeClass((double)new Random().nextDouble()));
   pool.submit(new writeClass((double)new Random().nextDouble()));
   Thread.sleep(1000);
  }

  pool.shutdown();
 }

}

之前我们提到的锁都是排它锁(同一时刻只允许一个线程进行访问),而读写锁维护了一对锁,一个读锁,一个写锁。读写锁在同一时刻允许多个线程进行读操作,但是写线程访问过程中,所有的读线程和其他写线程均被阻塞。如此,并发性有了很大的提升。这样,在某些读远远大于写的场景中,读写锁能够提供比排它锁更好的并发量和吞吐量。

一个关于读写锁的Demo:

分析:设计一个模拟队列,拥有一个data成员变量用于存储数据和存取两种操作。

import java.util.Random;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockDemo
{

  public static void main(String[] args)
  {
    DefQueue queue = new DefQueue();
    for (int i = 1; i < 10; i++)
    {
      //启动线程进行读操作
      new Thread(new Runnable()
      {
        @Override
        public void run()
        {
          while (true)
          {
            queue.get();
          }
        }

      }).start();

      //启动线程进行写操作
      new Thread(new Runnable()
      {
        @Override
        public void run()
        {
          while(true)
          {
            queue.put(new Random().nextInt(10000));
          }
        }
      }).start();
    }
  }

}

class DefQueue
{
  private int data;
  ReadWriteLock rwLock = new ReentrantReadWriteLock();

  public void get()
  {
    rwLock.readLock().lock();//加读锁
    try
    {
      System.out.println(Thread.currentThread().getName() + "be ready to get data");
      Thread.sleep((long) (Math.random() * 1000));

      System.out.println(Thread.currentThread().getName() + "get the data:  " + data);

    } catch (InterruptedException e)
    {
      e.printStackTrace();
    } finally
    {
      rwLock.readLock().unlock();//释放读锁
    }
  }

  public void put(int data)
  {
    rwLock.writeLock().lock();//加写锁

    try
    {
      System.out.println(Thread.currentThread().getName() + " be ready to write data");

      Thread.sleep((long) (Math.random() * 1000));

      this.data = data;

      System.out.println(Thread.currentThread().getName() + " has wrote the data: "+data);
    } catch (InterruptedException e)
    {
      e.printStackTrace();
    } finally
    {
      rwLock.writeLock().unlock();//释放写锁
    }

  }
}

程序部分运行结果:

Thread-0be ready to get data
Thread-0get the data:  0
Thread-1 be ready to write data
Thread-1 has wrote the data: 1156
Thread-2be ready to get data
Thread-2get the data:  1156
Thread-3 be ready to write data
Thread-3 has wrote the data: 9784
Thread-3 be ready to write data
Thread-3 has wrote the data: 4370
Thread-3 be ready to write data
Thread-3 has wrote the data: 1533
Thread-4be ready to get data
Thread-4get the data:  1533
Thread-5 be ready to write data
Thread-5 has wrote the data: 2345
Thread-6be ready to get data
Thread-6get the data:  2345
Thread-9 be ready to write data
Thread-9 has wrote the data: 9463
Thread-9 be ready to write data
Thread-9 has wrote the data: 9301
Thread-9 be ready to write data
Thread-9 has wrote the data: 549
Thread-9 be ready to write data
Thread-9 has wrote the data: 4673
Thread-9 be ready to write data

我们可以看到打印语句结果很正常。

下面我们再来实现一个模拟缓冲区的小Demo:

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

/*
 * @author vayne
 *
 * 多线程实现缓存的小demo
 */
class Cachend
{
  volatile Map<String, String> cachmap = new HashMap<String, String>();//加volatile关键字保证可见性。

  ReadWriteLock rwLock = new ReentrantReadWriteLock();//这个读写锁要定义在方法外面,使得每一个线程用的是同一个读写锁。
  public String getS(String key)           //如果定义在方法内部,就是跟方法栈有关的读写锁。这样可能不是同一个锁。
  {
    rwLock.readLock().lock();
    String value = null;
    try
    {
      value = cachmap.get(key);

      if (cachmap.get(key) == null)//这里要重新获得key对应的value值
      {
        rwLock.readLock().unlock();
        rwLock.writeLock().lock();
        try
        {
          if (cachmap.get(key) == null)//这里也是
          {
            value = "" + Thread.currentThread().getName();

            cachmap.put(key, value);

            System.out.println(Thread.currentThread().getName() + " put the value ::::" + value);
          }
        } finally
        {
          rwLock.readLock().lock();  //将锁降级,这里跟下一句的顺序不能反。
          rwLock.writeLock().unlock();//关于这里的顺序问题,下面我会提到。
        }
      }

    } finally
    {
      rwLock.readLock().unlock();
    }

    return cachmap.get(key);
  }
}

public class CachendDemo
{
  public static void main(String[] args)
  {
    Cachend ca = new Cachend();
    for (int i = 0; i < 4; i++)
    {
      new Thread(new Runnable()
      {
        @Override
        public void run()
        {
          System.out.println(Thread.currentThread().getName()+" "+ca.getS("demo1"));
          System.out.println(Thread.currentThread().getName()+" "+ca.cachmap.entrySet());
        }
      }).start();
    }
  }
}

运行结果:

Thread-0 put the value ::::Thread-0
Thread-0 Thread-0
Thread-0 [demo1=Thread-0]
Thread-2 Thread-0
Thread-2 [demo1=Thread-0]
Thread-3 Thread-0
Thread-3 [demo1=Thread-0]
Thread-1 Thread-0
Thread-1 [demo1=Thread-0]

上面我给出了一些注释,其实这个代码是很不好写的,考虑的东西很多。下面我来讲一下上面的代码中提到的顺序问题。

对于读写锁我们应该了解下面的一些性质(这些性质是由源代码得出来的,因为源代码的设计,所以才有下列性质):

  • 如果存在读锁,则写锁不能被获取,原因在于:读写锁要确保写锁的操作对读锁可见。,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。因此,只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问将会被阻塞。
  • 锁降级:指的是写锁降级成为读锁。具体操作是获取到写锁之后,在释放写锁之前,要先再次获取读锁。这也就是上面我写注释提醒大家注意的地方。为什么要这样处理呢,答案就是为了保证数据可见性。如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,知道当前线程使用数据并释放读锁之后,T才能获取写锁进行数据更新。

第二条对应我们上面的程序就是,如果我们添加了“demo1”对应的value值,然后释放了写锁,此时在当前线程S还未获得读锁时,另一个线程T又获得了写锁,那么就会将S的操作给覆盖(如果取到的值已经缓存在S中,那么T的操作就无法被S感知了,到最后依然会返回S操作的值)。

再来看一个DEMO:

读写锁,分为读锁和写锁,多个读锁不互斥,读锁和写锁互斥,写锁与写锁互斥,这是JVM自己控制的,你只要上好相应的锁即可,如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁;如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁.总之,读的时候上读锁,写的时候上写锁!

看如下程序: 新建6个线程,3个线程用来读,3个线程用来写,

package javaplay.thread.test;

import java.util.Random;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockTest {
  public static void main(String[] args) {
    final Queue3 q3 = new Queue3();
    for (int i = 0; i < 3; i++) {
      new Thread() {
        public void run() {
          while (true) {
            q3.get();
          }
        }
      }.start();
      new Thread() {
        public void run() {
          while (true) {
            q3.put(new Random().nextInt(10000));
          }
        }
      }.start();
    }
  }
}

class Queue3 {
  private Object data = null;// 共享数据,只能有一个线程能写该数据,但可以有多个线程同时读该数据。
  // 读写锁
  ReadWriteLock rwl = new ReentrantReadWriteLock();

  // 相当于读操作
  public void get() {
    rwl.readLock().lock();
    try {
      System.out.println(Thread.currentThread().getName() + " be ready to read data!");
      Thread.sleep((long) (Math.random() * 1000));
      System.out.println(Thread.currentThread().getName() + "have read data :" + data);
    } catch (InterruptedException e) {
      e.printStackTrace();
    } finally {
      rwl.readLock().unlock();
    }
  }

  // 相当于写操作
  public void put(Object data) {
    rwl.writeLock().lock();
    try {
      System.out.println(Thread.currentThread().getName() + " be ready to write data!");
      Thread.sleep((long) (Math.random() * 1000));
      this.data = data;
      System.out.println(Thread.currentThread().getName() + " have write data: " + data);
    } catch (InterruptedException e) {
      e.printStackTrace();
    } finally {
      rwl.writeLock().unlock();
    }
  }
}

读写锁功能很强大!这样可以实现正常的逻辑,如果我们把读写锁相关的代码注释,发现程序正准备写的时候,就有线程读了,发现准备读的时候,有线程去写,这样不符合我们的逻辑;通过Java5的新特新可以很轻松的解决这样的问题;

查看Java API ReentrantReadWriteLock 上面有经典(缓存)的用法,下面是doc里面的伪代码,,它演示的是一个实体的缓存,不是缓存系统,相当于缓存代理,注意volatile的运用:

package javaplay.thread.test;

import java.util.concurrent.locks.ReentrantReadWriteLock;

/*
 * Sample usages. 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):
 */
class CachedData {
  Object data;
  volatile boolean cacheValid;
  final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

  void processCachedData() {
    rwl.readLock().lock();
    if (!cacheValid) {
      // Must release read lock before acquiring write lock
      rwl.readLock().unlock();
      rwl.writeLock().lock();
      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();
      } finally {
        rwl.writeLock().unlock(); // Unlock write, still hold read
      }
    }

    try {
      use(data);
    } finally {
      rwl.readLock().unlock();
    }
  }
}

假设现在多个线程来读了,那第一个线程读到的数据是空的,那它就要写就要填充数据,那么第二个第三个就应该互斥等着,一进来是来读数据的所以上读锁,进来后发现数据是空的,就先把读锁释放再重新获取写锁,就开始写数据,数据写完了,就把写锁释放,把读锁重新挂上,持有读锁时不能同时获取写锁,但拥有写锁时可同时再获取读锁,自己线程挂的写锁可同时挂读锁的,这就是降级,就是除了读锁和写锁外,还有读写锁也叫更新锁,就是自己即可以读又可以写的锁,也就是在自己拥有写锁还没释放写锁时就获取了读锁就降级为读写锁/更新锁,但是不能在持有读锁时再获取写锁;

基于上面的例子,我们可以实现一个缓存系统:

package javaplay.thread.test;

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

public class CacheDemo {
  private Map<String, Object> cache = new HashMap<>();

  public static void main(String[] args) {

  }

  // 可做到多个线程并必的读 读和写又互斥 系统性能很高
  // 这就是读写锁的价值
  private ReadWriteLock rwl = new ReentrantReadWriteLock();

  public Object getData(String key) {
    rwl.readLock().lock();
    Object value = null;
    try {
      value = cache.get(key);
      if (value == null) {// 避免首次多次查询要加synchronized
        rwl.readLock().unlock();
        rwl.writeLock().lock();
        try {
          if (value == null) // 就算第二个第三个线程进来时也不用再写了 跟伪代码相同原理
            value = "aaa";// 实际去query db
        } finally {
          rwl.writeLock().unlock();
        }
        rwl.readLock().lock();
      }
    } finally {
      rwl.readLock().unlock();
    }
    return value;
  }
}
错误之处:没有把不存在的值put;要用get(key)来判空

感谢大家对我们的支持。

(0)

相关推荐

  • 举例说明Java多线程编程中读写锁的使用

    以下示例为 java api并发库中 ReentrantReadWriteLock自带的实例,下面进行解读 class CachedData { Object data; volatile boolean cacheValid; ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); void processCachedData() { rwl.readLock().lock();//@1 if (!cacheValid) { //

  • Java并发编程之重入锁与读写锁

    重入锁 重入锁,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁.重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁阻塞,该特性的实现需要解决以下两个问题. 1.线程再次获取锁.锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取. 2.锁的最终释放.线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁.锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经成功释放

  • Java并发编程之显示锁ReentrantLock和ReadWriteLock读写锁

    在Java5.0之前,只有synchronized(内置锁)和volatile. Java5.0后引入了显示锁ReentrantLock. ReentrantLock概况 ReentrantLock是可重入的锁,它不同于内置锁, 它在每次使用都需要显示的加锁和解锁, 而且提供了更高级的特性:公平锁, 定时锁, 有条件锁, 可轮询锁, 可中断锁. 可以有效避免死锁的活跃性问题.ReentrantLock实现了 Lock接口: 复制代码 代码如下: public interface Lock {  

  • java多线程-读写锁原理

    Java5 在 java.util.concurrent 包中已经包含了读写锁.尽管如此,我们还是应该了解其实现背后的原理. 读/写锁的 Java 实现(Read / Write Lock Java Implementation) 读/写锁的重入(Read / Write Lock Reentrance) 读锁重入(Read Reentrance) 写锁重入(Write Reentrance) 读锁升级到写锁(Read to Write Reentrance) 写锁降级到读锁(Write to

  • Java多线程编程之读写锁ReadWriteLock用法实例

    读写锁:分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由jvm自己控制的,你只要上好相应的锁即可.如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁:如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁.总之,读的时候上读锁,写的时候上写锁! 三个线程读数据,三个线程写数据示例: 可以同时读,读的时候不能写,不能同时写,写的时候不能读. 读的时候上读锁,读完解锁:写的时候上写锁,写完解锁. 注意finally解锁. package com.ljq.test.th

  • Java多线程编程中线程锁与读写锁的使用示例

    线程锁Lock Lock  相当于 当前对象的 Synchronized import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /* * Lock lock = new ReentrantLock(); * lock.lock(); lock.unLock(); * 类似于 synchronized,但不能与synchronized 混用 */ public class L

  • Java编程读写锁详解

    ReadWriteLock也是一个接口,提供了readLock和writeLock两种锁的操作机制,一个资源可以被多个线程同时读,或者被一个线程写,但是不能同时存在读和写线程. 基本规则: 读读不互斥 读写互斥 写写互斥 问题: 既然读读不互斥,为何还要加读锁 答: 如果只是读,是不需要加锁的,加锁本身就有性能上的损耗 如果读可以不是最新数据,也不需要加锁 如果读必须是最新数据,必须加读写锁 读写锁相较于互斥锁的优点仅仅是允许读读的并发,除此之外并无其他. 结论: 读写锁能够保证读取数据的 严格

  • java并发编程StampedLock高性能读写锁详解

    目录 一.读写锁 二.悲观读锁 三.乐观读 一.读写锁 在我的<java并发编程>上一篇文章中为大家介绍了<ReentrantLock读写锁>,ReentrantReadWriteLock可以保证最多同时有一个线程在写数据,或者可以同时有多个线程读数据,但读写不能同时进行. 比如你正在做的是日志,有一个线程正在做写操作,但是在写日志的时候你可能需要把日志集中转移到集中管理日志服务,但是此时读线程不能读数据(因为无法获取读锁).面对这个需求,ReentrantReadWriteLoc

  • GO语言并发编程之互斥锁、读写锁详解

    在本节,我们对Go语言所提供的与锁有关的API进行说明.这包括了互斥锁和读写锁.我们在第6章描述过互斥锁,但却没有提到过读写锁.这两种锁对于传统的并发程序来说都是非常常用和重要的. 一.互斥锁 互斥锁是传统的并发程序对共享资源进行访问控制的主要手段.它由标准库代码包sync中的Mutex结构体类型代表.sync.Mutex类型(确切地说,是*sync.Mutex类型)只有两个公开方法--Lock和Unlock.顾名思义,前者被用于锁定当前的互斥量,而后者则被用来对当前的互斥量进行解锁. 类型sy

  • java编程学习输入输出详解看完快速上手

    目录 一.输出到控制台 二.从键盘输入 1.读取一个字符(了解) 2.Scanner 三.循环读取 总结 一.输出到控制台 基本语法 public static void main(String[] args) { System.out.println("输出且换行"); System.out.print("输出且不换行");//print和println的区别就是c语言中printf加不加\n的区别 System.out.printf("%d\n&quo

  • java编程数据类型全面详解教程新手必入

    目录 前言 变量和类型 1 整形int 2 长整形long 3 双精度浮点型(重点) 4 单精度浮点型 5 字符型 6 字节类型 7 短整型 8 布尔类型 9 字符串类型(重点) 10 变量的作用域 11 变量的命名规则 硬性指标: 软性指标: 12 常量 13 强制类型转换 14 理解数值提升 15 int和String类型的相互转换 总结 前言 大家好呀!这是笔者的java板块,今天起笔者开始了java部分的相关学习,会不定期更新java的文章. 提示:以下是本篇文章正文内容,下面案例可供参

  • Java多线程读写锁ReentrantReadWriteLock类详解

    目录 ReentrantReadWriteLock 读读共享 写写互斥 读写互斥 源码分析 写锁的获取与释放 读锁的获取与释放 参考文献 真实的多线程业务开发中,最常用到的逻辑就是数据的读写,ReentrantLock虽然具有完全互斥排他的效果(即同一时间只有一个线程正在执行lock后面的任务),这样做虽然保证了实例变量的线程安全性,但效率却是非常低下的.所以在JDK中提供了一种读写锁ReentrantReadWriteLock类,使用它可以加快运行效率. 读写锁表示两个锁,一个是读操作相关的锁

  • Java中读写锁ReadWriteLock的原理与应用详解

    目录 什么是读写锁? 为什么需要读写锁? 读写锁的特点 读写锁的使用场景 读写锁的主要成员和结构图 读写锁的实现原理 读写锁总结 Java并发编程提供了读写锁,主要用于读多写少的场景,今天我就重点来讲解读写锁的底层实现原理 什么是读写锁? 读写锁并不是JAVA所特有的读写锁(Readers-Writer Lock)顾名思义是一把锁分为两部分:读锁和写锁,其中读锁允许多个线程同时获得,因为读操作本身是线程安全的,而写锁则是互斥锁,不允许多个线程同时获得写锁,并且写操作和读操作也是互斥的. 所谓的读

  • 详解Java ReentrantReadWriteLock读写锁的原理与实现

    目录 概述 原理概述 加锁原理 图解过程 源码解析 解锁原理 图解过程 源码解析 概述 ReentrantReadWriteLock读写锁是使用AQS的集大成者,用了独占模式和共享模式.本文和大家一起理解下ReentrantReadWriteLock读写锁的实现原理.在这之前建议大家阅读下下面3篇关联文章: 深入浅出理解Java并发AQS的独占锁模式 深入浅出理解Java并发AQS的共享锁模式 通俗易懂读写锁ReentrantReadWriteLock的使用 原理概述 上图是ReentrantR

  • Java并发编程-volatile可见性详解

    前言 要学习好Java的多线程,就一定得对volatile关键字的作用机制了熟于胸.最近博主看了大量关于volatile的相关博客,对其有了一点初步的理解和认识,下面通过自己的话叙述整理一遍. 有什么用? volatile主要对所修饰的变量提供两个功能 可见性 防止指令重排序 <br>本篇博客主要对volatile可见性进行探讨,以后发表关于指令重排序的博文. 什么是可见性? 把JAVA内存模型(JMM)展示得很详细了,简单概括一下 1.每个Thread有一个属于自己的工作内存(可以理解为每个

  • Java aop面向切面编程(aspectJweaver)案例详解

    面向切面编程的目的就是:在不改变别人的代码的前提下,在别人代码方法执行前或后,执行(切入自己的逻辑) 准备:idea+maven+aspectjweaver-1.8.9.jar 结构图: pom.xml内容 <dependencies> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.8.9&

随机推荐