Java多线程面试常见问题

线程状态

线程状态转换
来源于https://www.uml-diagrams.org/java-thread-uml-state-machine-diagram-example.html

Thread vs Runnable
在这里插入图片描述
操作系统调用的是Thread的run方法,子类必须重写这个方法,否则会什么都不做,或者传入Runnable,赋值给target,调用target的run方法。

getStackTrace可以用来异常情况下跟踪调用栈

sleep vs wait

sleep是Thread的方法
wait是Object的方法

sleep不会释放对象锁
wait会释放对象锁

因为释放对象锁的原因
wait需要在同步代码块中,否则会抛出IllegalMonitorException

Runnable vs Callable

通过Callable创建FutureTask()对象,进而创建线程
在这里插入图片描述
Callable可以有返回值,可以抛出异常

yield

只会让CPU给相同优先级的线程

interrupt vs Thread.interrupted vs thread.isInterrupted

interrupt 不会中断IO阻塞和synchronized阻塞
Thread.interrupted会清空线程的interrupt标志位.
thread.isInterrupted不会

线程池

Executor
任务提交

  1. 核心线程数
  2. 队列是否满来
  3. 最大线程数
  4. 拒绝策略
shutdown vs shutdownNow

shutdown将线程池状态至为SHUTDOWN
shutdownNow将线程池状态至为STOP

shutdown任务会继续执行,线程池只会将闲置work线程杀死,会通过tryLock方法判断worker是否空闲
shutdownNow对work线程调用interrupt,队列内任务直接返回
在这里插入图片描述

线程池源码分析

线程池状态定义
在这里插入图片描述
runWorker
在这里插入图片描述
如果task不为空或者getTask不为空则不会线程不会执行结束
在这里插入图片描述
getTask方法,实现了keepAliveTime
如何开启线程

小于核心线程数,new Worker() woker继承自AQS,用来shutdown时检测是否线程在工作

如何判断线程超时

大于核心线程数 & 允许回收超过核心线程数的线程
用poll(time, timeUnit)返回空的方式来回收线程,返回空会把timedOut至为true,在下一次自旋的时候调用compareAndDecrementWorkerCount减少核心线程数

ctl

高三位为线程状态
Running 111
SHUTDOWN 000
STOP 001
TIDYING 010
TERMINATE 011

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

线程池如何结束

  1. 调用shutdown至标志位为SHUTDOWN,然后awaitTermination
  2. 线程执行完任务会调用processWorkerExit,该方法会调用tryTerminate。检查线程池是不是空,是空则termination.signAll()
    awaitTermination收到sign,继续运行

在这里插入图片描述

BlockingQueue

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

分组
存:offer,put,add,
取:peek,poll,take,remove,element

三种方式:
抛异常
add,remove,element
返回成功失败
offer, poll, peek
阻塞
take,put
在这里插入图片描述

实现类
LinkedBlockingQueue
ArrayBlockingQueue
PriorityBlockingQueue

Synchronized实现

synchronized方法常量池中多了ACC_SYNCHRONIZED标示符
代码块时通过指令monitorenter和monitorexit来完成
在这里插入图片描述
ContentionList
ContentionList并不是一个真正的Queue,而只是一个虚拟队列,原因在于ContentionList是由Node及其next指针逻辑构成,并不存在一个Queue的数据结构。ContentionList是一个后进先出(LIFO)的队列,每次新加入Node时都会在队头进行,通过CAS改变第一个节点的的指针为新增节点,同时设置新增节点的next指向后续节点,而取得操作则发生在队尾。显然,该结构其实是个Lock-Free的队列。

因为只有Owner线程才能从队尾取元素,也即线程出列操作无争用,当然也就避免了CAS的ABA问题。

EntryList
EntryList与ContentionList逻辑上同属等待队列,ContentionList会被线程并发访问,为了降低对ContentionList队尾的争用,而建立EntryList。Owner线程在unlock时会从ContentionList中迁移线程到EntryList,并会指定EntryList中的某个线程(一般为Head)为Ready(OnDeck)线程。Owner线程并不是把锁传递给OnDeck线程,只是把竞争锁的权利交给OnDeck,OnDeck线程需要重新竞争锁。这样做虽然牺牲了一定的公平性,但极大的提高了整体吞吐量,在Hotspot中把OnDeck的选择行为称之为“竞争切换”。

OnDeck线程获得锁后即变为owner线程,无法获得锁则会依然留在EntryList中,考虑到公平性,在EntryList中的位置不发生变化(依然在队头)。如果Owner线程被wait方法阻塞,则转移到WaitSet队列;如果在某个时刻被notify/notifyAll唤醒,则再次转移到EntryList。

AQS

独占非公平锁
lock

在这里插入图片描述
此处体现了非公平,来了先试试能不能加锁。acquire方法为AQS实现的方法

acquire

在这里插入图片描述

tryAcquire由子类实现

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
getState返回AQS的state,为0表示没有线程获得锁,此处再一次尝试获取锁,也体现了非公平。如果失败,看看是不是自己,此处体现了可重入
nonfairTryAcquire,方法体现了非公平和可重入
在这里插入图片描述

addWaiter(Node.EXCLUSIVE)

在这里插入图片描述
以当前线程新建一个节点,模式为独占(排他)
先尝试一下把新节点加到队尾,compareAndSetTail方法比较有趣,此处各种offset均为变量相对于对象头的偏移量,unsafe采用提换此变量的这个偏移量的方式实现cas(所谓if else for码农确实没见过这个新鲜玩意)
在这里插入图片描述
在这里插入图片描述
如果设置失败,采用enq进行自旋设置(volatile + cas实现原子性)
在这里插入图片描述
此方法进行了初始化头节点设置为空的节点,addWaiter方法中如果tail==null则不处理,在end中处理。如果不为空则cas进行自旋添加到尾部

acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

在这里插入图片描述
该方法首先判断自己是不是第二个节点,如果是则尝试拿锁,此处是为了提升效率如果拿锁失败则,shouldParkAfterFailedAcquire && parkAndCheckInterrupt

shouldParkAfterFailedAcquire

在这里插入图片描述
此方法用来检测,之前的节点是不是设置成了SIGNAL,还有一个作用是从后向前删除取消状态的前置节点(改了node的前置指针,改了非取消态节点的后置指针)如图,中间节点相当于无论是从前向后,还是从后向前都无法访问。通过上层的自旋,实现最后一个else设置到第一个if的成功。

        +-----------+    next  	+-----------+       	+-----------+
head    |			| --------> |			| -------->	|			|
		|			| <--------      		| <-------- |     		|  tail
        +-----------+    prev 	+-----------+       	+-----------+


        +-----------+    next  	+-----------+       	+-----------+
head    |			| -------------------------------->	|			|
		|			| <-------------------------------- |     		|  tail
        +-----------+    prev 	+-----------+       	+-----------+
parkAndCheckInterrupt

在这里插入图片描述
该方法先park当前线程,如果被唤醒则,返回线程是否被interrupt & 重置interrupt位。顺便说下LockSupport.park方法会使线程进入等待状态,非Block状态。由LockSupport.park方法的注视可知,如果该方法会因为三种情况返回,有人调用了unpark,有人调用了interrupt,没有原因的错误。所以acquireQueued方法进行了检验是不是第二个元素如果不是继续休眠。之所以调用Thread.interrupted而不是this.isInterrupted方法是因为,防止shouldParkAfterFailedAcquire方法空转,因为有人调用interrupt会导致LockSupport.park方法立即返回。

在这里插入图片描述
最后在finally里面调用了cancelAcquire方法,截图太长,粘代码了

 /**
     * Cancels an ongoing attempt to acquire.
     *
     * @param node the node
     */
    private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;

        node.thread = null;

        // Skip cancelled predecessors
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        // predNext is the apparent node to unsplice. CASes below will
        // fail if not, in which case, we lost race vs another cancel
        // or signal, so no further action is necessary.
        Node predNext = pred.next;

        // Can use unconditional write instead of CAS here.
        // After this atomic step, other Nodes can skip past us.
        // Before, we are free of interference from other threads.
        node.waitStatus = Node.CANCELLED;

        // If we are the tail, remove ourselves.
        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);
        } else {
            // If successor needs signal, try to set pred's next-link
            // so it will get one. Otherwise wake it up to propagate.
            int ws;
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                unparkSuccessor(node);
            }

            node.next = node; // help GC
        }
    }

该方法调用不保证成功,
主要作用有两个,一个是唤起下一个等待的(应该不是主要作用),另一个是删除队列中CANCLE的节点。分三种情况:
1是自己是队尾,就把自己设置成队尾。
2. pred已经是对头了或者自己不是头节点状态为CANCEL或者自己不是头节点

此方法后续再看
至此加锁过程已看完

解锁

解锁过程公平锁和非公平锁一样

unlock

在这里插入图片描述

release

在这里插入图片描述

tryRelease

在这里插入图片描述
释放锁时先判断是不是当前持有锁的线程在释放锁,不是的话跑异常,是的话就不需要同步,进行state的值的减少,如果减少后为0则至为free。释放锁成功后唤醒head的后继节点
在这里插入图片描述
尝试把当前节点(head)的ws设置为0,从后向前找到离自己最近的需要唤醒的节点 & 唤醒他。此时为多线程并发操作队列。意外的情况可能有,此时该节点已被唤醒(没影响)、已被取消(会tryAcquire失败),进入到shouldParkAfterFailedAcquire方法,删除掉自己。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值