面试问题-并发篇

线程和进程的区别?

二者对比

进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)

并行和并发有什么区别?

单核CPU

单核CPU下线程实际还是串行执行的

操作系统中有一个组件叫做任务调度器,将cpu的时间片(windows下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于cpu在线程间(时间片很短)的切换非常快,人类感觉是同时运行的。

总结为一句话就是:微观串行,宏观并行

一般会将这种线程轮流使用CPU的做法称为并发(concurrent)

现在都是多核CPU,在多核CPU下

并发是同一时间应对多件事情的能力,多个线程轮流使用一个或多个CPU

并行是同一时间动手做多件事情的能力,4核CPU同时执行4个线程

创建线程的方式有哪些?

继承Thread类

实现runnable接口

实现Callable接口

线程池创建线程(项目中使用方式)

runnable 和 callable 有什么区别?

1.Runnable 接口run方法没有返回值

2.Callable接口call方法有返回值,需要FutureTask获取结果

3.Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛

run()和 start()有什么区别?

start():用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次。

run():封装了要被线程执行的代码,可以被调用多次。

线程包括哪些状态?

新建(NEW)、可运行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)时间等待(TIMED_WALTING)、终止(TERMINATED)

线程状态之间是如何变化的

创建线程对象是新建状态

调用了start()方法转变为可执行状态

线程获取到了CPU的执行权,执行结束是终止状态

在可执行状态的过程中,如果没有获取CPU的执行权,可能会切换其他状态

  1. 如果没有获取锁(synchronized或lock)进入阻塞状态,获得锁再切换为可执行状态
  2. 如果线程调用了wait()方法进入等待状态,其他线程调用notify()唤醒后可切换为可执行状态
  3. 如果线程调用了sleep(50)方法,进入计时等待状态,到时间后可切换为可执行状态

新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?

可以使用线程中的join方法解决

notify()和 notifyAll()有什么区别?

notifyAll: 唤醒所有wait的线程

notify: 只随机唤醒一个 wait 线程

在java中wait和sleep方法的不同?

共同点

wait(),wait(long)和 sleep(long)的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态

不同点

1. 方法归属不同

sleep(long)是 Thread 的静态方法

而 wait(),wait(long)都是 Object 的成员方法,每个对象都有

2. 醒来时机不同

执行 sleep(long)和 wait(long)的线程都会在等待相应毫秒后醒来

wait(long)和 wait() 还可以被 notify 唤醒,wait() 如果不唤醒就一直等下去

它们都可以被打断唤醒

3. 锁特性不同(重点)

wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制

wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃cpu,但你们还可以用)

而 sleep 如果在 synchronized 代码块中执行,料不会释放对象锁(我放弃 cpu,你们也用不了)

如何停止一个正在运行的线程?

有三种方式可以停止线程

  1. 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止使用2.
  2. stop方法强行终止(不推荐,方法已作废)
  3. 使用interrupt方法中断线程

打断阻塞的线程(sleep,wait,join)的线程,线程会抛出InterruptedException异常打断正常的线程,可以根据打断状态来标记是否退出线程

synchronized关键字的底层原理?

Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】

它的底层由monitor实现的,monitor是jvm级别的对象(C++实现)线程获得锁需要使用对象(锁)关联monitor

在monitor内部有三个属性,分别是owner、entrylist、waitset其中owner是关联的获得锁的线程,并且只能关联一个线程;entrylist关联的是处于阻塞状态的线程;waitset关联的是处于Waiting状态的线程

Monitor 被翻译为监视器,是由jvm提供,c++语言实现

Owner:存储当前获取锁的线程的,只能有一个线程可以获取

EntryList:关联没有抢到锁的线程,处于Blocked状态的线程

WaitSet:关联调用了wait方法的线程,处于Waiting状态的线程

Monitor实现的锁属于重量级锁,你了解过锁升级吗?

Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。

重量级锁:底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高性能比较低。

轻量级锁:线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性

偏向锁:一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令

你谈谈 JMM(Java内存模型)?

JMM(Java Memory Model)Java内存模型,定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性

JMM把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)

线程跟线程之间是相互隔离,线程跟线程交互需要通过主内存

CAS 你知道吗?

CAS的全称是: Compare And Swap(比较再交换);它体现的一种乐观锁的思想在无锁状态下保证线程操作数据的原子性。

CAS使用到的地方很多:AQS框架、AtomicXXX类

在操作共享变量的时候使用的自旋锁,效率上更高一些

CAS的底层是调用的Unsafe类中的方法,都是操作系统提供的,其他语言实现

乐观锁和悲观锁的区别

CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。

synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。

请谈谈你对 volatile 的理解?

问题分析:主要是因为在JVM虚拟机中有一个JIT(即时编译器)给代码做了优化。

解决方案一:在程序运行的时候加入vm参数-Xint表示禁用即时编译器,不推荐,得不偿失(其他程序还要使用)

解决方案二:在修饰stop变量的时候加上volatile,当前告诉jit,不要对 volatile 修饰的变量做优化

①保证线程间的可见性

用 volatile 修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见

② 禁止进行指令重排序

指令重排:用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障阻止其他读写操作越过屏障,从而达到阻止重排序的效果

什么是AQS?

是多线程中的队列同步器。是一种锁机制,它是做为一个基础框架使用的像ReentrantLock、Semaphore都是基于AQS实现的AQS内部维护了一个先进先出的双向队列,队列中存储的排队的线程在AQS内部还有一个属性state,这个state就相当于是一个资源,默认是0(无锁状态),如果队列中的有一个线程修改成功了state为1,则当前线程就相等于获取了资源

在对state修改的时候使用的cas操作,保证多个线程修改的情况下原子性

ReentrantLock的实现原理?

ReentrantLock表示支持重新进入的锁,调用lock方法获取了锁之后,再次调用 lock,是不会再阻塞

ReentrantLock主要利用CAS+AQS队列来实现

支持公平锁和非公平锁,在提供的构造器的中无参默认是非公平锁,也可以传参设置为公平锁

synchronized和Lock有什么区别 ?

语法层面

synchronized 是关键字,源码在jvm 中,用 c++ 语言实现Lock 是接口,源码由jdk 提供,用java 语言实现使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁

功能层面

二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能Lock 提供了许多 synchronized 不具备的功能,例如公平锁、可打断、可超时、多条件变量Lock 有适合不同场景的实现,如 ReentrantLock,ReentrantReadWriteLock(读写锁)

性能层面

在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖在竞争激烈时,Lock 的实现通常会提供更好的性能

死锁产生的条件是什么?

一个线程需要同时获取多把锁,这时就容易发生死锁

如何进行死锁诊断?

当程序出现了死锁现象,我们可以使用jdk自带的工具:jps和jstackjps:输出JVM中运行的进程状态信息

jstack:查看java进程内线程的堆栈信息,查看日志,检查是否有死锁如果有死锁现象,需要查看具体代码分析后,可修复可视化工具jconsole、VisualVM也可以检查死锁问题

聊-下ConcurrentHashMap?

底层数据结构

JDK1.7底层采用分段的数组+链表实现

JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树

加锁的方式

JDK1.7采用Segment分段锁,底层使用的是ReentrantLock

JDK1.8采用CAS添加新节点,采用synchronized锁定链表或红黑二叉树的首节点,相对Segment分段锁粒度更细,性能更好

导致并发程序出现问题的根本原因是什么?

  1. 原子性synchronized、lock
  2. 内存可见性 volatile、synchronized、lock
  3. 有序性volatile

说一下线程池的核心参数?

corePoolSize 核心线程数目

maximumPoolSize 最大线程数目=(核心线程+救急线程的最大数目)

keepAliveTime 生存时间-救急线程的生存时间,生存时间内没有新任务,此线程资源会释放

unit 时间单位-救急线程的生存时间单位,如秒、毫秒等

workQueue-当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务

threadFactory 线程工厂-可以定制线程对象的创建,例如设置线程名字、是否是守护线程等

handler 拒绝策略-当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略

线程池的执行原理知道吗?

提交任务-->核心线程是否已满-->阻塞队列是否已满-->线程数是否小于最大线程数-->拒绝策略处理

如果核心或临时线程执行完成任务后会检查阻塞队列中是否有需要执行的线程,如果有,则使用非核心线程执行任务

1.AbortPolicy:直接抛出异常,默认策略;

2.CallerRunsPolicy:用调用者所在的线程来执行任务;

3.DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务

4.DiscardPolicy:直接丢弃任务;

线程池中有哪些常见的阻塞队列?

workQueue-当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务

  1. ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO。
  2. LinkedBlockingQueue:基于链表结构的有界阻塞队列,FIFO。
  3. DelayedWorkQueue:是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的
  4. SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。

如何确定核心线程数?

① 高并发、任务执行时间短 →(CPU核数+1),减少线程上下文的切换

② 并发不高、任务执行时间长

I0密集型的任务 →(CPU核数*2+1)

计算密集型任务 →(CPU核数f1)

③ 并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置

线程池的种类有哪些?

newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待

newsingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO)执行

newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程

newScheduledThreadPool:可以执行延迟任务的线程池,支持定时及周期性任务执行

了解CountDownLatch吗?

CountDownLatch(闭锁/倒计时锁)用来进行线程同步协作,等待所有线程完成倒计时(一个或者多个线程,等待其他多个线程完成某件事情之后才能执行)

其中构造参数用来初始化等待计数值

await()用来等待计数归零

countDown()用来让计数减一

你们项目哪里用到了多线程?

批量导入:使用了线程池+CountDownLatch批量把数据库中的数据导入到了ES(任意)中,避免OOM

数据汇总:调用多个接口来汇总数据,如果所有接口(或部分接口)的没有依赖关系,就可以使用线程池+future来提升性能

异步线程(线程池):为了避免下一级方法影响上一级方法(性能考虑),可使用异步线程调用下一个方法(不需要下一级方法返回值),可以提升方法响应时间

如何控制某个方法允许并发访问线程的数量?

在多线程中提供了一个工具类Semaphore,信号量。在并发的情况下,可以控制方法的访问量

  1. 创建Semaphore对象,可以给一个容量
  2. acquire()可以请求一个信号量,这时候的信号量个数-1
  3. release()释放一个信号量,此时信号量个数+1

谈谈你对ThreadLocal的理解?

1. ThreadLocal可以实现【资源对象】的线程隔离,让每个线程各用各的【资源对象】,避免争用引发的线程安全问题

2. ThreadLocal 同时实现了线程内的资源共享

3. 每个线程内有一个 ThreadLocalMap 类型的成员变量,用来存储资源对象

a)调用set 方法,就是以 ThreadLocal 自己作为 key,资源对象作为 value,放入当前线程的 ThreadLocalMap 集合中

b)调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中查找关联的资源值

c)调用 remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值

ThreadLocal内存泄漏问题

ThreadLocalMap 中的 key是弱引用,值为强引用;key会被GC释放内存,关联 value的内存并不会释放。建议主动remove 释放 key,value

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值