提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 基本概念
- 线程
- 线程和进程的区别
- 多线程常用方法
- 守护线程是什么,用户线程是什么
- 创建线程的几种方式
- Runnable和Callable有什么区别
- 线程状态及转换?
- 线程run方法和start方法的区别
- 为什么调用 start() 方法会执行 run() 方法,为什么不能直接调用run() 方法?
- sleep方法和wait方法的区别
- Thread interrupt方法和stop方法
- interrupt、interrupted 和 isInterrupted 方法的区别?
- Thread 类中的 yield 方法有什么作用?
- 为什么Thread 类的 sleep()和 yield ()方法是静态的?
- 线程的 sleep()方法和 yield()方法有什么区别?
- 线程调度算法有哪些?
- 线程的调度策略
- 什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing)?
- 请说出与线程同步以及线程调度相关的方法
- JAVA线程数过多会造成什么异常?
- 对ThreadLocal的理解,有哪些应用场景
- 在 Java 程序中怎么保证多线程的运行安全(并发)?
- 什么叫线程安全?servlet、struts2、springMvc是线程安全吗?
- Java线程同步的几种方法
- 线程池
- 什么是线程池?
- 线程池的作用?
- 线程池有什么优点?
- 线程池都有哪些状态?
- 常用的线程池以及线程池对应的使用场景
- Java 中 Executor 和 Executors 的区别?
- 使用 ThreadPoolExecutor 可以创建自定义线程池。ThreadPoolExecutor三个钩子方法
- 如果你提交任务时,线程池队列已满,这时会发生什么?
- 线程池执行原理
- 什么是CPU密集
- 什么是IO密集
- 如何合理分配线程池大小?
- 什么是线程组?
- 为什么不推荐使用线程组?
- 你对信号量Semaphre的理解
- CountDownLatch和CyclicBattler的区别
- 线程池的参数有哪些
- 执行execute方法和submit方法的区别是什么
- 说下Fork和Join并行计算框架的理解
- 进程
- 锁
- 说说对悲观锁和乐观锁的理解
- 悲观锁和乐观锁的缺点有哪些
- 说说对synchronized关键字的了解
- synchronized锁的原理
- 说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗?
- synchronized的使用方法
- synchronized 和 Lock 有什么区别?
- Lock 接口和synchronized 对比同步它有什么优势?
- synchronized 和 ReentrantLock 区别是什么?
- synchronized、volatile、CAS 比较
- synchronized 和 volatile 的区别是什么?
- 谈一下你对 volatile 关键字的理解?
- Java 中能创建 volatile 数组吗?
- atomic 的原理是什么?
- volatile 变量和 atomic 变量有什么不同?
- volatile 能使得一个非原子操作变成原子操作吗?
- 什么是CAS
- CAS 的会产生什么问题
- 对AQS同步器的理解,对资源的共享模式有哪些
- AQS底层使用了模板方式模式,说出几个重要重写的方法
- 什么是死锁?
- 死锁产生的必要条件?
- 解决死锁问题的方法
- Java 中导致饥饿的原因
- 死锁与活锁与饥饿的区别?
- 并发容器
- 并发队列
- 其他
- 总结
前言
有不足和错误的地方欢迎指正
基本概念
并发和并行的区别
- 并发:指多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。(两个人用一台电脑)
- 并行:指单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”。(两个人分配了两个电脑)
- 串行:有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题。(两个人排队使用一台电脑)
同步、异步、阻塞、非阻塞的概念
- 同步(线程策略)
当一个request发送出去以后,会得到一个response,这整个过程就是一个同步调用的过程。哪怕response为空,或者response的返回特别快,但是针对这一次请求而言就是一个同步的调用。请求必须响应,否则一直耗下去
- 异步
当一个request发送出去以后,没有得到想要的response,而是通过后面的callback、状态或通知的方式获得结果。不求结果该干嘛干嘛
- 阻塞(线程状态)
阻塞调用是指调用方发出request的线程因为某种原因(如:等待系统资源)被服务方挂起,当服务方得到response后就唤醒挂起线程,并将response返回给调用方。
- 非阻塞
非阻塞调用是指调用方发出request的线程 在没有等到结果时不会被服务方 挂起,直到得到response后才返回。阻塞和非阻塞最大的区别就是看调用方线程是否会被挂起。
并发编程的3个必要因素
- 原子性
原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败
- 可见性
一个线程对共享变量的修改,另一个线程能够立刻看到(synchronized,volatile)
- 有序性
程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
什么是重排序
一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,进行重新排序(重排序),它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致
这些重排序对于单线程没问题,但是多线程都可能会导致多线程程序出现内存可见性问题
- 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行的重排序。现代处理器采用了指令级并行技术(ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
重排序遵守的规则
- as-if-serial
不存在数据依赖的可以被编译器和处理器重排序,一个操作依赖两个操作,这两个操作如果不存在依赖可以重排序。单线程根据此规则不会有问题,但是重排序后多线程会有问题
- as-if-serial规则和happens-before规则的区别
as-if-serial语义保证单线程内程序的执行结果不被改变,
happens-before保证正确同步的多线程程序的执行结果不被改变。
as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。
线程
线程和进程的区别
线程 | 进程 | |
---|---|---|
基本单位 | CPU处理任务 调度和执行的基本单位 | 操作系统资源分配的基本单位 |
资源开销 | 每个线程都有独立的运行栈和程序计数器,线程之间切换的开销小 | 进程之间的切换开销大 |
内存分配 | 同进程的线程共享代码和数据空间, | 每个进程都有独立的代码和数据空间 |
包含关系 | 线程是进程的一部分 | 进程可以有多个线程 |
影响关系 | 一个线程崩溃整个进程都会死掉,但QQ音乐崩溃对typora没影响 | 在保护模式下一个进程崩溃对其他进程无影响 |
执行过程 | 线程不能独立运行,只能依存应用程序中 | 进程有程序运行的入口,执行序列、程序出口 |
线程和进程均可并发执行
多线程常用方法
方法 | 行为 | 方法 | 行为 |
---|---|---|---|
sleep() | 强迫一个线程睡眠N毫秒 | isDaemon() | 一个线程是否为守护线程 |
isAlive() | 判断一个线程是否存活 | setDaemon() | 设置一个线程为守护线程 |
join() | 等待线程终止 | setName() | 为线程设置一个名称 |
activeCount() | 程序中活跃的线程数 | wait() | 强迫一个线程等待 |
enumerate() | 枚举程序中的线程 | notify() | 通知一个线程继续运行 |
currentThread() | 得到当前线程 | setPriority() | 设置一个线程的优先级 |
守护线程是什么,用户线程是什么
- 守护线程:运行在后台,为其他前台线程服务。也可以说守护线程是 JVM 中非守护线程的 “佣人”。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作
- 用户线程:,运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程
守护线程经常被用来执行一些后台任务,但是呢,你又希望在程序退出时,或者说 JVM 退出时,线程能够自动关闭,此时,守护线程是你的首选。比如垃圾回收线程
创建线程的几种方式
- 继承Thread类:重写run方法,创建实例化对象,调用start方法
- 实现Runable接口:创建一个实现Runable接口的类,重写run方法,创建类实例化对象t,创建Thread类的实例化对象并将t作为参数传递,通过Thread类的对象调用start方法
- 实现Callable接口
- 使用匿名内部类
方法1通过继承创建一个对象。该对象包含了子类和父类的信息。
方法2是先创建一个实现Runable的对象,把该对象作为创建Thread对象的参数
两种方式得到的对象都包含了线程类和实例类的信息
Runnable和Callable有什么区别
- 相同:都是接口;都可以编写多线程程序;都采用Thread.start()启动线程
Runnable | Callable |
---|---|
无返回值 | 有返回值,类型是泛型 |
只能抛运行时异常,不能捕获 | 可以抛出异常,也可以捕获异常 |
和Future、FutureTask配合可以捕获异步执行结果(异步callback) |
Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
线程状态及转换?
-
新建(new):新创建了一个线程对象
-
就绪(可运行状态)(runnable):线程对象创建后,当调用线程对象的 start()方法,该线程处于就绪状态,等待被线程调度选中,获取cpu的使用权。
-
运行(running):可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。
注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
-
阻塞(block):处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,被 CPU 调用以进入到运行状态,必须再次进入就绪状态
- 等待阻塞:运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waitting queue)中,使本线程进入到等待阻塞状态;
- 同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),,则JVM会**把该线程放入锁池(**lock pool)中,线程会进入同步阻塞状态;(老子钱都给了,你说她还在跟别人做,让我先回去?没门,我等!)
- 其他阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。
-
死亡(dead):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
线程run方法和start方法的区别
每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,run()方法称为线程体。通过调用Thread类的start()方法来启动一个线程。
- run()
- 用于执行线程的运行时代码
- 可以重复调用
- 方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法
- start()
- 用于启动线程
- 只能调用一次
- 真正实现了多线程运行。调用start()方法无需等待run方法体代码执行完毕,可以直接继续执行其他的代码; 此时线程是处于就绪状态,并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, run()方法运行结束, 此线程终止。然后CPU再调度其它线程。
为什么调用 start() 方法会执行 run() 方法,为什么不能直接调用run() 方法?
-
new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行run() 的内容,这是真正的多线程工作
-
直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,不是多线程工作。
sleep方法和wait方法的区别
- wait方法
- 必须配合synchronized一起使用,不然会在运行时抛出illegalMonitorStateException异常
- 属于Object类的方法
- 可以不传递任何参数,不传递表示永久休眠直到另一个线程调用了notify或者notifyAll后休眠的线程才能被唤醒
- sleep方法
- 不需要配合synchronized一起使用
- 属于Thread类的方法
- sleep方法必须要传递一个参数,超过参数时间后,线程会自动唤醒
两者都可以暂停线程的执行
Thread interrupt方法和stop方法
interrupt、interrupted 和 isInterrupted 方法的区别?
- interrupt
线程中断是线程的中断状态,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException 的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。
- interrupted
静态方法,查看当前线程中断信号是true还是false并且清除中断信号。如果一个线程被中断了,第一次调用 interrupted 则返回 true,第二次和后面的就返回false 了。
- isInterrupted
实例方法,查看调用该方法的对象的线程的中断信号状态
Thread 类中的 yield 方法有什么作用?
使当前线程从执行状态(运行状态)变为可执行状态(就绪状态); running to runnable
当前线程到了就绪状态,那么接下来哪个线程会从就绪状态变成执行状态呢?可能是当前线程,也可能是其他线程,看系统的分配了
为什么Thread 类的 sleep()和 yield ()方法是静态的?
Thread.sleep()和Thread. yield()方法将在当前正在执行的线程上运行
一个进程只能有一个正在执行的线程,其他处于等待状态的线程上调用这些方法是没有意义的*。
线程的 sleep()方法和 yield()方法有什么区别?
sleep() | yield() |
---|---|
会给低优先级的线程以运行的机会 | 只会给相同优先级或更高优先级的线程以运行的机会(就绪和阻塞的区别) |
转入阻塞(blocked)状态 | 转入就绪(ready)状态 |
方法声明抛出 InterruptedException | 方法没有声明任何异常 |
具有更好的可移植性 | 建议使用yield()方法来控制并发线程的执行 |
线程调度算法有哪些?
- 分时调度模型
分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU的时间片这个也比较好理解
- 抢占式调度模型
Java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU
线程的调度策略
线程调度器选择优先级最高的线程运行,但是,如果发生以下情况,就会终止线程的运行
- 线程体中调用了 yield 方法让出了对 cpu 的占用权利
- 线程体中调用了 sleep 方法使线程进入睡眠阻塞状态
- 线程由于 IO 操作受到阻塞
- 另外一个更高优先级线程出现
- 在支持时间片的系统中,该线程的时间片用完
什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing)?
- 线程调度器是一个操作系统服务。它负责为 Runnable 状态的线程分配 CPU 时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现
- 时间分片是指将可用的 CPU 时间分配给可用的 Runnable 线程的过程。分配 CPU 时间可以基于线程优先级或者线程等待的时间
线程调度并不受到 Java 虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级)
请说出与线程同步以及线程调度相关的方法
-
wait()
使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁,可以是就绪也可以是执行状态的锁
-
sleep()
使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException 异常
-
notify()
唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关
-
notityAll()
唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态
JAVA线程数过多会造成什么异常?
-
线程生命周期开销非常高
-
消耗过多的CPU
资源如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争 CPU资源时还将产生其他性能的开销。
-
降低了稳定性JVM
可创建线程的数量上存在一个限制值,随着平台的不同而不同,并且承受着多个因素制约(包括 JVM 的启动参数、Thread 构造函数中请求栈的大小,以及底层操作系统对线程的限制等)。如果破坏了这些限制,那么可能抛出OutOfMemoryError 异常
这题应该是线程池的引子
对ThreadLocal的理解,有哪些应用场景
用ThreadLocal来保存的数据,只对当前线程生效,当前线程对该数据的任何操作,对别的线程是不生效的(线程间数据隔离)
应用场景:
- Spring,用ThreadLocal来保存数据库连接,这样可以保证单个线程的操作使用的是同一个数据库连接
- 用来做session、cookie的隔离
在 Java 程序中怎么保证多线程的运行安全(并发)?
出现线程安全问题的原因一般都是三个原因
- 线程切换带来的原子性问题 解决办法:使用多线程之间同步synchronized或使用锁(lock)
- 缓存导致的可见性问题 解决办法:synchronized、volatile、LOCK,可以解决可见性问题
- 译优化带来的有序性问题 解决办法:Happens-Before 规则可以解决有序性问题
什么叫线程安全?servlet、struts2、springMvc是线程安全吗?
- 非线程安全的
Servlet 是单实例多线程的,当多个线程同时访问同一个方法,是不能保证共享变量的线程安全性
SpringMVC 的 Controller ,和 Servlet 类似的处理流程
- 线程安全的
struts2 的 action 是多实例多线程的,是线程安全的,每个请求过来都会 new 一个新的 action 分配给这个请求,请求完成后销毁
Struts2 好处是不用考虑线程安全问题;Servlet 和 SpringMVC 需要考虑线程安全问题,但是性能可以提升不用处理太多的 gc,可以使用 ThreadLocal 来处理多线程的问题
Java线程同步的几种方法
- 使用synchronized关键字
- 使用重入锁lock
- 使用特殊域变量volatile实现线程同步
- wait和notify
- 使用局部变量
- 使用阻塞队列
- 使用原子变量实现线程同步
线程池
什么是线程池?
Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来许多好处。(同步并发用锁,异步并发用线程池)
线程池的作用?
线程池是为突然大量爆发的线程设计的,通过有限的几个固定线程为大量的操作服务,减少了创建和销毁线程所需的时间(生命周期消耗),从而提高效率。
如果一个线程所需要执行的时间非常长的话,就没必要用线程池了(线程池的作用在于降低线程创建和销毁时间,结果你那么久我还不好控制还不如直接创建线程),况且我们还不能控制线程池中线程的开始、挂起、和中止。
线程池有什么优点?
- 降低资源消耗:重用存在的线程,减少对象创建销毁的开销。
- 提高响应速度。可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
附加功能:提供定时执行、定期执行、单线程、并发数控制等功能。
线程池都有哪些状态?
running | shutdown | stop | tidying | terminated | |
---|---|---|---|---|---|
新任务 | 接受 | 不接受 | 不接受 | 已销毁 | |
等待队列任务 | 处理 | 继续处理 | 不再处理 | 已销毁 | |
正在执行任务 | 执行 | 继续执行 | 中断执行 | 已销毁 |
- TIDYING
所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。
- TERMINATED
terminated()方法结束后,线程池的状态就会变成这个。
常用的线程池以及线程池对应的使用场景
1、newFixedThreadPool 创建一个指定工作线程数量的线程池。 每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。
2、newCachedThreadPool 创建一个可缓存的线程池。 这种类型的线程池特点是:
1).工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE),这样可灵活的往线程池中添加线程。
2).如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程
3、newSingleThreadExecutor创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,如果这个线程异常结束,会有另一个取代它,保证顺序执行(我觉得这点是它的特色)。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。
4、newScheduleThreadPool 创建一个定长的线程池,而且支持定时的以及周期性的任务执行,类似于Timer。
Java 中 Executor 和 Executors 的区别?
-
Executors 工具类;线程池创建工厂
Executors的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。
Executor | ExecutorService |
---|---|
父接口 | 子接口 |
execute()方法用来接收一个Runnable接口的对象 | submit()方法可以接受Runnable和Callable接口的对象 |
execute() 方法不返回任何结果 | submit()方法可以通过一个 Future 对象返回运算结果。 |
提供用来控制线程池的方法。比如:调用 shutDown() 方法终止线程池。 |
ThreadPoolExecutor是Java的线程池
ThreadPoolTaskExecutor是spring封装的线程池
使用 ThreadPoolExecutor 可以创建自定义线程池。ThreadPoolExecutor三个钩子方法
beforeExecute() | afterExcute() | terminated() |
---|---|---|
线程池工作线程在异步执行目标实例前调用 | 异步执行**(如Runnable实例)**后调用 | 所有任务执行完后回调 |
默认不执行任何操作,可在子类对其进行自定义 | 可以在调度器子类中对其进行自定义 | 默认实现不执行任何操作 |
可用于重新初始化ThreadLocal线程本地变量实例 | 可用于清除ThreadLocal线程本地变量 | |
更新日志记录、开始计时统计、更新上下文变量 | 更新日志记录、收集统计信息、更新上下文变量 |
如果你提交任务时,线程池队列已满,这时会发生什么?
- 如果使用了无界队列,例如:LinkedBlockingQueue
没有影响,继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务
- 如果使用了有界队列,例如:ArrayBlockingQueue
任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue 满了,会根据maximumPoolSize 的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue 继续满,那么则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是 AbortPolicy
线程池执行原理
上图用于判断提交的任务的结果:
- 创建核心工作线程addwork()
- 创建不带任务的工作线程addwork()
- 拒绝任务reject()
上图是从任务被提交到工作线程销毁的过程(workers是线程池,里面都是核心线程)
什么是CPU密集
CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。
CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程,该任务都不可能得到加速,因为CPU总的运算能力就那样。
什么是IO密集
IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会导致大量的CPU运算能力浪费在等待。所以在IO密集型任务中使用多线程可以大大的加速程序运行,即时在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。
如何合理分配线程池大小?
要合理的分配线程池的大小要根据实际情况来定,简单的来说的话就是根据CPU密集和IO密集来分配
- CPU密集型:核心线程数=CPU核心数 或者 CPU核心数+1 (并行)
- IO密集型:核心线程数=2*CPU核心数 或者 CPU核心数/(1-阻塞系数) (并发)
- 混合型:核心线程数=(线程等待时间/线程CPU时间 +1)*CPU核心数
cpu密集用到cpu的任务重几乎没有阻塞,io密集读写任务重阻塞多单线程cpu浪费可以一部分线程用来处理cpu任务一部分线程拿来处理io同时进行
什么是线程组?
ThreadGroup 类,可以把线程归属到某一个线程组中,线程组中可以有线程对象,也可以有线程组,组中还可以有线程,这样的组织结构有点类似于树的形式。
线程组和线程池作用完全不同,
-
线程组是为了方便线程的管理,
-
线程池是为了管理线程的生命周期----->复用线程----->减少创建销毁线程的开销
为什么不推荐使用线程组?
因为使用有很多的安全隐患吧,没有具体追究,如果需要使用,推荐使用线程池。
你对信号量Semaphre的理解
信号量是一个线程同步结构,它能够在线程间发送信号,避免信号丢失.
信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
CountDownLatch和CyclicBattler的区别
CountDownLatch | CyclicBattler |
---|---|
允许1个以上线程等待其他线程完成执行 | 允许2个以上线程相互等待 |
计数器无法被重置 | 计数器可以被重置 |
线程池的参数有哪些
- corePoolSize:表示当前线程的核心线程数。线程池会维护档期那数据的线程,在线程池中即使这些线程一直处于闲置状态,也不会被销毁
- maxPoolSize:表示线程池中允许的最大线程数:
- keepAliveTime:表示空闲线程的存活时间,当线程池中的线程数大于核心线程数目,线程处于空闲状态,那么在指定时间后,这个空闲线程会被销毁,从而逐渐恢复到稳定的核心线程数量
- unit:当前unit表示的是keepAliveTimekeepAliveTime的计量单位,通常使用TimeUnit.SECOND五秒
- workQueue:任务工作队列,
- queueCapacity:任务队列容量。会影响到线程的变化
- allowCoreThreadTimeout:是否允许核心线程空闲退出,默认是false
执行execute方法和submit方法的区别是什么
execute和submit方法都属于线程池的方法
execute() | submit() |
---|---|
只能提交Runnable类型的任务 | Runnable和Callable类型的都可以提交 |
直接抛出任务执行时的异常 | 会吃掉异常,Future.get()可以重新抛出 |
所属顶层接口时Executor | 所属顶层接口时ExecutorService |
在实现类ThreadPoolExecutor重写 | 在抽象类AbstractExecutorService重写 |
说下Fork和Join并行计算框架的理解
分治的思想:把一个复杂的问题分解成相似的子问题,子问题再分子问题,知道问题不可再分,然后层层返回问题的结果,最终合并结果
分治算法有很多应用,类似大数据的MapReduce,归并算法,快速排序算法。在JUC中也提供了一个叫Fork/Join的并行计算框架来处理分治的情况,类似单机版的MapReduce
分治的两个阶段:
- 分解任务(fork阶段),把任务分解为一个个小任务直至小任务可以简单的计算返回结果
- 合并结果(join阶段),把每个小任务的结果合并返回得到最终结果
分治的框架主要包含两部分:ForkJoinPool,ForkJoinTask
进程
进程间的通信方式有哪些?
-
管道:pipe管道和FIFO管道
-
消息队列(MessageQueue):消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
-
共享存储(SharedMemory):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
-
信号量(Semaphore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
-
套接字(Socket):套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
-
信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
pipe管道 FIFO命名管道 半双工的通信方式,数据只能单向流动 半双工的通信方式, 只能在亲缘关系(父子进程)间使用 允许无亲缘关系进程间的通信
锁
说说对悲观锁和乐观锁的理解
- 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁.。降低并行性,其他事物必须等待该事物处理完才可以处理那行数据
- 传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁
- 乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。不会产生任何锁和死锁
- 像数据库提供的类似于 write_condition 机制。在 Java中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的
悲观锁和乐观锁的缺点有哪些
- 乐观锁没加锁,效率高。但锁的粒度掌握不好,更新失败的概率会比较高
- 悲观锁依赖数据库锁,效率低,会死锁。更新失败的概率也低
随着互联网三高架构(高并发,高性能,高可用)的提出,悲观锁已经越来越少的被使用到生产环境中了,尤其是并发量比较大的业务场景
说说对synchronized关键字的了解
synchronized关键字可以保证被他修饰的方法或者代码块在任意时刻只能有一个线程执行
- synchronized发展史。在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销
synchronized锁的原理
- synchronized 底层实现原理
Synchronized的语义底层是通过一个monitor(监视器锁)的对象来完成。每个对象有一个监视器锁(monitor)。每个Synchronized修饰过的代码当它的monitor被占用时就会处于锁定状态并且尝试获取monitor的所有权
如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者
如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1
如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权
- synchronized可重入的原理
重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁。底层原理维护一个计数器,当线程获取该锁时,计数器加一,再次获得该锁时继续加一,释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁
- synchronized锁升级原理
锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方 式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。
在锁对象的对象头里面有一个 threadid 字段,第一次访问时,threadid为空,JVM让其持有偏向锁,并将threadid设置为其线程id,再次访问判断threadid和线程id是否一致,如果一致则直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数以后,如果还是没有正常获取要使用的对象,此时就会把轻量级锁升级为重量级锁,此过程就构成了synchronized锁的升级
说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗?
JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
锁主要存在四种状态
-
无锁状态
-
偏向锁状态
顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,减少加锁/解锁的一些CAS操作(比如等待队列的一些CAS操作),这种情况下,就会给线程加一个偏向锁。
-
轻量级锁状态
轻量级锁是由偏向所升级来的, 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
-
重量级锁状态
重量级锁是synchronized ,是 Java 虚拟机中最为基础的锁实现。在这种状态下,Java 虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程。偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,轻量级锁就会升级为重量级锁;
他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率
synchronized的使用方法
- 修饰实例方法:对当前对象加锁,进入同步代码要想获得当前对象的锁
- 修饰静态方法:给当前类枷锁,会作用于类的所有对象,进入同步代码需要想获得当前class的锁。如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,
- 修饰代码块:对指定对象/类枷锁,synchronized(this | object)表示进入代码需要获得指定对象的锁。synchronized(类.class)表示进入同步代码需要获得当前class的锁(是对象的锁还是类的锁要看synchronized的具体实现)
synchronized 和 Lock 有什么区别?
synchronized | Lock |
---|---|
Java内置的关键字 | Java的一个接口 |
可以给类、方法、代码块加锁 | 只能给代码块加锁 |
自动获取锁和释放锁,发生异常自动放锁,不会死锁 | 手动加锁和释放锁,没有释放可能会死锁 |
不能知道是否获取到锁 | 可以知道是否获取到锁 |
Lock 接口和synchronized 对比同步它有什么优势?
Lock 接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。
优势
- 可以使锁更公平
- 可以使线程在等待锁的时候响应中断
- 可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
- 可以在不同的范围,以不同的顺序获取和释放锁
整体上来说 Lock 是 synchronized 的扩展版,Lock 提供了可轮询的(tryLock 方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition 方法)锁操作。
Lock 的实现类基本都支持非公平锁(默认)和公平锁,synchronized 只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择。
synchronized 和 ReentrantLock 区别是什么?
-
相同点:两者都是可重入锁
-
不同
- synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量
- synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在Java 6 中对 synchronized 进行了非常多的改进。
ReentrantLock | synchronized |
---|---|
手动获取和释放锁 | 自动释放和开启锁 |
只适用于代码块锁 | 可以修饰类、方法、变量 |
底层调用的是Unsafe的park方法加锁 | 操作的应该是对象头中mark word |
synchronized、volatile、CAS 比较
-
synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。
-
volatile 提供多线程共享变量可见性和禁止指令重排序优化。
-
CAS 是基于冲突检测的乐观锁(非阻塞)
synchronized 和 volatile 的区别是什么?
synchronized 表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程(挂牌显示房间里有人正在干活)
volatile 表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序
区别
volatile | synchronized |
---|---|
变量修饰符 | 可以修饰类、方法、变量 |
只能实现变量的修改可见性,不保证原子性 | 保证变量的修改可见性和原子性 |
不会造成线程阻塞 | 可能造成线程阻塞 |
标记的变量不会被编译器优化 | 标记的变量可以被编译器优化 |
线程同步的轻量级实现 | 1.6之后引入偏向锁和轻量级锁,用的多 |
谈一下你对 volatile 关键字的理解?
-
对于可见性
Java 提供了 volatile 关键字来保证可见性和禁止指令重排。volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主内存中,当有其他线程需要读取时,它会去内存中读取新值
-
从实践角度而言
volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详细的可以参见java.util.concurrent.atomic 包下的类,比如 AtomicInteger。volatile 常用于多线程环境下的单次操作(单次读或者单次写)。
Java 中能创建 volatile 数组吗?
能,Java 中可以创建 volatile 类型数组,不过volatile作用于指向数组的引用,而不是整个数组。意思是,如果改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线程同时改变数组的元素,volatile 标示符就不能起到之前的保护作用了。
atomic 的原理是什么?
Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。
volatile 变量和 atomic 变量有什么不同?
volatile 变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。
例如用 volatile 修饰 count 变量,那么 count++ 操作就不是原子性的。
而 AtomicInteger 类提供的 atomic 方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。
volatile 能使得一个非原子操作变成原子操作吗?
关键字volatile的主要作用是使变量在多个线程间可见,但无法保证原子性,对于多个线程访问同一个实例变量需要加锁进行同步。
用volatile修饰long和double可以保证其操作原子性。
什么是CAS
- CAS 是 compare and swap 的缩写,即我们所说的比较交换
- cas 是一种基于锁的操作,而且是乐观锁。在 java 中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加 version 来获取数据,性能较悲观锁有很大的提高
- CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果V对应的值和 A 的值是一样的,那么就将V对应的值更新成 B。CAS是通过无限循环来获取数据的,如果在第一轮循环中,a 线程获取地址里面的值被b 线程修改了,那么 a 线程需要自旋,到下次循环才有可能机会执行
CAS 的会产生什么问题
- ABA 问题.
比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但可能存在潜藏的问题
解决方式:从Java1.5 开始 JDK 的 atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题
- 循环时间长开销大
对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized
- 只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁
对AQS同步器的理解,对资源的共享模式有哪些
-
全程是AbstractQueuedSynchronizer,抽象队列同步器。是用来构建锁同步器的基础框架
-
AQS是并发包的一个基础组件,用来实现各种锁和同步组件的
-
包含了state变量,加锁线程。等待队列等并发中的核心组件
共享模式:
- 独占式,只允许一个线程获取同步状态,其他线程只能加入到同步队列等待(ReentrantLock)
- 共享式,允许多个线程同时获取到同步状态(semaphore,CounDownLatch)
AQS底层使用了模板方式模式,说出几个重要重写的方法
tryacquire(int):独占式获取资源,成功返回true,否则返回false
tryrelease(int):独占式释放资源,成功返回true,否则返回false
tryacquireshared(int):共享式获取资源,返回大于等于0的值表示成功,否则表示失败
tryreleaseshared(int):共享式释放资源,成功返回true,否则返回false
isheldexclusive():该线程是否在独占资源
什么是死锁?
死锁是指多个进程(线程)在执行过程中,它们中的一个或者全部都在等待某个资源被释放,由于线程被无限期地阻塞,因此程序不可能正常终止。这些永远在互相等待的进程(线程)称为死锁进程(线程)
死锁产生的必要条件?
- 互斥使用条件:即当资源被一个线程使用(占有)时,别的线程不能使用(不允许多p)
- 请求和保持条件:即当资源请求者在请求其他的资源的同时保持对原有资源的占有。(不允许双飞)
- 不可抢占条件:资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。(不允许横刀夺爱)
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。如一个进程集合A在等B,B在等C,C在等A(不允许三角恋)
四个条件都成立才会进入死锁状态,只要打破一个条件就可以解除死锁
解决死锁问题的方法
一种是用synchronized,一种是用Lock显式锁实现。
Java 中导致饥饿的原因
高优先级线程吞噬所有的低优先级线程的 CPU 时间。
线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。
线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法),因为其他线程总是被持续地获得唤醒。
死锁与活锁与饥饿的区别?
死锁:指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,死锁不能自行解开
活锁:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。活锁有可能自行解开
饥饿:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。
并发容器
JDK中提供了哪些并发容器
并发容器 | 说明 |
---|---|
ConcurrentHashMap | 线程安全的HashMap |
CopyOnwriteArrayList | 线程安全的List,在读多写少的场合比Vector好 |
ConcurrentLinkedQueue | 一个线程安全的LinkedList,是一个非阻塞队列 |
BlockingQueue | 接口,实现它表示阻塞队列 |
ConcurrentSkipListMap | 实现调表的Map,使用调表的数据结构快速查找 |
### SynchronizedMap 和 ConcurrentHashMap 有什么区别
SynchronizedMap 一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为 map。
ConcurrentHashMap 使用分段锁来保证在多线程下的性能。
为什么要使用并发容器?
一般软件开发中容器用的最多的就是HashMap、ArrayList,LinkedList ,等等,但是在多线程开发中就不能乱用容器,如果使用了未加锁(非同步)的的集合,你的数据就会非常的混乱。由此在多线程开发中需要使用的容器必须是加锁(同步)的容器。
同步容器和并发容器的区别?
- 同步容器、
可以简单地理解为通过 synchronized 来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。比如 Vector,Hashtable,以及Collections.synchronizedSet,synchronizedList 等方法返回的容器。可以通过查看 Vector,Hashtable 等这些同步容器的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并在需要同步的方法上加上关键字 synchronized。
Vector
Vector与ArrayList一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,访问它比访问ArrayList慢很多
* ArrayList是最常用的List实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。当从 ArrayList的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找 和遍历,不适合插入和删除。ArrayList的缺点是每个元素之间不能有间隔。
* ArrayList和Vector有什么不同之处?
* Vector方法带上了synchronized关键字,是线程同步的
* ArrayList添加方法源码
* Vector添加源码(加锁了synchronized关键字)
Collections.synchronized
他完完全全的可以把List、Map、Set接口底下的集合变成线程安全的集合
- 并发容器
使用粒度更高的分段锁提高了伸缩性,在这种锁机制下,允许任意数量的读线程并发地访问 map,并且执行读操作的线程和写操作的线程也可以并发的访问map,同时允许一定数量的写操作线程并发地修改 map,所以它可以在并发环境下实现更高的吞吐量。
ConcurrentHashMap
ConcurrentHashMap是Java5中支持高并发、高吞吐量的线程安全HashMap实现。它由Segment数组结构和HashEntry数组结构组成。Segment数组在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键-值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构;一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素;每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。(有多少个元素就有多少个锁)
ConcurrentHashMap并发度是什么?
* ConcurrentHashMap 把实际 map 划分成若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度获得的,它是 ConcurrentHashMap 类构造函数的一个可选参数,默认值为 16,这样在多线程情况下就能避免争用。
* 在 JDK8 后,它摒弃了 Segment(锁段)的概念,而是启用了一种全新的方式实现,利用 CAS 算法。同时加入了更多的辅助变量来提高并发度,具体内容还是查看源码吧。
和HashTable区别
HashTable | ConcurrentHashMap |
---|---|
synchronized关键字锁住整张表 | 采用分段的数组+链表实现。Map被分为N个segement |
线程安全 | 相同的线程安全但是效率提升了N倍默认提升16倍 |
读操作不加锁,HashEntry的value是volatile的,能保证读取到最新的值。 | |
段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容避免无效扩容 |
CopyOnWriteArrayList
- CopyOnWriteArrayList 是什么?
CopyOnWriteArrayList 是一个并发容器。有很多人称它是线程安全的,我认为这句话不严谨,缺少一个前提条件,那就是非复合场景下操作它是线程安全的。
CopyOnWriteArrayList(免锁容器)的好处是当多个迭代器同时遍历和修改这个列表时,不会抛ConcurrentModificationException。在CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。
- CopyOnWriteArrayList 的使用场景?
合适读多写少的场景
- CopyOnWriteArrayList 的缺点?
-
由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致young gc 或者 full gc。
-
不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求。
-
由于实际使用中可能没法保证 CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次 add/set 都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。
- CopyOnWriteArrayList 的设计思想?
-
读写分离,读和写分开
-
最终一致性
-
使用另外开辟空间的思路,来解决并发冲突
CopyOnWriteArraySet
ConcurrentSkipListMap
ConcurrentSkipListSet
并发队列
什么是并发队列?什么是消息队列?
- 并发队列是多个线程以有次序 共享数据的重要组件
- 消息队列是分布式系统中重要的组件,是系统与系统直接的通信
并发队列和并发集合的区别?
- 并发队列遵循“先进先出”的规则,可以想象成排队检票,队列一般用来解决大数据量采集处理和显示的。
- 并发集合就是在多个线程中共享数据的
怎么判断并发队列是阻塞队列还是非阻塞队列?
在并发队列上JDK提供了Queue接口,一个是BlockingQueue接口为代表的阻塞队列,另一个是高性能(无堵塞)队列。
阻塞队列和非阻塞队列区别
- 试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素
- 试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程使队列重新变得空闲起来
常用并发列队
- 非堵塞队列
ArrayDeque(数组双端队列)
是JDK容器中的一个双端队列实现,内部使用数组进行元素存储,不允许存储null值,可以高效的进行元素查找和尾部插入取出,是用作队列、双端队列、栈的绝佳选择,性能比LinkedList还要好。
PriorityQueue(优先级队列)
一个基于优先级的无界优先级队列。优先级队列的元素按照其自然顺序进行排序,或者根据构造队列时提供的 Comparator 进行排序,具体取决于所使用的构造方法。该队列不允许使用 null 元素也不允许插入不可比较的对象
ConcurrentLinkedQueue (基于链表的并发队列)
是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能。ConcurrentLinkedQueue的性能要好于BlockingQueue接口,它是一个基于链接节点的无界线程安全队列。该队列的元素遵循先进先出的原则。该队列不允许null元素。
- 堵塞队列
DelayQueue(基于时间优先级的队列,延期阻塞队列)
是一个没有边界BlockingQueue实现,加入其中的元素必需实现Delayed接口。
当生产者线程调用put之类的方法加入元素时,会触发Delayed接口中的compareTo方法进行排序,也就是说队列中元素的顺序是按到期时间排序的,而非它们进入队列的顺序。排在队列头部的元素是最早到期的,越往后到期时间赿晚。
ArrayBlockingQueue (基于数组的并发阻塞队列)
是一个有边界的阻塞队列,它的内部实现是一个数组。有边界的意思是它的容量是有限的,我们必须在其初始化的时候指定它的容量大小,容量大小一旦指定就不可改变。ArrayBlockingQueue是以先进先出的方式存储数据
LinkedBlockingQueue(基于链表的FIFO阻塞队列)
阻塞队列大小的配置是可选的,如果我们初始化时指定一个大小,它就是有边界的,如果不指定,它就是无边界的。说是无边界,其实是采用了默认大小为Integer.MAX_VALUE的容量 。它的内部实现是一个链表。
LinkedBlockingDeque(基于链表的FIFO双端阻塞队列)
是一个由链表结构组成的双向阻塞队列,即可以从队列的两端插入和移除元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。它是可选容量的,在初始化时可以设置容量防止其过度膨胀,如果不设置,默认容量大小为Integer.MAX_VALUE。
PriorityBlockingQueue(带优先级的无界阻塞队列)
是一个无界队列,它没有限制,在内存允许的情况下可以无限添加元素;它又是具有优先级的队列,是通过构造函数传入的对象来判断,传入的对象必须实现comparable接口。
SynchronousQueue(并发同步阻塞队列)
是一个内部只能包含一个元素的队列。插入元素到队列的线程被阻塞,直到
另一个线程从队列中获取了队列中存储的元素。同样,如果线程尝试获取元素并且当前不存在任何元素,则该线程将被阻塞,直到线程将元素插入队列。
并发队列的常用方法
不管是哪种列队,是哪个类,但是他们使用的方法都是差不多的
add()
在不超出队列长度的情况下插入元素,可以立即执行,成功返回true,
如果队列满了就抛出异常。
offer()
在不超出队列长度的情况下插入元素的时候则可以立即在队列的尾部插
入指定元素,成功时返回true,如果此队列已满,则返回false。
put()
插入元素的时候,如果队列满了就进行等待,直到队列可用。
take()
从队列中获取值,如果队列中没有值,线程会一直阻塞,直到队列中有值,并且该方法取得了该值。
poll(long timeout,TimeUnit unit)
在给定的时间里,从队列中获取值,如果没有取到会抛出异常。
remainingCapacity()
获取队列中剩余的空间。
remove(Object o)
从队列中移除指定的值。
contains(Object o)
判断队列中是否拥有该值。
drainTo(Collectionc)
将队列中值,全部移除,并发设置到给定的集合中。
并发队列的工具类
CountDownLatch
类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他3个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。
CyclicBarrier (回环栅栏)
CyclicBarrie的作用就是会让所有线程都等待完成后才会继续下一步行动。
CyclicBarrier初始化时规定一个数目,然后计算调用了CyclicBarrier.await()进入等待的线程数。当线程数达到了这个数目时,所有进入等待状态的线程被唤醒并继续。
CyclicBarrier初始时还可带一个Runnable的参数, 此Runnable任务在CyclicBarrier的数目达到后,所有其它线程被唤醒前被执行。
Semaphore(信号量)
Semaphore 是 synchronized 的加强版,作用是控制线程的并发数量(允许自定义多少线程同时访问)。就这一点而言,单纯的synchronized 关键字是实现不了的。
其他
线程 B 怎么知道线程 A 修改了变量
while 轮询
谈谈对 BlockingQueue 的理解?分别有哪些实现类?
谈谈对 ConcurrentSkipListMap 的理解?
多线程不同步会出现的问题
public class threadNoSynchronization
{
public static void main( String[] args )
{
TestThread newThd = new TestThread();
// 启动了四个线程,实现了资源共享的目的
new Thread( newThd ).start();
new Thread( newThd ).start();
new Thread( newThd ).start();
new Thread( newThd ).start();
}
}
class TestThread implements Runnable
{
private int tickets = 20;
@Override
public void run()
{
while( tickets > 0 )
{
try
{
Thread.sleep( 100 );
}
catch( Exception e )
{
e.printStackTrace();
}
System.out.println( Thread.currentThread().getName() + "出售票" + tickets );
tickets -= 1;
}
}
}
//输出结果
Thread-2出售票20
Thread-1出售票19
Thread-0出售票18
Thread-3出售票17
Thread-0出售票16
Thread-1出售票15
Thread-2出售票14
Thread-3出售票13
Thread-2出售票12
Thread-1出售票12
Thread-0出售票10
Thread-3出售票9
Thread-3出售票8
Thread-1出售票8
Thread-2出售票8
Thread-0出售票8
Thread-1出售票4
Thread-2出售票4
Thread-3出售票4
Thread-0出售票4
Thread-1出售票0
Thread-2出售票-1
Thread-3出售票-2
//重复卖票或卖票出现负数的根本原因在于,没有对这些线程访问的临界资源(即多线程共享变量——tickets)做必要的同步控制。
总结
没什么好总结的