来自公众号: 捡田螺的小男孩
金九银十快要来了,整理了50道多线程并发面试题,大家可以点赞、收藏起来,慢慢品!~
1、为什么要使用多线程
选择多线程的原因,就是因为快。举个例子:
如果要 把1000块砖搬到楼顶,假设到楼顶有几个电梯,你觉得用一个电梯搬运快,还是同时用几个电梯同时搬运快呢?这个电梯就可以理解为线程。
所以,我们使用多线程就是因为: 在正确的场景下,设置恰当数目的线程,可以用来程提高序的运行速率。更专业点讲,就是充分地利用CPU和I/O的利用率,提升程序运行速率。
当然,有利就有弊,多线程场景下,我们要保证线程安全,就需要考虑加锁。加锁如果不恰当,就很很耗性能。
注意:compareAndSwapInt是一个native方法哈,它是基于CAS来操作int类型的变量。并且,其它的原子操作类基本也大同小异。
38 Java中用到的线程调度算法是什么?
我们知道有两种调度模型: 分时调度和抢占式调度。
-
分时调度模型:让所有的线程轮流获得cpu的使用权,并且平均分配每个线程占用的 CPU 的时间片。
-
抢占式调度:优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU。
Java默认的线程调度算法是抢占式。即线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。
-
shutdownNow能立即停止线程池,正在跑的和正在等待的任务都停下了。这样做立即生效,但是风险也比较大。
-
shutdown只是关闭了提交通道,用submit是无效的;而内部的任务该怎么跑还是怎么跑,跑完再彻底停止线程池。
-
核心线程数和最大线程数大小一样
-
非核心线程空闲存活时间为60秒
-
可以获取线程执行后的返回结果;
-
interrupt它是真正触发中断的方法。
-
isInterrupted是Thread类中的一个实例方法,可以判断实例线程是否被中断。。
-
DelayQueue 一个使用优先队列实现的无界阻塞队列。
-
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
-
优点:所有的线程都能得到资源,不会饿死在队列中。
-
缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
-
非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
-
优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
-
缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。
-
volatile关键字用来修饰共享变量,保证了共享变量的可见性,任何线程需要读取时都要到内存中读取(确保获得最新值)。
-
synchronized关键字确保只能同时有一个线程访问方法或者变量,保证了线程访问的可见性和排他性。
-
管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要 用于线程之间的数据传输,而传输的媒介为内存。
-
少用锁,应当缩小同步范围
-
多用并发集合少用同步集合
-
Java线程间通信方式讲解[2]
FixedThreadPool 适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。
当提交任务的速度大于处理任务的速度时,每次提交一个任务,就必然会创建一个线程。极端情况下会创建过多的线程,耗尽 CPU 和内存资源。由于空闲 60 秒的线程会被终止,长时间保持空闲的 CachedThreadPool 不会占用任何资源。
适用于串行执行任务的场景,一个任务一个任务地执行。
周期性执行任务的场景,需要限制线程数量的场景
FutureTask是一种可以取消的异步的计算任务。它的计算是通过Callable实现的,可以把它理解为是可以返回结果的Runnable。
它实现了Runnable接口和Future接口,底层基于生产者消费者模式实现。
FutureTask用于在异步操作场景中,FutureTask作为生产者(执行FutureTask的线程)和消费者(获取FutureTask结果的线程)的桥梁,如果生产者先生产出了数据,那么消费者get时能会直接拿到结果;如果生产者还未产生数据,那么get时会一直阻塞或者超时阻塞,一直到生产者产生数据唤醒阻塞的消费者为止。
43 有三个线程T1,T2,T3,怎么确保它们按顺序执行
可以使用join方法解决这个问题。 比如在线程A中,调用线程B的join方法表示的意思就是:A等待B线程执行完毕后(释放CPU执行权),在继续执行。
//主线程等待线程spring执行完,再往下执行
//主线程等待线程summer执行完,再往下执行
//主线程等待线程autumn执行完,再往下执行
并发度就是segment的个数,通常是2的N次方。默认是16
46 Java线程有哪些常用的调度方法?
long参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,线程自动转为就绪(Runnable)状态。
Object类中的wait方法,会导致当前的线程等待,直到其他线程调用此对象的notify方法或notifyAll唤醒方法。
Thread.yield方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
Object的notify方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。
notifyAll,则是唤醒在此对象监视器上等待的所有线程。
ReentrantLock,是可重入锁,是JDK5中添加在并发包下的一个高性能的工具。它支持同一个线程在未释放锁的情况下重复获取锁。
// 执行业务代码逻辑
47.2 什么是非公平锁,什么是公平锁?
ReentrantLock无参构造函数, 默认创建的是非公平锁,如下:
48. 线程间的通讯方式
等待/通知机制,是指一个线程A调用了对象O的wait方法进入等待状态,而另一个线程B 调用了对象O的notify或者notifyAll方法,线程A收到通知后从对象O的wait方法返回,进而 执行后续操作。
48.3 管道输入/输出流
时间里没有终止,那么将会从该超时方法中返回。
ThreadLocal,即线程本地变量(每个线程都有自己唯一的一个哦),是一个以ThreadLocal对象为键、任意对象为值的存储结构。底层是一个ThreadLocalMap来存储信息,key是弱引用,value是强引用,所以使用完毕后要及时清理(尤其使用线程池时)。
大家可以看下我之前这篇文章,ThreadLocal的八个关键知识点
49 写出3条你遵循的多线程最佳实践
50. 为什么阿里发布的 Java开发手册中强制线程池不允许使用 Executors 去创建?
这是因为,JDK开发者提供了线程池的实现类都是有坑的,如newFixedThreadPool和newCachedThreadPool都有内存泄漏的坑。大家可以看下我之前这篇文章哈