面试必备八股文第三篇(JUC)

面试必备八股文第三篇(JUC)

线程池

为什么要使用线程池?

线程是稀缺资源,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以重复使用。

线程池的实现原理?

1、判断核心线程数是否已满,未满:创建线程执行任务
2、如果核心线程数已满,校验阻塞队列是否已满,未满:放入队列等待
3、如果阻塞队列已满,判断最大线程数是否已满,未满:创建线程执行任务
4、如果最大线程数已满,执行拒绝策略

线程池有几大参数?分别是哪几个参数?

corePoolSize:核心线程数,线程池中的常驻核心线程数
maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1
keepAliveTime:多余的空闲线程存活时间
unit:keepAliveTime的单位
workQueue:存放任务的队列
threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程池 一般用默认即可
handler:拒绝策略,表示当队列满了并且工作线程大于线程池的最大线程数(maximumPoolSize)时

线程池被创建后如果没有任务过来,里面会有线程吗?

线程池被创建后如果没有任务过来,里面是不会有线程的

核心线程数会被回收吗?

核心线程数默认是不会被回收的,如果需要回收核心线程数,需要调用allowCoreThreadTimeOut(true)

线程通信方式

Semaphore信号量了解吗?

信号量主要用于两个目的
一个是用于共享资源的互斥使用
另一个用于并发线程数的控制
场景:模拟一个抢车位的场景,假设一共有6个车,3个停车位,那么我们首先需要定义信号量为3,也就是3个停车位

synchronized 、ReentrantLock 、Semaphore异同

synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。

CountDownLatch 的不足

CountDownLatch 是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 CountDownLatch 使用完毕后,它不能再次被使用

CyclicBarrier 和 CountDownLatch 的区别

CountDownLatch 是计数器,线程完成一个记录一个,只不过计数不是递增而是递减
CyclicBarrier 更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行。

volatile是什么?有什么用

是java关键字,JVM提供的最轻量的同步机制,用来修饰变量,保证该变量对所有线程可见,禁止指令重排,但是不保证原子性

volatile如何禁止指令重排?

使用volatile时候,汇编代码会多出一个 lock addl $0x0,(%esp)
lock指令相当于一个内存屏障:
1.重排序时不能把后面的指令重排序到内存屏障之前的位置
2.将本处理器的缓存写入内存
3.如果是写入动作,会导致其他处理器中对应的缓存无效。

使用volatile,java内存模型如何实现内存屏障

在每个volatile写操作的前面插入一个StoreStore屏障。
在每个volatile写操作的后面插入一个StoreLoad屏障。
在每个volatile读操作的后面插入一个LoadLoad屏障。
在每个volatile读操作的后面插入一个LoadStore屏障。

线程有哪几种状态?

1、新建
2、就绪;start方法,等待获取CPU使用权
3、运行:获取到CPU使用权,执行程序
4、阻塞:因为某种原因失去CPU使用权
5、死亡

终止线程有几种方式?平时采用哪种?

1、正常结束,程序运行完毕
2、使用标志
3、interrupt()方法
4、stop强制停止,线程不安全,将释放所有锁,数据不一致问题

sleep和wait区别?

(1)sleep方法属于Thread类,wait方法属于Object类。
(2)sleep方法暂停执行指定的时间,让出CPU给其他线程,但其监控状态依然保持在指定的时间过后又会自动恢复运行状态。
(3)在调用sleep方法的过程中,线程不会释放对象锁,而wait会释放对象锁。

sleep后进入什么状态,wait后进入什么状态?

sleep后进入Time waiting超时等待状态,wait后进入等待waiting状态。

wait为什么是数Object类下面的方法?

所谓的释放锁资源实际是通知对象内置的monitor对象进行释放,而只有所有对象都有内置的monitor对象才能实现任何对象的锁资源都可以释放。又因为所有类都继承自Object,所以wait()就成了Object方法,也就是通过wait()来通知对象内置的monitor对象释放,而且事实上因为这涉及对硬件底层的操作,所以wait()方法是native方法,底层是用C写的。

start和run的区别?

start是Thread类用来启动线程的一个方法,只是处于就绪状态;run直接让线程处于运行状态

什么是守护线程?

通过 setDaemon(true)来设置线程为“守护线程”
垃圾回收线程就是一个典型的守护线程,程序不运行任何线程就不会产生垃圾,垃圾回收器就无事可做,自动离开
守护线程与系统同生共死,当JVM里所有线程都是守护线程时候,JVM就会退出

interrupted 和 isInterrupted

interrupted : 判断当前线程是否已经中断,会清除状态。
isInterrupted :判断线程是否已经中断,不会清除状态。

内存

MESI协议是什么?

解决缓存一致性的协议,简单来说就是CPU写数据时,操作共享变量时候,主内存保证最新的值,其他CPU保存副本,不同就置为无效,重新从主内存中读取

M(Modified):该缓存行只被该CPU缓存,与主存的值不同,会在它被其他CPU读取之前写入内存,并设置为Shared
E(Exclusive):该缓存行只被该CPU缓存,与主存的值相同,被其他CPU读取时置为Shared,被其他CPU写时置为Modified
S(Shared): 该缓存行可能被多个CPU缓存,各个缓存中的数据与主存数据相同
I(Invalid): 该缓存行数据是无效,需要时需重新从主存载入

MESI协议是如何实现的?如何保证当前处理器的内部缓存、主内存和其他处理器的缓存数据在总线上保持一致的?

多处理器总线嗅探:每个处理器通过嗅探在总线上传播的数据来检查自己的缓存值,被修改时就会置为无效,并重新从主内存读取到自己的内存中

什么是内存泄露?

当对象不再被使用并且垃圾回收器无法回收,就发生了内存泄露

如何防止内存泄露?

1.使用List、Map等集合时,在使用完成后赋值为null
2.使用大对象时,在用完后赋值为null
3.避免一些死循环等重复创建或对集合添加元素,撑爆内存
4.简洁数据结构、少用静态集合等
5.流用完就关闭

读写锁ReentrantReadWriteLock

独占锁:指该锁一次只能被一个线程所持有。对ReentrantLock和Synchronized而言都是独占锁
共享锁:指该锁可以被多个线程锁持有
对ReentrantReadWriteLock其读锁是共享,其写锁是独占.写的时候只能一个人写,但是读的时候,可以多个人同时读,读写锁就可以提高效率

公平锁和非公平锁

公平锁:就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列中的第一个,就占用锁,否者就会加入到等待队列中,以后按照FIFO的规则从队列中取到自己
非公平锁: 非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。
ReenttrantLock通过构造函数指定该锁是否公平,默认是非公平锁,因为非公平锁的优点在于吞吐量比公平锁大,对于synchronized而言,也是一种非公平锁

公平锁和非公平锁底层源码区别是什么?

公平锁与非公平锁的区别:唯一区别有没有判断队列,hasQueuedPredecessors是公平锁加锁时判断等待队列中是否存在有效节点的方法。

可重入锁和递归锁ReentrantLock

可重入锁就是递归锁,线程可以进入任何一个它已经拥有的锁所同步的代码块,ReentrantLock / Synchronized 就是一个典型的可重入锁,可重入锁的最大作用就是避免死锁。
注意:申请几把锁,最后需要解除几把锁,不然会报错

你了解Synchronized的用法吗?

synchronized修饰普通同步方法:锁对象当前实例对象,所以是对象锁
synchronized(this){},它的作用域是当前对象,所以是对象锁
synchronized修饰static同步方法:锁对象是当前的类Class对象,所以是类锁
synchronized修饰同步代码块:锁对象是Synchronized后面括号里配置的对象,这个对象可以是某个对象(xlock),也可以是某个类(Xlock.class)

synchronized和lock的区别?

1、synchronized是关键字,lock是接口
2、synchronized不可中断,lock调用lock.lockInterruptibly()中断
3、synchronized自动释放锁,lock调用lock.unlock()手动释放
4、synchronized不能知道哪个线程是否拿到锁,lock可以判断
5、synchronized能锁方法和代码块,lock只能锁代码块
6、synchronized是非公平锁,ReentrantLock可以设置公平或者非公平

Synchronized对象锁和类锁多线程操作时候有什么区别?

synchronized修饰的实例方法,多线程并发访问时,只能有一个线程进入,获得对象内置锁,其他线程阻塞等待,但在此期间线程仍然可以访问其他方法。

synchronized修饰的静态方法,多线程并发访问时,只能有一个线程进入,获得类锁,其他线程阻塞等待,但在此期间线程仍然可以访问其他方法。

synchronized修饰的代码块,多线程并发访问时,只能有一个线程进入,根据括号中的对象或者是类,获得相应的对象内置锁或者是类锁

每个类都有一个类锁,类的每个对象也有一个内置锁,它们是互不干扰的,也就是说一个线程可以同时获得类锁和该类实例化对象的内置锁,当线程访问非synchronzied修饰的方法时,并不需要获得锁,因此不会产生阻塞。

Synchronized相关知识点

synchronized锁对象是存在对象头Mark Word最后两位,10标识重量级锁,其中指针指向的是monitor对象的起始地址
对于同步方法,JVM采用ACC_SYNCHRONIZED标记符来实现同步。
对于同步代码块JVM采用monitorenter、monitorexit两个指令来实现同步,每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为0,执行monitorenter+1,monitorexit-1

synchronized包括哪两个jvm重要的指令?

monitor enter 和 monitor exit

为什么synchronized监视器monitor里面有两个monitorexit?一个不行吗?

执行monitorexit理解为释放锁,一个是正常释放,另一个是异常保证也能释放,防止死锁

谈谈Synchronized锁的几种状态,以及锁升级过程

无锁->偏向锁->轻量级锁->重量级锁
锁可以升级但不能降级,但是偏向锁状态可以被重置为无锁状态

例子:
一个厕所,上厕所需要钥匙,只有一个线程,线程1一直上厕所,每次重复获取锁就很麻烦,于是升级成偏向锁,下次识别到是线程1就不用获取锁直接上:从无锁升级成偏向锁,CAS替换Thread id
–>此时多线程想要上厕所,产生竞争状态,于是等待线程1用完,撤销偏向锁:升级成轻量级锁
–>下次哪个线程先到,就哪个线程使用,其他线程自旋等待,等待很久变成线性阻塞后,升级成重量级锁

Unsafe

Unsafe是什么?

提供底层访问机制,几乎可以操作一切

Unsafe为什么是不安全的?

因为java被设计成安全特性,unsafe几乎可以操作一切,用不好容易导致问题所以被认为是不安全的

Unsafe的实例怎么获取?

java内部类可以直接实例化,外部使用反射方式获取

Unsafe的CAS操作?

CompareAndSwap大量用于无锁算法,就是比较要修改的值是否与之前一样,一样才修改,这样会有ABA问题,所以为了避免一般比较的值会加上另外的标记位

Unsafe的阻塞/唤醒操作?

LockSupport底层用的就是Unsafe的part和unpart方法

CAS

谈谈CAS?

getAndIncrement() 核心源码

// var1:当前对象Object
// var2:内存地址
// var4:1
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        // 获取主内存的值
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    // 循环比较 主内存的值和期望值是否相等,相等则只需var5+var4,即主内存值自增1
    return var5;
}

核心都是unsafe类的native操作底层,原子类都是使用CAS,而不是锁来保证一致性,CAS就是比较当前工作内存中的值和主物理内存中的值,如果相同则执行规定操作,否者一直循环比较直到主内存和工作内存的值一致为止

CAS的缺点?

1、可能会无限循环
2、只能保证一个共享变量的原子性
3、导致ABA问题

LockSupport

LockSupport是什么?

用于创建锁和其他同步类的基本线程阻塞原语,通过许可证permit(0,1)来控制,许可证最大是1,默认是0,使用park()阻塞许可证=0,unpark()解除阻塞许可证=1,仅允许使用一次

LockSupport和Synchronized、Lock的等待唤醒有什么不同?

Synchronized:wait()/notify(),必须wait先执行,notify后执行,在sync同步代码块中使用
Lock配合Condition:await()/signal(),必须成对出现,必须在lock代码块中使用
LockSupport:park()/unpark(),成对出现,先unpark再park或者park再unpark都可以,不可以多次使用,有且仅有一对park和unpark,因为一个线程有且仅有一张通行证,消费完就没了。

原子类

AtomicInteger怎么实现原子操作的?

CAS + volatile 和 native 方法来保证

AtomicInteger主要解决了什么问题?

自增调用的是Unsafe的CAS并使用自旋保证一定会成功,它可以保证两步操作的原子性。

AtomicInteger有哪些缺点?

ABA问题,可能长时间处于自旋

ABA的危害?

提款机有问题,导致2个线程进来操作提款

例子:
线程1(提款机):获取当前值100,期望更新为50
线程2(提款机):获取当前值100,期望更新为50
线程1成功执行,余额变成50,线程2某种原因block了,这时,某人给小明汇款50
线程3(默认):获取当前值50,期望更新为100
线程3成功执行,余额变为100,线程2从Block中恢复,获取到的也是100,compare之后,继续更新余额为50!!!

此时可以看到,实际余额应该为100(100-50+50),但是实际上变为了50(100-50+50-50)这就是ABA问题带来的成功提交

AtomicStampedReference是怎么解决ABA的?

时间戳原子引用,来这里应用于版本号的更新,也就是每次更新的时候,需要比较期望值和当前值,以及期望版本号和当前版本号

谈谈LongAddr?

LongAdder是java8中新增的原子类,在多线程环境中,它比AtomicLong性能要高出不少
不同的线程更新不同的段,这些段相加,通过cell数组来分,cells的数组最大只会到CPU核数量
最后,如果你要从LongAdder中获取当前累加的总值,就会把base值和所有Cell分段数值加起来返回给你。

AQS

AQS(AbstractQueuedSynchronizer)是什么?

抽象队列同步器,是用来构建锁(Lock)或者其它同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类型变量表示持有锁的状态。
一句话概括:AQS就是一个状态(state)+ 双向队列(FIFO)

AQS为什么是JUC内容中最重要的基石?

查看ReentrantLock、CountDownLatch、ReentrantReadWriteLock、Semaphore等锁和同步器的源码,可以发现都有一个内部类Sync继承AbstractQueuedSynchronizer

AQS内部结构了解过吗?

AbstractQueuedSynchronizer与HashMap也类似,里面有一个Node节点,线程Thread放在Node节点里面,AQS使用一个volatile的int类型的state变量来表示同步状态,初始值是0,通过内置的CLH(FIFO)队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成一个Node节点来实现锁的分配,通过CAS完成对State值的修改。

AQS源码看过吗?了解JUC包下加锁的AQS里方法的执行原理吗?

以NofairLock加锁为例

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

1、线程B执行AQS的acquire(1)方法

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

2、先调用tryAcquire(int arg)方法尝试获取,在AQS里是个模板方法,子类自己实现抢占锁逻辑
3、调用addWaiter(Node.EXCLUSIVE), arg),初始化一个空节点来当头结点,以及线程B入队接在头结点后,注意线程B不是头结点
4、入队完成,执行acquireQueued(),执行LockSupport.park阻塞线程B,开启自旋抢占等待线程A的释放
5、线程A解锁,线程B获取锁,出队,执行LockSupport.unpark
6、最后通过GC回收队列里面的B节点

ThreadLocal

ThreadLocal栈堆的结构

在这里插入图片描述

1、当一个线程运行时,栈中存在当前Thread的栈帧,它持有ThreadLocalMap的强引用。
2、ThreadLocal所在的类持有一个ThreadLocal的强引用;同时,ThreadLocalMap中的Entry持有一个ThreadLocal的弱引用。

ThreadLocal的key弱引用导致内存泄漏,那为什么key不设置为强引用?

如果key设置为强引用, 当threadLocal实例释放后,threadLocal=null,但是threadLocal会有强引用指向threadLocalMap,threadLocalMap.Entry又强引用threadLocal,这样会导致threadLocal不能正常被GC回收。
弱引用虽然会引起内存泄漏, 但是也有set、get、remove方法操作对null key进行擦除的补救措施,方案上略胜一筹。

那弱引用这么好,为什么Entry的value不使用弱引用?

假如value被设计成弱引用,那么很有可能当你需要取这个value值的时候,取出来的值是一个null。

Thread和ThreadLocalMap的关系

在这里插入图片描述

Thread类中的threadLocals就是ThreadLocal中的ThreadLocalMap
每个线程Thread都有自己的ThreadLocalMap成员变量
一个ThreadLocalMap中可以有多个ThreadLocal对象,其中一个ThreadLocal对象对应一个ThreadLocalMap中的一个Entry,Entry的key引用就是ThreadLocal对象

ThreadLocalMap和HashMap区别?

1、HashMap 处理哈希冲突使用的「链表法」。也就是当产生冲突时拉出一个链表,而且 JDK 1.8 进一步引入了红黑树进行优化。
2、ThreadLocalMap 则使用了「开放寻址法」中的「线性探测」。即,当某个位置出现冲突时,从当前位置往后查找,直到找到一个空闲位置。

用ThreadLocal如何维护变量

当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

ThreadLocal作用是什么?

ThreadLocal 主要用于当前线程从共享变量中保存一份「副本」

ThreadLocal是如何做到为每一个线程维护变量的副本的呢?

在ThreadLocal类中有一个static声明的Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。

ThreadLocalMap与WeakReference

static class ThreadLocalMap { 
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
   }
    ...
}

为什么要用 ThreadLocalMap 来保存线程局部对象呢?

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

原因是一个线程拥有的的局部对象可能有很多,这样实现的话,那么不管你一个线程拥有多少个局部变量,都是使用同一个 ThreadLocalMap 来保存的
ThreadLocalMap 中 private Entry[] table 的初始大小是16,超过容量的2/3时,会扩容。

ThreadLocal内存自动回收

1)在ThreadLocal层面的内存回收:当线程死亡时,那么所有的保存在的线程局部变量就会被回收,其实这里是指线程Thread对象中的ThreadLocal.ThreadLocalMap会被回收,这是显然的。

2)ThreadLocalMap层面的内存回收:如果线程可以活很长的时间,并且该线程保存的线程局部变量有很多(也就是 Entry 对象很多),那么就涉及到在线程的生命期内如何回收ThreadLocalMap的内存了,不然的话,Entry对象越多,那么ThreadLocalMap 就会越来越大,占用的内存就会越来越多,所以对于已经不需要了的线程局部变量,就应该清理掉其对应的Entry对象。

使用的方式是,Entry对象的key是WeakReference 的包装,当ThreadLocalMap 的 private Entry[] table ,已经被占用达到了三分之二时 threshold = 2/3 (也就是线程拥有的局部变量超过了10个) ,
就会尝试回收Entry对象,我们可以看到ThreadLocalMap.set()方法中有下面的代码:
if (!cleanSomeSlots(i, sz) && sz >= threshold),cleanSomeSlots 就是进行回收内存。

ThreadLocal可能引起的内存溢出问题简要分析

我们知道ThreadLocal变量是维护在Thread内部的,这样的话只要我们的线程不退出,对象的引用就会一直存在。
一种场景就是说如果使用了线程池并且设置了固定的线程,处理一次业务的时候存放到ThreadLocalMap中一个大对象,处理另一个业务的时候,又一个线程存放到ThreadLocalMap中一个大对象,但是这个线程由于是线程池创建的他会一直存在,不会被销毁,这样的话,以前执行业务的时候存放到ThreadLocalMap中的对象可能不会被再次使用,但是由于线程不会被关闭,因此无法释放

ThreadLocal为什么申明为private static final?

private:是否使用 private 修饰与 ThreadLocal 本身无关,一个建议推荐习惯而已
static:表示为类属性,只有在程序结束才会被回收
final:尽可能不让他人修改变更引用

Thread 类有个 ThreadlocalMap 属性的成员变量,但是ThreadlocalMap 的定义却在Threadlocal 中,为什么这么做?

ThreadLocalMap就是为维护线程本地变量而设计的,只做这一件事情。
这个也是为什么 ThreadLocalMap是Thread的成员变量,但是却是Threadlocal的内部类(非public,只有包访问权限,Thread和Threadlocal都在java.lang 包下)
就是让使用者知道ThreadLocalMap就只做保存线程局部变量这一件事的

既然是线程局部变量,那为什么不用线程对象(Thread对象)作为key,这样不是更清晰,直接用线程作为key获取线程变量?

比如: 我已经把用户信息存在线程变量里了,这个时候需要新增加一个线程变量,比方说新增用户地理位置信息,我们ThreadlocalMap 的key用的是线程,再存一个地理位置信息,key都是同一个线程(key一样),不就把原来的用户信息覆盖了嘛。

那既然ThreadLocal对象有强引用,回收不掉,干嘛还要设计成WeakReference类型呢?

ThreadLocal的设计者考虑到线程往往生命周期很长,比如经常会用到线程池,线程一直存活着,一直存在 Thread -> ThreadLocalMap -> Entry(元素)这样一条引用链路, 如下图,如果key不设计成WeakReference类型,是强引用的话,就一直不会被GC回收,key就一直不会是null,不为null Entry元素就不会被清理(ThreadLocalMap是根据key是否为null来判断是否清理Entry)

CPU

CPU的缓存架构是怎样的?

主内存、 一级、二级、三级缓存、CPU核心

CPU的缓存行是什么?

Cache line 是 cache 和 RAM 交换数据的最小单位,通常为 64 Byte

内存屏障又是什么?

一些cpu指令,编译器会通过放置指令保证重排序顺序

伪共享是什么原因导致的?

当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能

怎么避免伪共享?

java8 @Contended注解

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值