并发编程 java锁机制

1、什么是锁,为什么需要锁?

并发环境下,会存在多个线程对同一个资源进行争抢的情况,假设线程A对资源正在进行修改,此时线程B又对同一资源进行了修改,就会导致数据不一致的问题。为了解决这个问题,很多编程语言引入了锁机制。

通过一种抽象的“锁”来对资源进行锁定,当一个线程持有“锁”时,其他线程必须等待 ------  在临界资源上对线程进行一种串行化处理。

2、悲观锁 VS 乐观锁

乐观锁和悲观锁是一种广义上的概念,体现了看待线程并发时的角度。

  • 悲观锁:认为自己在使用数据的时候一定会有别的线程修改数据,因此获取数据时会先加锁,确保不会被别的线程修改
  • 乐观锁:认为自己使用数据的时候不会有别的线程修改数据,所以不会去加锁,只是在更新数据的时候去判断有没有别的线程更新了这个数据,如果没有被更新,则自己执行相关操作,如果被其他线程更新了,则执行对应的操作(重试或者报错)
3、介绍一下悲观锁。

java中,悲观锁的实现是基于object的 ------ 也就是说,每个对象都拥有一把锁,这个锁存放在对象头中,记录了当前对象被哪个线程所占用(持有)。

3.1、对象结构,对象头结构

首先说一下对象的结构:

  • 对象头
  • 实例数据
  • 填充字节(为了满足java对象大小是8字节的整数倍,没有实际意义)

对象头结构:

  • mark word --- 见下图
  • class pointer  --- 是一个指针,指向当前对象类型所在方法区中的class信息

相比于实例数据,对象头其实是一种额外的开销,因此设计的很小(32bit或者64bit)

从上图可以看到,“锁”的信息就存放在mark word中,根据后两个标志位,锁的状态又可以分为 无锁,偏向锁,轻量级锁,重量级锁。---  java中启用对象锁的方式就是 synchronized 关键字。下面会详细介绍一下synchronized背后的原理。

3.2 synchronized关键字

在java中,synchronized关键字可以用来加锁,而synchronized被编译后会生成两个字节码指令:monitorenter 和 monitorexit, 这两个字节码指令底层是依赖于操作系统的 mutex lock 来实现的,这个时候,java线程实际就是操作系统线程的映射,每当唤醒或者挂起一个线程的时候,都需要切换到操作系统的内核态,这个操作是重量级的(换句话说就是比较耗费时间,甚至切换的时间就超出了线程执行任务的时间。)因此,使用synchronized其实是会对程序的性能产生影响。

 刚刚提到了monitor(又叫监视器、管程等),我们可以将它理解为只能容纳一个客人的饭店,客人就是想要获取对象锁的线程,一旦有一个线程进入了monitor,那么其他线程只能等待,只有当这个线程退出去,其他线程才有机会进入。

3.3 ,介绍一下对象锁的四种状态
1、无锁

顾名思义就是没有对系统资源进行锁定,1)比如某种资源不会出现在多线程环境下,或者即便在多线程环境下面也不会有竞争情况,那么就不需要加锁。2)虽然会被竞争,但是采用了其他机制来控制,而不是使用操作系统对资源锁定(CAS,后面会介绍)。

2、偏向锁

假如一个对象被加锁了,但是实际运行过程中,只有一个线程会获取这个对象锁(前提),最理想的方式是只在用户态就把锁交出去,而不是通过操作系统来切换线程状态。 ---- 如果对象能够认识这个唯一的线程,只要这个线程过来,就直接把锁交出去就行了。(对象偏爱这个线程,称为偏向锁)

偏向锁是怎么实现的呢? 回到mark word 中,当锁标志位为01的时候,判断倒数第三个bit, 如果是1,则表示当前对象锁的状态为偏向锁,此时去读mark word的前23bit,这记录的就是线程ID,可以通过线程id来判断这个线程是不是被偏爱的线程。

3、轻量级锁

某一个时间,情况发生了变化,不止一个线程想要获取这个锁了(两个线程),这个时候,偏向锁就会升级成为轻量级锁。

轻量级锁是如何实现的? --- Markword 后两位变成00的时候,就意味着当前对象锁的状态是轻量级锁了,此时不在用线程id了,前30位变成了指向虚拟机栈中锁记录的指针。

当一个线程想要获取某个对象锁,发现Markword 后两位是00,此时会在自己的虚拟机栈中开辟一块成为 lock record 的空间,这个lock record存放的是Markword 的副本以及一个owner指针

线程通过cas去尝试获取锁,一旦获得,就会复制这个对象的Markword到自身虚拟机栈的lock record中,并且将lock record中的owner指针指向该对象锁。同时,对象的mark word的前30位生成一个指针,指向持有改对象锁的线程虚拟机栈中的lock record,这样就完成了线程和对象锁的绑定,双方都知道各自的存在了

此时,获得了对象锁的线程,就可以执行对应的任务了,那么没有获得锁的线程应该怎么办呢?--- 没有获得锁的线程会自旋等待,自旋就是一种轮询,不断判断锁有没有被释放,如果释放了,就获取锁,如果没有释放,就下一轮循环。 --- 这种自旋区别于被操作系统挂起阻塞,因为如果对象很快被释放的话,自旋获取锁就可以在用户态解决,而不用切换到内核态,效率比较高。

但是自旋其实就是CPU在空转,长时间的自旋会浪费CPU资源,于是出现了一种叫做“适应性自旋”的优化。简单来说就是,自旋时间不在固定了,而是又上一次在同一个锁上的自旋时间以及锁状态来决定,比如在同一个锁上,当前自旋等待的线程刚刚成功获得过锁,但是此时锁被其他线程持有,虚拟机就会认为下次自旋获取锁的概率很大,进而运行更长时间的自旋。

4、重量级锁

一旦自旋等待的线程超过一个,即有三个及以上的线程想获取同一个锁,这个时候就会升级成为重量级锁,这个时候就是通过monitor来对线程进行控制了,一旦进入了这个状态,就会调用内核空间,产生极大的开销。

上述描述了对象锁的四种状态,需要注意的是,锁只能升级,不能降级

4、介绍一下乐观锁

在讲述悲观锁的时候,提到了“无锁”这个概念,其中有一种是共享资源会出现被竞争的情况,但是不是适用操作系统同步原语进行保护,而是使用CAS这种方式进行线程同步,尽量将获取锁释放锁的操作在用户空间内完成,减少用户态和内核态之间的切换次数,提升程序的性能。 --------- 可以看到,其实乐观锁并不是锁,而是一种无锁的实现。

CAS就是实现乐观锁的一种经典巧妙的算法。 compare and swap,简单的翻译为,比较然后交换。

4.1, 如何理解CAS?

举例:比如说厕所的坑位,里面没人的时候你才可以进去,有人就只能在外面等着,设定开门状态是0,关门状态是1, 某一时间,两个人都想上厕所,A先冲了过去,并把门关上,这个时候B才过来,但是发现门已经关了,但是B也不能放弃啊,就不断回来看看门打开了没。

上述例子中,AB两个人就是代表线程,坑位就是共享资源,这样就应该比较容易理解CAS了,当一个共享资源状态值为0的一瞬间,AB线程读到了,此时两个线程都认为当前这个共享资源没有被占用,于是他们会各自生成两个值:

  • old value,代表之前读到的共享资源对象的状态值 -- 上述例子中都为0
  • new value,代表想要将共享资源对象状态值更新的值  -- 上述例子中都为1

此时AB线程都去争抢修改对象的状态值,然后占用对象。假设A运气比较好, A将old value 和 资源对象的状态值进行compare 比较,发现是一致的,于是将对象的状态值 swap 为new value; B落后一步,compare的时候发现自己的old value 和对象的状态值不一样,只能放弃swap操作(一般是自旋操作,同时配置自旋次数防止死循环,默认值10 )。

上述是一个CAS函数,但是其实是有问题的,因为这个函数本身没有任何同步措施,还是存在不安全的问题,因此,想要通过 CAS实现乐观锁,有一个必须的前提:CAS操作本身必须是原子性的

那么CAS是如何实现原子性的呢? --- 不同架构的CPU都提供了指令级的CAS原子操作,比如X86架构下的cmpxchg指令,ARM架构下的LL/SC指令。也就是说,CPU已经原生的支持了CAS,那么上层直接调用即可。

// 乐观锁的实现举例private static AtomicInteger num = new AtomicInteger();
public void testCAS() {for (int i = 0; i < 3; i++) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {while (num.get() < 1000) {System.out.println(Thread.currentThread().getName() + ":" + num.getAndIncrement());}}});}
}

通过AtomicIteger的源码,发现是使用Unsafe类来实现CAS操作的,这个cas方法是一个native函数,说明是本地方法,和具体的平台实现相关。

5、AQS 机制是什么?

通过上述介绍CAS,我们知道了java通过unsafe 类封装了CAS方法,支持了对CAS原语的调用,但是针对上层业务开发,怎么能够无感知的调用?并且业务场景中,我们最常竞争的资源往往是通过对象进行封装的,而CAS只能原始的修改内存上的一个值。如何进一步对CAS进行抽象呢?

下面就首先介绍一下JUC中经典的同步框架AQS (AbstractQueuedSynchronizer)

5.1 、AQS的成员属性
  1. state是用于判断共享资源是否正在占用的标记为,volatile关键字保证了线程之间的可见性。至于为什么用int类型而不是布尔类型,是因为AQS中有独占锁和共享锁的区别,共享模式下,state可以表示占用锁的线程的数量。
  2. AQS中还存在一个队列,用于管理等待获取锁的线程。FIFO双向链表,head和tail定义了头和尾
    // 队列的头 尾部private transient volatile Node head;private transient volatile Node tail;// 判断共享资源的标志位private volatile int state;
5.2、AQS的内部类

AQS维护了一个FIFO队列,因此定义了一个Node节点,里面存储了线程对象 thread,节点在队列中的等待状态 waitstatus,前后指针等信息。

  • waitStatus:AQS工作的时候,必然伴随着Node 节点状态值的各种变化,这里的waitStatus是一个枚举值
    • 0:节点初始化默认值,或者节点已经释放锁
    • 1:取消获取锁
    • -1:节点的后续节点需要被唤醒
    • -2:条件模式相关
    • -3:共享模式相关,传递共享模式下锁的释放状态
  • predecessor() 方法:获取当前节点的前置Node
     // 简化的,删除了很多东西static final class Node {static final int CANCELLED =  1;static final int SIGNAL    = -1;static final int CONDITION = -2;static final int PROPAGATE = -3;volatile int waitStatus;volatile Node prev;volatile Node next;volatile Thread thread;final Node predecessor() throws NullPointerException {Node p = prev;if (p == null)throw new NullPointerException();elsereturn p;}}

5.3、AQS中的核心方法

一般业务,可以分为两种场景使用锁:

  1. 尝试获取锁,不管有没有获取到,立即返回
  2. 必须获取锁,没有获取到则进行等待

恰好,AQS中针对上述两种场景,提供了两个方法,tryAcquire 和 acquire 两个方法。

5.3.1 tryAcquire -- 尝试获取锁

这个方法是参数是一个int 类型的值,代表对 state的增加操作,返回值是boolean,代表是否成功获取锁。该方法只抛出了一个异常,目的就是为了让子类进行重写,在子类中定义相关的业务逻辑,比如没有获取到锁是等待,还是别的处理。

    protected boolean tryAcquire(int arg) {throw new UnsupportedOperationException();}

5.3.2 acquire -- 必须获取锁

acquire方法被final修饰,无法重写,想要等待并获取锁,直接调用这个方法就可以。

    public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}

if条件包含了两个判断条件,tryAcquire已经说过了,现在讲一下acquireQueue方法。

addWaiter

首先看一下 addWaiter发方法 --- 这个方法作用就是将当前线程封装成一个node,然后加入等待队列中。

    private Node addWaiter(Node mode) {// 封装当前线程为一个NODE节点Node node = new Node(Thread.currentThread(), mode);// 获取当前尾节点Node pred = tail;if (pred != null) {// 先将当前节点前指针指向尾节点node.prev = pred;// CAS判断当前尾节点是否还是尾节点,如果是,就把当前节点变为尾节点,然后返回出去if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}// 如果当前队列为空,或者CAS失败,就进入这个方法enq(node);return node;}private Node enq(final Node node) {// 自旋 ,如果队列没有初始化,那么就初始化,如果尾节点插入失败,就不断重试,直到插入为止for (;;) {Node t = tail;if (t == null) { // Must initializeif (compareAndSetHead(new Node()))tail = head;} else {node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}}

既然将node加入队列成功了,后面肯定还会从队列中取出节点,一般这种FIFO队列都会使用 “生产者-消费者”模式,但是AQS却不是这么使用的,我们接着往下看。

【acquireQueued】方法

  1. 首先定义了一个failed变量,默认是true,如果当前线程正常获取到了锁,这个值就改为false,finally语句块里面的方法只是为了解决异常情况下,取消当前线程获取锁的行为
  2. 在AQS中,头节点head是个虚节点,即队列中的第一个节点的前一个节点是头节点
    1. 首先只有队列中的第一个节点,才有权限尝试获取锁,如果获取到锁,进入if中,然后返回结果,如果没有获取到锁,就进入下一个判断。
    2. 第二个if看判断条件,从名字上来看,首先判断当前线程是否需要挂起,如果需要挂起,就执行挂起操作,如果不需要,就继续自旋获取锁。
  3. 【shouldParkAfterFailedAcquire】判断获取锁失败后,是否需要挂起
    final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {final Node p = node.predecessor();// 如果是队列中的第一个节点,并且获取到了锁,就进入这个if中if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;return interrupted;}// 判断是否需要挂起,如果需要挂起就执行挂起操作,否则下一次for循环if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)// 取消当前线程获取锁的行为。处理try中的异常情况cancelAcquire(node);}}private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {int ws = pred.waitStatus;if (ws == Node.SIGNAL)// ws为-1,当前节点也在等待拿锁,因此可以挂起休息return true;if (ws > 0) {// 表示获取锁请求被取消了,就从当前队列中删除do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {// 将当前节点状态改为-1,然后外层重试compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;}private final boolean parkAndCheckInterrupt() {// 这一行就是执行了挂起操作。是通过unsafe的native方法操作系统原语LockSupport.park(this);// 被唤醒之后,返回当前线程有没有被中断。return Thread.interrupted();}

通过对acquireQueued 方法的分析,可以说,这个方法就是将当前队列中的线程节点都挂起,避免不必要的自旋浪费CPU资源。

既然有线程被挂起,那么就需要将这些挂起的线程唤醒,当持有锁的线程释放了锁,那么就会尝试唤醒后续的节点,AQS中提供了release方法用来唤醒挂起的线程。

5.3.3 tryRelease方法

和tryAcquire方法一样,tryRelease方法也是开放给上层实现的。

    protected boolean tryRelease(int arg) {throw new UnsupportedOperationException();}
5.3.4  release 方法

release方法中,假设尝试释放锁成功,下一步就要唤醒等待队列中的其他节点,unparkSuccessor方法中传入的是头节点。

【unparkSuccessor】方法

  1. 设置头节点的状态为0,表示已经释放了锁
  2. 获取队列中最靠前的一个准备获取锁的节点(状态不能是canceled:取消获取锁)
  3. 将这个节点唤醒,去争抢锁。
    public final boolean release(int arg) {if (tryRelease(arg)) {Node h = head;if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false;}private void unparkSuccessor(Node node) {// 获取头节点的状态,如果不是0,就修改为0,表示锁已经释放了int ws = node.waitStatus;if (ws < 0)compareAndSetWaitStatus(node, ws, 0);// 获取头节点的后续节点Node s = node.next;// 如果为空,或者处于canceled状态,那么就从后往前搜索,找到除head外最靠前的nodeif (s == null || s.waitStatus > 0) {s = null;for (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}// 唤醒这个node,让他起来尝试拿锁if (s != null)LockSupport.unpark(s.thread);}

【注意】 unparkSuccessor中搜索节点时是从后往前搜的,为什么这样操作呢?

我们前面介绍了addWaiter方法,后节点的pre指针先指向前节点,前节点的next指针才会指向后节点,这两个步骤不是原子性操作,因此,如果从前往后搜索,可能前节点的next还没有建立好,那么搜索可能就中断了。

6、ReentrantLock介绍

ReentrantLock被称为可重入锁,是JUC包中并发锁的实现之一,底层调用了AQS,这个锁还包含了公平锁和非公平锁的特性

6.1 ReentrantLock的属性

ReentrantLock中只有一个sync属性,sync属性被final修饰,意味着一旦ReentrantLock被初始化,sync属性就不可修改了。

ReentrantLock提供了两个构造器,通过名字可以看出来,我们可以通过传参的形式将ReentrantLock实例化为公平锁 或者是非公平锁。

    private final Sync sync;// 默认构造器,初始化为非公平锁public ReentrantLock() {sync = new NonfairSync();}// 入参为true的时候是公平锁,否则是非公平锁public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();}
6.2 ReentrantLock的内部类  - Sync

上面说了ReentrantLock存在一个Sync类型的构造器,那么一起看看这个内部类吧,这个内部类没有属性,除了lock和readObject方法,其余方法都用final修饰了,不希望被外部破坏。

  • 首先Sync内部类继承了AbstractQueuedSynchronizer,说明AQS中机制Sync都可以借用了,Sync被abstrat修饰,说明需要通过子类来进行实例化,NonfairSync / FairSync 后续会做相关介绍。
  • lock() 方法,加锁的抽象方法,需要子类来实现,后续介绍
  • nonfairTryAcquire() 方法,从名字来看,是获取非公平锁,在父类中定义,是因为外层有调用
  • tryRelease方法,返回当前锁是否完全释放
  • 其余的一些方法简单了解即可
    abstract static class Sync extends AbstractQueuedSynchronizer {private static final long serialVersionUID = -5179523762034025860L;abstract void lock();// 尝试获取非公平锁 final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();// 获取AQS的state属性int c = getState();if (c == 0) {// state属性为0说明锁空闲,通过cas来更改state,// 如果成功则获取到了锁,返回true,否则在下面返回falseif (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}// state不为0判断当前线程是否是独占线程---- 这就是可重入锁的实现// 是独占锁,则将state+1,并赋值回去,因此AQS中的state数量其实就是当前线程重入次数else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) // overflow// state是int类型的,16位,小于0就代表溢出了 -- 可重入的最大次数throw new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}// 释放锁 -- 注意返回值是“是否完全释放”protected final boolean tryRelease(int releases) {int c = getState() - releases;if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;if (c == 0) {free = true;setExclusiveOwnerThread(null);}setState(c);return free;}// 判断当前线程是否获取到锁protected final boolean isHeldExclusively() {return getExclusiveOwnerThread() == Thread.currentThread();}final ConditionObject newCondition() {return new ConditionObject();}// 获取占用锁的线程对象final Thread getOwner() {return getState() == 0 ? null : getExclusiveOwnerThread();}// 返回state的值final int getHoldCount() {return isHeldExclusively() ? getState() : 0;}// 判断锁是否空闲final boolean isLocked() {return getState() != 0;}// 反序列化,可以不关注private void readObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundException {s.defaultReadObject();setState(0); // reset to unlocked state}}

6.3 ReentrantLock的内部类  - FairSync
  • 公平锁,锁的分配会按照请求锁的顺序,比如按照AQS中的FIFO队列来排队获取锁,就是公平锁的实现。
  • 公平锁能够保证只要你排队了,就一定可以拿到锁。
    static final class FairSync extends Sync {private static final long serialVersionUID = -3000897897090466540L;// 调用AQS获取锁的逻辑final void lock() {acquire(1);}// 重写AQS的tryAcquire方法protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();// 锁空闲,并且AQS队列中没有其他节点,那么就尝试取锁 --- 体现了公平性if (c == 0) {if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {// 如果获取锁的是当前线程,就state+1 -- 可重入int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}}
6.4 ReentrantLock的内部类  - NonFairSync
  • 非公平锁:获取锁不会按照锁请求顺序,比如抢占式
  • 非公平锁不会保证排队的线程一定会获取锁,某个线程可能一直处于阻塞状态,称为“饥饿”
  • 为什么设计非公平锁呢? --- 某些时候,唤醒已经挂起的线程,这个线程的状态切换会产生短暂的延时,而这个延时时间可能就够进行一次业务处理,因此非公平锁可利用这段时间完成操作,提高效率。
    static final class NonfairSync extends Sync {private static final long serialVersionUID = 7316153563782823691L;// 加锁,先尝试进行一次CAS,失败则进入队列排队// 因为AQS的acquire方法中调用了tryAcquire,下面又重写了tryAcquire方法// 结合nonfairTryAcquire方法代码,可以看到,非公平锁是先两次尝试获取锁,失败之后在排队拿锁final void lock() {if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);}// 直接调用父类的nonfairTryAcquire方法。protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);}}

6.5 ReentrantLock的公共方法 -  lock / tryLock / unlock
  1. lock方法 : 加锁的方法很简单,就是通过sync属性去调用,sync的lock方法在上述内部类中都已经介绍了。-- 多态
  2. tryLock方法:无论sync的实现是否是公平锁,tryLock的实现都是非公平的 --- nonfairTryAcquire方法写在Sync中的原因
  3. unlock :释放锁操作,每次对于每次执行,state-1
    public void lock() {sync.lock();}// 无论sync的实现是否是公平锁,tryLock的实现都是非公平的public boolean tryLock() {return sync.nonfairTryAcquire(1);}// 含参的,给了个超时时间public boolean tryLock(long timeout, TimeUnit unit)throws InterruptedException {return sync.tryAcquireNanos(1, unit.toNanos(timeout));}public void unlock() {sync.release(1);}
6.6  ReentrantLock 类的使用介绍
6.6.1 使用ReentrantLock进行普通加锁

功能类似于synchronized关键字,获取到锁的线程释放锁之后,其他线程才可以获取到锁。

public class TestReentrantLock {private static final Lock lock = new ReentrantLock();public void testLock() {try {lock.lock();System.out.println("Thread name: " + Thread.currentThread().getName() + " lock");Thread.sleep(2000);lock.unlock();System.out.println("Thread name: " + Thread.currentThread().getName() + " unlock");} catch (InterruptedException e) {throw new RuntimeException(e);}}public static void main(String[] args) {TestReentrantLock testLock = new TestReentrantLock();// 起两个线程,第一个线程释放锁之后,第二个线程才可以获取到锁new Thread(new Runnable() {@Overridepublic void run() {testLock.testMethod();}}).start();new Thread(new Runnable() {@Overridepublic void run() {testLock.testMethod();}}).start();}
}------print------------
Thread name: Thread-0 lock
Thread name: Thread-0 un lock
Thread name: Thread-1 lock
Thread name: Thread-1 un lock

6.6.2 使用ReentrantLock实现锁的可入

main方法中先加锁,,然后在解锁前调用testLock方法,因为是在一个线程中,所以不需要释放锁,也可以获取到锁。

public class TestReentrantLock {private static final Lock lock = new ReentrantLock();public void testLock() {try {lock.lock();System.out.println("Thread name: " + Thread.currentThread().getName() + " 加锁");Thread.sleep(2000);lock.unlock();System.out.println("Thread name: " + Thread.currentThread().getName() + " 解锁");} catch (InterruptedException e) {throw new RuntimeException(e);}}public static void main(String[] args) {TestReentrantLock testLock = new TestReentrantLock();// main线程先加锁lock.lock();System.out.println(Thread.currentThread().getName() + " 线程获得了锁");testLock.testLock();lock.unlock();System.out.println(Thread.currentThread().getName() + " 线程释放了锁");}
}------print-----
main 线程获得了锁
Thread name: main 加锁
Thread name: main 解锁
main 线程释放了锁

7、CountDownLatch 介绍

CountDownLatch是JUC工具包中很重要的一个同步工具。CountDownLatch基于AQS, 他的作用是,让某一个线程等待多个线程的操作完成之后,再执行自己的操作。

CountDownLatch定义了一个计数器和一个阻塞队列,当计数器的值递减为0之前,阻塞队列里面的线程,

7.1、CountDownLatch 的使用。
public class DemoCountDownLatch {private final static Random random = new Random();static class SearchTask implements Runnable {private int id;private CountDownLatch latch;public SearchTask(int id, CountDownLatch latch) {this.id = id;this.latch = latch;}@Overridepublic void run() {System.out.println("开始寻找" + id + "号龙珠");int seconds = random.nextInt(10);try {Thread.sleep(seconds * 1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("找到" + id + "号龙珠" + "花费了" + seconds + "秒时间");/*** 调用countDown() 方法,计数器会减一*/latch.countDown();}}public static void main(String[] args) throws InterruptedException {List<Integer> idList = Arrays.asList(1, 2, 3, 4, 5, 6, 7);/*** CountDownLatch初始化的时候,给定一个整数计数器,不可变*/CountDownLatch latch = new CountDownLatch(idList.size());for (Integer id : idList) {Thread thread = new Thread(new SearchTask(id, latch));thread.start(); // 注意,start 是开启新线程多线程执行,run方法是串行的}/*** 调用 await() 方法时 ,如果计数器大于0,当前线程阻塞,直到计数器被countDown方法减到0时,线程才会继续执行*/latch.await();/*** 调用 await方法时,设置超时参数* 如果计数器大于0,当前线程阻塞,直到计数器被countDown方法减到0时,线程才会继续执行*/// latch.await(3, TimeUnit.SECONDS);System.out.println("所有龙珠找到!召唤神龙!");}
}-----await()   print ------------
----- 子线程都执行完了,主线程才开始执行 ----------
开始寻找1号龙珠
开始寻找3号龙珠
开始寻找2号龙珠
开始寻找5号龙珠
开始寻找6号龙珠
开始寻找4号龙珠
开始寻找7号龙珠
找到3号龙珠花费了3秒时间
找到1号龙珠花费了3秒时间
找到7号龙珠花费了5秒时间
找到6号龙珠花费了5秒时间
找到2号龙珠花费了6秒时间
找到4号龙珠花费了6秒时间
找到5号龙珠花费了8秒时间
所有龙珠找到!召唤神龙!-----await(3, TimeUnit.SECONDS)   print  ------------
--- 设置了三秒超时,三秒之后无论子线程有没有全部结束,都执行主线程 ------
开始寻找1号龙珠
开始寻找3号龙珠
开始寻找2号龙珠
开始寻找4号龙珠
开始寻找5号龙珠
开始寻找7号龙珠
开始寻找6号龙珠
找到5号龙珠花费了3秒时间
所有龙珠找到!召唤神龙!
找到6号龙珠花费了5秒时间
找到7号龙珠花费了8秒时间
找到2号龙珠花费了8秒时间
找到1号龙珠花费了8秒时间
找到4号龙珠花费了9秒时间
找到3号龙珠花费了9秒时间-----不执行await   print ------------
--- 主线程直接执行,不等待子线程 ------
开始寻找3号龙珠
所有龙珠找到!召唤神龙!
开始寻找4号龙珠
开始寻找5号龙珠
开始寻找6号龙珠
开始寻找2号龙珠
开始寻找7号龙珠
开始寻找1号龙珠
找到1号龙珠花费了2秒时间
找到3号龙珠花费了3秒时间
找到4号龙珠花费了4秒时间
找到2号龙珠花费了5秒时间
找到5号龙珠花费了6秒时间
找到6号龙珠花费了9秒时间
找到7号龙珠花费了9秒时间

7.2、CountDownLatch设计思路?

CountDownLatch,我们就简单的描述为,主线程等待子线程处理完任务之后在继续执行自己的任务。既然主线程在等待,根据前面学习的AQS,此时主线程应该放入等待队列中,那么什么时候唤醒主线程呢?当然是子任务都执行结束之后,那么AQS中的state就可以派上用场了,state表示子线程的数量(也就是主线程需要等待的线程数目),每当一个任务完成了,state就减去1,当state值为0 的时候,就唤醒正在等待的主线程。

CountDownLatch 的设计思路大致就是上面介绍的,下面会根据源码来进行剖析。

7.2.1 sync内部类

CountDownLatch内部也定义了一个sync内部类,并继承了AQS;

【tryAcquireShared】方法,这个方法是尝试获取共享锁,是对AQS方法的一个重写。

这个方法很简单,获取state的值,如果等于0,就返回1,否则返回-1;


子类对父类方法的重写,也是要按照约定去重写的,我们在看看AQS中对tryAcquireShared方法的定义:

  • 返回负值:获取锁失败
  • 返回0:共享模式下获取锁成功,不唤醒后续节点
  • 返回整数:获取锁成功,并唤醒后续节点

从父类方法定义来看,如果tryAcquireShared方法返回整数,是需要获取锁的,但是子类实现的方法并没有获取锁的操作,这个是为什么呢?

----

实际上,我们需要从CountDownLatch这个组件需要解决的问题来看待,当CountDownLatch被初始化时,必须传入一个count,这个值会赋给aqs的state,每当一个子任务完成,state值就会减一,一直到state为0,然后主任务继续操作。这其实就是一个子任务不断释放锁,主任务不断检查锁有没有完全释放的过程,所有的操作不涉及到加锁的情况,虽然主任务在state为0的时候也可以加锁,但是完全没有必要。

【tryReleaseShared】方法,就是一个释放锁的操作,

锁完全释放返回true,否则返回false

    private static final class Sync extends AbstractQueuedSynchronizer {private static final long serialVersionUID = 4982264981922014374L;// 构造方法,需要传入一个count值Sync(int count) {setState(count);}// 获取当前count值int getCount() {return getState();}// 对AQS的重写,从名字看,方法被shared修饰,应该是用到了共享模式// 共享模式下,state可以被多个线程同时修改,加1代表获取共享锁,减1代表释放共享锁// 这个方法就是如果state为0,代表子任务全部完成,否则就是还有没完成的protected int tryAcquireShared(int acquires) {return (getState() == 0) ? 1 : -1;}// 释放锁的操作,一个自旋的过程// 不需要释放锁或者没有完全释放锁,返回false// 锁完全释放了,返回trueprotected boolean tryReleaseShared(int releases) {// Decrement count; signal when transition to zerofor (;;) {int c = getState();if (c == 0)return false;int nextc = c-1;if (compareAndSetState(c, nextc))return nextc == 0;}}}
7.2.2 内部属性和构造器

内部属性只有一个sync,只有一个有参数构造器,必须传递一个大于0的整数(主线程需要等待子线程的数量。)

    private final Sync sync;public CountDownLatch(int count) {if (count < 0) throw new IllegalArgumentException("count < 0");this.sync = new Sync(count);}
7.2.3 await 方法

这个方法就是主线程等待子线程执行完的逻辑。

    public void await() throws InterruptedException {sync.acquireSharedInterruptibly(1);}// AQS的方法public final void acquireSharedInterruptibly(int arg)throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();// 锁没有完全释放的情况,也就是主线程等待子线程的场景if (tryAcquireShared(arg) < 0)doAcquireSharedInterruptibly(arg);}private void doAcquireSharedInterruptibly(int arg)throws InterruptedException {// 设置共享模式的nodefinal Node node = addWaiter(Node.SHARED);boolean failed = true;try {for (;;) {final Node p = node.predecessor();// 当前节点的前置节点为head,说明当前节点可以被唤醒if (p == head) {int r = tryAcquireShared(arg);if (r >= 0) {// 到这个判断里面说明锁已经空闲了,也就是子任务都执行结束了setHeadAndPropagate(node, r);p.next = null; // help GCfailed = false;return;}}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())throw new InterruptedException();}} finally {if (failed)cancelAcquire(node);}}private void setHeadAndPropagate(Node node, int propagate) {Node h = head; // Record old head for check belowsetHead(node);// 传播行为大于0,唤醒后续节点// 被唤醒的节点会在doAcquireSharedInterruptibly方法中的for循环继续执行// 不断的唤醒队列中处于等待状态的且共享模式的线程。if (propagate > 0 || h == null || h.waitStatus < 0 ||(h = head) == null || h.waitStatus < 0) {Node s = node.next;if (s == null || s.isShared())doReleaseShared();}}
7.2.4 countDown方法

就是为了保证state的自减操作,调用此方法,state减1

    public void countDown() {sync.releaseShared(1);}public final boolean releaseShared(int arg) {if (tryReleaseShared(arg)) {doReleaseShared();return true;}return false;}public final boolean releaseShared(int arg) {if (tryReleaseShared(arg)) {doReleaseShared();return true;}return false;}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/672861.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【flink状态管理(2)各状态初始化入口】状态初始化流程详解与源码剖析

文章目录 1. 状态初始化总流程梳理2.创建StreamOperatorStateContext3. StateInitializationContext的接口设计。4. 状态初始化举例&#xff1a;UDF状态初始化 在TaskManager中启动Task线程后&#xff0c;会调用StreamTask.invoke()方法触发当前Task中算子的执行&#xff0c;在…

常用的前端模块化标准总结

1、模块化标准出现以前使用的模块化方案&#xff1a; 1&#xff09;文件划分&#xff1a; 将不同的模块定义在不同的文件中&#xff0c;然后使用时通过script标签引入这些文件 缺点&#xff1a; 模块变量相当于是定义在全局的&#xff0c;容易造成变量名冲突&#xff08;即不…

flink反压及解决思路和实操

1. 反压原因 反压其实就是 task 处理不过来&#xff0c;算子的 sub-task 需要处理的数据量 > 能够处理的数据量&#xff0c;比如&#xff1a; 当前某个 sub-task 只能处理 1w qps 的数据&#xff0c;但实际上到来 2w qps 的数据&#xff0c;但是实际只能处理 1w 条&#…

Qt信号和槽机制(什么是信号和槽,connect函数的形式,按钮的常用信号,QWidget的常用槽,自定义槽函数案例 点击按钮,输出文本)

一.什么是信号和槽 信号槽式Qt中的一个很重要的机制。信号槽实际上是观察者模式,当发生了感兴趣的事件&#xff0c;某一个操作就会被自动触发。当某个事件发生之后&#xff0c;比如按钮检测到自己被点击了一下&#xff0c;它就会发出一个信号。这种发出类似广播。如果有对象对…

ArcGIS学习(五)坐标系-2

3.不同基准面坐标系之间的转换 在上一关中,我们学习了ArcGIS中的投影(投影栅格)工具,并以"WGS1984地理坐标系与WGS1984的UTM投影坐标系的转换”为例进行讲解。 "WGS1984地理坐标系与WGS1984的UTM投影坐标系的转换”代表的是同一个基准面下的两个坐标的转换。 …

人工智能 | 深度学习的进展

深度学习的进展 深度学习是人工智能领域的一个重要分支&#xff0c;它利用神经网络模拟人类大脑的学习过程&#xff0c;通过大量数据训练模型&#xff0c;使其能够自动提取特征、识别模式、进行分类和预测等任务。近年来&#xff0c;深度学习在多个领域取得了显著的进展&#…

cesium mapboxgl+threebox glb 朝向问题

一、3Dbuilder打开glb 二、cesium在pitch和heading都为0的情况下&#xff0c;不设置模型的朝向 三、mapboxglthreebox在pitch和bearing都为0的情况下&#xff0c;不设置模型的朝向 四、对于地图默认视角&#xff0c;cesium设置pitch-90、heading0的时候和mapboxglthreebox设置p…

光学PCIe 6.0技术引领AI时代超大规模集群

随着云计算、大数据和人工智能技术的快速发展&#xff0c;超大规模数据中心正经历一场前所未有的变革。传统的集中式架构逐渐转变为解聚式&#xff08;disaggregated&#xff09;架构&#xff0c;这种架构将计算、存储和网络资源从单一的物理服务器中分离出来&#xff0c;形成独…

前端vite+vue3——自动化配置路由布局

文章目录 ⭐前言&#x1f496;vue3系列文章 ⭐ 自动化配置路由&#x1f496;引入vite版本自定义目录映射&#x1f496;自动化读取文件下的路由&#x1f496;main入口加载路由&#x1f496;入口app.vue配置&#x1f496;layout基础布局配置&#x1f496;效果 ⭐总结⭐结束 ⭐前言…

text-generation-webui搭建大模型运行环境与踩坑记录

text-generation-webui搭建大模型运行环境 text-generation-webui环境初始化准备模型启动项目Bug说明降低版本启动项目 text-generation-webui text-generation-webui是一个基于Gradio的LLM Web UI开源项目&#xff0c;可以利用其快速搭建部署各种大模型环境。 环境初始化 下载…

【漏洞复现】EPON上行A8-C政企网关未授权下载漏洞

Nx01 产品简介 EPON上行A8-C政企网关是一款终端产品&#xff0c;提供企业网络解决方案。 Nx02 漏洞描述 EPON上行A8-C政企网关配置文件未授权下载漏洞&#xff0c;攻击者在未授权状态下下载配置文件&#xff0c;获取配置文件内敏感信息。 Nx03 产品主页 fofa-query: "Z…

Retinexformer论文精读笔记

Retinexformer论文精读笔记 论文为2023年ICCV的Retinexformer: One-stage Retinex-based Transformer for Low-light Image Enhancement。论文链接&#xff1a;browse.arxiv.org/pdf/2303.06705.pdf&#xff0c;代码链接&#xff1a;caiyuanhao1998/Retinexformer: “Retinexfo…

Mac 下载安装Java、maven并配置环境变量

下载Java8 下载地址&#xff1a;https://www.oracle.com/java/technologies/downloads/ 根据操作系统选择版本 没有oracle账号需要注册、激活登录 mac直接选择.dmg文件进行下载&#xff0c;下载后安装。 默认安装路径&#xff1a;/Library/Java/JavaVirtualMachines/jdk-1…

【C#】.net core 6.0 创建默认Web应用,以及默认结构讲解,适合初学者

欢迎来到《小5讲堂》 大家好&#xff0c;我是全栈小5。 这是《C#》系列文章&#xff0c;每篇文章将以博主理解的角度展开讲解&#xff0c; 特别是针对知识点的概念进行叙说&#xff0c;大部分文章将会对这些概念进行实际例子验证&#xff0c;以此达到加深对知识点的理解和掌握。…

SpringCloud-创建多模块项目

在微服务架构中&#xff0c;项目的组织结构对于代码的维护和团队的协作至关重要。Spring Cloud作为一个强大的微服务框架&#xff0c;提供了丰富的功能和组件&#xff0c;同时也支持多模块项目的创建&#xff0c;使得代码结构更加清晰、易于管理。本文将介绍如何使用 Spring Cl…

HTML5+CSS3+移动web——HTML 基础

目录 一、标签语法 HTML的基本框架 1. 标题标签 2. 段落标签 3. 换行和水平线 4. 文本格式化标签 5. 图像标签 6. 路径 相对路径 绝对路径 7. 超链接标签 8. 音频 9. 视频 10. 注释 二、标签结构 一、标签语法 HTML 超文本标记语言——HyperText Markup Langua…

Kubernetes基础(十四)-k8s网络通信

1 k8s网络类型 2 Pod网络 2.1 同一pod内不同容器通信 Pod是Kubernetes中最小的可部署单元&#xff0c;它是一个或多个紧密关联的容器的组合&#xff0c;这些容器共享同一个网络命名空间和存储卷&#xff0c;因此Pod中的所有容器都共享相同的网络命名空间和IP地址——PodIP&a…

成员对象与封闭类

1. 成员对象与封闭类 类里有其他对象则该对象叫成员对象&#xff1b;有成员对象的类叫 封闭类&#xff1b;上例中&#xff0c;如果CCar类不定义构造函数&#xff0c;则会使用默认的无参构造函数&#xff0c;那么下面的语句会编译出错: 因为编译器不明白CCar类中的tyre成员对象…

C语言:函数

创作不易&#xff0c;友友们给个三连吧&#xff01;&#xff01; 一、函数的概念 数学中我们见过函数的概念&#xff0c;例如ykxb&#xff0c;k和b都是常数&#xff0c;给任意一个x就可以得到y 而C语言也引入了函数&#xff08;function&#xff09;这个概念&#xff0c;C语…

生成式学习,特别是生成对抗网络(GANs),存在哪些优点和缺点,在使用时需要注意哪些注意事项?

生成对抗网络&#xff08;GANs&#xff09; 1. 生成对抗网络&#xff08;GANs&#xff09;的优点&#xff1a;2. 生成对抗网络&#xff08;GANs&#xff09;的缺点&#xff1a;3. 使用生成对抗网络&#xff08;GANs&#xff09;需要注意的问题 1. 生成对抗网络&#xff08;GANs…