成品网站怎么新建网页全国疫情高峰时间表最新
多线程 进阶
- 常见的锁策略
- 1. 乐观锁 vs 悲观锁
- 同学请教问题
- Synchronized是乐观锁,但是当发现竞争频繁则悲观锁
- 如何检测数据访问是否冲突
- 2. 读写锁(两把锁 读读不互斥,其余才互斥)
- Synchronized 不是读写锁
- 3. 重量级锁 vs 轻量级锁(内核态 用户态)
- Synchronized是轻量级锁 冲突多 变成 重量级锁
- 4. 自旋锁(不放弃CPU,轻量)挂起等待锁(重量)
- 好比追女生,自旋锁一直问
- Synchronized就是通过自旋实现轻量级
- 5. 公平锁(先来后到) vs 非公平锁
- synchronized是非公平锁
- 6. 可重入锁 vs 不可重入锁
- 相关面试题
- 7. 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
- 8. 介绍下读写锁?
- 9. 什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?
- 10. synchronized 是可重入锁么?
- CAS
- 11. 什么是 CAS(B V A 乐观锁)
- 12. CAS 是怎么实现的
- 13. CAS 有哪些应用
- 1) 实现原子类
- 2) 实现自旋锁
- 14. CAS 的 ABA 问题
- 相关面试题
- 15. 讲解下你自己理解的 CAS 机制
- 16. ABA问题怎么解决?
- Synchronized 原理
- 17. 基本特点
- 18. 加锁工作过程
- 相关面试题
- 19. 什么是偏向锁?
- 20. synchronized 实现原理 是什么?
- Callable 接口
- 21. Callable 的用法
- 相关面试题
- 22. 介绍下 Callable 是什么
- JUC(java.util.concurrent) 的常见类
- 23. ReentrantLock
- 原子类 Atomic
- 线程池 ExecutorService 和 Executors
- 信号量 Semaphore
- CountDownLatch
- 相关面试题
- 1) 线程同步的方式有哪些?
- 2) 为什么有了 synchronized 还需要 juc 下的 lock?
- 3) AtomicInteger 的实现原理是什么?
- 4) 信号量听说过么?之前都用在过哪些场景下?
- 5) 解释一下 ThreadPoolExecutor 构造方法的参数的含义
- 线程安全的集合类
- 多线程环境使用 ArrayList
- 多线程环境使用队列
- 多线程环境使用哈希表
- 1) Hashtable
- 为什么 HashMap 线程不安全?
- 2) ConcurrentHashMap
- 相关面试题
- 1) ConcurrentHashMap的读是否要加锁,为什么?
- 2) 介绍下 ConcurrentHashMap的锁分段技术?
- 3) ConcurrentHashMap在jdk1.8做了哪些优化?
- 4) Hashtable和HashMap、ConcurrentHashMap 之间的区别?
- COncurrentHashMap为什么不能插入null
- 死锁
- 死锁是什么
- 如何避免死锁
- 相关面试题
- 谈谈死锁是什么,如何避免死锁,避免算法? 实际解决过没有?
- 8. 其他常见问题
- 面试题:
- 1) 谈谈 volatile关键字的用法?
- 2) Java多线程是如何实现数据共享的?
- 3) Java创建线程池的接口是什么?参数 LinkedBlockingQueue 的作用是什么?
- 4) Java线程共有几种状态?状态之间怎么切换的?
- 5) 在多线程下,如果对一个数进行叠加,该怎么做?
- 6) Servlet是否是线程安全的?
- 7) Thread和Runnable的区别和联系?
- 8) 多次start一个线程会怎么样
- 9) 有synchronized两个方法,两个线程分别同时用这个方法,请问会发生什么?
- 10) 进程和线程的区别?
- 面试题:并行和并发有什么区别?
- 问题:进程和线程有什么区别?
- 什么是进程
- 什么是线程
- 问题:多线程有什么优缺点?
- 问题:线程的创建方式有哪些?
- 问题:runnable 和 callable 有什么区别?
- 问题:线程有哪些状态?
- 问题:notify()和 notifyAll()有什么区别?
- 问题:如何简单的使用线程?
- 问题:用户线程和守护线程有什么区别?
- 问题:线程常用的方法有哪些?
- 问题:start 和 run 方法有什么区别?
- 问题:wait 方法和 sleep 方法有什么区别?
- 问题:说一下线程的生命周期?
- 问题:WAITTING 和 TIMED_WATTING 有什么区别?
- 问题:怎么终止线程?
- 问题:线程和线程池有什么区别?
- 问题:如何创建线程池?
- 问题:推荐使用哪种方式来创建线程池?
- 问题:说一下 ThreadPoolExecutor 的参数含义?
- 问题:说一下线程池的执行流程?
- 问题:线程池的拒绝策略都有哪些?
- 问题:如何实现自定义拒绝策略?
- 问题:如何实现线程池的扩展?
- 问题:线程池的状态和线程的状态一样吗?
- 问题:什么情况会导致线程池的状态发生改变?
- 问题:线程池中 shutdownNow() 和 shutdown() 有什么区别?
- 问题:多线程存在什么问题?
- 问题:为什么会有线程安全问题?
- 问题:如何解决线程安全问题?
- 问题:synchronized 有几种用法?
- 问题:synchronized 修饰静态方法和普通方法有什么区别吗?
- 问题:synchronized 底层是如何实现的?
- 问题:什么是监视器?
- 问题:说一下 synchronized 底层执行流程?
- 问题:说一下 synchronized 锁升级的流程?
- 问题:synchronized 自旋锁有什么优点?
- 问题:synchronized 和 ReentrantLock 有什么区别?
- 问题:ReentrantLock 注意事项有哪些?
- 问题:lock 和 lockInterruptibly 有什么区别?
- 问题:volatile 能保证线程安全吗?
- 问题:说一下指令重排序及其问题?(和单例模式的懒汉模式结合)
- 问题:volatile 使用场景有哪些?
- 问题:产生死锁的原因有哪些?
- 问题:如何解决死锁问题?
- 问题:常见的锁策略有哪些?
- 问题:乐观锁和 CAS 有什么区别?
- 问题:说一下 CAS 的执行流程?
- 问题:CAS 底层是如何实现的?
- 问题:CAS 有什么问题?
- 问题:怎么解决 ABA 问题?
- 问题:CountDownLatch 和 CyclicBarrier 有什么区别?
- 问题:Semaphore 有啥用?
- 问题:如何实现线程安全的单例模式?
- 问题:为什么 ThreadLocal 是线程安全的?
- 问题:为什么 ThreadLoal 会导致内存溢出?
- 问题:如何解决 ThreadLocal 内存溢出问题?
- 问题:ThreadLocal 如何实现多线程间的数据共享?
- 问题:ThreadLocal 和 Synchonized 有什么区别?
- 问题:线程通讯的方式
- 线程等待和唤醒的实现手段
- 如何停止线程
- 线程池的优点
- 如何停止线程池
- 如何判断线程池已经完成
- volatile底层如何实现
- volatile的两个作用
- 保证线程安全的手段
- synchronized 和 Lock有什么区别
- synchronized 底层如何实现
- synchronized 锁升级机制
- 产生死锁的条件
- 什么是AQS
- 2022-08-10_CPU工作原理_作业
- 2022-08-12_CPU工作原理等_作业
- 操作系统
- 操作系统如何进行进程调度
- 2022-08-13_线程概念等_作业
- 进程和线程的区别和联系
- 5种创建线程的方式
- 编写代码, 实现多线程数组求和.
- 2022-08-14_线程概念等_作业
- 在子线程执行完毕后再执行主线程代码(Thread数组)
- 请说明Thread类中run和start的区别
- java线程的状态
- 2022-09-06_线程安全_作业
- 以下可以完全解决线程安全问题的选项是?
- 使用两个线程来累加 count 的值
- 编写博客, 总结线程安全问题的原因和解决方案
- 2022-09-13_线程安全_作业
- 请描述 volatile 关键字的作用
- 顺序打印
- 2022-09-15_线程安全等_作业
- 顺序打印-进阶版
- 方法一:三个线程竞争同一个锁,通过count判断是否打印
- 方法二:三个线程同时start,分别上锁,从a开始,打印后唤醒b
- 简述 wait 和 sleep 有什么区别?
- 编写代码, 实现线程安全版本的单例模式
- 2022-09-18_多线程代码案例
- 简述线程池有什么优点?
- 编写代码, 实现阻塞队列案例
- 编写代码, 实现定时器案例
- 编写代码, 实现线程池案例
- 2022-09-20_多线程面试题
- 描述一下线程池的执行流程和拒绝策略有哪些?
- 使用ThreadPoolExecutor创建一个忽略最新任务的线程池
- 2022-09-25_多线程面试题
- 编写代码实现两个线程增加同一个变量
- 总结 HashTable, HashMap, ConcurrentHashMap 之间的区别
- 总结死锁的成因, 和解决方案.
- 简述 synchronized 和 ReentrantLock 之间的区别
- synchronized是什么锁
- 总结锁策略, cas 和 synchronized 优化过程
- 编写代码, 基于 Callable 实现 1+2+3+...+1000
- 编写代码, 基于 AtomicInteger 实现多线程自增同一个变量
- 编写代码, 基于 CountDownLatch 模拟跑步比赛的过程
常见的锁策略
1. 乐观锁 vs 悲观锁
悲观锁:
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
乐观锁:
假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。
同学请教问题
Synchronized是乐观锁,但是当发现竞争频繁则悲观锁
如何检测数据访问是否冲突
2. 读写锁(两把锁 读读不互斥,其余才互斥)
Synchronized 不是读写锁
3. 重量级锁 vs 轻量级锁(内核态 用户态)
Synchronized是轻量级锁 冲突多 变成 重量级锁
4. 自旋锁(不放弃CPU,轻量)挂起等待锁(重量)
好比追女生,自旋锁一直问
Synchronized就是通过自旋实现轻量级
5. 公平锁(先来后到) vs 非公平锁
synchronized是非公平锁
6. 可重入锁 vs 不可重入锁
相关面试题
7. 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
8. 介绍下读写锁?
9. 什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?
10. synchronized 是可重入锁么?
CAS
11. 什么是 CAS(B V A 乐观锁)
12. CAS 是怎么实现的
13. CAS 有哪些应用
1) 实现原子类
2) 实现自旋锁
14. CAS 的 ABA 问题
相关面试题
15. 讲解下你自己理解的 CAS 机制
16. ABA问题怎么解决?
Synchronized 原理
17. 基本特点
18. 加锁工作过程
相关面试题
19. 什么是偏向锁?
20. synchronized 实现原理 是什么?
Callable 接口
21. Callable 的用法
相关面试题
22. 介绍下 Callable 是什么
JUC(java.util.concurrent) 的常见类
23. ReentrantLock
原子类 Atomic
线程池 ExecutorService 和 Executors
信号量 Semaphore
CountDownLatch
相关面试题
1) 线程同步的方式有哪些?
2) 为什么有了 synchronized 还需要 juc 下的 lock?
3) AtomicInteger 的实现原理是什么?
4) 信号量听说过么?之前都用在过哪些场景下?
5) 解释一下 ThreadPoolExecutor 构造方法的参数的含义
线程安全的集合类
多线程环境使用 ArrayList
多线程环境使用队列
多线程环境使用哈希表
1) Hashtable
为什么 HashMap 线程不安全?
2) ConcurrentHashMap
参考资料:https://blog.csdn.net/u010723709/article/details/48007881
相关面试题
1) ConcurrentHashMap的读是否要加锁,为什么?
2) 介绍下 ConcurrentHashMap的锁分段技术?
3) ConcurrentHashMap在jdk1.8做了哪些优化?
4) Hashtable和HashMap、ConcurrentHashMap 之间的区别?
COncurrentHashMap为什么不能插入null
死锁
死锁是什么
如何避免死锁
相关面试题
谈谈死锁是什么,如何避免死锁,避免算法? 实际解决过没有?
8. 其他常见问题
面试题:
1) 谈谈 volatile关键字的用法?
2) Java多线程是如何实现数据共享的?
3) Java创建线程池的接口是什么?参数 LinkedBlockingQueue 的作用是什么?
4) Java线程共有几种状态?状态之间怎么切换的?
5) 在多线程下,如果对一个数进行叠加,该怎么做?
6) Servlet是否是线程安全的?
7) Thread和Runnable的区别和联系?
8) 多次start一个线程会怎么样
9) 有synchronized两个方法,两个线程分别同时用这个方法,请问会发生什么?
10) 进程和线程的区别?
多线程面试题大总结
- 常见的锁策略
- 1. 乐观锁 vs 悲观锁
- 同学请教问题
- Synchronized是乐观锁,但是当发现竞争频繁则悲观锁
- 如何检测数据访问是否冲突
- 2. 读写锁(两把锁 读读不互斥,其余才互斥)
- Synchronized 不是读写锁
- 3. 重量级锁 vs 轻量级锁(内核态 用户态)
- Synchronized是轻量级锁 冲突多 变成 重量级锁
- 4. 自旋锁(不放弃CPU,轻量)挂起等待锁(重量)
- 好比追女生,自旋锁一直问
- Synchronized就是通过自旋实现轻量级
- 5. 公平锁(先来后到) vs 非公平锁
- synchronized是非公平锁
- 6. 可重入锁 vs 不可重入锁
- 相关面试题
- 7. 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
- 8. 介绍下读写锁?
- 9. 什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?
- 10. synchronized 是可重入锁么?
- CAS
- 11. 什么是 CAS(B V A 乐观锁)
- 12. CAS 是怎么实现的
- 13. CAS 有哪些应用
- 1) 实现原子类
- 2) 实现自旋锁
- 14. CAS 的 ABA 问题
- 相关面试题
- 15. 讲解下你自己理解的 CAS 机制
- 16. ABA问题怎么解决?
- Synchronized 原理
- 17. 基本特点
- 18. 加锁工作过程
- 相关面试题
- 19. 什么是偏向锁?
- 20. synchronized 实现原理 是什么?
- Callable 接口
- 21. Callable 的用法
- 相关面试题
- 22. 介绍下 Callable 是什么
- JUC(java.util.concurrent) 的常见类
- 23. ReentrantLock
- 原子类 Atomic
- 线程池 ExecutorService 和 Executors
- 信号量 Semaphore
- CountDownLatch
- 相关面试题
- 1) 线程同步的方式有哪些?
- 2) 为什么有了 synchronized 还需要 juc 下的 lock?
- 3) AtomicInteger 的实现原理是什么?
- 4) 信号量听说过么?之前都用在过哪些场景下?
- 5) 解释一下 ThreadPoolExecutor 构造方法的参数的含义
- 线程安全的集合类
- 多线程环境使用 ArrayList
- 多线程环境使用队列
- 多线程环境使用哈希表
- 1) Hashtable
- 为什么 HashMap 线程不安全?
- 2) ConcurrentHashMap
- 相关面试题
- 1) ConcurrentHashMap的读是否要加锁,为什么?
- 2) 介绍下 ConcurrentHashMap的锁分段技术?
- 3) ConcurrentHashMap在jdk1.8做了哪些优化?
- 4) Hashtable和HashMap、ConcurrentHashMap 之间的区别?
- COncurrentHashMap为什么不能插入null
- 死锁
- 死锁是什么
- 如何避免死锁
- 相关面试题
- 谈谈死锁是什么,如何避免死锁,避免算法? 实际解决过没有?
- 8. 其他常见问题
- 面试题:
- 1) 谈谈 volatile关键字的用法?
- 2) Java多线程是如何实现数据共享的?
- 3) Java创建线程池的接口是什么?参数 LinkedBlockingQueue 的作用是什么?
- 4) Java线程共有几种状态?状态之间怎么切换的?
- 5) 在多线程下,如果对一个数进行叠加,该怎么做?
- 6) Servlet是否是线程安全的?
- 7) Thread和Runnable的区别和联系?
- 8) 多次start一个线程会怎么样
- 9) 有synchronized两个方法,两个线程分别同时用这个方法,请问会发生什么?
- 10) 进程和线程的区别?
- 面试题:并行和并发有什么区别?
- 问题:进程和线程有什么区别?
- 什么是进程
- 什么是线程
- 问题:多线程有什么优缺点?
- 问题:线程的创建方式有哪些?
- 问题:runnable 和 callable 有什么区别?
- 问题:线程有哪些状态?
- 问题:notify()和 notifyAll()有什么区别?
- 问题:如何简单的使用线程?
- 问题:用户线程和守护线程有什么区别?
- 问题:线程常用的方法有哪些?
- 问题:start 和 run 方法有什么区别?
- 问题:wait 方法和 sleep 方法有什么区别?
- 问题:说一下线程的生命周期?
- 问题:WAITTING 和 TIMED_WATTING 有什么区别?
- 问题:怎么终止线程?
- 问题:线程和线程池有什么区别?
- 问题:如何创建线程池?
- 问题:推荐使用哪种方式来创建线程池?
- 问题:说一下 ThreadPoolExecutor 的参数含义?
- 问题:说一下线程池的执行流程?
- 问题:线程池的拒绝策略都有哪些?
- 问题:如何实现自定义拒绝策略?
- 问题:如何实现线程池的扩展?
- 问题:线程池的状态和线程的状态一样吗?
- 问题:什么情况会导致线程池的状态发生改变?
- 问题:线程池中 shutdownNow() 和 shutdown() 有什么区别?
- 问题:多线程存在什么问题?
- 问题:为什么会有线程安全问题?
- 问题:如何解决线程安全问题?
- 问题:synchronized 有几种用法?
- 问题:synchronized 修饰静态方法和普通方法有什么区别吗?
- 问题:synchronized 底层是如何实现的?
- 问题:什么是监视器?
- 问题:说一下 synchronized 底层执行流程?
- 问题:说一下 synchronized 锁升级的流程?
- 问题:synchronized 自旋锁有什么优点?
- 问题:synchronized 和 ReentrantLock 有什么区别?
- 问题:ReentrantLock 注意事项有哪些?
- 问题:lock 和 lockInterruptibly 有什么区别?
- 问题:volatile 能保证线程安全吗?
- 问题:说一下指令重排序及其问题?(和单例模式的懒汉模式结合)
- 问题:volatile 使用场景有哪些?
- 问题:产生死锁的原因有哪些?
- 问题:如何解决死锁问题?
- 问题:常见的锁策略有哪些?
- 问题:乐观锁和 CAS 有什么区别?
- 问题:说一下 CAS 的执行流程?
- 问题:CAS 底层是如何实现的?
- 问题:CAS 有什么问题?
- 问题:怎么解决 ABA 问题?
- 问题:CountDownLatch 和 CyclicBarrier 有什么区别?
- 问题:Semaphore 有啥用?
- 问题:如何实现线程安全的单例模式?
- 问题:为什么 ThreadLocal 是线程安全的?
- 问题:为什么 ThreadLoal 会导致内存溢出?
- 问题:如何解决 ThreadLocal 内存溢出问题?
- 问题:ThreadLocal 如何实现多线程间的数据共享?
- 问题:ThreadLocal 和 Synchonized 有什么区别?
- 问题:线程通讯的方式
- 线程等待和唤醒的实现手段
- 如何停止线程
- 线程池的优点
- 如何停止线程池
- 如何判断线程池已经完成
- volatile底层如何实现
- volatile的两个作用
- 保证线程安全的手段
- synchronized 和 Lock有什么区别
- synchronized 底层如何实现
- synchronized 锁升级机制
- 产生死锁的条件
- 什么是AQS
- 2022-08-10_CPU工作原理_作业
- 2022-08-12_CPU工作原理等_作业
- 操作系统
- 操作系统如何进行进程调度
- 2022-08-13_线程概念等_作业
- 进程和线程的区别和联系
- 5种创建线程的方式
- 编写代码, 实现多线程数组求和.
- 2022-08-14_线程概念等_作业
- 在子线程执行完毕后再执行主线程代码(Thread数组)
- 请说明Thread类中run和start的区别
- java线程的状态
- 2022-09-06_线程安全_作业
- 以下可以完全解决线程安全问题的选项是?
- 使用两个线程来累加 count 的值
- 编写博客, 总结线程安全问题的原因和解决方案
- 2022-09-13_线程安全_作业
- 请描述 volatile 关键字的作用
- 顺序打印
- 2022-09-15_线程安全等_作业
- 顺序打印-进阶版
- 方法一:三个线程竞争同一个锁,通过count判断是否打印
- 方法二:三个线程同时start,分别上锁,从a开始,打印后唤醒b
- 简述 wait 和 sleep 有什么区别?
- 编写代码, 实现线程安全版本的单例模式
- 2022-09-18_多线程代码案例
- 简述线程池有什么优点?
- 编写代码, 实现阻塞队列案例
- 编写代码, 实现定时器案例
- 编写代码, 实现线程池案例
- 2022-09-20_多线程面试题
- 描述一下线程池的执行流程和拒绝策略有哪些?
- 使用ThreadPoolExecutor创建一个忽略最新任务的线程池
- 2022-09-25_多线程面试题
- 编写代码实现两个线程增加同一个变量
- 总结 HashTable, HashMap, ConcurrentHashMap 之间的区别
- 总结死锁的成因, 和解决方案.
- 简述 synchronized 和 ReentrantLock 之间的区别
- synchronized是什么锁
- 总结锁策略, cas 和 synchronized 优化过程
- 编写代码, 基于 Callable 实现 1+2+3+...+1000
- 编写代码, 基于 AtomicInteger 实现多线程自增同一个变量
- 编写代码, 基于 CountDownLatch 模拟跑步比赛的过程
面试题:并行和并发有什么区别?
并行:多个处理器或多核处理器同时处理多个任务。
并发:多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。
问题:进程和线程有什么区别?
什么是进程
参考答案:进程(Process)是操作系统分配资源的基本单位,一个进程拥有的资源有自己的堆、栈、虚存空间(页表)、文件描述符等信息。
从编程的角度来理解进程,可以把它看作是一个类或一个 PCB(Process Control Block)进程控制块的结构体,这个结构体中大致包含以下几个内容:
什么是线程
线程(Thread)是操作系统能够进行运算调度的基本单位。它包含在进程中,是进程中的实际运行单位。在 Unix System V 及 SunOS 中线程也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。
线程是轻量级的进程,一个进程中包含了多个线程,因此多个线程间可以共享进程资源,线程和进程的关系如下图所示:
其中,堆和方法区是可以共享的区域,而程序计数器和栈是每个线程私有的。
- 程序计数器是一块内存区域,用来记录线程当前要执行的指令地址;
- 栈是用来记录每个线程自己的局部变量的;
- 堆中存放的是当前程序创建的所有对象;
- 方法区存放的是常量和静态变量等信息。
进程和线程的区别主要体现在以下几点:
- 从属关系不同:进程是正在运行程序的实例,进程中包含了线程,而线程中不能包含进程;
- 描述侧重点不同:进程是操作系统分配资源的基本单位,而线程是操作系统调度的基本单位;
- 共享资源不同:多个进程间不能共享资源,每个进程有自己的堆、栈、虚存空间(页表)、文件描述符等信息,而线程可以共享进程资源文件(堆和方法区);
- 上下文切换速度不同:线程上下文切换速度很快(上下文切换指的是从一个线程切换到另一个线程),而进程的上下文切换的速度比较慢;
- 操纵者不同:一般情况下进程的操纵者是操作系统,而线程的操纵者是编程人员。
- 创建和销毁不同
小结:简单来说,进程是操作系统分配资源的基本单位,而线程是操作系统调度的基本单位;一个进程中至少包含一个线程,线程不能独立于进程而存在;进程不能共享资源,而线程可以;线程可以看作是轻量级的进程,它们的主要区别体现在:从属关系、描述侧重点、共享资源、上下文切换速度和操纵对象等不同,线程可以看作是一个轻量级的“进程”。
问题:多线程有什么优缺点?
参考答案:多线程的优点是可以提高程序的执行性能。例如,有个 90 平方的房子,一个人打扫需要花费 30 分钟,三个人打扫就只需要 10 分钟,这三个人就是程序中的“多线程”。
多线程的缺点是它带来了编码的复杂度,并且带来了**线程安全性问题,**也就是程序的执行不符合预期结果的问题。
问题:线程的创建方式有哪些?
参考答案:线程的创建,分为以下三种方式:
继承 Thread 类,重写 run 方法;
实现 Runnable 接口,实现 run 方法;
实现 Callable 接口,实现 call 方法。
问题:runnable 和 callable 有什么区别?
在Java中,Runnable和Callable都是用于多线程编程的接口,但它们有以下几个区别:
- 方法不同
Runnable接口只有一个run()方法,该方法不返回任何值,因此无法抛出任何checked Exception。
Callable接口则有一个call()方法,它可以返回一个值,并且可以抛出一个checked Exception。
- 返回值不同
Runnable的run()方法没有返回值,只是一个void类型的方法。
Callable的call()方法却必须有一个返回值,并且返回值的类型可以通过泛型进行指定。
- 异常处理不同
在Runnable中,我们无法对run()方法抛出的异常进行任何处理。
但在Callable中,自定义的call()方法可以抛出一个checked Exception,并由其执行者Handler进行捕获并处理。
- 使用场景不同
Runnable适用于那些不需要返回值,且不会抛出checked Exception的情况,比如简单的打印输出或者修改一些共享的变量。
Callable适用于那些需要返回值或者需要抛出checked Exception的情况,比如对某个任务的计算结果进行处理,或者需要进行网络或IO操作等。在Java中,常常使用Callable来实现异步任务的处理,以提高系统的吞吐量和响应速度。
问题:线程有哪些状态?
问题:notify()和 notifyAll()有什么区别?
一、区别
notify()和notifyAll()都是用来用来唤醒调用wait()方法进入等待锁资源队列的线程,区别在于:
notify()
唤醒正在等待此对象监视器的单个线程。 如果有多个线程在等待,则选择其中一个随机唤醒(由调度器决定),唤醒的线程享有公平竞争资源的权利
notifyAll()
唤醒正在等待此对象监视器的所有线程,唤醒的所有线程公平竞争资源
二、示例
notify()
public class ThreadDemo implements Runnable{static Object lock = new Object();public static void main(String[] args) {Thread thread1 = new Thread(new ThreadDemo(), "Thread-a");Thread thread2 = new Thread(new ThreadDemo(), "Thread-b");thread1.start();thread2.start();try {TimeUnit.SECONDS.sleep(1); // 睡会,让走到子线程} catch (InterruptedException e) {e.printStackTrace();}synchronized (lock) {System.out.println(Thread.currentThread().getName() + " 获得了锁");System.out.println("notify前" + thread1.getName() + "状态是" + thread1.getState());System.out.println("notify前" + thread2.getName() + "状态是 " + thread2.getState());lock.notify(); // 唤醒一个等待lock锁的线程}try {TimeUnit.MILLISECONDS.sleep(300); // 睡会,让被唤醒的子线程走完} catch (InterruptedException e) {e.printStackTrace();}System.out.println("notify后" + thread1.getName() + "状态是" + thread1.getState());System.out.println("notify后" + thread2.getName() + "状态是" + thread2.getState());}@Overridepublic void run() {synchronized (lock) {System.out.println(Thread.currentThread().getName() + " 获得了锁");try {lock.wait(); // 等待被唤醒} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " end");}}
}
结果:
可以看到,子线程在wait()后释放了锁并进入WAITTINT状态,主线程获得锁后调用notify(),这时候其中一个线程(栗子中是Thread-b,也有可能是a)被唤醒并获取到锁继续执行到TERMINATED,而另一个线程a一直WAITTINT状态
Thread-b 获得了锁
Thread-a 获得了锁
main 获得了锁
notify前Thread-a状态是WAITING
notify前Thread-b状态是 WAITING
Thread-b end
notify后Thread-a状态是WAITING
notify后Thread-b状态是TERMINATED
notifyAll()
上面的栗子如果使用notifyAll(),看下结果
public class ThreadDemo implements Runnable{static Object lock = new Object();public static void main(String[] args) {Thread thread1 = new Thread(new ThreadDemo(), "Thread-a");Thread thread2 = new Thread(new ThreadDemo(), "Thread-b");thread1.start();thread2.start();try {TimeUnit.SECONDS.sleep(1); // 睡会,让走到子线程} catch (InterruptedException e) {e.printStackTrace();}System.out.println("哈哈哈哈");synchronized (lock) {System.out.println(Thread.currentThread().getName() + " 获得了锁");System.out.println("notify前" + thread1.getName() + "状态是" + thread1.getState());System.out.println("notify前" + thread2.getName() + "状态是 " + thread2.getState());lock.notifyAll(); // 唤醒所有等待lock锁的线程}try {TimeUnit.MILLISECONDS.sleep(300); // 睡会,让被唤醒的子线程走完} catch (InterruptedException e) {e.printStackTrace();}System.out.println("notify后" + thread1.getName() + "状态是" + thread1.getState());System.out.println("notify后" + thread2.getName() + "状态是" + thread2.getState());}@Overridepublic void run() {synchronized (lock) {System.out.println(Thread.currentThread().getName() + " 获得了锁");try {lock.wait(); // 等待被唤醒} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " end");}}
}
结果,可以看到所有子线程都被唤醒并再次公平竞争锁直到线程终止
Thread-b 获得了锁
Thread-a 获得了锁
哈哈哈哈
main 获得了锁
notify前Thread-a状态是WAITING
notify前Thread-b状态是 WAITING
Thread-a end
Thread-b end
notify后Thread-a状态是TERMINATED
notify后Thread-b状态是TERMINATED
问题:如何简单的使用线程?
参考答案:JDK 8 之后可以使用 Lambda 表达式很方便地创建线程,请参考以下代码:
new Thread(() -> System.out.println("Lambda Of Thread.")).start();
问题:用户线程和守护线程有什么区别?
参考答案:在 Java 语言中,线程分为两类:用户线程和守护线程,默认情况下我们创建的线程或线程池都是用户线程,所以用户线程也被称之为普通线程。
想要查看线程到底是用户线程还是守护线程,可以通过 Thread.isDaemon() 方法来判断,如果返回的结果是 true 则为守护线程,反之则为用户线程。
守护线程(Daemon Thread)也被称之为后台线程或服务线程,守护线程是为用户线程服务的,当程序中的用户线程全部执行结束之后,守护线程也会跟随结束。
守护线程的角色就像“服务员”,而用户线程的角色就像“顾客”,当“顾客”全部走了之后(全部执行结束),那“服务员”(守护线程)也就没有了存在的意义,所以当一个程序中的全部用户线程都结束执行之后,那么无论守护线程是否还在工作都会随着用户线程一块结束,整个程序也会随之结束运行。
小结:默认情况下我们创建的线程或线程池都是用户线程,守护线程是为用户线程服务的,当一个程序中的所有用户线程都执行完成之后程序就会结束运行,程序结束运行时不会管守护线程是否正在运行,由此我们可以看出守护线程在 Java 体系中权重是比较低的,这就是守护线程和用户线程的区别。
问题:线程常用的方法有哪些?
- start():启动线程;
- wait():实现线程等待;
- notify()/notifyAll():唤醒线程;
- sleep(xxx):带有结束时间让线程休眠的方法;
- yield():交出 CPU 执行权。
问题:start 和 run 方法有什么区别?
参考答案:
作用功能不同:
- run方法的作用是描述线程具体要执行的任务
- start方法的作用是直正的去申请系统线程
运行结果不同:
- run方法是一个类中的普通方法,主动调用和调用普通方法一样,会顺序执行一次;
- start调用方法后,start方法内部会调用Java 本地方法 (封装了对系统底层的调用)真正的启动线程,并执行run方法中的代码,run 方法执行完成后线程进入销毁阶段。
- 调用次数不同:run 方法可以被重复调用,而 start 方法只能被调用一次。
start 方法之所以不能被重复调用的原因是,线程的状态是不可逆的,Thread 在 start 的实现源码中做了判断,如果线程不是新建状态 NEW,则会抛出非法线程状态异常 IllegalThreadStateException。
问题:wait 方法和 sleep 方法有什么区别?
参考答案:sleep 和 wait 方法都可以让线程进入休眠状态,但二者是完全不同的,它们的区别主要体现在以下几点。
① 使用语法不同
wait 方法必须配合 synchronized 一起使用,不然在运行时就会抛出 IllegalMonitorStateException 的异常,如下代码所示:
初看代码好像没啥问题,编译器也没报错,然而当我们运行以上程序时就会发生如下错误:
而 sleep 可以单独使用,无需配合 synchronized 一起使用。
② 所属类不同
wait 方法属于 Object 类的方法,而 sleep 属于 Thread 类的方法。
③ 唤醒方式不同
wait 方法执行时会主动的释放锁,而 sleep 方法则不会释放锁。
问题:说一下线程的生命周期?
参考答案:线程的生命周期指的是线程从创建到销毁的整个过程,通常情况下线程的生命周期有以下 5 种:
- 初始状态
- 可运行状态
- 运行状态
- 休眠状态
- 终止状态
它们的状态转换如下图所示:
Java 线程的生命周期和上面说的生命周期是不同的,它有以下 6 种状态:
- NEW(初始化状态)
- RUNNABLE(可运行/运行状态)
- BLOCKED(阻塞状态)
- WAITING(无时限等待状态)
- TIMED_WAITING(有时限等待状态)
- TERMINATED(终止状态)
我们可以在 Thread 的源码中可以找到这 6 种状态,如下所示:
当然你也可以使用 Java 代码,来打印所有的线程状态,如下代码所示:
for (Thread.State value : Thread.State.values()) {System.out.println(value);
}
以上程序的执行结果如下图所示:
Java 中线程的生命周期有 6 种:NEW(初始化状态)、RUNNABLE(可运行/运行状态)、BLOCKED(阻塞状态)、WAITING(无时限等待状态)、TIMED_WAITING(有时限等待状态)、TERMINATED(终止状态)。线程生命周期的转换流程如下图所示:
问题:WAITTING 和 TIMED_WATTING 有什么区别?
参考答案:WAITING 无时限等待状态,比如调用 wait() 方法;而 TIMED_WATTING 是有超时时间的等待方法时,如 sleep(1000),线程会从 RUNNABLE 状态变成 TIMED_WAITING 有时限状态。
问题:怎么终止线程?
参考答案:在 Java 中终止线程的实现方法有以下 3 种:
- 自定义中断标识符的停止方法,此方法的缺点是不能及时响应中断请求;
- 使用 interrupt 中断线程方法,此方法是发送一个中断信号给线程,它可以及时响应中断,也是最推荐使用的方法;
- 最后是 stop 方法,虽然它也可以停止线程,但此方法已经是过时的不建议使用的方法,在 Java 最新版本中已经被直接移除了,所以不建议使用。
问题:线程和线程池有什么区别?
参考答案: 线程池(Thread Pool):把一个或多个线程通过统一的方式进行调度和重复使用的技术,避免了因为线程过多而带来使用上的开销。
相比于线程来说,线程池具备以下优点:
- 可重复使用已有线程,避免对象创建、消亡和过度切换的性能开销。
- 避免创建大量同类线程所导致的资源过度竞争和内存溢出的问题。
- 支持更多功能,比如延迟任务线程池(newScheduledThreadPool)和缓存线程池(newCachedThreadPool)等。
问题:如何创建线程池?
参考答案:线程池的创建可以分为以下两类:
- 通过 ThreadPoolExecutor 手动创建线程池。
- 通过 Executors 执行器自动创建线程池。
而以上两类创建线程池的方式,又有 7 种具体实现方法,这 7 种实现方法分别是:
- Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待。
- Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程。
- Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序。
- Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池。
- Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池。
- Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】。
- ThreadPoolExecutor:手动创建线程池的方式,它创建时最多可以设置 7 个参数。
问题:推荐使用哪种方式来创建线程池?
参考答案:推荐使用 ThreadPoolExecutor 的方式来创建线程池,这样的处理方式让写的读者更加明确线程池的运行规则,规避资源耗尽的风险。
Executors 创建线程池的弊端如下:
FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM;
CachedThreadPool 和 ScheduledThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
问题:说一下 ThreadPoolExecutor 的参数含义?
参考答案:ThreadPoolExecutor 最多支持 7 个参数的设置,如下代码所示:
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler) {if (corePoolSize < 0 ||// maximumPoolSize 必须大于 0,且必须大于 corePoolSizemaximumPoolSize <= 0 ||maximumPoolSize < corePoolSize ||keepAliveTime < 0)throw new IllegalArgumentException();if (workQueue == null || threadFactory == null || handler == null)throw new NullPointerException();this.acc = System.getSecurityManager() == null ?null :AccessController.getContext();this.corePoolSize = corePoolSize;this.maximumPoolSize = maximumPoolSize;this.workQueue = workQueue;this.keepAliveTime = unit.toNanos(keepAliveTime);this.threadFactory = threadFactory;this.handler = handler;
}
这 7 个参数分别是:
- corePoolSize:核心线程数。
- maximumPoolSize:最大线程数。
- keepAliveTime:空闲线程存活时间。
- TimeUnit:时间单位。
- BlockingQueue:线程池任务队列。
- ThreadFactory:创建线程的工厂。
- RejectedExecutionHandler:拒绝策略。
参数1:corePoolSize
核心线程数:是指线程池中长期存活的线程数。
这就好比古代大户人家,会长期雇佣一些“长工”来给他们干活,这些人一般比较稳定,无论这一年的活多活少,这些人都不会被辞退,都是长期生活在大户人家的。
参数2:maximumPoolSize
最大线程数:线程池允许创建的最大线程数量,当线程池的任务队列满了之后,可以创建的最大线程数。
这是古代大户人家最多可以雇佣的人数,比如某个节日或大户人家有人过寿时,因为活太多,仅靠“长工”是完不成任务,这时就会再招聘一些“短工”一起来干活,这个最大线程数就是“长工”+“短工”的总人数,也就是招聘的人数不能超过 maximumPoolSize。
注意事项
最大线程数 maximumPoolSize 的值不能小于核心线程数 corePoolSize,否则在程序运行时会报 IllegalArgumentException 非法参数异常,如下图所示:
参数3:keepAliveTime
空闲线程存活时间,当线程池中没有任务时,会销毁一些线程,销毁的线程数=maximumPoolSize(最大线程数)-corePoolSize(核心线程数)。
还是以大户人家为例,当大户人家比较忙的时候就会雇佣一些“短工”来干活,但等干完活之后,不忙了,就会将这些“短工”辞退掉,而 keepAliveTime 就是用来描述没活之后,短工可以在大户人家待的(最长)时间。
参数4:TimeUnit
时间单位:空闲线程存活时间的描述单位,此参数是配合参数 3 使用的。
参数 3 是一个 long 类型的值,比如参数 3 传递的是 1,那么这个 1 表示的是 1 天?还是 1 小时?还是 1 秒钟?是由参数 4 说了算的。
TimeUnit 有以下 7 个值:
TimeUnit.DAYS:天
TimeUnit.HOURS:小时
TimeUnit.MINUTES:分
TimeUnit.SECONDS:秒
TimeUnit.MILLISECONDS:毫秒
TimeUnit.MICROSECONDS:微妙
TimeUnit.NANOSECONDS:纳秒
参数5:BlockingQueue
阻塞队列:线程池存放任务的队列,用来存储线程池的所有待执行任务。
它可以设置以下几个值:
- ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
- SynchronousQueue:一个不存储元素的阻塞队列,即直接提交给线程不保持它们。
- PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
- DelayQueue:一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素。
- LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。与SynchronousQueue类似,还含有非阻塞方法。
- LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
比较常用的是 LinkedBlockingQueue,线程池的排队策略和 BlockingQueue 息息相关。
参数6:ThreadFactory
线程工厂:线程池创建线程时调用的工厂方法,通过此方法可以设置线程的优先级、线程命名规则以及线程类型(用户线程还是守护线程)等。 线程工厂的使用示例如下:
public static void main(String[] args) {// 创建线程工厂ThreadFactory threadFactory = new ThreadFactory() {@Overridepublic Thread newThread(Runnable r) {// 创建线程池中的线程Thread thread = new Thread(r);// 设置线程名称thread.setName("Thread-" + r.hashCode());// 设置线程优先级(最大值:10)thread.setPriority(Thread.MAX_PRIORITY);//......return thread;}};// 创建线程池ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10, 0,TimeUnit.SECONDS, new LinkedBlockingQueue<>(),threadFactory); // 使用自定义的线程工厂threadPoolExecutor.submit(new Runnable() {@Overridepublic void run() {Thread thread = Thread.currentThread();System.out.println(String.format("线程:%s,线程优先级:%d",thread.getName(), thread.getPriority()));}});
}
以上程序的执行结果如下:
从上述执行结果可以看出,自定义线程工厂起作用了,线程的名称和线程的优先级都是通过线程工厂设置的。
参数7:RejectedExecutionHandler
拒绝策略:当线程池的任务超出线程池队列可以存储的最大值之后,执行的策略。
默认的拒绝策略有以下 4 种:
AbortPolicy:拒绝并抛出异常。
CallerRunsPolicy:使用当前调用的线程来执行此任务。
DiscardOldestPolicy:抛弃队列头部(最旧)的一个任务,并执行当前任务。
DiscardPolicy:忽略并抛弃当前任务。
线程池的默认策略是 AbortPolicy 拒绝并抛出异常。
总结
本文介绍了线程池的 7 大参数:
- corePoolSize:核心线程数,线程池正常情况下保持的线程数,大户人家“长工”的数量。
- maximumPoolSize:最大线程数,当线程池繁忙时最多可以拥有的线程数,大户人家“长工”+“短工”的总数量。
- keepAliveTime:空闲线程存活时间,没有活之后“短工”可以生存的最大时间。
- TimeUnit:时间单位,配合参数 3 一起使用,用于描述参数 3 的时间单位。
- BlockingQueue:线程池的任务队列,用于保存线程池待执行任务的容器。
- ThreadFactory:线程工厂,用于创建线程池中线程的工厂方法,通过它可以设置线程的命名规则、优先级和线程类型。
- RejectedExecutionHandler:拒绝策略,当任务量超过线程池可以保存的最大任务数时,执行的策略。
问题:说一下线程池的执行流程?
参考答案: 线程池的执行流程是:
- 先判断当前线程数是否大于核心线程数?如果结果为 false,则新建线程并执行任务;
- 如果结果为 true,则判断任务队列是否已满?如果结果为 false,则把任务添加到任务队列中等待线程执行,
- 否则则判断当前线程数量是否超过最大线程数?如果结果为 false,则新建线程执行此任务,
- 否则将执行线程池的拒绝策略,如下图所示:
问题:线程池的拒绝策略都有哪些?
参考答案:当任务过多且线程池的任务队列已满时,此时就会执行线程池的拒绝策略,线程池的拒绝策略默认有以下 4 种:
- AbortPolicy:中止策略,线程池会抛出异常并中止执行此任务;
- CallerRunsPolicy:把任务交给添加此任务的(main)线程来执行;
- DiscardPolicy:忽略此任务,忽略最新的一个任务;
- DiscardOldestPolicy:忽略最早的任务,最先加入队列的任务。
默认的拒绝策略为 AbortPolicy 中止策略。
问题:如何实现自定义拒绝策略?
参考答案:除了 JDK 提供的四种拒绝策略之外,我们还可以实现通过 new RejectedExecutionHandler,并重写 rejectedExecution 方法来实现自定义拒绝策略,实现代码如下:
public static void main(String[] args) {// 任务的具体方法Runnable runnable = new Runnable() {@Overridepublic void run() {System.out.println("当前任务被执行,执行时间:" + new Date() +" 执行线程:" + Thread.currentThread().getName());try {// 等待 1sTimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}}};// 创建线程,线程的任务队列的长度为 1ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1,100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1),new RejectedExecutionHandler() {@Overridepublic void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {// 执行自定义拒绝策略的相关操作System.out.println("我是自定义拒绝策略~");}});// 添加并执行 4 个任务threadPool.execute(runnable);threadPool.execute(runnable);threadPool.execute(runnable);threadPool.execute(runnable);
}
以上程序的执行结果如下:
问题:如何实现线程池的扩展?
参考答案:线程池 ThreadPoolExecutor 的扩展主要是通过重写它的 beforeExecute() 和 afterExecute() 方法实现的,我们可以在扩展方法中添加日志或者实现数据统计,比如统计线程的执行时间,如下代码所示:
public class ThreadPoolExtend {public static void main(String[] args) throws ExecutionException, InterruptedException {// 线程池扩展调用MyThreadPoolExecutor executor = new MyThreadPoolExecutor(2, 4, 10,TimeUnit.SECONDS, new LinkedBlockingQueue());for (int i = 0; i < 3; i++) {executor.execute(() -> {Thread.currentThread().getName();});}}/*** 线程池扩展*/static class MyThreadPoolExecutor extends ThreadPoolExecutor {// 保存线程执行开始时间private final ThreadLocal<Long> localTime = new ThreadLocal<>();public MyThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue) {super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}/*** 开始执行之前* @param t 线程* @param r 任务*/@Overrideprotected void beforeExecute(Thread t, Runnable r) {Long sTime = System.nanoTime(); // 开始时间 (单位:纳秒)localTime.set(sTime);System.out.println(String.format("%s | before | time=%s",t.getName(), sTime));super.beforeExecute(t, r);}/*** 执行完成之后* @param r 任务* @param t 抛出的异常*/@Overrideprotected void afterExecute(Runnable r, Throwable t) {Long eTime = System.nanoTime(); // 结束时间 (单位:纳秒)Long totalTime = eTime - localTime.get(); // 执行总时间System.out.println(String.format("%s | after | time=%s | 耗时:%s 毫秒",Thread.currentThread().getName(), eTime, (totalTime / 1000000.0)));super.afterExecute(r, t);}}
}
问题:线程池的状态和线程的状态一样吗?
参考答案:在 Java 中,线程池的状态和线程的状态是完全不同的,线程有 6 种状态:NEW:初始化状态、RUNNABLE:可运行/运行状态、BLOCKED:阻塞状态、WAITING:无时限等待状态、TIMED_WAITING:有时限等待状态和 TERMINATED:终止状态。而线程池的状态有以下 5 种:
- RUNNING:运行状态,线程池创建好之后就会进入此状态,如果不手动调用关闭方法,那么线程池在整个程序运行期间都是此状态。
- SHUTDOWN:关闭状态,不再接受新任务提交,但是会将已保存在任务队列中的任务处理完。
- STOP:停止状态,不再接受新任务提交,并且会中断当前正在执行的任务、放弃任务队列中已有的任务。
- TIDYING:整理状态,所有的任务都执行完毕后(也包括任务队列中的任务执行完),当前线程池中的活动线程数降为 0 时的状态。到此状态之后,会调用线程池的 terminated() 方法。
- TERMINATED:销毁状态,当执行完线程池的 terminated() 方法之后就会变为此状态。
这 5 种状态可以在 ThreadPoolExecutor 源码中找到,如下图所示:
问题:什么情况会导致线程池的状态发生改变?
参考答案:线程池的状态转移有两条路径:
- 当调用 shutdown() 方法时,线程池的状态会从 RUNNING 到 SHUTDOWN,再到 TIDYING,最后到 TERMENATED 销毁状态。
- 当调用 shutdownNow() 方法时,线程池的状态会从 RUNNING 到 STOP,再到 TIDYING,最后到 TERMENATED 销毁状态。
线程状态转换的流程如下图所示:
问题:线程池中 shutdownNow() 和 shutdown() 有什么区别?
参考答案:shutdownNow() 和 shutdown() 都是用来终止线程池的,它们的区别是,使用 shutdown() 程序不会报错,也不会立即终止线程,它会等待线程池中的缓存任务执行完之后再退出,执行了 shutdown() 之后就不能给线程池添加新任务了;shutdownNow() 会试图立马停止任务,如果线程池中还有缓存任务正在执行,则会抛出 java.lang.InterruptedException: sleep interrupted 异常。
问题:多线程存在什么问题?
参考答案:多线程的优点可以同时执行多个任务,而缺点是多线程存在线程安全问题,也就是线程的执行不符合预期结果的问题。
问题:为什么会有线程安全问题?
参考答案:导致线程安全问题的因素有以下 5 个:
- 多线程抢占式执行;
- 多线程同时修改同一个变量;
- 非原子性操作:原子性操作是指操作不能再被分割就叫原子性操作,而导致线程安全性问题的一大因素就是非原子性操作;
- 内存可见性:多个线程同时操作统一变量,但因为某些原因导致变量已经被一个线程修改,但另一个线程不可见,从而导致了线程安全性问题;
- 指令重排序:指令重排序是指 Java 程序为了提高程序的执行速度,所以会对一下操作进行合并和优化的操作,比如某些操作本来的顺序是 A -> B -> C,但被指令重排序之后就变成了 A -> C -> B,但这样重排之后就会导致程序的执行结果和预期的结果不相符的问题。
问题:如何解决线程安全问题?
参考答案:在 Java 中,解决线程安全问题有以下 3 种手段:
- 使用线程安全类,比如 AtomicInteger;
- 加锁排队执行
2.1 使用 synchronized 加锁;
2.2 使用 ReentrantLock 加锁; - 使用线程本地变量 ThreadLocal。
线程安全类通常是使用锁机制(乐观锁或悲观锁)来保证程序的正常执行的。
问题:synchronized 有几种用法?
参考答案:synchronized 常见用法有 3 种:
- 使用 synchronized 修饰普通方法;
- 使用 synchronized 修饰静态方法;
- 使用 synchronized 修饰代码块。
① 使用 synchronized 修饰普通方法
synchronized 修饰普通方法的用法如下:
/*** synchronized 修饰普通方法*/
public synchronized void method() {// ....
}
当 synchronized 修饰普通方法时,被修饰的方法被称为同步方法,其作用范围是整个方法,作用的对象是调用这个方法的对象。
② synchronized 修饰静态方法
synchronized 修饰静态方法和修饰普通方法类似,它的用法如下:
/*** synchronized 修饰静态方法*/
public static synchronized void staticMethod() {// .......
}
当 synchronized 修饰静态方法时,其作用范围是整个程序,这个锁对于所有调用这个锁的对象都是互斥的。
所谓的互斥,指的是同一时间只能有一个线程能使用,其他线程只能排队等待。
③ 修饰代码块
public void classMethod() throws InterruptedException {// 前置代码...// 加锁代码synchronized (SynchronizedUsage.class) {// ......}// 后置代码...
}
问题:synchronized 修饰静态方法和普通方法有什么区别吗?
参考答案:synchronized 修饰普通方法和静态方法看似相同,但二者完全不同,对于静态方法来说 synchronized 加锁是全局的,也就是整个程序运行期间,所有调用这个静态方法的对象都是互斥的,而普通方法是针对对象级别的,不同的对象对应着不同的锁。
问题:synchronized 底层是如何实现的?
参考答案:synchronized 是通过 JVM 内置的 Monitor 监视器实现的,而监视器又是依赖操作系统的互斥锁 Mutex 实现的。
问题:什么是监视器?
参考答案:监视器是一个概念或者说是一个机制,它用来保障在任何时候,只有一个线程能够执行指定区域的代码。
一个监视器像是一个建筑,建筑里有一个特殊的房间,这个房间同一时刻只能被一个线程所占有。一个线程从进入该房间到离开该房间,可以全程独占该房间的所有数据。进入该建筑叫做进入监视器(entering the monitor),进入该房间叫做获得监视器(acquiring the monitor),独自占有该房间叫做拥有监视器(owning the monitor),离开该房间叫做释放监视器(releasing the monitor),离开该建筑叫做退出监视器(exiting the monitor)。
严格意义来说监视器和锁的概念是不同的,但很多地方也把二者相互指代。
问题:说一下 synchronized 底层执行流程?
参考答案:synchronized 是通过 JVM 内置的 Monitor 监视器实现的,在 HotSpot 虚拟机中,Monitor 底层是由 C++实现的,它的实现对象是 ObjectMonitor,ObjectMonitor 结构体的实现如下:
pp
ObjectMonitor::ObjectMonitor() { _header = NULL; _count = 0; _waiters = 0, _recursions = 0; //线程的重入次数_object = NULL; _owner = NULL; //标识拥有该monitor的线程_WaitSet = NULL; //等待线程组成的双向循环链表,_WaitSet是第一个节点_WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; //多线程竞争锁进入时的单向链表FreeNext = NULL ; _EntryList = NULL ; //_owner从该双向循环链表中唤醒线程结点,_EntryList是第一个节点_SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ;
}
在以上代码中有几个关键的属性:
- _count:记录该线程获取锁的次数(也就是前前后后,这个线程一共获取此锁多少次)。
- _recursions:锁的重入次数。
- _owner:The Owner 拥有者,是持有该 ObjectMonitor(监视器)对象的线程;
- _EntryList:EntryList 监控集合,存放的是处于阻塞状态的线程队列,在多线程下,竞争失败的线程会进入 EntryList 队列。
- _WaitSet:WaitSet 待授权集合,存放的是处于 wait 状态的线程队列,当线程执行了 wait() 方法之后,会进入 WaitSet 队列。
监视器执行的流程如下:
- 线程通过 CAS(对比并替换)尝试获取锁,如果获取成功,就将 _owner 字段设置为当前线程,说明当前线程已经持有锁,并将 _recursions 重入次数的属性 +1。如果获取失败则先通过自旋 CAS 尝试获取锁,如果还是失败则将当前线程放入到 EntryList 监控队列(阻塞)。
- 当拥有锁的线程执行了 wait 方法之后,线程释放锁,将 owner 变量恢复为 null 状态,同时将该线程放入 WaitSet 待授权队列中等待被唤醒。
- 当调用 notify 方法时,随机唤醒 WaitSet 队列中的某一个线程,当调用 notifyAll 时唤醒所有的 WaitSet 中的线程尝试获取锁。
- 线程执行完释放了锁之后,会唤醒 EntryList 中的所有线程尝试获取锁。
以上就是监视器的执行流程,执行流程如下图所示:
问题:说一下 synchronized 锁升级的流程?
参考答案:synchronized 锁类型有以下几个:
- 无锁
- 偏向锁
- 轻量级锁
- 重量级锁.
synchronized 会按照上述先后顺序依次升级,我们把这个升级的过程称之为“锁膨胀”。
synchronized 锁升级的流程如下:
- 刚开始程序先是无锁状态;
- 当一个线程访问同步代码块并获取锁时,会在对象头的 Mark Word 里存储锁偏向的线程 ID,此时就是偏向锁;
- 如果 Mark Word 中的线程 ID 和访问的线程 ID 一致,则可以直接进入同步块进行代码执行,如果线程 ID 不同,则使用 CAS 尝试获取锁,如果获取成功则进入同步块执行代码,否则会将锁的状态升级为轻量级锁;
- 轻量级锁之后会通过自旋来获取锁,自旋执行一定次数之后还未成功获取到锁,此时就会升级为重量级锁,并且进入阻塞状态。
问题:synchronized 自旋锁有什么优点?
参考答案:synchronized 是自适应自旋锁。自适应自旋锁是指,线程自旋的次数不再是固定的值,而是一个动态改变的值,这个值会根据前一次自旋获取锁的状态来决定此次自旋的次数。比如上一次通过自旋成功获取到了锁,那么这次通过自旋也有可能会获取到锁,所以这次自旋的次数就会增多一些,而如果上一次通过自旋没有成功获取到锁,那么这次自旋可能也获取不到锁,所以为了避免资源的浪费,就会少循环或者不循环,以提高程序的执行效率。简单来说,如果线程自旋成功了,则下次自旋的次数会增多,如果失败,下次自旋的次数会减少。
问题:synchronized 和 ReentrantLock 有什么区别?
参考答案:synchronized 属于独占式悲观锁,是通过 JVM 隐式实现的,synchronized 只允许同一时刻只有一个线程操作资源。
ReentrantLock 是 Lock 的默认实现方式之一,它是基于 AQS(Abstract Queued Synchronizer,队列同步器)实现的,它默认是通过非公平锁实现的,在它的内部有一个 state 的状态字段用于表示锁是否被占用,如果是 0 则表示锁未被占用,此时线程就可以把 state 改为 1,并成功获得锁,而其他未获得锁的线程只能去排队等待获取锁资源。
synchronized 和 ReentrantLock 都提供了锁的功能,具备互斥性和不可见性。在 JDK 1.5 中 synchronized 的性能远远低于 ReentrantLock,但在 JDK 1.6 之后 synchronized 的性能略低于 ReentrantLock,它的区别如下:
- ReentrantLock 只能修饰代码块,而 synchronized 可以用于修饰方法、修饰代码块等;
- ReentrantLock 需要手动加锁和释放锁,如果忘记释放锁,则会造成资源被永久占用,而 synchronized 无需手动释放锁;
- ReentrantLock 使用时更加灵活,比如 ReentrantLock 可以知道是否成功获得了锁,而 synchronized 却不行;
- ReentrantLock 可设置为公平锁,而 synchronized 却不行;
- 二者的底层实现不同,synchronized 是 JVM 层面通过监视器(Monitor)实现的,而 ReentrantLock 是通过 AQS(AbstractQueuedSynchronizer)程序级别的 API 实现。
问题:ReentrantLock 注意事项有哪些?
参考答案:在使用 ReentrantLock 时需要注意以下 3 个问题:
- 在 finally 中释放锁:使用 ReentrantLock 时一定要记得释放锁,否则就会导致该锁一直被占用,其他使用该锁的线程则会永久的等待下去,所以我们在使用 ReentrantLock 时,一定要在 finally 中释放锁,这样就可以保证锁一定会被释放;
- 加锁和释放锁的次数需相同:加锁方法 lock 操作的次数,一定要和释放锁的方法 unlock 的操作次数一一对应,且不能出现一个锁被释放多次的情况,因为这样就会导致程序报错;
- lock 不要放在 try 代码内:在使用 ReentrantLock 时,需要注意不要将加锁操作放在 try 代码中,这样会导致未加锁成功就执行了释放锁的操作,从而导致程序执行异常。
问题:lock 和 lockInterruptibly 有什么区别?
参考答案:lock 和 lockInterruptibly 都是 ReentrantLock 的加锁方法,但二者在响应线程中断时是有很大的区别的,lock 方法会忽略异常继续等待获取线程,而 lockInterruptibly 方法则会抛出 InterruptedException 异常。
比如以下示例代码:
Lock interruptLock = new ReentrantLock();
interruptLock.lock();
Thread thread = new Thread(new Runnable() {@Overridepublic void run() {try {interruptLock.lock();//interruptLock.lockInterruptibly(); // java.lang.InterruptedException} catch (Exception e) {e.printStackTrace();}}
});
thread.start();
TimeUnit.SECONDS.sleep(1);
thread.interrupt();
TimeUnit.SECONDS.sleep(3);
System.out.println("Over");
System.exit(0);
执行以下代码会发现使用 lock 方法时程序不会报错,运行完成直接退出;而使用 lockInterruptibly 方法则会抛出异常 java.lang.InterruptedException ,这就说明:在获取线程的途中如果所在的线程中断,lock 方法会忽略异常继续等待获取线程,而 lockInterruptibly 方法则会抛出 InterruptedException 异常。
问题:volatile 能保证线程安全吗?
参考答案:volatile 是不能保证线程执行安全的,volatile 的主要作用有两个:保证内存的可见性和禁止指令重排序。也就说使用 volatile 可以保证多个线程在操作同一个变量时,始终可以读取到最新的数据;并且使用 volatile 可以禁止指令重排序,从而关闭掉系统优化所给程序代码执行结果不一致的风险。
问题:说一下指令重排序及其问题?(和单例模式的懒汉模式结合)
参考答案:指令重排序是指编译器或 CPU 为了优化程序的执行性能,而对指令进行重新排序的一种手段。
指令重排序的实现初衷是好的,但是在多线程执行中,如果执行了指令重排序可能会导致程序执行出错。指令重排序最典型的一个问题就发生在单例模式中,比如以下问题代码:
public class Singleton {private Singleton() {}private static Singleton instance = null;public static Singleton getInstance() {if (instance == null) { // ①synchronized (Singleton.class) {if (instance == null) {instance = new Singleton(); // ②}}}return instance;}
}
以上问题发生在代码 ② 这一行“instance = new Singleton();”,这行代码看似只是一个创建对象的过程,然而它的实际执行却分为以下 3 步:
- 创建内存空间。
- 在内存空间中初始化对象 Singleton。
- 将内存地址赋值给 instance 对象(执行了此步骤,instance 就不等于 null 了)。
如果此变量不加 volatile,那么线程 1 在执行到上述代码的第 ② 处时就可能会执行指令重排序,将原本是 1、2、3 的执行顺序,重排为 1、3、2。但是特殊情况下,线程 1 在执行完第 3 步之后,如果来了线程 2 执行到上述代码的第 ① 处,判断 instance 对象已经不为 null,但此时线程 1 还未将对象实例化完,那么线程 2 将会得到一个被实例化“一半”的对象,从而导致程序执行出错,这就是为什么要给私有变量添加 volatile 的原因了。
要使以上单例模式变为线程安全的程序,需要给 instance 变量添加 volatile 修饰,它的最终实现代码如下:
public class Singleton {private Singleton() {}// 使用 volatile 禁止指令重排序private static volatile Singleton instance = null; // 【主要是此行代码发生了变化】public static Singleton getInstance() {if (instance == null) { // ①synchronized (Singleton.class) {if (instance == null) {instance = new Singleton(); // ②}}}return instance;}
}
问题:volatile 使用场景有哪些?
参考答案:volatile 常使用在一写多读的场景中,比如 CopyOnWriteArrayList 集合,它在操作的时候会把全部数据复制出来对写操作加锁,修改完之后再使用 setArray 方法把此数组赋值为更新后的值,使用 volatile 可以使读线程很快的告知到数组被修改,不会进行指令重排,操作完成后就可以对其他线程可见了。
问题:产生死锁的原因有哪些?
参考答案:死锁(Dead Lock)指的是两个或两个以上的运算单元(进程、线程或协程),都在等待对方释放资源,但没有一方提起释放资源,从而造成了一种阻塞的现象就称为死锁。
死锁的产生需要满足以下 4 个条件:
- 互斥条件:指运算单元(进程、线程或协程)对所分配到的资源具有排它性,也就是说在一段时间内某个锁资源只能被一个运算单元所占用。
- 请求和保持条件:指运算单元已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它运算单元占有,此时请求运算单元阻塞,但又对自己已获得的其它资源保持不放。
- 不可剥夺条件:指运算单元已获得的资源,在未使用完之前,不能被剥夺。
- 环路等待条件:指在发生死锁时,必然存在运算单元和资源的环形链,即运算单元正在等待另一个运算单元占用的资源,而对方又在等待自己占用的资源,从而造成环路等待的情况。
只有以上 4 个条件同时满足,才会造成死锁。
问题:如何解决死锁问题?
参考答案:我们可以通过顺序锁或轮询锁来解决死锁的问题。
① 顺序锁
所谓的顺序锁指的是通过有顺序的获取锁,从而避免产生环路等待条件,从而解决死锁问题的。
当我们没有使用顺序锁时,程序的执行可能是这样的:
线程 1 先获取了锁 A,再获取锁 B,线程 2 与 线程 1 同时执行,线程 2 先获取锁 B,再获取锁 A,这样双方都先占用了各自的资源(锁 A 和锁 B)之后,再尝试获取对方的锁,从而造成了环路等待问题,最后造成了死锁的问题。
此时我们只需要将线程 1 和线程 2 获取锁的顺序进行统一,也就是线程 1 和线程 2 同时执行之后,都先获取锁 A,再获取锁 B,执行流程如下图所示:
因为只有一个线程能成功获取到锁 A,没有获取到锁 A 的线程就会等待先获取锁 A,此时得到锁 A 的线程继续获取锁 B,因为没有线程争抢和拥有锁 B,那么得到锁 A 的线程就会顺利的拥有锁 B,之后执行相应的代码再将锁资源全部释放,然后另一个等待获取锁 A 的线程就可以成功获取到锁资源,执行后续的代码,这样就不会出现死锁的问题了。
顺序锁的实现代码如下所示:
public class SolveDeadLockExample {public static void main(String[] args) {Object lockA = new Object(); // 创建锁 AObject lockB = new Object(); // 创建锁 B// 创建线程 1Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {synchronized (lockA) {System.out.println("线程 1:获取到锁 A!");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程 1:等待获取 B...");synchronized (lockB) {System.out.println("线程 1:获取到锁 B!");}}}});t1.start(); // 运行线程// 创建线程 2Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {synchronized (lockA) {System.out.println("线程 2:获取到锁 A!");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程 2:等待获取B...");synchronized (lockB) {System.out.println("线程 2:获取到锁 B!");}}}});t2.start(); // 运行线程}
}
以上程序的执行结果如下:
从上述执行结果可以看出,程序并没有出现死锁的问题。
② 轮询锁
轮询锁是通过打破“请求和保持条件”来避免造成死锁的,它的实现思路简单来说就是通过轮询来尝试获取锁,如果有一个锁获取失败,则释放当前线程拥有的所有锁,等待下一轮再尝试获取锁。
轮询锁的实现需要使用到 ReentrantLock 的 tryLock 方法,具体实现代码如下:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class SolveDeadLockExample {public static void main(String[] args) {Lock lockA = new ReentrantLock(); // 创建锁 ALock lockB = new ReentrantLock(); // 创建锁 B// 创建线程 1(使用轮询锁)Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {// 调用轮询锁pollingLock(lockA, lockB);}});t1.start(); // 运行线程// 创建线程 2Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {lockB.lock(); // 加锁System.out.println("线程 2:获取到锁 B!");try {Thread.sleep(1000);System.out.println("线程 2:等待获取 A...");lockA.lock(); // 加锁try {System.out.println("线程 2:获取到锁 A!");} finally {lockA.unlock(); // 释放锁}} catch (InterruptedException e) {e.printStackTrace();} finally {lockB.unlock(); // 释放锁}}});t2.start(); // 运行线程}/*** 轮询锁*/public static void pollingLock(Lock lockA, Lock lockB) {while (true) {if (lockA.tryLock()) { // 尝试获取锁System.out.println("线程 1:获取到锁 A!");try {Thread.sleep(1000);System.out.println("线程 1:等待获取 B...");if (lockB.tryLock()) { // 尝试获取锁try {System.out.println("线程 1:获取到锁 B!");} finally {lockB.unlock(); // 释放锁System.out.println("线程 1:释放锁 B.");break;}}} catch (InterruptedException e) {e.printStackTrace();} finally {lockA.unlock(); // 释放锁System.out.println("线程 1:释放锁 A.");}}// 等待一秒再继续执行try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}
以上程序的执行结果如下:
从上述结果可以看出,以上代码也没有出现死锁的问题.
问题:常见的锁策略有哪些?
参考答案:常见的锁策略有:
- 悲观锁:悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观地认为,不加锁的并发操作一定会出问题;
- 乐观锁:乐观锁正好和悲观锁相反,它获取数据的时候,并不担心数据被修改,每次获取数据的时候也不会加锁,只是在更新数据的时候,通过判断现有的数据是否和原数据一致来判断数据是否被其他线程操作,如果没被其他线程修改则进行数据更新,如果被其他线程修改则不进行数据更新;
- 公平锁:公平锁是指多个线程按照申请锁的顺序来获取锁;
- 非公平锁:非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁;
- 独占锁:独占锁是指任何时候都只有一个线程能执行资源操作;
- 共享锁:共享锁指定是可以同时被多个线程读取,但只能被一个线程修改。比如 Java 中的 ReentrantReadWriteLock 就是共享锁的实现方式,它允许一个线程进行写操作,允许多个线程读操作;
- 可重入锁:可重入锁指的是该线程获取了该锁之后,可以无限次的进入该锁锁住的代码;
- 自旋锁:自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗 CPU。
问题:乐观锁和 CAS 有什么区别?
参考答案:乐观锁是一种锁实现的策略(思想),而 CAS 是一项实现乐观锁的具体技术。
问题:说一下 CAS 的执行流程?
参考答案:CAS(Compare And Swap)比较并替换,CAS 比较并替换的流程是这样的,CAS 中包含了三个操作单位:V(内存值)、A(预期的旧值)、B(新值),比较 V 值和 A 是否相等,如果相等的话则将 V 的值更换成 B,否则就提示用户修改失败,从而实现了 CAS 的机制。
问题:CAS 底层是如何实现的?
参考答案:CAS 实现是借助 Unsafe 类实现的,而 Unsafe 类调用操作系统的 Atomic::cmpxchg(原子性汇编指令)。
问题:CAS 有什么问题?
参考答案:CAS 存在 ABA 问题。
ABA 问题指的是,比如磊哥去银行取钱,余额有 200 元,磊哥取 100 元,但因为程序的问题,启动了两个线程,线程一和线程二进行比对扣款,线程一获取原本有 200 元,扣除 100 元,余额等于 100 元,此时张三给磊哥转账 100 元,于是启动了线程三抢先在线程二之前执行了转账操作,把 100 元又变成了 200 元,而此时线程二对比自己事先拿到的 200 元和此时经过改动的 200 元值一样,就进行了减法操作,把余额又变成了 100 元。这显然不是我们要的正确结果,我们期望的结果是余额是 200 元(虽然取了 100 元,但别人又给我转了 100 元),而此时余额变成了 100 元,显然有悖常理,这就是著名的 ABA 的问题。
它的执行流程如下:
- 线程一:取款,获取原值 200 元,与 200 元比对成功,减去 100 元,修改结果为 100 元。
- 线程二:取款,获取原值 200 元,阻塞等待修改。
- 线程三:转账,获取原值 100 元,与 100 元比对成功,加上 100 元,修改结果为 200 元。
- 线程二:取款,恢复执行,原值为 200 元,与 200 元对比成功,减去 100 元,修改结果为 100 元。
最终的结果变成了 100 元。
问题:怎么解决 ABA 问题?
参考答案:常见解决 ABA 问题的方案加版本号,来区分值是否有变动。以磊哥取钱的例子为例,如果加上版本号,执行流程如下。
- 线程一:取款,获取原值 200_V1,与 200_V1 比对成功,减去 100 元,修改结果为 100_V2。
- 线程二:取款,获取原值 200_V1 阻塞等待修改。
- 线程三:转账,获取原值 100_V2,与 100_V2 对比成功,加 100 元,修改结果为 200_V3。
- 线程二:取款,恢复执行,原值 200_V1 与现值 200_V3 对比不相等,退出修改。
最终的结果为 200 元,这显然是我们需要的结果。
在程序中,要怎么解决 ABA 的问题呢?
在 JDK 1.5 的时候,Java 提供了一个 AtomicStampedReference 原子引用变量,通过添加版本号来解决 ABA 的问题,具体使用示例如下:
String name = "磊哥";
String newName = "Java";
AtomicStampedReference<String> as = new AtomicStampedReference<String>(name, 1);
System.out.println("值:" + as.getReference() + " | Stamp:" + as.getStamp());
as.compareAndSet(name, newName, as.getStamp(), as.getStamp() + 1);
System.out.println("值:" + as.getReference() + " | Stamp:" + as.getStamp());
以上程序执行结果如下:
值:磊哥 | Stamp:1
值:Java | Stamp:2
问题:CountDownLatch 和 CyclicBarrier 有什么区别?
参考答案:CountDownLatch 和 CyclicBarrier 都是 JUC(java.util.concurrent)下的包,但它们的功能有所不同。
① CountDownLatch
CountDownLatch(闭锁)可以看作一个只能做减法的计数器,可以让一个或多个线程等待执行。
CountDownLatch 有两个重要的方法:
- countDown():使计数器减 1;
- await():当计数器不为 0 时,则调用该方法的线程阻塞,当计数器为 0 时,可以唤醒等待的一个或者全部线程。
CountDownLatch 使用场景:
以生活中的情景为例,比如去医院体检,通常人们会提前去医院排队,但只有等到医生开始上班,才能正式开始体检,医生也要给所有人体检完才能下班,这种情况就要使用 CountDownLatch,流程为:患者排队 → 医生上班 → 体检完成 → 医生下班。
② CyclicBarrier
CyclicBarrier(循环屏障)通过它可以实现让一组线程等待满足某个条件后同时执行。
CyclicBarrier 经典使用场景是公交发车,为了简化理解我们这里定义,每辆公交车只要上满 4 个人就发车,后面来的人都会排队依次遵循相应的标准。
小结:CountDownLatch 和 CyclicBarrier 内部都是使用计时器实现的,但 CountDownLatch 的计数器只能使用一次,而 CyclicBarrier 的计数器可以循环使用,这就是二者最大的区别。
问题:Semaphore 有啥用?
参考答案:Semaphore(信号量)用于管理多线程中控制资源的访问与使用,Semaphore 可以实习限流器。
Semaphore 就好比停车场的门卫,可以控制车位的使用资源。比如来了 5 辆车,只有 2 个车位,门卫可以先放两辆车进去,等有车出来之后,再让后面的车进入,它的实现代码如下:
Semaphore semaphore = new Semaphore(2);
ThreadPoolExecutor semaphoreThread = new ThreadPoolExecutor(10, 50, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
for (int i = 0; i < 5; i++) {semaphoreThread.execute(() -> {try {// 堵塞获取许可semaphore.acquire();System.out.println("Thread:" + Thread.currentThread().getName() + " 时间:" + LocalDateTime.now());TimeUnit.SECONDS.sleep(2);// 释放许可semaphore.release();} catch (InterruptedException e) {e.printStackTrace();}});
}
以上程序执行结果如下:
Thread:pool-1-thread-1 时间:2022-07-10 21:18:42
Thread:pool-1-thread-2 时间:2022-07-10 21:18:42
Thread:pool-1-thread-3 时间:2022-07-10 21:18:44
Thread:pool-1-thread-4 时间:2022-07-10 21:18:44
Thread:pool-1-thread-5 时间:2022-07-10 21:18:46
执行流程如下图:
问题:如何实现线程安全的单例模式?
参考答案:线程安全的单例模式常见的实现方法有两种:一种是天生线程安全的饿汉模式,另一种是双重效验锁实现的懒汉单例模式,具体实现如下。
① 饿汉模式
饿汉模式也叫预加载模式,它是在类加载时直接创建并初始化单例对象,所以它并不存在线程安全的问题。它是依靠 ClassLoader 类机制,在程序启动时只加载一次,因此不存在线程安全问题,它的实现代码如下:
public class Singleton {// 1.防止外部直接 new 对象破坏单例模式private Singleton() {}// 2.通过私有变量保存单例对象private static Singleton instance = new Singleton();// 3.提供公共获取单例对象的方法public static Singleton getInstance() {return instance;}
}
优点:实现简单、不存在线程安全问题。
缺点:类加载时就创建了对象,创建之后如果没被使用,就造成了资源浪费的情况。
② 懒汉模式(双重效验锁)
懒汉模式和饿汉模式正好是相反的,所谓的懒汉模式也就是懒加载(延迟加载),指的是它只有在第一次被使用时,才会被初始化,它的实现代码如下:
public class Singleton {// 1.防止外部直接 new 对象破坏单例模式private Singleton() {}// 2.通过私有变量保存单例对象private static volatile Singleton instance = null;// 3.提供公共获取单例对象的方法public static Singleton getInstance() {if (instance == null) { // 第一次效验synchronized (Singleton.class) {if (instance == null) { // 第二次效验instance = new Singleton();}}}return instance;}
}
懒汉模式使用的是双重效验锁和 volatile 来保证线程安全的,从上述代码可以看出,无论是饿汉模式还是懒汉模式,它们的实现步骤都是一样的:
- 创建一个私有的构造方法,防止其他调用的地方直接 new 对象,这样创建出来的对象就不是单例对象了。
- 创建一个私有变量来保存单例对象。
- 提供一个公共的方法返回单例对象。
懒汉模式相比于饿汉模式来说,不会造成资源的浪费,但写法要复杂一些。
问题:为什么 ThreadLocal 是线程安全的?
参考答案:ThreadLocal 为每一个线程维护变量的副本,把共享数据的可见范围限制在同一个线程之内,因此 ThreadLocal 是线程安全的,每个线程都有属于自己的变量。
在 Java 中解决线程安全问题通常有 3 种方案:
- 使用线程安全类,比如 AtomicInteger;
- 加锁排队执行
2.1 使用 synchronized 加锁;
2.2 使用 ReentrantLock 加锁; - 使用线程本地变量 ThreadLocal。
问题:为什么 ThreadLoal 会导致内存溢出?
参考答案:要搞清楚内存溢出的问题,需要从 ThreadLocal 源码入手,所以我们首先打开 set 方法的源码(在示例中使用到了 set 方法),如下所示:
public void set(T value) {// 得到当前线程Thread t = Thread.currentThread();// 根据线程获取到 ThreadMap 变量ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value); // 将内容存储到 map 中elsecreateMap(t, value); // 创建 map 并将值存储到 map 中
}
从上述代码我们可以看出 Thread、ThreadLocalMap 和 set 方法之间的关系:每个线程 Thread 都拥有一个数据存储容器 ThreadLocalMap,当执行 ThreadLocal.set 方法执行时,会将要存储的值放到 ThreadLocalMap 容器中,所以接下来我们再看一下 ThreadLocalMap 的源码:
static class ThreadLocalMap {// 实际存储数据的数组private Entry[] table;// 存数据的方法private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();// 如果有对应的 key 直接更新 value 值if (k == key) {e.value = value;return;}// 发现空位插入 valueif (k == null) {replaceStaleEntry(key, value, i);return;}}// 新建一个 Entry 插入数组中tab[i] = new Entry(key, value);int sz = ++size;// 判断是否需要进行扩容if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();}// ... 忽略其他源码
}
从上述源码我们可以看出:ThreadMap 中有一个 Entry[] 数组用来存储所有的数据,而 Entry 是一个包含 key 和 value 的键值对,其中 key 为 ThreadLocal 本身,而 value 则是要存储在 ThreadLocal 中的值。
根据上面的内容,我们可以得出 ThreadLocal 相关对象的关系图,如下所示:
也就是说它们之间的引用关系是这样的:Thread -> ThreadLocalMap -> Entry -> Key,Value,因此当我们使用线程池来存储对象时,因为线程池有很长的生命周期,所以线程池会一直持有 value 值,那么垃圾回收器就无法回收 value,所以就会导致内存一直被占用,从而导致内存溢出问题的发生。
简单来说,ThreadLocal 造成内存溢出的原因:如果 ThreadLocal 没有被直接引用(外部强引用),在 GC(垃圾回收)时,由于 ThreadLocalMap 中的 key 是弱引用,所以一定就会被回收,这样一来 ThreadLocalMap 中就会出现 key 为 null 的 Entry,并且没有办法访问这些数据,如果当前线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 并且永远无法回收,从而造成内存泄漏。
问题:如何解决 ThreadLocal 内存溢出问题?
参考答案:ThreadLocal 内存溢出的解决方案很简单,我们只需要在使用完 ThreadLocal 之后,执行 remove 方法就可以避免内存溢出问题的发生了。
问题:ThreadLocal 如何实现多线程间的数据共享?
参考答案:通过 ThreadLocal 的子类 InheritableThreadLocal 可以天然的支持多线程间的信息共享。
如以下代码所示:
ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("磊哥");
new Thread(() -> {System.out.println(threadLocal.get());
}).start();
程序的执行结果为:磊哥
因为使用 InheritableThreadLocal 类,所以多线程间是可以实现数据共享的。
问题:ThreadLocal 和 Synchonized 有什么区别?
参考答案:ThreadLocal 和 Synchonized 都用于解决多线程并发访问,防止任务在共享资源上产生冲突,但是 ThreadLocal 与 Synchronized 有本质的区别,Synchronized 用于实现同步机制,是利用锁的机制使变量或代码块在某一时刻只能被一个线程访问,是一种“以时间换空间”的方式;而 ThreadLocal 为每一个线程提供了独立的变量副本,这样每个线程的(变量)操作都是相互隔离的,这是一种“以空间换时间”的方式。
问题:线程通讯的方式
线程等待和唤醒的实现手段
如何停止线程
线程池的优点
如何停止线程池
如何判断线程池已经完成
volatile底层如何实现
volatile的两个作用
保证线程安全的手段
synchronized 和 Lock有什么区别
synchronized 底层如何实现
synchronized 锁升级机制
产生死锁的条件
什么是AQS
2022-08-10_CPU工作原理_作业
2022-08-12_CPU工作原理等_作业
操作系统
操作系统如何进行进程调度
博客总结
2022-08-13_线程概念等_作业
进程和线程的区别和联系
5种创建线程的方式
public class CreatThread_five {private static class MyThread1 extends Thread{@Overridepublic void run() {System.out.println("线程1");}}private static class MyThread2 implements Runnable{@Overridepublic void run() {System.out.println("我是线程2");}}public static void main(String[] args) {Thread t1 = new MyThread1();Thread t2 = new Thread(new MyThread2());Thread t3 = new Thread(){@Overridepublic void run() {System.out.println("我是线程3,通过内部类创建Thread 子对象");}};Thread t4 = new Thread(new Runnable() {@Overridepublic void run() {System.out.println("我是线程4,通过内部类创建Runable接口");}});Thread t5 = new Thread(()-> System.out.println("我是线程5"));t1.start();t2.start();t3.start();t4.start();t5.start();}}
编写代码, 实现多线程数组求和.
import java.util.Random;/*** 题目名称 :* 编写代码, 实现多线程数组求和.* 题目内容 :* 1. 给定一个很长的数组 (长度 1000w), 通过随机数的方式生成 1-100 之间的整数.* 2. 实现代码, 能够创建两个线程, 对这个数组的所有元素求和.* 3. 其中线程1 计算偶数下标元素的和, 线程2 计算奇数下标元素的和.* 4. 最终再汇总两个和, 进行相加* 5. 记录程序的执行时间.**/
class Thread_ {public static void main(String[] args) throws InterruptedException {// 记录开始时间long start = System.currentTimeMillis();// 1. 给定一个很长的数组 (长度 1000w), 通过随机数的方式生成 1-100 之间的整数.int total = 1000_0000;int [] arr = new int[total];// 构造随机数,填充数组Random random = new Random();for (int i = 0; i < total; i++) {int num = random.nextInt(100) + 1;arr[i] = num;}// 2. 实现代码, 能够创建两个线程, 对这个数组的所有元素求和.// 3. 其中线程1 计算偶数下标元素的和, 线程2 计算奇数下标元素的和.// 实例化操作类SumOperator operator = new SumOperator();// 定义具体的执行线程Thread t1 = new Thread(() -> {// 遍历数组,累加偶数下标for (int i = 0; i < total; i += 2) {operator.addEvenSum(arr[i]);}});Thread t2 = new Thread(() -> {// 遍历数组,累加奇数下标for (int i = 1; i < total; i += 2) {operator.addOddSum(arr[i]);}});// 启动线程t1.start();t2.start();// 等待线程结束t1.join();t2.join();// 记录结束时间long end = System.currentTimeMillis();// 结果System.out.println("结算结果为 = " + operator.result());System.out.println("总耗时 " + (end - start) + "ms.");}
}// 累加操作用这个类来完成
class SumOperator {long evenSum;long oddSum;public void addEvenSum (int num) {evenSum += num;}public void addOddSum (int num) {oddSum += num;}public long result() {System.out.println("偶数和:" + evenSum);System.out.println("奇数和:" + oddSum);return evenSum + oddSum;}
}
2022-08-14_线程概念等_作业
在子线程执行完毕后再执行主线程代码(Thread数组)
public static void main(String[] args) throws InterruptedException {Thread[] threads = new Thread[20];for(int i=0; i<20; i++){final int n = i;threads[i] = new Thread(new Runnable() {@Overridepublic void run() {//内部类使用外部的变量,必须是final修饰System.out.println(n);}});}for(Thread t : threads){t.start();}for(Thread t : threads){//同时执行20个线程,再等待所有线程执行完毕t.join();}System.out.println("OK");}
请说明Thread类中run和start的区别
java线程的状态
java线程状态
2022-09-06_线程安全_作业
以下可以完全解决线程安全问题的选项是?
使用两个线程来累加 count 的值
public class Thread_demo07 {// 累加次数public static int num = 10000;// 实例化执行累加的类public static Counter counter = new Counter();public static void main(String[] args) throws InterruptedException {// 定义两个线程Thread t1 = new Thread(() -> {for (int i = 0; i < num; i++) {counter.increase();}});Thread t2 = new Thread(() -> {for (int i = 0; i < num; i++) {counter.increase();}});// 启动线程t1.start();t2.start();// 等待线程执行完成t1.join();t2.join();System.out.println("累加结果 = " + counter.count);}
}// 定义一个执行累加的类
class Counter {// 初始值为0public int count = 0;// 累加方法,用synchronized保证线程安全public synchronized void increase () {count++;}
}
编写博客, 总结线程安全问题的原因和解决方案
博客
2022-09-13_线程安全_作业
请描述 volatile 关键字的作用
顺序打印
/*** 题目名称 :* 请完成以下多线程编程:顺序打印* 题目内容 :* 有三个线程,线程名称分别为:a,b,c。** 每个线程打印自己的名称。* 需要让他们同时启动,并按 c,b,a的顺序打印** @Author 比特就业课* @Date 2022-06-20*/
public class Thread_2152 {public static void main(String[] args) throws InterruptedException {// 创建三个线程Thread tc = new Thread(() -> {// 打印cSystem.out.print(Thread.currentThread().getName() + " ");}, "c");Thread tb = new Thread(() -> {try {// 等待c 执行完成tc.join();} catch (InterruptedException e) {e.printStackTrace();}// 打印bSystem.out.print(Thread.currentThread().getName() + " ");}, "b");Thread ta = new Thread(() -> {try {// 等待b 执行完成tb.join();} catch (InterruptedException e) {e.printStackTrace();}// 打印aSystem.out.print(Thread.currentThread().getName() + " ");}, "a");// 需要让他们同时启动,并按 c,b,a的顺序打印ta.start();tb.start();tc.start();}
}
2022-09-15_线程安全等_作业
顺序打印-进阶版
方法一:三个线程竞争同一个锁,通过count判断是否打印
方法二:三个线程同时start,分别上锁,从a开始,打印后唤醒b
三个线程分别打印A,B,C
方法一:通过count计数打印(三个线程上同样的锁,打印一个,召唤所有锁,如果不满足条件,则wait等待,锁自动解锁)
方法二:
/*** 有三个线程,分别只能打印A,B和C* 要求按顺序打印ABC,打印10次* 输出示例:* ABC* ABC* ABC* ABC* ABC* ABC* ABC* ABC* ABC* ABC*/
public class Thread_ {// 计数器private static volatile int COUNTER = 0;// 定义一个单独的锁对象private static Object lock = new Object();public static void main(String[] args) {// 创建三个线程,并指定线程名,每个线程名分别用A,B,C表示Thread t1 = new Thread(() -> {// 循环10次for (int i = 0; i < 10; i++) {// 执行的代码加锁synchronized (lock) {// 每次唤醒后都重新判断是否满足条件// 每条线程判断的条件不一样,注意线程t1,t2while (COUNTER % 3 != 0) {try {// 不满足输出条件时,主动等待并释放锁lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}// 满足输出条件,打印线程名,每条线程打印的内容不同System.out.print(Thread.currentThread().getName());// 累加计数COUNTER++;// 唤醒其他线程lock.notifyAll();}}}, "A");Thread t2 = new Thread(() -> {for (int i = 0; i < 10; i++) {synchronized (lock) {while (COUNTER % 3 != 1) {try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.print(Thread.currentThread().getName());COUNTER++;lock.notifyAll();}}}, "B");Thread t3 = new Thread(() -> {for (int i = 0; i < 10; i++) {synchronized (lock) {while (COUNTER % 3 != 2) {try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}// 换行打印System.out.println(Thread.currentThread().getName());COUNTER++;lock.notifyAll();}}}, "C");// 启动线程t1.start();t2.start();t3.start();}
}
public class Demo {private static Object locker1 = new Object();private static Object locker2 = new Object();private static Object locker3 = new Object();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {try {for (int i = 0; i < 10; i++) {synchronized (locker1) {locker1.wait();}System.out.print("A");synchronized (locker2) {locker2.notify();}}} catch (InterruptedException e) {e.printStackTrace();}});Thread t2 = new Thread(() -> {try {for (int i = 0; i < 10; i++) {synchronized (locker2) {locker2.wait();}System.out.print("B");synchronized (locker3) {locker3.notify();}}} catch (InterruptedException e) {e.printStackTrace();}});Thread t3 = new Thread(() -> {try {for (int i = 0; i < 10; i++) {synchronized (locker3) {locker3.wait();}System.out.println("C");synchronized (locker1) {locker1.notify();}}} catch (InterruptedException e) {e.printStackTrace();}});t1.start();t2.start();t3.start();Thread.sleep(1000);// 从线程 t1 启动synchronized (locker1) {locker1.notify();}}
}
简述 wait 和 sleep 有什么区别?
编写代码, 实现线程安全版本的单例模式
先上锁
锁里面判断null 是为了 减少new
锁外面判断null 是为了减少排队的人
public class Singleton {// 声明,并用volatile修饰,保证在多线程环境下的有序性private volatile static Singleton instance = null;// 私有构造方法private Singleton () {}// 对外提供一个获取实例的方法,public static Singleton getInstance() {// 使用双重if检查, 降低锁竞争的频率if (instance == null) {// instance没有被实例化才去加锁synchronized (Singleton.class) {// 获取到锁后再检查一遍,确认instance没有被实例化后去真正的实例化if (instance == null) {instance = new Singleton();}}}return instance;}
}
2022-09-18_多线程代码案例
简述线程池有什么优点?
编写代码, 实现阻塞队列案例
package com.bitejiuyeke.exercises;import java.util.Random;/*** 实现一个存放整形的阻塞队列* 作业编码 2543** @Author 比特就业课* @Date 2022-06-22*/
public class MyBlockingQueue {// 定义一个存放数据的数组,指定大小为1000private int[] item = new int[1000];// 记录当前使用的大小private int size = 0;// 队列头private int head = 0;// 队列尾private int tail = 0;// 存入public void put(int val) throws InterruptedException {// 保证写入操作的原子性synchronized (this) {// 这里使用while循环,使每次唤醒后都重新去做一次判断while (size == item.length) {// 如果队列已满,就等待
// System.out.println("队列满了,暂停生产...");this.wait();}// 插入尾部item[tail] = val;// 更新尾标的值,如果大于数组大小,从头开始计数tail = (tail + 1) % item.length;// 更新size的值size++;// 唤醒其他线程notifyAll();}}// 取出public int take() throws InterruptedException {int result = 0;// 保证原子性synchronized (this) {// 这里使用while循环,使每次唤醒后都重新去做一次判断while (size == 0) {// 如果队列为空,就等待
// System.out.println("队列空了,暂停消费...");this.wait();}// 取出队列头的值result = item[head];// 超出数组大小从头开始计数head = (head + 1) % item.length;// 更新size的大小size--;// 唤醒其他线程this.notifyAll();}return result;}// 保证原子性和内存可见性public synchronized int getSize() {return size;}public static void main(String[] args) {// 实例化自定义的阻塞队列MyBlockingQueue blockingQueue = new MyBlockingQueue();// 定义消费者线程Thread customer = new Thread(() -> {while (true) {try {// 取出数据进行消费int num = blockingQueue.take();System.out.println("消费了:" + num + ", 队列大小: \t" + blockingQueue.getSize());} catch (InterruptedException e) {e.printStackTrace();}}}, "customer");// 启动消费者customer.start();// 定义生产线程Thread producer = new Thread(() -> {Random random = new Random();while (true) {try {// 生产数据并存入队列int num = random.nextInt(10000);blockingQueue.put(num);System.out.println("生产了: \t" + num + ", 队列大小: \t" + blockingQueue.getSize());} catch (InterruptedException e) {e.printStackTrace();}}}, "producer");// 启动生产者producer.start();}
}
编写代码, 实现定时器案例
/*** 实现一个自定义的定时器** @Author 比特就业课* @Date 2022-06-22*/public class Timer {// 通过PriorityBlockingQueue来组织任务private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue();// 存在的意义是避免 worker 线程出现忙等的情况private Object mailBox = new Object();// 构造方法,直接启动worker线程public Timer () {// 创建worker线程对象Worker worker = new Worker();// 启动线程worker.start();}// 执行计划,通过这个方法添加定时任务public void schedule(Runnable runnable, long time) {Task task = new Task(runnable, time);queue.offer(task);// 每增加一个任务就唤醒一次worker看有没有要执行的任务synchronized (mailBox) {mailBox.notifyAll();}}// 能过work完成对线程class Worker extends Thread {@Overridepublic void run() {while (true) {try {// 获取距当前时间最短的任务Task task = queue.take();// 获取当前时间long curr = System.currentTimeMillis();if (curr >= task.getTime()) {task.run();} else {// 时间没有到put回队列queue.put(task);// 最小等待时间long waitTime = task.getTime() - curr;synchronized (mailBox) {mailBox.wait(waitTime);}}} catch (InterruptedException e) {e.printStackTrace();}}}}// 测试public static void main(String[] args) {Timer timer = new Timer();Runnable runnable = new Runnable() {@Overridepublic void run() {System.out.println("时间到了,我开始执行...");}};timer.schedule(runnable, 3000);}
}// 定义一个描述任务的类, 实现Comparable接口用于比较时长
class Task implements Comparable<Task> {// 具体的任务private Runnable runnable;// 等待执行的时长private long time;// 构造方法public Task(Runnable runnable, long time) {this.runnable = runnable;// 记录绝对时间,到了这个时间后就开始执行this.time = time + System.currentTimeMillis();}// 运行任务public void run() {runnable.run();}public long getTime() {return time;}@Overridepublic int compareTo(Task o) {// 小的在前return (int) (this.time - o.time);}
}
编写代码, 实现线程池案例
/*** 实现线程池案例* @Author 比特就业课* @Date 2022-06-22*/
class MyThreadPool {// 1. 描述任务,直接使用Runnable// 2. 管理和组织任务private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();// 3. 描述线程,功能是从队列中获取任务并执行static class Worker extends Thread {// 当前线程池中有若干个任务,都持有上述队列并从中获取任务private BlockingQueue<Runnable> queue = null;public Worker(BlockingQueue<Runnable> queue) {this.queue = queue;}@Overridepublic void run() {while (true) {try {// 循环获取任务队列中的任务,如果队列为空就阻塞,非空就获取执行Runnable runnable = queue.take();// 获取之后就去执行runnable.run();} catch (InterruptedException e) {e.printStackTrace();}}}}// 4. 组织线程private List<Worker> workers = new ArrayList<>();// 提供一个构造方法,接收要线程池中线程的数量public MyThreadPool(int num) {for (int i = 0; i < num; i++) {// 创建线程Worker worker = new Worker(queue);// 启动线程worker.start();// 把线程添加到List中进行组织管理workers.add(worker);}}// 5. 创建一个方法,提供添加任务的功能public void submit (Runnable runnable) {try {queue.put(runnable);} catch (InterruptedException e) {e.printStackTrace();}}}public class Thread_2545 {// 测试public static void main(String[] args) {// 创建线程池com.bitejiuyeke.exercises.MyThreadPool pool = new com.bitejiuyeke.exercises.MyThreadPool(10);// 添加任务for (int i = 0; i < 100; i++) {pool.submit(new Runnable() {@Overridepublic void run() {System.out.println("hello, thread pool");}});}}
}
2022-09-20_多线程面试题
描述一下线程池的执行流程和拒绝策略有哪些?
使用ThreadPoolExecutor创建一个忽略最新任务的线程池
/*** 使用ThreadPoolExecutor创建一个忽略最新任务的线程池,创建规则:* 1.核心线程数为5* 2.最大线程数为10* 3.任务队列为100* 4.拒绝策略为忽略最新任务** @Author 比特就业课* @Date 2022-06-23*/
public class Thread_2554 {public static void main(String[] args) throws InterruptedException {// 依题意创建线程池ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, // 核心线程数10, // 最大线程数3, // 线程空闲时长TimeUnit.SECONDS, // 线程空闲时长的时间单位new LinkedBlockingQueue<>(100), // 任务队列new ThreadPoolExecutor.DiscardPolicy()); // 拒绝策略为忽略最新任务// 测试执行for (int i = 0; i < 2000; i++) {new Thread(() -> {System.out.println(Thread.currentThread().getName() + "已执行.");},"thread-" + (i + 1)).start();}}
}
2022-09-25_多线程面试题
编写代码实现两个线程增加同一个变量
总结 HashTable, HashMap, ConcurrentHashMap 之间的区别
总结死锁的成因, 和解决方案.
简述 synchronized 和 ReentrantLock 之间的区别
synchronized是什么锁
总结锁策略, cas 和 synchronized 优化过程
参考博客
编写代码, 基于 Callable 实现 1+2+3+…+1000
/*** 编写代码, 基于 Callable 实现 1+2+3+...+1000** @Author 比特就业课* @Date 2022-06-22*/
public class Thread_2547 {public static void main(String[] args) {// Thread实现methods_01();// 线程池实现methods_02();}// methods_02 用线程池的方式实现// 把1000拆成10组,分别用10个线程去执行,最后汇总结果private static void methods_02() {// 定义一个Callable描述任务class MyCallable implements Callable<Integer> {// 每组数据的开始值private int start;// 每组数据的结束值private int end;// 构造方法,传入开始与结束值,明确累加区间public MyCallable(int start, int end) {this.start = start;this.end = end;}// 实现call方法@Overridepublic Integer call() throws Exception {// 完成累加操作并返回结果int sum = 0;for (int i = start; i <= end; i++) {sum += i;}return sum;}}// 将0 ~ 1000拆成果10组数据分别用10个线程去执行,最后统计结果int count = 10;// 创建线程池ExecutorService pool = Executors.newFixedThreadPool(5);// 定义一个Future数组Future<Integer>[] futures = new Future[count];// 拆分大数,并创建任务,提交到线程池for (int i = 0; i < count; i++) {int start = i * 100 + 1;int end = (i + 1) * 100;// 提交到线程池,并保存返回的FutuerFuture<Integer> future = pool.submit(new MyCallable(start, end));futures[i] = future;}// 先定义返回的结果,初始为0int result = 0;for (int i = 0; i < futures.length; i++) {// 获取FutureFuture<Integer> future = futures[i];try {// 从Future中获取每条线程执行的结果int sum = future.get();// 累加result += sum;} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();}}// 停止线程池pool.shutdown();System.out.println("methods_02结果为:" + result);}// 使用一条线程去执行任务public static void methods_01() {// 定义callable描述任务Callable<Integer> callable = new Callable<Integer>() {int sum = 0;@Overridepublic Integer call() throws Exception {for (int i = 1; i <= 1000; i++) {sum += i;}return sum;}};// 创建FutureTask并绑定Callable任务FutureTask<Integer> futureTask = new FutureTask<>(callable);// FutureTask也实现了Runnable可以做为入参Thread t = new Thread(futureTask);// 启动线程t.start();try {// 从Future中获取结果并打印Integer result = futureTask.get();System.out.println("methods_01结果为:" + result);} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();}}
}
编写代码, 基于 AtomicInteger 实现多线程自增同一个变量
/*** 编写代码, 基于 AtomicInteger 实现多线程自增同一个变量** @Author 比特就业课* @Date 2022-06-23*/
public class Thread_2548 {// 定义一个原子类,默认值为0private static volatile AtomicInteger counter = new AtomicInteger();public static void main(String[] args) throws InterruptedException {// 定义两个线程,分别对counter执行5W次自增Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {// 调用获取并自增的方法,类似于counter++操作// 原子类里要通过这个方法来完成counter.getAndIncrement();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {// 调用获取并自增的方法,类似于counter++操作// 原子类里要通过这个方法来完成counter.getAndIncrement();}});// 启动线程t1.start();t2.start();// 等待线程完成t1.join();t2.join();// 打印结果System.out.println("结果 = " + counter.get());}
}
编写代码, 基于 CountDownLatch 模拟跑步比赛的过程
/*** 编写代码, 基于 CountDownLatch 模拟跑步比赛的过程** @Author 比特就业课* @Date 2022-06-23*/
public class Thread_2549 {// 定义参赛选手数量private static int playerCount = 5;// 定义选手准备的CountDownLatchprivate static CountDownLatch standby = new CountDownLatch(playerCount);// 定义起跑的CountDownLatchprivate static CountDownLatch start = new CountDownLatch(1);// 定义选手到达终点的CountDownLatchprivate static CountDownLatch finished = new CountDownLatch(playerCount);static class Runner implements Runnable {@Overridepublic void run() {// 准备就绪,计数减1System.out.println(Thread.currentThread().getName() + " 准备就绪.");standby.countDown();try {// 等待开始比赛start.await();System.out.println(Thread.currentThread().getName() + "出发");// 模拟比赛用时Random random = new Random();int time = random.nextInt(10) + 10;Thread.sleep(time);System.out.println(Thread.currentThread().getName() + "到在终点,用时 " + time + "秒.");// 计数减1finished.countDown();} catch (InterruptedException e) {e.printStackTrace();}}}public static void main(String[] args) throws InterruptedException {// 定义选手线程并启动for (int i = 0; i < playerCount; i++) {new Thread(new Runner(), "player " + (i + 1)).start();}// 等待选手准备standby.await();// 等待参赛选手准备就绪System.out.println("所有参赛选手准备就绪,比赛开始...");// 发令枪响start.countDown();// 比赛中,等待选手到达终点finished.await();System.out.println("比赛结束.");}
}