搜索
房产
装修
汽车
婚嫁
健康
理财
旅游
美食
跳蚤
二手房
租房
招聘
二手车
教育
茶座
我要买房
买东西
装修家居
交友
职场
生活
网购
亲子
情感
龙城车友
找美食
谈婚论嫁
美女
兴趣
八卦
宠物
手机

深入理解 Java 并发锁

[复制链接]
查看: 65|回复: 0

1万

主题

1万

帖子

3万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
32685
发表于 2020-1-15 01:18 | 显示全部楼层 |阅读模式
本文以及示例源码已归档在 javacore
一、并发锁简介

确保线程平安最多见的做法是操纵锁机制(Lock、sychronized)来对同享数据做互斥同步,这样在同一个时候,只要一个线程可以尝试某个方式大要某个代码块,那末操纵必定是原子性的,线程平安的。
在工作、口试中,经常会听到各类五花八门的锁,听的人云里雾里。锁的概念术语很多,它们是针对差此外题目所提出的,经过简单的梳理,也不难明确。
可重入锁

可重入锁别名递归锁,是指 同一个线程在外层方式获得了锁,在进入内层方式会自动获得锁
可重入锁可以在必定水平上制止死锁

  • ReentrantLock 、ReentrantReadWriteLock 是可重入锁。这点,从其命名也不丢脸出。
  • synchronized 也是一个可重入锁
  1. synchronized void setA() throws Exception{    Thread.sleep(1000);    setB();}synchronized void setB() throws Exception{    Thread.sleep(1000);}
复制代码
上面的代码就是一个典范场景:假如操纵的锁不是可重入锁的话,setB 大要不会被当前方程尝试,从而形成死锁。
公允锁与非公允锁


  • 公允锁 - 公允锁是指 多线程依照申请锁的次第来获得锁
  • 非公允锁 - 非公允锁是指 多线程不依照申请锁的次第来获得锁 。这就大要会出现优先级反转(后来者居上)大要饥饿现象(某线程总是抢不外此外线程,致使始终没法尝试)。
公允锁为了保证线程申请次第,势必要支出必定的性能价格,是以其吞吐量一样平常低于非公允锁。
公允锁与非公允锁 在 Java 中的典范实现:

  • synchronized 只支持非公允锁
  • ReentrantLock 、ReentrantReadWriteLock,默许黑白公允锁,但支持公允锁
独享锁与同享锁

独享锁与同享锁是一种广义上的说法,从现适用处上来看,也常被称为互斥锁与读写锁。

  • 独享锁 - 独享锁是指 锁一次只能被一个线程所持有
  • 同享锁 - 同享锁是指 锁可被多个线程所持有
独享锁与同享锁在 Java 中的典范实现:

  • synchronized 、ReentrantLock 只支持独享锁
  • ReentrantReadWriteLock 其写锁是独享锁,其读锁是同享锁。读锁是同享锁使得并发读黑白常高效的,读写,写读 ,写写的进程是互斥的。
灰心锁与悲观锁

悲观锁与灰心锁不是指具体的什么典范的锁,而是处置惩罚并发同步的计谋

  • 灰心锁 - 灰心锁对于并发采纳灰心的态度,以为:不加锁的并发操纵必定会出题目灰心锁适当写操纵频仍的场景
  • 悲观锁 - 悲观锁对于并发采纳悲观的态度,以为:不加锁的并发操纵也没什么题目。对于同一个数据的并发操纵,是不会发生点窜的。在更新数据的时候,会采纳不停尝试更新的方式更新数据。悲观锁适当读多写少的场景
灰心锁与悲观锁在 Java 中的典范实现:

  • 灰心锁在 Java 中的利用就是经过操纵 synchronized 和 Lock 表现加锁来举行互斥同步,这是一种阻塞同步。
  • 悲观锁在 Java 中的利用就是采纳 CAS 机制(CAS 操纵经过 Unsafe 类供给,但这个类不间接袒露为 API,所以都是间接操纵,如各类原子类)。
轻量级锁、重量级锁与偏向锁

所谓轻量级锁与重量级锁,指的是锁控制粒度的粗细。明显,控制粒度越细,阻塞开销越小,并发性也就越高。
Java 1.6 畴前,重量级锁一样平常指的是 synchronized ,而轻量级锁指的是 volatile。
Java 1.6 今后,针对 synchronized 做了大量优化,引入 4 种锁状态: 无锁状态、偏向锁、轻量级锁和重量级锁。锁可以单向的从偏向锁升级到轻量级锁,再从轻量级锁升级到重量级锁 。

  • 偏向锁 - 偏向锁是指一段同步代码不停被一个线程所拜候,那末该线程会自动获得锁。低落获得锁的价格。
  • 轻量级锁 - 是指当锁是偏向锁的时候,被另一个线程所拜候,偏向锁就会升级为轻量级锁,其他线程会经过自旋的形式尝试获得锁,不会阻塞,进步性能。
  • 重量级锁 - 是指当锁为轻量级锁的时候,另一个线程固然是自旋,但自旋不会不停持续下去,当自旋必定次数的时候,还没有获得到锁,就会进入阻塞,该锁收缩为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能低落。
分段锁

分段锁实在是一种锁的计划,并不是具体的一种锁。所谓分段锁,就是把锁的工具分红多段,每段自力控制,使得锁粒度更细,淘汰阻塞开销,从而进步并发性。这实在很好明白,就像高速公路上的免费站,假如只要一个免费口,那全数的车只能排成一条队缴费;倘使有多个免费口,便可以分流了。
Hashtable 操纵 synchronized 修饰方式来保证线程平安性,那末面临线程的拜候,Hashtable 就会锁住全部工具,全数的此外线程只能期待,这类阻塞方式的吞吐量明显很低。
Java 1.7 畴前的 ConcurrentHashMap 就是分段锁的典范案例。ConcurrentHashMap 保护了一个 Segment 数组,一样平常称为分段桶。
  1. final Segment[] segments;
复制代码
当有线程拜候 ConcurrentHashMap 的数据时,ConcurrentHashMap 会先按照 hashCode 盘算出数据在哪个桶(即哪个 Segment),然后锁住这个 Segment。
表现锁和内置锁

Java 1.5 之前,和谐对同享工具的拜候时可以操纵的机制只要 synchronized 和 volatile。这两个都属于内置锁,即锁的申请和开释都是由 JVM 所控制。
Java 1.5 以后,增加了新的机制:ReentrantLock、ReentrantReadWriteLock ,这类锁的申请和开释都可以由步伐所控制,所以常被称为表现锁。
synchronized 的用法和道理可以参考:Java 并发底子机制 - synchronized
留意:假如不需要 ReentrantLock、ReentrantReadWriteLock 所供给的高级同步特征,应当优先考虑操纵 synchronized 。出处以下:

  • Java 1.6 今后,synchronized 做了大量的优化,其性能已经与 ReentrantLock、ReentrantReadWriteLock 底子上持平。
  • 从趋历来看,Java 未来更大要会优化 synchronized ,而不是 ReentrantLock、ReentrantReadWriteLock ,由于 synchronized 是 JVM 内置属性,它能尝试一些优化。
  • ReentrantLock、ReentrantReadWriteLock 申请和开释锁都是由步伐控制,假如操纵不妥,大要形成死锁,这是很危险的。
以下对照一下表现锁和内置锁的不同:

  • 自动获得锁和开释锁

    • synchronized 不能自动获得锁和开释锁。获得锁和开释锁都是 JVM 控制的。
    • ReentrantLock 可以自动获得锁和开释锁。(假如忘记开释锁,就大要产生死锁)。

  • 响应停止

    • synchronized 不能响应停止。
    • ReentrantLock 可以响应停止。

  • 超机遇制

    • synchronized 没有超机遇制。
    • ReentrantLock 有超机遇制。ReentrantLock 可以设备超不时候,超时后自动开释锁,制止不停期待。

  • 支持公允锁

    • synchronized 只支持非公允锁。
    • ReentrantLock 支持非公允锁和公允锁。

  • 能否支持同享

    • 被 synchronized 修饰的方式或代码块,只能被一个线程拜候(独享)。假如这个线程被阻塞,其他线程也只能期待
    • ReentrantLock 可以基于 Condition 灵活的控制同步条件。

  • 能否支持读写分手

    • synchronized 不支持读写锁分手;
    • ReentrantReadWriteLock 支持读写锁,从而使阻塞读写的操纵分隔,有用进步并发性。

二、AQS

AbstractQueuedSynchronizer(简称 AQS)是行列同步器,望文生义,其垂危感化是处置惩罚同步。它是并发锁和很多同步工具类的实现基石(如 ReentrantLock、ReentrantReadWriteLock、Semaphore 等)。
是以,要想深入明白 ReentrantLock、ReentrantReadWriteLock 等并发锁和同步工具,必须先明白 AQS 的要点和道理。
AQS 的要点

在 java.util.concurrent.locks 包中的关连锁(常用的有 ReentrantLock、 ReadWriteLock)都是基于 AQS 来实现。这些锁都没有间接继续 AQS,而是界说了一个 Sync 类去继续 AQS。为什么要这样呢?由于锁面向的是操纵用户,而同步器面向的则是线程控制,那末在锁的实现中聚公约步器而不是间接继续 AQS 便可以很好的隔离两者所关注的变乱。
AQS 供给了对独享锁与同享锁的支持
独享锁 API

获得、开释独享锁的垂危 API 以下:
  1. public final void acquire(int arg)public final void acquireInterruptibly(int arg)public final boolean tryAcquireNanos(int arg, long nanosTimeout)public final boolean release(int arg)
复制代码

  • acquire - 获得独占锁。
  • acquireInterruptibly - 获得可停止的独占锁。
  • tryAcquireNanos - 尝试在指按时候内获得可停止的独占锁。在以下三种情况下回返回:

    • 在超不时候内,当前方程乐成获得了锁;
    • 当前方程在超不时候内被停止;
    • 超不时候竣事,仍未获得锁返回 false。

  • release - 开释独占锁。
同享锁 API

获得、开释同享锁的垂危 API 以下:
  1. public final void acquireShared(int arg)public final void acquireSharedInterruptibly(int arg)public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)public final boolean releaseShared(int arg)
复制代码

  • acquireShared - 获得同享锁。
  • acquireSharedInterruptibly - 获得可停止的同享锁。
  • tryAcquireSharedNanos - 尝试在指按时候内获得可停止的同享锁。
  • release - 开释同享锁。
AQS 的道理

AQS 的数据结构

阅读 AQS 的源码,可以发现:AQS 继续自 AbstractOwnableSynchronize。
  1. public abstract class AbstractQueuedSynchronizer    extends AbstractOwnableSynchronizer    implements java.io.Serializable {    /** 期待行列的队头,懒加载。只能经过 setHead 方式点窜。 */    private transient volatile Node head;    /** 期待行列的队尾,懒加载。只能经过 enq 方式增加新的期待节点。*/    private transient volatile Node tail;    /** 同步状态 */    privatevolatile int state;}
复制代码

  • state - AQS 操纵一个整型的 volatile 变量来 保护同步状态

    • 这个整数状态的意义由子类来赋予,如ReentrantLock 中该状态值表现全数者线程已经反复获得该锁的次数,Semaphore 中该状态值表现残剩的答应数目。

  • head 和 tail - AQS 保护了一个 Node 典范(AQS 的内部类)的双链表来完成同步状态的治理。这个双链表是一个双向的 FIFO 行列,经过 head 和 tail 指针举行拜候。当 有线程获得锁失利后,就被增加到行列末端
深入理解 Java 并发锁  热点新闻 318837-20200115001414997-1133782389

再来看一下 Node 的源码
  1. static final class Node {    /** 该期待同步的节点处于同享形式 */    static final Node SHARED = new Node();    /** 该期待同步的节点处于独占形式 */    static final Node EXCLUSIVE = null;    /** 线程期待状态,状态值有: 0、1、-1、-2、-3 */    volatile int waitStatus;    static final int CANCELLED =  1;    static final int SIGNAL    = -1;    static final int CONDITION = -2;    static final int PROPAGATE = -3;    /** 先驱节点 */    volatile Node prev;    /** 后继节点 */    volatile Node next;    /** 期待锁的线程 */    volatile Thread thread;    /** 和节点能否同享有关 */    Node nextWaiter;}
复制代码
很明显,Node 是一个双链表结构。

  • waitStatus - Node 操纵一个整型的 volatile 变量来 保护 AQS 同队伍列中线程节点的状态。waitStatus 有五个状态值:

    • CANCELLED(1) - 此状态表现:该节点的线程大要由于超时或被停止而 处于被取消(取消)状态,一旦处于这个状态,表现这个节点应当从期待行列中移除。
    • SIGNAL(-1) - 此状态表现:后继节点会被挂起,是以在当前节点开释锁或被取消以后,必须叫醒(unparking)后来继结点。
    • CONDITION(-2) - 此状态表现:该节点的线程 处于期待条件状态,不会被看成是同队伍列上的节点,直到被叫醒(signal),设备其值为 0,再重新进入阻塞状态。
    • PROPAGATE(-3) - 此状态表现:下一个 acquireShared 应无条件传布。
    • 0 - 非以上状态。

独占锁的获得和开释

获得独占锁

AQS 中操纵 acquire(int arg) 方式获得独占锁,其大略流程以下:

  • 先尝试获得同步状态,假如获得同步状态乐成,则竣事方式,间接返回。
  • 假如获得同步状态不乐成,AQS 会不停尝试操纵 CAS 操纵将当前方程插入期待同队伍列的队尾,直到乐成为止。
  • 接着,不停尝试为期待行列中的线程节点获得独占锁。
深入理解 Java 并发锁  热点新闻 318837-20200115001415216-348571953

深入理解 Java 并发锁  热点新闻 318837-20200115001415388-1586733197

具体流程可以用下图来表现,请团结源码来明白(一图胜千言):
深入理解 Java 并发锁  热点新闻 318837-20200115001416401-2133116045

开释独占锁

AQS 中操纵 release(int arg) 方式开释独占锁,其大略流程以下:

  • 先尝试获得解锁线程的同步状态,假如获得同步状态不乐成,则竣事方式,间接返回。
  • 假如获得同步状态乐成,AQS 会尝试叫醒当前方程节点的后继节点。
获得可停止的独占锁

AQS 中操纵 acquireInterruptibly(int arg) 方式获得可停止的独占锁。
acquireInterruptibly(int arg) 实现方式相较于获得独占锁方式( acquire)很是类似,区分仅在于它会经过 Thread.interrupted 检测当前方程能否被停止,假如是,则立即抛出停止很是(InterruptedException)。
获得超期间待式的独占锁

AQS 中操纵 tryAcquireNanos(int arg) 方式获得超期间待的独占锁。
doAcquireNanos 的实现方式 相较于获得独占锁方式( acquire)很是类似,区分在于它会按照超不时候和当前时候盘算出停止时候。在获得锁的流程中,会不停判定能否超时,假如超时,间接返回 false;假如没超时,则用 LockSupport.parkNanos 来阻塞当前方程。
同享锁的获得和开释

获得同享锁

AQS 中操纵 acquireShared(int arg) 方式获得同享锁。
acquireShared 方式和 acquire 方式的逻辑很类似,区分仅在于自旋的条件以及节点出队的操纵有所不同。
乐成获得同享锁的条件以下:

  • tryAcquireShared(arg) 返回值大于即是 0 (这意味着同享锁的 permit 还没有用完)。
  • 当前节点的先驱节点是头结点。
开释同享锁

AQS 中操纵 releaseShared(int arg) 方式开释同享锁。
releaseShared 首先会尝试开释同步状态,假如乐成,则解锁一个或多个后继线程节点。开释同享锁和开释独享锁流程大要类似,区分在于:
对于独享形式,假如需要 SIGNAL,开释仅相当于挪用头节点的 unparkSuccessor。
获得可停止的同享锁

AQS 中操纵 acquireSharedInterruptibly(int arg) 方式获得可停止的同享锁。
acquireSharedInterruptibly 方式与 acquireInterruptibly 几乎同等,不再赘述。
获得超期间待式的同享锁

AQS 中操纵 tryAcquireSharedNanos(int arg) 方式获得超期间待式的同享锁。
tryAcquireSharedNanos 方式与 tryAcquireNanos 几乎同等,不再赘述。
三、ReentrantLock

ReentrantLock 类是 Lock 接口的具体实现,它是一个可重入锁。与内置锁 synchronized 不同,ReentrantLock 供给了一组无条件的、可轮询的、按时的以及可停止的锁操纵,全数获得锁、开释锁的操纵都是显式的操纵。
ReentrantLock 的特征

ReentrantLock 的特征以下:

  • ReentrantLock 供给了与 synchronized 类似的互斥性、内存可见性和可重入性
  • ReentrantLock 支持公允锁和非公允锁(默许)两种形式。
  • ReentrantLock 实现了 Lock 接口,支持了 synchronized 所不具有的灵活性

    • synchronized 没法停止一个正在期待获得锁的线程
    • synchronized 没法在请求获得一个锁时无停止地期待

Lock 的接口界说以下:
  1. public interface Lock {    void lock();    void lockInterruptibly() throws InterruptedException;    boolean tryLock();    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;    void unlock();    Condition newCondition();}
复制代码

  • lock() - 获得锁
  • unlock() - 开释锁
  • tryLock() - 尝试获得锁,仅在挪用时锁未被另一个线程持有的情况下,才获得该锁。
  • tryLock(long time, TimeUnit unit) - 和 tryLock() 类似,区分仅在于限按时候,假如限按时候内未获得到锁,视为失利。
  • lockInterruptibly() - 锁未被另一个线程持有,且线程没有被停止的情况下,才华获得锁。
  • newCondition() - 返回一个绑定到 Lock 工具上的 Condition 实例。
ReentrantLock 的用法

前文了解了 ReentrantLock 的特征,接下来,我们要报告其具体用法。
ReentrantLock 的机关方式

ReentrantLock 有两个机关方式:
  1. public ReentrantLock() {}public ReentrantLock(boolean fair) {}
复制代码

  • ReentrantLock() - 默许机关方式会初始化一个非公允锁(NonfairSync)
  • ReentrantLock(boolean) - new ReentrantLock(true) 会初始化一个公允锁(FairSync)
lock 和 unlock 方式


  • lock() - 无条件获得锁。假如当前方程没法获得锁,则当前方程进入休眠状态不成用,直至当前方程获得到锁。假如该锁没有被另一个线程持有,则获得该锁并立即返回,将锁的持有计数设备为 1。
  • unlock() - 用于开释锁
留意:请务必服膺,获得锁操纵 lock() 必须在 try catch 块及第行,而且将开释锁操纵 unlock() 放在 finally 块及第行,以保证锁必定被被开释,避免死锁的发生
示例:ReentrantLock 的底子操纵
  1. public class ReentrantLockDemo {    public static void main(String[] args) {        Task task = new Task();        MyThread tA = new MyThread("Thread-A", task);        MyThread tB = new MyThread("Thread-B", task);        MyThread tC = new MyThread("Thread-C", task);        tA.start();        tB.start();        tC.start();    }    static class MyThread extends Thread {        private Task task;        public MyThread(String name, Task task) {            super(name);            this.task = task;        }        @Override        public void run() {            task.execute();        }    }    static class Task {        private ReentrantLock lock = new ReentrantLock();        public void execute() {            lock.lock();            try {                for (int i = 0; i < 3; i++) {                    System.out.println(lock.toString());                    // 查询当前方程 hold 住此锁的次数                    System.out.println("\t holdCount: " + lock.getHoldCount());                    // 查询正期待获得此锁的线程数                    System.out.println("\t queuedLength: " + lock.getQueueLength());                    // 能否为公允锁                    System.out.println("\t isFair: " + lock.isFai());                    // 能否被锁住                    System.out.println("\t isLocked: " + lock.isLocked());                    // 能否被当前方程持有锁                    System.out.println("\t isHeldByCurrentThread: " + lock.isHeldByCurrentThread());                    try {                        Thread.sleep(500);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                }            } finally {                lock.unlock();            }        }    }}
复制代码
输出结果:
  1. java.util.concurrent.locks.ReentrantLock@64fcd88a[Locked by thread Thread-A]     holdCount: 1     queuedLength: 2     isFair: false     isLocked: true     isHeldByCurrentThread: truejava.util.concurrent.locks.ReentrantLock@64fcd88a[Locked by thread Thread-C]     holdCount: 1     queuedLength: 1     isFair: false     isLocked: true     isHeldByCurrentThread: true// ...
复制代码
tryLock 方式

与无条件获得锁相比,tryLock 有更美满的容错机制。

  • tryLock() - 可轮询获得锁。假如乐成,则返回 true;假如失利,则返回 false。也就是说,这个方式不管成败城市立即返回,获得不到锁(锁已被其他线程获得)时不会不停期待。
  • tryLock(long, TimeUnit) - 可按时获得锁。和 tryLock() 类似,区分仅在于这个方式在获得不到锁时会期待必定的时候,在时候期限之内假如还获得不到锁,就返回 false。假如假如一路头拿到锁大要在期待时代内拿到了锁,则返回 true。
示例:ReentrantLock 的 tryLock() 操纵
点窜上个示例中的 execute() 方式
  1. public void execute() {    if (lock.tryLock()) {        try {            for (int i = 0; i < 3; i++) {               // 略...            }        } finally {            lock.unlock();        }    } else {        System.out.println(Thread.currentThread().getName() + " 获得锁失利");    }}
复制代码
示例:ReentrantLock 的 tryLock(long, TimeUnit) 操纵
点窜上个示例中的 execute() 方式
  1. public void execute() {    try {        if (lock.tryLock(2, TimeUnit.SECONDS)) {            try {                for (int i = 0; i < 3; i++) {                    // 略...                }            } finally {                lock.unlock();            }        } else {            System.out.println(Thread.currentThread().getName() + " 获得锁失利");        }    } catch (InterruptedException e) {        System.out.println(Thread.currentThread().getName() + " 获得锁超时");        e.printStackTrace();    }}
复制代码
lockInterruptibly 方式


  • lockInterruptibly() - 可停止获得锁。可停止获得锁可以在获得锁的同时连结对停止的响应。可停止获得锁比此外获得锁的方式稍微复杂一些,需要两个 try-catch 块(假如在获得锁的操纵中抛出了 InterruptedException ,那末可以操纵标准的 try-finally 加锁形式)。

    • 举例来说:假定有两个线程同时经过 lock.lockInterruptibly() 获得某个锁时,若线程 A 获得到了锁,则线程 B 只能期待。若此时对线程 B 挪用 threadB.interrupt() 方式可以也许停止线程 B 的期待进程。由于 lockInterruptibly() 的声明中抛出了很是,所以 lock.lockInterruptibly() 必须放在 try 块中大要在挪用 lockInterruptibly() 的方式外声明抛出 InterruptedException。

留意:当一个线程获得了锁以后,是不会被 interrupt() 方式停止的。零丁挪用 interrupt() 方式不能停止正在运转状态中的线程,只能停止阻塞状态中的线程。是以当经过 lockInterruptibly() 方式获得某个锁时,假如未获得到锁,只要在期待的状态下,才可以响应停止。
示例:ReentrantLock 的 lockInterruptibly() 操纵
点窜上个示例中的 execute() 方式
  1. public void execute() {    try {        lock.lockInterruptibly();        for (int i = 0; i < 3; i++) {            // 略...        }    } catch (InterruptedException e) {        System.out.println(Thread.currentThread().getName() + "被停止");        e.printStackTrace();    } finally {        lock.unlock();    }}
复制代码
newCondition 方式

newCondition() - 返回一个绑定到 Lock 工具上的 Condition 实例。Condition 的特征和具体方式请阅读下文 Condition
ReentrantLock 的道理

ReentrantLock 的数据结构

阅读 ReentrantLock 的源码,可以发现它有一个焦点字段:
  1. private final Sync sync;
复制代码

  • sync - 内部笼统类 ReentrantLock.Sync 工具,Sync 继续自 AQS。它有两个子类:
  • ReentrantLock.FairSync - 公允锁。
  • ReentrantLock.NonfairSync - 非公允锁。
检察源码可以发现,ReentrantLock 实现 Lock 接口实在是挪用 ReentrantLock.FairSync 或 ReentrantLock.NonfairSync 中各自的实现,这里不逐一列举。
ReentrantLock 的获得锁和开释锁

ReentrantLock 获得锁和开释锁的接口,从表象看,是挪用 ReentrantLock.FairSync 或 ReentrantLock.NonfairSync 中各自的实现;从本质上看,是基于 AQS 的实现。
细致阅读源码很轻易发现:

  • void lock() 挪用 Sync 的 lock() 方式。
  • void lockInterruptibly() 间接挪用 AQS 的 获得可停止的独占锁 方式 lockInterruptibly()。
  • boolean tryLock() 挪用 Sync 的 nonfairTryAcquire() 。
  • boolean tryLock(long time, TimeUnit unit) 间接挪用 AQS 的 获得超期间待式的独占锁 方式 tryAcquireNanos(int arg, long nanosTimeout)。
  • void unlock() 间接挪用 AQS 的 开释独占锁 方式 release(int arg) 。
间接挪用 AQS 接口的方式就不再赘述了,其道理在 AQS 的道理 中已经用很大篇幅举行过讲授。
nonfairTryAcquire 方式源码以下:
  1. // 公允锁和非公允锁城市用这个方式区尝试获得锁final boolean nonfairTryAcquire(int acquires) {    final Thread current = Thread.currentThread();    int c = getState();    if (c == 0) {        if (compareAndSetState(0, acquires)) {         // 假如同步状态为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;}
复制代码
处置惩罚流程很简单:

  • 假如同步状态为 0,设备同步状态设为 acquires,并设备当前方程为排它线程,然后返回 true,获得锁乐成。
  • 假如同步状态不为 0 且当前方程为排它线程,设备同步状态为当前状态值+acquires 值,然后返回 true,获得锁乐成。
  • 否则,返回 false,获得锁失利。
lock 方式在公允锁和非公允锁中的实现:
两者的区分仅在于申请非公允锁时,假如同步状态为 0,尝试将其设为 1,假如乐成,间接将当前方程置为排它线程;否则和公允锁一样,挪用 AQS 获得独占锁方式 acquire。
  1. // 非公允锁实现final void lock() {    if (compareAndSetState(0, 1))    // 假如同步状态为0,将其设为1,并设备当前方程为排它线程        setExclusiveOwnerThread(Thread.currentThread());    else    // 挪用 AQS 获得独占锁方式 acquire        acquire(1);}// 公允锁实现final void lock() {    // 挪用 AQS 获得独占锁方式 acquire    acquire(1);}
复制代码
四、ReentrantReadWriteLock

ReentrantReadWriteLock 类是 ReadWriteLock 接口的具体实现,它是一个可重入的读写锁ReentrantReadWriteLock 保护了一对读写锁,将读写锁分隔,有益于进步并发服从
ReentrantLock 实现了一种标准的互斥锁:每次最多只要一个线程能持有 ReentrantLock。但对于保护数据的完整性来说,互斥凡是是一种过于倔强的加锁计谋,是以也就不必要地限制了并发性。大大都场景下,读操纵比写操纵频仍,只要保证每个线程都能读取到最新数据,而且在读数据时不会有此外线程在点窜数据,那末就不会出现线程平安题目。这类计谋淘汰了互斥同步,自然也提升了并发性能,ReentrantReadWriteLock 就是这类计谋的具体实现。
ReentrantReadWriteLock 的特征

ReentrantReadWriteLock 的特征以下:

  • ReentrantReadWriteLock 适用于读多写少的场景。假如是写多读少的场景,由于 ReentrantReadWriteLock 其内部实现比 ReentrantLock 复杂,性能大要反而要差一些。假如存在这样的题目,需要具体题目具体分析。由于 ReentrantReadWriteLock 的读写锁(ReadLock、WriteLock)都实现了 Lock 接口,所以要更换为 ReentrantLock 也较为轻易。
  • ReentrantReadWriteLock 实现了 ReadWriteLock 接口,支持了 ReentrantLock 所不具有的读写锁分手。ReentrantReadWriteLock 保护了一对读写锁(ReadLock、WriteLock)。将读写锁分隔,有益于进步并发服从。ReentrantReadWriteLock 的加锁计谋是:答应多个读操纵并发尝试,但每次只答应一个写操纵</strong>。
  • ReentrantReadWriteLock 为读写锁都供给了可重入的加锁语义。
  • ReentrantReadWriteLock 支持公允锁和非公允锁(默许)两种形式。
ReadWriteLock 接口界说以下:
  1. public interface ReadWriteLock {    Lock readLock();    Lock writeLock();}
复制代码

  • readLock - 返回用于读操纵的锁(ReadLock)。
  • writeLock - 返回用于写操纵的锁(WriteLock)。
在读写锁和写入锁之间的交互可以采纳多种实现方式,ReadWriteLock 的一些可选实现包含:

  • 开释优先 - 当一个写入操纵开释写锁,而且行列中同时存在读线程和写线程,那末应当优先挑选读线程、写线程,还是起头发出请求的线程?
  • 读线程插队 - 假如锁是由读线程持有,但有写线程正在期待,那末新到达的读线程能否立即获得拜候权,还是应当在写线程背面期待?假如答应读线程插队到写线程之前,那末将进步并发性,但大要形成线程饥饿题目。
  • 重入性 - 读锁和写锁能否是可重入的?
  • 升级 - 假如一个线程持有写入锁,那末它能否在不开释该锁的情况下获得读锁?这大要会使得写锁被升级为读锁,同时不答应其他写线程点窜被保护的资本。
  • 升级 - 读锁能否优先于其他正在期待的读线程和写线程而升级为一个写锁?在大大都的读写锁实现中并不支持升级,由于假如没有显式的升级操纵,那末很轻易形成死锁。
ReentrantReadWriteLock 的用法

前文了解了 ReentrantReadWriteLock 的特征,接下来,我们要报告其具体用法。
ReentrantReadWriteLock 的机关方式

ReentrantReadWriteLock 和 ReentrantLock 一样,也有两个机关方式,且用法类似。
  1. public ReentrantReadWriteLock() {}public ReentrantReadWriteLock(boolean fair) {}
复制代码

  • ReentrantReadWriteLock() - 默许机关方式会初始化一个非公允锁(NonfairSync)。在非公允的锁中,线程获得锁的次第是不肯定的。写线程升级为读线程是可以的,但读线程升级为写线程是不成以的(这样会致使死锁)。
  • ReentrantReadWriteLock(boolean) - new ReentrantLock(true) 会初始化一个公允锁(FairSync)。对于公允锁,期待时候最长的线程将优先获得锁。假如这个锁是读线程持有,则另一个线程请求写锁,那末其他读线程都不能获得读锁,直到写线程开释写锁。
ReentrantReadWriteLock 的操纵实例

ReentrantReadWriteLock 的特征 中已经先容过,ReentrantReadWriteLock 的读写锁(ReadLock、WriteLock)都实现了 Lock 接口,所以其各自自力的操纵方式与 ReentrantLock 一样,这里不再赘述。
ReentrantReadWriteLock 与 ReentrantLock 用法上的不同,垂危在于读写锁的配合操纵。本文以一个典范操纵场景来举行讲授。
示例:基于 ReentrantReadWriteLock 实现一个简单确当地缓存
  1. /** * 简单的无界缓存实现 *
  2. * 操纵 WeakHashMap 存储键值对。WeakHashMap 中存储的工具是弱援用,JVM GC 时会自动扫除没有被援用的弱援用工具。 */static class UnboundedCache {    private final Map cacheMap = new WeakHashMap();    private final ReentrantReadWriteLock cacheLock = new ReentrantReadWriteLock();    public V get(K key) {        cacheLock.readLock().lock();        V value;        try {            value = cacheMap.get(key);            String log = String.format("%s 读数据 %s:%s", Thread.currentThread().getName(), key, value);            System.out.println(log);        } finally {            cacheLock.readLock().unlock();        }        return value;    }    public V put(K key, V value) {        cacheLock.writeLock().lock();        try {            cacheMap.put(key, value);            String log = String.format("%s 写入数据 %s:%s", Thread.currentThread().getName(), key, value);            System.out.println(log);        } finally {            cacheLock.writeLock().unlock();        }        return value;    }    public V remove(K key) {        cacheLock.writeLock().lock();        try {            return cacheMap.remove(key);        } finally {            cacheLock.writeLock().unlock();        }    }    public void clear() {        cacheLock.writeLock().lock();        try {            this.cacheMap.clear();        } finally {            cacheLock.writeLock().unlock();        }    }}
复制代码
分析:

  • 操纵 WeakHashMap 而不是 HashMap 来存储键值对。WeakHashMap 中存储的工具是弱援用,JVM GC 时会自动扫除没有被援用的弱援用工具。
  • 向 Map 写数据前加写锁,写完后,开释写锁。
  • 向 Map 读数据前加读锁,读完后,开释读锁。
测试其线程平安性:
  1. /** * @author [email=forbreak@163.com]Zhang Peng[/email] * @since 2020-01-01 */public class ReentrantReadWriteLockDemo {    static UnboundedCache cache = new UnboundedCache();    public static void main(String[] args) {        ExecutorService executorService = Executors.newCachedThreadPool();        for (int i = 0; i < 20; i++) {            executorService.execute(new MyThread());            cache.get(0);        }        executorService.shutdown();    }    /** 线程使命每次向缓存中写入 3 个随机值,key 牢固 */    static class MyThread implements Runnable {        @Override        public void run() {            Random random = new Random();            for (int i = 0; i < 3; i++) {                cache.put(i, random.nextInt(100));            }        }    }}
复制代码
分析:示例中,经过线程池启动 20 个并发使命。使命每次向缓存中写入 3 个随机值,key 牢固;然后主线程每次牢固读取缓存中第一个 key 的值。
输出结果:
  1. main 读数据 0:nullpool-1-thread-1 写入数据 0:16pool-1-thread-1 写入数据 1:58pool-1-thread-1 写入数据 2:50main 读数据 0:16pool-1-thread-1 写入数据 0:85pool-1-thread-1 写入数据 1:76pool-1-thread-1 写入数据 2:46pool-1-thread-2 写入数据 0:21pool-1-thread-2 写入数据 1:41pool-1-thread-2 写入数据 2:63main 读数据 0:21main 读数据 0:21// ...
复制代码
ReentrantReadWriteLock 的道理

前面了解了 ReentrantLock 的道理,明白 ReentrantReadWriteLock 就轻易多了。
ReentrantReadWriteLock 的数据结构

阅读 ReentrantReadWriteLock 的源码,可以发现它有三个焦点字段:
  1. /** Inner class providing readlock */private final ReentrantReadWriteLock.ReadLock readerLock;/** Inner class providing writelock */private final ReentrantReadWriteLock.WriteLock writerLock;/** Performs all synchronization mechanics */final Sync sync;public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }
复制代码

  • sync - 内部类 ReentrantReadWriteLock.Sync 工具。与 ReentrantLock 类似,它有两个子类:ReentrantReadWriteLock.FairSync 和 ReentrantReadWriteLock.NonfairSync ,别离表现公允锁和非公允锁的实现。
  • readerLock - 内部类 ReentrantReadWriteLock.ReadLock 工具,这是一把读锁。
  • writerLock - 内部类 ReentrantReadWriteLock.WriteLock 工具,这是一把写锁。
ReentrantReadWriteLock 的获得锁和开释锁
  1. public static class ReadLock implements Lock, java.io.Serializable {    // 挪用 AQS 获得同享锁方式    public void lock() {        sync.acquireShared(1);    }    // 挪用 AQS 开释同享锁方式    public void unlock() {        sync.releaseShared(1);    }}public static class WriteLock implements Lock, java.io.Serializable {    // 挪用 AQS 获得独占锁方式    public void lock() {        sync.acquire(1);    }    // 挪用 AQS 开释独占锁方式    public void unlock() {        sync.release(1);    }}
复制代码
五、Condition

前文中提过 Lock 接口中 有一个 newCondition() 方式用于返回一个绑定到 Lock 工具上的 Condition 实例。Condition 是什么?有什么感化?本节将逐一讲授。
在单线程中,一段代码的尝试大要依靠于某个状态,假如不满足状态条件,代码就不会被尝试(典范的场景,如:if ... else ...)。在并发情况中,当一个线程判定某个状态条件时,其状态大如果由于其他线程的操纵而改变,这时就需要有必定的和谐机制来确保在同一时候,数据只能被一个线程锁点窜,且点窜的数据状态被全数线程所感知。
Java 1.5 之前,垂危是操纵 Object 类中的 wait、notify、notifyAll 配合 synchronized 来举行线程间通讯(假如不了解其特征,可以参考:Java 线程底子 - wait/notify/notifyAll)。
wait、notify、notifyAll 需要配合 synchronized 操纵,不适用于 Lock。而操纵 Lock 的线程,相互间通讯应当操纵 Condition 。这可以明白为,什么样的锁配什么样的钥匙。内置锁(synchronized)配合内置条件行列(wait、notify、notifyAll ),显式锁(Lock)配合显式条件行列(Condition )
Condition 的特征

Condition 接口界说以下:
  1. public interface Condition {    void await() throws InterruptedException;    void awaitUninterruptibly();    long awaitNanos(long nanosTimeout) throws InterruptedException;    boolean await(long time, TimeUnit unit) throws InterruptedException;    boolean awaitUntil(Date deadline) throws InterruptedException;    void signal();    void signalAll();}
复制代码
其中,await、signal、signalAll 与 wait、notify、notifyAll 相对应,功用也类似。除此之外,Condition 相比内置条件行列( wait、notify、notifyAll ),供给了更加丰富的功用:

  • 每个锁(Lock)上可以存在多个 Condition,这意味着锁的状态条件可以有多个。
  • 支持公允的或非公允的行列操纵。
  • 支持可停止的条件期待,关连方式:awaitUninterruptibly() 。
  • 支持可按时的期待,关连方式:awaitNanos(long) 、await(long, TimeUnit)、awaitUntil(Date)。
Condition 的用法

这里以 Condition 来实现一个消耗者、生产者形式。
留意:究竟上,治理此类题目操纵 CountDownLatch、Semaphore 等工具更加便利、平安。想了解详情,可以参考 Java 并发工具类
产物类
  1. class Message {    private final Lock lock = new ReentrantLock();    private final Condition producedMsg = lock.newCondition();    private final Condition consumedMsg = lock.newCondition();    private String message;    private boolean state;    private boolean end;    public void consume() {        //lock        lock.lock();        try {            // no new message wait for new message            while (!state) { producedMsg.await(); }            System.out.println("consume message : " + message);            state = false;            // message consumed, notify waiting thread            consumedMsg.signal();        } catch (InterruptedException ie) {            System.out.println("Thread interrupted - viewMessage");        } finally {            lock.unlock();        }    }    public void produce(String message) {        lock.lock();        try {            // last message not consumed, wait for it be consumed            while (state) { consumedMsg.await(); }            System.out.println("produce msg: " + message);            this.message = message;            state = true;            // new message added, notify waiting thread            producedMsg.signal();        } catch (InterruptedException ie) {            System.out.println("Thread interrupted - publishMessage");        } finally {            lock.unlock();        }    }    public boolean isEnd() {        return end;    }    public void setEnd(boolean end) {        this.end = end;    }}
复制代码
消耗者
  1. class MessageConsumer implements Runnable {    private Message message;    public MessageConsumer(Message msg) {        message = msg;    }    @Override    public void run() {        while (!message.isEnd()) { message.consume(); }    }}
复制代码
生产者
  1. class MessageProducer implements Runnable {    private Message message;    public MessageProducer(Message msg) {        message = msg;    }    @Override    public void run() {        produce();    }    public void produce() {        List msgs = new ArrayList();        msgs.add("Begin");        msgs.add("Msg1");        msgs.add("Msg2");        for (String msg : msgs) {            message.produce(msg);            try {                Thread.sleep(100);            } catch (InterruptedException e) {                e.printStackTrace();            }        }        message.produce("End");        message.setEnd(true);    }}
复制代码
测试
  1. public class LockConditionDemo {    public static void main(String[] args) {        Message msg = new Message();        Thread producer = new Thread(new MessageProducer(msg));        Thread consumer = new Thread(new MessageConsumer(msg));        producer.start();        consumer.start();    }}
复制代码
参考材料


免责声明:假如加害了您的权益,请联系站长,我们会实时删除侵权内容,感谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Copyright © 2006-2014 全椒百姓网-全椒知名**,发布及时新鲜的全椒新闻资讯 生活信息 版权所有 法律顾问:高律师 客服电话:0791-88289918
技术支持:迪恩网络科技公司  Powered by Discuz! X3.2
快速回复 返回顶部 返回列表