进程和线程
Java中的进程和线程?
一个进程包含一个或者多个线程,运行一个mian函数其实就是运行一个线程,同时可能还有其他守护线程也在运行。而进程则是分布式服务中每个服务可以看作一个进程。
jdk1.2之后的线程其实就是操作系统的线程。
对堆、方法区还有虚拟机栈、本地方法栈和程序计数器的了解?
堆和方法区为进程所有,进程中的线程可以进行访问,堆的大小和方法区都可以在JVM中配置:
- 堆:存放对象的地方;
- 方法区:存放类信息,方法信息还有字符串常量池等静态资源。
虚拟机栈、本地方法栈和程序计数器是每个线程私有的:
- 虚拟机栈:存放该线程执行方法所需要的变量,方法的执行对应着入栈和出栈操作;
- 本地方法栈:作用和虚拟机栈差不多,区别在于,虚拟机栈执行的是Java方法,本都方法栈执行的是native(本地)方法。
- 程序计数器:存放线程的下一条命令,为在多线程执行的状态,让线程恢复之前执行的位置。
线程的生命周期?
线程有六个生命周期:
- 线程创建:线程创建出来,还没有执行start方法;
- 线程就绪状态:线程执行了strat方法,等待调用;
- 线程阻塞:线程等待锁的释放;
- 线程等待:线程等待其他线程执行了某种行为;
- 线程超时等待:线程等待其他,超时自动返回;
- 线程销毁:线程生命周期结束,销毁。
线程的sleep()方法和wait方法的区别?Thread()能直接调用run方法吗?
区别:
- 位置不同:sleep是Thread下的的静态方法,而wait本地方法是对象的Object的方法,一般配合notify和notifyall实现获取对象锁或者释放锁。
- 目的不同:sleep锁是用于线程,wait则是用于对象。
不能直接调用,线程创建后会调用start方法,进入就绪状态,分配了时间片即可开始多线程运行。如果直接调用run方法,这样相当于执行了一个普通的方法,不会创建多线程。
创建线程的几种方法?
继承Thread类,实现Run方法;
实现Runable类,实现Run()方法;
实现Callable类,实现call()方法,有返回值。
但是最多的还得是通过线程池创建了。
如何保持核心线程的存活?
核心在于 workQueue.poll 和 workQueue.take() 两个方法,且前面会判断 当前线程数,如果 当前线程数等于核心线程数时,就会调用 take 方法,让线程向队列获取任务的状态一直保持阻塞状态,通过这个方法就可以保证核心线程一直存活状态。
看下 take 与 poll 方法的区别:
take:会响应中断,会一直阻塞直到取得元素或当前线程中断。如果队列中没有数据,则线程wait释放CPU
poll:会响应中断,会阻塞,阻塞时间参照方法里参数 timeout,timeUnit,当阻塞时间到了还没取得元素会返回null
线程的死锁?如何破坏?
死锁的定义:两个以上线程互相竞争资源而导致不能执行的状态。
导致死锁的条件:
- 互斥性:共享资源是互斥的;
- 请求与保持:线程获取到部分资源后,不能执行,不能释放资源;
- 不可剥夺:线程获取到线程,除非线程自己释放,否则不可以获取;
- 循环等待,两个以上的线程循环等待资源的释放。
如何破坏:
- 破坏请求与保持:一次性请求获取所有资源;
- 破坏不可剥掉:线程获取不到全部资源,会自己释放已有的资源;
- 破坏环路等待:线程按序获取资源。
ThreadLocal
ThreadLocal的定义和作用?
ThreadLocal是线程的本地变量,用来存储独属于该线程变量。
是否使用过Threadlocal?
有的,在项目中用了jwt验证用户身份,验证完成将解析出来的用户信息存储在了Threadlocal用于后续的业务处理。
Threadlocal的原理?存在什么问题?为什么key要设置成弱引用?
Threadlocal的原理:在Thread里面有一个ThreadlocalMap,存储着ThreadLocal为key,Object为value。Threadlocal本身是不存储数据的,而是作为一个key值,查询数据;
存在什么问题:如果不及时清理,容易出现内存泄漏,因为key是用的弱引用,而value是强引用,很大可能key被回收了,value还存在。解决方案也比较简单,调用remove方法就可以,会删除key被回收了value的值。
为什么key值也要设置成弱引用:我的理解是这样的,ThreadlocalMap的生命周期是和该线程是一样长的,如果某个线程中的数据明明不需要了,但是一直也不做处理,时间久了自然也会出现内存泄露。
JMM(Java内存模型)
JMM的概念和作用?
这个就是Java内存模型,是为了屏蔽掉在不同平台上硬件和操作系统的内存访问差异,使Java程序在各个平台达到一样的并发效果。
什么是内存缓存?指令重排序?
CPU内存缓存是用户程序给内核请求数据的时候,为了提高效率有一个缓存的机制,从缓存中读取内容,但是容易产生缓存不一致的问题。
指令重排序:程序执行的的顺序和我们写的顺序不一致,而是经过重排序,这个在可以保证串行化的条件下语义一至,在并行状态却不能保证。
并发编程的三个特点?
有序性:有序性即是禁止指令重排序,多线程下重排序不能保证语义一致,通过volatile实现。
原子性:指的是操作要么全部执行完,要么全部不执行们,没有中间状态,通过syn,lock实现,或者通过CAS操作也可以。
可见性:线程(进程)对共享数据的修改,后面的数据可见,通过volatile,syn,lock实现。如果把一个变量命名为volatile,说明这个变量共享且不稳定,每次获取都跳过缓存,直接从cpu中获取。
锁
说一下乐观锁和悲观锁?
乐观锁:多线程处理同一数据的时候,乐观的认为数据不会被改变,先对数据修改后,然后再判断数据是否经过修改,如果已经有了修改,会撤销修改继续访问修改,直到成功为止。可以通过CAS实现。
悲观锁:多线程处理同一数据时候,悲观的认为数据一旦被访问就会被修改,所以在访问的时候就会提前加锁,直到其释放锁给其他线程,才能对数据修改。
如何实现乐观锁或者悲观锁?
悲观锁:比如syn,或者lock都是悲观锁;
乐观锁:可以使用版本号或者用CAS。
具体来说,就是给修改的数据加一个版本号,修改数据之前拿到版本号,修改好数据之后,对比版本号是否对应,不对应则撤销操作,并且重新操作。
CAS操作就是,有三个参数,V参数值,E预期值,N新值,只有V = E的时候,才会去根据新值更新数据。
但是CAS操作容易出现几个问题:
①最常见的就是ABA问题,举例而言就是原本数据是A,线程拿到的也是A值,但是在修改数据的过程中,原本的A值经过由A变为B又变回了A,会被误认为数据没有被修改过。解决方法就是就一个版本号。
②其次,资源开销大,通过自选锁实现,线程提交失败之后会不断重复提交,直到成功为止。
③只能对一个共享变量执行操作。
Volatile有什么作用?
这是一个关键字,用来修饰变量,保证变量的有序性和可见性。
底层原理:有序性,会有内存屏障,禁止指令重排序;
可见性:当一个变量被volatile修饰,获取这个值的时候就会不经过CPU缓存,直接从内存获取。
synchronized的作用
synchronized是什么?有什么作用?
这是一个基于JVM实现的悲观锁,作用就是在并发编程中,保证共享资源的互斥访问。
synchronized如何使用,修饰什么?
- 修饰方法,如果修饰的是静态方法,那么获取的是这个class类的锁;如果修饰的是实例方法,那么获取的是对象的锁;
- 修饰代码块,如果括号中的对象名,那么获取的是对象锁,如果括号里面的是对象名.class那么获取的是class类的锁。
synchronized底层是怎么实现的?
- 对于同步方法,会有acc_synchronized的标识,然后底层就根据标识执行对应的操作。
- 修饰同步代码,底层是在JVM层面上,有monitorenter 和 monitorexit, 代码块开始的位置和结束的位置。当执行monitorenter时候,会去获取监视器锁,如果获取到则进行后续操作,没有获取就会阻塞。当执行monitorexit时候,会判断是否持有监视器锁,如果有就释放该锁。(一般不只有一个monitorexit,因为除了正常执行完释放,还可能存在有异常时候释放)
synchronized的可见性、有序性和可重入性性是怎么实现的?
- 可见性:synchronized在修改了数据之后,加锁前,会清空缓存中对应的内容,获取数据必须得去内存中,加锁后在没执行完成之前不能从主存获取到数据,释放锁之前会把信息刷新进入缓存中,则保证了可见性。
- 有序性:被synchronized修饰的代码块,只会让一个线程执行,根据aas-if-serial语义(不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果不能被改变 ),保证了结果的有序性。
- 可重入性:在synchronized中有一个计时器,当代码获取到锁,每获取一次锁计数器+1,运行完一个代码块会计数器-1。直到清零释放锁。
jdk1.6之后做了什么优化?锁状态?
- 优化了锁的状态,引入了轻量锁,自选锁等优化了锁开销。
锁的状态:无锁->偏向锁->轻量锁->重量锁,随着竞争越激烈,锁会进化,过程不可逆。
synchronized和volatile的区别?
volatile比synchronized更加轻量,二者各有优势。
1、synchronized保证了并发编程的原子性、有序性和可见性,volatile不能保证原子性。
2、功能不同:synchronized针对方法或者代码块,而volatile则是对变量。
ReentranLock
ReetranLock是什么?
这是可重入,默认非公平的锁,功能比synchronized更强大。
锁的原理是基于AQS+CAS实现。
ReentrantLock中的公平锁和非公平锁是通过fairSycn和NofairSycn实现的,fairSycn和NofairSycn是继承AQS实现的,具体来说。在AQS里面有states变量,如果判断states为0,那么就可以获取锁,如果大于则不可以将信息存储到CLH队列中。CLH这是一个双向链表,头节点是为空的node,后面的信息插入到队尾,然后不断自旋判断前驱节点是否是头节点,是就获取锁。
(ReentrantLock内部通过FairSync和NonfairSync来实现公平锁和非公平锁。它们都是继承自AQS实现,在AQS内部通过state来标记同步状态,如果state为0,线程可以直接获取锁,如果state大于0,则线程会被封装成Node节点进入CLH队列并阻塞线程。AQS的CLH队列是一个双向的链表结构,头结点是一个空的Node节点。新来的node节点会被插入队尾并开启自旋去判断它的前驱节点是不是头结点。如果是头结点则尝试获取锁,如果不是头结点,则根据条件进行挂起操作。链接:juejin.cn/post/697543...
什么是AQS?
AQS,全称AbstractQueuedSynchronizer,是Java并发包下的基础组件,内部维护着一个FIFO(先进先出)的队列用于存储等待获取锁的线程,同时使用一个volatile修饰的states变量用于记录锁的状态。它通过CAS(Compare-and-Swap)操作来实现对状态的修改和线程的调度。AQS定义了两种资源共享方式,独占或者共享。
ReentrantLock和synchronized有什么区别?
二者都是可重入的悲观锁。
- ReentrantLock基于Java的API实现原理是AQS,synchronized是基于JVM;
- ReentrantLock是显式锁,锁的创建和释放需要手动实现,synchronized是隐式锁不需要关心。
- ReentrantLock的功能更加强大:①ReentrantLock可以实现公平锁;②ReentrantLock可以超时中断;③ReentrantLock配合Condition接口可以选择性通知。
CAS(Compare and swap)是什么?有什么问题?
CAS操作就是,有三个参数,V参数值,E预期值,N新值,只有V = E的时候,才会去根据新值更新数据。
但是CAS操作容易出现几个问题:
①最常见的就是ABA问题,举例而言就是原本数据是A,线程拿到的也是A值,但是在修改数据的过程中,原本的A值经过由A变为B又变回了A,会被误认为数据没有被修改过。解决方法就是就一个版本号。
②其次,资源开销大,通过自选锁实现,线程提交失败之后会不断重复提交,直到成功为止。
③只能对一个共享变量执行操作。
原子类操作,AtonmicIntger原理?
这是针对变量提供的原子操作,底层实现是通过CAS实现的。
线程池
线程池的概念?线程池的优势?
顾名思义,线程池就是管理一系列线程资源。
优势:
- 提高响应速度:可以很快处理任务;
- 减少资源开销:频繁的创建销毁线程,会造成资源浪费;
- 统一管理线程。
线程的执行流程?
- 当任务发来请求,如果此时线程数小于核心线程数,创建线程处理任务;
- 如果此时线程等于核心线程,那么将任务放到工作队列中;
- 如果队列已满,并且线程小于最大线程,那么创建临时线程;处理任务;
- 如果当前线程达到最大线程,并且线程没有空闲,那么根据线程池饱和策略处理。
如何创建线程?为什么不推荐使用内置线程池?
一是通过:ThreadPoolExecutor构造方法,自定义相关参数;
二是通过:通过 Executor 框架的工具类 Executors 来创建,内置线程池。
不推荐后者,因为部分参数是默认的,可能导致OOM也就是内存溢出。
线程池的七大参数?
核心线程数、最大线程数、临时线程的存活时间、临时线程的存活时间单位、工作队列、线程工厂、 饱和策略
kotlin
/**
* 用给定的初始参数创建一个新的ThreadPoolExecutor。
*/
public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
int maximumPoolSize,//线程池的最大线程数
long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
TimeUnit unit,//时间单位
BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
线程池的四种饱和策略?
- AbortPolicy:也是默认的策略,抛出异常,并且拒绝任务;
- DiscardPolicy:直接拒绝任务;
- CallerRunsPolicy:让调用的线程,直接执行任务;
- DiscardOldestPolicy:丢弃最开始未执行的任务。
线程池最常见的五种阻塞队列?
ArrayBlockingqueue:是一种有界阻塞队列,按照FIFO排序;
LinkedBlockingqueue:是一种无界阻塞队列,如果不设置大小,默认大小为Integer.Max_value;
DelayBlockingqueue:是一种无界延时阻塞队列,根据定时任务的时间,从小到大先后排序;
PriorityBlockingqueue:是一种无界优先级队列,根据任务的优先级处理任务;
SynchronousBlockingqueue:是一种不存储任务的队列,每个插入操作必须等另一个线程调用移除操作。
四种常见的线程池?
FixedThreadPool:这是线程数确定的线程池,适用于CPU密集性任务。
- 最大线程数等于核心线程数;
- 存活时间0
- 阻塞队列是LinkedBlockingqueue,可能会OOM;
SingleThread:这是只有一个线程的线程池
- 最大线程数1;
- 核心线程数据1;
- 阻塞队列LinkedBlockingqueue,可能会导致OOM;
- 存活时间0
CacheThreadPool:缓存线程池,适用于高并发任务多,但是任务量小的场景。
- 最大线程数Integer.Max_value;
- 核心线程数0;
- 阻塞队列是SynchronsBlockingqueue;
- 线程存活时间是 :60s;
ScheduledThreadPool:处理定时任务的线程,适用于处理定时和周期性任务。
- 最大线程数据为Integer.MAX.VALUE,也有OOM的风险;
- 阻塞队列是DelayWorkQueue;
- 存活时间为0;
如何设计线程池的大小?
判断是CPU密集型任务还是I/O密集型任务,
如果是CPU密集型任务,线程数据设置为n+1;如果是I/O密集型任务,设置为2n。
线程池调优需要注意什么?
事前评估->监控警告->动态调整->事后观察