【并发专题探索总结与常见面试题总汇】

文章目录

一、并发学习总结

1.1、为什么要使用并发编程

充分的利用CPU计算能力,将多核的CPU计算能力发挥到极致,性能也能得到提升,而且针对访问量较高的业务,例如百万级或千万级的并发量,多线程并发编程正是解决的基础

1.2、使用并发会产生一些什么常见问题

在使用并发编程时,虽然能在一定程度上提高性能,但是也是会产生一些潜在的问题:例如内存泄漏、上下文切换、死锁、并发数据的安全性等诸多问题

1.3、使用并发产生的问题解决方案

1.3.1、线程安全问题与解决方案底层实现机制剖析总结

1.3.1.1、什么是线程安全问题

线程安全是指:多线程操作同一个变量,这个操作(例如:累加操作)产生的结果是不确定的,有可能是正确的,有可能是错误的,这种情况我们就称当前的操作是线程不安全的

1.3.1.2、如何解决
1.3.1.2.1、使用CAS

我们可以使用CAS(Compare And Swap)方式确保其操作是线程安全的,过程与原理如下介绍:

1.3.1.2.1.1、CAS操作过程详解与底层实现机制

我们可以来看一张图:
在这里插入图片描述
由上图可知:多线程场景下,使用CAS机制进行值更改时,线程内部会维护三个值,分别是:要更改的数据、更改数据的地址个更改地址期望的原始值,在更改数据时,会用当前持有的这个期望值和地址当前值进行比较,如果相同就替换值,如果不相同,就不做操作,重新读取地址中的值,以自旋的方式进入下一个轮回重试

1.3.1.2.2、加锁ReentrantLock

我们也可以通过加锁方式保证线程安全,就相当于我们给资源设置了一道门,需要访问资源的话,就先获取开门的钥匙,而这把钥匙,想相当于这里的锁,如下介绍:

1.3.1.2.2.1、深入理解AQS与ReentrantLock底层实现机制

在了解AQS之前,我们可以先来看下管程模型图MESA:
在这里插入图片描述
而AQS其实就是将诸多的同步器持有的共同部分抽取出来,例如:线程阻塞队列、条件队列、获取公平/非公平锁、可重入锁等行为抽取出来,从而形成一个同步框架==>Abstract Queue Synchronized,其结构与上图的MESA是类似的

ReentrantLock在加锁时,默认是非公平锁,主要体现在lock源码中,首次获取锁时,是先不考虑是否有线程在排队等待,是先直接尝试获取锁资源,如下图:
在这里插入图片描述

但是如果是公平锁的话,获取锁时,就不会有这一步操作,如下图:
在这里插入图片描述
而且ReentrantLock底层支持可重入锁,具体底层实现是,有一个获取锁的标记字段,如果当前对象同时获取多把锁,就会进行累加,源码如下:
在这里插入图片描述
总体来说,ReentrantLock加锁的过程,我们可以查看下图:
在这里插入图片描述

1.3.1.2.3、使用线程本地变量ThreadLocal

线程本地变量TreadLock是每个线程内部独有的,主要是用于跨方法进行数据参数的,其内部结构如下图示:
在这里插入图片描述
其每个线程中都是维护了一个ThreadLocalMap集合,这个集合中存储了Entry数组,每一个Entry对象中是以TreadLocal对象实例为key,存储的值的为value进行存储数据,但是Entry的这个key指向的ThreadLocal对象是一个弱引用对象,如果在线程栈中的ThreadLocal出栈了,那发生GC后,ThreadLocal对象实例就会被回收了,那此时Entry指向的ThreadLocal对象就为null了,没发访问数据,线程如果不结束的话(例如线程池),就会存在内存泄漏问题,所以,在方法使用完毕后,我们是需要执行一个remove方法进行清除对应的Entry对象信息

1.3.1.2.4、线程封闭

也就是将对象变量信息封存在一个线程中,其他线程对当前这个对象变量是不可见且不可用的,如下图示:
在这里插入图片描述

1.3.1.2.5、栈封闭

这个是在JVM运行时数据区的栈中就体现,每个线程都会独有一个线程栈,其栈内部以栈帧方式存储局部变量与方法等信息,保证当前的线程栈中的方法与变量指在当前线程栈中存储于使用,可以确保线程安全

1.3.1.2.6、让类不可变

我们可以让类被final修饰,被这个关键字修饰后,其就引用就不能发生变化,但是需要注意的是,如果当前类中不是无状态的类,或者所有的属性都加入final修饰,否则也有可能会存在线程安全问题

1.3.1.2.7、使用无状态的类

无状态的类主要是指,当前类中是无任何属性和相关行为的,这种类一定是线程安全的类

1.4、并发内部方法详解

1.4.1、并发的等待与通知机制详解

也就是wait()和notify/notifyAll,等待通知机制,其规则如下示例:
等待方:

  1. 获取对象的锁
  2. 如果条件不满足,那么调用对象的wait()方法,被通知后也需要校验条件是否满足
  3. 如果条件满足,就执行相应逻辑业务代码
synchronizd(对象实例){
	while(条件不满足){
		对象.wait()}
	逻辑业务代码
}

唤醒方:

  1. 获得对象的锁
  2. 改变条件
  3. 通知所有等待在对象上的线程
synchronizd(对象实例){
   改变条件
   对象.notifyAll()
}

需要注意的是:在调用wait()、notify()系列方法之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait()方法、notify()系列方法,原因可详见常见面试题中的介绍

1.5、线程池详解

1.5.1、线程池内部参数详解

线程池中主要有以下参数信息:

  1. 核心线程数:这个是核心工作的线程,当任务来时,核心线程优先处理
  2. 最大线程数:线程池的一个最大线程设置参数,包含核心线程数,最大能创建出多少个线程
  3. 线程工厂:主要是用来生产线程的
  4. 最大存活时间与时间单位设置:线程处理完任务后,会处于一个空闲状态,那如果设置这个参数,且设置生效的话,空闲时间到达预设值后,线程就会被干掉
  5. 任务队列:这个是一个阻塞队列(BlockQueue),主要是用于存放待处理的任务数据的
  6. 解决策略:这个是在线程池饱和状态下,接受不了新任务给出的一种相应,主要有:1、不做任何处理;2、抛出异常信息;3、让主线程负责运行;4、在任务队列中,丢弃最早进入队列中的任务,让当前任务进入任务队列中去;
    在这里插入图片描述

1.5.2、线程池执行流程

当有任务进入线程池中后,线程池先判断当前的线程数是否大于核心线程数,若不大于,直接新建核心线程处理任务,如果是大于,就需要进行任务队列是否满判断,如果未满,加入队列,否则再次判断其线程数是否大于最大线程数,不大于情况就新建工作线程处理任务,反之会执行解决策略,具体流程可见下图示:
在这里插入图片描述

1.5.3、线程池状态

线程池中,主要有以下几种状态:

  1. Runing:表示当前线程池处于工作状态=>正常接收任务和处理任务
  2. shutDown:表示当前线程池即将关闭,不接受新任务,但是会把在执行的和任务队列中的任务执行完毕后才会关闭
  3. stop:相比shutDown,是直接将线程池关闭回收,例如调用shutDownNow方法,会中断正在执行任务的线程,且不会执行任务队列中任务
  4. Tidying:所有的线程都会终止回收,线程池中没有线程,这种情况就会把线程池状态改为:Tidying
  5. Terminated:执行terminated方法后,线程池状态就会变为当前这个Terminated状态

二、并发专题常见面试题

一、基础篇

1.1、为什么要使用并发?有什么缺点?

参考答案:使用并发编程,会在一定程度上提高程序的性能,支持在高并发场景下的业务,让多核的CPU计算能力得到最大利用化,虽然使用并发会带来诸多好处,但其也是一把双刃剑,也会存在些潜在问题,例如:内存泄漏、上下文切换、死锁、数据并发安全性等问题

1.2、并发的三要数是什么?在Java程序中,如何保证多线程运行安全的?

参考答案:并发三要素主要是指:原子性、有序性、可见性,在Java程序中,我们可以通过加锁(Synchronized、ReentrantLock)、CAS等方式对存在线程安全问题的业务代码块进行保护

1.3、什么是多线程?多线程的优缺点是什么?

参考答案:多线程是指程序中包含多个执行流,即在一个程序中可以同时开启多个不同的线程来执行不同的任务,这种多线程方式给我带来了诸多好处,例如,可以提高CPU的利用率,还能在一定程度上提高程序的运行效率,与此同时也会存在一些问题,例如:线程也是程序,需要占用内存资源,线程越多占用内存资源就会越多,而且线程需要协调和管理,所以需要CPU塞纳跟踪线程,还要解决线程共享资源的问题等

1.4、什么是上下文切换?守护线程与用户线程有什么区别?

参考答案:在多线程编程中,线程数一般都是大于CPU核心数的,但是一个CPU核心在任意时刻就只能被一个线程使用,那为了让这些线程都能得到有效的执行,CPU采取的策略是为每个线程分发时间片并轮转的形式,当有个线程时间片用完后,就会重新处于就绪状态,同时将CPU的资源让给其他线程使用,那这个过程就属于一次上下文切换;
用户线程主要是指=>运行在前台,执行具体任务的线程,例如,程序的主线程、连接网络的线程等,而守护线程是指=>运行在后台的线程,为用户线程做服务支持的;

1.5、如何在Windows和Linux下查找那个线程CPU利用率最高?

参考答案:无论是在Windows环境下还是Linux下,都可以通过top命令来查看CPU飙高的线程信息,首先,我们先利用top找出消耗CPU较高的进程pid,根据上述的pid使用命令top -H -p pid,然后按shift+p,查看CPU利用率最高的线程号,将其线程号装换为16进制,使用Jstack pid可查看线程具体飙高信息

1.6、什么是线程死锁?为什么会产生死锁?如何解决?

参考答案:死锁是指多个线程在争夺多个资源的过程中,一个线程A持有部分资源,但是想要往下执行,就需要另一个线程B持有的资源,与此同时,线程B在现持有的资源基础上,想要往下执行的条件是需要线程A持有的资源,由此导致双方互相长时间等待,若无外力介入的情况下,处于一个僵局状态,这个情况我们就称为死锁
解决死锁==>我们首先是先要明白产生死锁的前提条件是什么,只需要打破其条件即可,简单总结产生死锁原因如下:
1、多个线程争夺多个资源
2、争夺资源的是无序的
3、且各线程不会释放持有的资源数据
那解决死锁问题,我们就可以从产生死锁的必要条件出发,打破其中一个即可,例如:我们可以让多个线程在抢夺资源时,变得有序,必须要拿到资源X,才能有资格获取资源Y,再例如:如果当前线程去抢夺资源,很遗憾没抢着,那我就把自己持有的资源一并松开手

1.7、线程创建的方式有哪些?

参考答案:从Thread源码类上来说,创建线程的方式有两种,分别为衍生Thread类和实现Runable接口,具体如下:
在这里插入图片描述
当然还有callable接口,但是实现callable接口,最终会将其对象封装为一个Runable接口,交由Thread类进行运行,还有一个线程池方式,这种当时更多是偏于池化技术、资源复用,所以,创建线程方式按照源码来说只有两种方式

当然,本质上只有一种,就是new Thread类,通过调用Tread类的start方法开启线程

1.8、线程的生命周期是什么?

参考答案:线程的生命周期主要有:线程的创建、可运行、运行、阻塞、死亡,其中阻塞分为等待阻塞和抢占锁的阻塞,如下图示:
在这里插入图片描述

1.9、Java中用到的线程调度算法是什么?调度策略又是什么?

参考答案:主要有两种线程调度模型,分别是:分时调用与抢占式调度
分时调度模型是指让所有的线程轮流获得 cpu 的使用权,平均分配每个线程占用的 CPU 的时间片
在Java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU

线程调度策略是选择优先级最高的线程运行,但是,如果发生以下情况,就会终止线程的运行:
(1)线程体中调用了 yield 方法让出了对 cpu 的占用权利
(2)线程体中调用了 sleep 方法使线程进入睡眠状态
(3)线程由于 IO 操作受到阻塞
(4)另外一个更高优先级线程出现
(5)在支持时间片的系统中,该线程的时间片用完

1.10、什么是线程调度器和时间分片?

参考答案:线程调度器是一个操作系统服务,它负责为 Runnable 状态的线程分配 CPU 时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。
时间分片是指将可用的 CPU 时间分配给可用的 Runnable 线程的过程。分配CPU 时间可以基于线程优先级或者线程等待的时间。
线程调度并不受到 Java 虚拟机控制,所以由应用程序来控制它是更好的选择

1.11、线程同步与线程调度的相关方法有哪些?

参考答案:
(1) wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
(2)sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常;
(3)notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关;
(4)notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;

1.12、sleep()和wait()有什么区别?

参考答案:首先是方法所属的类不同,wait方法是属于Object类的,而sleep方法是属于Thread的,wait方式会释放锁资源,sleep不会,而且wait通常是用于线程之间的交互通信的sleep主要是用于暂停执行,而且wait被调用后,需要使用notify或者notifyAll唤醒,而sleep方法执行后,线程会自动苏醒

1.13、wait()是使用if块还是while循环?介绍下原因

参考答案:是在while条件中,原因是等待的线程可能会收到错误的警报或伪唤醒,如果不在循环中检查等待条件,程序就很有可能没有满足条件的情况下退出

1.14、为什么线程通信的方法wait()、notify()、notifyAll()被定义在Object类中?

参考答案:因为任何对象都可以作为锁,并且可使用等待与唤醒方法去线程等待或唤醒线程

1.15、为什么线程通信的方法wait()、notify()、notifyAll()必须要放在同步方法或者同步块中?

参考答案:当一个线程需要调用对象的 wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的notify()方法。同样的,当一个线程需要调用对象的 notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。

1.16、在Thread类中、yield()方法有什么用?为什么Thread类中的sleep()和yield()方法都是静态的?两者有什么区别?

参考答案:yield()方法是讲当前线程持有的CPU资源释放,处于一个就绪状态,Thread类中的yield()和sleep()方法都是在当前正在执行的线程上运行的,所以在其他处于等待状态的线程上调用这两个方法是没有意义的,所以设置为静态的,两者的区别主要有:sleep方法给其他线程运行机会时,不会考虑线程的优先级,而yield()方法只会给相同优先级的线程或者比自己优先级更高的线程运行的机会,而且sleep方法执行后,当前线程是处于阻塞状态,而yield()方法执行完后,当前线程处于就绪状态,当然sleep会抛出线程中断异常,而yield()不会

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

参考答案:我们可以使用线程类中stop方法(不过是过时的方法),更多是使用中断机制==>interrupt

1.18、Java中interrupted和isInterrupted方法有什么区别?

参考答案:首先我们先了解interrupt方法,主要是去中断当前正在运行的线程,中断的方式为,更改其标识:flag为true,interrupted方法和isInterrupted方法都是去查看这个标识的值,是否被修改过,不同点在于,interrupted方法查看完值后会将这个值改为false,而isInterrupted不会

1.19、什么是阻塞式方法?

参考答案:阻塞式方法是指程序会一直等待该方法完成期间不做其他事情,ServerSocket的accept()方法就是一直等待客户端连接。这里的阻塞是指调用结果返回之前,当前线程会被挂起,直到得到结果之后才会返回。此外,还有异步和非阻塞式方法在任务完成前就返回

1.20、两个线程之间是如何共享数据的?

参考答案:两个线程间共享变量即可实现数据共享

1.21、Java是如何实现多线程之间的通讯与协作的?

参考答案:Java中线程通信协作的最常见的两种方式:
1、syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll()
2、ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll()

1.22、什么是线程同步与线程互斥?有哪些实现方式?

参考答案:线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步
线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。内核模式下的方法有:事件,信号量,互斥量
实现线程同步的方法
同步代码方法:sychronized 关键字修饰的方法
同步代码块:sychronized 关键字修饰的代码块
使用特殊变量域volatile实现线程同步:volatile关键字为域变量的访问提供了一种免锁机制
使用重入锁实现线程同步:reentrantlock类是可冲入、互斥、实现了lock接口的锁他与sychronized方法具有相同的基本行为和语义

1.23、在监听器(Monitor)内部,是如何做线程同步的?程序应该做哪种级别的同步?

参考答案:一旦方法或者代码块被 synchronized 修饰,那么对象与Monitor部分就放入了监视器的监视区域,确保一次只能有一个线程执行该部分的代码,线程在获取锁之前不允许执行该部分的代码,另外 java 还提供了显式监视器( Lock )和隐式监视器( synchronized )两种锁方案

1.24、什么叫线程安全?servlet是线程安全的吗?

参考答案:线程安全是指在多线程场景下,操作同一个资源时,产生的结果有可能是错误的,有可能是正确的,这种现象我们称为线程安全问题,Servlet 不是线程安全的,servlet 是单实例多线程的,当多个线程同时访问同一个方法,是不能保证共享变量的线程安全性的

1.25、线程类的构造方法、静态块是被那个线程调用的?

参考答案:线程类的构造方法、静态块是被
new这个线程类所在的线程所调用的,而 run 方法里面的代码才是被线程自身所调用的。
例如:假设 Thread2 中 new 了Thread1,main 函数中 new 了 Thread2,那么:
(1)Thread2 的构造方法、静态块是 main 线程调用的,Thread2 的 run()方法是Thread2 自己调用的
(2)Thread1 的构造方法、静态块是 Thread2 调用的,Thread1 的 run()方法是Thread1 自己调用的

1.26、Java中如何获取线程的dump文件?又如何获取线程堆栈信息的?

参考答案:
在 Linux 下:你可以通过命令 kill -3 pid (Java 进程的进程 ID)来获取 Java应用的 dump 文件。
在 Windows 下:你可以按下 Ctrl + Break 来获取。这样 JVM 就会将线程的dump 文件打印到标准输出或错误文件中,它可能打印在控制台或者日志文件中,具体位置依赖应用的配置。

1.27、一个线程发生异常会是怎样的?

参考答案:首先会抛出UncaughtExceptionHandler异常信息,可捕获处理,当然,如果是线程池的话,当前异常的线程也因此会被回收,同时也会替补一个新的线程进入线程池中

1.28、Java中的线程数过多会造成什么异常?

参考答案:1、线程的生命周期开销会非常大;2、消耗过多的CPU资源;3、降低JVM的稳定性==>有可能会OOM异常

1.29、什么是原子操作类?在Java中有哪些原子操作类?原理是什么?

参考答案:

二、并发理论篇

2.1、为什么代码会重排序?

参考答案:初衷是为了提供性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件:
1、在单线程环境下不能改变程序运行的结果;
2、存在数据依赖关系的不允许重排序

需要注意的是:重排序不会影响单线程环境的执行结果,但是会破坏多线程的执行语义

2.2、synchronized有什么作用?在项目中如何使用的?底层实现原理是什么?

参考答案:Synchronized关键字是用来控制线程同步的,能保证线程安全;
在项目中:
1、Synchronized关键字可修饰实例方法:用当前对象实例加锁,进入同步代码块需要先获取当前锁
2、Synchronized关键字可修饰静态方法:就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)
3、Synchronized关键字可修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁

在执行同步代码块之前之后都有一个monitor字样,其中前面的是monitorenter,后面的是离开monitorexit,不难想象一个线程也执行同步代码块,首先要获取锁,而获取锁的过程就是monitorenter ,在执行完代码块之后,要释放锁,释放锁就是执行monitorexit指令

2.3、什么是自旋?

参考答案:例如CAS中,在多线程情况下,每次的操作只有一个线程操作成功,那其他线程就不断在循环重试,这个过程我们就称之为:自旋

2.4、多线程下的synchronized锁升级的原理是什么?

参考答案:在锁对象头中有一个ThreadID字段,在第一次访问的时候,ThreadID为空,JVM会让其持有一把偏向锁,把ThreadID设置为当前线程ID,如果下一次获取锁时,先判断ThreadID与当前锁对象头的ThreadID是否一致,一致就让其持有使用,否则就会将偏向锁升级为轻量级锁,类似的操作经过一定次数循环获取锁后,如果还没有正常获取锁资源,就会升级为重量级锁,这个锁升级的过程目的是为了降低锁带来的性能消耗问题

2.5、线程B是怎么知道线程A修改了变量?

参考答案:1、使用volatile关键字;2、加锁Synchronized或者ReentrantLock;3、使用线程的等待通知机制

2.6、当一个线程进入一个对象的synchronized修饰的方式A后,其他线程是否能进入此对象的synchronized修饰的方法B?

参考答案:不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。因为非静态方法上的 synchronized 修饰符要求执行方法时要获得对象的锁,如果已经进入A 方法说明对象锁已经被取走,那么试图进入 B 方法的线程就只能在等锁池(注意不是等待池哦)中等待对象的锁

2.7、synchronized、volatile、CAS之间的比较?

参考答案:
1、Synchronized:悲观锁、属于抢占式,会引起其他线程阻塞
2、volatile:提供多线程共享变量可见性和禁止指令重排序优化
3、CAS:是基于冲突检测的乐观锁,不会引起线程阻塞

2.8、synchronized和lock、ReentrantLock的区别是什么?

参考答案:
1、Synchronized是Java关键字,而lock是一个类,而且Synchronized可以给类、方法、代码块加锁,但是lock、ReentrantLock只能给代码块加锁,Synchronized不需要手动释放锁,lock和ReentrantLock都需要手动释放锁,但是Synchronized没法知道当前是否加锁成功,lock和ReentrantLock可以做到

2.9、volatile有什么用?可以用volatile修饰的数组吗?和atomic有什么不同?

参考答案: volatile 关键字来保证可见性和禁止指令重排。可以用volatile来创建数组,不过只是一个指向数组的引用,而不是整个数组。意思是,如果改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线程同时改变数组的元素,volatile 标示符就不能起到之前的保护作用了;
volatile 变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用 volatile 修饰 count 变量,那么count++ 操作就不是原子性的。而 AtomicInteger 类提供的 atomic 方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作

2.10、volatile能使得一个非原子操作变成原子操作吗?

参考答案:关键字volatile的主要作用是使变量在多个线程间可见,但无法保证原子性,对于多个线程访问同一个实例变量需要加锁进行同步。虽然volatile只能保证可见性不能保证原子性,但用volatile修饰long和double可以保证其操作原子性

2.11、对volatile有过什么实践?

参考答案:单例模式=>对于Double-Check这种可能出现的问题(当然这种概率已经非常小了,但毕竟还是有的嘛~),解决方案是:只需要给instance的声明加上volatile关键字即可volatile关键字的一个作用是禁止指令重排,把instance声明为volatile之后,对它的写操作就会有一个内存屏障(什么是内存屏障?),这样,在它的赋值完成之前,就不用会调用读操作。注意:volatile阻止的不是singleton =newSingleton()这句话内部[1-2-3]的指令重排,而是保证了在一个写操作([1-2-3])完成之前,不会调用读操作(if (instance == null))。

2.12、什么是不可变对象?对写并发应用有什么帮助?

参考答案:不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,反之即为可变对象(Mutable Objects)。不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率

三、Lock体系

3.1、Java Concurrency API中的Lock接口是什么?对比同步有什么优势?

参考答案:Lock接口相对同步方法和同步块下,提供了更具备拓展性的锁操作,允许更灵活的结构,可以支持多个相关类的条件对象,其优势主要有:
1、可以使锁更公平
2、可以使线程在等待锁的时候响应中断
3、可以让线程尝试的去获取锁,并在无法获取锁的时候立即返回或者等待一段时间
4、可以在不同的范围,以不同的顺序获取锁和释放锁资源

3.2、乐观锁与悲观锁的是什么,有哪些实现方式?

参考答案:1、悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次拿数据的时候,都会上锁,别人想要操作或者获取数据时,都需要阻塞直到拿到这把锁之后
2、乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改数据,所以不会上锁,但是在更新的时候,会判断在此时间段中数据是否被别人修改过,例如原子操作类底层使用的CAS机制

3.3、什么是CAS?

参考答案:CAS为Compare And Swap,也就是比较并且交换,在并发场景下,每个线程中都会维护三个值,分别是:1、修改数据的地址值;2、需要更新的值;3、更新前期望的原始值,在操作的过程中,分为两步,分别是比较和交换,比较是比较当前线程持有的期望地址原始值与地址当前值,如果相同就交换值,否则就不做操作,自旋进入下一轮重试

3.4、死锁与活锁的区别?死锁与饥饿的区别?

参考答案:1、死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
2、活锁:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。

活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,这就是所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。

饥饿:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。 Java 中导致饥饿的原因: 1、高优先级线程吞噬所有的低优先级线程的 CPU 时间。 2、线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。 3、线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait方法),因为其他线程总是被持续地获得唤醒。

3.6、AQS介绍?原理分析?

参考答案:AQS为Abstract Queue Synchronized,是将大部分同步器的共同行为抽象出来的框架,例如:阻塞队列、条件队列、获取公平/非公平锁机制等,其内部结构原理可见MESA,如下图示:
在这里插入图片描述

3.7、什么是公平锁、非公平锁、可重入锁?

参考答案:1、公平锁:按照线程在队列中的排队顺序,先到者先拿到锁;
2、非公平锁:当线程获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
2、可重入锁:当前线程在原持有锁的基础上,可再次重复获取锁资源

3.8、ReentrantLock实现公平锁、非公平锁以及可重入锁的原理是什么?

参考答案:ReentrantLock中,在声明其锁对象实例时,可设置构参是否为公平锁/非公平锁,在加锁方法lock上,公平锁的实现是直接调用acquire方法==>如果有线程持有锁,就先加入到阻塞队列中排队,按照顺序拿锁资源,但是如果是非公平锁的话,在调用acquire方法之前,会先以CAS方式尝试获取锁,如果获取成功,直接返回,如果获取失败=>调用acquire方法,加入到队列中去排队,可从入锁的实现原理是在加锁时,会判断其当前线程是否和持有锁的线程是同一个,如果是同一个,那就需要将锁标识的计数器+1,相当于是重入锁

3.9、ReadWriteLock是什么?

参考答案:ReadWriteLock 是一个读写锁接口,读写锁是用来提升并发程序性能的锁分离技术,ReentrantReadWriteLock 是 ReadWriteLock 接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。其特点主要有:
1、公平选择性:支持非公平(默认选择)和公平锁的获取方式,吞吐量还是非公平优先于公平
2、重进入:读锁和写锁都支持线程重进入
3、锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级为读锁

四、并发容器

4.1、什么是ConcurrentHashMap?

参考答案:其是一个线程安全且高效的HashMap实现,其中利用锁分段(JDK1.7)思想提高并发度,其1.8版本抛弃了Segment分段锁,而采用的是CAS+Synchronized来保证并发安全性

4.2、ConcurrentHashMap的并发度是什么?

参考答案:ConcurrentHashMap 把实际 map 划分成若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度获得的,它是 ConcurrentHashMap 类构造函数的一个可选参数,默认值为 16

4.3、Java中的同步集合与并发集合有什么区别?

参考答案:同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。在 Java1.5 之前程序员们只有同步集合来用且在多线程并发的时候会导致争用,阻碍了系统的扩展性。Java5 介绍了并发集合像ConcurrentHashMap,不仅提供线程安全还用锁分离和内部分区等现代技术提高了可扩展性

4.4、SynchronizedMap与ConcurrentHashMap有什么区别?

参考答案:SynchronizedMap 一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为 map,ConcurrentHashMap 中则是一次锁住一个桶。ConcurrentHashMap 默认将hash 表分为 16 个桶,诸如 get,put,remove 等常用操作只锁当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有 16 个写线程执行,并发性能的提升是显而易见的

4.5、CopyOnWriteArrayList是什么?可以用于什么样的应用场景?有哪些优缺点

参考答案:CopyOnWriteArrayList 是一个并发容器。有很多人称它是线程安全的,我认为这句话不严谨,当然前提条件就是非复合场景下操作它是线程安全的。CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出 ConcurrentModificationException。在CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。所以其使用场景多为:合适读多写少的场景

其存在的缺点为: 1、不能用于实时读的场景,因为底层在拷贝数组时,也需要耗费时间,有可能还未拷贝完成,原始的老数据就被读出去使用 2、底层的拷贝数组,会消耗内存,如果数组数据较多,可能会导致young gc或full gc 其存在的优点为: 1、读写分离,效率较高 2、保证最终一致性 3、使用另外开辟空间的思路,来解决并发冲突

4.6、ThreadLocal是什么?有哪些应用场景?

参考答案:ThreadLocal是一个线程副本变量工具类,在每个线程中都创建了一个ThreadLocalMap对象,其实这就是一种以空间换取时间的做法,其使用场景主要有:为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection; 还有 Session 管理 等问题

4.7、什么是线程局部变量?

参考答案:线程局部变量是指局限于线程内部的变量,属于线程自身所有,不在多个线程间共享

4.8、ThreadLocal造成内存泄漏的原因是什么?如何解决?

参考答案:ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。

4.9、是什么阻塞队列?实现原理是什么?如何使用阻塞队列实现生产者=消费者模型

参考答案:阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。
这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。
阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

五、线程池

5.1、什么是线程池?有哪几种创建方式?

参考答案:线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
(1)newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
(2)newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。如果希望在服务器上使用线程池,建议使用 newFixedThreadPool方法来创建线程池,这样能获得更好的性能。
(3) newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。
(4)newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

5.2、使用线程池有什么优点?

参考答案:
1、降低资源消耗:重用存在的线程,减少对象创建销毁的开销。
2、提高响应速度。可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
3、提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控

5.3、线程池的状态有哪些?

参考答案:
1、RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。
2、SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。
3、STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。
4、TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为TIDYING 状态时,会执行钩子方法 terminated()。
6、TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。

5.4、什么是Executor框架?为什么使用Executor框架?Executor与Executors有什么区别?

参考答案:Executor 框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架
每次执行任务创建线程 new Thread()比较消耗性能,创建一个线程是比较耗时、耗资源的,而且无限制的创建线程会引起应用程序内存溢出。所以创建一个线程池是个更好的的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。利用Executors 框架可以非常方便的创建一个线程池

Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。
Executor 接口对象能执行我们的线程任务。
ExecutorService 接口继承了 Executor 接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。
使用 ThreadPoolExecutor 可以创建自定义线程池。
Future 表示异步计算的结果,他提供了检查计算是否完成的方法,以等待计算的完成,并可以使用 get()方法获取计算的结果。

5.5、线程池中的submit方法和execute方法有什么区别?

参考答案:
接收参数:execute()只能执行 Runnable 类型的任务。submit()可以执行Runnable 和 Callable 类型的任务。
返回值:submit()方法可以返回持有计算结果的 Future 对象,而execute()没有异常处理:submit()方便Exception处理

5.7、ThreadPoolExecutor构造函数重要参数分析?饱和策略是什么?

参考答案:
1、corePoolSize :核心线程数,线程数定义了最小可以同时运行的线程数量。
2、maximumPoolSize :线程池中允许存在的工作线程的最大数量
3、workQueue:当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,任务就会被存放在队列中
4、keepAliveTime:线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
5、unit :keepAliveTime 参数的时间单位。
6、threadFactory:为线程池提供创建新线程的线程工厂
7、handler :线程池任务队列超过 maxinumPoolSize 之后的拒绝策略

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值