并发编程中关于锁的思考

Table of Contents

一 Lock接口

二 AQS队列同步器

2.1 锁是面向使用者的

2.2 同步器面向的是锁的实现者

2.3 同步队列

三 ReentrantLock重入锁

3.1 实现重进入

3.2 公平与非公平获取锁的区别

四 ReentrantReadWriteLock读写锁

4.1 读写确定状态

4.2 写锁的获取与释放

4.3 读锁的获取与释放

4.4 锁降级

五 LockSupport工具

六 Condition工具


一 Lock接口

    锁是用来控制多个线程访问共享资源的方式

synchronized 关键字将会隐式地获取锁,但是锁的获取和释放就被固化了,也就是先获取在释放。

针对特殊场景先获取A锁,在获取B锁,在B锁获得后,释放A锁,同时再去争夺C锁,获得C锁后,在释放B锁同时获得D锁。。。这种场景synchronized 就显的不灵活了

并发编程中关于锁的思考_第1张图片

lock接口相比synchronized的优势

  1. 尝试非阻塞地获取锁    ==》如果这一时刻锁没有被其他线程获取到,则成功获取到
  2. 能被中断的获取锁       ==》 获取到锁的线程可以响应中断,抛出异常,释放锁
  3. 超时获取锁                  ==》在指定的截止时间内未获得锁,返回false

二 AQS队列同步器

AbstractQueueSynchronizer同步器 是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。

核心三个方法-保证状态的改变是安全的

  1. getState();
  2. setState(int newState);
  3. compareAndSetState(int expect, int update)
     

2.1 锁是面向使用者的

它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;

2.2 同步器面向的是锁的实现者

它简化了锁的实现方式,屏蔽了同步状态的管理,线程的排队,等待与唤醒底层操作。

锁和同步器很好的隔离了使用者和实现者所需关注的领域。

同步器的设计是基于模版方法模式的,同步器可重写的方法

  • tryAcquire(int arg)      独占式获取同步状态
  • tryRelease(int arg)     独占式释放同步状态
  • tryAcquireShared(int arg).     共享式获取同步状态 返回大于0,表示获取成功
  • tryReleaseShared(int arg).    共享式释放同步状态
  • isHeldExclusively().               当前同步器是否在独占模式下被线程占用,一般表示该方法是否被当前线程所读占用

     

2.3 同步队列

       依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。

同步队列中的节点Node用来保存同步状态失败的线程的引用/等待状态/以及前驱和后继节点,节点的属性类型与名称预计描述如下。

并发编程中关于锁的思考_第2张图片

       当一个线程成功地获取到了同步状态,其他线程将无法获取到同步状态,转而被构造成为节点并加入到同步队列中,而这个加入的过程必须是线程安全的。同步器提供了一个compareAndSetTail(Node expect, Node update)。保证设置成功后,当前节点才正式与之前的尾节点建立关联

 

并发编程中关于锁的思考_第3张图片

        上述代码完成了 同步状态获取,节点构造,加入同步队列以及在同步队列中自旋等待的相关工作。
主要逻辑为调用自定义同步器的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态。
1. 如果获取成功,结束操作
2. 如果获取失败,则构造同步节点(独占式Node.EXCLUSIVE)
3. 然后通过addWaiter(Node node)方法将该节点加入到同步队列的尾部,
4. 最后调用acquireQueued(Node node, int arg)方法,使得该节点以死循环的当时获取同步状态,如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现

精髓:使用compareAndSetTail(Node expect, Node update)来确保节点能够被线程安全添加。然后开始自旋操作

 

共享式同步状态获取与释放

通过调用同步器的acquireShared(int arg)方法可以共享式的获取同步状态。
通过调用同步器的releaseShared(int arg) 方法可以释放同步状态。

 


独占式超时获取同步状态

通过调用同步器的doAcquireNanos(int arg, long nanosTimeout)方法可以超时获取同步状态,即在指定的时间段内获取同步状态,如果获取成功则返回true,否则返回false。这是传统的synchronized 所不具备的特性

java5 提供了一个acquireInterruptibly方法,这个方法在等待获取同步状态时,如果当前线程被中断,会立刻返回,并抛出InterrputedException

 

三 ReentrantLock重入锁

可重入的锁,就是能够支持一个线程对资源的重复加锁操作,该锁还支持获取锁时的公平和非公平性的选择。公平机制能够减少“饥饿”发送的概率。

3.1 实现重进入


锁的可重入,实际是在解决两个问题
1. 线程再次获取锁,锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取

2. 锁的最终释放,线程重复n此获取了锁,其它线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而被释放时,计数自减。当计数等于0时表示锁以及成功释放。

并发编程中关于锁的思考_第4张图片

加锁时会判断是否为当前线程请求,如果是则对state进行自增运算
解锁是和加锁对立的,也就是加锁N次就需要解锁N次,最终判断的条件是比较状态是否为0
 

3.2 公平与非公平获取锁的区别

公平锁在当 getState()==0 时多了一个hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早的请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。 

vmstat统计测试运行时系统线程上下文切换的次数来看,
fair:nonfair  耗时 93:1
fair:nonfair  上下文切换次数 133:1

 

四 ReentrantReadWriteLock读写锁

      通过维护双锁的机制,一个读锁 一个写锁,增强并发性 readLock() && writeLock()。读写锁将一个32位的整型变量按位切割使用,前16位表示读,后16位表示写

并发编程中关于锁的思考_第5张图片

 

4.1 读写确定状态

假设当前同步状态为S,

写操作等于S&0x0000FFFF将高16位全部抹去,读状态等于S>>>16;
写操作增加1 等于 S+1
读状态增加1 等于S+(1<<16) 也就是 S+0x00010000

4.2 写锁的获取与释放

获取:只有等待其它读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦获取,则其它的读写线程的后续访问均被阻塞(自身除外,支持可重入)

释放:每次释放减少写状态,当写状态为0时表示写锁已被释放,从而等待的读写线程能够访问读写锁,同时前次写线程的修改对后续读写线程可见。

4.3 读锁的获取与释放

获取:读锁是一个支持重进入的共享锁,能够支持多个线程同时获取,在没有其它写线程访问 也就是 写状态为0时,读锁总被成功的获取。如果其它线程获得写锁,则获取失败,进入等待状态。

释放:每次释放均减少读状态

4.4 锁降级

这里指的是写锁降低成为读锁。如果一个线程先是写锁,释放后再获得读锁,这种分段完成的操作不能称之为锁降级。是把持住写锁,基础上再获取到读锁,随后释放先前拥有的写锁的过程。

在持有写锁时,获得读锁。这种操作是必要的。目的是为了保证数据的可见性,

如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程T获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更改。这样保证了数据真实有效。

 

五 LockSupport工具

定义了一组的公共静态方法,提供了最基本的线程阻塞和唤醒功能

park()   阻塞当前线程,如果调用unpark(Thread thread) 方法或者当前线程被中断,才能从park方法返回
parkNanos(long nanos)  阻塞当前线程,在park的基础上增加了 不超过nanos纳秒的限制
parkUntil(long deadline)  阻塞当前线程,知道deadline时间(从1970到deadline的毫秒数)
unpark(Thread thread)   唤醒处于阻塞的线程

 

六 Condition工具

任何对象拥有一组监视器方法 主要包括 wait(),wait(long timeout),notify(),notifyAll() 这些方法与synchronized同步关键字配合,可以实现等待/通知模式

Condition 与 Lock配合实现等待/通知模式 Condition依赖Lock

并发编程中关于锁的思考_第6张图片

await()   前线程会释放锁并在此等待,而其它线程调用signal()方法,通知当前线程后,当前线程才从await()方法返回,并且在返回前已经获取了锁
signal()   唤醒单个Condition上的线程,能够从等待方法返回的线程必须获得与Condition的锁
signalAll() 唤醒所有等待在Condition上的线程,能够从等待方法返回的线程必须获得与Condition相关联的锁

添加和删除方法中使用while循环而非if判断,目的是防止过早或意外的通知,只有条件符合才能够退费循环。回想之前提到的等待/通知的经典范式,二者是非常类似的

你可能感兴趣的:(java)