Java线程安全Lock、ReentrantLock、ReentrantReadWriteLock

2022-07-13 09:57:29

转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/120750932
本文出自【赵彦军的博客】

Java线程安全StampedLock
Java线程安全Lock、ReentrantLock、ReentrantReadWriteLock
Java线程安全集合总结
Java原子操作Atomic

前言

java5之后,并发包中新增了Lock接口(以及相关实现类)用来实现锁的功能,它提供了与synchronized关键字类似的同步功能。

既然有了synchronized这种内置的锁功能,为何要新增Lock接口?先来想象一个场景:手把手的进行锁获取和释放,先获得锁A,然后再获取锁B,当获取锁B后释放锁A同时获取锁C,当锁C获取后,再释放锁B同时获取锁D,以此类推,这种场景下,synchronized关键字就不那么容易实现了,而使用Lock却显得容易许多。

Lock

Lock 是一个接口

publicinterfaceLock{voidlock();voidlockInterruptibly()throwsInterruptedException;booleantryLock();booleantryLock(long time,TimeUnit unit)throwsInterruptedException;voidunlock();ConditionnewCondition();}

ReentrantLock

publicclassReentrantLockimplementsLock,java.io.Serializable{}

使用如下:

publicclassLockTest{Lock lock=newReentrantLock();publicvoidrun(){//获取锁
        lock.lock();//doSomething//释放锁
        lock.unlock();}}

使用起来,也非常简单,首先lock.lock(); 获取锁,然后去执行自己的逻辑,最后调用lock.unlock(); 释放锁。

但是这里有个问题,如果我们的逻辑发生异常,那就永远无法释放锁,所以我们优化一下,把释放锁的逻辑放在finally 块中,如下

publicclassLockTest{Lock lock=newReentrantLock();publicvoidrun(){//获取锁
        lock.lock();try{//doSomething}catch(Exception e){}finally{//释放锁
            lock.unlock();}}}

公平锁/非公平锁

  • 非公平锁:如果同时还有另一个线程进来尝试获取,那么有可能会让这个线程抢先获取;

  • 公平锁:如果同时还有另一个线程进来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁。

ReentrantLock 主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似。

  • CAS:Compare and Swap,比较并交换。CAS有3个操作数:内存值V、预期值A、要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。该操作是一个原子操作,被广泛的应用在Java的底层实现中。在Java中,CAS主要是由sun.misc.Unsafe这个类通过JNI调用CPU底层指令实现

ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似。

AbstractQueuedSynchronizer 简称AQS

我们来看一下ReentrantLock 构造函数

publicReentrantLock(){
        sync=newNonfairSync();}/**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */publicReentrantLock(boolean fair){
        sync= fair?newFairSync():newNonfairSync();}

构造支持传入fair 来指定是否是公平锁

  • FairSync() 公平锁
  • NonfairSync() 非公平锁

如果使用无参构造函数,则是非公平锁。

超时机制

ReetrantLock的tryLock(long timeout, TimeUnit unit) 提供了超时获取锁的功能。它的语义是在指定的时间内如果获取到锁就返回true,获取不到则返回false。这种机制避免了线程无限期的等待锁释放。

可重入锁

  • 可重入锁。可重入锁是指同一个线程可以多次获取同一把锁。ReentrantLock和synchronized都是可重入锁。

  • 可中断锁。可中断锁是指线程尝试获取锁的过程中,是否可以响应中断。synchronized是不可中断锁,而ReentrantLock则提供了中断功能。

  • 公平锁与非公平锁。公平锁是指多个线程同时尝试获取同一把锁时,获取锁的顺序按照线程达到的顺序,而非公平锁则允许线程“插队”。synchronized是非公平锁,而ReentrantLock的默认实现是非公平锁,但是也可以设置为公平锁。

读写锁 ReentrantReadWriteLock

现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。

在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。

针对这种场景,JAVA的并发包提供了读写锁ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁,描述如下:

线程进入读锁的前提条件:

  • 没有其他线程的写锁,

  • 没有写请求或者有写请求,但调用线程和持有锁的线程是同一个。

线程进入写锁的前提条件:

  • 没有其他线程的读锁

  • 没有其他线程的写锁

而读写锁有以下三个重要的特性:

(1)公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。

(2)重进入:读锁和写锁都支持线程重进入。

(3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。

源码结构

publicclassReentrantReadWriteLockimplementsReadWriteLock,java.io.Serializable{/** 读锁 */privatefinalReentrantReadWriteLock.ReadLock readerLock;/** 写锁 */privatefinalReentrantReadWriteLock.WriteLock writerLock;finalSync sync;/** 使用默认(非公平)的排序属性创建一个新的 ReentrantReadWriteLock */publicReentrantReadWriteLock(){this(false);}/** 使用给定的公平策略创建一个新的 ReentrantReadWriteLock */publicReentrantReadWriteLock(boolean fair){
        sync= fair?newFairSync():newNonfairSync();
        readerLock=newReadLock(this);
        writerLock=newWriteLock(this);}/** 返回用于写入操作的锁 */publicReentrantReadWriteLock.WriteLockwriteLock(){return writerLock;}/** 返回用于读取操作的锁 */publicReentrantReadWriteLock.ReadLockreadLock(){return readerLock;}abstractstaticclassSyncextendsAbstractQueuedSynchronizer{}staticfinalclassNonfairSyncextendsSync{}staticfinalclassFairSyncextendsSync{}publicstaticclassReadLockimplementsLock,java.io.Serializable{}publicstaticclassWriteLockimplementsLock,java.io.Serializable{}}

1、类的继承关系

publicclassReentrantReadWriteLockimplementsReadWriteLock,java.io.Serializable{}

说明:可以看到,ReentrantReadWriteLock实现了ReadWriteLock接口,ReadWriteLock接口定义了获取读锁和写锁的规范,具体需要实现类去实现;同时其还实现了Serializable接口,表示可以进行序列化,在源代码中可以看到ReentrantReadWriteLock实现了自己的序列化逻辑。

2、ReentrantReadWriteLock有五个内部类,五个内部类之间也是相互关联的。内部类的关系如下图所示。

在这里插入图片描述
说明:如上图所示,Sync继承自AQS、NonfairSync继承自Sync类、FairSync继承自Sync类(通过构造函数传入的布尔值决定要构造哪一种Sync实例);ReadLock实现了Lock接口、WriteLock也实现了Lock接口。

总结

  • 在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。

  • 在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。

仔细想想,这个设计是合理的:因为当线程获取读锁的时候,可能有其他线程同时也在持有读锁,因此不能把获取读锁的线程“升级”为写锁;而对于获得写锁的线程,它一定独占了读写锁,因此可以继续让它获取读锁,当它同时获取了写锁和读锁后,还可以先释放写锁继续持有读锁,这样一个写锁就“降级”为了读锁。

综上:

一个线程要想同时持有写锁和读锁,必须先获取写锁再获取读锁;写锁可以“降级”为读锁;读锁不能“升级”为写锁。

示例

publicclassLockTest{ReadWriteLock lock=newReentrantReadWriteLock();Lock readLock= lock.readLock();Lock writeLock= lock.writeLock();}
  • 作者:赵彦军
  • 原文链接:https://blog.csdn.net/zhaoyanjun6/article/details/120750932
    更新时间:2022-07-13 09:57:29