目录
- 1.并发编程的优缺点
- 2.并发编程的目的
- 3.并发编程3要素
- 4.线程的安全问题产生的原因
- 5.解决多线程安全问题的方案
- 6.并发与并行
- 7.什么是多线程
- 8.多线程的优缺点
- 9.线程与进程
- 10.线程与进程的区别
- 11.用户线程和守护线程
- 12.死锁
- 13.死锁产生的四个必要条件
- 14.避免死锁
- 15.创建线程的方式
- 16.Runnable接口和Callable接口有何区别
- 17.run()方法和start()方法有和区别
- 18.为什么需要调用start()方法来执行run()方法
- 19.线程生命周期的6种状态
- 20.调度算法
- 21.Java线程同步和线程调度的相关方法
- 22.sleep()和wait()有什么区别
- 23.为什么线程通信方法wait(),notify(),notifyAll()要被定义到Object类中
- 24.为什么线程通信方法wait(),notify(),notifyAll()要在同步代码块或同步方法中被调用?
- 25.Thread的yiele方法
- 26.线程sleep和yield方法对比
- 27.如何停止一个线程
- 28.同步代码块和同步方法怎么选?
- 29.java中如何保证多线程安全
- 30.如何让多个线程按顺序执行
- 31.线程数过多会造成什么异常
- 32.Runnable 和 Thread 用哪个好?
- 33.线程池是什么
- 34.为什么使用线程池
- 35.线程池的核心属性
- 36.线程池运行流程及原理
- 37.线程池状态
- 38.线程池队列
- 39.队列注意事项
- 40.拒绝策略
- 41. 核心线程怎么实现一直存活?
- 42.非核心线程如何实现在 keepAliveTime 后死亡?
- 43.非核心线程能成为核心线程吗
- 44.如何终止线程池
- 45.Executors 提供了哪些创建线程池的方法?
- 46.ctl
- 47.ctl 为什么这么设计?有什么好处吗?
- 48.线程池大小如何配置
- 49.为什么不建议使用Executors创建线程池
- 50.程中断是否能直接调用stop,为什么?
- 51.线程的优先级
1.并发编程的优缺点
优点
- 充分发挥多核CPU的计算能力,提升性能
- 提高系统的并发能力及性能,方便业务拆分
缺点
会出现内存泄漏,线程安全,死锁等问题
2.并发编程的目的
提高程序的执行效率,提高程序运行速度
3.并发编程3要素
- 原子性
一个或多个操作要么全部执行成功,要么全部执行失败。 - 可见性
一个线程对共享变量的修改,另一个线程能看到 - 有序性
程序的执行顺序按照代码的先后顺序
4.线程的安全问题产生的原因
线程切换带来的原子性问题
缓存导致的可见性问题
编译优化带来的有序性问题
5.解决多线程安全问题的方案
- JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
- synchronized、volatile、LOCK,可以解决可见性问题
- Happens-Before 规则可以解决有序性问题
6.并发与并行
- 并发:多个任务在一个CPU上按照时间片轮转的方式交替执行
- 并行:多个任务在多个CPU上同时执行
7.什么是多线程
多线程是指程序中包含多个流,即在一个程序中可以同时进行多个不同的线程来执行不同的任务
8.多线程的优缺点
优点
- 提高CPU的利用率
缺点
- 需要一定内存
- 需要进行管理
- 共享资源会相互影响
9.线程与进程
- 线程
进程中最小的任务执行单位,一个进程可以有多个线程,一个线程只能属于一个进程,多个线程间可以共享数据 - 进程
内存中运行的程序,拥有独立的内存空间,可以拥有多个线程。
10.线程与进程的区别
- 根本区别
进程是操作系统资源分配的基本单位,线程是处理器任务调度的和执行的基本单位 - 资源开销
每个进程都有自己独立的代码和空间(程序上下文),程序之间的切换会有较大的开销;线程可以看作轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。 - 包含关系
一个进程可以拥有多个线程,由所有线程共同来完成任务,线程是进程的一部分,可以看作是轻量级的进程。 - 内存分配
同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的。
11.用户线程和守护线程
- 用户线程
由用户创建的,运行在前台执行具体任务。 - 守护线程
由系统自动创建的,运行在后台,为其他线程提供服务,所有用户线程执行完毕后,守护线程会跟随JVM一起结束。
main函数就是一个用户线程,main函数启动时,同时JVM还启动了好多的守护线程,如垃圾回收线程,比较明显的区别时,用户线程结束,JVM退出,不管这个时候有没有守护线程的运行,都不会影响JVM的退出。
12.死锁
死锁是指两个或两个以上进程(线程)在执行过程中,由于竞争资源或由于彼此通信造成的一种堵塞的现象,若无外力的作用下,都将无法推进,此时的系统处于死锁状态
13.死锁产生的四个必要条件
- 互斥条件
资源分配的排他性,一个资源只能被一个进程占用 - 请求与保持条件
一个进程(线程)因请求被占有资源而发生堵塞时,对已获取的资源保持不放。 - 不可剥夺条件
已获取的资源只有等自己使用完才释放资源。 - 循环等待条件
存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。
14.避免死锁
我们只需破坏形参死锁的四个必要条件之一即可。
- 破坏互斥条件
无法破坏,我们的🔒本身就是来个线程(进程)来产生互斥 - 破坏请求与保持条件
一次申请所有资源 - 破坏不剥夺条件
占有部分资源的线程尝试申请其它资源,如果申请不到,可以主动释放它占有的资源。 - 破坏循环等待条件
- 按序来申请资源。
1.减少同步共享变量
2.采用专门的算法,多个线程之间规定先后执行的顺序,规避死锁问题
3.减少锁的嵌套。
15.创建线程的方式
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
- Executors工具类创建线程池
16.Runnable接口和Callable接口有何区别
相同点
- 都是接口
- 用来创建多线程
- 使用Thread.start()启动线程
不同点
- 返回值
Runnable接口run方法无返回值,Callable接口call方法有返回值,是个泛型,和Futrue和FutureTask配合用来获取异步执行结果。 - 异常处理
Runable接口run方法只能抛出运行时的异常,且无法捕获处理;Callable接口call方法允许抛出异常,可以获取异常信息。
Callable接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会堵塞主线程继续往下执行,如果不调用就不会堵塞。
17.run()方法和start()方法有和区别
- run()方法
每个线程都是通过对某个特定的Thread对象重写run()方法来完成其操作的,run方法称为线程体,用于执行线程的运行代码,run()可以反复调用 - start()
start()方法来启动一个线程,只能被调用一次。
start()方法来启动一个线程,真正实现了多线程的运行。调用start()方法无需等待run()方法体代码执行结束,可以直接继续执行其它的代码;调用start()方法线程进入就绪状态,随时等该CPU的调度,然后可以通过Thread调用run()方法来让其进入运行状态,run()方法运行结束,此线程终止,然后CPU再调度其它线程。
18.为什么需要调用start()方法来执行run()方法
调用start方法启动线程可使线程进入就绪状态,等待运行;run方法只是thread的一个普通方法调用,还是在主线程里执行。
new Thread,线程进入了新建的状态,start方法的作用是使线程进入就绪的状态,当分配到时间片后就可以运行了。start方法会执行线程前的相应准备工作,然后在执行run方法运行线程体,这才是真正的多线程工作。
如果直接执行了run方法,run方法会被当作一个main线程下的普通方法执行,并不会在某个线程中去执行它,所以这并不是多线程工作。
19.线程生命周期的6种状态
在Thread源码中,State状态有六种,分别为
NEW(新建) 状态
RUNNABLE(可运行) 状态
WAITING(等待)状态
TIME_WAITING(超时等待) 状态
BLOCKED(阻塞) 状态
TERMINATED(终止)
线程中将就绪和运行中状态统称为可运行状态。
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
线程创建之后它将处于 NEW(新建) 状态,调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 cpu 时间片(timeslice)后就处于 RUNNING(运行) 状态。当线程执行 wait()方法之后,线程进入 WAITING(等待)状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 TIME_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)方法或 wait(long millis)方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。线程在执行 Runnable 的run()方法之后将会进入到 TERMINATED(终止) 状态。
20.调度算法
- 分时调度
让所有的线程轮流获得CPU的使用权,并且平均分配到各个线程占有CPU的时间片 - 抢占式调度
Java虚拟机采用抢占式调度模型,是指优先让线程池中优先级高的线程首先占用CPU,如果线程池中优先级相同,那么随机选择一个线程,使其占有CPU,处于这个状态的CPU会一直运行,优先级高的分的CPU的时间片相对会多一点。
不一定优先级高的就一定先执行
21.Java线程同步和线程调度的相关方法
- wait()
调用后线程进入等待对列,并释放锁 - sleep()
使线程进行休眠,不释放锁 - notify()
随机唤醒一个处于等待状态的线程 - notityAll()
唤醒所有处于等待状态的线程,让它们去竞争锁,谁先获取到锁,谁先进入就绪状态。
22.sleep()和wait()有什么区别
- 所属类不同
sleep()是Thread下的静态方法,wait()是Object类下的方法 - 是否释放锁
sleep()不释放锁,wait()释放锁 - 用法
wait()如果没有加入等待时间,就只能等待被唤醒,同时会释放锁,唤醒后进入阻塞状态,等待获取锁;sleep()在等待时间结束后自动苏醒,由于没有释放锁,苏醒后进入就绪状态。
23.为什么线程通信方法wait(),notify(),notifyAll()要被定义到Object类中
Java中任何对象都可以被当作锁对象,wait(),notify(),notifyAll()方法用于唤醒等待对象去获取锁,Java中没有提供任何对象使用的锁,但是任何对象都继承于Object类,所以定义在Object类中最合适。
24.为什么线程通信方法wait(),notify(),notifyAll()要在同步代码块或同步方法中被调用?
wait(),notify(),notifyAll()方法都有一个特点,就是对象去调用它们的时候必须持有锁对象。在调用的时候会释放自身的锁,等待下个线程来获取。
25.Thread的yiele方法
让出CPU进入就绪状态,等待CPU调度。
让出CPU后会进入就绪队列,进行新一轮的CPU竞争
26.线程sleep和yield方法对比
- sleep()阻塞时间结束后进入就绪状态,yield()直接进入就绪状态
- sleep()需要异常处理
- 都不会释放锁
27.如何停止一个线程
- 线程run()执行结束,正常退出
- 使用stop,或者 interrupt方法,都已经过期不建议使用
- 设置标记位记录线程状态
28.同步代码块和同步方法怎么选?
原则:锁的范围也小越好
同步代码块更容易对需要进行加锁的代码进行加锁操作,更为灵活,避免死锁产生的概率;
同步方法会对整个对象加锁。
29.java中如何保证多线程安全
- 使用集合类
比如 java.util.concurrent 下的类,使用原子类AtomicInteger - 加锁
synchronized锁,Lock
30.如何让多个线程按顺序执行
使用join()方法
31.线程数过多会造成什么异常
线程过多会造成栈溢出,也有可能会造成堆异常
32.Runnable 和 Thread 用哪个好?
Java 不支持类的多重继承,但允许你实现多个接口。所以如果你要继承其他类,也为了减少类之间的耦合性,Runnable 会更好。
33.线程池是什么
“线程池”,顾名思义就是一个线程缓存,线程是稀缺资源,如果被无限制的创建,不 仅会消耗系统资源,还会降低系统的稳定性,因此Java中提供线程池对线程进行统一分配、 调优和监控
34.为什么使用线程池
- 降低线程频繁创建销毁带来的资源消耗
- 提高任务到达时的响应速度
- 增加线程的可管理性,统一管理。
35.线程池的核心属性
- threadFactory(线程工厂)
创建线程 - corePoolSize(核心线程数)
当线程池运行的线程少于 corePoolSize 时,将创建一个新线程来处理请求,即使其他工作线程处于空闲状态。 - maximumPoolSize(最大线程数)
线程池允许开启的最大线程数。 - workQueue(队列)
用于保留任务并移交给工作线程的阻塞队列。 - keepAliveTime(保持存活时间)
线程池中核心线程之外线程的空闲存活时间 - handler(拒绝策略)
线程池和阻塞队列都满时,对新来线程的处理策略
36.线程池运行流程及原理
- 在创建了线程池后,开始等待请求。
- 当调用execute()方法添加一个请求任务时,线程池会做出如下判断:
- 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
- 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
- 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
- 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
- 当一个线程完成任务时,它会从队列中取下一个任务来执行。
- 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
- 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
- 所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。
37.线程池状态
- RUNNING
接受新任务并处理排队的任务。 - SHUTDOWN
不接受新任务,但处理排队的任务。 - STOP
不接受新任务,不处理排队的任务,并中断正在进行的任务。 - TIDYING
所有任务都已终止,workerCount 为零,线程转换到 TIDYING 状态将运行 terminated() 钩子方法。 - TERMINATED
terminated() 已完成。
38.线程池队列
- ArrayBlockingQueue
基于数组结构的有界阻塞队列,按先进先出对元素进行排序。 - LinkedBlockingQueue
基于链表结构的有界/无界阻塞队列,按先进先出对元素进行排序,吞吐量通常高于 ArrayBlockingQueue。 - SynchronousQueue
不是一个真正的队列,而是一种在线程之间移交的机制。新放入的元素必须有线程接收,如果没有线程接收将被拒绝。 - PriorityBlockingQueue
具有优先级的无界队列,按优先级对元素进行排序。元素的优先级是通过自然顺序或 Comparator 来定义的。
39.队列注意事项
- 有界队列
需要有拒绝策略 - 无界队列
如果任务的提交速度大于线程池的处理速度,可能会导致内存溢出。
40.拒绝策略
- AbortPolicy
终止策略,默认的拒绝策略,抛弃拒绝的任务并直接抛出 RejectedExecutionException。调用者可以捕获这个异常,然后根据需求编写自己的处理代码。 - DiscardPolicy
抛弃策略。什么都不做,直接抛弃被拒绝的任务。 - DiscardOldestPolicy
抛弃最老策略。抛弃阻塞队列中最老的任务
相当于就是队列中下一个将要被执行的任务,然后重新提交被拒绝的任务。如果阻塞队列是一个优先队列,那么“抛弃最旧的”策略将导致抛弃优先级最高的任务,因此最好不要将该策略和优先级队列放在一起使用。
- CallerRunsPolicy
调用者运行策略。将任务回退到调用者执行
不会抛弃任务,也不会抛出异常,而是将任务回退到调用者(调用线程池执行任务的主线程),由于执行任务需要一定时间,因此主线程至少在一段时间内不能提交任务,从而使得线程池有时间来处理完正在执行的任务。
41. 核心线程怎么实现一直存活?
核心线程在获取任务时,通过阻塞队列的 take() 方法实现的一直阻塞(存活)
42.非核心线程如何实现在 keepAliveTime 后死亡?
利用阻塞队列的方法,在获取任务时通过阻塞队列的 poll(time,unit) 方法实现的在延迟死亡。
43.非核心线程能成为核心线程吗
线程池内部是不区分核心线程和非核心线程的。只是根据当前线程池的工作线程数来进行调整,因此看起来像是有核心线程于非核心线程。
44.如何终止线程池
- shutdown
不接受新任务,但是在关闭前会将之前提交的任务处理完毕。 - shutdownNow
也就是直接关闭线程池,通过 Thread#interrupt() 方法终止所有线程,不会等待之前提交的任务执行完毕。但是会返回队列中未处理的任务。
45.Executors 提供了哪些创建线程池的方法?
- newFixedThreadPool
固定线程数的线程池。corePoolSize = maximumPoolSize,keepAliveTime为0,工作队列使用无界的LinkedBlockingQueue。适用于为了满足资源管理的需求,而需要限制当前线程数量的场景,适用于负载比较重的服务器。 - newSingleThreadExecutor
只有一个线程的线程池。corePoolSize = maximumPoolSize = 1,keepAliveTime为0, 工作队列使用无界的LinkedBlockingQueue。适用于需要保证顺序的执行各个任务的场景。 - newCachedThreadPool
按需要创建新线程的线程池。核心线程数为0,最大线程数为 Integer.MAX_VALUE,keepAliveTime为60秒,工作队列使用同步移交 SynchronousQueue。该线程池可以无限扩展,当需求增加时,可以添加新的线程,而当需求降低时会自动回收空闲线程。适用于执行很多的短期异步任务,或者是负载较轻的服务器。 - newScheduledThreadPool
创建一个以延迟或定时的方式来执行任务的线程池,工作队列为 DelayedWorkQueue。适用于需要多个后台线程执行周期任务。 - newWorkStealingPool
JDK 1.8 新增,用于创建一个可以窃取的线程池,底层使用 ForkJoinPool 实现。
FixedThreadPool,SingleThreadExecutor使用的无界队列作为线程池的工作队列,不会存在拒绝任务,只会将任务添加到等待队列中,可能会导致OOM。CachedThreadPool的maximumPoolSize被设置为Integer.MAX.VALUE,即它是无界的,这也就意味着如果主线程提交任务的速度高于maximumPool中线程处理任务的速度时,CachedThreadPool会不断创建新的线程。极端情况下,这样会导致耗尽cpu 和内存资源,CachedThreadPool允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM
46.ctl
用来检查线程池的状态,有效线程的数目,而它们都是基于一个整型变量来实现的。
有两部分组成
- workerCount:指线程的有效数量;
- runState:指线程池的运行状态,有 RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED 等状态。
int 类型有32位,其中 ctl 的低29为用于表示 workerCount,高3位用于表示 runState
当我们的线程池运行状态为 RUNNING,工作线程个数为3,则此时 ctl 的原码为:1010 0000 0000 0000 0000 0000 0000 0011
RUNNING -- 对应的高3位值是111。
SHUTDOWN -- 对应的高3位值是000。
STOP -- 对应的高3位值是001。
TIDYING -- 对应的高3位值是010。
TERMINATED -- 对应的高3位值是011。
47.ctl 为什么这么设计?有什么好处吗?
ctl 这么设计的主要好处是将对 runState 和 workerCount 的操作封装成了一个原子操作。runState 和 workerCount 是线程池正常运转中的2个最重要属性,线程池在某一时刻该做什么操作,取决于这2个属性的值。
因此无论是查询还是修改,我们必须保证对这2个属性的操作是属于“同一时刻”的,也就是原子操作,否则就会出现错乱的情况。如果我们使用2个变量来分别存储,要保证原子性则需要额外进行加锁操作,这显然会带来额外的开销,而将这2个变量封装成1个 AtomicInteger 则不会带来额外的加锁开销,而且只需使用简单的位操作就能分别得到 runState 和 workerCount。
workerCount 的上限 CAPACITY = (1 << 29) - 1,对应的二进制原码为:0001 1111 1111 1111 1111 1111 1111 1111(不用数了,29个1)。
通过 ctl 得到 runState,只需通过位操作:ctl & ~CAPACITY。
于是“~CAPACITY”的值为:1110 0000 0000 0000 0000 0000 0000 0000,只有高3位为1,与 ctl 进行 & 操作,结果为 ctl 高3位的值,也就是 runState。
通过 ctl 得到 workerCount 则更简单了,只需通过位操作:c & CAPACITY。
48.线程池大小如何配置
要想合理的配置线程池大小,首先我们需要区分任务是计算密集型还是I/O密集型。
对于计算密集型,设置 线程数 = CPU数 + 1,通常能实现最优的利用率。
对于I/O密集型,网上常见的说法是设置 线程数 = CPU数 * 2 ,这个做法是可以的,但个人觉得不是最优的。
在我们日常的开发中,我们的任务几乎是离不开I/O的,常见的网络I/O(RPC调用)、磁盘I/O(数据库操作),并且I/O的等待时间通常会占整个任务处理时间的很大一部分,在这种情况下,开启更多的线程可以让 CPU 得到更充分的使用,一个较合理的计算公式如下:
线程数 = CPU数 * CPU利用率 * (任务等待时间 / 任务计算时间 + 1)
例如我们有个定时任务,部署在4核的服务器上,该任务有100ms在计算,900ms在I/O等待,则线程数约为:4 * 1 * (1 + 900 / 100) = 40个。
当然,具体我们还要结合实际的使用场景来考虑。如果要求比较精确,可以通过压测来获取一个合理的值。
49.为什么不建议使用Executors创建线程池
因为默认封装的Executors创建线程池使用的是无界队列(没有最大值的队列)来存放缓存任务,无限缓存任务容易发生内存溢出,也会导致最大线程数失效,一直使用核心线程
50.程中断是否能直接调用stop,为什么?
不可以,stop方法是从外部强行终止一个线程,会导致不可预知的错误。如使用IO流时不能关流
51.线程的优先级
线程的优先级就是设置哪个线程优先执行,但也不是绝对的,只是让优先级高的线程优先运行的几率高一些。线程默认是NORM_PRIORITY = 5; 设置优先级使用的是setPriority()函数。