并发编程体系-ReentrantLock

本文详细解读了AQS框架如何利用state属性和CAS操作实现资源管理和线程同步,重点介绍了ReentrantLock的特性、加锁解锁机制以及读写锁的应用。比较了ReentrantLock与synchronized的差异,涵盖公平锁、非公平锁、条件变量和选择性通知等内容。
摘要由CSDN通过智能技术生成

AQS

 AbstractQueuedSynchronizer,AQS是⼀个⽤来构建锁和同步器的框架,使⽤AQS能简单且⾼效地构造出应⽤⼴泛的⼤量的同步器。
  1. AQS的特点:
    1. 用 state 属性来表示资源的状态(分独占模式和共享模式),用CAS来设置state的状态,独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源
    2. 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList
    3. 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet

  2. AQS核心思想:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的⼯作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占⽤,那么就需要⼀套线程阻塞等待以及被唤醒时锁分配(CLH队列锁)的机制,即将暂时获取不到锁的线程加⼊到队列中。

    CLH队列是⼀个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成⼀个CLH锁队列的⼀个结点(Node)来实现锁的分配。
    

在这里插入图片描述

  1. AQS两种方式
    1. 独占方式(Exclusive ):只有⼀个线程能执⾏,如ReentrantLock。⼜可分为公平锁和⾮公平锁
    2. 共享方式(share):多个线程可同时执⾏

ReentrantLock

  1. ReentranLock的特点
    1. 可重入锁。和synchronized一样,都是可重入锁。能重复获得已持有的锁。
    原理:当发生重入时,首先判断线程是不是占用的线程,如果是就将state状态+1,如果释放锁,就将state状态-1.当状态是0时,就将锁的owner清除。
    2. 可中断。通过lock.lockInterruptibly()来实现这个功能。
    3. 锁超时。在请求锁时,如果其他线程一直持有锁不释放,不会一直等待,可以在一段时间后放弃等待,避免线程无限制等待下去,通过lock.tryLock()来实现
    4. 可实现公平锁和非公平锁。默认是非公平锁,可以通过new ReentrantLock(true)来实现公平锁
    原理:非公平锁遇到state=0时会直接去CAS抢占,公平锁会先判断队列中有没有线程,顺序执行
    5. 条件变量。 很大一个程度上是为了解决Object.wait/notify/notifyAll难以使用的问题。实现了“选择性通知”,通过Condition c1=lock.newCondition();来实现选择性通知功能,这样在调用signalAll()方法时,只会唤醒Condition具体实例的线程,不会唤醒全部线程。
    await原理、signal原理在下面。
  2. 非公平锁的加锁原理
    1. 如果第一次加锁,通过if(compareAndSetState(0,1))来将state状态修改为1,并设置线程为当前独占线程
    2. 如果加锁失败,即尝试CAS将state由0改1结果失败。那么就进入tryacquire()方法中,该方法还会再进行一次尝试加锁,如果还失败,那么将线程关联到一个Node对象上,同时在死循环里判断当前线程的节点的前驱节点是不是头结点,如果是就再尝试获取一次锁,如果此时还是获取不到锁,通过ShouldParkAfterAcquire()将前驱节点的waitStatus设置为 -1 。-1表示节点有责任唤醒后继节点。同时再进行一次循环,尝试获得锁,将线程打断进入阻塞状态。这个过程中如果是第二个节点会进行四次重试,其他的三次。
    在这里插入图片描述
  3. 解锁竞争流程
    1. 线程释放锁,进入tryRelease流程,将非公平锁的独占线程设置为null,状态设置为0
    2. 如果竞争成功,判断队列不为null,同时头结点的waitSet=-1,就唤醒最近的一个Node,恢复其运行(在加锁流程中阻塞的位置开始),成功后将原来的头结点断开连接,将当前线程的节点设置为NULL并设为头结点。
    3. 如果竞争失败,即唤醒的线程竞争不过其他线程,那么他就需要重新加入队列
    在这里插入图片描述
  4. await原理
    1. 首先当前线程持有锁,调用await,进入 ConditionObject(等待队列) 的 addConditionWaiter 流程,同时在ConditionObject中创建新的Node且状态为-1,关联当前线程,加入等待队列尾部。
    2. 接下来调用线程的fullyRelease流程,将他的锁全部释放,为什么是fullyRelease,因为可能有重入锁。
    3. 让队列中其他线程来竞争锁,并将自己阻塞。
      在这里插入图片描述
  5. signal原理
    1. 当前运行线程来唤醒 等待线程,取出等待队列中的第一个线程的Node,将这个Node加入到AQS队列尾部,并将要加入的线程的waitstatus从-2设置为0,将原来AQS队列尾部的Node的waitstatus从0改为-1,等待当前线程释放自己的锁,执行unpark流程。
    在这里插入图片描述
    在这里插入图片描述

在这里插入图片描述

ReentrantReadWriteLock读写锁

当读操作远远高于写操作时,这时候使用 读写锁 让 读-读 可以并发,提高性能。
注意事项:1.读锁不支持条件变量。2.重入时升级不支持:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待。3.重入时降级支持:即持有写锁的情况下去获取读锁。
在读写锁中,state低16位是写锁状态,高16位是读锁状态,加锁解锁过程和ReentrantLock过程相似。
1. 锁降级的目的:写锁降级成读锁。先持有写锁,再获取读锁,再释放写锁。保证数据的可见性,如果当前线程不获取读锁而直接释放写锁,如果此时有一个线程T获取写锁并修改了数据,会导致当前线程无法感知数据的更新,因为必须等到写锁释放才能知道。
2. 为什么不支持锁升级保证数据的可见性,如果读锁被多个线程获取,任意线程成功获取了写锁并更新数据,其他获取读锁的线程是不可见的。

ReentrantLock和Synchronized的区别

  1. 底层原理。synchronized属于JVM层面,是Java关键字,ReentrantLock属于API层面
  2. 实现方面。synchronized 的实现涉及到锁的升级,具体为无锁、偏向锁、自旋锁、向OS申请重量级锁,ReentrantLock实现则是通过利用CAS(CompareAndSwap)自旋机制保证线程操作的原子性和volatile保证数据可见性以实现锁的功能。
  3. 锁的释放。synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用; ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。一般通过lock()和unlock()方法配合try/finally语句块来完成,使用释放更加灵活。
  4. 是否可中断。synchronized是不可中断类型的锁,除非加锁的代码中出现异常或正常执行完成; ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断。
  5. 是否公平锁。synchronized为非公平锁 ReentrantLock则即可以选公平锁也可以选非公平锁,通过构造方法new ReentrantLock()时传入boolean值。
  6. 选择性通知。Reentrantlock可以结合Condition来实现选择性通知,每次通知都是Condition实例的线程,而synchronized只能唤醒要么一个要么全部线程。
  7. 锁的对象。synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值