第01讲:为何说只有 1 种实现线程的方法?
1、创建线程实际只有一种方式
- 实现
Runnable接口,重写run()方法;实现Callable接口(有返回值),重写call()方法 - 继承
Thread类,重写run()方法;线程池
2、实现Runnable接口比继承Thread好
- 从代码架构考虑,实现了
Runnable与Thread类的解耦,Runnable是可执行内容,Thread负责线程的启动 Thread每执行一次任务,需要开启新的线程,执行完成后销毁;Runnable可直接传入线程池,不需要每次新建销毁线程,可复用Java不支持多继承,继承Thread限制了可拓展性
第02讲:如何正确停止线程?为什么 volatile 标记位的停止方法是错误的?
最正确的停止线程的方式是使用interrupt,Thread.currentThread().interrupt(),但interrupt仅仅起到通知被停止线程的作用。
Java希望程序间能够相互通知、相互协作地管理线程,贸然强制停止线程就可能会造成一些安全的问题
使用volatile标记位停止线程,如果线程休眠或阻塞无法及时感知标记信号,使用interrupt能及时感知中断信号,休眠或阻塞时抛出InterruptedException,同时清除中断信号
第03讲:线程是如何在 6 种状态之间转换的?
线程的 6 种状态
New(新创建)Runnable(可运行)Blocked(被阻塞)Waiting(等待)Timed Waiting(计时等待)Terminated(被终止)
第04讲:wait/notify/notifyAll 方法的使用注意事项?
为什么 wait 必须在 synchronized 保护的同步代码中使用?
因为while判断和wait方法必须是原子操作
第05讲:有哪几种实现生产者消费者模式的方法?
- BlockingQueue
- ReentrantLock.newCondition()
- Object.wait/notify
第06讲:一共有哪 3 类线程安全问题?
第07讲:哪些场景需要额外注意线程安全问题?
第08讲:为什么多线程会带来性能问题?
- 调度开销:上下文切换、缓存失效
- 协作开销
第09讲:使用线程池比手动创建线程好在哪里?
- 线程复用,减小线程的创建、销毁开销
- 统筹内存和CPU,控制线程数量,避免线程过多导致内存溢出,线程太少浪费资源
- 统一管理线程,管理任务
第10讲:线程池的各个参数的含义?
第11讲:线程池有哪 4 种拒绝策略?
AbortPolicy:拒绝时抛出RejectedExecutionException异常DiscardPolicy:直接丢弃新任务,有一定风险DiscardOldestPolicy:丢弃队列中最老的任务,为新任务让出空间,有一定风险CallerRunsPolicy:将任务交于提交任务的线程执行,谁提交,谁执行(不会被丢弃,提交任务线程被占用,减缓任务提交速度,相当于负反馈)
第13讲:线程池常用的阻塞队列有哪些?
LinkedBlockingQueue:容量Integer.MAX_VALUE,由于FixedThreadPool的线程数量固定,所需要没有容量限制的阻塞队列来存放任务SynchronousQueue:由于CachedThreadPool的最大线程数是Integer.MAX_VALUE,所以并不需要队列来存储任务DelayedWorkQueue:可延迟执行任务队列,内部按照时间对任务进行排序
第14讲:为什么不应该自动创建线程池?
第15讲:合适的线程数量是多少?CPU 核心数和线程数的关系?
第16讲:如何根据实际需要,定制自己的线程池?
第17讲:如何正确关闭线程池?shutdown 和 shutdownNow 的区别?
shutdown():调用后并不会立刻关闭,执行完当前任务和队列中的任务才真正关闭,新任务走拒绝策略shutdownNow():调用后首先会给所有线程发送interrupt中断信号,尝试中断任务,然后将队列中任务转移到List中返回isShutdown():是否执行了shutdown或shutdownNow方法,返回true并不代表真正关闭isTerminated():是否真正关闭了awaitTermination():尝试等待一定时间,如果等待期间内线程池真正关闭,返回true,否则返回false,等待期间线程被中断,抛出InterruptedException
第18讲:线程池实现“线程复用”的原理?
通过取Worker的firstTask或者getTask方法从workQueue中取出新任务,并直接调用Runnable的run方法来执行任务,每个线程都始终在一个大循环中,反复获取任务,执行任务,从而实现线程的复用
第19讲:你知道哪几种锁?分别有什么特点?
-
偏向锁/轻量级锁/重量级锁(特指
synchronized)- 偏向锁:如果自始至终,对于这把锁都不存在竞争,那么就没必要上锁,打个标记就行了,开销小,性能最好
- 轻量级锁:如果
synchronized中的代码被多个线程交替执行,不存在实际竞争,或者只有短时间的竞争,用自旋或者CAS就可以解决,不会陷入阻塞 - 重量级锁:重量级锁是互斥锁,利用操作系统的同步机制实现,开销较大
-
可重入锁/非可重入锁
可重入指当前线程已经持有这把锁了,能在不释放这把锁的情况下,再次获取这把锁,如
ReentrantLock -
共享锁/独占锁
共享锁指的是同一把锁可以被多个线程同时获得,独占锁指的是一把锁只能被一个线程获得,如读写锁
-
公平锁/非公平锁
公平锁先到先得
-
悲观锁/乐观锁
悲观锁获取资源前必须独占锁,乐观锁利用CAS理念,不会独占资源
-
自旋锁/非自旋锁
自旋锁指当线程拿不到锁时,不直接陷入阻塞或释放CPU资源,而是利用循环不停的尝试获取锁,非自旋锁拿不到锁就放弃,直接陷入阻塞等
-
可中断锁/不可中断锁
synchronized是不可中断锁,一旦申请了锁,只能等拿到锁才能进行其他操作,ReentrantLock是可中断锁,如果在申请过程中不想等待,使用lockInterruptibly获取锁可进行中断
第20讲:悲观锁和乐观锁的本质是什么?
悲观锁
悲观锁比较悲观,认为如果不锁住这个资源,别的线程就会争抢,造成数据错误,所以为了确保结果的正确性,每次获取并修改数据时,都把数据锁住(如synchronized关键字和Lock接口)
乐观锁比较乐观,认为在操作资源的时候不会有其他线程干扰,并不会锁住被操作对象,但为了确保数据的正确性,在更新之前,会对比数据有没有修改,如果没有修改,就说明只有自己在操作,可正常修改数据,如果修改了,就放弃本次修改(如原子类AtomicInteger)
第21讲:如何看到 synchronized 背后的“monitor 锁”?
第22讲:synchronized 和 Lock 孰优孰劣,如何选择?
相同点
都是用来保护资源线程安全的,都可以保证可见性,都可重入
不同点
用法不同,加解锁顺序不同,synchronized不够灵活,synchronized只能同时被一个线程拥有,synchronized不能设置公平非公平,原理不同,性能不同
第23讲:Lock 有哪几个常用方法?分别有什么用?
lock():最基础的获取锁的方法,在获取锁时如果锁已被其他线程获取,则进行等待tryLock():尝试获取锁,如果锁没有被其他线程占用,则获取成功,返回true,否则返回falsetryLock(long time, TimeUnit unit):多了等待时间lockInterruptibly():可响应中断获取锁unlock():释放锁
第24讲:讲一讲公平锁和非公平锁,为什么要“非公平”?
第25讲:读写锁 ReadWriteLock 获取锁有哪些规则?
有两把锁,读锁和写锁,读锁只能查看数据,可被多个线程同时持有,写锁可以读取和修改数据,只能被一个线程持有
要么是一个或多个线程同时有读锁,要么是一个线程有写锁,两者不会同时出现,可总结为:读读共享、其他都互斥
第26讲:读锁应该插队吗?什么是读写锁的升降级?
ReentrantReadWriteLock插队策略
- 公平策略:只要队列中有线程等待,
hasQueuedPredecessors()返回true,都不允许插队 - 非公平策略:如果允许读锁插队,读锁可以同时被多个线程持有,可能造成后续读锁一直插队成功,写锁一直等待,所以不允许读锁插队;写锁可以随时插队,因为写锁并不容易插队成功,写锁只有当没有其他线程持有读锁和写锁的时候,才能插队成功,同时写锁一旦插队失败就会进入等待队列,所以很难造成“饥饿”的情况,允许写锁插队是为了提高效率
第27讲:什么是自旋锁?自旋的好处和后果是什么呢?
自旋就是利用循环不停的尝试,直到目标达成
线程的阻塞和唤醒开销较大,如果使用自旋能在较短时间内获得锁,就提高了效率;但如果一直获取不到锁,浪费了时间和资源
第28讲:JVM 对锁进行了哪些优化?
- 自适应的自旋锁
- 锁消除
- 锁粗化
- 偏向锁/轻量级锁/重量级锁