0
点赞
收藏
分享

微信扫一扫

并发-显式锁[老的,有时间我重新整理一下]


并发-显式锁[老的,有时间我重新整理一下]

文章是直接从我本地word笔记粘贴过来的,排版啥的可能有点乱,凑合看吧,有时间我会慢慢整理

(一)什么是显示锁

Lock子类就是显示锁.

首先synchronized是内置锁,

有了synchronized为什么还要Lock?
Java程序是靠synchronized关键字实现锁功能的,使用synchronized关键字将会隐式地获取锁,但是它将锁的获取和释放固化了,也就是先获取再释放。显示锁是我们手动的去声明一个锁,手动的去拿取锁,手动的去释放锁.

synchronized拿锁取锁过程是非常固定化的,没得调用选择,synchronized没有超时机制中断机制等等.必须要拿到锁线程才能往下走.
但是对于显示锁来说,提供了机制就很多,Lock接口是显示锁的共同接口, lock()方法获取锁,然后通过unlock()来释放锁,还提供了可以拿锁的过程中(等待期间)可以中断.

tryLock()尝试拿锁的过程中,如果拿不到tryLock()就会返回false,然后程序员就可以做点其它逻辑,然后再尝试着去拿到锁,这样就可以避免线程长时间阻塞在那里.

tryLock()还可以设置等待时间,如果到了一定时间还拿不到锁,再返回false.

显示锁特性

特性

描述

尝试非阻塞地获取锁

当前线程尝试获取锁.如果这一时刻锁没有被其它线程获取到,则成功获取并持有锁.

能被中断地获取锁

与synchronized不同,获取到的锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会释放

超时获取锁

在指定的截止时间之前获取锁,如果截止时间到了仍旧无法获取锁,则返回.

1.显示锁和synchronized关键字如何使用?

如果没有用到lock接口的尝试获取锁的API,没有用到拿锁的过程中中断的特性,那么你尽量就用synchronized关键字好一点,
因为synchronized关键字被jdk优化的力度非常大,比如jdk1.8的ConcurrentHashMap用的是synchronized关键字,而在1.8之前用的还是lock接口

synchronized的资源消耗比显示锁的消耗少一点,因为synchronized是语言的特性,而Lock接口是一个类,如果用Lock需要实例化对象,就需要消耗内存,所以synchronized资源消耗要少一点.

lock接口的实现类用的比较多的就是可重入锁以及读写锁.

2.Lock的标准用法

一定要这么用,不然会出现问题.

| **public void **test() {

**lock**.lock(); //加锁

**try **{

**age**++; //业务逻辑

/* 但是一定要用finally代码块儿,万一业务逻辑被中断了(抛出了一个异常),

lock.unlock();很可能就不能执行,就无法释放锁*/

} **finally **{

**lock**.unlock(); //解锁

}

}

在finally块中释放锁,目的是保证在获取到锁之后,最终能够被释放。(不管try里面的业务逻辑是否执行成功,在finally的锁一定会被释放)
不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常,异常抛出的同时,也会导致锁无故释放。

3.Lock的常用API

public void lock()
获取锁。
如果该锁没有被另一个线程保持,则获取该锁并立即返回,将锁的保持计数设置为 1。
如果当前线程已经保持该锁,则将保持计数加 1,并且该方法立即返回。
如果该锁被另一个线程保持,则出于线程调度的目的,禁用当前线程,并且在获得锁之前,该线程将一
直处于休眠状态,此时锁保持计数被设置为 1。

指定者:
接口 Lock 中的 lock

lockInterruptibly
public void lockInterruptibly() throws InterruptedException

1)如果当前线程未被中断,则获取锁。

2)如果该锁没有被另一个线程保持,则获取该锁并立即返回,将锁的保持计数设置为 1。

3)如果当前线程已经保持此锁,则将保持计数加 1,并且该方法立即返回。

4)如果锁被另一个线程保持,则出于线程调度目的,禁用当前线程,并且在发生以下两种情况之一以
前,该线程将一直处于休眠状态:
1)锁由当前线程获得;或者

2)其他某个线程中断当前线程。

5)如果当前线程获得该锁,则将锁保持计数设置为 1。
如果当前线程:
1)在进入此方法时已经设置了该线程的中断状态;或者

2)在等待获取锁的同时被中断。

则抛出 InterruptedException,并且清除当前线程的已中断状态。

6)在此实现中,因为此方法是一个显式中断点,所以要优先考虑响应中断,而不是响应锁的普通获取或
重入获取。

指定者: 接口 Lock 中的 lockInterruptibly
抛出: InterruptedException 如果当前线程已中断。

tryLock public boolean tryLock()

仅在调用时锁未被另一个线程保持的情况下,才获取该锁。

1)如果该锁没有被另一个线程保持,并且立即返回 true 值,则将锁的保持计数设置为 1。
即使已将此锁设置为使用公平排序策略,但是调用 tryLock() 仍将 立即获取锁(如果有可用的),
而不管其他线程当前是否正在等待该锁。在某些情况下,此“闯入”行为可能很有用,即使它会打破公
平性也如此。如果希望遵守此锁的公平设置,则使用 tryLock(0, TimeUnit.SECONDS)
,它几乎是等效的(也检测中断)。

2)如果当前线程已经保持此锁,则将保持计数加 1,该方法将返回 true。

3)如果锁被另一个线程保持,则此方法将立即返回 false 值。

指定者:
接口 Lock 中的 tryLock
返回:
如果锁是自由的并且被当前线程获取,或者当前线程已经保持该锁,则返回 true;否则返回
false

关于中断又是一段很长的叙述,先不谈。

1)lock(), 拿不到lock就不罢休,不然线程就一直block。 比较无赖的做法。
2)tryLock(),马上返回,拿到lock就返回true,不然返回false。 比较潇洒的做法。
带时间限制的tryLock(),拿不到lock,就等一段时间,超时返回false。比较聪明的做法。

3)lockInterruptibly()就稍微难理解一些。

先说说线程的打扰机制,每个线程都有一个 打扰 标志。这里分两种情况,

  1. 线程在sleep或wait,join, 此时如果别的进程调用此进程的 interrupt()方法,此线程会被唤醒并被要求处理InterruptedException;(thread在做IO操作时也可能有类似行为,见java thread api)
  2. 此线程在运行中, 则不会收到提醒。但是 此线程的 “打扰标志”会被设置, 可以通过isInterrupted()查看并 作出处理。

lockInterruptibly()和上面的第一种情况是一样的, 线程在请求lock并被阻塞时,如果被interrupt,则“此线程会被唤醒并被要求处理InterruptedException”。并且如果线程已经被interrupt,再使用lockInterruptibly的时候,此线程也会被要求处理interruptedException

并发-显式锁[老的,有时间我重新整理一下]_封装

AQS

全称AbstractQueuedSynchronizer

AQS是一个同步工具,是JUC包的核心,显示锁要解决线程的同步问题,假如说10个人去银行取钱,如果没有排队机制的话,那么银行就会变得很混乱,所以要有个同步工具去实现一个线程的同步,所以AQS就是这样一个同步组件.如果你搞懂AQS,那么JUC包下的绝大多数同步的类你都能掌握.

AQS有两个功能:

独占(互斥,在同一时刻只能有一个线程获取锁.)

共享(读写锁的读锁, 共享意思是允许多个线程同时获取锁)

(一)AQS 的内部实现

AQS 队列内部维护的是一个 FIFO 的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向直接的后继节点和直接前驱节点。所以双向链表可以从任意一个节点开始很方便的访问前驱和后继。

每个 Node 其实是由线程封装,当线程争抢锁失败后会封装成 Node 加入到 AQS队列中去;
当获取锁的线程释放锁以后,会从Node队列头部中唤醒一个阻塞的节点(线程),然后这个节点会尝试获取锁,如果获取成功这个节点就从Node队列里面删除。

并发-显式锁[老的,有时间我重新整理一下]_等待时间_02


Node是抢占锁失败的线程

1.原理

AQS使用一个int类型的成员变量state来表示同步状态,当state>0时表示已经获取了锁,当state = 0时表示释放了锁。它提供了三个方法(getState()、setState(int newState)、compareAndSetState(int expect,int update))来对同步状态state进行操作,当然AQS可以确保对state的操作是安全的。

(二)Node节点的组成

并发-显式锁[老的,有时间我重新整理一下]_封装_03

(三)释放锁以及添加线程对于队列的变化

当出现锁竞争以及释放锁的时候,AQS 同步队列中的节点会发生变化,首先看一下添加节点的场景。

并发-显式锁[老的,有时间我重新整理一下]_封装_04

里会涉及到两个变化

  1. 新的线程封装成 Node 节点追加到同步队列中,设置 prev 节点以及修改当前节点的前置节点的 next 节点指向自己
  2. 通过 CAS 讲 tail 重新指向新的尾部节点head 节点表示获取锁成功的节点,当头结点在释放同步状态时,会唤醒后继节点,如果后继节点获得锁成功,会把自己设置为头结点,节点的变化过程如下

并发-显式锁[老的,有时间我重新整理一下]_封装_05

这个过程也是涉及到两个变化

  1. 修改 head 节点指向下一个获得锁的节点
  2. 新的获得锁的节点,将 prev 的指针指向 null设置 head 节点不需要用 CAS,原因是设置 head 节点是由获得锁的线程来完成的,而同步锁只能由一个线程获得,所以不需要 CAS 保证,只需要把 head 节点设置为原首节点的后继节点,并且断开原 head 节点的 next 引用即可

(四)AQS同步组件

CountDownLatch Semaphore CyclicBarrier ReentrantLock Condition FutureTask

(五)AQS主要方法

getState():返回同步状态的当前值;

setState(int newState):设置当前同步状态;

compareAndSetState(int expect, int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性;

tryAcquire(int arg):独占式获取同步状态,获取同步状态成功后,其他线程需要等待该线程释放同步状态才能获取同步状态;

tryRelease(int arg):独占式释放同步状态;

tryAcquireShared(int arg):共享式获取同步状态,返回值大于等于0则表示获取成功,否则获取失败;

tryReleaseShared(int arg):共享式释放同步状态;

isHeldExclusively():当前同步器是否在独占式模式下被线程占用,一般该方法表示是否被当前线程所独占;

acquire(int arg):独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用可重写的tryAcquire(int arg)方法;

acquireInterruptibly(int arg):与acquire(int arg)相同,但是该方法响应中断,当前线程为获取到同步状态而进入到同步队列中,如果当前线程被中断,则该方法会抛出InterruptedException异常并返回;

tryAcquireNanos(int arg,long nanos):超时获取同步状态,如果当前线程在nanos时间内没有获取到同步状态,那么将会返回false,已经获取则返回true;

acquireShared(int arg):共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式的主要区别是在同一时刻可以有多个线程获取到同步状态;

acquireSharedInterruptibly(int arg):共享式获取同步状态,响应中断;

tryAcquireSharedNanos(int arg, long nanosTimeout):共享式获取同步状态,增加超时限制;

release(int arg):独占式释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒;

releaseShared(int arg):共享式释放同步状态;

ReentrantLock(可重入锁)

(一)锁的可重入

一个线程如果获取了ReentrantLock这个锁,那么他在下一次进入这个方法的时候可以继续拿到同一把锁.
当一个线程递归调用的时候如果不能实现可重入的话,意味着这个线程递归再进入这个方法我就拿不到了.就会自己把自己锁死

简单地讲就是:“同一个线程对于已经获得到的锁,可以多次继续申请到该锁的使用权”。而synchronized关键字隐式的支持重进入,比如一个synchronized修饰的递归方法,在方法执行时,执行线程在获取了锁之后仍能连续多次地获得该锁。ReentrantLock在调用lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞。

(二)公平和非公平锁(面试会问)

锁的公平和非公平

1.公平锁:是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。
2.非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或者饥饿现象

公平锁和非公平锁区别

公平锁:Threads acquire a fair lock in which they requested
公平锁:就是很公平,在并发坏境中.每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁.否则就会加入到等待队列中.以后会按照FIFO的规则从队列中取到自己。

非公平锁:a nonfair lock permis barging:threads requesting a lock can jump ahead of the queue of waiting threads if the lock
happens to be available when it is requested.
非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。

ReentrantLock和synchronized是公平锁还是非公平锁?

synchronized 是非公平锁(出于性能考虑)

ReentrantLock 通过构造方法指定该锁是否为公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大.

Lock lock = new ReentrantLock(); //非公平锁
Lock lock = new ReentrantLock(true); //公平锁

公平的获取锁,也就是等待时间最长的线程最优先获取锁,也可以说锁获取是顺序的。 ReentrantLock提供了一个构造函数,能够控制锁是否是公平的。事实上,公平的锁机制往往没有非公平的效率高。

在激烈竞争的情况下,非公平锁的性能高于公平锁的性能的一个原因是:
在恢复一个被挂起的线程与该线程真正开始运行之间存在着严重的延迟。假设线程A持有一个锁,并且线程B请求这个锁。由于这个锁已被线程A持有,因此B将被挂起。当A释放锁时,B将被唤醒,因此会再次尝试获取锁。与此同时,如果C也请求这个锁,那么C很可能会在B被完全唤醒之前获得、使用以及释放这个锁。这样的情况是一种“双赢”的局面:B获得锁的时刻并没有推迟,C更早地获得了锁,并且吞吐量也获得了提高。

(三)API

// 创建一个 ReentrantLock ,默认是“非公平锁”。
ReentrantLock()
// 创建策略是fair的 ReentrantLock。fair为true表示是公平锁,fair为false表示是非公平锁。
ReentrantLock(boolean fair)

// 查询当前线程保持此锁的次数。
int getHoldCount()
// 返回目前拥有此锁的线程,如果此锁不被任何线程拥有,则返回 null。
protected Thread getOwner()
// 返回一个 collection,它包含可能正等待获取此锁的线程。
protected Collection getQueuedThreads()
// 返回正等待获取此锁的线程估计数。
int getQueueLength()
// 返回一个 collection,它包含可能正在等待与此锁相关给定条件的那些线程。
protected Collection getWaitingThreads(Condition condition)
// 返回等待与此锁相关的给定条件的线程估计数。
int getWaitQueueLength(Condition condition)
// 查询给定线程是否正在等待获取此锁。
boolean hasQueuedThread(Thread thread)
// 查询是否有些线程正在等待获取此锁。
boolean hasQueuedThreads()
// 查询是否有些线程正在等待与此锁有关的给定条件。
boolean hasWaiters(Condition condition)
// 如果是“公平锁”返回true,否则返回false。
boolean isFair()
// 查询当前线程是否保持此锁。
boolean isHeldByCurrentThread()
// 查询此锁是否由任意线程保持。
boolean isLocked()
// 获取锁。
void lock()
// 如果当前线程未被中断,则获取锁。
void lockInterruptibly()
// 返回用来与此 Lock 实例一起使用的 Condition 实例。
Condition newCondition()
// 仅在调用时锁未被另一个线程保持的情况下,才获取该锁。
boolean tryLock()
// 如果锁在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁。
boolean tryLock(long timeout, TimeUnit unit)
// 试图释放此锁。
void unlock()

(四)实现原理

1.lock方法

并发-显式锁[老的,有时间我重新整理一下]_等待时间_06

ReentrantLock调用lock方法会调用 ReentrantLock.NonfairSync?lock (这个是非公平锁的lock方法),
会调用AbstractQueuedSynchronizer?compareAndSetState更新一个state状态值为1(cas操作),有两种情况

1.如果更新成功(说明获取到锁了)就给当前线程设置一个独占锁线程直接返回

2.如果更新失败说明没有获取到锁
然后调用AbstractQueuedSynchronizer?acquire (这是AQS里面的实现),调用 ReentrantLock.NonfairSync?tryAcquire调用ReentrantLock.Sync?nonfairTryAcquire. 这个nonfairTryAcquire方法是boolean类型的,true代表获得锁成功,false代表获取锁失败. 如果在这里返回了false的话,AQS会给当前的线程封装成Node节点加入到同步队列的尾节点进行排队

nonfairTryAcquire内部逻辑是先判断state是否为0(0代表当前是无锁状态),如果是0的话又会通过cas尝试给state设置为1(设置为1意思是给当前线程设置为有锁状态)如果设置成功了就返回true,这么做的意义是因为B第一次获取锁的时候.假如说B线程没有获取成功,进入这个nonfairTryAcquire方法了,获取A线程已经释放锁了呢?所以就又尝试一次获取锁.
如果设置失败了,此时state不等于0说明已经是有锁状态了,判断当前线程是不是独占锁线程(就是当前线程是不是持有锁的线程),如果是的话,就给state值加1(目的表示锁的可重入次数,当state为0的时候说明没有锁,state大于0的时候说明有锁),

| **final boolean nonfairTryAcquire_(int acquires) {

final Thread current = Thread.currentThread()_**;

int c = getState();

**if (c == 0_) {

_if _(_**compareAndSetState**_(_**0, acquires**_)) { _**//如果设置成功

setExclusiveOwnerThread**_(_**current**_)_**; //给当前线程标示为独占锁线程

**return true**;

**_}

}

_**//上个if判断失败了,说明有锁了,然后判断当前线程是否是独占锁线程,

//如果是的话就给state值基于cas加1(重入锁),

**else if (current == getExclusiveOwnerThread_()) {

_int **nextc = c + acquires;

**if _(_**nextc < 0**_) _**// overflow

**throw new **Error**_(_"Maximum lock count exceeded"_)_**;

setState**_(_**nextc**_)_**;

**return true**;

**_}

_return false**;

}

如果nonfairTryAcquire返回的false,说明没有获取到锁,就会给当前独占锁封装成Node然后走AbstractQueuedSynchronizer?acquireQueued 如果Node是头节点的话再尝试获取锁,如果恰巧别的线程释放了锁呢,抢到锁成功就返回true,如果获取锁失败了会给线程挂起.

2.unlock

unlock调用AbstractQueuedSynchronizer?release 就是给 state变量减1,当state变为0的时候说明这个线程彻底释放锁了,此时唤醒Node队列队列头节点,头结点被唤醒之后开始出来抢占获取锁.

3.公平锁和非公平锁区别

非公平锁是上来就直接抢锁,公平锁是检查是否有线程在等待获取锁,如果有的话就给当前线程封装成Node节点放入到Node队列尾部,等待排队获取锁

非公平锁获取锁细节代码

| **final boolean nonfairTryAcquire_(int acquires) {

final Thread current = Thread.currentThread()_**;

int c = getState();

**if (c == 0_) {

_if _(_**compareAndSetState**_(_**0, acquires**_)) { _**//如果设置成功

setExclusiveOwnerThread**_(_**current**_)_**; //给当前线程标示为独占锁线程

**return true**;

**_}

}

_**//上个if判断失败了,说明有锁了,然后判断当前线程是否是独占锁线程,

//如果是的话就给state值基于cas加1(重入锁),

**else if (current == getExclusiveOwnerThread_()) {

_int **nextc = c + acquires;

**if _(_**nextc < 0**_) _**// overflow

**throw new **Error**_(_"Maximum lock count exceeded"_)_**;

setState**_(_**nextc**_)_**;

**return true**;

**_}

_return false**;

}

**final boolean nonfairTryAcquire_(int acquires) {

_final **Thread current = Thread._currentThread_**_()_**;

**int **c = getState**_()_**;

**if _(_**c == 0**_) {

_if _(_**compareAndSetState**_(_**0, acquires**_)) {

_**setExclusiveOwnerThread**_(_**current**_)_**;

**return true**;

**_}

}

_else if _(_**current == getExclusiveOwnerThread**_()) {

_int **nextc = c + acquires;

**if _(_**nextc < 0**_) _**// overflow

**throw new **Error**_(_"Maximum lock count exceeded"_)_**;

setState**_(_**nextc**_)_**;

**return true**;

**_}

_return false**;

} | |

公平锁获取锁细节

| **protected final boolean tryAcquire_(int acquires) {

_final **Thread current = Thread._currentThread_**_()_**;

**int **c = getState**_()_**;

**if _(_**c == 0**_) {

_**// 如果有获取失败的锁在排队了,并且当前线程不是持有锁的线程的话,

//就直接给当前线程放到Node队列里面尾部,让它排队

**if _(_**!hasQueuedPredecessors**_() _**&&

compareAndSetState**_(_**0, acquires**_)) {

_**setExclusiveOwnerThread**_(_**current**_)_**;

**return true**;

**_}

}

_else if _(_**current == getExclusiveOwnerThread**_()) {

_int **nextc = c + acquires;

**if _(_**nextc < 0**_)

_throw new **Error**_(_"Maximum lock count exceeded"_)_**;

setState**_(_**nextc**_)_**;

**return true**;

**_}

_return false**;

**_}

}_**

Condition接口

在使用显示锁,怎么使用等待和通知,就需要用Condition接口

任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait()、wait(long timeout)、notify()以及notifyAll()方法,通过这些方法,我们可以实现线程间通信与协作(也称为等待唤醒机制),如生产者-消费者模式,而且这些方法必须配合着synchronized关键字使用,可以实现等待/通知模式。

Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式。
与synchronized的等待唤醒机制相比Condition具有更多的灵活性以及精确性,这是因为notify()在唤醒线程时是随机(同一个锁),而Condition则可通过多个Condition实例对象建立更加精细的线程控制,也就带来了更多灵活性了,我们可以简单理解为以下两点

通过Condition能够精细的控制多线程的休眠与唤醒。
对于一个锁,我们可以为多个线程间建立不同的Condition。

(一)API

Condition是一个接口类,其主要方法如下:

public interface Condition { /** * 使当前线程进入等待状态直到被通知(signal)或中断, 当前线程将进行运行状态且从await()方法返回的情况,包括: * 当其他线程调用singal()或singalAll()方法时,该线程将被唤醒 * 当其他线程调用interrupt()方法中断当前线程 如果当前等待线程从await()方法返回,那么表明该线程已经获取了Condition对象所对应的锁 * await()相当于synchronized等待唤醒机制中的wait()方法 / void await() throws InterruptedException; /* 当前线程进入等待状态,直到被通知,从方法名称上可以看出该方法对中断不敏感 */ void awaitUninterruptibly(); /*当前线程进入等待状态直到被通知,中断或者超时,返回值表示剩余时间,如果在nanosTimeout纳秒之前被被唤醒, 那么返回值就是(nanosTimeout-实际耗时.)如果返回值是0或者负数,那么可以认定已经超时了 */ long awaitNanos(long nanosTimeout) throws InterruptedException; /*await()可以理解为synchronized的wait() */ boolean await(long time, TimeUnit unit) throws InterruptedException; /*当前线程进入等待状态直到被通知,中断或者到某个时间,如果没有到指定的时间就被通知, 方法返回true,否则,表示到了指定时间,方法返回false */ boolean awaitUntil(Date deadline) throws InterruptedException; /*唤醒一个等待在condition上的线程,该线程从等待方法返回前必须获取与condition相关联的锁 signal() 可以连接为synchronized的notify() */ void signal(); /*唤醒索引等待在condition上的线程,能够从等待方法返回的线程必须获取与condition相关联的锁 signalAll()可以理解为synchronized的otifyAll() */ void signalAll();}

关于Condition的实现类是AQS的内部类ConditionObject

(二)Condition使用范式

并发-显式锁[老的,有时间我重新整理一下]_等待时间_07

读写锁ReentrantReadWriteLock

synchronized和可重入锁是独占锁 ,意思是线程做任何事儿之前都需要先拿到锁,是悲观锁的思想.拿到了锁之后其它线程就无法拿到锁了.

平时业务工作,读多写少的场景多(在大多数业务场景中占了百分之99,有人曾经统计过,在互联网公司,读和写的比例是10:1,就可以采用一种读写分离的思想大大提升程序的性能)

读是不需要修改的,就应该允许多个读线程去读,这样加大了应用程序的性能.但是如何你用独占锁的话只能有一个线程去操作,哪怕是读操作,这样对效率是很不利的.

而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。
除了保证写操作对读操作的可见性以及并发性的提升之外,读写锁能够简化读写交互场景的编程方式。假设在程序中定义一个共享的用作缓存数据结构,它大部分时间提供读服务(例如查询和搜索),而写操作占有的时间很少,但是写操作完成之后的更新需要对后续的读服务可见。
在没有读写锁支持的(Java 5之前)时候,如果需要完成上述工作就要使用Java的等待通知机制,就是当写操作开始时,所有晚于写操作的读操作均会进入等待状态,只有写操作完成并进行通知之后,所有等待的读操作才能继续执行(写操作之间依靠synchronized关键进行同步),这样做的目的是使读操作能读取到正确的数据,不会出现脏读。改用读写锁实现上述功能,只需要在读操作时获取读锁,写操作时获取写锁即可。当写锁被获取到时,后续(非当前写操作线程)的读写操作都会被阻塞,写锁释放之后,所有操作继续执行,编程方式相对于使用等待通知机制的实现方式而言,变得简单明了。
一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量
ReentrantReadWriteLock其实实现的是ReadWriteLock接口

(一)读写锁的互斥性

当有线程持有读锁的时候,写线程想拿写锁是不允许的,必须等待读锁释放了,才能拿到写锁,其它的读线程可以共享.

当有线程持有写锁的时候,不管是读线程还是其它的写线程都是不能拿到锁的.

StampedLock

StampedLock是Java8引入的一种新的读写锁机制,简单的理解,可以认为它是读写锁的一个改进版本,性能要比ReentrantReadWriteLock性能高很多,但是缺点就是编写业务代码稍微复杂(牵扯到读写锁转换).

读写锁虽然分离了读和写的功能,使得读与读之间可以完全并发,但是读和写之间依然是冲突的,读锁会完全阻塞写锁,它使用的依然是悲观的锁策略.如果有大量的读线程(读锁不断获取资源,读锁是排斥写锁),他也有可能引起写线程的饥饿。

而StampedLock则提供了一种乐观的读策略,这种乐观策略的锁非常类似于无锁的操作,使得乐观锁完全不会阻塞写线程。
它的思想是读写锁中读不仅不阻塞读,同时也不应该阻塞写。

读不阻塞写的实现思路:
在读的时候如果发生了写,则应当重读而不是在读的时候直接阻塞写!即读写之间不会阻塞对方,但是写和写之间还是阻塞的!

如何知道在读的时候发生了写操作呢?其实就用到了stamp(票据),拿到锁之后会返回给你一个stamp(票据)的值,根据这个值是否被改变来判断读的过程中是否发生了写

StampedLock的内部实现是基于CLH的,里面有乐观锁和悲观锁两种锁,在读的时候如果获取的是乐观锁,就会读写不护持,如果在读的时候获取的是悲观锁,那么读写就会发生互斥

CLH队列锁.

参考:
​​​https://www.yuque.com/crow/rm8cfs/42151173-c315-4c70-bf87-a767adafd4b5​​

(一)基本API

writeLock() 独占的获取一把写锁(一直拿到写锁之后再进行返回)

long tryWriteLock() 独占的获取一把写锁(立即返回),如果没拿到锁返回的0

long readLock() 获取读锁(非独占的获取锁,一直拿到读锁再返回,拿不到就阻塞)

long tryReadLock() 非独占的获取锁,(立即返回),如果拿不到就返回0

long readLockInterruptibly 获取可中断的锁

long tryOptimisticRead() 获取乐观的读锁,会返回一个stamp,后边可以对stamp进行校验,校验是否在读的过程中被修改过了,如果被修改过了就加上readLock()再进行重新读取

boolean validate(long stamp) 根据票据返回值,根据值判断是否被修改过了,

void unlockRead () 释放锁

void unlock(long stamp) 释放读锁或者写锁

tryConvertToWriteLock(long stamp) 把读锁转换成写锁

tryConvertToReadLock(long stamp) 把写锁转换成读锁

LockSupport

案例:ZJJ_JavaBasic_2020/01/14_18:16:55_36tzc

LockSupport是JDK中一个线程阻塞工具类,所有的方法都是静态方法,,这些方法提供了最基本的线程阻塞和唤醒功能,可以让线程在任意位置阻塞,当然阻塞之后肯定得有唤醒的方法。LockSupport也成为构建同步组件的基础工具。

不用担心阻塞和唤醒操作的顺序,但要注意连续多次唤醒的效果和一次唤醒是一样的。

JDK并发包下的锁和其他同步工具的底层实现中大量使用了LockSupport进行线程的阻塞和唤醒,掌握它的用法和原理可以让我们更好的理解锁和其它同步工具的底层实现。

LockSupport定义了一组以park开头的方法用来阻塞当前线程,以及unpark(Thread thread)方法来唤醒一个被阻塞的线程。LockSupport增加了park(Object blocker)、parkNanos(Object blocker,long nanos)和parkUntil(Object blocker,long deadline)3个方法,用于实现阻塞当前线程的功能,其中参数blocker是用来标识当前线程在等待的对象(以下称为阻塞对象),该对象主要用于问题排查和系统监控。

总结:
park和unpark可以实现类似wait和notify的功能,但是并不和wait和notify交叉,也就是说unpark不会对wait起作用,notify也不会对park起作用。
park和unpark的使用不会出现死锁的情况

// 返回提供给最近一次尚未解除阻塞的 park 方法调用的 blocker 对象,如果该调用不受阻塞,则返回 null。

**static **Object getBlocker(Thread t)

// 为了线程调度,禁用当前线程,除非许可可用。

**static void **park()

// 为了线程调度,在许可可用之前禁用当前线程。

**static void **park(Object blocker)

// 为了线程调度禁用当前线程,最多等待指定的等待时间,除非许可可用。

**static void **parkNanos(**long **nanos)

// 为了线程调度,在许可可用前禁用当前线程,并最多等待指定的等待时间。

**static void **parkNanos(Object blocker, **long **nanos)

// 为了线程调度,在指定的时限前禁用当前线程,除非许可可用。

**static void **parkUntil(**long **deadline)

// 为了线程调度,在指定的时限前禁用当前线程,除非许可可用。

**static void **parkUntil(Object blocker, **long **deadline)

// 如果给定线程的许可尚不可用,则使其可用。

**static void **unpark(Thread thread)

举报

相关推荐

0 条评论