1.线程和进程和协程的区别
- 进程是操作系统中资源分配和调度的基本单位,是程序的一次执行过程,因此是动态的,即一个进程从创建到运行再到消亡。每个进程都有独立的内存空间,一位置一个进程的变量修改不会影响到其他经常。进程之间的同学通常需要通过操作系统提供的机制,如管道、消息队列和共享内存等。进程之间切换的开销较大,因为进程之间是完全隔离的,每次切换都需要保存当前进程的上下文并加载新进程的上下文。
- 线程是一个比进程更小的执行单元,是CPU调度和分派的基本单位。一个进程的过程可以产生多个线程,这些线程共享同一进程的资源,如堆和方法区资源。线程之间的通信和数据共享要比进程间容易得多。线程切换的开销比进程小,因为它们共享相同的内存上下文,但多个线程同时访问共享资源时,需要同步机制来防止数据竞争和不一致的情况,比如使用锁或者信号量。
- 协程是一个比线程更轻量级的执行单元,它在用户空间内调度和管理。协程的控制权转移由协程自身决定,而不是由操作系统调度器决定。这就意味着协程可以在任何地方暂停和恢复执行,协程之间的切换开销远低于线程。所以协程非常适合用于I/O密集型的任务,因为它们可以在等待I/O操作完成时主动放弃控制权,然后在完成后恢复执行,这样可以避免线程在等待I/O时的阻塞状态,提高系统的并发性能。
进程 | 线程 | 协程 | |
---|---|---|---|
定义 | 操作系统资源分配和调度的基本单位 | 进程内执行单元,CPU 调度和分派的基本单位 | 用户空间内的轻量级执行单元 |
内存 | 拥有独立的内存空间 | 共享所属进程的内存空间 | 共享所属进程和线程的内存空间 |
开销 | 创建和切换成本高 | 创建和切换成本较低 | 创建和切换成本基地 |
通信 | 需要通过进程间进行通信 | 可以直接访问共享数据 | 可以直接访问共享数据 |
并发能力 | 较低,受限于系统调度和资源限制 | 较高,受限于系统调度和资源限制 | 极高,几乎无限制,受限于系统资源 |
调度 | 由操作系统调度 | 由操作系统调度 | 由用户空间调度 |
隔离性 | 最强,完全隔离 | 较弱,共享内存和资源 | 最弱,共享内存和资源 |
使用场景 | 高隔离需求,多核CPU利用 | 高效资源共享,多任务处理 | I/O密集型任务,异步编程 |
2.堆和方法区的区别
堆和方法区都是所有线程共享的资源堆是进程中最大的一块内存,主要存放用户新创建的对象。方法区主要用于存放已被加载的类信息、常量、静态变量等。
3.并发和并行的区别
在多核CPU下
- 并发:两个及其以上的作业在同一时间段执行
- 并行:两个及其以上的作业在同一时刻执行
4.同步和异步的区别
同步:发出一个调用后,在没有得到结果之前,会一直等待
异步:发出一个调用后,不用等待结果,该调用直接返回
在项目中,例如注册完成后会给邮箱发一条注册成功的信息,假设注册完成需要50ms,而发一条注册成功的信息到邮箱需要50ms,如果是同步则一共需要50ms,而异步的话,就只需要50ms,因为只需要注册完成就行了,发送信息由消息队列来异步发送。
5.多线程相关问题
- 为什么使用多线程:为了减少线程上下文切换的开销,现在的系统要求百万甚至千万级并发量,利用多线程机制可以大大提高系统的并发能力和性能。
- 多线程带来的问题:内存泄漏、死锁、线程不安全(对同一份数据进行访问,是否能够保证数据的正确性和一致性)
- 单核CPU运行多个线程效率一定会高吗?不会,要看是CPU密集型还是IP密集型,如果是CPU密集型,多个线程同时运行会导致频繁的线程切换,会影响效率,而IO密集型会有很多线程来提高效率。
6.创建线程的方法有哪些?
- 继承Tread类重写run方法
- 实现Runnable接口重写run方法
- 实现Callable接口,重写call方法,通过FutureTask类来传入到Thread中。与Runnable的区别
- call() 方法有返回值(String类型),而run方法没有
- call() 方法可以抛异常,而run方法不行
- callable接口有泛型,而Runnable没有
- 使用线程池
7.什么是线程的上下文切换
是指线程在占用CPU状态中退出,从运行态->其他状态(就绪态或者阻塞态,例如调用sleep和wait往返主动让出,或者是时间片用完,以及线程被阻塞等),需要保存当前线程的上下文,线程下次占用CPU的时候进行恢复现场。
8.什么是死锁,怎么避免死锁?
- 死锁的定义:一个线程或多个线程在等待某个资源被释放,多个线程同时阻塞。
- 产生死锁的四个条件:
- 互斥条件:该资源任意时刻只被一个线程占用
- 请求与保持条件:一个线程因请求资源而被阻塞时,对已获得的资源保持不放
- 不剥夺条件:只要该线程在使用资源,其他线程不能剥夺
- 循环等待条件:若干个线程之间形成一种头尾相接的等待资源
- 预防死锁:破坏请求与保持条件(一次性申请所有资源),破坏剥夺条件(占用部分资源的线程申请资源,如果申请不到则释放该资源),破坏循环等待条件(按某一顺序申请资源, 反序释放资源)
- 避免死锁:在资源分配时,接祖算法对资源进行评估(银行家算法),使得进入安全状态。
死锁诊断:
- 通过jdk自带工具
- jps:输出JVM运行的进程信息(可以查看进程id)
- jstack:查看Java进程内线程的堆栈信息(jstack -l 进行id)
- jdk自带可视化工具,通过jdk安装的bin目录
- jconsole:可以通过提供的日志信息检查死锁
- VisualVM:能够监控线程,内存情况,查看方法的CPU时间和内存中的对象,已被GC的对象,反向查看分配的堆栈。
9.线程包括哪些状态,状态之间是怎么变换的
- NEW(新建):当一个线程对象被创建当尚未启动时,他处于新建状态,此时线程还未调用start()方法
- RUNNABLE(可运行):当线程调用了start()方法后,线程将进入可运行状态。而在这个状态下有两个子状态(就绪态和运行态),当时间片用完时就会回到就绪队列里,以便继续执行。如果线程的run()方法已经完成执行,那么线程也将回到非活动状态,最终会被垃圾回收器回收。
- BLOCKED(阻塞)或WAITING(等待):当线程因为某些原因不能继续执行时,它会进入阻塞或等待状态。这可能是因为:
- synchronized阻塞,被另外一个线程只有锁
- 调用了wait、join或sleep方法,使得线程主动放弃CPU并等待特定条件发生
- I/O操作或其他系统调用导致的阻塞
- TIMED-WAITING(定时等待):当线程调用了一个带时间限制的等待方法时的状态,例如Thread.sleep(300)或者Object.wait(300)。线程在指定时间内完成。
- TERMINTED(终止):当线程的run()方法正常完成执行,或者线程由于未捕获的异常而提前结束时,就会进入终止状态。
状态之间的转换如图所示。
追问:sleep和wait方法有什么不同- 都是使线程暂停,进入等待
- sleep没有释放锁,而wait释放了锁(其他线程可以获得该对象锁)
- wait方法常用于线程之间的交互,而sleep方法用于暂停线程执行
- wait方法被调用后不会自动苏醒,需要通过其他对象调用notify或notifyAll方法将其唤醒
- sleep是Thread的静态方法,wait是Object的本地方法
- sleep是让当前线程停止执行,不涉及对象类,也不需要获得对象锁。
10.可以直接调用Thread的run方法吗
- new Thread方法线程进入新建态,调用start方法线程进入就绪态,分配到时间片就可以运行。start会执行线程的相应准备工作,然后执行run方法的内容
- 而直接调用run方法会把run方法当做main线程下的普通方法执行,并不是多线程工作。
- run方法封装了要被线程执行的代码,可以被调用多次,而start方法用来启动线程,通过该线程调用run方法执行run方法中的逻辑,start方法只能被调用一次。
11.乐观锁和悲观锁是什么
- 悲观锁:假设总是会发生最坏的情况,共享资源每次被访问都会出现问题,所以每次获取资源的操作都加了锁,直到该线程用完这个资源后才会释放锁,其他线程才有机会拿到这个资源。Java中的synchronized和ReentrantLock可以实现悲观锁。在高并发的情况下,如果使用悲观锁,大量线程阻塞会导致上下文切换。
- 乐观锁:假设总是会发生最好的情况,共享资源每次被访问都不会出现问题,无需加锁,也不要等待,线程一直执行。只是提交资源时要去验证该资源是否被其他资源修改。
- 乐观锁多用于多读写少的场景
- 悲观锁多用于多写少读的场景
12.如何实现乐观锁
- 版本号机制:在表中加一个版本号字段用int修饰,当线程去修改该表数据后,会去判断当前版本号和数据库里的版本号是否相同,如果相同则更新,否则重试更新操作,直到更新成功。然后版本号+1
- CAS算法(compare and swap):CAS算法是用一个预期值和要更新的值进行比较,相等才更新,在无锁的情况下保证线程操作的原子性。CAS算法是一个原子操作,依赖于一条CPU的原子指令。Unsafe类提供了compareAndSwapObject(Int,Long)方法来实现Object,Int和Long类型的CAS操作。
- 场景:AQS框架,AtomicXXX类
- 在操作共享变量的时候使用自旋锁,效率上更高一些
13.CAS算法存在哪些问题
- 但CAS算法存在场景的ABA问题,有可能将V值从A改为B再改回A,然后与E值对比相同,认为没有修改,则更新。解决ABA问题在变量前面加上版本号或者时间戳。使用AtomicStampedReference类,其中的compareAndSet方法会检查当前引用是否等于预期引用,当前标识是否等于预期标识。
- 循环时间长开销大:CAS经常会用到自旋操作来重试,不成功就一直循环执行下去直到成功,这样会给CPU带来很大的执行开销。JVM提供的pause指令会提升效率。
- 只能保证一个共享变量的原子操作,CAS算法只对单个变量有用,如果有多个变量,可以封装到compareAndSwapObject里,可以利用AtomicReference类把多个共享变量合成一个共享变量来操作。
14.解释一下volatile关键字
- volatile 修饰的变量,该变量是共享的但是不稳定,每次使用都要在主存中进行读取
- volatile关键字可以保证数据的可见性(线程间是否可见,指的是第一个线程对该数据进行修改后,第二个线程看不到第一个线程修改的数据,而使用了原来的数据),但不能保证数据的原子性,而synchronized关键字都能保证。
- 防止指令重排序,被volatile修饰的变量在进行读写操作时,会通过插入特定的内存屏障来禁止指令重排序。volatile使用技巧:
- 写变量让volatile修饰的变量的在代码最后位置
- 读变量让volatile修饰的变量在代码最开始位置
- 使用场景:单例模式的成员属性需要用volatile修饰,禁止指令重排序,在new一个对象的时候。有三部分组成,第一为该对象分配内存空间,第二初始化该对象,第三将该对象指向分配的内存地址。如果不禁止重排序,执行顺序可能会是1->3->2,这样在单线程下不会有问题,如果在多线程下,T1先执行1和3,T2调用实例化方法后发现这个对象不为空,直接返回,但没被初始化。
单例模式下的双重检查锁定:
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
15.解释一下synchronized关键字
- 主要解决多线程之间访问共享资源的同步性,被synchronized关键字修饰的方法和代码块在任意时刻只能被一个线程执行。
- 修饰静态方法(Class锁)、修饰实例方法(对象锁)、修饰代码块(Class锁、对象锁),构造方法不能用synchronized修饰,因为本身就是安全的。
- synchronized底层是用的一个monitorenter和monitorexit指令,保证锁在同步代码块代码正常运行,以及出现异常都能被正常释放。jdk1.6之后,synchronized引入了自旋锁、适应性自旋锁等。
- Monitor实现的锁属于重量级锁,你了解过锁升级吗?
锁升级策略的目的是尽量避免使用重量级锁,因为重量级锁的性能开销相对较大。通过先使用偏向锁和轻量级锁,只有在确实需要的情况下才升级到重量级锁,可以显著提高多线程程序的执行效率。然而,锁升级的具体实现细节依赖于JVM的版本和具体的实现,不同版本的JVM可能有不同的锁升级策略。
16 synchronized和volatile关键字的区别?
- volatile是线程同步的轻量级实现,其性能要比synchronized高,volatile只能用于变量,而synchronized关键字可以用于代码块和方法
- volatile保证数据的可见性,不保证原子性,而synchronized都能保证
- volatile解决变量在多个线程之间的可见性,synchronized解决多个线程访问资源的同步性。
17 什么是ReentrantLock锁?
- ReentrantLock实现Lock接口,是一个可冲入独占式的锁,和synchronized关键字类似。ReentrantLock更灵活、更强大,增加了轮询,超时,中断等。ReentrantLock里有一个内部类Sync,继承了AQS(抽象队列同步器),添加锁和释放锁都在Sync内部类里完成。Sync有公平锁和非公平锁两个子类
- 公平锁:锁被释放后,先申请的线程先得到,性能要差一些,上下文切换更加频繁。
- 非公平锁:锁被释放后,会随机或者按照一些优先级排序来申请线程,但可能会导致一些线程永远无法获取到锁。
- AQS基本工作机制
- 作为基础框架使用的,是一种锁机制(多线程的队列同步器)
- AQS内部维护一个先进先出的双向队列,队列中存储的排队线程
- 在AQS内部还有一个state属性,相当于一个资源,默认为0(无锁状态),如果队列中的一个线程修改成功了,state为1
- 语法
ReentrantLock lock = new ReentrantLock(); try{lock.lock();//获取锁} finally{lock.unlock() //释放锁}
- 原理:CAS+AQS队列实现,支持非公平锁和公平锁。
18 ReentrantLock和synchronized有什么区别
- 都是可重入锁,指线程可以再次获取到自己的内部锁。可重入锁是指,一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其还想获取这个对象锁时是可以获取的。
- synchronized有依赖于JVM(monitor监视器中的monitorenter和monitorexit指令),而ReentrantLock依赖于API
- ReentrantLock比synchronized增加了一些高级功能
- 等待可中断,获取锁的过程可以被中断,可以通过Lock.lockInterruptibly()来实现,而synchronized是不可中断锁。
- 可实现公平锁(需要传入参数,默认为非公平锁),ReentrantLock可以通过公平锁子类继承Sync内部类来实现公平锁,synchronized只能是非公平锁。
- 支持多个条件变量
- 在竞争激烈时,ReentrantLock能够提供更好的性能,而非竞争时,ReentrantLock的性能也可以,因为做了一些偏向锁和轻量级锁的优化。
- synchronized与AQS的区别
synchronized | AQS |
---|---|
关键字,C++实现 | Java实现,非关键字 |
悲观锁,自动释放锁 | 悲观锁,手动开启和关闭 |
锁竞争激烈都是重量级锁,性能差 | 锁竞争激烈的情况下,提供了多种解决方案 |
19 你对线程池了解多少,以及一些相关问题
- 线程池:
- 线程池管理一系列线程的资源池,当有任务要处理时,直接从线程池中获取线程来处理,处理完后线程并不会理解被摧毁,而是等待下一个任务。
- 线程池的好处,降低资源的损耗,提高响应速度,提高线程的可管理性。
- 两种实现方式,ThreadPoolExecutor和Executors。
- ThreadPoolExecutor有哪些参数
- 核心线程数(corePoolSize)
- 最大线程数(maximumPoolSize):也就是核心线程+救急线程的最大数目
- 时间单位(unit):救急线程的生存时间单位,如秒,毫秒等
- 工作队列(workQueue):当没有空闲的核心线程时,新来的任务会加入到此队列排队,队列满会创建救急线程执行任务
- 线程工厂(threadFactory):可以定制线程对象的创建,例如设置线程名字、是否是守护线程等。
- 拒绝策略(handler):当所有线程都在繁忙,workQueue也放满时,会出发拒绝策略。
- 线程池执行流程
- 线程中有哪些阻塞队列
- ArrayBlockingQueue:基于数据结构的有界阻塞队列,FIFO
- LinkedBlockingQueue(使用最多):基于链表结构的有界阻塞队列,FIFO,默认容量是Integer的最大值
- 还有两种不常用的阻塞队列,DelayedWorkQueue和SynchronousQueue
- 两者的区别:
- 如何确定核心线程数(N表示CPU核数)
- IO密集型任务:文件读写,DB读写,网络请求等。核心线程数为2*N + 1
- CPU密集型任务:计算型代码、BitMap转换等。核心线程数为N + 1
- 高并发、任务执行时间短 -> (N + 1),减少线程上下文的切换
- 高并发、任务务执行时间长,解决这种类型的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存时第一步,增加服务器是第二步,再考虑线程池的设置。
- 线程种类有哪些?
- java.util.concurrent.Executors类提供了大量创建连接池的静态方法,常见的就有四种
- 创建使用固定线程数的线程池 通过ThreadPoolExecutor(五个参数),没有救急线程(适用场景:任务量已知,相对耗时的任务)
- 单线程化的线程池:核心线程数和最大线程数都是1,阻塞队列为LinkedBlockingQueue,最大容量为Integer.MAX_VALUE(适用场景:按照顺序执行的任务)
- 可缓存线程池:没有核心线程数,最大线程数为Integer.MAX_VALUE,阻塞队列为SynchronousQueue:不对出元素的阻塞队列,每个插入操作都必须等待一个移出操作。(使用场景:任务数比较密集,但每个任务执行时间较短的情况)
- 提供了“延迟”和“周期执行”的线程池
- 为什么不建议用Executors创建线程池,而是使用ThreadPoolExecutor的方式
- 阿里巴巴开发手册-嵩山版(强制要求):线程池不允许使用Executors创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让开发人员更加明确线程池的运行规则,规避资源耗尽的风险。
- FixedThreadPool和SingleThreadPool:运行的请求队列长度为int的最大值,可能会堆积大量的请求,从而导致OOM(内存溢出)
- CacheThreadPool:运行创建线程数量为int的最大值,可能会创建大量的线程,从而导致OOM
- 线程池的使用
- CountDownLatch(封闭/倒计时锁)用来进行线程同步协作,等待所有线程完成倒计时(一个或多个线程,等待其他多个线程完成某件事之后才能执行):
- 其中构造参数用来初始化等待计数值
- await() 用来等待计数归零
- countDown() 用来让计数减一
- 使用场景一:(es数据批量导入)
- 使用场景二:Future,在实际的开发中,会调用多个接口来汇总数据,所有接口没有依赖关系,就可以通过线程池+future来提升性能。(数据汇总,报表汇总),注:在智慧教育平台中,我们在实现优惠卷算法时,由于优惠的方案众多,我们先用排列组合筛选出满足的优惠卷组合,然后用for循环计算出所有的优惠卷组合计算出的金额。这样使用for循环计算效率不高,我们可以利用多线程并行计算,for循环将每个方案交给一个线程去任务执行。
- 使用场景三:异步调用,为了避免下一级方法影响上一级方法(性能考虑),可以使用异步线程调用下一个方法(不需要下一级方法返回值),可以提升方法相应时间。(文章搜索与历史记录)
- 如何控制某个方法允许并发访问线程数量
- Semaphore信号量,是JUC包下的工具类,步骤
- 创建信号量,给一个容量
- 请求信号量,信号量个数-1,如果个数变为负数,则请求会阻塞
- 释放信号量,信号量个数+1
20 ThreadLocal的原理
- 定义:为每一个线程分配一个独立的线程副本,从而解决变量并发访问冲突问题。ThreadLocal实现了线程内的资源共享。(例如登录后的用户,将这个用户ID存到ThreadLocal里,以后可以直接使用这个用户ID,不用去判断当前是哪个ID在使用)【本质上就是一个线程内部存储类,让多个线程只能操作自己内部的值,从而实现多线程数据隔离】
- 原理:每个线程持有一个ThreadLocalMap的成员变量,用于存储资源对象
- 调用set方法,以ThreadLocal作为key,资源对象作为value,放入当前线程的ThreadLocalMap集合中
- 调用get方法,以ThreadLocal作为key,获取到当前线程的资源值
- 调用remove方法,以ThreadLocal作为key,移出当前线程关联的资源值
- ThreadLocal内存泄漏问题
- 强引用:一个对象处理有用且必须得状态,GC不会回收它,即使内存不足,会出现OOM也不会回收。
- 弱引用:一个对象处于可能有用且不必须的状态,在GC线程扫描内存区域发现弱引用时,就会回收该对象。
- 其中key为弱引用,而value是强引用,内存中如果内存不够,GC会进行回收,而value是强引用不会被回收,就会存在内存泄漏。
- 解决办法:当使用ThreadLocal时,先使用remove方法,将(key和value)清除
21 JMM(Java内存模型)
- Java内存模型定义了共享内存中多线程程序读写操作的规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性。
- JMM把内存分成了两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)
线程与线程之间是相互隔离,线程与线程交互需要通过主内存。
22 其他
- 如何保证T1、T2、T3三个线程按顺序进行
- 解决:可以用join方法,当重写T2的run方法时,加一个T1.join。当重写T3的run方法时,加一个t2.join。
- 如何停止一个正在运行的线程?
- 使用退出标志,例如while(flag),然后如果使用完该线程后将flag=false
- 使用stop强制停止(不推荐)
- 使用interrupt方法中断线程
- 导致并发程序出现问题的根本原因是什么?
- 原子性:一个线程在CPU中操作不能暂停、中断。要么全执行,要么都不执行。
- 解决方案:使用synchronized或者ReentrantLock
- 内存可见性:让一个线程对共享变量的修改对另外一个线程可见
- 解决方案:加一个volatile关键字修饰,当然锁也可以实现,但是性能很低。
- 有序性:指令重排序,有可能程序中的代码执行的先后顺序不同
- 解决方案:加一个volatile关键字修饰,volatile是静止指令重排序的。