并发编程面试问题

更多问题参考:

Java多线程面试题通关手册:https://mp.weixin.qq.com/s/w8KTx1K8RpcZl_In-CxIMQ

 

问题答案
线程与进程的区别?

进程是操作系统分配资源的最小单元,线程是CPU调度和分派的最小单元。

一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行

进程有独立的地址空间,线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间

守护线程

Java分为两种线程:用户线程和守护线程

是个服务线程,准确地来说就是服务其他的线程(比如垃圾回收线程)

 

守护线程的退出:如果其他的线程(即用户自定义线程)都执行完毕,连main线程也执行完毕,那么jvm就会退出,此时会杀死会杀死进程中的所有守护线程

Thread对象的setDaemon(true) 设置守护线程。

线程的状态

参考:

https://blog.csdn.net/hla199106/article/details/47840505

 

Thread 类中的 sleep、join、yield、interrupt

object 类中的 wait、notify、notifyAll

Condition 接口

参考:

https://blog.csdn.net/hla199106/article/details/47840505

https://www.cnblogs.com/dolphin0520/p/3920385.html

sleep:相当于让线程进入阻塞状态,不会释放锁

 

join:线程A中调用另一个线程B的join方法,线程A将会等待线程B执行完毕后再执行。调用了Object的wait方法,让线程进入阻塞状态,会释放锁

 

yield:并不会让线程进入阻塞状态,而是让线程重回就绪状态,不会释放锁,让出CPU执行权给同等级的线程,如果没有相同级别的线程在等待CPU的执行权,则该线程继续执行。

 

interrupt:可以用来中断一个正处于阻塞状态的线程;直接调用interrupt方法不能中断正在运行中的线程。

 

wait()、notify()和notifyAll()方法是本地方法,并且为final方法,无法被重写。必须在同步块或者同步方法中进行。

wait:让当前线程阻塞,并且当前线程必须拥有此对象的monitor,会释放锁 (选择哪个线程取决于操作系统对多线程管理的实现)

notify:能够唤醒一个正在等待这个对象的monitor的线程

notifyAll:能够唤醒所有正在等待这个对象的monitor的线程

 

sleep和wait区别:来自不同的类;sleep没有释放锁,wait释放锁;wait,notify只能用于同步方法,sleep任何地方使用;sleep必须捕获异常,wait不需要

 

wait,notify 为什么放在Object类中? 1.每个对象都可以成为锁,不仅仅是Thread对象 2.线程可以拥有多个对象锁,不知道操作的对象锁是哪个

 

Condition是个接口,基本的方法就是await()和signal()方法;

  • Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition()

  • 调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用

  Conditon中的await()对应Object的wait();

  Condition中的signal()对应Object的notify();

  Condition中的signalAll()对应Object的notifyAll()。

线程中断

Java的中断是一种协作机制

参考:

https://blog.csdn.net/xinxiaoyong100440105

/article/details/80931705

Thread类中断方法

void interrupt():并不一定就立马中断了正在运行的线程,它只是要求线程自己在合适的时机中断自己。每个线程都有一个boolean的中断状态(这个状态不在Thread的属性上),interrupt方法仅仅只是将该状态置为true。

bool interrupted() : 测试当前线程是否已经中断,并清除中断状态

bool isInterrupted() 测试当前线程是否已经中断,不设置中断状态

 

一个方法声明抛出InterruptedException,表示该方法是可中断的,比如wait,sleep,join,中断方法会对interrupt调用做出响应(sleep响应interrupt的操作包括清除中断状态,抛出InterruptedException)

Object.wait, Thread.sleep方法,会不断的轮询监听 interrupted 标志位,发现其设置为true后,会停止阻塞并抛出 InterruptedException异常

不可中断的操作:包括进入synchronized段以及Lock.lock(),inputSteam.read()等,调用interrupt()对于这几个问题无效,因为它们都不抛出中断异常。如果拿不到资源,它们会无限期阻塞下去。

可被中断的加锁操作:Lock.lockInterruptibly() 抛出中断异常

CAS 机制

参考来源:

https://blog.csdn.net/bjweimengshu

/article/details/79000506

Compare And Swap的缩写,翻译过来就是比较并替换,在无锁的情况下实现原子操作。

3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。利用unsafe提供了原子性操作方法(硬件命令)。

 

CAS存在的问题?

1.ABA问题,利用版本号比较可以有效解决ABA问题 AtomicStampedReference 已解决

2.循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。

3.只能保证一个共享变量的原子操作。AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

Atomic包

Atomic的包名为java.util.concurrent.atomic。这个包里面提供了一组原子变量的操作类,这些类可以保证在多线程环境下,当某个线程在执行atomic的方法时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个线程执行 。

LongAdder(只能做自增、自减)

使用分段CAS以及自动分段迁移

参考:

 

代码思路:每一步都优先考虑代价相对小的操作

(1) 在开始并发比较低的时候先考虑在base上累加,有竞争失败才去创建cells

(2) 如果有了cells之后,先考虑在对应的cell上进行cas操作,有竞争失败才会进入自旋

(3)内部实现了自动分段迁移的机制,也就是如果某个Cell的value执行CAS失败了,那么就会自动去找另外一个Cell分段内的value值进行CAS操作

要从LongAdder中获取当前累加的总值,就会把base值和所有Cell分段数值加起来返回给你

 

AtomicLong可不可以不要 ? incrementAndGet()有返回值 而 increment() 无返回值,需要再进行一次求和

为什么用Cell数组而不是long数组?扩容的时候,long是栈中存储,可能会发生数据丢失的问题

synchronized

(非公平锁)

synchronized关键字经过编译后,会在同步块的前后分别形成monitorentermonitorexit这两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。

1.synchronized方法

一般方法 对象锁、

static方法 类锁(与对象锁不互斥)

2.synchronized代码块

对象锁

对于synchronized方法或者synchronized代码块,当出现异常时,JVM会自动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象。 缺点:无法响应中断、无法实现读写锁

锁对象的对象头里面有一个 threadid 字段

再次进去,如果是自己则自己使用,否则锁升级为轻量级锁

3 锁升级

锁对象的对象头里面有一个 threadid 字段(1标识偏向锁,0表示无锁。00时表示轻量级锁,10表示重量级锁,10是GC标记)

第一次进去设置为自己id 持有偏向锁

再次进去,如果是自己则自己使用,否则锁升级为轻量级锁

通过自旋一定次数获取锁,没获取到,升级为重量级锁

Lock

声明了四个方法来获取锁

 

使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。

ReentrantLock 如果锁具备可重入性,则称作为可重入锁。可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配

ReentrantReadWriteLock 读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁

Lock和synchronized的选择

Lock是一个接口,而synchronized是Java中的关键字

synchronized在发生异常时自动释放锁,Lock自己处理释放锁

Lock可响应线程中断 lockInterruptibly(),synchronized 不行

Lock可以知道有没有成功获取锁,而synchronized却无法办到

最重要 读写锁、可以提高多个线程进行读操作的效率,synchronized不行

ThreadLocal

参考:

https://mp.weixin.qq.com/s/c0eNbNXJAR7CMRBfotf4kw

线程局部变量。变量是同一个,但是每个线程都使用同一个初始值。

就是每个线程都维护了一个map,而这个map的key就是threadLocal,而值就是我们set的那个值,每次线程在get的时候,都从自己的变量中取值,既然从自己的变量中取值,那肯定就不存在线程安全问题,总体来讲,ThreadLocal这个变量的状态根本没有发生变化,他仅仅是充当一个key的角色,另外提供给每一个线程一个初始值。

 

ThreadLocalMap key是弱引用,容易被回收;回收后 键值为null,value值不回收;ThreadLocalMap 做了一些额外的回收工作。

ThreadLocal导致的内存泄露: 核心线程池中线程使用ThreadLocal

ThreadLocal应用: 实现数据库连接Connection对象保存上下文用户信息、

为什么不直接用线程id来作为ThreadLocalMap的key?

直接用线程id来作为ThreadLocalMap的key,无法区分放入ThreadLocalMap中的多个value

volatile关键字及其应用

ava并发编程---volatile

 

不了解这些“高级货”,活该你面试当炮灰。。。

 

大白话聊聊Java并发面试问题之volatile到底是什么?

 
创建线程有哪几种方式?
  1. 继承Thread类 重写run方法

  2. 实现Runnable接口 重写run方法

  3. Callable与Future 创建线程 (Callable接口里面有call方法,有返回值及会抛异常)

Callable、Future和FutureTask

参考:

https://www.cnblogs.com/dolphin0520/p/3949310.html

Future提供了三种功能:

  1)判断任务是否完成;

  2)能够中断任务;

  3)能够获取任务执行结果。

简述Executor框架

参考:

https://blog.csdn.net/tongdanping

/article/details/79604637

Executor框架包括3大部分:

(1)任务。也就是工作单元,包括被执行任务需要实现的接口:Runnable接口或者Callable接口

(2)任务的执行。也就是把任务分派给多个线程的执行机制,包括Executor接口及继承自Executor接口ExecutorService接口

(3)异步计算的结果。包括Future接口及实现了Future接口的FutureTask类。

execute() 没有返回结果

submit() 有返回结果,但实际调用execute()

 

线程池运行原理

参考:

https://www.cnblogs.com/dolphin0520

/p/3932921.html

ThreadPoolExecutor

  • 如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;

  • 如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;

  • 如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理;

  • 如果线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止

线程池状态

RUNNING 当创建线程池后,初始时,线程池处于RUNNING状态;

 

SHUTDOWN 调用了shutdown()方法,此时线程池不能够接受新的任务,它会等待所有任务执行完毕

 

STOP 调用了shutdownNow() 此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务

 

TERMINATED(结束) 当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后

线程池的分类

Executors类中提供的几个静态方法来创建线程池:

 

Executors.newCachedThreadPool(); //将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用的SynchronousQueue(一个不存储元素的阻塞队,插入后必须等待删除,否则会处于阻塞),也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。

 

Executors.newSingleThreadExecutor(); //创建容量为1的缓冲池,corePoolSize和maximumPoolSize都设置为1,也使用的LinkedBlockingQueue

 

Executors.newFixedThreadPool(int); //创建固定容量大小的缓冲池,线程池corePoolSize和maximumPoolSize值是相等的,它使用的LinkedBlockingQueue

 

Executors.newSingleThreadScheduledExecutor() 创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求

任务拒绝策略

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常默认

 

ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。

 

ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)

 

ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

CountDownLatch

CyclicBarrier

Semaphore(信号量)

CountDownLatch(int count) 等待其他count个任务执行完毕之后才能执

await() throws InterruptedException // 挂起

countDown() // count 减1

 

 1)CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:

    CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;

    而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;

    另外,CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。

  2)Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限。

 

Semaphore(int permits) //permits表示许可数目,即同时可以允许多少线程进行访问

acquire() throws InterruptedException // 获取锁 acquire(int permits) 获得多个

release() 释放锁

BlockingQueue

 

ReentrantLock // 可重入锁

Condition

boolean add(E e); // 加元素,超过最大值报错

boolean offer(E e); // 加元素 e的值不能为空,否则抛出空指针异常

void put(E e) throws InterruptedException; // 加元素,没有多余的空间,该方法会一直阻塞

E take() throws InterruptedException; // 取元素,如果队列中没有值,线程会一直阻塞

poll(long timeout, TimeUnit unit) throws InterruptedException; // 取元素,在给定的时间里,从队列中获取值,时间到了直接调用普通的poll方法

 

分类:

ArrayBlockingQueue:是一个用数组实现的有界阻塞队列 ,支持公平锁和非公平锁

LinkedBlockingQueue:一个由链表结构组成的有界队列,此队列的长度为Integer.MAX_VALUE

PriorityBlockingQueue: 一个支持线程优先级排序的无界队列,默认自然序进行排序

DelayQueue: 一个实现PriorityBlockingQueue实现延迟获取的无界队列,在创建元素时,可以指定多久才能从队列中获取当前元素。只有延时期满后才能从队列中获取元素。

DelayQueue

无界、延迟、阻塞队列

 

支持延时获取元素的无界阻塞队列

BlockingQueue+PriorityQueue(堆排序)+Delayed

DelayQueue中存放的对象需要实现compareTo()方法和getDelay()方法,必须实现Delayed接口

getDelay方法返回该元素距离失效还剩余的时间,当<=0时元素就失效了

取数据:获取对重第一个元素 无数据:线程阻塞 有数据:过期时间没到,线程阻塞 有数据:过期时间到了,取数据,唤醒所有等待线程

存数据:堆排序存数据,如果存的数据在堆顶,则唤醒一个等待线程

Timer、TimerTask

参考:

https://www.cnblogs.com/lingiu/p/3782813.html

Timer是jdk中提供的一个定时器工具,使用的时候会在主线程之外起一个单独的线程执行指定的计划任务,可以指定执行一次或者反复执行多次。

TimerTask是一个实现了Runnable接口的抽象类,代表一个可以被Timer执行的任务。

Timer 里面有 TaskQueue(存TimerTask);TimerThread 线程

如何终止Timer线程 调用timer的cancle方法;当所有任务执行结束后,删除对应timer对象的引用,线程也会被终止;调用System.exit方法终止程序

几个缺陷:

  • 多任务在单线程里执行,一个任务结束,另一个任务才能开始,时间间隔不准;

  • 出现异常会导致全部任务停止;

  • 绝对时间,受系统时间影响。

happens-before规则(八大原则)

先与什么发生,没有任何时间上的含义

参考:

https://www.jianshu.com/p/1508eedba54d

单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。

锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。

volatile的happen-before原则:对一个volatile变量的写操作happen-before对此变量的任意操作(当然也包括写操作了)。

happen-before的传递性原则:如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。

线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。

线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。

线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测。

对象创建的happen-before原则:一个对象的初始化完成先于他的finalize方法调用

AQS (AbstractQueuedSynchronizer)

参考:

https://mp.weixin.qq.com/s/Os8tT1kBqsq0vteB6KD4hg

 

AQS内部核心变量

 

用来实现各种锁,各种同步组件的。它包含了state变量、加锁线程、等待队列等并发中的核心组件

int state 初始化为0,加锁的状态

关键变量 记录当前加锁的是哪个线程

AQS实现原理

  1. 线程1 调用ReentrantLock的lock() 加锁 CAS操作将state值从0变为1,设置当前加锁线程为自己

  2. 可重入锁 线程再次加锁,会判断一下当前加锁线程是否是自己,是 state加1

  3. 线程2 尝试加锁,CAS将state从0变到1,失败! 将自己放入AQS中的一个等待队列

  4. 线程1 执行完逻辑,释放锁,将state 递减1,如果state值为0,则彻底释放锁,会将“加锁线程”变量也设置为null

  5. 线程1从等待队列的队头唤醒线程2重新尝试加锁

  6. 线程2尝试CAS加锁,如果成功,出队列并把“加锁线程”设置为线程2

AQS实现公平锁与非公平锁

如上步骤6,线程2尝试CAS加锁,如果此时线程3也加锁

非公平锁:线程3直接获取锁成功,不一定说先来排队的线程就就先会得到机会加锁,而是出现各种线程随意抢占的情况。

公平锁:AQS的队列里真的有线程排着队,线程3也得排队

RateLimiter
  • SmoothRateLimiter 稳定模式,令牌生成速度恒定

  • SmoothWarmingUp 渐进模式,令牌生成速度缓慢提升直到维持在一个稳定值

// 当前桶里存储的令牌数量 double storedPermits;

// 桶可存储最大令牌数量 double maxPermits;

// 生成一个令牌的间隔 微秒级别 double stableIntervalMicros;

// 下一个请求允许获取到令牌的微秒数 private long nextFreeTicketMicros = 0L

  
  
  
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值