并发编程 java锁机制

news2024/11/15 4:22:40
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() {
            @Override
            public 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();
            else
                return 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 initialize
                if (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 GC
                    failed = 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外最靠前的node
        if (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,否则在下面返回false
                if (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());
            else
                acquire(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() {
            @Override
            public void run() {
                testLock.testMethod();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public 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;
        }

        @Override
        public 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
        // 锁完全释放了,返回true
        protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                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 {
        // 设置共享模式的node
        final 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 GC
                        failed = 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 below
        setHead(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.coloradmin.cn/o/1440408.html

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

相关文章

不到1s生成mesh! 高效文生3D框架AToM

论文题目&#xff1a; AToM: Amortized Text-to-Mesh using 2D Diffusion 论文链接&#xff1a; https://arxiv.org/abs/2402.00867 项目主页&#xff1a; AToM: Amortized Text-to-Mesh using 2D Diffusion 随着AIGC的爆火&#xff0c;生成式人工智能在3D领域也实现了非常显著…

【人工智能】横扫市场的巨星大模型:探秘当今最热门的AI力量(14)

今年&#xff0c;ChatGPT成了大家的明星&#xff0c;简直是个神奇的助手&#xff01;问什么问题&#xff0c;都秒回&#xff0c;写各种文字、甚至代码&#xff0c;简直是工作利器。而国内这半年AI领域热度不减&#xff0c;涌现了一批新公司和产品&#xff0c;大厂也在风头上。A…

【学习笔记】TypeScript编译选项

TS 中的编译选项 我们写了一个TS的文件&#xff0c;我们需要使用如下的命令将我们的TS文件转换为JS文件。 tsc xxx.ts 这样会产生一个对应的js文件 自动编译文件 编译文件时&#xff0c;使用 -W 指令后&#xff0c;TS编译器会自动监视文件的变化&#xff0c;并在文件发生变…

MySQL篇之定位与优化MySQL慢查询

一、如何定位慢查询 1.方案一&#xff1a;开源工具 调试工具&#xff1a;Arthas。 运维工具&#xff1a;Prometheus 、Skywalking。 2.方案二&#xff1a;MySQL自带慢日志 慢查询日志记录了所有执行时间超过指定参数&#xff08;long_query_time&#xff0c;单位&#xff1a;…

Habitat环境学习四:Habitat-sim基础用于导航——使用导航网格NavMesh

如何使用导航网格NavMesh 官方教程1、NavMesh基础定义1.1 使用NavMesh的原因1.2 什么是NavMesh 2、NavMesh的使用方法2.1 获取自上而下Top down view视角地图2.2 在NavMesh中进行查询以及随机产生可导航点2.3 查找最短路径2.4 场景加载NavMesh2.5 重新计算并生成NavMesh2.6 什么…

python-分享篇-画樱花

文章目录 画樱花代码效果 画樱花 代码 from turtle import * from random import * from math import *def tree(n,l):pd()#下笔#阴影效果t cos(radians(heading()45))/80.25pencolor(t,t,t)pensize(n/3)forward(l)#画树枝if n>0:b random()*1510 #右分支偏转角度c ran…

openGauss学习笔记-216 openGauss性能调优-确定性能调优范围-硬件瓶颈点分析-CPU

文章目录 openGauss学习笔记-216 openGauss性能调优-确定性能调优范围-硬件瓶颈点分析-CPU216.1 CPU216.2 查看CPU状况216.3 性能参数分析 openGauss学习笔记-216 openGauss性能调优-确定性能调优范围-硬件瓶颈点分析-CPU 获取openGauss节点的CPU、内存、I/O和网络资源使用情况…

神经网络 | 常见的激活函数

Hi&#xff0c;大家好&#xff0c;我是半亩花海。本文主要介绍神经网络中必要的激活函数的定义、分类、作用以及常见的激活函数的功能。 目录 一、激活函数定义 二、激活函数分类 三、常见的几种激活函数 1. Sigmoid 函数 &#xff08;1&#xff09;公式 &#xff08;2&a…

Open CASCADE学习|点和曲线的相互转化

目录 1、把曲线离散成点 1.1按数量离散 1.2按长度离散 1.3按弦高离散 2、由点合成曲线 2.1B样条插值 2.2B样条近似 1、把曲线离散成点 计算机图形学中绘制曲线&#xff0c;无论是绘制参数曲线还是非参数曲线&#xff0c;都需要先将参数曲线进行离散化&#xff0c;通过离…

JavaScript综合练习1

JavaScript 综合练习 1 1. 循环嵌套 两个以上的循环进行套娃。 1.1. 语法 for (外部声明记录循环次数的变量; 循环条件; 变化值) {for (内部声明记录循环次数的变量; 循环条件; 变化值) {循环体;} }2. 倒三角 2.1. 案例 2.2. 代码实现 // 外层打印几行 for (let i 1; i …

【高阶数据结构】位图布隆过滤器

文章目录 1. 位图1.1什么是位图1.2为什么会有位图1.3 实现位图1.4 位图的应用 2. 布隆过滤器2.1 什么是布隆过滤器2.2 为什么会有布隆过滤器2.3 布隆过滤器的插入2.4 布隆过滤器的查找2.5 布隆过滤器的模拟实现2.6 布隆过滤器的优点2.7 布隆过滤器缺陷 3. 海量数据面试题3.1 哈…

在windows的控制台实现贪吃蛇小游戏

欢迎来到博主的文章 博主id&#xff1a;代码小豪 前言&#xff1a;看懂这篇文章需要具有C语言基础&#xff0c;还要对单链表具有一定的理解。如果你只是想要试玩这个游戏&#xff0c;可以直接在文章末尾找到源码 由于实现贪吃蛇需要调用Win32 API函数&#xff0c;这些函数我会…

vue3项目中的404页面

vue3项目中的404页面 春节前的最后一篇技术博客了 写了不少vue项目&#xff0c;发现一直没有正确处理404页面。404页面的出现有这么几种可能&#xff1a; 错误输入了页面地址路由连接跳转时&#xff0c;某些路由已经不存在了&#xff0c;而程序员并没有正确处理 也就是说40…

jvm问题自查思路

本文聊一下最近处理了一些jvm的问题上&#xff0c;将这个排查和学习过程分享一下&#xff0c;看了很多资料&#xff0c;最终都会落地到几个工具的使用&#xff0c;本文主要是从文档学习、工具学习和第三方技术验证来打开认知和实践&#xff0c;希望有用。 一、文档 不仅知道了…

假期刷题打卡--Day26

1、MT1212乘法表 请编写一个简单程序&#xff0c;输出九九乘法表。输入n&#xff0c;就输出乘法表到n的地方。 格式 输入格式&#xff1a; 输入整型 输出格式&#xff1a; 输出整型。形式如&#xff1a;1*11 样例 1 输入&#xff1a; 5输出&#xff1a; 1*11 2*12 …

编曲入门软件哪个好 编曲入门教程 Studio One哪个版本好 Studio One6.5正版多少钱 FL Studio下载

新手编曲软件推荐&#xff1f;新手学编曲要先熟悉编曲逻辑&#xff0c;因此需要选择编曲逻辑简明易懂的宿主软件。编曲新手应该做哪些准备&#xff1f;准备好编曲设备、宿主软件、基础乐理学习资料。 一、编曲入门软件哪个好 新手入门阶段还没有形成系统的编曲思维&#xff0…

整合 Axios

大家好我是苏麟 , 今天聊一下Axios . Axios Axios 是一个基于 promise 网络请求库&#xff0c;作用于node.js 和浏览器中。 它是 isomorphic 的(即同一套代码可以运行在浏览器和node.js中)。在服务端它使用原生 node.js http 模块, 而在客户端 (浏览端) 则使用 XMLHttpReques…

七、Nacos源码系列:Nacos服务发现

目录 一、服务发现 二、getServices()&#xff1a;获取服务列表 2.1、获取服务列表 2.2、总结图 三、getInstances(serviceId)&#xff1a;获取服务实例列表 3.1、从缓存中获取服务信息 3.2、缓存为空&#xff0c;执行订阅服务 3.2.1、调度更新&#xff0c;往线程池中…

【算法】{画决策树 + dfs + 递归 + 回溯 + 剪枝} 解决排列、子集问题(C++)

文章目录 1. 前言2. 算法例题 理解思路、代码46.全排列78.子集 3. 算法题练习1863.找出所有子集的异或总和再求和47.全排列II17.电话号码的字母组合 1. 前言 dfs问题 我们已经学过&#xff0c;对于排列、子集类的问题&#xff0c;一般可以想到暴力枚举&#xff0c;但此类问题用…

基于Chrome插件的Chatgpt对话无损导出markdown格式(Typora完美显示)

刚刚提交插件到Chrome插件商店正在审核&#xff0c;想尝试的可以先使用&#xff1a; https://github.com/thisisbaiy/ChatGPT-To-Markdown-google-plugin/tree/main 我将源代码上传至了GitHub&#xff0c;欢迎star, IssueGoogle插件名称为&#xff1a;ChatGPT to MarkDown plus…