JUC并发编程面试题以及知识点

JUC相关面试题

谈谈什么是线程池?

线程池和数据库连接池非常类似,可以统一管理和维护线程,减少没必要的系统开销。

为什么要使用线程池?

因为频繁地开启或者停止线程,线程需要重新被CPU从就绪到运行状态调度,需要发生CPU的上下文切换,效率非常低。

JUC并发编程面试题以及知识点_第1张图片

你们哪些地方会使用到多线程?

实际开发项目中,禁止自己new线程。

必须使用线程池来维护和创建线程。

线程池有哪些作用?

核心点:复用机制 -- 提前创建好固定的线程一直保持在运行状态,从而实现复用,限制线程创建的数量。

1.降低资源消耗:通过池化技术重复利用自己创建的线程,降低线程创建和消耗造成的损耗。

2.提高响应速度:任务到达时无需等待线程创建即可立即执行。

3.提高了线程的可管理性:线程是稀缺资源,如果无限制创建,不仅仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以统一地分配、调优和监控。

4.提供更多更强大的功能:线程池具备可扩展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或者是定时执行。

线程池的创建方式有哪些?

Executors.newCachedThreadPool();可缓存线程池

Executors.newFixedThreadPool();可定长度 限制了最大线程数

Executors.newScheduledThreadPool();可定时线程池

Executors.newSingleThreadExecutor();单例

底层都是基于ThreadPoolExceutor构造函数封装

线程池是如何实现复用的?

本质思想:创建一个线程,不会立马停止或者销毁,而是一直实现复用。

1.提前创建好固定大小的线程,一直保持着正在运行状态;(但是这也可能会非常消耗CPU资源)

2.当需要线程执行任务,将该任务提交缓存在并发队列中;如果缓存队列满了,则会执行拒绝策略。

3.正在运行的线程从并发队列中获取任务执行,从而实现多线程复用问题。

JUC并发编程面试题以及知识点_第2张图片

线程池的核心点:复用机制 ---

1.提前创建好固定的线程一直在运行状态----死循环实现

2.提交的线程任务缓存到一个并发队列集合中,交给我们正在运行的线程执行

3.正在运行的线程就从队列中获取该任务执行

package com.mayikt;

import java.util.ArrayList;

import java.util.List;

import java.util.concurrent.BlockingDeque;

import java.util.concurrent.LinkedBlockingDeque;

/**

* 线程池核心点:复用机制 ----

* 1.提前创建好固定的线程,一直处于运行状态----死循环实现

* 2.提交的线程任务缓存到一个并发队列集合中,交给我们正在运行的线程执行

* 3.正在运行的线程就从队列中获取该任务执行

*/

public class MyExecutors {

// 线程并发队列集合

private List workThreads;

// 缓存我们线程任务

private BlockingDeque runnableDeque;

private boolean isRun = true;

/**

* @param maxThreadCount 最大线程数

*/

public MyExecutors(int maxThreadCount, int dequeSize) {

//1.限制队列容量缓存

runnableDeque = new LinkedBlockingDeque(dequeSize);

//2.提前创建好固定的线程,一直处于运行状态----死循环实现

workThreads = new ArrayList(maxThreadCount);

for (int i = 0; i < maxThreadCount; i++) {

new WorkThread().start();

}

}

class WorkThread extends Thread {

@Override

public void run() {

while (isRun || runnableDeque.size() > 0) {

Runnable runnable = runnableDeque.poll();

if (runnable != null) {

runnable.run();

}

}

}

}

public boolean execute(Runnable command) {

return runnableDeque.offer(command);

}

public static void main(String[] args) {

MyExecutors myExecutors = new MyExecutors(2, 20);

for (int i = 0; i < 10; i++) {

final int finalI = i;

myExecutors.execute(() -> System.out.println(Thread.currentThread().getName() + "," + finalI));

}

myExecutors.isRun = false;

}

}

ThreadPoolExecutor核心参数有哪些?

corePoolSize:核心线程数量--- 一直正在保持运行的线程的数量

maximumPoolSize:最大线程数,线程池允许创建的最大线程数

keepAliveTime:超出corePoolSize后创建的线程的存活时间

unit:keepAliveTime的时间单位

workQueue:任务队列,用于保存待执行的任务

threadFactory:线程池内部创建线程所用的工厂

handler:任务无法执行时的处理器

线程池创建的线程会一直保持为运行状态吗?

不会的。

例如:配置核心线程数corePoolSize为2,最大线程数maximumPoolSize为5

我们可以通过配置超出corePoolSize核心线程数后创建的线程的存活时间,例如为60s

在60s内,如果核心线程一直没有任务执行,则会停止该线程。

为什么阿里巴巴不建议使用Executors?

因为默认的Executors线程池底层是基于ThreadPoolExeutor构造函数封装的,采用无界队列存放缓存任务,这样会导致由于可无限制地缓存任务从而发生内存溢出,最终导致最大线程数失效。

JUC并发编程面试题以及知识点_第3张图片

线程池底层ThreadPoolExecutor底层实现原理?

1.当线程数小于核心线程数时,创建线程。

2.当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列

3.当线程数大于等于核心线程数,且任务队列已满时

3.1若线程数小于最大线程数,创建线程

3.2若线程数等于最大线程数,抛出异常,拒绝任务

到底能执行多少个任务:公式:任务队列容量+最大线程数=真实能执行的任务量

线程池队列满了,任务是否会丢失?

如果队列满了,且任务总数>最大线程数,则当前线程走拒绝策略。

可以自定义拒绝异常,将该任务缓存到redis、本地文件、MySQL中后期项目启动实现补偿。

1.AbortPolicy 丢弃任务,抛运行时异常

2.CallerRunsPolicy 执行任务

3.DiscardPolicy 忽视,什么都不会发生

4.DiscardOldestPolicy 从队列中剔除最先进入队列(最后一个执行)的任务

5.实现RejectedExecutionHandler接口,可自定义处理器

线程池拒绝策略类型有哪些呢?

1.AbortPolicy 丢弃任务,抛运行时异常

2.CallerRunsPolicy 执行任务

3.DiscardPolicy 忽视,什么都不会发生

4.DiscardOldestPolicy 从队列中剔除最先进入队列(最后一个执行)的任务

5.实现RejectedExecutionHandler接口,可自定义处理器

线程池如何合理配置参数?

自定义线程池就需要我们自己配置最大线程数maximumPoolSize,为了高效地并发运行,当然这个不能够随便设置。这时需要看我们的业务是IO密集型还是CPU密集型。

CPU密集型

CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟器的多线程任务都不可能得到加速,因为CPU总的运算能力就那些。

CPU密集型任务配置尽可能少的线程数量:这是为了以保证每个CPU高效地运行一个线程。

一般公式:(CPU核数+1)个 线程的线程池

IO密集型

IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO 密集型的任务会导致浪费大量的CPU运算能力在等待。所以在IO密集型任务中使用多线程可以大大地加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。

IO密集型时,大部分线程都阻塞,故需要配置多线程数:

公式:

CPU核数×2

CPU核数÷(1-阻塞系数),阻塞系数在0.8~0.9之间

查看CPU核数:

System.out.println(Runtime.getRuntime().availableProcessors());

什么是悲观锁?什么是乐观锁?

悲观锁:

1.站在MySQL的角度分析:悲观锁就是比较悲观,当多个线程对同一行数据实现修改的时候,最后只有一个线程才能修改成功,只要谁能够对获取到行锁则其他线程是不能够对该数据做任何修改操作,且是阻塞状态。

2.站在Java锁层面,如果没有获取到锁,则会阻塞等待,后期唤醒的锁的成本就会非常高,重新被我们额CPU从就绪调度为运行状态。

Lock syn锁 悲观锁没有获取到锁的线程会阻塞等待。

乐观锁:

乐观锁比较乐观,通过预期或者版本号比较。如果不一致的情况则通过循环控制修改,当前线程不会被阻塞,是乐观,效率较高,但是乐观锁比较消耗CPU的资源。

JUC并发编程面试题以及知识点_第4张图片

乐观锁:获取锁---如果没有获取到锁,当前线程是不会阻塞等待 通过死循环控制。

乐观锁属于无锁机制,美亚由竞争锁流程。

注意:MySQL的innodb引擎中存在行锁的概念!

MySQL层面如何实现乐观锁呢?

在我们表结构中,会新增一个字段,就是版本字段。

version varchar(255)DEFAULT NULL,

多个线程对同一行数据实现修改操作,提前查询当前最新的version版本号码,作为update条件查询;如果当前version版本号码发生了变化,则查询不到该数据。表示如果修改数据失败,则不断重试,又重新查询最新的版本实现update;但是需要注意控制乐观锁循环的次数,避免CPU飙高的问题。

注意:Java实现乐观锁是通过cas的方式

乐观锁实现方式

1.cas(无锁)(juc并发包框架原子类)(结合自旋),底层基于修改内存值(V),E(旧的预期值)

E==V 则表示修改v=n;

2.基于cas实现无锁机制

原理:

变量===0

0表示没有线程获取该锁

1表示已经有线程获取到该锁

cas获取锁:

cas 修改该变量=0,1==cas修改成功的话则表示获取锁成功

cas释放锁 变量1,0 cas释放锁成功

Java有哪些锁的分类呢?

1.悲观锁与乐观锁

2.公平锁与非公平锁

3.自旋锁 /重入锁

4.重量级锁与轻量级锁

5.独占锁与共享锁

6.读锁,写锁与读写锁

7.互斥锁/排它锁

公平锁与非公平锁之间的区别?

公平锁:就是比较公平,根据请求锁的顺序排列,先来请求的就先获取锁,后来获取锁就最后获取到,采用队列存放,类似于吃饭排队。

非公平锁:不是根据请求的顺序排列,通过争抢的方式获取锁。

非公平锁效率比公平锁效率要高,synchronized是非公平锁。

new ReentramtLock(true)----公平锁

new ReentramtLock(false)----非公平锁

底层是基于aqs实现

公平锁底层是如何实现的?

公平锁:就是比较公平,根据请求锁的顺序排列,先来请求的就先获取锁,后来获取锁就最后获取到 ,采用队列存放,类似于吃饭排队。

队列---底层实现方式---数组或者链表实现

独占锁与共享锁之间的区别?

独占锁:在多线程中,只允许有一个线程获取到锁,其他线程都会等待。

共享锁:多个线程可以同时持有锁,例如RenntrantLock读写锁。读读可以共享,写写互斥、读写互斥,写读互斥。

什么是锁的可重入性?

在同一个线程中,锁可以不断传递的,可以直接获取。

synchronized/lock

底层基于aqs实现。

什么是CAS(自旋锁)?以及其优缺点?

synchronized/lock

CAS

没有获取到锁的线程是不会阻塞的,通过循环控制一直不断地获取锁。

CAS:Compare and Swap,翻译“比较并交换”。执行函数CAS(V,E,N)

CAS有3个操作数,内存值V,旧的预期值E,要修改的新值N。当且仅当预期值E和内存值V相同时,将 内存值V修改成N,否则什么都不做。

JUC并发编程面试题以及知识点_第5张图片

1.CAS是通过硬件指令,保证原子性

2.Java是通过unsafe jni技术

原子类:AtomicBoolean,AtomicInteger,AtomicLong等使用CAS实现。

优点:没有获取到锁的线程,会一直在用户态,不会阻塞,没有锁的线程会一直通过循环控制重试。

缺点:通过死循环控制,消耗CPU资源比较高,需要控制循环次数,避免CPU飙高问题。

CAS本质的原理:

旧的预期值==V(共享变量中值),才会修改我们的V。

基于CAS实现机制原理。

CAS无锁机制原理:

1.定义一个锁的状态;

2.状态值=0,则表示没有线程获取到该锁;

3.状态值=1,则表示有线程已经持有该锁。

实现细节:

CAS获取锁:

将该锁的状态值从0改为1--- 能够修改成功,CAS成功则表示获取锁成功

如果获取锁失败---修改失败,则不会阻塞而是通过循环(自旋来控制重试)

CAS释放锁:

将该锁的状态从1改为0,如果能够修改成功,CAS成功则表示释放锁成功。

CAS如何避免ABA的问题?

CAS主要检查内存值V与旧的预期值E是否一致,如果一致的情况下,则修改。

这时候存在ABA的问题。

如果将原来的值A改为了B,B又改为了A,发现没有发生变化。但实际上已经发生了变化,所以存在ABA问题。

解决方法:通过版本号码,对每个变量更新的版本号做+1操作。

解决ABA问题是否生效:概念产生冲突,但是不影响结果,换一种方式,通过版本号码的方式。

JUC并发编程面试题以及知识点_第6张图片

演示ABA问题的示例代码

package com.mayikt;

import java.util.concurrent.atomic.AtomicStampedReference;

/**

* 演示ABA的问题

* (1)第一个参数expectedReference:表示预期值

* (2)第二个参数newReference;表示要更新的值

* (3)第三个参数expectedStamp:表示预期的时间戳

* (4)第四个参数newStamp:表示要更新的时间戳

*/

public class AtomicMarkableReferenceTest {

//注意:如果引用类型是Long、Integer、Short、Byte、Character一定要注意值的缓存区间!

// 比如Long、Integer、Short、Byte缓存区间是在-128~127,会直接存在常量池中,而不在这个区间内对象的值则会每次都new一个对象,

// 那么即使两个对象的值相同,CAS方法都会返回false

// 先声明初始值,修改后的值和临时的值是为了保证使用CAS方法不会因为对象不一样而返回false

private static final Integer INIT_NUM = 1000;

private static final Integer UPDATE_NUM = 100;

private static final Integer TEM_NUM = 200;

private static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(INIT_NUM, 1);

public static void main(String[] args) {

new Thread(() -> {

Integer value = (Integer) atomicStampedReference.getReference();

int stamp = atomicStampedReference.getStamp();

System.out.println(Thread.currentThread().getName() + ":当前值为:" + value + "版本号为:" + stamp);

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

//value 旧值 内存中的值 UPDATE_NUM 修改的值

if (atomicStampedReference.compareAndSet(value, UPDATE_NUM, 1, stamp + 1)) {

System.out.println(Thread.currentThread().getName() +

":当前值为:" + atomicStampedReference.getReference() +

"版本号为:" + atomicStampedReference.getStamp());

} else {

System.out.println("版本号不同,更新失败!");

}

if (atomicStampedReference.compareAndSet(value, UPDATE_NUM, 2, stamp + 1)) {

System.out.println(Thread.currentThread().getName() +

":当前值为:" + atomicStampedReference.getReference() +

"版本号为:" + atomicStampedReference.getStamp());

} else {

System.out.println("版本号不同,更新失败!");

}

}, "线程A").start();

}

}

利用原子类手写CAS无锁的示例代码

package com.mayikt;

import java.util.concurrent.atomic.AtomicLong;

import java.util.stream.IntStream;

/**

* 利用cas,手写无锁机制

*/

public class AtomicTryLock {

private AtomicLong cas = new AtomicLong(0);

private Thread lockCurrentThread;

/**

* 获取锁

* 获取锁:()

* 锁是有状态:0----表示没有人获取到该锁/持有该锁

* 1----表示该锁已经被线程持有

* 获取锁成功:cas 0 变为11 cas=true

* 获取锁失败: cas false

*

* @return

*/

public boolean tryLock() {

boolean result =cas.compareAndSet(0,1);

if (result) {

lockCurrentThread=Thread.currentThread();

}

return result;

}

/**

* 释放锁

*

* @return

*/

public boolean unLock() {

if (lockCurrentThread!=Thread.currentThread()){

return false;

}

return cas.compareAndSet(1, 0);

}

public static void main(String[] args) {

AtomicTryLock atomicTryLock = new AtomicTryLock();

IntStream.range(1,10).forEach((i)->new Thread(()->{

try {

boolean result = atomicTryLock.tryLock();

if (result) {

atomicTryLock.lockCurrentThread=Thread.currentThread();

System.out.println(Thread.currentThread().getName() + ",获取锁成功");

}else {

System.out.println(Thread.currentThread().getName() + ",获取锁失败");

}

}catch (Exception e){

}finally {

if (atomicTryLock != null) {

atomicTryLock.unLock();

}

}

}).start());

}

}

谈谈你对ThreadLocal的理解?

ThreadLocal提供了线程本地变量,它可以保证访问到的变量属于当前线程;每个线程都保存有一个变量副本,每个线程的变量都不同。ThreadLocal相当于提供了一种线程隔离,将变量与线程相互绑定。

ThreadLocal适用于多线程的情况下,可以实现传递数据,实现线程隔离。

ThreadLocal提供给我们每个线程缓存局部变量

ThreadLocal基本API

1.new ThreadLocal();----创建ThreadLocal

2.set 设置当前线程绑定的局部变量

3.get 获取当前线程绑定的局部变量

4.remove() 移除当前线程绑定的变量

哪些地方有使用到ThreadLocal?

1.Spring事务模板类

2.获取HttpRequest

3.AOP调用链

ThreadLocal底层实现原理?

1.在每个线程中都有自己独立的T和read LocalMap对象,内部存放有Entry对象

2.如果当前线程对应的ThreadLocalMap对象为空的情况下,则创建该ThreadLocalMap对象,并且赋值键值对

key为当前new ThreadLocalMap对象,value就是为Object变量值

JUC并发编程面试题以及知识点_第7张图片

为什么线程缓存的是ThreadLocalMap对象?

ThreadLocalMap可以存放n多个不同的ThreadLoal对象;

每个ThreadLocal对象只能缓存一个变量值。

ThreadLocalMap threadLocalMap

ThreadLocal.get();

ThreadLocal.get(threadLocalMap);---缓存变量值

谈谈强、软、弱、虚引用直接的区别?

强引用:当内存不足时,JVM开始进行GC(垃圾回收),对于强引用对象,就算是出现了OOM也不会对该对象进行回收,死都不会回收。

软引用:当系统内存充足的时候,不会被回收;当系统内存不足的时候,它会被回收,软引用通常在对内存敏感的程序中。比如:高速缓存就会用到软引用,内存够用时就保留,不够时就回收。

弱引用:弱引用需要用到java.lang.ref.WeakReference类来实现,它比软引用的生存周期更短。对于只有弱引用的对象来说,只要有垃圾回收,不管JVM的内存空间是否充足够用,都会回收该对象占用的内存空间。

虚引用:虚引用需要java.lang.ref.Phantomreference类来实现。顾名思义,虚引用就是形同虚设。与其它集中引用不同,虚引用并不会决定对象的生命周期。

ThreadLocal为何会引发内存泄露问题?

补充概念:

什么是内存泄露问题?

内存泄露:表示就是我们程序员申请了内存,但是该内存一直无法释放;

内存泄露溢出问题:申请内存时,发现申请内存不足,就会报错 内存溢出的问题。

key为ThreadLocal作为Entry对象的key,是弱引用;当ThreadLocal指向null的时候,Entry对象中的key变为null,GC如果没有清理垃圾时,则该对象会一直无法被垃圾收集机制回收,一直占用了系统内存,就有可能会发生内存泄露的问题。

那么既然说到了这里,你是如何避免或者说如何解决ThreadLocal内存泄露的问题?

1.可以自己调用remove方法,将不要的数据移除,从而避免内存泄露的问题。

2.每次在做set方法的时候会清除之前key为null的entry对象

3.ThreadLocal采用弱引用方式

ThreadLocal为何采用的是弱引用而不是强引用?

1.如果key是为强引用:当我们现在将ThreadLocal的引用指向为null,但是每个线程中有自己独立的ThreadLocalMap还一直继续持有该对象,且我们的ThreadLocal对象不会被回收,就会发生ThreadLocal内存泄露问题。

2.如果key是为弱引用:当我们现在将ThreadLocal的引用指向为null,Entry中的key指向为null;但是下次调用set方法的时候,会根据判断--如果key为空的情况下,直接删除;这样就避免了Entry发生内存泄露的问题。

3.不管是用强引用还是弱引用,都是会发生内存泄露的问题,但是弱引用中不会发生ThreadLocal内存泄露的问题。

4.但是最终根本的原因关于ThreadLocal内存泄露的问题,产生于ThreadLocalMap与我们当前线程的生命周期一样;如果没有手动删除的情况下,就有可能会发生内存泄露的问题。

JUC相关面试题

谈谈什么是线程池?

线程池和数据库连接池非常类似,可以统一管理和维护线程,减少没必要的系统开销。

为什么要使用线程池?

因为频繁地开启或者停止线程,线程需要重新被CPU从就绪到运行状态调度,需要发生CPU的上下文切换,效率非常低。

JUC并发编程面试题以及知识点_第8张图片

你们哪些地方会使用到多线程?

实际开发项目中,禁止自己new线程。

必须使用线程池来维护和创建线程。

线程池有哪些作用?

核心点:复用机制 -- 提前创建好固定的线程一直保持在运行状态,从而实现复用,限制线程创建的数量。

1.降低资源消耗:通过池化技术重复利用自己创建的线程,降低线程创建和消耗造成的损耗。

2.提高响应速度:任务到达时无需等待线程创建即可立即执行。

3.提高了线程的可管理性:线程是稀缺资源,如果无限制创建,不仅仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以统一地分配、调优和监控。

4.提供更多更强大的功能:线程池具备可扩展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或者是定时执行。

线程池的创建方式有哪些?

Executors.newCachedThreadPool();可缓存线程池

Executors.newFixedThreadPool();可定长度 限制了最大线程数

Executors.newScheduledThreadPool();可定时线程池

Executors.newSingleThreadExecutor();单例

底层都是基于ThreadPoolExceutor构造函数封装

线程池是如何实现复用的?

本质思想:创建一个线程,不会立马停止或者销毁,而是一直实现复用。

1.提前创建好固定大小的线程,一直保持着正在运行状态;(但是这也可能会非常消耗CPU资源)

2.当需要线程执行任务,将该任务提交缓存在并发队列中;如果缓存队列满了,则会执行拒绝策略。

3.正在运行的线程从并发队列中获取任务执行,从而实现多线程复用问题。

JUC并发编程面试题以及知识点_第9张图片

线程池的核心点:复用机制 ---

1.提前创建好固定的线程一直在运行状态----死循环实现

2.提交的线程任务缓存到一个并发队列集合中,交给我们正在运行的线程执行

3.正在运行的线程就从队列中获取该任务执行

package com.mayikt;

import java.util.ArrayList;

import java.util.List;

import java.util.concurrent.BlockingDeque;

import java.util.concurrent.LinkedBlockingDeque;

/**

* 线程池核心点:复用机制 ----

* 1.提前创建好固定的线程,一直处于运行状态----死循环实现

* 2.提交的线程任务缓存到一个并发队列集合中,交给我们正在运行的线程执行

* 3.正在运行的线程就从队列中获取该任务执行

*/

public class MyExecutors {

// 线程并发队列集合

private List workThreads;

// 缓存我们线程任务

private BlockingDeque runnableDeque;

private boolean isRun = true;

/**

* @param maxThreadCount 最大线程数

*/

public MyExecutors(int maxThreadCount, int dequeSize) {

//1.限制队列容量缓存

runnableDeque = new LinkedBlockingDeque(dequeSize);

//2.提前创建好固定的线程,一直处于运行状态----死循环实现

workThreads = new ArrayList(maxThreadCount);

for (int i = 0; i < maxThreadCount; i++) {

new WorkThread().start();

}

}

class WorkThread extends Thread {

@Override

public void run() {

while (isRun || runnableDeque.size() > 0) {

Runnable runnable = runnableDeque.poll();

if (runnable != null) {

runnable.run();

}

}

}

}

public boolean execute(Runnable command) {

return runnableDeque.offer(command);

}

public static void main(String[] args) {

MyExecutors myExecutors = new MyExecutors(2, 20);

for (int i = 0; i < 10; i++) {

final int finalI = i;

myExecutors.execute(() -> System.out.println(Thread.currentThread().getName() + "," + finalI));

}

myExecutors.isRun = false;

}

}

ThreadPoolExecutor核心参数有哪些?

corePoolSize:核心线程数量--- 一直正在保持运行的线程的数量

maximumPoolSize:最大线程数,线程池允许创建的最大线程数

keepAliveTime:超出corePoolSize后创建的线程的存活时间

unit:keepAliveTime的时间单位

workQueue:任务队列,用于保存待执行的任务

threadFactory:线程池内部创建线程所用的工厂

handler:任务无法执行时的处理器

线程池创建的线程会一直保持为运行状态吗?

不会的。

例如:配置核心线程数corePoolSize为2,最大线程数maximumPoolSize为5

我们可以通过配置超出corePoolSize核心线程数后创建的线程的存活时间,例如为60s

在60s内,如果核心线程一直没有任务执行,则会停止该线程。

为什么阿里巴巴不建议使用Executors?

因为默认的Executors线程池底层是基于ThreadPoolExeutor构造函数封装的,采用无界队列存放缓存任务,这样会导致由于可无限制地缓存任务从而发生内存溢出,最终导致最大线程数失效。

JUC并发编程面试题以及知识点_第10张图片

线程池底层ThreadPoolExecutor底层实现原理?

1.当线程数小于核心线程数时,创建线程。

2.当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列

3.当线程数大于等于核心线程数,且任务队列已满时

3.1若线程数小于最大线程数,创建线程

3.2若线程数等于最大线程数,抛出异常,拒绝任务

到底能执行多少个任务:公式:任务队列容量+最大线程数=真实能执行的任务量

线程池队列满了,任务是否会丢失?

如果队列满了,且任务总数>最大线程数,则当前线程走拒绝策略。

可以自定义拒绝异常,将该任务缓存到redis、本地文件、MySQL中后期项目启动实现补偿。

1.AbortPolicy 丢弃任务,抛运行时异常

2.CallerRunsPolicy 执行任务

3.DiscardPolicy 忽视,什么都不会发生

4.DiscardOldestPolicy 从队列中剔除最先进入队列(最后一个执行)的任务

5.实现RejectedExecutionHandler接口,可自定义处理器

线程池拒绝策略类型有哪些呢?

1.AbortPolicy 丢弃任务,抛运行时异常

2.CallerRunsPolicy 执行任务

3.DiscardPolicy 忽视,什么都不会发生

4.DiscardOldestPolicy 从队列中剔除最先进入队列(最后一个执行)的任务

5.实现RejectedExecutionHandler接口,可自定义处理器

线程池如何合理配置参数?

自定义线程池就需要我们自己配置最大线程数maximumPoolSize,为了高效地并发运行,当然这个不能够随便设置。这时需要看我们的业务是IO密集型还是CPU密集型。

CPU密集型

CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟器的多线程任务都不可能得到加速,因为CPU总的运算能力就那些。

CPU密集型任务配置尽可能少的线程数量:这是为了以保证每个CPU高效地运行一个线程。

一般公式:(CPU核数+1)个 线程的线程池

IO密集型

IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO 密集型的任务会导致浪费大量的CPU运算能力在等待。所以在IO密集型任务中使用多线程可以大大地加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。

IO密集型时,大部分线程都阻塞,故需要配置多线程数:

公式:

CPU核数×2

CPU核数÷(1-阻塞系数),阻塞系数在0.8~0.9之间

查看CPU核数:

System.out.println(Runtime.getRuntime().availableProcessors());

什么是悲观锁?什么是乐观锁?

悲观锁:

1.站在MySQL的角度分析:悲观锁就是比较悲观,当多个线程对同一行数据实现修改的时候,最后只有一个线程才能修改成功,只要谁能够对获取到行锁则其他线程是不能够对该数据做任何修改操作,且是阻塞状态。

2.站在Java锁层面,如果没有获取到锁,则会阻塞等待,后期唤醒的锁的成本就会非常高,重新被我们额CPU从就绪调度为运行状态。

Lock syn锁 悲观锁没有获取到锁的线程会阻塞等待。

乐观锁:

乐观锁比较乐观,通过预期或者版本号比较。如果不一致的情况则通过循环控制修改,当前线程不会被阻塞,是乐观,效率较高,但是乐观锁比较消耗CPU的资源。

JUC并发编程面试题以及知识点_第11张图片

乐观锁:获取锁---如果没有获取到锁,当前线程是不会阻塞等待 通过死循环控制。

乐观锁属于无锁机制,美亚由竞争锁流程。

注意:MySQL的innodb引擎中存在行锁的概念!

MySQL层面如何实现乐观锁呢?

在我们表结构中,会新增一个字段,就是版本字段。

version varchar(255)DEFAULT NULL,

多个线程对同一行数据实现修改操作,提前查询当前最新的version版本号码,作为update条件查询;如果当前version版本号码发生了变化,则查询不到该数据。表示如果修改数据失败,则不断重试,又重新查询最新的版本实现update;但是需要注意控制乐观锁循环的次数,避免CPU飙高的问题。

注意:Java实现乐观锁是通过cas的方式

乐观锁实现方式

1.cas(无锁)(juc并发包框架原子类)(结合自旋),底层基于修改内存值(V),E(旧的预期值)

E==V 则表示修改v=n;

2.基于cas实现无锁机制

原理:

变量===0

0表示没有线程获取该锁

1表示已经有线程获取到该锁

cas获取锁:

cas 修改该变量=0,1==cas修改成功的话则表示获取锁成功

cas释放锁 变量1,0 cas释放锁成功

Java有哪些锁的分类呢?

1.悲观锁与乐观锁

2.公平锁与非公平锁

3.自旋锁 /重入锁

4.重量级锁与轻量级锁

5.独占锁与共享锁

6.读锁,写锁与读写锁

7.互斥锁/排它锁

公平锁与非公平锁之间的区别?

公平锁:就是比较公平,根据请求锁的顺序排列,先来请求的就先获取锁,后来获取锁就最后获取到,采用队列存放,类似于吃饭排队。

非公平锁:不是根据请求的顺序排列,通过争抢的方式获取锁。

非公平锁效率比公平锁效率要高,synchronized是非公平锁。

new ReentramtLock(true)----公平锁

new ReentramtLock(false)----非公平锁

底层是基于aqs实现

公平锁底层是如何实现的?

公平锁:就是比较公平,根据请求锁的顺序排列,先来请求的就先获取锁,后来获取锁就最后获取到 ,采用队列存放,类似于吃饭排队。

队列---底层实现方式---数组或者链表实现

独占锁与共享锁之间的区别?

独占锁:在多线程中,只允许有一个线程获取到锁,其他线程都会等待。

共享锁:多个线程可以同时持有锁,例如RenntrantLock读写锁。读读可以共享,写写互斥、读写互斥,写读互斥。

什么是锁的可重入性?

在同一个线程中,锁可以不断传递的,可以直接获取。

synchronized/lock

底层基于aqs实现。

什么是CAS(自旋锁)?以及其优缺点?

synchronized/lock

CAS

没有获取到锁的线程是不会阻塞的,通过循环控制一直不断地获取锁。

CAS:Compare and Swap,翻译“比较并交换”。执行函数CAS(V,E,N)

CAS有3个操作数,内存值V,旧的预期值E,要修改的新值N。当且仅当预期值E和内存值V相同时,将 内存值V修改成N,否则什么都不做。

JUC并发编程面试题以及知识点_第12张图片

1.CAS是通过硬件指令,保证原子性

2.Java是通过unsafe jni技术

原子类:AtomicBoolean,AtomicInteger,AtomicLong等使用CAS实现。

优点:没有获取到锁的线程,会一直在用户态,不会阻塞,没有锁的线程会一直通过循环控制重试。

缺点:通过死循环控制,消耗CPU资源比较高,需要控制循环次数,避免CPU飙高问题。

CAS本质的原理:

旧的预期值==V(共享变量中值),才会修改我们的V。

基于CAS实现机制原理。

CAS无锁机制原理:

1.定义一个锁的状态;

2.状态值=0,则表示没有线程获取到该锁;

3.状态值=1,则表示有线程已经持有该锁。

实现细节:

CAS获取锁:

将该锁的状态值从0改为1--- 能够修改成功,CAS成功则表示获取锁成功

如果获取锁失败---修改失败,则不会阻塞而是通过循环(自旋来控制重试)

CAS释放锁:

将该锁的状态从1改为0,如果能够修改成功,CAS成功则表示释放锁成功。

CAS如何避免ABA的问题?

CAS主要检查内存值V与旧的预期值E是否一致,如果一致的情况下,则修改。

这时候存在ABA的问题。

如果将原来的值A改为了B,B又改为了A,发现没有发生变化。但实际上已经发生了变化,所以存在ABA问题。

解决方法:通过版本号码,对每个变量更新的版本号做+1操作。

解决ABA问题是否生效:概念产生冲突,但是不影响结果,换一种方式,通过版本号码的方式。

JUC并发编程面试题以及知识点_第13张图片

演示ABA问题的示例代码

package com.mayikt;

import java.util.concurrent.atomic.AtomicStampedReference;

/**

* 演示ABA的问题

* (1)第一个参数expectedReference:表示预期值

* (2)第二个参数newReference;表示要更新的值

* (3)第三个参数expectedStamp:表示预期的时间戳

* (4)第四个参数newStamp:表示要更新的时间戳

*/

public class AtomicMarkableReferenceTest {

//注意:如果引用类型是Long、Integer、Short、Byte、Character一定要注意值的缓存区间!

// 比如Long、Integer、Short、Byte缓存区间是在-128~127,会直接存在常量池中,而不在这个区间内对象的值则会每次都new一个对象,

// 那么即使两个对象的值相同,CAS方法都会返回false

// 先声明初始值,修改后的值和临时的值是为了保证使用CAS方法不会因为对象不一样而返回false

private static final Integer INIT_NUM = 1000;

private static final Integer UPDATE_NUM = 100;

private static final Integer TEM_NUM = 200;

private static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(INIT_NUM, 1);

public static void main(String[] args) {

new Thread(() -> {

Integer value = (Integer) atomicStampedReference.getReference();

int stamp = atomicStampedReference.getStamp();

System.out.println(Thread.currentThread().getName() + ":当前值为:" + value + "版本号为:" + stamp);

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

//value 旧值 内存中的值 UPDATE_NUM 修改的值

if (atomicStampedReference.compareAndSet(value, UPDATE_NUM, 1, stamp + 1)) {

System.out.println(Thread.currentThread().getName() +

":当前值为:" + atomicStampedReference.getReference() +

"版本号为:" + atomicStampedReference.getStamp());

} else {

System.out.println("版本号不同,更新失败!");

}

if (atomicStampedReference.compareAndSet(value, UPDATE_NUM, 2, stamp + 1)) {

System.out.println(Thread.currentThread().getName() +

":当前值为:" + atomicStampedReference.getReference() +

"版本号为:" + atomicStampedReference.getStamp());

} else {

System.out.println("版本号不同,更新失败!");

}

}, "线程A").start();

}

}

利用原子类手写CAS无锁的示例代码

package com.mayikt;

import java.util.concurrent.atomic.AtomicLong;

import java.util.stream.IntStream;

/**

* 利用cas,手写无锁机制

*/

public class AtomicTryLock {

private AtomicLong cas = new AtomicLong(0);

private Thread lockCurrentThread;

/**

* 获取锁

* 获取锁:()

* 锁是有状态:0----表示没有人获取到该锁/持有该锁

* 1----表示该锁已经被线程持有

* 获取锁成功:cas 0 变为11 cas=true

* 获取锁失败: cas false

*

* @return

*/

public boolean tryLock() {

boolean result =cas.compareAndSet(0,1);

if (result) {

lockCurrentThread=Thread.currentThread();

}

return result;

}

/**

* 释放锁

*

* @return

*/

public boolean unLock() {

if (lockCurrentThread!=Thread.currentThread()){

return false;

}

return cas.compareAndSet(1, 0);

}

public static void main(String[] args) {

AtomicTryLock atomicTryLock = new AtomicTryLock();

IntStream.range(1,10).forEach((i)->new Thread(()->{

try {

boolean result = atomicTryLock.tryLock();

if (result) {

atomicTryLock.lockCurrentThread=Thread.currentThread();

System.out.println(Thread.currentThread().getName() + ",获取锁成功");

}else {

System.out.println(Thread.currentThread().getName() + ",获取锁失败");

}

}catch (Exception e){

}finally {

if (atomicTryLock != null) {

atomicTryLock.unLock();

}

}

}).start());

}

}

谈谈你对ThreadLocal的理解?

ThreadLocal提供了线程本地变量,它可以保证访问到的变量属于当前线程;每个线程都保存有一个变量副本,每个线程的变量都不同。ThreadLocal相当于提供了一种线程隔离,将变量与线程相互绑定。

ThreadLocal适用于多线程的情况下,可以实现传递数据,实现线程隔离。

ThreadLocal提供给我们每个线程缓存局部变量

ThreadLocal基本API

1.new ThreadLocal();----创建ThreadLocal

2.set 设置当前线程绑定的局部变量

3.get 获取当前线程绑定的局部变量

4.remove() 移除当前线程绑定的变量

哪些地方有使用到ThreadLocal?

1.Spring事务模板类

2.获取HttpRequest

3.AOP调用链

ThreadLocal底层实现原理?

1.在每个线程中都有自己独立的T和read LocalMap对象,内部存放有Entry对象

2.如果当前线程对应的ThreadLocalMap对象为空的情况下,则创建该ThreadLocalMap对象,并且赋值键值对

key为当前new ThreadLocalMap对象,value就是为Object变量值

JUC并发编程面试题以及知识点_第14张图片

为什么线程缓存的是ThreadLocalMap对象?

ThreadLocalMap可以存放n多个不同的ThreadLoal对象;

每个ThreadLocal对象只能缓存一个变量值。

ThreadLocalMap threadLocalMap

ThreadLocal.get();

ThreadLocal.get(threadLocalMap);---缓存变量值

谈谈强、软、弱、虚引用直接的区别?

强引用:当内存不足时,JVM开始进行GC(垃圾回收),对于强引用对象,就算是出现了OOM也不会对该对象进行回收,死都不会回收。

软引用:当系统内存充足的时候,不会被回收;当系统内存不足的时候,它会被回收,软引用通常在对内存敏感的程序中。比如:高速缓存就会用到软引用,内存够用时就保留,不够时就回收。

弱引用:弱引用需要用到java.lang.ref.WeakReference类来实现,它比软引用的生存周期更短。对于只有弱引用的对象来说,只要有垃圾回收,不管JVM的内存空间是否充足够用,都会回收该对象占用的内存空间。

虚引用:虚引用需要java.lang.ref.Phantomreference类来实现。顾名思义,虚引用就是形同虚设。与其它集中引用不同,虚引用并不会决定对象的生命周期。

ThreadLocal为何会引发内存泄露问题?

补充概念:

什么是内存泄露问题?

内存泄露:表示就是我们程序员申请了内存,但是该内存一直无法释放;

内存泄露溢出问题:申请内存时,发现申请内存不足,就会报错 内存溢出的问题。

key为ThreadLocal作为Entry对象的key,是弱引用;当ThreadLocal指向null的时候,Entry对象中的key变为null,GC如果没有清理垃圾时,则该对象会一直无法被垃圾收集机制回收,一直占用了系统内存,就有可能会发生内存泄露的问题。

那么既然说到了这里,你是如何避免或者说如何解决ThreadLocal内存泄露的问题?

1.可以自己调用remove方法,将不要的数据移除,从而避免内存泄露的问题。

2.每次在做set方法的时候会清除之前key为null的entry对象

3.ThreadLocal采用弱引用方式

ThreadLocal为何采用的是弱引用而不是强引用?

1.如果key是为强引用:当我们现在将ThreadLocal的引用指向为null,但是每个线程中有自己独立的ThreadLocalMap还一直继续持有该对象,且我们的ThreadLocal对象不会被回收,就会发生ThreadLocal内存泄露问题。

2.如果key是为弱引用:当我们现在将ThreadLocal的引用指向为null,Entry中的key指向为null;但是下次调用set方法的时候,会根据判断--如果key为空的情况下,直接删除;这样就避免了Entry发生内存泄露的问题。

3.不管是用强引用还是弱引用,都是会发生内存泄露的问题,但是弱引用中不会发生ThreadLocal内存泄露的问题。

4.但是最终根本的原因关于ThreadLocal内存泄露的问题,产生于ThreadLocalMap与我们当前线程的生命周期一样;如果没有手动删除的情况下,就有可能会发生内存泄露的问题。

你可能感兴趣的:(JUC并发安全,java)