├── .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 | 36 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 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 | [![QQ群](https://img.shields.io/badge/QQ%E7%BE%A4-660108379-yellowgreen.svg)](https://jq.qq.com/?_wv=1027&k=5HPYvQk) 4 | ![微信公众号](https://img.shields.io/badge/微信公众号-码上Java-yellowgreen.svg) 5 | [![GitHubPages](https://img.shields.io/badge/在线阅读-GitHubPages-yellowgreen.svg)](https://msjavacoder.github.io/msJava) 6 | [![GiteePages](https://img.shields.io/badge/在线阅读-GiteePages-yellowgreen.svg)](https://msjavacoder.gitee.io/msjava) 7 | ![version-v2.0.0](https://img.shields.io/badge/version-v2.0.0-green.svg) 8 | ![license-GPL](https://img.shields.io/badge/license-GPL-blue.svg) 9 | 10 | --- 11 | ![ms-Java.webp](https://cdn.jsdelivr.net/gh/msJavaCoder/msJava@master/image/码上Java.webp) 12 | 13 | --- 14 | 15 | ### ☎ 联系作者 16 | > 关注微信订阅号: 码上Java 🔥🔥🔥 -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /_coverpage.md: -------------------------------------------------------------------------------- 1 | ![logo](https://docsify.js.org/_media/icon.svg) 2 | 3 |

JAVA后端核心知识体系

4 |

风儿哪儿吹,不要问跟风的人。💯

5 | 6 | ![QQ群](https://img.shields.io/badge/QQ%E7%BE%A4-660108379-yellowgreen.svg) 7 | ![微信公众号](https://img.shields.io/badge/微信公众号-码上Java-yellowgreen.svg) 8 | ![version-v2.0.0](https://img.shields.io/badge/version-v2.0.0-green.svg) 9 | ![license-GPL](https://img.shields.io/badge/license-GPL-blue.svg) 10 | 11 | [GitHub](https://github.com/msJavaCoder/msJava) 12 | [开始阅读](#🔥-微信公众号-:-码上java) 13 | 14 | 15 | 16 | ![GGIxB.jpg](https://s.im5i.com/2021/04/14/GGIxB.jpg) 17 | 18 | ![color](#000000) -------------------------------------------------------------------------------- /_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 | ![](https://imgkr2.cn-bj.ufileos.com/c20b4fd7-09d2-44a8-93b2-3bc56a9cc8fa.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=R2EsHFZ3HpgxLBivB1YMldwMKbg%253D&Expires=1599055198) 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 | ![](https://imgkr2.cn-bj.ufileos.com/d98be439-2529-423d-a72d-0a090d4a1714.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=1PJteW%252FObFcinppVV1QA5uOAByc%253D&Expires=1599744613) 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 | ![img](https://i.loli.net/2019/07/19/5d313f638492c49210.jpg) 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 | ![](https://imgkr2.cn-bj.ufileos.com/b60c00a6-5701-474a-b69a-6c27cbe68dab.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=Rxwu7AipZw%252F2JKFk5%252FBsGJO5%252Bn0%253D&Expires=1599225423) 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 | ![Ga7vC.png](https://s.im5i.com/2021/04/14/Ga7vC.png) -------------------------------------------------------------------------------- /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 | ![img](https://i.loli.net/2019/07/19/5d313f7215c4240040.jpg) 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 | ![img](https://i.loli.net/2019/07/19/5d313f751f85f13539.jpg) 59 | 60 | 1.8 中的 ConcurrentHashMap 数据结构和实现与 1.7 还是有着明显的差异。 61 | 62 | 其中抛弃了原有的 Segment 分段锁,而采用了 `CAS + synchronized` 来保证并发安全性。 63 | 64 | ![img](https://i.loli.net/2019/07/19/5d313f76c30a232619.jpg) 65 | 66 | 也将 1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的。 67 | 68 | 其中的 `val next` 都用了 volatile 修饰,保证了可见性。 69 | 70 | ### [put 方法] 71 | 72 | 重点来看看 put 函数: 73 | 74 | ![img](https://i.loli.net/2019/07/19/5d313f78c31dc41505.jpg) 75 | 76 | - 根据 key 计算出 hashcode 。 77 | - 判断是否需要进行初始化。 78 | - `f` 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。 79 | - 如果当前位置的 `hashcode == MOVED == -1`,则需要进行扩容。 80 | - 如果都不满足,则利用 synchronized 锁写入数据。 81 | - 如果数量大于 `TREEIFY_THRESHOLD` 则要转换为红黑树。 82 | 83 | ### [get 方法] 84 | 85 | ![img](https://i.loli.net/2019/07/19/5d313f7aab95b41715.jpg) 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 | ![img](https://s0.lgstatic.com/i/image3/M01/60/C7/Cgq2xl4YRJeAC6fuAAA8JO4TxM0077.png) 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 | ![图片描述](https://img02.sogoucdn.com/app/a/100520146/36a3d5e423869264d2b6bcd668e4d83c) 打印出来的结果是: 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 | ![图片描述](https://img03.sogoucdn.com/app/a/100520146/ede7c8130735e2e1d3249d94fad07f45) 可以看到两点: 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 | ![图片描述](https://img01.sogoucdn.com/app/a/100520146/00a3f76b262769c00cf2ee3d3920b29e) 从图中我们可以看到,线程 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 | ![图片描述](https://img02.sogoucdn.com/app/a/100520146/f3cb95c14b3375d311a8638789e7ce81) 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 | ![enter image description here](https://img01.sogoucdn.com/app/a/100520146/ce4396a172a0c78f16705674b22ea316) 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` 和 `List` 有什么区别? 216 | 217 | 答:`List` 可以容纳任意类型,只不过 `List` 被赋值之后,就不允许添加和修改操作了;而 `List` 和 `List` 不同的是它在赋值之后,可以进行添加和修改操作,如下图所示: 218 | 219 | ![enter image description here](https://img03.sogoucdn.com/app/a/100520146/8c3a203e7fef1e6ba89a54c2daed526b) 220 | 221 | #### 6.可以把 `List` 赋值给 `List` 吗? 222 | 223 | 答:不可以,编译器会报错,如下图所示: 224 | 225 | ![enter image description here](https://img02.sogoucdn.com/app/a/100520146/ab3616ef5b2c22c59d4e333029c875e1) 226 | 227 | #### 7. `List` 和 `List` 的区别是什么? 228 | 229 | 答: `List` 和 `List` 都能存储任意类型的数据,但 `List` 和 `List` 的唯一区别就是,`List` 不会触发编译器的类型安全检查,比如把 `List` 赋值给 `List` 是没有任何问题的,但赋值给 `List` 就不行,如下图所示: 230 | 231 | ![enter image description here](https://img02.sogoucdn.com/app/a/100520146/3c0e932d9a67c62888b3dd62f19051be) 232 | 233 | #### 8.以下程序执行的结果是? 234 | 235 | ``` 236 | List list = new ArrayList<>(); 237 | list.add("Java"); 238 | list.add("Java虚拟机"); 239 | list.add("Java中文社群"); 240 | Iterator iterator = list.iterator(); 241 | while (iterator.hasNext()) { 242 | String str = (String) iterator.next(); 243 | if (str.equals("Java中文社群")) { 244 | iterator.remove(); 245 | } 246 | } 247 | while (iterator.hasNext()) { 248 | System.out.println(iterator.next()); 249 | } 250 | System.out.println("Over"); 251 | ``` 252 | 253 | 答:程序打印结果是 `Over`。 254 | 题目解析:因为第一个 while 循环之后,iterator.hasNext() 返回值就为 false 了,所以不会进入第二个循环,之后打印最后的 Over。 255 | 256 | #### 9.泛型的工作原理是什么?为什么要有类型擦除? 257 | 258 | 答:泛型是通过类型擦除来实现的,类型擦除指的是编译器在编译时,会擦除了所有类型相关的信息,比如 `List` 在编译后就会变成 `List` 类型,这样做的目的就是确保能和 Java 5 之前的版本(二进制类库)进行兼容。 259 | 260 | ### 总结 261 | 262 | 通过本文知道了泛型的优点:安全性、避免类型转换、提高了代码的可读性。泛型的本质是类型参数化,但编译之后会执行类型擦除,这样就可以和 Java 5 之前的二进制类库进行兼容。本文也介绍了迭代器(Iterator)的使用,使用迭代器的好处是不用关注容器的内部细节,用同样的方式遍历不同类型的容器。 -------------------------------------------------------------------------------- /docs/Java核心基础/理解浅克隆和深克隆.md: -------------------------------------------------------------------------------- 1 | # 浅克隆和深克隆 2 | 3 | ## 浅克隆 4 | 5 | **浅克隆**(Shadow Clone)是把原型对象中成员变量为值类型的属性都复制给克隆对象,把原型对象中成员变量为引用类型的引用地址也复制给克隆对象,也就是原型对象中如果有成员变量为引用对象,则此引用对象的地址是共享给原型对象和克隆对象的。 6 | 7 | img 8 | 9 | 10 | 11 | ## 深克隆 12 | 13 | **深克隆**(Deep Clone)是将原型对象中的所有类型,无论是值类型还是引用类型,都复制一份给克隆对象,也就是说深克隆会把原型对象和原型对象所引用的对象,都复制一份给克隆对象。 14 | 15 | img 16 | 17 | -------------------------------------------------------------------------------- /docs/Java核心基础/理解面向对象.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msJavaCoder/msJava/2553fc4f1c5b62305a685ad6a051031731ccc251/docs/Java核心基础/理解面向对象.md -------------------------------------------------------------------------------- /docs/Java源码分析/HashSet与TreeSet源码分析.md: -------------------------------------------------------------------------------- 1 | # 👉 HashSet与TreeSet 2 | 3 | HashSet、TreeSet 两个类是在 Map 的基础上组装起来的类,我们学习的侧重点,主要在于 Set 是如何利用 Map 现有的功能,来达成自己的目标的,也就是说如何基于现有的功能进行创新,然后再看看一些改变的小细节是否值得我们学习。 4 | 5 | ## 1 HashSet 6 | 7 | ### 1.1 类注释 8 | 9 | 看源码先看类注释上,我们可以得到的信息有: 10 | 11 | 1. 底层实现基于 HashMap,所以迭代时不能保证按照插入顺序,或者其它顺序进行迭代; 12 | 2. add、remove、contanins、size 等方法的耗时性能,是不会随着数据量的增加而增加的,这个主要跟 HashMap 底层的数组数据结构有关,不管数据量多大,不考虑 hash 冲突的情况下,时间复杂度都是 O (1); 13 | 3. 线程不安全的,如果需要安全请自行加锁,或者使用 Collections.synchronizedSet; 14 | 4. 迭代过程中,如果数据结构被改变,会快速失败的,会抛出 ConcurrentModificationException 异常。 15 | 16 | 我们之前也看过 List、Map 的类注释,我们发现 2、3、4 点信息在类注释中都有提到,所以如果有人问 List、Map、 Set 三者的共同点,那么就可以说 2、3、4 三点。 17 | 18 | ### 1.2 HashSet 是如何组合 HashMap 的 19 | 20 | 刚才是从类注释 1 中看到,HashSet 的实现是基于 HashMap 的,在 Java 中,要基于基础类进行创新实现,有两种办法: 21 | 22 | - 继承基础类,覆写基础类的方法,比如说继承 HashMap , 覆写其 add 的方法; 23 | - 组合基础类,通过调用基础类的方法,来复用基础类的能力。 24 | 25 | HashSet 使用的就是组合 HashMap,其优点如下: 26 | 27 | 1. 继承表示父子类是同一个事物,而 Set 和 Map 本来就是想表达两种事物,所以继承不妥,而且 Java 语法限制,子类只能继承一个父类,后续难以扩展。 28 | 2. 组合更加灵活,可以任意的组合现有的基础类,并且可以在基础类方法的基础上进行扩展、编排等,而且方法命名可以任意命名,无需和基础类的方法名称保持一致。 29 | 30 | 我们在工作中,如果碰到类似问题,我们的原则也是尽量多用组合,少用继承。 31 | 32 | 组合就是把 HashMap 当作自己的一个局部变量,以下是 HashSet 的组合实现: 33 | 34 | ```java 35 | // 把 HashMap 组合进来,key 是 Hashset 的 key,value 是下面的 PRESENT 36 | private transient HashMap map; 37 | // HashMap 中的 value 38 | private static final Object PRESENT = new Object(); 39 | ``` 40 | 41 | 从这两行代码中,我们可以看出两点: 42 | 43 | 1. 我们在使用 HashSet 时,比如 add 方法,只有一个入参,但组合的 Map 的 add 方法却有 key,value 两个入参,相对应上 Map 的 key 就是我们 add 的入参,value 就是第二行代码中的 PRESENT,此处设计非常巧妙,用一个默认值 PRESENT 来代替 Map 的 Value; 44 | 2. 如果 HashSet 是被共享的,当多个线程访问的时候,就会有线程安全问题,因为在后续的所有操作中,并没有加锁。 45 | 46 | HashSet 在以 HashMap 为基础进行实现的时候,首先选择组合的方式,接着使用默认值来代替了 Map 中的 Value 值,设计得非常巧妙,给使用者的体验很好,使用起来简单方便,我们在工作中也可以借鉴这种思想,可以把底层复杂实现包装一下,一些默认实现可以自己吃掉,使吐出去的接口尽量简单好用。 47 | 48 | #### 1.2.1 初始化 49 | 50 | HashSet 的初始化比较简单,直接 new HashMap 即可,比较有意思的是,当有原始集合数据进行初始化的情况下,会对 HashMap 的初始容量进行计算,源码如下: 51 | 52 | ```java 53 | // 对 HashMap 的容量进行了计算 54 | public HashSet(Collection c) { 55 | map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16)); 56 | addAll(c); 57 | } 58 | ``` 59 | 60 | 上述代码中:Math.max ((int) (c.size ()/.75f) + 1, 16),就是对 HashMap 的容量进行了计算,翻译成中文就是 取括号中两个数的最大值(期望的值 / 0.75+1,默认值 16),从计算中,我们可以看出 HashSet 的实现者对 HashMap 的底层实现是非常清楚的,主要体现在两个方面: 61 | 62 | 1. 和 16 比较大小的意思是说,如果给定 HashMap 初始容量小于 16 ,就按照 HashMap 默认的 16 初始化好了,如果大于 16,就按照给定值初始化。 63 | 2. HashMap 扩容的伐值的计算公式是:Map 的容量 * 0.75f,一旦达到阀值就会扩容,此处用 (int) (c.size ()/.75f) + 1 来表示初始化的值,这样使我们期望的大小值正好比扩容的阀值还大 1,就不会扩容,符合 HashMap 扩容的公式。 64 | 65 | 从简单的构造器中,我们就可以看出要很好的组合 api 接口,并没有那么简单,我们可能需要去了解一下被组合的 api 底层的实现,这样才能用好 api。 66 | 67 | 同时这种写法,也提供了一种思路给我们,如果有人问你,往 HashMap 拷贝大集合时,如何给 HashMap 初始化大小时,完全可以借鉴这种写法:取最大值(期望的值 / 0.75 + 1,默认值 16)。 68 | 69 | 至于 HashSet 的其他方法就比较简单了,就是对 Map 的 api 进行了一些包装,如下的 add 方法实现: 70 | 71 | ```java 72 | public boolean add(E e) { 73 | // 直接使用 HashMap 的 put 方法,进行一些简单的逻辑判断 74 | return map.put(e, PRESENT)==null; 75 | } 76 | ``` 77 | 78 | 从 add 方法中,我们就可以看到组合的好处,方法的入参、名称、返回值都可以自定义,如果是继承的话就不行了。 79 | 80 | #### 1.2.2 小结 81 | 82 | HashSet 具体实现值得我们借鉴的地方主要有如下地方,我们平时写代码的时候,完全可以参考参考: 83 | 84 | 1. 对组合还是继承的分析和把握; 85 | 2. 对复杂逻辑进行一些包装,使吐出去的接口尽量简单好用; 86 | 3. 组合其他 api 时,尽量多对组合的 api 多些了解,这样才能更好的使用 api; 87 | 4. HashMap 初始化大小值的模版公式:取括号内两者的最大值(期望的值 / 0.75+1,默认值 16)。 88 | 89 | ## 2 TreeSet 90 | 91 | TreeSet 大致的结构和 HashSet 相似,底层组合的是 TreeMap,所以继承了 TreeMap key 能够排序的功能,迭代的时候,也可以按照 key 的排序顺序进行迭代,我们主要来看复用 TreeMap 时,复用的两种思路: 92 | 93 | #### 2.1 复用 TreeMap 的思路一 94 | 95 | 场景一: TreeSet 的 add 方法,我们来看下其源码: 96 | 97 | ```java 98 | public boolean add(E e) { 99 | return m.put(e, PRESENT)==null; 100 | } 101 | ``` 102 | 103 | 可以看到,底层直接使用的是 HashMap 的 put 的能力,直接拿来用就好了。 104 | 105 | #### 2.2 复用 TreeMap 的思路二 106 | 107 | 场景二:需要迭代 TreeSet 中的元素,那应该也是像 add 那样,直接使用 HashMap 已有的迭代能力,比如像下面这样: 108 | 109 | ```java 110 | // 模仿思路一的方式实现 111 | public Iterator descendingIterator() { 112 | // 直接使用 HashMap.keySet 的迭代能力 113 | return m.keySet().iterator(); 114 | } 115 | ``` 116 | 117 | 这种是思路一的实现方式,TreeSet 组合 TreeMap,直接选择 TreeMap 的底层能力进行包装,但 TreeSet 实际执行的思路却完全相反,我们看源码: 118 | 119 | ```java 120 | // NavigableSet 接口,定义了迭代的一些规范,和一些取值的特殊方法 121 | // TreeSet 实现了该方法,也就是说 TreeSet 本身已经定义了迭代的规范 122 | public interface NavigableSet extends SortedSet { 123 | Iterator iterator(); 124 | E lower(E e); 125 | } 126 | // m.navigableKeySet() 是 TreeMap 写了一个子类实现了 NavigableSet 127 | // 接口,实现了 TreeSet 定义的迭代规范 128 | public Iterator iterator() { 129 | return m.navigableKeySet().iterator(); 130 | } 131 | ``` 132 | 133 | TreeMap 中对 NavigableSet 接口的实现源码截图如下: 134 | 135 | 图片描述 从截图中(截图是在 TreeMap 中),我们可以看出 TreeMap 实现了 TreeSet 定义的各种特殊方法。 136 | 137 | 我们可以看到,这种思路是 TreeSet 定义了接口的规范,TreeMap 负责去实现,实现思路和思路一是相反的。 138 | 139 | 我们总结下 TreeSet 组合 TreeMap 实现的两种思路: 140 | 141 | 1. TreeSet 直接使用 TreeMap 的某些功能,自己包装成新的 api。 142 | 2. TreeSet 定义自己想要的 api,自己定义接口规范,让 TreeMap 去实现。 143 | 144 | 方案 1 和 2 的调用关系,都是 TreeSet 调用 TreeMap,但功能的实现关系完全相反,第一种是功能的定义和实现都在 TreeMap,TreeSet 只是简单的调用而已,第二种 TreeSet 把接口定义出来后,让 TreeMap 去实现内部逻辑,TreeSet 负责接口定义,TreeMap 负责具体实现,这样子的话因为接口是 TreeSet 定义的,所以实现一定是 TreeSet 最想要的,TreeSet 甚至都不用包装,可以直接把返回值吐出去都行。 145 | 146 | 我们思考下这两种复用思路的原因: 147 | 148 | 1. 像 add 这些简单的方法,我们直接使用的是思路 1,主要是 add 这些方法实现比较简单,没有复杂逻辑,所以 TreeSet 自己实现起来比较简单; 149 | 2. 思路 2 主要适用于复杂场景,比如说迭代场景,TreeSet 的场景复杂,比如要能从头开始迭代,比如要能取第一个值,比如要能取最后一个值,再加上 TreeMap 底层结构比较复杂,TreeSet 可能并不清楚 TreeMap 底层的复杂逻辑,这时候让 TreeSet 来实现如此复杂的场景逻辑,TreeSet 就搞不定了,不如接口让 TreeSet 来定义,让 TreeMap 去负责实现,TreeMap 对底层的复杂结构非常清楚,实现起来既准确又简单。 150 | 151 | #### 2.3 小结 152 | 153 | TreeSet 对 TreeMap 的两种不同复用思路,很重要,在工作中经常会遇到,特别是思路二,比如说 dubbo 的泛化调用,DDD 中的依赖倒置等等,原理都是 TreeSet 第二种的复用思想。 154 | 155 | ## 3 面试题 156 | 157 | HashSet 和 TreeSet 的面试概率比不上 List 和 Map,但只要有机会,并把本文的内容表达出来,绝对是加分项,因为现在 List 和 Map 面试题太多,面试官认为你能答的出来是应该的,但只要你有机会对 HashSet 和 TreeSet 说出本文见解,并且说自己是看源码时领悟到的,绝对肯定是加分项,这些就是你超过面试官预期的惊喜,以下是一些常用的题目: 158 | 159 | ### 3.1 TreeSet 有用过么,平时都在什么场景下使用? 160 | 161 | 答:有木有用过如实回答就好了,我们一般都是在需要把元素进行排序的时候使用 TreeSet,使用时需要我们注意元素最好实现 Comparable 接口,这样方便底层的 TreeMap 根据 key 进行排序。 162 | 163 | ### 3.2 追问,如果我想实现根据 key 的新增顺序进行遍历怎么办? 164 | 165 | 答:要按照 key 的新增顺序进行遍历,首先想到的应该就是 LinkedHashMap,而 LinkedHashSet 正好是基于 LinkedHashMap 实现的,所以我们可以选择使用 LinkedHashSet。 166 | 167 | ### 3.3 追问,如果我想对 key 进行去重,有什么好的办法么? 168 | 169 | 答:我们首先想到的是 TreeSet,TreeSet 底层使用的是 TreeMap,TreeMap 在 put 的时候,如果发现 key 是相同的,会把 value 值进行覆盖,所有不会产生重复的 key ,利用这一特性,使用 TreeSet 正好可以去重。 170 | 171 | ### 3.4 说说 TreeSet 和 HashSet 两个 Set 的内部实现结构和原理? 172 | 173 | 答: HashSet 底层对 HashMap 的能力进行封装,比如说 add 方法,是直接使用 HashMap 的 put 方法,比较简单,但在初始化的时候,我看源码有一些感悟:说一下 HashSet 小结的四小点。 174 | 175 | TreeSet 主要是对 TreeMap 底层能力进行封装复用,我发现了两种非常有意思的复用思路,重复 TreeSet 两种复用思路。 176 | 177 | ## 总结 178 | 179 | 本小节主要说了 Set 源码中两处亮点: 180 | 181 | 1. HashSet 对组合的 HashMap 类扩容的门阀值的深入了解和设计,值得我们借鉴; 182 | 2. TreeSet 对 TreeMap 两种复用思路,值得我们学习,特别是第二种复用思路。 183 | 184 | HashSet 和 TreeSet 不会是面试的重点,但通过以上两点,可以让我们给面试官一种精益求精的感觉,成为加分项。 -------------------------------------------------------------------------------- /docs/Java源码分析/LinkedHashMap源码分析.md: -------------------------------------------------------------------------------- 1 | # 👉 LinkedHashMap源码分析 2 | 3 | HashMap 是无序的,TreeMap 可以按照 key 进行排序,那有木有 Map 是可以维护插入的顺序的呢?接下来我们一起来看下 LinkedHashMap。 4 | 5 | LinkedHashMap 本身是继承 HashMap 的,所以它拥有 HashMap 的所有特性,再此基础上,还提供了两大特性: 6 | 7 | - 按照插入顺序进行访问; 8 | - 实现了访问最少最先删除功能,其目的是把很久都没有访问的 key 自动删除。 9 | 10 | 接着我们来看下上述两大特性。 11 | 12 | ## 1 按照插入顺序访问 13 | 14 | ### 1.1 LinkedHashMap 链表结构 15 | 16 | 我们看下 LinkedHashMap 新增了哪些属性,以达到了链表结构的: 17 | 18 | ```java 19 | // 链表头 20 | transient LinkedHashMap.Entry head; 21 | 22 | // 链表尾 23 | transient LinkedHashMap.Entry tail; 24 | 25 | // 继承 Node,为数组的每个元素增加了 before 和 after 属性 26 | static class Entry extends HashMap.Node { 27 | Entry before, after; 28 | Entry(int hash, K key, V value, Node next) { 29 | super(hash, key, value, next); 30 | } 31 | } 32 | 33 | // 控制两种访问模式的字段,默认 false 34 | // true 按照访问顺序,会把经常访问的 key 放到队尾 35 | // false 按照插入顺序提供访问 36 | final boolean accessOrder; 37 | ``` 38 | 39 | 从上述 Map 新增的属性可以看到,LinkedHashMap 的数据结构很像是把 LinkedList 的每个元素换成了 HashMap 的 Node,像是两者的结合体,也正是因为增加了这些结构,从而能把 Map 的元素都串联起来,形成一个链表,而链表就可以保证顺序了,就可以维护元素插入进来的顺序。 40 | 41 | ### 1.2 如何按照顺序新增 42 | 43 | LinkedHashMap 初始化时,默认 accessOrder 为 false,就是会按照插入顺序提供访问,插入方法使用的是父类 HashMap 的 put 方法,不过覆写了 put 方法执行中调用的 newNode/newTreeNode 和 afterNodeAccess 方法。 44 | 45 | newNode/newTreeNode 方法,控制新增节点追加到链表的尾部,这样每次新节点都追加到尾部,即可保证插入顺序了,我们以 newNode 源码为例: 46 | 47 | ```java 48 | // 新增节点,并追加到链表的尾部 49 | Node newNode(int hash, K key, V value, Node e) { 50 | // 新增节点 51 | LinkedHashMap.Entry p = 52 | new LinkedHashMap.Entry(hash, key, value, e); 53 | // 追加到链表的尾部 54 | linkNodeLast(p); 55 | return p; 56 | } 57 | // link at the end of list 58 | private void linkNodeLast(LinkedHashMap.Entry p) { 59 | LinkedHashMap.Entry last = tail; 60 | // 新增节点等于位节点 61 | tail = p; 62 | // last 为空,说明链表为空,首尾节点相等 63 | if (last == null) 64 | head = p; 65 | // 链表有数据,直接建立新增节点和上个尾节点之间的前后关系即可 66 | else { 67 | p.before = last; 68 | last.after = p; 69 | } 70 | } 71 | ``` 72 | 73 | LinkedHashMap 通过新增头节点、尾节点,给每个节点增加 before、after 属性,每次新增时,都把节点追加到尾节点等手段,在新增的时候,就已经维护了按照插入顺序的链表结构了。 74 | 75 | ### 1.3 按照顺序访问 76 | 77 | LinkedHashMap 只提供了单向访问,即按照插入的顺序从头到尾进行访问,不能像 LinkedList 那样可以双向访问。 78 | 79 | 我们主要通过迭代器进行访问,迭代器初始化的时候,默认从头节点开始访问,在迭代的过程中,不断访问当前节点的 after 节点即可。 80 | 81 | Map 对 key、value 和 entity(节点) 都提供出了迭代的方法,假设我们需要迭代 entity,就可使用 `LinkedHashMap.entrySet().iterator()` 这种写法直接返回 LinkedHashIterator ,LinkedHashIterator 是迭代器,我们调用迭代器的 nextNode 方法就可以得到下一个节点,迭代器的源码如下: 82 | 83 | ```java 84 | // 初始化时,默认从头节点开始访问 85 | LinkedHashIterator() { 86 | // 头节点作为第一个访问的节点 87 | next = head; 88 | expectedModCount = modCount; 89 | current = null; 90 | } 91 | 92 | final LinkedHashMap.Entry nextNode() { 93 | LinkedHashMap.Entry e = next; 94 | if (modCount != expectedModCount)// 校验 95 | throw new ConcurrentModificationException(); 96 | if (e == null) 97 | throw new NoSuchElementException(); 98 | current = e; 99 | next = e.after; // 通过链表的 after 结构,找到下一个迭代的节点 100 | return e; 101 | } 102 | ``` 103 | 104 | 在新增节点时,我们就已经维护了元素之间的插入顺序了,所以迭代访问时非常简单,只需要不断的访问当前节点的下一个节点即可。 105 | 106 | ## 2 访问最少删除策略 107 | 108 | ### 2.1 举例理解 109 | 110 | 这种策略也叫做 LRU(Least recently used,最近最少使用),大概的意思就是经常访问的元素会被追加到队尾,这样不经常访问的数据自然就靠近队头,然后我们可以通过设置删除策略,比如当 Map 元素个数大于多少时,把头节点删除,我们写个 demo 方便大家理解。demo 如下,完整代码可到 github 上查看: 111 | 112 | ```java 113 | public void testAccessOrder() { 114 | // 新建 LinkedHashMap 115 | LinkedHashMap map = new LinkedHashMap(4,0.75f,true) { 116 | { 117 | put(10, 10); 118 | put(9, 9); 119 | put(20, 20); 120 | put(1, 1); 121 | } 122 | 123 | @Override 124 | // 覆写了删除策略的方法,我们设定当节点个数大于 3 时,就开始删除头节点 125 | protected boolean removeEldestEntry(Map.Entry eldest) { 126 | return size() > 3; 127 | } 128 | }; 129 | 130 | log.info("初始化:{}",JSON.toJSONString(map)); 131 | Assert.assertNotNull(map.get(9)); 132 | log.info("map.get(9):{}",JSON.toJSONString(map)); 133 | Assert.assertNotNull(map.get(20)); 134 | log.info("map.get(20):{}",JSON.toJSONString(map)); 135 | 136 | } 137 | ``` 138 | 139 | 打印出来的结果如下: 140 | 141 | ```java 142 | 初始化:{9:9,20:20,1:1} 143 | map.get(9):{20:20,1:1,9:9} 144 | map.get(20):{1:1,9:9,20:20} 145 | ``` 146 | 147 | 可以看到,map 初始化的时候,我们放进去四个元素,但结果只有三个元素,10 不见了,这个主要是因为我们覆写了 removeEldestEntry 方法,我们实现了如果 map 中元素个数大于 3 时,我们就把队头的元素删除,当 put(1, 1) 执行的时候,正好把队头的 10 删除,这个体现了达到我们设定的删除策略时,会自动的删除头节点。 148 | 149 | 当我们调用 map.get(9) 方法时,元素 9 移动到队尾,调用 map.get(20) 方法时, 元素 20 被移动到队尾,这个体现了经常被访问的节点会被移动到队尾。 150 | 151 | 这个例子就很好的说明了访问最少删除策略,接下来我们看下原理。 152 | 153 | ### 2.2 元素被转移到队尾 154 | 155 | 我们先来看下为什么 get 时,元素会被移动到队尾: 156 | 157 | ```java 158 | public V get(Object key) { 159 | Node e; 160 | // 调用 HashMap get 方法 161 | if ((e = getNode(hash(key), key)) == null) 162 | return null; 163 | // 如果设置了 LRU 策略 164 | if (accessOrder) 165 | // 这个方法把当前 key 移动到队尾 166 | afterNodeAccess(e); 167 | return e.value; 168 | } 169 | ``` 170 | 171 | 从上述源码中,可以看到,通过 afterNodeAccess 方法把当前访问节点移动到了队尾,其实不仅仅是 get 方法,执行 getOrDefault、compute、computeIfAbsent、computeIfPresent、merge 方法时,也会这么做,通过不断的把经常访问的节点移动到队尾,那么靠近队头的节点,自然就是很少被访问的元素了。 172 | 173 | ### 2.3 删除策略 174 | 175 | 上述 demo 我们在执行 put 方法时,发现队头元素被删除了,LinkedHashMap 本身是没有 put 方法实现的,调用的是 HashMap 的 put 方法,但 LinkedHashMap 实现了 put 方法中的调用 afterNodeInsertion 方法,这个方式实现了删除,我们看下源码: 176 | 177 | ```java 178 | // 删除很少被访问的元素,被 HashMap 的 put 方法所调用 179 | void afterNodeInsertion(boolean evict) { 180 | // 得到元素头节点 181 | LinkedHashMap.Entry first; 182 | // removeEldestEntry 来控制删除策略,如果队列不为空,并且删除策略允许删除的情况下,删除头节点 183 | if (evict && (first = head) != null && removeEldestEntry(first)) { 184 | K key = first.key; 185 | // removeNode 删除头节点 186 | removeNode(hash(key), key, null, false, true); 187 | } 188 | } 189 | ``` 190 | 191 | ## 3 小结 192 | 193 | LinkedHashMap 提供了两个很有意思的功能:按照插入顺序访问和删除最少访问元素策略,简单地通过链表的结构就实现了,设计得非常巧妙。 -------------------------------------------------------------------------------- /docs/Java源码分析/README.md: -------------------------------------------------------------------------------- 1 | # Java并发编程 2 | 3 | * [ArrayList源码分析](docs/Java源码分析/ArrayList源码分析.md) 4 | * [HashMap源码分析](docs/Java源码分析/HashMap源码分析.md) 5 | * [HashSet与TreeSet源码分析](docs/Java源码分析/HashSet与TreeSet源码分析.md) 6 | * [LinkedHashMap源码分析](docs/Java源码分析/LinkedHashMap源码分析.md) 7 | * [TreeMap源码分析](docs/Java源码分析/TreeMap源码分析.md) 8 | 9 | -------------------------------------------------------------------------------- /docs/Java源码分析/TreeMap源码分析.md: -------------------------------------------------------------------------------- 1 | # 👉 TreeMap源码分析 2 | 3 | ## 1 知识储备 4 | 5 | 在了解 TreeMap 之前,我们来看下日常工作中排序的两种方式,作为我们学习的基础储备,两种方式的代码如下: 6 | 7 | ```java 8 | public class TreeMapDemo { 9 | 10 | @Data 11 | // DTO 为我们排序的对象 12 | class DTO implements Comparable { 13 | private Integer id; 14 | public DTO(Integer id) { 15 | this.id = id; 16 | } 17 | 18 | @Override 19 | public int compareTo(DTO o) { 20 | //默认从小到大排序 21 | return id - o.getId(); 22 | } 23 | } 24 | 25 | @Test 26 | public void testTwoComparable() { 27 | // 第一种排序,从小到大排序,实现 Comparable 的 compareTo 方法进行排序 28 | List list = new ArrayList<>(); 29 | for (int i = 5; i > 0; i--) { 30 | list.add(new DTO(i)); 31 | } 32 | Collections.sort(list); 33 | log.info(JSON.toJSONString(list)); 34 | 35 | // 第二种排序,从大到小排序,利用外部排序器 Comparator 进行排序 36 | Comparator comparator = (Comparator) (o1, o2) -> o2.getId() - o1.getId(); 37 | List list2 = new ArrayList<>(); 38 | for (int i = 5; i > 0; i--) { 39 | list2.add(new DTO(i)); 40 | } 41 | Collections.sort(list,comparator); 42 | log.info(JSON.toJSONString(list2)); 43 | } 44 | } 45 | ``` 46 | 47 | 第一种排序输出的结果从小到大,结果是:[{“id”:1},{“id”:2},{“id”:3},{“id”:4},{“id”:5}]; 48 | 49 | 第二种输出的结果恰好相反,结果是:[{“id”:5},{“id”:4},{“id”:3},{“id”:2},{“id”:1}]。 50 | 51 | 以上两种就是分别通过 Comparable 和 Comparator 两者进行排序的方式,而 TreeMap 利用的也是此原理,从而实现了对 key 的排序,我们一起来看下。 52 | 53 | 54 | 55 | ## 2 TreeMap 整体架构 56 | 57 | TreeMap 底层的数据结构就是红黑树,和 HashMap 的红黑树结构一样。 58 | 59 | 不同的是,TreeMap 利用了红黑树左节点小,右节点大的性质,根据 key 进行排序,使每个元素能够插入到红黑树大小适当的位置,维护了 key 的大小关系,适用于 key 需要排序的场景。 60 | 61 | 因为底层使用的是平衡红黑树的结构,所以 containsKey、get、put、remove 等方法的时间复杂度都是 log(n)。 62 | 63 | ### 2.1 属性 64 | 65 | TreeMap 常见的属性有: 66 | 67 | ```java 68 | //比较器,如果外部有传进来 Comparator 比较器,首先用外部的 69 | //如果外部比较器为空,则使用 key 自己实现的 Comparable#compareTo 方法 70 | //比较手段和上面日常工作中的比较 demo 是一致的 71 | private final Comparator comparator; 72 | 73 | //红黑树的根节点 74 | private transient Entry root; 75 | 76 | //红黑树的已有元素大小 77 | private transient int size = 0; 78 | 79 | //树结构变化的版本号,用于迭代过程中的快速失败场景 80 | private transient int modCount = 0; 81 | 82 | //红黑树的节点 83 | static final class Entry implements Map.Entry {} 84 | ``` 85 | 86 | ### 2.2 新增节点 87 | 88 | 我们来看下 TreeMap 新增节点的步骤: 89 | 90 | 1. 判断红黑树的节点是否为空,为空的话,新增的节点直接作为根节点,代码如下: 91 | 92 | ```java 93 | Entry t = root; 94 | //红黑树根节点为空,直接新建 95 | if (t == null) { 96 | // compare 方法限制了 key 不能为 null 97 | compare(key, key); // type (and possibly null) check 98 | // 成为根节点 99 | root = new Entry<>(key, value, null); 100 | size = 1; 101 | modCount++; 102 | return null; 103 | } 104 | ``` 105 | 106 | 2. 根据红黑树左小右大的特性,进行判断,找到应该新增节点的父节点,代码如下: 107 | 108 | ```java 109 | Comparator cpr = comparator; 110 | if (cpr != null) { 111 | //自旋找到 key 应该新增的位置,就是应该挂载那个节点的头上 112 | do { 113 | //一次循环结束时,parent 就是上次比过的对象 114 | parent = t; 115 | // 通过 compare 来比较 key 的大小 116 | cmp = cpr.compare(key, t.key); 117 | //key 小于 t,把 t 左边的值赋予 t,因为红黑树左边的值比较小,循环再比 118 | if (cmp < 0) 119 | t = t.left; 120 | //key 大于 t,把 t 右边的值赋予 t,因为红黑树右边的值比较大,循环再比 121 | else if (cmp > 0) 122 | t = t.right; 123 | //如果相等的话,直接覆盖原值 124 | else 125 | return t.setValue(value); 126 | // t 为空,说明已经到叶子节点了 127 | } while (t != null); 128 | } 129 | ``` 130 | 131 | 3. 在父节点的左边或右边插入新增节点,代码如下: 132 | 133 | ```java 134 | //cmp 代表最后一次对比的大小,小于 0 ,代表 e 在上一节点的左边 135 | if (cmp < 0) 136 | parent.left = e; 137 | //cmp 代表最后一次对比的大小,大于 0 ,代表 e 在上一节点的右边,相等的情况第二步已经处理了。 138 | else 139 | parent.right = e; 140 | ``` 141 | 142 | 4. 着色旋转,达到平衡,结束。 143 | 144 | 从源码中,我们可以看到: 145 | 146 | 1. 新增节点时,就是利用了红黑树左小右大的特性,从根节点不断往下查找,直到找到节点是 null 为止,节点为 null 说明到达了叶子结点; 147 | 2. 查找过程中,发现 key 值已经存在,直接覆盖; 148 | 3. TreeMap 是禁止 key 是 null 值的。 149 | 150 | 类似的,TreeMap 查找也是类似的原理,有兴趣的同学可以去 github 上面去查看源码。 151 | 152 | ## 3 小结 153 | 154 | TreeMap 相对来说比较简单,红黑树和 HashMap 比较类似,比较关键的是通过 compare 来比较 key 的大小,然后利用红黑树左小右大的特性,为每个 key 找到自己的位置,从而维护了 key 的大小排序顺序。 -------------------------------------------------------------------------------- /docs/Java源码分析/_sidebar.md: -------------------------------------------------------------------------------- 1 | * **👉 Java源码分析** [↩](/README) 2 | * [ArrayList源码分析](docs/Java源码分析/ArrayList源码分析.md) 3 | * [HashMap源码分析](docs/Java源码分析/HashMap源码分析.md) 4 | * [HashSet与TreeSet源码分析](docs/Java源码分析/HashSet与TreeSet源码分析.md) 5 | * [LinkedHashMap源码分析](docs/Java源码分析/LinkedHashMap源码分析.md) 6 | * [TreeMap源码分析](docs/Java源码分析/TreeMap源码分析.md) -------------------------------------------------------------------------------- /docs/Java虚拟机/JVM确认可回收对象的方式.md: -------------------------------------------------------------------------------- 1 | # 👉 JVM确认可回收对象的方式 2 | 3 | ## 前言 4 | 5 | 在开始之前,我们先回顾一下`堆`是个什么玩意,大家可能都知道,我们每天创建的Java对象几乎都存放在堆上面,所以说堆是一个巨大的对象池一点都不过分,在这个对象池里面管理者数据巨大的对象实例。 6 | 7 | 在对象池中对象的引用层次,有的是很深的。比如一个调用非常频繁的接口,生产对象的速度是非常可观的。对象之间的关系,可以形容成一张网。虽然Java总是给人一种有使不完的内存的感觉,但是对象也不能一直增加不减少啊,所以就必须有**垃圾回收**这个操作。 8 | 9 | ## JVM是怎么认识`垃圾`的呢? 10 | 11 | **"垃圾回收"本文中简称 GC** 12 | 13 | 你还记得电视剧中的“诛九族""? 14 | 15 | img 16 | 17 | 比如小憨批打了皇帝老儿一巴掌,把皇帝老儿打的鼻青脸肿滴,皇帝老儿非常生气,他要下令诛小憨批的九族,以平心头只恨。 18 | 19 | 哈哈哈嗝~ 小憨批完了~ 20 | 21 | img 22 | 23 | 那么我们看看在古代这个诛九族是具体操作的呢?首先需要追溯到共同的祖先(也就是小憨批家族的大哥大),再往下逐一细数和小憨批有关系的(小憨批真坑啊)。 24 | 25 | img 26 | 27 | 28 | 29 | 其实发生在堆上的垃圾回收和这个“诛九族“的是相同思路,那么我们下面具体分析一下JVM是如何进行GC的呢? 30 | 31 | **关于JVM的GC是不受程序控制的,当满足一定条件的时候就会主动触发。** 32 | 33 | 当发生GC的时候,对于一个对象来说,JVM总能够找到引用它的祖先,当找到最后的时候,JVM发现这家伙的有些祖先已经玩完了,那么它们就会被JVM给干掉。 34 | 35 | 为什么还有没有被干掉的祖先呢?因为这些躲过GC的祖先们,它们是GC Roots ,长得比较特殊嘛。 36 | 37 | 当从GC Roots 向下追溯、搜索,就会产生一个引用链。当碰到有对象没有任何一个GC Roots 产生关系的话,这个对象就会被无情的干掉。(一根绳上的蚂蚱嘛) 38 | 39 | 来,我们画个图瞅瞅咋回事,如下图所示,Obj5、Obj6、Obj7,由于不能和 GC Root 产生关联,发生 GC 时,就会被摧毁。 40 | 41 | img 42 | 43 | 44 | 45 | 其实所谓的垃圾回收就是围绕着GC Roots 来的,但是同时,GC Roots 也存在着很多内存泄漏的根源,因为其他引用小弟压根没有这个权利。 46 | 47 | 那么什么样的对象才会是GC Roots 呢? 这个不在于它是什么样的对象,关键是它所处的位置(仔细品~)。 48 | 49 | ## GC Roots 是什么 50 | 51 | 首先,GC Roots必须是一组必须活跃的引用。简单的讲,就是程序接下来通过直接引用或间接引用,能够被访问到的潜在被使用的对象(咋感觉还是有点绕呢)。 52 | 53 | **GC Roots:** 54 | 55 | 1. Java线程中,当前所有正在被调用的方法的引用类型参数、局部变量、临时值等等。也就是与我们栈帧相关的各种引用。 56 | 2. 所有当前被加载的Java类。 57 | 3. Java类的引用类型静态变量。 58 | 4. 运行时常量池里的引用类型常量。 59 | 5. JVM内部数据结构的一些引用,比如sun.jvm.hotspot.memory.Univers类。 60 | 6. 用于同步的监控对象。比如调用了对象的wait()方法。 61 | 7. JNI handles,包括global handles 和 local handles。 62 | 63 | 以上GC Roots大致可以分为一下三大类。 64 | 65 | 1. 活动线程相关的各种引用。 66 | 2. 类的静态变量的引用。 67 | 3. JNI引用。 68 | 69 | 最后我们需要注意的是,我们这里说的是活跃的引用,而不是对象,对象是不能作为GC Roots的。 70 | 71 | 整个GC过程中是找到那些活对象,并把剩余的空间都认得为“无用”。而不是找到所有死掉的对象,并回收它们占用的空间。所有说,哪怕JVM的堆非常大,基于tracing的GC方式,回收速度也是跟快的。 72 | 73 | ## 总结 74 | 75 | GC Roots 就是可达性分析法。还有一种叫作引用计数法的方式。下面我们简单介绍一下。 76 | 77 | 引用计数法:在Java中如果要操作对象,就必须先获取该对象的引用,因此可以通过引用计数法来判断一个对象是否可以被回收。在为一个对象添加一个引用时,引用计数器就加1;为对象删除一个引用时,引用计数器就减1;如果一个对象的引用计数为0,则说明该对象没有被引用,可以回收。**优点是垃圾回收比较及时,实时性比较高,只要对象计数器为 0,则可以直接进行回收操作;而缺点是无法解决循环引用的问题。** 78 | 79 | 因为存在循环引用这个致命的硬伤,没有一个是采用引用计数法来实现 GC 的,所有你现在完全忘记引用计数这种方式了。 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /docs/Java虚拟机/Java运行时内存划分.md: -------------------------------------------------------------------------------- 1 | # 👉 JVM的内存区域 2 | 3 | ​ **线程私有区域**的生命周期与线程相同,随线程的启动而创建,随线程的启动而创建,随线程的结束而销毁。在JVM内部,每个线程都与操作系统的本地线程直接映射,因此线程私有内存区域的存在与否和本地线程的启动和销毁对应。 4 | 5 | ​ **线程共享区域**随虚拟机的启动而创建,随虚拟机的关闭而销毁。 6 | 7 | ## 程序计数器 : 线程私有,无内存溢出问题 8 | 9 | ​ 程序计数器是一块很小的内存空间,用于存储当前运行的线程所执行的字节码的行号指令,每个运行中的线程都有一个独立的程序计数器,在方法正在执行时,该方法的程序计数器记录的是实时虚拟机字节码指令的地址;如果该方法执行的是Native方法,则程序计数器的值为空。 10 | 11 | ​ 程序计数器是唯一没有内存溢出的区域。 12 | 13 | ## 虚拟机栈 : 线程私有,描述Java方法的执行过程 14 | 15 | ​ 虚拟机栈是描述Java方法的执行过程的内存模型,它在当前栈帧存储了局部变量表、操作数栈、动态链接、方法出口等信息。同时,栈帧用来存储部分运行时数据及其数据结构,处理动态链接方法的返回值和异常分派。 16 | 17 | ​ 栈帧用来记录方法的执行过程,在方法被执行时虚拟机会为其创建一个与之对应的栈帧,方法的执行和返回对应栈帧在虚拟机中的入栈和出栈。无法方法是正常运行完成还是异常完成,都可以视为方法运行结束。 18 | 19 | ## 本地方法区 : 线程私有 20 | 21 | ​ 本地方法区和虚拟机栈作用类似,区别就是本地方法栈是为Native方法服务,而虚拟机栈是为了Java方法服务。 22 | 23 | ## 堆内存 : 线程共享,运行时数据区 24 | 25 | ​ 在JVM运行过程中创建的对象和产生的数据都被存储在堆中,堆是被线程共享的内存区域,也是垃圾收集器进行垃圾回首的最主要的内存区域。 26 | 27 | ## 方法区 :线程共享 28 | 29 | ​ 方法区用于存储常量、静态变量、类信息、即时编译器编译后的机器码、运行时常量池等数据。 30 | 31 | ​ JVM把GC分代收集扩展至方法区,即使用Java堆的永久代来实现方法区,这样JVM的垃圾收集器就可以像管理Java堆一样管理这部分内存。永久带的内存回收主要针对常量池的回收和类的卸载,因此可回收的对象很少。 32 | 33 | ​ 常量被存储在运行时常量池中,是方法区的一部分。静态也属于方法区的一部分。在类信息中不但保存了类的版本/字段/方法/接口等描述信息,还保存了常量信息。 34 | 35 | ​ 在即使编译后,代码的内容将在执行阶段被保存在方法区的运行时常量池中。Java虚拟机堆Class文件每一部分的格式都明确的规定,只有符合规范的Class文件才能通过虚拟机的检查,然后被装载和执行。 36 | 37 | -------------------------------------------------------------------------------- /docs/Java虚拟机/README.md: -------------------------------------------------------------------------------- 1 | # JVM虚拟机 2 | 3 | * [Java运行时内存划分](docs/Java虚拟机/Java运行时内存划分.md) 4 | * [JVM确认可回收对象的方式](docs/Java虚拟机/JVM确认可回收对象的方式.md) 5 | * [类加载机制](docs/Java虚拟机/类加载机制.md) 6 | * [双亲委派机制](docs/Java虚拟机/双亲委派机制.md) 7 | * [四种引用类型](docs/Java虚拟机/四种引用类型.md) 8 | * [垃圾回收器](docs/Java虚拟机/垃圾回收器.md) 9 | * [垃圾回收算法](docs/Java虚拟机/Jav垃圾回收算法a运行时内存划分.md) 10 | -------------------------------------------------------------------------------- /docs/Java虚拟机/_sidebar.md: -------------------------------------------------------------------------------- 1 | * **👉 Java虚拟机** [↩](/README) 2 | * [Java运行时内存划分](docs/Java虚拟机/Java运行时内存划分.md) 3 | * [JVM确认可回收对象的方式](docs/Java虚拟机/JVM确认可回收对象的方式.md) 4 | * [类加载机制](docs/Java虚拟机/类加载机制.md) 5 | * [双亲委派机制](docs/Java虚拟机/双亲委派机制.md) 6 | * [四种引用类型](docs/Java虚拟机/四种引用类型.md) 7 | * [垃圾回收器](docs/Java虚拟机/垃圾回收器.md) 8 | * [垃圾回收算法](docs/Java虚拟机/Jav垃圾回收算法a运行时内存划分.md) 9 | -------------------------------------------------------------------------------- /docs/Java虚拟机/双亲委派机制.md: -------------------------------------------------------------------------------- 1 | # 👉 双亲委派机制 2 | 3 | ​ 双亲委派机制是指一个类在收到类加载请求之后不会尝试自己加载这个类,而是把该类加载请求向上委派给其父类,其父类在接受到该类的加载请求之后又会将其委派给自己的父类,以此类推,这样所有的类加载请求都被向上委派到启动类加载器中。 4 | 5 | ​ 但是若父类加载器在接收到类加载请求后发现自己也无法加载该类,则父类会将该信息反馈给子类并向下委派子类加载器,直到该类被成功加载,若找不到该类,则JVM会抛出ClassNotFind异常。 6 | 7 | ​ **双亲委派加载机制的类加载流程。** 8 | 9 | 1. 将自定义加载器挂载到应用程序类加载器。 10 | 2. 应用程序类加载器将类加载请求委托给扩展类加载器。 11 | 3. 扩展器加载器将类加载请求委托给启动类加载器。 12 | 4. 启动类加载器在加载路径下查找并加载Class文件,如果未找到目标Class文件,则交给应用程序类加载器加载。 13 | 5. 扩张类加载器在加载路径下查找并加载Class文件,如果未找到目标Class文件,则交由应用程序类加载器加载。 14 | 6. 应用程序类加载器在加载路径下查找并加载Class文件,如果未找到目标Class文件,则交由自定义类加载器加载。 15 | 7. 在自定义加载器下查找并加载用户指定目录下的Class文件,如果在自定义加载路径下为找到目标Class文件,则抛出ClassNotFind异常。 16 | 17 | ​ **双亲委派加载机制的核心就是保证类的唯一性和安全性。** -------------------------------------------------------------------------------- /docs/Java虚拟机/四种引用类型.md: -------------------------------------------------------------------------------- 1 | # 👉 四种引用类型 2 | Java中一切皆为对象,Java中的引用类型有四种,分别为强引用/软引用/弱引用/虚引用等。 3 | 4 | ## 强引用—trong references 5 | 6 | 当内存空间不足,系统撑不住了,JVM 就会抛出 OutOfMemoryError 错误。即使程序会异常终止,这种对象也不会被回收。这种引用属于最普通最强硬的一种存在,只有在和 GC Roots 断绝关系时,才会被消灭掉。 7 | 8 | 这种引用,你每天的编码都在用。例如:new 一个普通的对象。 9 | 10 | ```java 11 | Object obj = new Object() 12 | ``` 13 | 14 | 这种方式可能是有问题的。假如你的系统被大量用户(User)访问,你需要记录这个 User 访问的时间。可惜的是,User 对象里并没有这个字段,所以我们决定将这些信息额外开辟一个空间进行存放。 15 | 16 | ```java 17 | static Map userVisitMap = new HashMap<>(); 18 | 19 | ... 20 | 21 | userVisitMap.put(user, time); 22 | ``` 23 | 24 | 当你用完了 User 对象,其实你是期望它被回收掉的。但是,由于它被 userVisitMap 引用,我们没有其他手段 remove 掉它。这个时候,就发生了内存泄漏(memory leak)。 25 | 26 | 这种情况还通常发生在一个没有设定上限的 Cache 系统,由于设置了不正确的引用方式,加上不正确的容量,很容易造成 OOM。 27 | 28 | ## 软引用—Soft references 29 | 30 | 软引用用于维护一些可有可无的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。 31 | 32 | 可以看到,这种特性非常适合用在缓存技术上。比如网页缓存、图片缓存等。 33 | 34 | Guava 的 CacheBuilder,就提供了软引用和弱引用的设置方式。在这种场景中,软引用比强引用安全的多。 35 | 36 | 软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java 虚拟机就会把这个软引用加入到与之关联的引用队列中。 37 | 38 | 我们可以看一下它的代码。软引用需要显式的声明,使用泛型来实现。 39 | 40 | ```java 41 | // 伪代码 42 | 43 | Object object = new Object(); 44 | 45 | SoftReference softRef = new SoftReference(object); 46 | ``` 47 | 48 | 这里有一个相关的 JVM 参数。它的意思是:每 MB 堆空闲空间中 SoftReference 的存活时间。这个值的默认时间是1秒(1000)。 49 | 50 | ```java 51 | -XX:SoftRefLRUPolicyMSPerMB= 52 | ``` 53 | 54 | 这里要特别说明的是,网络上一些流传的优化方法,即把这个值设置成 0,其实是错误的,这样容易引发故障,感兴趣的话你可以自行搜索一下。 55 | 56 | 这种比较偏门的优化手段,除非在你对其原理相当了解的情况下,才能设置一些比较特殊的值。比如 0 值,无限大等,这种值在 JVM 的设置中,最好不要发生。 57 | 58 | ## 弱引用—Weak references 59 | 60 | 弱引用对象相比较软引用,要更加无用一些,它拥有更短的生命周期。 61 | 62 | 当 JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。弱引用拥有更短的生命周期,在 Java 中,用 java.lang.ref.WeakReference 类来表示。 63 | 64 | 它的应用场景和软引用类似,可以在一些对内存更加敏感的系统里采用。它的使用方式类似于这段的代码: 65 | 66 | ```java 67 | // 伪代码 68 | 69 | Object object = new Object(); 70 | 71 | WeakReference softRef = new WeakReference(object); 72 | ``` 73 | 74 | ## 虚引用—Phantom References 75 | 76 | 这是一种形同虚设的引用,在现实场景中用的不是很多。虚引用必须和引用队列(ReferenceQueue)联合使用。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。 77 | 78 | 实际上,虚引用的 get,总是返回 null。 79 | 80 | ```java 81 | Object  object = new Object(); 82 | 83 | ReferenceQueue queue = new ReferenceQueue(); 84 | 85 | // 虚引用,必须与一个引用队列关联 86 | 87 | PhantomReference pr = new PhantomReference(object, queue); 88 | ``` 89 | 90 | 虚引用主要用来跟踪对象被垃圾回收的活动。 91 | 92 | 当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象之前,把这个虚引用加入到与之关联的引用队列中。 93 | 94 | 程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 95 | 96 | 下面的方法,就是一个用于监控 GC 发生的例子。 97 | 98 | ```java 99 | private static void startMonitoring(ReferenceQueue referenceQueue, Reference ref) { 100 |      ExecutorService ex = Executors.newSingleThreadExecutor(); 101 |      ex.execute(() -> { 102 |          while (referenceQueue.poll()!=ref) { 103 |              //don't hang forever 104 |              if(finishFlag){ 105 |                  break; 106 |             } 107 |         } 108 |          System.out.println("-- ref gc'ed --"); 109 |     }); 110 |      ex.shutdown(); 111 | } 112 | ``` 113 | 114 | 基于虚引用,有一个更加优雅的实现方式,那就是 Java 9 以后新加入的 Cleaner,用来替代 Object 类的 finalizer 方法。 -------------------------------------------------------------------------------- /docs/Java虚拟机/垃圾回收器.md: -------------------------------------------------------------------------------- 1 | # 👉 垃圾回收器 2 | 3 | ![img](https://img1.sycdn.imooc.com/5e6afe7d0001656f16000566.jpg) 4 | 5 | + 新生代 6 | + Serial(单线程复制算法) 7 | + ParNew(多线程复制算法) 8 | + Parallel Scavenge(多线程复制算法) 9 | 10 | + 老年代 11 | + CMS(多线程标记清除算法) 12 | + Serial Old(单线程标记整理算法) 13 | + Parallel Old(多线程标记整理算法) 14 | 15 | G1(多线程标记整理算法) 16 | 17 | ![image.png](https://s0.lgstatic.com/i/image/M00/17/58/CgqCHl7XFo-AYbIbAABPIp2dreY362.png) -------------------------------------------------------------------------------- /docs/Java虚拟机/垃圾回收算法.md: -------------------------------------------------------------------------------- 1 | # 👉 垃圾回收算法 2 | 3 | ## 复制算法 4 | 5 | - 将可用内存按容量划分为相等的两部分,每次只使用其中的一块,当一块内存用完时,就将还存活的对象复制到第二块内存上,然后一次性清除第一块内存,再将第二块上的对象复制到第一块。 6 | - 实现方便,运行高效,不用考虑内存碎片,但是内存利用率只有一半。 7 | 8 | ## 标记清除算法 9 | 10 | - 分为**标记**和**清除**两个阶段。首先标记出所有需要回收的对象,在标记完成后统一回收被标记的对象。 11 | - 算法简单,但是有两个缺点: 12 | - 1、效率不高,标记和清除的效率都很低; 13 | - 2、空间问题,会产生大量不连续的内存碎片,导致以后程序在分配较大的对象时,由于没有充足的连续内存而提前触发一次 GC 动作。 14 | 15 | ## 标记整理算法 16 | 17 | - 标记过程仍然与 “标记 - 清除” 算法一样,但不是直接对可回收对象进行清理,而是让所有存活的对象向一端移动,然后直接清理掉边界以外的内存,形成一版连续的内存区域。 18 | - 解决标记 - 清除算法产生的大量内存碎片问题;当对象存活率较高时,也解决了复制算法的空间效率问题,不过它本身也存在时间效率方面的问题。 19 | 20 | ## 分代收集算法 21 | 22 | - 根据对象的生存周期,将堆分为新生代和老年代,然后根据各个年代的特点采用最适当的收集算法。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用**复制**算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,所以可以使用**标记 - 整理** 或者 **标记 - 清除**。 23 | - 严格地说,这并非是一种算法,而是一种思想,或者说是一种复合算法。 24 | 25 | Java 的堆内存被分代管理,为什么要分代管理呢?分代管理主要是为了方便垃圾回收,这样做基于2个事实,第一,大部分对象很快就不再使用;第二,还有一部分不会立即无用,但也不会持续很长时间。 26 | 27 | 虚拟机划分为年轻代、老年代、和永久代,如下图所示。 28 | 29 | img 30 | 31 | - 年轻代主要用来存放新创建的对象,年轻代分为 Eden 区和两个 Survivor 区。大部分对象在 Eden 区中生成。当 Eden 区满时,还存活的对象会在两个 Survivor 区交替保存,达到一定次数的对象会晋升到老年代。 32 | 33 | - 老年代用来存放从年轻代晋升而来的,存活时间较长的对象。 34 | 35 | - 永久代,主要保存类信息等内容,这里的永久代是指对象划分方式,不是专指 1.7 的 PermGen,或者 1.8 之后的 Metaspace。 36 | 37 | 38 | 根据年轻代与老年代的特点,JVM 提供了不同的垃圾回收算法。垃圾回收算法按类型可以分为引用计数法、复制法和标记清除法。 39 | 40 | - 引用计数法是通过对象被引用的次数来确定对象是否被使用,缺点是无法解决循环引用的问题。 41 | 42 | - 复制算法需要 from 和 to 两块相同大小的内存空间,对象分配时只在 from 块中进行,回收时把存活对象复制到 to 块中,并清空 from 块,然后交换两块的分工,即把 from 块作为 to 块,把 to 块作为 from 块。缺点是内存使用率较低。 43 | 44 | - 标记清除算法分为标记对象和清除不在使用的对象两个阶段,标记清除算法的缺点是会产生内存碎片。 45 | 46 | 47 | JVM 中提供的年轻代回收算法 Serial、ParNew、Parallel Scavenge 都是复制算法,而 CMS、G1、ZGC 都属于标记清除算法。 48 | 49 | ## CMS 算法 50 | 51 | 基于分代回收理论,详细介绍几个典型的垃圾回收算法,先来看 CMS 回收算法。CMS 在 JDK1.7 之前可以说是最主流的垃圾回收算法。CMS 使用标记清除算法,优点是并发收集,停顿小。 52 | 53 | CMS 算法如下图所示。 54 | 55 | img 56 | 57 | 1. 第一个阶段是初始标记,这个阶段会 stop the world,标记的对象只是从 root 集最直接可达的对象; 58 | 59 | 2. 第二个阶段是并发标记,这时 GC 线程和应用线程并发执行。主要是标记可达的对象; 60 | 61 | 3. 第三个阶段是重新标记阶段,这个阶段是第二个 stop the world 的阶段,停顿时间比并发标记要小很多,但比初始标记稍长,主要对对象进行重新扫描并标记; 62 | 63 | 4. 第四个阶段是并发清理阶段,进行并发的垃圾清理; 64 | 65 | 5. 最后一个阶段是并发重置阶段,为下一次 GC 重置相关数据结构。 66 | 67 | ## G1 算法 68 | 69 | G1 在 1.9 版本后成为 JVM 的默认垃圾回收算法,G1 的特点是保持高回收率的同时,减少停顿。 70 | 71 | G1 算法取消了堆中年轻代与老年代的物理划分,但它仍然属于分代收集器。G1 算法将堆划分为若干个区域,称作 Region,如下图中的小方格所示。一部分区域用作年轻代,一部分用作老年代,另外还有一种专门用来存储巨型对象的分区。 72 | 73 | img 74 | 75 | G1 也和 CMS 一样会遍历全部的对象,然后标记对象引用情况,在清除对象后会对区域进行复制移动整合碎片空间。 76 | 77 | 78 | 79 | G1 回收过程如下。 80 | 81 | - G1 的年轻代回收,采用复制算法,并行进行收集,收集过程会 STW。 82 | 83 | - G1 的老年代回收时也同时会对年轻代进行回收。主要分为四个阶段: 84 | 85 | - 依然是初始标记阶段完成对根对象的标记,这个过程是STW的; 86 | 87 | - 并发标记阶段,这个阶段是和用户线程并行执行的; 88 | - 最终标记阶段,完成三色标记周期; 89 | - 复制/清除阶段,这个阶段会优先对可回收空间较大的 Region 进行回收,即 garbage first,这也是 G1 名称的由来。 90 | 91 | G1 采用每次只清理一部分而不是全部的 Region 的增量式清理,由此来保证每次 GC 停顿时间不会过长。 92 | 93 | 总结如下,G1 是逻辑分代不是物理划分,需要知道回收的过程和停顿的阶段。此外还需要知道,G1 算法允许通过 JVM 参数设置 Region 的大小,范围是 1~32MB,可以设置期望的最大 GC 停顿时间等。有兴趣读者也可以对 CMS 和 G1 使用的三色标记算法做简单了解。 94 | 95 | ## 考察点 96 | 97 | 总结 JVM 相关的面试考察点如下: 98 | 99 | 1. 深入了解 JVM 的内存模型和 Java 的内存模型; 100 | 101 | 2. 要了解类的加载过程,了解双亲委派机制; 102 | 103 | 3. 要理解内存的可见性与 Java 内存模型对原子性、可见性、有序性的保证机制; 104 | 105 | 4. 要了解常用的 GC 算法的特点、执行过程,和适用场景,例如 G1 适合对最大延迟有要求的场合,ZGC 适用于 64 位系统的大内存服务中; 106 | 107 | 5. 要了解常用的 JVM 参数,明白对不同参数的调整会有怎样的影响,适用什么样的场景,例如垃圾回收的并发数、偏向锁设置等。 108 | 109 | 附录:JVM 相关的面试真题 110 | 111 | img 112 | 113 | img -------------------------------------------------------------------------------- /docs/Java虚拟机/类加载机制.md: -------------------------------------------------------------------------------- 1 | # 👉 类加载机制 2 | 3 | JVM 通过双亲委派模型进行类的加载,即当某个类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。 4 | 5 | ![GN0Do.png](https://s.im5i.com/2021/04/16/GN0Do.png) 6 | 7 | **类加载器:** 8 | 9 | 1. **启动类加载器 (Bootstrap ClassLoader)** :负责加载 JAVA_HOME\lib 目录中的,或通过 - Xbootclasspath 参数指定路径中的,且被虚拟机认可(按文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录也不会被加载)的类。启动类加载器无法被 Java 程序直接引用; 10 | 2. **扩展类加载器 (Extension ClassLoader)** :负责加载 JAVA_HOME\jre\lib\ext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库; 11 | 3. **应用程序类加载器 (Application ClassLoader)** :负责加载用户路径(classpath)上的类库。 12 | 4. 通过继承 java.lang.ClassLoader 类实现**自定义类加载器**(主要是重写 findClass 方法)。 13 | 14 | **总结:** 类加载器和字节码是Java平台无关性的基石,对于任意一个类,都需要由它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。 15 | 16 | **双亲委派模型的优点:** 17 | 18 | 1. 基础类的统一加载问题(越基础的类由越上层的加载器进行加载)。如类 java.lang.String,无论哪一个类加载器要加载这个类,最终都是委派给启动类加载器进行加载,所以在程序的各种类加载器环境中都是同一个类。 19 | 2. 提高 java 代码的安全性。比如说用户自定义了一个与系统库里同名的 java.lang.String 类,那么这个类就不会被加载,因为最顶层的类加载器会首先加载系统的 java.lang.String 类,而不会加载自定义的 String 类,防止了恶意代码的注入。 20 | 3. 可以避免类的重复加载,另外也避免了 Java 的核心 API 被篡改。 21 | 22 | 23 | 24 | # 类加载流程 25 | 26 | ![GNEkW.png](https://s.im5i.com/2021/04/16/GNEkW.png) 27 | 28 | 类的生命周期会经历以下 7 个阶段: 29 | 30 | - **加载阶段** 31 | 32 | 此阶段用于查到相应的类(通过类名进行查找)并将此类的字节流转换为方法区运行时的数据结构,然后再在内存中生成一个能代表此类的 java.lang.Class 对象,作为其他数据访问的入口。 33 | 34 | - **验证阶段** 35 | 36 | 此步骤主要是为了验证字节码的安全性,如果不做安全校验的话可能会载入非安全或有错误的字节码,从而导致系统崩溃,它是 JVM 自我保护的一项重要举措。 37 | 38 | 验证的主要动作大概有以下几个: 39 | 40 | 1. 文件格式校验包括常量池中的常量类型、Class 文件的各个部分是否被删除或被追加了其他信息等; 41 | 2. 元数据校验包括父类正确性校验(检查父类是否有被 final 修饰)、抽象类校验等; 42 | 3. 字节码校验,此步骤最为关键和复杂,主要用于校验程序中的语义是否合法且符合逻辑; 43 | 4. 符号引用校验,对类自身以外比如常量池中的各种符号引用的信息进行匹配性校验。 44 | 45 | + **准备阶段** 46 | 47 | 此阶段是用来初始化并为类中定义的静态变量分配内存的,这些静态变量会被分配到方法区上。 48 | 49 | HotSpot 虚拟机在 JDK 1.7 之前都在方法区,而 JDK 1.8 之后此变量会随着类对象一起存放到 Java 堆中。 50 | 51 | - **解析阶段** 52 | 53 | 此阶段主要是用来解析类、接口、字段及方法的,解析时会把符号引用替换成直接引用。 54 | 55 | 所谓的符号引用是指以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可;而直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。 56 | 57 | 符号引用和直接引用有一个重要的区别:使用符号引用时被引用的目标不一定已经加载到内存中;而使用直接引用时,引用的目标必定已经存在虚拟机的内存中了。 58 | 59 | - **初始化** 60 | 61 | 初始化阶段 JVM 就正式开始执行类中编写的 Java 业务代码了。到这一步骤之后,类的加载过程就算正式完成了。 62 | 63 | 如上图所示,浅绿的两个部分表示类的生命周期,就是从类的加载到类实例的创建与使用,再到类对象不再被使用时可以被 GC 卸载回收。这里要注意一点,由 Java 虚拟机自带的三种类加载器加载的类在虚拟机的整个生命周期中是不会被卸载的,只有用户自定义的类加载器所加载的类才可以被卸载。 64 | 65 | -------------------------------------------------------------------------------- /docs/MyBatis/深入剖析 MyBatis 核心原理/MyBatis 三层架构图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msJavaCoder/msJava/2553fc4f1c5b62305a685ad6a051031731ccc251/docs/MyBatis/深入剖析 MyBatis 核心原理/MyBatis 三层架构图.png -------------------------------------------------------------------------------- /docs/MyBatis/深入剖析 MyBatis 核心原理/MyBatis 执行一条 SQL 语句的核心过程.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msJavaCoder/msJava/2553fc4f1c5b62305a685ad6a051031731ccc251/docs/MyBatis/深入剖析 MyBatis 核心原理/MyBatis 执行一条 SQL 语句的核心过程.png -------------------------------------------------------------------------------- /docs/MySQL/B树与B+树详谈.md: -------------------------------------------------------------------------------- 1 | # B树与B+树详谈 2 | 3 | ## B 树 4 | 5 | > B 树的英文是 Balance Tree,也就是平衡的多路搜索树,它的高度远小于平衡二叉树的高度。在文件系统和数据库系统中的索引结构经常采用 B 树来实现。 6 | 7 | B 树的结构如下图所示: 8 | 9 | ![image-20200704163615247](C:\Users\孙白胖的爸爸\AppData\Roaming\Typora\typora-user-images\image-20200704163615247.png) 10 | 11 | B 树作为平衡的多路搜索树,它的每一个节点最多可以包括 M 个子节点,M 称为 B 树的阶。同时你能看到,每个磁盘块中包括了关键字和子节点的指针。如果一个磁盘块中包括了 x 个关键字,那么指针数就是 x+1。对于一个 100 阶的 B 树来说,如果有 3 层的话最多可以存储约 100 万的索引数据。对于大量的索引数据来说,采用 B 树的结构是非常适合的,因为树的高度要远小于二叉树的高度。 12 | 13 | 一个 M 阶的 B 树(M>2)有以下的特性: 14 | 15 | 1. 根节点的儿子数的范围是 [2,M]。 16 | 2. 每个中间节点包含 k-1 个关键字和 k 个孩子,孩子的数量 = 关键字的数量 +1,k 的取值范围为 [ceil(M/2), M]。 17 | 3. 叶子节点包括 k-1 个关键字(叶子节点没有孩子),k 的取值范围为 [ceil(M/2), M]。 18 | 4. 假设中间节点节点的关键字为:Key[1], Key[2], …, Key[k-1],且关键字按照升序排序,即 Key[i] 事务是逻辑上的一组操作,要么都执行,要么都不执行。 13 | 14 | 1. 原子性 :事务是最小的执行单元,不允许分割。 15 | 2. 一致性: 执行事务前后,数据保持一致。多个事务对同一个数据读取的结果是相同的。 16 | 3. 隔离性: 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的。 17 | 4. 持久性: 一个事务被提交之后。它对数据库中的数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。 18 | 19 | ## 并发事务导致的问题 20 | 21 | 1. 脏读 22 | 2. 丢失修改 23 | 3. 不可重复读 24 | 4. 幻读 25 | 26 | ## 事务隔离级别 27 | 28 | 1. 读未提交 29 | 2. 读已提交 30 | 3. 可重复读 31 | 4. 可串行化 32 | 33 | ## 数据库三范式 34 | 35 | 1. 第一范式 :如何每列都是不可再分割的最小数据单元,则满足第一范式,第一范式的目标是确保每列的原子性。 36 | 2. 第二范式: 满足第一范式的基础上,规定表中的非主键列不存在对主键的部分依赖,也就是说一张表只描述一件事。 37 | 3. 第三范式: 满足第一范式和第二范式的基础上,并且表中的列不存在对非主键列的传递依赖。 38 | 39 | -------------------------------------------------------------------------------- /docs/MySQL/MySQL实战宝典/08 索引:排序的艺术.md: -------------------------------------------------------------------------------- 1 | # 08 | 索引:排序的艺术 2 | 3 | ## 前言 4 | 5 | 索引是关系型数据库中最核心的概念之一,只有正确设计索引,业务才能达到上线的初步标准。本文我们一起探索——索引,目的是认识索引、用好索引。 6 | 7 | ## 索引是什么? 8 | 9 | 索引是提升查询速度的一种数据结构。 10 | 11 | > 思考:索引为什么会提升查询速度呢? 12 | 13 | 索引之所以能够提升查询速度,那是因为在插入时候对数据进行了排序操作,当然什么事情都要两面性,这个排序操作虽然提升了查询速度,但是会导致插入或者更新的效率下降。 14 | 15 | 所以说索引是一门排序的艺术,只有用好索引,才能提升整个数据库系统的性能。在MySQL 8.0版本中,InnoDB 存储引擎支持的索引有 B+ 树索引、全文索引、R 树索引。当然,使用最为广泛的是B+树索引了。 16 | 17 | ### B+树索引结构 18 | 19 | B+ 树索引是数据库系统中最为常见的一种索引数据结构,几乎所有的关系型数据库都支持它。 20 | 21 | > 思考:为什么呢?它有什么“魔力"? 22 | 23 | 因为它是**目前为止**排序最有效率的数据结构。像二叉树,哈希索引、红黑树、SkipList,在海量数据基于磁盘存储效率方面远不如 B+ 树索引高效。 24 | 25 | 所以,上述的数据结构一般仅用于内存对象,基于磁盘的数据排序与存储,最有效的依然是 B+ 树索引了。 -------------------------------------------------------------------------------- /docs/MySQL/README.md: -------------------------------------------------------------------------------- 1 | # MySQL 2 | 3 | * [MySQL基础概念](docs/MySQL/MySQL基础概念.md) 4 | * [常见SQL优化方式](docs/MySQL/常见SQL优化方式.md) 5 | * [浅谈MySQL的优化方案](docs/MySQL/浅谈MySQL的优化方案.md) 6 | * [如何使用索引](docs/MySQL/如何使用索引.md) 7 | * [如何使用EXPLAIN查看执行计划](docs/MySQL/如何使用EXPLAIN查看执行计划.md) 8 | * [什么时候不需要创建索引](docs/MySQL/什么时候不需要创建索引.md) 9 | * [什么情况下索引失效](docs/MySQL/什么情况下索引失效.md) 10 | * [Hash索引与B+树索引的区别](docs/MySQL/Hash索引与B+树索引的区别.md) 11 | * [B树与B+树详谈](docs/MySQL/树详谈.md) 12 | * [SQL经典笔试题目](docs/MySQL/SQL经典笔试题目.md) 13 | * [SQL进阶](docs/MySQL/SQL进阶.md) 14 | -------------------------------------------------------------------------------- /docs/MySQL/SQL经典笔试题目.md: -------------------------------------------------------------------------------- 1 | # SQL经典练习题目 2 | 3 | ## 1. 创建表结构及测试数据 4 | 5 | ### **学生表(student)** 6 | 7 | ```sql 8 | CREATE TABLE `student` ( 9 | `sid` varchar(10) DEFAULT NULL, 10 | `sname` varchar(10) DEFAULT NULL, 11 | `sage` datetime DEFAULT NULL, 12 | `ssex` varchar(10) DEFAULT NULL 13 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 14 | ``` 15 | 16 | ### **教师表(teacher)** 17 | 18 | ```sql 19 | CREATE TABLE `teacher` ( 20 | `tid` varchar(10) DEFAULT NULL, 21 | `tname` varchar(10) DEFAULT NULL 22 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 23 | ``` 24 | 25 | ### **成绩表(sc)** 26 | 27 | ```sql 28 | CREATE TABLE `sc` ( 29 | `sid` varchar(10) DEFAULT NULL, 30 | `cid` varchar(10) DEFAULT NULL, 31 | `score` decimal(18,1) DEFAULT NULL 32 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 33 | ``` 34 | 35 | ### **课程表(course)** 36 | 37 | ```sql 38 | CREATE TABLE `course` ( 39 | `cid` varchar(10) DEFAULT NULL, 40 | `cname` varchar(10) DEFAULT NULL, 41 | `tid` varchar(10) DEFAULT NULL 42 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 43 | ``` 44 | 45 | ### 创建测试数据 46 | 47 | ```sql 48 | # 学生表数据 49 | insert into Student values('01' , '赵雷' , '1990-01-01' , '男'); 50 | insert into Student values('02' , '钱电' , '1990-12-21' , '男'); 51 | insert into Student values('03' , '孙风' , '1990-05-20' , '男'); 52 | insert into Student values('04' , '李云' , '1990-08-06' , '男'); 53 | insert into Student values('05' , '周梅' , '1991-12-01' , '女'); 54 | insert into Student values('06' , '吴兰' , '1992-03-01' , '女'); 55 | insert into Student values('07' , '郑竹' , '1989-07-01' , '女'); 56 | insert into Student values('08' , '王菊' , '1990-01-20' , '女'); 57 | 58 | # 课程表数据 59 | insert into Course values('01' , '语文' , '02'); 60 | insert into Course values('02' , '数学' , '01'); 61 | insert into Course values('03' , '英语' , '03'); 62 | 63 | 64 | # 教师表数据 65 | insert into Teacher values('01' , '张三'); 66 | insert into Teacher values('02' , '李四'); 67 | insert into Teacher values('03' , '王五'); 68 | 69 | 70 | # 成绩表数据 71 | insert into SC values('01' , '01' , 80); 72 | insert into SC values('01' , '02' , 90); 73 | insert into SC values('01' , '03' , 99); 74 | insert into SC values('02' , '01' , 70); 75 | insert into SC values('02' , '02' , 60); 76 | insert into SC values('02' , '03' , 80); 77 | insert into SC values('03' , '01' , 80); 78 | insert into SC values('03' , '02' , 80); 79 | insert into SC values('03' , '03' , 80); 80 | insert into SC values('04' , '01' , 50); 81 | insert into SC values('04' , '02' , 30); 82 | insert into SC values('04' , '03' , 20); 83 | insert into SC values('05' , '01' , 76); 84 | insert into SC values('05' , '02' , 87); 85 | insert into SC values('06' , '01' , 31); 86 | insert into SC values('06' , '03' , 34); 87 | insert into SC values('07' , '02' , 89); 88 | insert into SC values('07' , '03' , 98); 89 | ``` 90 | 91 | ## 2.SQL题目(1~10) 92 | 93 | ### 2.1 第一题 94 | 95 | > 查询“01”课程比“02”课程成绩高的所有学生的学号; 96 | 97 | ```sql 98 | select t1.sid 99 | from 100 | (select sid,score from sc where cid='01') as t1 101 | join 102 | (select sid,score from sc where cid='02') as t2 103 | on t1.sid=t2.sid and t1.score > t2.score 104 | ``` 105 | 106 | ### 2.2 第二题 107 | 108 | > 查询平均成绩大于60分的同学的学号和平均成绩; 109 | 110 | ```sql 111 | select sid,avg(score) as avgscore 112 | from sc 113 | group by sid 114 | having avgscore > 60 115 | ``` 116 | 117 | ### 2.3 第三题 118 | 119 | >查询所有同学的学号、姓名、选课数、总成绩; 120 | 121 | ```sql 122 | select t1.sid , t1.sname , t2.cnt , t2.total_score 123 | from 124 | ( select sid,sname from student group by sid) as t1 125 | join 126 | (select sid,count(*) as cnt , sum(score) as total_score from sc group by sid ) as t2 127 | on t1.sid=t2.sid 128 | ``` 129 | 130 | ### 2.4 第四题 131 | 132 | > 查询姓“李”的老师的个数; 133 | 134 | ```sql 135 | select count(*) as count 136 | from teacher 137 | where tname like "李%" 138 | ``` 139 | 140 | ### 2.5 第五题 141 | 142 | > 查询没学过“张三”老师课的同学的学号、姓名; 143 | 144 | ```sql 145 | select distinct sid,sname 146 | from student 147 | where sid not in ( 148 | select sid 149 | from sc s 150 | left join course as c on s.cid =c.cid 151 | left join teacher as t on c.tid =t.tid 152 | where tname = '张三' 153 | ) 154 | ``` 155 | 156 | ### 2.6 第六题 157 | 158 | > 查询学过编号“01”并且也学过编号“02”课程的同学的学号、姓名;**(类比题目1)** 159 | 160 | ```sql 161 | select sid ,sname 162 | from student 163 | where sid in 164 | ( 165 | select distinct t1.sid 166 | from 167 | ( select sid,score from sc where cid ='01') as t1 168 | join 169 | ( select sid,score from sc where cid ='02') as t2 170 | on t1.sid=t2.sid 171 | ) 172 | ``` 173 | 174 | ### 2.7 第七题 175 | 176 | > 查询学过“张三”老师所教的课的同学的学号、姓名;**(类比题目5)** 177 | 178 | ```sql 179 | select distinct sid,sname 180 | from student 181 | where sid in ( 182 | select sid 183 | from sc s 184 | left join course as c on s.cid =c.cid 185 | left join teacher as t on c.tid =t.tid 186 | where tname = '张三' 187 | ) 188 | ``` 189 | 190 | ### 2.8 第八题 191 | 192 | > 查询课程编号“01”的成绩比课程编号“02”课程低的所有同学的学号、姓名;**(类比题目1和题目6)** 193 | 194 | ```sql 195 | select sid ,sname 196 | from student 197 | where sid in 198 | ( 199 | select distinct t1.sid 200 | from 201 | ( select sid,score from sc where cid ='01') as t1 202 | join 203 | ( select sid,score from sc where cid ='02') as t2 204 | on t1.sid=t2.sid and t1.score < t2.score 205 | ) 206 | ``` 207 | 208 | ### 2.9 第九题 209 | 210 | > 查询**所有课程**成绩小于60分的同学的学号、姓名; 211 | 212 | ```sql 213 | select distinct sid ,sname 214 | from student 215 | where sid in ( 216 | select sid from sc where score<60 217 | ) 218 | ``` 219 | 220 | ### 2.10 第十题 221 | 222 | > 查询没有学全所有课的同学的学号、姓名; 223 | 224 | ```sql 225 | select s.sid ,s.sname 226 | from student as s 227 | left join sc 228 | on sc.sid = s.sid 229 | group by s.sid 230 | having count(sc.cid ) < 3 231 | ``` 232 | 233 | ## 3.SQL题目(11~20) 234 | 235 | ### 3.1 第十一道 236 | 237 | > 查询至少有一门课与学号为“01”的同学所学相同的同学的学号和姓名; 238 | 239 | ```sql 240 | select distinct sc.sid,sname 241 | from sc 242 | join student as s 243 | on sc.sid=s.sid 244 | where cid in 245 | (select distinct cid from sc where sid='01') 246 | ``` 247 | 248 | ### 3.2 第十二道 249 | 250 | > 查询和"01"号的同学学习的课程完全相同的其他同学的学号和姓名; 251 | 252 | ```sql 253 | 254 | ``` 255 | 256 | 257 | 258 | ### 3.3 第十三道 259 | 260 | ### 3.4 第十四道 261 | 262 | ### 3.5 第十五道 263 | 264 | ### 3.6 第十六道 265 | 266 | ### 3.7 第十七道 267 | 268 | ### 3.8 第十八道 269 | 270 | ### 3.9 第十九道 271 | 272 | ### 3.10 第二十道 273 | 274 | 275 | 276 | -------------------------------------------------------------------------------- /docs/MySQL/SQL进阶.md: -------------------------------------------------------------------------------- 1 | # SQL 2 | ## 1.内连接 3 | >内连接(Inner Join),是将一张表的每一条记录与另一张表的每一行记录进行比较,得到两张表匹配的记录集合。 4 | 5 | ![图片描述](http://img.mukewang.com/wiki/5e7303980941eebd02200149.jpg) 6 | 7 | ```sql 8 | SELECT * FROM imooc_user INNER JOIN imooc_class ON imooc_user.class_id = imooc_class.id; 9 | ``` 10 | 11 | ## 2. 外连接 12 | 13 | > 外连接有些许不同,它并不要求两张表中的记录都能够匹配,即使没有匹配到也会保留数据,被保留全部数据的表被称为保留表。 14 | 15 | > 外连接可以根据保留表来进一步分为:`左外连接`(左边的表数据会被保留),`右外连接`(右边的表数据会被保留)和`全连接`(两边的表均被保留)。 16 | 17 | > 外连接没有隐式的连接方式,必须通过 Join 与 On 显式的指定连接方式和连接条件。 18 | 19 | - ## 左外连接 20 | 21 | > 左外连接(Left Outer Join),简称左连接(Left Join);若 A 和 B 两表进行左外连接,会在结果中包含左表(即表 A)的所有记录,即使那些记录在右表B 没有符合连接条件相应的匹配记录,未匹配的记录会给予 NULL 填充。 22 | 23 | ![图片描述](http://img.mukewang.com/wiki/5e730461092cd30002200149.jpg) 24 | 25 | ```sql 26 | SELECT username,class_name FROM imooc_user LEFT OUTER JOIN imooc_class ON imooc_user.class_id = imooc_class.id; 27 | ``` 28 | 29 | - ## 右外连接 30 | 31 | > 右外连接(Right Outer Join),简称右连接(Right Join);若 A 和 B 两表进行右外连接,会在结果中包含右表(即表 B)的所有记录,即使那些记录在左表 A 中没有符合连接条件相应的匹配记录,未匹配的记录会给予 NULL 填充。 32 | 33 | ![图片描述](http://img.mukewang.com/wiki/5e7304930944f95402200149.jpg) 34 | 35 | ```sql 36 | SELECT class_name,username FROM imooc_user RIGHT OUTER JOIN imooc_class ON imooc_class.id = imooc_user.class_id; 37 | ``` 38 | 39 | ## 3.Group By & Having 40 | 41 | > `Group By` 用于数据分组,一般与聚合函数一起使用,对分组后的数据进行聚合操作。由于 Where 无法与聚合函数一起搭配使用,因此 SQL 增加`Having` 指令。 42 | 43 | Group By 提供了分组功能对数据分门别类,Having 可以与聚合函数搭配用于筛选数据。 44 | 45 | ```sql 46 | SELECT age, COUNT(*) FROM imooc_user GROUP BY age; 47 | ``` 48 | 49 | > `COUNT(*)`在数据库层面有专门的优化,其性能跟`COUNT(1)`大致相同. 50 | 51 | Having 不能单独出现,须于聚合函数搭配使用,且常与 Group By 一起出现。Having 本身并无其他含义,它的主要功能是替代 Where。 52 | 53 | ```sql 54 | SELECT age, COUNT(*) FROM imooc_user GROUP BY age HAVING COUNT(*) > 1; 55 | ``` 56 | 57 | ## 4. 子查询 58 | 59 | 子查询虽然很灵活,但是也有一定的限制,它必须满足以下几个规则: 60 | 61 | 1. 子查询必须在括号()内。 62 | 2. 子查询中不能使用 Order By,主查询可以使用。 63 | 3. 子查询不能使用在聚合函数中。 64 | 4. Between 指令不能与子查询一起使用,但可使用在子查询内部。 65 | 5. 子查询若返回一条记录,则只能使用单值运算符,如 > ,若返回多条记录需使用多值运算符,如 In。 66 | 6. 若子查询返回多条记录,且使用 ANY 或 ALL 特殊语法,则可使用单值比较符,我们将在下小节介绍。 67 | 68 | -------------------------------------------------------------------------------- /docs/MySQL/_sidebar.md: -------------------------------------------------------------------------------- 1 | * **👉 MySQL** [↩](/README) 2 | * [MySQL基础概念](docs/MySQL/MySQL基础概念.md) 3 | * [常见SQL优化方式](docs/MySQL/常见SQL优化方式.md) 4 | * [浅谈MySQL的优化方案](docs/MySQL/浅谈MySQL的优化方案.md) 5 | * [如何使用索引](docs/MySQL/如何使用索引.md) 6 | * [如何使用EXPLAIN查看执行计划](docs/MySQL/如何使用EXPLAIN查看执行计划.md) 7 | * [什么时候不需要创建索引](docs/MySQL/什么时候不需要创建索引.md) 8 | * [什么情况下索引失效](docs/MySQL/什么情况下索引失效.md) 9 | * [Hash索引与B+树索引的区别](docs/MySQL/Hash索引与B+树索引的区别.md) 10 | * [B树与B+树详谈](docs/MySQL/B树与B+树详谈.md) 11 | * [SQL经典笔试题目](docs/MySQL/SQL经典笔试题目.md) 12 | * [SQL进阶](docs/MySQL/SQL进阶.md) 13 | -------------------------------------------------------------------------------- /docs/MySQL/什么情况下索引失效.md: -------------------------------------------------------------------------------- 1 | # 什么情况下索引失效 2 | 3 | **1. 如果索引进行了表达式计算,则会失效** 4 | 5 | 我们可以使用 EXPLAIN 关键字来查看 MySQL 中一条 SQL 语句的执行计划,比如: 6 | 7 | ```sql 8 | EXPLAIN SELECT comment_id, user_id, comment_text FROM product_comment WHERE comment_id+1 = 900001 9 | ``` 10 | 11 | 运行结果: 12 | 13 | ```sql 14 | +----+-------------+-----------------+------------+------+---------------+------+---------+------+--------+----------+-------------+ 15 | | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | 16 | +----+-------------+-----------------+------------+------+---------------+------+---------+------+--------+----------+-------------+ 17 | | 1 | SIMPLE | product_comment | NULL | ALL | NULL | NULL | NULL | NULL | 996663 | 100.00 | Using where | 18 | +----+-------------+-----------------+------------+------+---------------+------+---------+------+--------+----------+-------------+ 19 | ``` 20 | 21 | 你能看到如果对索引进行了表达式计算,索引就失效了。这是因为我们需要把索引字段的取值都取出来,然后依次进行表达式的计算来进行条件判断,因此采用的就是全表扫描的方式,运行时间也会慢很多,最终运行时间为 2.538 秒。 22 | 23 | 为了避免索引失效,我们对 SQL 进行重写: 24 | 25 | ```sql 26 | SELECT comment_id, user_id, comment_text FROM product_comment WHERE comment_id = 900000 27 | 复制代码 28 | ``` 29 | 30 | 运行时间为 0.039 秒。 31 | 32 | **2. 如果对索引使用函数,也会造成失效** 33 | 34 | 比如我们想要对 comment_text 的前三位为 abc 的内容进行条件筛选,这里我们来查看下执行计划: 35 | 36 | ```sql 37 | EXPLAIN SELECT comment_id, user_id, comment_text FROM product_comment WHERE SUBSTRING(comment_text, 1,3)='abc' 38 | ``` 39 | 40 | 运行结果: 41 | 42 | ```sql 43 | +----+-------------+-----------------+------------+------+---------------+------+---------+------+--------+----------+-------------+ 44 | | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | 45 | +----+-------------+-----------------+------------+------+---------------+------+---------+------+--------+----------+-------------+ 46 | | 1 | SIMPLE | product_comment | NULL | ALL | NULL | NULL | NULL | NULL | 996663 | 100.00 | Using where | 47 | +----+-------------+-----------------+------------+------+---------------+------+---------+------+--------+----------+-------------+ 48 | ``` 49 | 50 | 你能看到对索引字段进行函数操作,造成了索引失效,这时可以进行查询重写: 51 | 52 | ```sql 53 | SELECT comment_id, user_id, comment_text FROM product_comment WHERE comment_text LIKE 'abc%' 54 | ``` 55 | 56 | 使用 EXPLAIN 对查询语句进行分析: 57 | 58 | ```sql 59 | +----+-------------+-----------------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+ 60 | | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | 61 | +----+-------------+-----------------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+ 62 | | 1 | SIMPLE | product_comment | NULL | range | comment_text | comment_text | 767 | NULL | 213 | 100.00 | Using index condition | 63 | +----+-------------+-----------------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+ 64 | ``` 65 | 66 | 你能看到经过查询重写后,可以使用索引进行范围检索,从而提升查询效率。 67 | 68 | **3. 在 WHERE 子句中,如果在 OR 前的条件列进行了索引,而在 OR 后的条件列没有进行索引,那么索引会失效。** 69 | 70 | 比如下面的 SQL 语句,comment_id 是主键,而 comment_text 没有进行索引,因为 OR 的含义就是两个只要满足一个即可,因此只有一个条件列进行了索引是没有意义的,只要有条件列没有进行索引,就会进行全表扫描,因此索引的条件列也会失效: 71 | 72 | ```sql 73 | EXPLAIN SELECT comment_id, user_id, comment_text FROM product_comment WHERE comment_id = 900001 OR comment_text = '462eed7ac6e791292a79' 74 | ``` 75 | 76 | 运行结果: 77 | 78 | ```sql 79 | +----+-------------+-----------------+------------+------+---------------+------+---------+------+--------+----------+-------------+ 80 | | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | 81 | +----+-------------+-----------------+------------+------+---------------+------+---------+------+--------+----------+-------------+ 82 | | 1 | SIMPLE | product_comment | NULL | ALL | PRIMARY | NULL | NULL | NULL | 996663 | 10.00 | Using where | 83 | +----+-------------+-----------------+------------+------+---------------+------+---------+------+--------+----------+-------------+ 84 | ``` 85 | 86 | 如果我们把 comment_text 创建了索引会是怎样的呢? 87 | 88 | ```sql 89 | +----+-------------+-----------------+------------+-------------+----------------------+----------------------+---------+------+------+----------+------------------------------------------------+ 90 | | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | 91 | +----+-------------+-----------------+------------+-------------+----------------------+----------------------+---------+------+------+----------+------------------------------------------------+ 92 | | 1 | SIMPLE | product_comment | NULL | index_merge | PRIMARY,comment_text | PRIMARY,comment_text | 4,767 | NULL | 2 | 100.00 | Using union(PRIMARY,comment_text); Using where | 93 | +----+-------------+-----------------+------------+-------------+----------------------+----------------------+---------+------+------+----------+------------------------------------------------+ 94 | ``` 95 | 96 | 你能看到这里使用到了 index merge,简单来说 index merge 就是对 comment_id 和 comment_text 分别进行了扫描,然后将这两个结果集进行了合并。这样做的好处就是避免了全表扫描。 97 | 98 | **4. 当我们使用 LIKE 进行模糊查询的时候,后面不能是 %** 99 | 100 | ```sql 101 | EXPLAIN SELECT comment_id, user_id, comment_text FROM product_comment WHERE comment_text LIKE '%abc' 102 | ``` 103 | 104 | 运行结果: 105 | 106 | ```sql 107 | +----+-------------+-----------------+------------+------+---------------+------+---------+------+--------+----------+-------------+ 108 | | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | 109 | +----+-------------+-----------------+------------+------+---------------+------+---------+------+--------+----------+-------------+ 110 | | 1 | SIMPLE | product_comment | NULL | ALL | NULL | NULL | NULL | NULL | 996663 | 11.11 | Using where | 111 | +----+-------------+-----------------+------------+------+---------------+------+---------+------+--------+----------+-------------+ 112 | ``` 113 | 114 | 这个很好理解,如果一本字典按照字母顺序进行排序,我们会从首位开始进行匹配,而不会对中间位置进行匹配,否则索引就失效了。 115 | 116 | **5. 索引列与 NULL 或者 NOT NULL 进行判断的时候也会失效。** 117 | 118 | 这是因为索引并不存储空值,所以最好在设计数据表的时候就将字段设置为 NOT NULL 约束,比如你可以将 INT 类型的字段,默认值设置为 0。将字符类型的默认值设置为空字符串 (’’)。 119 | 120 | **6. 我们在使用联合索引的时候要注意最左原则** 121 | 122 | 最左原则也就是需要从左到右的使用索引中的字段,一条 SQL 语句可以只使用联合索引的一部分,但是需要从最左侧开始,否则就会失效。我在讲联合索引的时候举过索引失效的例子。 -------------------------------------------------------------------------------- /docs/MySQL/什么时候不需要创建索引.md: -------------------------------------------------------------------------------- 1 | # 什么时候不需要创建索引 2 | 3 | 1. WHERE 条件(包括 GROUP BY、ORDER BY)里用不到的字段不需要创建索引,索引的价值是快速定位,如果起不到定位的字段通常是不需要创建索引的。 4 | 2. 如果表记录太少,比如少于 1000 个,那么是不需要创建索引的。 5 | 3. 字段中如果有大量重复数据,也不用创建索引,比如性别字段。 6 | 4. 频繁更新的字段不一定要创建索引。因为更新数据的时候,也需要更新索引,如果索引太多,在更新索引的时候也会造成负担,从而影响效率。 7 | 8 | -------------------------------------------------------------------------------- /docs/MySQL/如何使用EXPLAIN查看执行计划.md: -------------------------------------------------------------------------------- 1 | ## 如何使用 EXPLAIN 查看执行计划 2 | 3 | ```sql 4 | explain select * from tb_blog 5 | ``` 6 | 7 | 查看结果如下: 8 | 9 | | id | select_type | table | type | possible_keys | key | ken_len | ref | rows | Extra | 10 | | :--: | :---------: | :-----: | ---- | :-----------: | :--: | :-----: | :--: | :--: | :---: | 11 | | 1 | SIMPLE | tb_blog | ALL | Null | Null | Null | Null | 3 | Null | 12 | 13 | 数据表的访问类型所对应的 type 列是我们比较关注的信息。type 可能有以下几种情况: 14 | 15 | | type | 说明 | 16 | | :---------: | :----------------------------------------------------------: | 17 | | all | 全数据表扫描 | 18 | | index | 全索引表扫描 | 19 | | range | 对索引列进行范围查找 | 20 | | index_merge | 合并索引,使用多个单列索引搜索 | 21 | | ref | 根据索引查找找一个或多个值 | 22 | | eq_ref | 搜索时使用primary key或unique类型,常用于多表联查 | 23 | | const | 常量,表最多有一个匹配行,因为只有一行,在这行的列值可被优化器认为是常数 | 24 | | | 系统,表只有一行,是const连接类型的特例 | 25 | 26 | 在这些情况里,all 是最坏的情况,因为采用了全表扫描的方式。index 和 all 差不多,只不过 index 对索引表进行全扫描,这样做的好处是不再需要对数据进行排序,但是开销依然很大。如果我们在 extra 列中看到 Using index,说明采用了索引覆盖,也就是索引可以覆盖所需的 SELECT 字段,就不需要进行回表,这样就减少了数据查找的开销。 27 | 28 | range 表示采用了索引范围扫描,这里不进行举例,从这一级别开始,索引的作用会越来越明显,因此我们需要尽量让 SQL 查询可以使用到 range 这一级别及以上的 type 访问方式。 29 | 30 | index_merge 说明查询同时使用了两个或以上的索引,最后取了交集或者并集。 31 | 32 | S -------------------------------------------------------------------------------- /docs/MySQL/如何使用索引.md: -------------------------------------------------------------------------------- 1 | ## 创建索引有哪些规律? 2 | 3 | **1. 字段的数值有唯一性的限制,比如用户名** 4 | 5 | 索引本身可以起到约束的作用,比如唯一索引、主键索引都是可以起到唯一性约束的,因此在我们的数据表中,如果某个字段是唯一性的,就可以直接创建唯一性索引,或者主键索引。 6 | 7 | **2. 频繁作为 WHERE 查询条件的字段,尤其在数据表大的情况下** 8 | 9 | 在数据量大的情况下,某个字段在 SQL 查询的 WHERE 条件中经常被使用到,那么就需要给这个字段创建索引了。创建普通索引就可以大幅提升数据查询的效率。 10 | 11 | **3. 需要经常 GROUP BY 和 ORDER BY 的列** 12 | 13 | 索引就是让数据按照某种顺序进行存储或检索,因此当我们使用 GROUP BY 对数据进行分组查询,或者使用 ORDER BY 对数据进行排序的时候,就需要对分组或者排序的字段进行索引。 14 | 15 | **4.UPDATE、DELETE 的 WHERE 条件列,一般也需要创建索引** 16 | 17 | 我们刚才说的是数据检索的情况。那么当我们对某条数据进行 UPDATE 或者 DELETE 操作的时候,是否也需要对 WHERE 的条件列创建索引呢? 18 | 19 | 对数据按照某个条件进行查询后再进行 UPDATE 或 DELETE 的操作,如果对 WHERE 字段创建了索引,就能大幅提升效率。原理是因为我们需要先根据 WHERE 条件列检索出来这条记录,然后再对它进行更新或删除。如果进行更新的时候,更新的字段是非索引字段,提升的效率会更明显,这是因为非索引字段更新不需要对索引进行维护。 20 | 21 | 不过在实际工作中,我们也需要注意平衡,如果索引太多了,在更新数据的时候,如果涉及到索引更新,就会造成负担。 22 | 23 | **5.DISTINCT 字段需要创建索引** 24 | 25 | 有时候我们需要对某个字段进行去重,使用 DISTINCT,那么对这个字段创建索引,也会提升查询效率。 26 | 27 | **6. 做多表 JOIN 连接操作时,创建索引需要注意以下的原则** 28 | 29 | 首先,连接表的数量尽量不要超过 3 张,因为每增加一张表就相当于增加了一次嵌套的循环,数量级增长会非常快,严重影响查询的效率。 30 | 31 | 其次,对 WHERE 条件创建索引,因为 WHERE 才是对数据条件的过滤。如果在数据量非常大的情况下,没有 WHERE 条件过滤是非常可怕的。 32 | 33 | 最后,对用于连接的字段创建索引,并且该字段在多张表中的类型必须一致。比如 user_id 在 product_comment 表和 user 表中都为 int(11) 类型,而不能一个为 int 另一个为 varchar 类型。 34 | 35 | -------------------------------------------------------------------------------- /docs/MySQL/常见SQL优化方式.md: -------------------------------------------------------------------------------- 1 | # 常见SQL优化方式 2 | 3 | ## 1.创建索引 4 | 5 | - [ ] 要尽量避免全表扫描,首先应考虑where及order by 涉及的列上建立索引。 6 | - [ ] 在经常需要进行检索的字段上创建索引。 7 | - [ ] 一个表的索引数最好不要超过六个。索引虽然提高了select的效率,但是也降低了insert及update的效率。 8 | 9 | ## 2. 避免在索引上使用计算 10 | 11 | + 在where字句中,如果索引列是计算或者函数的一部分,DBMS的优化器将不会使用索引而使用全表查询,函数 12 | 属于计算的一种,同时在in和exists中通常情况下使用EXISTS,因为in不走索引。 13 | 效率低: 14 | 15 | ```sql 16 | select * from user where salary*22>11000 (salary是索引列) 17 | ``` 18 | 19 | 20 | ​ 效率高: 21 | 22 | ```sql 23 | select * from user where salary>11000/22 (salary是索引列) 24 | ``` 25 | 26 | ## 3. 使用预编译查询 27 | 28 | + 程序中通常是根据用户的输入来动态执行SQL,这时应该尽量使用参数化SQL,这样不仅可以避免SQL注入漏洞 29 | 攻击,最重要数据库会对这些参数化SQL进行预编译,这样第一次执行的时候DBMS会为这个SQL语句进行查询优化 30 | 并且执行预编译,这样以后再执行这个SQL的时候就直接使用预编译的结果,这样可以大大提高执行的速度。 31 | 32 | ## 4.调整Where字句中的连接顺序 33 | 34 | + DBMS一般采用自下而上的顺序解析where字句,根据这个原理表连接最好写在其他where条件之前,那些可以 35 | 过滤掉最大数量记录。 36 | 37 | ## 5. 尽量将多条SQL语句压缩到一句SQL中 38 | 39 | + 每次执行SQL的时候都要建立网络连接、进行权限校验、进行SQL语句的查询优化、发送执行结果,这个过程是非常耗时的,因此应该避免过多的执行SQL语句,能够压缩到一句SQL执行的语句就不要多条来执行。 40 | 41 | ## 6.用where字句替换HAVING字句 42 | 43 | + 避免使用HAVING字句,因为HAVING只会在检索出所有记录之后才对结果集进行过滤,而where则是在聚合前刷选记录,如果能通过where字句限制记录的数目,那就能减少这方面的开销。HAVING中的条件一般用于聚合函数的过滤,除此之外,应该将条件写where字句中。 44 | 45 | ## 7.查询select语句优化 46 | 47 | + 任何地方都不要使用select * from t, 要具体的字段列表代替“*“ ,不要返回用不到的字段。 48 | + 应尽量避免在where子句中字段进行null值判断,否则将导致引擎放弃使用索引而进行全表扫描。 49 | 50 | ## 8. 更新update语句优化 51 | 52 | + 如果只更新一两个字段,就不要更新全部字段,否则会导致频繁调用引起明细的性能消耗,同时带来大量日志。 53 | 54 | ## 9. 插入Insert语句优化 55 | 56 | + 在新建临时表时,如果一次性插入数据量很大,那么可以使用 select into 代替 create table,避免造成大量 log ,以提高速度;如果数据量不大,为了缓和系统表的资源,应先create table,然后insert。 -------------------------------------------------------------------------------- /docs/MySQL/浅谈MySQL的优化方案.md: -------------------------------------------------------------------------------- 1 | # 浅谈MySQL的优化方案 2 | 3 | > MySQL 数据库常见的优化手段分为三个层面:SQL 和索引优化、数据库结构优化、系统硬件优化等,然而每个大的方向中又包含多个小的优化点。 4 | 5 | ## 1. SQL和索引优化 6 | 7 | - 使用正确的索引 8 | 9 | 1. 我们应该尽可能的使用主键查询,而非其他索引查询,因为主键查询不会触发回表查询,因此节省了一部分时间,变相的提高了查询的性能。 10 | 2. 避免在 where 查询条件中使用 != 或者 <> 操作符,因为这些操作符会导致查询引擎放弃索引而进行全表扫描。 11 | 3. 适当使用前缀索引,MySQL 是支持前缀索引的,也就是说我们可以定义字符串的一部分来作为索引。我们知道索引越长占用的磁盘空间就越大,那么在相同数据页中能放下的索引值也就越少,这就意味着搜索索引需要的查询时间也就越长,进而查询的效率就会降低,所以我们可以适当的选择使用前缀索引,以减少空间的占用和提高查询效率。比如,邮箱的后缀都是固定的“@xxx.com”,那么类似这种后面几位为固定值的字段就非常适合定义为前缀索引。 12 | 13 | - 查询具体的字段而非全部字段 14 | 15 | 1. 要尽量避免使用 select *,而是查询需要的字段,这样可以提升速度,以及减少网络传输的带宽压力。 16 | 17 | + 优化子查询 18 | 19 | 1. 尽量使用 Join 语句来替代子查询,因为子查询是嵌套查询,而嵌套查询会新创建一张临时表,而临时表的创建与销毁会占用一定的系统资源以及花费一定的时间,但 Join 语句并不会创建临时表,因此性能会更高。 20 | 21 | + 注意查询结果集 22 | 23 | 我们要尽量使用小表驱动大表的方式进行查询,也就是如果 B 表的数据小于 A 表的数据,那执行的顺序就是先查 B 表再查 A 表,具体查询语句如下: 24 | 25 | ```sql 26 | select name from A where id in (select id from B); 27 | ``` 28 | 29 | - 不要在列上进行运算操作 30 | 31 | 不要在列字段上进行算术运算或其他表达式运算,否则可能会导致查询引擎无法正确使用索引,从而影响了查询的效率。 32 | 33 | - 适当增加冗余字段 34 | 35 | 增加冗余字段可以减少大量的连表查询,因为多张表的连表查询性能很低,所有可以适当的增加冗余字段,以减少多张表的关联查询,这是以空间换时间的优化策略。 36 | 37 | ## 2. 数据库结构优化 38 | 39 | - 最小数据长度 40 | 41 | 一般说来数据库的表越小,那么它的查询速度就越快,因此为了提高表的效率,应该将表的字段设置的尽可能小,比如身份证号,可以设置为 char(18) 就不要设置为 varchar(18)。 42 | 43 | - 使用最简单数据类型 44 | 45 | 能使用 int 类型就不要使用 varchar 类型,因为 int 类型比 varchar 类型的查询效率更高。 46 | 47 | - 尽量少定义 text 类型 48 | 49 | text 类型的查询效率很低,如果必须要使用 text 定义字段,可以把此字段分离成子表,需要查询此字段时使用联合查询,这样可以提高主表的查询效率。 50 | 51 | - 适当分表、分库策略 52 | 53 | 分表和分库方案也是我们经常说的垂直分隔(分表)和水平分隔(分库)。 54 | 55 | 分表是指当一张表中的字段更多时,可以尝试将一张大表拆分为多张子表,把使用比较高频的主信息放入主表中,其他的放入子表,这样我们大部分查询只需要查询字段更少的主表就可以完成了,从而有效的提高了查询的效率。 56 | 57 | 分库是指将一个数据库分为多个数据库。比如我们把一个数据库拆分为了多个数据库,一个主数据库用于写入和修改数据,其他的用于同步主数据并提供给客户端查询,这样就把一个库的读和写的压力,分摊给了多个库,从而提高了数据库整体的运行效率。 58 | 59 | ## 3. 硬件优化 60 | 61 | MySQL 对硬件的要求主要体现在三个方面:磁盘、网络和内存。 62 | 63 | ① 磁盘 64 | 65 | 磁盘应该尽量使用有高性能读写能力的磁盘,比如固态硬盘,这样就可以减少 I/O 运行的时间,从而提高了 MySQL 整体的运行效率。 66 | 67 | 磁盘也可以尽量使用多个小磁盘而不是一个大磁盘,因为磁盘的转速是固定的,有多个小磁盘就相当于拥有多个并行运行的磁盘一样。 68 | 69 | ② 网络 70 | 71 | 保证网络带宽的通畅(低延迟)以及够大的网络带宽是 MySQL 正常运行的基本条件,如果条件允许的话也可以设置多个网卡,以提高网络高峰期 MySQL 服务器的运行效率。 72 | 73 | ③ 内存 74 | 75 | MySQL 服务器的内存越大,那么存储和缓存的信息也就越多,而内存的性能是非常高的,从而提高了整个 MySQL 的运行效率。 76 | 77 | ## 知识扩展 78 | 79 | ### 正确使用联合索引,联合索引需要注意什么问题? 80 | 81 | 使用了 B+ 树的 MySQL 数据库引擎,比如 InnoDB 引擎,在每次查询复合字段时是从左往右匹配数据的,因此在创建联合索引的时候需要注意索引创建的顺序。例如,我们创建了一个联合索引是 idx(name,age,sex),那么当我们使用,姓名+年龄+性别、姓名+年龄、姓名等这种最左前缀查询条件时,就会触发联合索引进行查询;然而如果非最左匹配的查询条件,例如,性别+姓名这种查询条件就不会触发联合索引。 82 | 83 | 当然,当我们已经有了(name,age)这个联合索引之后,一般情况下就不需要在 name 字段单独创建索引了,这样就可以少维护一个索引。 84 | 85 | ### 慢查询 86 | 87 | 慢查询通常的排查手段是先使用慢查询日志功能,查询出比较慢的 SQL 语句,然后再通过 explain 来查询 SQL 语句的执行计划,最后分析并定位出问题的根源,再进行处理。 88 | 89 | 慢查询日志指的是在 MySQL 中可以通过配置来开启慢查询日志的记录功能,超过 long_query_time 值的 SQL 将会被记录在日志中。我们可以通过设置“slow_query_log=1”来开启慢查询,它的开启方式有两种: 90 | 91 | 通过 MySQL 命令行的模式进行开启,只需要执行“set global slow_query_log=1”即可,然而这种配置模式再重启 MySQL 服务之后就会失效; 92 | 另一种方式可通过修改 MySQL 配置文件的方式进行开启,我们需要配置 my.cnf 中的“slow_query_log=1”即可,并且可以通过设置“slow_query_log_file=/tmp/mysql_slow.log”来配置慢查询日志的存储目录,但这种方式配置完成之后需要重启 MySQL 服务器才可生效。 93 | 需要注意的是,在开启慢日志功能之后,会对 MySQL 的性能造成一定的影响,因此在生产环境中要慎用此功能。 94 | 95 | explain 执行计划的使用示例 SQL 如下: 96 | 97 | ![image-20200626141551018](C:\Users\孙白胖的爸爸\AppData\Roaming\Typora\typora-user-images\image-20200626141551018.png) 98 | 99 | ![img](https://s0.lgstatic.com/i/image/M00/03/33/CgqCHl6ycGyAAMg7AADD5S9L1ek214.png) 100 | 101 | 以上字段中最重要的就是 type 字段,它的所有值如下所示: 102 | 103 | ![3.png](https://s0.lgstatic.com/i/image/M00/03/33/CgqCHl6ycKeAA46vAACNn0J31Ik660.png) 104 | 105 | 当 type 为 all 时,则表示全表扫描,因此效率会比较低,此时需要查看一下为什么会造成此种原因,是没有创建索引还是索引创建的有问题?以此来优化整个 MySQL 运行的速度。 -------------------------------------------------------------------------------- /docs/Redis/README.md: -------------------------------------------------------------------------------- 1 | # Redis TODO 2 | 3 | -------------------------------------------------------------------------------- /docs/Redis/_sidebar.md: -------------------------------------------------------------------------------- 1 | * **👉 Redis** [↩](/README) 2 | * [**Reids**]() 3 | -------------------------------------------------------------------------------- /docs/SpringBoot/README.md: -------------------------------------------------------------------------------- 1 | # SpringBoot 2 | 3 | * [SpringBoot的常用注解](docs/SpringBoot/SpringBoot的常用注解.md) 4 | * [基于SpringBoot集成Mybatis-Plus实现代码生成器](docs/SpringBoot/基于SpringBoot集成Mybatis-Plus实现代码生成器.md) 5 | -------------------------------------------------------------------------------- /docs/SpringBoot/SpringBoot的常用注解.md: -------------------------------------------------------------------------------- 1 | # 👉 SpringBoot的常用注解 2 | 3 | >**注解用来定义一个类、属性或方法,方便程序能够被编译处理。它也相当于一个说明文件。告诉程序被某个注解标注的类或属性是什么,要怎么处理。注解可以用在标注包、类、方法和变量。** 4 | 5 | ## 1. 类上常使用的注解 6 | 7 | | 注解 | 使用位置 | 说明 | 8 | | :-------------------: | :----------------------------: | :----------------------------------------------------------: | 9 | | @RestController | 类名上 | @RestController=@Controller + @RequestBody 前后端分离情况下,用于返回JSON、XML等格式数据 | 10 | | @Controller | 类名上 | 声明当前类是一个控制器层,相当于MVC开发模式中的 C(控制器) | 11 | | @Service | 类名上 | 声明当前类是一个业务处理类,用于标注服务层,处理业务逻辑类 | 12 | | @Reporitory | 类名上 | 用于标注数据访问层 | 13 | | @Component | 类名上 | 通用的注解,可标注任意类为 `Spring` 组件。如果一个 Bean 不知道属于哪个层,可以使用`@Component` 注解标注。 | 14 | | @Configuration | 类名上 | 一般用来声明配置类,可以使用 `@Component`注解替代,不过使用`Configuration`注解声明配置类更加语义化 | 15 | | @Resource | 类名上、属性上或构造函数参数上 | 和@Autowired都可以用来装配Bean,默认是byType自动注入 | 16 | | @Autowired | 类名上、属性上或构造函数参数上 | 自动导入对象到类中,被注入进的类同样要被 Spring 容器管理,默认是byName自动注入 | 17 | | @RequestMapping | 类名或者方法上 | 用来处理请求地址映射 | 18 | | @Transactional | 类名或者方法上 | 在要开启事务的方法上使用`@Transactional`注解即可 | 19 | | @Qualifier | 类名或属性上 | 常与@Autowired一起使用,用于标注哪一个实现类才是需要注入的 | 20 | | @JsonIgnoreProperties | 类名上 | 用于过滤掉特定字段不返回或者不解析 | 21 | | @Transient | 属性上 | 声明不需要与数据库映射的字段,在保存的时候不需要保存进数据库,虚拟字段 | 22 | 23 | ## 2. 方法上常使用的注解 24 | 25 | | 注解 | 使用位置 | 说明 | 26 | | :-----------: | :--------: | :----------------------------------------------------------: | 27 | | @Bean | 方法上 | 声明一个Bean并交给Spring管理。 | 28 | | @ReponseBody | 方法上 | 将方法返回的对象转换为JSON或XML格式后,写入Response对象的body数据区 | 29 | | @RequestBody | 方法参数前 | 简而言之,就是将JSON 字符串转换为 Java对象 | 30 | | @PathVariable | 方法参数前 | 将URL获取的参数映射到方法上 | 31 | | ··· | | | 32 | 33 | ## 3. 其他常使用的注解 34 | 35 | | 注解 | 使用位置 | 说明 | 36 | | :----------------------: | :------------: | :----------------------------------------------------------: | 37 | | @EnableAutoConfiguration | 入口类、类名上 | 开启自动配置 | 38 | | @SpringBootApplication | 入口类、类名上 | 启动入口类Application(@SpringBootApplication看作是 @Configuration、@EnableAutoConfiguration、@ComponentScan 注解的集合。) | 39 | | @EnableScheduling | 入口类、类名上 | 用来开启计划任务,如定时执行的任务 | 40 | | @EnableAsync | 入口类、类名上 | 开启异步注解功能 | 41 | | @ComponentScan | 入口类、类名上 | 用来扫描组件,看自动发现和装配一些Bean | 42 | | @Aspec | 入口类、类名上 | 标注切面,可以用来配置事务、日志、权限验证等 | 43 | | @ControllerAdvice | 类名上 | 包含@Component,可以被扫描到,统一处理异常 | 44 | | @ExceptionHandler | 方法上 | 表示遇到这个异常就执行该方法 | 45 | | @Value | 属性上 | 用于获取配置文件中的值 | 46 | | ··· | | | 47 | 48 | ## 4. 总结 49 | 50 | ​ 当然SpringBoot提供给我们开发人员的注解非常之多,上文中只是总结了SpringBoot开发过程中常用的一些注解,以及简述了其注解的作用或用途。 -------------------------------------------------------------------------------- /docs/SpringBoot/_sidebar.md: -------------------------------------------------------------------------------- 1 | * **👉 SpringBoot** [↩](/README) 2 | * [SpringBoot的常用注解](docs/SpringBoot/SpringBoot的常用注解.md) 3 | * [基于SpringBoot集成Mybatis-Plus实现代码生成器](docs/SpringBoot/基于SpringBoot集成Mybatis-Plus实现代码生成器.md) 4 | -------------------------------------------------------------------------------- /docs/工具/Emoji符号大全.md: -------------------------------------------------------------------------------- 1 | # 👉 Emoji符号大全 2 | 3 | > 如有符号显示不正常,请更换浏览器或操作系统浏览 4 | 5 | | 表情 | emoji | 6 | | ---- | ------------------------------------------------------------ | 7 | | 常见 | 🌹🍀🍎💰📱🌙🍁🍂🍃🌷💎🔪🔫🏀⚽⚡👄👍🔥 | 8 | | 表情 | 😀😁😂😃😄😅😆😉😊😋😎😍😘😗😙😚☺😇😐😑😶😏😣😥😮😯😪😫😴😌😛😜😝😒😓😔😕😲😷😖😞😟😤😢😭😦😧😨😬😰😱😳😵😡😠 | 9 | | 人物 | 👦👧👨👩👴👵👶👱👮👲👳👷👸💂🎅👰👼💆💇🙍🙎🙅🙆💁🙋🙇🙌🙏👤👥🚶🏃👯💃👫👬👭💏💑👪 | 10 | | 手势 | ☕️💪👈👉☝👆👇✌✋👌👍👎✊👊👋👏👐✍ | 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 | | 箭头 | ⬆↗➡↘⬇↙⬅↖↕↔↩↪⤴⤵🔃🔄🔙🔚🔛🔜🔝 | -------------------------------------------------------------------------------- /docs/算法与数据结构/README.md: -------------------------------------------------------------------------------- 1 | ![GRHeq.png](https://s.im5i.com/2021/04/15/GRHeq.png) -------------------------------------------------------------------------------- /docs/算法与数据结构/_sidebar.md: -------------------------------------------------------------------------------- 1 | * **👉 算法与数据结构** [↩](/README) 2 | * [算法概述](docs/算法与数据结构/算法概述.md) 3 | * [数组](docs/算法与数据结构/数组.md) 4 | * [链表](docs/算法与数据结构/链表.md) 5 | * [队列](docs/算法与数据结构/队列.md) 6 | * [栈](docs/算法与数据结构/栈.md) 7 | * [散列表](docs/算法与数据结构/散列表.md) 8 | * [树](docs/算法与数据结构/树.md) 9 | * [图](docs/算法与数据结构/图.md) 10 | * [排序算法](docs/算法与数据结构/排序算法.md) 11 | * [动态规划](docs/算法与数据结构/动态规划.md) 12 | * [递归算法](docs/算法与数据结构/递归算法.md) 13 | * [贪心算法](docs/算法与数据结构/贪心算法.md) 14 | * [剑指offer](docs/算法与数据结构/剑指offer题解.md) 15 | -------------------------------------------------------------------------------- /docs/算法与数据结构/剑指offer题解.md: -------------------------------------------------------------------------------- 1 | # 👉 剑指offer题解 2 | 3 | - [数组中重复的数字](http://mp.weixin.qq.com/s?__biz=MzUzMzM2NTQ0Ng==&mid=100000753&idx=1&sn=cccd8da3a96253839533b7cf41f21edc&chksm=7aa454584dd3dd4ed4644ab3a69372121f1fd8ca42fdb3bd5ee92e6285794e10f786a8c5694e#rd) 4 | - [二维数组中的查找](http://mp.weixin.qq.com/s?__biz=MzUzMzM2NTQ0Ng==&mid=100000773&idx=1&sn=8dc1150518955041da93dd76571f2837&chksm=7aa453ac4dd3daba8b8ff766e54368906cb7028df912fb51833b99d4ad1572129c73cbc9d416#rd) 5 | - [替换空格](http://mp.weixin.qq.com/s?__biz=MzUzMzM2NTQ0Ng==&mid=100000801&idx=1&sn=dcbca06ed25ca77b76de426d3c7910b3&chksm=7aa453884dd3da9e80d22652af33afb411e526fa1fc47538b41e10fa8b7b85a9a048ebe45726#rd) 6 | - [用两个栈实现队列](http://mp.weixin.qq.com/s?__biz=MzUzMzM2NTQ0Ng==&mid=100000846&idx=1&sn=a1e9f3c2ccc75717018b6797b44b19b7&chksm=7aa453e74dd3daf10f930f28065cb62c30b5fa2dffbd38761ae7683690305d801ec00da5ddd1#rd) 7 | - [删除链表节点](http://mp.weixin.qq.com/s?__biz=MzUzMzM2NTQ0Ng==&mid=100000881&idx=1&sn=30ff81c0cec276e8b9fd2c55e6a05468&chksm=7aa453d84dd3dace71e61de78c4404646f7877da39718568396677e25d34778a41044142b300#rd) 8 | - [调整数组顺序使奇数位于偶数前面](http://mp.weixin.qq.com/s?__biz=MzUzMzM2NTQ0Ng==&mid=100000881&idx=1&sn=30ff81c0cec276e8b9fd2c55e6a05468&chksm=7aa453d84dd3dace71e61de78c4404646f7877da39718568396677e25d34778a41044142b300#rd) 9 | - ··· 10 | -------------------------------------------------------------------------------- /docs/算法与数据结构/动态规划.md: -------------------------------------------------------------------------------- 1 | # 👉 动态规划: 2 | 3 | todo~ -------------------------------------------------------------------------------- /docs/算法与数据结构/图.md: -------------------------------------------------------------------------------- 1 | # 👉 图: 2 | 3 | todo~ -------------------------------------------------------------------------------- /docs/算法与数据结构/排序算法.md: -------------------------------------------------------------------------------- 1 | # 👉 排序算法: 2 | 3 | todo~ -------------------------------------------------------------------------------- /docs/算法与数据结构/散列表.md: -------------------------------------------------------------------------------- 1 | # 👉 哈希表: 2 | 3 | todo~ -------------------------------------------------------------------------------- /docs/算法与数据结构/数据结构与算法专栏/01 为什么要学习数据结构和算法?.md: -------------------------------------------------------------------------------- 1 | # 01 | 为什么要学习数据结构和算法? 2 | 3 | img 4 | 5 | ## 前言 6 | 7 | 很多人认为数据结构和算法、计算机网络、操作系统以及计算机组成原理,实际工作中用的非常少,是脱离实际工作的,也就面试的时候会有被问到的机会吧~ 8 | 9 | 这样的声音很多很多,就像我们上学的时候,总会人经常说买菜也不需要勾股定理啊,来抵制应试教育~ 10 | 11 | 其实这样的说法很真实,但是看的还是不够本质吧~ 买菜的时候是不需要勾股定理来计算一下,但是会决定你在哪里买菜~ 仔细品! 12 | 13 | 同样的道理,为什么大厂面试的时候总会被问到数据结构和算法、计算机网络、操作系统等等基础知识呢? 14 | 15 | 有人会说,我在实际工作中很少或者几乎都用不到这数据结构那算法的,不照样把代码写的很“好”?但是事实真的是这样? 16 | 17 | 18 | 19 | 问题来了:到底为什么建议要去学习数据结构与算法呢? 20 | 21 | 22 | 23 | ## 大厂面试必备 24 | 25 | 目前很多的大厂面试,算法与数据结构的考察几乎是必备的。为什么这些大厂都偏向于这方面的考察呢? 26 | 27 | + 如果你是参加校招,从公司考察角度来看,你没有什么实际的项目经验,只能从基础知识考察,比如计算机网络、操作系统、数据库原理、算法与数据结构等等。 28 | 29 | + 如果你是参加社招,公司考察算法与数据结构,其其目的更看重你的长期潜力,而非短期能力。 30 | 31 | ## 提升个人竞争力 32 | 33 | 作为业务开发,我们会用到各种框架、中间件和底层系统,比如 Spring、RPC 框架、消息中间件、Redis 等等。在这些基础框架中,一般都揉和了很多基础数据结构和算法的设计思想。 34 | 35 | 比如,我们常用的 Key-Value 数据库 Redis 中,里面的有序集合是用什么数据结构来实现的呢?为什么要用跳表来实现呢?为什么不用二叉树呢? 36 | 37 | 如果你能弄明白这些底层原理,你就能更好地使用它们。即便出现问题,也很容易就能定位。因此,掌握数据结构和算法,不管对于阅读框架源码,还是理解其背后的设计思想,都是非常有用的。 38 | 39 | 在平时的工作中,数据结构和算法的应用到处可见。我来举一个你非常熟悉的例子:如何实时地统计业务接口的 99% 响应时间? 40 | 41 | 你可能最先想到,每次查询时,从小到大排序所有的响应时间,如果总共有 1200 个数据,那第 1188 个数据就是 99% 的响应时间。很显然,每次用这个方法查询的话都要排序,效率是非常低的。但是,如果你知道“堆”这个数据结构,用两个堆可以非常高效地解决这个问题。 42 | 43 | 44 | 45 | ## 总结 46 | 47 | 最重要的是掌握了数据结构与算法,你看待问题的深度,解决问题的角度就会完全不一样 48 | 49 | 因为这样的你,就像是站在巨人的肩膀上,拿着生存利器行走世界。数据结构与算法,会为你的编程之路,甚至人生之路打开一扇通往新世界的大门。 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /docs/算法与数据结构/数据结构与算法专栏/02 如何抓住重点,系统高效地学习数据结构与算法?.md: -------------------------------------------------------------------------------- 1 | # 02 | 如何抓住重点,系统高效地学习数据结构与算法? 2 | 3 | img 4 | 5 | ## 理解数据结构与算法 6 | 7 | 1. 数据结构就是指一组数据的存储结构。算法就是操作数据的一组方法。 8 | 2. 数据结构和算法是相辅相成的,数据结构是为算法服务的,算法要作用在特定的数据结构之上。 9 | 3. 数据结构是静态的吗,它只是组织数据的一种方式。如果不在它的基础上操作、构建算法、孤立存在的数据结构就是没用的。 10 | 11 | img 12 | 13 | ## 学习重点 14 | 15 | ### 复杂度分析 16 | 17 | 数据结构和算法解决的是如何更省、更快地存储和处理数据的问题,因此,我们就需要一个考量效率和资源消耗的方法,这就是复杂度分析方法。 18 | 19 | > 思考:在实际开发中,如何选取合适的算法与数据结构,都取决于复杂度分析。 20 | 21 | ### 20个最常用的、最基础的数据结构与算法 22 | 23 | #### 数据结构 24 | 25 | 1. 数组 26 | 2. 链表 27 | 3. 栈 28 | 4. 队列 29 | 5. 散列表 30 | 6. 二叉树 31 | 7. 堆 32 | 8. 跳表 33 | 9. 图 34 | 10. Trie树 35 | 36 | #### 算法 37 | 38 | 1. 递归 39 | 2. 排序 40 | 3. 二分查找 41 | 4. 搜索 42 | 5. 哈希算法 43 | 6. 贪心算法 44 | 7. 分治算法 45 | 8. 回溯算法 46 | 9. 动态规划 47 | 10. 字符串匹配算法 48 | 49 | > 思考:在学习数据结构和算法的过程中,你也要注意,不要只是死记硬背,不要为了学习而学习,而是要学习它的“来历”“自身的特点”“适合解决的问题”以及“实际的应用场景”。对于每一种数据结构或算法,我都会从这几个方面进行详细讲解。只要你掌握了我每节课里讲的内容,就能在开发中灵活应用。 50 | 51 | ## 如何学习才能事半功倍 52 | 53 | ### 1. 边学边练,适度刷题 54 | 55 | “边学边练”这一招非常有用。建议你每周花 1~2 个小时的时间,集中把这周的三节内容涉及的数据结构和算法,全都自己写出来,用代码实现一遍。这样一定会比单纯地看或者听的效果要好很多! 56 | 57 | ### 2. 多问、多思考、多互动 58 | 59 | 学习最好的方法是,找到几个人一起学习,一块儿讨论切磋,有问题及时寻求老师答疑。 60 | 61 | > 思考:比如:创建一个交流群,多交流,多思考。 62 | 63 | ### 3. 打怪升级学习法 64 | 65 | 学习的过程中,我们碰到最大的问题就是,坚持不下来。 66 | 67 | > 思考:每次学习一个知识点之后,要学会总结与思考,记下自己的学习心得,针对性的设立阶段目标 。 68 | 69 | ### 4. 知识需要沉淀,不要想试图一下子掌握所有 70 | 71 | 在学习的过程中,一定会碰到“拦路虎”。如果哪个知识点没有怎么学懂,不要着急,这是正常的。因为,想听一遍、看一遍就把所有知识掌握,这肯定是不可能的。 72 | 73 | 学习知识的过程是反复迭代、不断沉淀的过程。如果碰到“拦路虎”,你可以尽情地在留言区问我,也可以先沉淀一下,过几天再重新学一遍。所谓,书读百遍其义自见,我觉得是很有道理的! 74 | 75 | > 思考:说的非常对,学习就是一个反反复复的过程。 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /docs/算法与数据结构/数据结构与算法专栏/03 复杂度分析(上):如何分析、统计算法的执行效率和资源消耗?.md: -------------------------------------------------------------------------------- 1 | # 03 | 复杂度分析(上):如何分析、统计算法的执行效率和资源消耗? 2 | 3 | -------------------------------------------------------------------------------- /docs/算法与数据结构/数据结构与算法专栏/04 复杂度分析(下):浅析最好、最坏、平均、均摊时间复杂度.md: -------------------------------------------------------------------------------- 1 | # 04 | 复杂度分析(下):浅析最好、最坏、平均、均摊时间复杂度 2 | 3 | -------------------------------------------------------------------------------- /docs/算法与数据结构/数据结构与算法专栏/05 数组:为什么很多编程语言中数组都从0开始编号?.md: -------------------------------------------------------------------------------- 1 | # 05 | 数组:为什么很多编程语言中数组都从0开始编号? 2 | 3 | ## 数组的定义 4 | 5 | > 数组是一种线性表数据结构,它是一种线性表数据结构。数组使用一组连续的内存空间,存储一组具有**相同类型**的数据。 6 | 7 | ## 什么是线性表? 8 | 9 | 线性表是数据按照线性排列的结构,每一个线性表上的数据最多只有前后两个方向。像数组、链表、队列、栈等都是线性表结构。 10 | 11 | 如下图所示: 12 | 13 | img 14 | 15 | ## 什么是非线性表? 16 | 17 | 非线性表的概念与线性表相对立,像图、二叉树、堆等。不像线性表一样,在非线性表中的数据并不是简单的前后关系。 18 | 19 | 如下图所示: 20 | 21 | img 22 | 23 | ## 数组的特征:连续的内存空间、相同的数据类型 24 | 25 | 连续的内存空间,使得数据的**随机访问**非常高效。但是也使得在数组中删除、插入一个数据变得非常低效。 26 | 27 | 这是因为数据的连续性带来的限制,在删除或插入一个数据之后,数组需要保持其连续性就必须会涉及到大量的数据搬移操作。 28 | 29 | ## 数组是如何实现随机访问? 30 | 31 | -------------------------------------------------------------------------------- /docs/算法与数据结构/数据结构与算法专栏/06 链表(上):如何实现LRU缓存淘汰算法.md: -------------------------------------------------------------------------------- 1 | # 06 | 链表(上):如何实现LRU缓存淘汰算法? 2 | 3 | -------------------------------------------------------------------------------- /docs/算法与数据结构/数据结构与算法专栏/README.md: -------------------------------------------------------------------------------- 1 | ![GRHeq.png](https://s.im5i.com/2021/04/15/GRHeq.png) -------------------------------------------------------------------------------- /docs/算法与数据结构/数据结构与算法专栏/_sidebar.md: -------------------------------------------------------------------------------- 1 | * **👉 数据结构与算法专栏** [↩](/README) 2 | * **01.为什么要学习数据结构和算法?.md** 3 | * **02.如何抓住重点,系统高效地学习数据结构与算法?.md** 4 | * **03.复杂度分析(上):如何分析、统计算法的执行效率和资源消耗?.md** 5 | * **04.复杂度分析(下):浅析最好、最坏、平均、均摊时间复杂度.md** 6 | 7 | -------------------------------------------------------------------------------- /docs/算法与数据结构/数据结构与算法面试宝典专栏/01 栈:从简单栈到单调栈,解决经典栈问题.md: -------------------------------------------------------------------------------- 1 | # 01 | 栈:从简单栈到单调栈,解决经典栈问题 2 | 3 | ## 前言 4 | 5 | 栈这种数据结构,使用是非常广泛的,比如我们Java中函数的调用、浏览器中的前进与后退功能、操作系统中从用户态到内核态寄存器的保存、网络消息的处理等都会用到栈。 6 | 7 | ## 如何理解栈? 8 | 9 | 如何去理解栈呢? 10 | 11 | 12 | 13 | 子弹上膛的这个过程,后进的子弹最先射出,最上面的子弹就相当于栈顶。一句话可以概况栈的特性:**先进后出**,俗称“**出多了吐**”。 14 | 15 | ![img](https://gimg2.baidu.com/image_search/src=http%3A%2F%2F5b0988e595225.cdn.sohucs.com%2Fimages%2F20171206%2F36518da2f8f341a2b6db088a7e156f5f.jpeg&refer=http%3A%2F%2F5b0988e595225.cdn.sohucs.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1627127265&t=c1701cb993dd287a2bedda0feeed0207) 16 | 17 | 在Java中如何使用栈,代码如下: 18 | 19 | ```java 20 | // 创建一个栈 21 | Stack stack = new Stack(); 22 | // 想栈中放入4个元素 23 | stack.push(1); 24 | stack.push(2); 25 | stack.push(3); 26 | stack.push(4); 27 | 28 | // 查看此时栈顶的元素 29 | System.out.println(stack.peek()); // 输出结果:4 30 | // 将栈顶元素出站 31 | System.out.println(stack.pop()); // 输出结果:4 32 | // 再次查看此时栈顶元素 33 | System.out.println(stack.peek()); // 输出结果:3 34 | // 再次将栈顶元素出站 35 | System.out.println(stack.pop()); // 输出结果:3 36 | ``` 37 | 38 | ## 如何实现栈? 39 | 40 | 实现栈的方式既可以选择数组,也可以链表。那么选择数组来实现的栈就叫做顺序栈,选择链表来实现的栈就叫做链式栈。 41 | 42 | 接下来,我们用Java代码分别实现**顺序栈**、**链式栈**。 43 | 44 | ### 顺序栈 45 | 46 | ```java 47 | /** 48 | * @author 微信公众号:码上Java 49 | * @Description: 用数组实现站结构 50 | */ 51 | 52 | public class MyArratStack { 53 | 54 | private Object[] data = null; // 泛型数组 55 | private int maxSize = 0; //栈容量 56 | private int top = -1; //栈顶指针 57 | 58 | /** 59 | * 初始化构造方法 60 | * @param initialSize 初始栈的容量 61 | */ 62 | MyArratStack(int initialSize) { 63 | if (initialSize >= 0) { 64 | this.maxSize = initialSize; 65 | data = new Object[initialSize]; 66 | top = -1; 67 | } else { 68 | throw new RuntimeException("初始化大小不能小于0: " + initialSize); 69 | } 70 | } 71 | 72 | /** 73 | * 初始化构造方法 默认栈容量为10 74 | */ 75 | public MyArratStack() { 76 | this(10); 77 | } 78 | 79 | /** 80 | * 入栈操作 81 | * @param e 82 | * @return 83 | */ 84 | public boolean push(E e) { 85 | //首先判断一下栈是否已经满了 86 | if (top == maxSize - 1) { 87 | //可优化 TODO 扩容操作 88 | throw new RuntimeException("栈已满,元素无法入栈"); 89 | } else { 90 | data[top] = e; 91 | top++; 92 | return true; 93 | } 94 | } 95 | 96 | /** 97 | * 出栈操作 98 | * @return 99 | */ 100 | public E pop() { 101 | //首先查看一下栈是否为空 102 | if (top == -1) { 103 | throw new RuntimeException("栈为空 "); 104 | } else { 105 | //将栈顶元素返回后维护一下栈顶指针 106 | return (E) data[top--]; 107 | } 108 | } 109 | 110 | /** 111 | * 查看栈顶元素 112 | * @return 113 | */ 114 | public E peek() { 115 | if (top == -1) { 116 | throw new RuntimeException("栈为空"); 117 | } else { 118 | // 查看栈顶元素并不移除所以说不需要维护栈顶指针 119 | return (E) data[top]; 120 | } 121 | } 122 | 123 | /** 124 | * 判断栈是否为空 125 | * @return 126 | */ 127 | public boolean isEmpty() { 128 | return maxSize == 0; 129 | } 130 | 131 | } 132 | ``` 133 | 134 | ### 链式栈 135 | 136 | ```java 137 | public class StackBasedOnLinkedList { 138 | private Node top = null; 139 | 140 | public void push(int value) { 141 | Node newNode = new Node(value, null); 142 | // 判断是否栈空 143 | if (top == null) { 144 | top = newNode; 145 | } else { 146 | newNode.next = top; 147 | top = newNode; 148 | } 149 | } 150 | 151 | /** 152 | * 我用-1表示栈中没有数据。 153 | */ 154 | public int pop() { 155 | if (top == null) return -1; 156 | int value = top.data; 157 | top = top.next; 158 | return value; 159 | } 160 | 161 | public void printAll() { 162 | Node p = top; 163 | while (p != null) { 164 | System.out.print(p.data + " "); 165 | p = p.next; 166 | } 167 | System.out.println(); 168 | } 169 | 170 | private static class Node { 171 | private int data; 172 | private Node next; 173 | 174 | public Node(int data, Node next) { 175 | this.data = data; 176 | this.next = next; 177 | } 178 | 179 | public int getData() { 180 | return data; 181 | } 182 | } 183 | } 184 | ``` 185 | 186 | ### 小结 187 | 188 | 不管是顺序栈还是链式栈,因为栈本身的局限性,也就是一个口子,只能涉及到个别元素的操作,所以说时间复杂度都是O(1)。 189 | 190 | 还有一点你发现没有,顺序栈是存在一个扩容的问题,因为数组不像链表,链表只要你内存够大,是可以支持无限扩展的。但是数组就不一样了,因为数组本身大小是已经初始化好了的,如果数组满了的话,你是无法继续往栈中添加元素的。如果想要添加元素,就必须解决这个扩容的问题。 191 | 192 | 那么如何解决呢?无非就是创建一个更大点的新数组,将就旧数组的元素通过遍历放到新数组里面。此时的时间复杂度会变成O(n)了。 193 | 194 | 那么我们可以总结下,最好的情况是栈不满,时间复杂度是O(1)。最坏的情况是栈满了,时间复杂度是O(1)。 195 | 196 | ## 实践演练 197 | 198 | ### 判断字符串括号是否合法 199 | 200 | 题目描述:给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。 201 | 202 | 有效字符串需满足: 203 | 204 | + 左括号必须用相同类型的右括号闭合。 205 | + 左括号必须以正确的顺序闭合。 206 | 207 | **解题思路:** 208 | 209 | 1. 从左到右遍历字符串,当遇到左括号的时候,将其压入栈中,当遇到有括号的时候,将栈顶的元素与其匹配,如果能够匹配说明合法字符,继续以上操作。如果遇到不能匹配或者栈为空,则说明非法字符。 210 | 2. 如果遍历结束,栈为空,则说明字符串为合法字符。否则,说明有未能匹配的左括号,则为非法字符。 211 | 212 | **代码实现:** 213 | 214 | ```java 215 | /** 216 | * 有效括号校验 217 | * 218 | * @param str 219 | * @return 220 | */ 221 | public static boolean isValid(String str) { 222 | // 如果字符串为空,直接返回false 223 | if (str.length() == 0 || str == null) { 224 | return false; 225 | } 226 | // 如果字符串的长度为奇数,直接返回false 227 | if (str.length() % 2 != 0) { 228 | return false; 229 | } 230 | // 创建一个栈 231 | Stack stack = new Stack(); 232 | 233 | for (int i = 0; i < str.length(); i++) { 234 | char c = str.charAt(i); 235 | if (c == '(') { 236 | stack.push(')'); 237 | } else if (c == '{') { 238 | stack.push('}'); 239 | } else if (c == '[') { 240 | stack.push(']'); 241 | } else if (stack.isEmpty() || c != stack.pop()) { 242 | return false; 243 | } 244 | } 245 | return stack.isEmpty(); 246 | } 247 | ``` 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | -------------------------------------------------------------------------------- /docs/算法与数据结构/数据结构与算法面试宝典专栏/_sidebar.md: -------------------------------------------------------------------------------- 1 | * **👉 数据结构与算法面试宝典专栏** [↩](/README) 2 | * **01.栈:从简单栈到单调栈,解决经典栈问题.md** 3 | * **链表** 4 | * **队列** 5 | * **栈** 6 | * **散列表** 7 | * **树** 8 | * **图** 9 | * **排序算法** 10 | * **动态规划** 11 | * **递归算法** 12 | * **贪心算法** 13 | -------------------------------------------------------------------------------- /docs/算法与数据结构/数据结构与算法面试宝典专栏/树.md: -------------------------------------------------------------------------------- 1 | ~ todo -------------------------------------------------------------------------------- /docs/算法与数据结构/数组.md: -------------------------------------------------------------------------------- 1 | # 👉 数组: 一种非常基础且重要的数据结构 2 | 3 | ![](https://cdn.jsdelivr.net/gh/msJavaCoder/msJava@master/image/数组.png) 4 | 5 | ## 前言 6 | 7 | 数组是一种非常基础且重要的数据结构,很多复杂的数据结构都是基于数组实现的。深入理解数据的存储原理和特点,有利于我们在实际开发工作中,充分发挥数据的优势。 8 | 9 | ## 数据是什么 10 | 11 | 数组的定义:数组(Array)是一种**线性表**数据结构。它用一组**连续的内存空间**,存储一组具有**相同类型的数据**。 12 | 13 | 在上面的定义中加黑的描述,我们可以发现数组的几个特点,分别是:线性表、连续的内存空间、相同类型的数据。如下图所示: 14 | 15 | ![数组1](https://cdn.jsdelivr.net/gh/msJavaCoder/msJava@master/image/数组1.png) 16 | 17 | 数组因具有连续的内存空间的特点,让数据拥有非常高效率的“随机访问”,但也是因为要保持这个连续的内存空间,导致数组在删除或插入操作的时非常低效。因为数组为了保持连续性,必然会涉及大量数据的搬移,这个是非常消耗时间的。 18 | 19 | > 思考:这里你可能会有疑问:什么是连续的内存空间? 20 | 21 | 首先,我们来说说内存,内存是由一个个连续的内存单元组成的,每一个内存单元都有自己的地址。在这些内存单元中,有些被其他数据占用了,有些是空闲的。 22 | 23 | 然而数据中的每个元素,都存储在小小的内存单元中,并且元素之间紧密排列,既不能打乱元素的存储顺序,也不能跳过某个存储单元进行存储。 24 | 25 | ## 数组的随机访问 26 | 27 | 数组的随机访问是有个寻址公式的,上问中我们提到过数组是用一组**连续的内存空间**存储数据元素的,然而每个内存单元都有自己的地址(在计算机里面就是通过这个地址访问数据的),又加上每个内存单元的大小都是一样的,这样就很容易得到一个公式了,如下所示: 28 | 29 | ``` 30 | a[i]_address=base_address+i*data_type_size 31 | ``` 32 | 33 | 我们来简单解释一下上述公式,其中data_type_size表示数组中每个元素的大小、base_address表示内存块的首地址、i 表示数组下标。 34 | 35 | ## 数组的基本操作 36 | 37 | 在开始之前我们先创建一个数组类,来模拟数组操作时候的相关操作。代码如下: 38 | 39 | ```java 40 | public class MyArray { 41 | 42 | private int[] array; 43 | // 数组大小 44 | private int size; 45 | 46 | public MyArray(int capacity) { 47 | this.size = 0; 48 | this.array = new int[capacity]; 49 | } 50 | 51 | } 52 | ``` 53 | 54 | ### 1. 读取元素 55 | 56 | 我们知道数组在内存中是连续存储的,所以根据上文的寻址公式可以知道,我们可以根据数组下标 i 快速定位到对应的元素。 57 | 58 | 简单举例,代码如下: 59 | 60 | ```java 61 | int[] array={1,2,3,4,5,6}; 62 | System.out.println(array[1]); // 输出的是2 因为数组的下标是从0开始的。 63 | ``` 64 | 65 | ### 2. 更新元素 66 | 67 | 我们可以根据数组下标快速查找到对应元素。那么同样道理,我们可以根据数组下标 i 快速更新元素,这中间涉及两个过程,首先就是找到数组下标 i 对应的数据元素A,然后将新的数据元素B赋值给A即完成更新。 68 | 69 | 简单举例,代码如下: 70 | 71 | ```java 72 | int[] array={1,2,3,4,5,6}; 73 | System.out.println(array[1]); // 输出的是2 74 | 75 | //更新数组下标为 1 的数组元素 76 | array[1]=22; 77 | System.out.println(array[1]); // 输出的是22 78 | ``` 79 | 80 | ### 3. 插入元素 81 | 82 | 相比读取、更新操作,插入元素稍微复杂一些,分为以下两种情况: 83 | 84 | 尾部插入:首先,我们看看尾部插入,这种情况很简单,在数组的最后新增一个新的元素,此时对于原数组来说没有任何影响,时间复杂度为0(1)。如下图所示: 85 | 86 | 87 | 88 | 中间插入:如果在数组的中间位置插入元素的话,此时会对插入元素位置之后的元素产生影响,也就是这些数据需要向后依次挪动一个位置。如下图所示: 89 | 90 | 91 | 92 | 中间插入的代码如下: 93 | 94 | ```java 95 | /** 96 | * 插入元素 97 | * @param index 待插入的位置 98 | * @param element 待插入的元素 99 | */ 100 | public void insert(int index,int element){ 101 | if(index<0 || index>size){ 102 | throw new IndexOutOfBoundsException("超过数组容量 ! 插入失败!"); 103 | } 104 | // 从左到右,将元素向右移动一位 105 | for (int i=size-1 ; i>index ; i--){ 106 | array[i+1]=array[i]; 107 | } 108 | // 此时index这个位置已经腾空了,可以放进入element 109 | array[index]=element; 110 | //数组中元素个数+1 111 | size++; 112 | } 113 | ``` 114 | 115 | #### 3.1 数组扩容 116 | 117 | 因为数组的长度在创建的时候已经确定了,当插入元素的时候如果数组已经满了,是没办法插入成功的。这个时候就要考虑数组扩容的问题了,那么该如何实现扩容呢? 118 | 119 | 其实我们可以这样,比如此时的数组是A, A已经满了,我们再创建一个数组B且数组长度是A的2倍,然后我们将数组A的元素全部放到数组B中,这样就完成了数组扩容了。 120 | 121 | 数组扩容的代码如下: 122 | 123 | ```java 124 | /** 125 | * 数组扩容为原数组的二倍 126 | */ 127 | public void resize(){ 128 | int[] newArray=new int[array.length*2]; 129 | System.arraycopy(array,0,newArray,0,array.length); 130 | array=newArray; 131 | } 132 | ``` 133 | 134 | ### 4. 删除元素 135 | 136 | 删除元素和插入元素类似,如果我们删除第k个位置的数据,为了内存的连续性,同样会涉及数据的挪动。如下图所示: 137 | 138 | 139 | 140 | 删除元素的代码如下: 141 | 142 | ```java 143 | /** 144 | * 根据数组下标删除元素 145 | * 146 | * @param index 数组下标 147 | * @return 148 | */ 149 | public int delete(int index) { 150 | if (index < 0 || index > size) { 151 | throw new IndexOutOfBoundsException("已经超过数组容量 ! 插入失败!"); 152 | } 153 | int deleteElement = array[index]; 154 | // 从左到右,将元素向左移动一位 155 | for (int i = index; i < size - 1; i++) { 156 | array[i] = array[i + 1]; 157 | } 158 | size--; 159 | return deleteElement; 160 | } 161 | ``` 162 | 163 | ## 总结 164 | 165 | 数组是使用一块连续的内存空间,存储相同类型的一组数据,其最大的优点是数组支持随机访问,是因为数组可以通过数组下标(寻址公式)快速访问对应元素,时间复杂度为O(1)。 166 | 167 | 数组在删除元素和插入元素这两个操作比较低效,是因为数组为了保持数据的连续性,会涉及到数据的挪动,平均时间复杂度为O(N)。 168 | 169 | 故数组适合“读多写少” 的场景。 -------------------------------------------------------------------------------- /docs/算法与数据结构/栈.md: -------------------------------------------------------------------------------- 1 | # 栈:一吃多就会吐的家伙~ 2 | 3 | ## 前言 4 | 5 | 前两篇文章中我们学习了线性表中的数组和链表,数组和链表是最基础的数据结构,很多数据结构的实现都是基于数据或链表的。那么今天我们一起学习一个非常简单的数据结构—**栈**。栈使用是非常广泛的,比如我们Java中函数的调用、浏览器中的前进与后退功能等都会用到栈。 6 | 7 | ## 什么是栈 8 | 9 | 先画张图,看看栈长什么样。如下图所示: 10 | 11 | ![// 配图](https://cdn.jsdelivr.net/gh/msJavaCoder/msJava@master/image/顺序栈与链式栈.png) 12 | 13 | 从图中看到栈是有些特殊,对于栈的操作被限制只能在栈的一端(栈顶)进行,也就是不允许在栈的中间进行数据操作,只能在栈顶进行数据操作(也就是插入和删除数据)。 14 | 15 | > 思考:“受限制”的栈有什么用呢? 16 | 17 | 特定的数据结构肯定有其特定的使用场景,相比于数组或者链表而言,栈虽然没有怎么灵活(只能在栈的一端进行数据操作),但是对于新增或者删除数据的时候,因为栈只涉及到一端,效率肯定不低。 18 | 19 | 如何去理解栈呢?其实也非常简单,一句话可以概况栈的特性:**先进后出**,俗称“**吃多了吐**”。哈哈~ 20 | 21 | ## 栈的基本操作 22 | 23 | 栈有不同的实现方式,基于数组实现的栈,被叫做**顺序栈**。基于链表实现的栈,被叫做**链式栈**。不管用什么方式实现的栈,其原理都是一样的,不用担心! 24 | 25 | 栈的操作主要就两个:入栈(push)和出栈(pop)。 26 | 27 | - 顺序栈 28 | 29 | 下面我们先基于数组来实现一个顺序栈,代码如下: 30 | 31 | ```java 32 | public class MyStack { 33 | private Object[] data = null; // 数组 34 | private int maxSize = 0; //栈容量 35 | private int top = -1; //栈顶指针 36 | 37 | // 初始化构造方法 38 | MyStack(int initialSize) { 39 | if (initialSize >= 0) { 40 | this.maxSize = initialSize; 41 | data = new Object[initialSize]; 42 | top = -1; 43 | } else { 44 | throw new RuntimeException("初始化大小不能小于0: " + initialSize); 45 | } 46 | } 47 | 48 | // 初始化构造方法 默认栈容量为10 49 | public MyStack() { 50 | this(10); 51 | } 52 | 53 | //入栈操作 54 | public boolean push(E e) { 55 | //首先判断一下栈是否已经满了 56 | if (top == maxSize - 1) { 57 | // 扩容 58 | resize(); 59 | } 60 | data[top] = e; 61 | top++; 62 | return true; 63 | } 64 | 65 | //出栈操作 66 | public E pop() { 67 | //首先查看一下栈是否为空 68 | if (top == -1) { 69 | throw new RuntimeException("栈为空"); 70 | } else { 71 | //将栈顶元素返回后维护一下栈顶指针 72 | return (E) data[top--]; 73 | } 74 | } 75 | 76 | //查看栈顶元素 77 | public E peek() { 78 | if (top == -1) { 79 | throw new RuntimeException("栈为空"); 80 | } else { 81 | // 查看栈顶元素并不移除所以说不需要维护栈顶指针 82 | return (E) data[top]; 83 | } 84 | } 85 | 86 | // 查看栈是否为空 87 | public boolean isEmpty() { 88 | return maxSize == 0; 89 | } 90 | 91 | // 扩容操作 92 | public void resize() { 93 | // 创建一个新数组 94 | Object[] newArray = new Object[data.length * 2]; 95 | System.arraycopy(data, 0, newArray, 0, data.length); 96 | data = newArray; 97 | } 98 | 99 | 100 | } 101 | 102 | ``` 103 | 104 | 在顺序栈中,数组的第一个元素最为栈底,最后一个元素最为栈顶。当top=-1的时候,此时栈为空。 105 | 106 | 每当新增数据入栈push的时候,maxSize加一,同理删除元素出栈pop的时候,maxSize减一。因为是基础数组的实现,所以顺序栈会涉及一个扩容的情况。 107 | 108 | - 链式栈 109 | 110 | 我们再来看看基于链表来实现一个链式栈,代码如下: 111 | 112 | ```java 113 | public class MyStack { 114 | StackNode top = null; //栈顶 115 | 116 | private class StackNode{ 117 | E data; 118 | StackNode next; 119 | StackNode(E data) { 120 | this.data=data; 121 | } 122 | } 123 | 124 | /** 125 | * 入栈 126 | * 首先将要push的数据的next赋值为栈顶top 127 | * 然后将栈顶指针指向新push进来的节点 128 | * @param data 129 | */ 130 | public void push(E data) { 131 | StackNode newNode = new StackNode(data); 132 | newNode.next = top; 133 | top = newNode; 134 | } 135 | 136 | /** 137 | * 出栈 138 | * @return 139 | */ 140 | public E pop() { 141 | if(this.isEmpty()) { 142 | throw new RuntimeException("栈为空"); 143 | } 144 | E data = top.data; 145 | top = top.next; 146 | return data; 147 | } 148 | 149 | /** 150 | * 查看栈顶元素 151 | * @return 152 | */ 153 | public E peek() { 154 | if(isEmpty()) { 155 | throw new RuntimeException("栈为空"); 156 | } 157 | return top.data; 158 | } 159 | 160 | // 判断栈是否为空 161 | public boolean isEmpty() { 162 | return top == null; 163 | } 164 | } 165 | ``` 166 | 167 | 在链式栈中,单链表的头部最为栈顶,因为栈的特性是先进后出,所以不需要头节点的。每当新增数据入栈push的时候,需要让新的结点指向原栈顶,然后再让top指向新增的这个结点。同理删除元素出栈pop的时候,只需要栈顶的 top 指向栈顶元素的 next 指针即可完成删除。 168 | 169 | ## 总结 170 | 171 | 栈作为一个受限制的线性表,只允许对栈顶的数据进行操作,也就是所谓的:先进后出,后进先出。不管是顺序栈还是链式栈,新增或者删除数据时都只能在栈顶进行,故时间复杂度都是O(1),查找数据的时候都需要进行全局遍历,故时间复杂度都是O(n)。顺序栈基于数组实现,初始化时大小便已经固定,后续需要考虑扩容的情况,而链式栈基于链表实现,不需要考虑扩容。 172 | 173 | ~ todo -------------------------------------------------------------------------------- /docs/算法与数据结构/树.md: -------------------------------------------------------------------------------- 1 | # 👉 树: 2 | 3 | todo~ -------------------------------------------------------------------------------- /docs/算法与数据结构/算法概述.md: -------------------------------------------------------------------------------- 1 | # 👉 算法概述: 2 | 3 | todo~ -------------------------------------------------------------------------------- /docs/算法与数据结构/贪心算法.md: -------------------------------------------------------------------------------- 1 | # 👉 贪心算法: 2 | 3 | todo~ -------------------------------------------------------------------------------- /docs/算法与数据结构/递归算法.md: -------------------------------------------------------------------------------- 1 | # 👉 递归算法: 2 | 3 | todo~ -------------------------------------------------------------------------------- /docs/算法与数据结构/链表.md: -------------------------------------------------------------------------------- 1 | # 👉 链表:想写好链表代码可真要下点功夫 2 | 3 | 4 | ![](https://cdn.jsdelivr.net/gh/msJavaCoder/msJava@master/image/链表.png) 5 | 6 | ## 前言 7 | 8 | 上一篇文章我们探讨了数组这个非常基础的数据结构。对于数组,我们知道了数组在内存中是按照顺序存储并线性排列,所以具有“随机访问"的能力,但是对于删除和插入等操作却十分低效。 9 | 10 | 今天我们一起探讨一个新的数据结构—**链表**,看看链表是什么?学习链表有什么用? 11 | 12 | ## 链表是什么 13 | 14 | 链表是一种非常重要的数据结构,应用的非常广泛,在写链表代码非常容易出错,所以面试中链表经常会被用来考察面试者的逻辑是否严谨。 15 | 16 | 链表它不像数组,数组需要的是一块连续的内存空间来存储,而链表并不需要一块连续的内存你空间(也就是可连续也可不连续),它可以利用“**指针**”(`next`域)将一组零散的内存块串联起来,所有链表的存储方式是随机存储。我们看看链表中的单个节点长什么样,如下如所示: 17 | 18 | ![链表单个节点](https://cdn.jsdelivr.net/gh/msJavaCoder/msJava@master/image/链表单个节点.png) 19 | 20 | 图中你可以看到,`data`和`next`。 解释一下: 21 | 22 | 1. **data**: 存放结点值的数据域 ; 23 | 2. **next**: 记录下个结点地址的指针,也叫做后继指针域; 24 | 25 | 链表之所以能够将零散的内存块串联起来,主要就是依靠这个`next`指针。 26 | 27 | 那么接下来,今天我们一起了解三种最常见的链表结构,分别是**单链表**、**双向链表**、**循环链表**等。 28 | 29 | ### 1. 单链表 30 | 31 | 我们先来看看单链表的结构,如下图所示: 32 | 33 | ![](https://cdn.jsdelivr.net/gh/msJavaCoder/msJava@master/image/单链表.png) 34 | 35 | 图中我们可以发现,在单链表中每个单节点都包含两部分,也就是上面我们说的`data`和`next`。这里就不再解释了。除此之外,还有一个`head`,这个是什么呢? 这个其实是**头结点**,也就是链表的第一个节点。同样道理,链表最后一个结点我们称为**尾结点**,尾结点比较特殊,它的next指针是指向null的,也就是表示链表的最后一个结点。 36 | 37 | ### 2. 双向链表 38 | 39 | 我们先再来看看双向链表的结构,如下图所示: 40 | 41 | ![](https://cdn.jsdelivr.net/gh/msJavaCoder/msJava@master/image/双向链表.png) 42 | 43 | 图中我们可以发现,双向链表是比单链表稍微复杂一些的,在单链表中只有一个方向,每个结点只有一个后继指针`next` , 而双向链表支持两个方向,每个结点中不仅有一个后继指针`next`,还有一个前继指针`pre`,而且第一个结点的前继指针`pre`是指向`null`的。 44 | 45 | > 思考:双向链表每个结点使用两个指针有什么优缺点呢? 46 | 47 | 由图可知,单链表只支持一个方向的遍历,而双向链表是支持两个方向的遍历的。优点就是双向链表要比单链表灵活的多,但是这种灵活是要付出代价的。缺点就是如果存储相同数量的元素,相比单链表而言,双向链表的两个指针是比较浪费空间的。 48 | 49 | ### 3. 循环链表 50 | 51 | 提到循环链表,可分为单向循环链表和双向循环链表,其实都是由上述的两种链表演化而来。如下图所示: 52 | 53 | ![](https://cdn.jsdelivr.net/gh/msJavaCoder/msJava@master/image/单向循环链表.png) 54 | 55 | ![](https://cdn.jsdelivr.net/gh/msJavaCoder/msJava@master/image/双向循环链表.png) 56 | 57 | 单链表的尾结点后继指针是指向`null`,而循环链表的尾结点后继指针是指向链表的头结点的,图中我们可以发现,循环链表就像一个环一样首尾连接。 58 | 59 | ## 链表的基本操作 60 | 61 | 上文中我们一起简单聊了几种常见的链表结构,下面我们以单链表为例,用图解的方式看看链表是怎么进行增删改查的,在开始之前我们先创建一个类。代码如下: 62 | 63 | ```java 64 | public class MyLinked { 65 | 66 | private Node head; 67 | 68 | private Node last; 69 | 70 | private int size; 71 | 72 | private static class Node{ 73 | 74 | public int data; 75 | 76 | public Node next; 77 | 78 | public Node(int data){ 79 | this.data=data; 80 | } 81 | } 82 | 83 | } 84 | ``` 85 | 86 | ### 1. 查找结点 87 | 88 | 当数组在查找元素的时候,可以通过下标快速定位到对应元素。但是链表可没这个能力,在链表中查找某个元素,只能从头结点开始一个个向后查找,直到找到要查找的元素或者找不到。由于从头开始遍历,故时间复杂度为O(N)。链表查找结点过程如下图所示: 89 | 90 | ![](https://cdn.jsdelivr.net/gh/msJavaCoder/msJava@master/image/单链表查找元素.png) 91 | 92 | 查找指定结点的代码如下: 93 | 94 | ```java 95 | /** 96 | * 获取指定位置的元素 97 | * 98 | * @param index 指定位置 99 | * @return 100 | * @throws Exception 101 | */ 102 | public Node find(int index) throws Exception { 103 | if (index < 0 || index > size) { 104 | throw new IndexOutOfBoundsException("超出链表实际节点范围!"); 105 | } 106 | Node temp = head; 107 | for (int i = 0; i < index; i++) { 108 | temp = temp.next; 109 | } 110 | return temp; 111 | } 112 | ``` 113 | 114 | ### 2. 更新结点 115 | 116 | 链表中更新结点如查找过程类似,也是从头开始遍历,找到要更新的结点那个位置,然后直接赋值就可以了。链表更新结点过程如下图所示: 117 | 118 | ![](https://cdn.jsdelivr.net/gh/msJavaCoder/msJava@master/image/单链表更新元素.png) 119 | 120 | ### 3. 新增结点 121 | 122 | 链表中新增结点需要考虑三种情况,分别是:头部新增、中间新增、尾部新增。 123 | 124 | 我们先来看看最简单的尾部新增的情况,只需要遍历链表,如果当前结点的`next`指向`null `的话,就直接该结点的`next`指针指向新增的这个结点就可以了。如下图所示: 125 | 126 | ![](https://cdn.jsdelivr.net/gh/msJavaCoder/msJava@master/image/单链表尾部新增元素.png)接下来我们在看看头部插入情况,因为在链表头部插入,所有我们不需要遍历链表。我们先将新增的这个结点的`next`指针指向原链表的头结点`head`,然后修改一下头结点的位置为新增的这个结点即可。如下图所示: 127 | 128 | ![](https://cdn.jsdelivr.net/gh/msJavaCoder/msJava@master/image/单链表头部新增元素.png) 129 | 130 | 最后我们再看看中间新增结点的情况,此时我们需要遍历链表,第一步:将新增的结点的`next`指针指向新增的位置的结点,第二步:将新增的这个位置的前置结点的`next`指针指向新结点即可。这个过程一点要注意,一点不能颠倒顺序,否则容易链表的断开。如下图所示: 131 | 132 | ![](https://cdn.jsdelivr.net/gh/msJavaCoder/msJava@master/image/单链表中间新增元素.png) 133 | 134 | 指定位置新增结点的代码如下: 135 | 136 | ```java 137 | /** 138 | * 指定位置新增元素 139 | * 140 | * @param data 141 | * @param index 142 | * @throws Exception 143 | */ 144 | public void insert(int data, int index) throws Exception { 145 | if (index < 0 || index > size) { 146 | throw new IndexOutOfBoundsException("超出链表实际节点范围!"); 147 | } 148 | Node insertNode = new Node(data); 149 | if (size == 0) { 150 | // 空链表 新增 151 | head = insertNode; 152 | last = insertNode; 153 | } else if (index == 0) { 154 | // 头部新增 155 | insertNode.next = head; 156 | head = insertNode; 157 | } else if (size == index) { 158 | // 尾部新增 159 | last.next = insertNode; 160 | last = insertNode; 161 | } else { 162 | // 获得 新增的位置前面一个元素 163 | Node preNode = find(index - 1); 164 | insertNode.next = preNode.next; 165 | preNode.next = insertNode; 166 | } 167 | // 链表实际长度+1 168 | size++; 169 | } 170 | ``` 171 | 172 | 173 | 174 | ### 4. 删除结点 175 | 176 | 链表中删除结点同样需要考虑三种情况,分别是:头部删除、中间删除、尾部删除。 177 | 178 | 我们先来看看最简单的尾部删除的情况,当遍历到链表倒数第二个结点的结点,直接将该结点的`next`结点指向`null`即可。如下图所示: 179 | 180 | ![](https://cdn.jsdelivr.net/gh/msJavaCoder/msJava@master/image/单链表尾部删除元素.png) 181 | 182 | 接下来我们在看看头部删除情况,当删除链表头部结点的时候,只需要将头结点变更为原头结点的下一个结点为新的头结点即可。如下图所示: 183 | 184 | ![](https://cdn.jsdelivr.net/gh/msJavaCoder/msJava@master/image/单链表头部删除元素.png) 185 | 186 | 最后我们再看看中间删除结点的情况,这个情况的关键是找到待删除结点的前置结点。修改这个前置结点的下一个结点为待删除结点的下一个结点接口。如下图所示: 187 | 188 | ![](https://cdn.jsdelivr.net/gh/msJavaCoder/msJava@master/image/单链表中间删除元素.png) 189 | 190 | 指定位置新增结点的代码如下: 191 | 192 | ```java 193 | /** 194 | * 删除指定位置的链表元素 195 | * 196 | * @param index 指定位置 197 | * @return 删除的元素 198 | * @throws Exception 199 | */ 200 | public Node delete(int index) throws Exception { 201 | if (index < 0 || index > size) { 202 | throw new IndexOutOfBoundsException("超出链表实际节点范围!"); 203 | } 204 | Node removeNode = null; 205 | if (size == 0) { 206 | // 头部删除 207 | removeNode = head; 208 | head = head.next; 209 | } else if (size - 1 == index) { 210 | // 尾部删除 211 | // 获得 删除的位置前面一个元素 212 | Node preNode = find(index - 1); 213 | removeNode = preNode.next; 214 | preNode.next = null; 215 | last = preNode; 216 | } else { 217 | // 中间删除 218 | // 获得 删除的位置前面一个元素 219 | Node preNode = find(index - 1); 220 | removeNode = preNode.next; 221 | preNode.next = preNode.next.next; 222 | } 223 | // 链表实际长度-1 224 | size--; 225 | return removeNode; 226 | } 227 | ``` 228 | 229 | ## 总结 230 | 231 | 本文简单介绍了链表这个数据结构,我们知道了链表的优点是大小可变,插入和删除的效率很都非常高。缺点就是如果查找一个元素,你只能从头开始遍历,所以说查询的效率很低。 232 | 233 | -------------------------------------------------------------------------------- /docs/算法与数据结构/队列.md: -------------------------------------------------------------------------------- 1 | # 👉 队列: 排队买包子,还不允许插队的那种 2 | 3 | ## 前言 4 | 5 | 上一篇文章我们简单阐述了**栈**这个基本数据结构,我们知道了,栈最大的特点就是`后进先出`,以及**入栈**和**出栈**这两个基本的操作。今天我们再来学习与**栈**非常相似的另一个数据结构—**队列**,那么接下来我们看看队列到底是什么吧。 6 | 7 | ## 队列是什么 8 | 9 | 首先,当你看到**队列**这两个字的时候,你脑袋里面会不会联想到每天在早餐店排队买包子的场景呢?(什么?你不吃早餐),这个时候不考虑插队情况(拒绝插队,从你我做起)的话,那就是站在队列前面的人先买到包子,后来的人只能站在队尾等待,故先来的先买包子,也就是队列的**先进先出**。 10 | 11 | 上面我们提到`不能插队`,这个其实就是队列的限制,只能按照`先进先出`的规则,所以说队列同栈一样,也是一个操作受限的数据结构。画张图看看队列,如下图所示: 12 | 13 | ![](https://cdn.jsdelivr.net/gh/msJavaCoder/msJava@master/image/顺序队列与链式队列.1y4xfbj2zun4.png) 14 | 15 | ## 队列的基本操作 16 | 17 | 队列与栈相似,数组和链表均可以实现队列。其中基于数组实现的队列被称为顺序队列,基于链表实现的队列被称为链式队列。队列支持两种基本操作,分别是**入队**和**出队**,数据入队操作是在队列的队尾,数据的出队是在队列的队头。下面我们以基于数组实现的顺序队列为例,看看队列是如何进行入队和出队操作的。 18 | 19 | 我们先创建一个属于我们的队列,代码就不做解释了,该注释的都注释了,如下代码所示: 20 | 21 | ```java 22 | /** 23 | * msJava 24 | * 25 | * @Description 基于数组实现顺序队列 26 | * @Date 2021-08-01 27 | */ 28 | public class MyArrayQueue { 29 | 30 | private Object[] array; 31 | //队列容量 32 | private int n=0; 33 | // 队头 34 | private int head=0; 35 | // 队尾 36 | private int tail=0; 37 | 38 | /** 39 | * 队列构造 40 | * @param capacity 队列容量 41 | */ 42 | public MyArrayQueue(int capacity){ 43 | array=new Object[capacity]; 44 | n=capacity; 45 | } 46 | 47 | /** 48 | * 查看当前队列是否为空 49 | * @return 50 | */ 51 | public boolean isEmpty() { 52 | return n == 0; 53 | } 54 | 55 | /** 56 | * 遍历当前队列 57 | */ 58 | public void ergodic(){ 59 | for (int i = head; i < tail; i++) { 60 | System.out.print(array[i]+" "); 61 | } 62 | System.out.println(); 63 | } 64 | 65 | } 66 | ``` 67 | 68 | ### 入队 69 | 70 | 我们先来画张图,再唠两毛钱的入队操作,如下图所示: 71 | 72 | **![](https://cdn.jsdelivr.net/gh/msJavaCoder/msJava@master/image/顺序队列—入队.5zboyf0cmqc0.png)** 73 | 74 | 如上图中,当6、3、2、1、7、8依次入队之后,此时队头第一个红色块6,队尾是图中黄色块。当有新的数据9入队的时候是直接放到黄色块中的,当数据入队的后,队尾需要向后移动一个位置。 75 | 76 | 我们已经学习过数组了,知道了数组在创建的时候容量已经确定,那么我们基于数组实现其他的数据结构,比如栈和队列,都必然会涉及到数组已满的情况,那么当实现队列的时候,队列已满的情况,你该怎么办呢?类比我们前面文章谈到了,这个你可以好好想想哦。 77 | 78 | 队列入队的代码如下: 79 | 80 | ```java 81 | /** 82 | * 入队 83 | * @param e 84 | * @return 85 | */ 86 | public boolean enqueue(E e){ 87 | if(n==tail){ 88 | throw new RuntimeException("队列已满~" ); 89 | } 90 | array[tail]=e; 91 | tail++; 92 | return true; 93 | } 94 | ``` 95 | 96 | ### 出队 97 | 98 | 然后再画张图,我们看看出队操作,如下图所示: 99 | 100 | ![](https://cdn.jsdelivr.net/gh/msJavaCoder/msJava@master/image/顺序队列—出队.5kqhi5ajn0w0.png) 101 | 102 | 如上图中,当6、3、2等依次出队,每次出队一个数据之后,队头都要向后移动一个位置。队列出队的代码如下: 103 | 104 | ```java 105 | /** 106 | * 出队 107 | * @return 108 | */ 109 | public E dequeue(){ 110 | if(head==tail){ 111 | throw new RuntimeException("队列为空~" ); 112 | } 113 | E ref=(E)array[head]; 114 | ++head; 115 | return ref; 116 | } 117 | ``` 118 | 119 | 图中可以看到,已经出队的块变成了灰色,随着不断的出队,队列的容量逐渐减少,但是队头左侧的数组空间已经无法利用了(因为队列的只能队头出、队尾进),这样不是造成了空间浪费嘛。的确是这样,其实这样的问题可以采用**循环队列**,我们下一篇文章再来唠唠其他类型的队列。 120 | 121 | ## 总结 122 | 123 | 队列是一种操作受限的数据结构,只能先进先出,队列支持两种基本操作:入队和出队。基于数组实现的队列被称为顺序队列,基于链表实现的队列被称为链式队列。 -------------------------------------------------------------------------------- /docs/计算机网络/OSI七层模型.md: -------------------------------------------------------------------------------- 1 | # 👉 OSI 七层模型 2 | 3 | image.png 4 | 5 | OSI 模型全称为开放式通信系统互连参考模型,是国际标准化组织 ( ISO ) 提出的一个试图使各种计算机在世界范围内互连为网络的标准框架。 OSI 将计算机网络体系结构划分为七层,每一层实现各自的功能和协议,并完成与相邻层的接口通信。OSI 的服务定义详细说明了各层所提供的服务。某一层的服务就是该层及其下各层的一种能力,它通过接口提供给更高一层。各层所提供的服务与这些服务是怎么实现的无关 6 | 7 | ## ① 应用层 8 | 9 | 应用层位于 OSI 参考模型的第七层,其作用是通过应用程序间的交互来完成特定的网络应用。该层协议定义了应用进程之间的交互规则,通过不同的应用层协议为不同的网络应用提供服务。例如域名系统 DNS,支持万维网应用的 HTTP 协议,电子邮件系统采用的 SMTP 协议等。在应用层交互的数据单元我们称之为报文。 10 | 11 | ## ② 表示层 12 | 13 | 表示层的作用是使通信的应用程序能够解释交换数据的含义,其位于 OSI 参考模型的第六层,向上为应用层提供服务,向下接收来自会话层的服务。该层提供的服务主要包括数据压缩,数据加密以及数据描述。这使得应用程序不必担心在各台计算机中表示和存储的内部格式差异。 14 | 15 | ## ③ 会话层 16 | 17 | 会话层就是负责建立、管理和终止表示层实体之间的通信会话。该层提供了数据交换的定界和同步功能,包括了建立检查点和恢复方案的方法。 18 | 19 | ## ④ 传输层 20 | 21 | 传输层的主要任务是为两台主机进程之间的通信提供服务。应用程序利用该服务传送应用层报文。该服务并不针对某一特定的应用,多种应用可以使用同一个运输层服务。由于一台主机可同时运行多个线程,因此运输层有复用和分用的功能。所谓复用就是指多个应用层进程可同时使用下面运输层的服务,分用和复用相反,是运输层把收到的信息分别交付上面应用层中的相应进程。 22 | 23 | ## ⑤ 网络层 24 | 25 | 两台计算机之间传送数据时其通信链路往往不止一条,所传输的信息甚至可能经过很多通信子网。网络层的主要任务就是选择合适的网间路由和交换节点,确保数据按时成功传送。在发送数据时,网络层把运输层产生的报文或用户数据报封装成分组和包向下传输到数据链路层。在网络层使用的协议是无连接的网际协议(Internet Protocol)和许多路由协议,因此我们通常把该层简单地成为 IP 层。 26 | 27 | ## ⑥ 数据链路层 28 | 29 | 数据链路层通常也叫做链路层,在物理层和网络层之间。两台主机之间的数据传输,总是在一段一段的链路上传送的,这就需要使用专门的链路层协议。在两个相邻节点之间传送数据时,数据链路层将网络层交下来的 IP 数据报组装成帧,在两个相邻节点间的链路上传送帧。每一帧包括数据和必要的控制信息。通过控制信息我们可以知道一个帧的起止比特位置,此外,也能使接收端检测出所收到的帧有无差错,如果发现差错,数据链路层能够简单的丢弃掉这个帧,以避免继续占用网络资源。 30 | 31 | ## ⑦ 物理层 32 | 33 | 作为 OSI 参考模型中最低的一层,物理层的作用是实现计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异。使其上面的数据链路层不必考虑网络的具体传输介质是什么。该层的主要任务是确定与传输媒体的接口的一些特性(机械特性、电气特性、功能特性,过程特性)。 34 | -------------------------------------------------------------------------------- /docs/计算机网络/README.md: -------------------------------------------------------------------------------- 1 | ![GJ14d.png](https://s.im5i.com/2021/04/14/GJ14d.png) -------------------------------------------------------------------------------- /docs/计算机网络/_sidebar.md: -------------------------------------------------------------------------------- 1 | * **👉 计算机网络** [↩](/README) 2 | * [**OSI七层模型**](docs/计算机网络/OSI七层模型.md) 3 | * [**网络协议分层**](docs/计算机网络/网络协议分层.md) 4 | * [**TCP和UDP**](docs/计算机网络/理解TCP和UDP.md) 5 | * [**HTTP与HTTPS**](docs/计算机网络/理解HTTP与HTTPS.md) 6 | -------------------------------------------------------------------------------- /docs/计算机网络/理解TCP和UDP.md: -------------------------------------------------------------------------------- 1 | # 👉 理解TCP和UDP 2 | 3 | ## 1. TCP是什么 4 | 5 | > TCP(Transmission Control Protocol,传输控制协议)是一个面向连接的、可靠的、基于字节流的传输层协议。从它的概念中我们可以看出 TCP 的三个特点:面向连接、可靠性和面向字节流。 6 | 7 | **TCP 的特点** 8 | 9 | **面向连接**:是指 TCP 是面向客户端和服务器端连接的通讯协议,使用它可以将客户端和服务器端进行连接。 10 | 11 | **可靠性**:是指无论网络环境多差,TCP 都可以保证信息一定能够传递到接收端。 12 | 13 | TCP 之所以可以保证可靠性主要得益于两个方面,一个是“状态性”,另一个是“可控制性”。所谓状态性是指 TCP 会记录信息的发送状态,例如,哪些数据收到了、哪些数据没收到等状态信息都会被记录;可控制性是指 TCP 会根据状态情况控制自己的行为,比如当 TCP 意识到丢包了就会控制重发此包,这样就实现了 TCP 的可靠性。 14 | 15 | **面向字节流**:是指 TCP 是以字节流的方式进行数据传输的。 16 | 17 | **TCP 是传输层协议,对应 OSI 网络模型的第四层传输层,特点如下。** 18 | 19 | - TCP 协议是基于链接的,也就是传输数据前需要先建立好链接,然后再进行传输。 20 | 21 | - TCP 链接一旦建立,就可以在链接上进行双向的通信。 22 | 23 | - TCP 的传输是基于字节流而不是报文,将数据按字节大小进行编号,接收端通过 ACK 来确认收到的数据编号,通过这种机制,TCP 协议能够保证接收数据的有序性和完整性,因此 TCP 能够提供可靠性传输。 24 | 25 | - TCP 还能提供流量控制能力,通过滑动窗口来控制数据的发送速率。滑动窗口的本质是动态缓冲区,接收端根据自己的处理能力,在 TCP 的 Header 中动态调整窗口大小,通过 ACK 应答包通知给发送端,发送端根据窗口大小调整发送的的速度。 26 | 27 | - 仅仅有了流量控制能力还不够,TCP 协议还考虑到了网络问题可能会导致大量重传,进而导致网络情况进一步恶化,因此 TCP 协议还提供拥塞控制。TCP 处理拥塞控制主要用到了慢启动、拥塞避免、拥塞发生、快速恢复四个算法,感兴趣的同学可以进一步了解。 28 | 29 | ## 2. TCP三次握手与四次挥手 30 | 31 | TCP 三次握手的执行流程,如下图所示: 32 | 33 | img 34 | 35 | TCP 三次握手的执行流程图 36 | 37 | 关键字说明: 38 | 39 | **SYN(Synchronize Sequence Numbers),同步序列编号;** 40 | 41 | **ACK(Acknowledge Character),确认字符;** 42 | 43 | **SEQ(Sequence Number),序列号。** 44 | 45 | TCP 的三次握手执行流程如下: 46 | 47 | - 最开始时客户端和服务端都处于 CLOSED 状态,然后服务端先主动监听某个端口,此时服务器端就变成了 LISTEN(监听)状态; 48 | - 然后客户端主动发起连接,发送 SYN(同步序列编号),此时客户端就变成了 SYN-SENT 状态; 49 | - 服务端接收到信息之后返回 SYN 和 ACK 至客户端,此时服务器端就变成了 SYN-REVD 状态; 50 | - 客户端接收到消息之后,再发送 ACK 至服务器端,此时客户端就变成了 ESTABLISHED(已确认)状态,服务端收到 ACK 之后,也变成了 ESTABLISHED 状态,此时连接工作就执行完了。 51 | 52 | TCP 四次挥手的执行流程,如下图所示: 53 | 54 | ![img](https://img01.sogoucdn.com/app/a/100520146/7bacaab50092f15af650aa33c70d81fb) 55 | 56 | TCP 的四次挥手执行流程如下: 57 | 58 | - TCP 链接的关闭,通信双方都可以先发起,我们暂且把先发起的一方看作 Client,从图中看出,通信中 Client 和 Server 两端的链接都是 ESTABLISHED 状态,然后 Client 先主动发起了关闭链接请求,Client 向 Server 发送了一个 FIN 包,表示 Client 端已经没有数据要发送了,然后 Client 进入了 FIN_WAIT_1 状态。 59 | 60 | - Server 端收到 FIN 后,返回 ACK,然后进入 CLOSE_WAIT 状态。此时 Server 属于半关闭状态,因为此时 Client 向 Server 方向已经不会发送数据了,可是 Server 向 Client 端可能还有数据要发送。 61 | 62 | - 当 Server 端数据发送完毕后,Server 端会向 Client 端发送 FIN,表示 Server 端也没有数据要发送了,此时 Server 进入 LAST_ACK 状态,就等待 Client 的应答就可以关闭链接了。 63 | 64 | - Client 端收到 Server 端的 FIN 后,回复 ACK,然后进入 TIME_WAIT 状态。TIME_WAIT 状态下需要等待 2 倍的最大报文段生存时间,来保证链接的可靠关闭,之后才会进入 CLOSED 关闭状态。而 Server 端收到 ACK 后直接就进入 CLOSED 状态。 65 | 66 | 67 | ## 3.为什么TCP需要三次握手呢? 68 | 69 | - **原因一:防止重复连接** 70 | 71 | **三次握手的主要原因是为了防止旧的重复连接引起连接混乱问题。** 72 | 73 | 比如在网络状况比较复杂或者网络状况比较差的情况下,发送方可能会连续发送多次建立连接的请求。如果 TCP 握手的次数只有两次,那么接收方只能选择接受请求或者拒绝接受请求,但它并不清楚这次的请求是正常的请求,还是由于网络环境问题而导致的过期请求,如果是过期请求的话就会造成错误的连接。 74 | 75 | 所以如果 TCP 是三次握手的话,那么客户端在接收到服务器端 SEQ+1 的消息之后,就可以判断当前的连接是否为历史连接,如果判断为历史连接的话就会发送终止报文(RST)给服务器端终止连接;如果判断当前连接不是历史连接的话就会发送指令给服务器端来建立连接。 76 | 77 | - **原因二:同步初始化序列化** 78 | 79 | 通过上面的概念我们知道 TCP 的一个重要特征就是可靠性,而 TCP 为了保证在不稳定的网络环境中构建一个稳定的数据连接,它就需要一个“序列号”字段来保证自己的稳定性,而这个序列号的作用就是防止数据包重复发送,以及有效的解决数据包接收时顺序颠倒的问题。 80 | 81 | 那么在**建立 TCP 连接时就需要同步初始化一个序列号来保证 TCP 的稳定性**,因此它需要执行以下过程: 82 | 83 | 1. 首先客户端发送一个携带了初始序列号的 SYN 报文给服务器端; 84 | 2. 服务端接收到消息之后会回复一个 ACK 的应答报文,表示客户端的 SYN 报文已被服务端成功接收了; 85 | 3. 而客户端收到消息之后也会发送一个 ACK 给服务端,服务器端拿到这个消息之后,我们就可以得到一个可靠的初始化序列号了。 86 | 4. 而如果是两次握手的话,就无法进行序列号的确认工作了,因此也就无法得到一个可靠的序列号了,所以 TCP 连接至少需要三次握手。 87 | 5. 以上两种原因就是 TCP 连接为什么需要三次握手的主要原因,当然 TCP 连接还可以四次握手,甚至是五次握手,也能实现 TCP 连接的稳定性,但三次握手是最节省资源的连接方式,因此 TCP 连接应该为三次握手。 88 | 89 | ## 4. 为什么需要等待 2 倍最大报文段生存时间之后再关闭链接? 90 | 91 | 1. 保证 TCP 协议的全双工连接能够可靠关闭; 92 | 93 | 2. 保证这次连接的重复数据段从网络中消失,防止端口被重用时可能产生数据混淆。 94 | 95 | ## 5. UDP是什么? 96 | 97 | > UDP(User Data Protocol,用户数据报协议)是无连接的、简单的、面向数据报的传输层协议。也就是 UDP 在发送数据之前,无须建立客户端与服务端的连接,直接发送消息即可。 98 | 99 | UDP 常见的使用场景有:语音、视频等多媒体通信、DNS(域名转化)、TFTP 等。 100 | 101 | **TCP 和 UDP 的区别主要体现在以下 7 个方面:** 102 | 103 | 1. 可靠性,TCP 有“状态性”和“可控制性”可以保证消息不重复、按顺序、不丢失的发送和接收,而 UDP 则不能保证消息的可靠性; 104 | 105 | 2. 连接,TCP 是面向连接的传输层协议,传输数据前先要建立连接,而 UDP 发送数据之前无需建立连接; 106 | 107 | 3. 服务对象,TCP 服务的对象为一对一的双端应用,而 UDP 可以应用于一对一、一对多和多对多的通信场景; 108 | 109 | 4. 效率TCP 和 UDP 的使用场景如下图所示:,TCP 的传输效率较低,而 UDP 的传输效率较高; 110 | 111 | 5. 流量控制,TCP 有滑动窗口可以用来控制流量,而 UDP 则不具备流量控制的能力; 112 | 113 | 6. 报文,TCP 是面向字节流的传输层协议,而 UDP 是面向报文的传输层协议; 114 | 115 | 7. 应用场景,TCP 的应用场景是对消息准确性和顺序要求较高的场景,而 UDP 则是应用于对通信效率较高、准确性要求相对较低的场景。 116 | 117 | TCP 和 UDP 的使用场景如下图所示: 118 | 119 | | 使用场景 | 应用层协议 | 传输层协议 | 120 | | :-----------: | :-----------------: | :--------: | 121 | | 浏览器/万维网 | HTPP/HTTPS | TCP | 122 | | 文件传输 | FTP | TCP | 123 | | 电子邮件 | SMTP | TCP | 124 | | 音、视频通讯 | RTP(实时传输协议) | UDP | 125 | | 域名转化 | DNS | UDP | 126 | | 网格管理 | SNMP | UDP | 127 | 128 | ## 总结 129 | 130 | 上文我们介绍了 **TCP 三个特点:面向连接、可靠性和面向字节流**,其中可靠性主要是依赖它的状态记录和根据实际情况调整自身的行为方式。例如,当 TCP 意识到丢包时就会重发此包,这样就保证了通信的可靠性。在传输效率要求比较高且对可靠性要求不高的情况下可以使用 UDP,反之则应该使用 TCP。 131 | 132 | **TCP 之所以需要三次握手的主要原因是为了防止在网络环境比较差的情况下不会进行无效的连接,同时三次握手可以实现 TCP 初始化序列号的确认工作,TCP 需要初始化一个序列号来保证消息的顺序。如果是两次握手则不能确认序列号是否正常,如果是四次握手的话会浪费系统的资源,因此 TCP 三次握手是最优的解决方案,所以 TCP 连接需要三次握手。** 133 | 134 | -------------------------------------------------------------------------------- /docs/计算机网络/网络协议分层.md: -------------------------------------------------------------------------------- 1 | # 👉 网络协议分层 2 | 3 | > 国际标准化组织 ISO 提出了 OSI 开放互连的七层计算机网络模型,从上到下分别是应用层、表示层、会 4 | > 话层、运输层、网络层、链路层和物理层。OSI 模型的概念清楚,理论也比较完善,但是既复杂又不实 5 | > 用。还有一种是 TCP/IP 体系结构,它分为四层,从上到下分别是应用层、运输层、网际层和网络接口 6 | > 层,不过从实质上将只有三层,因为最下面的网络接口层并没有什么具体内容。因特网的协议栈使用一 7 | > 种五层的模型结构,从上到下依次是应用层、运输层、网络层、链路层和物理层,其中下层是为上层提 8 | > 供服务的,每层执行某些动作或使用下层的服务来提高服务。 9 | 10 | ## 应用层 11 | 12 | 应用层是网络体系结构中的最高层,应用层的任务就是通过应用进程之间的交互来完成特定网络应用, 13 | 这一层的数据单元叫做报文。 14 | 应用层的协议定义了应用进程之间通信和交互的规则,主要包括了域名系统 DNS、支持万维网的 HTTP 15 | 协议、支持电子邮件的 SMTP 协议、文件传输协议 FTP 等。 16 | 17 | ### **域名解析系统 DNS** 18 | 19 | DNS 被设计为一个联机分布式数据库系统,并采用客户服务器方式。DNS 使大多数名字都在本地进行 20 | 解析,仅少量解析需要在互联网上通信,因此 DNS 的效率很高。由于 DNS 是分布式系统,即使单个计 21 | 算机出现了故障也不会妨碍到整个 DNS 系统的正常运行。 22 | **主机向本地域名服务器的查询一般都采用递归查询**,递归查询指如果主机所询问的本地域名服务器不知 23 | 道被查询域名的 IP 地址,那么本地域名服务器就以 DNS 客户的身份向其他根域名服务器继续发出查询 24 | 请求报文。递归查询的结果是要查询的 IP 地址,或者是报错,表示无法查询到所需的 IP 地址。 25 | **本地域名服务器向根域名服务器查询通常采用迭代查询**,迭代查询指当根域名服务器收到本地域名服务 26 | 器发出的迭代查询请求报文时,要么给出所要查询的 IP 地址,要么告诉它该向哪一个域名服务器进行 27 | 查询。本地域名服务器也可以采用递归查询,这取决于最初的查询请求报文设置的查询方式。 28 | 29 | ### **文件传送协议 FTP** 30 | 31 | FTP 使用 TCP 可靠的运输服务,FTP 使用客户服务器方式,一个 FTP 服务器进程可以同时为多个客户 32 | 进程提供服务,在进行文件传输时,FTP 的客户和服务器之间要建立两个并行的 TCP 连接:控制连接和 33 | 数据连接,实际用于传输文件的是数据连接。 34 | 35 | ### **电子邮件系统协议 SMTP/POP3/IMAP** 36 | 37 | 一个电子邮件系统有三个主要组成构件,即用户代理、邮件服务器、以及邮件协议。 38 | 从用户代理把邮件传送到邮件服务器,以及在邮件服务器之间的传送都要使用 SMTP,但用户代理从邮 39 | 件服务器读取邮件时则要使用 POP3 或 IMAP 协议。 40 | 基于万维网的电子邮件使用户可以利用浏览器收发电子邮件,用户浏览器和邮件服务器之间使用 HTTP 41 | 协议,而邮件服务器之间的传送仍然使用 SMTP 协议。 42 | 43 | ## 运输层 44 | 45 | **运输层的任务就是负责向两台主机中进程之间的通信提供通用的数据传输服务**,应用进程利用该服务来 46 | 传送应用层报文。由于一台主机同时可以运行多个进程,因此运输层具有复用和分用的功能,复用就是 47 | 多个应用层进程可以同时使用下面运输层的服务,分用就是把运输层收到的信息分别交付给上面应用层 48 | 中的对应进程。 49 | 运输层主要使用两种协议:① 用户数据报协议 UDP,这是一种提供无连接的、尽最大努力交付的数据 50 | 传输服务,不保证数据传输的可靠性,数据传输单位是用户数据报。② 传输控制协议 TCP,这是一种面 51 | 向连接的、可靠的数据传输服务,数据传输单元是报文。 52 | 53 | ## 网络层 54 | 55 | **网络层负责为分组交换网上的不同主机提供通信服务**,在发生数据时,网络层把数据层产生的报文或用 56 | 户数据报封装成分组进行传送,由于网络层使用 IP 协议,因此分组也叫 IP 数据报。网络层的另一个任 57 | 务就是选择合适的路由,使源主机运输层所传下来的分组能够通过网络中的路由器找到目的主机。 58 | 网络层的协议包括了网际协议 IP、地址解析协议 ARP、网际控制报文协议 ICMP 以及路由选择协议 59 | RIP/OSPF/BGP-4 等。 60 | 61 | ### **网际协议 IP** 62 | 63 | 网际协议 IP 是 TCP/IP 体系中两个最主要的协议之一,一般指的是 IPv4。与 IP 协议配套使用的协议还 64 | 有 ARP、ICMP 和 IGMP,IP 使用 ARP,ICMP 和 IGMP 要使用 IP。由于网际协议 IP 是用来使互连起来 65 | 的许多计算机网络能够进行通信的,因此 TCP/IP 体系中的网络层也称网际层或 IP 层。要解决 IP 地址 66 | 耗尽的问题,根本方法是采用具有更大地址空间的新版本 IP 协议即 IPv6,向 IPv6 过渡可以使用双协议 67 | 栈或使用隧道技术。 68 | 69 | ### **地址解析协议 ARP** 70 | 71 | 由于 IP 协议使用了 ARP 协议,因此把 ARP 协议归到网络层,但 ARP 的作用是通过一个 ARP 高速缓存 72 | 存储本地局域网的各主机和路由器的 IP 地址到硬件地址的映射表,以从网络层的 IP 地址解析出在数据 73 | 链路层使用的硬件地址,因此也可以把 ARP 划归在数据链路层。与 ARP 对应的协议是 RARP,逆地址 74 | 解析协议,作用是使只知道自己硬件地址的主机能够找出 IP 地址,但被 DHCP 协议取代。 75 | 76 | ### **路由选择协议 RIP/OSPF/BGP-4** 77 | 78 | 路由选择协议有两大类:内部网关协议,如 RIP 和 OSPF;外部网关协议,如 BGP-4。 79 | RIP 是分布式的基于距离向量的路由选择协议,只适用于小型互联网。RIP 按照固定的时间间隔与相邻 80 | 路由器交换信息,交换的信息是当前路由表。OSPF 是分布式的链路状态协议,适用于大型互联网,只 81 | 在链路状态发生变化时才向本自治系统中的所有路由器用洪泛法发送与本路由器相邻的所有路由器的链 82 | 路状态信息。 83 | BGP-4 是不同自治系统的路由器之间交换路由信息的协议,是一种路径向量路由选择协议。其目标是寻 84 | 找一条能够到达目的网络且比较好的路由而不是最佳路由。 85 | 86 | ### 网际控制报文协议 ICMP 87 | 88 | ICMP 报文作为 IP 数据报的数据,加上首部后组成 IP 数据报发送出去,使用 ICMP 并非为了实现可靠 89 | 传输,ICMP 允许主机或路由器报告差错情况和提供有关异常情况的报告。ICMP 报文的种类有两种, 90 | 即 ICMP 差错报告报文和 ICMP 询问报文。 91 | ICMP 的一个重要应用就是分组间探测 PING,用来测试两台主机之间的连通性,PING 使用了 ICMP 回 92 | 送请求与回送回答报文。 93 | 94 | ### 网际组管理协议 IGMP 95 | 96 | IP 多播使用 IGMP 协议,IGMP 并非在互联网范围内对所有多播组成员进行管理,它不知道 IP 多播组 97 | 包含的成员个数也不知道这些成员都分布在哪些网络上。 98 | IGMP 协议是让连接在本地局域网上的多播路由器知道本局域网上是否有主机上的某个进程参加或推出 99 | 了某个多播组。 100 | 101 | ## 链路层 102 | 103 | **数据链路层的任务是将网络层交下来的 IP 数据报组装成帧,在两个相邻结点之间的链路上传输帧,每** 104 | **一帧包括数据和必要的控制信息(同步信息、地址信息、差错控制等)**。在接收数据时,控制信息使接 105 | 收端能够知道一个帧从哪个比特开始到哪个比特结束,这样链路层就可以从帧中提取出数据部分上交给 106 | 网络层。控制信息还使接收端能够检测到所收到的帧有无差错,如果有差错就简单地丢弃这个帧以免继 107 | 续传送而浪费网络资源。 108 | 数据链路层的协议包括了点对点协议 PPP 和 CSMA/CD 协议等。 109 | 110 | ### 点对点协议 PPP 111 | 112 | 在通信线路质量较差的年代,使用高级数据链路控制 HDLC 作为实现可靠传输的数据链路层协议,但现 113 | 在 HDLC 已经很少使用了,对于点对点的链路,简单得多的点对点协议 PPP 是目前使用得最广泛的数 114 | 据链路层协议。PPP 协议的特点是简单、只检测差错而不纠正差错、不使用序号也不进行流量控制、可 115 | 同时支持多种网络层协议。 116 | 117 | ### CSMA/CD 协议 118 | 119 | 以太网采用的是具有冲突检测的载波监听多点接入 CSMA/CD 协议,协议的要点是:发送前先监听、边 120 | 发送边监听,一旦发现总线上出现了碰撞就立即停止发送。然后按照退避算法等待一段随机时间后再次 121 | 发送,因此每一个站在自己发送数据之后的一小段时间内存在遭遇碰撞的可能性。以太网上各站点都平 122 | 等地争用以太网信道。 123 | 124 | ## 物理层 125 | 126 | **物理层的任务是尽可能地屏蔽掉传输媒体和通信手段的差异,使物理层上面的数据链路层感觉不到这些** 127 | **差异,使其只需考虑本层的协议和服务。** 128 | 物理层所传输的数据单位是比特,发送方发送 1 或 0,接收方也接收 1 或 0,因此物理层需要考虑用多 129 | 大的电压代表 1 或 0,以及接收方如何识别出发送方所发送的比特。除此之外,物理层还要确定连接电 130 | 缆的插头应当有多少根引以及各引脚如何连接等问题。 -------------------------------------------------------------------------------- /docs/设计模式/README.md: -------------------------------------------------------------------------------- 1 | 2 | ![](https://cdn.jsdelivr.net/gh/msJavaCoder/msJava@master/image/设计模式思维导图.png) 3 | -------------------------------------------------------------------------------- /docs/设计模式/_sidebar.md: -------------------------------------------------------------------------------- 1 | * **👉 设计模式** [↩](/README) 2 | * [单例模式](docs/设计模式/单例模式.md) 3 | * [原型模式](docs/设计模式/原型模式.md) 4 | -------------------------------------------------------------------------------- /docs/设计模式/单例模式.md: -------------------------------------------------------------------------------- 1 | # 👉 单例模式 2 | 3 | ## 1. 单例模式的定义 4 | 5 | ​ **单例模式指的是一个类中在任何情况下都绝对只有一个实例,并且提供一个全局访问点。** 6 | 7 | ## 2. 单例模式的应用场景 8 | 9 | > 单例模式的应用非常广泛,如数据库中的连接池、J2EE中的ServletContext和ServletContextConfig、Spring框架中的ApplicationContext等等。然而在Java中,单例模式还可以保证一个JVM中只存在一个唯一的实例。 10 | 11 | 单例模式的应用场景主要有以下几个方面: 12 | 13 | - 当需要频繁创建一些类的时候,使用单例可以降低系统的内存压力,减少GC(垃圾回收) ; 14 | - 当某些类创建实例时候需要占用的资源较多,或者实例化过程耗时比较长,且经常使用的情况; 15 | - 当存在频繁访问数据库或者文件的对象; 16 | - 当对于一些控制硬件级别的操作,或者从系统上来讲应当是单一控制逻辑的操作,是不允许存在多个实例的,否则玩完; 17 | 18 | ## 3. 单例模式的优缺点 19 | 20 | ### 3.1 单例模式的优点 21 | 22 | - **单例模式可以保证内存中只有一个实例对象,从而会减少内存的开销;** 23 | - **单例模式可以避免对资源的多重占用;** 24 | - **单例模式设置全局访问点,可以起到优化和共享资源的访问的作用;** 25 | 26 | ### 3.2 单例模式的缺点 27 | 28 | - **扩展难**, 因为单例模式通常是没有接口的啊,如果想要扩展,那么你唯一途径就是修改之前的代码,所以说单例模式违背了开闭原则; 29 | - **调试难**,因为在并发测试中,单例模式是不利于代码的调试的,单例中的代码没有执行完,也不能模拟生成一个新对象; 30 | - **违背单一职责原则**,因为单例模式的业务代码通常写在一个类中,如果功能设计不合理,就很容易违背单一职责原则; 31 | 32 | ## 4. 单例模式的实现方式及其优缺点 33 | 34 | ### 4.1 单例模式的饿汉式实现 35 | 36 | #### 4.1.1 饿汉式标准写法 37 | 38 | Singleton类称为单例类,通过内部初始化一次 , 隐藏构造方法, 并提供一个全局访问点的方式实现。** 39 | 40 | ```java 41 | /** 42 | * msJava 43 | * 44 | * @Description 单例模式的通用写法 45 | * @Date 2021-01-23 46 | */ 47 | public class Singleton { 48 | /** 49 | * 内部初始化一次 50 | */ 51 | private static final Singleton instance = new Singleton(); 52 | 53 | /** 54 | * 隐藏构造方法 55 | */ 56 | private Singleton() { 57 | } 58 | 59 | /** 60 | * 提供一个全局访问点 61 | * 62 | * @return Singleton 63 | */ 64 | public static Singleton getInstance() { 65 | return instance; 66 | } 67 | 68 | } 69 | 70 | ``` 71 | 72 | ​ **以上饿汉式单例写法在类的初始化的时候就会进行初始化操作,并且创建对象,绝对的线程安全,因为此时线程还没有出现就已经实例化了,故不会存在访问安全的问题。** 73 | 74 | #### 4.1.2 饿汉式静态块机制写法 75 | 76 | ​ **饿汉式还有一种实现,那就是静态块机制,如下代码所示:** 77 | 78 | ```java 79 | /** 80 | * msJava 81 | * 82 | * @Description 单例模式 饿汉式静态机制 实现 83 | * @Date 2021-01-23 84 | */ 85 | public class HungryStaticSingleton { 86 | 87 | private static final HungryStaticSingleton hungrySingleton; 88 | //静态代码块 类加载的时候就初始化 89 | static { 90 | hungrySingleton=new HungryStaticSingleton(); 91 | } 92 | /** 93 | * 私有化构造函数 94 | */ 95 | private HungryStaticSingleton(){} 96 | 97 | /** 98 | * 提供一个全局访问点 99 | * @return 100 | */ 101 | public static HungryStaticSingleton getInstance() { 102 | return hungrySingleton; 103 | } 104 | } 105 | ``` 106 | 107 | ​ **我们分析一下这种是写法 ,可以明显的看到所以对象是类在加载的时候就进行实例化了,那么这样一来,会导致单例对象的数量不确定,从而会导致系统初始化的时候就造成大量内存浪费,况且你用不用还不一定,还一直占着空间,俗称“占着茅坑不拉屎”。** 108 | 109 | ### 4.2 单例模式的懒汉式实现 110 | 111 | ​ **为了解决饿汉式单例写法可能带来的内存浪费问题,这里分析一下懒汉式单例的写法。如下代码所示:** 112 | 113 | ```java 114 | /** 115 | * msJava 116 | * 117 | * @Description 单例模式 懒汉式单例实现 118 | * @Date 2021-01-23 119 | */ 120 | public class LazySimpleSingleton { 121 | 122 | private static LazySimpleSingleton lazySingleton = null; 123 | 124 | /** 125 | * 私有化构造函数 126 | */ 127 | private LazySimpleSingleton() { 128 | 129 | } 130 | /** 131 | * 提供一个全局访问点 132 | * 133 | * @return 134 | */ 135 | public static LazySimpleSingleton getInstance() { 136 | if (lazySingleton == null) { 137 | lazySingleton = new LazySimpleSingleton(); 138 | } 139 | return lazySingleton; 140 | } 141 | } 142 | 143 | ``` 144 | 145 | ​ **这样实现的好处就是只有对象被使用的时候才会进行初始化,不会存在内存浪费的问题,但是它会在多线程环境下,存在线程安全问题。我们可以利用synchronized关键字将全局访问点方法变成一个同步方法,这样就可以解决线程安全的问题,代码如下所示:** 146 | 147 | ```java 148 | /** 149 | * msJava 150 | * 151 | * @Description 单例模式 懒汉式单例实现 synchronized修饰 152 | * @Date 2021-01-23 153 | */ 154 | public class LazySimpleSingleton { 155 | private static LazySimpleSingleton lazySingleton = null; 156 | /** 157 | * 私有化构造函数 158 | */ 159 | private LazySimpleSingleton() {} 160 | /** 161 | * 提供一个全局访问点 162 | * 163 | * @return 164 | */ 165 | public synchronized static LazySimpleSingleton getInstance() { 166 | if (lazySingleton == null) { 167 | lazySingleton = new LazySimpleSingleton(); 168 | } 169 | return lazySingleton; 170 | } 171 | } 172 | ``` 173 | 174 | ​ **但是,这样虽然解决了线程安全的问题,可是如果在线程数量剧增的情况下,用synchronized加锁,则会导致大批线程阻塞,从而骤减系统性能。** 175 | 176 | ### 4.3 单例模式的双重检测实现 177 | 178 | 在上述代码上进一步优化,代码如下所示: 179 | 180 | ```java 181 | 182 | /** 183 | * msJava 184 | * 185 | * @Description 单例模式 懒汉式-双重检测单例实现 186 | * @Date 2021-01-23 187 | */ 188 | public class LazyDoubleCheckSingleton { 189 | // volatile 关键字修饰 190 | private volatile static LazyDoubleCheckSingleton lazySingleton ; 191 | /** 192 | * 私有化构造函数 193 | */ 194 | private LazyDoubleCheckSingleton() {} 195 | /** 196 | * 提供一个全局访问点 197 | * 198 | * @return 199 | */ 200 | public static LazyDoubleCheckSingleton getInstance() { 201 | // 这里先判断一下是否阻塞 202 | if (lazySingleton == null) { 203 | synchronized (LazyDoubleCheckSingleton.class){ 204 | // 判断是否需要重新创建实例 205 | if (lazySingleton == null) { 206 | lazySingleton = new LazyDoubleCheckSingleton(); 207 | } 208 | } 209 | } 210 | return lazySingleton; 211 | } 212 | } 213 | ``` 214 | 215 | ​ **当第一个线程调用getInstance()方法时,第二个线程也可以调用,但是第一个线程执行synchronized时候,第二个线程就会发现阻塞,但是此时的阻塞是getInstance()内部的阻塞。** 216 | 217 | #### 4.4 单例模式的静态内部类实现 218 | 219 | ​ **虽然双重检测锁的单例模式解决了线程安全和性能问题,但是毕竟涉及加锁的操作,多多少少就会到了性能的影响,下面我们分享一下更加优雅的单例模式实现,如下代码所示:** 220 | 221 | ```java 222 | /** 223 | * msJava 224 | * 225 | * @Description 单例模式 静态内部类单例实现 226 | * @Date 2021-01-23 227 | */ 228 | public class LazyStaticInnerClassSingleton { 229 | // 在构造方法里面抛出异常真的合适? 230 | private LazyStaticInnerClassSingleton(){ 231 | if(LazyHolder.INSTANCE != null){ 232 | throw new RuntimeException("不允许创建多个实例"); 233 | } 234 | } 235 | // static 保证这个方法不会被重写 覆盖 236 | private static LazyStaticInnerClassSingleton getInstance(){ 237 | return LazyHolder.INSTANCE; 238 | } 239 | // Java 默认不会加载内部类 240 | private static class LazyHolder{ 241 | private static final LazyStaticInnerClassSingleton INSTANCE=new LazyStaticInnerClassSingleton(); 242 | } 243 | } 244 | 245 | ``` 246 | 247 | ## 5. 总结 248 | 249 | ​ 单例模式面试几乎必备! -------------------------------------------------------------------------------- /docs/设计模式/原型模式.md: -------------------------------------------------------------------------------- 1 | # 👉 原型模式 2 | 3 | todo~ -------------------------------------------------------------------------------- /docs/设计模式/工厂模式.md: -------------------------------------------------------------------------------- 1 | todo~ -------------------------------------------------------------------------------- /docs/设计模式/抽象工厂模式.md: -------------------------------------------------------------------------------- 1 | todo~ -------------------------------------------------------------------------------- /docs/设计模式/设计模式六大设计原则.md: -------------------------------------------------------------------------------- 1 | # 👉 六大设计原则 2 | > 本文我们一起学习设计模式中的六大设计原则 3 | 4 | 六大设计原则包括:单一职责原则、里氏替换原则、依赖倒置原则、接口隔离原则、迪米特法则、开闭原则,接下来我们一一来看看它们分别是什么。 5 | 6 | 7 | ## 1. 单一职责原则 8 | 9 | 单一职责是指一个类只负责一个职责。比如现在比较流行的微服务,就是将之前很复杂耦合性很高的业务,分成多个独立的功能单一的简单接口,然后通过服务编排组装的方式实现不同的业务需求,而这种细粒度的独立接口就是符合单一职责原则的具体实践。 10 | 11 | ## 2. 开闭原则 12 | 13 | 开闭原则指的是对拓展开放、对修改关闭。它是说我们在实现一个新功能时,首先应该想到的是扩展原来的功能,而不是修改之前的功能。 14 | 15 | 这个设计思想非常重要,也是一名优秀工程师所必备的设计思想。至于为什么要这样做?其实非常简单,我们团队在开发后端接口时遵循的也是这个理念。 16 | 17 | 18 | ## 3. 里氏替换原则 19 | 20 | 里氏替换原则是面向对象(OOP)编程的实现基础,它指的是所有引用了父类的地方都能被子类所替代,并且使用子类替代不会引发任何异常或者是错误的出现。 21 | 22 | ## 4. 依赖倒置原则 23 | 24 | 依赖倒置原则指的是要针对接口编程,而不是面向具体的实现编程。也就说高层模块不应该依赖底层模块,因为底层模块的职责通常更单一,不足以应对高层模块的变动,因此我们在实现时,应该依赖高层模块而非底层模块。 25 | 26 | ## 5. 接口隔离原则 27 | 28 | 接口隔离原则是指使用多个专门的接口比使用单一的总接口要好,即接口应该是相互隔离的小接口,而不是一个臃肿且庞杂的大接口。 29 | 30 | 使用接口隔离原则的好处是避免接口的污染,提高了程序的灵活性。 31 | 32 | 可以看出,接口隔离原则和单一职责原则的概念很像,单一职责原则要求接口的职责要单一,而接口隔离原则要求接口要尽量细化,二者虽然有异曲同工之妙,但可以看出单一职责原则要求的粒度更细。 33 | 34 | ## 6. 迪米特法则 35 | 36 | 迪米特法则又叫最少知识原则,它是指一个类对于其他类知道的越少越好。 37 | 38 | 迪米特法则设计的初衷是降低类之间的耦合,让每个类对其他类都不了解,因此每个类都在做自己的事情,这样就能降低类之间的耦合性。 39 | 40 | 这就好比我们在一些电视中看到的有些人在遇到强盗时,会选择闭着眼睛不看强盗,因为知道的信息越少反而对自己就越安全,这就是迪米特法则的基本思想。 41 | 42 | 43 | ## 总结 44 | 45 | 本文中我们总结回顾了设计模式中的六大设计原则,分别为单一职责原则、开闭原则、里氏替换原则、依赖倒置原则、接口隔离原则和迪米特法则,在真是开发中我们应该结合这些概念对照日常项目中的代码,看看还有哪些代码可以进行优化和改进。 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /docs/设计模式/设计模式总结.md: -------------------------------------------------------------------------------- 1 | # 设计模式总结 2 | 3 | ## 1.设计模式一般遵循哪些原则? 4 | 5 | 设计模式一般遵循六大原则: 6 | 7 | **(1)单一职责原则(Single Responsibility Principle)**:不要存在多于一个导致类变更的原因。通俗的说,即一个类只负责一项职责。 8 | 9 | **(2)里氏代换原则(Liskov Substitution Principle)**:所有引用基类的地方必须能透明地使用其子类的对象。简单地说,一个软件如果使用的是一个父类的话,那么一定适用于其子类,而察觉不出父类。 10 | 11 | **(3)依赖倒置原则(Dependence Inversion Principle)**:细节应该依赖于抽象,而抽象不应该依赖于细节。依赖倒置原则的核心思想是面向接口编程。 12 | 13 | **(4)接口隔离原则(Interface Segregation Principle)**:客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上。 14 | 15 | **(5)迪米特法则(最少知道原则)(Demeter Principle)**:一个对象应该对其他对象保持最少的了解,即尽量降低耦合。 16 | 17 | **(6)合成复用原则(Composite Reuse Principle)**:尽量首先使用合成/聚合的方式,而不是使用继承。 18 | 19 | **点评:** 20 | 21 | 设计模式的六大原则基本上都围绕着两点展开:**高内聚**和**松耦合**。如何使你的程序做到这两点就是看你对设计模式的理解程度了。设计模式是一种思想,可以去指导你的行为。理解和掌握了设计模式,并不是说将 23 种设计模式背下来就行了,而是需要你对整个面向对象设计要有非常深刻的理解,将整个设计模式的思想要根深蒂固的植入到你的头脑中。从**两大要求**到**六大原则**再到**二十三种设计模式**,都是对面向对象的不同诠释。 22 | 23 | ## 2. 你在开发中都用到了那些设计模式?用在什么场合?为什么使用这些设计模式? 24 | 25 | **(1)代理模式**:给一个对象提供一个代理对象,并由代理对象控制原对象的引用。通常是需要修改或屏蔽某一个或若干个类的部分功能,复用另外一部分功能。 常见的比如说日志代理、缓存代理等; 26 | 27 | **(2)适配器模式**:把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起使用的类能够一起工作。 28 | 29 | **(3)模板方法模式**:提供一个抽象类,规范整个流程。其中部分逻辑以具体方法实现,部分逻辑声明为抽象方法,让不同的子类实现不同的业务逻辑。适用于固定流程下有部分公共逻辑也有部分个性化逻辑的场景。 30 | 31 | ## 3. 说说开源软件或者JDK中的一些常见的设计模式 32 | 33 | **1. 创建型模式**: 34 | 35 | **(1) 工厂模式**:返回具体对象的方法。比如说:java.lang.Proxy#newProxyInstance(); 36 | 37 | **(2) 抽象工厂模式**:抽象工厂模式提供了一个协议来生成一系列的相关或者独立的对象,而不用指定具体对象的类型。简单地说,一个创建新对象的方法,返回的却是接口或者抽象类的,比如说:java.util.Calendar#getInstance(); 38 | 39 | **(3) 单例模式**:用来确保类只有一个实例。比如说:java.lang.Runtime#getRuntime(); 40 | 41 | **(4) Builder 建造者模式**:定义了一个新的类来构建另一个类的实例,以简化复杂对象的创建。建造模式通常也使用方法链接来实现。比如说:java.lang.StringBuilder#append(); 42 | 43 | **(5) 原型模式**:使得类的实例能够生成自身的拷贝。如果创建一个对象的实例非常复杂且耗时,就可以使用这种模式,而不重新创建一个新的实例,你可以拷贝一个对象并直接修改它。比如说:java.lang.Object#clone() 44 | 45 | **2. 结构型模式:** 46 | 47 | **(1)适配器模式**:用来把一个接口转化成另一个接口。使得原本由于接口不兼容而不能一起工作的那些类可以在一起工作。比如说:java.util.Arrays#asList(); 48 | 49 | **(2)装饰器模式**:动态的给一个对象附加额外的功能,这也是子类的一种替代方式。可以看到,在创建一个类型的时候,同时也传入同一类型的对象。比如说:java.io.BufferedInputStream(InputStream); 50 | 51 | **(3) 代理模式**:代理模式是用一个简单的对象来代替一个复杂的或者创建耗时的对象。比如说:java.lang.reflect.Proxy 52 | 53 | **(4) 外观模式**:给一组组件,接口,抽象,或者子系统提供一个简单的接口。比如说:java.lang.Class; 54 | 55 | **(5)桥接模式**:将抽象和实现进行了解耦,这样使得抽象和实现可以独立地变化。比如说 jdbc 的实现,java.sql.Driver 56 | 57 | **(6)组合模式**:将对象组合成树状层次结构,使用户对单个对象和组合对象具有一致的访问性。简单地说,即某个类型的方法同时也接受自身类型作为参数。比如说:java.util.Map#putAll(Map); 58 | 59 | **(7)享元模式**:运用共享技术(如缓存)来有效地支持大量细粒度对象的复用。比如说:java.lang.Integer#valueOf(int) 60 | 61 | **3. 行为型模式:** 62 | 63 | **(1)策略模式**:将一组算法封装成一系列对象。通过传递这些对象可以灵活的改变程序的功能。比如说:java.util.Comparator#compare() 64 | 65 | **(2)模板方法模式**:让子类可以重写方法的一部分,而不是整个重写,你可以控制子类需要重写那些操作。比如说:java.util.Collections#sort() 66 | 67 | **(3)观察者模式**:它使得一个对象可以灵活的将消息发送给感兴趣的对象。比如说java.util.EventListener 68 | 69 | **(4)迭代器模式**:提供一个一致的方法来顺序访问集合中的对象,这个方法与底层的集合的具体实现无关。比如说:java.util.Iterator 70 | 71 | **(5)责任链模式**:通过把请求从一个对象传递到链条中下一个对象的方式,直到请求被处理完毕,以实现对象间的解耦。比如说:java.util.logging.Logger#log() 72 | 73 | **(6)命令模式**:将操作封装到对象内,以便存储,传递和返回。比如说:java.lang.Runnable 74 | 75 | **(7)备忘录模式**:生成对象状态的一个快照,以便对象可以恢复原始状态而不用暴露自身的内容。比如说,Date对象通过自身内部的一个long值来实现备忘录模式。 76 | 77 | **(8)状态模式**:通过改变对象内部的状态,使得你可以在运行时动态改变一个对象的行为。比如说:java.util.Iterator 78 | 79 | **(9)访问者模式**:提供一个方便的可维护的方式来操作一组对象。它使得你在不改变操作的对象前提下,可以修改或者扩展对象的行为。比如说:javax.lang.model.element.Element and javax.lang.model.element.ElementVisitor 80 | 81 | **(10)中介者模式**:通过使用一个中间对象来进行消息分发以及减少类之间的直接依赖。比如说java.util.concurrent.Executor#execute() 82 | 83 | **(11)解释器模式**:定义了一个语言的语法,然后解析相应语法的语句。比如说:java.util.Pattern 84 | 85 | ## 4.为什么要用单例模式?手写几种线程安全的单例模式? 86 | 87 | 单例模式在框架和项目中应用广泛 ,像线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象经常被设计成单例,主要在于它有以下优点: 88 | 89 | (1)对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销; 90 | 91 | (2)由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。 92 | 93 | (3)可以为整个系统提供一个全局访问点。 94 | 95 | **实现:** 96 | 97 | 单例模式有两种经典实现方式: 98 | 99 | - **饿汉式单例**:即在类加载初始化的时候就立即创建实例; 100 | - **懒汉式单例**:等到真正使用的时候才去创建实例,不用时不去主动创建。 101 | 102 | 下面分别予以说明: 103 | 104 | (1)饿汉式单例 105 | 106 | ```java 107 | /** 108 | * 饿汉式单例 109 | */ 110 | public class SingletonTest { 111 | 112 | // 1. 类对象 113 | private static SingletonTest instance = new SingletonTest(); 114 | 115 | // 2. 构造函数 设置为 私有权限,禁止他人创建实例 116 | private SingletonTest() { 117 | } 118 | 119 | // 3. 通过调用静态方法获得单例 120 | public static SingletonTest newInstance() { 121 | return instance; 122 | } 123 | } 124 | ``` 125 | 126 | (2)懒汉式单例 127 | 128 | ```java 129 | /** 130 | * 懒汉式单例 131 | */ 132 | public class SingletonTest { 133 | 134 | // 1. 创建静态内部类 135 | private static class SingletonTestHolder { 136 | // 在静态内部类里创建单例,由于是静态域,因此只会JVM只会加载1遍,Java虚拟机保证了线程安全性 137 | private static SingletonTest instance = new SingletonTest(); 138 | } 139 | 140 | // 2. 构造函数设置为私有权限,禁止他人创建实例 141 | private SingletonTest() { 142 | } 143 | 144 | // 3. 延迟加载,按需创建 145 | public static SingletonTest getInstance() { 146 | // 在调用装载被初始化时,会初始化它的静态域,从而创建单例; 147 | return SingletonTestHolder.instance; 148 | } 149 | } 150 | ``` 151 | 152 | **点评**: 153 | 154 | 单例模式是设计模式中出现频率最高的,一方面单例模式形式简单优点明显,所以在框架和开发中应用广泛 ;另一方面考察点相对来说也比较多,其他考察点还包括: 155 | 156 | (1)单例模式的其他实现方式,如同步锁、双重校验锁、枚举等; 157 | 158 | (2)单例模式的缺点(优点前面已经提到了)。 159 | 160 | 161 | 162 | A、 单例模式没有抽象层,扩展很困难,基本上只能修改原有代码; 163 | 164 | B、单例类的职责过重,在一定程度上违背了“单一职责原则”; 165 | 166 | C、不适用于变化的对象,如果对象在不同的场景要保存不同的状态,那么单例可能出现数据上的错误。 167 | 168 | (3)还有就是双重锁的潜在安全问题及改进。这一点这里不再展开,大家可以参考[彻头彻尾理解单例模式与多线程](https://blog.csdn.net/justloveyou_/article/details/64127789)这篇文章,本质上是因为创建对象 new SingletonTest() 这个过程可能不是原子的,需要加上 volatile 来禁止重排序。 169 | 170 | ## 5. 使用工厂模式最主要的好处是什么?一般什么情况下使用?工厂模式和抽象工厂模式的差异在哪里? 171 | 172 | *使用工厂模式最主要的好处是什么* 173 | 174 | (1)把对象的创建和调用解耦,当对象的创建发生变化时调用方无须感知,只需要在工厂里面修改即可,提升代码可维护性; 175 | 176 | (2) 对象创建过程很复杂时降低代码重复度和复杂度。 177 | 178 | *一般什么情况下使用* 179 | 180 | (1)对象的创建过程/实例化准备工作很复杂,需要初始化很多参数、查询数据库等; 181 | 182 | (2)类本身有好多子类,这些类的创建过程在业务中容易发生改变,或者对类的调用容易发生改变; 183 | 184 | (3)类的构造方法有多个,容易混淆;可以用一系列名字不同的工厂方法来代替不同的构造方法; 185 | 186 | (4)类需要提供给第三方使用。 187 | 188 | *工厂模式和抽象工厂模式的差异在哪里?* 189 | 190 | 工厂方法模式与抽象工厂模式最大的区别在于:在工厂方法模式中,工厂创造的是一个产品,而在抽象工厂模式中,工厂创造的是一个产品族,然后真正的产品是挑选不同的产品族来组合。 191 | 192 | **点评:** 193 | 194 | 设计模式的一个重要原则是针对接口编程,不要依赖实现类。工厂模式遵循了这一个原则,而且遵循开闭原则,即对于组件功能的扩展是开放的,是允许对其进行功能扩展的(“**开**”);同时对于原有代码的修改是封闭的,即不应该修改原有的代码(“**闭**”) 。 -------------------------------------------------------------------------------- /docs/资源分享/编程人生.md: -------------------------------------------------------------------------------- 1 | # 计算机基础 2 | + 《图解HTTP》 3 | + 《图解TCP/IP》 4 | 5 | # 数据结构与算法 6 | + 《算法 》 7 | + 《图解算法》 8 | + 《大话数据结构》 9 | + 《剑指offer 第二版》 10 | + 《程序员代码面试指南 第二版》 11 | + 《程序员面试金典 第六版》 12 | 13 | # Java 14 | + 《Java编程的逻辑》 15 | + 《Effective Java 第三版》 16 | + 《深入理解Java虚拟机 第三版》 17 | + 《Java并发编程实战》 18 | + 《Java并发编程的艺术》 19 | + 《Java并发编程之美》 20 | 21 | # 数据库 22 | + 《SQL必知必会》 23 | + 《MySQL必知必会》 24 | + 《高性能 MySQL》 25 | + 《Redis设计与实现》 26 | 27 | 28 | # 主流开源框架 29 | + 《SpringBoot实战派》 30 | + 《SpringBoot编程思想》 31 | + 《SpringCloud微服务架构实战派》 32 | + 《通用源码阅读指导书 MyBatis源码详解》 33 | + 《Spring5 核心原理与30个类手写实战》 34 | 35 | 36 | # 阅读人生 37 | + 《认识世界,认识自己》 38 | + 《中国通史》 39 | + 《毛泽东传》 40 | + 《如何学习》 41 | + 《围城》 42 | + 《平凡的世界》 43 | + 《人生》 44 | + 《人生海海》 45 | + 《理解人性》 46 | + 《乌合之众》 47 | + 《思考,快与慢》 48 | + 《金钱不能买什么》 49 | + 《人类简史:从动物到上帝》 50 | + 《未来简史:从智人到智神》 51 | + 《思维简史:从丛林到宇宙》 52 | + 《今日简史:人类命运大议题》 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /docs/踩坑记录/IDEAMaven依赖成功导入但仍然报错找不到包解决方案.md: -------------------------------------------------------------------------------- 1 | # IDEA 2020 Maven依赖成功导入但仍然报错找不到包解决方案 2 | 3 | ## 问题描述 4 | 5 | 项目中引入新的依赖,本地仓库和项目中都可以定位到依赖,但是点击运行,IDEA仍然提示如法加载该依赖。 6 | 7 | ## 报错原因 8 | 9 | IDEA启动程序按钮和maven的build使用的jar包环境不一样 。 10 | 11 | ## 解决方案 12 | 13 | 设置idea构建/运行操作委托给maven就行了。 14 | 15 | 具体设置:Settings搜索Runner,勾选`Delegate IDE build/run actions to Maven` -------------------------------------------------------------------------------- /docs/踩坑记录/README.md: -------------------------------------------------------------------------------- 1 | # 踩坑记录 2 | * [IDEAMaven依赖成功导入但仍然报错找不到包解决方案](docs/踩坑记录/IDEAMaven依赖成功导入但仍然报错找不到包解决方案.md) 3 | -------------------------------------------------------------------------------- /docs/踩坑记录/_sidebar.md: -------------------------------------------------------------------------------- 1 | * **踩坑记录** 2 | * [IDEAMaven依赖成功导入但仍然报错找不到包解决方案](docs/踩坑记录/IDEAMaven依赖成功导入但仍然报错找不到包解决方案.md) 3 | -------------------------------------------------------------------------------- /docs/面试题/MyBatis面试题汇总.md: -------------------------------------------------------------------------------- 1 | # 👉 MyBatis 2 | 3 | #### MyBatis 重要组件 4 | 5 | - Mapper 配置:用于组织具体的查询业务和映射数据库的字段关系,可以使用 XML 格式或 Java 注解格式来实现; 6 | - Mapper 接口:数据操作接口也就是通常说的 DAO 接口,要和 Mapper 配置文件中的方法一一对应; 7 | - Executor:MyBatis 中所有的 Mapper 语句的执行都是通过 Executor 执行的; 8 | - SqlSession:类似于 JDBC 中的 Connection,可以用 SqlSession 实例来直接执行被映射的 SQL 语句; 9 | - SqlSessionFactory:SqlSessionFactory 是创建 SqlSession 的工厂,可以通过 SqlSession openSession() 方法创建 SqlSession 对象。 10 | 11 | #### MyBatis 执行流程 12 | 13 | MyBatis 完整执行流程如下图所示: 14 | 15 | ![1](https://images.gitbook.cn/4070e4c0-da75-11e9-b7a4-5f21fd84c626) 16 | 17 | MyBatis 执行流程说明: 18 | 19 | 1. 首先加载 Mapper 配置的 SQL 映射文件,或者是注解的相关 SQL 内容。 20 | 2. 创建会话工厂,MyBatis 通过读取配置文件的信息来构造出会话工厂(SqlSessionFactory)。 21 | 3. 创建会话,根据会话工厂,MyBatis 就可以通过它来创建会话对象(SqlSession),会话对象是一个接口,该接口中包含了对数据库操作的增、删、改、查方法。 22 | 4. 创建执行器,因为会话对象本身不能直接操作数据库,所以它使用了一个叫做数据库执行器(Executor)的接口来帮它执行操作。 23 | 5. 封装 SQL 对象,在这一步,执行器将待处理的 SQL 信息封装到一个对象中(MappedStatement),该对象包括 SQL 语句、输入参数映射信息(Java 简单类型、HashMap 或 POJO)和输出结果映射信息(Java 简单类型、HashMap 或 POJO)。 24 | 6. 操作数据库,拥有了执行器和 SQL 信息封装对象就使用它们访问数据库了,最后再返回操作结果,结束流程。 25 | 26 | -------------------------------------------------------------------------------- /docs/面试题/README.md: -------------------------------------------------------------------------------- 1 | # 面试题 2 | 3 | * [Java核心面试题汇总](docs/面试题/Java核心面试题汇总.md) 4 | * [算法常用面试题汇总](docs/面试题/算法常用面试题汇总.md) 5 | * [设计模式常见面试题汇总](docs/面试题/设计模式常见面试题汇总.md) 6 | * [MySQL面试题汇总](docs/面试题/MySQL面试题汇总.md) 7 | * [Java虚拟机面试题汇总](docs/面试题/Java虚拟机面试题汇总.md) 8 | * [Spring面试题汇总](docs/面试题/Spring面试题汇总.md) 9 | * [SpringMVC面试题汇总](docs/面试题/SpringMVC面试题汇总.md) 10 | * [MyBatis面试题汇总](docs/面试题/MyBatis面试题汇总.md) 11 | * [SpringBoot面试题汇总](docs/面试题/SpringBoot面试题汇总.md) 12 | * [分布式框架面试题汇总](docs/面试题/分布式框架面试题汇总.md) 13 | * [消息队列面试题汇总](docs/面试题/消息队列面试题汇总.md) 14 | 15 | -------------------------------------------------------------------------------- /docs/面试题/SpringBoot面试题汇总.md: -------------------------------------------------------------------------------- 1 | # 👉 SpringBoot 2 | 3 | ···· 高质量的面试题 整理中 -------------------------------------------------------------------------------- /docs/面试题/_sidebar.md: -------------------------------------------------------------------------------- 1 | * **👉 面试题** [↩](/README) 2 | * [Java核心面试题汇总](docs/面试题/Java核心面试题汇总.md) 3 | * [算法常用面试题汇总](docs/面试题/算法常用面试题汇总.md) 4 | * [设计模式常见面试题汇总](docs/面试题/设计模式常见面试题汇总.md) 5 | * [MySQL面试题汇总](docs/面试题/MySQL面试题汇总.md) 6 | * [Java虚拟机面试题汇总](docs/面试题/Java虚拟机面试题汇总.md) 7 | * [Spring面试题汇总](docs/面试题/Spring面试题汇总.md) 8 | * [SpringMVC面试题汇总](docs/面试题/SpringMVC面试题汇总.md) 9 | * [MyBatis面试题汇总](docs/面试题/MyBatis面试题汇总.md) 10 | * [SpringBoot面试题汇总](docs/面试题/SpringBoot面试题汇总.md) 11 | * [分布式框架面试题汇总](docs/面试题/分布式框架面试题汇总.md) 12 | * [消息队列面试题汇总](docs/面试题/消息队列面试题汇总.md) 13 | -------------------------------------------------------------------------------- /docs/面试题/框架基础面试题.md: -------------------------------------------------------------------------------- 1 | # 👉 Spring经典面试题汇总 2 | 3 | ## 1. Spring的两大特性是什么? 4 | 5 | > AOP(面向切面编程)和IOC(控制反转)。 6 | > 7 | > + AOP的原理是什么?实现AOP有哪些方式? 8 | > 9 | > ​ 答案:Spring AOP 的底层用的是动态代理,有JDK动态代理和CGlib动态代理。 10 | > 11 | > ​ **JDK动态代理:** 利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。 12 | > 13 | > ​ **CGlib动态代理:** 以CGLIB的方式进行代理,它采用底层字节码技术,将代理类的class文件加载进来,通过修改其字节码生成子类来处理。 14 | > 15 | > ​ **区别:** JDK代理只能对实现接口的类生成代理;CGLIB是针对类实现代理,继承指定类并生成一个子类,因此不能代理final修饰类。 16 | 17 | ## 2. Spring中自动装配的方式有哪些? 18 | 19 | > + no : 不进行自动装配,手动设置Bean的依赖关系。 20 | > + byName: 根据Bean的名字进行自动装配。 21 | > + byType: 根据Bean的类型进行自动装配。 22 | > + constructor: 类似于byType,不过是应用于构造器的参数,如果正好有一个Bean与构造器的参数类型相同则可以自动装配,否则会导致错误。 23 | > + autodetect: 如果有默认的构造器,则通过constructor的方式进行自动装配,否则使用byType的方式进行自动装配。 24 | 25 | ## 3. Spring框架有哪些模块组成? 26 | 27 | > + Core 28 | > + Bean 29 | > + Context 30 | > + Expression 31 | > + JDBC 32 | > + ORM 33 | > + OXM 34 | > + beJava Messaging Service(JMS) 35 | > + Transaction 36 | > + Web 37 | > + Web-Servlet 38 | > + Web-Struts 39 | > + Web-Portlet 40 | 41 | ## 4. Spring IOC 的实现原理? 42 | 43 | > 工厂模式+反射机制 44 | > 45 | > + 加载配置文件,解析成BeanDefinition放在Map里; 46 | > + 调用getBean的时候,从BeanDefinition所属的Map里,拿出Class对象进行实例化,同时,如果有依赖关系,将递归调用getBean方法——完成依赖注入。 47 | > 48 | > 什么是Spring IOC容器? 49 | > 50 | > ​ Spring IOC负责创建对象,管理对象,装配对象,配置对象,并且管理这些对象的整个生命周期。 51 | 52 | ## 5. Spring AOP? 53 | 54 | > AOP 面向切面编程,将那些与业务无关却,却为业务模块所共同调用的逻辑或责任封装起来,一边减少系统的重复代码,降低模块间的耦合度,如日志管理、事物处理和权限控制。 55 | > 56 | > Aspect 切面 它将对个类的通用行为封装成可用的模块,该模块含有一组API提供横切功能。切面通过带有@Aspect注解实现。 57 | 58 | ## 6. Spring Bean 的作用域有哪些? 59 | 60 | > singleton : 唯一bean实例,Spring中的bean默认都是单例。 61 | > 62 | > prototype :  每次请求都会换件一个新的bean实例。 63 | > 64 | > request: 每一次http请求都会产生一个新的bean,该bean仅在当前http request内有效。 65 | > 66 | > session: 每一次http请求都会产生一个新的bean,该bean仅在当前http session内有效。 67 | > 68 | > global-session: 全局session作用域。 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /image/公众号二维码.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msJavaCoder/msJava/2553fc4f1c5b62305a685ad6a051031731ccc251/image/公众号二维码.jpg -------------------------------------------------------------------------------- /image/单向循环链表.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msJavaCoder/msJava/2553fc4f1c5b62305a685ad6a051031731ccc251/image/单向循环链表.png -------------------------------------------------------------------------------- /image/单链表.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msJavaCoder/msJava/2553fc4f1c5b62305a685ad6a051031731ccc251/image/单链表.png -------------------------------------------------------------------------------- /image/单链表中间删除元素.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msJavaCoder/msJava/2553fc4f1c5b62305a685ad6a051031731ccc251/image/单链表中间删除元素.png -------------------------------------------------------------------------------- /image/单链表中间新增元素.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msJavaCoder/msJava/2553fc4f1c5b62305a685ad6a051031731ccc251/image/单链表中间新增元素.png -------------------------------------------------------------------------------- /image/单链表头部删除元素.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msJavaCoder/msJava/2553fc4f1c5b62305a685ad6a051031731ccc251/image/单链表头部删除元素.png -------------------------------------------------------------------------------- /image/单链表头部新增元素.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msJavaCoder/msJava/2553fc4f1c5b62305a685ad6a051031731ccc251/image/单链表头部新增元素.png -------------------------------------------------------------------------------- /image/单链表尾部删除元素.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msJavaCoder/msJava/2553fc4f1c5b62305a685ad6a051031731ccc251/image/单链表尾部删除元素.png -------------------------------------------------------------------------------- /image/单链表尾部新增元素.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msJavaCoder/msJava/2553fc4f1c5b62305a685ad6a051031731ccc251/image/单链表尾部新增元素.png -------------------------------------------------------------------------------- /image/单链表更新元素.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msJavaCoder/msJava/2553fc4f1c5b62305a685ad6a051031731ccc251/image/单链表更新元素.png -------------------------------------------------------------------------------- /image/单链表查找元素.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msJavaCoder/msJava/2553fc4f1c5b62305a685ad6a051031731ccc251/image/单链表查找元素.png -------------------------------------------------------------------------------- /image/双向循环链表.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msJavaCoder/msJava/2553fc4f1c5b62305a685ad6a051031731ccc251/image/双向循环链表.png -------------------------------------------------------------------------------- /image/双向链表.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msJavaCoder/msJava/2553fc4f1c5b62305a685ad6a051031731ccc251/image/双向链表.png -------------------------------------------------------------------------------- /image/循环链表.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msJavaCoder/msJava/2553fc4f1c5b62305a685ad6a051031731ccc251/image/循环链表.png -------------------------------------------------------------------------------- /image/数据结构与算法专栏.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msJavaCoder/msJava/2553fc4f1c5b62305a685ad6a051031731ccc251/image/数据结构与算法专栏.png -------------------------------------------------------------------------------- /image/数组.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msJavaCoder/msJava/2553fc4f1c5b62305a685ad6a051031731ccc251/image/数组.png -------------------------------------------------------------------------------- /image/数组1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msJavaCoder/msJava/2553fc4f1c5b62305a685ad6a051031731ccc251/image/数组1.png -------------------------------------------------------------------------------- /image/数组2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msJavaCoder/msJava/2553fc4f1c5b62305a685ad6a051031731ccc251/image/数组2.png -------------------------------------------------------------------------------- /image/数组3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msJavaCoder/msJava/2553fc4f1c5b62305a685ad6a051031731ccc251/image/数组3.png -------------------------------------------------------------------------------- /image/数组4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msJavaCoder/msJava/2553fc4f1c5b62305a685ad6a051031731ccc251/image/数组4.png -------------------------------------------------------------------------------- /image/栈.276ioja9ty4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msJavaCoder/msJava/2553fc4f1c5b62305a685ad6a051031731ccc251/image/栈.276ioja9ty4.png -------------------------------------------------------------------------------- /image/码上Java.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msJavaCoder/msJava/2553fc4f1c5b62305a685ad6a051031731ccc251/image/码上Java.webp -------------------------------------------------------------------------------- /image/设计模式思维导图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msJavaCoder/msJava/2553fc4f1c5b62305a685ad6a051031731ccc251/image/设计模式思维导图.png -------------------------------------------------------------------------------- /image/链表.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msJavaCoder/msJava/2553fc4f1c5b62305a685ad6a051031731ccc251/image/链表.png -------------------------------------------------------------------------------- /image/链表单个节点.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msJavaCoder/msJava/2553fc4f1c5b62305a685ad6a051031731ccc251/image/链表单个节点.png -------------------------------------------------------------------------------- /image/顺序栈与链式栈.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msJavaCoder/msJava/2553fc4f1c5b62305a685ad6a051031731ccc251/image/顺序栈与链式栈.png -------------------------------------------------------------------------------- /image/顺序队列—入队.5zboyf0cmqc0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msJavaCoder/msJava/2553fc4f1c5b62305a685ad6a051031731ccc251/image/顺序队列—入队.5zboyf0cmqc0.png -------------------------------------------------------------------------------- /image/顺序队列—出队.5kqhi5ajn0w0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msJavaCoder/msJava/2553fc4f1c5b62305a685ad6a051031731ccc251/image/顺序队列—出队.5kqhi5ajn0w0.png -------------------------------------------------------------------------------- /image/顺序队列与链式队列.1y4xfbj2zun4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msJavaCoder/msJava/2553fc4f1c5b62305a685ad6a051031731ccc251/image/顺序队列与链式队列.1y4xfbj2zun4.png -------------------------------------------------------------------------------- /msJavaCoder.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1 3 | } 4 | -------------------------------------------------------------------------------- /src/top/msjava/Main.java: -------------------------------------------------------------------------------- 1 | package top.msjava; 2 | 3 | /** 4 | * msJava 5 | * 6 | * @Description 7 | * @Date 2021-12-12 8 | */ 9 | public class Main { 10 | 11 | 12 | public static void main(String[] args) { 13 | 14 | System.out.println("weixin"); 15 | 16 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /sw.js: -------------------------------------------------------------------------------- 1 | /* =========================================================== 2 | * docsify sw.js 3 | * =========================================================== 4 | * Copyright 2016 @huxpro 5 | * Licensed under Apache 2.0 6 | * Register service worker. 7 | * ========================================================== */ 8 | 9 | const RUNTIME = 'docsify' 10 | const HOSTNAME_WHITELIST = [ 11 | self.location.hostname, 12 | 'fonts.gstatic.com', 13 | 'fonts.googleapis.com', 14 | 'cdn.jsdelivr.net' 15 | ] 16 | 17 | // The Util Function to hack URLs of intercepted requests 18 | const getFixedUrl = (req) => { 19 | var now = Date.now() 20 | var url = new URL(req.url) 21 | 22 | // 1. fixed http URL 23 | // Just keep syncing with location.protocol 24 | // fetch(httpURL) belongs to active mixed content. 25 | // And fetch(httpRequest) is not supported yet. 26 | url.protocol = self.location.protocol 27 | 28 | // 2. add query for caching-busting. 29 | // Github Pages served with Cache-Control: max-age=600 30 | // max-age on mutable content is error-prone, with SW life of bugs can even extend. 31 | // Until cache mode of Fetch API landed, we have to workaround cache-busting with query string. 32 | // Cache-Control-Bug: https://bugs.chromium.org/p/chromium/issues/detail?id=453190 33 | if (url.hostname === self.location.hostname) { 34 | url.search += (url.search ? '&' : '?') + 'cache-bust=' + now 35 | } 36 | return url.href 37 | } 38 | 39 | /** 40 | * @Lifecycle Activate 41 | * New one activated when old isnt being used. 42 | * 43 | * waitUntil(): activating ====> activated 44 | */ 45 | self.addEventListener('activate', event => { 46 | event.waitUntil(self.clients.claim()) 47 | }) 48 | 49 | /** 50 | * @Functional Fetch 51 | * All network requests are being intercepted here. 52 | * 53 | * void respondWith(Promise r) 54 | */ 55 | self.addEventListener('fetch', event => { 56 | // Skip some of cross-origin requests, like those for Google Analytics. 57 | if (HOSTNAME_WHITELIST.indexOf(new URL(event.request.url).hostname) > -1) { 58 | // Stale-while-revalidate 59 | // similar to HTTP's stale-while-revalidate: https://www.mnot.net/blog/2007/12/12/stale 60 | // Upgrade from Jake's to Surma's: https://gist.github.com/surma/eb441223daaedf880801ad80006389f1 61 | const cached = caches.match(event.request) 62 | const fixedUrl = getFixedUrl(event.request) 63 | const fetched = fetch(fixedUrl, {cache: 'no-store'}) 64 | const fetchedCopy = fetched.then(resp => resp.clone()) 65 | 66 | // Call respondWith() with whatever we get first. 67 | // If the fetch fails (e.g disconnected), wait for the cache. 68 | // If there’s nothing in cache, wait for the fetch. 69 | // If neither yields a response, return offline pages. 70 | event.respondWith( 71 | Promise.race([fetched.catch(_ => cached), cached]) 72 | .then(resp => resp || fetched) 73 | .catch(_ => { /* eat any errors */ 74 | }) 75 | ) 76 | 77 | // Update the cache with the version we fetched (only for ok status) 78 | event.waitUntil( 79 | Promise.all([fetchedCopy, caches.open(RUNTIME)]) 80 | .then(([response, cache]) => response.ok && cache.put(event.request, response)) 81 | .catch(_ => { /* eat any errors */ 82 | }) 83 | ) 84 | } 85 | }) --------------------------------------------------------------------------------