diff --git a/README.md b/README.md index 99112b09..7a73df4f 100644 --- a/README.md +++ b/README.md @@ -1,135 +1,15 @@ -本项目对应 WebSite: https://duhouan.github.io/Java/#/ +本仓库是对 Java 的一些基础知识、数据库知识、以及框架知识进行收集、整理(持续更新中)。
+仓库对应 WebSite:https://duhouan.github.io/Java/#
+如何流畅访问 Github -# 📖 目录 +
-## ✏️ 计算机基础 +
+ + + +

-### I、计算机网络 - -- [第一节 概述](docs/Net/1_概述.md) -- [第二节 物理层](docs/Net/2_物理层.md) -- [第三节 数据链路层](docs/Net/3_数据链路层.md) -- [第四节 网络层](docs/Net/4_网络层.md) -- [第五节 运输层](docs/Net/5_运输层.md) -- [第六节 应用层](docs/Net/6_应用层.md) - -### II、操作系统 - -- [第一节 操作系统概述](docs/OS/1_操作系统概述.md) -- [第二节 进程管理](docs/OS/2_进程管理.md) -- [第三节 死锁](docs/OS/3_死锁.md) -- [第四节 内存管理](docs/OS/4_内存管理.md) -- [第五节 设备管理](docs/OS/4_设备管理.md) -- [第六节 链接](docs/OS/6_链接.md) - -## ☕️ Java 基础 - -- [第一节 数据类型](docs/JavaBasics/00数据类型.md) -- [第二节 String](docs/JavaBasics/01String.md) -- [第三节 运算](docs/JavaBasics/02%E8%BF%90%E7%AE%97.md) -- [第四节 Object 通用方法](docs/JavaBasics/03Object%E9%80%9A%E7%94%A8%E6%96%B9%E6%B3%95.md) -- [第五节 关键字](docs/JavaBasics/04%E5%85%B3%E9%94%AE%E5%AD%97.md) -- [第六节 反射](docs/JavaBasics/05%E5%8F%8D%E5%B0%84.md) -- [第七节 异常](docs/JavaBasics/06%E5%BC%82%E5%B8%B8.md) -- [第八节 泛型](docs/JavaBasics/07%E6%B3%9B%E5%9E%8B.md) -- [第九节 注解](docs/JavaBasics/08%E6%B3%A8%E8%A7%A3.md) -- [第十节 Java中常见对象](docs/JavaBasics/09Java%E5%B8%B8%E8%A7%81%E5%AF%B9%E8%B1%A1.md) -- [第十一节 其他](docs/JavaBasics/10%E5%85%B6%E4%BB%96.md) - -## 💾 Java 容器 - -- [第一节 Java容器概览](docs/JavaContainer/00Java%E5%AE%B9%E5%99%A8%E6%A6%82%E8%A7%88.md) -- [第二节 容器的设计模式](docs/JavaContainer/01%E5%AE%B9%E5%99%A8%E4%B8%AD%E7%9A%84%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md) -- [第三节 容器源码解析-List](docs/JavaContainer/02容器源码分析%20-%20List.md) -- [第三节 容器源码解析-Map](docs/JavaContainer/03容器源码分析%20-%20Map.md) -- [第三节 容器源码解析-并发容器](docs/JavaContainer/04容器源码分析%20-%20并发容器.md) - -## 🙊 Java 虚拟机 - -- [第一节 运行时数据区域](docs/JVM/1_JVM.md) -- [第二节 HotSpot 虚拟机对象](docs/JVM/2_JVM.md) -- [第三节 String 类和常量池](docs/JVM/3_JVM.md) -- [第四节 8 种基本类型的包装类和常量池](docs/JVM/4_JVM.md) -- [第五节 垃圾收集](docs/JVM/5_JVM.md) -- [第六节 内存分配与回收策略](docs/JVM/6_JVM.md) -- [第七节 类加载机制](docs/JVM/7_JVM.md) - -## 🔨 Java 并发 - -- [第一节 基础知识](docs/Java_Concurrency/1_基础知识.md) -- [第二节 并发理论](docs/Java_Concurrency/2_并发理论.md) -- [第三节 并发关键字](docs/Java_Concurrency/3_并发关键字.md) -- [第四节 Lock 体系](docs/Java_Concurrency/4_Lock%20体系.md) -- [第五节 原子操作类](docs/Java_Concurrency/5_原子操作类.md) -- [第六节 并发容器](docs/Java_Concurrency/6_并发容器.md) -- [第七节 并发工具](docs/Java_Concurrency/7_并发工具.md) -- [第八节 线程池](docs/Java_Concurrency/8_线程池.md) -- [第九节 并发实践](docs/Java_Concurrency/9_并发实践.md) - -## 💡 JavaIO - -- [第一节 概览](docs/JavaIO/00%E6%A6%82%E8%A7%88.md) -- [第二节 磁盘操作](docs/JavaIO/01%E7%A3%81%E7%9B%98%E6%93%8D%E4%BD%9C.md) -- [第三节 字节操作](docs/JavaIO/02%E5%AD%97%E8%8A%82%E6%93%8D%E4%BD%9C.md) -- [第四节 字符操作](docs/JavaIO/03%E5%AD%97%E7%AC%A6%E6%93%8D%E4%BD%9C.md) -- [第五节 对象操作](docs/JavaIO/04%E5%AF%B9%E8%B1%A1%E6%93%8D%E4%BD%9C.md) -- [第六节 网络操作](docs/JavaIO/05%E7%BD%91%E7%BB%9C%E6%93%8D%E4%BD%9C.md) -- [第七节 NIO](docs/JavaIO/06NIO.md) - -## 📝 JavaWeb - -- [第一节 Servlet 工作原理解析](docs/JavaWeb/00Servlet%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86%E8%A7%A3%E6%9E%90.md) -- [第二节 JSP 解析](docs/JavaWeb/01JSP%E8%A7%A3%E6%9E%90.md) -- [第三节 深入理解 Session 和 Cookie](docs/JavaWeb/02%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Session%E5%92%8CCookie.md) - -## 👫 面向对象 - -### I、设计模式 - -- [第一节 设计模式概述](docs/OO/00%E6%A6%82%E8%BF%B0.md) -- [第二节 创建型设计模式](docs/OO/01%E5%88%9B%E5%BB%BA%E5%9E%8B.md) -- [第三节 结构型设计模式](docs/OO/03%E7%BB%93%E6%9E%84%E5%9E%8B.md) - -### II、面向对象思想 - -- [第五节 面向对象三大特性](docs/OO/04%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%89%E5%A4%A7%E7%89%B9%E6%80%A7.md) -- [第六节 关系类图](docs/OO/05%E5%85%B3%E7%B3%BB%E7%B1%BB%E5%9B%BE.md) -- [第七节 面向对象设计原则](docs/OO/06%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E8%AE%BE%E8%AE%A1%E5%8E%9F%E5%88%99.md) - -## 🎨 剑指 Offer 题解 - -- [第一节 数组](docs/AimForOffer/1_数组.md) -- [第二节 字符串](docs/AimForOffer/2_字符串.md) -- [第三节 查找](docs/AimForOffer/3_查找.md) -- [第四节 链表](docs/AimForOffer/4_链表.md) -- [第五节 树](docs/AimForOffer/5_树.md) -- [第六节 栈](docs/AimForOffer/6_栈.md) -- [第七节 队列](docs/AimForOffer/7_队列.md) -- [第八节 动态规划](docs/AimForOffer/8_动态规划.md) -- [第九节 回溯](docs/AimForOffer/9_回溯.md) -- [第十节 深度优先](docs/AimForOffer/10_深度优先.md) -- [第十一节 贪心](docs/AimForOffer/11_贪心.md) -- [第十二节 位运算](docs/AimForOffer/12_位运算.md) -- [第十三节 排序](docs/AimForOffer/13_排序.md) -- [第十四节 堆](docs/AimForOffer/14_堆.md) -- [第十五节 哈希](docs/AimForOffer/15_哈希.md) - -## 💻 LeetCode 题解 - -- [第一节 数组问题](docs/LeetCode/1_数组问题.md) -- [第二节 查找表相关问题](docs/LeetCode/2_查找表相关问题.md) -- [第三节 链表问题](docs/LeetCode/3_链表问题.md) -- [第四节 栈和队列](docs/LeetCode/4_栈和队列.md) -- [第五节 二叉树](docs/LeetCode/5_二叉树.md) -- [第六节 回溯法](docs/LeetCode/6_回溯法.md) -- [第七节 动态规划](docs/LeetCode/7_动态规划.md) -- [第八节 字符串](docs/LeetCode/8_字符串.md) -- [第九节 数学问题](docs/LeetCode/9_数学问题.md) -- [第十节 数据结构设计](docs/LeetCode/10_数据结构设计.md) -- [第十一节 数据结构](docs/LeetCode/11_数据结构.md) -- [第十二节 常见算法思想](docs/LeetCode/12_算法思想.md) - -## 🔧 关注的小专栏 - -- [后端面试进阶指南](docs/https://xiaozhuanlan.com/CyC2018) -- [Java 面试进阶指南](docs/https://xiaozhuanlan.com/javainterview) +| ☕️ Java | 👫 面向对象 | 📝 编程题 | 💾 数据库 | 🎓 系统设计 | ☎️ 常用框架 | 📖 工具 | 📚 参考资料 | +| :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | +| Java | 面向对象 | 编程题 | 数据库 | 系统设计 | 常用框架 | 工具 | 参考资料 | diff --git "a/docs/1_\345\237\272\347\241\200.md" "b/docs/1_\345\237\272\347\241\200.md" new file mode 100644 index 00000000..406b2d57 --- /dev/null +++ "b/docs/1_\345\237\272\347\241\200.md" @@ -0,0 +1,99 @@ +# 系统设计基础 + +## 安全性 + +要求系统在应对各种攻击手段时能够有可靠的应对措施。 + +## 性能 + +### 性能指标 + +#### 响应时间 + +指某个请求从发出到接收到响应消耗的时间。 + +在对响应时间进行测试时,通常采用重复请求的方式,然后计算平均响应时间。 + +#### 吞吐量 + +指系统在单位时间内可以处理的请求数量,通常使用每秒的请求数来衡量。 + +#### 并发量 + +指系统能同时处理的并发用户请求数量。 + +在没有并发存在的系统中,请求被顺序执行,此时响应时间为吞吐量的倒数。例如系统支持的吞吐量为 100 req/s,那么平均响应时间应该为 0.01s。 + +目前的大型系统都支持多线程来处理并发请求,多线程能够提高吞吐量以及缩短响应时间,主要有两个原因: + +- 多 CPU +- IO 等待时间 + +使用 IO 多路复用等方式,系统在等待一个 IO 操作完成的这段时间内不需要被阻塞,可以去处理其它请求。通过将这个等待时间利用起来,使得 CPU 利用率大大提高。 + +并发用户数不是越高越好,因为如果并发用户数太高,系统来不及处理这么多的请求,会使得过多的请求需要等待,那么响应时间就会大大提高。 + +### 性能优化 + +#### 集群 + +将多台服务器组成集群,使用负载均衡将请求转发到集群中,避免单一服务器的负载压力过大导致性能降低。 + +#### 缓存 + +缓存能够提高性能的原因如下: + +- 缓存数据通常位于内存等介质中,这种介质对于读操作特别快; +- 缓存数据可以位于靠近用户的地理位置上; +- 可以将计算结果进行缓存,从而避免重复计算。 + +#### 异步 + +某些流程可以将操作转换为消息,将消息发送到**消息队列**之后立即返回,之后这个操作会被异步处理。 + + + +## 伸缩性 + +指不断向集群中添加服务器来缓解不断上升的用户并发访问压力和不断增长的数据存储需求。 + +### 伸缩性与性能 + +如果系统存在性能问题,那么单个用户的请求总是很慢的; + +如果系统存在伸缩性问题,那么单个用户的请求可能会很快,但是在并发数很高的情况下系统会很慢。 + +### 实现伸缩性 + +应用服务器只要不具有状态,那么就可以很容易地通过负载均衡器向集群中添加新的服务器。 + +关系型数据库的伸缩性通过 Sharding 来实现,将数据按一定的规则分布到不同的节点上,从而解决单台存储服务器的存储空间限制。 + +对于非关系型数据库,它们天生就是为海量数据而诞生,对伸缩性的支持特别好。 + +## 扩展性 + +指的是添加新功能时对现有系统的其它应用无影响,这就要求不同应用具备低耦合的特点。 + +实现可扩展主要有两种方式: + +- 使用消息队列进行解耦,应用之间通过消息传递进行通信; +- 使用分布式服务将业务和可复用的服务分离开来,业务使用分布式服务框架调用可复用的服务。新增的产品可以通过调用可复用的服务来实现业务逻辑,对其它产品没有影响。 + +## 可用性 + +### 冗余 + +保证高可用的主要手段是使用冗余,当某个服务器故障时就请求其它服务器。 + +应用服务器的冗余比较容易实现,只要保证应用服务器不具有状态,那么某个应用服务器故障时,负载均衡器将该应用服务器原先的用户请求转发到另一个应用服务器上,不会对用户有任何影响。 + +存储服务器的冗余需要使用主从复制来实现,当主服务器故障时,需要提升从服务器为主服务器,这个过程称为切换。 + +### 监控 + +对 CPU、内存、磁盘、网络等系统负载信息进行监控,当某个信息达到一定阈值时通知运维人员,从而在系统发生故障之前及时发现问题。 + +### 服务降级 + +服务降级是系统为了应对大量的请求,主动关闭部分功能,从而保证核心功能可用。 \ No newline at end of file diff --git "a/docs/1_\346\266\210\346\201\257\351\230\237\345\210\227.md" "b/docs/1_\346\266\210\346\201\257\351\230\237\345\210\227.md" new file mode 100644 index 00000000..6b758128 --- /dev/null +++ "b/docs/1_\346\266\210\346\201\257\351\230\237\345\210\227.md" @@ -0,0 +1,117 @@ +# 消息队列 + +消息队列中间件是**分布式系统**中重要的组件,主要用于:异步处理,应用解耦,流量削锋,消息通讯等问题,实现高性能,高可用,可伸缩和最终一致性架构。目前使用较多的消息队列有 ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ。 + +## 消息模型 + +### 1. 点对点 + +消息生产者向消息队列中发送了一个消息之后,只能被一个消费者消费一次。 + +
+ +### 2. 发布/订阅 + +消息生产者向频道发送一个消息之后,多个消费者可以从该频道订阅到这条消息并消费。 + +
+ +发布与订阅模式和观察者模式有以下不同: + +- 观察者模式中,观察者和主题都知道对方的存在;而在发布与订阅模式中,发布者与订阅者不知道对方的存在,它们之间通过频道进行通信。 +- 观察者模式是同步的,当事件触发时,主题会调用观察者的方法,然后等待方法返回;而发布与订阅模式是异步的,发布者向频道发送一个消息之后,就不需要关心订阅者何时去订阅这个消息,可以立即返回。 + +
+ +## 使用场景 + +### 1、异步处理 + +发送者将消息发送给消息队列之后,**不需要同步等待消息接收者处理完毕,而是立即返回进行其它操作**。消息接收者从消息队列中订阅消息之后异步处理。 + +比如:用户注册时,需要发注册邮件和注册短信。传统的做法有两种: + +> 1、串行方式 + +将注册信息写入数据库成功后,发送注册邮件,再发送注册短信。以上三个任务全部完成后,返回给客户端。 + +![img](https://gitee.com/duhouan/ImagePro/raw/master/MessageQueuue/mq_4.png) + +> 2、并行方式 + +将注册信息写入数据库成功后,发送注册邮件的同时,发送注册短信。以上三个任务完成后,返回给客户端。与串行的差别是,并行的方式可以减少处理的时间。 + +![img](https://gitee.com/duhouan/ImagePro/raw/master/MessageQueuue/mq_5.png) + +引入消息队列,将不是必须的业务逻辑异步处理,注册邮件,发送短信写入消息队列后,直接返回,因此写入消息队列的速度很快,基本可以忽略,因此用户的响应时间可能是 50 ms。比串行提高了 3 倍,比并行提高了2 倍。 + +![img](https://gitee.com/duhouan/ImagePro/raw/master/MessageQueuue/mq_6.png) + +只有在**业务流程允许异步处理的情况**下才能这么做,例如上面的注册流程中,如果要求用户对验证邮件进行点击之后才能完成注册的话,就不能再使用消息队列。 + +### 2、应用解耦 + +如果模块之间不直接进行调用,模块之间耦合度就会很低,那么修改一个模块或者新增一个模块对其它模块的影响会很小,从而实现可扩展性。 + +通过使用消息队列,一个模块只需要向消息队列中发送消息,其它模块可以**选择性地从消息队列中订阅消息**从而完成调用。 + +### 3、流量削峰 + +在高并发的场景下,如果短时间有大量的请求到达会压垮服务器。 + +可以将请求发送到消息队列中,服务器按照其处理能力从消息队列中订阅消息进行处理。 + +### 4、消息通讯 + +消息队列一般都内置了高效的通信机制,因此也可以用在纯消息通讯。比如实现点对点消息队列,或者聊天室等,也就是消息队列的两种消息模式:点对点或发布 / 订阅模式。 + + + +## 消费方式 + +在 JMS (Java Message Service,Java 消息服务) 中,消息的产生和消费都是异步的。对于消费来说,JMS 的消费者可以通过两种方式来消费消息:同步方式和异步方式。 + +### 1、同步方式 + +订阅者或消费者通过 receive() 方法来接收消息,receive() 方法在接收到消息之前(或超时之前)将一直阻塞。 + +### 2、异步方式 + +订阅者或消费者可以注册为一个消息监听器。当消息到达之后,系统自动调用监听器的 onMessage() 方法。 + + + +## 可靠性 + +### 发送端的可靠性 + +发送端完成操作后一定能将消息成功发送到消息队列中。 + +实现方法:在本地数据库建一张消息表,将**消息数据与业务数据保存在同一数据库实例**里,这样就可以利用本地数据库的**事务机制**。事务提交成功后,将消息表中的消息转移到消息队列中,若转移消息成功则删除消息表中的数据,否则继续重传。 + +### 接收端的可靠性 + +接收端能够从消息队列成功消费一次消息。 + +两种实现方法: + +- 保证接收端处理消息的业务逻辑具有幂等性:只要具有幂等性,那么消费多少次消息,最后处理的结果都是一样的。(幂等性:**被执行一次与连续执行多次的效果是一样的**。) +- 保证消息具有唯一编号,并使用一张日志表来记录已经消费的消息编号。 + + + +## 带来的问题 + +### 1、系统可用性降低 + +系统可用性在某种程度上降低,为什么这样说呢? + +在加入消息队列之前,不用考虑消息丢失或者说消息队列挂掉等等的情况,但是,引入消息队列之后你就需要去考虑了。 + +### 2、系统复杂性提高 + +加入消息队列之后,你需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺序性等问题。 + +### 3、一致性问题 + +消息队列可以实现异步,消息队列带来的异步确实可以提高系统响应速度。但是,消息的真正消费者并没有正确消费消息,就会导致数据不一致的情况。 \ No newline at end of file diff --git "a/docs/AimForOffer/10_\346\267\261\345\272\246\344\274\230\345\205\210.md" "b/docs/AimForOffer/10_\346\267\261\345\272\246\344\274\230\345\205\210.md" deleted file mode 100644 index 60cf3bd7..00000000 --- "a/docs/AimForOffer/10_\346\267\261\345\272\246\344\274\230\345\205\210.md" +++ /dev/null @@ -1,329 +0,0 @@ -# 深度优先 - -## 1、岛屿数量 - -[岛屿数量](https://leetcode-cn.com/problems/number-of-islands/) - -```java -// 思路:典型的 flood-fill 算法 -private int m,n; - -private boolean[][] visited; - -private boolean inArea(int x,int y){ - return (x>=0 && x=0 && y=0 && x=0 && y=0 && x=0 && y0){ - sum += num %10; - num/=10; - } - return sum; -} - -private boolean valid(int threshlod ,int x,int y){ - return (getNum(x)+getNum(y))<=threshlod; -} - -private int walk(int threshold,int startx,int starty){ - visited[startx][starty]=true; - int walks = 0; - if(valid(threshold,startx,starty)){ - walks=1; - } - for(int i=0;i<4;i++){ - int newX = startx+d[i][0]; - int newY = starty+d[i][1]; - if(inArea(newX,newY)){ - if(!visited[newX][newY] && valid(threshold,newX,newY)){ - walks += walk(threshold,newX,newY); - } - } - } - return walks; -} - -public int movingCount(int threshold, int rows, int cols) { - if(threshold<0){ //threshold<0,则机器人就不能走了 - return 0; - } - m = rows; - if(m==0){ - return 0; - } - n= cols; - visited = new boolean[m][n]; - return walk(threshold,0,0); -} -``` - - - -## 4、被围绕的区域 - -[被围绕的区域](https://leetcode-cn.com/problems/surrounded-regions/) - -```java -private int m,n; - -private boolean[][] visited; - -private int[][] d={ - {-1,0}, - {0,1}, - {1,0}, - {0,-1} -}; - -private boolean inArea(int x,int y){ - return (x>=0 && x=0 && y=0 && x=0 && y=matrix[startx][starty]){ - dfs(matrix,newX,newY,visited); - } - } - } -} - -public List> pacificAtlantic(int[][] matrix) { - List> res=new ArrayList<>(); - m=matrix.length; - if(m==0){ - return res; - } - n=matrix[0].length; - pacific=new boolean[m][n]; - atlantic=new boolean[m][n]; - - for(int j=0;j list=new ArrayList<>(); - for(int i=0;i() { //按照其实位置进行排序 - @Override - public int compare(Interval o1, Interval o2) { - int num=o1.start-o2.start; - int num2=(num==0)?(o1.end-o2.end):num; - return num2; - } - }); - - ArrayList res=new ArrayList<>(); - Interval cur=list.get(0); - for(int i=1;i=list.get(i).start){ //存在区间交叉 - //区间进行合并 - cur.end=Math.max(cur.end,list.get(i).end); - }else{ //不存在交叉 - res.add(cur); - cur=list.get(i); - } - if(i==m-1){ - res.add(cur); - } - } - - int n=res.size(); - int[][] matrix=new int[n][2]; - for(int i=0;i= 2,则表示未多个 -// 所以只需要 2 位就可表示字符出现的次数,即 0次(00)、1次(01)或者多次(11) - -public int FirstNotRepeatingChar(String str) { - BitSet bitSet = new BitSet(256); - BitSet bitSet2 = new BitSet(256); - - for(char c:str.toCharArray()){ - if(!bitSet.get(c) && !bitSet2.get(c)){ //00 - // c 原来出现次数 0次,加入 c 后出现次数为 1 - bitSet2.set(c); - } - else if(!bitSet.get(c) && bitSet2.get(c)){ //01 - // c 原来出现次数 1 次,加入 c 后出现次数为 3 (3 表示多次) - bitSet.set(c); - } - } - - for(int i=0;i records=new HashMap<>(); - for(int i=0;i 就是错误的 - if(records.containsValue(s)){ - return false; - } - records.put(c,s); - } - } - return true; -} -``` - - - -## 3、两个数组的交集 - -[两个数组的交集](https://leetcode-cn.com/problems/intersection-of-two-arrays/) - -```java -public int[] intersection(int[] nums1, int[] nums2) { - if(nums1==null || nums2==null){ - return new int[]{}; - } - - HashSet record=new HashSet<>(); - for(int num:nums1){ - record.add(num); - } - - HashSet resRecord=new HashSet<>(); - for(int num:nums2){ - if(record.contains(num)){ - resRecord.add(num); - } - } - - int[] res=new int[resRecord.size()]; - int index=0; - for(int num:resRecord){ - res[index++]=num; - } - return res; -} -``` - - - -## 4、两个数组的交集 II - -[两个数组的交集 II](https://leetcode-cn.com/problems/intersection-of-two-arrays-ii/) - -```java -public int[] intersect(int[] nums1, int[] nums2) { - if(nums1==null || nums2==null){ - return new int[]{}; - } - - //统计 nums1 中数字出现的次数 - HashMap record=new HashMap<>(); - for(int num:nums1){ - int freq=record.getOrDefault(num,0); - record.put(num,++freq); - } - - ArrayList resRecord=new ArrayList<>(); - for(int num:nums2){ - int freq=record.getOrDefault(num,0); //获取 num 的出现次数 - if(freq>0){ - resRecord.add(num); - record.put(num,--freq); - } - } - - int[] res=new int[resRecord.size()]; - int index=0; - for(int num:resRecord){ - res[index++]=num; - } - return res; -} -``` - - - -## 5、两数之和 - -[两数之和](https://leetcode-cn.com/problems/two-sum/) - -```java -public int[] twoSum(int[] nums, int target) { - - //record : key 记录元素值,value 记录元素出现的下标 - HashMap record=new HashMap<>(); - - for(int i=0;i key 记录数字,value 记录该数字出现次数 - HashMap record=new HashMap<>(); - - for(int k=0;k k 记录距离的平方 v 记录出现的次数 - HashMap record=new HashMap<>(); - for(int j=0;j到其他点的距离出现的次数 - //如果到点i的距离相等的点 >=2(假设为n),则有n*(n-1)种可能 - for(Integer key:record.keySet()){ - int freq=record.getOrDefault(key,0); - if(freq>=2){ - res+=freq*(freq-1); - } - } - } - return res; -} - -//计算点 i 和 点 j 之间的距离的平方 -private int distance(int[][] points,int i,int j){ - int x1=points[i][0],y1=points[i][1]; - int x2=points[j][0],y2=points[j][1]; - return (x1-x2)*(x1-x2)+(y1-y2)*(y1-y2); -} -``` - - - -## 8、存在重复元素 II - -[存在重复元素 II](https://leetcode-cn.com/problems/contains-duplicate-ii/) - -```java -//思路: -// 滑动窗口和 set 的综合使用: -// 维持一个长度为k的滑动窗口,只要在该窗口查找是否有两个相等的元素。 -// 同时窗口不断向前滑行。 -public boolean containsNearbyDuplicate(int[] nums, int k) { - //维护一个长度为 k 的窗口,set.size()长度为滑动窗口 - HashSet set=new HashSet<>(); - - for(int i=0;i x=i-k - set.remove(nums[i-k]); //删除该滑动窗口最左边元素 - } - } - return false; -} -``` - - - -## 9、存在重复元素 III - -[存在重复元素 III](https://leetcode-cn.com/problems/contains-duplicate-iii/) - -```java -//思路: -//nums [i] 和 nums [j] 的差的绝对值最大为 |nums[i]-nums[j]|<=t -//则对 nums[j] 有 (nums[i]-t) <= nums[j]<= (nums[i]+t) -//维持一个长度为k的滑动窗口,只要在该窗口查找是否存在元素在[nums[i]-t,nums[i]+t] 之间。 -//同时窗口不断向前滑行。 -//注意 k 和 t 的值,显然 k<1 或者 t<0,可以直接返回 false - -public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) { - if (k < 1 || t < 0){ - return false; - } - - //维护一个长度为 k 的有序的滑动窗口 - //这里使用 Long 类型,以为 nums[i]+t 有可能超过 int 范围 - SortedSet set=new TreeSet<>(); - for(int i=0;i subSet=set.subSet((long)(nums[i]-t),(long)(nums[i]+t+1)); - //nums[i]+t+1 因为是左闭右开的 - if(!subSet.isEmpty()){ - return true; - } - set.add((long)nums[i]); - if(set.size()==k+1){ - set.remove((long)nums[i-k]); - } - } - return false; -} -``` - diff --git "a/docs/AimForOffer/6_\346\240\210.md" "b/docs/AimForOffer/6_\346\240\210.md" deleted file mode 100644 index 15a07ef0..00000000 --- "a/docs/AimForOffer/6_\346\240\210.md" +++ /dev/null @@ -1,264 +0,0 @@ -# 栈 - -## 1、用两个栈实现队列(19) - -[用两个栈实现队列](https://www.nowcoder.com/practice/54275ddae22f475981afa2244dd448c6?tpId=13&tqId=11158&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -```java -Stack stack1 = new Stack(); -Stack stack2 = new Stack(); - -public void push(int node) { - stack1.push(node); -} - -public int pop() { - if(stack2.isEmpty()){ - while (!stack1.isEmpty()){ - stack2.push(stack1.pop()); - } - } - return stack2.pop(); -} -``` - - - -## *2、包含 min 函数的栈(35) - -[包含 min 函数的栈](https://www.nowcoder.com/practice/4c776177d2c04c2494f2555c9fcc1e49?tpId=13&tqId=11173&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -```java -private Stack stack = new Stack<>(); -private Stack minStack = new Stack<>(); - -public void push(int node) { - stack.push(node); - if(minStack.isEmpty() || node stack=new Stack<>(); - - for(int pushIndex=0,popIndex=0;pushIndex stack=new Stack<>(); - for(int i=0;i preorderTraversal(TreeNode root) { - List res=new ArrayList<>(); - if(root==null){ - return res; - } - - Stack stack=new Stack<>(); - stack.push(new StackNode(root,Command.GO)); - - //前序遍历,利用栈模拟递归方式 - while(!stack.isEmpty()){ - StackNode stackNode=stack.pop(); - TreeNode treeNode=stackNode.node; - Command command=stackNode.command; - if(command.equals(Command.PRINT)){ - res.add(treeNode.val); - }else{ // Command.GO 继续访问 - if(treeNode.right!=null){ - stack.push(new StackNode(treeNode.right,Command.GO)); - } - if(treeNode.left!=null){ - stack.push(new StackNode(treeNode.left,Command.GO)); - } - stack.push(new StackNode(treeNode,Command.PRINT)); - } - } - return res; -} -``` - - - -## 6、二叉树的中序遍历 - -[二叉树的中序遍历](https://leetcode-cn.com/problems/binary-tree-inorder-traversal/) - -```java -private enum Command{GO,PRINT}; - -private class StackNode{ - TreeNode node; - Command command; - public StackNode(TreeNode node,Command command){ - this.node=node; - this.command=command; - } -} - -public List inorderTraversal(TreeNode root) { - List res=new ArrayList<>(); - if(root==null){ - return res; - } - Stack stack=new Stack<>(); - stack.push(new StackNode(root,Command.GO)); - - while(!stack.isEmpty()){ - StackNode stackNode=stack.pop(); - TreeNode treeNode=stackNode.node; - Command command=stackNode.command; - if(command.equals(Command.PRINT)){ - res.add(treeNode.val); - }else{ - if(treeNode.right!=null){ - stack.push(new StackNode(treeNode.right,Command.GO)); - } - stack.push(new StackNode(treeNode,Command.PRINT)); - if(treeNode.left!=null){ - stack.push(new StackNode(treeNode.left,Command.GO)); - } - } - } - return res; -} -``` - - - -## 7、二叉树的后序遍历 - -[ 二叉树的后序遍历](https://leetcode-cn.com/problems/binary-tree-postorder-traversal/) - -```JAVA -private enum Command{GO,PRINT}; - -private class StackNode{ - TreeNode node; - Command command; - public StackNode(TreeNode node,Command command){ - this.node=node; - this.command=command; - } -} - -public List postorderTraversal(TreeNode root) { - List res=new ArrayList<>(); - if(root==null){ - return res; - } - - Stack stack=new Stack<>(); - stack.push(new StackNode(root,Command.GO)); - - while(!stack.isEmpty()){ - StackNode stackNode=stack.pop(); - TreeNode treeNode=stackNode.node; - Command command=stackNode.command; - if(command.equals(Command.PRINT)){ - res.add(treeNode.val); - }else{ - stack.push(new StackNode(treeNode,Command.PRINT)); - if(treeNode.right!=null){ - stack.push(new StackNode(treeNode.right,Command.GO)); - } - if(treeNode.left!=null){ - stack.push(new StackNode(treeNode.left,Command.GO)); - } - } - } - return res; -} -``` - diff --git "a/docs/AimForOffer/9_\345\233\236\346\272\257.md" "b/docs/AimForOffer/9_\345\233\236\346\272\257.md" deleted file mode 100644 index 9248774a..00000000 --- "a/docs/AimForOffer/9_\345\233\236\346\272\257.md" +++ /dev/null @@ -1,591 +0,0 @@ -# 回溯 - -## 1、二叉树中和为某一值的路径(39) - -[二叉树中和为某一值的路径](https://www.nowcoder.com/practice/b736e784e3e34731af99065031301bca?tpId=13&tqId=11177&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -```java -public ArrayList> FindPath(TreeNode root, int target) { - ArrayList> res = new ArrayList<>(); - ArrayList values = new ArrayList<>(); - backtrack(root,target,values,res); - return res; -} - -// values : 记录从根节点到叶子节点的所有路径 -// paths : 存储所有可能的结果 -private void backtrack(TreeNode root, int target, - ArrayList values, - ArrayList> paths) { - if (root == null) { - return; - } - values.add(root.val); - if(root.left==null && root.right==null && root.val==target){ - paths.add(new ArrayList<>(values)); - }else{ - backtrack(root.left,target-root.val,values,paths); - backtrack(root.right,target-root.val,values,paths); - } - values.remove(values.size()-1); -} -``` - - - -## 2、矩阵中的路径(65) - -[矩阵中的路径](https://www.nowcoder.com/practice/c61c6999eecb4b8f88a98f66b273a3cc?tpId=13&tqId=11218&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -```java -//思路:典型的回溯法思想 -private int m,n; //矩阵的长度、宽度 -private boolean[][] visted; //标记是否访问 - -private int[][] d={ - {0,1}, //向右 - {-1,0},//向上 - {0,-1},//向左 - {1,0} //向下 -}; - -private boolean inArea(int x,int y){ - return (x>=0 && x=0 && y=0 && x=0 && y - -```java -private List> res; - -private boolean[] visited; - -public List> permute(int[] nums) { - res = new ArrayList<>(); - if(nums==null || nums.length==0){ - return res; - } - visited = new boolean[nums.length]; - List p =new ArrayList<>(); - generatePermutation(nums,0,p); - return res; -} - -//p中保存一个有 index 的元素的排列 -//向这个排列的末尾添加第 (index+1) 个元素,组成有(index+1) 个元素排列 -private void generatePermutation(int[] nums,int index,List p){ - if(index==nums.length){ - res.add(new ArrayList<>(p)); - return; - } - for(int i=0;i> res; - -private boolean[] visited; - -public List> permuteUnique(int[] nums) { - res = new ArrayList<>(); - if(nums==null || nums.length==0){ - return res; - } - visited = new boolean[nums.length]; - Arrays.sort(nums); - List p = new ArrayList<>(); - findUniquePermutation(nums,0,p); - return res; -} - -//p 保存的是有 index 元素的排列 -//向这个排列的末尾添加第 (index+1) 个元素,组成有(index+1) 个元素排列 -private void findUniquePermutation(int[] nums,int index,List p){ - if(index==nums.length){ - res.add(new ArrayList<>(p)); - return; - } - for(int i=0;i0 && nums[i-1]==nums[i] && - !visited[i-1]){ //注意:相邻元素相同,并且都未被访问 - continue; - } - visited[i]=true; - p.add(nums[i]); - findUniquePermutation(nums,index+1,p); - p.remove(p.size()-1); - visited[i]=false; - } - } - return; -} -``` - - - -## *7、字符串的排列 - -[字符串的排列](https://www.nowcoder.com/practice/fe6b651b66ae47d7acce78ffdd9a96c7?tpId=13&tqId=11180&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) - -```java -private ArrayList res; -private boolean[] visited; - -public ArrayList Permutation(String str) { - res = new ArrayList<>(); - if(str==null || str.length()==0){ - return res; - } - char[] chs = str.toCharArray(); - Arrays.sort(chs); //方便后面的去重处理 - visited = new boolean[str.length()]; - permute(chs,0,new StringBuilder()); - return res; -} - -//产生排列 -//p中保存一个存在index个元素的排列 -//向这个排列的末尾添加第(index+1)个元素,获得包含(index+1)个元素的排列 -private void permute(char[] chs,int index,StringBuilder p){ - if(index==chs.length){ - res.add(p.toString()); - return; - } - for(int i=0;i0 && chs[i-1]==chs[i] && !visited[i-1]){ - continue; - } - if(!visited[i]){ - p.append(chs[i]); - visited[i] = true; - permute(chs,index+1,p); - p.deleteCharAt(p.length()-1); - visited[i] = false; - } - } -} -``` - - - -## 8、字母大小写全排列 - -[字母大小写全排列](https://leetcode-cn.com/problems/letter-case-permutation/) - -```java -private List res; - -//处理 index 位置的数据 -//如果 index 位置是字母的话,则替换 -private void replaceLetter(int index,StringBuilder p){ - if(index==p.length()){ - res.add(p.toString()); - return; - } - char ch=p.charAt(index); - if(Character.isLetter(ch)){ - p.setCharAt(index,Character.toUpperCase(ch)); - replaceLetter(index+1,p); - p.setCharAt(index,Character.toLowerCase(ch)); - replaceLetter(index+1,p); - }else{ - replaceLetter(index+1,p); - } - return; -} - -public List letterCasePermutation(String S) { - res=new ArrayList<>(); - if(S==null || S.length()==0){ - return res; - } - StringBuilder p=new StringBuilder(S); - replaceLetter(0,p); - return res; -} -``` - - - -## 9、电话号码的字母组合 - -[电话号码的字母组合](https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/) - -```java -private final String[] letterMap={ - " ",//0 - "",//1 - "abc",//2 - "def",//3 - "ghi",//4 - "jkl",//5 - "mno",//6 - "pqrs",//7 - "tuv",//8 - "wxyz"//9 -}; - -private List res; -//s 中保留了从 digits[0...index-1] 翻译得到的字符串 -//寻找和 digits[index] 匹配的字母,获取 digits[0...index] 翻译得到的解 -private void findCombination(String digits,int index,String s){ - if(index==digits.length()){ - res.add(s); - return; - } - - char c = digits.charAt(index); - String letters = letterMap[c-'0']; // 翻译字符串 - for(int i=0;i letterCombinations(String digits) { - res = new ArrayList<>(); - if(digits==null || digits.length()==0){ - return res; - } - findCombination(digits,0,""); - return res; -} -``` - - - -## 10、组合 - -[组合](https://leetcode-cn.com/problems/combinations/) - -```java -//思路一:暴力解法 - -private List> res; - -//c存储已经找到的组合 -//从start开始搜索新的元素 -private void findCombination(int n,int k,int start,List c){ - if(k==c.size()){ - res.add(new ArrayList<>(c)); - return; - } - for(int i=start;i<=n;i++){ - c.add(i); - findCombination(n,k,i+1,c); - c.remove(c.size()-1); - } - return; -} - -public List> combine(int n, int k) { - res=new ArrayList<>(); - if(n<=0 || k<=0 || n c=new ArrayList<>(); - findCombination(n,k,1,c); - return res; -} -``` - - - -```java -//思路二:优化,进行剪枝 -private List> res; - -//c存储已经找到的组合 -//从start开始搜索新的元素 -private void findCombination(int n,int k,int start,List c){ - if(k==c.size()){ - res.add(new ArrayList<>(c)); - return; - } - - //TODO:优化--剪枝 - //c存储的是已经找到的组合。 - //此时还剩下k-c.size()个空位, - //则 [i...n]之间的元素最少要有 k-c.size() 个,即 n-i+1 >= k-c.size() - //所以有 i <= n-(k-c.size())+1 - - for(int i=start;i<=n-(k-c.size())+1;i++){ - c.add(i); - findCombination(n,k,i+1,c); - c.remove(c.size()-1); - } - return; -} - -public List> combine(int n, int k) { - res=new ArrayList<>(); - if(n<=0 || k<=0 || n c=new ArrayList<>(); - findCombination(n,k,1,c); - return res; -} -``` - - - -## 11、N皇后 - -[N皇后](https://leetcode-cn.com/problems/n-queens/) - -
- -
- -- 思路: - -
- -
- -判断对角线不合法的情况: - -(1)竖向:col[i] 表示第 i 列被占用 - -(2)对角线1:dai1[i] 表示第 i 条对角线被1占用 - -
- -(3)对角线2:dai2[i]表示第i对角线被2占用 - -
- -```java -private List> res; - -//用于判断是否在同一竖线上,因为index表示行数,是变化的,所以不用判断是否在相同行 -private boolean[] cols; -//判断是否在1类对角线上,相应坐标 i+j -private boolean[] dial1; -//判断是否在2类对角线上,相应坐标 i-j+n-1 -private boolean[] dial2; - -//在 index 行放皇后 -//row 记录在 index 行能够放皇后的位置 -//比如 row.get(0) = 1,就表示在棋盘的 [0,1] 位置放上皇后 -private void putQueen(int n,int index,List row){ - if(index==n){ - res.add(generateBoard(n,row)); - return; - } - // [index,j] 放皇后 - for(int j=0;j generateBoard(int n, List row) { - List res=new ArrayList<>(); - if(n<=0){ - return res; - } - - char[][] board=new char[n][n]; - for(int i=0;i> solveNQueens(int n) { - res=new ArrayList<>(); - if(n==0){ - return res; - } - cols=new boolean[n]; - dial1=new boolean[2*n-1]; - dial2=new boolean[2*n-1]; - List row=new ArrayList<>(); - putQueen(n,0,row); - return res; -} -``` - - - -## 12、N皇后 II - -[N皇后 II](https://leetcode-cn.com/problems/n-queens-ii/) - -```java - -``` - - - -## 13、解数独 - -[解数独](https://leetcode-cn.com/problems/sudoku-solver/) - -```java - -``` - diff --git "a/docs/AimForOffer/1_\346\225\260\347\273\204.md" "b/docs/AimForOffer/\346\225\260\346\215\256\347\273\223\346\236\204\347\233\270\345\205\263/1_\346\225\260\347\273\204\345\222\214\347\237\251\351\230\265.md" similarity index 60% rename from "docs/AimForOffer/1_\346\225\260\347\273\204.md" rename to "docs/AimForOffer/\346\225\260\346\215\256\347\273\223\346\236\204\347\233\270\345\205\263/1_\346\225\260\347\273\204\345\222\214\347\237\251\351\230\265.md" index 8c53efc6..6d56d3ec 100644 --- "a/docs/AimForOffer/1_\346\225\260\347\273\204.md" +++ "b/docs/AimForOffer/\346\225\260\346\215\256\347\273\223\346\236\204\347\233\270\345\205\263/1_\346\225\260\347\273\204\345\222\214\347\237\251\351\230\265.md" @@ -1,6 +1,6 @@ -# 数组 +# 数组和矩阵 -## 1、二维数组中的查找(1) +## 1、二维数组中的查找 [二维数组中的查找](https://www.nowcoder.com/practice/abc3fe2ce8e146608e868a70efebf62e?tpId=13&tqId=11154&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -30,7 +30,7 @@ public boolean Find(int target, int [][] array) { -## 2、数组中重复的数字(2) +## 2、数组中重复的数字 [数组中重复的数字](https://www.nowcoder.com/practice/623a5ac0ea5b4e5f95552655361ae0a8?tpId=13&tqId=11203&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -61,24 +61,41 @@ private void swap(int[] nums,int i,int j){ -## *3、构建乘积数组(3) +## *3、构建乘积数组 [构建乘积数组](https://www.nowcoder.com/practice/94a4d381a68b47b7a8bed86f2975db46?tpId=13&tqId=11204&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) ```java +/** + 思路: + B0 | 1 A1 A2 A3 A4 ... A(n-2) A(n-1) + B1 | A0 1 A2 A3 A4 ... A(n-2) A(n-1) + B2 | A0 A1 1 A3 A4 ... A(n-2) A(n-1) + B3 | A0 A1 A2 1 A4 ... A(n-2) A(n-1) + B4 | A0 A1 A2 A3 1 ... A(n-2) A(n-1) + ... + B(n-2) | A0 A1 A2 A3 A4 ... 1 A(n-1) + B(n-1) | A0 A1 A2 A3 A4 ... A(n-2) 1 +*/ public int[] multiply(int[] A) { - int n = A.length; - int[] B = new int[n]; + if(A==null || A.length==0){ + return null; + } + + int n=A.length; + int[] B=new int[n]; - //B[i] = A[0]*A[1]*...*A[i-1] - int product =1; - for(int i=0;i=0;product*=A[i],i--){ - B[i]*= product; + + //计算右上角乘积 + int product=1; + for(int i=n-2;i>=0;i--){ + product*=A[i+1]; + B[i]=B[i]*product; //最后是获取 B[0]=A[1]*A[2]*A[3]*...*A[n-1] } return B; } @@ -86,7 +103,7 @@ public int[] multiply(int[] A) { -## *4、调整数组顺序使奇数位于偶数前面(28) +## *4、调整数组顺序使奇数位于偶数前面 [调整数组顺序使奇数位于偶数前面](https://www.nowcoder.com/practice/beb5aa231adc45b2a5dcc5b62c93f593?tpId=13&tqId=11166&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -150,10 +167,6 @@ public void reOrderArray(int [] array) { } } } - - for(int num : array){ - System.out.println(num); - } } private void swap(int[] nums,int i,int j){ @@ -170,7 +183,7 @@ private boolean isEven(int num){ -## 5、顺时针打印矩阵(34) +## 5、顺时针打印矩阵 [顺时针打印矩阵](https://www.nowcoder.com/practice/9b4c81a02cd34f76be2659fa0d54342a?tpId=13&tqId=11172&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -210,7 +223,7 @@ public ArrayList printMatrix(int [][] matrix) { -## 6、连续子数组的最大和(45) +## 6、连续子数组的最大和 [连续子数组的最大和](https://www.nowcoder.com/practice/459bd355da1549fa8a49e350bf3df484?tpId=13&tqId=11183&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -275,7 +288,7 @@ public int FindGreatestSumOfSubArray(int[] array) { -## 7、把数组排成最小的数(47) +## 7、把数组排成最小的数 [把数组排成最小的数](https://www.nowcoder.com/practice/8fecd3f8ba334add803bf2a06af1b993?tpId=13&tqId=11185&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -418,297 +431,3 @@ public boolean isContinuous(int [] numbers) { return cnt>=0; } ``` - - - -## 11、至少是其他数字两倍的最大数 - -[至少是其他数字两倍的最大数](https://leetcode-cn.com/problems/largest-number-at-least-twice-of-others/) - -```java -public int dominantIndex(int[] nums) { - //获取数组中的最大值和次大值 - int max1=Integer.MIN_VALUE; - int max2=Integer.MIN_VALUE; - - int res=-1; - - for(int i=0;imax1){ - res=i; - max2=max1; - max1=num; - }else if(num>max2 && num!=max1){ // max2 < num < max1 - max2=num; - } - } - - if(2*max2<=max1){ - return res; - } - return -1; -} -``` - - - -## 12、移动零 - -[移动零](https://leetcode-cn.com/problems/move-zeroes/) - -```java -// 思路一: -// 1、引入另外一个指针 k, 用于指向数组中非0元素(原有一个遍历数组的指针i),很显然k <= nums.length-1 -// 2、则 nums 的前 k 个都是非0元素了,剩下的元素就是 0 元素 - -public void moveZeroes(int[] nums) { - if(nums==null || nums.length==0){ - return; - } - - int k=0; - for(int num:nums){ - if(num!=0){ - nums[k++]=num; - } - } - for(int i=k;i=s){ - res=Math.min(res,(r-l)+1); - } - } - - if(res==n+1){ - //res 仍然是 (n+1),则说明,不存在 sum>=s 的情况,直接返回 0 - return 0; - } - return res; -} -``` - - - -```java -// 写法二:[l,r) 滑动窗口 - -public int minSubArrayLen(int s, int[] nums) { - int n=nums.length; - - int l=0; - int r=0; - //[l,r) 表示滑动窗口,初始时不包括任何数据 - int sum=0;//记录滑动窗口中的元素值 - - int res=n+1; - while(l=s){ - res=Math.min(res,r-l);//[l,r) 中窗口长度为 r-l - } - } - if(res==n+1){ - return 0; - } - return res; -} -``` - - - -## 15、无重复字符的最长子串 - -[无重复字符的最长子串](https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/) - -```java -// 写法一:[l,r] 滑动窗口 - -public int lengthOfLongestSubstring(String s) { - if(s==null || s.length()==0){ - return 0; - } - - int n=s.length(); - int l=0; - int r=-1; - //[l,r] 区间内没有重复元素 - - //统计[l,r] 区间内字符出现的次数 - int[] freq=new int[256]; - - int res=1; - while(l=0 || j>=0){ - c+=(i>=0)?chs1[i--]-'0':0; - c+=(j>=0)?chs2[j--]-'0':0; - res.append(c%10); - c/=10; - } - if(c==1){ - res.append(1); - } - return res.reverse().toString(); -} -``` - - +思路一:朴素模式匹配算法 -## *12、KMP 算法 +最朴素的方法就是依次从待匹配串的每一个位置开始,逐一与模版串匹配, +因为最多检查 (n - m)个位置,所以方法的复杂度为 O(m*(n-1))。 -[实现 strStr()](https://leetcode-cn.com/problems/implement-strstr/) +
```java +//haystack 是待匹配串 +//needle 是模板串 public int strStr(String haystack, String needle) { - int m=haystack.length(); - int n=needle.length(); - - if(n==0){ // needle 是空字符串,默认返回 0 + if(needle==null || needle.length()==0){ return 0; } - int i=0,j=0; - int k=0; - while(i set=new HashSet<>(); - - for(int i=0;i=0;i--){ - dp[i][i]=1; // TODO:只有一个元素,最长回文子序列的长度为 1 - for(int j=i+1;j=0 && rmaxN){ //更新最长回文子串 - maxN=len; - startIndex=l; - endIndex=r; - } - //从中心向两边扩展,所以这里需要 l-- 并且 r++ - l--; - r++; - } + //说明模板串是待匹配串的子串 + if(j==needle.length()){ + //返回的是待匹配串的下标 + return i-needle.length(); } + return -1; +} - //处理 baab 这种情况,以 i,i+1 为中心向两边扩展 - for(int i=0;i=0 && rmaxN){ - maxN=len; - startIndex=l; - endIndex=r; - } - l--; - r++; +private int[] getNext(String needle) { + int[] next=new int[needle.length()]; + next[0]=-1; + int j=0,t=-1; + while (j printListFromTailToHead(ListNode listNode) { -## *2、链表中环的入口结点(9) +## *2、链表中环的入口结点 [链表中环的入口结点](https://www.nowcoder.com/practice/253d2c59ec3e4bc68da16833f79a38e4?tpId=13&tqId=11208&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -128,7 +128,7 @@ public ListNode removeElements(ListNode head, int val) { -## *4、删除链表中重复的结点(10) +## *4、删除链表中重复的结点 [删除链表中重复的结点](https://www.nowcoder.com/practice/fc533c45b73a41b0b44ccba763f866ef?tpId=13&tqId=11209&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -171,7 +171,7 @@ public ListNode deleteDuplication(ListNode pHead) { -## 5、链表中倒数第 k 个结点(29) +## 5、链表中倒数第 k 个结点 [链表中倒数第 k 个结点](https://www.nowcoder.com/practice/529d3ae5a407492994ad2a246518148a?tpId=13&tqId=11167&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -214,7 +214,7 @@ public ListNode FindKthToTail(ListNode head, int k) { -## 7、反转链表(30) +## 7、反转链表 [反转链表](https://www.nowcoder.com/practice/75e878df47f24fdc9dc3e400ec6058ca?tpId=13&tqId=11168&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -262,7 +262,7 @@ public ListNode ReverseList(ListNode head) { -## 8、合并两个排序的链表(31) +## 8、合并两个排序的链表 [合并两个排序的链表](https://www.nowcoder.com/practice/d8b6b4358f774294a89de2a6ac4d9337?tpId=13&tqId=11169&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -343,7 +343,7 @@ public ListNode Merge(ListNode list1, ListNode list2) { -## 9、二叉搜索树与双向链表(41) +## 9、二叉搜索树与双向链表 [二叉搜索树与双向链表](https://www.nowcoder.com/practice/947f6eb80d944a84850b0538bf0ec3a5?tpId=13&tqId=11179&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -386,7 +386,7 @@ private void inOrder(TreeNode node){ -## 10、两个链表的第一个公共结点(51) +## 10、两个链表的第一个公共结点 [两个链表的第一个公共结点](https://www.nowcoder.com/practice/6ab1d9a29e88450685099d45c9e31e46?tpId=13&tqId=11189&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -529,37 +529,5 @@ public ListNode deleteNode(ListNode head, ListNode tobeDelete) { -## 13、移除链表元素 - -[移除链表元素](https://leetcode-cn.com/problems/remove-linked-list-elements/) - -```java -// 思路:创建虚拟头结点 - - -``` - - - -## 14、两两交换链表中的节点 - -[两两交换链表中的节点](https://leetcode-cn.com/problems/swap-nodes-in-pairs/) - -```java - -``` - - - -## 15、删除链表中的节点 - -[删除链表中的节点](https://leetcode-cn.com/problems/delete-node-in-a-linked-list/) - -```java - -``` - - - diff --git "a/docs/AimForOffer/5_\346\240\221.md" "b/docs/AimForOffer/\346\225\260\346\215\256\347\273\223\346\236\204\347\233\270\345\205\263/4_\346\240\221.md" similarity index 96% rename from "docs/AimForOffer/5_\346\240\221.md" rename to "docs/AimForOffer/\346\225\260\346\215\256\347\273\223\346\236\204\347\233\270\345\205\263/4_\346\240\221.md" index 78ad779a..a49dc51d 100644 --- "a/docs/AimForOffer/5_\346\240\221.md" +++ "b/docs/AimForOffer/\346\225\260\346\215\256\347\273\223\346\236\204\347\233\270\345\205\263/4_\346\240\221.md" @@ -1,6 +1,6 @@ # 树 -## 1、重建二叉树(11) +## 1、重建二叉树 [重建二叉树](https://www.nowcoder.com/practice/8a19cbe657394eeaac2f6ea9b0f6fcf6?tpId=13&tqId=11157&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -34,7 +34,7 @@ private TreeNode reConstructBinaryTree(int[] pre,int[] in, -## 2、二叉树的下一个结点(12) +## 2、二叉树的下一个结点 [二叉树的下一个结点](https://www.nowcoder.com/practice/9023a0c988684a53960365b889ceaf5e?tpId=13&tqId=11210&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -79,7 +79,7 @@ public TreeLinkNode GetNext(TreeLinkNode pNode){ -## 3、对称的二叉树(13) +## 3、对称的二叉树 [对称的二叉树](https://www.nowcoder.com/practice/ff05d44dfdb04e1d83bdbdab320efbcb?tpId=13&tqId=11211&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -109,7 +109,7 @@ private boolean isSymmetrical(TreeNode p,TreeNode q){ -## 4、按之字形顺序打印二叉树(14) +## 4、按之字形顺序打印二叉树 [按之字形顺序打印二叉树](https://www.nowcoder.com/practice/91b69814117f4e8097390d107d2efbe0?tpId=13&tqId=11212&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -153,7 +153,7 @@ public ArrayList> Print(TreeNode pRoot) { -## *5、把二叉树打印成多行(15) +## *5、把二叉树打印成多行 [把二叉树打印成多行](https://www.nowcoder.com/practice/445c44d982d04483b04a54f298796288?tpId=13&tqId=11213&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -192,7 +192,7 @@ ArrayList> Print(TreeNode pRoot) { -## *6、序列化二叉树(16) +## *6、序列化二叉树 [序列化二叉树](https://www.nowcoder.com/practice/cf7e25aa97c04cc1a68c8f040e71fb84?tpId=13&tqId=11214&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -238,7 +238,7 @@ private TreeNode Deserialize(){ -## *7、树的子结构(32) +## *7、树的子结构 [树的子结构](https://www.nowcoder.com/practice/6e196c44c7004d15b1610b9afca8bd88?tpId=13&tqId=11170&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -279,7 +279,7 @@ private boolean isSubtree(TreeNode root1,TreeNode root2){ -## 8、从上往下打印二叉树(37) +## 8、从上往下打印二叉树 [从上往下打印二叉树](https://www.nowcoder.com/practice/7fe2212963db4790b57431d9ed259701?tpId=13&tqId=11175&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -312,7 +312,7 @@ public ArrayList PrintFromTopToBottom(TreeNode root) { -## 9、二叉树的深度(53) +## 9、二叉树的深度 [二叉树的深度](https://www.nowcoder.com/practice/435fb86331474282a3499955f0a41e8b?tpId=13&tqId=11191&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -424,7 +424,7 @@ private String buildPath(List values){ -## 13、二叉树中和为某一值的路径(39) +## 13、二叉树中和为某一值的路径 [二叉树中和为某一值的路径](https://www.nowcoder.com/practice/b736e784e3e34731af99065031301bca?tpId=13&tqId=11177&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -481,7 +481,7 @@ private void swap(TreeNode root){ -## *15、平衡二叉树(54) +## *15、平衡二叉树 [平衡二叉树](https://www.nowcoder.com/practice/8b3b95850edb4115918ecebdf1b4d222?tpId=13&tqId=11192&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -631,7 +631,7 @@ private void inOrder(TreeNode pRoot,int k){ -## *19、二叉搜索树的后序遍历序列(38) +## *19、二叉搜索树的后序遍历序列 [二叉搜索树的后序遍历序列](https://www.nowcoder.com/practice/a861533d45854474ac791d90e447bafd?tpId=13&tqId=11176&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -722,7 +722,7 @@ private boolean verify(int[] squence,int first,int last){ -## *20、二叉搜索树与双向链表(41) +## *20、二叉搜索树与双向链表 [二叉搜索树与双向链表](https://www.nowcoder.com/practice/947f6eb80d944a84850b0538bf0ec3a5?tpId=13&tqId=11179&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -761,12 +761,3 @@ private void inOrder(TreeNode node){ ``` - -## 21、路径总和 III - -[路径总和 III](https://leetcode-cn.com/problems/path-sum-iii/) - -```java - -``` - diff --git "a/docs/AimForOffer/\346\225\260\346\215\256\347\273\223\346\236\204\347\233\270\345\205\263/5_\346\240\210.md" "b/docs/AimForOffer/\346\225\260\346\215\256\347\273\223\346\236\204\347\233\270\345\205\263/5_\346\240\210.md" new file mode 100644 index 00000000..1bfbef76 --- /dev/null +++ "b/docs/AimForOffer/\346\225\260\346\215\256\347\273\223\346\236\204\347\233\270\345\205\263/5_\346\240\210.md" @@ -0,0 +1,121 @@ +# 栈 + +## 1、用两个栈实现队列 + +[用两个栈实现队列](https://www.nowcoder.com/practice/54275ddae22f475981afa2244dd448c6?tpId=13&tqId=11158&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +```java +Stack stack1 = new Stack(); +Stack stack2 = new Stack(); + +public void push(int node) { + stack1.push(node); +} + +public int pop() { + if(stack2.isEmpty()){ + while (!stack1.isEmpty()){ + stack2.push(stack1.pop()); + } + } + return stack2.pop(); +} +``` + + + +## *2、包含 min 函数的栈 + +[包含 min 函数的栈](https://www.nowcoder.com/practice/4c776177d2c04c2494f2555c9fcc1e49?tpId=13&tqId=11173&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +```java +private Stack stack = new Stack<>(); +private Stack minStack = new Stack<>(); + +public void push(int node) { + stack.push(node); + if(minStack.isEmpty() || node stack=new Stack<>(); + + for(int pushIndex=0,popIndex=0;pushIndex stack=new Stack<>(); + for(int i=0;i topKFrequent(int[] nums, int k) { - List res=new ArrayList<>(); - - //统计数组中元素出现次数 - HashMap records=new HashMap<>(); - for(int num:nums){ - int freq=records.getOrDefault(num,0); - records.put(num,++freq); - } - - PriorityQueue queue=new PriorityQueue<>(new Comparator() { - @Override - public int compare(EleFreq o1, EleFreq o2) { - int num=o1.freq-o2.freq; - int num2=(num==0?o2.num-o1.num:num); - return num2; - } - }); - - for(Integer num : records.keySet()){ - int freq=records.get(num); - queue.add(new EleFreq(num,freq)); - if(queue.size()==k+1){ - queue.poll(); - } - } - - while(!queue.isEmpty()){ - res.add(queue.poll().num); - } - return res; -} - -private class EleFreq{ - int num; - int freq; - public EleFreq(int num,int freq){ - this.num=num; - this.freq=freq; - } -} -``` - - - diff --git "a/docs/AimForOffer/14_\345\240\206.md" "b/docs/AimForOffer/\346\225\260\346\215\256\347\273\223\346\236\204\347\233\270\345\205\263/7_\345\240\206.md" similarity index 96% rename from "docs/AimForOffer/14_\345\240\206.md" rename to "docs/AimForOffer/\346\225\260\346\215\256\347\273\223\346\236\204\347\233\270\345\205\263/7_\345\240\206.md" index 8521dfd4..c5f503d0 100644 --- "a/docs/AimForOffer/14_\345\240\206.md" +++ "b/docs/AimForOffer/\346\225\260\346\215\256\347\273\223\346\236\204\347\233\270\345\205\263/7_\345\240\206.md" @@ -1,6 +1,6 @@ # 堆 -## 1、字符流中第一个不重复的字符(7) +## 1、字符流中第一个不重复的字符 [字符流中第一个不重复的字符](https://www.nowcoder.com/practice/00de97733b8e4f97a3fb5c680ee10720?tpId=13&tqId=11207&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -27,7 +27,7 @@ public char FirstAppearingOnce(){ -## 2、数据流中的中位数(18) +## 2、数据流中的中位数 [数据流中的中位数](https://www.nowcoder.com/practice/9be0172896bd43948f8a32fb954e1be1?tpId=13&tqId=11216&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -70,7 +70,7 @@ public Double GetMedian() { -## 3、滑动窗口的最大值(20) +## 3、滑动窗口的最大值 [滑动窗口的最大值](https://www.nowcoder.com/practice/1624bc35a45c42c0bc17d17fa0cba788?tpId=13&tqId=11217&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -139,7 +139,7 @@ public ArrayList maxInWindows(int [] num, int size){ -## 4、最小的 k 个数(44) +## 4、最小的 k 个数 [最小的 k 个数](https://www.nowcoder.com/practice/6a296eb82cf844ca8539b57c23e6e9bf?tpId=13&tqId=11182&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) diff --git "a/docs/AimForOffer/\346\225\260\346\215\256\347\273\223\346\236\204\347\233\270\345\205\263/8_\345\223\210\345\270\214.md" "b/docs/AimForOffer/\346\225\260\346\215\256\347\273\223\346\236\204\347\233\270\345\205\263/8_\345\223\210\345\270\214.md" new file mode 100644 index 00000000..f386aaa9 --- /dev/null +++ "b/docs/AimForOffer/\346\225\260\346\215\256\347\273\223\346\236\204\347\233\270\345\205\263/8_\345\223\210\345\270\214.md" @@ -0,0 +1,58 @@ +# 哈希 + +## 1、第一个只出现一次的字符位置 + +[第一个只出现一次的字符位置](https://www.nowcoder.com/practice/1c82e8cf713b4bbeb2a5b31cf5b0417c?tpId=13&tqId=11187&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +```java +//思路一: +public int FirstNotRepeatingChar(String str) { + int[] freq = new int[256]; + + for(char c:str.toCharArray()){ + freq[c]++; + } + + for(int i=0;i= 2,则表示未多个 +// 所以只需要 2 位就可表示字符出现的次数,即 0次(00)、1次(01)或者多次(11) + +public int FirstNotRepeatingChar(String str) { + BitSet bitSet = new BitSet(256); + BitSet bitSet2 = new BitSet(256); + + for(char c:str.toCharArray()){ + if(!bitSet.get(c) && !bitSet2.get(c)){ //00 + // c 原来出现次数 0次,加入 c 后出现次数为 1 + bitSet2.set(c); + } + else if(!bitSet.get(c) && bitSet2.get(c)){ //01 + // c 原来出现次数 1 次,加入 c 后出现次数为 3 (3 表示多次) + bitSet.set(c); + } + } + + for(int i=0;i arr[j] +//则 arr[i] 后面的元素都会大于 arr[j],即 [i,mid] 的长度就是逆序对数 + +private long P; //逆序对数 + +public int InversePairs(int [] array) { + sort(array,0,array.length-1); + return (int)(P%1000000007); +} + +private void sort(int[] arr,int l,int r){ + if(l>=r){ + return; + } + int m=(r-l)/2+l; + sort(arr,l,m); + sort(arr,m+1,r); + merge(arr,l,m,r); +} + +//合并 [l,m] 和 [m+1,r] 两个有序数组 +private void merge(int[] arr,int l,int m,int r){ + int[] newArr = new int[r-l+1]; + int index =0; + int i=l; + int j=m+1; + while(i<=m && j<=r){ + if(arr[i]<=arr[j]){ + newArr[index++]=arr[i++]; + }else{ //arr[i]>arr[j],arr[i]后面的元素都会大于 arr[j] + P+=(m-i+1); + newArr[index++]=arr[j++]; + } + } + while(i<=m){ + newArr[index++]=arr[i++]; + } + while(j<=r){ + newArr[index++]=arr[j++]; + } + index=0; + for(int k=l;k<=r;k++){ + arr[k]=newArr[index++]; + } +} +``` + + + +## 2、最小的 k 个数 + +[最小的 k 个数](https://www.nowcoder.com/practice/6a296eb82cf844ca8539b57c23e6e9bf?tpId=13&tqId=11182&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +```java +public ArrayList GetLeastNumbers_Solution(int [] input, int k) { + ArrayList res = new ArrayList<>(); + if(k<=0 || k>input.length){ + return res; + } + int index = findKthSmallest(input,k-1); + if(index==-1){ + return res; + } + + for(int i=0;i<=index;i++){ + res.add(input[i]); + } + return res; +} + +//查找数组中第 k 小的元素 +private int findKthSmallest(int[] input,int k){ + int l=0,r=input.length-1; + while (l<=r){ + int p = partion(input,l,r); + if(p==k){ + return p; + }else if(pk; + r = p-1; + } + } + return -1; +} + +//使用快速排序中的切分思路 +private int partion(int[] nums,int start,int end){ + int pivot = nums[start]; + + while (start=pivot){ + end--; + } + nums[start]=nums[end]; + while(start=lcs[i][j-1]){ - lcs[i][j]=lcs[i-1][j]; - x[i][j]='u'; - }else if(lcs[i-1][j]lcs[i][j-1]){ + lcs[i][j]=lcs[i-1][j]; + x[i][j]='u'; + } } } } - return lcs[m][n]; + print(s,m,n); + return res.toString(); } -public void print(String s,int m,int n){ - if(m==0 || n==0){ +private void print(String s,int m,int n){ + if(s==null || s.length()==0){ return; } if(x[m][n]=='c'){ @@ -727,6 +731,26 @@ private int binarySearch(int[] tails,int l,int r,int key){ [完全平方数](https://leetcode-cn.com/problems/perfect-squares/) ```java - +// 思路:dp[i] 表示整数 i 的完全平方数的最少数量 +// 显然这些平方数开方后数值的大小范围在 [1,sqrc(n)] 之间 +// 假设 j 在 [1,sqrc(n)] 中取值,我们知道 i 和(i-j^2)之间就差了一个j^2完全平方数 +// 则有 dp[i]=1 + min{dp[i-j^2]} +public int numSquares(int n) { + if(n<=2){ + return n; + } + int N=(int)Math.sqrt(n*1.0); + int[] dp=new int[n+1]; + dp[0]=0; // 0不是正整数,显然不能使用完全平方数去表示 + for(int i=1;i<=n;i++){ + int min=dp[i-1]; + for(int j=1;i-j*j>=0 && j<=N;j++){ + if(dp[i-j*j]> FindPath(TreeNode root, int target) { + ArrayList> res = new ArrayList<>(); + ArrayList values = new ArrayList<>(); + backtrack(root,target,values,res); + return res; +} + +// values : 记录从根节点到叶子节点的所有路径 +// paths : 存储所有可能的结果 +private void backtrack(TreeNode root, int target, + ArrayList values, + ArrayList> paths) { + if (root == null) { + return; + } + values.add(root.val); + if(root.left==null && root.right==null && root.val==target){ + paths.add(new ArrayList<>(values)); + }else{ + backtrack(root.left,target-root.val,values,paths); + backtrack(root.right,target-root.val,values,paths); + } + values.remove(values.size()-1); +} +``` + + + +## *2、二叉树的所有路径 + +[二叉树的所有路径](https://leetcode-cn.com/problems/binary-tree-paths/) + +```java +public List binaryTreePaths(TreeNode root) { + List paths=new ArrayList<>(); + if(root==null){ + return paths; + } + List values=new ArrayList<>(); + backtrack(root,values,paths); + return paths; +} + +//values 记录从根节点到叶子节点 +//paths 记录路径 +private void backtrack(TreeNode root, List values,List paths){ + if(root==null){ + return; + } + values.add(root.val); + if(root.left==null && root.right==null){ // 到达叶子节点 + paths.add(buildPath(values)); + }else{ + backtrack(root.left,values,paths); + backtrack(root.right,values,paths); + } + values.remove(values.size()-1); +} + +private String buildPath(List values){ + StringBuilder res=new StringBuilder(); + for(int i=0;i"); + } + } + return res.toString(); +} +``` + + + +## *3、矩阵中的路径 + +[矩阵中的路径](https://www.nowcoder.com/practice/c61c6999eecb4b8f88a98f66b273a3cc?tpId=13&tqId=11218&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +```java +//思路:典型的回溯法思想 +private int m,n; //矩阵的长度、宽度 +private boolean[][] visted; //标记是否访问 + +private int[][] d={ + {0,1}, //向右 + {-1,0},//向上 + {0,-1},//向左 + {1,0} //向下 +}; + +private boolean inArea(int x,int y){ + return (x>=0 && x=0 && y=0 && x=0 && y + +
+ +- 思路: + +
+ +
+ +在第 i 行放上皇后,判断是否其他地方能够放皇后的情况: + +(1)竖向:col[j] 表示第 j 列被占用 + +(2)对角线1:dai1[m] 表示第 m 条对角线1被占用,如 dia1[0]=true 表示[0,0] 位置不能放皇后, + +dia1[1] =true 就表示 [0,1]和[1,0] 位置不能放皇后。 + +对角线1总共有 (2*n-1) 条;m 与 i 、 j 的关系:m=i+j。 + +
+ +(3)对角线2:dai2[m]表示第 m 条对角线被2占用,如 dai2[0]=true 表示[0,3] 位置不能放皇后, + +dai2[2]=true 则表示 [0,2] 和 [1,3] 不能放皇后, + +对角线2总共有(2*n-1)条;m 与 i、j 关系:m=i-j+n-1。 + +
+ +```java +private List> res; + +//用于判断是否在同一竖线上,因为index表示行数,是变化的,所以不用判断是否在相同行 +private boolean[] cols; +//判断是否在1类对角线上,相应坐标 i+j +private boolean[] dial1; +//判断是否在2类对角线上,相应坐标 i-j+n-1 +private boolean[] dial2; + +//在 index 行放皇后 +//row 记录在 index 行能够放皇后的位置 +//比如 row.get(0) = 1,就表示在棋盘的 [0,1] 位置放上皇后 +private void putQueen(int n,int index,List row){ + if(index==n){ + res.add(generateBoard(n,row)); + return; + } + // [index,j] 放皇后 + for(int j=0;j generateBoard(int n, List row) { + List res=new ArrayList<>(); + if(n<=0){ + return res; + } + + char[][] board=new char[n][n]; + for(int i=0;i> solveNQueens(int n) { + res=new ArrayList<>(); + if(n==0){ + return res; + } + cols=new boolean[n]; + dial1=new boolean[2*n-1]; + dial2=new boolean[2*n-1]; + List row=new ArrayList<>(); + putQueen(n,0,row); + return res; +} +``` + + + +## 6、解数独 + +[解数独](https://leetcode-cn.com/problems/sudoku-solver/) + +```java +// 思路:回溯法 +// 数独矩阵是 9*9 的方阵,也就是有 81 个位置, +// 可以看成是一个长度为 81 的数组,使用 pos(从0开始) 记录在数组中位置 +// row[i][m] 表示第 i 行填数字 m 的占用情况,即 row[i][m]=true,表示在第i行填上数字 m 了。 +// col[i][m] 表示第 i 列填数字 m 的占用情况,即 col[i][m]=true,表示在第i列填上数字 m 了。 +// block[i][m] 表示第 i个九宫格填数字 m 的占用情况 +public void solveSudoku(char[][] board) { + if(board==null){ + return; + } + boolean[][] row=new boolean[9][10]; + boolean[][] col=new boolean[9][10]; + boolean[][] block=new boolean[9][10]; + for(int i=0;i<9;i++){ + for(int j=0;j<9;j++){ + if(board[i][j]!='.'){ // [i,j]位置已填上数字 + int num= board[i][j]-'0'; + row[i][num]=true; + col[j][num]=true; + block[i/3*3+j/3][num]=true; + } + } + } + for(int pos=0;pos<81;pos++){ + int x=pos/9; + int y=pos%9; + if(board[x][y]=='.'){ + if(!putNum(board,pos,row,col,block)){ + continue; + } + } + } +} + +// 向 pos 位置放数字 +private boolean putNum(char[][] board, int pos, + boolean[][] row, boolean[][] col, boolean[][] block){ + if(pos==81){ + return true; + } + int nextPos = pos+1; // pos 位置的下一未填数字的位置 + for(;nextPos<81;nextPos++){ + if(board[nextPos/9][nextPos%9]=='.'){ + break; + } + } + // [x,y] 表示 pos 在表格中位置 + int x=pos/9; + int y=pos%9; + + for(int num=1;num<=9;num++){ // pos 位置可以填 1-9 任意整数 + if(!row[x][num] && !col[y][num] && !block[x/3*3+y/3][num]){ + board[x][y]=(char)(num+'0'); + row[x][num]=true; + col[y][num]=true; + block[x/3*3+y/3][num]=true; + if(putNum(board,nextPos,row,col,block)){ + return true; + } + block[x/3*3+y/3][num]=false; + col[y][num]=false; + row[x][num]=false; + board[x][y]='.'; + } + } + return false; +} +``` + + + +# 广度优先-BFS + +## 1、单词接龙 + +[单词接龙](https://leetcode-cn.com/problems/word-ladder/description/) + +```java +public int ladderLength(String beginWord, String endWord, List wordList) { + wordList.add(beginWord); + int N = wordList.size(); + int start = N - 1; + int end = 0; + while (end < N && !wordList.get(end).equals(endWord)) { + end++; + } + if (end == N) { + return 0; + } + List[] graphic = buildGraphic(wordList); + return getShortestPath(graphic, start, end); +} + +private List[] buildGraphic(List wordList) { + int N = wordList.size(); + List[] graphic = new List[N]; + for (int i = 0; i < N; i++) { + graphic[i] = new ArrayList<>(); + for (int j = 0; j < N; j++) { + if (isConnect(wordList.get(i), wordList.get(j))) { + graphic[i].add(j); + } + } + } + return graphic; +} + +private boolean isConnect(String s1, String s2) { + int diffCnt = 0; + for (int i = 0; i < s1.length() && diffCnt <= 1; i++) { + if (s1.charAt(i) != s2.charAt(i)) { + diffCnt++; + } + } + return diffCnt == 1; +} + +private int getShortestPath(List[] graphic, int start, int end) { + Queue queue = new LinkedList<>(); + boolean[] marked = new boolean[graphic.length]; + queue.add(start); + marked[start] = true; + int path = 1; + while (!queue.isEmpty()) { + int size = queue.size(); + path++; + while (size-- > 0) { + int cur = queue.poll(); + for (int next : graphic[cur]) { + if (next == end) { + return path; + } + if (marked[next]) { + continue; + } + marked[next] = true; + queue.add(next); + } + } + } + return 0; +} +``` + + + +# 深度优先-DFS + +## 1、岛屿数量 + +[岛屿数量](https://leetcode-cn.com/problems/number-of-islands/) + +```java +// 思路:典型的 flood-fill 算法 +private int m,n; + +private boolean[][] visited; + +private boolean inArea(int x,int y){ + return (x>=0 && x=0 && y=0 && x=0 && y=0 && x=0 && y0){ + sum += num %10; + num/=10; + } + return sum; +} + +private boolean valid(int threshlod ,int x,int y){ + return (getNum(x)+getNum(y))<=threshlod; +} + +private int walk(int threshold,int startx,int starty){ + visited[startx][starty]=true; + int walks = 0; + if(valid(threshold,startx,starty)){ + walks=1; + } + for(int i=0;i<4;i++){ + int newX = startx+d[i][0]; + int newY = starty+d[i][1]; + if(inArea(newX,newY)){ + if(!visited[newX][newY] && valid(threshold,newX,newY)){ + walks += walk(threshold,newX,newY); + } + } + } + return walks; +} + +public int movingCount(int threshold, int rows, int cols) { + if(threshold<0){ //threshold<0,则机器人就不能走了 + return 0; + } + m = rows; + if(m==0){ + return 0; + } + n= cols; + visited = new boolean[m][n]; + return walk(threshold,0,0); +} +``` + + + +## 4、被围绕的区域 + +[被围绕的区域](https://leetcode-cn.com/problems/surrounded-regions/) + +```java +private int m,n; + +private boolean[][] visited; + +private int[][] d={ + {-1,0}, + {0,1}, + {1,0}, + {0,-1} +}; + +private boolean inArea(int x,int y){ + return (x>=0 && x=0 && y=0 && x=0 && y=matrix[startx][starty]){ + dfs(matrix,newX,newY,visited); + } + } + } +} + +public List> pacificAtlantic(int[][] matrix) { + List> res=new ArrayList<>(); + m=matrix.length; + if(m==0){ + return res; + } + n=matrix[0].length; + pacific=new boolean[m][n]; + atlantic=new boolean[m][n]; + + for(int j=0;j + +```java +private List> res; + +private boolean[] visited; + +public List> permute(int[] nums) { + res = new ArrayList<>(); + if(nums==null || nums.length==0){ + return res; + } + visited = new boolean[nums.length]; + List p =new ArrayList<>(); + generatePermutation(nums,0,p); + return res; +} + +//p中保存一个有 index 的元素的排列 +//向这个排列的末尾添加第 (index+1) 个元素,组成有(index+1) 个元素排列 +private void generatePermutation(int[] nums,int index,List p){ + if(index==nums.length){ + res.add(new ArrayList<>(p)); + return; + } + for(int i=0;i> res; + +private boolean[] visited; + +public List> permuteUnique(int[] nums) { + res = new ArrayList<>(); + if(nums==null || nums.length==0){ + return res; + } + visited = new boolean[nums.length]; + Arrays.sort(nums); + List p = new ArrayList<>(); + findUniquePermutation(nums,0,p); + return res; +} + +//p 保存的是有 index 元素的排列 +//向这个排列的末尾添加第 (index+1) 个元素,组成有(index+1) 个元素排列 +private void findUniquePermutation(int[] nums,int index,List p){ + if(index==nums.length){ + res.add(new ArrayList<>(p)); + return; + } + for(int i=0;i0 && nums[i-1]==nums[i] && + !visited[i-1]){ //注意:相邻元素相同,并且都未被访问 + continue; + } + visited[i]=true; + p.add(nums[i]); + findUniquePermutation(nums,index+1,p); + p.remove(p.size()-1); + visited[i]=false; + } + } + return; +} +``` + + + +## 3、字符串的排列 + +[字符串的排列](https://www.nowcoder.com/practice/fe6b651b66ae47d7acce78ffdd9a96c7?tpId=13&tqId=11180&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +```java +private ArrayList res; +private boolean[] visited; + +public ArrayList Permutation(String str) { + res = new ArrayList<>(); + if(str==null || str.length()==0){ + return res; + } + char[] chs = str.toCharArray(); + Arrays.sort(chs); //方便后面的去重处理 + visited = new boolean[str.length()]; + permute(chs,0,new StringBuilder()); + return res; +} + +//产生排列 +//p中保存一个存在index个元素的排列 +//向这个排列的末尾添加第(index+1)个元素,获得包含(index+1)个元素的排列 +private void permute(char[] chs,int index,StringBuilder p){ + if(index==chs.length){ + res.add(p.toString()); + return; + } + for(int i=0;i0 && chs[i-1]==chs[i] && !visited[i-1]){ + continue; + } + if(!visited[i]){ + p.append(chs[i]); + visited[i] = true; + permute(chs,index+1,p); + p.deleteCharAt(p.length()-1); + visited[i] = false; + } + } +} +``` + + + +## *4、字母大小写全排列 + +[字母大小写全排列](https://leetcode-cn.com/problems/letter-case-permutation/) + +```java +private List res; + +//处理 index 位置的数据 +//如果 index 位置是字母的话,则替换 +private void replaceLetter(int index,StringBuilder p){ + if(index==p.length()){ + res.add(p.toString()); + return; + } + char ch=p.charAt(index); + if(Character.isLetter(ch)){ + p.setCharAt(index,Character.toUpperCase(ch)); + replaceLetter(index+1,p); + p.setCharAt(index,Character.toLowerCase(ch)); + replaceLetter(index+1,p); + }else{ + replaceLetter(index+1,p); + } + return; +} + +public List letterCasePermutation(String S) { + res=new ArrayList<>(); + if(S==null || S.length()==0){ + return res; + } + StringBuilder p=new StringBuilder(S); + replaceLetter(0,p); + return res; +} +``` + + + +## *5、电话号码的字母组合 + +[电话号码的字母组合](https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/) + +```java +private final String[] letterMap={ + " ",//0 + "",//1 + "abc",//2 + "def",//3 + "ghi",//4 + "jkl",//5 + "mno",//6 + "pqrs",//7 + "tuv",//8 + "wxyz"//9 +}; + +private List res; + +public List letterCombinations(String digits) { + res=new ArrayList<>(); + if(digits==null || digits.length()==0){ + return res; + } + generateLetterCombinations(digits,0,new StringBuilder()); + return res; +} + +// p 保存 digits[0,index-1] 翻译得到的字符串 +private void generateLetterCombinations(String digits,int index,StringBuilder p){ + if(index==digits.length()){ + res.add(p.toString()); + return; + } + char[] chs=letterMap[digits.charAt(index)-'0'].toCharArray(); + for(int i=0;i> res; + +//c存储已经找到的组合 +//从start开始搜索新的元素 +private void findCombination(int n,int k,int start,List c){ + if(k==c.size()){ + res.add(new ArrayList<>(c)); + return; + } + for(int i=start;i<=n;i++){ + c.add(i); + findCombination(n,k,i+1,c); + c.remove(c.size()-1); + } + return; +} + +public List> combine(int n, int k) { + res=new ArrayList<>(); + if(n<=0 || k<=0 || n c=new ArrayList<>(); + findCombination(n,k,1,c); + return res; +} +``` + +```java +//思路二:剪枝优化 +private List> res; + +//c存储已经找到的组合 +//从start开始搜索新的元素 +private void findCombination(int n,int k,int start,List c){ + if(k==c.size()){ + res.add(new ArrayList<>(c)); + return; + } + + //优化--剪枝 + //c存储的是已经找到的组合。 + //此时还剩下k-c.size()个空位, + //则 [i...n]之间的元素最少要有 k-c.size() 个,即 n-i+1 >= k-c.size() + //所以有 i <= n-(k-c.size())+1 + + for(int i=start;i<=n-(k-c.size())+1;i++){ + c.add(i); + findCombination(n,k,i+1,c); + c.remove(c.size()-1); + } + return; +} + +public List> combine(int n, int k) { + res=new ArrayList<>(); + if(n<=0 || k<=0 || n c=new ArrayList<>(); + findCombination(n,k,1,c); + return res; +} +``` + diff --git "a/docs/AimForOffer/\347\256\227\346\263\225\346\200\235\346\203\263\347\233\270\345\205\263/6_\350\264\252\345\277\203.md" "b/docs/AimForOffer/\347\256\227\346\263\225\346\200\235\346\203\263\347\233\270\345\205\263/6_\350\264\252\345\277\203.md" new file mode 100644 index 00000000..b9735576 --- /dev/null +++ "b/docs/AimForOffer/\347\256\227\346\263\225\346\200\235\346\203\263\347\233\270\345\205\263/6_\350\264\252\345\277\203.md" @@ -0,0 +1,289 @@ +# 贪心思想 + +## 1、连续子数组的最大和 + +[连续子数组的最大和](https://www.nowcoder.com/practice/459bd355da1549fa8a49e350bf3df484?tpId=13&tqId=11183&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +```java +//思路: +//贪心策略:遍历数组,到当前位置,上次保留的最大值如果为负数就要刷新一次, +// 即 curSum<=0时,当即果断丢弃,因为只会越加越小,刷新为当前值; +// 如果为正数,继续累加。 + +public int FindGreatestSumOfSubArray(int[] array) { + if (array == null || array.length == 0){ + return 0; + } + int curSum = 0; + int res= Integer.MIN_VALUE; + for(int i=0;i=0 && j>=0){ + if(s[j]>=g[i]){ + num++; + j--; + } + i--; + } + return num; +} +``` + + + +## 4、救生艇 + +[救生艇](https://leetcode-cn.com/problems/boats-to-save-people/) + +```java +// 思路: +// 贪心策略: +// 将最重的人和最轻的安排在同一艘船上 +// 如果只能载一个人,则载最重的,如果剩下一个人,则只能单独趁一艘船。 +public int numRescueBoats(int[] people, int limit) { + if(people==null || people.length==0 || limit<=0){ + return 0; + } + Arrays.sort(people); + int light=0; + int heavy=people.length-1; + int res=0; + while (light<=heavy){ + if(light==heavy){ // 只剩下一个人,则单独一艘船 + res++; + break; + } + if(people[light]+people[heavy]>limit){ // 如果只能载一个人,则载最重的 + res++; + heavy--; + }else{ // people[light]+people[heavy]<=limit,船可以搭载两个人 + res++; + light++; + heavy--; + } + } + return res; +} +``` + + + +## *5、汇总区间 + +[汇总区间](https://leetcode-cn.com/problems/summary-ranges/) + +```java +public List summaryRanges(int[] nums) { + List res=new ArrayList<>(); + if(nums==null || nums.length==0){ + return res; + } + if(nums.length==1) { + res.add(nums[0] + ""); + return res; + } + int start=nums[0]; + int end=start; + for(int i=1;i"+end; +} +``` + + + +## 6、无重叠区间 + +[无重叠区间](https://leetcode-cn.com/problems/non-overlapping-intervals/) + +```java +// 思路: +// 首先计算最多能组成的不重叠区间个数,然后用区间总个数减去不重叠区间的个数。 +// 其中区间结尾至关重要,选择的区间结尾越小,留给后面的空间就越大,那么后面能够选择的区间个数也就越大。 +public int eraseOverlapIntervals(int[][] intervals) { + if(intervals==null || intervals.length==0){ + return 0; + } + // 按照区间结尾进行升序排序 + // 写法一:比较器 + // 这里使用 p1[1]() { + @Override + public int compare(int[] p1, int[] p2) { + return p1[1] p1[1]p[1]));*/ + int cnt=1; // 记录最大不重叠区间数 + int end=intervals[0][1]; // 记录前一个不重叠区间结尾位置 + for(int i=1;i() { + @Override + public int compare(int[] o1, int[] o2) { + return o1[1](o1[1]o[1])); + int res=1; + int end=points[0][1]; + for(int i=1;i() { + @Override + public int compare(int[] o1, int[] o2) { + return (o1[0] + o1[0]o[0]));*/ + List intervalList=new ArrayList<>(); // 保存结果区间 + int[] cur=intervals[0]; // 记录当前重叠区间 + int m=intervals.length; + for(int i=1;i=intervals[i][0]){ // cur.end >= next.start + cur[1]=Math.max(cur[1],intervals[i][1]); + }else{ + intervalList.add(cur); + cur=intervals[i]; + } + if(i==m-1){ + intervalList.add(cur); + } + } + int[][] res=new int[intervalList.size()][2]; + for(int i=0;i=0 || j>=0){ + c+=(i>=0)?num1.charAt(i)-'0':0; + c+=(j>=0)?num2.charAt(j)-'0':0; + res.append(c%10); + c/=10; + i--; + j--; } - - int l=1; - int r=x; - while(l<=r){ - int mid=(r-l)/2+l; - int sqrt=x/mid; - if(sqrt==mid){ - return mid; - }else if(sqrtmid; - l=mid+1; - } + if(c==1){ // 考虑 num1="90"、num2="90" 的情况 + res.append("1"); } - return r; + return res.reverse().toString(); } ``` -## 7、求 1+2+3+...+n +## 2、求 1+2+3+...+n [求 1+2+3+...+n](https://www.nowcoder.com/practice/7a0da8fc483247ff8800059e12d7caf1?tpId=13&tqId=11200&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -245,11 +216,46 @@ public int Sum_Solution(int n) { -## *8、从 1 到 n 整数中 1 出现的次数 +## *3、从 1 到 n 整数中 1 出现的次数 [从 1 到 n 整数中 1 出现的次数](https://www.nowcoder.com/practice/bd7f978302044eee894445e244c7eee6?tpId=13&tqId=11184&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) ```java +// 思路一:暴力法 +// 时间复杂度:O(n^2) +public int NumberOf1Between1AndN_Solution(int n) { + if(n<=0){ + return 0; + } + int res=0; + for(int i=1;i<=n;i++){ + res+=getNumberOf1(i); + } + return res; +} + +private int getNumberOf1(int n){ + int cnt=0; + while(n>0){ + if(n%10==1){ + cnt++; + } + n/=10; + } + return cnt; +} +``` + +```java +// 思路二:参考 https://blog.csdn.net/yi_Afly/article/details/52012593 +// 总结如下规律: +// base 为当前位数 base=1,10,100, +// weight 为当前位值 +// formatter 为 weight 的后一位 +// round 就是 weight 前的所有位 +// 当 weight==0 时,count=round*base +// 当 weight==1 时,count=round*base+formatter+1 +// 当 weight>1 时,count==round*base+base public int NumberOf1Between1AndN_Solution(int n) { if(n<1){ return 0; @@ -276,13 +282,11 @@ public int NumberOf1Between1AndN_Solution(int n) { } ``` -> [Leetcode : 233. Number of Digit One](https://leetcode.com/problems/number-of-digit-one/discuss/64381/4+-lines-O(log-n)-C++JavaPython) - > [参考:从 1 到 n 整数中 1 出现的次数](https://blog.csdn.net/yi_Afly/article/details/52012593) -## *9、数字序列中的某一位数字 +## *4、数字序列中的某一位数字 数字以 0123456789101112131415... 的格式序列化到一个字符串中,求这个字符串的第 index 位。 @@ -340,7 +344,85 @@ private int getDigitAtIndex(int index, int place) { -## 10、圆圈中最后剩下的数 +# 经典数学问题 + +## *1、数值的整数次方 + +[数值的整数次方](https://www.nowcoder.com/practice/1a834e5e3e1a4b7ba251417554e07c00?tpId=13&tqId=11165&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +```java +//思路:分治法 +// a^n 次方 +// 若 a 为奇数 a^n = a*(a*a)^((n-1)/2) +// 若 a 为偶数 a^n = (a*a)^(n/2) + +//注意: +// n<0 时,则 a^n = 1 /(a ^(-n)) +// 但是当 n == Integer.MIN_VALUE 时,-n = Integer.MAX_VALUE+1 +// 我们可以转化为 1 / (a^(Integer.MAX_VALUE)*a) + +public double Power(double base, int exponent) { + if(exponent==0){ + return 1.0; + } + if(exponent==1){ + return base; + } + if(exponent==Integer.MIN_VALUE){ + return 1/(Power(base,Integer.MAX_VALUE)*base); + } + + if(exponent<0){ + exponent = -exponent; + return 1/Power(base,exponent); + } + + if(exponent%2==1){ + return Power(base*base,(exponent-1)/2)*base; + }else{ + return Power(base*base,exponent/2); + } +} +``` + + + +## 2、x 的平方根 + +[x 的平方根](https://leetcode-cn.com/problems/sqrtx/) + +```java +// 思路:要求是非负整数,则必然 x>=0 +// 当 x<4时,可以证明 sqrt(x) > x/2 +// 当 x=4时,sqrt(4)==4/2=2 +// 当 x>4时,可以证明 sqrt(x) < x/2 +public int mySqrt(int x) { + if(x==0){ + return 0; + } + if(x<4){ // x=1,x=2,x=3,sqrt(x)=1 + return 1; + } + // 考虑 x>=4的情况 + int l=1; + int r=x/2; + while(l<=r){ + int mid=(r-l)/2+l; + if(x/midx,采用 x/midmid){ + l=mid+1; + }else{ + return mid; + } + } + return r; // 只保留整数部分,所以取r +} +``` + + + +## *3、圆圈中最后剩下的数 [圆圈中最后剩下的数](https://www.nowcoder.com/practice/f78a359491e64a50bce2d89cff857eb6?tpId=13&tqId=11199&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -386,100 +468,183 @@ public int LastRemaining_Solution(int n, int m) { > [参考:约瑟夫环问题](https://blog.csdn.net/u011500062/article/details/72855826) -## 11、计数质数 + + +## 4、计数质数 [计数质数](https://leetcode-cn.com/problems/count-primes/) ```java - +// 思路: +// 定义一个非素数数组 notPrimes,其中 notPrimes[i] 表示元素 i 是否是素数 +// notPrimes[i]=true,说明 i 是素数 +// notPrimes[i]=false,说明 i 不是素数 +public int countPrimes(int n) { + int cnt=0; + if(n<=1){ + return cnt; + } + boolean[] notPrimes=new boolean[n]; + for(int x=2;x=n 循环停止 + for(long j=(long)x*x;j +// 11!=11*(2*5)*9*(4*2)*7*(3*2)*(1*5)*(2*2)*3*(1*2)*1=39916800,有2对<2,5> +// 也就是说,有多少对<2,5>就有多少0 +// 对于含有2的因子的话是 1*2, 2*2, 3*2, 4*2 +// 对于含有5的因子的话是 1*5, 2*5 +// 含有2的因子每两个出现一次,含有5的因子每5个出现一次,所以2出现的个数远远多于 5, +// 换言之找到一个5,一定能找到一个2与之配对。 +// 所以我们只需要找有多少个 5。 +// n! = 1 * 2 * 3 * 4 * (1 * 5) * ... * (2 * 5) * ... * (3 * 5) *... * n +// 可以得出如下规律:每隔5个数会出现一个5,每隔25个数,出现2个5,每隔125个数,出现3个5 +// 所以最终 5的个数:n/5+ n/25 (有1个5在每隔5个数已经出现过一次了) + n/125 (有1个在每隔5个数出现过了,另一个则载每隔25个数出现过了) +public int trailingZeroes(int n) { + if(n<5){ + return 0; + } + int res=0; + while (n>0){ + res+=n/5; + n/=5; + } + return res; +} ``` -## 13、字符串相加 +## 6、最少移动次数使数组元素相等 II -[字符串相加](https://leetcode-cn.com/problems/add-strings/) +[ 最少移动次数使数组元素相等 II](https://leetcode-cn.com/problems/minimum-moves-to-equal-array-elements-ii/) ```java -public String addStrings(String num1, String num2) { - if(num1==null || num1.length()==0){ - return num2; - } - if(num2==null || num2.length()==0){ - return num1; - } - - int i=num1.length()-1; - int j=num2.length()-1; - int c=0; - - StringBuilder res=new StringBuilder(); - - while(i>=0 || j>=0 || c==1){ - c+=(i>=0)?num1.charAt(i)-'0':0; - c+=(j>=0)?num2.charAt(j)-'0':0; - res.append(c%10); - c/=10; - i--; - j--; +// 思路: +// 当 x 为这个 N 个数的中位数时,可以使得距离最小。 +// 具体地,若 N 为奇数,则 x 必须为这 N 个数中的唯一中位数; +// 若 N 为偶数,中间的两个数为 p 和 q,中位数为 (p + q) / 2, +// 此时 x 只要是区间 [p, q](注意是闭区间) 中的任意一个数即可。 + +// 写法一:排序求中位数 +public int minMoves2(int[] nums) { + Arrays.sort(nums); + // 获取数组中位数获取[p,q]中的数值p + int mid=nums[nums.length/2]; + int res=0; + for(int num:nums){ + res+=Math.abs(num-mid); } - - return res.reverse().toString(); + return res; } ``` - - -## 14、最少移动次数使数组元素相等 II - -[ 最少移动次数使数组元素相等 II](https://leetcode-cn.com/problems/minimum-moves-to-equal-array-elements-ii/) - ```java +// 写法二:利用快速排序求中位数 +public int minMoves2(int[] nums) { + if(nums.length==1){ + return 0; + } + int k=nums.length/2; + int mid=nums[findKth(nums,k)]; + int res=0; + for(int num:nums){ + res+=Math.abs(num-mid); + } + return res; +} +private int findKth(int[] nums,int k){ + int start=0; + int end=nums.length-1; + while (start<=end){ + int p=partition(nums,start,end); + if(p==k){ + return p; + }else if(pk + end=p-1; + } + } + return -1; +} + +private int partition(int[] nums,int start,int end){ + int pivot=nums[start]; + while(start=pivot){ + end--; + } + nums[start]=nums[end]; + // 从左向右找第一个大于 pivot 的数 + while (start n/2,但在本题目 m 即是多数元素 public int majorityElement(int[] nums) { - int cnt=0; //统计众数 - int majority=-1; - int n=nums.length; - + int m=nums[0]; + int cnt=0; for(int num:nums){ if(cnt==0){ - majority=num; - cnt++; + m=num; + cnt=1; }else{ - if(majority!=num){ - cnt--; - }else{ + if(num==m){ cnt++; + }else{ + cnt--; } } } - - cnt=0; - for(int num:nums){ - if(num==majority){ - cnt++; - } - } - return (cnt>n/2)?majority:-1; + return m; } ``` diff --git "a/docs/AimForOffer/16_\345\205\266\344\273\226.md" "b/docs/AimForOffer/\347\256\227\346\263\225\346\200\235\346\203\263\347\233\270\345\205\263/8_\345\205\266\344\273\226.md" similarity index 75% rename from "docs/AimForOffer/16_\345\205\266\344\273\226.md" rename to "docs/AimForOffer/\347\256\227\346\263\225\346\200\235\346\203\263\347\233\270\345\205\263/8_\345\205\266\344\273\226.md" index a69d6523..cbf78eab 100644 --- "a/docs/AimForOffer/16_\345\205\266\344\273\226.md" +++ "b/docs/AimForOffer/\347\256\227\346\263\225\346\200\235\346\203\263\347\233\270\345\205\263/8_\345\205\266\344\273\226.md" @@ -47,7 +47,7 @@ public class Shuffle { ## 2、抢红包算法 -线段切割法:在一条线段上找 (N-1) 个随机点,就可以将该线段随机且公平地切割成 N 段。 +线段切割法:在一条线段上找 (N-1) 个随机点,就可以将该线段随机且公平地切割成 N 段。算上首端点和尾端点的话,总共有 (N+1) 个点 ```java public class RedPacket { @@ -57,29 +57,27 @@ public class RedPacket { * 如果是小数的话先转化为整数,相应的结果除以 100 即可 * @return List 存储 n 个红包的金额 */ - public List generatePocketByLineCutting(int n,double money){ + public List generatePocketByLineCutting(int n, double money){ Random random=new Random(); //如果是小数的话先转化为整数 int newMoney=(int)money*100; - //存储线段的的 (n-1) 个随机点,线段长度为 newMoney - Set set=new TreeSet<>(); - - while(set.size() points=new ArrayList<>(); + while (points.size() res=new ArrayList<>(); - - int pre=0; - for(Integer p:set) { - res.add((p - pre) * 1.0 / 100); - pre = p; + for(int i=1;i + +### 脏读 + +T1 修改一个数据,T2 随后读取这个数据。如果 T1 撤销了这次修改,那么 T2 读取的数据是脏数据。 + +
+ +### 不可重复读 + +T2 读取一个数据,T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同。 + +
+ +### 幻影读 + +T1 读取某个**范围**的数据,T2 在这个**范围**内插入新的数据,T1 再次读取这个**范围**的数据,此时读取的结果和和第一次读取的结果不同。 + +
+ +## 事务隔离级别 + +产生并发不一致性问题主要原因是破坏了事务的**隔离性**,解决方法是通过**并发控制**来保证隔离性。并发控制可以通过封锁(加锁)来实现,但是封锁操作需要用户自己控制,相当复杂。数据库管理系统提供了事务的**隔离级别**,让用户以一种更轻松的方式处理并发一致性问题。 + +### 未提交读(READ UNCOMMITTED) + +事务中的修改,即使没有提交,对其它事务也是可见的。 + +### 提交读(READ COMMITTED) + +一个事务只能读取已经提交的事务所做的修改。换句话说,一个事务所做的修改在提交之前对其它事务是不可见的。 + +### 可重复读(REPEATABLE READ) + +保证在同一个事务中多次读取同一数据的结果是一样的。 + +### 可串行化(SERIALIZABLE) + +强制事务串行执行,这样多个事务互不干扰,不会出现并发一致性问题。 + +隔离级别能解决的并发一致性问题: + +| 并发访问问题 | 事务隔离级别 | +| :----------: | :------------------------------------------: | +| 丢失修改 | MySQL 所有事务隔离级别在数据库层面上均可避免 | +| 脏读 | READ-COMMITTED 事务隔离级别以上可避免 | +| 不可重复读 | REPEATABLE-READ 事务隔离级别以上可避免 | +| 幻读 | SERIALIZABLE 事务隔离级别以上可避免 | + +即: + +| 事务隔离级别 \ 并发问题 | 丢失修改 | 脏读 | 不可重复读 | 幻读 | +| :---------------------------: | :------: | :--: | :--------: | :--: | +| 未提交读 (READ UNCOMMITTED) | 避免 | 发生 | 发生 | 发生 | +| 提交读 (READ COMMITTED) | 避免 | 避免 | 发生 | 发生 | +| 可重复读 (REPEATABLE READ) | 避免 | 避免 | 避免 | 发生 | +| 可串行化 (SERIALIZABLE) | 避免 | 避免 | 避免 | 避免 | diff --git "a/docs/DataBase/2_\345\205\263\347\263\273\346\225\260\346\215\256\345\272\223\350\256\276\350\256\241\347\220\206\350\256\272.md" "b/docs/DataBase/2_\345\205\263\347\263\273\346\225\260\346\215\256\345\272\223\350\256\276\350\256\241\347\220\206\350\256\272.md" new file mode 100644 index 00000000..244398b2 --- /dev/null +++ "b/docs/DataBase/2_\345\205\263\347\263\273\346\225\260\346\215\256\345\272\223\350\256\276\350\256\241\347\220\206\350\256\272.md" @@ -0,0 +1,125 @@ +# 关系数据库设计理论 + +## 函数依赖 + +记 A->B 表示 A 函数决定 B,也可以说 B 函数依赖于 A。 + +如果 {A1,A2,... ,An} 是关系的一个或多个属性的集合,该集合函数决定了关系的其它所有属性并且是最小的,那么该集合就称为键码。 + +对于 A->B,如果能找到 A 的真子集 A',使得 A'-> B,那么 A->B 就是部分函数依赖,否则就是完全函数依赖。 + +对于 A->B,B->C,则 A->C 是一个传递函数依赖。 + +## 异常 + +以下的学生课程关系的函数依赖为 Sno, Cname -> Sname, Sdept, Mname, Grade,键码为 {Sno, Cname}。也就是说,确定学生和课程之后,就能确定其它信息。 + +| Sno | Sname | Sdept | Mname | Cname | Grade | +| ---- | ------ | ------ | ------ | ------ | ----- | +| 1 | 学生-1 | 学院-1 | 院长-1 | 课程-1 | 90 | +| 2 | 学生-2 | 学院-2 | 院长-2 | 课程-2 | 80 | +| 2 | 学生-2 | 学院-2 | 院长-2 | 课程-1 | 100 | +| 3 | 学生-3 | 学院-2 | 院长-2 | 课程-2 | 95 | + +不符合范式的关系,会产生很多异常,主要有以下四种异常: + +- 冗余数据:例如 `学生-2` 出现了两次。 +- 修改异常:修改了一个记录中的信息,但是另一个记录中相同的信息却没有被修改。 +- 删除异常:删除一个信息,那么也会丢失其它信息。例如删除了 `课程-1` 需要删除第一行和第三行,那么 `学生-1` 的信息就会丢失。 +- 插入异常:例如想要插入一个学生的信息,如果这个学生还没选课,那么就无法插入。 + +## 范式 + +范式理论是为了解决以上提到四种异常。 + +高级别范式的依赖于低级别的范式,1NF 是最低级别的范式。 + +
+ + + +### 1. 第一范式 (1NF) + +属性不可分。 + +### 2. 第二范式 (2NF) + +每个非主属性完全函数依赖于键码。 + +(一是表必须有一个主键;二是没有包含在主键中的列必须完全依赖于主键,而不能只依赖于主键的部分。) + +可以通过分解来满足。 + +**分解前** + +| Sno | Sname | Sdept | Mname | Cname | Grade | +| ---- | ------ | ------ | ------ | ------ | ----- | +| 1 | 学生-1 | 学院-1 | 院长-1 | 课程-1 | 90 | +| 2 | 学生-2 | 学院-2 | 院长-2 | 课程-2 | 80 | +| 2 | 学生-2 | 学院-2 | 院长-2 | 课程-1 | 100 | +| 3 | 学生-3 | 学院-2 | 院长-2 | 课程-2 | 95 | + +以上学生课程关系中,{Sno, Cname} 为键码,有如下函数依赖: + +- Sno -> Sname, Sdept +- Sdept -> Mname +- Sno, Cname-> Grade + +Grade 完全函数依赖于键码,它没有任何冗余数据,每个学生的每门课都有特定的成绩。 + +Sname, Sdept 和 Mname 都部分依赖于键码,当一个学生选修了多门课时,这些数据就会出现多次,造成大量冗余数据。 + +**分解后** + +关系-1 + +| Sno | Sname | Sdept | Mname | +| ---- | ------ | ------ | ------ | +| 1 | 学生-1 | 学院-1 | 院长-1 | +| 2 | 学生-2 | 学院-2 | 院长-2 | +| 3 | 学生-3 | 学院-2 | 院长-2 | + +有以下函数依赖: + +- Sno -> Sname, Sdept +- Sdept -> Mname + +关系-2 + +| Sno | Cname | Grade | +| ---- | ------ | ----- | +| 1 | 课程-1 | 90 | +| 2 | 课程-2 | 80 | +| 2 | 课程-1 | 100 | +| 3 | 课程-2 | 95 | + +有以下函数依赖: + +- Sno, Cname -> Grade + +### 3. 第三范式 (3NF) + +非主属性不传递函数依赖于键码。 + +(确保每列都和主键列直接相关,而不是间接相关。) + +上面的 关系-1 中存在以下传递函数依赖: + +- Sno -> Sdept -> Mname + +可以进行以下分解: + +关系-11 + +| Sno | Sname | Sdept | +| ---- | ------ | ------ | +| 1 | 学生-1 | 学院-1 | +| 2 | 学生-2 | 学院-2 | +| 3 | 学生-3 | 学院-2 | + +关系-12 + +| Sdept | Mname | +| ------ | ------ | +| 学院-1 | 院长-1 | +| 学院-2 | 院长-2 | diff --git "a/docs/DataBase/3_\350\256\276\350\256\241\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223.md" "b/docs/DataBase/3_\350\256\276\350\256\241\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223.md" new file mode 100644 index 00000000..0309567f --- /dev/null +++ "b/docs/DataBase/3_\350\256\276\350\256\241\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223.md" @@ -0,0 +1,49 @@ +# 设计关系型数据库 + +关系数据库管理系统(RDBMS)架构图如下: + +
+ +## 存储 + +存储即文件系统。存储介质可以是机械硬盘、SSD 固态。 + +## 程序实例 + +- **存储管理** + + 对数据格式、文件风格进行统一管理,将物理数据通过逻辑形式组织、表示出来。 + + 优化:一次性读取多行,逻辑存取单位是页(page)。 + +- **缓存机制** + + 将取出的数据放入缓存中,一次性加载多个页数据,相当一部分不是本次访问所需的行。根据“一旦数据被访问,其相邻数据极有可能下次被访问到”,优化访问效率。 + + 缓存管理机制:可以使用 LRU 对缓存进行管理。 + +- **SQL 解析** + + 提供给外界指令来操作数据库,即可读的 SQL 语言。 + + 优化:可以将 SQL 缓存,方便下次解析。缓存不宜过大,管理缓存的算法中要有淘汰机制。 + +- **日志管理** + + 主从同步、灾难恢复。 + +- **权限划分** + + 支持多用户。 + +- **容灾机制** + + 数据库挂了,进行恢复,恢复到什么程度。 + +- **索引管理** + + 引入索引,提高查询效率。 + +- **锁管理** + + 引入锁机制,支持并发操作。 \ No newline at end of file diff --git "a/docs/DataBase/5_LeetCode_Database\351\242\230\350\247\243.md" "b/docs/DataBase/5_LeetCode_Database\351\242\230\350\247\243.md" new file mode 100644 index 00000000..0f3123f8 --- /dev/null +++ "b/docs/DataBase/5_LeetCode_Database\351\242\230\350\247\243.md" @@ -0,0 +1,1012 @@ +# 1、大的国家(595) + +[595. 大的国家](https://leetcode-cn.com/problems/big-countries/) + +- 问题描述 + +```html ++-----------------+------------+------------+--------------+---------------+ +| name | continent | area | population | gdp | ++-----------------+------------+------------+--------------+---------------+ +| Afghanistan | Asia | 652230 | 25500100 | 20343000 | +| Albania | Europe | 28748 | 2831741 | 12960000 | +| Algeria | Africa | 2381741 | 37100000 | 188681000 | +| Andorra | Europe | 468 | 78115 | 3712000 | +| Angola | Africa | 1246700 | 20609294 | 100990000 | ++-----------------+------------+------------+--------------+---------------+ +``` + +查找面积超过 3,000,000 或者人口数超过 25,000,000 的国家。 + +```html ++--------------+-------------+--------------+ +| name | population | area | ++--------------+-------------+--------------+ +| Afghanistan | 25500100 | 652230 | +| Algeria | 37100000 | 2381741 | ++--------------+-------------+--------------+ +``` + +- SQL Schema + +```sql +DROP TABLE +IF + EXISTS World; +CREATE TABLE World ( NAME VARCHAR ( 255 ), continent VARCHAR ( 255 ), area INT, population INT, gdp INT ); +INSERT INTO World ( NAME, continent, area, population, gdp ) +VALUES + ( 'Afghanistan', 'Asia', '652230', '25500100', '203430000' ), + ( 'Albania', 'Europe', '28748', '2831741', '129600000' ), + ( 'Algeria', 'Africa', '2381741', '37100000', '1886810000' ), + ( 'Andorra', 'Europe', '468', '78115', '37120000' ), + ( 'Angola', 'Africa', '1246700', '20609294', '1009900000' ); +``` + +- 解题 + +```sql +# 思路: +# 1、根据样例,我们知道。查询字段是 name population 和 area +# 2、查询条件是 area > 3000000 || population > 25000000 +SELECT name,population,area +FROM World +WHERE area > 3000000 || population > 25000000; +``` + +# 2、交换工资(627) + +[627. 交换工资](https://leetcode-cn.com/problems/swap-salary/) + +- 问题描述 + +```html +| id | name | sex | salary | +|----|------|-----|--------| +| 1 | A | m | 2500 | +| 2 | B | f | 1500 | +| 3 | C | m | 5500 | +| 4 | D | f | 500 | +``` + +只用一个 SQL 查询,将 sex 字段反转。 + +```html +| id | name | sex | salary | +|----|------|-----|--------| +| 1 | A | f | 2500 | +| 2 | B | m | 1500 | +| 3 | C | f | 5500 | +| 4 | D | m | 500 | +``` + +- SQL Schema + +```sql +DROP TABLE +IF + EXISTS salary; +CREATE TABLE salary ( id INT, NAME VARCHAR ( 100 ), sex CHAR ( 1 ), salary INT ); +INSERT INTO salary ( id, NAME, sex, salary ) +VALUES + ( '1', 'A', 'm', '2500' ), + ( '2', 'B', 'f', '1500' ), + ( '3', 'C', 'm', '5500' ), + ( '4', 'D', 'f', '500' ); +``` + +- 解题 + +```sql +# 思路: +# l、利用位运算:比如若 x^b^a = a,则判断 x=b +# 2、利用 ASCII 函数将字符转换为数值进行运算,然后再利用 CHAR 函数将数值转换为字符 +UPDATE +salary +SET sex = CHAR(ASCII(sex)^ASCII('m')^ASCII('f')); +``` + +# 3、有趣的电影(62) + +[620. 有趣的电影](https://leetcode-cn.com/problems/not-boring-movies/) + +- 问题描述 + + +```html ++---------+-----------+--------------+-----------+ +| id | movie | description | rating | ++---------+-----------+--------------+-----------+ +| 1 | War | great 3D | 8.9 | +| 2 | Science | fiction | 8.5 | +| 3 | irish | boring | 6.2 | +| 4 | Ice song | Fantacy | 8.6 | +| 5 | House card| Interesting| 9.1 | ++---------+-----------+--------------+-----------+ +``` + +查找 id 为奇数,并且 description 不是 boring 的电影,按 rating 降序。 + +```html ++---------+-----------+--------------+-----------+ +| id | movie | description | rating | ++---------+-----------+--------------+-----------+ +| 5 | House card| Interesting| 9.1 | +| 1 | War | great 3D | 8.9 | ++---------+-----------+--------------+-----------+ +``` + +- SQL Schema + +```sql +DROP TABLE +IF + EXISTS cinema; +CREATE TABLE cinema ( id INT, movie VARCHAR ( 255 ), description VARCHAR ( 255 ), rating FLOAT ( 2, 1 ) ); +INSERT INTO cinema ( id, movie, description, rating ) +VALUES + ( 1, 'War', 'great 3D', 8.9 ), + ( 2, 'Science', 'fiction', 8.5 ), + ( 3, 'irish', 'boring', 6.2 ), + ( 4, 'Ice song', 'Fantacy', 8.6 ), + ( 5, 'House card', 'Interesting', 9.1 ); +``` + +- 解题 + +```sql +#思路: +#1、观察测试用例的查询结果,我们知道,其实查询的是所有的字段 +#2、id 为奇数,则查询条件为 id % 2 = 1 +SELECT id,movie,description,rating +FROM cinema +WHERE id%2=1 AND description != 'boring' +ORDER BY rating DESC; +``` + +# 4、超过5名学生的课(596) + +[596. 超过5名学生的课](https://leetcode-cn.com/problems/classes-more-than-5-students/) + +- 问题描述 + +```html ++---------+------------+ +| student | class | ++---------+------------+ +| A | Math | +| B | English | +| C | Math | +| D | Biology | +| E | Math | +| F | Computer | +| G | Math | +| H | Math | +| I | Math | ++---------+------------+ +``` + +查找有五名及以上 student 的 class。 + +```html ++---------+ +| class | ++---------+ +| Math | ++---------+ +``` + +- SQL Schema + +```sql +DROP TABLE +IF + EXISTS courses; +CREATE TABLE courses ( student VARCHAR ( 255 ), class VARCHAR ( 255 ) ); +INSERT INTO courses ( student, class ) +VALUES + ( 'A', 'Math' ), + ( 'B', 'English' ), + ( 'C', 'Math' ), + ( 'D', 'Biology' ), + ( 'E', 'Math' ), + ( 'F', 'Computer' ), + ( 'G', 'Math' ), + ( 'H', 'Math' ), + ( 'I', 'Math' ); +``` + +- 解答 + +```sql +# 思路: +# 1、很显然要按照 class 进行分组 +# 2、然后按照分组后的 class 来统计学生的人数 +SELECT class +FROM courses +GROUP BY class +HAVING COUNT( DISTINCT student) >= 5; +``` + +# 5、查找重复的电子邮箱(182) + +[182. 查找重复的电子邮箱](https://leetcode-cn.com/problems/duplicate-emails/) + +- 问题描述 + +邮件地址表: + +```html ++----+---------+ +| Id | Email | ++----+---------+ +| 1 | a@b.com | +| 2 | c@d.com | +| 3 | a@b.com | ++----+---------+ +``` + +查找重复的邮件地址: + +```html ++---------+ +| Email | ++---------+ +| a@b.com | ++---------+ +``` + +- SQL Schema + +```sql +DROP TABLE +IF + EXISTS Person; +CREATE TABLE Person ( Id INT, Email VARCHAR ( 255 ) ); +INSERT INTO Person ( Id, Email ) +VALUES + ( 1, 'a@b.com' ), + ( 2, 'c@d.com' ), + ( 3, 'a@b.com' ); +``` + +- 解题 + +```sql +# 思路:与 596 题类似 +# 1、按照 mail 进行分组 +# 2、统计出现次数 >=2 就是重复的邮件 +SELECT Email +FROM Person +GROUP BY EMAIL +HAVING COUNT(id) >= 2; +``` + +# *6、删除重复的电子邮箱(196) + +[196. 删除重复的电子邮箱](https://leetcode-cn.com/problems/delete-duplicate-emails/) + +- 问题描述 + +邮件地址表: + +```html ++----+---------+ +| Id | Email | ++----+---------+ +| 1 | a@b.com | +| 2 | c@d.com | +| 3 | a@b.com | ++----+---------+ +``` + +删除重复的邮件地址: + +```html ++----+------------------+ +| Id | Email | ++----+------------------+ +| 1 | john@example.com | +| 2 | bob@example.com | ++----+------------------+ +``` + +- SQL Schema + +与 182 相同。 + +- 解题: + +```sql +# 思路一:将一张表看成两张表来进行操作 +DELETE p1 +FROM + Person p1, + Person p2 +WHERE + p1.Email = p2.Email + AND p1.Id > p2.Id +``` + +```sql +# 思路二: +# 第一步:根据 email 进行分组,获取 email 对应的最小 id,一个 email 对应一个最小的 id +SELECT min( id ) AS id FROM Person GROUP BY email; + +# 第二步:删除不在该 id 集合中的数据 +DELETE +FROM + Person +WHERE + id NOT IN ( SELECT id FROM ( SELECT min( id ) AS id FROM Person GROUP BY email ) AS m ); +# 应该注意的是上述解法额外嵌套了一个 SELECT 语句。 +# 如果不这么做,会出现错误:You can't specify target table 'Person' for update in FROM clause。 +``` + +```sql +DELETE +FROM + Person +WHERE + id NOT IN ( SELECT id FROM ( SELECT min( id ) AS id FROM Person GROUP BY email ) AS m ); +# 发生 You can't specify target table 'Person' for update in FROM clause。 +``` + +参考:[pMySQL Error 1093 - Can't specify target table for update in FROM clause](https://stackoverflow.com/questions/45494/mysql-error-1093-cant-specify-target-table-for-update-in-from-clause) + +# 7、组合两个表(175) + +[175. 组合两个表](https://leetcode-cn.com/problems/combine-two-tables/) + +- 问题描述: + +Person 表: + +```html ++-------------+---------+ +| Column Name | Type | ++-------------+---------+ +| PersonId | int | +| FirstName | varchar | +| LastName | varchar | ++-------------+---------+ +PersonId is the primary key column for this table. +``` + +Address 表: + +```html ++-------------+---------+ +| Column Name | Type | ++-------------+---------+ +| AddressId | int | +| PersonId | int | +| City | varchar | +| State | varchar | ++-------------+---------+ +AddressId is the primary key column for this table. +``` + +查找 FirstName, LastName, City, State 数据,而不管一个用户有没有填地址信息。 + +- SQL Schema + +```sql +DROP TABLE +IF + EXISTS Person; +CREATE TABLE Person ( PersonId INT, FirstName VARCHAR ( 255 ), LastName VARCHAR ( 255 ) ); +DROP TABLE +IF + EXISTS Address; +CREATE TABLE Address ( AddressId INT, PersonId INT, City VARCHAR ( 255 ), State VARCHAR ( 255 ) ); +INSERT INTO Person ( PersonId, LastName, FirstName ) +VALUES + ( 1, 'Wang', 'Allen' ); +INSERT INTO Address ( AddressId, PersonId, City, State ) +VALUES + ( 1, 2, 'New York City', 'New York' ); +``` + +- 解题: + +```sql +# 思路:左外连接 +SELECT p.FirstName,p.LastName,a.City,a.State +FROM Person p +LEFT JOIN Address a +ON p.PersonId=a.PersonId; +``` + +- 扩展: + + * 内连接:返回两张表的交集部分。 + +
+ + * 左连接: + +
+ + * 右连接: + +
+ +# *8、超过经理收入的员工(181) + +[181. 超过经理收入的员工](https://leetcode-cn.com/problems/employees-earning-more-than-their-managers/) + +- 问题描述: + +Employee 表: + +```html ++----+-------+--------+-----------+ +| Id | Name | Salary | ManagerId | ++----+-------+--------+-----------+ +| 1 | Joe | 70000 | 3 | +| 2 | Henry | 80000 | 4 | +| 3 | Sam | 60000 | NULL | +| 4 | Max | 90000 | NULL | ++----+-------+--------+-----------+ +``` + +查找薪资大于其经理薪资的员工信息。 + +- SQL Schema + +```sql +DROP TABLE +IF + EXISTS Employee; +CREATE TABLE Employee ( Id INT, NAME VARCHAR ( 255 ), Salary INT, ManagerId INT ); +INSERT INTO Employee ( Id, NAME, Salary, ManagerId ) +VALUES + ( 1, 'Joe', 70000, 3 ), + ( 2, 'Henry', 80000, 4 ), + ( 3, 'Sam', 60000, NULL ), + ( 4, 'Max', 90000, NULL ); +``` + +- 解题: + +```sql +# 思路:Employee e1 INNER JOIN Employee e2 ON e1.managerid = e2.id 比如 + ++----+-------+--------+-----------+ +| Id | Name | Salary | ManagerId | ++----+-------+--------+-----------+ +| 1 | Joe | 70000 | 3 | +| 2 | Henry | 80000 | 4 | +| 3 | Sam | 60000 | NULL | +| 4 | Max | 90000 | NULL | ++----+-------+--------+-----------+ + +根据 e1.managerid = e2.id 条件进行内连接后,得到 + ++----+-------+--------+-----------+-------+--------+-----------+ +| Id | Name | Salary | ManagerId | Name | Salary | ManagerId | ++----+-------+--------+-----------+-------+--------+-----------+ +| 1 | Joe | 70000 | 3 | Sam | 60000 | NULL | +| 2 | Henry | 80000 | 4 | Max | 90000 | NULL | ++----+-------+--------+-----------+-------+--------+-----------+ +``` + +```sql +SELECT + e1.name AS Employee # 注意:这里的 Employee 是给该字段起的别名,实际上查找的是姓名 +FROM + Employee e1 + INNER JOIN Employee e2 + ON e1.managerid = e2.id + AND e1.salary > e2.salary; +``` + +# 9、从不订购的客户(183) + +[181. 超过经理收入的员工](https://leetcode-cn.com/problems/employees-earning-more-than-their-managers/) + +- 问题描述: + +Curstomers 表: + +```html ++----+-------+ +| Id | Name | ++----+-------+ +| 1 | Joe | +| 2 | Henry | +| 3 | Sam | +| 4 | Max | ++----+-------+ +``` + +Orders 表: + +```html ++----+------------+ +| Id | CustomerId | ++----+------------+ +| 1 | 3 | +| 2 | 1 | ++----+------------+ +``` + +查找没有订单的顾客信息: + +```html ++-----------+ +| Customers | ++-----------+ +| Henry | +| Max | ++-----------+ +``` + +- SQL Schema + +```sql +DROP TABLE +IF + EXISTS Customers; +CREATE TABLE Customers ( Id INT, NAME VARCHAR ( 255 ) ); +DROP TABLE +IF + EXISTS Orders; +CREATE TABLE Orders ( Id INT, CustomerId INT ); +INSERT INTO Customers ( Id, NAME ) +VALUES + ( 1, 'Joe' ), + ( 2, 'Henry' ), + ( 3, 'Sam' ), + ( 4, 'Max' ); +INSERT INTO Orders ( Id, CustomerId ) +VALUES + ( 1, 3 ), + ( 2, 1 ); +``` + +- 解答 + +```sql +# 解法一:左外连接 +SELECT + c.Name AS Customers +FROM + Customers c + LEFT JOIN Orders o + ON c.Id = o.CustomerId +WHERE + o.CustomerId IS NULL; +``` + +```sql +# 解法二:子查询方式 +SELECT Name AS Customers +FROM + Customers +WHERE + Id NOT IN (SELECT CustomerId FROM Orders); +``` + +# *10、部门工资最高的员工(184) + +[184. 部门工资最高的员工](https://leetcode-cn.com/problems/department-highest-salary/) + +- 问题描述: + +Employee 表: + +```html ++----+-------+--------+--------------+ +| Id | Name | Salary | DepartmentId | ++----+-------+--------+--------------+ +| 1 | Joe | 70000 | 1 | +| 2 | Henry | 80000 | 2 | +| 3 | Sam | 60000 | 2 | +| 4 | Max | 90000 | 1 | ++----+-------+--------+--------------+ +``` + +Department 表: + +```html ++----+----------+ +| Id | Name | ++----+----------+ +| 1 | IT | +| 2 | Sales | ++----+----------+ +``` + +查找一个 Department 中收入最高者的信息: + +```html ++------------+----------+--------+ +| Department | Employee | Salary | ++------------+----------+--------+ +| IT | Max | 90000 | +| Sales | Henry | 80000 | ++------------+----------+--------+ +``` + +- SQL Schema + +```sql +DROP TABLE IF EXISTS Employee; +CREATE TABLE Employee ( Id INT, NAME VARCHAR ( 255 ), Salary INT, DepartmentId INT ); +DROP TABLE IF EXISTS Department; +CREATE TABLE Department ( Id INT, NAME VARCHAR ( 255 ) ); +INSERT INTO Employee ( Id, NAME, Salary, DepartmentId ) +VALUES + ( 1, 'Joe', 70000, 1 ), + ( 2, 'Henry', 80000, 2 ), + ( 3, 'Sam', 60000, 2 ), + ( 4, 'Max', 90000, 1 ); +INSERT INTO Department ( Id, NAME ) +VALUES + ( 1, 'IT' ), + ( 2, 'Sales' ); +``` + +- 解题: + +```sql +# 创建一个临时表,包含了部门员工的最大薪资。 +# 可以对部门进行分组,然后使用 MAX() 汇总函数取得最大薪资。 + +SELECT DepartmentId, MAX( Salary ) Salary FROM Employee GROUP BY DepartmentId; + +# 结果: ++--------------+--------+ +| DepartmentId | Salary | ++--------------+--------+ +| 1 | 90000 | +| 2 | 80000 | ++--------------+--------+ +``` + +使用连接找到一个部门中薪资等于临时表中最大薪资的员工。 + +```sql +SELECT d.name as Department, e.name as Employee, m.Salary +FROM + Employee e, + Department d, + (SELECT DepartmentId, MAX( Salary ) Salary FROM Employee GROUP BY DepartmentId) m +WHERE + e.DepartmentId=d.Id + AND e.DepartmentId=m.DepartmentId + AND e.Salary=m.Salary; +``` + +# 11、第二高的薪水(176) + +[176. 第二高的薪水](https://leetcode-cn.com/problems/second-highest-salary/) + +- 问题描述: + +```html ++----+--------+ +| Id | Salary | ++----+--------+ +| 1 | 100 | +| 2 | 200 | +| 3 | 300 | ++----+--------+ +``` + +查找工资第二高的员工。 + +```html ++---------------------+ +| SecondHighestSalary | ++---------------------+ +| 200 | ++---------------------+ +``` + +没有找到返回 null 而不是不返回数据。 + +- SQL Schema + +```sql +DROP TABLE +IF + EXISTS Employee; +CREATE TABLE Employee ( Id INT, Salary INT ); +INSERT INTO Employee ( Id, Salary ) +VALUES + ( 1, 100 ), + ( 2, 200 ), + ( 3, 300 ); +``` + +- 解题: + +```sql +# 查询所有 salary 按照降序排列 +SELECT DISTINCT Salary FROM Employee ORDER BY Salary DESC; +``` + +```sql +# 要获取第二高的的薪水,就是获取第二个元素, +# 使用 limit start,count; start:开始查询的位置,count 是查询多少条语句 +SELECT DISTINCT Salary FROM Employee ORDER BY Salary DESC LIMIT 1,1; +``` + +```sql +# 为了在没有查找到数据时返回 null,需要在查询结果外面再套一层 SELECT。 +SELECT + ( SELECT DISTINCT Salary FROM Employee ORDER BY Salary DESC LIMIT 1, 1 ) SecondHighestSalary; +``` + +# 12、第N高的薪水(177) + +[177. 第N高的薪水](https://leetcode-cn.com/problems/nth-highest-salary/) + +- 问题描述: + +查找工资第 N 高的员工。 + +- SQL Schema + +同 176。 + +- 解题: + +```sql +# 思路:其实与 176题目类似 +CREATE FUNCTION getNthHighestSalary ( N INT ) RETURNS INT BEGIN + +SET N = N - 1; #注意 LIMIT 的 start 是从 0 开始的,第 N 实际上是第 (N-1) +RETURN ( SELECT ( SELECT DISTINCT Salary FROM Employee ORDER BY Salary DESC LIMIT N, 1 ) ); + +END +``` + +# 13、分数排名(178) + +[178. 分数排名](https://leetcode-cn.com/problems/rank-scores/) + +- 问题描述: + +得分表: + +```html ++----+-------+ +| Id | Score | ++----+-------+ +| 1 | 3.50 | +| 2 | 3.65 | +| 3 | 4.00 | +| 4 | 3.85 | +| 5 | 4.00 | +| 6 | 3.65 | ++----+-------+ +``` + +将得分排序,并统计排名。 + +```html ++-------+------+ +| Score | Rank | ++-------+------+ +| 4.00 | 1 | +| 4.00 | 1 | +| 3.85 | 2 | +| 3.65 | 3 | +| 3.65 | 3 | +| 3.50 | 4 | ++-------+------+ +``` + +- SQL Schema + +```sql +DROP TABLE +IF + EXISTS Scores; +CREATE TABLE Scores ( Id INT, Score DECIMAL ( 3, 2 ) ); +INSERT INTO Scores ( Id, Score ) +VALUES + ( 1, 3.5 ), + ( 2, 3.65 ), + ( 3, 4.0 ), + ( 4, 3.85 ), + ( 5, 4.0 ), + ( 6, 3.65 ); +``` + +- 解题: + +```sql +#思路:关键在于如何统计 Rank +#这里使用同一张表进行内连接,注意连接的条件 S1.score <= S2.score 就是为了方便统计 Rank +SELECT + S1.score, + COUNT( DISTINCT S2.score ) Rank +FROM + Scores S1 + INNER JOIN Scores S2 + ON S1.score <= S2.score +GROUP BY + S1.id +ORDER BY + S1.score DESC; +``` + +# *14、连续出现的数字(180) + +[180. 连续出现的数字](https://leetcode-cn.com/problems/consecutive-numbers/) + +- 问题描述: + +数字表: + +```html ++----+-----+ +| Id | Num | ++----+-----+ +| 1 | 1 | +| 2 | 1 | +| 3 | 1 | +| 4 | 2 | +| 5 | 1 | +| 6 | 2 | +| 7 | 2 | ++----+-----+ +``` + +查找连续出现三次的数字。 + +```html ++-----------------+ +| ConsecutiveNums | ++-----------------+ +| 1 | ++-----------------+ +``` + +- SQL Schema + +```sql +DROP TABLE +IF + EXISTS LOGS; +CREATE TABLE LOGS ( Id INT, Num INT ); +INSERT INTO LOGS ( Id, Num ) +VALUES + ( 1, 1 ), + ( 2, 1 ), + ( 3, 1 ), + ( 4, 2 ), + ( 5, 1 ), + ( 6, 2 ), + ( 7, 2 ); +``` + +- 解题: + + +```sql +# 思路:要求是连续出现 3 次,可使用 3 张该表 +SELECT L1.Num ConsecutiveNums +FROM + Logs L1, + Logs L2, + Logs L3 +WHERE + L1.Id = L2.Id-1 + AND L2.ID = L3.Id-1 + AND L1.Num = L2.Num + AND L2.Num = L3.Num; +# 判断条件 Id 是不相同的,但是 Num 是相同的,并且 Id 是连续变化的 +``` + +```sql +# 由于要求是至少出现 3 次,所以需要 DISTINCT 进行去重 +SELECT DISTINCT L1.Num ConsecutiveNums +FROM + Logs L1, + Logs L2, + Logs L3 +WHERE + L1.Id = L2.Id-1 + AND L2.ID = L3.Id-1 + AND L1.Num = L2.Num + AND L2.Num = L3.Num; +``` + +```sql +# 另外一种写法 +SELECT DISTINCT L1.Num ConsecutiveNums +FROM + Logs L1 + LEFT JOIN Logs L2 ON L1.Id = L2.Id-1 + LEFT JOIN Logs L3 ON L1.Id = L3.Id-2 +WHERE L1.Num = L2.Num + AND L2.Num = L3.Num; +``` + +# 15、换座位(626)(了解) + +[626. 换座位](https://leetcode-cn.com/problems/exchange-seats/) + +- 问题描述: + +seat 表存储着座位对应的学生。 + +```html ++---------+---------+ +| id | student | ++---------+---------+ +| 1 | Abbot | +| 2 | Doris | +| 3 | Emerson | +| 4 | Green | +| 5 | Jeames | ++---------+---------+ +``` + +要求交换相邻座位的两个学生,如果最后一个座位是奇数,那么不交换这个座位上的学生。 + +```html ++---------+---------+ +| id | student | ++---------+---------+ +| 1 | Doris | +| 2 | Abbot | +| 3 | Green | +| 4 | Emerson | +| 5 | Jeames | ++---------+---------+ +``` + +- SQL Schema + +```sql +DROP TABLE +IF + EXISTS seat; +CREATE TABLE seat ( id INT, student VARCHAR ( 255 ) ); +INSERT INTO seat ( id, student ) +VALUES + ( '1', 'Abbot' ), + ( '2', 'Doris' ), + ( '3', 'Emerson' ), + ( '4', 'Green' ), + ( '5', 'Jeames' ); +``` + +- 解题: + +使用多个 union。 + +```sql +SELECT + s1.id - 1 AS id, + s1.student +FROM + seat s1 +WHERE + s1.id MOD 2 = 0 UNION +SELECT + s2.id + 1 AS id, + s2.student +FROM + seat s2 +WHERE + s2.id MOD 2 = 1 + AND s2.id != ( SELECT max( s3.id ) FROM seat s3 ) UNION +SELECT + s4.id AS id, + s4.student +FROM + seat s4 +WHERE + s4.id MOD 2 = 1 + AND s4.id = ( SELECT max( s5.id ) FROM seat s5 ) +ORDER BY + id; +``` diff --git "a/docs/HTTP/1_HTTP\346\246\202\350\277\260.md" "b/docs/HTTP/1_HTTP\346\246\202\350\277\260.md" new file mode 100644 index 00000000..edddb6c4 --- /dev/null +++ "b/docs/HTTP/1_HTTP\346\246\202\350\277\260.md" @@ -0,0 +1,85 @@ +# HTTP概述 + +## HTTP 协议的特点 + +- **支持客户端/服务器模式**。 + +- **简单快速。** + + 客户端向服务端请求服务时,只需要**传输请求方法和路径**。请求的方法常用的有 GET、POST。 + + HTTP 协议简单,是使得 **HTTP 服务程序规模小**,所以通信速度快。 + +- **灵活。** + + HTTP 允许传输任意类型的数据对象。 + +- **无连接。** + + 这里的无连接是指**限制每次连接只处理一个请求**。服务器处理客户端的请求,并且收到客户端的应答后,即可断开连接,可节省传输时间。 + +- **无状态。** + + HTTP 协议是无状态协议。缺少状态意味着如果候选处理需要前面的的信息,则必须重传。另一方面,服务端不需要先前信息时,应答较快。 + +## URL + +URI 包含 URL 和 URN,目前 WEB 只有 URL 比较流行,所以见到的基本都是 URL。 + +- URI(Uniform Resource Identifier,统一资源标识符) +- URL(Uniform Resource Locator,统一资源定位符) +- URN(Uniform Resource Name,统一资源名称) + +

+ +## 请求和响应报文 + +### 1. 请求报文 + +
+ +### 2. 响应报文 + +
+ +### 3. 请求响应步骤 + +- 客户端连接到 Web 服务器 +- 发送 HTTP 请求 +- 服务器接受 HTTP 请求并且返回 HTTP 响应 +- 释放 TCP 连接 +- 客户端浏览器解析 HTML 内容 + +## 输入 URL 地址,显示主页 + +- DNS 解析获取相应的 IP 地址。 + +- TCP 连接。根据 IP 地址、端口号和服务器建立 TCP 连接。 + +- 发送 HTTP 请求。 + + 将请求发送给服务器,Cookie 也会随着请求发送给服务器。 + +- 服务器接收请求并处理,返回 HTTP 报文。 + +- 客户端收到 HTML 进行解析并渲染页面。 + +- 断开连接。 + +## HTTP 1.0 与 HTTP 1.1 的区别 + +- HTTP/1.1 默认是长连接。关闭长连接 `Connection : close` + + HTTP/1.0 默认是短连接。使用长连接 `Connection:Keep-Alive` + +- HTTP/1.1 支持同时打开多个 TCP 连接 + +- HTTP/1.1 支持虚拟主机。 + +- HTTP/1.1 新增状态码。比如 409 Conflict 表请求的资源与**资源的当前状态**发生冲突。 + +- 带宽优化: + + HTTP/1.0 存在一些浪费带宽的现象,如客户端只需要某个对象的一部分,而服务器却将整个对象发送过过来。 + + HTTP/1.1 在请求头中引入 range,它允许请求资源的某个部分,返回状态码为 206 Partial Content。 \ No newline at end of file diff --git "a/docs/HTTP/2_HTTP\347\212\266\346\200\201\347\240\201.md" "b/docs/HTTP/2_HTTP\347\212\266\346\200\201\347\240\201.md" new file mode 100644 index 00000000..8b6d9264 --- /dev/null +++ "b/docs/HTTP/2_HTTP\347\212\266\346\200\201\347\240\201.md" @@ -0,0 +1,50 @@ +# HTTP状态码 + +服务器返回的 **响应报文** 中第一行为状态行,包含了状态码以及原因短语,用来告知客户端请求的结果。 + +| 状态码 | 类别 | 原因短语 | +| :---: | :---: | :---: | +| 1XX | Informational(信息性状态码) | 表示请求已接收,继续处理 | +| 2XX | Success(成功状态码) | 请求已被成功接收 | +| 3XX | Redirection(重定向状态码) | 要完成请求必须进行更进一步的处理的操作 | +| 4XX | Client Error(客户端错误状态码) | 请求无法错误或请求无法实现 | +| 5XX | Server Error(服务器错误状态码) | 服务器未能处理合法请求 | + +## 1XX 信息 + +- **100 Continue** :表明到目前为止都很正常,客户端可以继续发送请求或者忽略这个响应。 + +## 2XX 成功 + +- **200 OK** + +- **204 No Content** :请求已经成功处理,但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息,而不需要返回数据时使用。 + +- **206 Partial Content** :表示客户端进行了范围请求,响应报文包含由 Content-Range 指定范围的实体内容。 + +## 3XX 重定向 + +- **301 Moved Permanently** :永久性重定向 + +- **302 Found** :临时性重定向 + +- **303 See Other** :和 302 有着相同的功能,但是 303 明确要求客户端应该采用 GET 方法获取资源。 + +- 注:虽然 HTTP 协议规定 301、302 状态下重定向时不允许把 POST 方法改成 GET 方法,但是大多数浏览器都会在 301、302 和 303 状态下的重定向把 POST 方法改成 GET 方法。 + +- **304 Not Modified** :如果请求报文首部包含一些条件,例如:If-Match,If-Modified-Since,If-None-Match,If-Range,If-Unmodified-Since,如果不满足条件,则服务器会返回 304 状态码。 + +- **307 Temporary Redirect** :临时重定向,与 302 的含义类似,但是 307 要求浏览器不会把重定向请求的 POST 方法改成 GET 方法。 + +## 4XX 客户端错误 + +- **400 Bad Request** :客户端请求有语法错误,不能被服务器所理解 +- **401 Unauthorized** :请求未经授权,这个状态码必须和 *WWW-Authenticate* 一起使用 +- **403 Forbidden** :服务器收到请求,但是拒绝提供服务 +- **404 Not Found** :请求的资源不存在,比如输入错误的 URL + +## 5XX 服务器错误 + +- **500 Internal Server Error** :服务器发生不可预期的错误。 + +- **503 Service Unavailable** :服务器当前不能处理客户端的请求,一段时间后可能恢复正常。 diff --git "a/docs/HTTP/3_\345\205\267\344\275\223\345\272\224\347\224\250.md" "b/docs/HTTP/3_\345\205\267\344\275\223\345\272\224\347\224\250.md" new file mode 100644 index 00000000..9f3dc365 --- /dev/null +++ "b/docs/HTTP/3_\345\205\267\344\275\223\345\272\224\347\224\250.md" @@ -0,0 +1,214 @@ +# 具体应用 + +## 连接管理 + +

+ +### 1. 短连接与长连接 + +当浏览器访问一个包含多张图片的 HTML 页面时,除了请求访问 HTML 页面资源,还会请求图片资源。如果每进行一次 HTTP 通信就要新建一个 TCP 连接,那么开销会很大。 + +长连接只需要建立一次 TCP 连接就能进行多次 HTTP 通信。 + +- 从 HTTP/1.1 开始默认是长连接的,如果要断开连接,需要由客户端或者服务器端提出断开,使用 `Connection : close`; +- 在 HTTP/1.1 之前默认是短连接的,如果需要使用长连接,则使用 `Connection : Keep-Alive`。 + +### 2. 流水线 + +默认情况下,HTTP 请求是按顺序发出的,下一个请求只有在当前请求收到响应之后才会被发出。由于会受到网络延迟和带宽的限制,在下一个请求被发送到服务器之前,可能需要等待很长时间。 + +流水线是在同一条长连接上发出连续的请求,而不用等待响应返回,这样可以避免连接延迟。 + +## Cookie + +HTTP 协议是无状态的,主要是为了让 HTTP 协议尽可能简单,使得它能够处理大量事务。 + +HTTP/1.1 引入 Cookie 来**保存状态信息**。 + +
+ +
+ +Cookie 是服务器发送到用户浏览器并**保存在本地的一小块数据**,在浏览器之后向同一服务器再次发起请求时被携带上,用于告知服务端两个请求是否来自同一浏览器。由于之后每次请求都会需要携带 Cookie 数据,因此会带来额外的性能开销(尤其是在移动环境下)。 + +Cookie 曾一度用于客户端数据的存储,因为当时并没有其它合适的存储办法而作为唯一的存储手段,但现在随着现代浏览器开始支持各种各样的存储方式,Cookie 渐渐被淘汰。新的浏览器 API 已经允许开发者直接将数据存储到本地,如使用 Web storage API(本地存储和会话存储)或 IndexedDB。 + +### 1. 用途 + +- 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息) +- 个性化设置(如用户自定义设置、主题等) +- 浏览器行为跟踪(如跟踪分析用户行为等) + +### 2. 创建过程 + +服务器发送的响应报文包含 **Set-Cookie 首部字段**,客户端得到响应报文后把 Cookie 内容保存到浏览器中。 + +```html +HTTP/1.0 200 OK +Content-type: text/html +Set-Cookie: yummy_cookie=choco +Set-Cookie: tasty_cookie=strawberry + +[page content] +``` + +客户端之后对同一个服务器发送请求时,会从浏览器中取出 Cookie 信息并通过 Cookie 请求首部字段发送给服务器。 + +```html +GET /sample_page.html HTTP/1.1 +Host: www.example.org +Cookie: yummy_cookie=choco; tasty_cookie=strawberry +``` + +### 3. 分类 + +- 会话期 Cookie:浏览器关闭之后它会被自动删除,也就是说它仅在会话期内有效。 +- 持久性 Cookie:指定一个特定的过期时间(Expires)或有效期(max-age)之后就成为了持久性的 Cookie。 + +```html +Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; +``` + +### 4. 作用域 + +Domain 标识指定了哪些主机可以接受 Cookie。如果不指定,默认为当前文档的主机(不包含子域名)。如果指定了 Domain,则一般包含子域名。例如,如果设置 Domain=mozilla.org,则 Cookie 也包含在子域名中(如 developer.mozilla.org)。 + +Path 标识指定了主机下的哪些路径可以接受 Cookie(该 URL 路径必须存在于请求 URL 中)。以字符 %x2F ("/") 作为路径分隔符,子路径也会被匹配。例如,设置 Path=/docs,则以下地址都会匹配: + +- /docs +- /docs/Web/ +- /docs/Web/HTTP + +### 5. Session + +除了可以将用户信息通过 Cookie 存储在用户浏览器中,也可以利用 Session 存储在服务器端,存储在服务器端的信息更加安全。 + +服务器为每个 Session 分配一个唯一的 Session ID。Session 被创建后,调用 Session 相关方法向 Session 中添加内容,而这些内容保存在服务端,响应到客户端的只有 Session ID。 + +客户端再次发送请求后,会根据这个 Session ID,查找到相应的 Session。 + +Session 可以**存储在服务器**上的文件、数据库或者内存中。也可以**将 Session 存储在 Redis 这种内存型数据库中,效率会更高**。 + +使用 Session 维护用户登录状态的过程如下: + +- 用户进行登录时,用户提交包含用户名和密码的表单,放入 HTTP 请求报文中; +- 服务器验证该用户名和密码,如果正确则把用户信息存储到 Redis 中,它在 Redis 中的 Key 称为 Session ID; +- 服务器返回的响应报文的 Set-Cookie 首部字段包含了这个 Session ID,客户端收到响应报文之后将该 Cookie 值存入浏览器中; +- 客户端之后对同一个服务器进行请求时会包含该 Cookie 值,服务器收到之后提取出 Session ID,从 Redis 中取出用户信息,继续之前的业务操作。 + +应该注意 Session ID 的安全性问题,不能让它被恶意攻击者轻易获取,那么就不能产生一个容易被猜到的 Session ID 值。此外,还需要经常重新生成 Session ID。在对安全性要求极高的场景下,例如转账等操作,除了使用 Session 管理用户状态之外,还需要对用户进行重新验证,比如重新输入密码,或者使用短信验证码等方式。 + +> **补充:Session 的实现方式** + +> - 使用Cookie 来实现 + +服务器为每个Session分配一个唯一的JSESSIONID,并通过cookie发送给客户端。当客户端发起新的请求的时候,会在请求中携带这个JSESSIONID,这样服务器就能找到该客户端对应的Session。 + +> - 使用 URL 回写来实现 + +URL回写是指,服务器在发送给浏览器的所有连接中,都携带JSESSIONID的参数。 + +TOMCAT是默认使用两种方式,当 Cookie 被禁用就使用 URL 回写的方式。 + +### 6. 浏览器禁用 Cookie + +此时无法使用 Cookie 来保存用户信息,只能使用 Session。除此之外,不能再将 Session ID 存放到 Cookie 中,而是使用 URL 重写技术,将 Session ID 作为 URL 的参数进行传递。 + +### 7. Cookie 与 Session 选择 + +- Cookie 只能存储 ASCII 码字符串, + + Session 则可以存取任何类型的数据,因此在考虑数据复杂性时首选 Session; + +- Cookie 存储在浏览器中,容易被恶意查看。如果非要将一些隐私数据存在 Cookie 中,可以将 Cookie 值进行加密,然后在服务器进行解密; + +- 对于大型网站,如果用户所有的信息都存储在 Session 中,那么开销是非常大的,因此不建议将所有的用户信息都存储到 Session 中。若考虑减轻服务器负担,应当使用 Cookie。 + +## 缓存 + +### 1. 优点 + +- 缓解服务器压力; +- 降低客户端获取资源的延迟:缓存通常位于内存中,读取缓存的速度更快。并且缓存在地理位置上也有可能比源服务器来得近,例如浏览器缓存。 + +### 2. 实现方法 + +- 让代理服务器进行缓存; +- 让客户端浏览器进行缓存。 + +### 3. Cache-Control + +HTTP/1.1 通过 Cache-Control 首部字段来控制缓存。 + +**3.1 禁止进行缓存** + +no-store 指令规定不能对请求或响应的任何一部分进行缓存。 + +```html +Cache-Control: no-store +``` + +**3.2 强制确认缓存** + +no-cache 指令规定缓存服务器需要先向源服务器验证缓存资源的有效性,只有当缓存资源有效才将能使用该缓存对客户端的请求进行响应。 + +```html +Cache-Control: no-cache +``` + +**3.3 私有缓存和公共缓存** + +private 指令规定了将资源作为私有缓存,只能被单独用户所使用,一般存储在用户浏览器中。 + +```html +Cache-Control: private +``` + +public 指令规定了将资源作为公共缓存,可以被多个用户所使用,一般存储在代理服务器中。 + +```html +Cache-Control: public +``` + +**3.4 缓存过期机制** + +max-age 指令出现在请求报文中,并且缓存资源的缓存时间小于该指令指定的时间,那么就能接受该缓存。 + +max-age 指令出现在响应报文中,表示缓存资源在缓存服务器中保存的时间。 + +```html +Cache-Control: max-age=31536000 +``` + +Expires 首部字段也可以用于告知缓存服务器该资源什么时候会过期。 + +```html +Expires: Wed, 04 Jul 2012 08:26:05 GMT +``` + +- 在 HTTP/1.1 中,会优先处理 max-age 指令; +- 在 HTTP/1.0 中,max-age 指令会被忽略掉。 + +### 4. 缓存验证 + +需要先了解 ETag 首部字段的含义,它是资源的唯一标识。URL 不能唯一表示资源,例如 `http://www.google.com/` 有中文和英文两个资源,只有 ETag 才能对这两个资源进行唯一标识。 + +```html +ETag: "82e22293907ce725faf67773957acd12" +``` + +可以将缓存资源的 ETag 值放入 If-None-Match 首部,服务器收到该请求后,判断缓存资源的 ETag 值和资源的最新 ETag 值是否一致,如果一致则表示缓存资源有效,返回 304 Not Modified。 + +```html +If-None-Match: "82e22293907ce725faf67773957acd12" +``` + +Last-Modified 首部字段也可以用于缓存验证,它包含在源服务器发送的响应报文中,指示源服务器对资源的最后修改时间。但是它是一种弱校验器,因为只能精确到一秒,所以它通常作为 ETag 的备用方案。如果响应首部字段里含有这个信息,客户端可以在后续的请求中带上 If-Modified-Since 来验证缓存。服务器只在所请求的资源在给定的日期时间之后对内容进行过修改的情况下才会将资源返回,状态码为 200 OK。如果请求的资源从那时起未经修改,那么返回一个不带有消息主体的 304 Not Modified 响应。 + +```html +Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT +``` + +```html +If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT +``` diff --git a/docs/HTTP/4_HTTPs.md b/docs/HTTP/4_HTTPs.md new file mode 100644 index 00000000..e246b341 --- /dev/null +++ b/docs/HTTP/4_HTTPs.md @@ -0,0 +1,83 @@ +# HTTPS + +HTTP 有以下安全性问题: + +- 使用明文进行通信,内容可能会被窃听; +- 不验证通信方的身份,通信方的身份有可能遭遇伪装; +- 无法证明报文的完整性,报文有可能遭篡改。 + +HTTPS 并不是新协议,而是让 HTTP 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信,也就是说 HTTPS 使用了隧道进行通信。 + +通过使用 SSL,HTTPS 具有了加密(防窃听)、认证(防伪装)和完整性保护(防篡改)。 + +

+ +## 加密 + +### 1. 对称密钥加密 + +对称密钥加密(Symmetric-Key Encryption),加密和解密使用同一密钥。 + +- 优点:运算速度快; +- 缺点:无法安全地将密钥传输给通信方。 + +

+ +### 2.非对称密钥加密 + +非对称密钥加密,又称公开密钥加密(Public-Key Encryption),加密和解密使用不同的密钥。 + +公开密钥所有人都可以获得,通信发送方获得接收方的公开密钥之后,就可以使用公开密钥进行加密,接收方收到通信内容后使用私有密钥解密。 + +非对称密钥除了用来加密,还可以用来进行签名。因为私有密钥无法被其他人获取,因此通信发送方使用其私有密钥进行签名,通信接收方使用发送方的公开密钥对签名进行解密,就能判断这个签名是否正确。 + +- 优点:可以更安全地将公开密钥传输给通信发送方; +- 缺点:运算速度慢。 + +

+ +### 3. HTTPs 采用的加密方式 + +HTTPs 采用混合的加密机制,使用非对称密钥加密用于传输对称密钥来保证传输过程的安全性,之后使用对称密钥加密进行通信来保证通信过程的效率。(下图中的 Session Key 就是对称密钥) + +

+ +## 认证 + +通过使用 **证书** 来对通信方进行认证。 + +数字证书认证机构(CA,Certificate Authority)是客户端与服务器双方都可信赖的第三方机构。 + +服务器的运营人员向 CA 提出**公开密钥**的申请,CA 在判明提出申请者的身份之后,会对已申请的**服务器的公开密钥**做数字签名,然后分配这个已签名的公开密钥,并将该公开密钥放入公开密钥证书后绑定在一起。 + +进行 HTTPs 通信时,服务器会把证书发送给客户端。客户端取得其中的公开密钥之后,先使用数字签名进行验证,如果验证通过,就可以开始通信了。(客户端验证服务器的公钥是否有效) + +通信开始时,客户端需要使用服务器的公开密钥将自己的私有密钥传输给服务器,之后再进行对称密钥加密。 + +

+ +## 完整性保护 + +SSL 提供报文摘要功能来进行完整性保护。 + +HTTP 也提供了 MD5 报文摘要功能,但不是安全的。例如报文内容被篡改之后,同时重新计算 MD5 的值,通信接收方是无法意识到发生了篡改。 + +HTTPs 的报文摘要功能之所以安全,是因为它结合了加密和认证这两个操作。试想一下,加密之后的报文,遭到篡改之后,也很难重新计算报文摘要,因为无法轻易获取明文。 + + + +## HTTP 与 HTTPS 的区别 + +- HTTPS 需要向 CA 申请证书,需要支持证书授权的费用; + + HTTP 不需要收费 + +- HTTPS 是密文传输的,存在加密、加密过程,速度会更慢; + + HTTP 是密文传输 + +- HTTPS 默认使用 443 端口; + + HTTP 默认使用 80 端口 + +- HTTPS 先与 SSL 通信,再由 SSL 与 TCP 通信,在 HTTP 基础上增加了加密、认证、完整性保护的功能,比 HTTP 更安全。 \ No newline at end of file diff --git "a/docs/HTTP/5_get\345\222\214post\346\257\224\350\276\203.md" "b/docs/HTTP/5_get\345\222\214post\346\257\224\350\276\203.md" new file mode 100644 index 00000000..f3946147 --- /dev/null +++ "b/docs/HTTP/5_get\345\222\214post\346\257\224\350\276\203.md" @@ -0,0 +1,87 @@ +# GET 和 POST 比较 + +## Http报文层面: + +### 作用 + +GET 用于获取资源,而 POST 用于传输**实体主体**。 + +### 参数 + +GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中。不能因为 POST 参数存储在实体主体中就认为它的安全性更高,因为照样可以通过一些抓包工具(Fiddler)查看。 + +因为 URL 只支持 ASCII 码,因此 GET 的参数中如果存在中文等字符就需要先进行编码。例如 `中文` 会转换为 `%E4%B8%AD%E6%96%87`,而空格会转换为 `%20`。POST 参考支持标准字符集。 + +``` +GET /test/demo_form.asp?name1=value1&name2=value2 HTTP/1.1 +``` + +``` +POST /test/demo_form.asp HTTP/1.1 +Host: w3schools.com +name1=value1&name2=value2 +``` + +## 数据库层面: + +### 安全性 + +**安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的**。 + +GET 方法是安全的,而 POST 却不是,因为 POST 的目的是传送实体主体内容,这个内容可能是用户上传的表单数据,上传成功之后,服务器可能把这个数据存储到数据库中,因此状态也就发生了改变。 + +安全的方法除了 GET 之外还有:HEAD、OPTIONS。 + +不安全的方法除了 POST 之外还有 PUT、DELETE。 + +### 幂等性 + +**幂等的 HTTP 方法,同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的**。换句话说就是,幂等方法不应该具有副作用(统计用途除外)。 + +**所有的安全方法也都是幂等的**。 + +在正确实现的条件下,GET,HEAD,PUT 和 DELETE 等方法都是幂等的,而 POST 方法不是。 + +GET /pageX HTTP/1.1 是幂等的,连续调用多次,客户端接收到的结果都是一样的: + +``` +GET /pageX HTTP/1.1 +GET /pageX HTTP/1.1 +GET /pageX HTTP/1.1 +GET /pageX HTTP/1.1 +``` + +POST /add_row HTTP/1.1 不是幂等的,如果调用多次,就会增加多行记录: + +``` +POST /add_row HTTP/1.1 -> Adds a 1nd row +POST /add_row HTTP/1.1 -> Adds a 2nd row +POST /add_row HTTP/1.1 -> Adds a 3rd row +``` + +DELETE /idX/delete HTTP/1.1 是幂等的,即便不同的请求接收到的状态码不一样: + +``` +DELETE /idX/delete HTTP/1.1 -> Returns 200 if idX exists +DELETE /idX/delete HTTP/1.1 -> Returns 404 as it just got deleted +DELETE /idX/delete HTTP/1.1 -> Returns 404 +``` + +## 其他层面: + +### 可缓存 + +如果要对响应进行缓存,需要满足以下条件: + +- 请求报文的 HTTP 方法本身是可缓存的,包括 GET 和 HEAD,但是 PUT 和 DELETE 不可缓存,POST 在多数情况下不可缓存的。 +- 响应报文的状态码是可缓存的,包括:200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501。 +- 响应报文的 Cache-Control 首部字段没有指定不进行缓存。 + +### XMLHttpRequest + +为了阐述 POST 和 GET 的另一个区别,需要先了解 XMLHttpRequest: + +> XMLHttpRequest 是一个 API,它为客户端提供了在客户端和服务器之间传输数据的功能。它提供了一个通过 URL 来获取数据的简单方式,并且不会使整个页面刷新。这使得网页只更新一部分页面而不会打扰到用户。XMLHttpRequest 在 AJAX 中被大量使用。 + +- 在使用 XMLHttpRequest 的 POST 方法时,浏览器会先发送 Header 再发送 Data。但并不是所有浏览器会这么做,例如火狐就不会。 +- 而 GET 方法 Header 和 Data 会一起发送。 \ No newline at end of file diff --git a/docs/JVM/1_JVM.md b/docs/JVM/1_JVM.md index df862793..3ec65d58 100644 --- a/docs/JVM/1_JVM.md +++ b/docs/JVM/1_JVM.md @@ -1,8 +1,8 @@ -## 一、运行时数据区域 +# 运行时数据区域
-### 程序计数器(Program Counter Register) +## 程序计数器(Program Counter Register) - 当前线程所执行的字节码行号指示器(逻辑) - 通过改变计数器的值来选取下一条需要执行的字节码指令 @@ -10,7 +10,7 @@ - 对 Java 方法计数,如果是 Native 方法则计数器值为 Undefined - 只是计数,不会发生内存泄漏 -### Java 虚拟机栈 +## Java 虚拟机栈 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。 @@ -57,7 +57,7 @@ javap -verbose JVMTest
-### 本地方法栈 +## 本地方法栈 本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。 @@ -65,7 +65,7 @@ javap -verbose JVMTest
-### 堆 +## 堆 所有对象都在这里分配内存,是垃圾收集的主要区域("GC 堆")。 @@ -90,25 +90,49 @@ java -Xms1M -Xmx2M HackTheJava 3、堆式存储:编译时或运行时模块入口都无法确定,动态分配 -> JVM 内存模型中堆和栈的联系 + + +> **问题一:堆和栈的联系** 引用对象、数组时,栈里定义变量保存堆中目标的首地址。
-> JVM 内存模型中堆和栈的区别 +> **问题二:栈和堆的区别** + +- 物理地址 + + 堆的物理内存分配是不连续的; + + 栈的物理内存分配是连续的 + +- 分配内存 + + 堆是不连续的,分配的内存是在运行期确定的,大小不固定; + + 栈是连续的,分配的内存在编译器就已经确定,大小固定 + +- 存放内容 + + 堆中存放的是对象和数组,关注的是数据的存储; + + 栈中存放局部变量,关注的是程序方法的执行 + +- 是否线程私有 + + 堆内存中的对象对所有线程可见,可被所有线程访问; + + 栈内存属于某个线程私有的 -管理方式:栈自动释放,堆需要 GC +- 异常 -空间大小:栈比堆小 + 栈扩展失败,会抛出 StackOverflowError; -碎片相关:栈产生的碎片远小于堆 + 堆内存不足,会抛出 OutOfMemoryError -分配方式:栈支持静态和动态分配,而堆仅支持动态分配 -效率:栈的效率比堆高 -### 方法区 +## 方法区 用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 @@ -132,7 +156,7 @@ HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永 3、永久代会为 GC 带来不必要的复杂性 -### 运行时常量池 +## 运行时常量池 运行时常量池是方法区的一部分。 @@ -140,11 +164,11 @@ Class 文件中的常量池(编译器生成的字面量和符号引用)会 除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。 -### 直接内存 +## 直接内存 在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据。 -### JVM常见参数 +## JVM常见参数 | 参数 | 含义 | | ---- | ------------------------------------------------------------ | diff --git "a/docs/JVM/1_Java\345\206\205\345\255\230\345\214\272\345\237\237.md" "b/docs/JVM/1_Java\345\206\205\345\255\230\345\214\272\345\237\237.md" new file mode 100644 index 00000000..ae76568a --- /dev/null +++ "b/docs/JVM/1_Java\345\206\205\345\255\230\345\214\272\345\237\237.md" @@ -0,0 +1,458 @@ +# Java 运行数数据区域 + +Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。 + +JDK 1.8 和之前的版本略有不同: + +- JDK 1.7 之前**运行时常量池**逻辑包含**字符串常量池**存放在方法区 +- JDK 1.7 字符串常量池被从方法区中拿到了堆,运行时常量池剩下的内容还在方法区 +- JDK1.8 HotSpot 虚拟机**移除了永久代**,采用**元空间(Metaspace)** 代替方法区,这时候**字符串常量池还在堆**,运行时常量池还在方法区,只不过方法区的实现从永久代变成了元空间。 + +
+ +## 程序计数器 + +程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的**行号指示器(逻辑上)**。主要有以下两个作用: + +- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。 +- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。 + +此外,程序计数器还有如下特性: + +- 通过改变计数器的值来选取下一条需要执行的字节码指令 +- 和线程一对一的关系,即“线程私有” +- 对 Java 方法计数,如果是 Native 方法则计数器值为 Undefined +- 只是计数,不会发生内存泄漏,生命周期随着线程的创建而创建,随着线程的结束而死亡。 + +## Java 虚拟机栈 + +每个 Java 方法在执行的同时会创建一个栈帧用于存储**局部变量表**、**操作数栈**、动态链接、方法出口信息等。 + +
+ +从方法调用直至执行完成的过程,就对应着**一个栈帧在 Java 虚拟机栈中入栈和出栈的过程**。Java 方法有两种返回方式: + +- return 语句 +- 抛出异常 + +不管使用哪种返回方式都会导致栈帧被弹出。 + +可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小: + +```java +java -Xss512M HackTheJava +``` + +该区域可能抛出以下异常: + +- 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常; +- 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。 + +注意:**HotSpot 虚拟机的栈容量不可以进行动态扩展的**,所以在 HotSpot 虚拟机是不会由于虚拟机栈无法扩展而导致 OOM 的,但是如果申请时就失败,仍然会出现 OOM 异常。 + +### 局部变量表 + +局部变量表主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)和对象引用。 + +### 操作数栈 + +- 局部变量表:包含方法执行过程中的所有变量 +- 操作数栈:入栈、出栈、复制、交换、产生消费变量 + +通过 javap 命令分析 Java 汇编指令,感受操作数栈和局部变量表的关系。 + +定义测试类:该类中定义了一个静态方法 add() + +```java +public class JVMTest { + public static int add(int a ,int b) { + int c = 0; + c = a + b; + return c; + } +} +``` + +使用 javap 命令(javap 分析的是字节码文件) + +```html +javap -verbose JVMTest +``` + +得到如下汇编指令: + +```html + public static int add(int, int); + descriptor: (II)I + flags: ACC_PUBLIC, ACC_STATIC + Code: + stack=2, locals=3, args_size=2 + 0: iconst_0 + 1: istore_2 + 2: iload_0 + 3: iload_1 + 4: iadd + 5: istore_2 + 6: iload_2 + 7: ireturn + LineNumberTable: + line 5: 0 + line 6: 2 + line 7: 6 +``` + +解读上述指令: + +- stack = 2 说明栈的深度是 2 ; +- locals = 3 说明有 3 个本地变量 ; +- args_size = 2 说明该方法需传入 2 个参数 +- load 指令表示入操作数栈,store 表示出操作数栈 + +执行 add(1,2),说明局部变量表和操作数栈的关系: + +- 首先会将栈帧按照程序计数器指令从大到小依次入栈,栈帧按照程序计数器指令依次出栈。 +- 数据 1、2 是入参,已经存在局部变量表 0、1 位置 +- 首先执行 iconst_0,将数据 0 push 进操作数栈 +- 执行 istore_2,将数据 0 pop出操作数栈并放入局部变量表中 2 位置 +- 执行 iload_0,将 0 位置元素(数值 1) push 进操作数栈 +- 执行 iload_1,将 1 位置元素(数值 2) push 进操作数栈 +- 执行 iadd,将数值1和数值2元素 pop出操作数栈,执行加法运算后,得到结果3,将 3 push 进操作数栈 +- 执行 istore_2,将数据 3 pop出操作数栈并放入局部变量表中 2 位置 +- 执行 iload_2,将 2 位置元素(数值 3)push 进操作数栈 +- 执行 ireturn,返回操作数栈栈顶元素 + +
+
+ +## 本地方法栈 + +本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。 + +本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。 + +本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。 + +方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 。 + +## 堆 + +几乎所有的对象实例以及数组都在这里分配内存,是垃圾收集的主要区域,所以也被称作 **GC 堆(Garbage Collected Heap)**。 + +现代的垃圾收集器基本都是**采用分代收集算法**,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法。可以将堆分成两块: + +- 新生代 (Young Generation),新生代可以划分为 Eden 、From Survivor、To Survivor 空间 +- 老年代 (Old Generation) + +堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。 + +可以通过 -Xms 和 -Xmx 这两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。 + +```java +java -Xms1M -Xmx2M HackTheJava +``` + +JDK 7 版本及 JDK 7 版本之前,Hotspot 虚拟机的堆结构如下: + +- 新生代 (Young Generation) +- 老年代 (Old Generation) +- 永久代 (Permanent Generation) + +
+ +JDK 8 版本之后 HotSpot 虚拟机的永久代被彻底移除了,取而代之是元空间,元空间使用的是直接内存。 + +
+ +## 堆和栈的关系 + +Java 内存可以粗糙的区分为堆内存(Heap)和栈内存 (Stack),其中**栈就是现在说的虚拟机栈**,或者说是虚拟机栈中局部变量表部分。引用对象、数组时,栈里定义变量保存堆中目标的首地址。 + +栈和堆的区别: + +- 物理地址 + + 堆的物理内存分配是不连续的; + + 栈的物理内存分配是连续的 + +- 分配内存 + + 堆是不连续的,分配的内存是在运行期确定的,大小不固定; + + 栈是连续的,分配的内存在编译器就已经确定,大小固定 + +- 存放内容 + + 堆中存放的是对象和数组,关注的是数据的存储; + + 栈中存放局部变量,关注的是程序方法的执行 + +- 是否线程私有 + + 堆内存中的对象对所有线程可见,可被所有线程访问; + + 栈内存属于某个线程私有的 + +- 异常 + + 栈扩展失败,会抛出 StackOverflowError; + + 堆内存不足,会抛出 OutOfMemoryError + +## 方法区 + +用于存放已被加载的**类信息**、**常量**、**静态变量**、**即时编译器编译后的代码**等数据。 + +和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。 + +对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。 + +HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。 + +### 方法区与永久代 + +**方法区只是一个 JVM 规范**,在不同的 JVM 上方法区的实现可能是不同的。 + +方法区和永久代的关系类似 Java 中接口和类的关系,类实现了接口,**永久代就是 HotSpot 虚拟机对 JVM 规范中方法区的一种实现方式**。 + +方法区是 JVM 规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久代这一说法。 + +### 元空间与永久代 + +**方法区只是一个 JVM 规范,永久代与元空间都是其一种实现方式**。在 JDK 1.8 之后,原来永久代的数据被分到了堆和元空间中:**元空间存储类的元信息,静态变量和常量池等则放入堆中**。 + +元空间与永久代的最大区别在于:元空间使用本地内存,而永久代使用 JVM 的内存,元空间相比永久代具有如下优势: + +- 永久代存在一个 JVM 本身设置的固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。当元空间溢出时会得到如下错误: `java.lang.OutOfMemoryError: MetaSpace` 可以使用 `-XX:MaxMetaspaceSize` 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。 +- 元空间里面存放的是类的元数据,这样加载多少类的元数据就不由 `MaxPermSize` 控制了, 而由系统的实际可用空间来控制,可以加载更多的类。 + + + +## 运行时常量池 + +运行时常量池是方法区的一部分。 + +Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池表(用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中)。 + +运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量 +一定只有编译期才能产生,运行期间也可以将新的常量放入池中,例如 String 类的 intern()。 + + + +## 字符串常量池 + +JDK 1.7 之前**运行时常量池**逻辑包含**字符串常量池**存放在方法区。 + +JDK 1.7 **字符串常量池被单独拿到堆**,运行时常量池剩下的内容还在方法区。 + +JDK1.8 HotSpot 虚拟机**移除了永久代**,采用**元空间(Metaspace)** 代替方法区,这时候**字符串常量池还在堆**,运行时常量池还在方法区,只不过方法区的实现从永久代变成了元空间。 + +### String 对象创建方式 + +创建方式 1: + +```java +String str1 = "abcd"; +``` + +创建方式 2: + +```java +String str2 = new String("abcd"); +``` + +这两种不同的创建方法是有差别的: + +方式 1 是在**常量池**中获取对象("abcd" 属于字符串字面量,因此编译时期会在常量池中创建一个字符串对象)。 + +方式 2 会创建两个字符串对象(前提是常量池中还没有 "abcd" 字符串对象): + +- "abcd" 属于字符串字面量,因此编译时期会在常量池中创建一个字符串对象,该字符串对象指向这个 "abcd" 字符串字面量; +- 使用 new 的方式会在堆中创建一个字符串对象。 + +(**字符串常量"abcd"在编译期**就已经确定放入常量池,而 Java **堆上的"abcd"是在运行期**初始化阶段才确定)。 + +**str1 指向常量池中的 “abcd” 对象,而 str2 指向堆中的字符串对象。** + +### String 的 intern() 方法 + +String 的 intern() 是一个 **Native 方法**,当调用 intern() 方法时: + +- 如果运行时常量池中已经包含一个等于该 String 对象内容的字符串,则返回常量池中该字符串的引用。 +- 如果没有等于该 String 对象的字符串,JDK1.7 之前(不包含 1.7)是在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用,**JDK1.7 以及之后的处理方式是对于存在堆中的对象,在常量池中直接保存对象的引用,而不会重新创建对象。** + +```java +String s3 = new String("1") + new String("1"); +s3.intern(); +String s4 = "11"; +System.out.println(s3 == s4); +``` + +JDK 6 输出结果: + +```html +false +``` + +JDK 8 输出结果: + +```html +true +``` + +补充:[String 的 intern() 方法详解](https://www.cnblogs.com/wxgblogs/p/5635099.html) + +### 字符串拼接问题 + +```java +String str1 = "hello"; +String str2 = "world"; + +String str3 = "hello" + "world";//常量池中的对象 +String str4 = str1 + str2; //在堆上创建的新的对象 +String str5 = "helloworld";//常量池中的对象 + +System.out.println(str3 == str4); +System.out.println(str3 == str5); +System.out.println(str4 == str5); +``` + +输出结果如下: + +```html +false +true +false +``` + +str1、str2 是从字符串常量池中获取的对象。 + +对于 str3,字符串 "hello" 和字符串 "world" 相加有后得到 "helloworld",在字符串常量池中创建 "helloworld" 对象。 + +对于 str4,str1+str2 则会在堆中创建新的 "helloworld" 对象。 + +对于 str5,字符串常量池已有 "helloworld" 对象,str5 直接引用该对象。 + +
+ +所以,尽量避免多个字符串拼接,因为这样会重新创建新的对象。如果需要改变字符串的话,可以使用 StringBuilder 或者 StringBuffer。 + +## 直接内存 + +在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据。 + + + +# Hotpot 虚拟机对象 + +## 对象的内存布局 + +在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域: + +- 对象头 +- 实例数据 +- 对齐填充 + +
+ +### 对象头 + +Hotspot 虚拟机的对象头包括两部分信息: + +一部分用于存储对象自身的运行时数据(哈希码、GC分代年龄、锁状态标志等等); + +另一部分是类型指针,即对象指向它的**类元数据的指针**,虚拟机通过这个指针来**确定这个对象是哪个类的实例**。 + +### 实例数据 + +实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。 + +### 对齐填充 + +对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起**占位**作用。 + +因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。 + + + +## 对象的创建 + +Java 对象的创建过程分为以下5步: + +- 类加载检查 +- 分配内存 +- 初始化零值 +- 设置对象头 +- 执行 \ 方法 + +### 1. 类加载检查 + +虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的**符号引用**, +并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。 +如果没有,那必须先执行相应的类加载过程。 + +### 2. 分配内存 + +在类加载检查通过后,接下来虚拟机将为新生对象分配内存。 +对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定。 + + Java 堆内存是否规整,则取决于 GC 收集器的算法是“标记-清除”,还是“标记-整理”(也称作“标记-压缩”),值得注意的是,“复制算法”内存也是规整的。 + +#### 两种内存分配方式 + +##### 指针碰撞 + +- 原理:用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界值指针,只需要向着没用过的内存方向将指针移动一段与对象大小相等的距离。 +- 适用场景:堆内存规整(即没有内存碎片)的情况 +- GC(Garbage Collection)收集器:Serial、ParNew + +##### 空闲列表 + +- 原理:虚拟机会维护一个列表,在该列表中记录哪些内存块是可用的,在分配的时候,找一块足够大的内存块划分给对象示例,然后更新列表记录 +- 适用场景:堆内存规整 +- GC(Garbage Collection)收集器:CMS + +#### 内存分配并发问题 + +在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情, +作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全: + +- CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。 +- TLAB:每一个线程预先在Java堆中分配一块内存,称为**本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)**。哪个线程要分配内存,就在哪个线程的 TLAB 上分配,只有 TLAB 用完并分配新的 TLAB 时,才采用上述的 CAS 进行内存分配。 + +### 3. 初始化零值 + +内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作**保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用**,程序能访问到这些字段的数据类型所对应的零值。 + +### 4. 设置对象头 + +初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 +另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。 + +### 5. 执行 init 方法 + +在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,**\ 方法还没有执行,所有的字段都还为零**。所以一般来说,执行 new 指令之后会接着执行 \ 方法,把**对象按照程序员的意愿进行初始化**,这样一个真正可用的对象才算完全产生出来。 + + + +## 对象的访问定位 + +建立对象就是为了使用对象,我们的Java程序通过栈上的 reference 数据来操作堆上的具体对象。 +对象的访问方式视虚拟机的实现而定,目前主流的访问方式有两种:使用句柄、直接指针。 + +### 使用句柄 + +如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是**对象的句柄地址**,而句柄中包含了对象实例数据与类型数据各自的具体地址信息 。 + +
+ +### 直接指针 + +如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是**对象的地址**。 + +
+ +这两种对象访问方式各有优势: + +- 使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 **reference 本身不需要修改**。 +- 使用直接指针访问方式最大的好处就是**速度快**,它节省了一次指针定位的时间开销。 \ No newline at end of file diff --git a/docs/JVM/2_JVM.md b/docs/JVM/2_JVM.md index 3e315ceb..1b14d97e 100644 --- a/docs/JVM/2_JVM.md +++ b/docs/JVM/2_JVM.md @@ -1,6 +1,6 @@ -## 二、HotSpot 虚拟机对象 +# HotSpot 虚拟机对象 -### 对象的创建 +## 对象的创建 对象的创建步骤: @@ -52,7 +52,7 @@ 所以一般来说,执行 new 指令之后会接着执行 \ 方法, 把**对象按照程序员的意愿进行初始化**,这样一个真正可用的对象才算完全产生出来。 -### 对象的内存布局 +## 对象的内存布局 在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:对象头、实例数据、对齐填充。 @@ -79,7 +79,7 @@ Hotspot虚拟机的对象头包括两部分信息: 换句话说就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或2倍), 因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。 -### 对象的访问定位 +## 对象的访问定位 建立对象就是为了使用对象,我们的Java程序通过栈上的 reference 数据来操作堆上的具体对象。 对象的访问方式视虚拟机的实现而定,目前主流的访问方式有两种:使用句柄、直接指针。 diff --git a/docs/JavaBasics/01String.md b/docs/JVM/2_String.md similarity index 99% rename from docs/JavaBasics/01String.md rename to docs/JVM/2_String.md index 39442868..08de7058 100644 --- a/docs/JavaBasics/01String.md +++ b/docs/JVM/2_String.md @@ -1,4 +1,4 @@ -# 二、String +# String ## 概览 diff --git "a/docs/JVM/2_\345\236\203\345\234\276\346\224\266\351\233\206.md" "b/docs/JVM/2_\345\236\203\345\234\276\346\224\266\351\233\206.md" new file mode 100644 index 00000000..020c1779 --- /dev/null +++ "b/docs/JVM/2_\345\236\203\345\234\276\346\224\266\351\233\206.md" @@ -0,0 +1,425 @@ +# 垃圾收集 (Garbage Collection,GC) + +垃圾收集主要是针对堆和方法区进行。程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收。 + +## 判断一个对象是否可被回收 + +堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。 + +### 引用计数算法 + +为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。 + +在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正是因为**循环引用**的存在,因此 Java 虚拟机不使用引用计数算法。 + +```java +public class Test { + + public Object instance = null; + + public static void main(String[] args) { + Test a = new Test(); + Test b = new Test(); + a.instance = b; + b.instance = a; + a = null; + b = null; + doSomething(); + } +} +``` + +在上述代码中,a 与 b 引用的对象实例互相持有了对象的引用,因此当我们把对 a 对象与 b 对象的引用去除之后,由于两个对象还存在互相之间的引用,导致两个 Test 对象无法被回收。 + +- 优点:执行效率高,程序执行受影响较小。 +- 缺点:无法检测出循环引用的情况,引起内存泄漏。 + +### 可达性分析算法 + +通过判断对象的引用链是否可达来决定对象是否可以被回收。 + +以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。 + +
+ +**Java 虚拟机使用可达性分析算法来判断对象是否可被回收**,GC Roots 一般包含以下几种: + +- 虚拟机栈中局部变量表中引用的对象(栈帧中的本地方法变量表) +- 本地方法栈中 JNI(Native方法) 中引用的对象 +- 方法区中类静态属性引用的对象 +- 方法区中的常量引用的对象 +- 活跃线程的引用对象 + + + +### 方法区的回收 + +因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,所以在方法区上进行回收性价比不高。 + +主要是对常量池的回收和对类的卸载。 + +为了避免内存溢出,在大量使用反射和动态代理的场景都需要虚拟机具备类卸载功能。 + +类的卸载条件很多,需要满足以下三个条件,并且满足了条件也不一定会被卸载: + +- 该类所有的实例都已经被回收,此时堆中不存在该类的任何实例。 +- 加载该类的 ClassLoader 已经被回收。 +- 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。 + + + +### finalize() + +类似 C++ 的析构函数(注意:只是类似,C++ 析构函数调用确定,而 finalize() 方法是不确定的),用于关闭外部资源。但是 try-finally 等方式可以做得更好,并且该方法运行代价很高,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。 + +当垃圾回收器要宣告一个对象死亡时,至少要经历两次标记过程。 + +如果对象在进行可达性分析以后,没有与GC Root 直接相连接的引用量,就会被**第一次标记**,并且判断是否执行 finalize() 方法; + +如果这个对象覆盖了 finalize() 方法,并且未被引用,就会被放置于 F-Queue 队列,稍后由虚拟机创建的一个低优先级的 finalize() 线程去执行**触发 finalize() 方法**,在该方法中让对象重新被引用,从而实现自救。但是该线程的优先级比较低,执行过程随时可能会被终止。此外,自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会再调用该方法。 + + + +### 引用类型 + +无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。 + +JDK1.2 之前,Java 中引用定义:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。 + +JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)。 + +#### 强引用 (Strong Reference) + +被强引用关联的对象不会被回收。 + +使用 new 一个新对象的方式来创建强引用。 + +```java +Object obj = new Object(); +``` + +当内存空间不足,JVM 抛出 OOM Error 终止程序也不会回收具有强引用的对象,只有通过将对象设置为 null 来弱化引用,才能使其被回收。 + +#### 软引用 (Soft Reference) + +表示对象处在**有用但非必须**的状态。 + +被软引用关联的对象只有在内存不够的情况下才会被回收。可以用来实现内存敏感的高速缓存。 + +软引用可以和一个引用队列 ReferenceQueue 联合使用,如果软引用所引用的对象被垃圾回收,JVM 就会把这个软引用加入到与之关联的引用队列中。如果一个弱引用对象本身在引用队列中,就说明该引用对象所指向的对象被回收了。 + +使用 SoftReference 类来创建软引用。 + +```java +Object obj = new Object(); +SoftReference sf = new SoftReference(obj); +obj = null; // 使对象只被软引用关联 +``` + +#### 弱引用 (Weak Reference) + +表示非必须的对象,比软引用更弱一些。适用于偶尔被使用且不影响垃圾收集的对象。 + +被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。 + +弱引用可以和一个引用队列 ReferenceQueue 联合使用,如果弱引用所引用的对象被垃圾回收,JVM 就会把这个弱引用加入到与之关联的引用队列中。如果一个弱引用对象本身在引用队列中,就说明该引用对象所指向的对象被回收了。 + +使用 WeakReference 类来创建弱引用。 + +```java +Object obj = new Object(); +WeakReference wf = new WeakReference(obj); +obj = null; +``` + +#### 虚引用 (Phantom Reference) + +又称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。 + +不会决定对象的生命周期,任何时候都可能被垃圾回收器回收。**必须和引用队列 ReferenceQueue 联合使用**。 + +为一个对象设置虚引用的唯一目的是**能在这个对象被回收时收到一个系统通知,起哨兵作用**。具体来说,就是通过判断引用队列 ReferenceQueue 是否加入虚引用来判断被引用对象是否被 GC(垃圾回收线程) 回收:当 GC 准备回收一个对象时,如果发现它还仅有虚引用指向它,就会在回收该对象之前,把这个虚引用加入到与之关联的引用队列 ReferenceQueue 中。**如果一个虚引用对象本身就在引用队列中,就说明该引用对象所指向的对象被回收了**。 + +使用 PhantomReference 来创建虚引用。 + +```java +Object obj = new Object(); +ReferenceQueue queue = new ReferenceQueue(); +// 虚引用必须和引用队列 ReferenceQueue 联合使用 +PhantomReference pf = new PhantomReference(obj, queue); +obj = null; +``` + +总结: + +| 引用类型 | 被垃圾回收的时间 | 用途 | 生存时间 | +| -------- | ---------------- | -------------- | ----------------- | +| 强引用 | 从来不会 | 对象的一般状态 | JVM停止运行时终止 | +| 软引用 | 在内存不足的时候 | 对象缓存 | 内存不足时终止 | +| 弱引用 | 在垃圾回收的时候 | 对象缓存 | GC运行后终止 | +| 虚引用 | Unknown | 标记、哨兵 | Unknown | + + + +## 垃圾收集算法 + +### “标记-清除” 算法 + +
+ +在标记阶段,从根集合进行扫描,会检查每个对象是否为活动对象,如果是活动对象,则程序会在对象头部打上标记。 + +在清除阶段,会进行对象回收并取消标志位,另外,还会判断回收后的分块与前一个空闲分块是否连续,若连续,会合并这两个分块。回收对象就是把对象作为分块,连接到被称为 “空闲链表” 的单向链表,之后进行分配时只需要遍历这个空闲链表,就可以找到分块。 + +在分配时,程序会搜索空闲链表寻找空间大于等于新对象大小 size 的块 block。如果它找到的块等于 size,会直接返回这个分块;如果找到的块大于 size,会将块分割成大小为 size 与 (block - size) 的两部分,返回大小为 size 的分块,并把大小为 (block - size) 的块返回给空闲链表。 + +不足: + +- 标记和清除过程效率都不高; +- 会产生大量不连续的内存碎片,导致无法给大对象分配内存。 + +### ”标记-整理“ 算法 + +
+ +标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。 + +优点:不会产生内存碎片 + +不足:需要移动大量对象,处理效率比较低。 + +### ”复制“ 算法 + +
+ +将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。 + +主要不足是只使用了内存的一半。 + +现在的商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象全部复制到另一块 Survivor 上,最后清理 Eden 和使用过的那一块 Survivor。 + +HotSpot 虚拟机的 Eden 和 Survivor 大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 就不够用了,此时需要依赖于老年代进行空间分配担保,也就是借用老年代的空间存储放不下的对象。 + +### 分代收集 + +现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。 + +一般将堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法: + +- 新生代:新生代对象存活时间很短,所以可以选择**“复制”算法**,只需要付出少量对象的复制成本就可以完成每次垃圾收集。 +- 老年代:老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择**“标记-清除”或“标记-整理”算法**进行垃圾收集。 + +## Stop-the-World & SafePoint + +### Stop-the-World + +所谓 Stop-the-World(简称 STW),指的是 **JVM 由于要执行 GC 而停止了应用程序的执行** : + +- 可达性分析算法中 GC Roots 会导致所有 Java 执行线程停顿,原因如下: + - 分析工作必须在一一个能确保一致性的快照中进行 + - 一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上 + - 如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证 +- 被 STW 中断的应用程序线程会在完成 GC 之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样, 影响用户体验,所以需要减少 STW 的发生。 + +STW 事件和采用哪款垃圾收集器无关,所有的 GC 都有这个事件。哪怕是 G1 也不能完全避免 STW 情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间。 + +STW 是 JVM 在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。开发中不要用 System.gc() 这样会导致 STW 的发生。 + +目前,降低系统的停顿时间两种算法:增量收集算法和分区算法。 + +#### 增量收集算法 + +基本思想:如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。**垃圾收集线程一次只收集一小片区域的内存空间,接着切换到应用程序线程**。依次反复,直到垃圾收集完成。总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作。 + +不足:由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。 + +#### 分区算法 + +基本思想:一般来说,在相同条件下,堆空间越大,一次 GC 时所需要的时间就长,有关 GC 产生的停顿也越长。为了更好地控制 GC 产生的停顿时间,**将一块大的内存区域分割成多个小块**,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次 GC 所产生的停顿。 + +注意分区算法与分代收集算法是不同的:分代收集算法将按照对象的生命周期长短划分成两个部分,而分区算法将整个堆空间划分成**连续的不同小区间**,其中每个小区间都独立使用,独立回收,这样可以控制一次回收多少个小区间。 + +总结: + +- 增量收集算法是将总的收集量一部分一部分的去执行 +- 分区算法是将总的内存空间分为小分区,一次可控的去收集多少个小区间。 + +### SafePoint + +程序执行时并非可以在任何地方都能停顿下来开始 GC,只有在特定的位置才能停顿下来开始 GC,这些位置称为Safepoint 。 + +SafePoint 的选择很重要,如果太少可能导致 GC 等待的时间太长,如果太频繁可能导致运行时的性能问题。 + +大部分指令的执行时间都非常短暂,通常会根据是否具有让程序长时间执行的特征为标准。比如选择一些执行时间较长的指令作为 SafePoint,如方法调用、循环跳转和异常跳转等。 + + + +## 垃圾收集器 + +### 评估 GC 的性能指标 + +#### 吞吐量 + +运行用户代码的时间占总运行时间的比例。其中总运行时间 = 程序的运行时间 + 内存回收的时间。 + +比如:虚拟机总共运行了100分钟, 其中垃圾收集花掉1分钟,那吞吐量就是99%。 + +#### 暂停时间 + +执行垃圾收集时,程序的工作线程被暂停的时间。 + +”高吞吐量” 和 ”低暂停时间” 是矛盾的: + +- 如果选择以吞吐量优先,那么必然需要降低内存回收的执行频率,但是这样会导致 GC 需要更长的暂停时间来执行内存回收。 +- 如果选择以低延迟优先为原则,那么为了降低每次执行内存回收时的暂停时间,也只能频繁地执行内存回收,但这又引起了年轻代内存的缩减和导致程序吞吐量的下降。 + +**现在标准:在最大吞吐量优先的情况下,降低停顿时间**。 + +
+ + + +### 经典垃圾收集器 + +**如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。** + +虽然我们对各个收集器进行比较,但并非要挑选出一个最好的收集器。因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是**根据具体应用场景选择适合自己的垃圾收集器**。 + +
+ +以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。 JDK 9 取消了对 Serial+CMS、 +ParNew+Serial Old 这两个组合的支持。 + +- 单线程与多线程:单线程指的是垃圾收集器只使用一个线程,而多线程使用多个线程; +- 串行与并行:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并行指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。 + +#### 1. Serial 收集器 + +
+ +Serial 翻译为串行,也就是说它以串行的方式执行。 + +它是单线程的收集器,只会使用一个线程进行垃圾收集工作。 + +它的优点是简单高效,在单个 CPU 环境下,由于没有线程交互的开销,因此拥有最高的单线程收集效率。 + +它是 Client 场景下的默认新生代收集器,因为在该场景下内存一般来说不会很大。它收集一两百兆垃圾的停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿时间是可以接受的。 + +#### 2. ParNew 收集器 + +
+ +它是 Serial 收集器的多线程版本。 + +它是 Server 场景下默认的新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合使用。 + +#### 3. Parallel Scavenge 收集器 + +与 ParNew 一样是多线程收集器。 + +其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,因此**它被称为“吞吐量优先”收集器**。这里的吞吐量指 CPU 用于运行用户程序的时间占总时间的比值。 + +停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。 + +缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。 + +可以通过一个开关参数打开 GC 自适应的调节策略(GC Ergonomics),就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。 + +#### 4. Serial Old 收集器 + +
+ +是 Serial 收集器的老年代版本,也是给 Client 场景下的虚拟机使用。如果用在 Server 场景下,它有两大用途: + +- 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。 +- 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。 + +#### 5. Parallel Old 收集器 + +
+ +是 Parallel Scavenge 收集器的老年代版本。 + +**在注重吞吐量以及 CPU 资源敏感的场合**,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。 + +#### 6. CMS 收集器 + +
+ +CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。 + +分为以下六个流程: + +- 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。 +- 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。 +- 并发预清理:查找执行并发标记阶段从年轻代晋升到老年代的对象 +- **重新标记**:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。 +- 并发清除:清理垃圾对象,不需要停顿。 +- 并发重置:重置CMS收集器的数据结构,等待下一次垃圾回收。 + +在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。 + +CMS 具有以下缺点: + +- 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。 +- 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。 +- 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。 + +#### 7. G1 收集器 + +G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。 + +堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。 + +
+ +G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。 + +通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region,保证了 G1 在有限的时间内可以获取尽可能高的收集效率。这种方式的侧重点在于回收垃圾最大量的 Region),G1 (Garbage First) 因此得名。 + +每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。 + +
+ +如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤: + +- 初始标记 +- 并发标记 +- 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。 +- 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。 + +与其他垃圾收集器相比,G1 具备如下特点: + +- 并行和并发 + + 并行性:G1 在回收期间,可以有多个 GC 线程同时工作,有效利用多核计算能力。此时用户线程STW + + 并发性:G1 拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况 + +- 分代收集 + 将堆空间分为若干个区域(Region) ,这些区域中包含了逻辑上的年轻代和老年代。 + +- 空间整合 + + G1将内存划分为一个个的 Region,内存的回收以 Region 作为基本单位。从局部(两个 Region 之间)上来看是基于“复制”算法,整体来看是基于“标记 - 整理”算法,这两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC。 尤其是当 Java 堆非常大的时候,G1 的优势更加明显。 + +- 可预测的停顿 + + G1 除了追求低停顿外(G1 可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制),还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。 + +相较于CMS,G1还不具备全方位、压倒性优势: + +比如在用户程序运行过程中,G1 无论是为了垃圾收集产生的内存占用还是程序运行时的额外执行负载(overload) 都要比 CMS 要高。从使用经验上来说,对于小内存应用,CMS 的表现大概率会优于 G1,而 G1 在大内存应用上则发挥其优势。平衡点在6- 8GB之间。 + +7 种经典垃圾收集器总结如下: + +| 垃圾收集器 | 运行状态 | 作用位置 | 收集算法 | 特点 | 适用场景 | +| :----------: | :---------: | :------------: | :-----------------------: | :----------: | :--------------------------------: | +| Serial | 串行 | 新生代 | “复制”算法 | 响应速度优先 | 单 CPU 下的 Client 模式 | +| ParNew | 并行 | 新生代 | “复制”算法 | 响应速度优先 | 多 CPU 的 Server 模式配合 CMS 使用 | +| Parallel | 并行 | 新生代 | “复制”算法 | 吞吐量优先 | 后台运算且不需要太多交互的场景 | +| Serial Old | 串行 | 老年代 | “标记-整理”算法 | 响应速度优先 | 单 CPU 下的 Client 模式 | +| Parallel Old | 并行 | 老年代 | “标记-整理”算法 | 吞吐量优先 | 后台运算且不需要太多交互的场景 | +| CMS | 并发 | 老年代 | “标记-清除”算法 | 响应速度优先 | 互联网或 B/S 业务 | +| G1 | 并发 + 并行 | 新生代+ 老年代 | “标记-整理” + “复制” 算法 | 响应速度优先 | 面向服务端应用 | + diff --git a/docs/JVM/3_JVM.md b/docs/JVM/3_JVM.md index ad37c42b..706b4813 100644 --- a/docs/JVM/3_JVM.md +++ b/docs/JVM/3_JVM.md @@ -1,6 +1,6 @@ -## 三、String 类和常量池 +# String 类和常量池 -### 1、String 对象的两种创建方式 +## 1、String 对象的两种创建方式 ```java String str1 = "abcd"; @@ -19,7 +19,7 @@ System.out.println(str1==str2); //false str1 指向常量池中的 “abcd”,而 str2 指向堆中的字符串对象。 -### 2、intern() 方法 +## 2、intern() 方法 intern() 方法设计的初衷,就是重用 String 对象,以节省内存消耗。 @@ -83,7 +83,7 @@ true `String s4 = "11"`, 这一行代码会直接去常量池中创建,但是发现已经有这个对象了,此时 s4 就是指向 s3 引用对象的一个引用。因此 `s3 == s4 `返回了true。 -### 3、字符串拼接 +## 3、字符串拼接 ```java String str1 = "str"; diff --git a/docs/JVM/6_JVM.md "b/docs/JVM/3_\345\206\205\345\255\230\345\210\206\351\205\215\344\270\216\345\233\236\346\224\266\347\255\226\347\225\245.md" similarity index 71% rename from docs/JVM/6_JVM.md rename to "docs/JVM/3_\345\206\205\345\255\230\345\210\206\351\205\215\344\270\216\345\233\236\346\224\266\347\255\226\347\225\245.md" index 072e3a96..e1874468 100644 --- a/docs/JVM/6_JVM.md +++ "b/docs/JVM/3_\345\206\205\345\255\230\345\210\206\351\205\215\344\270\216\345\233\236\346\224\266\347\255\226\347\225\245.md" @@ -1,19 +1,26 @@ -## 六、内存分配与回收策略 +# 内存分配与回收策略 -[内存回收脑图](http://naotu.baidu.com/file/488e2f0745f7cfff1b03eb1c3d81fe3e?token=df2309819db31dde) +## HotSpot 虚拟机 GC 分类 -### Minor GC 和 Full GC +针对 HotSpot 虚拟机的实现,GC 可以分为 2 大类: -- Minor GC:回收新生代,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。 -- Full GC:回收老年代和新生代,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。 +- **部分收集(Partial GC)** -### 内存分配策略 + - 新生代收集(**Minor GC** / Young GC):回收新生代,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。 + - 老年代收集(**Major GC** / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代 Full GC。只有 CMS 的并发清除存在这个模式 + - 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。只有 G1 存在这个模式 -#### 1. 对象优先在 Eden 分配 +- **整堆收集(Full GC)** + + 收集整个 Java 堆,包括新生代、老年代和永久代(如果存在的话)。老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。 + +## 内存分配策略 + +### 1. 对象优先在 Eden 分配 大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。 -#### 2. 大对象直接进入老年代 +### 2. 大对象直接进入老年代 大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。 @@ -21,41 +28,41 @@ -XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 和 Survivor 之间的大量内存复制。 -#### 3. 长期存活的对象进入老年代 +### 3. 长期存活的对象进入老年代 为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。 -XX:MaxTenuringThreshold 用来定义年龄的阈值,默认值为15。 -#### 4. 动态对象年龄判定 +### 4. 动态对象年龄判定 虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。 -#### 5. 空间分配担保 +### 5. 空间分配担保 在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。 如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。 -### Full GC 的触发条件 +## Full GC 的触发条件 对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件: -#### 1. 调用 System.gc() +### 1. 调用 System.gc() 只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。 -#### 2. 老年代空间不足 +### 2. 老年代空间不足 老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。 为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。 -#### 3. 空间分配担保失败 +### 3. 空间分配担保失败 使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。具体内容请参考上面的第 5 小节。 -#### 4. JDK 1.7 及以前的永久代空间不足 +### 4. JDK 1.7 及以前的永久代空间不足 在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。 @@ -63,14 +70,6 @@ 为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。 -#### 5. Concurrent Mode Failure +### 5. Concurrent Mode Failure 执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。 - -### 常见的调优参数 - -| 参数 | 解释 | -| ------------------------ | ---------------------------------------------- | -| -XX:SurvivorRatio | Eden和Survivor的比值,默认为8:1 | -| -XX:NewRatio | 老年代和年轻代内存大小的比例 | -| -XX:MaxTenuringThreshold | 对象从年轻代晋升到老年代经过的GC次数的最大阈值 | \ No newline at end of file diff --git a/docs/JVM/4_JVM.md b/docs/JVM/4_JVM.md index 6d05dfc1..7de0cc58 100644 --- a/docs/JVM/4_JVM.md +++ b/docs/JVM/4_JVM.md @@ -1,4 +1,4 @@ -## 四、8 种基本类型的包装类和常量池 +# 8 种基本类型的包装类和常量池 - Java 基本类型的包装类的大部分都实现了常量池技术, 即Byte,Short,Integer,Long,Character,Boolean; 这 5 种包装类默认创建了数值 **[-128,127]** 的相应类型的缓存数据, 但是超出此范围仍然会去创建新的对象。 - **两种浮点数类型的包装类 Float , Double 并没有实现常量池技术**。 diff --git "a/docs/JVM/4_JVM\350\260\203\344\274\230.md" "b/docs/JVM/4_JVM\350\260\203\344\274\230.md" new file mode 100644 index 00000000..6624b20a --- /dev/null +++ "b/docs/JVM/4_JVM\350\260\203\344\274\230.md" @@ -0,0 +1,3 @@ +# JVM 调优 + +待补充。 \ No newline at end of file diff --git a/docs/JVM/5_JVM.md b/docs/JVM/5_JVM.md index ca7aa428..53ac509f 100644 --- a/docs/JVM/5_JVM.md +++ b/docs/JVM/5_JVM.md @@ -1,12 +1,12 @@ -## 五、垃圾收集 +# 垃圾收集 [垃圾回收的脑图](http://naotu.baidu.com/file/1eb8ce88025d3d160c2efbf03c7b62b5?token=64d1a334774221b1) 垃圾收集主要是针对堆和方法区进行。程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收。 -### 判断一个对象是否可被回收 +## 判断一个对象是否可被回收 -#### 1. 引用计数算法 +### 1. 引用计数算法 为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。 @@ -34,7 +34,7 @@ public class Test { - 优点:执行效率高,程序执行受影响较小。 - 缺点:无法检测出循环引用的情况,引起内存泄漏。 -#### 2. 可达性分析算法 +### 2. 可达性分析算法 通过判断对象的引用链是否可达来决定对象是否可以被回收。 @@ -50,7 +50,7 @@ Java 虚拟机使用该算法来判断对象是否可被回收,GC Roots 一般
-#### 3. 方法区的回收 +### 3. 方法区的回收 因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,所以在方法区上进行回收性价比不高。 @@ -64,7 +64,7 @@ Java 虚拟机使用该算法来判断对象是否可被回收,GC Roots 一般 - 加载该类的 ClassLoader 已经被回收。 - 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。 -#### 4. finalize() +### 4. finalize() 类似 C++ 的析构函数,用于关闭外部资源。但是 try-finally 等方式可以做得更好,并且该方法运行代价很高,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。 @@ -77,13 +77,13 @@ Java 虚拟机使用该算法来判断对象是否可被回收,GC Roots 一般 - 由于线程的优先级比较低,执行过程随时可能会被终止; - 给予对象最后一次重生的机会 -### 引用类型 +## 引用类型 无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。 Java 提供了四种强度不同的引用类型。 -#### 1. 强引用 +### 1. 强引用 被强引用关联的对象不会被回收。 @@ -95,7 +95,7 @@ Object obj = new Object(); 抛出OOM Error终止程序也不会回收具有强引用的对象,只有通过将对象设置为null来弱化引用,才能使其被回收。 -#### 2. 软引用 +### 2. 软引用 表示对象处在**有用但非必须**的状态。 @@ -109,7 +109,7 @@ SoftReference sf = new SoftReference(obj); obj = null; // 使对象只被软引用关联 ``` -#### 3. 弱引用 +### 3. 弱引用 表示非必须的对象,比软引用更弱一些。适用于偶尔被使用且不影响垃圾收集的对象。 @@ -123,7 +123,7 @@ WeakReference wf = new WeakReference(obj); obj = null; ``` -#### 4. 虚引用 +### 4. 虚引用 又称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。 @@ -149,9 +149,9 @@ obj = null; > 引用队列(ReferenceQueue):当GC(垃圾回收线程)准备回收一个对象时,如果发现它还仅有软引用(或弱引用,或虚引用)指向它,就会在回收该对象之前,把这个软引用(或弱引用,或虚引用)加入到与之关联的引用队列(ReferenceQueue)中。**如果一个软引用(或弱引用,或虚引用)对象本身在引用队列中,就说明该引用对象所指向的对象被回收了**。无实际的存储结构,存储逻辑依赖于内部节点之间的关系来表达。 -### 垃圾收集算法 +## 垃圾收集算法 -#### 1. 标记 - 清除 +### 1. 标记 - 清除
@@ -166,7 +166,7 @@ obj = null; - 标记和清除过程效率都不高; - 会产生大量不连续的内存碎片,导致无法给大对象分配内存。 -#### 2. 标记 - 整理 +### 2. 标记 - 整理
@@ -180,7 +180,7 @@ obj = null; - 需要移动大量对象,处理效率比较低。 -#### 3. 复制 +### 3. 复制
@@ -192,7 +192,7 @@ obj = null; HotSpot 虚拟机的 Eden 和 Survivor 大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 就不够用了,此时需要依赖于老年代进行空间分配担保,也就是借用老年代的空间存储放不下的对象。 -#### 4. 分代收集 +### 4. 分代收集 Stop-the-World @@ -212,7 +212,7 @@ Safepoint - 新生代使用:复制算法 - 老年代使用:标记 - 清除 或者 标记 - 整理 算法 -### 垃圾收集器 +## 垃圾收集器
@@ -221,7 +221,7 @@ Safepoint - 单线程与多线程:单线程指的是垃圾收集器只使用一个线程,而多线程使用多个线程; - 串行与并行:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并行指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。 -#### 1. Serial 收集器(-XX:+UseSerialGC) +### 1. Serial 收集器(-XX:+UseSerialGC)
@@ -233,7 +233,7 @@ Serial 翻译为串行,也就是说它以串行的方式执行。 它是 Client 场景下的默认新生代收集器,因为在该场景下内存一般来说不会很大。它收集一两百兆垃圾的停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿时间是可以接受的。 -#### 2. ParNew 收集器(-XX:+UseParNewGC) +### 2. ParNew 收集器(-XX:+UseParNewGC)
@@ -241,7 +241,7 @@ Serial 翻译为串行,也就是说它以串行的方式执行。 它是 Server 场景下默认的新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合使用。 -#### 3. Parallel Scavenge 收集器(-XX:+UseParallelGC) +### 3. Parallel Scavenge 收集器(-XX:+UseParallelGC) 与 ParNew 一样是多线程收集器。 @@ -253,7 +253,7 @@ Serial 翻译为串行,也就是说它以串行的方式执行。 可以通过一个开关参数打开 GC 自适应的调节策略(GC Ergonomics),就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。 -#### 4. Serial Old 收集器(-XX:+UseSerialOldGC) +### 4. Serial Old 收集器(-XX:+UseSerialOldGC)
@@ -262,7 +262,7 @@ Serial 翻译为串行,也就是说它以串行的方式执行。 - 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。 - 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。 -#### 5. Parallel Old 收集器(-XX:+UseParallelOldGC) +### 5. Parallel Old 收集器(-XX:+UseParallelOldGC)
@@ -270,7 +270,7 @@ Serial 翻译为串行,也就是说它以串行的方式执行。 **在注重吞吐量以及 CPU 资源敏感的场合**,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。 -#### 6. CMS 收集器(-XX:+UseConcMarkSweepGC) +### 6. CMS 收集器(-XX:+UseConcMarkSweepGC)
@@ -293,7 +293,7 @@ CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。 - 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。 - 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。 -#### 7. G1 收集器(-XX:+UseG1GC) +### 7. G1 收集器(-XX:+UseG1GC) G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。 @@ -323,4 +323,26 @@ G1 把堆划分成多个大小相等的独立区域(Region),新生代和 - 并行和并发 - 分代收集 - 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。 -- 可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。 \ No newline at end of file +- 可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。 + + + +## 降低停顿时间 + +### 1. 使用 CMS 收集器 + +CMS 收集器进行垃圾回收,有 4 个步骤: + +- 初始标记 +- 并发标记 +- 重新标记 +- 并发清除 + +其中初始标记和重新标记需要 "stop the world",但耗时时间最长的并发标记、并发清除过程中,GC 线程都可与用户线程一起工作。整体上说,CMS 和用户线程是并行的。 + +### 2. 增量算法 + +基本思路:若一次性将所有垃圾进行处理,会造成系统长时间的停顿,则就让 GC 线程与用户线程交替执行。每次 GC 线程只收集一小块区域的内存空间,接着切换到用户线程,重复几次,直至 GC 完成。 + +问题:存在线程切换和上下文切换,造成系统吞吐量下降。 + diff --git "a/docs/JVM/5_\347\261\273\346\226\207\344\273\266\347\273\223\346\236\204.md" "b/docs/JVM/5_\347\261\273\346\226\207\344\273\266\347\273\223\346\236\204.md" new file mode 100644 index 00000000..33dbf928 --- /dev/null +++ "b/docs/JVM/5_\347\261\273\346\226\207\344\273\266\347\273\223\346\236\204.md" @@ -0,0 +1,126 @@ +# 类文件结构 + +## 类文件概述 + +JVM 可以理解的代码就叫做**字节码**(即扩展名为 .class 的文件,即类文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。 + +字节码并不针对一种特定的机器,因此 Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。 + + + +## 类文件结构 + +根据 JVM 规范,Class 文件通过 ClassFile 定义: + +```c +ClassFile { + u4 magic; // Class 文件的标志 + u2 minor_version; // Class 的小版本号 + u2 major_version; // Class 的大版本号 + u2 constant_pool_count; // 常量池的数量 + cp_info constant_pool[constant_pool_count-1]; // 常量池 + u2 access_flags;// Class 的访问标记 + u2 this_class; // 当前类 + u2 super_class; // 父类 + u2 interfaces_count; // 接口 + u2 interfaces[interfaces_count]; // 一个类可以实现多个接口 + u2 fields_count; // Class 文件的字段属性 + field_info fields[fields_count]; // 一个类会可以有多个字段 + u2 methods_count; // Class 文件的方法数量 + method_info methods[methods_count]; // 一个类可以有个多个方法 + u2 attributes_count; // 此类的属性表中的属性数 + attribute_info attributes[attributes_count]; // 属性表集合 +} +``` + +通过分析 ClassFilee,得到 class 文件的组成: + +
+ +
+ + + +### 魔数 + +每个 Class 文件的头 4 个字节称为魔数(Magic Number),它的唯一作用是**确定这个文件是否为一个能被虚拟机接收的 Class 文件**。 + +### 文件版本号 + +紧接着魔数的四个字节存储的是 Class 文件的版本号:第 5 和第 6 字节位是**次版本号**(Minor Version),第 7 和第 8 字节位是**主版本号**(Major Version)。 + +每当 Java 发布大版本(比如 Java 8,Java 9)的时候,主版本号都会加 1。 + +可以使用 `javap -v` 命令来快速查看 Class 文件的版本号信息。 + +高版本的 Java 虚拟机可以执行低版本编译器生成的 Class 文件,但是低版本的 Java 虚拟机不能执行高版本编译器生成的 Class 文件。在开发时要确保开发的的 JDK 版本和生产环境的 JDK 版本保持一致。 + +### 常量池 + +紧接着主次版本号之后的是常量池,常量池的数量是 constant_pool_count-1。 + +这是因为常量池计数器是从 1 开始计数的,将第 0 项常量空出来是有特殊考虑的,索引值为 0 代表“不引用任何一个常量池项。 + +常量池主要存放两大常量:**字面量**和**符号引用**。 + +字面量比较接近于 Java 语言层面的的常量概念,如文本字符串、声明为 final 的常量值等。 + +符号引用则属于编译原理方面的概念,包括下面三类常量: + +- 类和接口的全限定名 +- 字段的名称和描述符 +- 方法的名称和描述符 + +常量池中每一项常量都是一个表:表开始的第一位是一个 u1(占 1 个字节) 类型的标志位 \- tag 来标识常量的类型,代表当前这个常量属于哪种常量类型。 + +### 访问标志 + +紧接着常量池的两个字节代表访问标志(Access Flag),这个标志用于识别一些类或者接口层次的访问信息,包括: + +- 这个 Class 是类还是接口 +- 是否为 public 或者 abstract 类型 +- 如果是类的话是否声明为 final 等 + +### 当前类 & 父类 + +类索引(This Class)用于确定这个类的全限定名,父类索引(Super Class)用于确定这个类的父类的全限定名,由于 Java 语言的单继承,所以父类索引只有一个,除了 `java.lang.Object` 之外,所有的 Java 类都有父类,因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为 0。 + +### 接口索引集合 + +接口索引集合用来描述这个类实现了哪些接口,这些被实现的接口将按 implements (如果这个类本身是接口的话则是extends)后的接口顺序从左到右排列在接口索引集合中。 + +### 字段表集合 + +字段表(Fields)用于描述接口或类中声明的变量。 + +字段包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。 + +```c +field_info{ + u2 access_flags; + u2 name_index; + u2 descriptor_index; + u2 attributes_count; + attribute_info attributes[attributes_count]; +} +``` + +其中: + +- access_flags:字段的作用域(public,protected,private);实例变量还是类变量(static);是否可被序列化(transient);可变性(final);可见性(volatile) +- name_index:对常量池的引用,表示字段的名称 +- descriptor_index:对常量池的引用,表示字段和方法的描述符 +- attributes_count:一个字段还会拥有一些额外的属性,表示额外属性的个数 +- attributes[attributes_count]:存放属性具体内容。 + +### 方法表集合 + +methods_count 表示方法的数量,而 method_info 表示方法表。 + +Class 文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式。方法表的结构如同字段表一样,依次包括了访问标志、名称索引、描述符索引、属性表集合几项。 + +因为 volatile 修饰符和 transient 修饰符不可以修饰方法,所以方法表的访问标志中没有这两个对应的标志,但是增加了 synchronized、native、abstract 等关键字修饰方法,所以也就多了这些关键字对应的标志。 + +### 属性表集合 + +在 Class 文件,字段表,方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。与 Class 文件中其它的数据项目要求的顺序、长度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写 入自己定义的属性信息,JVM 运行时会忽略掉它不认识的属性。 \ No newline at end of file diff --git a/docs/JVM/7_JVM.md "b/docs/JVM/6_\347\261\273\345\212\240\350\275\275\346\234\272\345\210\266.md" similarity index 53% rename from docs/JVM/7_JVM.md rename to "docs/JVM/6_\347\261\273\345\212\240\350\275\275\346\234\272\345\210\266.md" index b4f4ca9a..210636ca 100644 --- a/docs/JVM/7_JVM.md +++ "b/docs/JVM/6_\347\261\273\345\212\240\350\275\275\346\234\272\345\210\266.md" @@ -1,28 +1,18 @@ -## 七、类加载机制 +# 类的生命周期 -类是在运行期间第一次使用时动态加载的,而不是一次性加载所有类。因为如果一次性加载,那么会占用很多的内存。 - -### 类的生命周期 - -
+从类被加载到虚拟机内存中开始,到释放内存总共有 7 个阶段:加载(Loading),验证(Verification),准备(Preparation),解析(Resolution),初始化(Initialization),使用(Using),卸载(Unloading)。 -包括以下 7 个阶段: +其中验证,准备,解析三个部分统称为**连接(Linking)**。 -- **加载(Loading)** -- **验证(Verification)** -- **准备(Preparation)** -- **解析(Resolution)** -- **初始化(Initialization)** -- 使用(Using) -- 卸载(Unloading) +## 类加载过程 -### 类加载过程 +类是在运行期间第一次使用时动态加载的,而不是一次性加载所有类。因为如果一次性加载,那么会占用很多的内存。 -包含了加载、验证、准备、解析和初始化这 5 个阶段。 +类加载过程包括加载、连接和初始化这 3 个阶段。其中连接过程又分为 3 步:验证、准备和解析,所以可以说类加载过程包含了加载、验证、准备、解析和初始化这 5 个阶段。 -#### 1. 加载 +### 1. 加载 -加载是类加载的一个阶段,注意不要混淆。 +**加载是类加载的一个阶段,注意不要混淆**。 加载过程完成以下三件事: @@ -37,17 +27,22 @@ - 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。 - 由其他文件生成,例如由 JSP 文件生成对应的 Class 类。 -#### 2. 验证 +### 2. 验证 + +确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。包括如下验证: -确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。 +- 文件格式验证:验证字节流是否符合 Class 文件格式规范 +- 元数据验证:对字节码描述的信息进行语义分析,保证其描述的信息符合 Java 语言规范的要求 +- 字节码验证:通过数据流和控制流分析,确保程序语义是合法的、符合逻辑的。 +- 符号引用验证:确保解析动作能正确执行。 -#### 3. 准备 +### 3. 准备 -类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。 +类变量是被 static 修饰的变量,**准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存**。注意:在 JDK 7 及之后,HotSpot 已经把原本放在永久代的字符串常量池、**静态变量等移动到堆中**,这个时候类变量则会随着 Class 对象一起存放在 Java 堆中。 实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中。应该注意到,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。 -初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。 +初始值一般为 0 值,例如下面的类变量 value 的初始值是 0 而不是 123(初始化阶段才会赋值)。 ```java public static int value = 123; @@ -59,17 +54,15 @@ public static int value = 123; public static final int value = 123; ``` -#### 4. 解析 +### 4. 解析 -将常量池的符号引用替换为直接引用的过程。 +解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。 -其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。 +其中,符号引用指的是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。在程序实际运行时,只有符号引用是不够的,举个例子:在程序执行方法时,系统需要明确知道这个方法所在的位置。Java 虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法了。通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。 -
+**解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或字段、方法在内存中的指针或者偏移量**。 -#### 5. 初始化 - -
+### 5. 初始化 初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段是虚拟机执行类构造器 <clinit>() 方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。 @@ -79,13 +72,13 @@ public static final int value = 123; public class Test { static { i = 0; // 给变量赋值可以正常编译通过 - System.out.print(i); // 这句编译器会提示“非法向前引用” + System.out.print(i); // 这句编译器会提示“非法向前引用”,因为变量 i 在后面才定义的。 } static int i = 1; } ``` -由于父类的 <clinit>() 方法先执行,也就意味着父类中定义的静态语句块的执行要优先于子类。例如以下代码: +由于父类的 <clinit>() 方法先执行,也就意味着**父类中定义的静态语句块的执行要优先于子类**。例如以下代码: ```java static class Parent { @@ -108,52 +101,169 @@ public static void main(String[] args) { 虚拟机会保证一个类的 <clinit>() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 <clinit>() 方法完毕。如果在一个类的 <clinit>() 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。 -> loadClass 和 forName 的区别? -- loadClass:得到的是第一个阶段加载的class对象,并没有初始化,例如Spring中Bean的实例的懒加载就是通过这种方式实现的。 -- forName :得到的是第五阶段初始化的class对象,例如JDBC中的数据连接。 -### 类初始化时机 +## 类初始化时机 + +### 1. 主动引用 -#### 1. 主动引用 +只有主动去使用类才会初始化类。 -虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随之发生): +虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列 6 种情况必须对类进行初始化(加载、验证、准备都会随之发生): -- 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。 -- 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。 -- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。 -- 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类; -- 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化; +- 情况 1:遇到 new、getstatic、putstatic 或 invokestatic 这四条字节码指令时,如果类型没有进行过初始 + 化,则需要先触发其初始化阶段。能够生成这四条指令的典型 Java 代码场景有: + - 使用 new 关键字实例化对象的时候。 + - 设置一个类型的静态字段(**被 final 修饰、已在编译期把结果放入常量池的静态字段除外**)的时候。 + - 读取一个类型的静态字段(**被 final 修饰、已在编译期把结果放入常量池的静态字段除外**)的时候。 + - 调用一个类型的静态方法的时候。( JVM 执行 invokestatic 指令时会初始化类) +- 情况 2:使用 java.lang.reflect 包的方法对类型进行**反射调用**的时候,如果类型没有进行过初始化,则需 + 要先触发其初始化。 +- 情况 3:当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。 +- 情况 4:当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先 + 初始化这个主类。 +- 情况 5:当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。 +- 情况 6:当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。 -#### 2. 被动引用 +### 2. 被动引用 -以上 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括: +以上 6 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括: - 通过子类引用父类的静态字段,不会导致子类初始化。 -```java -System.out.println(SubClass.value); // value 字段在 SuperClass 中定义 -``` + 举个例子: + + ```java + public class SuperClass { + static { + System.out.println("SuperClass init!"); + } + + public static int value=123; + } + ``` + + ```java + public class SubClass extends SuperClass{ + static { + System.out.println("SubClass init!"); + } + } + ``` + + ```java + public class NotInitialization { + public static void main(String[] args) { + System.out.println(SubClass.value); + } + } + ``` + + 输出结果为: + + ```html + SuperClass init! + 123 + ``` + + 这是因为对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。 + +- 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,**数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法**(数组类型不通过类加载器创建,它由 JVM 直接创建)。 + + 举个例子: + + ```java + public class SuperClass { + static { + System.out.println("SuperClass init!"); + } + + public static int value=123; + } + ``` + + ```java + public class SubClass extends SuperClass{ + static { + System.out.println("SubClass init!"); + } + } + ``` + + ```java + public class NotInitialization { + public static void main(String[] args) { + SuperClass[] sca = new SuperClass[10]; + } + } + ``` + + 输出结果为: + + ```html + 没有输出 SuperClass init! + ``` -- 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。 +- 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。 -```java -SuperClass[] sca = new SuperClass[10]; -``` + 举了例子: -- 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。 + ```java + public class ConstClass { + static { + System.out.println("ConstClass init!"); + } + public static final String HELLOWORLD = "hello world"; + } + ``` -```java -System.out.println(ConstClass.HELLOWORLD); -``` + ```java + public class NotInitialization { + public static void main(String[] args) { + System.out.println(ConstClass.HELLOWORLD); + } + } + ``` + + 输出结果为: + + ```html + hello world + ``` -### 类与类加载器 + +## loadClass 和 forName + +loadClass 和 forName 方法都可以获取 Class 对象,但是两者是有区别的: + +- loadClass 得到的是加载阶段的 Class 对象,并没有初始化,例如 Spring 中 Bean 的实例的懒加载就是通过这种方式实现的。 +- forName 得到的是初始化阶段的 Class 对象,例如 JDBC 中的数据连接。 + +## 卸载 + +卸载类即该类的 Class 对象被 GC。 + +类的卸载条件很多,需要满足以下三个条件,并且满足了条件也不一定会被卸载: + +- 该类所有的实例都已经被回收,此时堆中不存在该类的任何实例。 +- 加载该类的 ClassLoader 已经被回收。 +- 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。 + +JDK 自带的 BootstrapClassLoader, ExtClassLoader 和 AppClassLoader 负责加载 JDK 提供的类,所以这些类加载器的实例肯定不会被回收。但是我们自定义的类加载器的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。 + + + +# 类加载器 + +## 类与类加载器 + +所有的类都由类加载器加载,加载的作用就是将 .class 文件加载到内存中。 两个类相等,需要类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间。 这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true。 -### 类加载器分类 +## 类加载器分类 从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器: @@ -168,27 +278,25 @@ System.out.println(ConstClass.HELLOWORLD);
-### 双亲委派模型 +## 双亲委派模型 -应用程序是由三种类加载器互相配合从而实现类加载,除此之外还可以加入自己定义的类加载器。 +每一个类都有一个对应它的类加载器。系统中的 ClassLoader 在协同工作的时候会默认使用双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器。这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance)。 -下图展示了类加载器之间的层次关系,称为双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器。这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance)。 +
-
+### 1. 工作过程 -#### 1. 工作过程 +在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,首先会把该请求委派给父类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。当父类加载器无法处理时,才由自己来处理。当父类加载器为 null 时,会使用启动类加载器 BootstrapClassLoader 作为父类加载器。 -一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载。 - -#### 2. 好处 +### 2. 好处 使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。 -可以避免多份同样的字节码的加载 +可以避免多份同样的字节码的加载。 例如 java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 并放到 ClassPath 中,程序可以编译通过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,这是因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。rt.jar 中的 Object 优先级更高,那么程序中所有的 Object 都是这个 Object。 -#### 3. 实现 +### 3. 实现 以下是抽象类 java.lang.ClassLoader 的代码片段,其中的 loadClass() 方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException,此时尝试自己去加载。 @@ -236,7 +344,7 @@ public abstract class ClassLoader { } ``` -### 自定义类加载器实现 +## 自定义类加载器实现 以下代码中的 FileSystemClassLoader 是自定义类加载器,继承自 java.lang.ClassLoader,用于加载文件系统上的类。它首先根据类的全名在文件系统上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过 defineClass() 方法来把这些字节代码转换成 java.lang.Class 类的实例。 @@ -283,4 +391,8 @@ public class FileSystemClassLoader extends ClassLoader { + className.replace('.', File.separatorChar) + ".class"; } } -``` \ No newline at end of file +``` + +# 补充 + +- [main() 方法详解](https://www.cnblogs.com/bingyimeiling/p/10409728.html) \ No newline at end of file diff --git "a/docs/JVM/7_Java\347\250\213\345\272\217\347\274\226\350\257\221\345\222\214\350\277\220\350\241\214\350\277\207\347\250\213.md" "b/docs/JVM/7_Java\347\250\213\345\272\217\347\274\226\350\257\221\345\222\214\350\277\220\350\241\214\350\277\207\347\250\213.md" new file mode 100644 index 00000000..6e207c69 --- /dev/null +++ "b/docs/JVM/7_Java\347\250\213\345\272\217\347\274\226\350\257\221\345\222\214\350\277\220\350\241\214\350\277\207\347\250\213.md" @@ -0,0 +1,84 @@ +# Java 程序编译和运行过程 + +Java程序从 \.java 文件创建到程序运行要经过两大过程: + +- \.java 文件由编译器编译成 \.class文件 +- 字节码由 JVM 解释运行 + +## 编译过程 + +.java 源文件会被 Java编译器进行编译为.class文件: + +- Java 编译一个类时,如果这个类所依赖的类还没有被编译,编译器会自动的先编译这个所依赖的类,然后引用。如果 Java 编译器在指定的目录下找不到该类所依赖的类的 \.class文件或者 \.java源文件,则会报 + "Cant found sysbol" 的异常错误。 +- 编译后的 \.class 文件主要分为两部分:常量池和方法表集合。 + 常量池记录的是代码出现过的(常量、类名、成员变量等)以及符号引用(类引用、方法引用,成员变量引用等); + 方法表集合则记录各个方法的字节码。 + + + +## 运行过程 + +JVM 并不是在运行时就会把所有使用到的类都加载到内存中,而是用到的时候,才加载进方法区,并且只加载一次。 +Java类运行的过程大概分为两个步骤: + +- 类加载 +- 执行类 + +举例说明 Java 程序运行过程: + +```java +public class Person { + private String name; + + public Person(String name){ + this.name=name; + } + + public void sayHello(){ + System.out.println("Hello! My Name is: " + name); + } +} +``` + +```java +public class JVMTest { + public static void main(String[] args) { + Person p=new Person("Li Ming"); + p.sayHello(); + } +} +``` + +### 1. 类加载 + +首先编译 JVMTest.java 文件得到 JVMTest.class 文件,系统启动一个 JVM 进程,从 classpath 路径中找到 JVMTest.class 文件,将 JVMTest 的类信息加载到方法区中,这个过程称为 JVMTest 类的加载。 + +(只有类信息在方法区中,才能创建对象,使用类中的成员变量) + +### 2. JVM 找 main() 方法入口 + +在 main() 方法 入口持有一个指向当前类 (JVMTest) 常量池的指针,常量池中的第一项是一个对 Person 对象的符号引用。 + +main 方法中 `Person p=new Person("Li Ming"),JVM 需要创建一个 Person 对象,但是此时方法区中是没有 Person 类信息的,所以 JVM 需要加载 Person 类,将 Person 类的信息加载到方法区中。 + +JVM 以一个直接指向方法区 Person 类的指针替换了常量池中第一项的符号引用。 + +### 3. 实例化对象 + +加载完 Person 类的信息以后,JVM 就会在堆中为一个 Person 实例分配内存,然后调用构造方法初始化 Person 实例,并且该实例**持有指向方法区中的 Person 类的类型信息(其中包括方法表)的引用**。 + +(p 为指向该 Person 实例的引用,会被放到栈中) + +### 4. 运行方法 + +执行 p.sayHello(),JVM 根据栈中 p 的引用找到 Person 对象,然后根据 Person 对象持有的引用定位到方法区中 Person 类类信息的**方法表**,获得 sayHello 方法的字节码地址,然后开始运行方法。 + +
+ +
+ + +# 补充 + +- [main() 方法详解](https://www.cnblogs.com/bingyimeiling/p/10409728.html) \ No newline at end of file diff --git "a/docs/JavaBasics/05\345\217\215\345\260\204.md" "b/docs/JavaBasics/05\345\217\215\345\260\204.md" deleted file mode 100644 index 0ddc9eb8..00000000 --- "a/docs/JavaBasics/05\345\217\215\345\260\204.md" +++ /dev/null @@ -1,337 +0,0 @@ -# 六、反射 -## 反射简介 -每个类都有一个 **Class** 对象,包含了与类有关的信息。 -当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。 - -类加载相当于 Class 对象的加载,类在第一次使用时才动态加载到 JVM 中。也可以使用 `Class.forName("com.mysql.jdbc.Driver")` 这种方式来控制类的加载,该方法会返回一个 Class 对象。 - -反射可以提供**运行时的类信息**,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。 - -Class 和 java.lang.reflect 一起对反射提供了支持,java.lang.reflect 类库主要包含了以下三个类: - -- **Field** :可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段; -- **Method** :可以使用 invoke() 方法调用与 Method 对象关联的方法; -- **Constructor** :可以用 Constructor 创建新的对象。 - -**反射的优点:** - -* **可扩展性** :应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类。 -* **类浏览器和可视化开发环境** :一个类浏览器需要可以枚举类的成员。可视化开发环境(如 IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码。 -* **调试器和测试工具** : 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率。 - -**反射的缺点:** - -尽管反射非常强大,但也不能滥用。如果一个功能可以不用反射完成,那么最好就不用。在我们使用反射技术时,下面几条内容应该牢记于心。 - -* **性能开销** :反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。 - -* **安全限制** :使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了。 - -* **内部暴露** :由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。 - -## 反射的使用 -**1. 获取Class对象** - -获得Class对象方法有三种: - -(1)使用Class类的forName静态方法 - -public static Class forName(String className) - -(2)直接获取某一个对象的class,比如: - -Class klass = int.class; -Class classInt = Integer.TYPE; - -(3)调用某个对象的getClass()方法,比如: - - StringBuilder str = new StringBuilder("123"); - Class klass = str.getClass(); -```java -public class GetClazzObject { - public static void main(String[] args) throws ClassNotFoundException { - Class clazz=Class.forName("code_05_reflection.Obj"); - System.out.println(clazz);//class code_05_reflection.Obj - - Class clazz2=Obj.class; - System.out.println(clazz2);//class code_05_reflection.Obj - - Obj obj=new Obj(); - Class clazz3=obj.getClass(); - System.out.println(clazz3);//class code_05_reflection.Obj - } -} -``` - -**2.判断是否是某个类实例** - -判断是否是某个类实例,使用Class类中的 isInstance()方法 -```java -public native boolean isInstance(Object obj); -``` -```java -public class IsInstanceOfClass { - public static void main(String[] args) { - Class clazz=Obj.class; - - Obj obj=new Obj(); - - //判断是否是某个类的实例 - System.out.println(clazz.isInstance(obj));//true - } -} -``` - -**3.通过反射创建实例** - -通过反射来生成对象主要有两种方式: - -- 使用Class对象的newInstance()方法来创建Class对象对应类的实例。 -- 先**通过Class对象获取指定的Constructor对象**,再调用Constructor对象的newInstance()方法来创建实例 - -```java -public class CreateInstance { - public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException { - //(1)使用Class对象的newInstance()方法来创建Class对象对应类的实例。 - Class clazz=String.class; - String str=(String)clazz.newInstance(); - - //(2)先通过Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建实例。 - Constructor strConstructor=clazz.getConstructor(String.class);////获取String构造函数 String(String str) - String str2=(String)strConstructor.newInstance("sss"); - System.out.println(str2);//sss - } -} -``` - -**4.获取成员方法** - - 获取某个Class对象的方法集合,主要有以下几个方法: - (1)public Method[] getDeclaredMethods() throws SecurityException - - getDeclaredMethods()方法返回类或接口声明的所有方法,包括公共、保护、默认(包)访问和私有方法,但**不包括继承的方法**。 - -(2)public Method[] getMethods() throws SecurityException - - getMethods()方法返回某个类的所有公用(public)方法,**包括其继承类的公用方法**。 - -(3)public Method getMethod(String name, Class... parameterTypes) - - getMethod方法返回一个特定的方法,其中第一个参数为方法名称,后面的参数为方法的参数对应Class的对象 - -```java -public class GetMethods { - public static void main(String[] args) throws NoSuchMethodException { - Class clazz=Obj.class; - - //方法返回类或接口声明的所有方法,包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法。 - Method[] methods=clazz.getDeclaredMethods(); - for(Method method:methods){ - System.out.println(method.getName()); - } - System.out.println("============="); - /** - * 输出结果: - add - sub - */ - - //返回某个类的所有公用(public)方法,包括其继承类的公用方法。会有许多的Object的方法 - Method[] methods2=clazz.getMethods(); - for(Method method:methods2){ - System.out.println(method.getName()); - } - System.out.println("============="); - /** - * 输出结果 - sub - wait - wait - wait - equals - toString - hashCode - getClass - notify - notifyAll - */ - - //返回一个特定的方法,其中第一个参数为方法名称,后面的参数为方法的参数对应Class的对象 - Method methods3=clazz.getMethod("sub",int.class,int.class); - System.out.println(methods3.getName()); - System.out.println(methods3); - /** - * 输出结果: - sub - public int code_05_reflection.Obj.sub(int,int) - */ - } -} -``` - -**5.获取构造器信息** - -获取类构造器的用法与上述获取方法的用法类似。 - -主要是通过Class类的**getConstructor方法**得到Constructor类的一个实例, -而Constructor类有一个newInstance方法可以创建一个对象实例。 - -```java -public T newInstance(Object ... initargs) -``` - -```java -public class GetConstructorInfo { - public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { - Class clazz=Obj.class; - Constructor constructor=clazz.getConstructor(); - //Constructor类有一个newInstance方法可以创建一个对象实例 - Obj obj=(Obj) constructor.newInstance(); - System.out.println(obj.sub(1,2)); - } -} -``` - -**6.获取类的成员变量信息** - -主要是这几个方法: -(1)getFiled: 访问公有的成员变量 - -(2)getDeclaredField:所有已声明的成员变量。但不能得到其父类的成员变量 - -(3)getFileds和getDeclaredFields用法同上(参照Method) - -**7.利用反射创建数组** - -Array类为java.lang.reflect.Array类。我们通过Array.newInstance()创建数组对象。 - -```java -public static Object newInstance(Class componentType, int length) - throws NegativeArraySizeException { - return newArray(componentType, length); //newArray()方法是一个Native方法 - } -``` - -```java -public class CreateArrays { - public static void main(String[] args) { - Class clazz=String.class; - - Object[] arr= (Object[]) Array.newInstance(clazz,25); - Array.set(arr,0,"aaa"); - Array.set(arr,1,"bbb"); - Array.set(arr,2,"ccc"); - Array.set(arr,3,"ddd"); - Array.set(arr,4,"eee"); - Array.set(arr,5,"fff"); - System.out.println(arr.length); //25 - System.out.println(Array.get(arr,2));//ccc - - } -} -``` - -**8.调用方法** - -当我们从类中**获取了一个方法**后,我们就可以用invoke()方法来调用这个方法。 - -```java -public Object invoke(Object obj, Object... args) - throws IllegalAccessException, IllegalArgumentException, - InvocationTargetException - //obj就是表示这个实例对象 - //args表示该方法的参数 -``` - -```java -public class InvokeMethod { - public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException { - Class clazz=Class.forName("code_05_reflection.Obj"); - - //先获取该类的实例对象 - Obj obj=(Obj)clazz.newInstance(); - - Method method=clazz.getMethod("sub",int.class,int.class); - - //利用反射调用该方法 - Integer num=(Integer)method.invoke(obj,1,2); - System.out.println(num); - } -} -``` - -## 浅析invoke方法 -invoke()源码如下: -```java -@CallerSensitive -public Object invoke(Object obj, Object... args) - throws IllegalAccessException, IllegalArgumentException, - InvocationTargetException -{ - if (!override) { - //TODO:(1)权限校验 - //invoke方法会首先检查AccessibleObject的override属性的值。 - //AccessibleObject 类是 Field、Method 和 Constructor 对象的基类。 - //它提供了将反射的对象标记为在使用时取消默认 Java 语言访问控制检查的能力。 - //override的值默认是false,表示需要权限调用规则,调用方法时需要检查权限; - //TODO:可以用setAccessible方法设置为true, - //若override的值为true,表示忽略权限规则,调用方法时无需检查权限 - // (也就是说可以调用任意的private方法,违反了封装)。 - if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { - //Reflection.quickCheckMemberAccess(clazz, modifiers)方法检查方法是否为public,如果是的话,就跳出本步 - Class caller = Reflection.getCallerClass(); //Reflection.getCallerClass()方法获取调用这个方法的Class对象,这是一个native方法 - checkAccess(caller, clazz, obj, modifiers); - } - } - MethodAccessor ma = methodAccessor; // read volatile - if (ma == null) { - ma = acquireMethodAccessor(); - } - return ma.invoke(obj, args); //TODO:(2)调用MethodAccessor的invoke方法 - //TODO:Method.invoke()实际上并不是自己实现的反射调用逻辑,而是委托给sun.reflect.MethodAccessor来处理。 -} -``` -Method的invoke()实际上并不是自己实现的反射调用逻辑,而是委托给sun.reflect.MethodAccessor来处理。 - -> **MethodAccessor接口** - -```java -public interface MethodAccessor { - /** Matches specification in {@link java.lang.reflect.Method} */ - public Object invoke(Object obj, Object[] args) - throws IllegalArgumentException, InvocationTargetException; -} -``` -Method对象的基本构成,每个Java方法有且只有一个Method对象作为root, -它相当于根对象,对用户不可见。 -当我们创建Method对象时,我们代码中获得的Method对象都相当于它的**副本(或引用)**。 - -root对象持有一个MethodAccessor对象,所以所有获取到的Method对象都**共享这一个MethodAccessor对象**, -因此必须保证它在内存中的可见性。 -```java -//volatile表示该对象是可见的 -private volatile MethodAccessor methodAccessor; -private Method root; -``` -> **acquireMethodAccessor()** - -```java -private MethodAccessor acquireMethodAccessor() { - // First check to see if one has been created yet, and take it - // if so - MethodAccessor tmp = null; - if (root != null) tmp = root.getMethodAccessor(); - if (tmp != null) { - methodAccessor = tmp; - } else { - // Otherwise fabricate one and propagate it up to the root - tmp = reflectionFactory.newMethodAccessor(this); - setMethodAccessor(tmp); - } - return tmp; -} -``` -**第一次调用一个Java方法对应的Method对象的invoke()方法之前, -实现调用逻辑的MethodAccessor对象还没有创建; -等第一次调用时才新创建MethodAccessor并更新给root**, -然后调用MethodAccessor.invoke()完成反射调用。 \ No newline at end of file diff --git "a/docs/JavaBasics/06\345\274\202\345\270\270.md" "b/docs/JavaBasics/06\345\274\202\345\270\270.md" deleted file mode 100644 index 19ca7524..00000000 --- "a/docs/JavaBasics/06\345\274\202\345\270\270.md" +++ /dev/null @@ -1,177 +0,0 @@ -# 七、异常 - -## 异常的概念 -Java 异常是一个描述在代码段中**发生异常的对象**,当发生异常情况时,一个代表该异常的对象被创建并且在导致该异常的方法中被抛出,而该方法可以选择自己处理异常或者传递该异常。 - -## 异常继承体系 -Throwable 可以用来表示任何可以作为异常抛出的类,分为两种: **Error** 和 **Exception**。 - -- Error:通常是灾难性的致命的错误,是**程序无法控制和处理**的,当出现这些错误时,建议终止程序; -- Exception:通常情况下是可以被程序处理的,捕获后可能恢复,并且在程序中应该**尽可能地去处理**这些异常。 - -Exception 分为两种: - - -- **受检异常**:在正确的程序运行过程中,很容易出现的、情理可容的异常状况,在一定程度上这种异常的发生是可以预测的,并且一旦发生该种异常,就必须采取某种方式进行处理。(注意:**除了RuntimeException及其子类以外**,其他的 Exception 类及其子类都属于这种异常,当程序中可能出现这类异常,要么使用 try-catch 语句进行捕获,要么用 throws 子句抛出,否则编译无法通过。) -- **非受检异常**:包括RuntimeException及其子类和Error。 - (注意:非受检查异常为编译器不要求强制处理的异常,受检异常则是编译器要求必须处置的异常。) - -

- -从责任角度看: - -1. Error 属于JVM 需要负担的责任; -2. RuntimException是程序应该负担的责任; -3. Checked Exception(受检异常)是 Java 编译器应该负担的责任。 - -## Java 异常的处理机制 - -Java 异常处理机制本质上就是**抛出异常**和**捕捉异常**。 - -**抛出异常** - -i.普通问题:指在当前环境下能得到足够的信息,总能处理这个错误。 - -ii.异常情形:是指**阻止当前方法或作用域继续执行的问题**。对于异常情形,已经程序无法执行继续下去了, -因为在当前环境下无法获得必要的信息来解决问题,我们所能做的就是从当前环境中跳出,并把问题提交给上一级环境, -这就是抛出异常时所发生的事情。 - -iii.抛出异常后,会有几件事随之发生: -​ -第一:像创建普通的java对象一样将使用new在堆上创建一个异常对象 - - 第二:当前的执行路径(已经无法继续下去了)被终止,并且从当前环境中弹出对异常对象的引用。 -​ -此时,**异常处理机制接管程序,并开始寻找一个恰当的地方继续执行程序**, -这个恰当的地方就是异常处理程序或者异常处理器, -它的任务是**将程序从错误状态中恢复**,以使程序要么换一种方式运行,要么继续运行下去。 -​ -**捕捉异常** - -在方法抛出异常之后,运行时系统将转为寻找合适的**异常处理器**(exception handler)。 -潜在的异常处理器是异常发生时依次存留在**调用栈**中的方法的集合。 -当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适的异常处理器。 -运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。 -当运行时系统遍历调用栈而未找到合适的异常处理器,则运行时系统终止。同时,意味着Java程序的终止。 - -注意: - -对于运行时异常、错误和受检异常,Java技术所要求的异常处理方式有所不同。 - -(1)由于**运行时异常及其子类**的不可查性,为了更合理、更容易地实现应用程序, -Java规定,运行时异常将由Java运行时系统自动抛出,**允许应用程序忽略运行时异常**。 - -(2)对于方法运行中可能出现的Error,当运行方法不欲捕捉时,Java允许该方法不做任何抛出声明。 -因为,大多数Error异常属于永远不能被允许发生的状况,也属于合理的应用程序不该捕捉的异常。 - -(3)对于所有的受检异常, -Java规定:一个方法必须捕捉,或者声明抛出方法之外。 -也就是说,当一个方法选择不捕捉受检异常时,它必须声明将抛出异常。 - -## Java 异常的处理原则 - -- **具体明确**:抛出的异常应能通过异常类名和message准确说明异常的类型和产生异常的原因; -- **提早抛出**:应尽可能早地发现并抛出异常,便于精确定位问题; -- **延迟捕获**:异常的捕获和处理应尽可能延迟,让掌握更多信息的作用域来处理异常 - -## Java 常见异常以及错误 - -| 类型 | 说明 | -| :--: | :--: | -| **RuntimeException 子类** | | -| java.lang.ArrayIndexOutOfBoundsException | 数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出 | -| java.lang.ArithmeticException | 算术条件异常。譬如:整数除零等 | -| java.lang.NullPointerException | 空指针异常。当应用试图在要求使用对象的地方使用了null时,抛出该异常。譬如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等 | -| java.lang.ClassNotFoundException | 找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常 | -| java.lang.SecurityException | 安全性异常 | -| java.lang.IllegalArgumentException | 非法参数异常 | -| **IOException** | | -| IOException | 操作输入流和输出流时可能出现的异常 | -| EOFException | 文件已结束异常 | -| FileNotFoundException | 文件未找到异常 | -| **其他** | | -| ClassCastException | 类型转换异常类 | -| ArrayStoreException | 数组中包含不兼容的值抛出的异常 | -| SQLException | 操作数据库异常类 | -| NoSuchFieldException | 字段未找到异常 | -| NumberFormatException | 字符串转换为数字抛出的异常 | -| StringIndexOutOfBoundsException | 字符串索引超出范围抛出的异常 | -| IllegalAccessException | 不允许访问某类异常 | -| **Error** | | -| NoClassDefFoundError | 找不到class定义的错误 | -| StackOverflowError | 深递归导致栈被耗尽而抛出的错误 | -| OutOfMemoryError | 内存溢出错误 | - -## Java异常常见面试题 - -### try-catch-finally语句块的执行 - -

- -(1)try 块:用于捕获异常。其后可接零个或多个catch块,如果没有catch块,则必须跟一个finally块。 - -(2)catch 块:用于处理try捕获到的异常。 - -(3)finally 块:无论是否捕获或处理异常,finally块里的语句都会被执行。 -当在try块或catch块中遇到return语句时,finally语句块将在**方法返回之前**被执行。 - -在以下4种特殊情况下,finally块不会被执行: - -1)在finally语句块中发生了异常。 -​ -2)在前面的代码中用了System.exit()退出程序。 - -3)程序所在的线程死亡。 - -4)关闭CPU。 - -### try catch代码块的性能如何? - -1. 会影响JVM的重排序优化; -2. 异常对象实例需要保存栈快照等信息,开销比较大。 - -### final和finally和finalize的区别? - -(1)final:最终的意思,可以修饰类,修饰成员变量,修饰成员方法 - -修饰类:类不能被继承 -​ -修饰变量:变量是常量 -​ -修饰方法:方法不能被重写(Override) - -(2)finally:是异常处理的关键字,用于释放资源。一般来说,代码必须执行(特殊情况:在执行到finally JVM就退出了) - -(3)finalize:是Object的一个方法,用于垃圾回收。 - -- 看程序写结果 - -```java -public class TestException { - public static void main(String[] args) { - System.out.println(getInt()); - } - - public static int getInt(){ - int a=10; - try{ - System.out.println(a/0); - }catch (ArithmeticException e){ - a=30; - return a; - }finally { - a=40; - } - System.out.println("a="+a); - return a; - } -} -``` -结果为: -```html -30 -``` - -- [Java异常常见面试题及答案1](https://github.com/DuHouAn/Java/blob/master/JavaBasics/01Java%E5%BC%82%E5%B8%B8%E5%B8%B8%E8%A7%81%E9%9D%A2%E8%AF%95%E9%A2%98%E5%8F%8A%E7%AD%94%E6%A1%88.txt) - -- [Java异常常见面试题及答案2](https://github.com/DuHouAn/Java/blob/master/JavaBasics/02Java%E5%BC%82%E5%B8%B8%E5%B8%B8%E8%A7%81%E9%9D%A2%E8%AF%95%E9%A2%98%E5%8F%8A%E7%AD%94%E6%A1%88.txt) \ No newline at end of file diff --git "a/docs/JavaBasics/08\346\263\250\350\247\243.md" "b/docs/JavaBasics/08\346\263\250\350\247\243.md" deleted file mode 100644 index 06b9e809..00000000 --- "a/docs/JavaBasics/08\346\263\250\350\247\243.md" +++ /dev/null @@ -1,267 +0,0 @@ - -* [九、注解](#九注解) - * [注解概述](#注解概述) - * [注解的用处](#注解的用处) - * [注解的原理](#注解的原理) - * [元注解](#元注解) - * [常见标准的Annotation](#常见标准的Annotation) - * [自定义注解](#自定义注解) - * [运行时注解](#运行时注解) - * [编译时注解](#编译时注解) - -# 九、注解 -## 注解概述 -Annontation是Java5开始引入的新特征,中文名称叫注解。 -提供了一种**安全的类似注释的机制**, -用来将任何的信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联。 -为程序的元素(类、方法、成员变量)加上更直观明了的说明, -这些说明信息是与程序的业务逻辑无关,并且供指定的工具或框架使用。 - -## 注解的用处 -- 生成文档。这是最常见的,也是java 最早提供的注解。常用的有@param @return 等 -- 跟踪代码依赖性,实现替代**配置文件**功能。如Spring中@Autowired; -- 在编译时进行格式检查。如@override 放在方法前,如果你这个方法并不是覆盖了超类方法,则编译时就能检查出。 - -## 注解的原理 -注解**本质是一个继承了Annotation的特殊接口**,其具体实现类是Java运行时生成的**动态代理类**。 -我们通过反射获取注解时,返回的是Java运行时生成的**动态代理对象**$Proxy1。 -通过代理对象调用自定义注解(接口)的方法,会最终调用AnnotationInvocationHandler的invoke方法。 -该方法会从memberValues这个Map中索引出对应的值。 -而memberValues的来源是Java常量池。 - -### 元注解 -java.lang.annotation提供了四种元注解,专门注解其他的注解(在自定义注解的时候,需要使用到元注解): - -| 注解 | 说明 | -| :--: | :--: | -| @Documented | 是否将注解包含在JavaDoc中 | -| @Retention | 什么时候使用该注解 | -| @Target | 注解用于什么地方 | -| @Inherited | 是否允许子类继承该注解 | - -- @Documented - -一个简单的Annotations标记注解,表示是否将注解信息添加在java文档中。 - -- @Retention - -定义该注解的生命周期。 - -(1)RetentionPolicy.SOURCE : 在编译阶段丢弃。 - 这些注解在编译结束之后就不再有任何意义,所以它们不会写入字节码。 - @Override, @SuppressWarnings都属于这类注解。 - -(2)RetentionPolicy.CLASS : 在类加载的时候丢弃。 -在字节码文件的处理中有用。注解默认使用这种方式 - -(3)RetentionPolicy.RUNTIME : 始终不会丢弃,运行期也保留该注解, -因此**可以使用反射机制读取该注解的信息**。我们自定义的注解通常使用这种方式。 - -- @Target - -表示该注解用于什么地方。 -默认值为任何元素,表示该注解用于什么地方。可用的ElementType参数包括: - -> ElementType.CONSTRUCTOR:用于描述构造器 -> ElementType.FIELD:成员变量、对象、属性(包括enum实例) -> ElementType.LOCAL_VARIABLE:用于描述局部变量 -> ElementType.METHOD:用于描述方法 -> ElementType.PACKAGE:用于描述包 -> ElementType.PARAMETER:用于描述参数 -> ElementType.TYPE:用于描述类、接口(包括注解类型) 或enum声明 - -- @Inherited - -定义该注释和子类的关系。 -@Inherited 元注解是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的。 -如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。 - -### 常见标准的Annotation -- Override (RetentionPolicy.SOURCE : 在编译阶段丢弃。属于@Retention) - - Override是一个标记类型注解,它被用作标注方法。 - 它说明了被标注的方法重载了父类的方法,起到了断言的作用。 - 如果我们使用了这种注解在一个没有覆盖父类方法的方法时,**java编译器将以一个编译错误来警示**。 - -- Deprecated - -Deprecated也是一种标记类型注解。 -当一个类型或者类型成员使用@Deprecated修饰的话,编译器将不鼓励使用这个被标注的程序元素。 -所以使用这种修饰具有一定的“延续性”: - 如果我们在代码中通过继承或者覆盖的方式使用了这个过时的类型或者成员, - 虽然继承或者覆盖后的类型或者成员并不是被声明为@Deprecated,但编译器仍然要报警。 - -- SuppressWarnings - -SuppressWarning不是一个标记类型注解。 -它有一个类型为String[]的成员,这个成员的值为**被禁止的警告名**。 -对于javac编译器来讲,对-Xlint选项有效的警告名也同样对@SuppressWarings有效,同时编译器忽略掉无法识别的警告名。 -@SuppressWarnings("unchecked") - -### 自定义注解 -自定义注解类编写的一些规则: -(1) Annotation型定义为@interface, -所有的Annotation会自动继承java.lang.Annotation这一接口,并且不能再去继承别的类或是接口 - -(2)参数成员只能用public或默认(default)这两个访问权修饰 - -(3)参数成员只能用基本类型byte,short,char,int,long,float,double,boolean八种基本数据类型 -和String、Enum、Class、Annotations等数据类型,以及这一些类型的数组 - -(4)要获取类方法和字段的注解信息,必须通过Java的反射技术来获取Annotation对象, -因为除此之外没有别的获取注解对象的方法 - -(5)注解也可以没有定义成员, 不过这样注解就没啥用了 - -注意:**自定义注解需要使用到元注解** - -- 自定义注解示例: - -自定义水果颜色注解 -```java -/** - * 水果颜色注解 - */ -@Target(FIELD) -@Retention(RUNTIME) -@Documented -@interface FruitColor { - /** - * 颜色枚举 - */ - public enum Color{绿色,红色,青色}; - - /** - * 颜色属性 (注意:这里的属性指的就是方法) - */ - Color fruitColor() default Color.绿色;//默认是是绿色的 -} -``` -自定义水果名称注解 -```java -/** - * 水果名称注解 - */ -@Target(FIELD) //ElementType.FIELD:成员变量、对象、属性(包括enum实例) -@Retention(RUNTIME)// 始终不会丢弃,运行期也保留该注解,因此可以使用反射机制读取该注解的信息。 -@Documented // Deprecated也是一种标记类型注解。 -public @interface FruitName { - public String fruitName() default ""; -} -``` -水果供应商注解 -```java -/** - * 水果供应者注解 - */ -@Target(FIELD) -@Retention(RUNTIME) -@Documented -public @interface FruitProvider { - /** - * 供应者编号 - */ - public int id() default -1; - - /** - * 供应商名称 - */ - public String name() default ""; - - /** - * 供应商地址 - */ - public String address() default ""; -} -``` -通过反射来获取水果信息 -```java -/** - * 通过反射获取水果信息 - */ -public class FruitInfoUtil { - public static void getFruitInfo(Class clazz){ - String strFruitName=" 水果名称:"; - String strFruitColor=" 水果颜色:"; - String strFruitProvider="供应商信息:"; - - //获取属性值 - Field[] fields=clazz.getDeclaredFields(); - for(Field field:fields){ - if(field.isAnnotationPresent(FruitName.class)){ - //判断注解是不是 FruitName - FruitName fruitName=field.getAnnotation(FruitName.class); - strFruitName=strFruitName+fruitName.fruitName(); - System.out.println(strFruitName); - }else if(field.isAnnotationPresent(FruitColor.class)){ - FruitColor fruitColor=field.getAnnotation(FruitColor.class); - strFruitColor=strFruitColor+fruitColor.fruitColor().toString(); - System.out.println(strFruitColor); - }else if(field.isAnnotationPresent(FruitProvider.class)){ - FruitProvider fruitProvider=field.getAnnotation(FruitProvider.class); - strFruitProvider=strFruitProvider - + "[ 供应商编号:"+fruitProvider.id() - +" 供应商名称:" +fruitProvider.name() - +" 供应商地址:"+fruitProvider.address()+"]"; - System.out.println(strFruitProvider); - } - } - } -} -``` -使用注解初始化实例类 -```java -/** - * 定义一个实例类 - * 这里使用注解来初始化 - */ -public class Apple { - @FruitName(fruitName = "苹果") - private String appleName; - - @FruitColor(fruitColor = FruitColor.Color.红色) - private String appleColor; - - @FruitProvider(id=1,name="红富士",address="陕西省西安市延安路89号红富士大厦") - private String appleProvider; - - public String getAppleName() { - return appleName; - } - - public void setAppleName(String appleName) { - this.appleName = appleName; - } - - public String getAppleColor() { - return appleColor; - } - - public void setAppleColor(String appleColor) { - this.appleColor = appleColor; - } - - public String getAppleProvider() { - return appleProvider; - } - - public void setAppleProvider(String appleProvider) { - this.appleProvider = appleProvider; - } -} -``` -### 运行时注解 -运行时注解是通过反射在程序运行时获取注解信息,然后利用信息进行其他处理。 - -[运行时注解 代码示例](https://github.com/DuHouAn/Java/tree/master/JavaBasics/src/code_08_annotation/code_01) - -### 编译时注解 -编译时注解是在程序编译的时候动态的生成一些类或者文件, -所以编译时注解不会影响程序运行时的性能,而**运行时注解则依赖于反射**, -反射肯定会影响程序运行时的性能,所以一些知名的三方库一般都是使用编译时时注解, -比如大名鼎鼎的ButterKnife、Dagger、Afinal等。 - -下面编译时注解是编译时打印使用指定注解的方法的方法信息, -注解的定义和运行时注解一样,主要是注解处理器对注解的处理不同 。 - -[编译时注解 代码示例](https://github.com/DuHouAn/Java/tree/master/JavaBasics/src/code_08_annotation/code_02) \ No newline at end of file diff --git "a/docs/JavaBasics/09Java\345\270\270\350\247\201\345\257\271\350\261\241.md" "b/docs/JavaBasics/09Java\345\270\270\350\247\201\345\257\271\350\261\241.md" deleted file mode 100644 index e585386e..00000000 --- "a/docs/JavaBasics/09Java\345\270\270\350\247\201\345\257\271\350\261\241.md" +++ /dev/null @@ -1,1677 +0,0 @@ -# 十、Java常见对象 - -## Arrays -Arrays:针对数组进行操作的工具类。 - -- Arrays的常用成员方法: -```java -public static String toString(int[] a) //把数组转成字符串 - -public static void sort(int[] a) //对数组进行排序 - -public static int binarySearch(int[] a,int key) //二分查找 -``` - -- toString()源码如下: -```java -public static String toString(int[] a) { - if (a == null) - return "null"; - int iMax = a.length - 1; - if (iMax == -1) - return "[]"; - - StringBuilder b = new StringBuilder(); - b.append('['); - for (int i = 0; ; i++) { - b.append(a[i]); - if (i == iMax) - return b.append(']').toString(); - b.append(", "); - } - } -``` -- binarySearch()调用的是binarySearch0(),源码如下: -```java - private static int binarySearch0(int[] a, int fromIndex, int toIndex,int key) { - int low = fromIndex; - int high = toIndex - 1; - - while (low <= high) { - int mid = (low + high) >>> 1; - int midVal = a[mid]; - - if (midVal < key) - low = mid + 1; - else if (midVal > key) - high = mid - 1; - else - return mid; // key found - } - return -(low + 1); // key not found. - } -``` - -- 使用示例: -```java -public class ArraysDemo { - public static void main(String[] args) { - // 定义一个数组 - int[] arr = { 24, 69, 80, 57, 13 }; - - // public static String toString(int[] a) 把数组转成字符串 - System.out.println("排序前:" + Arrays.toString(arr));//排序前:[24, 69, 80, 57, 13] - - // public static void sort(int[] a) 对数组进行排序 - Arrays.sort(arr); - System.out.println("排序后:" + Arrays.toString(arr));//排序后:[13, 24, 57, 69, 80] - - // [13, 24, 57, 69, 80] - // public static int binarySearch(int[] a,int key) 二分查找 - System.out.println("binarySearch:" + Arrays.binarySearch(arr, 57));//binarySearch:2 - System.out.println("binarySearch:" + Arrays.binarySearch(arr, 577));//binarySearch:-6 - } -} -``` -## BigDemical -BigDecimal类:不可变的、任意精度的有符号十进制数,可以解决数据丢失问题。 - -看如下程序,写出结果 -```java -public static void main(String[] args) { - System.out.println(0.09 + 0.01); - System.out.println(1.0 - 0.32); - System.out.println(1.015 * 100); - System.out.println(1.301 / 100); - System.out.println(1.0 - 0.12); -} -``` -输出结果 -```html -0.09999999999999999 -0.6799999999999999 -101.49999999999999 -0.013009999999999999 -0.88 -``` -结果和我们想的有一点点不一样,这是因为浮点数类型的数据存储和整数不一样导致的。 -它们大部分的时候,都是带有有效数字位。由于在运算的时候,float类型和double很容易丢失精度, -所以,为了能精确的表示、计算浮点数,Java提供了BigDecimal。 - -- BigDecimal的常用成员方法: -```java -public BigDecimal(String val) //构造方法 - -public BigDecimal add(BigDecimal augend) //加 - - public BigDecimal subtract(BigDecimal subtrahend)//减 - - public BigDecimal multiply(BigDecimal multiplicand) //乘 - - public BigDecimal divide(BigDecimal divisor) //除 - - public BigDecimal divide(BigDecimal divisor,int scale,int roundingMode)//除法,scale:几位小数,roundingMode:如何舍取 -``` - -- 使用BigDecimal改进 -```java -public static void main(String[] args) { - /*System.out.println(0.09 + 0.01); - System.out.println(1.0 - 0.32); - System.out.println(1.015 * 100); - System.out.println(1.301 / 100); - System.out.println(1.0 - 0.12);*/ - - BigDecimal bd1 = new BigDecimal("0.09"); - BigDecimal bd2 = new BigDecimal("0.01"); - System.out.println("add:" + bd1.add(bd2));//add:0.10 - System.out.println("-------------------"); - - BigDecimal bd3 = new BigDecimal("1.0"); - BigDecimal bd4 = new BigDecimal("0.32"); - System.out.println("subtract:" + bd3.subtract(bd4));//subtract:0.68 - System.out.println("-------------------"); - - BigDecimal bd5 = new BigDecimal("1.015"); - BigDecimal bd6 = new BigDecimal("100"); - System.out.println("multiply:" + bd5.multiply(bd6));//multiply:101.500 - System.out.println("-------------------"); - - BigDecimal bd7 = new BigDecimal("1.301"); - BigDecimal bd8 = new BigDecimal("100"); - System.out.println("divide:" + bd7.divide(bd8));//divide:0.01301 - - //四舍五入 - System.out.println("divide:" - + bd7.divide(bd8, 3, BigDecimal.ROUND_HALF_UP));//保留三位有效数字 - //divide:0.013 - - System.out.println("divide:" - + bd7.divide(bd8, 8, BigDecimal.ROUND_HALF_UP));//保留八位有效数字 - //divide:0.01301000 -} -``` - -## BigInteger -BigInteger:可以让超过Integer范围内的数据进行运算 - -```java -public static void main(String[] args) { - Integer num = new Integer("2147483647"); - System.out.println(num); - - //Integer num2 = new Integer("2147483648"); - // Exception in thread "main" java.lang.NumberFormatException: For input string: "2147483648" - //System.out.println(num2); - - // 通过 BigIntege来创建对象 - BigInteger num2 = new BigInteger("2147483648"); - System.out.println(num2); -} -``` - -- BigInteger的常用成员方法: -```java -public BigInteger add(BigInteger val) //加 - -public BigInteger subtract(BigInteger val) //减 - -public BigInteger multiply(BigInteger val) //乘 - -public BigInteger divide(BigInteger val) //除 - -public BigInteger[] divideAndRemainder(BigInteger val)//返回商和余数的数组 -``` -- 使用实例: -```java -public class BigIntegerDemo { - public static void main(String[] args) { - Integer num = new Integer("2147483647"); - System.out.println(num); - - //Integer num2 = new Integer("2147483648"); - // Exception in thread "main" java.lang.NumberFormatException: For input string: "2147483648" - //System.out.println(num2); - - // 通过 BigIntege来创建对象 - BigInteger num2 = new BigInteger("2147483648"); - System.out.println(num2); - } -} -``` - -```java -public class BigIntegerDemo2 { - public static void main(String[] args) { - BigInteger bi1 = new BigInteger("100"); - BigInteger bi2 = new BigInteger("50"); - - // public BigInteger add(BigInteger val):加 - System.out.println("add:" + bi1.add(bi2)); //add:150 - // public BigInteger subtract(BigInteger Val):减 - System.out.println("subtract:" + bi1.subtract(bi2));//subtract:50 - // public BigInteger multiply(BigInteger val):乘 - System.out.println("multiply:" + bi1.multiply(bi2));//multiply:5000 - // public BigInteger divide(BigInteger val):除 - System.out.println("divide:" + bi1.divide(bi2));//divide:2 - - // public BigInteger[] divideAndRemainder(BigInteger val):返回商和余数的数组 - BigInteger[] bis = bi1.divideAndRemainder(bi2); - System.out.println("divide:" + bis[0]);//divide:2 - System.out.println("remainder:" + bis[1]);//remainder:0 - } -} -``` - -## Calendar -Calendar为特定瞬间与一组诸如 YEAR、MONTH、DAY_OF_MONTH、HOUR 等日历字段之间的转换提供了一些方法, -并为操作日历字段(例如获得下星期的日期)提供了一些方法。 - -Calendar中常用的方法: - -```java -public int get(int field) //返回给定日历字段的值。日历类中的每个日历字段都是静态的成员变量,并且是int类型。 - -public void add(int field,int amount)//根据给定的日历字段和对应的时间,来对当前的日历进行操作。 - -public final void set(int year,int month,int date)//设置当前日历的年月日 -``` -- 使用示例: -```java -public class CalendarDemo { - public static void main(String[] args) { - // 其日历字段已由当前日期和时间初始化: - Calendar rightNow = Calendar.getInstance(); // 子类对象 - int year=rightNow.get(Calendar.YEAR); - int month=rightNow.get(Calendar.MONTH);//注意月份是从0开始的 - int date=rightNow.get(Calendar.DATE); - System.out.println(year + "年" + (month + 1) + "月" + date + "日"); - //2018年12月25日 - } -} -``` -- 使用示例2: -```java -public class CalendarDemo2 { - public static void main(String[] args) { - // 其日历字段已由当前日期和时间初始化: - Calendar calendar = Calendar.getInstance(); // 子类对象 - System.out.println(getYearMonthDay(calendar));//2018年12月25日 - - //三年前的今天 - calendar.add(Calendar.YEAR,-3); - System.out.println(getYearMonthDay(calendar));//2015年12月25日 - - //5年后的10天前 - calendar.add(Calendar.YEAR,5); - calendar.add(Calendar.DATE,-10); - System.out.println(getYearMonthDay(calendar));//2020年12月15日 - - //设置 2011年11月11日 - calendar.set(2011,10,11); - System.out.println(getYearMonthDay(calendar));//2011年11月11日 - } - - //获取年、月、日 - public static String getYearMonthDay(Calendar calendar){ - int year=calendar.get(Calendar.YEAR); - int month=calendar.get(Calendar.MONTH); - int date=calendar.get(Calendar.DATE); - return year + "年" + (month + 1) + "月" + date + "日"; - } -} -``` - -- 小练习:获取任意一年的二月有多少天 -```java -/** - *获取任意一年的二月有多少天 - *分析: - * A:键盘录入任意的年份 - * B:设置日历对象的年月日 - * 年就是输入的数据 - * 月是2 - * 日是1 - * C:把时间往前推一天,就是2月的最后一天 - * D:获取这一天输出即可 - */ -public class CalendarTest { - public static void main(String[] args) { - Scanner sc=new Scanner(System.in); - int year=sc.nextInt(); - Calendar c= Calendar.getInstance(); - c.set(year,2,1); //得到的就是该年的3月1日 - c.add(Calendar.DATE,-1);//把时间往前推一天,就是2月的最后一天 - //public void add(int field,int amount):根据给定的日历字段和对应的时间,来对当前的日历进行操作。 - - System.out.println(year+"年,二月有"+c.get(Calendar.DATE)+"天"); - } -} -``` - -## Character -Character 类在对象中包装一个基本类型 char 的值.此外,该类提供了几种方法, -以确定字符的类别(小写字母,数字,等等),并将字符从大写转换成小写,反之亦然。 - -- Character常用方法: -```java -Character(char value) //构造方法 - -public static boolean isUpperCase(char ch) //判断给定的字符是否是大写字符 - -public static boolean isLowerCase(char ch) //判断给定的字符是否是小写字符 - -public static boolean isDigit(char ch) //判断给定的字符是否是数字字符 - -public static char toUpperCase(char ch) //把给定的字符转换为大写字符 - -public static char toLowerCase(char ch) //把给定的字符转换为小写字符 -``` -- 使用示例: -```java -public class CharacterDemo { - public static void main(String[] args) { - // public static boolean isUpperCase(char ch):判断给定的字符是否是大写字符 - System.out.println("isUpperCase:" + Character.isUpperCase('A'));//true - System.out.println("isUpperCase:" + Character.isUpperCase('a'));//false - System.out.println("isUpperCase:" + Character.isUpperCase('0'));//false - System.out.println("-----------------------------------------"); - - // public static boolean isLowerCase(char ch):判断给定的字符是否是小写字符 - System.out.println("isLowerCase:" + Character.isLowerCase('A'));//false - System.out.println("isLowerCase:" + Character.isLowerCase('a'));//true - System.out.println("isLowerCase:" + Character.isLowerCase('0'));//false - System.out.println("-----------------------------------------"); - - // public static boolean isDigit(char ch):判断给定的字符是否是数字字符 - System.out.println("isDigit:" + Character.isDigit('A'));//false - System.out.println("isDigit:" + Character.isDigit('a'));//false - System.out.println("isDigit:" + Character.isDigit('0'));//true - System.out.println("-----------------------------------------"); - - // public static char toUpperCase(char ch):把给定的字符转换为大写字符 - System.out.println("toUpperCase:" + Character.toUpperCase('A'));//A - System.out.println("toUpperCase:" + Character.toUpperCase('a'));//A - System.out.println("-----------------------------------------"); - - // public static char toLowerCase(char ch):把给定的字符转换为小写字符 - System.out.println("toLowerCase:" + Character.toLowerCase('A'));//a - System.out.println("toLowerCase:" + Character.toLowerCase('a'));//a - } -} -``` - -- 小练习:统计一个字符串中大写字母字符,小写字母字符,数字字符出现的次数。(不考虑其他字符) - -```java -/** - * 统计一个字符串中大写字母字符,小写字母字符,数字字符出现的次数。(不考虑其他字符) - * - * 分析: - * A:定义三个统计变量。 - * int bigCont=0; - * int smalCount=0; - * int numberCount=0; - * B:键盘录入一个字符串。 - * C:把字符串转换为字符数组。 - * D:遍历字符数组获取到每一个字符 - * E:判断该字符是 - * 大写 bigCount++; - * 小写 smalCount++; - * 数字 numberCount++; - * F:输出结果即可 - */ -public class CharacterTest { - public static void main(String[] args) { - Scanner sc=new Scanner(System.in); - String str=sc.nextLine(); - printCount(str); - printCount2(str); - } - - //原来的写法 - public static void printCount(String str) { - int numberCount=0; - int lowercaseCount=0; - int upercaseCount=0; - - for(int index=0;index='0' && ch<='9'){ - numberCount++; - }else if(ch>='A' && ch<='Z'){ - upercaseCount++; - }else if(ch>='a' && ch<='z'){ - lowercaseCount++; - } - } - System.out.println("数字有"+numberCount+"个"); - System.out.println("小写字母有"+lowercaseCount+"个"); - System.out.println("大写字母有"+upercaseCount+"个"); - } - - //使用包装类来改进 - public static void printCount2(String str) { - int numberCount=0; - int lowercaseCount=0; - int upercaseCount=0; - - for(int index=0;index 当前时间 - - // Date(long date):根据给定的毫秒值创建日期对象 - //long time = System.currentTimeMillis(); - long time = 1000 * 60 * 60; // 1小时 - Date d2 = new Date(time); - System.out.println("d2:" + d2); - //格林威治时间 1970年01月01日00时00分00 - //Thu Jan 01 09:00:00 GMT+08:00 1970 GMT+表示 标准时间加8小时,因为中国是东八区 - - // 获取时间 - long time2 = d.getTime(); - System.out.println(time2); //1545739438466 毫秒 - System.out.println(System.currentTimeMillis()); - - // 设置时间 - d.setTime(1000*60*60); - System.out.println("d:" + d); - //Thu Jan 01 09:00:00 GMT+08:00 1970 - } -} -``` - - -DateForamt:可以进行日期和字符串的格式化和解析,但是由于是抽象类,所以使用具体子类SimpleDateFormat。 -SimpleDateFormat的构造方法: - -```java -SimpleDateFormat() //默认模式 - -SimpleDateFormat(String pattern) //给定的模式 -``` - -这个模式字符串该如何写呢? 通过查看API,我们就找到了对应的模式: - -| 中文说明 | 模式字符 | -| :--: | :--: | -| 年 | y | -| 月 | M | -| 日 | d | -| 时 | H | -| 分 | m | -| 秒 | s | - -- Date类型和String类型的相互转换 - -```java -public class DateFormatDemo { - public static void main(String[] args) { - Date date=new Date(); - SimpleDateFormat sdf=new SimpleDateFormat("yyyy年MM月dd日"); - String s=dateToString(date,sdf); - System.out.println(s); //2018年12月25日 - System.out.println(stringToDate(s,sdf));//Tue Dec 25 00:00:00 GMT+08:00 2018 - } - - /** - * Date -- String(格式化) - * public final String format(Date date) - */ - public static String dateToString(Date d, SimpleDateFormat sdf) { - return sdf.format(d); - } - - /** - * * String -- Date(解析) - * public Date parse(String source) - */ - public static Date stringToDate(String s, SimpleDateFormat sdf){ - Date date=null; - try { - date=sdf.parse(s); - } catch (ParseException e) { - e.printStackTrace(); - } - return date; - } -} -``` -- 小练习: 算一下你来到这个世界多少天? - -```java -/** - * * - * 算一下你来到这个世界多少天? - * - * 分析: - * A:键盘录入你的出生的年月日 - * B:把该字符串转换为一个日期 - * C:通过该日期得到一个毫秒值 - * D:获取当前时间的毫秒值 - * E:用D-C得到一个毫秒值 - * F:把E的毫秒值转换为年 - * /1000/60/60/24 - */ -public class DateTest { - public static void main(String[] args) throws ParseException { - // 键盘录入你的出生的年月日 - Scanner sc = new Scanner(System.in); - System.out.println("请输入你的出生年月日(格式 yyyy-MM-dd):"); - String line = sc.nextLine(); - - // 把该字符串转换为一个日期 - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); - Date d = sdf.parse(line); - long birth=d.getTime(); //出生的时间 - long current=System.currentTimeMillis();//当前时间 - - long days=(current-birth)/1000/60/60/24; - System.out.println("你出生了"+days+"天"); - } -} -``` - -## Integer -Java就针对每一种基本数据类型提供了对应的类类型: - -| 基础类型 | 包装类类型 | -| :--: | :--: | -| byte | Byte | -| short | Short | -| int | Integer | -| long | Long | -| float | Float | -| double | Double | -| char | Character | -| boolean | Boolean | - -用于基本数据类型与字符串之间的转换。 - -- Integer的常用成员方法: -```java -public Integer(int value) - -public Integer(String s) //注意:这个字符串必须是由数字字符组成 -``` - -- int和String的相互转化: -```java -String.valueOf(number) //int --> String - -Integer.parseInt(s) //String --> int -``` - -```java -public static void main(String[] args) { - System.out.println(intToString(100)); //100 - System.out.println(stringToInt("100")); //100 -} - -//int --> String -public static String intToString(int number) { - //方式一 - /*String strNumber=""+number; - return strNumber;*/ - //方式二 - String strNumber=String.valueOf(number); - return strNumber; -} - -//String --> int -public static int stringToInt(String strNumber) { - //方式一 - /*Integer number=new Integer(strNumber); - return number;*/ - //方式二 - Integer number=Integer.parseInt(strNumber); - return number; -} -``` - -- 常用的进制转换: -```java -//常用的基本进制转换 -public static String toBinaryString(int i) - -public static String toOctalString(int i) - -public static String toHexString(int i) - -//十进制到其他进制 -public static String toString(int i,int radix)//进制的范围:2-36,为什么呢?0,...9,a...z(10个数字+26个字母) - -//其他进制到十进制 -public static int parseInt(String s,int radix) -``` - -```java -public class IntegerDemo3 { - public static void main(String[] args) { - //test(); - //test2(); - test3(); - } - - //常用的基本进制转换 - public static void test(){ - System.out.println(Integer.toBinaryString(100));//1100100 - System.out.println(Integer.toOctalString(100));//144 - System.out.println(Integer.toHexString(100));//64 - System.out.println("-----------------------------"); - } - - //十进制到其他进制 - public static void test2(){ - System.out.println(Integer.toString(100, 10));//100 - System.out.println(Integer.toString(100, 2));//1100100 - System.out.println(Integer.toString(100, 8));//144 - System.out.println(Integer.toString(100, 16));//64 - System.out.println(Integer.toString(100, 5));//400 - System.out.println(Integer.toString(100, 7));//202 - //进制的范围在2-36之间,超过这个范围,就作为十进制处理 - System.out.println(Integer.toString(100, -7)); //100 - System.out.println(Integer.toString(100, 70));//100 - System.out.println(Integer.toString(100, 1));//100 - System.out.println(Integer.toString(100, 37));//100 - - System.out.println(Integer.toString(100, 17));//5f - System.out.println(Integer.toString(100, 32));//34 - System.out.println(Integer.toString(100, 36));//2s - System.out.println("-------------------------"); - } - - //任意进制转换为十进制 - public static void test3(){ - System.out.println(Integer.parseInt("100", 10));//100 - System.out.println(Integer.parseInt("100", 2));//4 - System.out.println(Integer.parseInt("100", 8));//64 - System.out.println(Integer.parseInt("100", 16));//256 - System.out.println(Integer.parseInt("100", 23));//529 - //NumberFormatException,因为二进制是不可能存在 123的 - //System.out.println(Integer.parseInt("123", 2)); - } -} -``` - -- JDK5的新特性 - -自动装箱:把基本类型转换为包装类类型 - -自动拆箱:把包装类类型转换为基本类型 - -```java -public class IntegerDemo4 { - public static void main(String[] args) { - Integer num=null; - if(num!=null){ - num+=100; - System.out.println(num); - } - test(); //num:200 - test2(); //num:200 - } - - //自动装箱,拆箱 - public static void test() { - Integer num=100; - //自动装箱,实际上就是 Integer num=Integer.valueOf(100) - num+=100; - //先拆箱再装箱 num.intValue()+100=200 --> Integer num=Integer.valueOf(num.intValue()+100) - System.out.println(new StringBuilder("num:").append(num).toString()); - } - - //手动拆装箱 - public static void test2() { - Integer num=Integer.valueOf(100); //手动装箱 - num=Integer.valueOf(num.intValue()+100); //先手动拆箱,进行运算后,再手动装箱 - System.out.println(new StringBuilder("num:").append(num).toString()); - } -} -``` - -- 小练习:看程序写结果 -```java -public class IntegerTest { - public static void main(String[] args) { - Integer i1 = new Integer(127); - Integer i2 = new Integer(127); - System.out.println(i1 == i2); - System.out.println(i1.equals(i2)); - System.out.println("-----------"); - - Integer i3 = new Integer(128); - Integer i4 = new Integer(128); - System.out.println(i3 == i4); - System.out.println(i3.equals(i4)); - System.out.println("-----------"); - - Integer i5 = 128; - Integer i6 = 128; - System.out.println(i5 == i6); - System.out.println(i5.equals(i6)); - System.out.println("-----------"); - - Integer i7 = 127; - Integer i8 = 127; - System.out.println(i7 == i8); - System.out.println(i7.equals(i8)); - } -} -``` -输出结果: -```html -false -true ------------ -false -true ------------ -false -true ------------ -true -true -``` -分析: -Integer的数据直接赋值,如果在-128到127之间,会直接从缓冲池里获取数据。 -通过查看源码,针对-128到127之间的数据,做了一个**数据缓冲池IntegerCache**, -如果数据是该范围内的,每次并不创建新的空间。 - -```java -public static Integer valueOf(int i) { - if (i >= IntegerCache.low && i <= IntegerCache.high) - return IntegerCache.cache[i + (-IntegerCache.low)]; - return new Integer(i); - } - - private static class IntegerCache { - static final int low = -128; //low=-128 - static final int high; - static final Integer cache[]; - - static { - // high value may be configured by property - int h = 127; - //... - } - high = h; //high=127 -``` -## Object -Object类是类层次结构的根类。每个类都使用 Object 作为超类。每个类都直接或者间接的继承自Object类。 - -- Object类的方法: -```java -public int hashCode() //返回该对象的哈希码值。 -// 注意:哈希值是根据哈希算法计算出来的一个值,这个值和地址值有关,但是不是实际地址值。 - -public final Class getClass() //返回此 Object 的运行时类 - -public String toString() //返回该对象的字符串表示。 - -protected Object clone() //创建并返回此对象的一个副本。可重写该方法 - -protected void finalize() -//当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。用于垃圾回收,但是什么时候回收不确定。 -``` - -- [Object的使用 代码示例](https://github.com/DuHouAn/Java/tree/master/JavaBasics/src/code_03_Object) - -## Scanner -Scanner:用于接收键盘录入数据。 - -- 使用Scanner三部曲: - -A:导包 - -B :创建对象 - -C :使用相应方法 - -- Scanner常用成员方法 -```java -Scanner(InputStream source) //构造方法 - -public boolean hasNextXxx() //判断是否是某种类型的元素,Xxx表示类型,比如 public boolean hasNextInt() - -public Xxx nextXxx() //获取该元素,Xxx表示类型,比如public int nextInt() -``` -注意:InputMismatchException:表示输入的和你想要的不匹配 - -- 使用示例1: -```java -public class ScannerDemo { - public static void main(String[] args) { - Scanner sc=new Scanner(System.in); - if(sc.hasNextInt()){ - int x=sc.nextInt(); - System.out.println("x="+x); - }else{ - System.out.println("输入的数据有误"); - } - } -} -``` - -- 先获取一个数值,换行后,再获取一个字符串,会出现问题。主要原因:就是换行符号的问题。如何解决呢? -```java -public static void main(String[] args) { - Scanner sc=new Scanner(System.in); - - //输入 12 换行后在输出 "sss" - int x=sc.nextInt(); - System.out.println("x:"+x); //x:12 - - String line=sc.nextLine(); //这里会有问题 因为会将换行符当作字符输输入 - System.out.println("line:"+line); //line: -} -``` - -> 解决方案一:先获取一个数值后,再创建一个新的键盘录入对象获取字符串。 - -```java -public static void method() { - Scanner sc=new Scanner(System.in); - int x=sc.nextInt(); - System.out.println("x:"+x); - - Scanner sc2=new Scanner(System.in); - String line=sc2.nextLine(); - System.out.println("line:"+line); -} -``` - -> 解决方案二:把所有的数据都先按照字符串获取,然后要什么,就进行相应的转换。 - -```java -public static void method2() { - Scanner sc=new Scanner(System.in); - - String xStr=sc.nextLine(); - String line=sc.nextLine(); - - int x=Integer.parseInt(xStr); - - System.out.println("x:"+x); - System.out.println("line:"+line); -} -``` -## String -字符串:就是由多个字符组成的一串数据。也可以看成是一个字符数组。 - -- **String的构造方法方法**: -```java -//String的构造方法 -public String() //空构造 - -public String(byte[] bytes) //把字节数组转成字符串 - -public String(byte[] bytes,int index,int length) //把字节数组的一部分转成字符串 - -public String(char[] value) //把字符数组转成字符串 - -public String(char[] value,int index,int count) //把字符数组的一部分转成字符串,第三个参数表示的是数目 - -public String(String original) //把字符串常量值转成字符串 -``` - -```java -public class StringDemo { - public static void main(String[] args) { - //public String():空构造 - String s=new String(); - System.out.println("s:"+s); - System.out.println("s.length="+s.length()); - System.out.println("-----------------------"); - - //public String(byte[] bytes):把字节数组转成字符串 - byte[] bys={97,98,99,100,101}; - - String s2=new String(bys); - System.out.println("s2:"+s2); - System.out.println("s2.length="+s2.length()); - System.out.println("-----------------------"); - - //public String(byte[] bytes,int index,int length):把字节数组的一部分转成字符串 - String s3=new String(bys,0,3); //从0位置开始,3个字符 - System.out.println("s3:"+s3);//s3:abc - System.out.println("s3.length="+s3.length());//s3.length=3 - System.out.println("-----------------------"); - - - //public String(char[] value):把字符数组转成字符串 - char[] chs={'a','b','c','d','e'}; - String s4=new String(chs); //从0位置开始,3个字符 - System.out.println("s4:"+s4); - System.out.println("s4.length="+s4.length()); - System.out.println("-----------------------"); - - //public String(char[] value,int index,int count):把字符数组的一部分转成字符串 - String s5=new String(chs,0,3); //从0位置开始,3个字符 - System.out.println("s5:"+s5); - System.out.println("s5.length="+s5.length()); - System.out.println("-----------------------"); - - //public String(String original):把字符串常量值转成字符串 - String s6=new String("abcde"); - System.out.println("s6:"+s6); - System.out.println("s6.length="+s6.length()); - } -} -``` - -- 字符串的特点:一旦被赋值,就不能改变 -```java -public static void test() { - String s = "hello"; - s += "world"; - System.out.println("s:" + s); // helloworld -} -``` - -- 小练习:看程序,写结果: -```java -public static void test2(){ - String s1 = new String("hello"); - String s2 = new String("hello"); - System.out.println(s1 == s2); - System.out.println(s1.equals(s2)); - - String s3 = new String("hello"); - String s4 = "hello"; - System.out.println(s3 == s4); - System.out.println(s3.equals(s4)); - - String s5 = "hello"; - String s6 = "hello"; - System.out.println(s5 == s6); - System.out.println(s5.equals(s6)); -} -``` - -输出结果: -```html -false -true -false -true -true -true -``` -分析: -> =和qeuals的区别 - -==:比较引用类型,比较的是地址值是否相同 - -equals:比较引用类型,默认也是比较地址值是否相同 - -String类重写了equals()方法,比较的是内容是否相同。 - -> String s = new String(“hello”)和String s = “hello”的区别? - -前者会创建2个对象,后者创建1个对象。更具体的说,前者会创建2个或者1个对象,后者会创建1个或者0个对象。 - -- **String类的判断功能**: -```java -boolean equals(Object obj) //比较字符串的内容是否相同,区分大小写 - -boolean equalsIgnoreCase(String str) //比较字符串的内容是否相同,忽略大小写 - -boolean contains(String str) //判断大字符串中是否包含小字符串 - -boolean startsWith(String str) //判断字符串是否以某个指定的字符串开头 - -boolean endsWith(String str) //判断字符串是否以某个指定的字符串结尾 - -boolean isEmpty()// 判断字符串是否为空 -``` -注意:字符串内容为空和字符串对象为空。 -```java -String s = "";//字符串内容为空 -String s = null;//字符串对象为空 -``` - -```java -public class StringDemo3 { - public static void main(String[] args) { - // 创建字符串对象 - String s1 = "helloworld"; - String s2 = "helloworld"; - String s3 = "HelloWorld"; - - // boolean equals(Object obj):比较字符串的内容是否相同,区分大小写 - System.out.println("equals:" + s1.equals(s2));//true - System.out.println("equals:" + s1.equals(s3));//false - System.out.println("-----------------------"); - - // boolean equalsIgnoreCase(String str):比较字符串的内容是否相同,忽略大小写 - System.out.println("equals:" + s1.equalsIgnoreCase(s2));//true - System.out.println("equals:" + s1.equalsIgnoreCase(s3));//true - System.out.println("-----------------------"); - - // boolean contains(String str):判断大字符串中是否包含小字符串 - System.out.println("contains:" + s1.contains("hello"));//true - System.out.println("contains:" + s1.contains("hw"));//false - System.out.println("-----------------------"); - - // boolean startsWith(String str):判断字符串是否以某个指定的字符串开头 - System.out.println("startsWith:" + s1.startsWith("h"));//true - System.out.println("startsWith:" + s1.startsWith("hello"));//true - System.out.println("startsWith:" + s1.startsWith("world"));//false - System.out.println("-----------------------"); - - //boolean endsWith(String str):判断字符串是否以某个指定的字符串结尾 - System.out.println("startsWith:" + s1.endsWith("d"));//true - System.out.println("startsWith:" + s1.endsWith("world"));//true - System.out.println("startsWith:" + s1.endsWith("hello"));//false - System.out.println("-----------------------"); - - // boolean isEmpty():判断字符串是否为空。 - System.out.println("isEmpty:" + s1.isEmpty());//false - - String s4 = ""; //字符串内容为空 - String s5 = null;//字符串对象为空 - System.out.println("isEmpty:" + s4.isEmpty());//true - // NullPointerException - // s5对象都不存在,所以不能调用方法,空指针异常 - //System.out.println("isEmpty:" + s5.isEmpty()); - } -} -``` - -- **String类的获取功能**: -```java -int length() //获取字符串的长度。 - -char charAt(int index) //获取指定索引位置的字符 - -int indexOf(int ch) //返回指定字符在此字符串中第一次出现处的索引。为什么这里参数int类型,而不是char类型?原因是:'a'和97其实都可以代表'a' - -int indexOf(String str) //返回指定字符串在此字符串中第一次出现处的索引。 - -int indexOf(int ch,int fromIndex) //返回指定字符在此字符串中从指定位置后第一次出现处的索引。 - -int indexOf(String str,int fromIndex) //返回指定字符串在此字符串中从指定位置后第一次出现处的索引。 - -String substring(int start) //从指定位置开始截取字符串,默认到末尾。 - -String substring(int start,int end) //从指定位置开始到指定位置结束截取字符串。左闭右开 -``` - -```java -public class StringDemo4 { - public static void main(String[] args) { - // 定义一个字符串对象 - String s = "helloworld"; - - // int length():获取字符串的长度。 - System.out.println("s.length:" + s.length());//10 - System.out.println("----------------------"); - - // char charAt(int index):获取指定索引位置的字符 - System.out.println("charAt:" + s.charAt(7));//r - System.out.println("----------------------"); - - // int indexOf(int ch):返回指定字符在此字符串中第一次出现处的索引。 - System.out.println("indexOf:" + s.indexOf('l'));//2 - System.out.println("----------------------"); - - // int indexOf(String str):返回指定字符串在此字符串中第一次出现处的索引。 - System.out.println("indexOf:" + s.indexOf("owo"));//4 - System.out.println("----------------------"); - - // int indexOf(int ch,int fromIndex):返回指定字符在此字符串中从指定位置后第一次出现处的索引。 - System.out.println("indexOf:" + s.indexOf('l', 4));//8 - System.out.println("indexOf:" + s.indexOf('k', 4)); // -1 - System.out.println("indexOf:" + s.indexOf('l', 40)); // -1 - System.out.println("----------------------"); - - // int indexOf(String str,intfromIndex):返回指定字符串在此字符串中从指定位置后第一次出现处的索引。 - System.out.println("indexOf:" + s.indexOf("owo", 4));//4 - System.out.println("indexOf:" + s.indexOf("ll", 4)); //-1 - System.out.println("indexOf:" + s.indexOf("ld", 40)); // -1 - System.out.println("----------------------"); - - // String substring(int start):从指定位置开始截取字符串,默认到末尾。包含start这个索引 - System.out.println("substring:" + s.substring(5));//world - System.out.println("substring:" + s.substring(0));//helloworld - System.out.println("----------------------"); - - // String substring(int start,int - // end):从指定位置开始到指定位置结束截取字符串。包括start索引但是不包end索引 - System.out.println("substring:" + s.substring(3, 8));//lowor - System.out.println("substring:" + s.substring(0, s.length()));//helloworld - - /** - * 获取 字符串中的每个字符 - */ - for(int i=0;i compareTo()方法源码解析 - -```java -private final char value[]; //字符串会自动转换为一个字符数组。 -/** -* 举例 - String s6 = "hello"; - String s9 = "xyz"; - System.out.println(s6.compareTo(s9));// -16 -*/ -public int compareTo(String anotherString) { - int len1 = value.length; - int len2 = anotherString.value.length; - - int lim = Math.min(len1, len2); //lim=3 - char v1[] = value; //v1[h e l l o] - char v2[] = anotherString.value;//v2[x y z] - - int k = 0; - while (k < lim) { - char c1 = v1[k];//h - char c2 = v2[k];//x - if (c1 != c2) { //先比较的是对应的字符是否相等 - return c1 - c2; //h=104 x=120 104-120=-16 - } - k++; - } - //如果在指定位置,字符都相同,则长度相减 - return len1 - len2; -} -``` - -- 练习1:把数组中的数据按照指定个格式拼接成一个字符串。举例:int[] arr = {1,2,3};输出结果:[1, 2, 3] -```java -/** - * - * 需求:把数组中的数据按照指定个格式拼接成一个字符串 - * 举例: - * int[] arr = {1,2,3}; - * 输出结果: - * "[1, 2, 3]" - * 分析: - * A:定义一个字符串对象,只不过内容为空 - * B:先把字符串拼接一个"[" - * C:遍历int数组,得到每一个元素 - * D:先判断该元素是否为最后一个 - * 是:就直接拼接元素和"]" - * 不是:就拼接元素和逗号以及空格 - * E:输出拼接后的字符串 - */ -public class StringTest2 { - public static void main(String[] args) { - int[] arr={1,2,3,4}; - System.out.println(arrayToString(arr)); - } - - /** - * 把数组中的数据按照指定个格式拼接成一个字符串 - */ - public static String arrayToString(int[] arr){ - String str="["; - for(int i=0;iStringBuffer - public static StringBuffer stringToStringBuffer(String s) { - // 注意:不能把字符串的值直接赋值给StringBuffer - // StringBuffer sb = "hello"; - // StringBuffer sb = s; - - // 方式1:通过构造方法 - /* StringBuffer sb = new StringBuffer(s); - return sb;*/ - // 方式2:通过append()方法 - StringBuffer sb2 = new StringBuffer(); - sb2.append(s); - return sb2; - } - - //StringBuffer -->String - public static String stringBufferToString(StringBuffer buffer){ - // 方式1:通过构造方法 - /* String str = new String(buffer); - return str;*/ - // 方式2:通过toString()方法 - String str2 = buffer.toString(); - return str2; - } -} -``` - -- 练习1:将数组拼接成一个字符串 - -```java -public class StringBufferTest { - public static void main(String[] args) { - int[] arr={1,2,3,4}; - System.out.println(arrToString(arr));//[1,2,3,4] - } - - public static String arrToString(int[] arr){ - StringBuffer buffer=new StringBuffer(); - buffer.append("["); - for(int i=0;i set = new HashSet<>(); +set.add(e1); +set.add(e2); +System.out.println(set.size()); // 2 +``` + +理想的散列函数应当具有均匀性,即不相等的对象应当均匀分布到所有可能的散列值上。 +这就要求了散列函数要把**所有域的值都考虑进来**。 +可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。 +R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位。 + +一个数与 31 相乘可以转换成移位和减法:`31*x == (x<<5)-x`,编译器会自动进行这个优化。 + +```java +@Override +public int hashCode() { + int result = 17; + result = 31 * result + x; + result = 31 * result + y; + result = 31 * result + z; + return result; +} +``` + +> 了解:IDEA中 Alt+Insert 快捷键就可以快速生成 hashCode() 和 equals() 方法。 + +### toString() 方法 + +默认返回 ToStringExample@4554617c 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。 + +```java +public class ToStringExample { + + private int number; + + public ToStringExample(int number) { + this.number = number; + } +} +``` + +```java +ToStringExample example = new ToStringExample(123); +System.out.println(example.toString()); +``` + +```html +ToStringExample@4554617c +``` + +### clone() 方法 + +**1. Cloneable** + +clone() 是 Object 的 **protected 方法**,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。 + +```java +public class CloneExample { + private int a; + private int b; +} +``` + +```java +CloneExample e1 = new CloneExample(); +// CloneExample e2 = e1.clone(); +// 'clone()' has protected access in 'java.lang.Object' +``` + +重写 clone() 得到以下实现: + +```java +public class CloneExample { + private int a; + private int b; + + // CloneExample 默认继承 Object + @Override + public CloneExample clone() throws CloneNotSupportedException { + return (CloneExample)super.clone(); + } +} +``` + +```java +CloneExample e1 = new CloneExample(); +try { + CloneExample e2 = e1.clone(); +} catch (CloneNotSupportedException e) { + e.printStackTrace(); +} +``` + +```html +java.lang.CloneNotSupportedException: CloneExample +``` + +以上抛出了 CloneNotSupportedException,这是因为 CloneExample 没有实现 Cloneable 接口。 + +应该注意的是,**clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法**。 + +**Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException**。 + +```java +public class CloneExample implements Cloneable { + private int a; + private int b; + + @Override + public Object clone() throws CloneNotSupportedException { + return super.clone(); + } +} +``` + +**2. 浅拷贝** + +拷贝对象和原始对象的引用类型引用同一个对象。 + +```java +public class ShallowCloneExample implements Cloneable { + + private int[] arr; + + public ShallowCloneExample() { + arr = new int[10]; + for (int i = 0; i < arr.length; i++) { + arr[i] = i; + } + } + + public void set(int index, int value) { + arr[index] = value; + } + + public int get(int index) { + return arr[index]; + } + + @Override + protected ShallowCloneExample clone() throws CloneNotSupportedException { + return (ShallowCloneExample) super.clone(); + } +} +``` + +```java +// 拷贝对象和原始对象的引用类型引用同一个对象。 +ShallowCloneExample e1 = new ShallowCloneExample(); +ShallowCloneExample e2 = null; +try { + e2 = e1.clone(); +} catch (CloneNotSupportedException e) { + e.printStackTrace(); +} +e1.set(2, 222); +System.out.println(e1.get(2)); // 222 +System.out.println(e2.get(2)); // 222 +``` + +**3. 深拷贝** + +拷贝对象和原始对象的引用类型引用不同对象。 + +```java +public class DeepCloneExample implements Cloneable { + + private int[] arr; + + public DeepCloneExample() { + arr = new int[10]; + for (int i = 0; i < arr.length; i++) { + arr[i] = i; + } + } + + public void set(int index, int value) { + arr[index] = value; + } + + public int get(int index) { + return arr[index]; + } + + @Override + protected DeepCloneExample clone() throws CloneNotSupportedException { + DeepCloneExample result = (DeepCloneExample) super.clone(); + // 创建新对象 + result.arr = new int[arr.length]; + for (int i = 0; i < arr.length; i++) { + result.arr[i] = arr[i]; + } + return result; + } +} +``` + +```java +DeepCloneExample e1 = new DeepCloneExample(); +DeepCloneExample e2 = null; +try { + e2 = e1.clone(); +} catch (CloneNotSupportedException e) { + e.printStackTrace(); +} +e1.set(2, 222); +System.out.println(e1.get(2)); // 222 +System.out.println(e2.get(2)); // 2 +``` + +**4. clone() 的替代方案** + +使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。 +Effective Java 书上讲到,最好不要去使用 clone(),可以使用**拷贝构造函数或者拷贝工厂来拷贝一个对象**。 + +```java +public class CloneConstructorExample { + + private int[] arr; + + public CloneConstructorExample() { //构造函数 + arr = new int[10]; + for (int i = 0; i < arr.length; i++) { + arr[i] = i; + } + } + + public CloneConstructorExample(CloneConstructorExample original) { // 拷贝构造函数 + arr = new int[original.arr.length]; + for (int i = 0; i < original.arr.length; i++) { + arr[i] = original.arr[i]; + } + } + + public void set(int index, int value) { + arr[index] = value; + } + + public int get(int index) { + return arr[index]; + } +} +``` + +```java +CloneConstructorExample e1 = new CloneConstructorExample(); +CloneConstructorExample e2 = new CloneConstructorExample(e1); +e1.set(2, 222); +System.out.println(e1.get(2)); // 222 +System.out.println(e2.get(2)); // 2 +``` + + + +## String + +String 被声明为 final,因此它不可被继承。 + +**内部使用 char 数组存储数据,该数组被声明为 final**, +这意味着 value 数组初始化之后就不能再引用其它数组。 +并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变。 + +```java +public final class String + implements java.io.Serializable, Comparable, CharSequence { + /** The value is used for character storage. */ + private final char value[]; +``` + +### 构造方法 + +```java +public String() //空构造 + +public String(byte[] bytes) //把字节数组转成字符串 + +public String(byte[] bytes,int index,int length) //把字节数组的一部分转成字符串 + +public String(char[] value) //把字符数组转成字符串 + +public String(char[] value,int index,int count) //把字符数组的一部分转成字符串,第三个参数表示的是数目 + +public String(String original) //把字符串常量值转成字符串 +``` + +使用示例: + +```java +public class StringDemo { + public static void main(String[] args) { + //public String():空构造 + String s=new String(); + System.out.println("s:"+s); + System.out.println("s.length="+s.length()); + System.out.println("-----------------------"); + + //public String(byte[] bytes):把字节数组转成字符串 + byte[] bys={97,98,99,100,101}; + + String s2=new String(bys); + System.out.println("s2:"+s2); + System.out.println("s2.length="+s2.length()); + System.out.println("-----------------------"); + + //public String(byte[] bytes,int index,int length):把字节数组的一部分转成字符串 + String s3=new String(bys,0,3); //从0位置开始,3个字符 + System.out.println("s3:"+s3);//s3:abc + System.out.println("s3.length="+s3.length());//s3.length=3 + System.out.println("-----------------------"); + + + //public String(char[] value):把字符数组转成字符串 + char[] chs={'a','b','c','d','e'}; + String s4=new String(chs); //从0位置开始,3个字符 + System.out.println("s4:"+s4); + System.out.println("s4.length="+s4.length()); + System.out.println("-----------------------"); + + //public String(char[] value,int index,int count):把字符数组的一部分转成字符串 + String s5=new String(chs,0,3); //从0位置开始,3个字符 + System.out.println("s5:"+s5); + System.out.println("s5.length="+s5.length()); + System.out.println("-----------------------"); + + //public String(String original):把字符串常量值转成字符串 + String s6=new String("abcde"); + System.out.println("s6:"+s6); + System.out.println("s6.length="+s6.length()); + } +} +``` + +字符串的特点:一旦被赋值,就不能改变 + +```java +public static void test() { + String s = "hello"; + s += "world"; + System.out.println("s:" + s); // helloworld +} +``` + +小练习: + +```java +public static void test2(){ + String s1 = new String("hello"); + String s2 = new String("hello"); + System.out.println(s1 == s2); // false + System.out.println(s1.equals(s2)); // true + + String s3 = new String("hello"); + String s4 = "hello"; + System.out.println(s3 == s4); // false + System.out.println(s3.equals(s4)); // true + + String s5 = "hello"; + String s6 = "hello"; + System.out.println(s5 == s6); // true + System.out.println(s5.equals(s6)); // true +} +``` + +### 判断功能 + +```java +boolean equals(Object obj) //比较字符串的内容是否相同,区分大小写 + +boolean equalsIgnoreCase(String str) //比较字符串的内容是否相同,忽略大小写 + +boolean contains(String str) //判断大字符串中是否包含小字符串 + +boolean startsWith(String str) //判断字符串是否以某个指定的字符串开头 + +boolean endsWith(String str) //判断字符串是否以某个指定的字符串结尾 + +boolean isEmpty()// 判断字符串是否为空 +``` + +注意:字符串内容为空和字符串对象为空。 + +```java +String s = "";//字符串内容为空 +String s = null;//字符串对象为空 +``` + +使用示例: + +```java +public class StringDemo3 { + public static void main(String[] args) { + // 创建字符串对象 + String s1 = "helloworld"; + String s2 = "helloworld"; + String s3 = "HelloWorld"; + + // boolean equals(Object obj):比较字符串的内容是否相同,区分大小写 + System.out.println("equals:" + s1.equals(s2));//true + System.out.println("equals:" + s1.equals(s3));//false + System.out.println("-----------------------"); + + // boolean equalsIgnoreCase(String str):比较字符串的内容是否相同,忽略大小写 + System.out.println("equals:" + s1.equalsIgnoreCase(s2));//true + System.out.println("equals:" + s1.equalsIgnoreCase(s3));//true + System.out.println("-----------------------"); + + // boolean contains(String str):判断大字符串中是否包含小字符串 + System.out.println("contains:" + s1.contains("hello"));//true + System.out.println("contains:" + s1.contains("hw"));//false + System.out.println("-----------------------"); + + // boolean startsWith(String str):判断字符串是否以某个指定的字符串开头 + System.out.println("startsWith:" + s1.startsWith("h"));//true + System.out.println("startsWith:" + s1.startsWith("hello"));//true + System.out.println("startsWith:" + s1.startsWith("world"));//false + System.out.println("-----------------------"); + + //boolean endsWith(String str):判断字符串是否以某个指定的字符串结尾 + System.out.println("startsWith:" + s1.endsWith("d"));//true + System.out.println("startsWith:" + s1.endsWith("world"));//true + System.out.println("startsWith:" + s1.endsWith("hello"));//false + System.out.println("-----------------------"); + + // boolean isEmpty():判断字符串是否为空。 + System.out.println("isEmpty:" + s1.isEmpty());//false + + String s4 = ""; //字符串内容为空 + String s5 = null;//字符串对象为空 + System.out.println("isEmpty:" + s4.isEmpty());//true + // NullPointerException + // s5对象都不存在,所以不能调用方法,空指针异常 + //System.out.println("isEmpty:" + s5.isEmpty()); + } +} +``` + +### 获取功能 + +```java +int length() //获取字符串的长度。 + +char charAt(int index) //获取指定索引位置的字符 + +int indexOf(int ch) //返回指定字符在此字符串中第一次出现处的索引。为什么这里参数int类型,而不是char类型?原因是:'a'和97其实都可以代表'a' + +int indexOf(String str) //返回指定字符串在此字符串中第一次出现处的索引。 + +int indexOf(int ch,int fromIndex) //返回指定字符在此字符串中从指定位置后第一次出现处的索引。 + +int indexOf(String str,int fromIndex) //返回指定字符串在此字符串中从指定位置后第一次出现处的索引。 + +String substring(int start) //从指定位置开始截取字符串,默认到末尾。 + +String substring(int start,int end) //从指定位置开始到指定位置结束截取字符串。左闭右开 +``` + +使用示例: + +```java +public class StringDemo4 { + public static void main(String[] args) { + // 定义一个字符串对象 + String s = "helloworld"; + + // int length():获取字符串的长度。 + System.out.println("s.length:" + s.length());//10 + System.out.println("----------------------"); + + // char charAt(int index):获取指定索引位置的字符 + System.out.println("charAt:" + s.charAt(7));//r + System.out.println("----------------------"); + + // int indexOf(int ch):返回指定字符在此字符串中第一次出现处的索引。 + System.out.println("indexOf:" + s.indexOf('l'));//2 + System.out.println("----------------------"); + + // int indexOf(String str):返回指定字符串在此字符串中第一次出现处的索引。 + System.out.println("indexOf:" + s.indexOf("owo"));//4 + System.out.println("----------------------"); + + // int indexOf(int ch,int fromIndex):返回指定字符在此字符串中从指定位置后第一次出现处的索引。 + System.out.println("indexOf:" + s.indexOf('l', 4));//8 + System.out.println("indexOf:" + s.indexOf('k', 4)); // -1 + System.out.println("indexOf:" + s.indexOf('l', 40)); // -1 + System.out.println("----------------------"); + + // int indexOf(String str,intfromIndex):返回指定字符串在此字符串中从指定位置后第一次出现处的索引。 + System.out.println("indexOf:" + s.indexOf("owo", 4));//4 + System.out.println("indexOf:" + s.indexOf("ll", 4)); //-1 + System.out.println("indexOf:" + s.indexOf("ld", 40)); // -1 + System.out.println("----------------------"); + + // String substring(int start):从指定位置开始截取字符串,默认到末尾。包含start这个索引 + System.out.println("substring:" + s.substring(5));//world + System.out.println("substring:" + s.substring(0));//helloworld + System.out.println("----------------------"); + + // String substring(int start,int + // end):从指定位置开始到指定位置结束截取字符串。包括start索引但是不包end索引 + System.out.println("substring:" + s.substring(3, 8));//lowor + System.out.println("substring:" + s.substring(0, s.length()));//helloworld + + /** + * 获取 字符串中的每个字符 + */ + for(int i=0;iStringBuffer + public static StringBuffer stringToStringBuffer(String s) { + // 注意:不能把字符串的值直接赋值给StringBuffer + // StringBuffer sb = "hello"; + // StringBuffer sb = s; + + // 方式1:通过构造方法 + /* StringBuffer sb = new StringBuffer(s); + return sb;*/ + // 方式2:通过append()方法 + StringBuffer sb2 = new StringBuffer(); + sb2.append(s); + return sb2; + } + + //StringBuffer -->String + public static String stringBufferToString(StringBuffer buffer){ + // 方式1:通过构造方法 + /* String str = new String(buffer); + return str;*/ + // 方式2:通过toString()方法 + String str2 = buffer.toString(); + return str2; + } +} +``` + +### 练习 + +练习1:将数组拼接成一个字符串 + +```java +public class StringBufferTest { + public static void main(String[] args) { + int[] arr={1,2,3,4}; + System.out.println(arrToString(arr));//[1,2,3,4] + } + + public static String arrToString(int[] arr){ + StringBuffer buffer=new StringBuffer(); + buffer.append("["); + for(int i=0;i **三者使用的总结** + +- 操作少量的数据,使用String +- 单线程操作字符串缓冲区下操作大量数据,使用StringBuilder +- 多线程操作字符串缓冲区下操作大量数据,使用StringBuffer \ No newline at end of file diff --git "a/docs/JavaBasics/10_Java\345\270\270\350\247\201\345\257\271\350\261\241_2.md" "b/docs/JavaBasics/10_Java\345\270\270\350\247\201\345\257\271\350\261\241_2.md" new file mode 100644 index 00000000..ba733417 --- /dev/null +++ "b/docs/JavaBasics/10_Java\345\270\270\350\247\201\345\257\271\350\261\241_2.md" @@ -0,0 +1,687 @@ +# Java 常见类 II + +## Arrays +Arrays 是针对数组进行操作的工具类。 + +### 常用成员方法 + +```java +public static String toString(int[] a) //把数组转成字符串 +public static void sort(int[] a) //对数组进行排序 +public static int binarySearch(int[] a,int key) //二分查找 +``` + +其中,toString() 源码如下: + +```java +public static String toString(int[] a) { + if (a == null) + return "null"; + int iMax = a.length - 1; + if (iMax == -1) + return "[]"; + + StringBuilder b = new StringBuilder(); + b.append('['); + for (int i = 0; ; i++) { + b.append(a[i]); + if (i == iMax) + return b.append(']').toString(); + b.append(", "); + } +} +``` + +binarySearch() 调用的是 binarySearch0(),binarySearch0() 源码如下: + +```java +private static int binarySearch0(int[] a, int fromIndex, int toIndex,int key) { + int low = fromIndex; + int high = toIndex - 1; + + while (low <= high) { + int mid = (low + high) >>> 1; + int midVal = a[mid]; + + if (midVal < key) + low = mid + 1; + else if (midVal > key) + high = mid - 1; + else + return mid; // key found + } + return -(low + 1); // key not found. +} +``` + +### 使用示例 + +```java +public class ArraysDemo { + public static void main(String[] args) { + // 定义一个数组 + int[] arr = { 24, 69, 80, 57, 13 }; + + // public static String toString(int[] a) 把数组转成字符串 + System.out.println("排序前:" + Arrays.toString(arr));//排序前:[24, 69, 80, 57, 13] + + // public static void sort(int[] a) 对数组进行排序 + Arrays.sort(arr); + System.out.println("排序后:" + Arrays.toString(arr));//排序后:[13, 24, 57, 69, 80] + + // [13, 24, 57, 69, 80] + // public static int binarySearch(int[] a,int key) 二分查找 + System.out.println("binarySearch:" + Arrays.binarySearch(arr, 57));//binarySearch:2 + System.out.println("binarySearch:" + Arrays.binarySearch(arr, 577));//binarySearch:-6 + } +} +``` + + + +## BigDemical + +BigDecimal类:不可变的、任意精度的有符号十进制数,可以解决数据丢失问题。 + +看如下程序,写出结果: +```java +public static void main(String[] args) { + System.out.println(0.09 + 0.01); // 0.09999999999999999 + System.out.println(1.0 - 0.32); // 0.6799999999999999 + System.out.println(1.015 * 100); // 101.49999999999999 + System.out.println(1.301 / 100); // 0.013009999999999999 + System.out.println(1.0 - 0.12); // 0.88 +} +``` +结果和我们想的有一点点不一样,这是因为浮点数类型的数据存储和整数不一样导致的。 +它们大部分的时候,都是带有有效数字位。由于在运算的时候,float类型和double很容易丢失精度, +所以,为了能精确的表示、计算浮点数,Java 提供了 BigDecimal。 + +### 常用成员方法 + +```java +public BigDecimal(String val) //构造方法 + +public BigDecimal add(BigDecimal augend) //加 + +public BigDecimal subtract(BigDecimal subtrahend)//减 + +public BigDecimal multiply(BigDecimal multiplicand) //乘 + +public BigDecimal divide(BigDecimal divisor) //除 + +public BigDecimal divide(BigDecimal divisor,int scale,int roundingMode) +//除法,scale:几位小数,roundingMode:如何舍取 +``` + +### 使用示例 + +```java +public static void main(String[] args) { + /*System.out.println(0.09 + 0.01); + System.out.println(1.0 - 0.32); + System.out.println(1.015 * 100); + System.out.println(1.301 / 100); + System.out.println(1.0 - 0.12);*/ + + BigDecimal bd1 = new BigDecimal("0.09"); + BigDecimal bd2 = new BigDecimal("0.01"); + System.out.println("add:" + bd1.add(bd2));//add:0.10 + System.out.println("-------------------"); + + BigDecimal bd3 = new BigDecimal("1.0"); + BigDecimal bd4 = new BigDecimal("0.32"); + System.out.println("subtract:" + bd3.subtract(bd4));//subtract:0.68 + System.out.println("-------------------"); + + BigDecimal bd5 = new BigDecimal("1.015"); + BigDecimal bd6 = new BigDecimal("100"); + System.out.println("multiply:" + bd5.multiply(bd6));//multiply:101.500 + System.out.println("-------------------"); + + BigDecimal bd7 = new BigDecimal("1.301"); + BigDecimal bd8 = new BigDecimal("100"); + System.out.println("divide:" + bd7.divide(bd8));//divide:0.01301 + + //四舍五入 + System.out.println("divide:" + + bd7.divide(bd8, 3, BigDecimal.ROUND_HALF_UP));//保留三位有效数字 + //divide:0.013 + + System.out.println("divide:" + + bd7.divide(bd8, 8, BigDecimal.ROUND_HALF_UP));//保留八位有效数字 + //divide:0.01301000 +} +``` + + + +## BigInteger + +BigInteger:可以让超过 Integer 范围内的数据进行运算。 + +### 常用成员方法 + +```java +public BigInteger add(BigInteger val) //加 + +public BigInteger subtract(BigInteger val) //减 + +public BigInteger multiply(BigInteger val) //乘 + +public BigInteger divide(BigInteger val) //除 + +public BigInteger[] divideAndRemainder(BigInteger val)//返回商和余数的数组 +``` + +### 使用示例 + +示例1: + +```java +public class BigIntegerDemo { + public static void main(String[] args) { + Integer num = new Integer("2147483647"); + System.out.println(num); + + //Integer num2 = new Integer("2147483648"); + // Exception in thread "main" java.lang.NumberFormatException: For input string: "2147483648" + //System.out.println(num2); + + // 通过 BigIntege来创建对象 + BigInteger num2 = new BigInteger("2147483648"); + System.out.println(num2); + } +} +``` + +示例2: + +```java +public class BigIntegerDemo2 { + public static void main(String[] args) { + BigInteger bi1 = new BigInteger("100"); + BigInteger bi2 = new BigInteger("50"); + + // public BigInteger add(BigInteger val):加 + System.out.println("add:" + bi1.add(bi2)); //add:150 + // public BigInteger subtract(BigInteger Val):减 + System.out.println("subtract:" + bi1.subtract(bi2));//subtract:50 + // public BigInteger multiply(BigInteger val):乘 + System.out.println("multiply:" + bi1.multiply(bi2));//multiply:5000 + // public BigInteger divide(BigInteger val):除 + System.out.println("divide:" + bi1.divide(bi2));//divide:2 + + // public BigInteger[] divideAndRemainder(BigInteger val):返回商和余数的数组 + BigInteger[] bis = bi1.divideAndRemainder(bi2); + System.out.println("divide:" + bis[0]);//divide:2 + System.out.println("remainder:" + bis[1]);//remainder:0 + } +} +``` + + + + + +## Character + +Character 类在对象中包装一个基本类型 char 的值.此外,该类提供了几种方法, +以确定字符的类别(小写字母,数字,等等),并将字符从大写转换成小写,反之亦然。 + +### 常用成员方法 + +```java +Character(char value) //构造方法 + +public static boolean isUpperCase(char ch) //判断给定的字符是否是大写字符 + +public static boolean isLowerCase(char ch) //判断给定的字符是否是小写字符 + +public static boolean isDigit(char ch) //判断给定的字符是否是数字字符 + +public static char toUpperCase(char ch) //把给定的字符转换为大写字符 + +public static char toLowerCase(char ch) //把给定的字符转换为小写字符 +``` + +### 使用示例 + +```java +public class CharacterDemo { + public static void main(String[] args) { + // public static boolean isUpperCase(char ch):判断给定的字符是否是大写字符 + System.out.println("isUpperCase:" + Character.isUpperCase('A'));//true + System.out.println("isUpperCase:" + Character.isUpperCase('a'));//false + System.out.println("isUpperCase:" + Character.isUpperCase('0'));//false + System.out.println("-----------------------------------------"); + + // public static boolean isLowerCase(char ch):判断给定的字符是否是小写字符 + System.out.println("isLowerCase:" + Character.isLowerCase('A'));//false + System.out.println("isLowerCase:" + Character.isLowerCase('a'));//true + System.out.println("isLowerCase:" + Character.isLowerCase('0'));//false + System.out.println("-----------------------------------------"); + + // public static boolean isDigit(char ch):判断给定的字符是否是数字字符 + System.out.println("isDigit:" + Character.isDigit('A'));//false + System.out.println("isDigit:" + Character.isDigit('a'));//false + System.out.println("isDigit:" + Character.isDigit('0'));//true + System.out.println("-----------------------------------------"); + + // public static char toUpperCase(char ch):把给定的字符转换为大写字符 + System.out.println("toUpperCase:" + Character.toUpperCase('A'));//A + System.out.println("toUpperCase:" + Character.toUpperCase('a'));//A + System.out.println("-----------------------------------------"); + + // public static char toLowerCase(char ch):把给定的字符转换为小写字符 + System.out.println("toLowerCase:" + Character.toLowerCase('A'));//a + System.out.println("toLowerCase:" + Character.toLowerCase('a'));//a + } +} +``` + +### 练习 + +统计一个字符串中大写字母字符,小写字母字符,数字字符出现的次数。(不考虑其他字符) + +```java +/** + * 统计一个字符串中大写字母字符,小写字母字符,数字字符出现的次数。(不考虑其他字符) + * + * 分析: + * A:定义三个统计变量。 + * int bigCont=0; + * int smalCount=0; + * int numberCount=0; + * B:键盘录入一个字符串。 + * C:把字符串转换为字符数组。 + * D:遍历字符数组获取到每一个字符 + * E:判断该字符是 + * 大写 bigCount++; + * 小写 smalCount++; + * 数字 numberCount++; + * F:输出结果即可 + */ +public class CharacterTest { + public static void main(String[] args) { + Scanner sc=new Scanner(System.in); + String str=sc.nextLine(); + printCount(str); + printCount2(str); + } + + //原来的写法 + public static void printCount(String str) { + int numberCount=0; + int lowercaseCount=0; + int upercaseCount=0; + + for(int index=0;index='0' && ch<='9'){ + numberCount++; + }else if(ch>='A' && ch<='Z'){ + upercaseCount++; + }else if(ch>='a' && ch<='z'){ + lowercaseCount++; + } + } + System.out.println("数字有"+numberCount+"个"); + System.out.println("小写字母有"+lowercaseCount+"个"); + System.out.println("大写字母有"+upercaseCount+"个"); + } + + //使用包装类来改进 + public static void printCount2(String str) { + int numberCount=0; + int lowercaseCount=0; + int upercaseCount=0; + + for(int index=0;index 解决方案一:先获取一个数值后,再创建一个新的键盘录入对象获取字符串。 + +```java +public static void method() { + Scanner sc=new Scanner(System.in); + int x=sc.nextInt(); + System.out.println("x:"+x); + + Scanner sc2=new Scanner(System.in); + String line=sc2.nextLine(); + System.out.println("line:"+line); +} +``` + +> 解决方案二:把所有的数据都先按照字符串获取,然后要什么,就进行相应的转换。 + +```java +public static void method2() { + Scanner sc=new Scanner(System.in); + + String xStr=sc.nextLine(); + String line=sc.nextLine(); + + int x=Integer.parseInt(xStr); + + System.out.println("x:"+x); + System.out.println("line:"+line); +} +``` + + + +## Calendar + +Calendar为特定瞬间与一组诸如 YEAR、MONTH、DAY_OF_MONTH、HOUR 等日历字段之间的转换提供了一些方法, +并为操作日历字段(例如获得下星期的日期)提供了一些方法。 + +### 常用成员方法 + +```java +public int get(int field) +//返回给定日历字段的值。日历类中的每个日历字段都是静态的成员变量,并且是int类型。 + +public void add(int field,int amount) +//根据给定的日历字段和对应的时间,来对当前的日历进行操作。 + +public final void set(int year,int month,int date) +//设置当前日历的年月日 +``` + +### 使用示例 + +示例1: + +```java +public class CalendarDemo { + public static void main(String[] args) { + // 其日历字段已由当前日期和时间初始化: + Calendar rightNow = Calendar.getInstance(); // 子类对象 + int year=rightNow.get(Calendar.YEAR); + int month=rightNow.get(Calendar.MONTH);//注意月份是从0开始的 + int date=rightNow.get(Calendar.DATE); + System.out.println(year + "年" + (month + 1) + "月" + date + "日"); + //2018年12月25日 + } +} +``` + +示例2: + +```java +public class CalendarDemo2 { + public static void main(String[] args) { + // 其日历字段已由当前日期和时间初始化: + Calendar calendar = Calendar.getInstance(); // 子类对象 + System.out.println(getYearMonthDay(calendar));//2018年12月25日 + + //三年前的今天 + calendar.add(Calendar.YEAR,-3); + System.out.println(getYearMonthDay(calendar));//2015年12月25日 + + //5年后的10天前 + calendar.add(Calendar.YEAR,5); + calendar.add(Calendar.DATE,-10); + System.out.println(getYearMonthDay(calendar));//2020年12月15日 + + //设置 2011年11月11日 + calendar.set(2011,10,11); + System.out.println(getYearMonthDay(calendar));//2011年11月11日 + } + + //获取年、月、日 + public static String getYearMonthDay(Calendar calendar){ + int year=calendar.get(Calendar.YEAR); + int month=calendar.get(Calendar.MONTH); + int date=calendar.get(Calendar.DATE); + return year + "年" + (month + 1) + "月" + date + "日"; + } +} +``` + +### 练习 + +获取任意一年的二月有多少天 + +```java +/** + *获取任意一年的二月有多少天 + *分析: + * A:键盘录入任意的年份 + * B:设置日历对象的年月日 + * 年就是输入的数据 + * 月是2 + * 日是1 + * C:把时间往前推一天,就是2月的最后一天 + * D:获取这一天输出即可 + */ +public class CalendarTest { + public static void main(String[] args) { + Scanner sc=new Scanner(System.in); + int year=sc.nextInt(); + Calendar c= Calendar.getInstance(); + c.set(year,2,1); //得到的就是该年的3月1日 + c.add(Calendar.DATE,-1);//把时间往前推一天,就是2月的最后一天 + //public void add(int field,int amount):根据给定的日历字段和对应的时间,来对当前的日历进行操作。 + + System.out.println(year+"年,二月有"+c.get(Calendar.DATE)+"天"); + } +} +``` + + + +## Date + +Date: 表示特定的瞬间,精确到毫秒。 + +### 常用成员方法 + +```java +Date() //根据当前的默认毫秒值创建日期对象 + +Date(long date) //根据给定的毫秒值创建日期对象 + +public long getTime() //获取时间,以毫秒为单位 + +public void setTime(long time) //设置时间 +``` + +### 使用示例 + +```java +/** + * 把一个毫秒值转换为Date,有两种方式: + * (1)构造方法 + * (2)setTime(long time) + */ +public class DateDemo { + public static void main(String[] args) { + // Date():根据当前的默认毫秒值创建日期对象 + Date d = new Date(); + System.out.println("d:" + d); + //d:Tue Dec 25 20:01:17 GMT+08:00 2018 --> 当前时间 + + // Date(long date):根据给定的毫秒值创建日期对象 + //long time = System.currentTimeMillis(); + long time = 1000 * 60 * 60; // 1小时 + Date d2 = new Date(time); + System.out.println("d2:" + d2); + //格林威治时间 1970年01月01日00时00分00 + //Thu Jan 01 09:00:00 GMT+08:00 1970 GMT+表示 标准时间加8小时,因为中国是东八区 + + // 获取时间 + long time2 = d.getTime(); + System.out.println(time2); //1545739438466 毫秒 + System.out.println(System.currentTimeMillis()); + + // 设置时间 + d.setTime(1000*60*60); + System.out.println("d:" + d); + //Thu Jan 01 09:00:00 GMT+08:00 1970 + } +} +``` + +### Date 和 String 类型的相互转换 + +```java +public class DateFormatDemo { + public static void main(String[] args) { + Date date=new Date(); + SimpleDateFormat sdf=new SimpleDateFormat("yyyy年MM月dd日"); + String s=dateToString(date,sdf); + System.out.println(s); //2018年12月25日 + System.out.println(stringToDate(s,sdf));//Tue Dec 25 00:00:00 GMT+08:00 2018 + } + + /** + * Date -- String(格式化) + * public final String format(Date date) + */ + public static String dateToString(Date d, SimpleDateFormat sdf) { + return sdf.format(d); + } + + /** + * * String -- Date(解析) + * public Date parse(String source) + */ + public static Date stringToDate(String s, SimpleDateFormat sdf){ + Date date=null; + try { + date=sdf.parse(s); + } catch (ParseException e) { + e.printStackTrace(); + } + return date; + } +} +``` + +## DateFormat + +DateForamt:可以进行日期和字符串的格式化和解析,但是由于是抽象类,所以使用具体子类SimpleDateFormat。 + +### SimpleDateFormat 构造方法 + +```java +SimpleDateFormat() //默认模式 + +SimpleDateFormat(String pattern) //给定的模式 +``` + +这个模式字符串该如何写呢? 通过查看 API,我们就找到了对应的模式: + +| 中文说明 | 模式字符 | +| :------: | :------: | +| 年 | y | +| 月 | M | +| 日 | d | +| 时 | H | +| 分 | m | +| 秒 | s | + +### 练习 + +算一下你来到这个世界多少天? + +```java +小练习:/** + * * + * 算一下你来到这个世界多少天? + * + * 分析: + * A:键盘录入你的出生的年月日 + * B:把该字符串转换为一个日期 + * C:通过该日期得到一个毫秒值 + * D:获取当前时间的毫秒值 + * E:用D-C得到一个毫秒值 + * F:把E的毫秒值转换为年 + * /1000/60/60/24 + */ +public class DateTest { + public static void main(String[] args) throws ParseException { + // 键盘录入你的出生的年月日 + Scanner sc = new Scanner(System.in); + System.out.println("请输入你的出生年月日(格式 yyyy-MM-dd):"); + String line = sc.nextLine(); + + // 把该字符串转换为一个日期 + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + Date d = sdf.parse(line); + long birth=d.getTime(); //出生的时间 + long current=System.currentTimeMillis();//当前时间 + + long days=(current-birth)/1000/60/60/24; + System.out.println("你出生了"+days+"天"); + } +} +``` + diff --git "a/docs/JavaBasics/10\345\205\266\344\273\226.md" "b/docs/JavaBasics/10\345\205\266\344\273\226.md" deleted file mode 100644 index 1d577038..00000000 --- "a/docs/JavaBasics/10\345\205\266\344\273\226.md" +++ /dev/null @@ -1,61 +0,0 @@ - -* [十一、其他](#十一其他) - * [Java 各版本的新特性](#java-各版本的新特性) - * [Java 与 C++ 的区别](#java-与-c-的区别) - * [JRE or JDK](#jre-or-jdk) - * [Java基础学习书籍推荐](#Java基础学习书籍推荐) - -# 十一、其他 - -## Java 各版本的新特性 - -**New highlights in Java SE 8** - -1. Lambda Expressions -2. Pipelines and Streams -3. Date and Time API -4. Default Methods -5. Type Annotations -6. Nashhorn JavaScript Engine -7. Concurrent Accumulators -8. Parallel operations -9. PermGen Error Removed - -**New highlights in Java SE 7** - -1. Strings in Switch Statement -2. Type Inference for Generic Instance Creation -3. Multiple Exception Handling -4. Support for Dynamic Languages -5. Try with Resources -6. Java nio Package -7. Binary Literals, Underscore in literals -8. Diamond Syntax - -- [Difference between Java 1.8 and Java 1.7?](http://www.selfgrowth.com/articles/difference-between-java-18-and-java-17) -- [Java 8 特性](http://www.importnew.com/19345.html) - -## Java 与 C++ 的区别 - -- Java 是纯粹的面向对象语言,所有的对象都继承自 java.lang.Object,C++ 为了兼容 C 即支持面向对象也支持面向过程。 -- Java 通过虚拟机从而实现跨平台特性,但是 C++ 依赖于特定的平台。 -- Java 没有指针,它的引用可以理解为安全指针,而 C++ 具有和 C 一样的指针。 -- Java 支持自动垃圾回收,而 C++ 需要手动回收。 -- Java 不支持多重继承,只能通过实现多个接口来达到相同目的,而 C++ 支持多重继承。 -- Java 不支持操作符重载,虽然可以对两个 String 对象执行加法运算,但是这是语言内置支持的操作,不属于操作符重载,而 C++ 可以。 -- Java 的 goto 是保留字,但是不可用,C++ 可以使用 goto。 -- Java 不支持条件编译,C++ 通过 #ifdef #ifndef 等预处理命令从而实现条件编译。 - -[What are the main differences between Java and C++?](http://cs-fundamentals.com/tech-interview/java/differences-between-java-and-cpp.php) - -## JRE or JDK - -- JRE is the JVM program, Java application need to run on JRE. -- JDK is a superset of JRE, JRE + tools for developing java programs. e.g, it provides the compiler "javac" - -## Java基础学习书籍推荐 -- 《Head First Java.第二版》 - -- 《Java核心技术卷1+卷2》 - -- 《Java编程思想(第4版)》 diff --git "a/docs/JavaBasics/00\346\225\260\346\215\256\347\261\273\345\236\213.md" "b/docs/JavaBasics/1_\346\225\260\346\215\256\347\261\273\345\236\213.md" similarity index 55% rename from "docs/JavaBasics/00\346\225\260\346\215\256\347\261\273\345\236\213.md" rename to "docs/JavaBasics/1_\346\225\260\346\215\256\347\261\273\345\236\213.md" index d3f08cac..13d74dc6 100644 --- "a/docs/JavaBasics/00\346\225\260\346\215\256\347\261\273\345\236\213.md" +++ "b/docs/JavaBasics/1_\346\225\260\346\215\256\347\261\273\345\236\213.md" @@ -1,18 +1,20 @@ -# 一、数据类型 +# 数据类型 ## 包装类型 -八个基本类型: +Java 八个基本类型,基本类型都有对应的包装类型: -- boolean/1 -- byte/8 -- char/16 -- short/16 -- int/32 -- float/32 -- long/64 -- double/64 +| 基本类型 | 大小(bit) | 最小值 | 最大值 | 默认值 | 包装类 | +| :------: | :---------: | :-------: | :------------: | :---------------: | :-------: | +| boolean | 1 | - | - | false | Boolean | +| char | 16 | Unicode 0 | Unicode 2^16-1 | \u0000(Unicode 0) | Character | +| byte | 8 | -128 | 127 | (byte)0 | Byte | +| short | 16 | -2^15 | 2^15-1 | (short)0 | Short | +| int | 32 | -2^31 | 2^31-1 | 0 | Int | +| long | 64 | -2^63 | 2^63-1 | 0L | Long | +| float | 32 | - | - | 0.0F | Float | +| double | 64 | - | - | 0.0D | Double | -基本类型都有对应的包装类型,基本类型与其对应的包装类型之间的赋值使用**自动装箱**与**自动拆箱**完成。 + 基本类型与其对应的包装类型之间的赋值使用**自动装箱**与**自动拆箱**完成。 ```java //自动装箱和自动拆箱 @@ -26,6 +28,14 @@ x=Integer.valueOf(x.intValue()+5);//x.intValue()就是拆箱-->先拆箱,再 ## 缓存池 +Java 基本类型的包装类的大部分都实现了缓存池技术,对应的缓冲池如下: + +- Boolean 直接返回 true / false +- Byte/ Short / Integer / Long 创建了数值范围在 [-128,127] 的相应类型的缓存数据 +- Character 创建了数值在 [0,127] 的缓存数据 + +如果超出对应范围仍然会去创建新的对象。 + new Integer(123) 与 Integer.valueOf(123) 的区别在于: - new Integer(123) 每次都会新建一个对象; @@ -92,12 +102,12 @@ Integer n = 123; System.out.println(m == n); // true ``` -基本类型对应的缓冲池如下: +\+ 不适用于 Integer 对象,首先 i,j 进行自动拆箱,然后数值相加,得到数值 80,进行自动装箱后,和 m 引用相同的对象。Integer 对象无法与数值直接进行比较,自动拆箱后转为数值 80,显然 80 == 80,输出 true。 -- boolean values true and false -- all byte values -- short values between -128 and 127 -- int values between -128 and 127 -- char in the range \u0000 to \u007F - -在使用这些基本类型对应的包装类型时,就可以**直接使用缓冲池中的对象**。 +```java +Integer i = 40; +Integer j = 40; +Integer m = 80; +System.out.println(m == i+j); // true +System.out.println(80 == i+j); // true +``` diff --git "a/docs/JavaBasics/02\350\277\220\347\256\227.md" "b/docs/JavaBasics/3_\350\277\220\347\256\227.md" similarity index 77% rename from "docs/JavaBasics/02\350\277\220\347\256\227.md" rename to "docs/JavaBasics/3_\350\277\220\347\256\227.md" index 2552210b..35b2fa42 100644 --- "a/docs/JavaBasics/02\350\277\220\347\256\227.md" +++ "b/docs/JavaBasics/3_\350\277\220\347\256\227.md" @@ -1,12 +1,43 @@ - -* [三、运算](#三运算) - * [参数传递](#参数传递) - * [==和equals()](#==和equals) - * [float 与 double](#float-与-double) - * [隐式类型转换](#隐式类型转换) - * [switch](#switch) - -# 三、运算 +# 基本运算 + +## == 和 equals() + +- == 判断两个对象的地址是不是相等。即判断两个对象是不是同一个对象。 + - 基本数据类型:== 比较的是值 + - 引用数据类型:== 比较的是内存地址 + +- equals() 判断两个对象是否相等。但它一般有两种使用情况: + - 情况1:类没有重写 equals() 方法。等价于“==”。 + - 情况2:类重写了 equals() 方法。一般用来**比较两个对象的内容**,若它们的内容相等,则返回 true (即,认为这两个对象相等)。 + +注意: + +- String 中的 **equals 方法是被重写过**的,因为 object 的 equals 方法是比较的对象的内存地址, 而 String 的 equals 方法比较的是对象的值。 + +- 当创建 String 类型的对象时,虚拟机会在**常量池**中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。 + 如果没有就在常量池中重新创建一个 String 对象。 + +```java +public class EqualsDemo { + public static void main(String[] args) { + String a = new String("ab"); // a 为一个引用 + String b = new String("ab"); // b为另一个引用,对象的内容一样 + String aa = "ab"; // 放在常量池中 + String bb = "ab"; // 从常量池中查找 + if (aa == bb) // true + System.out.println("aa==bb"); + if (a == b) // false,非同一对象 + System.out.println("a==b"); + if (a.equals(b)) // true + System.out.println("aEQb"); + if (42 == 42.0) { // true + System.out.println("true"); + } + } +} +``` + + ## 参数传递 @@ -14,8 +45,7 @@ Java 的参数是以**值传递**的形式传入方法中,而不是引用传 以下代码中 Dog dog 的 dog 是一个指针,存储的是对象的地址。 在将一个参数传入一个方法时,本质上是**将对象的地址以值的方式传递到形参中**。 -因此在方法中使指针引用其它对象,那么这两个指针此时指向的是完全不同的对象, -在一方改变其所指向对象的内容时对另一方没有影响。 +因此在方法中使指针引用其它对象,那么这两个指针此时指向的是完全不同的对象,在一方改变其所指向对象的内容时对另一方没有影响。 ```java public class Dog { @@ -75,46 +105,7 @@ class PassByValueExample { } ``` -## ==和equals() -- == 判断两个对象的地址是不是相等。即判断两个对象是不是同一个对象。 - -基本数据类型"=="比较的是值 - -引用数据类型"=="比较的是内存地址 - -- equals()判断两个对象是否相等。但它一般有两种使用情况: - -情况1:类没有重写 equals() 方法。等价于“==”。 - -情况2:类重写了 equals() 方法。一般用来**比较两个对象的内容**; -若它们的内容相等,则返回 true (即,认为这两个对象相等)。 - -```java -public class EqualsDemo { - public static void main(String[] args) { - String a = new String("ab"); // a 为一个引用 - String b = new String("ab"); // b为另一个引用,对象的内容一样 - String aa = "ab"; // 放在常量池中 - String bb = "ab"; // 从常量池中查找 - if (aa == bb) // true - System.out.println("aa==bb"); - if (a == b) // false,非同一对象 - System.out.println("a==b"); - if (a.equals(b)) // true - System.out.println("aEQb"); - if (42 == 42.0) { // true - System.out.println("true"); - } - } -} -``` -> **说明** - -- String 中的 **equals 方法是被重写过**的,因为 object 的 equals 方法是比较的对象的内存地址, -而 String 的 equals 方法比较的是对象的值。 -- 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。 -如果没有就在常量池中重新创建一个 String 对象。 ## float 与 double @@ -132,6 +123,8 @@ Java 不能隐式执行向下转型,因为这会使得精度降低。 float f = 1.1f; ``` + + ## 隐式类型转换 因为字面量 1 是 int 类型,它比 short 类型精度要高,因此不能隐式地将 int 类型下转型为 short 类型。 @@ -154,6 +147,8 @@ s1 += 1; s1 = (short) (s1 + 1); ``` + + ## switch 从 Java 7 开始,可以在 switch 条件判断语句中使用 String 对象。 diff --git "a/docs/JavaBasics/03Object\351\200\232\347\224\250\346\226\271\346\263\225.md" "b/docs/JavaBasics/4_Object\351\200\232\347\224\250\346\226\271\346\263\225.md" similarity index 99% rename from "docs/JavaBasics/03Object\351\200\232\347\224\250\346\226\271\346\263\225.md" rename to "docs/JavaBasics/4_Object\351\200\232\347\224\250\346\226\271\346\263\225.md" index 6a2599ac..08d0ac74 100644 --- "a/docs/JavaBasics/03Object\351\200\232\347\224\250\346\226\271\346\263\225.md" +++ "b/docs/JavaBasics/4_Object\351\200\232\347\224\250\346\226\271\346\263\225.md" @@ -1,4 +1,4 @@ -# 四、Object 通用方法 +# Object 通用方法 Object 类中的方法一览: @@ -27,7 +27,7 @@ public final void wait(long timeout, int nanos) throws InterruptedException public final void wait() throws InterruptedException ``` -## equals() +## *equals() **1. 等价关系** @@ -117,7 +117,7 @@ public class EqualExample { } ``` -## hashCode() +## *hashCode() hashCode() 返回散列值,而 equals() 是用来判断两个对象是否等价。 **等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价**。 @@ -382,4 +382,5 @@ CloneConstructorExample e2 = new CloneConstructorExample(e1); e1.set(2, 222); System.out.println(e1.get(2)); // 222 System.out.println(e2.get(2)); // 2 -``` \ No newline at end of file +``` + diff --git "a/docs/JavaBasics/04\345\205\263\351\224\256\345\255\227.md" "b/docs/JavaBasics/5_\345\205\263\351\224\256\345\255\227.md" similarity index 98% rename from "docs/JavaBasics/04\345\205\263\351\224\256\345\255\227.md" rename to "docs/JavaBasics/5_\345\205\263\351\224\256\345\255\227.md" index 613bc416..627747c6 100644 --- "a/docs/JavaBasics/04\345\205\263\351\224\256\345\255\227.md" +++ "b/docs/JavaBasics/5_\345\205\263\351\224\256\345\255\227.md" @@ -1,6 +1,6 @@ -# 五、关键字 +# final 和 static 关键字 -## final +## final 关键字 **1. 数据** @@ -26,7 +26,7 @@ private 方法隐式地被指定为 final,如果在子类中定义的方法和 **声明类不允许被继承**。 -## static +## static 关键字 **1. 静态变量** diff --git "a/docs/JavaBasics/6_\345\217\215\345\260\204.md" "b/docs/JavaBasics/6_\345\217\215\345\260\204.md" new file mode 100644 index 00000000..ba3e78a5 --- /dev/null +++ "b/docs/JavaBasics/6_\345\217\215\345\260\204.md" @@ -0,0 +1,287 @@ +# 异常-Exception + +## 异常的概念 + +Java 异常是一个描述在代码段中**发生异常的对象**,当发生异常情况时,一个代表该异常的对象被创建并且在导致该异常的方法中被抛出,而该方法可以选择自己处理异常或者传递该异常。 + +## 异常继承体系 + +
+ +Throwable 可以用来表示任何可以作为异常抛出的类,分为两种: **Error** 和 **Exception**。 + +- Error:通常是灾难性的致命的错误,是**程序无法控制和处理**的,当出现这些错误时,建议终止程序; +- Exception:通常情况下是可以被程序处理的,捕获后可能恢复,并且在程序中应该**尽可能地去处理**这些异常。 + +Java 异常分为两种: + +- 受检异常:**除了 RuntimeException 及其子类以外,其他的 Exception 类及其子类都属于这种异常**。 +- 非受检异常:包括 RuntimeException 及其子类和 Error。 + +
+ +注意:非受检查异常为编译器不要求强制处理的异常,受检异常则是编译器要求必须处置的异常。 + +Exception 这类异常分为**运行时异常**和**非运行时异常(编译异常)**: + +- 运行时异常 :包括 RuntimeException 及其子类。比如 NullPointerException、IndexOutOfBoundsException。属于非受检异常,可以进行捕捉处理,也可以不处理。 +- 非运行时异常(编译异常):RuntimeExcaption 以外的 Exception。IOException、SQLException 已经自定义的异常,必须要进行处理。 + + + +## Java 异常的处理机制 + +Java 异常处理机制本质上就是**抛出异常**和**捕捉异常**。 + +**抛出异常** + +i.普通问题:指在当前环境下能得到足够的信息,总能处理这个错误。 + +ii.异常情形:是指**阻止当前方法或作用域继续执行的问题**。对于异常情形,已经程序无法执行继续下去了, +因为在当前环境下无法获得必要的信息来解决问题,我们所能做的就是从当前环境中跳出,并把问题提交给上一级环境, +这就是抛出异常时所发生的事情。 + +iii.抛出异常后,会有几件事随之发生: +​ +第一:像创建普通的java对象一样将使用new在堆上创建一个异常对象 + + 第二:当前的执行路径(已经无法继续下去了)被终止,并且从当前环境中弹出对异常对象的引用。 +​ +此时,**异常处理机制接管程序,并开始寻找一个恰当的地方继续执行程序**, +这个恰当的地方就是异常处理程序或者异常处理器, +它的任务是**将程序从错误状态中恢复**,以使程序要么换一种方式运行,要么继续运行下去。 +​ +**捕捉异常** + +在方法抛出异常之后,运行时系统将转为寻找合适的**异常处理器**(exception handler)。 +潜在的异常处理器是异常发生时依次存留在**调用栈**中的方法的集合。 +当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适的异常处理器。 +运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。 +当运行时系统遍历调用栈而未找到合适的异常处理器,则运行时系统终止。同时,意味着Java程序的终止。 + +注意: + +对于运行时异常、错误和受检异常,Java技术所要求的异常处理方式有所不同。 + +(1)由于**运行时异常及其子类**的不可查性,为了更合理、更容易地实现应用程序, +Java规定,运行时异常将由Java运行时系统自动抛出,**允许应用程序忽略运行时异常**。 + +(2)对于方法运行中可能出现的Error,当运行方法不欲捕捉时,Java允许该方法不做任何抛出声明。 +因为,大多数Error异常属于永远不能被允许发生的状况,也属于合理的应用程序不该捕捉的异常。 + +(3)对于所有的受检异常, +Java规定:一个方法必须捕捉,或者声明抛出方法之外。 +也就是说,当一个方法选择不捕捉受检异常时,它必须声明将抛出异常。 + +## Java 异常的处理原则 + +- **具体明确**:抛出的异常应能通过异常类名和message准确说明异常的类型和产生异常的原因; +- **提早抛出**:应尽可能早地发现并抛出异常,便于精确定位问题; +- **延迟捕获**:异常的捕获和处理应尽可能延迟,让掌握更多信息的作用域来处理异常 + +## Java 常见异常以及错误 + +| 类型 | 说明 | +| :--------------------------------------: | :----------------------------------------------------------: | +| **RuntimeException 子类** | | +| java.lang.ArrayIndexOutOfBoundsException | 数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出 | +| java.lang.ArithmeticException | 算术条件异常。譬如:整数除零等 | +| java.lang.NullPointerException | 空指针异常。当应用试图在要求使用对象的地方使用了null时,抛出该异常。譬如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等 | +| java.lang.ClassNotFoundException | 找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常 | +| java.lang.SecurityException | 安全性异常 | +| java.lang.IllegalArgumentException | 非法参数异常 | +| **IOException** | | +| IOException | 操作输入流和输出流时可能出现的异常 | +| EOFException | 文件已结束异常 | +| FileNotFoundException | 文件未找到异常 | +| **其他** | | +| ClassCastException | 类型转换异常类 | +| ArrayStoreException | 数组中包含不兼容的值抛出的异常 | +| SQLException | 操作数据库异常类 | +| NoSuchFieldException | 字段未找到异常 | +| NumberFormatException | 字符串转换为数字抛出的异常 | +| StringIndexOutOfBoundsException | 字符串索引超出范围抛出的异常 | +| IllegalAccessException | 不允许访问某类异常 | +| **Error** | | +| NoClassDefFoundError | 找不到class定义的错误 | +| StackOverflowError | 深递归导致栈被耗尽而抛出的错误 | +| OutOfMemoryError | 内存溢出错误 | + +## try-catch-finally语句块的执行 + +

+ +(1) try 块:用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。 + +(2) catch 块:用于处理try捕获到的异常。 + +(3) finally 块:无论是否捕获或处理异常,finally 块里的语句都会被执行。 +当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在**方法返回之前**被执行。 + +在以下 4 种特殊情况下,finally 语句块不会被执行: + +- 在finally语句块中发生了异常 +- 在前面的代码中用了System.exit()退出程序。 +- 程序所在的线程死亡。 +- 关闭 CPU。 + +## try catch代码块的性能如何 + +- 会影响 JVM 的重排序优化; +- 异常对象实例需要保存栈快照等信息,开销比较大。 + +## try-with-resources + +适用范围:任何实现 java.lang.AutoCloseable 或者 java.io.Closeable 的对象。 + + 在 try-with-resources 语句中,任何 catch 或 finally 代码块在声明的资源关闭后运行。 + +使用 try-catch-finally 关闭资源: + +```java +//读取文本文件的内容 +Scanner scanner = null; +try { + scanner = new Scanner(new File("D://read.txt")); + while (scanner.hasNext()) { + System.out.println(scanner.nextLine()); + } +} catch (FileNotFoundException e) { + e.printStackTrace(); +} finally { + if (scanner != null) { + scanner.close(); + } +} +``` + +使用 try-with-resources 语句改造上面的代码: + +```java +try (Scanner scanner = new Scanner(new File("test.txt"))) { + while (scanner.hasNext()) { + System.out.println(scanner.nextLine()); + } +} catch (FileNotFoundException e) { + e.printStackTrace(); +} +``` + +使用 try-with-resources 语句关闭多个资源: + +```java +try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt"))); // 这里采用 ";" 进行分割 + BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) { + int b; + while ((b = bin.read()) != -1) { + bout.write(b); + } +} +catch (IOException e) { + e.printStackTrace(); +} +``` + + + +## final & finally & finalize + +final:最终的意思,可以修饰类,修饰成员变量,修饰成员方法 + +修饰类:类不能被继承 + +修饰变量:变量是常量 + +修饰方法:方法不能被重写(Override) + +(2)finally:是异常处理的关键字,用于释放资源。一般来说,代码必须执行(特殊情况:在执行到finally JVM就退出了) + +(3)finalize:是Object的一个方法,用于垃圾回收。 + +## 看程序写结果 + +```java +public class TestException { + public static void main(String[] args) { + System.out.println(getInt()); + } + + public static int getInt(){ + int a=10; + try{ + System.out.println(a/0); + }catch (ArithmeticException e){ + a=30; + return a; + }finally { + a=40; + } + System.out.println("a="+a); + return a; + } +} +``` + +结果为: + +```html +30 +``` + + + +# 反射-Reflection + +## 反射概念 + +Java 的反射机制是指在运行状态中: + +- 对任意一个类,都能够知道这个类的所有属性和方法 +- 对任意一个对象,都能调用这个对象的所有属性和方法 + +## 反射的作用 + +- 检查类的属性和方法 +- 在运行时检查对象的类型 +- 动态构造某个类的对象 +- 可以任意调用对象的方法 + +## 反射的使用 + +### 1. java.lang.reflect 包中的三个类 + +- Field:成员变量 +- Method:成员方法 +- Constructor:构造方法 + +### 2. 对 public 域的方法 + +包括父类成员。 + +- getField +- getMethod +- getConstructor + +### 3. 对其他域的方法 + +包括 private 和 protected 的成员,但不包括父类成员。 + +- getDeclaredField +- getDeclaredMethod +- getDeclaredConstructor + +利用反射访问私有属性:使用 setAccessible(true) + + + +## 反射的不足 + +性能是一个问题。反射相当于一系列解释操作,通知 Java 虚拟机要做什么,性能比直接的 Java 要慢很多。 + + + +# 补充 + +- [Java提高篇——Java异常处理](https://www.cnblogs.com/Qian123/p/5715402.html) + +- [深入解析Java反射(1) - 基础](https://www.sczyh30.com/posts/Java/java-reflection-1/) \ No newline at end of file diff --git "a/docs/JavaBasics/07\346\263\233\345\236\213.md" "b/docs/JavaBasics/8_\346\263\233\345\236\213.md" similarity index 65% rename from "docs/JavaBasics/07\346\263\233\345\236\213.md" rename to "docs/JavaBasics/8_\346\263\233\345\236\213.md" index 9e10b08f..4c2e8e02 100644 --- "a/docs/JavaBasics/07\346\263\233\345\236\213.md" +++ "b/docs/JavaBasics/8_\346\263\233\345\236\213.md" @@ -1,8 +1,7 @@ -# 八、泛型 +# 泛型-Generics ## 泛型的概念 在集合中存储对象并在**使用前进行类型转换**是多么的不方便。泛型防止了那种情况的发生。 -它提供了**编译期**的类型安全,确保你只能把正确类型的对象放入集合中, -避免了在运行时出现ClassCastException。 +它提供了**编译期**的类型安全,确保你只能把正确类型的对象放入集合中,避免了在运行时出现ClassCastException。 - 不使用泛型 ```java @@ -243,10 +242,11 @@ public static void copy(List dest, List src) { ``` ## 类型擦除 ### 类型擦除简介 + +Java 中的泛型是伪泛型。 + 类型擦除就是Java泛型只能用于在**编译期间的静态类型检查**,然后**编译器生成的代码会擦除相应的类型信息**, -这样到了**运行期间**实际上JVM根本不知道泛型所代表的具体类型。 -这样做的目的是因为Java泛型是1.5之后才被引入的,为了保持**向下兼容**, -所以只能做类型擦除来兼容以前的非泛型代码。 +这样到了**运行期间**实际上JVM根本不知道泛型所代表的具体类型。这样做的目的是因为Java泛型是1.5之后才被引入的,为了保持**向下兼容**。 对于这一点,如果阅读 Java 集合框架的源码,可以发现有些类其实并不支持泛型。 ```java @@ -304,7 +304,7 @@ public class Node { ### 类型擦除带来的问题 -- 1、在 Java 中不允许创建**泛型数组** +- 在 Java 中不允许创建**泛型数组** ```java public class Problem1 { public static void main(String[] args) { @@ -341,7 +341,7 @@ public class Problem1 { } ``` -- 2、对于泛型代码,Java 编译器实际上还会偷偷帮我们实现一个 **Bridge Method**。 +- 对于泛型代码,Java 编译器实际上还会偷偷帮我们实现一个 **Bridge Method**。 ```java public class Node { public T data; @@ -400,8 +400,7 @@ public class MyNode extends Node { } ``` -- Java 泛型很大程度上只能提供静态类型检查,然后类型的信息就会被擦除, -所以利用类型参数创建实例的做法编译器不会通过。 +- Java 泛型很大程度上只能提供静态类型检查,然后类型的信息就会被擦除,所以**利用类型参数创建实例的做法编译器不会通过**。 ```java public static void append(List list) { @@ -419,9 +418,9 @@ public static void append(List list, Class cls) throws Exception { ``` -- 无法对泛型代码直接使用 **instanceof 关键字**, -因为Java编译器在生成代码的时候会擦除所有相关泛型的类型信息。 -JVM在运行时期无法识别出ArrayList和ArrayList的之间的区别: +- 无法对泛型代码直接使用 **instanceof 关键字**,因为 Java 编译器在生成代码的时候会擦除所有相关泛型的类型信息。 + + JVM 在运行时期无法识别出ArrayList和ArrayList的之间的区别: ```java public static void rtti(List list) { @@ -652,13 +651,271 @@ public class FIFOCache { ``` ### 泛型的实现方式 -Java的泛型是一种**伪泛型,编译为字节码时参数类型会在代码中被擦除**, -单独记录在Class文件的attributes域内,而在使用泛型处做类型检查与类型转换。 +Java 的泛型是一种**伪泛型,编译为字节码时参数类型会在代码中被擦除**,单独记录在 Class 文件的 attributes 域内,而在使用泛型处做类型检查与类型转换。 假设参数类型的占位符为T,擦除规则(保留上界)如下: -(1)擦除后变为Object +(1) \ 擦除后变为 Object + +(2) 擦除后变为 A + +(3) <? super A> 擦除后变为 Object + +# 注解-Annontation + +## 注解概述 + +Annontation 是 Java5 开始引入的新特征,中文名称叫注解。 +注解是插入到代码中的一种**注释**或者说是一种**元数据**。 + +这些注解信息可以在编译期使用编译工具进行处理,也可以在运行期使用 Java 反射机制进行处理。 + +## 注解的用处 + +- 生成文档。这是最常见的,也是java 最早提供的注解。常用的有@param @return 等 +- 跟踪代码依赖性,实现替代**配置文件**功能。如Spring中@Autowired; +- 在编译时进行格式检查。如@override 放在方法前,如果你这个方法并不是覆盖了超类方法,则编译时就能检查出。 + +## 注解的原理 + +注解**本质是一个继承了Annotation的特殊接口**,其具体实现类是Java运行时生成的**动态代理类**。 +我们通过反射获取注解时,返回的是Java运行时生成的**动态代理对象**$Proxy1。 +通过代理对象调用自定义注解(接口)的方法,会最终调用AnnotationInvocationHandler的invoke方法。 +该方法会从memberValues这个Map中索引出对应的值。 +而memberValues的来源是Java常量池。 + +### 元注解 + +java.lang.annotation提供了四种元注解,专门注解其他的注解(在自定义注解的时候,需要使用到元注解): + +| 注解 | 说明 | +| :---------: | :-----------------------: | +| @Documented | 是否将注解包含在JavaDoc中 | +| @Retention | 什么时候使用该注解 | +| @Target | 注解用于什么地方 | +| @Inherited | 是否允许子类继承该注解 | + +- @Documented + +一个简单的Annotations标记注解,表示是否将注解信息添加在java文档中。 + +- @Retention + +定义该注解的生命周期。 + +(1)RetentionPolicy.SOURCE : 在编译阶段丢弃。 + 这些注解在编译结束之后就不再有任何意义,所以它们不会写入字节码。 + @Override, @SuppressWarnings都属于这类注解。 + +(2)RetentionPolicy.CLASS : 在类加载的时候丢弃。 +在字节码文件的处理中有用。注解默认使用这种方式 + +(3)RetentionPolicy.RUNTIME : 始终不会丢弃,运行期也保留该注解, +因此**可以使用反射机制读取该注解的信息**。我们自定义的注解通常使用这种方式。 + +- @Target + +表示该注解用于什么地方。 +默认值为任何元素,表示该注解用于什么地方。可用的ElementType参数包括: + +| 参数 | 说明 | +| :--------------------------------------------------------: | :----------------------------------: | +| ElementType.CONSTRUCTOR | 用于描述构造器 | +| ElementType.FIELD | 成员变量、对象、属性(包括enum实例) | +| ElementType.LOCAL_VARIABLE | 用于描述局部变量 | +| ElementType.METHOD | 用于描述方法 | +| ElementType.PACKAGE | 用于描述包 | +| ElementType.PARAMETER | 用于描述参数 | +| ElementType.TYPE:用于描述类、接口(包括注解类型) 或enum声明 | | + +- @Inherited + +定义该注释和子类的关系。 +@Inherited 元注解是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的。 +如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。 + +### 常见标准的Annotation + +- Override (RetentionPolicy.SOURCE : 在编译阶段丢弃。属于@Retention) + + Override是一个标记类型注解,它被用作标注方法。 + 它说明了被标注的方法重载了父类的方法,起到了断言的作用。 + 如果我们使用了这种注解在一个没有覆盖父类方法的方法时,**java编译器将以一个编译错误来警示**。 + +- Deprecated + +Deprecated也是一种标记类型注解。 +当一个类型或者类型成员使用@Deprecated修饰的话,编译器将不鼓励使用这个被标注的程序元素。 +所以使用这种修饰具有一定的“延续性”: + 如果我们在代码中通过继承或者覆盖的方式使用了这个过时的类型或者成员, + 虽然继承或者覆盖后的类型或者成员并不是被声明为@Deprecated,但编译器仍然要报警。 + +- SuppressWarnings + +SuppressWarning不是一个标记类型注解。 +它有一个类型为String[]的成员,这个成员的值为**被禁止的警告名**。 +对于javac编译器来讲,对-Xlint选项有效的警告名也同样对@SuppressWarings有效,同时编译器忽略掉无法识别的警告名。 +@SuppressWarnings("unchecked") + +### 自定义注解使用的规则 + +自定义注解类编写的一些规则: +(1) Annotation型定义为@interface, +所有的Annotation会自动继承java.lang.Annotation这一接口,并且不能再去继承别的类或是接口 + +(2)参数成员只能用public或默认(default)这两个访问权修饰 + +(3)参数成员只能用基本类型byte,short,char,int,long,float,double,boolean八种基本数据类型 +和String、Enum、Class、Annotations等数据类型,以及这一些类型的数组 + +(4)要获取类方法和字段的注解信息,必须通过Java的反射技术来获取Annotation对象, +因为除此之外没有别的获取注解对象的方法 + +(5)注解也可以没有定义成员, 不过这样注解就没啥用了 + +注意:**自定义注解需要使用到元注解** + +> 自定义注解示例 + +自定义水果颜色注解 + +```java +/** + * 水果颜色注解 + */ +@Target(FIELD) +@Retention(RUNTIME) +@Documented +@interface FruitColor { + /** + * 颜色枚举 + */ + public enum Color{绿色,红色,青色}; + + /** + * 颜色属性 (注意:这里的属性指的就是方法) + */ + Color fruitColor() default Color.绿色;//默认是是绿色的 +} +``` + +自定义水果名称注解 + +```java +/** + * 水果名称注解 + */ +@Target(FIELD) //ElementType.FIELD:成员变量、对象、属性(包括enum实例) +@Retention(RUNTIME)// 始终不会丢弃,运行期也保留该注解,因此可以使用反射机制读取该注解的信息。 +@Documented // Deprecated也是一种标记类型注解。 +public @interface FruitName { + public String fruitName() default ""; +} +``` + +水果供应商注解 + +```java +/** + * 水果供应者注解 + */ +@Target(FIELD) +@Retention(RUNTIME) +@Documented +public @interface FruitProvider { + /** + * 供应者编号 + */ + public int id() default -1; + + /** + * 供应商名称 + */ + public String name() default ""; + + /** + * 供应商地址 + */ + public String address() default ""; +} +``` + +通过反射来获取水果信息 + +```java +/** + * 通过反射获取水果信息 + */ +public class FruitInfoUtil { + public static void getFruitInfo(Class clazz){ + String strFruitName=" 水果名称:"; + String strFruitColor=" 水果颜色:"; + String strFruitProvider="供应商信息:"; + + //获取属性值 + Field[] fields=clazz.getDeclaredFields(); + for(Field field:fields){ + if(field.isAnnotationPresent(FruitName.class)){ + //判断注解是不是 FruitName + FruitName fruitName=field.getAnnotation(FruitName.class); + strFruitName=strFruitName+fruitName.fruitName(); + System.out.println(strFruitName); + }else if(field.isAnnotationPresent(FruitColor.class)){ + FruitColor fruitColor=field.getAnnotation(FruitColor.class); + strFruitColor=strFruitColor+fruitColor.fruitColor().toString(); + System.out.println(strFruitColor); + }else if(field.isAnnotationPresent(FruitProvider.class)){ + FruitProvider fruitProvider=field.getAnnotation(FruitProvider.class); + strFruitProvider=strFruitProvider + + "[ 供应商编号:"+fruitProvider.id() + +" 供应商名称:" +fruitProvider.name() + +" 供应商地址:"+fruitProvider.address()+"]"; + System.out.println(strFruitProvider); + } + } + } +} +``` + +使用注解初始化实例类 -(2)擦除后变为A +```java +/** + * 定义一个实例类 + * 这里使用注解来初始化 + */ +public class Apple { + @FruitName(fruitName = "苹果") + private String appleName; -(3)<? super A>擦除后变为Object \ No newline at end of file + @FruitColor(fruitColor = FruitColor.Color.红色) + private String appleColor; + + @FruitProvider(id=1,name="红富士",address="陕西省西安市延安路89号红富士大厦") + private String appleProvider; + + public String getAppleName() { + return appleName; + } + + public void setAppleName(String appleName) { + this.appleName = appleName; + } + + public String getAppleColor() { + return appleColor; + } + + public void setAppleColor(String appleColor) { + this.appleColor = appleColor; + } + + public String getAppleProvider() { + return appleProvider; + } + + public void setAppleProvider(String appleProvider) { + this.appleProvider = appleProvider; + } +} +``` \ No newline at end of file diff --git "a/docs/JavaBasics/JDK8\346\226\260\347\211\271\346\200\247.md" "b/docs/JavaBasics/JDK8\346\226\260\347\211\271\346\200\247.md" new file mode 100644 index 00000000..f774bf07 --- /dev/null +++ "b/docs/JavaBasics/JDK8\346\226\260\347\211\271\346\200\247.md" @@ -0,0 +1,3 @@ +# JDK 8 新特性 + +待补充。 \ No newline at end of file diff --git "a/docs/JavaBasics/\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217.md" "b/docs/JavaBasics/\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217.md" new file mode 100644 index 00000000..7217feb9 --- /dev/null +++ "b/docs/JavaBasics/\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217.md" @@ -0,0 +1,173 @@ +# 正则表达式 + +## 概述 + +是指一个用来描述或者匹配一系列符合某个句法规则的字符串的单个字符串。 + +其实就是**一种规则**。 + +## 组成规则 + +规则字符在java.util.regex Pattern类中:[Pattern API](https://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html) + +## 常见组成规则 + +### 1. 字符 + +| 元字符 | 说明 | +| :----: | :-----------------------: | +| x | 字符 x | +| \ | 反斜线字符 | +| \n | 新行(换行)符 ('\u000A') | +| \r | 回车符 ('\u000D') | + +### 2. 字符类 + +| 元字符 | 说明 | +| :------: | :----------------------------------------: | +| [abc] | a、b 或 c(简单类) | +| [^abc] | 任何字符,除了 a、b 或 c(否定) | +| [a-zA-Z] | a到 z 或 A到 Z,两头的字母包括在内(范围) | +| [0-9] | 0到9的字符都包括 | + +### 3. 预定义字符类 + +| 元字符 | 说明 | +| :----: | :--------------------------: | +| . | 任何字符 | +| \d | 数字。等价于[0-9] | +| \w | 单词字符。等待雨[a-zA-Z_0-9] | + +### 4. 边界匹配器 + +| 元字符 | 说明 | +| :----: | :------: | +| ^ | 行的开头 | +| $ | 行的结尾 | +| \b | 单词边界 | + +### 5. 数量词 + +| 元字符 | 说明 | +| :----: | :---------------------------: | +| X? | X,零次或一次 | +| X* | X,零次或多次 | +| X+ | X,一次或多次 | +| X{n} | X,恰好 n 次 | +| X{n,} | X,至少 n 次 | +| X{n,m} | X,至少 n 次,但是不超过 m 次 | + +## 应用 + +### 判断功能 + +String 类 matches 方法 + +```java +public boolean matches(String regex) +``` + +```java +/** +* 判断手机号码是否满足要求 +*/ +public static void main(String[] args) { + //键盘录入手机号码 + Scanner sc = new Scanner(System.in); + System.out.println("请输入你的手机号码:"); + String phone = sc.nextLine(); + + //定义手机号码的规则 + String regex = "1[38]\\d{9}"; + + //调用功能,判断即可 + boolean flag = phone.matches(regex); + + //输出结果 + System.out.println("flag:"+flag); +} +``` + + + +### 分割功能 + +String类的 split 方法 + +```java +public String[] split(String regex) +``` + +```java +/** +* 根据给定正则表达式的匹配拆分此字符串 +*/ +public static void main(String[] args) { + //定义一个年龄搜索范围 + String ages = "18-24"; + + //定义规则 + String regex = "-"; + + //调用方法 + String[] strArray = ages.split(regex); + int startAge = Integer.parseInt(strArray[0]); + int endAge = Integer.parseInt(strArray[1]); +} +``` + + + +### 替换功能 + +String 类的 replaceAll 方法 + +```java +public String replaceAll(String regex,String replacement) +``` + +```java +/** +* 去除所有的数字 +*/ +public static void main(String[] args) { + // 定义一个字符串 + String s = "helloqq12345worldkh622112345678java"; + + + // 直接把数字干掉 + String regex = "\\d+"; + String ss = ""; + + String result = s.replaceAll(regex, ss); + System.out.println(result); +} +``` + + + +### 获取功能 + +Pattern和Matcher类的使用 + +```java +/** +* 模式和匹配器的基本顺序 +*/ +public static void main(String[] args) { + // 模式和匹配器的典型调用顺序 + // 把正则表达式编译成模式对象 + Pattern p = Pattern.compile("a*b"); + // 通过模式对象得到匹配器对象,这个时候需要的是被匹配的字符串 + Matcher m = p.matcher("aaaaab"); + // 调用匹配器对象的功能 + boolean b = m.matches(); + System.out.println(b); + + //这个是判断功能,但是如果做判断,这样做就有点麻烦了,我们直接用字符串的方法做 + String s = "aaaaab"; + String regex = "a*b"; + boolean bb = s.matches(regex); + System.out.println(bb); +} +``` \ No newline at end of file diff --git "a/docs/JavaContainer/00Java\345\256\271\345\231\250\346\246\202\350\247\210.md" "b/docs/JavaContainer/1_\345\256\271\345\231\250\346\246\202\350\247\210.md" similarity index 84% rename from "docs/JavaContainer/00Java\345\256\271\345\231\250\346\246\202\350\247\210.md" rename to "docs/JavaContainer/1_\345\256\271\345\231\250\346\246\202\350\247\210.md" index 8f611b9f..26515fb0 100644 --- "a/docs/JavaContainer/00Java\345\256\271\345\231\250\346\246\202\350\247\210.md" +++ "b/docs/JavaContainer/1_\345\256\271\345\231\250\346\246\202\350\247\210.md" @@ -1,10 +1,12 @@ -# 一、Java容器概览 +# Java容器概览 容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。 ## Collection -
+Collection 集合体系图: + +
### 1. Set @@ -33,7 +35,9 @@ ## Map -
+Map 集合体系图: + +
- TreeMap:基于红黑树实现。 diff --git "a/docs/JavaContainer/01\345\256\271\345\231\250\344\270\255\347\232\204\350\256\276\350\256\241\346\250\241\345\274\217.md" "b/docs/JavaContainer/2_\345\256\271\345\231\250\344\270\255\347\232\204\350\256\276\350\256\241\346\250\241\345\274\217.md" similarity index 64% rename from "docs/JavaContainer/01\345\256\271\345\231\250\344\270\255\347\232\204\350\256\276\350\256\241\346\250\241\345\274\217.md" rename to "docs/JavaContainer/2_\345\256\271\345\231\250\344\270\255\347\232\204\350\256\276\350\256\241\346\250\241\345\274\217.md" index 483a9e5e..f795cada 100644 --- "a/docs/JavaContainer/01\345\256\271\345\231\250\344\270\255\347\232\204\350\256\276\350\256\241\346\250\241\345\274\217.md" +++ "b/docs/JavaContainer/2_\345\256\271\345\231\250\344\270\255\347\232\204\350\256\276\350\256\241\346\250\241\345\274\217.md" @@ -1,10 +1,8 @@ -# 二、容器中的设计模式 +# 容器中的设计模式 ## 迭代器模式 -[参考迭代器模式笔记](https://github.com/DuHouAn/Java/blob/master/Object_Oriented/notes/02%E8%A1%8C%E4%B8%BA%E5%9E%8B.md#4-%E8%BF%AD%E4%BB%A3%E5%99%A8iterator) - -

+

Collection 继承了 Iterable 接口,其中的 iterator() 方法能够产生一个 Iterator 对象,通过这个对象就可以迭代遍历 Collection 中的元素。 @@ -21,8 +19,6 @@ for (String item : list) { ## 适配器模式 -[参考适配器模式笔记](https://github.com/DuHouAn/Java/blob/master/Object_Oriented/notes/03%E7%BB%93%E6%9E%84%E5%9E%8B.md#1-%E9%80%82%E9%85%8D%E5%99%A8adapter) - java.util.Arrays#asList() 可以把数组类型转换为 List 类型。 ```java diff --git "a/docs/JavaContainer/02\345\256\271\345\231\250\346\272\220\347\240\201\345\210\206\346\236\220 - List.md" "b/docs/JavaContainer/3_\345\256\271\345\231\250\346\272\220\347\240\201\345\210\206\346\236\220 - List.md" similarity index 96% rename from "docs/JavaContainer/02\345\256\271\345\231\250\346\272\220\347\240\201\345\210\206\346\236\220 - List.md" rename to "docs/JavaContainer/3_\345\256\271\345\231\250\346\272\220\347\240\201\345\210\206\346\236\220 - List.md" index cea08c10..161d764e 100644 --- "a/docs/JavaContainer/02\345\256\271\345\231\250\346\272\220\347\240\201\345\210\206\346\236\220 - List.md" +++ "b/docs/JavaContainer/3_\345\256\271\345\231\250\346\272\220\347\240\201\345\210\206\346\236\220 - List.md" @@ -1,6 +1,6 @@ -# 三、容器源码分析 - List +# 容器源码分析 - List -如果没有特别说明,以下源码分析基于 JDK 1.8。 +以下源码分析基于 JDK 1.8。 ## ArrayList @@ -176,7 +176,7 @@ private void readObject(java.io.ObjectInputStream s) } ``` -### 6.System.arraycopy()和Arrays.copyOf()方法 +### 6. System.arraycopy()和Arrays.copyOf()方法 Arrays.copyOf() 的源代码内部调用了 System.arraycopy() 方法。 - System.arraycopy() 方法需要目标数组,将原数组拷贝到目标数组里,而且可以选择**拷贝的起点和长度以及放入新数组中的位置**; @@ -225,6 +225,8 @@ List synList = Collections.synchronizedList(list); List list = new CopyOnWriteArrayList<>(); ``` + + ## LinkedList ### 1. 概览 @@ -246,10 +248,11 @@ transient Node first; transient Node last; ``` -

+
### 2. 添加元素 -- 将元素添加到链表尾部 +将元素添加到链表尾部: + ```java public boolean add(E e) { linkLast(e);//这里就只调用了这一个方法 @@ -274,7 +277,8 @@ void linkLast(E e) { } ``` -- 将元素添加到链表头部 +将元素添加到链表头部: + ```java public void addFirst(E e) { linkFirst(e); diff --git "a/docs/JavaContainer/03\345\256\271\345\231\250\346\272\220\347\240\201\345\210\206\346\236\220 - Map.md" "b/docs/JavaContainer/4_\345\256\271\345\231\250\346\272\220\347\240\201\345\210\206\346\236\220 - Map.md" similarity index 97% rename from "docs/JavaContainer/03\345\256\271\345\231\250\346\272\220\347\240\201\345\210\206\346\236\220 - Map.md" rename to "docs/JavaContainer/4_\345\256\271\345\231\250\346\272\220\347\240\201\345\210\206\346\236\220 - Map.md" index 23113a9f..595262cc 100644 --- "a/docs/JavaContainer/03\345\256\271\345\231\250\346\272\220\347\240\201\345\210\206\346\236\220 - Map.md" +++ "b/docs/JavaContainer/4_\345\256\271\345\231\250\346\272\220\347\240\201\345\210\206\346\236\220 - Map.md" @@ -1,6 +1,6 @@ -# 三、容器源码分析 - Map +# 容器源码分析 - Map -如果没有特别说明,以下源码分析基于 JDK 1.8。 +以下源码分析基于 JDK 1.8。 ## HashMap @@ -18,7 +18,7 @@ Entry 存储着键值对。它包含了四个字段,从 next 字段我们可 即数组中的每个位置被当成一个桶,一个桶存放一个链表。HashMap 使用**拉链法**来解决冲突, 同一个链表中存放哈希值相同的 Entry。 -

+

```java static class Entry implements Map.Entry { @@ -98,7 +98,7 @@ map.put("K3", "V3"); - 计算键值对所在的桶; - 在链表上顺序查找,时间复杂度显然和链表的长度成正比。 -

+

### 3. put 操作 diff --git "a/docs/JavaContainer/04\345\256\271\345\231\250\346\272\220\347\240\201\345\210\206\346\236\220 - \345\271\266\345\217\221\345\256\271\345\231\250.md" "b/docs/JavaContainer/5_\345\256\271\345\231\250\346\272\220\347\240\201\345\210\206\346\236\220 - \345\271\266\345\217\221\345\256\271\345\231\250.md" similarity index 87% rename from "docs/JavaContainer/04\345\256\271\345\231\250\346\272\220\347\240\201\345\210\206\346\236\220 - \345\271\266\345\217\221\345\256\271\345\231\250.md" rename to "docs/JavaContainer/5_\345\256\271\345\231\250\346\272\220\347\240\201\345\210\206\346\236\220 - \345\271\266\345\217\221\345\256\271\345\231\250.md" index 4487d922..98396c27 100644 --- "a/docs/JavaContainer/04\345\256\271\345\231\250\346\272\220\347\240\201\345\210\206\346\236\220 - \345\271\266\345\217\221\345\256\271\345\231\250.md" +++ "b/docs/JavaContainer/5_\345\256\271\345\231\250\346\272\220\347\240\201\345\210\206\346\236\220 - \345\271\266\345\217\221\345\256\271\345\231\250.md" @@ -1,6 +1,6 @@ -# 三、容器源码分析 - 并发容器 +# 容器源码分析 - 并发容器 -如果没有特别说明,以下源码分析基于 JDK 1.8。 +以下源码分析基于 JDK 1.8。 ## CopyOnWriteArrayList @@ -103,7 +103,7 @@ final Segment[] segments; static final int DEFAULT_CONCURRENCY_LEVEL = 16; ``` -
+
### 2. size 操作 @@ -223,24 +223,16 @@ static final Node tabAt(Node[] tab, int i) { **底层数据结构:** -- JDK1.7 的ConcurrentHashMap底层采用**分段的数组+链表**实现, - JDK1.8 的ConcurrentHashMap底层采用的数据结构与JDK1.8 的HashMap的结构一样,**数组+链表/红黑二叉树**。 -- Hashtable和JDK1.8 之前的HashMap的底层数据结构类似都是采用**数组+链表**的形式, +- JDK1.7 的 ConcurrentHashMap 底层采用**分段的数组+链表**实现, + JDK1.8 的 ConcurrentHashMap 底层采用的数据结构与 JDK1.8 的 HashMap 的结构一样,**数组+链表/红黑二叉树**。 +- Hashtable 和 JDK1.8 之前的HashMap的底层数据结构类似都是采用**数组+链表**的形式, 数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。 **实现线程安全的方式** -- JDK1.7的ConcurrentHashMap(分段锁)对整个桶数组进行了分割分段(Segment), +- JDK1.7 的 ConcurrentHashMap(分段锁)对整个桶数组进行了分割分段(Segment), 每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问度。 - JDK 1.8 采用**数组+链表/红黑二叉树**的数据结构来实现,并发控制使用**synchronized和CAS**来操作。 -- Hashtable:使用 synchronized 来保证线程安全,效率非常低下。 + JDK 1.8 采用**数组+链表/红黑二叉树**的数据结构来实现,并发控制使用 **synchronized** 和 **CAS**来操作。 +- Hashtable 使用 synchronized 来保证线程安全,效率非常低下。 当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态, 如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈。 - -Hashtable 全表锁 - -
- -ConcurrentHashMap 分段锁 - -
\ No newline at end of file diff --git "a/docs/JavaIO/00\346\246\202\350\247\210.md" "b/docs/JavaIO/00\346\246\202\350\247\210.md" deleted file mode 100644 index e52ebf1b..00000000 --- "a/docs/JavaIO/00\346\246\202\350\247\210.md" +++ /dev/null @@ -1,16 +0,0 @@ -# 一、概览 - -## Java 的 I/O 大概可以分成以下几类: - -- 磁盘操作:File -- 字节操作:InputStream 和 OutputStream -- 字符操作:Reader 和 Writer -- 对象操作:Serializable -- 网络操作:Socket -- 新的输入/输出:NIO - -## IO流的分类: - -

- -注意:一般我们在探讨IO流的时候,如果没有明确说明按哪种分类来说,默认情况下是按照数据类型来分的。 diff --git "a/docs/JavaIO/07\351\235\242\350\257\225\351\242\230.md" "b/docs/JavaIO/07\351\235\242\350\257\225\351\242\230.md" deleted file mode 100644 index 2ab08ec6..00000000 --- "a/docs/JavaIO/07\351\235\242\350\257\225\351\242\230.md" +++ /dev/null @@ -1,60 +0,0 @@ -# 1. BIO、AIO、NIO的区别 - -## BIO:Block-IO - -同步阻塞IO,数据的读取写入必须**阻塞在一个线程内等待其完成**。 - -基于字节流的InputStream和OutputStream - -基于字符流的Reader和Writer - -
- -
- -对于C/S模型来说,就是一个连接对应一个线程,特点是在IO的执行过程中,程序都被阻塞住了。 - -## NIO:NonBlock-IO - -构建[多路复用]()、同步非阻塞的IO操作 - -
- -
- -NIO的核心: - -- Channels - - FileChannel:避免了两次用户态和内核态之间的上下文切换,即“零拷贝”,效率极高 - - DatagramChannel - - SocketChannel - - ServerSocketChannel -- Buffers -- Selectors:允许单线程处理多个Channel - -
- -
- -## AIO:Asynchronous IO - -基于事件和回调机制 - -
- -
- -AIO如何进一步加工处理结果 - -- 基于回调:实现CompletionHandler接口,调用时触发回调函数 -- 返回Future:通过isDone()查看是否准备好,通过get()等待返回数据 - -## 对比 - -| 属性\模型 | BIO | NIO | AIO | -| ----------------------- | ---------- | ---------- | ---------- | -| blocking | 阻塞并同步 | 非阻塞同步 | 非阻塞异步 | -| 线程数(server:client) | 1: 1 | 1: N | 0: N | -| 复杂度 | 简单 | 较复杂 | 复杂 | -| 吞吐量 | 低 | 高 | 高 | - diff --git "a/docs/JavaIO/1_\346\246\202\350\247\210.md" "b/docs/JavaIO/1_\346\246\202\350\247\210.md" new file mode 100644 index 00000000..5260aca5 --- /dev/null +++ "b/docs/JavaIO/1_\346\246\202\350\247\210.md" @@ -0,0 +1,45 @@ +# Java I/O 概览 + +## I/O 介绍 + +I/O(**I**nput/**O**utpu) 即**输入/输出** 。 + +学术的说 I/O 是信息处理系统(计算机)与外界(人或信息处理系统)间的通信。如计算机,即 CPU 访问任何寄存器和 Cache 等封装以外的数据资源都可当成 I/O ,包括且不限于内存,磁盘,显卡。 + +软件开发中的 I/O 则常指磁盘、网络 IO。 + +补充: + +- [如何完成一次IO](https://llc687.top/126.html) +- [5 种IO模型](https://mp.weixin.qq.com/s?__biz=Mzg3MjA4MTExMw==&mid=2247484746&idx=1&sn=c0a7f9129d780786cabfcac0a8aa6bb7&source=41&scene=21#wechat_redirect) + +## 同步 & 异步 + +同步、异步是消息通知机制。 + +- 同步:同步就是发起一个调用后,被调用者未处理完请求之前,调用不返回。 +- 异步:异步就是发起一个调用后,立刻得到被调用者的回应表示已接收到请求,但是被调用者并没有返回结果,此时我们可以处理其他的请求,被调用者通常依靠事件,回调等机制来通知调用者其返回结果。 + +同步和异步的区别最大在于异步的话调用者不需要等待处理结果,被调用者会通过回调等机制来通知调用者其返回结果。 + +## 阻塞 & 非阻塞 + +阻塞、非阻塞是等待通知时的状态。 + +- 阻塞: 阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。 +- 非阻塞:非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。 + +举个生活中简单的例子: + +你妈妈让你烧水,小时候你比较笨啊,在哪里傻等着水开( **同步阻塞** )。等你稍微再长大一点,你知道每次烧水的空隙可以去干点其他事,然后只需要时不时来看看水开了没有( **同步非阻塞** )。后来,你们家用上了水开了会发出声音的壶,这样你就只需要听到响声后就知道水开了,在这期间你可以随便干自己的事情( **异步非阻塞** )。 + +## Java I/O 分类 + +Java 的 I/O 大概可以分成以下几类: + +- 磁盘操作:File +- 字节操作:InputStream 和 OutputStream +- 字符操作:Reader 和 Writer +- 对象操作:Serializable +- 网络操作:Socket +- 新的输入/输出:NIO & AIO diff --git "a/docs/JavaIO/01\347\243\201\347\233\230\346\223\215\344\275\234.md" "b/docs/JavaIO/2_\347\243\201\347\233\230\346\223\215\344\275\234.md" similarity index 99% rename from "docs/JavaIO/01\347\243\201\347\233\230\346\223\215\344\275\234.md" rename to "docs/JavaIO/2_\347\243\201\347\233\230\346\223\215\344\275\234.md" index 772780e7..c52436ba 100644 --- "a/docs/JavaIO/01\347\243\201\347\233\230\346\223\215\344\275\234.md" +++ "b/docs/JavaIO/2_\347\243\201\347\233\230\346\223\215\344\275\234.md" @@ -1,4 +1,4 @@ -# 二、磁盘操作 +# 磁盘操作 我们要想实现IO的操作,就必须知道硬盘上文件的表现形式。而Java就提供了一个类File供我们使用。 ## File对象 diff --git "a/docs/JavaIO/02\345\255\227\350\212\202\346\223\215\344\275\234.md" "b/docs/JavaIO/3_\345\255\227\350\212\202\346\223\215\344\275\234.md" similarity index 98% rename from "docs/JavaIO/02\345\255\227\350\212\202\346\223\215\344\275\234.md" rename to "docs/JavaIO/3_\345\255\227\350\212\202\346\223\215\344\275\234.md" index 0dd66e10..c0c9cc63 100644 --- "a/docs/JavaIO/02\345\255\227\350\212\202\346\223\215\344\275\234.md" +++ "b/docs/JavaIO/3_\345\255\227\350\212\202\346\223\215\344\275\234.md" @@ -1,4 +1,4 @@ -# 三、字节操作 +# 字节操作 ## 字节输出流FileOutputStream ### 创建输出流对象 OutputStream 流对象是一个抽象类,不能实例化。所以,我们要找一个具体的子类 :FileOutputStream。 @@ -309,7 +309,7 @@ Java I/O 使用了装饰者模式来实现。以 InputStream 为例, - FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作; - FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能。例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。 -

+

实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。 diff --git "a/docs/JavaIO/03\345\255\227\347\254\246\346\223\215\344\275\234.md" "b/docs/JavaIO/4_\345\255\227\347\254\246\346\223\215\344\275\234.md" similarity index 99% rename from "docs/JavaIO/03\345\255\227\347\254\246\346\223\215\344\275\234.md" rename to "docs/JavaIO/4_\345\255\227\347\254\246\346\223\215\344\275\234.md" index 3cf7ad81..95434868 100644 --- "a/docs/JavaIO/03\345\255\227\347\254\246\346\223\215\344\275\234.md" +++ "b/docs/JavaIO/4_\345\255\227\347\254\246\346\223\215\344\275\234.md" @@ -1,4 +1,4 @@ -# 四、字符操作 +# 字符操作 ## 编码与解码 diff --git "a/docs/JavaIO/04\345\257\271\350\261\241\346\223\215\344\275\234.md" "b/docs/JavaIO/5_\345\257\271\350\261\241\346\223\215\344\275\234.md" similarity index 87% rename from "docs/JavaIO/04\345\257\271\350\261\241\346\223\215\344\275\234.md" rename to "docs/JavaIO/5_\345\257\271\350\261\241\346\223\215\344\275\234.md" index 32a007d9..bb15fcd9 100644 --- "a/docs/JavaIO/04\345\257\271\350\261\241\346\223\215\344\275\234.md" +++ "b/docs/JavaIO/5_\345\257\271\350\261\241\346\223\215\344\275\234.md" @@ -1,8 +1,12 @@ -# 五、对象操作 +# 对象操作 -## 序列化 +## 序列化 & 反序列化 -序列化就是将一个对象转换成字节序列,方便存储和传输。 +如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。 + +序列化就是将数据结构或对象转换成二进制字节流的过程。 + +反序列化将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程。 - 序列化:ObjectOutputStream.writeObject() - 反序列化:ObjectInputStream.readObject() diff --git "a/docs/JavaIO/05\347\275\221\347\273\234\346\223\215\344\275\234.md" "b/docs/JavaIO/6_\347\275\221\347\273\234\346\223\215\344\275\234.md" similarity index 98% rename from "docs/JavaIO/05\347\275\221\347\273\234\346\223\215\344\275\234.md" rename to "docs/JavaIO/6_\347\275\221\347\273\234\346\223\215\344\275\234.md" index bbd74976..eaa5928e 100644 --- "a/docs/JavaIO/05\347\275\221\347\273\234\346\223\215\344\275\234.md" +++ "b/docs/JavaIO/6_\347\275\221\347\273\234\346\223\215\344\275\234.md" @@ -1,4 +1,4 @@ -# 六、网络操作 +# 网络操作 Java 中的网络支持: @@ -67,7 +67,7 @@ public static void main(String[] args) throws IOException { - Socket:客户端类 - 服务器和客户端通过 InputStream 和 OutputStream 进行输入输出。 -
+
## Datagram diff --git a/docs/JavaIO/06NIO.md b/docs/JavaIO/7_NIO.md similarity index 78% rename from docs/JavaIO/06NIO.md rename to docs/JavaIO/7_NIO.md index 413cb678..537fe3a5 100644 --- a/docs/JavaIO/06NIO.md +++ b/docs/JavaIO/7_NIO.md @@ -1,8 +1,8 @@ -# 七、NIO +# 一、NIO -新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的,弥补了原来的 I/O 的不足,提供了高速的、面向块的 I/O。 +NIO(New I/O)即新的输入/输出库是在 JDK 1.4 中引入的,弥补了原来的 I/O 的不足,提供了高速的、面向块的 I/O。 -NIO核心组件: +NIO 核心组件: - 通道(Channels) @@ -65,23 +65,23 @@ I/O 包和 NIO 已经很好地集成了,java.io.\* 已经以 NIO 为基础重 ① 新建一个大小为 8 个字节的缓冲区,此时 position 为 0,而 limit = capacity = 8。capacity 变量不会改变,下面的讨论会忽略它。 -

+

② 从输入通道中读取 5 个字节数据写入缓冲区中,此时 position 为 5,limit 保持不变。 -

+

③ 在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position,并将 position 设置为 0。 -

+

④ 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。 -

+

⑤ 最后需要调用 clear() 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。 -

+

## 文件 NIO 实例 ### FileChannel的使用 @@ -427,7 +427,7 @@ NIO 实现了 IO 多路复用中的 **Reactor 模型**,一个线程 Thread 使 应该注意的是,只有套接字 Channel 才能配置为非阻塞,而 FileChannel 不能, 为 FileChannel 配置非阻塞也没有意义。 -

+

使用Selector的优点: @@ -660,7 +660,9 @@ public class NIOClient { ## 内存映射文件 -内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。 +内存映射文件((memory-mapped file)) I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。 + +内存映射文件能让你创建和修改那些大到无法读入内存的文件。有了内存映射文件,可以认为文件已经全部读进了内存,然后把它当成一个非常大的数组来访问了。将文件的一段区域映射到内存中,比传统的文件处理速度要快很多。内存映射文件它虽然最终也是要从磁盘读取数据,但是它并不需要将数据读取到 OS 内核缓冲区,而是直接将进程的用户私有地址空间中的一部分区域与文件对象建立起映射关系,就好像直接从内存中读、写文件一样,速度当然快了。 向内存映射文件写入可能是危险的,只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。 @@ -670,13 +672,7 @@ public class NIOClient { MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024); ``` -## NIO与IO对比 - -NIO 与普通 I/O 的区别主要有以下三点: -- NIO 是非阻塞的; -- NIO 面向块,I/O 面向流。 -- NIO有选择器,而I/O没有。 ## Path Java7中文件IO发生了很大的变化,专门引入了很多新的类来取代原来的 @@ -927,6 +923,205 @@ public class FilesDemo7 { paths:[a.txt, test2.txt, test.txt, test3.txt] ``` -# 参考资料 +# 二、AIO + +AIO(Asynchronous I/O)即异步输入/输出库是在 JDK 1.7 中引入的。虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。 + +对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行IO操作,IO操作本身是同步的。但是对AIO来说,则更加进了一步,它不是在 IO 准备好时再通知线程,而是在 IO 操作已经完成后,再给线程发出通知。因此 AIO 是不会阻塞的,此时我们的业务逻辑将变成一个**回调函数**,等待 IO 操作完成后,由系统自动触发。 + +## AsynchronousServerSocketChannel + +在 AIO Socket 编程中,服务端通道是 AsynchronousServerSocketChannel,该类主要有如下方法: + +- 提供了一个 open() 静态工厂 +- bind() 方法用于绑定服务端 IP 地址 + 端口号 +- accept() 用于接收用户连接请求 + +## AsynchronousSocketChannel + +在客户端使用的通道是 AsynchronousSocketChannel,这个通道处理除了提供 open 静态工厂方法外,还提供了read() 和 write() 方法。 + +## CompletionHandler + +在AIO编程中,发出一个事件(accept read write等)之后要指定事件处理类(回调函数),AIO 中的事件处理类是 CompletionHandler,这个接口定义了如下两个方法,分别在异步操作成功和失败时被回调: + +```java +void completed(V result,A attachment); +void failed(Throwable exc,A attachment); +``` + +## 示例 + +### 服务端 + +```java +package com.southeast.cn; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousServerSocketChannel; +import java.nio.channels.AsynchronousSocketChannel; +import java.nio.channels.CompletionHandler; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class AIOEchoServer { + + public final static int PORT = 8001; + public final static String IP = "127.0.0.1"; + + private AsynchronousServerSocketChannel server = null; + + public AIOEchoServer(){ + try { + //同样是利用工厂方法产生一个通道,异步通道 AsynchronousServerSocketChannel + server = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(IP,PORT)); + } catch (IOException e) { + e.printStackTrace(); + } + } + + //使用这个通道(server)来进行客户端的接收和处理 + public void start(){ + System.out.println("Server listen on "+PORT); + + //注册事件和事件完成后的处理器,这个CompletionHandler就是事件完成后的处理器 + server.accept(null,new CompletionHandler(){ + + final ByteBuffer buffer = ByteBuffer.allocate(1024); + + // 回调函数 + @Override + public void completed(AsynchronousSocketChannel result,Object attachment) { + + System.out.println(Thread.currentThread().getName()); + Future writeResult = null; + + try{ + buffer.clear(); + result.read(buffer).get(100,TimeUnit.SECONDS); + + System.out.println("In server: "+ new String(buffer.array())); + + //将数据写回客户端 + buffer.flip(); + writeResult = result.write(buffer); + }catch(InterruptedException | ExecutionException | TimeoutException e){ + e.printStackTrace(); + }finally{ + server.accept(null,this); + try { + writeResult.get(); + result.close(); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + } + + // 回调函数 + @Override + public void failed(Throwable exc, Object attachment) { + System.out.println("failed:"+exc); + } + + }); + } + + public static void main(String[] args) { + new AIOEchoServer().start(); + while(true){ + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + +} +``` + + + +### 客户端 + +```java +package com.southeast.cn; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousSocketChannel; +import java.nio.channels.CompletionHandler; + +public class AIOClient { + + public static void main(String[] args) throws IOException { + + final AsynchronousSocketChannel client = AsynchronousSocketChannel.open(); + + InetSocketAddress serverAddress = new InetSocketAddress("127.0.0.1",8001); -- https://juejin.im/post/5b4570cce51d451984695a9b \ No newline at end of file + CompletionHandler handler = new CompletionHandler(){ + + @Override + public void completed(Void result, Object attachment) { + client.write(ByteBuffer.wrap("Hello".getBytes()),null, + new CompletionHandler(){ + + @Override + public void completed(Integer result, + Object attachment) { + final ByteBuffer buffer = ByteBuffer.allocate(1024); + client.read(buffer,buffer,new CompletionHandler(){ + + @Override + public void completed(Integer result, + ByteBuffer attachment) { + buffer.flip(); + System.out.println(new String(buffer.array())); + try { + client.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void failed(Throwable exc, + ByteBuffer attachment) { + } + + }); + } + + @Override + public void failed(Throwable exc, Object attachment) { + } + + }); + } + + @Override + public void failed(Throwable exc, Object attachment) { + } + + }; + + client.connect(serverAddress, null, handler); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + +} +``` \ No newline at end of file diff --git a/docs/JavaIO/8_AIO.md b/docs/JavaIO/8_AIO.md new file mode 100644 index 00000000..f8e8666a --- /dev/null +++ b/docs/JavaIO/8_AIO.md @@ -0,0 +1,205 @@ +# AIO + +AIO(Asynchronous I/O)即异步输入/输出库是在 JDK 1.7 中引入的。虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。 + +对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行IO操作,IO操作本身是同步的。但是对AIO来说,则更加进了一步,它不是在 IO 准备好时再通知线程,而是在 IO 操作已经完成后,再给线程发出通知。因此 AIO 是不会阻塞的,此时我们的业务逻辑将变成一个**回调函数**,等待 IO 操作完成后,由系统自动触发。 + +## AsynchronousServerSocketChannel + +在 AIO Socket 编程中,服务端通道是 AsynchronousServerSocketChannel,该类主要有如下方法: + +- 提供了一个 open() 静态工厂 +- bind() 方法用于绑定服务端 IP 地址 + 端口号 +- accept() 用于接收用户连接请求 + +## AsynchronousSocketChannel + +在客户端使用的通道是 AsynchronousSocketChannel,这个通道处理除了提供 open 静态工厂方法外,还提供了read() 和 write() 方法。 + +## CompletionHandler + +在AIO编程中,发出一个事件(accept read write等)之后要指定事件处理类(回调函数),AIO 中的事件处理类是 CompletionHandler,这个接口定义了如下两个方法,分别在异步操作成功和失败时被回调: + +```java +void completed(V result,A attachment); +void failed(Throwable exc,A attachment); +``` + +## 示例 + +### 服务端 + +```java +package com.southeast.cn; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousServerSocketChannel; +import java.nio.channels.AsynchronousSocketChannel; +import java.nio.channels.CompletionHandler; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class AIOEchoServer { + + public final static int PORT = 8001; + public final static String IP = "127.0.0.1"; + + private AsynchronousServerSocketChannel server = null; + + public AIOEchoServer(){ + try { + //同样是利用工厂方法产生一个通道,异步通道 AsynchronousServerSocketChannel + server = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(IP,PORT)); + } catch (IOException e) { + e.printStackTrace(); + } + } + + //使用这个通道(server)来进行客户端的接收和处理 + public void start(){ + System.out.println("Server listen on "+PORT); + + //注册事件和事件完成后的处理器,这个CompletionHandler就是事件完成后的处理器 + server.accept(null,new CompletionHandler(){ + + final ByteBuffer buffer = ByteBuffer.allocate(1024); + + // 回调函数 + @Override + public void completed(AsynchronousSocketChannel result,Object attachment) { + + System.out.println(Thread.currentThread().getName()); + Future writeResult = null; + + try{ + buffer.clear(); + result.read(buffer).get(100,TimeUnit.SECONDS); + + System.out.println("In server: "+ new String(buffer.array())); + + //将数据写回客户端 + buffer.flip(); + writeResult = result.write(buffer); + }catch(InterruptedException | ExecutionException | TimeoutException e){ + e.printStackTrace(); + }finally{ + server.accept(null,this); + try { + writeResult.get(); + result.close(); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + } + + // 回调函数 + @Override + public void failed(Throwable exc, Object attachment) { + System.out.println("failed:"+exc); + } + + }); + } + + public static void main(String[] args) { + new AIOEchoServer().start(); + while(true){ + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + +} +``` + + + +### 客户端 + +```java +package com.southeast.cn; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousSocketChannel; +import java.nio.channels.CompletionHandler; + +public class AIOClient { + + public static void main(String[] args) throws IOException { + + final AsynchronousSocketChannel client = AsynchronousSocketChannel.open(); + + InetSocketAddress serverAddress = new InetSocketAddress("127.0.0.1",8001); + + CompletionHandler handler = new CompletionHandler(){ + + @Override + public void completed(Void result, Object attachment) { + client.write(ByteBuffer.wrap("Hello".getBytes()),null, + new CompletionHandler(){ + + @Override + public void completed(Integer result, + Object attachment) { + final ByteBuffer buffer = ByteBuffer.allocate(1024); + client.read(buffer,buffer,new CompletionHandler(){ + + @Override + public void completed(Integer result, + ByteBuffer attachment) { + buffer.flip(); + System.out.println(new String(buffer.array())); + try { + client.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void failed(Throwable exc, + ByteBuffer attachment) { + } + + }); + } + + @Override + public void failed(Throwable exc, Object attachment) { + } + + }); + } + + @Override + public void failed(Throwable exc, Object attachment) { + } + + }; + + client.connect(serverAddress, null, handler); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + +} +``` + + + diff --git "a/docs/JavaIO/8_JavaIO\346\226\271\345\274\217.md" "b/docs/JavaIO/8_JavaIO\346\226\271\345\274\217.md" new file mode 100644 index 00000000..5ba72d74 --- /dev/null +++ "b/docs/JavaIO/8_JavaIO\346\226\271\345\274\217.md" @@ -0,0 +1,65 @@ +# Java I/O 方式 + +## BIO & NIO & AIO + +Java IO 的方式通常分为阻塞的 BIO(Blocking IO)、同步非阻塞的 NIO(New IO) 和异步非阻塞的 AIO(Asynchronous IO)。 + +JDK1.4 之前只支持 BIO,JDK1.4 以后开始支持 NIO,JDK1.7 开始支持 AIO。 + +### 1. BIO + +BIO 是同步阻塞的。服务器的模式为**一个连接一个线程**。 + +客户端有连接请求时,就需要启动一个线程进行处理。如果这个连接不做任何事情,就会造成不必要的开销,可以通过[线程池机制](https://duhouan.github.io/Java/#/Java_Concurrency/8_%E7%BA%BF%E7%A8%8B%E6%B1%A0)改善。 + +### 2. NIO + +NIO 是同步非阻塞的。服务器模式为**一个请求一个线程**。 + +NIO 最重要的地方是当一个连接建立后,不需要对应一个线程,这个连接会被注册到多路复用器上面,所以所有的连接只需要一个线程即可,当这个线程中的多路复用器进行轮询时,发现连接上有请求的话,才开启一个线程进行处理。 + +### 3. AIO + +AIO 是异步阻塞的。服务器模式为**一个有效请求一个线程**。 + +客户端的 IO 请求都是由操作系统完成,再通知服务器去启动线程进行处理。真正的 IO 读写已经由内核完成了。 + + + +## BIO 和 NIO 的区别 + +- **是否阻塞** + + BIO 是阻塞的,NIO 是非阻塞的。 + + BIO 中一个线程调用 read() / write() 时,该线程被阻塞; + + NIO 中一个线程从 Channel 中读取数据到 Buffer 中,可以继续做别的操作,当数据读取到 Buffer 中后,线程再继续处理数据;一个线程请求写入数据到某个 Channel,但不需要等待完全写入,该线程可进行别的操作。 + +- **缓冲区(Buffer)** + + BIO 是面向流的,NIO 是面向缓冲区。 + + 发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要先读到缓冲区中。也就是说,**不会直接对通道进行读写数据,而是要先经过缓冲区**。 + +- **通道(Channel)** + + NIO 通过 Chammel 进行读写。 + + 通道和流的不同之处在于:流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而**通道是双向的**,可以用于读、写或者同事用于读写。 + + 通道只能和缓冲区交互,因为有缓冲区,通道可以异步读写。 + +- **选择器(Selector)** + + NIO 中一个线程使用一个选择器 Selector,通过**轮询**的方式去监听多个 Channel 上的事件。从而让一个线程就可以处理多个事件。 + + 使用 Selector 的好处:使用更少的线程就可以来处理通道了,相比使用多个线程,**减少了线程上下文切换带来的开销**。 + + + +## 应用场景 + +- BIO 适用于**连接数目少且固定的架构**。这种方式对服务器要求比较高,并发局限于应用中,程序直观简单,容易理解。 +- NIO 适用于**连接数目多且连接比较短的架构**。这种方式并发局限于应用中,但编程较复杂。 +- AIO 适用于**连接数目多且连接比较长的架构**。这种方式充分调用服务器的并发操作,但是编程复杂。 \ No newline at end of file diff --git "a/docs/JavaWeb/00Servlet\345\267\245\344\275\234\345\216\237\347\220\206\350\247\243\346\236\220.md" "b/docs/JavaWeb/1_Servlet\345\267\245\344\275\234\345\216\237\347\220\206\350\247\243\346\236\220.md" similarity index 100% rename from "docs/JavaWeb/00Servlet\345\267\245\344\275\234\345\216\237\347\220\206\350\247\243\346\236\220.md" rename to "docs/JavaWeb/1_Servlet\345\267\245\344\275\234\345\216\237\347\220\206\350\247\243\346\236\220.md" diff --git "a/docs/JavaWeb/01JSP\350\247\243\346\236\220.md" "b/docs/JavaWeb/2_JSP\350\247\243\346\236\220.md" similarity index 100% rename from "docs/JavaWeb/01JSP\350\247\243\346\236\220.md" rename to "docs/JavaWeb/2_JSP\350\247\243\346\236\220.md" diff --git "a/docs/JavaWeb/02\346\267\261\345\205\245\347\220\206\350\247\243Session\345\222\214Cookie.md" "b/docs/JavaWeb/3_\346\267\261\345\205\245\347\220\206\350\247\243Session\345\222\214Cookie.md" similarity index 100% rename from "docs/JavaWeb/02\346\267\261\345\205\245\347\220\206\350\247\243Session\345\222\214Cookie.md" rename to "docs/JavaWeb/3_\346\267\261\345\205\245\347\220\206\350\247\243Session\345\222\214Cookie.md" diff --git "a/docs/Java_Concurrency/1_\345\237\272\347\241\200\347\237\245\350\257\206.md" "b/docs/Java_Concurrency/1_\345\237\272\347\241\200\347\237\245\350\257\206.md" index 5a066dab..4850e65d 100644 --- "a/docs/Java_Concurrency/1_\345\237\272\347\241\200\347\237\245\350\257\206.md" +++ "b/docs/Java_Concurrency/1_\345\237\272\347\241\200\347\237\245\350\257\206.md" @@ -1,54 +1,52 @@ -# 基础知识 +# 一、进程和线程 -## 一、进程和线程 +## 进程 -### 1. 进程 +进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。在 Java 中,当启动 main 方法时其实就是启动了一个 JVM 的进程,而 main 方法所在的线程就是这个进程中的一个线程,称为**主线程**。 -进程是**资源分配的基本单位**。 +进程是**资源分配的基本单位**。每一个进程都有它自己的内存空间和系统资源。 -进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对 PCB 的操作。 +## 线程 -下图显示了 4 个程序创建了 4 个进程,这 4 个进程可以并发地执行。 +线程与进程相似,但线程是一个比进程更小的**执行单位**,是进程中的单个顺序控制流,是一条执行路径。 -![img](https://gitee.com/duhouan/ImagePro/raw/master/java-notes/os/a6ac2b08-3861-4e85-baa8-382287bfee9f.png) +一个进程如果只有一条执行路径,则称为单线程程序;一个进程如果有多条执行路径,则称为多线程程序。 -### 2. 线程 +一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的**堆**和**方法区**资源,但每个线程有自己的**程序计数器**、**虚拟机栈**和**本地方法栈**,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为**轻量级进程**。 -**线程是 CPU 调度的基本单位**。 +Java 程序是多线程程序,这是因为 JVM 启动至少了主线程和垃圾回收线程。 -一个进程中可以有多个线程,它们共享进程资源。 - -QQ 和浏览器是两个进程,浏览器进程里面有很多线程,例如 HTTP 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP 请求时,浏览器还可以响应用户的其它事件。 - -![img](https://gitee.com/duhouan/ImagePro/raw/master/java-notes/os/3cd630ea-017c-488d-ad1d-732b4efeddf5.png) - -### 3. 进程与线程的区别 +## 进程和线程的区别 - **资源** - 进程是资源调度的基本单位。 + 一个进程中可以有多个线程,多个线程共享进程的**堆**和**方法区**资源,但是每个线程有自己的**程序计数器**、**虚拟机栈** 和**本地方法栈**。 - 线程不占有任何资源。 +- **是否独立** -- **调度** + 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。 - 线程是独立调度的基本单位。 +- **开销** - 同一进程中,线程的切换不会引起进程切换,但一个进程中线程切换到另一个进程中的线程时,就会引起进程切换了。 + 线程执行开销小,但不利于资源的管理和保护,而进程正相反。 -- **系统开销** +> 思考 1:程序计数器线程私有? - 进程的创建和撤销所付出的开销比线程的创建和撤销要大很多。 +程序计数器是主要有以下两个作用: - 类似的,进程切换的开销也比线程切换的开销要大很多,因为进程切换时,涉及到**当前执行进程的 CPU 环境的保存**和**新调度进程 CPU 环境的设置**,而线程切换时只需要设置和保存少量的寄存器内容,开销很小。 +- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。 +- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。 -- **通信** +所以,程序计数器私有主要是为了**线程切换后能恢复到正确的执行位置**。 - 线程可以通过**直接读写同一进程中的数据**进行通信。 +> 思考 2 :Java 虚拟机栈、本地方法栈线程私有? - 进程之间的通信需要借助 IPC (InterProcess Communication)。 +- Java 虚拟机栈中每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。 +- 本地方法栈和虚拟机栈所发挥的作用非常相似,它们之间的区别只不过是本地方法栈为本地方法服务。 -### 4. 多进程与多线程 +所以,为了**保证线程中的局部变量不被别的线程访问到**,Java 虚拟机栈和本地方法栈是线程私有的。 + +## 多进程与多线程 多进程的目的是**提高 CPU 的使用率**。 @@ -56,49 +54,125 @@ QQ 和浏览器是两个进程,浏览器进程里面有很多线程,例如 H 多线程共享同一进程资源(堆内存和方法区),但是栈内存是独立的,JVM 中一个线程对应一个线程栈。多线程抢夺的是 CPU 资源,一个时间点上只有一个线程运行。 -### 5. 并发与并行 +## 上下文切换 + +线程在执行过程中会有自己的运行条件和状态,即上下文,比如程序计数器,栈信息等。当出现以下几种情况的时,线程会从占用 CPU 状态中退出: + +- 主动让出 CPU,比如调用了 `sleep()`, `wait()` 等。 +- 时间片用完,因为操作系统要防止一个线程或者进程长时间占用 CPU 导致其他线程或者进程饿死。 +- 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。 +- 被终止或结束运行 -- 并发:一个处理器同时处理多个任务,是**逻辑上的同时发生**。 +其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场,并加载下一个将要占用 CPU 的线程上下文。这就是**上下文切换**。 -- 并行:多个处理器或者多核处理器同时处理多个不同的任务,是**物理上的同时发生**。 +上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息,这将会占用 CPU,内存等系统资源进行处理,也就意味着效率会有一定损耗,如果频繁切换就会造成整体效率低下。 -来个比喻:并发是一个人同时吃三馒头,而并行就是三个人同时吃三个馒头。 +## 并发与并行 -下图反映了一个包含 8 个操作的任务在一个有两核心的 CPU 中创建四个线程运行的情况。 +并发:一个处理器同时处理多个任务,是**逻辑上的同时发生**。 -假设每个核心有两个线程,那么每个CPU中两个线程会交替并发,两个CPU之间的操作会并行运算。 +并行:多个处理器或者多核处理器同时处理多个不同的任务,是**物理上的同时发生**。 + +下图反映了一个包含 8 个操作的任务在一个有两核心的 CPU 中创建四个线程运行的情况。 假设每个核心有两个线程,那么每个CPU中两个线程会交替并发,两个CPU之间的操作会并行运算。 单就一个 CPU 而言两个线程可以解决线程阻塞造成的不流畅问题,其本身运行效率并没有提高, 多 CPU 的并行运算才真正解决了运行效率问题,这也正是并发和并行的区别。 -
+
+ + + +## 多线程的好处 + +- **从计算机底层来说:** 线程可以比作是**轻量级的进程**,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。 +- **从当代互联网发展趋势来说:** 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。 + -## 二、创建线程的几种方式 -### 1. 三种创建线程的方式 +## 多线程带来的问题 + +并发编程的目的就是为了能提高程序的执行效率,从而提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:**内存泄漏**、**死锁**、**线程不安全**等等。 + + + +# 二、创建线程 + +## 三种创建线程的方式 Java 有 3 种创建线程的方式: - 继承 Thread 类,重写 run() 方法 +- 实现 Runnable 接口,重写 run() 方法 +- 实现 Callable 接口,重写 call() 方法 - ```java - public class MyThread extends Thread{ - private void attack() { - System.out.println("Fight"); - System.out.println("Current Thread is : " + Thread.currentThread().getName()); - } - - @Override - public void run() { //重写 run() 方法 - attack(); - } - } - ``` +### 继承 Thread 类 -- 实现 Runnable 接口,重写 run() 方法 +需要实现 run() 方法,因为 Thread 类也实现了 Runable 接口。 -- 实现 Callable 接口,重写 call() 方法 +当调用 start() 方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run() 方法。 -### 2. 选择 +```java +public class MyThread extends Thread { + public void run() { + // ... + } +} +``` + +```java +public static void main(String[] args) { + MyThread mt = new MyThread(); + mt.start(); +} +``` + +### 实现 Runnable 接口 + +需要实现接口中的 run() 方法。使用 Runnable 实例再创建一个 Thread 实例,然后调用 Thread 实例的 start() 方法来启动线程。 + +```java +public class MyRunnable implements Runnable { + @Override + public void run() { + // ... + } +} +``` + +```java +public static void main(String[] args) { + MyRunnable instance = new MyRunnable(); + Thread thread = new Thread(instance); + thread.start(); +} +``` + + + +### 实现 Callable 接口 + +需要实现接口中的 call() 方法。 call() 方法有返回值,返回值通过 FutureTask 进行封装。 + +```java +public class MyCallable implements Callable { + public Integer call() { + return 1; + } +} +``` + +```java +public static void main(String[] args) throws ExecutionException, InterruptedException { + MyCallable mc = new MyCallable(); + FutureTask ft = new FutureTask<>(mc); // 返回值通过 FutureTask 进行封装 + Thread thread = new Thread(ft); + thread.start(); + System.out.println(ft.get()); +} +``` + + + +## 选择线程创建方式 实现接口会更好一些: @@ -118,7 +192,7 @@ Java 有 3 种创建线程的方式: 注意:一个线程只能 start 一次,如果多次 start,会出现 `java.lang.IllegalException`。 -### 3. Runnable 和 Callable 的区别 +## Runnable 和 Callable 的区别 - Callable 的实现方法是 **call()** @@ -132,9 +206,9 @@ Java 有 3 种创建线程的方式: Runnable 的 run() 方法不能抛出异常 -### 4. 实现处理线程的返回值 +## 实现处理线程的返回值 -#### 方式一:主线程等待法 +### 方式一:主线程等待法 如果未获取到值,则主线程等待,一直到获取值为止。 @@ -168,7 +242,7 @@ public class CycleWait implements Runnable{ } ``` -#### 方式二:join() +### 方式二:join() 使用 Thread 的 join() **阻塞当前线程以等待子线程处理完毕**。 @@ -200,7 +274,7 @@ public class CycleWait2 implements Runnable{ } ``` -#### 方式三:Callable + FutureTask +### 方式三:Callable + FutureTask 实现 Callablee 接口,使用 FutureTask 接收 call() 方法返回的数据。 @@ -233,7 +307,7 @@ public class CycleWait3 implements Callable{ } ``` -#### 方式四:线程池 + Future +### 方式四:线程池 + Future 线程池执行任务,返回的结果使用 Future 接收。 @@ -269,67 +343,11 @@ public class CycleWait4 implements Callable{ } ``` -## 三、线程的几种状态 - -![img](https://gitee.com/duhouan/ImagePro/raw/master/pics/concurrent/96706658-b3f8-4f32-8eb3-dcb7fc8d5381.jpg) - -### 新建(New) - -创建后尚未启动。 - -### 可运行(Runnable) - -可能正在运行,也可能正在等待 CPU 时间片。 - -包含了操作系统线程状态中的 Running 和 Ready。 - -调用 `start()` 方法后开始运行,线程这时候处于 Ready 状态。可运行状态的线程获得了 CPU 时间片后就进入Running 状态。 - -### 阻塞(Blocked) - -等待获取一个**排它锁**,如果其他线程释放了锁就会结束此状态。 - -### 无限期等待(Waiting) - -等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。 - -| 进入方法 | 退出方法 | -| ------------------------------------------ | ------------------------------------ | -| 没有设置 Timeout 参数的 Object.wait() 方法 | Object.notify() / Object.notifyAll() | -| 没有设置 Timeout 参数的 Thread.join() 方法 | 被调用的线程执行完毕 | -| LockSupport.park() 方法 | LockSupport.unpark(Thread) | - -### *限期等待(Timed Waiting) - -无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。 - -调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。 - -调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。 -**睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态**。 -| 进入方法 | 退出方法 | -| ---------------------------------------- | ----------------------------------------------- | -| Thread.sleep() 方法 | 时间结束 | -| 设置了 Timeout 参数的 Object.wait() 方法 | 时间结束 / Object.notify() / Object.notifyAll() | -| 设置了 Timeout 参数的 Thread.join() 方法 | 时间结束 / 被调用的线程执行完毕 | -| LockSupport.parkNanos() 方法 | LockSupport.unpark(Thread) | -| LockSupport.parkUntil() 方法 | LockSupport.unpark(Thread) | +# 三、基础线程机制 -**阻塞和等待的区别**在于: - -阻塞是被动的,它是在等待获取一个排它锁。 - -等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。 - -### 死亡(Terminated) - -可以是线程完成任务后自己结束,或者产生了异常而结束。 - -## 四、基础线程机制 - -### Executor +## Executor Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指**多个任务的执行互不干扰,不需要进行同步操作**。 @@ -349,7 +367,7 @@ public static void main(String[] args) { } ``` -### Daemon +## Daemon Java 中有两类线程: @@ -367,7 +385,7 @@ public static void main(String[] args) { } ``` -### start() 和 run() +## start() & run() - start() @@ -381,7 +399,7 @@ public static void main(String[] args) { 单独调用 run() 不会启动新线程 -### sleep() 和 wait() +## sleep() & wait() sleep() 和 wait() 的区别: @@ -403,7 +421,7 @@ sleep() 和 wait() 的区别: sleep() 可在任何地方使用 -### yield() 和 wait() +## yield() & wait() Thread.yield() 声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。 @@ -421,7 +439,7 @@ yield() 和 wait() 的区别: wait() 会释放同步锁,使得其他线程可以使用同步块或者同步方法 -### notify() 和 notifyAll() +## notify() & notifyAll() 锁池:线程 A 已经拥有某个对象的锁,线程 B、C 想要调用这个对象的同步方法,此时线程 B、C 就会被阻塞,进入一个地方去等待锁的释放,这个地方就是该对象的锁池。 @@ -431,20 +449,98 @@ yield() 和 wait() 的区别: -## 五、终止线程的几种方式 +# 四、线程生命周期和状态 + +## 6 种线程状态 -### 1. 退出标志 +### 新建(New) + +线程被创建,但是还没有调用 start() 方法 + +### 可运行(Runnable) + +可能正在运行,也可能正在等待 CPU 时间片。 + +包含了操作系统线程状态中的运行状态(Running)和就绪状态( Ready)。 + +调用 start() 方法后开始运行,线程这时候处于 Ready 状态。Ready 状态的线程获得了 CPU 时间片后就进入Running 状态。 + +### 阻塞(Blocked) + +等待获取一个**排它锁**,如果其他线程释放了锁就会结束此状态。 + +### 无限期等待(Waiting) + +等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。 + +| 进入方法 | 退出方法 | +| ------------------------------------------ | ------------------------------------ | +| 没有设置 Timeout 参数的 Object.wait() 方法 | Object.notify() / Object.notifyAll() | +| 没有设置 Timeout 参数的 Thread.join() 方法 | 被调用的线程执行完毕 | +| LockSupport.park() 方法 | LockSupport.unpark(Thread) | + +### 限期等待(Timed Waiting) + +无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。 + +| 进入方法 | 退出方法 | +| ---------------------------------------- | ----------------------------------------------- | +| Thread.sleep() 方法 | 时间结束 | +| 设置了 Timeout 参数的 Object.wait() 方法 | 时间结束 / Object.notify() / Object.notifyAll() | +| 设置了 Timeout 参数的 Thread.join() 方法 | 时间结束 / 被调用的线程执行完毕 | +| LockSupport.parkNanos() 方法 | LockSupport.unpark(Thread) | +| LockSupport.parkUntil() 方法 | LockSupport.unpark(Thread) | + +调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。 + +调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。 + +**睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态**。 + +**阻塞和等待的区别**在于: + +阻塞是被动的,它是在等待获取一个排它锁。 + +等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。 + +### 死亡(Terminated) + +可以是线程完成任务后自己结束,或者产生了异常而结束。 + + + +## 线程生命周期 + +线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换: + +
+ + + +线程创建之后它将处于NEW 状态,调用 `start()` 方法后开始运行,线程这时候处于 READY 状态。Ready 状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING 状态。 + +当线程执行 `wait()`方法之后,线程进入 WAITING 状态。进入 WAITING 状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 TIME_WAITING 状态相当于在等待状态的基础上增加了超时限制,比如通过 `sleep(long millis)`方法或 `wait(long millis)`方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。 + +当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED 状态。 + +线程在执行 Runnable 的`run()`方法之后将会进入到 TERMINATE 状态。 + + + +# 五、终止线程的几种方式 + +## 1. 退出标志 使用退出标志,使线程正常退出,也就是 run 方法完成后线程终止。 需要 while() 循环以某种特定的条件下退出,最直接的方法就是设计一个 boolean 类型的标志,并且通过设置这个标志来控制循环是否退出。 一般需要加上 volatile 来保证标志的可见性。 -### 2. Thread.stop() +## 2. Thread.stop() Thread.stop() 强制终止线程(不建议使用)。 -### 3. interrupt() 中断线程 +## 3. interrupt() 中断线程 - 线程处于阻塞状态,使用 interrupt() 则会抛出 InteruptedException 异常。 @@ -517,4 +613,5 @@ t1 (RUNNABLE) loop 2 t1 (TIMED_WAITING) is interrupted. t1 (RUNNABLE) catch InterruptedException. t1 (TERMINATED) is interrupted now. -``` \ No newline at end of file +``` + diff --git "a/docs/Java_Concurrency/2_\345\271\266\345\217\221\347\220\206\350\256\272.md" "b/docs/Java_Concurrency/2_\345\271\266\345\217\221\347\220\206\350\256\272.md" index 65864941..e354e7ad 100644 --- "a/docs/Java_Concurrency/2_\345\271\266\345\217\221\347\220\206\350\256\272.md" +++ "b/docs/Java_Concurrency/2_\345\271\266\345\217\221\347\220\206\350\256\272.md" @@ -6,123 +6,57 @@ Java 内存模型(即 Java Memory Model,简称 JMM)试图屏蔽各种硬 本身是一种**抽象的概念,并不真实存在**,它描述的是一组规则或规范。 -### 1. 主内存与工作内存 +### CPU 缓存 -处理器上的寄存器的读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存。 +**CPU 缓存的是内存数据用于解决 CPU 处理速度和内存不匹配的问题**。 -加入高速缓存带来了一个新的问题:缓存一致性。如果多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致,需要一些协议来解决这个问题。 +通常情况下,当一个 CPU 需要读取主存数据时,它会将主存的数据读到 CPU 缓存中,甚至可能将缓存中的部分内容读到它的内部寄存器中,然后在寄存器中执行操作。 -![img](https://gitee.com/duhouan/ImagePro/raw/master/pics/concurrent/d2a12961-2b36-4463-b017-ca46a3308b8e.png) +当 CPU 需要将结果写回到主存中去时,它会将内部寄存器的值刷新到缓存中,然后在某个时间点将值刷新回主存。 -JMM 规定所有的变量都存储在主内存中,每个线程还有自己的工作内存,**工作内存存储在高速缓存或者寄存器**中,保存了该线程使用的**变量的主内存副本拷贝**。 +
-**线程只能直接操作工作内存中的变量,不同线程之间的变量值传递需要通过主内存来完成**。 - -![img](https://gitee.com/duhouan/ImagePro/raw/master/pics/concurrent/8162aebb-8fd2-4620-b771-e65751ba7e41.png) - -### 2. JMM 三大特性 - -> **内存间的交互操作** - -Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作。 - -![img](https://gitee.com/duhouan/ImagePro/raw/master/pics/concurrent/b6a7e8af-91bf-44b2-8874-ccc6d9d52afc.jpg) +加入 CPU 缓存带来了一些新的问题: -- read:把一个变量的值从主内存传输到工作内存中 -- load:在 read 之后执行,把 read 得到的值放入工作内存的变量副本中 -- use:把工作内存中一个变量的值传递给执行引擎 -- assign:把一个从执行引擎接收到的值赋给工作内存的变量 -- store:把工作内存的一个变量的值传送到主内存中 -- write:在 store 之后执行,把 store 得到的值放入主内存的变量中 -- lock:作用于主内存的变量 -- unlock +- 缓存一致性问题:当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致的情况,如果真的发生这种情况,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作,这类协议有 MSI、MESI 等。 +- 指令重排序问题: 为了使得处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行**乱序执行(Out-Of-Order Execution)优化**,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致。 -#### 特性一:原子性 +### 主内存与工作内存 -**Java 内存模型保证了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性**。 +JMM 规定线程之间的共享变量存放在主内存(主内存就是硬件内存)中,每个线程还有自己的工作内存,存放该线程读/写共享变量的拷贝副本,**工作内存存储在 CPU 高速缓存或者寄存器中**。 -例如对一个 int 类型的变量执行 assign 赋值操作,这个操作就是原子性的。但是 Java 内存模型允许虚拟机将没有被 volatile 修饰的 64 位数据(long,double)的读写操作划分为两次 32 位的操作来进行,即 load、store、read 和 write 操作可以不具备原子性。 - -有一个错误认识就是,int 等原子性的类型在多线程环境中不会出现线程安全问题。前面的线程不安全示例代码中,cnt 属于 int 类型变量,1000 个线程对它进行自增操作之后,得到的值为 997 而不是 1000。 - -为了方便讨论,将内存间的交互操作简化为 3 个:load、assign、store。 +**线程只能直接操作工作内存中的变量,不同线程之间的变量值传递需要通过主内存来完成**。 -下图演示了两个线程同时对 cnt 进行操作,load、assign、store 这一系列操作整体上看不具备原子性,那么在 T1 修改 cnt 并且还没有将修改后的值写入主内存,T2 依然可以读入旧值。可以看出,这两个线程虽然执行了两次自增运算,但是主内存中 cnt 的值最后为 1 而不是 2。因此对 int 类型读写操作满足原子性只是说明 load、assign、store 这些单个操作具备原子性。 +
-![img](https://gitee.com/duhouan/ImagePro/raw/master/pics/concurrent/847b9ba1-b3cd-4e52-aa72-dee0222ae09f.jpg) +### 主内存和工作内存的交互 -AtomicInteger 能保证多个线程修改的原子性。 +JMM 定义了 8 个操作来完成主内存和工作内存的交互操作。 -![img](https://gitee.com/duhouan/ImagePro/raw/master/pics/concurrent/3144015c-dcfb-47ac-94a5-bab3b78b0f14.jpg) +
-使用 AtomicInteger 重写之前线程不安全的代码之后得到以下线程安全实现: +- lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。 +- unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。 +- read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用 +- load(载入):作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。 +- use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。 +- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。 +- store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的 write 的操作。 +- write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。 -```java -public class AtomicExample { - private AtomicInteger cnt = new AtomicInteger(); - public void add() { - cnt.incrementAndGet(); - } - public int get() { - return cnt.get(); - } -}Copy to clipboardErrorCopied -public static void main(String[] args) throws InterruptedException { - final int threadSize = 1000; - AtomicExample example = new AtomicExample(); // 只修改这条语句 - final CountDownLatch countDownLatch = new CountDownLatch(threadSize); - ExecutorService executorService = Executors.newCachedThreadPool(); - for (int i = 0; i < threadSize; i++) { - executorService.execute(() -> { - example.add(); - countDownLatch.countDown(); - }); - } - countDownLatch.await(); - executorService.shutdown(); - System.out.println(example.get()); -}Copy to clipboardErrorCopied -1000Copy to clipboardErrorCopied -``` +### 内存模型三大特性 -除了使用原子类之外,也可以使用 synchronized 互斥锁来保证操作的原子性。它对应的内存间交互操作为:lock 和 unlock,在虚拟机实现上对应的字节码指令为 monitorenter 和 monitorexit。 +#### 原子性 -```java -public class AtomicSynchronizedExample { - private int cnt = 0; +原子性指一次的操作或多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么所有的操作都执行,要么都不执行。 - public synchronized void add() { - cnt++; - } +**JMM 保证了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性**。例如对一个 int 类型的变量执行 assign 赋值操作,这个操作就是原子性的。但是 JMM 允许虚拟机将没有被 volatile 修饰的 64 位数据(long,double)的读写操作划分为两次 32 位的操作来进行,即 load、store、read 和 write 操作可以不具备原子性。 - public synchronized int get() { - return cnt; - } -}Copy to clipboardErrorCopied -public static void main(String[] args) throws InterruptedException { - final int threadSize = 1000; - AtomicSynchronizedExample example = new AtomicSynchronizedExample(); - final CountDownLatch countDownLatch = new CountDownLatch(threadSize); - ExecutorService executorService = Executors.newCachedThreadPool(); - for (int i = 0; i < threadSize; i++) { - executorService.execute(() -> { - example.add(); - countDownLatch.countDown(); - }); - } - countDownLatch.await(); - executorService.shutdown(); - System.out.println(example.get()); -} -``` +AtomicInteger 等原子操作类能保证多个线程修改的原子性。除了使用原子操作类之外,也可以使用 synchronized 互斥锁来保证操作的原子性。它对应的内存间交互操作为:lock 和 unlock,在虚拟机实现上对应的字节码指令为 monitorenter 和 monitorexit。 -```java -1000 -``` - -#### 特性二:可见性 +#### 可见性 可见性指当一个线程修改了**共享变量的值**,其它线程能够立即得知这个修改。 @@ -134,7 +68,7 @@ public static void main(String[] args) throws InterruptedException { 注意: volatile 并不能保证操作的原子性。 -#### 特性三:有序性 +#### 有序性 有序性是指在一个线程内观察,所有的操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,**无序是因为发生了指令重排序**。 @@ -145,67 +79,26 @@ volatile 和 synchronized 都可保证有序性: - volatile 关键字通过添加**内存屏障**的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前。 - synchronized 来保证有序性,它**保证每个时刻只有一个线程执行同步代码**,相当于是让线程顺序执行同步代码。 -### 3. 先行发生原则 - -#### 规则一:单一线程原则 - -> Single Thread rule - -在一个线程内,在程序前面的操作先行发生于后面的操作。 - -![img](https://gitee.com/duhouan/ImagePro/raw/master/pics/concurrent/94414cd3-5db9-4aca-a2af-539140955c62.jpg) - -#### 规则二:管程锁定规则 - -一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。 - -![img](https://gitee.com/duhouan/ImagePro/raw/master/pics/concurrent/de9d8133-4c98-4e07-b39c-302e162784ea.jpg) - -#### 规则三:volatile 变量规则 - -> Volatile Variable Rule - -对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。 - -![img](https://gitee.com/duhouan/ImagePro/raw/master/pics/concurrent/5e6e05d6-1028-4f5c-b9bd-1a40b90d6070.jpg) - -#### 规则四:线程启动规则 - -> Thread Start Rule - -Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。 - -![img](https://gitee.com/duhouan/ImagePro/raw/master/pics/concurrent/bc5826f5-014d-47b4-9a76-d86b80968643.jpg) - -#### 规则五:线程加入规则 - -> Thread Join Rule - -Thread 对象的结束先行发生于 join() 方法返回。 - -![img](https://gitee.com/duhouan/ImagePro/raw/master/pics/concurrent/54e6d499-80df-488e-aa7e-081766c41538.jpg) - -#### 规则六:线程中断规则 - -> Thread Interruption Rule - -对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 interrupted() 方法检测到是否有中断发生。 - -#### 规则七:对象终结规则 +### 先行发生原则 -> Finalizer Rule +除了使用 volatile 和 synchronized 来保证有序性之外,JVM 还规定了先行发生原则,让一个操作无需控制就能先于另一个操作完成。 -一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。 +- 单一线程原则(Single Thread Rule):在一个线程内,在程序前面的操作先行发生于后面的操作。 +- 管程锁定规则(Monitor Lock Rule):一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。 +- volatile 变量规则(Volatile Variable Rule):对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。 +- 线程启动规则(Thread Start Rule):Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。 +- 线程加入规则(Thread Join Rule):Thread 对象的结束先行发生于 join() 方法返回。 +- 线程中断规则(Thread Interruption Rule):对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 interrupted() 方法检测到是否有中断发生。 +- 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。 +- 传递性(Transitivity):如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C。 -#### 规则八:传递性 +如果两个操作之间的关系不在此列,并且无法从以上规则中推导出来的话,则它们就没有顺序性保障,虚拟机可以对它们随意地进行重排序。 -> Transitivity -如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C。 -## 乐观锁和悲观锁 +## 乐观锁和悲观锁思想 -### 1. 悲观锁 +### 悲观锁 互斥同步最主要的问题就是线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。 @@ -213,13 +106,13 @@ Thread 对象的结束先行发生于 join() 方法返回。 Java 中 **synchronized** 和 **ReentrantLock** 等独占锁就是悲观锁思想的实现。 -### 2. 乐观锁 +### 乐观锁 乐观锁基于**冲突检测**的乐观并发策略:**先进行操作,如果没有其他线程争用共享数据,操作成功;如果数据存在竞争,就采用补偿措施(常见的有不断重试,直到成功)**。这种乐观的并发策略的许多实现是不需要将线程挂起的,因此这种同步操作称为**非阻塞同步**。 -### 3. CAS +### CAS -乐观锁需要**操作**和**冲突检测**这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。、 +乐观锁需要**操作**和**冲突检测**这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠**硬件**来完成。 硬件支持的原子性操作最典型的是:CAS(Compare-and-Swap)。 diff --git "a/docs/Java_Concurrency/3_\345\271\266\345\217\221\345\205\263\351\224\256\345\255\227.md" "b/docs/Java_Concurrency/3_\345\271\266\345\217\221\345\205\263\351\224\256\345\255\227.md" index e017026c..b5f51931 100644 --- "a/docs/Java_Concurrency/3_\345\271\266\345\217\221\345\205\263\351\224\256\345\255\227.md" +++ "b/docs/Java_Concurrency/3_\345\271\266\345\217\221\345\205\263\351\224\256\345\255\227.md" @@ -139,7 +139,7 @@ public class SynchronizedDemo { } ``` -
+
任意一个对象都拥有自己的 Monitor,当这个对象由同步块或者同步方法调用时, 执行方法的线程必须先获取该对象的 Monitor 才能进入同步块和同步方法, 如果没有获取到 Monitor 的线程将会被阻塞在同步块和同步方法的入口处,进入到 BLOCKED 状态。 @@ -159,12 +159,7 @@ synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令 JDK 1.6 之后对 synchronized 进行优化。 -锁的 4 种状态: - -- 无锁 -- 偏向锁 -- 轻量级锁 -- 重量级锁 +锁的 4 种状态:无锁、偏向锁、轻量级锁、重量级锁。 #### 1. 自旋锁 @@ -236,4 +231,7 @@ public static String concatString(String s1, String s2, String s3) { - volatile 修饰的变量不会被编译器优化 - synchronized 修饰的变量可以被编译器优化 \ No newline at end of file + synchronized 修饰的变量可以被编译器优化 + + + diff --git "a/docs/Java_Concurrency/4_Lock \344\275\223\347\263\273.md" "b/docs/Java_Concurrency/4_Lock \344\275\223\347\263\273.md" index aba74653..12de88f0 100644 --- "a/docs/Java_Concurrency/4_Lock \344\275\223\347\263\273.md" +++ "b/docs/Java_Concurrency/4_Lock \344\275\223\347\263\273.md" @@ -1,5 +1,27 @@ # Lock 体系 +## AQS + +AQS(AbtsractQueueSynchronized) 即同步队列器。 + +AQS 是一个**抽象类**,本身并没有实现任何同步接口的,只是通过提供**同步状态的获取和释放**来供自定义的同步组件使用。 + +AQS 的实现依赖内部的双向队列(底层是双向链表),称为同步队列。 + +如果当前线程获取同步状态失败,则会将该线程以及等待状态等信息封装为 Node,将其**加入同步队列的尾部,同时阻塞当前线程**,当同步状态释放时,唤醒队列的头结点。 + +AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。 + +```java +private transient volatile Node head; //同步队列的头结点 +private transient volatile Node tail; //同步队列的尾结点 +private volatile int state; //同步状态。 +// state=0,表示同步状态可用 +// state=1,表示同步状态已被占用 +``` + +补充:[AQS详解](https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html) + ## Condition 条件对象 条件对象是**线程同步对象中的一种**,主要用来等待某种条件的发生,条件发生后,可以唤醒等待在该条件上的一个线程或者所有线程。 @@ -11,61 +33,51 @@ ReentrantLock lock = new ReentrantLock(); Condition condition = lock.newCondition(); ``` +Condition 中有 awai()、signal()、signalAll() 方法,分别对应 Object 中放入 wait()、notify()、notifyAll() 方法,其实 Condition 也有上述三种方法,改变方法名称是为了避免使用上语义的混淆。 +补充:[Condition 详解](https://blog.csdn.net/qq_38293564/article/details/80554516) -注意: -- Condition 中有 await、signal、signalAll ,分别对应 Object 中放入 wait、notify、notifyAll 方法,其实 Condition 也有上述三种方法,改变方法名称是为了避免使用上语义的混淆。 - await 和 signal / signalAll 方法就像一个开关控制着线程 A(等待方)和线程 B(通知方)。 +## 可重入 - ![img](https://gitee.com/duhouan/ImagePro/raw/master/pics/blog/aqs_10.png) +某个线程试图获取一个已经由该线程持有的锁,那么这个请求就会成功。“重入”意味着获取的锁的操作的粒度是“线程”而不是“调用”。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。 - 线程 awaitThread 先通过 lock.lock() 方法获取锁成功后调用了 condition.await 方法进入**等待队列**, +重入的一种实现方法是:为每个锁关联一个**计数器**(方便解锁)和一个**所有者线程**(知道是哪个线程是可重入的),同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。 - 另一个线程 signalThread 通过 lock.lock() 方法获取锁成功后调用了 **condition.signal / signalAll, 使得线程 awaitThread 能够有机会移入到同步队列**中, 当其他线程释放 Lock 后使得线程 awaitThread 能够有机会获取 Lock, 从而使得线程 awaitThread 能够从 await 方法中退出,然后执行后续操作。 如果 awaitThread 获取 Lock 失败会直接进入到同步队列。 -- 一个 Lock 可以与多个 Condition 对象绑定。 -## AQS +## 公平锁与非公平锁 -AQS(AbtsractQueueSynchronized) 即同步队列器。 +公平锁是指多个线程在等待同一个锁时,**按照申请锁的顺序来依次获取锁**。 -AQS 是一个抽象类,本身并没有实现任何同步接口的,只是通过提供**同步状态的获取和释放**来供自定义的同步组件使用。 +### 公平锁 -AQS 的实现依赖内部的双向队列(底层是双向链表)。 +- 公平锁每次获取到锁为同步队列中的第一个节点,保证请求资源时间上的绝对顺序。 -如果当前线程获取同步状态失败,则会将该线程以及等待状态等信息封装为 Node,将其**加入同步队列的尾部,同时阻塞当前线程**,当同步状态释放时,唤醒队列的头结点。 +- 公平锁为了保证时间上的绝对顺序,需要频繁的上下文切换,性能较差 -```java -private transient volatile Node head; //同步队列的头结点 -private transient volatile Node tail; //同步队列的尾结点 -private volatile int state; //同步状态。 -// state=0,表示同步状态可用;state=1,表示同步状态已被占用 -``` +### 非公平锁 -## 可重入 +- 非公平锁有可能刚释放锁的线程下次继续获取该锁,则有可能导致其他线程永远无法获取到锁,造成 “饥饿” 现象。 -某个线程试图获取一个已经有该线程持有的锁,那么这个请求就会成功。“重入”意味着获取的锁的操作的粒度是“线程”而不是“调用”。重入的一种实现方法是,为每个锁关联一个**计数器**(方便解锁)和一个**所有者线程**(知道是哪个线程是可重入的)。 +- 非公平锁会**降低一定的上下文切换**,降低性能开销。**ReentrantLock 默认选择的是非公平锁**。 -## 公平锁与非公平锁 - -公平锁是指多个线程在等待同一个锁时,**按照申请锁的顺序来依次获取锁**。 -| 公平锁 | 非公平锁 | -| :----------------------------------------------------------: | :----------------------------------------------------------: | -| 公平锁每次获取到锁为同步队列中的第一个节点,
保证请求资源时间上的绝对顺序 | 非公平锁有可能刚释放锁的线程下次继续获取该锁,
则有可能导致其他线程永远无法获取到锁,造成“饥饿”现象。 | -| 公平锁为了保证时间上的绝对顺序,
需要频繁的上下文切换 | 非公平锁会**降低一定的上下文切换**,降低性能开销
因此,ReentrantLock 默认选择的是非公平锁 | ## 独占锁和共享锁 -独占锁模式下,每次只能有一个线程能持有锁,ReentrantLock 就是以独占方式实现的互斥锁。 +### 独占锁 + +- 独占锁模式下,每次只能有一个线程能持有锁,ReentrantLock 就是以独占方式实现的互斥锁。 +- 独占锁是一种悲观保守的加锁策略,它**避免了读/读冲突**,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。 + +### 共享锁 -共享锁,则允许多个线程同时获取锁,并发访问共享资源,如:ReadWriteLock。 +- 共享锁允许多个线程同时获取锁,并发访问共享资源,如:ReadWriteLock。 +- 共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。 -很显然,独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。 -共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。 ## ReentrantLock @@ -85,11 +97,11 @@ static final class NonfairSync extends Sync { } ``` -- Sync 是一个继承 AQS 的抽象类,并发控制就是通过 Sync 实现的。 +- Sync 是一个继承 AQS 的抽象类,并重写了 tryRelease() 方法,并发控制就是通过 Sync 实现的。 - 重写了 tryRelease() , 有两个子类 FiarSync 和 NonfairSync,即公平锁和非公平锁。 + 此外 Sync 还有两个子类 FiarSync 和 NonfairSync,即公平锁和非公平锁。 -- 由于 Sync 重写 tryRealese() 方法,并且 FairSync 和 NonfairSync没有再次重写该方法,所以 **公平锁和非公平锁释放锁的操作是一样的**,即**唤醒等待队列中第一个被挂起的线程**。 +- 由于 Sync 重写 tryRealese() 方法,并且 FairSync 和 NonfairSync 没有再次重写该方法,所以**公平锁和非公平锁释放锁的操作是一样的**,即**唤醒等待队列中第一个被挂起的线程**。 - 公平锁和非公平锁获取锁的方式是不同的。 @@ -97,17 +109,19 @@ static final class NonfairSync extends Sync { 非公平锁获取锁的方式是一种**抢占式**的,不考虑线程等待时间,无论是哪个线程获取了锁,则其他线程就进入等待队列。 -```java -private final Sync sync; - -public ReentrantLock() { //默认是非公平锁 - sync = new NonfairSync(); -} +- ReentrantLock 默认选择的是非公平锁。 -public ReentrantLock(boolean fair) { //可设置为公平锁 - sync = fair ? new FairSync() : new NonfairSync(); -} -``` + ```java + private final Sync sync; + + public ReentrantLock() { // 默认是非公平锁 + sync = new NonfairSync(); + } + + public ReentrantLock(boolean fair) { // 可设置为公平锁 + sync = fair ? new FairSync() : new NonfairSync(); + } + ``` ## ReentrantLock 与 synchronized 的区别 @@ -137,17 +151,15 @@ public ReentrantLock(boolean fair) { //可设置为公平锁 ## LockSupport -LockSupport 位于 java.util.concurrent.locks 包下。 LockSupprot 是线程的**阻塞原语**,用来**阻塞线程**和**唤醒线程**。 - -每个使用 LockSupport 的线程都会与一个许可关联, +LockSupport 位于 java.util.concurrent.locks 包下。 -如果该许可可用,并且可在线程中使用,则调用 park() 将会立即返回,否则可能阻塞。 +LockSupprot 是线程的**阻塞原语**,用来**阻塞线程**和**唤醒线程**。 -如果许可尚不可用,则可以调用 unpark 使其可用。 +每个使用 LockSupport 的线程都会与一个许可关联, 如果该许可可用,并且可在线程中使用,则调用 park() 将会立即返回,否则可能阻塞。 如果许可尚不可用,则可以调用 unpark() 方法使其可用。 但是注意**许可不可重入**,也就是说只能调用一次 park() 方法,否则会一直阻塞。 -### LockSupport 中方法 +### 成员方法 | 方法 | 说明 | | :-------------------------------------------: | :----------------------------------------------------------: | @@ -163,40 +175,39 @@ LockSupport 位于 java.util.concurrent.locks 包下。 LockSupprot 是线程的 - 调用 park() 方法 dump 线程: -``` +```html "main" #1 prio=5 os_prio=0 tid=0x02cdcc00 nid=0x2b48 waiting on condition [0x00d6f000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:304) at learn.LockSupportDemo.main(LockSupportDemo.java:7) -Copy to clipboardErrorCopied ``` - 调用 park(Object blocker) 方法 dump 线程: -```java +```html "main" #1 prio=5 os_prio=0 tid=0x0069cc00 nid=0x6c0 waiting on condition [0x00dcf000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x048c2d18> (a java.lang.String) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) - at learn.LockSupportDemo.main(LockSupportDemo.java:7)Copy to clipboardErrorCopied + at learn.LockSupportDemo.main(LockSupportDemo.java:7) ``` 通过分别调用这两个方法然后 dump 线程信息可以看出, 带 Object 的 park 方法相较于无参的 park 方法会增加 ```java -- parking to wait for <0x048c2d18> (a java.lang.String)Copy to clipboardErrorCopied +- parking to wait for <0x048c2d18> (a java.lang.String) ``` 这种信息就类似于记录“案发现场”,有助于工程人员能够迅速发现问题解决问题。 注意: -- synchronized 使线程阻塞,线程会进入到 BLOCKED 状态 -- 调用 LockSupprt 方法阻塞线程会使线程进入到 WAITING 状态 +- synchronized 使线程阻塞,线程会进入到 Blocked 状态(阻塞状态) +- 调用 LockSupport 方法阻塞线程会使线程进入到 Waiting 状态(等待状态) -### LockSupport 使用示例 +### 使用示例 ```java import java.util.concurrent.locks.LockSupport; diff --git "a/docs/Java_Concurrency/5_\345\216\237\345\255\220\346\223\215\344\275\234\347\261\273.md" "b/docs/Java_Concurrency/5_\345\216\237\345\255\220\346\223\215\344\275\234\347\261\273.md" index 879713c3..a6a706d3 100644 --- "a/docs/Java_Concurrency/5_\345\216\237\345\255\220\346\223\215\344\275\234\347\261\273.md" +++ "b/docs/Java_Concurrency/5_\345\216\237\345\255\220\346\223\215\344\275\234\347\261\273.md" @@ -1,5 +1,7 @@ # 原子操作类 +## AtomicInteger 源码解析 + `java.util.concurrent.atomic`下的所有原子操作类都实现了 CAS。 AtomicInteger 内部维护一个变量 **Unsafe**: @@ -49,7 +51,31 @@ public final int getAndAddInt(Object var1, long var2, int var4) { } ``` -## 1. 原子更新基本类 +除了 CAS 操作外,AtomicInteger 还使用 volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。 + +```java + +private static final Unsafe unsafe = Unsafe.getUnsafe(); +private static final long valueOffset; + +static { + try { + valueOffset = unsafe.objectFieldOffset + (AtomicInteger.class.getDeclaredField("value")); + } catch (Exception ex) { throw new Error(ex); } +} + +private volatile int value; +``` + +UnSafe 类的 objectFieldOffset() 方法是一个 native 方法,这个方法是用来拿到 “原来的值” 的内存地址,返回值是 valueOffset。 + +另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。 + + + +## 原子更新基本类 + atomic 包提高原子更新基本类的工具类,如下: - AtomicBoolean @@ -133,7 +159,7 @@ public final boolean compareAndSet(boolean expect, boolean update) { ``` 可以看出,compareAndSet 方法的实际上也是先转换成 0,1 的整型变量,然后是通过针对 int 型变量的原子更新方法 compareAndSwapInt 来实现的。 -## 2. 原子更新数组 +## 原子更新数组 atomic 包下提供能原子更新数组中元素的类有: - AtomicIntegerArray @@ -170,7 +196,10 @@ public class AtomicIntegerArrayDemo { 2 //注意仍然返回原来的旧值 ``` -## 3. 原子更新引用类型 + + +## 原子更新引用类型 + 如果需要原子更新引用类型变量的话,为了保证线程安全,atomic 也提供了相关的类: - AtomicReference @@ -219,7 +248,10 @@ User{userName='b', age=2} ``` 首先将对象 user1 用 AtomicReference 进行封装,然后调用 getAndSet 方法,从结果可以看出,该方法会原子更新引用的 User 对象,变为`User{userName='b', age=2}`,返回的是原来的User 对象`User{userName='a', age=1}`。 -## 4. 原子更新字段类型 + + +## 原子更新字段类型 + 如果需要更新对象的某个字段,并在多线程的情况下,能够保证线程安全,atomic同样也提供了相应的原子操作类: - AtomicIntegeFieldUpdater diff --git "a/docs/Java_Concurrency/6_\345\271\266\345\217\221\345\256\271\345\231\250.md" "b/docs/Java_Concurrency/6_\345\271\266\345\217\221\345\256\271\345\231\250.md" index ceab71e6..910c97a6 100644 --- "a/docs/Java_Concurrency/6_\345\271\266\345\217\221\345\256\271\345\231\250.md" +++ "b/docs/Java_Concurrency/6_\345\271\266\345\217\221\345\256\271\345\231\250.md" @@ -113,7 +113,7 @@ final Segment[] segments; static final int DEFAULT_CONCURRENCY_LEVEL = 16; ``` -
+
### 2. size 操作 @@ -244,34 +244,3 @@ static final Node tabAt(Node[] tab, int i) { - JDK1.7的ConcurrentHashMap(分段锁)对整个桶数组进行了分割分段(Segment), 每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问度。 JDK 1.8 采用**数组+链表/红黑二叉树**的数据结构来实现,并发控制使用**synchronized和CAS**来操作。 - Hashtable:使用 synchronized 来保证线程安全,效率非常低下。 当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态, 如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈。 -Hashtable 全表锁 - -![img](https://gitee.com/duhouan/ImagePro/raw/master/java-notes/java/16_03.jpg) - -ConcurrentHashMap 分段锁 - -![img](https://gitee.com/duhouan/ImagePro/raw/master/java-notes/java/16_00.jpg) - -# 补充 - -## 红黑树性质 - -1. 每个节点或者是红色的,或者是黑色的 - -2. 根节点是黑色的 - -3. 每一个叶子节点(最后的空节点)是黑色的 - - > 这其实是一个定义 - -4. 如果一个节点是红色的,那么它们的孩子节点都是黑色的 - - > 因为红节点和其父亲节点表示一个3节点,黑色节点的右孩子一定是黑色节点。 - -5. 从任意一个节点到叶子节点经过的**黑色节点**都是一样的 - - > 因为2-3树是绝对平衡的 - -![img](https://gitee.com/duhouan/ImagePro/raw/master/java-notes/dataStructure/redBlackTree//12_11.png) - -相较于 AVL 树,**红黑树上的查找的操作的效率的低于 AVL 树的。但对于增删操作,红黑树的性能是更优的。** \ No newline at end of file diff --git "a/docs/Java_Concurrency/7_\345\271\266\345\217\221\345\267\245\345\205\267.md" "b/docs/Java_Concurrency/7_\345\271\266\345\217\221\345\267\245\345\205\267.md" index 9e7897e6..35812b70 100644 --- "a/docs/Java_Concurrency/7_\345\271\266\345\217\221\345\267\245\345\205\267.md" +++ "b/docs/Java_Concurrency/7_\345\271\266\345\217\221\345\267\245\345\205\267.md" @@ -1,22 +1,270 @@ -# 并发工具 +# 一、并发容器 + +## CopyOnWriteArrayList + +### 1. 读写分离 + +**写操作在一个复制的数组**上进行,**读操作还是在原数组**中进行,**读写分离**,互不影响。 + +写操作需要加锁,防止并发写入时导致写入数据丢失。 + +写操作结束之后需要把原数组指向新的复制数组。 + +```java +//写操作: +//通过过创建底层数组的新副本来实现的。 +//当 List 需要被修改的时候,并不修改原有内容,而是对原有数据进行一次复制,将修改的内容写入副本。 +//写完之后,把原数组指向新的复制数组。 +//这样可以保证写操作实在一个复制的数组上进行,而读操作还是在原数组中进行,不会影响读操作。 +public boolean add(E e) { + //加锁 + final ReentrantLock lock = this.lock; + lock.lock(); + try { + Object[] elements = getArray(); + int len = elements.length; + // newElements 是一个复制的数组 + Object[] newElements = Arrays.copyOf(elements, len + 1); + newElements[len] = e; + // 写操作在一个复制的数组上进行 + setArray(newElements); + return true; + } finally { + lock.unlock(); + } +} + +final void setArray(Object[] a) { + array = a; +} +``` + +```java +//读操作 +//读操作没有任何同步控制和锁操作, +//因为内部数组 array 不会被修改。 +private transient volatile Object[] array; + +public E get(int index) { + return get(getArray(), index); +} + +@SuppressWarnings("unchecked") +private E get(Object[] a, int index) { + return (E) a[index]; +} + +final Object[] getArray() { + return array; +} +``` + +### 2. 适用场景 + +CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读操作的性能,很适合**读多写少**的应用场景。 + +CopyOnWriteArrayList 有其缺陷: + +- 内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右; +- 数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。 + +所以 CopyOnWriteArrayList 不适合**内存敏感**以及对**实时性要求很高**的场景。 + +## ConcurrentHashMap + +### 1. 存储结构 + +```java +static final class HashEntry { + final int hash; + final K key; + volatile V value; + volatile HashEntry next; +} +``` + +ConcurrentHashMap 采用了**分段锁**(Segment),每个分段锁维护着几个桶(HashEntry),**多个线程可以同时访问不同分段锁上的桶**, 从而使其并发度更高(并发度就是 Segment 的个数)。 + +```java +//Segment 继承自 ReentrantLock。 +static final class Segment extends ReentrantLock implements Serializable { + + private static final long serialVersionUID = 2249069246763182397L; + + static final int MAX_SCAN_RETRIES = + Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1; + + transient volatile HashEntry[] table; + + transient int count; + + transient int modCount; + + transient int threshold; + + final float loadFactor; +} +``` + +```java +final Segment[] segments; + +//默认的并发级别为 16,也就是说默认创建 16 个 Segment。 +static final int DEFAULT_CONCURRENCY_LEVEL = 16; +``` + +
+ +### 2. size 操作 + +每个 Segment 维护了一个 count 变量来统计该 Segment 中的键值对个数。 + +```java +/** + * The number of elements. Accessed only either within locks + * or among other volatile reads that maintain visibility. + */ +transient int count;Copy to clipboardErrorCopied +``` + +在执行 size 操作时,需要遍历所有 Segment 然后把 count 累计起来。 + +ConcurrentHashMap 在执行 size 操作时**先尝试不加锁,如果连续两次不加锁操作得到的结果一致,那么可以认为这个结果是正确的**。 + +尝试次数使用 RETRIES_BEFORE_LOCK 定义,该值为 2,retries 初始值为 -1,因此尝试次数为 3。 + +如果尝试的次数超过 3 次,就需要对每个 Segment 加锁。 + +```java +/** + * Number of unsynchronized retries in size and containsValue + * methods before resorting to locking. This is used to avoid + * unbounded retries if tables undergo continuous modification + * which would make it impossible to obtain an accurate result. + */ +static final int RETRIES_BEFORE_LOCK = 2; + +public int size() { + // Try a few times to get accurate count. On failure due to + // continuous async changes in table, resort to locking. + final Segment[] segments = this.segments; + int size; + boolean overflow; // true if size overflows 32 bits + long sum; // sum of modCounts + long last = 0L; // previous sum + int retries = -1; // first iteration isn't retry + try { + for (;;) { + // 超过尝试次数,则对每个 Segment 加锁 + if (retries++ == RETRIES_BEFORE_LOCK) { + for (int j = 0; j < segments.length; ++j) + ensureSegment(j).lock(); // force creation + } + sum = 0L; + size = 0; + overflow = false; + for (int j = 0; j < segments.length; ++j) { + Segment seg = segmentAt(segments, j); + if (seg != null) { + sum += seg.modCount; + int c = seg.count; + if (c < 0 || (size += c) < 0) + overflow = true; + } + } + // 连续两次得到的结果一致,则认为这个结果是正确的 + if (sum == last) + break; + last = sum; + } + } finally { + if (retries > RETRIES_BEFORE_LOCK) { + for (int j = 0; j < segments.length; ++j) + segmentAt(segments, j).unlock(); + } + } + return overflow ? Integer.MAX_VALUE : size; +}Copy to clipboardErrorCopied +``` + +### 3. JDK 1.8 的改动 + +ConcurrentHashMap 取消了 Segment 分段锁。 + +JDK 1.8 使用 **CAS 操作**来支持更高的并发度,在 CAS 操作失败时使用**内置锁 synchronized**。 + +数据结构与HashMap 1.8 的结构类似,数组+链表 / 红黑二叉树(链表长度 > 8 时,转换为红黑树 )。synchronized 只锁定当前**链表或红黑二叉树的首节点**,这样只要 Hash 值不冲突,就不会产生并发。 + +### 4. JDK 1.8 中的 put 方法 + +(1)hash 算法 + +```java +static final int spread(int h) { + return (h ^ (h >>> 16)) & HASH_BITS; +} +``` + +(2)定位索引位置 + +```java +i = (n - 1) & hash +``` + +(3)获取 table 中对应索引的元素 f + +```java +f = tabAt(tab, i = (n - 1) & hash +``` + +```java +// Unsafe.getObjectVolatile 获取 f +// 因为可以直接指定内存中的数据,保证了每次拿到的数据都是新的 +static final Node tabAt(Node[] tab, int i) { + return (Node)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE); +} +``` + +(4)如果 f 是 null,说明 table 中是第一次插入数据,利用 + +- 如果 CAS 成功,说明 Node 节点插入成功 +- 如果 CAS 失败,说明有其他线程提前插入了节点,**自旋重新尝试**在该位置插入 Node + +(5)其余情况把新的 Node 节点按链表或红黑树的方式插入到合适位置,这个过程采用内置锁实现并发。 + +### 5. 和 Hashtable 的区别 + +**底层数据结构:** + +- JDK1.7 的ConcurrentHashMap底层采用**分段的数组+链表**实现, JDK1.8 的ConcurrentHashMap底层采用的数据结构与JDK1.8 的HashMap的结构一样,**数组+链表/红黑二叉树**。 +- Hashtable和JDK1.8 之前的HashMap的底层数据结构类似都是采用**数组+链表**的形式, 数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。 + +**实现线程安全的方式** + +- JDK1.7的ConcurrentHashMap(分段锁)对整个桶数组进行了分割分段(Segment), 每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问度。 JDK 1.8 采用**数组+链表/红黑二叉树**的数据结构来实现,并发控制使用**synchronized和CAS**来操作。 +- Hashtable:使用 synchronized 来保证线程安全,效率非常低下。 当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态, 如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈。 + + + +# 二、并发工具 ## J.U.C - AQS java.util.concurrent(J.U.C)大大提高了并发性能,AQS 被认为是 J.U.C 的核心。 -### 1. CountDownLatch +### CountDownLatch 用来控制一个或者多个线程等待多个线程。 维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些因为调用 await() 方法而在等待的线程就会被唤醒。 -![img](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/ba078291-791e-4378-b6d1-ece76c2f0b14.png) +
```java public class CountdownLatchExample { public static void main(String[] args) throws InterruptedException { - final int totalThread = 10; + final int totalThread = 3; CountDownLatch countDownLatch = new CountDownLatch(totalThread); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < totalThread; i++) { @@ -33,10 +281,10 @@ public class CountdownLatchExample { ``` ```html -run..run..run..run..run..run..run..run..run..run..end +run..run..run..end ``` -### 2. CyclicBarrier +### CyclicBarrier 用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行。 @@ -59,7 +307,7 @@ public CyclicBarrier(int parties) { } ``` -![img](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/f71af66b-0d54-4399-a44b-f47b58321984.png) +
```java public class CyclicBarrierExample { @@ -88,7 +336,7 @@ public class CyclicBarrierExample { before..before..before..before..before..before..before..before..before..before..after..after..after..after..after..after..after..after..after..after.. ``` -### 3. Semaphore +### Semaphore Semaphore 类似于操作系统中的信号量,可以控制对互斥资源的访问线程数。 @@ -144,7 +392,7 @@ Thread: 8 Thread: 9 ``` -### 4. CountDownLatch 和 CyclicBarrier 比较 +### CountDownLatch 和 CyclicBarrier 比较 - **循环使用** @@ -172,7 +420,7 @@ Thread: 9 ## J.U.C - 其它组件 -### 1. FutureTask +### FutureTask 在介绍 Callable 时我们知道它可以有返回值,返回值通过 Future 进行封装。FutureTask 实现了 RunnableFuture 接口,该接口继承自 Runnable 和 Future 接口,这使得 FutureTask 既可以当做一个任务执行,也可以有返回值。 @@ -222,7 +470,7 @@ other task is running... 4950 ``` -### 2. BlockingQueue +### BlockingQueue java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现: @@ -299,7 +547,7 @@ consume... -### 3. ForkJoin +### ForkJoin 主要用于并行计算中,和 MapReduce 原理类似,都是把大的计算任务拆分成多个小任务并行计算。 diff --git "a/docs/Java_Concurrency/8_\347\272\277\347\250\213\346\261\240.md" "b/docs/Java_Concurrency/8_\347\272\277\347\250\213\346\261\240.md" index e8c38876..3dc260da 100644 --- "a/docs/Java_Concurrency/8_\347\272\277\347\250\213\346\261\240.md" +++ "b/docs/Java_Concurrency/8_\347\272\277\347\250\213\346\261\240.md" @@ -6,7 +6,7 @@ Executor 框架是在 Java5 中引入的,**通过该框架来控制线程的 Executor 基于**生产者-消费者模式**,**提交任务的线程相当于生产者,执行任务的线程相当于消费者**。同时,Executor 的实现还提供了对任务执行的生命周期管理的支持。 -![img](https://gitee.com/duhouan/ImagePro/raw/master/pics/concurrent/c_19.png) +
- Executor @@ -48,7 +48,7 @@ Eexcutor 框架由 3 大部分组成: 异步任务需要返回结果,提交任务后需要返回 Future, FutureTask 实现。 -
+
运行过程: @@ -190,7 +190,7 @@ private static ExecutorService executor = 状态转换图: -![img](https://gitee.com/duhouan/ImagePro/raw/master/pics/concurrent/c_22.png) +![img](https://github.com/DuHouAn/ImagePro/raw/master/pics/concurrent/c_22.png) shutdown() 和 shutdownNow() 这两个方法的原理都是**遍历线程池中所有的线程,然后依次中断线程**。 shutdown() 和 shutdownNow() 还是有不一样的地方: @@ -205,9 +205,9 @@ shutdown() 和 shutdownNow() 这两个方法的原理都是**遍历线程池中 #### 1. 任务的性质 -**CPU 密集型任务**配置尽可能少的线程数量,如配置 (N cpu) + 1 个线程的线程池。 +**CPU 密集型任务**(指利用 CPU 计算能力的任务,比如在内存中对大量数据进行排序)配置尽可能少的线程数量,如配置 (N cpu) + 1 个线程的线程池。 -**IO 密集型任务**则由于需要等待 IO 操作,线程并不是一直在执行任务,则配置尽可能多的线程,如 2 * (N cpu) +1。 +**IO 密集型任务**(比如网络读取、文件读取)则由于需要等待 IO 操作,线程并不是一直在执行任务,则配置尽可能多的线程,如 2 * (N cpu) +1。 **混合型的任务**,如果可以拆分,则将其拆分成一个 CPU 密集型任务和一个 IO 密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。 diff --git "a/docs/Java_Concurrency/9_\345\271\266\345\217\221\345\256\236\350\267\265.md" "b/docs/Java_Concurrency/9_\345\271\266\345\217\221\345\256\236\350\267\265.md" index ae614b5d..3707d113 100644 --- "a/docs/Java_Concurrency/9_\345\271\266\345\217\221\345\256\236\350\267\265.md" +++ "b/docs/Java_Concurrency/9_\345\271\266\345\217\221\345\256\236\350\267\265.md" @@ -1,6 +1,143 @@ -# 并发实践 +# 一、死锁 -## 线程不安全示例 +## 线程死锁 + +**死锁是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待现象**。 + +假设线程 A 持有资源 1,线程 B 持有资源 2,它们同时都想申请对方的资源,那么这两个线程就会互相等待而进入死锁状态。 + +
+ +使用 Java 代码模拟上述死锁场景: + +```java +public class Resources { + public static final Object resource1 = new Object(); // 资源 1 + public static final Object resource2 = new Object(); // 资源 2 +} +``` + +```java +public class ThreadA extends Thread{ + + @Override + public void run() { + synchronized (Resources.resource1) { + System.out.println(Thread.currentThread().getName() + " get Resource-1"); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println(Thread.currentThread().getName() + " waiting get Resource-2"); + synchronized (Resources.resource2) { + System.out.println(Thread.currentThread().getName() + " get Resource-2"); + } + } + } +} +``` + +```java +public class ThreadB extends Thread{ + + @Override + public void run() { + synchronized (Resources.resource2) { + System.out.println(Thread.currentThread().getName() + " get Resource-2"); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println(Thread.currentThread().getName() + " waiting get Resource-1"); + synchronized (Resources.resource1) { + System.out.println(Thread.currentThread().getName() + " get Resource-1"); + } + } + } +} +``` + +```java +public class DeadLockDemo { + public static void main(String[] args) { + Thread threadA=new ThreadA(); + threadA.setName("Thread A"); + threadA.start(); + + Thread threadB=new ThreadB(); + threadB.setName("Thread B"); + threadB.start(); + } +} +``` + +输出结果: + +```html +Thread A get Resource-1 +Thread B get Resource-2 +Thread B waiting get Resource-1 +Thread A waiting get Resource-2 +``` + +线程 A 通过 `synchronized (resource1)` 获得 `resource1` 的监视器锁,然后线程 A 休眠 1s,执行线程 B 获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。 + +上面的例子符合产生死锁的四个必要条件: + +- 互斥:每个资源要么已经分配给了一个进程,要么就是可用的。 +- 占有和等待:已经得到了某个资源的进程可以再请求新的资源。 +- 不可抢占:已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显式地释放。 +- 环路等待:有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。 + +## 死锁预防 + +在程序运行之前预防发生死锁。 + +- 破坏占有和等待条件:一种实现方式是规定所有进程在开始执行前请求所需要的全部资源。 +- 破坏不可抢占条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。 +- 破坏环路等待条件:给资源统一编号,进程只能按编号顺序来请求资源。 + +我们对线程 B 的代码进行如下改造,就不会出现死锁了。 + +```JAVA +public class ThreadB extends Thread{ + + @Override + public void run() { + synchronized (Resources.resource1) { + System.out.println(Thread.currentThread().getName() + " get Resource-1"); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println(Thread.currentThread().getName() + " waiting get Resource-2"); + synchronized (Resources.resource1) { + System.out.println(Thread.currentThread().getName() + " get Resource-2"); + } + } + } +} +``` + +输出结果: + +```html +Thread A get Resource-1 +Thread A waiting get Resource-2 +Thread A get Resource-2 +Thread B get Resource-1 +Thread B waiting get Resource-2 +Thread B get Resource-2 +``` + +线程 A 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了环路等待条件,因此避免了死锁。 + + + +# 二、线程不安全示例 如果多个线程对同一个共享数据进行访问而不采取同步操作的话,那么操作的结果是不一致的。 @@ -176,19 +313,15 @@ public static void main(String[] args) throws InterruptedException { 1000 ``` -## 线程安全 - -多个线程不管以何种方式访问某个类,并且在**主调代码中不需要进行同步**,都能表现正确的行为。 -Servlet 存在线程安全问题,原因如下: -Servlet 在 Tomcat 中是以**单例模式**存在的,也就是说,当客户端第一次请求 Servlet 时,Tomcat 会根据 web.xml 生成对应的 Servlet 实例,当客户端第二次请求的是同一个 Servlet,Tomcat 不会再生成新的TServlet。**不同请求共享同一 Servlet 实例的资源,从而导致线程不安全的问题**。 +# 三、实现线程安全方式 -多个线程不管以何种方式访问某个类,并且在主调代码中不需要进行同步,都能表现正确的行为。 +所谓线程安全就是说,多个线程不管以何种方式访问某个类,并且在**主调代码中不需要进行同步**,都能表现正确的行为。 -线程安全有以下几种实现方式: +线程安全有以下几种实现方式:不可变、互斥同步、非阻塞同步、无同步方案(栈封闭和线程本地存储) -### 方式一:不可变 +## 不可变 不可变(Immutable)的对象一定是线程安全的,不需要再采取任何的线程安全保障措施。只要一个不可变的对象被正确地构建出来,永远也不会看到它在多个线程之中处于不一致的状态。多线程环境下,应当尽量使对象成为不可变,来满足线程安全。 @@ -208,10 +341,13 @@ public class ImmutableExample { Map unmodifiableMap = Collections.unmodifiableMap(map); unmodifiableMap.put("a", 1); } -}Copy to clipboardErrorCopied +} +``` + +```html Exception in thread "main" java.lang.UnsupportedOperationException at java.util.Collections$UnmodifiableMap.put(Collections.java:1457) - at ImmutableExample.main(ImmutableExample.java:9)Copy to clipboardErrorCopied + at ImmutableExample.main(ImmutableExample.java:9) ``` Collections.unmodifiableXXX() 先对原始的集合进行拷贝,需要对集合进行修改的方法都直接抛出异常。 @@ -219,22 +355,40 @@ Collections.unmodifiableXXX() 先对原始的集合进行拷贝,需要对集 ```java public V put(K key, V value) { throw new UnsupportedOperationException(); -}Copy to clipboardErrorCopied +} ``` -### 方式二:互斥同步 +## 互斥同步 + +互斥同步最主要的问题就是线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。 + +互斥同步属于一种悲观的并发策略:**总是认为只要不去做正确的同步措施,那就肯定会出现问题**。无论共享数据是否真的会出现竞争,它都要进行加锁。 + +Java 中 **synchronized** 和 **ReentrantLock** 等独占锁就是悲观锁思想的实现。 + +## 非阻塞同步 + +乐观锁基于**冲突检测**的乐观并发策略:**先进行操作,如果没有其他线程争用共享数据,操作成功;如果数据存在竞争,就采用补偿措施(常见的有不断重试,直到成功)**。这种乐观的并发策略的许多实现是不需要将线程挂起的,因此这种同步操作称为**非阻塞同步**。 + +### 1. CAS + +乐观锁需要**操作**和**冲突检测**这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。、 + +硬件支持的原子性操作最典型的是:CAS(Compare-and-Swap)。 + +当多个线程尝试使用 CAS 同时更新一个共享变量时,只有其中一个线程能够更新共享变量中的值,其他线程都失败,失败的线程不会被挂起,而是被告知在这次竞争中失败,并且可以再次尝试。 -synchronized 和 ReentrantLock。 +CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。 -### 方式三:非阻塞同步 +### 2. 原子操作类 -原子操作类(Unsafe 的 CAS 操作)。 +J.U.C 包里面的原子操作类的方法调用了 Unsafe 类的 CAS 操作。 -### 方式四:无同步方案 +## 无同步方案 要保证线程安全,并不是一定就要进行同步。如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性。 -#### 1. 栈封闭 +### 1. 栈封闭 **多个线程访问同一个方法的局部变量**时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的。 @@ -247,27 +401,35 @@ public class StackClosedExample { } System.out.println(cnt); } -}Copy to clipboardErrorCopied +} +``` + +```java public static void main(String[] args) { StackClosedExample example = new StackClosedExample(); ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(() -> example.add100()); executorService.execute(() -> example.add100()); executorService.shutdown(); -}Copy to clipboardErrorCopied +} +``` + +```html +100 100 -100Copy to clipboardErrorCopied ``` -#### 2. 线程本地存储(Thread Local Storage) + + +### 2. 线程本地存储(Thread Local Storage) 如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。 符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完。其中最重要的一个应用实例就是经典 Web 交互模型中的“一个请求对应一个服务器线程”(Thread-per-Request)的处理方式,这种处理方式的广泛应用使得很多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题。 -可以使用 java.lang.ThreadLocal 类来实现线程本地存储功能。 +可以**使用 java.lang.ThreadLocal 类来实现线程本地存储功能**。如果创建一个 ThreadLocal 变量,那么访问这个变量的**每个线程都会有这个变量的一个副本**,在实际多线程操作的时候,操作的是**自己本地内存中的变量**,从而规避了线程安全问题。 -对于以下代码,thread1 中设置 threadLocal 为 1,而 thread2 设置 threadLocal 为 2。过了一段时间之后,thread1 读取 threadLocal 依然是 1,不受 thread2 的影响。 +#### ThreadLocal 示例 ```java public class ThreadLocalExample { @@ -290,54 +452,44 @@ public class ThreadLocalExample { thread1.start(); thread2.start(); } -}Copy to clipboardErrorCopied -1Copy to clipboardErrorCopied +} ``` -为了理解 ThreadLocal,先看以下代码: - -```java -public class ThreadLocalExample1 { - public static void main(String[] args) { - ThreadLocal threadLocal1 = new ThreadLocal(); - ThreadLocal threadLocal2 = new ThreadLocal(); - Thread thread1 = new Thread(() -> { - threadLocal1.set(1); - threadLocal2.set(1); - }); - Thread thread2 = new Thread(() -> { - threadLocal1.set(2); - threadLocal2.set(2); - }); - thread1.start(); - thread2.start(); - } -}Copy to clipboardErrorCopied +```html +1 ``` -它所对应的底层结构图为: +thread1 中设置 threadLocal 为 1,而 thread2 设置 threadLocal 为 2。过了一段时间之后,thread1 读取 threadLocal 依然是 1,不受 thread2 的影响。 -![img](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/6782674c-1bfe-4879-af39-e9d722a95d39.png) +#### ThreadLocal 原理 每个 Thread 都有一个 ThreadLocal.ThreadLocalMap 对象。 ```java -/* ThreadLocal values pertaining to this thread. This map is maintained - * by the ThreadLocal class. */ -ThreadLocal.ThreadLocalMap threadLocals = null;Copy to clipboardErrorCopied +public class Thread implements Runnable { + + // 与此线程有关的 ThreadLocal 值,由 ThreadLocal 类维护 + ThreadLocal.ThreadLocalMap threadLocals = null; +} ``` -当调用一个 ThreadLocal 的 set(T value) 方法时,先得到当前线程的 ThreadLocalMap 对象,然后将 ThreadLocal->value 键值对插入到该 Map 中。 +默认情况下 threadLocals 变量值为 null,只有当前线程调用 ThreadLocal 类的 set() 或 get() 方法时才创建,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap 类对应的 get()、set()方法。 + +当调用一个 ThreadLocal 的 set(T value) 方法时,先得到当前线程的 ThreadLocalMap 对象,然后将 `` 键值对插入到该 Map 中。**变量是存放在当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 中**。 ```java public void set(T value) { Thread t = Thread.currentThread(); - ThreadLocalMap map = getMap(t); + ThreadLocalMap map = getMap(t); // 获取当前线程的 ThreadLocalMap 对象 if (map != null) map.set(this, value); else createMap(t, value); -}Copy to clipboardErrorCopied +} + +ThreadLocalMap getMap(Thread t) { + return t.threadLocals; +} ``` get() 方法类似。 @@ -345,7 +497,7 @@ get() 方法类似。 ```java public T get() { Thread t = Thread.currentThread(); - ThreadLocalMap map = getMap(t); + ThreadLocalMap map = getMap(t); // 获取当前线程的 ThreadLocalMap 对象 if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @@ -355,18 +507,63 @@ public T get() { } } return setInitialValue(); -}Copy to clipboardErrorCopied +} ``` -ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因为根本不存在多线程竞争。 +所以对于以下代码: -在一些场景 (尤其是使用线程池) 下,由于 ThreadLocal.ThreadLocalMap 的底层数据结构导致 ThreadLocal 有内存泄漏的情况,应该尽可能在每次使用 ThreadLocal 后手动调用 remove(),以避免出现 ThreadLocal 经典的内存泄漏甚至是造成自身业务混乱的风险。 +```java +public class ThreadLocalExample1 { + public static void main(String[] args) { + ThreadLocal threadLocal1 = new ThreadLocal(); + ThreadLocal threadLocal2 = new ThreadLocal(); + Thread thread1 = new Thread(() -> { + threadLocal1.set(1); + threadLocal2.set(1); + }); + Thread thread2 = new Thread(() -> { + threadLocal1.set(2); + threadLocal2.set(2); + }); + thread1.start(); + thread2.start(); + } +} +``` + +其对应的底层结构图为: + +
+ +由上图可以看出,ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因为根本不存在多线程竞争。 + +#### ThreadLocal 内存泄露问题 + +ThreadLocalMap 中使用的 **key 为 ThreadLocal 的弱引用,而 value 是强引用**。 + +```java +static class Entry extends WeakReference> { + /** The value associated with this ThreadLocal. */ + Object value; + + Entry(ThreadLocal k, Object v) { + super(k); + value = v; + } +} +``` + +如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。ThreadLocalMap 中就会出现 key 为 null 的 Entry,如果我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。 + +ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。应该尽可能在每次使用 ThreadLocal 后手动调用 remove(),以避免出现 ThreadLocal 经典的内存泄漏甚至是造成自身业务混乱的风险。 -## 线程间通信 + + +# 四、线程间通信 当多个线程可以一起工作去解决某个问题时,如果某些部分必须在其它部分之前完成,那么就需要对线程进行协调。 -### join() +## join() 在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。 @@ -422,7 +619,7 @@ B -### wait() notify() notifyAll() +## wait() & notify()/notifyAll() 调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。 @@ -463,7 +660,7 @@ before after ``` -### await() signal() signalAll() +## await() & signal()/signalAll() java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。 @@ -515,9 +712,9 @@ after -## 实现生产者和消费者 +# 五、实现生产者和消费者 -### 1. wait() / notifyAll() +## wait() & notifyAll() ```java public class ProducerConsumer { @@ -621,7 +818,7 @@ public class ProducerConsumer { -### 2. await() / sigalAll() +## await() & sigalAll() ```java public class ProducerConsumer { @@ -732,7 +929,7 @@ public class ProducerConsumer { -### 3. 阻塞队列 +## 阻塞队列 ```java public class ProducerConsumer { @@ -812,60 +1009,19 @@ public class ProducerConsumer { 消费者consumer正在消费数据-1171326759 ``` -## LeetCode 上多线程编程问题 - -[1114. 按序打印](https://leetcode-cn.com/problems/print-in-order/) - -我们提供了一个类: - -```java -public class Foo { - public void one() { print("one"); } - public void two() { print("two"); } - public void three() { print("three"); } -} -``` - -三个不同的线程将会共用一个 Foo 实例。 - -- 线程 A 将会调用 one() 方法 -- 线程 B 将会调用 two() 方法 -- 线程 C 将会调用 three() 方法 - -请设计修改程序,以确保 two() 方法在 one() 方法之后被执行,three() 方法在 two() 方法之后被执行。 - -示例 1: - -```html -输入: [1,2,3] -输出: "onetwothree" -解释: -有三个线程会被异步启动。 -输入 [1,2,3] 表示线程 A 将会调用 one() 方法,线程 B 将会调用 two() 方法,线程 C 将会调用 three() 方法。 -正确的输出是 "onetwothree"。 -``` - -示例 2: -```html -输入: [1,3,2] -输出: "onetwothree" -解释: -输入 [1,3,2] 表示线程 A 将会调用 one() 方法,线程 B 将会调用 three() 方法,线程 C 将会调用 two() 方法。 -正确的输出是 "onetwothree"。 -``` +# 六、多线程编程实战 -注意: +## LeetCode-按序打印 -尽管输入中的数字似乎暗示了顺序,但是我们并不保证线程在操作系统中的调度顺序。 +[1114. 按序打印](https://leetcode-cn.com/problems/print-in-order/) -你看到的输入格式主要是为了确保测试的全面性。 +参考代码: ```java import java.util.concurrent.CountDownLatch; -//解法一: class Foo { private CountDownLatch countDownLatchA; //等待 A 线程执行完 @@ -900,9 +1056,7 @@ class Foo { } ``` - - -## 多线程良好开发实践 +## 多线程良好开发习惯 - 给线程起个有意义的名字,这样可以方便找 Bug。 - 缩小同步范围,从而减少锁争用。例如对于 synchronized,应该尽量使用同步块而不是同步方法。 diff --git a/docs/Kafka&RabbitMQ/Kafka.md b/docs/Kafka&RabbitMQ/Kafka.md new file mode 100644 index 00000000..9d38cbb8 --- /dev/null +++ b/docs/Kafka&RabbitMQ/Kafka.md @@ -0,0 +1,331 @@ +# 一、Kafka 的相关概念 + +Kafka 是一种高吞吐、分布式、基于**发布订阅模型**的消息系统。Kafka 用于离线和在线消息的消费。主要有以下 3 个功能: + +- 消息队列:发布和订阅消息流 +- 容错的持久化方式存储记录消息流:Kafka 将消息数据按顺序保存在磁盘上,并在集群内以副本的形式存储以防止数据丢失 +- 流式处理平台:在消息发布的时候进行处理,Kafka 提供了一个完整的流式处理类库。 + +此外,Kafka 依赖 Zookeeper 进行集群的管理。 + +## Message + +消息(Message)是 Kafka 中**最基本的数据单元**。Kafka 消息由一个**定长的 Header 和变长的字节数组**组成。 + +## Broker + +Kafka 集群包含一个或多个服务器,这些服务器就被称为 Broker。 + +## Topic + +Kafka 根据主题(Topic)对消息进行归类,**发布到 Kafka 集群的每条消息(Message)都需要指定一个 Topic**。 + +## Partition + +Partition 即分区,每个 Topic 包含一个或多个分区。 + +消息发送时都被发送到一个 Topic 中,其本质就是一个目录,而 Topic 由是由一些 Partition Logs(分区日志)组成,其组织结构如下: + +
+ +**每个 Partition 中的消息都是有序的**,生产的消息被不断追加到 Partition Log 上,其中的每一个消息都被赋予了一个唯一的 offset 值,Kafka 通过 **offset 保证消息在分区内的顺序**,offset 的顺序性不跨分区,即 Kafka 只保证在同一个分区内的消息是有序的;同一 Topic 的多个分区内的消息,Kafka 并不保证其顺序性。 + +**Kafka 集群会保存所有的消息,不管消息有没有被消费**;可以设定消息的过期时间,只有过期的数据才会被自动清除以释放磁盘空间。比如设置消息过期时间为 2 天,那么这 2 天内的所有消息都会被保存到集群中,数据只有超过了 2 天才会被清除。 + +Kafka 需要维持的元数据只有一个,即消费消息在 Partition 中的 offset 值,Consumer 每消费一个消息,offset就会 +1。其实消息的状态完全是由 Consumer 控制的,**Consumer 可以跟踪和重设这个 offset 值,Consumer 就可以读取任意位置的消息**。 + +把消息日志以 Partition 的形式存放有多重考虑: + +- 第一,方便在集群中扩展,每个 Partition 可以通过调整以适应它所在的机器,而一个 Topic 又可以由多个Partition 组成,因此整个集群就可以适应任意大小的数据了; +- 第二,可以提高并发,因为是**以 Partition 为单位进行读写**,Partition 的个数对应了消费者个生产者的并发度,比如 Partition 的个数为 3,则集群中最多同时有 3 个线程的消费者并发处理数据。 + +注意:Partition 并不是越多越好的(Partition 的个数不能超过 Broker 结点),原因如下: + +- 分区越多,服务端和客户端需要使用的**内存**就越多。 +- 会降低一定的可用性。某个 Leader 挂了,相比较较少分区的情况,重新选出 Leader,花的时间就会更长。 + +## Replication + +Kafka 中每个 Partition 可以有多个副本(Replication),每个副本中包含的消息是一样的。 + +每个分区的副本集合中,都会选举出一个副本作为 Leader 副本,Kafka 在不同的场景下会采用不同的选举策略。**所有的读写请求都由选举出的 Leader 副本处理**,**Follower 副本仅仅是从 Leader 副本处把数据拉取(pull)到本地之后,同步更新到自己的 Log 中**。 + +一般情况下,同一分区的多个副本会被分配到不同的 Broker上。当 Leader 所在的 Broker 宕机之后,可以重新选举新的 Leader,继续对外提供服务。 + +
+ +## Producer + +消息生产者(Producer),向 Broker 发送消息的客户端。 + +**Producer 直接发送消息到 Broker上的 Leader Partition**,不需要经过任何中介或其他路由转发。 + +**Producer 客户端自己控制着消息被推送(push)到哪些 Partition**。Kafka 提供了接口供用户实现自定义的 Partition,用户可以为每个消息指定一个 Partition Key,通过这个 Key 来实现一些 Hash 分区算法。比如,把 userid 作为 Partition Key 的话,相同 userid 的消息将会被推送到同一个 Partition。**Producer 可以通过随机或者 Hash 等方式将消息平均发送到多个 Partition 上以实现负载均衡**。 + +### 批量发送消息 + +批量发送消息是提高吞吐量的重要方式。Kafka Producer 可以将消息在内存中累计到一定数量后作为一个**批量发送请求**。批量发送的数量大小可以通过Producer 的参数控制,参数值可以设置为累计的消息的数量(如 500 条)、累计的时间间隔(如 100ms )或者累计的数据大小(64 KB)。通过增加 Batch 的大小,可以减少网络请求和磁盘 I / O 的次数,当然具体参数设置需要在**效率**和**时效性**方面做一个权衡。 + +### 压缩消息 + +Producer 端可通过 gzip 或者 snappy 格式对消息集合进行压缩。消息在 Producer 端进行压缩,在 Consumer 端进行解压。压缩的好处是减少网络传输的数据量,减轻对网络带宽传输的压力。 + +### 异步并行发送消息 + +Producer 可以**异步地并行地**向 Kafka发送消息,但是通常 Producer 在发送完消息之后会得到一个 future 响应,返回的是 offset 值或者发送过程中遇到的错误。通过 request.required.acks 参数来设置 Leader Partition 收到确认的副本个数。ack 参数的具体说明如下: + +| ack | 说明 | +| :--: | :----------------------------------------------------------: | +| 0 | Producer **不会等待 Broker 的响应**
Producer 无法知道消息是否发送成功, 这样**可能会导致数据丢失**,但会得到最大的系统吞吐量。 | +| 1 | Producer 会在 **Leader Partition** 收到消息时得到 Broker 的一个确认
这样会有更好的可靠性,因为客户端会等待直到 Broker 确认收到消息。 | +| -1 | Producer 会在**所有备份的 Partition** 收到消息时得到 Broker 的确认
这个设置可以得到最高的可靠性保证。 | + +发布消息时,Kafka Client 先构造一条消息,将消息加入到消息集 set 中(Kafka支持批量发布,可以往消息集合中添加多条消息,一次行发布),send 消息时,Producer Client 需指定消息所属的 Topic。 + +## Consumer + +消息消费者(Consumer),从 Broker 读取消息的客户端。 + +消费者(Consumer)的主要工作是从 Topic 中拉取消息,并对消息进行消费。某个消费者消费到 Partition 的哪个位置(offset)的相关信息,是 Consumer 自己维护的。Consumer 可以自己决定如何读取 Kafka 中的数据。比如,Consumer 可以通过重设 offset 值来重新消费已消费过的数据。不管有没有被消费,Kafka 会保存数据一段时间,这个时间周期是可配置的,只有到了过期时间,Kafka 才会删除这些数据。 + +这样设计非常巧妙,**避免了 Kafka Server 端维护消费者消费位置的开销**,尤其是在消费数量较多的情况下。另一方面,如果是由 Kafka Server 端管理每个 Consumer 消费状态,一旦 Kafka Server 端出现延时或是消费状态丢失,将会影响大量的 Consumer。另一方面,这一设计也提高了 Consumer 的灵活性,Consumer 可以按照自己需要的顺序和模式拉取消息进行消费。例如:Consumer 可以通过修改其消费的位置实现针对某些特殊 key 的消息进行反复消费,或是跳过某些消息的需求。 + +Kafka 提供了两套 Consumer Api,分为 Simple Api 和 High-Level Api。 + +- Simple Api 是一个底层的 API,它维持了一个和单一 Broker 的连接,并且这个 API 是完全无状态的,每次请求都需要指定 offset 值,因此,这套 API 也是最灵活的。 + +- High-Level API 封装了对集群中一系列 Broker 的访问,可以透明的消费一个 Topic。它自己维持了已消费消息的状态,即每次消费的都是下一个消息。 + + High-Level API 还支持以组的形式消费 Topic,如果 Consumers 有同一个组名,那么 Kafka 就相当于一个队列消息服务,而各个 Consumer 均衡地消费相应 Partition 中的数据。若 Consumers 有不同的组名,那么此时 Kafka 就相当于一个广播服务,会把 Topic 中的所有消息广播到每个 Consumer。 + +
+ +## Consumer Group + +Consumer Group 即消费者组,多个 Consumer 可以组成一个 Consumer Group,一个 Consumer 只能属于一个 Consumer Group。**同一 Consumer Group 中的多个 Consumer 不能同时消费同一个 Partition 上的数据。**如果不同 Consumer Group 订阅了同一 Topic,Consumer Group 彼此之间不会干扰。这样,如果要实现一个消息可以被多个消费者同时消费(“广播”)的效果,则将每个消费者放入单独的一个 Consumer Group;如果要实现一个消息只被一个消费者消费(“独占”)的效果,则将所有的 Consumer 放入一个 Consumer Group 中。 + +注意:Consumer Group 中消费者的数量并不是越多越好,当其中消费者数量超过分区的数量时,会导致有消费者分配不到分区,从而造成消费者的浪费。 + +Producer、Consumer 和 Consumer Group 之间的关系如下图: + +
+ + + +# 二、Kafka 与 Zookeeper 关系 + +Zookeeper 为 Kafka 提供集群的管理。不仅保存着集群的 Broker、Topic、Partition 等元数据,还负责 Broker 故障发现、Leader 选举、负载均衡等。 + +- Broker 元数据管理 + + 在 Zookeeper 上会有一个专门**用来进行 Broker 服务器列表记录**的节点。每个 Broker 在启动时,都会到 Zookeeper 上进行注册,即到 `/brokers/ids` 下创建属于自己的节点,每个 Broker 会将自己的 IP 地址和端口等信息记录到该节点中去。 + +- Topic 元数据管理 + + 在 Kafka 中,同一个Topic 的消息会被分成多个分区并将其分布在多个 Broker 上,这些分区信息及与 Broker 的对应关系由 Zookeeper 维护。比如 my-topic 的 Topic 有 2 个分区,对应到 Zookeeper 中会创建这些文件夹:`/brokers/topics/my-topic/Partitions/0`、`/brokers/topics/my-topic/Partitions/1` + +- 负载均衡 + + 对于同一个 Topic 的不同 Partition,Kafka 会将这些 Partition 分布到不同的 Broker 服务器上,生产者产生消息后也会尽量投递到不同 Broker 的 Partition 中,当 Consumer 消费的时候,Zookeeper 可以根据当前的 Partition 数量以及 Consumer 数量来实现动态负载均衡。 + +# 三、Kafka 集群 + +## 典型拓扑结构 + +
+ +Kafka 集群包含若干个 Producer,若干个 Broker (Kafka 集群支持水平扩展,一般 Broker 数量越多,整个 Kafka 集群的吞吐量也就越高),若干个 Consumer Group,以及一个 Zookeeper 集群。 + +Kafka 通过 Zookeeper 管理集群配置。 + +Producer 使用 Push 模式将消息发布到 Broker 上,Consumer 使用 Pull 模式从 Broker 上订阅并消费消息。 + +## Kafka 数据流 + +
+ +Producers 往 Brokers 中指定的 Topic Push 消息,Consumers 从 Brokers 里面 Pull 指定 Topic 的消息,然后进行业务处理。 + +图中有两个 Topic,并且每个 Partition 上色数据在其他 Broker 服务器上都有一份副本: + +- Topic-0 有两个 Partition,Partition-0 和 Partition-1; +- Topic-1 有一个 Partition。 + +可以看到 Consumer-Group-1 中的 Consumer-2 没有分到 Partition 处理,这是有可能出现的。 + +# 四、Kafka 的数据存储设计 + +## Segment 数据文件 + +Partition 在物理上由多个 Segment 数据文件组成,每个 Segment 数据文件大小相等、按顺序读写。每个 Segment 数据文件的第一个文件名从 0 开始,后续每个 Segment 文件名为上一个全局 Partition 的最大offset,文件扩展名为 .log。在查找指定 Offset 的 Message 中,用二分查找就可以定位到该 Message 在哪个 Segment 数据文件中。 + +Segment 数据文件会首先被存储在内存中,当 Segment 上的消息条数达到配置值或消息发布时间超过阈值时,Segment 上的消息会被 flush 到磁盘,只有 flush 到磁盘上的消息才能被 Consumer 消费,Segment 达到一定的大小(默认是 1 GB,可配置)后将不会再往该 Segment写数据,Broker 会创建新的 Segment。 + +## Segment 索引文件 + +Kafka 为每个 Segment 数据文件都建立了索引文件,索引文件的文件名与数据文件的文件名一致,不同的是索引文件的扩展名为 .index。比如: + +
+ +Segment 索引文件索引文件并不会为数据文件中的每条 Message 都建立索引,而是采用系数索引的方式,每个一定字节建立一条索引。这样可有效降低索引文件大小,方便将索引文件加载到内存中,提高集群的吞吐量。 + +如下图所示,000000000000036869.index 文件中记录了 (3,497) ,在数据文件中表示第 3 个 Message(在全局Partition 表示第 368772 个 Message),该 Message 的物理偏移地址为 497。 + +
+ +## Partition 中通过 Offset 查找 Message + +比如读取 Offset=368776 的 Message,需要通过下面 2 个步骤进行查找: + +- 查找 Segment 文件 + + 其中00000000000000000000.index 表示最开始的文件,起始 Offset=0。第二个文件 00000000000000368769.index 文件的起始偏移量为 368770 (368769 + 1)。同样,第三个文件00000000000000737337.index 文件的起始偏移量为 737338 (737337 + 1) ,其他后续文件依次类推,以起始偏移量命名并排序这些文件,根据 Offset **二分查找**文件列表,快速定位到 00000000000000368769.index 文件。 + +- 通过 Segment 文件查找 Message + + 当 Offset=368776 时,依次定位到 00000000000000368769.index 的元数据物理位置和 00000000000000368769.log 的物理偏移地址,然后再通过 00000000000000368769.log 顺序查找直到 Offset=368776为止。 + +# 五、Kafka 的高可用原理 + +Kafka 集群由若干个 Broker 组成,Topic 由若干个 Partition 组成,每个 Partition 可存在不同的 Broker 上。可以这样说,一个 Topic 的数据,分散在多个机器上,即每个机器上都存放一部分数据。 + +## Kafka 0.8 以前 + +Kafka 0.8 以前是没有高可用机制的。 + +假设一个 Topic,由 3 个 Partiton 组成。3 个 Partition 在不同机器上,如果其中某一台机器宕掉了,则 Topic 的部分数据就丢失了。 + +## Kafka 0.8 以后 + +Kafka 0.8 以后,通过**副本机制**来实现高可用。 + +每个 Partition 的数据都会同步到其他机器上,形成多个 Partition 副本。从每个 Partition 的副本集合中,选举出 Leader,其他的都是 Follower。Producer 和 Consumer 就和 Leader 打交道: + +- 写数据时,Leader 会将所用数据都同步到 Follower 上 +- 读数据时,直接读取 Leader 上的数据 + +这样,若某个 Broker 宕机了,Broker 上的 Partition 在其他机器上是有副本的;若宕机的 Broker 上面有某个 Partition 的 Leader,则此时会从 Follower 中重新选择一个新的 Leader 出来。 + +注意:Leader 的读写细节 + +- 写数据时,Producer 向 Leader 写入,接着其他 Follower 主动从 Leader 中 pull 数据,当所有 Follower 同步好数据,就发送确认信息给 Leader,Leader 收到 Follower 的确认后,就返回写成功消息给 Producer。 +- 读数据时,只会从 Leader 中读取,但当只有一个消息已被所有 Follower 都同步成功,返回确认后时,这个消息才被消费者读到。 + +# 六、Kafka 中一些常见的问题 + +## 消息消费的顺序问题 + +消息在被追加到 Partition 的时候都会分配一个特定的偏移量(offset),Kafka 通过偏移量(offset)来保证消息在分区内的顺序性。为了保证 Kafka 中消息消费的顺序,可以采用以下 2 种方法: + +- 设置 1 个 Topic 只对应一个 Partition + + 破坏了 Kafka 的设计初衷,不推荐使用。 + +- 发送消息的时候指定 key + + 同一个 key 的消息可以保证只发送到同一个 Partition。 + +**提升:[如何保证消息的顺序性?](https://doocs.github.io/advanced-java/#/./docs/high-concurrency/how-to-ensure-the-order-of-messages)** + +## 消息丢失问题 + +### Producer 丢失数据 + +如果 Producer 端设置了 `acks=all`,则不会丢失数据。 + +Leader 在所有的 Follower 都同步到了消息之后,才认为本次写成功。如果没满足这个条件,生产者会进行无限次重试。 + +### Consumer 丢失数据 + +默认情况下,Kafka 会自动提交 Offset,Kafka 认为 Consumer 已经处理消息了,但是 Consumer 可能在处理消息的过程中挂掉了。重启系统后,Consumer 会根据提交的 Offset 进行消费,也就丢失了一部分数据。 + +解决:关闭自动提交 Offset,在处理完之后自己手动提交 Offset,就可以保证数据不会丢失。但可能会存在消息重复消费问题。 + +### Broker 丢失数据 + +比较常见的一个场景:Kafka 某个Leader 所在的 Broker 宕机,需要重新选举新的 Leader ,但此时其他的 Follower 部分数据尚未同步完成,选举某个 Follower 成 Leader 后就丢失一部分数据。 + +所以此时一般设置如下 4 个参数: + +- Topic 设置 `replication.factor` 参数 + + 参数值必须大于 1,要求每个 Partition 必须有至少 2 个副本。 + +- Kafka 服务端设置 `min.insync.replicas` 参数 + + 参数值必须大于 1,要求每个 Partition 必须有至少 2 个副本。 + +- Producer 设置 `acks=all` + + 要求每条数据,必须是**写入所有副本,才认为写成功**。 + +- Producer 端设置 `retries=MAX` + + MAX 即是一个超级大的数字,表示无限次重试。`retries=MAX `要求一旦写入数据失败,就无限重试。 + +## 消息重复消费问题 + +Consumer 消费了数据后,每个一段时间,会将已消费过的消息的 Offset 进行提交,这样,重启后,可以继续从上次消费过的 Offset 来继续消费。测试时,直接 kill 进程,然后再重启后,会导致 Consumer 将有些消息处理了,但是还未来得及提交 Offset,重启后,少数消息会再消费一次。 + +解决:需要结合具体业务来思考,可从以下几个思路来考虑: + +- 如果要将数据写入数据库中,先根据主键查查询,如果这数据已存在,就不用插入数据了。 +- 向 Redis 中写入数据,可以使用 set,这样数据不会重复 +- 基于数据库的唯一键来保证重复数据不会重复插入多条。因为有唯一键约束了,重复数据插入只会报错,不会导致数据库中出现脏数据。 + +# 七、Kafka 特性总结 + +Kafka 特性可总结如下: + +## 1. 高可用 + +Kafka 0.8 以前是没有高可用机制的。 + +Kafka 0.8 以后,通过**副本机制**来实现高可用,基于**副本机制**实现 Kafka 的高可用。 + +## 2. 持久性 + +Kafka 集群接收到 Producer 发过来的消息后,将其持久化到磁盘。此外,还支持数据备份。 + +## 3. 数据不易丢失 + +通过合理的配置,Kafka 消息不易丢失。 + +## 4. 高吞吐量 + +Kafka 高吞吐量的原因:分区、批量发送和压缩消息、顺序读写、零拷贝 + +### 分区 + +当生产者向对应 Topic 传递消息,消息通过**负载均衡机制**传递到不同的 Partition 以减轻单个服务器实例的压力; + +一个 Consumer Group 中可以有多个 Consumer,多个 Consumer 可以同时消费不同 Partition 的消息,大大的提高了消费者的并行消费能力。 + +### 批量发送和压缩消息 + +- 批量发送:在发送消息的时候,Kafka 不会直接将少量数据发送出去,否则每次发送少量的数据会增加网络传输频率,降低网络传输效率。Kafka 会先将消息缓存在内存中,当超过一个的大小或者超过一定的时间,那么会将这些消息进行批量发送。 +- 端到端压缩消息: Kfaka会将这些批量的数据进行压缩,将一批消息打包后进行压缩,发送给 Broker 服务器后,最终这些数据还是提供给消费者用,所以数据在服务器上还是保持压缩状态,不会进行解压,而且频繁的压缩和解压也会降低性能,最终还是以压缩的方式传递到消费者的手上,在 Consumer 端进行解压。 + +### 顺序读写 + +Kafka 是个可持久化的日志服务,它将数据以数据日志的形式进行追加,最后持久化在磁盘中。 + +Kafka 消息存储时依赖于**文件系统**。为了利用数据的**局部相关性**:操作系统从磁盘中**以数据块为单位**读取数据,将一个数据块读入内存中,如果有相邻的数据,就不用再去磁盘中读取。所以,在某些情况下,**顺序磁盘访问能比随机内存访问还要快**。同时在写数据的时候也是将一整块数据块写入磁盘中,大大提升 I / O 效率。 + +### 零拷贝 + +普通的数据拷贝 read & write: + +
+ +零拷贝主要的任务是**避免 CPU 做大量的数据拷贝任务,减少不必要的拷贝**。 + +**内存映射文件(Memory Mapped Files,mmap)**在 64 位操作系统中一般可以表示 20G 的数据文件,它的工作原理是直接利用操作系统的页缓存来实现文件到物理内存的直接映射。 + +
+ +显然,使用 mmap 替代 read 很明显减少了 1 次拷贝,当拷贝数据量很大时,无疑提升了效率。 + +# 补充 + +- [Kafka史上最详细原理总结](https://blog.csdn.net/lingbo229/article/details/80761778?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-0.no_search_link&spm=1001.2101.3001.4242) \ No newline at end of file diff --git a/docs/Kafka&RabbitMQ/Kafka_RabbitMQ.md b/docs/Kafka&RabbitMQ/Kafka_RabbitMQ.md new file mode 100644 index 00000000..a9ebb4a9 --- /dev/null +++ b/docs/Kafka&RabbitMQ/Kafka_RabbitMQ.md @@ -0,0 +1,47 @@ +## Kafka 和 RabbitMQ 比较 + +从以下几个方面比较 Kafka 和 RabbitMQ: + +- 吞吐量 + + Kafka:十万数量级,高吞吐量 + + RabbitMQ:万数量级 + +- Topic 数量对吞吐量影响 + + Kafka 的 Topic 可达百/千级,吞吐量下降幅度小,在同等机器下,可以支撑大量的 Topic。 + + RabbitMQ 无 Topic 概念。 + +- 时效性 + + Kafka 毫秒级;RabbitMQ 微秒级 + +- 可用性 + + Kafka 基于分布式架构,可用性非常高 + + RabbitMQ 基于主从架构实现高可用 + +- 可靠性 + + Kafka 优化参数配置,可以做到零丢失 + + RabbitMQ 基本不会丢失数据 + +- 功能 + + Kafka 功能较为简单,主要支持简单的消息队列功能,在大数据领域的实时计算以及日志采集被大规模使用 + + RabbitMQ 基于 ErLang 开发,并发能力很强,性能极好,延时很低 + +总结: + +- Kafka 主要特点是基于 Pull 的模式来处理消息消费,追求高吞吐量,一开始的目的就是用于日志收集和传输。0.8 版本开始支持复制,不支持事务,对消息的重复、丢失、错误没有严格要求,**适合产生大量数据的互联网服务的数据收集业务**。 +- RabbitMQ 是使用 Erlang 语言开发的开源消息队列系统,基于 AMQP 协议来实现。AMQP 的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。AMQP 协议更多用在企业系统内,**对数据一致性、稳定性和可靠性要求很高的场景,对性能和吞吐量的要求还在其次**。 + + + + + diff --git a/docs/Kafka&RabbitMQ/RabbitMQ.md b/docs/Kafka&RabbitMQ/RabbitMQ.md new file mode 100644 index 00000000..45ae2732 --- /dev/null +++ b/docs/Kafka&RabbitMQ/RabbitMQ.md @@ -0,0 +1,1930 @@ +# 一、Rabbit 概述 + +RabbitMQ 是一个开源的消息代理和队列服务器,用来通过普通协议在完全不同的应用中间共享数据,RabbitMQ 是使用 **Erlang 语言**来编写的,并且 RabbitMQ 是基于 AMQP 协议的。 + +特点: + +- 开源、性能优秀 + + **Erlang 语言**最初用在交换机的架构模式,这样使得 RabbitMQ 在 Broker 之间进行数据交互的性能时非常优秀的。Erlang 的优点:Erlang 有着和原生 Socket 一样的延迟。 + +- 可靠性 + + 提供可靠性消息投递模式(confirm)、返回模式(return)。 + +- 扩展性 + + 多个RabbitMQ 节点可以组成一个集群,也可以根据实际业务情况动态地扩展集群中节点。 + +- 与 SpringAOP 完美的整合、API 丰富 +- 保证数据不丢失的前提做到高可靠性、可用性 + +# 二、AMQP 协议 + +AMQP (Advanced Message Queuing Protocol) 即高级消息队列协议,是一个进程间传递**异步消息**的**网络协议**。 + +## AMQP 模型 + +
+ +工作过程如下:首先发布者(Publisher)发布消息(Message),经由交换机 Exchange。交换机根据**路由规则**将收到的消息分发给与该交换机绑定的 Queue。最后 AMQP 代理会将消息投递给订阅了此队列的消费者,或者消费者按照需求自行获取。 + +关于 AMQP 模型的几点说明: + +- 发布者、交换机、队列、消费者都可以有多个。AMQP 是一个网络协议,所以这个过程中的发布者,消费者,消息代理可以分别存在于不同的设备上。 +- 布者发布消息时可以给消息指定各种消息属性(Message Meta-data)。有些属性有可能会被消息代理(Brokers)使用,然而其他的属性则是完全不透明的,它们只能被接收消息的应用所使用。 +- 从安全角度考虑,网络是不可靠的,又或是消费者在处理消息的过程中意外挂掉,这样没有处理成功的消息就会丢失。基于此原因,AMQP 模块包含了一个消息确认机制:当一个消息从队列中投递给消费者后,不会立即从队列中删除,直到它收到来自消费者的确认回执(Acknowledgement)后,才完全从队列中删除。 +- 在某些情况下,例如当一个消息无法被成功路由时(无法从交换机分发到队列),消息或许会被返回给发布者并被丢弃。或者,如果消息代理执行了**延期操作**,消息会被放入一个**死信队列**中。此时,消息发布者可以选择某些参数来处理这些特殊情况。 + +## Producer & Consumer + +消息生产者(Producer),向 Broker 发送消息的客户端。 + +消息消费者(Consumer),从 Broker 消费消息的客户端。 + +## Broker + +一个 RabbitMQ Broker 可以简单地看作一个 RabbitMQ 服务节点,或者 RabbitMQ 服务实例。大多数情况下可以将一个 RabbitMQ Broker 看作一台 RabbitMQ 服务器。 + +## Exchange + +Exchange 即交换器,是用来发送消息的 AMQP 实体。Exchange 拿到一个消息之后将它**路由**给一个或零个队列。Exchange 使用哪种路由算法是由**交换机类型**和**绑定(Bindings)规则**所决定的。 + +### Binding + +**Producer 将消息发给 Exchange 时,一般会指定一个 RoutingKey (路由键),用来指定这个消息的路由规则,而这个 RoutingKey 需要与交换器类型和 BindingKey (绑定键) 联合使用才能最终生效**。 + +RabbitMQ 中通过 **Binding (绑定)** 将 Exchange 与 Queue(消息队列) 关联起来,在绑定时一般会指定一个 BindingKey,这样 RabbitMQ 就知道如何正确将消息路由到 Queue 中。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则。 + +生产者将消息发送给交换器,当 BindingKey 和 RoutingKey 相匹配时,消息会被路由到对应的队列中。注意BindingKey 并不是在所有的情况下都生效,它依赖于交换器类型,比如 fanout 类型的交换器就会无视,而是将消息路由到所有绑定到该交换器的队列中。 + +### Exchange 类型 + +Exchange 有以下 4 种类型,不同的类型对应着不同的路由策略: + +#### direct + +Exchange 默认类型。**路由规则是把消息路由到 Bindingkey 与 RoutingKey 完全匹配的 Queue 中**。direct 类型常用在**处理有优先级的任务**,根据任务的优先级把消息发送到对应的队列,这样可以指派更多的资源去处理高优先级的队列。 + +
+ +以上图为例,如果发送消息的时候 RoutingKey="booking",那么消息会路由到 Queue1 和 Queue2。如果在发送消息的时候设置 RoutingKey="create" 或 "confirm",消息只会路由到Queue2。如果以其他的 RoutingKey 发送消息,则消息不会路由到这两个队列中。 + +#### fanout + +路由规则是把所有发送到该 Exchange 的消息路由到所有与它绑定的 Queue 中,不需要做任何判断操作,所以 fanout 类型是所有的交换机类型里面速度最快的。**fanout 类型常用来广播消息**。 + +#### topic + +direct 类型的 Exchange 路由规则是完全匹配 BindingKey 和 RoutingKey ,但是这种严格的匹配方式在很多情况下不能满足实际业务的需求。 + +topic 类型的 Exchange 在匹配规则上进行了扩展,它与 direct 类型的 Exchange 相似,也是将消息路由到 BindingKey 和 RoutingKey 相匹配的队列中,但这里的匹配规则有些不同,它约定: + +- RoutingKey 为一个点号`.`分隔的字符串,其中**被 `.`分隔开的每一段独立的字符串称为一个单词**; +- BindingKey 和 RoutingKey 一样也是点号 `.` 分隔的字符串; +- BindingKey 中可以存在两种特殊字符串 `*` 和 `#`,用于做模糊匹配,其中 `*` 用于匹配一个单词, `#` 用于匹配零个或多个单词。 + +
+ +以上图为例,如果发送消息的时候 RoutingKey 为 + +- "com.rabbitmq.client",那么消息会路由到 Queue1 和 Queue2 +- "com.hidden.client",那么消息只会路由到 Queue2 中 +- "com.hidden.demo",那么消息只会路由到 Queue2 中 +- "java.rabbitmq.demo",那么消息只会路由到 Queue1 中 +- "java.util.concurrent",那么消息将会被丢弃或者返回给生产者,因为它没有匹配任何路由键。 + +#### headers + +headers 类型的 Exchange 不依赖于路由键的匹配规则来路由消息,而是根据发送的消息内容中的 headers 属性进行匹配。在绑定队列和交换器时指定一组键值对,当发送消息到交换器时,RabbitMQ 会获取到该消息的 headers(也是一个键值对的形式),对比其中的键值对是否完全匹配队列和交换器绑定时指定的键值对,如果完全匹配则消息会路由到该队列,否则不会路由到该队列。headers 类型的 Exchange 性能会很差,不推荐使用。 + +## Queue + +Queue 其实是 Message Queue 即消息队列,保存消息并将它们转发给消费者。Queue 是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其消费。 + +**RabbitMQ 中消息只能存储在队列中**,而 Kafka 将消息存储在 **Topic** 中,即该 Topic 对应的 Partition 中。RabbitMQ 的生产者生产消息并最终投递到队列中,消费者可以从队列中获取消息并消费。 + +当多个消费者订阅同一个队列时,队列中的消息会被平均分摊(Round-Robin,即轮询)给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理,这样避免消息被重复消费。 + +### 队列属性 + +Queue 跟 Exchange 共享某些属性,但是队列也有一些另外的属性: + +- Name +- Durable:消息代理重启后,队列依旧存在 +- Exclusive:只被一个连接使用,而且当连接关闭后队列即被删除 +- Auto-delete:当最后一个消费者退订后即被删除 +- Arguments:一些消息代理用他来完成类似与 TTL 的某些额外功能 + +### 队列创建 + +队列在声明(declare)后才能被使用。 + +如果一个队列尚不存在,声明一个队列会创建它。如果声明的队列已经存在,并且属性完全相同,那么此次声明不会对原有队列产生任何影响。如果声明中的属性与已存在队列的属性有差异,那么一个错误代码为 406 的通道级异常就会被抛出。 + +### 队列持久化 + +持久化队列(Durable Queues)会被存储在磁盘上,当消息代理(Broker)重启的时候,它依旧存在。没有被持久化的队列称作暂存队列(Transient Queues)。并不是所有的场景和案例都需要将队列持久化。 + +持久化的队列并不会使得路由到它的消息也具有持久性。倘若消息代理挂掉了,重新启动,那么在重启的过程中持久化队列会被重新声明,无论怎样,**只有经过持久化的消息才能被重新恢复**。 + +## 消息机制 + +### 消息确认 + +AMQP 代理在什么时候删除消息才是正确的?AMQP 0-9-1 规范给我们两种建议: + +- **自动确认模式**:当消息代理(Broker)将消息发送给应用后立即删除。(使用 AMQP 方法:basic.deliver 或 basic.get-ok) + +- **显示确认模式**:待 Consumer 发送一个确认回执(acknowledgement)后再删除消息。(使用 AMQP 方法:basic.ack) + + 如果一个消费者在尚未发送确认回执的情况下挂掉了,那 AMQP 代理会将消息重新投递给另一个消费者。如果当时没有可用的消费者了,消息代理会死等下一个注册到此队列的消费者,然后再次尝试投递。 + +### 拒绝消息 + +当拒绝某条消息时,应用可以告诉消息代理销毁该条消息或者重新将该条消息放入队列。 + +当此队列只有一个消费者时,有可能存在拒绝消息并将消息重新放入队列的行为而引起消息在同一个消费者身上无限循环的情况。 + +### 预取消息 + +在多个消费者共享一个队列时,明确指定在收到下一个确认回执前每个消费者一次可以接受多少条消息是非常有用的。这可以在试图批量发布消息的时候起到**简单的负载均衡和提高消息吞吐量**的作用。 + +### 消息属性 + +AMQP 模型中的消息(Message)对象是带有属性(Attributes)的: + +| 属性 | 说明 | +| --------------------------------- | ------------------------------ | +| Content type | 内容类型 | +| Content encoding | 内容编码 | +| **Routing key** | 路由键 | +| Delivery mode (persistent or not) | 投递模式(持久化 或 非持久化) | +| Message priority | 消息优先权 | +| Message publishing timestamp | 消息发布的时间戳 | +| Expiration period | 消息有效期 | +| Publisher application id | 发布应用的 ID | + +有些属性是被 AMQP 代理所使用的,但是大多数是开放给接收它们的应用解释器用的。有些属性是可选的也被称作消息头(headers)。和 HTTP 协议的 X-Headers 很相似,消息属性需要在消息被发布的时候定义。 + +### 消息主体 + +AMQP 的消息除属性外,也含有一个有效载荷 Payload(消息实际携带的数据),它被 AMQP 代理当作不透明的字节数组来对待。 + +消息代理不会检查或者修改 Payload,消息可以只包含属性而不携带有效载荷,它通常会使用类似 JSON 这种序列化的格式数据。 + +### 消息持久化 + +消息能够以持久化的方式发布,AMQP 代理会将此消息存储在磁盘上。如果服务器重启,系统会确认收到的持久化消息未丢失。 + +简单地将消息发送给一个持久化的交换机或者路由给一个持久化的队列,并不会使得此消息具有持久化性质:它完全取决与消息本身的持久模式(persistence mode)。将消息以持久化方式发布时,会对性能造成一定的影响(就像数据库操作一样,健壮性的存在必定造成一些性能损失)。 + +# 三、RabbitMQ 命令行操作 + +
+ +## 启动 & 停止服务器 + +- 启动服务器 + + ```html + rabbitmq-server start & + ``` + +- 停止服务器 + + ```html + rabbitmqctl stop_app + ``` + +## 查看管控台 + +```html +http://localhost:15672/ + +# 用户名 guest +# 密码 guest +``` + + + +## 命令行基础操作 + +### 1. 应用 + +- 关闭应用 + + ```html + rabbitmqctl stop_app + ``` + +- 启动应用 + + ```html + rabbitmqctl start_app + ``` + +- 查看节点状态 + + ```html + rabbitmqctl status + ``` + +### 2. 用户 + +- 添加用户 + + ```html + rabbitmqctl add_user username password + ``` + +- 删除用户 + + ```html + rabbitmqctl delete_user username + ``` + +- 列出所有用户 + + ```html + rabbitmqctl list_users + ``` + +- 清除用户权限 + + ```html + rabbitmqctl clear_permissions -p vhostpath username + ``` + +- 列出用户权限 + + ```html + rabbitmqctl list_user_permissions username + ``` + +- 修改密码 + + ```html + rabbitmqctl change_password username newpassword + ``` + +- 设置用户权限 + + ```html + rabbitmqctl set_permissions -p vhostpath username ".*" ".*" ".*" + ``` + +### 3. 虚拟主机 + +- 创建虚拟主机 + + ```html + rabbitmqctl add_vhost vhostpath + ``` + +- 删除虚拟主机 + + ```html + rabbitmqctl delete_vhost vhostpath + ``` + +- 列出所有虚拟主机 + + ```html + rabbitmqctl list_vhosts + ``` + +- 列出虚拟主机上所有权限 + + ```html + rabbitmqctl list_permissions -p vhostpath + ``` + +### 4. 队列 + +- 查看所有队列信息 + + ```html + rabbitmqctl list_queues + ``` + +- 清除队列里的消息 + + ```html + rabbitmqctl -p vhostpath purge_queue blue + ``` + + + +## 命令行高级操作 + +- 移除所有数据 + + ```html + rabbitmqctl reset + # 要在 rabbitmqctl stop_app 之后使用 + ``` + +- 组成集群命令 + + ```html + rabbitmqctl join_cluster [--ram] + ``` + +- 查看集群状态 + + ```html + rabbitmq cluster_status + ``` + +- 修改集群节点的存储形式 + + ```html + rabbitmqctl change_cluser_node_type disc | ram + ``` + +- 摘除节点(忘记节点) + + ```html + rabbitmqctl forget_cluster_node [--offline] + ``` + +- 修改节点名称 + + ```html + rabbitmqctl rename_cluster_node oldnode1 newnode1 [oldnode2] [newnode2] + ``` + +# 四、Rabbit MQ 入门 + +## 简单案例:消息生产与消费 + +pom.xml 配置 + +```html + + com.rabbitmq + amqp-client + + 3.6.5 + +``` + +### 生产者 + +```java +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +/** + *

简单案例:消息生产与消费

+ * 消息生产者 + * Created by DHA on 2019/11/18. + */ +public class Producer { + public static void main(String[] args) throws IOException, TimeoutException { + + //1 创建一个 Connectionfactory,并进行设置 + ConnectionFactory connectionFactory = new ConnectionFactory(); + connectionFactory.setHost("localhost"); + connectionFactory.setPort(5672); + connectionFactory.setVirtualHost("/"); + + //2 通过连接工厂创建连接 + Connection connection = connectionFactory.newConnection(); + + //3 通过 connecion 创建一个 Channel + Channel channel = connection.createChannel(); + + //4 通过 chanel 发送数据 + for(int i=0;i<10;i++){ + String data="Hello!"; + channel.basicPublish("","test001",null,data.getBytes()); + } + + //5 关闭相关连接 + channel.close(); + connection.close(); + } +} +``` + +### 消费者 + +```java +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +/** + *

简单案例:消息生产与消费

+ * 消息生产者 + * Created by DHA on 2019/11/18. + */ +public class Producer { + public static void main(String[] args) throws IOException, TimeoutException { + + //1 创建一个 Connectionfactory,并进行设置 + ConnectionFactory connectionFactory = new ConnectionFactory(); + connectionFactory.setHost("localhost"); + connectionFactory.setPort(5672); + connectionFactory.setVirtualHost("/"); + + //2 通过连接工厂创建连接 + Connection connection = connectionFactory.newConnection(); + + //3 通过 connecion 创建一个 Channel + Channel channel = connection.createChannel(); + + //4 通过 chanel 发送数据 + for(int i=0;i<10;i++){ + String data="Hello!"; + channel.basicPublish("","test001",null,data.getBytes()); + } + + //5 关闭相关连接 + channel.close(); + connection.close(); + } +} +``` + +## Direct Exchange + +### 生产者 + +```java +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +/** + *

Direct Exchange

+ * 所有发送到 Direct Exchange 的消息被转发到 routing key 中指定的 Queue。 + * 消息生产者 + * Created by DHA on 2019/11/19. + */ +public class Producer4DirectExchange { + public static void main(String[] args) throws IOException, TimeoutException { + + //1 创建一个 Connectionfactory,并进行设置 + ConnectionFactory connectionFactory = new ConnectionFactory(); + connectionFactory.setHost("localhost"); + connectionFactory.setPort(5672); + connectionFactory.setVirtualHost("/"); + + //2 通过连接工厂创建连接 + Connection connection = connectionFactory.newConnection(); + + //3 通过 connecion 创建一个 Channel + Channel channel = connection.createChannel(); + + //4 声明 + //声明 exchange 名称 + String exchangeName="test_direct_exchange"; + String routingKey = "test.direct"; + + //5 通过 chanel 发送数据 + String msg = "Hello World RabbitMQ 4 Direct Exchange Message ... "; + channel.basicPublish(exchangeName, routingKey , null , msg.getBytes()); + + //6 关闭相关连接 + channel.close(); + connection.close(); + } +} +``` + + + +### 消费者 + +```java +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.QueueingConsumer; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +/** + *

Direct Exchange

+ * 所有发送到 Direct Exchange 的消息被转发到 routing key 中指定的 Queue。 + * 消息消费者 + * Created by DHA on 2019/11/19. + */ +public class Consumer4DirectExchange { + public static void main(String[] args) throws IOException, TimeoutException, InterruptedException { + + //1 创建一个 Connectionfactory,并进行设置 + ConnectionFactory connectionFactory = new ConnectionFactory(); + connectionFactory.setHost("localhost"); + connectionFactory.setPort(5672); + connectionFactory.setVirtualHost("/"); + + //2 通过连接工厂创建连接 + Connection connection = connectionFactory.newConnection(); + + //3 通过 connecion 创建一个 Channel + Channel channel = connection.createChannel(); + + //4 声明 + String exchangeName="test_direct_exchange"; + String exchangeType="direct"; + String queueName="test_direct_queue"; + String routingKey="test.direct"; + + // 声明一个交换机 + channel.exchangeDeclare(exchangeName,exchangeType,true,false,false,null); + // 声明一个队列 + channel.queueDeclare(queueName,false,false,false,null); + // 绑定:将一个队列绑定到一个交换机上 + channel.queueBind(queueName,exchangeName,routingKey); + + //5 创建消费者 + QueueingConsumer queueingConsumer=new QueueingConsumer(channel); + + //6 设置 channel + channel.basicConsume(queueName,true,queueingConsumer); + + //7 获取数据 + while(true){ + QueueingConsumer.Delivery delivery=queueingConsumer.nextDelivery(); + String msg=new String(delivery.getBody()); + System.out.println("消费端:"+msg); + } + } +} +``` + + + +## Topic Exchange + +### 生产者 + +```java +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +/** + *

Topic Exchange

+ * Topic Exchange 将 routing key 与某 Topic 进行模糊匹配,此时队列需要绑定一个 Topic。 + * 消息生产者 + * Created by DHA on 2019/11/19. + */ +public class Producer4TopicExchange { + public static void main(String[] args) throws IOException, TimeoutException { + + //1 创建一个 Connectionfactory,并进行设置 + ConnectionFactory connectionFactory = new ConnectionFactory(); + connectionFactory.setHost("localhost"); + connectionFactory.setPort(5672); + connectionFactory.setVirtualHost("/"); + + //2 通过连接工厂创建连接 + Connection connection = connectionFactory.newConnection(); + + //3 通过 connecion 创建一个 Channel + Channel channel = connection.createChannel(); + + //4 声明 + //声明 exchange 名称 + String exchangeName="test_topic_exchange"; + String routingKey1 = "user.save"; + String routingKey2 = "user.update"; + String routingKey3 = "user.delete.abc"; + + //5 通过 chanel 发送数据 + String msg = "Hello World RabbitMQ 4 Topic Exchange Message ... "; + channel.basicPublish(exchangeName, routingKey1 , null , msg.getBytes()); + channel.basicPublish(exchangeName, routingKey2 , null , msg.getBytes()); + channel.basicPublish(exchangeName, routingKey3 , null , msg.getBytes()); + + //6 关闭相关连接 + channel.close(); + connection.close(); + } +} +``` + + + +### 消费者 + +```java +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.QueueingConsumer; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +/** + *

Topic Exchange

+ * Topic Exchange 将 routing key 与某 Topic 进行模糊匹配,此时队列需要绑定一个 Topic。 + * 消息消费者 + * Created by DHA on 2019/11/19. + */ +public class Consumer4TopicExchange { + public static void main(String[] args) throws IOException, TimeoutException, InterruptedException { + + //1 创建一个 Connectionfactory,并进行设置 + ConnectionFactory connectionFactory = new ConnectionFactory(); + connectionFactory.setHost("localhost"); + connectionFactory.setPort(5672); + connectionFactory.setVirtualHost("/"); + + //2 通过连接工厂创建连接 + Connection connection = connectionFactory.newConnection(); + + //3 通过 connecion 创建一个 Channel + Channel channel = connection.createChannel(); + + //4 声明 + String exchangeName="test_topic_exchange"; + String exchangeType="topic"; + String queueName="test_topic_queue"; + String routingKey="user.*"; + + // 声明一个交换机 + channel.exchangeDeclare(exchangeName,exchangeType,true,false,false,null); + // 声明一个队列 + channel.queueDeclare(queueName,false,false,false,null); + // 绑定:将一个队列绑定到一个交换机上 + channel.queueBind(queueName,exchangeName,routingKey); + + //5 创建消费者 + QueueingConsumer queueingConsumer=new QueueingConsumer(channel); + + //6 设置 channel + channel.basicConsume(queueName,true,queueingConsumer); + + //7 获取数据 + while(true){ + QueueingConsumer.Delivery delivery=queueingConsumer.nextDelivery(); + String msg=new String(delivery.getBody()); + System.out.println("消费端:"+msg); + } + } +} +``` + + + +## Fanout Exchange + +### 生产者 + +```java +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +/** + *

Fanout Exchange

+ * Fanout Exchange 不处理 routing key,只需要简单的将队列绑定到交换机上,发送到交换机的消息都会被转发到交换机绑定的所有队列上。 + * 消息生产者 + * Created by DHA on 2019/11/19. + */ +public class Producer4FanoutExchange { + public static void main(String[] args) throws IOException, TimeoutException { + + //1 创建一个 Connectionfactory,并进行设置 + ConnectionFactory connectionFactory = new ConnectionFactory(); + connectionFactory.setHost("localhost"); + connectionFactory.setPort(5672); + connectionFactory.setVirtualHost("/"); + + //2 通过连接工厂创建连接 + Connection connection = connectionFactory.newConnection(); + + //3 通过 connecion 创建一个 Channel + Channel channel = connection.createChannel(); + + //4 声明 + //声明 exchange 名称 + String exchangeName="test_fanout_exchange"; + + //5 通过 chanel 发送数据 + for(int i = 0; i < 10; i ++) { + String msg = "Hello World RabbitMQ 4 Fanout Exchange Message ..."; + channel.basicPublish(exchangeName, "", null , msg.getBytes()); + } + + //6 关闭相关连接 + channel.close(); + connection.close(); + } +} +``` + + + +### 消费者 + +```java +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.QueueingConsumer; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +/** + *

Fanout Exchange

+ * Fanout Exchange 不处理 routing key,只需要简单的将队列绑定到交换机上,发送到交换机的消息都会被转发到交换机绑定的所有队列上。 + * 消息消费者 + * Created by DHA on 2019/11/19. + */ +public class Consumer4FanoutExchange { + public static void main(String[] args) throws IOException, TimeoutException, InterruptedException { + + //1 创建一个 Connectionfactory,并进行设置 + ConnectionFactory connectionFactory = new ConnectionFactory(); + connectionFactory.setHost("localhost"); + connectionFactory.setPort(5672); + connectionFactory.setVirtualHost("/"); + + //2 通过连接工厂创建连接 + Connection connection = connectionFactory.newConnection(); + + //3 通过 connecion 创建一个 Channel + Channel channel = connection.createChannel(); + + //4 声明 + String exchangeName="test_fanout_exchange"; + String exchangeType="fanout"; + String queueName="test_fanout_queue"; + String routingKey=""; + + // 声明一个交换机 + channel.exchangeDeclare(exchangeName,exchangeType,true,false,false,null); + // 声明一个队列 + channel.queueDeclare(queueName,false,false,false,null); + // 绑定:将一个队列绑定到一个交换机上 + channel.queueBind(queueName,exchangeName,routingKey); + + //5 创建消费者 + QueueingConsumer queueingConsumer=new QueueingConsumer(channel); + + //6 设置 channel + channel.basicConsume(queueName,true,queueingConsumer); + + //7 获取数据 + while(true){ + QueueingConsumer.Delivery delivery=queueingConsumer.nextDelivery(); + String msg=new String(delivery.getBody()); + System.out.println("消费端:"+msg); + } + } +} +``` + + + +## 设置消息属性 + +### 生产者 + +```java +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeoutException; + +/** + *

消息属性设置

+ * 消息生产者 + * Created by DHA on 2019/11/18. + */ +public class Producer { + public static void main(String[] args) throws IOException, TimeoutException { + + //1 创建一个 Connectionfactory,并进行设置 + ConnectionFactory connectionFactory = new ConnectionFactory(); + connectionFactory.setHost("localhost"); + connectionFactory.setPort(5672); + connectionFactory.setVirtualHost("/"); + + //2 通过连接工厂创建连接 + Connection connection = connectionFactory.newConnection(); + + //3 通过 connecion 创建一个 Channel + Channel channel = connection.createChannel(); + + // 设置自定义属性 + Map headers = new HashMap<>(); + headers.put("attr1", "111"); + headers.put("attr2", "222"); + + //4 设置消息属性 + AMQP.BasicProperties properties=new AMQP.BasicProperties.Builder() + .deliveryMode(2) // 2 表示持久化的投递 + .contentEncoding("UTF-8") // 设置内容编码 + .expiration("10000") // 设置过期时间为 10 秒 + .headers(headers) // 自定义属性 + .build(); + + //5 通过 chanel 发送数据 + for(int i=0;i<5;i++){ + String data="Hello!"; + channel.basicPublish("","test001",properties,data.getBytes()); + } + + //6 关闭相关连接 + channel.close(); + connection.close(); + } +} +``` + + + +### 消费者 + +```java +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.QueueingConsumer; +import com.rabbitmq.client.QueueingConsumer.Delivery; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.TimeoutException; + +/** + *

消息属性设置

+ * 消息消费者 + * Created by DHA on 2019/11/18. + */ +public class Consumer { + public static void main(String[] args) throws IOException, TimeoutException, InterruptedException { + + //1 创建一个 Connectionfactory,并进行设置 + ConnectionFactory connectionFactory = new ConnectionFactory(); + connectionFactory.setHost("localhost"); + connectionFactory.setPort(5672); + connectionFactory.setVirtualHost("/"); + + //2 通过连接工厂创建连接 + Connection connection = connectionFactory.newConnection(); + + //3 通过 connecion 创建一个 Channel + Channel channel = connection.createChannel(); + + //4 声明一个队列 + String queueName="test001"; + channel.queueDeclare(queueName,true,false,false,null); + + //5 创建消费者 + QueueingConsumer queueingConsumer=new QueueingConsumer(channel); + + //6 设置 channel + channel.basicConsume(queueName,true,queueingConsumer); + + //7 获取数据 + while(true){ + Delivery delivery=queueingConsumer.nextDelivery(); + String msg=new String(delivery.getBody()); + System.out.println("消费端:"+msg); + // 获取自定义属性数据 + Map headers=delivery.getProperties().getHeaders(); + System.err.println("headers get attribute attr1 value: " + headers.get("attr1")); + } + } +} +``` + + + +# 五、RabbitMQ 高级特性 + +## 消息100%可靠性投递的解决方案 + +### 生产端可靠性投递 + +- 保障消息成功发出 +- 保障 MQ 节点的成功接收 +- 发送端收到 MQ 节点(Broker)确认应答 +- **完善的消息补偿机制** + +### 解决方案1:消息落库 + +消息落库,对消息状态进行打标。 + +
+ + + +### 解决方案2:二次确认,回调检查 + +消息的延迟投递,做二次确认,回调检查。 + +
+ + + +## 消费端幂等性操作 + +- 唯一 ID + 指纹码 机制,利用数据库主键去重 + + 优点:实现简单 + + 缺点:高并罚下有数据库写入的性能瓶颈 + + 解决方案:根据 ID 进行分库分表进行算法路由 + +- 利用 Redis 原子特性实现 + +## Confirm 消息机制 + +消息的确认是指生产者投递消息后,如果 Broker 收到消息,则会给生产者一个应答,生产者进行接收应答,用来确定这条消息是否正常地发送到 Broker。 + +
+ +实现机制: + +- 第一步:在 channel 上开启确认模式 + + ```java + channel.confirmSelect() + ``` + +- 第二步:在 channel 上添加监听 + + ```java + channel.addConfirmListener() + ``` + + 监听成功和失败的返回结果,根据具体的结果对消息进行重新发送或记录日志等后续处理。 + +### 生产者 + +```java +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.ConfirmListener; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +/** + *

Confirm 消息机制

+ * 消息的确认是指生产者投递消息后,如果 Broker 收到消息,则会给生产者一个应答,生产者进行接收应答,用来确定这条消息是否正常地发送到 Broker。 + * 消息生产者 + * Created by DHA on 2019/11/18. + */ +public class Producer { + public static void main(String[] args) throws IOException, TimeoutException { + + //1 创建一个 Connectionfactory,并进行设置 + ConnectionFactory connectionFactory = new ConnectionFactory(); + connectionFactory.setHost("localhost"); + connectionFactory.setPort(5672); + connectionFactory.setVirtualHost("/"); + + //2 通过连接工厂创建连接 + Connection connection = connectionFactory.newConnection(); + + //3 通过 connecion 创建一个 Channel + Channel channel = connection.createChannel(); + + //4 指定消息投递模式:confirm 模式 + channel.confirmSelect(); + + String exchangeName = "test_confirm_exchange"; + String routingKey = "confirm.save"; + + //5 通过 chanel 发送数据 + String msg="Hello!"; + channel.basicPublish(exchangeName,routingKey,null,msg.getBytes()); + + //6 添加一个确认监听 + channel.addConfirmListener(new ConfirmListener() { + @Override + public void handleAck(long l, boolean b) throws IOException { + System.out.println("------ack!-------"); + } + + @Override + public void handleNack(long l, boolean b) throws IOException { + System.out.println("------Nack!-------"); + } + }); + } +} +``` + + + +### 消费者 + +```java +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.QueueingConsumer; +import com.rabbitmq.client.QueueingConsumer.Delivery; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +/** + *

Confirm 消息机制

+ * 消息的确认是指生产者投递消息后,如果 Broker 收到消息,则会给生产者一个应答,生产者进行接收应答,用来确定这条消息是否正常地发送到 Broker。 + * 消息消费者 + * Created by DHA on 2019/11/18. + */ +public class Consumer { + public static void main(String[] args) throws IOException, TimeoutException, InterruptedException { + + //1 创建一个 Connectionfactory,并进行设置 + ConnectionFactory connectionFactory = new ConnectionFactory(); + connectionFactory.setHost("localhost"); + connectionFactory.setPort(5672); + connectionFactory.setVirtualHost("/"); + + //2 通过连接工厂创建连接 + Connection connection = connectionFactory.newConnection(); + + //3 通过 connecion 创建一个 Channel + Channel channel = connection.createChannel(); + + //4 声明 + String exchangeName="test_confirm_exchange"; + String exchangeType="topic"; + String queueName="test_confirm_queue"; + String routingKey="confirm.#"; + + // 声明一个交换机 + channel.exchangeDeclare(exchangeName,exchangeType,true,false,false,null); + // 声明一个队列 + channel.queueDeclare(queueName,false,false,false,null); + // 绑定:将一个队列绑定到一个交换机上 + channel.queueBind(queueName,exchangeName,routingKey); + + //5 创建消费者 + QueueingConsumer queueingConsumer=new QueueingConsumer(channel); + + //6 设置 channel + channel.basicConsume(queueName,queueingConsumer); + + //7 获取数据 + while(true){ + Delivery delivery=queueingConsumer.nextDelivery(); + String msg=new String(delivery.getBody()); + System.out.println("消费端:"+msg); + } + } +} +``` + + + +## Return 消息机制 + +消息生产者通过制动一个 Exchange 和 routing key,把消息送达到某一个队列中去,然后消费者监听队列,进行消费处理操作。 + +在某些情况下,如果我们在发送消息的时候,当前的 **Exchange 不存在**或者指定的 **routing key路由不到**,此时我们需要监听这种不可达的消息,就要使用 Return Listener。 + +基础 API 有一个配置项 mandatory + +- 如果为 true,那么监听器会接收到路由不可达的消息,然后进行后续处理 +- 如果为 false, 那么 Broker 端自动删除该消息 + + + +
+ + + +### 生产者 + +```java +import com.rabbitmq.client.*; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +/** + *

Return 消息机制

+ * 消息生产者通过制动一个 Exchange 和 routing key,把消息送达到某一个队列中去,然后消费者监听队列,进行消费处理操作。 + * 在某些情况下,如果我们在发送消息的时候,当前的 Exchange 不存在或者指定的 routing key路由不到,此时我们需要监听这种不可达的消息,就要使用 Return Listener。 + * + * 消息生产者 + * Created by DHA on 2019/11/18. + */ +public class Producer { + public static void main(String[] args) throws IOException, TimeoutException { + + //1 创建一个 Connectionfactory,并进行设置 + ConnectionFactory connectionFactory = new ConnectionFactory(); + connectionFactory.setHost("localhost"); + connectionFactory.setPort(5672); + connectionFactory.setVirtualHost("/"); + + //2 通过连接工厂创建连接 + Connection connection = connectionFactory.newConnection(); + + //3 通过 connecion 创建一个 Channel + Channel channel = connection.createChannel(); + + //4 指定消息投递模式:confirmListener 模式 + channel.confirmSelect(); + + String exchangeName = "test_return_exchange"; + String routingKey = "returnListener.save"; + String routingKeyError = "return.save"; + + //5 通过 chanel 发送数据 + String msg="Hello!"; + // mandatory 如果为 true,那么监听器会接收到路由不可达的消息,然后进行后续处理 + // mandatory 如果为 false, 那么 Broker 端自动删除该消息 + channel.basicPublish(exchangeName,routingKeyError,true,null,msg.getBytes()); + + //6 添加一个监听 + channel.addReturnListener(new ReturnListener() { + @Override + public void handleReturn(int replyCode, String replyText, String exchange, + String routingKey, AMQP.BasicProperties properties, byte[] body) + throws IOException { + System.err.println("---------handle return----------"); + System.err.println("replyCode: " + replyCode); + System.err.println("replyText: " + replyText); + System.err.println("exchange: " + exchange); + System.err.println("routingKey: " + routingKey); + System.err.println("properties: " + properties); + System.err.println("body: " + new String(body)); + } + }); + + } +} +``` + + + +### 消费者 + +```java +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.QueueingConsumer; +import com.rabbitmq.client.QueueingConsumer.Delivery; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +/** + *

Return 消息机制

+ * 消息生产者通过制动一个 Exchange 和 routing key,把消息送达到某一个队列中去,然后消费者监听队列,进行消费处理操作。 + * 在某些情况下,如果我们在发送消息的时候,当前的 Exchange 不存在或者指定的 routing key路由不到,此时我们需要监听这种不可达的消息,就要使用 Return Listener。 + * + * 消息消费者 + * Created by DHA on 2019/11/18. + */ +public class Consumer { + public static void main(String[] args) throws IOException, TimeoutException, InterruptedException { + + //1 创建一个 Connectionfactory,并进行设置 + ConnectionFactory connectionFactory = new ConnectionFactory(); + connectionFactory.setHost("localhost"); + connectionFactory.setPort(5672); + connectionFactory.setVirtualHost("/"); + + //2 通过连接工厂创建连接 + Connection connection = connectionFactory.newConnection(); + + //3 通过 connecion 创建一个 Channel + Channel channel = connection.createChannel(); + + //4 声明 + String exchangeName="test_return_exchange"; + String exchangeType="topic"; + String queueName="test_return_queue"; + String routingKey="returnListener.#"; + + // 声明一个交换机 + channel.exchangeDeclare(exchangeName,exchangeType,true,false,false,null); + // 声明一个队列 + channel.queueDeclare(queueName,false,false,false,null); + // 绑定:将一个队列绑定到一个交换机上 + channel.queueBind(queueName,exchangeName,routingKey); + + //5 创建消费者 + QueueingConsumer queueingConsumer=new QueueingConsumer(channel); + + //6 设置 channel + channel.basicConsume(queueName,queueingConsumer); + + //7 获取数据 + while(true){ + Delivery delivery=queueingConsumer.nextDelivery(); + String msg=new String(delivery.getBody()); + System.out.println("消费端:"+msg); + } + } +} +``` + + + +## 消费端自定义监听 + +我们一般在代码中编写 while 循环,进行 consumer.nextDelivery 方法获取下一条消息,然后进行消费处理! + +但是,我们使用自定义的 Counsumer 更加方便,解耦性更强,在实际工作中广泛使用。 + +### 自定义消费者 + +实现步骤: + +- 先继承 `com.rabbitmq.client.DefaultConsumer` +- 再重写 `handleDelivery()` 方法 + +```java +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.DefaultConsumer; +import com.rabbitmq.client.Envelope; + +import java.io.IOException; + +/** + *

自定义消费者

+ * 1 先继承 DefaultConsumer + * 2 然后重写 handleDelivery() 方法 + * + * Created by DHA on 2019/11/20. + */ +public class MyConsumer extends DefaultConsumer{ + public MyConsumer(Channel channel) { + super(channel); + } + + @Override + public void handleDelivery(String consumerTag, Envelope envelope, + AMQP.BasicProperties properties, byte[] body) + throws IOException { + System.err.println("----------consumer message-----------"); + System.err.println("consumerTag:"+consumerTag); + System.err.println("envelope:"+envelope); + System.err.println("properties:"+properties); + System.err.println("body:"+new String(body)); + } +} +``` + + + +### 生产者 + +```java +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +/** + *

消费端自定义监听

+ * 消息生产者 + * Created by DHA on 2019/11/19. + */ +public class Producer { + public static void main(String[] args) throws IOException, TimeoutException { + + //1 创建一个 Connectionfactory,并进行设置 + ConnectionFactory connectionFactory = new ConnectionFactory(); + connectionFactory.setHost("localhost"); + connectionFactory.setPort(5672); + connectionFactory.setVirtualHost("/"); + + //2 通过连接工厂创建连接 + Connection connection = connectionFactory.newConnection(); + + //3 通过 connecion 创建一个 Channel + Channel channel = connection.createChannel(); + + //4 声明 + //声明 exchange 名称 + String exchangeName="test_consumer_exchange"; + String routingKey = "consumer.save"; + + //5 通过 chanel 发送数据 + String msg = "Hello World RabbitMQ 4 Consumer Exchange Message ... "; + channel.basicPublish(exchangeName, routingKey , true,null , msg.getBytes()); + } +} +``` + + + +### 消费者 + +```java +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.QueueingConsumer; +import com.rabbitmq.client.QueueingConsumer.Delivery; + +public class Consumer { + + public static void main(String[] args) throws Exception { + //1 创建一个 Connectionfactory,并进行设置 + ConnectionFactory connectionFactory = new ConnectionFactory(); + connectionFactory.setHost("localhost"); + connectionFactory.setPort(5672); + connectionFactory.setVirtualHost("/"); + + //2 通过连接工厂创建连接 + Connection connection = connectionFactory.newConnection(); + + //3 通过 connecion 创建一个 Channel + Channel channel = connection.createChannel(); + + //4 声明 + String exchangeName = "test_consumer_exchange"; + String exchangeType= "topic"; + String routingKey = "consumer.#"; + String queueName = "test_consumer_queue"; + + // 声明一个交换机 + channel.exchangeDeclare(exchangeName,exchangeType,true,false,false,null); + // 声明一个队列 + channel.queueDeclare(queueName,false,false,false,null); + // 绑定:将一个队列绑定到一个交换机上 + channel.queueBind(queueName,exchangeName,routingKey); + + /* + //5 创建消费者 + QueueingConsumer queueingConsumer=new QueueingConsumer(channel); + + //6 设置 channel + channel.basicConsume(queueName,true,queueingConsumer); + + //7 获取数据 + while(true){ + QueueingConsumer.Delivery delivery=queueingConsumer.nextDelivery(); + String msg=new String(delivery.getBody()); + System.out.println("消费端:"+msg); + } + */ + + //5 消费端自定义监听 使用 MyConsumer 相应实例 + channel.basicConsume(queueName, true, new MyConsumer(channel)); + } +} +``` + + + +## 消费端限流 + +RabbitMQ 提供了一种 QoS(服务质量保证) 功能,**在非自动确认消息的前提下**,如果一定数目的消息(通过基于 Consume 或者 Channel 设置 QoS 值)未被确认前,不进行消费新的消息。 + +涉及到的方法: + +```erlang +void BasicQoS(unit prefetchSize,ushort prefetchCount,bool global) +``` + +- prefetchSize:0 +- prefetchCount:告知 RabbitMQ 不要同时给一个消费者推送多个 N 个消息,即一旦有 N 个消息还没有 ACK,则该 Consumer 将 block 掉,一直到有消息 ack +- golbal:true 表示将上面设置应用于 Channel;true 表示将上面设置应用于 Consumer。 + +注意: + +- prefetchSize 和 global 这两项,RabbitMQ 没有实现,暂且不研究 +- prefetchCount 在 no_ask-false 的情况下生效,即在自动应答的情况下是不生效的 + +### 生产者 + +```java +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +/** + *

消费端限流

+ * 消息生产者 + * Created by DHA on 2019/11/19. + */ +public class Producer { + public static void main(String[] args) throws IOException, TimeoutException { + + //1 创建一个 Connectionfactory,并进行设置 + ConnectionFactory connectionFactory = new ConnectionFactory(); + connectionFactory.setHost("localhost"); + connectionFactory.setPort(5672); + connectionFactory.setVirtualHost("/"); + + //2 通过连接工厂创建连接 + Connection connection = connectionFactory.newConnection(); + + //3 通过 connecion 创建一个 Channel + Channel channel = connection.createChannel(); + + //4 声明 + //声明 exchange 名称 + String exchangeName="test_qos_exchange"; + String routingKey = "qos.save"; + + //5 通过 chanel 发送数据 + for(int i=0;i<5;i++){ + String msg = "Hello World RabbitMQ 4 Qos Message ... "; + channel.basicPublish(exchangeName, routingKey , true,null , msg.getBytes()); + } + } +} +``` + + + +### 消费者 + +```java +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.DefaultConsumer; +import com.rabbitmq.client.Envelope; + +import java.io.IOException; + +/** + *

自定义消费者

+ * 1 先继承 DefaultConsumer + * 2 然后重写 handleDelivery() 方法 + * + * Created by DHA on 2019/11/20. + */ +public class MyConsumer extends DefaultConsumer{ + + // channel 进行签收 + private Channel channel; + + public MyConsumer(Channel channel) { + super(channel); + this.channel=channel; + } + + @Override + public void handleDelivery(String consumerTag, Envelope envelope, + AMQP.BasicProperties properties, byte[] body) + throws IOException { + System.err.println("----------consumer message-----------"); + System.err.println("consumerTag:"+consumerTag); + System.err.println("envelope:"+envelope); + System.err.println("properties:"+properties); + System.err.println("body:"+new String(body)); + + // false 表示不支持批量签收 + channel.basicAck(envelope.getDeliveryTag(),false); + } +} +``` + + + +```java +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; + +/** + *

消费端限流

+ * 消息消费者 + * + * basicQoS(prefetchSize,refetchCount,global) + * - prefetchSize:0 + * - prefetchCount:告知 RabbitMQ 不要同时给一个消费者推送多个 N 个消息,即一旦有 N 个消息还没有 ACK, + * 则该 Consumer 将 block 掉,一直到有消息 ack + * - golbal:true 表示将上面设置应用于 Channel;true 表示将上面设置应用于 Consumer。 + * + * Created by DHA on 2019/11/19. + */ +public class Consumer { + + public static void main(String[] args) throws Exception { + //1 创建一个 Connectionfactory,并进行设置 + ConnectionFactory connectionFactory = new ConnectionFactory(); + connectionFactory.setHost("localhost"); + connectionFactory.setPort(5672); + connectionFactory.setVirtualHost("/"); + + //2 通过连接工厂创建连接 + Connection connection = connectionFactory.newConnection(); + + //3 通过 connecion 创建一个 Channel + Channel channel = connection.createChannel(); + + //4 声明 + String exchangeName = "test_qos_exchange"; + String exchangeType= "topic"; + String routingKey = "qos.#"; + String queueName = "test_qos_queue"; + + // 声明一个交换机 + channel.exchangeDeclare(exchangeName,exchangeType,true,false,false,null); + // 声明一个队列 + channel.queueDeclare(queueName,false,false,false,null); + // 绑定:将一个队列绑定到一个交换机上 + channel.queueBind(queueName,exchangeName,routingKey); + + // 第二个参数为 1,表示一次处理一条消息 + // 第三个参数为 false,表示应用到 Consumer 级别 + channel.basicQos(0,1,false); + + //5 消费端自定义监听 + // 首先将第二个参数设置为 false,进行手动签收 + channel.basicConsume(queueName, false, new MyConsumer(channel)); + } +} +``` + + + +## 消费端 ACK 与重回队列 + +- **消费端的手工 ACK 和 NACK** + + 消费端进行消费时: + + 如果由于业务异常,我们可以进行日志的记录,然后进行补偿; + + 如果由于服务器宕机等严重问题,那么需要手工进行 ACK 保障消费端消费成功 + +- **消费端的重回队列** + + 消费端重回队列是为了对没有成功的消息, 消息会被重新投递给 Broker。一般在使用应用中,都会关闭重回队列,即设置为 false。 + +### 生产者 + +```java +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeoutException; + +/** + *

消费端的手工 ACK 和 NACK

+ * 消息生产者 + * + * Created by DHA on 2019/11/19. + */ +public class Producer { + public static void main(String[] args) throws IOException, TimeoutException { + + //1 创建一个 Connectionfactory,并进行设置 + ConnectionFactory connectionFactory = new ConnectionFactory(); + connectionFactory.setHost("localhost"); + connectionFactory.setPort(5672); + connectionFactory.setVirtualHost("/"); + + //2 通过连接工厂创建连接 + Connection connection = connectionFactory.newConnection(); + + //3 通过 connecion 创建一个 Channel + Channel channel = connection.createChannel(); + + //4 声明 + //声明 exchange 名称 + String exchangeName="test_ack_exchange"; + String routingKey = "ack.save"; + + //5 通过 chanel 发送数据 + for(int i =0; i<5; i ++){ + + Map headers = new HashMap(); + headers.put("num", i); + + AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder() + .deliveryMode(2) + .contentEncoding("UTF-8") + .headers(headers) + .build(); + String msg = "Hello RabbitMQ ACK Message " + i; + channel.basicPublish(exchangeName, routingKey, true, properties, msg.getBytes()); + } + } +} +``` + + + +### 消费者 + +```java +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.DefaultConsumer; +import com.rabbitmq.client.Envelope; + +import java.io.IOException; + +/** + *

消费端的重回队列

+ * 消费端重回队列是为了对没有成功的消息, 消息会被重新投递给 Broker。 + * 一般在使用应用中,都会关闭重回队列,即设置为 false。 + * + * Created by DHA on 2019/11/20. + */ +public class MyConsumer extends DefaultConsumer{ + + // channel 进行签收 + private Channel channel; + + public MyConsumer(Channel channel) { + super(channel); + this.channel=channel; + } + + @Override + public void handleDelivery(String consumerTag, Envelope envelope, + AMQP.BasicProperties properties, byte[] body) + throws IOException { + System.err.println("-----------consume message----------"); + System.err.println("body: " + new String(body)); + + // 为了实验效果明显 + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + Integer num=(Integer) properties.getHeaders().get("num"); + if(num==0){ + // 第二个参数表示是否支持批量签收,如果为 false,表示不支持批量签收 + // 第三个参数表示是否重回队列,如果为 true,表示支持重回队列,则会重回到队列的尾端 + channel.basicNack(envelope.getDeliveryTag(),false,true); + }else{ + // false 表示不支持批量签收 + channel.basicAck(envelope.getDeliveryTag(),false); + } + //channel.basicAck(envelope.getDeliveryTag(),false); + } +} +``` + + + +```java +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; + +/** + *

消费端的手工 ACK 和 NACK

+ * 消息消费者 + * + * Created by DHA on 2019/11/19. + */ +public class Consumer { + + public static void main(String[] args) throws Exception { + //1 创建一个 Connectionfactory,并进行设置 + ConnectionFactory connectionFactory = new ConnectionFactory(); + connectionFactory.setHost("localhost"); + connectionFactory.setPort(5672); + connectionFactory.setVirtualHost("/"); + + //2 通过连接工厂创建连接 + Connection connection = connectionFactory.newConnection(); + + //3 通过 connecion 创建一个 Channel + Channel channel = connection.createChannel(); + + //4 声明 + String exchangeName = "test_ack_exchange"; + String exchangeType= "topic"; + String routingKey = "ack.#"; + String queueName = "test_ack_queue"; + + // 声明一个交换机 + channel.exchangeDeclare(exchangeName,exchangeType,true,false,false,null); + // 声明一个队列 + channel.queueDeclare(queueName,false,false,false,null); + // 绑定:将一个队列绑定到一个交换机上 + channel.queueBind(queueName,exchangeName,routingKey); + + //5 消费端自定义监听 + // 首先将第二个参数 autoACK 设置为 false,进行手动签收 + channel.basicConsume(queueName, false, new MyConsumer(channel)); + } +} +``` + + + +## TTL + +TTL(Time To Live)即生存时间。 + +- RabbitMQ 支持**消息**的过期时间,在消息发送时可以进行指定 +- RabbitMQ 支持**队列**的过期时间,从消息如队列开始计算,只要超过了队列的超时时间配置,那么会自动清除消息 + +## 死信队列(DLX,Dead-Letter-Exchange ) + +利用 DLX,当消息在一个队列中变成死信(dead message)之后,其能被重新 publish 到另一个 Exchange,这个 Exchange 就是 DLX。 + +消息变成死信的几种情况: + +- 消息被拒绝(basic.reject / basic.nack),并且 requeue=false +- 消息 TTL 过期 +- 队列达到最大长度 + +注意: + +- DLX 也是一个正常的 Exchange,和一般的 Exchange 没有区别,它能在任何队列上被指定,实际上就是设置某个队列的属性。 + +- 当这个队列中有死信时,RabbitMQ 就会自动的将这个消息重新发布到设置的 Exchange 上去,进而被路由到另一个队列。 + +- 死信队列设置需要设置 Exchange 和 队列,然后绑定 + + ```java + channel.exchangeDeclare("dlx.exchange", "topic", true, false, null); + channel.queueDeclare("dlx.queue", true, false, false, null); + channel.queueBind("dlx.queue", "dlx.exchange", "#"); + ``` + + 然后我们进行正常声明 Exchange、队列和绑定,此时需要在队列上加上参数 arguments + + ```java + Map agruments = new HashMap(); + agruments.put("x-dead-letter-exchange", "dlx.exchange"); + //这个agruments属性,要设置到声明队列上 + channel.queueDeclare(queueName, true, false, false, agruments); + ``` + +### 生产者 + +```java +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; + +/** + *

死信队列

+ * 利用 DLX,当消息在一个队列中变成死信(dead message)之后, + * 其能被重新 publish 到另一个 Exchange,这个 Exchange 就是 DLX。 + * + * 消息生产者 + * Created by DHA on 2019/11/20. + */ +public class Producer { + + public static void main(String[] args) throws Exception { + + ConnectionFactory connectionFactory = new ConnectionFactory(); + connectionFactory.setHost("localhost"); + connectionFactory.setPort(5672); + connectionFactory.setVirtualHost("/"); + + Connection connection = connectionFactory.newConnection(); + Channel channel = connection.createChannel(); + + String exchange = "test_dlx_exchange"; + String routingKey = "dlx.save"; + + String msg = "Hello RabbitMQ DLX Message"; + + AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder() + .deliveryMode(2) + .contentEncoding("UTF-8") + .expiration("10000") + .build(); + channel.basicPublish(exchange, routingKey, true, properties, msg.getBytes()); + } +} +``` + + + +### 消费者 + +```java +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.DefaultConsumer; +import com.rabbitmq.client.Envelope; + +import java.io.IOException; + +/** + *

自定义消费者

+ * 1 先继承 DefaultConsumer + * 2 然后重写 handleDelivery() 方法 + * + * Created by DHA on 2019/11/20. + */ +public class MyConsumer extends DefaultConsumer{ + public MyConsumer(Channel channel) { + super(channel); + } + + @Override + public void handleDelivery(String consumerTag, Envelope envelope, + AMQP.BasicProperties properties, byte[] body) + throws IOException { + System.err.println("----------consumer message-----------"); + System.err.println("consumerTag:"+consumerTag); + System.err.println("envelope:"+envelope); + System.err.println("properties:"+properties); + System.err.println("body:"+new String(body)); + } +} +``` + + + +```java +import java.util.HashMap; +import java.util.Map; + +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; + +/** + *

死信队列

+ * 利用 DLX,当消息在一个队列中变成死信(dead message)之后, + * 其能被重新 publish 到另一个 Exchange,这个 Exchange 就是 DLX。 + * + * 消息消费者 + * Created by DHA on 2019/11/20. + */ +public class Consumer { + + public static void main(String[] args) throws Exception { + + ConnectionFactory connectionFactory = new ConnectionFactory(); + connectionFactory.setHost("localhost"); + connectionFactory.setPort(5672); + connectionFactory.setVirtualHost("/"); + + Connection connection = connectionFactory.newConnection(); + Channel channel = connection.createChannel(); + + // 这就是一个普通的交换机 和 队列 以及路由 + String exchangeName = "test_dlx_exchange"; + String routingKey = "dlx.#"; + String queueName = "test_dlx_queue"; + + channel.exchangeDeclare(exchangeName, "topic", true, false, null); + Map agruments = new HashMap(); + agruments.put("x-dead-letter-exchange", "dlx.exchange"); + //这个agruments属性,要设置到声明队列上 + channel.queueDeclare(queueName, true, false,false,agruments); + channel.queueBind(queueName, exchangeName, routingKey); + + //要进行死信队列的声明: + channel.exchangeDeclare("dlx.exchange", "topic", true, false, null); + channel.queueDeclare("dlx.queue", true, false, false, null); + channel.queueBind("dlx.queue", "dlx.exchange", "#"); + + channel.basicConsume(queueName, true, new MyConsumer(channel)); + } +} +``` + diff --git "a/docs/LeetCode/12_\347\256\227\346\263\225\346\200\235\346\203\263.md" "b/docs/LeetCode/12_\347\256\227\346\263\225\346\200\235\346\203\263.md" deleted file mode 100644 index e54d8fcb..00000000 --- "a/docs/LeetCode/12_\347\256\227\346\263\225\346\200\235\346\203\263.md" +++ /dev/null @@ -1,1570 +0,0 @@ -# 常见的算法思想 - -## 一、排序思想 - -### 1、前K个高频元素(347) - -[347. 前K个高频元素](https://leetcode-cn.com/problems/top-k-frequent-elements/) - -问题描述:给定一个非空的整数数组,返回其中出现频率前 **k** 高的元素。 - -**示例 1:** - -``` -输入: nums = [1,1,1,2,2,3], k = 2 -输出: [1,2] -``` - -**示例 2:** - -``` -输入: nums = [1], k = 1 -输出: [1] -``` - -**说明:** - -- 你可以假设给定的 *k* 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。 -- 你的算法的时间复杂度**必须**优于 O(*n* log *n*) , *n* 是数组的大小。 - -```java -//思路一:桶排序 -//设置若干个桶,每个桶存储出现频率相同的数,并且桶的下标代表桶中数出现的频率,即第 i 个桶中存储的数出现的频率为 i。 -//把数都放到桶之后,从后向前遍历桶,最先得到的 k 个数就是出现频率最多的的 k 个数。 -class Solution { - - public List topKFrequent(int[] nums, int k) { - //统计数组中元素出现频率 - HashMap map = new HashMap<>(); - for(int num : nums){ - map.put(num, map.getOrDefault(num,0)+1); - } - - //每个桶存储出现频率相同的数,并且桶的下标代表桶中数出现的频率 - List[] buckets = new ArrayList[nums.length+1]; - for(Integer num : map.keySet()){ - Integer freq = map.get(num); - if(buckets[freq] == null){ - buckets[freq] = new ArrayList<>(); - } - buckets[freq].add(num); - } - - List res = new ArrayList<>(); - for(int i=buckets.length-1;i>=0 && res.size() topKFrequent(int[] nums, int k) { - List res = new ArrayList<>(); - - HashMap map = new HashMap<>(); - for(int num : nums){ - int freq = map.getOrDefault(num,0); - map.put(num,++freq); - } - - Set set = map.keySet(); - Element[] elements = new Element[set.size()]; - int i=0; - for(Integer key : set){ - elements[i] = new Element(key,map.get(key)); - i++; - } - - PriorityQueue pq = new PriorityQueue<>(); - for(Element element : elements){ - pq.add(element); - if(pq.size() > k){ - pq.poll(); - } - } - - while(!pq.isEmpty()){ - Element element = pq.poll(); - res.add(element.num); - } - return res; - } -} - -class Element implements Comparable{ - int num; - int freq; - Element(int num,int freq){ - this.num = num; - this.freq = freq; - } - - @Override - public int compareTo(Element o) { - return this.freq - o.freq; - } -} -``` - - - -### 2、根据字符出现频率排序(451) - -[451. 根据字符出现频率排序](https://leetcode-cn.com/problems/sort-characters-by-frequency/) - -问题描述:给定一个字符串,请将字符串里的字符按照出现的频率降序排列。 - -**示例 1:** - -``` -输入: -"tree" - -输出: -"eert" - -解释: -'e'出现两次,'r'和't'都只出现一次。 -因此'e'必须出现在'r'和't'之前。此外,"eetr"也是一个有效的答案。 -``` - -**示例 2:** - -``` -输入: -"cccaaa" - -输出: -"cccaaa" - -解释: -'c'和'a'都出现三次。此外,"aaaccc"也是有效的答案。 -注意"cacaca"是不正确的,因为相同的字母必须放在一起。 -``` - -**示例 3:** - -``` -输入: -"Aabb" - -输出: -"bbAa" - -解释: -此外,"bbaA"也是一个有效的答案,但"Aabb"是不正确的。 -注意'A'和'a'被认为是两种不同的字符。 -``` - -``` -class Solution { - public String frequencySort(String s) { - //统计 s 中字符串出现的次数 - HashMap map = new HashMap<>(); - for(int i=0;i[] buckets = new ArrayList[s.length()+1]; - for(Character c : map.keySet()){ - Integer freq = map.get(c); - if(buckets[freq]==null){ - buckets[freq] = new ArrayList<>(); - } - buckets[freq].add(c); - } - - StringBuilder res = new StringBuilder(); - for(int freq = buckets.length-1;freq>=0;freq--){ - if(buckets[freq] == null){ - continue; - } - List list = buckets[freq]; //list 中所有元素都出现了 freq 次数 - for(Character c : list){ - for(int i=0;i - -```java -//思路如上图: -//i指向值为0的元素,swap(i,zero+1) ,交换后,i位置元素是1(nums[zero+1..i+1]==1), -//不需要处理,i直接+1 -//i指向值为1的元素,不需要处理直接+1 -//i指向值为2的元素,swap(i,two-1),交换后,two-1位置元素就是2 -public void sortColors(int[] nums) { - int zero = -1; - int two = nums.length; - int i = 0; - while(i k-1 - assert p > (k-1); - hi--; - } - } - return nums[0]; -} - -private int partition(int[] nums,int l,int r){ - int pivot = nums[l]; - - while(l < r){ - //从右向左查找第一个 < pivot 的元素 - while(l < r && nums[r] >= pivot){ - r--; - } - nums[l] = nums[r]; - //从左向右查找第一个 > pivot 的元素 - while(l < r && nums[l] <= pivot){ - l++; - } - nums[r] = nums[l]; - } - nums[l] = pivot; - return l; -} - - -``` - -## 二、分治思想 - -### 1、为运算表达式设计优先级(241) - -[241. 为运算表达式设计优先级](https://leetcode-cn.com/problems/different-ways-to-add-parentheses/) - -给定一个含有数字和运算符的字符串,为表达式添加括号,改变其运算优先级以求出不同的结果。你需要给出所有可能的组合的结果。有效的运算符号包含 `+`, `-` 以及 `*` 。 - -**示例 1:** - -``` -输入: "2-1-1" -输出: [0, 2] -解释: -((2-1)-1) = 0 -(2-(1-1)) = 2 - -``` - -**示例 2:** - -``` -输入: "2*3-4*5" -输出: [-34, -14, -10, -10, 10] -解释: -(2*(3-(4*5))) = -34 -(2*((3-4)*5)) = -10 -((2*3)-(4*5)) = -14 -((2*(3-4))*5) = -10 -(((2*3)-4)*5) = 10 - -``` - -```java -public List diffWaysToCompute(String input) { - List res = new ArrayList<>(); - - for(int i=0;i left = diffWaysToCompute(input.substring(0,i)); - List right = diffWaysToCompute(input.substring(i+1)); - System.out.println(input); - System.out.println("left:"+left+",right:"+right); - System.out.println("=================="); - for(int l : left){ - for(int r : right){ - switch (c){ - case '+' : - res.add(l+r); - break; - case '-' : - res.add(l-r); - break; - case '*' : - res.add(l*r); - break; - } - } - } - } - } - - if(res.size() ==0){ //说明input 是一个数字 - res.add(Integer.valueOf(input)); - } - return res; -} - -``` - -### 2、最大子序和(53) - -[53. 最大子序和](https://leetcode-cn.com/problems/maximum-subarray/) - -给定一个整数数组 `nums` ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 - -**示例:** - -``` -输入: [-2,1,-3,4,-1,2,1,-5,4], -输出: 6 -解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。 - -``` - -**进阶:** - -如果你已经实现复杂度为 O(*n*) 的解法,尝试使用更为精妙的分治法求解。 - -```java -//思路:分治法 -//如果把数组分成左右两段,那么加和最大的连续子序列, -//要么出现在数组的左半部分, -//要么出现在数组的右半部分, -//要么出现在中间,即从左半部分和右半部分相邻的地方各取一段。 -public int maxSubArray(int[] nums) { - return maxSubArray(nums,0,nums.length-1); -} - -private int maxSubArray(int[] nums,int l,int r){ - if(l==r){ - return nums[l]; - } - int mid = l + (r-l)/2; - int leftSubMax = maxSubArray(nums,l,mid); - int rightSubMax = maxSubArray(nums,mid+1,r); - - int subMax = nums[mid]; - int sum = subMax; //中间元素必然在该最大连续子序列中 - - //从中间向左半数组遍历 - for(int i=mid-1;i>=l;i--){ - sum += nums[i]; - subMax = Math.max(subMax,sum); - } - sum = subMax; - //从中间向右半数组遍历 - for(int i=mid+1;i<=r;i++){ - sum += nums[i]; - subMax = Math.max(subMax,sum); - } - return max3(leftSubMax,subMax,rightSubMax); -} - -private int max3(int a,int b,int c){ - int tmp = (a>b) ? a:b; - return (tmp>c? tmp : c); -} - -``` - - - -### 3、不同的二叉搜索树 II(95) - -[95. 不同的二叉搜索树 II](https://leetcode-cn.com/problems/unique-binary-search-trees-ii/) - -给定一个整数 *n*,生成所有由 1 ... *n* 为节点所组成的**二叉搜索树**。 - -**示例:** - -``` -输入: 3 -输出: -[ - [1,null,3,2], - [3,2,null,1], - [3,1,null,null,2], - [2,1,3], - [1,null,2,null,3] -] -解释: -以上的输出对应以下 5 种不同结构的二叉搜索树: - - 1 3 3 2 1 - \ / / / \ \ - 3 2 1 1 3 2 - / / \ \ - 2 1 2 3 - -``` - -```java -public List generateTrees(int n) { - if(n==0){ - return new LinkedList<>(); - } - return generateSubTrees(1,n); -} - -public List generateSubTrees(int s,int e){ - List res = new LinkedList<>(); - if(s>e){ - res.add(null); - return res; - } - for(int i=s;i<=e;i++){ - List left = generateSubTrees(s,i-1); - List right = generateSubTrees(i+1,e); - for(TreeNode l : left){ - for(TreeNode r : right){ - TreeNode root = new TreeNode(i); - root.left = l; - root.right = r; - res.add(root); - } - } - } - return res; -} - -``` - -## 三、贪心思想 - -保证每次操作都是局部最优的,并且最后得到的结果是全局最优的。 - -### *1、分配饼干(455) - -[455. Assign Cookies (Easy)](https://leetcode.com/problems/assign-cookies/description/) - -```html -Input: [1,2], [1,2,3] -Output: 2 - -Explanation: You have 2 children and 3 cookies. The greed factors of 2 children are 1, 2. -You have 3 cookies and their sizes are big enough to gratify all of the children, -You need to output 2. - -``` - -题目描述:每个孩子都有一个满足度,每个饼干都有一个大小,只有饼干的大小大于等于一个孩子的满足度,该孩子才会获得满足。求解最多可以获得满足的孩子数量。 - -给一个孩子的饼干应当尽量小又能满足该孩子,这样大饼干就能拿来给满足度比较大的孩子。因为最小的孩子最容易得到满足,所以先满足最小的孩子。 - -证明:假设在某次选择中,贪心策略选择给当前满足度最小的孩子分配第 m 个饼干,第 m 个饼干为可以满足该孩子的最小饼干。假设存在一种最优策略,给该孩子分配第 n 个饼干,并且 m < n。我们可以发现,经过这一轮分配,贪心策略分配后剩下的饼干一定有一个比最优策略来得大。因此在后续的分配中,贪心策略一定能满足更多的孩子。也就是说不存在比贪心策略更优的策略,即贪心策略就是最优策略。 - -```java -//思路:贪心策略 -//每次选取最大的饼干给最"贪心"的小朋友。 -public int findContentChildren(int[] g, int[] s) { - Arrays.sort(g); - Arrays.sort(s); - - int gi = g.length-1; - int si = s.length-1; - int res=0; - while(gi>=0 && si>=0){ - if(s[si]>=g[gi]){ - res++; - gi--; - si--; - }else{ //不能满足这个小朋友,抱歉,饼干就不分给他了。 - gi--; - } - } - return res; -} -``` - -### *2、不重叠的区间个数(453) - -[435. Non-overlapping Intervals (Medium)](https://leetcode.com/problems/non-overlapping-intervals/description/) - -```html -Input: [ [1,2], [1,2], [1,2] ] - -Output: 2 - -Explanation: You need to remove two [1,2] to make the rest of intervals non-overlapping. - -``` - -```html -Input: [ [1,2], [2,3] ] - -Output: 0 - -Explanation: You don't need to remove any of the intervals since they're already non-overlapping. - -``` - -题目描述:计算让一组区间不重叠所需要移除的区间个数。 - -先计算最多能组成的不重叠区间个数,然后用区间总个数减去不重叠区间的个数。 - -在每次选择中,区间的结尾最为重要,选择的区间结尾越小,留给后面的区间的空间越大,那么后面能够选择的区间个数也就越大。 - -按区间的结尾进行排序,每次选择结尾最小,并且和前一个区间不重叠的区间。 - -```java -public int eraseOverlapIntervals(Interval[] intervals) { - if (intervals.length == 0) { - return 0; - } - Arrays.sort(intervals, Comparator.comparingInt(o -> o.end)); - int cnt = 1; - int end = intervals[0].end; - for (int i = 1; i < intervals.length; i++) { - if (intervals[i].start < end) { - continue; - } - end = intervals[i].end; - cnt++; - } - return intervals.length - cnt; -} - -``` - -使用 lambda 表示式创建 Comparator 会导致算法运行时间过长,如果注重运行时间,可以修改为普通创建 Comparator 语句: - -```java -Arrays.sort(intervals, new Comparator() { - @Override - public int compare(Interval o1, Interval o2) { - return o1.end - o2.end; - } -}); - -``` - -### 3、投飞镖刺破气球(452) - -[452. Minimum Number of Arrows to Burst Balloons (Medium)](https://leetcode.com/problems/minimum-number-of-arrows-to-burst-balloons/description/) - -``` -Input: -[[10,16], [2,8], [1,6], [7,12]] - -Output: -2 - -``` - -题目描述:气球在一个水平数轴上摆放,可以重叠,飞镖垂直投向坐标轴,使得路径上的气球都会刺破。求解最小的投飞镖次数使所有气球都被刺破。 - -也是计算不重叠的区间个数,不过和 Non-overlapping Intervals 的区别在于,[1, 2] 和 [2, 3] 在本题中算是重叠区间。 - -```java -public int findMinArrowShots(int[][] points) { - if (points.length == 0) { - return 0; - } - Arrays.sort(points, Comparator.comparingInt(o -> o[1])); - int cnt = 1, end = points[0][1]; - for (int i = 1; i < points.length; i++) { - if (points[i][0] <= end) { - continue; - } - cnt++; - end = points[i][1]; - } - return cnt; -} - -``` - -### 4、根据身高和序号重组队列(406) - -[406. Queue Reconstruction by Height(Medium)](https://leetcode.com/problems/queue-reconstruction-by-height/description/) - -```html -Input: -[[7,0], [4,4], [7,1], [5,0], [6,1], [5,2]] - -Output: -[[5,0], [7,0], [5,2], [6,1], [4,4], [7,1]] - - -``` - -题目描述:一个学生用两个分量 (h, k) 描述,h 表示身高,k 表示排在前面的有 k 个学生的身高比他高或者和他一样高。 - -为了使插入操作不影响后续的操作,身高较高的学生应该先做插入操作,否则身高较小的学生原先正确插入的第 k 个位置可能会变成第 k+1 个位置。 - -身高降序、k 值升序,然后按排好序的顺序插入队列的第 k 个位置中。 - -```java -public int[][] reconstructQueue(int[][] people) { - if (people == null || people.length == 0 || people[0].length == 0) { - return new int[0][0]; - } - Arrays.sort(people, (a, b) -> (a[0] == b[0] ? a[1] - b[1] : b[0] - a[0])); - List queue = new ArrayList<>(); - for (int[] p : people) { - queue.add(p[1], p); - } - return queue.toArray(new int[queue.size()][]); -} - - -``` - -### 5、分隔字符串使同种字符出现在一起(763) - -[763. Partition Labels (Medium)](https://leetcode.com/problems/partition-labels/description/) - -```html -Input: S = "ababcbacadefegdehijhklij" -Output: [9,7,8] -Explanation: -The partition is "ababcbaca", "defegde", "hijhklij". -This is a partition so that each letter appears in at most one part. -A partition like "ababcbacadefegde", "hijhklij" is incorrect, because it splits S into less parts. - - -``` - -```java -public List partitionLabels(String S) { - int[] lastIndexsOfChar = new int[26]; - for (int i = 0; i < S.length(); i++) { - lastIndexsOfChar[char2Index(S.charAt(i))] = i; - } - List partitions = new ArrayList<>(); - int firstIndex = 0; - while (firstIndex < S.length()) { - int lastIndex = firstIndex; - for (int i = firstIndex; i < S.length() && i <= lastIndex; i++) { - int index = lastIndexsOfChar[char2Index(S.charAt(i))]; - if (index > lastIndex) { - lastIndex = index; - } - } - partitions.add(lastIndex - firstIndex + 1); - firstIndex = lastIndex + 1; - } - return partitions; -} - -private int char2Index(char c) { - return c - 'a'; -} - - -``` - -### 6、种植花朵(605) - -[605. Can Place Flowers (Easy)](https://leetcode.com/problems/can-place-flowers/description/) - -```html -Input: flowerbed = [1,0,0,0,1], n = 1 -Output: True - - -``` - -题目描述:花朵之间至少需要一个单位的间隔,求解是否能种下 n 朵花。 - -```java -public boolean canPlaceFlowers(int[] flowerbed, int n) { - int len = flowerbed.length; - int cnt = 0; - for (int i = 0; i < len && cnt < n; i++) { - if (flowerbed[i] == 1) { - continue; - } - int pre = i == 0 ? 0 : flowerbed[i - 1]; - int next = i == len - 1 ? 0 : flowerbed[i + 1]; - if (pre == 0 && next == 0) { - cnt++; - flowerbed[i] = 1; - } - } - return cnt >= n; -} - - -``` - -### 7、判断是否为子序列(392) - -[392. Is Subsequence (Medium)](https://leetcode.com/problems/is-subsequence/description/) - -```html -s = "abc", t = "ahbgdc" -Return true. - - -``` - -```java -public boolean isSubsequence(String s, String t) { - int index = -1; - for (char c : s.toCharArray()) { - index = t.indexOf(c, index + 1); - if (index == -1) { - return false; - } - } - return true; -} - - -``` - -### 8、修改一个数成为非递减数组(665) - -[665. Non-decreasing Array (Easy)](https://leetcode.com/problems/non-decreasing-array/description/) - -```html -Input: [4,2,3] -Output: True -Explanation: You could modify the first 4 to 1 to get a non-decreasing array. - - -``` - -题目描述:判断一个数组能不能只修改一个数就成为非递减数组。 - -在出现 nums[i] < nums[i - 1] 时,需要考虑的是应该修改数组的哪个数,使得本次修改能使 i 之前的数组成为非递减数组,并且 **不影响后续的操作** 。优先考虑令 nums[i - 1] = nums[i],因为如果修改 nums[i] = nums[i - 1] 的话,那么 nums[i] 这个数会变大,就有可能比 nums[i + 1] 大,从而影响了后续操作。还有一个比较特别的情况就是 nums[i] < nums[i - 2],只修改 nums[i - 1] = nums[i] 不能使数组成为非递减数组,只能修改 nums[i] = nums[i - 1]。 - -```java -public boolean checkPossibility(int[] nums) { - int cnt = 0; - for (int i = 1; i < nums.length && cnt < 2; i++) { - if (nums[i] >= nums[i - 1]) { - continue; - } - cnt++; - if (i - 2 >= 0 && nums[i - 2] > nums[i]) { - nums[i] = nums[i - 1]; - } else { - nums[i - 1] = nums[i]; - } - } - return cnt <= 1; -} - - -``` - -### 9、股票的最大收益(122) - -[122. Best Time to Buy and Sell Stock II (Easy)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-ii/description/) - -题目描述:一次股票交易包含买入和卖出,多个交易之间不能交叉进行。 - -对于 [a, b, c, d],如果有 a <= b <= c <= d ,那么最大收益为 d - a。而 d - a = (d - c) + (c - b) + (b - a) ,因此当访问到一个 prices[i] 且 prices[i] - prices[i-1] > 0,那么就把 prices[i] - prices[i-1] 添加到收益中,从而在局部最优的情况下也保证全局最优。 - -```java -public int maxProfit(int[] prices) { - int profit = 0; - for (int i = 1; i < prices.length; i++) { - if (prices[i] > prices[i - 1]) { - profit += (prices[i] - prices[i - 1]); - } - } - return profit; -} - - -``` - -### 10、子数组最大的和(53) - -[53. Maximum Subarray (Easy)](https://leetcode.com/problems/maximum-subarray/description/) - -```html -For example, given the array [-2,1,-3,4,-1,2,1,-5,4], -the contiguous subarray [4,-1,2,1] has the largest sum = 6. - - -``` - -```java -public int maxSubArray(int[] nums) { - if (nums == null || nums.length == 0) { - return 0; - } - int preSum = nums[0]; - int maxSum = preSum; - for (int i = 1; i < nums.length; i++) { - preSum = preSum > 0 ? preSum + nums[i] : nums[i]; - maxSum = Math.max(maxSum, preSum); - } - return maxSum; -} - - -``` - -### 11、买入和售出股票最大的收益(121) - -[121. Best Time to Buy and Sell Stock (Easy)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock/description/) - -题目描述:只进行一次交易。 - -只要记录前面的最小价格,将这个最小价格作为买入价格,然后将当前的价格作为售出价格,查看当前收益是不是最大收益。 - -```java -public int maxProfit(int[] prices) { - int n = prices.length; - if (n == 0) return 0; - int soFarMin = prices[0]; - int max = 0; - for (int i = 1; i < n; i++) { - if (soFarMin > prices[i]) soFarMin = prices[i]; - else max = Math.max(max, prices[i] - soFarMin); - } - return max; -} - -``` - -## 五、LRU 思想 - -### LRU 缓存机制(146) - -[146. LRU缓存机制](https://leetcode-cn.com/problems/lru-cache/) - -```java -class LRUCache { - private List list; //使用 LinkedList 来代替双向链表,维护数据顺序 - private Map map; //使用 map 保存数据,实现 O(1) 的访问 - private int size;//链表长度 - private int capacity;//缓存总量,方便后面判断是否链表长度是否超过缓存容量 - - public LRUCache(int capacity) { - list = new LinkedList<>(); - map = new HashMap<>(); - size=0; - this.capacity = capacity; - } - - //判断 key 在 HashMap 中是否存在, - //如果存在,则把对应的节点移动到链表头部,并返回对应的 value 值; - //如果不存在,则返回-1。 - public int get(int key) { - if(!map.containsKey(key)){ - return -1; - } - //将对应节点移动到链表头部 - list.remove((Integer)key); - list.add(0,key); - return map.get(key); - } - - //如果 key 在 HashMap 中存在,则先重置对应的 value 值,将节点移动到链表的头部; - //如果 key 在 HashMap 中不存在,则新建一个节点,并将节点放到链表的头部。 - //注意:当 Cache 存满的时候,将链表最后一个节点删除。 - public void put(int key, int value) { - if(map.containsKey(key)){ - map.put(key,value); - //将节点移到链表的头部 - list.remove((Integer)key); - list.add(0,key); - }else{ - list.add(0,key); - map.put(key,value); - size++; - if(size>capacity){ //每次插入新元素,都会检查是否超过 capacity,只会多出一个元素 - //如果超过容量,则删除链表最后一个元素,同时要去相应的 map 中删除元素 - int lastElement = list.remove(list.size()-1); - map.remove(lastElement); - } - } - } -} - -@Test -public void test(){ - LRUCache cache = new LRUCache( 2); - - cache.put(1, 1); - cache.put(2, 2); - System.out.println(cache.get(1)); // 返回 1 - cache.put(3, 3); // 该操作会使得密钥 2 作废 - System.out.println(cache.get(2)); // 返回 -1 (未找到) - cache.put(4, 4); // 该操作会使得密钥 1 作废 - System.out.println(cache.get(1)); // 返回 -1 (未找到) - System.out.println(cache.get(3)); // 返回 3 - System.out.println(cache.get(4)); // 返回 4 -} -``` - -## 六、抢红包算法 - -线段切割法:在一条线段上找 (N-1) 个随机点,就可以将该线段随机且公平地切割成 N 段。 - -```java -//思路:线段切割法 -//在一条线段上找 N-1 个随机点,就可以将该线段随机且公平地切割成 N 段。 -public class RedPocket { - public List generatePacketsByLineCutting(int people, int money) { - List res = new ArrayList<>(); - - //在[0,money] 这条线段上找 (people-1) 个随机点 - Set points = new TreeSet<>(); //存储(people-1)个随机点 - //使用 treeset 存储,保证这些点是有序的,方便后面的切分 - - Random random = new Random(); - - while(points.size() < people-1){ - points.add(random.nextInt(money)); - } - //此时 points.size() == people-1 - points.add(money); - - int pre=0; - for(int point:points){ - res.add(point-pre); - pre = point; - } - return res; - } - - @Test - public void test(){ - int people =10; - int money = 100; - List res= generatePacketsByLineCutting(people,money); - System.out.println(res); - int result =0; - for(Integer d:res){ - result += d; - } - System.out.println("reslut:"+result); - } -} -``` - -## 七、洗牌算法 - -```java -// 算法的基本思想:每次从一组数中随机选出一个数, -// 然后与最后一个数交换位置,并且不再考虑最后一个数。 -// 时间复杂度:O(N) -public static void shuffle(int[] nums){ - Random random = new Random(); - for(int i=nums.length-1;i>=0;i--){ - int j = random.nextInt(i+1); //在[0,i]范围内 - if(i==j){ //一个小优化 - continue; - } - swap(nums,i,j); // i 指向数组最后一个元素 - } -} - -private static void swap(int[] nums,int i,int j){ - int tmp = nums[i]; - nums[i] = nums[j]; - nums[j] = tmp; -} - -public static void main(String[] args) { - int[] nums = {1,2,3,4,5,6,7,8}; - shuffle(nums); - for(int num:nums){ - System.out.println(num); - } -} -``` - -## 八、海量数据处理 - - - -## 五、DFS - -### 1、岛屿的个数(200) - -[200. 岛屿的个数](https://leetcode-cn.com/problems/number-of-islands/) - -给定一个由 `'1'`(陆地)和 `'0'`(水)组成的的二维网格,计算岛屿的数量。一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设网格的四个边均被水包围。 - -**示例 1:** - -``` -输入: -11110 -11010 -11000 -00000 - -输出: 1 - - -``` - -**示例 2:** - -``` -输入: -11000 -11000 -00100 -00011 - -输出: 3 - - -``` - -```java -private int m; -private int n; - -//用来标记是否是同一个岛屿 -private boolean[][] visited; - -//TODO:二维数组四个方向查找的小技巧 -private int[][] d={{-1,0},{0,1},{1,0},{0,-1}}; - -private boolean isArea(int x,int y){ - return (x>=0 && x=0 && y=0 && x=0 && y=0 && x=0 && y=0 && x=0 && y=matrix[x][y])){ - dfs(matrix,visited,newx,newy); - } - } - } -} - -public List pacificAtlantic(int[][] matrix) { - List res=new ArrayList<>(); - m=matrix.length; - if(m==0){ - return res; - } - n=matrix[0].length; - pacific=new boolean[m][n]; - atlantic=new boolean[m][n]; - - for(int i=0;i> res; - -private boolean[] visited; //标记元素是否已经被访问 - -public List> permute(int[] nums) { - res = new ArrayList<>(); - if(nums==null || nums.length==0){ - return res; - } - visited = new boolean[nums.length]; - List p = new ArrayList<>(); - generatePermutation(nums,0,p); - return res; -} - -//产生排列 -//p中保存一个存在index个元素的排列 -//向这个排列的末尾添加第(index+1)个元素,获得包含(index+1)个元素的排列 -private void generatePermutation(int[] nums,int index,List p){ - if(index == nums.length){ - res.add(new ArrayList<>(p)); - return; - } - - for(int i=0;i> res=permute(nums); - for(List list : res){ - System.out.println(list); - } -} -``` - -### 2、全排列 II(47) - -[47. 全排列 II](https://leetcode-cn.com/problems/permutations-ii/) - -```java -//思路: -//1、先对数组进行排序,这样如果存在重复元素,则这些元素是相邻的 -//2、对于重复元素,最后一个元素加入排列 -private List> res; - -private boolean[] visited; - -public List> permuteUnique(int[] nums) { - res = new ArrayList<>(); - if(nums==null || nums.length==0){ - return res; - } - Arrays.sort(nums); - - visited = new boolean[nums.length]; - - List p = new ArrayList<>(); - generatePermutation(nums,0,p); - - return res; -} - -private void generatePermutation(int[] nums,int index,List p){ - if(index==nums.length){ - res.add(new ArrayList<>(p)); - return; - } - for(int i=0;i0 && (nums[i-1]==nums[i] && !visited[i-1])){ - // 为甚要加上这个 !visited[i-1] - //实际上是指 nums[i-1] 和 nums[i] 相邻元素,并且都没有被访问,则后面的元素加入排列 - continue; - } - p.add(nums[i]); - visited[i]=true; - generatePermutation(nums,index+1,p); - visited[i]=false; - p.remove(p.size()-1); - } - } - return; -} - -@Test -public void test(){ - int[] nums = {1,1,2}; - List> res=permuteUnique(nums); - for(List list : res){ - System.out.println(list); - } -} -``` - -### *3、字母大小写全排列(784) - -[784. 字母大小写全排列](https://leetcode-cn.com/problems/letter-case-permutation/) - -```java -private List res; - -public List letterCasePermutation(String S) { - res = new ArrayList<>(); - if(S.length()==0){ - res.add(S); - return res; - } - - StringBuilder builder = new StringBuilder(S); - replaceCh(0,builder); - return res; -} - -//替换 index 位置元素 -private void replaceCh(int index, StringBuilder builder){ - if(index==builder.length()){ - res.add(builder.toString()); - return; - } - - char ch = builder.charAt(index); - if(Character.isLetter(ch)){ - builder.setCharAt(index,Character.toLowerCase(ch)); - replaceCh(index+1,builder); - builder.setCharAt(index,Character.toUpperCase(ch)); - replaceCh(index+1,builder); - }else{ - replaceCh(index+1,builder); - } - return; -} - -@Test -public void test(){ - //String S = "a1b2"; - String S = "3z4"; - System.out.println(letterCasePermutation(S)); -} -``` - -## 二、组合问题 - -### *1、组合(77) - -[77. 组合](https://leetcode-cn.com/problems/combinations/) - -```java -private List> res; - -public List> combine(int n, int k) { - res = new ArrayList<>(); - if(n<=0 || k<=0 || n c= new ArrayList<>(); - findCombination(n,k,0,c); - - return res; -} - -//c存储已经找到的组合 -//从start开始搜索新的元素 -private void findCombination(int n,int k,int start,List c){ - if(k==c.size()){ //已经找到 k 个元素 - res.add(new ArrayList<>(c)); - return; - } - for(int i=start;i<=n;i++){ - c.add(i); - findCombination(n,k,i+1,c); - c.remove(c.size()-1); - } - return; -} - -@Test -public void test(){ - int n=4; - int k=2; - List> res=combine(n,k); - for(List list : res){ - System.out.println(list); - } -} -``` - - - -### 2、组合总和(39) - -[39. 组合总和](https://leetcode-cn.com/problems/combination-sum/) - -### 3、组合总和 II(40) - -[40. 组合总和 II](https://leetcode-cn.com/problems/combination-sum-ii/) - -### 4、组合总和 III(216) - -[216. 组合总和 III](https://leetcode-cn.com/problems/combination-sum-iii/) - -### 78 - -### 90 - -### 401 \ No newline at end of file diff --git "a/docs/LeetCode/8_\345\255\227\347\254\246\344\270\262.md" "b/docs/LeetCode/8_\345\255\227\347\254\246\344\270\262.md" deleted file mode 100644 index 309bacd6..00000000 --- "a/docs/LeetCode/8_\345\255\227\347\254\246\344\270\262.md" +++ /dev/null @@ -1,342 +0,0 @@ -# 字符串 - -## 一、KMP 算法???? - -
- -## 二、字符串数组 - -### 1、翻转字符串里的单词(151) - -[151. 翻转字符串里的单词](https://leetcode-cn.com/problems/reverse-words-in-a-string/) - -```java -//思路:关键是如何去除空字符串,比如 ""、" " 和 " " 等等。 -public String reverseWords(String s) { - if(s==null){ - return s; - } - String[] arr = s.trim().split(" "); - - Stack stack = new Stack<>(); - for(String ss : arr){ - String str=ss.trim(); - if(str.length()!=0){ //去除空字符串 - stack.push(str); - } - } - StringBuilder res = new StringBuilder(); - while (!stack.isEmpty()){ - res.append(stack.pop()).append(" "); - } - return res.toString().trim(); -} - -@Test -public void test(){ - //String s="the sky is blue"; - //String s=" hello world! "; - String s="a good example"; - System.out.println(reverseWords(s)); -} -``` - -### 2、反转字符串中的单词 III(557) - -[557. 反转字符串中的单词 III](https://leetcode-cn.com/problems/reverse-words-in-a-string-iii/) - -```java -public String reverseWords(String s) { - StringBuilder res = new StringBuilder(); - - String[] words =s.split(" "); - for(String word : words){ - word=reverseWord(word); - res.append(word).append(" "); - } - return res.toString().trim(); -} - -//翻转字符串 -private String reverseWord(String word){ - char[] chs = word.toCharArray(); - int start =0,end=chs.length-1; - while(start=0 || indexB>=0 || c==1){ - //TODO: c==1 判断条件是针对:a = "11", b = "1" 两数相加的情况的。 - c+=((indexA>=0)?chsA[indexA]-'0':0); - c+=((indexB>=0)?chsB[indexB]-'0':0); - res.append(c%2); - c/=2; //作为下一次的值 - indexA--; - indexB--; - } - return res.reverse().toString(); //注意是从后向前加的 -} - -@Test -public void test(){ - //String a = "11", b = "1"; - String a = "1010", b = "1011"; - System.out.println(addBinary(a,b)); -} -``` - -### 2、字符串中的第一个唯一字符(387) - -[387. 字符串中的第一个唯一字符](https://leetcode-cn.com/problems/first-unique-character-in-a-string/) - -```java -public int firstUniqChar(String s) { - char[] chs = s.toCharArray(); - - int[] freq = new int[26]; - for(char ch : chs){ - freq[ch-'a']++; - } - - for(int i=0;i 由内向外 "01"、"0011"和"000111"。 - int res=0; - for(int i=0;i 100){ - line++; - i--; //TODO: i 元素不能写到 line 行了 - width = 0; - }else if(width==100){ - line++; - width =0; - } - } - - return new int[]{line,width}; -} - -@Test -public void test(){ - int[] widths = {4,10,10,10,10,10,10,10,10,10,10,10, - 10,10,10,10,10,10,10,10,10,10,10,10,10,10}; - String S = "bbbcccdddaaa"; - //int [] widths = {10,10,10,10,10,10,10,10,10,10,10, - // 10,10,10,10,10,10,10,10,10,10,10,10,10,10,10}; - //String S = "abcdefghijklmnopqrstuvwxyz"; - - int[] res = numberOfLines(widths,S); - for(int num : res){ - System.out.println(num); - } -} -``` - -### 443 - -### 791 - -### - -### 809 - -### - -## 四、回文串 - -### 1、最长回文串(409) - -[409. 最长回文串](https://leetcode-cn.com/problems/longest-palindrome/) - -```java -//思路: -//构成回文串的 2 种情况: -//1、字符出现次数为双数的组合 -//2、字符出现次数为双数的组合 + 一个只出现一次的字符 -``` - -```java -//写法一:使用数组来统计 -public int longestPalindrome(String s) { - int[] cnts = new int[256]; - for (char c : s.toCharArray()) { - cnts[c]++; - } - - int res=0; - //统计出现双数的次数,比如 "aaa" -> 3/2=1 (出现 1 次双数) - for(int cnt : cnts){ - res+=(cnt/2)*2; // cnt/2 该字符能够有多少次双数 - } - if(res < s.length()){ - res++;// res++,就是加上一个值出现一次的字符 - } - return res; -} - -@Test -public void test(){ - String s="abccccdd"; - System.out.println(longestPalindrome(s)); -} -``` - -```java -//写法二:使用 set 进行统计 -public int longestPalindrome(String s) { - int count =0; //用于统计字符出现次数为 2 的字符个数 - - char[] chs = s.toCharArray(); - Set set=new HashSet<>(); //利用 set 存储的值不能相同的特性 - for(char ch:chs){ - if(!set.contains(ch)){ - set.add(ch); - }else{ //说明 ch 出现次数是偶数 - set.remove(ch); - count++; - } - } - return set.isEmpty()?(2*count):(2*count+1); -} - -@Test -public void test(){ - String s="abccccdd"; - System.out.println(longestPalindrome(s)); -} -``` - -### 2、125 - -### 3、5 - -### *4、516 - -### 5、647 - - - diff --git "a/docs/LeetCode/1_\346\225\260\347\273\204\351\227\256\351\242\230.md" "b/docs/LeetCode/\346\225\260\346\215\256\347\273\223\346\236\204\347\233\270\345\205\263/1_\346\225\260\347\273\204\351\227\256\351\242\230.md" similarity index 100% rename from "docs/LeetCode/1_\346\225\260\347\273\204\351\227\256\351\242\230.md" rename to "docs/LeetCode/\346\225\260\346\215\256\347\273\223\346\236\204\347\233\270\345\205\263/1_\346\225\260\347\273\204\351\227\256\351\242\230.md" diff --git "a/docs/LeetCode/3_\351\223\276\350\241\250\351\227\256\351\242\230.md" "b/docs/LeetCode/\346\225\260\346\215\256\347\273\223\346\236\204\347\233\270\345\205\263/2_\351\223\276\350\241\250\351\227\256\351\242\230.md" similarity index 100% rename from "docs/LeetCode/3_\351\223\276\350\241\250\351\227\256\351\242\230.md" rename to "docs/LeetCode/\346\225\260\346\215\256\347\273\223\346\236\204\347\233\270\345\205\263/2_\351\223\276\350\241\250\351\227\256\351\242\230.md" diff --git "a/docs/LeetCode/4_\346\240\210\345\222\214\351\230\237\345\210\227.md" "b/docs/LeetCode/\346\225\260\346\215\256\347\273\223\346\236\204\347\233\270\345\205\263/3_\346\240\210\345\222\214\351\230\237\345\210\227.md" similarity index 100% rename from "docs/LeetCode/4_\346\240\210\345\222\214\351\230\237\345\210\227.md" rename to "docs/LeetCode/\346\225\260\346\215\256\347\273\223\346\236\204\347\233\270\345\205\263/3_\346\240\210\345\222\214\351\230\237\345\210\227.md" diff --git "a/docs/LeetCode/5_\344\272\214\345\217\211\346\240\221.md" "b/docs/LeetCode/\346\225\260\346\215\256\347\273\223\346\236\204\347\233\270\345\205\263/4_\344\272\214\345\217\211\346\240\221.md" similarity index 99% rename from "docs/LeetCode/5_\344\272\214\345\217\211\346\240\221.md" rename to "docs/LeetCode/\346\225\260\346\215\256\347\273\223\346\236\204\347\233\270\345\205\263/4_\344\272\214\345\217\211\346\240\221.md" index c9174138..5f67f87a 100644 --- "a/docs/LeetCode/5_\344\272\214\345\217\211\346\240\221.md" +++ "b/docs/LeetCode/\346\225\260\346\215\256\347\273\223\346\236\204\347\233\270\345\205\263/4_\344\272\214\345\217\211\346\240\221.md" @@ -652,7 +652,9 @@ public void test(){ } ``` -## 二、完全二叉树 + + +## 三、完全二叉树 ### 1、判断一棵树是否是完全二叉树 diff --git "a/docs/LeetCode/\346\225\260\346\215\256\347\273\223\346\236\204\347\233\270\345\205\263/5_\345\255\227\347\254\246\344\270\262.md" "b/docs/LeetCode/\346\225\260\346\215\256\347\273\223\346\236\204\347\233\270\345\205\263/5_\345\255\227\347\254\246\344\270\262.md" new file mode 100644 index 00000000..0328dcd2 --- /dev/null +++ "b/docs/LeetCode/\346\225\260\346\215\256\347\273\223\346\236\204\347\233\270\345\205\263/5_\345\255\227\347\254\246\344\270\262.md" @@ -0,0 +1,744 @@ +# 字符串 + +## 一、KMP 算法 + +[28 Implement strStr()](https://leetcode.com/problems/implement-strstr/description/) + +### 朴素模式匹配算法 + +最朴素的方法就是依次从待匹配串的每一个位置开始,逐一与模版串匹配, +因为最多检查 (n - m)个位置,所以方法的复杂度为 O(m*(n-1))。 + +
+ +```java +//haystack 是待匹配串 +//needle 是模板串 +public int strStr(String haystack, String needle) { + if(needle==null || needle.length()==0){ + return 0; + } + int i=0,j=0; + //k存储的是模板串在待匹配串的位置 + int k=i; + while(i stack = new Stack<>(); + for(String ss : arr){ + String str=ss.trim(); + if(str.length()!=0){ //去除空字符串 + stack.push(str); + } + } + StringBuilder res = new StringBuilder(); + while (!stack.isEmpty()){ + res.append(stack.pop()).append(" "); + } + return res.toString().trim(); +} + +@Test +public void test(){ + //String s="the sky is blue"; + //String s=" hello world! "; + String s="a good example"; + System.out.println(reverseWords(s)); +} +``` + +### 2、反转字符串中的单词 III(557) + +[557. 反转字符串中的单词 III](https://leetcode-cn.com/problems/reverse-words-in-a-string-iii/) + +```java +public String reverseWords(String s) { + StringBuilder res = new StringBuilder(); + + String[] words =s.split(" "); + for(String word : words){ + word=reverseWord(word); + res.append(word).append(" "); + } + return res.toString().trim(); +} + +//翻转字符串 +private String reverseWord(String word){ + char[] chs = word.toCharArray(); + int start =0,end=chs.length-1; + while(start=0 || indexB>=0 || c==1){ + //TODO: c==1 判断条件是针对:a = "11", b = "1" 两数相加的情况的。 + c+=((indexA>=0)?chsA[indexA]-'0':0); + c+=((indexB>=0)?chsB[indexB]-'0':0); + res.append(c%2); + c/=2; //作为下一次的值 + indexA--; + indexB--; + } + return res.reverse().toString(); //注意是从后向前加的 +} + +@Test +public void test(){ + //String a = "11", b = "1"; + String a = "1010", b = "1011"; + System.out.println(addBinary(a,b)); +} +``` + +### 2、字符串中的第一个唯一字符(387) + +[387. 字符串中的第一个唯一字符](https://leetcode-cn.com/problems/first-unique-character-in-a-string/) + +```java +public int firstUniqChar(String s) { + char[] chs = s.toCharArray(); + + int[] freq = new int[26]; + for(char ch : chs){ + freq[ch-'a']++; + } + + for(int i=0;i 由内向外 "01"、"0011"和"000111"。 + int res=0; + for(int i=0;i 100){ + line++; + i--; //TODO: i 元素不能写到 line 行了 + width = 0; + }else if(width==100){ + line++; + width =0; + } + } + + return new int[]{line,width}; +} + +@Test +public void test(){ + int[] widths = {4,10,10,10,10,10,10,10,10,10,10,10, + 10,10,10,10,10,10,10,10,10,10,10,10,10,10}; + String S = "bbbcccdddaaa"; + //int [] widths = {10,10,10,10,10,10,10,10,10,10,10, + // 10,10,10,10,10,10,10,10,10,10,10,10,10,10,10}; + //String S = "abcdefghijklmnopqrstuvwxyz"; + + int[] res = numberOfLines(widths,S); + for(int num : res){ + System.out.println(num); + } +} +``` + +### 5、压缩字符串(443) + +[443. 压缩字符串](https://leetcode-cn.com/problems/string-compression/) + +```java +/** + * 思路: + * 指针i指向修改内容的位置,指针j遍历整个数组chars, + * 当下一个字符与当前字符不相同时,直接将该字符赋值到i处,然后i++,j++; + * 否则若相同,k指向j所在位置,j继续向前出发遍历所有与k处相同的字符,则相同的个数为j-k, + * 将j-k转化为字符串s, + * 将s的每一个字符都赋值在i所在位置开始的chars中~最后直到j>=n时退出循环,此时i的值即为in-place后新数组中的个数 + */ +public int compress(char[] chars) { + int i = 0, j = 0; + int n=chars.length; + while (j=1){ + visited[S.charAt(i)-'a']=true; + for(int j=0;j=1){ + visited[T.charAt(i)-'a']=true; + for(int j=0;j [, , , ] + * 根据规则,判断每个待测字符串是否满足标准。其实就是根据字母的次数决定字母在待测字符串的正确存在。具体如下: + * (1)出现次数小于3:根据题意可知,这组字母不属于扩展组,所以待测字符串中必须有等于该次数的该字母。 + * (2)出现次数大于等于3:属于扩展组,待测字符串中可以有小于等于该次数的该字母。 +*/ +public int expressiveWords(String S, String[] words) { + char[] sc=S.toCharArray(); + int res=0; + for(String word:words){ + char[] wc=word.toCharArray(); + if(check(sc,wc)){ + res++; + } + } + return res; +} + +private boolean check(char[] sc, char[] wc) { + if (sc.length < wc.length) { + return false; + } + int i = 0, j = 0; + while (i < sc.length && j < wc.length) { + if (sc[i] != wc[j]) { + return false; + } + int sStart = i, wStart = j; + while (i < sc.length && sc[i] == sc[sStart]) { + i++; + } + while (j < wc.length && wc[j] == wc[wStart]) { + j++; + } + //计算S、W中连续相等元素的长度 + int sLen = i - sStart; + int wLen = j - wStart; + //出现次数大于等于3:属于扩展组,待测字符串中可以有小于等于该次数的该字母。 + if (sLen >= 3 && sLen >= wLen) { + continue; + } + //出现次数小于3:根据题意可知,这组字母不属于扩展组,所以待测字符串中必须要等于该次数的该字母。 + if (sLen == wLen) { + continue; + } + return false; + } + return i == sc.length && j == wc.length; +} +``` + + + + + +## 四、回文串 + +### 1、最长回文串(409) + +[409. 最长回文串](https://leetcode-cn.com/problems/longest-palindrome/) + +```java +//思路: +//构成回文串的 2 种情况: +//1、字符出现次数为双数的组合 +//2、字符出现次数为双数的组合 + 一个只出现一次的字符 +``` + +```java +//写法一:使用数组来统计 +public int longestPalindrome(String s) { + int[] cnts = new int[256]; + for (char c : s.toCharArray()) { + cnts[c]++; + } + + int res=0; + //统计出现双数的次数,比如 "aaa" -> 3/2=1 (出现 1 次双数) + for(int cnt : cnts){ + res+=(cnt/2)*2; // cnt/2 该字符能够有多少次双数 + } + if(res < s.length()){ + res++;// res++,就是加上一个值出现一次的字符 + } + return res; +} + +@Test +public void test(){ + String s="abccccdd"; + System.out.println(longestPalindrome(s)); +} +``` + +```java +//写法二:使用 set 进行统计 +public int longestPalindrome(String s) { + int count =0; //用于统计字符出现次数为 2 的字符个数 + + char[] chs = s.toCharArray(); + Set set=new HashSet<>(); //利用 set 存储的值不能相同的特性 + for(char ch:chs){ + if(!set.contains(ch)){ + set.add(ch); + }else{ //说明 ch 出现次数是偶数 + set.remove(ch); + count++; + } + } + return set.isEmpty()?(2*count):(2*count+1); +} + +@Test +public void test(){ + String s="abccccdd"; + System.out.println(longestPalindrome(s)); +} +``` + +### 2、 验证回文串(125) + +[125. 验证回文串](https://leetcode-cn.com/problems/valid-palindrome/) + +```java +/** + * 思路: + * 这里只考虑字母(忽略大小写)和数字, + * 使用Character来进行判断。 + */ +public boolean isPalindrome(String s) { + if(s==null || s.length()==0){ + return true; + } + //将所有的字母都转换成小写 + s=s.toLowerCase(); + int start=0; + char[] chs=new char[s.length()]; + for(int i=0;i=0 && rmaxLen){ + maxLen=tmpLen; + startIndex=l; + endIndex=r; + } + l--; + r++; + } + } + + //类似于abba这种情况,以i,i+1为中心向两边扩展 + for(int i=0;i=0 && rmaxLen){ + maxLen=tmpLen; + startIndex=l; + endIndex=r; + } + l--; + r++; + } + } + return s.substring(startIndex,endIndex+1); +} +``` + + + +### *4、最长回文子序列(516) + +[516. 最长回文子序列](https://leetcode-cn.com/problems/longest-palindromic-subsequence/) + +```java +public int longestPalindromeSubseq(String s) { + if(s.length()==0){ + return 0; + } + int len=s.length(); + + //dp[i][j]表示字符串i-j下标所构成的子串中最长回文子串的长度 + int[][] dp=new int[len][len]; + + for(int i=len-1;i>=0;i--){ + dp[i][i]=1; + for(int j=i+1;j topKFrequent(int[] nums, int k) { + //统计数组中元素出现频率 + HashMap map = new HashMap<>(); + for(int num : nums){ + map.put(num, map.getOrDefault(num,0)+1); + } + + //每个桶存储出现频率相同的数,并且桶的下标代表桶中数出现的频率 + List[] buckets = new ArrayList[nums.length+1]; + for(Integer num : map.keySet()){ + Integer freq = map.get(num); + if(buckets[freq] == null){ + buckets[freq] = new ArrayList<>(); + } + buckets[freq].add(num); + } + + List res = new ArrayList<>(); + for(int i=buckets.length-1;i>=0 && res.size() topKFrequent(int[] nums, int k) { + List res = new ArrayList<>(); + + HashMap map = new HashMap<>(); + for(int num : nums){ + int freq = map.getOrDefault(num,0); + map.put(num,++freq); + } + + Set set = map.keySet(); + Element[] elements = new Element[set.size()]; + int i=0; + for(Integer key : set){ + elements[i] = new Element(key,map.get(key)); + i++; + } + + PriorityQueue pq = new PriorityQueue<>(); + for(Element element : elements){ + pq.add(element); + if(pq.size() > k){ + pq.poll(); + } + } + + while(!pq.isEmpty()){ + Element element = pq.poll(); + res.add(element.num); + } + return res; + } +} + +class Element implements Comparable{ + int num; + int freq; + Element(int num,int freq){ + this.num = num; + this.freq = freq; + } + + @Override + public int compareTo(Element o) { + return this.freq - o.freq; + } +} +``` + + + +## 2、根据字符出现频率排序(451) + +[451. 根据字符出现频率排序](https://leetcode-cn.com/problems/sort-characters-by-frequency/) + +问题描述:给定一个字符串,请将字符串里的字符按照出现的频率降序排列。 + +**示例 1:** + +``` +输入: +"tree" + +输出: +"eert" + +解释: +'e'出现两次,'r'和't'都只出现一次。 +因此'e'必须出现在'r'和't'之前。此外,"eetr"也是一个有效的答案。 +``` + +**示例 2:** + +``` +输入: +"cccaaa" + +输出: +"cccaaa" + +解释: +'c'和'a'都出现三次。此外,"aaaccc"也是有效的答案。 +注意"cacaca"是不正确的,因为相同的字母必须放在一起。 +``` + +**示例 3:** + +``` +输入: +"Aabb" + +输出: +"bbAa" + +解释: +此外,"bbaA"也是一个有效的答案,但"Aabb"是不正确的。 +注意'A'和'a'被认为是两种不同的字符。 +``` + +``` +class Solution { + public String frequencySort(String s) { + //统计 s 中字符串出现的次数 + HashMap map = new HashMap<>(); + for(int i=0;i[] buckets = new ArrayList[s.length()+1]; + for(Character c : map.keySet()){ + Integer freq = map.get(c); + if(buckets[freq]==null){ + buckets[freq] = new ArrayList<>(); + } + buckets[freq].add(c); + } + + StringBuilder res = new StringBuilder(); + for(int freq = buckets.length-1;freq>=0;freq--){ + if(buckets[freq] == null){ + continue; + } + List list = buckets[freq]; //list 中所有元素都出现了 freq 次数 + for(Character c : list){ + for(int i=0;i + +```java +//思路如上图: +//i指向值为0的元素,swap(i,zero+1) ,交换后,i位置元素是1(nums[zero+1..i+1]==1), +//不需要处理,i直接+1 +//i指向值为1的元素,不需要处理直接+1 +//i指向值为2的元素,swap(i,two-1),交换后,two-1位置元素就是2 +public void sortColors(int[] nums) { + int zero = -1; + int two = nums.length; + int i = 0; + while(i k-1 + assert p > (k-1); + hi--; + } + } + return nums[0]; +} + +private int partition(int[] nums,int l,int r){ + int pivot = nums[l]; + + while(l < r){ + //从右向左查找第一个 < pivot 的元素 + while(l < r && nums[r] >= pivot){ + r--; + } + nums[l] = nums[r]; + //从左向右查找第一个 > pivot 的元素 + while(l < r && nums[l] <= pivot){ + l++; + } + nums[r] = nums[l]; + } + nums[l] = pivot; + return l; +} +``` \ No newline at end of file diff --git "a/docs/LeetCode/\347\256\227\346\263\225\346\200\235\346\203\263\347\233\270\345\205\263/2_\345\210\206\346\262\273\346\200\235\346\203\263.md" "b/docs/LeetCode/\347\256\227\346\263\225\346\200\235\346\203\263\347\233\270\345\205\263/2_\345\210\206\346\262\273\346\200\235\346\203\263.md" new file mode 100644 index 00000000..91b2bfed --- /dev/null +++ "b/docs/LeetCode/\347\256\227\346\263\225\346\200\235\346\203\263\347\233\270\345\205\263/2_\345\210\206\346\262\273\346\200\235\346\203\263.md" @@ -0,0 +1,194 @@ +# 分治思想 + +## 1、为运算表达式设计优先级(241) + +[241. 为运算表达式设计优先级](https://leetcode-cn.com/problems/different-ways-to-add-parentheses/) + +给定一个含有数字和运算符的字符串,为表达式添加括号,改变其运算优先级以求出不同的结果。你需要给出所有可能的组合的结果。有效的运算符号包含 `+`, `-` 以及 `*` 。 + +**示例 1:** + +``` +输入: "2-1-1" +输出: [0, 2] +解释: +((2-1)-1) = 0 +(2-(1-1)) = 2 + +``` + +**示例 2:** + +``` +输入: "2*3-4*5" +输出: [-34, -14, -10, -10, 10] +解释: +(2*(3-(4*5))) = -34 +(2*((3-4)*5)) = -10 +((2*3)-(4*5)) = -14 +((2*(3-4))*5) = -10 +(((2*3)-4)*5) = 10 + +``` + +```java +public List diffWaysToCompute(String input) { + List res = new ArrayList<>(); + + for(int i=0;i left = diffWaysToCompute(input.substring(0,i)); + List right = diffWaysToCompute(input.substring(i+1)); + System.out.println(input); + System.out.println("left:"+left+",right:"+right); + System.out.println("=================="); + for(int l : left){ + for(int r : right){ + switch (c){ + case '+' : + res.add(l+r); + break; + case '-' : + res.add(l-r); + break; + case '*' : + res.add(l*r); + break; + } + } + } + } + } + + if(res.size() ==0){ //说明input 是一个数字 + res.add(Integer.valueOf(input)); + } + return res; +} + +``` + + + +## 2、最大子序和(53) + +[53. 最大子序和](https://leetcode-cn.com/problems/maximum-subarray/) + +给定一个整数数组 `nums` ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 + +**示例:** + +``` +输入: [-2,1,-3,4,-1,2,1,-5,4], +输出: 6 +解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。 + +``` + +**进阶:** + +如果你已经实现复杂度为 O(*n*) 的解法,尝试使用更为精妙的分治法求解。 + +```java +//思路:分治法 +//如果把数组分成左右两段,那么加和最大的连续子序列, +//要么出现在数组的左半部分, +//要么出现在数组的右半部分, +//要么出现在中间,即从左半部分和右半部分相邻的地方各取一段。 +public int maxSubArray(int[] nums) { + return maxSubArray(nums,0,nums.length-1); +} + +private int maxSubArray(int[] nums,int l,int r){ + if(l==r){ + return nums[l]; + } + int mid = l + (r-l)/2; + int leftSubMax = maxSubArray(nums,l,mid); + int rightSubMax = maxSubArray(nums,mid+1,r); + + int subMax = nums[mid]; + int sum = subMax; //中间元素必然在该最大连续子序列中 + + //从中间向左半数组遍历 + for(int i=mid-1;i>=l;i--){ + sum += nums[i]; + subMax = Math.max(subMax,sum); + } + sum = subMax; + //从中间向右半数组遍历 + for(int i=mid+1;i<=r;i++){ + sum += nums[i]; + subMax = Math.max(subMax,sum); + } + return max3(leftSubMax,subMax,rightSubMax); +} + +private int max3(int a,int b,int c){ + int tmp = (a>b) ? a:b; + return (tmp>c? tmp : c); +} + +``` + + + +## 3、不同的二叉搜索树 II(95) + +[95. 不同的二叉搜索树 II](https://leetcode-cn.com/problems/unique-binary-search-trees-ii/) + +给定一个整数 *n*,生成所有由 1 ... *n* 为节点所组成的**二叉搜索树**。 + +**示例:** + +``` +输入: 3 +输出: +[ + [1,null,3,2], + [3,2,null,1], + [3,1,null,null,2], + [2,1,3], + [1,null,2,null,3] +] +解释: +以上的输出对应以下 5 种不同结构的二叉搜索树: + + 1 3 3 2 1 + \ / / / \ \ + 3 2 1 1 3 2 + / / \ \ + 2 1 2 3 + +``` + +```java +public List generateTrees(int n) { + if(n==0){ + return new LinkedList<>(); + } + return generateSubTrees(1,n); +} + +public List generateSubTrees(int s,int e){ + List res = new LinkedList<>(); + if(s>e){ + res.add(null); + return res; + } + for(int i=s;i<=e;i++){ + List left = generateSubTrees(s,i-1); + List right = generateSubTrees(i+1,e); + for(TreeNode l : left){ + for(TreeNode r : right){ + TreeNode root = new TreeNode(i); + root.left = l; + root.right = r; + res.add(root); + } + } + } + return res; +} +``` \ No newline at end of file diff --git "a/docs/LeetCode/\347\256\227\346\263\225\346\200\235\346\203\263\347\233\270\345\205\263/3_\350\264\252\345\277\203\346\200\235\346\203\263.md" "b/docs/LeetCode/\347\256\227\346\263\225\346\200\235\346\203\263\347\233\270\345\205\263/3_\350\264\252\345\277\203\346\200\235\346\203\263.md" new file mode 100644 index 00000000..2847a386 --- /dev/null +++ "b/docs/LeetCode/\347\256\227\346\263\225\346\200\235\346\203\263\347\233\270\345\205\263/3_\350\264\252\345\277\203\346\200\235\346\203\263.md" @@ -0,0 +1,401 @@ +# 贪心思想 + +保证每次操作都是局部最优的,并且最后得到的结果是全局最优的。 + +## *1、分配饼干(455) + +[455. Assign Cookies (Easy)](https://leetcode.com/problems/assign-cookies/description/) + +```html +Input: [1,2], [1,2,3] +Output: 2 + +Explanation: You have 2 children and 3 cookies. The greed factors of 2 children are 1, 2. +You have 3 cookies and their sizes are big enough to gratify all of the children, +You need to output 2. + +``` + +题目描述:每个孩子都有一个满足度,每个饼干都有一个大小,只有饼干的大小大于等于一个孩子的满足度,该孩子才会获得满足。求解最多可以获得满足的孩子数量。 + +给一个孩子的饼干应当尽量小又能满足该孩子,这样大饼干就能拿来给满足度比较大的孩子。因为最小的孩子最容易得到满足,所以先满足最小的孩子。 + +证明:假设在某次选择中,贪心策略选择给当前满足度最小的孩子分配第 m 个饼干,第 m 个饼干为可以满足该孩子的最小饼干。假设存在一种最优策略,给该孩子分配第 n 个饼干,并且 m < n。我们可以发现,经过这一轮分配,贪心策略分配后剩下的饼干一定有一个比最优策略来得大。因此在后续的分配中,贪心策略一定能满足更多的孩子。也就是说不存在比贪心策略更优的策略,即贪心策略就是最优策略。 + +```java +//思路:贪心策略 +//每次选取最大的饼干给最"贪心"的小朋友。 +public int findContentChildren(int[] g, int[] s) { + Arrays.sort(g); + Arrays.sort(s); + + int gi = g.length-1; + int si = s.length-1; + int res=0; + while(gi>=0 && si>=0){ + if(s[si]>=g[gi]){ + res++; + gi--; + si--; + }else{ //不能满足这个小朋友,抱歉,饼干就不分给他了。 + gi--; + } + } + return res; +} +``` + +## *2、不重叠的区间个数(453) + +[435. Non-overlapping Intervals (Medium)](https://leetcode.com/problems/non-overlapping-intervals/description/) + +```html +Input: [ [1,2], [1,2], [1,2] ] + +Output: 2 + +Explanation: You need to remove two [1,2] to make the rest of intervals non-overlapping. + +``` + +```html +Input: [ [1,2], [2,3] ] + +Output: 0 + +Explanation: You don't need to remove any of the intervals since they're already non-overlapping. + +``` + +题目描述:计算让一组区间不重叠所需要移除的区间个数。 + +先计算最多能组成的不重叠区间个数,然后用区间总个数减去不重叠区间的个数。 + +在每次选择中,区间的结尾最为重要,选择的区间结尾越小,留给后面的区间的空间越大,那么后面能够选择的区间个数也就越大。 + +按区间的结尾进行排序,每次选择结尾最小,并且和前一个区间不重叠的区间。 + +```java +public int eraseOverlapIntervals(Interval[] intervals) { + if (intervals.length == 0) { + return 0; + } + Arrays.sort(intervals, Comparator.comparingInt(o -> o.end)); + int cnt = 1; + int end = intervals[0].end; + for (int i = 1; i < intervals.length; i++) { + if (intervals[i].start < end) { + continue; + } + end = intervals[i].end; + cnt++; + } + return intervals.length - cnt; +} + +``` + +使用 lambda 表示式创建 Comparator 会导致算法运行时间过长,如果注重运行时间,可以修改为普通创建 Comparator 语句: + +```java +Arrays.sort(intervals, new Comparator() { + @Override + public int compare(Interval o1, Interval o2) { + return o1.end - o2.end; + } +}); + +``` + +## 3、投飞镖刺破气球(452) + +[452. Minimum Number of Arrows to Burst Balloons (Medium)](https://leetcode.com/problems/minimum-number-of-arrows-to-burst-balloons/description/) + +``` +Input: +[[10,16], [2,8], [1,6], [7,12]] + +Output: +2 + +``` + +题目描述:气球在一个水平数轴上摆放,可以重叠,飞镖垂直投向坐标轴,使得路径上的气球都会刺破。求解最小的投飞镖次数使所有气球都被刺破。 + +也是计算不重叠的区间个数,不过和 Non-overlapping Intervals 的区别在于,[1, 2] 和 [2, 3] 在本题中算是重叠区间。 + +```java +public int findMinArrowShots(int[][] points) { + if (points.length == 0) { + return 0; + } + Arrays.sort(points, Comparator.comparingInt(o -> o[1])); + int cnt = 1, end = points[0][1]; + for (int i = 1; i < points.length; i++) { + if (points[i][0] <= end) { + continue; + } + cnt++; + end = points[i][1]; + } + return cnt; +} + +``` + +## 4、根据身高和序号重组队列(406) + +[406. Queue Reconstruction by Height(Medium)](https://leetcode.com/problems/queue-reconstruction-by-height/description/) + +```html +Input: +[[7,0], [4,4], [7,1], [5,0], [6,1], [5,2]] + +Output: +[[5,0], [7,0], [5,2], [6,1], [4,4], [7,1]] + + +``` + +题目描述:一个学生用两个分量 (h, k) 描述,h 表示身高,k 表示排在前面的有 k 个学生的身高比他高或者和他一样高。 + +为了使插入操作不影响后续的操作,身高较高的学生应该先做插入操作,否则身高较小的学生原先正确插入的第 k 个位置可能会变成第 k+1 个位置。 + +身高降序、k 值升序,然后按排好序的顺序插入队列的第 k 个位置中。 + +```java +public int[][] reconstructQueue(int[][] people) { + if (people == null || people.length == 0 || people[0].length == 0) { + return new int[0][0]; + } + Arrays.sort(people, (a, b) -> (a[0] == b[0] ? a[1] - b[1] : b[0] - a[0])); + List queue = new ArrayList<>(); + for (int[] p : people) { + queue.add(p[1], p); + } + return queue.toArray(new int[queue.size()][]); +} + + +``` + +## 5、分隔字符串使同种字符出现在一起(763) + +[763. Partition Labels (Medium)](https://leetcode.com/problems/partition-labels/description/) + +```html +Input: S = "ababcbacadefegdehijhklij" +Output: [9,7,8] +Explanation: +The partition is "ababcbaca", "defegde", "hijhklij". +This is a partition so that each letter appears in at most one part. +A partition like "ababcbacadefegde", "hijhklij" is incorrect, because it splits S into less parts. + + +``` + +```java +public List partitionLabels(String S) { + int[] lastIndexsOfChar = new int[26]; + for (int i = 0; i < S.length(); i++) { + lastIndexsOfChar[char2Index(S.charAt(i))] = i; + } + List partitions = new ArrayList<>(); + int firstIndex = 0; + while (firstIndex < S.length()) { + int lastIndex = firstIndex; + for (int i = firstIndex; i < S.length() && i <= lastIndex; i++) { + int index = lastIndexsOfChar[char2Index(S.charAt(i))]; + if (index > lastIndex) { + lastIndex = index; + } + } + partitions.add(lastIndex - firstIndex + 1); + firstIndex = lastIndex + 1; + } + return partitions; +} + +private int char2Index(char c) { + return c - 'a'; +} + + +``` + +## 6、种植花朵(605) + +[605. Can Place Flowers (Easy)](https://leetcode.com/problems/can-place-flowers/description/) + +```html +Input: flowerbed = [1,0,0,0,1], n = 1 +Output: True + + +``` + +题目描述:花朵之间至少需要一个单位的间隔,求解是否能种下 n 朵花。 + +```java +public boolean canPlaceFlowers(int[] flowerbed, int n) { + int len = flowerbed.length; + int cnt = 0; + for (int i = 0; i < len && cnt < n; i++) { + if (flowerbed[i] == 1) { + continue; + } + int pre = i == 0 ? 0 : flowerbed[i - 1]; + int next = i == len - 1 ? 0 : flowerbed[i + 1]; + if (pre == 0 && next == 0) { + cnt++; + flowerbed[i] = 1; + } + } + return cnt >= n; +} + + +``` + +## 7、判断是否为子序列(392) + +[392. Is Subsequence (Medium)](https://leetcode.com/problems/is-subsequence/description/) + +```html +s = "abc", t = "ahbgdc" +Return true. + + +``` + +```java +public boolean isSubsequence(String s, String t) { + int index = -1; + for (char c : s.toCharArray()) { + index = t.indexOf(c, index + 1); + if (index == -1) { + return false; + } + } + return true; +} + + +``` + +## 8、修改一个数成为非递减数组(665) + +[665. Non-decreasing Array (Easy)](https://leetcode.com/problems/non-decreasing-array/description/) + +```html +Input: [4,2,3] +Output: True +Explanation: You could modify the first 4 to 1 to get a non-decreasing array. + + +``` + +题目描述:判断一个数组能不能只修改一个数就成为非递减数组。 + +在出现 nums[i] < nums[i - 1] 时,需要考虑的是应该修改数组的哪个数,使得本次修改能使 i 之前的数组成为非递减数组,并且 **不影响后续的操作** 。优先考虑令 nums[i - 1] = nums[i],因为如果修改 nums[i] = nums[i - 1] 的话,那么 nums[i] 这个数会变大,就有可能比 nums[i + 1] 大,从而影响了后续操作。还有一个比较特别的情况就是 nums[i] < nums[i - 2],只修改 nums[i - 1] = nums[i] 不能使数组成为非递减数组,只能修改 nums[i] = nums[i - 1]。 + +```java +public boolean checkPossibility(int[] nums) { + int cnt = 0; + for (int i = 1; i < nums.length && cnt < 2; i++) { + if (nums[i] >= nums[i - 1]) { + continue; + } + cnt++; + if (i - 2 >= 0 && nums[i - 2] > nums[i]) { + nums[i] = nums[i - 1]; + } else { + nums[i - 1] = nums[i]; + } + } + return cnt <= 1; +} + + +``` + +## 9、股票的最大收益(122) + +[122. Best Time to Buy and Sell Stock II (Easy)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-ii/description/) + +题目描述:一次股票交易包含买入和卖出,多个交易之间不能交叉进行。 + +对于 [a, b, c, d],如果有 a <= b <= c <= d ,那么最大收益为 d - a。而 d - a = (d - c) + (c - b) + (b - a) ,因此当访问到一个 prices[i] 且 prices[i] - prices[i-1] > 0,那么就把 prices[i] - prices[i-1] 添加到收益中,从而在局部最优的情况下也保证全局最优。 + +```java +public int maxProfit(int[] prices) { + int profit = 0; + for (int i = 1; i < prices.length; i++) { + if (prices[i] > prices[i - 1]) { + profit += (prices[i] - prices[i - 1]); + } + } + return profit; +} + + +``` + +## 10、子数组最大的和(53) + +[53. Maximum Subarray (Easy)](https://leetcode.com/problems/maximum-subarray/description/) + +```html +For example, given the array [-2,1,-3,4,-1,2,1,-5,4], +the contiguous subarray [4,-1,2,1] has the largest sum = 6. + + +``` + +```java +public int maxSubArray(int[] nums) { + if (nums == null || nums.length == 0) { + return 0; + } + int preSum = nums[0]; + int maxSum = preSum; + for (int i = 1; i < nums.length; i++) { + preSum = preSum > 0 ? preSum + nums[i] : nums[i]; + maxSum = Math.max(maxSum, preSum); + } + return maxSum; +} + + +``` + +## 11、买入和售出股票最大的收益(121) + +[121. Best Time to Buy and Sell Stock (Easy)](https://leetcode.com/problems/best-time-to-buy-and-sell-stock/description/) + +题目描述:只进行一次交易。 + +只要记录前面的最小价格,将这个最小价格作为买入价格,然后将当前的价格作为售出价格,查看当前收益是不是最大收益。 + +```java +public int maxProfit(int[] prices) { + int n = prices.length; + + if (n == 0) return 0; + + int soFarMin = prices[0]; + + int max = 0; + + for (int i = 1; i < n; i++) { + + if (soFarMin > prices[i]) soFarMin = prices[i]; + + else max = Math.max(max, prices[i] - soFarMin); + + } + + return max; + +} +``` \ No newline at end of file diff --git "a/docs/LeetCode/\347\256\227\346\263\225\346\200\235\346\203\263\347\233\270\345\205\263/4_LRU.md" "b/docs/LeetCode/\347\256\227\346\263\225\346\200\235\346\203\263\347\233\270\345\205\263/4_LRU.md" new file mode 100644 index 00000000..8ad32470 --- /dev/null +++ "b/docs/LeetCode/\347\256\227\346\263\225\346\200\235\346\203\263\347\233\270\345\205\263/4_LRU.md" @@ -0,0 +1,70 @@ +# LRU + +## LRU 缓存机制(146) + +[146. LRU缓存机制](https://leetcode-cn.com/problems/lru-cache/) + +```java +class LRUCache { + private List list; //使用 LinkedList 来代替双向链表,维护数据顺序 + private Map map; //使用 map 保存数据,实现 O(1) 的访问 + private int size;//链表长度 + private int capacity;//缓存总量,方便后面判断是否链表长度是否超过缓存容量 + + public LRUCache(int capacity) { + list = new LinkedList<>(); + map = new HashMap<>(); + size=0; + this.capacity = capacity; + } + + //判断 key 在 HashMap 中是否存在, + //如果存在,则把对应的节点移动到链表头部,并返回对应的 value 值; + //如果不存在,则返回-1。 + public int get(int key) { + if(!map.containsKey(key)){ + return -1; + } + //将对应节点移动到链表头部 + list.remove((Integer)key); + list.add(0,key); + return map.get(key); + } + + //如果 key 在 HashMap 中存在,则先重置对应的 value 值,将节点移动到链表的头部; + //如果 key 在 HashMap 中不存在,则新建一个节点,并将节点放到链表的头部。 + //注意:当 Cache 存满的时候,将链表最后一个节点删除。 + public void put(int key, int value) { + if(map.containsKey(key)){ + map.put(key,value); + //将节点移到链表的头部 + list.remove((Integer)key); + list.add(0,key); + }else{ + list.add(0,key); + map.put(key,value); + size++; + if(size>capacity){ //每次插入新元素,都会检查是否超过 capacity,只会多出一个元素 + //如果超过容量,则删除链表最后一个元素,同时要去相应的 map 中删除元素 + int lastElement = list.remove(list.size()-1); + map.remove(lastElement); + } + } + } +} + +@Test +public void test(){ + LRUCache cache = new LRUCache( 2); + + cache.put(1, 1); + cache.put(2, 2); + System.out.println(cache.get(1)); // 返回 1 + cache.put(3, 3); // 该操作会使得密钥 2 作废 + System.out.println(cache.get(2)); // 返回 -1 (未找到) + cache.put(4, 4); // 该操作会使得密钥 1 作废 + System.out.println(cache.get(1)); // 返回 -1 (未找到) + System.out.println(cache.get(3)); // 返回 3 + System.out.println(cache.get(4)); // 返回 4 +} +``` \ No newline at end of file diff --git "a/docs/LeetCode/\347\256\227\346\263\225\346\200\235\346\203\263\347\233\270\345\205\263/5_DFS.md" "b/docs/LeetCode/\347\256\227\346\263\225\346\200\235\346\203\263\347\233\270\345\205\263/5_DFS.md" new file mode 100644 index 00000000..33d16790 --- /dev/null +++ "b/docs/LeetCode/\347\256\227\346\263\225\346\200\235\346\203\263\347\233\270\345\205\263/5_DFS.md" @@ -0,0 +1,484 @@ +# DFS + +## 1、岛屿的个数(200) + +[200. 岛屿的个数](https://leetcode-cn.com/problems/number-of-islands/) + +给定一个由 `'1'`(陆地)和 `'0'`(水)组成的的二维网格,计算岛屿的数量。一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设网格的四个边均被水包围。 + +**示例 1:** + +``` +输入: +11110 +11010 +11000 +00000 + +输出: 1 + + +``` + +**示例 2:** + +``` +输入: +11000 +11000 +00100 +00011 + +输出: 3 + + +``` + +```java +private int m; +private int n; + +//用来标记是否是同一个岛屿 +private boolean[][] visited; + +//TODO:二维数组四个方向查找的小技巧 +private int[][] d={{-1,0},{0,1},{1,0},{0,-1}}; + +private boolean isArea(int x,int y){ + return (x>=0 && x=0 && y=0 && x=0 && y=0 && x=0 && y=0 && x=0 && y=matrix[x][y])){ + dfs(matrix,visited,newx,newy); + } + } + } +} + +public List pacificAtlantic(int[][] matrix) { + List res=new ArrayList<>(); + m=matrix.length; + if(m==0){ + return res; + } + n=matrix[0].length; + pacific=new boolean[m][n]; + atlantic=new boolean[m][n]; + + for(int i=0;i> res; + +private boolean[] visited; //标记元素是否已经被访问 + +public List> permute(int[] nums) { + res = new ArrayList<>(); + if(nums==null || nums.length==0){ + return res; + } + visited = new boolean[nums.length]; + List p = new ArrayList<>(); + generatePermutation(nums,0,p); + return res; +} + +//产生排列 +//p中保存一个存在index个元素的排列 +//向这个排列的末尾添加第(index+1)个元素,获得包含(index+1)个元素的排列 +private void generatePermutation(int[] nums,int index,List p){ + if(index == nums.length){ + res.add(new ArrayList<>(p)); + return; + } + + for(int i=0;i> res=permute(nums); + for(List list : res){ + System.out.println(list); + } +} +``` + +### 2、全排列 II(47) + +[47. 全排列 II](https://leetcode-cn.com/problems/permutations-ii/) + +```java +//思路: +//1、先对数组进行排序,这样如果存在重复元素,则这些元素是相邻的 +//2、对于重复元素,最后一个元素加入排列 +private List> res; + +private boolean[] visited; + +public List> permuteUnique(int[] nums) { + res = new ArrayList<>(); + if(nums==null || nums.length==0){ + return res; + } + Arrays.sort(nums); + + visited = new boolean[nums.length]; + + List p = new ArrayList<>(); + generatePermutation(nums,0,p); + + return res; +} + +private void generatePermutation(int[] nums,int index,List p){ + if(index==nums.length){ + res.add(new ArrayList<>(p)); + return; + } + for(int i=0;i0 && (nums[i-1]==nums[i] && !visited[i-1])){ + // 为甚要加上这个 !visited[i-1] + //实际上是指 nums[i-1] 和 nums[i] 相邻元素,并且都没有被访问,则后面的元素加入排列 + continue; + } + p.add(nums[i]); + visited[i]=true; + generatePermutation(nums,index+1,p); + visited[i]=false; + p.remove(p.size()-1); + } + } + return; +} + +@Test +public void test(){ + int[] nums = {1,1,2}; + List> res=permuteUnique(nums); + for(List list : res){ + System.out.println(list); + } +} +``` + +### *3、字母大小写全排列(784) + +[784. 字母大小写全排列](https://leetcode-cn.com/problems/letter-case-permutation/) + +```java +private List res; + +public List letterCasePermutation(String S) { + res = new ArrayList<>(); + if(S.length()==0){ + res.add(S); + return res; + } + + StringBuilder builder = new StringBuilder(S); + replaceCh(0,builder); + return res; +} + +//替换 index 位置元素 +private void replaceCh(int index, StringBuilder builder){ + if(index==builder.length()){ + res.add(builder.toString()); + return; + } + + char ch = builder.charAt(index); + if(Character.isLetter(ch)){ + builder.setCharAt(index,Character.toLowerCase(ch)); + replaceCh(index+1,builder); + builder.setCharAt(index,Character.toUpperCase(ch)); + replaceCh(index+1,builder); + }else{ + replaceCh(index+1,builder); + } + return; +} + +@Test +public void test(){ + //String S = "a1b2"; + String S = "3z4"; + System.out.println(letterCasePermutation(S)); +} +``` + +## 二、组合问题 + +### *1、组合(77) + +[77. 组合](https://leetcode-cn.com/problems/combinations/) + +```java +private List> res; + +public List> combine(int n, int k) { + res = new ArrayList<>(); + if(n<=0 || k<=0 || n c= new ArrayList<>(); + findCombination(n,k,0,c); + + return res; +} + +//c存储已经找到的组合 +//从start开始搜索新的元素 +private void findCombination(int n,int k,int start,List c){ + if(k==c.size()){ //已经找到 k 个元素 + res.add(new ArrayList<>(c)); + return; + } + for(int i=start;i<=n;i++){ + c.add(i); + findCombination(n,k,i+1,c); + c.remove(c.size()-1); + } + return; +} + +@Test +public void test(){ + int n=4; + int k=2; + List> res=combine(n,k); + for(List list : res){ + System.out.println(list); + } +} +``` + + + +### 2、组合总和(39) + +[39. 组合总和](https://leetcode-cn.com/problems/combination-sum/) + +```java +private List> res; + +public List> combinationSum(int[] candidates, int target) { + res=new ArrayList<>(); + if(candidates.length==0){ + return res; + } + List p=new ArrayList<>(); + solve(candidates,0,target,p); + return res; +} + +private void solve(int[] candidates,int startIndex,int target,List p){ + if(target==0){ + res.add(new ArrayList<>(p)); + } + for(int i=startIndex;i=candidates[i]){ + p.add(candidates[i]); + solve(candidates,i,target-candidates[i],p); + p.remove(p.size()-1); + } + } +} +``` + + + +### 3、组合总和 II(40) + +[40. 组合总和 II](https://leetcode-cn.com/problems/combination-sum-ii/) + +```java +private List> res; + +public List> combinationSum2(int[] candidates, int target) { + res=new ArrayList<>(); + if(candidates.length==0){ + return res; + } + Arrays.sort(candidates); + List p=new ArrayList<>(); + solve(candidates,0,target,p); + return res; +} + +private void solve(int[] candidates,int startIndex,int target,List p){ + if(target==0){ + res.add(new ArrayList<>(p)); + return; + } + for(int i=startIndex;i startIndex && candidates[i] == candidates[i-1]) + continue; + if(target>=candidates[i]){ + p.add(candidates[i]); + solve(candidates,i+1,target-candidates[i],p); + p.remove(p.size()-1); + } + } + return; +} +``` + + + +### 4、组合总和 III(216) + +[216. 组合总和 III](https://leetcode-cn.com/problems/combination-sum-iii/) + +```java +private List> res; + +public List> combinationSum3(int k, int n) { + res=new ArrayList<>(); + if(k>n || k==0){ + return res; + } + List p=new ArrayList<>(); + //数值是从1开始的 + findCombination(k,n,1,p); + return res; +} + +private void findCombination(int k,int n,int num,List p){ + //注意,这里的递归结束条件 + if(k==0 && n==0){ + res.add(new ArrayList<>(p)); + return; + } + for(int i=num;i<=9;i++){ + //要求n>=i,这样保证(n-i)>=0,保证递归能终止 + if(n>=i){ + p.add(i); + findCombination(k-1,n-i,i+1,p); + p.remove(p.size()-1); + } + } +} +``` + + + +### 5、子集(78) + +[78. 子集](https://leetcode-cn.com/problems/subsets/) + +```java +private List> res; + +private void findSubset(int[] nums,int index,List p){ + //这里不需要递归的结束条件 + //因为当index==nums.length之后,就会自动终止递归 + //为什么自己不递归?这样会导致解缺失的情况 + //if(index==nums.length){ + // res.add(new ArrayList<>(p)); + //return; + //} + //结果:[[1, 2, 3], [1, 3], [2, 3], [3]] + //因为每次递归对数组长度是没有要求的,加上条件后,每次只能获取定长的数据 + res.add(new ArrayList<>(p)); + for(int i=index;i> subsets(int[] nums) { + res=new ArrayList<>(); + if(nums.length==0){ + return res; + } + List p=new ArrayList<>(); + findSubset(nums,0,p); + return res; +} +``` + + + +### 6、子集II(90) + +[90. 子集 II](https://leetcode-cn.com/problems/subsets-ii/) + +```java +private List> res; + +private void findSubsets(int[] nums,int index,List p){ + res.add(new ArrayList<>(p)); + for(int i=index;i0 && nums[i-1]==nums[i]) 相邻的重复元素 + //i==index 表示在同一层 + if(i!=index && (i>0 && nums[i-1]==nums[i])){ + continue; + } + p.add(nums[i]); + findSubsets(nums,i+1,p); + p.remove(p.size()-1); + } +} + +public List> subsetsWithDup(int[] nums) { + res=new ArrayList<>(); + if(nums.length==0){ + return res; + } + Arrays.sort(nums); + List p=new ArrayList<>(); + findSubsets(nums,0,p); + return res; +} +``` + + + +### 7、二进制手表(401) + +[401. 二进制手表](https://leetcode-cn.com/problems/binary-watch/) + +```java +public List readBinaryWatch(int num) { + List res=new ArrayList<>(); + if(num<0){ + return res; + } + for(int h=0;h<12;h++){ + for(int m=0;m<60;m++){ + //统计h在二进制下“1”的数量 + //h=5 --> 转化为二进制(101)-->结果为2 + if(Integer.bitCount(h)+Integer.bitCount(m)==num){ + res.add(String.format("%d:%02d",h,m)); + } + } + } + return res; +} +``` + +```java +//表示小时的灯 +int[] hour={1,2,4,8}; +//表示分钟的灯 +int[] minute={1,2,4,8,16,32}; + +private List res; + +//num表示还需要点亮的LED数 +//index表示所点的表示小时的LED的下标 +private void solve(int num,int index,int[] watch){ + if(num==0){ + int h=0; + int m=0; + for(int i=0;i11 和 min>59的情况舍去 + if(!((watch[2]==1 && watch[3]==1)||(watch[6]==1 && watch[7]==1 && watch[8]==1 && watch[9]==1))){ + solve(num-1,index+1,watch); + } + watch[index]=0; + index++; + } +} + +public List readBinaryWatch(int num) { + res=new ArrayList<>(); + //表示手表的10个LED灯,0:表示灯为开启,1表示灯开了 + int[] watch=new int[10]; + solve(num,0,watch); + return res; +} +``` + diff --git "a/docs/LeetCode/7_\345\212\250\346\200\201\350\247\204\345\210\222.md" "b/docs/LeetCode/\347\256\227\346\263\225\346\200\235\346\203\263\347\233\270\345\205\263/7_\345\212\250\346\200\201\350\247\204\345\210\222.md" similarity index 76% rename from "docs/LeetCode/7_\345\212\250\346\200\201\350\247\204\345\210\222.md" rename to "docs/LeetCode/\347\256\227\346\263\225\346\200\235\346\203\263\347\233\270\345\205\263/7_\345\212\250\346\200\201\350\247\204\345\210\222.md" index 57ed4969..56d19e5c 100644 --- "a/docs/LeetCode/7_\345\212\250\346\200\201\350\247\204\345\210\222.md" +++ "b/docs/LeetCode/\347\256\227\346\263\225\346\200\235\346\203\263\347\233\270\345\205\263/7_\345\212\250\346\200\201\350\247\204\345\210\222.md" @@ -1147,32 +1147,446 @@ public int findMaxForm(String[] strs, int m, int n) { -### 139 +### 9、单词拆分(139) + +[139. 单词拆分](https://leetcode-cn.com/problems/word-break/) + +```java +/** + * 思路: + * dp[i]表示字符串s[0...i)能否拆分成符合要求的子字符串。 + * 我们可以看出,如果s[j...i)在给定的字符串组中,且dp[j]为True(即字符串s[0...j)能够拆分成符合要求的子字符串),那么此时dp[i]也就为True了。 + * 状态转移方程: + * dp[0]=true; + * dp[i]=dp[j] && DICT(s[j...i)),0<=j wordDict) { + int n=s.length(); + boolean[] memo=new boolean[n+1]; + memo[0]=true; + for (int i = 1; i <= n; i++){ + for (int j = 0; j < i; j++) { + // 注意substring是前闭后开 + String tmp = s.substring(j, i); + if (memo[j] && wordDict.contains(tmp)) { + memo[i] = true; + break; + } + } + } + return memo[n]; +} +``` + + + +### 10、打家劫舍 III(337) + +[337. 打家劫舍 III](https://leetcode-cn.com/problems/house-robber-iii/) + +```java +public int rob(TreeNode root) { + return rob(root,true); +} + +private int rob(TreeNode root,boolean include){ + if(root==null){ + return 0; + } + int res=rob(root.left,true)+rob(root.right,true); + if(include){ + res=Math.max(res,root.val+rob(root.left,false)+rob(root.right,false)); + } + return res; +} +``` + +```java +public int rob(TreeNode root) { + int[] res=tryRob(root); + return Math.max(res[0],res[1]); +} + +private int[] tryRob(TreeNode root){ + if(root==null){ + return new int[]{0,0}; + } + int[] resultL=tryRob(root.left); + int[] resultR=tryRob(root.right); + int[] res=new int[2]; + //小标0 表示不抢劫该根节点的最大价值,小标1表示抢劫该根节点的最大价值 + + //不抢劫该根节点 + res[0]=resultL[1]+resultR[1]; + res[1]=Math.max(res[0],root.val+resultL[0]+resultR[0]); + return res; +} +``` -### 377 -### ## 四、数组区间 -### 303 +### 1、区域和检索 - 数组不可变(303) + +[303. 区域和检索 - 数组不可变](https://leetcode-cn.com/problems/range-sum-query-immutable/) + +```java +class NumArray { + + private int[] sums; + + public NumArray(int[] nums) { + sums = new int[nums.length + 1]; + for (int i = 1; i <= nums.length; i++) { + sums[i] = sums[i - 1] + nums[i - 1]; + } + } + + public int sumRange(int i, int j) { + return sums[j + 1] - sums[i]; + } +} +``` + +### 2、等差数列划分(413) + +[413. 等差数列划分](https://leetcode-cn.com/problems/arithmetic-slices/) + +```java +public int numberOfArithmeticSlices(int[] A) { + if (A == null || A.length == 0) { + return 0; + } + int n = A.length; + int[] dp = new int[n]; + for (int i = 2; i < n; i++) { + if (A[i] - A[i - 1] == A[i - 1] - A[i - 2]) { + dp[i] = dp[i - 1] + 1; + } + } + int total = 0; + for (int cnt : dp) { + total += cnt; + } + return total; +} +``` + -### 413 ## 四、LIS 问题 -### 300 +### 1、最长上升子序列(300) + +[300. 最长上升子序列](https://leetcode-cn.com/problems/longest-increasing-subsequence/) + +```html +/** +* 思路: +* LIS(i):表示以第i个数字为结尾的最长上升子序列的长度, +* 即在[0...i]范围内,选择数字nums[i]可以获得的最长子序列的长度。 +* LIS(i)=max(jnums[j])} +*/ +public int lengthOfLIS(int[] nums) { + int n=nums.length; + if(n==0){ + return 0; + } + int[] memo=new int[n]; + for(int i=0;inums[j]){ + memo[i]=Math.max(memo[i],1+memo[j]); + } + } + } + + int res=1; + for(int i=0;inums[i-1]){ //下降 + down[i]=up[i-1]+1; + up[i]=up[i-1]; + }else if(nums[i]nums[i-1]){ + down=up+1; + } + if(nums[i]nums[j])} + * 这里要求统计 最长上升子序列的数目 + * + * count[i]在[0...i]范围内的最长子序列的个数 + */ +public int findNumberOfLIS(int[] nums) { + int n=nums.length; + if(n==0){ + return 0; + } + int[] memo=new int[n]; + int[] count=new int[n]; + int maxlen=1; + for(int i=0;inums[j]){ + if(memo[j]+1>memo[i]){ + //说明这个长度的序列是新出现 + memo[i]=1+memo[j]; + count[i]=count[j]; + }else if(memo[j]+1==memo[i]){ + count[i]+=count[j]; + } + } + } + maxlen=Math.max(maxlen,memo[i]); + } + int res=0; + for(int i=0;i=lcs[i][j-1]){ + lcs[i][j]=lcs[i-1][j]; + x[i][j]='u'; + }else if(lcs[i-1][j] majorityElement(int[] nums) { + List res=new ArrayList<>(); + if(nums==null || nums.length==0){ + return res; + } + //出现次数大于三分之n的数字最多只有 2 个 + int majority1=nums[0]; + int majority2=-nums[0]; + int cnt1=0; + int cnt2=0; + + for(int num : nums){ + if(majority1==num){ + cnt1++; + }else if(majority2==num){ + cnt2++; + }else if(cnt1==0){ + majority1=num; + cnt1=1; + }else if(cnt2==0) { + majority2 = num; + cnt2 = 1; + } else{ + cnt1--; + cnt2--; + } + } + + cnt1=0; + cnt2=0; + for(int num:nums){ + if(majority1==num){ + cnt1++; + }else if(majority2==num){ + cnt2++; + } + } + int n=nums.length; + if(cnt1>n/3){ + res.add(majority1); + } + if(cnt2>n/3){ + res.add(majority2); + } + return res; +} +``` diff --git "a/docs/Linux/1_Linux\346\246\202\350\256\272.md" "b/docs/Linux/1_Linux\346\246\202\350\256\272.md" new file mode 100644 index 00000000..06c55860 --- /dev/null +++ "b/docs/Linux/1_Linux\346\246\202\350\256\272.md" @@ -0,0 +1,25 @@ +# Linux 概论 + +Linux 是一套作业系统,不是应用程序。 + +Linux 有如下优点: + +- 免费 + +- 开源,可被定制 + +- 很多软件原生是在 Linux 下运行的,庞大的社区支持,生态环境好 + +- 相对安全稳定 + + Linux 是基于 UNIX 的概念而开发出来的操作系统,因此,Linux 具有与 UNIX 系统相似的程序接口和操作方式,当然也继承了 UNIX 稳定并且有效率的特点。安装 Linux 的主机连续运行一年以上而不曾宕机、不必关机是很平常的事。 + +- 多任务、多用户 + + 与 Windows 系统不同,Linux 主机上可以同时允许多人上线来工作,并且资源的分配较为公平。可在一部 Linux 主机上规划不同等级的用户,而且每个用户登录系统时的工作环境都可以不同,此外,还允许不同的用户在同一时刻登录主机,以同时使用主机的资源。 + +Linux 还存在一些不足: + +- 没有特定的厂商支持 +- 游戏的支持度不足 +- 专业软件的支持度不足 \ No newline at end of file diff --git "a/docs/Linux/2_Linux\346\226\207\344\273\266\347\263\273\347\273\237.md" "b/docs/Linux/2_Linux\346\226\207\344\273\266\347\263\273\347\273\237.md" new file mode 100644 index 00000000..769edd9e --- /dev/null +++ "b/docs/Linux/2_Linux\346\226\207\344\273\266\347\263\273\347\273\237.md" @@ -0,0 +1,321 @@ +# Linux 文件系统 + +## Linux 文件系统简介 + +在 Linux 操作系统中,所有被操作系统管理的资源,例如网络接口卡、磁盘驱动器、打印机、输入输出设备、普通文件或是目录都被看作是一个文件。 + +在 Linux 系统中有一个重要的概念:**一切都是文件**。其实这是 Unix 哲学的一个体现,而 Linux 是重写 UNIX 而来,所以这个概念也就传承了下来。在 UNIX 系统中,把一切资源都看作是文件,包括硬件设备。UNIX 系统把每个硬件都看成是一个文件,通常称为**设备文件**,这样用户就可以用**读写文件的方式实现对硬件的访问**。 + + + +## Linux 目录结构 + +目录结构: + +Linux 文件系统的结构层次鲜明,就像一棵倒立的树,最顶层是其根目录 + +
+ +目录说明: + +- **/root:** 超级用户(系统管理员)的主目录; +- **/bin:** 存放二进制可执行文件(ls、cat、mkdir等),常用命令一般都在这里; +- **/etc:** 存放系统管理和配置文件; +- **/home:** 存放所有用户文件的根目录,是用户主目录的基点,比如用户user的主目录就是/home/user,可以用~user表示; +- **/usr :** 用于存放系统应用程序; +- **/opt:** 额外安装的可选应用程序包所放置的位置。一般情况下,我们可以把tomcat等都安装到这里; +- **/proc:** 虚拟文件系统目录,是系统内存的映射。可直接访问这个目录来获取系统信息; +- **/root:** 超级用户(系统管理员)的主目录; +- **/sbin:** 存放二进制可执行文件,只有root才能访问。这里存放的是系统管理员使用的系统级别的管理命令和程序。如ifconfig等; +- **/dev:** 用于存放设备文件; +- **/mnt:** 系统管理员安装临时文件系统的安装点,系统提供这个目录是让用户临时挂载其他的文件系统; +- **/boot:** 存放用于系统引导时使用的各种文件; +- **/lib :** 存放着和系统运行相关的库文件 ; +- **/tmp:** 用于存放各种临时文件,是公用的临时文件存储点; +- **/var:** 用于存放运行时需要改变数据的文件,也是某些大文件的溢出区,比方说各种服务的日志文件(系统启动日志等。)等; +- **/lost+found:** 这个目录平时是空的,系统非正常关机而留下“无家可归”的文件(windows下叫什么.chk)就在这里。 + + + +## Linux 文件类型 + +常见的文件类型及其含义有: + +- d:目录 +- -:普通文件 +- l:链接文件 +- c:字符设备文件 +- b:块设备文件 + +## Linux 文件权限 + +用户分为三种: + +- 文件拥有者(ower) +- 群组(group) +- 其它人(other) + +对不同的用户有不同的文件权限。 + +文件权限字段一共有 9 位。每 3 个为一组,共 3 组,每一组分别代表对文件拥有者、所属群组以及其它人的文件权限。一组权限中的 3 位分别为 r、w、x 权限,表示可读、可写、可执行。 + +### 文件权限字段的八进制模式 + +| 权限 | 二进制值 | 八进制值 | +| :--: | :------: | :------: | +| --- | 000 | 0 | +| --x | 001 | 1 | +| -w- | 010 | 2 | +| -wx | 011 | 3 | +| r-- | 100 | 4 | +| r-x | 101 | 5 | +| rw- | 110 | 6 | +| rwx | 111 | 7 | + +### 修改权限 + +使用八进制模式修改权限命令: + +```html +chmod [-R] xyz dirname/filename +``` + +示例:将 .bashrc 文件的权限修改为 -rwxr-xr--。 + +```html +chmod 754 .bashrc +``` + +也可以使用符号来设定权限。 + +```html +# chmod [ugoa] [+-=] [rwx] dirname/filename +- u:拥有者 +- g:所属群组 +- o:其他人 +- a:所有人 +- +:添加权限 +- -:移除权限 +- =:设定权限Copy to clipboardErrorCopied +``` + +示例:为 .bashrc 文件的所有用户添加写权限。 + +```html +chmod a+w .bashrc +``` + + + +## Linux 文件属性 + +使用 ls 查看一个文件时,会显示一个文件的信息,例如 + +`drwxr-xr-x 3 root root 17 May 6 00:14 .config` + +对这个信息的解释如下: + +- drwxr-xr-x:文件类型以及权限,第 1 位为文件类型字段(这里是 d),后 9 位为文件权限字段(rwxr-xr-x) +- 3:链接数 +- root:文件拥有者 +- root:所属群组 +- 17:文件大小 +- May 6 00:14:文件最后被修改的时间 +- .config:文件名 + + + +## 目录的基本操作 + +### 1. ls + +列出文件或者目录的信息,目录的信息就是其中包含的文件。 + +```html +# ls [-aAdfFhilnrRSt] file|dir +-a :列出全部的文件 +-d :仅列出目录本身 +-l :以长数据串行列出,包含文件的属性与权限等等数据 +``` + +### 2. cd + +更换当前目录。 + +``` +cd [相对路径或绝对路径] +``` + +### 3. mkdir + +创建目录。 + +``` +# mkdir [-mp] 目录名称 +-m :配置目录权限 +-p :递归创建目录 +``` + +### 4. rmdir + +删除目录,目录必须为空。 + +```html +rmdir [-p] 目录名称 +-p :递归删除目录 +``` + +### 5. pwd + +显示当前所在位置。 + + + +## 文件的基本操作 + +### 1. touch + +更新文件时间或者建立新文件。 + +```html +# touch [-acdmt] filename +-a : 更新 atime +-c : 更新 ctime,若该文件不存在则不建立新文件 +-m : 更新 mtime +-d : 后面可以接更新日期而不使用当前日期,也可以使用 --date="日期或时间" +-t : 后面可以接更新时间而不使用当前时间,格式为[YYYYMMDDhhmm] +``` + +### 2. cp + +复制文件。如果源文件有两个以上,则目的文件一定要是目录才行。 + +```html +cp [-adfilprsu] source destination +-a :相当于 -dr --preserve=all +-d :若来源文件为链接文件,则复制链接文件属性而非文件本身 +-i :若目标文件已经存在时,在覆盖前会先询问 +-p :连同文件的属性一起复制过去 +-r :递归复制 +-u :destination 比 source 旧才更新 destination,或 destination 不存在的情况下才复制 +--preserve=all :除了 -p 的权限相关参数外,还加入 SELinux 的属性, links, xattr 等也复制了 +``` + +### 3. rm + +删除文件。 + +```html +# rm [-fir] 文件或目录 +-r :递归删除 +``` + +### 4. mv + +移动文件。 + +```html +# mv [-fiu] source destination +# mv [options] source1 source2 source3 .... directory +-f : force 强制的意思,如果目标文件已经存在,不会询问而直接覆盖 +``` + + + +### 5. vim + +文本编辑器。 + +- 按下 "i" 等按键之后进入,可以对文本进行编辑; + +- 按下 ":" 按键之后进入,用于保存退出等操作。 + + 以下命令用于离开或者保存文件: + + | 命令 | 作用 | + | :--: | :----------------------------------------------------------: | + | :w | 写入磁盘 | + | :w! | 当文件为只读时,强制写入磁盘。到底能不能写入,与用户对该文件的权限有关 | + | :q | 离开 | + | :q! | 强制离开不保存 | + | :wq | 写入磁盘后离开 | + | :wq! | 强制写入磁盘后离开 | + + + +## 获取文件内容 + +### 1. cat + +取得文件内容。 + +```html +# cat [-AbEnTv] filename +-n :打印出行号,连同空白行也会有行号,-b 不会 +``` + +### 2. tac + +是 cat 的反向操作,从最后一行开始打印。 + +### 3. more + +和 cat 不同的是它可以一页一页查看文件内容,比较适合大文件的查看。 + +### 4. less + +和 more 类似,但是多了一个向前翻页的功能。 + +### 5. head + +取得文件前几行。 + +```html +# head [-n number] filename +-n :后面接数字,代表显示几行的意思 +``` + +### 6. tail + +是 head 的反向操作,只是取得是后几行。 + + + +## 文件搜索 + +### 1. whereis + +文件搜索。速度比较快,因为它只搜索几个特定的目录。 + +```html +# whereis [-bmsu] dirname/filename +``` + +### 2. locate + +文件搜索。可以用关键字或者正则表达式进行搜索。 + +locate 使用 /var/lib/mlocate/ 这个数据库来进行搜索,它存储在内存中,并且每天更新一次,所以无法用 locate 搜索新建的文件。可以使用 updatedb 来立即更新数据库。 + +```html +# locate [-ir] keyword +-r:正则表达式 +``` + +### 3. find + +文件搜索。可以使用文件的属性和权限进行搜索。 + +```html +# find [basedir] [option] +``` + +```html +# examples: +find -name "target.java" # 当面目录下查找文件 +find / -name "target.java" # 根目录下查找文件 +find ~ -name "target.java" # /home 目录下查找文件 +find ~ -name "target.*" # /home 目录下查找文件(模糊查询) +find ~ -iname "target.java" # /home 目录下查找文件(忽略大小写) +``` + diff --git "a/docs/Linux/3_Linux\345\270\270\347\224\250\345\221\275\344\273\244.md" "b/docs/Linux/3_Linux\345\270\270\347\224\250\345\221\275\344\273\244.md" new file mode 100644 index 00000000..b2429b31 --- /dev/null +++ "b/docs/Linux/3_Linux\345\270\270\347\224\250\345\221\275\344\273\244.md" @@ -0,0 +1,127 @@ +# Linux 常用命令 + +## grep + +g/re/p(globally search a regular expression and print),使用正则表示式进行全局查找并打印。 + +```html +$ grep [-acinv] [--color=auto] 搜寻字符串 filename +-c : 统计个数 +-i : 忽略大小写 +-n : 输出行号 +-v : 反向选择,也就是显示出没有 搜寻字符串 内容的那一行 +--color=auto :找到的关键字加颜色显示 +``` + +示例:把含有 the 字符串的行提取出来(注意默认会有 --color=auto 选项,因此以下内容在 Linux 中有颜色显示 the 字符串) + +```html +$ grep -n 'the' regular_express.txt +8:I can't finish the test. +12:the symbol '*' is represented as start. +15:You are the best is mean you are the no. 1. +16:The world Happy is the same with "glad". +18:google is the best tools for search keyword +``` + +因为 { 和 } 在 shell 是有特殊意义的,因此必须要使用转义字符进行转义。 + +```html +$ grep -n 'go\{2,5\}g' regular_express.txt +``` + +## printf + +用于格式化输出。它不属于管道命令,在给 printf 传数据时需要使用 $( ) 形式。 + +```html +$ printf '%10s %5i %5i %5i %8.2f \n' $(cat printf.txt) + DmTsai 80 60 92 77.33 + VBird 75 55 80 70.00 + Ken 60 90 70 73.33 +``` + +## awk + +是由 Alfred Aho,Peter Weinberger, 和 Brian Kernighan 创造,awk 这个名字就是这三个创始人名字的首字母。 + +awk 每次处理一行,处理的最小单位是字段,每个字段的命名方式为:\$n,n 为字段号,从 1 开始,\$0 表示一整行。 + +示例:取出最近五个登录用户的用户名和 IP + +```html +$ last -n 5 +dmtsai pts/0 192.168.1.100 Tue Jul 14 17:32 still logged in +dmtsai pts/0 192.168.1.100 Thu Jul 9 23:36 - 02:58 (03:22) +dmtsai pts/0 192.168.1.100 Thu Jul 9 17:23 - 23:36 (06:12) +dmtsai pts/0 192.168.1.100 Thu Jul 9 08:02 - 08:17 (00:14) +dmtsai tty1 Fri May 29 11:55 - 12:11 (00:15) +``` + +```html +$ last -n 5 | awk '{print $1 "\t" $3}' +``` + +可以根据字段的某些条件进行匹配,例如匹配字段小于某个值的那一行数据。 + +```html +$ awk '条件类型 1 {动作 1} 条件类型 2 {动作 2} ...' filename +``` + +示例:/etc/passwd 文件第三个字段为 UID,对 UID 小于 10 的数据进行处理。 + +```text +$ cat /etc/passwd | awk 'BEGIN {FS=":"} $3 < 10 {print $1 "\t " $3}' +root 0 +bin 1 +daemon 2 +``` + +awk 变量: + +| 变量名称 | 代表意义 | +| :------: | ---------------------------- | +| NF | 每一行拥有的字段总数 | +| NR | 目前所处理的是第几行数据 | +| FS | 目前的分隔字符,默认是空格键 | + +示例:显示正在处理的行号以及每一行有多少字段 + +```html +$ last -n 5 | awk '{print $1 "\t lines: " NR "\t columns: " NF}' +dmtsai lines: 1 columns: 10 +dmtsai lines: 2 columns: 10 +dmtsai lines: 3 columns: 10 +dmtsai lines: 4 columns: 10 +dmtsai lines: 5 columns: 9 +``` + + + +## 其他命令 + +### 1. ifconfig + +查看当前系统的网卡信息。 + +### 2. ping + +查看与某台机器的连接情况。 + +### 3. shutdown + +关机。 + +```html +shutdown -h now +# 指定现在立即关机 +``` + +```java +shutdown +5 "System will shutdown after 5 minutes" +# 指定5分钟后关机,同时送出警告信息给登入用户 +``` + +### 4. reboot + +重新开机。 \ No newline at end of file diff --git "a/docs/Linux/4_Liunx\350\277\233\347\250\213\347\256\241\347\220\206.md" "b/docs/Linux/4_Liunx\350\277\233\347\250\213\347\256\241\347\220\206.md" new file mode 100644 index 00000000..c7e4e5b0 --- /dev/null +++ "b/docs/Linux/4_Liunx\350\277\233\347\250\213\347\256\241\347\220\206.md" @@ -0,0 +1,69 @@ +# 进程管理 + +## 查看进程 + +### 1. ps + +查看某个时间点的进程信息 + +示例一:查看自己的进程 + +```sh +# ps -l +``` + +示例二:查看系统所有进程 + +```sh +# ps aux +``` + +示例三:查看特定的进程 + +```sh +# ps aux | grep threadx +``` + +### 2. pstree + +查看进程树 + +示例:查看所有进程树 + +```sh +# pstree -A +``` + +### 3. top + +实时显示进程信息 + +示例:两秒钟刷新一次 + +```sh +# top -d 2 +``` + +### 4. netstat + +查看占用端口的进程 + +示例:查看特定端口的进程 + +```sh +# netstat -anp | grep port +``` + + + +## 杀死进程 + +### kill + +杀死进程。 + +```sh +# kill -9 pid +# -9 表示强制终止 +# 注意:先用 ps 查找进程,然后用 kill 杀掉 +``` \ No newline at end of file diff --git "a/docs/Linux/5_Linux\345\216\213\347\274\251\344\270\216\346\211\223\345\214\205.md" "b/docs/Linux/5_Linux\345\216\213\347\274\251\344\270\216\346\211\223\345\214\205.md" new file mode 100644 index 00000000..7d96a9e5 --- /dev/null +++ "b/docs/Linux/5_Linux\345\216\213\347\274\251\344\270\216\346\211\223\345\214\205.md" @@ -0,0 +1,86 @@ +# Linux 压缩与打包 + +## 压缩文件名 + +Linux 底下有很多压缩文件名,常见的如下: + +| 扩展名 | 压缩程序 | +| ---------- | ------------------------------------- | +| \*.Z | compress | +| \*.zip | zip | +| \*.gz | gzip | +| \*.bz2 | bzip2 | +| \*.xz | xz | +| \*.tar | tar 程序打包的数据,没有经过压缩 | +| \*.tar.gz | tar 程序打包的文件,经过 gzip 的压缩 | +| \*.tar.bz2 | tar 程序打包的文件,经过 bzip2 的压缩 | +| \*.tar.xz | tar 程序打包的文件,经过 xz 的压缩 | + +## 压缩指令 + +### 1. gzip + +gzip 是 Linux 使用最广的压缩指令,可以解开 compress、zip 与 gzip 所压缩的文件。 + +经过 gzip 压缩过,源文件就不存在了。 + +有 9 个不同的压缩等级可以使用。 + +可以使用 zcat、zmore、zless 来读取压缩文件的内容。 + +```html +$ gzip [-cdtv#] filename +-c :将压缩的数据输出到屏幕上 +-d :解压缩 +-t :检验压缩文件是否出错 +-v :显示压缩比等信息 +-# : # 为数字的意思,代表压缩等级,数字越大压缩比越高,默认为 6 +``` + +### 2. bzip2 + +提供比 gzip 更高的压缩比。 + +查看命令:bzcat、bzmore、bzless、bzgrep。 + +```html +$ bzip2 [-cdkzv#] filename +-k :保留源文件 +``` + +### 3. xz + +提供比 bzip2 更佳的压缩比。 + +可以看到,gzip、bzip2、xz 的压缩比不断优化。不过要注意的是,压缩比越高,压缩的时间也越长。 + +查看命令:xzcat、xzmore、xzless、xzgrep。 + +```html +$ xz [-dtlkc#] filename +``` + +## 打包 + +压缩指令只能对一个文件进行压缩,而打包能够将多个文件打包成一个大文件。tar 不仅可以用于打包,也可以使用 gip、bzip2、xz 将打包文件进行压缩。 + +```html +$ tar [-z|-j|-J] [cv] [-f 新建的 tar 文件] filename... ==打包压缩 +$ tar [-z|-j|-J] [tv] [-f 已有的 tar 文件] ==查看 +$ tar [-z|-j|-J] [xv] [-f 已有的 tar 文件] [-C 目录] ==解压缩 +-z :使用 zip; +-j :使用 bzip2; +-J :使用 xz; +-c :新建打包文件; +-t :查看打包文件里面有哪些文件; +-x :解打包或解压缩的功能; +-v :在压缩/解压缩的过程中,显示正在处理的文件名; +-f : filename:要处理的文件; +-C 目录 : 在特定目录解压缩。 +``` + +| 使用方式 | 命令 | +| :------: | :---------------------------------------------------: | +| 打包压缩 | tar -jcv -f filename.tar.bz2 要被压缩的文件或目录名称 | +| 查 看 | tar -jtv -f filename.tar.bz2 | +| 解压缩 | tar -jxv -f filename.tar.bz2 -C 要解压缩的目录 | \ No newline at end of file diff --git "a/docs/MassDataProcessing/1_\346\246\202\350\277\260.md" "b/docs/MassDataProcessing/1_\346\246\202\350\277\260.md" new file mode 100644 index 00000000..64adf044 --- /dev/null +++ "b/docs/MassDataProcessing/1_\346\246\202\350\277\260.md" @@ -0,0 +1,33 @@ +# 概述 + +海量数据处理是指基于海量数据的存储、处理、和操作。 + +正因为数据量太大,所以导致要么无法在较短时间内迅速解决,要么无法一次性装入内存。 + +- 对于时间问题,可以采用巧妙的算法搭配合适的数据结构(如布隆过滤器、哈希、位图、堆、 数据库、倒排索引、Trie 树)来解决; +- 对于空间问题,可以采取分而治之(哈希映射)的方法,也就是说,把规模大的数据转化为规模小的,从而各个击破。 + +此外,针对常说的单机及集群问题: + +- 单机就是指处理装载数据的机器有限(只要考虑 CPU、 内存、和硬盘之间的数据交互) +- 集群的意思是指机器有多台,适合分布式处理或并行计算,更多考虑节点与节点之间的数据交互。 + +处理海量数据问题,有 10 种典型方法: + +- 哈希分治 + +- simhash 算法 + +- 外排序 + +- MapReduce + +- 多层划分 + +- 位图 + +- 布隆过滤器 +- Trie 树 + +- 数据库 +- 倒排索引 \ No newline at end of file diff --git "a/docs/MassDataProcessing/2_\345\223\210\345\270\214\345\210\206\346\262\273.md" "b/docs/MassDataProcessing/2_\345\223\210\345\270\214\345\210\206\346\262\273.md" new file mode 100644 index 00000000..3f2acf6f --- /dev/null +++ "b/docs/MassDataProcessing/2_\345\223\210\345\270\214\345\210\206\346\262\273.md" @@ -0,0 +1,110 @@ +# 哈希分治 + +## hash 映射 + +为了便于计算机在有限的内存中处理大数据,我们通过一种映射散列的方式让数据均匀分布在对应的内存位置(如大数据通过取余的方式映射成小数据存放在内存中, 或大文件映射成多个小文件),而这种映射散列的方式便是我们通常所说的 hash 函数,好的 **hash 函数**能让数据均匀分布而减少冲突。 + +对于海量数据而言,由于无法一次性装进内存处理,不得不把海量的数据通过 hash 映射的方法分割成相应的小块数据,然后再针对各个小块数据通过 hashMap 进行统计或其他操作。 + +## 1. 寻找 TOP IP + +### 问题 + +海量日志数据,提取出某日访问百度次数最多的那个 IP。 + +### 思路 + +如果想一次性把所有 IP 数据装进内存处理,则内存容量通常不够,故**针对数据太大,内存受限的情况**,可以把大文件转化成(取模映射)小文件,从而大而化小,逐个处理。即,**先映射,而后统计,最后排序**。 + +### 步骤 + +- 先映射。 + + 首先将该日访问百度的所有 IP 从访问日志中提取出来,然后逐个写入到一个大文件中,接着采取 hash 映射的方法(比如 hash(IP)%1000),把整个大文件的数据映射到 1000 个小文件中。 + +- 统计。 + + 当大文件转化成了小文件,那么我们便可以采用 hashMap(ip,cnt) 来分别对 1000 个小文件中的 IP 进行频率统计,找出每个小文件中出现频率最大的 IP,总共 1000 个 IP。 + +- 堆排序/快速排序。 + + 统计出 1000 个频率最大的 IP 后,依据它们各自频率的大小进行排序,找出那个出现频率最大的 IP,即为所求。 + + + +## 2. 寻找热门查询 + +### 问题 + +搜索引擎会通过日志文件把用户每次检索所使用的所有查询串都记录下来, 每个查询串的长度为 1-255 字节。假设目前有 1000 万个查询记录(但因为这些查询串的重复度比较高,所以虽然总数是 1000 万,但如果除去重复后,不超过 300 万个查询字符串),请统计其中最热门的 10 个查询串,要求使用的 内存不能超过 1G。 + +一个查询串的重复度越高,说明查询它的用户越多,也就是越热门。 + +### 思路 + +针对本题数据规模是比较小的,能一次性装入内存。虽然有 1000 万条查询记录,但是去除重复后,不超过 300 万条查询记录。占用的最大内存为 300 0000 * 255 B = 0.75 GB,可以将字符串都放入内存进行处理。 + +### 步骤 + +- 统计。 + + 使用 hashMap 进行频率统计。时间复杂度为 O(n) + +- 堆排序。 + + 借助堆这个数据结构,找出 Top K,维护一个 K 大小的小根堆,然后遍历 300 万条查询记录,分别和根元素进行比较。时间复杂度为 N'O(log K)。 + +所以,最终的时间复杂度是:O(n)+N'O(log K),其中,N 为 1000 万,N' 为 300 万,K 为 10。 + + + +## 3. 寻找频数最高的 100 个词 + +### 问题 + +有一个 1G 大小的文件,里面每一行是一个词,词的大小不超过 16 字节,内存限制大小是 1M。返回频数最高的 100 个词。 + +### 思路 + +内存大小只有 1 M,而文件却有 1 GB,所有必须将该大文件切分为多个小于 1 M 的小文件。 + +### 步骤 + +- 哈希映射。 + + 按先后顺序读取文件,对于每个词 x,执行 hash(x)%5000,然后将该值存到 5000 个小文件(记为 x0, x1, ..., x4999)中。如此每个文件的大小大概是 200k 左右。当然,如果其中有的小文件超过了 1M 大小,则可以按照类似的方法继续往下分,直到分解得到的**小文件的大小都不超过 1M**。 + +- 统计。 + + 对每个小文件中出现的词进行频率统计。 + +- 堆排序 / 归并排序。 + + 取出每个文件中出现频率最大的 100 个词(可以用含 100 个结点的小根堆)后,再把 100 个词及相应的频率存入文件,这样又得到了 5000 个文件。最后把这 5000 个文件进行归并(可以用归并排序)。 + + + +## 4. 寻找共同的 URL + +### 问题 + +给定 a、b 两个文件,各存放 50 亿个 url,每个 url 各占 64 字节,内存限制是 4G,请找出 a、b 文件共同的 url。 + +### 思路 + +可以估计每个文件的大小为 5G * 64=320G,远远大于内存限制的 4G。所以不可能将其完全加载到内存中处理。 + +### 步骤 + +- 哈希映射。 + + 遍历文件 a,对每个 url 求取 ,然后根据所取得的值将 url 分别 存储到 1000 个小文件(记为 a1 a2 a3 a4)中。这样每个小文件的大约为 300M。 + + 遍历文件 b,采取和 a 相同的方式将 url 分别存储到 1000 小文件中(记为 b1 b2 b3 b4)。 + + 这样处理后,所有可能相同的 url 都在对应的小文件url。然后我们只要求出 1000 对小文件中相同的 url 即可。 + +- 统计。 + + 求每对小文件中相同的 url 时,可以把其中一个小文件的 url 存储到 hashSet 中。然 后遍历另一个小文件的每个 url,看其是否在刚才构建的 hashSet 中,如果是,那么就是共同的 url,存到 文件里面就可以了。 + diff --git "a/docs/MassDataProcessing/3_\344\275\215\345\233\276.md" "b/docs/MassDataProcessing/3_\344\275\215\345\233\276.md" new file mode 100644 index 00000000..3093644b --- /dev/null +++ "b/docs/MassDataProcessing/3_\344\275\215\345\233\276.md" @@ -0,0 +1,47 @@ +# 位图 + +## 位图(Bit-map) + +位图(Bit-map)就是用一个 bit 位来标记某个元素对应的 value, 而 key 即是该元素。由于采 用了 bit 为单位来存储数据,因此在存储空间方面,可以大大节省。 + +位图通过使用 bit 数组来表示某些元素是否存在,可进行数据的快速查找、判重、删除,一般来说数 据范围是 int 的 10 倍以下。 + +例如要对 {4,7,2,5,3} 进行排序,可以设置一个范围为 0~8 的比特数组,读入数据之后将比特数组第 2、3、4、5、7 位置设置为 1。最后从头遍历比特数组,将比特数组值为 1 的数据读出得到 {2,3,4,5,7} 这个已排序的数据。 + +## 1. 2.5 亿个数的去重 + +### 问题 + +在 2.5 亿个整数中找出**不重复的整数**,注,内存不足以容纳这 2.5 亿个整数。 + +### 分析 + +采用 2-Bitmap(即每个整数分配 2 bit:00 表示不存在,01表示出现一次,10 表示多次,11 表示无意义)。 + +这样,所占内存 2^32 * 2 bit = 1 GB。 + +### 步骤 + +- 扫描。 + + 扫描这 2.5 亿个整数,查看 Bitmap 中相对应位, 如果是 00 变 01,01 变 10,10 保持不变。 + +- 输出。 + + 查看对应位为 01 的整数,输出即可。 + + + +## 2. 整数的快速查询 + +### 问题 + +给 40 亿个不重复的 unsigned int 的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那 40 亿个数当中? + +### 分析 + +使用用位图的方法,40*10^8 = 4 *10^9 bit = 500 M,所以可以申请 512M 的内存,一个 bit 位代表一个 unsignedint 值。 + +### 步骤 + +读入 40 亿个数,设置相应的 bit 位,读入要查询的数,查看相应 bit 位是否为 1,为 1 表示存在,为 0 表示不存在。 \ No newline at end of file diff --git "a/docs/MassDataProcessing/4_\345\270\203\351\232\206\350\277\207\346\273\244\345\231\250.md" "b/docs/MassDataProcessing/4_\345\270\203\351\232\206\350\277\207\346\273\244\345\231\250.md" new file mode 100644 index 00000000..d1aaf8bc --- /dev/null +++ "b/docs/MassDataProcessing/4_\345\270\203\351\232\206\350\277\207\346\273\244\345\231\250.md" @@ -0,0 +1,38 @@ +# 布隆过滤器 + +## 布隆过滤器(Bloom Filter) + +布隆过滤器(BloomFilter)是一种空间效率很高的随机数据结构,可以看做是对位图(bit-map)的扩展。 + +原理是:当一个元素被加入集合时,通过 **K 个 Hash 函数**将这个元素映射成一个位阵列(Bitarray) 中的 K 个点,并将它们置为 1。在检索一个元素是否在一个集合中时,我们只要看看这些点是不是都是 1 就能大约判断出集合中有没有它了:如果这些点有任何一个 0,则被检索元素一定不在;如果都是 1,则**被检索元素很可能存在集合中**(因为哈希函数的特点,两个不同的数通过哈希函数得到的值可能相同)。 + +布隆过滤器有一定的误判率:在判断一个元素是否属于某个集合时,有可能会把不属于这个集合的元素误认为属于这个集合。因此,它**不适合那些“零错误”的应用场合**。而在能容忍低错误率的应用场合下,布隆过滤器通过极少的错误换取了存储空间的极大节省。 + +## 1. 寻找通过 URL + +### 问题 + +给你 A,B 两个文件,各存放 50 亿条 URL,每条 URL 占用 64 字节,内存限制是 4G,让你找出 A,B 文件共同的 URL。 + +### 分析 + +如果允许有一定的错误率,可以使用布隆过滤器,4G 内存大概可以表示 320 亿 bit。 + +### 步骤 + +将其中一 个文件中的 url 使用布隆过滤器映射为这 340 亿 bit,然后读取另外一个文件的 url,使用布隆过滤器进行判重,如果是重复的,那么该 url 就是共同的 url(允许有一定的错误率的情况)。 + + + +## 2. 垃圾邮件过滤 + +### 问题 + +如何过滤垃圾邮件。 + +### 分析 + +比较直观的想法是把常见的垃圾邮件地址存到一个巨大的集合中,然后遇到某个新的邮件,将它的地址和集合中的全部垃圾邮件地址一一进行比较,如果有元素与之匹配,则判定新邮件为垃圾邮件。如果允许一定的误判率的话,我们可以使用布隆过滤器。 + + + diff --git "a/docs/MassDataProcessing/5_Trie\346\240\221.md" "b/docs/MassDataProcessing/5_Trie\346\240\221.md" new file mode 100644 index 00000000..90bcb019 --- /dev/null +++ "b/docs/MassDataProcessing/5_Trie\346\240\221.md" @@ -0,0 +1,37 @@ +# Trie 树 + +## Trie 树(字典树) + +Trie 树,即字典树,又称单词查找树或键树,是一种树形结构。 + +典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是最大限度地减少无谓的字符串比较,查询效率比较高。 + +Trie 的核心思想是**空间换时间**,利用字符串的**公共前缀**来降低查询时间的开销以达到提高效率的目的。 + +它有 3 个基本性质: + +- 根节点不包含字符,除根节点外每一个节点都只包含一个字符 +- 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串 +- 每个节点的所有子节点包含的字符都不相同 + +## 1. 10 个频繁出现的词 + +### 问题 + +一个文本文件,大约有一万行,每行一个词,要求统计出其中出现次数最频繁的 10 个词,请给出思路和时间复杂度的分析。 + +### 分析 + +用 Trie 树统计每个词出现的次数,时间复杂度是 O(n*l)(l 表示单词的平均长度),然后是找出出现最频繁的前 10 个词。 + +## 2. 寻找热门查询 + +### 问题 + +搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为 1-255 字节。假设目前有一千万个记录,这些查询串的重复度比较高,虽然总数是 1 千万,但是如果去除重复和,不超过 3 百万个。一个查询串的重复度越高,说明查询它的用户越多,也就越热门。请你统计最热门的 10 个查询串,要求使用的内存不能超过 1G。 + +### 分析 + +3 百万字符占用的最大内存为 300 0000 * 255 B = 0.75 GB。 + +利用 Trie 树,观察关键字域存该查询串出现的次数,若没有出现则为 0。最后用 10 个元素的最小堆来对出现频率进行排序。 \ No newline at end of file diff --git "a/docs/MassDataProcessing/6_\346\225\260\346\215\256\345\272\223.md" "b/docs/MassDataProcessing/6_\346\225\260\346\215\256\345\272\223.md" new file mode 100644 index 00000000..ba6ca611 --- /dev/null +++ "b/docs/MassDataProcessing/6_\346\225\260\346\215\256\345\272\223.md" @@ -0,0 +1,3 @@ +# 数据库 + +当遇到大数据量的增删改查时,一般把数据装进数据库中,从而利用数据的设计实现方法,对海量数据的增删改查进行处理。而数据库[索引](https://duhouan.github.io/Java/#/MySQL/3_%E7%B4%A2%E5%BC%95)的建立则对查询速度起着至关重要的作用。 \ No newline at end of file diff --git "a/docs/MassDataProcessing/7_\345\200\222\346\216\222\347\264\242\345\274\225.md" "b/docs/MassDataProcessing/7_\345\200\222\346\216\222\347\264\242\345\274\225.md" new file mode 100644 index 00000000..717ced66 --- /dev/null +++ "b/docs/MassDataProcessing/7_\345\200\222\346\216\222\347\264\242\345\274\225.md" @@ -0,0 +1,28 @@ +# 倒排索引 + +倒排索引((Inverted Index))是一种索引方法,被用来存储在全文搜索下某个单词在一个文档或者一 组文档中的**存储位置的映射**。 + +以英文为例,下面是要被索引的文本 T0、T1、T2: + +```html +T0 = "it is what it is" +``` + +```html +T1 = "what is it" +``` + +```html +T2 = "it is a banana" +``` + +得到反向文件索引: + +```html +"a": {2} +"banana": {2} +"is": {0, 1, 2} +"it": {0, 1, 2} +"what": {0, 1} +``` + diff --git a/docs/Maven/1_Maven.md b/docs/Maven/1_Maven.md new file mode 100644 index 00000000..441e1592 --- /dev/null +++ b/docs/Maven/1_Maven.md @@ -0,0 +1,266 @@ +# Maven + +Maven 是基于**项目对象模型(POM)**,可以通过一小段描述信息来管理项目的构建、报告和文档的软件项目管理工具。 + +[Maven 下载](http://maven.apache.org/download.cgi) + +[Maven 官网](https://maven.apache.org/) + + + +## Maven 目录结构 + +```html +src + |- main + |--java + |-- packages + |-- resources + |- test + |--java + |-- packages +``` + + + +## Maven 坐标和仓库 + +### 坐标 + +- groupID + + **项目组织**的唯一标识符,实际对应 java 的包的结构,即 main目录里 java 的目录结构。 + +- artifactID + + **项目**的唯一标识符,实际对应项目的名称,就是项目根目录的名称。 + +groupId 和 artifactId 被统称为**坐标**。构件通过坐标作为其唯一标识。 + +groupId 一般为 `组织名+公司地址反写+项目名`,比如 `com.southeast.myprojects` + +artifactId 一般为 `项目名-模块名`,比如`myprojects-demo` + +则相应的 package 为 `com.southeast.myprojects.demo` + +### 镜像仓库 + +setting.xml 文件中配置: + +```html + + maven.net.cn + central + central mirror in china + http://maven.aliyun.com/nexus/content/groups/public/ + +``` + +### 修改本地仓库位置 + +setting.xml 文件中配置: + +```html +F:/Java/apache-maven-3.6.2-bin/repo +``` + + + +## Maven 常用构建命令 + +### 1. 查看版本 + +```html +mvn - v +``` + +### 2. 编译 + +```html +mvn compile +``` + +### 3. 测试 + +```html +mvn test +``` + +### 4. 打包 + +```html +mvn package +``` + +### 5. 清除 target + +```html +mvn clean +``` + +### 6. 安装 jar + +```html +mvn install +``` + +### 7. 自动创建目录骨架 + +```html +mvn archetype:generate +# 按照提示进行选择 +``` + +```html +mvn archetype:generate -DgroupId=组织名,公司网址反写+项目名 + -DartifactId=项目名-模块名 + -Dversion=版本号 + -Dpackage=代码所存在的包名 +``` + + + +## pom.xml 解析 + +- 指定当前 pom 版本 + + ```html + + ``` + +- 坐标 + + ```html + 反写公司网址+项目名 + 项目名+模块名 + + 0.0.1snapshot + + jar + + + + + ``` + +- 依赖列表 + + ```html + + + + + + + + test + + + + + + + + + + + ``` + +- 依赖管理 + + ```html + + + + + + ``` + +- build 标签 + + ```html + + + + + + + + + + + ``` + +- parent 标签(继承) + + ```html + + + + ``` + +- modules 标签(聚合) + + ```html + + + + ``` + +## 依赖冲突 + +### 1. 短路优先 + +```html +路径1:A -> B -> C -> X(jar) +路径2:A -> D -> X(jar) +``` + +这里优先选择路径 2。 + +### 2. 先声明先优先 + +如果路径长度相同,则谁先声明,先解析谁。 + +、 + + + + + diff --git "a/docs/MySQL/1_\351\224\201\346\234\272\345\210\266.md" "b/docs/MySQL/1_\351\224\201\346\234\272\345\210\266.md" new file mode 100644 index 00000000..d3998817 --- /dev/null +++ "b/docs/MySQL/1_\351\224\201\346\234\272\345\210\266.md" @@ -0,0 +1,116 @@ +# 锁机制 + +## 数据库锁的分类 + +按照锁的**粒度**划分: + +- 表级锁 +- 行级锁 +- 页级锁 + +按照锁的**级别**划分: + +- 共享锁 +- 排它锁 + +按照**使用方式**划分: + +- 乐观锁 +- 悲观锁 + +## 行级锁、表级锁、页级锁 + +### 1. 行级锁 + +行级锁是 MySQL 中锁定粒度最细的一种锁,表示只针对当前行进行加锁。行级锁分为共享锁和排他锁。 + +特点: + +- 开销大,加锁慢 +- 会出现死锁 +- 锁定粒度最小,发生锁冲突的概率最低,并发度最高 + +### 2. 表级锁 + +表级锁时 MySQL 中锁定粒度最大的一种锁,表示对当前操作的整张表进行加锁。最常用的 MyISAM 和 InnoDB 都支持表级锁。 + +特点: + +- 开销小,加锁快 +- 不会出现死锁 +- 锁定粒度最大,发生锁冲突的概率最高,并发度最低 + +### 3. 页级锁 + +页级锁是 MySQL 中锁定粒度介于表级锁和行级锁之间的一种锁。 + +特点: + +- 开销和加锁时间介于表级锁和行级锁之间 +- 会出现死锁 +- 锁定粒度介于表锁和行锁之间,并发度一般 + +
+ + + +## 共享锁和排他锁 + +### 1. 共享锁(S) + +共享锁(Shared)又称读锁,是读取操作创建的锁。 + +如果事务 T 对数据 A 加上共享锁后,则其他事务只能对 A 再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能修改数据。 + +### 2. 排他锁(X) + +排他锁(eXclusive)又称写锁。 + +如果事务 T 对数据 A 加上排他锁后,则其他事务不能再对 A 加上任何类型的锁。获取排他锁的事务既能读数据,又能修改数据。 + +共享锁和排他锁的兼容关系如下: + +| - | X | S | +| :--: | :--: | :--: | +| X | 冲突 | 冲突 | +| S | 冲突 | 兼容 | + + + +## 意向锁 + +意向锁是**表级锁**,但表示事务正在读写某一行记录,而不是整个表。所以意向锁之间不会发生冲突,真正的冲突发生在加行锁时检查。 + +### 1. 意向共享锁(IS) + +表示**事务准备给数据行加共享锁**,也就是说事务在一个数据行加共享锁前必须先获取该表的 IS 锁。 + +### 2. 意向排他锁(IX) + +表示事务准备给数据行加排他锁,也就是说事务在一个数据行加排他锁前必须先获取该表的 IX 锁。 + + + +## 悲观锁和乐观锁 + +### 1. 悲观锁 + +悲观锁机制认为每一步如果不采取同步措施都会出现问题,**依赖于数据库的锁机制**。关系型数据库里边就用到了很多悲观锁机制,比如**行锁、表锁**等,**读锁、写锁**等,都是在做操作之前先上锁。 + +如果锁定时间过长,用户长时间无法访问,会影响程序的并发访问。 + +### 2. 乐观锁 + +先执行,如果存在冲突,则采取一个补偿措施(比如告知用户失败)。一般有 2 种实现方式: + +- 使用版本号 +- 使用时间戳 + +## MySQL 隐式与显式锁定 +MySQL 的 InnoDB 存储引擎采用两段锁协议,会根据隔离级别在需要的时候自动加锁,并且所有的锁都是在同一时刻被释放,这被称为隐式锁定。 + +InnoDB 也可以使用特定的语句进行显示锁定: +```sql +SELECT ... LOCK In SHARE MODE; +SELECT ... FOR UPDATE; +``` diff --git "a/docs/MySQL/2_\344\272\213\345\212\241\351\232\224\347\246\273\347\272\247\345\210\253\345\256\236\347\216\260.md" "b/docs/MySQL/2_\344\272\213\345\212\241\351\232\224\347\246\273\347\272\247\345\210\253\345\256\236\347\216\260.md" new file mode 100644 index 00000000..575e0580 --- /dev/null +++ "b/docs/MySQL/2_\344\272\213\345\212\241\351\232\224\347\246\273\347\272\247\345\210\253\345\256\236\347\216\260.md" @@ -0,0 +1,245 @@ +# 事务的隔离级别的实现 + +## 多版本并发控制-MVCC + +多版本并发控制(Multi-Version Concurrency Control, MVCC)以**乐观锁**为理论基础,和基于锁的并发控制最大的区别和优点是:**读不加锁,读写不冲突**。 + +### **事务版本号** + +每次事务开启前都会从数据库获得一个自增长的事务 id,可以从事务 id 判断事务的执行先后顺序。 + +### 隐藏字段 + +InnoDB 存储引擎为每行记录都添加了 3 个隐藏字段: + +- DB_ROW_ID:数据行 id,用于标识一行数据。并不是必要的,如果创建的表中有主键或者非 NULL 唯一键时都不会包含 DB_ROW_ID 列 + +- DB_TRX_ID:表示最后一次插入或更新该行的事务 id。此外,delete 操作在内部被视为更新,只不过会在记录头中的 deleted_flag 字段将其标记为已删除 + +- DB_ROLL_PTR:当前数据记录的上一个版本的指针。每次对某条数据记录进行改动时,都会把旧版本数据记录按照一定格式写入到回滚日志 (undo log) 中,而 DB_ROLL_PTR 列则保存了该旧版本数据记录在回滚日志中的位置,相当于一个指针。 + +### undo log + +MVCC 将每一个更新的数据标记一个版本号,在更新时进行版本号的递增,插入时新建一个版本号,同时旧版本数据存储在 Undo 日志中,该日志通过**回滚指针**把一个数据行(Record)的所有快照连接起来。 + +假设在 MySQL 创建一个表 user,包含主键 id 和一个字段 name。我们先插入一个数据行,然后对该数据行执行两次更新操作。 + +```sql +INSERT INTO user(id, name) VALUES(1, "a"); +UPDATE name SET name="b" WHERE id=1; +UPDATE name SET name="c" WHERE id=1; +``` + +因为没有使用 `START TRANSACTION` 将上面的操作当成一个事务来执行,根据 MySQL 的 AUTOCOMMIT 机制,每个操作都会被当成一个事务来执行,所以上面的操作总共涉及到三个事务。 + +
+ +undo log 主要有两个作用: + +- 当事务回滚时用于将数据恢复到修改前的样子,保证事务原子性。 +- 另一个作用是实现 MVCC ,当读取记录时,若该记录被其他事务占用或当前版本对该事务不可见,则可以通过 undo log 读取之前的版本数据,以此实现非锁定读 + +### 快照(ReadView) + +#### ReadView 结构 + +```c +class ReadView { + private: + trx_id_t m_low_limit_id; /* 大于这个 id 的事务均不可见 */ + trx_id_t m_up_limit_id; /* 小于这个 id 的事务均可见 */ + trx_id_t m_creator_trx_id; /* 创建该 ReadView 的事务 id */ + ids_t m_ids; /* 创建 ReadView 时的活跃事务列表 */ + m_closed; /* 标记 ReadView 是否 close */ +} +``` + +ReadView 主要有以下字段: + +- m_creator_trx_id:创建该 ReadView 的事务 id + +- m_ids:ReadView 创建时其他未提交的活跃事务 id 列表。创建 ReadView 时,将当前未提交事务 ID 记录下来,后续即使它们修改了记录行的数据,对于当前事务也是不可见的。注意 m_ids 不包括当前事务自己和已提交的事务(正在内存中) + +- m_low_limit_id:目前出现过的**最大的事务 id+1**,即下一个将被分配的事务 id。 +- m_up_limit_id :活跃事务列表 m_ids 中**最小的事务 id**,如果 m_ids 为空,则 m_up_limit_id 等于 m_low_limit_id。 + +#### 数据可见性算法 + +InnoDB 存储引擎在开启一个新事务后,执行每个 select 语句前,都会创建一个 ReadView,ReadView 中保存了当前数据库系统中正处于活跃(没有 commit)的事务的 id 号,即系统中当前不应该被该事务看到的其他事务 id 列表 m_ids。当用户在该事务读取某行记录时,InnoDB 会将该行记录的 DB_TRX_ID 与 ReadView 中的一些变量及当前事务 id 进行比较,判断是否满足可见性条件: + +- DB_TRX_ID < m_up_limit_id,表明最新修改该行的事务(DB_TRX_ID)在当前事务创建快照之前就提交了,所以该记录行的值对当前事务是可见的 + +- DB_TRX_ID >= m_low_limit_id,那么表明最新修改该行的事务(DB_TRX_ID)在当前事务创建快照之后才修改该行,所以该记录行的值对当前事务不可见 +- m_ids 为空,则表明在当前事务创建快照之前,修改该行的事务就已经提交了,所以该记录行的值对当前事务是可见的 +- m_up_limit_id <= DB_TRX_ID < m_up_limit_id,表明最新修改该行的事务(DB_TRX_ID)在当前事务创建快照的时候可能处于“活动状态”或者“已提交状态”;所以就要对活跃事务列表 m_ids 进行二分查找(m_ids 是有序的): + - 在 m_ids 中找到 DB_TRX_ID,表明在当前事务创建快照前,该记录行的值被事务 id 为 DB_TRX_ID 的事务修改了,但没有提交;或者在当前事务创建快照后,该记录行的值被事务 id 为 DB_TRX_ID 的事务修改了。这些情况下,这个记录行的值对当前事务都是不可见的 + - 在 m_ids 中找不到 DB_TRX_ID,表明是事务 id 为 DB_TRX_ID 的事务在修改该记录行的值后,在当前事务创建快照前就已经提交了,所以记录行对当前事务可见 + +在记录行快照不可见的情况下,在该记录行的 DB_ROLL_PTR 指针所指向的 undo log 取出快照记录,用快照记录的 DB_TRX_ID 再重新开始判断,直到找到可见的快照版本或返回空。 + +事务可见性示意图: + +
+ +### 快照读 & 当前读 + +#### 快照读 + +快照读只是针对于目标数据的版本号小于等于当前事务的版本号,也就是说读数据的时候可能读到旧数据,但是这种快照读不需要加锁。也就是说,使用 MVCC 读取的是快照中的数据,这样可以减少加锁所带来的开销。 + +```sql +select * from table ...; +``` + +#### 当前读 + +当前读是读取当前数据的最新版本,但是更新等操作会对数据加锁,所以当前读需要获取记录的行锁,存在锁争的问题。以下第一个语句需要加 S 锁,其它都需要加 X 锁。 + +```sql +select * from table where ? lock in share mode; # 加 S 锁 +select * from table where ? for update; +insert; +update; +delete; +``` + + + +## MySQL 中事务隔离级别的实现 + +### 1. 可串行化(SERIALIZABLE) + +读加共享锁(S),写加排他锁(X),读写互斥。 + +使用的是悲观锁的理论,实现简单,数据更加安全。 + +### 2. 提交读(READ COMMITTED) 和可重复读(REPEATABLE READ) + +RR 是 InnoDB 存储引擎的默认事务隔离级别。 + +RC 和 RR 都是基于 MVCC 实现的,但生成快照的时机不同: + +- RC 级别下。**每次 select 查询**前都会生成一个 ReadView。有可能会出现一个事务中两次读到了不同的结果。 +- RR 级别下。只在事务开始后, **第一次 select 查询**前生成一个ReadView。 + +### 3. 未提交读(READ UNCOMMITTED) + +总是读取最新的数据行,无需使用 MVCC。 + +## InnoDB 行锁算法 + +Next-Key Locks 是 MySQL 的 InnoDB 存储引擎的一种锁实现。 + +### Record Locks + +**锁定一个记录上的索引,而不是记录本身**。 + +如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚簇索引,因此 Record Locks 依然可以使用。 + +### Gap Locks + +**锁定索引之间的间隙,但是不包含索引本身**。 + +例如当一个事务执行以下语句,其它事务就不能在 c 中插入 15。 + +```sql +SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE; +``` + +### Next-Key Locks + +它是 Record Locks 和 Gap Locks 的结合,**不仅锁定一个记录上的索引,也锁定索引之间的间隙,是一个前开后闭区间**。例如一个索引包含以下值:10, 11 and 20,那么就需要锁定以下区间: + +```sql +(-∞, 10] +(10, 11] +(11, 20] +(20, +∞) +``` + +如下图所示: + +
+ +### 几个问题 + +> 问题一:对主键索引或唯一索引会使用间隙锁吗? + +不一定。视情况而定: + +- **如果 where 条件全部命中(不会出现幻读),则不会加间隙锁**,只会加记录锁 + +- 如果 where 条件部分命中 / 全都不命中,则会加间隙锁 + +```sql +delete from tb where id = 9 +-- Table: tb(name primary key,id unique key) +-- key 是唯一索引 +``` + +根据 id=9 条件定位,此时给 id = 9 的索引加上记录锁,根据 name 值(name是主键)到主索引中检索获得记录,再给该记录加上记录锁。 + +
+ +> 问题二:间隙锁是否用在非唯一索引的当前读中? + +是的。 + +```mysql +delete from tb1 where id = 9 +-- Table: tb1(name primary key,id key) +-- key 是非唯一索引 +``` + +
+ +可以看出,在 (6,9]、(9,11] 加了间隙锁。 + +> 问题三:间隙锁是否用在不走索引的当前读中? + +是的。 + +```sql +delete from tb2 where id = 9 +-- Table: tb2(name primary key,id) +-- 没有为 id 建立索引 +``` + +
+ +此时对所有的间隙都上锁(功能上相当于锁表)。 + +总结以上三个问题,我们得到如下结论: + +- 主键索引 / 唯一索引: + + 如果 where 条件全部命中(不会出现幻读),则不会加间隙锁,只会加记录锁 + + 如果 where 条件部分命中 / 全都不命中,则会加间隙锁 + +- 非唯一索引: + + 会加间隙锁 + +- 不走索引: + + 对所有间隙都加间隙锁,相当于锁表 + +## 解决幻读问题 + +InnoDB 存储引擎在 RR 级别下通过 MVCC + Next-key Lock 来解决幻读问题: + +- 执行 `select * from table ...;` 会以 MVCC **快照读**的方式读取数据。 + + 在快照读的情况下,RR 隔离级别只会在事务开启后的第一次查询生成 ReadView,使用至事务提交。 在生成 ReadView 之后其它事务所做的更新、插入记录版本对当前事务并不可见,实现了可重复读和防止快照读下的 “幻读”。 + +- 执行 `select * from table where ? lock in share mode/for update; `、`insert`、`update`、`delete` 会以 MVCC **当前读**的方式读取数据。 + + 在当前读的情况下,读取的都是最新的数据,如果存在其它事务插入新的记录,并且刚好在当前事务查询范围内,就会产生幻读。 + + InnoDB 就使用 Next-key Lock 来防止这种情况:当执行当前读时,在锁定读取到的记录时,也会锁定它们的间隙,防止其它事务在查询范围内插入数据。 + +# 参考资料 + +- [MySQL-InnoDB-MVCC多版本并发控制](https://segmentfault.com/a/1190000012650596) +- [Innodb中的事务隔离级别和锁的关系](https://tech.meituan.com/2014/08/20/innodb-lock.html) diff --git "a/docs/MySQL/3_\347\264\242\345\274\225.md" "b/docs/MySQL/3_\347\264\242\345\274\225.md" new file mode 100644 index 00000000..b08a6ddf --- /dev/null +++ "b/docs/MySQL/3_\347\264\242\345\274\225.md" @@ -0,0 +1,204 @@ +# 索引 + +## 定义 + +索引是对数据库表中一列或者多列的值进行排序的结构。 + +## 目的 + +数据库索引好比一本书的目录,提高查询效率。但是为表设置索引要付出相应的代价: + +- 增加了数据库的存储空间 +- 在插入和修改时需花费更多的时间(因为索引也要随之变动) + +## 分类 + +### 1. 聚集索引 + +索引项的顺序与表中记录的物理顺序一致。对于聚集索引,叶子结点即存储其真实的数据行,不再有另外单独的数据页。 + +### 2. 非聚集索引 + +表数据存储顺序与索引顺序无关。对于非聚集索引,叶子结点包含索引字段值和数据页**数据行的地址**,其行数量与数据表中行数量一致。 + +注意:一个表中只有一个聚集索引,但是可以有多个非聚集索引。 + +### 3. 唯一索引 + +不允许具有索引值相同的行,但是可以为 NULL,不能有多个 NULL。 + +### 4. 主键索引 + +是唯一索引的特殊类型。数据库表中经常有一列或多列组合,其值唯一标识表中的每一行,该列称为表的主键。 + +在数据库中为表定义主键将自动创建主键索引。 + +## 索引存储结构 + +### B 树 + +对于 m 阶 B 树,有如下性质: + +- 根节点至少有 2 个孩子节点 + +- 树中每个节点最多含有 m 个孩子(m >= 2) + +- 除根节点、叶子节点外其他节点至少有 ceil(m/2) 个孩子 + +- 所有叶子节点都在同一层 + +- 假设每个非终端节点中包含 n 个关键字信息,其中 + + a)Ki(i=1..n)为关键字,being且找顺序升序排序 K(i-1) < Ki + + b)关键字的个数 n 必须满足:ceil(m/2)-1 <= n <= (m-1) + + c)非叶子节点的指针:P[1],P[2],... ,P[M];其中 P[1] 指向关键字小于 K[1] 的子树,P[M] 指向关键字大于 K[M-1] 的子树,其他 P[i] 关键字属于(K[i-1],K[i]) 的子树 + +
+ +### B+ 树 + +B+ 树是 B 树的变体,其定义基本与 B 树相同,除了: + +- 非叶子节点的子树指针和关键字个数相同 + +- 非叶子节点的子树指针 P[i],指向关键字值 [K[i],K[i+1]) 的子树 + +- **非叶子节点仅用来索引,数据都保存在叶子节点** + +- 所有叶子节点均有一个链指针指向下一个叶子节点 + +
+ +数据库系统普遍采用 B+ 树作为索引结构,主要有以下原因: + +- **B+ 树的磁盘读写代价更低** + + 因为非叶子结点只存储索引信息,其内部节点相同 B 树更小,如果把 key 存入同一盘块中,盘块所能包含的 key 越多,一次性读入内存中需要查找的 key 就越多,相对来说磁盘的 I/O次数就减少了。 + + 举个例子:假设磁盘中的一个盘块容纳 16 字节,而一个 key 占 2 字节,一个 key 具体信息指针占 2 字节。一棵 9 阶 B 树(一个结点最多 8 个关键字)的内部结点需要 2 ( (8*(2+2) / 16 = 2)个盘块。B+ 树内部结点只需要 1 (8 * 2 / 16 = 1)个盘块。当需要把内部结点读入内存中的时候,B 树就比 B+ 树多 1 次盘块查找时间。 + +- **B+ 树的查询效率更加稳定** + + 由于非叶子结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。 + +- **B+ 树更有利于对数据库的扫描** + + B+ 树只要遍历叶子结点就可以遍历到所有数据。 + +### HASH + +哈希索引就是采用一定的**哈希算法**,把键值换算成新的哈希值,检索时不需要类似 B+ 树那样从根节点到叶子节点逐级查找,只需一次哈希算法即可**立刻定位到相应位置,速度非常快**。 + +哈希索引底层的数据结构是哈希表,能以 O(1) 时间进行查找,但是失去了有序性;因此在绝大多数需求为**单条记录查询**的时候,可以选择哈希索引,查询性能最快。 + +哈希索引的不足: + +- 无法用于排序与分组 +- 只支持**精确查找**,无法用于部分查找和范围查找 +- 不能避免全表扫描 +- 遇到大量 Hash 冲突的情况效率会大大降低 + + + +## 索引的物理存储 + +MySQL 索引使用的是 B 树中的 B+ 树,但索引是在存储引擎层实现的,而不是在服务器层实现的,所以不同存储引擎具有不同的索引类型和实现。 + +### MyISAM 索引存储机制 + +MyISAM 引擎使用 B+ 树作索引结构,**叶子节点的 data 域存放的是数据记录的地址**,所有索引均是非聚集索引。 + +
+ +上图是一个 MyISAM 表的主索引(Primary key)示意图。 + +假设该表一共有三列,以 Col1 为主键。MyISAM 的索引文件仅仅保存数据记录的地址。 + +在 MyISAM 中,主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求 key 是唯一的,而辅助索引的 **key 可以重复**。如果在 Col2 上建立一个辅助索引,则该辅助索引的结构如下: + +
+ +同样也是一棵 B+ 树,data 域保存数据记录的地址。 + +MyISAM 中首先按照 B+ 树搜索算法搜索索引,如果指定的 key 存在,则取出其 data 域的值,然后以 data 域的值为地址,读取相应数据记录。 MyISAM 的索引方式也叫做**非聚集索引(稀疏索引)**(索引和数据是分开存储的)。 + +### InnoDB 索引存储机制 + +InnoDB 也使用 B+ 树作为索引结构。有且仅有一个聚集索引,和多个非聚集索引。 + +InnoDB 的数据文件本身就是索引文件。MyISAM 索引文件和数据文件是分离的,索引文件仅保存数据记录的地址。而在 InnoDB 中,表数据文件本身就是按 B+ 树组织的一个索引结构,这棵树的**叶子节点 data 域保存了完整的数据记录**。这个索引的 key 是数据表的主键,因此 **InnoDB 表数据文件本身就是主索引**。 + +
+ +上图是 InnoDB 主索引(同时也是数据文件)的示意图。可以看到叶子节点包含了完整的数据记录。 + +这种索引叫做**聚集索引(密集索引)**(索引和数据保存在同一文件中): + +- 若一个主键被定义,该主键作为聚集索引; +- 若没有主键定义,该表的第一个唯一非空索引作为聚集索引; +- 若均不满足,则会生成一个隐藏的主键( MySQL 自动为 InnoDB 表生成一个隐含字段作为主键,这个字段是递增的,长度为 6 个字节)。 + +与 MyISAM 索引的不同是 **InnoDB 的辅助索引 data 域存储相应记录主键的值**而不是地址。例如,定义在 Col3 上的一个辅助索引: + +
+ + + +聚集索引这种实现方式使得按主键的搜索十分高效,但是辅助索引搜索需要检索 2 遍索引: + +首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。 + +注意 InnoDB 索引机制中: + +- 不建议使用过长的字段作为主键,因为所有辅助索引都引用主索引,过长的主索引会令辅助索引变得过大。 + +- 不建议用非单调的字段作为主键,因为 InnoDB 数据文件本身是一棵 B+ 树,非单调的主键会造成在插入新记录时数据文件为了维持 B+ 树的特性而频繁的分裂调整,十分低效。 + + 使用自增字段作为主键则是一个很好的选择。 + + + +## 索引的优点 + +- 大大减少了服务器需要扫描的数据行数。 +- 帮助服务器避免进行排序和分组,以及避免创建临时表(B+Tree 索引是有序的,可以用于 ORDER BY 和 GROUP BY 操作。临时表主要是在排序和分组过程中创建,不需要排序和分组,也就不需要创建临时表)。 +- 将随机 I/O 变为顺序 I/O(B+Tree 索引是有序的,会将相邻的数据都存储在一起)。 + +## 建索引的原则 + +- 最左前缀匹配原则 + + MySQL 会一直向右匹配知道遇到范围查询(>、<、between、like)就停止匹配。比如 `a=3 and b=4 and c>5 and d=6`,如果建立 (a,b,c,d) 顺序的索引,d 就是用不到索引的,如果建立(a,b,d,c) 的索引则都可以用到,并且 a,b,d 的顺序可以任意调整。 + +- = 和 in 可以乱序 + + 比如 `a = 1 and b = 2 and c = 3`建立 (a,b,c) 索引可以任意顺序,MySQL 的查询优惠器可进行优化。 + +- 尽量选择**选择度高**的列建索引 + + ```sql + # 选择度计算 + SELECT COUNT(DISTINCT staff_id)/COUNT(*) AS staff_id_selectivity, + COUNT(DISTINCT customer_id)/COUNT(*) AS customer_id_selectivity, + COUNT(*) + FROM payment; + ``` + +- 使用 like 进行模糊查询时,如果已经建立索引,第一个位置不要使用 '%',否则索引会失效。 +- 当检索性能远远大于检索性能时,不应该建立索引。 + + + +## 索引失效 + +- 最左前缀匹配原则,遇到范围查询 + +- like 模糊查询,第一个位置使用 '%' + +- 没有查询条件 + +- 表比较小时,全表扫描速度比索引速度快时,索引失效 + + (由于索引扫描后要利用索引中的指针去逐一访问记录,假设每个记录都使用索引访问,则**读取磁盘的次数**是查询包含的记录数T,而如果表扫描则读取磁盘的次数是存储记录的块数B,如果T>B 的话索引就没有优势了。) \ No newline at end of file diff --git "a/docs/MySQL/4_MySQL\346\236\266\346\236\204.md" "b/docs/MySQL/4_MySQL\346\236\266\346\236\204.md" new file mode 100644 index 00000000..38a768d9 --- /dev/null +++ "b/docs/MySQL/4_MySQL\346\236\266\346\236\204.md" @@ -0,0 +1,199 @@ +# MySQL 架构 + +## MySQL 基本架构 + +简单来说 MySQL 主要分为 **Server 层**和**存储引擎层**: + +- Server 层 + + 主要包括连接器、查询缓存、分析器、优化器、执行器等,所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图,函数等,还有一个通用的日志模块 binglog 日志模块。 + +- 存储引擎层 + + 主要负责数据的存储和读取,采用可以替换的插件式架构,支持 InnoDB、MyISAM、Memory 等多个存储引擎,其中 InnoDB 引擎有自有的日志模块 redolog 模块。**现在最常用的存储引擎是 InnoDB,它从 MySQL 5.5.5 版本开始就被当做默认存储引擎了**。 + +
+ + + +## Server 层基本组件介绍 + +### 1. 连接器 + +连接器主要和身份认证和权限相关的功能相关,就好比一个级别很高的门卫一样。 + +主要负责用户登录数据库,进行用户的身份认证,包括校验账户密码,权限等操作,如果用户账户密码已通过,连接器会到权限表中查询该用户的所有权限,之后在这个连接里的权限逻辑判断都是会依赖此时读取到的权限数据,也就是说,后续只要这个连接不断开,即时管理员修改了该用户的权限,该用户也是不受影响的。 + +### 2. 查询缓存 + +MySQL 8.0 版本后移除。 + +查询缓存主要用来缓存我们所执行的 SELECT 语句以及该语句的结果集。 + +连接建立后,执行查询语句的时候,会先查询缓存,MySQL 会先校验这个 sql 是否执行过,以 Key-Value 的形式缓存在内存中,Key 是查询预计,Value 是结果集。如果缓存 key 被命中,就会直接返回给客户端,如果没有命中,就会执行后续的操作,完成后也会把结果缓存起来,方便下一次调用。当然在真正执行缓存查询的时候还是会校验用户的权限,是否有该表的查询条件。 + +MySQL 查询不建议使用缓存,因为查询缓存失效在实际业务场景中可能会非常频繁,假如你对一个表更新的话,这个表上的所有的查询缓存都会被清空。对于不经常更新的数据来说,使用缓存还是可以的。 + +所以,一般在大多数情况下我们都是不推荐去使用查询缓存的。 + +MySQL 8.0 版本后删除了缓存的功能,官方也是认为该功能在实际的应用场景比较少,所以干脆直接删掉了。 + +### 3. 分析器 + +MySQL 没有命中缓存,那么就会进入分析器,分析器主要是用来分析 SQL 语句是来干嘛的,分析器也会分为几步: + +**第一步,词法分析**,一条 SQL 语句有多个字符串组成,首先要提取关键字,比如 select,提出查询的表,提出字段名,提出查询条件等等。做完这些操作后,就会进入第二步。 + +**第二步,语法分析**,主要就是判断你输入的 sql 是否正确,是否符合 MySQL 的语法。 + +完成这 2 步之后,MySQL 就准备开始执行了,但是如何执行,怎么执行是最好的结果呢?这个时候就需要优化器上场了。 + +### 4. 优化器 + +优化器的作用就是它认为的最优的执行方案去执行(有时候可能也不是最优,这篇文章涉及对这部分知识的深入讲解),比如多个索引的时候该如何选择索引,多表查询的时候如何选择关联顺序等。 + +可以说,经过了优化器之后可以说这个语句具体该如何执行就已经定下来。 + +### 5. 执行器 + +当选择了执行方案后,MySQL 就准备开始执行了,首先执行前会校验该用户有没有权限,如果没有权限,就会返回错误信息,如果有权限,就会去调用引擎的接口,返回接口执行的结果。 + + + +## 存储引擎 + +### InnoDB + +**是 MySQL 默认的事务型存储引擎,只有在需要它不支持的特性时,才考虑使用其它存储引擎**。 + +实现了四个标准的隔离级别,默认级别是可重复读(REPEATABLE READ)。在可重复读隔离级别下,通过多版本并发控制(MVCC)+ 间隙锁(Next-Key Locking)防止幻影读。 + +主索引是聚簇索引,在索引中保存了数据,从而避免直接读取磁盘,因此对查询性能有很大的提升。 + +内部做了很多优化,包括从磁盘读取数据时采用的可预测性读、能够加快读操作并且自动创建的自适应哈希索引、能够加速插入操作的插入缓冲区等。 + +支持真正的在线热备份。其它存储引擎不支持在线热备份,要获取一致性视图需要停止对所有表的写入,而在读写混合场景中,停止写入可能也意味着停止读取。 + +### MyISAM + +设计简单,数据以紧密格式存储。对于只读数据,或者表比较小、可以容忍修复操作,则依然可以使用它。 + +提供了大量的特性,包括压缩表、空间数据索引等。 + +不支持事务。 + +不支持行级锁,只能对整张表加锁,读取时会对需要读到的所有表加共享锁,写入时则对表加排它锁。但在表有读取操作的同时,也可以往表中插入新的记录,这被称为并发插入(CONCURRENT INSERT)。 + +可以手工或者自动执行检查和修复操作,但是和事务恢复以及崩溃恢复不同,可能导致一些数据丢失,而且修复操作是非常慢的。 + +如果指定了 DELAY_KEY_WRITE 选项,在每次修改执行完成时,不会立即将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区,只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入磁盘。这种方式可以极大的提升写入性能,但是在数据库或者主机崩溃时会造成索引损坏,需要执行修复操作。 + +### 比较 + +| 区别 | MyISAM | InnoDB | +| :------: | :----------------------------------------------------------: | :----------------------------------------------------------: | +| 事务 | 不支持事务
强调的是性能,每次查询具有原子性,其执行速度比InnoDB更快 | 支持事务 | +| 锁的粒度 | 只支持表锁 | 既支持表锁,又支持行锁 | +| 索引结构 | 非聚集索引 | 聚集索引 | +| 主键 | 允许没有任何索引和主键的表存在,索引都是保存行的地址 | 如果没有设定主键或者非空唯一索引,
会自动生成一个6字节的主键(用户不可见),
数据是主索引的一部分,附加索引保存的是主索引的值 | +| 外键 | 不支持 | 支持 | + + + +为什么MyISAM会比Innodb 的查询速度快? + +- 数据块,InnoDB要缓存,MyISAM只缓存索引块, 这中间还有换进换出的减少 +- InnoDB寻址要映射到块,再到行,MyISAM 记录的直接是文件的OFFSET,定位比InnoDB要快 +- InnoDB还需要维护MVCC一致;虽然你的场景没有,但他还是需要去检查和维护 + + + +## 语句执行分析 + +一条 sql 语句是如何执行的呢?其实我们的 sql 可以分为两种: + +- 一种是查询 +- 一种是更新(增加,更新,删除) + +### 查询语句分析 + +```sql +SELECT * FROM +tb_student +WHERE age='18' and name=' 张三 '; +``` + +执行流程如下: + +- 先检查该语句是否有**权限**,如果没有权限,直接返回错误信息; + + 如果有权限,在 MySQL8.0 版本以前,会**先查询缓存**,以这条 sql 语句为 key 在内存中查询是否有结果,如果有直接缓存,如果没有,执行下一步 + +- 通过**分析器**进行词法分析,提取 sql 语句的关键元素,比如提取上面这个语句是查询 select,提取需要查询的表名为 tb_student,需要查询所有的列,查询条件是这个表的 id='1'; + + 判断这个 sql 语句是否有语法错误,比如关键词是否正确等等,如果检查没问题就执行下一步。 + +- 接下来就是**优化器**进行确定执行方案,上面的 sql 语句,可以有两种执行方案: + + 第一种方案:先查询学生表中姓名为“张三”的学生,然后判断是否年龄是 18 + + 第二种方案:先查询学生表中年龄 18 岁的学生,然后再查询姓名为“张三” + + 那么优化器根据自己的优化算法进行选择执行效率最好的一个方案(只是优化器认为,有时候不一定最好)。那么确认了执行计划后就准备开始执行了。 + +- 进行**权限校验**,如果没有权限就会返回错误信息, + + 如果有权限就会调用数据库引擎接口,返回引擎的执行结果。 + + +### 更新语句分析 + +```sql +UPDATE tb_student SET age='19' +WHERE name=' 张三 '; +``` + +本条语句也基本上会沿着上一个查询的流程走,只不过执行更新的时候肯定要记录日志,这就会引入日志模块了,MySQL 自带的日志模块式 **binlog(归档日志)** ,所有的存储引擎都可以使用,我们常用的 InnoDB 引擎还自带了一个日志模块 **redo log(重做日志)**,我们就以 InnoDB 模式下来探讨这个语句的执行流程。流程如下: + +- 先查询到张三这一条数据,如果有缓存,也是会用到缓存 +- 然后拿到查询的语句,把 age 改为 19,然后调用引擎 API 接口,写入这一行数据,InnoDB 引擎把数据保存在内存中,同时记录 redo log,此时 redo log 进入 prepare 状态,然后告诉执行器,执行完成了,随时可以提交 +- 执行器收到通知后记录 binlog,然后调用引擎接口,提交 redo log 为 commit 状态 +- 更新完成 + +**为什么要用两个日志模块,用一个日志模块不行吗?** + +这是因为最开始 MySQL 并没与 InnoDB 引擎( InnoDB 引擎是其他公司以插件形式插入 MySQL 的) ,MySQL 自带的引擎是 MyISAM,但是我们知道 redo log 是 InnoDB 引擎特有的,其他存储引擎都没有,这就导致会没有 crash-safe 的能力(crash-safe 的能力即使数据库发生异常重启,之前提交的记录都不会丢失),binlog 日志只能用来归档。 + +并不是说只用一个日志模块不可以,只是 InnoDB 引擎就是通过 redo log 来支持事务的。那么,又会有同学问,我用两个日志模块,但是不要这么复杂行不行,为什么 redo log 要引入 prepare 预提交状态?这里我们用反证法来说明下为什么要这么做? + +- **先写 redo log 直接提交,然后写 binlog**,假设写完 redo log 后,机器挂了,binlog 日志没有被写入,那么机器重启后,这台机器会通过 redo log 恢复数据,但是这个时候 bingog 并没有记录该数据,后续进行机器备份的时候,就会丢失这一条数据,同时主从同步也会丢失这一条数据。 +- **先写 binlog,然后写 redo log**,假设写完了 binlog,机器异常重启了,由于没有 redo log,本机是无法恢复这一条记录的,但是 binlog 又有记录,那么和上面同样的道理,就会产生数据不一致的情况。 + +如果采用 redo log 两阶段提交的方式就不一样了,写完 binglog 后,然后再提交 redo log 就会防止出现上述的问题,从而保证了数据的一致性。那么问题来了,有没有一个极端的情况呢?假设 redo log 处于预提交状态,binglog 也已经写完了,这个时候发生了异常重启会怎么样呢? 这个就要依赖于 MySQL 的处理机制了,MySQL 的处理过程如下: + +- 判断 redo log 是否完整,如果判断是完整的,就立即提交。 +- 如果 redo log 只是预提交但不是 commit 状态,这个时候就会去判断 binlog 是否完整,如果完整就提交 redo log, 不完整就回滚事务。 + +这样就解决了数据一致性的问题。 + +### 总结 + +查询语句过程: + +1. 分析器,进行权限校验 +2. 查询缓存 +3. 分析器进行词汇分析,语法分析 +4. 优化器生成执行计划 +5. 权限校验 +6. 执行器 +7. 引擎 + +更新语句过程: + +1. 分析器 +2. 权限校验 +3. 执行器 +4. 引擎 +5. redo log prepare +6. binlog +7. redo log commit \ No newline at end of file diff --git "a/docs/MySQL/5_MySQL\344\274\230\345\214\226.md" "b/docs/MySQL/5_MySQL\344\274\230\345\214\226.md" new file mode 100644 index 00000000..d470d532 --- /dev/null +++ "b/docs/MySQL/5_MySQL\344\274\230\345\214\226.md" @@ -0,0 +1,162 @@ +# MySQL 调优 + +## SQL 语句优化 + +### 查询优化 + +#### 1. Explain 分析 + +- 先开启慢查询日志 + +```sql +set global slow_query_log = on # 开启慢查询日志,默认是关闭的 +set global long_qurey_time=0.5 # 设置慢查询时间阈值,单位:秒 +``` + +- 定位慢查询语句 +- explain 进行分析,相应字段: + - id:id 值越大,越先执行。无子查询时,id=1 + - type: + - NULL,执行时甚至不用访问表或索引 + - eq_ref,使用的是唯一索引 + - ref,使用的是非唯一索引或者是唯一索引的前缀 + - index,类似全表扫描,按照索引次序扫描表 + - rows:表扫描的行数 + - key:实际用到的索引,为空表没有用到索引 + - extra:十分重要的额外信息 + - using filesort + - using temporary + +#### 2. 优化数据访问 + +**减少请求的数据量** + +- 只返回必要的列:最好不要使用 SELECT * 语句。 +- 只返回必要的行:使用 LIMIT 语句来限制返回的数据。 +- 缓存重复查询的数据:使用缓存可以避免在数据库中进行查询,特别在要查询的数据经常被重复查询时,缓存带来的查询性能提升将会是非常明显的。 + +**减少服务器端扫描的行数** + +最有效的方式是使用索引来覆盖查询。 + +#### 3. 重构查询方式 + +**切分大查询** + +一个大查询如果一次性执行的话,可能一次锁住很多数据、占满整个事务日志、耗尽系统资源、阻塞很多小的但重要的查询。 + +**分解大连接查询** + +将一个大连接查询分解成对每一个表进行一次单表查询,然后在应用程序中进行关联,这样做的好处有: + +- 让缓存更高效。对于连接查询,如果其中一个表发生变化,那么整个查询缓存就无法使用。而分解后的多个查询,即使其中一个表发生变化,对其它表的查询缓存依然可以使用。 + +- 分解成多个单表查询,这些单表查询的缓存结果更可能被其它查询使用到,从而减少冗余记录的查询。 + +- 减少锁竞争; + +- 在应用层进行连接,可以更容易对数据库进行拆分,从而更容易做到高性能和可伸缩。 + +- 查询本身效率也可能会有所提升。例如下面的例子中,使用 IN() 代替连接查询,可以让 MySQL 按照 ID 顺序进行查询,这可能比随机的连接要更高效。 + + ```sql + SELECT * FROM tag + JOIN tag_post ON tag_post.tag_id=tag.id + JOIN post ON tag_post.post_id=post.id + WHERE tag.tag='mysql'; + ``` + + ```sql + SELECT * FROM tag WHERE tag='mysql'; + SELECT * FROM tag_post WHERE tag_id=1234; + SELECT * FROM post WHERE post.id IN (123,456,567,9098,8904); + ``` + +### limit 优化 + +```sql +limit m,n +-- m 表示从第 (m+1) 条记录开始检索 +-- n 表示取出 n 条数据 +``` + +当一个数据库过于庞大,SQL 查询语句会比较慢。 + +```sql +SELECT * +FROM product +LIMIT 1000000,20 +``` + +优化措施: + +#### 1. 使用覆盖索引 + +```sql +SELECT id +FROM product +LIMIT 1000000,20 +# id 是主键 +``` + +#### 2. 使用 id 进行过滤 + +```sql +SELECT * +FROM product +WHERE id>=(SELECT id FROM product LIMIT 1000000,1) +LIMIT 20; +``` + +#### 3. 使用 jion + +```sql +SELECT * +FROM product a +JION (SELECT id FROM product LIMIT 1000000,20) b +ON a.id=b.id; +``` + + + +## 数据库设计优化 + +- 可以适当地违反第三范式(3NF),减少 join 操作 + +- 设计一些中间表 + +- 对表格进行垂直拆分 + + 不常用的字段单独放在一个表中;常用的字段单独放在一个表中;一些大字段单独放在一个表中。 + +- 对表格进行水平拆分 + + 水平切分是将同一个表中的记录拆分到多个结构相同的表中。 + + 常用的水平拆分策略: + + - 哈希取模:hash(key) % N + - 范围:可以是 ID 范围也可以是时间范围 + +- 主键尽量用自增 + + + +## 数据库参数配置 + +最主要是调整内存:show status 确定内存大小 + +- 关闭一些不必要的二进制文件 +- 增加 MySQL 的最大连接数 +- 删除大量数据行后,使用 OPTIMIZE TABLE 命令进行**碎片整理** + + + +## 切分 + + + + + + + diff --git "a/docs/MySQL/6_MySQL\346\225\260\346\215\256\347\261\273\345\236\213.md" "b/docs/MySQL/6_MySQL\346\225\260\346\215\256\347\261\273\345\236\213.md" new file mode 100644 index 00000000..b15db3d3 --- /dev/null +++ "b/docs/MySQL/6_MySQL\346\225\260\346\215\256\347\261\273\345\236\213.md" @@ -0,0 +1,73 @@ +# 数据类型 + +## 整型 + +TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT 分别使用 8, 16, 24, 32, 64 位存储空间,一般情况下越小的列越好。 + +INT(11) 中的数字只是规定了交互工具显示字符的个数,对于存储和计算来说是没有意义的。 + +## 浮点数 + +FLOAT 和 DOUBLE 为浮点类型,DECIMAL 为高精度小数类型。CPU 原生支持浮点运算,但是不支持 DECIMAl 类型的计算,因此 DECIMAL 的计算比浮点类型需要更高的代价。 + +FLOAT、DOUBLE 和 DECIMAL 都可以指定列宽,例如 DECIMAL(18, 9) 表示总共 18 位,取 9 位存储小数部分,剩下 9 位存储整数部分。 + +## 字符串 + +主要有 CHAR 和 VARCHAR 两种类型,一种是定长的,一种是变长的。 + +VARCHAR 这种变长类型能够节省空间,因为只需要存储必要的内容。但是在执行 UPDATE 时可能会使行变得比原来长,当超出一个页所能容纳的大小时,就要执行额外的操作。MyISAM 会将行拆成不同的片段存储,而 InnoDB 则需要分裂页来使行放进页内。 + +在进行存储和检索时,会保留 VARCHAR 末尾的空格,而会删除 CHAR 末尾的空格。 + +## 时间和日期 + +MySQL 提供了两种相似的日期时间类型:DATETIME 和 TIMESTAMP。 + +### DATETIME + +能够保存从 1000 年到 9999 年的日期和时间,精度为秒,使用 8 字节的存储空间。 + +它与时区无关。 + +默认情况下,MySQL 以一种可排序的、无歧义的格式显示 DATETIME 值,例如“2008-01-16 22:37:08”,这是 ANSI 标准定义的日期和时间表示方法。 + +### TIMESTAMP + +和 UNIX 时间戳相同,保存从 1970 年 1 月 1 日午夜(格林威治时间)以来的秒数,使用 4 个字节,只能表示从 1970 年到 2038 年。 + +它和时区有关,也就是说一个时间戳在不同的时区所代表的具体时间是不同的。 + +MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期,并提供了 UNIX_TIMESTAMP() 函数把日期转换为 UNIX 时间戳。 + +默认情况下,如果插入时没有指定 TIMESTAMP 列的值,会将这个值设置为当前时间。 + +**应该尽量使用 TIMESTAMP**: + +- DATETIME 类型与时区无关,即没有时区信息。当更换时区后,比如服务器更换地址或者更换客户端连接时区设置,就会导致从数据库中读出的时间错误。 + + TIMESTAMP 类型和时区有关。TIMESTAMP 类型字段的值会随着服务器时区的变化而变化,自动换算成相应的时间 + +- TIMESTAMP 只需要使用 4 个字节的存储空间,但 DATETIME 需要耗费 8 个字节的存储空间。但是,这样同样造成了一个问题,TIMESTAMP 表示的时间范围更小。 + + - DATETIME :`1000-01-01 00:00:00 ~ 9999-12-31 23:59:59` + - TIMESTAMP: `1970-01-01 00:00:00 ~ 2037-12-31 23:59:59` + + 注意 5.6.4 之后的 MySQL 多出了一个需要 0 ~ 3 字节的小数位。DATETIME 和 TIMESTAMP 会有几种不同的存储空间占用。 + +### 数值型时间戳 + +时间戳的定义是从一个基准时间 [ 1970-1-1 00:00:00 +0:00 ] 开始算起,用整数表示,以秒计时,随着时间的流逝这个时间整数不断增加。 + +一个数值,就可以完美地表示时间了,而且这个数值是一个绝对数值,即无论的身处地球的任何角落,这个表示时间的时间戳,都是一样的,生成的数值都是一样的,并且没有时区的概念。 + +MySQL 中可以使用 int 或者 bigint 类型的时间戳来表示时间。 + +小结: + +| 日期类型 | 存储空间 | 日期格式 | 日期范围 | 时区问题 | +| --------- | -------- | ------------------- | ---------------------------------------------- | -------- | +| DATETIME | 8 字节 | yyyy-MM-dd HH:mm:ss | 1000-01-01 00:00:00 ~
9999-12-31 23:59:59 | 存在 | +| TIMESTAMP | 4 字节 | yyyy-MM-dd HH:mm:ss | 1970-01-01 00:00:00 ~
2037-12-31 23:59:59 | 不存在 | +| 时间戳 | 4 字节 | 数值 | 1970-01-01 00:00:00 之后的时间 | 不存在 | + diff --git "a/docs/MySQL/6_\350\241\245\345\205\205\347\237\245\350\257\206.md" "b/docs/MySQL/6_\350\241\245\345\205\205\347\237\245\350\257\206.md" new file mode 100644 index 00000000..ae91f0e1 --- /dev/null +++ "b/docs/MySQL/6_\350\241\245\345\205\205\347\237\245\350\257\206.md" @@ -0,0 +1,157 @@ +# 补充知识 + +## inner join、outer join 和 full jion + +### 1. inner join(内连接) + +返回两表中**连接字段**相等的记录。 + +### 2. outer join(外连接) + +- left join(左外连接) + + 返回左表中所有记录和右边中连接字段相等的记录。 + +- right join(右外连接) + + 返回右表中所有记录和左边中连接字段相等的记录。 + +注意: + +```sql +-- Stu 表(10 条记录) +|-1-|-A-| +|-2-|-B-| +|-3-|-C-| +|-4-|-D-| +|-5-|-E-| +|-6-|-F-| +|-7-|-G-| +|-8-|-H-| +|-9-|-I-| +|-10-|-J-| + +-- S 表(5 条记录) +|-1-|-B-| +|-2-|-B-| +|-3-|-B-| +|-4-|-B-| +|-5-|-B-| +``` + +```sql +SELECT stu.id,stu.name +FROM Stu stu +LEFT JOIN S s +ON stu.name=s.name; + +# 返回 14 条记录 +# 因为 jion 操作可能会有多条记录 +``` + +### 3. full join(全连接) + +返回两表中的所有记录和连接字段相等的记录。 + + + +## 子查询和 join 查询 + +- 子查询不一定需要 2 个表有关联字段; + + join 查询必须要求有关联字段 + +- 数据量比较大时,使用 join 查询寻性能会更好。(因为子查询走的是笛卡尔积) + +- 子查询只有一条记录; + + join 查询可能会有多条记录 + + 所以将子查询转换为 join 查询时,要注意去重 + + + +## where 和 on + +on 一般与 join 结合使用,使用 join 会产生临时表。 + +- on 是在生成临时表时使用的条件 + +- where 是在生成临时表后使用的条件 + +即 on 在 where 之前执行。 + + + +## where 和 having + +- where + + 是一个约束声明,在查询结果返回前对查询结果进行约束; + + where 后面不可以使用 “聚合函数”。 + +- having + + 是一个过滤声明,在查询结果返回后对查询结果进行过滤; + + where 后面可以使用 “聚合函数”。 + + + +## order by 和 group by + +功能上: + +- order by 是用来排序,分为 desc 和 asc +- group by 是用来进行分组,并且可以和 "聚合函数" 一起使用 + +使用上: + +group by 在 order by 之前使用,即先进行分组,然后再进行排序。 + +## drop、delete 与 truncate + +- drop(丢弃数据): `drop table 表名` ,直接将表都删除掉,在删除表的时候使用。 +- truncate (清除数据) : `truncate table 表名` ,只删除表中的数据,再插入数据的时候自增长 id 又从 1 开始,在清空表中数据的时候使用。 +- delete(删除数据) : `delete from 表名 where 列名=值`,删除某一列的数据,如果不加 where 子句和`truncate table 表名`作用类似。 + +truncate 和不带 where 子句的 delete、以及 drop 都会删除表内的数据,但是 truncate 和 delete 只删除数据不删除表的结构(定义),执行 drop 语句,此表的结构也会删除,也就是执行 drop 之后对应的表不复存在。 + +## 查询顺序 + +select > from > where > group by > having > order by > limit + + + +## 优先级 + +优先级:not > and > or + +可以使用 () 来改变优先级。 + + + +## 数据库备份 + +### 1. 备份方式 + +- 热备份:当数据库进行备份时,数据库的读写操作均不受影响 +- 温备份:当数据库进行备份时,数据库的读操作可以进行,但不能执行写操作 +- 冷备份:当数据库进行备份时,数据库不能进行读写操作 + +### 2. 备份策略 + +- 直接复制数据库文件 + + 针对数据量较小的场景。 + +- mysqldump + 复制 binlog + + 针对数据量适中的场景。mysqldump 对数据库进行完全备份,定期备份 binlog 达到增量备份的效果。 + + mysqldump 实际上就是讲表结构和数据存储在文本文件中,原理:先根据表结构生成 CREATE 语句,然后再将数据转换为 INSERT 语句 + +- ivm2 快照 + 复制 binlog + +注:binlog 即二进制日志,记录对数据发生或者潜在发生更改的 SQL 语句,以二进制形式保存在文件中。 diff --git "a/docs/MySQL/\345\210\207\345\210\206\345\244\215\345\210\266\351\227\256\351\242\230.md" "b/docs/MySQL/\345\210\207\345\210\206\345\244\215\345\210\266\351\227\256\351\242\230.md" new file mode 100644 index 00000000..b8567dd9 --- /dev/null +++ "b/docs/MySQL/\345\210\207\345\210\206\345\244\215\345\210\266\351\227\256\351\242\230.md" @@ -0,0 +1,8 @@ +# 一、切分 + + + + + +# 二、复制 + diff --git "a/docs/Net/1_\346\246\202\350\277\260.md" "b/docs/Net/1_\346\246\202\350\277\260.md" index 8ef6b9a8..bd6c2c66 100644 --- "a/docs/Net/1_\346\246\202\350\277\260.md" +++ "b/docs/Net/1_\346\246\202\350\277\260.md" @@ -1,15 +1,4 @@ - -* [一、概述](#一概述) - * [网络的网络](#网络的网络) - * [ISP](#isp) - * [主机之间的通信方式](#主机之间的通信方式) - * [电路交换与分组交换](#电路交换与分组交换) - * [时延](#时延) - * [计算机网络体系结构](#计算机网络体系结构) - * [计算机网络的性能指标](#计算机网络的性能指标) - - -# 一、概述 +# 概述 ## 网络的网络 diff --git "a/docs/Net/2_\347\211\251\347\220\206\345\261\202.md" "b/docs/Net/2_\347\211\251\347\220\206\345\261\202.md" index 379310d1..bd179c16 100644 --- "a/docs/Net/2_\347\211\251\347\220\206\345\261\202.md" +++ "b/docs/Net/2_\347\211\251\347\220\206\345\261\202.md" @@ -1,4 +1,4 @@ -# 二、物理层 +# 物理层 ## 通信方式 diff --git "a/docs/Net/3_\346\225\260\346\215\256\351\223\276\350\267\257\345\261\202.md" "b/docs/Net/3_\346\225\260\346\215\256\351\223\276\350\267\257\345\261\202.md" index 73a8e277..4b183dd7 100644 --- "a/docs/Net/3_\346\225\260\346\215\256\351\223\276\350\267\257\345\261\202.md" +++ "b/docs/Net/3_\346\225\260\346\215\256\351\223\276\350\267\257\345\261\202.md" @@ -1,4 +1,4 @@ -# 三、数据链路层 +# 数据链路层 ## 基本概念 diff --git "a/docs/Net/4_\347\275\221\347\273\234\345\261\202.md" "b/docs/Net/4_\347\275\221\347\273\234\345\261\202.md" index a5a91662..0acf8fcc 100644 --- "a/docs/Net/4_\347\275\221\347\273\234\345\261\202.md" +++ "b/docs/Net/4_\347\275\221\347\273\234\345\261\202.md" @@ -1,18 +1,4 @@ - -* [四、网络层](#四网络层) - * [概述](#概述) - * [IP 数据报格式](#ip-数据报格式) - * [IP 地址编址方式](#ip-地址编址方式) - * [地址解析协议 ARP](#地址解析协议-arp) - * [网际控制报文协议 ICMP](#网际控制报文协议-icmp) - * [虚拟专用网 VPN](#虚拟专用网-vpn) - * [网络地址转换 NAT](#网络地址转换-nat) - * [路由器的结构](#路由器的结构) - * [路由器分组转发流程](#路由器分组转发流程) - * [路由选择协议](#路由选择协议) - - -# 四、网络层 +# 网络层 ## 概述 diff --git "a/docs/Net/5_\350\277\220\350\276\223\345\261\202.md" "b/docs/Net/5_\350\277\220\350\276\223\345\261\202.md" index cdce3831..0ad33ede 100644 --- "a/docs/Net/5_\350\277\220\350\276\223\345\261\202.md" +++ "b/docs/Net/5_\350\277\220\350\276\223\345\261\202.md" @@ -1,17 +1,4 @@ -, -* [五、运输层](#五运输层) - * [UDP 和 TCP 的特点](#udp-和-tcp-的特点) - * [UDP 首部格式](#udp-首部格式) - * [TCP 首部格式](#tcp-首部格式) - * [TCP 的三次握手](#tcp-的三次握手) - * [TCP 的四次挥手](#tcp-的四次挥手) - * [TCP 可靠传输](#tcp-可靠传输) - * [TCP 滑动窗口](#tcp-滑动窗口) - * [TCP 流量控制](#tcp-流量控制) - * [TCP 拥塞控制](#tcp-拥塞控制) - - -# 五、运输层 +# 运输层 网络层只把分组发送到目的主机,但是真正通信的并不是主机而是主机中的进程。运输层提供了进程间的逻辑通信,运输层向高层用户屏蔽了下面网络层的核心细节,使应用程序看起来像是在两个运输层实体之间有一条端到端的逻辑通信信道。 diff --git "a/docs/Net/6_\345\272\224\347\224\250\345\261\202.md" "b/docs/Net/6_\345\272\224\347\224\250\345\261\202.md" index d7a7c2bd..be4630ba 100644 --- "a/docs/Net/6_\345\272\224\347\224\250\345\261\202.md" +++ "b/docs/Net/6_\345\272\224\347\224\250\345\261\202.md" @@ -1,15 +1,4 @@ - -* [六、应用层](#六应用层) - * [域名系统](#域名系统) - * [文件传送协议](#文件传送协议) - * [动态主机配置协议](#动态主机配置协议) - * [远程登录协议](#远程登录协议) - * [电子邮件协议](#电子邮件协议) - * [常用端口](#常用端口) - * [Web 页面请求过程](#web-页面请求过程) - - -# 六、应用层 +# 应用层 ## 域名系统 @@ -89,7 +78,7 @@ IMAP 协议中客户端和服务器上的邮件保持同步,如果不手动删 ## 常用端口 |应用| 应用层协议 | 端口号 | 运输层协议 | 备注 | -| :---: | :--: | :--: | :--: | :--: +| :---: | :--: | :--: | :--: | :--:| | 域名解析 | DNS | 53 | UDP/TCP | 长度超过 512 字节时使用 TCP | | 动态主机配置协议 | DHCP | 67/68 | UDP | | | 简单网络管理协议 | SNMP | 161/162 | UDP | | diff --git "a/docs/OO/11_\346\212\275\350\261\241\347\261\273\345\222\214\346\216\245\345\217\243.md" "b/docs/OO/11_\346\212\275\350\261\241\347\261\273\345\222\214\346\216\245\345\217\243.md" new file mode 100644 index 00000000..cf326b38 --- /dev/null +++ "b/docs/OO/11_\346\212\275\350\261\241\347\261\273\345\222\214\346\216\245\345\217\243.md" @@ -0,0 +1,84 @@ +# 抽象类和接口 + +## 抽象类 + +### 抽象类特点 + +- 抽象类和抽象方法必须用 abstract 关键字修饰 + +- 抽象类不一定有抽象方法,有抽象方法的一定是抽象类 + +- 抽象类不能实例化 + +- 抽象类的子类: + + 要么是抽象类;要么重写抽象类的所有抽象方法 + +### 抽象类成员特点 + +- 成员变量:可以是常量,也可以是变量 +- 构造方法:有构造方法,但是不能实例化(作用:**用于子类访问父类数据的初始化**) +- 成员方法:可以有抽象方法和非抽象方法 + +注意: + +- abstract 与 private、final 冲突; + + (abstract 修饰的类,必须被继承;abstract 修饰的方法,应被重写。 + + 但是 final 修饰的类、属性、方法不可被更改,所以final 修饰的方法不可以被重写。 + + private 修饰的类只能是内部类,private 修饰的属性、方法只能在本类中调用。) + + +- abstract 与 static 放一起是没有意义的。 + + (static 修饰的方法是静态的,可以直接被类所调用。 + + 但是 abstract 修饰的方法抽象方法,没有方法体不能够被直接调用。) + + + +## 接口 + +### 接口特点 + +- 使用 interface + +- 类实现接口使用 implements + +- 接口不能实例化 + +- 接口的子类: + + 要么是抽象类;要么实现接口中所有抽象方法 + +### 接口成员特点 + +- 成员变量:只能是常量,默认是 public static final +- 构造方法:无 +- 成员方法:都是抽象方法,默认是 public abstract + + + +## 抽象类和接口的区别 + +- 成员区别 + + 抽象类:常量/变量;有构造方法;有抽象方法和非抽象方法 + + 接口:常量;无构造方法;只有抽象方法 + +- 关系区别 + + 类与类:继承关系(单继承) + + 类与接口:实现关系(可以多实现) + + 接口与接口:继承关系(可以多继承) + +- 设计理念区别 + + 抽象类被继承,体现 "is a " 理念 + + 接口被实现,体现 "like a" 理念 \ No newline at end of file diff --git "a/docs/OO/00\346\246\202\350\277\260.md" "b/docs/OO/1_\346\246\202\350\277\260.md" similarity index 80% rename from "docs/OO/00\346\246\202\350\277\260.md" rename to "docs/OO/1_\346\246\202\350\277\260.md" index 37ddb344..33223ded 100644 --- "a/docs/OO/00\346\246\202\350\277\260.md" +++ "b/docs/OO/1_\346\246\202\350\277\260.md" @@ -1,8 +1,4 @@ - -* [一、概述](#一概述) - - -# 一、概述 +# 概述 设计模式是解决问题的方案,学习现有的设计模式可以做到经验复用。 diff --git "a/docs/OO/01\345\210\233\345\273\272\345\236\213.md" "b/docs/OO/2_\345\210\233\345\273\272\345\236\213.md" similarity index 92% rename from "docs/OO/01\345\210\233\345\273\272\345\236\213.md" rename to "docs/OO/2_\345\210\233\345\273\272\345\236\213.md" index 7ea2950f..27f2ac57 100644 --- "a/docs/OO/01\345\210\233\345\273\272\345\236\213.md" +++ "b/docs/OO/2_\345\210\233\345\273\272\345\236\213.md" @@ -1,16 +1,4 @@ - -* [二、创建型](#二创建型) - * [1. *单例(Singleton)](#1-单例singleton) - * [2. *简单工厂(Simple Factory)](#2-简单工厂simple-factory) - * [3. *工厂方法(Factory Method)](#3-工厂方法factory-method) - * [4. *抽象工厂(Abstract Factory)](#4-抽象工厂abstract-factory) - * [5. 生成器(Builder)](#5-生成器builder) - * [6. 原型模式(Prototype)](#6-原型模式prototype) - - -> *号表示面试常见 - -# 二、创建型 +# 创建型 ## 1. 单例(Singleton) @@ -24,7 +12,7 @@ 私有构造函数保证了不能通过构造函数来创建对象实例,只能通过公有静态函数返回唯一的私有静态变量。 -

+

### Implementation @@ -291,7 +279,7 @@ public class Singleton { 客户类往往有多个,如果不使用简单工厂,那么所有的客户类都要知道所有子类的细节。而且一旦子类发生改变,例如增加子类,那么所有的客户类都要进行修改。 -

+

### Implementation @@ -388,7 +376,7 @@ public class Client { 下图中,Factory 有一个 doSomething() 方法,这个方法需要用到一个产品对象,这个产品对象由 factoryMethod() 方法创建。该方法是抽象的,需要由子类去实现。 -

+

### Implementation @@ -452,7 +440,7 @@ public class ConcreteFactory2 extends Factory { 从高层次来看,抽象工厂使用了组合,即 Cilent 组合了 AbstractFactory,而工厂方法模式使用了继承。 -

+

### Implementation @@ -548,7 +536,7 @@ public class Client { 当然,光有指导者是不够的,必须要有能具体实现每步的对象,在生成器模式中称这些实现对象为**生成器**。 这样一来,**指导者就是可以重用的构建过程,而生成器是可以被切换的具体实现**。 -

+

### Implementation1 @@ -758,7 +746,7 @@ public class Client { 生成器的调用顺序: -

+

### Implementation2 @@ -848,49 +836,59 @@ abcdefghijklmnopqrstuvwxyz ### Class Diagram -

+

### Implementation ```java -public abstract class Prototype { - abstract Prototype myClone(); +public class Prototype implements Cloneable{ + + @Override + protected Prototype clone() throws CloneNotSupportedException { + Prototype p=null; + try{ + p=(Prototype)super.clone(); + }catch (ClassCastException e){ + e.printStackTrace(); + } + return p; + } } ``` ```java -public class ConcretePrototype extends Prototype { +public class ConcretePrototype extends Prototype{ - private String filed; + private String field; - public ConcretePrototype(String filed) { - this.filed = filed; - } - - @Override - Prototype myClone() { - return new ConcretePrototype(filed); + public ConcretePrototype(String filed){ + this.field=filed; } @Override public String toString() { - return filed; + return field; } } ``` ```java public class Client { - public static void main(String[] args) { - Prototype prototype = new ConcretePrototype("abc"); - Prototype clone = prototype.myClone(); - System.out.println(clone.toString()); + public static void main(String[] args) throws CloneNotSupportedException { + Prototype obj1=new ConcretePrototype("abc"); + Prototype obj2=obj1.clone(); + Prototype obj3=obj1.clone(); + System.out.println(obj1); + System.out.println(obj2); + System.out.println(obj3); } } ``` ```html abc +abc +abc ``` ### JDK diff --git "a/docs/OO/02\350\241\214\344\270\272\345\236\213.md" "b/docs/OO/3_\350\241\214\344\270\272\345\236\213.md" similarity index 92% rename from "docs/OO/02\350\241\214\344\270\272\345\236\213.md" rename to "docs/OO/3_\350\241\214\344\270\272\345\236\213.md" index 4aac9abc..e91dd3cc 100644 --- "a/docs/OO/02\350\241\214\344\270\272\345\236\213.md" +++ "b/docs/OO/3_\350\241\214\344\270\272\345\236\213.md" @@ -1,20 +1,4 @@ - -* [三、行为型](#三行为型) - * [1. *责任链(Chain Of Responsibility)](#1-责任链chain-of-responsibility) - * [2. 命令(Command)](#2-命令command) - * [3. 解释器(Interpreter)](#3-解释器interpreter) - * [4. 迭代器(Iterator)](#4-迭代器iterator) - * [5. 中介者(Mediator)](#5-中介者mediator) - * [6. 备忘录(Memento)](#6-备忘录memento) - * [7. *观察者(Observer)](#7-观察者observer) - * [8. 状态(State)](#8-状态state) - * [9. 策略(Strategy)](#9-策略strategy) - * [10. 模板方法(Template Method)](#10-模板方法template-method) - * [11. 访问者(Visitor)](#11-访问者visitor) - * [12. 空对象(Null)](#12-空对象null) - - -# 三、行为型 +# 行为型 ## 1. 责任链(Chain Of Responsibility) @@ -27,7 +11,7 @@ - Handler:定义处理请求的接口,并且实现后继链(successor) -

+

### Implementation @@ -174,7 +158,7 @@ request-2 is handle by ConcreteHandler2 - Invoker:通过它来调用命令 - Client:可以设置命令与命令的接收者 -

+

### Implementation1 ```java @@ -248,7 +232,7 @@ public class Client { 设计一个遥控器,可以控制电灯开关。 -

+

```java public interface Command { @@ -365,7 +349,7 @@ public class Client { - TerminalExpression:终结符表达式,每个终结符都需要一个 TerminalExpression。 - Context:上下文,包含解释器之外的一些全局信息。 -

+

### Implementation @@ -498,7 +482,7 @@ false - Iterator 主要定义了 hasNext() 和 next() 方法。 - Client 组合了 Aggregate,为了迭代遍历 Aggregate,也需要组合 Iterator。 -

+

### Implementation @@ -587,17 +571,17 @@ public class Client { - Mediator:中介者,定义一个接口用于与各同事(Colleague)对象通信。 - Colleague:同事,相关对象 -

+

### Implementation Alarm(闹钟)、CoffeePot(咖啡壶)、Calendar(日历)、Sprinkler(喷头)是一组相关的对象,在某个对象的事件产生时需要去操作其它对象,形成了下面这种依赖结构: -

+

使用中介者模式可以将复杂的依赖结构变成星形结构: -

+

```java public abstract class Colleague { @@ -760,7 +744,7 @@ doSprinkler() 备忘录实际上有两个接口,一个是提供给 Caretaker 的窄接口:它只能将备忘录传递给其它对象; 一个是提供给 Originator 的宽接口,允许它访问到先前状态所需的所有数据。理想情况是只允许 Originator 访问本备忘录的内部状态。 -

+

### Implementation @@ -932,7 +916,7 @@ public class Client { 主题(Subject)是被观察的对象,而其所有依赖者(Observer)称为观察者。 -

+

### Class Diagram @@ -940,13 +924,13 @@ public class Client { 观察者(Observer)的注册功能需要调用主题的 registerObserver() 方法。 -

+

### Implementation 天气数据布告板会在天气信息发生改变时更新其内容,布告板有多个,并且在将来会继续增加。 -

+

```java public interface Subject { @@ -1075,7 +1059,7 @@ State:状态接口,用来封装与上下文的**一个特定状态所对应 ConcreteState:具体实现状态处理的类,每个类实现一个跟上下文相关的状态的具体处理。 -

+

### Implementation1 实现在线投票: @@ -1096,7 +1080,7 @@ ConcreteState:具体实现状态处理的类,每个类实现一个跟上下 程序结构如下图: -

+

```java /** @@ -1229,7 +1213,7 @@ public class Client { 糖果销售机有多种状态,每种状态下销售机有不同的行为,状态可以发生转移,使得销售机的行为也发生改变。 -

+

```java public interface State { @@ -1530,7 +1514,7 @@ No gumball dispensed - Strategy 接口定义了一个算法族,它们都实现了 behavior() 方法。 - Context 是使用到该算法族的类,其中的 doSomething() 方法会调用 behavior(),setStrategy(Strategy) 方法可以动态地改变 strategy 对象,也就是说能动态地改变 Context 所使用的算法。 -

+

### 与状态模式的比较 @@ -1551,7 +1535,7 @@ No gumball dispensed 2. 对老客户报的价格,根据客户年限,给予一定的折扣 3. 对大客户报的价格,根据大客户的累计消费金额,给予一定的折扣 -

+

```java /** @@ -1718,13 +1702,13 @@ squeak! ### Class Diagram -

+

### Implementation 冲咖啡和冲茶都有类似的流程,但是某些步骤会有点不一样,要求复用那些相同步骤的代码。 -

+

```java public abstract class CaffeineBeverage { @@ -1821,7 +1805,7 @@ Tea.addCondiments - ConcreteVisitor:具体访问者,存储遍历过程中的累计结果 - ObjectStructure:对象结构,可以是组合结构,或者是一个集合。 -

+

### Implementation @@ -2026,7 +2010,7 @@ Number of items: 6 ### Class Diagram -

+

### Implementation diff --git "a/docs/OO/03\347\273\223\346\236\204\345\236\213.md" "b/docs/OO/4_\347\273\223\346\236\204\345\236\213.md" similarity index 95% rename from "docs/OO/03\347\273\223\346\236\204\345\236\213.md" rename to "docs/OO/4_\347\273\223\346\236\204\345\236\213.md" index cbf2d0d4..f3144f52 100644 --- "a/docs/OO/03\347\273\223\346\236\204\345\236\213.md" +++ "b/docs/OO/4_\347\273\223\346\236\204\345\236\213.md" @@ -1,15 +1,4 @@ - -* [四、结构型](#四结构型) - * [1. *适配器(Adapter)](#1-适配器adapter) - * [2. 桥接(Bridge)](#2-桥接bridge) - * [3. 组合(Composite)](#3-组合composite) - * [4. 装饰(Decorator)](#4-装饰decorator) - * [5. 外观(Facade)](#5-外观facade) - * [6. 享元(Flyweight)](#6-享元flyweight) - * [7. *代理(Proxy)](#7-代理proxy) - - -# 四、结构型 +# 结构型 ## 1. 适配器(Adapter) @@ -17,7 +6,7 @@ 把一个类接口转换成另一个用户需要的接口。 -

+

### Class Diagram Target:定义客户端需要的跟特定领域相关的接口。 @@ -27,7 +16,7 @@ Adaptee:已经存在的接口,通常能满足客户端的功能要求, Adapter:适配器,把Adaptee适配成为Client需要的Target。 -

+

### Implementation1 美国的电饭煲是在电压为 110V 下工作,而中国的电饭煲在电压 220V 下工作。要求将在美国使用的电饭煲适配成能在中国使用。 @@ -193,7 +182,7 @@ public class Client { - Abstraction:定义抽象类的接口 - Implementor:定义实现类接口 -

+

### Implementation @@ -354,7 +343,7 @@ Composite:组合对象,通常会存储子组件,定义包含子组件的 组合对象拥有一个或者多个组件对象,因此组合对象的操作可以委托给组件对象去处理,而组件对象可以是另一个组合对象或者叶子对象。 -

+

### Implementation1 商品类别树的管理,比如有如下所示的商品类别树: @@ -651,7 +640,7 @@ public class Client { 装饰者(Decorator)和具体组件(ConcreteComponent)都继承自组件(Component),具体组件的方法实现不需要依赖于其它对象,而装饰者组合了一个组件,这样它可以装饰其它装饰者或者具体组件。所谓装饰,就是把这个装饰者套在被装饰者之上,从而动态扩展被装饰者的功能。装饰者的方法有一部分是自己的,这属于它的功能,然后调用被装饰者的方法实现,从而也保留了被装饰者的功能。可以看到,具体组件应当是装饰层次的最低层,因为只有具体组件的方法实现不需要依赖于其它对象。 -

+

### Implementation1 给普通手机装饰上彩铃等功能。 @@ -789,7 +778,7 @@ IPhone打电话 下图表示在 DarkRoast 饮料上新增新添加 Mocha 配料,之后又添加了 Whip 配料。DarkRoast 被 Mocha 包裹,Mocha 又被 Whip 包裹。它们都继承自相同父类,都有 cost() 方法,外层类的 cost() 方法调用了内层类的 cost() 方法。 -

+

```java public interface Beverage { @@ -894,7 +883,7 @@ public class Client { ### Class Diagram -

+

### Implementation @@ -953,7 +942,7 @@ public class Client { - IntrinsicState:内部状态,享元对象共享内部状态 - ExtrinsicState:外部状态,每个享元对象的外部状态不同 -

+

### Implementation @@ -1053,7 +1042,7 @@ Java 利用缓存来加速大量小对象的访问时间。 - RealSubject:具体的目标对象,真正实现目标接口要求的功能。 -

+

### Implementation diff --git "a/docs/OO/04\351\235\242\345\220\221\345\257\271\350\261\241\344\270\211\345\244\247\347\211\271\346\200\247.md" "b/docs/OO/5_\351\235\242\345\220\221\345\257\271\350\261\241\344\270\211\345\244\247\347\211\271\346\200\247.md" similarity index 95% rename from "docs/OO/04\351\235\242\345\220\221\345\257\271\350\261\241\344\270\211\345\244\247\347\211\271\346\200\247.md" rename to "docs/OO/5_\351\235\242\345\220\221\345\257\271\350\261\241\344\270\211\345\244\247\347\211\271\346\200\247.md" index d379467f..bbbf23bb 100644 --- "a/docs/OO/04\351\235\242\345\220\221\345\257\271\350\261\241\344\270\211\345\244\247\347\211\271\346\200\247.md" +++ "b/docs/OO/5_\351\235\242\345\220\221\345\257\271\350\261\241\344\270\211\345\244\247\347\211\271\346\200\247.md" @@ -1,11 +1,4 @@ - -* [五、三大特性](#五三大特性) - * [封装](#封装) - * [继承](#继承) - * [多态](#多态) - - -# 五、三大特性 +# 三大特性 ## 封装 @@ -26,9 +19,9 @@ ### 3. 成员变量和局部变量的区别 | 区别 | 成员变量 | 局部变量 | -| :--: | :--: | :--: | +| :--: | :--: | :--: | | 在类中位置不同 | 类中方法外 | 方法内或者方法声明上 | -| 在内存中位置不同 | 堆内存 | 栈内存 | +| 在内存中位置不同 | 堆内存 | 栈内存 | | 声明周期不同 | 随着**对象**存在而存在,随着对象消失而消失 | 随着**方法**调用而存在,随着方法调用完毕而消失 | | 初始化值不同 | 有默认的初始值 | 没有默认的初始值,必须先定义,赋值,才能使用 | @@ -69,23 +62,17 @@ Student s=new Student(); 具体步骤: 1. 加载Student.class文件进内存 - 2. 在**栈内存**为s开辟空间 - 3. 在**堆内存**为学生对象开辟空间 - 4. 对学生成员变量进行默认初始化 - 5. 对学生成员变量进行显示初始化 - 6. 通过构造方法对学生对象的成员赋值 - 7. 学生成员对象初始化完毕,将对象地址赋值给s变量。 +**一个标准的手机类的代码及测试**: -> 封装:一个标准的手机类的代码及测试 +- 手机类 -> 手机类 ```java public class SmartPhone { private String brand; @@ -130,7 +117,8 @@ public class SmartPhone { } ``` -> 手机测试类1 +- 手机测试类1 + ```java public class SmartPhoneDemo { public static void main(String[] args) { @@ -148,10 +136,7 @@ public class SmartPhoneDemo { } ``` -一个手机对象的内存简图: -

- -> 手机测试类2 +- 手机测试类2 ```java public class SmartPhoneDemo2 { @@ -180,16 +165,14 @@ public class SmartPhoneDemo2 { } ``` -多个手机对象的内存简图: -

### 5. 静态变量和成员变量的区别 | 区别 | 静态变量 | 成员变量 | | :--: | :--: | :--: | -| 所属不同 | 属于类,所以也称为**类变量** | 属于对象,所以也称为**实例变量**(对象变量) | +| 所属不同 | 属于类,所以也称为**类变量** | 属于对象,所以也称为**实例变量**(对象变量) | | 内存中位置不同 | 存储于**方法区**的静态区 | 存储于堆内存 | | 内存中出现时间不同 | 随着**类**的加载而加载,随着类的消失而消失 | 随着**对象**的创建而存在,随着对象的消失而消失 | | 调用不同 | 可以通过类名调用,也可以通过对象调用 | 只能通过的对象来调用 | @@ -267,15 +250,16 @@ Animal animal = new Cat(); ### 5. 继承中成员变量的关系 - 子类中成员变量和父类中成员变量名称不同。 - 子类中成员变量和父类中成员变量名称相同。 - + + 在子类方法中的查找顺序: - +​ 在子类方法的局部范围找,有就使用 在子类的成员范围找,有就使用 在**父类的成员范围**找,有就使用 - + 如果还找不到,就报错 ### 6. 继承中构造方法的关系 @@ -290,7 +274,7 @@ Animal animal = new Cat(); 3. 如果父类中没有无参构造方法,该怎么办呢? 方式一:子类通过super去显示调用父类其他的带参的构造方法 - + 方式二:子类通过this去调用本类的其他构造方法 (子类一定要有一个去访问父类的构造方法,否则父类数据就没有初始化) @@ -458,7 +442,7 @@ final关键字是最终的意思,可以修饰类,修饰变量,修饰成员 在方法内部,该变量不可以被改变 在方法声明上,分为基本类型和引用类型的情况 - +​ (1)基本类型:是值不能变 (2)引用类型:是地址不能变,但是该对象的堆内存中的值是可以改变的 @@ -467,9 +451,10 @@ final关键字是最终的意思,可以修饰类,修饰变量,修饰成员 在对象构造完毕前即可(非静态常量),被final修饰的变量只能赋值一次。 -> 继承:一个标准的动物类、猫类、狗类的代码及测试 +**一个标准的动物类、猫类、狗类的代码及测试**: + +- 动物类 -> 动物类 ```java class Animal{ private String name; @@ -506,7 +491,8 @@ class Animal{ } ``` -> 猫类 +- 猫类 + ```java class Cat extends Animal{ @Override @@ -521,7 +507,8 @@ class Cat extends Animal{ } ``` -> 狗类 +- 狗类 + ```java class Dog extends Animal{ @Override @@ -536,7 +523,8 @@ class Dog extends Animal{ } ``` -> 测试 +- 测试 + ```java public class AnimalDemo { public static void main(String[] args) { @@ -680,9 +668,7 @@ public class PolymorphismDemo3 { } ``` -多态中对象内存图 -

### 5. 多态中的一些问题 diff --git "a/docs/OO/05\345\205\263\347\263\273\347\261\273\345\233\276.md" "b/docs/OO/6_\345\205\263\347\263\273\347\261\273\345\233\276.md" similarity index 65% rename from "docs/OO/05\345\205\263\347\263\273\347\261\273\345\233\276.md" rename to "docs/OO/6_\345\205\263\347\263\273\347\261\273\345\233\276.md" index 78d5f3a4..a0532acf 100644 --- "a/docs/OO/05\345\205\263\347\263\273\347\261\273\345\233\276.md" +++ "b/docs/OO/6_\345\205\263\347\263\273\347\261\273\345\233\276.md" @@ -1,14 +1,4 @@ - -* [六、类图](#六类图) - * [泛化关系 (Generalization)](#泛化关系-generalization) - * [实现关系 (Realization)](#实现关系-realization) - * [聚合关系 (Aggregation)](#聚合关系-aggregation) - * [组合关系 (Composition)](#组合关系-composition) - * [关联关系 (Association)](#关联关系-association) - * [依赖关系 (Dependency)](#依赖关系-dependency) - - -# 六、类图 +# 类图 以下类图使用 [PlantUML](https://www.planttext.com/) 绘制,更多语法及使用请参考:http://plantuml.com/ 。 @@ -16,7 +6,7 @@ 用来描述继承关系,在 Java 中使用 extends 关键字。 -

+

```text @startuml @@ -37,7 +27,7 @@ Vihical <|-- Trunck 用来实现一个接口,在 Java 中使用 implements 关键字。 -

+

```text @startuml @@ -58,7 +48,7 @@ MoveBehavior <|.. Run 表示整体由部分组成,但是整体和部分不是强依赖的,整体不存在了部分还是会存在。 -

+

```text @startuml @@ -81,7 +71,7 @@ Computer o-- Screen 和聚合不同,组合中整体和部分是强依赖的,整体不存在了部分也不存在了。比如公司和部门,公司没了部门就不存在了。但是公司和员工就属于聚合关系了,因为公司没了员工还在。 -

+

```text @startuml @@ -102,7 +92,7 @@ Company *-- DepartmentB 表示不同类对象之间有关联,这是一种静态关系,与运行过程的状态无关,在最开始就可以确定。因此也可以用 1 对 1、多对 1、多对多这种关联关系来表示。比如学生和学校就是一种关联关系,一个学校可以有很多学生,但是一个学生只属于一个学校,因此这是一种多对一的关系,在运行开始之前就可以确定。 -

+

```text @startuml @@ -125,7 +115,7 @@ School "1" - "n" Student - A 类是 B 类方法当中的一个参数; - A 类向 B 类发送消息,从而影响 B 类发生变化。 -

+

```text @startuml diff --git "a/docs/OO/06\351\235\242\345\220\221\345\257\271\350\261\241\350\256\276\350\256\241\345\216\237\345\210\231.md" "b/docs/OO/7_\351\235\242\345\220\221\345\257\271\350\261\241\350\256\276\350\256\241\345\216\237\345\210\231.md" similarity index 93% rename from "docs/OO/06\351\235\242\345\220\221\345\257\271\350\261\241\350\256\276\350\256\241\345\216\237\345\210\231.md" rename to "docs/OO/7_\351\235\242\345\220\221\345\257\271\350\261\241\350\256\276\350\256\241\345\216\237\345\210\231.md" index 155ce166..9c8c7821 100644 --- "a/docs/OO/06\351\235\242\345\220\221\345\257\271\350\261\241\350\256\276\350\256\241\345\216\237\345\210\231.md" +++ "b/docs/OO/7_\351\235\242\345\220\221\345\257\271\350\261\241\350\256\276\350\256\241\345\216\237\345\210\231.md" @@ -1,4 +1,4 @@ -# 七、设计原则 +# 面向对象设计原则 ## S.O.L.I.D @@ -82,4 +82,13 @@ ### 5. 稳定依赖原则 -包之间的依赖关系都应该是稳定方向依赖的,包要依赖的包要比自己更具有稳定性。 \ No newline at end of file +包之间的依赖关系都应该是稳定方向依赖的,包要依赖的包要比自己更具有稳定性。 + +## 设计模式的六大原则 + +- 开放闭合原则 +- 里式替换原则 +- 接口分离原则 +- 依赖倒置原则 +- 迪米特法则 +- 合成复用原则 \ No newline at end of file diff --git "a/docs/OS/1_\346\223\215\344\275\234\347\263\273\347\273\237\346\246\202\350\277\260.md" "b/docs/OS/1_\346\223\215\344\275\234\347\263\273\347\273\237\346\246\202\350\277\260.md" index 0c49c2b4..006e24f9 100644 --- "a/docs/OS/1_\346\223\215\344\275\234\347\263\273\347\273\237\346\246\202\350\277\260.md" +++ "b/docs/OS/1_\346\223\215\344\275\234\347\263\273\347\273\237\346\246\202\350\277\260.md" @@ -1,13 +1,16 @@ - -* [一、概述](#一概述) - * [基本特征](#基本特征) - * [基本功能](#基本功能) - * [系统调用](#系统调用) - * [大内核和微内核](#大内核和微内核) - * [中断分类](#中断分类) - - -# 一、概述 + + +# 操作系统概述 + +什么是操作系统? + +- 操作系统(Operation System,简称OS)是管理计算机硬件与软件资源的程序,是计算机系统的内核与基石 +- 操作系统本质上是运行在计算机上的软件程序 +- 为用户提供一个与系统交互的操作界面 + +- 操作系统分内核与外壳(我们可以把外壳理解成围绕着内核的应用程序,而内核就是能操作硬件的程序)。 + + ## 基本特征 diff --git "a/docs/OS/2_\350\277\233\347\250\213\347\256\241\347\220\206.md" "b/docs/OS/2_\350\277\233\347\250\213\347\256\241\347\220\206.md" index e1814132..c9972eb5 100644 --- "a/docs/OS/2_\350\277\233\347\250\213\347\256\241\347\220\206.md" +++ "b/docs/OS/2_\350\277\233\347\250\213\347\256\241\347\220\206.md" @@ -1,14 +1,4 @@ - -* [二、进程管理](#二进程管理) - * [进程与线程](#进程与线程) - * [进程状态的切换](#进程状态的切换) - * [进程调度算法](#进程调度算法) - * [进程同步](#进程同步) - * [经典同步问题](#经典同步问题) - * [进程通信](#进程通信) - - -# 二、进程管理 +# 进程管理 ## 进程与线程 diff --git "a/docs/OS/3_\346\255\273\351\224\201.md" "b/docs/OS/3_\346\255\273\351\224\201.md" index 9154d4e0..59152576 100644 --- "a/docs/OS/3_\346\255\273\351\224\201.md" +++ "b/docs/OS/3_\346\255\273\351\224\201.md" @@ -1,14 +1,4 @@ - -* [三、死锁](#三死锁) - * [必要条件](#必要条件) - * [处理方法](#处理方法) - * [鸵鸟策略](#鸵鸟策略) - * [死锁检测与死锁恢复](#死锁检测与死锁恢复) - * [死锁预防](#死锁预防) - * [死锁避免](#死锁避免) - - -# 三、死锁 +# 死锁 ## 必要条件 diff --git "a/docs/OS/4_\345\206\205\345\255\230\347\256\241\347\220\206.md" "b/docs/OS/4_\345\206\205\345\255\230\347\256\241\347\220\206.md" index a2de3ec1..b1e4d772 100644 --- "a/docs/OS/4_\345\206\205\345\255\230\347\256\241\347\220\206.md" +++ "b/docs/OS/4_\345\206\205\345\255\230\347\256\241\347\220\206.md" @@ -1,14 +1,4 @@ - -* [四、内存管理](#四内存管理) - * [虚拟内存](#虚拟内存) - * [分页系统地址映射](#分页系统地址映射) - * [页面置换算法](#页面置换算法) - * [分段](#分段) - * [段页式](#段页式) - * [分页与分段的比较](#分页与分段的比较) - - -# 四、内存管理 +# 内存管理 ## 虚拟内存 diff --git "a/docs/OS/5_\350\256\276\345\244\207\347\256\241\347\220\206.md" "b/docs/OS/5_\350\256\276\345\244\207\347\256\241\347\220\206.md" index 87d7cf5d..e84b016b 100644 --- "a/docs/OS/5_\350\256\276\345\244\207\347\256\241\347\220\206.md" +++ "b/docs/OS/5_\350\256\276\345\244\207\347\256\241\347\220\206.md" @@ -1,10 +1,4 @@ - -* [五、设备管理](#五设备管理) - * [磁盘结构](#磁盘结构) - * [磁盘调度算法](#磁盘调度算法) - - -# 五、设备管理 +# 设备管理 ## 磁盘结构 diff --git "a/docs/OS/6_\351\223\276\346\216\245.md" "b/docs/OS/6_\351\223\276\346\216\245.md" index 10cd0bbe..c8362790 100644 --- "a/docs/OS/6_\351\223\276\346\216\245.md" +++ "b/docs/OS/6_\351\223\276\346\216\245.md" @@ -1,12 +1,4 @@ - -* [六、链接](#六链接) - * [编译系统](#编译系统) - * [静态链接](#静态链接) - * [目标文件](#目标文件) - * [动态链接](#动态链接) - - -# 六、链接 +# 链接 ## 编译系统 diff --git a/docs/README.md b/docs/README.md index 459db825..03951a42 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,124 +1,223 @@ -# 📖 目录 - -## ✏️ 计算机基础 - -### I、计算机网络 - -- [第一节 概述](Net/1_概述.md) -- [第二节 物理层](Net/2_物理层.md) -- [第三节 数据链路层](Net/3_数据链路层.md) -- [第四节 网络层](Net/4_网络层.md) -- [第五节 运输层](Net/5_运输层.md) -- [第六节 应用层](Net/6_应用层.md) - -### II、操作系统 - -- [第一节 操作系统概述](OS/1_操作系统概述.md) -- [第二节 进程管理](OS/2_进程管理.md) -- [第三节 死锁](OS/3_死锁.md) -- [第四节 内存管理](OS/4_内存管理.md) -- [第五节 设备管理](OS/4_设备管理.md) -- [第六节 链接](OS/6_链接.md) - -## ☕️ Java 基础 - -- [第一节 数据类型](JavaBasics/00数据类型.md) -- [第二节 String](JavaBasics/01String.md) -- [第三节 运算](JavaBasics/02%E8%BF%90%E7%AE%97.md) -- [第四节 Object 通用方法](JavaBasics/03Object%E9%80%9A%E7%94%A8%E6%96%B9%E6%B3%95.md) -- [第五节 关键字](JavaBasics/04%E5%85%B3%E9%94%AE%E5%AD%97.md) -- [第六节 反射](JavaBasics/05%E5%8F%8D%E5%B0%84.md) -- [第七节 异常](JavaBasics/06%E5%BC%82%E5%B8%B8.md) -- [第八节 泛型](JavaBasics/07%E6%B3%9B%E5%9E%8B.md) -- [第九节 注解](JavaBasics/08%E6%B3%A8%E8%A7%A3.md) -- [第十节 Java中常见对象](JavaBasics/09Java%E5%B8%B8%E8%A7%81%E5%AF%B9%E8%B1%A1.md) -- [第十一节 其他](JavaBasics/10%E5%85%B6%E4%BB%96.md) - -## 💾 Java 容器 - -- [第一节 Java容器概览](JavaContainer/00Java%E5%AE%B9%E5%99%A8%E6%A6%82%E8%A7%88.md) -- [第二节 容器的设计模式](JavaContainer/01%E5%AE%B9%E5%99%A8%E4%B8%AD%E7%9A%84%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md) -- [第三节 容器源码解析-List](JavaContainer/02容器源码分析%20-%20List.md) -- [第三节 容器源码解析-Map](JavaContainer/03容器源码分析%20-%20Map.md) -- [第三节 容器源码解析-并发容器](JavaContainer/04容器源码分析%20-%20并发容器.md) - -## 🙊 Java 虚拟机 - -- [第一节 运行时数据区域](JVM/1_JVM.md) -- [第二节 HotSpot 虚拟机对象](JVM/2_JVM.md) -- [第三节 String 类和常量池](JVM/3_JVM.md) -- [第四节 8 种基本类型的包装类和常量池](JVM/4_JVM.md) -- [第五节 垃圾收集](JVM/5_JVM.md) -- [第六节 内存分配与回收策略](JVM/6_JVM.md) -- [第七节 类加载机制](JVM/7_JVM.md) - -## 🔨 Java 并发 - -- [第一节 基础知识](Java_Concurrency/1_基础知识.md) -- [第二节 并发理论](Java_Concurrency/2_并发理论.md) -- [第三节 并发关键字](Java_Concurrency/3_并发关键字.md) -- [第四节 Lock 体系](Java_Concurrency/4_Lock%20体系.md) -- [第五节 原子操作类](Java_Concurrency/5_原子操作类.md) -- [第六节 并发容器](Java_Concurrency/6_并发容器.md) -- [第七节 并发工具](Java_Concurrency/7_并发工具.md) -- [第八节 线程池](Java_Concurrency/8_线程池.md) -- [第九节 并发实践](Java_Concurrency/9_并发实践.md) - -## 💡 JavaIO - -- [第一节 概览](JavaIO/00%E6%A6%82%E8%A7%88.md) -- [第二节 磁盘操作](JavaIO/01%E7%A3%81%E7%9B%98%E6%93%8D%E4%BD%9C.md) -- [第三节 字节操作](JavaIO/02%E5%AD%97%E8%8A%82%E6%93%8D%E4%BD%9C.md) -- [第四节 字符操作](JavaIO/03%E5%AD%97%E7%AC%A6%E6%93%8D%E4%BD%9C.md) -- [第五节 对象操作](JavaIO/04%E5%AF%B9%E8%B1%A1%E6%93%8D%E4%BD%9C.md) -- [第六节 网络操作](JavaIO/05%E7%BD%91%E7%BB%9C%E6%93%8D%E4%BD%9C.md) -- [第七节 NIO](JavaIO/06NIO.md) - -## 📝 JavaWeb - -- [第一节 Servlet 工作原理解析](JavaWeb/00Servlet%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86%E8%A7%A3%E6%9E%90.md) -- [第二节 JSP 解析](JavaWeb/01JSP%E8%A7%A3%E6%9E%90.md) -- [第三节 深入理解 Session 和 Cookie](JavaWeb/02%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Session%E5%92%8CCookie.md) - -## 👫 面向对象 - -### I、设计模式 - -- [第一节 设计模式概述](OO/00%E6%A6%82%E8%BF%B0.md) -- [第二节 创建型设计模式](OO/01%E5%88%9B%E5%BB%BA%E5%9E%8B.md) -- [第三节 结构型设计模式](OO/03%E7%BB%93%E6%9E%84%E5%9E%8B.md) - -### II、面向对象思想 - -- [第五节 面向对象三大特性](OO/04%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E4%B8%89%E5%A4%A7%E7%89%B9%E6%80%A7.md) -- [第六节 关系类图](OO/05%E5%85%B3%E7%B3%BB%E7%B1%BB%E5%9B%BE.md) -- [第七节 面向对象设计原则](OO/06%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E8%AE%BE%E8%AE%A1%E5%8E%9F%E5%88%99.md) - -## 🎨 剑指 Offer 题解 - -- [第一节 数组](AimForOffer/1_数组.md) -- [第二节 字符串](AimForOffer/2_字符串.md) -- [第三节 查找](AimForOffer/3_查找.md) -- [第四节 链表](AimForOffer/4_链表.md) -- [第五节 树](AimForOffer/5_树.md) -- [第六节 栈](AimForOffer/6_栈.md) -- [第七节 队列](AimForOffer/7_队列.md) -- [第八节 动态规划](AimForOffer/8_动态规划.md) -- [第九节 回溯](AimForOffer/9_回溯.md) -- [第十节 深度优先](AimForOffer/10_深度优先.md) -- [第十一节 贪心](AimForOffer/11_贪心.md) -- [第十二节 数学运算](AimForOffer/12_数学运算.md) -- [第十三节 排序](AimForOffer/13_排序.md) -- [第十四节 堆](AimForOffer/14_堆.md) -- [第十五节 哈希](AimForOffer/15_哈希.md) -- [第十六节 其他](AimForOffer/16_其他.md) - -## 💻 Redis 实战 - - - -## 🔧 关注的小专栏 +# ☕️ Java + +## Java 基础 + +- [数据类型](JavaBasics/1_数据类型.md) +- [常见运算](JavaBasics/3_运算.md) +- [final & static](JavaBasics/5_关键字.md) +- [Java 常见类 I ](JavaBasics/10_Java常见对象.md) +- [Java 常见类 II](JavaBasics/10_Java常见对象_2.md) +- [异常 & 反射](JavaBasics/6_反射.md) +- [泛型 & 注解](JavaBasics/8_泛型.md) +- [正则表达式](JavaBasics/正则表达式.md) +- [JDK8 新特性](JavaBasics/JDK8新特性.md) + +## Java 容器 + +- [容器概览](JavaContainer/1_容器概览.md) +- [容器中的设计模式](JavaContainer/2_容器中的设计模式.md) +- [容器源码分析 - List](JavaContainer/3_容器源码分析%20-%20List.md) +- [容器源码分析 - Map](JavaContainer/4_容器源码分析%20-%20Map.md) +- [容器源码分析 - 并发容器](JavaContainer/5_容器源码分析%20-%20并发容器.md) + +## Java 虚拟机 + +- [Java 内存区域](JVM/1_Java内存区域.md) +- [垃圾收集](JVM/2_垃圾收集.md) +- [内存分配与回收策略](JVM/3_内存分配与回收策略.md) +- [JVM 调优](JVM/4_JVM调优.md) +- [类文件结构](JVM/5_类文件结构.md) +- [类加载机制](JVM/6_类加载机制.md) +- [Java 程序编译和运行过程](JVM/7_Java程序编译和运行过程.md) + +## Java 并发 -- [后端面试进阶指南](https://xiaozhuanlan.com/CyC2018) -- [Java 面试进阶指南](https://xiaozhuanlan.com/javainterview) +- [进程和线程](Java_Concurrency/1_基础知识.md) +- [并发理论](Java_Concurrency/2_并发理论.md) +- [并发关键字](Java_Concurrency/3_并发关键字.md) +- [Lock 体系](Java_Concurrency/4_Lock%20体系.md) +- [原子操作类](Java_Concurrency/5_原子操作类.md) +- [并发容器 & 并发工具](Java_Concurrency/7_并发工具.md) +- [线程池](Java_Concurrency/8_线程池.md) +- [并发实践](Java_Concurrency/9_并发实践.md) + +## Java I/O + +- [Java I/O 概览](JavaIO/1_概览.md) +- [磁盘操作](JavaIO/2_磁盘操作.md) +- [字节操作](JavaIO/3_字节操作.md) +- [字符操作](JavaIO/4_字符操作.md) +- [对象操作](JavaIO/5_对象操作.md) +- [网络操作](JavaIO/6_网络操作.md) +- [NIO & AIO](JavaIO/7_NIO.md) +- [Java I/O 方式](JavaIO/8_JavaIO方式.md) + +# 👫 面向对象 + +## 设计模式 + +- [概述](OO/1_概述.md) +- [创建型](OO/2_创建型.md) +- [行为型](OO/3_行为型.md) +- [结构型](OO/4_结构型.md) + +## 面向对象思想 + +- [面向对象三大特性](OO/5_面向对象三大特性.md) +- [关系类图](OO/6_关系类图.md) +- [面向对象设计原则](OO/7_面向对象设计原则.md) + +# 📝 编程题 + +## 数据结构系列 + +- [数组 & 矩阵](AimForOffer/数据结构相关/1_数组和矩阵.md) +- [字符串](AimForOffer/数据结构相关/2_字符串.md) +- [链表](AimForOffer/数据结构相关/3_链表.md) +- [树](AimForOffer/数据结构相关/4_树.md) +- [栈](AimForOffer/数据结构相关/5_栈.md) +- [队列](AimForOffer/数据结构相关/6_队列.md) +- [堆](AimForOffer/数据结构相关/7_堆.md) +- [ 哈希.](AimForOffer/数据结构相关/8_哈希.md) + +## 算法思维系列 + +- [查找](AimForOffer/算法思想相关/1_查找.md) +- [排序](AimForOffer/算法思想相关/2_排序.md) +- [动态规划](AimForOffer/算法思想相关/3_动态规划.md) +- [搜索](AimForOffer/算法思想相关/4_搜索.md) +- [排列组合](AimForOffer/算法思想相关/5_排列组合.md) +- [贪心](AimForOffer/算法思想相关/6_贪心.md) +- [数学运算](AimForOffer/算法思想相关/7_数学运算.md) +- [其他](AimForOffer/算法思想相关/8_其他.md) + +# 💾 数据库 + +## 数据库理论 + +- [数据库系统原理](DataBase/1_数据库系统原理.md) +- [关系数据库设计理论](DataBase/2_关系数据库设计理论.md) +- [如何设计关系型数据库?](DataBase/3_设计关系型数据库.md) + +## SQL 编程题 + +- [LeetCode SQL 练习](DataBase/5_LeetCode_Database题解.md) + +## MySQL + +- [锁机制](MySQL/1_锁机制.md) +- [事务隔离级别实现](MySQL/2_事务隔离级别实现.md) +- [索引](MySQL/3_索引.md) +- [MySQL架构](MySQL/4_MySQL架构.md) +- [MySQL优化](MySQL/5_MySQL优化.md) +- [MySQL数据类型](MySQL/6_MySQL数据类型.md) +- [切分 & 复制问题](MySQL/切分复制问题.md) +- [补充知识](MySQL/6_补充知识.md) + +## Redis + +- [Redis 概述](Redis/1_概述.md) +- [Redis 数据类型](Redis/2_数据类型.md) +- [Redis 单线程模型](Redis/3_单线程模型.md) +- [键的过期时间 & 内存淘汰机制](Redis/4_键的过期时间和内存淘汰机制.md) +- [Redis 持久化机制](Redis/5_持久化机制.md) +- [Redis 事务](Redis/6_事务.md) +- [缓存问题](Redis/7_缓存问题.md) +- [Redis 部署方式](Redis/8_部署方式.md) +- [Redis 实战](Redis/9_实战.md) + +# 🎓 系统设计 + +- [系统设计基础](1_基础.md) + +## 安全性 + +- [Cookie & Session &Token](Safety/Cookie_Session_Token.md) +- [常见攻击技术及防御](Safety/常见攻击技术及防御.md) + +## 分布式 +- [分布式系统基本概念](distribution/1_分布式系统设计理念.md) +- [CAP 理论 & BASE 理论](distribution/2_CAP理论.md) +- [分布式锁](distribution/3_分布式锁.md) +- [分布式事务](distribution/4_分布式事务.md) +- [Paxos 算法 & Raft 算法](distribution/5_Paxos算法.md) + +## 集群 + +- [负载均衡](cluster/1_负载均衡.md) +- [集群下的 Session 管理](cluster/2_集群下的Session管理.md) + +## 缓存 + +- [缓存需要考虑的问题](cache/1_缓存需要考虑的问题.md) +- [缓存常见问题](cache/2_缓存常见问题.md) +- [数据分布](cache/3_数据分布.md) + +## 消息队列 + +- [消息队列](1_消息队列.md) +- [Kafka 原理及应用](Kafka&RabbitMQ/Kafka.md) +- [RabbitMQ 原理及应用](Kafka&RabbitMQ/RabbitMQ.md) +- [Kafka & RabbitMQ 比较](Kafka&RabbitMQ/Kafka_RabbitMQ.md) + +# ☎️ 常用框架 + +## Spring + +- [Spring 概述](Spring/1_Spring概述.md) +- [Spring IoC 原理](Spring/2_SpringIoC原理.md) +- [Spring AOP 原理](Spring/3_SpringAOP原理.md) +- [Spring MVC 原理](Spring/4_SpringMVC原理.md) +- [Spring 事务管理](Spring/5_Spring事务管理.md) +- [Spring 中用到的设计模式](Spring/6_Spring中用到的设计模式.md) +- [MyBatis](Spring/7_MyBatis.md) + +## SpringBoot + +- [SpringBoot 概述](SpringBoot/1_SpringBoot概述.md) +- [配置文件](SpringBoot/2_配置文件.md) +- [常用注解](SpringBoot/3_常用注解.md) +- [请求参数校验](SpringBoot/4_请求参数校验.md) +- [全局异常处理](SpringBoot/5_全局异常处理.md) +- [实现定时任务](SpringBoot/6_实现定时任务.md) + +## Zookeeper + +- [Zookeeper 概述](Zookeeper/1_概述.md) +- [Zookeeper 数据模型](Zookeeper/2_数据模型.md) +- [Zookeeper 的原理](Zookeeper/3_原理.md) +- [Zookeeper 的应用](Zookeeper/4_应用.md) + +# 📖 工具 + +## Git + +- [git - 简明指南](http://rogerdudler.github.io/git-guide/index.zh.html) +- [git - 图解](http://marklodato.github.io/visual-git-guide/index-zh-cn.html) +- [github - 小技巧](https://snailclimb.gitee.io/javaguide/#/docs/tools/Github%E6%8A%80%E5%B7%A7) + +## Docker + +- [Docker 概述](https://snailclimb.gitee.io/javaguide/#/docs/tools/Docker) +- [Docker 实战](https://snailclimb.gitee.io/javaguide/#/docs/tools/Docker%E4%BB%8E%E5%85%A5%E9%97%A8%E5%88%B0%E5%AE%9E%E6%88%98) + +# 📚 参考资料 + +## 参考仓库 + +- [Java 基础知识](reference/Java基础_1.md) +- [Java 进阶知识](reference/Java进阶_1.md) +- [编程题](reference/编程题_1.md) + +## 参考书籍 + +- [Java 基础](reference/Java基础_2.md) +- [编程题](reference/编程题_2.md) +- [数据库](reference/数据库.md) +- [常用框架](reference/常用框架.md) + +## 小专栏 + +- [后端面试进阶指南](https://xiaozhuanlan.com/CyC2018) +- [Java 面试进阶指南](https://xiaozhuanlan.com/javainterview) \ No newline at end of file diff --git "a/docs/Redis/1_\346\246\202\350\277\260.md" "b/docs/Redis/1_\346\246\202\350\277\260.md" new file mode 100644 index 00000000..92dabc04 --- /dev/null +++ "b/docs/Redis/1_\346\246\202\350\277\260.md" @@ -0,0 +1,98 @@ +# Redis 概述 + +Redis 是速度非常快的**非关系型(NoSQL)**基于**内存**的**键值**数据库。 + +键的类型只能为字符串,值可以支持五种数据类型:字符串(stirng)、列表(list)、集合(set)、散列表(hash)、有序集合(zset)。 + +Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,使用复制来扩展**读性能**,使用分片来扩展**写性能**。 + +Redis 除了做缓存之外,也经常用来做分布式锁,甚至是消息队列。 + +## Redis & Memcached + +分布式缓存主要解决的是单机缓存的容量受服务器限制并且无法保存通用信息的问题。 + +分布式缓存兴起时,比较常用的是 Memcached。随着 Redis 的发展,大家慢慢都转而使用更加强大的 Redis 了。现在基本没有看过还有项目使用 Memcached 来做缓存了。 + +两者相同点: + +- 都是基于内存的非关系型内存键值数据库,一般都用来当做缓存使用。 +- 都有过期策略 +- 性能都非常高 + +主要有以下不同: + +- 数据类型: + + Memcached 仅支持字符串类型,而 Redis 支持五种不同的数据类型,可以更灵活地解决问题。 + +- 线程模型: + + Memcached 是多线程,非阻塞 IO 复用的网络模型,而 Redis 使用单线程的多路 IO 复用模型。 + +- 删除策略: + + Memcached 过期数据的删除策略只用了惰性删除,而 Redis 同时使用了惰性删除与定期删除。 + +- 数据持久化: + + Redis 支持两种持久化策略:RDB 快照和 AOF 日志,而 Memcached 不支持持久化。 + +- 分布式: + + Memcached 不支持分布式,只能通过在客户端使用一致性哈希来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。 + + Redis Cluster 实现了分布式的支持。 + +- 内存管理机制 + + - 在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘,而 Memcached 的数据则会一直在内存中。 + - Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题。但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。 + +## 使用场景 + +### 计数器 + +可以对 String 进行自增自减运算,从而实现计数器功能。 + +Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。 + +### 缓存 + +将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。 + +### 查找表 + +例如 DNS 记录就很适合使用 Redis 进行存储。 + +查找表和缓存类似,也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效,因为缓存不作为可靠的数据来源。 + +### 分布式锁实现 + +在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。 + +可以使用 Redis 自带的 SETNX 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现。 + +### 其它业务场景 + +Bitmap 可以实现统计统计活跃用户等功能。 + +Set 可以实现交集、并集等操作,从而实现共同好友等功能。 + +ZSet 可以实现有序性操作,从而实现排行榜等功能。 + + + +## 使用 Redis 缓存的好处 + +**高性能**: + +假如用户第一次访问数据库中的某些数据的话,这个过程是比较慢,毕竟是从硬盘中读取的。但如果用户访问的数据属于高频数据并且不会经常改变的话,那么我们就可以很放心地将该用户访问的数据存在缓存中。 + +可以保证用户下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。不过,要保持数据库和缓存中的数据的一致性。 如果数据库中的对应数据改变的之后,需要同步改变缓存中相应的数据。 + +**高并发**: + +一般像 MySQL 这类的数据库的 QPS(Query Per Second,服务器每秒可以执行的查询次数)大概都在 1w 左右(4 核 8g) ,使用 Redis 缓存之后很容易达到 10 w+,甚至最高能达到 30 w+(单机 Redis 的情况,Redis 集群的话会更高)。 + +由此可见,直接操作缓存能够承受的数据库请求数量是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库,也就提高了系统整体的并发。 diff --git "a/docs/Redis/2_\346\225\260\346\215\256\347\261\273\345\236\213.md" "b/docs/Redis/2_\346\225\260\346\215\256\347\261\273\345\236\213.md" new file mode 100644 index 00000000..79b0791d --- /dev/null +++ "b/docs/Redis/2_\346\225\260\346\215\256\347\261\273\345\236\213.md" @@ -0,0 +1,319 @@ +# Redis 数据类型 + +## STRING + +### 简介 + +STRING 可以存储字符串、整数或者浮点数, 既可以对整个字符串或者字符串的其中一部分执行操作,也可以对整数和浮点数执行自增或者自减操作。 + +Redis 底层构建了一种**简单动态字符串(Simple Dynamic String,SDS)**。相比于 C 的原生字符串,SDS 不光可以保存文本数据还可以保存二进制数据,并且获取字符串长度时间复杂度为 O(1)(C 字符串为 O(N)),除此之外,SDS 的 API 是安全的,不会造成缓冲区溢出。 + +### 常用命令 + +- 普通字符串的基本操作: + + ```html + > set hello world + OK + > get hello + "world" + > exists hello + (integer) 1 + > del hello + (integer) 1 + > get hello + (nil)s + ``` + +- 批量设置: + + ```html + > mset key1 value1 key2 value2 + OK + > mget key1 key2 + 1) "value1" + 2) "value2" + ``` + +- 计数器: + + ```html + > set number 1 + OK + > incr number + (integer) 2 + 127.0.0.1:6379> get number + "2" + 127.0.0.1:6379> decr number + (integer) 1 + 127.0.0.1:6379> get number + "1" + ``` + +- 过期(默认永不过期): + + ```html + > expire key 60 # 设置 key 在 60s 后过期 + (integer) 1 + > setex key 60 value + # 设置 key-value 值且 key 在 60s 后过期 (setex:[set] + [ex]pire) + OK + > ttl key # 查看数据还有多久过期 + (integer) 56 + ``` + +### 应用场景 + +一般常用在需要计数的场景,比如用户的访问次数、热点文章的点赞转发数量等等。 + + + +## LIST + +### 简介 + +LIST 即列表,可以从两端压入或者弹出元素,还可以对单个或者多个元素进行修剪,只保留一个范围内的元素。 + +Redis 的 LIST 的底层实现为一个**双向链表**,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。 + +### 常用命令 + +- rpush/lpop 实现队列: + + ```html + > rpush myList value1 + (integer) 1 + > rpush myList value2 value3 + (integer) 3 + > lpop myList + "value1" + > lrange myList 0 1 + 1) "value2" + 2) "value3" + > lrange myList 0 -1 # 查看列表中的所有元素,-1表示倒数第一 + 1) "value2" + 2) "value3" + > llen myList # 查看列表长度 + (integer) 2 + ``` + +- rpush/rpop 实现栈: + + ```html + > rpush myList2 value1 value2 value3 + (integer) 3 + > rpop myList2 + "value3" + ``` + +### 应用场景 + +发布与订阅或者说消息队列、慢查询。 + + + +## HASH + +### 简介 + +HASH 即包含键值对的无序散列表,可以添加、获取、移除单个键值对;获取所有键值对;检查某个键是否存在。 + +Redis 底层实现类 JDK1.8 前的 HashMap(数组+链表)。 + +### 常用命令 + +```html +> hmset user name "a" description "student" age "12" +OK +> hexists user name # 查看 key 对应的value 中指定的字段是否存在 +(integer) 1 +> hget user name +"a" +> hget user age +"12" +127.0.0.1:6379> hgetall user +1) "name" +2) "a" +3) "description" +4) "student" +5) "age" +6) "12" +> hkeys user +1) "name" +2) "description" +3) "age" +127.0.0.1:6379> hvals user +1) "a" +2) "student" +3) "12" +> hset user name "b" +> hget user name +"b" +``` + +### 应用场景 + +系统中对象数据的存储。 + + + +## SET + +### 简介 + +SET 即无序集合。当需要存储一个列表数据,又不希望出现重复数据时,SET 是一个很好的选择。基于 SET 可以轻易实现交集、并集、差集的操作。比如:将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis 可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能,求交集即可。 + +### 常用命令 + +```html +> sadd mySet value1 value2 +(integer) 2 +> sadd mySet value1 +(integer) 0 +> smembers mySet +1) "value1" +2) "value2" +> scard mySet # 查看 set 的长度 +(integer) 2 +> sismember mySet value1 # 检查某个元素是否存在 set 中,只能接收单个元素 +(integer) 1 +> sadd mySet2 value2 value3 +(integer) 2 +> sinterstore mySet3 mySet mySet2 # 获取 mySet 和 mySet2 的交集并存放在 mySet3 中 +(integer) 1 +127.0.0.1:6379> smembers mySet3 +1) "value2" +``` + +### 应用场景 + +存放不能重复的数据以及需要获取多个数据源交集和并集等场景 + + + +## ZSET + +### 简介 + +ZSET 即有序集合 (sorted set) ,增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,还可以通过 score 的范围来获取元素的列表。 + +### 常用命令 + +```html +> zadd myZset 3.0 value1 # 添加元素到 sorted set 中 3.0 为权重 +(integer) 1 +> zadd myZset 2.0 value2 1.0 value3 +(integer) 2 +127.0.0.1:6379> zcard myZset # 查看 sorted set 中的元素数量 +(integer) 3 +> zscore myZset value1 # 查看某个 value 的权重 +"3" +> zrange myZset 0 -1 # 顺序输出某个范围区间的元素,0 -1 表示输出所有元素 +1) "value3" +2) "value2" +3) "value1" +> zrange myZset 0 1 # 顺序输出某个范围区间的元素,0 为 start 1 为 stop +1) "value3" +2) "value2" +> zrevrange myZset 0 1 # 逆序输出某个范围区间的元素,0 为 start 1 为 stop +1) "value1" +2) "value2" +``` + +### 应用场景 + +需要对数据根据某个权重进行排序的场景。比如在直播系统中,在线用户列表,各种礼物排行榜等。 + + + +## BITMAP + +### 简介 + +BITMAP 即位图。用一个 bit 位来标记某个元素对应的 value,而 key 即是该元素。我们知道 8 个 bit 可以组成一个 byte,所以 BITMAP 本身会极大的节省储存空间: + +假设在 32 位操作系统中,从 20 亿个随机整数中找出某个数 m。 + + Java 中,如果每个数字用 int 存储,占用的空间约为 + +(2000000000*4/1024/1024/1024) ≈ 7.45 G + +如果按位存储,20亿个数就是 20 亿位,占用空间约为 + +(2000000000/8/1024/1024/1024) ≈ 0.23 G + +### 常用命令 + +```html +> setbit mykey 7 1 # setbit 会返回之前位的值(默认是 0) +(integer) 0 +> setbit mykey 7 0 +(integer) 1 +> getbit mykey 7 +(integer) 0 +> setbit mykey 6 1 +(integer) 0 +> setbit mykey 8 1 +(integer) 0 +> bitcount mykey # 通过 bitcount 统计被被设置为 1 的位的数量 +(integer) 2 +``` + +### 应用场景 + +适合需要**保存状态信息**(比如是否签到、是否登录 ...)并需要进一步对这些信息进行分析的场景。 + +#### 场景 1:用户行为分析 + +很多网站会对用户进行行为分析,比如记录用户是否点赞过某个视频。 + +```html +> setbit AoTeMan_mp4 用户id 1 +# 用户id 为 AoTeMan_mp4 视频点赞 +``` + +#### 场景 2:统计活跃用户 + +可以使用时间作为 key,用户 id 为 offset,如果用户当日活跃过就设置为 1。 + +计算某几天/月/年的活跃用户(我们约定:统计时间内只要有一天在线就称为活跃) + +举个例子: + +- 初始化数据: + + 用户 1 在 20210908、20210909 两天活跃过,用户 2 在 20210908 这一天活跃过 + + ```html + > setbit 20210908 1 1 + (integer) 0 + > setbit 20210908 2 1 + (integer) 0 + > setbit 20210909 1 1 + (integer) 0 + ``` + +- 统计 20210308~20210309 每天都活跃用户数: + + ```html + > bitop and active1 20210308 20210309 + # bittop 命令支持 and、or、not 、xor 这四种操作 + (integer) 1 + > bitcount active1 + (integer) 1 + ``` + +- 统计 20210308~20210309 活跃用户数: + + ```html + 127.0.0.1:6379> bitop or active2 20210308 20210309 + (integer) 1 + 127.0.0.1:6379> bitcount active2 + (integer) 2 + ``` + +#### 场景 3:用户在线情况 + +对于获取或者统计用户在线状态,使用 BITMAP 是一个节约空间且效率又高的一种方法。 + +只需要一个 key,然后用户 id 为 offset,如果在线就设置为 1,不在线就设置为 0。 diff --git "a/docs/Redis/3_\345\215\225\347\272\277\347\250\213\346\250\241\345\236\213.md" "b/docs/Redis/3_\345\215\225\347\272\277\347\250\213\346\250\241\345\236\213.md" new file mode 100644 index 00000000..50cd7fd4 --- /dev/null +++ "b/docs/Redis/3_\345\215\225\347\272\277\347\250\213\346\250\241\345\236\213.md" @@ -0,0 +1,101 @@ +# Redis 单线程模型 + +## Redis 单线程模型 + +Redis 基于 Reactor 模式开发了自己的网络事件处理器,这个处理器被称为文件事件处理器(file event handler)。由于文件事件处理器(file event handler)是单线程方式运行的,所以说 Redis 是单线程模型。 + +虽然 Redis 是单线程的,但仍然可以监听大量的客户端连接,原因在于文件事件处理器使用 **I/O 多路复用(multiplexing)程序**来同时监听多个客户端连接(即多个套接字),并根据套接字目前执行的任务来为套接字关联不同的事件处理器。也就是说,**I/O 多路复用技术使得 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗**。 + +### I/O 多路复用 + +多路指的是多个 Socke t连接,复用指的是复用一个线程。 + +多路复用主要有三种技术:select,poll,epoll。epoll 是最新的也是目前最好的多路复用技术。 + +
+ +### 文件事件处理器 + +Redis 服务器是一个事件驱动程序。服务器需要处理两类事件: + +- 文件事件:服务器通过套接字与客户端或者其它服务器进行通信,**文件事件就是对套接字操作的抽象**。 +- 时间事件:服务器有一些操作需要在给定的时间点执行,时间事件是对这类定时操作的抽象。 + +文件事件处理器主要是包含 4 个部分: + +- 多个 Socket(客户端连接) +- IO 多路复用程序(支持多个客户端连接) +- 文件事件分派器(将 Socket 关联到相应的事件处理器) +- 事件处理器(比如命令请求处理器、命令回复处理器、连接应答处理器) + +Redis 选择使用单线程模型处理来自客户端的绝大多数网络请求,主要有以下 3 个原因: + +- 几乎不存在 CPU 成为瓶颈的情况, Redis 主要受限于内存和网络。 + + Redis 并不是 CPU 密集型的服务,如果不开启 AOF 备份,所有 Redis 的操作都会在内存中完成不会涉及任何的 I/O 操作,这些**数据的读写由于只发生在内存中**,所以处理速度是非常快的; + + 整个服务的瓶颈在于**网络传输带来的延迟**和**等待客户端的数据传输**,也就是网络 I/O,所以使用多线程模型处理全部的外部请求可能不是一个好的方案。 + +- 使用单线程模型能带来更好的可维护性,方便开发和调试 + + 单线程不需要并发控制。 + +- 使用单线程模型也能并发的处理客户端的请求 + + I/O 多路复用机制。 + + + +## Redis 4.0 后引入多线程 + +在 Redis 4.0 之后的版本,Redis 服务在执行一些命令时就会使用 ”主处理线程“ 之外的其他线程,例如 `UNLINK`、`FLUSHALL ASYNC`、`FLUSHDB ASYNC` 等非阻塞的删除操作。 + +对于 Redis 中的一些超大键值对,几十 MB 或者几百 MB 的数据并不能在几毫秒的时间内处理完,Redis 可能会需要在释放内存空间上消耗较多的时间。其实释放内存空间的工作可以由后台线程异步进行处理,只需要将键从元数据中删除,而真正的删除操作会在后台异步执行。Redis 引入多线程后,对于一些**大键值对的删除操作**,可以通过**多线程非阻塞地释放内存空间**减少对 Redis 主线程阻塞的时间,提高执行的效率。 + +总的来说,**Redis 6.0 之前主要还是单线程处理**。 + + + +## Redis 6.0 引入多线程 + +Redis6.0 引入多线程主要是为了提高网络 IO 读写性能。 + +虽然 Redis6.0 引入了多线程,但是 Redis 的多线程只是在**网络数据的读写这类耗时操作上使用**,执行命令仍然是单线程顺序执行。所以仍然是线程安全的。 + +Redis 6.0 的多线程默认是禁用的,只使用主线程。如需开启需要修改 redis.conf 配置文件: + +```html +io-threads-do-reads yes # 开启多线程 +``` + +开启多线程后,还需要设置线程数,否则是不生效的。同样需要修改 redis.conf 配置文件: + +```html +io-threads 2 # 官网建议 4 核的机器建议设置为2或3个线程,8 核的建议设置为6个线程 +``` + +关于线程数,官方有一个建议:4 核的机器建议设置为 2 或 3 个线程,8 核的建议设置为 6个线程,线程数一定要小于机器核数。 + +### Redis 6.0 多线程的实现机制 + +实现机制流程如下: + +- 主线程负责接收建立连接请求,获取 Socket 放入等待队列 +- 主线程处理完读事件之后,通过轮询调度方法将这些连接分配给这些 IO 线程 +- 主线程阻塞等待 IO 线程读取 Socket 完毕 +- 主线程执行请求命令,请求数据读取并解析完成 +- 主线程阻塞等待 IO 线程将数据回写 Socket 完毕 +- 解除绑定,清空等待队列 + +其中 IO 线程: + +- 要么同时在读 Socket,要么同时在写,不会同时读或写 +- 只负责读写 Socket 解析命令,不负责命令处理 + +
+ +### Redis 6.0 多线程和 Memcached 多线程模型对比 + +相同点:都采用了 Master 线程-Worker 线程模型 + +不同点:Memcached 执行主逻辑也是在 Worker 线程里,模型更加简单,实现了真正的线程隔离,符合我们对线程隔离的常规理解。而 Redis 把处理逻辑交还给 Master 线程,虽然一定程度上增加了模型复杂度,但也解决了线程并发安全等问题。 \ No newline at end of file diff --git "a/docs/Redis/4_\351\224\256\347\232\204\350\277\207\346\234\237\346\227\266\351\227\264\345\222\214\345\206\205\345\255\230\346\267\230\346\261\260\346\234\272\345\210\266.md" "b/docs/Redis/4_\351\224\256\347\232\204\350\277\207\346\234\237\346\227\266\351\227\264\345\222\214\345\206\205\345\255\230\346\267\230\346\261\260\346\234\272\345\210\266.md" new file mode 100644 index 00000000..8b219d63 --- /dev/null +++ "b/docs/Redis/4_\351\224\256\347\232\204\350\277\207\346\234\237\346\227\266\351\227\264\345\222\214\345\206\205\345\255\230\346\267\230\346\261\260\346\234\272\345\210\266.md" @@ -0,0 +1,240 @@ +# 键的过期时间 & 内存淘汰机制 + +## 键的过期时间 + +Redis 可以为每个键设置过期时间,当键过期时,会自动删除该键。 + +对于散列表这种容器,只能为整个键设置过期时间(整个散列表),而不能为键里面的单个元素设置过期时间。 + +### 设置键的过期时间 + +内存是有限的,所以缓存中的数据不能一直保存在内存中,通过为键设置过期时间的方式有助于缓解内存的消耗。此外,在有些业务场景中要求数据只能在某一时间段内有效,比如要求用户登录的 token 只在当天有效,我们只需要将 token 的过期时间设置为 1 天后过期。 + +Redis 中 STRING 类型可以通过 `setex` 命令设置键的过期时间,其他类型需要使用 `expire` 命令来设置键的过期时间。 + +```html +> setex key 60 value # 设置 key 在 60s 后过期(setex:[set] + [ex]pire) +OK +> expire key 60 +(integer) 1 +127.0.0.1:6379> ttl key # ttl 即 time to live,用于查看数据还有多久过期 +(integer) 56 +``` + +### 过期数据删除策略 + +#### 定期删除 + +Redis 默认是每隔 100ms 就**随机抽取一些**设置了过期时间的 key,检查其是否过期,如果过期就删除。 + +Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。但是定期删除可能会导致很多过期 key 到了时间并没有被删除掉。 + +#### 惰性删除 + +获取某个 key 时,Redis 进行过期检查,key 已经过期,就删除。 + +这样对 CPU 最友好,但是也有可能会造成太多过期 key 没有被删除。 + +定期删除对内存更加友好,惰性删除对 CPU 更加友好,所以 Redis 采用的是定期删除+惰性删除。实际上这还是有问题的,如果定期删除漏掉了很多过期 key,又没有及时去获取 key 值,也就没走惰性删除,大量过期 key 堆积在内存里,导致 Redis 内存块耗尽了。为了解决这个问题,Redis 引入**内存淘汰机制**。 + + + +## 内存淘汰机制 + +### LRU(Least Recently Used) + +LRU(Least Recently Used)即最近最久未使用策略,优先淘汰最久未使用的数据,也就是上次被访问时间距离现在最久的数据。该策略可以**保证内存中的数据都是热点数据,也就是经常被访问的数据,从而保证缓存命中率**。 + +#### 双向链表 + HashMap 实现 LRU 算法 + +以下是基于 `双向链表 + HashMap` 的 LRU 算法实现,对算法的解释如下: + +- 访问某个节点时,将其从原来的位置删除,并重新插入到链表头部。这样就能保证链表尾部存储的就是最近最久未使用的节点,当节点数量大于缓存最大空间时就淘汰链表尾部的节点。 +- 为了使删除操作时间复杂度为 O(1),就不能采用遍历的方式找到某个节点。HashMap 存储着 Key 到节点的映射,通过 Key 就能以 O(1) 的时间得到节点,然后再以 O(1) 的时间将其从双向队列中删除。 + +```java +public class LRU implements Iterable { + + private Node head; + private Node tail; + private HashMap map; + private int maxSize; + + private class Node { + + Node pre; + Node next; + K k; + V v; + + public Node(K k, V v) { + this.k = k; + this.v = v; + } + } + + + public LRU(int maxSize) { + + this.maxSize = maxSize; + this.map = new HashMap<>(maxSize * 4 / 3); + + head = new Node(null, null); + tail = new Node(null, null); + + head.next = tail; + tail.pre = head; + } + + + public V get(K key) { + + if (!map.containsKey(key)) { + return null; + } + + Node node = map.get(key); + unlink(node); + appendHead(node); + + return node.v; + } + + + public void put(K key, V value) { + + if (map.containsKey(key)) { + Node node = map.get(key); + unlink(node); + } + + Node node = new Node(key, value); + map.put(key, node); + appendHead(node); + + if (map.size() > maxSize) { + Node toRemove = removeTail(); + map.remove(toRemove.k); + } + } + + + private void unlink(Node node) { + + Node pre = node.pre; + Node next = node.next; + + pre.next = next; + next.pre = pre; + + node.pre = null; + node.next = null; + } + + + private void appendHead(Node node) { + Node next = head.next; + node.next = next; + next.pre = node; + node.pre = head; + head.next = node; + } + + + private Node removeTail() { + + Node node = tail.pre; + + Node pre = node.pre; + tail.pre = pre; + pre.next = tail; + + node.pre = null; + node.next = null; + + return node; + } + + + @Override + public Iterator iterator() { + + return new Iterator() { + private Node cur = head.next; + + @Override + public boolean hasNext() { + return cur != tail; + } + + @Override + public K next() { + Node node = cur; + cur = cur.next; + return node.k; + } + }; + } +} +``` + +#### LinkedHashMap 实现 LRU 算法 + +基于 `LinkedHashMap` ,因为 `LinkedHashMap` 的底层就是双向链表,只需要重写 removeEldestEntry 方法即可。 + +```java +public class LRUCache extends LinkedHashMap { + + private int maxSize; + + /** + * 传递进来最多能缓存多少数据 + * + * @param maxSize 缓存大小 + */ + public LRUCache(int maxSize) { + super(maxSize, 0.75f, true); + this.maxSize = maxSize; + } + + /** + * 如果 map 中的数据量大于设定的最大容量,返回true, + * 再新加入对象时删除最老的数据 + * + * @param eldest 最老的数据项 + * @return true-移除最老的数据 + */ + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + // 当 map中的数据量大于指定的缓存个数的时候,自动移除最老的数据 + return size() > capacity; + } +} +``` + + + +### LFU(Least Frequently Used) + +LFU(Least Frequently Used)即最不经常使用策略,优先淘汰一段时间内使用次数最少的数据。 + + + +### Redis 数据淘汰策略 + +当内存使用量超出设置时,会施行数据淘汰策略。Redis 具体有 6 种数据淘汰策略: + +| 策略 | 描述 | +| :-------------- | :--------------------------------------------------- | +| volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 | +| volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 | +| volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 | +| allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 | +| allkeys-random | 从所有数据集中任意选择数据进行淘汰 | +| noeviction | 禁止驱逐数据 | + +作为内存数据库,出于对性能和内存消耗的考虑,Redis 的淘汰算法实际实现上并非针对所有 key,而是抽样一小部分并且从中选出被淘汰的 key。 + +使用 Redis 缓存数据时,为了提高缓存命中率,需要保证缓存数据都是**热点数据**。可以将内存最大使用量设置为热点数据占用的内存量,然后启用 **allkeys-lru** 淘汰策略,将最近最少使用的数据淘汰。 + +Redis 4.0 引入了 volatile-lfu 和 allkeys-lfu 淘汰策略,LFU 策略通过统计访问频率,将访问频率最少的键值对淘汰。 diff --git "a/docs/Redis/5_\346\214\201\344\271\205\345\214\226\346\234\272\345\210\266.md" "b/docs/Redis/5_\346\214\201\344\271\205\345\214\226\346\234\272\345\210\266.md" new file mode 100644 index 00000000..ded71902 --- /dev/null +++ "b/docs/Redis/5_\346\214\201\344\271\205\345\214\226\346\234\272\345\210\266.md" @@ -0,0 +1,113 @@ +# Redis 持久化机制 + +持久化数据是将内存中的数据写入到硬盘里面,大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了防止系统故障而将数据备份到一个远程位置 + +Redis 是内存型数据库,支持将内存中数据持久化到磁盘。一方面,可保证数据断电后不丢失,另一方面,保存中间结果,不必重新计算。 + +Redis 支持 2 种持久化: + +- RDB (redis database) 持久化 +- AOF (append only file) 持久化 + +## RDB 持久化 + +RDB 持久化即快照持久化,通过创建**快照**来获得 Redis 存储在内存中数据在某个时间点上的副本。在创建快照后,用户可对快照备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本,还可以将快照留在原地以便重启服务器的时候使用。 + +### RDB 持久化配置 + +快照持久化是 Redis 默认采用的持久化方式,可在 Redis.conf 文件中进行配置: + +```html +save 60 1 +# 在60秒(1分钟)之后,如果至少有1个键发生变化,Redis 就会自动触发 BGSAVE 命令创建快照。 + +stop-writes-on-bgsave-error yes +# 表示备份进程出错的时候,主进程就停止接收新的写入操作,是为了保护持久化数据的一致性。 + +rdbcompression no +# RDB 的压缩设置为 no,因为压缩会占用更多的 CPU 资源 +``` + +### BGAVSE 命令 + +客户端向 Redis 发送 BGSAVE 命令来**创建一个快照**。对于支持 BGSAVE 命令的平台来说(基本上所有平台支持,除了 Windows 平台),**Redis 会调用 fork 来创建一个子进程,然后子进程负责将快照写入硬盘,而主进程则继续处理命令请求**。 + +对于如下命令: + +```html +save 60 10000 +``` + +表示从 **Redis 最近一次创建快照之后**算起,满足 “60 s 内 10000 次写入”,Redis 会自动触发 BGSAVE 命令。 + + + +## AOF 持久化 + +AOF 持久化将被执行的写命令写到 AOF 文件末尾,记录数据发生的变化。 + +Redis 值只要从头到尾重新执行一次 AOF 文件包含的所有写命令,就可恢复 AOF 文件所记录的数据。 + +默认情况下,Redis 没有开启 AOF(append only file)方式的持久化,可以通过 appendonly 参数开启: + +```html +appendonly yes +``` + +### AOF 持久化配置 + +```html +appendfsync always # 同步选项 + +auto-aof-rewrite-percentage 100 + +auto-aof-rewrite-min-size 64mb +``` + +### 同步选项 + +使用 AOF 持久化需要设置同步选项,从而确保写命令同步到磁盘文件上的时机。 + +这是因为对文件进行写入并不会马上将内容同步到磁盘上,而是先存储到缓冲区,然后由操作系统决定什么时候同步到磁盘。有以下同步选项: + +| 同步选项 | 同步频率 | 说明 | +| :------: | :--------------------------------------------: | :----------------------------------------------------------: | +| always | 每个 Redis 写命令都要同步写入磁盘 | 当系统发生崩溃时,丢失的数据减到最少;
需要对磁盘大量写入,速度会受到磁盘限制 | +| everysec | 每秒执行一次同步,显示地将多个写命令同步到磁盘 | 性能上和不使用任何持久化特性时相差无几,
可保证数据即使出现系统崩溃,用户也最多只会丢失 1 秒内产生的数据 | +| no | 由操作系统来决定何时进行同步 | 不会对 Redis 性能产生影响,但系统崩溃将导致 Redis 丢失不定量的数据,
若用户写入磁盘处理写入速度不够快,当缓冲区被等待写入磁盘的数据填满时,Redis 写入操作会被阻塞,并导致 Redis 处理命令请求变慢 | + +### 重写/压缩 AOF + +AOF 持久化存在的问题: + +- 随着 Redis 不断运行,AOF 文件的体积会不断增长,占用更多的磁盘空间 +- 若 AOF 文件体积非常大,则恢复的时间可能会比较长 + +为了解决 AOF 文件不断增大的问题,我们采用重写/压缩 AOF 文件的方式: + +用户发送 BGREWRITEAOF 命令,通过**移除 AOF 文件中冗余命令**来重写 AOF 文件来减小 AOF 文件的体积。也就是说,AOF 重写产生了一个新的 AOF 文件,这个新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。 + +BGREWRITEAOF 原理: + +
+ +Redis 维护一个 AOF 重写缓冲区(aof_rewrite_buf)。在子进程创建新的 AOF 文件期间,记录服务器执行的所有写命令。当子进程完成创建新 AOF 文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新 AOF 文件的末尾,使得新旧两个 AOF 文件所保存的数据库状态一致。最后,服务器用新的 AOF 文件替换旧的 AOF 文件,以此来完成 AOF 文件重写操作。 + +## RDB 和 AOF 对比 + +| 持久化方式 | 优点 | 缺点 | +| :--------: | :--------------------------------------------------: | :----------------------------------------------------------: | +| RDB | 文件小,恢复快 | 如果系统发生故障,会丢失最近一次生成快照后写入的数据;
是压缩文件,可读性较差 | +| AOF | 可读性好;
数据不易丢失(丢失 1 秒内产生的数据) | 文件体积大,占用磁盘空间;
恢复时间长 | + + + +## Redis 4.0 优化 + +Redis 4.0 开始支持 RDB和 AOF 的混合持久化。默认是关闭的,需要配置: + +```html +aof-user-rdb-preamble yes +``` + +如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。 diff --git "a/docs/Redis/6_\344\272\213\345\212\241.md" "b/docs/Redis/6_\344\272\213\345\212\241.md" new file mode 100644 index 00000000..81872e19 --- /dev/null +++ "b/docs/Redis/6_\344\272\213\345\212\241.md" @@ -0,0 +1,55 @@ +# Redis 事务 + +## MULTI & EXEC & WATCH + +- MULTI 命令:将客户端从非事务状态切换到事务状态,标志着事务的开始。 +- EXEC 命令:客户端向服务端发送该命令后,服务器会遍历这个客户端的事务队列,并将所有命令的执行结果返回给客户端。 +- WATCH 命令:乐观锁,可以在 EXEC 命令执行之前,监视任意数量的数据库键,并在EXEC 命令执行的时候,检查被监视的键是否至少有一个已经被修改过了,如果有,服务器将拒绝执行该事务,并向客户端返回代表事务执行失败的空回复。 + +```html +> MULTI # 事务开始 +OK +> SET USER "a" +QUEUED +> GET USER +QUEUED +> EXEC # 遍历客户端的事务队列,并将所有命令的执行结果返回给客户端 +1) OK # 设置 user 为 "a" 成功 +2) "a" # 获取 user 数据 +``` + +```html +> WATCH USER # 监视 user +OK +> MULTI +> SET USER "a" # 修改 user 数据 +OK +> GET USER +"a" +> EXEC # user 发生了修改,拒绝执行该事务 +ERR EXEC without MULTI +``` + +## Redis 事务 + +**Redis 通过 MULTI、EXEC、WATCH 等命令来实现事务功能**。 + +事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制。服务器在执行事务期间,不会改去执行其它客户端的命令请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。 + +事务中的多个命令被一次性发送给服务器,而不是一条一条发送,这种方式被称为**流水线 (pipeline)**。 + +注意: + +- Redis 最简单的事务实现方式是使用 MULTI 和 EXEC 命令将事务操作包围起来。 + +- 使用 pineline 的好处:Redis 使用的是客户端 / 服务器(C/S)模型和请求/响应协议的 TCP 服务器。Redis 客户端与 Redis 服务器之间使用 TCP 协议进行连接,一个客户端可以通过一个 Socket 连接发起多个请求命令。每个请求命令发出后客户端通常会阻塞并等待 Redis 服务器处理,Redis 处理完请求命令后会将结果通过响应报文返回给客户端,因此当执行多条命令的时候都需要等待上一条命令执行完毕才能执行。 + + pipeline 可以一次性发送多条命令并在执行完后一次性将结果返回,可以减少客户端与服务器之间的**网络通信次数**从而提升性能,并且 **pineline 基于队列**,而队列的特点是先进先出,这样就保证数据的**顺序性**。 + +## 事务回滚 & 原子性 + +**Redis 是不支持事务回滚的,因而不满足数据库原子性的(而且不满足持久性)。** + +即使事务队列中的某个命令在执行期间出现了错误,整个事务也会继续执行下去,直到将事务队列中的所有命令都执行完毕为止。因为其作者认为,Redis 事务执行时错误通常都是编程错误产生的,这种错误通常只会出现在开发环境中,而很少会在实际的生产环境中出现,所以他任务没有必要开发 Redis 的回滚功能。 + +Redis 事务本质上提供了一种将多个命令请求打包功能,然后按顺序执行打包的所有命令,并且不会被中途打断。 diff --git "a/docs/Redis/7_\347\274\223\345\255\230\351\227\256\351\242\230.md" "b/docs/Redis/7_\347\274\223\345\255\230\351\227\256\351\242\230.md" new file mode 100644 index 00000000..509860c5 --- /dev/null +++ "b/docs/Redis/7_\347\274\223\345\255\230\351\227\256\351\242\230.md" @@ -0,0 +1,134 @@ +# 缓存问题 + +## 缓存雪崩 + +### 问题描述 + +缓存雪崩指的是**一些被大量访问数据(热点缓存)在某一时刻大面积失效**或者**缓存服务器宕机**,导致对应的请求直接到达数据库。 + +在有缓存的系统中,系统非常依赖于缓存,缓存分担了很大一部分的数据请求。当发生缓存雪崩时,数据库无法处理这么大的请求,导致数据库崩溃。 + +### 解决方案 + +针对热点缓存失效的情况: + +- 为了防止缓存在同一时间大面积过期导致的缓存雪崩,可以通过观察用户行为,合理设置缓存过期时间来实现; +- 热点缓存设置为永不失效。 + +针对缓存服务器宕机的情况: + +- 为了防止缓存服务器宕机出现的缓存雪崩,可以使用分布式缓存,分布式缓存中每一个节点只缓存部分的数据,当某个节点宕机时可以保证其它节点的缓存仍然可用; +- 限流,避免同时处理大量的请求。 + +此外,还可以通过 Redis 持久化机制保存的数据快速恢复缓存。 + +## 缓存穿透 + +### 问题描述 + +缓存穿透指的是对某个一定不存在的数据进行请求,该请求将会穿透缓存到达数据库。 + +举个例子:某个黑客故意制造我们缓存中不存在的键,并发起大量请求,这些请求就都落到数据库中,给数据库造成压力。 + +### 解决方案 + +- 参数校验: + + 将一些不合法的参数请求直接抛出异常信息返回给客户端。比如传入的邮箱格式、电话号码长度不对时直接返回错误消息给客户端。 + +- 缓存无效键: + + 如果一个查询返回的数据为空(不管数据是否存在,还是系统故障),我们仍然把这个**空结果进行缓存**,并且设置好过期时间,过期时间最长不超过 5 分钟。这种方式可以解决请求的键变化不频繁的情况。如果遭遇恶意攻击,每次构建不同的请求键,则会导致 Redis 中缓存大量的无效键 。如果要用这种方式来解决缓存穿透问题的话,那么需要尽量将无效的键的过期时间设置短一点比如 1 分钟。 + +- 使用布隆过滤器对这类请求进行过滤: + + 当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。布隆过滤器能够以极小的空间开销解决海量数据判重问题。**一个一定不存在的数据会被该过滤器拦截掉**,从而避免了对底层存储系统的查询压力。 + +### 布隆过滤器 + +布隆过滤器能够以极小的空间开销解决海量数据判重问题,但是会有一定的误判概率:布隆过滤器判断某个元素存在,小概率会误判。布隆过滤器判断某个元素不存在,那么这个元素一定不在。 + +布隆过滤器也是使用 BitSet 存储数据(m 位),但是它进行了一定的改进,从而解除了 BitSet 要求数据的范围不大的限制。在存储时,它要求数据先经过 k 个哈希函数得到 k 个位置,并将 BitSet 中对应位置设置为 1。在查找时,也需要先经过 k 个哈希函数得到 k 个位置,如果所有位置上都为 1,那么表示这个数据存在。 + +由于哈希函数的特点,两个不同的数通过哈希函数得到的值可能相同。如果两个数通过 k 个哈希函数得到的值都相同,那么使用布隆过滤器会将这两个数判为相同。 + +可以令 k 和 m 都大一些会使得误判率降低,但是这会带来更高的时间和空间开销。 + +
+ +补充:[布隆过滤器详解](https://github.com/Snailclimb/JavaGuide/blob/master/docs/cs-basics/data-structure/bloom-filter.md) + +## 缓存击穿 + +### 问题描述 + +缓存击穿是指某个键非常热点,访问非常频繁,处于集中式高并发访问的情况。当缓存的键在某个时间点过期的时,大量的请求就 ”击穿“ 了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。 + +### 解决方案 + +- 将热点数据设置为**永远不过期**,这样不会出现热点键过期问题。 +- 基于 Redis 或 Zookeeper 实现互斥锁。当键获得的值为空时,先加锁,然后从数据库加载数据,加载完毕后再释放锁。其他线程发现获取锁失败,等待一段时间后重试。 + +## 双写一致性问题 + +一致性问题分为**最终一致性**和**强一致性**。数据库和缓存双写,就必然会存在不一致的问题。对数据有强一致性要求,则数据不能放入缓存。我们所做的一切,只能保证最终一致性。另外,我们所做的方案从根本上来说,只是降低双写不一致发生的概率,无法完全避免。 + +### 方案1:先删除缓存,后更新数据库 + +#### 可能存在的问题 + +数据更新,先删除了缓存,然后要去修改数据库数据,此时还没修改。另一个查询请求去读缓存,发现缓存数据为空,则去查询数据库,查到了修改前的旧数据,并放到缓存中。随后更新数据的程序完成了数据库的修改,此时,数据库和缓存中的数据又不一致了。 + +举个例子,线程 A 更新数据 ,线程 B 读取数据,则并发场景下,可能存在如下执行过程: + +- 线程 A 删除了缓存 +- 线程 B 查询,发现缓存数据为空 +- 线程 B 去数据库查询得到旧值 +- 线程 B 将旧值写入缓存 +- 线程 A 将新值写入数据库 + +显然缓存中是旧值,但是数据库中已经是新值了。 + +#### 解决方案 + +将读请求和写请求串行化,串到一个**内存队列**里去,这样就可以保证一定不会出现不一致的情况。串行化之后,就会导致系统的**吞吐量会大幅度的降低**,往往需要使用比正常情况下多几倍的机器去支撑线上的请求。 + +> **问题:为什么是删除缓存,而不是更新缓存?** + +原因:举个例子,一个缓存涉及的表的字段,在 1 分钟内就修改了 100 次,那么缓存更新 100 次;但是这个缓存在 1 分钟内只被**读取**了 1 次,有**大量的冷数据**。实际上,如果只是删除缓存的话,那么在 1 分钟内,这个缓存不过就重新计算一次而已,开销大幅度降低。因此对于频繁更新的场景,如果每次更新数据库,都要更新缓存,倒不如直接删除掉缓存。其实删除缓存,而不是更新缓存,就是一个 lazy 计算的思想:**不要每次都重新做复杂的计算,不管它会不会用到,而是让它在需要被使用时才重新计算**。 + + + +### 方案2:旁路缓存模式 + +旁路缓存模式 (Cache Aside Pattern): + +- 读的时候,先读缓存,缓存中没有数据的话,就读数据库中数据,然后取出数据库中数据放入缓存,同时返回响应。 +- 更新时,先更新数据库,然后再删除缓存。 + +#### 可能存在的问题 + +先修改数据库,再删除缓存。 + +如果**删除缓存失败**,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现不一致。 + +如果在高并发的场景下,也有可能会出现数据库与缓存数据不一致的情况: + +- 缓存**刚好**失效 +- 线程 A 查询数据库,得一个旧值 +- 线程 B 将新值写入数据库 +- 线程 B 删除缓存 +- 线程 A 将查到的旧值写入缓存 + +上述情况,出现的**概率是特别低的**,因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。实际上数据库的写操作会比读操作慢得多,而且还要锁表,**而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存**,所有的这些条件都具备的概率是非常小的。 + +#### 解决方案 + +如果更新数据库成功,而删除缓存失败,有以下两个解决方案: + +- 缩短缓存失效时间:将缓存数据的过期时间设置短一点,这样的话缓存就会从数据库中加载数据。 +- **增加缓存更新重试机制**:如果当前缓存服务不可用导致缓存删除失败的话,可以隔一段时间进行重试,自行设置重试次数。如果多次重试还是失败,则可以把当前更新失败的 键存入队列中,等缓存服务可用之后,再将缓存中对应的键删除即可。 + +### 方案3:读请求和写请求串行化 + +读请求和写请求串行化,串入到一个内存队列。读串行化可保证一定不会出现双写一致性的情况,但会导致系统吞吐量大幅降低,需要比正常情况下多几倍的机器去支持线上一个请求,若不是严格要求双写一致性,则最好不要使用。 diff --git "a/docs/Redis/8_\351\203\250\347\275\262\346\226\271\345\274\217.md" "b/docs/Redis/8_\351\203\250\347\275\262\346\226\271\345\274\217.md" new file mode 100644 index 00000000..744745c6 --- /dev/null +++ "b/docs/Redis/8_\351\203\250\347\275\262\346\226\271\345\274\217.md" @@ -0,0 +1,350 @@ +Redis 有以下 4 种部署方式: + +# 一、单机模式 + +最基本的部署方式,只需要一台机器负责读写,一般只用于开发人员开发自测。 + +# 二、主从复制模式 + +对于缓存来说,一般都是用来支撑**读高并发**的,主从(Master-Slave)模式架构,一主多从,其中 Master 负责写,并且将数据复制到其它的 Slave 节点,而 Slave 节点负责读。所有的**读请求全部访问 Slave节点**,这样也可以很轻松实现水平扩容,支撑读高并发。 + +单机的 Redis,能够承载的 QPS 大概就在上万到几万不等。对于主从架构,增加 Slave 节点的数量可以使得 Redis的 QPS 达到 10 万以上。 + +
+ +## 主从同步方法 + +Master 节点接收到写请求并处理后,需要告知 Slave 节点数据发生变化,保证主从节点数据一致即为主从同步。主从同步方法有:增量同步、快照同步和无盘复制。 + +### 增量同步 + +Redis 同步的是指令流,Mater 节点会将对自己的状态产生修改的指令记录在本地 buffer 中,然后异步将 buffer 中的指令同步到 Slave 节点,Slave 节点一边执行同步的指令流来达到和 Master 节点一样的状态,一边向主节点反馈同步偏移量。 + +Slave 节点同步数据的时候不会影响 Master 节点的正常工作,也不会影响自己对外提供读服务的功能,Slave 节点会用旧的数据来提供服务,当同步完成后,需要删除旧数据集,加载新数据,此时会暂停对外服务。 + +因为内存的 buffer 是有限的,所以 Master 节点不能将所有的指令都记录在内存 buffer 中。Redis 的复制内存 buffer 是一个定长的环形数组,如果数组内容满了,就会从头开始覆盖前面的内容。 + +### 快照同步 + +节点间网络通信不好,当 Slave 节点同步的速度不如 Master 节点接收新写请求的速度时,buffer 中会丢失一部分指令,Slave 节点中的数据将与 Master 节点中的数据不一致,此时将会触发快照同步。 + +快照同步先在 Master 节点执行 bgsave 生成 rdb 快照文件,然后将快照文件的内容全部传送到 Slave 节点。Slave 节点将快照文件接受完毕后,立即执行一次**全量加载**,加载前会将当前内存数据清空。加载完毕后通知 Master 节点继续进行增量同步。 + +在整个快照同步进行的过程中,Master 节点的 buffer 仍在往前移动,如果快照同步的时间过长或者 buffer 太小,同步期间的增量指令在 buffer 中被覆盖,这样就会导致快照同步完成后无法进行增量复制,然后会再次发起快照同步,如此极有可能会陷入快照同步的死循环。所以需要配置一个合适的 buffer 大小参数,避免快照复制的死循环。 + +
+ +### 无盘复制 + +在进行快照同步时,存在大量的文件 IO 操作,特别是对于非 SSD 磁盘存储时,快照会对系统的负载产生较大影响。特别是当系统正在进行 AOF 的 fsync 操作时如果发生快照复制,fsync 将会被推迟执行,这就会严重影响 Master 节点的服务效率。 + +从 Redis 2.8.18 版开始支持无盘复制。无盘复制指的是 Master 节点会**一边遍历内存,一遍将序列化的内容发送给 Slave 节点,而不是生成完整的 rdb 快照文件后才进行 IO 传输**。Slave 节点还是跟之前一样,先将接收到的内容存储到磁盘文件中,再进行一次性加载。 + +## 主从同步完整流程 + +### 1. 建立连接 + +- Slave 节点配置文件中的 slaveof 配置项中配置 Master 节点的 IP 和 port 后, Slave 节点就知道自己要和那个 Master 节点进行连接。 +- Slave 节点内部有个定时任务,会每秒检查自己要连接的 Master 节点是否上线,如果发现 Master 节点上线,就跟 Master 节点进行网络连接,此时还没有进行主从同步。 +- Slave 节点发送 ping 命令给 Master 节点进行连接,如果设置了口令认证(Master 节点设置了requirepass),那么 Slave 节点必须发送正确的口令(masterauth)进行认证。 + +### 2. 快照同步 + +- Master 和 Slave 节点连接成功后,判断是否进行快照同步:如果 Slave 节点发现已经连接过某个 `run id` 的 Master 节点,则此次连接为重新连接,不会进行快照同步。相同 IP 和 port 的 Master 节点每次重启服务都会生成一个新的 `run id`,所以每次 Master 节点重启服务都会进行一次快照同步,如果想重启主节点服务而不改变 `run id`,使用 `redis-cli debug reload` 命令。 + +- 进行快照同步开始后, Master 节点在本地生成一份 rdb 快照文件,并将这个 rdb 文件发送给 Slave 节点,如果复制时间超过 60 秒(配置项:`repl-timeout`),那么就会认为复制失败,所以数据量比较大,要适当调大这个参数的值。 + + 进行快照同步时,Master 节点会把接收到的新请求命令写到 buffer 中,当快照同步完成后,再把 buffer 中的指令增量同步到 Slave 节点。如果在快照同步期间,内存缓冲区大小超过 64MB / 256MB 的状态持续时间超过60s(配置项:`client-output-buffer-limit slave 256MB 64MB 60`),那么快照同步失败。 + +- Slave 节点接收到 rdb 快照文件后,清空自己的旧数据,然后重新加载 rdb 到自己的内存中,在这个过程中基于旧的数据对外提供服务。如果 Master 节点开启了AOF,那么在快照同步结束后会立即执行 BGREWRITEAOF 命令,重写 AOF 文件。 + +- Master 节点维护了一个 backlog 文件,默认大小是 1MB 。Master 节点向 Slave 节点发送全量数据(即 rdb 快照文件)时,也会同步到 backlog 中,这样当发送全量数据这个过程意外中断后,从 backlog 文件中可以得知数据有哪些是发送成功了,哪些还没有发送,然后当主从节点再次连接后,从失败的地方开始增量同步。**当快照同步连接中断后,主从节点再次连接并非是第一次连接,进行增量同步,而不是继续进行快照同步**。 + +### 3. 增量同步 + +- 快照同步完成后,Master 节点后续接收到写请求导致数据变化后,将和 Slave 节点进行增量同步,遇到 buffer 溢出则会再触发快照同步。 + +## 主从同步补充 + +- 主从节点都会维护一个 offset,随着 Master 节点的数据变化以及主从同步的进行,主从节点会不断累加自己维护的 offset,Slave 节点每秒都会上报自己的 offset 给 Master 节点,Master 节点也会保存每个 Slave 节点的 offset,这样主从节点就能知道互相之间的数据一致性情况。 + + Slave 节点发送`psync runid offset`命令给 Master 节点从而开始主从同步,Master 节点会根据自身的情况返回响应信息,可能是`FULLRESYNC runid offset`触发快照复制,也可能是 `CONTINUE` 触发增量复制。 + +- 主从节点因为网络原因导致断开,当网络接通后,不需要手工干预,可以自动重新连接。 + +- Master 节点如果发现有多个 Slave 节点连接,在快照同步过程中仅仅会生成一个 red 快照文件,用一份数据服务所有 Slave 节点进行快照同步。 + +- Slave 节点不会处理过期 key,当 Slave 节点处理了一个过期 key,会模拟一条 del 命令发送给 Slave 节点。 + +- 主从节点会保持心跳(heartbeat)来检测对方是否在线,Master 节点默认每隔 10 秒发送一次 heartbeat,Slave 节点默认每隔 1 秒发送一个heartbeat。 + +- 建议 Master 节点使用 AOF+RDB 的持久化方式,并且在 Master 节点定期备份 red 快照文件,而 Slave 节点不要开启 AOF 机制,原因有两个: + + - Slave 节点开启 AOF 会降低性能 + - 如果 Master 节点数据丢失,数据同步给 Slave 节点后,Slave 节点会收到空数据,如果开启了 AOF,会生成空的 AOF 文件,基于 AOF 恢复数据后,全部数据就都丢失了,而如果不开启 AOF 机制, Slave 节点启动后,基于自身的 rdb 快照文件恢复数据,不至于丢失全部数据。 + +扩展:[主从模式实战](https://www.jianshu.com/p/f0e042b95249) + + + +# 三、哨兵模式 + +在 Redis 主从复制模式下,一旦 Master 节点由于故障不能提供服务,需要**人工**将 Slave 节点晋升为 Master 节点,再通知所有的程序把 Master 地址统统改一遍,然后重新上线。 + +Redis 从 2.8 开始正式提供了 **哨兵 (sentinel) 架构**来解决这个问题。 + +Redis 中多个 sentinel 组成**分布式架构**,持续监控主从节点的健康状况,当 Master 节点挂掉时,自动选择一个最优的 Slave 节点切换为 Master 节点。客户端在连接集群时,会先连接 sentinel,通过 sentinel 来查询 Master 节点的地址,然后再去连接 Master 节点进行数据交互。当 Master 节点发生故障时,客户端会重新向 sentinel 获取地址,sentinel 会将最新的 Master 节点地址告诉客户端,这样应用程序将无需重启即可自动完成节点切换。 + +
+ +sentinel 集群在 Redis 主从架构高可用中起到的 4 个作用: + +- 集群监控 + sentinel 节点会定期检测 Redis 数据节点、其余 sentinel 节点是否故障。 +- 故障转移 + 实现 Slave 节点晋升为 Master 节点并维护后续正确的主从关系。 +- 配置中心 + sentinel 架构中,客户端在初始化的时候连接的是 sentinel 集群,从中获取 Master 节点信息。 +- 消息通知 + sentinel 节点会将故障转移的结果通知给客户端。 + +注意:使用 sentinel 集群而不是单个 sentinel 节点去监控 Redis 主从架构,这是因为: + +- 对于节点的故障判断由多个 sentinel 节点共同完成,这样可**有效防止误判**。 +- sentinel 集群可以保证自身的**高可用性**,即某个 sentinel 节点自身故障也不会影响 sentinel 集群的健壮性。 + +## 监控功能 + +sentinel 集群通过 3 个定时监控任务完成对各个节点发现和监控: + +- 每隔10 秒,每个 sentinel 节点会向 Master 节点和 Slave 节点发送 info 命令获取 Redis 主从架构的最新情况。 +- 每隔 2 秒,每个 sentinel 节点会向 Redis 数据节点的 `__sentinel__:hello` 这个channel(频道)发送一条消息,该定时任务可以完成以下 2 个工作: + - 发现新的 sentinel节点 + - sentinel 节点之间交换 Master 节点的状态,用于确认 Master 下线和故障处理的 Leader 选举。 +- 每隔 1 秒,每个 sentinel 节点会向 Master 节点、Slave 节点、其余 sentinel 节点发送一条 ping 命令做一次心跳检测,来确认这些节点是否可达。 + +## sdown & odown + +sdown 即主观下线 (subjective down) :一个 sentinel 节点每隔 1 秒对 Master 节点、Slave 节点、其他 sentinel 节点发送 ping 命令做心跳检测,当某个节点超过 + `down-after-milliseconds` 没有进行有效回复,该 sentinel 节点就会认为该节点下线,这就是主观下线。 + +odown 即客观下线 (objective down) :超过 quorum 数量(quorum可配置)的 sentinel 节点认为 Master 节点确实有问题,即大部分是 sentinel 节点都对 Master 节点的下线做了同意的判定,这就是客观下线。 + +## quorum & majority + +只有大于等于 quorum 数量的 sentinel 节点都认为 Master 主观下线,sentinel 集群才会认为 Master 客观下线。 + +quorum 可在 sentinel.conf 中手动配置,默认值为 2: + +```conf +# sentinel monitor [master-name] [master-ip] [master-port] [quorum] +sentinel monitor testmaster 127.0.0.1 6379 2 +# quorum 默认值是 2 +``` + +majority 代表 sentinel 集群中大部分 sentinel 节点数,majority 的计算方法为:majority = num(sentinels) / 2 + 1。例如 2 个节点的 sentinel 集群的 majority 为 2 + 3 个节点的 sentinel 集群的 majority 为 2;4 个节点的 sentinel 集群的 majority为 3。 + +只有大于等于 max(quorum, majority) 个节点给某个 sentinel 节点投票,该 sentinel 节点才能被选为 Leader。 + +注意:sentinel 集群的节点个数至少为 3 个。 + +原因如下:假设当节点数为 2 时,此时一个 sentinel 节点宕机,那么剩余一个节点是无法让自己成为 Leader 的,因为 2 个节点的 sentinel 集群的 majority 是 2,此时没有 2 个节点都给剩余的节点投票,也就无法选择出 Leader,从而无法进行故障转移。 + +此外最好把 quorum 的值设置为小于等于 majority,否则即使 sentinel 集群剩余的节点满足 majority 数,但是有可能不能满足 quorum 数,那还是无法选举 Leader,也就不能进行故障转移。 + +## Leader 选举 + +当 sentinel 集群确认某个 Master 是客观下线了,需要选举出一个 **Leader 节点来进行故障转移**,选举过程如下: + +- 每个在线的 sentinel 节点都有资格成为 Leader,当某个 sentinel 节点确认 Master 节点 odown 时,会向其他 sentinel 节点发送 `sentinel is-master-down-by-addr`命令,要求将自己设置为 Leader。 +- 每个 sentinel 节点都只能投出一票,如果该 sentinel 节点得到的**票数大于 max(quorum, num(sentinels) / 2 + 1)** ,那么该 sentinel 节点成为 Leader。 +- 如果一次选举没有选举出 Leader,那么会进行下一次选举。 + +一般情况下,哪个 sentinel 节点最先确认 Master 客观下线,该 sentinel 节点就会成为执行故障转移的 Leader。 + +## 新 Master 选择 + +要执行故障转移,首先要从 Slave 中选择一个作为新的 Master。 + +不选择不健康的 Slave,以下状态的 slave 是不健康的: + +- 主观下线的 Slave +- 与 Master 失联超过`down-after-milliseconds * 10`秒的 Slave +- 大于等于 5 秒没有回复过 sentinel 节点 ping 响应的 Slave + +对健康的 slave 进行排序: + +- 选择 priority(可配置,默认100)最低的 Slave 节点,如果有优先级相同的节点,进行下一步。注意如果 priority=0,那么禁止该节点成为 Master +- 选择复制偏移量最大的 Slave 节点(复制的最完整),如果有复制偏移量相等的节点,进行下一步 +- 选择 runid 最小的 Slave 节点 + +## 故障转移 + +选举出 Leader 和新 Master 后,由 Leader 进行故障转移: + +- Leader 对选举出的新 Master(此时还是 Slave)执行 `slaveof no one` 命令让其成为 Master。 +- Leader 向剩余 Slave 发送命令,使它们称为新 Master 的 Slave。 +- Leader 会将原来的 Master 更新为 Slave,并保持着对其关注,当其恢复后命令它去复制新 Master。 + +## configuration epoch + +configuration epoch 是当前 Redis 主从架构的配置版本号,无论是 sentinel 集群选举 Leader 还是进行故障转移的时候,要求各 sentinel 节点得到的 configuration epoch 都是相同的,`sentinel is-master-down-by-addr` 命令中就必须有当前配置版本号这个参数,在选举 Leader 过程中,如果本次选举失败,那么进行下一次选举,就会更新配置版本号,也就是说,每次选举都对应一个新的 configuration epoch,在故障转移的过程中,也要求各个 sentinel 节点使用相同的 configuration epoch。 + +在故障转移成功之后,Leader 会更新生成最新的 master 配置,configuration epoch 也会更新,然后同步给其他的 sentinel 节点,这样保证 sentinel 集群中保存的 Master 和 Slave 配置都是最新的,当 Client 请求的时候就会拿到最新的配置信息。 + +## 数据丢失问题 + +### 数据丢失原因 + +Redis sentinel 可能会导致数据丢失: + +- 异步复制导致的数据丢失 + + Master 到 Slave 的复制是异步的,所以可能有部分数据还没复制到 Slave,Master 就宕机了,此时这部分数据就丢失了。 + +- 脑裂导致的数据丢失 + + 某个 Master 所在机器突然网络故障,跟其他 Slave 机器不能连接,但是实际上 Master 还运行着。sentinel 会认为该 Master 宕机了,然后开启选举,将其他 Slave 切换成了 Master,集群里就会有两个 Master,也就是所谓的脑裂。 + + 虽然某个 Slave 被切换成了 Master,但是 Client 还没来得及切换到新的 Master,还继续写向旧 Master 。旧 Master 再次恢复的时候,会被作为一个 Slave 挂到新的 Master 上去,自己的数据会清空,重新从新的 Master 复制数据,而新的 Master 并没有后来 Client 写入的数据,这部分数据就丢失了。 + +### 解决方案 + +Redis 提供了两个配置参数可以尽量丢失少的数据: + +```conf +min-slaves-to-write 1 +min-slaves-max-lag 10 +``` + +Master 至少有 1 个 slave,数据复制和同步的延迟不能超过 10 秒。如果说一旦所有的 Slave,数据复制和同步的延迟都超过了 10 秒钟,那么 Master 就不会再接收任何请求。 + +- 减少异步复制数据的丢失 + + `min-slaves-max-lag` 配置可确保一旦 Slave 复制数据和 ack 延时太长,就认为可能 Master 宕机后损失的数据太多了,那么就拒绝写请求,这样可以把 Master 宕机时由于部分数据未同步到 Slave 导致的数据丢失降低的可控范围内。 + +- 减少脑裂的数据丢失 + + 如果一个 Master 出现了脑裂,跟其他 Slave 丢了连接,那么上面两个配置可以确保如果不能继续给指定数量的 Slave 发送数据且 Slave 超过 10 秒没有给自己 ack 消息,那么就直接拒绝客户端的写请求。因此在脑裂场景下,最多就丢失 10 秒的数据。 + +注意:redis-2.6 版本提供的是 redis sentinel v1 版本,但是功能性和健壮性都有一些问题。如果想使用 redis sentinel 的话,建议使用 2.8 以上版本。 + +扩展:[哨兵模式实战](https://www.jianshu.com/p/9d873e7a205a) + + + +# 四、Cluster 集群模式 + +## Redis Cluster 架构 + +单 Master 架构中 Master 节点的数据和 Slave 节点的数据是一模一样的,Master 节点的最多能容纳多少数据量,Slave 节点也就只能容纳这么多数据。当数据量超过 Master 的内存,Redis 会使用 LRU 算法清除部分数据。如果确实要容纳更多的数据量,Redis 主从架构是无法解决这个问题的。 + +Redis Cluster 是 Redis 的**分布式解决方案**,Redis 3.0 正式推出,解决单Master 架构的内存、并发、流量等瓶颈,以达到负载均衡的目的。 + +
+ +Redis cluster 适用于海量数据、高并发、高可用场景,在 Redis Cluster 架构中: + +- 每个 Master 负责整个集群的一部分数据,每个节点负责数据量可能不一样 +- 每个 Master 的角色是对等的,每个 Master 节点都可以有多个 Slave 节点,当一个 Master 节点挂掉后,它的其中一个 Slave 节点升级为 Master +- Redis Cluster 已经自动具备了主从复制能力,直接集成了 replication 和 sentinel 的功能。 + +## 分布式寻址算法 + +### Hash 算法 + +#### 思路 + +根据 key 计算 Hash 值,然后对节点数取模,将 key 分配到对应的 Master 中。 + +#### 问题 + +一旦某个 Master 宕机,就需要调整 hash 算法,所有的数据都需要重新计算取模,然后重新分配数据。大部分的请求都无法正确的拿到数据,从而不得不去访问数据库,在高并发场景下,这样是不能接受的,所以目前分布式缓存不再使用此种算法分配数据。 + +### 一致性 Hash 算法 + +#### 思路 + +将哈希空间 [0, 2^n-1] 看成一个哈希环,每个服务器节点都配置到哈希环上。每个数据对象通过哈希取模得到哈希值之后,存放到哈希环中**顺时针方向**第一个大于等于该哈希值的节点上。 + +例如有 Object A、Object B、Object C、Object D 四个数据对象,经过哈希计算后,在哈希环上的位置如下: + +
+ +根据一致性 Hash 算法:Object A 会被定位到 Node A 上;Object B 会被定位到 Node B 上;Object C 会被定位到 Node C 上;Object D 会被定位到 Node D 上。 + +一致性哈希在增加或者删除节点时只会影响到哈希环中相邻的节点: + +例如删除节点 C,只需将 C 上的数据分布到节点 D 即可。 + +
+ +例如新增节点 X,只需要将它前一个节点 C 上的数据重新进行分布即可,对于节点 A、B、D 都没有影响。 + +
+ +综上所述,一致性 Hash 算法对于节点的增减都只需**重定位哈希环中的一小部分数据**,具有**较好的容错性和可扩展性**。 + +#### 问题 + +- 加减节点会造成哈希环中部分数据无法命中,当一个 Master 挂掉之后,例如节点 A 挂掉,那么当请求从 A 获取数据时,是获取不到的,于是继续顺时针去节点 B、节点 C、节点 D 获取数据,当然也是获取不到的,需要手动处理这些无法命中的数据 +- 节点数量越多,增减节点带来的影响越小,因此不适用与集群中只有少量节点的情况 +- 容易造成数据热点问题 + +#### 优化-虚拟节点 + +一致性哈希存在数据分布不均匀的问题,节点存储的数据量有可能会存在很大的不同。数据不均匀主要是因为节点在哈希环上分布的不均匀,这种情况在节点数量很少的情况下尤其明显。 + +
+ +解决方式是通过增加虚拟节点,然后将虚拟节点映射到真实节点上。虚拟节点的数量比真实节点来得多,那么虚拟节点在哈希环上分布的均匀性就会比原来的真实节点好,从而使得数据分布也更加均匀。 + +例如针对上面的情况,可以为每台服务器计算 3 个虚拟节点: + +- Node A 的 3 个虚拟节点:"Node A#1"、"Node A#2"、"Node A#3" +- Node B 的 3 个虚拟节点:"Node B#1"、"Node B#2"、"Node B#3" + +
+ +同时**数据定位算法不变**,只是多了一步虚拟节点到实际节点的映射过程,例如"Node A#1"、"Node A#2"、"Node A#3" 这 3 个虚拟节点的数据均定位到 Node A 上,解决了服务节点少时数据倾斜的问题。在实际应用中,通常将虚拟节点数设置为 32 甚至更大,因此即使很少的服务节点也能做到相对均匀的数据分布。 + +### hash slot 算法 + +#### 思路 + +Redis Cluster **使用 16384 个槽 (slot)** 来管理一段整数集合 (哈希值),slot 是集群内数据管理和迁移的基本单位。 + +每个 Master 节点负责管理一部分 slot,例如有 5 个节点,那个每个节点管理大约 3276 (16384 / 5 )个槽。对每个 key 使用 CRC32 算法进行哈希得到哈希值,使用哈希值对 16384 进行取模,得到此数据应该分配到的 slot 编号。每个 Master 管理的 slot 的信息就缓存在本地,客户端连接集群时,会获取集群 slot 配置信息,从而通过 key 精确找到 slot 所在的节点,此外还可以强制分配某个 key 到指定的 slot 上。 + +#### 优点 + +- 解耦数据和节点之间的关系,简化了节点扩容和收缩难度: + + 增加一个 Master,就让其他的 Master 分一部分 slot 给新的 Master 管理; + + 移除一个 Master,就把这个 Master 管理的 slot 分配给其他的 Master。 + +- 某个 Master 挂掉,不影响整个集群,因为请求是到 slot,而不是到 Master,但在 slot 迁移完成之前,请求到挂掉的节点也是不行的。 + +- slot 迁移的过程是很快的 + +- 节点自身维护 slot 的映射关系,无需人为管理 + +- 支持槽、节点、key之间的映射关系查询 + +### Redis 故障恢复 + +判断故障的逻辑其实与哨兵模式有点类似,在集群中,每个节点都会**定期的向其他节点发送 ping 命令**,通过有没有收到回复来判断其他节点是否已经下线。 + +如果**长时间没有回复,那么发起 ping 命令的节点就会认为目标节点疑似下线**,也可以和哨兵一样称作主观下线,当集群中超过半数的节点都认为该节点下线则判定该节点下线。 + +对宕机的 Master 节点,从其所有的 Slave 节点中选择一个切换成 Master,需要检查每个 Slave 节点与 Master 节点断开连接的时间,如果超过了 `cluster-node-timeout * cluster-slave-validity-factor` ,那么该 Slave 就没有资格切换成 Master。 + +每个 Slave 节点,都根据自己对 Master 复制数据的 offset,来设置一个选举时间,offset 越大(复制数据越多)的 Slave 节点,选举时间越靠前,优先进行选举。 + +所有的 Master 节点给要进行选举的 Slave 进行投票,如果超过半数的 Master 节点都投票给了某个 Slave 节点,那么选举通过,那个该 Slave 节点可以切换成 Master。 + +扩展:[Redis Cluster搭建及测试](https://www.jianshu.com/p/48713d6f35b8) + +补充:[Redis-简书](https://www.jianshu.com/nb/33672761) diff --git "a/docs/Redis/9_\345\256\236\346\210\230.md" "b/docs/Redis/9_\345\256\236\346\210\230.md" new file mode 100644 index 00000000..3759be90 --- /dev/null +++ "b/docs/Redis/9_\345\256\236\346\210\230.md" @@ -0,0 +1,32 @@ +# Redis 实战——实现简单点赞系统 + +该点赞系统功能如下: + +- 可以发布文章 +- 可以对文章进行点赞 +- 在首页可以按文章的发布时间或者文章的点赞数进行排序显示 + +## 文章信息存储 + +文章包括标题、作者、赞数等信息,在关系型数据库中很容易构建一张表来存储这些信息,在 Redis 中可以使用 HASH 来存储每种信息以及其对应的值的映射。 + +
+ +## 点赞功能实现 + +当有用户为一篇文章点赞时,除了要对该文章的 votes 字段进行加 1 操作,还必须记录该用户已经对该文章进行了点赞,防止用户点赞次数超过 1。可以建立文章的已投票用户集合来进行记录。 + +为了节约内存,规定一篇文章发布满一周之后,就不能再对它进行投票,而文章的已投票集合也会被删除,可以为文章的已投票集合设置一个一周的过期时间就能实现这个规定。 + +
+ +## 对文章进行排序 + +为了按发布时间和点赞数进行排序,可以建立一个文章发布时间的有序集合和一个文章点赞数的有序集合。(下图中的 score 就是这里所说的点赞数;下面所示的有序集合分值并不直接是时间和点赞数,而是根据时间和点赞数间接计算出来的) + +
+ +115423 号用户给 100408 号文章投票的时候,数据结构发生的变化: + +
+ diff --git "a/docs/Regex/1_\346\246\202\350\277\260.md" "b/docs/Regex/1_\346\246\202\350\277\260.md" new file mode 100644 index 00000000..6a71e46f --- /dev/null +++ "b/docs/Regex/1_\346\246\202\350\277\260.md" @@ -0,0 +1,59 @@ +# 概述 + +## 正则表达式 + +是指一个用来描述或者匹配一系列符合某个句法规则的字符串的单个字符串。 + +其实就是**一种规则**。 + +## 组成规则 + +规则字符在java.util.regex Pattern类中:[Pattern API](https://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html) + +## 常见组成规则 + +### 1. 字符 + +| 元字符 | 说明 | +| :----: | :-----------------------: | +| x | 字符 x | +| \ | 反斜线字符 | +| \n | 新行(换行)符 ('\u000A') | +| \r | 回车符 ('\u000D') | + +### 2. 字符类 + +| 元字符 | 说明 | +| :------: | :----------------------------------------: | +| [abc] | a、b 或 c(简单类) | +| [^abc] | 任何字符,除了 a、b 或 c(否定) | +| [a-zA-Z] | a到 z 或 A到 Z,两头的字母包括在内(范围) | +| [0-9] | 0到9的字符都包括 | + +### 3. 预定义字符类 + +| 元字符 | 说明 | +| :----: | :--------------------------: | +| . | 任何字符 | +| \d | 数字。等价于[0-9] | +| \w | 单词字符。等待雨[a-zA-Z_0-9] | + +### 4. 边界匹配器 + +| 元字符 | 说明 | +| :----: | :------: | +| ^ | 行的开头 | +| $ | 行的结尾 | +| \b | 单词边界 | + +### 5. 数量词 + +| 元字符 | 说明 | +| :----: | :---------------------------: | +| X? | X,零次或一次 | +| X* | X,零次或多次 | +| X+ | X,一次或多次 | +| X{n} | X,恰好 n 次 | +| X{n,} | X,至少 n 次 | +| X{n,m} | X,至少 n 次,但是不超过 m 次 | + diff --git "a/docs/Regex/2_\345\272\224\347\224\250.md" "b/docs/Regex/2_\345\272\224\347\224\250.md" new file mode 100644 index 00000000..992b8b8e --- /dev/null +++ "b/docs/Regex/2_\345\272\224\347\224\250.md" @@ -0,0 +1,114 @@ +# 应用 + +## 判断功能 + +String 类 + +```java +public boolean matches(String regex) +``` + +```java +/** +* 判断手机号码是否满足要求 +*/ +public static void main(String[] args) { + //键盘录入手机号码 + Scanner sc = new Scanner(System.in); + System.out.println("请输入你的手机号码:"); + String phone = sc.nextLine(); + + //定义手机号码的规则 + String regex = "1[38]\\d{9}"; + + //调用功能,判断即可 + boolean flag = phone.matches(regex); + + //输出结果 + System.out.println("flag:"+flag); +} +``` + + + +## 分割功能 + +String类的 + +```java +public String[] split(String regex) +``` + +```java +/** +* 根据给定正则表达式的匹配拆分此字符串 +*/ +public static void main(String[] args) { + //定义一个年龄搜索范围 + String ages = "18-24"; + + //定义规则 + String regex = "-"; + + //调用方法 + String[] strArray = ages.split(regex); + int startAge = Integer.parseInt(strArray[0]); + int endAge = Integer.parseInt(strArray[1]); +} +``` + + + +## 替换功能 + +String 类的 + +```java +public String replaceAll(String regex,String replacement) +``` + +```java +/** +* 去除所有的数字 +*/ +public static void main(String[] args) { + // 定义一个字符串 + String s = "helloqq12345worldkh622112345678java"; + + + // 直接把数字干掉 + String regex = "\\d+"; + String ss = ""; + + String result = s.replaceAll(regex, ss); + System.out.println(result); +} +``` + + + +## 获取功能 + +Pattern和Matcher类的使用 + +```java +/** +* 模式和匹配器的基本顺序 +*/ +public static void main(String[] args) { + // 模式和匹配器的典型调用顺序 + // 把正则表达式编译成模式对象 + Pattern p = Pattern.compile("a*b"); + // 通过模式对象得到匹配器对象,这个时候需要的是被匹配的字符串 + Matcher m = p.matcher("aaaaab"); + // 调用匹配器对象的功能 + boolean b = m.matches(); + System.out.println(b); + + //这个是判断功能,但是如果做判断,这样做就有点麻烦了,我们直接用字符串的方法做 + String s = "aaaaab"; + String regex = "a*b"; + boolean bb = s.matches(regex); + System.out.println(bb); +} +``` \ No newline at end of file diff --git "a/docs/Safety/10_\344\277\241\346\201\257\346\263\204\351\234\262.md" "b/docs/Safety/10_\344\277\241\346\201\257\346\263\204\351\234\262.md" new file mode 100644 index 00000000..5245ab48 --- /dev/null +++ "b/docs/Safety/10_\344\277\241\346\201\257\346\263\204\351\234\262.md" @@ -0,0 +1,53 @@ +# 信息泄露 + +## 常见信息泄露问题 + +- 泄露系统敏感信息 +- 泄露用户敏感信息 +- 泄露用户密码 + +## 信息泄露的途径 + +- 错误信息失控 +- SQL 注入 +- 水平权限控制不当 +- XSS +- CSRF + +## 社会工程学 + +社会工程学(Social Engineering),是一种通过人际交流的方式获得信息的**非技术渗透手段**。 + +不幸的是,这种手段非常有效,而且应用效率极高。事实上,社会工程学已是企业安全最大的威胁之一。 + +
+ +## OAuth + +### 概念 + +OAuth(Open Authorization) 协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是 OAuth 的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此 OAuth 是安全的。 + +### 思想 + +- 一切行为由用户授权 +- 授权行为不泄露敏感信息 +- 授权会过期 + +### 流程 + +1. 登录 +2. 敏感资料模块派发票据给用户 +3. 用户带票据请求业务模块 +4. 业务使用票据请求敏感资料模块 +5. 返回票据对应数据 +6. 返回数据 + +
+ +特点: + +- 用户授权读取资料 +- 无授权的资料不可读取 +- 不允许批量获取数据 +- 数据结构可风控审计 \ No newline at end of file diff --git "a/docs/Safety/11_DoS\346\224\273\345\207\273.md" "b/docs/Safety/11_DoS\346\224\273\345\207\273.md" new file mode 100644 index 00000000..6428706c --- /dev/null +++ "b/docs/Safety/11_DoS\346\224\273\345\207\273.md" @@ -0,0 +1,30 @@ +# DoS 攻击 + +## 概念 + +拒绝服务攻击(denial-of-service attack,DoS),亦称洪水攻击,模拟正常用户使目标电脑的网络或系统资源耗尽,使服务暂时中断或停止,导致正常用户无法访问。 + +## 类型 + +- TCP 半连接 +- HTTP 连接 +- DNS + +## DDoS + +分布式拒绝服务攻击(distributed denial-of-service attack,DDoS),指攻击者使用两个或以上被攻陷的电脑作为“僵尸”向特定的目标发动“拒绝服务”式攻击。 + +## 防御 + +- 防火墙 +- 交换机、路由器 +- 流量清洗 +- 高仿 IP + +## 预防 + +- 避免重逻辑业务 +- 快速失败快速返回 +- 防雪崩机制 +- 有损服务 +- CDN diff --git "a/docs/Safety/12_\351\207\215\346\224\276\346\224\273\345\207\273.md" "b/docs/Safety/12_\351\207\215\346\224\276\346\224\273\345\207\273.md" new file mode 100644 index 00000000..4e5a375d --- /dev/null +++ "b/docs/Safety/12_\351\207\215\346\224\276\346\224\273\345\207\273.md" @@ -0,0 +1,16 @@ +# 重放攻击 + +## 概念 + +重放攻击指攻击者发送一个目的主机已接收过的包,来达到欺骗系统的目的,主要用于身份认证过程,破坏认证的正确性。 + +重放攻击可以由发起者,也可以由拦截并重发该数据的敌方进行。攻击者利用网络监听或者其他方式盗取认证凭据,之后再把它重新发给认证服务器。 + +## 防御 + +- HTTPS +- 时间戳 +- token +- nonce(number once) +- 签名 + diff --git "a/docs/Safety/1_\345\270\270\350\247\201\345\256\211\345\205\250\351\227\256\351\242\230.md" "b/docs/Safety/1_\345\270\270\350\247\201\345\256\211\345\205\250\351\227\256\351\242\230.md" new file mode 100644 index 00000000..2f3ecb60 --- /dev/null +++ "b/docs/Safety/1_\345\270\270\350\247\201\345\256\211\345\205\250\351\227\256\351\242\230.md" @@ -0,0 +1,23 @@ +# 常见安全问题 + +## 常见开发中遇到的问题 + +- 用户身份被盗用 +- 用户密码泄露 +- 用户资料被盗取 +- 网站数据库泄露 + +## 常见前端安全问题 + +- XSS +- CSRF +- Cookies 问题 +- 点击劫持问题 +- 传输安全 + +## 常见后端安全问题 + +- 密码安全 +- 接入层注入 +- 接入层上传问题 +- 信息泄露 \ No newline at end of file diff --git "a/docs/Safety/2_\350\267\250\347\253\231\350\204\232\346\234\254\346\224\273\345\207\273.md" "b/docs/Safety/2_\350\267\250\347\253\231\350\204\232\346\234\254\346\224\273\345\207\273.md" new file mode 100644 index 00000000..2160320f --- /dev/null +++ "b/docs/Safety/2_\350\267\250\347\253\231\350\204\232\346\234\254\346\224\273\345\207\273.md" @@ -0,0 +1,181 @@ +# 跨站脚本攻击(XSS) + +## 概念 + +跨站脚本攻击(Cross-Site Scripting, XSS),可以将代码注入到用户浏览的网页上,这种代码包括 HTML 和 JavaScript。 + + + +## 攻击原理 + +
+ + + + + +## 危害 + +- 偷取网站任意数据 +- 偷取用户资料 +- 劫持前端逻辑 +- 显示伪造的文章或者图片 + + + +## 攻击分类 + +### 1. 反射型 + +反射型指的是 url 参数直接注入。比如:在浏览器地址栏中 + +```html +localhost:1521?from=Bai +``` + + + +### 2. 存储型 + +存储到数据库后读取时注入。 + +一个攻击者在论坛的楼层中包含了一段 JavaScript 代码,并且服务器没有正确进行过滤,那就会造在浏览这个页面时会执行这段 JavaScript 代码。 + + + +## XSS 攻击注入点 + +### 1. HTML 节点内容 + +```html +
+ #{content} +
+``` + +注入点: + +```html +
+ +
+``` + +### 2. HTML 属性 + +```html + +``` + +注入点: + +```html + +``` + +其中 1" onerror="alert(1) 就是 #{image} 。这里可看出 ``多了一个属性 onerror。 + +### 3. JavaScript 代码 + +```html + +``` + +注入点: + +```html + +``` + +其中 hello";alert(1);" 就是 #{data}。这里可看出实际上是即为 data 赋值,也进行了 alert 操作。 + +### 4. 富文本 + +富文本需要保留 HTML,但 HTML 存在 XSS 攻击风险。 + + + +## 防御 + +### 浏览器自带防御 + +使用浏览器自带防御可防御一些 XSS。但是存在一些不足: + +- 只能对参数出现在 HMTL 内容或属性的反射型 XSS 进行防御 +- 并不是所有浏览器都支持对 XSS 的防御 + +### HTML 内容 + +```html +
+ +
+``` + +解决: + +- 将 `<` 转义为 `<` +- 将 `>` 转义为 `>` + +转义后: + +```html +<div> + <script> + </script> +</div> +``` + +### HTML 属性 + +```html + +``` + +解决: + +- 将 `"`转义为 `&quto;` +- 将 `'`转义为 `'` +- 将空格转义为 ` ` + +转义后: + +```html + +``` + +### JavaScript 代码 + +```java + +``` + +解决: + +- 将 `"`转义为 `\"` +- 将 `\`转义为 `\\` +- 或者转化为 json + +转义后: + +```html + +``` + +### 富文本 + +[按**白名单**保留部分标签和属性。](https://github.com/leizongmin/js-xss/blob/master/README.zh.md) + +### CSP + +[CSP](https://blog.csdn.net/Fly_hps/article/details/86466367) 即内容安全策略(Content Security Policy):用于指定哪些内容可执行。 \ No newline at end of file diff --git "a/docs/Safety/3_\350\267\250\347\253\231\350\257\267\346\261\202\344\274\252\351\200\240.md" "b/docs/Safety/3_\350\267\250\347\253\231\350\257\267\346\261\202\344\274\252\351\200\240.md" new file mode 100644 index 00000000..32b7b42f --- /dev/null +++ "b/docs/Safety/3_\350\267\250\347\253\231\350\257\267\346\261\202\344\274\252\351\200\240.md" @@ -0,0 +1,52 @@ +# 跨站请求伪造 + +## 概念 + +跨站请求伪造(Cross-site request forgery,CSRF),是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并执行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去执行。 + +XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户浏览器的信任。 + + + +## 攻击原理 + +
+ +- 用户登录 A 网站 +- A 网站确认身份 +- B 网站页面向 A 网站发起请求(带 A 网站身份) + - B 网站向 A 网站请求,带 A 网站 Cookies + - 不访问 A 网站前端 + - referer 为 B 网站 + + + +## 危害 + +- 利用用户登录态 +- 用户不知情 +- 完成业务请求 + + + +## 防御 + +### 带 A 网站 Cookies + +利用 Cookie 中新增**属性 same-site**,禁止第三方网站带 Cookies。但是这样适配性比较差,目前只有 Chrome 支持。 + +### 不访问 A 网站前端 + +在前端页面加入验证消息,有 2 种方式: + +- **验证码** + + 因为 CSRF 攻击是在用户无意识的情况下发生的,所以要求用户输入验证码可以让用户知道自己正在做的操作。 + +- **token** + + 例如服务器生成随机数并附加在表单中,并要求客户端传回这个随机数。 + +### referer 为 B 网站 + +**验证 referer**。判断请求的来源是否合法。 \ No newline at end of file diff --git "a/docs/Safety/4_Cookies\351\227\256\351\242\230.md" "b/docs/Safety/4_Cookies\351\227\256\351\242\230.md" new file mode 100644 index 00000000..5870cd2a --- /dev/null +++ "b/docs/Safety/4_Cookies\351\227\256\351\242\230.md" @@ -0,0 +1,57 @@ +# Cookies 问题 + +## Cookies 特点 + +- 前端数据存储 +- 后端可通过 HTTP 头设置 +- 请求时通过 HTTP 头传给后端 +- 前端可读写 +- 遵守同源(同协议、同域名、同端口)策略 + +## Cookies 属性 + +- 域名(Domain) +- 有效期(Expires) +- 路径(Path) +- http-only +- secure + +## Cookies 作用 + +- 存储个性化设置 +- 存储未登录时用户唯一标识 +- 存储已登录用户的凭证 +- 存储其他业务数据 + +## 登录用户凭证 + +### 1. 过程 + +- 前端提交用户名和密码 +- 后端验证用户名和密码 +- 后端通过 HTTP 头设置用户凭证 +- 后续访问时后端先验证用户凭证 + +### 2. 用户凭证 + +- 用户 ID(容易被查看) +- 用户 ID + 签名 (签名不易被猜到,比较安全) +- sessionId (Cookie 中只维护 sessionId) + +## Cookies 和 XSS 的关系 + +XSS 可能会偷取 Cookies。防御:将 Cookie 设置为 http-only。 + +## Cookies 和 CSRF 的关系 + +CSRF 利用了用户 Cookies,但攻击站点无法读写 Cookies。 + +防御:禁止第三方网站带 Cookies,但是这样适配性比较差,目前只有 Chrome 支持。 + +## Cookies 安全策略 + +- 签名防篡改 +- 私有变换(加密) +- http-only +- same-site + diff --git "a/docs/Safety/5_\347\202\271\345\207\273\345\212\253\346\214\201\351\227\256\351\242\230.md" "b/docs/Safety/5_\347\202\271\345\207\273\345\212\253\346\214\201\351\227\256\351\242\230.md" new file mode 100644 index 00000000..d8a7d04b --- /dev/null +++ "b/docs/Safety/5_\347\202\271\345\207\273\345\212\253\346\214\201\351\227\256\351\242\230.md" @@ -0,0 +1,22 @@ +# 点击劫持问题 + +## 概念 + +点击劫持(Click Jacking)是通过不可见框架底部的内容误导受害者点击,虽然受害者点击的是他所看到的网页,但其实他所点击的是被黑客精心构建的另一个置于原网页上面的透明页面。 + +## 原理 + +这种攻击利用了 HTML 中 `