【学习JVM】JAVA并发工具

58 篇文章 0 订阅

JVM并发同步机制

需要参考的准备数据:

《深入理解JAVA虚拟机》

《java并发编程的艺术》

需要参考的知识点:

JVM并发同步机制

重要笔记:
  • Reentrantlock基于AQS实现。
  • AQS基于Volatile与CAS实现,在unlock修改volatile变量时不存在多线程访问(unlock在临界区内),所以不需要CAS指令。
  • CAS的底层通过操作系统的cmpxchg指令实现。(如果是单线程,非并行的情况下,不会执行这个指令。即通过LOCK_IF_MP(mp)来判断是否使用cmpxchg)
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VqDfFkdw-1579505051286)(https://i.loli.net/2019/04/26/5cc2ca011d5ab.png)]
  • 线程有6个主要状态。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JJHbJBId-1579505051288)(https://i.loli.net/2019/04/26/5cc2ca01ddc21.png)]
  • LockSupport(LockSupport不是一个lock,而是一个support,它封装了lock需要的support函数)中的park和unpark调用的是Unsafer的park和unpark方法,这两个方法在Linux系统下,是用的Posix线程库pthread中的mutex(互斥量),condition(条件变量)来实现的。
    mutex和condition保护了一个_counter的变量,当park时,这个变量被设置为0,当unpark时,这个变量被设置为1。
    也就说,这两个方法的本质依然是系统调用的阻塞方法。最底层和syntronzied并无区别。
  • 守护进程的finally语句块不一定会执行(不可靠)
中断的理解:

首先,一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。所以,Thread.stop, Thread.suspend, Thread.resume 都已经被废弃了。而 Thread.interrupt 的作用其实也不是中断线程,而是「通知线程应该中断了」,具体到底中断还是继续运行,应该由被通知的线程自己处理。具体来说,当对一个线程,调用 interrupt() 时,① 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。仅此而已。② 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。也就是说,一个线程如果有被中断的需求,那么就可以这样做。① 在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程。② 在调用阻塞方法时正确处理InterruptedException异常。(例如,catch异常后就结束线程。)
作者:Intopass
链接:https://www.zhihu.com/question/41048032/answer/89431513
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注>明出处。

为什么objectmonitor有两个队列呢(阻塞队列和等待队列)?

因为当线程执行wait时除了释放锁以外,还会被送进等待队列,在等待队列里的线程不会再参与monitor的锁竞争。需要等待notify触发后通知才会加入到阻塞队列中参与后续锁的竞争。

notify和notifyAll的区别:notify只会将一个等待队列的线程加入到阻塞队列,而notifyAll则会把所有等待队列的线程全部加进去。

wait(timeout)方法会在等待超时后自动将线程从等到队列转移到阻塞队列参与锁的竞争。

总结:
  1. 阻塞队列位于ObjectMonitor中,专门管理临界区互斥的工具。
  2. 等待队列则是一个用来存放不使用CPU资源线程的集合,通过wait、join(底层通过thread对象的wait/notifyAll实现)、park、sleep命令线程停止使用CPU,并进入等待队列(park,sleep,wait他们所在等待队列不同)。
    3.从等待队列进入阻塞队列的方法,可以通过“notify”“超时”“unpark”。
    4.park与wait在应用上的区别就是,wait需要获得monitor的情况下调用才有效果,底层上的区别则是他们虽然都是通过mutex与同步条件实现,但是具体的实现方式完全不同,即便是等待队列,也不是同一个等待队列。唯一相同就是他们触发后的线程状态都是“WAITING”

LockSupport并不需要获取对象的监视器。LockSupport机制是每次unpark给线程1个“许可”——最多只能是1,而park则相反,如果当前 线程有许可,那么park方法会消耗1个并返回,否则会阻塞线程直到线程重新获得许可,在线程启动之前调用park/unpark方法没有任何效果。

// 1次unpark给线程1个许可
LockSupport.unpark(Thread.currentThread());
// 如果线程非阻塞重复调用没有任何效果
LockSupport.unpark(Thread.currentThread());
// 消耗1个许可
LockSupport.park(Thread.currentThread());
// 阻塞
LockSupport.park(Thread.currentThread());

因为它们本身的实现机制不一样,所以它们之间没有交集,也就是说>LockSupport阻塞的线程,notify/notifyAll没法唤醒。

实际上现在很少能看到直接用wait/notify的代码了,即使生产者/消费者也基本都会用Lock和Condition来实现


  • lock()方法和syntronized在阻塞队列中竞争锁的时候无法被中断,Reentrantlock.lockInterruptibly()throws InterruptedException就可以响应中断。
  • AQS利用了volatile与CAS还有自定义的队列机制实现了syntronized的所有功能。它是基于JAVA API对同步器的一个实现,用户可以利用它实现一些复杂的功能,比如Reentrantlock、reentrantreadwritelock、Countdownlatch.
  • AQS同步器利用模版模式提供了拓展接口。可重写的方法如下:
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5NyVBpXo-1579505051288)(https://i.loli.net/2019/04/26/5cc2ca01d683d.png)]
    tips: AbstractQueuedSyntronizer比较复杂,很多时候看文档和书籍并无法完全理解,最好要和源码相结合。

AbstractQueuedSyntronizer

tips: 在下面文字中同步队列与阻塞队列指的同一个东西

AbstractQueuedSyntronizer 它是基于Java Api实现的一个类似syntronized同步机制。
它的功能比syntronized强大很多,也相对复杂,性能方面无从比较。
为了理解它我们先把它和syntronized相似的也就是最基本的功能进行类比学习。

  • acquire(int arg)它是一个获取互斥锁的函数,功能上相当于monitorenter(竞争获取objectmonitor.owner,如果失败就会送入阻塞队列进行阻塞,如果被唤醒就检测是否有中断信号)。
/**
     * Acquires in exclusive mode, ignoring interrupts.  Implemented
     * by invoking at least once {@link #tryAcquire},
     * returning on success.  Otherwise the thread is queued, possibly
     * repeatedly blocking and unblocking, invoking {@link
     * #tryAcquire} until success.  This method can be used
     * to implement method {@link Lock#lock}.
     *
     * @param arg the acquire argument.  This value is conveyed to
     *        {@link #tryAcquire} but is otherwise uninterpreted and
     *        can represent anything you like.
     */
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
  • acquire(int arg)被调用时,会执行tryAcquire(arg)尝试获取竞争条件(检测竞争锁资源的状态,如果可以获得锁,就直接返回,否则就会addWaiter进入阻塞队列,并且park线程,之后进入中断检测状态)

  • release(int arg) 被调用时会修改竞争状态,并且将阻塞队列中的头节点退出来,unpark后加入下一轮竞争,这个方法类似于syntronized的monitorexit。

  • acquireInterruptibly 被中断唤醒后会触发中断异常

  • Mutex.java是组合AbtractQueuedSyntronizer的子类实现Lock接口最简单最经典的例子,它和可重入的最大区别在于,reentrantlock可以重入,而Mutex.java不可以,mutex只能lock一次。

  • reentrantlock允许被同一个线程lock很多次,但是要unlock一样的次数才可以让AQS的status为0,达到释放锁的效果。

  • AQS的同步队列(阻塞队列)采用的是基于双向链表实现的FIFO队列。从队列头部唤醒需要同步状态的节点,从队列尾部加入阻塞节点。

  • 释放锁这个操作不会出现竞争,因为释放锁都在临界区进行,所以不需要CAS来修改状态,从同步队列头部移除线程节点时也不需要CAS。(加入同步队列以及获取同步状态status则需要CAS)

  • 从尾部加入同步队列采用的是CAS乐观锁的方式,对乐观锁的补偿方法是for(;😉{}重试到成功位置。

  • acquire()过程不会涉及waitStatus。waitStatus是在涉及condition.await()的时候才需要考虑。

  • 如果线程A加入阻塞队列时发现,阻塞队列为空,在enq(final Node node)函数初始化阻塞队列,使其头尾指针都指向A线程的Node,并且在中让node.prev和node.next也指向A的node节点自己。
    源代码如下(代码采用了乐观锁加重试机制对head与tail进行设置):

 /**
     * Inserts node into queue, initializing if necessary. See picture above.
     * @param node the node to insert
     * @return node's predecessor
     */
    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;
                }
            }
        }
    }
  • 每次release都会唤醒头节点参与条件竞争,这是遵守FIFO原则。代码如下:
 /**
     * Releases in exclusive mode.  Implemented by unblocking one or
     * more threads if {@link #tryRelease} returns true.
     * This method can be used to implement method {@link Lock#unlock}.
     *
     * @param arg the release argument.  This value is conveyed to
     *        {@link #tryRelease} but is otherwise uninterpreted and
     *        can represent anything you like.
     * @return the value returned from {@link #tryRelease}
     */
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            //头节点的waitStatus == -1 表示SIGNAL态,即普通态,普通态在释放锁时会唤醒后续非Cenceled态和initial态的节点(这两种节点都废弃的节点)
            if (h != null && h.waitStatus != 0)
                // 唤醒头节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    
     /**
     * Wakes up node's successor, if one exists.
     *
     * @param node the node
     */
    private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            //  设置为废弃节点
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        Node s = node.next;
        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;
        }
        // 唤醒该节点
        if (s != null)
            LockSupport.unpark(s.thread);
    }
  • 被唤醒后的节点会tryAcquire()竞争状态锁,如果竞争成功,则会将头设置为下一个节点,并将上一个节点的next设置为空(自己称为头节点的接班人successor)。

  • 总结,线程在tryAcquire失败后会被加入同步队列,之后可能会被中断唤醒或者作为首节点的接班人被release时的unparkSuccessor唤醒(非头节点从逻辑上不大可能会被release唤醒)。如果调用了可中断的acquireInterruptibly的话会处理被中断唤醒的请求,否则会继续阻塞。

关于CLH最好的一篇文章

示例图解析
下面属于回顾环节,用简单的示例来说一遍,如果上面的有些东西没看懂,这里还有一次帮助你理解的机会。
首先,第一个线程调用 reentrantLock.lock(),翻到最前面可以发现,tryAcquire(1) 直接就返回 true 了,结束。只是设置了 state=1,连 head 都没有初始化,更谈不上什么阻塞队列了。要是线程 1 调用 unlock() 了,才有线程 2 来,那世界就太太太平了,完全没有交集嘛,那我还要 AQS 干嘛。
如果线程 1 没有调用 unlock() 之前,线程 2 调用了 lock(), 想想会发生什么?
线程 2 会初始化 head【new Node()】,同时线程 2 也会插入到阻塞队列并挂起 (注意看这里是一个 for 循环,而且设置 head 和 tail 的部分是不 return 的,只有入队成功才会跳出循环)

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;
            }
        }
    }
}

首先,是线程 2 初始化 head 节点,此时 headtail, waitStatus0
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SrNDsOoV-1579505051289)(https://i.loli.net/2019/04/26/5cc2ca010289b.png)]
然后线程 2 入队:
并发工具-入队2.png
同时我们也要看此时节点的 waitStatus,我们知道 head 节点是线程 2 初始化的,此时的 waitStatus 没有设置, java 默认会设置为 0,但是到 shouldParkAfterFailedAcquire 这个方法的时候,线程 2 会把前驱节点,也就是 head 的waitStatus设置为-1。
那线程 2 节点此时的 waitStatus 是多少呢,由于没有设置,所以是 0;
如果线程3此时再进来,直接插到线程2的后面就可以了,此时线程 3 的 waitStatus 是 0,到 shouldParkAfterFailedAcquire 方法的时候把前驱节点线程 2 的 waitStatus 设置为 -1。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UHU3N4Y0-1579505051291)(https://i.loli.net/2019/04/26/5cc2ca0109cd2.png)]
这里可以简单说下 waitStatus 中 SIGNAL(-1) 状态的意思,Doug Lea 注释的是:代表后继节点需要被唤醒。也就是说这个 waitStatus 其实代表的不是自己的状态,而是后继节点的状态,我们知道,每个 node 在入队的时候,都会把前驱节点的状态改为 SIGNAL,然后阻塞,等待被前驱唤醒。这里涉及的是两个问题:有线程取消了排队、唤醒操作。其实本质是一样的,读者也可以顺着 “waitStatus代表后继节点的状态” 这种思路去看一遍源码。
本文作者: h2pl本文链接: http://h2pl.github.io/2018/05/20/concurrent7/版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值