├── .idea
├── .gitignore
├── inspectionProfiles
│ └── Project_Default.xml
├── misc.xml
├── modules.xml
├── msJava.iml
├── uiDesigner.xml
└── vcs.xml
├── .nojekyll
├── README.md
├── _config.yml
├── _coverpage.md
├── _navbar.md
├── _sidebar.md
├── docs
├── Java并发编程
│ ├── README.md
│ ├── _sidebar.md
│ ├── 为什么说本质上实现线程的方法只有一种.md
│ ├── 可能会遇到的三类线程安全问题.md
│ ├── 哪些场景需要额外注意线程安全问题.md
│ ├── 理解CAS优缺点.md
│ ├── 理解Callable和Runnable的不同.md
│ ├── 理解JVM内存结构与Java内存模型.md
│ ├── 理解Java中的各种锁.md
│ ├── 理解Java中的锁及其特点.md
│ ├── 理解ThreadLocal.md
│ ├── 理解synchronized关键字.md
│ ├── 理解线程与死锁.md
│ ├── 理解线程安全synchronized与ReentrantLock.md
│ ├── 理解线程池.md
│ ├── 理解线程池4种拒绝策略.md
│ ├── 理解线程的状态及如何进行转换的.md
│ └── 第2章Java并发机制的底层原理.md
├── Java开发规范
│ └── Java开发手册嵩山版.md
├── Java性能优化
│ ├── README.md
│ ├── _sidebar.md
│ └── 常见Java代码优化法则.md
├── Java核心基础
│ ├── README.md
│ ├── _sidebar.md
│ ├── 理解ConcurrentHashMap底层实现原理.md
│ ├── 理解HashMap为什么是线程不安全的.md
│ ├── 理解HashMap底层实现原理.md
│ ├── 理解Java中的各种锁.md
│ ├── 理解Java关键字.md
│ ├── 理解String字符串.md
│ ├── 理解克隆与序列化应用.md
│ ├── 理解动态代理.md
│ ├── 理解各种内部类和枚举类.md
│ ├── 理解基本数据类型与包装类.md
│ ├── 理解异常处理.md
│ ├── 理解抽象类与接口.md
│ ├── 理解数据结构队列.md
│ ├── 理解泛型与迭代器.md
│ ├── 理解浅克隆和深克隆.md
│ ├── 理解类与Object.md
│ ├── 理解集合Collection.md
│ ├── 理解集合Map.md
│ └── 理解面向对象.md
├── Java源码分析
│ ├── ArrayList源码分析.md
│ ├── ConcurrentHashMap源码分析.md
│ ├── HashMap源码分析.md
│ ├── HashSet与TreeSet源码分析.md
│ ├── LinkedHashMap源码分析.md
│ ├── README.md
│ ├── TreeMap源码分析.md
│ └── _sidebar.md
├── Java虚拟机
│ ├── JVM确认可回收对象的方式.md
│ ├── Java运行时内存划分.md
│ ├── README.md
│ ├── _sidebar.md
│ ├── 双亲委派机制.md
│ ├── 四种引用类型.md
│ ├── 垃圾回收器.md
│ ├── 垃圾回收算法.md
│ └── 类加载机制.md
├── MyBatis
│ └── 深入剖析 MyBatis 核心原理
│ │ ├── MyBatis 三层架构图.png
│ │ └── MyBatis 执行一条 SQL 语句的核心过程.png
├── MySQL
│ ├── B树与B+树详谈.md
│ ├── Hash索引与B+树索引的区别.md
│ ├── MySQL基础概念.md
│ ├── MySQL实战宝典
│ │ └── 08 索引:排序的艺术.md
│ ├── README.md
│ ├── SQL经典笔试题目.md
│ ├── SQL进阶.md
│ ├── _sidebar.md
│ ├── 《MySQL必知必会》.md
│ ├── 什么情况下索引失效.md
│ ├── 什么时候不需要创建索引.md
│ ├── 如何使用EXPLAIN查看执行计划.md
│ ├── 如何使用索引.md
│ ├── 常见SQL优化方式.md
│ └── 浅谈MySQL的优化方案.md
├── Redis
│ ├── README.md
│ └── _sidebar.md
├── SpringBoot
│ ├── README.md
│ ├── SpringBoot的常用注解.md
│ ├── _sidebar.md
│ └── 基于SpringBoot集成Mybatis-Plus实现代码生成器.md
├── 工具
│ └── Emoji符号大全.md
├── 算法与数据结构
│ ├── README.md
│ ├── _sidebar.md
│ ├── 剑指offer题解.md
│ ├── 动态规划.md
│ ├── 图.md
│ ├── 排序算法.md
│ ├── 散列表.md
│ ├── 数据结构与算法专栏
│ │ ├── 01 为什么要学习数据结构和算法?.md
│ │ ├── 02 如何抓住重点,系统高效地学习数据结构与算法?.md
│ │ ├── 03 复杂度分析(上):如何分析、统计算法的执行效率和资源消耗?.md
│ │ ├── 04 复杂度分析(下):浅析最好、最坏、平均、均摊时间复杂度.md
│ │ ├── 05 数组:为什么很多编程语言中数组都从0开始编号?.md
│ │ ├── 06 链表(上):如何实现LRU缓存淘汰算法.md
│ │ ├── README.md
│ │ └── _sidebar.md
│ ├── 数据结构与算法面试宝典专栏
│ │ ├── 01 栈:从简单栈到单调栈,解决经典栈问题.md
│ │ ├── _sidebar.md
│ │ └── 树.md
│ ├── 数组.md
│ ├── 栈.md
│ ├── 树.md
│ ├── 算法概述.md
│ ├── 贪心算法.md
│ ├── 递归算法.md
│ ├── 链表.md
│ └── 队列.md
├── 计算机网络
│ ├── OSI七层模型.md
│ ├── README.md
│ ├── _sidebar.md
│ ├── 理解HTTP与HTTPS.md
│ ├── 理解TCP和UDP.md
│ └── 网络协议分层.md
├── 设计模式
│ ├── README.md
│ ├── _sidebar.md
│ ├── 单例模式.md
│ ├── 原型模式.md
│ ├── 工厂模式.md
│ ├── 抽象工厂模式.md
│ ├── 设计模式六大设计原则.md
│ └── 设计模式总结.md
├── 资源分享
│ └── 编程人生.md
├── 踩坑记录
│ ├── IDEAMaven依赖成功导入但仍然报错找不到包解决方案.md
│ ├── README.md
│ └── _sidebar.md
└── 面试题
│ ├── Java核心面试题汇总.md
│ ├── Java虚拟机面试题汇总.md
│ ├── MyBatis面试题汇总.md
│ ├── MySQL面试题汇总.md
│ ├── README.md
│ ├── SpringBoot面试题汇总.md
│ ├── SpringMVC面试题汇总.md
│ ├── Spring面试题汇总.md
│ ├── _sidebar.md
│ ├── 分布式框架面试题汇总.md
│ ├── 框架基础面试题.md
│ ├── 消息队列面试题汇总.md
│ ├── 算法常用面试题汇总.md
│ └── 设计模式常见面试题汇总.md
├── image
├── 公众号二维码.jpg
├── 单向循环链表.png
├── 单链表.png
├── 单链表中间删除元素.png
├── 单链表中间新增元素.png
├── 单链表头部删除元素.png
├── 单链表头部新增元素.png
├── 单链表尾部删除元素.png
├── 单链表尾部新增元素.png
├── 单链表更新元素.png
├── 单链表查找元素.png
├── 双向循环链表.png
├── 双向链表.png
├── 循环链表.png
├── 数据结构与算法专栏.png
├── 数组.png
├── 数组1.png
├── 数组2.png
├── 数组3.png
├── 数组4.png
├── 栈.276ioja9ty4.png
├── 码上Java.webp
├── 设计模式思维导图.png
├── 链表.png
├── 链表单个节点.png
├── 顺序栈与链式栈.png
├── 顺序队列—入队.5zboyf0cmqc0.png
├── 顺序队列—出队.5kqhi5ajn0w0.png
└── 顺序队列与链式队列.1y4xfbj2zun4.png
├── index.html
├── msJavaCoder.iml
├── package-lock.json
├── src
└── top
│ └── msjava
│ └── Main.java
└── sw.js
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Datasource local storage ignored files
5 | /dataSources/
6 | /dataSources.local.xml
7 | # Editor-based HTTP Client requests
8 | /httpRequests/
9 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/msJava.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/msJavaCoder/msJava/2553fc4f1c5b62305a685ad6a051031731ccc251/.nojekyll
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🔥 微信公众号 : 码上Java
2 |
3 | [](https://jq.qq.com/?_wv=1027&k=5HPYvQk)
4 | 
5 | [](https://msjavacoder.github.io/msJava)
6 | [](https://msjavacoder.gitee.io/msjava)
7 | 
8 | 
9 |
10 | ---
11 | 
12 |
13 | ---
14 |
15 | ### ☎ 联系作者
16 | > 关注微信订阅号: 码上Java 🔥🔥🔥
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-cayman
--------------------------------------------------------------------------------
/_coverpage.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 |
JAVA后端核心知识体系
4 | 风儿哪儿吹,不要问跟风的人。💯
5 |
6 | 
7 | 
8 | 
9 | 
10 |
11 | [GitHub](https://github.com/msJavaCoder/msJava)
12 | [开始阅读](#🔥-微信公众号-:-码上java)
13 |
14 |
15 |
16 | 
17 |
18 | 
--------------------------------------------------------------------------------
/_navbar.md:
--------------------------------------------------------------------------------
1 | * Java
2 | * [Java核心基础](docs/Java核心基础/README.md)
3 | * [Java源码分析](docs/Java源码分析/README.md)
4 | * [Java性能优化](docs/Java性能优化/README.md)
5 | * [Java并发编程](docs/Java并发编程/README.md)
6 | * [Java虚拟机](docs/Java虚拟机/README.md)
7 |
8 | * 主流框架
9 | * Spring
10 | * SpringMVC
11 | * MyBatis
12 | * [SpringBoot](docs/SpringBoot/README.md)
13 | * SpringCloud
14 |
15 | * 数据库
16 | * [MySQL](docs/MySQL/README.md)
17 | * [Redis](docs/Redis/README.md)
18 |
19 |
20 |
21 | * 数据结构与算法
22 | * [排序算法](docs/算法与数据结构/排序算法.md)
23 | * [数据结构](docs/算法与数据结构/README.md)
24 | * [剑指offer](docs/算法与数据结构/剑指offer题解.md)
25 |
26 | * 计算机基础
27 | * 操作系统
28 | * 计算机组成原理
29 | * 计算机网络
30 |
31 | * 项目实战
32 | * 个人博客
33 | * wiki知识库
34 | * ···
35 |
36 | * [面试题](docs/面试题/README.md)
37 |
38 | * [首页](/)
39 |
40 |
--------------------------------------------------------------------------------
/_sidebar.md:
--------------------------------------------------------------------------------
1 |
2 | * [**📕 数据结构与算法**](docs/算法与数据结构/README.md)
3 |
4 |
5 | ---
6 |
7 | * [**💦 Java核心基础**](docs/Java核心基础/README.md)
8 |
9 | ---
10 |
11 | * [**🙈 Java源码分析**](docs/Java源码分析/README.md)
12 |
13 | ---
14 |
15 | * [**🐯 Java并发编程**](docs/Java并发编程/README.md)
16 |
17 | ---
18 |
19 | * [**🐷 Java虚拟机**](docs/Java虚拟机/README.md)
20 |
21 | ---
22 |
23 | * [**🌱 SpringBoot**](docs/SpringBoot/README.md)
24 |
25 |
26 | ---
27 |
28 | * [**📚 MySQL**](docs/MySQL/README.md)
29 |
30 | ---
31 |
32 | * **🌀 Redis**
33 |
34 |
35 | ---
36 |
37 | * [**📑 设计模式**](docs/设计模式/README.md)
38 |
39 | ---
40 |
41 | * [**👀 面试题**](docs/面试题/README.md)
42 |
43 | ---
44 |
45 | * **🔨 杂谈**
46 |
47 | ---
48 |
--------------------------------------------------------------------------------
/docs/Java并发编程/README.md:
--------------------------------------------------------------------------------
1 | # Java并发编程
2 | * [为什么说本质上实现线程的方法只有一种](docs/Java并发编程/为什么说本质上实现线程的方法只有一种.md)
3 | * [可能会遇到的三类线程安全问题](docs/Java并发编程/可能会遇到的三类线程安全问题.md)
4 | * [哪些场景需要额外注意线程安全问题](docs/Java并发编程/哪些场景需要额外注意线程安全问题.md)
5 | * [理解Callable和Runnable的不同](docs/Java并发编程/理解Callable和Runnable的不同.md)
6 | * [理解CAS优缺点](docs/Java并发编程/理解CAS优缺点.md)
7 | * [理解Java中的各种锁](docs/Java并发编程/理解Java中的各种锁.md)
8 | * [理解Java中的锁及其特点](docs/Java并发编程/理解Java中的锁及其特点.md)
9 | * [理解JVM内存结构与Java内存模型](docs/Java并发编程/理解JVM内存结构与Java内存模型.md)
10 | * [理解synchronized关键字](docs/Java并发编程/理解synchronized关键字.md)
11 | * [理解ThreadLocal](docs/Java并发编程/理解ThreadLocal.md)
12 | * [理解线程与死锁](docs/Java并发编程/理解线程与死锁.md)
13 | * [理解线程安全synchronized与ReentrantLock](docs/Java并发编程/理解线程安全synchronized与ReentrantLock.md)
14 | * [理解线程池](docs/Java并发编程/理解线程池.md)
15 | * [理解线程池4种拒绝策略](docs/Java并发编程/理解线程池4种拒绝策略.md)
16 | * [理解线程的状态及如何进行转换的](docs/Java并发编程/理解线程的状态及如何进行转换的.md)
17 | * [第2章Java并发机制的底层原理](docs/Java并发编程/为什么说本质上实现线程的方法只有一种.md)
18 |
--------------------------------------------------------------------------------
/docs/Java并发编程/_sidebar.md:
--------------------------------------------------------------------------------
1 | * **👉 Java并发编程** [↩](/README)
2 | * [为什么说本质上实现线程的方法只有一种](docs/Java并发编程/为什么说本质上实现线程的方法只有一种.md)
3 | * [可能会遇到的三类线程安全问题](docs/Java并发编程/可能会遇到的三类线程安全问题.md)
4 | * [哪些场景需要额外注意线程安全问题](docs/Java并发编程/哪些场景需要额外注意线程安全问题.md)
5 | * [理解Callable和Runnable的不同](docs/Java并发编程/理解Callable和Runnable的不同.md)
6 | * [理解CAS优缺点](docs/Java并发编程/理解CAS优缺点.md)
7 | * [理解Java中的各种锁](docs/Java并发编程/理解Java中的各种锁.md)
8 | * [理解Java中的锁及其特点](docs/Java并发编程/理解Java中的锁及其特点.md)
9 | * [理解JVM内存结构与Java内存模型](docs/Java并发编程/理解JVM内存结构与Java内存模型.md)
10 | * [理解synchronized关键字](docs/Java并发编程/理解synchronized关键字.md)
11 | * [理解ThreadLocal](docs/Java并发编程/理解ThreadLocal.md)
12 | * [理解线程与死锁](docs/Java并发编程/理解线程与死锁.md)
13 | * [理解线程安全synchronized与ReentrantLock](docs/Java并发编程/理解线程安全synchronized与ReentrantLock.md)
14 | * [理解线程池](docs/Java并发编程/理解线程池.md)
15 | * [理解线程池4种拒绝策略](docs/Java并发编程/理解线程池4种拒绝策略.md)
16 | * [理解线程的状态及如何进行转换的](docs/Java并发编程/理解线程的状态及如何进行转换的.md)
17 | * [第2章Java并发机制的底层原理](docs/Java并发编程/为什么说本质上实现线程的方法只有一种.md)
--------------------------------------------------------------------------------
/docs/Java并发编程/为什么说本质上实现线程的方法只有一种.md:
--------------------------------------------------------------------------------
1 | # 👉 为什么说本质上实现线程的方法只有一种?
2 |
3 | > 在本课时我们主要学习为什么说本质上只有一种实现线程的方式?实现 Runnable 接口究竟比继承 Thread 类实现线程好在哪里?
4 |
5 | 实现线程是并发编程中基础中的基础,因为我们必须要先实现多线程,才可以继续后续的一系列操作。所以本课时就先从并发编程的基础如何实现线程开始讲起,希望你能够夯实基础,虽然实现线程看似简单、基础,但实际上却暗藏玄机。首先,我们来看下为什么说本质上实现线程只有一种方式?
6 |
7 | 实现线程的方式到底有几种?大部分人会说有 2 种、3 种或是 4 种,很少有人会说有 1 种。我们接下来看看它们具体指什么?2 种实现方式的描述是最基本的,也是最为大家熟知的,我们就先来看看 2 种线程实现方式的源码。
8 |
9 | ## 实现 Runnable 接口
10 |
11 | ```java
12 | public class RunnableThread implements Runnable {
13 | @Override
14 | public void run() {
15 | System.out.println('用实现Runnable接口实现线程');
16 | }
17 | }
18 |
19 | ```
20 |
21 | 第 1 种方式是通过实现 Runnable 接口实现多线程,如代码所示,首先通过 RunnableThread 类实现 Runnable 接口,然后重写 run() 方法,之后只需要把这个实现了 run() 方法的实例传到 Thread 类中就可以实现多线程。
22 |
23 | ## 继承 Thread 类
24 |
25 | ```java
26 | public class ExtendsThread extends Thread {
27 | @Override
28 | public void run() {
29 | System.out.println('用Thread类实现线程');
30 | }
31 | }
32 |
33 | ```
34 |
35 | 第 2 种方式是继承 Thread 类,如代码所示,与第 1 种方式不同的是它没有实现接口,而是继承 Thread 类,并重写了其中的 run() 方法。相信上面这两种方式你一定非常熟悉,并且经常在工作中使用它们。
36 |
37 | ## 线程池创建线程
38 |
39 | 那么为什么说还有第 3 种或第 4 种方式呢?我们先来看看第 3 种方式:通过线程池创建线程。线程池确实实现了多线程,比如我们给线程池的线程数量设置成 10,那么就会有 10 个子线程来为我们工作,接下来,我们深入解析线程池中的源码,来看看线程池是怎么实现线程的?
40 |
41 | ```java
42 | static class DefaultThreadFactory implements ThreadFactory {
43 |
44 | DefaultThreadFactory() {
45 | SecurityManager s = System.getSecurityManager();
46 | group = (s != null) ? s.getThreadGroup() :
47 | Thread.currentThread().getThreadGroup();
48 | namePrefix = "pool-" +
49 | poolNumber.getAndIncrement() +
50 | "-thread-";
51 | }
52 |
53 |
54 | public Thread newThread(Runnable r) {
55 | Thread t = new Thread(group, r,
56 | namePrefix + threadNumber.getAndIncrement(),
57 | 0);
58 |
59 | if (t.isDaemon())
60 | t.setDaemon(false);
61 | if (t.getPriority() != Thread.NORM_PRIORITY)
62 | t.setPriority(Thread.NORM_PRIORITY);
63 | return t;
64 | }
65 | }
66 |
67 | ```
68 |
69 | 对于线程池而言,本质上是通过线程工厂创建线程的,默认采用 DefaultThreadFactory ,它会给线程池创建的线程设置一些默认值,比如:线程的名字、是否是守护线程,以及线程的优先级等。但是无论怎么设置这些属性,最终它还是通过 new Thread() 创建线程的 ,只不过这里的构造函数传入的参数要多一些,由此可以看出通过线程池创建线程并没有脱离最开始的那两种基本的创建方式,因为本质上还是通过 new Thread() 实现的。
70 |
71 | 在面试中,如果你只是知道这种方式可以创建线程但不了解其背后的实现原理,就会在面试的过程中举步维艰,想更好的表现自己却给自己挖了“坑”。
72 |
73 | 所以我们在回答线程实现的问题时,描述完前两种方式,可以进一步引申说“我还知道线程池和Callable 也是可以创建线程的,但是它们本质上也是通过前两种基本方式实现的线程创建。”这样的回答会成为面试中的加分项。然后面试官大概率会追问线程池的构成及原理,这部分内容会在后面的课时中详细分析。
74 |
75 | ## 有返回值的 Callable 创建线程
76 |
77 | ```java
78 | class CallableTask implements Callable {
79 |
80 | @Override
81 | public Integer call() throws Exception {
82 | return new Random().nextInt();
83 | }
84 | }
85 |
86 | //创建线程池
87 | ExecutorService service = Executors.newFixedThreadPool(10);
88 | //提交任务,并用 Future提交返回结果
89 | Future future = service.submit(new CallableTask());
90 |
91 | ```
92 |
93 | 第 4 种线程创建方式是通过有返回值的 Callable 创建线程,Runnable 创建线程是无返回值的,而 Callable 和与之相关的 Future、FutureTask,它们可以把线程执行的结果作为返回值返回,如代码所示,实现了 Callable 接口,并且给它的泛型设置成 Integer,然后它会返回一个随机数。
94 |
95 | 但是,无论是 Callable 还是 FutureTask,它们首先和 Runnable 一样,都是一个任务,是需要被执行的,而不是说它们本身就是线程。它们可以放到线程池中执行,如代码所示, submit() 方法把任务放到线程池中,并由线程池创建线程,不管用什么方法,最终都是靠线程来执行的,而子线程的创建方式仍脱离不了最开始讲的两种基本方式,也就是实现 Runnable 接口和继承 Thread 类。
96 |
97 | ## 其他创建方式
98 |
99 | #### 定时器 Timer
100 |
101 | ```java
102 | class TimerThread extends Thread {
103 | //具体实现
104 | }
105 | ```
106 |
107 | 讲到这里你可能会说,我还知道一些其他的实现线程的方式。比如,定时器也可以实现线程,如果新建一个 Timer,令其每隔 10 秒或设置两个小时之后,执行一些任务,那么这时它确实也创建了线程并执行了任务,但如果我们深入分析定时器的源码会发现,本质上它还是会有一个继承自 Thread 类的 TimerThread,所以定时器创建线程最后又绕回到最开始说的两种方式。
108 |
109 | #### 其他方法
110 |
111 | ```java
112 | /**
113 | *描述:匿名内部类创建线程
114 | */
115 | new Thread(new Runnable() {
116 | @Override
117 | public void run() {
118 | System.out.println(Thread.currentThread().getName());
119 | }
120 | }).start();
121 |
122 | }
123 | }
124 | ```
125 |
126 | 或许你还会说,我还知道一些其他方式,比如匿名内部类或 lambda 表达式方式,实际上,匿名内部类或 lambda 表达式创建线程,它们仅仅是在语法层面上实现了线程,并不能把它归结于实现多线程的方式,如匿名内部类实现线程的代码所示,它仅仅是用一个匿名内部类把需要传入的 Runnable 给实例出来。
127 |
128 | ```java
129 | new Thread(() -> System.out.println(Thread.currentThread().getName())).start();
130 |
131 | }
132 | ```
133 |
134 | 我们再来看下 lambda 表达式方式。如代码所示,最终它们依然符合最开始所说的那两种实现线程的方式。
135 |
136 | ## 实现线程只有一种方式
137 |
138 | 关于这个问题,我们先不聚焦为什么说创建线程只有一种方式,先认为有两种创建线程的方式,而其他的创建方式,比如线程池或是定时器,它们仅仅是在 new Thread() 外做了一层封装,如果我们把这些都叫作一种新的方式,那么创建线程的方式便会千变万化、层出不穷,比如 JDK 更新了,它可能会多出几个类,会把 new Thread() 重新封装,表面上看又会是一种新的实现线程的方式,透过现象看本质,打开封装后,会发现它们最终都是基于 Runnable 接口或继承 Thread 类实现的。
139 |
140 | 接下来,我们进行更深层次的探讨,为什么说这两种方式本质上是一种呢?
141 |
142 | ```java
143 | @Override
144 | public void run() {
145 | if (target != null) {
146 | target.run();
147 | }
148 | }
149 |
150 | ```
151 |
152 | 首先,启动线程需要调用 start() 方法,而 start() 方法最终还会调用 run() 方法,我们先来看看第一种方式中 run() 方法究竟是怎么实现的,可以看出 run() 方法的代码非常短小精悍,第 1 行代码 **if (target != null)** ,判断 target 是否等于 null,如果不等于 null,就执行第 2 行代码 target.run(),而 target 实际上就是一个 Runnable,即使用 Runnable 接口实现线程时传给Thread类的对象。
153 |
154 | 然后,我们来看第二种方式,也就是继承 Thread 方式,实际上,继承 Thread 类之后,会把上述的 run() 方法重写,重写后 run() 方法里直接就是所需要执行的任务,但它最终还是需要调用 thread.start() 方法来启动线程,而 start() 方法最终也会调用这个已经被重写的 run() 方法来执行它的任务,这时我们就可以彻底明白了,事实上创建线程只有一种方式,就是构造一个 Thread 类,这是创建线程的唯一方式。
155 |
156 | 我们上面已经了解了两种创建线程方式本质上是一样的,它们的不同点仅仅在于**实现线程运行内容的不同**,那么运行内容来自于哪里呢?
157 |
158 | 运行内容主要来自于两个地方,要么来自于 target,要么来自于重写的 run() 方法,在此基础上我们进行拓展,可以这样描述:本质上,实现线程只有一种方式,而要想实现线程执行的内容,却有两种方式,也就是可以通过 实现 Runnable 接口的方式,或是继承 Thread 类重写 run() 方法的方式,把我们想要执行的代码传入,让线程去执行,在此基础上,如果我们还想有更多实现线程的方式,比如线程池和 Timer 定时器,只需要在此基础上进行封装即可。
159 |
160 | ## 实现 Runnable 接口比继承 Thread 类实现线程要好
161 |
162 | 下面我们来对刚才说的两种实现线程内容的方式进行对比,也就是为什么说实现 Runnable 接口比继承 Thread 类实现线程要好?好在哪里呢?
163 |
164 | 首先,我们从代码的架构考虑,实际上,Runnable 里只有一个 run() 方法,它定义了需要执行的内容,在这种情况下,实现了 Runnable 与 Thread 类的解耦,Thread 类负责线程启动和属性设置等内容,权责分明。
165 |
166 | 第二点就是在某些情况下可以提高性能,使用继承 Thread 类方式,每次执行一次任务,都需要新建一个独立的线程,执行完任务后线程走到生命周期的尽头被销毁,如果还想执行这个任务,就必须再新建一个继承了 Thread 类的类,如果此时执行的内容比较少,比如只是在 run() 方法里简单打印一行文字,那么它所带来的开销并不大,相比于整个线程从开始创建到执行完毕被销毁,这一系列的操作比 run() 方法打印文字本身带来的开销要大得多,相当于捡了芝麻丢了西瓜,得不偿失。如果我们使用实现 Runnable 接口的方式,就可以把任务直接传入线程池,使用一些固定的线程来完成任务,不需要每次新建销毁线程,大大降低了性能开销。
167 |
168 | 第三点好处在于 Java 语言不支持双继承,如果我们的类一旦继承了 Thread 类,那么它后续就没有办法再继承其他的类,这样一来,如果未来这个类需要继承其他类实现一些功能上的拓展,它就没有办法做到了,相当于限制了代码未来的可拓展性。
169 |
170 | 综上所述,我们应该优先选择通过实现 Runnable 接口的方式来创建线程。
171 |
172 | 好啦,本课时的全部内容就讲完了,在这一课时我们主要学习了 通过 Runnable 接口和继承 Thread 类等几种方式创建线程,又详细分析了为什么说本质上只有一种实现线程的方式,以及实现 Runnable 接口究竟比继承 Thread 类实现线程好在哪里?学习完本课时相信你一定对创建线程有了更深入的理解。
--------------------------------------------------------------------------------
/docs/Java并发编程/可能会遇到的三类线程安全问题.md:
--------------------------------------------------------------------------------
1 | # 👉 可能会遇到的三类线程安全问题
2 |
3 | >本文我们一起学习在实际工作中可能会遇到的三类线程安全问题。
4 | ## 什么是线程安全
5 | 要想弄清楚有哪 3 类线程安全问题,首先需要了解什么是线程安全,线程安全经常在工作中被提到,比如:你的对象不是线程安全的,你的线程发生了安全错误,虽然线程安全经常被提到,但我们可能对线程安全并没有一个明确的定义。
6 |
7 | 《Java Concurrency In Practice》的作者 Brian Goetz 对线程安全是这样理解的,当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行问题,也不需要进行额外的同步,而调用这个对象的行为都可以获得正确的结果,那这个对象便是线程安全的。
8 |
9 | 事实上,Brian Goetz 想表达的意思是,如果某个对象是线程安全的,那么对于使用者而言,在使用时就不需要考虑方法间的协调问题,比如不需要考虑不能同时写入或读写不能并行的问题,也不需要考虑任何额外的同步问题,比如不需要额外自己加 synchronized 锁,那么它才是线程安全的,可以看出对线程安全的定义还是非常苛刻的。
10 |
11 | 而我们在实际开发中经常会遇到线程不安全的情况,那么一共有哪 3 种典型的线程安全问题呢?
12 | 1. 运行结果错误;
13 | 2. 发布和初始化导致线程安全问题;
14 | 3. 活跃性问题
15 | ### 运行结果错误
16 | 首先,来看多线程同时操作一个变量导致的运行结果错误。
17 | ```java
18 | public class WrongResult {
19 |
20 | volatile static int i;
21 |
22 | public static void main(String[] args) throws InterruptedException {
23 | Runnable r = new Runnable() {
24 | @Override
25 | public void run() {
26 | for (int j = 0; j < 10000; j++) {
27 | i++;
28 | }
29 | }
30 | };
31 | Thread thread1 = new Thread(r);
32 | thread1.start();
33 | Thread thread2 = new Thread(r);
34 | thread2.start();
35 | thread1.join();
36 | thread2.join();
37 | System.out.println(i);
38 | }
39 | }
40 |
41 |
42 | ```
43 | 如代码所示,首先定义了一个 int 类型的静态变量 i,然后启动两个线程,分别对变量 i 进行 10000 次 i++ 操作。理论上得到的结果应该是 20000,但实际结果却远小于理论结果,比如可能是12996,也可能是13323,每次的结果都还不一样,这是为什么呢?
44 |
45 | 是因为在多线程下,CPU 的调度是以时间片为单位进行分配的,每个线程都可以得到一定量的时间片。但如果线程拥有的时间片耗尽,它将会被暂停执行并让出 CPU 资源给其他线程,这样就有可能发生线程安全问题。比如 i++ 操作,表面上看只是一行代码,但实际上它并不是一个原子操作,它的执行步骤主要分为三步,而且在每步操作之间都有可能被打断。
46 |
47 | - 第一个步骤是读取;
48 | - 第二个步骤是增加;
49 | - 第三个步骤是保存。那么我们接下来看如何发生的线程不安全问题。
50 |
51 |
52 | 
53 |
54 | 我们根据箭头指向依次看,线程 1 首先拿到 i=1 的结果,然后进行 i+1 操作,但此时 i+1 的结果并没有保存下来,线程 1 就被切换走了,于是 CPU 开始执行线程 2,它所做的事情和线程 1 是一样的 i++ 操作,但此时我们想一下,它拿到的 i 是多少?实际上和线程 1 拿到的 i 的结果一样都是 1,为什么呢?因为线程 1 虽然对 i 进行了 +1 操作,但结果没有保存,所以线程 2 看不到修改后的结果。
55 |
56 | 然后假设等线程 2 对 i 进行 +1 操作后,又切换到线程 1,让线程 1 完成未完成的操作,即将 i+1 的结果 2 保存下来,然后又切换到线程 2 完成 i=2 的保存操作,虽然两个线程都执行了对 i 进行 +1 的操作,但结果却最终保存了 i=2 的结果,而不是我们期望的 i=3,这样就发生了线程安全问题,导致了数据结果错误,这也是最典型的线程安全问题。
57 | ### 发布和初始化导致线程安全问题
58 |
59 | 第二种是对象发布和初始化时导致的线程安全问题,我们创建对象并进行发布和初始化供其他类或对象使用是常见的操作,但如果我们操作的时间或地点不对,就可能导致线程安全问题。如代码所示:
60 | ```java
61 | public class WrongInit {
62 |
63 | private Map students;
64 |
65 | public WrongInit() {
66 | new Thread(new Runnable() {
67 | @Override
68 | public void run() {
69 | students = new HashMap<>();
70 | students.put(1, "王小美");
71 | students.put(2, "钱二宝");
72 | students.put(3, "周三");
73 | students.put(4, "赵四");
74 | }
75 | }).start();
76 | }
77 |
78 | public Map getStudents() {
79 | return students;
80 | }
81 |
82 | public static void main(String[] args) throws InterruptedException {
83 | WrongInit multiThreadsError6 = new WrongInit();
84 | System.out.println(multiThreadsError6.getStudents().get(1));
85 |
86 | }
87 | }
88 |
89 | ```
90 | 在类中,定义一个类型为 Map 的成员变量 students,Integer 是学号,String 是姓名。然后在构造函数中启动一个新线程,并在线程中为 students 赋值。
91 |
92 | - 学号:1,姓名:王小美;
93 | - 学号:2,姓名:钱二宝;
94 | - 学号:3,姓名:周三;
95 | - 学号:4,姓名:赵四。
96 |
97 | 只有当线程运行完 run() 方法中的全部赋值操作后,4 名同学的全部信息才算是初始化完毕,可是我们看在主函数 mian() 中,初始化 WrongInit 类之后并没有进行任何休息就直接打印 1 号同学的信息,试想这个时候程序会出现什么情况?实际上会发生空指针异常。
98 | ```java
99 |
100 | Exception in thread "main" java.lang.NullPointerException
101 | at lesson6.WrongInit.main(WrongInit.java:32)
102 | ```
103 | ## 活跃性问题
104 | 第三种线程安全问题统称为活跃性问题,最典型的有三种,分别为死锁、活锁和饥饿。
105 |
106 | 什么是活跃性问题呢,活跃性问题就是程序始终得不到运行的最终结果,相比于前面两种线程安全问题带来的数据错误或报错,活跃性问题带来的后果可能更严重,比如发生死锁会导致程序完全卡死,无法向下运行。
107 | ### 死锁
108 | 最常见的活跃性问题是死锁,死锁是指两个线程之间相互等待对方资源,但同时又互不相让,都想自己先执行,如代码所示。
109 | ```java
110 | public class MayDeadLock {
111 |
112 | Object o1 = new Object();
113 | Object o2 = new Object();
114 |
115 | public void thread1() throws InterruptedException {
116 | synchronized (o1) {
117 | Thread.sleep(500);
118 | synchronized (o2) {
119 | System.out.println("线程1成功拿到两把锁");
120 | }
121 | }
122 | }
123 |
124 | public void thread2() throws InterruptedException {
125 | synchronized (o2) {
126 | Thread.sleep(500);
127 | synchronized (o1) {
128 | System.out.println("线程2成功拿到两把锁");
129 | }
130 | }
131 | }
132 |
133 | public static void main(String[] args) {
134 | MayDeadLock mayDeadLock = new MayDeadLock();
135 | new Thread(new Runnable() {
136 | @Override
137 | public void run() {
138 | try {
139 | mayDeadLock.thread1();
140 | } catch (InterruptedException e) {
141 | e.printStackTrace();
142 | }
143 | }
144 | }).start();
145 | new Thread(new Runnable() {
146 | @Override
147 | public void run() {
148 | try {
149 | mayDeadLock.thread2();
150 | } catch (InterruptedException e) {
151 | e.printStackTrace();
152 | }
153 | }
154 | }).start();
155 | }
156 | }
157 |
158 | ```
159 | 首先,代码中创建了两个 Object 作为 synchronized 锁的对象,线程 1 先获取 o1 锁,sleep(500) 之后,获取 o2 锁;线程 2 与线程 1 执行顺序相反,先获取 o2 锁,sleep(500) 之后,获取 o1 锁。 假设两个线程几乎同时进入休息,休息完后,线程 1 想获取 o2 锁,线程 2 想获取 o1 锁,这时便发生了死锁,两个线程不主动调和,也不主动退出,就这样死死地等待对方先释放资源,导致程序得不到任何结果也不能停止运行。
160 | ### 活锁
161 | 第二种活跃性问题是活锁,活锁与死锁非常相似,也是程序一直等不到结果,但对比于死锁,活锁是活的,什么意思呢?因为正在运行的线程并没有阻塞,它始终在运行中,却一直得不到结果。
162 |
163 | 举一个例子,假设有一个消息队列,队列里放着各种各样需要被处理的消息,而某个消息由于自身被写错了导致不能被正确处理,执行时会报错,可是队列的重试机制会重新把它放在队列头进行优先重试处理,但这个消息本身无论被执行多少次,都无法被正确处理,每次报错后又会被放到队列头进行重试,周而复始,最终导致线程一直处于忙碌状态,但程序始终得不到结果,便发生了活锁问题。
164 | ### 饥饿
165 | 第三个典型的活跃性问题是饥饿,饥饿是指线程需要某些资源时始终得不到,尤其是CPU 资源,就会导致线程一直不能运行而产生的问题。在 Java 中有线程优先级的概念,Java 中优先级分为 1 到 10,1 最低,10 最高。如果我们把某个线程的优先级设置为 1,这是最低的优先级,在这种情况下,这个线程就有可能始终分配不到 CPU 资源,而导致长时间无法运行。或者是某个线程始终持有某个文件的锁,而其他线程想要修改文件就必须先获取锁,这样想要修改文件的线程就会陷入饥饿,长时间不能运行。
166 | ## 总结
167 | 通过本文的学习我们知道了线程安全问题主要有 3 种,i++ 等情况导致的运行结果错误,通常是因为并发读写导致的,第二种是对象没有在正确的时间、地点被发布或初始化,而第三种线程安全问题就是活跃性问题,包括死锁、活锁和饥饿。
168 |
169 |
170 |
171 |
172 |
173 |
--------------------------------------------------------------------------------
/docs/Java并发编程/哪些场景需要额外注意线程安全问题.md:
--------------------------------------------------------------------------------
1 | # 👉 哪些场景需要额外注意线程安全问题
2 |
3 | > 本文我们一起学习在实际开发中哪些场景需要额外注意线程安全问题?
4 | ## 1.访问共享变量或资源
5 |
6 | 第一种场景是访问共享变量或共享资源的时候,典型的场景有访问共享对象的属性,访问 static 静态变量,访问共享的缓存,等等。因为这些信息不仅会被一个线程访问到,还有可能被多个线程同时访问,那么就有可能在并发读写的情况下发生线程安全问题。比如我们上一课时讲过的多线程同时 i++ 的例子:
7 |
8 | ```java
9 | /**
10 | * 描述: 共享的变量或资源带来的线程安全问题
11 | */
12 | public class ThreadNotSafe1 {
13 | static int i;
14 | public static void main(String[] args) throws InterruptedException {
15 | Runnable r = new Runnable() {
16 | @Override
17 | public void run() {
18 | for (int j = 0; j < 10000; j++) {
19 | i++;
20 | }
21 | }
22 | };
23 | Thread thread1 = new Thread(r);
24 | Thread thread2 = new Thread(r);
25 | thread1.start();
26 | thread2.start();
27 | thread1.join();
28 | thread2.join();
29 | System.out.println(i);
30 | }
31 | }
32 | ```
33 | 如上述代码所示,两个线程同时对 i 进行 i++ 操作,最后的输出可能是 15875 等小于20000的数,而不是我们期待的20000,这便是非常典型的共享变量带来的线程安全问题。
34 |
35 | ## 2.依赖时序的操作
36 |
37 | 第二个需要我们注意的场景是依赖时序的操作,如果我们操作的正确性是依赖时序的,而在多线程的情况下又不能保障执行的顺序和我们预想的一致,这个时候就会发生线程安全问题,如下面的代码所示:
38 |
39 | ```java
40 | if (map.containsKey(key)) {
41 | map.remove(obj)
42 | }
43 | ```
44 | 代码中首先检查 map 中有没有 key 对应的元素,如果有则继续执行 remove 操作。此时,这个组合操作就是危险的,因为它是先检查后操作,而执行过程中可能会被打断。如果此时有两个线程同时进入 if() 语句,然后它们都检查到存在 key 对应的元素,于是都希望执行下面的 remove 操作,随后一个线程率先把 obj 给删除了,而另外一个线程它刚已经检查过存在 key 对应的元素,if 条件成立,所以它也会继续执行删除 obj 的操作,但实际上,集合中的 obj 已经被前面的线程删除了,这种情况下就可能导致线程安全问题。
45 |
46 | 类似的情况还有很多,比如我们先检查 x=1,如果 x=1 就修改 x 的值,代码如下所示:
47 |
48 | ```
49 | if (x == 1) {
50 | x = 7 * x;
51 | }
52 |
53 | ```
54 | 这样类似的场景都是同样的道理,“检查与执行”并非原子性操作,在中间可能被打断,而检查之后的结果也可能在执行时已经过期、无效,换句话说,获得正确结果取决于幸运的时序。这种情况下,我们就需要对它进行加锁等保护措施来保障操作的原子性。
55 |
56 | ## 3.不同数据之间存在绑定关系
57 |
58 | 第三种需要我们注意的线程安全场景是不同数据之间存在相互绑定关系的情况。有时候,我们的不同数据之间是成组出现的,存在着相互对应或绑定的关系,最典型的就是 IP 和端口号。有时候我们更换了 IP,往往需要同时更换端口号,如果没有把这两个操作绑定在一起,就有可能出现单独更换了 IP 或端口号的情况,而此时信息如果已经对外发布,信息获取方就有可能获取一个错误的 IP 与端口绑定情况,这时就发生了线程安全问题。在这种情况下,我们也同样需要保障操作的原子性。
59 |
60 | ## 4.对方没有声明自己是线程安全的
61 |
62 | 第四种值得注意的场景是在我们使用其他类时,如果对方没有声明自己是线程安全的,那么这种情况下对其他类进行多线程的并发操作,就有可能会发生线程安全问题。举个例子,比如说我们定义了 ArrayList,它本身并不是线程安全的,如果此时多个线程同时对 ArrayList 进行并发读/写,那么就有可能会产生线程安全问题,造成数据出错,而这个责任并不在 ArrayList,因为它本身并不是并发安全的。
63 |
64 | ## 总结
65 |
66 | 本文中我们总结了在在存在并发读写的业务场景下,需要注意线程安全的四种常见场景。分别是访问共享变量或资源,依赖时序的操作,不同数据之间存在绑定关系,以及对方没有声明自己是线程安全的。
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/docs/Java并发编程/理解CAS优缺点.md:
--------------------------------------------------------------------------------
1 | # 👉 CAS有很多优点,但它的缺点呢?
2 |
3 | > 本文我们一起学习探讨CAS的缺点。
4 |
5 | 在面试中,面试官常问是了解CAS吗?它有什么优点?你可能一听,这多简单,我会!于是你就开始噼里啪啦说可以避免加互斥锁,可以提高程序的运行效率等。
6 |
7 | 但是CAS 的缺点,你知道吗?因为对于开发人员来说,对于任何一门技术或者某个知识点,我们都应该熟悉它的优缺点及适合的应用场景,这样才能写出更优雅且高效的代码了。
8 |
9 | 首先,我们就来看一下 CAS 有哪几个主要的缺点。
10 |
11 | ## ABA 问题
12 | **首先,CAS 最大的缺点就是 ABA 问题。**
13 |
14 | 决定 CAS 是否进行 swap 的判断标准是“当前的值和预期的值是否一致”,如果一致,就认为在此期间这个数值没有发生过变动,这在大多数情况下是没有问题的。
15 |
16 | 但是在有的业务场景下,我们想确切知道从上一次看到这个值以来到现在,这个值是否发生过变化。例如,**这个值假设从 A 变成了 B,再由 B 变回了 A,此时,我们不仅认为它发生了变化,并且会认为它变化了两次。**
17 |
18 | 在这种场景下,我们使用 CAS,就看不到这两次的变化,因为仅判断“当前的值和预期的值是否一致”就是不够的了。CAS 检查的并不是值有没有发生过变化,而是去比较这当前的值和预期值是不是相等,如果变量的值从旧值 A 变成了新值 B 再变回旧值 A,由于最开始的值 A 和现在的值 A 是相等的,所以 CAS 会认为变量的值在此期间没有发生过变化。所以,CAS 并不能检测出在此期间值是不是被修改过,它只能检查出现在的值和最初的值是不是一样。
19 |
20 | **那么如何解决这个问题呢?添加一个版本号就可以解决。**
21 |
22 | 我们在变量值自身之外,再添加一个版本号,那么这个值的变化路径就从 A→B→A 变成了 1A→2B→3A,这样一来,就可以通过对比版本号来判断值是否变化过,这比我们直接去对比两个值是否一致要更靠谱,所以通过这样的思路就可以解决 ABA 的问题了。
23 |
24 | ## 自旋时间过长
25 |
26 | **CAS 的第二个缺点就是自旋时间过长。**
27 |
28 | 由于单次 CAS 不一定能执行成功,所以 CAS 往往是配合着循环来实现的,有的时候甚至是死循环,不停地进行重试,直到线程竞争不激烈的时候,才能修改成功。
29 |
30 | 可是如果我们的应用场景本身就是高并发的场景,就有可能导致 CAS 一直都操作不成功,这样的话,循环时间就会越来越长。而且在此期间,CPU 资源也是一直在被消耗的,这会对性能产生很大的影响。所以这就要求我们,要根据实际情况来选择是否使用 CAS,在高并发的场景下,通常 CAS 的效率是不高的。
31 |
32 | ## 范围不能灵活控制
33 | **CAS 的第三个缺点就是不能灵活控制线程安全的范围。**
34 |
35 | 通常我们去执行 CAS 的时候,是针对某一个,而不是多个共享变量的,这个变量可能是 Integer 类型,也有可能是 Long 类型、对象类型等等,但是我们不能针对多个共享变量同时进行 CAS 操作,因为这多个变量之间是独立的,简单的把原子操作组合到一起,并不具备原子性。因此如果我们想对多个对象同时进行 CAS 操作并想保证线程安全的话,是比较困难的。
36 |
37 | 有一个解决方案,那就是利用一个新的类,来整合刚才这一组共享变量,这个新的类中的多个成员变量就是刚才的那多个共享变量,然后再利用 atomic 包中的 AtomicReference 来把这个新对象整体进行 CAS 操作,这样就可以保证线程安全。
38 |
39 | 相比之下,如果我们使用其他的线程安全技术,那么调整线程安全的范围就可能变得非常容易,比如我们用 synchronized 关键字时,如果想把更多的代码加锁,那么只需要把更多的代码放到同步代码块里面就可以了。
40 |
41 | ## 总结
42 | 本文中我们学习了 CAS 的三个缺点,分别是**ABA 问题、自旋时间过长以及线程安全的范围不能灵活控制**。
43 |
44 | ## 干货分享
45 | **小伙伴们**关注【**码上Java**】微信公众号,回复关键字“**面试宝典**”,领取一份**嘟嘟**平时收集的一些优质资源,包含我们代码侠必备的优质简历模板、面试题库、电子书等资源大礼包一份,助力**小伙伴们**早日收获**心仪offer**,遇见更好的自己~
46 |
47 |
48 |
--------------------------------------------------------------------------------
/docs/Java并发编程/理解Callable和Runnable的不同.md:
--------------------------------------------------------------------------------
1 |
2 | # 👉 Callable和Runnable的不同?
3 |
4 | > 本文我们一起学习 Callable 和 Runnable 的不同。
5 |
6 | ## Runnable接口
7 | 首先,我们先来看看Runnable有什么缺点?
8 | ### 1.不能返回一个返回值
9 | 第一个缺陷,对于 Runnable 而言,它不能返回一个返回值,虽然可以利用其他的一些办法,比如在 Runnable 方法中写入日志文件或者修改某个共享的对象的办法,来达到保存线程执行结果的目的,但这种解决问题的行为千曲百折,属于曲线救国,效率着实不高。
10 |
11 | 实际上,在很多情况下执行一个子线程时,我们都希望能得到执行的任务的结果,也就是说,我们是需要得到返回值的,比如请求网络、查询数据库等。可是 Runnable 不能返回一个返回值,这是它第一个非常严重的缺陷。
12 |
13 | ### 2. 不能抛出 checked Exception
14 | 第二个缺陷就是不能抛出 checked Exception,如下面这段代码所示:
15 |
16 | ```java
17 | public class RunThrowException {
18 | /**
19 | * 普通方法内可以 throw 异常,并在方法签名上声明 throws
20 | */
21 | public void normalMethod() throws Exception {
22 | throw new IOException();
23 | }
24 | Runnable runnable = new Runnable() {
25 | /**
26 | * run方法上无法声明 throws 异常,且run方法内无法 throw 出 checked Exception,除非使用try catch进行处理
27 | */
28 | @Override
29 | public void run() {
30 | try {
31 | throw new IOException();
32 | } catch (IOException e) {
33 | e.printStackTrace();
34 | }
35 | }
36 | }
37 | }
38 | ```
39 | 在这段代码中,有两个方法,第一个方法是一个普通的方法,叫作 normalMethod,可以看到,在它的方法签名中有 throws Exception,并且在它的方法内也 throw 了一个 new IOException()。
40 |
41 | 然后在下面的的代码中,我们新建了一个 Runnable 对象,同时重写了它的 run 方法,我们没有办法在这个 run 方法的方法签名上声明 throws 一个异常出来。同时,在这个 run 方法里面也没办法 throw 一个 checked Exception,除非如代码所示,用 try catch 包裹起来,但是如果不用 try catch 是做不到的。
42 |
43 | 这就是对于 Runnable 而言的两个重大缺陷。
44 |
45 | ## Callable 接口
46 | Callable 是一个类似于 Runnable 的接口,实现 Callable 接口的类和实现 Runnable 接口的类都是可以被其他线程执行的任务。 我们看一下 Callable 的源码:
47 |
48 | ```java
49 | public interface Callable {
50 | V call() throws Exception;
51 | }
52 | ```
53 |
54 | 可以看出它也是一个 interface,并且它的 call 方法中已经声明了 throws Exception,前面还有一个 V 泛型的返回值,这就和之前的 Runnable 有很大的区别。实现 Callable 接口,就要实现 call 方法,这个方法的返回值是泛型 V,如果把 call 中计算得到的结果放到这个对象中,就可以利用 call 方法的返回值来获得子线程的执行结果了。
55 |
56 | ## Callable 和 Runnable 的区别
57 | 1. 方法名,Callable 规定的执行方法是 call(),而 Runnable 规定的执行方法是 run();
58 | 2. 返回值,Callable 的任务执行后有返回值,而 Runnable 的任务执行后是没有返回值的;
59 | 3. 抛出异常,call() 方法可抛出异常,而 run() 方法是不能抛出受检查异常的;
60 | 和 Callable 配合的有一个 Future 类,通过 Future 可以了解任务执行情况,或者取消任务的执行,还可获取任务执行的结果,这些功能都是 Runnable 做不到的,Callable 的功能要比 Runnable 强大。
61 |
62 | ## 总结
63 | 本文中我们学习了 Runnable 的两个缺陷,第一个是没有返回值,第二个是不能抛出受检查异常;接下来分析了 Callable 接口,并且把 Callable 接口和 Runnable 接口的区别进行了对比和总结。
64 |
65 |
66 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/docs/Java并发编程/理解JVM内存结构与Java内存模型.md:
--------------------------------------------------------------------------------
1 | # 👉 不要在混淆JVM内存结构与Java内存模型了!
2 |
3 | > 本文我们一起学习什么是Java内存模型。
4 |
5 | 在学习Java内存模型之前,我们简单了解一下什么JVM内存结构。
6 |
7 | ## JVM内存结构
8 |
9 | Java 代码是要运行在虚拟机上的,而虚拟机在执行 Java 程序的过程中会把所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途。 JVM 运行时内存区域结构可分为以下 6 个区。
10 |
11 | 1. 堆区(Heap):堆是存储类实例和数组的,通常是内存中最大的一块。实例很好理解,比如 new Object() 就会生成一个实例;而数组也是保存在堆上面的,因为在 Java 中,数组也是对象。
12 |
13 | 2. 虚拟机栈(Java Virtual Machine Stacks):它保存局部变量和部分结果,并在方法调用和返回中起作用。
14 |
15 | 3. 方法区(Method Area):它存储每个类的结构,例如运行时的常量池、字段和方法数据,以及方法和构造函数的代码,包括用于类初始化以及接口初始化的特殊方法。
16 |
17 | 4. 本地方法栈(Native Method Stacks):与虚拟机栈基本类似,区别在于虚拟机栈为虚拟机执行的 Java 方法服务,而本地方法栈则是为 Native 方法服务。
18 |
19 | 5. 程序计数器(The PC Register):是最小的一块内存区域,它的作用通常是保存当前正在执行的 JVM 指令地址。
20 |
21 | 6. 运行时常量池(Run-Time Constant Pool):是方法区的一部分,包含多种常量,范围从编译时已知的数字到必须在运行时解析的方法和字段引用。
22 |
23 | ## JMM内存模型是什么
24 |
25 | JMM其实是一种规范,是一种和多线程相关的一组规范,需要各个 JVM 的实现来遵守 JMM 规范,以便于开发者可以利用这些规范,更方便地开发多线程程序。这样一来,即便同一个程序在不同的虚拟机上运行,得到的程序结果也是一致的。
26 |
27 | 如果没有 JMM 内存模型来规范,那么很可能在经过了不同 JVM 的“翻译”之后,导致在不同的虚拟机上运行的结果不一样,那是很大的问题。这也是为什么需要JMM的一个很重要的原因。
28 |
29 | 因此,JMM 与处理器、缓存、并发、编译器有关。它解决了 CPU 多级缓存、处理器优化、指令重排等导致的结果不可预期的问题。
30 | ## 总结
31 | 本文我们学习了JVM内存结构与Java内存模型这两个容易混淆的概念,理解了JMM内存模型。其实之前我们使用了各种同步工具和关键字,包括 volatile、synchronized、Lock 等,其实它们的原理都涉及 JMM。正是 JMM 的参与和帮忙,才让各个同步工具和关键字能够发挥作用,帮我们开发出并发安全的程序。
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/docs/Java并发编程/理解Java中的锁及其特点.md:
--------------------------------------------------------------------------------
1 | # 👉 Java中哪几种锁?分别有什么特点?
2 | > 本文中我们一起学习Java中锁的分类及其特点。
3 |
4 | 锁是用来控制多线程访问共享资源的方式,一般的讲,一个锁能够防止多个线程同时访问共享资源。
5 |
6 | 对于 Java 中的锁而言,根据分类标准我们把锁分为以下 7 大类别,分别是:
7 |
8 | 1. 偏向锁/轻量级锁/重量级锁;
9 |
10 | 2. 可重入锁/非可重入锁;
11 |
12 | 3. 共享锁/独占锁;
13 |
14 | 4. 公平锁/非公平锁;
15 |
16 | 5. 悲观锁/乐观锁;
17 |
18 | 6. 自旋锁/非自旋锁;
19 |
20 | 7. 可中断锁/不可中断锁。
21 |
22 | ## 1. 偏向锁/轻量级锁/重量级锁
23 |
24 | 第一种分类是偏向锁/轻量级锁/重量级锁,这三种锁特指 synchronized 锁的状态,通过在对象头中的 mark word 来表明锁的状态。
25 |
26 | - 偏向锁
27 |
28 | 如果自始至终,对于这把锁都不存在竞争,那么其实就没必要上锁,只需要打个标记就行了,这就是偏向锁的思想。一个对象被初始化后,还没有任何线程来获取它的锁时,那么它就是可偏向的,当有第一个线程来访问它并尝试获取锁的时候,它就将这个线程记录下来,以后如果尝试获取锁的线程正是偏向锁的拥有者,就可以直接获得锁,开销很小,性能最好。
29 |
30 | - 轻量级锁
31 |
32 | JVM 开发者发现在很多情况下,synchronized 中的代码是被多个线程交替执行的,而不是同时执行的,也就是说并不存在实际的竞争,或者是只有短时间的锁竞争,用 CAS 就可以解决,这种情况下,用完全互斥的重量级锁是没必要的。轻量级锁是指当锁原来是偏向锁的时候,被另一个线程访问,说明存在竞争,那么偏向锁就会升级为轻量级锁,线程会通过自旋的形式尝试获取锁,而不会陷入阻塞。
33 |
34 | - 重量级锁
35 |
36 | 重量级锁是互斥锁,它是利用操作系统的同步机制实现的,所以开销相对比较大。当多个线程直接有实际竞争,且锁竞争时间长的时候,轻量级锁不能满足需求,锁就会膨胀为重量级锁。重量级锁会让其他申请却拿不到锁的线程进入阻塞状态。
37 |
38 | 
39 |
40 | 你可以发现锁升级的路径:无锁→偏向锁→轻量级锁→重量级锁。
41 |
42 | 综上所述,偏向锁性能最好,可以避免执行 CAS 操作。而轻量级锁利用自旋和 CAS 避免了重量级锁带来的线程阻塞和唤醒,性能中等。重量级锁则会把获取不到锁的线程阻塞,性能最差。
43 |
44 | ## 2. 可重入锁/非可重入锁
45 |
46 | 第 2 个分类是可重入锁和非可重入锁。可重入锁指的是线程当前已经持有这把锁了,能在不释放这把锁的情况下,再次获取这把锁。同理,不可重入锁指的是虽然线程当前持有了这把锁,但是如果想再次获取这把锁,也必须要先释放锁后才能再次尝试获取。
47 |
48 | 对于可重入锁而言,最典型的就是 ReentrantLock 了,正如它的名字一样,reentrant 的意思就是可重入,它也是 Lock 接口最主要的一个实现类。
49 |
50 | ## 3.共享锁/独占锁
51 |
52 | 第 3 种分类标准是共享锁和独占锁。共享锁指的是我们同一把锁可以被多个线程同时获得,而独占锁指的就是,这把锁只能同时被一个线程获得。我们的读写锁,就最好地诠释了共享锁和独占锁的理念。读写锁中的读锁,是共享锁,而写锁是独占锁。读锁可以被同时读,可以同时被多个线程持有,而写锁最多只能同时被一个线程持有。
53 |
54 | ## 4.公平锁/非公平锁
55 |
56 | 第 4 种分类是公平锁和非公平锁。公平锁的公平的含义在于如果线程现在拿不到这把锁,那么线程就都会进入等待,开始排队,在等待队列里等待时间长的线程会优先拿到这把锁,有先来先得的意思。而非公平锁就不那么“完美”了,它会在一定情况下,忽略掉已经在排队的线程,发生插队现象。
57 |
58 | ## 5.悲观锁/乐观锁
59 |
60 | 第 5 种分类是悲观锁,以及与它对应的乐观锁。悲观锁的概念是在获取资源之前,必须先拿到锁,以便达到“独占”的状态,当前线程在操作资源的时候,其他线程由于不能拿到锁,所以其他线程不能来影响我。而乐观锁恰恰相反,它并不要求在获取资源前拿到锁,也不会锁住资源;相反,乐观锁利用 CAS 理念,在不独占资源的情况下,完成了对资源的修改。
61 |
62 | ## 6.自旋锁/非自旋锁
63 |
64 | 第 6 种分类是自旋锁与非自旋锁。自旋锁的理念是如果线程现在拿不到锁,并不直接陷入阻塞或者释放 CPU 资源,而是开始利用循环,不停地尝试获取锁,这个循环过程被形象地比喻为“自旋”,就像是线程在“自我旋转”。相反,非自旋锁的理念就是没有自旋的过程,如果拿不到锁就直接放弃,或者进行其他的处理逻辑,例如去排队、陷入阻塞等。
65 |
66 | ## 7.可中断锁/不可中断锁
67 |
68 | 第 7 种分类是可中断锁和不可中断锁。在 Java 中,synchronized 关键字修饰的锁代表的是不可中断锁,一旦线程申请了锁,就没有回头路了,只能等到拿到锁以后才能进行其他的逻辑处理。而我们的 ReentrantLock 是一种典型的可中断锁,例如使用 lockInterruptibly 方法在获取锁的过程中,突然不想获取了,那么也可以在中断之后去做其他的事情,不需要一直傻等到获取到锁才离开。
69 |
70 | ## 总结
71 | 本文中我们首先会对锁的分类有一个整体的概念,了解锁究竟有哪些分类标准。然后在后续的课程中,会对其中重要的锁进行详细讲解。
72 |
--------------------------------------------------------------------------------
/docs/Java并发编程/理解synchronized关键字.md:
--------------------------------------------------------------------------------
1 | # 👉 synchronized关键字原理
2 |
3 | 众所周知 `synchronized` 关键字是解决并发问题常用解决方案,有以下三种使用方式:
4 |
5 | - 同步普通方法,锁的是当前对象。
6 | - 同步静态方法,锁的是当前 `Class` 对象。
7 | - 同步块,锁的是 `()` 中的对象。
8 |
9 | 实现原理: `JVM` 是通过进入、退出对象监视器( `Monitor` )来实现对方法、同步块的同步的。
10 |
11 | 具体实现是在编译之后在同步方法调用前加入一个 `monitor.enter` 指令,在退出方法和异常处插入 `monitor.exit` 的指令。
12 |
13 | 其本质就是对一个对象监视器( `Monitor` )进行获取,而这个获取过程具有排他性从而达到了同一时刻只能一个线程访问的目的。
14 |
15 | 而对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程 `monitor.exit` 之后才能尝试继续获取锁。
16 |
17 | 流程图如下:
18 |
19 | 
20 |
21 | 通过一段代码来演示:
22 |
23 | ```java
24 | public static void main(String[] args) {
25 | synchronized (Synchronize.class){
26 | System.out.println("Synchronize");
27 | }
28 | }
29 | ```
30 |
31 | 使用 `javap -c Synchronize` 可以查看编译之后的具体信息。
32 |
33 | ```
34 | public class com.crossoverjie.synchronize.Synchronize {
35 | public com.crossoverjie.synchronize.Synchronize();
36 | Code:
37 | 0: aload_0
38 | 1: invokespecial #1 // Method java/lang/Object."":()V
39 | 4: return
40 |
41 | public static void main(java.lang.String[]);
42 | Code:
43 | 0: ldc #2 // class com/crossoverjie/synchronize/Synchronize
44 | 2: dup
45 | 3: astore_1
46 | **4: monitorenter**
47 | 5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
48 | 8: ldc #4 // String Synchronize
49 | 10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
50 | 13: aload_1
51 | **14: monitorexit**
52 | 15: goto 23
53 | 18: astore_2
54 | 19: aload_1
55 | 20: monitorexit
56 | 21: aload_2
57 | 22: athrow
58 | 23: return
59 | Exception table:
60 | from to target type
61 | 5 15 18 any
62 | 18 21 18 any
63 | }
64 | ```
65 |
66 | 可以看到在同步块的入口和出口分别有 `monitorenter,monitorexit` 指令。
67 |
68 | ## [锁优化]
69 |
70 | `synchronized` 很多都称之为重量锁,`JDK1.6` 中对 `synchronized` 进行了各种优化,为了能减少获取和释放锁带来的消耗引入了`偏向锁`和`轻量锁`。
71 |
72 | ### [轻量锁]
73 |
74 | 当代码进入同步块时,如果同步对象为无锁状态时,当前线程会在栈帧中创建一个锁记录(`Lock Record`)区域,同时将锁对象的对象头中 `Mark Word` 拷贝到锁记录中,再尝试使用 `CAS` 将 `Mark Word` 更新为指向锁记录的指针。
75 |
76 | 如果更新**成功**,当前线程就获得了锁。
77 |
78 | 如果更新**失败** `JVM` 会先检查锁对象的 `Mark Word` 是否指向当前线程的锁记录。
79 |
80 | 如果是则说明当前线程拥有锁对象的锁,可以直接进入同步块。
81 |
82 | 不是则说明有其他线程抢占了锁,如果存在多个线程同时竞争一把锁,**轻量锁就会膨胀为重量锁**。
83 |
84 | #### [解锁]
85 |
86 | 轻量锁的解锁过程也是利用 `CAS` 来实现的,会尝试锁记录替换回锁对象的 `Mark Word` 。如果替换成功则说明整个同步操作完成,失败则说明有其他线程尝试获取锁,这时就会唤醒被挂起的线程(此时已经膨胀为`重量锁`)
87 |
88 | 轻量锁能提升性能的原因是:
89 |
90 | 认为大多数锁在整个同步周期都不存在竞争,所以使用 `CAS` 比使用互斥开销更少。但如果锁竞争激烈,轻量锁就不但有互斥的开销,还有 `CAS` 的开销,甚至比重量锁更慢。
91 |
92 | ### [偏向锁]
93 |
94 | 为了进一步的降低获取锁的代价,`JDK1.6` 之后还引入了偏向锁。
95 |
96 | 偏向锁的特征是:锁不存在多线程竞争,并且应由一个线程多次获得锁。
97 |
98 | 当线程访问同步块时,会使用 `CAS` 将线程 ID 更新到锁对象的 `Mark Word` 中,如果更新成功则获得偏向锁,并且之后每次进入这个对象锁相关的同步块时都不需要再次获取锁了。
99 |
100 | #### [释放锁]
101 |
102 | 当有另外一个线程获取这个锁时,持有偏向锁的线程就会释放锁,释放时会等待全局安全点(这一时刻没有字节码运行),接着会暂停拥有偏向锁的线程,根据锁对象目前是否被锁来判定将对象头中的 `Mark Word` 设置为无锁或者是轻量锁状态。
103 |
104 | 偏向锁可以提高带有同步却没有竞争的程序性能,但如果程序中大多数锁都存在竞争时,那偏向锁就起不到太大作用。可以使用 `-XX:-UseBiasedLocking` 来关闭偏向锁,并默认进入轻量锁。
105 |
106 | ### [其他优化]
107 |
108 | #### [适应性自旋]
109 |
110 | 在使用 `CAS` 时,如果操作失败,`CAS` 会自旋再次尝试。由于自旋是需要消耗 `CPU` 资源的,所以如果长期自旋就白白浪费了 `CPU`。`JDK1.6`加入了适应性自旋:
111 |
112 | > 如果某个锁自旋很少成功获得,那么下一次就会减少自旋。
113 |
114 |
--------------------------------------------------------------------------------
/docs/Java并发编程/理解线程池.md:
--------------------------------------------------------------------------------
1 | # 线程池
2 |
3 | ## 1.线程池工作原理
4 |
5 | > JVM先根据用户的参数创建一定数量的可运行的线程任务,并将其放入队列中,在线程创建后启动这些任务,如果正在运行的线程数量超过了最大线程数量(用户设置的线程池大小),则超出数量的线程排队等候,在有任务执行完后,线程池调度器会发现有可用的线程,进而再次从队列中取出任务并执行。
6 |
7 | ## 2.线程池的核心组件和核心类、
8 |
9 | **核心组件**
10 |
11 | + 线程池管理器:用户创建并管理线程池。
12 | + 工作线程:线程池中执行具体任务的线程。
13 | + 任务接口:用于定义工作线程的调度和执行策略,只有线程实现了该接口,线程中的任务才能够被线程池调度。
14 | + 任务队列:存放待处理的任务,新的任务会不断被加入队列中,执行完成的任务将被从队列移除。
15 |
16 | **核心类**
17 |
18 | + Executor
19 | + Executors
20 | + ExecutorService
21 | + ThreadPoolExecutor
22 | + Callable
23 | + Future
24 | + FutureTask
25 |
26 | **ThreadPoolExecutor**是构建线程的核心方法:
27 |
28 | ```java
29 | public ThreadPoolExecutor(int corePoolSize,
30 | int maximumPoolSize,
31 | long keepAliveTime,
32 | TimeUnit unit,
33 | BlockingQueue workQueue) {
34 | this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
35 | Executors.defaultThreadFactory(), defaultHandler);
36 | }
37 | ```
38 |
39 | **ThreadPoolExecutor构造函数的具体参数**
40 |
41 | | 序号 | 参数 | 说明 |
42 | | :--: | :-------------: | :----------------------------------------------------------: |
43 | | 1 | corePoolSize | 线程池中核心线程的数量 |
44 | | 2 | maximumPoolSize | 线程池中最大线程的数量 |
45 | | 3 | keepAliveTime | 当前线程池数量超过corePoolSize是,空闲线程的存活时间 |
46 | | 4 | unit | keepAliveTime的时间单位 |
47 | | 5 | workQueue | 任务队列,被提交但尚未被执行的任务存放的地方 |
48 | | 6 | threadFactor | 线程工厂,用于创建线程,可使用默认的线程工厂或自定义线程工厂 |
49 | | 7 | handler | 由于任务过多或其他原因导致线程池无法处理时的任务拒绝策略 |
50 |
51 | ## 3.线程池的工作流程
52 |
53 | **Java线程池的工作流程为:线程池刚被创建时,只是向系统申请一个用于执行线程队列和管理线程池的线程资源。在调用execute()添加一个任务时,线程池会按照以下流程执行任务。**
54 |
55 | + 如果正在运行的线程数量少于corePoolSize(用户自定义的核心线程数),线程池就会立刻创建线程并执行该线程任务。
56 | + 如果正在运行的线程数量大于等于corePoolSize(用户自定义的核心线程数),该任务就将被放入阻塞队列中。
57 | + 在阻塞队列已满且正在运行的线程数量小于 maximumPoolSize时,线程池会创建被非核心线程立即执行该线程任务。
58 | + 在阻塞队列已满且正在运行的线程数量大于等于 maximumPoolSize时,线程池将拒绝执行该线程任务并抛出RejectExecutionException异常。
59 | + 在线程任务执行完毕后,该任务将被从线程池队列中移除,线程池将从队列中取下一个线程任务继续执行。
60 | + 在线程处于空闲状态的时间超过keepAliveTime时间时,正在运行的线程数量超过corePoolSize,该线程将会认定为空闲线程并停止。因此在线程池中所以线程任务都执行完毕后,线程池会收缩到corePoolSize大小。
61 |
62 | ## 4.线程池的拒绝策略
63 |
64 | **若线程池中的核心线程数被用完且阻塞队列已排满,则此时线程池的资源已耗尽,线程池将没有足够的线程资源执行新的任务。为了保证操作系统的安全,线程池将通过拒绝策略处理新添加的线程任务。**
65 |
66 | | 序号 | 名称 | 说明 |
67 | | :--: | :-----------------: | :----------------------------------------------------------: |
68 | | 1 | AbortPolicy | 直接抛出异常,阻止线程正常运行 |
69 | | 2 | CallerRunsPolicy | 如果被丢弃的线程任务未关闭,则执行该线程任务。 |
70 | | 3 | DiscardOldestPolicy | 移除线程队列中最早的一个线程任务,并尝试提交当前任务。 |
71 | | 4 | DiscardPolicy | 丢弃当前的线程任务而不做任何处理。如果系统允许在资源不足的情况相下丢弃部分任务,则这将是保障系统安全、稳定的一种很好的方案。 |
72 | | 5 | 自定义拒绝策略 | 以上四种拒绝策略无法满足实际需要,用户可以扩展RejectedExecutionHandler接口实现拒绝策略。 |
73 |
74 | ## 5.五种常用的线程池
75 |
76 | | 序号 | 名称 | 说明 |
77 | | :--: | :---------------------: | :--------------------------: |
78 | | 1 | AbortPolicy | 可缓存的线程池 |
79 | | 2 | newFixedThreadPool | 固定大小的线程池 |
80 | | 3 | newScheduledThreadPool | 可做任务调度的线程池 |
81 | | 4 | newSingleThreadExecutor | 单个线程的线程池 |
82 | | 5 | newWorkStealingPool | 足够大小的线程池,JDK1.8新增 |
83 |
84 | ## 6. 向线程池提交任务
85 |
86 | - execute()方法:当提交不需要返回值的任务,缺点无法判断任务是否被线程池执行成功;
87 | - submit()方法:当提交需要返回值的任务,此时线程池会返回一个future类型的对象,通过该对象可以判断线程池是否执行成功,并且可以通过调用该对象的get()方法获取返回值;
88 |
89 | ## 7. 关闭线程池
90 |
91 | 关闭线程池可以调用shutdown或者shutdownNow方法。区别如下:
92 |
93 | - 共同点:
94 | - 它们的原理都是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程( 无法响应中断的任务可能永远无法终止)。
95 | - 不同点:
96 | - shutdownNow首先将线程池的状态设置为STOP,然后尝试停止所有的正在执行或暂停任务的线程。
97 | - shutdown只是将线程池的状态设置成SHOTDOWN状态,然后中断所有没有正在执行任务的线程。
98 |
99 | **总结:通常调用shutdown方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法。**
100 |
101 | ## 总结
102 |
103 | **线程池的好处**
104 |
105 | + 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁的消耗。
106 | + 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
107 | + 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
--------------------------------------------------------------------------------
/docs/Java并发编程/理解线程池4种拒绝策略.md:
--------------------------------------------------------------------------------
1 | # 👉 线程池的拒绝策略
2 |
3 | > 本文我们一起学习线程池有哪 4 种默认的拒绝策略。
4 |
5 | 若线程池中的核心线程数被用完且阻塞队列已排满,则此时线程池的资源已耗尽,线程池将没有足够的线程资源执行新的任务。为了保证操作系统的安全,线程池将通过拒绝策略处理新添加的线程任务。
6 |
7 | ## 1. AbortPolicy
8 | 第一种拒绝策略是 `AbortPolicy`,这种拒绝策略在拒绝任务时,会直接抛出一个类型为 RejectedExecutionException的RuntimeException,让你感知到任务被拒绝了,于是你便可以根据业务逻辑选择重试或者放弃提交等策略。
9 |
10 |
11 | ## 2.DiscardPolicy
12 | 第2种拒绝策略是 `DiscardPolicy`,这种拒绝策略正如它的名字所描述的一样,当新任务被提交后直接被丢弃掉,也不会给你任何的通知,相对而言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失。
13 |
14 | ## 3.DiscardOldestPolicy
15 | 第3种拒绝策略是 `DiscardOldestPolicy`,如果线程池没被关闭且没有能力执行,则会丢弃任务队列中的头结点,通常是存活时间最长的任务,这种策略与第二种不同之处在于它丢弃的不是最新提交的,而是队列中存活时间最长的,这样就可以腾出空间给新提交的任务,但同理它也存在一定的数据丢失风险。
16 |
17 | ## 4.CallerRunsPolicy
18 | 第4种拒绝策略是 CallerRunsPolicy,相对而言它就比较完善了,当有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务。这样做主要有两点好处。
19 |
20 | 1. 第一点新提交的任务不会被丢弃,这样也就不会造成业务损失。
21 | 2. 第二点好处是,由于谁提交任务谁就要负责执行任务,这样提交任务的线程就得负责执行任务,而执行任务又是比较耗时的,在这段期间,提交任务的线程被占用,也就不会再提交新的任务,减缓了任务提交的速度,相当于是一个负反馈。在此期间,线程池中的线程也可以充分利用这段时间来执行掉一部分任务,腾出一定的空间,相当于是给了线程池一定的缓冲期。
22 | ## 总结
23 |
24 | 本文中我们学习线程池中的4 种默认的拒绝策略。线程池会在以下两种情况下会拒绝新提交的任务。
25 | - 第一种情况是当我们调用 shutdown 等方法关闭线程池后,即便此时可能线程池内部依然有没执行完的任务正在执行,但是由于线程池已经关闭,此时如果再向线程池内提交任务,就会遭到拒绝。
26 | - 第二种情况是线程池没有能力继续处理新提交的任务,也就是工作已经非常饱和的时候。
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/docs/Java并发编程/理解线程的状态及如何进行转换的.md:
--------------------------------------------------------------------------------
1 | # 👉 理解线程的状态及如何进行转换的
2 | > 本文我们一起学习线程的状态有哪些以及它们之间是如何进行转换的?
3 |
4 | 线程(Thread)是并发编程的基础,也是程序执行的最小单元,它依托进程而存在。一个进程中可以包含多个线程,多线程可以共享一块内存空间和一组系统资源,因此线程之间的切换更加节省资源、更加轻量化,也因此被称为轻量级的进程。
5 |
6 | ## 线程状态六君子
7 |
8 | 线程的状态在 JDK 1.5 之后以枚举的方式被定义在 Thread 的源码中,它总共包含以下 6 个状态:
9 | 1. NEW,新建状态,线程被创建出来,但尚未启动时的线程状态;
10 | 2. RUNNABLE,就绪状态,表示可以运行的线程状态,它可能正在运行,或者是在排队等待操作系统给它分配 CPU 资源;
11 | 3. BLOCKED,阻塞等待锁的线程状态,表示处于阻塞状态的线程正在等待监视器锁,比如等待执行 synchronized 代码块或者使用 synchronized 标记的方法;
12 | 4. WAITING,等待状态,一个处于等待状态的线程正在等待另一个线程执行某个特定的动作,比如,一个线程调用了 Object.wait() 方法,那它就在等待另一个线程调用 Object.notify() 或 Object.notifyAll() 方法;
13 | 5. TIMED_WAITING,计时等待状态,和等待状态(WAITING)类似,它只是多了超时时间,比如调用了有超时时间设置的方法 Object.wait(long timeout) 和 Thread.join(long timeout) 等这些方法时,它才会进入此状态;
14 | 6. TERMINATED,终止状态,表示线程已经执行完成。
15 |
16 | ## 揭露源码
17 |
18 | ```java
19 | public enum State {
20 | /**
21 | * 新建状态,线程被创建出来,但尚未启动时的线程状态
22 | */
23 | NEW,
24 |
25 | /**
26 | * 就绪状态,表示可以运行的线程状态,但它在排队等待来自操作系统的 CPU 资源
27 | */
28 | RUNNABLE,
29 |
30 | /**
31 | * 阻塞等待锁的线程状态,表示正在处于阻塞状态的线程
32 | * 正在等待监视器锁,比如等待执行 synchronized 代码块或者
33 | * 使用 synchronized 标记的方法
34 | */
35 | BLOCKED,
36 |
37 | /**
38 | * 等待状态,一个处于等待状态的线程正在等待另一个线程执行某个特定的动作。
39 | * 例如,一个线程调用了 Object.wait() 它在等待另一个线程调用
40 | * Object.notify() 或 Object.notifyAll()
41 | */
42 | WAITING,
43 |
44 | /**
45 | * 计时等待状态,和等待状态 (WAITING) 类似,只是多了超时时间,比如
46 | * 调用了有超时时间设置的方法 Object.wait(long timeout) 和
47 | * Thread.join(long timeout) 就会进入此状态
48 | */
49 | TIMED_WAITING,
50 |
51 | /**
52 | * 终止状态,表示线程已经执行完成
53 | */
54 | }
55 |
56 | ```
57 | 线程的工作模式是,首先先要创建线程并指定线程需要执行的业务方法,然后再调用线程的 start() 方法,此时线程就从 NEW(新建)状态变成了 RUNNABLE(就绪)状态,此时线程会判断要执行的方法中有没有 synchronized 同步代码块,如果有并且其他线程也在使用此锁,那么线程就会变为 BLOCKED(阻塞等待)状态,当其他线程使用完此锁之后,线程会继续执行剩余的方法。
58 |
59 | 当遇到 Object.wait() 或 Thread.join() 方法时,线程会变为 WAITING(等待状态)状态,如果是带了超时时间的等待方法,那么线程会进入 TIMED_WAITING(计时等待)状态,当有其他线程执行了 notify() 或 notifyAll() 方法之后,线程被唤醒继续执行剩余的业务方法,直到方法执行完成为止,此时整个线程的流程就执行完了,执行流程如下图所示:
60 |
61 |
62 | 
63 |
64 |
65 | ## 总结
66 |
67 | 本文中我们学习了线程中的六种状态,这个问题一般出现在面试的起始问题上,由此逐渐延伸更多的并发编程的面试问题,考验面试者的并发编程掌握程度。
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/docs/Java并发编程/第2章Java并发机制的底层原理.md:
--------------------------------------------------------------------------------
1 | # 第2章 Java并发机制的底层原理
2 |
3 | ## 1. valatile关键字
4 |
5 | `valatile`是轻量级的`synchronized`,在多处理器开发中保证了共享变量的`可见性`。
6 |
7 | > 可见性是指当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
8 |
9 | 如果`valatile`关键字使用恰到,它的使用和执行成本会比`synchronized`底,因为它不会引起线程上下文的切换和调度。 如果一个字段被声明成`valatile`,Java线程内存模型确保所有线程看到这个变量的值是一致的。
10 |
11 | ## 2. synchronized关键字
12 |
13 | Java中的每一个对象都可以作为锁。
14 |
15 | - 对于普通同步方法,锁的是当前实例对象。
16 | - 对于静态同步方法,锁的是当前类的Class对象。
17 | - 对于同步方法块,锁的是`synchronized`括号里面配置的对象。
18 |
19 | 当一个线程试图访问同步代码块时候,首先线程必须得到锁,退出或抛出异常时必须释放锁。
20 |
21 | ## 3. Java对象头
22 |
23 | `synchronized`用的锁是存在Java对象头里面的,如果对象是数组类型,则虚拟机用3个字宽存储对象头,如果对象是非数组类型,则用2字宽存储对象头。
24 |
25 | ## 4. Java中的锁
26 |
27 | **锁的优缺点总结**
28 |
29 | | 锁 | 优点 | 缺点 | 适用场景 |
30 | | :------: | :----------------------------------------------------------: | :----------------------------------------------: | :----------------------------------: |
31 | | 偏向锁 | 加锁和解锁不需要额外的开销,和执行非同步方法相比仅存在纳秒级的差距 | 如何线程之间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块场景 |
32 | | 轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程,使用自旋会消耗CPU | 追求响应时间、头同步块执行速度非常快 |
33 | | 重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步块执行速度较长 |
34 |
35 |
--------------------------------------------------------------------------------
/docs/Java性能优化/README.md:
--------------------------------------------------------------------------------
1 | # Java性能优化
2 |
3 | * [常见Java代码优化法则](docs/Java并发编程/常见Java代码优化法则.md)
4 |
--------------------------------------------------------------------------------
/docs/Java性能优化/_sidebar.md:
--------------------------------------------------------------------------------
1 | * **👉 Java性能优化** [↩](/README)
2 | * [常见Java代码优化法则](docs/Java性能优化/常见Java代码优化法则.md)
3 |
--------------------------------------------------------------------------------
/docs/Java性能优化/常见Java代码优化法则.md:
--------------------------------------------------------------------------------
1 | # 👉 常见Java代码优化法则
2 |
3 | ## 1.使用局部变量可避免在堆上分配
4 | 由于堆资源是多线程共享的,是垃圾回收器工作的主要区域,过多的对象会造成 GC 压力。可以通过局部变量的方式,将变量在栈上分配。这种方式变量会随着方法执行的完毕而销毁,能够减轻 GC 的压力。
5 |
6 | ## 2.削弱变量的作用范围
7 | 注意变量的作用范围,尽量减少对象的创建。如下面的代码,变量 a 每次进入方法都会创建,可以将它移动到 if 语句内部。
8 |
9 | ## 3.使用类名方式访问静态变量
10 | 有的同学习惯使用对象访问静态变量,这种方式多了一步寻址操作,需要先找到变量对应的类,再找到类对应的变量。
11 |
12 | ## 4.字符串拼接不要使用 ”+”
13 | 字符串拼接,使用 `StringBuilder `或者` StringBuffer`,不要使用 + 号。
14 |
15 | ## 5.重写对象的`HashCode`,不要简单地返回固定值
16 | 开发时重写 `HashCode` 和`Equals` 方法时,会把 `HashCode `的值返回固定的 0,而这样做是不恰当的。
17 |
18 | 当这些对象存入 `HashMap` 时,性能就会非常低,因为 `HashMap `是通过 `HashCode` 定位到 Hash 槽,有冲突的时候,才会使用链表或者红黑树组织节点。固定地返回 0,相当于把 Hash 寻址功能给废除了。
19 |
20 | ## 6.`HashMap `等集合初始化的时候,尽量指定初始值大小
21 | 通过指定初始值大小可减少扩容造成的性能损耗。
22 |
23 | ## 7.遍历`Map` 的时候,使用 `EntrySet` 方法
24 | 使用 EntrySet 方法,可以直接返回 set 对象,直接拿来用即可;而使用 KeySet 方法,获得的是key 的集合,需要再进行一次 get 操作,多了一个操作步骤。所以更推荐使用 EntrySet 方式遍历 Map。
25 |
26 | ## 8.不要在多线程下使用同一个 Random
27 | Random 类的 seed 会在并发访问的情况下发生竞争,造成性能降低,建议在多线程环境下使用 `ThreadLocalRandom` 类。
28 |
29 | > 在 Linux 上,通过加入 JVM 配置 -Djava.security.egd=file:/dev/./urandom,使用 urandom 随机生成器,在进行随机数获取时,速度会更快。
30 |
31 | ## 9.自增推荐使用 LongAddr
32 | 自增运算可以通过 synchronized 和 volatile 的组合,或者也可以使用原子类(比如 AtomicLong)。
33 |
34 | 后者的速度比前者要高一些,AtomicLong 使用 CAS 进行比较替换,在线程多的情况下会造成过多无效自旋,所以可以使用 LongAdder 替换 AtomicLong 进行进一步的性能提升。
35 |
36 | ## 10.不要使用异常控制程序流程
37 | 异常,是用来了解并解决程序中遇到的各种不正常的情况,它的实现方式比较昂贵,比平常的条件判断语句效率要低很多。
38 |
39 | 这是因为异常在字节码层面,需要生成一个如下所示的异常表(Exception table),多了很多判断步骤。
40 |
41 | ## 11.不要在循环中使用 try catch
42 | 道理与上面类似,很多文章介绍,不要把异常处理放在循环里,而应该把它放在最外层,但实际测试情况表明这两种方式性能相差并不大。
43 |
44 | 既然性能没什么差别,那么就推荐根据业务的需求进行编码。比如,循环遇到异常时,不允许中断,也就是允许在发生异常的时候能够继续运行下去,那么异常就只能在 for 循环里进行处理。
45 |
46 | ## 12.不要捕捉 RuntimeException
47 | Java 异常分为两种,一种是可以通过预检查机制避免的 RuntimeException;另外一种就是普通异常。
48 |
49 | 其中,RuntimeException 不应该通过 catch 语句去捕捉,而应该使用编码手段进行规避。
50 |
51 | ## 13.合理使用 PreparedStatement
52 | PreparedStatement 使用预编译对 SQL 的执行进行提速,大多数数据库都会努力对这些能够复用的查询语句进行预编译优化,并能够将这些编译结果缓存起来。
53 |
54 | 这样等到下次用到的时候,就可以很快进行执行,也就少了一步对 SQL 的解析动作。
55 |
56 | PreparedStatement 还能提高程序的安全性,能够有效防止 SQL 注入。
57 |
58 | 但如果你的程序每次 SQL 都会变化,不得不手工拼接一些数据,那么 PreparedStatement 就失去了它的作用,反而使用普通的 Statement 速度会更快一些。
59 |
60 | ## 14.日志打印的注意事项
61 | debug 输出一些调试信息,然后在线上关掉它。
62 |
63 | ## 15.减少事务的作用范围
64 | 如果的程序使用了事务,那一定要注意事务的作用范围,尽量以最快的速度完成事务操作。这是因为,事务的隔离性是使用锁实现的。
65 |
66 | ## 16.使用位移操作替代乘除法
67 | 计算机是使用二进制表示的,位移操作会极大地提高性能。
68 |
69 | - `<<` 左移相当于乘以 2;
70 | - `<<` 右移相当于除以 2;
71 | - `>>>`无符号右移相当于除以 2,但它会忽略符号位,空位都以 0 补齐。
72 |
73 | ## 17.不要打印大集合或者使用大集合的 toString 方法
74 | 有的开发喜欢将集合作为字符串输出到日志文件中,这个习惯是非常不好的。
75 |
76 | 拿 ArrayList 来说,它需要遍历所有的元素来迭代生成字符串。在集合中元素非常多的情况下,这不仅会占用大量的内存空间,执行效率也非常慢
77 |
78 | ## 18.尽量少在程序中使用反射
79 | 反射的功能很强大,但它是通过解析字节码实现的,性能就不是很理想。
80 |
81 | 现实中有很多对反射的优化方法,比如把反射执行的过程(比如 Method)缓存起来,使用复用来加快反射速度。
82 |
83 | Java 7.0 之后,加入了新的包 java.lang.invoke,同时加入了新的 JVM 字节码指令 invokedynamic,用来支持从 JVM 层面,直接通过字符串对目标方法进行调用。
84 |
85 | 如果你对性能有非常苛刻的要求,则使用 invoke 包下的 MethodHandle 对代码进行着重优化,但它的编程不如反射方便,在平常的编码中,反射依然是首选。
86 |
87 | ## 19.正则表达式可以预先编译,加快速度
88 | Java 的正则表达式需要先编译再使用。
89 |
90 |
--------------------------------------------------------------------------------
/docs/Java核心基础/README.md:
--------------------------------------------------------------------------------
1 | 
--------------------------------------------------------------------------------
/docs/Java核心基础/_sidebar.md:
--------------------------------------------------------------------------------
1 | * **👉 Java核心基础** [↩](/README)
2 | * [理解Java关键字](docs/Java核心基础/理解Java关键字.md)
3 | * [理解String字符串](docs/Java核心基础/理解String字符串.md)
4 | * [理解基本数据类型与包装类](docs/Java核心基础/理解基本数据类型与包装类.md)
5 | * [理解各种内部类和枚举类](docs/Java核心基础/理解各种内部类和枚举类.md)
6 | * [理解动态代理](docs/Java核心基础/理解动态代理.md)
7 | * [理解克隆与序列化应用](docs/Java核心基础/理解克隆与序列化应用.md)
8 | * [理解异常处理](docs/Java核心基础/理解异常处理.md)
9 | * [理解抽象类与接口](docs/Java核心基础/理解抽象类与接口.md)
10 | * [理解泛型与迭代器](docs/Java核心基础/理解泛型与迭代器.md)
11 | * [理解浅克隆和深克隆](docs/Java核心基础/理解浅克隆和深克隆.md)
12 | * [理解类与Object](docs/Java核心基础/理解类与Object.md)
13 | * [理解集合Collection](docs/Java核心基础/理解集合Collection.md)
14 | * [理解集合Map](docs/Java核心基础/理解集合Map.md)
15 | * [理解Java中的各种锁](docs/Java核心基础/理解Java中的各种锁.md)
16 | * [理解HashMap底层实现原理](docs/Java核心基础/理解HashMap底层实现原理.md)
17 | * [理解HashMap为什么是线程不安全的](docs/Java核心基础/理解HashMap为什么是线程不安全的.md)
18 | * [理解ConcurrentHashMap底层实现原理](docs/Java核心基础/理解ConcurrentHashMap底层实现原理.md)
19 | * [理解数据结构队列](docs/Java核心基础/理解数据结构队列.md)
20 |
--------------------------------------------------------------------------------
/docs/Java核心基础/理解ConcurrentHashMap底层实现原理.md:
--------------------------------------------------------------------------------
1 | # [ConcurrentHashMap 实现原理]
2 |
3 | 由于 `HashMap` 是一个线程不安全的容器,主要体现在容量大于`总量*负载因子`发生扩容时会出现环形链表从而导致死循环。
4 |
5 | 因此需要支持线程安全的并发容器 `ConcurrentHashMap` 。
6 |
7 | ## [JDK1.7 实现]
8 | ### [数据结构]
9 |
10 | 
11 |
12 | 如图所示,是由 `Segment` 数组、`HashEntry` 数组组成,和 `HashMap` 一样,仍然是数组加链表组成。
13 |
14 | `ConcurrentHashMap` 采用了分段锁技术,其中 `Segment` 继承于 `ReentrantLock`。不会像 `HashTable` 那样不管是 `put` 还是 `get` 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 `CurrencyLevel` (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 `Segment` 时,不会影响到其他的 `Segment`。
15 |
16 | ### [get 方法]
17 |
18 | `ConcurrentHashMap` 的 `get` 方法是非常高效的,因为整个过程都不需要加锁。
19 |
20 | 只需要将 `Key` 通过 `Hash` 之后定位到具体的 `Segment` ,再通过一次 `Hash` 定位到具体的元素上。由于 `HashEntry` 中的 `value` 属性是用 `volatile` 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值([volatile 相关知识点](https://github.com/crossoverJie/Java-Interview/blob/master/MD/Threadcore.md#可见性))。
21 |
22 | ### [put 方法]
23 |
24 | 内部 `HashEntry` 类 :
25 |
26 | ```java
27 | static final class HashEntry {
28 | final int hash;
29 | final K key;
30 | volatile V value;
31 | volatile HashEntry next;
32 |
33 | HashEntry(int hash, K key, V value, HashEntry next) {
34 | this.hash = hash;
35 | this.key = key;
36 | this.value = value;
37 | this.next = next;
38 | }
39 | }
40 | ```
41 |
42 | 虽然 HashEntry 中的 value 是用 `volatile` 关键词修饰的,但是并不能保证并发的原子性,所以 put 操作时仍然需要加锁处理。
43 |
44 | 首先也是通过 Key 的 Hash 定位到具体的 Segment,在 put 之前会进行一次扩容校验。这里比 HashMap 要好的一点是:HashMap 是插入元素之后再看是否需要扩容,有可能扩容之后后续就没有插入就浪费了本次扩容(扩容非常消耗性能)。
45 |
46 | 而 ConcurrentHashMap 不一样,它是在将数据插入之前检查是否需要扩容,之后再做插入操作。
47 |
48 | ### [size 方法]
49 |
50 | 每个 `Segment` 都有一个 `volatile` 修饰的全局变量 `count` ,求整个 `ConcurrentHashMap` 的 `size` 时很明显就是将所有的 `count` 累加即可。但是 `volatile` 修饰的变量却不能保证多线程的原子性,所有直接累加很容易出现并发问题。
51 |
52 | 但如果每次调用 `size` 方法将其余的修改操作加锁效率也很低。所以做法是先尝试两次将 `count` 累加,如果容器的 `count` 发生了变化再加锁来统计 `size`。
53 |
54 | 至于 `ConcurrentHashMap` 是如何知道在统计时大小发生了变化呢,每个 `Segment` 都有一个 `modCount` 变量,每当进行一次 `put remove` 等操作,`modCount` 将会 +1。只要 `modCount` 发生了变化就认为容器的大小也在发生变化。
55 |
56 | ## [JDK1.8 实现]
57 |
58 | 
59 |
60 | 1.8 中的 ConcurrentHashMap 数据结构和实现与 1.7 还是有着明显的差异。
61 |
62 | 其中抛弃了原有的 Segment 分段锁,而采用了 `CAS + synchronized` 来保证并发安全性。
63 |
64 | 
65 |
66 | 也将 1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的。
67 |
68 | 其中的 `val next` 都用了 volatile 修饰,保证了可见性。
69 |
70 | ### [put 方法]
71 |
72 | 重点来看看 put 函数:
73 |
74 | 
75 |
76 | - 根据 key 计算出 hashcode 。
77 | - 判断是否需要进行初始化。
78 | - `f` 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
79 | - 如果当前位置的 `hashcode == MOVED == -1`,则需要进行扩容。
80 | - 如果都不满足,则利用 synchronized 锁写入数据。
81 | - 如果数量大于 `TREEIFY_THRESHOLD` 则要转换为红黑树。
82 |
83 | ### [get 方法]
84 |
85 | 
86 |
87 | - 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
88 | - 如果是红黑树那就按照树的方式获取值。
89 | - 都不满足那就按照链表的方式遍历获取值。
90 |
91 | ## [总结]
92 |
93 | 1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(`O(logn)`),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。
--------------------------------------------------------------------------------
/docs/Java核心基础/理解HashMap为什么是线程不安全的.md:
--------------------------------------------------------------------------------
1 | # HashMap 为什么是线程不安全的?
2 |
3 | ## 1. 同时 put 碰撞导致数据丢失
4 |
5 | 比如,有多个线程同时使用 put 来添加元素,而且恰好两个 put 的 key 是一样的,它们发生了碰撞,也就是根据 hash 值计算出来的 bucket 位置一样,并且两个线程又同时判断该位置是空的,可以写入,所以这两个线程的两个不同的 value 便会添加到数组的同一个位置,这样最终就只会保留一个数据,丢失一个数据。
6 |
7 | JDK1.8源码分析:
8 |
9 | ```java
10 | public V put(K key, V value) {
11 | //调用putVal
12 | return putVal(hash(key), key, value, false, true);
13 | }
14 |
15 | final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
16 | boolean evict) {
17 | Node[] tab; Node p; int n, i;
18 | if ((tab = table) == null || (n = tab.length) == 0)
19 | n = (tab = resize()).length;
20 | if ((p = tab[i = (n - 1) & hash]) == null)
21 | tab[i] = newNode(hash, key, value, null);
22 | else {
23 | Node e; K k;
24 | if (p.hash == hash &&
25 | ((k = p.key) == key || (key != null && key.equals(k))))
26 | e = p;
27 | else if (p instanceof TreeNode)
28 | e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
29 | else {
30 | for (int binCount = 0; ; ++binCount) {
31 | if ((e = p.next) == null) {
32 | p.next = newNode(hash, key, value, null);
33 | if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
34 | treeifyBin(tab, hash);
35 | break;
36 | }
37 | if (e.hash == hash &&
38 | ((k = e.key) == key || (key != null && key.equals(k))))
39 | break;
40 | p = e;
41 | }
42 | }
43 | if (e != null) { // existing mapping for key
44 | V oldValue = e.value;
45 | if (!onlyIfAbsent || oldValue == null)
46 | e.value = value;
47 | afterNodeAccess(e);
48 | return oldValue;
49 | }
50 | }
51 | //线程不安全所在,++操作并不是原子操作
52 | /**
53 | * 第一个步骤是读取;
54 | * 第二个步骤是增加;
55 | * 第三个步骤是保存。
56 | */
57 | ++modCount;
58 | if (++size > threshold)
59 | resize();
60 | afterNodeInsertion(evict);
61 | return null;
62 | }
63 | ```
64 |
65 | 
66 |
67 | 我们根据箭头指向依次看,假设线程 1 首先拿到 i=1 的结果,然后进行 i+1 操作,但此时 i+1 的结果并没有保存下来,线程 1 就被切换走了,于是 CPU 开始执行线程 2,它所做的事情和线程 1 是一样的 i++ 操作,但此时我们想一下,它拿到的 i 是多少?实际上和线程 1 拿到的 i 的结果一样都是 1,为什么呢?因为线程 1 虽然对 i 进行了 +1 操作,但结果没有保存,所以线程 2 看不到修改后的结果。
68 |
69 | 然后假设等线程 2 对 i 进行 +1 操作后,又切换到线程 1,让线程 1 完成未完成的操作,即将 i + 1 的结果 2 保存下来,然后又切换到线程 2 完成 i = 2 的保存操作,虽然两个线程都执行了对 i 进行 +1 的操作,但结果却最终保存了 i = 2 的结果,而不是我们期望的 i = 3,这样就发生了线程安全问题,导致了数据结果错误,这也是最典型的线程安全问题。
70 |
71 | ## 2.可见性问题无法保证
72 |
73 | 可见性也是线程安全的一部分,如果某一个数据结构声称自己是线程安全的,那么它同样需要保证可见性,也就是说,当一个线程操作这个容器的时候,该操作需要对另外的线程都可见,也就是其他线程都能感知到本次操作。可是 HashMap 对此是做不到的,如果线程 1 给某个 key 放入了一个新值,那么线程 2 在获取对应的 key 的值的时候,它的可见性是无法保证的,也就是说线程 2 可能可以看到这一次的更改,但也有可能看不到。所以从可见性的角度出发,HashMap 同样是线程非安全的。
74 |
75 | ## 3.死循环造成 CPU 100%
76 |
77 | HashMap 有可能会发生死循环并且造成 CPU 100% ,这种情况发生最主要的原因就是在扩容的时候,也就是内部新建新的 HashMap 的时候,扩容的逻辑会反转散列桶中的节点顺序,当有多个线程同时进行扩容的时候,由于 HashMap 并非线程安全的,所以如果两个线程同时反转的话,便可能形成一个循环,并且这种循环是链表的循环,相当于 A 节点指向 B 节点,B 节点又指回到 A 节点,这样一来,在下一次想要获取该 key 所对应的 value 的时候,便会在遍历链表的时候发生永远无法遍历结束的情况,也就发生 CPU 100% 的情况。
78 |
79 | ## 4. 扩容期间取出的值不准确
80 |
81 | HashMap 本身默认的容量不是很大,如果不停地往 map 中添加新的数据,它便会在合适的时机进行扩容。而在扩容期间,它会新建一个新的空数组,并且用旧的项填充到这个新的数组中去。那么,在这个填充的过程中,如果有线程获取值,很可能会取到 null 值,而不是我们所希望的、原来添加的值。
82 |
83 |
--------------------------------------------------------------------------------
/docs/Java核心基础/理解Java关键字.md:
--------------------------------------------------------------------------------
1 | # Java关键字理解
2 |
3 | ## 引导语
4 |
5 | Java 中的关键字很多,大约有 50+,在命名上我们不能和这些关键字冲突的,编译会报错,每个关键字都代表着不同场景下的不同含义,接下来我们挑选 6 个比较重要的关键字,深入学习一下。
6 |
7 | ## 1 static
8 |
9 | 意思是静态的、全局的,一旦被修饰,说明被修饰的东西在一定范围内是共享的,谁都可以访问,这时候需要注意并发读写的问题。
10 |
11 | ### 1.1 修饰的对象
12 |
13 | static 只能修饰类变量、方法和方法块。
14 |
15 | **当 static 修饰类变量时**,如果该变量是 public 的话,表示该变量任何类都可以直接访问,而且无需初始化类,直接使用 **类名.static 变量** 这种形式访问即可。
16 |
17 | 这时候我们非常需要注意的一点就是线程安全的问题了,因为当多个线程同时对共享变量进行读写时,很有可能会出现并发问题,如我们定义了:`public static List list = new ArrayList();`这样的共享变量。这个 list 如果同时被多个线程访问的话,就有线程安全的问题,这时候一般有两个解决办法:
18 |
19 | 1. 把线程不安全的 ArrayList 换成 线程安全的 CopyOnWriteArrayList;
20 | 2. 每次访问时,手动加锁。
21 |
22 | 所以在使用 static 修饰类变量时,如何保证线程安全是我们常常需要考虑的。
23 |
24 | **当 static 修饰方法时**,代表该方法和当前类是无关的,任意类都可以直接访问(如果权限是 public 的话)。
25 |
26 | 有一点需要注意的是,该方法内部只能调用同样被 static 修饰的方法,不能调用普通方法,我们常用的 util 类里面的各种方法,我们比较喜欢用 static 修饰方法,好处就是调用特别方便。
27 |
28 | static 方法内部的变量在执行时是没有线程安全问题的。方法执行时,数据运行在栈里面,栈的数据每个线程都是隔离开的,所以不会有线程安全的问题,所以 util 类的各个 static 方法,我们是可以放心使用的。
29 |
30 | **当 static 修饰方法块时**,我们叫做静态块,静态块常常用于在类启动之前,初始化一些值,比如:
31 |
32 | ```java
33 | public static List list = new ArrayList();
34 | // 进行一些初始化的工作
35 | static {
36 | list.add("1");
37 | }
38 | ```
39 |
40 | 这段代码演示了静态块做一些初始化的工作,但需要注意的是,静态块只能调用同样被 static 修饰的变量,并且 static 的变量需要写在静态块的前面,不然编译也会报错。
41 |
42 | ### 1.2 初始化时机
43 |
44 | 对于被 static 修饰的类变量、方法块和静态方法的初始化时机,我们写了一个测试 demo,如下图:
45 |  打印出来的结果是:
46 |
47 | 父类静态变量初始化
48 | 父类静态块初始化
49 | 子类静态变量初始化
50 | 子类静态块初始化
51 | main 方法执行
52 | 父类构造器初始化
53 | 子类构造器初始化
54 |
55 | 从结果中,我们可以看出两点:
56 |
57 | 1. 父类的静态变量和静态块比子类优先初始化;
58 | 2. 静态变量和静态块比类构造器优先初始化。
59 |
60 | 被 static 修饰的方法,在类初始化的时候并不会初始化,只有当自己被调用时,才会被执行。
61 |
62 | ## 2 final
63 |
64 | final 的意思是不变的,一般来说用于以下三种场景:
65 |
66 | 1. 被 final 修饰的类,表明该类是无法继承的;
67 | 2. 被 final 修饰的方法,表明该方法是无法覆写的;
68 | 3. 被 final 修饰的变量,说明该变量在声明的时候,就必须初始化完成,而且以后也不能修改其内存地址。
69 |
70 | 第三点注意下,我们说的是无法修改其内存地址,并没有说无法修改其值。因为对于 List、Map 这些集合类来说,被 final 修饰后,是可以修改其内部值的,但却无法修改其初始化时的内存地址。
71 |
72 | ## 3 try、catch、finally
73 |
74 | 这三个关键字常用于我们捕捉异常的一整套流程,try 用来确定代码执行的范围,catch 捕捉可能会发生的异常,finally 用来执行一定要执行的代码块,除了这些,我们还需要清楚,每个地方如果发生异常会怎么办,我们举一个例子来演示一下:
75 |
76 | ```java
77 | public void testCatchFinally() {
78 | try {
79 | log.info("try is run");
80 | if (true) {
81 | throw new RuntimeException("try exception");
82 | }
83 | } catch (Exception e) {
84 | log.info("catch is run");
85 | if (true) {
86 | throw new RuntimeException("catch exception");
87 | }
88 | } finally {
89 | log.info("finally is run");
90 | }
91 | }
92 | ```
93 |
94 | 这个代码演示了在 try、catch 中都遇到了异常,代码的执行顺序为:try -> catch -> finally,输出的结果如下:
95 |  可以看到两点:
96 |
97 | 1. finally 先执行后,再抛出 catch 的异常;
98 | 2. 最终捕获的异常是 catch 的异常,try 抛出来的异常已经被 catch 吃掉了,所以当我们遇见 catch 也有可能会抛出异常时,我们可以先打印出 try 的异常,这样 try 的异常在日志中就会有所体现。
99 |
100 | ## 4 volatile
101 |
102 | volatile 的意思是可见的,常用来修饰某个共享变量,意思是当共享变量的值被修改后,会及时通知到其它线程上,其它线程就能知道当前共享变量的值已经被修改了。
103 |
104 | 我们再说原理之前,先说下基础知识。就是在多核 CPU 下,为了提高效率,线程在拿值时,是直接和 CPU 缓存打交道的,而不是内存。主要是因为 CPU 缓存执行速度更快,比如线程要拿值 C,会直接从 CPU 缓存中拿, CPU 缓存中没有,就会从内存中拿,所以线程读的操作永远都是拿 CPU 缓存的值。
105 |
106 | 这时候会产生一个问题,CPU 缓存中的值和内存中的值可能并不是时刻都同步,导致线程计算的值可能不是最新的,共享变量的值有可能已经被其它线程所修改了,但此时修改是机器内存的值,CPU 缓存的值还是老的,导致计算会出现问题。
107 |
108 | 这时候有个机制,就是内存会主动通知 CPU 缓存。当前共享变量的值已经失效了,你需要重新来拉取一份,CPU 缓存就会重新从内存中拿取一份最新的值。
109 |
110 | volatile 关键字就会触发这种机制,加了 volatile 关键字的变量,就会被识别成共享变量,内存中值被修改后,会通知到各个 CPU 缓存,使 CPU 缓存中的值也对应被修改,从而保证线程从 CPU 缓存中拿取出来的值是最新的。
111 |
112 | 我们画了一个图来说明一下:
113 |  从图中我们可以看到,线程 1 和线程 2 一开始都读取了 C 值,CPU 1 和 CPU 2 缓存中也都有了 C 值,然后线程 1 把 C 值修改了,这时候内存的值和 CPU 2 缓存中的 C 值就不等了,内存这时发现 C 值被 volatile 关键字修饰,发现其是共享变量,就会使 CPU 2 缓存中的 C 值状态置为无效,CPU 2 会从内存中重新拉取最新的值,这时候线程 2 再来读取 C 值时,读取的已经是内存中最新的值了。
114 |
115 | ## 5 transient
116 |
117 | transient 关键字我们常用来修饰类变量,意思是当前变量是无需进行序列化的。在序列化时,就会忽略该变量,这些在序列化工具底层,就已经对 transient 进行了支持。
118 |
119 | ## 6 default
120 |
121 | default 关键字一般会用在接口的方法上,意思是对于该接口,子类是无需强制实现的,但自己必须有默认实现,我们举个例子如下:
122 | 
123 |
124 | ## 7 面试题
125 |
126 | ### 7.1 如何证明 static 静态变量和类无关?
127 |
128 | 答:从三个方面就可以看出静态变量和类无关。
129 |
130 | 1. 我们不需要初始化类就可直接使用静态变量;
131 | 2. 我们在类中写个 main 方法运行,即便不写初始化类的代码,静态变量都会自动初始化;
132 | 3. 静态变量只会初始化一次,初始化完成之后,不管我再 new 多少个类出来,静态变量都不会再初始化了。
133 |
134 | 不仅仅是静态变量,静态方法块也和类无关。
135 |
136 | ### 7.2 常常看见变量和方法被 static 和 final 两个关键字修饰,为什么这么做?
137 |
138 | 答:这么做有两个目的:
139 |
140 | 1. 变量和方法于类无关,可以直接使用,使用比较方便;
141 | 2. 强调变量内存地址不可变,方法不可继承覆写,强调了方法内部的稳定性。
142 |
143 | ### 7.3 catch 中发生了未知异常,finally 还会执行么?
144 |
145 | 答:会的,catch 发生了异常,finally 还会执行的,并且是 finally 执行完成之后,才会抛出 catch 中的异常。
146 |
147 | 不过 catch 会吃掉 try 中抛出的异常,为了避免这种情况,在一些可以预见 catch 中会发生异常的地方,先把 try 抛出的异常打印出来,这样从日志中就可以看到完整的异常了。
148 |
149 | ## 总结
150 |
151 | Java 的关键字属于比较基础的内容,我们需要清晰明确其含义,才能在后续源码阅读和工作中碰到这些关键字时了然于心,才能明白为什么会在这里使用这样的关键字。比如 String 源码是如何使用 final 关键字达到起不变性的,比如 Java 8 集合中 Map 是如何利用 default 关键字新增各种方法的,这些我们在后续内容都会提到。
152 |
153 |
--------------------------------------------------------------------------------
/docs/Java核心基础/理解泛型与迭代器.md:
--------------------------------------------------------------------------------
1 | ## 理解泛型与迭代器
2 |
3 | ### 泛型
4 |
5 | #### 1)为什么要用泛型?
6 |
7 | 在泛型没有诞生之前,我们经常会遇到这样的问题,如以下代码所示:
8 |
9 | ```
10 | ArrayList arrayList = new ArrayList();
11 | arrayList.add("Java");
12 | arrayList.add(24);
13 | for (int i = 0; i < arrayList.size(); i++) {
14 | String str = (String) arrayList.get(i);
15 | System.out.println(str);
16 | }
17 | ```
18 |
19 | 看起来好像没有什么大问题,也能正常编译,但真正运行起来就会报错:
20 |
21 | > Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
22 | >
23 | > at xxx(xxx.java:12)
24 | >
25 | > 类型转换出错,当我们给 ArrayList 放入不同类型的数据,却使用一种类型进行接收的时候,就会出现很多类似的错误,可能更多的时候,是因为开发人员的不小心导致的。那有没有好的办法可以杜绝此类问题的发生呢?这个时候 Java 语言提供了一个很好的解决方案——“泛型”。
26 |
27 | #### 2)泛型介绍
28 |
29 | **泛型**:泛型本质上是类型参数化,解决了不确定对象的类型问题。
30 | 泛型的使用,请参考以下代码:
31 |
32 | ```
33 | ArrayList arrayList = new ArrayList();
34 | arrayList.add("Java");
35 | ```
36 |
37 | 这个时候如果给 arrayList 添加非 String 类型的元素,编译器就会报错,提醒开发人员插入相同类型的元素。
38 |
39 | 报错信息如下图所示:
40 |
41 | 
42 |
43 | 这样就可以避免开头示例中,类型不一致导致程序运行过程中报错的问题了。
44 |
45 | #### 3)泛型的优点
46 |
47 | 泛型的优点主要体现在以下三个方面。
48 |
49 | - 安全:不用担心程序运行过程中出现类型转换的错误。
50 | - 避免了类型转换:如果是非泛型,获取到的元素是 Object 类型的,需要强制类型转换。
51 | - 可读性高:编码阶段就明确的知道集合中元素的类型。
52 |
53 | ### 迭代器(Iterator)
54 |
55 | #### 1)为什么要用迭代器?
56 |
57 | 我们回想一下,在迭代器(Iterator)没有出现之前,如果要遍历数组和集合,需要使用方法。
58 |
59 | 数组遍历,代码如下:
60 |
61 | ```
62 | String[] arr = new String[]{"Java", "Java虚拟机", "Java中文社群"};
63 | for (int i = 0; i < arr.length; i++) {
64 | String item = arr[i];
65 | }
66 | ```
67 |
68 | 集合遍历,代码如下:
69 |
70 | ```
71 | List list = new ArrayList() {{
72 | add("Java");
73 | add("Java虚拟机");
74 | add("Java中文社群");
75 | }};
76 | for (int i = 0; i < list.size(); i++) {
77 | String item = list.get(i);
78 | }
79 | ```
80 |
81 | 而迭代器的产生,就是为不同类型的容器遍历,提供标准统一的方法。
82 |
83 | 迭代器遍历,代码如下:
84 |
85 | ```
86 | Iterator iterator = list.iterator();
87 | while (iterator.hasNext()) {
88 | Object object = iterator.next();
89 | // do something
90 | }
91 | ```
92 |
93 | **总结**:使用了迭代器就可以不用关注容器的内部细节,用同样的方式遍历不同类型的容器。
94 |
95 | #### 2)迭代器介绍
96 |
97 | 迭代器是用来遍历容器内所有元素对象的,也是一种常见的设计模式。
98 |
99 | 迭代器包含以下四个方法。
100 |
101 | - hasNext():boolean —— 容器内是否还有可以访问的元素。
102 | - next():E —— 返回下一个元素。
103 | - remove():void —— 删除当前元素。
104 | - forEachRemaining(Consumer):void —— JDK 8 中添加的,提供一个 lambda 表达式遍历容器元素。
105 |
106 | 迭代器使用如下:
107 |
108 | ```
109 | List list = new ArrayList() {{
110 | add("Java");
111 | add("Java虚拟机");
112 | add("Java中文社群");
113 | }};
114 | Iterator iterator = list.iterator();
115 | // 遍历
116 | while (iterator.hasNext()){
117 | String str = (String) iterator.next();
118 | if (str.equals("Java中文社群")){
119 | iterator.remove();
120 | }
121 | }
122 | System.out.println(list);
123 | ```
124 |
125 | 程序执行结果:
126 |
127 | ```
128 | [Java, Java虚拟机]
129 | ```
130 |
131 | forEachRemaining 使用如下:
132 |
133 | ```
134 | List list = new ArrayList() {{
135 | add("Java");
136 | add("Java虚拟机");
137 | add("Java中文社群");
138 | }};
139 | // forEachRemaining 使用
140 | list.iterator().forEachRemaining(item -> System.out.println(item));
141 | ```
142 |
143 | ### 相关面试题
144 |
145 | #### 1.为什么迭代器的 next() 返回的是 Object 类型?
146 |
147 | 答:因为迭代器不需要关注容器的内部细节,所以 next() 返回 Object 类型就可以接收任何类型的对象。
148 |
149 | #### 2.HashMap 的遍历方式都有几种?
150 |
151 | 答:HashMap 的遍历分为以下四种方式。
152 |
153 | - 方式一:entrySet 遍历
154 | - 方式二:iterator 遍历
155 | - 方式三:遍历所有的 key 和 value
156 | - 方式四:通过 key 值遍历
157 |
158 | 以上方式的代码实现如下:
159 |
160 | ```
161 | Map hashMap = new HashMap();
162 | hashMap.put("name", "老王");
163 | hashMap.put("sex", "你猜");
164 | // 方式一:entrySet 遍历
165 | for (Map.Entry item : hashMap.entrySet()) {
166 | System.out.println(item.getKey() + ":" + item.getValue());
167 | }
168 | // 方式二:iterator 遍历
169 | Iterator> iterator = hashMap.entrySet().iterator();
170 | while (iterator.hasNext()) {
171 | Map.Entry entry = iterator.next();
172 | System.out.println(entry.getKey() + ":" + entry.getValue());
173 | }
174 | // 方式三:遍历所有的 key 和 value
175 | for (Object k : hashMap.keySet()) {
176 | // 循环所有的 key
177 | System.out.println(k);
178 | }
179 | for (Object v : hashMap.values()) {
180 | // 循环所有的值
181 | System.out.println(v);
182 | }
183 | // 方式四:通过 key 值遍历
184 | for (Object k : hashMap.keySet()) {
185 | System.out.println(k + ":" + hashMap.get(k));
186 | }
187 | ```
188 |
189 | #### 3.以下关于泛型说法错误的是?
190 |
191 | A:泛型可以修饰类
192 | B:泛型可以修饰方法
193 | C:泛型不可以修饰接口
194 | D:以上说法全错
195 |
196 | 答:选 C,泛型可以修饰类、方法、接口、变量。
197 | 例如:
198 |
199 | ```
200 | public interface Iterable {
201 | }
202 | ```
203 |
204 | #### 4.以下程序执行的结果是什么?
205 |
206 | ```java
207 | List list = new ArrayList<>();
208 | List list2 = new ArrayList<>();
209 | System.out.println(list.getClass() == list2.getClass());
210 | ```
211 |
212 | 答:程序的执行结果是 `true`。
213 | 题目解析:Java 中泛型在编译时会进行类型擦除,因此 `List list` 和 `List list2` 类型擦除后的结果都是 java.util.ArrayLis ,进而 list.getClass() == list2.getClass() 的结果也一定是 true。
214 |
215 | #### 5. `List