Java面试题(JVM、并发)

02 对于volitale的理解

volitale是JVM提供的轻量级同步机制:

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排序

CAS底层原理

cas的底层调用了很多Unsafe类native方法

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;
    
    /**
     * Creates a new AtomicInteger with the given initial value.
     *
     * @param initialValue the initial value
     */
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }

    /**
     * Creates a new AtomicInteger with initial value {@code 0}.
     */
    public AtomicInteger() {
    }
    
    ...
            
    /**
     * Atomically increments by one the current value.
     *
     * @return the previous value
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    
    ...
}
    

Unsafe是cas的核心类,由于java方法无法直接访问底层系统,需要通过本地方法来访问,Unsafe相当于一个后门,该类可以直接操作特定内存的数据,它在sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存。
Unsafe类中的所有方法都是native方法,都直接调用操作系统底层资源。
变量valueoffset表示该变量值在内存中的偏移量,因为Unsafe就是根据内存偏移来获取数据,变量value是由volitale来修饰的,保证了多线程之间的内存可见性问题。

CAS 存在的问题

  1. 循环带来的cpu占用开销
  2. 只能保证一个变量的原子性(可以用原子引用)
  3. ABA问题(增加版本号)

阻塞队列

阻塞队列的作用
不得不阻塞的情况,如何来管理阻塞队列中的各个线程

为什么需要BlockingQueue?
好处是我们不需要关心什么时候需要阻塞线程,什么时候去唤醒线程,这一切都由阻塞队列来帮我们来处理。

BlockingQueue接口
在这里插入图片描述

  1. ArrayBlockingQueue:由数组结构组成的有界阻塞队列
  2. LinkedBlockingQueue:由链表结构组成的有界阻塞队列,长度是Integer的最大值
  3. PriorityBlockingQueue:支持优先级排序的无界阻塞队列
  4. DelayQueue:使用优先级队列实现的延迟无界阻塞队列
  5. SynchronousQueue:不存储元素的阻塞队列,单个元素的队列
  6. LinkedTransferQueue:由链表结构组成的无界阻塞队列
  7. LinkedBlockingDeque:链表结构组成的双向阻塞队列
    在这里插入图片描述

三组api:
add remove 抛出异常:非法状态异常,没有这个元素异常
offer poll 插入返回bool值 取出返回对象或者null
put take 阻塞

阻塞队列的用处:生产者消费者模式、线程池、消息中间件

synchronized 和 lock

synchronized、wait、notify
lock await signal

public class TestPack {
    public static void main(String[] args) {

        ShareData shareData = new ShareData();
        new Thread(() ->{
            for (int i = 0; i < 5; i++) {
                try {
                    shareData.incrementData();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(() ->{
            for (int i = 0; i < 5; i++) {
                try {
                    shareData.consumerData();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}
class ShareData{
    ReentrantLock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    private int num = 0;

    void incrementData() throws InterruptedException {
        lock.lock();

        try {
            while (num != 0){
                //等待不能生产
                condition.await();
            }
            System.out.println("num  = " + num);
            num ++;
            condition.signal();

        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    void consumerData() throws InterruptedException {
        lock.lock();
        try {
            while (num != 1){
                //等待不能生产
                condition.await();
            }
            System.out.println("num  = " + num);
            num --;
            condition.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

虚假唤醒

为了防止虚假唤醒,多线程条件下wait等要放入到loop循环中
在这里插入图片描述

Lock和synchronized区别

  1. synchronized是关键字,JVM层面依靠monitorenter和monitorexit来实现,wait和notify也依赖于monitor对象,所以只有在同步方法和同步代码块之内才能调用这两个方法,否则抛IllegalMonitorStateException, Lock是JUC包下的一个接口,是api层面的锁
    2.synchronized不需要用户去手动释放锁,锁是自动释放的,Lock需要手动释放,否则可能出现死锁
    3.synchronized是不可以被中断的,除非抛出异常或者正常运行完成;Lock可以中断:设置超时时间tryLock(long time)
    lockIntertuptibly放在代码块中,调用interrupt方法可以中断
    4.加锁是否公平:synchronized非公平锁
    Lock既可以是公平也可以是非公平锁,默认是非公平(效率较高)
  2. 绑定多个condition条件
    synchronized不可以 Lock可以实现分组唤醒需要唤醒的线程,精确唤醒,不同的condition唤醒不同的线程
    例如 A -> B -> C ->D ->A轮流唤醒
    例题:A打印5遍 AA,B打印10遍BB,C打印15遍CC 循环十次
//资源类
class ShareData{
    private int num  = 1; // 1a  2b 3c
    private Lock lock = new ReentrantLock();
    private Condition c1 = lock.newCondition();
    private Condition c2 = lock.newCondition();
    private Condition c3 = lock.newCondition();

    public void printA(){
        lock.lock();
        try {
            while (num != 1){
                c1.await();
            }
            for (int i = 0; i < 5; i++) {
                System.out.println("AA");
            }
            num = 2;
            c2.signal();//a唤醒b
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void printB(){
        lock.lock();
        try {
            while (num != 2){
                c2.await();
            }
            for (int i = 0; i < 9; i++) {
                System.out.println("BB");
            }
            num = 3;
            c3.signal();//a唤醒c
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    public void printC(){
        lock.lock();
        try {
            while (num != 3){
                c3.await();
            }
            for (int i = 0; i < 14; i++) {
                System.out.println("CC");
            }
            num = 1;
            c1.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

volitale、CAS、atomicInteger、blockingQueue,线程交互,原子引用
不用锁来实现生产者和消费者

class MySource{
    private volatile boolean flag = true;//默认开启生产加上消费
    private AtomicInteger atomicInteger = new AtomicInteger();

    BlockingQueue<String> blockingQueue = null;
    public MySource(BlockingQueue<String> blockingQueue) {//传入接口
        this.blockingQueue = blockingQueue;
        System.out.println(blockingQueue.getClass().getName());
    }
    //生产方法
    public void myProduct() throws Exception{
        String data = null;
        boolean retValue = false;
        while (flag){
            data = atomicInteger.incrementAndGet() + "";
            retValue = blockingQueue.offer(data,2L, TimeUnit.SECONDS);
            if(retValue){
                System.out.println(Thread.currentThread().getName() + "插入数据" + data + "成功");
            }else {
                System.out.println(Thread.currentThread().getName() + "插入数据" + data + "失败");
            }
            TimeUnit.SECONDS.sleep(1);
        }
        System.out.println(Thread.currentThread().getName() + "生产结束flag = false");

    }
    //消费方法
    public void myConsumer() throws Exception{
        String result = null;
        while (flag){
            result = blockingQueue.poll(2L,TimeUnit.SECONDS);
            if (result == null || result.equalsIgnoreCase("")){
                flag = false;
                System.out.println("超过两秒没有取到数据");
                return;
            }
            System.out.println(Thread.currentThread().getName() + "消费数据" + result + "成功");
        }
    }
    public void stop() throws Exception{
        this.flag = false;
    }
}

创建线程的方法

  1. 继承Thread类
  2. 实现Runnable接口,实现run方法
  3. 实现Callable接口 实现call方法(可以有返回值,抛出异常)
  4. 线程池
    在这里插入图片描述
    futuretask过早去取结果会阻塞主线程,无法达到分支合并的效果
class MyThread implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {
        return 1024;
    }
}

	public static void main(String[] args) throws Exception {

        MyThread myThread = new MyThread();
        FutureTask<Integer> futureTask = new FutureTask<>(myThread);
        Thread thread = new Thread(futureTask);
        thread.start();
        while (!futureTask.isDone()){

        }
        System.out.println(futureTask.get());
    }

多个线程去抢夺一个futureTask时,只会调用一次,结果复用

线程池

线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放到队列中,然后在线程创建之后启动这些任务,如果线程数量超过了最大线程数就在队列中排队等候,其他线程执行完毕将任务取出。
主要特点:线程复用,控制最大并发数,管理线程

  1. 降低资源的消耗,复用线程避免创建和销毁的消耗
  2. 提高响应的速度,任务不需要等待线程的创建
  3. 提高程序的可管理性,线程是稀缺资源,如果无限制创建会消耗系统资源,降低系统稳定性,线程池可以对资源进行统一的分配和管理、调优和监控。

在这里插入图片描述
线程池底层就是ThreadPoolExecutor

  1. Executors.newFixedThreadPool 固定数量线程
  2. Executors.newSingleThreadExecutor 单个线程
  3. Executros.newCachedThreadPool 多个缓冲线程

execute只能传入runnable的实现类

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

	public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
    }

	public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

在这里插入图片描述

ThreadPoolExecutor

线程池参数介绍

	public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

七个参数的构造方法

  1. corePoolSize:常驻核心线程数
  2. maximumPoolSize:线程池能够容纳同时执行的最大线程数,大于等于1
  3. keepAliveTime:多余的空闲线程的存活时间,线程数量超过核心线程数时,当空闲时间超过这个值,多余线程被销毁到只剩下corePoolSize个线程为止
  4. unit :keepAliveTime的单位
  5. workQueue:任务队列,被提交但尚未被执行的任务
  6. threadFactory表示生成线程池中工作线程的线程工厂,创建线程一般用默认的工厂即可
  7. handler:拒绝策略,任务队列已满,并且工作线程数大于最大线程数时会调用拒绝策略。

在这里插入图片描述
四种拒绝策略:

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务
ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务

在这里插入图片描述
在这里插入图片描述
阿里巴巴开发手册:
在这里插入图片描述
在这里插入图片描述

        new ThreadPoolExecutor(2,
                5,
                1,TimeUnit.SECONDS,
                new LinkedBlockingDeque<Runnable>(5),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

如何配置合理线程池数量
首先通过

System.out.println(Runtime.getRuntime().availableProcessors());
获取服务器核心数量

cpu密集型在这里插入图片描述
IO密集型
由于cpu不是一直在执行命令,因此应该配置多个线程,CPU核心数* 2
在这里插入图片描述

死锁编码以及定位分析

public static void main(String[] args) throws Exception {
        Object lock1 = new Object();
        Object lock2 = new Object();
        new Thread(()->{
            synchronized (lock1){
                try {
                    TimeUnit.SECONDS.sleep(1000);
                    synchronized (lock2){
                        System.out.println("线程1任务");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        new Thread(()->{
            synchronized (lock2){
                try {
                    TimeUnit.SECONDS.sleep(1000);
                    synchronized (lock1){
                        System.out.println("线程2任务");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

通过 jsp -l命令得到正在运行的java相关的线程id
然后通过 jstak 线程号 找到堆栈相信息
在这里插入图片描述

LockSupport类

对于传统的synchronized和Lock在线程同步时,等待和唤醒的操作一定要在同步代码块内部,即获取锁之后执行,否则会抛出非法的监视器异常。
并且,如果先唤醒再阻塞则会使得唤醒失败。

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语,它使用了一种名为许可证permit的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可证,许可证只有1和零,默认是0,可以把许可证看做是累加上限为1的信号量。
主要方法:
在这里插入图片描述

阻塞

park()/park(Object blocker)
park方法作用:阻塞当前线程或者阻塞传入的线程。
permit默认是0,所以刚开始调用park方法,线程就会阻塞,直到别的线程将许可证设置为1,park方法被唤醒,然后将permit再次设置为0并且返回。

// Disables the current thread for thread scheduling purposes unless the permit is available.
public static void park() {
    UNSAFE.park(false, 0L);
}

唤醒

unpark(Thread thread)
unpark方法的作用是唤醒处于阻塞状态的指定线程。
调用unpark方法后会将thread线程的permint设置为1,多次调用unpark还是会设置为1,自动唤醒thread线程

// Makes available the permit for the given thread
public static void unpark(Thread thread) {
    if (thread != null)
        UNSAFE.unpark(thread);
}

可以精确唤醒指定的线程,并且先唤醒再阻塞也可以起作用

private static void lockSupportParkUnpark() {
    Thread a = new Thread(() -> {
        try {
            TimeUnit.SECONDS.sleep(3L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().`在这里插入代码片`getName() + "\t" + "------come in" + System.currentTimeMillis());
        LockSupport.park();
        System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒" + System.currentTimeMillis());
    }, "A");
    a.start();

    new Thread(() -> {
        LockSupport.unpark(a);
        System.out.println(Thread.currentThread().getName() + "\t" + "------通知");
    }, "B").start();
}

异常情况:没有考虑到permit上限为1

private static void lockSupportParkUnpark() {
    Thread a = new Thread(() -> {
        try {
            TimeUnit.SECONDS.sleep(3L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "------come in" + System.currentTimeMillis());
        LockSupport.park();
        LockSupport.park();
        System.out.println(Thread.currentThread().getName() + "\t" + "------被唤醒" + System.currentTimeMillis());
    }, "A");
    a.start();

    new Thread(() -> {
        LockSupport.unpark(a);
        LockSupport.unpark(a);
        System.out.println(Thread.currentThread().getName() + "\t" + "------通知");
    }, "B").start();
}

运行时会阻塞,由于permit上限值为1,执行两次park将会导致线程阻塞。

LockSupport不用持有锁块,不用加锁,程序性能好,无须注意唤醒和阻塞的先后顺序,不容易导致卡死

AQS 抽象队列同步器

AQS是用来构建锁或者其它的同步器组件的重量级基础框架以及整个JUC的基石,通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量state表示持有锁的状态。
CLH队列是一个双向链表,AQS中的队列是CLH边提的虚拟双向队列FIFO
在这里插入图片描述

AQS是JUC的基石,以下是和AQS有关的并发编程类

  1. CountDownLatch
  2. ReentrantLock
  3. ReentrantReadWriteLock
  4. CyclicBarrier
  5. Seamaphore

ReentrantLock
在这里插入图片描述
CountDownLatch
在这里插入图片描述
ReentrantReadWriteLock
在这里插入图片描述
Semaphore
在这里插入图片描述

进一步理解锁和同步器的关系
锁面向的是锁的使用者,定义了程序员和锁交互的使用层API隐藏了实现细节
同步器,面向锁的管理者,提出同意规范并且简化了的实现,屏蔽了同步状态管理,阻塞线程排队和通知,唤醒机制,简化Java中各种锁的实现。

AQS能干嘛

加锁会导致线程阻塞,有阻塞就需要进行排队,而排队的线程需要以某种方式的队列来进行管理。
抢到资源的线程继续办理业务,而抢占不到资源的线程必然涉及一种排队等待机制,抢占资源失败的线程继续等待,仍然保留获取锁的可能性并且获取锁的流程仍然在继续。
既然有排队机制,那么一定会有某种队列形成,这样的队列是什么数据结构呢?如果共享资源被占用,需要一定的阻塞等待唤醒机制来保证锁的分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到等待队列中去,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的节点NOde,通过CAS、自旋以及LockSupport.park()方式维护state变量的状态,使得并发达到同步的效果。
在这里插入图片描述
在这里插入图片描述

  1. AQS使用一个volitale修饰的int类型变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作,将每条要抢占资源的线程封装成为一个个Node节点来实现锁 的分配。通过CAS操作来实现对State值的修改。
  2. Node节点是啥?包含线程属性的一个内部类
  3. 可以将NOde和Thread类比候客区的椅子和顾客

在这里插入图片描述
在这里插入图片描述

AQS同步状态的State成员变量,类似于银行办理业务的受理窗口状态:0就是没人,自由状态可以办理,大于等于1,有人占用窗口,排队

内部类Node,Node的等待状态waitState成员变量,勒斯与等候区其他顾客的等待状态,队列中每个排队的个体就是一个Node

static final class Node{
    //共享
    static final Node SHARED = new 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;
    
    // 初始为e,状态是上面的几种
    volatile int waitStatus;
    
    // 前置节点
    volatile Node prev;
    
    // 后继节点
    volatile Node next;

    // ...
    

在这里插入图片描述

AQS的底层是如何排队的

通过调用LockSupport.park来实现排队

reentrantLock

ReentrantLock实现了Lock接口,在它的内部聚合了一个AQS的实现类Sync
在这里插入图片描述
在ReentrantLock内定义了静态内部类,分别为NoFairSync非公平锁和FairSync公平锁
在这里插入图片描述
ReentrantLock默认创建的是非公平锁。
看一下lock方法执行流程:
在这里插入图片描述
在ReentrantLock中,NoFairSync和FairSync中的tryAcquire方法的区别,可以明显看出公平锁公平锁和非公平锁lock方法唯一区别在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors
在这里插入图片描述
hasQueuedPredecessors方法时公平锁加锁时等待队列中是否存在有效节点的方法
在这里插入图片描述

对比公平锁和非公平锁的tryAcqure()方法的实现代码, 其实差别就在于非公平锁获取锁时比公平锁中少了一个判断!hasQueuedPredecessors(),hasQueuedPredecessors()中判断了是否需要排队,导致公平锁和非公平锁的差异如下:
1公平锁:公平锁讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中
2 非公平锁:不管是否有等待队列,如果可以获取锁,则立刻占有锁的对象。也就是说队列的第一个排队线层在unpark,之后还是需要竞争锁。
在这里插入图片描述
而在acquire方法中最终都会调用tryAcquire方法
在这里插入图片描述
在 NonfairSync 和 FairSync 中均重写了其父类 AbstractQueuedSynchronizer 中的 tryAcquire() 方法
在这里插入图片描述
lock首先通过cas操作来查看state的状态是否设置为被占用,如果没有被占用,则更新state,然后将锁的持有者设置为当前线程。
如果发现state已经被设置为1,表示锁已经被其他线层持有,则进入acquire方法,而acquire方法又会调用tryAcquire方法,对于公平锁和非公平锁又有不同的tryAcquire方法。
以非公平锁的tryAcquire方法为例,里面调用了nonfairTryAcquire方法,传入的参数是1
在这里插入图片描述
nonfairTryAcquire方法的正常执行流程:
在nonfairTryAcquire方法中,大部分情况下的执行流程如下:线程b执行getState方法,获取state变量的值为1,表示lock锁正在被占用,state==0不成立,然后判断当前线程是否是持有锁的线程,发现是线程A,则返回false,表示该线程并没有抢到锁
在这里插入图片描述
nonfairTryAcquire(acquires) 比较特殊的执行流程:
第一种情况是,走到 int c = getState() 语句时,此时线程 A 恰好执行完成,让出了 lock 锁,那么 state 变量的值为 0,当然发生这种情况的概率很小,那么线程 B 执行 CAS 操作成功后,将占用 lock 锁的线程修改为自己,然后返回 true,表示抢占锁成功。其实这里还有一种情况,需要留到 unlock() 方法才能说清楚
第二种情况为可重入锁的表现,假设 A 线程又再次抢占 lock 锁(当然示例代码里面并没有体现出来),这时 current == getExclusiveOwnerThread() 条件成立,将 state 变量的值加上 acquire,这种情况下也应该 return true,表示线程 A 正在占用 lock 锁。因此,state 变量的值是可以大于 1 的
在这里插入图片描述

往下继续执行addWaiter(Node.EXCLUSIVE)方法

在 tryAcquire() 方法返回 false 之后,进行 ! 操作后为 true,那么会继续执行 addWaiter() 方法

addWaiter方法做了什么?
之前讲过Node节点用于封装用户线程,这里将正在执行的线程通过NOde封装起来,判断 tail 尾指针是否为空,双端队列此时还没有元素呢~肯定为空呀,那么执行 enq(node) 方法,将封装了线程 B 的 Node 节点入队
在这里插入图片描述
enq(node) 方法:构建双端同步队列
在双端同步队列总,第一个节点是虚节点,也叫哨兵节点,其实并不存储任何信息,只是用来站位,真正封装线程的节点从第二个节点开始。
在这里插入图片描述
第一次执行 for 循环:现在解释起来就不费劲了,当线程 B 进来时,双端同步队列为空,此时肯定要先构建一个哨兵节点。此时 tail == null,因此进入 if(t == null) { 的分支,头指针指向哨兵节点,此时队列中只有一个节点,尾节点即是头结点,因此尾指针也指向该哨兵节点

第二次执行for循环,将装着线程B的节点放入到双端队列中,此时tail指向了哨兵节点,并不是null,因此进入else分支,以尾插法的方式,现将nodeB的prev指向之前的tail,再

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值