首页 > 编程语言 >【Android面试】2023最新面试专题六:Java并发编程(一)

【Android面试】2023最新面试专题六:Java并发编程(一)

时间:2023-10-10 19:32:56浏览次数:55  
标签:Node node Java 队列 面试 int 线程 2023 节点

1、假如只有一个cpu,单核,多线程还有用吗 ?

详细讲解

享学课堂移动互联网系统课程:架构师筑基必备技能《线程与进程的理论知识入门1》

这道题想考察什么?

是否了解并发相关的理论知识

考察的知识点

  1. cpu多线程的基本概念
  2. 操作系统的调度任务机制
  3. CPU密集型和IO密集型理论

考生应该如何回答

CPU的执行速度要远大于IO的过程,因此在大多数情况下增加一些复杂的CPU计算都比增加一次IO要快。单核CPU可以通过给每个线程分配CPU时间片(时间单元)来实现多线程机制。由于CPU频率很高,故时间单元非常短。所以单核也可以实现多线程机制。

从用户体现上说,单核多线程也能够减少用户响应时间,例如web页面,也是防止IO阻塞。处理器的数量和并不影响程序结构, 所以不管处理器的个数多少, 程序都可以通过使用多线程得到简化。

2、sychronied修饰普通方法和静态方法的区别?什么是可见性?(小米)

详细讲解

享学课堂移动互联网系统课程:架构师筑基必备技能《线程与进程的理论知识入门1》

这道题想考察什么?

是否了解Java并发编程的相关知识

考察的知识点

  1. sychronied的原理
  2. 并发的特性

考生应该如何回答

sychronied是Java中并发编程的重要关键字之一。在并发编程中synchronized一直是解决线程安全问题,它可以保证原子性,可见性,以及有序性。

  • 原子性:原子是构成物质的基本单位,所以原子的意思代表着—“不可分”。由不可分可知,具有原子性的操作也就是拒绝线程调度器中断。
  • 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到,称为可见性。
  • 有序性:程序按照代码的先后顺序执行。编译器为了优化性能,有时会改变程序中语句的顺序,但是不会影响最终的结果。有序性经典的例子就是利用DCL双重检查创建单例对象。

synchronized可以修饰方法,也能够使用synchronized(obj){}定义同步代码块。

  • 修饰方法:
  1. 实例方法,作用于当前实例加锁,进入方法前需要获取当前实例的锁;
  2. 静态方法,作用于当前类对象加锁,进入方法前需要获取当前类对象的锁;
  • 修饰代码块,指定加锁对象,对给定对象加锁,进入代码块前要获得给定对象的锁。

使用sychronied修饰普通方法和静态方法,其实也等价于synchronized(this){}synchronized(class){}


3、 Synchronized在JDK1.6之后做了哪些优化 (京东)

详细讲解

享学课堂移动互联网系统课程:架构师筑基必备技能《线程与进程的理论知识入门1》

这道题想考察什么?

对并发编程的掌握

考察的知识点

并发与synchronized原理

考生如何回答

synchronized是Java中非常重要的一个关键字,对于Android开发同学来说,考虑到多线程的情况,一般都直接使用到synchronized关键字对方法或者对象上锁。但是问题是为什么加上synchronized关键字就能实现锁,它的原理是怎么回事呢?

字节码

如果我们使用javap -v xxx.class 反编译这样一个class文件

public static void main(String[] args) {
        synchronized (InjectTest.class) {
            System.out.println("hello!");
        }
}

此时我们获得到结果为:

【Android面试】2023最新面试专题六:Java并发编程(一)_Java

可以看到javap的输出信息中存在:monitorenter与monitorexit指令。这就代表了同步代码块的入口与出口。

这里的monitor是:对象监视器。在JVM中,每个对象都会和一个对象监视器(monitor)相关联。monitorenter指令插入到同步代码块开始的位置、monitorexit指令插入到同步代码块结束位置,jvm需要保证每个monitorenter都有一个monitorexit对应。这两个指令,本质上都是对 对象监视器(monitor)进行获取,这个过程是排他的,也就是说同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。线程执行到monitorenter指令时,会尝试获取对象所对应的monitor所有权,也就是尝试获取对象的锁;而执行monitorexit,就是释放monitor的所有权。 如果其他线程已经占用了monitor,则当前线程进入阻塞状态。

当然这是jdk1.6之前的行为,而jdk1.6以后为了减少获得锁和释放锁带来的性能消耗,对synchronized锁进行了优化,包含偏向锁、轻量级锁、重量级锁;

关于锁类型与相关信息的信息都是存放在锁对象的对象头中 ,在了解偏向锁、轻量级锁、重量级锁之前,我们必须先认识一下什么是对象头!

Java对象头

对象在虚拟机内存中的布局分为三块区域:对象头、实例数据和对齐填充;Java对象头是实现synchronized的锁对象的基础,一般而言,synchronized使用的锁对象是存储在Java对象头里。

对象头由:存储对象自身的运行时数据的Mark Word 32位系统中4 + 指向类的指针 kClass pointer ,如果是数组对象还会有数组长度 Array Length。

其中mark word中就存储了锁状态:

【Android面试】2023最新面试专题六:Java并发编程(一)_多线程_02

在无锁状态下,mark word中的数据为:

【Android面试】2023最新面试专题六:Java并发编程(一)_Java_03

包含对象hashcode,gc年龄,是否偏向锁与锁标志信息。

偏向锁

而偏向锁下,数据则为:

【Android面试】2023最新面试专题六:Java并发编程(一)_面试必问_04

拥有锁的线程ID, epoch 大家可以先理解为校验位,同时 是否偏向锁标记由相对无锁状态下的0变为1。

首先之所以会引入偏向锁是因为:大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁,减少不必要的操作。

在程序进入同步代码块时,会访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,若为偏向锁状态,则查看偏向锁状态下线程ID是否指向当前线程。如果是则直接执行同步代码。但是mark word中记录的线程ID如果不是当前线程,则通过CAS比较与交换尝试修改对象头获得锁。CAS操作成功则可以直接执行同步代码,否则表示有其他线程竞争,此时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁 ,然后被阻塞的线程继续往下执行同步代码。

轻量级锁

【Android面试】2023最新面试专题六:Java并发编程(一)_多线程_05

轻量级锁状态下,代码进入同步块时,如果同步对象锁状态为无锁状态, 虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝 ,接着虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针 。这个操作如果成功则代表获取到了锁,但是如果失败,则会检查对象Mark Word是不是指向当前线程栈帧中的锁记录,如果是,则说明本身当前线程就拥有此对象的锁,就可以直接执行同步代码。否则说明锁对象被其他线程获取,当前线程是竞争者,那么当前线程会自旋等待锁,也就是不断重试,当重试一定次数后,总不能一直重试下去吧,太耗CPU了。所以这时候就要升级为重量级锁。

重量级锁

重量级锁就是通过对象监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。主要是,当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。

4.4 CAS无锁编程的原理(字节跳动)

详细讲解

享学课堂移动互联网系统课程:架构师筑基必备技能《并发基础与CAS基本原理》

这道题想考察什么?

并发相关问题,原子操作

考察的知识点

Java并发编程,乐观锁机制

考生如何回答

Jdk中java.util.concurrent.atomic包下的类都是采用CAS来实现的。

CAS原理分析

CAS(比较与交换,Compare and swap) 是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。实现非阻塞同步的方案称为“无锁编程算法”( Non-blocking algorithm)。

CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

1.在内存地址V当中,存储着值为10的变量。

【Android面试】2023最新面试专题六:Java并发编程(一)_多线程_06

2.此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11。

【Android面试】2023最新面试专题六:Java并发编程(一)_多线程_07

3.在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。

【Android面试】2023最新面试专题六:Java并发编程(一)_多线程_08

4.线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败。

【Android面试】2023最新面试专题六:Java并发编程(一)_Java_09

5.线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋

【Android面试】2023最新面试专题六:Java并发编程(一)_Android_10

6.这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。

【Android面试】2023最新面试专题六:Java并发编程(一)_面试必问_11

7.线程1进行SWAP,把地址V的值替换为B,也就是12。

【Android面试】2023最新面试专题六:Java并发编程(一)_并发编程_12

从思想上来说,Synchronized属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守。CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。

CAS的缺点

ABA 问题

由于 CAS 设计机制就是获取某两个时刻(初始预期值和当前内存值)变量值,并进行比较更新,所以说如果在获取初始预期值和当前内存值这段时间间隔内,变量值由 A 变为 B 再变为 A,那么对于 CAS 来说是不可感知的,但实际上变量已经发生了变化;解决办法是在每次获取时加版本号,并且每次更新对版本号 +1,这样当发生 ABA 问题时通过版本号可以得知变量被改动过。JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

循环时间长开销大

所谓循环时间长开销大问题就是当 CAS 判定变量被修改了以后则放弃本次修改,但往往为了保证数据正确性该计算会以循环的方式再次发起 CAS,如果多次 CAS 判定失败,则会产生大量的时间消耗和性能浪费;如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

只能保证一个共享变量的原子操作
  1. CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效;从 JDK 1.5开始提供了 AtomicReference 类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作
  2. Unsafe是CAS的核心类,Java无法直接访问底层操作系统,而是通过本地(native)方法来访问。不过尽管如此,JVM还是开了一个后门,JDK中有一个类Unsafe,它提供了硬件级别的原子操作。
  3. valueOffset表示的是变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的原值的。
  4. value是用volatile修饰的,保证了多线程之间看到的value值是同一份。

4.5 AQS原理 (小米 京东)

详细讲解

享学课堂移动互联网系统课程:架构师筑基必备技能《深入理解并发编程-AQS与JMM》

这道题想考察什么?

是否了解Java并发编程的相关知识?

考察的知识点

AQS的原理

考生应该如何回答

什么是AQS

AQS即AbstractQueuedSynchronizer,是一个用于构建锁和同步器的框架。它能降低构建锁和同步器的工作量,还可以避免处理多个位置上发生的竞争问题。在基于AQS构建的同步器中,只可能在一个时刻发生阻塞,从而降低上下文切换的开销,并提高吞吐量。

AQS核心思想是,如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中。

CLH

CLH指的是:三位创作者的名字简称:Craig、Landin and Hagersten (CLH)。是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程仅仅在本地变量上自旋,它不断轮询前驱的状态,假设发现前驱释放了锁就结束自旋。

AQS中的队列是CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。

【Android面试】2023最新面试专题六:Java并发编程(一)_Android_13

AQS支持独占锁(exclusive)和共享锁(share)两种模式。

  1. 独占锁:只能被一个线程获取到(Reentrantlock)。
  2. 共享锁:可以被多个线程同时获取(CountDownLatch,ReadWriteLock)。

无论是独占锁还是共享锁,本质上都是对AQS内部的一个变量state的获取。state是一个原子的int变量,用来表示锁状态、资源数等。

【Android面试】2023最新面试专题六:Java并发编程(一)_Android_14

同步队列的作用是:当线程获取资源失败之后,就进入同步队列的尾部保持自旋等待,不断判断自己是否是链表的头节点,如果是头节点,就不断参试获取资源,获取成功后则退出同步队列。

条件队列是为Lock实现的一个基础同步器,并且一个线程可能会有多个条件队列,只有在使用了Condition才会存在条件队列。

AQS中包含一个内部类:Node。该内部类是一个双向链表,保存前后节点,然后每个节点存储了当前的状态waitStatus、当前线程thread。同步队列和条件队列都是由一个个Node组成的。

static final class Node {
        static final Node EXCLUSIVE = null;

        //当前节点由于超时或中断被取消
        static final int CANCELLED =  1;
     
        //表示当前节点的前节点被阻塞
        static final int SIGNAL    = -1;
        
        //当前节点在等待condition
        static final int CONDITION = -2;
      
        //状态需要向后传播
        static final int PROPAGATE = -3;
        
        volatile int waitStatus;
        
        volatile Node prev;
        volatile Node next;
        volatile Thread thread;

        Node nextWaiter;

        final boolean isShared() {
            return nextWaiter == SHARED;
        }

        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node() {    // Used to establish initial head or SHARED marker
        }

        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

独占模式下获取资源:

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

acquire(int arg)首先调用tryAcquire(arg)尝试直接获取资源,如果获取成功,因为与运算的短路性质,就不再执行后面的判断,直接返回。tryAcquire(int arg)的具体实现由子类负责。如果没有直接获取到资源,就将当前线程加入等待队列的尾部,并标记为独占模式,使线程在等待队列中自旋等待获取资源,直到获取资源成功才返回。如果线程在等待的过程中被中断过,就返回true,否则返回false。

如果acquireQueued(addWaiter(Node.EXCLUSIVE), arg)执行过程中被中断过,那么if语句的条件就全部成立,就会执行selfInterrupt();方法。因为在等待队列中自旋状态的线程是不会响应中断的,它会把中断记录下来,如果在自旋时发生过中断,就返回true。然后就会执行selfInterrupt()方法,而这个方法就是简单的中断当前线程Thread.currentThread().interrupt();其作用就是补上在自旋时没有响应的中断。

可以看出在整个方法中,最重要的就是acquireQueued(addWaiter(Node.EXCLUSIVE), arg)。我们首先看Node addWaiter(Node mode)方法,顾名思义,这个方法的作用就是添加一个等待者,根据之前对AQS中数据结构的分析,可以知道,添加等待者就是将该节点加入等待队列。

private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        //尝试快速入队
        if (pred != null) { //队列已经初始化
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node; //快速入队成功后,就直接返回了
            }
        }
        //快速入队失败,也就是说队列都还没初始化,就使用enq
        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;//打断循环
                }
            }
        }
    }
final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();//拿到node的上一个节点
                //前置节点为head,说明可以尝试获取资源。排队成功后,尝试拿锁
                if (p == head && tryAcquire(arg)) {
                    setHead(node);//获取成功,更新head节点
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //尝试拿锁失败后,根据条件进行park
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    //获取资源失败后,检测并更新等待状态
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
            //如果前节点取消了,那就往前找到一个等待状态的接待你,并排在它的后面
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
    //阻塞当前线程,返回中断状态
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

公平锁的实现

在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。

protected final boolean tryAcquire(int acquires) {
     final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {//状态为0表示可以加锁
                if (!hasQueuedPredecessors() && //hasQueuedPredecessors表示之前的线程是否有在排队的,这里加了!表示没有排队
                    compareAndSetState(0, acquires)) { //那么就去尝试cas state
                    setExclusiveOwnerThread(current); //如果cas成功设置排他线程为当前线程,表示成功得到锁
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {//如果当前的排他线程是当前线程,表示是重入
                int nextc = c + acquires; //重入计数器增加
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);//因为已经获得锁了,所以不用cas去设,直接设值就行
                return true;
            }
            return false;
}

非公平锁的实现

直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

final boolean nonfairTryAcquire(int acquires) {
    // 获取当前线程
    final Thread current = Thread.currentThread();
    // 获取当前state的值
    int c = getState(); 
    if (c == 0) {
      // 看看设置值是否能成功
            if (compareAndSetState(0, acquires)) {
           // 则将当前线程设置为独占线程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
 // 返回由setExclusiveOwnerThread设置的最后一个线程;如果从不设置,则返回null 
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        // 设置state的值
        setState(nextc);
        return true;
    }
    return false;
}

释放锁实现

释放锁代码分析:尝试释放此锁。如果当前线程是此锁的持有者,则保留计数将减少。 如果保持计数现在为零,则释放锁定。 如果当前线程不是此锁的持有者,则抛出IllegalMonitorStateException。

public void unlock() {
    sync.release(1);
}

sync.release(1) 调用的是AbstractQueuedSynchronizer中的release方法

## AbstractQueuedSynchronizer
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

分析tryRelease(arg)方法,tryRelease(arg)该方法调用的是ReentrantLock中

protected final boolean tryRelease(int releases) {
// 获取当前锁持有的线程数量和需要释放的值进行相减
    int c = getState() - releases; 
    // 如果当前线程不是锁占有的线程抛出异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 如果此时c = 0就意味着state = 0,当前锁没有被任意线程占有
    // 将当前所的占有线程设置为空
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    // 设置state的值为 0
    setState(c);
    return free;
}

如果头节点不为空,并且waitStatus != 0,唤醒后续节点如果存在的话。这里的判断条件为什么是h != null && h.waitStatus != 0?因为h == null的话,Head还没初始化。初始情况下,head == null,第一个节点入队,Head会被初始化一个虚拟节点。所以说,这里如果还没来得及入队,就会出现head == null 的情况。

  1. h != null && waitStatus == 0 表明后继节点对应的线程仍在运行中,不需要唤醒
  2. h != null && waitStatus < 0 表明后继节点可能被阻塞了,需要唤醒
private void unparkSuccessor(Node node) {
// 获取头结点waitStatus
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
// 获取当前节点的下一个节点
    Node s = node.next;
//如果下个节点是null或者下个节点被cancelled,就找到队列最开始的非cancelled的节点
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 就从尾部节点开始找往前遍历,找到队列中第一个waitStatus<0的节点。
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
  // 如果当前节点的下个节点不为空,而且状态<=0,就把当前节点唤醒
    if (s != null)
        LockSupport.unpark(s.thread);
}

为什么要从后往前找第一个非Cancelled的节点?

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

从此处可以看到,节点入队并不是原子操作,也就是说,node.prev = pred, compareAndSetTail(pred, node) 这两个地方可以看作Tail入队的原子操作,但是此时pred.next = node;还没执行,如果这个时候执行了unparkSuccessor方法,就没办法从前往后找了,所以需要从后往前找。还有一点原因,在产生CANCELLED状态节点的时候,先断开的是Next指针,Prev指针并未断开,因此也是必须要从后往前遍历才能够遍历完全部的Node。 所以,如果是从前往后找,由于极端情况下入队的非原子操作和CANCELLED节点产生过程中断开Next指针的操作,可能会导致无法遍历所有的节点。所以,唤醒对应的线程后,对应的线程就会继续往下执行。

【Android面试】2023最新面试专题六:Java并发编程(一)_并发编程_15

有需要的小伙伴,可以点击下方课程链接详细了解!!!

https://edu.51cto.com/course/32703.html

标签:Node,node,Java,队列,面试,int,线程,2023,节点
From: https://blog.51cto.com/u_16163453/7798786

相关文章

  • 2023暑假集训总结
    Part120230807~20230816这段时间主要是lyn学长给我们讲课。和上次寒假集训不同,这次lyn学长准备的东西难度非常之大,大部分都是NOI级的数据结构和算法,比如一些高难数论(exCRT、各种定理等等)、一些奇妙的DP优化、一些奇妙的数学期望、网络流、进阶的线段树,很难,感觉自己只......
  • Java观察者模式-SpringBoot实现观察者模式
    观察者模式一、Java观察者模式Java观察者模式是一种设计模式,用于实现对象之间的一对多依赖关系。在观察者模式中,当一个对象的状态发生变化时,它的所有依赖对象(观察者)都会自动收到通知并进行相应的更新。观察者模式由以下几个核心组件组成:主题(Subject):也称为被观察者或可观察对......
  • Java 集合
    目录Java集合List,Set,Queue,Map的区别集合框架底层数据结构CollectionListSetQueueMap如何选用集合?ListArrayList和Array(数组)的区别转换ArrayList转换为数组数组转换为ArrayListSetComparable和Comparator的区别Comparable接口Comparator接口总结HashSetLinkedHas......
  • LY1376 [ 20231008 NOIP 模拟赛 T0 ] 递增路径
    题意\(A\),\(B\)两人轮流在一张图上移动一个点。要求这次移动的边权必须大于上次的。\(A\)希望游戏进行的轮数多,\(B\)希望游戏进行的轮数少。对于每个\(s=1,2,...,n\)作为起点,若双方都采用最优策略,游戏会进行多少轮。Sol考虑将所有边按照从大到小的顺序排序。每......
  • 20231010
    20231010NOIP#17总结时间安排7:50~8:30看题,\(A,B\)一眼切,\(D\)会\(30\)分,别的不会。8:30~8:50写\(A,B\)的正解。8:50~9:20写\(D\)的\(30\)分,再拼一个特殊性质包。9:20~10:20写\(E\)的第一档暴力,奈何题目错了我还读错了,调了这么长时间等于没调。10:20~11:45......
  • 2023.10.1
    2023.10.1T1:​​​​思路展环为链用前缀和记录异或和\(O(1)\)查询T2​​​​思路按题意模拟即可T3​​思路\(O(\sqrt{n})\)枚举b1的因子,并判断T4​​思路dp30pts​dp[i]表示到达i时最少的踩到的石子数100pts因为\(m\le100\)且\(l\le10^9\)......
  • Java 中 extends 与implements 的区别 ?
    一、介绍extends与implements的概念1、类与类之间的继承使用extends:子类extends父类的属性和方法,并且进行扩展或者重写。//父类classAnimal{publicvoideat(){System.out.println("Animaliseating");}publicvoidnoeat(){......
  • Java设计模式之责任链模式
    1.1.概述在现实生活中,常常会出现这样的事例:一个请求有多个对象可以处理,但每个对象的处理条件或权限不同。例如,公司员工请假,可批假的领导有部门负责人、副总经理、总经理等,但每个领导能批准的天数不同,员工必须根据自己要请假的天数去找不同的领导签名,也就是说员工必须记住每个领......
  • 「魔怔」鲜花 2023/10/9
    碎碎念。嘿嘿,我的嘿嘿。今天怎么这么摆。什么?你以为这篇文章是鲜花?这是压缩诈骗。建议大家都学点新东西,不然就会像我一样在NOIP模拟赛中被T2放PowerfulNumber筛的出题人创死。我尝试把开头写长一点让预览看不到下面的内容,这样你就要点进来看了,嘿嘿,我是不是很机智。让......
  • Java创建PKCS12证书Http请求
    //证书地址publicstaticfinalStringPATH="XX.pfx";//密码publicstaticfinalStringPASSWORD="aaa";publicstaticCloseableHttpClientinitSSLConfig()throwsException{//证书类型KeyStorekeyStore=KeyStore.getInstanc......