diff --git a/README.md b/README.md index d602d73f804..7720a5e24f7 100644 --- a/README.md +++ b/README.md @@ -1,204 +1,197 @@ -如果 Github 访问速度比较慢或者图片无法刷新出来的话,可以转移到[码云](https://gitee.com/SnailClimb/JavaGuide )查看,或者[在线阅读](https://snailclimb.gitee.io/javaguide )。**如果你要提交 issue 或者 pr 的话请到 [Github](https://github.com/Snailclimb/JavaGuide) 提交。** +👍推荐 [送你一个 0.73折购买阿里云服务器的机会](https://www.aliyun.com/minisite/goods?taskCode=pintuan20201212&recordId=311803&userCode=hf47liqn) 。1核2G 84元即可薅1年,力度很大,推荐3年(不建议非人新购买,非人新可以注关一下其他云厂商活动或者用朋友/人亲的身份证购买) -《JavaGuide面试突击版》PDF版本+3本PDF Java 学习手册,在[公众号JavaGuide](#公众号)后台回复“面试突击”即可获取。 +👍推荐 [在线阅读](https://snailclimb.gitee.io/javaguide) (Github 访问速度比较慢可能会导致部分图片无法刷新出来) -如要进群或者请教问题,请[联系我](#联系我) (备注来自Github。请直入问题,工作时间不回复)。 +👍推荐 [图解Java+操作系统+HTTP+计算机网络的 PDF 资料](#优质原创PDF资源) -> JavaGuide 的Star数量虽然比较多,但是它的价值和含金量一定是不能和 Dubbo、Nacos这些优秀的国产开源项目比的。希望国内可以出更多优秀的开源项目! -> -> 另外,希望大家对面试不要抱有侥幸的心理,打铁还需自身硬! 我希望这个文档是为你学习 Java 指明方向,而不是用来应付面试用的。加油!奥利给! +## 一些闲话: -**开始阅读之前必看** : - -1. [完结撒花!JavaGuide面试突击版来啦!](./docs/javaguide面试突击版.md) -2. [JavaGuide重大更新记录](./docs/update-history.md) +> 1. **介绍**:关于 JavaGuide 的相关介绍请看:[关于 JavaGuide 的一些说明](https://www.yuque.com/snailclimb/dr6cvl/mr44yt) 。PDF 版本请看:[完结撒花!JavaGuide 面试突击版来啦!](./docs/javaguide面试突击版.md) 。 +> 2. **PDF版本** : [《JavaGuide 面试突击版》PDF 版本](#公众号) 。 +> 3. **面试专版** :准备面试的小伙伴可以考虑面试专版:[《Java 面试进阶指南》](https://xiaozhuanlan.com/javainterview?rel=javaguide) , +> 4. **知识星球** : 简历指导/Java学习/面试指导/offer选择。欢迎加入[我的知识星球](https://wx.zsxq.com/dweb2/index/group/48418884588288)。 +> 5. **联系我** :如要进群或者请教问题,请[联系我](#联系我) (备注来自 Github。请直入问题,工作时间不回复)。 +> 6. **转载须知** :以下所有文章如非文首说明皆为我(Guide哥)的原创,转载在文首注明出处,如发现恶意抄袭/搬运,会动用法律武器维护自己的权益。让我们一起维护一个良好的技术创作环境!⛽️

-

阅读 公众号 公众号 投稿 + 投稿 投稿

+

Sponsor

- - - - -
- - - - - -
- - + + +
-## 目录 + + + + - [Java](#java) - - [基础](#基础) - - [容器](#容器) - - [并发](#并发) - - [JVM](#jvm) - - [其他](#其他) + - [基础](#基础) + - [容器](#容器) + - [并发](#并发) + - [JVM (必看 :+1:)](#jvm-必看-1) + - [新特性](#新特性) - [网络](#网络) - [操作系统](#操作系统) - - [Linux](#linux) -- **[数据结构与算法](#数据结构与算法)** - - [数据结构](#数据结构) - - [算法](#算法) +- [数据结构与算法](#数据结构与算法) + - [数据结构](#数据结构) + - [算法](#算法) - [数据库](#数据库) - - [MySQL](#mysql) - - [Redis](#redis) + - [MySQL](#mysql) + - [Redis](#redis) - [系统设计](#系统设计) - - [必知](#必知) - - [常用框架](#常用框架) - - [Spring](#springspringboot) - - [SpringBoot](#springboot) - - [MyBatis](#mybatis) - - [认证授权(JWT、SSO)](#认证授权) - - [分布式](#分布式) - - [分布式搜索引擎](#分布式搜索引擎) - - [RPC](#rpc) - - [消息队列](#消息队列) - - [API 网关](#api-网关) - - [分布式id](#分布式id) - - [分布式限流](#分布式限流) - - [分布式接口幂等性](#分布式接口幂等性) - - [数据库扩展](#数据库扩展) - - [ZooKeeper](#zookeeper) - - [大型网站架构](#大型网站架构) - - [性能测试](#性能测试) - - [高并发](#高并发) - - [高可用](#高可用) - - [微服务](#微服务) - - [Spring Cloud](#spring-cloud) -- [必会工具](#必会工具) - - [Git](#git) - - [Docker](#docker) + - [编码之道(必看 :+1:)](#编码之道必看-1) + - [常用框架](#常用框架) + - [Spring/SpringBoot](#springspringboot) + - [MyBatis](#mybatis) + - [Netty (必看 :+1:)](#netty-必看-1) + - [ZooKeeper](#zookeeper) + - [认证授权](#认证授权) + - [JWT](#jwt) + - [SSO(单点登录)](#sso单点登录) + - [分布式](#分布式) + - [CAP 理论](#cap-理论) + - [BASE 理论](#base-理论) + - [Paxos 算法和 Raft 算法](#paxos-算法和-raft-算法) + - [搜索引擎](#搜索引擎) + - [RPC](#rpc) + - [API 网关](#api-网关) + - [分布式 id](#分布式-id) + - [微服务](#微服务) + - [高并发](#高并发) + - [消息队列](#消息队列) + - [读写分离](#读写分离) + - [分库分表](#分库分表) + - [负载均衡](#负载均衡) + - [高可用](#高可用) + - [限流](#限流) + - [降级](#降级) + - [熔断](#熔断) + - [排队](#排队) + - [大型网站架构](#大型网站架构) +- [工具](#工具) - [面试指南](#面试指南) -- [Java学习常见问题汇总](#java学习常见问题汇总) -- [资源](#资源) - - [Java程序员必备书单](#java程序员必备书单) - - [实战项目推荐](#实战项目推荐) -- [待办](#待办) -- [说明](#说明) +- [Java 学习常见问题汇总](#java-学习常见问题汇总) +- [书单](#书单) +- [其他](#其他) + - [待办](#待办) + - [联系我](#联系我) + - [捐赠支持](#捐赠支持) + - [Contributor](#贡献者) + - [公众号](#公众号) + + + ## Java ### 基础 -**基础知识系统总结:** +**知识点/面试题:**(必看:+1: ) -1. **[Java 基础知识](docs/java/Java基础知识.md)** -2. **[Java 基础知识疑难点/易错点](docs/java/Java疑难点.md)** -3. [【选看】J2EE 基础知识](docs/java/J2EE基础知识.md) +1. **[Java 基础知识](docs/java/basis/Java基础知识.md)** +2. **[Java 基础知识疑难点/易错点](docs/java/basis/Java基础知识疑难点.md)** **重要知识点详解:** -1. [枚举](docs/java/basic/用好Java中的枚举真的没有那么简单.md) (很重要的一个数据结构,用好枚举真的没有那么简单!) -2. [Java 常见关键字总结:final、static、this、super!](docs/java/basic/final,static,this,super.md) -3. [什么是反射机制?反射机制的应用场景有哪些?](docs/java/basic/reflection.md) -4. [代理模式详解:静态代理+JDK/CGLIB 动态代理实战(动态代理和静态代理的区别?JDK动态代理 和 CGLIB 动态代理的区别?)](docs/java/basic/java-proxy.md) - -**其他:** - -1. [JAD反编译](docs/java/JAD反编译tricks.md) -2. [手把手教你定位常见Java性能问题](./docs/java/手把手教你定位常见Java性能问题.md) +1. [枚举](docs/java/basis/用好Java中的枚举真的没有那么简单.md) (很重要的一个数据结构,用好枚举真的没有那么简单!) +2. [Java 常见关键字总结:final、static、this、super!](docs/java/basis/Java常见关键字总结.md) +3. [什么是反射机制?反射机制的应用场景有哪些?](docs/java/basis/反射机制.md) +4. [代理模式详解:静态代理+JDK/CGLIB 动态代理实战](docs/java/basis/代理模式详解.md) +5. [BIO,NIO,AIO 总结 ](docs/java/basis/BIO,NIO,AIO总结.md) ### 容器 -1. **[Java容器常见面试题/知识点总结](docs/java/collection/Java集合框架常见面试题.md)** -2. 源码分析:[ArrayList 源码](docs/java/collection/ArrayList.md) 、[LinkedList 源码](docs/java/collection/LinkedList.md) 、[HashMap(JDK1.8)源码](docs/java/collection/HashMap.md) 、[ConcurrentHashMap源码](docs/java/collection/ConcurrentHashMap.md) +1. **[Java 容器常见面试题/知识点总结](docs/java/collection/Java集合框架常见面试题.md)** (必看 :+1:) +2. **源码分析** :[ArrayList 源码+扩容机制分析](docs/java/collection/ArrayList源码+扩容机制分析.md) 、[LinkedList 源码](docs/java/collection/LinkedList源码分析.md) 、[HashMap(JDK1.8)源码+底层数据结构分析]() 、[ConcurrentHashMap 源码+底层数据结构分析](docs/java/collection/ConcurrentHashMap源码+底层数据结构分析.md) ### 并发 -**[多线程学习指南](./docs/java/Multithread/多线程学习指南.md)** +并发这部分内容非常重要,还是面试中的重点中的重点!但是,学习起来难度较大,因此我写了:**[多线程学习指南](./docs/java/multi-thread/多线程学习指南.md)** 帮助你学习。 -**面试题总结:** +**知识点/面试题:** (必看 :+1:) -1. **[Java 并发基础常见面试题总结](docs/java/Multithread/JavaConcurrencyBasicsCommonInterviewQuestionsSummary.md)** -2. **[Java 并发进阶常见面试题总结](docs/java/Multithread/JavaConcurrencyAdvancedCommonInterviewQuestions.md)** +1. **[Java 并发基础常见面试题总结](docs/java/multi-thread/2020最新Java并发基础常见面试题总结.md)** +2. **[Java 并发进阶常见面试题总结](docs/java/multi-thread/2020最新Java并发进阶常见面试题总结.md)** -**面试常问知识点:** +**重要知识点详解:** -1. [并发容器总结](docs/java/Multithread/并发容器总结.md) -2. **线程池**:[Java线程池学习总结](./docs/java/Multithread/java线程池学习总结.md)、[拿来即用的线程池最佳实践](./docs/java/Multithread/best-practice-of-threadpool.md) +2. **线程池**:[Java 线程池学习总结](./docs/java/multi-thread/java线程池学习总结.md)、[拿来即用的线程池最佳实践](./docs/java/multi-thread/拿来即用的线程池最佳实践.md) 3. [乐观锁与悲观锁](docs/essential-content-for-interview/面试必备之乐观锁与悲观锁.md) -4. [万字图文深度解析 ThreadLocal](docs/java/Multithread/ThreadLocal.md) -5. [JUC 中的 Atomic 原子类总结](docs/java/Multithread/Atomic.md) -6. [AQS 原理以及 AQS 同步组件总结](docs/java/Multithread/AQS.md) +4. [ ThreadLocal 关键字解析](docs/java/multi-thread/万字详解ThreadLocal关键字.md) +5. [并发容器总结](docs/java/multi-thread/并发容器总结.md) +6. [JUC 中的 Atomic 原子类总结](docs/java/multi-thread/Atomic原子类总结.md) +7. [AQS 原理以及 AQS 同步组件总结](docs/java/multi-thread/AQS原理以及AQS同步组件总结.md) -### JVM +### JVM (必看 :+1:) -1. **[Java内存区域](docs/java/jvm/Java内存区域.md)** -2. **[JVM垃圾回收](docs/java/jvm/JVM垃圾回收.md)** +1. **[Java 内存区域](docs/java/jvm/Java内存区域.md)** +2. **[JVM 垃圾回收](docs/java/jvm/JVM垃圾回收.md)** 3. [JDK 监控和故障处理工具](docs/java/jvm/JDK监控和故障处理工具总结.md) 4. [类文件结构](docs/java/jvm/类文件结构.md) 5. **[类加载过程](docs/java/jvm/类加载过程.md)** 6. [类加载器](docs/java/jvm/类加载器.md) 7. **[【待完成】最重要的 JVM 参数指南(翻译完善了一半)](docs/java/jvm/最重要的JVM参数指南.md)** 8. [JVM 配置常用参数和常用 GC 调优策略](docs/java/jvm/GC调优参数.md) -9. **[【加餐】大白话带你认识JVM](docs/java/jvm/[加餐]大白话带你认识JVM.md)** +9. **[【加餐】大白话带你认识 JVM](docs/java/jvm/[加餐]大白话带你认识JVM.md)** -### 其他 +### 新特性 -1. **I/O** :[BIO,NIO,AIO 总结 ](docs/java/BIO-NIO-AIO.md) -2. **Java 8** :[Java 8 新特性总结](docs/java/What's%20New%20in%20JDK8/Java8Tutorial.md)、[Java 8 学习资源推荐](docs/java/What's%20New%20in%20JDK8/Java8教程推荐.md)、[Java8 forEach 指南](docs/java/What's%20New%20in%20JDK8/Java8foreach指南.md) -3. **Java9~Java14** : [一文带你看遍JDK9~14的重要新特性!](./docs/java/jdk-new-features/new-features-from-jdk8-to-jdk14.md) -4. Java编程规范:**[Java 编程规范以及优雅 Java 代码实践总结](docs/java/Java编程规范.md)** 、[告别编码5分钟,命名2小时!史上最全的Java命名规范参考!](docs/java/java-naming-conventions.md) -5. 设计模式 :[设计模式系列文章](docs/system-design/设计模式.md) +1. **Java 8** :[Java 8 新特性总结](docs/java/new-features/Java8新特性总结.md)、[Java 8 学习资源推荐](docs/java/new-features/Java8教程推荐.md)、[Java8 forEach 指南](docs/java/new-features/Java8foreach指南.md) +2. **Java9~Java14** : [一文带你看遍 JDK9~14 的重要新特性!](./docs/java/new-features/一文带你看遍JDK9到14的重要新特性.md) ## 网络 1. [计算机网络常见面试题](docs/network/计算机网络.md) -2. [计算机网络基础知识总结](docs/network/干货:计算机网络知识总结.md) +2. [计算机网络基础知识总结](docs/network/计算机网络知识总结.md) ## 操作系统 -[最硬核的操作系统常见问题总结!](docs/operating-system/basis.md) - -### Linux - -* [后端程序员必备的 Linux 基础知识](docs/operating-system/linux.md) -* [Shell 编程入门](docs/operating-system/Shell.md) -* [完全使用GNU_Linux学习](docs/operating-system/完全使用GNU_Linux学习.md) -* [Linux 性能分析工具合集](docs/operating-system/Linux性能分析工具合集.md) +1. [操作系统常见问题总结!](docs/operating-system/basis.md) +2. [后端程序员必备的 Linux 基础知识](docs/operating-system/linux.md) +3. [Shell 编程入门](docs/operating-system/Shell.md) ## 数据结构与算法 ### 数据结构 -- [不了解布隆过滤器?一文给你整的明明白白!](docs/dataStructures-algorithms/data-structure/bloom-filter.md) -- [数据结构知识学习与面试](docs/dataStructures-algorithms/数据结构.md) +1. [不了解布隆过滤器?一文给你整的明明白白!](docs/dataStructures-algorithms/data-structure/bloom-filter.md) +2. 常见数据结构总结(正在路上) ### 算法 -- [硬核的算法学习书籍+资源推荐](docs/dataStructures-algorithms/算法学习资源推荐.md) -- 常见算法问题总结: - - [几道常见的字符串算法题总结 ](docs/dataStructures-algorithms/几道常见的子符串算法题.md) - - [几道常见的链表算法题总结 ](docs/dataStructures-algorithms/几道常见的链表算法题.md) - - [剑指offer部分编程题](docs/dataStructures-algorithms/剑指offer部分编程题.md) - - [公司真题](docs/dataStructures-algorithms/公司真题.md) - - [回溯算法经典案例之N皇后问题](docs/dataStructures-algorithms/Backtracking-NQueens.md) +算法这部分内容非常重要,如果你不知道如何学习算法的话,可以看下我写的: + +- [算法学习书籍+资源推荐](https://www.zhihu.com/question/323359308/answer/1545320858) 。 +- [如何刷Leetcode?](https://www.zhihu.com/question/31092580/answer/1534887374) + +**常见算法问题总结:** + +- [几道常见的字符串算法题总结 ](docs/dataStructures-algorithms/几道常见的子符串算法题.md) +- [几道常见的链表算法题总结 ](docs/dataStructures-algorithms/几道常见的链表算法题.md) +- [剑指 offer 部分编程题](docs/dataStructures-algorithms/剑指offer部分编程题.md) ## 数据库 @@ -208,316 +201,271 @@ 1. **[【推荐】MySQL/数据库 知识点总结](docs/database/MySQL.md)** 2. **[阿里巴巴开发手册数据库部分的一些最佳实践](docs/database/阿里巴巴开发手册数据库部分的一些最佳实践.md)** -3. **[一千行MySQL学习笔记](docs/database/一千行MySQL命令.md)** -4. [MySQL高性能优化规范建议](docs/database/MySQL高性能优化规范建议.md) +3. **[一千行 MySQL 学习笔记](docs/database/一千行MySQL命令.md)** +4. [MySQL 高性能优化规范建议](docs/database/MySQL高性能优化规范建议.md) **重要知识点:** -1. [数据库索引总结1](docs/database/MySQL%20Index.md)、[数据库索引总结2](docs/database/数据库索引.md) -2. [事务隔离级别(图文详解)](docs/database/事务隔离级别(图文详解).md) -3. [一条SQL语句在MySQL中如何执行的](docs/database/一条sql语句在mysql中如何执行的.md) +1. [数据库索引总结 1](docs/database/MySQL%20Index.md)、[数据库索引总结 2](docs/database/数据库索引.md) +2. [事务隔离级别(图文详解)]() +3. [一条 SQL 语句在 MySQL 中如何执行的](docs/database/一条sql语句在mysql中如何执行的.md) 4. **[关于数据库中如何存储时间的一点思考](docs/database/关于数据库存储时间的一点思考.md)** ### Redis -* [关于缓存的一些重要概念(Redis前置菜)](docs/database/Redis/some-concepts-of-caching.md) -* [Redis 常见问题总结](docs/database/Redis/redis-all.md) -* **Redis 系列文章合集:** - 1. 数据结构和算法 :[5种基本数据结构](docs/database/Redis/redis-collection/Redis(1)——5种基本数据结构.md)、[跳跃表](docs/database/Redis/redis-collection/Redis(2)——跳跃表.md)、[神奇的HyperLoglog解决统计问题](docs/database/Redis/redis-collection/Reids(4)——神奇的HyperLoglog解决统计问题.md)、[亿级数据过滤和布隆过滤器](docs/database/Redis/redis-collection/Redis(5)——亿级数据过滤和布隆过滤器.md)、[GeoHash查找附近的人](docs/database/Redis/redis-collection/Redis(6)——GeoHash查找附近的人.md) - 3. Redis锁:[分布式锁深入探究](docs/database/Redis/redis-collection/Redis(3)——分布式锁深入探究.md) 、 [Redlock分布式锁](docs/database/Redis/Redlock分布式锁.md) 、[如何做可靠的分布式锁,Redlock真的可行么](docs/database/Redis/如何做可靠的分布式锁,Redlock真的可行么.md) - 7. [持久化](docs/database/Redis/redis-collection/Redis(7)——持久化.md) - 8. [发布订阅与Stream](docs/database/Redis/redis-collection/Redis(8)——发布订阅与Stream.md) - 9. [史上最强【集群】入门实践教程](docs/database/Redis/redis-collection/Redis(9)——集群入门实践教程.md) - 10. [Redis数据类型、编码、底层数据结构的关系看这篇](docs/database/Redis/redis-collection/Redis(10)——Redis数据类型、编码、数据结构的关系.md) +2. [Redis 常见问题总结](docs/database/Redis/redis-all.md) ## 系统设计 -### 必知 +### 编码之道(必看 :+1:) -1. **[RestFul API 简明教程](docs/system-design/restful-api.md)** -2. **[因为命名被diss无数次。Guide简单聊聊编程最头疼的事情之一:命名](docs/system-design/naming.md)** +1. [RestFul API 简明教程](docs/system-design/coding-way/RESTfulAPI简明教程.md) +2. [Java 编程规范以及优雅 Java 代码实践总结](docs/java/Java编程规范.md) +3. [Java 命名之道](docs/system-design/naming.md) ### 常用框架 -#### Spring/SpringBoot +如果你没有接触过 Java Web 开发的话,可以先看一下我总结的 [《J2EE 基础知识》](docs/java/J2EE基础知识.md) 。虽然,这篇文章中的很多内容已经淘汰,但是可以让你对 Java 后台技术发展有更深的认识。 + +#### Spring/SpringBoot (必看 :+1:) -1. **[Spring 常见问题总结](docs/system-design/framework/spring/SpringInterviewQuestions.md)** -3. **[SpringBoot 指南/常见面试题总结](https://github.com/Snailclimb/springboot-guide)** -3. **[Spring/Spring常用注解总结!安排!](./docs/system-design/framework/spring/spring-annotations.md)** -4. **[Spring事务总结](docs/system-design/framework/spring/spring-transaction.md)** -5. [Spring IoC 和 AOP详解](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486938&idx=1&sn=c99ef0233f39a5ffc1b98c81e02dfcd4&chksm=cea24211f9d5cb07fa901183ba4d96187820713a72387788408040822ffb2ed575d28e953ce7&token=1666190828&lang=zh_CN#rd) -6. [Spring中 Bean 的作用域与生命周期](docs/system-design/framework/spring/SpringBean.md) -7. [SpringMVC 工作原理详解](docs/system-design/framework/spring/SpringMVC-Principle.md) -8. [Spring中都用到了那些设计模式?](docs/system-design/framework/spring/Spring-Design-Patterns.md) +**知识点/面试题:** + +1. **[Spring 常见问题总结](docs/system-design/framework/spring/Spring常见问题总结.md)** +2. **[SpringBoot 指南/常见面试题总结](https://github.com/Snailclimb/springboot-guide)** + +**重要知识点详解:** + +1. **[Spring/Spring 常用注解总结!安排!](./docs/system-design/framework/spring/SpringBoot+Spring常用注解总结.md)** +2. **[Spring 事务总结](docs/system-design/framework/spring/Spring事务总结.md)** +3. [Spring 中都用到了那些设计模式?](docs/system-design/framework/spring/Spring-Design-Patterns.md) #### MyBatis -- [MyBatis常见面试题总结](docs/system-design/framework/mybatis/mybatis-interview.md) +- [MyBatis 常见面试题总结](docs/system-design/framework/mybatis/mybatis-interview.md) + +#### Netty (必看 :+1:) + +1. [剖析面试最常见问题之 Netty(上)](https://xiaozhuanlan.com/topic/4028536971) +2. [剖析面试最常见问题之 Netty(下)](https://xiaozhuanlan.com/topic/3985146207) + +#### ZooKeeper + +> 前两篇文章可能有内容重合部分,推荐都看一遍。 + +1. [【入门】ZooKeeper 相关概念总结](docs/system-design/distributed-system/zookeeper/zookeeper-intro.md) +2. [【进阶】ZooKeeper 相关概念总结](docs/system-design/distributed-system/zookeeper/zookeeper-plus.md) +3. [【实战】ZooKeeper 实战](docs/system-design/distributed-system/zookeeper/zookeeper-in-action.md) ### 认证授权 -**[认证授权基础:搞清Authentication,Authorization以及Cookie、Session、Token、OAuth 2、SSO](docs/system-design/authority-certification/basis-of-authority-certification.md)** +**[《认证授权基础》](docs/system-design/authority-certification/basis-of-authority-certification.md)** 这篇文章中我会介绍认证授权常见概念: **Authentication**,**Authorization** 以及 **Cookie**、**Session**、Token、**OAuth 2**、**SSO** 。如果你不清楚这些概念的话,建议好好阅读一下这篇文章。 #### JWT -- **[JWT 优缺点分析以及常见问题解决方案](docs/system-design/authority-certification/JWT-advantages-and-disadvantages.md)** -- **[适合初学者入门 Spring Security With JWT 的 Demo](https://github.com/Snailclimb/spring-security-jwt-guide)** +1. [JWT 优缺点分析以及常见问题解决方案](docs/system-design/authority-certification/JWT优缺点分析以及常见问题解决方案.md) +2. [适合初学者入门 Spring Security With JWT 的 Demo](https://github.com/Snailclimb/spring-security-jwt-guide) #### SSO(单点登录) -SSO(Single Sign On)即单点登录说的是用户登陆多个子系统的其中一个就有权访问与其相关的其他系统。举个例子我们在登陆了京东金融之后,我们同时也成功登陆京东的京东超市、京东家电等子系统。相关阅读:**[SSO 单点登录看这篇就够了!](docs/system-design/authority-certification/sso.md)** +**SSO(Single Sign On)** 即单点登录说的是用户登陆多个子系统的其中一个就有权访问与其相关的其他系统。举个例子我们在登陆了京东金融之后,我们同时也成功登陆京东的京东超市、京东家电等子系统。相关阅读:**[SSO 单点登录看这篇就够了!](docs/system-design/authority-certification/SSO单点登录看这一篇就够了.md)** ### 分布式 -[分布式相关概念入门](docs/system-design/website-architecture/分布式.md) +#### CAP 理论 -#### 分布式搜索引擎 +CAP 也就是 Consistency(一致性)、Availability(可用性)、Partition Tolerance(分区容错性) 这三个单词首字母组合。 -提高搜索效率。常见于电商购物网站的商品搜索于分类。 +关于 CAP 的详细解读请看:[《CAP理论解读》](docs/system-design/distributed-system/CAP理论.md)。 -比较常用的是 Elasticsearch 和 Solr。 +#### BASE 理论 -代办。 +**BASE** 是 **Basically Available(基本可用)** 、**Soft-state(软状态)** 和 **Eventually Consistent(最终一致性)** 三个短语的缩写。BASE 理论是对 CAP 中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于 CAP 定理逐步演化而来的,它大大降低了我们对系统的要求。 -#### RPC +关于 CAP 的详细解读请看:[《BASE理论解读》](docs/system-design/distributed-system/BASE理论.md)。 -让调用远程服务调用像调用本地方法那样简单。 +#### Paxos 算法和 Raft 算法 -- [Dubbo 总结:关于 Dubbo 的重要知识点](docs/system-design/data-communication/dubbo.md) -- [服务之间的调用为啥不直接用 HTTP 而用 RPC?](docs/system-design/data-communication/why-use-rpc.md) +**Paxos 算法**诞生于 1900 年,这是一种解决分布式系统一致性的经典算法 。但是,由于 Paxos 算法非常难以理解和实现,不断有人尝试简化这一算法。到了2013 年才诞生了一个比 Paxos 算法更易理解和实现的分布式一致性算法—**Raft 算法**。 -#### 消息队列 +#### 搜索引擎 -消息队列在分布式系统中主要是为了解耦和削峰。相关阅读: **[消息队列总结](docs/system-design/data-communication/message-queue.md)** 。 +用于提高搜索效率,功能和浏览器搜索引擎类似。比较常见的搜索引擎是 Elasticsearch(推荐) 和 Solr。 -**RabbitMQ:** +#### RPC -1. [RabbitMQ 入门](docs/system-design/data-communication/rabbitmq.md) +RPC 让调用远程服务调用像调用本地方法那样简单。 -**RocketMQ:** +1. [Dubbo 总结:关于 Dubbo 的重要知识点](docs/system-design/distributed-system/rpc/关于Dubbo的重要知识点.md) +2. [服务之间的调用为啥不直接用 HTTP 而用 RPC?](docs/system-design/distributed-system/rpc/服务之间的调用为啥不直接用HTTP而用RPC.md) +3. [一款基于 Netty+Kyro+Zookeeper 实现的自定义 RPC 框架](https://github.com/Snailclimb/guide-rpc-framework) -1. [RocketMQ 入门](docs/system-design/data-communication/RocketMQ.md) -2. [RocketMQ的几个简单问题与答案](docs/system-design/data-communication/RocketMQ-Questions.md) +#### API 网关 -**Kafka:** +网关主要用于请求转发、安全认证、协议转换、容灾。 -1. **[Kafka 入门+SpringBoot整合Kafka系列](https://github.com/Snailclimb/springboot-kafka)** -2. **[Kafka 常见面试题总结](docs/system-design/data-communication/kafka-inverview.md)** -3. [【加餐】Kafka入门看这一篇就够了](docs/system-design/data-communication/Kafka入门看这一篇就够了.md) +1. [为什么要网关?你知道有哪些常见的网关系统?](docs/system-design/distributed-system/api-gateway/为什么要网站有哪些常见的网站系统.md) +2. [如何设计一个亿级网关(API Gateway)?](docs/system-design/distributed-system/api-gateway/如何设计一个亿级网关.md) -#### API 网关 +#### 分布式 id -网关主要用于请求转发、安全认证、协议转换、容灾。 +在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识。比如数据量太大之后,往往需要对进行对数据进行分库分表,分库分表后需要有一个唯一 ID 来标识一条数据或消息,数据库的自增 ID 显然不能满足需求。相关阅读:[为什么要分布式 id ?分布式 id 生成方案有哪些?](docs/system-design/micro-service/分布式id生成方案总结.md) -1. [为什么要网关?你知道有哪些常见的网关系统?](docs/system-design/micro-service/api-gateway-intro.md) -2. [如何设计一个亿级网关(API Gateway)?](docs/system-design/micro-service/API网关.md) +#### 分布式事务 -#### 分布式id +**分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。** -1. [为什么要分布式 id ?分布式 id 生成方案有哪些?](docs/system-design/micro-service/分布式id生成方案总结.md) +简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。 -#### 分布式限流 +### 微服务 -1. [限流算法有哪些?](docs/system-design/micro-service/limit-request.md) +1. [ 大白话入门 Spring Cloud](docs/system-design/micro-service/spring-cloud.md) +2. [微服务/分布式大厂真实面试问题解答](https://xiaozhuanlan.com/topic/2895047136) -#### 分布式接口幂等性 +### 高并发 -#### ZooKeeper +#### 消息队列 -> 前两篇文章可能有内容重合部分,推荐都看一遍。 +消息队列在分布式系统中主要是为了解耦和削峰。相关阅读: **[消息队列总结](docs/system-design/distributed-system/message-queue/message-queue.md)** 。 -1. [【入门】ZooKeeper 相关概念总结 01](docs/system-design/framework/zookeeper-intro.md) -2. [【进阶】ZooKeeper 相关概念总结 02](docs/system-design/framework/zookeeper-plus.md) -3. [【实战】ZooKeeper 实战](docs/system-design/framework/zookeeper-in-action.md) +1. **RabbitMQ** : [RabbitMQ 入门](docs/system-design/distributed-system/message-queue/RabbitMQ入门看这一篇就够了.md) +2. **RocketMQ** : [RocketMQ 入门](docs/system-design/distributed-system/message-queue/RocketMQ.md)、[RocketMQ 的几个简单问题与答案](docs/system-design/distributed-system/message-queue/RocketMQ-Questions.md) +3. **Kafka** :**[Kafka 常见面试题总结](docs/system-design/distributed-system/message-queue/Kafka常见面试题总结.md)** -#### 其他 +#### 读写分离 -- 接口幂等性(代办):分布式系统必须要考虑接口的幂等性。 +读写分离主要是为了将数据库的读和写操作分不到不同的数据库节点上。主服务器负责写,从服务器负责读。另外,一主一从或者一主多从都可以。 -#### 数据库扩展 +**读写分离可以大幅提高读性能,小幅提高写的性能。因此,读写分离更适合单机并发读请求比较多的场景。** -读写分离、分库分表。 +#### 分库分表 -代办..... +**分库分表是为了解决由于库、表数据量过大,而导致数据库性能持续下降的问题。** 常见的分库分表工具有:`sharding-jdbc`(当当)、`TSharding`(蘑菇街)、`MyCAT`(基于 Cobar)、`Cobar`(阿里巴巴)...。 -### 大型网站架构 +**推荐使用 `sharding-jdbc`** 。 因为,`sharding-jdbc` 是一款轻量级 `Java` 框架,以 `jar` 包形式提供服务,不要我们做额外的运维工作,并且兼容性也很好。 -- [8 张图读懂大型网站技术架构](docs/system-design/website-architecture/8%20张图读懂大型网站技术架构.md) -- [关于大型网站系统架构你不得不懂的10个问题](docs/system-design/website-architecture/关于大型网站系统架构你不得不懂的10个问题.md) +#### 负载均衡 -#### 性能测试 +负载均衡系统通常用于将任务比如用户请求处理分配到多个服务器处理以提高网站、应用或者数据库的性能和可靠性。 -- [后端程序员也要懂的性能测试知识](https://articles.zsxq.com/id_lwl39teglv3d.html) (知识星球) +常见的负载均衡系统包括 3 种: -#### 高并发 +1. **DNS 负载均衡** :一般用来实现地理级别的均衡。 +2. **硬件负载均衡** : 通过单独的硬件设备比如 F5 来实现负载均衡功能(硬件的价格一般很贵)。 +3. **软件负载均衡** :通过负载均衡软件比如 Nginx 来实现负载均衡功能。 -待办...... +### 高可用 -#### 高可用 +高可用描述的是一个系统在大部分时间都是可用的,可以为我们提供服务的。高可用代表系统即使在发生硬件故障或者系统升级的时候,服务仍然是可用的 。 -高可用描述的是一个系统在大部分时间都是可用的,可以为我们提供服务的。高可用代表系统即使在发生硬件故障或者系统升级的时候,服务仍然是可用的 。相关阅读: **《[如何设计一个高可用系统?要考虑哪些地方?](docs/system-design/website-architecture/如何设计一个高可用系统?要考虑哪些地方?.md)》** 。 +相关阅读: **《[如何设计一个高可用系统?要考虑哪些地方?](docs/system-design/high-availability/如何设计一个高可用系统要考虑哪些地方.md)》** 。 -### 微服务 +#### 限流 -#### Spring Cloud +限流是从用户访问压力的角度来考虑如何应对系统故障。 -- [ 大白话入门 Spring Cloud](docs/system-design/micro-service/spring-cloud.md) +限流为了对服务端的接口接受请求的频率进行限制,防止服务挂掉。比如某一接口的请求限制为 100 个每秒, 对超过限制的请求放弃处理或者放到队列中等待处理。限流可以有效应对突发请求过多。相关阅读:[限流算法有哪些?](docs/system-design/high-availability/limit-request.md) -## 必会工具 +#### 降级 -### Git +降级是从系统功能优先级的角度考虑如何应对系统故障。 -* [Git入门](docs/tools/Git.md) +服务降级指的是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行。 -### Docker +#### 熔断 -1. [Docker 基本概念解读](docs/tools/Docker.md) -2. [一文搞懂 Docker 镜像的常用操作!](docs/tools/Docker-Image.md ) +熔断和降级是两个比较容易混淆的概念,两者的含义并不相同。 -### 其他 +降级的目的在于应对系统自身的故障,而熔断的目的在于应对当前系统依赖的外部系统或者第三方系统的故障。 -- [【原创】如何使用云服务器?希望这篇文章能够对你有帮助!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485738&idx=1&sn=f97e91a50e444944076c30b0717b303a&chksm=cea246e1f9d5cff73faf6a778b147ea85162d1f3ed55ca90473c6ebae1e2c4d13e89282aeb24&token=406194678&lang=zh_CN#rd) +#### 排队 -## 面试指南 +另类的一种限流,类比于现实世界的排队。玩过英雄联盟的小伙伴应该有体会,每次一有活动,就要经历一波排队才能进入游戏。 -> 这部分很多内容比如大厂面经、真实面经分析被移除,详见[完结撒花!JavaGuide面试突击版来啦!](./docs/javaguide面试突击版.md)。 +#### 集群 -1. **[【备战面试1】程序员的简历就该这样写](docs/essential-content-for-interview/PreparingForInterview/程序员的简历之道.md)** -2. **[【备战面试2】初出茅庐的程序员该如何准备面试?](docs/essential-content-for-interview/PreparingForInterview/interviewPrepare.md)** -3. **[【备战面试3】7个大部分程序员在面试前很关心的问题](docs/essential-content-for-interview/PreparingForInterview/JavaProgrammerNeedKnow.md)** -4. **[【备战面试4】Github上开源的Java面试/学习相关的仓库推荐](docs/essential-content-for-interview/PreparingForInterview/JavaInterviewLibrary.md)** -5. **[【备战面试5】如果面试官问你“你有什么问题问我吗?”时,你该如何回答](docs/essential-content-for-interview/PreparingForInterview/面试官-你有什么问题要问我.md)** -6. [【备战面试6】应届生面试最爱问的几道 Java 基础问题](docs/essential-content-for-interview/PreparingForInterview/应届生面试最爱问的几道Java基础问题.md) -7. **[【备战面试6】美团面试常见问题总结(附详解答案)](docs/essential-content-for-interview/PreparingForInterview/美团面试常见问题总结.md)** +相同的服务部署多份,避免单点故障。 -## Java学习常见问题汇总 +#### 超时和重试机制 -1. [Java学习路线和方法推荐](docs/questions/java-learning-path-and-methods.md) -2. [Java培训四个月能学会吗?](docs/questions/java-training-4-month.md) -3. [新手学习Java,有哪些Java相关的博客,专栏,和技术学习网站推荐?](docs/questions/java-learning-website-blog.md) -4. [Java 还是大数据,你需要了解这些东西!](docs/questions/java-big-data.md) +**一旦用户的请求超过某个时间得不到响应就结束此次请求并抛出异常。** 如果不进行超时设置可能会导致请求响应速度慢,甚至导致请求堆积进而让系统无法在处理请求。 -## 资源 +另外,重试的次数一般设为 3 次,再多次的重试没有好处,反而会加重服务器压力(部分场景使用失败重试机制会不太适合)。 -### Java程序员必备书单 +### 大型网站架构 -1. [「基础篇」Guide的Java后端书架来啦!都是Java程序员必看的书籍?](./docs/books/java基础篇.md) +- [8 张图读懂大型网站技术架构](docs/system-design/website-architecture/8%20张图读懂大型网站技术架构.md) +- [关于大型网站系统架构你不得不懂的 10 个问题](docs/system-design/website-architecture/关于大型网站系统架构你不得不懂的10个问题.md) -### 实战项目推荐 +## 工具 -- **[Java、SpringBoot实战项目推荐](https://github.com/Snailclimb/awesome-java#实战项目)** +1. **Java** :[JAD 反编译](docs/java/JAD反编译tricks.md)、[手把手教你定位常见 Java 性能问题](./docs/java/手把手教你定位常见Java性能问题.md) +2. **Git** :[Git 入门](docs/tools/Git.md) +3. **Github** : [我使用Github 5 年总结了这些骚操作](docs/tools/Github技巧.md) +4. **Docker** : [Docker 基本概念解读](docs/tools/Docker.md) 、[一文搞懂 Docker 镜像的常用操作!](docs/tools/Docker-Image.md) -### Github +## 面试指南 -- [Github 上非常棒的 Java 开源项目集合](https://github.com/Snailclimb/awesome-java) -- [Github 上 Star 数最多的 10 个项目,看完之后很意外!](docs/tools/github/github-star-ranking.md) -- [年末将至,值得你关注的16个Java 开源项目!](docs/github-trending/2019-12.md) -- [Java 项目历史月榜单](docs/github-trending/JavaGithubTrending.md) +> 这部分很多内容比如大厂面经、真实面经分析被移除,详见[完结撒花!JavaGuide 面试突击版来啦!](./docs/javaguide面试突击版.md)。 -*** +1. **[【备战面试 1】程序员的简历就该这样写](docs/essential-content-for-interview/PreparingForInterview/程序员的简历之道.md)** +2. **[【备战面试 2】初出茅庐的程序员该如何准备面试?](docs/essential-content-for-interview/PreparingForInterview/interviewPrepare.md)** +3. **[【备战面试 3】7 个大部分程序员在面试前很关心的问题](docs/essential-content-for-interview/PreparingForInterview/JavaProgrammerNeedKnow.md)** +4. **[【备战面试 4】Github 上开源的 Java 面试/学习相关的仓库推荐](docs/essential-content-for-interview/PreparingForInterview/JavaInterviewLibrary.md)** +5. **[【备战面试 5】如果面试官问你“你有什么问题问我吗?”时,你该如何回答](docs/essential-content-for-interview/PreparingForInterview/面试官-你有什么问题要问我.md)** +6. [【备战面试 6】应届生面试最爱问的几道 Java 基础问题](docs/essential-content-for-interview/PreparingForInterview/应届生面试最爱问的几道Java基础问题.md) +7. **[【备战面试 6】美团面试常见问题总结(附详解答案)](docs/essential-content-for-interview/PreparingForInterview/美团面试常见问题总结.md)** -## 待办 +## Java 学习常见问题汇总 -- [x] Netty 总结 -- [ ] 数据结构总结重构(---正在进行中---) +1. [Java 学习路线和方法推荐](docs/questions/java-learning-path-and-methods.md) +2. [Java 培训四个月能学会吗?](docs/questions/java-training-4-month.md) +3. [新手学习 Java,有哪些 Java 相关的博客,专栏,和技术学习网站推荐?](docs/questions/java-learning-website-blog.md) +4. [Java 还是大数据,你需要了解这些东西!](docs/questions/java-big-data.md) -## 说明 +## 书单 -开源项目在于大家的参与,这才使得它的价值得到提升。感谢🙏有你! +1. [「基础篇」Java 书单](./docs/books/java基础篇.md) -### JavaGuide介绍 +--- -开源 JavaGuide 初始想法源于自己的个人那一段比较迷茫的学习经历。主要目的是为了通过这个开源平台来帮助一些在学习 Java 或者面试过程中遇到问题的小伙伴。 +## 其他 -* **对于 Java 初学者来说:** 本文档倾向于给你提供一个比较详细的学习路径,让你对于Java整体的知识体系有一个初步认识。另外,本文的一些文章 -也是你学习和复习 Java 知识不错的实践; -* **对于非 Java 初学者来说:** 本文档更适合回顾知识,准备面试,搞清面试应该把重心放在那些问题上。要搞清楚这个道理:提前知道那些面试常见,不是为了背下来应付面试,而是为了让你可以更有针对的学习重点。 +### 贡献者 -Markdown 格式参考:[Github Markdown格式](https://guides.github.com/features/mastering-markdown/),表情素材来自:[EMOJI CHEAT SHEET](https://www.webpagefx.com/tools/emoji-cheat-sheet/)。 +[你可以点此链接查看JavaGuide的所有贡献者。](https://github.com/Snailclimb/JavaGuide/graphs/contributors) 感谢你们让 JavaGuide 变得更好!如果你们来到武汉一定要找我,我请你们吃饭玩耍。 -利用 docsify 生成文档部署在 Github pages: [docsify 官网介绍](https://docsify.js.org/#/) ,另见[《Guide哥手把手教你搭建一个文档类型的网站!免费且高速!》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486555&idx=2&sn=8486026ee9f9ba645ff0363df6036184&chksm=cea24390f9d5ca86ff4177c0aca5e719de17dc89e918212513ee661dd56f17ca8269f4a6e303&token=298703358&lang=zh_CN#rd) 。 +*悄悄话:JavaGuide 会不定时为贡献者们送福利。* -Logo下的小图标是使用[Shields.IO](https://shields.io/) 生成的。 +### 待办 -### 关于转载 +- [x] Netty 总结 +- [ ] 数据结构总结重构 -如果你需要转载本仓库的一些文章到自己的博客的话,记得注明原文地址就可以了。 +### 优质原创PDF资源 -### 如何对该开源文档进行贡献 +![](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images-2@main/%E8%AE%A1%E7%AE%97%E6%9C%BA%E4%B8%93%E4%B8%9A/image-20201027160348395.png) -1. 笔记内容大多是手敲,所以难免会有笔误,你可以帮我找错别字。 -2. 很多知识点我可能没有涉及到,所以你可以对其他知识点进行补充。 -3. 现有的知识点难免存在不完善或者错误,所以你可以对已有知识点进行修改/补充。 +为了避免恶意传播,微信搜“**Github掘金计划**”后台回复 **“006”** 即可获取。 -> 如果要提 issue/question 的话,强烈推荐阅读 [《提问的智慧》](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way)、[《如何向开源社区提问题》](https://github.com/seajs/seajs/issues/545) 和 [《如何有效地报告 Bug》](http://www.chiark.greenend.org.uk/~sgtatham/bugs-cn.html)、[《如何向开源项目提交无法解答的问题》](https://zhuanlan.zhihu.com/p/25795393)。 + -### 联系我 +### 捐赠支持 -![个人微信](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images/2020-08/wechat3.jpeg) +项目的发展离不开你的支持,如果 JavaGuide 帮助到了你找到自己满意的 offer,请作者喝杯咖啡吧 ☕ 后续会继续完善更新!加油! -### Contributor +[点击捐赠支持作者](https://www.yuque.com/snailclimb/dr6cvl/mr44yt#vu3ok) -下面是笔主收集的一些对本仓库提过有价值的pr或者issue的朋友,人数较多,如果你也对本仓库提过不错的pr或者issue的话,你可以加我的微信与我联系。下面的排名不分先后! +### 联系我 - - - - - - - - - - - - + - - - - - - - - - - - - - - - -" - - - - - - - - - - - - - - - - - - - - - ### 公众号 -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 +如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号“**JavaGuide**”。 -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 +**《Java 面试突击》:** 由本文档衍生的专为面试而生的《Java 面试突击》V3.0 PDF 版本[公众号](#公众号)后台回复 **"面试突击"** 即可免费领取! ![我的公众号](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images/2020-08/167598cd2e17b8ec.png) - diff --git a/docs/dataStructures-algorithms/Backtracking-NQueens.md b/docs/dataStructures-algorithms/Backtracking-NQueens.md deleted file mode 100644 index 1e1367d3f98..00000000000 --- a/docs/dataStructures-algorithms/Backtracking-NQueens.md +++ /dev/null @@ -1,145 +0,0 @@ -# N皇后 -[51. N皇后](https://leetcode-cn.com/problems/n-queens/) -### 题目描述 -> n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。 -> -![ANUzjA.png](https://s2.ax1x.com/2019/03/26/ANUzjA.png) -> -上图为 8 皇后问题的一种解法。 -> -给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。 -> -每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。 - -示例: - -``` -输入: 4 -输出: [ - [".Q..", // 解法 1 - "...Q", - "Q...", - "..Q."], - - ["..Q.", // 解法 2 - "Q...", - "...Q", - ".Q.."] -] -解释: 4 皇后问题存在两个不同的解法。 -``` - -### 问题分析 -约束条件为每个棋子所在的行、列、对角线都不能有另一个棋子。 - -使用一维数组表示一种解法,下标(index)表示行,值(value)表示该行的Q(皇后)在哪一列。 -每行只存储一个元素,然后递归到下一行,这样就不用判断行了,只需要判断列和对角线。 -### Solution1 -当result[row] = column时,即row行的棋子在column列。 - -对于[0, row-1]的任意一行(i 行),若 row 行的棋子和 i 行的棋子在同一列,则有result[i] == column; -若 row 行的棋子和 i 行的棋子在同一对角线,等腰直角三角形两直角边相等,即 row - i == Math.abs(result[i] - column) - -布尔类型变量 isValid 的作用是剪枝,减少不必要的递归。 -```java -public List> solveNQueens(int n) { - // 下标代表行,值代表列。如result[0] = 3 表示第1行的Q在第3列 - int[] result = new int[n]; - List> resultList = new LinkedList<>(); - dfs(resultList, result, 0, n); - return resultList; -} - -void dfs(List> resultList, int[] result, int row, int n) { - // 递归终止条件 - if (row == n) { - List list = new LinkedList<>(); - for (int x = 0; x < n; ++x) { - StringBuilder sb = new StringBuilder(); - for (int y = 0; y < n; ++y) - sb.append(result[x] == y ? "Q" : "."); - list.add(sb.toString()); - } - resultList.add(list); - return; - } - for (int column = 0; column < n; ++column) { - boolean isValid = true; - result[row] = column; - /* - * 逐行往下考察每一行。同列,result[i] == column - * 同对角线,row - i == Math.abs(result[i] - column) - */ - for (int i = row - 1; i >= 0; --i) { - if (result[i] == column || row - i == Math.abs(result[i] - column)) { - isValid = false; - break; - } - } - if (isValid) dfs(resultList, result, row + 1, n); - } -} -``` -### Solution2 -使用LinkedList表示一种解法,下标(index)表示行,值(value)表示该行的Q(皇后)在哪一列。 - -解法二和解法一的不同在于,相同列以及相同对角线的校验。 -将对角线抽象成【一次函数】这个简单的数学模型,根据一次函数的截距是常量这一特性进行校验。 - -这里,我将右上-左下对角线,简称为“\”对角线;左上-右下对角线简称为“/”对角线。 - -“/”对角线斜率为1,对应方程为y = x + b,其中b为截距。 -对于线上任意一点,均有y - x = b,即row - i = b; -定义一个布尔类型数组anti_diag,将b作为下标,当anti_diag[b] = true时,表示相应对角线上已经放置棋子。 -但row - i有可能为负数,负数不能作为数组下标,row - i 的最小值为-n(当row = 0,i = n时),可以加上n作为数组下标,即将row -i + n 作为数组下标。 -row - i + n 的最大值为 2n(当row = n,i = 0时),故anti_diag的容量设置为 2n 即可。 - -![ANXG79.png](https://s2.ax1x.com/2019/03/26/ANXG79.png) - -“\”对角线斜率为-1,对应方程为y = -x + b,其中b为截距。 -对于线上任意一点,均有y + x = b,即row + i = b; -同理,定义数组main_diag,将b作为下标,当main_diag[row + i] = true时,表示相应对角线上已经放置棋子。 - -有了两个校验对角线的数组,再来定义一个用于校验列的数组cols,这个太简单啦,不解释。 - -**解法二时间复杂度为O(n!),在校验相同列和相同对角线时,引入三个布尔类型数组进行判断。相比解法一,少了一层循环,用空间换时间。** - -```java -List> resultList = new LinkedList<>(); - -public List> solveNQueens(int n) { - boolean[] cols = new boolean[n]; - boolean[] main_diag = new boolean[2 * n]; - boolean[] anti_diag = new boolean[2 * n]; - LinkedList result = new LinkedList<>(); - dfs(result, 0, cols, main_diag, anti_diag, n); - return resultList; -} - -void dfs(LinkedList result, int row, boolean[] cols, boolean[] main_diag, boolean[] anti_diag, int n) { - if (row == n) { - List list = new LinkedList<>(); - for (int x = 0; x < n; ++x) { - StringBuilder sb = new StringBuilder(); - for (int y = 0; y < n; ++y) - sb.append(result.get(x) == y ? "Q" : "."); - list.add(sb.toString()); - } - resultList.add(list); - return; - } - for (int i = 0; i < n; ++i) { - if (cols[i] || main_diag[row + i] || anti_diag[row - i + n]) - continue; - result.add(i); - cols[i] = true; - main_diag[row + i] = true; - anti_diag[row - i + n] = true; - dfs(result, row + 1, cols, main_diag, anti_diag, n); - result.removeLast(); - cols[i] = false; - main_diag[row + i] = false; - anti_diag[row - i + n] = false; - } -} -``` \ No newline at end of file diff --git a/docs/dataStructures-algorithms/data-structure/bloom-filter.md b/docs/dataStructures-algorithms/data-structure/bloom-filter.md index 21663ba84b8..b9d129d28cd 100644 --- a/docs/dataStructures-algorithms/data-structure/bloom-filter.md +++ b/docs/dataStructures-algorithms/data-structure/bloom-filter.md @@ -39,7 +39,7 @@ ![布隆过滤器hash计算](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/布隆过滤器-hash运算.png) -如图所示,当字符串存储要加入到布隆过滤器中时,该字符串首先由多个哈希函数生成不同的哈希值,然后在对应的位数组的下表的元素设置为 1(当位数组初始化时 ,所有位置均为0)。当第二次存储相同字符串时,因为先前的对应位置已设置为1,所以很容易知道此值已经存在(去重非常方便)。 +如图所示,当字符串存储要加入到布隆过滤器中时,该字符串首先由多个哈希函数生成不同的哈希值,然后在对应的位数组的下表的元素设置为 1(当位数组初始化时 ,所有位置均为0)。当第二次存储相同字符串时,因为先前的对应位置已设置为 1,所以很容易知道此值已经存在(去重非常方便)。 如果我们需要判断某个字符串是否在布隆过滤器中时,只需要对给定字符串再次进行相同的哈希计算,得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。 @@ -49,7 +49,7 @@ ### 3.布隆过滤器使用场景 -1. 判断给定数据是否存在:比如判断一个数字是否在于包含大量数字的数字集中(数字集很大,5亿以上!)、 防止缓存穿透(判断请求的数据是否有效避免直接绕过缓存请求数据库)等等、邮箱的垃圾邮件过滤、黑名单功能等等。 +1. 判断给定数据是否存在:比如判断一个数字是否存在于包含大量数字的数字集中(数字集很大,5亿以上!)、 防止缓存穿透(判断请求的数据是否有效避免直接绕过缓存请求数据库)等等、邮箱的垃圾邮件过滤、黑名单功能等等。 2. 去重:比如爬给定网址的时候对已经爬取过的 URL 去重。 ### 4.通过 Java 编程手动实现布隆过滤器 diff --git a/docs/dataStructures-algorithms/images/Github-CodingInterviews.png b/docs/dataStructures-algorithms/images/Github-CodingInterviews.png deleted file mode 100644 index a02d8a12e6e..00000000000 Binary files a/docs/dataStructures-algorithms/images/Github-CodingInterviews.png and /dev/null differ diff --git "a/docs/dataStructures-algorithms/images/\345\211\221\346\214\207Offer.png" "b/docs/dataStructures-algorithms/images/\345\211\221\346\214\207Offer.png" deleted file mode 100644 index 90c296a01fe..00000000000 Binary files "a/docs/dataStructures-algorithms/images/\345\211\221\346\214\207Offer.png" and /dev/null differ diff --git "a/docs/dataStructures-algorithms/images/\345\225\212\345\223\210!\347\256\227\346\263\225.png" "b/docs/dataStructures-algorithms/images/\345\225\212\345\223\210!\347\256\227\346\263\225.png" deleted file mode 100644 index 5a7c48cbb88..00000000000 Binary files "a/docs/dataStructures-algorithms/images/\345\225\212\345\223\210!\347\256\227\346\263\225.png" and /dev/null differ diff --git "a/docs/dataStructures-algorithms/images/\346\210\221\347\232\204\347\254\254\344\270\200\346\234\254\347\256\227\346\263\225\344\271\246.png" "b/docs/dataStructures-algorithms/images/\346\210\221\347\232\204\347\254\254\344\270\200\346\234\254\347\256\227\346\263\225\344\271\246.png" deleted file mode 100644 index 17c0418e2c3..00000000000 Binary files "a/docs/dataStructures-algorithms/images/\346\210\221\347\232\204\347\254\254\344\270\200\346\234\254\347\256\227\346\263\225\344\271\246.png" and /dev/null differ diff --git "a/docs/dataStructures-algorithms/images/\347\250\213\345\272\217\345\221\230\344\273\243\347\240\201\351\235\242\350\257\225\346\214\207\345\215\227.png" "b/docs/dataStructures-algorithms/images/\347\250\213\345\272\217\345\221\230\344\273\243\347\240\201\351\235\242\350\257\225\346\214\207\345\215\227.png" deleted file mode 100644 index fa50afece6d..00000000000 Binary files "a/docs/dataStructures-algorithms/images/\347\250\213\345\272\217\345\221\230\344\273\243\347\240\201\351\235\242\350\257\225\346\214\207\345\215\227.png" and /dev/null differ diff --git "a/docs/dataStructures-algorithms/images/\347\256\227\346\263\225-4.png" "b/docs/dataStructures-algorithms/images/\347\256\227\346\263\225-4.png" deleted file mode 100644 index bbfd3b5c977..00000000000 Binary files "a/docs/dataStructures-algorithms/images/\347\256\227\346\263\225-4.png" and /dev/null differ diff --git "a/docs/dataStructures-algorithms/images/\347\256\227\346\263\225\345\233\276\350\247\243.png" "b/docs/dataStructures-algorithms/images/\347\256\227\346\263\225\345\233\276\350\247\243.png" deleted file mode 100644 index ce1edb0acc8..00000000000 Binary files "a/docs/dataStructures-algorithms/images/\347\256\227\346\263\225\345\233\276\350\247\243.png" and /dev/null differ diff --git "a/docs/dataStructures-algorithms/images/\347\256\227\346\263\225\345\257\274\350\256\272.png" "b/docs/dataStructures-algorithms/images/\347\256\227\346\263\225\345\257\274\350\256\272.png" deleted file mode 100644 index fc1f52b5b4a..00000000000 Binary files "a/docs/dataStructures-algorithms/images/\347\256\227\346\263\225\345\257\274\350\256\272.png" and /dev/null differ diff --git "a/docs/dataStructures-algorithms/images/\347\256\227\346\263\225\350\256\276\350\256\241\346\211\213\345\206\214.png" "b/docs/dataStructures-algorithms/images/\347\256\227\346\263\225\350\256\276\350\256\241\346\211\213\345\206\214.png" deleted file mode 100644 index 1fed6659ad8..00000000000 Binary files "a/docs/dataStructures-algorithms/images/\347\256\227\346\263\225\350\256\276\350\256\241\346\211\213\345\206\214.png" and /dev/null differ diff --git "a/docs/dataStructures-algorithms/images/\347\274\226\347\250\213\344\271\213\347\276\216.png" "b/docs/dataStructures-algorithms/images/\347\274\226\347\250\213\344\271\213\347\276\216.png" deleted file mode 100644 index d137b6188ea..00000000000 Binary files "a/docs/dataStructures-algorithms/images/\347\274\226\347\250\213\344\271\213\347\276\216.png" and /dev/null differ diff --git "a/docs/dataStructures-algorithms/images/\347\274\226\347\250\213\347\217\240\347\216\221.png" "b/docs/dataStructures-algorithms/images/\347\274\226\347\250\213\347\217\240\347\216\221.png" deleted file mode 100644 index 7695d88eb7d..00000000000 Binary files "a/docs/dataStructures-algorithms/images/\347\274\226\347\250\213\347\217\240\347\216\221.png" and /dev/null differ diff --git "a/docs/dataStructures-algorithms/images/\350\256\241\347\256\227\346\234\272\347\250\213\345\272\217\350\256\276\350\256\241\350\211\272\346\234\257.png" "b/docs/dataStructures-algorithms/images/\350\256\241\347\256\227\346\234\272\347\250\213\345\272\217\350\256\276\350\256\241\350\211\272\346\234\257.png" deleted file mode 100644 index 0a86066d06d..00000000000 Binary files "a/docs/dataStructures-algorithms/images/\350\256\241\347\256\227\346\234\272\347\250\213\345\272\217\350\256\276\350\256\241\350\211\272\346\234\257.png" and /dev/null differ diff --git "a/docs/dataStructures-algorithms/\345\205\254\345\217\270\347\234\237\351\242\230.md" "b/docs/dataStructures-algorithms/\345\205\254\345\217\270\347\234\237\351\242\230.md" deleted file mode 100644 index c78ed8f3e5a..00000000000 --- "a/docs/dataStructures-algorithms/\345\205\254\345\217\270\347\234\237\351\242\230.md" +++ /dev/null @@ -1,254 +0,0 @@ -# 网易 2018 - -下面三道编程题来自网易2018校招编程题,这三道应该来说是非常简单的编程题了,这些题目大家稍微有点编程和数学基础的话应该没什么问题。看答案之前一定要自己先想一下如果是自己做的话会怎么去做,然后再对照这我的答案看看,和你自己想的有什么区别?那一种方法更好? - -## 问题 - -### 一 获得特定数量硬币问题 - -小易准备去魔法王国采购魔法神器,购买魔法神器需要使用魔法币,但是小易现在一枚魔法币都没有,但是小易有两台魔法机器可以通过投入x(x可以为0)个魔法币产生更多的魔法币。 - -魔法机器1:如果投入x个魔法币,魔法机器会将其变为2x+1个魔法币 - -魔法机器2:如果投入x个魔法币,魔法机器会将其变为2x+2个魔法币 - -小易采购魔法神器总共需要n个魔法币,所以小易只能通过两台魔法机器产生恰好n个魔法币,小易需要你帮他设计一个投入方案使他最后恰好拥有n个魔法币。 - -**输入描述:** 输入包括一行,包括一个正整数n(1 ≤ n ≤ 10^9),表示小易需要的魔法币数量。 - -**输出描述:** 输出一个字符串,每个字符表示该次小易选取投入的魔法机器。其中只包含字符'1'和'2'。 - -**输入例子1:** 10 - -**输出例子1:** 122 - -### 二 求“相反数”问题 - -为了得到一个数的"相反数",我们将这个数的数字顺序颠倒,然后再加上原先的数得到"相反数"。例如,为了得到1325的"相反数",首先我们将该数的数字顺序颠倒,我们得到5231,之后再加上原先的数,我们得到5231+1325=6556.如果颠倒之后的数字有前缀零,前缀零将会被忽略。例如n = 100, 颠倒之后是1. - -**输入描述:** 输入包括一个整数n,(1 ≤ n ≤ 10^5) - -**输出描述:** 输出一个整数,表示n的相反数 - -**输入例子1:** 1325 - -**输出例子1:** 6556 - -### 三 字符串碎片的平均长度 - -一个由小写字母组成的字符串可以看成一些同一字母的最大碎片组成的。例如,"aaabbaaac"是由下面碎片组成的:'aaa','bb','c'。牛牛现在给定一个字符串,请你帮助计算这个字符串的所有碎片的平均长度是多少。 - -**输入描述:** 输入包括一个字符串s,字符串s的长度length(1 ≤ length ≤ 50),s只含小写字母('a'-'z') - -**输出描述:** 输出一个整数,表示所有碎片的平均长度,四舍五入保留两位小数。 - -**如样例所示:** s = "aaabbaaac" -所有碎片的平均长度 = (3 + 2 + 3 + 1) / 4 = 2.25 - -**输入例子1:** aaabbaaac - -**输出例子1:** 2.25 - -## 答案 - -### 一 获得特定数量硬币问题 - -#### 分析: - -作为该试卷的第一题,这道题应该只要思路正确就很简单了。 - -解题关键:明确魔法机器1只能产生奇数,魔法机器2只能产生偶数即可。我们从后往前一步一步推回去即可。 - -#### 示例代码 - -注意:由于用户的输入不确定性,一般是为了程序高可用性使需要将捕获用户输入异常然后友好提示用户输入类型错误并重新输入的。所以下面我给了两个版本,这两个版本都是正确的。这里只是给大家演示如何捕获输入类型异常,后面的题目中我给的代码没有异常处理的部分,参照下面两个示例代码,应该很容易添加。(PS:企业面试中没有明确就不用添加异常处理,当然你有的话也更好) - -**不带输入异常处理判断的版本:** - -```java -import java.util.Scanner; - -public class Main2 { - // 解题关键:明确魔法机器1只能产生奇数,魔法机器2只能产生偶数即可。我们从后往前一步一步推回去即可。 - - public static void main(String[] args) { - System.out.println("请输入要获得的硬币数量:"); - Scanner scanner = new Scanner(System.in); - int coincount = scanner.nextInt(); - StringBuilder sb = new StringBuilder(); - while (coincount >= 1) { - // 偶数的情况 - if (coincount % 2 == 0) { - coincount = (coincount - 2) / 2; - sb.append("2"); - // 奇数的情况 - } else { - coincount = (coincount - 1) / 2; - sb.append("1"); - } - } - // 输出反转后的字符串 - System.out.println(sb.reverse()); - - } -} -``` - -**带输入异常处理判断的版本(当输入的不是整数的时候会提示重新输入):** - -```java -import java.util.InputMismatchException; -import java.util.Scanner; - - -public class Main { - // 解题关键:明确魔法机器1只能产生奇数,魔法机器2只能产生偶数即可。我们从后往前一步一步推回去即可。 - - public static void main(String[] args) { - System.out.println("请输入要获得的硬币数量:"); - Scanner scanner = new Scanner(System.in); - boolean flag = true; - while (flag) { - try { - int coincount = scanner.nextInt(); - StringBuilder sb = new StringBuilder(); - while (coincount >= 1) { - // 偶数的情况 - if (coincount % 2 == 0) { - coincount = (coincount - 2) / 2; - sb.append("2"); - // 奇数的情况 - } else { - coincount = (coincount - 1) / 2; - sb.append("1"); - } - } - // 输出反转后的字符串 - System.out.println(sb.reverse()); - flag=false;//程序结束 - } catch (InputMismatchException e) { - System.out.println("输入数据类型不匹配,请您重新输入:"); - scanner.nextLine(); - continue; - } - } - - } -} - -``` - -### 二 求“相反数”问题 - -#### 分析: - -解决本道题有几种不同的方法,但是最快速的方法就是利用reverse()方法反转字符串然后再将字符串转换成int类型的整数,这个方法是快速解决本题关键。我们先来回顾一下下面两个知识点: - -**1)String转int;** - -在 Java 中要将 String 类型转化为 int 类型时,需要使用 Integer 类中的 parseInt() 方法或者 valueOf() 方法进行转换. - -```java - String str = "123"; - int a = Integer.parseInt(str); -``` - - 或 - -```java - String str = "123"; - int a = Integer.valueOf(str).intValue(); -``` - -**2)next()和nextLine()的区别** - -在Java中输入字符串有两种方法,就是next()和nextLine().两者的区别就是:nextLine()的输入是碰到回车就终止输入,而next()方法是碰到空格,回车,Tab键都会被视为终止符。所以next()不会得到带空格的字符串,而nextLine()可以得到带空格的字符串。 - -#### 示例代码: - -```java -import java.util.Scanner; - -/** - * 本题关键:①String转int;②next()和nextLine()的区别 - */ -public class Main { - - public static void main(String[] args) { - - System.out.println("请输入一个整数:"); - Scanner scanner = new Scanner(System.in); - String s=scanner.next(); - //将字符串转换成数字 - int number1=Integer.parseInt(s); - //将字符串倒序后转换成数字 - //因为Integer.parseInt()的参数类型必须是字符串所以必须加上toString() - int number2=Integer.parseInt(new StringBuilder(s).reverse().toString()); - System.out.println(number1+number2); - - } -} -``` - -### 三 字符串碎片的平均长度 - -#### 分析: - -这道题的意思也就是要求:(字符串的总长度)/(相同字母团构成的字符串的个数)。 - -这样就很简单了,就变成了字符串的字符之间的比较。如果需要比较字符串的字符的话,我们可以利用charAt(i)方法:取出特定位置的字符与后一个字符比较,或者利用toCharArray()方法将字符串转换成字符数组采用同样的方法做比较。 - -#### 示例代码 - -**利用charAt(i)方法:** - -```java -import java.util.Scanner; - -public class Main { - - public static void main(String[] args) { - - Scanner sc = new Scanner(System.in); - while (sc.hasNext()) { - String s = sc.next(); - //个数至少为一个 - float count = 1; - for (int i = 0; i < s.length() - 1; i++) { - if (s.charAt(i) != s.charAt(i + 1)) { - count++; - } - } - System.out.println(s.length() / count); - } - } - -} -``` - -**利用toCharArray()方法:** - -```java -import java.util.Scanner; - -public class Main2 { - - public static void main(String[] args) { - - Scanner sc = new Scanner(System.in); - while (sc.hasNext()) { - String s = sc.next(); - //个数至少为一个 - float count = 1; - char [] stringArr = s.toCharArray(); - for (int i = 0; i < stringArr.length - 1; i++) { - if (stringArr[i] != stringArr[i + 1]) { - count++; - } - } - System.out.println(s.length() / count); - } - } - -} -``` \ No newline at end of file diff --git "a/docs/dataStructures-algorithms/\345\207\240\351\201\223\345\270\270\350\247\201\347\232\204\351\223\276\350\241\250\347\256\227\346\263\225\351\242\230.md" "b/docs/dataStructures-algorithms/\345\207\240\351\201\223\345\270\270\350\247\201\347\232\204\351\223\276\350\241\250\347\256\227\346\263\225\351\242\230.md" index 85e2934e407..9daa0fc159c 100644 --- "a/docs/dataStructures-algorithms/\345\207\240\351\201\223\345\270\270\350\247\201\347\232\204\351\223\276\350\241\250\347\256\227\346\263\225\351\242\230.md" +++ "b/docs/dataStructures-algorithms/\345\207\240\351\201\223\345\270\270\350\247\201\347\232\204\351\223\276\350\241\250\347\256\227\346\263\225\351\242\230.md" @@ -324,7 +324,7 @@ public class Solution { **进阶——一次遍历法:** -> **链表中倒数第N个节点也就是正数第(L-N+1)个节点。 +> 链表中倒数第N个节点也就是正数第(L-N+1)个节点。 其实这种方法就和我们上面第四题找“链表中倒数第k个节点”所用的思想是一样的。**基本思路就是:** 定义两个节点 node1、node2;node1 节点先跑,node1节点 跑到第 n+1 个节点的时候,node2 节点开始跑.当node1 节点跑到最后一个节点时,node2 节点所在的位置就是第 (L-n ) 个节点(L代表总链表长度,也就是倒数第 n+1 个节点) diff --git "a/docs/dataStructures-algorithms/\346\225\260\346\215\256\347\273\223\346\236\204.md" "b/docs/dataStructures-algorithms/\346\225\260\346\215\256\347\273\223\346\236\204.md" index 3a117dd36a4..314b2a855ab 100644 --- "a/docs/dataStructures-algorithms/\346\225\260\346\215\256\347\273\223\346\236\204.md" +++ "b/docs/dataStructures-algorithms/\346\225\260\346\215\256\347\273\223\346\236\204.md" @@ -1,5 +1,5 @@ -下面只是简单地总结,给了一些参考文章,后面会对这部分内容进行重构。 - +> 注意!!!这部分内容会进行重构,以下内容仅作为参考。 +> - [Queue](#queue) - [什么是队列](#什么是队列) @@ -38,10 +38,6 @@ Java 集合中的 Queue 继承自 Collection 接口 ,Deque, LinkedList, Priori Queue 用来存放 等待处理元素 的集合,这种场景一般用于缓冲、并发访问。 除了继承 Collection 接口的一些方法,Queue 还添加了额外的 添加、删除、查询操作。 -### 推荐文章 - -- [Java 集合深入理解(9):Queue 队列](https://blog.csdn.net/u011240877/article/details/52860924) - ## Set ### 什么是 Set @@ -60,11 +56,6 @@ Set 继承于 Collection 接口,是一个不允许出现重复元素,并且 **TreeSet** 是红黑树结构,每一个元素都是树中的一个节点,插入的元素都会进行排序; - -### 推荐文章 - -- [Java集合--Set(基础)](https://www.jianshu.com/p/b48c47a42916) - ## List ### 什么是List @@ -79,23 +70,7 @@ Set 继承于 Collection 接口,是一个不允许出现重复元素,并且 **Vector** 是矢量队列,和ArrayList一样,它也是一个动态数组,由数组实现。但是ArrayList是非线程安全的,而Vector是线程安全的。 -**Stack** 是栈,它继承于Vector。它的特性是:先进后出(FILO, First In Last Out)。相关阅读:[java数据结构与算法之栈(Stack)设计与实现](https://blog.csdn.net/javazejian/article/details/53362993) - -### ArrayList 和 LinkedList 源码学习 - -- [ArrayList 源码学习](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/ArrayList.md) -- [LinkedList 源码学习](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/LinkedList.md) - -### 推荐阅读 - -- [java 数据结构与算法之顺序表与链表深入分析](https://blog.csdn.net/javazejian/article/details/52953190) - - -## Map - - -- [集合框架源码学习之 HashMap(JDK1.8)](https://juejin.im/post/5ab0568b5188255580020e56) -- [ConcurrentHashMap 实现原理及源码分析](https://www.cnblogs.com/chengxiao/p/6842045.html) +**Stack** 是栈,它继承于Vector。它的特性是:先进后出(FILO, First In Last Out)。 ## 树 @@ -129,7 +104,7 @@ Set 继承于 Collection 接口,是一个不允许出现重复元素,并且 ### 4 二叉查找树(BST) -[浅谈算法和数据结构: 七 二叉查找树](http://www.cnblogs.com/yangecnu/p/Introduce-Binary-Search-Tree.html) +[浅谈算法和数据结构: 七 二叉查找树](https://www.yycoding.xyz/post/2014/3/24/introduce-binary-search-tree) 二叉查找树的特点: diff --git "a/docs/dataStructures-algorithms/\347\256\227\346\263\225\345\255\246\344\271\240\350\265\204\346\272\220\346\216\250\350\215\220.md" "b/docs/dataStructures-algorithms/\347\256\227\346\263\225\345\255\246\344\271\240\350\265\204\346\272\220\346\216\250\350\215\220.md" deleted file mode 100644 index 5af08c9d30d..00000000000 --- "a/docs/dataStructures-algorithms/\347\256\227\346\263\225\345\255\246\344\271\240\350\265\204\346\272\220\346\216\250\350\215\220.md" +++ /dev/null @@ -1,142 +0,0 @@ -先占个坑,说一下我觉得算法这部分学习比较好的规划: - -1. 未入门(对算法和基本数据结构不了解)之前建议先找一本入门书籍看; -2. 如果时间比较多可以看一下我推荐的经典部分的书籍,《算法》这本书是首要要看的,其他推荐的神书看自己时间和心情就好,不要太纠结。 -3. 如果要准备面试,时间比较紧的话,就不需要再去看《算法》这本书了,时间来不及,当然你也可以选取其特定的章节查看。我也推荐了几本不错的专门为算法面试准备的书籍比如《剑指offer》和《程序员代码面试指南》。除了这两本书籍的话,我在下面推荐了 Leetcode 和牛客网这两个常用的刷题网站以及一些比较好的题目资源。 - -## 书籍推荐 - -> 以下提到的部分书籍的 PDF 高清阅读版本在我的公众号“JavaGuide”后台回复“书籍”即可获取。 - -先来看三本入门书籍,这三本入门书籍中的任何一本拿来作为入门学习都非常好。我个人比较倾向于 **《我的第一本算法书》** 这本书籍,虽然它相比于其他两本书集它的豆瓣评分略低一点。我觉得它的配图以及讲解是这三本书中最优秀,唯一比较明显的问题就是没有代码示例。但是,我觉得这不影响它是一本好的算法书籍。因为本身下面这三本入门书籍的目的就不是通过代码来让你的算法有多厉害,只是作为一本很好的入门书籍让你进入算法学习的大门。 - -### 入门 - - - -**[我的第一本算法书](https://book.douban.com/subject/30357170/) (豆瓣评分 7.1,0.2K+人评价)** - -一本不那么“专业”的算法书籍。和下面两本推荐的算法书籍都是比较通俗易懂,“不那么深入”的算法书籍。我个人非常推荐,配图和讲解都非常不错! - -img - -**[《算法图解》](https://book.douban.com/subject/26979890/)(豆瓣评分 8.4,1.5K+人评价)** - -入门类型的书籍,读起来比较浅显易懂,非常适合没有算法基础或者说算法没学好的小伙伴用来入门。示例丰富,图文并茂,以让人容易理解的方式阐释了算法.读起来比较快,内容不枯燥! - -![啊哈!算法](images/啊哈!算法.png) - -**[啊哈!算法](https://book.douban.com/subject/25894685/) (豆瓣评分 7.7,0.5K+人评价)** - -和《算法图解》类似的算法趣味入门书籍。 - -### 经典 - - - -**[《算法 第四版》](https://book.douban.com/subject/10432347/)(豆瓣评分 9.3,0.4K+人评价)** - -我在大二的时候被我们的一个老师强烈安利过!自己也在当时购买了一本放在宿舍,到离开大学的时候自己大概看了一半多一点。因为内容实在太多了!另外,这本书还提供了详细的Java代码,非常适合学习 Java 的朋友来看,可以说是 Java 程序员的必备书籍之一了。 - -再来介绍一下这本书籍吧!这本书籍算的上是算法领域经典的参考书,全面介绍了关于算法和数据结构的必备知识,并特别针对排序、搜索、图处理和字符串处理进行了论述。 - -> **下面这些书籍都是经典中的经典,但是阅读起来难度也比较大,不做太多阐述,神书就完事了!推荐先看 《算法》,然后再选下面的书籍进行进一步阅读。不需要都看,找一本好好看或者找某本书的某一个章节知识点好好看。** - - - -**[编程珠玑](https://book.douban.com/subject/3227098/)(豆瓣评分 9.1,2K+人评价)** - -经典名著,被无数读者强烈推荐的书籍,几乎是顶级程序员必看的书籍之一了。这本书的作者也非常厉害,Java之父 James Gosling 就是他的学生。 - -很多人都说这本书不是教你具体的算法,而是教你一种编程的思考方式。这种思考方式不仅仅在编程领域适用,在其他同样适用。 - - - - - -**[《算法设计手册》](https://book.douban.com/subject/4048566/)(豆瓣评分9.1 , 45人评价)** - -被 [Teach Yourself Computer Science](https://teachyourselfcs.com/) 强烈推荐的一本算法书籍。 - - - -**[《算法导论》](https://book.douban.com/subject/20432061/) (豆瓣评分 9.2,0.4K+人评价)** - -![](images/计算机程序设计艺术.png) - -**[《计算机程序设计艺术(第1卷)》](https://book.douban.com/subject/1130500/)(豆瓣评分 9.4,0.4K+人评价)** - -### 面试 - -![](images/剑指Offer.png) - -**[《剑指Offer》](https://book.douban.com/subject/6966465/)(豆瓣评分 8.3,0.7K+人评价)** - -这本面试宝典上面涵盖了很多经典的算法面试题,如果你要准备大厂面试的话一定不要错过这本书。 - -《剑指Offer》 对应的算法编程题部分的开源项目解析:[CodingInterviews](https://github.com/gatieme/CodingInterviews) - -![](images/Github-CodingInterviews.png) - - - - - -**[程序员代码面试指南:IT名企算法与数据结构题目最优解(第2版)](https://book.douban.com/subject/30422021/) (豆瓣评分 8.7,0.2K+人评价)** - -题目相比于《剑指 offer》 来说要难很多,题目涵盖面相比于《剑指 offer》也更加全面。全书一共有将近300道真实出现过的经典代码面试题。 - - - - - - - -**[编程之美](https://book.douban.com/subject/3004255/)(豆瓣评分 8.4,3K+人评价)** - -这本书收集了约60道算法和程序设计题目,这些题目大部分在近年的笔试、面试中出现过,或者是被微软员工热烈讨论过。作者试图从书中各种有趣的问题出发,引导读者发现问题,分析问题,解决问题,寻找更优的解法。 - -## 网站推荐 - -我比较推荐大家可以刷一下 Leetcode ,我自己平时没事也会刷一下,我觉得刷 Leetcode 不仅是为了能让你更从容地面对面试中的手撕算法问题,更可以提高你的编程思维能力、解决问题的能力以及你对某门编程语言 API 的熟练度。当然牛客网也有一些算法题,我下面也整理了一些。 - -### [LeetCode](https://leetcode-cn.com/) - -[如何高效地使用 LeetCode](https://leetcode-cn.com/articles/%E5%A6%82%E4%BD%95%E9%AB%98%E6%95%88%E5%9C%B0%E4%BD%BF%E7%94%A8-leetcode/) - -- [《程序员代码面试指南》](https://leetcode-cn.com/problemset/lcci/) -- [《剑指offer》](https://leetcode-cn.com/problemset/lcof/) - - -### [牛客网](https://www.nowcoder.com) - -**[在线编程](https://www.nowcoder.com/activity/oj):** - -- [《剑指offer》](https://www.nowcoder.com/ta/coding-interviews) -- [《程序员代码面试指南》](https://www.nowcoder.com/ta/programmer-code-interview-guide) -- [2019 校招真题](https://www.nowcoder.com/ta/2019test) -- [大一大二编程入门训练](https://www.nowcoder.com/ta/beginner-programmers) -- ....... - -**[大厂编程面试真题](https://www.nowcoder.com/contestRoom?filter=0&orderByHotValue=3&target=content&categories=-1&mutiTagIds=2491&page=1)** - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/database/MySQL Index.md b/docs/database/MySQL Index.md index df3f0a05149..8003a69faa3 100644 --- a/docs/database/MySQL Index.md +++ b/docs/database/MySQL Index.md @@ -24,7 +24,7 @@ 5. 在经常用在连接的列上,这 些列主要是一些外键,可以加快连接的速度; -6. 避免 where 子句中对宇段施加函数,这会造成无法命中索引。 +6. 避免 where 子句中对字段施加函数,这会造成无法命中索引。 7. 在使用InnoDB时使用与业务无关的自增主键作为主键,即使用逻辑主键,而不要使用业务主键。 @@ -75,7 +75,7 @@ select username , age from user where username = 'Java' and age = 22 ## 选择索引和编写利用这些索引的查询的3个原则 1. 单行访问是很慢的。特别是在机械硬盘存储中(SSD的随机I/O要快很多,不过这一点仍然成立)。如果服务器从存储中读取一个数据块只是为了获取其中一行,那么就浪费了很多工作。最好读取的块中能包含尽可能多所需要的行。使用索引可以创建位置引,用以提升效率。 -2. 按顺序访问范围数据是很快的,这有两个原因。第一,顺序1/0不需要多次磁盘寻道,所以比随机I/O要快很多(特别是对机械硬盘)。第二,如果服务器能够按需要顺序读取数据,那么就不再需要额外的排序操作,并且GROUPBY查询也无须再做排序和将行按组进行聚合计算了。 +2. 按顺序访问范围数据是很快的,这有两个原因。第一,顺序 I/O 不需要多次磁盘寻道,所以比随机I/O要快很多(特别是对机械硬盘)。第二,如果服务器能够按需要顺序读取数据,那么就不再需要额外的排序操作,并且GROUPBY查询也无须再做排序和将行按组进行聚合计算了。 3. 索引覆盖查询是很快的。如果一个索引包含了查询需要的所有列,那么存储引擎就 不需要再回表查找行。这避免了大量的单行访问,而上面的第1点已经写明单行访 问是很慢的。 diff --git a/docs/database/MySQL.md b/docs/database/MySQL.md index 8fd54b7d490..efd1561a08a 100644 --- a/docs/database/MySQL.md +++ b/docs/database/MySQL.md @@ -98,7 +98,7 @@ MyISAM是MySQL的默认数据库引擎(5.5版之前)。虽然性能极佳, **两者的对比:** 1. **是否支持行级锁** : MyISAM 只有表级锁(table-level locking),而InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。 -2. **是否支持事务和崩溃后的安全恢复: MyISAM** 强调的是性能,每次查询具有原子性,其执行速度比InnoDB类型更快,但是不提供事务支持。但是**InnoDB** 提供事务支持事务,外部键等高级数据库功能。 具有事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant))型表。 +2. **是否支持事务和崩溃后的安全恢复: MyISAM** 强调的是性能,每次查询具有原子性,其执行速度比InnoDB类型更快,但是不提供事务支持。但是**InnoDB** 提供事务支持,外部键等高级数据库功能。 具有事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant))型表。 3. **是否支持外键:** MyISAM不支持,而InnoDB支持。 4. **是否支持MVCC** :仅 InnoDB 支持。应对高并发事务, MVCC比单纯的加锁更高效;MVCC只在 `READ COMMITTED` 和 `REPEATABLE READ` 两个隔离级别下工作;MVCC可以使用 乐观(optimistic)锁 和 悲观(pessimistic)锁来实现;各数据库中MVCC实现并不统一。推荐阅读:[MySQL-InnoDB-MVCC多版本并发控制](https://segmentfault.com/a/1190000012650596) 5. ...... @@ -148,7 +148,7 @@ set global query_cache_size=600000; 缓存建立之后,MySQL的查询缓存系统会跟踪查询中涉及的每张表,如果这些表(数据或结构)发生变化,那么和这张表相关的所有缓存数据都将失效。 -**缓存虽然能够提升数据库的查询性能,但是缓存同时也带来了额外的开销,每次查询后都要做一次缓存操作,失效后还要销毁。** 因此,开启缓存查询要谨慎,尤其对于写密集的应用来说更是如此。如果开启,要注意合理控制缓存空间大小,一般来说其大小设置为几十MB比较合适。此外,**还可以通过sql_cache和sql_no_cache来控制某个查询语句是否需要缓存:** +**缓存虽然能够提升数据库的查询性能,但是缓存同时也带来了额外的开销,每次查询后都要做一次缓存操作,失效后还要销毁。** 因此,开启查询缓存要谨慎,尤其对于写密集的应用来说更是如此。如果开启,要注意合理控制缓存空间大小,一般来说其大小设置为几十MB比较合适。此外,**还可以通过sql_cache和sql_no_cache来控制某个查询语句是否需要缓存:** ```sql select sql_no_cache count(*) from usr; ``` @@ -159,12 +159,12 @@ select sql_no_cache count(*) from usr; 事务最经典也经常被拿出来说例子就是转账了。假如小明要给小红转账1000元,这个转账会涉及到两个关键操作就是:将小明的余额减少1000元,将小红的余额增加1000元。万一在这两个操作之间突然出现错误比如银行系统崩溃,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。 -### 事物的四大特性(ACID) +### 事务的四大特性(ACID) -![事物的特性](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/事务特性.png) +![事务的特性](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/事务特性.png) 1. **原子性(Atomicity):** 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用; -2. **一致性(Consistency):** 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的; +2. **一致性(Consistency):** 执行事务后,数据库从一个正确的状态变化到另一个正确的状态; 3. **隔离性(Isolation):** 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的; 4. **持久性(Durability):** 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。 diff --git "a/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-read.drawio" "b/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-read.drawio" new file mode 100644 index 00000000000..bc4c6d0cca7 --- /dev/null +++ "b/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-read.drawio" @@ -0,0 +1 @@ +7Vpbc+o2EP41ekzHl/j2iAmk02nmdA6dpn3qCFsYTWSLY0Qg/fWVbMnYluE4B4jTQh4Se6WVVt+32pU2BvY43T3mcLV8ojEiwDLiHbAfgGWZluPzP0LyVkoCwygFSY5j2WkvmOF/kBSqbhsco3WjI6OUMLxqCiOaZShiDRnMc7ptdltQ0px1BROkCWYRJLr0GcdsWUp9y9vLf0Y4WaqZTTcoW1KoOsuVrJcwptuayJ4Ae5xTysqndDdGRICncCn1pgdaK8NylLE+Ck/Pf/xthHPraf7NePliLeLZ1+c7OcorJBu5YGkse1MI5HSTxUgMYgI73C4xQ7MVjETrlnPOZUuWEtm8wISMKaF5oWsvFgsrirh8zXL6gmotsTt3HVe0KFiEekRTzLs/GPw5IXC9ls/SSpQztDu4fLMClXsjoili+RvvIhVs5VHSEX3fKd+3e1pNxdWyRqkrZVB6UlINvQebP0i834G9db3YW0Njb2vYx5BBDX++ZNYEuQlmRjN0BHlIcJJxWcRxQrwxFChiHlxGsiHFcSzm6qR2T75RTaxiUME3zZgMmFbQIq1O5jkIvG8RqN5rBLod/NmX4u9e4y8TVN3468dflYWH4s+5mtjnGJ8t77jXi/3gece75Z13EWi3845OYFfcUjyfnT//lndO4W/wvBNcTexzg8+Wd9Ql4BrBHzzxmPpN/5Z5jjDoWZ/sxmPq5YJb6nkHgYOnHlOvOfxfw5/vf7rcoxcMrgb84XOPftu/5Z4jDAbtcunguUcvGdxyzzsIHD736HUHjTyUxSPxDzNBgIBGANKIeC38mgjbXIJ2mP0pe4vnvwSiPzny7WEnAS5e3hpoo1j7L1wLa24q3eQROrbIbk5qmDsdmCtZjghk+LVpRhcRcobfKOYGHiw1BSrjqCFK86XWns4eA7VqGQzmCWLaQIVfVMs+wVX0Esd/zFWGcgG/vevbzPV1gXYG/3AX6FElublAF3Nu+9Lxo1GgXUDQBrqwC6h1NFzAJUV2t/bK7reN+HwhBJN7EPrAD8DEAf4U+GPxwNcQmGASgMAG4QRMfDAywcivqdVdSgmF59ytC9cZ8Q62vdrVNdyknJDP8wACp3N4UnadClNlfymM8esJk8pB5vnpY3DrCluU9PSTlBSd9RBVv7IcOi/Vdrptn+f8ZPt+w/s9Vz8/OeZHHqAsvXz2PZcSFN5JNoQ/ELRgh/1hvYJZH68yne7twPcf3w5jsQt49uD37ckUhGMQjGYZxGRMcDrnnWEqSM7m61XTFcvZj3toKRXmnGDnBTCrBQQHjMYFFHzhngxFoVetu/z9iNlyM6/w+QW+wkfxkdkPwcGlJSK99/HF92e9vNDOxEKJ8hExEzsrEK8EzhEJYfSSFGO3AwxXrl3VjOKn0qJ5jPIOjSlMMREzPMIcpjSLlRkSGENS/aUyxTNKPiOcJb/TlTwiSMGvwgsakpAyRtOm7KsMA2eKQZ6qWakM7OhlFKsrBt1fLAbpFeDbsazXscw0vnep6nsuMw1v2LO5pVeRs41Q+9jAM2R1RefgcuUV/rr/QrfkcP+dsz35Fw== \ No newline at end of file diff --git "a/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-read.png" "b/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-read.png" new file mode 100644 index 00000000000..f8b9589d6d7 Binary files /dev/null and "b/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-read.png" differ diff --git "a/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-write.drawio" "b/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-write.drawio" new file mode 100644 index 00000000000..6fddf10f064 --- /dev/null +++ "b/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-write.drawio" @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git "a/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-write.png" "b/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-write.png" new file mode 100644 index 00000000000..c976cc99b91 Binary files /dev/null and "b/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/cache-aside-write.png" differ diff --git "a/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/read-through.drawio" "b/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/read-through.drawio" new file mode 100644 index 00000000000..7f7bfd71641 --- /dev/null +++ "b/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/read-through.drawio" @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git "a/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/read-through.png" "b/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/read-through.png" new file mode 100644 index 00000000000..f8f457c7490 Binary files /dev/null and "b/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/read-through.png" differ diff --git "a/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/write-through.drawio" "b/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/write-through.drawio" new file mode 100644 index 00000000000..be5c87df8b5 --- /dev/null +++ "b/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/write-through.drawio" @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git "a/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/write-through.png" "b/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/write-through.png" new file mode 100644 index 00000000000..d240e5da454 Binary files /dev/null and "b/docs/database/Redis/images/\347\274\223\345\255\230\350\257\273\345\206\231\347\255\226\347\225\245/write-through.png" differ diff --git a/docs/database/Redis/redis-all.md b/docs/database/Redis/redis-all.md index 6a2ef0f1d6c..a5d8d715422 100644 --- a/docs/database/Redis/redis-all.md +++ b/docs/database/Redis/redis-all.md @@ -462,7 +462,7 @@ typedef struct redisDb { 常用的过期数据的删除策略就两个(重要!自己造缓存轮子的时候需要格外考虑的东西): 1. **惰性删除** :只会在取出key的时候才对数据进行过期检查。这样对CPU最友好,但是可能会造成太多过期 key 没有被删除。 -2. **定期删除** : 每隔一段时间抽取一批 key 执行删除过期key操作。并且,Redis 底层会并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。 +2. **定期删除** : 每隔一段时间抽取一批 key 执行删除过期key操作。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。 定期删除对内存更加友好,惰性删除对CPU更加友好。两者各有千秋,所以Redis 采用的是 **定期删除+惰性/懒汉式删除** 。 @@ -476,7 +476,7 @@ typedef struct redisDb { Redis 提供 6 种数据淘汰策略: -1. **volatile-lru(least frequently used)**:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰 +1. **volatile-lru(least recently used)**:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰 2. **volatile-ttl**:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰 3. **volatile-random**:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰 4. **allkeys-lru(least recently used)**:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的) @@ -485,8 +485,8 @@ Redis 提供 6 种数据淘汰策略: 4.0 版本后增加以下两种: -7. **volatile-lfu**:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰 -8. **allkeys-lfu**:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key +7. **volatile-lfu(least frequently used)**:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰 +8. **allkeys-lfu(least frequently used)**:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key ### 14. Redis 持久化机制(怎么保证 Redis 挂掉之后再重启数据可以进行恢复) diff --git "a/docs/database/Redis/redis-collection/Redis(1)\342\200\224\342\200\2245\347\247\215\345\237\272\346\234\254\346\225\260\346\215\256\347\273\223\346\236\204.md" "b/docs/database/Redis/redis-collection/Redis(1)\342\200\224\342\200\2245\347\247\215\345\237\272\346\234\254\346\225\260\346\215\256\347\273\223\346\236\204.md" deleted file mode 100644 index a42332f6c2d..00000000000 --- "a/docs/database/Redis/redis-collection/Redis(1)\342\200\224\342\200\2245\347\247\215\345\237\272\346\234\254\346\225\260\346\215\256\347\273\223\346\236\204.md" +++ /dev/null @@ -1,515 +0,0 @@ -> 授权转载自: https://github.com/wmyskxz/MoreThanJava#part3-redis - -![](https://upload-images.jianshu.io/upload_images/7896890-2899175817bb0069.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -# 一、Redis 简介 - -> **"Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker."** —— Redis是一个开放源代码(BSD许可)的内存中数据结构存储,用作数据库,缓存和消息代理。 *(摘自官网)* - -**Redis** 是一个开源,高级的键值存储和一个适用的解决方案,用于构建高性能,可扩展的 Web 应用程序。**Redis** 也被作者戏称为 *数据结构服务器* ,这意味着使用者可以通过一些命令,基于带有 TCP 套接字的简单 *服务器-客户端* 协议来访问一组 **可变数据结构** 。*(在 Redis 中都采用键值对的方式,只不过对应的数据结构不一样罢了)* - -## Redis 的优点 - -以下是 Redis 的一些优点: - -- **异常快** - Redis 非常快,每秒可执行大约 110000 次的设置(SET)操作,每秒大约可执行 81000 次的读取/获取(GET)操作。 -- **支持丰富的数据类型** - Redis 支持开发人员常用的大多数数据类型,例如列表,集合,排序集和散列等等。这使得 Redis 很容易被用来解决各种问题,因为我们知道哪些问题可以更好使用地哪些数据类型来处理解决。 -- **操作具有原子性** - 所有 Redis 操作都是原子操作,这确保如果两个客户端并发访问,Redis 服务器能接收更新的值。 -- **多实用工具** - Redis 是一个多实用工具,可用于多种用例,如:缓存,消息队列(Redis 本地支持发布/订阅),应用程序中的任何短期数据,例如,web应用程序中的会话,网页命中计数等。 - -## Redis 的安装 - -这一步比较简单,你可以在网上搜到许多满意的教程,这里就不再赘述。 - -给一个菜鸟教程的安装教程用作参考:[https://www.runoob.com/redis/redis-install.html](https://www.runoob.com/redis/redis-install.html) - -## 测试本地 Redis 性能 - -当你安装完成之后,你可以先执行 `redis-server` 让 Redis 启动起来,然后运行命令 `redis-benchmark -n 100000 -q` 来检测本地同时执行 10 万个请求时的性能: - -![](https://upload-images.jianshu.io/upload_images/7896890-7aea279e8fcdafe5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -当然不同电脑之间由于各方面的原因会存在性能差距,这个测试您可以权当是一种 **「乐趣」** 就好。 - -# 二、Redis 五种基本数据结构 - -**Redis** 有 5 种基础数据结构,它们分别是:**string(字符串)**、**list(列表)**、**hash(字典)**、**set(集合)** 和 **zset(有序集合)**。这 5 种是 Redis 相关知识中最基础、最重要的部分,下面我们结合源码以及一些实践来给大家分别讲解一下。 - -注意: - -> 每种数据结构都有自己底层的内部编码实现,而且是多种实现,这样Redis会在合适的场景选择合适的内部编码。 -> -> 可以看到每种数据结构都有两种以上的内部编码实现,例如string数据结构就包含了raw、int和embstr三种内部编码。 -> -> 同时,有些内部编码可以作为多种外部数据结构的内部实现,例如ziplist就是hash、list和zset共有的内部编码。 - -## 1)字符串 string - -Redis 中的字符串是一种 **动态字符串**,这意味着使用者可以修改,它的底层实现有点类似于 Java 中的 **ArrayList**,有一个字符数组,从源码的 **sds.h/sdshdr 文件** 中可以看到 Redis 底层对于字符串的定义 **SDS**,即 *Simple Dynamic String* 结构: - -```c -/* Note: sdshdr5 is never used, we just access the flags byte directly. - * However is here to document the layout of type 5 SDS strings. */ -struct __attribute__ ((__packed__)) sdshdr5 { - unsigned char flags; /* 3 lsb of type, and 5 msb of string length */ - char buf[]; -}; -struct __attribute__ ((__packed__)) sdshdr8 { - uint8_t len; /* used */ - uint8_t alloc; /* excluding the header and null terminator */ - unsigned char flags; /* 3 lsb of type, 5 unused bits */ - char buf[]; -}; -struct __attribute__ ((__packed__)) sdshdr16 { - uint16_t len; /* used */ - uint16_t alloc; /* excluding the header and null terminator */ - unsigned char flags; /* 3 lsb of type, 5 unused bits */ - char buf[]; -}; -struct __attribute__ ((__packed__)) sdshdr32 { - uint32_t len; /* used */ - uint32_t alloc; /* excluding the header and null terminator */ - unsigned char flags; /* 3 lsb of type, 5 unused bits */ - char buf[]; -}; -struct __attribute__ ((__packed__)) sdshdr64 { - uint64_t len; /* used */ - uint64_t alloc; /* excluding the header and null terminator */ - unsigned char flags; /* 3 lsb of type, 5 unused bits */ - char buf[]; -}; -``` - -你会发现同样一组结构 Redis 使用泛型定义了好多次,**为什么不直接使用 int 类型呢?** - -因为当字符串比较短的时候,len 和 alloc 可以使用 byte 和 short 来表示,**Redis 为了对内存做极致的优化,不同长度的字符串使用不同的结构体来表示。** - -### SDS 与 C 字符串的区别 - -为什么不考虑直接使用 C 语言的字符串呢?因为 C 语言这种简单的字符串表示方式 **不符合 Redis 对字符串在安全性、效率以及功能方面的要求**。我们知道,C 语言使用了一个长度为 N+1 的字符数组来表示长度为 N 的字符串,并且字符数组最后一个元素总是 `'\0'`。*(下图就展示了 C 语言中值为 "Redis" 的一个字符数组)* - -![](https://upload-images.jianshu.io/upload_images/7896890-3a7d503c81b6e6e4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -这样简单的数据结构可能会造成以下一些问题: - -- **获取字符串长度为 O(N) 级别的操作** → 因为 C 不保存数组的长度,每次都需要遍历一遍整个数组; -- 不能很好的杜绝 **缓冲区溢出/内存泄漏** 的问题 → 跟上述问题原因一样,如果执行拼接 or 缩短字符串的操作,如果操作不当就很容易造成上述问题; -- C 字符串 **只能保存文本数据** → 因为 C 语言中的字符串必须符合某种编码(比如 ASCII),例如中间出现的 `'\0'` 可能会被判定为提前结束的字符串而识别不了; - -我们以追加字符串的操作举例,Redis 源码如下: - -```c -/* Append the specified binary-safe string pointed by 't' of 'len' bytes to the - * end of the specified sds string 's'. - * - * After the call, the passed sds string is no longer valid and all the - * references must be substituted with the new pointer returned by the call. */ -sds sdscatlen(sds s, const void *t, size_t len) { - // 获取原字符串的长度 - size_t curlen = sdslen(s); - - // 按需调整空间,如果容量不够容纳追加的内容,就会重新分配字节数组并复制原字符串的内容到新数组中 - s = sdsMakeRoomFor(s,len); - if (s == NULL) return NULL; // 内存不足 - memcpy(s+curlen, t, len); // 追加目标字符串到字节数组中 - sdssetlen(s, curlen+len); // 设置追加后的长度 - s[curlen+len] = '\0'; // 让字符串以 \0 结尾,便于调试打印 - return s; -} -``` - -- **注:Redis 规定了字符串的长度不得超过 512 MB。** - -### 对字符串的基本操作 - -安装好 Redis,我们可以使用 `redis-cli` 来对 Redis 进行命令行的操作,当然 Redis 官方也提供了在线的调试器,你也可以在里面敲入命令进行操作:[http://try.redis.io/#run](http://try.redis.io/#run) - -#### 设置和获取键值对 - -```console -> SET key value -OK -> GET key -"value" -``` - -正如你看到的,我们通常使用 `SET` 和 `GET` 来设置和获取字符串值。 - -值可以是任何种类的字符串(包括二进制数据),例如你可以在一个键下保存一张 `.jpeg` 图片,只需要注意不要超过 512 MB 的最大限度就好了。 - -当 key 存在时,`SET` 命令会覆盖掉你上一次设置的值: - -```console -> SET key newValue -OK -> GET key -"newValue" -``` - -另外你还可以使用 `EXISTS` 和 `DEL` 关键字来查询是否存在和删除键值对: - -```console -> EXISTS key -(integer) 1 -> DEL key -(integer) 1 -> GET key -(nil) -``` - -#### 批量设置键值对 - -```console -> SET key1 value1 -OK -> SET key2 value2 -OK -> MGET key1 key2 key3 # 返回一个列表 -1) "value1" -2) "value2" -3) (nil) -> MSET key1 value1 key2 value2 -> MGET key1 key2 -1) "value1" -2) "value2" -``` - -#### 过期和 SET 命令扩展 - -可以对 key 设置过期时间,到时间会被自动删除,这个功能常用来控制缓存的失效时间。*(过期可以是任意数据结构)* - -```console -> SET key value1 -> GET key -"value1" -> EXPIRE name 5 # 5s 后过期 -... # 等待 5s -> GET key -(nil) -``` - -等价于 `SET` + `EXPIRE` 的 `SETEX` 命令: - -```console -> SETEX key 5 value1 -... # 等待 5s 后获取 -> GET key -(nil) - -> SETNX key value1 # 如果 key 不存在则 SET 成功 -(integer) 1 -> SETNX key value1 # 如果 key 存在则 SET 失败 -(integer) 0 -> GET key -"value" # 没有改变 -``` - -#### 计数 - -如果 value 是一个整数,还可以对它使用 `INCR` 命令进行 **原子性** 的自增操作,这意味着及时多个客户端对同一个 key 进行操作,也决不会导致竞争的情况: - -```console -> SET counter 100 -> INCR counter -(integer) 101 -> INCRBY counter 50 -(integer) 151 -``` - -#### 返回原值的 GETSET 命令 - -对字符串,还有一个 `GETSET` 比较让人觉得有意思,它的功能跟它名字一样:为 key 设置一个值并返回原值: - -```console -> SET key value -> GETSET key value1 -"value" -``` - -这可以对于某一些需要隔一段时间就统计的 key 很方便的设置和查看,例如:系统每当由用户进入的时候你就是用 `INCR` 命令操作一个 key,当需要统计时候你就把这个 key 使用 `GETSET` 命令重新赋值为 0,这样就达到了统计的目的。 - -## 2)列表 list - -Redis 的列表相当于 Java 语言中的 **LinkedList**,注意它是链表而不是数组。这意味着 list 的插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为 O(n)。 - -我们可以从源码的 `adlist.h/listNode` 来看到对其的定义: - -```c -/* Node, List, and Iterator are the only data structures used currently. */ - -typedef struct listNode { - struct listNode *prev; - struct listNode *next; - void *value; -} listNode; - -typedef struct listIter { - listNode *next; - int direction; -} listIter; - -typedef struct list { - listNode *head; - listNode *tail; - void *(*dup)(void *ptr); - void (*free)(void *ptr); - int (*match)(void *ptr, void *key); - unsigned long len; -} list; -``` - -可以看到,多个 listNode 可以通过 `prev` 和 `next` 指针组成双向链表: - -![](https://upload-images.jianshu.io/upload_images/7896890-8f569f06506845c1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -虽然仅仅使用多个 listNode 结构就可以组成链表,但是使用 `adlist.h/list` 结构来持有链表的话,操作起来会更加方便: - -![](https://upload-images.jianshu.io/upload_images/7896890-c6fb10cdbb32f517.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -### 链表的基本操作 - -- `LPUSH` 和 `RPUSH` 分别可以向 list 的左边(头部)和右边(尾部)添加一个新元素; -- `LRANGE` 命令可以从 list 中取出一定范围的元素; -- `LINDEX` 命令可以从 list 中取出指定下表的元素,相当于 Java 链表操作中的 `get(int index)` 操作; - -示范: - -```console -> rpush mylist A -(integer) 1 -> rpush mylist B -(integer) 2 -> lpush mylist first -(integer) 3 -> lrange mylist 0 -1 # -1 表示倒数第一个元素, 这里表示从第一个元素到最后一个元素,即所有 -1) "first" -2) "A" -3) "B" -``` - -#### list 实现队列 - -队列是先进先出的数据结构,常用于消息排队和异步逻辑处理,它会确保元素的访问顺序: - -```console -> RPUSH books python java golang -(integer) 3 -> LPOP books -"python" -> LPOP books -"java" -> LPOP books -"golang" -> LPOP books -(nil) -``` - -#### list 实现栈 - -栈是先进后出的数据结构,跟队列正好相反: - -```console -> RPUSH books python java golang -> RPOP books -"golang" -> RPOP books -"java" -> RPOP books -"python" -> RPOP books -(nil) -``` - -## 3)字典 hash - -Redis 中的字典相当于 Java 中的 **HashMap**,内部实现也差不多类似,都是通过 **"数组 + 链表"** 的链地址法来解决部分 **哈希冲突**,同时这样的结构也吸收了两种不同数据结构的优点。源码定义如 `dict.h/dictht` 定义: - -```c -typedef struct dictht { - // 哈希表数组 - dictEntry **table; - // 哈希表大小 - unsigned long size; - // 哈希表大小掩码,用于计算索引值,总是等于 size - 1 - unsigned long sizemask; - // 该哈希表已有节点的数量 - unsigned long used; -} dictht; - -typedef struct dict { - dictType *type; - void *privdata; - // 内部有两个 dictht 结构 - dictht ht[2]; - long rehashidx; /* rehashing not in progress if rehashidx == -1 */ - unsigned long iterators; /* number of iterators currently running */ -} dict; -``` - -`table` 属性是一个数组,数组中的每个元素都是一个指向 `dict.h/dictEntry` 结构的指针,而每个 `dictEntry` 结构保存着一个键值对: - -```c -typedef struct dictEntry { - // 键 - void *key; - // 值 - union { - void *val; - uint64_t u64; - int64_t s64; - double d; - } v; - // 指向下个哈希表节点,形成链表 - struct dictEntry *next; -} dictEntry; -``` - -可以从上面的源码中看到,**实际上字典结构的内部包含两个 hashtable**,通常情况下只有一个 hashtable 是有值的,但是在字典扩容缩容时,需要分配新的 hashtable,然后进行 **渐进式搬迁** *(下面说原因)*。 - -### 渐进式 rehash - -大字典的扩容是比较耗时间的,需要重新申请新的数组,然后将旧字典所有链表中的元素重新挂接到新的数组下面,这是一个 O(n) 级别的操作,作为单线程的 Redis 很难承受这样耗时的过程,所以 Redis 使用 **渐进式 rehash** 小步搬迁: - -![](https://upload-images.jianshu.io/upload_images/7896890-325d968300c47100.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -渐进式 rehash 会在 rehash 的同时,保留新旧两个 hash 结构,如上图所示,查询时会同时查询两个 hash 结构,然后在后续的定时任务以及 hash 操作指令中,循序渐进的把旧字典的内容迁移到新字典中。当搬迁完成了,就会使用新的 hash 结构取而代之。 - -### 扩缩容的条件 - -正常情况下,当 hash 表中 **元素的个数等于第一维数组的长度时**,就会开始扩容,扩容的新数组是 **原数组大小的 2 倍**。不过如果 Redis 正在做 `bgsave(持久化命令)`,为了减少内存也得过多分离,Redis 尽量不去扩容,但是如果 hash 表非常满了,**达到了第一维数组长度的 5 倍了**,这个时候就会 **强制扩容**。 - -当 hash 表因为元素逐渐被删除变得越来越稀疏时,Redis 会对 hash 表进行缩容来减少 hash 表的第一维数组空间占用。所用的条件是 **元素个数低于数组长度的 10%**,缩容不会考虑 Redis 是否在做 `bgsave`。 - -### 字典的基本操作 - -hash 也有缺点,hash 结构的存储消耗要高于单个字符串,所以到底该使用 hash 还是字符串,需要根据实际情况再三权衡: - -```shell -> HSET books java "think in java" # 命令行的字符串如果包含空格则需要使用引号包裹 -(integer) 1 -> HSET books python "python cookbook" -(integer) 1 -> HGETALL books # key 和 value 间隔出现 -1) "java" -2) "think in java" -3) "python" -4) "python cookbook" -> HGET books java -"think in java" -> HSET books java "head first java" -(integer) 0 # 因为是更新操作,所以返回 0 -> HMSET books java "effetive java" python "learning python" # 批量操作 -OK -``` - -## 4)集合 set - -Redis 的集合相当于 Java 语言中的 **HashSet**,它内部的键值对是无序、唯一的。它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值 NULL。 - -### 集合 set 的基本使用 - -由于该结构比较简单,我们直接来看看是如何使用的: - -```shell -> SADD books java -(integer) 1 -> SADD books java # 重复 -(integer) 0 -> SADD books python golang -(integer) 2 -> SMEMBERS books # 注意顺序,set 是无序的 -1) "java" -2) "python" -3) "golang" -> SISMEMBER books java # 查询某个 value 是否存在,相当于 contains -(integer) 1 -> SCARD books # 获取长度 -(integer) 3 -> SPOP books # 弹出一个 -"java" -``` - -## 5)有序列表 zset - -这可能使 Redis 最具特色的一个数据结构了,它类似于 Java 中 **SortedSet** 和 **HashMap** 的结合体,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以为每个 value 赋予一个 score 值,用来代表排序的权重。 - -它的内部实现用的是一种叫做 **「跳跃表」** 的数据结构,由于比较复杂,所以在这里简单提一下原理就好了: - -![](https://upload-images.jianshu.io/upload_images/7896890-efd5114939a651ed.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - -想象你是一家创业公司的老板,刚开始只有几个人,大家都平起平坐。后来随着公司的发展,人数越来越多,团队沟通成本逐渐增加,渐渐地引入了组长制,对团队进行划分,于是有一些人**又是员工又有组长的身份**。 - -再后来,公司规模进一步扩大,公司需要再进入一个层级:部门。于是每个部门又会从组长中推举一位选出部长。 - -跳跃表就类似于这样的机制,最下面一层所有的元素都会串起来,都是员工,然后每隔几个元素就会挑选出一个代表,再把这几个代表使用另外一级指针串起来。然后再在这些代表里面挑出二级代表,再串起来。**最终形成了一个金字塔的结构。** - -想一下你目前所在的地理位置:亚洲 > 中国 > 某省 > 某市 > ....,**就是这样一个结构!** - -### 有序列表 zset 基础操作 - -```console -> ZADD books 9.0 "think in java" -> ZADD books 8.9 "java concurrency" -> ZADD books 8.6 "java cookbook" - -> ZRANGE books 0 -1 # 按 score 排序列出,参数区间为排名范围 -1) "java cookbook" -2) "java concurrency" -3) "think in java" - -> ZREVRANGE books 0 -1 # 按 score 逆序列出,参数区间为排名范围 -1) "think in java" -2) "java concurrency" -3) "java cookbook" - -> ZCARD books # 相当于 count() -(integer) 3 - -> ZSCORE books "java concurrency" # 获取指定 value 的 score -"8.9000000000000004" # 内部 score 使用 double 类型进行存储,所以存在小数点精度问题 - -> ZRANK books "java concurrency" # 排名 -(integer) 1 - -> ZRANGEBYSCORE books 0 8.91 # 根据分值区间遍历 zset -1) "java cookbook" -2) "java concurrency" - -> ZRANGEBYSCORE books -inf 8.91 withscores # 根据分值区间 (-∞, 8.91] 遍历 zset,同时返回分值。inf 代表 infinite,无穷大的意思。 -1) "java cookbook" -2) "8.5999999999999996" -3) "java concurrency" -4) "8.9000000000000004" - -> ZREM books "java concurrency" # 删除 value -(integer) 1 -> ZRANGE books 0 -1 -1) "java cookbook" -2) "think in java" -``` - -# 扩展/相关阅读 - -### 优秀文章 - -1. 阿里云 Redis 开发规范 - [https://www.infoq.cn/article/K7dB5AFKI9mr5Ugbs_px](https://www.infoq.cn/article/K7dB5AFKI9mr5Ugbs_px) -2. 为什么要防止 bigkey? - [https://mp.weixin.qq.com/s?__biz=Mzg2NTEyNzE0OA==&mid=2247483677&idx=1&sn=5c320b46f0e06ce9369a29909d62b401&chksm=ce5f9e9ef928178834021b6f9b939550ac400abae5c31e1933bafca2f16b23d028cc51813aec&scene=21#wechat_redirect](https://mp.weixin.qq.com/s?__biz=Mzg2NTEyNzE0OA==&mid=2247483677&idx=1&sn=5c320b46f0e06ce9369a29909d62b401&chksm=ce5f9e9ef928178834021b6f9b939550ac400abae5c31e1933bafca2f16b23d028cc51813aec&scene=21#wechat_redirect) -3. Redis【入门】就这一篇! - [https://www.wmyskxz.com/2018/05/31/redis-ru-men-jiu-zhe-yi-pian/](https://www.wmyskxz.com/2018/05/31/redis-ru-men-jiu-zhe-yi-pian/) - -#### Redis数据结构源码分析 - -1. Redis 数据结构-字符串源码分析:[https://my.oschina.net/mengyuankan/blog/1926320](https://my.oschina.net/mengyuankan/blog/1926320) -2. Redis 数据结构-字典源码分析: [https://my.oschina.net/mengyuankan/blog/1929593](https://my.oschina.net/mengyuankan/blog/1929593) - - - -# 参考资料 - -1. 《Redis 设计与实现》 - [http://redisbook.com/](http://redisbook.com/) -2. 【官方文档】Redis 数据类型介绍 - [http://www.redis.cn/topics/data-types-intro.html](http://www.redis.cn/topics/data-types-intro.html) -3. 《Redis 深度历险》 - [https://book.douban.com/subject/30386804/](https://book.douban.com/subject/30386804/) -4. 阿里云 Redis 开发规范 - [https://www.infoq.cn/article/K7dB5AFKI9mr5Ugbs_px](https://www.infoq.cn/article/K7dB5AFKI9mr5Ugbs_px) -5. Redis 快速入门 - 易百教程 - [https://www.yiibai.com/redis/redis_quick_guide.html](https://www.yiibai.com/redis/redis_quick_guide.html) -6. Redis【入门】就这一篇! - [https://www.wmyskxz.com/2018/05/31/redis-ru-men-jiu-zhe-yi-pian/](https://www.wmyskxz.com/2018/05/31/redis-ru-men-jiu-zhe-yi-pian/) - diff --git "a/docs/database/Redis/redis-collection/Redis(10)\342\200\224\342\200\224Redis\346\225\260\346\215\256\347\261\273\345\236\213\343\200\201\347\274\226\347\240\201\343\200\201\346\225\260\346\215\256\347\273\223\346\236\204\347\232\204\345\205\263\347\263\273.md" "b/docs/database/Redis/redis-collection/Redis(10)\342\200\224\342\200\224Redis\346\225\260\346\215\256\347\261\273\345\236\213\343\200\201\347\274\226\347\240\201\343\200\201\346\225\260\346\215\256\347\273\223\346\236\204\347\232\204\345\205\263\347\263\273.md" deleted file mode 100644 index 2252d9c50b9..00000000000 --- "a/docs/database/Redis/redis-collection/Redis(10)\342\200\224\342\200\224Redis\346\225\260\346\215\256\347\261\273\345\236\213\343\200\201\347\274\226\347\240\201\343\200\201\346\225\260\346\215\256\347\273\223\346\236\204\347\232\204\345\205\263\347\263\273.md" +++ /dev/null @@ -1,599 +0,0 @@ -## Redis构建的类型系统 - -Redis构建了自己的类型系统,主要包括 - -+ redisObject对象 -+ 基于redisObject对象的类型检查 -+ 基于redisObject对象的显示多态函数 -+ 对redisObject进行分配、共享和销毁的机制 - -__C语言不是面向对象语言,这里将redisObject称呼为对象是为了讲述方便,让里面的内容更容易被理解,redisObject其实是一个结构体。__ - -### redisObject对象 - -Redis内部使用一个redisObject对象来表示所有的key和value,每次在Redis数据块中创建一个键值对时,一个是键对象,一个是值对象,而Redis中的每个对象都是由redisObject结构来表示。 - -__在Redis中,键总是一个字符串对象,而值可以是字符串、列表、集合等对象,所以我们通常说键为字符串键,表示这个键对应的值为字符串对象,我们说一个键为集合键时,表示这个键对应的值为集合对象__ - -redisobject最主要的信息: - -``` -redisobject源码 -typedef struct redisObject{ - //类型 - unsigned type:4; - //编码 - unsigned encoding:4; - //指向底层数据结构的指针 - void *ptr; - //引用计数 - int refcount; - //记录最后一次被程序访问的时间 - unsigned lru:22; -}robj -``` - -+ type代表一个value对象具体是何种数据类型 - - + type key :判断对象的数据类型 -+ encoding属性和*prt指针 - + prt指针指向对象底层的数据结构,而数据结构由encoding属性来决定 - - ![数据结构和编码的对应](https://own-pic-bed.oss-cn-beijing.aliyuncs.com/encoding对应数据结构.png) - - + 每种类型的对象至少使用了两种不同的编码,而这些编码对用户是完全透明的。 - - ![数据类型和编码的对应](https://own-pic-bed.oss-cn-beijing.aliyuncs.com/类型和编码对应.png) - - + object encoding key命令可以查看值对象的编码 - -### 命令的类型检查和多态 - -#### Redis命令分类 - -+ 一种是只能用于对应数据类型的命令,例如LPUSH和LLEN只能用于列表键, SADD 和 SRANDMEMBER只能用于集合键。 -+ 另一种是可以用于任何类型键的命令。比如TTL。 - -当执行一个处理数据类型的命令时,Redis执行以下步骤: - -+ 根据给定 `key` ,在数据库字典中查找和它相对应的 `redisObject` ,如果没找到,就返回 `NULL` 。 -+ 检查 `redisObject` 的 `type` 属性和执行命令所需的类型是否相符,如果不相符,返回类型错误。 -+ 根据 `redisObject` 的 `encoding` 属性所指定的编码,选择合适的操作函数来处理底层的数据结构。 -+ 返回数据结构的操作结果作为命令的返回值。 - -## 5种数据类型对应的编码和数据结构 - -### string - -__string 是最常用的一种数据类型,普通的key/value存储都可以归结为string类型,value不仅是string,也可以是数字。其他几种数据类型的构成元素也都是字符串,注意Redis规定字符串的长度不能超过512M__ - -+ 编码 - __字符串对象的编码可以是int raw embstr__ - + int编码 - + 保存的是可以用long类型表示的整数值 - + raw编码 - + 保存长度大于44字节的字符串 - + embstr编码 - + 保存长度小于44字节的字符串 - - int用来保存整数值,raw用来保存长字符串,embstr用来保存短字符串。embstr编码是用来专门保存短字符串的一种优化编码。 - - Redis中对于浮点型也是作为字符串保存的,在需要时再将其转换成浮点数类型 - -+ 编码的转换 - + 当 int 编码保存的值不再是整数,或大小超过了long的范围时,自动转化为raw - + 对于 embstr 编码,由于 Redis 没有对其编写任何的修改程序(embstr 是只读的),在对embstr对象进行修改时,都会先转化为raw再进行修改,因此,只要是修改embstr对象,修改后的对象一定是raw的,无论是否达到了44个字节。 - -+ 常用命令 - - + set/get - - + set:设置key对应的值为string类型的value (多次set name会覆盖) - + get:获取key对应的值 - - + mset /mget - - + mset 批量设置多个key的值,如果成功表示所有值都被设置,否则返回0表示没有任何值被设置 - + mget批量获取多个key的值,如果不存在则返回null - - ```shell - 127.0.0.1:6379> mset user1:name redis user1:age 22 - OK - 127.0.0.1:6379> mget user1:name user1:age - 1) "redis" - 2) "22" - ``` - - + 应用场景 - + 类似于哈希操作,存储对象 - - + incr && incrby<原子操作> - - + incr对key对应的值进行加加操作,并返回新的值,incrby加指定的值 - - + decr && decrby<原子操作> - - + decr对key对应的值进行减减操做,并返回新的值,decrby减指定的值 - - + setnx <小小体验一把分布式锁,真香> - - + 设置Key对应的值为string类型的值,如果已经存在则返回0 - - + setex - - + 设置key对应的值为string类型的value,并设定有效期 - - + setrange/getrange - - + setrange从指定位置替换字符串 - + getrange获取key对应value子字符串 - -+ 其他命令 - - + msetnx 同mset,不存在就设置,不会覆盖已有的key - + getset 设置key的值,并返回key旧的值 - + append 给指定的key的value追加字符串,并返回新字符串的长度 - + strlen 返回key对应的value字符串的长度 - -+ 应用场景 - - + 因为string类型是二进制安全的,可以用来存放图片,视频等内容。 - + 由于redis的高性能的读写功能,而string类型的value也可以是数字,可以用做计数器(使用INCR,DECR指令)。比如分布式环境中统计系统的在线人数,秒杀等。 - + 除了上面提到的,还有用于SpringSession实现分布式session - + 分布式系统全局序列号 - -### list - -__list列表,它是简单的字符串列表,你可以添加一个元素到列表的头部,或者尾部__。 - -+ 编码 - - + 列表对象的编码可以是ziplist(压缩列表)和linkedlist(双端链表)。 - + 编码转换 - + 同时满足下面两个条件时使用压缩列表: - + 列表保存元素个数小于512个 - + 每个元素长度小于64字节 - + 不能满足上面两个条件使用linkedlist(双端列表)编码 - -+ 常用命令 - - + lpush: 从头部加入元素 - - ```shell - 127.0.0.1:6379> lpush list1 hello - (integer) 1 - 127.0.0.1:637 9> lpush list1 world - (integer) 2 - 127.0.0.1:6379> lrange list1 0 -1 - 1) "world" - 2) "hello" - ``` - - + rpush:从尾部加入元素 - - ```shell - 127.0.0.1:6379> rpush list2 world - (integer) 1 - 127.0.0.1:6379> rpush list2 hello - (integer) 2 - 127.0.0.1:6379> lrange list2 0 -1 - 1) "world" - 2) "hello" - ``` - - + lpop: 从list的头部删除元素,并返回删除的元素 - - ```shell - 127.0.0.1:6379> lrange list1 0 -1 - 1) "world" - 2) "hello" - 127.0.0.1:6379> lpop list1 - "world" - 127.0.0.1:6379> lrange list1 0 -1 - 1) "hello" - ``` - - + rpop:从list的尾部删除元素,并返回删除的元素 - - ```shell - 127.0.0.1:6379> lrange list2 0 -1 - 1) "hello" - 2) "world" - 127.0.0.1:6379> rpop list2 - "world" - 127.0.0.1:6379> lrange list2 0 -1 - 1) "hello" - ``` - - + rpoplpush: 第一步从尾部删除元素,第二步从首部插入元素 结合着使用 - + linsert :插入方法 linsert listname before [集合的元素] [插入的元素] - - ```shell - 127.0.0.1:6379> lpush list3 hello - (integer) 1 - 127.0.0.1:6379> lpush list3 world - (integer) 2 - 127.0.0.1:6379> linsert list3 before hello start - (integer) 3 - 127.0.0.1:6379> lrange list3 0 -1 - 1) "world" - 2) "start" - 3) "hello" - ``` - + lset :替换指定下标的元素 - ```shell - 127.0.0.1:6379> lrange list1 0 -1 - 1) "a" - 2) "b" - 127.0.0.1:6379> lset list1 0 v - OK - 127.0.0.1:6379> lrange list1 0 -1 - 1) "v" - 2) "b" - ``` - + lrm : 删除元素,返回删除的个数 - ```shell - 127.0.0.1:6379> lrange list1 0 -1 - 1) "b" - 2) "b" - 3) "a" - 4) "b" - 127.0.0.1:6379> lrange list1 0 -1 - 1) "a" - 2) "b" - ``` - - + lindex: 返回list中指定位置的元素 - + llen: 返回list中的元素的个数 -+ 实现数据结构 - - + Stack(栈) - + LPUSH+LPOP - + Queue(队列) - + LPUSH + RPOP - + Blocking MQ(阻塞队列) - + LPUSH+BRPOP - -+ 应用场景 - - + 实现简单的消息队列 - + 利用LRANGE命令,实现基于Redis的分页功能 - -### set - -__集合对象set是string类型(整数也会转成string类型进行存储)的无序集合。注意集合和列表的区别:集合中的元素是无序的,因此不能通过索引来操作元素;集合中的元素不能有重复。__ - -+ 编码 - - + 集合对象的编码可以是intset或者hashtable - + intset编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合中。 - + hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,这里的每个字符串对象就是一个集合中的元素,而字典的值全部设置为null。__当使用HT编码时,Redis中的集合SET相当于Java中的HashSet,内部的键值对是无序的,唯一的。内部实现相当于一个特殊的字典,字典中所有value都是NULL。__ - - + 编码转换 - + 当集合满足下列两个条件时,使用intset编码: - + 集合对象中的所有元素都是整数 - + 集合对象所有元素数量不超过512 - -+ 常用命令 - - + sadd: 向集合中添加元素 (set不允许元素重复) - + smembers: 查看集合中的元素 - - ```shell - 127.0.0.1:6379> sadd set1 aaa - (integer) 1 - 127.0.0.1:6379> sadd set1 bbb - (integer) 1 - 127.0.0.1:6379> sadd set1 ccc - (integer) 1 - 127.0.0.1:6379> smembers set1 - 1) "aaa" - 2) "ccc" - 3) "bbb" - ``` - - + srem: 删除集合元素 - + spop: 随机返回删除的key - - + sdiff :返回两个集合的不同元素 (哪个集合在前就以哪个集合为标准) - - ```shell - 127.0.0.1:6379> smembers set1 - 1) "ccc" - 2) "bbb" - 127.0.0.1:6379> smembers set2 - 1) "fff" - 2) "rrr" - 3) "bbb" - 127.0.0.1:6379> sdiff set1 set2 - 1) "ccc" - 127.0.0.1:6379> sdiff set2 set1 - 1) "fff" - 2) "rrr" - ``` - - + sinter: 返回两个集合的交集 - + sinterstore: 返回交集结果,存入目标集合 - - ```shell - 127.0.0.1:6379> sinterstore set3 set1 set2 - (integer) 1 - 127.0.0.1:6379> smembers set3 - 1) "bbb" - ``` - - + sunion: 取两个集合的并集 - + sunionstore: 取两个集合的并集,并存入目标集合 - - + smove: 将一个集合中的元素移动到另一个集合中 - + scard: 返回集合中的元素个数 - + sismember: 判断某元素是否存在某集合中,0代表否 1代表是 - + srandmember: 随机返回一个元素 - - ```shell - 127.0.0.1:6379> srandmember set1 1 - 1) "bbb" - 127.0.0.1:6379> srandmember set1 2 - 1) "ccc" - 2) "bbb" - ``` -+ 应用场景 - - + 对于 set 数据类型,由于底层是字典实现的,查找元素特别快,另外set 数据类型不允许重复,利用这两个特性我们可以进行全局去重,比如在用户注册模块,判断用户名是否注册;微信点赞,微信抽奖小程序 - + 另外就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好,可能认识的人等功能。 - -### zset - -__和集合对象相比,有序集合对象是有序的。与列表使用索引下表作为排序依据不同,有序集合为每一个元素设置一个分数(score)作为排序依据。__ - -+ 编码 - - + 有序集合的编码可以使ziplist或者skiplist - - + ziplist编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个节点保存元素的分值。并且压缩列表内的集合元素按分值从小到大的顺序进行排列,小的放置在靠近表头的位置,大的放置在靠近表尾的位置。 - + skiplist编码的依序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表 - - ``` - typedef struct zset{ - //跳跃表 - zskiplist *zsl; - //字典 - dict *dice; - }zset - 字典的键保存元素的值,字典的值保存元素的分值,跳跃表节点的object属性保存元素的成员,跳跃表节点的score属性保存元素的分值。这两种数据结构会通过指针来共享相同元素的成员和分值,所以不会产生重复成员和分值,造成内存的浪费。 - ``` - - + 编码转换 - - + 当有序结合对象同时满足以下两个条件时,对象使用ziplist编码,否则使用skiplist编码 - + 保存的元素数量小于128 - + 保存的所有元素长度都小于64字节 - -+ 常用命令 - - + zrem: 删除集合中名称为key的元素member - + zincrby: 以指定值去自动递增 - + zcard: 查看元素集合的个数 - + zcount: 返回score在给定区间中的数量 - - ```shell - 127.0.0.1:6379> zrange zset 0 -1 - 1) "one" - 2) "three" - 3) "two" - 4) "four" - 5) "five" - 6) "six" - 127.0.0.1:6379> zcard zset - (integer) 6 - 127.0.0.1:6379> zcount zset 1 4 - (integer) 4 - ``` - - + zrangebyscore: 找到指定区间范围的数据进行返回 - - ```shell - 127.0.0.1:6379> zrangebyscore zset 0 4 withscores - 1) "one" - 2) "1" - 3) "three" - 4) "2" - 5) "two" - 6) "2" - 7) "four" - 8) "4" - ``` - - + zremrangebyrank zset from to: 删除索引 - - ```shell - 127.0.0.1:6379> zrange zset 0 -1 - 1) "one" - 2) "three" - 3) "two" - 4) "four" - 5) "five" - 6) "six" - 127.0.0.1:6379> zremrangebyrank zset 1 3 - (integer) 3 - 127.0.0.1:6379> zrange zset 0 -1 - 1) "one" - 2) "five" - 3) "six" - - ``` - - + zremrangebyscore zset from to: 删除指定序号 - - ```shell - 127.0.0.1:6379> zrange zset 0 -1 withscores - 1) "one" - 2) "1" - 3) "five" - 4) "5" - 5) "six" - 6) "6" - 127.0.0.1:6379> zremrangebyscore zset 3 6 - (integer) 2 - 127.0.0.1:6379> zrange zset 0 -1 withscores - 1) "one" - 2) "1" - ``` - - + zrank: 返回排序索引 (升序之后再找索引) - + zrevrank: 返回排序索引 (降序之后再找索引) - -+ 应用场景 - - + 对于 zset 数据类型,有序的集合,可以做范围查找,排行榜应用,取 TOP N 操作等。 - -### hash - -__hash对象的键是一个字符串类型,值是一个键值对集合__ - -+ 编码 - - + hash对象的编码可以是ziplist或者hashtable - + 当使用ziplist,也就是压缩列表作为底层实现时,新增的键值是保存到压缩列表的表尾。 - + hashtable 编码的hash表对象底层使用字典数据结构,哈希对象中的每个键值对都使用一个字典键值对。__Redis中的字典相当于Java里面的HashMap,内部实现也差不多类似,都是通过“数组+链表”的链地址法来解决哈希冲突的,这样的结构吸收了两种不同数据结构的优点。__ - - + 编码转换 - + 当同时满足下面两个条件使用ziplist编码,否则使用hashtable编码 - + 列表保存元素个数小于512个 - + 每个元素长度小于64字节 - -+ hash是一个String类型的field和value之间的映射表 - -+ Hash特别适合存储对象 - -+ 所存储的成员较少时数据存储为zipmap,当成员数量增大时会自动转成真正的HashMap,此时encoding为ht - -+ Hash命令详解 - - + hset/hget - - + hset hashname hashkey hashvalue - + hget hashname hashkey - - ```shell - 127.0.0.1:6379> hset user id 1 - (integer) 1 - 127.0.0.1:6379> hset user name z3 - (integer) 1 - 127.0.0.1:6379> hset user add shanxi - (integer) 1 - 127.0.0.1:6379> hget user id - "1" - 127.0.0.1:6379> hget user name - "z3" - 127.0.0.1:6379> hget user add - "shanxi" - - ``` - - - - + hmset/hmget - - + hmset hashname hashkey1hashvalue1 hashkey2 hashvalue2 hashkey3 hashvalue3 - + hget hashname hashkey1 hashkey2 hashkey3 - - ```shell - 127.0.0.1:6379> hmset user id 1 name z3 add shanxi - OK - 127.0.0.1:6379> hmget user id name add - 1) "1" - 2) "z3" - 3) "shanxi" - ``` - - + hsetnx/hgetnx - - + hincrby/hdecrby - - ```shell - 127.0.0.1:6379> hincrby user2 id 3 - (integer) 6 - 127.0.0.1:6379> hget user2 id - "6" - ``` - - + hexist 判断是否存在key,不存在返回0 - - ```shell - 127.0.0.1:6379> hget user2 id - "6" - ``` - - + hlen 返回hash集合里所有的键值数 - - ```shell - 127.0.0.1:6379> hmset user3 id 3 name w5 - OK - 127.0.0.1:6379> hlen user3 - (integer) 2 - ``` - - + hdel :删除指定的hash的key - + hkeys 返回hash里所有的字段 - + hvals 返回hash里所有的value - + hgetall:返回hash集合里所有的key和value - - ```shell - 127.0.0.1:6379> hgetall user3 - 1) "id" - 2) "3" - 3) "name" - 4) "w3" - 5) "add" - 6) "beijing" - ``` - -+ 优点 - - + 同类数据归类整合存储,方便数据管理,比如单个用户的所有商品都放在一个hash表里面。 - + 相比string操作消耗内存cpu更小 - -+ 缺点 - - + hash结构的存储消耗要高于单个字符串 - + 过期功能不能使用在field上,只能用在key上 - + redis集群架构不适合大规模使用 - -+ 应用场景 - - + 对于 hash 数据类型,value 存放的是键值对,比如可以做单点登录存放用户信息。 - + 存放商品信息,实现购物车 - -## 内存回收和内存共享 - -``` -typedef struct redisObject{ - //类型 - unsigned type:4; - //编码 - unsigned encoding:4; - //指向底层数据结构的指针 - void *ptr; - //引用计数 - int refcount; - //记录最后一次被程序访问的时间 - unsigned lru:22; - -}robj -``` - -+ 内存回收 - __因为c语言不具备自动内存回收功能,当将redisObject对象作为数据库的键或值而不是作为参数存储时其生命周期是非常长的,为了解决这个问题,Redis自己构建了一个内存回收机制,通过redisobject结构中的refcount实现.这个属性会随着对象的使用状态而不断变化。__ - 1. 创建一个新对象,属性初始化为1 - 2. 对象被一个新程序使用,属性refcount加1 - 3. 对象不再被一个程序使用,属性refcount减1 - 4. 当对象的引用计数值变为0时,对象所占用的内存就会被释放 -+ 内存共享 - __refcount属性除了能实现内存回收以外,还能实现内存共享__ - 1. 将数据块的键的值指针指向一个现有值的对象 - 2. 将被共享的值对象引用refcount加1 - Redis的共享对象目前只支持整数值的字符串对象。之所以如此,实际上是对内存和CPU(时间)的平衡:共享对象虽然会降低内存消耗,但是判断两个对象是否相等却需要消耗额外的时间。对于整数值,判断操作复杂度为o(1),对于普通字符串,判断复杂度为o(n);而对于哈希,列表,集合和有序集合,判断的复杂度为o(n^2).虽然共享的对象只能是整数值的字符串对象,但是5种类型都可能使用共享对象。 - - - diff --git "a/docs/database/Redis/redis-collection/Redis(2)\342\200\224\342\200\224\350\267\263\350\267\203\350\241\250.md" "b/docs/database/Redis/redis-collection/Redis(2)\342\200\224\342\200\224\350\267\263\350\267\203\350\241\250.md" deleted file mode 100644 index 451c91bd028..00000000000 --- "a/docs/database/Redis/redis-collection/Redis(2)\342\200\224\342\200\224\350\267\263\350\267\203\350\241\250.md" +++ /dev/null @@ -1,392 +0,0 @@ -> 授权转载自: https://github.com/wmyskxz/MoreThanJava#part3-redis - - - -![](https://upload-images.jianshu.io/upload_images/7896890-97a4ce9191464f62.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -# 一、跳跃表简介 - -跳跃表(skiplist)是一种随机化的数据结构,由 **William Pugh** 在论文[《Skip lists: a probabilistic alternative to balanced trees》](https://www.cl.cam.ac.uk/teaching/0506/Algorithms/skiplists.pdf)中提出,是一种可以于平衡树媲美的层次化链表结构——查找、删除、添加等操作都可以在对数期望时间下完成,以下是一个典型的跳跃表例子: - -![](https://upload-images.jianshu.io/upload_images/7896890-65a5b1a2849fb91c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -我们在上一篇中提到了 Redis 的五种基本结构中,有一个叫做 **有序列表 zset** 的数据结构,它类似于 Java 中的 **SortedSet** 和 **HashMap** 的结合体,一方面它是一个 set 保证了内部 value 的唯一性,另一方面又可以给每个 value 赋予一个排序的权重值 score,来达到 **排序** 的目的。 - -它的内部实现就依赖了一种叫做 **「跳跃列表」** 的数据结构。 - -## 为什么使用跳跃表 - -首先,因为 zset 要支持随机的插入和删除,所以它 **不宜使用数组来实现**,关于排序问题,我们也很容易就想到 **红黑树/ 平衡树** 这样的树形结构,为什么 Redis 不使用这样一些结构呢? - -1. **性能考虑:** 在高并发的情况下,树形结构需要执行一些类似于 rebalance 这样的可能涉及整棵树的操作,相对来说跳跃表的变化只涉及局部 _(下面详细说)_; -2. **实现考虑:** 在复杂度与红黑树相同的情况下,跳跃表实现起来更简单,看起来也更加直观; - -基于以上的一些考虑,Redis 基于 **William Pugh** 的论文做出一些改进后采用了 **跳跃表** 这样的结构。 - -## 本质是解决查找问题 - -我们先来看一个普通的链表结构: - -![](https://upload-images.jianshu.io/upload_images/7896890-11b7eebde1779904.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -我们需要这个链表按照 score 值进行排序,这也就意味着,当我们需要添加新的元素时,我们需要定位到插入点,这样才可以继续保证链表是有序的,通常我们会使用 **二分查找法**,但二分查找是有序数组的,链表没办法进行位置定位,我们除了遍历整个找到第一个比给定数据大的节点为止 _(时间复杂度 O(n))_ 似乎没有更好的办法。 - -但假如我们每相邻两个节点之间就增加一个指针,让指针指向下一个节点,如下图: - -![](https://upload-images.jianshu.io/upload_images/7896890-8cae2c261c950b32.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -这样所有新增的指针连成了一个新的链表,但它包含的数据却只有原来的一半 _(图中的为 3,11)_。 - -现在假设我们想要查找数据时,可以根据这条新的链表查找,如果碰到比待查找数据大的节点时,再回到原来的链表中进行查找,比如,我们想要查找 7,查找的路径则是沿着下图中标注出的红色指针所指向的方向进行的: - -![](https://upload-images.jianshu.io/upload_images/7896890-9c0262c7a85c120e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -这是一个略微极端的例子,但我们仍然可以看到,通过新增加的指针查找,我们不再需要与链表上的每一个节点逐一进行比较,这样改进之后需要比较的节点数大概只有原来的一半。 - -利用同样的方式,我们可以在新产生的链表上,继续为每两个相邻的节点增加一个指针,从而产生第三层链表: - -![](https://upload-images.jianshu.io/upload_images/7896890-22036e274bedaa5a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -在这个新的三层链表结构中,我们试着 **查找 13**,那么沿着最上层链表首先比较的是 11,发现 11 比 13 小,于是我们就知道只需要到 11 后面继续查找,**从而一下子跳过了 11 前面的所有节点。** - -可以想象,当链表足够长,这样的多层链表结构可以帮助我们跳过很多下层节点,从而加快查找的效率。 - -## 更进一步的跳跃表 - -**跳跃表 skiplist** 就是受到这种多层链表结构的启发而设计出来的。按照上面生成链表的方式,上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就非常类似于一个二分查找,使得查找的时间复杂度可以降低到 _O(logn)_。 - -但是,这种方法在插入数据的时候有很大的问题。新插入一个节点之后,就会打乱上下相邻两层链表上节点个数严格的 2:1 的对应关系。如果要维持这种对应关系,就必须把新插入的节点后面的所有节点 _(也包括新插入的节点)_ 重新进行调整,这会让时间复杂度重新蜕化成 _O(n)_。删除数据也有同样的问题。 - -**skiplist** 为了避免这一问题,它不要求上下相邻两层链表之间的节点个数有严格的对应关系,而是 **为每个节点随机出一个层数(level)**。比如,一个节点随机出的层数是 3,那么就把它链入到第 1 层到第 3 层这三层链表中。为了表达清楚,下图展示了如何通过一步步的插入操作从而形成一个 skiplist 的过程: - -![](https://upload-images.jianshu.io/upload_images/7896890-1e0626c013de095e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -从上面的创建和插入的过程中可以看出,每一个节点的层数(level)是随机出来的,而且新插入一个节点并不会影响到其他节点的层数,因此,**插入操作只需要修改节点前后的指针,而不需要对多个节点都进行调整**,这就降低了插入操作的复杂度。 - -现在我们假设从我们刚才创建的这个结构中查找 23 这个不存在的数,那么查找路径会如下图: - -![](https://upload-images.jianshu.io/upload_images/7896890-a8f66d808e8a4d1e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -# 二、跳跃表的实现 - -Redis 中的跳跃表由 `server.h/zskiplistNode` 和 `server.h/zskiplist` 两个结构定义,前者为跳跃表节点,后者则保存了跳跃节点的相关信息,同之前的 `集合 list` 结构类似,其实只有 `zskiplistNode` 就可以实现了,但是引入后者是为了更加方便的操作: - -```c -/* ZSETs use a specialized version of Skiplists */ -typedef struct zskiplistNode { - // value - sds ele; - // 分值 - double score; - // 后退指针 - struct zskiplistNode *backward; - // 层 - struct zskiplistLevel { - // 前进指针 - struct zskiplistNode *forward; - // 跨度 - unsigned long span; - } level[]; -} zskiplistNode; - -typedef struct zskiplist { - // 跳跃表头指针 - struct zskiplistNode *header, *tail; - // 表中节点的数量 - unsigned long length; - // 表中层数最大的节点的层数 - int level; -} zskiplist; -``` - -正如文章开头画出来的那张标准的跳跃表那样。 - -## 随机层数 - -对于每一个新插入的节点,都需要调用一个随机算法给它分配一个合理的层数,源码在 `t_zset.c/zslRandomLevel(void)` 中被定义: - -```c -int zslRandomLevel(void) { - int level = 1; - while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF)) - level += 1; - return (level-63 的概率被分配到最顶层,因为这里每一层的晋升率都是 50%。 - -**Redis 跳跃表默认允许最大的层数是 32**,被源码中 `ZSKIPLIST_MAXLEVEL` 定义,当 `Level[0]` 有 264 个元素时,才能达到 32 层,所以定义 32 完全够用了。 - -## 创建跳跃表 - -这个过程比较简单,在源码中的 `t_zset.c/zslCreate` 中被定义: - -```c -zskiplist *zslCreate(void) { - int j; - zskiplist *zsl; - - // 申请内存空间 - zsl = zmalloc(sizeof(*zsl)); - // 初始化层数为 1 - zsl->level = 1; - // 初始化长度为 0 - zsl->length = 0; - // 创建一个层数为 32,分数为 0,没有 value 值的跳跃表头节点 - zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL); - - // 跳跃表头节点初始化 - for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) { - // 将跳跃表头节点的所有前进指针 forward 设置为 NULL - zsl->header->level[j].forward = NULL; - // 将跳跃表头节点的所有跨度 span 设置为 0 - zsl->header->level[j].span = 0; - } - // 跳跃表头节点的后退指针 backward 置为 NULL - zsl->header->backward = NULL; - // 表头指向跳跃表尾节点的指针置为 NULL - zsl->tail = NULL; - return zsl; -} - -``` - -即执行完之后创建了如下结构的初始化跳跃表: - -![](https://upload-images.jianshu.io/upload_images/7896890-551660604afd1041.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - -## 插入节点实现 - -这几乎是最重要的一段代码了,但总体思路也比较清晰简单,如果理解了上面所说的跳跃表的原理,那么很容易理清楚插入节点时发生的几个动作 *(几乎跟链表类似)*: - -1. 找到当前我需要插入的位置 *(其中包括相同 score 时的处理)*; -2. 创建新节点,调整前后的指针指向,完成插入; - -为了方便阅读,我把源码 `t_zset.c/zslInsert` 定义的插入函数拆成了几个部分 - -### 第一部分:声明需要存储的变量 - -```c -// 存储搜索路径 -zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x; -// 存储经过的节点跨度 -unsigned int rank[ZSKIPLIST_MAXLEVEL]; -int i, level; -``` - -### 第二部分:搜索当前节点插入位置 - -```c -serverAssert(!isnan(score)); -x = zsl->header; -// 逐步降级寻找目标节点,得到 "搜索路径" -for (i = zsl->level-1; i >= 0; i--) { - /* store rank that is crossed to reach the insert position */ - rank[i] = i == (zsl->level-1) ? 0 : rank[i+1]; - // 如果 score 相等,还需要比较 value 值 - while (x->level[i].forward && - (x->level[i].forward->score < score || - (x->level[i].forward->score == score && - sdscmp(x->level[i].forward->ele,ele) < 0))) - { - rank[i] += x->level[i].span; - x = x->level[i].forward; - } - // 记录 "搜索路径" - update[i] = x; -} -``` - -**讨论:** 有一种极端的情况,就是跳跃表中的所有 score 值都是一样,zset 的查找性能会不会退化为 O(n) 呢? - -从上面的源码中我们可以发现 zset 的排序元素不只是看 score 值,也会比较 value 值 *(字符串比较)* - -### 第三部分:生成插入节点 - -```c -/* we assume the element is not already inside, since we allow duplicated - * scores, reinserting the same element should never happen since the - * caller of zslInsert() should test in the hash table if the element is - * already inside or not. */ -level = zslRandomLevel(); -// 如果随机生成的 level 超过了当前最大 level 需要更新跳跃表的信息 -if (level > zsl->level) { - for (i = zsl->level; i < level; i++) { - rank[i] = 0; - update[i] = zsl->header; - update[i]->level[i].span = zsl->length; - } - zsl->level = level; -} -// 创建新节点 -x = zslCreateNode(level,score,ele); -``` - -### 第四部分:重排前向指针 - -```c -for (i = 0; i < level; i++) { - x->level[i].forward = update[i]->level[i].forward; - update[i]->level[i].forward = x; - - /* update span covered by update[i] as x is inserted here */ - x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]); - update[i]->level[i].span = (rank[0] - rank[i]) + 1; -} - -/* increment span for untouched levels */ -for (i = level; i < zsl->level; i++) { - update[i]->level[i].span++; -} -``` - -### 第五部分:重排后向指针并返回 - -```c -x->backward = (update[0] == zsl->header) ? NULL : update[0]; -if (x->level[0].forward) - x->level[0].forward->backward = x; -else - zsl->tail = x; -zsl->length++; -return x; -``` - -## 节点删除实现 - -删除过程由源码中的 `t_zset.c/zslDeleteNode` 定义,和插入过程类似,都需要先把这个 **"搜索路径"** 找出来,然后对于每个层的相关节点重排一下前向后向指针,同时还要注意更新一下最高层数 `maxLevel`,直接放源码 *(如果理解了插入这里还是很容易理解的)*: - -```c -/* Internal function used by zslDelete, zslDeleteByScore and zslDeleteByRank */ -void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) { - int i; - for (i = 0; i < zsl->level; i++) { - if (update[i]->level[i].forward == x) { - update[i]->level[i].span += x->level[i].span - 1; - update[i]->level[i].forward = x->level[i].forward; - } else { - update[i]->level[i].span -= 1; - } - } - if (x->level[0].forward) { - x->level[0].forward->backward = x->backward; - } else { - zsl->tail = x->backward; - } - while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL) - zsl->level--; - zsl->length--; -} - -/* Delete an element with matching score/element from the skiplist. - * The function returns 1 if the node was found and deleted, otherwise - * 0 is returned. - * - * If 'node' is NULL the deleted node is freed by zslFreeNode(), otherwise - * it is not freed (but just unlinked) and *node is set to the node pointer, - * so that it is possible for the caller to reuse the node (including the - * referenced SDS string at node->ele). */ -int zslDelete(zskiplist *zsl, double score, sds ele, zskiplistNode **node) { - zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x; - int i; - - x = zsl->header; - for (i = zsl->level-1; i >= 0; i--) { - while (x->level[i].forward && - (x->level[i].forward->score < score || - (x->level[i].forward->score == score && - sdscmp(x->level[i].forward->ele,ele) < 0))) - { - x = x->level[i].forward; - } - update[i] = x; - } - /* We may have multiple elements with the same score, what we need - * is to find the element with both the right score and object. */ - x = x->level[0].forward; - if (x && score == x->score && sdscmp(x->ele,ele) == 0) { - zslDeleteNode(zsl, x, update); - if (!node) - zslFreeNode(x); - else - *node = x; - return 1; - } - return 0; /* not found */ -} -``` - -## 节点更新实现 - -当我们调用 `ZADD` 方法时,如果对应的 value 不存在,那就是插入过程,如果这个 value 已经存在,只是调整一下 score 的值,那就需要走一个更新流程。 - -假设这个新的 score 值并不会带来排序上的变化,那么就不需要调整位置,直接修改元素的 score 值就可以了,但是如果排序位置改变了,那就需要调整位置,该如何调整呢? - -从源码 `t_zset.c/zsetAdd` 函数 `1350` 行左右可以看到,Redis 采用了一个非常简单的策略: - -```c -/* Remove and re-insert when score changed. */ -if (score != curscore) { - zobj->ptr = zzlDelete(zobj->ptr,eptr); - zobj->ptr = zzlInsert(zobj->ptr,ele,score); - *flags |= ZADD_UPDATED; -} -``` - -**把这个元素删除再插入这个**,需要经过两次路径搜索,从这一点上来看,Redis 的 `ZADD` 代码似乎还有进一步优化的空间。 - -## 元素排名的实现 - -跳跃表本身是有序的,Redis 在 skiplist 的 forward 指针上进行了优化,给每一个 forward 指针都增加了 `span` 属性,用来 **表示从前一个节点沿着当前层的 forward 指针跳到当前这个节点中间会跳过多少个节点**。在上面的源码中我们也可以看到 Redis 在插入、删除操作时都会小心翼翼地更新 `span` 值的大小。 - -所以,沿着 **"搜索路径"**,把所有经过节点的跨度 `span` 值进行累加就可以算出当前元素的最终 rank 值了: - -```c -/* Find the rank for an element by both score and key. - * Returns 0 when the element cannot be found, rank otherwise. - * Note that the rank is 1-based due to the span of zsl->header to the - * first element. */ -unsigned long zslGetRank(zskiplist *zsl, double score, sds ele) { - zskiplistNode *x; - unsigned long rank = 0; - int i; - - x = zsl->header; - for (i = zsl->level-1; i >= 0; i--) { - while (x->level[i].forward && - (x->level[i].forward->score < score || - (x->level[i].forward->score == score && - sdscmp(x->level[i].forward->ele,ele) <= 0))) { - // span 累加 - rank += x->level[i].span; - x = x->level[i].forward; - } - - /* x might be equal to zsl->header, so test if obj is non-NULL */ - if (x->ele && sdscmp(x->ele,ele) == 0) { - return rank; - } - } - return 0; -} -``` - - -# 扩展阅读 - -1. 跳跃表 Skip List 的原理和实现(Java) - [https://blog.csdn.net/DERRANTCM/article/details/79063312](https://blog.csdn.net/DERRANTCM/article/details/79063312) -2. 【算法导论33】跳跃表(Skip list)原理与java实现 - [https://blog.csdn.net/brillianteagle/article/details/52206261](https://blog.csdn.net/brillianteagle/article/details/52206261) - -# 参考资料 - -1. 《Redis 设计与实现》 - [http://redisbook.com/](http://redisbook.com/) -2. 【官方文档】Redis 数据类型介绍 - [http://www.redis.cn/topics/data-types-intro.html](http://www.redis.cn/topics/data-types-intro.html) -3. 《Redis 深度历险》 - [https://book.douban.com/subject/30386804/](https://book.douban.com/subject/30386804/) -4. Redis 源码 - [https://github.com/antirez/redis](https://github.com/antirez/redis) -5. Redis 快速入门 - 易百教程 - [https://www.yiibai.com/redis/redis_quick_guide.html](https://www.yiibai.com/redis/redis_quick_guide.html) -6. Redis【入门】就这一篇! - [https://www.wmyskxz.com/2018/05/31/redis-ru-men-jiu-zhe-yi-pian/](https://www.wmyskxz.com/2018/05/31/redis-ru-men-jiu-zhe-yi-pian/) -7. Redis为什么用跳表而不用平衡树? - [https://mp.weixin.qq.com/s?__biz=MzA4NTg1MjM0Mg==&mid=2657261425&idx=1&sn=d840079ea35875a8c8e02d9b3e44cf95&scene=21#wechat_redirect](https://mp.weixin.qq.com/s?__biz=MzA4NTg1MjM0Mg==&mid=2657261425&idx=1&sn=d840079ea35875a8c8e02d9b3e44cf95&scene=21#wechat_redirect) -8. 为啥 redis 使用跳表(skiplist)而不是使用 red-black? - 知乎@于康 - [https://www.zhihu.com/question/20202931](https://www.zhihu.com/question/20202931) - diff --git "a/docs/database/Redis/redis-collection/Redis(3)\342\200\224\342\200\224\345\210\206\345\270\203\345\274\217\351\224\201\346\267\261\345\205\245\346\216\242\347\251\266.md" "b/docs/database/Redis/redis-collection/Redis(3)\342\200\224\342\200\224\345\210\206\345\270\203\345\274\217\351\224\201\346\267\261\345\205\245\346\216\242\347\251\266.md" deleted file mode 100644 index d05ff59401f..00000000000 --- "a/docs/database/Redis/redis-collection/Redis(3)\342\200\224\342\200\224\345\210\206\345\270\203\345\274\217\351\224\201\346\267\261\345\205\245\346\216\242\347\251\266.md" +++ /dev/null @@ -1,228 +0,0 @@ -> 授权转载自: https://github.com/wmyskxz/MoreThanJava#part3-redis - -![](https://upload-images.jianshu.io/upload_images/7896890-5fe2adf61ccf11aa.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -# 一、分布式锁简介 - -**锁** 是一种用来解决多个执行线程 **访问共享资源** 错误或数据不一致问题的工具。 - -如果 *把一台服务器比作一个房子*,那么 *线程就好比里面的住户*,当他们想要共同访问一个共享资源,例如厕所的时候,如果厕所门上没有锁...更甚者厕所没装门...这是会出原则性的问题的.. - -![](https://upload-images.jianshu.io/upload_images/7896890-26a364bddb9218eb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -装上了锁,大家用起来就安心多了,本质也就是 **同一时间只允许一个住户使用**。 - -而随着互联网世界的发展,单体应用已经越来越无法满足复杂互联网的高并发需求,转而慢慢朝着分布式方向发展,慢慢进化成了 **更大一些的住户**。所以同样,我们需要引入分布式锁来解决分布式应用之间访问共享资源的并发问题。 - -## 为何需要分布式锁 - -一般情况下,我们使用分布式锁主要有两个场景: - -1. **避免不同节点重复相同的工作**:比如用户执行了某个操作有可能不同节点会发送多封邮件; -2. **避免破坏数据的正确性**:如果两个节点在同一条数据上同时进行操作,可能会造成数据错误或不一致的情况出现; - -## Java 中实现的常见方式 - -上面我们用简单的比喻说明了锁的本质:**同一时间只允许一个用户操作**。所以理论上,能够满足这个需求的工具我们都能够使用 *(就是其他应用能帮我们加锁的)*: - -1. **基于 MySQL 中的锁**:MySQL 本身有自带的悲观锁 `for update` 关键字,也可以自己实现悲观/乐观锁来达到目的; -2. **基于 Zookeeper 有序节点**:Zookeeper 允许临时创建有序的子节点,这样客户端获取节点列表时,就能够当前子节点列表中的序号判断是否能够获得锁; -3. **基于 Redis 的单线程**:由于 Redis 是单线程,所以命令会以串行的方式执行,并且本身提供了像 `SETNX(set if not exists)` 这样的指令,本身具有互斥性; - -每个方案都有各自的优缺点,例如 MySQL 虽然直观理解容易,但是实现起来却需要额外考虑 **锁超时**、**加事务** 等,并且性能局限于数据库,诸如此类我们在此不作讨论,重点关注 Redis。 - -## Redis 分布式锁的问题 - -### 1)锁超时 - -假设现在我们有两台平行的服务 A B,其中 A 服务在 **获取锁之后** 由于未知神秘力量突然 **挂了**,那么 B 服务就永远无法获取到锁了: - -![](https://upload-images.jianshu.io/upload_images/7896890-4ea386c23ef0eec9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -所以我们需要额外设置一个超时时间,来保证服务的可用性。 - -但是另一个问题随即而来:**如果在加锁和释放锁之间的逻辑执行得太长,以至于超出了锁的超时限制**,也会出现问题。因为这时候第一个线程持有锁过期了,而临界区的逻辑还没有执行完,与此同时第二个线程就提前拥有了这把锁,导致临界区的代码不能得到严格的串行执行。 - -为了避免这个问题,**Redis 分布式锁不要用于较长时间的任务**。如果真的偶尔出现了问题,造成的数据小错乱可能就需要人工的干预。 - -有一个稍微安全一点的方案是 **将锁的 `value` 值设置为一个随机数**,释放锁时先匹配随机数是否一致,然后再删除 key,这是为了 **确保当前线程占有的锁不会被其他线程释放**,除非这个锁是因为过期了而被服务器自动释放的。 - -但是匹配 `value` 和删除 `key` 在 Redis 中并不是一个原子性的操作,也没有类似保证原子性的指令,所以可能需要使用像 Lua 这样的脚本来处理了,因为 Lua 脚本可以 **保证多个指令的原子性执行**。 - -### 延伸的讨论:GC 可能引发的安全问题 - -[Martin Kleppmann](https://martin.kleppmann.com/) 曾与 Redis 之父 Antirez 就 Redis 实现分布式锁的安全性问题进行过深入的讨论,其中有一个问题就涉及到 **GC**。 - -熟悉 Java 的同学肯定对 GC 不陌生,在 GC 的时候会发生 **STW(Stop-The-World)**,这本身是为了保障垃圾回收器的正常执行,但可能会引发如下的问题: - -![](https://upload-images.jianshu.io/upload_images/7896890-cf3a403968a23be4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -服务 A 获取了锁并设置了超时时间,但是服务 A 出现了 STW 且时间较长,导致了分布式锁进行了超时释放,在这个期间服务 B 获取到了锁,待服务 A STW 结束之后又恢复了锁,这就导致了 **服务 A 和服务 B 同时获取到了锁**,这个时候分布式锁就不安全了。 - -不仅仅局限于 Redis,Zookeeper 和 MySQL 有同样的问题。 - -想吃更多瓜的童鞋,可以访问下列网站看看 Redis 之父 Antirez 怎么说:[http://antirez.com/news/101](http://antirez.com/news/101) - -### 2)单点/多点问题 - -如果 Redis 采用单机部署模式,那就意味着当 Redis 故障了,就会导致整个服务不可用。 - -而如果采用主从模式部署,我们想象一个这样的场景:*服务 A* 申请到一把锁之后,如果作为主机的 Redis 宕机了,那么 *服务 B* 在申请锁的时候就会从从机那里获取到这把锁,为了解决这个问题,Redis 作者提出了一种 **RedLock 红锁** 的算法 *(Redission 同 Jedis)*: - -```java -// 三个 Redis 集群 -RLock lock1 = redissionInstance1.getLock("lock1"); -RLock lock2 = redissionInstance2.getLock("lock2"); -RLock lock3 = redissionInstance3.getLock("lock3"); - -RedissionRedLock lock = new RedissionLock(lock1, lock2, lock2); -lock.lock(); -// do something.... -lock.unlock(); -``` - -# 二、Redis 分布式锁的实现 - -分布式锁类似于 "占坑",而 `SETNX(SET if Not eXists)` 指令就是这样的一个操作,只允许被一个客户端占有,我们来看看 **源码(t_string.c/setGenericCommand)** 吧: - -```c -// SET/ SETEX/ SETTEX/ SETNX 最底层实现 -void setGenericCommand(client *c, int flags, robj *key, robj *val, robj *expire, int unit, robj *ok_reply, robj *abort_reply) { - long long milliseconds = 0; /* initialized to avoid any harmness warning */ - - // 如果定义了 key 的过期时间则保存到上面定义的变量中 - // 如果过期时间设置错误则返回错误信息 - if (expire) { - if (getLongLongFromObjectOrReply(c, expire, &milliseconds, NULL) != C_OK) - return; - if (milliseconds <= 0) { - addReplyErrorFormat(c,"invalid expire time in %s",c->cmd->name); - return; - } - if (unit == UNIT_SECONDS) milliseconds *= 1000; - } - - // lookupKeyWrite 函数是为执行写操作而取出 key 的值对象 - // 这里的判断条件是: - // 1.如果设置了 NX(不存在),并且在数据库中找到了 key 值 - // 2.或者设置了 XX(存在),并且在数据库中没有找到该 key - // => 那么回复 abort_reply 给客户端 - if ((flags & OBJ_SET_NX && lookupKeyWrite(c->db,key) != NULL) || - (flags & OBJ_SET_XX && lookupKeyWrite(c->db,key) == NULL)) - { - addReply(c, abort_reply ? abort_reply : shared.null[c->resp]); - return; - } - - // 在当前的数据库中设置键为 key 值为 value 的数据 - genericSetKey(c->db,key,val,flags & OBJ_SET_KEEPTTL); - // 服务器每修改一个 key 后都会修改 dirty 值 - server.dirty++; - if (expire) setExpire(c,c->db,key,mstime()+milliseconds); - notifyKeyspaceEvent(NOTIFY_STRING,"set",key,c->db->id); - if (expire) notifyKeyspaceEvent(NOTIFY_GENERIC, - "expire",key,c->db->id); - addReply(c, ok_reply ? ok_reply : shared.ok); -} -``` - -就像上面介绍的那样,其实在之前版本的 Redis 中,由于 `SETNX` 和 `EXPIRE` 并不是 **原子指令**,所以在一起执行会出现问题。 - -也许你会想到使用 Redis 事务来解决,但在这里不行,因为 `EXPIRE` 命令依赖于 `SETNX` 的执行结果,而事务中没有 `if-else` 的分支逻辑,如果 `SETNX` 没有抢到锁,`EXPIRE` 就不应该执行。 - -为了解决这个疑难问题,Redis 开源社区涌现了许多分布式锁的 library,为了治理这个乱象,后来在 Redis 2.8 的版本中,加入了 `SET` 指令的扩展参数,使得 `SETNX` 可以和 `EXPIRE` 指令一起执行了: - -```console -> SET lock:test true ex 5 nx -OK -... do something critical ... -> del lock:test -``` - -你只需要符合 `SET key value [EX seconds | PX milliseconds] [NX | XX] [KEEPTTL]` 这样的格式就好了,你也在下方右拐参照官方的文档: - -- 官方文档:[https://redis.io/commands/set](https://redis.io/commands/set) - -另外,官方文档也在 [`SETNX` 文档](https://redis.io/commands/setnx)中提到了这样一种思路:**把 SETNX 对应 key 的 value 设置为 **,这样在其他客户端访问时就能够自己判断是否能够获取下一个 value 为上述格式的锁了。 - -## 代码实现 - -下面用 Jedis 来模拟实现以下,关键代码如下: - -```java -private static final String LOCK_SUCCESS = "OK"; -private static final Long RELEASE_SUCCESS = 1L; -private static final String SET_IF_NOT_EXIST = "NX"; -private static final String SET_WITH_EXPIRE_TIME = "PX"; - -@Override -public String acquire() { - try { - // 获取锁的超时时间,超过这个时间则放弃获取锁 - long end = System.currentTimeMillis() + acquireTimeout; - // 随机生成一个 value - String requireToken = UUID.randomUUID().toString(); - while (System.currentTimeMillis() < end) { - String result = jedis - .set(lockKey, requireToken, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); - if (LOCK_SUCCESS.equals(result)) { - return requireToken; - } - try { - Thread.sleep(100); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - } catch (Exception e) { - log.error("acquire lock due to error", e); - } - - return null; -} - -@Override -public boolean release(String identify) { - if (identify == null) { - return false; - } - - String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; - Object result = new Object(); - try { - result = jedis.eval(script, Collections.singletonList(lockKey), - Collections.singletonList(identify)); - if (RELEASE_SUCCESS.equals(result)) { - log.info("release lock success, requestToken:{}", identify); - return true; - } - } catch (Exception e) { - log.error("release lock due to error", e); - } finally { - if (jedis != null) { - jedis.close(); - } - } - - log.info("release lock failed, requestToken:{}, result:{}", identify, result); - return false; -} -``` - -- 引用自下方 *参考资料 3*,其中还有 RedLock 的实现和测试,有兴趣的童鞋可以戳一下 - -# 推荐阅读 - -1. 【官方文档】Distributed locks with Redis - [https://redis.io/topics/distlock](https://redis.io/topics/distlock) -2. Redis【入门】就这一篇! - [https://www.wmyskxz.com/2018/05/31/redis-ru-men-jiu-zhe-yi-pian/](https://www.wmyskxz.com/2018/05/31/redis-ru-men-jiu-zhe-yi-pian/) -3. Redission - Redis Java Client 源码 - [https://github.com/redisson/redisson](https://github.com/redisson/redisson) -4. 手写一个 Jedis 以及 JedisPool - [https://juejin.im/post/5e5101c46fb9a07cab3a953a](https://juejin.im/post/5e5101c46fb9a07cab3a953a) - -# 参考资料 - -1. 再有人问你分布式锁,这篇文章扔给他 - [https://juejin.im/post/5bbb0d8df265da0abd3533a5#heading-0](https://juejin.im/post/5bbb0d8df265da0abd3533a5#heading-0) -2. 【官方文档】Distributed locks with Redis - [https://redis.io/topics/distlock](https://redis.io/topics/distlock) -3. 【分布式缓存系列】Redis实现分布式锁的正确姿势 - [https://www.cnblogs.com/zhili/p/redisdistributelock.html](https://www.cnblogs.com/zhili/p/redisdistributelock.html) -4. Redis源码剖析和注释(九)--- 字符串命令的实现(t_string) - [https://blog.csdn.net/men_wen/article/details/70325566](https://blog.csdn.net/men_wen/article/details/70325566) -5. 《Redis 深度历险》 - 钱文品/ 著 - diff --git "a/docs/database/Redis/redis-collection/Redis(5)\342\200\224\342\200\224\344\272\277\347\272\247\346\225\260\346\215\256\350\277\207\346\273\244\345\222\214\345\270\203\351\232\206\350\277\207\346\273\244\345\231\250.md" "b/docs/database/Redis/redis-collection/Redis(5)\342\200\224\342\200\224\344\272\277\347\272\247\346\225\260\346\215\256\350\277\207\346\273\244\345\222\214\345\270\203\351\232\206\350\277\207\346\273\244\345\231\250.md" deleted file mode 100644 index b522f656de1..00000000000 --- "a/docs/database/Redis/redis-collection/Redis(5)\342\200\224\342\200\224\344\272\277\347\272\247\346\225\260\346\215\256\350\277\207\346\273\244\345\222\214\345\270\203\351\232\206\350\277\207\346\273\244\345\231\250.md" +++ /dev/null @@ -1,361 +0,0 @@ -> 授权转载自: https://github.com/wmyskxz/MoreThanJava#part3-redis - -![](https://upload-images.jianshu.io/upload_images/7896890-59d043fad3a66d7f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -# 一、布隆过滤器简介 - -[上一次](https://www.wmyskxz.com/2020/03/02/reids-4-shen-qi-de-hyperloglog-jie-jue-tong-ji-wen-ti/) 我们学会了使用 **HyperLogLog** 来对大数据进行一个估算,它非常有价值,可以解决很多精确度不高的统计需求。但是如果我们想知道某一个值是不是已经在 **HyperLogLog** 结构里面了,它就无能为力了,它只提供了 `pfadd` 和 `pfcount` 方法,没有提供类似于 `contains` 的这种方法。 - -就举一个场景吧,比如你 **刷抖音**: - -![](https://upload-images.jianshu.io/upload_images/7896890-c7b6b5c8a47caf4a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -你有 **刷到过重复的推荐内容** 吗?这么多的推荐内容要推荐给这么多的用户,它是怎么保证每个用户在看推荐内容时,保证不会出现之前已经看过的推荐视频呢?也就是说,抖音是如何实现 **推送去重** 的呢? - -你会想到服务器 **记录** 了用户看过的 **所有历史记录**,当推荐系统推荐短视频时会从每个用户的历史记录里进行 **筛选**,过滤掉那些已经存在的记录。问题是当 **用户量很大**,每个用户看过的短视频又很多的情况下,这种方式,推荐系统的去重工作 **在性能上跟的上么?** - -实际上,如果历史记录存储在关系数据库里,去重就需要频繁地对数据库进行 `exists` 查询,当系统并发量很高时,数据库是很难抗住压力的。 - -![image](https://upload-images.jianshu.io/upload_images/7896890-099f3600b7022ef6.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -你可能又想到了 **缓存**,但是这么多用户这么多的历史记录,如果全部缓存起来,那得需要 **浪费多大的空间** 啊.. *(可能老板看一眼账单,看一眼你..)* 并且这个存储空间会随着时间呈线性增长,就算你用缓存撑得住一个月,但是又能继续撑多久呢?不缓存性能又跟不上,咋办呢? - -![](https://upload-images.jianshu.io/upload_images/7896890-204e7440395a31b5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -如上图所示,**布隆过滤器(Bloom Filter)** 就是这样一种专门用来解决去重问题的高级数据结构。但是跟 **HyperLogLog** 一样,它也一样有那么一点点不精确,也存在一定的误判概率,但它能在解决去重的同时,在 **空间上能节省 90%** 以上,也是非常值得的。 - -## 布隆过滤器是什么 - -**布隆过滤器(Bloom Filter)** 是 1970 年由布隆提出的。它 **实际上** 是一个很长的二进制向量和一系列随机映射函数 *(下面详细说)*,实际上你也可以把它 **简单理解** 为一个不怎么精确的 **set** 结构,当你使用它的 `contains` 方法判断某个对象是否存在时,它可能会误判。但是布隆过滤器也不是特别不精确,只要参数设置的合理,它的精确度可以控制的相对足够精确,只会有小小的误判概率。 - -当布隆过滤器说某个值存在时,这个值 **可能不存在**;当它说不存在时,那么 **一定不存在**。打个比方,当它说不认识你时,那就是真的不认识,但是当它说认识你的时候,可能是因为你长得像它认识的另外一个朋友 *(脸长得有些相似)*,所以误判认识你。 - -![image](https://upload-images.jianshu.io/upload_images/7896890-757891d52045869d.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -## 布隆过滤器的使用场景 - -基于上述的功能,我们大致可以把布隆过滤器用于以下的场景之中: - -- **大数据判断是否存在**:这就可以实现出上述的去重功能,如果你的服务器内存足够大的话,那么使用 HashMap 可能是一个不错的解决方案,理论上时间复杂度可以达到 O(1 的级别,但是当数据量起来之后,还是只能考虑布隆过滤器。 -- **解决缓存穿透**:我们经常会把一些热点数据放在 Redis 中当作缓存,例如产品详情。 通常一个请求过来之后我们会先查询缓存,而不用直接读取数据库,这是提升性能最简单也是最普遍的做法,但是 **如果一直请求一个不存在的缓存**,那么此时一定不存在缓存,那就会有 **大量请求直接打到数据库** 上,造成 **缓存穿透**,布隆过滤器也可以用来解决此类问题。 -- **爬虫/ 邮箱等系统的过滤**:平时不知道你有没有注意到有一些正常的邮件也会被放进垃圾邮件目录中,这就是使用布隆过滤器 **误判** 导致的。 - -# 二、布隆过滤器原理解析 - -布隆过滤器 **本质上** 是由长度为 `m` 的位向量或位列表(仅包含 `0` 或 `1` 位值的列表)组成,最初所有的值均设置为 `0`,所以我们先来创建一个稍微长一些的位向量用作展示: - -![](https://upload-images.jianshu.io/upload_images/7896890-362a693c82af3c8e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -当我们向布隆过滤器中添加数据时,会使用 **多个** `hash` 函数对 `key` 进行运算,算得一个证书索引值,然后对位数组长度进行取模运算得到一个位置,每个 `hash` 函数都会算得一个不同的位置。再把位数组的这几个位置都置为 `1` 就完成了 `add` 操作,例如,我们添加一个 `wmyskxz`: - -![](https://upload-images.jianshu.io/upload_images/7896890-fdbf75a56fb03c02.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -向布隆过滤器查查询 `key` 是否存在时,跟 `add` 操作一样,会把这个 `key` 通过相同的多个 `hash` 函数进行运算,查看 **对应的位置** 是否 **都** 为 `1`,**只要有一个位为 `0`**,那么说明布隆过滤器中这个 `key` 不存在。如果这几个位置都是 `1`,并不能说明这个 `key` 一定存在,只能说极有可能存在,因为这些位置的 `1` 可能是因为其他的 `key` 存在导致的。 - -就比如我们在 `add` 了一定的数据之后,查询一个 **不存在** 的 `key`: - -![](https://upload-images.jianshu.io/upload_images/7896890-0beb6acc89d5c927.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -很明显,`1/3/5` 这几个位置的 `1` 是因为上面第一次添加的 `wmyskxz` 而导致的,所以这里就存在 **误判**。幸运的是,布隆过滤器有一个可以预判误判率的公式,比较复杂,感兴趣的朋友可以自行去阅读,比较烧脑.. 只需要记住以下几点就好了: - -- 使用时 **不要让实际元素数量远大于初始化数量**; -- 当实际元素数量超过初始化数量时,应该对布隆过滤器进行 **重建**,重新分配一个 `size` 更大的过滤器,再将所有的历史元素批量 `add` 进行; - -# 三、布隆过滤器的使用 - -**Redis 官方** 提供的布隆过滤器到了 **Redis 4.0** 提供了插件功能之后才正式登场。布隆过滤器作为一个插件加载到 Redis Server 中,给 Redis 提供了强大的布隆去重功能。下面我们来体验一下 Redis 4.0 的布隆过滤器,为了省去繁琐安装过程,我们直接用 -Docker 吧。 - -```bash -> docker pull redislabs/rebloom # 拉取镜像 -> docker run -p6379:6379 redislabs/rebloom # 运行容器 -> redis-cli # 连接容器中的 redis 服务 -``` - -如果上面三条指令执行没有问题,下面就可以体验布隆过滤器了。 - -- 当然,如果你不想使用 Docker,也可以在检查本机 Redis 版本合格之后自行安装插件,可以参考这里: [https://blog.csdn.net/u013030276/article/details/88350641](https://blog.csdn.net/u013030276/article/details/88350641) - -## 布隆过滤器的基本用法 - -布隆过滤器有两个基本指令,`bf.add` 添加元素,`bf.exists` 查询元素是否存在,它的用法和 set 集合的 `sadd` 和 `sismember` 差不多。注意 `bf.add` 只能一次添加一个元素,如果想要一次添加多个,就需要用到 `bf.madd` 指令。同样如果需要一次查询多个元素是否存在,就需要用到 `bf.mexists` 指令。 - -```bash -127.0.0.1:6379> bf.add codehole user1 -(integer) 1 -127.0.0.1:6379> bf.add codehole user2 -(integer) 1 -127.0.0.1:6379> bf.add codehole user3 -(integer) 1 -127.0.0.1:6379> bf.exists codehole user1 -(integer) 1 -127.0.0.1:6379> bf.exists codehole user2 -(integer) 1 -127.0.0.1:6379> bf.exists codehole user3 -(integer) 1 -127.0.0.1:6379> bf.exists codehole user4 -(integer) 0 -127.0.0.1:6379> bf.madd codehole user4 user5 user6 -1) (integer) 1 -2) (integer) 1 -3) (integer) 1 -127.0.0.1:6379> bf.mexists codehole user4 user5 user6 user7 -1) (integer) 1 -2) (integer) 1 -3) (integer) 1 -4) (integer) 0 -``` - -上面使用的布隆过过滤器只是默认参数的布隆过滤器,它在我们第一次 `add` 的时候自动创建。Redis 也提供了可以自定义参数的布隆过滤器,只需要在 `add` 之前使用 `bf.reserve` 指令显式创建就好了。如果对应的 `key` 已经存在,`bf.reserve` 会报错。 - -`bf.reserve` 有三个参数,分别是 `key`、`error_rate` *(错误率)* 和 `initial_size`: - -- **`error_rate` 越低,需要的空间越大**,对于不需要过于精确的场合,设置稍大一些也没有关系,比如上面说的推送系统,只会让一小部分的内容被过滤掉,整体的观看体验还是不会受到很大影响的; -- **`initial_size` 表示预计放入的元素数量**,当实际数量超过这个值时,误判率就会提升,所以需要提前设置一个较大的数值避免超出导致误判率升高; - -如果不适用 `bf.reserve`,默认的 `error_rate` 是 `0.01`,默认的 `initial_size` 是 `100`。 - -# 四、布隆过滤器代码实现 - -## 自己简单模拟实现 - -根据上面的基础理论,我们很容易就可以自己实现一个用于 `简单模拟` 的布隆过滤器数据结构: - -```java -public static class BloomFilter { - - private byte[] data; - - public BloomFilter(int initSize) { - this.data = new byte[initSize * 2]; // 默认创建大小 * 2 的空间 - } - - public void add(int key) { - int location1 = Math.abs(hash1(key) % data.length); - int location2 = Math.abs(hash2(key) % data.length); - int location3 = Math.abs(hash3(key) % data.length); - - data[location1] = data[location2] = data[location3] = 1; - } - - public boolean contains(int key) { - int location1 = Math.abs(hash1(key) % data.length); - int location2 = Math.abs(hash2(key) % data.length); - int location3 = Math.abs(hash3(key) % data.length); - - return data[location1] * data[location2] * data[location3] == 1; - } - - private int hash1(Integer key) { - return key.hashCode(); - } - - private int hash2(Integer key) { - int hashCode = key.hashCode(); - return hashCode ^ (hashCode >>> 3); - } - - private int hash3(Integer key) { - int hashCode = key.hashCode(); - return hashCode ^ (hashCode >>> 16); - } -} -``` - -这里很简单,内部仅维护了一个 `byte` 类型的 `data` 数组,实际上 `byte` 仍然占有一个字节之多,可以优化成 `bit` 来代替,这里也仅仅是用于方便模拟。另外我也创建了三个不同的 `hash` 函数,其实也就是借鉴 `HashMap` 哈希抖动的办法,分别使用自身的 `hash` 和右移不同位数相异或的结果。并且提供了基础的 `add` 和 `contains` 方法。 - -下面我们来简单测试一下这个布隆过滤器的效果如何: - -```java -public static void main(String[] args) { - Random random = new Random(); - // 假设我们的数据有 1 百万 - int size = 1_000_000; - // 用一个数据结构保存一下所有实际存在的值 - LinkedList existentNumbers = new LinkedList<>(); - BloomFilter bloomFilter = new BloomFilter(size); - - for (int i = 0; i < size; i++) { - int randomKey = random.nextInt(); - existentNumbers.add(randomKey); - bloomFilter.add(randomKey); - } - - // 验证已存在的数是否都存在 - AtomicInteger count = new AtomicInteger(); - AtomicInteger finalCount = count; - existentNumbers.forEach(number -> { - if (bloomFilter.contains(number)) { - finalCount.incrementAndGet(); - } - }); - System.out.printf("实际的数据量: %d, 判断存在的数据量: %d \n", size, count.get()); - - // 验证10个不存在的数 - count = new AtomicInteger(); - while (count.get() < 10) { - int key = random.nextInt(); - if (existentNumbers.contains(key)) { - continue; - } else { - // 这里一定是不存在的数 - System.out.println(bloomFilter.contains(key)); - count.incrementAndGet(); - } - } -} -``` - -输出如下: - -```bash -实际的数据量: 1000000, 判断存在的数据量: 1000000 -false -true -false -true -true -true -false -false -true -false -``` - -这就是前面说到的,当布隆过滤器说某个值 **存在时**,这个值 **可能不存在**,当它说某个值 **不存在时**,那就 **肯定不存在**,并且还有一定的误判率... - - -## 手动实现参考 - -当然上面的版本特别 low,不过主体思想是不差的,这里也给出一个好一些的版本用作自己实现测试的参考: - -```java -import java.util.BitSet; - -public class MyBloomFilter { - - /** - * 位数组的大小 - */ - private static final int DEFAULT_SIZE = 2 << 24; - /** - * 通过这个数组可以创建 6 个不同的哈希函数 - */ - private static final int[] SEEDS = new int[]{3, 13, 46, 71, 91, 134}; - - /** - * 位数组。数组中的元素只能是 0 或者 1 - */ - private BitSet bits = new BitSet(DEFAULT_SIZE); - - /** - * 存放包含 hash 函数的类的数组 - */ - private SimpleHash[] func = new SimpleHash[SEEDS.length]; - - /** - * 初始化多个包含 hash 函数的类的数组,每个类中的 hash 函数都不一样 - */ - public MyBloomFilter() { - // 初始化多个不同的 Hash 函数 - for (int i = 0; i < SEEDS.length; i++) { - func[i] = new SimpleHash(DEFAULT_SIZE, SEEDS[i]); - } - } - - /** - * 添加元素到位数组 - */ - public void add(Object value) { - for (SimpleHash f : func) { - bits.set(f.hash(value), true); - } - } - - /** - * 判断指定元素是否存在于位数组 - */ - public boolean contains(Object value) { - boolean ret = true; - for (SimpleHash f : func) { - ret = ret && bits.get(f.hash(value)); - } - return ret; - } - - /** - * 静态内部类。用于 hash 操作! - */ - public static class SimpleHash { - - private int cap; - private int seed; - - public SimpleHash(int cap, int seed) { - this.cap = cap; - this.seed = seed; - } - - /** - * 计算 hash 值 - */ - public int hash(Object value) { - int h; - return (value == null) ? 0 : Math.abs(seed * (cap - 1) & ((h = value.hashCode()) ^ (h >>> 16))); - } - - } -} -``` - -## 使用 Google 开源的 Guava 中自带的布隆过滤器 - -自己实现的目的主要是为了让自己搞懂布隆过滤器的原理,Guava 中布隆过滤器的实现算是比较权威的,所以实际项目中我们不需要手动实现一个布隆过滤器。 - -首先我们需要在项目中引入 Guava 的依赖: - -```xml - - com.google.guava - guava - 28.0-jre - -``` - -实际使用如下: - -我们创建了一个最多存放 最多 1500 个整数的布隆过滤器,并且我们可以容忍误判的概率为百分之(0.01) - -```java -// 创建布隆过滤器对象 -BloomFilter filter = BloomFilter.create( - Funnels.integerFunnel(), - 1500, - 0.01); -// 判断指定元素是否存在 -System.out.println(filter.mightContain(1)); -System.out.println(filter.mightContain(2)); -// 将元素添加进布隆过滤器 -filter.put(1); -filter.put(2); -System.out.println(filter.mightContain(1)); -System.out.println(filter.mightContain(2)); -``` - -在我们的示例中,当 `mightContain()` 方法返回 `true` 时,我们可以 **99%** 确定该元素在过滤器中,当过滤器返回 `false` 时,我们可以 **100%** 确定该元素不存在于过滤器中。 - -Guava 提供的布隆过滤器的实现还是很不错的 *(想要详细了解的可以看一下它的源码实现)*,但是它有一个重大的缺陷就是只能单机使用 *(另外,容量扩展也不容易)*,而现在互联网一般都是分布式的场景。为了解决这个问题,我们就需要用到 **Redis** 中的布隆过滤器了。 - -# 相关阅读 - -1. Redis(1)——5种基本数据结构 - [https://www.wmyskxz.com/2020/02/28/redis-1-5-chong-ji-ben-shu-ju-jie-gou/](https://www.wmyskxz.com/2020/02/28/redis-1-5-chong-ji-ben-shu-ju-jie-gou/) -2. Redis(2)——跳跃表 - [https://www.wmyskxz.com/2020/02/29/redis-2-tiao-yue-biao/](https://www.wmyskxz.com/2020/02/29/redis-2-tiao-yue-biao/) -3. Redis(3)——分布式锁深入探究 - [https://www.wmyskxz.com/2020/03/01/redis-3/](https://www.wmyskxz.com/2020/03/01/redis-3/) -4. Reids(4)——神奇的HyperLoglog解决统计问题 - [https://www.wmyskxz.com/2020/03/02/reids-4-shen-qi-de-hyperloglog-jie-jue-tong-ji-wen-ti/](https://www.wmyskxz.com/2020/03/02/reids-4-shen-qi-de-hyperloglog-jie-jue-tong-ji-wen-ti/)\ - -# 参考资料 - -1. 《Redis 深度历险》 - 钱文品/ 著 - [https://book.douban.com/subject/30386804/](https://book.douban.com/subject/30386804/) -2. 5 分钟搞懂布隆过滤器,亿级数据过滤算法你值得拥有! - [https://juejin.im/post/5de1e37c5188256e8e43adfc](https://juejin.im/post/5de1e37c5188256e8e43adfc) -3. 【原创】不了解布隆过滤器?一文给你整的明明白白! - [https://github.com/Snailclimb/JavaGuide/blob/master/docs/dataStructures-algorithms/data-structure/bloom-filter.md](https://github.com/Snailclimb/JavaGuide/blob/master/docs/dataStructures-algorithms/data-structure/bloom-filter.md) - diff --git "a/docs/database/Redis/redis-collection/Redis(6)\342\200\224\342\200\224GeoHash\346\237\245\346\211\276\351\231\204\350\277\221\347\232\204\344\272\272.md" "b/docs/database/Redis/redis-collection/Redis(6)\342\200\224\342\200\224GeoHash\346\237\245\346\211\276\351\231\204\350\277\221\347\232\204\344\272\272.md" deleted file mode 100644 index 47152bf19db..00000000000 --- "a/docs/database/Redis/redis-collection/Redis(6)\342\200\224\342\200\224GeoHash\346\237\245\346\211\276\351\231\204\350\277\221\347\232\204\344\272\272.md" +++ /dev/null @@ -1,227 +0,0 @@ -> 授权转载自: https://github.com/wmyskxz/MoreThanJava#part3-redis - -![](https://upload-images.jianshu.io/upload_images/7896890-8ccb98beab9aff6a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -像微信 **"附近的人"**,美团 **"附近的餐厅"**,支付宝共享单车 **"附近的车"** 是怎么设计实现的呢? - -# 一、使用数据库实现查找附近的人 - -我们都知道,地球上的任何一个位置都可以使用二维的 **经纬度** 来表示,经度范围 *[-180, 180]*,纬度范围 *[-90, 90]*,纬度正负以赤道为界,北正南负,经度正负以本初子午线 *(英国格林尼治天文台)* 为界,东正西负。比如说,北京人民英雄纪念碑的经纬度坐标就是 *(39.904610, 116.397724)*,都是正数,因为中国位于东北半球。 - -所以,当我们使用数据库存储了所有人的 **经纬度** 信息之后,我们就可以基于当前的坐标节点,来划分出一个矩形的范围,来得知附近的人,如下图: - -![](https://upload-images.jianshu.io/upload_images/7896890-c5e82d3cab59ad22.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -所以,我们很容易写出下列的伪 SQL 语句: - -```sql -SELECT id FROM positions WHERE x0 - r < x < x0 + r AND y0 - r < y < y0 + r -``` - -如果我们还想进一步地知道与每个坐标元素的距离并排序的话,就需要一定的计算。 - -当两个坐标元素的距离不是很远的时候,我们就可以简单利用 **勾股定理** 就能够得出他们之间的 **距离**。不过需要注意的是,地球不是一个标准的球体,**经纬度的密度** 是 **不一样** 的,所以我们使用勾股定理计算平方之后再求和时,需要按照一定的系数 **加权** 再进行求和。当然,如果不准求精确的话,加权也不必了。 - -参考下方 *参考资料 2* 我们能够差不多能写出如下优化之后的 SQL 语句来:*(仅供参考)* - -```sql -SELECT - * -FROM - users_location -WHERE - latitude > '.$lat.' - 1 - AND latitude < '.$lat.' + 1 AND longitude > '.$lon.' - 1 - AND longitude < '.$lon.' + 1 -ORDER BY - ACOS( - SIN( ( '.$lat.' * 3.1415 ) / 180 ) * SIN( ( latitude * 3.1415 ) / 180 ) + COS( ( '.$lat.' * 3.1415 ) / 180 ) * COS( ( latitude * 3.1415 ) / 180 ) * COS( ( '.$lon.' * 3.1415 ) / 180 - ( longitude * 3.1415 ) / 180 ) - ) * 6380 ASC - LIMIT 10 '; -``` - -为了满足高性能的矩形区域算法,数据表也需要把经纬度坐标加上 **双向复合索引 (x, y)**,这样可以满足最大优化查询性能。 - -# 二、GeoHash 算法简述 - -这是业界比较通用的,用于 **地理位置距离排序** 的一个算法,**Redis** 也采用了这样的算法。GeoHash 算法将 **二维的经纬度** 数据映射到 **一维** 的整数,这样所有的元素都将在挂载到一条线上,距离靠近的二维坐标映射到一维后的点之间距离也会很接近。当我们想要计算 **「附近的人时」**,首先将目标位置映射到这条线上,然后在这个一维的线上获取附近的点就行了。 - -它的核心思想就是把整个地球看成是一个 **二维的平面**,然后把这个平面不断地等分成一个一个小的方格,**每一个** 坐标元素都位于其中的 **唯一一个方格** 中,等分之后的 **方格越小**,那么坐标也就 **越精确**,类似下图: - -![](https://upload-images.jianshu.io/upload_images/7896890-6396ae153a485857.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -经过划分的地球,我们需要对其进行编码: - -![](https://upload-images.jianshu.io/upload_images/7896890-573525c3f1179bbc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -经过这样顺序的编码之后,如果你仔细观察一会儿,你就会发现一些规律: - -- 横着的所有编码中,**第 2 位和第 4 位都是一样的**,例如第一排第一个 `0101` 和第二个 `0111`,他们的第 2 位和第 4 位都是 `1`; -- 竖着的所有编码中,**第 1 位和第 3 位是递增的**,例如第一排第一个 `0101`,如果单独把第 1 位和第 3 位拎出来的话,那就是 `00`,同理看第一排第二个 `0111`,同样的方法第 1 位和第 3 位拎出来是 `01`,刚好是 `00` 递增一个; - -通过这样的规律我们就把每一个小方块儿进行了一定顺序的编码,这样做的 **好处** 是显而易见的:每一个元素坐标既能够被 **唯一标识** 在这张被编码的地图上,也不至于 **暴露特别的具体的位置**,因为区域是共享的,我可以告诉你我就在公园附近,但是在具体的哪个地方你就无从得知了。 - -总之,我们通过上面的思想,能够把任意坐标变成一串二进制的编码了,类似于 `11010010110001000100` 这样 *(注意经度和维度是交替出现的哦..)*,通过这个整数我们就可以还原出元素的坐标,整数越长,还原出来的坐标值的损失程序就越小。对于 **"附近的人"** 这个功能来说,损失的一点经度可以忽略不计。 - -最后就是一个 `Base32` *(0~9, a~z, 去掉 a/i/l/o 四个字母)* 的编码操作,让它变成一个字符串,例如上面那一串儿就变成了 `wx4g0ec1`。 - -在 **Redis** 中,经纬度使用 `52` 位的整数进行编码,放进了 zset 里面,zset 的 `value` 是元素的 `key`,`score` 是 **GeoHash** 的 `52` 位整数值。zset 的 `score` 虽然是浮点数,但是对于 `52` 位的整数值来说,它可以无损存储。 - -# 三、在 Redis 中使用 Geo - -> 下方内容引自 *参考资料 1 - 《Redis 深度历险》* - -在使用 **Redis** 进行 **Geo 查询** 时,我们要时刻想到它的内部结构实际上只是一个 **zset(skiplist)**。通过 zset 的 `score` 排序就可以得到坐标附近的其他元素 *(实际情况要复杂一些,不过这样理解足够了)*,通过将 `score` 还原成坐标值就可以得到元素的原始坐标了。 - -Redis 提供的 Geo 指令只有 6 个,很容易就可以掌握。 - -## 增加 - -`geoadd` 指令携带集合名称以及多个经纬度名称三元组,注意这里可以加入多个三元组。 - -```bash -127.0.0.1:6379> geoadd company 116.48105 39.996794 juejin -(integer) 1 -127.0.0.1:6379> geoadd company 116.514203 39.905409 ireader -(integer) 1 -127.0.0.1:6379> geoadd company 116.489033 40.007669 meituan -(integer) 1 -127.0.0.1:6379> geoadd company 116.562108 39.787602 jd 116.334255 40.027400 xiaomi -(integer) 2 -``` - -不过很奇怪.. Redis 没有直接提供 Geo 的删除指令,但是我们可以通过 zset 相关的指令来操作 Geo 数据,所以元素删除可以使用 `zrem` 指令即可。 - -## 距离 - -`geodist` 指令可以用来计算两个元素之间的距离,携带集合名称、2 个名称和距离单位。 - -```bash -127.0.0.1:6379> geodist company juejin ireader km -"10.5501" -127.0.0.1:6379> geodist company juejin meituan km -"1.3878" -127.0.0.1:6379> geodist company juejin jd km -"24.2739" -127.0.0.1:6379> geodist company juejin xiaomi km -"12.9606" -127.0.0.1:6379> geodist company juejin juejin km -"0.0000" -``` - -我们可以看到掘金离美团最近,因为它们都在望京。距离单位可以是 `m`、`km`、`ml`、`ft`,分别代表米、千米、英里和尺。 - -## 获取元素位置 - -`geopos` 指令可以获取集合中任意元素的经纬度坐标,可以一次获取多个。 - -```bash -127.0.0.1:6379> geopos company juejin -1) 1) "116.48104995489120483" - 2) "39.99679348858259686" -127.0.0.1:6379> geopos company ireader -1) 1) "116.5142020583152771" - 2) "39.90540918662494363" -127.0.0.1:6379> geopos company juejin ireader -1) 1) "116.48104995489120483" - 2) "39.99679348858259686" -2) 1) "116.5142020583152771" - 2) "39.90540918662494363" -``` - -我们观察到获取的经纬度坐标和 `geoadd` 进去的坐标有轻微的误差,原因是 **Geohash** 对二维坐标进行的一维映射是有损的,通过映射再还原回来的值会出现较小的差别。对于 **「附近的人」** 这种功能来说,这点误差根本不是事。 - -## 获取元素的 hash 值 - -`geohash` 可以获取元素的经纬度编码字符串,上面已经提到,它是 `base32` 编码。 你可以使用这个编码值去 `http://geohash.org/${hash}` 中进行直接定位,它是 **Geohash** 的标准编码值。 - -```bash -127.0.0.1:6379> geohash company ireader -1) "wx4g52e1ce0" -127.0.0.1:6379> geohash company juejin -1) "wx4gd94yjn0" -``` - -让我们打开地址 `http://geohash.org/wx4g52e1ce0`,观察地图指向的位置是否正确: - -![](https://upload-images.jianshu.io/upload_images/7896890-b5d4215d6397729c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -很好,就是这个位置,非常准确。 - -## 附近的公司 -`georadiusbymember` 指令是最为关键的指令,它可以用来查询指定元素附近的其它元素,它的参数非常复杂。 - -```bash -# 范围 20 公里以内最多 3 个元素按距离正排,它不会排除自身 -127.0.0.1:6379> georadiusbymember company ireader 20 km count 3 asc -1) "ireader" -2) "juejin" -3) "meituan" -# 范围 20 公里以内最多 3 个元素按距离倒排 -127.0.0.1:6379> georadiusbymember company ireader 20 km count 3 desc -1) "jd" -2) "meituan" -3) "juejin" -# 三个可选参数 withcoord withdist withhash 用来携带附加参数 -# withdist 很有用,它可以用来显示距离 -127.0.0.1:6379> georadiusbymember company ireader 20 km withcoord withdist withhash count 3 asc -1) 1) "ireader" - 2) "0.0000" - 3) (integer) 4069886008361398 - 4) 1) "116.5142020583152771" - 2) "39.90540918662494363" -2) 1) "juejin" - 2) "10.5501" - 3) (integer) 4069887154388167 - 4) 1) "116.48104995489120483" - 2) "39.99679348858259686" -3) 1) "meituan" - 2) "11.5748" - 3) (integer) 4069887179083478 - 4) 1) "116.48903220891952515" - 2) "40.00766997707732031" -``` - -除了 `georadiusbymember` 指令根据元素查询附近的元素,**Redis** 还提供了根据坐标值来查询附近的元素,这个指令更加有用,它可以根据用户的定位来计算「附近的车」,「附近的餐馆」等。它的参数和 `georadiusbymember` 基本一致,除了将目标元素改成经纬度坐标值: - -```bash -127.0.0.1:6379> georadius company 116.514202 39.905409 20 km withdist count 3 asc -1) 1) "ireader" - 2) "0.0000" -2) 1) "juejin" - 2) "10.5501" -3) 1) "meituan" - 2) "11.5748" -``` - -## 注意事项 - -在一个地图应用中,车的数据、餐馆的数据、人的数据可能会有百万千万条,如果使用 **Redis** 的 **Geo** 数据结构,它们将 **全部放在一个** zset 集合中。在 **Redis** 的集群环境中,集合可能会从一个节点迁移到另一个节点,如果单个 key 的数据过大,会对集群的迁移工作造成较大的影响,在集群环境中单个 key 对应的数据量不宜超过 1M,否则会导致集群迁移出现卡顿现象,影响线上服务的正常运行。 - -所以,这里建议 **Geo** 的数据使用 **单独的 Redis 实例部署**,不使用集群环境。 - -如果数据量过亿甚至更大,就需要对 **Geo** 数据进行拆分,按国家拆分、按省拆分,按市拆分,在人口特大城市甚至可以按区拆分。这样就可以显著降低单个 zset 集合的大小。 - -# 相关阅读 - -1. Redis(1)——5种基本数据结构 - [https://www.wmyskxz.com/2020/02/28/redis-1-5-chong-ji-ben-shu-ju-jie-gou/](https://www.wmyskxz.com/2020/02/28/redis-1-5-chong-ji-ben-shu-ju-jie-gou/) -2. Redis(2)——跳跃表 - [https://www.wmyskxz.com/2020/02/29/redis-2-tiao-yue-biao/](https://www.wmyskxz.com/2020/02/29/redis-2-tiao-yue-biao/) -3. Redis(3)——分布式锁深入探究 - [https://www.wmyskxz.com/2020/03/01/redis-3/](https://www.wmyskxz.com/2020/03/01/redis-3/) -4. Reids(4)——神奇的HyperLoglog解决统计问题 - [https://www.wmyskxz.com/2020/03/02/reids-4-shen-qi-de-hyperloglog-jie-jue-tong-ji-wen-ti/](https://www.wmyskxz.com/2020/03/02/reids-4-shen-qi-de-hyperloglog-jie-jue-tong-ji-wen-ti/) -5. Redis(5)——亿级数据过滤和布隆过滤器 - [https://www.wmyskxz.com/2020/03/11/redis-5-yi-ji-shu-ju-guo-lu-he-bu-long-guo-lu-qi/](https://www.wmyskxz.com/2020/03/11/redis-5-yi-ji-shu-ju-guo-lu-he-bu-long-guo-lu-qi/) - -# 参考资料 - -1. 《Redis 深度历险》 - 钱文品/ 著 - [https://book.douban.com/subject/30386804/](https://book.douban.com/subject/30386804/) -2. mysql经纬度查询并且计算2KM范围内附近用户的sql查询性能优化实例教程 - [https://www.cnblogs.com/mgbert/p/4146538.html](https://www.cnblogs.com/mgbert/p/4146538.html) -3. Geohash算法原理及实现 - [https://www.jianshu.com/p/2fd0cf12e5ba](https://www.jianshu.com/p/2fd0cf12e5ba) -4. GeoHash算法学习讲解、解析及原理分析 - [https://zhuanlan.zhihu.com/p/35940647](https://zhuanlan.zhihu.com/p/35940647) - -> - 本文已收录至我的 Github 程序员成长系列 **【More Than Java】,学习,不止 Code,欢迎 star:[https://github.com/wmyskxz/MoreThanJava](https://github.com/wmyskxz/MoreThanJava)** -> - **个人公众号** :wmyskxz,**个人独立域名博客**:wmyskxz.com,坚持原创输出,下方扫码关注,2020,与您共同成长! - -![](https://upload-images.jianshu.io/upload_images/7896890-fca34cfd601e7449.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -非常感谢各位人才能 **看到这里**,如果觉得本篇文章写得不错,觉得 **「我没有三颗心脏」有点东西** 的话,**求点赞,求关注,求分享,求留言!** - -创作不易,各位的支持和认可,就是我创作的最大动力,我们下篇文章见! \ No newline at end of file diff --git "a/docs/database/Redis/redis-collection/Redis(7)\342\200\224\342\200\224\346\214\201\344\271\205\345\214\226.md" "b/docs/database/Redis/redis-collection/Redis(7)\342\200\224\342\200\224\346\214\201\344\271\205\345\214\226.md" deleted file mode 100644 index a2eb1d61b6f..00000000000 --- "a/docs/database/Redis/redis-collection/Redis(7)\342\200\224\342\200\224\346\214\201\344\271\205\345\214\226.md" +++ /dev/null @@ -1,215 +0,0 @@ -> 授权转载自: https://github.com/wmyskxz/MoreThanJava#part3-redis - -![](https://upload-images.jianshu.io/upload_images/7896890-7879862264eeea7b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -# 一、持久化简介 - -**Redis** 的数据 **全部存储** 在 **内存** 中,如果 **突然宕机**,数据就会全部丢失,因此必须有一套机制来保证 Redis 的数据不会因为故障而丢失,这种机制就是 Redis 的 **持久化机制**,它会将内存中的数据库状态 **保存到磁盘** 中。 - -## 持久化发生了什么 | 从内存到磁盘 - -我们来稍微考虑一下 **Redis** 作为一个 **"内存数据库"** 要做的关于持久化的事情。通常来说,从客户端发起请求开始,到服务器真实地写入磁盘,需要发生如下几件事情: - -![](https://upload-images.jianshu.io/upload_images/7896890-5c209bc08da11abb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -**详细版** 的文字描述大概就是下面这样: - -1. 客户端向数据库 **发送写命令** *(数据在客户端的内存中)* -2. 数据库 **接收** 到客户端的 **写请求** *(数据在服务器的内存中)* -3. 数据库 **调用系统 API** 将数据写入磁盘 *(数据在内核缓冲区中)* -4. 操作系统将 **写缓冲区** 传输到 **磁盘控控制器** *(数据在磁盘缓存中)* -5. 操作系统的磁盘控制器将数据 **写入实际的物理媒介** 中 *(数据在磁盘中)* - -**注意:** 上面的过程其实是 **极度精简** 的,在实际的操作系统中,**缓存** 和 **缓冲区** 会比这 **多得多**... - -## 如何尽可能保证持久化的安全 - -如果我们故障仅仅涉及到 **软件层面** *(该进程被管理员终止或程序崩溃)* 并且没有接触到内核,那么在 *上述步骤 3* 成功返回之后,我们就认为成功了。即使进程崩溃,操作系统仍然会帮助我们把数据正确地写入磁盘。 - -如果我们考虑 **停电/ 火灾** 等 **更具灾难性** 的事情,那么只有在完成了第 **5** 步之后,才是安全的。 - -![机房”火了“](https://upload-images.jianshu.io/upload_images/7896890-de083f477fe1bce4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -所以我们可以总结得出数据安全最重要的阶段是:**步骤三、四、五**,即: - -- 数据库软件调用写操作将用户空间的缓冲区转移到内核缓冲区的频率是多少? -- 内核多久从缓冲区取数据刷新到磁盘控制器? -- 磁盘控制器多久把数据写入物理媒介一次? -- **注意:** 如果真的发生灾难性的事件,我们可以从上图的过程中看到,任何一步都可能被意外打断丢失,所以只能 **尽可能地保证** 数据的安全,这对于所有数据库来说都是一样的。 - -我们从 **第三步** 开始。Linux 系统提供了清晰、易用的用于操作文件的 `POSIX file API`,`20` 多年过去,仍然还有很多人对于这一套 `API` 的设计津津乐道,我想其中一个原因就是因为你光从 `API` 的命名就能够很清晰地知道这一套 API 的用途: - -```c -int open(const char *path, int oflag, .../*,mode_t mode */); -int close (int filedes);int remove( const char *fname ); -ssize_t write(int fildes, const void *buf, size_t nbyte); -ssize_t read(int fildes, void *buf, size_t nbyte); -``` - -- 参考自:API 设计最佳实践的思考 - [https://www.cnblogs.com/yuanjiangw/p/10846560.html](https://www.cnblogs.com/yuanjiangw/p/10846560.html) - -所以,我们有很好的可用的 `API` 来完成 **第三步**,但是对于成功返回之前,我们对系统调用花费的时间没有太多的控制权。 - -然后我们来说说 **第四步**。我们知道,除了早期对电脑特别了解那帮人 *(操作系统就这帮人搞的)*,实际的物理硬件都不是我们能够 **直接操作** 的,都是通过 **操作系统调用** 来达到目的的。为了防止过慢的 I/O 操作拖慢整个系统的运行,操作系统层面做了很多的努力,譬如说 **上述第四步** 提到的 **写缓冲区**,并不是所有的写操作都会被立即写入磁盘,而是要先经过一个缓冲区,默认情况下,Linux 将在 **30 秒** 后实际提交写入。 - -![image](https://upload-images.jianshu.io/upload_images/7896890-c08b7572ef02d67b.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -但是很明显,**30 秒** 并不是 Redis 能够承受的,这意味着,如果发生故障,那么最近 30 秒内写入的所有数据都可能会丢失。幸好 `PROSIX API` 提供了另一个解决方案:`fsync`,该命令会 **强制** 内核将 **缓冲区** 写入 **磁盘**,但这是一个非常消耗性能的操作,每次调用都会 **阻塞等待** 直到设备报告 IO 完成,所以一般在生产环境的服务器中,**Redis** 通常是每隔 1s 左右执行一次 `fsync` 操作。 - -到目前为止,我们了解到了如何控制 `第三步` 和 `第四步`,但是对于 **第五步**,我们 **完全无法控制**。也许一些内核实现将试图告诉驱动实际提交物理介质上的数据,或者控制器可能会为了提高速度而重新排序写操作,不会尽快将数据真正写到磁盘上,而是会等待几个多毫秒。这完全是我们无法控制的。 - - -# 二、Redis 中的两种持久化方式 - -## 方式一:快照 - -![image](https://upload-images.jianshu.io/upload_images/7896890-9a4d234c53120b33.gif?imageMogr2/auto-orient/strip) - -**Redis 快照** 是最简单的 Redis 持久性模式。当满足特定条件时,它将生成数据集的时间点快照,例如,如果先前的快照是在2分钟前创建的,并且现在已经至少有 *100* 次新写入,则将创建一个新的快照。此条件可以由用户配置 Redis 实例来控制,也可以在运行时修改而无需重新启动服务器。快照作为包含整个数据集的单个 `.rdb` 文件生成。 - -但我们知道,Redis 是一个 **单线程** 的程序,这意味着,我们不仅仅要响应用户的请求,还需要进行内存快照。而后者要求 Redis 必须进行 IO 操作,这会严重拖累服务器的性能。 - -还有一个重要的问题是,我们在 **持久化的同时**,**内存数据结构** 还可能在 **变化**,比如一个大型的 hash 字典正在持久化,结果一个请求过来把它删除了,可是这才刚持久化结束,咋办? - -![](https://upload-images.jianshu.io/upload_images/7896890-fbfcbd606e95f105.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -### 使用系统多进程 COW(Copy On Write) 机制 | fork 函数 - -操作系统多进程 **COW(Copy On Write) 机制** 拯救了我们。**Redis** 在持久化时会调用 `glibc` 的函数 `fork` 产生一个子进程,简单理解也就是基于当前进程 **复制** 了一个进程,主进程和子进程会共享内存里面的代码块和数据段: - -![](https://upload-images.jianshu.io/upload_images/7896890-bc264b6a9f0c3404.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -这里多说一点,**为什么 fork 成功调用后会有两个返回值呢?** 因为子进程在复制时复制了父进程的堆栈段,所以两个进程都停留在了 `fork` 函数中 *(都在同一个地方往下继续"同时"执行)*,等待返回,所以 **一次在父进程中返回子进程的 pid,另一次在子进程中返回零,系统资源不够时返回负数**。 *(伪代码如下)* - -```python -pid = os.fork() -if pid > 0: - handle_client_request() # 父进程继续处理客户端请求 -if pid == 0: - handle_snapshot_write() # 子进程处理快照写磁盘 -if pid < 0: - # fork error -``` - -所以 **快照持久化** 可以完全交给 **子进程** 来处理,**父进程** 则继续 **处理客户端请求**。**子进程** 做数据持久化,它 **不会修改现有的内存数据结构**,它只是对数据结构进行遍历读取,然后序列化写到磁盘中。但是 **父进程** 不一样,它必须持续服务客户端请求,然后对 **内存数据结构进行不间断的修改**。 - -这个时候就会使用操作系统的 COW 机制来进行 **数据段页面** 的分离。数据段是由很多操作系统的页面组合而成,当父进程对其中一个页面的数据进行修改时,会将被共享的页面复 -制一份分离出来,然后 **对这个复制的页面进行修改**。这时 **子进程** 相应的页面是 **没有变化的**,还是进程产生时那一瞬间的数据。 - -子进程因为数据没有变化,它能看到的内存里的数据在进程产生的一瞬间就凝固了,再也不会改变,这也是为什么 **Redis** 的持久化 **叫「快照」的原因**。接下来子进程就可以非常安心的遍历数据了进行序列化写磁盘了。 - -## 方式二:AOF - -![](https://upload-images.jianshu.io/upload_images/7896890-e4e08ebef2cf0144.gif?imageMogr2/auto-orient/strip) - -**快照不是很持久**。如果运行 Redis 的计算机停止运行,电源线出现故障或者您 `kill -9` 的实例意外发生,则写入 Redis 的最新数据将丢失。尽管这对于某些应用程序可能不是什么大问题,但有些使用案例具有充分的耐用性,在这些情况下,快照并不是可行的选择。 - -**AOF(Append Only File - 仅追加文件)** 它的工作方式非常简单:每次执行 **修改内存** 中数据集的写操作时,都会 **记录** 该操作。假设 AOF 日志记录了自 Redis 实例创建以来 **所有的修改性指令序列**,那么就可以通过对一个空的 Redis 实例 **顺序执行所有的指令**,也就是 **「重放」**,来恢复 Redis 当前实例的内存数据结构的状态。 - -为了展示 AOF 在实际中的工作方式,我们来做一个简单的实验: - -```bash -./redis-server --appendonly yes # 设置一个新实例为 AOF 模式 -``` - -然后我们执行一些写操作: - -```bash -redis 127.0.0.1:6379> set key1 Hello -OK -redis 127.0.0.1:6379> append key1 " World!" -(integer) 12 -redis 127.0.0.1:6379> del key1 -(integer) 1 -redis 127.0.0.1:6379> del non_existing_key -(integer) 0 -``` - -前三个操作实际上修改了数据集,第四个操作没有修改,因为没有指定名称的键。这是 AOF 日志保存的文本: - -```bash -$ cat appendonly.aof -*2 -$6 -SELECT -$1 -0 -*3 -$3 -set -$4 -key1 -$5 -Hello -*3 -$6 -append -$4 -key1 -$7 - World! -*2 -$3 -del -$4 -key1 -``` - -如您所见,最后的那一条 `DEL` 指令不见了,因为它没有对数据集进行任何修改。 - -就是这么简单。当 Redis 收到客户端修改指令后,会先进行参数校验、逻辑处理,如果没问题,就 **立即** 将该指令文本 **存储** 到 AOF 日志中,也就是说,**先执行指令再将日志存盘**。这一点不同于 `MySQL`、`LevelDB`、`HBase` 等存储引擎,如果我们先存储日志再做逻辑处理,这样就可以保证即使宕机了,我们仍然可以通过之前保存的日志恢复到之前的数据状态,但是 **Redis 为什么没有这么做呢?** - -> Emmm... 没找到特别满意的答案,引用一条来自知乎上的回答吧: -> - **@缘于专注** - 我甚至觉得没有什么特别的原因。仅仅是因为,由于AOF文件会比较大,为了避免写入无效指令(错误指令),必须先做指令检查?如何检查,只能先执行了。因为语法级别检查并不能保证指令的有效性,比如删除一个不存在的key。而MySQL这种是因为它本身就维护了所有的表的信息,所以可以语法检查后过滤掉大部分无效指令直接记录日志,然后再执行。 -> - 更多讨论参见:[为什么Redis先执行指令,再记录AOF日志,而不是像其它存储引擎一样反过来呢? - https://www.zhihu.com/question/342427472](https://www.zhihu.com/question/342427472) - -### AOF 重写 - -![](https://upload-images.jianshu.io/upload_images/7896890-c21e2a37892ee989.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -**Redis** 在长期运行的过程中,AOF 的日志会越变越长。如果实例宕机重启,重放整个 AOF 日志会非常耗时,导致长时间 Redis 无法对外提供服务。所以需要对 **AOF 日志 "瘦身"**。 - -**Redis** 提供了 `bgrewriteaof` 指令用于对 AOF 日志进行瘦身。其 **原理** 就是 **开辟一个子进程** 对内存进行 **遍历** 转换成一系列 Redis 的操作指令,**序列化到一个新的 AOF 日志文件** 中。序列化完毕后再将操作期间发生的 **增量 AOF 日志** 追加到这个新的 AOF 日志文件中,追加完毕后就立即替代旧的 AOF 日志文件了,瘦身工作就完成了。 - -### fsync - -![](https://upload-images.jianshu.io/upload_images/7896890-384a546b5bf6b86d.gif?imageMogr2/auto-orient/strip) - -AOF 日志是以文件的形式存在的,当程序对 AOF 日志文件进行写操作时,实际上是将内容写到了内核为文件描述符分配的一个内存缓存中,然后内核会异步将脏数据刷回到磁盘的。 - -就像我们 *上方第四步* 描述的那样,我们需要借助 `glibc` 提供的 `fsync(int fd)` 函数来讲指定的文件内容 **强制从内核缓存刷到磁盘**。但 **"强制开车"** 仍然是一个很消耗资源的一个过程,需要 **"节制"**!通常来说,生产环境的服务器,Redis 每隔 1s 左右执行一次 `fsync` 操作就可以了。 - -Redis 同样也提供了另外两种策略,一个是 **永不 `fsync`**,来让操作系统来决定合适同步磁盘,很不安全,另一个是 **来一个指令就 `fsync` 一次**,非常慢。但是在生产环境基本不会使用,了解一下即可。 - -## Redis 4.0 混合持久化 - -![](https://upload-images.jianshu.io/upload_images/7896890-7de9f7706be6216c.gif?imageMogr2/auto-orient/strip) - -重启 Redis 时,我们很少使用 `rdb` 来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 `rdb` 来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。 - -**Redis 4.0** 为了解决这个问题,带来了一个新的持久化选项——**混合持久化**。将 `rdb` 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是 **自持久化开始到持久化结束** 的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小: - -![](https://upload-images.jianshu.io/upload_images/7896890-2f7887f84eaa34d9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -于是在 Redis 重启的时候,可以先加载 `rdb` 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。 - -# 相关阅读 - -1. Redis(1)——5种基本数据结构 - [https://www.wmyskxz.com/2020/02/28/redis-1-5-chong-ji-ben-shu-ju-jie-gou/](https://www.wmyskxz.com/2020/02/28/redis-1-5-chong-ji-ben-shu-ju-jie-gou/) -2. Redis(2)——跳跃表 - [https://www.wmyskxz.com/2020/02/29/redis-2-tiao-yue-biao/](https://www.wmyskxz.com/2020/02/29/redis-2-tiao-yue-biao/) -3. Redis(3)——分布式锁深入探究 - [https://www.wmyskxz.com/2020/03/01/redis-3/](https://www.wmyskxz.com/2020/03/01/redis-3/) -4. Reids(4)——神奇的HyperLoglog解决统计问题 - [https://www.wmyskxz.com/2020/03/02/reids-4-shen-qi-de-hyperloglog-jie-jue-tong-ji-wen-ti/](https://www.wmyskxz.com/2020/03/02/reids-4-shen-qi-de-hyperloglog-jie-jue-tong-ji-wen-ti/) -5. Redis(5)——亿级数据过滤和布隆过滤器 - [https://www.wmyskxz.com/2020/03/11/redis-5-yi-ji-shu-ju-guo-lu-he-bu-long-guo-lu-qi/](https://www.wmyskxz.com/2020/03/11/redis-5-yi-ji-shu-ju-guo-lu-he-bu-long-guo-lu-qi/) -6. Redis(6)——GeoHash查找附近的人[https://www.wmyskxz.com/2020/03/12/redis-6-geohash-cha-zhao-fu-jin-de-ren/](https://www.wmyskxz.com/2020/03/12/redis-6-geohash-cha-zhao-fu-jin-de-ren/) - -# 扩展阅读 - -1. Redis 数据备份与恢复 | 菜鸟教程 - [https://www.runoob.com/redis/redis-backup.html](https://www.runoob.com/redis/redis-backup.html) -2. Java Fork/Join 框架 - [https://www.cnblogs.com/cjsblog/p/9078341.html](https://www.cnblogs.com/cjsblog/p/9078341.html) - -# 参考资料 - -1. Redis persistence demystified | antirez weblog (作者博客) - [http://oldblog.antirez.com/post/redis-persistence-demystified.html](http://oldblog.antirez.com/post/redis-persistence-demystified.html) -2. 操作系统 — fork()函数的使用与底层原理 - [https://blog.csdn.net/Dawn_sf/article/details/78709839](https://blog.csdn.net/Dawn_sf/article/details/78709839) -3. 磁盘和内存读写简单原理 - [https://blog.csdn.net/zhanghongzheng3213/article/details/54141202](https://blog.csdn.net/zhanghongzheng3213/article/details/54141202) - diff --git "a/docs/database/Redis/redis-collection/Redis(8)\342\200\224\342\200\224\345\217\221\345\270\203\350\256\242\351\230\205\344\270\216Stream.md" "b/docs/database/Redis/redis-collection/Redis(8)\342\200\224\342\200\224\345\217\221\345\270\203\350\256\242\351\230\205\344\270\216Stream.md" deleted file mode 100644 index 55dad166603..00000000000 --- "a/docs/database/Redis/redis-collection/Redis(8)\342\200\224\342\200\224\345\217\221\345\270\203\350\256\242\351\230\205\344\270\216Stream.md" +++ /dev/null @@ -1,531 +0,0 @@ -> 授权转载自: https://github.com/wmyskxz/MoreThanJava#part3-redis - -![](https://upload-images.jianshu.io/upload_images/7896890-31406a824536c54a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -# 一、Redis 中的发布/订阅功能 - -**发布/ 订阅系统** 是 Web 系统中比较常用的一个功能。简单点说就是 **发布者发布消息,订阅者接受消息**,这有点类似于我们的报纸/ 杂志社之类的: *(借用前边的一张图)* - -![](https://upload-images.jianshu.io/upload_images/7896890-13aa5cb2668368fe.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -- 图片引用自:「消息队列」看过来! - [https://www.wmyskxz.com/2019/07/16/xiao-xi-dui-lie-kan-guo-lai/](https://www.wmyskxz.com/2019/07/16/xiao-xi-dui-lie-kan-guo-lai/) - -从我们 *前面(下方相关阅读)* 学习的知识来看,我们虽然可以使用一个 `list` 列表结构结合 `lpush` 和 `rpop` 来实现消息队列的功能,但是似乎很难实现实现 **消息多播** 的功能: - -![](https://upload-images.jianshu.io/upload_images/7896890-526a5b110a7c4ea2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -为了支持消息多播,**Redis** 不能再依赖于那 5 种基础的数据结构了,它单独使用了一个模块来支持消息多播,这个模块就是 **PubSub**,也就是 **PublisherSubscriber** *(发布者/ 订阅者模式)*。 - -## PubSub 简介 - -我们从 *上面的图* 中可以看到,基于 `list` 结构的消息队列,是一种 `Publisher` 与 `Consumer` 点对点的强关联关系,**Redis** 为了消除这样的强关联,引入了另一种概念:**频道** *(channel)*: - -![](https://upload-images.jianshu.io/upload_images/7896890-cc3bb012eeca9fca.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -当 `Publisher` 往 `channel` 中发布消息时,关注了指定 `channel` 的 `Consumer` 就能够同时受到消息。但这里的 **问题** 是,消费者订阅一个频道是必须 **明确指定频道名称** 的,这意味着,如果我们想要 **订阅多个** 频道,那么就必须 **显式地关注多个** 名称。 - -为了简化订阅的繁琐操作,**Redis** 提供了 **模式订阅** 的功能 **Pattern Subscribe**,这样就可以 **一次性关注多个频道** 了,即使生产者新增了同模式的频道,消费者也可以立即受到消息: - -![](https://upload-images.jianshu.io/upload_images/7896890-18ac258e4e9387da.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -例如上图中,**所有** 位于图片下方的 **`Consumer` 都能够受到消息**。 - -`Publisher` 往 `wmyskxz.chat` 这个 `channel` 中发送了一条消息,不仅仅关注了这个频道的 `Consumer 1` 和 `Consumer 2` 能够受到消息,图片中的两个 `channel` 都和模式 `wmyskxz.*` 匹配,所以 **Redis** 此时会同样发送消息给订阅了 `wmyskxz.*` 这个模式的 `Consumer 3` 和关注了在这个模式下的另一个频道 `wmyskxz.log` 下的 `Consumer 4` 和 `Consumer 5`。 - -另一方面,如果接收消息的频道是 `wmyskxz.chat`,那么 `Consumer 3` 也会受到消息。 - -## 快速体验 - -在 **Redis** 中,**PubSub** 模块的使用非常简单,常用的命令也就下面这么几条: - -```bash -# 订阅频道: -SUBSCRIBE channel [channel ....] # 订阅给定的一个或多个频道的信息 -PSUBSCRIBE pattern [pattern ....] # 订阅一个或多个符合给定模式的频道 -# 发布频道: -PUBLISH channel message # 将消息发送到指定的频道 -# 退订频道: -UNSUBSCRIBE [channel [channel ....]] # 退订指定的频道 -PUNSUBSCRIBE [pattern [pattern ....]] #退订所有给定模式的频道 -``` - -我们可以在本地快速地来体验一下 **PubSub**: - -![](https://upload-images.jianshu.io/upload_images/7896890-518e0d1e93135775.gif?imageMogr2/auto-orient/strip) - -具体步骤如下: - -1. 开启本地 Redis 服务,新建两个控制台窗口; -2. 在其中一个窗口输入 `SUBSCRIBE wmyskxz.chat` 关注 `wmyskxz.chat` 频道,让这个窗口成为 **消费者**。 -3. 在另一个窗口输入 `PUBLISH wmyskxz.chat 'message'` 往这个频道发送消息,这个时候就会看到 **另一个窗口实时地出现** 了发送的测试消息。 - -## 实现原理 - -可以看到,我们通过很简单的两条命令,几乎就可以简单使用这样的一个 **发布/ 订阅系统** 了,但是具体是怎么样实现的呢? - -**每个 Redis 服务器进程维持着一个标识服务器状态** 的 `redis.h/redisServer` 结构,其中就 **保存着有订阅的频道** 以及 **订阅模式** 的信息: - -```c -struct redisServer { - // ... - dict *pubsub_channels; // 订阅频道 - list *pubsub_patterns; // 订阅模式 - // ... -}; -``` - -### 订阅频道原理 - -当客户端订阅某一个频道之后,Redis 就会往 `pubsub_channels` 这个字典中新添加一条数据,实际上这个 `dict` 字典维护的是一张链表,比如,下图展示的 `pubsub_channels` 示例中,`client 1`、`client 2` 就订阅了 `channel 1`,而其他频道也分别被其他客户端订阅: - -![](https://upload-images.jianshu.io/upload_images/7896890-218fc15f7c368eee.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -#### SUBSCRIBE 命令 - -`SUBSCRIBE` 命令的行为可以用下列的伪代码表示: - -```python -def SUBSCRIBE(client, channels): - # 遍历所有输入频道 - for channel in channels: - # 将客户端添加到链表的末尾 - redisServer.pubsub_channels[channel].append(client) -``` - -通过 `pubsub_channels` 字典,程序只要检查某个频道是否为字典的键,就可以知道该频道是否正在被客户端订阅;只要取出某个键的值,就可以得到所有订阅该频道的客户端的信息。 - -#### PUBLISH 命令 - -了解 `SUBSCRIBE`,那么 `PUBLISH` 命令的实现也变得十分简单了,只需要通过上述字典定位到具体的客户端,再把消息发送给它们就好了:*(伪代码实现如下)* - -```python -def PUBLISH(channel, message): - # 遍历所有订阅频道 channel 的客户端 - for client in server.pubsub_channels[channel]: - # 将信息发送给它们 - send_message(client, message) -``` - -#### UNSUBSCRIBE 命令 - -使用 `UNSUBSCRIBE` 命令可以退订指定的频道,这个命令执行的是订阅的反操作:它从 `pubsub_channels` 字典的给定频道(键)中,删除关于当前客户端的信息,这样被退订频道的信息就不会再发送给这个客户端。 - -### 订阅模式原理 - -![](https://upload-images.jianshu.io/upload_images/7896890-18ac258e4e9387da.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -正如我们上面说到了,当发送一条消息到 `wmyskxz.chat` 这个频道时,Redis 不仅仅会发送到当前的频道,还会发送到匹配于当前模式的所有频道,实际上,`pubsub_patterns` 背后还维护了一个 `redis.h/pubsubPattern` 结构: - -```c -typedef struct pubsubPattern { - redisClient *client; // 订阅模式的客户端 - robj *pattern; // 订阅的模式 -} pubsubPattern; -``` - -每当调用 `PSUBSCRIBE` 命令订阅一个模式时,程序就创建一个包含客户端信息和被订阅模式的 `pubsubPattern` 结构,并将该结构添加到 `redisServer.pubsub_patterns` 链表中。 - -我们来看一个 `pusub_patterns` 链表的示例: - -![](https://upload-images.jianshu.io/upload_images/7896890-d0d3b1849fdb6162.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -这个时候客户端 `client 3` 执行 `PSUBSCRIBE wmyskxz.java.*`,那么 `pubsub_patterns` 链表就会被更新成这样: - -![](https://upload-images.jianshu.io/upload_images/7896890-edbf11995590de50.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -通过遍历整个 `pubsub_patterns` 链表,程序可以检查所有正在被订阅的模式,以及订阅这些模式的客户端。 - -#### PUBLISH 命令 - -上面给出的伪代码并没有 **完整描述** `PUBLISH` 命令的行为,因为 `PUBLISH` 除了将 `message` 发送到 **所有订阅 `channel` 的客户端** 之外,它还会将 `channel` 和 `pubsub_patterns` 中的 **模式** 进行对比,如果 `channel` 和某个模式匹配的话,那么也将 `message` 发送到 **订阅那个模式的客户端**。 - -完整描述 `PUBLISH` 功能的伪代码定于如下: - -```python -def PUBLISH(channel, message): - # 遍历所有订阅频道 channel 的客户端 - for client in server.pubsub_channels[channel]: - # 将信息发送给它们 - send_message(client, message) - # 取出所有模式,以及订阅模式的客户端 - for pattern, client in server.pubsub_patterns: - # 如果 channel 和模式匹配 - if match(channel, pattern): - # 那么也将信息发给订阅这个模式的客户端 - send_message(client, message) -``` - -#### PUNSUBSCRIBE 命令 - -使用 `PUNSUBSCRIBE` 命令可以退订指定的模式,这个命令执行的是订阅模式的反操作:序会删除 `redisServer.pubsub_patterns` 链表中,所有和被退订模式相关联的 `pubsubPattern` 结构,这样客户端就不会再收到和模式相匹配的频道发来的信息。 - -## PubSub 的缺点 - -尽管 **Redis** 实现了 **PubSub** 模式来达到了 **多播消息队列** 的目的,但在实际的消息队列的领域,几乎 **找不到特别合适的场景**,因为它的缺点十分明显: - -- **没有 Ack 机制,也不保证数据的连续:** PubSub 的生产者传递过来一个消息,Redis 会直接找到相应的消费者传递过去。如果没有一个消费者,那么消息会被直接丢弃。如果开始有三个消费者,其中一个突然挂掉了,过了一会儿等它再重连时,那么重连期间的消息对于这个消费者来说就彻底丢失了。 -- **不持久化消息:** 如果 Redis 停机重启,PubSub 的消息是不会持久化的,毕竟 Redis 宕机就相当于一个消费者都没有,所有的消息都会被直接丢弃。 - - -基于上述缺点,Redis 的作者甚至单独开启了一个 Disque 的项目来专门用来做多播消息队列,不过该项目目前好像都没有成熟。不过后来在 2018 年 6 月,**Redis 5.0** 新增了 `Stream` 数据结构,这个功能给 Redis 带来了 **持久化消息队列**,从此 PubSub 作为消息队列的功能可以说是就消失了.. - -![image](https://upload-images.jianshu.io/upload_images/7896890-3a144fda1a0dafcb.gif?imageMogr2/auto-orient/strip) - -# 二、更为强大的 Stream | 持久化的发布/订阅系统 - -**Redis Stream** 从概念上来说,就像是一个 **仅追加内容** 的 **消息链表**,把所有加入的消息都一个一个串起来,每个消息都有一个唯一的 ID 和内容,这很简单,让它复杂的是从 Kafka 借鉴的另一种概念:**消费者组(Consumer Group)** *(思路一致,实现不同)*: - -![](https://upload-images.jianshu.io/upload_images/7896890-b9d8afde068a165f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -上图就展示了一个典型的 **Stream** 结构。每个 Stream 都有唯一的名称,它就是 Redis 的 `key`,在我们首次使用 `xadd` 指令追加消息时自动创建。我们对图中的一些概念做一下解释: - -- **Consumer Group**:消费者组,可以简单看成记录流状态的一种数据结构。消费者既可以选择使用 `XREAD` 命令进行 **独立消费**,也可以多个消费者同时加入一个消费者组进行 **组内消费**。同一个消费者组内的消费者共享所有的 Stream 信息,**同一条消息只会有一个消费者消费到**,这样就可以应用在分布式的应用场景中来保证消息的唯一性。 -- **last_delivered_id**:用来表示消费者组消费在 Stream 上 **消费位置** 的游标信息。每个消费者组都有一个 Stream 内 **唯一的名称**,消费者组不会自动创建,需要使用 `XGROUP CREATE` 指令来显式创建,并且需要指定从哪一个消息 ID 开始消费,用来初始化 `last_delivered_id` 这个变量。 -- **pending_ids**:每个消费者内部都有的一个状态变量,用来表示 **已经** 被客户端 **获取**,但是 **还没有 ack** 的消息。记录的目的是为了 **保证客户端至少消费了消息一次**,而不会在网络传输的中途丢失而没有对消息进行处理。如果客户端没有 ack,那么这个变量里面的消息 ID 就会越来越多,一旦某个消息被 ack,它就会对应开始减少。这个变量也被 Redis 官方称为 **PEL** *(Pending Entries List)*。 - - - -## 消息 ID 和消息内容 - -#### 消息 ID - -消息 ID 如果是由 `XADD` 命令返回自动创建的话,那么它的格式会像这样:`timestampInMillis-sequence` *(毫秒时间戳-序列号)*,例如 `1527846880585-5`,它表示当前的消息是在毫秒时间戳 `1527846880585` 时产生的,并且是该毫秒内产生的第 5 条消息。 - -这些 ID 的格式看起来有一些奇怪,**为什么要使用时间来当做 ID 的一部分呢?** 一方面,我们要 **满足 ID 自增** 的属性,另一方面,也是为了 **支持范围查找** 的功能。由于 ID 和生成消息的时间有关,这样就使得在根据时间范围内查找时基本上是没有额外损耗的。 - -当然消息 ID 也可以由客户端自定义,但是形式必须是 **"整数-整数"**,而且后面加入的消息的 ID 必须要大于前面的消息 ID。 - -#### 消息内容 - -消息内容就是普通的键值对,形如 hash 结构的键值对。 - -## 增删改查示例 - -增删改查命令很简单,详情如下: - -1. `xadd`:追加消息 -2. `xdel`:删除消息,这里的删除仅仅是设置了标志位,不影响消息总长度 -3. `xrange`:获取消息列表,会自动过滤已经删除的消息 -4. `xlen`:消息长度 -5. `del`:删除Stream - -使用示例: - -```bash -# *号表示服务器自动生成ID,后面顺序跟着一堆key/value -127.0.0.1:6379> xadd codehole * name laoqian age 30 # 名字叫laoqian,年龄30岁 -1527849609889-0 # 生成的消息ID -127.0.0.1:6379> xadd codehole * name xiaoyu age 29 -1527849629172-0 -127.0.0.1:6379> xadd codehole * name xiaoqian age 1 -1527849637634-0 -127.0.0.1:6379> xlen codehole -(integer) 3 -127.0.0.1:6379> xrange codehole - + # -表示最小值, +表示最大值 -1) 1) 1527849609889-0 - 2) 1) "name" - 2) "laoqian" - 3) "age" - 4) "30" -2) 1) 1527849629172-0 - 2) 1) "name" - 2) "xiaoyu" - 3) "age" - 4) "29" -3) 1) 1527849637634-0 - 2) 1) "name" - 2) "xiaoqian" - 3) "age" - 4) "1" -127.0.0.1:6379> xrange codehole 1527849629172-0 + # 指定最小消息ID的列表 -1) 1) 1527849629172-0 - 2) 1) "name" - 2) "xiaoyu" - 3) "age" - 4) "29" -2) 1) 1527849637634-0 - 2) 1) "name" - 2) "xiaoqian" - 3) "age" - 4) "1" -127.0.0.1:6379> xrange codehole - 1527849629172-0 # 指定最大消息ID的列表 -1) 1) 1527849609889-0 - 2) 1) "name" - 2) "laoqian" - 3) "age" - 4) "30" -2) 1) 1527849629172-0 - 2) 1) "name" - 2) "xiaoyu" - 3) "age" - 4) "29" -127.0.0.1:6379> xdel codehole 1527849609889-0 -(integer) 1 -127.0.0.1:6379> xlen codehole # 长度不受影响 -(integer) 3 -127.0.0.1:6379> xrange codehole - + # 被删除的消息没了 -1) 1) 1527849629172-0 - 2) 1) "name" - 2) "xiaoyu" - 3) "age" - 4) "29" -2) 1) 1527849637634-0 - 2) 1) "name" - 2) "xiaoqian" - 3) "age" - 4) "1" -127.0.0.1:6379> del codehole # 删除整个Stream -(integer) 1 -``` - -## 独立消费示例 - -我们可以在不定义消费组的情况下进行 Stream 消息的 **独立消费**,当 Stream 没有新消息时,甚至可以阻塞等待。Redis 设计了一个单独的消费指令 `xread`,可以将 Stream 当成普通的消息队列(list)来使用。使用 `xread` 时,我们可以完全忽略 **消费组(Consumer Group)** 的存在,就好比 Stream 就是一个普通的列表(list): - -```bash -# 从Stream头部读取两条消息 -127.0.0.1:6379> xread count 2 streams codehole 0-0 -1) 1) "codehole" - 2) 1) 1) 1527851486781-0 - 2) 1) "name" - 2) "laoqian" - 3) "age" - 4) "30" - 2) 1) 1527851493405-0 - 2) 1) "name" - 2) "yurui" - 3) "age" - 4) "29" -# 从Stream尾部读取一条消息,毫无疑问,这里不会返回任何消息 -127.0.0.1:6379> xread count 1 streams codehole $ -(nil) -# 从尾部阻塞等待新消息到来,下面的指令会堵住,直到新消息到来 -127.0.0.1:6379> xread block 0 count 1 streams codehole $ -# 我们从新打开一个窗口,在这个窗口往Stream里塞消息 -127.0.0.1:6379> xadd codehole * name youming age 60 -1527852774092-0 -# 再切换到前面的窗口,我们可以看到阻塞解除了,返回了新的消息内容 -# 而且还显示了一个等待时间,这里我们等待了93s -127.0.0.1:6379> xread block 0 count 1 streams codehole $ -1) 1) "codehole" - 2) 1) 1) 1527852774092-0 - 2) 1) "name" - 2) "youming" - 3) "age" - 4) "60" -(93.11s) -``` - -客户端如果想要使用 `xread` 进行 **顺序消费**,一定要 **记住当前消费** 到哪里了,也就是返回的消息 ID。下次继续调用 `xread` 时,将上次返回的最后一个消息 ID 作为参数传递进去,就可以继续消费后续的消息。 - -`block 0` 表示永远阻塞,直到消息到来,`block 1000` 表示阻塞 `1s`,如果 `1s` 内没有任何消息到来,就返回 `nil`: - -```bash -127.0.0.1:6379> xread block 1000 count 1 streams codehole $ -(nil) -(1.07s) -``` - -## 创建消费者示例 - -Stream 通过 `xgroup create` 指令创建消费组(Consumer Group),需要传递起始消息 ID 参数用来初始化 `last_delivered_id` 变量: - -```bash -127.0.0.1:6379> xgroup create codehole cg1 0-0 # 表示从头开始消费 -OK -# $表示从尾部开始消费,只接受新消息,当前Stream消息会全部忽略 -127.0.0.1:6379> xgroup create codehole cg2 $ -OK -127.0.0.1:6379> xinfo codehole # 获取Stream信息 - 1) length - 2) (integer) 3 # 共3个消息 - 3) radix-tree-keys - 4) (integer) 1 - 5) radix-tree-nodes - 6) (integer) 2 - 7) groups - 8) (integer) 2 # 两个消费组 - 9) first-entry # 第一个消息 -10) 1) 1527851486781-0 - 2) 1) "name" - 2) "laoqian" - 3) "age" - 4) "30" -11) last-entry # 最后一个消息 -12) 1) 1527851498956-0 - 2) 1) "name" - 2) "xiaoqian" - 3) "age" - 4) "1" -127.0.0.1:6379> xinfo groups codehole # 获取Stream的消费组信息 -1) 1) name - 2) "cg1" - 3) consumers - 4) (integer) 0 # 该消费组还没有消费者 - 5) pending - 6) (integer) 0 # 该消费组没有正在处理的消息 -2) 1) name - 2) "cg2" - 3) consumers # 该消费组还没有消费者 - 4) (integer) 0 - 5) pending - 6) (integer) 0 # 该消费组没有正在处理的消息 -``` - -## 组内消费示例 - -Stream 提供了 `xreadgroup` 指令可以进行消费组的组内消费,需要提供 **消费组名称、消费者名称和起始消息 ID**。它同 `xread` 一样,也可以阻塞等待新消息。读到新消息后,对应的消息 ID 就会进入消费者的 **PEL** *(正在处理的消息)* 结构里,客户端处理完毕后使用 `xack` 指令 **通知服务器**,本条消息已经处理完毕,该消息 ID 就会从 **PEL** 中移除,下面是示例: - -```bash -# >号表示从当前消费组的last_delivered_id后面开始读 -# 每当消费者读取一条消息,last_delivered_id变量就会前进 -127.0.0.1:6379> xreadgroup GROUP cg1 c1 count 1 streams codehole > -1) 1) "codehole" - 2) 1) 1) 1527851486781-0 - 2) 1) "name" - 2) "laoqian" - 3) "age" - 4) "30" -127.0.0.1:6379> xreadgroup GROUP cg1 c1 count 1 streams codehole > -1) 1) "codehole" - 2) 1) 1) 1527851493405-0 - 2) 1) "name" - 2) "yurui" - 3) "age" - 4) "29" -127.0.0.1:6379> xreadgroup GROUP cg1 c1 count 2 streams codehole > -1) 1) "codehole" - 2) 1) 1) 1527851498956-0 - 2) 1) "name" - 2) "xiaoqian" - 3) "age" - 4) "1" - 2) 1) 1527852774092-0 - 2) 1) "name" - 2) "youming" - 3) "age" - 4) "60" -# 再继续读取,就没有新消息了 -127.0.0.1:6379> xreadgroup GROUP cg1 c1 count 1 streams codehole > -(nil) -# 那就阻塞等待吧 -127.0.0.1:6379> xreadgroup GROUP cg1 c1 block 0 count 1 streams codehole > -# 开启另一个窗口,往里塞消息 -127.0.0.1:6379> xadd codehole * name lanying age 61 -1527854062442-0 -# 回到前一个窗口,发现阻塞解除,收到新消息了 -127.0.0.1:6379> xreadgroup GROUP cg1 c1 block 0 count 1 streams codehole > -1) 1) "codehole" - 2) 1) 1) 1527854062442-0 - 2) 1) "name" - 2) "lanying" - 3) "age" - 4) "61" -(36.54s) -127.0.0.1:6379> xinfo groups codehole # 观察消费组信息 -1) 1) name - 2) "cg1" - 3) consumers - 4) (integer) 1 # 一个消费者 - 5) pending - 6) (integer) 5 # 共5条正在处理的信息还有没有ack -2) 1) name - 2) "cg2" - 3) consumers - 4) (integer) 0 # 消费组cg2没有任何变化,因为前面我们一直在操纵cg1 - 5) pending - 6) (integer) 0 -# 如果同一个消费组有多个消费者,我们可以通过xinfo consumers指令观察每个消费者的状态 -127.0.0.1:6379> xinfo consumers codehole cg1 # 目前还有1个消费者 -1) 1) name - 2) "c1" - 3) pending - 4) (integer) 5 # 共5条待处理消息 - 5) idle - 6) (integer) 418715 # 空闲了多长时间ms没有读取消息了 -# 接下来我们ack一条消息 -127.0.0.1:6379> xack codehole cg1 1527851486781-0 -(integer) 1 -127.0.0.1:6379> xinfo consumers codehole cg1 -1) 1) name - 2) "c1" - 3) pending - 4) (integer) 4 # 变成了5条 - 5) idle - 6) (integer) 668504 -# 下面ack所有消息 -127.0.0.1:6379> xack codehole cg1 1527851493405-0 1527851498956-0 1527852774092-0 1527854062442-0 -(integer) 4 -127.0.0.1:6379> xinfo consumers codehole cg1 -1) 1) name - 2) "c1" - 3) pending - 4) (integer) 0 # pel空了 - 5) idle - 6) (integer) 745505 -``` - -## QA 1:Stream 消息太多怎么办? | Stream 的上限 - -很容易想到,要是消息积累太多,Stream 的链表岂不是很长,内容会不会爆掉就是个问题了。`xdel` 指令又不会删除消息,它只是给消息做了个标志位。 - -Redis 自然考虑到了这一点,所以它提供了一个定长 Stream 功能。在 `xadd` 的指令提供一个定长长度 `maxlen`,就可以将老的消息干掉,确保最多不超过指定长度,使用起来也很简单: - -```bash -> XADD mystream MAXLEN 2 * value 1 -1526654998691-0 -> XADD mystream MAXLEN 2 * value 2 -1526654999635-0 -> XADD mystream MAXLEN 2 * value 3 -1526655000369-0 -> XLEN mystream -(integer) 2 -> XRANGE mystream - + -1) 1) 1526654999635-0 - 2) 1) "value" - 2) "2" -2) 1) 1526655000369-0 - 2) 1) "value" - 2) "3" -``` - -如果使用 `MAXLEN` 选项,当 Stream 的达到指定长度后,老的消息会自动被淘汰掉,因此 Stream 的大小是恒定的。目前还没有选项让 Stream 只保留给定数量的条目,因为为了一致地运行,这样的命令必须在很长一段时间内阻塞以淘汰消息。*(例如在添加数据的高峰期间,你不得不长暂停来淘汰旧消息和添加新的消息)* - -另外使用 `MAXLEN` 选项的花销是很大的,Stream 为了节省内存空间,采用了一种特殊的结构表示,而这种结构的调整是需要额外的花销的。所以我们可以使用一种带有 `~` 的特殊命令: - -```bash -XADD mystream MAXLEN ~ 1000 * ... entry fields here ... -``` - -它会基于当前的结构合理地对节点执行裁剪,来保证至少会有 `1000` 条数据,可能是 `1010` 也可能是 `1030`。 - -## QA 2:PEL 是如何避免消息丢失的? - - -在客户端消费者读取 Stream 消息时,Redis 服务器将消息回复给客户端的过程中,客户端突然断开了连接,消息就丢失了。但是 PEL 里已经保存了发出去的消息 ID,待客户端重新连上之后,可以再次收到 PEL 中的消息 ID 列表。不过此时 `xreadgroup` 的起始消息 ID 不能为参数 `>` ,而必须是任意有效的消息 ID,一般将参数设为 `0-0`,表示读取所有的 PEL 消息以及自 `last_delivered_id` 之后的新消息。 - - -## Redis Stream Vs Kafka - -Redis 基于内存存储,这意味着它会比基于磁盘的 Kafka 快上一些,也意味着使用 Redis 我们 **不能长时间存储大量数据**。不过如果您想以 **最小延迟** 实时处理消息的话,您可以考虑 Redis,但是如果 **消息很大并且应该重用数据** 的话,则应该首先考虑使用 Kafka。 - -另外从某些角度来说,`Redis Stream` 也更适用于小型、廉价的应用程序,因为 `Kafka` 相对来说更难配置一些。 - - -# 相关阅读 - -1. Redis(1)——5种基本数据结构 - [https://www.wmyskxz.com/2020/02/28/redis-1-5-chong-ji-ben-shu-ju-jie-gou/](https://www.wmyskxz.com/2020/02/28/redis-1-5-chong-ji-ben-shu-ju-jie-gou/) -2. Redis(2)——跳跃表 - [https://www.wmyskxz.com/2020/02/29/redis-2-tiao-yue-biao/](https://www.wmyskxz.com/2020/02/29/redis-2-tiao-yue-biao/) -3. Redis(3)——分布式锁深入探究 - [https://www.wmyskxz.com/2020/03/01/redis-3/](https://www.wmyskxz.com/2020/03/01/redis-3/) -4. Reids(4)——神奇的HyperLoglog解决统计问题 - [https://www.wmyskxz.com/2020/03/02/reids-4-shen-qi-de-hyperloglog-jie-jue-tong-ji-wen-ti/](https://www.wmyskxz.com/2020/03/02/reids-4-shen-qi-de-hyperloglog-jie-jue-tong-ji-wen-ti/) -5. Redis(5)——亿级数据过滤和布隆过滤器 - [https://www.wmyskxz.com/2020/03/11/redis-5-yi-ji-shu-ju-guo-lu-he-bu-long-guo-lu-qi/](https://www.wmyskxz.com/2020/03/11/redis-5-yi-ji-shu-ju-guo-lu-he-bu-long-guo-lu-qi/) -6. Redis(6)——GeoHash查找附近的人[https://www.wmyskxz.com/2020/03/12/redis-6-geohash-cha-zhao-fu-jin-de-ren/](https://www.wmyskxz.com/2020/03/12/redis-6-geohash-cha-zhao-fu-jin-de-ren/) -7. Redis(7)——持久化【一文了解】 - [https://www.wmyskxz.com/2020/03/13/redis-7-chi-jiu-hua-yi-wen-liao-jie/](https://www.wmyskxz.com/2020/03/13/redis-7-chi-jiu-hua-yi-wen-liao-jie/) - - -# 参考资料 - -1. 订阅与发布——Redis 设计与实现 - [https://redisbook.readthedocs.io/en/latest/feature/pubsub.html](https://redisbook.readthedocs.io/en/latest/feature/pubsub.html) -2. 《Redis 深度历险》 - 钱文品/ 著 - [https://book.douban.com/subject/30386804/](https://book.douban.com/subject/30386804/) -3. Introduction to Redis Streams【官方文档】 - [https://redis.io/topics/streams-intro](https://redis.io/topics/streams-intro) -4. Kafka vs. Redis: Log Aggregation Capabilities and Performance - [https://logz.io/blog/kafka-vs-redis/](https://logz.io/blog/kafka-vs-redis/) diff --git "a/docs/database/Redis/redis-collection/Redis(9)\342\200\224\342\200\224\351\233\206\347\276\244\345\205\245\351\227\250\345\256\236\350\267\265\346\225\231\347\250\213.md" "b/docs/database/Redis/redis-collection/Redis(9)\342\200\224\342\200\224\351\233\206\347\276\244\345\205\245\351\227\250\345\256\236\350\267\265\346\225\231\347\250\213.md" deleted file mode 100644 index b90f00793ae..00000000000 --- "a/docs/database/Redis/redis-collection/Redis(9)\342\200\224\342\200\224\351\233\206\347\276\244\345\205\245\351\227\250\345\256\236\350\267\265\346\225\231\347\250\213.md" +++ /dev/null @@ -1,648 +0,0 @@ -> 授权转载自: https://github.com/wmyskxz/MoreThanJava#part3-redis - -![](https://upload-images.jianshu.io/upload_images/7896890-80c61b0ae541a750.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -# 一、Redis 集群概述 - -#### Redis 主从复制 - -到 [目前](#相关阅读) 为止,我们所学习的 Redis 都是 **单机版** 的,这也就意味着一旦我们所依赖的 Redis 服务宕机了,我们的主流程也会受到一定的影响,这当然是我们不能够接受的。 - -所以一开始我们的想法是:搞一台备用机。这样我们就可以在一台服务器出现问题的时候切换动态地到另一台去: - -![](https://upload-images.jianshu.io/upload_images/7896890-c48d255bc0b13672.gif?imageMogr2/auto-orient/strip) - -幸运的是,两个节点数据的同步我们可以使用 Redis 的 **主从同步** 功能帮助到我们,这样一来,有个备份,心里就踏实多了。 - -![](https://upload-images.jianshu.io/upload_images/7896890-4a32b9efa3885655.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -#### Redis 哨兵 - -后来因为某种神秘力量,Redis 老会在莫名其妙的时间点出问题 *(比如半夜 2 点)*,我总不能 24 小时时刻守在电脑旁边切换节点吧,于是另一个想法又开始了:给所有的节点找一个 **"管家"**,自动帮我监听照顾节点的状态并切换: - -![](https://upload-images.jianshu.io/upload_images/7896890-de8d9ce9e77bf211.gif?imageMogr2/auto-orient/strip) - -这大概就是 **Redis 哨兵** *(Sentinel)* 的简单理解啦。什么?管家宕机了怎么办?相较于有大量请求的 Redis 服务来说,管家宕机的概率就要小得多啦.. 如果真的宕机了,我们也可以直接切换成当前可用的节点保证可用.. - -![](https://upload-images.jianshu.io/upload_images/7896890-c7657fb8140d7cc6.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -#### Redis 集群化 - -好了,通过上面的一些解决方案我们对 Redis 的 **稳定性** 稍微有了一些底气了,但单台节点的计算能力始终有限,所谓人多力量大,如果我们把 **多个节点组合** 成 **一个可用的工作节点**,那就大大增加了 Redis 的 **高可用、可扩展、分布式、容错** 等特性: - -![](https://upload-images.jianshu.io/upload_images/7896890-8957aa6d1484c5de.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -# 二、主从复制 - -![](https://upload-images.jianshu.io/upload_images/7896890-4956a718c124a81f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -**主从复制**,是指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器。前者称为 **主节点(master)**,后者称为 **从节点(slave)**。且数据的复制是 **单向** 的,只能由主节点到从节点。Redis 主从复制支持 **主从同步** 和 **从从同步** 两种,后者是 Redis 后续版本新增的功能,以减轻主节点的同步负担。 - -#### 主从复制主要的作用 - -- **数据冗余:** 主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。 -- **故障恢复:** 当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复 *(实际上是一种服务的冗余)*。 -- **负载均衡:** 在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务 *(即写 Redis 数据时应用连接主节点,读 Redis 数据时应用连接从节点)*,分担服务器负载。尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高 Redis 服务器的并发量。 -- **高可用基石:** 除了上述作用以外,主从复制还是哨兵和集群能够实施的 **基础**,因此说主从复制是 Redis 高可用的基础。 - -## 快速体验 - -在 **Redis** 中,用户可以通过执行 `SLAVEOF` 命令或者设置 `slaveof` 选项,让一个服务器去复制另一个服务器,以下三种方式是 **完全等效** 的: - -- **配置文件**:在从服务器的配置文件中加入:`slaveof ` -- **启动命令**:redis-server 启动命令后加入 `--slaveof ` -- **客户端命令**:Redis 服务器启动后,直接通过客户端执行命令:`slaveof `,让该 Redis 实例成为从节点。 - -需要注意的是:**主从复制的开启,完全是在从节点发起的,不需要我们在主节点做任何事情。** - -#### 第一步:本地启动两个节点 - -在正确安装好 Redis 之后,我们可以使用 `redis-server --port ` 的方式指定创建两个不同端口的 Redis 实例,例如,下方我分别创建了一个 `6379` 和 `6380` 的两个 Redis 实例: - -```bash -# 创建一个端口为 6379 的 Redis 实例 -redis-server --port 6379 -# 创建一个端口为 6380 的 Redis 实例 -redis-server --port 6380 -``` - -此时两个 Redis 节点启动后,都默认为 **主节点**。 - -#### 第二步:建立复制 - -我们在 `6380` 端口的节点中执行 `slaveof` 命令,使之变为从节点: - -```bash -# 在 6380 端口的 Redis 实例中使用控制台 -redis-cli -p 6380 -# 成为本地 6379 端口实例的从节点 -127.0.0.1:6380> SLAVEOF 127.0.0.1ø 6379 -OK -``` - -#### 第三步:观察效果 - -下面我们来验证一下,主节点的数据是否会复制到从节点之中: - -- 先在 **从节点** 中查询一个 **不存在** 的 key: -```bash -127.0.0.1:6380> GET mykey -(nil) -``` -- 再在 **主节点** 中添加这个 key: -```bash -127.0.0.1:6379> SET mykey myvalue -OK -``` -- 此时再从 **从节点** 中查询,会发现已经从 **主节点** 同步到 **从节点**: -```bash -127.0.0.1:6380> GET mykey -"myvalue" -``` - -#### 第四步:断开复制 - -通过 `slaveof ` 命令建立主从复制关系以后,可以通过 `slaveof no one` 断开。需要注意的是,从节点断开复制后,**不会删除已有的数据**,只是不再接受主节点新的数据变化。 - -从节点执行 `slaveof no one` 之后,从节点和主节点分别打印日志如下:、 - -```bash -# 从节点打印日志 -61496:M 17 Mar 2020 08:10:22.749 # Connection with master lost. -61496:M 17 Mar 2020 08:10:22.749 * Caching the disconnected master state. -61496:M 17 Mar 2020 08:10:22.749 * Discarding previously cached master state. -61496:M 17 Mar 2020 08:10:22.749 * MASTER MODE enabled (user request from 'id=4 addr=127.0.0.1:55096 fd=8 name= age=1664 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=34 qbuf-free=32734 obl=0 oll=0 omem=0 events=r cmd=slaveof') - -# 主节点打印日志 -61467:M 17 Mar 2020 08:10:22.749 # Connection with replica 127.0.0.1:6380 lost. -``` - -## 实现原理简析 - -![](https://upload-images.jianshu.io/upload_images/7896890-c97a6bcc0936cd17.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -为了节省篇幅,我把主要的步骤都 **浓缩** 在了上图中,其实也可以 **简化成三个阶段:准备阶段-数据同步阶段-命令传播阶段**。下面我们来进行一些必要的说明。 - -#### 身份验证 | 主从复制安全问题 - -在上面的 **快速体验** 过程中,你会发现 `slaveof` 这个命令居然不需要验证?这意味着只要知道了 ip 和端口就可以随意拷贝服务器上的数据了? - -![](https://upload-images.jianshu.io/upload_images/7896890-d0c7a74da972fca3.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -那当然不能够了,我们可以通过在 **主节点** 配置 `requirepass` 来设置密码,这样就必须在 **从节点** 中对应配置好 `masterauth` 参数 *(与主节点 `requirepass` 保持一致)* 才能够进行正常复制了。 - -#### SYNC 命令是一个非常耗费资源的操作 - -每次执行 `SYNC` 命令,主从服务器需要执行如下动作: - -1. **主服务器** 需要执行 `BGSAVE` 命令来生成 RDB 文件,这个生成操作会 **消耗** 主服务器大量的 **CPU、内存和磁盘 I/O 的资源**; -2. **主服务器** 需要将自己生成的 RDB 文件 发送给从服务器,这个发送操作会 **消耗** 主服务器 **大量的网络资源** *(带宽和流量)*,并对主服务器响应命令请求的时间产生影响; -3. 接收到 RDB 文件的 **从服务器** 需要载入主服务器发来的 RBD 文件,并且在载入期间,从服务器 **会因为阻塞而没办法处理命令请求**; - -特别是当出现 **断线重复制** 的情况是时,为了让从服务器补足断线时确实的那一小部分数据,却要执行一次如此耗资源的 `SYNC` 命令,显然是不合理的。 - -#### PSYNC 命令的引入 - -所以在 **Redis 2.8** 中引入了 `PSYNC` 命令来代替 `SYNC`,它具有两种模式: - -1. **全量复制:** 用于初次复制或其他无法进行部分复制的情况,将主节点中的所有数据都发送给从节点,是一个非常重型的操作; -2. **部分复制:** 用于网络中断等情况后的复制,只将 **中断期间主节点执行的写命令** 发送给从节点,与全量复制相比更加高效。**需要注意** 的是,如果网络中断时间过长,导致主节点没有能够完整地保存中断期间执行的写命令,则无法进行部分复制,仍使用全量复制; - - -部分复制的原理主要是靠主从节点分别维护一个 **复制偏移量**,有了这个偏移量之后断线重连之后一比较,之后就可以仅仅把从服务器断线之后确实的这部分数据给补回来了。 - -> 更多的详细内容可以参考下方 *参考资料 3* - -# 三、Redis Sentinel 哨兵 - -![](https://upload-images.jianshu.io/upload_images/7896890-884d5be9a2ddfebc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -*上图* 展示了一个典型的哨兵架构图,它由两部分组成,哨兵节点和数据节点: - -- **哨兵节点:** 哨兵系统由一个或多个哨兵节点组成,哨兵节点是特殊的 Redis 节点,不存储数据; -- **数据节点:** 主节点和从节点都是数据节点; - -在复制的基础上,哨兵实现了 **自动化的故障恢复** 功能,下方是官方对于哨兵功能的描述: - -- **监控(Monitoring):** 哨兵会不断地检查主节点和从节点是否运作正常。 -- **自动故障转移(Automatic failover):** 当 **主节点** 不能正常工作时,哨兵会开始 **自动故障转移操作**,它会将失效主节点的其中一个 **从节点升级为新的主节点**,并让其他从节点改为复制新的主节点。 -- **配置提供者(Configuration provider):** 客户端在初始化时,通过连接哨兵来获得当前 Redis 服务的主节点地址。 -- **通知(Notification):** 哨兵可以将故障转移的结果发送给客户端。 - -其中,监控和自动故障转移功能,使得哨兵可以及时发现主节点故障并完成转移。而配置提供者和通知功能,则需要在与客户端的交互中才能体现。 - -## 快速体验 - -#### 第一步:创建主从节点配置文件并启动 - -正确安装好 Redis 之后,我们去到 Redis 的安装目录 *(mac 默认在 `/usr/local/`)*,找到 `redis.conf` 文件复制三份分别命名为 `redis-master.conf`/`redis-slave1.conf`/`redis-slave2.conf`,分别作为 `1` 个主节点和 `2` 个从节点的配置文件 *(下图演示了我本机的 `redis.conf` 文件的位置)* - -![](https://upload-images.jianshu.io/upload_images/7896890-34de77bfca56d32e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -打开可以看到这个 `.conf` 后缀的文件里面有很多说明的内容,全部删除然后分别改成下面的样子: - -```bash -#redis-master.conf -port 6379 -daemonize yes -logfile "6379.log" -dbfilename "dump-6379.rdb" - -#redis-slave1.conf -port 6380 -daemonize yes -logfile "6380.log" -dbfilename "dump-6380.rdb" -slaveof 127.0.0.1 6379 - -#redis-slave2.conf -port 6381 -daemonize yes -logfile "6381.log" -dbfilename "dump-6381.rdb" -slaveof 127.0.0.1 6379 -``` - -然后我们可以执行 `redis-server ` 来根据配置文件启动不同的 Redis 实例,依次启动主从节点: - -```bash -redis-server /usr/local/redis-5.0.3/redis-master.conf -redis-server /usr/local/redis-5.0.3/redis-slave1.conf -redis-server /usr/local/redis-5.0.3/redis-slave2.conf -``` - -节点启动后,我们执行 `redis-cli` 默认连接到我们端口为 `6379` 的主节点执行 `info Replication` 检查一下主从状态是否正常:*(可以看到下方正确地显示了两个从节点)* - -![](https://upload-images.jianshu.io/upload_images/7896890-a1c935f094240cac.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -#### 第二步:创建哨兵节点配置文件并启动 - -按照上面同样的方法,我们给哨兵节点也创建三个配置文件。*(哨兵节点本质上是特殊的 Redis 节点,所以配置几乎没什么差别,只是在端口上做区分就好)* - -```bash -# redis-sentinel-1.conf -port 26379 -daemonize yes -logfile "26379.log" -sentinel monitor mymaster 127.0.0.1 6379 2 - -# redis-sentinel-2.conf -port 26380 -daemonize yes -logfile "26380.log" -sentinel monitor mymaster 127.0.0.1 6379 2 - -# redis-sentinel-3.conf -port 26381 -daemonize yes -logfile "26381.log" -sentinel monitor mymaster 127.0.0.1 6379 2 -``` - -其中,`sentinel monitor mymaster 127.0.0.1 6379 2` 配置的含义是:该哨兵节点监控 `127.0.0.1:6379` 这个主节点,该主节点的名称是 `mymaster`,最后的 `2` 的含义与主节点的故障判定有关:至少需要 `2` 个哨兵节点同意,才能判定主节点故障并进行故障转移。 - -执行下方命令将哨兵节点启动起来: - -```bash -redis-server /usr/local/redis-5.0.3/redis-sentinel-1.conf --sentinel -redis-server /usr/local/redis-5.0.3/redis-sentinel-2.conf --sentinel -redis-server /usr/local/redis-5.0.3/redis-sentinel-3.conf --sentinel -``` - -使用 `redis-cil` 工具连接哨兵节点,并执行 `info Sentinel` 命令来查看是否已经在监视主节点了: - -```bash -# 连接端口为 26379 的 Redis 节点 -➜ ~ redis-cli -p 26379 -127.0.0.1:26379> info Sentinel -# Sentinel -sentinel_masters:1 -sentinel_tilt:0 -sentinel_running_scripts:0 -sentinel_scripts_queue_length:0 -sentinel_simulate_failure_flags:0 -master0:name=mymaster,status=ok,address=127.0.0.1:6379,slaves=2,sentinels=3 -``` - -此时你打开刚才写好的哨兵配置文件,你还会发现出现了一些变化: - -#### 第三步:演示故障转移 - -首先,我们使用 `kill -9` 命令来杀掉主节点,**同时** 在哨兵节点中执行 `info Sentinel` 命令来观察故障节点的过程: - -```bash -➜ ~ ps aux | grep 6379 -longtao 74529 0.3 0.0 4346936 2132 ?? Ss 10:30上午 0:03.09 redis-server *:26379 [sentinel] -longtao 73541 0.2 0.0 4348072 2292 ?? Ss 10:18上午 0:04.79 redis-server *:6379 -longtao 75521 0.0 0.0 4286728 728 s008 S+ 10:39上午 0:00.00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn 6379 -longtao 74836 0.0 0.0 4289844 944 s006 S+ 10:32上午 0:00.01 redis-cli -p 26379 -➜ ~ kill -9 73541 -``` - -如果 **刚杀掉瞬间** 在哨兵节点中执行 `info` 命令来查看,会发现主节点还没有切换过来,因为哨兵发现主节点故障并转移需要一段时间: - -```bash -# 第一时间查看哨兵节点发现并未转移,还在 6379 端口 -127.0.0.1:26379> info Sentinel -# Sentinel -sentinel_masters:1 -sentinel_tilt:0 -sentinel_running_scripts:0 -sentinel_scripts_queue_length:0 -sentinel_simulate_failure_flags:0 -master0:name=mymaster,status=ok,address=127.0.0.1:6379,slaves=2,sentinels=3 -``` - -一段时间之后你再执行 `info` 命令,查看,你就会发现主节点已经切换成了 `6381` 端口的从节点: - -```bash -# 过一段时间之后在执行,发现已经切换了 6381 端口 -127.0.0.1:26379> info Sentinel -# Sentinel -sentinel_masters:1 -sentinel_tilt:0 -sentinel_running_scripts:0 -sentinel_scripts_queue_length:0 -sentinel_simulate_failure_flags:0 -master0:name=mymaster,status=ok,address=127.0.0.1:6381,slaves=2,sentinels=3 -``` - -但同时还可以发现,**哨兵节点认为新的主节点仍然有两个从节点** *(上方 slaves=2)*,这是因为哨兵在将 `6381` 切换成主节点的同时,将 `6379` 节点置为其从节点。虽然 `6379` 从节点已经挂掉,但是由于 **哨兵并不会对从节点进行客观下线**,因此认为该从节点一直存在。当 `6379` 节点重新启动后,会自动变成 `6381` 节点的从节点。 - -另外,在故障转移的阶段,哨兵和主从节点的配置文件都会被改写: - -- **对于主从节点:** 主要是 `slaveof` 配置的变化,新的主节点没有了 `slaveof` 配置,其从节点则 `slaveof` 新的主节点。 -- **对于哨兵节点:** 除了主从节点信息的变化,纪元(epoch) *(记录当前集群状态的参数)* 也会变化,纪元相关的参数都 +1 了。 - -## 客户端访问哨兵系统代码演示 - -上面我们在 *快速体验* 中主要感受到了服务端自己对于当前主从节点的自动化治理,下面我们以 Java 代码为例,来演示一下客户端如何访问我们的哨兵系统: - -```java -public static void testSentinel() throws Exception { - String masterName = "mymaster"; - Set sentinels = new HashSet<>(); - sentinels.add("127.0.0.1:26379"); - sentinels.add("127.0.0.1:26380"); - sentinels.add("127.0.0.1:26381"); - - // 初始化过程做了很多工作 - JedisSentinelPool pool = new JedisSentinelPool(masterName, sentinels); - Jedis jedis = pool.getResource(); - jedis.set("key1", "value1"); - pool.close(); -} -``` - -#### 客户端原理 - -Jedis 客户端对哨兵提供了很好的支持。如上述代码所示,我们只需要向 Jedis 提供哨兵节点集合和 `masterName` ,构造 `JedisSentinelPool` 对象,然后便可以像使用普通 Redis 连接池一样来使用了:通过 `pool.getResource()` 获取连接,执行具体的命令。 - -在整个过程中,我们的代码不需要显式的指定主节点的地址,就可以连接到主节点;代码中对故障转移没有任何体现,就可以在哨兵完成故障转移后自动的切换主节点。之所以可以做到这一点,是因为在 `JedisSentinelPool` 的构造器中,进行了相关的工作;主要包括以下两点: - -1. **遍历哨兵节点,获取主节点信息:** 遍历哨兵节点,通过其中一个哨兵节点 + `masterName` 获得主节点的信息;该功能是通过调用哨兵节点的 `sentinel get-master-addr-by-name` 命令实现; -2. **增加对哨兵的监听:** 这样当发生故障转移时,客户端便可以收到哨兵的通知,从而完成主节点的切换。具体做法是:利用 Redis 提供的 **发布订阅** 功能,为每一个哨兵节点开启一个单独的线程,订阅哨兵节点的 + switch-master 频道,当收到消息时,重新初始化连接池。 - -## 新的主服务器是怎样被挑选出来的? - -**故障转移操作的第一步** 要做的就是在已下线主服务器属下的所有从服务器中,挑选出一个状态良好、数据完整的从服务器,然后向这个从服务器发送 `slaveof no one` 命令,将这个从服务器转换为主服务器。但是这个从服务器是怎么样被挑选出来的呢? - -![](https://upload-images.jianshu.io/upload_images/7896890-02dfea57f44fc27e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -简单来说 Sentinel 使用以下规则来选择新的主服务器: - -1. 在失效主服务器属下的从服务器当中, 那些被标记为主观下线、已断线、或者最后一次回复 PING 命令的时间大于五秒钟的从服务器都会被 **淘汰**。 -2. 在失效主服务器属下的从服务器当中, 那些与失效主服务器连接断开的时长超过 down-after 选项指定的时长十倍的从服务器都会被 **淘汰**。 -3. 在 **经历了以上两轮淘汰之后** 剩下来的从服务器中, 我们选出 **复制偏移量(replication offset)最大** 的那个 **从服务器** 作为新的主服务器;如果复制偏移量不可用,或者从服务器的复制偏移量相同,那么 **带有最小运行 ID** 的那个从服务器成为新的主服务器。 - -# 四、Redis 集群 - -![](https://upload-images.jianshu.io/upload_images/7896890-516eb4a9465451a6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -*上图* 展示了 **Redis Cluster** 典型的架构图,集群中的每一个 Redis 节点都 **互相两两相连**,客户端任意 **直连** 到集群中的 **任意一台**,就可以对其他 Redis 节点进行 **读写** 的操作。 - -#### 基本原理 - -![](https://upload-images.jianshu.io/upload_images/7896890-f65c71ca6811c634.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -Redis 集群中内置了 `16384` 个哈希槽。当客户端连接到 Redis 集群之后,会同时得到一份关于这个 **集群的配置信息**,当客户端具体对某一个 `key` 值进行操作时,会计算出它的一个 Hash 值,然后把结果对 `16384` **求余数**,这样每个 `key` 都会对应一个编号在 `0-16383` 之间的哈希槽,Redis 会根据节点数量 **大致均等** 的将哈希槽映射到不同的节点。 - -再结合集群的配置信息就能够知道这个 `key` 值应该存储在哪一个具体的 Redis 节点中,如果不属于自己管,那么就会使用一个特殊的 `MOVED` 命令来进行一个跳转,告诉客户端去连接这个节点以获取数据: - -```bash -GET x --MOVED 3999 127.0.0.1:6381 -``` - -`MOVED` 指令第一个参数 `3999` 是 `key` 对应的槽位编号,后面是目标节点地址,`MOVED` 命令前面有一个减号,表示这是一个错误的消息。客户端在收到 `MOVED` 指令后,就立即纠正本地的 **槽位映射表**,那么下一次再访问 `key` 时就能够到正确的地方去获取了。 - -#### 集群的主要作用 - -1. **数据分区:** 数据分区 *(或称数据分片)* 是集群最核心的功能。集群将数据分散到多个节点,**一方面** 突破了 Redis 单机内存大小的限制,**存储容量大大增加**;**另一方面** 每个主节点都可以对外提供读服务和写服务,**大大提高了集群的响应能力**。Redis 单机内存大小受限问题,在介绍持久化和主从复制时都有提及,例如,如果单机内存太大,`bgsave` 和 `bgrewriteaof` 的 `fork` 操作可能导致主进程阻塞,主从环境下主机切换时可能导致从节点长时间无法提供服务,全量复制阶段主节点的复制缓冲区可能溢出…… -2. **高可用:** 集群支持主从复制和主节点的 **自动故障转移** *(与哨兵类似)*,当任一节点发生故障时,集群仍然可以对外提供服务。 - -## 快速体验 - -#### 第一步:创建集群节点配置文件 - -首先我们找一个地方创建一个名为 `redis-cluster` 的目录: - -```bash -mkdir -p ~/Desktop/redis-cluster -``` - -然后按照上面的方法,创建六个配置文件,分别命名为:`redis_7000.conf`/`redis_7001.conf`.....`redis_7005.conf`,然后根据不同的端口号修改对应的端口值就好了: - -```bash -# 后台执行 -daemonize yes -# 端口号 -port 7000 -# 为每一个集群节点指定一个 pid_file -pidfile ~/Desktop/redis-cluster/redis_7000.pid -# 启动集群模式 -cluster-enabled yes -# 每一个集群节点都有一个配置文件,这个文件是不能手动编辑的。确保每一个集群节点的配置文件不通 -cluster-config-file nodes-7000.conf -# 集群节点的超时时间,单位:ms,超时后集群会认为该节点失败 -cluster-node-timeout 5000 -# 最后将 appendonly 改成 yes(AOF 持久化) -appendonly yes -``` - -记得把对应上述配置文件中根端口对应的配置都修改掉 *(port/ pidfile/ cluster-config-file)*。 - -#### 第二步:分别启动 6 个 Redis 实例 - -```bash -redis-server ~/Desktop/redis-cluster/redis_7000.conf -redis-server ~/Desktop/redis-cluster/redis_7001.conf -redis-server ~/Desktop/redis-cluster/redis_7002.conf -redis-server ~/Desktop/redis-cluster/redis_7003.conf -redis-server ~/Desktop/redis-cluster/redis_7004.conf -redis-server ~/Desktop/redis-cluster/redis_7005.conf -``` - -然后执行 `ps -ef | grep redis` 查看是否启动成功: - -![](https://upload-images.jianshu.io/upload_images/7896890-452c3152054c36f1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -可以看到 `6` 个 Redis 节点都以集群的方式成功启动了,**但是现在每个节点还处于独立的状态**,也就是说它们每一个都各自成了一个集群,还没有互相联系起来,我们需要手动地把他们之间建立起联系。 - -#### 第三步:建立集群 - -执行下列命令: - -```bash -redis-cli --cluster create --cluster-replicas 1 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 -``` - -- 这里稍微解释一下这个 `--replicas 1` 的意思是:我们希望为集群中的每个主节点创建一个从节点。 - -观察控制台输出: - -![](https://upload-images.jianshu.io/upload_images/7896890-d5ab644e76e9cc87.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -看到 `[OK]` 的信息之后,就表示集群已经搭建成功了,可以看到,这里我们正确地创建了三主三从的集群。 - -#### 第四步:验证集群 - -我们先使用 `redic-cli` 任意连接一个节点: - -```bash -redis-cli -c -h 127.0.0.1 -p 7000 -127.0.0.1:7000> -``` - -- `-c`表示集群模式;`-h` 指定 ip 地址;`-p` 指定端口。 - -然后随便 `set` 一些值观察控制台输入: - -```bash -127.0.0.1:7000> SET name wmyskxz --> Redirected to slot [5798] located at 127.0.0.1:7001 -OK -127.0.0.1:7001> -``` - -可以看到这里 Redis 自动帮我们进行了 `Redirected` 操作跳转到了 `7001` 这个实例上。 - -我们再使用 `cluster info` *(查看集群信息)* 和 `cluster nodes` *(查看节点列表)* 来分别看看:*(任意节点输入均可)* - -```bash -127.0.0.1:7001> CLUSTER INFO -cluster_state:ok -cluster_slots_assigned:16384 -cluster_slots_ok:16384 -cluster_slots_pfail:0 -cluster_slots_fail:0 -cluster_known_nodes:6 -cluster_size:3 -cluster_current_epoch:6 -cluster_my_epoch:2 -cluster_stats_messages_ping_sent:1365 -cluster_stats_messages_pong_sent:1358 -cluster_stats_messages_meet_sent:4 -cluster_stats_messages_sent:2727 -cluster_stats_messages_ping_received:1357 -cluster_stats_messages_pong_received:1369 -cluster_stats_messages_meet_received:1 -cluster_stats_messages_received:2727 - -127.0.0.1:7001> CLUSTER NODES -56a04742f36c6e84968cae871cd438935081e86f 127.0.0.1:7003@17003 slave 4ec8c022e9d546c9b51deb9d85f6cf867bf73db6 0 1584428884000 4 connected -4ec8c022e9d546c9b51deb9d85f6cf867bf73db6 127.0.0.1:7000@17000 master - 0 1584428884000 1 connected 0-5460 -e2539c4398b8258d3f9ffa714bd778da107cb2cd 127.0.0.1:7005@17005 slave a3406db9ae7144d17eb7df5bffe8b70bb5dd06b8 0 1584428885222 6 connected -d31cd1f423ab1e1849cac01ae927e4b6950f55d9 127.0.0.1:7004@17004 slave 236cefaa9cdc295bc60a5bd1aed6a7152d4f384d 0 1584428884209 5 connected -236cefaa9cdc295bc60a5bd1aed6a7152d4f384d 127.0.0.1:7001@17001 myself,master - 0 1584428882000 2 connected 5461-10922 -a3406db9ae7144d17eb7df5bffe8b70bb5dd06b8 127.0.0.1:7002@17002 master - 0 1584428884000 3 connected 10923-16383 -127.0.0.1:7001> -``` - -## 数据分区方案简析 - -#### 方案一:哈希值 % 节点数 - -哈希取余分区思路非常简单:计算 `key` 的 hash 值,然后对节点数量进行取余,从而决定数据映射到哪个节点上。 - -不过该方案最大的问题是,**当新增或删减节点时**,节点数量发生变化,系统中所有的数据都需要 **重新计算映射关系**,引发大规模数据迁移。 - -#### 方案二:一致性哈希分区 - -一致性哈希算法将 **整个哈希值空间** 组织成一个虚拟的圆环,范围是 *[0 , 232-1]*,对于每一个数据,根据 `key` 计算 hash 值,确数据在环上的位置,然后从此位置沿顺时针行走,找到的第一台服务器就是其应该映射到的服务器: - -![](https://upload-images.jianshu.io/upload_images/7896890-40e8a2c096c8da92.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -与哈希取余分区相比,一致性哈希分区将 **增减节点的影响限制在相邻节点**。以上图为例,如果在 `node1` 和 `node2` 之间增加 `node5`,则只有 `node2` 中的一部分数据会迁移到 `node5`;如果去掉 `node2`,则原 `node2` 中的数据只会迁移到 `node4` 中,只有 `node4` 会受影响。 - -一致性哈希分区的主要问题在于,当 **节点数量较少** 时,增加或删减节点,**对单个节点的影响可能很大**,造成数据的严重不平衡。还是以上图为例,如果去掉 `node2`,`node4` 中的数据由总数据的 `1/4` 左右变为 `1/2` 左右,与其他节点相比负载过高。 - -#### 方案三:带有虚拟节点的一致性哈希分区 - -该方案在 **一致性哈希分区的基础上**,引入了 **虚拟节点** 的概念。Redis 集群使用的便是该方案,其中的虚拟节点称为 **槽(slot)**。槽是介于数据和实际节点之间的虚拟概念,每个实际节点包含一定数量的槽,每个槽包含哈希值在一定范围内的数据。 - -在使用了槽的一致性哈希分区中,**槽是数据管理和迁移的基本单位**。槽 **解耦** 了 **数据和实际节点** 之间的关系,增加或删除节点对系统的影响很小。仍以上图为例,系统中有 `4` 个实际节点,假设为其分配 `16` 个槽(0-15); - -- 槽 0-3 位于 node1;4-7 位于 node2;以此类推.... - -如果此时删除 `node2`,只需要将槽 4-7 重新分配即可,例如槽 4-5 分配给 `node1`,槽 6 分配给 `node3`,槽 7 分配给 `node4`;可以看出删除 `node2` 后,数据在其他节点的分布仍然较为均衡。 - -## 节点通信机制简析 - -集群的建立离不开节点之间的通信,例如我们上访在 *快速体验* 中刚启动六个集群节点之后通过 `redis-cli` 命令帮助我们搭建起来了集群,实际上背后每个集群之间的两两连接是通过了 `CLUSTER MEET ` 命令发送 `MEET` 消息完成的,下面我们展开详细说说。 - -#### 两个端口 - -在 **哨兵系统** 中,节点分为 **数据节点** 和 **哨兵节点**:前者存储数据,后者实现额外的控制功能。在 **集群** 中,没有数据节点与非数据节点之分:**所有的节点都存储数据,也都参与集群状态的维护**。为此,集群中的每个节点,都提供了两个 TCP 端口: - -- **普通端口:** 即我们在前面指定的端口 *(7000等)*。普通端口主要用于为客户端提供服务 *(与单机节点类似)*;但在节点间数据迁移时也会使用。 -- **集群端口:** 端口号是普通端口 + 10000 *(10000是固定值,无法改变)*,如 `7000` 节点的集群端口为 `17000`。**集群端口只用于节点之间的通信**,如搭建集群、增减节点、故障转移等操作时节点间的通信;不要使用客户端连接集群接口。为了保证集群可以正常工作,在配置防火墙时,要同时开启普通端口和集群端口。 - -#### Gossip 协议 - -节点间通信,按照通信协议可以分为几种类型:单对单、广播、Gossip 协议等。重点是广播和 Gossip 的对比。 - -- 广播是指向集群内所有节点发送消息。**优点** 是集群的收敛速度快(集群收敛是指集群内所有节点获得的集群信息是一致的),**缺点** 是每条消息都要发送给所有节点,CPU、带宽等消耗较大。 -- Gossip 协议的特点是:在节点数量有限的网络中,**每个节点都 “随机” 的与部分节点通信** *(并不是真正的随机,而是根据特定的规则选择通信的节点)*,经过一番杂乱无章的通信,每个节点的状态很快会达到一致。Gossip 协议的 **优点** 有负载 *(比广播)* 低、去中心化、容错性高 *(因为通信有冗余)* 等;**缺点** 主要是集群的收敛速度慢。 - -#### 消息类型 - -集群中的节点采用 **固定频率(每秒10次)** 的 **定时任务** 进行通信相关的工作:判断是否需要发送消息及消息类型、确定接收节点、发送消息等。如果集群状态发生了变化,如增减节点、槽状态变更,通过节点间的通信,所有节点会很快得知整个集群的状态,使集群收敛。 - -节点间发送的消息主要分为 `5` 种:`meet 消息`、`ping 消息`、`pong 消息`、`fail 消息`、`publish 消息`。不同的消息类型,通信协议、发送的频率和时机、接收节点的选择等是不同的: - -- **MEET 消息:** 在节点握手阶段,当节点收到客户端的 `CLUSTER MEET` 命令时,会向新加入的节点发送 `MEET` 消息,请求新节点加入到当前集群;新节点收到 MEET 消息后会回复一个 `PONG` 消息。 -- **PING 消息:** 集群里每个节点每秒钟会选择部分节点发送 `PING` 消息,接收者收到消息后会回复一个 `PONG` 消息。**PING 消息的内容是自身节点和部分其他节点的状态信息**,作用是彼此交换信息,以及检测节点是否在线。`PING` 消息使用 Gossip 协议发送,接收节点的选择兼顾了收敛速度和带宽成本,**具体规则如下**:(1)随机找 5 个节点,在其中选择最久没有通信的 1 个节点;(2)扫描节点列表,选择最近一次收到 `PONG` 消息时间大于 `cluster_node_timeout / 2` 的所有节点,防止这些节点长时间未更新。 -- **PONG消息:** `PONG` 消息封装了自身状态数据。可以分为两种:**第一种** 是在接到 `MEET/PING` 消息后回复的 `PONG` 消息;**第二种** 是指节点向集群广播 `PONG` 消息,这样其他节点可以获知该节点的最新信息,例如故障恢复后新的主节点会广播 `PONG` 消息。 -- **FAIL 消息:** 当一个主节点判断另一个主节点进入 `FAIL` 状态时,会向集群广播这一 `FAIL` 消息;接收节点会将这一 `FAIL` 消息保存起来,便于后续的判断。 -- **PUBLISH 消息:** 节点收到 `PUBLISH` 命令后,会先执行该命令,然后向集群广播这一消息,接收节点也会执行该 `PUBLISH` 命令。 - -## 数据结构简析 - -节点需要专门的数据结构来存储集群的状态。所谓集群的状态,是一个比较大的概念,包括:集群是否处于上线状态、集群中有哪些节点、节点是否可达、节点的主从状态、槽的分布…… - -节点为了存储集群状态而提供的数据结构中,最关键的是 `clusterNode` 和 `clusterState` 结构:前者记录了一个节点的状态,后者记录了集群作为一个整体的状态。 - -#### clusterNode 结构 - -`clusterNode` 结构保存了 **一个节点的当前状态**,包括创建时间、节点 id、ip 和端口号等。每个节点都会用一个 `clusterNode` 结构记录自己的状态,并为集群内所有其他节点都创建一个 `clusterNode` 结构来记录节点状态。 - -下面列举了 `clusterNode` 的部分字段,并说明了字段的含义和作用: - -```c -typedef struct clusterNode { - //节点创建时间 - mstime_t ctime; - //节点id - char name[REDIS_CLUSTER_NAMELEN]; - //节点的ip和端口号 - char ip[REDIS_IP_STR_LEN]; - int port; - //节点标识:整型,每个bit都代表了不同状态,如节点的主从状态、是否在线、是否在握手等 - int flags; - //配置纪元:故障转移时起作用,类似于哨兵的配置纪元 - uint64_t configEpoch; - //槽在该节点中的分布:占用16384/8个字节,16384个比特;每个比特对应一个槽:比特值为1,则该比特对应的槽在节点中;比特值为0,则该比特对应的槽不在节点中 - unsigned char slots[16384/8]; - //节点中槽的数量 - int numslots; - ………… -} clusterNode; -``` - -除了上述字段,`clusterNode` 还包含节点连接、主从复制、故障发现和转移需要的信息等。 - -#### clusterState 结构 - -`clusterState` 结构保存了在当前节点视角下,集群所处的状态。主要字段包括: - -```c -typedef struct clusterState { - //自身节点 - clusterNode *myself; - //配置纪元 - uint64_t currentEpoch; - //集群状态:在线还是下线 - int state; - //集群中至少包含一个槽的节点数量 - int size; - //哈希表,节点名称->clusterNode节点指针 - dict *nodes; - //槽分布信息:数组的每个元素都是一个指向clusterNode结构的指针;如果槽还没有分配给任何节点,则为NULL - clusterNode *slots[16384]; - ………… -} clusterState; -``` - -除此之外,`clusterState` 还包括故障转移、槽迁移等需要的信息。 - -> 更多关于集群内容请自行阅读《Redis 设计与实现》,其中有更多细节方面的介绍 - [http://redisbook.com/](http://redisbook.com/) - -# 相关阅读 - -1. Redis(1)——5种基本数据结构 - [https://www.wmyskxz.com/2020/02/28/redis-1-5-chong-ji-ben-shu-ju-jie-gou/](https://www.wmyskxz.com/2020/02/28/redis-1-5-chong-ji-ben-shu-ju-jie-gou/) -2. Redis(2)——跳跃表 - [https://www.wmyskxz.com/2020/02/29/redis-2-tiao-yue-biao/](https://www.wmyskxz.com/2020/02/29/redis-2-tiao-yue-biao/) -3. Redis(3)——分布式锁深入探究 - [https://www.wmyskxz.com/2020/03/01/redis-3/](https://www.wmyskxz.com/2020/03/01/redis-3/) -4. Reids(4)——神奇的HyperLoglog解决统计问题 - [https://www.wmyskxz.com/2020/03/02/reids-4-shen-qi-de-hyperloglog-jie-jue-tong-ji-wen-ti/](https://www.wmyskxz.com/2020/03/02/reids-4-shen-qi-de-hyperloglog-jie-jue-tong-ji-wen-ti/) -5. Redis(5)——亿级数据过滤和布隆过滤器 - [https://www.wmyskxz.com/2020/03/11/redis-5-yi-ji-shu-ju-guo-lu-he-bu-long-guo-lu-qi/](https://www.wmyskxz.com/2020/03/11/redis-5-yi-ji-shu-ju-guo-lu-he-bu-long-guo-lu-qi/) -6. Redis(6)——GeoHash查找附近的人[https://www.wmyskxz.com/2020/03/12/redis-6-geohash-cha-zhao-fu-jin-de-ren/](https://www.wmyskxz.com/2020/03/12/redis-6-geohash-cha-zhao-fu-jin-de-ren/) -7. Redis(7)——持久化【一文了解】 - [https://www.wmyskxz.com/2020/03/13/redis-7-chi-jiu-hua-yi-wen-liao-jie/](https://www.wmyskxz.com/2020/03/13/redis-7-chi-jiu-hua-yi-wen-liao-jie/) -8. Redis(8)——发布/订阅与Stream - [https://www.wmyskxz.com/2020/03/15/redis-8-fa-bu-ding-yue-yu-stream/](https://www.wmyskxz.com/2020/03/15/redis-8-fa-bu-ding-yue-yu-stream/) - -# 参考资料 - -1. 《Redis 设计与实现》 | 黄健宏 著 - [http://redisbook.com/](http://redisbook.com/) -2. 《Redis 深度历险》 | 钱文品 著 - [https://book.douban.com/subject/30386804/](https://book.douban.com/subject/30386804/) -3. 深入学习Redis(3):主从复制 - [https://www.cnblogs.com/kismetv/p/9236731.html](https://www.cnblogs.com/kismetv/p/9236731.html) -4. Redis 主从复制 原理与用法 - [https://blog.csdn.net/Stubborn_Cow/article/details/50442950](https://blog.csdn.net/Stubborn_Cow/article/details/50442950) -5. 深入学习Redis(4):哨兵 - [https://www.cnblogs.com/kismetv/p/9609938.html](https://www.cnblogs.com/kismetv/p/9609938.html) -6. Redis 5 之后版本的高可用集群搭建 - [https://www.jianshu.com/p/8045b92fafb2](https://www.jianshu.com/p/8045b92fafb2) - -> - 本文已收录至我的 Github 程序员成长系列 **【More Than Java】,学习,不止 Code,欢迎 star:[https://github.com/wmyskxz/MoreThanJava](https://github.com/wmyskxz/MoreThanJava)** -> - **个人公众号** :wmyskxz,**个人独立域名博客**:wmyskxz.com,坚持原创输出,下方扫码关注,2020,与您共同成长! - -![](https://upload-images.jianshu.io/upload_images/7896890-fca34cfd601e7449.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -非常感谢各位人才能 **看到这里**,如果觉得本篇文章写得不错,觉得 **「我没有三颗心脏」有点东西** 的话,**求点赞,求关注,求分享,求留言!** - -创作不易,各位的支持和认可,就是我创作的最大动力,我们下篇文章见! \ No newline at end of file diff --git "a/docs/database/Redis/redis-collection/Reids(4)\342\200\224\342\200\224\347\245\236\345\245\207\347\232\204HyperLoglog\350\247\243\345\206\263\347\273\237\350\256\241\351\227\256\351\242\230.md" "b/docs/database/Redis/redis-collection/Reids(4)\342\200\224\342\200\224\347\245\236\345\245\207\347\232\204HyperLoglog\350\247\243\345\206\263\347\273\237\350\256\241\351\227\256\351\242\230.md" deleted file mode 100644 index 98847b842ab..00000000000 --- "a/docs/database/Redis/redis-collection/Reids(4)\342\200\224\342\200\224\347\245\236\345\245\207\347\232\204HyperLoglog\350\247\243\345\206\263\347\273\237\350\256\241\351\227\256\351\242\230.md" +++ /dev/null @@ -1,469 +0,0 @@ -> 授权转载自: https://github.com/wmyskxz/MoreThanJava#part3-redis - -![](https://upload-images.jianshu.io/upload_images/7896890-a408d790b0b4f4b9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -# 一、HyperLogLog 简介 - -**HyperLogLog** 是最早由 [Flajolet](http://algo.inria.fr/flajolet/Publications/FlFuGaMe07.pdf) 及其同事在 2007 年提出的一种 **估算基数的近似最优算法**。但跟原版论文不同的是,好像很多书包括 Redis 作者都把它称为一种 **新的数据结构(new datastruct)** *(算法实现确实需要一种特定的数据结构来实现)*。 - -![](http://wx2.sinaimg.cn/large/006oOWahly1fpsc3t7fnng30ab05tkjl.gif) - -## 关于基数统计 - -**基数统计(Cardinality Counting)** 通常是用来统计一个集合中不重复的元素个数。 - -**思考这样的一个场景:** 如果你负责开发维护一个大型的网站,有一天老板找产品经理要网站上每个网页的 **UV(独立访客,每个用户每天只记录一次)**,然后让你来开发这个统计模块,你会如何实现? - -![](https://upload-images.jianshu.io/upload_images/7896890-a9dbcf6374d482ba.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - -如果统计 **PV(浏览量,用户没点一次记录一次)**,那非常好办,给每个页面配置一个独立的 Redis 计数器就可以了,把这个计数器的 key 后缀加上当天的日期。这样每来一个请求,就执行 `INCRBY` 指令一次,最终就可以统计出所有的 **PV** 数据了。 - -但是 **UV** 不同,它要去重,**同一个用户一天之内的多次访问请求只能计数一次**。这就要求了每一个网页请求都需要带上用户的 ID,无论是登录用户还是未登录的用户,都需要一个唯一 ID 来标识。 - -你也许马上就想到了一个 *简单的解决方案*:那就是 **为每一个页面设置一个独立的 set 集合** 来存储所有当天访问过此页面的用户 ID。但这样的 **问题** 就是: - -1. **存储空间巨大:** 如果网站访问量一大,你需要用来存储的 set 集合就会非常大,如果页面再一多.. 为了一个去重功能耗费的资源就可以直接让你 **老板打死你**; -2. **统计复杂:** 这么多 set 集合如果要聚合统计一下,又是一个复杂的事情; - -![](https://upload-images.jianshu.io/upload_images/7896890-b8ddfcd39cb46cb5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - -## 基数统计的常用方法 - -对于上述这样需要 **基数统计** 的事情,通常来说有两种比 set 集合更好的解决方案: - -### 第一种:B 树 - -**B 树最大的优势就是插入和查找效率很高**,如果用 B 树存储要统计的数据,可以快速判断新来的数据是否存在,并快速将元素插入 B 树。要计算基础值,只需要计算 B 树的节点个数就行了。 - -不过将 B 树结构维护到内存中,能够解决统计和计算的问题,但是 **并没有节省内存**。 - -### 第二种:bitmap - -**bitmap** 可以理解为通过一个 bit 数组来存储特定数据的一种数据结构,**每一个 bit 位都能独立包含信息**,bit 是数据的最小存储单位,因此能大量节省空间,也可以将整个 bit 数据一次性 load 到内存计算。如果定义一个很大的 bit 数组,基础统计中 **每一个元素对应到 bit 数组中的一位**,例如: - -![](https://upload-images.jianshu.io/upload_images/7896890-fb4283ad7dbd89a2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -bitmap 还有一个明显的优势是 **可以轻松合并多个统计结果**,只需要对多个结果求异或就可以了,也可以大大减少存储内存。可以简单做一个计算,如果要统计 **1 亿** 个数据的基数值,**大约需要的内存**:`100_000_000/ 8/ 1024/ 1024 ≈ 12 M`,如果用 **32 bit** 的 int 代表 **每一个** 统计的数据,**大约需要内存**:`32 * 100_000_000/ 8/ 1024/ 1024 ≈ 381 M` - -可以看到 bitmap 对于内存的节省显而易见,但仍然不够。统计一个对象的基数值就需要 `12 M`,如果统计 1 万个对象,就需要接近 `120 G`,对于大数据的场景仍然不适用。 - -![](https://upload-images.jianshu.io/upload_images/7896890-1ebb3265b4297fa1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -## 概率算法 - -实际上目前还没有发现更好的在 **大数据场景** 中 **准确计算** 基数的高效算法,因此在不追求绝对精确的情况下,使用概率算法算是一个不错的解决方案。 - -概率算法 **不直接存储** 数据集合本身,通过一定的 **概率统计方法预估基数值**,这种方法可以大大节省内存,同时保证误差控制在一定范围内。目前用于基数计数的概率算法包括: - -- **Linear Counting(LC)**:早期的基数估计算法,LC 在空间复杂度方面并不算优秀,实际上 LC 的空间复杂度与上文中简单 bitmap 方法是一样的(但是有个常数项级别的降低),都是 O(Nmax) -- **LogLog Counting(LLC)**:LogLog Counting 相比于 LC 更加节省内存,空间复杂度只有 O(log2(log2(Nmax))) -- **HyperLogLog Counting(HLL)**:HyperLogLog Counting 是基于 LLC 的优化和改进,在同样空间复杂度情况下,能够比 LLC 的基数估计误差更小 - -其中,**HyperLogLog** 的表现是惊人的,上面我们简单计算过用 **bitmap** 存储 **1 个亿** 统计数据大概需要 `12 M` 内存,而在 **HyperLoglog** 中,只需要不到 **1 K** 内存就能够做到!在 Redis 中实现的 **HyperLoglog** 也只需要 **12 K** 内存,在 **标准误差 0.81%** 的前提下,**能够统计 264 个数据**! - -![](https://upload-images.jianshu.io/upload_images/7896890-439fe643e2dc081a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -**这是怎么做到的?!** 下面赶紧来了解一下! - -# 二、HyperLogLog 原理 - -我们来思考一个抛硬币的游戏:你连续掷 n 次硬币,然后说出其中**连续掷为正面的最大次数,我来猜你一共抛了多少次**。 - -这很容易理解吧,例如:你说你这一次 *最多连续出现了 2 次* 正面,那么我就可以知道你这一次投掷的次数并不多,所以 *我可能会猜是 5* 或者是其他小一些的数字,但如果你说你这一次 *最多连续出现了 20 次* 正面,虽然我觉得不可能,但我仍然知道你花了特别多的时间,所以 *我说 GUN...*。 - -![](https://upload-images.jianshu.io/upload_images/7896890-2042926c4383c027.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - -这期间我可能会要求你重复实验,然后我得到了更多的数据之后就会估计得更准。**我们来把刚才的游戏换一种说法**: - -![](https://upload-images.jianshu.io/upload_images/7896890-24e8f48f5e3eb81f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -这张图的意思是,我们给定一系列的随机整数,**记录下低位连续零位的最大长度 K**,即为图中的 `maxbit`,**通过这个 K 值我们就可以估算出随机数的数量 N**。 - -## 代码实验 - -我们可以简单编写代码做一个实验,来探究一下 `K` 和 `N` 之间的关系: - -```java -public class PfTest { - - static class BitKeeper { - - private int maxbit; - - public void random() { - long value = ThreadLocalRandom.current().nextLong(2L << 32); - int bit = lowZeros(value); - if (bit > this.maxbit) { - this.maxbit = bit; - } - } - - private int lowZeros(long value) { - int i = 0; - for (; i < 32; i++) { - if (value >> i << i != value) { - break; - } - } - return i - 1; - } - } - - static class Experiment { - - private int n; - private BitKeeper keeper; - - public Experiment(int n) { - this.n = n; - this.keeper = new BitKeeper(); - } - - public void work() { - for (int i = 0; i < n; i++) { - this.keeper.random(); - } - } - - public void debug() { - System.out - .printf("%d %.2f %d\n", this.n, Math.log(this.n) / Math.log(2), this.keeper.maxbit); - } - } - - public static void main(String[] args) { - for (int i = 1000; i < 100000; i += 100) { - Experiment exp = new Experiment(i); - exp.work(); - exp.debug(); - } - } -} -``` - -跟上图中的过程是一致的,话说为啥叫 `PfTest` 呢,包括 Redis 中的命令也一样带有一个 `PF` 前缀,还记得嘛,因为 **HyperLogLog** 的提出者上文提到过的,叫 `Philippe Flajolet`。 - -截取部分输出查看: - -```java -//n n/log2 maxbit -34000 15.05 13 -35000 15.10 13 -36000 15.14 16 -37000 15.18 17 -38000 15.21 14 -39000 15.25 16 -40000 15.29 14 -41000 15.32 16 -42000 15.36 18 -``` - -会发现 `K` 和 `N` 的对数之间存在显著的线性相关性:**N 约等于 2k** - -## 更近一步:分桶平均 - -**如果 `N` 介于 2k 和 2k+1 之间,用这种方式估计的值都等于 2k,这明显是不合理的**,所以我们可以使用多个 `BitKeeper` 进行加权估计,就可以得到一个比较准确的值了: - -```java -public class PfTest { - - static class BitKeeper { - // 无变化, 代码省略 - } - - static class Experiment { - - private int n; - private int k; - private BitKeeper[] keepers; - - public Experiment(int n) { - this(n, 1024); - } - - public Experiment(int n, int k) { - this.n = n; - this.k = k; - this.keepers = new BitKeeper[k]; - for (int i = 0; i < k; i++) { - this.keepers[i] = new BitKeeper(); - } - } - - public void work() { - for (int i = 0; i < this.n; i++) { - long m = ThreadLocalRandom.current().nextLong(1L << 32); - BitKeeper keeper = keepers[(int) (((m & 0xfff0000) >> 16) % keepers.length)]; - keeper.random(); - } - } - - public double estimate() { - double sumbitsInverse = 0.0; - for (BitKeeper keeper : keepers) { - sumbitsInverse += 1.0 / (float) keeper.maxbit; - } - double avgBits = (float) keepers.length / sumbitsInverse; - return Math.pow(2, avgBits) * this.k; - } - } - - public static void main(String[] args) { - for (int i = 100000; i < 1000000; i += 100000) { - Experiment exp = new Experiment(i); - exp.work(); - double est = exp.estimate(); - System.out.printf("%d %.2f %.2f\n", i, est, Math.abs(est - i) / i); - } - } -} -``` - -这个过程有点 **类似于选秀节目里面的打分**,一堆专业评委打分,但是有一些评委因为自己特别喜欢所以给高了,一些评委又打低了,所以一般都要 **屏蔽最高分和最低分**,然后 **再计算平均值**,这样的出来的分数就差不多是公平公正的了。 - -![](https://upload-images.jianshu.io/upload_images/7896890-6c927d25750f20d1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -上述代码就有 **1024** 个 "评委",并且在计算平均值的时候,采用了 **调和平均数**,也就是倒数的平均值,它能有效地平滑离群值的影响: - -```java -avg = (3 + 4 + 5 + 104) / 4 = 29 -avg = 4 / (1/3 + 1/4 + 1/5 + 1/104) = 5.044 -``` - -观察脚本的输出,误差率百分比控制在个位数: - -```java -100000 94274.94 0.06 -200000 194092.62 0.03 -300000 277329.92 0.08 -400000 373281.66 0.07 -500000 501551.60 0.00 -600000 596078.40 0.01 -700000 687265.72 0.02 -800000 828778.96 0.04 -900000 944683.53 0.05 -``` - -真实的 HyperLogLog 要比上面的示例代码更加复杂一些,也更加精确一些。上面这个算法在随机次数很少的情况下会出现除零错误,因为 `maxbit = 0` 是不可以求倒数的。 - -## 真实的 HyperLogLog - -有一个神奇的网站,可以动态地让你观察到 HyperLogLog 的算法到底是怎么执行的:[http://content.research.neustar.biz/blog/hll.html](http://content.research.neustar.biz/blog/hll.html) - -![](https://upload-images.jianshu.io/upload_images/7896890-72f00a9983a1395e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -其中的一些概念这里稍微解释一下,您就可以自行去点击 `step` 来观察了: - -- **m 表示分桶个数:** 从图中可以看到,这里分成了 64 个桶; -- **蓝色的 bit 表示在桶中的位置:** 例如图中的 `101110` 实则表示二进制的 `46`,所以该元素被统计在中间大表格 `Register Values` 中标红的第 46 个桶之中; -- **绿色的 bit 表示第一个 1 出现的位置**: 从图中可以看到标绿的 bit 中,从右往左数,第一位就是 1,所以在 `Register Values` 第 46 个桶中写入 1; -- **红色 bit 表示绿色 bit 的值的累加:** 下一个出现在第 46 个桶的元素值会被累加; - - -### 为什么要统计 Hash 值中第一个 1 出现的位置? - -因为第一个 1 出现的位置可以同我们抛硬币的游戏中第一次抛到正面的抛掷次数对应起来,根据上面掷硬币实验的结论,记录每个数据的第一个出现的位置 `K`,就可以通过其中最大值 Kmax 来推导出数据集合中的基数:**N = 2Kmax** - -### PF 的内存占用为什么是 12 KB? - -我们上面的算法中使用了 **1024** 个桶,网站演示也只有 **64** 个桶,不过在 Redis 的 HyperLogLog 实现中,用的是 **16384** 个桶,即:214,也就是说,就像上面网站中间那个 `Register Values` 大表格有 **16384** 格。 - -**而Redis 最大能够统计的数据量是 264**,即每个桶的 `maxbit` 需要 **6** 个 bit 来存储,最大可以表示 `maxbit = 63`,于是总共占用内存就是:**(214) x 6 / 8** *(每个桶 6 bit,而这么多桶本身要占用 16384 bit,再除以 8 转换成 KB)*,算出来的结果就是 `12 KB`。 - -# 三、Redis 中的 HyperLogLog 实现 - -从上面我们算是对 **HyperLogLog** 的算法和思想有了一定的了解,并且知道了一个 **HyperLogLog** 实际占用的空间大约是 `12 KB`,但 Redis 对于内存的优化非常变态,当 **计数比较小** 的时候,大多数桶的计数值都是 **零**,这个时候 Redis 就会适当节约空间,转换成另外一种 **稀疏存储方式**,与之相对的,正常的存储模式叫做 **密集存储**,这种方式会恒定地占用 `12 KB`。 - -## 密集型存储结构 - -密集型的存储结构非常简单,就是 **16384 个 6 bit 连续串成** 的字符串位图: - -![](https://upload-images.jianshu.io/upload_images/7896890-0ba2adb0214afd0c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -我们都知道,一个字节是由 8 个 bit 组成的,这样 6 bit 排列的结构就会导致,有一些桶会 **跨越字节边界**,我们需要 **对这一个或者两个字节进行适当的移位拼接** 才可以得到具体的计数值。 - -假设桶的编号为 `index`,这个 6 bity 计数值的起始字节偏移用 `offset_bytes` 表示,它在这个字节的其实比特位置偏移用 `offset_bits` 表示,于是我们有: - -```python -offset_bytes = (index * 6) / 8 -offset_bits = (index * 6) % 8 -``` - -前者是商,后者是余数。比如 `bucket 2` 的字节偏移是 1,也就是第 2 个字节。它的位偏移是 4,也就是第 2 个字节的第 5 个位开始是 bucket 2 的计数值。需要注意的是 **字节位序是左边低位右边高位**,而通常我们使用的字节都是左边高位右边低位。 - -这里就涉及到两种情况,**如果 `offset_bits` 小于等于 2**,说明这 **6 bit 在一个字节的内部**,可以直接使用下面的表达式得到计数值 `val`: - -```python -val = buffer[offset_bytes] >> offset_bits # 向右移位 -``` - -**如果 `offset_bits` 大于 2**,那么就会涉及到 **跨越字节边界**,我们需要拼接两个字节的位片段: - -```python -# 低位值 -low_val = buffer[offset_bytes] >> offset_bits -# 低位个数 -low_bits = 8 - offset_bits -# 拼接,保留低6位 -val = (high_val << low_bits | low_val) & 0b111111 -``` - -不过下面 Redis 的源码要晦涩一点,看形式它似乎只考虑了跨越字节边界的情况。这是因为如果 6 bit 在单个字节内,上面代码中的 `high_val` 的值是零,所以这一份代码可以同时照顾单字节和双字节: - -```c -// 获取指定桶的计数值 -#define HLL_DENSE_GET_REGISTER(target,p,regnum) do { \ - uint8_t *_p = (uint8_t*) p; \ - unsigned long _byte = regnum*HLL_BITS/8; \ - unsigned long _fb = regnum*HLL_BITS&7; \ # %8 = &7 - unsigned long _fb8 = 8 - _fb; \ - unsigned long b0 = _p[_byte]; \ - unsigned long b1 = _p[_byte+1]; \ - target = ((b0 >> _fb) | (b1 << _fb8)) & HLL_REGISTER_MAX; \ -} while(0) - -// 设置指定桶的计数值 -#define HLL_DENSE_SET_REGISTER(p,regnum,val) do { \ - uint8_t *_p = (uint8_t*) p; \ - unsigned long _byte = regnum*HLL_BITS/8; \ - unsigned long _fb = regnum*HLL_BITS&7; \ - unsigned long _fb8 = 8 - _fb; \ - unsigned long _v = val; \ - _p[_byte] &= ~(HLL_REGISTER_MAX << _fb); \ - _p[_byte] |= _v << _fb; \ - _p[_byte+1] &= ~(HLL_REGISTER_MAX >> _fb8); \ - _p[_byte+1] |= _v >> _fb8; \ -} while(0) -``` - -## 稀疏存储结构 - -稀疏存储适用于很多计数值都是零的情况。下图表示了一般稀疏存储计数值的状态: - -![](https://upload-images.jianshu.io/upload_images/7896890-9d5a9018d2eedbd8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -当 **多个连续桶的计数值都是零** 时,Redis 提供了几种不同的表达形式: - -- `00xxxxxx`:前缀两个零表示接下来的 6bit 整数值加 1 就是零值计数器的数量,注意这里要加 1 是因为数量如果为零是没有意义的。比如 `00010101` 表示连续 `22` 个零值计数器。 -- `01xxxxxx yyyyyyyy`:6bit 最多只能表示连续 `64` 个零值计数器,这样扩展出的 14bit 可以表示最多连续 `16384` 个零值计数器。这意味着 HyperLogLog 数据结构中 `16384` 个桶的初始状态,所有的计数器都是零值,可以直接使用 2 个字节来表示。 -- `1vvvvvxx`:中间 5bit 表示计数值,尾部 2bit 表示连续几个桶。它的意思是连续 `(xx +1)` 个计数值都是 `(vvvvv + 1)`。比如 `10101011` 表示连续 `4` 个计数值都是 `11`。 - -注意 *上面第三种方式* 的计数值最大只能表示到 `32`,而 HyperLogLog 的密集存储单个计数值用 6bit 表示,最大可以表示到 `63`。**当稀疏存储的某个计数值需要调整到大于 `32` 时,Redis 就会立即转换 HyperLogLog 的存储结构,将稀疏存储转换成密集存储。** - -## 对象头 - -HyperLogLog 除了需要存储 16384 个桶的计数值之外,它还有一些附加的字段需要存储,比如总计数缓存、存储类型。所以它使用了一个额外的对象头来表示: - -```c -struct hllhdr { - char magic[4]; /* 魔术字符串"HYLL" */ - uint8_t encoding; /* 存储类型 HLL_DENSE or HLL_SPARSE. */ - uint8_t notused[3]; /* 保留三个字节未来可能会使用 */ - uint8_t card[8]; /* 总计数缓存 */ - uint8_t registers[]; /* 所有桶的计数器 */ -}; -``` - -所以 **HyperLogLog** 整体的内部结构就是 **HLL 对象头** 加上 **16384** 个桶的计数值位图。它在 Redis 的内部结构表现就是一个字符串位图。你可以把 **HyperLogLog 对象当成普通的字符串来进行处理:** - -```console -> PFADD codehole python java golang -(integer) 1 -> GET codehole -"HYLL\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80C\x03\x84MK\x80P\xb8\x80^\xf3" -``` - -但是 **不可以** 使用 **HyperLogLog** 指令来 **操纵普通的字符串**,**因为它需要检查对象头魔术字符串是否是 "HYLL"**。 - -# 四、HyperLogLog 的使用 - -**HyperLogLog** 提供了两个指令 `PFADD` 和 `PFCOUNT`,字面意思就是一个是增加,另一个是获取计数。`PFADD` 和 `set` 集合的 `SADD` 的用法是一样的,来一个用户 ID,就将用户 ID 塞进去就是,`PFCOUNT` 和 `SCARD` 的用法是一致的,直接获取计数值: - -```console -> PFADD codehole user1 -(interger) 1 -> PFCOUNT codehole -(integer) 1 -> PFADD codehole user2 -(integer) 1 -> PFCOUNT codehole -(integer) 2 -> PFADD codehole user3 -(integer) 1 -> PFCOUNT codehole -(integer) 3 -> PFADD codehole user4 user 5 -(integer) 1 -> PFCOUNT codehole -(integer) 5 -``` - -我们可以用 Java 编写一个脚本来试试 HyperLogLog 的准确性到底有多少: - -```java -public class JedisTest { - public static void main(String[] args) { - for (int i = 0; i < 100000; i++) { - jedis.pfadd("codehole", "user" + i); - } - long total = jedis.pfcount("codehole"); - System.out.printf("%d %d\n", 100000, total); - jedis.close(); - } -} -``` - -结果输出如下: - -```java -100000 99723 -``` - -发现 `10` 万条数据只差了 `277`,按照百分比误差率是 `0.277%`,对于巨量的 UV 需求来说,这个误差率真的不算高。 - -当然,除了上面的 `PFADD` 和 `PFCOUNT` 之外,还提供了第三个 `PFMEGER` 指令,用于将多个计数值累加在一起形成一个新的 `pf` 值: - -```console -> PFADD nosql "Redis" "MongoDB" "Memcached" -(integer) 1 - -> PFADD RDBMS "MySQL" "MSSQL" "PostgreSQL" -(integer) 1 - -> PFMERGE databases nosql RDBMS -OK - -> PFCOUNT databases -(integer) 6 -``` - -# 相关阅读 - -1. Redis(1)——5种基本数据结构 - [https://www.wmyskxz.com/2020/02/28/redis-1-5-chong-ji-ben-shu-ju-jie-gou/](https://www.wmyskxz.com/2020/02/28/redis-1-5-chong-ji-ben-shu-ju-jie-gou/) -2. Redis(2)——跳跃表 - [https://www.wmyskxz.com/2020/02/29/redis-2-tiao-yue-biao/](https://www.wmyskxz.com/2020/02/29/redis-2-tiao-yue-biao/) -3. Redis(3)——分布式锁深入探究 - [https://www.wmyskxz.com/2020/03/01/redis-3/](https://www.wmyskxz.com/2020/03/01/redis-3/) - -# 扩展阅读 - -1. 【算法原文】HyperLogLog: the analysis of a near-optimal -cardinality estimation algorithm - [http://algo.inria.fr/flajolet/Publications/FlFuGaMe07.pdf](http://algo.inria.fr/flajolet/Publications/FlFuGaMe07.pdf) - -# 参考资料 - -1. 【Redis 作者博客】Redis new data structure: the HyperLogLog - [http://antirez.com/news/75](http://antirez.com/news/75) -2. 神奇的HyperLogLog算法 - [http://www.rainybowe.com/blog/2017/07/13/%E7%A5%9E%E5%A5%87%E7%9A%84HyperLogLog%E7%AE%97%E6%B3%95/index.html](http://www.rainybowe.com/blog/2017/07/13/%E7%A5%9E%E5%A5%87%E7%9A%84HyperLogLog%E7%AE%97%E6%B3%95/index.html) -3. 深度探索 Redis HyperLogLog 内部数据结构 - [https://zhuanlan.zhihu.com/p/43426875](https://zhuanlan.zhihu.com/p/43426875) -4. 《Redis 深度历险》 - 钱文品/ 著 - - -> - 本文已收录至我的 Github 程序员成长系列 **【More Than Java】,学习,不止 Code,欢迎 star:[https://github.com/wmyskxz/MoreThanJava](https://github.com/wmyskxz/MoreThanJava)** -> - **个人公众号** :wmyskxz,**个人独立域名博客**:wmyskxz.com,坚持原创输出,下方扫码关注,2020,与您共同成长! - -![](https://upload-images.jianshu.io/upload_images/7896890-fca34cfd601e7449.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -非常感谢各位人才能 **看到这里**,如果觉得本篇文章写得不错,觉得 **「我没有三颗心脏」有点东西** 的话,**求点赞,求关注,求分享,求留言!** - -创作不易,各位的支持和认可,就是我创作的最大动力,我们下篇文章见! \ No newline at end of file diff --git a/docs/database/Redis/some-concepts-of-caching.md b/docs/database/Redis/some-concepts-of-caching.md deleted file mode 100644 index e60966daa19..00000000000 --- a/docs/database/Redis/some-concepts-of-caching.md +++ /dev/null @@ -1,125 +0,0 @@ - - - - - - -- [1. 缓存的基本思想](#1-缓存的基本思想) -- [2. 使用缓存为系统带来了什么问题](#2-使用缓存为系统带来了什么问题) -- [3. 本地缓存解决方案](#3-本地缓存解决方案) -- [4. 为什么要有分布式缓存?/为什么不直接用本地缓存?](#4-为什么要有分布式缓存为什么不直接用本地缓存) -- [5. 缓存读写模式/更新策略](#5-缓存读写模式更新策略) - - [5.1. Cache Aside Pattern(旁路缓存模式)](#51-cache-aside-pattern旁路缓存模式) - - [5.2. Read/Write Through Pattern(读写穿透)](#52-readwrite-through-pattern读写穿透) - - [5.3. Write Behind Pattern(异步缓存写入)](#53-write-behind-pattern异步缓存写入) - - - - -### 1. 缓存的基本思想 - -很多朋友,只知道缓存可以提高系统性能以及减少请求相应时间,但是,不太清楚缓存的本质思想是什么。 - -缓存的基本思想其实很简单,就是我们非常熟悉的空间换时间。不要把缓存想的太高大上,虽然,它的确对系统的性能提升的性价比非常高。 - -其实,我们在学习使用缓存的时候,你会发现缓存的思想实际在操作系统或者其他地方都被大量用到。 比如 **CPU Cache 缓存的是内存数据用于解决 CPU 处理速度和内存不匹配的问题,内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题。** **再比如操作系统在 页表方案 基础之上引入了 快表 来加速虚拟地址到物理地址的转换。我们可以把块表理解为一种特殊的高速缓冲存储器(Cache)。** - -回归到业务系统来说:**我们为了避免用户在请求数据的时候获取速度过于缓慢,所以我们在数据库之上增加了缓存这一层来弥补。** - -当别人再问你,缓存的基本思想的时候,就把上面 👆 这段话告诉他,我觉得会让别人对你刮目相看。 - -### 2. 使用缓存为系统带来了什么问题 - -**软件系统设计中没有银弹,往往任何技术的引入都像是把双刃剑。** 但是,你使用好了之后,这把剑就是好剑。 - -简单来说,为系统引入缓存之后往往会带来下面这些问题: - -_ps:其实我觉得引入本地缓存来做一些简单业务场景的话,实际带来的代价几乎可以忽略,下面 👇 主要是针对分布式缓存来说的。_ - -1. **系统复杂性增加** :引入缓存之后,你要维护缓存和数据库的数据一致性、维护热点缓存等等。 -2. **系统开发成本往往会增加** :引入缓存意味着系统需要一个单独的缓存服务,这是需要花费相应的成本的,并且这个成本还是很贵的,毕竟耗费的是宝贵的内存。但是,如果你只是简单的使用一下本地缓存存储一下简单的数据,并且数据量不大的话,那么就不需要单独去弄一个缓存服务。 - -### 3. 本地缓存解决方案 - -_先来聊聊本地缓存,这个实际在很多项目中用的蛮多,特别是单体架构的时候。数据量不大,并且没有分布式要求的话,使用本地缓存还是可以的。_ - -常见的单体架构图如下,我们使用 **Nginx** 来做**负载均衡**,部署两个相同的服务到服务器,两个服务使用同一个数据库,并且使用的是本地缓存。 - -![单体架构](./images/redis-all/单体架构.png) - -_那本地缓存的方案有哪些呢?且听 Guide 给你来说一说。_ - -**一:JDK 自带的 `HashMap` 和 `ConcurrentHashMap` 了。** - -`ConcurrentHashMap` 可以看作是线程安全版本的 `HashMap` ,两者都是存放 key/value 形式的键值对。但是,大部分场景来说不会使用这两者当做缓存,因为只提供了缓存的功能,并没有提供其他诸如过期时间之类的功能。一个稍微完善一点的缓存框架至少要提供:过期时间、淘汰机制、命中率统计这三点。 - -**二: `Ehcache` 、 `Guava Cache` 、 `Spring Cache` 这三者是使用的比较多的本地缓存框架。** - -`Ehcache` 的话相比于其他两者更加重量。不过,相比于 `Guava Cache` 、 `Spring Cache` 来说, `Ehcache` 支持可以嵌入到 hibernate 和 mybatis 作为多级缓存,并且可以将缓存的数据持久化到本地磁盘中、同时也提供了集群方案(比较鸡肋,可忽略)。 - -`Guava Cache` 和 `Spring Cache` 两者的话比较像。 - -`Guava` 相比于 `Spring Cache` 的话使用的更多一点,它提供了 API 非常方便我们使用,同时也提供了设置缓存有效时间等功能。它的内部实现也比较干净,很多地方都和 `ConcurrentHashMap` 的思想有异曲同工之妙。 - -使用 `Spring Cache` 的注解实现缓存的话,代码会看着很干净和优雅,但是很容易出现问题比如缓存穿透、内存溢出。 - -**三: 后起之秀 Caffeine。** - -相比于 `Guava` 来说 `Caffeine` 在各个方面比如性能要更加优秀,一般建议使用其来替代 `Guava` 。并且, `Guava` 和 `Caffeine` 的使用方式很像! - -本地缓存固然好,但是缺陷也很明显,比如多个相同服务之间的本地缓存的数据无法共享。 - -_下面我们从为什么要有分布式缓存为接入点来正式进入 Redis 的相关问题总结。_ - -### 4. 为什么要有分布式缓存?/为什么不直接用本地缓存? - -_我们可以把分布式缓存(Distributed Cache) 看作是一种内存数据库的服务,它的最终作用就是提供缓存数据的服务。_ - -如下图所示,就是一个简单的使用分布式缓存的架构图。我们使用 Nginx 来做负载均衡,部署两个相同的服务到服务器,两个服务使用同一个数据库和缓存。 - -![集中式缓存架构](./images/redis-all/集中式缓存架构.png) - -本地的缓存的优势是低依赖,比较轻量并且通常相比于使用分布式缓存要更加简单。 - -再来分析一下本地缓存的局限性: - -1. **本地缓存对分布式架构支持不友好**,比如同一个相同的服务部署在多台机器上的时候,各个服务之间的缓存是无法共享的,因为本地缓存只在当前机器上有。 -2. **本地缓存容量受服务部署所在的机器限制明显。** 如果当前系统服务所耗费的内存多,那么本地缓存可用的容量就很少。 - -使用分布式缓存之后,缓存部署在一台单独的服务器上,即使同一个相同的服务部署在再多机器上,也是使用的同一份缓存。 并且,单独的分布式缓存服务的性能、容量和提供的功能都要更加强大。 - -使用分布式缓存的缺点呢,也很显而易见,那就是你需要为分布式缓存引入额外的服务比如 Redis 或 Memcached,你需要单独保证 Redis 或 Memcached 服务的高可用。 - -### 5. 缓存读写模式/更新策略 - -**下面介绍到的三种模式各有优劣,不存在最佳模式,根据具体的业务场景选择适合自己的缓存读写模式。** - -#### 5.1. Cache Aside Pattern(旁路缓存模式) - -1. 写:更新 DB,然后直接删除 cache 。 -2. 读:从 cache 中读取数据,读取到就直接返回,读取不到的话,就从 DB 中取数据返回,然后再把数据放到 cache 中。 - -Cache Aside Pattern 中服务端需要同时维系 DB 和 cache,并且是以 DB 的结果为准。另外,Cache Aside Pattern 有首次请求数据一定不在 cache 的问题,对于热点数据可以提前放入缓存中。 - -**Cache Aside Pattern 是我们平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景。** - -#### 5.2. Read/Write Through Pattern(读写穿透) - -Read/Write Through 套路是:服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 DB,从而减轻了应用程序的职责。 - -1. 写(Write Through):先查 cache,cache 中不存在,直接更新 DB。 cache 中存在,则先更新 cache,然后 cache 服务自己更新 DB(**同步更新 cache 和 DB**)。 -2. 读(Read Through): 从 cache 中读取数据,读取到就直接返回 。读取不到的话,先从 DB 加载,写入到 cache 后返回响应。 - -Read-Through Pattern 实际只是在 Cache-Aside Pattern 之上进行了封装。在 Cache-Aside Pattern 下,发生读请求的时候,如果 cache 中不存在对应的数据,是由客户端自己负责把数据写入 cache,而 Read Through Pattern 则是 cache 服务自己来写入缓存的,这对客户端是透明的。 - -和 Cache Aside Pattern 一样, Read-Through Pattern 也有首次请求数据一定不再 cache 的问题,对于热点数据可以提前放入缓存中。 - -#### 5.3. Write Behind Pattern(异步缓存写入) - -Write Behind Pattern 和 Read/Write Through Pattern 很相似,两者都是由 cache 服务来负责 cache 和 DB 的读写。 - -但是,两个又有很大的不同:**Read/Write Through 是同步更新 cache 和 DB,而 Write Behind Caching 则是只更新缓存,不直接更新 DB,而是改为异步批量的方式来更新 DB。** - -**Write Behind Pattern 下 DB 的写性能非常高,尤其适合一些数据经常变化的业务场景比如说一篇文章的点赞数量、阅读数量。** 往常一篇文章被点赞 500 次的话,需要重复修改 500 次 DB,但是在 Write Behind Pattern 下可能只需要修改一次 DB 就可以了。 - -但是,这种模式同样也给 DB 和 Cache 一致性带来了新的考验,很多时候如果数据还没异步更新到 DB 的话,Cache 服务宕机就 gg 了。 \ No newline at end of file diff --git "a/docs/database/\344\272\213\345\212\241\351\232\224\347\246\273\347\272\247\345\210\253(\345\233\276\346\226\207\350\257\246\350\247\243).md" "b/docs/database/\344\272\213\345\212\241\351\232\224\347\246\273\347\272\247\345\210\253(\345\233\276\346\226\207\350\257\246\350\247\243).md" index 449b7fa7858..95da8be960d 100644 --- "a/docs/database/\344\272\213\345\212\241\351\232\224\347\246\273\347\272\247\345\210\253(\345\233\276\346\226\207\350\257\246\350\247\243).md" +++ "b/docs/database/\344\272\213\345\212\241\351\232\224\347\246\273\347\272\247\345\210\253(\345\233\276\346\226\207\350\257\246\350\247\243).md" @@ -80,11 +80,11 @@ mysql> SELECT @@tx_isolation; +-----------------+ ``` -这里需要注意的是:与 SQL 标准不同的地方在于InnoDB 存储引擎在 **REPEATABLE-READ(可重读)** 事务隔离级别下,允许应用使用 Next-Key Lock 锁算法来避免幻读的产生。这与其他数据库系统(如 SQL Server)是不同的。所以说虽然 InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)**,但是可以通过应用加锁读(例如 `select * from table for update` 语句)来保证不会产生幻读,而这个加锁度使用到的机制就是 Next-Key Lock 锁算法。从而达到了 SQL 标准的 **SERIALIZABLE(可串行化)** 隔离级别。 +这里需要注意的是:与 SQL 标准不同的地方在于InnoDB 存储引擎在 **REPEATABLE-READ(可重读)** 事务隔离级别下,允许应用使用 Next-Key Lock 锁算法来避免幻读的产生。这与其他数据库系统(如 SQL Server)是不同的。所以说虽然 InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)** ,但是可以通过应用加锁读(例如 `select * from table for update` 语句)来保证不会产生幻读,而这个加锁度使用到的机制就是 Next-Key Lock 锁算法。从而达到了 SQL 标准的 **SERIALIZABLE(可串行化)** 隔离级别。 -因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是**READ-COMMITTED(读取提交内容):**,但是你要知道的是InnoDB 存储引擎默认使用 **REPEATABLE-READ(可重读)**并不会有任何性能损失。 +因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是**READ-COMMITTED(读取提交内容):**,但是你要知道的是InnoDB 存储引擎默认使用 **REPEATABLE-READ(可重读)** 并不会有任何性能损失。 -InnoDB 存储引擎在 **分布式事务** 的情况下一般会用到**SERIALIZABLE(可串行化)**隔离级别。 +InnoDB 存储引擎在 **分布式事务** 的情况下一般会用到**SERIALIZABLE(可串行化)** 隔离级别。 ### 实际情况演示 diff --git "a/docs/database/\346\225\260\346\215\256\345\272\223\347\264\242\345\274\225.md" "b/docs/database/\346\225\260\346\215\256\345\272\223\347\264\242\345\274\225.md" index 568ba833fab..4e5a5657685 100644 --- "a/docs/database/\346\225\260\346\215\256\345\272\223\347\264\242\345\274\225.md" +++ "b/docs/database/\346\225\260\346\215\256\345\272\223\347\264\242\345\274\225.md" @@ -6,7 +6,7 @@ ## 为什么要用索引?索引的优缺点分析 ### 索引的优点 -**可以大大加快 数据的检索速度(大大减少的检索的数据量), 这也是创建索引的最主要的原因。毕竟大部分系统的读请求总是大于写请求的。 ** 另外,通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。 +**可以大大加快 数据的检索速度(大大减少的检索的数据量), 这也是创建索引的最主要的原因。毕竟大部分系统的读请求总是大于写请求的。** 另外,通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。 ### 索引的缺点 1. **创建索引和维护索引需要耗费许多时间**:当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态的修改,会降低SQL执行效率。 @@ -58,7 +58,7 @@ B+树是有序的,在这种范围查询中,优势非常大,直接遍历比 **PS:不懂的同学可以暂存疑,慢慢往下看,后面会有答案的,也可以自行搜索。** -1. **唯一索引(Unique Key)** :唯一索引也是一种约束。**唯一索引的属性列不能出现重复的数据,但是允许数据为NULL,一张表允许创建多个唯一索引。**建立唯一索引的目的大部分时候都是为了该属性列的数据的唯一性,而不是为了查询效率。 +1. **唯一索引(Unique Key)** :唯一索引也是一种约束。**唯一索引的属性列不能出现重复的数据,但是允许数据为NULL,一张表允许创建多个唯一索引。** 建立唯一索引的目的大部分时候都是为了该属性列的数据的唯一性,而不是为了查询效率。 2. **普通索引(Index)** :**普通索引的唯一作用就是为了快速查询数据,一张表允许创建多个普通索引,并允许数据重复和NULL。** 3. **前缀索引(Prefix)** :前缀索引只适用于字符串类型的数据。前缀索引是对文本的前几个字符创建索引,相比普通索引建立的数据更小, 因为只取前几个字符。 @@ -222,4 +222,4 @@ ALTER TABLE table ADD INDEX index_name (num,name,age) ### 使用索引一定能提高查询性能吗? -大多数情况下,索引查询都是比全表扫描要快的。但是如果数据库的数据量不大,那么使用索引也不一定能够带来很大提升。 \ No newline at end of file +大多数情况下,索引查询都是比全表扫描要快的。但是如果数据库的数据量不大,那么使用索引也不一定能够带来很大提升。 diff --git "a/docs/essential-content-for-interview/PreparingForInterview/\347\276\216\345\233\242\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\346\200\273\347\273\223.md" "b/docs/essential-content-for-interview/PreparingForInterview/\347\276\216\345\233\242\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\346\200\273\347\273\223.md" index 869416ce018..c3487bf35d0 100644 --- "a/docs/essential-content-for-interview/PreparingForInterview/\347\276\216\345\233\242\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\346\200\273\347\273\223.md" +++ "b/docs/essential-content-for-interview/PreparingForInterview/\347\276\216\345\233\242\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\346\200\273\347\273\223.md" @@ -106,7 +106,7 @@ request.getRequestDispatcher("login_success.jsp").forward(request, response); ``` -**重定向(Redirect)** 是利用服务器返回的状态码来实现的。客户端浏览器请求服务器的时候,服务器会返回一个状态码。服务器通过 HttpServletRequestResponse 的 setStatus(int status)方法设置状态码。如果服务器返回 301 或者 302,则浏览器会到新的网址重新请求该资源。 +**重定向(Redirect)** 是利用服务器返回的状态码来实现的。客户端浏览器请求服务器的时候,服务器会返回一个状态码。服务器通过 HttpServletResponse 的 setStatus(int status)方法设置状态码。如果服务器返回 301 或者 302,则浏览器会到新的网址重新请求该资源。 1. **从地址栏显示来说**:forward 是服务器请求资源,服务器直接访问目标地址的 URL,把那个 URL 的响应内容读取过来,然后把这些内容再发给浏览器。浏览器根本不知道服务器发送的内容从哪里来的,所以它的地址栏还是原来的地址。redirect 是服务端根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址。所以地址栏显示的是新的 URL。 2. **从数据共享来说**:forward:转发页面和转发到的页面可以共享 request 里面的数据。redirect:不能共享数据。 @@ -493,7 +493,7 @@ public interface RandomAccess { 查看源码我们发现实际上 RandomAccess 接口中什么都没有定义。所以,在我看来 RandomAccess 接口不过是一个标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。 -在 binarySearch() 方法中,它要判断传入的 list 是否 RamdomAccess 的实例,如果是,调用 indexedBinarySearch() 方法,如果不是,那么调用 iteratorBinarySearch() 方法 +在 binarySearch() 方法中,它要判断传入的 list 是否 RandomAccess 的实例,如果是,调用 indexedBinarySearch() 方法,如果不是,那么调用 iteratorBinarySearch() 方法 ```java public static diff --git "a/docs/essential-content-for-interview/\351\235\242\350\257\225\345\277\205\345\244\207\344\271\213\344\271\220\350\247\202\351\224\201\344\270\216\346\202\262\350\247\202\351\224\201.md" "b/docs/essential-content-for-interview/\351\235\242\350\257\225\345\277\205\345\244\207\344\271\213\344\271\220\350\247\202\351\224\201\344\270\216\346\202\262\350\247\202\351\224\201.md" index 00aaecd8c5b..0ed56e12595 100644 --- "a/docs/essential-content-for-interview/\351\235\242\350\257\225\345\277\205\345\244\207\344\271\213\344\271\220\350\247\202\351\224\201\344\270\216\346\202\262\350\247\202\351\224\201.md" +++ "b/docs/essential-content-for-interview/\351\235\242\350\257\225\345\277\205\345\244\207\344\271\213\344\271\220\350\247\202\351\224\201\344\270\216\346\202\262\350\247\202\351\224\201.md" @@ -48,8 +48,8 @@ 1. 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。 2. 在操作员 A 操作的过程中,操作员B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 )。 -3. 操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。 -4. 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。 +3. 操作员 A 完成了修改工作,将数据版本号( version=1 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本等于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。 +4. 操作员 B 完成了操作,也将版本号( version=1 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 1 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须等于当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。 这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能。 @@ -83,14 +83,14 @@ JDK 1.5 以后的 `AtomicStampedReference 类`就提供了此种能力,其中 CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了`AtomicReference类`来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用`AtomicReference类`把多个共享变量合并成一个共享变量来操作。 -### CAS与synchronized的使用情景 +### CAS与`synchronized`的使用情景 > **简单的来说CAS适用于写比较少的情况下(多读场景,冲突一般较少),synchronized适用于写比较多的情况下(多写场景,冲突一般较多)** -1. 对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。 +1. 对于资源竞争较少(线程冲突较轻)的情况,使用`synchronized`同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。 2. 对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。 -补充: Java并发编程这个领域中synchronized关键字一直都是元老级的角色,很久之前很多人都会称它为 **“重量级锁”** 。但是,在JavaSE 1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的 **偏向锁** 和 **轻量级锁** 以及其它**各种优化**之后变得在某些情况下并不是那么重了。synchronized的底层实现主要依靠 **Lock-Free** 的队列,基本思路是 **自旋后阻塞**,**竞争切换后继续竞争锁**,**稍微牺牲了公平性,但获得了高吞吐量**。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。 +补充: Java并发编程这个领域中`synchronized`关键字一直都是元老级的角色,很久之前很多人都会称它为 **“重量级锁”** 。但是,在JavaSE 1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的 **偏向锁** 和 **轻量级锁** 以及其它**各种优化**之后变得在某些情况下并不是那么重了。`synchronized`的底层实现主要依靠 **Lock-Free** 的队列,基本思路是 **自旋后阻塞**,**竞争切换后继续竞争锁**,**稍微牺牲了公平性,但获得了高吞吐量**。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。 ## 公众号 diff --git a/docs/github-trending/2018-12.md b/docs/github-trending/2018-12.md deleted file mode 100644 index 3637e93ea0c..00000000000 --- a/docs/github-trending/2018-12.md +++ /dev/null @@ -1,78 +0,0 @@ -本文数据统计于 1.1 号凌晨,由 SnailClimb 整理。 - -### 1. JavaGuide - -- **Github地址**: [https://github.com/Snailclimb/JavaGuide](https://github.com/Snailclimb/JavaGuide) -- **star**: 18.2k -- **介绍**: 【Java学习+面试指南】 一份涵盖大部分Java程序员所需要掌握的核心知识。 - -### 2. mall - -- **Github地址**: [https://github.com/macrozheng/mall](https://github.com/macrozheng/mall) -- **star**: 3.3k -- **介绍**: mall项目是一套电商系统,包括前台商城系统及后台管理系统,基于SpringBoot+MyBatis实现。 前台商城系统包含首页门户、商品推荐、商品搜索、商品展示、购物车、订单流程、会员中心、客户服务、帮助中心等模块。 后台管理系统包含商品管理、订单管理、会员管理、促销管理、运营管理、内容管理、统计报表、财务管理、权限管理、设置等模块。 - -### 3. advanced-java - -- **Github地址**:[https://github.com/doocs/advanced-java](https://github.com/doocs/advanced-java) -- **star**: 3.3k -- **介绍**: 互联网 Java 工程师进阶知识完全扫盲 - -### 4. matrix - -- **Github地址**:[https://github.com/Tencent/matrix](https://github.com/Tencent/matrix) -- **star**: 2.5k -- **介绍**: Matrix 是一款微信研发并日常使用的 APM(Application Performance Manage),当前主要运行在 Android 平台上。 Matrix 的目标是建立统一的应用性能接入框架,通过各种性能监控方案,对性能监控项的异常数据进行采集和分析,输出相应的问题分析、定位与优化建议,从而帮助开发者开发出更高质量的应用。 - -### 5. miaosha - -- **Github地址**:[https://github.com/qiurunze123/miaosha](https://github.com/qiurunze123/miaosha) -- **star**: 2.4k -- **介绍**: 高并发大流量如何进行秒杀架构,我对这部分知识做了一个系统的整理,写了一套系统。 - -### 6. arthas - -- **Github地址**:[https://github.com/alibaba/arthas](https://github.com/alibaba/arthas) -- **star**: 8.2k -- **介绍**: Arthas 是Alibaba开源的Java诊断工具,深受开发者喜爱。 - -### 7 spring-boot - -- **Github地址**: [https://github.com/spring-projects/spring-boot](https://github.com/spring-projects/spring-boot) -- **star:** 32.6k -- **介绍**: 虽然Spring的组件代码是轻量级的,但它的配置却是重量级的(需要大量XML配置),不过Spring Boot 让这一切成为了过去。 另外Spring Cloud也是基于Spring Boot构建的,我个人非常有必要学习一下。 - - **关于Spring Boot官方的介绍:** - - > Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can “just run”…Most Spring Boot applications need very little Spring configuration.(Spring Boot可以轻松创建独立的生产级基于Spring的应用程序,只要通过 “just run”(可能是run ‘Application’或java -jar 或 tomcat 或 maven插件run 或 shell脚本)便可以运行项目。大部分Spring Boot项目只需要少量的配置即可) - -### 8. tutorials - -- **Github地址**:[https://github.com/eugenp/tutorials](https://github.com/eugenp/tutorials) -- **star**: 10k -- **介绍**: 该项目是一系列小而专注的教程 - 每个教程都涵盖Java生态系统中单一且定义明确的开发领域。 当然,它们的重点是Spring Framework - Spring,Spring Boot和Spring Securiyt。 除了Spring之外,还有以下技术:核心Java,Jackson,HttpClient,Guava。 - -### 9. qmq - -- **Github地址**:[https://github.com/qunarcorp/qmq](https://github.com/qunarcorp/qmq) -- **star**: 1.1k -- **介绍**: QMQ是去哪儿网内部广泛使用的消息中间件,自2012年诞生以来在去哪儿网所有业务场景中广泛的应用,包括跟交易息息相关的订单场景; 也包括报价搜索等高吞吐量场景。 - - -### 10. symphony - -- **Github地址**:[https://github.com/b3log/symphony](https://github.com/b3log/symphony) -- **star**: 9k -- **介绍**: 一款用 Java 实现的现代化社区(论坛/BBS/社交网络/博客)平台。 - -### 11. incubator-dubbo - -- **Github地址**:[https://github.com/apache/incubator-dubbo](https://github.com/apache/incubator-dubbo) -- **star**: 23.6k -- **介绍**: 阿里开源的一个基于Java的高性能开源RPC框架。 - -### 12. apollo - -- **Github地址**:[https://github.com/ctripcorp/apollo](https://github.com/ctripcorp/apollo) -- **star**: 10k -- **介绍**: Apollo(阿波罗)是携程框架部门研发的分布式配置中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景。 diff --git a/docs/github-trending/2019-1.md b/docs/github-trending/2019-1.md deleted file mode 100644 index aa1de92f623..00000000000 --- a/docs/github-trending/2019-1.md +++ /dev/null @@ -1,76 +0,0 @@ -### 1. JavaGuide - -- **Github地址**: [https://github.com/Snailclimb/JavaGuide](https://github.com/Snailclimb/JavaGuide) -- **star**: 22.8k -- **介绍**: 【Java学习+面试指南】 一份涵盖大部分Java程序员所需要掌握的核心知识。 - -### 2. advanced-java - -- **Github地址**:[https://github.com/doocs/advanced-java](https://github.com/doocs/advanced-java) -- **star**: 7.9k -- **介绍**: 互联网 Java 工程师进阶知识完全扫盲 - -### 3. fescar - -- **Github地址**:[https://github.com/alibaba/fescar](https://github.com/alibaba/fescar) -- **star**: 4.6k -- **介绍**: 具有 **高性能** 和 **易用性** 的 **微服务架构** 的 **分布式事务** 的解决方案。(特点:高性能且易于使用,旨在实现简单并快速的事务提交与回滚。 - -### 4. mall - -- **Github地址**: [https://github.com/macrozheng/mall](https://github.com/macrozheng/mall) -- **star**: 5.6 k -- **介绍**: mall项目是一套电商系统,包括前台商城系统及后台管理系统,基于SpringBoot+MyBatis实现。 前台商城系统包含首页门户、商品推荐、商品搜索、商品展示、购物车、订单流程、会员中心、客户服务、帮助中心等模块。 后台管理系统包含商品管理、订单管理、会员管理、促销管理、运营管理、内容管理、统计报表、财务管理、权限管理、设置等模块。 - -### 5. miaosha - -- **Github地址**:[https://github.com/qiurunze123/miaosha](https://github.com/qiurunze123/miaosha) -- **star**: 4.4k -- **介绍**: 高并发大流量如何进行秒杀架构,我对这部分知识做了一个系统的整理,写了一套系统。 - -### 6. flink - -- **Github地址**:[https://github.com/apache/flink](https://github.com/apache/flink) -- **star**: 7.1 k -- **介绍**: Apache Flink是一个开源流处理框架,具有强大的流和批处理功能。 - -### 7. cim - -- **Github地址**:[https://github.com/crossoverJie/cim](https://github.com/crossoverJie/cim) -- **star**: 1.8 k -- **介绍**: cim(cross IM) 适用于开发者的即时通讯系统。 - -### 8. symphony - -- **Github地址**:[https://github.com/b3log/symphony](https://github.com/b3log/symphony) -- **star**: 10k -- **介绍**: 一款用 Java 实现的现代化社区(论坛/BBS/社交网络/博客)平台。 - -### 9. spring-boot - -- **Github地址**: [https://github.com/spring-projects/spring-boot](https://github.com/spring-projects/spring-boot) -- **star:** 32.6k -- **介绍**: 虽然Spring的组件代码是轻量级的,但它的配置却是重量级的(需要大量XML配置),不过Spring Boot 让这一切成为了过去。 另外Spring Cloud也是基于Spring Boot构建的,我个人非常有必要学习一下。 - - **关于Spring Boot官方的介绍:** - - > Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can “just run”…Most Spring Boot applications need very little Spring configuration.(Spring Boot可以轻松创建独立的生产级基于Spring的应用程序,只要通过 “just run”(可能是run ‘Application’或java -jar 或 tomcat 或 maven插件run 或 shell脚本)便可以运行项目。大部分Spring Boot项目只需要少量的配置即可) - -### 10. arthas - -- **Github地址**:[https://github.com/alibaba/arthas](https://github.com/alibaba/arthas) -- **star**: 9.5k -- **介绍**: Arthas 是Alibaba开源的Java诊断工具。 - -**概览:** - -当你遇到以下类似问题而束手无策时,`Arthas`可以帮助你解决: - -0. 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception? -1. 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了? -2. 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗? -3. 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现! -4. 是否有一个全局视角来查看系统的运行状况? -5. 有什么办法可以监控到JVM的实时运行状态? - -`Arthas`支持JDK 6+,支持Linux/Mac/Winodws,采用命令行交互模式,同时提供丰富的 `Tab` 自动补全功能,进一步方便进行问题的定位和诊断。 diff --git a/docs/github-trending/2019-12.md b/docs/github-trending/2019-12.md deleted file mode 100644 index 9a8f79ed6e8..00000000000 --- a/docs/github-trending/2019-12.md +++ /dev/null @@ -1,144 +0,0 @@ -# 年末将至,值得你关注的16个Java 开源项目! - -Star 的数量统计于 2019-12-29。 - -### 1.JavaGuide - -Guide 哥大三开始维护的,目前算是纯 Java 类型项目中 Star 数量最多的项目了。但是,本仓库的价值远远(+N次 )比不上像 Spring Boot、Elasticsearch 等等这样非常非常非常优秀的项目。希望以后我也有能力为这些项目贡献一些有价值的代码。 - -- **Github 地址**: -- **Star**: 66.3k -- **介绍**: 【Java 学习+面试指南】 一份涵盖大部分 Java 程序员所需要掌握的核心知识。 - -### 2.java-design-patterns - -感觉还不错。根据官网介绍: - -> 设计模式是程序员在设计应用程序或系统时可以用来解决常见问题的最佳形式化实践。 设计模式可以通过提供经过测试的,经过验证的开发范例来加快开发过程。 重用设计模式有助于防止引起重大问题的细微问题,并且还可以提高熟悉模式的编码人员和架构师的代码可读性。 - -![java-design-patterns-website](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/java-design-patterns-website.jpg) - -- **Github 地址** : [https://github.com/iluwatar/java-design-patterns](https://github.com/iluwatar/java-design-patterns) -- **Star**: 53.8k -- **介绍**: 用 Java 实现的设计模式。[https://java-design-patterns.com](https://java-design-patterns.com/)。 - -### 3.elasticsearch - -搜索引擎界的扛把子,但不仅仅是搜素引擎那么简单。 - -- **Github 地址** : [https://github.com/elastic/elasticsearch](https://github.com/elastic/elasticsearch) -- **Star**: 46.2k -- **介绍**: 开源,分布式,RESTful 搜索引擎。 - -### 4.spring-boot - -必须好好学啊,一定要好好学!现在 Java 后端新项目有不用 Spring Boot 开发的有吗?如果有的话,请把这个人的联系方式告诉我,我有很多话想给他交流交流! - -- **Github地址**: [https://github.com/spring-projects/spring-boot](https://github.com/spring-projects/spring-boot) -- **star:** 34.8k (1,073 stars this month) -- **介绍**: 虽然Spring的组件代码是轻量级的,但它的配置却是重量级的(需要大量XML配置),不过Spring Boot 让这一切成为了过去。 另外Spring Cloud也是基于Spring Boot构建的,我个人非常有必要学习一下。 - -### 5.RxJava - -这个没怎么用过,不做太多评价。 - -- **Github 地址** : [https://github.com/ReactiveX/RxJava](https://github.com/ReactiveX/RxJava) -- **Star**: 41.5k -- **介绍**: `RxJava` 是一个 基于事件流、实现异步操作的库。 - -### 6.advanced-java - -本项目大部分内容来自中华石杉的一个课程,内容涵盖高并发、分布式、高可用、微服务、海量数据处理等领域知识,非常不错了! - -- **Github 地址**:[https://github.com/doocs/advanced-java](https://github.com/doocs/advanced-java) -- **Star**: 36.7k -- **介绍**: 互联网 Java 工程师进阶知识完全扫盲:涵盖高并发、分布式、高可用、微服务等领域知识,后端同学必看,前端同学也可学习。 - -### 7.mall - -很牛逼的实战项目,还附有详细的文档,作为毕设或者练手项目都再好不过了。 - -- **Github地址**: [https://github.com/macrozheng/mall](https://github.com/macrozheng/mall) -- **star**: 27.6k -- **介绍**: mall项目是一套电商系统,包括前台商城系统及后台管理系统,基于SpringBoot+MyBatis实现。 前台商城系统包含首页门户、商品推荐、商品搜索、商品展示、购物车、订单流程、会员中心、客户服务、帮助中心等模块。 后台管理系统包含商品管理、订单管理、会员管理、促销管理、运营管理、内容管理、统计报表、财务管理、权限管理、设置等模块。 - -### 8.okhttp - -给我感觉是安卓项目中用的居多。当然,Java 后端项目也会经常用,但是一般使用 Spring Boot 进行开发的时候,如果需要远程调用的话建议使用 Spring 封装的 `RestTemplate `。 - -- **Github地址**:[https://github.com/square/okhttp](https://github.com/square/okhttp) -- **star**: 35.4k -- **介绍**: 适用于Android,Kotlin和Java的HTTP客户端。https://square.github.io/okhttp/。 - -### 9.guava - -很厉害很厉害!提供了很多非常实用的工具类、更加实用的集合类、一些常用的数据结构比如布隆过滤器、缓存等等。 - -- **Github地址**:[https://github.com/google/guava](https://github.com/google/guava) -- **star**: 35.3k -- **介绍**: Guava是一组核心库,其中包括新的集合类型(例如 multimap 和 multiset),不可变集合,图形库以及用于并发,I / O,哈希,基元,字符串等的实用程序! - -### 10.Spark - -我木有用过,留下了没有技术的眼泪。 - -- **Github地址**:[https://github.com/apache/spark](https://github.com/apache/spark) -- **star**: 24.7k -- **介绍**: Spark 是一个快速、通用的大规模数据处理引擎,和Hadoop的MapReduce计算框架类似,但是相对于MapReduce,Spark凭借其可伸缩、基于内存计算等特点,以及可以直接读写Hadoop上任何格式数据的优势,进行批处理时更加高效,并有更低的延迟。 - -### 11.arthas - -虽然我自己没有亲身用过,但是身边用过的朋友评价都还挺好的。根据官网介绍,这家伙可以解决下面这些让人脑壳疼的问题: - -1. 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception? -2. 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了? -3. 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗? -4. 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现! -5. 是否有一个全局视角来查看系统的运行状况? -6. 有什么办法可以监控到JVM的实时运行状态? -7. 怎么快速定位应用的热点,生成火焰图? - -- **Github 地址**:[https://github.com/alibaba/arthas](https://github.com/alibaba/arthas) -- **star**: 18.8 k -- **介绍**: Arthas 是 Alibaba 开源的 Java 诊断工具。 - -### 12.spring-boot-examples - -学习 Spring Boot 必备!配合上我的 **springboot-guide** :[https://github.com/Snailclimb/springboot-guide](https://github.com/Snailclimb/springboot-guide),效果杠杠滴! - -- **Github 地址**:[https://github.com/ityouknow/spring-boot-examples](https://github.com/ityouknow/spring-boot-examples) -- **star**: 20.2 k -- **介绍**: Spring Boot 教程、技术栈示例代码,快速简单上手教程。 - -### 13.lombok - -使用 Lombok 我们可以简化我们的 Java 代码,比如使用它之后我们通过注释就可以实现 getter/setter、equals等方法。 - -- **Github 地址**:[https://github.com/rzwitserloot/lombok](https://github.com/rzwitserloot/lombok) -- **star**: 20.2 k -- **介绍**: 对 Java 编程语言的非常刺激的补充。[https://projectlombok.org/](https://projectlombok.org/) 。 - -### 14.p3c - -与我而言,没有特别惊艳,但是一些提供的一些代码规范确实挺有用的! - -- **Github 地址**:[https://github.com/alibaba/p3c](https://github.com/alibaba/p3c) -- **star**: 19.8 k -- **介绍**: 阿里巴巴Java编码指南pmd实现和IDE插件。 - -### 15.spring-boot-demo - -- **Github 地址**:[https://github.com/xkcoding/spring-boot-demo](https://github.com/xkcoding/spring-boot-demo) -- **Star**: 8.8k -- **介绍**: spring boot demo 是一个用来深度学习并实战 spring boot 的项目。 - -### 16. awesome-java - -Guide 哥半个多月前开始维护的,虽然现在 Star 数量比较少,我相信后面一定会有更多人喜欢上这个项目,我也会继续认真维护下去。 - -- **Github 地址**:[https://github.com/Snailclimb/awesome-java](https://github.com/Snailclimb/awesome-java) -- **Star**: 0.3 k -- **介绍**: Github 上非常棒的 Java 开源项目集合。 - - - diff --git a/docs/github-trending/2019-2.md b/docs/github-trending/2019-2.md deleted file mode 100644 index 51d34b32f78..00000000000 --- a/docs/github-trending/2019-2.md +++ /dev/null @@ -1,64 +0,0 @@ -### 1. JavaGuide - -- **Github地址**: [https://github.com/Snailclimb/JavaGuide](https://github.com/Snailclimb/JavaGuide) -- **Star**: 27.2k (4,437 stars this month) -- **介绍**: 【Java学习+面试指南】 一份涵盖大部分Java程序员所需要掌握的核心知识。 - -### 2.DoraemonKit - -- **Github地址**: -- **Star**: 5.2k (3,786 stars this month) -- **介绍**: 简称 "DoKit" 。一款功能齐全的客户端( iOS 、Android )研发助手,你值得拥有。 - -### 3.advanced-java - -- **Github地址**:[https://github.com/doocs/advanced-java](https://github.com/doocs/advanced-java) -- **Star**:11.2k (3,042 stars this month) -- **介绍**: 互联网 Java 工程师进阶知识完全扫盲。 - -### 4. spring-boot-examples - -- **Github地址**: -- **star**: 9.6 k (1,764 stars this month) -- **介绍**: Spring Boot 教程、技术栈示例代码,快速简单上手教程。 - -### 5. mall - -- **Github地址**: [https://github.com/macrozheng/mall](https://github.com/macrozheng/mall) -- **star**: 7.4 k (1,736 stars this month) -- **介绍**: mall项目是一套电商系统,包括前台商城系统及后台管理系统,基于SpringBoot+MyBatis实现。 前台商城系统包含首页门户、商品推荐、商品搜索、商品展示、购物车、订单流程、会员中心、客户服务、帮助中心等模块。 后台管理系统包含商品管理、订单管理、会员管理、促销管理、运营管理、内容管理、统计报表、财务管理、权限管理、设置等模块。 - -### 6. fescar - -- **Github地址**:[https://github.com/alibaba/fescar](https://github.com/alibaba/fescar) -- **star**: 6.0 k (1,308 stars this month) -- **介绍**: 具有 **高性能** 和 **易用性** 的 **微服务架构** 的 **分布式事务** 的解决方案。(特点:高性能且易于使用,旨在实现简单并快速的事务提交与回滚。) - -### 7. h4cker - -- **Github地址**: -- **star**: 2.1 k (1,303 stars this month) -- **介绍**: 该仓库主要由Omar Santos维护,包括与道德黑客/渗透测试,数字取证和事件响应(DFIR),漏洞研究,漏洞利用开发,逆向工程等相关的资源。 - -### 8. spring-boot - -- **Github地址**: [https://github.com/spring-projects/spring-boot](https://github.com/spring-projects/spring-boot) -- **star:** 34.8k (1,073 stars this month) -- **介绍**: 虽然Spring的组件代码是轻量级的,但它的配置却是重量级的(需要大量XML配置),不过Spring Boot 让这一切成为了过去。 另外Spring Cloud也是基于Spring Boot构建的,我个人非常有必要学习一下。 - - **关于Spring Boot官方的介绍:** - - > Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can “just run”…Most Spring Boot applications need very little Spring configuration.(Spring Boot可以轻松创建独立的生产级基于Spring的应用程序,只要通过 “just run”(可能是run ‘Application’或java -jar 或 tomcat 或 maven插件run 或 shell脚本)便可以运行项目。大部分Spring Boot项目只需要少量的配置即可) - -### 9. arthas - -- **Github地址**:[https://github.com/alibaba/arthas](https://github.com/alibaba/arthas) -- **star**: 10.5 k (970 stars this month) -- **介绍**: Arthas 是Alibaba开源的Java诊断工具。 - -### 10. tutorials - -- **Github地址**:[https://github.com/eugenp/tutorials](https://github.com/eugenp/tutorials) -- **star**: 12.1 k (789 stars this month) -- **介绍**: 该项目是一系列小而专注的教程 - 每个教程都涵盖Java生态系统中单一且定义明确的开发领域。 当然,它们的重点是Spring Framework - Spring,Spring Boot和Spring Securiyt。 除了Spring之外,还有以下技术:核心Java,Jackson,HttpClient,Guava。 - diff --git a/docs/github-trending/2019-3.md b/docs/github-trending/2019-3.md deleted file mode 100644 index eaed4a5d483..00000000000 --- a/docs/github-trending/2019-3.md +++ /dev/null @@ -1,60 +0,0 @@ -### 1. JavaGuide - -- **Github 地址**: [https://github.com/Snailclimb/JavaGuide](https://github.com/Snailclimb/JavaGuide) -- **Star**: 32.9k (6,196 stars this month) -- **介绍**: 【Java 学习+面试指南】 一份涵盖大部分 Java 程序员所需要掌握的核心知识。 - -### 2.advanced-java - -- **Github 地址**:[https://github.com/doocs/advanced-java](https://github.com/doocs/advanced-java) -- **Star**: 15.1k (4,012 stars this month) -- **介绍**: 互联网 Java 工程师进阶知识完全扫盲。 - -### 3.spring-boot-examples - -- **Github 地址**:[https://github.com/ityouknow/spring-boot-examples](https://github.com/ityouknow/spring-boot-examples) -- **Star**: 12.8k (3,462 stars this month) -- **介绍**: Spring Boot 教程、技术栈示例代码,快速简单上手教程。 - -### 4. mall - -- **Github 地址**: [https://github.com/macrozheng/mall](https://github.com/macrozheng/mall) -- **star**: 9.7 k (2,418 stars this month) -- **介绍**: mall 项目是一套电商系统,包括前台商城系统及后台管理系统,基于 SpringBoot+MyBatis 实现。 前台商城系统包含首页门户、商品推荐、商品搜索、商品展示、购物车、订单流程、会员中心、客户服务、帮助中心等模块。 后台管理系统包含商品管理、订单管理、会员管理、促销管理、运营管理、内容管理、统计报表、财务管理、权限管理、设置等模块。 - -### 5. seata - -- **Github 地址** : [https://github.com/seata/seata](https://github.com/seata/seata) -- **star**: 7.2 k (1359 stars this month) -- **介绍**: Seata 是一种易于使用,高性能,基于 Java 的开源分布式事务解决方案。 - -### 6. quarkus - -- **Github 地址**:[https://github.com/quarkusio/quarkus](https://github.com/quarkusio/quarkus) -- **star**: 12 k (1,224 stars this month) -- **介绍**: Quarkus 是为 GraalVM 和 HotSpot 量身定制的 Kubernetes Native Java 框架,由最佳的 Java 库和标准精心打造而成。Quarkus 的目标是使 Java 成为 Kubernetes 和无服务器环境中的领先平台,同时为开发人员提供统一的反应式和命令式编程模型,以优化地满足更广泛的分布式应用程序架构。 - -### 7. arthas - -- **Github 地址**:[https://github.com/alibaba/arthas](https://github.com/alibaba/arthas) -- **star**: 11.6 k (1,199 stars this month) -- **介绍**: Arthas 是 Alibaba 开源的 Java 诊断工具。 - -### 8.DoraemonKit - -- **Github 地址**: -- **Star**: 6.2k (1,177 stars this month) -- **介绍**: 简称 "DoKit" 。一款功能齐全的客户端( iOS 、Android )研发助手,你值得拥有。 - -### 9.elasticsearch - -- **Github 地址** [https://github.com/elastic/elasticsearch](https://github.com/elastic/elasticsearch) -- **Star**: 39.7k (1,069 stars this month) -- **介绍**: 开源,分布式,RESTful 搜索引擎。 - -### 10. tutorials - -- **Github 地址**:[https://github.com/eugenp/tutorials](https://github.com/eugenp/tutorials) -- **star**: 13 k (998 stars this month) -- **介绍**: 该项目是一系列小而专注的教程 - 每个教程都涵盖 Java 生态系统中单一且定义明确的开发领域。 当然,它们的重点是 Spring Framework - Spring,Spring Boot 和 Spring Securiyt。 除了 Spring 之外,还有以下技术:核心 Java,Jackson,HttpClient,Guava。 - diff --git a/docs/github-trending/2019-4.md b/docs/github-trending/2019-4.md deleted file mode 100644 index 713a76da642..00000000000 --- a/docs/github-trending/2019-4.md +++ /dev/null @@ -1,98 +0,0 @@ -以下涉及到的数据统计与 2019 年 5 月 1 日 12 点,数据来源: 。 - -下面的内容从 Java 学习文档到最热门的框架再到热门的工具应有尽有,比如下面推荐到的开源项目 Hutool 就是近期比较热门的项目之一,它是 Java 工具包,能够帮助我们简化代码!我觉得下面这些项目对于学习 Java 的朋友还是很有帮助的! - - -### 1. JavaGuide - -- **Github 地址**: [https://github.com/Snailclimb/JavaGuide](https://github.com/Snailclimb/JavaGuide) -- **Star**: 37.9k (5,660 stars this month) -- **介绍**: 【Java 学习+面试指南】 一份涵盖大部分 Java 程序员所需要掌握的核心知识。 - -### 2. advanced-java - -- **Github 地址**:[https://github.com/doocs/advanced-java](https://github.com/doocs/advanced-java) -- **Star**: 15.1k (4,654 stars this month) -- **介绍**: 互联网 Java 工程师进阶知识完全扫盲。 - -### 3. CS-Notes - -- **Github 地址**: -- **Star**: 59.2k (4,012 stars this month) -- **介绍**: 技术面试必备基础知识。 - -### 4. ghidra - -- **Github 地址**: -- **Star**: 15.0k (2,995 stars this month) -- **介绍**: Ghidra是一个软件逆向工程(SRE)框架。 - -### 5. mall - -- **Github 地址**: [https://github.com/macrozheng/mall](https://github.com/macrozheng/mall) -- **star**: 11.6 k (2,100 stars this month) -- **介绍**: mall 项目是一套电商系统,包括前台商城系统及后台管理系统,基于 SpringBoot+MyBatis 实现。 前台商城系统包含首页门户、商品推荐、商品搜索、商品展示、购物车、订单流程、会员中心、客户服务、帮助中心等模块。 后台管理系统包含商品管理、订单管理、会员管理、促销管理、运营管理、内容管理、统计报表、财务管理、权限管理、设置等模块。 - -### 6. ZXBlog - -- **Github 地址**: -- **star**: 2.1 k (2,086 stars this month) -- **介绍**: 记录各种学习笔记(算法、Java、数据库、并发......)。 - -### 7.DoraemonKit - -- **Github地址**: -- **Star**: 7.6k (1,541 stars this month) -- **介绍**: 简称 "DoKit" 。一款功能齐全的客户端( iOS 、Android )研发助手,你值得拥有。 - -### 8. spring-boot - -- **Github地址**: [https://github.com/spring-projects/spring-boot](https://github.com/spring-projects/spring-boot) -- **star:** 37.3k (1,489 stars this month) -- **介绍**: 虽然Spring的组件代码是轻量级的,但它的配置却是重量级的(需要大量XML配置),不过Spring Boot 让这一切成为了过去。 另外Spring Cloud也是基于Spring Boot构建的,我个人非常有必要学习一下。 - -**Spring Boot官方的介绍:** - -> Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can “just run”…Most Spring Boot applications need very little Spring configuration.(Spring Boot可以轻松创建独立的生产级基于Spring的应用程序,只要通过 “just run”(可能是run ‘Application’或java -jar 或 tomcat 或 maven插件run 或 shell脚本)便可以运行项目。大部分Spring Boot项目只需要少量的配置即可) - -### 9. spring-boot-examples - -- **Github 地址**:[https://github.com/ityouknow/spring-boot-examples](https://github.com/ityouknow/spring-boot-examples) -- **Star**: 12.8k (1,453 stars this month) -- **介绍**: Spring Boot 教程、技术栈示例代码,快速简单上手教程。 - -### 10. seata - -- **Github 地址** : [https://github.com/seata/seata](https://github.com/seata/seata) -- **star**: 8.4 k (1441 stars this month) -- **介绍**: Seata 是一种易于使用,高性能,基于 Java 的开源分布式事务解决方案。 - -### 11. litemall - -- **Github 地址**:[https://github.com/ityouknow/spring-boot-examples](https://github.com/ityouknow/spring-boot-examples) -- **Star**: 6.0k (1,427 stars this month) -- **介绍**: 又一个小商城。litemall = Spring Boot后端 + Vue管理员前端 + 微信小程序用户前端 + Vue用户移动端。 - -### 12. skywalking - -- **Github 地址**: -- **Star**: 8.0k (1,381 stars this month) -- **介绍**: 针对分布式系统的应用性能监控,尤其是针对微服务、云原生和面向容器的分布式系统架构。 - -### 13. elasticsearch - -- **Github 地址** [https://github.com/elastic/elasticsearch](https://github.com/elastic/elasticsearch) -- **Star**: 4.0k (1,068stars this month) -- **介绍**: 开源,分布式,RESTful 搜索引擎。 - -### 14. arthas - -- **Github地址**:[https://github.com/alibaba/arthas](https://github.com/alibaba/arthas) -- **star**: 12.6 k (1,080 stars this month) -- **介绍**: Arthas 是Alibaba开源的Java诊断工具。 - -### 15. hutool - -- **Github地址**: -- **star**: 4.5 k (1,031 stars this month) -- **介绍**: Hutool是一个Java工具包,也只是一个工具包,它帮助我们简化每一行代码,减少每一个方法,让Java语言也可以“甜甜的”。Hutool最初是我项目中“util”包的一个整理,后来慢慢积累并加入更多非业务相关功能,并广泛学习其它开源项目精髓,经过自己整理修改,最终形成丰富的开源工具集。官网: 。 \ No newline at end of file diff --git a/docs/github-trending/2019-5.md b/docs/github-trending/2019-5.md deleted file mode 100644 index df327b791fe..00000000000 --- a/docs/github-trending/2019-5.md +++ /dev/null @@ -1,125 +0,0 @@ -以下涉及到的数据统计与 2019 年 6 月 1 日 18 点,数据来源: 。下面推荐的内容从 Java 学习文档到最热门的框架再到热门的工具应有尽有,建议收藏+在看! - -### 1.LeetCodeAnimation - -- **Github 地址**: -- **Star**: 29.0k (11,492 stars this month) -- **介绍**: Demonstrate all the questions on LeetCode in the form of animation.(用动画的形式呈现解LeetCode题目的思路)。 - -### 2.CS-Notes - -- **Github 地址**: -- **Star**: 64.4k (5513 stars this month) -- **介绍**: 技术面试必备基础知识、Leetcode 题解、后端面试、Java 面试、春招、秋招、操作系统、计算机网络、系统设计。 - -### 3.JavaGuide - -- **Github 地址**: -- **Star**: 42.0k (4,442 stars this month) -- **介绍**: 【Java 学习+面试指南】 一份涵盖大部分 Java 程序员所需要掌握的核心知识。 - -### 4.mall - -- **Github 地址**: [https://github.com/macrozheng/mall](https://github.com/macrozheng/mall) -- **star**: 14.6 k (3,086 stars this month) -- **介绍**: mall 项目是一套电商系统,包括前台商城系统及后台管理系统,基于 SpringBoot+MyBatis 实现。 前台商城系统包含首页门户、商品推荐、商品搜索、商品展示、购物车、订单流程、会员中心、客户服务、帮助中心等模块。 后台管理系统包含商品管理、订单管理、会员管理、促销管理、运营管理、内容管理、统计报表、财务管理、权限管理、设置等模块。 - -### 5.advanced-java - -- **Github 地址**:[https://github.com/doocs/advanced-java](https://github.com/doocs/advanced-java) -- **Star**: 20.8k (2,394 stars this month) -- **介绍**: 互联网 Java 工程师进阶知识完全扫盲。 - -### 6.spring-boot - -- **Github地址**: [https://github.com/spring-projects/spring-boot](https://github.com/spring-projects/spring-boot) -- **star:** 38.5k (1,339 stars this month) -- **介绍**: 虽然Spring的组件代码是轻量级的,但它的配置却是重量级的(需要大量XML配置),不过Spring Boot 让这一切成为了过去。 另外Spring Cloud也是基于Spring Boot构建的,我个人非常有必要学习一下。 - -**Spring Boot官方的介绍:** - -> Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can “just run”…Most Spring Boot applications need very little Spring configuration.(Spring Boot可以轻松创建独立的生产级基于Spring的应用程序,只要通过 “just run”(可能是run ‘Application’或java -jar 或 tomcat 或 maven插件run 或 shell脚本)便可以运行项目。大部分Spring Boot项目只需要少量的配置即可) - -### 7. Java - -- **Github 地址**: -- **Star**:14.3k (1,334 stars this month) -- **介绍**: All Algorithms implemented in Java。 - -### 8.server - -- **Github 地址**: -- **star**: 2.2 k (1,275 stars this month) -- **介绍**: 全开源即时通讯(IM)系统。 - -### 9.litemall - -- **Github 地址**: -- **Star**: 7.1k (1,114 stars this month) -- **介绍**: 又一个小商城。litemall = Spring Boot后端 + Vue管理员前端 + 微信小程序用户前端 + Vue用户移动端。 - -### 10.Linkage-RecyclerView - -- **Github 地址**: -- **Star**: 10.0k (1,093 stars this month) -- **介绍**: 即使不用饿了么订餐,也请务必收藏好该库!🔥 一行代码即可接入,二级联动订餐列表 - Even if you don't order food by PrubHub, be sure to collect this library, please! 🔥 This secondary linkage list widget can be accessed by only one line of code. Supporting by RecyclerView & AndroidX. - -### 11.toBeTopJavaer - -- **Github 地址** : -- **Star**: 3.3k (1,007 stars this month) -- **介绍**: To Be Top Javaer - Java工程师成神之路 - -### 12.elasticsearch - -- **Github 地址** : [https://github.com/elastic/elasticsearch](https://github.com/elastic/elasticsearch) -- **Star**: 48.0k (968 stars this month) -- **介绍**: 开源,分布式,RESTful 搜索引擎。 - -### 13.java-design-patterns - -- **Github 地址** : -- **Star**: 41.5k (955 stars this month) -- **介绍**: Design patterns implemented in Java。 - -### 14.apollo - -- **Github 地址** : -- **Star**: 14.5k (927 stars this month) -- **介绍**: Apollo(阿波罗)是携程框架部门研发的分布式配置中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景。 - -### 15.arthas - -- **Github地址**:[https://github.com/alibaba/arthas](https://github.com/alibaba/arthas) -- **star**: 13.5 k (933 stars this month) -- **介绍**: Arthas 是Alibaba开源的Java诊断工具。 - -### 16.dubbo - -- **Github地址**: -- **star**: 26.9 k (769 stars this month) -- **介绍**: Apache Dubbo是一个基于Java的高性能开源RPC框架。 - -### 17.DoraemonKit - -- **Github地址**: -- **Star**: 8.5k (909 stars this month) -- **介绍**: 简称 "DoKit" 。一款功能齐全的客户端( iOS 、Android )研发助手,你值得拥有。 - -### 18.halo - -- **Github地址**: -- **Star**: 4.1k (829 stars this month) -- **介绍**: Halo 可能是最好的 Java 博客系统。 - -### 19.seata - -- **Github 地址** : [https://github.com/seata/seata](https://github.com/seata/seata) -- **star**: 9.2 k (776 stars this month) -- **介绍**: Seata 是一种易于使用,高性能,基于 Java 的开源分布式事务解决方案。 - -### 20.hutool - -- **Github地址**: -- **star**: 5,3 k (812 stars this month) -- **介绍**: Hutool是一个Java工具包,也只是一个工具包,它帮助我们简化每一行代码,减少每一个方法,让Java语言也可以“甜甜的”。Hutool最初是我项目中“util”包的一个整理,后来慢慢积累并加入更多非业务相关功能,并广泛学习其它开源项目精髓,经过自己整理修改,最终形成丰富的开源工具集。官网: 。 \ No newline at end of file diff --git a/docs/github-trending/2019-6.md b/docs/github-trending/2019-6.md deleted file mode 100644 index 2a395e160d6..00000000000 --- a/docs/github-trending/2019-6.md +++ /dev/null @@ -1,119 +0,0 @@ -### 1.CS-Notes - -- **Github 地址**:https://github.com/CyC2018/CS-Notes -- **Star**: 69.8k -- **介绍**: 技术面试必备基础知识、Leetcode 题解、后端面试、Java 面试、春招、秋招、操作系统、计算机网络、系统设计。 - -### 2.toBeTopJavaer - -- **Github 地址:**[https://github.com/hollischuang/toBeTopJavaer](https://github.com/hollischuang/toBeTopJavaer) -- **Star**: 4.7k -- **介绍**: To Be Top Javaer - Java工程师成神之路。 - -### 3.p3c - -- **Github 地址:** [https://github.com/alibaba/p3c](https://github.com/alibaba/p3c) -- **Star**: 16.6k -- **介绍**: Alibaba Java Coding Guidelines pmd implements and IDE plugin。Eclipse 和 IDEA 上都有该插件,推荐使用! - -### 4.SpringCloudLearning - -- **Github 地址:** [https://github.com/forezp/SpringCloudLearning](https://github.com/forezp/SpringCloudLearning) -- **Star**: 8.7k -- **介绍**: 史上最简单的Spring Cloud教程源码。 - -### 5.dubbo - -- **Github地址**: -- **star**: 27.6 k -- **介绍**: Apache Dubbo是一个基于Java的高性能开源RPC框架。 - -### 6.jeecg-boot - -- **Github地址**: [https://github.com/zhangdaiscott/jeecg-boot](https://github.com/zhangdaiscott/jeecg-boot) -- **star**: 3.3 k -- **介绍**: 一款基于代码生成器的JAVA快速开发平台!全新架构前后端分离:SpringBoot 2.x,Ant Design&Vue,Mybatis,Shiro,JWT。强大的代码生成器让前后端代码一键生成,无需写任何代码,绝对是全栈开发福音!! JeecgBoot的宗旨是提高UI能力的同时,降低前后分离的开发成本,JeecgBoot还独创在线开发模式,No代码概念,一系列在线智能开发:在线配置表单、在线配置报表、在线设计流程等等。 - -### 7.advanced-java - -- **Github 地址**:[https://github.com/doocs/advanced-java](https://github.com/doocs/advanced-java) -- **Star**: 24.2k -- **介绍**: 互联网 Java 工程师进阶知识完全扫盲:涵盖高并发、分布式、高可用、微服务等领域知识,后端同学必看,前端同学也可学习。 - -### 8.FEBS-Shiro - -- **Github 地址**:[https://github.com/wuyouzhuguli/FEBS-Shiro](https://github.com/wuyouzhuguli/FEBS-Shiro) -- **Star**: 2.6k -- **介绍**: Spring Boot 2.1.3,Shiro1.4.0 & Layui 2.5.4 权限管理系统。预览地址:http://49.234.20.223:8080/login。 - -### 9.SpringAll - -- **Github 地址**: [https://github.com/wuyouzhuguli/SpringAll](https://github.com/wuyouzhuguli/SpringAll) -- **Star**: 5.4k -- **介绍**: 循序渐进,学习Spring Boot、Spring Boot & Shiro、Spring Cloud、Spring Security & Spring Security OAuth2,博客Spring系列源码。 - -### 10.JavaGuide - -- **Github 地址**: -- **Star**: 47.2k -- **介绍**: 【Java 学习+面试指南】 一份涵盖大部分 Java 程序员所需要掌握的核心知识。 - -### 11.vhr - -- **Github 地址**:[https://github.com/lenve/vhr](https://github.com/lenve/vhr) -- **Star**: 4.9k -- **介绍**: 微人事是一个前后端分离的人力资源管理系统,项目采用SpringBoot+Vue开发。 - -### 12. tutorials - -- **Github 地址**:[https://github.com/eugenp/tutorials](https://github.com/eugenp/tutorials) -- **star**: 15.4 k -- **介绍**: 该项目是一系列小而专注的教程 - 每个教程都涵盖 Java 生态系统中单一且定义明确的开发领域。 当然,它们的重点是 Spring Framework - Spring,Spring Boot 和 Spring Securiyt。 除了 Spring 之外,还有以下技术:核心 Java,Jackson,HttpClient,Guava。 - -### 13.EasyScheduler - -- **Github 地址**:[https://github.com/analysys/EasyScheduler](https://github.com/analysys/EasyScheduler) -- **star**: 1.1 k -- **介绍**: Easy Scheduler是一个分布式工作流任务调度系统,主要解决“复杂任务依赖但无法直接监控任务健康状态”的问题。Easy Scheduler以DAG方式组装任务,可以实时监控任务的运行状态。同时,它支持重试,重新运行等操作... 。https://analysys.github.io/easyscheduler_docs_cn/ - -### 14.thingsboard - -- **Github 地址**:[https://github.com/thingsboard/thingsboard](https://github.com/thingsboard/thingsboard) -- **star**: 3.7 k -- **介绍**: 开源物联网平台 - 设备管理,数据收集,处理和可视化。 [https://thingsboard.io](https://thingsboard.io/) - -### 15.mall-learning - -- **Github 地址**: [https://github.com/macrozheng/mall-learning](https://github.com/macrozheng/mall-learning) -- **star**: 0.6 k -- **介绍**: mall学习教程,架构、业务、技术要点全方位解析。mall项目(16k+star)是一套电商系统,使用现阶段主流技术实现。 涵盖了SpringBoot2.1.3、MyBatis3.4.6、Elasticsearch6.2.2、RabbitMQ3.7.15、Redis3.2、Mongodb3.2、Mysql5.7等技术,采用Docker容器化部署。 https://github.com/macrozheng/mall - -### 16. flink - -- **Github地址**:[https://github.com/apache/flink](https://github.com/apache/flink) -- **star**: 9.3 k -- **介绍**: Apache Flink是一个开源流处理框架,具有强大的流和批处理功能。 - -### 17.spring-cloud-kubernetes - -- **Github地址**:[https://github.com/spring-cloud/spring-cloud-kubernetes](https://github.com/spring-cloud/spring-cloud-kubernetes) -- **star**: 1.4 k -- **介绍**: Kubernetes 集成 Spring Cloud Discovery Client, Configuration, etc... - -### 18.springboot-learning-example - -- **Github地址**:[https://github.com/JeffLi1993/springboot-learning-example](https://github.com/JeffLi1993/springboot-learning-example) -- **star**: 10.0 k -- **介绍**: spring boot 实践学习案例,是 spring boot 初学者及核心技术巩固的最佳实践。 - -### 19.canal - -- **Github地址**:[https://github.com/alibaba/canal](https://github.com/alibaba/canal) -- **star**: 9.3 k -- **介绍**: 阿里巴巴 MySQL binlog 增量订阅&消费组件。 - -### 20.react-native-device-info - -- **Github地址**:[https://github.com/react-native-community/react-native-device-info](https://github.com/react-native-community/react-native-device-info) -- **star**: 4.0 k -- **介绍**: React Native iOS和Android的设备信息。 \ No newline at end of file diff --git a/docs/github-trending/JavaGithubTrending.md b/docs/github-trending/JavaGithubTrending.md deleted file mode 100644 index 91d544ed37e..00000000000 --- a/docs/github-trending/JavaGithubTrending.md +++ /dev/null @@ -1,8 +0,0 @@ -- [2018 年 12 月](https://github.com/Snailclimb/JavaGuide/blob/master/docs/github-trending/2018-12.md) -- [2019 年 1 月](https://github.com/Snailclimb/JavaGuide/blob/master/docs/github-trending/2019-1.md) -- [2019 年 2 月](https://github.com/Snailclimb/JavaGuide/blob/master/docs/github-trending/2019-2.md) -- [2019 年 3 月](https://github.com/Snailclimb/JavaGuide/blob/master/docs/github-trending/2019-3.md) -- [2019 年 4 月](https://github.com/Snailclimb/JavaGuide/blob/master/docs/github-trending/2019-4.md) -- [2019 年 5 月](https://github.com/Snailclimb/JavaGuide/blob/master/docs/github-trending/2019-5.md) -- [2019 年 6 月](https://github.com/Snailclimb/JavaGuide/blob/master/docs/github-trending/2019-6.md) - diff --git "a/docs/java/Java IO\344\270\216NIO.md" "b/docs/java/Java IO\344\270\216NIO.md" deleted file mode 100644 index 74bd850e696..00000000000 --- "a/docs/java/Java IO\344\270\216NIO.md" +++ /dev/null @@ -1,200 +0,0 @@ - - -- [IO流学习总结](#io流学习总结) - - [一 Java IO,硬骨头也能变软](#一-java-io,硬骨头也能变软) - - [二 java IO体系的学习总结](#二-java-io体系的学习总结) - - [三 Java IO面试题](#三-java-io面试题) -- [NIO与AIO学习总结](#nio与aio学习总结) - - [一 Java NIO 概览](#一-java-nio-概览) - - [二 Java NIO 之 Buffer\(缓冲区\)](#二-java-nio-之-buffer缓冲区) - - [三 Java NIO 之 Channel(通道)](#三-java-nio-之-channel(通道)) - - [四 Java NIO之Selector(选择器)](#四-java-nio之selector(选择器)) - - [五 Java NIO之拥抱Path和Files](#五-java-nio之拥抱path和files) - - [六 NIO学习总结以及NIO新特性介绍](#六-nio学习总结以及nio新特性介绍) - - [七 Java NIO AsynchronousFileChannel异步文件通](#七-java-nio-asynchronousfilechannel异步文件通) - - [八 高并发Java(8):NIO和AIO](#八-高并发java(8):nio和aio) -- [推荐阅读](#推荐阅读) - - [在 Java 7 中体会 NIO.2 异步执行的快乐](#在-java-7-中体会-nio2-异步执行的快乐) - - [Java AIO总结与示例](#java-aio总结与示例) - - - - - -## IO流学习总结 - -### [一 Java IO,硬骨头也能变软](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483981&idx=1&sn=6e5c682d76972c8d2cf271a85dcf09e2&chksm=fd98542ccaefdd3a70428e9549bc33e8165836855edaa748928d16c1ebde9648579d3acaac10#rd) - -**(1) 按操作方式分类结构图:** - -![IO-操作方式分类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/IO-操作方式分类.png) - - -**(2)按操作对象分类结构图** - -![IO-操作对象分类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/IO-操作对象分类.png) - -### [二 java IO体系的学习总结](https://blog.csdn.net/nightcurtis/article/details/51324105) -1. **IO流的分类:** - - 按照流的流向分,可以分为输入流和输出流; - - 按照操作单元划分,可以划分为字节流和字符流; - - 按照流的角色划分为节点流和处理流。 -2. **流的原理浅析:** - - java Io流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java Io流的40多个类都是从如下4个抽象类基类中派生出来的。 - - - **InputStream/Reader**: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。 - - **OutputStream/Writer**: 所有输出流的基类,前者是字节输出流,后者是字符输出流。 -3. **常用的io流的用法** - -### [三 Java IO面试题](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483985&idx=1&sn=38531c2cee7b87f125df7aef41637014&chksm=fd985430caefdd26b0506aa84fc26251877eccba24fac73169a4d6bd1eb5e3fbdf3c3b940261#rd) - -## NIO与AIO学习总结 - - -### [一 Java NIO 概览](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483956&idx=1&sn=57692bc5b7c2c6dfb812489baadc29c9&chksm=fd985455caefdd4331d828d8e89b22f19b304aa87d6da73c5d8c66fcef16e4c0b448b1a6f791#rd) - -1. **NIO简介**: - - Java NIO 是 java 1.4, 之后新出的一套IO接口NIO中的N可以理解为Non-blocking,不单纯是New。 - -2. **NIO的特性/NIO与IO区别:** - - 1)IO是面向流的,NIO是面向缓冲区的; - - 2)IO流是阻塞的,NIO流是不阻塞的; - - 3)NIO有选择器,而IO没有。 -3. **读数据和写数据方式:** - - 从通道进行数据读取 :创建一个缓冲区,然后请求通道读取数据。 - - - 从通道进行数据写入 :创建一个缓冲区,填充数据,并要求通道写入数据。 - -4. **NIO核心组件简单介绍** - - **Channels** - - **Buffers** - - **Selectors** - - -### [二 Java NIO 之 Buffer(缓冲区)](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483961&idx=1&sn=f67bef4c279e78043ff649b6b03fdcbc&chksm=fd985458caefdd4e3317ccbdb2d0a5a70a5024d3255eebf38183919ed9c25ade536017c0a6ba#rd) - -1. **Buffer(缓冲区)介绍:** - - Java NIO Buffers用于和NIO Channel交互。 我们从Channel中读取数据到buffers里,从Buffer把数据写入到Channels; - - Buffer本质上就是一块内存区; - - 一个Buffer有三个属性是必须掌握的,分别是:capacity容量、position位置、limit限制。 -2. **Buffer的常见方法** - - Buffer clear() - - Buffer flip() - - Buffer rewind() - - Buffer position(int newPosition) -3. **Buffer的使用方式/方法介绍:** - - 分配缓冲区(Allocating a Buffer): - ```java - ByteBuffer buf = ByteBuffer.allocate(28);//以ByteBuffer为例子 - ``` - - 写入数据到缓冲区(Writing Data to a Buffer) - - **写数据到Buffer有两种方法:** - - 1.从Channel中写数据到Buffer - ```java - int bytesRead = inChannel.read(buf); //read into buffer. - ``` - 2.通过put写数据: - ```java - buf.put(127); - ``` - -4. **Buffer常用方法测试** - - 说实话,NIO编程真的难,通过后面这个测试例子,你可能才能勉强理解前面说的Buffer方法的作用。 - - -### [三 Java NIO 之 Channel(通道)](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483966&idx=1&sn=d5cf18c69f5f9ec2aff149270422731f&chksm=fd98545fcaefdd49296e2c78000ce5da277435b90ba3c03b92b7cf54c6ccc71d61d13efbce63#rd) - - -1. **Channel(通道)介绍** - - 通常来说NIO中的所有IO都是从 Channel(通道) 开始的。 - - NIO Channel通道和流的区别: -2. **FileChannel的使用** -3. **SocketChannel和ServerSocketChannel的使用** -4. **️DatagramChannel的使用** -5. **Scatter / Gather** - - Scatter: 从一个Channel读取的信息分散到N个缓冲区中(Buufer). - - Gather: 将N个Buffer里面内容按照顺序发送到一个Channel. -6. **通道之间的数据传输** - - 在Java NIO中如果一个channel是FileChannel类型的,那么他可以直接把数据传输到另一个channel。 - - transferFrom() :transferFrom方法把数据从通道源传输到FileChannel - - transferTo() :transferTo方法把FileChannel数据传输到另一个channel - - -### [四 Java NIO之Selector(选择器)](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483970&idx=1&sn=d5e2b133313b1d0f32872d54fbdf0aa7&chksm=fd985423caefdd354b587e57ce6cf5f5a7bec48b9ab7554f39a8d13af47660cae793956e0f46#rd) - - -1. **Selector(选择器)介绍** - - Selector 一般称 为选择器 ,当然你也可以翻译为 多路复用器 。它是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel(通道)的状态是否处于可读、可写。如此可以实现单线程管理多个channels,也就是可以管理多个网络链接。 - - 使用Selector的好处在于: 使用更少的线程来就可以来处理通道了, 相比使用多个线程,避免了线程上下文切换带来的开销。 -2. **Selector(选择器)的使用方法介绍** - - Selector的创建 - ```java - Selector selector = Selector.open(); - ``` - - 注册Channel到Selector(Channel必须是非阻塞的) - ```java - channel.configureBlocking(false); - SelectionKey key = channel.register(selector, Selectionkey.OP_READ); - ``` - - SelectionKey介绍 - - 一个SelectionKey键表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系。 - - 从Selector中选择channel(Selecting Channels via a Selector) - - 选择器维护注册过的通道的集合,并且这种注册关系都被封装在SelectionKey当中. - - 停止选择的方法 - - wakeup()方法 和close()方法。 -3. **模板代码** - - 有了模板代码我们在编写程序时,大多数时间都是在模板代码中添加相应的业务代码。 -4. **客户端与服务端简单交互实例** - - - -### [五 Java NIO之拥抱Path和Files](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483976&idx=1&sn=2296c05fc1b840a64679e2ad7794c96d&chksm=fd985429caefdd3f48e2ee6fdd7b0f6fc419df90b3de46832b484d6d1ca4e74e7837689c8146&token=537240785&lang=zh_CN#rd) - -**一 文件I/O基石:Path:** -- 创建一个Path -- File和Path之间的转换,File和URI之间的转换 -- 获取Path的相关信息 -- 移除Path中的冗余项 - -**二 拥抱Files类:** -- Files.exists() 检测文件路径是否存在 -- Files.createFile() 创建文件 -- Files.createDirectories()和Files.createDirectory()创建文件夹 -- Files.delete()方法 可以删除一个文件或目录 -- Files.copy()方法可以吧一个文件从一个地址复制到另一个位置 -- 获取文件属性 -- 遍历一个文件夹 -- Files.walkFileTree()遍历整个目录 - -### [六 NIO学习总结以及NIO新特性介绍](https://blog.csdn.net/a953713428/article/details/64907250) - -- **内存映射:** - -这个功能主要是为了提高大文件的读写速度而设计的。内存映射文件(memory-mappedfile)能让你创建和修改那些大到无法读入内存的文件。有了内存映射文件,你就可以认为文件已经全部读进了内存,然后把它当成一个非常大的数组来访问了。将文件的一段区域映射到内存中,比传统的文件处理速度要快很多。内存映射文件它虽然最终也是要从磁盘读取数据,但是它并不需要将数据读取到OS内核缓冲区,而是直接将进程的用户私有地址空间中的一部分区域与文件对象建立起映射关系,就好像直接从内存中读、写文件一样,速度当然快了。 - -### [七 Java NIO AsynchronousFileChannel异步文件通](http://wiki.jikexueyuan.com/project/java-nio-zh/java-nio-asynchronousfilechannel.html) - -Java7中新增了AsynchronousFileChannel作为nio的一部分。AsynchronousFileChannel使得数据可以进行异步读写。 - -### [八 高并发Java(8):NIO和AIO](http://www.importnew.com/21341.html) - - - -## 推荐阅读 - -### [在 Java 7 中体会 NIO.2 异步执行的快乐](https://www.ibm.com/developerworks/cn/java/j-lo-nio2/index.html) - -### [Java AIO总结与示例](https://blog.csdn.net/x_i_y_u_e/article/details/52223406) -AIO是异步IO的缩写,虽然NIO在网络操作中,提供了非阻塞的方法,但是NIO的IO行为还是同步的。对于NIO来说,我们的业务线程是在IO操作准备好时,得到通知,接着就由这个线程自行进行IO操作,IO操作本身是同步的。 - - -**欢迎关注我的微信公众号:"Java面试通关手册"(一个有温度的微信公众号,期待与你共同进步~~~坚持原创,分享美文,分享各种Java学习资源):** diff --git a/docs/java/Multithread/JavaConcurrencyAdvancedCommonInterviewQuestions.md b/docs/java/Multithread/JavaConcurrencyAdvancedCommonInterviewQuestions.md deleted file mode 100644 index 97cc51d88c0..00000000000 --- a/docs/java/Multithread/JavaConcurrencyAdvancedCommonInterviewQuestions.md +++ /dev/null @@ -1,940 +0,0 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 - - - -- [Java 并发进阶常见面试题总结](#java-并发进阶常见面试题总结) - - [1. synchronized 关键字](#1-synchronized-关键字) - - [1.1. 说一说自己对于 synchronized 关键字的了解](#11-说一说自己对于-synchronized-关键字的了解) - - [1.2. 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗](#12-说说自己是怎么使用-synchronized-关键字在项目中用到了吗) - - [1.3. 讲一下 synchronized 关键字的底层原理](#13-讲一下-synchronized-关键字的底层原理) - - [1.4. 说说 JDK1.6 之后的synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗](#14-说说-jdk16-之后的synchronized-关键字底层做了哪些优化可以详细介绍一下这些优化吗) - - [1.5. 谈谈 synchronized和ReentrantLock 的区别](#15-谈谈-synchronized和reentrantlock-的区别) - - [2. volatile关键字](#2-volatile关键字) - - [2.1. 讲一下Java内存模型](#21-讲一下java内存模型) - - [2.2. 说说 synchronized 关键字和 volatile 关键字的区别](#22-说说-synchronized-关键字和-volatile-关键字的区别) - - [3. ThreadLocal](#3-threadlocal) - - [3.1. ThreadLocal简介](#31-threadlocal简介) - - [3.2. ThreadLocal示例](#32-threadlocal示例) - - [3.3. ThreadLocal原理](#33-threadlocal原理) - - [3.4. ThreadLocal 内存泄露问题](#34-threadlocal-内存泄露问题) - - [4. 线程池](#4-线程池) - - [4.1. 为什么要用线程池?](#41-为什么要用线程池) - - [4.2. 实现Runnable接口和Callable接口的区别](#42-实现runnable接口和callable接口的区别) - - [4.3. 执行execute()方法和submit()方法的区别是什么呢?](#43-执行execute方法和submit方法的区别是什么呢) - - [4.4. 如何创建线程池](#44-如何创建线程池) - - [5. Atomic 原子类](#5-atomic-原子类) - - [5.1. 介绍一下Atomic 原子类](#51-介绍一下atomic-原子类) - - [5.2. JUC 包中的原子类是哪4类?](#52-juc-包中的原子类是哪4类) - - [5.3. 讲讲 AtomicInteger 的使用](#53-讲讲-atomicinteger-的使用) - - [5.4. 能不能给我简单介绍一下 AtomicInteger 类的原理](#54-能不能给我简单介绍一下-atomicinteger-类的原理) - - [6. AQS](#6-aqs) - - [6.1. AQS 介绍](#61-aqs-介绍) - - [6.2. AQS 原理分析](#62-aqs-原理分析) - - [6.2.1. AQS 原理概览](#621-aqs-原理概览) - - [6.2.2. AQS 对资源的共享方式](#622-aqs-对资源的共享方式) - - [6.2.3. AQS底层使用了模板方法模式](#623-aqs底层使用了模板方法模式) - - [6.3. AQS 组件总结](#63-aqs-组件总结) - - [7 Reference](#7-reference) - - - -# Java 并发进阶常见面试题总结 - -## 1. synchronized 关键字 - -### 1.1. 说一说自己对于 synchronized 关键字的了解 - -synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。 - -另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。 - - -### 1.2. 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗 - -**synchronized关键字最主要的三种使用方式:** - -- **修饰实例方法:** 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁 -- **修饰静态方法:** 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,**因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁**。 -- **修饰代码块:** 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。 - -**总结:** synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能! - -下面我以一个常见的面试题为例讲解一下 synchronized 关键字的具体使用。 - -面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理呗!” - -**双重校验锁实现对象单例(线程安全)** - -```java -public class Singleton { - - private volatile static Singleton uniqueInstance; - - private Singleton() { - } - - public static Singleton getUniqueInstance() { - //先判断对象是否已经实例过,没有实例化过才进入加锁代码 - if (uniqueInstance == null) { - //类对象加锁 - synchronized (Singleton.class) { - if (uniqueInstance == null) { - uniqueInstance = new Singleton(); - } - } - } - return uniqueInstance; - } -} -``` -另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。 - -uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行: - -1. 为 uniqueInstance 分配内存空间 -2. 初始化 uniqueInstance -3. 将 uniqueInstance 指向分配的内存地址 - -但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。 - -使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。 - -### 1.3. 讲一下 synchronized 关键字的底层原理 - -**synchronized 关键字底层原理属于 JVM 层面。** - -**① synchronized 同步语句块的情况** - -```java -public class SynchronizedDemo { - public void method() { - synchronized (this) { - System.out.println("synchronized 代码块"); - } - } -} - -``` - -通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 `javac SynchronizedDemo.java` 命令生成编译后的 .class 文件,然后执行`javap -c -s -v -l SynchronizedDemo.class`。 - -![synchronized关键字原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/synchronized关键字原理.png) - -从上面我们可以看出: - -**synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。** 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。 - -**② synchronized 修饰方法的的情况** - -```java -public class SynchronizedDemo2 { - public synchronized void method() { - System.out.println("synchronized 方法"); - } -} - -``` - -![synchronized关键字原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/synchronized关键字原理2.png) - -synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。 - - -### 1.4. 说说 JDK1.6 之后的synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗 - -JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。 - -锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。 - -关于这几种优化的详细信息可以查看笔主的这篇文章: - -### 1.5. 谈谈 synchronized和ReentrantLock 的区别 - - -**① 两者都是可重入锁** - -两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。 - -**② synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API** - -synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。 - -**③ ReentrantLock 比 synchronized 增加了一些高级功能** - -相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:**①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)** - -- **ReentrantLock提供了一种能够中断等待锁的线程的机制**,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 -- **ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。** ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的`ReentrantLock(boolean fair)`构造方法来制定是否是公平的。 -- synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),**线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知”** ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。 - -如果你想使用上述功能,那么选择ReentrantLock是一个不错的选择。 - -**④ 性能已不是选择标准** - -## 2. volatile关键字 - -### 2.1. 讲一下Java内存模型 - - -在 JDK1.2 之前,Java的内存模型实现总是从**主存**(即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变量保存**本地内存**(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成**数据的不一致**。 - -![数据不一致](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/数据不一致.png) - -要解决这个问题,就需要把变量声明为**volatile**,这就指示 JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。 - -说白了, **volatile** 关键字的主要作用就是保证变量的可见性然后还有一个作用是防止指令重排序。 - -![volatile关键字的可见性](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/volatile关键字的可见性.png) - -### 2.2 并发编程的三个重要特性 - -1. **原子性** : 一个的操作或者多次操作,要么所有的操作全部都得到执行并且不会收到任何因素的干扰而中断,要么所有的操作都执行,要么都不执行。`synchronized ` 可以保证代码片段的原子性。 -2. **可见性** :当一个变量对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。`volatile` 关键字可以保证共享变量的可见性。 -3. **有序性** :代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。`volatile` 关键字可以禁止指令进行重排序优化。 - -### 2.3. 说说 synchronized 关键字和 volatile 关键字的区别 - -`synchronized` 关键字和 `volatile` 关键字是两个互补的存在,而不是对立的存在: - -- **volatile关键字**是线程同步的**轻量级实现**,所以**volatile性能肯定比synchronized关键字要好**。但是**volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块**。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,**实际开发中使用 synchronized 关键字的场景还是更多一些**。 -- **多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞** -- **volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。** -- **volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。** - -## 3. ThreadLocal - -### 3.1. ThreadLocal简介 - -通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。**如果想实现每一个线程都有自己的专属本地变量该如何解决呢?** JDK中提供的`ThreadLocal`类正是为了解决这样的问题。 **`ThreadLocal`类主要解决的就是让每个线程绑定自己的值,可以将`ThreadLocal`类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。** - -**如果你创建了一个`ThreadLocal`变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是`ThreadLocal`变量名的由来。他们可以使用 `get()` 和 `set()` 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。** - -再举个简单的例子: - -比如有两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么ThreadLocal就是用来避免这两个线程竞争的。 - -### 3.2. ThreadLocal示例 - -相信看了上面的解释,大家已经搞懂 ThreadLocal 类是个什么东西了。 - -```java -import java.text.SimpleDateFormat; -import java.util.Random; - -public class ThreadLocalExample implements Runnable{ - - // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本 - private static final ThreadLocal formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm")); - - public static void main(String[] args) throws InterruptedException { - ThreadLocalExample obj = new ThreadLocalExample(); - for(int i=0 ; i<10; i++){ - Thread t = new Thread(obj, ""+i); - Thread.sleep(new Random().nextInt(1000)); - t.start(); - } - } - - @Override - public void run() { - System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern()); - try { - Thread.sleep(new Random().nextInt(1000)); - } catch (InterruptedException e) { - e.printStackTrace(); - } - //formatter pattern is changed here by thread, but it won't reflect to other threads - formatter.set(new SimpleDateFormat()); - - System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern()); - } - -} - -``` - -Output: - -``` -Thread Name= 0 default Formatter = yyyyMMdd HHmm -Thread Name= 0 formatter = yy-M-d ah:mm -Thread Name= 1 default Formatter = yyyyMMdd HHmm -Thread Name= 2 default Formatter = yyyyMMdd HHmm -Thread Name= 1 formatter = yy-M-d ah:mm -Thread Name= 3 default Formatter = yyyyMMdd HHmm -Thread Name= 2 formatter = yy-M-d ah:mm -Thread Name= 4 default Formatter = yyyyMMdd HHmm -Thread Name= 3 formatter = yy-M-d ah:mm -Thread Name= 4 formatter = yy-M-d ah:mm -Thread Name= 5 default Formatter = yyyyMMdd HHmm -Thread Name= 5 formatter = yy-M-d ah:mm -Thread Name= 6 default Formatter = yyyyMMdd HHmm -Thread Name= 6 formatter = yy-M-d ah:mm -Thread Name= 7 default Formatter = yyyyMMdd HHmm -Thread Name= 7 formatter = yy-M-d ah:mm -Thread Name= 8 default Formatter = yyyyMMdd HHmm -Thread Name= 9 default Formatter = yyyyMMdd HHmm -Thread Name= 8 formatter = yy-M-d ah:mm -Thread Name= 9 formatter = yy-M-d ah:mm -``` - -从输出中可以看出,Thread-0已经改变了formatter的值,但仍然是thread-2默认格式化程序与初始化值相同,其他线程也一样。 - -上面有一段代码用到了创建 `ThreadLocal` 变量的那段代码用到了 Java8 的知识,它等于下面这段代码,如果你写了下面这段代码的话,IDEA会提示你转换为Java8的格式(IDEA真的不错!)。因为ThreadLocal类在Java 8中扩展,使用一个新的方法`withInitial()`,将Supplier功能接口作为参数。 - -```java - private static final ThreadLocal formatter = new ThreadLocal(){ - @Override - protected SimpleDateFormat initialValue() - { - return new SimpleDateFormat("yyyyMMdd HHmm"); - } - }; -``` - -### 3.3. ThreadLocal原理 - -从 `Thread`类源代码入手。 - -```java -public class Thread implements Runnable { - ...... -//与此线程有关的ThreadLocal值。由ThreadLocal类维护 -ThreadLocal.ThreadLocalMap threadLocals = null; - -//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护 -ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; - ...... -} -``` - -从上面`Thread`类 源代码可以看出`Thread` 类中有一个 `threadLocals` 和 一个 `inheritableThreadLocals` 变量,它们都是 `ThreadLocalMap` 类型的变量,我们可以把 `ThreadLocalMap` 理解为`ThreadLocal` 类实现的定制化的 `HashMap`。默认情况下这两个变量都是null,只有当前线程调用 `ThreadLocal` 类的 `set`或`get`方法时才创建它们,实际上调用这两个方法的时候,我们调用的是`ThreadLocalMap`类对应的 `get()`、`set() `方法。 - -`ThreadLocal`类的`set()`方法 - -```java - public void set(T value) { - Thread t = Thread.currentThread(); - ThreadLocalMap map = getMap(t); - if (map != null) - map.set(this, value); - else - createMap(t, value); - } - ThreadLocalMap getMap(Thread t) { - return t.threadLocals; - } -``` - -通过上面这些内容,我们足以通过猜测得出结论:**最终的变量是放在了当前线程的 `ThreadLocalMap` 中,并不是存在 `ThreadLocal` 上,`ThreadLocal` 可以理解为只是`ThreadLocalMap`的封装,传递了变量值。** `ThrealLocal` 类中可以通过`Thread.currentThread()`获取到当前线程对象后,直接通过`getMap(Thread t)`可以访问到该线程的`ThreadLocalMap`对象。 - -**每个`Thread`中都具备一个`ThreadLocalMap`,而`ThreadLocalMap`可以存储以`ThreadLocal`为key ,Object 对象为 value的键值对。** - -```java -ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { - ...... -} -``` - -比如我们在同一个线程中声明了两个 `ThreadLocal` 对象的话,会使用 `Thread`内部都是使用仅有那个`ThreadLocalMap` 存放数据的,`ThreadLocalMap`的 key 就是 `ThreadLocal`对象,value 就是 `ThreadLocal` 对象调用`set`方法设置的值。 - -![ThreadLocal数据结构](images/threadlocal数据结构.png) - -`ThreadLocalMap`是`ThreadLocal`的静态内部类。 - -![ThreadLocal内部类](images/ThreadLocal内部类.png) - -### 3.4. ThreadLocal 内存泄露问题 - -`ThreadLocalMap` 中使用的 key 为 `ThreadLocal` 的弱引用,而 value 是强引用。所以,如果 `ThreadLocal` 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,`ThreadLocalMap` 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 `set()`、`get()`、`remove()` 方法的时候,会清理掉 key 为 null 的记录。使用完 `ThreadLocal`方法后 最好手动调用`remove()`方法 - -```java - static class Entry extends WeakReference> { - /** The value associated with this ThreadLocal. */ - Object value; - - Entry(ThreadLocal k, Object v) { - super(k); - value = v; - } - } -``` - -**弱引用介绍:** - -> 如果一个对象只具有弱引用,那就类似于**可有可无的生活用品**。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。 -> -> 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。 - -## 4. 线程池 - -### 4.1. 为什么要用线程池? - -> **池化技术相比大家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。** - -**线程池**提供了一种限制和管理资源(包括执行一个任务)。 每个**线程池**还维护一些基本统计信息,例如已完成任务的数量。 - -这里借用《Java 并发编程的艺术》提到的来说一下**使用线程池的好处**: - -- **降低资源消耗**。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 -- **提高响应速度**。当任务到达时,任务可以不需要的等到线程创建就能立即执行。 -- **提高线程的可管理性**。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 - -### 4.2. 实现Runnable接口和Callable接口的区别 - -`Runnable`自Java 1.0以来一直存在,但`Callable`仅在Java 1.5中引入,目的就是为了来处理`Runnable`不支持的用例。**`Runnable` 接口**不会返回结果或抛出检查异常,但是**`Callable` 接口**可以。所以,如果任务不需要返回结果或抛出异常推荐使用 **`Runnable` 接口**,这样代码看起来会更加简洁。 - -工具类 `Executors` 可以实现 `Runnable` 对象和 `Callable` 对象之间的相互转换。(`Executors.callable(Runnable task`)或 `Executors.callable(Runnable task,Object resule)`)。 - -`Runnable.java` - -```java -@FunctionalInterface -public interface Runnable { - /** - * 被线程执行,没有返回值也无法抛出异常 - */ - public abstract void run(); -} -``` - -`Callable.java` - -```java -@FunctionalInterface -public interface Callable { - /** - * 计算结果,或在无法这样做时抛出异常。 - * @return 计算得出的结果 - * @throws 如果无法计算结果,则抛出异常 - */ - V call() throws Exception; -} -``` - -### 4.3. 执行execute()方法和submit()方法的区别是什么呢? - -1. **`execute()`方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;** -2. **`submit()`方法用于提交需要返回值的任务。线程池会返回一个 `Future` 类型的对象,通过这个 `Future` 对象可以判断任务是否执行成功**,并且可以通过 `Future` 的 `get()`方法来获取返回值,`get()`方法会阻塞当前线程直到任务完成,而使用 `get(long timeout,TimeUnit unit)`方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。 - -我们以**`AbstractExecutorService`**接口中的一个 `submit` 方法为例子来看看源代码: - -```java - public Future submit(Runnable task) { - if (task == null) throw new NullPointerException(); - RunnableFuture ftask = newTaskFor(task, null); - execute(ftask); - return ftask; - } -``` - -上面方法调用的 `newTaskFor` 方法返回了一个 `FutureTask` 对象。 - -```java - protected RunnableFuture newTaskFor(Runnable runnable, T value) { - return new FutureTask(runnable, value); - } -``` - -我们再来看看`execute()`方法: - -```java - public void execute(Runnable command) { - ... - } -``` - -### 4.4. 如何创建线程池 - -《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险 - -> Executors 返回线程池对象的弊端如下: -> -> - **FixedThreadPool 和 SingleThreadExecutor** : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致OOM。 -> - **CachedThreadPool 和 ScheduledThreadPool** : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。 - -**方式一:通过构造方法实现** -![ThreadPoolExecutor构造方法](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/ThreadPoolExecutor构造方法.png) -**方式二:通过Executor 框架的工具类Executors来实现** -我们可以创建三种类型的ThreadPoolExecutor: - -- **FixedThreadPool** : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。 -- **SingleThreadExecutor:** 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。 -- **CachedThreadPool:** 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。 - -对应Executors工具类中的方法如图所示: -![Executor框架的工具类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/Executor框架的工具类.png) - -### 4.5 ThreadPoolExecutor 类分析 - -`ThreadPoolExecutor` 类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法的基础上产生(其他几个构造方法说白点都是给定某些默认参数的构造方法比如默认制定拒绝策略是什么),这里就不贴代码讲了,比较简单。 - -```java - /** - * 用给定的初始参数创建一个新的ThreadPoolExecutor。 - */ - public ThreadPoolExecutor(int corePoolSize, - int maximumPoolSize, - long keepAliveTime, - TimeUnit unit, - BlockingQueue workQueue, - ThreadFactory threadFactory, - RejectedExecutionHandler handler) { - if (corePoolSize < 0 || - maximumPoolSize <= 0 || - maximumPoolSize < corePoolSize || - keepAliveTime < 0) - throw new IllegalArgumentException(); - if (workQueue == null || threadFactory == null || handler == null) - throw new NullPointerException(); - this.corePoolSize = corePoolSize; - this.maximumPoolSize = maximumPoolSize; - this.workQueue = workQueue; - this.keepAliveTime = unit.toNanos(keepAliveTime); - this.threadFactory = threadFactory; - this.handler = handler; - } -``` - -**下面这些对创建 非常重要,在后面使用线程池的过程中你一定会用到!所以,务必拿着小本本记清楚。** - -#### 4.5.1 `ThreadPoolExecutor`构造函数重要参数分析 - -**`ThreadPoolExecutor` 3 个最重要的参数:** - -- **`corePoolSize` :** 核心线程数线程数定义了最小可以同时运行的线程数量。 -- **`maximumPoolSize` :** 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 -- **`workQueue`:** 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。 - -`ThreadPoolExecutor`其他常见参数: - -1. **`keepAliveTime`**:当线程池中的线程数量大于 `corePoolSize` 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 `keepAliveTime`才会被回收销毁; -2. **`unit`** : `keepAliveTime` 参数的时间单位。 -3. **`threadFactory`** :executor 创建新线程的时候会用到。 -4. **`handler`** :饱和策略。关于饱和策略下面单独介绍一下。 - -#### 4.5.2 `ThreadPoolExecutor` 饱和策略 - -**`ThreadPoolExecutor` 饱和策略定义:** - -如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,`ThreadPoolTaskExecutor` 定义一些策略: - -- **`ThreadPoolExecutor.AbortPolicy`**:抛出 `RejectedExecutionException`来拒绝新任务的处理。 -- **`ThreadPoolExecutor.CallerRunsPolicy`**:调用执行自己的线程运行任务,也就是直接在调用`execute`方法的线程中运行(`run`)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。 -- **`ThreadPoolExecutor.DiscardPolicy`:** 不处理新任务,直接丢弃掉。 -- **`ThreadPoolExecutor.DiscardOldestPolicy`:** 此策略将丢弃最早的未处理的任务请求。 - -举个例子: Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecutor` 的构造函数创建线程池的时候,当我们不指定 `RejectedExecutionHandler` 饱和策略的话来配置线程池的时候默认使用的是 `ThreadPoolExecutor.AbortPolicy`。在默认情况下,`ThreadPoolExecutor` 将抛出 `RejectedExecutionException` 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 `ThreadPoolExecutor.CallerRunsPolicy`。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 `ThreadPoolExecutor` 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了) - -### 4.6 一个简单的线程池Demo:`Runnable`+`ThreadPoolExecutor` - -为了让大家更清楚上面的面试题中的一些概念,我写了一个简单的线程池 Demo。 - -首先创建一个 `Runnable` 接口的实现类(当然也可以是 `Callable` 接口,我们上面也说了两者的区别。) - -`MyRunnable.java` - -```java -import java.util.Date; - -/** - * 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。 - * @author shuang.kou - */ -public class MyRunnable implements Runnable { - - private String command; - - public MyRunnable(String s) { - this.command = s; - } - - @Override - public void run() { - System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date()); - processCommand(); - System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date()); - } - - private void processCommand() { - try { - Thread.sleep(5000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - @Override - public String toString() { - return this.command; - } -} - -``` - -编写测试程序,我们这里以阿里巴巴推荐的使用 `ThreadPoolExecutor` 构造函数自定义参数的方式来创建线程池。 - -`ThreadPoolExecutorDemo.java` - -```java -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -public class ThreadPoolExecutorDemo { - - private static final int CORE_POOL_SIZE = 5; - private static final int MAX_POOL_SIZE = 10; - private static final int QUEUE_CAPACITY = 100; - private static final Long KEEP_ALIVE_TIME = 1L; - public static void main(String[] args) { - - //使用阿里巴巴推荐的创建线程池的方式 - //通过ThreadPoolExecutor构造函数自定义参数创建 - ThreadPoolExecutor executor = new ThreadPoolExecutor( - CORE_POOL_SIZE, - MAX_POOL_SIZE, - KEEP_ALIVE_TIME, - TimeUnit.SECONDS, - new ArrayBlockingQueue<>(QUEUE_CAPACITY), - new ThreadPoolExecutor.CallerRunsPolicy()); - - for (int i = 0; i < 10; i++) { - //创建WorkerThread对象(WorkerThread类实现了Runnable 接口) - Runnable worker = new MyRunnable("" + i); - //执行Runnable - executor.execute(worker); - } - //终止线程池 - executor.shutdown(); - while (!executor.isTerminated()) { - } - System.out.println("Finished all threads"); - } -} - -``` - -可以看到我们上面的代码指定了: - -1. `corePoolSize`: 核心线程数为 5。 -2. `maximumPoolSize` :最大线程数 10 -3. `keepAliveTime` : 等待时间为 1L。 -4. `unit`: 等待时间的单位为 TimeUnit.SECONDS。 -5. `workQueue`:任务队列为 `ArrayBlockingQueue`,并且容量为 100; -6. `handler`:饱和策略为 `CallerRunsPolicy`。 - -**Output:** - -``` -pool-1-thread-2 Start. Time = Tue Nov 12 20:59:44 CST 2019 -pool-1-thread-5 Start. Time = Tue Nov 12 20:59:44 CST 2019 -pool-1-thread-4 Start. Time = Tue Nov 12 20:59:44 CST 2019 -pool-1-thread-1 Start. Time = Tue Nov 12 20:59:44 CST 2019 -pool-1-thread-3 Start. Time = Tue Nov 12 20:59:44 CST 2019 -pool-1-thread-5 End. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-3 End. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-2 End. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-4 End. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-1 End. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-2 Start. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-1 Start. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-4 Start. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-3 Start. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-5 Start. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-2 End. Time = Tue Nov 12 20:59:54 CST 2019 -pool-1-thread-3 End. Time = Tue Nov 12 20:59:54 CST 2019 -pool-1-thread-4 End. Time = Tue Nov 12 20:59:54 CST 2019 -pool-1-thread-5 End. Time = Tue Nov 12 20:59:54 CST 2019 -pool-1-thread-1 End. Time = Tue Nov 12 20:59:54 CST 2019 - -``` - -### 4.7 线程池原理分析 - -承接 4.6 节,我们通过代码输出结果可以看出:**线程池每次会同时执行 5 个任务,这 5 个任务执行完之后,剩余的 5 个任务才会被执行。** 大家可以先通过上面讲解的内容,分析一下到底是咋回事?(自己独立思考一会) - -现在,我们就分析上面的输出内容来简单分析一下线程池原理。 - -**为了搞懂线程池的原理,我们需要首先分析一下 `execute`方法。**在 4.6 节中的 Demo 中我们使用 `executor.execute(worker)`来提交一个任务到线程池中去,这个方法非常重要,下面我们来看看它的源码: - -```java - // 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount) - private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); - - private static int workerCountOf(int c) { - return c & CAPACITY; - } - - private final BlockingQueue workQueue; - - public void execute(Runnable command) { - // 如果任务为null,则抛出异常。 - if (command == null) - throw new NullPointerException(); - // ctl 中保存的线程池当前的一些状态信息 - int c = ctl.get(); - - // 下面会涉及到 3 步 操作 - // 1.首先判断当前线程池中之行的任务数量是否小于 corePoolSize - // 如果小于的话,通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。 - if (workerCountOf(c) < corePoolSize) { - if (addWorker(command, true)) - return; - c = ctl.get(); - } - // 2.如果当前之行的任务数量大于等于 corePoolSize 的时候就会走到这里 - // 通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态才会被并且队列可以加入任务,该任务才会被加入进去 - if (isRunning(c) && workQueue.offer(command)) { - int recheck = ctl.get(); - // 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。 - if (!isRunning(recheck) && remove(command)) - reject(command); - // 如果当前线程池为空就新创建一个线程并执行。 - else if (workerCountOf(recheck) == 0) - addWorker(null, false); - } - //3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。 - //如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。 - else if (!addWorker(command, false)) - reject(command); - } -``` - -通过下图可以更好的对上面这 3 步做一个展示,下图是我为了省事直接从网上找到,原地址不明。 - -![图解线程池实现原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/图解线程池实现原理.png) - -现在,让我们在回到 4.6 节我们写的 Demo, 现在应该是不是很容易就可以搞懂它的原理了呢? - -没搞懂的话,也没关系,可以看看我的分析: - -> 我们在代码中模拟了 10 个任务,我们配置的核心线程数为 5 、等待队列容量为 100 ,所以每次只可能存在 5 个任务同时执行,剩下的 5 个任务会被放到等待队列中去。当前的 5 个任务之行完成后,才会之行剩下的 5 个任务。 - -## 5. Atomic 原子类 - -### 5.1. 介绍一下Atomic 原子类 - -Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。 - -所以,所谓原子类说简单点就是具有原子/原子操作特征的类。 - - -并发包 `java.util.concurrent` 的原子类都存放在`java.util.concurrent.atomic`下,如下图所示。 - -![JUC原子类概览](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/JUC原子类概览.png) - -### 5.2. JUC 包中的原子类是哪4类? - -**基本类型** - -使用原子的方式更新基本类型 - -- AtomicInteger:整形原子类 -- AtomicLong:长整型原子类 -- AtomicBoolean:布尔型原子类 - -**数组类型** - -使用原子的方式更新数组里的某个元素 - - -- AtomicIntegerArray:整形数组原子类 -- AtomicLongArray:长整形数组原子类 -- AtomicReferenceArray:引用类型数组原子类 - -**引用类型** - -- AtomicReference:引用类型原子类 -- AtomicStampedReference:原子更新引用类型里的字段原子类 -- AtomicMarkableReference :原子更新带有标记位的引用类型 - -**对象的属性修改类型** - -- AtomicIntegerFieldUpdater:原子更新整形字段的更新器 -- AtomicLongFieldUpdater:原子更新长整形字段的更新器 -- AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。 - - -### 5.3. 讲讲 AtomicInteger 的使用 - - **AtomicInteger 类常用方法** - -```java -public final int get() //获取当前的值 -public final int getAndSet(int newValue)//获取当前的值,并设置新的值 -public final int getAndIncrement()//获取当前的值,并自增 -public final int getAndDecrement() //获取当前的值,并自减 -public final int getAndAdd(int delta) //获取当前的值,并加上预期的值 -boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update) -public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。 -``` - - **AtomicInteger 类的使用示例** - -使用 AtomicInteger 之后,不用对 increment() 方法加锁也可以保证线程安全。 -```java -class AtomicIntegerTest { - private AtomicInteger count = new AtomicInteger(); - //使用AtomicInteger之后,不需要对该方法加锁,也可以实现线程安全。 - public void increment() { - count.incrementAndGet(); - } - - public int getCount() { - return count.get(); - } -} - -``` - -### 5.4. 能不能给我简单介绍一下 AtomicInteger 类的原理 - -AtomicInteger 线程安全原理简单分析 - -AtomicInteger 类的部分源码: - -```java - // setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用) - 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; -``` - -AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。 - -CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。 - -关于 Atomic 原子类这部分更多内容可以查看我的这篇文章:并发编程面试必备:[JUC 中的 Atomic 原子类总结](https://mp.weixin.qq.com/s/joa-yOiTrYF67bElj8xqvg) - -## 6. AQS - -### 6.1. AQS 介绍 - -AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。 - -![AQS类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/AQS类.png) - -AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。 - -### 6.2. AQS 原理分析 - -AQS 原理这部分参考了部分博客,在5.2节末尾放了链接。 - -> 在面试中被问到并发知识的时候,大多都会被问到“请你说一下自己对于AQS原理的理解”。下面给大家一个示例供大家参加,面试不是背题,大家一定要加入自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来。 - -下面大部分内容其实在AQS类注释上已经给出了,不过是英语看着比较吃力一点,感兴趣的话可以看看源码。 - -#### 6.2.1. AQS 原理概览 - -**AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。** - -> CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。 - -看个AQS(AbstractQueuedSynchronizer)原理图: - - -![AQS原理图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/AQS原理图.png) - -AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。 - -```java -private volatile int state;//共享变量,使用volatile修饰保证线程可见性 -``` - -状态信息通过protected类型的getState,setState,compareAndSetState进行操作 - -```java - -//返回同步状态的当前值 -protected final int getState() { - return state; -} - // 设置同步状态的值 -protected final void setState(int newState) { - state = newState; -} -//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值) -protected final boolean compareAndSetState(int expect, int update) { - return unsafe.compareAndSwapInt(this, stateOffset, expect, update); -} -``` - -#### 6.2.2. AQS 对资源的共享方式 - -**AQS定义两种资源共享方式** - -- **Exclusive**(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁: - - 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁 - - 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的 -- **Share**(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。 - -ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。 - -不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。 - -#### 6.2.3. AQS底层使用了模板方法模式 - -同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用): - -1. 使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放) -2. 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。 - -这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。 - -**AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:** - -```java -isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。 -tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。 -tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。 -tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 -tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。 - -``` - -默认情况下,每个方法都抛出 `UnsupportedOperationException`。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS类中的其他方法都是final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。 - -以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。 - -再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS(Compare and Swap)减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。 - -一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现`tryAcquire-tryRelease`、`tryAcquireShared-tryReleaseShared`中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如`ReentrantReadWriteLock`。 - -推荐两篇 AQS 原理和相关源码分析的文章: - -- http://www.cnblogs.com/waterystone/p/4920797.html -- https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html - -### 6.3. AQS 组件总结 - -- **Semaphore(信号量)-允许多个线程同时访问:** synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。 -- **CountDownLatch (倒计时器):** CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。 -- **CyclicBarrier(循环栅栏):** CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await()方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。 - -## 7 Reference - -- 《深入理解 Java 虚拟机》 -- 《实战 Java 高并发程序设计》 -- 《Java并发编程的艺术》 -- http://www.cnblogs.com/waterystone/p/4920797.html -- https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html -- - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) diff --git a/docs/java/Multithread/synchronized.md b/docs/java/Multithread/synchronized.md deleted file mode 100644 index 4d270ff5dfe..00000000000 --- a/docs/java/Multithread/synchronized.md +++ /dev/null @@ -1,171 +0,0 @@ - - -![Synchronized 关键字使用、底层原理、JDK1.6 之后的底层优化以及 和ReenTrantLock 的对比](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/Java%20%E7%A8%8B%E5%BA%8F%E5%91%98%E5%BF%85%E5%A4%87%EF%BC%9A%E5%B9%B6%E5%8F%91%E7%9F%A5%E8%AF%86%E7%B3%BB%E7%BB%9F%E6%80%BB%E7%BB%93/%E4%BA%8C%20%20Synchronized%20%E5%85%B3%E9%94%AE%E5%AD%97%E4%BD%BF%E7%94%A8%E3%80%81%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86%E3%80%81JDK1.6%20%E4%B9%8B%E5%90%8E%E7%9A%84%E5%BA%95%E5%B1%82%E4%BC%98%E5%8C%96%E4%BB%A5%E5%8F%8A%20%E5%92%8CReenTrantLock%20%E7%9A%84%E5%AF%B9%E6%AF%94.png) - -### synchronized关键字最主要的三种使用方式的总结 - -- **修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁** -- **修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁** 。也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份,所以对该类的所有对象都加了锁)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,**因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁**。 -- **修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁。** 和 synchronized 方法一样,synchronized(this)代码块也是锁定当前对象的。synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。这里再提一下:synchronized关键字加到非 static 静态方法上是给对象实例上锁。另外需要注意的是:尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓冲功能! - -下面我已一个常见的面试题为例讲解一下 synchronized 关键字的具体使用。 - -面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理呗!” - - - -**双重校验锁实现对象单例(线程安全)** - -```java -public class Singleton { - - private volatile static Singleton uniqueInstance; - - private Singleton() { - } - - public static Singleton getUniqueInstance() { - //先判断对象是否已经实例过,没有实例化过才进入加锁代码 - if (uniqueInstance == null) { - //类对象加锁 - synchronized (Singleton.class) { - if (uniqueInstance == null) { - uniqueInstance = new Singleton(); - } - } - } - return uniqueInstance; - } -} -``` -另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。 - -uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行: - -1. 为 uniqueInstance 分配内存空间 -2. 初始化 uniqueInstance -3. 将 uniqueInstance 指向分配的内存地址 - -但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。 - -使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。 - - -###synchronized 关键字底层原理总结 - - - -**synchronized 关键字底层原理属于 JVM 层面。** - -**① synchronized 同步语句块的情况** - -```java -public class SynchronizedDemo { - public void method() { - synchronized (this) { - System.out.println("synchronized 代码块"); - } - } -} - -``` - -通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 `javac SynchronizedDemo.java` 命令生成编译后的 .class 文件,然后执行`javap -c -s -v -l SynchronizedDemo.class`。 - -![synchronized 关键字原理](https://images.gitbook.cn/abc37c80-d21d-11e8-aab3-09d30029e0d5) - -从上面我们可以看出: - -**synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。** 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权.当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。 - -**② synchronized 修饰方法的的情况** - -```java -public class SynchronizedDemo2 { - public synchronized void method() { - System.out.println("synchronized 方法"); - } -} - -``` - -![synchronized 关键字原理](https://images.gitbook.cn/7d407bf0-d21e-11e8-b2d6-1188c7e0dd7e) - -synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。 - - -在 Java 早期版本中,synchronized 属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。 - - -### JDK1.6 之后的底层优化 - -JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。 - -锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。 - -**①偏向锁** - -**引入偏向锁的目的和引入轻量级锁的目的很像,他们都是为了没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。但是不同是:轻量级锁在无竞争的情况下使用 CAS 操作去代替使用互斥量。而偏向锁在无竞争的情况下会把整个同步都消除掉**。 - -偏向锁的“偏”就是偏心的偏,它的意思是会偏向于第一个获得它的线程,如果在接下来的执行中,该锁没有被其他线程获取,那么持有偏向锁的线程就不需要进行同步!关于偏向锁的原理可以查看《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版的13章第三节锁优化。 - -但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。 - -**② 轻量级锁** - -倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的)。**轻量级锁不是为了代替重量级锁,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,因为使用轻量级锁时,不需要申请互斥量。另外,轻量级锁的加锁和解锁都用到了CAS操作。** 关于轻量级锁的加锁和解锁的原理可以查看《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版的13章第三节锁优化。 - -**轻量级锁能够提升程序同步性能的依据是“对于绝大部分锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥操作的开销。但如果存在锁竞争,除了互斥量开销外,还会额外发生CAS操作,因此在有锁竞争的情况下,轻量级锁比传统的重量级锁更慢!如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁!** - -**③ 自旋锁和自适应自旋** - -轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。 - -互斥同步对性能最大的影响就是阻塞的实现,因为挂起线程/恢复线程的操作都需要转入内核态中完成(用户态转换到内核态会耗费时间)。 - -**一般线程持有锁的时间都不是太长,所以仅仅为了这一点时间去挂起线程/恢复线程是得不偿失的。** 所以,虚拟机的开发团队就这样去考虑:“我们能不能让后面来的请求获取锁的线程等待一会而不被挂起呢?看看持有锁的线程是否很快就会释放锁”。**为了让一个线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就叫做自旋**。 - -百度百科对自旋锁的解释: - -> 何谓自旋锁?它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。 - -自旋锁在 JDK1.6 之前其实就已经引入了,不过是默认关闭的,需要通过`--XX:+UseSpinning`参数来开启。JDK1.6及1.6之后,就改为默认开启的了。需要注意的是:自旋等待不能完全替代阻塞,因为它还是要占用处理器时间。如果锁被占用的时间短,那么效果当然就很好了!反之,相反!自旋等待的时间必须要有限度。如果自旋超过了限定次数任然没有获得锁,就应该挂起线程。**自旋次数的默认值是10次,用户可以修改`--XX:PreBlockSpin`来更改**。 - -另外,**在 JDK1.6 中引入了自适应的自旋锁。自适应的自旋锁带来的改进就是:自旋的时间不在固定了,而是和前一次同一个锁上的自旋时间以及锁的拥有者的状态来决定,虚拟机变得越来越“聪明”了**。 - -**④ 锁消除** - -锁消除理解起来很简单,它指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。 - -**⑤ 锁粗化** - -原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,——直在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。 - -大部分情况下,上面的原则都是没有问题的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,那么会带来很多不必要的性能消耗。 - -### Synchronized 和 ReenTrantLock 的对比 - - -**① 两者都是可重入锁** - -两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。 - -**② synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API** - -synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。 - -**③ ReenTrantLock 比 synchronized 增加了一些高级功能** - -相比synchronized,ReenTrantLock增加了一些高级功能。主要来说主要有三点:**①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)** - -- **ReenTrantLock提供了一种能够中断等待锁的线程的机制**,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 -- **ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。** ReenTrantLock默认情况是非公平的,可以通过 ReenTrantLock类的`ReentrantLock(boolean fair)`构造方法来制定是否是公平的。 -- synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),**线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知”** ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。 - -如果你想使用上述功能,那么选择ReenTrantLock是一个不错的选择。 - -**synchronized 异常就会释放锁,而 ReenTrantLock 异常需要在 finally 里 unlock** - -**④ 性能已不是选择标准** - -在JDK1.6之前,synchronized 的性能是比 ReenTrantLock 差很多。具体表示为:synchronized 关键字吞吐量随线程数的增加,下降得非常严重。而ReenTrantLock 基本保持一个比较稳定的水平。我觉得这也侧面反映了, synchronized 关键字还有非常大的优化余地。后续的技术发展也证明了这一点,我们上面也讲了在 JDK1.6 之后 JVM 团队对 synchronized 关键字做了很多优化。**JDK1.6 之后,synchronized 和 ReenTrantLock 的性能基本是持平了。所以网上那些说因为性能才选择 ReenTrantLock 的文章都是错的!JDK1.6之后,性能已经不是选择synchronized和ReenTrantLock的影响因素了!而且虚拟机在未来的性能改进中会更偏向于原生的synchronized,所以还是提倡在synchronized能满足你的需求的情况下,优先考虑使用synchronized关键字来进行同步!优化后的synchronized和ReenTrantLock一样,在很多地方都是用到了CAS操作**。 diff --git a/docs/java/basic/Arrays,CollectionsCommonMethods.md b/docs/java/basic/Arrays,CollectionsCommonMethods.md deleted file mode 100644 index 0710de44a95..00000000000 --- a/docs/java/basic/Arrays,CollectionsCommonMethods.md +++ /dev/null @@ -1,383 +0,0 @@ - - -- [Collections 工具类和 Arrays 工具类常见方法](#collections-工具类和-arrays-工具类常见方法) - - [Collections](#collections) - - [排序操作](#排序操作) - - [查找,替换操作](#查找替换操作) - - [同步控制](#同步控制) - - [Arrays类的常见操作](#arrays类的常见操作) - - [排序 : `sort()`](#排序--sort) - - [查找 : `binarySearch()`](#查找--binarysearch) - - [比较: `equals()`](#比较-equals) - - [填充 : `fill()`](#填充--fill) - - [转列表 `asList()`](#转列表-aslist) - - [转字符串 `toString()`](#转字符串-tostring) - - [复制 `copyOf()`](#复制-copyof) - - -# Collections 工具类和 Arrays 工具类常见方法 - -## Collections - -Collections 工具类常用方法: - -1. 排序 -2. 查找,替换操作 -3. 同步控制(不推荐,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合) - -### 排序操作 - -```java -void reverse(List list)//反转 -void shuffle(List list)//随机排序 -void sort(List list)//按自然排序的升序排序 -void sort(List list, Comparator c)//定制排序,由Comparator控制排序逻辑 -void swap(List list, int i , int j)//交换两个索引位置的元素 -void rotate(List list, int distance)//旋转。当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将 list的前distance个元素整体移到后面。 -``` - -**示例代码:** - -```java - ArrayList arrayList = new ArrayList(); - arrayList.add(-1); - arrayList.add(3); - arrayList.add(3); - arrayList.add(-5); - arrayList.add(7); - arrayList.add(4); - arrayList.add(-9); - arrayList.add(-7); - System.out.println("原始数组:"); - System.out.println(arrayList); - // void reverse(List list):反转 - Collections.reverse(arrayList); - System.out.println("Collections.reverse(arrayList):"); - System.out.println(arrayList); - - - Collections.rotate(arrayList, 4); - System.out.println("Collections.rotate(arrayList, 4):"); - System.out.println(arrayList); - - // void sort(List list),按自然排序的升序排序 - Collections.sort(arrayList); - System.out.println("Collections.sort(arrayList):"); - System.out.println(arrayList); - - // void shuffle(List list),随机排序 - Collections.shuffle(arrayList); - System.out.println("Collections.shuffle(arrayList):"); - System.out.println(arrayList); - - // void swap(List list, int i , int j),交换两个索引位置的元素 - Collections.swap(arrayList, 2, 5); - System.out.println("Collections.swap(arrayList, 2, 5):"); - System.out.println(arrayList); - - // 定制排序的用法 - Collections.sort(arrayList, new Comparator() { - - @Override - public int compare(Integer o1, Integer o2) { - return o2.compareTo(o1); - } - }); - System.out.println("定制排序后:"); - System.out.println(arrayList); -``` - -### 查找,替换操作 - -```java -int binarySearch(List list, Object key)//对List进行二分查找,返回索引,注意List必须是有序的 -int max(Collection coll)//根据元素的自然顺序,返回最大的元素。 类比int min(Collection coll) -int max(Collection coll, Comparator c)//根据定制排序,返回最大元素,排序规则由Comparatator类控制。类比int min(Collection coll, Comparator c) -void fill(List list, Object obj)//用指定的元素代替指定list中的所有元素。 -int frequency(Collection c, Object o)//统计元素出现次数 -int indexOfSubList(List list, List target)//统计target在list中第一次出现的索引,找不到则返回-1,类比int lastIndexOfSubList(List source, list target). -boolean replaceAll(List list, Object oldVal, Object newVal), 用新元素替换旧元素 -``` - -**示例代码:** - -```java - ArrayList arrayList = new ArrayList(); - arrayList.add(-1); - arrayList.add(3); - arrayList.add(3); - arrayList.add(-5); - arrayList.add(7); - arrayList.add(4); - arrayList.add(-9); - arrayList.add(-7); - ArrayList arrayList2 = new ArrayList(); - arrayList2.add(-3); - arrayList2.add(-5); - arrayList2.add(7); - System.out.println("原始数组:"); - System.out.println(arrayList); - - System.out.println("Collections.max(arrayList):"); - System.out.println(Collections.max(arrayList)); - - System.out.println("Collections.min(arrayList):"); - System.out.println(Collections.min(arrayList)); - - System.out.println("Collections.replaceAll(arrayList, 3, -3):"); - Collections.replaceAll(arrayList, 3, -3); - System.out.println(arrayList); - - System.out.println("Collections.frequency(arrayList, -3):"); - System.out.println(Collections.frequency(arrayList, -3)); - - System.out.println("Collections.indexOfSubList(arrayList, arrayList2):"); - System.out.println(Collections.indexOfSubList(arrayList, arrayList2)); - - System.out.println("Collections.binarySearch(arrayList, 7):"); - // 对List进行二分查找,返回索引,List必须是有序的 - Collections.sort(arrayList); - System.out.println(Collections.binarySearch(arrayList, 7)); -``` - -### 同步控制 - -Collections提供了多个`synchronizedXxx()`方法·,该方法可以将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题。 - -我们知道 HashSet,TreeSet,ArrayList,LinkedList,HashMap,TreeMap 都是线程不安全的。Collections提供了多个静态方法可以把他们包装成线程同步的集合。 - -**最好不要用下面这些方法,效率非常低,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合。** - -方法如下: - -```java -synchronizedCollection(Collection c) //返回指定 collection 支持的同步(线程安全的)collection。 -synchronizedList(List list)//返回指定列表支持的同步(线程安全的)List。 -synchronizedMap(Map m) //返回由指定映射支持的同步(线程安全的)Map。 -synchronizedSet(Set s) //返回指定 set 支持的同步(线程安全的)set。 -``` - -### Collections还可以设置不可变集合,提供了如下三类方法: - -```java -emptyXxx(): 返回一个空的、不可变的集合对象,此处的集合既可以是List,也可以是Set,还可以是Map。 -singletonXxx(): 返回一个只包含指定对象(只有一个或一个元素)的不可变的集合对象,此处的集合可以是:List,Set,Map。 -unmodifiableXxx(): 返回指定集合对象的不可变视图,此处的集合可以是:List,Set,Map。 -上面三类方法的参数是原有的集合对象,返回值是该集合的”只读“版本。 -``` - -**示例代码:** - -```java - ArrayList arrayList = new ArrayList(); - arrayList.add(-1); - arrayList.add(3); - arrayList.add(3); - arrayList.add(-5); - arrayList.add(7); - arrayList.add(4); - arrayList.add(-9); - arrayList.add(-7); - HashSet integers1 = new HashSet<>(); - integers1.add(1); - integers1.add(3); - integers1.add(2); - Map scores = new HashMap(); - scores.put("语文" , 80); - scores.put("Java" , 82); - - //Collections.emptyXXX();创建一个空的、不可改变的XXX对象 - List list = Collections.emptyList(); - System.out.println(list);//[] - Set objects = Collections.emptySet(); - System.out.println(objects);//[] - Map objectObjectMap = Collections.emptyMap(); - System.out.println(objectObjectMap);//{} - - //Collections.singletonXXX(); - List> arrayLists = Collections.singletonList(arrayList); - System.out.println(arrayLists);//[[-1, 3, 3, -5, 7, 4, -9, -7]] - //创建一个只有一个元素,且不可改变的Set对象 - Set> singleton = Collections.singleton(arrayList); - System.out.println(singleton);//[[-1, 3, 3, -5, 7, 4, -9, -7]] - Map nihao = Collections.singletonMap("1", "nihao"); - System.out.println(nihao);//{1=nihao} - - //unmodifiableXXX();创建普通XXX对象对应的不可变版本 - List integers = Collections.unmodifiableList(arrayList); - System.out.println(integers);//[-1, 3, 3, -5, 7, 4, -9, -7] - Set integers2 = Collections.unmodifiableSet(integers1); - System.out.println(integers2);//[1, 2, 3] - Map objectObjectMap2 = Collections.unmodifiableMap(scores); - System.out.println(objectObjectMap2);//{Java=82, 语文=80} - - //添加出现异常:java.lang.UnsupportedOperationException -// list.add(1); -// arrayLists.add(arrayList); -// integers.add(1); -``` - -## Arrays类的常见操作 -1. 排序 : `sort()` -2. 查找 : `binarySearch()` -3. 比较: `equals()` -4. 填充 : `fill()` -5. 转列表: `asList()` -6. 转字符串 : `toString()` -7. 复制: `copyOf()` - - -### 排序 : `sort()` - -```java - // *************排序 sort**************** - int a[] = { 1, 3, 2, 7, 6, 5, 4, 9 }; - // sort(int[] a)方法按照数字顺序排列指定的数组。 - Arrays.sort(a); - System.out.println("Arrays.sort(a):"); - for (int i : a) { - System.out.print(i); - } - // 换行 - System.out.println(); - - // sort(int[] a,int fromIndex,int toIndex)按升序排列数组的指定范围 - int b[] = { 1, 3, 2, 7, 6, 5, 4, 9 }; - Arrays.sort(b, 2, 6); - System.out.println("Arrays.sort(b, 2, 6):"); - for (int i : b) { - System.out.print(i); - } - // 换行 - System.out.println(); - - int c[] = { 1, 3, 2, 7, 6, 5, 4, 9 }; - // parallelSort(int[] a) 按照数字顺序排列指定的数组(并行的)。同sort方法一样也有按范围的排序 - Arrays.parallelSort(c); - System.out.println("Arrays.parallelSort(c):"); - for (int i : c) { - System.out.print(i); - } - // 换行 - System.out.println(); - - // parallelSort给字符数组排序,sort也可以 - char d[] = { 'a', 'f', 'b', 'c', 'e', 'A', 'C', 'B' }; - Arrays.parallelSort(d); - System.out.println("Arrays.parallelSort(d):"); - for (char d2 : d) { - System.out.print(d2); - } - // 换行 - System.out.println(); - -``` - -在做算法面试题的时候,我们还可能会经常遇到对字符串排序的情况,`Arrays.sort()` 对每个字符串的特定位置进行比较,然后按照升序排序。 - -```java -String[] strs = { "abcdehg", "abcdefg", "abcdeag" }; -Arrays.sort(strs); -System.out.println(Arrays.toString(strs));//[abcdeag, abcdefg, abcdehg] -``` - -### 查找 : `binarySearch()` - -```java - // *************查找 binarySearch()**************** - char[] e = { 'a', 'f', 'b', 'c', 'e', 'A', 'C', 'B' }; - // 排序后再进行二分查找,否则找不到 - Arrays.sort(e); - System.out.println("Arrays.sort(e)" + Arrays.toString(e)); - System.out.println("Arrays.binarySearch(e, 'c'):"); - int s = Arrays.binarySearch(e, 'c'); - System.out.println("字符c在数组的位置:" + s); -``` - -### 比较: `equals()` - -```java - // *************比较 equals**************** - char[] e = { 'a', 'f', 'b', 'c', 'e', 'A', 'C', 'B' }; - char[] f = { 'a', 'f', 'b', 'c', 'e', 'A', 'C', 'B' }; - /* - * 元素数量相同,并且相同位置的元素相同。 另外,如果两个数组引用都是null,则它们被认为是相等的 。 - */ - // 输出true - System.out.println("Arrays.equals(e, f):" + Arrays.equals(e, f)); -``` - -### 填充 : `fill()` - -```java - // *************填充fill(批量初始化)**************** - int[] g = { 1, 2, 3, 3, 3, 3, 6, 6, 6 }; - // 数组中所有元素重新分配值 - Arrays.fill(g, 3); - System.out.println("Arrays.fill(g, 3):"); - // 输出结果:333333333 - for (int i : g) { - System.out.print(i); - } - // 换行 - System.out.println(); - - int[] h = { 1, 2, 3, 3, 3, 3, 6, 6, 6, }; - // 数组中指定范围元素重新分配值 - Arrays.fill(h, 0, 2, 9); - System.out.println("Arrays.fill(h, 0, 2, 9);:"); - // 输出结果:993333666 - for (int i : h) { - System.out.print(i); - } -``` - -### 转列表 `asList()` - -```java - // *************转列表 asList()**************** - /* - * 返回由指定数组支持的固定大小的列表。 - * (将返回的列表更改为“写入数组”。)该方法作为基于数组和基于集合的API之间的桥梁,与Collection.toArray()相结合 。 - * 返回的列表是可序列化的,并实现RandomAccess 。 - * 此方法还提供了一种方便的方式来创建一个初始化为包含几个元素的固定大小的列表如下: - */ - List stooges = Arrays.asList("Larry", "Moe", "Curly"); - System.out.println(stooges); -``` - -### 转字符串 `toString()` - -```java - // *************转字符串 toString()**************** - /* - * 返回指定数组的内容的字符串表示形式。 - */ - char[] k = { 'a', 'f', 'b', 'c', 'e', 'A', 'C', 'B' }; - System.out.println(Arrays.toString(k));// [a, f, b, c, e, A, C, B] -``` - -### 复制 `copyOf()` - -```java - // *************复制 copy**************** - // copyOf 方法实现数组复制,h为数组,6为复制的长度 - int[] h = { 1, 2, 3, 3, 3, 3, 6, 6, 6, }; - int i[] = Arrays.copyOf(h, 6); - System.out.println("Arrays.copyOf(h, 6);:"); - // 输出结果:123333 - for (int j : i) { - System.out.print(j); - } - // 换行 - System.out.println(); - // copyOfRange将指定数组的指定范围复制到新数组中 - int j[] = Arrays.copyOfRange(h, 6, 11); - System.out.println("Arrays.copyOfRange(h, 6, 11):"); - // 输出结果66600(h数组只有9个元素这里是从索引6到索引11复制所以不足的就为0) - for (int j2 : j) { - System.out.print(j2); - } - // 换行 - System.out.println(); -``` diff --git a/docs/java/BIO-NIO-AIO.md "b/docs/java/basis/BIO,NIO,AIO\346\200\273\347\273\223.md" similarity index 97% rename from docs/java/BIO-NIO-AIO.md rename to "docs/java/basis/BIO,NIO,AIO\346\200\273\347\273\223.md" index d56074cdd9d..50f6b7fec83 100644 --- a/docs/java/BIO-NIO-AIO.md +++ "b/docs/java/basis/BIO,NIO,AIO\346\200\273\347\273\223.md" @@ -11,10 +11,10 @@ - [2. NIO \(New I/O\)](#2-nio-new-io) - [2.1 NIO 简介](#21-nio-简介) - [2.2 NIO的特性/NIO与IO区别](#22-nio的特性nio与io区别) - - [1)Non-blocking IO(非阻塞IO)](#1non-blocking-io(非阻塞io)) + - [1)Non-blocking IO(非阻塞IO)](#1non-blocking-io非阻塞io) - [2)Buffer\(缓冲区\)](#2buffer缓冲区) - [3)Channel \(通道\)](#3channel-通道) - - [4)Selectors\(选择器\)](#4selectors选择器) + - [4)Selectors\(选择器\)](#4selector-选择器) - [2.3 NIO 读数据和写数据方式](#23-nio-读数据和写数据方式) - [2.4 NIO核心组件简单介绍](#24-nio核心组件简单介绍) - [2.5 代码示例](#25-代码示例) @@ -37,7 +37,7 @@ > 当你同步执行某项任务时,你需要等待其完成才能继续执行其他任务。当你异步执行某些操作时,你可以在完成另一个任务之前继续进行。 - **同步** :两个同步任务相互依赖,并且一个任务必须以依赖于另一任务的某种方式执行。 比如在`A->B`事件模型中,你需要先完成 A 才能执行B。 再换句话说,同步调用中被调用者未处理完请求之前,调用不返回,调用者会一直等待结果的返回。 -- **异步**: 两个异步的任务完全独立的,一方的执行不需要等待另外一方的执行。再换句话说,异步调用种一调用就返回结果不需要等待结果返回,当结果返回的时候通过回调函数或者其他方式拿着结果再做相关事情, +- **异步**: 两个异步的任务是完全独立的,一方的执行不需要等待另外一方的执行。再换句话说,异步调用中一调用就返回结果不需要等待结果返回,当结果返回的时候通过回调函数或者其他方式拿着结果再做相关事情, **阻塞和非阻塞** diff --git "a/docs/java/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" "b/docs/java/basis/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" similarity index 74% rename from "docs/java/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" rename to "docs/java/basis/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" index cb90a38c8e5..0f67fc7c455 100644 --- "a/docs/java/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" +++ "b/docs/java/basis/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" @@ -1,93 +1,88 @@ + -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java 面试突击》以及 Java 工程师必备学习资源。 - - + - [1. Java 基本功](#1-java-基本功) - - [1.1. Java 入门(基础概念与常识)](#11-java-入门基础概念与常识) - - [1.1.1. Java 语言有哪些特点?](#111-java-语言有哪些特点) - - [1.1.2. 关于 JVM JDK 和 JRE 最详细通俗的解答](#112-关于-jvm-jdk-和-jre-最详细通俗的解答) - - [1.1.2.1. JVM](#1121-jvm) - - [1.1.2.2. JDK 和 JRE](#1122-jdk-和-jre) - - [1.1.3. Oracle JDK 和 OpenJDK 的对比](#113-oracle-jdk-和-openjdk-的对比) - - [1.1.4. Java 和 C++的区别?](#114-java-和-c的区别) - - [1.1.5. 什么是 Java 程序的主类 应用程序和小程序的主类有何不同?](#115-什么是-java-程序的主类-应用程序和小程序的主类有何不同) - - [1.1.6. Java 应用程序与小程序之间有哪些差别?](#116-java-应用程序与小程序之间有哪些差别) - - [1.1.7. import java 和 javax 有什么区别?](#117-import-java-和-javax-有什么区别) - - [1.1.8. 为什么说 Java 语言“编译与解释并存”?](#118-为什么说-java-语言编译与解释并存) - - [1.2. Java 语法](#12-java-语法) - - [1.2.1. 字符型常量和字符串常量的区别?](#121-字符型常量和字符串常量的区别) - - [1.2.2. 关于注释?](#122-关于注释) - - [1.2.3. 标识符和关键字的区别是什么?](#123-标识符和关键字的区别是什么) - - [1.2.4. Java中有哪些常见的关键字?](#124-java中有哪些常见的关键字) - - [1.2.5. 自增自减运算符](#125-自增自减运算符) - - [1.2.6. continue、break、和return的区别是什么?](#126-continuebreak和return的区别是什么) - - [1.2.7. Java泛型了解么?什么是类型擦除?介绍一下常用的通配符?](#127-java泛型了解么什么是类型擦除介绍一下常用的通配符) - - [1.2.8. ==和equals的区别](#128-和equals的区别) - - [1.2.9. hashCode()与 equals()](#129-hashcode与-equals) - - [1.3. 基本数据类型](#13-基本数据类型) - - [1.3.1. Java中的几种基本数据类型是什么?对应的包装类型是什么?各自占用多少字节呢?](#131-java中的几种基本数据类型是什么对应的包装类型是什么各自占用多少字节呢) - - [1.3.2. 自动装箱与拆箱](#132-自动装箱与拆箱) - - [1.3.3. 8种基本类型的包装类和常量池](#133-8种基本类型的包装类和常量池) - - [1.4. 方法(函数)](#14-方法函数) - - [1.4.1. 什么是方法的返回值?返回值在类的方法里的作用是什么?](#141-什么是方法的返回值返回值在类的方法里的作用是什么) - - [1.4.2. 为什么 Java 中只有值传递?](#142-为什么-java-中只有值传递) - - [1.4.3. 重载和重写的区别](#143-重载和重写的区别) - - [1.4.3.1. 重载](#1431-重载) - - [1.4.3.2. 重写](#1432-重写) - - [1.4.4. 深拷贝 vs 浅拷贝](#144-深拷贝-vs-浅拷贝) - - [1.4.5. 方法的四种类型](#145-方法的四种类型) + - [1.1. Java 入门(基础概念与常识)](#11-java-入门基础概念与常识) + - [1.1.1. Java 语言有哪些特点?](#111-java-语言有哪些特点) + - [1.1.2. 关于 JVM JDK 和 JRE 最详细通俗的解答](#112-关于-jvm-jdk-和-jre-最详细通俗的解答) + - [1.1.2.1. JVM](#1121-jvm) + - [1.1.2.2. JDK 和 JRE](#1122-jdk-和-jre) + - [1.1.3. Oracle JDK 和 OpenJDK 的对比](#113-oracle-jdk-和-openjdk-的对比) + - [1.1.4. Java 和 C++的区别?](#114-java-和-c的区别) + - [1.1.5. 什么是 Java 程序的主类 应用程序和小程序的主类有何不同?](#115-什么是-java-程序的主类-应用程序和小程序的主类有何不同) + - [1.1.6. import java 和 javax 有什么区别?](#116-import-java-和-javax-有什么区别) + - [1.1.7. 为什么说 Java 语言“编译与解释并存”?](#117-为什么说-java-语言编译与解释并存) + - [1.2. Java 语法](#12-java-语法) + - [1.2.1. 字符型常量和字符串常量的区别?](#121-字符型常量和字符串常量的区别) + - [1.2.2. 关于注释?](#122-关于注释) + - [1.2.3. 标识符和关键字的区别是什么?](#123-标识符和关键字的区别是什么) + - [1.2.4. Java 中有哪些常见的关键字?](#124-java-中有哪些常见的关键字) + - [1.2.5. 自增自减运算符](#125-自增自减运算符) + - [1.2.6. continue、break、和 return 的区别是什么?](#126-continue-break-和-return-的区别是什么) + - [1.2.7. Java 泛型了解么?什么是类型擦除?介绍一下常用的通配符?](#127-java-泛型了解么什么是类型擦除介绍一下常用的通配符) + - [1.2.8. ==和 equals 的区别](#128-和-equals-的区别) + - [1.2.9. hashCode()与 equals()](#129-hashcode与-equals) + - [1.3. 基本数据类型](#13-基本数据类型) + - [1.3.1. Java 中的几种基本数据类型是什么?对应的包装类型是什么?各自占用多少字节呢?](#131-java-中的几种基本数据类型是什么对应的包装类型是什么各自占用多少字节呢) + - [1.3.2. 自动装箱与拆箱](#132-自动装箱与拆箱) + - [1.3.3. 8 种基本类型的包装类和常量池](#133-8-种基本类型的包装类和常量池) + - [1.4. 方法(函数)](#14-方法函数) + - [1.4.1. 什么是方法的返回值?返回值在类的方法里的作用是什么?](#141-什么是方法的返回值返回值在类的方法里的作用是什么) + - [1.4.2. 为什么 Java 中只有值传递?](#142-为什么-java-中只有值传递) + - [1.4.3. 重载和重写的区别](#143-重载和重写的区别) + - [1.4.4. 深拷贝 vs 浅拷贝](#144-深拷贝-vs-浅拷贝) + - [1.4.5. 方法的四种类型](#145-方法的四种类型) - [2. Java 面向对象](#2-java-面向对象) - - [2.1. 类和对象](#21-类和对象) - - [2.1.1. 面向对象和面向过程的区别](#211-面向对象和面向过程的区别) - - [2.1.2. 构造器 Constructor 是否可被 override?](#212-构造器-constructor-是否可被-override) - - [2.1.3. 在 Java 中定义一个不做事且没有参数的构造方法的作用](#213-在-java-中定义一个不做事且没有参数的构造方法的作用) - - [2.1.4. 成员变量与局部变量的区别有哪些?](#214-成员变量与局部变量的区别有哪些) - - [2.1.5. 创建一个对象用什么运算符?对象实体与对象引用有何不同?](#215-创建一个对象用什么运算符对象实体与对象引用有何不同) - - [2.1.6. 一个类的构造方法的作用是什么? 若一个类没有声明构造方法,该程序能正确执行吗? 为什么?](#216-一个类的构造方法的作用是什么-若一个类没有声明构造方法该程序能正确执行吗-为什么) - - [2.1.7. 构造方法有哪些特性?](#217-构造方法有哪些特性) - - [2.1.8. 在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是?](#218-在调用子类构造方法之前会先调用父类没有参数的构造方法其目的是) - - [2.1.9. 对象的相等与指向他们的引用相等,两者有什么不同?](#219-对象的相等与指向他们的引用相等两者有什么不同) - - [2.2. 面向对象三大特征](#22-面向对象三大特征) - - [2.2.1. 封装](#221-封装) - - [2.2.2. 继承](#222-继承) - - [2.2.3. 多态](#223-多态) - - [2.3. 修饰符](#23-修饰符) - - [2.3.1. 在一个静态方法内调用一个非静态成员为什么是非法的?](#231-在一个静态方法内调用一个非静态成员为什么是非法的) - - [2.3.2. 静态方法和实例方法有何不同](#232-静态方法和实例方法有何不同) - - [2.3.3. 常见关键字总结:static,final,this,super](#233-常见关键字总结staticfinalthissuper) - - [2.4. 接口和抽象类](#24-接口和抽象类) - - [2.4.1. 接口和抽象类的区别是什么?](#241-接口和抽象类的区别是什么) - - [2.5. 其它重要知识点](#25-其它重要知识点) - - [2.5.1. String StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的?](#251-string-stringbuffer-和-stringbuilder-的区别是什么-string-为什么是不可变的) - - [2.5.2. Object 类的常见方法总结](#252-object-类的常见方法总结) - - [2.5.3. == 与 equals(重要)](#253--与-equals重要) - - [2.5.4. hashCode 与 equals (重要)](#254-hashcode-与-equals-重要) - - [2.5.4.1. hashCode()介绍](#2541-hashcode介绍) - - [2.5.4.2. 为什么要有 hashCode](#2542-为什么要有-hashcode) - - [2.5.4.3. hashCode()与 equals()的相关规定](#2543-hashcode与-equals的相关规定) - - [2.5.5. Java 序列化中如果有些字段不想进行序列化,怎么办?](#255-java-序列化中如果有些字段不想进行序列化怎么办) - - [2.5.6. 获取用键盘输入常用的两种方法](#256-获取用键盘输入常用的两种方法) + - [2.1. 类和对象](#21-类和对象) + - [2.1.1. 面向对象和面向过程的区别](#211-面向对象和面向过程的区别) + - [2.1.2. 构造器 Constructor 是否可被 override?](#212-构造器-constructor-是否可被-override) + - [2.1.3. 在 Java 中定义一个不做事且没有参数的构造方法的作用](#213-在-java-中定义一个不做事且没有参数的构造方法的作用) + - [2.1.4. 成员变量与局部变量的区别有哪些?](#214-成员变量与局部变量的区别有哪些) + - [2.1.5. 创建一个对象用什么运算符?对象实体与对象引用有何不同?](#215-创建一个对象用什么运算符对象实体与对象引用有何不同) + - [2.1.6. 一个类的构造方法的作用是什么? 若一个类没有声明构造方法,该程序能正确执行吗? 为什么?](#216-一个类的构造方法的作用是什么-若一个类没有声明构造方法该程序能正确执行吗-为什么) + - [2.1.7. 构造方法有哪些特性?](#217-构造方法有哪些特性) + - [2.1.8. 在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是?](#218-在调用子类构造方法之前会先调用父类没有参数的构造方法其目的是) + - [2.1.9. 对象的相等与指向他们的引用相等,两者有什么不同?](#219-对象的相等与指向他们的引用相等两者有什么不同) + - [2.2. 面向对象三大特征](#22-面向对象三大特征) + - [2.2.1. 封装](#221-封装) + - [2.2.2. 继承](#222-继承) + - [2.2.3. 多态](#223-多态) + - [2.3. 修饰符](#23-修饰符) + - [2.3.1. 在一个静态方法内调用一个非静态成员为什么是非法的?](#231-在一个静态方法内调用一个非静态成员为什么是非法的) + - [2.3.2. 静态方法和实例方法有何不同](#232-静态方法和实例方法有何不同) + - [2.5. 其它重要知识点](#25-其它重要知识点) + - [2.5.1. String StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的?](#251-string-stringbuffer-和-stringbuilder-的区别是什么-string-为什么是不可变的) + - [2.5.2. Object 类的常见方法总结](#252-object-类的常见方法总结) + - [2.5.3. == 与 equals(重要)](#253-与-equals重要) + - [2.5.4. hashCode 与 equals (重要)](#254-hashcode-与-equals-重要) + - [2.5.4.1. hashCode()介绍](#2541-hashcode介绍) + - [2.5.4.2. 为什么要有 hashCode](#2542-为什么要有-hashcode) + - [2.5.4.3. hashCode()与 equals()的相关规定](#2543-hashcode与-equals的相关规定) + - [2.5.5. Java 序列化中如果有些字段不想进行序列化,怎么办?](#255-java-序列化中如果有些字段不想进行序列化怎么办) + - [2.5.6. 获取用键盘输入常用的两种方法](#256-获取用键盘输入常用的两种方法) - [3. Java 核心技术](#3-java-核心技术) - - [3.1. 集合](#31-集合) - - [3.1.1. Collections 工具类和 Arrays 工具类常见方法总结](#311-collections-工具类和-arrays-工具类常见方法总结) - - [3.2. 异常](#32-异常) - - [3.2.1. Java 异常类层次结构图](#321-java-异常类层次结构图) - - [3.2.2. Throwable 类常用方法](#322-throwable-类常用方法) - - [3.2.3. try-catch-finally](#323-try-catch-finally) - - [3.2.4. 使用 `try-with-resources` 来代替`try-catch-finally`](#324-使用-try-with-resources-来代替try-catch-finally) - - [3.3. 多线程](#33-多线程) - - [3.3.1. 简述线程、程序、进程的基本概念。以及他们之间关系是什么?](#331-简述线程程序进程的基本概念以及他们之间关系是什么) - - [3.3.2. 线程有哪些基本状态?](#332-线程有哪些基本状态) - - [3.4. 文件与 I\O 流](#34-文件与-i\o-流) - - [3.4.1. Java 中 IO 流分为几种?](#341-java-中-io-流分为几种) - - [3.4.1.1. 既然有了字节流,为什么还要有字符流?](#3411-既然有了字节流为什么还要有字符流) - - [3.4.1.2. BIO,NIO,AIO 有什么区别?](#3412-bionioaio-有什么区别) + - [3.1. 反射机制](#31-反射机制) + - [3.1.1.静态编译和动态编译](#311静态编译和动态编译) + - [3.1.2.反射机制优缺点](#312反射机制优缺点) + - [3.1.3.反射的应用场景](#313反射的应用场景) + - [3.2. 异常](#32-异常) + - [3.2.1. Java 异常类层次结构图](#321-java-异常类层次结构图) + - [3.2.2. Throwable 类常用方法](#322-throwable-类常用方法) + - [3.2.3. try-catch-finally](#323-try-catch-finally) + - [3.2.4. 使用 `try-with-resources` 来代替`try-catch-finally`](#324-使用-try-with-resources-来代替try-catch-finally) + - [3.3. 多线程](#33-多线程) + - [3.3.1. 简述线程、程序、进程的基本概念。以及他们之间关系是什么?](#331-简述线程-程序-进程的基本概念以及他们之间关系是什么) + - [3.3.2. 线程有哪些基本状态?](#332-线程有哪些基本状态) + - [3.4. 文件与 I\O 流](#34-文件与-io-流) + - [3.4.1. Java 中 IO 流分为几种?](#341-java-中-io-流分为几种) + - [3.4.1.1. 既然有了字节流,为什么还要有字符流?](#3411-既然有了字节流为什么还要有字符流) + - [3.4.1.2. BIO,NIO,AIO 有什么区别?](#3412-bionioaio-有什么区别) - [4. 参考](#4-参考) -- [5. 公众号](#5-公众号) - + + ## 1. Java 基本功 @@ -130,7 +125,7 @@ Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不 ##### 1.1.2.2. JDK 和 JRE -JDK 是 Java Development Kit,它是功能齐全的 Java SDK。它拥有 JRE 所拥有的一切,还有编译器(javac)和工具(如 javadoc 和 jdb)。它能够创建和编译程序。 +JDK 是 Java Development Kit 缩写,它是功能齐全的 Java SDK。它拥有 JRE 所拥有的一切,还有编译器(javac)和工具(如 javadoc 和 jdb)。它能够创建和编译程序。 JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。但是,它不能用于创建新程序。 @@ -162,24 +157,20 @@ JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有 - 都是面向对象的语言,都支持封装、继承和多态 - Java 不提供指针来直接访问内存,程序内存更加安全 - Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。 -- Java 有自动内存管理机制,不需要程序员手动释放无用内存 -- **在 C 语言中,字符串或字符数组最后都会有一个额外的字符‘\0’来表示结束。但是,Java 语言中没有结束符这一概念。** 这是一个值得深度思考的问题,具体原因推荐看这篇文章: [https://blog.csdn.net/sszgg2006/article/details/49148189](https://blog.csdn.net/sszgg2006/article/details/49148189) +- Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存 +- **在 C 语言中,字符串或字符数组最后都会有一个额外的字符`'\0'`来表示结束。但是,Java 语言中没有结束符这一概念。** 这是一个值得深度思考的问题,具体原因推荐看这篇文章: [https://blog.csdn.net/sszgg2006/article/details/49148189](https://blog.csdn.net/sszgg2006/article/details/49148189) #### 1.1.5. 什么是 Java 程序的主类 应用程序和小程序的主类有何不同? -一个程序中可以有多个类,但只能有一个类是主类。在 Java 应用程序中,这个主类是指包含 main()方法的类。而在 Java 小程序中,这个主类是一个继承自系统类 JApplet 或 Applet 的子类。应用程序的主类不一定要求是 public 类,但小程序的主类要求必须是 public 类。主类是 Java 程序执行的入口点。 - -#### 1.1.6. Java 应用程序与小程序之间有哪些差别? +一个程序中可以有多个类,但只能有一个类是主类。在 Java 应用程序中,这个主类是指包含 `main()` 方法的类。而在 Java 小程序中,这个主类是一个继承自系统类 JApplet 或 Applet 的子类。应用程序的主类不一定要求是 public 类,但小程序的主类要求必须是 public 类。主类是 Java 程序执行的入口点。 -简单说应用程序是从主线程启动(也就是 `main()` 方法)。applet 小程序没有 `main()` 方法,主要是嵌在浏览器页面上运行(调用`init()`或者`run()`来启动),嵌入浏览器这点跟 flash 的小游戏类似。 - -#### 1.1.7. import java 和 javax 有什么区别? +#### 1.1.6. import java 和 javax 有什么区别? 刚开始的时候 JavaAPI 所必需的包是 java 开头的包,javax 当时只是扩展 API 包来使用。然而随着时间的推移,javax 逐渐地扩展成为 Java API 的组成部分。但是,将扩展从 javax 包移动到 java 包确实太麻烦了,最终会破坏一堆现有的代码。因此,最终决定 javax 包将成为标准 API 的一部分。 所以,实际上 java 和 javax 没有区别。这都是一个名字。 -#### 1.1.8. 为什么说 Java 语言“编译与解释并存”? +#### 1.1.7. 为什么说 Java 语言“编译与解释并存”? 高级编程语言按照程序的执行方式分为编译型和解释型两种。简单来说,编译型语言是指编译器针对特定的操作系统将源代码一次性翻译成可被该平台执行的机器码;解释型语言是指解释器对源程序逐行解释成特定平台的机器码并立即执行。比如,你想阅读一本英文名著,你可以找一个英文翻译人员帮助你阅读, 有两种选择方式,你可以先等翻译人员将全本的英文名著(也就是源码)都翻译成汉语,再去阅读,也可以让翻译人员翻译一段,你在旁边阅读一段,慢慢把书读完。 @@ -190,9 +181,11 @@ Java 语言既具有编译型语言的特征,也具有解释型语言的特征 #### 1.2.1. 字符型常量和字符串常量的区别? -1. 形式上: 字符常量是单引号引起的一个字符; 字符串常量是双引号引起的若干个字符 +1. 形式上: 字符常量是单引号引起的一个字符; 字符串常量是双引号引起的 0 个或若干个字符 2. 含义上: 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置) -3. 占内存大小 字符常量只占 2 个字节; 字符串常量占若干个字节 (**注意: char 在 Java 中占两个字节**) +3. 占内存大小 字符常量只占 2 个字节; 字符串常量占若干个字节 (**注意: char 在 Java 中占两个字节**), + + > 字符封装类 `Character` 有一个成员常量 `Character.SIZE` 值为 16,单位是`bits`,该值除以 8(`1byte=8bits`)后就可以得到 2 个字节 > java 编程思想第四版:2.2.2 节 > ![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-15/86735519.jpg) @@ -207,7 +200,7 @@ Java 中的注释有三种: 3. 文档注释。 -在我们编写代码的时候,如果代码量比较少,我们自己或者团队其他成员还可以很轻易地看懂代码,但是当项目结构一旦复杂起来,我们就需要用到注释了。注释并不会执行,是我们程序员写给自己看的,注释是你的代码说明书,能够帮助看代码的人快速地理清代码之间的逻辑关系。因此,在写程序的时候随手加上注释是一个非常好的习惯。 +在我们编写代码的时候,如果代码量比较少,我们自己或者团队其他成员还可以很轻易地看懂代码,但是当项目结构一旦复杂起来,我们就需要用到注释了。注释并不会执行(编译器在编译代码之前会把代码中的所有注释抹掉,字节码中不保留注释),是我们程序员写给自己看的,注释是你的代码说明书,能够帮助看代码的人快速地理清代码之间的逻辑关系。因此,在写程序的时候随手加上注释是一个非常好的习惯。 《Clean Code》这本书明确指出: @@ -234,7 +227,7 @@ Java 中的注释有三种: 在我们编写程序的时候,需要大量地为程序、类、变量、方法等取名字,于是就有了标识符,简单来说,标识符就是一个名字。但是有一些标识符,Java 语言已经赋予了其特殊的含义,只能用于特定的地方,这种特殊的标识符就是关键字。因此,关键字是被赋予特殊含义的标识符。比如,在我们的日常生活中 ,“警察局”这个名字已经被赋予了特殊的含义,所以如果你开一家店,店的名字不能叫“警察局”,“警察局”就是我们日常生活中的关键字。 -#### 1.2.4. Java中有哪些常见的关键字? +#### 1.2.4. Java 中有哪些常见的关键字? | 访问控制 | private | protected | public | | | | | | -------------------- | -------- | ---------- | -------- | ------------ | ---------- | --------- | ------ | @@ -253,9 +246,9 @@ Java 中的注释有三种: 在写代码的过程中,常见的一种情况是需要某个整数类型变量增加 1 或减少 1,Java 提供了一种特殊的运算符,用于这种表达式,叫做自增运算符(++)和自减运算符(--)。 -++和--运算符可以放在操作数之前,也可以放在操作数之后,当运算符放在操作数之前时,先自增/减,再赋值;当运算符放在操作数之后时,先赋值,再自增/减。例如,当“b=++a”时,先自增(自己增加 1),再赋值(赋值给 b);当“b=a++”时,先赋值(赋值给 b),再自增(自己增加 1)。也就是,++a 输出的是 a+1 的值,a++输出的是 a 值。用一句口诀就是:“符号在前就先加/减,符号在后就后加/减”。 +++和--运算符可以放在变量之前,也可以放在变量之后,当运算符放在变量之前时(前缀),先自增/减,再赋值;当运算符放在变量之后时(后缀),先赋值,再自增/减。例如,当 `b = ++a` 时,先自增(自己增加 1),再赋值(赋值给 b);当 `b = a++` 时,先赋值(赋值给 b),再自增(自己增加 1)。也就是,++a 输出的是 a+1 的值,a++输出的是 a 值。用一句口诀就是:“符号在前就先加/减,符号在后就后加/减”。 -#### 1.2.6. continue、break、和return的区别是什么? +#### 1.2.6. continue、break、和 return 的区别是什么? 在循环结构中,当循环条件不满足或者循环次数达到要求时,循环会正常结束。但是,有时候可能需要在循环的过程中,当发生了某种条件之后 ,提前终止循环,这就需要用到下面几个关键词: @@ -267,11 +260,11 @@ return 用于跳出所在方法,结束该方法的运行。return 一般有两 1. `return;` :直接使用 return 结束方法执行,用于没有返回值函数的方法 2. `return value;` :return 一个特定值,用于有返回值函数的方法 -#### 1.2.7. Java泛型了解么?什么是类型擦除?介绍一下常用的通配符? +#### 1.2.7. Java 泛型了解么?什么是类型擦除?介绍一下常用的通配符? Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。 -**Java的泛型是伪泛型,这是因为Java在编译期间,所有的泛型信息都会被擦掉,这也就是通常所说类型擦除 。** 更多关于类型擦除的问题,可以查看这篇文章:[《Java泛型类型擦除以及类型擦除带来的问题》](https://www.cnblogs.com/wuqinglong/p/9456193.html) 。 +**Java 的泛型是伪泛型,这是因为 Java 在编译期间,所有的泛型信息都会被擦掉,这也就是通常所说类型擦除 。** 更多关于类型擦除的问题,可以查看这篇文章:[《Java 泛型类型擦除以及类型擦除带来的问题》](https://www.cnblogs.com/wuqinglong/p/9456193.html) 。 ```java List list = new ArrayList<>(); @@ -294,15 +287,15 @@ System.out.println(list) ```java //此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型 //在实例化泛型类时,必须指定T的具体类型 -public class Generic{ - +public class Generic{ + private T key; - public Generic(T key) { + public Generic(T key) { this.key = key; } - public T getKey(){ + public T getKey(){ return key; } } @@ -348,8 +341,8 @@ class GeneratorImpl implements Generator{ ```java public static < E > void printArray( E[] inputArray ) - { - for ( E element : inputArray ){ + { + for ( E element : inputArray ){ System.out.printf( "%s ", element ); } System.out.println(); @@ -362,24 +355,24 @@ class GeneratorImpl implements Generator{ // 创建不同类型数组: Integer, Double 和 Character Integer[] intArray = { 1, 2, 3 }; String[] stringArray = { "Hello", "World" }; -printArray( intArray ); -printArray( stringArray ); +printArray( intArray ); +printArray( stringArray ); ``` **常用的通配符为: T,E,K,V,?** - ? 表示不确定的 java 类型 -- T (type) 表示具体的一个java类型 -- K V (key value) 分别代表java键值中的Key Value -- E (element) 代表Element +- T (type) 表示具体的一个 java 类型 +- K V (key value) 分别代表 java 键值中的 Key Value +- E (element) 代表 Element -更多关于Java 泛型中的通配符可以查看这篇文章:[《聊一聊-JAVA 泛型中的通配符 T,E,K,V,?》](https://juejin.im/post/5d5789d26fb9a06ad0056bd9) +更多关于 Java 泛型中的通配符可以查看这篇文章:[《聊一聊-JAVA 泛型中的通配符 T,E,K,V,?》](https://juejin.im/post/5d5789d26fb9a06ad0056bd9) -#### 1.2.8. ==和equals的区别 +#### 1.2.8. ==和 equals 的区别 **`==`** : 它的作用是判断两个对象的地址是不是相等。即判断两个对象是不是同一个对象。(**基本数据类型==比较的是值,引用数据类型==比较的是内存地址**) -> 因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。 +> 因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。 **`equals()`** : 它的作用也是判断两个对象是否相等,它不能用于比较基本数据类型的变量。`equals()`方法存在于`Object`类中,而`Object`类是所有类的直接或间接父类。 @@ -393,7 +386,7 @@ public boolean equals(Object obj) { `equals()` 方法存在两种使用情况: -- 情况 1:类没有覆盖 `equals()`方法。则通过` equals()`比较该类的两个对象时,等价于通过“==”比较这两个对象。使用的默认是 `Object`类`equals()`方法。 +- 情况 1:类没有覆盖 `equals()`方法。则通过`equals()`比较该类的两个对象时,等价于通过“==”比较这两个对象。使用的默认是 `Object`类`equals()`方法。 - 情况 2:类覆盖了 `equals()`方法。一般,我们都覆盖 `equals()`方法来两个对象的内容相等;若它们的内容相等,则返回 true(即,认为这两个对象相等)。 **举个例子:** @@ -451,11 +444,11 @@ public boolean equals(Object anObject) { #### 1.2.9. hashCode()与 equals() -面试官可能会问你:“你重写过 `hashcode` 和 `equals `么,为什么重写 `equals` 时必须重写 `hashCode` 方法?” +面试官可能会问你:“你重写过 `hashcode` 和 `equals`么,为什么重写 `equals` 时必须重写 `hashCode` 方法?” **1)hashCode()介绍:** -`hashCode()` 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。`hashCode() `定义在 JDK 的 `Object` 类中,这就意味着 Java 中的任何类都包含有 `hashCode()` 函数。另外需要注意的是: `Object` 的 hashcode 方法是本地方法,也就是用 c 语言或 c++ 实现的,该方法通常用来将对象的 内存地址 转换为整数之后返回。 +`hashCode()` 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。`hashCode()`定义在 JDK 的 `Object` 类中,这就意味着 Java 中的任何类都包含有 `hashCode()` 函数。另外需要注意的是: `Object` 的 hashcode 方法是本地方法,也就是用 c 语言或 c++ 实现的,该方法通常用来将对象的 内存地址 转换为整数之后返回。 ```java public native int hashCode(); @@ -467,13 +460,13 @@ public native int hashCode(); 我们以“`HashSet` 如何检查重复”为例子来说明为什么要有 hashCode? -当你把对象加入 `HashSet` 时,`HashSet` 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,`HashSet` 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,`HashSet` 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的 Java 启蒙书《Head fist java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。 +当你把对象加入 `HashSet` 时,`HashSet` 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,`HashSet` 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 `equals()` 方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,`HashSet` 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的 Java 启蒙书《Head First Java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。 **3)为什么重写 `equals` 时必须重写 `hashCode` 方法?** 如果两个对象相等,则 hashcode 一定也是相同的。两个对象相等,对两个对象分别调用 equals 方法都返回 true。但是,两个对象有相同的 hashcode 值,它们也不一定是相等的 。**因此,equals 方法被覆盖过,则 `hashCode` 方法也必须被覆盖。** -> `hashCode()`的默认行为是对堆上的对象产生独特值。如果没有重写 `hashCode()`,则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据) +> `hashCode()`的默认行为是对堆上的对象产生独特值。如果没有重写 `hashCode()`,则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据) **4)为什么两个对象有相同的 hashcode 值,它们也不一定是相等的?** @@ -483,20 +476,17 @@ public native int hashCode(); 我们刚刚也提到了 `HashSet`,如果 `HashSet` 在对比的时候,同样的 hashcode 有多个对象,它会使用 `equals()` 来判断是否真的相同。也就是说 `hashcode` 只是用来缩小查找成本。 - - - 更多关于 `hashcode()` 和 `equals()` 的内容可以查看:[Java hashCode() 和 equals()的若干问题解答](https://www.cnblogs.com/skywang12345/p/3324958.html) ### 1.3. 基本数据类型 -#### 1.3.1. Java中的几种基本数据类型是什么?对应的包装类型是什么?各自占用多少字节呢? +#### 1.3.1. Java 中的几种基本数据类型是什么?对应的包装类型是什么?各自占用多少字节呢? -Java**中**有8种基本数据类型,分别为: +Java**中**有 8 种基本数据类型,分别为: -1. 6种数字类型 :byte、short、int、long、float、double -2. 1种字符类型:char -3. 1种布尔型:boolean。 +1. 6 种数字类型 :byte、short、int、long、float、double +2. 1 种字符类型:char +3. 1 种布尔型:boolean。 这八种基本类型都有对应的包装类分别为:Byte、Short、Integer、Long、Float、Double、Character、Boolean @@ -511,7 +501,7 @@ Java**中**有8种基本数据类型,分别为: | double | 64 | 8 | 0d | | boolean | 1 | | false | -对于boolean,官方文档未明确定义,它依赖于 JVM 厂商的具体实现。逻辑上理解是占用 1位,但是实际中会考虑计算机高效存储因素。 +对于 boolean,官方文档未明确定义,它依赖于 JVM 厂商的具体实现。逻辑上理解是占用 1 位,但是实际中会考虑计算机高效存储因素。 注意: @@ -525,9 +515,9 @@ Java**中**有8种基本数据类型,分别为: 更多内容见:[深入剖析 Java 中的装箱和拆箱](https://www.cnblogs.com/dolphin0520/p/3780005.html) -#### 1.3.3. 8种基本类型的包装类和常量池 +#### 1.3.3. 8 种基本类型的包装类和常量池 -**Java 基本类型的包装类的大部分都实现了常量池技术,即 Byte,Short,Integer,Long,Character,Boolean;前面 4 种包装类默认创建了数值[-128,127] 的相应类型的缓存数据,Character创建了数值在[0,127]范围的缓存数据,Boolean 直接返回True Or False。如果超出对应范围仍然会去创建新的对象。** 为啥把缓存设置为[-128,127]区间?([参见issue/461](https://github.com/Snailclimb/JavaGuide/issues/461))性能和资源之间的权衡。 +**Java 基本类型的包装类的大部分都实现了常量池技术,即 Byte,Short,Integer,Long,Character,Boolean;前面 4 种包装类默认创建了数值[-128,127] 的相应类型的缓存数据,Character 创建了数值在[0,127]范围的缓存数据,Boolean 直接返回 True Or False。如果超出对应范围仍然会去创建新的对象。** 为啥把缓存设置为[-128,127]区间?([参见 issue/461](https://github.com/Snailclimb/JavaGuide/issues/461))性能和资源之间的权衡。 ```java public static Boolean valueOf(boolean b) { @@ -536,18 +526,18 @@ public static Boolean valueOf(boolean b) { ``` ```java -private static class CharacterCache { +private static class CharacterCache { private CharacterCache(){} - - static final Character cache[] = new Character[127 + 1]; - static { - for (int i = 0; i < cache.length; i++) - cache[i] = new Character((char)i); - } + + static final Character cache[] = new Character[127 + 1]; + static { + for (int i = 0; i < cache.length; i++) + cache[i] = new Character((char)i); + } } ``` -两种浮点数类型的包装类 Float,Double 并没有实现常量池技术。** +**两种浮点数类型的包装类 Float,Double 并没有实现常量池技术。** ```java Integer i1 = 33; @@ -561,7 +551,7 @@ private static class CharacterCache { System.out.println(i3 == i4);// 输出 false ``` -**Integer 缓存源代码:** +**Integer 缓存源代码:** ```java /** @@ -576,14 +566,16 @@ private static class CharacterCache { ``` **应用场景:** + 1. Integer i1=40;Java 在编译的时候会直接将代码封装成 Integer i1=Integer.valueOf(40);,从而使用常量池中的对象。 -2. Integer i1 = new Integer(40);这种情况下会创建新的对象。 +2. Integer i1 = new Integer(40);这种情况下会创建新的对象。 ```java Integer i1 = 40; Integer i2 = new Integer(40); - System.out.println(i1==i2);//输出 false + System.out.println(i1 == i2);//输出 false ``` + **Integer 比较更丰富的一个例子:** ```java @@ -593,13 +585,13 @@ private static class CharacterCache { Integer i4 = new Integer(40); Integer i5 = new Integer(40); Integer i6 = new Integer(0); - + System.out.println("i1=i2 " + (i1 == i2)); System.out.println("i1=i2+i3 " + (i1 == i2 + i3)); System.out.println("i1=i4 " + (i1 == i4)); System.out.println("i4=i5 " + (i4 == i5)); - System.out.println("i4=i5+i6 " + (i4 == i5 + i6)); - System.out.println("40=i5+i6 " + (40 == i5 + i6)); + System.out.println("i4=i5+i6 " + (i4 == i5 + i6)); + System.out.println("40=i5+i6 " + (40 == i5 + i6)); ``` 结果: @@ -770,7 +762,7 @@ Java 程序设计语言对对象采用的不是引用调用,实际上,对象 > > 重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法 -###### 1.4.3.1. 重载 +**重载:** 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。 @@ -778,9 +770,9 @@ Java 程序设计语言对对象采用的不是引用调用,实际上,对象 ![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/bg/desktopjava核心技术-重载.jpg) -**综上:重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理。** +综上:重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理。 -###### 1.4.3.2. 重写 +**重写:** 重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。 @@ -788,20 +780,18 @@ Java 程序设计语言对对象采用的不是引用调用,实际上,对象 2. 如果父类方法访问修饰符为 `private/final/static` 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。 3. 构造方法无法被重写 -**综上:重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变** +综上:重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变 -**暖心的 Guide 哥最后再来个图表总结一下!** +暖心的 Guide 哥最后再来个图表总结一下! -| 区别点 | 重载方法 | 重写方法 | -| :--------- | :------- | :----------------------------------------------------------- | -| 发生范围 | 同一个类 | 子类 | -| 参数列表 | 必须修改 | 一定不能修改 | -| 返回类型 | 可修改 | 子类方法返回值类型应比父类方法返回值类型更小或相等 | +| 区别点 | 重载方法 | 重写方法 | +| :--------- | :------- | :--------------------------------------------------------------- | +| 发生范围 | 同一个类 | 子类 | +| 参数列表 | 必须修改 | 一定不能修改 | +| 返回类型 | 可修改 | 子类方法返回值类型应比父类方法返回值类型更小或相等 | | 异常 | 可修改 | 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等; | -| 访问修饰符 | 可修改 | 一定不能做更严格的限制(可以降低限制) | -| 发生阶段 | 编译期 | 运行期 | - - +| 访问修饰符 | 可修改 | 一定不能做更严格的限制(可以降低限制) | +| 发生阶段 | 编译期 | 运行期 | **方法的重写要遵循“两同两小一大”**(以下内容摘录自《疯狂 Java 讲义》,[issue#892](https://github.com/Snailclimb/JavaGuide/issues/892) ): @@ -863,9 +853,9 @@ public int f4(int a, int b) { ```java // return在无返回值方法的特殊使用 public void f5(int a) { - if (a>10) { - return;//表示结束所在方法 (f5方法)的执行,下方的输出语句不会执行 -} + if (a > 10) { + return;//表示结束所在方法 (f5方法)的执行,下方的输出语句不会执行 + } System.out.println(a); } ``` @@ -895,7 +885,7 @@ Java 程序在执行子类的构造方法之前,如果没有用 `super()`来 #### 2.1.4. 成员变量与局部变量的区别有哪些? -1. 从语法形式上看:成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。 +1. 从语法形式上看:成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。 2. 从变量在内存中的存储方式来看:如果成员变量是使用`static`修饰的,那么这个成员变量是属于类的,如果没有使用`static`修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。 3. 从变量在内存中的生存时间上看:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。 4. 成员变量如果没有被赋初值:则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。 @@ -972,8 +962,6 @@ public class Student { **多态的特点:** - 对象类型和引用类型之间具有继承(类)/实现(接口)的关系; -- 对象类型不可变,引用类型可变; -- 方法具有多态性,属性不具有多态性; - 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定; - 多态不能调用“只在子类存在但在父类不存在”的方法; - 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。 @@ -990,36 +978,11 @@ public class Student { 2. 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制。 -#### 2.3.3. 常见关键字总结:static,final,this,super - -详见笔主的这篇文章: https://snailclimb.gitee.io/javaguide/#/docs/java/basic/final,static,this,super - -### 2.4. 接口和抽象类 - -#### 2.4.1. 接口和抽象类的区别是什么? - -1. 接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),而抽象类可以有非抽象的方法。 -2. 接口中除了 static、final 变量,不能有其他变量,而抽象类中则不一定。 -3. 一个类可以实现多个接口,但只能实现一个抽象类。接口自己本身可以通过 extends 关键字扩展多个接口。 -4. 接口方法默认修饰符是 public,抽象方法可以有 public、protected 和 default 这些修饰符(抽象方法就是为了被重写所以不能使用 private 关键字修饰!)。 -5. 从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。 - -> 备注: -> -> 1. 在 JDK8 中,接口也可以定义静态方法,可以直接用接口名调用。实现类和实现是不可以调用的。如果同时实现两个接口,接口中定义了一样的默认方法,则必须重写,不然会报错。(详见 issue:[https://github.com/Snailclimb/JavaGuide/issues/146](https://github.com/Snailclimb/JavaGuide/issues/146)。 -> 2. jdk9 的接口被允许定义私有方法 。 - -总结一下 jdk7~jdk9 Java 中接口概念的变化([相关阅读](https://www.geeksforgeeks.org/private-methods-java-9-interfaces/)): - -1. 在 jdk 7 或更早版本中,接口里面只能有常量变量和抽象方法。这些接口方法必须由选择实现接口的类实现。 -2. jdk8 的时候接口可以有默认方法和静态方法功能。 -3. Jdk 9 在接口中引入了私有方法和私有静态方法。 - ### 2.5. 其它重要知识点 #### 2.5.1. String StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的? -简单的来说:`String` 类中使用 final 关键字修饰字符数组来保存字符串,`private final char value[]`,所以` String` 对象是不可变的。 +简单的来说:`String` 类中使用 final 关键字修饰字符数组来保存字符串,`private final char value[]`,所以`String` 对象是不可变的。 > 补充(来自[issue 675](https://github.com/Snailclimb/JavaGuide/issues/675)):在 Java 9 之后,String 类的实现改用 byte 数组存储字符串 `private final byte[] value`; @@ -1125,7 +1088,6 @@ public class test1 { - String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。 - 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。 - #### 2.5.4. hashCode 与 equals (重要) 面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写 equals 时必须重写 hashCode 方法?” @@ -1177,35 +1139,65 @@ String s = input.readLine(); ## 3. Java 核心技术 -### 3.1. 集合 +### 3.1. 反射机制 + +JAVA 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 java 语言的反射机制。 + +#### 3.1.1.静态编译和动态编译 + +- **静态编译:** 在编译时确定类型,绑定对象 +- **动态编译:** 运行时确定类型,绑定对象 + +#### 3.1.2.反射机制优缺点 + +- **优点:** 运行期类型的判断,动态加载类,提高代码灵活度。 +- **缺点:** 1,性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的 java 代码要慢很多。2,安全问题,让我们可以动态操作改变类的属性同时也增加了类的安全隐患。 + +#### 3.1.3.反射的应用场景 + +**反射是框架设计的灵魂。** -#### 3.1.1. Collections 工具类和 Arrays 工具类常见方法总结 +在我们平时的项目开发过程中,基本上很少会直接使用到反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架也大量使用到了反射机制。 -详见笔主的这篇文章: https://gitee.com/SnailClimb/JavaGuide/blob/master/docs/java/basic/Arrays,CollectionsCommonMethods.md +举例: + +1. 我们在使用 JDBC 连接数据库时使用 `Class.forName()`通过反射加载数据库的驱动程序; +2. Spring 框架的 IOC(动态加载管理 Bean)创建对象以及 AOP(动态代理)功能都和反射有联系; +3. 动态配置实例的属性; +4. ...... ### 3.2. 异常 #### 3.2.1. Java 异常类层次结构图 -![](images/Java异常类层次结构图.png) - +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-12/Java%E5%BC%82%E5%B8%B8%E7%B1%BB%E5%B1%82%E6%AC%A1%E7%BB%93%E6%9E%84%E5%9B%BE.png)

图片来自:https://simplesnippets.tech/exception-handling-in-java-part-1/

-![](images/Java异常类层次结构图2.png) - +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-12/Java%E5%BC%82%E5%B8%B8%E7%B1%BB%E5%B1%82%E6%AC%A1%E7%BB%93%E6%9E%84%E5%9B%BE2.png)

图片来自:https://chercher.tech/java-programming/exceptions-java

-在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 **Throwable 类**。Throwable: 有两个重要的子类:**Exception(异常)** 和 **Error(错误)** ,二者都是 Java 异常处理的重要子类,各自都包含大量子类。 +在 Java 中,所有的异常都有一个共同的祖先 `java.lang` 包中的 `Throwable` 类。`Throwable` 类有两个重要的子类 `Exception`(异常)和 `Error`(错误)。`Exception` 能被程序本身处理(`try-catch`), `Error` 是无法处理的(只能尽量避免)。 + +`Exception` 和 `Error` 二者都是 Java 异常处理的重要子类,各自都包含大量子类。 + +- **`Exception`** :程序本身可以处理的异常,可以通过 `catch` 来进行捕获。`Exception` 又可以分为 受检查异常(必须处理) 和 不受检查异常(可以不处理)。 +- **`Error`** :`Error` 属于程序无法处理的错误 ,我们没办法通过 `catch` 来进行捕获 。例如,Java 虚拟机运行错误(`Virtual MachineError`)、虚拟机内存不够错误(`OutOfMemoryError`)、类定义错误(`NoClassDefFoundError`)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。 + +**受检查异常** -**Error(错误):是程序无法处理的错误**,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java 虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。 +Java 代码在编译过程中,如果受检查异常没有被 `catch`/`throw` 处理的话,就没办法通过编译 。比如下面这段 IO 操作的代码。 -这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如 Java 虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java 中,错误通过 Error 的子类描述。 +![check-exception](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-12/check-exception.png) -**Exception(异常):是程序本身可以处理的异常**。Exception 类有一个重要的子类 **RuntimeException**。RuntimeException 异常由 Java 虚拟机抛出。**NullPointerException**(要访问的变量没有引用任何对象时,抛出该异常)、**ArithmeticException**(算术运算异常,一个整数除以 0 时,抛出该异常)和 **ArrayIndexOutOfBoundsException** (下标越界异常)。 +除了`RuntimeException`及其子类以外,其他的`Exception`类及其子类都属于检查异常 。常见的受检查异常有: IO 相关的异常、`ClassNotFoundException` 、`SQLException`...。 -**注意:异常和错误的区别:异常能被程序本身处理,错误是无法处理。** +**不受检查异常** + +Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。 + +`RuntimeException` 及其子类都统称为非受检查异常,例如:`NullPointExecrption`、`NumberFormatException`(字符串转换为数字)、`ArrayIndexOutOfBoundsException`(数组越界)、`ClassCastException`(类型转换错误)、`ArithmeticException`(算术错误)等。 #### 3.2.2. Throwable 类常用方法 @@ -1216,14 +1208,13 @@ String s = input.readLine(); #### 3.2.3. try-catch-finally -- **try 块:** 用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。 -- **catch 块:** 用于处理 try 捕获到的异常。 -- **finally 块:** 无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。 +- **`try`块:** 用于捕获异常。其后可接零个或多个 `catch` 块,如果没有 `catch` 块,则必须跟一个 `finally` 块。 +- **`catch`块:** 用于处理 try 捕获到的异常。 +- **`finally` 块:** 无论是否捕获或处理异常,`finally` 块里的语句都会被执行。当在 `try` 块或 `catch` 块中遇到 `return` 语句时,`finally` 语句块将在方法返回之前被执行。 -**在以下 4 种特殊情况下,finally 块不会被执行:** +**在以下 3 种特殊情况下,`finally` 块不会被执行:** -1. 在 finally 语句块第一行发生了异常。 因为在其他行,finally 块还是会得到执行 -2. 在前面的代码中用了 System.exit(int)已退出程序。 exit 是带参函数 ;若该语句在异常语句之后,finally 会执行 +2. 在 `try` 或 `finally `块中用了 `System.exit(int)`退出程序。但是,如果 `System.exit(int)` 在异常语句之后,`finally` 还是会被执行 3. 程序所在的线程死亡。 4. 关闭 CPU。 @@ -1250,7 +1241,7 @@ public class Test { #### 3.2.4. 使用 `try-with-resources` 来代替`try-catch-finally` 1. **适用范围(资源的定义):** 任何实现 `java.lang.AutoCloseable`或者``java.io.Closeable` 的对象 -2. **关闭资源和final的执行顺序:** 在 `try-with-resources` 语句中,任何 catch 或 finally 块在声明的资源关闭后运行 +2. **关闭资源和 final 的执行顺序:** 在 `try-with-resources` 语句中,任何 catch 或 finally 块在声明的资源关闭后运行 《Effecitve Java》中明确指出: @@ -1275,7 +1266,7 @@ Java 中类似于`InputStream`、`OutputStream` 、`Scanner` 、`PrintWriter`等 } ``` -使用Java 7之后的 `try-with-resources` 语句改造上面的代码: +使用 Java 7 之后的 `try-with-resources` 语句改造上面的代码: ```java try (Scanner scanner = new Scanner(new File("test.txt"))) { @@ -1287,7 +1278,7 @@ try (Scanner scanner = new Scanner(new File("test.txt"))) { } ``` -当然多个资源需要关闭的时候,使用 `try-with-resources` 实现起来也非常简单,如果你还是用`try-catch-finally`可能会带来很多问题。 +当然多个资源需要关闭的时候,使用 `try-with-resources` 实现起来也非常简单,如果你还是用`try-catch-finally`可能会带来很多问题。 通过使用分号分隔,可以在`try-with-resources`块中声明多个资源。 @@ -1333,7 +1324,7 @@ Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种 ![RUNNABLE-VS-RUNNING](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3/RUNNABLE-VS-RUNNING.png) -当线程执行 `wait()`方法之后,线程进入 **WAITING(等待)**状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 **TIME_WAITING(超时等待)** 状态相当于在等待状态的基础上增加了超时限制,比如通过 `sleep(long millis)`方法或 `wait(long millis)`方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 **BLOCKED(阻塞)** 状态。线程在执行 Runnable 的`run()`方法之后将会进入到 **TERMINATED(终止)** 状态。 +当线程执行 `wait()`方法之后,线程进入 **WAITING(等待)** 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 **TIME_WAITING(超时等待)** 状态相当于在等待状态的基础上增加了超时限制,比如通过 `sleep(long millis)`方法或 `wait(long millis)`方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 **BLOCKED(阻塞)** 状态。线程在执行 Runnable 的`run()`方法之后将会进入到 **TERMINATED(终止)** 状态。 ### 3.4. 文件与 I\O 流 @@ -1374,12 +1365,3 @@ Java Io 流共涉及 40 多个类,这些类看上去很杂乱,但实际上 - https://www.educba.com/oracle-vs-openjdk/ - https://stackoverflow.com/questions/22358071/differences-between-oracle-jdk-and-openjdk?answertab=active#tab-top -## 5. 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java 面试突击》:** 由本文档衍生的专为面试而生的《Java 面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java 面试突击"** 即可免费领取! - -**Java 工程师必备学习资源:** 一些 Java 工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) diff --git "a/docs/java/Java\347\226\221\351\232\276\347\202\271.md" "b/docs/java/basis/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\226\221\351\232\276\347\202\271.md" similarity index 83% rename from "docs/java/Java\347\226\221\351\232\276\347\202\271.md" rename to "docs/java/basis/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\226\221\351\232\276\347\202\271.md" index 4232e66edee..fc669f19b1d 100644 --- "a/docs/java/Java\347\226\221\351\232\276\347\202\271.md" +++ "b/docs/java/basis/Java\345\237\272\347\241\200\347\237\245\350\257\206\347\226\221\351\232\276\347\202\271.md" @@ -1,23 +1,23 @@ -- [1. 基础](#_1-基础) - - [1.1. 正确使用 equals 方法](#_11-正确使用-equals-方法) - - [1.2. 整型包装类值的比较](#_12-整型包装类值的比较) - - [1.3. BigDecimal](#_13-bigdecimal) - - [1.3.1. BigDecimal 的用处](#_131-bigdecimal-的用处) - - [1.3.2. BigDecimal 的大小比较](#_132-bigdecimal-的大小比较) - - [1.3.3. BigDecimal 保留几位小数](#_133-bigdecimal-保留几位小数) - - [1.3.4. BigDecimal 的使用注意事项](#_134-bigdecimal-的使用注意事项) - - [1.3.5. 总结](#_135-总结) - - [1.4. 基本数据类型与包装数据类型的使用标准](#_14-基本数据类型与包装数据类型的使用标准) +- [1. 基础](#1-基础) + - [1.1. 正确使用 equals 方法](#11-正确使用-equals-方法) + - [1.2. 整型包装类值的比较](#12-整型包装类值的比较) + - [1.3. BigDecimal](#13-bigdecimal) + - [1.3.1. BigDecimal 的用处](#131-bigdecimal-的用处) + - [1.3.2. BigDecimal 的大小比较](#132-bigdecimal-的大小比较) + - [1.3.3. BigDecimal 保留几位小数](#133-bigdecimal-保留几位小数) + - [1.3.4. BigDecimal 的使用注意事项](#134-bigdecimal-的使用注意事项) + - [1.3.5. 总结](#135-总结) + - [1.4. 基本数据类型与包装数据类型的使用标准](#14-基本数据类型与包装数据类型的使用标准) - [2. 集合](#_2-集合) - - [2.1. Arrays.asList()使用指南](#_21-arraysaslist使用指南) - - [2.1.1. 简介](#_211-简介) - - [2.1.2. 《阿里巴巴Java 开发手册》对其的描述](#_212-阿里巴巴java-开发手册对其的描述) - - [2.1.3. 使用时的注意事项总结](#_213-使用时的注意事项总结) - - [2.1.4. 如何正确的将数组转换为ArrayList?](#_214-如何正确的将数组转换为arraylist) - - [2.2. Collection.toArray()方法使用的坑&如何反转数组](#_22-collectiontoarray方法使用的坑如何反转数组) - - [2.3. 不要在 foreach 循环里进行元素的 remove/add 操作](#_23-不要在-foreach-循环里进行元素的-removeadd-操作) + - [2.1. Arrays.asList()使用指南](#21-arraysaslist使用指南) + - [2.1.1. 简介](#211-简介) + - [2.1.2. 《阿里巴巴Java 开发手册》对其的描述](#212-阿里巴巴java-开发手册对其的描述) + - [2.1.3. 使用时的注意事项总结](#213-使用时的注意事项总结) + - [2.1.4. 如何正确的将数组转换为ArrayList?](#214-如何正确的将数组转换为arraylist) + - [2.2. Collection.toArray()方法使用的坑&如何反转数组](#22-collectiontoarray方法使用的坑如何反转数组) + - [2.3. 不要在 foreach 循环里进行元素的 remove/add 操作](#23-不要在-foreach-循环里进行元素的-removeadd-操作) @@ -52,9 +52,9 @@ Objects.equals(null,"SnailClimb");// false 我们看一下`java.util.Objects#equals`的源码就知道原因了。 ```java public static boolean equals(Object a, Object b) { - // 可以避免空指针异常。如果a==null的话此时a.equals(b)就不会得到执行,避免出现空指针异常。 - return (a == b) || (a != null && a.equals(b)); - } + // 可以避免空指针异常。如果a==null的话此时a.equals(b)就不会得到执行,避免出现空指针异常。 + return (a == b) || (a != null && a.equals(b)); +} ``` **注意:** @@ -83,7 +83,7 @@ System.out.println(a.equals(b));//true 当使用自动装箱方式创建一个Integer对象时,当数值在-128 ~127时,会将创建的 Integer 对象缓存起来,当下次再出现该数值时,直接从缓存中取出对应的Integer对象。所以上述代码中,x和y引用的是相同的Integer对象。 -**注意:**如果你的IDE(IDEA/Eclipse)上安装了阿里巴巴的p3c插件,这个插件如果检测到你用 ==的话会报错提示,推荐安装一个这个插件,很不错。 +**注意:** 如果你的IDE(IDEA/Eclipse)上安装了阿里巴巴的p3c插件,这个插件如果检测到你用 ==的话会报错提示,推荐安装一个这个插件,很不错。 ## 1.3. BigDecimal @@ -104,14 +104,18 @@ System.out.println(a == b);// false BigDecimal a = new BigDecimal("1.0"); BigDecimal b = new BigDecimal("0.9"); BigDecimal c = new BigDecimal("0.8"); -BigDecimal x = a.subtract(b);// 0.1 -BigDecimal y = b.subtract(c);// 0.1 -System.out.println(x.equals(y));// true + +BigDecimal x = a.subtract(b); +BigDecimal y = b.subtract(c); + +System.out.println(x); /* 0.1 */ +System.out.println(y); /* 0.1 */ +System.out.println(Objects.equals(x, y)); /* true */ ``` ### 1.3.2. BigDecimal 的大小比较 -`a.compareTo(b)` : 返回 -1 表示小于,0 表示 等于, 1表示 大于。 +`a.compareTo(b)` : 返回 -1 表示 `a` 小于 `b`,0 表示 `a` 等于 `b` , 1表示 `a` 大于 `b`。 ```java BigDecimal a = new BigDecimal("1.0"); @@ -167,7 +171,7 @@ Reference:《阿里巴巴Java开发手册》 `Arrays.asList()`在平时开发中还是比较常见的,我们可以使用它将一个数组转换为一个List集合。 ```java -String[] myArray = { "Apple", "Banana", "Orange" }; +String[] myArray = {"Apple", "Banana", "Orange"}; List myList = Arrays.asList(myArray); //上面两个语句等价于下面一条语句 List myList = Arrays.asList("Apple","Banana", "Orange"); @@ -177,8 +181,9 @@ JDK 源码对于这个方法的说明: ```java /** - *返回由指定数组支持的固定大小的列表。此方法作为基于数组和基于集合的API之间的桥梁,与 Collection.toArray()结合使用。返回的List是可序列化并实现RandomAccess接口。 - */ + *返回由指定数组支持的固定大小的列表。此方法作为基于数组和基于集合的API之间的桥梁, + * 与 Collection.toArray()结合使用。返回的List是可序列化并实现RandomAccess接口。 + */ public static List asList(T... a) { return new ArrayList<>(a); } @@ -197,12 +202,12 @@ public static List asList(T... a) { `Arrays.asList()`是泛型方法,传入的对象必须是对象数组。 ```java -int[] myArray = { 1, 2, 3 }; +int[] myArray = {1, 2, 3}; List myList = Arrays.asList(myArray); System.out.println(myList.size());//1 System.out.println(myList.get(0));//数组地址值 System.out.println(myList.get(1));//报错:ArrayIndexOutOfBoundsException -int [] array=(int[]) myList.get(0); +int[] array = (int[]) myList.get(0); System.out.println(array[0]);//1 ``` 当传入一个原生数据类型数组时,`Arrays.asList()` 的真正得到的参数就不是数组中的元素,而是数组对象本身!此时List 的唯一元素就是这个数组,这也就解释了上面的代码。 @@ -210,7 +215,7 @@ System.out.println(array[0]);//1 我们使用包装类型数组就可以解决这个问题。 ```java -Integer[] myArray = { 1, 2, 3 }; +Integer[] myArray = {1, 2, 3}; ``` **使用集合的修改方法:`add()`、`remove()`、`clear()`会抛出异常。** @@ -296,7 +301,7 @@ static List arrayToList(final T[] array) { for (final T s : array) { l.add(s); } - return (l); + return l; } ``` @@ -344,6 +349,14 @@ List list = new ArrayList(); CollectionUtils.addAll(list, str); ``` +**6. 使用 Java9 的 `List.of()`方法** +``` java +Integer[] array = {1, 2, 3}; +List list = List.of(array); +System.out.println(list); /* [1, 2, 3] */ +/* 不支持基本数据类型 */ +``` + ## 2.2. Collection.toArray()方法使用的坑&如何反转数组 该方法是一个泛型方法:` T[] toArray(T[] a);` 如果`toArray`方法中没有传递任何参数的话返回的是`Object`类型数组。 @@ -365,6 +378,16 @@ s=list.toArray(new String[0]);//没有指定类型的话会报错 > **fail-fast 机制** :多个线程对 fail-fast 集合进行修改的时,可能会抛出ConcurrentModificationException,单线程下也会出现这种情况,上面已经提到过。 +Java8开始,可以使用`Collection#removeIf()`方法删除满足特定条件的元素,如 +``` java +List list = new ArrayList<>(); +for (int i = 1; i <= 10; ++i) { + list.add(i); +} +list.removeIf(filter -> filter % 2 == 0); /* 删除list中的所有偶数 */ +System.out.println(list); /* [1, 3, 5, 7, 9] */ +``` + `java.util`包下面的所有的集合类都是fail-fast的,而`java.util.concurrent`包下面的所有的类都是fail-safe的。 ![不要在 foreach 循环里进行元素的 remove/add 操作](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019/7/foreach-remove:add.png) diff --git a/docs/java/basic/final,static,this,super.md "b/docs/java/basis/Java\345\270\270\350\247\201\345\205\263\351\224\256\345\255\227\346\200\273\347\273\223.md" similarity index 93% rename from docs/java/basic/final,static,this,super.md rename to "docs/java/basis/Java\345\270\270\350\247\201\345\205\263\351\224\256\345\255\227\346\200\273\347\273\223.md" index d6dc8ab8eb7..d9eaf1a846b 100644 --- a/docs/java/basic/final,static,this,super.md +++ "b/docs/java/basis/Java\345\270\270\350\247\201\345\205\263\351\224\256\345\255\227\346\200\273\347\273\223.md" @@ -92,7 +92,7 @@ public class Sub extends Super { **使用 this 和 super 要注意的问题:** -- 在构造器中使用 `super()` 调用父类中的其他构造方法时,该语句必须处于构造器的首行,否则编译器会报错。另外,this 调用本类中的其他构造方法时,也要放在首行。 +- 在构造器中使用 `super()` 调用父类中的其他构造方法时,该语句必须处于构造器的首行,否则编译器会报错。另外,this 调用本类中的其他构造方法时,也要放在首行。 - this、super不能用在static方法中。 **简单解释一下:** @@ -141,7 +141,7 @@ public class StaticBean { this.name = name; } //静态方法 - static void SayHello() { + static void sayHello() { System.out.println("Hello i am java"); } @Override @@ -164,7 +164,7 @@ public class StaticDemo { StaticBean.age = 33; System.out.println(staticBean + " " + staticBean2 + " " + staticBean3 + " " + staticBean4); //StaticBean{name=1,age=33} StaticBean{name=2,age=33} StaticBean{name=3,age=33} StaticBean{name=4,age=33} - StaticBean.SayHello();//Hello i am java + StaticBean.sayHello();//Hello i am java } } @@ -173,7 +173,7 @@ public class StaticDemo { ### 静态代码块 -静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—非静态代码块—构造方法)。 该类不管创建多少对象,静态代码块只执行一次. +静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块 —> 非静态代码块 —> 构造方法)。 该类不管创建多少对象,静态代码块只执行一次. 静态代码块的格式是 @@ -274,7 +274,11 @@ class Foo { } ``` -你可以像这样调用静态方法:`Foo.method1()`。 如果您尝试使用这种方法调用 method2 将失败。 但这样可行:`Foo bar = new Foo(1);bar.method2();` +你可以像这样调用静态方法:`Foo.method1()`。 如果您尝试使用这种方法调用 method2 将失败。 但这样可行 +``` java +Foo bar = new Foo(1); +bar.method2(); +``` 总结: @@ -285,7 +289,7 @@ class Foo { 相同点: 都是在JVM加载类时且在构造方法执行之前执行,在类中都可以定义多个,定义多个时按定义的顺序执行,一般在代码块中对一些static变量进行赋值。 -不同点: 静态代码块在非静态代码块之前执行(静态代码块—非静态代码块—构造方法)。静态代码块只在第一次new执行一次,之后不再执行,而非静态代码块在每new一次就执行一次。 非静态代码块可在普通方法中定义(不过作用不大);而静态代码块不行。 +不同点: 静态代码块在非静态代码块之前执行(静态代码块 -> 非静态代码块 -> 构造方法)。静态代码块只在第一次new执行一次,之后不再执行,而非静态代码块在每new一次就执行一次。 非静态代码块可在普通方法中定义(不过作用不大);而静态代码块不行。 > 修正 [issue #677](https://github.com/Snailclimb/JavaGuide/issues/677):静态代码块可能在第一次new的时候执行,但不一定只在第一次new的时候执行。比如通过 `Class.forName("ClassDemo")`创建 Class 对象的时候也会执行。 @@ -347,6 +351,6 @@ public class Test { ### 参考 -- httpsblog.csdn.netchen13579867831articledetails78995480 -- httpwww.cnblogs.comchenssyp3388487.html -- httpwww.cnblogs.comQian123p5713440.html +- https://blog.csdn.net/chen13579867831/article/details/78995480 +- https://www.cnblogs.com/chenssy/p/3388487.html +- https://www.cnblogs.com/Qian123/p/5713440.html diff --git a/docs/java/basic/java-proxy.md "b/docs/java/basis/\344\273\243\347\220\206\346\250\241\345\274\217\350\257\246\350\247\243.md" similarity index 97% rename from docs/java/basic/java-proxy.md rename to "docs/java/basis/\344\273\243\347\220\206\346\250\241\345\274\217\350\257\246\350\247\243.md" index b563571026b..45c7c2c5533 100644 --- a/docs/java/basic/java-proxy.md +++ "b/docs/java/basis/\344\273\243\347\220\206\346\250\241\345\274\217\350\257\246\350\247\243.md" @@ -1,7 +1,5 @@ > 本文首更于[《从零开始手把手教你实现一个简单的RPC框架》](https://t.zsxq.com/iIUv7Mn) 。 - - @@ -48,7 +46,7 @@ 1. 定义一个接口及其实现类; 2. 创建一个代理类同样实现这个接口 -3. 将目标对象注注入进代理类,然后在代理类的对应方法调用目标类中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。 +3. 将目标对象注入进代理类,然后在代理类的对应方法调用目标类中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。 下面通过代码展示! @@ -154,7 +152,7 @@ after method send() 1. **loader** :类加载器,用于加载代理对象。 2. **interfaces** : 被代理类实现的一些接口; -3. **h** : 实现了 `InvocationHandle`r 接口的对象; +3. **h** : 实现了 `InvocationHandler` 接口的对象; 要实现动态代理的话,还必须需要实现`InvocationHandler` 来自定义处理逻辑。 当我们的动态代理对象调用一个方法时候,这个方法的调用就会被转发到实现`InvocationHandler` 接口类的 `invoke` 方法来调用。 @@ -261,8 +259,7 @@ public class JdkProxyFactory { **5.实际使用** ```java -DebugProxy debugProxy = new DebugProxy(new SmsServiceImpl()); -SmsService smsService = debugProxy.getProxy(SmsService.class); +SmsService smsService = (SmsService) JdkProxyFactory.getProxy(new SmsServiceImpl()); smsService.send("java"); ``` @@ -420,4 +417,4 @@ after method send 这篇文章中主要介绍了代理模式的两种实现:静态代理以及动态代理。涵盖了静态代理和动态代理实战、静态代理和动态代理的区别、JDK 动态代理和 Cglib 动态代理区别等内容。 -文中涉及到的所有源码,你可以在这里找到:[https://github.com/Snailclimb/guide-rpc-framework-learning/tree/master/src/main/java/github/javaguide/proxy](https://github.com/Snailclimb/guide-rpc-framework-learning/tree/master/src/main/java/github/javaguide/proxy) 。 \ No newline at end of file +文中涉及到的所有源码,你可以在这里找到:[https://github.com/Snailclimb/guide-rpc-framework-learning/tree/master/src/main/java/github/javaguide/proxy](https://github.com/Snailclimb/guide-rpc-framework-learning/tree/master/src/main/java/github/javaguide/proxy) 。 diff --git a/docs/java/basic/reflection.md "b/docs/java/basis/\345\217\215\345\260\204\346\234\272\345\210\266.md" similarity index 84% rename from docs/java/basic/reflection.md rename to "docs/java/basis/\345\217\215\345\260\204\346\234\272\345\210\266.md" index ae1cc384bb4..2f024c9c1c7 100644 --- a/docs/java/basic/reflection.md +++ "b/docs/java/basis/\345\217\215\345\260\204\346\234\272\345\210\266.md" @@ -2,9 +2,9 @@ JAVA 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 java 语言的反射机制。 -### 获取 Class 对象的两种方式 +### 获取 Class 对象的四种方式 -如果我们动态获取到这些信息,我们需要依靠 Class 对象。Class 类对象将一个类的方法、变量等信息告诉运行的程序。Java 提供了三种方式获取 Class 对象: +如果我们动态获取到这些信息,我们需要依靠 Class 对象。Class 类对象将一个类的方法、变量等信息告诉运行的程序。Java 提供了四种方式获取 Class 对象: 1.知道具体类的情况下可以使用: @@ -12,18 +12,29 @@ JAVA 反射机制是在运行状态中,对于任意一个类,都能够知道 Class alunbarClass = TargetObject.class; ``` -但是我们一般是不知道具体类的,基本都是通过遍历包下面的类来获取 Class 对象 +但是我们一般是不知道具体类的,基本都是通过遍历包下面的类来获取 Class 对象,通过此方式获取Class对象不会进行初始化 2.通过 `Class.forName()`传入类的路径获取: ```java Class alunbarClass1 = Class.forName("cn.javaguide.TargetObject"); ``` +Class.forName(className)方法,内部实际调用的是一个native方法 forName0(className, true, ClassLoader.getClassLoader(caller), caller); + +第2个boolean参数表示类是否需要初始化,Class.forName(className)默认是需要初始化。 + +一旦初始化,就会触发目标对象的 static块代码执行,static参数也会被再次初始化。 + 3.通过对象实例`instance.getClass()`获取: -``` -Employee e; +```java +Employee e = new Employee(); Class alunbarClass2 = e.getClass(); ``` +4.通过类加载器`xxxClassLoader.loadClass()`传入类路径获取 +```java +class clazz = ClassLoader.LoadClass("cn.javaguide.TargetObject"); +``` +通过类加载器获取Class对象不会进行初始化,意味着不进行包括初始化等一些列步骤,静态块和静态对象不会得到执行 ### 代码实例 diff --git "a/docs/java/basic/\347\224\250\345\245\275Java\344\270\255\347\232\204\346\236\232\344\270\276\347\234\237\347\232\204\346\262\241\346\234\211\351\202\243\344\271\210\347\256\200\345\215\225.md" "b/docs/java/basis/\347\224\250\345\245\275Java\344\270\255\347\232\204\346\236\232\344\270\276\347\234\237\347\232\204\346\262\241\346\234\211\351\202\243\344\271\210\347\256\200\345\215\225.md" similarity index 92% rename from "docs/java/basic/\347\224\250\345\245\275Java\344\270\255\347\232\204\346\236\232\344\270\276\347\234\237\347\232\204\346\262\241\346\234\211\351\202\243\344\271\210\347\256\200\345\215\225.md" rename to "docs/java/basis/\347\224\250\345\245\275Java\344\270\255\347\232\204\346\236\232\344\270\276\347\234\237\347\232\204\346\262\241\346\234\211\351\202\243\344\271\210\347\256\200\345\215\225.md" index 20b02297758..e88f0b5c5d7 100644 --- "a/docs/java/basic/\347\224\250\345\245\275Java\344\270\255\347\232\204\346\236\232\344\270\276\347\234\237\347\232\204\346\262\241\346\234\211\351\202\243\344\271\210\347\256\200\345\215\225.md" +++ "b/docs/java/basis/\347\224\250\345\245\275Java\344\270\255\347\232\204\346\236\232\344\270\276\347\234\237\347\232\204\346\262\241\346\234\211\351\202\243\344\271\210\347\256\200\345\215\225.md" @@ -51,10 +51,7 @@ public class Pizza { } public boolean isDeliverable() { - if (getStatus() == PizzaStatus.READY) { - return true; - } - return false; + return getStatus() == PizzaStatus.READY; } // Methods that set and get the status variable. @@ -63,16 +60,16 @@ public class Pizza { ## 3.使用 == 比较枚举类型 -由于枚举类型确保JVM中仅存在一个常量实例,因此我们可以安全地使用“ ==”运算符比较两个变量,如上例所示;此外,“ ==”运算符可提供编译时和运行时的安全性。 +由于枚举类型确保JVM中仅存在一个常量实例,因此我们可以安全地使用 `==` 运算符比较两个变量,如上例所示;此外,`==` 运算符可提供编译时和运行时的安全性。 -首先,让我们看一下以下代码段中的运行时安全性,其中“ ==”运算符用于比较状态,并且如果两个值均为null 都不会引发 NullPointerException。相反,如果使用equals方法,将抛出 NullPointerException: +首先,让我们看一下以下代码段中的运行时安全性,其中 `==` 运算符用于比较状态,并且如果两个值均为null 都不会引发 NullPointerException。相反,如果使用equals方法,将抛出 NullPointerException: ```java if(testPz.getStatus().equals(Pizza.PizzaStatus.DELIVERED)); if(testPz.getStatus() == Pizza.PizzaStatus.DELIVERED); ``` -对于编译时安全性,我们看另一个示例,两个不同枚举类型进行比较,使用equal方法比较结果确定为true,因为getStatus方法的枚举值与另一个类型枚举值一致,但逻辑上应该为false。这个问题可以使用==操作符避免。因为编译器会表示类型不兼容错误: +对于编译时安全性,我们看另一个示例,两个不同枚举类型进行比较,使用equal方法比较结果确定为true,因为`getStatus`方法的枚举值与另一个类型枚举值一致,但逻辑上应该为false。这个问题可以使用==操作符避免。因为编译器会表示类型不兼容错误: ```java if(testPz.getStatus().equals(TestColor.GREEN)); @@ -84,9 +81,12 @@ if(testPz.getStatus() == TestColor.GREEN); ```java public int getDeliveryTimeInDays() { switch (status) { - case ORDERED: return 5; - case READY: return 2; - case DELIVERED: return 0; + case ORDERED: + return 5; + case READY: + return 2; + case DELIVERED: + return 0; } return 0; } @@ -257,22 +257,17 @@ EnumMap map; 让我们快速看一个真实的示例,该示例演示如何在实践中使用它: ```java -public static EnumMap> - groupPizzaByStatus(List pizzaList) { - EnumMap> pzByStatus = - new EnumMap>(PizzaStatus.class); - - for (Pizza pz : pizzaList) { - PizzaStatus status = pz.getStatus(); - if (pzByStatus.containsKey(status)) { - pzByStatus.get(status).add(pz); - } else { - List newPzList = new ArrayList(); - newPzList.add(pz); - pzByStatus.put(status, newPzList); - } +Iterator iterator = pizzaList.iterator(); +while (iterator.hasNext()) { + Pizza pz = iterator.next(); + PizzaStatus status = pz.getStatus(); + if (pzByStatus.containsKey(status)) { + pzByStatus.get(status).add(pz); + } else { + List newPzList = new ArrayList<>(); + newPzList.add(pz); + pzByStatus.put(status, newPzList); } - return pzByStatus; } ``` diff --git a/docs/java/collection/ArrayList-Grow.md b/docs/java/collection/ArrayList-Grow.md deleted file mode 100644 index 2449190f07b..00000000000 --- a/docs/java/collection/ArrayList-Grow.md +++ /dev/null @@ -1,361 +0,0 @@ - -## 一 先从 ArrayList 的构造函数说起 - -**ArrayList有三种方式来初始化,构造方法源码如下:** - -```java - /** - * 默认初始容量大小 - */ - private static final int DEFAULT_CAPACITY = 10; - - - private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; - - /** - *默认构造函数,使用初始容量10构造一个空列表(无参数构造) - */ - public ArrayList() { - this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; - } - - /** - * 带初始容量参数的构造函数。(用户自己指定容量) - */ - public ArrayList(int initialCapacity) { - if (initialCapacity > 0) {//初始容量大于0 - //创建initialCapacity大小的数组 - this.elementData = new Object[initialCapacity]; - } else if (initialCapacity == 0) {//初始容量等于0 - //创建空数组 - this.elementData = EMPTY_ELEMENTDATA; - } else {//初始容量小于0,抛出异常 - throw new IllegalArgumentException("Illegal Capacity: "+ - initialCapacity); - } - } - - - /** - *构造包含指定collection元素的列表,这些元素利用该集合的迭代器按顺序返回 - *如果指定的集合为null,throws NullPointerException。 - */ - public ArrayList(Collection c) { - elementData = c.toArray(); - if ((size = elementData.length) != 0) { - // c.toArray might (incorrectly) not return Object[] (see 6260652) - if (elementData.getClass() != Object[].class) - elementData = Arrays.copyOf(elementData, size, Object[].class); - } else { - // replace with empty array. - this.elementData = EMPTY_ELEMENTDATA; - } - } - -``` - -细心的同学一定会发现 :**以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为10。** 下面在我们分析 ArrayList 扩容时会讲到这一点内容! - -## 二 一步一步分析 ArrayList 扩容机制 - -这里以无参构造函数创建的 ArrayList 为例分析 - -### 1. 先来看 `add` 方法 - -```java - /** - * 将指定的元素追加到此列表的末尾。 - */ - public boolean add(E e) { - //添加元素之前,先调用ensureCapacityInternal方法 - ensureCapacityInternal(size + 1); // Increments modCount!! - //这里看到ArrayList添加元素的实质就相当于为数组赋值 - elementData[size++] = e; - return true; - } -``` - -> **注意** :JDK11 移除了 `ensureCapacityInternal()` 和 `ensureExplicitCapacity()` 方法 - -### 2. 再来看看 `ensureCapacityInternal()` 方法 - -可以看到 `add` 方法 首先调用了`ensureCapacityInternal(size + 1)` - -```java - //得到最小扩容量 - private void ensureCapacityInternal(int minCapacity) { - if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { - // 获取默认的容量和传入参数的较大值 - minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); - } - - ensureExplicitCapacity(minCapacity); - } -``` -**当 要 add 进第1个元素时,minCapacity为1,在Math.max()方法比较后,minCapacity 为10。** - -### 3. `ensureExplicitCapacity()` 方法 - -如果调用 `ensureCapacityInternal()` 方法就一定会进过(执行)这个方法,下面我们来研究一下这个方法的源码! - -```java - //判断是否需要扩容 - private void ensureExplicitCapacity(int minCapacity) { - modCount++; - - // overflow-conscious code - if (minCapacity - elementData.length > 0) - //调用grow方法进行扩容,调用此方法代表已经开始扩容了 - grow(minCapacity); - } - -``` - -我们来仔细分析一下: - -- 当我们要 add 进第1个元素到 ArrayList 时,elementData.length 为0 (因为还是一个空的 list),因为执行了 `ensureCapacityInternal()` 方法 ,所以 minCapacity 此时为10。此时,`minCapacity - elementData.length > 0 `成立,所以会进入 `grow(minCapacity)` 方法。 -- 当add第2个元素时,minCapacity 为2,此时e lementData.length(容量)在添加第一个元素后扩容成 10 了。此时,`minCapacity - elementData.length > 0 ` 不成立,所以不会进入 (执行)`grow(minCapacity)` 方法。 -- 添加第3、4···到第10个元素时,依然不会执行grow方法,数组容量都为10。 - -直到添加第11个元素,minCapacity(为11)比elementData.length(为10)要大。进入grow方法进行扩容。 - -### 4. `grow()` 方法 - -```java - /** - * 要分配的最大数组大小 - */ - private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; - - /** - * ArrayList扩容的核心方法。 - */ - private void grow(int minCapacity) { - // oldCapacity为旧容量,newCapacity为新容量 - int oldCapacity = elementData.length; - //将oldCapacity 右移一位,其效果相当于oldCapacity /2, - //我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍, - int newCapacity = oldCapacity + (oldCapacity >> 1); - //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量, - if (newCapacity - minCapacity < 0) - newCapacity = minCapacity; - // 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE, - //如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。 - if (newCapacity - MAX_ARRAY_SIZE > 0) - newCapacity = hugeCapacity(minCapacity); - // minCapacity is usually close to size, so this is a win: - elementData = Arrays.copyOf(elementData, newCapacity); - } -``` - -**int newCapacity = oldCapacity + (oldCapacity >> 1),所以 ArrayList 每次扩容之后容量都会变为原来的 1.5 倍左右(oldCapacity为偶数就是1.5倍,否则是1.5倍左右)!** 奇偶不同,比如 :10+10/2 = 15, 33+33/2=49。如果是奇数的话会丢掉小数. - -> ">>"(移位运算符):>>1 右移一位相当于除2,右移n位相当于除以 2 的 n 次方。这里 oldCapacity 明显右移了1位所以相当于oldCapacity /2。对于大数据的2进制运算,位移运算符比那些普通运算符的运算要快很多,因为程序仅仅移动一下而已,不去计算,这样提高了效率,节省了资源   - -**我们再来通过例子探究一下`grow()` 方法 :** - -- 当add第1个元素时,oldCapacity 为0,经比较后第一个if判断成立,newCapacity = minCapacity(为10)。但是第二个if判断不会成立,即newCapacity 不比 MAX_ARRAY_SIZE大,则不会进入 `hugeCapacity` 方法。数组容量为10,add方法中 return true,size增为1。 -- 当add第11个元素进入grow方法时,newCapacity为15,比minCapacity(为11)大,第一个if判断不成立。新容量没有大于数组最大size,不会进入hugeCapacity方法。数组容量扩为15,add方法中return true,size增为11。 -- 以此类推······ - -**这里补充一点比较重要,但是容易被忽视掉的知识点:** - -- java 中的 `length `属性是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性. -- java 中的 `length()` 方法是针对字符串说的,如果想看这个字符串的长度则用到 `length()` 这个方法. -- java 中的 `size()` 方法是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看! - -### 5. `hugeCapacity()` 方法。 - -从上面 `grow()` 方法源码我们知道: 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。 - - -```java - private static int hugeCapacity(int minCapacity) { - if (minCapacity < 0) // overflow - throw new OutOfMemoryError(); - //对minCapacity和MAX_ARRAY_SIZE进行比较 - //若minCapacity大,将Integer.MAX_VALUE作为新数组的大小 - //若MAX_ARRAY_SIZE大,将MAX_ARRAY_SIZE作为新数组的大小 - //MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; - return (minCapacity > MAX_ARRAY_SIZE) ? - Integer.MAX_VALUE : - MAX_ARRAY_SIZE; - } -``` - - - -## 三 `System.arraycopy()` 和 `Arrays.copyOf()`方法 - - -阅读源码的话,我们就会发现 ArrayList 中大量调用了这两个方法。比如:我们上面讲的扩容操作以及`add(int index, E element)`、`toArray()` 等方法中都用到了该方法! - - -### 3.1 `System.arraycopy()` 方法 - -```java - /** - * 在此列表中的指定位置插入指定的元素。 - *先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大; - *再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。 - */ - public void add(int index, E element) { - rangeCheckForAdd(index); - - ensureCapacityInternal(size + 1); // Increments modCount!! - //arraycopy()方法实现数组自己复制自己 - //elementData:源数组;index:源数组中的起始位置;elementData:目标数组;index + 1:目标数组中的起始位置; size - index:要复制的数组元素的数量; - System.arraycopy(elementData, index, elementData, index + 1, size - index); - elementData[index] = element; - size++; - } -``` - -我们写一个简单的方法测试以下: - -```java -public class ArraycopyTest { - - public static void main(String[] args) { - // TODO Auto-generated method stub - int[] a = new int[10]; - a[0] = 0; - a[1] = 1; - a[2] = 2; - a[3] = 3; - System.arraycopy(a, 2, a, 3, 3); - a[2]=99; - for (int i = 0; i < a.length; i++) { - System.out.print(a[i] + " "); - } - } - -} -``` - -结果: - -``` -0 1 99 2 3 0 0 0 0 0 -``` - -### 3.2 `Arrays.copyOf()`方法 - -```java - /** - 以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素); 返回的数组的运行时类型是指定数组的运行时类型。 - */ - public Object[] toArray() { - //elementData:要复制的数组;size:要复制的长度 - return Arrays.copyOf(elementData, size); - } -``` - -个人觉得使用 `Arrays.copyOf()`方法主要是为了给原有数组扩容,测试代码如下: - -```java -public class ArrayscopyOfTest { - - public static void main(String[] args) { - int[] a = new int[3]; - a[0] = 0; - a[1] = 1; - a[2] = 2; - int[] b = Arrays.copyOf(a, 10); - System.out.println("b.length"+b.length); - } -} -``` - -结果: - -``` -10 -``` - -### 3.3 两者联系和区别 - -**联系:** - -看两者源代码可以发现 copyOf() 内部实际调用了 `System.arraycopy()` 方法 - -**区别:** - -`arraycopy()` 需要目标数组,将原数组拷贝到你自己定义的数组里或者原数组,而且可以选择拷贝的起点和长度以及放入新数组中的位置 `copyOf()` 是系统自动在内部新建一个数组,并返回该数组。 - -## 四 `ensureCapacity`方法 - -ArrayList 源码中有一个 `ensureCapacity` 方法不知道大家注意到没有,这个方法 ArrayList 内部没有被调用过,所以很显然是提供给用户调用的,那么这个方法有什么作用呢? - -```java - /** - 如有必要,增加此 ArrayList 实例的容量,以确保它至少可以容纳由minimum capacity参数指定的元素数。 - * - * @param minCapacity 所需的最小容量 - */ - public void ensureCapacity(int minCapacity) { - int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) - // any size if not default element table - ? 0 - // larger than default for default empty table. It's already - // supposed to be at default size. - : DEFAULT_CAPACITY; - - if (minCapacity > minExpand) { - ensureExplicitCapacity(minCapacity); - } - } - -``` - -**最好在 add 大量元素之前用 `ensureCapacity` 方法,以减少增量重新分配的次数** - -我们通过下面的代码实际测试以下这个方法的效果: - -```java -public class EnsureCapacityTest { - public static void main(String[] args) { - ArrayList list = new ArrayList(); - final int N = 10000000; - long startTime = System.currentTimeMillis(); - for (int i = 0; i < N; i++) { - list.add(i); - } - long endTime = System.currentTimeMillis(); - System.out.println("使用ensureCapacity方法前:"+(endTime - startTime)); - - } -} -``` - -运行结果: - -``` -使用ensureCapacity方法前:2158 -``` - -```java -public class EnsureCapacityTest { - public static void main(String[] args) { - ArrayList list = new ArrayList(); - final int N = 10000000; - list = new ArrayList(); - long startTime1 = System.currentTimeMillis(); - list.ensureCapacity(N); - for (int i = 0; i < N; i++) { - list.add(i); - } - long endTime1 = System.currentTimeMillis(); - System.out.println("使用ensureCapacity方法后:"+(endTime1 - startTime1)); - } -} -``` - -运行结果: - -``` - -使用ensureCapacity方法前:1773 -``` - -通过运行结果,我们可以看出向 ArrayList 添加大量元素之前最好先使用`ensureCapacity` 方法,以减少增量重新分配的次数。 diff --git a/docs/java/collection/ArrayList.md "b/docs/java/collection/ArrayList\346\272\220\347\240\201+\346\211\251\345\256\271\346\234\272\345\210\266\345\210\206\346\236\220.md" similarity index 57% rename from docs/java/collection/ArrayList.md rename to "docs/java/collection/ArrayList\346\272\220\347\240\201+\346\211\251\345\256\271\346\234\272\345\210\266\345\210\206\346\236\220.md" index 43e81ba6257..675f585ca27 100644 --- a/docs/java/collection/ArrayList.md +++ "b/docs/java/collection/ArrayList\346\272\220\347\240\201+\346\211\251\345\256\271\346\234\272\345\210\266\345\210\206\346\236\220.md" @@ -1,34 +1,35 @@ - +## 1. ArrayList 简介 -- [ArrayList简介](#arraylist简介) -- [ArrayList核心源码](#arraylist核心源码) -- [ArrayList源码分析](#arraylist源码分析) - - [System.arraycopy\(\)和Arrays.copyOf\(\)方法](#systemarraycopy和arrayscopyof方法) - - [两者联系与区别](#两者联系与区别) - - [ArrayList核心扩容技术](#arraylist核心扩容技术) - - [内部类](#内部类) -- [ArrayList经典Demo](#arraylist经典demo) +`ArrayList` 的底层是数组队列,相当于动态数组。与 Java 中的数组相比,它的容量能动态增长。在添加大量元素前,应用程序可以使用`ensureCapacity`操作来增加 `ArrayList` 实例的容量。这可以减少递增式再分配的数量。 - +`ArrayList`继承于 **`AbstractList`**,实现了 **`List`**, **`RandomAccess`**, **`Cloneable`**, **`java.io.Serializable`** 这些接口。 +```java + +public class ArrayList extends AbstractList + implements List, RandomAccess, Cloneable, java.io.Serializable{ -### ArrayList简介 -  ArrayList 的底层是数组队列,相当于动态数组。与 Java 中的数组相比,它的容量能动态增长。在添加大量元素前,应用程序可以使用`ensureCapacity`操作来增加 ArrayList 实例的容量。这可以减少递增式再分配的数量。 - - 它继承于 **AbstractList**,实现了 **List**, **RandomAccess**, **Cloneable**, **java.io.Serializable** 这些接口。 - - 在我们学数据结构的时候就知道了线性表的顺序存储,插入删除元素的时间复杂度为**O(n)**,求表长以及增加元素,取第 i 元素的时间复杂度为**O(1)** + } +``` -  ArrayList 继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。 +- `RandomAccess` 是一个标志接口,表明实现这个这个接口的 List 集合是支持**快速随机访问**的。在 `ArrayList` 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。 +- `ArrayList` 实现了 **`Cloneable` 接口** ,即覆盖了函数`clone()`,能被克隆。 +- `ArrayList` 实现了 `java.io.Serializable `接口,这意味着`ArrayList`支持序列化,能通过序列化去传输。 -  ArrayList 实现了**RandomAccess 接口**, RandomAccess 是一个标志接口,表明实现这个这个接口的 List 集合是支持**快速随机访问**的。在 ArrayList 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。 +### 1.1. Arraylist 和 Vector 的区别? -  ArrayList 实现了**Cloneable 接口**,即覆盖了函数 clone(),**能被克隆**。 +1. `ArrayList` 是 `List` 的主要实现类,底层使用 `Object[ ]`存储,适用于频繁的查找工作,线程不安全 ; +2. `Vector` 是 `List` 的古老实现类,底层使用 `Object[ ]`存储,线程安全的。 -  ArrayList 实现**java.io.Serializable 接口**,这意味着ArrayList**支持序列化**,**能通过序列化去传输**。 +### 1.2. Arraylist 与 LinkedList 区别? -  和 Vector 不同,**ArrayList 中的操作不是线程安全的**!所以,建议在单线程中才使用 ArrayList,而在多线程中可以选择 Vector 或者 CopyOnWriteArrayList。 -### ArrayList核心源码 +1. **是否保证线程安全:** `ArrayList` 和 `LinkedList` 都是不同步的,也就是不保证线程安全; +2. **底层数据结构:** `Arraylist` 底层使用的是 **`Object` 数组**;`LinkedList` 底层使用的是 **双向链表** 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!) +3. **插入和删除是否受元素位置的影响:** ① **`ArrayList` 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。** 比如:执行`add(E e)`方法的时候, `ArrayList` 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(`add(int index, E element)`)时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② **`LinkedList` 采用链表存储,所以对于`add(E e)`方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置`i`插入和删除元素的话(`(add(int index, E element)`) 时间复杂度近似为`o(n))`因为需要先移动到指定位置再插入。** +4. **是否支持快速随机访问:** `LinkedList` 不支持高效的随机元素访问,而 `ArrayList` 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于`get(int index)`方法)。 +5. **内存空间占用:** `ArrayList` 的空 间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 `LinkedList` 的空间花费则体现在它的每一个元素都需要消耗比 `ArrayList` 更多的空间(因为要存放直接后继和直接前驱以及数据)。 + +## 2. ArrayList 核心源码解读 ```java package java.util; @@ -100,7 +101,7 @@ public class ArrayList extends AbstractList elementData = c.toArray(); //如果elementData数组的长度不为0 if ((size = elementData.length) != 0) { - // 如果elementData不是Object类型数据(c.toArray可能返回的不是Object类型的数组所以加上下面的语句用于判断) + // 如果elementData不是Object类型数据(c.toArray可能返回的不是Object类型的数组所以加上下面的语句用于判断) if (elementData.getClass() != Object[].class) //将原来不是Object类型的elementData数组的内容,赋值给新的Object类型的elementData数组 elementData = Arrays.copyOf(elementData, size, Object[].class); @@ -111,7 +112,7 @@ public class ArrayList extends AbstractList } /** - * 修改这个ArrayList实例的容量是列表的当前大小。 应用程序可以使用此操作来最小化ArrayList实例的存储。 + * 修改这个ArrayList实例的容量是列表的当前大小。 应用程序可以使用此操作来最小化ArrayList实例的存储。 */ public void trimToSize() { modCount++; @@ -195,7 +196,7 @@ public class ArrayList extends AbstractList } /** - *返回此列表中的元素数。 + *返回此列表中的元素数。 */ public int size() { return size; @@ -213,12 +214,12 @@ public class ArrayList extends AbstractList * 如果此列表包含指定的元素,则返回true 。 */ public boolean contains(Object o) { - //indexOf()方法:返回此列表中指定元素的首次出现的索引,如果此列表不包含此元素,则为-1 + //indexOf()方法:返回此列表中指定元素的首次出现的索引,如果此列表不包含此元素,则为-1 return indexOf(o) >= 0; } /** - *返回此列表中指定元素的首次出现的索引,如果此列表不包含此元素,则为-1 + *返回此列表中指定元素的首次出现的索引,如果此列表不包含此元素,则为-1 */ public int indexOf(Object o) { if (o == null) { @@ -251,7 +252,7 @@ public class ArrayList extends AbstractList } /** - * 返回此ArrayList实例的浅拷贝。 (元素本身不被复制。) + * 返回此ArrayList实例的浅拷贝。 (元素本身不被复制。) */ public Object clone() { try { @@ -267,7 +268,7 @@ public class ArrayList extends AbstractList } /** - *以正确的顺序(从第一个到最后一个元素)返回一个包含此列表中所有元素的数组。 + *以正确的顺序(从第一个到最后一个元素)返回一个包含此列表中所有元素的数组。 *返回的数组将是“安全的”,因为该列表不保留对它的引用。 (换句话说,这个方法必须分配一个新的数组)。 *因此,调用者可以自由地修改返回的数组。 此方法充当基于阵列和基于集合的API之间的桥梁。 */ @@ -276,11 +277,11 @@ public class ArrayList extends AbstractList } /** - * 以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素); - *返回的数组的运行时类型是指定数组的运行时类型。 如果列表适合指定的数组,则返回其中。 - *否则,将为指定数组的运行时类型和此列表的大小分配一个新数组。 + * 以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素); + *返回的数组的运行时类型是指定数组的运行时类型。 如果列表适合指定的数组,则返回其中。 + *否则,将为指定数组的运行时类型和此列表的大小分配一个新数组。 *如果列表适用于指定的数组,其余空间(即数组的列表数量多于此元素),则紧跟在集合结束后的数组中的元素设置为null 。 - *(这仅在调用者知道列表不包含任何空元素的情况下才能确定列表的长度。) + *(这仅在调用者知道列表不包含任何空元素的情况下才能确定列表的长度。) */ @SuppressWarnings("unchecked") public T[] toArray(T[] a) { @@ -311,7 +312,7 @@ public class ArrayList extends AbstractList } /** - * 用指定的元素替换此列表中指定位置的元素。 + * 用指定的元素替换此列表中指定位置的元素。 */ public E set(int index, E element) { //对index进行界限检查 @@ -324,7 +325,7 @@ public class ArrayList extends AbstractList } /** - * 将指定的元素追加到此列表的末尾。 + * 将指定的元素追加到此列表的末尾。 */ public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! @@ -334,7 +335,7 @@ public class ArrayList extends AbstractList } /** - * 在此列表中的指定位置插入指定的元素。 + * 在此列表中的指定位置插入指定的元素。 *先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大; *再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。 */ @@ -350,7 +351,7 @@ public class ArrayList extends AbstractList } /** - * 删除该列表中指定位置的元素。 将任何后续元素移动到左侧(从其索引中减去一个元素)。 + * 删除该列表中指定位置的元素。 将任何后续元素移动到左侧(从其索引中减去一个元素)。 */ public E remove(int index) { rangeCheck(index); @@ -363,7 +364,7 @@ public class ArrayList extends AbstractList System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work - //从列表中删除的元素 + //从列表中删除的元素 return oldValue; } @@ -402,7 +403,7 @@ public class ArrayList extends AbstractList } /** - * 从列表中删除所有元素。 + * 从列表中删除所有元素。 */ public void clear() { modCount++; @@ -488,7 +489,7 @@ public class ArrayList extends AbstractList } /** - * 从此列表中删除指定集合中包含的所有元素。 + * 从此列表中删除指定集合中包含的所有元素。 */ public boolean removeAll(Collection c) { Objects.requireNonNull(c); @@ -498,7 +499,7 @@ public class ArrayList extends AbstractList /** * 仅保留此列表中包含在指定集合中的元素。 - *换句话说,从此列表中删除其中不包含在指定集合中的所有元素。 + *换句话说,从此列表中删除其中不包含在指定集合中的所有元素。 */ public boolean retainAll(Collection c) { Objects.requireNonNull(c); @@ -508,8 +509,8 @@ public class ArrayList extends AbstractList /** * 从列表中的指定位置开始,返回列表中的元素(按正确顺序)的列表迭代器。 - *指定的索引表示初始调用将返回的第一个元素为next 。 初始调用previous将返回指定索引减1的元素。 - *返回的列表迭代器是fail-fast 。 + *指定的索引表示初始调用将返回的第一个元素为next 。 初始调用previous将返回指定索引减1的元素。 + *返回的列表迭代器是fail-fast 。 */ public ListIterator listIterator(int index) { if (index < 0 || index > size) @@ -518,7 +519,7 @@ public class ArrayList extends AbstractList } /** - *返回列表中的列表迭代器(按适当的顺序)。 + *返回列表中的列表迭代器(按适当的顺序)。 *返回的列表迭代器是fail-fast 。 */ public ListIterator listIterator() { @@ -526,75 +527,103 @@ public class ArrayList extends AbstractList } /** - *以正确的顺序返回该列表中的元素的迭代器。 - *返回的迭代器是fail-fast 。 + *以正确的顺序返回该列表中的元素的迭代器。 + *返回的迭代器是fail-fast 。 */ public Iterator iterator() { return new Itr(); } - + ``` -### ArrayList源码分析 -#### System.arraycopy()和Arrays.copyOf()方法 -  通过上面源码我们发现这两个实现数组复制的方法被广泛使用而且很多地方都特别巧妙。比如下面add(int index, E element)方法就很巧妙的用到了arraycopy()方法让数组自己复制自己实现让index开始之后的所有成员后移一个位置: -```java - /** - * 在此列表中的指定位置插入指定的元素。 - *先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大; - *再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。 + +## 3. ArrayList 扩容机制分析 + +### 3.1. 先从 ArrayList 的构造函数说起 + +**(JDK8)ArrayList 有三种方式来初始化,构造方法源码如下:** + +```java + /** + * 默认初始容量大小 */ - public void add(int index, E element) { - rangeCheckForAdd(index); + private static final int DEFAULT_CAPACITY = 10; - ensureCapacityInternal(size + 1); // Increments modCount!! - //arraycopy()方法实现数组自己复制自己 - //elementData:源数组;index:源数组中的起始位置;elementData:目标数组;index + 1:目标数组中的起始位置; size - index:要复制的数组元素的数量; - System.arraycopy(elementData, index, elementData, index + 1, size - index); - elementData[index] = element; - size++; + + private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; + + /** + *默认构造函数,使用初始容量10构造一个空列表(无参数构造) + */ + public ArrayList() { + this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } -``` -又如toArray()方法中用到了copyOf()方法 -```java /** - *以正确的顺序(从第一个到最后一个元素)返回一个包含此列表中所有元素的数组。 - *返回的数组将是“安全的”,因为该列表不保留对它的引用。 (换句话说,这个方法必须分配一个新的数组)。 - *因此,调用者可以自由地修改返回的数组。 此方法充当基于阵列和基于集合的API之间的桥梁。 + * 带初始容量参数的构造函数。(用户自己指定容量) */ - public Object[] toArray() { - //elementData:要复制的数组;size:要复制的长度 - return Arrays.copyOf(elementData, size); + public ArrayList(int initialCapacity) { + if (initialCapacity > 0) {//初始容量大于0 + //创建initialCapacity大小的数组 + this.elementData = new Object[initialCapacity]; + } else if (initialCapacity == 0) {//初始容量等于0 + //创建空数组 + this.elementData = EMPTY_ELEMENTDATA; + } else {//初始容量小于0,抛出异常 + throw new IllegalArgumentException("Illegal Capacity: "+ + initialCapacity); + } } + + + /** + *构造包含指定collection元素的列表,这些元素利用该集合的迭代器按顺序返回 + *如果指定的集合为null,throws NullPointerException。 + */ + public ArrayList(Collection c) { + elementData = c.toArray(); + if ((size = elementData.length) != 0) { + // c.toArray might (incorrectly) not return Object[] (see 6260652) + if (elementData.getClass() != Object[].class) + elementData = Arrays.copyOf(elementData, size, Object[].class); + } else { + // replace with empty array. + this.elementData = EMPTY_ELEMENTDATA; + } + } + ``` -##### 两者联系与区别 -**联系:** -看两者源代码可以发现`copyOf()`内部调用了`System.arraycopy()`方法 -**区别:** -1. arraycopy()需要目标数组,将原数组拷贝到你自己定义的数组里,而且可以选择拷贝的起点和长度以及放入新数组中的位置 -2. copyOf()是系统自动在内部新建一个数组,并返回该数组。 -#### ArrayList 核心扩容技术 + +细心的同学一定会发现 :**以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为 10。** 下面在我们分析 ArrayList 扩容时会讲到这一点内容! + +> 补充:JDK7 new无参构造的ArrayList对象时,直接创建了长度是10的Object[]数组elementData 。jdk7中的ArrayList的对象的创建**类似于单例的饿汉式**,而jdk8中的ArrayList的对象的创建**类似于单例的懒汉式**。JDK8的内存优化也值得我们在平时开发中学习。 + +### 3.2. 一步一步分析 ArrayList 扩容机制 + +这里以无参构造函数创建的 ArrayList 为例分析 + +#### 3.2.1. 先来看 `add` 方法 + ```java -//下面是ArrayList的扩容机制 -//ArrayList的扩容机制提高了性能,如果每次只扩充一个, -//那么频繁的插入会导致频繁的拷贝,降低性能,而ArrayList的扩容机制避免了这种情况。 /** - * 如有必要,增加此ArrayList实例的容量,以确保它至少能容纳元素的数量 - * @param minCapacity 所需的最小容量 + * 将指定的元素追加到此列表的末尾。 */ - public void ensureCapacity(int minCapacity) { - int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) - // any size if not default element table - ? 0 - // larger than default for default empty table. It's already - // supposed to be at default size. - : DEFAULT_CAPACITY; - - if (minCapacity > minExpand) { - ensureExplicitCapacity(minCapacity); - } + public boolean add(E e) { + //添加元素之前,先调用ensureCapacityInternal方法 + ensureCapacityInternal(size + 1); // Increments modCount!! + //这里看到ArrayList添加元素的实质就相当于为数组赋值 + elementData[size++] = e; + return true; } +``` + +> **注意** :JDK11 移除了 `ensureCapacityInternal()` 和 `ensureExplicitCapacity()` 方法 + +#### 3.2.2. 再来看看 `ensureCapacityInternal()` 方法 + +(JDK7)可以看到 `add` 方法 首先调用了`ensureCapacityInternal(size + 1)` + +```java //得到最小扩容量 private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { @@ -604,25 +633,49 @@ public class ArrayList extends AbstractList ensureExplicitCapacity(minCapacity); } - //判断是否需要扩容,上面两个方法都要调用 +``` + +**当 要 add 进第 1 个元素时,minCapacity 为 1,在 Math.max()方法比较后,minCapacity 为 10。** + +> 此处和后续 JDK8 代码格式化略有不同,核心代码基本一样。 + +#### 3.2.3. `ensureExplicitCapacity()` 方法 + +如果调用 `ensureCapacityInternal()` 方法就一定会进入(执行)这个方法,下面我们来研究一下这个方法的源码! + +```java + //判断是否需要扩容 private void ensureExplicitCapacity(int minCapacity) { modCount++; - // 如果说minCapacity也就是所需的最小容量大于保存ArrayList数据的数组的长度的话,就需要调用grow(minCapacity)方法扩容。 - //这个minCapacity到底为多少呢?举个例子在添加元素(add)方法中这个minCapacity的大小就为现在数组的长度加1 + // overflow-conscious code if (minCapacity - elementData.length > 0) //调用grow方法进行扩容,调用此方法代表已经开始扩容了 grow(minCapacity); } ``` + +我们来仔细分析一下: + +- 当我们要 add 进第 1 个元素到 ArrayList 时,elementData.length 为 0 (因为还是一个空的 list),因为执行了 `ensureCapacityInternal()` 方法 ,所以 minCapacity 此时为 10。此时,`minCapacity - elementData.length > 0`成立,所以会进入 `grow(minCapacity)` 方法。 +- 当 add 第 2 个元素时,minCapacity 为 2,此时 e lementData.length(容量)在添加第一个元素后扩容成 10 了。此时,`minCapacity - elementData.length > 0` 不成立,所以不会进入 (执行)`grow(minCapacity)` 方法。 +- 添加第 3、4···到第 10 个元素时,依然不会执行 grow 方法,数组容量都为 10。 + +直到添加第 11 个元素,minCapacity(为 11)比 elementData.length(为 10)要大。进入 grow 方法进行扩容。 + +#### 3.2.4. `grow()` 方法 + ```java + /** + * 要分配的最大数组大小 + */ + private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; + /** * ArrayList扩容的核心方法。 */ private void grow(int minCapacity) { - //elementData为保存ArrayList数据的数组 - ///elementData.length求数组长度elementData.size是求数组中的元素个数 // oldCapacity为旧容量,newCapacity为新容量 int oldCapacity = elementData.length; //将oldCapacity 右移一位,其效果相当于oldCapacity /2, @@ -631,110 +684,217 @@ public class ArrayList extends AbstractList //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量, if (newCapacity - minCapacity < 0) newCapacity = minCapacity; - //再检查新容量是否超出了ArrayList所定义的最大容量, - //若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE, - //如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Interger.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。 + // 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE, + //如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); } - ``` -  扩容机制代码已经做了详细的解释。另外值得注意的是大家很容易忽略的一个运算符:**移位运算符** -  **简介**:移位运算符就是在二进制的基础上对数字进行平移。按照平移的方向和填充数字的规则分为三种:<<(左移)>>(带符号右移)>>>(无符号右移)。 -  **作用**:**对于大数据的2进制运算,位移运算符比那些普通运算符的运算要快很多,因为程序仅仅移动一下而已,不去计算,这样提高了效率,节省了资源** -  比如这里:int newCapacity = oldCapacity + (oldCapacity >> 1); -右移一位相当于除2,右移n位相当于除以 2 的 n 次方。这里 oldCapacity 明显右移了1位所以相当于oldCapacity /2。 -**另外需要注意的是:** +**int newCapacity = oldCapacity + (oldCapacity >> 1),所以 ArrayList 每次扩容之后容量都会变为原来的 1.5 倍左右(oldCapacity 为偶数就是 1.5 倍,否则是 1.5 倍左右)!** 奇偶不同,比如 :10+10/2 = 15, 33+33/2=49。如果是奇数的话会丢掉小数. + +> ">>"(移位运算符):>>1 右移一位相当于除 2,右移 n 位相当于除以 2 的 n 次方。这里 oldCapacity 明显右移了 1 位所以相当于 oldCapacity /2。对于大数据的 2 进制运算,位移运算符比那些普通运算符的运算要快很多,因为程序仅仅移动一下而已,不去计算,这样提高了效率,节省了资源 -1. java 中的**length 属性**是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性. +**我们再来通过例子探究一下`grow()` 方法 :** -2. java 中的**length()方法**是针对字 符串String说的,如果想看这个字符串的长度则用到 length()这个方法. +- 当 add 第 1 个元素时,oldCapacity 为 0,经比较后第一个 if 判断成立,newCapacity = minCapacity(为 10)。但是第二个 if 判断不会成立,即 newCapacity 不比 MAX_ARRAY_SIZE 大,则不会进入 `hugeCapacity` 方法。数组容量为 10,add 方法中 return true,size 增为 1。 +- 当 add 第 11 个元素进入 grow 方法时,newCapacity 为 15,比 minCapacity(为 11)大,第一个 if 判断不成立。新容量没有大于数组最大 size,不会进入 hugeCapacity 方法。数组容量扩为 15,add 方法中 return true,size 增为 11。 +- 以此类推······ -3. .java 中的**size()方法**是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看! +**这里补充一点比较重要,但是容易被忽视掉的知识点:** +- java 中的 `length`属性是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性. +- java 中的 `length()` 方法是针对字符串说的,如果想看这个字符串的长度则用到 `length()` 这个方法. +- java 中的 `size()` 方法是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看! + +#### 3.2.5. `hugeCapacity()` 方法。 + +从上面 `grow()` 方法源码我们知道: 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,如果 minCapacity 大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。 -#### 内部类 ```java - (1)private class Itr implements Iterator - (2)private class ListItr extends Itr implements ListIterator - (3)private class SubList extends AbstractList implements RandomAccess - (4)static final class ArrayListSpliterator implements Spliterator + private static int hugeCapacity(int minCapacity) { + if (minCapacity < 0) // overflow + throw new OutOfMemoryError(); + //对minCapacity和MAX_ARRAY_SIZE进行比较 + //若minCapacity大,将Integer.MAX_VALUE作为新数组的大小 + //若MAX_ARRAY_SIZE大,将MAX_ARRAY_SIZE作为新数组的大小 + //MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; + return (minCapacity > MAX_ARRAY_SIZE) ? + Integer.MAX_VALUE : + MAX_ARRAY_SIZE; + } ``` -  ArrayList有四个内部类,其中的**Itr是实现了Iterator接口**,同时重写了里面的**hasNext()**, **next()**, **remove()** 等方法;其中的**ListItr** 继承 **Itr**,实现了**ListIterator接口**,同时重写了**hasPrevious()**, **nextIndex()**, **previousIndex()**, **previous()**, **set(E e)**, **add(E e)** 等方法,所以这也可以看出了 **Iterator和ListIterator的区别:** ListIterator在Iterator的基础上增加了添加对象,修改对象,逆向遍历等方法,这些是Iterator不能实现的。 -### ArrayList经典Demo + +### 3.3. `System.arraycopy()` 和 `Arrays.copyOf()`方法 + +阅读源码的话,我们就会发现 ArrayList 中大量调用了这两个方法。比如:我们上面讲的扩容操作以及`add(int index, E element)`、`toArray()` 等方法中都用到了该方法! + +#### 3.3.1. `System.arraycopy()` 方法 ```java -package list; -import java.util.ArrayList; -import java.util.Iterator; - -public class ArrayListDemo { - - public static void main(String[] srgs){ - ArrayList arrayList = new ArrayList(); - - System.out.printf("Before add:arrayList.size() = %d\n",arrayList.size()); - - arrayList.add(1); - arrayList.add(3); - arrayList.add(5); - arrayList.add(7); - arrayList.add(9); - System.out.printf("After add:arrayList.size() = %d\n",arrayList.size()); - - System.out.println("Printing elements of arrayList"); - // 三种遍历方式打印元素 - // 第一种:通过迭代器遍历 - System.out.print("通过迭代器遍历:"); - Iterator it = arrayList.iterator(); - while(it.hasNext()){ - System.out.print(it.next() + " "); - } - System.out.println(); - - // 第二种:通过索引值遍历 - System.out.print("通过索引值遍历:"); - for(int i = 0; i < arrayList.size(); i++){ - System.out.print(arrayList.get(i) + " "); - } - System.out.println(); - - // 第三种:for循环遍历 - System.out.print("for循环遍历:"); - for(Integer number : arrayList){ - System.out.print(number + " "); - } - - // toArray用法 - // 第一种方式(最常用) - Integer[] integer = arrayList.toArray(new Integer[0]); - - // 第二种方式(容易理解) - Integer[] integer1 = new Integer[arrayList.size()]; - arrayList.toArray(integer1); - - // 抛出异常,java不支持向下转型 - //Integer[] integer2 = new Integer[arrayList.size()]; - //integer2 = arrayList.toArray(); - System.out.println(); - - // 在指定位置添加元素 - arrayList.add(2,2); - // 删除指定位置上的元素 - arrayList.remove(2); - // 删除指定元素 - arrayList.remove((Object)3); - // 判断arrayList是否包含5 - System.out.println("ArrayList contains 5 is: " + arrayList.contains(5)); - - // 清空ArrayList - arrayList.clear(); - // 判断ArrayList是否为空 - System.out.println("ArrayList is empty: " + arrayList.isEmpty()); + /** + * 在此列表中的指定位置插入指定的元素。 + *先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大; + *再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。 + */ + public void add(int index, E element) { + rangeCheckForAdd(index); + + ensureCapacityInternal(size + 1); // Increments modCount!! + //arraycopy()方法实现数组自己复制自己 + //elementData:源数组;index:源数组中的起始位置;elementData:目标数组;index + 1:目标数组中的起始位置; size - index:要复制的数组元素的数量; + System.arraycopy(elementData, index, elementData, index + 1, size - index); + elementData[index] = element; + size++; } +``` + +我们写一个简单的方法测试以下: + +```java +public class ArraycopyTest { + + public static void main(String[] args) { + // TODO Auto-generated method stub + int[] a = new int[10]; + a[0] = 0; + a[1] = 1; + a[2] = 2; + a[3] = 3; + System.arraycopy(a, 2, a, 3, 3); + a[2]=99; + for (int i = 0; i < a.length; i++) { + System.out.print(a[i] + " "); + } + } + } ``` +结果: + +``` +0 1 99 2 3 0 0 0 0 0 +``` + +#### 3.3.2. `Arrays.copyOf()`方法 + +```java + /** + 以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素); 返回的数组的运行时类型是指定数组的运行时类型。 + */ + public Object[] toArray() { + //elementData:要复制的数组;size:要复制的长度 + return Arrays.copyOf(elementData, size); + } +``` + +个人觉得使用 `Arrays.copyOf()`方法主要是为了给原有数组扩容,测试代码如下: + +```java +public class ArrayscopyOfTest { + + public static void main(String[] args) { + int[] a = new int[3]; + a[0] = 0; + a[1] = 1; + a[2] = 2; + int[] b = Arrays.copyOf(a, 10); + System.out.println("b.length"+b.length); + } +} +``` + +结果: + +``` +10 +``` + +#### 3.3.3. 两者联系和区别 + +**联系:** + +看两者源代码可以发现 `copyOf()`内部实际调用了 `System.arraycopy()` 方法 + +**区别:** + +`arraycopy()` 需要目标数组,将原数组拷贝到你自己定义的数组里或者原数组,而且可以选择拷贝的起点和长度以及放入新数组中的位置 `copyOf()` 是系统自动在内部新建一个数组,并返回该数组。 + +### 3.4. `ensureCapacity`方法 + +ArrayList 源码中有一个 `ensureCapacity` 方法不知道大家注意到没有,这个方法 ArrayList 内部没有被调用过,所以很显然是提供给用户调用的,那么这个方法有什么作用呢? + +```java + /** + 如有必要,增加此 ArrayList 实例的容量,以确保它至少可以容纳由minimum capacity参数指定的元素数。 + * + * @param minCapacity 所需的最小容量 + */ + public void ensureCapacity(int minCapacity) { + int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) + // any size if not default element table + ? 0 + // larger than default for default empty table. It's already + // supposed to be at default size. + : DEFAULT_CAPACITY; + + if (minCapacity > minExpand) { + ensureExplicitCapacity(minCapacity); + } + } + +``` + +**最好在 add 大量元素之前用 `ensureCapacity` 方法,以减少增量重新分配的次数** + +我们通过下面的代码实际测试以下这个方法的效果: + +```java +public class EnsureCapacityTest { + public static void main(String[] args) { + ArrayList list = new ArrayList(); + final int N = 10000000; + long startTime = System.currentTimeMillis(); + for (int i = 0; i < N; i++) { + list.add(i); + } + long endTime = System.currentTimeMillis(); + System.out.println("使用ensureCapacity方法前:"+(endTime - startTime)); + + } +} +``` + +运行结果: + +``` +使用ensureCapacity方法前:2158 +``` + +```java +public class EnsureCapacityTest { + public static void main(String[] args) { + ArrayList list = new ArrayList(); + final int N = 10000000; + list = new ArrayList(); + long startTime1 = System.currentTimeMillis(); + list.ensureCapacity(N); + for (int i = 0; i < N; i++) { + list.add(i); + } + long endTime1 = System.currentTimeMillis(); + System.out.println("使用ensureCapacity方法后:"+(endTime1 - startTime1)); + } +} +``` + +运行结果: + +``` +使用ensureCapacity方法前:1773 +``` + +通过运行结果,我们可以看出向 ArrayList 添加大量元素之前最好先使用`ensureCapacity` 方法,以减少增量重新分配的次数。 diff --git a/docs/java/collection/ConcurrentHashMap.md "b/docs/java/collection/ConcurrentHashMap\346\272\220\347\240\201+\345\272\225\345\261\202\346\225\260\346\215\256\347\273\223\346\236\204\345\210\206\346\236\220.md" similarity index 100% rename from docs/java/collection/ConcurrentHashMap.md rename to "docs/java/collection/ConcurrentHashMap\346\272\220\347\240\201+\345\272\225\345\261\202\346\225\260\346\215\256\347\273\223\346\236\204\345\210\206\346\236\220.md" diff --git a/docs/java/collection/HashMap.md "b/docs/java/collection/HashMap(JDK1.8)\346\272\220\347\240\201+\345\272\225\345\261\202\346\225\260\346\215\256\347\273\223\346\236\204\345\210\206\346\236\220.md" similarity index 100% rename from docs/java/collection/HashMap.md rename to "docs/java/collection/HashMap(JDK1.8)\346\272\220\347\240\201+\345\272\225\345\261\202\346\225\260\346\215\256\347\273\223\346\236\204\345\210\206\346\236\220.md" diff --git "a/docs/java/collection/Java\351\233\206\345\220\210\346\241\206\346\236\266\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230.md" "b/docs/java/collection/Java\351\233\206\345\220\210\346\241\206\346\236\266\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230.md" index 71e89ac8bc6..f9481b606b8 100644 --- "a/docs/java/collection/Java\351\233\206\345\220\210\346\241\206\346\236\266\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230.md" +++ "b/docs/java/collection/Java\351\233\206\345\220\210\346\241\206\346\236\266\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230.md" @@ -10,11 +10,6 @@ - [1.1.3.3. Map](#1133-map) - [1.1.4. 如何选用集合?](#114-如何选用集合) - [1.1.5. 为什么要使用集合?](#115-为什么要使用集合) - - [1.1.6. Iterator 迭代器](#116-iterator-迭代器) - - [1.1.6.1. 迭代器 Iterator 是什么?](#1161-迭代器-iterator-是什么) - - [1.1.6.2. 迭代器 Iterator 有啥用?](#1162-迭代器-iterator-有啥用) - - [1.1.6.3. 如何使用?](#1163-如何使用) - - [1.1.7. 有哪些集合是线程不安全的?怎么解决呢?](#117-有哪些集合是线程不安全的怎么解决呢) - [1.2. Collection 子接口之 List](#12-collection-子接口之-list) - [1.2.1. Arraylist 和 Vector 的区别?](#121-arraylist-和-vector-的区别) - [1.2.2. Arraylist 与 LinkedList 区别?](#122-arraylist-与-linkedlist-区别) @@ -46,13 +41,6 @@ - [1.5.1. 排序操作](#151-排序操作) - [1.5.2. 查找,替换操作](#152-查找替换操作) - [1.5.3. 同步控制](#153-同步控制) - - [1.6. 其他重要问题](#16-其他重要问题) - - [1.6.1. 什么是快速失败(fail-fast)?](#161-什么是快速失败fail-fast) - - [1.6.2. 什么是安全失败(fail-safe)呢?](#162-什么是安全失败fail-safe呢) - - [1.6.3. Arrays.asList()避坑指南](#163-arraysaslist避坑指南) - - [1.6.3.1. 简介](#1631-简介) - - [1.6.3.2. 《阿里巴巴 Java 开发手册》对其的描述](#1632-阿里巴巴-java-开发手册对其的描述) - - [1.6.3.3. 使用时的注意事项总结](#1633-使用时的注意事项总结) @@ -95,9 +83,9 @@ #### 1.1.3.3. Map -- `HashMap`: JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间 +- `HashMap`: JDK1.8 之前 `HashMap` 由数组+链表组成的,数组是 `HashMap` 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间 - `LinkedHashMap`: `LinkedHashMap` 继承自 `HashMap`,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,`LinkedHashMap` 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看:[《LinkedHashMap 源码详细分析(JDK1.8)》](https://www.imooc.com/article/22931) -- `Hashtable`: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的 +- `Hashtable`: 数组+链表组成的,数组是 `HashMap` 的主体,链表则是主要为了解决哈希冲突而存在的 - `TreeMap`: 红黑树(自平衡的排序二叉树) ### 1.1.4. 如何选用集合? @@ -112,65 +100,14 @@ 因为我们在实际开发中,存储的数据的类型是多种多样的,于是,就出现了“集合”,集合同样也是用来存储多个数据的。 数组的缺点是一旦声明之后,长度就不可变了;同时,声明数组时的数据类型也决定了该数组存储的数据的类型;而且,数组存储的数据是有序的、可重复的,特点单一。 -但是集合提高了数据存储的灵活性,Java 集合不仅可以用来存储不同类型不同数量的对象,还可以保存具有映射关系的数据 - -### 1.1.6. Iterator 迭代器 - -#### 1.1.6.1. 迭代器 Iterator 是什么? - -```java -public interface Iterator { - //集合中是否还有元素 - boolean hasNext(); - //获得集合中的下一个元素 - E next(); - ...... -} -``` - -`Iterator` 对象称为迭代器(设计模式的一种),迭代器可以对集合进行遍历,但每一个集合内部的数据结构可能是不尽相同的,所以每一个集合存和取都很可能是不一样的,虽然我们可以人为地在每一个类中定义 `hasNext()` 和 `next()` 方法,但这样做会让整个集合体系过于臃肿。于是就有了迭代器。 - -迭代器是将这样的方法抽取出接口,然后在每个类的内部,定义自己迭代方式,这样做就规定了整个集合体系的遍历方式都是 `hasNext()`和`next()`方法,使用者不用管怎么实现的,会用即可。迭代器的定义为:提供一种方法访问一个容器对象中各个元素,而又不需要暴露该对象的内部细节。 - -#### 1.1.6.2. 迭代器 Iterator 有啥用? - -`Iterator` 主要是用来遍历集合用的,它的特点是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出 `ConcurrentModificationException` 异常。 - -#### 1.1.6.3. 如何使用? - -我们通过使用迭代器来遍历 `HashMap`,演示一下 迭代器 Iterator 的使用。 - -```java - -Map map = new HashMap(); -map.put(1, "Java"); -map.put(2, "C++"); -map.put(3, "PHP"); -Iterator> iterator = map.entrySet().iterator(); -while (iterator.hasNext()) { - Map.Entry entry = iterator.next(); - System.out.println(entry.getKey() + entry.getValue()); -} -``` - -### 1.1.7. 有哪些集合是线程不安全的?怎么解决呢? - -我们常用的 `Arraylist` ,`LinkedList`,`Hashmap`,`HashSet`,`TreeSet`,`TreeMap`,`PriorityQueue` 都不是线程安全的。解决办法很简单,可以使用线程安全的集合来代替。 - -如果你要使用线程安全的集合的话, `java.util.concurrent` 包中提供了很多并发容器供你使用: - -1. `ConcurrentHashMap`: 可以看作是线程安全的 `HashMap` -2. `CopyOnWriteArrayList`:可以看作是线程安全的 `ArrayList`,在读多写少的场合性能非常好,远远好于 `Vector`. -3. `ConcurrentLinkedQueue`:高效的并发队列,使用链表实现。可以看做一个线程安全的 `LinkedList`,这是一个非阻塞队列。 -4. `BlockingQueue`: 这是一个接口,JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道。 -5. `ConcurrentSkipListMap` :跳表的实现。这是一个`Map`,使用跳表的数据结构进行快速查找。 +但是集合提高了数据存储的灵活性,Java 集合不仅可以用来存储不同类型不同数量的对象,还可以保存具有映射关系的数据。 ## 1.2. Collection 子接口之 List ### 1.2.1. Arraylist 和 Vector 的区别? -1. ArrayList 是 List 的主要实现类,底层使用 Object[ ]存储,适用于频繁的查找工作,线程不安全 ; -2. Vector 是 List 的古老实现类,底层使用 Object[ ]存储,线程安全的。 +- `ArrayList` 是 `List` 的主要实现类,底层使用 `Object[ ]`存储,适用于频繁的查找工作,线程不安全 ; +- `Vector` 是 `List` 的古老实现类,底层使用` Object[ ]` 存储,线程安全的。 ### 1.2.2. Arraylist 与 LinkedList 区别? @@ -217,7 +154,7 @@ public interface RandomAccess { ### 1.2.3. 说一说 ArrayList 的扩容机制吧 -详见笔主的这篇文章:[通过源码一步一步分析 ArrayList 扩容机制](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/ArrayList-Grow.md) +详见笔主的这篇文章:[通过源码一步一步分析 ArrayList 扩容机制](https://snailclimb.gitee.io/javaguide/#/docs/java/collection/ArrayList%E6%BA%90%E7%A0%81+%E6%89%A9%E5%AE%B9%E6%9C%BA%E5%88%B6%E5%88%86%E6%9E%90) ## 1.3. Collection 子接口之 Set @@ -358,23 +295,23 @@ Output: ### 1.3.3. 比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同 -HashSet 是 Set 接口的主要实现类 ,HashSet 的底层是 HashMap,线程不安全的,可以存储 null 值; +`HashSet` 是 `Set` 接口的主要实现类 ,`HashSet` 的底层是 `HashMap`,线程不安全的,可以存储 null 值; -LinkedHashSet 是 HashSet 的子类,能够按照添加的顺序遍历; +`LinkedHashSet` 是 `HashSet` 的子类,能够按照添加的顺序遍历; -TreeSet 底层使用红黑树,能够按照添加元素的顺序进行遍历,排序的方式有自然排序和定制排序。 +`TreeSet` 底层使用红黑树,能够按照添加元素的顺序进行遍历,排序的方式有自然排序和定制排序。 ## 1.4. Map 接口 ### 1.4.1. HashMap 和 Hashtable 的区别 -1. **线程是否安全:** HashMap 是非线程安全的,HashTable 是线程安全的,因为 HashTable 内部的方法基本都经过`synchronized` 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!); -2. **效率:** 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它; -3. **对 Null key 和 Null value 的支持:** HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;HashTable 不允许有 null 键和 null 值,否则会抛出 NullPointerException。 -4. **初始容量大小和每次扩充容量大小的不同 :** ① 创建时如果不指定容量初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小(HashMap 中的`tableSizeFor()`方法保证,下面给出了源代码)。也就是说 HashMap 总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。 -5. **底层数据结构:** JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。 +1. **线程是否安全:** `HashMap` 是非线程安全的,`HashTable` 是线程安全的,因为 `HashTable` 内部的方法基本都经过`synchronized` 修饰。(如果你要保证线程安全的话就使用 `ConcurrentHashMap` 吧!); +2. **效率:** 因为线程安全的问题,`HashMap` 要比 `HashTable` 效率高一点。另外,`HashTable` 基本被淘汰,不要在代码中使用它; +3. **对 Null key 和 Null value 的支持:** `HashMap` 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;HashTable 不允许有 null 键和 null 值,否则会抛出 `NullPointerException`。 +4. **初始容量大小和每次扩充容量大小的不同 :** ① 创建时如果不指定容量初始值,`Hashtable` 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。`HashMap` 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 `HashMap` 会将其扩充为 2 的幂次方大小(`HashMap` 中的`tableSizeFor()`方法保证,下面给出了源代码)。也就是说 `HashMap` 总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。 +5. **底层数据结构:** JDK1.8 以后的 `HashMap` 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。 -**HashMap 中带有初始容量的构造函数:** +**`HashMap` 中带有初始容量的构造函数:** ```java public HashMap(int initialCapacity, float loadFactor) { @@ -394,7 +331,7 @@ TreeSet 底层使用红黑树,能够按照添加元素的顺序进行遍历, } ``` -下面这个方法保证了 HashMap 总是使用 2 的幂作为哈希表的大小。 +下面这个方法保证了 `HashMap` 总是使用 2 的幂作为哈希表的大小。 ```java /** @@ -413,14 +350,14 @@ TreeSet 底层使用红黑树,能够按照添加元素的顺序进行遍历, ### 1.4.2. HashMap 和 HashSet 区别 -如果你看过 `HashSet` 源码的话就应该知道:HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常少,因为除了 `clone()`、`writeObject()`、`readObject()`是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。 +如果你看过 `HashSet` 源码的话就应该知道:`HashSet` 底层就是基于 `HashMap` 实现的。(`HashSet` 的源码非常非常少,因为除了 `clone()`、`writeObject()`、`readObject()`是 `HashSet` 自己不得不实现之外,其他方法都是直接调用 `HashMap` 中的方法。 -| HashMap | HashSet | -| :--------------------------------: | :----------------------------------------------------------: | -| 实现了 Map 接口 | 实现 Set 接口 | -| 存储键值对 | 仅存储对象 | -| 调用 `put()`向 map 中添加元素 | 调用 `add()`方法向 Set 中添加元素 | -| HashMap 使用键(Key)计算 Hashcode | HashSet 使用成员对象来计算 hashcode 值,对于两个对象来说 hashcode 可能相同,所以 equals()方法用来判断对象的相等性, | +| `HashMap` | `HashSet` | +| :------------------------------------: | :----------------------------------------------------------: | +| 实现了 `Map` 接口 | 实现 `Set` 接口 | +| 存储键值对 | 仅存储对象 | +| 调用 `put()`向 map 中添加元素 | 调用 `add()`方法向 `Set` 中添加元素 | +| `HashMap` 使用键(Key)计算 `hashcode` | `HashSet` 使用成员对象来计算 `hashcode` 值,对于两个对象来说 `hashcode` 可能相同,所以` equals()`方法用来判断对象的相等性 | ### 1.4.3. HashMap 和 TreeMap 区别 @@ -492,15 +429,17 @@ TreeMap treeMap = new TreeMap<>((person1, person2) -> { ### 1.4.4. HashSet 如何检查重复 -当你把对象加入`HashSet`时,HashSet 会先计算对象的`hashcode`值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用`equals()`方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让加入操作成功。(摘自我的 Java 启蒙书《Head fist java》第二版) +以下内容摘自我的 Java 启蒙书《Head fist java》第二版: + +当你把对象加入`HashSet`时,`HashSet` 会先计算对象的`hashcode`值来判断对象加入的位置,同时也会与其他加入的对象的 `hashcode` 值作比较,如果没有相符的 `hashcode`,`HashSet` 会假设对象没有重复出现。但是如果发现有相同 `hashcode` 值的对象,这时会调用`equals()`方法来检查 `hashcode` 相等的对象是否真的相同。如果两者相同,`HashSet` 就不会让加入操作成功。 -**hashCode()与 equals()的相关规定:** +**`hashCode()`与 `equals()` 的相关规定:** -1. 如果两个对象相等,则 hashcode 一定也是相同的 -2. 两个对象相等,对两个 equals 方法返回 true -3. 两个对象有相同的 hashcode 值,它们也不一定是相等的 -4. 综上,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖 -5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。 +1. 如果两个对象相等,则 `hashcode` 一定也是相同的 +2. 两个对象相等,对两个 `equals()` 方法返回 true +3. 两个对象有相同的 `hashcode` 值,它们也不一定是相等的 +4. 综上,`equals()` 方法被覆盖过,则 `hashCode()` 方法也必须被覆盖 +5. `hashCode() `的默认行为是对堆上的对象产生独特值。如果没有重写 `hashCode()`,则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。 **==与 equals 的区别** @@ -579,10 +518,10 @@ static int hash(int h) { ### 1.4.9. ConcurrentHashMap 和 Hashtable 的区别 -ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。 +`ConcurrentHashMap` 和 `Hashtable` 的区别主要体现在实现线程安全的方式上不同。 -- **底层数据结构:** JDK1.7 的 ConcurrentHashMap 底层采用 **分段的数组+链表** 实现,JDK1.8 采用的数据结构跟 HashMap1.8 的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 **数组+链表** 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的; -- **实现线程安全的方式(重要):** ① **在 JDK1.7 的时候,ConcurrentHashMap(分段锁)** 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 **到了 JDK1.8 的时候已经摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6 以后 对 synchronized 锁做了很多优化)** 整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② **Hashtable(同一把锁)** :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。 +- **底层数据结构:** JDK1.7 的 `ConcurrentHashMap` 底层采用 **分段的数组+链表** 实现,JDK1.8 采用的数据结构跟 `HashMap1.8` 的结构一样,数组+链表/红黑二叉树。`Hashtable` 和 JDK1.8 之前的 `HashMap` 的底层数据结构类似都是采用 **数组+链表** 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的; +- **实现线程安全的方式(重要):** ① **在 JDK1.7 的时候,`ConcurrentHashMap`(分段锁)** 对整个桶数组进行了分割分段(`Segment`),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 **到了 JDK1.8 的时候已经摒弃了 `Segment` 的概念,而是直接用 `Node` 数组+链表+红黑树的数据结构来实现,并发控制使用 `synchronized` 和 CAS 来操作。(JDK1.6 以后 对 `synchronized` 锁做了很多优化)** 整个看起来就像是优化过且线程安全的 `HashMap`,虽然在 JDK1.8 中还能看到 `Segment` 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② **`Hashtable`(同一把锁)** :使用 `synchronized` 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。 **两者的对比图:** @@ -610,22 +549,22 @@ JDK1.8 的 `ConcurrentHashMap` 不在是 **Segment 数组 + HashEntry 数组 + 首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。 -**ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成**。 +**`ConcurrentHashMap` 是由 `Segment` 数组结构和 `HashEntry` 数组结构组成**。 -Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。 +Segment 实现了 `ReentrantLock`,所以 `Segment` 是一种可重入锁,扮演锁的角色。`HashEntry` 用于存储键值对数据。 ```java static class Segment extends ReentrantLock implements Serializable { } ``` -一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和 HashMap 类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 的锁。 +一个 `ConcurrentHashMap` 里包含一个 `Segment` 数组。`Segment` 的结构和 `HashMap` 类似,是一种数组和链表结构,一个 `Segment` 包含一个 `HashEntry` 数组,每个 `HashEntry` 是一个链表结构的元素,每个 `Segment` 守护着一个 `HashEntry` 数组里的元素,当对 `HashEntry` 数组的数据进行修改时,必须首先获得对应的 `Segment` 的锁。 #### 1.4.10.2. JDK1.8 (上面有示意图) -ConcurrentHashMap 取消了 Segment 分段锁,采用 CAS 和 synchronized 来保证并发安全。数据结构跟 HashMap1.8 的结构类似,数组+链表/红黑二叉树。Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N))) +`ConcurrentHashMap` 取消了 `Segment` 分段锁,采用 CAS 和 `synchronized` 来保证并发安全。数据结构跟 HashMap1.8 的结构类似,数组+链表/红黑二叉树。Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N))) -synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。 +`synchronized` 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。 ## 1.5. Collections 工具类 @@ -675,170 +614,6 @@ synchronizedMap(Map m) //返回由指定映射支持的同步(线程安 synchronizedSet(Set s) //返回指定 set 支持的同步(线程安全的)set。 ``` -## 1.6. 其他重要问题 - -### 1.6.1. 什么是快速失败(fail-fast)? - -**快速失败(fail-fast)** 是 Java 集合的一种错误检测机制。**在使用迭代器对集合进行遍历的时候,我们在多线程下操作非安全失败(fail-safe)的集合类可能就会触发 fail-fast 机制,导致抛出 `ConcurrentModificationException` 异常。 另外,在单线程下,如果在遍历过程中对集合对象的内容进行了修改的话也会触发 fail-fast 机制。** - -> 注:增强 for 循环也是借助迭代器进行遍历。 - -举个例子:多线程下,如果线程 1 正在对集合进行遍历,此时线程 2 对集合进行修改(增加、删除、修改),或者线程 1 在遍历过程中对集合进行修改,都会导致线程 1 抛出 `ConcurrentModificationException` 异常。 - -**为什么呢?** - -每当迭代器使用 `hashNext()`/`next()`遍历下一个元素之前,都会检测 `modCount` 变量是否为 `expectedModCount` 值,是的话就返回遍历;否则抛出异常,终止遍历。 - -如果我们在集合被遍历期间对其进行修改的话,就会改变 `modCount` 的值,进而导致 `modCount != expectedModCount` ,进而抛出 `ConcurrentModificationException` 异常。 - -> 注:通过 `Iterator` 的方法修改集合的话会修改到 `expectedModCount` 的值,所以不会抛出异常。 - -```java -final void checkForComodification() { - if (modCount != expectedModCount) - throw new ConcurrentModificationException(); -} -``` - -好吧!相信大家已经搞懂了快速失败(fail-fast)机制以及它的原理。 - -我们再来趁热打铁,看一个阿里巴巴手册相关的规定: - -![](images/ad28e3ba-e419-4724-869c-73879e604da1.png) - -有了前面讲的基础,我们应该知道:使用 `Iterator` 提供的 `remove` 方法,可以修改到 `expectedModCount` 的值。所以,才不会再抛出`ConcurrentModificationException` 异常。 - -### 1.6.2. 什么是安全失败(fail-safe)呢? - -明白了快速失败(fail-fast)之后,安全失败(fail-safe)我们就很好理解了。 - -采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。所以,在遍历过程中对原集合所作的修改并不能被迭代器检测到,故不会抛 `ConcurrentModificationException` 异常。 - -### 1.6.3. Arrays.asList()避坑指南 - -最近使用`Arrays.asList()`遇到了一些坑,然后在网上看到这篇文章:[Java Array to List Examples](http://javadevnotes.com/java-array-to-list-examples) 感觉挺不错的,但是还不是特别全面。所以,自己对于这块小知识点进行了简单的总结。 - -#### 1.6.3.1. 简介 - -`Arrays.asList()`在平时开发中还是比较常见的,我们可以使用它将一个数组转换为一个 List 集合。 - -```java -String[] myArray = { "Apple", "Banana", "Orange" }; -List myList = Arrays.asList(myArray); -//上面两个语句等价于下面一条语句 -List myList = Arrays.asList("Apple","Banana", "Orange"); -``` - -JDK 源码对于这个方法的说明: - -```java -/** - *返回由指定数组支持的固定大小的列表。此方法作为基于数组和基于集合的API之间的桥梁,与 Collection.toArray()结合使用。返回的List是可序列化并实现RandomAccess接口。 - */ -public static List asList(T... a) { - return new ArrayList<>(a); -} -``` - -#### 1.6.3.2. 《阿里巴巴 Java 开发手册》对其的描述 - -`Arrays.asList()`将数组转换为集合后,底层其实还是数组,《阿里巴巴 Java 开发手册》对于这个方法有如下描述: - -![阿里巴巴Java开发手-Arrays.asList()方法]() - -#### 1.6.3.3. 使用时的注意事项总结 - -**传递的数组必须是对象数组,而不是基本类型。** - -`Arrays.asList()`是泛型方法,传入的对象必须是对象数组。 - -```java -int[] myArray = { 1, 2, 3 }; -List myList = Arrays.asList(myArray); -System.out.println(myList.size());//1 -System.out.println(myList.get(0));//数组地址值 -System.out.println(myList.get(1));//报错:ArrayIndexOutOfBoundsException -int [] array=(int[]) myList.get(0); -System.out.println(array[0]);//1 -``` - -当传入一个原生数据类型数组时,`Arrays.asList()` 的真正得到的参数就不是数组中的元素,而是数组对象本身!此时 List 的唯一元素就是这个数组,这也就解释了上面的代码。 - -我们使用包装类型数组就可以解决这个问题。 - -```java -Integer[] myArray = { 1, 2, 3 }; -``` - -**使用集合的修改方法:`add()`、`remove()`、`clear()`会抛出异常。** - -```java -List myList = Arrays.asList(1, 2, 3); -myList.add(4);//运行时报错:UnsupportedOperationException -myList.remove(1);//运行时报错:UnsupportedOperationException -myList.clear();//运行时报错:UnsupportedOperationException -``` - -`Arrays.asList()` 方法返回的并不是 `java.util.ArrayList` ,而是 `java.util.Arrays` 的一个内部类,这个内部类并没有实现集合的修改方法或者说并没有重写这些方法。 - -```java -List myList = Arrays.asList(1, 2, 3); -System.out.println(myList.getClass());//class java.util.Arrays$ArrayList -``` - -下图是`java.util.Arrays$ArrayList`的简易源码,我们可以看到这个类重写的方法有哪些。 - -```java - private static class ArrayList extends AbstractList - implements RandomAccess, java.io.Serializable - { - ... - - @Override - public E get(int index) { - ... - } - - @Override - public E set(int index, E element) { - ... - } - - @Override - public int indexOf(Object o) { - ... - } - - @Override - public boolean contains(Object o) { - ... - } - - @Override - public void forEach(Consumer action) { - ... - } - - @Override - public void replaceAll(UnaryOperator operator) { - ... - } - - @Override - public void sort(Comparator c) { - ... - } - } -``` - -我们再看一下`java.util.AbstractList`的`remove()`方法,这样我们就明白为啥会抛出`UnsupportedOperationException`。 - -```java -public E remove(int index) { - throw new UnsupportedOperationException(); -} -``` - **《Java面试突击》:** Java 程序员面试必备的《Java面试突击》V3.0 PDF 版本扫码关注下面的公众号,在后台回复 **"面试突击"** 即可免费领取! diff --git a/docs/java/collection/LinkedList.md "b/docs/java/collection/LinkedList\346\272\220\347\240\201\345\210\206\346\236\220.md" similarity index 99% rename from docs/java/collection/LinkedList.md rename to "docs/java/collection/LinkedList\346\272\220\347\240\201\345\210\206\346\236\220.md" index d26bc752267..a8159a34367 100644 --- a/docs/java/collection/LinkedList.md +++ "b/docs/java/collection/LinkedList\346\272\220\347\240\201\345\210\206\346\236\220.md" @@ -23,8 +23,10 @@ List list=Collections.synchronizedList(new LinkedList(...)); ``` ## 内部结构分析 **如下图所示:** -![LinkedList内部结构](https://user-gold-cdn.xitu.io/2018/3/19/1623e363fe0450b0?w=600&h=481&f=jpeg&s=18502) + +![LinkedList内部结构](images/linkedlist/LinkedList内部结构.png) 看完了图之后,我们再看LinkedList类中的一个**内部私有类Node**就很好理解了: + ```java private static class Node { E item;//节点值 diff --git "a/docs/java/collection/images/linkedlist/LinkedList\345\206\205\351\203\250\347\273\223\346\236\204.png" "b/docs/java/collection/images/linkedlist/LinkedList\345\206\205\351\203\250\347\273\223\346\236\204.png" new file mode 100644 index 00000000000..b70a93721e1 Binary files /dev/null and "b/docs/java/collection/images/linkedlist/LinkedList\345\206\205\351\203\250\347\273\223\346\236\204.png" differ diff --git "a/docs/java/images/Java\345\274\202\345\270\270\347\261\273\345\261\202\346\254\241\347\273\223\346\236\204\345\233\276.png" "b/docs/java/images/Java\345\274\202\345\270\270\347\261\273\345\261\202\346\254\241\347\273\223\346\236\204\345\233\276.png" deleted file mode 100644 index 595dc8afaf6..00000000000 Binary files "a/docs/java/images/Java\345\274\202\345\270\270\347\261\273\345\261\202\346\254\241\347\273\223\346\236\204\345\233\276.png" and /dev/null differ diff --git "a/docs/java/images/Java\345\274\202\345\270\270\347\261\273\345\261\202\346\254\241\347\273\223\346\236\204\345\233\2762.png" "b/docs/java/images/Java\345\274\202\345\270\270\347\261\273\345\261\202\346\254\241\347\273\223\346\236\204\345\233\2762.png" deleted file mode 100644 index fd2a910dec9..00000000000 Binary files "a/docs/java/images/Java\345\274\202\345\270\270\347\261\273\345\261\202\346\254\241\347\273\223\346\236\204\345\233\2762.png" and /dev/null differ diff --git a/docs/java/java-naming-conventions.md b/docs/java/java-naming-conventions.md deleted file mode 100644 index d205f5f5847..00000000000 --- a/docs/java/java-naming-conventions.md +++ /dev/null @@ -1,416 +0,0 @@ -> 原文链接:https://www.cnblogs.com/liqiangchn/p/12000361.html - -简洁清爽的代码风格应该是大多数工程师所期待的。在工作中笔者常常因为起名字而纠结,夸张点可以说是编程5分钟,命名两小时!究竟为什么命名成为了工作中的拦路虎。 - -每个公司都有不同的标准,目的是为了保持统一,减少沟通成本,提升团队研发效能。所以本文中是笔者结合阿里巴巴开发规范,以及工作中的见闻针对Java领域相关命名进行整理和总结,仅供参考。 - -## 一,Java中的命名规范 - -好的命名能体现出代码的特征,含义或者是用途,让阅读者可以根据名称的含义快速厘清程序的脉络。不同语言中采用的命名形式大相径庭,Java中常用到的命名形式共有三种,既首字母大写的UpperCamelCase,首字母小写的lowerCamelCase以及全部大写的并用下划线分割单词的UPPER_CAMEL_UNSER_SCORE。通常约定,**类一般采用大驼峰命名,方法和局部变量使用小驼峰命名,而大写下划线命名通常是常量和枚举中使用。** - -| 类型 | 约束 | 例 | -| :----: | :----------------------------------------------------------: | :--------------------------------------------: | -| 项目名 | 全部小写,多个单词用中划线分隔‘-’ | spring-cloud | -| 包名 | 全部小写 | com.alibaba.fastjson | -| 类名 | 单词首字母大写 | Feature, ParserConfig,DefaultFieldDeserializer | -| 变量名 | 首字母小写,多个单词组成时,除首个单词,其他单词首字母都要大写 | password, userName | -| 常量名 | 全部大写,多个单词,用'_'分隔 | CACHE_EXPIRED_TIME | -| 方法 | 同变量 | read(), readObject(), getById() | - -## 二,包命名 - -**包名**统一使用**小写**,**点分隔符**之间有且仅有一个自然语义的英文单词或者多个单词自然连接到一块(如 springframework,deepspace不需要使用任何分割)。包名统一使用单数形式,如果类命有复数含义,则可以使用复数形式。 - -包名的构成可以分为以下几四部分【前缀】 【发起者名】【项目名】【模块名】。常见的前缀可以分为以下几种: - -| 前缀名 | 例 | 含义 | -| :-------------: | :----------------------------: | :----------------------------------------------------------: | -| indi(或onem ) | indi.发起者名.项目名.模块名.…… | 个体项目,指个人发起,但非自己独自完成的项目,可公开或私有项目,copyright主要属于发起者。 | -| pers | pers.个人名.项目名.模块名.…… | 个人项目,指个人发起,独自完成,可分享的项目,copyright主要属于个人 | -| priv | priv.个人名.项目名.模块名.…… | 私有项目,指个人发起,独自完成,非公开的私人使用的项目,copyright属于个人。 | -| team | team.团队名.项目名.模块名.…… | 团队项目,指由团队发起,并由该团队开发的项目,copyright属于该团队所有 | -| 顶级域名 | com.公司名.项目名.模块名.…… | 公司项目,copyright由项目发起的公司所有 | - -## 三,类命名 - -**类名使用大驼峰命名形式**,类命通常时**名词或名词短语**,接口名除了用名词和名词短语以外,还可以使用形容词或形容词短语,如Cloneable,Callable等,表示实现该接口的类有某种功能或能力。对于测试类则以它要测试的类开头,以Test结尾,如HashMapTest。 - -对于一些特殊特有名词缩写也可以使用全大写命名,比如XMLHttpRequest,不过笔者认为缩写三个字母以内都大写,超过三个字母则按照要给单词算。这个没有标准如阿里巴巴中fastjson用JSONObject作为类命,而google则使用JsonObjectRequest命名,对于这种特殊的缩写,原则是统一就好。 - -| 属性 | 约束 | 例 | -| -------------- | ----------------------------------------- | ------------------------------------------------------------ | -| 抽象类 | Abstract 或者 Base 开头 | BaseUserService | -| 枚举类 | Enum 作为后缀 | GenderEnum | -| 工具类 | Utils作为后缀 | StringUtils | -| 异常类 | Exception结尾 | RuntimeException | -| 接口实现类 | 接口名+ Impl | UserServiceImpl | -| 领域模型相关 | /DO/DTO/VO/DAO | 正例:UserDAO 反例: UserDo, UserDao | -| 设计模式相关类 | Builder,Factory等 | 当使用到设计模式时,需要使用对应的设计模式作为后缀,如ThreadFactory | -| 处理特定功能的 | Handler,Predicate, Validator | 表示处理器,校验器,断言,这些类工厂还有配套的方法名如handle,predicate,validate | -| 测试类 | Test结尾 | UserServiceTest, 表示用来测试UserService类的 | -| MVC分层 | Controller,Service,ServiceImpl,DAO后缀 | UserManageController,UserManageDAO | - -## 四,方法 - -**方法命名采用小驼峰的形式**,首字小写,往后的每个单词首字母都要大写。 和类名不同的是,方法命名一般为**动词或动词短语**,与参数或参数名共同组成动宾短语,即动词 + 名词。一个好的函数名一般能通过名字直接获知该函数实现什么样的功能。 - -### 4.1 返回真伪值的方法 - -注:Prefix-前缀,Suffix-后缀,Alone-单独使用 - -| 位置 | 单词 | 意义 | 例 | -| ------ | ------ | ------------------------------------------------------------ | ------------- | -| Prefix | is | 对象是否符合期待的状态 | isValid | -| Prefix | can | 对象**能否执行**所期待的动作 | canRemove | -| Prefix | should | 调用方执行某个命令或方法是**好还是不好**,**应不应该**,或者说**推荐还是不推荐** | shouldMigrate | -| Prefix | has | 对象**是否持有**所期待的数据和属性 | hasObservers | -| Prefix | needs | 调用方**是否需要**执行某个命令或方法 | needsMigrate | - -### 4.2 用来检查的方法 - -| 单词 | 意义 | 例 | -| -------- | ---------------------------------------------------- | -------------- | -| ensure | 检查是否为期待的状态,不是则抛出异常或返回error code | ensureCapacity | -| validate | 检查是否为正确的状态,不是则抛出异常或返回error code | validateInputs | - -### 4.3 按需求才执行的方法 - -| 位置 | 单词 | 意义 | 例 | -| ------ | --------- | ----------------------------------------- | ---------------------- | -| Suffix | IfNeeded | 需要的时候执行,不需要的时候什么都不做 | drawIfNeeded | -| Prefix | might | 同上 | mightCreate | -| Prefix | try | 尝试执行,失败时抛出异常或是返回errorcode | tryCreate | -| Suffix | OrDefault | 尝试执行,失败时返回默认值 | getOrDefault | -| Suffix | OrElse | 尝试执行、失败时返回实际参数中指定的值 | getOrElse | -| Prefix | force | 强制尝试执行。error抛出异常或是返回值 | forceCreate, forceStop | - -### 4.4 异步相关方法 - -| 位置 | 单词 | 意义 | 例 | -| --------------- | ------------ | -------------------------------------------- | --------------------- | -| Prefix | blocking | 线程阻塞方法 | blockingGetUser | -| Suffix | InBackground | 执行在后台的线程 | doInBackground | -| Suffix | Async | 异步方法 | sendAsync | -| Suffix | Sync | 对应已有异步方法的同步方法 | sendSync | -| Prefix or Alone | schedule | Job和Task放入队列 | schedule, scheduleJob | -| Prefix or Alone | post | 同上 | postJob | -| Prefix or Alone | execute | 执行异步方法(注:我一般拿这个做同步方法名) | execute, executeTask | -| Prefix or Alone | start | 同上 | start, startJob | -| Prefix or Alone | cancel | 停止异步方法 | cancel, cancelJob | -| Prefix or Alone | stop | 同上 | stop, stopJob | - -### 4.5 回调方法 - -| 位置 | 单词 | 意义 | 例 | -| ------ | ------ | -------------------------- | ------------ | -| Prefix | on | 事件发生时执行 | onCompleted | -| Prefix | before | 事件发生前执行 | beforeUpdate | -| Prefix | pre | 同上 | preUpdate | -| Prefix | will | 同上 | willUpdate | -| Prefix | after | 事件发生后执行 | afterUpdate | -| Prefix | post | 同上 | postUpdate | -| Prefix | did | 同上 | didUpdate | -| Prefix | should | 确认事件是否可以发生时执行 | shouldUpdate | - -### 4.6 操作对象生命周期的方法 - -| 单词 | 意义 | 例 | -| ---------- | ------------------------------ | --------------- | -| initialize | 初始化。也可作为延迟初始化使用 | initialize | -| pause | 暂停 | onPause ,pause | -| stop | 停止 | onStop,stop | -| abandon | 销毁的替代 | abandon | -| destroy | 同上 | destroy | -| dispose | 同上 | dispose | - -### 4.7 与集合操作相关的方法 - -| 单词 | 意义 | 例 | -| -------- | ---------------------------- | ---------- | -| contains | 是否持有与指定对象相同的对象 | contains | -| add | 添加 | addJob | -| append | 添加 | appendJob | -| insert | 插入到下标n | insertJob | -| put | 添加与key对应的元素 | putJob | -| remove | 移除元素 | removeJob | -| enqueue | 添加到队列的最末位 | enqueueJob | -| dequeue | 从队列中头部取出并移除 | dequeueJob | -| push | 添加到栈头 | pushJob | -| pop | 从栈头取出并移除 | popJob | -| peek | 从栈头取出但不移除 | peekJob | -| find | 寻找符合条件的某物 | findById | - -### 4.8 与数据相关的方法 - -| 单词 | 意义 | 例 | -| ------ | -------------------------------------- | ------------- | -| create | 新创建 | createAccount | -| new | 新创建 | newAccount | -| from | 从既有的某物新建,或是从其他的数据新建 | fromConfig | -| to | 转换 | toString | -| update | 更新既有某物 | updateAccount | -| load | 读取 | loadAccount | -| fetch | 远程读取 | fetchAccount | -| delete | 删除 | deleteAccount | -| remove | 删除 | removeAccount | -| save | 保存 | saveAccount | -| store | 保存 | storeAccount | -| commit | 保存 | commitChange | -| apply | 保存或应用 | applyChange | -| clear | 清除数据或是恢复到初始状态 | clearAll | -| reset | 清除数据或是恢复到初始状态 | resetAll | - -### 4.9 成对出现的动词 - -| 单词 | 意义 | -| -------------- | ----------------- | -| get获取 | set 设置 | -| add 增加 | remove 删除 | -| create 创建 | destory 移除 | -| start 启动 | stop 停止 | -| open 打开 | close 关闭 | -| read 读取 | write 写入 | -| load 载入 | save 保存 | -| create 创建 | destroy 销毁 | -| begin 开始 | end 结束 | -| backup 备份 | restore 恢复 | -| import 导入 | export 导出 | -| split 分割 | merge 合并 | -| inject 注入 | extract 提取 | -| attach 附着 | detach 脱离 | -| bind 绑定 | separate 分离 | -| view 查看 | browse 浏览 | -| edit 编辑 | modify 修改 | -| select 选取 | mark 标记 | -| copy 复制 | paste 粘贴 | -| undo 撤销 | redo 重做 | -| insert 插入 | delete 移除 | -| add 加入 | append 添加 | -| clean 清理 | clear 清除 | -| index 索引 | sort 排序 | -| find 查找 | search 搜索 | -| increase 增加 | decrease 减少 | -| play 播放 | pause 暂停 | -| launch 启动 | run 运行 | -| compile 编译 | execute 执行 | -| debug 调试 | trace 跟踪 | -| observe 观察 | listen 监听 | -| build 构建 | publish 发布 | -| input 输入 | output 输出 | -| encode 编码 | decode 解码 | -| encrypt 加密 | decrypt 解密 | -| compress 压缩 | decompress 解压缩 | -| pack 打包 | unpack 解包 | -| parse 解析 | emit 生成 | -| connect 连接 | disconnect 断开 | -| send 发送 | receive 接收 | -| download 下载 | upload 上传 | -| refresh 刷新 | synchronize 同步 | -| update 更新 | revert 复原 | -| lock 锁定 | unlock 解锁 | -| check out 签出 | check in 签入 | -| submit 提交 | commit 交付 | -| push 推 | pull 拉 | -| expand 展开 | collapse 折叠 | -| begin 起始 | end 结束 | -| start 开始 | finish 完成 | -| enter 进入 | exit 退出 | -| abort 放弃 | quit 离开 | -| obsolete 废弃 | depreciate 废旧 | -| collect 收集 | aggregate 聚集 | - -## 五,变量&常量命名 - -### 5.1 变量命名 - -变量是指在程序运行中可以改变其值的量,包括成员变量和局部变量。变量名由多单词组成时,第一个单词的首字母小写,其后单词的首字母大写,俗称骆驼式命名法(也称驼峰命名法),如 computedValues,index、变量命名时,尽量简短且能清楚的表达变量的作用,命名体现具体的业务含义即可。 - -变量名不应以下划线或美元符号开头,尽管这在语法上是允许的。变量名应简短且富于描述。变量名的选用应该易于记忆,即,能够指出其用途。尽量避免单个字符的变量名,除非是一次性的临时变量。pojo中的布尔变量,都不要加is(数据库中的布尔字段全都要加 is_ 前缀)。 - -### 5.2 常量命名 - -常量命名CONSTANT_CASE,一般采用全部大写(作为方法参数时除外),单词间用下划线分割。那么什么是常量呢? - -常量是在作用域内保持不变的值,一般使用final进行修饰。一般分为三种,全局常量(public static final修饰),类内常量(private static final 修饰)以及局部常量(方法内,或者参数中的常量),局部常量比较特殊,通常采用小驼峰命名即可。 - -```java -/** - * 一个demo - * - * @author Jann Lee - * @date 2019-12-07 00:25 - **/ -public class HelloWorld { - - /** - * 局部常量(正例) - */ - public static final long USER_MESSAGE_CACHE_EXPIRE_TIME = 3600; - - /** - * 局部常量(反例,命名不清晰) - */ - public static final long MESSAGE_CACHE_TIME = 3600; - - /** - * 全局常量 - */ - private static final String ERROR_MESSAGE = " error message"; - - /** - * 成员变量 - */ - private int currentUserId; - - /** - * 控制台打印 {@code message} 信息 - * - * @param message 消息体,局部常量 - */ - public void sayHello(final String message){ - System.out.println("Hello world!"); - } - -} -``` - -常量一般都有自己的业务含义,**不要害怕长度过长而进行省略或者缩写**。如,用户消息缓存过期时间的表示,那种方式更佳清晰,交给你来评判。 - -## 通用命名规则[#](https://www.cnblogs.com/liqiangchn/p/12000361.html#450918152) - -1. 尽量不要使用拼音;杜绝拼音和英文混用。对于一些通用的表示或者难以用英文描述的可以采用拼音,一旦采用拼音就坚决不能和英文混用。 - 正例: BeiJing, HangZhou - 反例: validateCanShu -2. 命名过程中尽量不要出现特殊的字符,常量除外。 -3. 尽量不要和jdk或者框架中已存在的类重名,也不能使用java中的关键字命名。 -4. 妙用介词,如for(可以用同音的4代替), to(可用同音的2代替), from, with,of等。 - 如类名采用User4RedisDO,方法名getUserInfoFromRedis,convertJson2Map等。 - -## 六,代码注解 - -### 6.1 注解的原则 - -好的命名增加代码阅读性,代码的命名往往有严格的限制。而注解不同,程序员往往可以自由发挥,单并不意味着可以为所欲为之胡作非为。优雅的注解通常要满足三要素。 - -1. Nothing is strange - 没有注解的代码对于阅读者非常不友好,哪怕代码写的在清除,阅读者至少从心理上会有抵触,更何况代码中往往有许多复杂的逻辑,所以一定要写注解,不仅要记录代码的逻辑,还有说清楚修改的逻辑。 -2. Less is more - 从代码维护角度来讲,代码中的注解一定是精华中的精华。合理清晰的命名能让代码易于理解,对于逻辑简单且命名规范,能够清楚表达代码功能的代码不需要注解。滥用注解会增加额外的负担,更何况大部分都是废话。 - -```java -// 根据id获取信息【废话注解】 -getMessageById(id) -``` - -1. Advance with the time - 注解应该随着代码的变动而改变,注解表达的信息要与代码中完全一致。通常情况下修改代码后一定要修改注解。 - -### 6.2 注解格式 - -注解大体上可以分为两种,一种是javadoc注解,另一种是简单注解。javadoc注解可以生成JavaAPI为外部用户提供有效的支持javadoc注解通常在使用IDEA,或者Eclipse等开发工具时都可以自动生成,也支持自定义的注解模板,仅需要对对应的字段进行解释。参与同一项目开发的同学,尽量设置成相同的注解模板。 - -#### a. 包注解 - -包注解在工作中往往比较特殊,通过包注解可以快速知悉当前包下代码是用来实现哪些功能,强烈建议工作中加上,尤其是对于一些比较复杂的包,包注解一般在包的根目录下,名称统一为package-info.java。 - -```java -/** - * 落地也质量检测 - * 1. 用来解决什么问题 - * 对广告主投放的广告落地页进行性能检测,模拟不同的系统,如Android,IOS等; 模拟不同的网络:2G,3G,4G,wifi等 - * - * 2. 如何实现 - * 基于chrome浏览器,用chromedriver驱动浏览器,设置对应的网络,OS参数,获取到浏览器返回结果。 - * - * 注意: 网络环境配置信息{@link cn.mycookies.landingpagecheck.meta.NetWorkSpeedEnum}目前使用是常规速度,可以根据实际情况进行调整 - * - * @author cruder - * @time 2019/12/7 20:3 下午 - */ -package cn.mycookies.landingpagecheck; -``` - -#### b. 类注接 - -javadoc注解中,每个类都必须有注解。 - -```java -/** -* Copyright (C), 2019-2020, Jann balabala... -* -* 类的介绍:这是一个用来做什么事情的类,有哪些功能,用到的技术..... -* -* @author 类创建者姓名 保持对齐 -* @date 创建日期 保持对齐 -* @version 版本号 保持对齐 -*/ -``` - -#### c. 属性注解 - -在每个属性前面必须加上属性注释,通常有以下两种形式,至于怎么选择,你高兴就好,不过一个项目中要保持统一。 - -```java -/** 提示信息 */ -private String userName; -/** - * 密码 - */ -private String password; -``` - -#### d. 方法注释 - -在每个方法前面必须加上方法注释,对于方法中的每个参数,以及返回值都要有说明。 - -```java -/** - * 方法的详细说明,能干嘛,怎么实现的,注意事项... - * - * @param xxx 参数1的使用说明, 能否为null - * @return 返回结果的说明, 不同情况下会返回怎样的结果 - * @throws 异常类型 注明从此类方法中抛出异常的说明 - */ -``` - -#### e. 构造方法注释 - -在每个构造方法前面必须加上注释,注释模板如下: - -```java - /** - * 构造方法的详细说明 - * - * @param xxx 参数1的使用说明, 能否为null - * @throws 异常类型 注明从此类方法中抛出异常的说明 - */ -``` - -而简单注解往往是需要工程师字节定义,在使用注解时应该注意以下几点: - -1. 枚举类的各个属性值都要使用注解,枚举可以理解为是常量,通常不会发生改变,通常会被在多个地方引用,对枚举的修改和添加属性通常会带来很大的影响。 -2. 保持排版整洁,不要使用行尾注释;双斜杠和星号之后要用1个空格分隔。 - -```java -id = 1;// 反例:不要使用行尾注释 -//反例:换行符与注释之间没有缩进 -int age = 18; -// 正例:姓名 -String name; -/** - * 1. 多行注释 - * - * 2. 对于不同的逻辑说明,可以用空行分隔 - */ -``` - -## 总结 - -无论是命名和注解,他们的目的都是为了让代码和工程师进行对话,增强代码的可读性,可维护性。优秀的代码往往能够见名知意,注解往往是对命名的补充和完善。命名太南了! - -参考文献: - -- 《码出高效》 -- https://www.cnblogs.com/wangcp-2014/p/10215620.html -- https://qiita.com/KeithYokoma/items/2193cf79ba76563e3db6 -- https://google.github.io/styleguide/javaguide.html#s2.1-file-name \ No newline at end of file diff --git "a/docs/java/java-programming-problem/Java\347\250\213\345\272\217\350\256\276\350\256\241\351\242\230.md" "b/docs/java/java-programming-problem/Java\347\250\213\345\272\217\350\256\276\350\256\241\351\242\230.md" deleted file mode 100644 index 112c1bff5f7..00000000000 --- "a/docs/java/java-programming-problem/Java\347\250\213\345\272\217\350\256\276\350\256\241\351\242\230.md" +++ /dev/null @@ -1,130 +0,0 @@ - - -- [0.0.1. 泛型的实际应用:实现最小值函数](#001-%e6%b3%9b%e5%9e%8b%e7%9a%84%e5%ae%9e%e9%99%85%e5%ba%94%e7%94%a8%e5%ae%9e%e7%8e%b0%e6%9c%80%e5%b0%8f%e5%80%bc%e5%87%bd%e6%95%b0) -- [0.0.2. 使用数组实现栈](#002-%e4%bd%bf%e7%94%a8%e6%95%b0%e7%bb%84%e5%ae%9e%e7%8e%b0%e6%a0%88) -- [0.0.3. 实现线程安全的 LRU 缓存](#003-%e5%ae%9e%e7%8e%b0%e7%ba%bf%e7%a8%8b%e5%ae%89%e5%85%a8%e7%9a%84-lru-%e7%bc%93%e5%ad%98) - - - -### 0.0.1. 泛型的实际应用:实现最小值函数 - -自己设计一个泛型的获取数组最小值的函数.并且这个方法只能接受Number的子类并且实现了Comparable接口。 - -```java -//注意:Number并没有实现Comparable -private static > T min(T[] values) { - if (values == null || values.length == 0) return null; - T min = values[0]; - for (int i = 1; i < values.length; i++) { - if (min.compareTo(values[i]) > 0) min = values[i]; - } - return min; -} -``` - -测试: - -```java -int minInteger = min(new Integer[]{1, 2, 3});//result:1 -double minDouble = min(new Double[]{1.2, 2.2, -1d});//result:-1d -String typeError = min(new String[]{"1","3"});//报错 -``` -### 0.0.2. 使用数组实现栈 - -**自己实现一个栈,要求这个栈具有`push()`、`pop()`(返回栈顶元素并出栈)、`peek()` (返回栈顶元素不出栈)、`isEmpty()`、`size()`这些基本的方法。** - -提示:每次入栈之前先判断栈的容量是否够用,如果不够用就用`Arrays.copyOf()`进行扩容; - -```java -public class MyStack { - private int[] storage;//存放栈中元素的数组 - private int capacity;//栈的容量 - private int count;//栈中元素数量 - private static final int GROW_FACTOR = 2; - - //不带初始容量的构造方法。默认容量为8 - public MyStack() { - this.capacity = 8; - this.storage=new int[8]; - this.count = 0; - } - - //带初始容量的构造方法 - public MyStack(int initialCapacity) { - if (initialCapacity < 1) - throw new IllegalArgumentException("Capacity too small."); - - this.capacity = initialCapacity; - this.storage = new int[initialCapacity]; - this.count = 0; - } - - //入栈 - public void push(int value) { - if (count == capacity) { - ensureCapacity(); - } - storage[count++] = value; - } - - //确保容量大小 - private void ensureCapacity() { - int newCapacity = capacity * GROW_FACTOR; - storage = Arrays.copyOf(storage, newCapacity); - capacity = newCapacity; - } - - //返回栈顶元素并出栈 - private int pop() { - if (count == 0) - throw new IllegalArgumentException("Stack is empty."); - count--; - return storage[count]; - } - - //返回栈顶元素不出栈 - private int peek() { - if (count == 0){ - throw new IllegalArgumentException("Stack is empty."); - }else { - return storage[count-1]; - } - } - - //判断栈是否为空 - private boolean isEmpty() { - return count == 0; - } - - //返回栈中元素的个数 - private int size() { - return count; - } - -} - -``` - -验证 - -```java -MyStack myStack = new MyStack(3); -myStack.push(1); -myStack.push(2); -myStack.push(3); -myStack.push(4); -myStack.push(5); -myStack.push(6); -myStack.push(7); -myStack.push(8); -System.out.println(myStack.peek());//8 -System.out.println(myStack.size());//8 -for (int i = 0; i < 8; i++) { - System.out.println(myStack.pop()); -} -System.out.println(myStack.isEmpty());//true -myStack.pop();//报错:java.lang.IllegalArgumentException: Stack is empty. -``` - - - diff --git a/docs/java/java-programming-problem/a-thread-safe-implementation-of-lru-cache.md b/docs/java/java-programming-problem/a-thread-safe-implementation-of-lru-cache.md deleted file mode 100644 index 7c367e11bd0..00000000000 --- a/docs/java/java-programming-problem/a-thread-safe-implementation-of-lru-cache.md +++ /dev/null @@ -1,441 +0,0 @@ - - -- [1. LRU 缓存介绍](#1-lru-%e7%bc%93%e5%ad%98%e4%bb%8b%e7%bb%8d) -- [2. ConcurrentLinkedQueue简单介绍](#2-concurrentlinkedqueue%e7%ae%80%e5%8d%95%e4%bb%8b%e7%bb%8d) -- [3. ReadWriteLock简单介绍](#3-readwritelock%e7%ae%80%e5%8d%95%e4%bb%8b%e7%bb%8d) -- [4. ScheduledExecutorService 简单介绍](#4-scheduledexecutorservice-%e7%ae%80%e5%8d%95%e4%bb%8b%e7%bb%8d) -- [5. 徒手撸一个线程安全的 LRU 缓存](#5-%e5%be%92%e6%89%8b%e6%92%b8%e4%b8%80%e4%b8%aa%e7%ba%bf%e7%a8%8b%e5%ae%89%e5%85%a8%e7%9a%84-lru-%e7%bc%93%e5%ad%98) - - [5.1. 实现方法](#51-%e5%ae%9e%e7%8e%b0%e6%96%b9%e6%b3%95) - - [5.2. 原理](#52-%e5%8e%9f%e7%90%86) - - [5.3. put方法具体流程分析](#53-put%e6%96%b9%e6%b3%95%e5%85%b7%e4%bd%93%e6%b5%81%e7%a8%8b%e5%88%86%e6%9e%90) - - [5.4. 源码](#54-%e6%ba%90%e7%a0%81) -- [6. 实现一个线程安全并且带有过期时间的 LRU 缓存](#6-%e5%ae%9e%e7%8e%b0%e4%b8%80%e4%b8%aa%e7%ba%bf%e7%a8%8b%e5%ae%89%e5%85%a8%e5%b9%b6%e4%b8%94%e5%b8%a6%e6%9c%89%e8%bf%87%e6%9c%9f%e6%97%b6%e9%97%b4%e7%9a%84-lru-%e7%bc%93%e5%ad%98) - - - -最近被读者问到“不用LinkedHashMap的话,如何实现一个线程安全的 LRU 缓存?网上的代码太杂太乱,Guide哥哥能不能帮忙写一个?”。 - -*划重点,手写一个 LRU 缓存在面试中还是挺常见的!* - -很多人就会问了:“网上已经有这么多现成的缓存了!为什么面试官还要我们自己实现一个呢?” 。咳咳咳,当然是为了面试需要。哈哈!开个玩笑,我个人觉得更多地是为了学习吧!今天Guide哥教大家: - -1. 实现一个线程安全的 LRU 缓存 -2. 实现一个线程安全并且带有过期时间的 LRU 缓存 - -考虑到了线程安全性我们使用了 `ConcurrentHashMap` 、`ConcurrentLinkedQueue` 这两个线程安全的集合。另外,还用到 `ReadWriteLock`(读写锁)。为了实现带有过期时间的缓存,我们用到了 `ScheduledExecutorService`来做定时任务执行。 - -如果有任何不对或者需要完善的地方,请帮忙指出! - -### 1. LRU 缓存介绍 - -**LRU (Least Recently Used,最近最少使用)是一种缓存淘汰策略。** - -LRU缓存指的是当缓存大小已达到最大分配容量的时候,如果再要去缓存新的对象数据的话,就需要将缓存中最近访问最少的对象删除掉以便给新来的数据腾出空间。 - -### 2. ConcurrentLinkedQueue简单介绍 - -**ConcurrentLinkedQueue是一个基于单向链表的无界无锁线程安全的队列,适合在高并发环境下使用,效率比较高。** 我们在使用的时候,可以就把它理解为我们经常接触的数据结构——队列,不过是增加了多线程下的安全性保证罢了。**和普通队列一样,它也是按照先进先出(FIFO)的规则对接点进行排序。** 另外,队列元素中不可以放置null元素。 - -`ConcurrentLinkedQueue` 整个继承关系如下图所示: - -![](./../../../media/pictures/java/my-lru-cache/ConcurrentLinkedQueue-Diagram.png) - -`ConcurrentLinkedQueue中`最主要的两个方法是:`offer(value)`和`poll()`,分别实现队列的两个重要的操作:入队和出队(`offer(value)`等价于 `add(value)`)。 - -我们添加一个元素到队列的时候,它会添加到队列的尾部,当我们获取一个元素时,它会返回队列头部的元素。 - -![单链表](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/单链表2.png) - -利用`ConcurrentLinkedQueue`队列先进先出的特性,每当我们 `put`/`get`(缓存被使用)元素的时候,我们就将这个元素存放在队列尾部,这样就能保证队列头部的元素是最近最少使用的。 - -### 3. ReadWriteLock简单介绍 - -`ReadWriteLock` 是一个接口,位于`java.util.concurrent.locks`包下,里面只有两个方法分别返回读锁和写锁: - -```java -public interface ReadWriteLock { - /** - * 返回读锁 - */ - Lock readLock(); - - /** - * 返回写锁 - */ - Lock writeLock(); -} -``` - -`ReentrantReadWriteLock` 是`ReadWriteLock`接口的具体实现类。 - -**读写锁还是比较适合缓存这种读多写少的场景。读写锁可以保证多个线程和同时读取,但是只有一个线程可以写入。** - -读写锁的特点是:写锁和写锁互斥,读锁和写锁互斥,读锁之间不互斥。也就说:同一时刻只能有一个线程写,但是可以有多个线程 -读。读写之间是互斥的,两者不能同时发生(当进行写操作时,同一时刻其他线程的读操作会被阻塞;当进行读操作时,同一时刻所有线程的写操作会被阻塞)。 - -另外,**同一个线程持有写锁时是可以申请读锁,但是持有读锁的情况下不可以申请写锁。** - -### 4. ScheduledExecutorService 简单介绍 - -`ScheduledExecutorService` 是一个接口,`ScheduledThreadPoolExecutor` 是其主要实现类。 - -![](./../../../media/pictures/java/my-lru-cache/ScheduledThreadPoolExecutor-diagram.png) - -**`ScheduledThreadPoolExecutor` 主要用来在给定的延迟后运行任务,或者定期执行任务。** 这个在实际项目用到的比较少,因为有其他方案选择比如`quartz`。但是,在一些需求比较简单的场景下还是非常有用的! - -**`ScheduledThreadPoolExecutor` 使用的任务队列 `DelayQueue` 封装了一个 `PriorityQueue`,`PriorityQueue` 会对队列中的任务进行排序,执行所需时间短的放在前面先被执行,如果执行所需时间相同则先提交的任务将被先执行。** - -### 5. 徒手撸一个线程安全的 LRU 缓存 - -#### 5.1. 实现方法 - - `ConcurrentHashMap` + `ConcurrentLinkedQueue` +`ReadWriteLock` - -#### 5.2. 原理 - -`ConcurrentHashMap` 是线程安全的Map,我们可以利用它缓存 key,value形式的数据。`ConcurrentLinkedQueue`是一个线程安全的基于链表的队列(先进先出),我们可以用它来维护 key 。每当我们put/get(缓存被使用)元素的时候,我们就将这个元素对应的 key 存放在队列尾部,这样就能保证队列头部的元素是最近最少使用的。当我们的缓存容量不够的时候,我们直接移除队列头部对应的key以及这个key对应的缓存即可! - -另外,我们用到了`ReadWriteLock`(读写锁)来保证线程安全。 - -#### 5.3. put方法具体流程分析 - -为了方便大家理解,我将代码中比较重要的 `put(key,value)`方法的原理图画了出来,如下图所示: - -![](./../../../media/pictures/java/my-lru-cache/MyLRUCachePut.png) - - - -#### 5.4. 源码 - -```java -/** - * @author shuang.kou - *

- * 使用 ConcurrentHashMap+ConcurrentLinkedQueue+ReadWriteLock实现线程安全的 LRU 缓存 - * 这里只是为了学习使用,本地缓存推荐使用 Guava 自带的,使用 Spring 的话,推荐使用Spring Cache - */ -public class MyLruCache { - - /** - * 缓存的最大容量 - */ - private final int maxCapacity; - - private ConcurrentHashMap cacheMap; - private ConcurrentLinkedQueue keys; - /** - * 读写锁 - */ - private ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); - private Lock writeLock = readWriteLock.writeLock(); - private Lock readLock = readWriteLock.readLock(); - - public MyLruCache(int maxCapacity) { - if (maxCapacity < 0) { - throw new IllegalArgumentException("Illegal max capacity: " + maxCapacity); - } - this.maxCapacity = maxCapacity; - cacheMap = new ConcurrentHashMap<>(maxCapacity); - keys = new ConcurrentLinkedQueue<>(); - } - - public V put(K key, V value) { - // 加写锁 - writeLock.lock(); - try { - //1.key是否存在于当前缓存 - if (cacheMap.containsKey(key)) { - moveToTailOfQueue(key); - cacheMap.put(key, value); - return value; - } - //2.是否超出缓存容量,超出的话就移除队列头部的元素以及其对应的缓存 - if (cacheMap.size() == maxCapacity) { - System.out.println("maxCapacity of cache reached"); - removeOldestKey(); - } - //3.key不存在于当前缓存。将key添加到队列的尾部并且缓存key及其对应的元素 - keys.add(key); - cacheMap.put(key, value); - return value; - } finally { - writeLock.unlock(); - } - } - - public V get(K key) { - //加读锁 - readLock.lock(); - try { - //key是否存在于当前缓存 - if (cacheMap.containsKey(key)) { - // 存在的话就将key移动到队列的尾部 - moveToTailOfQueue(key); - return cacheMap.get(key); - } - //不存在于当前缓存中就返回Null - return null; - } finally { - readLock.unlock(); - } - } - - public V remove(K key) { - writeLock.lock(); - try { - //key是否存在于当前缓存 - if (cacheMap.containsKey(key)) { - // 存在移除队列和Map中对应的Key - keys.remove(key); - return cacheMap.remove(key); - } - //不存在于当前缓存中就返回Null - return null; - } finally { - writeLock.unlock(); - } - } - - /** - * 将元素添加到队列的尾部(put/get的时候执行) - */ - private void moveToTailOfQueue(K key) { - keys.remove(key); - keys.add(key); - } - - /** - * 移除队列头部的元素以及其对应的缓存 (缓存容量已满的时候执行) - */ - private void removeOldestKey() { - K oldestKey = keys.poll(); - if (oldestKey != null) { - cacheMap.remove(oldestKey); - } - } - - public int size() { - return cacheMap.size(); - } - -} -``` - -**非并发环境测试:** - -```java -MyLruCache myLruCache = new MyLruCache<>(3); -myLruCache.put(1, "Java"); -System.out.println(myLruCache.get(1));// Java -myLruCache.remove(1); -System.out.println(myLruCache.get(1));// null -myLruCache.put(2, "C++"); -myLruCache.put(3, "Python"); -System.out.println(myLruCache.get(2));//C++ -myLruCache.put(4, "C"); -myLruCache.put(5, "PHP"); -System.out.println(myLruCache.get(2));// C++ -``` - -**并发环境测试:** - -我们初始化了一个固定容量为 10 的线程池和count为10的`CountDownLatch`。我们将1000000次操作分10次添加到线程池,然后我们等待线程池执行完成这10次操作。 - - -```java -int threadNum = 10; -int batchSize = 100000; -//init cache -MyLruCache myLruCache = new MyLruCache<>(batchSize * 10); -//init thread pool with 10 threads -ExecutorService fixedThreadPool = Executors.newFixedThreadPool(threadNum); -//init CountDownLatch with 10 count -CountDownLatch latch = new CountDownLatch(threadNum); -AtomicInteger atomicInteger = new AtomicInteger(0); -long startTime = System.currentTimeMillis(); -for (int t = 0; t < threadNum; t++) { - fixedThreadPool.submit(() -> { - for (int i = 0; i < batchSize; i++) { - int value = atomicInteger.incrementAndGet(); - myLruCache.put("id" + value, value); - } - latch.countDown(); - }); -} -//wait for 10 threads to complete the task -latch.await(); -fixedThreadPool.shutdown(); -System.out.println("Cache size:" + myLruCache.size());//Cache size:1000000 -long endTime = System.currentTimeMillis(); -long duration = endTime - startTime; -System.out.println(String.format("Time cost:%dms", duration));//Time cost:511ms -``` - -### 6. 实现一个线程安全并且带有过期时间的 LRU 缓存 - -实际上就是在我们上面时间的LRU缓存的基础上加上一个定时任务去删除缓存,单纯利用 JDK 提供的类,我们实现定时任务的方式有很多种: - -1. `Timer` :不被推荐,多线程会存在问题。 -2. `ScheduledExecutorService` :定时器线程池,可以用来替代 `Timer` -3. `DelayQueue` :延时队列 -4. `quartz` :一个很火的开源任务调度框架,很多其他框架都是基于 `quartz` 开发的,比如当当网的`elastic-job `就是基于`quartz`二次开发之后的分布式调度解决方案 -5. ...... - -最终我们选择了 `ScheduledExecutorService`,主要原因是它易用(基于`DelayQueue`做了很多封装)并且基本能满足我们的大部分需求。 - -我们在我们上面实现的线程安全的 LRU 缓存基础上,简单稍作修改即可!我们增加了一个方法: - -```java -private void removeAfterExpireTime(K key, long expireTime) { - scheduledExecutorService.schedule(() -> { - //过期后清除该键值对 - cacheMap.remove(key); - keys.remove(key); - }, expireTime, TimeUnit.MILLISECONDS); -} -``` -我们put元素的时候,如果通过这个方法就能直接设置过期时间。 - - -**完整源码如下:** - -```java -/** - * @author shuang.kou - *

- * 使用 ConcurrentHashMap+ConcurrentLinkedQueue+ReadWriteLock+ScheduledExecutorService实现线程安全的 LRU 缓存 - * 这里只是为了学习使用,本地缓存推荐使用 Guava 自带的,使用 Spring 的话,推荐使用Spring Cache - */ -public class MyLruCacheWithExpireTime { - - /** - * 缓存的最大容量 - */ - private final int maxCapacity; - - private ConcurrentHashMap cacheMap; - private ConcurrentLinkedQueue keys; - /** - * 读写锁 - */ - private ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); - private Lock writeLock = readWriteLock.writeLock(); - private Lock readLock = readWriteLock.readLock(); - - private ScheduledExecutorService scheduledExecutorService; - - public MyLruCacheWithExpireTime(int maxCapacity) { - if (maxCapacity < 0) { - throw new IllegalArgumentException("Illegal max capacity: " + maxCapacity); - } - this.maxCapacity = maxCapacity; - cacheMap = new ConcurrentHashMap<>(maxCapacity); - keys = new ConcurrentLinkedQueue<>(); - scheduledExecutorService = Executors.newScheduledThreadPool(3); - } - - public V put(K key, V value, long expireTime) { - // 加写锁 - writeLock.lock(); - try { - //1.key是否存在于当前缓存 - if (cacheMap.containsKey(key)) { - moveToTailOfQueue(key); - cacheMap.put(key, value); - return value; - } - //2.是否超出缓存容量,超出的话就移除队列头部的元素以及其对应的缓存 - if (cacheMap.size() == maxCapacity) { - System.out.println("maxCapacity of cache reached"); - removeOldestKey(); - } - //3.key不存在于当前缓存。将key添加到队列的尾部并且缓存key及其对应的元素 - keys.add(key); - cacheMap.put(key, value); - if (expireTime > 0) { - removeAfterExpireTime(key, expireTime); - } - return value; - } finally { - writeLock.unlock(); - } - } - - public V get(K key) { - //加读锁 - readLock.lock(); - try { - //key是否存在于当前缓存 - if (cacheMap.containsKey(key)) { - // 存在的话就将key移动到队列的尾部 - moveToTailOfQueue(key); - return cacheMap.get(key); - } - //不存在于当前缓存中就返回Null - return null; - } finally { - readLock.unlock(); - } - } - - public V remove(K key) { - writeLock.lock(); - try { - //key是否存在于当前缓存 - if (cacheMap.containsKey(key)) { - // 存在移除队列和Map中对应的Key - keys.remove(key); - return cacheMap.remove(key); - } - //不存在于当前缓存中就返回Null - return null; - } finally { - writeLock.unlock(); - } - } - - /** - * 将元素添加到队列的尾部(put/get的时候执行) - */ - private void moveToTailOfQueue(K key) { - keys.remove(key); - keys.add(key); - } - - /** - * 移除队列头部的元素以及其对应的缓存 (缓存容量已满的时候执行) - */ - private void removeOldestKey() { - K oldestKey = keys.poll(); - if (oldestKey != null) { - cacheMap.remove(oldestKey); - } - } - - private void removeAfterExpireTime(K key, long expireTime) { - scheduledExecutorService.schedule(() -> { - //过期后清除该键值对 - cacheMap.remove(key); - keys.remove(key); - }, expireTime, TimeUnit.MILLISECONDS); - } - - public int size() { - return cacheMap.size(); - } - -} - -``` - -**测试效果:** - -```java -MyLruCacheWithExpireTime myLruCache = new MyLruCacheWithExpireTime<>(3); -myLruCache.put(1,"Java",3000); -myLruCache.put(2,"C++",3000); -myLruCache.put(3,"Python",1500); -System.out.println(myLruCache.size());//3 -Thread.sleep(2000); -System.out.println(myLruCache.size());//2 -``` diff --git "a/docs/java/jvm/JDK\347\233\221\346\216\247\345\222\214\346\225\205\351\232\234\345\244\204\347\220\206\345\267\245\345\205\267\346\200\273\347\273\223.md" "b/docs/java/jvm/JDK\347\233\221\346\216\247\345\222\214\346\225\205\351\232\234\345\244\204\347\220\206\345\267\245\345\205\267\346\200\273\347\273\223.md" index d5cb29de7a9..c8263de02e2 100644 --- "a/docs/java/jvm/JDK\347\233\221\346\216\247\345\222\214\346\225\205\351\232\234\345\244\204\347\220\206\345\267\245\345\205\267\346\200\273\347\273\223.md" +++ "b/docs/java/jvm/JDK\347\233\221\346\216\247\345\222\214\346\225\205\351\232\234\345\244\204\347\220\206\345\267\245\345\205\267\346\200\273\347\273\223.md" @@ -1,5 +1,3 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 - - [JDK 监控和故障处理工具总结](#jdk-监控和故障处理工具总结) @@ -325,13 +323,3 @@ VisualVM 提供在 Java 虚拟机 (Java Virutal Machine, JVM) 上运行的 Java - - - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本公众号后台回复 **"Java面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) diff --git "a/docs/java/jvm/JVM\345\236\203\345\234\276\345\233\236\346\224\266.md" "b/docs/java/jvm/JVM\345\236\203\345\234\276\345\233\236\346\224\266.md" index 655266a7814..b979285a59d 100644 --- "a/docs/java/jvm/JVM\345\236\203\345\234\276\345\233\236\346\224\266.md" +++ "b/docs/java/jvm/JVM\345\236\203\345\234\276\345\233\236\346\224\266.md" @@ -1,39 +1,45 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 - + + + + - [JVM 垃圾回收](#jvm-垃圾回收) - - [写在前面](#写在前面) - - [本节常见面试题](#本节常见面试题) - - [本文导火索](#本文导火索) - - [1 揭开 JVM 内存分配与回收的神秘面纱](#1--揭开-jvm-内存分配与回收的神秘面纱) - - [1.1 对象优先在 eden 区分配](#11-对象优先在-eden-区分配) - - [1.2 大对象直接进入老年代](#12-大对象直接进入老年代) - - [1.3 长期存活的对象将进入老年代](#13-长期存活的对象将进入老年代) - - [1.4 动态对象年龄判定](#14-动态对象年龄判定) - - [2 对象已经死亡?](#2-对象已经死亡) - - [2.1 引用计数法](#21-引用计数法) - - [2.2 可达性分析算法](#22-可达性分析算法) - - [2.3 再谈引用](#23-再谈引用) - - [2.4 不可达的对象并非“非死不可”](#24-不可达的对象并非非死不可) - - [2.5 如何判断一个常量是废弃常量](#25-如何判断一个常量是废弃常量) - - [2.6 如何判断一个类是无用的类](#26-如何判断一个类是无用的类) - - [3 垃圾收集算法](#3-垃圾收集算法) - - [3.1 标记-清除算法](#31-标记-清除算法) - - [3.2 复制算法](#32-复制算法) - - [3.3 标记-整理算法](#33-标记-整理算法) - - [3.4 分代收集算法](#34-分代收集算法) - - [4 垃圾收集器](#4-垃圾收集器) - - [4.1 Serial 收集器](#41-serial-收集器) - - [4.2 ParNew 收集器](#42-parnew-收集器) - - [4.3 Parallel Scavenge 收集器](#43-parallel-scavenge-收集器) - - [4.4.Serial Old 收集器](#44serial-old-收集器) - - [4.5 Parallel Old 收集器](#45-parallel-old-收集器) - - [4.6 CMS 收集器](#46-cms-收集器) - - [4.7 G1 收集器](#47-g1-收集器) - - [参考](#参考) - - + - [写在前面](#写在前面) + - [本节常见面试题](#本节常见面试题) + - [本文导火索](#本文导火索) + - [1 揭开 JVM 内存分配与回收的神秘面纱](#1-揭开-jvm-内存分配与回收的神秘面纱) + - [1.1 对象优先在 eden 区分配](#11-对象优先在-eden-区分配) + - [1.2 大对象直接进入老年代](#12-大对象直接进入老年代) + - [1.3 长期存活的对象将进入老年代](#13-长期存活的对象将进入老年代) + - [1.4 动态对象年龄判定](#14-动态对象年龄判定) + - [1.5 主要进行 gc 的区域](#15-主要进行-gc-的区域) + - [2 对象已经死亡?](#2-对象已经死亡) + - [2.1 引用计数法](#21-引用计数法) + - [2.2 可达性分析算法](#22-可达性分析算法) + - [2.3 再谈引用](#23-再谈引用) + - [2.4 不可达的对象并非“非死不可”](#24-不可达的对象并非非死不可) + - [2.5 如何判断一个常量是废弃常量?](#25-如何判断一个常量是废弃常量) + - [2.6 如何判断一个类是无用的类](#26-如何判断一个类是无用的类) + - [3 垃圾收集算法](#3-垃圾收集算法) + - [3.1 标记-清除算法](#31-标记-清除算法) + - [3.2 复制算法](#32-复制算法) + - [3.3 标记-整理算法](#33-标记-整理算法) + - [3.4 分代收集算法](#34-分代收集算法) + - [4 垃圾收集器](#4-垃圾收集器) + - [4.1 Serial 收集器](#41-serial-收集器) + - [4.2 ParNew 收集器](#42-parnew-收集器) + - [4.3 Parallel Scavenge 收集器](#43-parallel-scavenge-收集器) + - [4.4.Serial Old 收集器](#44serial-old-收集器) + - [4.5 Parallel Old 收集器](#45-parallel-old-收集器) + - [4.6 CMS 收集器](#46-cms-收集器) + - [4.7 G1 收集器](#47-g1-收集器) + - [4.8 ZGC 收集器](#48-zgc-收集器) + - [参考](#参考) + + + + # JVM 垃圾回收 ## 写在前面 @@ -58,7 +64,7 @@ 当需要排查各种内存溢出问题、当垃圾收集成为系统达到更高并发的瓶颈时,我们就需要对这些“自动化”的技术实施必要的监控和调节。 -## 1 揭开 JVM 内存分配与回收的神秘面纱 +## 1 揭开 JVM 内存分配与回收的神秘面纱 Java 的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时,Java 自动内存管理最核心的功能是 **堆** 内存中对象的分配与回收。 @@ -70,9 +76,9 @@ Java 堆是垃圾收集器管理的主要区域,因此也被称作**GC 堆(G 上图所示的 Eden 区、From Survivor0("From") 区、To Survivor1("To") 区都属于新生代,Old Memory 区属于老年代。 -大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。 +大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。 -> 修正([issue552](https://github.com/Snailclimb/JavaGuide/issues/552)):“Hotspot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值”。 +> 修正([issue552](https://github.com/Snailclimb/JavaGuide/issues/552)):“Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor 区的一半时,取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值”。 > > **动态年龄计算的代码如下** > @@ -90,12 +96,10 @@ Java 堆是垃圾收集器管理的主要区域,因此也被称作**GC 堆(G > uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold; > ... > } -> -> ``` > -> +> ``` -经过这次GC后,Eden区和"From"区已经被清空。这个时候,"From"和"To"会交换他们的角色,也就是新的"To"就是上次GC前的“From”,新的"From"就是上次GC前的"To"。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,"To"区被填满之后,会将所有对象移动到老年代中。 +经过这次 GC 后,Eden 区和"From"区已经被清空。这个时候,"From"和"To"会交换他们的角色,也就是新的"To"就是上次 GC 前的“From”,新的"From"就是上次 GC 前的"To"。不管怎样,都会保证名为 To 的 Survivor 区域是空的。Minor GC 会一直重复这样的过程,直到“To”区被填满,"To"区被填满之后,会将所有对象移动到老年代中。 ![堆内存常见分配策略 ](./pictures/jvm垃圾回收/堆内存.png) @@ -105,13 +109,6 @@ Java 堆是垃圾收集器管理的主要区域,因此也被称作**GC 堆(G 大多数情况下,对象在新生代中 eden 区分配。当 eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC.下面我们来进行实际测试以下。 -在测试之前我们先来看看 **Minor GC 和 Full GC 有什么不同呢?** - -- **新生代 GC(Minor GC)**:指发生新生代的的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快。 -- **老年代 GC(Major GC/Full GC)**:指发生在老年代的 GC,出现了 Major GC 经常会伴随至少一次的 Minor GC(并非绝对),Major GC 的速度一般会比 Minor GC 的慢 10 倍以上。 - -> [issue#664 ](https://github.com/Snailclimb/JavaGuide/issues/664) :**[guang19](https://github.com/guang19)** 补充:个人在网上查阅相关资料的时候发现如题所说的观点。有的文章说 Major GC 与 Full GC 一样是属于对老年代的GC,也有的文章说 Major GC 是对整个堆区的GC,所以这点需要各位同学自行分辨 Major GC 语义。见: [知乎讨论](https://www.zhihu.com/question/41922036) - **测试:** ```java @@ -124,6 +121,7 @@ public class GCTest { } } ``` + 通过以下方式运行: ![](./pictures/jvm垃圾回收/25178350.png) @@ -139,6 +137,7 @@ public class GCTest { ```java allocation2 = new byte[900*1024]; ``` + ![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-26/28128785.jpg) **简单解释一下为什么会出现这种情况:** 因为给 allocation2 分配内存的时候 eden 区内存几乎已经被分配完了,我们刚刚讲了当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC.GC 期间虚拟机又发现 allocation1 无法存入 Survivor 空间,所以只好通过 **分配担保机制** 把新生代的对象提前转移到老年代中去,老年代上的空间足够存放 allocation1,所以不会出现 Full GC。执行 Minor GC 后,后面分配的对象如果能够存在 eden 区的话,还是会在 eden 区分配内存。可以执行如下代码验证: @@ -158,8 +157,8 @@ public class GCTest { ``` - ### 1.2 大对象直接进入老年代 + 大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。 **为什么要这样呢?** @@ -167,16 +166,16 @@ public class GCTest { 为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。 ### 1.3 长期存活的对象将进入老年代 + 既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。 如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1.对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。 ### 1.4 动态对象年龄判定 +大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。 -大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。 - -> 修正([issue552](https://github.com/Snailclimb/JavaGuide/issues/552)):“Hotspot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值”。 +> 修正([issue552](https://github.com/Snailclimb/JavaGuide/issues/552)):“Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor 区的一半时,取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值”。 > > **动态年龄计算的代码如下** > @@ -194,14 +193,35 @@ public class GCTest { > uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold; > ... > } -> +> > ``` > -> 额外补充说明([issue672](https://github.com/Snailclimb/JavaGuide/issues/672)):**关于默认的晋升年龄是15,这个说法的来源大部分都是《深入理解Java虚拟机》这本书。** -> 如果你去Oracle的官网阅读[相关的虚拟机参数](https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html),你会发现`-XX:MaxTenuringThreshold=threshold`这里有个说明 +> 额外补充说明([issue672](https://github.com/Snailclimb/JavaGuide/issues/672)):**关于默认的晋升年龄是 15,这个说法的来源大部分都是《深入理解 Java 虚拟机》这本书。** +> 如果你去 Oracle 的官网阅读[相关的虚拟机参数](https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html),你会发现`-XX:MaxTenuringThreshold=threshold`这里有个说明 > -> **Sets the maximum tenuring threshold for use in adaptive GC sizing. The largest value is 15. The default value is 15 for the parallel (throughput) collector, and 6 for the CMS collector.默认晋升年龄并不都是15,这个是要区分垃圾收集器的,CMS就是6.** +> **Sets the maximum tenuring threshold for use in adaptive GC sizing. The largest value is 15. The default value is 15 for the parallel (throughput) collector, and 6 for the CMS collector.默认晋升年龄并不都是 15,这个是要区分垃圾收集器的,CMS 就是 6.** + +### 1.5 主要进行 gc 的区域 + +周志明先生在《深入理解 Java 虚拟机》第二版中 P92 如是写道: +> ~~_“老年代 GC(Major GC/Full GC),指发生在老年代的 GC……”_~~ + +上面的说法已经在《深入理解 Java 虚拟机》第三版中被改正过来了。感谢 R 大的回答: + +![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2020-8/b48228c2-ac00-4668-a78f-6f221f8563b5.png) + +**总结:** + +针对 HotSpot VM 的实现,它里面的 GC 其实准确分类只有两大种: + +部分收集 (Partial GC): + +- 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集; +- 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集; +- 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。 + +整堆收集 (Full GC):收集整个 Java 堆和方法区。 ## 2 对象已经死亡? @@ -230,19 +250,18 @@ public class ReferenceCountingGc { } ``` - - ### 2.2 可达性分析算法 这个算法的基本思想就是通过一系列的称为 **“GC Roots”** 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。 ![可达性分析算法 ](./pictures/jvm垃圾回收/72762049.png) -可作为GC Roots的对象包括下面几种: -* 虚拟机栈(栈帧中的本地变量表)中引用的对象 -* 本地方法栈(Native方法)中引用的对象 -* 方法区中类静态属性引用的对象 -* 方法区中常量引用的对象 +可作为 GC Roots 的对象包括下面几种: + +- 虚拟机栈(栈帧中的本地变量表)中引用的对象 +- 本地方法栈(Native 方法)中引用的对象 +- 方法区中类静态属性引用的对象 +- 方法区中常量引用的对象 ### 2.3 再谈引用 @@ -264,7 +283,7 @@ JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引 **3.弱引用(WeakReference)** -如果一个对象只具有弱引用,那就类似于**可有可无的生活用品**。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。 +如果一个对象只具有弱引用,那就类似于**可有可无的生活用品**。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。 @@ -274,7 +293,7 @@ JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引 **虚引用主要用来跟踪对象被垃圾回收的活动**。 -**虚引用与软引用和弱引用的一个区别在于:** 虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 +**虚引用与软引用和弱引用的一个区别在于:** 虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为**软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生**。 @@ -288,7 +307,15 @@ JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引 运行时常量池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢? -假如在常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 "abc" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,"abc" 就会被系统清理出常量池。 +~~**JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。**~~ + +> 修正([issue747](https://github.com/Snailclimb/JavaGuide/issues/747),[reference](https://blog.csdn.net/q5706503/article/details/84640762)): +> +> 1. **JDK1.7 之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时 hotspot 虚拟机对方法区的实现为永久代** +> 2. **JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是 hotspot 中的永久代** 。 +> 3. **JDK1.8 hotspot 移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)** + +假如在字符串常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 "abc" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,"abc" 就会被系统清理出常量池了。 ### 2.6 如何判断一个类是无用的类 @@ -297,12 +324,11 @@ JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引 判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 **“无用的类”** : - 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。 -- 加载该类的 ClassLoader 已经被回收。 -- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 +- 加载该类的 `ClassLoader` 已经被回收。 +- 该类对应的 `java.lang.Class` 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。 - ## 3 垃圾收集算法 ![垃圾收集算法分类](./pictures/jvm垃圾回收/垃圾收集算法.png) @@ -320,7 +346,7 @@ JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引 为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。 -公众号 +![复制算法](./pictures/jvm垃圾回收/90984624.png) ### 3.3 标记-整理算法 @@ -346,11 +372,12 @@ JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引 虽然我们对各个收集器进行比较,但并非要挑选出一个最好的收集器。因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,**我们能做的就是根据具体应用场景选择适合自己的垃圾收集器**。试想一下:如果有一种四海之内、任何场景下都适用的完美收集器存在,那么我们的 HotSpot 虚拟机就不会实现那么多不同的垃圾收集器了。 - ### 4.1 Serial 收集器 -Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 **“单线程”** 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( **"Stop The World"** ),直到它收集结束。 - **新生代采用复制算法,老年代采用标记-整理算法。** +Serial(串行)收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 **“单线程”** 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( **"Stop The World"** ),直到它收集结束。 + +**新生代采用复制算法,老年代采用标记-整理算法。** + ![ Serial 收集器 ](./pictures/jvm垃圾回收/46873026.png) 虚拟机的设计者们当然知道 Stop The World 带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。 @@ -358,9 +385,11 @@ Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收 但是 Serial 收集器有没有优于其他垃圾收集器的地方呢?当然有,它**简单而高效(与其他收集器的单线程相比)**。Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。 ### 4.2 ParNew 收集器 + **ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。** - **新生代采用复制算法,老年代采用标记-整理算法。** +**新生代采用复制算法,老年代采用标记-整理算法。** + ![ParNew 收集器 ](./pictures/jvm垃圾回收/22018368.png) 它是许多运行在 Server 模式下的虚拟机的首要选择,除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。 @@ -371,13 +400,12 @@ Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收 - **并发(Concurrent)**:指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 CPU 上。 - ### 4.3 Parallel Scavenge 收集器 -Parallel Scavenge 收集器也是使用复制算法的多线程收集器,它看上去几乎和ParNew都一样。 **那么它有什么特别之处呢?** +Parallel Scavenge 收集器也是使用复制算法的多线程收集器,它看上去几乎和 ParNew 都一样。 **那么它有什么特别之处呢?** ``` --XX:+UseParallelGC +-XX:+UseParallelGC 使用 Parallel 收集器+ 老年代串行 @@ -387,17 +415,32 @@ Parallel Scavenge 收集器也是使用复制算法的多线程收集器,它 ``` -**Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。** Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,手工优化存在困难的话可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择。 +**Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。** Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 Parallel Scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。 + +**新生代采用复制算法,老年代采用标记-整理算法。** - **新生代采用复制算法,老年代采用标记-整理算法。** ![Parallel Scavenge 收集器 ](./pictures/jvm垃圾回收/parllel-scavenge收集器.png) +**这是 JDK1.8 默认收集器** + +使用 java -XX:+PrintCommandLineFlags -version 命令查看 + +``` +-XX:InitialHeapSize=262921408 -XX:MaxHeapSize=4206742528 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC +java version "1.8.0_211" +Java(TM) SE Runtime Environment (build 1.8.0_211-b12) +Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode) +``` + +JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old,如果指定了-XX:+UseParallelGC 参数,则默认指定了-XX:+UseParallelOldGC,可以使用-XX:-UseParallelOldGC 来禁用该功能 ### 4.4.Serial Old 收集器 + **Serial 收集器的老年代版本**,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。 ### 4.5 Parallel Old 收集器 - **Parallel Scavenge 收集器的老年代版本**。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。 + +**Parallel Scavenge 收集器的老年代版本**。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。 ### 4.6 CMS 收集器 @@ -422,7 +465,6 @@ Parallel Scavenge 收集器也是使用复制算法的多线程收集器,它 ### 4.7 G1 收集器 - **G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.** 被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。它具备一下特点: @@ -432,7 +474,6 @@ Parallel Scavenge 收集器也是使用复制算法的多线程收集器,它 - **空间整合**:与 CMS 的“标记--清理”算法不同,G1 从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。 - **可预测的停顿**:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。 - G1 收集器的运作大致分为以下几个步骤: - **初始标记** @@ -440,32 +481,18 @@ G1 收集器的运作大致分为以下几个步骤: - **最终标记** - **筛选回收** - **G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来)**。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。 -## 参考 - -- 《深入理解 Java 虚拟机:JVM 高级特性与最佳实践(第二版》 -- https://my.oschina.net/hosee/blog/644618 -- - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源[公众号](#公众号)后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) - - - - - - +### 4.8 ZGC 收集器 +与 CMS 中的 ParNew 和 G1 类似,ZGC 也采用标记-复制算法,不过 ZGC 对该算法做了重大改进。 +在 ZGC 中出现 Stop The World 的情况会更少! +详情可以看 : [《新一代垃圾回收器 ZGC 的探索与实践》](https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meituan.html) +## 参考 +- 《深入理解 Java 虚拟机:JVM 高级特性与最佳实践(第二版》 +- https://my.oschina.net/hosee/blog/644618 +- diff --git "a/docs/java/jvm/Java\345\206\205\345\255\230\345\214\272\345\237\237.md" "b/docs/java/jvm/Java\345\206\205\345\255\230\345\214\272\345\237\237.md" index 0cae9f0a7f8..1909ad3bbf4 100644 --- "a/docs/java/jvm/Java\345\206\205\345\255\230\345\214\272\345\237\237.md" +++ "b/docs/java/jvm/Java\345\206\205\345\255\230\345\214\272\345\237\237.md" @@ -1,5 +1,3 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 - - [Java 内存区域详解](#java-内存区域详解) @@ -125,7 +123,7 @@ Java 方法有两种返回方式: 本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。 -方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种错误。 +方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 `StackOverFlowError` 和 `OutOfMemoryError` 两种错误。 ### 2.4 堆 @@ -306,14 +304,16 @@ JDK1.4 中新加入的 **NIO(New Input/Output) 类**,引入了一种基于** ### 3.3 对象的访问定位 建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有**①使用句柄**和**②直接指针**两种: -1. **句柄:** 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息; +1. **句柄:** 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息; - ![对象的访问定位-使用句柄](./pictures/java内存区域/对象的访问定位-使用句柄.png) + ![对象的访问定位-使用句柄](./pictures/java内存区域/对象的访问定位-使用句柄.png) 2. **直接指针:** 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。 ![对象的访问定位-直接指针](./pictures/java内存区域/对象的访问定位-直接指针.png) + + **这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。** @@ -413,7 +413,7 @@ private static class CharacterCache { } ``` -两种浮点数类型的包装类 Float,Double 并没有实现常量池技术。** +两种浮点数类型的包装类 Float,Double 并没有实现常量池技术。 ```java Integer i1 = 33; @@ -492,13 +492,3 @@ i4=i5+i6 true - - - 深入解析String#intern - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源[公众号](#公众号)后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) diff --git "a/docs/java/jvm/[\345\212\240\351\244\220]\345\244\247\347\231\275\350\257\235\345\270\246\344\275\240\350\256\244\350\257\206JVM.md" "b/docs/java/jvm/[\345\212\240\351\244\220]\345\244\247\347\231\275\350\257\235\345\270\246\344\275\240\350\256\244\350\257\206JVM.md" index 91c1da9a7cb..db8ee340ca0 100644 --- "a/docs/java/jvm/[\345\212\240\351\244\220]\345\244\247\347\231\275\350\257\235\345\270\246\344\275\240\350\256\244\350\257\206JVM.md" +++ "b/docs/java/jvm/[\345\212\240\351\244\220]\345\244\247\347\231\275\350\257\235\345\270\246\344\275\240\350\256\244\350\257\206JVM.md" @@ -8,7 +8,7 @@ JVM 是 Java Virtual Machine 的缩写,它是一个虚构出来的计算机,一种规范。通过在实际的计算机上仿真模拟各类计算机功能实现··· -好,其实抛开这么专业的句子不说,就知道JVM其实就类似于一台小电脑运行在windows或者linux这些操作系统环境下即可。它直接和操作系统进行交互,与硬件不直接交互,可操作系统可以帮我们完成和硬件进行交互的工作。 +好,其实抛开这么专业的句子不说,就知道JVM其实就类似于一台小电脑运行在windows或者linux这些操作系统环境下即可。它直接和操作系统进行交互,与硬件不直接交互,而操作系统可以帮我们完成和硬件进行交互的工作。 ![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/d947f91e44c44c6c80222b49c2dee859-new-image19a36451-d673-486e-9c8e-3c7d8ab66929.png) ### 1.1 Java文件是如何被运行的 @@ -101,10 +101,10 @@ GC将无用对象从内存中卸载 加载一个Class类的顺序也是有优先级的,类加载器从最底层开始往上的顺序是这样的 1. BootStrap ClassLoader:rt.jar -2. Extention ClassLoader: 加载扩展的jar包 +2. Extension ClassLoader: 加载扩展的jar包 3. App ClassLoader:指定的classpath下面的jar包 4. Custom ClassLoader:自定义的类加载器 - + ### 2.3 双亲委派机制 当一个类收到了加载请求时,它是不会先自己去尝试加载的,而是委派给父类去完成,比如我现在要new一个Person,这个Person是我们自定义的类,如果我们要加载它,就会先委派App ClassLoader,只有当父类加载器都反馈自己无法完成这个请求(也就是父类加载器都没有找到加载所需的Class)时,子类加载器才会自行尝试加载 @@ -131,7 +131,7 @@ GC将无用对象从内存中卸载 ### 3.2 方法区 -方法区主要的作用技术存放类的元数据信息,常量和静态变量···等。当它存储的信息过大时,会在无法满足内存分配时报错。 +方法区主要的作用是存放类的元数据信息,常量和静态变量···等。当它存储的信息过大时,会在无法满足内存分配时报错。 ### 3.3 虚拟机栈和虚拟机堆 @@ -190,7 +190,7 @@ JVM内存会划分为堆内存和非堆内存,堆内存中也会划分为**年 当Eden空间满了之后,会触发一个叫做Minor GC(就是一个发生在年轻代的GC)的操作,存活下来的对象移动到Survivor0区。Survivor0区满后触发 Minor GC,就会将存活对象移动到Survivor1区,此时还会把from和to两个指针交换,这样保证了一段时间内总有一个survivor区为空且to所指向的survivor区为空。经过多次的 Minor GC后仍然存活的对象(**这里的存活判断是15次,对应到虚拟机参数为 -XX:MaxTenuringThreshold 。为什么是15,因为HotSpot会在对象投中的标记字段里记录年龄,分配到的空间仅有4位,所以最多只能记录到15**)会移动到老年代。老年代是存储长期存活的对象的,占满时就会触发我们最常听说的Full GC,期间会停止所有线程等待GC的完成。所以对于响应要求高的应用应该尽量去减少发生Full GC从而避免响应超时的问题。 -而且当老年区执行了full gc之后仍然无法进行对象保存的操作,就会产生OOM,这时候就是虚拟机中的堆内存不足,原因可能会是堆内存设置的大小过小,这个可以通过参数-Xms、-Xms来调整。也可能是代码中创建的对象大且多,而且它们一直在被引用从而长时间垃圾收集无法收集它们。 +而且当老年区执行了full gc之后仍然无法进行对象保存的操作,就会产生OOM,这时候就是虚拟机中的堆内存不足,原因可能会是堆内存设置的大小过小,这个可以通过参数-Xms、-Xmx来调整。也可能是代码中创建的对象大且多,而且它们一直在被引用从而长时间垃圾收集无法收集它们。 ![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/c02ecba3c33f43429a765987b928e423-new-image93b46f3d-33f9-46f9-a825-ec7129b004f6.png) @@ -297,7 +297,7 @@ JVM的参数非常之多,这里只列举比较重要的几个,通过各种 | -XX:MaxNewSize | 年轻代最大值(for 1.3/1.4) | | | -XX:PermSize | 设置持久代(perm gen)初始值 | 物理内存的1/64 | | -XX:MaxPermSize | 设置持久代最大值 | 物理内存的1/4 | -| -Xss | 每个线程的堆栈大小 | | JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K.更具应用的线程所需内存大小进行 调整.在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右一般小的应用, 如果栈不是很深, 应该是128k够用的 大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。(校长)和threadstacksize选项解释很类似,官方文档似乎没有解释,在论坛中有这样一句话:-Xss is translated in a VM flag named ThreadStackSize”一般设置这个值就可以了 +| -Xss | 每个线程的堆栈大小 | | JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K.根据应用的线程所需内存大小进行 调整.在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右一般小的应用, 如果栈不是很深, 应该是128k够用的 大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。(校长)和threadstacksize选项解释很类似,官方文档似乎没有解释,在论坛中有这样一句话:-Xss is translated in a VM flag named ThreadStackSize”一般设置这个值就可以了 | -XX:NewRatio | 年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代) | |-XX:NewRatio=4表示年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。 | -XX:SurvivorRatio | Eden区与Survivor区的大小比值 | |设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10 | -XX:+DisableExplicitGC | 关闭System.gc() | |这个参数需要严格的测试 @@ -319,7 +319,7 @@ JVM的参数非常之多,这里只列举比较重要的几个,通过各种 默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制.,默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制。简单点来说,你不停地往堆内存里面丢数据,等它剩余大小小于40%了,JVM就会动态申请内存空间不过会小于-Xmx,如果剩余大小大于70%,又会动态缩小不过不会小于–Xms。就这么简单 -开发过程中,通常会将 -Xms 与 -Xmx两个参数的配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小而浪费资源。 +开发过程中,通常会将 -Xms 与 -Xmx两个参数配置成相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小而浪费资源。 我们执行下面的代码 diff --git "a/docs/java/jvm/pictures/java\345\206\205\345\255\230\345\214\272\345\237\237/\345\257\271\350\261\241\347\232\204\350\256\277\351\227\256\345\256\232\344\275\215-\344\275\277\347\224\250\345\217\245\346\237\204.png" "b/docs/java/jvm/pictures/java\345\206\205\345\255\230\345\214\272\345\237\237/\345\257\271\350\261\241\347\232\204\350\256\277\351\227\256\345\256\232\344\275\215-\344\275\277\347\224\250\345\217\245\346\237\204.png" new file mode 100644 index 00000000000..88b494732af Binary files /dev/null and "b/docs/java/jvm/pictures/java\345\206\205\345\255\230\345\214\272\345\237\237/\345\257\271\350\261\241\347\232\204\350\256\277\351\227\256\345\256\232\344\275\215-\344\275\277\347\224\250\345\217\245\346\237\204.png" differ diff --git "a/docs/java/jvm/pictures/java\345\206\205\345\255\230\345\214\272\345\237\237/\345\257\271\350\261\241\347\232\204\350\256\277\351\227\256\345\256\232\344\275\215-\347\233\264\346\216\245\346\214\207\351\222\210.png" "b/docs/java/jvm/pictures/java\345\206\205\345\255\230\345\214\272\345\237\237/\345\257\271\350\261\241\347\232\204\350\256\277\351\227\256\345\256\232\344\275\215-\347\233\264\346\216\245\346\214\207\351\222\210.png" index 145bd405c21..f954d8a7375 100644 Binary files "a/docs/java/jvm/pictures/java\345\206\205\345\255\230\345\214\272\345\237\237/\345\257\271\350\261\241\347\232\204\350\256\277\351\227\256\345\256\232\344\275\215-\347\233\264\346\216\245\346\214\207\351\222\210.png" and "b/docs/java/jvm/pictures/java\345\206\205\345\255\230\345\214\272\345\237\237/\345\257\271\350\261\241\347\232\204\350\256\277\351\227\256\345\256\232\344\275\215-\347\233\264\346\216\245\346\214\207\351\222\210.png" differ diff --git "a/docs/java/jvm/\347\261\273\345\212\240\350\275\275\345\231\250.md" "b/docs/java/jvm/\347\261\273\345\212\240\350\275\275\345\231\250.md" index 1d0a826f152..37394a92444 100644 --- "a/docs/java/jvm/\347\261\273\345\212\240\350\275\275\345\231\250.md" +++ "b/docs/java/jvm/\347\261\273\345\212\240\350\275\275\345\231\250.md" @@ -1,5 +1,3 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 - - [回顾一下类加载过程](#回顾一下类加载过程) @@ -134,13 +132,5 @@ protected Class loadClass(String name, boolean resolve) - - -### 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源[公众号](#公众号)后台回复关键字 **“1”** 即可免费无套路获取。 -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) diff --git "a/docs/java/jvm/\347\261\273\345\212\240\350\275\275\350\277\207\347\250\213.md" "b/docs/java/jvm/\347\261\273\345\212\240\350\275\275\350\277\207\347\250\213.md" index 9330c58166a..41ee988043a 100644 --- "a/docs/java/jvm/\347\261\273\345\212\240\350\275\275\350\277\207\347\250\213.md" +++ "b/docs/java/jvm/\347\261\273\345\212\240\350\275\275\350\277\207\347\250\213.md" @@ -1,7 +1,3 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 - -> 公众号JavaGuide 后台回复关键字“1”,免费获取JavaGuide配套的Java工程师必备学习资源(文末有公众号二维码)。 - - [类的生命周期](#类的生命周期) @@ -39,7 +35,7 @@ Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚 2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构 3. 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口 -虚拟机规范多上面这3点并不具体,因此是非常灵活的。比如:"通过全类名获取定义此类的二进制字节流" 并没有指明具体从哪里获取、怎样获取。比如:比较常见的就是从 ZIP 包中读取(日后出现的JAR、EAR、WAR格式的基础)、其他文件生成(典型应用就是JSP)等等。 +虚拟机规范上面这3点并不具体,因此是非常灵活的。比如:"通过全类名获取定义此类的二进制字节流" 并没有指明具体从哪里获取、怎样获取。比如:比较常见的就是从 ZIP 包中读取(日后出现的JAR、EAR、WAR格式的基础)、其他文件生成(典型应用就是JSP)等等。 **一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的 `loadClass()` 方法)。数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。** @@ -72,7 +68,7 @@ Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚 ### 初始化 -初始化是类加载的最后一步,也是真正执行类中定义的 Java 程序代码(字节码),初始化阶段是执行类构造器 ` ()`方法的过程。 +初始化是类加载的最后一步,也是真正执行类中定义的 Java 程序代码(字节码),初始化阶段是执行初始化方法 ` ()`方法的过程。 对于`()` 方法的调用,虚拟机会自己确保其在多线程环境中的安全性。因为 `()` 方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起死锁,并且这种死锁很难被发现。 @@ -104,7 +100,7 @@ Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚 所以,在JVM生命周期类,由jvm自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。 -只要想通一点就好了,jdk自带的BootstrapClassLoader,PlatformClassLoader,AppClassLoader负责加载jdk提供的类,所以它们(类加载器的实例)肯定不会被回收。而我们自定义的类加载器的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。 +只要想通一点就好了,jdk自带的BootstrapClassLoader,ExtClassLoader,AppClassLoader负责加载jdk提供的类,所以它们(类加载器的实例)肯定不会被回收。而我们自定义的类加载器的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。 **参考** diff --git "a/docs/java/jvm/\347\261\273\346\226\207\344\273\266\347\273\223\346\236\204.md" "b/docs/java/jvm/\347\261\273\346\226\207\344\273\266\347\273\223\346\236\204.md" index fe282352150..852b4c4dd8f 100644 --- "a/docs/java/jvm/\347\261\273\346\226\207\344\273\266\347\273\223\346\236\204.md" +++ "b/docs/java/jvm/\347\261\273\346\226\207\344\273\266\347\273\223\346\236\204.md" @@ -46,7 +46,7 @@ ClassFile { u2 interfaces_count;//接口 u2 interfaces[interfaces_count];//一个类可以实现多个接口 u2 fields_count;//Class 文件的字段属性 - field_info fields[fields_count];//一个类会可以有个字段 + field_info fields[fields_count];//一个类会可以有多个字段 u2 methods_count;//Class 文件的方法数量 method_info methods[methods_count];//一个类可以有个多个方法 u2 attributes_count;//此类的属性表中的属性数 @@ -149,7 +149,7 @@ public class Employee { **类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,由于 Java 语言的单继承,所以父类索引只有一个,除了 `java.lang.Object` 之外,所有的 java 类都有父类,因此除了 `java.lang.Object` 外,所有 Java 类的父类索引都不为 0。** -**接口索引集合用来描述这个类实现了那些接口,这些被实现的接口将按`implents`(如果这个类本身是接口的话则是`extends`) 后的接口顺序从左到右排列在接口索引集合中。** +**接口索引集合用来描述这个类实现了那些接口,这些被实现的接口将按 `implements` (如果这个类本身是接口的话则是`extends`) 后的接口顺序从左到右排列在接口索引集合中。** ### 2.6 字段表集合 @@ -172,9 +172,9 @@ public class Employee { 上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。而字段叫什么名字、字段被定义为什么数据类型这些都是无法固定的,只能引用常量池中常量来描述。 -**字段的 access_flags 的取值:** +**字段的 access_flag 的取值:** -![字段的access_flags的取值](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/字段的access_flags的取值.png) +![字段的 access_flag 的取值](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/JVM/image-20201031084342859.png) ### 2.7 方法表集合 @@ -183,7 +183,7 @@ public class Employee { method_info methods[methods_count];//一个类可以有个多个方法 ``` -methods_count 表示方法的数量,而 method_info 表示的方法表。 +methods_count 表示方法的数量,而 method_info 表示方法表。 Class 文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式。方法表的结构如同字段表一样,依次包括了访问标志、名称索引、描述符索引、属性表集合几项。 @@ -193,7 +193,7 @@ Class 文件存储格式中对方法的描述与对字段的描述几乎采用 **方法表的 access_flag 取值:** -![方法表的 access_flag 取值](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/方法表的access_flag的所有标志位.png) +![方法表的 access_flag 取值](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/JVM/image-20201031084248965.png) 注意:因为`volatile`修饰符和`transient`修饰符不可以修饰方法,所以方法表的访问标志中没有这两个对应的标志,但是增加了`synchronized`、`native`、`abstract`等关键字修饰方法,所以也就多了这些关键字对应的标志。 @@ -212,13 +212,3 @@ Class 文件存储格式中对方法的描述与对字段的描述几乎采用 - - - 《实战 Java 虚拟机》 - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源[公众号](#公众号)后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) diff --git a/docs/java/Multithread/JavaConcurrencyBasicsCommonInterviewQuestionsSummary.md "b/docs/java/multi-thread/2020\346\234\200\346\226\260Java\345\271\266\345\217\221\345\237\272\347\241\200\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" similarity index 93% rename from docs/java/Multithread/JavaConcurrencyBasicsCommonInterviewQuestionsSummary.md rename to "docs/java/multi-thread/2020\346\234\200\346\226\260Java\345\271\266\345\217\221\345\237\272\347\241\200\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" index c987d1497d1..76a3a9768fa 100644 --- a/docs/java/Multithread/JavaConcurrencyBasicsCommonInterviewQuestionsSummary.md +++ "b/docs/java/multi-thread/2020\346\234\200\346\226\260Java\345\271\266\345\217\221\345\237\272\347\241\200\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" @@ -84,7 +84,7 @@ public class MultiThread { 从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的**堆**和**方法区 (JDK1.8 之后的元空间)**资源,但是每个线程有自己的**程序计数器**、**虚拟机栈** 和 **本地方法栈**。 -**总结:** 线程 是 进程 划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反 +**总结:** **线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。** 下面是该知识点的扩展内容! @@ -279,18 +279,18 @@ Process finished with exit code 0 ## 9. 说说 sleep() 方法和 wait() 方法区别和共同点? -- 两者最主要的区别在于:**sleep 方法没有释放锁,而 wait 方法释放了锁** 。 +- 两者最主要的区别在于:**`sleep()` 方法没有释放锁,而 `wait()` 方法释放了锁** 。 - 两者都可以暂停线程的执行。 -- Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。 -- wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒。 +- `wait()` 通常被用于线程间交互/通信,`sleep() `通常被用于暂停执行。 +- `wait()` 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 `notify() `或者 `notifyAll()` 方法。`sleep() `方法执行完成后,线程会自动苏醒。或者可以使用 `wait(long timeout)` 超时后线程会自动苏醒。 ## 10. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法? 这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来! -new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。 +new 一个 Thread,线程进入了新建状态。调用 `start()`方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 `start()` 会执行线程的相应准备工作,然后自动执行 ` run() ` 方法的内容,这是真正的多线程工作。 但是,直接执行 `run()` 方法,会把 `run()` 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。 -**总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。** +**总结: 调用 `start()` 方法方可启动线程并使线程进入就绪状态,直接执行 `run()` 方法的话不会以多线程的方式执行。** ## 公众号 diff --git "a/docs/java/multi-thread/2020\346\234\200\346\226\260Java\345\271\266\345\217\221\350\277\233\351\230\266\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" "b/docs/java/multi-thread/2020\346\234\200\346\226\260Java\345\271\266\345\217\221\350\277\233\351\230\266\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" new file mode 100644 index 00000000000..7097b672686 --- /dev/null +++ "b/docs/java/multi-thread/2020\346\234\200\346\226\260Java\345\271\266\345\217\221\350\277\233\351\230\266\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" @@ -0,0 +1,1119 @@ +点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java 面试突击》以及 Java 工程师必备学习资源。 + + + + + + +- [Java 并发进阶常见面试题总结](#java-并发进阶常见面试题总结) + - [1.synchronized 关键字](#1synchronized-关键字) + - [1.1.说一说自己对于 synchronized 关键字的了解](#11说一说自己对于-synchronized-关键字的了解) + - [1.2. 说说自己是怎么使用 synchronized 关键字](#12-说说自己是怎么使用-synchronized-关键字) + - [1.3. 构造方法可以使用 synchronized 关键字修饰么?](#13-构造方法可以使用-synchronized-关键字修饰么) + - [1.3. 讲一下 synchronized 关键字的底层原理](#13-讲一下-synchronized-关键字的底层原理) + - [1.3.1. synchronized 同步语句块的情况](#131-synchronized-同步语句块的情况) + - [1.3.2. `synchronized` 修饰方法的的情况](#132-synchronized-修饰方法的的情况) + - [1.3.3.总结](#133总结) + - [1.4. 说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗](#14-说说-jdk16-之后的-synchronized-关键字底层做了哪些优化可以详细介绍一下这些优化吗) + - [1.5. 谈谈 synchronized 和 ReentrantLock 的区别](#15-谈谈-synchronized-和-reentrantlock-的区别) + - [1.5.1. 两者都是可重入锁](#151-两者都是可重入锁) + - [1.5.2.synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API](#152synchronized-依赖于-jvm-而-reentrantlock-依赖于-api) + - [1.5.3.ReentrantLock 比 synchronized 增加了一些高级功能](#153reentrantlock-比-synchronized-增加了一些高级功能) + - [2. volatile 关键字](#2-volatile-关键字) + - [2.1. CPU 缓存模型](#21-cpu-缓存模型) + - [2.2. 讲一下 JMM(Java 内存模型)](#22-讲一下-jmmjava-内存模型) + - [2.3. 并发编程的三个重要特性](#23-并发编程的三个重要特性) + - [2.4. 说说 synchronized 关键字和 volatile 关键字的区别](#24-说说-synchronized-关键字和-volatile-关键字的区别) + - [3. ThreadLocal](#3-threadlocal) + - [3.1. ThreadLocal 简介](#31-threadlocal-简介) + - [3.2. ThreadLocal 示例](#32-threadlocal-示例) + - [3.3. ThreadLocal 原理](#33-threadlocal-原理) + - [3.4. ThreadLocal 内存泄露问题](#34-threadlocal-内存泄露问题) + - [4. 线程池](#4-线程池) + - [4.1. 为什么要用线程池?](#41-为什么要用线程池) + - [4.2. 实现 Runnable 接口和 Callable 接口的区别](#42-实现-runnable-接口和-callable-接口的区别) + - [4.3. 执行 execute()方法和 submit()方法的区别是什么呢?](#43-执行-execute方法和-submit方法的区别是什么呢) + - [4.4. 如何创建线程池](#44-如何创建线程池) + - [4.5 ThreadPoolExecutor 类分析](#45-threadpoolexecutor-类分析) + - [4.5.1 `ThreadPoolExecutor`构造函数重要参数分析](#451-threadpoolexecutor构造函数重要参数分析) + - [4.5.2 `ThreadPoolExecutor` 饱和策略](#452-threadpoolexecutor-饱和策略) + - [4.6 一个简单的线程池 Demo](#46-一个简单的线程池-demo) + - [4.7 线程池原理分析](#47-线程池原理分析) + - [5. Atomic 原子类](#5-atomic-原子类) + - [5.1. 介绍一下 Atomic 原子类](#51-介绍一下-atomic-原子类) + - [5.2. JUC 包中的原子类是哪 4 类?](#52-juc-包中的原子类是哪-4-类) + - [5.3. 讲讲 AtomicInteger 的使用](#53-讲讲-atomicinteger-的使用) + - [5.4. 能不能给我简单介绍一下 AtomicInteger 类的原理](#54-能不能给我简单介绍一下-atomicinteger-类的原理) + - [6. AQS](#6-aqs) + - [6.1. AQS 介绍](#61-aqs-介绍) + - [6.2. AQS 原理分析](#62-aqs-原理分析) + - [6.2.1. AQS 原理概览](#621-aqs-原理概览) + - [6.2.2. AQS 对资源的共享方式](#622-aqs-对资源的共享方式) + - [6.2.3. AQS 底层使用了模板方法模式](#623-aqs-底层使用了模板方法模式) + - [6.3. AQS 组件总结](#63-aqs-组件总结) + - [6.4. 用过 CountDownLatch 么?什么场景下用的?](#64-用过-countdownlatch-么什么场景下用的) + - [7 Reference](#7-reference) + - [公众号](#公众号) + + + + +# Java 并发进阶常见面试题总结 + +## 1.synchronized 关键字 + +![](images/interview-questions/synchronized关键字.png) + +### 1.1.说一说自己对于 synchronized 关键字的了解 + +**`synchronized` 关键字解决的是多个线程之间访问资源的同步性,`synchronized`关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。** + +另外,在 Java 早期版本中,`synchronized` 属于 **重量级锁**,效率低下。 + +**为什么呢?** + +因为监视器锁(monitor)是依赖于底层的操作系统的 `Mutex Lock` 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。 + +庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对 `synchronized` 较大优化,所以现在的 `synchronized` 锁效率也优化得很不错了。JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。 + +所以,你会发现目前的话,不论是各种开源框架还是 JDK 源码都大量使用了 `synchronized` 关键字。 + +### 1.2. 说说自己是怎么使用 synchronized 关键字 + +**synchronized 关键字最主要的三种使用方式:** + +**1.修饰实例方法:** 作用于当前对象实例加锁,进入同步代码前要获得 **当前对象实例的锁** + +```java +synchronized void method() { + //业务代码 +} +``` + +**2.修饰静态方法:** 也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 **当前 class 的锁**。因为静态成员不属于任何一个实例对象,是类成员( _static 表明这是该类的一个静态资源,不管 new 了多少个对象,只有一份_)。所以,如果一个线程 A 调用一个实例对象的非静态 `synchronized` 方法,而线程 B 需要调用这个实例对象所属类的静态 `synchronized` 方法,是允许的,不会发生互斥现象,**因为访问静态 `synchronized` 方法占用的锁是当前类的锁,而访问非静态 `synchronized` 方法占用的锁是当前实例对象锁**。 + +```java +synchronized void staic method() { + //业务代码 +} +``` + +**3.修饰代码块** :指定加锁对象,对给定对象/类加锁。`synchronized(this|object)` 表示进入同步代码库前要获得**给定对象的锁**。`synchronized(类.class)` 表示进入同步代码前要获得 **当前 class 的锁** + +```java +synchronized(this) { + //业务代码 +} +``` + +**总结:** + +- `synchronized` 关键字加到 `static` 静态方法和 `synchronized(class)` 代码块上都是是给 Class 类上锁。 +- `synchronized` 关键字加到实例方法上是给对象实例上锁。 +- 尽量不要使用 `synchronized(String a)` 因为 JVM 中,字符串常量池具有缓存功能! + +下面我以一个常见的面试题为例讲解一下 `synchronized` 关键字的具体使用。 + +面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理呗!” + +**双重校验锁实现对象单例(线程安全)** + +```java +public class Singleton { + + private volatile static Singleton uniqueInstance; + + private Singleton() { + } + + public static Singleton getUniqueInstance() { + //先判断对象是否已经实例过,没有实例化过才进入加锁代码 + if (uniqueInstance == null) { + //类对象加锁 + synchronized (Singleton.class) { + if (uniqueInstance == null) { + uniqueInstance = new Singleton(); + } + } + } + return uniqueInstance; + } +} +``` + +另外,需要注意 `uniqueInstance` 采用 `volatile` 关键字修饰也是很有必要。 + +`uniqueInstance` 采用 `volatile` 关键字修饰也是很有必要的, `uniqueInstance = new Singleton();` 这段代码其实是分为三步执行: + +1. 为 `uniqueInstance` 分配内存空间 +2. 初始化 `uniqueInstance` +3. 将 `uniqueInstance` 指向分配的内存地址 + +但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 `getUniqueInstance`() 后发现 `uniqueInstance` 不为空,因此返回 `uniqueInstance`,但此时 `uniqueInstance` 还未被初始化。 + +使用 `volatile` 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。 + +### 1.3. 构造方法可以使用 synchronized 关键字修饰么? + +先说结论:**构造方法不能使用 synchronized 关键字修饰。** + +构造方法本身就属于线程安全的,不存在同步的构造方法一说。 + +### 1.3. 讲一下 synchronized 关键字的底层原理 + +**synchronized 关键字底层原理属于 JVM 层面。** + +#### 1.3.1. synchronized 同步语句块的情况 + +```java +public class SynchronizedDemo { + public void method() { + synchronized (this) { + System.out.println("synchronized 代码块"); + } + } +} + +``` + +通过 JDK 自带的 `javap` 命令查看 `SynchronizedDemo` 类的相关字节码信息:首先切换到类的对应目录执行 `javac SynchronizedDemo.java` 命令生成编译后的 .class 文件,然后执行`javap -c -s -v -l SynchronizedDemo.class`。 + +![synchronized关键字原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/synchronized关键字原理.png) + +从上面我们可以看出: + +**`synchronized` 同步语句块的实现使用的是 `monitorenter` 和 `monitorexit` 指令,其中 `monitorenter` 指令指向同步代码块的开始位置,`monitorexit` 指令则指明同步代码块的结束位置。** + +当执行 `monitorenter` 指令时,线程试图获取锁也就是获取 **对象监视器 `monitor`** 的持有权。 + +> 在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++实现的,由[ObjectMonitor](https://github.com/openjdk-mirror/jdk7u-hotspot/blob/50bdefc3afe944ca74c3093e7448d6b889cd20d1/src/share/vm/runtime/objectMonitor.cpp)实现的。每个对象中都内置了一个 `ObjectMonitor`对象。 +> +> 另外,**`wait/notify`等方法也依赖于`monitor`对象,这就是为什么只有在同步的块或者方法中才能调用`wait/notify`等方法,否则会抛出`java.lang.IllegalMonitorStateException`的异常的原因。** + +在执行`monitorenter`时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。 + +在执行 `monitorexit` 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。 + +#### 1.3.2. `synchronized` 修饰方法的的情况 + +```java +public class SynchronizedDemo2 { + public synchronized void method() { + System.out.println("synchronized 方法"); + } +} + +``` + +![synchronized关键字原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/synchronized关键字原理2.png) + +`synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取得代之的确实是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。JVM 通过该 `ACC_SYNCHRONIZED` 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。 + +#### 1.3.3.总结 + +`synchronized` 同步语句块的实现使用的是 `monitorenter` 和 `monitorexit` 指令,其中 `monitorenter` 指令指向同步代码块的开始位置,`monitorexit` 指令则指明同步代码块的结束位置。 + +`synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取得代之的确实是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。 + +**不过两者的本质都是对对象监视器 monitor 的获取。** + +### 1.4. 说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗 + +JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。 + +锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。 + +关于这几种优化的详细信息可以查看下面这几篇文章: + +- [Java 性能 -- synchronized 锁升级优化](https://blog.csdn.net/qq_34337272/article/details/108498442) +- [Java6 及以上版本对 synchronized 的优化](https://www.cnblogs.com/wuqinglong/p/9945618.html) + +### 1.5. 谈谈 synchronized 和 ReentrantLock 的区别 + +#### 1.5.1. 两者都是可重入锁 + +**“可重入锁”** 指的是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。 + +#### 1.5.2.synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API + +`synchronized` 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 `synchronized` 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。`ReentrantLock` 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。 + +#### 1.5.3.ReentrantLock 比 synchronized 增加了一些高级功能 + +相比`synchronized`,`ReentrantLock`增加了一些高级功能。主要来说主要有三点: + +- **等待可中断** : `ReentrantLock`提供了一种能够中断等待锁的线程的机制,通过 `lock.lockInterruptibly()` 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 +- **可实现公平锁** : `ReentrantLock`可以指定是公平锁还是非公平锁。而`synchronized`只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。`ReentrantLock`默认情况是非公平的,可以通过 `ReentrantLock`类的`ReentrantLock(boolean fair)`构造方法来制定是否是公平的。 +- **可实现选择性通知(锁可以绑定多个条件)**: `synchronized`关键字与`wait()`和`notify()`/`notifyAll()`方法相结合可以实现等待/通知机制。`ReentrantLock`类当然也可以实现,但是需要借助于`Condition`接口与`newCondition()`方法。 + +> `Condition`是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个`Lock`对象中可以创建多个`Condition`实例(即对象监视器),**线程对象可以注册在指定的`Condition`中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用`notify()/notifyAll()`方法进行通知时,被通知的线程是由 JVM 选择的,用`ReentrantLock`类结合`Condition`实例可以实现“选择性通知”** ,这个功能非常重要,而且是 Condition 接口默认提供的。而`synchronized`关键字就相当于整个 Lock 对象中只有一个`Condition`实例,所有的线程都注册在它一个身上。如果执行`notifyAll()`方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而`Condition`实例的`signalAll()`方法 只会唤醒注册在该`Condition`实例中的所有等待线程。 + +**如果你想使用上述功能,那么选择 ReentrantLock 是一个不错的选择。性能已不是选择标准** + +## 2. volatile 关键字 + +我们先要从 **CPU 缓存模型** 说起! + +### 2.1. CPU 缓存模型 + +**为什么要弄一个 CPU 高速缓存呢?** + +类比我们开发网站后台系统使用的缓存(比如 Redis)是为了解决程序处理速度和访问常规关系型数据库速度不对等的问题。 **CPU 缓存则是为了解决 CPU 处理速度和内存处理速度不对等的问题。** + +我们甚至可以把 **内存可以看作外存的高速缓存**,程序运行的时候我们把外存的数据复制到内存,由于内存的处理速度远远高于外存,这样提高了处理速度。 + +总结:**CPU Cache 缓存的是内存数据用于解决 CPU 处理速度和内存不匹配的问题,内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题。** + +为了更好地理解,我画了一个简单的 CPU Cache 示意图如下(实际上,现代的 CPU Cache 通常分为三层,分别叫 L1,L2,L3 Cache): + +![CPU Cache](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-8/303a300f-70dd-4ee1-9974-3f33affc6574.png) + +**CPU Cache 的工作方式:** + +先复制一份数据到 CPU Cache 中,当 CPU 需要用到的时候就可以直接从 CPU Cache 中读取数据,当运算完成后,再将运算得到的数据写回 Main Memory 中。但是,这样存在 **内存缓存不一致性的问题** !比如我执行一个 i++操作的话,如果两个线程同时执行的话,假设两个线程从 CPU Cache 中读取的 i=1,两个线程做了 1++运算完之后再写回 Main Memory 之后 i=2,而正确结果应该是 i=3。 + +**CPU 为了解决内存缓存不一致性问题可以通过制定缓存一致协议或者其他手段来解决。** + +### 2.2. 讲一下 JMM(Java 内存模型) + +在 JDK1.2 之前,Java 的内存模型实现总是从**主存**(即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变量保存**本地内存**(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成**数据的不一致**。 + +![JMM(Java内存模型)](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-8/0ac7e663-7db8-4b95-8d8e-7d2b179f67e8.png) + +要解决这个问题,就需要把变量声明为**`volatile`**,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。 + +所以,**`volatile` 关键字 除了防止 JVM 的指令重排 ,还有一个重要的作用就是保证变量的可见性。** + +![volatile关键字的可见性](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-8/d49c5557-140b-4abf-adad-8aac3c9036cf.png) + +### 2.3. 并发编程的三个重要特性 + +1. **原子性** : 一个的操作或者多次操作,要么所有的操作全部都得到执行并且不会收到任何因素的干扰而中断,要么所有的操作都执行,要么都不执行。`synchronized` 可以保证代码片段的原子性。 +2. **可见性** :当一个变量对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。`volatile` 关键字可以保证共享变量的可见性。 +3. **有序性** :代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。`volatile` 关键字可以禁止指令进行重排序优化。 + +### 2.4. 说说 synchronized 关键字和 volatile 关键字的区别 + +`synchronized` 关键字和 `volatile` 关键字是两个互补的存在,而不是对立的存在! + +- **`volatile` 关键字**是线程同步的**轻量级实现**,所以**`volatile `性能肯定比` synchronized `关键字要好**。但是**`volatile` 关键字只能用于变量而 `synchronized` 关键字可以修饰方法以及代码块**。 +- **`volatile` 关键字能保证数据的可见性,但不能保证数据的原子性。`synchronized` 关键字两者都能保证。** +- **`volatile`关键字主要用于解决变量在多个线程之间的可见性,而 `synchronized` 关键字解决的是多个线程之间访问资源的同步性。** + +## 3. ThreadLocal + +### 3.1. ThreadLocal 简介 + +通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。**如果想实现每一个线程都有自己的专属本地变量该如何解决呢?** JDK 中提供的`ThreadLocal`类正是为了解决这样的问题。 **`ThreadLocal`类主要解决的就是让每个线程绑定自己的值,可以将`ThreadLocal`类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。** + +**如果你创建了一个`ThreadLocal`变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是`ThreadLocal`变量名的由来。他们可以使用 `get()` 和 `set()` 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。** + +再举个简单的例子: + +比如有两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么 ThreadLocal 就是用来避免这两个线程竞争的。 + +### 3.2. ThreadLocal 示例 + +相信看了上面的解释,大家已经搞懂 ThreadLocal 类是个什么东西了。 + +```java +import java.text.SimpleDateFormat; +import java.util.Random; + +public class ThreadLocalExample implements Runnable{ + + // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本 + private static final ThreadLocal formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm")); + + public static void main(String[] args) throws InterruptedException { + ThreadLocalExample obj = new ThreadLocalExample(); + for(int i=0 ; i<10; i++){ + Thread t = new Thread(obj, ""+i); + Thread.sleep(new Random().nextInt(1000)); + t.start(); + } + } + + @Override + public void run() { + System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern()); + try { + Thread.sleep(new Random().nextInt(1000)); + } catch (InterruptedException e) { + e.printStackTrace(); + } + //formatter pattern is changed here by thread, but it won't reflect to other threads + formatter.set(new SimpleDateFormat()); + + System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern()); + } + +} + +``` + +Output: + +``` +Thread Name= 0 default Formatter = yyyyMMdd HHmm +Thread Name= 0 formatter = yy-M-d ah:mm +Thread Name= 1 default Formatter = yyyyMMdd HHmm +Thread Name= 2 default Formatter = yyyyMMdd HHmm +Thread Name= 1 formatter = yy-M-d ah:mm +Thread Name= 3 default Formatter = yyyyMMdd HHmm +Thread Name= 2 formatter = yy-M-d ah:mm +Thread Name= 4 default Formatter = yyyyMMdd HHmm +Thread Name= 3 formatter = yy-M-d ah:mm +Thread Name= 4 formatter = yy-M-d ah:mm +Thread Name= 5 default Formatter = yyyyMMdd HHmm +Thread Name= 5 formatter = yy-M-d ah:mm +Thread Name= 6 default Formatter = yyyyMMdd HHmm +Thread Name= 6 formatter = yy-M-d ah:mm +Thread Name= 7 default Formatter = yyyyMMdd HHmm +Thread Name= 7 formatter = yy-M-d ah:mm +Thread Name= 8 default Formatter = yyyyMMdd HHmm +Thread Name= 9 default Formatter = yyyyMMdd HHmm +Thread Name= 8 formatter = yy-M-d ah:mm +Thread Name= 9 formatter = yy-M-d ah:mm +``` + +从输出中可以看出,Thread-0 已经改变了 formatter 的值,但仍然是 thread-2 默认格式化程序与初始化值相同,其他线程也一样。 + +上面有一段代码用到了创建 `ThreadLocal` 变量的那段代码用到了 Java8 的知识,它等于下面这段代码,如果你写了下面这段代码的话,IDEA 会提示你转换为 Java8 的格式(IDEA 真的不错!)。因为 ThreadLocal 类在 Java 8 中扩展,使用一个新的方法`withInitial()`,将 Supplier 功能接口作为参数。 + +```java + private static final ThreadLocal formatter = new ThreadLocal(){ + @Override + protected SimpleDateFormat initialValue() + { + return new SimpleDateFormat("yyyyMMdd HHmm"); + } + }; +``` + +### 3.3. ThreadLocal 原理 + +从 `Thread`类源代码入手。 + +```java +public class Thread implements Runnable { + ...... +//与此线程有关的ThreadLocal值。由ThreadLocal类维护 +ThreadLocal.ThreadLocalMap threadLocals = null; + +//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护 +ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; + ...... +} +``` + +从上面`Thread`类 源代码可以看出`Thread` 类中有一个 `threadLocals` 和 一个 `inheritableThreadLocals` 变量,它们都是 `ThreadLocalMap` 类型的变量,我们可以把 `ThreadLocalMap` 理解为`ThreadLocal` 类实现的定制化的 `HashMap`。默认情况下这两个变量都是 null,只有当前线程调用 `ThreadLocal` 类的 `set`或`get`方法时才创建它们,实际上调用这两个方法的时候,我们调用的是`ThreadLocalMap`类对应的 `get()`、`set()`方法。 + +`ThreadLocal`类的`set()`方法 + +```java + public void set(T value) { + Thread t = Thread.currentThread(); + ThreadLocalMap map = getMap(t); + if (map != null) + map.set(this, value); + else + createMap(t, value); + } + ThreadLocalMap getMap(Thread t) { + return t.threadLocals; + } +``` + +通过上面这些内容,我们足以通过猜测得出结论:**最终的变量是放在了当前线程的 `ThreadLocalMap` 中,并不是存在 `ThreadLocal` 上,`ThreadLocal` 可以理解为只是`ThreadLocalMap`的封装,传递了变量值。** `ThrealLocal` 类中可以通过`Thread.currentThread()`获取到当前线程对象后,直接通过`getMap(Thread t)`可以访问到该线程的`ThreadLocalMap`对象。 + +**每个`Thread`中都具备一个`ThreadLocalMap`,而`ThreadLocalMap`可以存储以`ThreadLocal`为 key ,Object 对象为 value 的键值对。** + +```java +ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { + ...... +} +``` + +比如我们在同一个线程中声明了两个 `ThreadLocal` 对象的话,会使用 `Thread`内部都是使用仅有那个`ThreadLocalMap` 存放数据的,`ThreadLocalMap`的 key 就是 `ThreadLocal`对象,value 就是 `ThreadLocal` 对象调用`set`方法设置的值。 + +![ThreadLocal数据结构](images/threadlocal数据结构.png) + +`ThreadLocalMap`是`ThreadLocal`的静态内部类。 + +![ThreadLocal内部类](images/ThreadLocal内部类.png) + +### 3.4. ThreadLocal 内存泄露问题 + +`ThreadLocalMap` 中使用的 key 为 `ThreadLocal` 的弱引用,而 value 是强引用。所以,如果 `ThreadLocal` 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,`ThreadLocalMap` 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 `set()`、`get()`、`remove()` 方法的时候,会清理掉 key 为 null 的记录。使用完 `ThreadLocal`方法后 最好手动调用`remove()`方法 + +```java + static class Entry extends WeakReference> { + /** The value associated with this ThreadLocal. */ + Object value; + + Entry(ThreadLocal k, Object v) { + super(k); + value = v; + } + } +``` + +**弱引用介绍:** + +> 如果一个对象只具有弱引用,那就类似于**可有可无的生活用品**。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。 +> +> 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。 + +## 4. 线程池 + +### 4.1. 为什么要用线程池? + +> **池化技术相比大家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。** + +**线程池**提供了一种限制和管理资源(包括执行一个任务)。 每个**线程池**还维护一些基本统计信息,例如已完成任务的数量。 + +这里借用《Java 并发编程的艺术》提到的来说一下**使用线程池的好处**: + +- **降低资源消耗**。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 +- **提高响应速度**。当任务到达时,任务可以不需要的等到线程创建就能立即执行。 +- **提高线程的可管理性**。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 + +### 4.2. 实现 Runnable 接口和 Callable 接口的区别 + +`Runnable`自 Java 1.0 以来一直存在,但`Callable`仅在 Java 1.5 中引入,目的就是为了来处理`Runnable`不支持的用例。**`Runnable` 接口**不会返回结果或抛出检查异常,但是**`Callable` 接口**可以。所以,如果任务不需要返回结果或抛出异常推荐使用 **`Runnable` 接口**,这样代码看起来会更加简洁。 + +工具类 `Executors` 可以实现 `Runnable` 对象和 `Callable` 对象之间的相互转换。(`Executors.callable(Runnable task`)或 `Executors.callable(Runnable task,Object resule)`)。 + +`Runnable.java` + +```java +@FunctionalInterface +public interface Runnable { + /** + * 被线程执行,没有返回值也无法抛出异常 + */ + public abstract void run(); +} +``` + +`Callable.java` + +```java +@FunctionalInterface +public interface Callable { + /** + * 计算结果,或在无法这样做时抛出异常。 + * @return 计算得出的结果 + * @throws 如果无法计算结果,则抛出异常 + */ + V call() throws Exception; +} +``` + +### 4.3. 执行 execute()方法和 submit()方法的区别是什么呢? + +1. **`execute()`方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;** +2. **`submit()`方法用于提交需要返回值的任务。线程池会返回一个 `Future` 类型的对象,通过这个 `Future` 对象可以判断任务是否执行成功**,并且可以通过 `Future` 的 `get()`方法来获取返回值,`get()`方法会阻塞当前线程直到任务完成,而使用 `get(long timeout,TimeUnit unit)`方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。 + +我们以**`AbstractExecutorService`**接口中的一个 `submit` 方法为例子来看看源代码: + +```java + public Future submit(Runnable task) { + if (task == null) throw new NullPointerException(); + RunnableFuture ftask = newTaskFor(task, null); + execute(ftask); + return ftask; + } +``` + +上面方法调用的 `newTaskFor` 方法返回了一个 `FutureTask` 对象。 + +```java + protected RunnableFuture newTaskFor(Runnable runnable, T value) { + return new FutureTask(runnable, value); + } +``` + +我们再来看看`execute()`方法: + +```java + public void execute(Runnable command) { + ... + } +``` + +### 4.4. 如何创建线程池 + +《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险 + +> Executors 返回线程池对象的弊端如下: +> +> - **FixedThreadPool 和 SingleThreadExecutor** : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致 OOM。 +> - **CachedThreadPool 和 ScheduledThreadPool** : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。 + +**方式一:通过构造方法实现** +![ThreadPoolExecutor构造方法](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/ThreadPoolExecutor构造方法.png) +**方式二:通过 Executor 框架的工具类 Executors 来实现** +我们可以创建三种类型的 ThreadPoolExecutor: + +- **FixedThreadPool** : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。 +- **SingleThreadExecutor:** 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。 +- **CachedThreadPool:** 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。 + +对应 Executors 工具类中的方法如图所示: +![Executor框架的工具类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/Executor框架的工具类.png) + +### 4.5 ThreadPoolExecutor 类分析 + +`ThreadPoolExecutor` 类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法的基础上产生(其他几个构造方法说白点都是给定某些默认参数的构造方法比如默认制定拒绝策略是什么),这里就不贴代码讲了,比较简单。 + +```java + /** + * 用给定的初始参数创建一个新的ThreadPoolExecutor。 + */ + public ThreadPoolExecutor(int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + BlockingQueue workQueue, + ThreadFactory threadFactory, + RejectedExecutionHandler handler) { + if (corePoolSize < 0 || + maximumPoolSize <= 0 || + maximumPoolSize < corePoolSize || + keepAliveTime < 0) + throw new IllegalArgumentException(); + if (workQueue == null || threadFactory == null || handler == null) + throw new NullPointerException(); + this.corePoolSize = corePoolSize; + this.maximumPoolSize = maximumPoolSize; + this.workQueue = workQueue; + this.keepAliveTime = unit.toNanos(keepAliveTime); + this.threadFactory = threadFactory; + this.handler = handler; + } +``` + +**下面这些对创建 非常重要,在后面使用线程池的过程中你一定会用到!所以,务必拿着小本本记清楚。** + +#### 4.5.1 `ThreadPoolExecutor`构造函数重要参数分析 + +**`ThreadPoolExecutor` 3 个最重要的参数:** + +- **`corePoolSize` :** 核心线程数线程数定义了最小可以同时运行的线程数量。 +- **`maximumPoolSize` :** 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 +- **`workQueue`:** 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。 + +`ThreadPoolExecutor`其他常见参数: + +1. **`keepAliveTime`**:当线程池中的线程数量大于 `corePoolSize` 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 `keepAliveTime`才会被回收销毁; +2. **`unit`** : `keepAliveTime` 参数的时间单位。 +3. **`threadFactory`** :executor 创建新线程的时候会用到。 +4. **`handler`** :饱和策略。关于饱和策略下面单独介绍一下。 + +#### 4.5.2 `ThreadPoolExecutor` 饱和策略 + +**`ThreadPoolExecutor` 饱和策略定义:** + +如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,`ThreadPoolTaskExecutor` 定义一些策略: + +- **`ThreadPoolExecutor.AbortPolicy`**:抛出 `RejectedExecutionException`来拒绝新任务的处理。 +- **`ThreadPoolExecutor.CallerRunsPolicy`**:调用执行自己的线程运行任务,也就是直接在调用`execute`方法的线程中运行(`run`)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。 +- **`ThreadPoolExecutor.DiscardPolicy`:** 不处理新任务,直接丢弃掉。 +- **`ThreadPoolExecutor.DiscardOldestPolicy`:** 此策略将丢弃最早的未处理的任务请求。 + +举个例子: Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecutor` 的构造函数创建线程池的时候,当我们不指定 `RejectedExecutionHandler` 饱和策略的话来配置线程池的时候默认使用的是 `ThreadPoolExecutor.AbortPolicy`。在默认情况下,`ThreadPoolExecutor` 将抛出 `RejectedExecutionException` 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 `ThreadPoolExecutor.CallerRunsPolicy`。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 `ThreadPoolExecutor` 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了) + +### 4.6 一个简单的线程池 Demo + +为了让大家更清楚上面的面试题中的一些概念,我写了一个简单的线程池 Demo。 + +首先创建一个 `Runnable` 接口的实现类(当然也可以是 `Callable` 接口,我们上面也说了两者的区别。) + +`MyRunnable.java` + +```java +import java.util.Date; + +/** + * 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。 + * @author shuang.kou + */ +public class MyRunnable implements Runnable { + + private String command; + + public MyRunnable(String s) { + this.command = s; + } + + @Override + public void run() { + System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date()); + processCommand(); + System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date()); + } + + private void processCommand() { + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + @Override + public String toString() { + return this.command; + } +} + +``` + +编写测试程序,我们这里以阿里巴巴推荐的使用 `ThreadPoolExecutor` 构造函数自定义参数的方式来创建线程池。 + +`ThreadPoolExecutorDemo.java` + +```java +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class ThreadPoolExecutorDemo { + + private static final int CORE_POOL_SIZE = 5; + private static final int MAX_POOL_SIZE = 10; + private static final int QUEUE_CAPACITY = 100; + private static final Long KEEP_ALIVE_TIME = 1L; + public static void main(String[] args) { + + //使用阿里巴巴推荐的创建线程池的方式 + //通过ThreadPoolExecutor构造函数自定义参数创建 + ThreadPoolExecutor executor = new ThreadPoolExecutor( + CORE_POOL_SIZE, + MAX_POOL_SIZE, + KEEP_ALIVE_TIME, + TimeUnit.SECONDS, + new ArrayBlockingQueue<>(QUEUE_CAPACITY), + new ThreadPoolExecutor.CallerRunsPolicy()); + + for (int i = 0; i < 10; i++) { + //创建WorkerThread对象(WorkerThread类实现了Runnable 接口) + Runnable worker = new MyRunnable("" + i); + //执行Runnable + executor.execute(worker); + } + //终止线程池 + executor.shutdown(); + while (!executor.isTerminated()) { + } + System.out.println("Finished all threads"); + } +} + +``` + +可以看到我们上面的代码指定了: + +1. `corePoolSize`: 核心线程数为 5。 +2. `maximumPoolSize` :最大线程数 10 +3. `keepAliveTime` : 等待时间为 1L。 +4. `unit`: 等待时间的单位为 TimeUnit.SECONDS。 +5. `workQueue`:任务队列为 `ArrayBlockingQueue`,并且容量为 100; +6. `handler`:饱和策略为 `CallerRunsPolicy`。 + +**Output:** + +``` +pool-1-thread-2 Start. Time = Tue Nov 12 20:59:44 CST 2019 +pool-1-thread-5 Start. Time = Tue Nov 12 20:59:44 CST 2019 +pool-1-thread-4 Start. Time = Tue Nov 12 20:59:44 CST 2019 +pool-1-thread-1 Start. Time = Tue Nov 12 20:59:44 CST 2019 +pool-1-thread-3 Start. Time = Tue Nov 12 20:59:44 CST 2019 +pool-1-thread-5 End. Time = Tue Nov 12 20:59:49 CST 2019 +pool-1-thread-3 End. Time = Tue Nov 12 20:59:49 CST 2019 +pool-1-thread-2 End. Time = Tue Nov 12 20:59:49 CST 2019 +pool-1-thread-4 End. Time = Tue Nov 12 20:59:49 CST 2019 +pool-1-thread-1 End. Time = Tue Nov 12 20:59:49 CST 2019 +pool-1-thread-2 Start. Time = Tue Nov 12 20:59:49 CST 2019 +pool-1-thread-1 Start. Time = Tue Nov 12 20:59:49 CST 2019 +pool-1-thread-4 Start. Time = Tue Nov 12 20:59:49 CST 2019 +pool-1-thread-3 Start. Time = Tue Nov 12 20:59:49 CST 2019 +pool-1-thread-5 Start. Time = Tue Nov 12 20:59:49 CST 2019 +pool-1-thread-2 End. Time = Tue Nov 12 20:59:54 CST 2019 +pool-1-thread-3 End. Time = Tue Nov 12 20:59:54 CST 2019 +pool-1-thread-4 End. Time = Tue Nov 12 20:59:54 CST 2019 +pool-1-thread-5 End. Time = Tue Nov 12 20:59:54 CST 2019 +pool-1-thread-1 End. Time = Tue Nov 12 20:59:54 CST 2019 + +``` + +### 4.7 线程池原理分析 + +承接 4.6 节,我们通过代码输出结果可以看出:**线程池每次会同时执行 5 个任务,这 5 个任务执行完之后,剩余的 5 个任务才会被执行。** 大家可以先通过上面讲解的内容,分析一下到底是咋回事?(自己独立思考一会) + +现在,我们就分析上面的输出内容来简单分析一下线程池原理。 + +**为了搞懂线程池的原理,我们需要首先分析一下 `execute`方法。**在 4.6 节中的 Demo 中我们使用 `executor.execute(worker)`来提交一个任务到线程池中去,这个方法非常重要,下面我们来看看它的源码: + +```java + // 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount) + private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); + + private static int workerCountOf(int c) { + return c & CAPACITY; + } + + private final BlockingQueue workQueue; + + public void execute(Runnable command) { + // 如果任务为null,则抛出异常。 + if (command == null) + throw new NullPointerException(); + // ctl 中保存的线程池当前的一些状态信息 + int c = ctl.get(); + + // 下面会涉及到 3 步 操作 + // 1.首先判断当前线程池中之行的任务数量是否小于 corePoolSize + // 如果小于的话,通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。 + if (workerCountOf(c) < corePoolSize) { + if (addWorker(command, true)) + return; + c = ctl.get(); + } + // 2.如果当前之行的任务数量大于等于 corePoolSize 的时候就会走到这里 + // 通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态才会被并且队列可以加入任务,该任务才会被加入进去 + if (isRunning(c) && workQueue.offer(command)) { + int recheck = ctl.get(); + // 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。 + if (!isRunning(recheck) && remove(command)) + reject(command); + // 如果当前线程池为空就新创建一个线程并执行。 + else if (workerCountOf(recheck) == 0) + addWorker(null, false); + } + //3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。 + //如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。 + else if (!addWorker(command, false)) + reject(command); + } +``` + +通过下图可以更好的对上面这 3 步做一个展示,下图是我为了省事直接从网上找到,原地址不明。 + +![图解线程池实现原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/图解线程池实现原理.png) + +现在,让我们在回到 4.6 节我们写的 Demo, 现在应该是不是很容易就可以搞懂它的原理了呢? + +没搞懂的话,也没关系,可以看看我的分析: + +> 我们在代码中模拟了 10 个任务,我们配置的核心线程数为 5 、等待队列容量为 100 ,所以每次只可能存在 5 个任务同时执行,剩下的 5 个任务会被放到等待队列中去。当前的 5 个任务之行完成后,才会之行剩下的 5 个任务。 + +## 5. Atomic 原子类 + +### 5.1. 介绍一下 Atomic 原子类 + +`Atomic` 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。 + +所以,所谓原子类说简单点就是具有原子/原子操作特征的类。 + +并发包 `java.util.concurrent` 的原子类都存放在`java.util.concurrent.atomic`下,如下图所示。 + +![JUC原子类概览](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/JUC原子类概览.png) + +### 5.2. JUC 包中的原子类是哪 4 类? + +**基本类型** + +使用原子的方式更新基本类型 + +- `AtomicInteger`:整形原子类 +- `AtomicLong`:长整型原子类 +- `AtomicBoolean`:布尔型原子类 + +**数组类型** + +使用原子的方式更新数组里的某个元素 + +- `AtomicIntegerArray`:整形数组原子类 +- `AtomicLongArray`:长整形数组原子类 +- `AtomicReferenceArray`:引用类型数组原子类 + +**引用类型** + +- `AtomicReference`:引用类型原子类 +- `AtomicStampedReference`:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。 +- `AtomicMarkableReference` :原子更新带有标记位的引用类型 + +**对象的属性修改类型** + +- `AtomicIntegerFieldUpdater`:原子更新整形字段的更新器 +- `AtomicLongFieldUpdater`:原子更新长整形字段的更新器 +- `AtomicReferenceFieldUpdater`:原子更新引用类型字段的更新器 + +### 5.3. 讲讲 AtomicInteger 的使用 + +**AtomicInteger 类常用方法** + +```java +public final int get() //获取当前的值 +public final int getAndSet(int newValue)//获取当前的值,并设置新的值 +public final int getAndIncrement()//获取当前的值,并自增 +public final int getAndDecrement() //获取当前的值,并自减 +public final int getAndAdd(int delta) //获取当前的值,并加上预期的值 +boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update) +public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。 +``` + +**AtomicInteger 类的使用示例** + +使用 AtomicInteger 之后,不用对 increment() 方法加锁也可以保证线程安全。 + +```java +class AtomicIntegerTest { + private AtomicInteger count = new AtomicInteger(); + //使用AtomicInteger之后,不需要对该方法加锁,也可以实现线程安全。 + public void increment() { + count.incrementAndGet(); + } + + public int getCount() { + return count.get(); + } +} + +``` + +### 5.4. 能不能给我简单介绍一下 AtomicInteger 类的原理 + +AtomicInteger 线程安全原理简单分析 + +AtomicInteger 类的部分源码: + +```java + // setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用) + 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; +``` + +AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。 + +CAS 的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个 volatile 变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。 + +关于 Atomic 原子类这部分更多内容可以查看我的这篇文章:并发编程面试必备:[JUC 中的 Atomic 原子类总结](https://mp.weixin.qq.com/s/joa-yOiTrYF67bElj8xqvg) + +## 6. AQS + +### 6.1. AQS 介绍 + +AQS 的全称为(`AbstractQueuedSynchronizer`),这个类在` java.util.concurrent.locks `包下面。 + +![AQS类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/AQS类.png) + +AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 `ReentrantLock`,`Semaphore`,其他的诸如 `ReentrantReadWriteLock`,`SynchronousQueue`,`FutureTask` 等等皆是基于 AQS 的。当然,我们自己也能利用 AQS 非常轻松容易地构造出符合我们自己需求的同步器。 + +### 6.2. AQS 原理分析 + +AQS 原理这部分参考了部分博客,在 5.2 节末尾放了链接。 + +> 在面试中被问到并发知识的时候,大多都会被问到“请你说一下自己对于 AQS 原理的理解”。下面给大家一个示例供大家参加,面试不是背题,大家一定要加入自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来。 + +下面大部分内容其实在 AQS 类注释上已经给出了,不过是英语看着比较吃力一点,感兴趣的话可以看看源码。 + +#### 6.2.1. AQS 原理概览 + +**AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。** + +> CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。 + +看个 AQS(AbstractQueuedSynchronizer)原理图: + +![AQS原理图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/AQS原理图.png) + +AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。 + +```java +private volatile int state;//共享变量,使用volatile修饰保证线程可见性 +``` + +状态信息通过 protected 类型的 getState,setState,compareAndSetState 进行操作 + +```java + +//返回同步状态的当前值 +protected final int getState() { + return state; +} + // 设置同步状态的值 +protected final void setState(int newState) { + state = newState; +} +//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值) +protected final boolean compareAndSetState(int expect, int update) { + return unsafe.compareAndSwapInt(this, stateOffset, expect, update); +} +``` + +#### 6.2.2. AQS 对资源的共享方式 + +**AQS 定义两种资源共享方式** + +- **Exclusive**(独占):只有一个线程能执行,如 `ReentrantLock`。又可分为公平锁和非公平锁: + - 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁 + - 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的 +- **Share**(共享):多个线程可同时执行,如` CountDownLatch`、`Semaphore`、`CountDownLatch`、 `CyclicBarrier`、`ReadWriteLock` 我们都会在后面讲到。 + +`ReentrantReadWriteLock` 可以看成是组合式,因为 `ReentrantReadWriteLock` 也就是读写锁允许多个线程同时对某一资源进行读。 + +不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS 已经在顶层实现好了。 + +#### 6.2.3. AQS 底层使用了模板方法模式 + +同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用): + +1. 使用者继承 `AbstractQueuedSynchronizer` 并重写指定的方法。(这些重写方法很简单,无非是对于共享资源 state 的获取和释放) +2. 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。 + +这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。 + +**AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的模板方法:** + +```java +isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。 +tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。 +tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。 +tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 +tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。 + +``` + +默认情况下,每个方法都抛出 `UnsupportedOperationException`。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS 类中的其他方法都是 final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。 + +以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程 lock()时,会调用 tryAcquire()独占该锁并将 state+1。此后,其他线程再 tryAcquire()时就会失败,直到 A 线程 unlock()到 state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。 + +再以 `CountDownLatch` 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后` countDown()` 一次,state 会 CAS(Compare and Swap)减 1。等到所有子线程都执行完后(即 state=0),会 unpark()主调用线程,然后主调用线程就会从 `await()` 函数返回,继续后余动作。 + +一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现`tryAcquire-tryRelease`、`tryAcquireShared-tryReleaseShared`中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如`ReentrantReadWriteLock`。 + +推荐两篇 AQS 原理和相关源码分析的文章: + +- http://www.cnblogs.com/waterystone/p/4920797.html +- https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html + +### 6.3. AQS 组件总结 + +- **`Semaphore`(信号量)-允许多个线程同时访问:** `synchronized` 和 `ReentrantLock` 都是一次只允许一个线程访问某个资源,`Semaphore`(信号量)可以指定多个线程同时访问某个资源。 +- **`CountDownLatch `(倒计时器):** `CountDownLatch` 是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。 +- **`CyclicBarrier`(循环栅栏):** `CyclicBarrier` 和 `CountDownLatch` 非常类似,它也可以实现线程间的技术等待,但是它的功能比 `CountDownLatch` 更加复杂和强大。主要应用场景和 `CountDownLatch` 类似。`CyclicBarrier` 的字面意思是可循环使用(`Cyclic`)的屏障(`Barrier`)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。`CyclicBarrier` 默认的构造方法是 `CyclicBarrier(int parties)`,其参数表示屏障拦截的线程数量,每个线程调用 `await()` 方法告诉 `CyclicBarrier` 我已经到达了屏障,然后当前线程被阻塞。 + +### 6.4. 用过 CountDownLatch 么?什么场景下用的? + +`CountDownLatch` 的作用就是 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。之前在项目中,有一个使用多线程读取多个文件处理的场景,我用到了 `CountDownLatch` 。具体场景是下面这样的: + +我们要读取处理 6 个文件,这 6 个任务都是没有执行顺序依赖的任务,但是我们需要返回给用户的时候将这几个文件的处理的结果进行统计整理。 + +为此我们定义了一个线程池和 count 为 6 的`CountDownLatch`对象 。使用线程池处理读取任务,每一个线程处理完之后就将 count-1,调用`CountDownLatch`对象的 `await()`方法,直到所有文件读取完之后,才会接着执行后面的逻辑。 + +伪代码是下面这样的: + +```java +public class CountDownLatchExample1 { + // 处理文件的数量 + private static final int threadCount = 6; + + public static void main(String[] args) throws InterruptedException { + // 创建一个具有固定线程数量的线程池对象(推荐使用构造方法创建) + ExecutorService threadPool = Executors.newFixedThreadPool(10); + final CountDownLatch countDownLatch = new CountDownLatch(threadCount); + for (int i = 0; i < threadCount; i++) { + final int threadnum = i; + threadPool.execute(() -> { + try { + //处理文件的业务操作 + ...... + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + //表示一个文件已经被完成 + countDownLatch.countDown(); + } + + }); + } + countDownLatch.await(); + threadPool.shutdown(); + System.out.println("finish"); + } + +} +``` + +**有没有可以改进的地方呢?** + +可以使用 `CompletableFuture` 类来改进!Java8 的 `CompletableFuture` 提供了很多对多线程友好的方法,使用它可以很方便地为我们编写多线程程序,什么异步、串行、并行或者等待所有线程执行完任务什么的都非常方便。 + +```java +CompletableFuture task1 = + CompletableFuture.supplyAsync(()->{ + //自定义业务操作 + }); +...... +CompletableFuture task6 = + CompletableFuture.supplyAsync(()->{ + //自定义业务操作 + }); +...... + CompletableFuture headerFuture=CompletableFuture.allOf(task1,.....,task6); + + try { + headerFuture.join(); + } catch (Exception ex) { + ...... + } +System.out.println("all done. "); +``` + +上面的代码还可以接续优化,当任务过多的时候,把每一个 task 都列出来不太现实,可以考虑通过循环来添加任务。 + +```java +//文件夹位置 +List filePaths = Arrays.asList(...) +// 异步处理所有文件 +List> fileFutures = filePaths.stream() + .map(filePath -> doSomeThing(filePath)) + .collect(Collectors.toList()); +// 将他们合并起来 +CompletableFuture allFutures = CompletableFuture.allOf( + fileFutures.toArray(new CompletableFuture[fileFutures.size()]) +); + +``` + +## 7 Reference + +- 《深入理解 Java 虚拟机》 +- 《实战 Java 高并发程序设计》 +- 《Java 并发编程的艺术》 +- http://www.cnblogs.com/waterystone/p/4920797.html +- https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html +- + +## 公众号 + +如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 + +**《Java 面试突击》:** 由本文档衍生的专为面试而生的《Java 面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"面试突击"** 即可免费领取! + +**Java 工程师必备学习资源:** 一些 Java 工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 + +![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) diff --git a/docs/java/Multithread/AQS.md "b/docs/java/multi-thread/AQS\345\216\237\347\220\206\344\273\245\345\217\212AQS\345\220\214\346\255\245\347\273\204\344\273\266\346\200\273\347\273\223.md" similarity index 100% rename from docs/java/Multithread/AQS.md rename to "docs/java/multi-thread/AQS\345\216\237\347\220\206\344\273\245\345\217\212AQS\345\220\214\346\255\245\347\273\204\344\273\266\346\200\273\347\273\223.md" diff --git a/docs/java/Multithread/Atomic.md "b/docs/java/multi-thread/Atomic\345\216\237\345\255\220\347\261\273\346\200\273\347\273\223.md" similarity index 100% rename from docs/java/Multithread/Atomic.md rename to "docs/java/multi-thread/Atomic\345\216\237\345\255\220\347\261\273\346\200\273\347\273\223.md" diff --git "a/docs/java/Multithread/ThreadLocal\357\274\210\346\234\252\345\256\214\346\210\220\357\274\211.md" "b/docs/java/multi-thread/ThreadLocal\357\274\210\346\234\252\345\256\214\346\210\220\357\274\211.md" similarity index 100% rename from "docs/java/Multithread/ThreadLocal\357\274\210\346\234\252\345\256\214\346\210\220\357\274\211.md" rename to "docs/java/multi-thread/ThreadLocal\357\274\210\346\234\252\345\256\214\346\210\220\357\274\211.md" diff --git "a/docs/java/Multithread/images/ThreadLocal\345\206\205\351\203\250\347\261\273.png" "b/docs/java/multi-thread/images/ThreadLocal\345\206\205\351\203\250\347\261\273.png" similarity index 100% rename from "docs/java/Multithread/images/ThreadLocal\345\206\205\351\203\250\347\261\273.png" rename to "docs/java/multi-thread/images/ThreadLocal\345\206\205\351\203\250\347\261\273.png" diff --git "a/docs/java/multi-thread/images/interview-questions/synchronized\345\205\263\351\224\256\345\255\227.png" "b/docs/java/multi-thread/images/interview-questions/synchronized\345\205\263\351\224\256\345\255\227.png" new file mode 100644 index 00000000000..24ac1a8cc26 Binary files /dev/null and "b/docs/java/multi-thread/images/interview-questions/synchronized\345\205\263\351\224\256\345\255\227.png" differ diff --git "a/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/CachedThreadPool-execute.png" "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/CachedThreadPool-execute.png" new file mode 100644 index 00000000000..8b2ede8ab54 Binary files /dev/null and "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/CachedThreadPool-execute.png" differ diff --git "a/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/Executors\345\267\245\345\205\267\347\261\273.png" "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/Executors\345\267\245\345\205\267\347\261\273.png" new file mode 100644 index 00000000000..87658aa3f09 Binary files /dev/null and "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/Executors\345\267\245\345\205\267\347\261\273.png" differ diff --git "a/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/Executor\346\241\206\346\236\266\347\232\204\344\275\277\347\224\250\347\244\272\346\204\217\345\233\276.png" "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/Executor\346\241\206\346\236\266\347\232\204\344\275\277\347\224\250\347\244\272\346\204\217\345\233\276.png" new file mode 100644 index 00000000000..5cc148dd79a Binary files /dev/null and "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/Executor\346\241\206\346\236\266\347\232\204\344\275\277\347\224\250\347\244\272\346\204\217\345\233\276.png" differ diff --git "a/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/FixedThreadPool.png" "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/FixedThreadPool.png" new file mode 100644 index 00000000000..fc1c7034fdf Binary files /dev/null and "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/FixedThreadPool.png" differ diff --git "a/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/ScheduledThreadPoolExecutor\346\211\247\350\241\214\345\221\250\346\234\237\344\273\273\345\212\241\346\255\245\351\252\244.png" "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/ScheduledThreadPoolExecutor\346\211\247\350\241\214\345\221\250\346\234\237\344\273\273\345\212\241\346\255\245\351\252\244.png" new file mode 100644 index 00000000000..c56521d283e Binary files /dev/null and "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/ScheduledThreadPoolExecutor\346\211\247\350\241\214\345\221\250\346\234\237\344\273\273\345\212\241\346\255\245\351\252\244.png" differ diff --git "a/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/ScheduledThreadPoolExecutor\346\234\272\345\210\266.png" "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/ScheduledThreadPoolExecutor\346\234\272\345\210\266.png" new file mode 100644 index 00000000000..bae0dc5b2dc Binary files /dev/null and "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/ScheduledThreadPoolExecutor\346\234\272\345\210\266.png" differ diff --git "a/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/SingleThreadExecutor.png" "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/SingleThreadExecutor.png" new file mode 100644 index 00000000000..c933674fad4 Binary files /dev/null and "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/SingleThreadExecutor.png" differ diff --git "a/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/threadpoolexecutor\346\236\204\351\200\240\345\207\275\346\225\260.png" "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/threadpoolexecutor\346\236\204\351\200\240\345\207\275\346\225\260.png" new file mode 100644 index 00000000000..30c298591bc Binary files /dev/null and "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/threadpoolexecutor\346\236\204\351\200\240\345\207\275\346\225\260.png" differ diff --git "a/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/\344\273\273\345\212\241\347\232\204\346\211\247\350\241\214\347\233\270\345\205\263\346\216\245\345\217\243.png" "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/\344\273\273\345\212\241\347\232\204\346\211\247\350\241\214\347\233\270\345\205\263\346\216\245\345\217\243.png" new file mode 100644 index 00000000000..6aebd60b591 Binary files /dev/null and "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/\344\273\273\345\212\241\347\232\204\346\211\247\350\241\214\347\233\270\345\205\263\346\216\245\345\217\243.png" differ diff --git "a/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/\345\233\276\350\247\243\347\272\277\347\250\213\346\261\240\345\256\236\347\216\260\345\216\237\347\220\206.png" "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/\345\233\276\350\247\243\347\272\277\347\250\213\346\261\240\345\256\236\347\216\260\345\216\237\347\220\206.png" new file mode 100644 index 00000000000..bc661944a0a Binary files /dev/null and "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/\345\233\276\350\247\243\347\272\277\347\250\213\346\261\240\345\256\236\347\216\260\345\216\237\347\220\206.png" differ diff --git "a/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/\347\272\277\347\250\213\346\261\240\345\220\204\344\270\252\345\217\202\346\225\260\344\271\213\351\227\264\347\232\204\345\205\263\347\263\273.png" "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/\347\272\277\347\250\213\346\261\240\345\220\204\344\270\252\345\217\202\346\225\260\344\271\213\351\227\264\347\232\204\345\205\263\347\263\273.png" new file mode 100644 index 00000000000..d609943bafe Binary files /dev/null and "b/docs/java/multi-thread/images/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223/\347\272\277\347\250\213\346\261\240\345\220\204\344\270\252\345\217\202\346\225\260\344\271\213\351\227\264\347\232\204\345\205\263\347\263\273.png" differ diff --git a/docs/java/Multithread/images/thread-local/1.png b/docs/java/multi-thread/images/thread-local/1.png similarity index 100% rename from docs/java/Multithread/images/thread-local/1.png rename to docs/java/multi-thread/images/thread-local/1.png diff --git a/docs/java/Multithread/images/thread-local/10.png b/docs/java/multi-thread/images/thread-local/10.png similarity index 100% rename from docs/java/Multithread/images/thread-local/10.png rename to docs/java/multi-thread/images/thread-local/10.png diff --git a/docs/java/Multithread/images/thread-local/11.png b/docs/java/multi-thread/images/thread-local/11.png similarity index 100% rename from docs/java/Multithread/images/thread-local/11.png rename to docs/java/multi-thread/images/thread-local/11.png diff --git a/docs/java/Multithread/images/thread-local/12.png b/docs/java/multi-thread/images/thread-local/12.png similarity index 100% rename from docs/java/Multithread/images/thread-local/12.png rename to docs/java/multi-thread/images/thread-local/12.png diff --git a/docs/java/Multithread/images/thread-local/13.png b/docs/java/multi-thread/images/thread-local/13.png similarity index 100% rename from docs/java/Multithread/images/thread-local/13.png rename to docs/java/multi-thread/images/thread-local/13.png diff --git a/docs/java/Multithread/images/thread-local/14.png b/docs/java/multi-thread/images/thread-local/14.png similarity index 100% rename from docs/java/Multithread/images/thread-local/14.png rename to docs/java/multi-thread/images/thread-local/14.png diff --git a/docs/java/Multithread/images/thread-local/15.png b/docs/java/multi-thread/images/thread-local/15.png similarity index 100% rename from docs/java/Multithread/images/thread-local/15.png rename to docs/java/multi-thread/images/thread-local/15.png diff --git a/docs/java/Multithread/images/thread-local/16.png b/docs/java/multi-thread/images/thread-local/16.png similarity index 100% rename from docs/java/Multithread/images/thread-local/16.png rename to docs/java/multi-thread/images/thread-local/16.png diff --git a/docs/java/Multithread/images/thread-local/17.png b/docs/java/multi-thread/images/thread-local/17.png similarity index 100% rename from docs/java/Multithread/images/thread-local/17.png rename to docs/java/multi-thread/images/thread-local/17.png diff --git a/docs/java/Multithread/images/thread-local/18.png b/docs/java/multi-thread/images/thread-local/18.png similarity index 100% rename from docs/java/Multithread/images/thread-local/18.png rename to docs/java/multi-thread/images/thread-local/18.png diff --git a/docs/java/Multithread/images/thread-local/19.png b/docs/java/multi-thread/images/thread-local/19.png similarity index 100% rename from docs/java/Multithread/images/thread-local/19.png rename to docs/java/multi-thread/images/thread-local/19.png diff --git a/docs/java/Multithread/images/thread-local/2.png b/docs/java/multi-thread/images/thread-local/2.png similarity index 100% rename from docs/java/Multithread/images/thread-local/2.png rename to docs/java/multi-thread/images/thread-local/2.png diff --git a/docs/java/Multithread/images/thread-local/20.png b/docs/java/multi-thread/images/thread-local/20.png similarity index 100% rename from docs/java/Multithread/images/thread-local/20.png rename to docs/java/multi-thread/images/thread-local/20.png diff --git a/docs/java/Multithread/images/thread-local/21.png b/docs/java/multi-thread/images/thread-local/21.png similarity index 100% rename from docs/java/Multithread/images/thread-local/21.png rename to docs/java/multi-thread/images/thread-local/21.png diff --git a/docs/java/Multithread/images/thread-local/22.png b/docs/java/multi-thread/images/thread-local/22.png similarity index 100% rename from docs/java/Multithread/images/thread-local/22.png rename to docs/java/multi-thread/images/thread-local/22.png diff --git a/docs/java/Multithread/images/thread-local/23.png b/docs/java/multi-thread/images/thread-local/23.png similarity index 100% rename from docs/java/Multithread/images/thread-local/23.png rename to docs/java/multi-thread/images/thread-local/23.png diff --git a/docs/java/Multithread/images/thread-local/24.png b/docs/java/multi-thread/images/thread-local/24.png similarity index 100% rename from docs/java/Multithread/images/thread-local/24.png rename to docs/java/multi-thread/images/thread-local/24.png diff --git a/docs/java/Multithread/images/thread-local/25.png b/docs/java/multi-thread/images/thread-local/25.png similarity index 100% rename from docs/java/Multithread/images/thread-local/25.png rename to docs/java/multi-thread/images/thread-local/25.png diff --git a/docs/java/Multithread/images/thread-local/26.png b/docs/java/multi-thread/images/thread-local/26.png similarity index 100% rename from docs/java/Multithread/images/thread-local/26.png rename to docs/java/multi-thread/images/thread-local/26.png diff --git a/docs/java/Multithread/images/thread-local/27.png b/docs/java/multi-thread/images/thread-local/27.png similarity index 100% rename from docs/java/Multithread/images/thread-local/27.png rename to docs/java/multi-thread/images/thread-local/27.png diff --git a/docs/java/Multithread/images/thread-local/28.png b/docs/java/multi-thread/images/thread-local/28.png similarity index 100% rename from docs/java/Multithread/images/thread-local/28.png rename to docs/java/multi-thread/images/thread-local/28.png diff --git a/docs/java/Multithread/images/thread-local/29.png b/docs/java/multi-thread/images/thread-local/29.png similarity index 100% rename from docs/java/Multithread/images/thread-local/29.png rename to docs/java/multi-thread/images/thread-local/29.png diff --git a/docs/java/Multithread/images/thread-local/3.png b/docs/java/multi-thread/images/thread-local/3.png similarity index 100% rename from docs/java/Multithread/images/thread-local/3.png rename to docs/java/multi-thread/images/thread-local/3.png diff --git a/docs/java/Multithread/images/thread-local/30.png b/docs/java/multi-thread/images/thread-local/30.png similarity index 100% rename from docs/java/Multithread/images/thread-local/30.png rename to docs/java/multi-thread/images/thread-local/30.png diff --git a/docs/java/Multithread/images/thread-local/31.png b/docs/java/multi-thread/images/thread-local/31.png similarity index 100% rename from docs/java/Multithread/images/thread-local/31.png rename to docs/java/multi-thread/images/thread-local/31.png diff --git a/docs/java/Multithread/images/thread-local/4.png b/docs/java/multi-thread/images/thread-local/4.png similarity index 100% rename from docs/java/Multithread/images/thread-local/4.png rename to docs/java/multi-thread/images/thread-local/4.png diff --git a/docs/java/Multithread/images/thread-local/5.png b/docs/java/multi-thread/images/thread-local/5.png similarity index 100% rename from docs/java/Multithread/images/thread-local/5.png rename to docs/java/multi-thread/images/thread-local/5.png diff --git a/docs/java/Multithread/images/thread-local/6.png b/docs/java/multi-thread/images/thread-local/6.png similarity index 100% rename from docs/java/Multithread/images/thread-local/6.png rename to docs/java/multi-thread/images/thread-local/6.png diff --git a/docs/java/Multithread/images/thread-local/7.png b/docs/java/multi-thread/images/thread-local/7.png similarity index 100% rename from docs/java/Multithread/images/thread-local/7.png rename to docs/java/multi-thread/images/thread-local/7.png diff --git a/docs/java/Multithread/images/thread-local/8.png b/docs/java/multi-thread/images/thread-local/8.png similarity index 100% rename from docs/java/Multithread/images/thread-local/8.png rename to docs/java/multi-thread/images/thread-local/8.png diff --git a/docs/java/Multithread/images/thread-local/9.png b/docs/java/multi-thread/images/thread-local/9.png similarity index 100% rename from docs/java/Multithread/images/thread-local/9.png rename to docs/java/multi-thread/images/thread-local/9.png diff --git a/docs/java/Multithread/images/thread-pool/19a0255a-6ef3-4835-98d1-a839d1983332.png b/docs/java/multi-thread/images/thread-pool/19a0255a-6ef3-4835-98d1-a839d1983332.png similarity index 100% rename from docs/java/Multithread/images/thread-pool/19a0255a-6ef3-4835-98d1-a839d1983332.png rename to docs/java/multi-thread/images/thread-pool/19a0255a-6ef3-4835-98d1-a839d1983332.png diff --git a/docs/java/Multithread/images/thread-pool/1bc44c67-26ba-42ab-bcb8-4e29e6fd99b9.png b/docs/java/multi-thread/images/thread-pool/1bc44c67-26ba-42ab-bcb8-4e29e6fd99b9.png similarity index 100% rename from docs/java/Multithread/images/thread-pool/1bc44c67-26ba-42ab-bcb8-4e29e6fd99b9.png rename to docs/java/multi-thread/images/thread-pool/1bc44c67-26ba-42ab-bcb8-4e29e6fd99b9.png diff --git a/docs/java/Multithread/images/thread-pool/5b9b814d-722a-4116-b066-43dc80fc1dc4.png b/docs/java/multi-thread/images/thread-pool/5b9b814d-722a-4116-b066-43dc80fc1dc4.png similarity index 100% rename from docs/java/Multithread/images/thread-pool/5b9b814d-722a-4116-b066-43dc80fc1dc4.png rename to docs/java/multi-thread/images/thread-pool/5b9b814d-722a-4116-b066-43dc80fc1dc4.png diff --git a/docs/java/Multithread/images/thread-pool/7888fb0d-4699-4d3a-8885-405cb5415617.png b/docs/java/multi-thread/images/thread-pool/7888fb0d-4699-4d3a-8885-405cb5415617.png similarity index 100% rename from docs/java/Multithread/images/thread-pool/7888fb0d-4699-4d3a-8885-405cb5415617.png rename to docs/java/multi-thread/images/thread-pool/7888fb0d-4699-4d3a-8885-405cb5415617.png diff --git a/docs/java/Multithread/images/thread-pool/b6fd95a7-4c9d-4fc6-ad26-890adb3f6c4c.png b/docs/java/multi-thread/images/thread-pool/b6fd95a7-4c9d-4fc6-ad26-890adb3f6c4c.png similarity index 100% rename from docs/java/Multithread/images/thread-pool/b6fd95a7-4c9d-4fc6-ad26-890adb3f6c4c.png rename to docs/java/multi-thread/images/thread-pool/b6fd95a7-4c9d-4fc6-ad26-890adb3f6c4c.png diff --git a/docs/java/Multithread/images/thread-pool/ddf22709-bff5-45b4-acb7-a3f2e6798608.png b/docs/java/multi-thread/images/thread-pool/ddf22709-bff5-45b4-acb7-a3f2e6798608.png similarity index 100% rename from docs/java/Multithread/images/thread-pool/ddf22709-bff5-45b4-acb7-a3f2e6798608.png rename to docs/java/multi-thread/images/thread-pool/ddf22709-bff5-45b4-acb7-a3f2e6798608.png diff --git "a/docs/java/Multithread/images/threadlocal\346\225\260\346\215\256\347\273\223\346\236\204.png" "b/docs/java/multi-thread/images/threadlocal\346\225\260\346\215\256\347\273\223\346\236\204.png" similarity index 100% rename from "docs/java/Multithread/images/threadlocal\346\225\260\346\215\256\347\273\223\346\236\204.png" rename to "docs/java/multi-thread/images/threadlocal\346\225\260\346\215\256\347\273\223\346\236\204.png" diff --git "a/docs/java/Multithread/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/Java\345\271\266\345\217\221\347\274\226\347\250\213\347\232\204\350\211\272\346\234\257.png" "b/docs/java/multi-thread/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/Java\345\271\266\345\217\221\347\274\226\347\250\213\347\232\204\350\211\272\346\234\257.png" similarity index 100% rename from "docs/java/Multithread/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/Java\345\271\266\345\217\221\347\274\226\347\250\213\347\232\204\350\211\272\346\234\257.png" rename to "docs/java/multi-thread/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/Java\345\271\266\345\217\221\347\274\226\347\250\213\347\232\204\350\211\272\346\234\257.png" diff --git "a/docs/java/Multithread/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/javaguide-\345\271\266\345\217\221.png" "b/docs/java/multi-thread/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/javaguide-\345\271\266\345\217\221.png" similarity index 100% rename from "docs/java/Multithread/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/javaguide-\345\271\266\345\217\221.png" rename to "docs/java/multi-thread/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/javaguide-\345\271\266\345\217\221.png" diff --git "a/docs/java/Multithread/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/java\345\271\266\345\217\221\347\274\226\347\250\213\344\271\213\347\276\216.png" "b/docs/java/multi-thread/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/java\345\271\266\345\217\221\347\274\226\347\250\213\344\271\213\347\276\216.png" similarity index 100% rename from "docs/java/Multithread/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/java\345\271\266\345\217\221\347\274\226\347\250\213\344\271\213\347\276\216.png" rename to "docs/java/multi-thread/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/java\345\271\266\345\217\221\347\274\226\347\250\213\344\271\213\347\276\216.png" diff --git "a/docs/java/Multithread/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/\345\256\236\346\210\230Java\351\253\230\345\271\266\345\217\221\347\250\213\345\272\217\350\256\276\350\256\241.png" "b/docs/java/multi-thread/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/\345\256\236\346\210\230Java\351\253\230\345\271\266\345\217\221\347\250\213\345\272\217\350\256\276\350\256\241.png" similarity index 100% rename from "docs/java/Multithread/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/\345\256\236\346\210\230Java\351\253\230\345\271\266\345\217\221\347\250\213\345\272\217\350\256\276\350\256\241.png" rename to "docs/java/multi-thread/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/\345\256\236\346\210\230Java\351\253\230\345\271\266\345\217\221\347\250\213\345\272\217\350\256\276\350\256\241.png" diff --git "a/docs/java/Multithread/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/\346\267\261\345\205\245\346\265\205\345\207\272Java\345\244\232\347\272\277\347\250\213.png" "b/docs/java/multi-thread/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/\346\267\261\345\205\245\346\265\205\345\207\272Java\345\244\232\347\272\277\347\250\213.png" similarity index 100% rename from "docs/java/Multithread/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/\346\267\261\345\205\245\346\265\205\345\207\272Java\345\244\232\347\272\277\347\250\213.png" rename to "docs/java/multi-thread/images/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227/\346\267\261\345\205\245\346\265\205\345\207\272Java\345\244\232\347\272\277\347\250\213.png" diff --git "a/docs/java/Multithread/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223.md" "b/docs/java/multi-thread/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223.md" similarity index 95% rename from "docs/java/Multithread/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223.md" rename to "docs/java/multi-thread/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223.md" index 54f4b650aa2..2e904d5fde6 100644 --- "a/docs/java/Multithread/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223.md" +++ "b/docs/java/multi-thread/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223.md" @@ -91,13 +91,13 @@ public class ThreadPoolExecutor extends AbstractExecutorService **`ScheduledThreadPoolExecutor` 类描述:** ```java -//ScheduledExecutorService实现了ExecutorService接口 +//ScheduledExecutorService继承ExecutorService接口 public class ScheduledThreadPoolExecutor extends ThreadPoolExecutor implements ScheduledExecutorService ``` -![任务的执行相关接口](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/任务的执行相关接口.png) +![任务的执行相关接口](images/java线程池学习总结/任务的执行相关接口.png) #### 3) 异步计算的结果(`Future`) @@ -107,7 +107,7 @@ public class ScheduledThreadPoolExecutor ### 2.3 Executor 框架的使用示意图 -![Executor 框架的使用示意图](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC01LTMwLzg0ODIzMzMwLmpwZw?x-oss-process=image/format,png) +![Executor 框架的使用示意图](images/java线程池学习总结/Executor框架的使用示意图.png) 1. **主线程首先要创建实现 `Runnable` 或者 `Callable` 接口的任务对象。** 2. **把创建完成的实现 `Runnable`/`Callable`接口的 对象直接交给 `ExecutorService` 执行**: `ExecutorService.execute(Runnable command)`)或者也可以把 `Runnable` 对象或`Callable` 对象提交给 `ExecutorService` 执行(`ExecutorService.submit(Runnable task)`或 `ExecutorService.submit(Callable task)`)。 @@ -167,7 +167,7 @@ public class ScheduledThreadPoolExecutor 下面这张图可以加深你对线程池中各个参数的相互关系的理解(图片来源:《Java 性能调优实战》): -![线程池各个参数的关系](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/线程池各个参数的关系.jpg) +![线程池各个参数的关系](images/java线程池学习总结/线程池各个参数之间的关系.png) **`ThreadPoolExecutor` 饱和策略定义:** @@ -198,7 +198,7 @@ public class ScheduledThreadPoolExecutor > - **CachedThreadPool 和 ScheduledThreadPool** : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。 **方式一:通过`ThreadPoolExecutor`构造函数实现(推荐)** -![通过构造方法实现](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC00LTE2LzE3ODU4MjMwLmpwZw?x-oss-process=image/format,png) +![通过构造方法实现](images/java线程池学习总结/threadpoolexecutor构造函数.png) **方式二:通过 Executor 框架的工具类 Executors 来实现** 我们可以创建三种类型的 ThreadPoolExecutor: @@ -207,7 +207,7 @@ public class ScheduledThreadPoolExecutor - **CachedThreadPool** 对应 Executors 工具类中的方法如图所示: -![通过Executor 框架的工具类Executors来实现](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/format,png.jpeg) +![通过Executor 框架的工具类Executors来实现](images/java线程池学习总结/Executors工具类.png) ## 四 (重要)ThreadPoolExecutor 使用示例 @@ -341,7 +341,7 @@ pool-1-thread-2 End. Time = Sun Apr 12 11:14:47 CST 2020 现在,我们就分析上面的输出内容来简单分析一下线程池原理。 -**为了搞懂线程池的原理,我们需要首先分析一下 `execute`方法。**在 4.1 节中的 Demo 中我们使用 `executor.execute(worker)`来提交一个任务到线程池中去,这个方法非常重要,下面我们来看看它的源码: +**为了搞懂线程池的原理,我们需要首先分析一下 `execute`方法。** 在 4.1 节中的 Demo 中我们使用 `executor.execute(worker)`来提交一个任务到线程池中去,这个方法非常重要,下面我们来看看它的源码: ```java // 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount) @@ -388,7 +388,7 @@ pool-1-thread-2 End. Time = Sun Apr 12 11:14:47 CST 2020 通过下图可以更好的对上面这 3 步做一个展示,下图是我为了省事直接从网上找到,原地址不明。 -![图解线程池实现原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/图解线程池实现原理.png) +![图解线程池实现原理](images/java线程池学习总结/图解线程池实现原理.png) @@ -543,7 +543,7 @@ public interface Callable { #### 4.3.2 `execute()` vs `submit()` 1. **`execute()`方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;** -2. **`submit()`方法用于提交需要返回值的任务。线程池会返回一个 `Future` 类型的对象,通过这个 `Future` 对象可以判断任务是否执行成功**,并且可以通过 `Future` 的 `get()`方法来获取返回值,`get()`方法会阻塞当前线程直到任务完成,而使用 `get(long timeout,TimeUnit unit)`方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。 +2. **`submit()`方法用于提交需要返回值的任务。线程池会返回一个 `Future` 类型的对象,通过这个 `Future` 对象可以判断任务是否执行成功** ,并且可以通过 `Future` 的 `get()`方法来获取返回值,`get()`方法会阻塞当前线程直到任务完成,而使用 `get(long timeout,TimeUnit unit)`方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。 我们以**`AbstractExecutorService`**接口中的一个 `submit` 方法为例子来看看源代码: @@ -705,7 +705,7 @@ Wed Nov 13 13:40:43 CST 2019::pool-1-thread-5 `FixedThreadPool` 的 `execute()` 方法运行示意图(该图片来源:《Java 并发编程的艺术》): -![FixedThreadPool的execute()方法运行示意图](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC00LTE2LzcxMzc1OTYzLmpwZw?x-oss-process=image/format,png) +![FixedThreadPool的execute()方法运行示意图](images/java线程池学习总结/FixedThreadPool.png) **上图说明:** @@ -755,7 +755,7 @@ Wed Nov 13 13:40:43 CST 2019::pool-1-thread-5 #### 5.2.2 执行任务过程介绍 **`SingleThreadExecutor` 的运行示意图(该图片来源:《Java 并发编程的艺术》):** -![SingleThreadExecutor的运行示意图](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC00LTE2LzgyMjc2NDU4LmpwZw?x-oss-process=image/format,png) +![SingleThreadExecutor的运行示意图](images/java线程池学习总结/SingleThreadExecutor.png) **上图说明;** @@ -799,7 +799,7 @@ Wed Nov 13 13:40:43 CST 2019::pool-1-thread-5 #### 5.3.2 执行任务过程介绍 **CachedThreadPool 的 execute()方法的执行示意图(该图片来源:《Java 并发编程的艺术》):** -![CachedThreadPool的execute()方法的执行示意图](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC00LTE2LzE4NjExNzY3LmpwZw?x-oss-process=image/format,png) +![CachedThreadPool的execute()方法的执行示意图](images/java线程池学习总结/CachedThreadPool-execute.png) **上图说明:** @@ -830,7 +830,7 @@ Wed Nov 13 13:40:43 CST 2019::pool-1-thread-5 ### 6.2 运行机制 -![ScheduledThreadPoolExecutor运行机制](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC00LTE2LzkyNTk0Njk4LmpwZw?x-oss-process=image/format,png) +![ScheduledThreadPoolExecutor运行机制](images/java线程池学习总结/ScheduledThreadPoolExecutor机制.png) **`ScheduledThreadPoolExecutor` 的执行主要分为两大部分:** @@ -845,7 +845,7 @@ Wed Nov 13 13:40:43 CST 2019::pool-1-thread-5 ### 6.3 ScheduledThreadPoolExecutor 执行周期任务的步骤 -![ScheduledThreadPoolExecutor执行周期任务的步骤](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC01LTMwLzU5OTE2Mzg5LmpwZw?x-oss-process=image/format,png) +![ScheduledThreadPoolExecutor执行周期任务的步骤](images/java线程池学习总结/ScheduledThreadPoolExecutor执行周期任务步骤.png) 1. 线程 1 从 `DelayQueue` 中获取已到期的 `ScheduledFutureTask(DelayQueue.take())`。到期任务是指 `ScheduledFutureTask`的 time 大于等于当前系统的时间; 2. 线程 1 执行这个 `ScheduledFutureTask`; diff --git "a/docs/java/multi-thread/synchronized\345\234\250JDK1.6\344\271\213\345\220\216\347\232\204\345\272\225\345\261\202\344\274\230\345\214\226.md" "b/docs/java/multi-thread/synchronized\345\234\250JDK1.6\344\271\213\345\220\216\347\232\204\345\272\225\345\261\202\344\274\230\345\214\226.md" new file mode 100644 index 00000000000..c3776c2da21 --- /dev/null +++ "b/docs/java/multi-thread/synchronized\345\234\250JDK1.6\344\271\213\345\220\216\347\232\204\345\272\225\345\261\202\344\274\230\345\214\226.md" @@ -0,0 +1,62 @@ +JDK1.6 对锁的实现引入了大量的优化来减少锁操作的开销,如: **偏向锁**、**轻量级锁**、**自旋锁**、**适应性自旋锁**、**锁消除**、**锁粗化** 等等技术。 + +锁主要存在四中状态,依次是: + +1. 无锁状态 +2. 偏向锁状态 +3. 轻量级锁状态 +4. 重量级锁状态 + +锁🔐会随着竞争的激烈而逐渐升级。 + +另外,需要注意:**锁可以升级不可降级,即 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁是单向的。** 这种策略是为了提高获得锁和释放锁的效率。 + +### 偏向锁 + +**引入偏向锁的目的和引入轻量级锁的目的很像,他们都是为了没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。但是不同是:轻量级锁在无竞争的情况下使用 CAS 操作去代替使用互斥量。而偏向锁在无竞争的情况下会把整个同步都消除掉**。 + +偏向锁的“偏”就是偏心的偏,它的意思是会偏向于第一个获得它的线程,如果在接下来的执行中,该锁没有被其他线程获取,那么持有偏向锁的线程就不需要进行同步!(关于偏向锁的原理可以查看《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版的13章第三节锁优化。) + +#### 偏向锁的加锁 + +当一个线程访问同步块并获取锁时, 会在锁对象的对象头和栈帧中的锁记录里存储锁偏向的线程ID, 以后该线程进入和退出同步块时不需要进行CAS操作来加锁和解锁, 只需要简单的测试一下锁对象的对象头的MarkWord里是否存储着指向当前线程的偏向锁(线程ID是当前线程), 如果测试成功, 表示线程已经获得了锁; 如果测试失败, 则需要再测试一下MarkWord中偏向锁的标识是否设置成1(表示当前是偏向锁), 如果没有设置, 则使用CAS竞争锁, 如果设置了, 则尝试使用CAS将锁对象的对象头的偏向锁指向当前线程. + +#### 偏向锁的撤销 + +偏向锁使用了一种等到竞争出现才释放锁的机制, 所以当其他线程尝试竞争偏向锁时, 持有偏向锁的线程才会释放锁. 偏向锁的撤销需要等到全局安全点(在这个时间点上没有正在执行的字节码). 首先会暂停持有偏向锁的线程, 然后检查持有偏向锁的线程是否存活, 如果线程不处于活动状态, 则将锁对象的对象头设置为无锁状态; 如果线程仍然活着, 则锁对象的对象头中的MarkWord和栈中的锁记录要么重新偏向于其它线程要么恢复到无锁状态, 最后唤醒暂停的线程(释放偏向锁的线程). + +但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。 + +### 轻量级锁 + +倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的)。**轻量级锁不是为了代替重量级锁,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,因为使用轻量级锁时,不需要申请互斥量。另外,轻量级锁的加锁和解锁都用到了CAS操作。** 关于轻量级锁的加锁和解锁的原理可以查看《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版的13章第三节锁优化。 + +**轻量级锁能够提升程序同步性能的依据是“对于绝大部分锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥操作的开销。但如果存在锁竞争,除了互斥量开销外,还会额外发生CAS操作,因此在有锁竞争的情况下,轻量级锁比传统的重量级锁更慢!如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁!** + +### 自旋锁和自适应自旋 + +轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。 + +互斥同步对性能最大的影响就是阻塞的实现,因为挂起线程/恢复线程的操作都需要转入内核态中完成(用户态转换到内核态会耗费时间)。 + +**一般线程持有锁的时间都不是太长,所以仅仅为了这一点时间去挂起线程/恢复线程是得不偿失的。** 所以,虚拟机的开发团队就这样去考虑:“我们能不能让后面来的请求获取锁的线程等待一会而不被挂起呢?看看持有锁的线程是否很快就会释放锁”。**为了让一个线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就叫做自旋**。 + +百度百科对自旋锁的解释: + +> 何谓自旋锁?它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。 + +自旋锁在 JDK1.6 之前其实就已经引入了,不过是默认关闭的,需要通过`--XX:+UseSpinning`参数来开启。JDK1.6及1.6之后,就改为默认开启的了。需要注意的是:自旋等待不能完全替代阻塞,因为它还是要占用处理器时间。如果锁被占用的时间短,那么效果当然就很好了!反之,相反!自旋等待的时间必须要有限度。如果自旋超过了限定次数任然没有获得锁,就应该挂起线程。**自旋次数的默认值是10次,用户可以修改`--XX:PreBlockSpin`来更改**。 + +另外,**在 JDK1.6 中引入了自适应的自旋锁。自适应的自旋锁带来的改进就是:自旋的时间不在固定了,而是和前一次同一个锁上的自旋时间以及锁的拥有者的状态来决定,虚拟机变得越来越“聪明”了**。 + +### 锁消除 + +锁消除理解起来很简单,它指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。 + +### 锁粗化 + +原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,——只在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。 + +大部分情况下,上面的原则都是没有问题的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,那么会带来很多不必要的性能消耗。 + + diff --git a/docs/java/Multithread/ThreadLocal.md "b/docs/java/multi-thread/\344\270\207\345\255\227\350\257\246\350\247\243ThreadLocal\345\205\263\351\224\256\345\255\227.md" similarity index 95% rename from docs/java/Multithread/ThreadLocal.md rename to "docs/java/multi-thread/\344\270\207\345\255\227\350\257\246\350\247\243ThreadLocal\345\205\263\351\224\256\345\255\227.md" index 6383ef1dba8..211f7aeeca4 100644 --- a/docs/java/Multithread/ThreadLocal.md +++ "b/docs/java/multi-thread/\344\270\207\345\255\227\350\257\246\350\247\243ThreadLocal\345\205\263\351\224\256\345\255\227.md" @@ -35,7 +35,7 @@ public class ThreadLocalTest { private List messages = Lists.newArrayList(); - public static final `ThreadLocal` holder = `ThreadLocal`.withInitial(ThreadLocalTest::new); + public static final ThreadLocal holder = ThreadLocal.withInitial(ThreadLocalTest::new); public static void add(String message) { holder.get().messages.add(message); @@ -70,8 +70,7 @@ size: 0 ![](./images/thread-local/2.png) - -`Thread`类有一个类型为``ThreadLocal`.`ThreadLocalMap``的实例变量`threadLocals`,也就是说每个线程有一个自己的`ThreadLocalMap`。 +`Thread`类有一个类型为`ThreadLocal.ThreadLocalMap`的实例变量`threadLocals`,也就是说每个线程有一个自己的`ThreadLocalMap`。 `ThreadLocalMap`有自己的独立实现,可以简单地将它的`key`视作`ThreadLocal`,`value`为代码中放入的值(实际上`key`并不是`ThreadLocal`本身,而是它的一个**弱引用**)。 @@ -79,11 +78,11 @@ size: 0 `ThreadLocalMap`有点类似`HashMap`的结构,只是`HashMap`是由**数组+链表**实现的,而`ThreadLocalMap`中并没有**链表**结构。 -我们还要注意`Entry`, 它的`key`是``ThreadLocal` k` ,继承自`WeakReference, 也就是我们常说的弱引用类型。 +我们还要注意`Entry`, 它的`key`是`ThreadLocal k` ,继承自`WeakReference`, 也就是我们常说的弱引用类型。 ### GC 之后key是否为null? -回应开头的那个问题, `ThreadLocal` 的`key`是弱引用,那么在` `ThreadLocal`.get()`的时候,发生`GC`之后,`key`是否是`null`? +回应开头的那个问题, `ThreadLocal` 的`key`是弱引用,那么在`ThreadLocal.get()`的时候,发生`GC`之后,`key`是否是`null`? 为了搞清楚这个问题,我们需要搞清楚`Java`的**四种引用类型**: @@ -110,7 +109,7 @@ public class ThreadLocalDemo { private static void test(String s,boolean isGC) { try { - new `ThreadLocal`<>().set(s); + new ThreadLocal<>().set(s); if (isGC) { System.gc(); } @@ -118,11 +117,11 @@ public class ThreadLocalDemo { Class clz = t.getClass(); Field field = clz.getDeclaredField("threadLocals"); field.setAccessible(true); - Object `ThreadLocalMap` = field.get(t); - Class tlmClass = `ThreadLocalMap`.getClass(); + Object ThreadLocalMap = field.get(t); + Class tlmClass = ThreadLocalMap.getClass(); Field tableField = tlmClass.getDeclaredField("table"); tableField.setAccessible(true); - Object[] arr = (Object[]) tableField.get(`ThreadLocalMap`); + Object[] arr = (Object[]) tableField.get(ThreadLocalMap); for (Object o : arr) { if (o != null) { Class entryClass = o.getClass(); @@ -142,8 +141,8 @@ public class ThreadLocalDemo { 结果如下: ```java -弱引用key:java.lang.`ThreadLocal`@433619b6,值:abc -弱引用key:java.lang.`ThreadLocal`@418a15e3,值:java.lang.ref.SoftReference@bf97a12 +弱引用key:java.lang.ThreadLocal@433619b6,值:abc +弱引用key:java.lang.ThreadLocal@418a15e3,值:java.lang.ref.SoftReference@bf97a12 --gc后-- 弱引用key:null,值:def ``` @@ -162,7 +161,7 @@ new ThreadLocal<>().set(s); 这个问题刚开始看,如果没有过多思考,**弱引用**,还有**垃圾回收**,那么肯定会觉得是`null`。 -其实是不对的,因为题目说的是在做 ``ThreadLocal`.get()` 操作,证明其实还是有**强引用**存在的,所以 `key` 并不为 `null`,如下图所示,`ThreadLocal`的**强引用**仍然是存在的。 +其实是不对的,因为题目说的是在做 `ThreadLocal.get()` 操作,证明其实还是有**强引用**存在的,所以 `key` 并不为 `null`,如下图所示,`ThreadLocal`的**强引用**仍然是存在的。 ![image.png](./images/thread-local/5.png) @@ -217,8 +216,8 @@ public class ThreadLocal { return nextHashCode.getAndAdd(HASH_INCREMENT); } - static class `ThreadLocalMap` { - `ThreadLocalMap`(`ThreadLocal` firstKey, Object firstValue) { + static class ThreadLocalMap { + ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); @@ -230,7 +229,7 @@ public class ThreadLocal { } ``` -每当创建一个`ThreadLocal`对象,这个``ThreadLocal`.nextHashCode` 这个值就会增长 `0x61c88647` 。 +每当创建一个`ThreadLocal`对象,这个`ThreadLocal.nextHashCode` 这个值就会增长 `0x61c88647` 。 这个值很特殊,它是**斐波那契数** 也叫 **黄金分割数**。`hash`增量为 这个数字,带来的好处就是 `hash` **分布非常均匀**。 @@ -244,7 +243,7 @@ public class ThreadLocal { > **注明:** 下面所有示例图中,**绿色块**`Entry`代表**正常数据**,**灰色块**代表`Entry`的`key`值为`null`,**已被垃圾回收**。**白色块**表示`Entry`为`null`。 -虽然`ThreadLocalMap`中使用了**黄金分隔数来**作为`hash`计算因子,大大减少了`Hash`冲突的概率,但是仍然会存在冲突。 +虽然`ThreadLocalMap`中使用了**黄金分割数来**作为`hash`计算因子,大大减少了`Hash`冲突的概率,但是仍然会存在冲突。 `HashMap`中解决冲突的方法是在数组上构造一个**链表**结构,冲突的数据挂载到链表上,如果链表长度超过一定数量则会转化成**红黑树**。 @@ -403,7 +402,7 @@ private static int prevIndex(int i, int len) { `java.lang.ThreadLocal.ThreadLocalMap.replaceStaleEntry()`: ```java -private void replaceStaleEntry(`ThreadLocal` key, Object value, +private void replaceStaleEntry(ThreadLocal key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; @@ -687,7 +686,7 @@ private void resize() { ![](./images/thread-local/27.png) -我们以`get(ThreadLocal1)`为例,通过`hash`计算后,正确的`slot`位置应该是4,而`index=4`的槽位已经有了数据,且`key`值不等于``ThreadLocal`1`,所以需要继续往后迭代查找。 +我们以`get(ThreadLocal1)`为例,通过`hash`计算后,正确的`slot`位置应该是4,而`index=4`的槽位已经有了数据,且`key`值不等于`ThreadLocal1`,所以需要继续往后迭代查找。 迭代到`index=5`的数据时,此时`Entry.key=null`,触发一次探测式数据回收操作,执行`expungeStaleEntry()`方法,执行完后,`index 5,8`的数据都会被回收,而`index 6,7`的数据都会前移,此时继续往后迭代,到`index = 6`的时候即找到了`key`值相等的`Entry`数据,如下图所示: @@ -698,7 +697,7 @@ private void resize() { `java.lang.ThreadLocal.ThreadLocalMap.getEntry()`: ```java -private Entry getEntry(`ThreadLocal` key) { +private Entry getEntry(ThreadLocal key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) @@ -707,7 +706,7 @@ private Entry getEntry(`ThreadLocal` key) { return getEntryAfterMiss(key, i, e); } -private Entry getEntryAfterMiss(`ThreadLocal` key, int i, Entry e) { +private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) { Entry[] tab = table; int len = tab.length; diff --git "a/docs/java/Multithread/\345\210\233\345\273\272\347\272\277\347\250\213\347\232\204\345\207\240\347\247\215\346\226\271\345\274\217\346\200\273\347\273\223.md" "b/docs/java/multi-thread/\345\210\233\345\273\272\347\272\277\347\250\213\347\232\204\345\207\240\347\247\215\346\226\271\345\274\217\346\200\273\347\273\223.md" similarity index 100% rename from "docs/java/Multithread/\345\210\233\345\273\272\347\272\277\347\250\213\347\232\204\345\207\240\347\247\215\346\226\271\345\274\217\346\200\273\347\273\223.md" rename to "docs/java/multi-thread/\345\210\233\345\273\272\347\272\277\347\250\213\347\232\204\345\207\240\347\247\215\346\226\271\345\274\217\346\200\273\347\273\223.md" diff --git "a/docs/java/Multithread/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227.md" "b/docs/java/multi-thread/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227.md" similarity index 100% rename from "docs/java/Multithread/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227.md" rename to "docs/java/multi-thread/\345\244\232\347\272\277\347\250\213\345\255\246\344\271\240\346\214\207\345\215\227.md" diff --git "a/docs/java/Multithread/\345\271\266\345\217\221\345\256\271\345\231\250\346\200\273\347\273\223.md" "b/docs/java/multi-thread/\345\271\266\345\217\221\345\256\271\345\231\250\346\200\273\347\273\223.md" similarity index 100% rename from "docs/java/Multithread/\345\271\266\345\217\221\345\256\271\345\231\250\346\200\273\347\273\223.md" rename to "docs/java/multi-thread/\345\271\266\345\217\221\345\256\271\345\231\250\346\200\273\347\273\223.md" diff --git a/docs/java/Multithread/best-practice-of-threadpool.md "b/docs/java/multi-thread/\346\213\277\346\235\245\345\215\263\347\224\250\347\232\204\347\272\277\347\250\213\346\261\240\346\234\200\344\275\263\345\256\236\350\267\265.md" similarity index 100% rename from docs/java/Multithread/best-practice-of-threadpool.md rename to "docs/java/multi-thread/\346\213\277\346\235\245\345\215\263\347\224\250\347\232\204\347\272\277\347\250\213\346\261\240\346\234\200\344\275\263\345\256\236\350\267\265.md" diff --git "a/docs/java/What's New in JDK8/Java8foreach\346\214\207\345\215\227.md" "b/docs/java/new-features/Java8foreach\346\214\207\345\215\227.md" similarity index 100% rename from "docs/java/What's New in JDK8/Java8foreach\346\214\207\345\215\227.md" rename to "docs/java/new-features/Java8foreach\346\214\207\345\215\227.md" diff --git "a/docs/java/What's New in JDK8/Java8\346\225\231\347\250\213\346\216\250\350\215\220.md" "b/docs/java/new-features/Java8\346\225\231\347\250\213\346\216\250\350\215\220.md" similarity index 100% rename from "docs/java/What's New in JDK8/Java8\346\225\231\347\250\213\346\216\250\350\215\220.md" rename to "docs/java/new-features/Java8\346\225\231\347\250\213\346\216\250\350\215\220.md" diff --git a/docs/java/What's New in JDK8/Java8Tutorial.md "b/docs/java/new-features/Java8\346\226\260\347\211\271\346\200\247\346\200\273\347\273\223.md" similarity index 98% rename from docs/java/What's New in JDK8/Java8Tutorial.md rename to "docs/java/new-features/Java8\346\226\260\347\211\271\346\200\247\346\200\273\347\273\223.md" index 01e71f5ea74..8cf809f75c8 100644 --- a/docs/java/What's New in JDK8/Java8Tutorial.md +++ "b/docs/java/new-features/Java8\346\226\260\347\211\271\346\200\247\346\200\273\347\273\223.md" @@ -15,12 +15,12 @@ - [访问字段和静态变量](#访问字段和静态变量) - [访问默认接口方法](#访问默认接口方法) - [内置函数式接口\(Built-in Functional Interfaces\)](#内置函数式接口built-in-functional-interfaces) - - [Predicates](#predicates) - - [Functions](#functions) - - [Suppliers](#suppliers) - - [Consumers](#consumers) - - [Comparators](#comparators) - - [Optionals](#optionals) + - [Predicate](#predicate) + - [Function](#function) + - [Supplier](#supplier) + - [Consumer](#consumer) + - [Comparator](#comparator) + - [Optional](#optional) - [Streams\(流\)](#streams流) - [Filter\(过滤\)](#filter过滤) - [Sorted\(排序\)](#sorted排序) @@ -287,7 +287,7 @@ JDK 1.8 API包含许多内置函数式接口。 其中一些借口在老版本 但是 Java 8 API 同样还提供了很多全新的函数式接口来让你的编程工作更加方便,有一些接口是来自 [Google Guava](https://code.google.com/p/guava-libraries/) 库里的,即便你对这些很熟悉了,还是有必要看看这些是如何扩展到lambda上使用的。 -#### Predicates +#### Predicate Predicate 接口是只有一个参数的返回布尔类型值的 **断言型** 接口。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非): @@ -340,7 +340,7 @@ Predicate isEmpty = String::isEmpty; Predicate isNotEmpty = isEmpty.negate(); ``` -#### Functions +#### Function Function 接口接受一个参数并生成结果。默认方法可用于将多个函数链接在一起(compose, andThen): @@ -382,7 +382,7 @@ Function backToString = toInteger.andThen(String::valueOf); backToString.apply("123"); // "123" ``` -#### Suppliers +#### Supplier Supplier 接口产生给定泛型类型的结果。 与 Function 接口不同,Supplier 接口不接受参数。 @@ -391,7 +391,7 @@ Supplier personSupplier = Person::new; personSupplier.get(); // new Person ``` -#### Consumers +#### Consumer Consumer 接口表示要对单个输入参数执行的操作。 @@ -400,7 +400,7 @@ Consumer greeter = (p) -> System.out.println("Hello, " + p.firstName); greeter.accept(new Person("Luke", "Skywalker")); ``` -#### Comparators +#### Comparator Comparator 是老Java中的经典接口, Java 8在此之上添加了多种默认方法: @@ -414,9 +414,9 @@ comparator.compare(p1, p2); // > 0 comparator.reversed().compare(p1, p2); // < 0 ``` -## Optionals +## Optional -Optionals不是函数式接口,而是用于防止 NullPointerException 的漂亮工具。这是下一节的一个重要概念,让我们快速了解一下Optionals的工作原理。 +Optional不是函数式接口,而是用于防止 NullPointerException 的漂亮工具。这是下一节的一个重要概念,让我们快速了解一下Optional的工作原理。 Optional 是一个简单的容器,其值可能是null或者不是null。在Java 8之前一般某个函数应该返回非空对象但是有时却什么也没有返回,而在Java 8中,你应该返回 Optional 而不是 null。 diff --git a/docs/java/jdk-new-features/images/fc66979f-7974-40e8-88ae-6dbff15ac9ef.png "b/docs/java/new-features/images/\344\270\200\346\226\207\345\270\246\344\275\240\347\234\213\351\201\215JDK9~14 \347\232\204\351\207\215\350\246\201\346\226\260\347\211\271\346\200\247/java\347\211\210\346\234\254\345\217\221\345\270\203.png" similarity index 100% rename from docs/java/jdk-new-features/images/fc66979f-7974-40e8-88ae-6dbff15ac9ef.png rename to "docs/java/new-features/images/\344\270\200\346\226\207\345\270\246\344\275\240\347\234\213\351\201\215JDK9~14 \347\232\204\351\207\215\350\246\201\346\226\260\347\211\271\346\200\247/java\347\211\210\346\234\254\345\217\221\345\270\203.png" diff --git a/docs/java/jdk-new-features/new-features-from-jdk8-to-jdk14.md "b/docs/java/new-features/\344\270\200\346\226\207\345\270\246\344\275\240\347\234\213\351\201\215JDK9\345\210\26014\347\232\204\351\207\215\350\246\201\346\226\260\347\211\271\346\200\247.md" similarity index 99% rename from docs/java/jdk-new-features/new-features-from-jdk8-to-jdk14.md rename to "docs/java/new-features/\344\270\200\346\226\207\345\270\246\344\275\240\347\234\213\351\201\215JDK9\345\210\26014\347\232\204\351\207\215\350\246\201\346\226\260\347\211\271\346\200\247.md" index 5b193cd5cac..64d70b3abde 100644 --- a/docs/java/jdk-new-features/new-features-from-jdk8-to-jdk14.md +++ "b/docs/java/new-features/\344\270\200\346\226\207\345\270\246\344\275\240\347\234\213\351\201\215JDK9\345\210\26014\347\232\204\351\207\215\350\246\201\346\226\260\347\211\271\346\200\247.md" @@ -109,7 +109,7 @@ Java 10 在现有的 CDS 功能基础上再次拓展,以允许应用类放置 Java11 于 2018 年 9 月 25 日正式发布,这是很重要的一个版本!Java 11 和 2017 年 9 月份发布的 Java 9 以及 2018 年 3 月份发布的 Java 10 相比,其最大的区别就是:在长期支持(Long-Term-Support)方面,**Oracle 表示会对 Java 11 提供大力支持,这一支持将会持续至 2026 年 9 月。这是据 Java 8 以后支持的首个长期版本。** -![](images/fc66979f-7974-40e8-88ae-6dbff15ac9ef.png) +![](images/一文带你看遍JDK9~14 的重要新特性/java版本发布.png) ### 字符串加强 diff --git a/docs/network/images/Cut-Trough-Switching_0.gif b/docs/network/images/Cut-Trough-Switching_0.gif new file mode 100644 index 00000000000..170dc3bbb19 Binary files /dev/null and b/docs/network/images/Cut-Trough-Switching_0.gif differ diff --git a/docs/network/images/isp.png b/docs/network/images/isp.png new file mode 100644 index 00000000000..fe5c0e96adb Binary files /dev/null and b/docs/network/images/isp.png differ diff --git "a/docs/network/images/\344\270\203\345\261\202\344\275\223\347\263\273\347\273\223\346\236\204\345\233\276.png" "b/docs/network/images/\344\270\203\345\261\202\344\275\223\347\263\273\347\273\223\346\236\204\345\233\276.png" new file mode 100644 index 00000000000..a2d24300fcb Binary files /dev/null and "b/docs/network/images/\344\270\203\345\261\202\344\275\223\347\263\273\347\273\223\346\236\204\345\233\276.png" differ diff --git "a/docs/network/images/\344\274\240\350\276\223\345\261\202.png" "b/docs/network/images/\344\274\240\350\276\223\345\261\202.png" new file mode 100644 index 00000000000..192af24536b Binary files /dev/null and "b/docs/network/images/\344\274\240\350\276\223\345\261\202.png" differ diff --git "a/docs/network/images/\345\272\224\347\224\250\345\261\202.png" "b/docs/network/images/\345\272\224\347\224\250\345\261\202.png" new file mode 100644 index 00000000000..31e1e447b73 Binary files /dev/null and "b/docs/network/images/\345\272\224\347\224\250\345\261\202.png" differ diff --git "a/docs/network/images/\346\225\260\346\215\256\351\223\276\350\267\257\345\261\202.png" "b/docs/network/images/\346\225\260\346\215\256\351\223\276\350\267\257\345\261\202.png" new file mode 100644 index 00000000000..c2b51a7c589 Binary files /dev/null and "b/docs/network/images/\346\225\260\346\215\256\351\223\276\350\267\257\345\261\202.png" differ diff --git "a/docs/network/images/\347\211\251\347\220\206\345\261\202.png" "b/docs/network/images/\347\211\251\347\220\206\345\261\202.png" new file mode 100644 index 00000000000..abb979261c1 Binary files /dev/null and "b/docs/network/images/\347\211\251\347\220\206\345\261\202.png" differ diff --git "a/docs/network/images/\347\275\221\347\273\234\345\261\202.png" "b/docs/network/images/\347\275\221\347\273\234\345\261\202.png" new file mode 100644 index 00000000000..376479d7989 Binary files /dev/null and "b/docs/network/images/\347\275\221\347\273\234\345\261\202.png" differ diff --git "a/docs/network/images/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\347\237\245\350\257\206\347\202\271\346\200\273\347\273\223/\344\270\207\347\273\264\347\275\221\347\232\204\345\244\247\350\207\264\345\267\245\344\275\234\345\267\245\347\250\213.png" "b/docs/network/images/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\347\237\245\350\257\206\347\202\271\346\200\273\347\273\223/\344\270\207\347\273\264\347\275\221\347\232\204\345\244\247\350\207\264\345\267\245\344\275\234\345\267\245\347\250\213.png" new file mode 100644 index 00000000000..6af03daa96a Binary files /dev/null and "b/docs/network/images/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\347\237\245\350\257\206\347\202\271\346\200\273\347\273\223/\344\270\207\347\273\264\347\275\221\347\232\204\345\244\247\350\207\264\345\267\245\344\275\234\345\267\245\347\250\213.png" differ diff --git "a/docs/network/\345\271\262\350\264\247\357\274\232\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\347\237\245\350\257\206\346\200\273\347\273\223.md" "b/docs/network/\345\271\262\350\264\247\357\274\232\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\347\237\245\350\257\206\346\200\273\347\273\223.md" deleted file mode 100644 index a20e6f85be7..00000000000 --- "a/docs/network/\345\271\262\350\264\247\357\274\232\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\347\237\245\350\257\206\346\200\273\347\273\223.md" +++ /dev/null @@ -1,420 +0,0 @@ -### 1. [计算机概述 ](#一计算机概述) -### 2. [物理层 ](#二物理层) -### 3. [数据链路层 ](#三数据链路层 ) -### 4. [网络层 ](#四网络层 ) -### 5. [运输层 ](#五运输层 ) -### 6. [应用层](#六应用层) - - -## 一计算机概述 -### (1),基本术语 - -#### 结点 (node): - - 网络中的结点可以是计算机,集线器,交换机或路由器等。 -#### 链路(link ): - - 从一个结点到另一个结点的一段物理线路。中间没有任何其他交点。 -#### 主机(host): - 连接在因特网上的计算机. -#### ISP(Internet Service Provider): - 因特网服务提供者(提供商). -#### IXP(Internet eXchange Point): - 互联网交换点IXP的主要作用就是允许两个网络直接相连并交换分组,而不需要再通过第三个网络来转发分组。. -#### RFC(Request For Comments) - 意思是“请求评议”,包含了关于Internet几乎所有的重要的文字资料。 -#### 广域网WAN(Wide Area Network) - 任务是通过长距离运送主机发送的数据 -#### 城域网MAN(Metropolitan Area Network) - 用来将多个局域网进行互连 - -#### 局域网LAN(Local Area Network) - 学校或企业大多拥有多个互连的局域网 -#### 个人区域网PAN(Personal Area Network) - 在个人工作的地方把属于个人使用的电子设备用无线技术连接起来的网络 -#### 端系统(end system): - 处在因特网边缘的部分即是连接在因特网上的所有的主机. -#### 分组(packet ): - 因特网中传送的数据单元。由首部header和数据段组成。分组又称为包,首部可称为包头。 -#### 存储转发(store and forward ): - 路由器收到一个分组,先存储下来,再检查其首部,查找转发表,按照首部中的目的地址,找到合适的接口转发出去。 -#### 带宽(bandwidth): - 在计算机网络中,表示在单位时间内从网络中的某一点到另一点所能通过的“最高数据率”。常用来表示网络的通信线路所能传送数据的能力。单位是“比特每秒”,记为b/s。 -#### 吞吐量(throughput ): - 表示在单位时间内通过某个网络(或信道、接口)的数据量。吞吐量更经常地用于对现实世界中的网络的一种测量,以便知道实际上到底有多少数据量能够通过网络。吞吐量受网络的带宽或网络的额定速率的限制。 - -### (2),重要知识点总结 - - 1,计算机网络(简称网络)把许多计算机连接在一起,而互联网把许多网络连接在一起,是网络的网络。 - - 2,小写字母i开头的internet(互联网)是通用名词,它泛指由多个计算机网络相互连接而成的网络。在这些网络之间的通信协议(即通信规则)可以是任意的。 - - 大写字母I开头的Internet(互联网)是专用名词,它指全球最大的,开放的,由众多网络相互连接而成的特定的互联网,并采用TCP/IP协议作为通信规则,其前身为ARPANET。Internet的推荐译名为因特网,现在一般流行称为互联网。 - - 3,路由器是实现分组交换的关键构件,其任务是转发收到的分组,这是网络核心部分最重要的功能。分组交换采用存储转发技术,表示把一个报文(要发送的整块数据)分为几个分组后再进行传送。在发送报文之前,先把较长的报文划分成为一个个更小的等长数据段。在每个数据端的前面加上一些由必要的控制信息组成的首部后,就构成了一个分组。分组又称为包。分组是在互联网中传送的数据单元,正是由于分组的头部包含了诸如目的地址和源地址等重要控制信息,每一个分组才能在互联网中独立的选择传输路径,并正确地交付到分组传输的终点。 - -4,互联网按工作方式可划分为边缘部分和核心部分。主机在网络的边缘部分,其作用是进行信息处理。由大量网络和连接这些网络的路由器组成核心部分,其作用是提供连通性和交换。 - - 5,计算机通信是计算机中进程(即运行着的程序)之间的通信。计算机网络采用的通信方式是客户-服务器方式(C/S方式)和对等连接方式(P2P方式)。 - - 6,客户和服务器都是指通信中所涉及的应用进程。客户是服务请求方,服务器是服务提供方。 - -7,按照作用范围的不同,计算机网络分为广域网WAN,城域网MAN,局域网LAN,个人区域网PAN。 - - 8,计算机网络最常用的性能指标是:速率,带宽,吞吐量,时延(发送时延,处理时延,排队时延),时延带宽积,往返时间和信道利用率。 - - 9,网络协议即协议,是为进行网络中的数据交换而建立的规则。计算机网络的各层以及其协议集合,称为网络的体系结构。 - - 10,五层体系结构由应用层,运输层,网络层(网际层),数据链路层,物理层组成。运输层最主要的协议是TCP和UDP协议,网络层最重要的协议是IP协议。 - -## 二物理层 -### (1),基本术语 -#### 数据(data): - 运送消息的实体。 -#### 信号(signal): - 数据的电气的或电磁的表现。或者说信号是适合在传输介质上传输的对象。 -#### 码元( code): - 在使用时间域(或简称为时域)的波形来表示数字信号时,代表不同离散数值的基本波形。 -#### 单工(simplex ): - 只能有一个方向的通信而没有反方向的交互。 -#### 半双工(half duplex ): - 通信的双方都可以发送信息,但不能双方同时发送(当然也就不能同时接收)。 -#### 全双工(full duplex): - 通信的双方可以同时发送和接收信息。 -#### 奈氏准则: - 在任何信道中,码元的传输的效率是有上限的,传输速率超过此上限,就会出现严重的码间串扰问题,使接收端对码元的判决(即识别)成为不可能。 -#### 基带信号(baseband signal): - 来自信源的信号。指没有经过调制的数字信号或模拟信号。 -#### 带通(频带)信号(bandpass signal): - 把基带信号经过载波调制后,把信号的频率范围搬移到较高的频段以便在信道中传输(即仅在一段频率范围内能够通过信道),这里调制过后的信号就是带通信号。 -#### 调制(modulation ): - 对信号源的信息进行处理后加到载波信号上,使其变为适合在信道传输的形式的过程。 -#### 信噪比(signal-to-noise ratio ): - 指信号的平均功率和噪声的平均功率之比,记为S/N。信噪比(dB)=10*log10(S/N) -#### 信道复用(channel multiplexing ): - 指多个用户共享同一个信道。(并不一定是同时) -#### 比特率(bit rate ): - 单位时间(每秒)内传送的比特数。 -#### 波特率(baud rate): - 单位时间载波调制状态改变的次数。针对数据信号对载波的调制速率。 -#### 复用(multiplexing): - 共享信道的方法 -#### ADSL(Asymmetric Digital Subscriber Line ): - 非对称数字用户线。 -#### 光纤同轴混合网(HFC网): - 在目前覆盖范围很广的有线电视网的基础上开发的一种居民宽带接入网 - -### (2),重要知识点总结 - - 1,物理层的主要任务就是确定与传输媒体接口有关的一些特性,如机械特性,电气特性,功能特性,过程特性。 - - 2,一个数据通信系统可划分为三大部分,即源系统,传输系统,目的系统。源系统包括源点(或源站,信源)和发送器,目的系统包括接收器和终点。 - - 3,通信的目的是传送消息。如话音,文字,图像等都是消息,数据是运送消息的实体。信号则是数据的电器或电磁的表现。 - - 4,根据信号中代表消息的参数的取值方式不同,信号可分为模拟信号(或连续信号)和数字信号(或离散信号)。在使用时间域(简称时域)的波形表示数字信号时,代表不同离散数值的基本波形称为码元。 - - 5,根据双方信息交互的方式,通信可划分为单向通信(或单工通信),双向交替通信(或半双工通信),双向同时通信(全双工通信)。 - - 6,来自信源的信号称为基带信号。信号要在信道上传输就要经过调制。调制有基带调制和带通调制之分。最基本的带通调制方法有调幅,调频和调相。还有更复杂的调制方法,如正交振幅调制。 - - 7,要提高数据在信道上的传递速率,可以使用更好的传输媒体,或使用先进的调制技术。但数据传输速率不可能任意被提高。 - - 8,传输媒体可分为两大类,即导引型传输媒体(双绞线,同轴电缆,光纤)和非导引型传输媒体(无线,红外,大气激光)。 - - 9,为了有效利用光纤资源,在光纤干线和用户之间广泛使用无源光网络PON。无源光网络无需配备电源,其长期运营成本和管理成本都很低。最流行的无源光网络是以太网无源光网络EPON和吉比特无源光网络GPON。 - -### (3),最重要的知识点 -#### **①,物理层的任务** - 透明地传送比特流。也可以将物理层的主要任务描述为确定与传输媒体的接口的一些特性,即:机械特性(接口所用接线器的一些物理属性如形状尺寸),电气特性(接口电缆的各条线上出现的电压的范围),功能特性(某条线上出现的某一电平的电压的意义),过程特性(对于不同功能能的各种可能事件的出现顺序)。 - -#### 拓展: - 物理层考虑的是怎样才能在连接各种计算机的传输媒体上传输数据比特流,而不是指具体的传输媒体。现有的计算机网络中的硬件设备和传输媒体的种类非常繁多,而且通信手段也有许多不同的方式。物理层的作用正是尽可能地屏蔽掉这些传输媒体和通信手段的差异,使物理层上面的数据链路层感觉不到这些差异,这样就可以使数据链路层只考虑完成本层的协议和服务,而不必考虑网络的具体传输媒体和通信手段是什么。 - -#### **②,几种常用的信道复用技术** -![这里写图片描述](https://user-gold-cdn.xitu.io/2018/4/1/1627f7a170ec6611?w=1247&h=425&f=png&s=36746) - -### **③,几种常用的宽带接入技术,主要是ADSL和FTTx** - 用户到互联网的宽带接入方法有非对称数字用户线ADSL(用数字技术对现有的模拟电话线进行改造,而不需要重新布线。ASDL的快速版本是甚高速数字用户线VDSL。),光纤同轴混合网HFC(是在目前覆盖范围很广的有线电视网的基础上开发的一种居民宽带接入网)和FTTx(即光纤到······)。 - -## 三数据链路层 -### (1),基本术语 - -#### 链路(link): - 一个结点到相邻结点的一段物理链路 -#### 数据链路(data link): - 把实现控制数据运输的协议的硬件和软件加到链路上就构成了数据链路 -#### 循环冗余检验CRC(Cyclic Redundancy Check): - 为了保证数据传输的可靠性,CRC是数据链路层广泛使用的一种检错技术 -#### 帧(frame): - 一个数据链路层的传输单元,由一个数据链路层首部和其携带的封包所组成协议数据单元。 -#### MTU(Maximum Transfer Uint ): - 最大传送单元。帧的数据部分的的长度上限。 -#### 误码率BER(Bit Error Rate ): - 在一段时间内,传输错误的比特占所传输比特总数的比率。 -#### PPP(Point-to-Point Protocol ): - 点对点协议。即用户计算机和ISP进行通信时所使用的数据链路层协议。以下是PPP帧的示意图: -![PPP](https://user-gold-cdn.xitu.io/2018/4/1/1627f8291c6b032c?w=624&h=359&f=jpeg&s=44271) -#### MAC地址(Media Access Control或者Medium Access Control): - 意译为媒体访问控制,或称为物理地址、硬件地址,用来定义网络设备的位置。 - 在OSI模型中,第三层网络层负责 IP地址,第二层数据链路层则负责 MAC地址。 - 因此一个主机会有一个MAC地址,而每个网络位置会有一个专属于它的IP地址 。 - 地址是识别某个系统的重要标识符,“名字指出我们所要寻找的资源,地址指出资源所在的地方,路由告诉我们如何到达该处” -#### 网桥(bridge): - 一种用于数据链路层实现中继,连接两个或多个局域网的网络互连设备。 -#### 交换机(switch ): - 广义的来说,交换机指的是一种通信系统中完成信息交换的设备。这里工作在数据链路层的交换机指的是交换式集线器,其实质是一个多接口的网桥 - - -### (2),重要知识点总结 - -1,链路是从一个结点到相邻节点的一段物理链路,数据链路则在链路的基础上增加了一些必要的硬件(如网络适配器)和软件(如协议的实现) - -2,数据链路层使用的主要是**点对点信道**和**广播信道**两种。 - -3,数据链路层传输的协议数据单元是帧。数据链路层的三个基本问题是:**封装成帧**,**透明传输**和**差错检测** - -4,**循环冗余检验CRC**是一种检错方法,而帧检验序列FCS是添加在数据后面的冗余码 - -5,**点对点协议PPP**是数据链路层使用最多的一种协议,它的特点是:简单,只检测差错而不去纠正差错,不使用序号,也不进行流量控制,可同时支持多种网络层协议 - - 6,PPPoE是为宽带上网的主机使用的链路层协议 - -7,局域网的优点是:具有广播功能,从一个站点可方便地访问全网;便于系统的扩展和逐渐演变;提高了系统的可靠性,可用性和生存性。 - -8,共向媒体通信资源的方法有二:一是静态划分信道(各种复用技术),而是动态媒体接入控制,又称为多点接入(随即接入或受控接入) - -9,计算机与外接局域网通信需要通过通信适配器(或网络适配器),它又称为网络接口卡或网卡。**计算器的硬件地址就在适配器的ROM中**。 - -10,以太网采用的无连接的工作方式,对发送的数据帧不进行编号,也不要求对方发回确认。目的站收到有差错帧就把它丢掉,其他什么也不做 - -11,以太网采用的协议是具有冲突检测的**载波监听多点接入CSMA/CD**。协议的特点是:**发送前先监听,边发送边监听,一旦发现总线上出现了碰撞,就立即停止发送。然后按照退避算法等待一段随机时间后再次发送。** 因此,每一个站点在自己发送数据之后的一小段时间内,存在这遭遇碰撞的可能性。以太网上的各站点平等的争用以太网信道 - -12,以太网的适配器具有过滤功能,它只接收单播帧,广播帧和多播帧。 - -13,使用集线器可以在物理层扩展以太网(扩展后的以太网仍然是一个网络) -### (3),最重要的知识点 -#### ① 数据链路层的点对点信道和广播信道的特点,以及这两种信道所使用的协议(PPP协议以及CSMA/CD协议)的特点 -#### ② 数据链路层的三个基本问题:**封装成帧**,**透明传输**,**差错检测** -#### ③ 以太网的MAC层硬件地址 -#### ④ 适配器,转发器,集线器,网桥,以太网交换机的作用以及适用场合 - -## 四网络层 -### (1),基本术语 - -#### 虚电路(Virtual Circuit): - 在两个终端设备的逻辑或物理端口之间,通过建立的双向的透明传输通道。虚电路表示这只是一条逻辑上的连接,分组都沿着这条逻辑连接按照存储转发方式传送,而并不是真正建立了一条物理连接。 -#### IP(Internet Protocol ): - 网际协议 IP 是 TCP/IP体系中两个最主要的协议之一,是TCP/IP体系结构网际层的核心。配套的有ARP,RARP,ICMP,IGMP。 - ![这里写图片描述](https://user-gold-cdn.xitu.io/2018/4/1/1627f92f98436286?w=453&h=331&f=jpeg&s=27535) -#### ARP(Address Resolution Protocol): - 地址解析协议 -#### ICMP(Internet Control Message Protocol ): - 网际控制报文协议 (ICMP 允许主机或路由器报告差错情况和提供有关异常情况的报告。) -#### 子网掩码(subnet mask ): - 它是一种用来指明一个IP地址的哪些位标识的是主机所在的子网以及哪些位标识的是主机的位掩码。子网掩码不能单独存在,它必须结合IP地址一起使用。 -#### CIDR( Classless Inter-Domain Routing ): - 无分类域间路由选择 (特点是消除了传统的 A 类、B 类和 C 类地址以及划分子网的概念,并使用各种长度的“网络前缀”(network-prefix)来代替分类地址中的网络号和子网号) -#### 默认路由(default route): - 当在路由表中查不到能到达目的地址的路由时,路由器选择的路由。默认路由还可以减小路由表所占用的空间和搜索路由表所用的时间。 -#### 路由选择算法(Virtual Circuit): - 路由选择协议的核心部分。因特网采用自适应的,分层次的路由选择协议。 - -### (2),重要知识点总结 -1,TCP/IP协议中的网络层向上只提供简单灵活的,无连接的,尽最大努力交付的数据报服务。网络层不提供服务质量的承诺,不保证分组交付的时限所传送的分组可能出错,丢失,重复和失序。进程之间通信的可靠性由运输层负责 - -2,在互联网的交付有两种,一是在本网络直接交付不用经过路由器,另一种是和其他网络的间接交付,至少经过一个路由器,但最后一次一定是直接交付 - -3,分类的IP地址由网络号字段(指明网络)和主机号字段(指明主机)组成。网络号字段最前面的类别指明IP地址的类别。IP地址是一种分等级的地址结构。IP地址管理机构分配IP地址时只分配网络号,主机号由得到该网络号的单位自行分配。路由器根据目的主机所连接的网络号来转发分组。一个路由器至少连接到两个网络,所以一个路由器至少应当有两个不同的IP地址 - -4,IP数据报分为首部和数据两部分。首部的前一部分是固定长度,共20字节,是所有IP数据包必须具有的(源地址,目的地址,总长度等重要地段都固定在首部)。一些长度可变的可选字段固定在首部的后面。IP首部中的生存时间给出了IP数据报在互联网中所能经过的最大路由器数。可防止IP数据报在互联网中无限制的兜圈子。 - -5,地址解析协议ARP把IP地址解析为硬件地址。ARP的高速缓存可以大大减少网络上的通信量。因为这样可以使主机下次再与同样地址的主机通信时,可以直接从高速缓存中找到所需要的硬件地址而不需要再去广播方式发送ARP请求分组 - -6,无分类域间路由选择CIDR是解决目前IP地址紧缺的一个好办法。CIDR记法把IP地址后面加上斜线“/”,然后写上前缀所所占的位数。前缀(或网络前缀用来指明网络),前缀后面的部分是后缀,用来指明主机。CIDR把前缀都相同的连续的IP地址组成一个“CIDR地址块”,IP地址分配都以CIDR地址块为单位。 - -7, 网际控制报文协议是IP层的协议.ICMP报文作为IP数据报的数据,加上首部后组成IP数据报发送出去。使用ICMP数据报并不是为了实现可靠传输。ICMP允许主机或路由器报告差错情况和提供有关异常情况的报告。ICMP报文的种类有两种 ICMP差错报告报文和ICMP询问报文。 - -8,要解决IP地址耗尽的问题,最根本的办法是采用具有更大地址空间的新版本IP协议-IPv6。IPv6所带来的变化有①更大的地址空间(采用128位地址)②灵活的首部格式③改进的选项④支持即插即用⑤支持资源的预分配⑥IPv6的首部改为8字节对齐。另外IP数据报的目的地址可以是以下三种基本类型地址之一:单播,多播和任播 - -9,虚拟专用网络VPN利用公用的互联网作为本机构专用网之间的通信载体。VPN内使用互联网的专用地址。一个VPN至少要有一个路由器具有合法的全球IP地址,这样才能和本系统的另一个VPN通过互联网进行通信。所有通过互联网传送的数据都需要加密 - -10, MPLS的特点是:①支持面向连接的服务质量②支持流量工程,平衡网络负载③有效的支持虚拟专用网VPN。MPLS在入口节点给每一个IP数据报打上固定长度的“标记”,然后根据标记在第二层(链路层)用硬件进行转发(在标记交换路由器中进行标记交换),因而转发速率大大加快。 - -### (3),最重要知识点 -#### ① 虚拟互联网络的概念 -#### ② IP地址和物理地址的关系 -#### ③ 传统的分类的IP地址(包括子网掩码)和无分类域间路由选择CIDR -#### ④ 路由选择协议的工作原理 - -## 五运输层 - -### (1),基本术语 - -#### 进程(process): - 指计算机中正在运行的程序实体 -#### 应用进程互相通信: - 一台主机的进程和另一台主机中的一个进程交换数据的过程(另外注意通信真正的端点不是主机而是主机中的进程,也就是说端到端的通信是应用进程之间的通信) -#### 传输层的复用与分用: - 复用指发送方不同的进程都可以通过统一个运输层协议传送数据。分用指接收方的运输层在剥去报文的首部后能把这些数据正确的交付到目的应用进程。 -#### TCP(Transmission Control Protocol): - 传输控制协议 -#### UDP(User Datagram Protocol): - 用户数据报协议 -#### 端口(port)(link): - 端口的目的是为了确认对方机器是那个进程在于自己进行交互,比如MSN和QQ的端口不同,如果没有端口就可能出现QQ进程和MSN交互错误。端口又称协议端口号。 -#### 停止等待协议(link): - 指发送方每发送完一个分组就停止发送,等待对方确认,在收到确认之后在发送下一个分组。 -#### 流量控制(link): - 就是让发送方的发送速率不要太快,既要让接收方来得及接收,也不要使网络发生拥塞。 -#### 拥塞控制(link): - 防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载。拥塞控制所要做的都有一个前提,就是网络能够承受现有的网络负荷。 - - -### (2),重要知识点总结 - -1,运输层提供应用进程之间的逻辑通信,也就是说,运输层之间的通信并不是真正在两个运输层之间直接传输数据。运输层向应用层屏蔽了下面网络的细节(如网络拓补,所采用的路由选择协议等),它使应用进程之间看起来好像两个运输层实体之间有一条端到端的逻辑通信信道。 - -2,网络层为主机提供逻辑通信,而运输层为应用进程之间提供端到端的逻辑通信。 - -3,运输层的两个重要协议是用户数据报协议UDP和传输控制协议TCP。按照OSI的术语,两个对等运输实体在通信时传送的数据单位叫做运输协议数据单元TPDU(Transport Protocol Data Unit)。但在TCP/IP体系中,则根据所使用的协议是TCP或UDP,分别称之为TCP报文段或UDP用户数据报。 - -4,UDP在传送数据之前不需要先建立连接,远地主机在收到UDP报文后,不需要给出任何确认。虽然UDP不提供可靠交付,但在某些情况下UDP确是一种最有效的工作方式。 TCP提供面向连接的服务。在传送数据之前必须先建立连接,数据传送结束后要释放连接。TCP不提供广播或多播服务。由于TCP要提供可靠的,面向连接的传输服务,这一难以避免增加了许多开销,如确认,流量控制,计时器以及连接管理等。这不仅使协议数据单元的首部增大很多,还要占用许多处理机资源。 - -5,硬件端口是不同硬件设备进行交互的接口,而软件端口是应用层各种协议进程与运输实体进行层间交互的一种地址。UDP和TCP的首部格式中都有源端口和目的端口这两个重要字段。当运输层收到IP层交上来的运输层报文时,就能够 根据其首部中的目的端口号把数据交付应用层的目的应用层。(两个进程之间进行通信不光要知道对方IP地址而且要知道对方的端口号(为了找到对方计算机中的应用进程)) - -6,运输层用一个16位端口号标志一个端口。端口号只有本地意义,它只是为了标志计算机应用层中的各个进程在和运输层交互时的层间接口。在互联网的不同计算机中,相同的端口号是没有关联的。协议端口号简称端口。虽然通信的终点是应用进程,但只要把所发送的报文交到目的主机的某个合适端口,剩下的工作(最后交付目的进程)就由TCP和UDP来完成。 - -7,运输层的端口号分为服务器端使用的端口号(0~1023指派给熟知端口,1024~49151是登记端口号)和客户端暂时使用的端口号(49152~65535) - -8,UDP的主要特点是①无连接②尽最大努力交付③面向报文④无拥塞控制⑤支持一对一,一对多,多对一和多对多的交互通信⑥首部开销小(只有四个字段:源端口,目的端口,长度和检验和) - -9,TCP的主要特点是①面向连接②每一条TCP连接只能是一对一的③提供可靠交付④提供全双工通信⑤面向字节流 - -10,TCP用主机的IP地址加上主机上的端口号作为TCP连接的端点。这样的端点就叫做套接字(socket)或插口。套接字用(IP地址:端口号)来表示。每一条TCP连接唯一被通信两端的两个端点所确定。 - - 11,停止等待协议是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组。 - -12,为了提高传输效率,发送方可以不使用低效率的停止等待协议,而是采用流水线传输。流水线传输就是发送方可连续发送多个分组,不必每发完一个分组就停下来等待对方确认。这样可使信道上一直有数据不间断的在传送。这种传输方式可以明显提高信道利用率。 - -13,停止等待协议中超时重传是指只要超过一段时间仍然没有收到确认,就重传前面发送过的分组(认为刚才发送过的分组丢失了)。因此每发送完一个分组需要设置一个超时计时器,其重转时间应比数据在分组传输的平均往返时间更长一些。这种自动重传方式常称为自动重传请求ARQ。另外在停止等待协议中若收到重复分组,就丢弃该分组,但同时还要发送确认。连续ARQ协议可提高信道利用率。发送维持一个发送窗口,凡位于发送窗口内的分组可连续发送出去,而不需要等待对方确认。接收方一般采用累积确认,对按序到达的最后一个分组发送确认,表明到这个分组位置的所有分组都已经正确收到了。 - -14,TCP报文段的前20个字节是固定的,后面有4n字节是根据需要增加的选项。因此,TCP首部的最小长度是20字节。 - -15,TCP使用滑动窗口机制。发送窗口里面的序号表示允许发送的序号。发送窗口后沿的后面部分表示已发送且已收到确认,而发送窗口前沿的前面部分表示不允许发送。发送窗口后沿的变化情况有两种可能,即不动(没有收到新的确认)和前移(收到了新的确认)。发送窗口的前沿通常是不断向前移动的。一般来说,我们总是希望数据传输更快一些。但如果发送方把数据发送的过快,接收方就可能来不及接收,这就会造成数据的丢失。所谓流量控制就是让发送方的发送速率不要太快,要让接收方来得及接收。 - -16,在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏。这种情况就叫拥塞。拥塞控制就是为了防止过多的数据注入到网络中,这样就可以使网络中的路由器或链路不致过载。拥塞控制所要做的都有一个前提,就是网络能够承受现有的网络负荷。拥塞控制是一个全局性的过程,涉及到所有的主机,所有的路由器,以及与降低网络传输性能有关的所有因素。相反,流量控制往往是点对点通信量的控制,是个端到端的问题。流量控制所要做到的就是抑制发送端发送数据的速率,以便使接收端来得及接收。 - -17,为了进行拥塞控制,TCP发送方要维持一个拥塞窗口cwnd的状态变量。拥塞控制窗口的大小取决于网络的拥塞程度,并且动态变化。发送方让自己的发送窗口取为拥塞窗口和接收方的接受窗口中较小的一个。 - -18,TCP的拥塞控制采用了四种算法,即慢开始,拥塞避免,快重传和快恢复。在网络层也可以使路由器采用适当的分组丢弃策略(如主动队列管理AQM),以减少网络拥塞的发生。 - -19,运输连接的三个阶段,即:连接建立,数据传送和连接释放。 - -20,主动发起TCP连接建立的应用进程叫做客户,而被动等待连接建立的应用进程叫做服务器。TCP连接采用三报文握手机制。服务器要确认用户的连接请求,然后客户要对服务器的确认进行确认。 - -21,TCP的连接释放采用四报文握手机制。任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送时,则发送连接释放通知,对方确认后就完全关闭了TCP连接 -### (3),最重要的知识点 -#### ① 端口和套接字的意义 - -#### ② 无连接UDP的特点 - -#### ③ 面向连接TCP的特点 - -#### ④ 在不可靠的网络上实现可靠传输的工作原理,停止等待协议和ARQ协议 - -#### ① TCP的滑动窗口,流量控制,拥塞控制和连接管理 - -## 六应用层 -### (1),基本术语 -#### 域名系统(DNS): - DNS(Domain Name System,域名系统),万维网上作为域名和IP地址相互映射的一个分布式数据库,能够使用户更方便的访问互联网,而不用去记住能够被机器直接读取的IP数串。 - 通过域名,最终得到该域名对应的IP地址的过程叫做域名解析(或主机名解析)。DNS协议运行在UDP协议之上,使用端口号53。在RFC文档中RFC 2181对DNS有规范说明,RFC 2136对DNS的动态更新进行说明,RFC 2308对DNS查询的反向缓存进行说明。 -#### 文件传输协议(FTP): - FTP 是File TransferProtocol(文件传输协议)的英文简称,而中文简称为“文传协议”。用于Internet上的控制文件的双向传输。同时,它也是一个应用程序(Application)。 - 基于不同的操作系统有不同的FTP应用程序,而所有这些应用程序都遵守同一种协议以传输文件。在FTP的使用当中,用户经常遇到两个概念:"下载"(Download)和"上传"(Upload)。 - "下载"文件就是从远程主机拷贝文件至自己的计算机上;"上传"文件就是将文件从自己的计算机中拷贝至远程主机上。用Internet语言来说,用户可通过客户机程序向(从)远程主机上传(下载)文件。 - -#### 简单文件传输协议(TFTP): - TFTP(Trivial File Transfer Protocol,简单文件传输协议)是TCP/IP协议族中的一个用来在客户机与服务器之间进行简单文件传输的协议,提供不复杂、开销不大的文件传输服务。端口号为69。 - -#### 远程终端协议(TELENET): - Telnet协议是TCP/IP协议族中的一员,是Internet远程登陆服务的标准协议和主要方式。它为用户提供了在本地计算机上完成远程主机工作的能力。 - 在终端使用者的电脑上使用telnet程序,用它连接到服务器。终端使用者可以在telnet程序中输入命令,这些命令会在服务器上运行,就像直接在服务器的控制台上输入一样。 - 可以在本地就能控制服务器。要开始一个telnet会话,必须输入用户名和密码来登录服务器。Telnet是常用的远程控制Web服务器的方法。 - - -#### 万维网(WWW): - WWW是环球信息网的缩写,(亦作“Web”、“WWW”、“'W3'”,英文全称为“World Wide Web”),中文名字为“万维网”,"环球网"等,常简称为Web。分为Web客户端和Web服务器程序。 - WWW可以让Web客户端(常用浏览器)访问浏览Web服务器上的页面。是一个由许多互相链接的超文本组成的系统,通过互联网访问。在这个系统中,每个有用的事物,称为一样“资源”;并且由一个全局“统一资源标识符”(URI)标识;这些资源通过超文本传输协议(Hypertext Transfer Protocol)传送给用户,而后者通过点击链接来获得资源。 - 万维网联盟(英语:World Wide Web Consortium,简称W3C),又称W3C理事会。1994年10月在麻省理工学院(MIT)计算机科学实验室成立。万维网联盟的创建者是万维网的发明者蒂姆·伯纳斯-李。 - 万维网并不等同互联网,万维网只是互联网所能提供的服务其中之一,是靠着互联网运行的一项服务。 -#### 万维网的大致工作工程: -![万维网的大致工作工程](https://user-gold-cdn.xitu.io/2018/4/1/1627ff96a96087af?w=839&h=610&f=jpeg&s=86703) - -#### 统一资源定位符(URL): - 统一资源定位符是对可以从互联网上得到的资源的位置和访问方法的一种简洁的表示,是互联网上标准资源的地址。互联网上的每个文件都有一个唯一的URL,它包含的信息指出文件的位置以及浏览器应该怎么处理它。 - -#### 超文本传输协议(HTTP): - 超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。所有的WWW文件都必须遵守这个标准。 - 设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法。1960年美国人Ted Nelson构思了一种通过计算机处理文本信息的方法,并称之为超文本(hypertext),这成为了HTTP超文本传输协议标准架构的发展根基。 - -#### 代理服务器(Proxy Server): - 代理服务器(Proxy Server)是一种网络实体,它又称为万维网高速缓存。 - 代理服务器把最近的一些请求和响应暂存在本地磁盘中。当新请求到达时,若代理服务器发现这个请求与暂时存放的的请求相同,就返回暂存的响应,而不需要按URL的地址再次去互联网访问该资源。 - 代理服务器可在客户端或服务器工作,也可以在中间系统工作。 - -#### http请求头: - http请求头,HTTP客户程序(例如浏览器),向服务器发送请求的时候必须指明请求类型(一般是GET或者POST)。如有必要,客户程序还可以选择发送其他的请求头。 - - Accept:浏览器可接受的MIME类型。 - - Accept-Charset:浏览器可接受的字符集。 - - Accept-Encoding:浏览器能够进行解码的数据编码方式,比如gzip。Servlet能够向支持gzip的浏览器返回经gzip编码的HTML页面。许多情形下这可以减少5到10倍的下载时间。 - - Accept-Language:浏览器所希望的语言种类,当服务器能够提供一种以上的语言版本时要用到。 - - Authorization:授权信息,通常出现在对服务器发送的WWW-Authenticate头的应答中。 - - Connection:表示是否需要持久连接。如果Servlet看到这里的值为“Keep-Alive”,或者看到请求使用的是HTTP 1.1(HTTP 1.1默认进行持久连接),它就可以利用持久连接的优点,当页面包含多个元素时(例如Applet,图片),显著地减少下载所需要的时间。要实现这一点,Servlet需要在应答中发送一个Content-Length头,最简单的实现方法是:先把内容写入ByteArrayOutputStream,然后在正式写出内容之前计算它的大小。 - - Content-Length:表示请求消息正文的长度。 - - Cookie:这是最重要的请求头信息之一 - - From:请求发送者的email地址,由一些特殊的Web客户程序使用,浏览器不会用到它。 - - Host:初始URL中的主机和端口。 - - If-Modified-Since:只有当所请求的内容在指定的日期之后又经过修改才返回它,否则返回304“Not Modified”应答。 - - Pragma:指定“no-cache”值表示服务器必须返回一个刷新后的文档,即使它是代理服务器而且已经有了页面的本地拷贝。 - - Referer:包含一个URL,用户从该URL代表的页面出发访问当前请求的页面。 - - User-Agent:浏览器类型,如果Servlet返回的内容与浏览器类型有关则该值非常有用。 -#### 简单邮件传输协议(SMTP): - SMTP(Simple Mail Transfer Protocol)即简单邮件传输协议,它是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。 - SMTP协议属于TCP/IP协议簇,它帮助每台计算机在发送或中转信件时找到下一个目的地。 - 通过SMTP协议所指定的服务器,就可以把E-mail寄到收信人的服务器上了,整个过程只要几分钟。SMTP服务器则是遵循SMTP协议的发送邮件服务器,用来发送或中转发出的电子邮件。 - -#### 搜索引擎: - 搜索引擎(Search Engine)是指根据一定的策略、运用特定的计算机程序从互联网上搜集信息,在对信息进行组织和处理后,为用户提供检索服务,将用户检索相关的信息展示给用户的系统。 - 搜索引擎包括全文索引、目录索引、元搜索引擎、垂直搜索引擎、集合式搜索引擎、门户搜索引擎与免费链接列表等。 -#### 全文索引: - 全文索引技术是目前搜索引擎的关键技术。 - 试想在1M大小的文件中搜索一个词,可能需要几秒,在100M的文件中可能需要几十秒,如果在更大的文件中搜索那么就需要更大的系统开销,这样的开销是不现实的。 - 所以在这样的矛盾下出现了全文索引技术,有时候有人叫倒排文档技术。 -#### 目录索引: - 目录索引( search index/directory),顾名思义就是将网站分门别类地存放在相应的目录中,因此用户在查询信息时,可选择关键词搜索,也可按分类目录逐层查找。 - - -#### 垂直搜索引擎: - 垂直搜索引擎是针对某一个行业的专业搜索引擎,是搜索引擎的细分和延伸,是对网页库中的某类专门的信息进行一次整合,定向分字段抽取出需要的数据进行处理后再以某种形式返回给用户。 - 垂直搜索是相对通用搜索引擎的信息量大、查询不准确、深度不够等提出来的新的搜索引擎服务模式,通过针对某一特定领域、某一特定人群或某一特定需求提供的有一定价值的信息和相关服务。 - 其特点就是“专、精、深”,且具有行业色彩,相比较通用搜索引擎的海量信息无序化,垂直搜索引擎则显得更加专注、具体和深入。 - -### (2),重要知识点总结 -1,文件传输协议(FTP)使用TCP可靠的运输服务。FTP使用客户服务器方式。一个FTP服务器进程可以同时为多个用户提供服务。在进进行文件传输时,FTP的客户和服务器之间要先建立两个并行的TCP连接:控制连接和数据连接。实际用于传输文件的是数据连接。 - -2,万维网客户程序与服务器之间进行交互使用的协议是超文本传输协议HTTP。HTTP使用TCP连接进行可靠传输。但HTTP本身是无连接、无状态的。HTTP/1.1协议使用了持续连接(分为非流水线方式和流水线方式) - -3,电子邮件把邮件发送到收件人使用的邮件服务器,并放在其中的收件人邮箱中,收件人可随时上网到自己使用的邮件服务器读取,相当于电子邮箱。 - -4,一个电子邮件系统有三个重要组成构件:用户代理、邮件服务器、邮件协议(包括邮件发送协议,如SMTP,和邮件读取协议,如POP3和IMAP)。用户代理和邮件服务器都要运行这些协议。 - - - -### (3),最重要知识点总结 - -#### ① 域名系统-从域名解析出IP地址 -#### ② 访问一个网站大致的过程 -#### ③ 系统调用和应用编程接口概念 - diff --git "a/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234.md" "b/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234.md" index 1cfe2695278..688d7f3e0e8 100644 --- "a/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234.md" +++ "b/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234.md" @@ -38,17 +38,18 @@ 这里强调指出,网络层中的“网络”二字已经不是我们通常谈到的具体网络,而是指计算机网络体系结构模型中第三层的名称. -互联网是由大量的异构(heterogeneous)网络通过路由器(router)相互连接起来的。互联网使用的网络层协议是无连接的网际协议(Intert Protocol)和许多路由选择协议,因此互联网的网络层也叫做**网际层**或**IP层**。 +互联网是由大量的异构(heterogeneous)网络通过路由器(router)相互连接起来的。互联网使用的网络层协议是无连接的网际协议(Internet Protocol)和许多路由选择协议,因此互联网的网络层也叫做**网际层**或**IP层**。 ### 1.4 数据链路层 **数据链路层(data link layer)通常简称为链路层。两台主机之间的数据传输,总是在一段一段的链路上传送的,这就需要使用专门的链路层的协议。** 在两个相邻节点之间传送数据时,**数据链路层将网络层交下来的 IP 数据报组装成帧**,在两个相邻节点间的链路上传送帧。每一帧包括数据和必要的控制信息(如同步信息,地址信息,差错控制等)。 在接收数据时,控制信息使接收端能够知道一个帧从哪个比特开始和到哪个比特结束。这样,数据链路层在收到一个帧后,就可从中提出数据部分,上交给网络层。 -控制信息还使接收端能够检测到所收到的帧中有误差错。如果发现差错,数据链路层就简单地丢弃这个出了差错的帧,以避免继续在网络中传送下去白白浪费网络资源。如果需要改正数据在链路层传输时出现差错(这就是说,数据链路层不仅要检错,而且还要纠错),那么就要采用可靠性传输协议来纠正出现的差错。这种方法会使链路层的协议复杂些。 +控制信息还使接收端能够检测到所收到的帧中有无差错。如果发现差错,数据链路层就简单地丢弃这个出了差错的帧,以避免继续在网络中传送下去白白浪费网络资源。如果需要改正数据在链路层传输时出现差错(这就是说,数据链路层不仅要检错,而且还要纠错),那么就要采用可靠性传输协议来纠正出现的差错。这种方法会使链路层的协议复杂些。 ### 1.5 物理层 在物理层上所传送的数据单位是比特。 - **物理层(physical layer)的作用是实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异。** 使其上面的数据链路层不必考虑网络的具体传输介质是什么。“透明传送比特流”表示经实际电路传送后的比特流没有发生变化,对传送的比特流来说,这个电路好像是看不见的。 + + **物理层(physical layer)的作用是实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异,** 使其上面的数据链路层不必考虑网络的具体传输介质是什么。“透明传送比特流”表示经实际电路传送后的比特流没有发生变化,对传送的比特流来说,这个电路好像是看不见的。 在互联网使用的各种协中最重要和最著名的就是 TCP/IP 两个协议。现在人们经常提到的TCP/IP并不一定单指TCP和IP这两个具体的协议,而往往表示互联网所使用的整个TCP/IP协议族。 @@ -56,7 +57,7 @@ 上面我们对计算机网络的五层体系结构有了初步的了解,下面附送一张七层体系结构图总结一下(图片来源于网络)。 -![七层体系结构图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019/7/七层体系结构图.png) +![七层体系结构图](images/七层体系结构图.png) ## 二 TCP 三次握手和四次挥手(面试常客) @@ -188,7 +189,7 @@ TCP的拥塞控制采用了四种算法,即 **慢开始** 、 **拥塞避免** -> 上图有一个错误,请注意,是OSPF不是OPSF。 OSPF(Open Shortest Path Fitst,ospf)开放最短路径优先协议,是由Internet工程任务组开发的路由选择协议 +> 上图有一个错误,请注意,是OSPF不是OPSF。 OSPF(Open Shortest Path First,ospf)开放最短路径优先协议,是由Internet工程任务组开发的路由选择协议 总体来说分为以下几个过程: diff --git "a/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\347\237\245\350\257\206\346\200\273\347\273\223.md" "b/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\347\237\245\350\257\206\346\200\273\347\273\223.md" new file mode 100644 index 00000000000..07e0c4c0c0a --- /dev/null +++ "b/docs/network/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\347\237\245\350\257\206\346\200\273\347\273\223.md" @@ -0,0 +1,348 @@ +本文是我在大二学习计算机网络期间整理, 大部分内容都来自于谢希仁老师的《计算机网络》这本书。 + +为了内容更容易理解,我对之前的整理进行了一波重构,并配上了一些相关的示意图便于理解。 + +![](https://img-blog.csdnimg.cn/img_convert/e7177b00248e30dc49bd6061093a0590.png) + + + + + + +- [1. 计算机网络概述](#1-计算机网络概述) + - [1.1. 基本术语](#11-基本术语) + - [1.2. 重要知识点总结](#12-重要知识点总结) +- [2. 物理层(Physical Layer)](#2-物理层physical-layer) + - [2.1. 基本术语](#21-基本术语) + - [2.2. 重要知识点总结](#22-重要知识点总结) + - [2.3. 补充](#23-补充) + - [2.3.1. 物理层主要做啥?](#231-物理层主要做啥) + - [2.3.2. 几种常用的信道复用技术](#232-几种常用的信道复用技术) + - [2.3.3. 几种常用的宽带接入技术,主要是 ADSL 和 FTTx](#233-几种常用的宽带接入技术主要是-adsl-和-fttx) +- [3. 数据链路层(Data Link Layer)](#3-数据链路层data-link-layer) + - [3.1. 基本术语](#31-基本术语) + - [3.2. 重要知识点总结](#32-重要知识点总结) + - [3.3. 补充](#33-补充) +- [4. 网络层(Network Layer)](#4-网络层network-layer) + - [4.1. 基本术语](#41-基本术语) + - [4.2. 重要知识点总结](#42-重要知识点总结) +- [5. 传输层(Transport Layer)](#5-传输层transport-layer) + - [5.1. 基本术语](#51-基本术语) + - [5.2. 重要知识点总结](#52-重要知识点总结) + - [5.3. 补充(重要)](#53-补充重要) +- [6. 应用层(Application Layer)](#6-应用层application-layer) + - [6.1. 基本术语](#61-基本术语) + - [6.2. 重要知识点总结](#62-重要知识点总结) + - [6.3. 补充(重要)](#63-补充重要) + + + + +## 1. 计算机网络概述 + +### 1.1. 基本术语 + +1. **结点 (node)** :网络中的结点可以是计算机,集线器,交换机或路由器等。 +2. **链路(link )** : 从一个结点到另一个结点的一段物理线路。中间没有任何其他交点。 +3. **主机(host)** :连接在因特网上的计算机。 +4. **ISP(Internet Service Provider)** :因特网服务提供者(提供商)。 + +![ISP (Internet Service Provider) Definition](https://img-blog.csdnimg.cn/img_convert/b83f6951e3f8f4bcde5b227257d603a8.png) + +5. **IXP(Internet eXchange Point)** : 互联网交换点 IXP 的主要作用就是允许两个网络直接相连并交换分组,而不需要再通过第三个网络来转发分组。 + +![IXP Traffic Levels During the Stratos Skydive — RIPE Labs](https://img-blog.csdnimg.cn/img_convert/7a9568a9e94001fc110801addc8c4ec0.png) + +

https://labs.ripe.net/Members/fergalc/ixp-traffic-during-stratos-skydive

+ +6. **RFC(Request For Comments)** :意思是“请求评议”,包含了关于 Internet 几乎所有的重要的文字资料。 +7. **广域网 WAN(Wide Area Network)** :任务是通过长距离运送主机发送的数据。 +8. **城域网 MAN(Metropolitan Area Network)**:用来将多个局域网进行互连。 +9. **局域网 LAN(Local Area Network)** : 学校或企业大多拥有多个互连的局域网。 + +![MAN & WMAN | Red de área metropolitana, Redes informaticas, Par trenzado](https://img-blog.csdnimg.cn/img_convert/25a5789f8e18995c649f2f864d51e7a9.png) + +

http://conexionesmanwman.blogspot.com/

+ +10. **个人区域网 PAN(Personal Area Network)** :在个人工作的地方把属于个人使用的电子设备用无线技术连接起来的网络 。 + +![Advantages and disadvantages of personal area network (PAN) - IT Release](https://img-blog.csdnimg.cn/img_convert/5c99dd6011439b1fab6cd2fece155dd5.png) + +

https://www.itrelease.com/2018/07/advantages-and-disadvantages-of-personal-area-network-pan/

+ +12. **分组(packet )** :因特网中传送的数据单元。由首部 header 和数据段组成。分组又称为包,首部可称为包头。 +13. **存储转发(store and forward )** :路由器收到一个分组,先检查分组是否正确,并过滤掉冲突包错误。确定包正确后,取出目的地址,通过查找表找到想要发送的输出端口地址,然后将该包发送出去。 + +![](https://img-blog.csdnimg.cn/20201025142342169.gif#pic_center) + +14. **带宽(bandwidth)** :在计算机网络中,表示在单位时间内从网络中的某一点到另一点所能通过的“最高数据率”。常用来表示网络的通信线路所能传送数据的能力。单位是“比特每秒”,记为 b/s。 +15. **吞吐量(throughput )** :表示在单位时间内通过某个网络(或信道、接口)的数据量。吞吐量更经常地用于对现实世界中的网络的一种测量,以便知道实际上到底有多少数据量能够通过网络。吞吐量受网络的带宽或网络的额定速率的限制。 + +### 1.2. 重要知识点总结 + +1. **计算机网络(简称网络)把许多计算机连接在一起,而互联网把许多网络连接在一起,是网络的网络。** +2. 小写字母 i 开头的 internet(互联网)是通用名词,它泛指由多个计算机网络相互连接而成的网络。在这些网络之间的通信协议(即通信规则)可以是任意的。大写字母 I 开头的 Internet(互联网)是专用名词,它指全球最大的,开放的,由众多网络相互连接而成的特定的互联网,并采用 TCP/IP 协议作为通信规则,其前身为 ARPANET。Internet 的推荐译名为因特网,现在一般流行称为互联网。 +3. 路由器是实现分组交换的关键构件,其任务是转发收到的分组,这是网络核心部分最重要的功能。分组交换采用存储转发技术,表示把一个报文(要发送的整块数据)分为几个分组后再进行传送。在发送报文之前,先把较长的报文划分成为一个个更小的等长数据段。在每个数据端的前面加上一些由必要的控制信息组成的首部后,就构成了一个分组。分组又称为包。分组是在互联网中传送的数据单元,正是由于分组的头部包含了诸如目的地址和源地址等重要控制信息,每一个分组才能在互联网中独立的选择传输路径,并正确地交付到分组传输的终点。 +4. 互联网按工作方式可划分为边缘部分和核心部分。主机在网络的边缘部分,其作用是进行信息处理。由大量网络和连接这些网络的路由器组成核心部分,其作用是提供连通性和交换。 +5. 计算机通信是计算机中进程(即运行着的程序)之间的通信。计算机网络采用的通信方式是客户-服务器方式(C/S 方式)和对等连接方式(P2P 方式)。 +6. 客户和服务器都是指通信中所涉及的应用进程。客户是服务请求方,服务器是服务提供方。 +7. 按照作用范围的不同,计算机网络分为广域网 WAN,城域网 MAN,局域网 LAN,个人区域网 PAN。 +8. **计算机网络最常用的性能指标是:速率,带宽,吞吐量,时延(发送时延,处理时延,排队时延),时延带宽积,往返时间和信道利用率。** +9. 网络协议即协议,是为进行网络中的数据交换而建立的规则。计算机网络的各层以及其协议集合,称为网络的体系结构。 +10. **五层体系结构由应用层,运输层,网络层(网际层),数据链路层,物理层组成。运输层最主要的协议是 TCP 和 UDP 协议,网络层最重要的协议是 IP 协议。** + +![s](https://img-blog.csdnimg.cn/2020102514243717.png#pic_center) + +下面的内容会介绍计算机网络的五层体系结构:**物理层+数据链路层+网络层(网际层)+运输层+应用层**。 + +## 2. 物理层(Physical Layer) + +![物理层](https://img-blog.csdnimg.cn/img_convert/4749289d6e152bab1c8a8ccfc946a797.png) + +### 2.1. 基本术语 + +1. **数据(data)** :运送消息的实体。 +2. **信号(signal)** :数据的电气的或电磁的表现。或者说信号是适合在传输介质上传输的对象。 +3. **码元( code)** :在使用时间域(或简称为时域)的波形来表示数字信号时,代表不同离散数值的基本波形。 +4. **单工(simplex )** : 只能有一个方向的通信而没有反方向的交互。 +5. **半双工(half duplex )** :通信的双方都可以发送信息,但不能双方同时发送(当然也就不能同时接收)。 +6. **全双工(full duplex)** : 通信的双方可以同时发送和接收信息。 + +![](https://img-blog.csdnimg.cn/img_convert/c5be4756d2d6f46cbb6d785d5b86faf1.png) + +7. **失真**:失去真实性,主要是指接受到的信号和发送的信号不同,有磨损和衰减。影响失真程度的因素:1.码元传输速率 2.信号传输距离 3.噪声干扰 4.传输媒体质量 + +![](https://img-blog.csdnimg.cn/img_convert/aef3aac72e86c1ee6ccb8a91647f656c.png) + +8. **奈氏准则** : 在任何信道中,码元的传输的效率是有上限的,传输速率超过此上限,就会出现严重的码间串扰问题,使接收端对码元的判决(即识别)成为不可能。 +9. **香农定理** :在带宽受限且有噪声的信道中,为了不产生误差,信息的数据传输速率有上限值。 +10. **基带信号(baseband signal)** : 来自信源的信号。指没有经过调制的数字信号或模拟信号。 +11. **带通(频带)信号(bandpass signal)** :把基带信号经过载波调制后,把信号的频率范围搬移到较高的频段以便在信道中传输(即仅在一段频率范围内能够通过信道),这里调制过后的信号就是带通信号。 +12. **调制(modulation )** : 对信号源的信息进行处理后加到载波信号上,使其变为适合在信道传输的形式的过程。 +13. **信噪比(signal-to-noise ratio )** : 指信号的平均功率和噪声的平均功率之比,记为 S/N。信噪比(dB)=10\*log10(S/N)。 +14. **信道复用(channel multiplexing )** :指多个用户共享同一个信道。(并不一定是同时)。 + +![信道复用技术](https://img-blog.csdnimg.cn/img_convert/a4889adaad3314f882e2cfab5f382064.png) + +15. **比特率(bit rate )** :单位时间(每秒)内传送的比特数。 +16. **波特率(baud rate)** :单位时间载波调制状态改变的次数。针对数据信号对载波的调制速率。 +17. **复用(multiplexing)** :共享信道的方法。 +18. **ADSL(Asymmetric Digital Subscriber Line )** :非对称数字用户线。 +19. **光纤同轴混合网(HFC 网)** :在目前覆盖范围很广的有线电视网的基础上开发的一种居民宽带接入网 + +### 2.2. 重要知识点总结 + +1. **物理层的主要任务就是确定与传输媒体接口有关的一些特性,如机械特性,电气特性,功能特性,过程特性。** +2. 一个数据通信系统可划分为三大部分,即源系统,传输系统,目的系统。源系统包括源点(或源站,信源)和发送器,目的系统包括接收器和终点。 +3. **通信的目的是传送消息。如话音,文字,图像等都是消息,数据是运送消息的实体。信号则是数据的电器或电磁的表现。** +4. 根据信号中代表消息的参数的取值方式不同,信号可分为模拟信号(或连续信号)和数字信号(或离散信号)。在使用时间域(简称时域)的波形表示数字信号时,代表不同离散数值的基本波形称为码元。 +5. 根据双方信息交互的方式,通信可划分为单向通信(或单工通信),双向交替通信(或半双工通信),双向同时通信(全双工通信)。 +6. 来自信源的信号称为基带信号。信号要在信道上传输就要经过调制。调制有基带调制和带通调制之分。最基本的带通调制方法有调幅,调频和调相。还有更复杂的调制方法,如正交振幅调制。 +7. 要提高数据在信道上的传递速率,可以使用更好的传输媒体,或使用先进的调制技术。但数据传输速率不可能任意被提高。 +8. 传输媒体可分为两大类,即导引型传输媒体(双绞线,同轴电缆,光纤)和非导引型传输媒体(无线,红外,大气激光)。 +9. 了有效利用光纤资源,在光纤干线和用户之间广泛使用无源光网络 PON。无源光网络无需配备电源,其长期运营成本和管理成本都很低。最流行的无源光网络是以太网无源光网络 EPON 和吉比特无源光网络 GPON。 + +### 2.3. 补充 + +#### 2.3.1. 物理层主要做啥? + +物理层主要做的事情就是 **透明地传送比特流**。也可以将物理层的主要任务描述为确定与传输媒体的接口的一些特性,即:机械特性(接口所用接线器的一些物理属性如形状尺寸),电气特性(接口电缆的各条线上出现的电压的范围),功能特性(某条线上出现的某一电平的电压的意义),过程特性(对于不同功能能的各种可能事件的出现顺序)。 + +**物理层考虑的是怎样才能在连接各种计算机的传输媒体上传输数据比特流,而不是指具体的传输媒体。** 现有的计算机网络中的硬件设备和传输媒体的种类非常繁多,而且通信手段也有许多不同的方式。物理层的作用正是尽可能地屏蔽掉这些传输媒体和通信手段的差异,使物理层上面的数据链路层感觉不到这些差异,这样就可以使数据链路层只考虑完成本层的协议和服务,而不必考虑网络的具体传输媒体和通信手段是什么。 + +#### 2.3.2. 几种常用的信道复用技术 + +1. **频分复用(FDM)** :所有用户在同样的时间占用不同的带宽资源。 +2. **时分复用(TDM)** :所有用户在不同的时间占用同样的频带宽度(分时不分频)。 +3. **统计时分复用 (Statistic TDM)** :改进的时分复用,能够明显提高信道的利用率。 +4. **码分复用(CDM)** : 用户使用经过特殊挑选的不同码型,因此各用户之间不会造成干扰。这种系统发送的信号有很强的抗干扰能力,其频谱类似于白噪声,不易被敌人发现。 +5. **波分复用( WDM)** :波分复用就是光的频分复用。 + +#### 2.3.3. 几种常用的宽带接入技术,主要是 ADSL 和 FTTx + +用户到互联网的宽带接入方法有非对称数字用户线 ADSL(用数字技术对现有的模拟电话线进行改造,而不需要重新布线。ASDL 的快速版本是甚高速数字用户线 VDSL。),光纤同轴混合网 HFC(是在目前覆盖范围很广的有线电视网的基础上开发的一种居民宽带接入网)和 FTTx(即光纤到······)。 + +## 3. 数据链路层(Data Link Layer) + +![数据链路层](https://img-blog.csdnimg.cn/img_convert/8d24d1684552fa9cc824ce72b3087637.png) + +### 3.1. 基本术语 + +1. **链路(link)** :一个结点到相邻结点的一段物理链路。 +2. **数据链路(data link)** :把实现控制数据运输的协议的硬件和软件加到链路上就构成了数据链路。 +3. **循环冗余检验 CRC(Cyclic Redundancy Check)** :为了保证数据传输的可靠性,CRC 是数据链路层广泛使用的一种检错技术。 +4. **帧(frame)** :一个数据链路层的传输单元,由一个数据链路层首部和其携带的封包所组成协议数据单元。 +5. **MTU(Maximum Transfer Uint )** :最大传送单元。帧的数据部分的的长度上限。 +6. **误码率 BER(Bit Error Rate )** :在一段时间内,传输错误的比特占所传输比特总数的比率。 +7. **PPP(Point-to-Point Protocol )** :点对点协议。即用户计算机和 ISP 进行通信时所使用的数据链路层协议。以下是 PPP 帧的示意图: + ![PPP](https://img-blog.csdnimg.cn/img_convert/298dbdeb16f98cec02c3954d8d95c1d6.png) +8. **MAC 地址(Media Access Control 或者 Medium Access Control)** :意译为媒体访问控制,或称为物理地址、硬件地址,用来定义网络设备的位置。在 OSI 模型中,第三层网络层负责 IP 地址,第二层数据链路层则负责 MAC 地址。因此一个主机会有一个 MAC 地址,而每个网络位置会有一个专属于它的 IP 地址 。地址是识别某个系统的重要标识符,“名字指出我们所要寻找的资源,地址指出资源所在的地方,路由告诉我们如何到达该处。 + +![ARP (Address Resolution Protocol) explained](https://img-blog.csdnimg.cn/img_convert/002b2e6e45d66e805008fafc310afef0.png) + +9. **网桥(bridge)** :一种用于数据链路层实现中继,连接两个或多个局域网的网络互连设备。 +10. **交换机(switch )** :广义的来说,交换机指的是一种通信系统中完成信息交换的设备。这里工作在数据链路层的交换机指的是交换式集线器,其实质是一个多接口的网桥 + +### 3.2. 重要知识点总结 + +1. 链路是从一个结点到相邻节点的一段物理链路,数据链路则在链路的基础上增加了一些必要的硬件(如网络适配器)和软件(如协议的实现) +2. 数据链路层使用的主要是**点对点信道**和**广播信道**两种。 +3. 数据链路层传输的协议数据单元是帧。数据链路层的三个基本问题是:**封装成帧**,**透明传输**和**差错检测** +4. **循环冗余检验 CRC** 是一种检错方法,而帧检验序列 FCS 是添加在数据后面的冗余码 +5. **点对点协议 PPP** 是数据链路层使用最多的一种协议,它的特点是:简单,只检测差错而不去纠正差错,不使用序号,也不进行流量控制,可同时支持多种网络层协议 +6. PPPoE 是为宽带上网的主机使用的链路层协议 +7. **局域网的优点是:具有广播功能,从一个站点可方便地访问全网;便于系统的扩展和逐渐演变;提高了系统的可靠性,可用性和生存性。** +8. 计算机与外接局域网通信需要通过通信适配器(或网络适配器),它又称为网络接口卡或网卡。**计算器的硬件地址就在适配器的 ROM 中**。 +9. 以太网采用的无连接的工作方式,对发送的数据帧不进行编号,也不要求对方发回确认。目的站收到有差错帧就把它丢掉,其他什么也不做 +10. 以太网采用的协议是具有冲突检测的**载波监听多点接入 CSMA/CD**。协议的特点是:**发送前先监听,边发送边监听,一旦发现总线上出现了碰撞,就立即停止发送。然后按照退避算法等待一段随机时间后再次发送。** 因此,每一个站点在自己发送数据之后的一小段时间内,存在这遭遇碰撞的可能性。以太网上的各站点平等的争用以太网信道 +11. 以太网的适配器具有过滤功能,它只接收单播帧,广播帧和多播帧。 +12. 使用集线器可以在物理层扩展以太网(扩展后的以太网仍然是一个网络) + +### 3.3. 补充 + +1. 数据链路层的点对点信道和广播信道的特点,以及这两种信道所使用的协议(PPP 协议以及 CSMA/CD 协议)的特点 +2. 数据链路层的三个基本问题:**封装成帧**,**透明传输**,**差错检测** +3. 以太网的 MAC 层硬件地址 +4. 适配器,转发器,集线器,网桥,以太网交换机的作用以及适用场合 + +## 4. 网络层(Network Layer) + +![网络层](https://img-blog.csdnimg.cn/img_convert/fbf78bdcf3db11526ac1a234a8b98234.png) + +### 4.1. 基本术语 + +1. **虚电路(Virtual Circuit)** : 在两个终端设备的逻辑或物理端口之间,通过建立的双向的透明传输通道。虚电路表示这只是一条逻辑上的连接,分组都沿着这条逻辑连接按照存储转发方式传送,而并不是真正建立了一条物理连接。 +2. **IP(Internet Protocol )** : 网际协议 IP 是 TCP/IP 体系中两个最主要的协议之一,是 TCP/IP 体系结构网际层的核心。配套的有 ARP,RARP,ICMP,IGMP。 +3. **ARP(Address Resolution Protocol)** : 地址解析协议。地址解析协议 ARP 把 IP 地址解析为硬件地址。 +4. **ICMP(Internet Control Message Protocol )** :网际控制报文协议 (ICMP 允许主机或路由器报告差错情况和提供有关异常情况的报告)。 +5. **子网掩码(subnet mask )** :它是一种用来指明一个 IP 地址的哪些位标识的是主机所在的子网以及哪些位标识的是主机的位掩码。子网掩码不能单独存在,它必须结合 IP 地址一起使用。 +6. **CIDR( Classless Inter-Domain Routing )**:无分类域间路由选择 (特点是消除了传统的 A 类、B 类和 C 类地址以及划分子网的概念,并使用各种长度的“网络前缀”(network-prefix)来代替分类地址中的网络号和子网号)。 +7. **默认路由(default route)** :当在路由表中查不到能到达目的地址的路由时,路由器选择的路由。默认路由还可以减小路由表所占用的空间和搜索路由表所用的时间。 +8. **路由选择算法(Virtual Circuit)** :路由选择协议的核心部分。因特网采用自适应的,分层次的路由选择协议。 + +### 4.2. 重要知识点总结 + +1. **TCP/IP 协议中的网络层向上只提供简单灵活的,无连接的,尽最大努力交付的数据报服务。网络层不提供服务质量的承诺,不保证分组交付的时限所传送的分组可能出错,丢失,重复和失序。进程之间通信的可靠性由运输层负责** +2. 在互联网的交付有两种,一是在本网络直接交付不用经过路由器,另一种是和其他网络的间接交付,至少经过一个路由器,但最后一次一定是直接交付 +3. 分类的 IP 地址由网络号字段(指明网络)和主机号字段(指明主机)组成。网络号字段最前面的类别指明 IP 地址的类别。IP 地址是一种分等级的地址结构。IP 地址管理机构分配 IP 地址时只分配网络号,主机号由得到该网络号的单位自行分配。路由器根据目的主机所连接的网络号来转发分组。一个路由器至少连接到两个网络,所以一个路由器至少应当有两个不同的 IP 地址 +4. IP 数据报分为首部和数据两部分。首部的前一部分是固定长度,共 20 字节,是所有 IP 数据包必须具有的(源地址,目的地址,总长度等重要地段都固定在首部)。一些长度可变的可选字段固定在首部的后面。IP 首部中的生存时间给出了 IP 数据报在互联网中所能经过的最大路由器数。可防止 IP 数据报在互联网中无限制的兜圈子。 +5. **地址解析协议 ARP 把 IP 地址解析为硬件地址。ARP 的高速缓存可以大大减少网络上的通信量。因为这样可以使主机下次再与同样地址的主机通信时,可以直接从高速缓存中找到所需要的硬件地址而不需要再去广播方式发送 ARP 请求分组** +6. 无分类域间路由选择 CIDR 是解决目前 IP 地址紧缺的一个好办法。CIDR 记法把 IP 地址后面加上斜线“/”,然后写上前缀所所占的位数。前缀(或网络前缀用来指明网络),前缀后面的部分是后缀,用来指明主机。CIDR 把前缀都相同的连续的 IP 地址组成一个“CIDR 地址块”,IP 地址分配都以 CIDR 地址块为单位。 +7. 网际控制报文协议是 IP 层的协议。ICMP 报文作为 IP 数据报的数据,加上首部后组成 IP 数据报发送出去。使用 ICMP 数据报并不是为了实现可靠传输。ICMP 允许主机或路由器报告差错情况和提供有关异常情况的报告。ICMP 报文的种类有两种 ICMP 差错报告报文和 ICMP 询问报文。 +8. **要解决 IP 地址耗尽的问题,最根本的办法是采用具有更大地址空间的新版本 IP 协议-IPv6。** IPv6 所带来的变化有 ① 更大的地址空间(采用 128 位地址)② 灵活的首部格式 ③ 改进的选项 ④ 支持即插即用 ⑤ 支持资源的预分配 ⑥IPv6 的首部改为 8 字节对齐。 +9. **虚拟专用网络 VPN 利用公用的互联网作为本机构专用网之间的通信载体。VPN 内使用互联网的专用地址。一个 VPN 至少要有一个路由器具有合法的全球 IP 地址,这样才能和本系统的另一个 VPN 通过互联网进行通信。所有通过互联网传送的数据都需要加密。** +10. MPLS 的特点是:① 支持面向连接的服务质量 ② 支持流量工程,平衡网络负载 ③ 有效的支持虚拟专用网 VPN。MPLS 在入口节点给每一个 IP 数据报打上固定长度的“标记”,然后根据标记在第二层(链路层)用硬件进行转发(在标记交换路由器中进行标记交换),因而转发速率大大加快。 + +## 5. 传输层(Transport Layer) + +![传输层](https://img-blog.csdnimg.cn/img_convert/09eb87a29bed99775ef5bde5eb216971.png) + +### 5.1. 基本术语 + +1. **进程(process)** :指计算机中正在运行的程序实体。 +2. **应用进程互相通信** :一台主机的进程和另一台主机中的一个进程交换数据的过程(另外注意通信真正的端点不是主机而是主机中的进程,也就是说端到端的通信是应用进程之间的通信)。 +3. **传输层的复用与分用** :复用指发送方不同的进程都可以通过统一个运输层协议传送数据。分用指接收方的运输层在剥去报文的首部后能把这些数据正确的交付到目的应用进程。 +4. **TCP(Transmission Control Protocol)** :传输控制协议。 +5. **UDP(User Datagram Protocol)** :用户数据报协议。 + +![TCP和UDP](https://img-blog.csdnimg.cn/img_convert/2bd5bf90676c338864807ade87b7bdea.png) + +6. **端口(port)** :端口的目的是为了确认对方机器是那个进程在于自己进行交互,比如 MSN 和 QQ 的端口不同,如果没有端口就可能出现 QQ 进程和 MSN 交互错误。端口又称协议端口号。 +7. **停止等待协议(stop-and-wait)** :指发送方每发送完一个分组就停止发送,等待对方确认,在收到确认之后在发送下一个分组。 +8. **流量控制** : 就是让发送方的发送速率不要太快,既要让接收方来得及接收,也不要使网络发生拥塞。 +9. **拥塞控制** :防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载。拥塞控制所要做的都有一个前提,就是网络能够承受现有的网络负荷。 + +### 5.2. 重要知识点总结 + +1. **运输层提供应用进程之间的逻辑通信,也就是说,运输层之间的通信并不是真正在两个运输层之间直接传输数据。运输层向应用层屏蔽了下面网络的细节(如网络拓补,所采用的路由选择协议等),它使应用进程之间看起来好像两个运输层实体之间有一条端到端的逻辑通信信道。** +2. **网络层为主机提供逻辑通信,而运输层为应用进程之间提供端到端的逻辑通信。** +3. 运输层的两个重要协议是用户数据报协议 UDP 和传输控制协议 TCP。按照 OSI 的术语,两个对等运输实体在通信时传送的数据单位叫做运输协议数据单元 TPDU(Transport Protocol Data Unit)。但在 TCP/IP 体系中,则根据所使用的协议是 TCP 或 UDP,分别称之为 TCP 报文段或 UDP 用户数据报。 +4. **UDP 在传送数据之前不需要先建立连接,远地主机在收到 UDP 报文后,不需要给出任何确认。虽然 UDP 不提供可靠交付,但在某些情况下 UDP 确是一种最有效的工作方式。 TCP 提供面向连接的服务。在传送数据之前必须先建立连接,数据传送结束后要释放连接。TCP 不提供广播或多播服务。由于 TCP 要提供可靠的,面向连接的传输服务,这一难以避免增加了许多开销,如确认,流量控制,计时器以及连接管理等。这不仅使协议数据单元的首部增大很多,还要占用许多处理机资源。** +5. 硬件端口是不同硬件设备进行交互的接口,而软件端口是应用层各种协议进程与运输实体进行层间交互的一种地址。UDP 和 TCP 的首部格式中都有源端口和目的端口这两个重要字段。当运输层收到 IP 层交上来的运输层报文时,就能够 根据其首部中的目的端口号把数据交付应用层的目的应用层。(两个进程之间进行通信不光要知道对方 IP 地址而且要知道对方的端口号(为了找到对方计算机中的应用进程)) +6. 运输层用一个 16 位端口号标志一个端口。端口号只有本地意义,它只是为了标志计算机应用层中的各个进程在和运输层交互时的层间接口。在互联网的不同计算机中,相同的端口号是没有关联的。协议端口号简称端口。虽然通信的终点是应用进程,但只要把所发送的报文交到目的主机的某个合适端口,剩下的工作(最后交付目的进程)就由 TCP 和 UDP 来完成。 +7. 运输层的端口号分为服务器端使用的端口号(0~1023 指派给熟知端口,1024~49151 是登记端口号)和客户端暂时使用的端口号(49152~65535) +8. **UDP 的主要特点是 ① 无连接 ② 尽最大努力交付 ③ 面向报文 ④ 无拥塞控制 ⑤ 支持一对一,一对多,多对一和多对多的交互通信 ⑥ 首部开销小(只有四个字段:源端口,目的端口,长度和检验和)** +9. **TCP 的主要特点是 ① 面向连接 ② 每一条 TCP 连接只能是一对一的 ③ 提供可靠交付 ④ 提供全双工通信 ⑤ 面向字节流** +10. **TCP 用主机的 IP 地址加上主机上的端口号作为 TCP 连接的端点。这样的端点就叫做套接字(socket)或插口。套接字用(IP 地址:端口号)来表示。每一条 TCP 连接唯一被通信两端的两个端点所确定。** +11. 停止等待协议是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组。 +12. 为了提高传输效率,发送方可以不使用低效率的停止等待协议,而是采用流水线传输。流水线传输就是发送方可连续发送多个分组,不必每发完一个分组就停下来等待对方确认。这样可使信道上一直有数据不间断的在传送。这种传输方式可以明显提高信道利用率。 +13. 停止等待协议中超时重传是指只要超过一段时间仍然没有收到确认,就重传前面发送过的分组(认为刚才发送过的分组丢失了)。因此每发送完一个分组需要设置一个超时计时器,其重转时间应比数据在分组传输的平均往返时间更长一些。这种自动重传方式常称为自动重传请求 ARQ。另外在停止等待协议中若收到重复分组,就丢弃该分组,但同时还要发送确认。连续 ARQ 协议可提高信道利用率。发送维持一个发送窗口,凡位于发送窗口内的分组可连续发送出去,而不需要等待对方确认。接收方一般采用累积确认,对按序到达的最后一个分组发送确认,表明到这个分组位置的所有分组都已经正确收到了。 +14. TCP 报文段的前 20 个字节是固定的,后面有 4n 字节是根据需要增加的选项。因此,TCP 首部的最小长度是 20 字节。 +15. **TCP 使用滑动窗口机制。发送窗口里面的序号表示允许发送的序号。发送窗口后沿的后面部分表示已发送且已收到确认,而发送窗口前沿的前面部分表示不允许发送。发送窗口后沿的变化情况有两种可能,即不动(没有收到新的确认)和前移(收到了新的确认)。发送窗口的前沿通常是不断向前移动的。一般来说,我们总是希望数据传输更快一些。但如果发送方把数据发送的过快,接收方就可能来不及接收,这就会造成数据的丢失。所谓流量控制就是让发送方的发送速率不要太快,要让接收方来得及接收。** +16. 在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏。这种情况就叫拥塞。拥塞控制就是为了防止过多的数据注入到网络中,这样就可以使网络中的路由器或链路不致过载。拥塞控制所要做的都有一个前提,就是网络能够承受现有的网络负荷。拥塞控制是一个全局性的过程,涉及到所有的主机,所有的路由器,以及与降低网络传输性能有关的所有因素。相反,流量控制往往是点对点通信量的控制,是个端到端的问题。流量控制所要做到的就是抑制发送端发送数据的速率,以便使接收端来得及接收。 +17. **为了进行拥塞控制,TCP 发送方要维持一个拥塞窗口 cwnd 的状态变量。拥塞控制窗口的大小取决于网络的拥塞程度,并且动态变化。发送方让自己的发送窗口取为拥塞窗口和接收方的接受窗口中较小的一个。** +18. **TCP 的拥塞控制采用了四种算法,即慢开始,拥塞避免,快重传和快恢复。在网络层也可以使路由器采用适当的分组丢弃策略(如主动队列管理 AQM),以减少网络拥塞的发生。** +19. 运输连接的三个阶段,即:连接建立,数据传送和连接释放。 +20. **主动发起 TCP 连接建立的应用进程叫做客户,而被动等待连接建立的应用进程叫做服务器。TCP 连接采用三报文握手机制。服务器要确认用户的连接请求,然后客户要对服务器的确认进行确认。** +21. TCP 的连接释放采用四报文握手机制。任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送时,则发送连接释放通知,对方确认后就完全关闭了 TCP 连接 + +### 5.3. 补充(重要) + +以下知识点需要重点关注: + +1. 端口和套接字的意义 +2. UDP 和 TCP 的区别以及两者的应用场景 +3. 在不可靠的网络上实现可靠传输的工作原理,停止等待协议和 ARQ 协议 +4. TCP 的滑动窗口,流量控制,拥塞控制和连接管理 +5. TCP 的三次握手,四次挥手机制 + +## 6. 应用层(Application Layer) + +![应用层](https://img-blog.csdnimg.cn/img_convert/3ff57c0632bc7f4017723b1d1b7d3a52.png) + +### 6.1. 基本术语 + +1. **域名系统(DNS)** :域名系统(DNS,Domain Name System)将人类可读的域名 (例如,www.baidu.com) 转换为机器可读的 IP 地址 (例如,220.181.38.148)。我们可以将其理解为专为互联网设计的电话薄。 + +![](https://img-blog.csdnimg.cn/img_convert/6af26a3293530061785df50e70d53e07.png) + +

https://www.seobility.net/en/wiki/HTTP_headers

+ +2. **文件传输协议(FTP)** :FTP 是 File TransferProtocol(文件传输协议)的英文简称,而中文简称为“文传协议”。用于 Internet 上的控制文件的双向传输。同时,它也是一个应用程序(Application)。基于不同的操作系统有不同的 FTP 应用程序,而所有这些应用程序都遵守同一种协议以传输文件。在 FTP 的使用当中,用户经常遇到两个概念:"下载"(Download)和"上传"(Upload)。 "下载"文件就是从远程主机拷贝文件至自己的计算机上;"上传"文件就是将文件从自己的计算机中拷贝至远程主机上。用 Internet 语言来说,用户可通过客户机程序向(从)远程主机上传(下载)文件。 + +![FTP工作过程](https://img-blog.csdnimg.cn/img_convert/3f1abf8adba4aa317eca69c489e3db23.png) + +3. **简单文件传输协议(TFTP)** :TFTP(Trivial File Transfer Protocol,简单文件传输协议)是 TCP/IP 协议族中的一个用来在客户机与服务器之间进行简单文件传输的协议,提供不复杂、开销不大的文件传输服务。端口号为 69。 +4. **远程终端协议(TELENET)** :Telnet 协议是 TCP/IP 协议族中的一员,是 Internet 远程登陆服务的标准协议和主要方式。它为用户提供了在本地计算机上完成远程主机工作的能力。在终端使用者的电脑上使用 telnet 程序,用它连接到服务器。终端使用者可以在 telnet 程序中输入命令,这些命令会在服务器上运行,就像直接在服务器的控制台上输入一样。可以在本地就能控制服务器。要开始一个 telnet 会话,必须输入用户名和密码来登录服务器。Telnet 是常用的远程控制 Web 服务器的方法。 +5. **万维网(WWW)** :WWW 是环球信息网的缩写,(亦作“Web”、“WWW”、“'W3'”,英文全称为“World Wide Web”),中文名字为“万维网”,"环球网"等,常简称为 Web。分为 Web 客户端和 Web 服务器程序。WWW 可以让 Web 客户端(常用浏览器)访问浏览 Web 服务器上的页面。是一个由许多互相链接的超文本组成的系统,通过互联网访问。在这个系统中,每个有用的事物,称为一样“资源”;并且由一个全局“统一资源标识符”(URI)标识;这些资源通过超文本传输协议(Hypertext Transfer Protocol)传送给用户,而后者通过点击链接来获得资源。万维网联盟(英语:World Wide Web Consortium,简称 W3C),又称 W3C 理事会。1994 年 10 月在麻省理工学院(MIT)计算机科学实验室成立。万维网联盟的创建者是万维网的发明者蒂姆·伯纳斯-李。万维网并不等同互联网,万维网只是互联网所能提供的服务其中之一,是靠着互联网运行的一项服务。 +6. **万维网的大致工作工程:** + +![万维网的大致工作工程](https://img-blog.csdnimg.cn/img_convert/735f55501e81898aa61b8032f7dbcb73.png) + +7. **统一资源定位符(URL)** :统一资源定位符是对可以从互联网上得到的资源的位置和访问方法的一种简洁的表示,是互联网上标准资源的地址。互联网上的每个文件都有一个唯一的 URL,它包含的信息指出文件的位置以及浏览器应该怎么处理它。 +8. **超文本传输协议(HTTP)** :超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。所有的 WWW 文件都必须遵守这个标准。设计 HTTP 最初的目的是为了提供一种发布和接收 HTML 页面的方法。1960 年美国人 Ted Nelson 构思了一种通过计算机处理文本信息的方法,并称之为超文本(hypertext),这成为了 HTTP 超文本传输协议标准架构的发展根基。 + +HTTP 协议的本质就是一种浏览器与服务器之间约定好的通信格式。HTTP 的原理如下图所示: + +![](https://img-blog.csdnimg.cn/img_convert/b273efef5f2388e26414135672b00295.png) + +10. **代理服务器(Proxy Server)** : 代理服务器(Proxy Server)是一种网络实体,它又称为万维网高速缓存。 代理服务器把最近的一些请求和响应暂存在本地磁盘中。当新请求到达时,若代理服务器发现这个请求与暂时存放的的请求相同,就返回暂存的响应,而不需要按 URL 的地址再次去互联网访问该资源。代理服务器可在客户端或服务器工作,也可以在中间系统工作。 +11. **简单邮件传输协议(SMTP)** : SMTP(Simple Mail Transfer Protocol)即简单邮件传输协议,它是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。 SMTP 协议属于 TCP/IP 协议簇,它帮助每台计算机在发送或中转信件时找到下一个目的地。 通过 SMTP 协议所指定的服务器,就可以把 E-mail 寄到收信人的服务器上了,整个过程只要几分钟。SMTP 服务器则是遵循 SMTP 协议的发送邮件服务器,用来发送或中转发出的电子邮件。 + +![一个电子邮件被发送的过程](https://img-blog.csdnimg.cn/img_convert/b16da4d4fea63de5fce53f54973967d7.png) + +

https://www.campaignmonitor.com/resources/knowledge-base/what-is-the-code-that-makes-bcc-or-cc-operate-in-an-email/

+ +11. **搜索引擎** :搜索引擎(Search Engine)是指根据一定的策略、运用特定的计算机程序从互联网上搜集信息,在对信息进行组织和处理后,为用户提供检索服务,将用户检索相关的信息展示给用户的系统。搜索引擎包括全文索引、目录索引、元搜索引擎、垂直搜索引擎、集合式搜索引擎、门户搜索引擎与免费链接列表等。 + +![搜索引擎](https://img-blog.csdnimg.cn/img_convert/68fe865a9d87de361c45f4a42d624035.png) + +12. **垂直搜索引擎** :垂直搜索引擎是针对某一个行业的专业搜索引擎,是搜索引擎的细分和延伸,是对网页库中的某类专门的信息进行一次整合,定向分字段抽取出需要的数据进行处理后再以某种形式返回给用户。垂直搜索是相对通用搜索引擎的信息量大、查询不准确、深度不够等提出来的新的搜索引擎服务模式,通过针对某一特定领域、某一特定人群或某一特定需求提供的有一定价值的信息和相关服务。其特点就是“专、精、深”,且具有行业色彩,相比较通用搜索引擎的海量信息无序化,垂直搜索引擎则显得更加专注、具体和深入。 +13. **全文索引** :全文索引技术是目前搜索引擎的关键技术。试想在 1M 大小的文件中搜索一个词,可能需要几秒,在 100M 的文件中可能需要几十秒,如果在更大的文件中搜索那么就需要更大的系统开销,这样的开销是不现实的。所以在这样的矛盾下出现了全文索引技术,有时候有人叫倒排文档技术。 +14. **目录索引** :目录索引( search index/directory),顾名思义就是将网站分门别类地存放在相应的目录中,因此用户在查询信息时,可选择关键词搜索,也可按分类目录逐层查找。 + +### 6.2. 重要知识点总结 + +1. 文件传输协议(FTP)使用 TCP 可靠的运输服务。FTP 使用客户服务器方式。一个 FTP 服务器进程可以同时为多个用户提供服务。在进进行文件传输时,FTP 的客户和服务器之间要先建立两个并行的 TCP 连接:控制连接和数据连接。实际用于传输文件的是数据连接。 +2. 万维网客户程序与服务器之间进行交互使用的协议是超文本传输协议 HTTP。HTTP 使用 TCP 连接进行可靠传输。但 HTTP 本身是无连接、无状态的。HTTP/1.1 协议使用了持续连接(分为非流水线方式和流水线方式) +3. 电子邮件把邮件发送到收件人使用的邮件服务器,并放在其中的收件人邮箱中,收件人可随时上网到自己使用的邮件服务器读取,相当于电子邮箱。 +4. 一个电子邮件系统有三个重要组成构件:用户代理、邮件服务器、邮件协议(包括邮件发送协议,如 SMTP,和邮件读取协议,如 POP3 和 IMAP)。用户代理和邮件服务器都要运行这些协议。 + +### 6.3. 补充(重要) + +以下知识点需要重点关注: + +1. 应用层的常见协议(重点关注 HTTP 协议) +2. 域名系统-从域名解析出 IP 地址 +3. 访问一个网站大致的过程 +4. 系统调用和应用编程接口概念 diff --git a/docs/operating-system/Linux_performance/image-20200604180850391.png b/docs/operating-system/Linux_performance/image-20200604180850391.png deleted file mode 100755 index 20b6a534ddc..00000000000 Binary files a/docs/operating-system/Linux_performance/image-20200604180850391.png and /dev/null differ diff --git a/docs/operating-system/Linux_performance/image-20200604180851790.png b/docs/operating-system/Linux_performance/image-20200604180851790.png deleted file mode 100755 index 20b6a534ddc..00000000000 Binary files a/docs/operating-system/Linux_performance/image-20200604180851790.png and /dev/null differ diff --git a/docs/operating-system/Linux_performance/image-20200604181133355.png b/docs/operating-system/Linux_performance/image-20200604181133355.png deleted file mode 100755 index 47d74f36722..00000000000 Binary files a/docs/operating-system/Linux_performance/image-20200604181133355.png and /dev/null differ diff --git a/docs/operating-system/Linux_performance/image-20200604203027136.png b/docs/operating-system/Linux_performance/image-20200604203027136.png deleted file mode 100755 index 753a72f6031..00000000000 Binary files a/docs/operating-system/Linux_performance/image-20200604203027136.png and /dev/null differ diff --git a/docs/operating-system/Linux_performance/image-20200605104607007.png b/docs/operating-system/Linux_performance/image-20200605104607007.png deleted file mode 100755 index 95b1791fbd2..00000000000 Binary files a/docs/operating-system/Linux_performance/image-20200605104607007.png and /dev/null differ diff --git a/docs/operating-system/Linux_performance/iostat.png b/docs/operating-system/Linux_performance/iostat.png deleted file mode 100755 index 4e8c446fab0..00000000000 Binary files a/docs/operating-system/Linux_performance/iostat.png and /dev/null differ diff --git a/docs/operating-system/Linux_performance/linux_xn.png b/docs/operating-system/Linux_performance/linux_xn.png deleted file mode 100755 index 0eb28c0177f..00000000000 Binary files a/docs/operating-system/Linux_performance/linux_xn.png and /dev/null differ diff --git a/docs/operating-system/Linux_performance/tcp_close.jpg b/docs/operating-system/Linux_performance/tcp_close.jpg deleted file mode 100755 index f58c76cdc33..00000000000 Binary files a/docs/operating-system/Linux_performance/tcp_close.jpg and /dev/null differ diff --git a/docs/operating-system/Linux_performance/tcp_conn.jpg b/docs/operating-system/Linux_performance/tcp_conn.jpg deleted file mode 100755 index fae229dba30..00000000000 Binary files a/docs/operating-system/Linux_performance/tcp_conn.jpg and /dev/null differ diff --git a/docs/operating-system/Linux_performance/tcpclose.png b/docs/operating-system/Linux_performance/tcpclose.png deleted file mode 100755 index 80d174300c9..00000000000 Binary files a/docs/operating-system/Linux_performance/tcpclose.png and /dev/null differ diff --git a/docs/operating-system/Linux_performance/tcpconn.png b/docs/operating-system/Linux_performance/tcpconn.png deleted file mode 100755 index 1985847c3c4..00000000000 Binary files a/docs/operating-system/Linux_performance/tcpconn.png and /dev/null differ diff --git "a/docs/operating-system/Linux\346\200\247\350\203\275\345\210\206\346\236\220\345\267\245\345\205\267\345\220\210\351\233\206.md" "b/docs/operating-system/Linux\346\200\247\350\203\275\345\210\206\346\236\220\345\267\245\345\205\267\345\220\210\351\233\206.md" deleted file mode 100755 index 41e17b973c2..00000000000 --- "a/docs/operating-system/Linux\346\200\247\350\203\275\345\210\206\346\236\220\345\267\245\345\205\267\345\220\210\351\233\206.md" +++ /dev/null @@ -1,634 +0,0 @@ -# Linux性能分析工具合集 - -> 本文由读者投稿,原文地址:[https://ysshao.cn/Linux/Linux_performance/](https://ysshao.cn/Linux/Linux_performance/) 。 - -## 1. 背景 - -有时候会遇到一些疑难杂症,并且监控插件并不能一眼立马发现问题的根源。这时候就需要登录服务器进一步深入分析问题的根源。那么分析问题需要有一定的技术经验积累,并且有些问题涉及到的领域非常广,才能定位到问题。所以,分析问题和踩坑是非常锻炼一个人的成长和提升自我能力。如果我们有一套好的分析工具,那将是事半功倍,能够帮助大家快速定位问题,节省大家很多时间做更深入的事情。 - -## 2. 说明 - -本篇文章主要介绍各种问题定位的工具以及会结合案例分析问题。 - -## 3. 分析问题的方法论 - -套用5W2H方法,可以提出性能分析的几个问题 - -- What-现象是什么样的 -- When-什么时候发生 -- Why-为什么会发生 -- Where-哪个地方发生的问题 -- How much-耗费了多少资源 -- How to do-怎么解决问题 - -## 4.性能分析工具合集 - -img - -### CPU - -针对应用程序,我们通常关注的是内核CPU调度器功能和性能。 - -线程的状态分析主要是分析线程的时间用在什么地方,而线程状态的分类一般分为: - -a. on-CPU:执行中,执行中的时间通常又分为用户态时间user和系统态时间sys。 - b. off-CPU:等待下一轮上CPU,或者等待I/O、锁、换页等等,其状态可以细分为可执行、匿名换页、睡 眠、锁、空闲等状态。 - -#### **分析工具** - -| 工具 | 描述 | -| -------- | ------------------------------ | -| uptime/w | 查看服务器运行时间、平均负载 | -| top | 监控每个进程的CPU用量分解 | -| vmstat | 系统的CPU平均负载情况 | -| mpstat | 查看多核CPU信息 | -| sar -u | 查看CPU过去或未来时点CPU利用率 | -| pidstat | 查看每个进程的用量分解 | - -#### uptime - -uptime 命令可以用来查看服务器已经运行了多久,当前登录的用户有多少,以及服务器在过去的1分钟、5分钟、15分钟的系统平均负载值 - -image-20200604180851790 - -第一项是当前时间,up 表示系统正在运行,6:47是系统启动的总时间,最后是系统的负载load信息 - -w 同上,增加了具体登陆了那些用户及登陆时间。 - -#### top - -常用来监控[Linux](http://lib.csdn.net/base/linux)的系统状况,比如cpu、内存的使用,显示系统上正在运行的进程。 - -image-20200604181133355 - -1. **系统运行时间和平均负载:** - - top命令的顶部显示与uptime命令相似的输出。 - - 这些字段显示: - - - 当前时间 - - 系统已运行的时间 - - 当前登录用户的数量 - - 相应最近5、10和15分钟内的平均负载。 - -2. **任务** - - 第二行显示的是任务或者进程的总结。进程可以处于不同的状态。这里显示了全部进程的数量。除此之外,还有正在运行、睡眠、停止、僵尸进程的数量(僵尸是一种进程的状态)。这些进程概括信息可以用’t’切换显示。 - -3. **CPU状态** - - 下一行显示的是CPU状态。 这里显示了不同模式下的所占CPU时间的百分比。这些不同的CPU时间表示: - - - us, user: 运行(未调整优先级的) 用户进程的CPU时间 - - sy,system: 运行内核进程的CPU时间 - - ni,niced:运行已调整优先级的用户进程的CPU时间 - - wa,IO wait: 用于等待IO完成的CPU时间 - - hi:处理硬件中断的CPU时间 - - si: 处理软件中断的CPU时间 - - st:这个虚拟机被hypervisor偷去的CPU时间(译注:如果当前处于一个hypervisor下的vm,实际上hypervisor也是要消耗一部分CPU处理时间的)。 - -4. **内存使用** - - 接下来两行显示内存使用率,有点像’free’命令。第一行是物理内存使用,第二行是虚拟内存使用(交换空间)。 - - 物理内存显示如下:全部可用内存、已使用内存、空闲内存、缓冲内存。相似地:交换部分显示的是:全部、已使用、空闲和缓冲交换空间。 - - > 这里要说明的是不能用windows的内存概念理解这些数据,如果按windows的方式此台服务器“危矣”:8G的内存总量只剩下530M的可用内存。Linux的内存管理有其特殊性,复杂点需要一本书来说明,这里只是简单说点和我们传统概念(windows)的不同。 - > - > 第四行中使用中的内存总量(used)指的是现在系统内核控制的内存数,空闲内存总量(free)是内核还未纳入其管控范围的数量。纳入内核管理的内存不见得都在使用中,还包括过去使用过的现在可以被重复利用的内存,内核并不把这些可被重新使用的内存交还到free中去,因此在[linux](http://lib.csdn.net/base/linux)上free内存会越来越少,但不用为此担心。 - > - > 如果出于习惯去计算可用内存数,这里有个近似的计算公式: - > - > ​ **第四行的free + 第四行的buffers + 第五行的cached。** - > - > 对于内存监控,在top里我们要时刻监控第五行swap交换分区的used,如果这个数值在不断的变化,说明内核在不断进行内存和swap的数据交换,这是真正的内存不够用了。 - -5. **字段/列** - - | 进程的属性 | 属性含义 | - | ---------- | ------------------------------------------------------------ | - | PID | 进程ID,进程的唯一标识符 | - | USER | 进程所有者的实际用户名。 | - | PR | 进程的调度优先级。这个字段的一些值是’rt’。这意味这这些进程运行在实时态。 | - | NI | 进程的nice值(优先级)。越小的值意味着越高的优先级。 | - | VIRT | 进程使用的虚拟内存。 | - | RES | 驻留内存大小。驻留内存是任务使用的非交换物理内存大小。 | - | SHR | SHR是进程使用的共享内存。 | - | S | 这个是进程的状态。它有以下不同的值:
D–不可中断的睡眠态、R–运行态、S–睡眠态、T–被跟踪或已停止、Z – 僵尸态 | - | %CPU | 自从上一次更新时到现在任务所使用的CPU时间百分比。 | - | %MEM | 进程使用的可用物理内存百分比。 | - | TIME+ | 任务启动后到现在所使用的全部CPU时间,精确到百分之一秒。 | - | COMMAND | 运行进程所使用的命令。 | - - 还有许多在默认情况下不会显示的输出,它们可以显示进程的页错误、有效组和组ID和其他更多的信息。 - - 常用交互命令: - - ‘B’:一些重要信息会以加粗字体显示(高亮)。这个命令可以切换粗体显示。 - - ‘b’: - - ‘D’或’S‘: 你将被提示输入一个值(以秒为单位),它会以设置的值作为刷新间隔。如果你这里输入了1,top将会每秒刷新。 top默认为3秒刷新 - - ‘l’、‘t’、‘m’: 切换负载、任务、内存信息的显示,这会相应地切换顶部的平均负载、任务/CPU状态和内存信息的概况显示。 - - ‘z’ : 切换彩色显示 - - ‘x’ 或者 ‘y’ - - 切换高亮信息:’x’将排序字段高亮显示(纵列);’y’将运行进程高亮显示(横行)。依赖于你的显示设置,你可能需要让输出彩色来看到这些高亮。 - - ‘u’: 特定用户的进程 - - ‘n’ 或 ‘#’: 任务的数量 - - ‘k’: 结束任务 - - **命令行选项** - - > top //每隔3秒显式所有进程的资源占用情况 - > - > top -u oracle -c //按照用户显示进程、并显示完整命令 - > - > top -d 2 //每隔2秒显式所有进程的资源占用情况 - > - > top -c //每隔3秒显式进程的资源占用情况,并显示进程的命令行参数(默认只有进程名) - > - > top -p 12345 -p 6789//每隔3秒显示pid是12345和pid是6789的两个进程的资源占用情况 - > - > top -d 2 -c -p 123456 //每隔2秒显示pid是12345的进程的资源使用情况,并显式该进程启动的命令行参数 - > - > top -n 设置显示多少次后就退出 - - **补充** - - top命令是Linux上进行系统监控的首选命令,但有时候却达不到我们的要求,比如当前这台服务器,top监控有很大的局限性。这台服务器运行着websphere集群,有两个节点服务,就是【top视图 01】中的老大、老二两个java进程,top命令的监控最小单位是进程,所以看不到我关心的java线程数和客户连接数,而这两个指标是java的web服务非常重要的指标,通常我用ps和netstate两个命令来补充top的不足。 - -#### vmstat - -​ vmstat命令是最常见的Linux/Unix监控工具,可以展现给定时间间隔的服务器的状态值,包括服务器的CPU使用率,内存使用,虚拟内存交换情况,IO读写情况。 - -​ 一般vmstat工具的使用是通过两个数字参数来完成的,第一个参数是采样的时间间隔数,单位是秒,第二个参数是采样的次数,如: - -image-20200604203027136 - -每个参数的含义: - -**Procs(进程)** - -| r: | 运行队列中进程数量,这个值也可以判断是否需要增加CPU。(长期大于1) | -| ---- | ------------------------------------------------------------ | -| b | 等待IO的进程数量。 | - -**Memory(内存)** - -| swpd | 使用虚拟内存大小,如果swpd的值不为0,但是SI,SO的值长期为0,这种情况不会影响系统性能。 | -| ----- | ------------------------------------------------------------ | -| free | 空闲物理内存大小。 | -| buff | 用作缓冲的内存大小。 | -| cache | 用作缓存的内存大小,如果cache的值大的时候,说明cache处的文件数多,如果频繁访问到的文件都能被cache处,那么磁盘的读IO bi会非常小。 | - -**Swap** - -| si | 每秒从交换区写到内存的大小,由磁盘调入内存。 | -| ---- | -------------------------------------------- | -| so | 每秒写入交换区的内存大小,由内存调入磁盘。 | - -注意:内存够用的时候,这2个值都是0,如果这2个值长期大于0时,系统性能会受到影响,磁盘IO和CPU资源都会被消耗。有些朋友看到空闲内存(free)很少的或接近于0时,就认为内存不够用了,不能光看这一点,还要结合si和so,如果free很少,但是si和so也很少(大多时候是0),那么不用担心,系统性能这时不会受到影响的。因为linux总是先把内存用光. - -**IO** - -| bi | 每秒读取的块数 | -| ---- | -------------- | -| bo | 每秒写入的块数 | - -注意:随机磁盘读写的时候,这2个值越大(如超出1024k),能看到CPU在IO等待的值也会越大。 - -**system(系统)** - -| in | 每秒中断数,包括时钟中断。 | -| ---- | -------------------------- | -| cs | 每秒上下文切换数。 | - -注意:上面2个值越大,会看到由内核消耗的CPU时间会越大。 - -**CPU(以百分比表示)** - -| us | 用户进程执行时间百分比(user time) us的值比较高时,说明用户进程消耗的CPU时间多,但是如果长期超50%的使用,那么我们就该考虑优化程序算法或者进行加速。 | -| ---- | ------------------------------------------------------------ | -| sy: | 内核系统进程执行时间百分比(system time) sy的值高时,说明系统内核消耗的CPU资源多,这并不是良性表现,我们应该检查原因。 | -| wa | IO等待时间百分比 wa的值高时,说明IO等待比较严重,这可能由于磁盘大量作随机访问造成,也有可能磁盘出现瓶颈(块操作)。 | -| id | 空闲时间百分比 | - -#### mpstat - -​ mpstat是一个实时监控工具,主要报告与CPU相关统计信息,在多核心cpu系统中,不仅可以查看cpu平均信息,还可以查看指定cpu信息。 - -> mpstat -P ALL //查看全部CPU的负载情况。 -> -> mpstat 2 5 //可指定间隔时间和次数。 - -| CPU: 处理器编号。关键字all表示统计信息计算为所有处理器之间的平均值。 | -| ------------------------------------------------------------ | -| %usr: 显示在用户级(应用程序)执行时发生的CPU利用率百分比。 | -| %nice: 显示以优先级较高的用户级别执行时发生的CPU利用率百分比。 | -| %sys: 显示在系统级(内核)执行时发生的CPU利用率百分比。请注意,这不包括维护硬件和软件的时间中断。 | -| %iowait: 显示系统具有未完成磁盘I / O请求的CPU或CPU空闲的时间百分比。 | -| %irq: 显示CPU或CPU用于服务硬件中断的时间百分比。 | -| %soft: 显示CPU或CPU用于服务软件中断的时间百分比。 | -| %steal: 显示虚拟CPU或CPU在管理程序为另一个虚拟处理器提供服务时非自愿等待的时间百分比。 | -| %guest: 显示CPU或CPU运行虚拟处理器所花费的时间百分比。 | - -#### sar - -系统活动情况报告,可以从多方面对系统的活动进行报告,包括:文件的读写情况、系统调用的使用情况、磁盘I/O、CPU效率、内存使用状况、进程活动及IPC有关的活动等 - -CPU相关: - -sar -p (查看全天) - -sar -u 1 10 (1:每隔一秒,10:写入10次) - -CPU输出项-详细说明 - -CPU:all 表示统计信息为所有 CPU 的平均值。 - -%user:显示在用户级别(application)运行使用 CPU 总时间的百分比。 - -%nice:显示在用户级别,用于nice操作,所占用 CPU 总时间的百分比。 - -%system:在核心级别(kernel)运行所使用 CPU 总时间的百分比。 - -%iowait:显示用于等待I/O操作占用 CPU 总时间的百分比。 - -%steal:管理程序(hypervisor)为另一个虚拟进程提供服务而等待虚拟 CPU 的百分比。 - -%idle:显示 CPU 空闲时间占用 CPU 总时间的百分比。 - -#### pidstat - -用于监控全部或指定进程的cpu、内存、线程、设备IO等系统资源的占用情况。 - -pidstat 和 pidstat -u -p ALL 是等效的。 - pidstat 默认显示了所有进程的cpu使用率。 - -详细说明 - -PID:进程ID - -%usr:进程在用户空间占用cpu的百分比 - -%system:进程在内核空间占用cpu的百分比 - -%guest:进程在虚拟机占用cpu的百分比 - -%CPU:进程占用cpu的百分比 - -CPU:处理进程的cpu编号 - -Command:当前进程对应的命令 - -### 内存 - -内存是为提高效率而生,实际分析问题的时候,内存出现问题可能不只是影响性能,而是影响服务或者引起其他问题。同样对于内存有些概念需要清楚: - -- 主存 -- 虚拟内存 -- 常驻内存 -- 地址空间 -- OOM -- 页缓存 -- 缺页 -- 换页 -- 交换空间 -- 交换 -- 用户分配器libc、glibc、libmalloc和mtmalloc -- LINUX内核级SLUB分配器 - -#### 分析工具 - -| 工具 | 描述 | -| ------- | ------------------------------ | -| free | 查看内存的使用情况 | -| top | 监控每个进程的内存使用情况 | -| vmstat | 虚拟内存统计信息 | -| sar -r | 查看内存 | -| sar | 查看CPU过去或未来时点CPU利用率 | -| pidstat | 查看每个进程的内存使用情况 | - -#### free - -free 命令显示系统内存的使用情况,包括物理内存、交换内存(swap)和内核缓冲区内存。 - -Mem 行(第二行)是内存的使用情况。 - Swap 行(第三行)是交换空间的使用情况。 - total 列显示系统总的可用物理内存和交换空间大小。 - used 列显示已经被使用的物理内存和交换空间。 - free 列显示还有多少物理内存和交换空间可用使用。 - shared 列显示被共享使用的物理内存大小。 - buff/cache 列显示被 buffer 和 cache 使用的物理内存大小。 - available 列显示还可以被应用程序使用的物理内存大小。 - -常用命令: - -> free -> -> free -g 以GB显示 -> -> free -m 以MB显示 -> -> free -h 自动转换展示 -> -> free -h -s 3 有时我们需要持续的观察内存的状况,此时可以使用 -s 选项并指定间隔的秒数 - -所以从应用程序的角度来说,**available = free + buffer + cache** - -可用内存=系统free memory+buffers+cached。 - -#### top - -请参考上面top的详解 - -#### vmstat - -请参考上面vmstat的详解 - -#### sar - -sar -r #查看内存使用情况 - -详解: - -kbmemfree 空闲的物理内存大小 - -kbmemused 使用中的物理内存大小 - -%memused 物理内存使用率 - -kbbuffers 内核中作为缓冲区使用的物理内存大小,kbbuffers和kbcached:这两个值就是free命令中的buffer 和cache. - -kbcached 缓存的文件大小 - -kbcommit 保证当前系统正常运行所需要的最小内存,即为了确保内存不溢出而需要的最少内存(物理内存 +Swap分区) - -commt 这个值是kbcommit与内存总量(物理内存+swap分区)的一个百分比的值 - -#### pidstat - -pidstat -r 查看内存使用情况 pidstat将显示各活动进程的内存使用统计 - -PID:进程标识符 - -Minflt/s:任务每秒发生的次要错误,不需要从磁盘中加载页 - -Majflt/s:任务每秒发生的主要错误,需要从磁盘中加载页 - -VSZ:虚拟地址大小,虚拟内存的使用KB - -RSS:常驻集合大小,非交换区五里内存使用KB - -Command:task命令名 - -### 磁盘IO - -磁盘通常是计算机最慢的子系统,也是最容易出现性能瓶颈的地方,因为磁盘离 CPU 距离最远而且 CPU 访问磁盘要涉及到机械操作,比如转轴、寻轨等。访问硬盘和访问内存之间的速度差别是以数量级来计算的,就像1天和1分钟的差别一样。要监测 IO 性能,有必要了解一下基本原理和 Linux 是如何处理硬盘和内存之间的 IO 的。 - -在理解磁盘IO之前,同样我们需要理解一些概念,例如: - -- 文件系统 -- VFS -- 文件系统缓存 -- 页缓存page cache -- 缓冲区高速缓存buffer cache -- 目录缓存 -- inode -- inode缓存 -- noop调用策略 - -#### 分析工具 - -| 工具 | 描述 | -| ------- | ---------------------------- | -| iostat | 磁盘详细统计信息 | -| iotop | 按进程查看磁盘IO统计信息 | -| pidstat | 查看每个进程的磁盘IO使用情况 | - -#### iostat - -iostat工具将对系统的磁盘操作活动进行监视。它的特点是汇报磁盘活动统计情况,同时也会汇报出CPU使用情况 - -image-20200604203027136 - -CPU属性 - -%user:CPU处在用户模式下的时间百分比。 - -%nice:CPU处在带NICE值的用户模式下的时间百分比。 - -%system:CPU处在系统模式下的时间百分比。 - -%iowait:CPU等待输入输出完成时间的百分比。 - -%steal:管理程序维护另一个虚拟处理器时,虚拟CPU的无意识等待时间百分比。 - -%idle:CPU空闲时间百分比。 - -备注: - -如果%iowait的值过高,表示硬盘存在I/O瓶颈 - -如果%idle值高,表示CPU较空闲 - -如果%idle值高但系统响应慢时,可能是CPU等待分配内存,应加大内存容量。 - -如果%idle值持续低于10,表明CPU处理能力相对较低,系统中最需要解决的资源是CPU。 - -Device属性 - -tps:该设备每秒的传输次数 - -kB_read/s:每秒从设备(drive expressed)读取的数据量; - -kB_wrtn/s:每秒向设备(drive expressed)写入的数据量; - -kB_read: 读取的总数据量; - -kB_wrtn:写入的总数量数据量 - -常用命令: - -iostat 2 3 每隔2秒刷新显示,且显示3次 - -iostat -m 以M为单位显示所有信息 - -查看设备使用率(%util)、响应时间(await) - -iostat -d -x -k 1 1 - -#### iotop - -在一般运维工作中经常会遇到这么一个场景,服务器的IO负载很高(iostat中的util),但是无法快速的定位到IO负载的来源进程和来源文件导致无法进行相应的策略来解决问题。 - -如果你想检查那个进程实际在做 I/O,那么运行 `iotop` 命令加上 `-o` 或者 `--only` 参数。 - -iotop --only - -#### pidstat - -显示各个进程的IO使用情况 - -pidstat -d - -报告IO统计显示以下信息: - -- PID:进程id -- kB_rd/s:每秒从磁盘读取的KB -- kB_wr/s:每秒写入磁盘KB -- kB_ccwr/s:任务取消的写入磁盘的KB。当任务截断脏的pagecache的时候会发生。 -- COMMAND:task的命令名 - -### 网络 - -#### 分析工具 - -| ping | 测试网络的连通性 | -| -------- | ---------------------------- | -| netstat | 检验本机各端口的网络连接情况 | -| hostname | 查看主机和域名 | - -#### ping - -常用命令参数: - --d 使用Socket的SO_DEBUG功能。 - --f 极限检测。大量且快速地送网络封包给一台机器,看它的回应。 - --n 只输出数值。 - --q 不显示任何传送封包的信息,只显示最后的结果。 - --r 忽略普通的Routing Table,直接将数据包送到远端主机上。通常是查看本机的网络接口是否有问题。 - --R 记录路由过程。 - --v 详细显示指令的执行过程。 - -

-c 数目:在发送指定数目的包后停止。 - --i 秒数:设定间隔几秒送一个网络封包给一台机器,预设值是一秒送一次。 - --I 网络界面:使用指定的网络界面送出数据包。 - --l 前置载入:设置在送出要求信息之前,先行发出的数据包。 - --p 范本样式:设置填满数据包的范本样式。 - --s 字节数:指定发送的数据字节数,预设值是56,加上8字节的ICMP头,一共是64ICMP数据字节。 - --t 存活数值:设置存活数值TTL的大小。 - -> ping -b 192.168.120.1 --ping网关 -> -> ping -c 10 192.168.120.206 --ping指定次数 -> -> ping -c 10 -i 0.5 192.168.120.206 --时间间隔和次数限制的ping - -#### netstat - -netstat命令是一个监控TCP/IP网络的非常有用的工具,它可以显示路由表、实际的网络连接以及每一个网络接口设备的状态信息。 - -netstat [选项] - -``` --a或--all:显示所有连线中的Socket; --a (all) 显示所有选项,默认不显示LISTEN相关。 --t (tcp) 仅显示tcp相关选项。 --u (udp) 仅显示udp相关选项。 --n 拒绝显示别名,能显示数字的全部转化成数字。 --l 仅列出有在 Listen (监听) 的服务状态。 --p 显示建立相关链接的程序名 --r 显示路由信息,路由表 --e 显示扩展信息,例如uid等 --s 按各个协议进行统计 --c 每隔一个固定时间,执行该netstat命令。 -``` - -常用命令: - -列出所有端口情况 - -``` -netstat -a # 列出所有端口 -netstat -at # 列出所有TCP端口 -netstat -au # 列出所有UDP端口 -``` - -列出所有处于监听状态的 Sockets - -``` -netstat -l # 只显示监听端口 -netstat -lt # 显示监听TCP端口 -netstat -lu # 显示监听UDP端口 -netstat -lx # 显示监听UNIX端口 -``` - -显示每个协议的统计信息 - -``` -netstat -s # 显示所有端口的统计信息 -netstat -st # 显示所有TCP的统计信息 -netstat -su # 显示所有UDP的统计信息 -``` - -显示 PID 和进程名称 - -``` -netstat -p -``` - -显示网络统计信息 - -``` -netstat -s -``` - -统计机器中网络连接各个状态个数 - -``` -netstat` `-an | ``awk` `'/^tcp/ {++S[$NF]} END {for (a in S) print a,S[a]} ' -``` - -**补充netstat网络状态详解:** - -一个正常的TCP连接,都会有三个阶段:1、TCP三次握手;2、数据传送;3、TCP四次挥手 - -在这里插入图片描述 - - - -**TCP的连接释放** - -在这里插入图片描述 - -``` -LISTEN:侦听来自远方的TCP端口的连接请求 -SYN-SENT:再发送连接请求后等待匹配的连接请求(如果有大量这样的状态包,检查是否中招了) -SYN-RECEIVED:再收到和发送一个连接请求后等待对方对连接请求的确认(如有大量此状态估计被flood攻击了) -ESTABLISHED:代表一个打开的连接 -FIN-WAIT-1:等待远程TCP连接中断请求,或先前的连接中断请求的确认 -FIN-WAIT-2:从远程TCP等待连接中断请求 -CLOSE-WAIT:等待从本地用户发来的连接中断请求 -CLOSING:等待远程TCP对连接中断的确认 -LAST-ACK:等待原来的发向远程TCP的连接中断请求的确认(不是什么好东西,此项出现,检查是否被攻击) -TIME-WAIT:等待足够的时间以确保远程TCP接收到连接中断请求的确认 -CLOSED:没有任何连接状态 -``` - ------- - -本文参考的文章: https://rdc.hundsun.com/portal/article/731.html?ref=myread - - - diff --git a/docs/operating-system/Shell.md b/docs/operating-system/Shell.md index 4a89061fc27..c88ebdd6939 100644 --- a/docs/operating-system/Shell.md +++ b/docs/operating-system/Shell.md @@ -262,7 +262,7 @@ echo $length2 #输出:5 echo ${array[2]} #输出:3 unset array[1]# 删除下标为1的元素也就是删除第二个元素 for i in ${array[@]};do echo $i ;done # 遍历数组,输出: 1 3 4 5 -unset arr_number; # 删除数组中的所有元素 +unset array; # 删除数组中的所有元素 for i in ${array[@]};do echo $i ;done # 遍历数组,数组元素为空,没有任何输出内容 ``` diff --git a/docs/operating-system/basis.md b/docs/operating-system/basis.md index 61133c1775e..dd28a91d9a5 100644 --- a/docs/operating-system/basis.md +++ b/docs/operating-system/basis.md @@ -64,7 +64,7 @@ 🙋 **我:** 好的! 下图是 Java 内存区域,我们从 JVM 的角度来说一下线程和进程之间的关系吧! -> 如果你对 Java 内存区域 (运行时数据区) 这部分知识不太了解的话可以阅读一下这篇文章:[《可能是把 Java 内存区域讲的最清楚的一篇文章》](<[https://snailclimb.gitee.io/javaguide/#/docs/java/jvm/Java%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F](https://snailclimb.gitee.io/javaguide/#/docs/java/jvm/Java内存区域)>) +> 如果你对 Java 内存区域 (运行时数据区) 这部分知识不太了解的话可以阅读一下这篇文章:[《可能是把 Java 内存区域讲的最清楚的一篇文章》](https://snailclimb.gitee.io/javaguide/#/docs/java/jvm/Java内存区域) ![jvm运行时数据区域](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/ff96fed0e2a354bb16bbc84dcedf503a.png) @@ -165,7 +165,7 @@ #### 快表 -为了解决虚拟地址到物理地址的转换速度,操作系统在 **页表方案** 基础之上引入了 **快表** 来加速虚拟地址到物理地址的转换。我们可以把块表理解为一种特殊的高速缓冲存储器(Cache),其中的内容是页表的一部分或者全部内容。作为页表的 Cache,它的作用与页表相似,但是提高了访问速率。由于采用页表做地址转换,读写内存数据时 CPU 要访问两次主存。有了快表,有时只要访问一次高速缓冲存储器,一次主存,这样可加速查找并提高指令执行速度。 +为了解决虚拟地址到物理地址的转换速度,操作系统在 **页表方案** 基础之上引入了 **快表** 来加速虚拟地址到物理地址的转换。我们可以把快表理解为一种特殊的高速缓冲存储器(Cache),其中的内容是页表的一部分或者全部内容。作为页表的 Cache,它的作用与页表相似,但是提高了访问速率。由于采用页表做地址转换,读写内存数据时 CPU 要访问两次主存。有了快表,有时只要访问一次高速缓冲存储器,一次主存,这样可加速查找并提高指令执行速度。 使用快表之后的地址转换流程是这样的: @@ -271,7 +271,9 @@ ### 4.3 虚拟存储器 -👨‍💻**面试官** :都说了虚拟内存了。你再讲讲**虚拟存储器**把! +> **勘误:虚拟存储器又叫做虚拟内存,都是 Virtual Memory 的翻译,属于同一个概念。** + +👨‍💻**面试官** :~~都说了虚拟内存了。你再讲讲**虚拟存储器**把!~~ 🙋 **我** : diff --git "a/docs/operating-system/\345\256\214\345\205\250\344\275\277\347\224\250GNU_Linux\345\255\246\344\271\240.md" "b/docs/operating-system/\345\256\214\345\205\250\344\275\277\347\224\250GNU_Linux\345\255\246\344\271\240.md" deleted file mode 100644 index d9e171dabb8..00000000000 --- "a/docs/operating-system/\345\256\214\345\205\250\344\275\277\347\224\250GNU_Linux\345\255\246\344\271\240.md" +++ /dev/null @@ -1,252 +0,0 @@ - - - * [完全使用GNU/Linux学习](#完全使用gnulinux学习) - * [为什么要写这篇文章?](#为什么要写这篇文章) - * [为什么我要从Windows切换到Linux?](#为什么我要从windows切换到linux) - * [Linux作为日常使用的缺点](#linux作为日常使用的缺点) - * [硬件驱动问题](#硬件驱动问题) - * [软件问题](#软件问题) - * [你真的需要完全使用Linux吗?](#你真的需要完全使用linux吗) - * [结尾](#结尾) - * [我使用Debian/Ubuntu时遇到的问题](#我使用debianubuntu时遇到的问题) - * [IDEA编辑Markdown预渲染问题](#idea编辑markdown预渲染问题) - * [wifi适配器找不到](#wifi适配器找不到) - * [XMind安装](#xmind安装) - * [Fcitx候选框的定位问题](#fcitx候选框的定位问题) - - - -# 完全使用GNU/Linux学习 - -喔,看到这个标题千万不要以为我要写和王垠前辈一样的内容啊,嘿嘿。不过在这里还是献上王垠前辈的那篇文章的链接吧:[完全用Linux工作](https://www.douban.com/group/topic/12121637/)。 - -## 为什么要写这篇文章? - -首先介绍本篇文章产出的时间,现在是2020/04/06。在三,四天之前,我其实并没有写这篇文章的打算,但是这三,四天以来,我一直在忙活从Ubuntu18换到Debian10 Buster的事情,没有时间写代码,手确实有些痒了。你可能想象不到,我这个之前一直使用Ubuntu的人,只是切换到Debian就花这么长时间,你可能以为我是在劝退各位同学,其实不是的,我只是想表达:我对Linux并不熟悉,这其中一部分原因是我使用的是对用户较为友好的发行版Ubuntu,另一部分原因是我仍然没有那么大的动力去学习Linux,即使它一直作为我的日常使用。 - -这篇文章并不是吹嘘或贬低Windows和Linux系统,而是想记录一下我一直以来使用Linux作为日常学习的心得,以及这几天再度折腾Debian以来的感触。 - - - -## 为什么我要从Windows切换到Linux? - -Windows是商业软件,这使它具备易用的性质。Linux是自由软件,这使得它拥有开源的性质。 - -易用软件通常带来的是对用户的友好度,以致于Windows发展至今,被许许多多的普通用户所采用。自由软件通常带来的是其社区的发展,所以你现在可以在网上看到许多如 ask ubuntu 这样的论坛。 - -我非常赞同《完全用Linux工作》中的一个观点: **UNIX 不是计算机专家的专利。** - -我对这句话的理解就是:即使你学习或工作的方向不是计算机,但你仍然可以去学习Unix/Linux,如果你是计算机方向的同学,那么,你就更应该去学习Unix/Linux了。 - -但这只是我从Win切换到Linux的一部分原因,另一个很重要的原因是我受够了Windows的 “易用性”。这里的易用性并不是说我排斥Windows的人性化,而是因为人性化给我带来了很多学习上的困难。举个很简单的栗子:你在学习一项技术的时候,无论是否有面试造火箭的需要,你是否都会好奇想了解其原理和实现,即使你可能知道它很复杂。 - -**为什么你会好奇一个事物的源头?** - -我个人认为的答案是:有趣的事情就在眼前,为什么不去了解它呢? - -而Windows只是有趣,但它并不在“眼前”。 - -我个人的体验哈,不知道有没有同学和我一样的经历,在很多时候,你的Windows可能会出现一些莫名奇妙的问题,但你却不知道如何解决它,你只能求助搜索引擎,当你解决完问题后,你不会想要去了解为什么会发生这种问题,因为Windows太庞大了。 - -就比如: 我现在安装了Git,使用起来没有任何问题。但等到过一段时间后,Git莫名奇妙的不能使用了,明明你啥都没干。更甚之,有一些流氓问题或流氓软件不能被解决和被屏蔽。 - -问题出现了,总得要解决吧,所以此时你开始在互联网上查询相关问题的解决方法,如果你的运气好,那么有人可能遇到过和你出现相同的问题,你也因此可能会得到答案。不过一般的答案只是教你怎么解决的,如打开注册表,添加或删除某个key,你不会想要知道为什么做,因为对于初学者来说,当你看到注册表那么多的内容时,你只想着不出错就行了,哪还有心思去学习这玩意啊。如果你的运气不好,且并没有更换系统的打算,那么你可能会将就着使用,但此时,你的心里可能已经衍生了对Windows的厌烦情绪。 - -我对流氓软件的定义是:当你想让一个软件如你的想法停止运行或停止弹出广告的时候,这个软件不能或不能做的很好的达到你的要求时,这就是一个流氓软件。你也许会说,每个人都有不同的要求,软件怎么可能达到每个人的标准呢?但我指的是停止和停止弹出广告等这样最基本的诉求,如果一个软件连最基本的诉求都实现不了,又何必再使用它呢? - -综上所述,我从Window切换到Linux的最主要的原因有:**学习和自由。** - -是的,你不得不承认Linux是你学习计算机的非常好的环境,与C/C++天然的集成,比你在Windows上冷冰冰的安装一个IDE就开始敲起代码来,显得多了那么一点感觉。 - -还有一点,可能有的同学和我一样,刚接触Linux的时候,是在Windows上安装一个虚拟机环境或使用Docker来进行学习。不可否认,这确实是在Windows上学习Linux的主要途径了,但是你有没有感觉到,你在采取这种方式学习的时候,对Linux始终有种陌生感,似乎我只是在为了学习而学习。 - -产生这种想法的主要原因就是你没有融入到Linux环境之中,当你融入到Linux环境之中时,你不再只是需要学习那些操作命令,你会不可避免的遇到某个你从来没有接触过的问题,这个问题不是你在Windows上“丢失图标”的那种烦人问题,而可能是令你有些害怕的因为Nvidia的驱动而黑屏的问题。你也会在互联网上查询为什么会出现这种问题,但你得到的并不是“修改注册表”这种答案,而是会学习到:为什么Nvidia在Linux上会出现这种问题?我怎么做才能解决驱动问题?其他驱动是否也有类似Nvidia这种问题? 当你解决问题后,你的电脑开始正常工作了,你便开始使用它作为你的日常使用... - -关于使用Linux学习的原因的最后一点是我认为自己不够慎独,不够克制。当我使用Windows的时候,并不能完全克制住自己接触那些新鲜游戏的念头,我玩起游戏来,通常会连续很长时间,可能是一天-_-。不过我并不是说Linux上没有游戏,相反,Linux是对很多游戏的支持是很好的,你可以玩到很多游戏,但你是否会因为使用Linux对游戏不再那么执着,至少我是如此了。这一点可以归结为“使用Linux对戒游戏有帮助吧” ,哈哈。 - -再谈谈自由: - -我对自由的理解是:软件在你的掌控之中,你可以了解它的每一部分,你可以去到你想到达的地方,不受任何限制,这只取决于你愿不愿意。 - -来看看基本的Linux目录吧: - -![Linux目录](../../media/pictures/linux/Linux目录.png) - -这些目录你可能有很多都不认识,但没关系,因为这就是Linux系统(大部分)所有的目录了,你稍微了解下,就知道这些目录里放的是什么文件了。 - -这也是我个人的体验而已,总之,Linux的自由是一种开源精神,比我描述的可大的多。至于Windows,我到现在连C盘的目录放了些什么都不太熟悉,但我并不是在贬低Windows,因为这就是Windows易用性的代价,相应的,Linux作为自由软件,它也有很多缺点。 - - - -## Linux作为日常使用的缺点 - -### 硬件驱动问题 - -硬件驱动问题一般是在安装Linux时会出现的问题,根据个人电脑配置的不同,你的电脑的硬件驱动可能与要安装的Linux发行版不兼容,导致系统出现相应的问题。我这几天对驱动问题最深刻的体会就明白了为啥Linus大神会吐槽: “Nvidia Fuck You”。很多驱动厂商对Linux系统是闭源的,你可以下载这些厂商的驱动,但是能不能用,或者用起来有什么毛病都得你自己买单。 - -随着Linux开始在普通用户中变得流行起来,我相信今后Linux的生态会发展的越来越好,且现在很多Linux发行版对各种硬件的兼容性也越来越好,就以我之前使用的Ubuntu18来说,Nvidia,Wifi,蓝牙等驱动使用都是没啥问题的。我现在使用的Debian10 Buster对Nvidia的支持可能还不是那么好,使用起来总有一些小毛病,不过无伤大雅,其实没毛病我还有点不适应,不是说Debian是Ubuntu的爸爸吗,哈哈。 - - - -### 软件问题 - -不得不承认的一点是Linux的软件生态确实没有Windows那么丰富,你在考虑切换系统之前,必须先调查清楚Linux上是否有你必需的软件,你所需的软件是否支持跨平台或者是否有可替代的应用。我个人对软件要求较为简单,大部分都是生产力工具,其他的应用如娱乐软件之类的都可以使用网页版作为替代。如果你在Linux系统上想尝试游戏的话,我认为也是OK的,因为我也尝试过Linux Dota2 ,体验非常好(不是广告-_-)。不过大多数国内游戏厂商对Linux的支持都是很差的,所以如果过不了这道坎,也不要切换系统了。 - -软件问题其实可以分为2部分看待,一部分就是刚刚介绍过的生态问题,另一部分就是当你在使用某些软件的时候,总会出现某些小Bug。 - -就以Fcitx来说,Fcitx是一款通用的Linux输入法框架,被称为小企鹅输入法,很多输入法都是在Fcitx之上开发的,如搜狗,Googlepinyin,Sunpinyin等。使用过Fcitx的同学可能会遇到这种问题:当你在使用Fcitx在某些软件上打字时,候选框并不会跟随你光标的位置,而是总会固定在某一个位置,并且你无法改变,这个问题是我目前见过的最大Bug。不过这个Bug只在部分软件上有,在Chrome,Typora上都没有这个问题,这让我怀疑是软件的国际化问题,而非Fcitx问题。 - -所以第二个部分总结起来就是某些软件可能会出现某些未知的Bug,你得寻求解决的办法,或者忍耐使用,使用Linux也是得牺牲一些代价的。 - - - -## 你真的需要完全使用Linux吗? - -说到这里,其实我想借用知乎某位前辈的话来表达一下我的真实想法: “**Linux最好的地方在与开放自由,最大的毛病也是在这里。普通人没有能力去选择,也没有时间做选择。透明就一定好么?也有很多人喜欢被安排啊!**“ ([知乎 - 汉卿](https://www.zhihu.com/question/309704636)) - -就像我开头说过的: “我对Linux并不熟悉,这其中一部分原因是我使用的是对用户较为友好的发行版Ubuntu,另一部分原因是我仍然没有那么大的动力去学习Linux,即使它一直作为我的日常使用。” - -我完全使用Linux是为了学习和自由,我确实在Linux上感受到了自由,且学到了很多东西,但我却一直沉溺在这种使用Linux带来的满足感之中,并不能真正理解Linux给我们带来的到底是什么。 - -这次从Ubuntu切换到Debian的原因是我想尝试换个新的环境,但是当我花了3,4天后,我明白了:我只是呆在一个地方久了,想换个新地方而已,但老地方不一定坏,因为我都没怎么了解过这个老地方,就像当初我从Windows换到Linux那样,我都没有深入的了解过Windows就换了,那一段时间我还抱怨Windows的各种缺点,现在看来,非常可笑。 - - - -#### 结尾 - -一文把想说的话几乎都给说了,个人文笔有限,且本文主观意识太强,如果觉得本文不符合您的胃口,就当看个笑话吧。 - - ---- - -## 我使用Debian/Ubuntu时遇到的问题 - -**以下内容是我在Debian10 Buster下遇到的问题以及相关解决办法, -使用Ubuntu和Debian其他版本的同学也可借鉴。** - -PS:欢迎各位同学在此处写下你遇到的问题和解决办法。 - -### IDEA编辑Markdown预渲染问题 -这个问题花了我很长时间。 - -当我安装IDEA后,使用它编辑markdown文件的时候,就出现了如下图所示的情况: - -![Debian10下IDEA的Markdown预渲染问题](../../media/pictures/linux/Debian10下IDEA的Markdown预渲染问题.png) - -你可以看到右边渲染的画面明显有问题。刚开始的时候我一度怀疑是IDEA版本的问题, -于是我又安装IDEA其他版本,但也没有任何作用,这时我怀疑是显卡的原因: - -![我的电脑配置](../../media/pictures/linux/我的电脑配置.png) - -可以看到使用的是Intel的核显,于是当我查询相关资料,使用脚本将核显换为了独显,这里没留截图,当你换到独显后, -图形会显示独显的配置,使用nvidia-smi命令可以查看独显使用状态。 -于是我满怀期待的打开IDEA,但还是无济于事。当我以为真的是Debian的Bug的时候, -我又发现Bumblebee可以管理显卡,何不一试?于是我安装Bumblebee后,使用optirun命令启动IDEA,没想到啊, -还真是可以: - -![Debian10下IDEA的Markdown预渲染解决后](../../media/pictures/linux/Debian10下IDEA的Markdown预渲染解决后.png) - -我真的就很奇怪,同样是使用了独显,为什么optirun启动就可以正常显示。 -于是我后来又查询optirun是否开启了gpu加速,但很可惜,我并没有得到相关答案,不过这让我确定了这个问题出现在 -显卡上。如果有知道原因的同学,敬请告之,感激不尽。 - - -### wifi适配器找不到 -我猜(不确定)这个问题应该发生在大多数使用联想笔记本的同学的电脑上,不止Debian,且Ubuntu也有这个问题。 -当安装完系统后,我们打开设置会发现wifi一栏显示 “wifi适配器找不到” 此类的错误信息。 -这个问题的大概原因是:无线网络适配器被阻塞了,需要手动将电脑上的wifi开关打开,而在我的笔记本上并wifi开关, -所以可以猜测是联想网络驱动的问题。 -可以使用 rfkill list all命令查询你的wlan是否被阻塞了,没有此命令的同学可以使用 - -````text -sudo apt-get install rfkill -```` - -安装,当wlan显示Hard blocked: true , 就证明你的无线驱动被阻塞了。 -解决办法是将阻塞无限驱动的那个模块从内核中移除掉,直接在 /etc/modprobe.d -目录下编辑 blacklist.conf文件,其内容为: - -````text -blacklist ideapad_laptop -```` - -文件名不一定要与我的一致,但是要以.conf结尾。 -你可以将modprobe.d目录下的文件理解为黑名单文件, -当Linux启动时就不会加载conf文件指定的模块, -这里的 ideapad_laptop 就是我们需要移除的那个无线模块。 - -**后遗症: -当我们移除 ideapad_laptop 模块后,以后开机的时候,有时会出现 -蓝牙适配器找不到的情况,之前在Ubuntu上却并未发现这种问题, -看来Debian在驱动方面没有Ubuntu做的好,不过这也是可以理解的, -而且大多数时候还是可以正常使用的-_-。** - - -### XMind安装 -XMind是使用Java编写的,依赖于Openjdk8。所以在Linux上使用XMind, -首先需要有Openjdk8的环境。 -其次启动的时候需要编写Shell脚本来启动(不是唯一办法,但却是非常简单的办法),没想到吧,我也没想到, -这也是我趟过很多坑才玩出来的。 - -首先我们需要准备一张XMind的软件启动图片:XMind.png, -这个我已经放到[目录](https://github.com/guang19/framework-learning/tree/dev/img/linux) -下了,需要的同学请自取。 - -其次我们进入XMind_amd64目录下,32位系统的同学进入XMind_i386目录, -我们创建并编辑 start.sh 脚本,其内容为: - -````text -#!/bin/bash -cd /home/guang19/SDK/xmind/XMind_amd64 (这个路径为你的XMind脚本的路径) -./XMind -```` - -这个脚本的内容很简单吧,当启动脚本的时候,进入目录,直接启动XMind。 - -脚本写完后需要让它能够被执行,使用 - -````text -chmod +x start.sh -```` - -命令让start.sh可以被执行。 - -此时你可以尝试执行 ./start.sh 命令来启动XMind,启动成功的话, -就已经完成了99%了,如果启动不成功,可以再检测下前面的步骤是否有误。 - -如果以后你只想用Shell启动XMind的话,那么到此也就为止了,连上面所说的图片都不需要了。 -如果你想更方便的启动的话,那么就需要创建桌面文件启动。 -在Debian/Ubuntu下,你所看到的桌面文件,都存储在 /usr/share/applications -目录下面(也有的在.local/share/applications目录下),这个目录下文件全是以.desktop结尾。 -我们现在就需要在这个目录下创建xmind.desktop文件(名字可以不叫xmind)。 - -其内容为: - -````text -[Desktop Entry] -Encoding=UTF-8 -Name=XMind -Type=Application -Exec=sh /home/guang19/SDK/xmind/XMind_amd64/start.sh -Icon=/home/guang19/SDK/xmind/XMind.png -```` - -我们暂时只需要理解Icon和Exec属性。 -Icon就是你在桌面上看到的应用的图标,把Icon的路径改为你XMind.png的路径就行了。 -再看Exec属性,当我们在桌面上点击XMind的图标的时候,就会执行Exec对应的命令或脚本, -我们把Exec改为start.sh文件的路径就行了,别掉了sh命令,因为start.sh是脚本, -需要sh命令启动。 - -以上步骤完成,保存desktop文件后,你就可以在桌面上看到XMind应用了。 - - -### Fcitx候选框的定位问题 -这个问题贴一张我处境的截图就明白了: - -![Fcitx候选框定位问题](../../media/pictures/linux/Fcitx候选框定位问题.png) - -可以看到我的光标定位在第207行,但是我输入法的候选框停留在IDEA的左下角。 -为什么我要说停留在IDEA的左下角?因为就目前我的使用而言,这个问题只在IDEA下存在, -不仅是Debian,Ubuntu也存在这种问题,我个人认为这应该是IDEA的问题, -查到的相关文章大部分都是说Swing的问题,看来这个问题还真是比较困难了。 -如果有同学知道解决办法,还请不吝分享,非常感谢。 \ No newline at end of file diff --git a/docs/questions/java-learning-path-and-methods.md b/docs/questions/java-learning-path-and-methods.md index 87f7f19bbfa..87c89deb63b 100644 --- a/docs/questions/java-learning-path-and-methods.md +++ b/docs/questions/java-learning-path-and-methods.md @@ -35,7 +35,7 @@ 3. [【加餐】一些重要的Java程序设计题](https://snailclimb.gitee.io/javaguide/#/docs/java/Java程序设计题) 4. [【选看】J2EE 基础知识](https://snailclimb.gitee.io/javaguide/#/docs/java/J2EE基础知识) -> 我们的网站需要运行在“操作系统”之上(一般是部署在Linux系统),并且我们与网站的每次交互都需要经过“网络”,需要经历三次握手和四次挥手才能简历连接,需要HTTP才能发出请求已经拿到网站后台的相应。所以第二步,我推荐可以适当花时间看一下 **操作系统与计算机网络 方面的知识。** 但是,不做强求!你抽时间一定要补上就行! +> 我们的网站需要运行在“操作系统”之上(一般是部署在Linux系统),并且我们与网站的每次交互都需要经过“网络”,需要经历三次握手和四次挥手才能建立连接,需要HTTP才能发出请求已经拿到网站后台的相应。所以第二步,我推荐可以适当花时间看一下 **操作系统与计算机网络 方面的知识。** 但是,不做强求!你抽时间一定要补上就行! ### step 2(可选):操作系统与计算机网络 @@ -172,11 +172,11 @@ ### step 10:深入学习 -可以再回来看一下多线程方面的知识,还可以利用业余时间学习一下 **[NIO](https://github.com/Snailclimb/JavaGuide#io "NIO")** 和 **Netty** ,这样简历上也可以多点东西。如果想去大厂,**[JVM](https://github.com/Snailclimb/JavaGuide#jvm "JVM")** 的一些知识也是必学的(**Java 内存区域、虚拟机垃圾算法、虚拟垃圾收集器、JVM 内存管理**)推荐《深入理解 Java 虚拟机:JVM 高级特性与最佳实践(最新第二版》和《实战 Java 虚拟机》,如果嫌看书麻烦的话,你也可以看我整理的文档。 +可以再回来看一下多线程方面的知识,还可以利用业余时间学习一下 **[NIO](https://github.com/Snailclimb/JavaGuide#io "NIO")** 和 **Netty** ,这样简历上也可以多点东西。如果想去大厂,**[JVM](https://github.com/Snailclimb/JavaGuide#jvm "JVM")** 的一些知识也是必学的(**Java 内存区域、虚拟机垃圾算法、虚拟垃圾收集器、JVM 内存管理**)推荐《深入理解 Java 虚拟机:JVM 高级特性与最佳实践(最新第二版)》和《实战 Java 虚拟机》,如果嫌看书麻烦的话,你也可以看我整理的文档。 另外,现在微服务特别火,很多公司在面试也明确要求需要微服务方面的知识。如果有精力的话可以去学一下 SpringCloud 生态系统微服务方面的东西。 -> **微服务的概念庞大,技术种类也很多,但是目前大型互联网公司广泛采用的,**实话实话这些东西我不在行,自己没有真实做过微服务的项目。不过下面是我自己总结的一些关于微服务比价重要的知识,选学。 +> **微服务的概念庞大,技术种类也很多,但是目前大型互联网公司广泛采用的,** 实话实话这些东西我不在行,自己没有真实做过微服务的项目。不过下面是我自己总结的一些关于微服务比价重要的知识,选学。 ### step 11:微服务 diff --git a/docs/questions/java-learning-website-blog.md b/docs/questions/java-learning-website-blog.md index 06ef65fe14c..d9ba95fae99 100644 --- a/docs/questions/java-learning-website-blog.md +++ b/docs/questions/java-learning-website-blog.md @@ -68,7 +68,7 @@ ### 一些不错的博客/Github 推荐 -- SnailClimb 的 Github :[https://github.com/Snailclimb](https://github.com/Snailclimb "https://github.com/Snailclimb") 。(自荐一波哈!主要专注在 Java 基础和进阶、Spring、Spiring Boot、Java 面试这方面。) +- SnailClimb 的 Github :[https://github.com/Snailclimb](https://github.com/Snailclimb "https://github.com/Snailclimb") 。(自荐一波哈!主要专注在 Java 基础和进阶、Spring、Spring Boot、Java 面试这方面。) - 徐靖峰个人博客 :[https://www.cnkirito.moe/](https://www.cnkirito.moe/ "https://www.cnkirito.moe/")(探讨 Java 生态的知识点,内容覆盖分布式服务治理、微服务、性能调优、各类源码分析) - 田小波:[http://www.tianxiaobo.com/](http://www.tianxiaobo.com/ "http://www.tianxiaobo.com/") (Java 、Spring 、MyBatis 、Dubbo) - 周立的博客: [http://www.itmuch.com/](http://www.itmuch.com/ "http://www.itmuch.com/")(Spring Cloud、Docker、Kubernetes,及其相关生态的技术) @@ -77,4 +77,4 @@ - 纯洁的微笑 : [http://www.ityouknow.com/](http://www.ityouknow.com/ "http://www.ityouknow.com/") (Java、SpringBoot、Spring Cloud) - 芋道源码: [http://www.iocoder.cn/](http://www.iocoder.cn/ "http://www.iocoder.cn/") (专注源码)。 - 欢迎自荐 -- ...... \ No newline at end of file +- ...... diff --git a/docs/system-design/authority-certification/JWT-advantages-and-disadvantages.md "b/docs/system-design/authority-certification/JWT\344\274\230\347\274\272\347\202\271\345\210\206\346\236\220\344\273\245\345\217\212\345\270\270\350\247\201\351\227\256\351\242\230\350\247\243\345\206\263\346\226\271\346\241\210.md" similarity index 100% rename from docs/system-design/authority-certification/JWT-advantages-and-disadvantages.md rename to "docs/system-design/authority-certification/JWT\344\274\230\347\274\272\347\202\271\345\210\206\346\236\220\344\273\245\345\217\212\345\270\270\350\247\201\351\227\256\351\242\230\350\247\243\345\206\263\346\226\271\346\241\210.md" diff --git a/docs/system-design/authority-certification/sso.md "b/docs/system-design/authority-certification/SSO\345\215\225\347\202\271\347\231\273\345\275\225\347\234\213\350\277\231\344\270\200\347\257\207\345\260\261\345\244\237\344\272\206.md" similarity index 100% rename from docs/system-design/authority-certification/sso.md rename to "docs/system-design/authority-certification/SSO\345\215\225\347\202\271\347\231\273\345\275\225\347\234\213\350\277\231\344\270\200\347\257\207\345\260\261\345\244\237\344\272\206.md" diff --git a/docs/system-design/authority-certification/basis-of-authority-certification.md b/docs/system-design/authority-certification/basis-of-authority-certification.md index 8441bc0fefa..341bfd17919 100644 --- a/docs/system-design/authority-certification/basis-of-authority-certification.md +++ b/docs/system-design/authority-certification/basis-of-authority-certification.md @@ -6,11 +6,11 @@ **认证 (Authentication):** 你是谁。 - +![](./images/basis-of-authority-certification/authentication.png) **授权 (Authorization):** 你有权限干什么。 - +![](./images/basis-of-authority-certification/authorization.png) 稍微正式点(啰嗦点)的说法就是: @@ -21,7 +21,7 @@ ## 2. 什么是Cookie ? Cookie的作用是什么?如何在服务端使用 Cookie ? -![](../pictures/cookie-sessionId.png) +![](./images/basis-of-authority-certification/cookie-sessionId.png) ### 2.1 什么是Cookie ? Cookie的作用是什么? @@ -90,7 +90,7 @@ public String readAllCookies(HttpServletRequest request) { 很多时候我们都是通过 SessionID 来实现特定的用户,SessionID 一般会选择存放在 Redis 中。举个例子:用户成功登陆系统,然后返回给客户端具有 SessionID 的 Cookie,当用户向后端发起请求的时候会把 SessionID 带上,这样后端就知道你的身份状态了。关于这种认证方式更详细的过程如下: -![Session Based Authentication flow](../pictures/Session-Based-Authentication-flow.png) +![Session Based Authentication flow](./images/basis-of-authority-certification/Session-Based-Authentication-flow.png) 1. 用户向服务器发送用户名和密码用于登陆系统。 2. 服务器验证通过后,服务器为用户创建一个 Session,并将 Session信息存储 起来。 @@ -105,7 +105,7 @@ public String readAllCookies(HttpServletRequest request) { 花了个图简单总结了一下Session认证涉及的一些东西。 - +![](./images/basis-of-authority-certification/session-cookie-intro.jpeg) 另外,Spring Session提供了一种跨多个应用程序或实例管理用户会话信息的机制。如果想详细了解可以查看下面几篇很不错的文章: @@ -117,7 +117,7 @@ public String readAllCookies(HttpServletRequest request) { 这是一道经典的面试题! -一般是通过 Cookie 来保存 SessionID ,假如你使用了 Cookie 保存 SessionID的方案的话, 如果客户端禁用了Cookie,那么Seesion就无法正常工作。 +一般是通过 Cookie 来保存 SessionID ,假如你使用了 Cookie 保存 SessionID的方案的话, 如果客户端禁用了Cookie,那么Session就无法正常工作。 但是,并不是没有 Cookie 之后就不能用 Session 了,比如你可以将SessionID放在请求的 url 里面`https://javaguide.cn/?session_id=xxx` 。这种方案的话可行,但是安全性和用户体验感降低。当然,为了你也可以对 SessionID 进行一次加密之后再传入后端。 @@ -143,9 +143,7 @@ public String readAllCookies(HttpServletRequest request) { XSS中攻击者会用各种方式将恶意代码注入到其他用户的页面中。就可以通过脚本盗用信息比如cookie。 -推荐阅读: - -1. [如何防止CSRF攻击?—美团技术团队](https://tech.meituan.com/2018/10/11/fe-security-csrf.html) +推荐阅读:[如何防止CSRF攻击?—美团技术团队](https://tech.meituan.com/2018/10/11/fe-security-csrf.html) ## 6. 什么是 Token?什么是 JWT?如何基于Token进行身份验证? @@ -167,7 +165,7 @@ JWT 由 3 部分构成: 在基于 Token 进行身份验证的的应用程序中,服务器通过`Payload`、`Header`和一个密钥(`secret`)创建令牌(`Token`)并将 `Token` 发送给客户端,客户端将 `Token` 保存在 Cookie 或者 localStorage 里面,以后客户端发出的所有请求都会携带这个令牌。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP Header 的 Authorization字段中:` Authorization: Bearer Token`。 -![Token Based Authentication flow](../pictures/Token-Based-Authentication.png) +![Token Based Authentication flow](./images/basis-of-authority-certification/Token-Based-Authentication.png) 1. 用户向服务器发送用户名和密码用于登陆系统。 2. 身份验证服务响应并返回了签名的 JWT,上面包含了用户是谁的内容。 @@ -194,7 +192,7 @@ OAuth 2.0 比较常用的场景就是第三方登录,当你的网站接入了 微信支付账户相关参数: - +![](./images/basis-of-authority-certification/微信支付-fnglfdlgdfj.png) **推荐阅读:** diff --git a/docs/system-design/pictures/Session-Based-Authentication-flow.png b/docs/system-design/authority-certification/images/basis-of-authority-certification/Session-Based-Authentication-flow.png similarity index 100% rename from docs/system-design/pictures/Session-Based-Authentication-flow.png rename to docs/system-design/authority-certification/images/basis-of-authority-certification/Session-Based-Authentication-flow.png diff --git a/docs/system-design/pictures/Token-Based-Authentication.png b/docs/system-design/authority-certification/images/basis-of-authority-certification/Token-Based-Authentication.png similarity index 100% rename from docs/system-design/pictures/Token-Based-Authentication.png rename to docs/system-design/authority-certification/images/basis-of-authority-certification/Token-Based-Authentication.png diff --git a/docs/system-design/pictures/authentication.png b/docs/system-design/authority-certification/images/basis-of-authority-certification/authentication.png similarity index 100% rename from docs/system-design/pictures/authentication.png rename to docs/system-design/authority-certification/images/basis-of-authority-certification/authentication.png diff --git a/docs/system-design/pictures/authorization.png b/docs/system-design/authority-certification/images/basis-of-authority-certification/authorization.png similarity index 100% rename from docs/system-design/pictures/authorization.png rename to docs/system-design/authority-certification/images/basis-of-authority-certification/authorization.png diff --git a/docs/system-design/pictures/cookie-sessionId.png b/docs/system-design/authority-certification/images/basis-of-authority-certification/cookie-sessionId.png similarity index 100% rename from docs/system-design/pictures/cookie-sessionId.png rename to docs/system-design/authority-certification/images/basis-of-authority-certification/cookie-sessionId.png diff --git a/docs/system-design/authority-certification/images/basis-of-authority-certification/session-cookie-intro.jpeg b/docs/system-design/authority-certification/images/basis-of-authority-certification/session-cookie-intro.jpeg new file mode 100644 index 00000000000..feca0114904 Binary files /dev/null and b/docs/system-design/authority-certification/images/basis-of-authority-certification/session-cookie-intro.jpeg differ diff --git "a/docs/system-design/pictures/\345\276\256\344\277\241\346\224\257\344\273\230-fnglfdlgdfj.png" "b/docs/system-design/authority-certification/images/basis-of-authority-certification/\345\276\256\344\277\241\346\224\257\344\273\230-fnglfdlgdfj.png" similarity index 100% rename from "docs/system-design/pictures/\345\276\256\344\277\241\346\224\257\344\273\230-fnglfdlgdfj.png" rename to "docs/system-design/authority-certification/images/basis-of-authority-certification/\345\276\256\344\277\241\346\224\257\344\273\230-fnglfdlgdfj.png" diff --git a/docs/system-design/restful-api.md "b/docs/system-design/coding-way/RESTfulAPI\347\256\200\346\230\216\346\225\231\347\250\213.md" similarity index 97% rename from docs/system-design/restful-api.md rename to "docs/system-design/coding-way/RESTfulAPI\347\256\200\346\230\216\346\225\231\347\250\213.md" index af4fbb43740..2747288f38f 100644 --- a/docs/system-design/restful-api.md +++ "b/docs/system-design/coding-way/RESTfulAPI\347\256\200\346\230\216\346\225\231\347\250\213.md" @@ -17,7 +17,7 @@ POST /classes:新建一个班级 ### 一、重要概念 -REST,即 **REpresentational State Transfer** 的缩写。这个词组的翻译过来就是"表现层状态转化"。这样理解起来甚是晦涩,实际上 REST 的全称是 **Resource Representational State Transfe** ,直白地翻译过来就是 **“资源”在网络传输中以某种“表现形式”进行“状态转移”** 。如果还是不能继续理解,请继续往下看,相信下面的讲解一定能让你理解到底啥是 REST 。 +REST,即 **REpresentational State Transfer** 的缩写。这个词组的翻译过来就是"表现层状态转化"。这样理解起来甚是晦涩,实际上 REST 的全称是 **Resource Representational State Transfer** ,直白地翻译过来就是 **“资源”在网络传输中以某种“表现形式”进行“状态转移”** 。如果还是不能继续理解,请继续往下看,相信下面的讲解一定能让你理解到底啥是 REST 。 我们分别对上面涉及到的概念进行解读,以便加深理解,不过实际上你不需要搞懂下面这些概念,也能看懂我下一部分要介绍到的内容。不过,为了更好地能跟别人扯扯 “RESTful API”我建议你还是要好好理解一下! diff --git "a/docs/system-design/data-communication/Kafka\345\205\245\351\227\250\347\234\213\350\277\231\344\270\200\347\257\207\345\260\261\345\244\237\344\272\206.md" "b/docs/system-design/data-communication/Kafka\345\205\245\351\227\250\347\234\213\350\277\231\344\270\200\347\257\207\345\260\261\345\244\237\344\272\206.md" deleted file mode 100644 index 3fd42c5e9dd..00000000000 --- "a/docs/system-design/data-communication/Kafka\345\205\245\351\227\250\347\234\213\350\277\231\344\270\200\347\257\207\345\260\261\345\244\237\344\272\206.md" +++ /dev/null @@ -1,323 +0,0 @@ -> 本文由 JavaGuide 读者推荐,JavaGuide 对文章进行了整理排版!原文地址:https://www.wmyskxz.com/2019/07/17/kafka-ru-men-jiu-zhe-yi-pian/ , 作者:我没有三颗心脏。 - -# 一、Kafka 简介 - ------- - -## Kafka 创建背景 - -**Kafka** 是一个消息系统,原本开发自 LinkedIn,用作 LinkedIn 的活动流(Activity Stream)和运营数据处理管道(Pipeline)的基础。现在它已被[多家不同类型的公司](https://cwiki.apache.org/confluence/display/KAFKA/Powered+By) 作为多种类型的数据管道和消息系统使用。 - -**活动流数据**是几乎所有站点在对其网站使用情况做报表时都要用到的数据中最常规的部分。活动数据包括页面访问量(Page View)、被查看内容方面的信息以及搜索情况等内容。这种数据通常的处理方式是先把各种活动以日志的形式写入某种文件,然后周期性地对这些文件进行统计分析。**运营数据**指的是服务器的性能数据(CPU、IO 使用率、请求时间、服务日志等等数据)。运营数据的统计方法种类繁多。 - -近年来,活动和运营数据处理已经成为了网站软件产品特性中一个至关重要的组成部分,这就需要一套稍微更加复杂的基础设施对其提供支持。 - -## Kafka 简介 - -**Kafka 是一种分布式的,基于发布 / 订阅的消息系统。** - -主要设计目标如下: - -- 以时间复杂度为 O(1) 的方式提供消息持久化能力,即使对 TB 级以上数据也能保证常数时间复杂度的访问性能。 -- 高吞吐率。即使在非常廉价的商用机器上也能做到单机支持每秒 100K 条以上消息的传输。 -- 支持 Kafka Server 间的消息分区,及分布式消费,同时保证每个 Partition 内的消息顺序传输。 -- 同时支持离线数据处理和实时数据处理。 -- Scale out:支持在线水平扩展。 - -## Kafka 基础概念 - -### 概念一:生产者与消费者 - -![生产者与消费者](./../../../media/pictures/kafka/生产者和消费者.png) - -对于 Kafka 来说客户端有两种基本类型: - -1. **生产者(Producer)** -2. **消费者(Consumer)**。 - -除此之外,还有用来做数据集成的 Kafka Connect API 和流式处理的 Kafka Streams 等高阶客户端,但这些高阶客户端底层仍然是生产者和消费者API,它们只不过是在上层做了封装。 - -这很容易理解,生产者(也称为发布者)创建消息,而消费者(也称为订阅者)负责消费or读取消息。 - -### 概念二:主题(Topic)与分区(Partition) - -![主题(Topic)与分区(Partition)](./../../../media/pictures/kafka/主题与分区.png) - -在 Kafka 中,消息以**主题(Topic)**来分类,每一个主题都对应一个 **「消息队列」**,这有点儿类似于数据库中的表。但是如果我们把所有同类的消息都塞入到一个“中心”队列中,势必缺少可伸缩性,无论是生产者/消费者数目的增加,还是消息数量的增加,都可能耗尽系统的性能或存储。 - -我们使用一个生活中的例子来说明:现在 A 城市生产的某商品需要运输到 B 城市,走的是公路,那么单通道的高速公路不论是在「A 城市商品增多」还是「现在 C 城市也要往 B 城市运输东西」这样的情况下都会出现「吞吐量不足」的问题。所以我们现在引入**分区(Partition)**的概念,类似“允许多修几条道”的方式对我们的主题完成了水平扩展。 - -### 概念三:Broker 和集群(Cluster) - -一个 Kafka 服务器也称为 Broker,它接受生产者发送的消息并存入磁盘;Broker 同时服务消费者拉取分区消息的请求,返回目前已经提交的消息。使用特定的机器硬件,一个 Broker 每秒可以处理成千上万的分区和百万量级的消息。(现在动不动就百万量级..我特地去查了一把,好像确实集群的情况下吞吐量挺高的..嗯..) - -若干个 Broker 组成一个集群(Cluster),其中集群内某个 Broker 会成为集群控制器(Cluster Controller),它负责管理集群,包括分配分区到 Broker、监控 Broker 故障等。在集群内,一个分区由一个 Broker 负责,这个 Broker 也称为这个分区的 Leader;当然一个分区可以被复制到多个 Broker 上来实现冗余,这样当存在 Broker 故障时可以将其分区重新分配到其他 Broker 来负责。下图是一个样例: - -![Broker和集群](./../../../media/pictures/kafka/Broker和集群.png) - -Kafka 的一个关键性质是日志保留(retention),我们可以配置主题的消息保留策略,譬如只保留一段时间的日志或者只保留特定大小的日志。当超过这些限制时,老的消息会被删除。我们也可以针对某个主题单独设置消息过期策略,这样对于不同应用可以实现个性化。 - -### 概念四:多集群 - -随着业务发展,我们往往需要多集群,通常处于下面几个原因: - -- 基于数据的隔离; -- 基于安全的隔离; -- 多数据中心(容灾) - -当构建多个数据中心时,往往需要实现消息互通。举个例子,假如用户修改了个人资料,那么后续的请求无论被哪个数据中心处理,这个更新需要反映出来。又或者,多个数据中心的数据需要汇总到一个总控中心来做数据分析。 - -上面说的分区复制冗余机制只适用于同一个 Kafka 集群内部,对于多个 Kafka 集群消息同步可以使用 Kafka 提供的 MirrorMaker 工具。本质上来说,MirrorMaker 只是一个 Kafka 消费者和生产者,并使用一个队列连接起来而已。它从一个集群中消费消息,然后往另一个集群生产消息。 - - -# 二、Kafka 的设计与实现 - ------- - -上面我们知道了 Kafka 中的一些基本概念,但作为一个成熟的「消息队列」中间件,其中有许多有意思的设计值得我们思考,下面我们简单列举一些。 - -## 讨论一:Kafka 存储在文件系统上 - -是的,**您首先应该知道 Kafka 的消息是存在于文件系统之上的**。Kafka 高度依赖文件系统来存储和缓存消息,一般的人认为 “磁盘是缓慢的”,所以对这样的设计持有怀疑态度。实际上,磁盘比人们预想的快很多也慢很多,这取决于它们如何被使用;一个好的磁盘结构设计可以使之跟网络速度一样快。 - -现代的操作系统针对磁盘的读写已经做了一些优化方案来加快磁盘的访问速度。比如,**预读**会提前将一个比较大的磁盘快读入内存。**后写**会将很多小的逻辑写操作合并起来组合成一个大的物理写操作。并且,操作系统还会将主内存剩余的所有空闲内存空间都用作**磁盘缓存**,所有的磁盘读写操作都会经过统一的磁盘缓存(除了直接 I/O 会绕过磁盘缓存)。综合这几点优化特点,**如果是针对磁盘的顺序访问,某些情况下它可能比随机的内存访问都要快,甚至可以和网络的速度相差无几。** - -**上述的 Topic 其实是逻辑上的概念,面相消费者和生产者,物理上存储的其实是 Partition**,每一个 Partition 最终对应一个目录,里面存储所有的消息和索引文件。默认情况下,每一个 Topic 在创建时如果不指定 Partition 数量时只会创建 1 个 Partition。比如,我创建了一个 Topic 名字为 test ,没有指定 Partition 的数量,那么会默认创建一个 test-0 的文件夹,这里的命名规则是:`-`。 - -![主题(Topic)与分区(Partition)](./../../../media/pictures/kafka/kafka存在文件系统上.png) - -任何发布到 Partition 的消息都会被追加到 Partition 数据文件的尾部,这样的顺序写磁盘操作让 Kafka 的效率非常高(经验证,顺序写磁盘效率比随机写内存还要高,这是 Kafka 高吞吐率的一个很重要的保证)。 - -每一条消息被发送到 Broker 中,会根据 Partition 规则选择被存储到哪一个 Partition。如果 Partition 规则设置的合理,所有消息可以均匀分布到不同的 Partition中。 - -## 讨论二:Kafka 中的底层存储设计 - -假设我们现在 Kafka 集群只有一个 Broker,我们创建 2 个 Topic 名称分别为:「topic1」和「topic2」,Partition 数量分别为 1、2,那么我们的根目录下就会创建如下三个文件夹: - -```shell - | --topic1-0 - | --topic2-0 - | --topic2-1 -``` - -在 Kafka 的文件存储中,同一个 Topic 下有多个不同的 Partition,每个 Partition 都为一个目录,而每一个目录又被平均分配成多个大小相等的 **Segment File** 中,Segment File 又由 index file 和 data file 组成,他们总是成对出现,后缀 “.index” 和 “.log” 分表表示 Segment 索引文件和数据文件。 - -现在假设我们设置每个 Segment 大小为 500 MB,并启动生产者向 topic1 中写入大量数据,topic1-0 文件夹中就会产生类似如下的一些文件: - -```shell - | --topic1-0 - | --00000000000000000000.index - | --00000000000000000000.log - | --00000000000000368769.index - | --00000000000000368769.log - | --00000000000000737337.index - | --00000000000000737337.log - | --00000000000001105814.index - | --00000000000001105814.log - | --topic2-0 - | --topic2-1 - -``` - -**Segment 是 Kafka 文件存储的最小单位。**Segment 文件命名规则:Partition 全局的第一个 Segment 从 0 开始,后续每个 Segment 文件名为上一个 Segment 文件最后一条消息的 offset 值。数值最大为 64 位 long 大小,19 位数字字符长度,没有数字用0填充。如 00000000000000368769.index 和 00000000000000368769.log。 - -以上面的一对 Segment File 为例,说明一下索引文件和数据文件对应关系: - -![索引文件和数据文件](./../../../media/pictures/kafka/segment是kafka文件存储的最小单位.png) - - - -其中以索引文件中元数据 `<3, 497>` 为例,依次在数据文件中表示第 3 个 message(在全局 Partition 表示第 368769 + 3 = 368772 个 message)以及该消息的物理偏移地址为 497。 - -注意该 index 文件并不是从0开始,也不是每次递增1的,这是因为 Kafka 采取稀疏索引存储的方式,每隔一定字节的数据建立一条索引,它减少了索引文件大小,使得能够把 index 映射到内存,降低了查询时的磁盘 IO 开销,同时也并没有给查询带来太多的时间消耗。 - -因为其文件名为上一个 Segment 最后一条消息的 offset ,所以当需要查找一个指定 offset 的 message 时,通过在所有 segment 的文件名中进行二分查找就能找到它归属的 segment ,再在其 index 文件中找到其对应到文件上的物理位置,就能拿出该 message 。 - -由于消息在 Partition 的 Segment 数据文件中是顺序读写的,且消息消费后不会删除(删除策略是针对过期的 Segment 文件),这种顺序磁盘 IO 存储设计师 Kafka 高性能很重要的原因。 - -> Kafka 是如何准确的知道 message 的偏移的呢?这是因为在 Kafka 定义了标准的数据存储结构,在 Partition 中的每一条 message 都包含了以下三个属性: -> -> - offset:表示 message 在当前 Partition 中的偏移量,是一个逻辑上的值,唯一确定了 Partition 中的一条 message,可以简单的认为是一个 id; -> - MessageSize:表示 message 内容 data 的大小; -> - data:message 的具体内容 - -## 讨论三:生产者设计概要 - -当我们发送消息之前,先问几个问题:每条消息都是很关键且不能容忍丢失么?偶尔重复消息可以么?我们关注的是消息延迟还是写入消息的吞吐量? - -举个例子,有一个信用卡交易处理系统,当交易发生时会发送一条消息到 Kafka,另一个服务来读取消息并根据规则引擎来检查交易是否通过,将结果通过 Kafka 返回。对于这样的业务,消息既不能丢失也不能重复,由于交易量大因此吞吐量需要尽可能大,延迟可以稍微高一点。 - -再举个例子,假如我们需要收集用户在网页上的点击数据,对于这样的场景,少量消息丢失或者重复是可以容忍的,延迟多大都不重要只要不影响用户体验,吞吐则根据实时用户数来决定。 - -不同的业务需要使用不同的写入方式和配置。具体的方式我们在这里不做讨论,现在先看下生产者写消息的基本流程: - -![生产者设计概要](./../../../media/pictures/kafka/生产者设计概要.png) - -图片来源:[http://www.dengshenyu.com/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/2017/11/12/kafka-producer.html](http://www.dengshenyu.com/分布式系统/2017/11/12/kafka-producer.html) - -流程如下: - -1. 首先,我们需要创建一个ProducerRecord,这个对象需要包含消息的主题(topic)和值(value),可以选择性指定一个键值(key)或者分区(partition)。 -2. 发送消息时,生产者会对键值和值序列化成字节数组,然后发送到分配器(partitioner)。 -3. 如果我们指定了分区,那么分配器返回该分区即可;否则,分配器将会基于键值来选择一个分区并返回。 -4. 选择完分区后,生产者知道了消息所属的主题和分区,它将这条记录添加到相同主题和分区的批量消息中,另一个线程负责发送这些批量消息到对应的Kafka broker。 -5. 当broker接收到消息后,如果成功写入则返回一个包含消息的主题、分区及位移的RecordMetadata对象,否则返回异常。 -6. 生产者接收到结果后,对于异常可能会进行重试。 - - - -## 讨论四:消费者设计概要 - -### 消费者与消费组 - -假设这么个场景:我们从Kafka中读取消息,并且进行检查,最后产生结果数据。我们可以创建一个消费者实例去做这件事情,但如果生产者写入消息的速度比消费者读取的速度快怎么办呢?这样随着时间增长,消息堆积越来越严重。对于这种场景,我们需要增加多个消费者来进行水平扩展。 - -Kafka消费者是**消费组**的一部分,当多个消费者形成一个消费组来消费主题时,每个消费者会收到不同分区的消息。假设有一个T1主题,该主题有4个分区;同时我们有一个消费组G1,这个消费组只有一个消费者C1。那么消费者C1将会收到这4个分区的消息,如下所示: - -![生产者设计概要](./../../../media/pictures/kafka/消费者设计概要1.png) - -如果我们增加新的消费者C2到消费组G1,那么每个消费者将会分别收到两个分区的消息,如下所示: - -![生产者设计概要](./../../../media/pictures/kafka/消费者设计概要2.png) - -如果增加到4个消费者,那么每个消费者将会分别收到一个分区的消息,如下所示: - -![生产者设计概要](./../../../media/pictures/kafka/消费者设计概要3.png) - -但如果我们继续增加消费者到这个消费组,剩余的消费者将会空闲,不会收到任何消息: - -![生产者设计概要](./../../../media/pictures/kafka/消费者设计概要4.png) - -总而言之,我们可以通过增加消费组的消费者来进行水平扩展提升消费能力。这也是为什么建议创建主题时使用比较多的分区数,这样可以在消费负载高的情况下增加消费者来提升性能。另外,消费者的数量不应该比分区数多,因为多出来的消费者是空闲的,没有任何帮助。 - -**Kafka一个很重要的特性就是,只需写入一次消息,可以支持任意多的应用读取这个消息。**换句话说,每个应用都可以读到全量的消息。为了使得每个应用都能读到全量消息,应用需要有不同的消费组。对于上面的例子,假如我们新增了一个新的消费组G2,而这个消费组有两个消费者,那么会是这样的: - -![生产者设计概要](./../../../media/pictures/kafka/消费者设计概要5.png) - -在这个场景中,消费组G1和消费组G2都能收到T1主题的全量消息,在逻辑意义上来说它们属于不同的应用。 - -最后,总结起来就是:如果应用需要读取全量消息,那么请为该应用设置一个消费组;如果该应用消费能力不足,那么可以考虑在这个消费组里增加消费者。 - -### 消费组与分区重平衡 - -可以看到,当新的消费者加入消费组,它会消费一个或多个分区,而这些分区之前是由其他消费者负责的;另外,当消费者离开消费组(比如重启、宕机等)时,它所消费的分区会分配给其他分区。这种现象称为**重平衡(rebalance)**。重平衡是 Kafka 一个很重要的性质,这个性质保证了高可用和水平扩展。**不过也需要注意到,在重平衡期间,所有消费者都不能消费消息,因此会造成整个消费组短暂的不可用。**而且,将分区进行重平衡也会导致原来的消费者状态过期,从而导致消费者需要重新更新状态,这段期间也会降低消费性能。后面我们会讨论如何安全的进行重平衡以及如何尽可能避免。 - -消费者通过定期发送心跳(heartbeat)到一个作为组协调者(group coordinator)的 broker 来保持在消费组内存活。这个 broker 不是固定的,每个消费组都可能不同。当消费者拉取消息或者提交时,便会发送心跳。 - -如果消费者超过一定时间没有发送心跳,那么它的会话(session)就会过期,组协调者会认为该消费者已经宕机,然后触发重平衡。可以看到,从消费者宕机到会话过期是有一定时间的,这段时间内该消费者的分区都不能进行消息消费;通常情况下,我们可以进行优雅关闭,这样消费者会发送离开的消息到组协调者,这样组协调者可以立即进行重平衡而不需要等待会话过期。 - -在 0.10.1 版本,Kafka 对心跳机制进行了修改,将发送心跳与拉取消息进行分离,这样使得发送心跳的频率不受拉取的频率影响。另外更高版本的 Kafka 支持配置一个消费者多长时间不拉取消息但仍然保持存活,这个配置可以避免活锁(livelock)。活锁,是指应用没有故障但是由于某些原因不能进一步消费。 - -### Partition 与消费模型 - -上面提到,Kafka 中一个 topic 中的消息是被打散分配在多个 Partition(分区) 中存储的, Consumer Group 在消费时需要从不同的 Partition 获取消息,那最终如何重建出 Topic 中消息的顺序呢? - -答案是:没有办法。Kafka 只会保证在 Partition 内消息是有序的,而不管全局的情况。 - -下一个问题是:Partition 中的消息可以被(不同的 Consumer Group)多次消费,那 Partition中被消费的消息是何时删除的? Partition 又是如何知道一个 Consumer Group 当前消费的位置呢? - -无论消息是否被消费,除非消息到期 Partition 从不删除消息。例如设置保留时间为 2 天,则消息发布 2 天内任何 Group 都可以消费,2 天后,消息自动被删除。 -Partition 会为每个 Consumer Group 保存一个偏移量,记录 Group 消费到的位置。 如下图: -![生产者设计概要](./../../../media/pictures/kafka/Partition与消费模型.png) - - - - -### 为什么 Kafka 是 pull 模型 - -消费者应该向 Broker 要数据(pull)还是 Broker 向消费者推送数据(push)?作为一个消息系统,Kafka 遵循了传统的方式,选择由 Producer 向 broker push 消息并由 Consumer 从 broker pull 消息。一些 logging-centric system,比如 Facebook 的[Scribe](https://github.com/facebookarchive/scribe)和 Cloudera 的[Flume](https://flume.apache.org/),采用 push 模式。事实上,push 模式和 pull 模式各有优劣。 - -**push 模式很难适应消费速率不同的消费者,因为消息发送速率是由 broker 决定的。**push 模式的目标是尽可能以最快速度传递消息,但是这样很容易造成 Consumer 来不及处理消息,典型的表现就是拒绝服务以及网络拥塞。**而 pull 模式则可以根据 Consumer 的消费能力以适当的速率消费消息。** - -**对于 Kafka 而言,pull 模式更合适。**pull 模式可简化 broker 的设计,Consumer 可自主控制消费消息的速率,同时 Consumer 可以自己控制消费方式——即可批量消费也可逐条消费,同时还能选择不同的提交方式从而实现不同的传输语义。 - -## 讨论五:Kafka 如何保证可靠性 - -当我们讨论**可靠性**的时候,我们总会提到*保证**这个词语。可靠性保证是基础,我们基于这些基础之上构建我们的应用。比如关系型数据库的可靠性保证是ACID,也就是原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。 - -Kafka 中的可靠性保证有如下四点: - -- 对于一个分区来说,它的消息是有序的。如果一个生产者向一个分区先写入消息A,然后写入消息B,那么消费者会先读取消息A再读取消息B。 -- 当消息写入所有in-sync状态的副本后,消息才会认为**已提交(committed)**。这里的写入有可能只是写入到文件系统的缓存,不一定刷新到磁盘。生产者可以等待不同时机的确认,比如等待分区主副本写入即返回,后者等待所有in-sync状态副本写入才返回。 -- 一旦消息已提交,那么只要有一个副本存活,数据不会丢失。 -- 消费者只能读取到已提交的消息。 - -使用这些基础保证,我们构建一个可靠的系统,这时候需要考虑一个问题:究竟我们的应用需要多大程度的可靠性?可靠性不是无偿的,它与系统可用性、吞吐量、延迟和硬件价格息息相关,得此失彼。因此,我们往往需要做权衡,一味的追求可靠性并不实际。 - -> 想了解更多戳这里:http://www.dengshenyu.com/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/2017/11/21/kafka-data-delivery.html - -# 三、动手搭一个 Kafka - -通过上面的描述,我们已经大致了解到了「Kafka」是何方神圣了,现在我们开始尝试自己动手本地搭一个来实际体验一把。 - -## 第一步:下载 Kafka - -这里以 Mac OS 为例,在安装了 Homebrew 的情况下执行下列代码: - -```shell -brew install kafka -``` - -由于 Kafka 依赖了 Zookeeper,所以在下载的时候会自动下载。 - -## 第二步:启动服务 - -我们在启动之前首先需要修改 Kafka 的监听地址和端口为 `localhost:9092`: - -```shell -vi /usr/local/etc/kafka/server.properties -``` - - -然后修改成下图的样子: - -![启动服务](./../../../media/pictures/kafka/启动服务.png) -依次启动 Zookeeper 和 Kafka: - -```shell -brew services start zookeeper -brew services start kafka -``` - -然后执行下列语句来创建一个名字为 “test” 的 Topic: - -```shell -kafka-topics --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test -``` - -我们可以通过下列的命令查看我们的 Topic 列表: - -```shell -kafka-topics --list --zookeeper localhost:2181 -``` - -## 第三步:发送消息 - -然后我们新建一个控制台,运行下列命令创建一个消费者关注刚才创建的 Topic: - -```shell -kafka-console-consumer --bootstrap-server localhost:9092 --topic test --from-beginning -``` - -用控制台往刚才创建的 Topic 中添加消息,并观察刚才创建的消费者窗口: - -```shel -kafka-console-producer --broker-list localhost:9092 --topic test -``` - -能通过消费者窗口观察到正确的消息: - -![发送消息](./../../../media/pictures/kafka/发送消息.png) - -# 参考资料 - ------- - -1. https://www.infoq.cn/article/kafka-analysis-part-1 - Kafka 设计解析(一):Kafka 背景及架构介绍 -2. [http://www.dengshenyu.com/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/2017/11/06/kafka-Meet-Kafka.html](http://www.dengshenyu.com/分布式系统/2017/11/06/kafka-Meet-Kafka.html) - Kafka系列(一)初识Kafka -3. https://lotabout.me/2018/kafka-introduction/ - Kafka 入门介绍 -4. https://www.zhihu.com/question/28925721 - Kafka 中的 Topic 为什么要进行分区? - 知乎 -5. https://blog.joway.io/posts/kafka-design-practice/ - Kafka 的设计与实践思考 -6. [http://www.dengshenyu.com/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/2017/11/21/kafka-data-delivery.html](http://www.dengshenyu.com/分布式系统/2017/11/21/kafka-data-delivery.html) - Kafka系列(六)可靠的数据传输 - - diff --git a/docs/system-design/data-communication/message-queue.md b/docs/system-design/data-communication/message-queue.md deleted file mode 100644 index 3f99c07c1f8..00000000000 --- a/docs/system-design/data-communication/message-queue.md +++ /dev/null @@ -1,157 +0,0 @@ - - -- [消息队列其实很简单](#消息队列其实很简单) - - [一 什么是消息队列](#一-什么是消息队列) - - [二 为什么要用消息队列](#二-为什么要用消息队列) - - [\(1\) 通过异步处理提高系统性能(削峰、减少响应所需时间)](#1-通过异步处理提高系统性能削峰减少响应所需时间) - - [\(2\) 降低系统耦合性](#2-降低系统耦合性) - - [三 使用消息队列带来的一些问题](#三-使用消息队列带来的一些问题) - - [四 JMS VS AMQP](#四-jms-vs-amqp) - - [4.1 JMS](#41-jms) - - [4.1.1 JMS 简介](#411-jms-简介) - - [4.1.2 JMS两种消息模型](#412-jms两种消息模型) - - [4.1.3 JMS 五种不同的消息正文格式](#413-jms-五种不同的消息正文格式) - - [4.2 AMQP](#42-amqp) - - [4.3 JMS vs AMQP](#43-jms-vs-amqp) - - [五 常见的消息队列对比](#五-常见的消息队列对比) - - - - -# 消息队列其实很简单 - -  “RabbitMQ?”“Kafka?”“RocketMQ?”...在日常学习与开发过程中,我们常常听到消息队列这个关键词。我也在我的多篇文章中提到了这个概念。可能你是熟练使用消息队列的老手,又或者你是不懂消息队列的新手,不论你了不了解消息队列,本文都将带你搞懂消息队列的一些基本理论。如果你是老手,你可能从本文学到你之前不曾注意的一些关于消息队列的重要概念,如果你是新手,相信本文将是你打开消息队列大门的一板砖。 - -## 一 什么是消息队列 - -  我们可以把消息队列比作是一个存放消息的容器,当我们需要使用消息的时候可以取出消息供自己使用。消息队列是分布式系统中重要的组件,使用消息队列主要是为了通过异步处理提高系统性能和削峰、降低系统耦合性。目前使用较多的消息队列有ActiveMQ,RabbitMQ,Kafka,RocketMQ,我们后面会一一对比这些消息队列。 - -  另外,我们知道队列 Queue 是一种先进先出的数据结构,所以消费消息时也是按照顺序来消费的。比如生产者发送消息1,2,3...对于消费者就会按照1,2,3...的顺序来消费。但是偶尔也会出现消息被消费的顺序不对的情况,比如某个消息消费失败又或者一个 queue 多个consumer 也会导致消息被消费的顺序不对,我们一定要保证消息被消费的顺序正确。 - -  除了上面说的消息消费顺序的问题,使用消息队列,我们还要考虑如何保证消息不被重复消费?如何保证消息的可靠性传输(如何处理消息丢失的问题)?......等等问题。所以说使用消息队列也不是十全十美的,使用它也会让系统可用性降低、复杂度提高,另外需要我们保障一致性等问题。 - -## 二 为什么要用消息队列 - -  我觉得使用消息队列主要有两点好处:1.通过异步处理提高系统性能(削峰、减少响应所需时间);2.降低系统耦合性。如果在面试的时候你被面试官问到这个问题的话,一般情况是你在你的简历上涉及到消息队列这方面的内容,这个时候推荐你结合你自己的项目来回答。 - - -  《大型网站技术架构》第四章和第七章均有提到消息队列对应用性能及扩展性的提升。 - -### (1) 通过异步处理提高系统性能(削峰、减少响应所需时间) - -![通过异步处理提高系统性能](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/Asynchronous-message-queue.png) -  如上图,**在不使用消息队列服务器的时候,用户的请求数据直接写入数据库,在高并发的情况下数据库压力剧增,使得响应速度变慢。但是在使用消息队列之后,用户的请求数据发送给消息队列之后立即 返回,再由消息队列的消费者进程从消息队列中获取数据,异步写入数据库。由于消息队列服务器处理速度快于数据库(消息队列也比数据库有更好的伸缩性),因此响应速度得到大幅改善。** - -  通过以上分析我们可以得出**消息队列具有很好的削峰作用的功能**——即**通过异步处理,将短时间高并发产生的事务消息存储在消息队列中,从而削平高峰期的并发事务。** 举例:在电子商务一些秒杀、促销活动中,合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击。如下图所示: - -![削峰](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/削峰-消息队列.png) - -  因为**用户请求数据写入消息队列之后就立即返回给用户了,但是请求数据在后续的业务校验、写数据库等操作中可能失败**。因此使用消息队列进行异步处理之后,需要**适当修改业务流程进行配合**,比如**用户在提交订单之后,订单数据写入消息队列,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进程真正处理完该订单之后,甚至出库后,再通过电子邮件或短信通知用户订单成功**,以免交易纠纷。这就类似我们平时手机订火车票和电影票。 - -### (2) 降低系统耦合性 - -   使用消息队列还可以降低系统耦合性。我们知道如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性无疑更好一些。还是直接上图吧: - -![解耦](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/消息队列-解耦.png) - -  生产者(客户端)发送消息到消息队列中去,接受者(服务端)处理消息,需要消费的系统直接去消息队列取消息进行消费即可而不需要和其他系统有耦合, 这显然也提高了系统的扩展性。 - -  **消息队列使利用发布-订阅模式工作,消息发送者(生产者)发布消息,一个或多个消息接受者(消费者)订阅消息。** 从上图可以看到**消息发送者(生产者)和消息接受者(消费者)之间没有直接耦合**,消息发送者将消息发送至分布式消息队列即结束对消息的处理,消息接受者从分布式消息队列获取该消息后进行后续处理,并不需要知道该消息从何而来。**对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,从而实现网站业务的可扩展性设计**。 - -  消息接受者对消息进行过滤、处理、包装后,构造成一个新的消息类型,将消息继续发送出去,等待其他消息接受者订阅该消息。因此基于事件(消息对象)驱动的业务架构可以是一系列流程。 - -  **另外为了避免消息队列服务器宕机造成消息丢失,会将成功发送到消息队列的消息存储在消息生产者服务器上,等消息真正被消费者服务器处理后才删除消息。在消息队列服务器宕机后,生产者服务器会选择分布式消息队列服务器集群中的其他服务器发布消息。** - -**备注:** 不要认为消息队列只能利用发布-订阅模式工作,只不过在解耦这个特定业务环境下是使用发布-订阅模式的。**除了发布-订阅模式,还有点对点订阅模式(一个消息只有一个消费者),我们比较常用的是发布-订阅模式。** 另外,这两种消息模型是 JMS 提供的,AMQP 协议还提供了 5 种消息模型。 - -## 三 使用消息队列带来的一些问题 - -- **系统可用性降低:** 系统可用性在某种程度上降低,为什么这样说呢?在加入MQ之前,你不用考虑消息丢失或者说MQ挂掉等等的情况,但是,引入MQ之后你就需要去考虑了! -- **系统复杂性提高:** 加入MQ之后,你需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺序性等等问题! -- **一致性问题:** 我上面讲了消息队列可以实现异步,消息队列带来的异步确实可以提高系统响应速度。但是,万一消息的真正消费者并没有正确消费消息怎么办?这样就会导致数据不一致的情况了! - -## 四 JMS VS AMQP - -### 4.1 JMS - -#### 4.1.1 JMS 简介 - -  JMS(JAVA Message Service,java消息服务)是java的消息服务,JMS的客户端之间可以通过JMS服务进行异步的消息传输。**JMS(JAVA Message Service,Java消息服务)API是一个消息服务的标准或者说是规范**,允许应用程序组件基于JavaEE平台创建、发送、接收和读取消息。它使分布式通信耦合度更低,消息服务更加可靠以及异步性。 - -**ActiveMQ 就是基于 JMS 规范实现的。** - -#### 4.1.2 JMS两种消息模型 - -①点到点(P2P)模型 - -![点到点(P2P)模型](https://user-gold-cdn.xitu.io/2018/4/21/162e7185572ca37d?w=575&h=135&f=gif&s=8530) -   - -使用**队列(Queue)**作为消息通信载体;满足**生产者与消费者模式**,一条消息只能被一个消费者使用,未被消费的消息在队列中保留直到被消费或超时。比如:我们生产者发送100条消息的话,两个消费者来消费一般情况下两个消费者会按照消息发送的顺序各自消费一半(也就是你一个我一个的消费。) - -② 发布/订阅(Pub/Sub)模型 - - ![发布/订阅(Pub/Sub)模型](https://user-gold-cdn.xitu.io/2018/4/21/162e7187c268eaa5?w=402&h=164&f=gif&s=15492) -   - -发布订阅模型(Pub/Sub) 使用**主题(Topic)**作为消息通信载体,类似于**广播模式**;发布者发布一条消息,该消息通过主题传递给所有的订阅者,**在一条消息广播之后才订阅的用户则是收不到该条消息的**。 - -#### 4.1.3 JMS 五种不同的消息正文格式 - -  JMS定义了五种不同的消息正文格式,以及调用的消息类型,允许你发送并接收以一些不同形式的数据,提供现有消息格式的一些级别的兼容性。 - -- StreamMessage -- Java原始值的数据流 -- MapMessage--一套名称-值对 -- TextMessage--一个字符串对象 -- ObjectMessage--一个序列化的 Java对象 -- BytesMessage--一个字节的数据流 - - -### 4.2 AMQP - -  ​ AMQP,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准 **高级消息队列协议**(二进制应用层协议),是应用层协议的一个开放标准,为面向消息的中间件设计,兼容 JMS。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件同产品,不同的开发语言等条件的限制。 - -**RabbitMQ 就是基于 AMQP 协议实现的。** - - - -### 4.3 JMS vs AMQP - - -|对比方向| JMS | AMQP | -| :-------- | --------:| :--: | -| 定义| Java API | 协议 | -| 跨语言 | 否 | 是 | -| 跨平台 | 否 | 是 | -| 支持消息类型 | 提供两种消息模型:①Peer-2-Peer;②Pub/sub| 提供了五种消息模型:①direct exchange;②fanout exchange;③topic change;④headers exchange;⑤system exchange。本质来讲,后四种和JMS的pub/sub模型没有太大差别,仅是在路由机制上做了更详细的划分;| -|支持消息类型| 支持多种消息类型 ,我们在上面提到过| byte[](二进制)| - -**总结:** - -- AMQP 为消息定义了线路层(wire-level protocol)的协议,而JMS所定义的是API规范。在 Java 体系中,多个client均可以通过JMS进行交互,不需要应用修改代码,但是其对跨平台的支持较差。而AMQP天然具有跨平台、跨语言特性。 -- JMS 支持TextMessage、MapMessage 等复杂的消息类型;而 AMQP 仅支持 byte[] 消息类型(复杂的类型可序列化后发送)。 -- 由于Exchange 提供的路由算法,AMQP可以提供多样化的路由方式来传递消息到消息队列,而 JMS 仅支持 队列 和 主题/订阅 方式两种。 - - -## 五 常见的消息队列对比 - - - -对比方向 |概要 --------- | --- - 吞吐量| 万级的 ActiveMQ 和 RabbitMQ 的吞吐量(ActiveMQ 的性能最差)要比 十万级甚至是百万级的 RocketMQ 和 Kafka 低一个数量级。 -可用性| 都可以实现高可用。ActiveMQ 和 RabbitMQ 都是基于主从架构实现高可用性。RocketMQ 基于分布式架构。 kafka 也是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 -时效性| RabbitMQ 基于erlang开发,所以并发能力很强,性能极其好,延时很低,达到微秒级。其他三个都是 ms 级。 -功能支持| 除了 Kafka,其他三个功能都较为完备。 Kafka 功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算以及日志采集被大规模使用,是事实上的标准 -消息丢失| ActiveMQ 和 RabbitMQ 丢失的可能性非常低, RocketMQ 和 Kafka 理论上不会丢失。 - - -**总结:** - -- ActiveMQ 的社区算是比较成熟,但是较目前来说,ActiveMQ 的性能比较差,而且版本迭代很慢,不推荐使用。 -- RabbitMQ 在吞吐量方面虽然稍逊于 Kafka 和 RocketMQ ,但是由于它基于 erlang 开发,所以并发能力很强,性能极其好,延时很低,达到微秒级。但是也因为 RabbitMQ 基于 erlang 开发,所以国内很少有公司有实力做erlang源码级别的研究和定制。如果业务场景对并发量要求不是太高(十万级、百万级),那这四种消息队列中,RabbitMQ 一定是你的首选。如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。 -- RocketMQ 阿里出品,Java 系开源项目,源代码我们可以直接阅读,然后可以定制自己公司的MQ,并且 RocketMQ 有阿里巴巴的实际业务场景的实战考验。RocketMQ 社区活跃度相对较为一般,不过也还可以,文档相对来说简单一些,然后接口这块不是按照标准 JMS 规范走的有些系统要迁移需要修改大量代码。还有就是阿里出台的技术,你得做好这个技术万一被抛弃,社区黄掉的风险,那如果你们公司有技术实力我觉得用RocketMQ 挺好的 -- kafka 的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms 级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展。同时 kafka 最好是支撑较少的 topic 数量即可,保证其超高吞吐量。kafka 唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略这个特性天然适合大数据实时计算以及日志收集。 - - -参考:《Java工程师面试突击第1季-中华石杉老师》 diff --git "a/docs/system-design/distributed-system/BASE\347\220\206\350\256\272.md" "b/docs/system-design/distributed-system/BASE\347\220\206\350\256\272.md" new file mode 100644 index 00000000000..61bc81c717c --- /dev/null +++ "b/docs/system-design/distributed-system/BASE\347\220\206\350\256\272.md" @@ -0,0 +1,58 @@ +## BASE 理论 + +[BASE 理论](https://dl.acm.org/doi/10.1145/1394127.1394128)起源于 2008 年, 由eBay的架构师Dan Pritchett在ACM上发表。 + +### 简介 + +**BASE** 是 **Basically Available(基本可用)** 、**Soft-state(软状态)** 和 **Eventually Consistent(最终一致性)** 三个短语的缩写。BASE 理论是对 CAP 中一致性 C 和可用性 A 权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于 CAP 定理逐步演化而来的,它大大降低了我们对系统的要求。 + +### BASE 理论的核心思想 + +即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。 + +> 也就是牺牲数据的一致性来满足系统的高可用性,系统中一部分数据不可用或者不一致时,仍需要保持系统整体“主要可用”。 + +**BASE 理论本质上是对 CAP 的延伸和补充,更具体地说,是对 CAP 中 AP 方案的一个补充。** + +**为什么这样说呢?** + +CAP 理论这节我们也说过了: + +> 如果系统没有发生“分区”的话,节点间的网络连接通信正常的话,也就不存在 P 了。这个时候,我们就可以同时保证 C 和 A 了。因此,**如果系统发生“分区”,我们要考虑选择 CP 还是 AP。如果系统没有发生“分区”的话,我们要思考如何保证 CA 。** + +因此,AP 方案只是在系统发生分区的时候放弃一致性,而不是永远放弃一致性。在分区故障恢复后,系统应该达到最终一致性。这一点其实就是 BASE 理论延伸的地方。 + +### BASE 理论三要素 + +![BASE理论三要素](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOC81LzI0LzE2MzkxNDgwNmQ5ZTE1YzY?x-oss-process=image/format,png) + +#### 1. 基本可用 + +基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性。但是,这绝不等价于系统不可用。 + +**什么叫允许损失部分可用性呢?** + +- **响应时间上的损失**: 正常情况下,处理用户请求需要 0.5s 返回结果,但是由于系统出现故障,处理用户请求的时间变为 3 s。 +- **系统功能上的损失**:正常情况下,用户可以使用系统的全部功能,但是由于系统访问量突然剧增,系统的部分非核心功能无法使用。 + +#### 2. 软状态 + +软状态指允许系统中的数据存在中间状态(**CAP 理论中的数据不一致**),并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。 + +#### 3. 最终一致性 + +最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。 + +> 分布式一致性的 3 种级别: +> +> 1. **强一致性** :系统写入了什么,读出来的就是什么。 +> +> 2. **弱一致性** :不一定可以读取到最新写入的值,也不保证多少时间之后读取到的数据是最新的,只是会尽量保证某个时刻达到数据一致的状态。 +> +> 3. **最终一致性** :弱一致性的升级版。,系统会保证在一定时间内达到数据一致的状态, +> +> **业界比较推崇是最终一致性级别,但是某些对数据一致要求十分严格的场景比如银行转账还是要保证强一致性。** + +### 总结 + +**ACID 是数据库事务完整性的理论,CAP 是分布式系统设计理论,BASE 是 CAP 理论中 AP 方案的延伸。** \ No newline at end of file diff --git "a/docs/system-design/distributed-system/CAP\347\220\206\350\256\272.md" "b/docs/system-design/distributed-system/CAP\347\220\206\350\256\272.md" new file mode 100644 index 00000000000..045581b916a --- /dev/null +++ "b/docs/system-design/distributed-system/CAP\347\220\206\350\256\272.md" @@ -0,0 +1,87 @@ +经历过技术面试的小伙伴想必对这个两个概念已经再熟悉不过了! + +Guide哥当年参加面试的时候,不夸张地说,只要问到分布式相关的内容,面试官几乎是必定会问这两个分布式相关的理论。 + +并且,这两个理论也可以说是小伙伴们学习分布式相关内容的基础了! + +因此,小伙伴们非常非常有必要将这理论搞懂,并且能够用自己的理解给别人讲出来。 + +这篇文章我会站在自己的角度对这两个概念进行解读! + +*个人能力有限。如果文章有任何需要改善和完善的地方,欢迎在评论区指出,共同进步!——爱你们的Guide哥* + +## CAP理论 + +[CAP 理论/定理](https://zh.wikipedia.org/wiki/CAP%E5%AE%9A%E7%90%86)起源于 2000年,由加州大学伯克利分校的Eric Brewer教授在分布式计算原理研讨会(PODC)上提出,因此 CAP定理又被称作 **布鲁尔定理(Brewer’s theorem)** + +2年后,麻省理工学院的Seth Gilbert和Nancy Lynch 发表了布鲁尔猜想的证明,CAP理论正式成为分布式领域的定理。 + +### 简介 + +**CAP** 也就是 **Consistency(一致性)**、**Availability(可用性)**、**Partition Tolerance(分区容错性)** 这三个单词首字母组合。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/cap.png) + +CAP 理论的提出者布鲁尔在提出 CAP 猜想的时候,并没有详细定义 **Consistency**、**Availability**、**Partition Tolerance** 三个单词的明确定义。 + +因此,对于 CAP 的民间解读有很多,一般比较被大家推荐的是下面 👇 这种版本的解。 + +在理论计算机科学中,CAP 定理(CAP theorem)指出对于一个分布式系统来说,当设计读写操作时,只能能同时满足以下三点中的两个: + +- **一致性(Consistence)** : 所有节点访问同一份最新的数据副本 +- **可用性(Availability)**: 非故障的节点在合理的时间内返回合理的响应(不是错误或者超时的响应)。 +- **分区容错性(Partition tolerance)** : 分布式系统出现网络分区的时候,仍然能够对外提供服务。 + +**什么是网络分区?** + +> 分布式系统中,多个节点之前的网络本来是连通的,但是因为某些故障(比如部分节点网络出了问题)某些节点之间不连通了,整个网络就分成了几块区域,这就叫网络分区。 + +![partition-tolerance](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/partition-tolerance.png) + +### 不是所谓的“3 选 2” + +大部分人解释这一定律时,常常简单的表述为:“一致性、可用性、分区容忍性三者你只能同时达到其中两个,不可能同时达到”。实际上这是一个非常具有误导性质的说法,而且在 CAP 理论诞生 12 年之后,CAP 之父也在 2012 年重写了之前的论文。 + +> **当发生网络分区的时候,如果我们要继续服务,那么强一致性和可用性只能 2 选 1。也就是说当网络分区之后 P 是前提,决定了 P 之后才有 C 和 A 的选择。也就是说分区容错性(Partition tolerance)我们是必须要实现的。** +> +> 简而言之就是:CAP 理论中分区容错性 P 是一定要满足的,在此基础上,只能满足可用性 A 或者一致性 C。 + +因此,**分布式系统理论上不可能选择 CA 架构,只能选择 CP 或者 AP 架构。** + +**为啥无同时保证 CA 呢?** + +举个例子:若系统出现“分区”,系统中的某个节点在进行写操作。为了保证 C, 必须要禁止其他节点的读写操作,这就和 A 发生冲突了。如果为了保证 A,其他节点的读写操作正常的话,那就和 C 发生冲突了。 + +**选择的关键在于当前的业务场景,没有定论,比如对于需要确保强一致性的场景如银行一般会选择保证 CP 。** + +### CAP 实际应用案例 + +我这里以注册中心来探讨一下 CAP 的实际应用。考虑到很多小伙伴不知道注册中心是干嘛的,这里简单以 Dubbo 为例说一说。 + +下图是 Dubbo 的架构图。**注册中心 Registry 在其中扮演了什么角色呢?提供了什么服务呢?** + +注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/dubbo-architecture.png) + +常见的可以作为注册中心的组件有:ZooKeeper、Eureka、Nacos...。 + +1. **ZooKeeper 保证的是 CP。** 任何时刻对 ZooKeeper 的读请求都能得到一致性的结果,但是, ZooKeeper 不保证每次请求的可用性比如在 Leader 选举过程中或者半数以上的机器不可用的时候服务就是不可用的。 +2. **Eureka 保证的则是 AP。** Eureka 在设计的时候就是优先保证 A (可用性)。在 Eureka 中不存在什么 Leader 节点,每个节点都是一样的、平等的。因此 Eureka 不会像 ZooKeeper 那样出现选举过程中或者半数以上的机器不可用的时候服务就是不可用的情况。 Eureka 保证即使大部分节点挂掉也不会影响正常提供服务,只要有一个节点是可用的就行了。只不过这个节点上的数据可能并不是最新的。 +3. **Nacos 不仅支持 CP 也支持 AP。** + +### 总结 + +在进行分布式系统设计和开发时,我们不应该仅仅局限在 CAP 问题上,还要关注系统的扩展性、可用性等等 + +在系统发生“分区”的情况下,CAP 理论只能满足 CP 或者 AP。要注意的是,这里的前提是系统发生了“分区” + +如果系统没有发生“分区”的话,节点间的网络连接通信正常的话,也就不存在 P 了。这个时候,我们就可以同时保证 C 和 A 了。 + +总结:**如果系统发生“分区”,我们要考虑选择 CP 还是 AP。如果系统没有发生“分区”的话,我们要思考如何保证 CA 。** + +### 推荐阅读 + +1. [CAP 定理简化](https://medium.com/@ravindraprasad/cap-theorem-simplified-28499a67eab4) (英文,有趣的案例) +2. [神一样的 CAP 理论被应用在何方](https://juejin.im/post/6844903936718012430) (中文,列举了很多实际的例子) +3. [请停止呼叫数据库 CP 或 AP ](https://martin.kleppmann.com/2015/05/11/please-stop-calling-databases-cp-or-ap.html) (英文,带给你不一样的思考) \ No newline at end of file diff --git a/docs/system-design/micro-service/api-gateway-intro.md "b/docs/system-design/distributed-system/api-gateway/\344\270\272\344\273\200\344\271\210\350\246\201\347\275\221\347\253\231\346\234\211\345\223\252\344\272\233\345\270\270\350\247\201\347\232\204\347\275\221\347\253\231\347\263\273\347\273\237.md" similarity index 100% rename from docs/system-design/micro-service/api-gateway-intro.md rename to "docs/system-design/distributed-system/api-gateway/\344\270\272\344\273\200\344\271\210\350\246\201\347\275\221\347\253\231\346\234\211\345\223\252\344\272\233\345\270\270\350\247\201\347\232\204\347\275\221\347\253\231\347\263\273\347\273\237.md" diff --git "a/docs/system-design/micro-service/API\347\275\221\345\205\263.md" "b/docs/system-design/distributed-system/api-gateway/\345\246\202\344\275\225\350\256\276\350\256\241\344\270\200\344\270\252\344\272\277\347\272\247\347\275\221\345\205\263.md" similarity index 100% rename from "docs/system-design/micro-service/API\347\275\221\345\205\263.md" rename to "docs/system-design/distributed-system/api-gateway/\345\246\202\344\275\225\350\256\276\350\256\241\344\270\200\344\270\252\344\272\277\347\272\247\347\275\221\345\205\263.md" diff --git a/docs/system-design/data-communication/kafka-inverview.md "b/docs/system-design/distributed-system/message-queue/Kafka\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" similarity index 100% rename from docs/system-design/data-communication/kafka-inverview.md rename to "docs/system-design/distributed-system/message-queue/Kafka\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" diff --git a/docs/system-design/data-communication/rabbitmq.md "b/docs/system-design/distributed-system/message-queue/RabbitMQ\345\205\245\351\227\250\347\234\213\350\277\231\344\270\200\347\257\207\345\260\261\345\244\237\344\272\206.md" similarity index 100% rename from docs/system-design/data-communication/rabbitmq.md rename to "docs/system-design/distributed-system/message-queue/RabbitMQ\345\205\245\351\227\250\347\234\213\350\277\231\344\270\200\347\257\207\345\260\261\345\244\237\344\272\206.md" diff --git a/docs/system-design/data-communication/RocketMQ-Questions.md b/docs/system-design/distributed-system/message-queue/RocketMQ-Questions.md similarity index 100% rename from docs/system-design/data-communication/RocketMQ-Questions.md rename to docs/system-design/distributed-system/message-queue/RocketMQ-Questions.md diff --git a/docs/system-design/data-communication/RocketMQ.md b/docs/system-design/distributed-system/message-queue/RocketMQ.md similarity index 100% rename from docs/system-design/data-communication/RocketMQ.md rename to docs/system-design/distributed-system/message-queue/RocketMQ.md diff --git a/docs/system-design/distributed-system/message-queue/message-queue.md b/docs/system-design/distributed-system/message-queue/message-queue.md new file mode 100644 index 00000000000..ed7461eb9a3 --- /dev/null +++ b/docs/system-design/distributed-system/message-queue/message-queue.md @@ -0,0 +1,153 @@ + +# 消息队列其实很简单 + +“RabbitMQ?”“Kafka?”“RocketMQ?”...在日常学习与开发过程中,我们常常听到消息队列这个关键词。我也在我的多篇文章中提到了这个概念。可能你是熟练使用消息队列的老手,又或者你是不懂消息队列的新手,不论你了不了解消息队列,本文都将带你搞懂消息队列的一些基本理论。如果你是老手,你可能从本文学到你之前不曾注意的一些关于消息队列的重要概念,如果你是新手,相信本文将是你打开消息队列大门的一板砖。 + +## 一 什么是消息队列 + +**我们可以把消息队列看作是一个存放消息的容器,当我们需要使用消息的时候,直接从容器中取出消息供自己使用即可。** + +![Message queue](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/message-queue-small.png) + +消息队列是分布式系统中重要的组件之一。使用消息队列主要是为了通过异步处理提高系统性能和削峰、降低系统耦合性。 + +我们知道队列 Queue 是一种先进先出的数据结构,所以消费消息时也是按照顺序来消费的。 + +## 二 为什么要用消息队列 + +我觉得使用消息队列主要有三点好处: + +1. **通过异步处理提高系统性能(减少响应所需时间)。** +2. **削峰/限流** +3. **降低系统耦合性。** + +如果在面试的时候你被面试官问到这个问题的话,一般情况是你在你的简历上涉及到消息队列这方面的内容,这个时候推荐你结合你自己的项目来回答。 + + +《大型网站技术架构》第四章和第七章均有提到消息队列对应用性能及扩展性的提升。 + +### 2.1 通过异步处理提高系统性能(减少响应所需时间) + + + +![通过异步处理提高系统性能](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/Asynchronous-message-queue.png) + + + +将用户的请求数据存储到消息队列之后就立即返回结果。随后,系统再对消息进行消费。 + +因为**用户请求数据写入消息队列之后就立即返回给用户了,但是请求数据在后续的业务校验、写数据库等操作中可能失败**。因此使用消息队列进行异步处理之后,需要**适当修改业务流程进行配合**,比如**用户在提交订单之后,订单数据写入消息队列,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进程真正处理完该订单之后,甚至出库后,再通过电子邮件或短信通知用户订单成功**,以免交易纠纷。这就类似我们平时手机订火车票和电影票。 + +### 2.2 削峰/限流 + +**先将短时间高并发产生的事务消息存储在消息队列中,然后后端服务再慢慢根据自己的能力去消费这些消息,这样就避免直接把后端服务打垮掉。** + +举例:在电子商务一些秒杀、促销活动中,合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击。如下图所示: + +![削峰](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/削峰-消息队列.png) + +### 2.3 降低系统耦合性 + +使用消息队列还可以降低系统耦合性。我们知道如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性无疑更好一些。还是直接上图吧: + +![解耦](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/消息队列-解耦.png) + +生产者(客户端)发送消息到消息队列中去,接受者(服务端)处理消息,需要消费的系统直接去消息队列取消息进行消费即可而不需要和其他系统有耦合, 这显然也提高了系统的扩展性。 + +**消息队列使利用发布-订阅模式工作,消息发送者(生产者)发布消息,一个或多个消息接受者(消费者)订阅消息。** 从上图可以看到**消息发送者(生产者)和消息接受者(消费者)之间没有直接耦合**,消息发送者将消息发送至分布式消息队列即结束对消息的处理,消息接受者从分布式消息队列获取该消息后进行后续处理,并不需要知道该消息从何而来。**对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,从而实现网站业务的可扩展性设计**。 + +消息接受者对消息进行过滤、处理、包装后,构造成一个新的消息类型,将消息继续发送出去,等待其他消息接受者订阅该消息。因此基于事件(消息对象)驱动的业务架构可以是一系列流程。 + +**另外为了避免消息队列服务器宕机造成消息丢失,会将成功发送到消息队列的消息存储在消息生产者服务器上,等消息真正被消费者服务器处理后才删除消息。在消息队列服务器宕机后,生产者服务器会选择分布式消息队列服务器集群中的其他服务器发布消息。** + +**备注:** 不要认为消息队列只能利用发布-订阅模式工作,只不过在解耦这个特定业务环境下是使用发布-订阅模式的。**除了发布-订阅模式,还有点对点订阅模式(一个消息只有一个消费者),我们比较常用的是发布-订阅模式。** 另外,这两种消息模型是 JMS 提供的,AMQP 协议还提供了 5 种消息模型。 + +## 三 使用消息队列带来的一些问题 + +- **系统可用性降低:** 系统可用性在某种程度上降低,为什么这样说呢?在加入MQ之前,你不用考虑消息丢失或者说MQ挂掉等等的情况,但是,引入MQ之后你就需要去考虑了! +- **系统复杂性提高:** 加入MQ之后,你需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺序性等等问题! +- **一致性问题:** 我上面讲了消息队列可以实现异步,消息队列带来的异步确实可以提高系统响应速度。但是,万一消息的真正消费者并没有正确消费消息怎么办?这样就会导致数据不一致的情况了! + +## 四 JMS VS AMQP + +### 4.1 JMS + +#### 4.1.1 JMS 简介 + +JMS(JAVA Message Service,java消息服务)是java的消息服务,JMS的客户端之间可以通过JMS服务进行异步的消息传输。**JMS(JAVA Message Service,Java消息服务)API是一个消息服务的标准或者说是规范**,允许应用程序组件基于JavaEE平台创建、发送、接收和读取消息。它使分布式通信耦合度更低,消息服务更加可靠以及异步性。 + +**ActiveMQ 就是基于 JMS 规范实现的。** + +#### 4.1.2 JMS两种消息模型 + +①点到点(P2P)模型 + +![点到点(P2P)模型](https://user-gold-cdn.xitu.io/2018/4/21/162e7185572ca37d?w=575&h=135&f=gif&s=8530) +   + +使用**队列(Queue)**作为消息通信载体;满足**生产者与消费者模式**,一条消息只能被一个消费者使用,未被消费的消息在队列中保留直到被消费或超时。比如:我们生产者发送100条消息的话,两个消费者来消费一般情况下两个消费者会按照消息发送的顺序各自消费一半(也就是你一个我一个的消费。) + +② 发布/订阅(Pub/Sub)模型 + + ![发布/订阅(Pub/Sub)模型](https://user-gold-cdn.xitu.io/2018/4/21/162e7187c268eaa5?w=402&h=164&f=gif&s=15492) +   + +发布订阅模型(Pub/Sub) 使用**主题(Topic)**作为消息通信载体,类似于**广播模式**;发布者发布一条消息,该消息通过主题传递给所有的订阅者,**在一条消息广播之后才订阅的用户则是收不到该条消息的**。 + +#### 4.1.3 JMS 五种不同的消息正文格式 + +JMS定义了五种不同的消息正文格式,以及调用的消息类型,允许你发送并接收以一些不同形式的数据,提供现有消息格式的一些级别的兼容性。 + +- StreamMessage -- Java原始值的数据流 +- MapMessage--一套名称-值对 +- TextMessage--一个字符串对象 +- ObjectMessage--一个序列化的 Java对象 +- BytesMessage--一个字节的数据流 + + +### 4.2 AMQP + +AMQP,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准 **高级消息队列协议**(二进制应用层协议),是应用层协议的一个开放标准,为面向消息的中间件设计,兼容 JMS。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件同产品,不同的开发语言等条件的限制。 + +**RabbitMQ 就是基于 AMQP 协议实现的。** + +### 4.3 JMS vs AMQP + + +|对比方向| JMS | AMQP | +| :-------- | --------:| :--: | +| 定义| Java API | 协议 | +| 跨语言 | 否 | 是 | +| 跨平台 | 否 | 是 | +| 支持消息类型 | 提供两种消息模型:①Peer-2-Peer;②Pub/sub| 提供了五种消息模型:①direct exchange;②fanout exchange;③topic change;④headers exchange;⑤system exchange。本质来讲,后四种和JMS的pub/sub模型没有太大差别,仅是在路由机制上做了更详细的划分;| +|支持消息类型| 支持多种消息类型 ,我们在上面提到过| byte[](二进制)| + +**总结:** + +- AMQP 为消息定义了线路层(wire-level protocol)的协议,而JMS所定义的是API规范。在 Java 体系中,多个client均可以通过JMS进行交互,不需要应用修改代码,但是其对跨平台的支持较差。而AMQP天然具有跨平台、跨语言特性。 +- JMS 支持TextMessage、MapMessage 等复杂的消息类型;而 AMQP 仅支持 byte[] 消息类型(复杂的类型可序列化后发送)。 +- 由于Exchange 提供的路由算法,AMQP可以提供多样化的路由方式来传递消息到消息队列,而 JMS 仅支持 队列 和 主题/订阅 方式两种。 + + +## 五 常见的消息队列对比 + + + +对比方向 |概要 +-------- | --- + 吞吐量| 万级的 ActiveMQ 和 RabbitMQ 的吞吐量(ActiveMQ 的性能最差)要比 十万级甚至是百万级的 RocketMQ 和 Kafka 低一个数量级。 +可用性| 都可以实现高可用。ActiveMQ 和 RabbitMQ 都是基于主从架构实现高可用性。RocketMQ 基于分布式架构。 kafka 也是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 +时效性| RabbitMQ 基于erlang开发,所以并发能力很强,性能极其好,延时很低,达到微秒级。其他三个都是 ms 级。 +功能支持| 除了 Kafka,其他三个功能都较为完备。 Kafka 功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算以及日志采集被大规模使用,是事实上的标准 +消息丢失| ActiveMQ 和 RabbitMQ 丢失的可能性非常低, RocketMQ 和 Kafka 理论上不会丢失。 + + +**总结:** + +- ActiveMQ 的社区算是比较成熟,但是较目前来说,ActiveMQ 的性能比较差,而且版本迭代很慢,不推荐使用。 +- RabbitMQ 在吞吐量方面虽然稍逊于 Kafka 和 RocketMQ ,但是由于它基于 erlang 开发,所以并发能力很强,性能极其好,延时很低,达到微秒级。但是也因为 RabbitMQ 基于 erlang 开发,所以国内很少有公司有实力做erlang源码级别的研究和定制。如果业务场景对并发量要求不是太高(十万级、百万级),那这四种消息队列中,RabbitMQ 一定是你的首选。如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。 +- RocketMQ 阿里出品,Java 系开源项目,源代码我们可以直接阅读,然后可以定制自己公司的MQ,并且 RocketMQ 有阿里巴巴的实际业务场景的实战考验。RocketMQ 社区活跃度相对较为一般,不过也还可以,文档相对来说简单一些,然后接口这块不是按照标准 JMS 规范走的有些系统要迁移需要修改大量代码。还有就是阿里出台的技术,你得做好这个技术万一被抛弃,社区黄掉的风险,那如果你们公司有技术实力我觉得用RocketMQ 挺好的 +- Kafka 的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms 级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展。同时 kafka 最好是支撑较少的 topic 数量即可,保证其超高吞吐量。kafka 唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略这个特性天然适合大数据实时计算以及日志收集。 + + +参考:《Java工程师面试突击第1季-中华石杉老师》 diff --git a/docs/system-design/data-communication/dubbo.md "b/docs/system-design/distributed-system/rpc/\345\205\263\344\272\216Dubbo\347\232\204\351\207\215\350\246\201\347\237\245\350\257\206\347\202\271.md" similarity index 100% rename from docs/system-design/data-communication/dubbo.md rename to "docs/system-design/distributed-system/rpc/\345\205\263\344\272\216Dubbo\347\232\204\351\207\215\350\246\201\347\237\245\350\257\206\347\202\271.md" diff --git a/docs/system-design/data-communication/why-use-rpc.md "b/docs/system-design/distributed-system/rpc/\346\234\215\345\212\241\344\271\213\351\227\264\347\232\204\350\260\203\347\224\250\344\270\272\345\225\245\344\270\215\347\233\264\346\216\245\347\224\250HTTP\350\200\214\347\224\250RPC.md" similarity index 99% rename from docs/system-design/data-communication/why-use-rpc.md rename to "docs/system-design/distributed-system/rpc/\346\234\215\345\212\241\344\271\213\351\227\264\347\232\204\350\260\203\347\224\250\344\270\272\345\225\245\344\270\215\347\233\264\346\216\245\347\224\250HTTP\350\200\214\347\224\250RPC.md" index f295e13beed..4a139911268 100644 --- a/docs/system-design/data-communication/why-use-rpc.md +++ "b/docs/system-design/distributed-system/rpc/\346\234\215\345\212\241\344\271\213\351\227\264\347\232\204\350\260\203\347\224\250\344\270\272\345\225\245\344\270\215\347\233\264\346\216\245\347\224\250HTTP\350\200\214\347\224\250RPC.md" @@ -41,7 +41,7 @@ http://www.importnew.com/22003.html ## 既有 HTTP ,为啥用 RPC 进行服务调用? -###RPC 只是一种设计而已 +### RPC 只是一种设计而已 RPC 只是一种概念、一种设计,就是为了解决 **不同服务之间的调用问题**, 它一般会包含有 **传输协议** 和 **序列化协议** 这两个。 diff --git a/docs/system-design/distributed-system/zookeeper/images/curator.png b/docs/system-design/distributed-system/zookeeper/images/curator.png new file mode 100644 index 00000000000..28da0247ec3 Binary files /dev/null and b/docs/system-design/distributed-system/zookeeper/images/curator.png differ diff --git "a/docs/system-design/distributed-system/zookeeper/images/watche\346\234\272\345\210\266.png" "b/docs/system-design/distributed-system/zookeeper/images/watche\346\234\272\345\210\266.png" new file mode 100644 index 00000000000..68144db1e54 Binary files /dev/null and "b/docs/system-design/distributed-system/zookeeper/images/watche\346\234\272\345\210\266.png" differ diff --git a/docs/system-design/distributed-system/zookeeper/images/znode-structure.png b/docs/system-design/distributed-system/zookeeper/images/znode-structure.png new file mode 100644 index 00000000000..746c3f6e3a4 Binary files /dev/null and b/docs/system-design/distributed-system/zookeeper/images/znode-structure.png differ diff --git "a/docs/system-design/distributed-system/zookeeper/images/zookeeper\351\233\206\347\276\244.png" "b/docs/system-design/distributed-system/zookeeper/images/zookeeper\351\233\206\347\276\244.png" new file mode 100644 index 00000000000..a3067cda18e Binary files /dev/null and "b/docs/system-design/distributed-system/zookeeper/images/zookeeper\351\233\206\347\276\244.png" differ diff --git "a/docs/system-design/distributed-system/zookeeper/images/zookeeper\351\233\206\347\276\244\344\270\255\347\232\204\350\247\222\350\211\262.png" "b/docs/system-design/distributed-system/zookeeper/images/zookeeper\351\233\206\347\276\244\344\270\255\347\232\204\350\247\222\350\211\262.png" new file mode 100644 index 00000000000..6b118fe08e1 Binary files /dev/null and "b/docs/system-design/distributed-system/zookeeper/images/zookeeper\351\233\206\347\276\244\344\270\255\347\232\204\350\247\222\350\211\262.png" differ diff --git "a/docs/system-design/distributed-system/zookeeper/images/\350\277\236\346\216\245ZooKeeper\346\234\215\345\212\241.png" "b/docs/system-design/distributed-system/zookeeper/images/\350\277\236\346\216\245ZooKeeper\346\234\215\345\212\241.png" new file mode 100644 index 00000000000..d391329949d Binary files /dev/null and "b/docs/system-design/distributed-system/zookeeper/images/\350\277\236\346\216\245ZooKeeper\346\234\215\345\212\241.png" differ diff --git a/docs/system-design/framework/zookeeper-in-action.md b/docs/system-design/distributed-system/zookeeper/zookeeper-in-action.md similarity index 98% rename from docs/system-design/framework/zookeeper-in-action.md rename to docs/system-design/distributed-system/zookeeper/zookeeper-in-action.md index 7b0a2a62919..4b1f9eb9175 100644 --- a/docs/system-design/framework/zookeeper-in-action.md +++ b/docs/system-design/distributed-system/zookeeper/zookeeper-in-action.md @@ -63,7 +63,7 @@ root@eaf70fc620cb:/apache-zookeeper-3.5.8-bin# cd bin 如果你看到控制台成功打印出如下信息的话,说明你已经成功连接 ZooKeeper 服务。 -![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-8/image-20200805144047420.png) +![](images/连接ZooKeeper服务.png) ### 2.3. 常用命令演示 @@ -189,7 +189,7 @@ numChildren = 1 Curator 是Netflix公司开源的一套 ZooKeeper Java客户端框架,相比于 Zookeeper 自带的客户端 zookeeper 来说,Curator 的封装更加完善,各种 API 都可以比较方便地使用。 -![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-8/ph-quote.png) +![](images/curator.png) 下面我们就来简单地演示一下 Curator 的使用吧! diff --git a/docs/system-design/framework/zookeeper-intro.md b/docs/system-design/distributed-system/zookeeper/zookeeper-intro.md similarity index 98% rename from docs/system-design/framework/zookeeper-intro.md rename to docs/system-design/distributed-system/zookeeper/zookeeper-intro.md index 11d339e3ecc..4978fb1a979 100644 --- a/docs/system-design/framework/zookeeper-intro.md +++ b/docs/system-design/distributed-system/zookeeper/zookeeper-intro.md @@ -111,7 +111,7 @@ ZooKeeper 数据模型采用层次化的多叉树形结构,每个节点上都 从下图可以更直观地看出:ZooKeeper 节点路径标识方式和 Unix 文件系统路径非常相似,都是由一系列使用斜杠"/"进行分割的路径表示,开发人员可以向这个节点中写人数据,也可以在节点下面创建子节点。这些操作我们后面都会介绍到。 -![ZooKeeper 数据模型](https://images.gitbook.cn/95a192b0-1c56-11e9-9a8e-f3b01b1ea9aa) +![ZooKeeper 数据模型](images/znode-structure.png) ### 3.2. znode(数据节点) @@ -204,7 +204,7 @@ ZooKeeper 采用 ACL(AccessControlLists)策略来进行权限控制,类似 Watcher(事件监听器),是 ZooKeeper 中的一个很重要的特性。ZooKeeper 允许用户在指定节点上注册一些 Watcher,并且在一些特定事件触发的时候,ZooKeeper 服务端会将事件通知到感兴趣的客户端上去,该机制是 ZooKeeper 实现分布式协调服务的重要特性。 -![watcher机制](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-8/watcher%E6%9C%BA%E5%88%B6.jpg) +![watcher机制](images/watche机制.png) _破音:非常有用的一个特性,都能出小本本记好了,后面用到 ZooKeeper 基本离不开 Watcher(事件监听器)机制。_ @@ -220,7 +220,7 @@ Session 有一个属性叫做:`sessionTimeout` ,`sessionTimeout` 代表会 为了保证高可用,最好是以集群形态来部署 ZooKeeper,这样只要集群中大部分机器是可用的(能够容忍一定的机器故障),那么 ZooKeeper 本身仍然是可用的。通常 3 台服务器就可以构成一个 ZooKeeper 集群了。ZooKeeper 官方提供的架构图就是一个 ZooKeeper 集群整体对外提供服务。 -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-10/68900686.jpg) +![](images/zookeeper集群.png) 上图中每一个 Server 代表一个安装 ZooKeeper 服务的服务器。组成 ZooKeeper 服务的服务器都会在内存中维护当前的服务器状态,并且每台服务器之间都互相保持着通信。集群间通过 ZAB 协议(ZooKeeper Atomic Broadcast)来保持数据的一致性。 @@ -230,7 +230,7 @@ Session 有一个属性叫做:`sessionTimeout` ,`sessionTimeout` 代表会 但是,在 ZooKeeper 中没有选择传统的 Master/Slave 概念,而是引入了 Leader、Follower 和 Observer 三种角色。如下图所示 -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-10/89602762.jpg) +![](images/zookeeper集群中的角色.png) ZooKeeper 集群中的所有机器通过一个 **Leader 选举过程** 来选定一台称为 “**Leader**” 的机器,Leader 既可以为客户端提供写服务又能提供读服务。除了 Leader 外,**Follower** 和 **Observer** 都只能提供读服务。Follower 和 Observer 唯一的区别在于 Observer 机器不参与 Leader 的选举过程,也不参与写操作的“过半写成功”策略,因此 Observer 机器可以在不影响写性能的情况下提升集群的读性能。 diff --git a/docs/system-design/framework/ZooKeeper-plus.md b/docs/system-design/distributed-system/zookeeper/zookeeper-plus.md similarity index 99% rename from docs/system-design/framework/ZooKeeper-plus.md rename to docs/system-design/distributed-system/zookeeper/zookeeper-plus.md index 0aa3eeff422..367720484a3 100644 --- a/docs/system-design/framework/ZooKeeper-plus.md +++ b/docs/system-design/distributed-system/zookeeper/zookeeper-plus.md @@ -75,7 +75,7 @@ ![](http://img.francisqiang.top/img/垃圾例子.jpg) -而上述前者就是 `Eureka` 的处理方式,它保证了AP(可用性),后者就是我们今天所要将的 `ZooKeeper` 的处理方式,它保证了CP(数据一致性)。 +而上述前者就是 `Eureka` 的处理方式,它保证了AP(可用性),后者就是我们今天所要讲的 `ZooKeeper` 的处理方式,它保证了CP(数据一致性)。 ## 4. 一致性协议和算法 @@ -405,4 +405,4 @@ * `zookeeper` 的典型应用场景,比如选主,注册中心等等。 - 如果忘了可以回去看看再次理解一下,如果有疑问和建议欢迎提出🤝🤝🤝。 \ No newline at end of file + 如果忘了可以回去看看再次理解一下,如果有疑问和建议欢迎提出🤝🤝🤝。 diff --git a/docs/system-design/framework/mybatis/mybatis-interview.md b/docs/system-design/framework/mybatis/mybatis-interview.md index 877d79dd6cd..0c4d4b9d668 100644 --- a/docs/system-design/framework/mybatis/mybatis-interview.md +++ b/docs/system-design/framework/mybatis/mybatis-interview.md @@ -1,12 +1,12 @@ > 本篇文章是JavaGuide收集自网络,原出处不明。 -Mybatis 技术内幕系列博客,从原理和源码角度,介绍了其内部实现细节,无论是写的好与不好,我确实是用心写了,由于并不是介绍如何使用 Mybatis 的文章,所以,一些参数使用细节略掉了,我们的目标是介绍 Mybatis 的技术架构和重要组成部分,以及基本运行原理。 +MyBatis 技术内幕系列博客,从原理和源码角度,介绍了其内部实现细节,无论是写的好与不好,我确实是用心写了,由于并不是介绍如何使用 MyBatis 的文章,所以,一些参数使用细节略掉了,我们的目标是介绍 MyBatis 的技术架构和重要组成部分,以及基本运行原理。 博客写的很辛苦,但是写出来却不一定好看,所谓开始很兴奋,过程很痛苦,结束很遗憾。要求不高,只要读者能从系列博客中,学习到一点其他博客所没有的技术点,作为作者,我就很欣慰了,我也读别人写的博客,通常对自己当前研究的技术,是很有帮助的。 尽管还有很多可写的内容,但是,我认为再写下去已经没有意义,任何其他小的功能点,都是在已经介绍的基本框架和基本原理下运行的,只有结束,才能有新的开始。写博客也积攒了一些经验,源码多了感觉就是复制黏贴,源码少了又觉得是空谈原理,将来再写博客,我希望是“精炼博文”,好读好懂美观读起来又不累,希望自己能再写一部开源分布式框架原理系列博客。 -有胆就来,我出几道 Mybatis 面试题,看你能回答上来几道(都是我出的,可不是网上找的)。 +有胆就来,我出几道 MyBatis 面试题,看你能回答上来几道(都是我出的,可不是网上找的)。 #### 1、#{}和\${}的区别是什么? @@ -15,7 +15,7 @@ Mybatis 技术内幕系列博客,从原理和源码角度,介绍了其内部 答: - `${}`是 Properties 文件中的变量占位符,它可以用于标签属性值和 sql 内部,属于静态文本替换,比如\${driver}会被静态替换为`com.mysql.jdbc.Driver`。 -- `#{}`是 sql 的参数占位符,Mybatis 会将 sql 中的`#{}`替换为?号,在 sql 执行前会使用 PreparedStatement 的参数设置方法,按序给 sql 的?号占位符设置参数值,比如 ps.setInt(0, parameterValue),`#{item.name}` 的取值方式为使用反射从参数对象中获取 item 对象的 name 属性值,相当于 `param.getItem().getName()`。 +- `#{}`是 sql 的参数占位符,MyBatis 会将 sql 中的`#{}`替换为?号,在 sql 执行前会使用 PreparedStatement 的参数设置方法,按序给 sql 的?号占位符设置参数值,比如 ps.setInt(0, parameterValue),`#{item.name}` 的取值方式为使用反射从参数对象中获取 item 对象的 name 属性值,相当于 `param.getItem().getName()`。 #### 2、Xml 映射文件中,除了常见的 select|insert|updae|delete 标签之外,还有哪些标签? @@ -27,65 +27,65 @@ Mybatis 技术内幕系列博客,从原理和源码角度,介绍了其内部 注:这道题也是京东面试官面试我时问的。 -答:Dao 接口,就是人们常说的 `Mapper`接口,接口的全限名,就是映射文件中的 namespace 的值,接口的方法名,就是映射文件中`MappedStatement`的 id 值,接口方法内的参数,就是传递给 sql 的参数。`Mapper`接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为 key 值,可唯一定位一个`MappedStatement`,举例:`com.mybatis3.mappers.StudentDao.findStudentById`,可以唯一找到 namespace 为`com.mybatis3.mappers.StudentDao`下面`id = findStudentById`的`MappedStatement`。在 Mybatis 中,每一个``、``、``、``标签,都会被解析为一个`MappedStatement`对象。 Dao 接口里的方法,是不能重载的,因为是全限名+方法名的保存和寻找策略。 -Dao 接口的工作原理是 JDK 动态代理,Mybatis 运行时会使用 JDK 动态代理为 Dao 接口生成代理 proxy 对象,代理对象 proxy 会拦截接口方法,转而执行`MappedStatement`所代表的 sql,然后将 sql 执行结果返回。 +Dao 接口的工作原理是 JDK 动态代理,MyBatis 运行时会使用 JDK 动态代理为 Dao 接口生成代理 proxy 对象,代理对象 proxy 会拦截接口方法,转而执行`MappedStatement`所代表的 sql,然后将 sql 执行结果返回。 -#### 4、Mybatis 是如何进行分页的?分页插件的原理是什么? +#### 4、MyBatis 是如何进行分页的?分页插件的原理是什么? 注:我出的。 -答:Mybatis 使用 RowBounds 对象进行分页,它是针对 ResultSet 结果集执行的内存分页,而非物理分页,可以在 sql 内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。 +答:MyBatis 使用 RowBounds 对象进行分页,它是针对 ResultSet 结果集执行的内存分页,而非物理分页,可以在 sql 内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。 -分页插件的基本原理是使用 Mybatis 提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的 sql,然后重写 sql,根据 dialect 方言,添加对应的物理分页语句和物理分页参数。 +分页插件的基本原理是使用 MyBatis 提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的 sql,然后重写 sql,根据 dialect 方言,添加对应的物理分页语句和物理分页参数。 举例:`select _ from student`,拦截 sql 后重写为:`select t._ from (select \* from student)t limit 0,10` -#### 5、简述 Mybatis 的插件运行原理,以及如何编写一个插件。 +#### 5、简述 MyBatis 的插件运行原理,以及如何编写一个插件。 注:我出的。 -答:Mybatis 仅可以编写针对 `ParameterHandler`、`ResultSetHandler`、`StatementHandler`、`Executor` 这 4 种接口的插件,Mybatis 使用 JDK 的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这 4 种接口对象的方法时,就会进入拦截方法,具体就是 `InvocationHandler` 的 `invoke()`方法,当然,只会拦截那些你指定需要拦截的方法。 +答:MyBatis 仅可以编写针对 `ParameterHandler`、`ResultSetHandler`、`StatementHandler`、`Executor` 这 4 种接口的插件,MyBatis 使用 JDK 的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这 4 种接口对象的方法时,就会进入拦截方法,具体就是 `InvocationHandler` 的 `invoke()`方法,当然,只会拦截那些你指定需要拦截的方法。 -实现 Mybatis 的 Interceptor 接口并复写` intercept()`方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住,别忘了在配置文件中配置你编写的插件。 +实现 MyBatis 的 Interceptor 接口并复写` intercept()`方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住,别忘了在配置文件中配置你编写的插件。 -#### 6、Mybatis 执行批量插入,能返回数据库主键列表吗? +#### 6、MyBatis 执行批量插入,能返回数据库主键列表吗? 注:我出的。 -答:能,JDBC 都能,Mybatis 当然也能。 +答:能,JDBC 都能,MyBatis 当然也能。 -#### 7、Mybatis 动态 sql 是做什么的?都有哪些动态 sql?能简述一下动态 sql 的执行原理不? +#### 7、MyBatis 动态 sql 是做什么的?都有哪些动态 sql?能简述一下动态 sql 的执行原理不? 注:我出的。 -答:Mybatis 动态 sql 可以让我们在 Xml 映射文件内,以标签的形式编写动态 sql,完成逻辑判断和动态拼接 sql 的功能,Mybatis 提供了 9 种动态 sql 标签 `trim|where|set|foreach|if|choose|when|otherwise|bind`。 +答:MyBatis 动态 sql 可以让我们在 Xml 映射文件内,以标签的形式编写动态 sql,完成逻辑判断和动态拼接 sql 的功能,MyBatis 提供了 9 种动态 sql 标签 `trim|where|set|foreach|if|choose|when|otherwise|bind`。 其执行原理为,使用 OGNL 从 sql 参数对象中计算表达式的值,根据表达式的值动态拼接 sql,以此来完成动态 sql 的功能。 -#### 8、Mybatis 是如何将 sql 执行结果封装为目标对象并返回的?都有哪些映射形式? +#### 8、MyBatis 是如何将 sql 执行结果封装为目标对象并返回的?都有哪些映射形式? 注:我出的。 -答:第一种是使用``标签,逐一定义列名和对象属性名之间的映射关系。第二种是使用 sql 列的别名功能,将列别名书写为对象属性名,比如 T_NAME AS NAME,对象属性名一般是 name,小写,但是列名不区分大小写,Mybatis 会忽略列名大小写,智能找到与之对应对象属性名,你甚至可以写成 T_NAME AS NaMe,Mybatis 一样可以正常工作。 +答:第一种是使用``标签,逐一定义列名和对象属性名之间的映射关系。第二种是使用 sql 列的别名功能,将列别名书写为对象属性名,比如 T_NAME AS NAME,对象属性名一般是 name,小写,但是列名不区分大小写,MyBatis 会忽略列名大小写,智能找到与之对应对象属性名,你甚至可以写成 T_NAME AS NaMe,MyBatis 一样可以正常工作。 -有了列名与属性名的映射关系后,Mybatis 通过反射创建对象,同时使用反射给对象的属性逐一赋值并返回,那些找不到映射关系的属性,是无法完成赋值的。 +有了列名与属性名的映射关系后,MyBatis 通过反射创建对象,同时使用反射给对象的属性逐一赋值并返回,那些找不到映射关系的属性,是无法完成赋值的。 -#### 9、Mybatis 能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别。 +#### 9、MyBatis 能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别。 注:我出的。 -答:能,Mybatis 不仅可以执行一对一、一对多的关联查询,还可以执行多对一,多对多的关联查询,多对一查询,其实就是一对一查询,只需要把 `selectOne()`修改为 `selectList()`即可;多对多查询,其实就是一对多查询,只需要把 `selectOne()`修改为 `selectList()`即可。 +答:能,MyBatis 不仅可以执行一对一、一对多的关联查询,还可以执行多对一,多对多的关联查询,多对一查询,其实就是一对一查询,只需要把 `selectOne()`修改为 `selectList()`即可;多对多查询,其实就是一对多查询,只需要把 `selectOne()`修改为 `selectList()`即可。 关联对象查询,有两种实现方式,一种是单独发送一个 sql 去查询关联对象,赋给主对象,然后返回主对象。另一种是使用嵌套查询,嵌套查询的含义为使用 join 查询,一部分列是 A 对象的属性值,另外一部分列是关联对象 B 的属性值,好处是只发一个 sql 查询,就可以把主对象和其关联对象查出来。 -那么问题来了,join 查询出来 100 条记录,如何确定主对象是 5 个,而不是 100 个?其去重复的原理是``标签内的``子标签,指定了唯一确定一条记录的 id 列,Mybatis 根据列值来完成 100 条记录的去重复功能,``可以有多个,代表了联合主键的语意。 +那么问题来了,join 查询出来 100 条记录,如何确定主对象是 5 个,而不是 100 个?其去重复的原理是``标签内的``子标签,指定了唯一确定一条记录的 id 列,MyBatis 根据列值来完成 100 条记录的去重复功能,``可以有多个,代表了联合主键的语意。 同样主对象的关联对象,也是根据这个原理去重复的,尽管一般情况下,只有主对象会有重复记录,关联对象一般不会重复。 -举例:下面 join 查询出来 6 条记录,一、二列是 Teacher 对象列,第三列为 Student 对象列,Mybatis 去重复处理后,结果为 1 个老师 6 个学生,而不是 6 个老师 6 个学生。 +举例:下面 join 查询出来 6 条记录,一、二列是 Teacher 对象列,第三列为 Student 对象列,MyBatis 去重复处理后,结果为 1 个老师 6 个学生,而不是 6 个老师 6 个学生。 t_id t_name s_id @@ -96,17 +96,17 @@ Dao 接口的工作原理是 JDK 动态代理,Mybatis 运行时会使用 JDK | 1 | teacher | 42 | | 1 | teacher | 43 | -#### 10、Mybatis 是否支持延迟加载?如果支持,它的实现原理是什么? +#### 10、MyBatis 是否支持延迟加载?如果支持,它的实现原理是什么? 注:我出的。 -答:Mybatis 仅支持 association 关联对象和 collection 关联集合对象的延迟加载,association 指的就是一对一,collection 指的就是一对多查询。在 Mybatis 配置文件中,可以配置是否启用延迟加载 `lazyLoadingEnabled=true|false。` +答:MyBatis 仅支持 association 关联对象和 collection 关联集合对象的延迟加载,association 指的就是一对一,collection 指的就是一对多查询。在 MyBatis 配置文件中,可以配置是否启用延迟加载 `lazyLoadingEnabled=true|false。` 它的原理是,使用` CGLIB` 创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用 `a.getB().getName()`,拦截器 `invoke()`方法发现 `a.getB()`是 null 值,那么就会单独发送事先保存好的查询关联 B 对象的 sql,把 B 查询上来,然后调用 a.setB(b),于是 a 的对象 b 属性就有值了,接着完成 `a.getB().getName()`方法的调用。这就是延迟加载的基本原理。 -当然了,不光是 Mybatis,几乎所有的包括 Hibernate,支持延迟加载的原理都是一样的。 +当然了,不光是 MyBatis,几乎所有的包括 Hibernate,支持延迟加载的原理都是一样的。 -#### 11、Mybatis 的 Xml 映射文件中,不同的 Xml 映射文件,id 是否可以重复? +#### 11、MyBatis 的 Xml 映射文件中,不同的 Xml 映射文件,id 是否可以重复? 注:我出的。 @@ -114,17 +114,17 @@ Dao 接口的工作原理是 JDK 动态代理,Mybatis 运行时会使用 JDK 原因就是 namespace+id 是作为 `Map`的 key 使用的,如果没有 namespace,就剩下 id,那么,id 重复会导致数据互相覆盖。有了 namespace,自然 id 就可以重复,namespace 不同,namespace+id 自然也就不同。 -#### 12、Mybatis 中如何执行批处理? +#### 12、MyBatis 中如何执行批处理? 注:我出的。 答:使用 BatchExecutor 完成批处理。 -#### 13、Mybatis 都有哪些 Executor 执行器?它们之间的区别是什么? +#### 13、MyBatis 都有哪些 Executor 执行器?它们之间的区别是什么? 注:我出的 -答:Mybatis 有三种基本的 Executor 执行器,**`SimpleExecutor`、`ReuseExecutor`、`BatchExecutor`。** +答:MyBatis 有三种基本的 Executor 执行器,**`SimpleExecutor`、`ReuseExecutor`、`BatchExecutor`。** **`SimpleExecutor`:**每执行一次 update 或 select,就开启一个 Statement 对象,用完立刻关闭 Statement 对象。 @@ -134,36 +134,36 @@ Dao 接口的工作原理是 JDK 动态代理,Mybatis 运行时会使用 JDK 作用范围:Executor 的这些特点,都严格限制在 SqlSession 生命周期范围内。 -#### 14、Mybatis 中如何指定使用哪一种 Executor 执行器? +#### 14、MyBatis 中如何指定使用哪一种 Executor 执行器? 注:我出的 -答:在 Mybatis 配置文件中,可以指定默认的 ExecutorType 执行器类型,也可以手动给 `DefaultSqlSessionFactory` 的创建 SqlSession 的方法传递 ExecutorType 类型参数。 +答:在 MyBatis 配置文件中,可以指定默认的 ExecutorType 执行器类型,也可以手动给 `DefaultSqlSessionFactory` 的创建 SqlSession 的方法传递 ExecutorType 类型参数。 -#### 15、Mybatis 是否可以映射 Enum 枚举类? +#### 15、MyBatis 是否可以映射 Enum 枚举类? 注:我出的 -答:Mybatis 可以映射枚举类,不单可以映射枚举类,Mybatis 可以映射任何对象到表的一列上。映射方式为自定义一个 `TypeHandler`,实现 `TypeHandler` 的 `setParameter()`和 `getResult()`接口方法。`TypeHandler` 有两个作用,一是完成从 javaType 至 jdbcType 的转换,二是完成 jdbcType 至 javaType 的转换,体现为 `setParameter()`和 `getResult()`两个方法,分别代表设置 sql 问号占位符参数和获取列查询结果。 +答:MyBatis 可以映射枚举类,不单可以映射枚举类,MyBatis 可以映射任何对象到表的一列上。映射方式为自定义一个 `TypeHandler`,实现 `TypeHandler` 的 `setParameter()`和 `getResult()`接口方法。`TypeHandler` 有两个作用,一是完成从 javaType 至 jdbcType 的转换,二是完成 jdbcType 至 javaType 的转换,体现为 `setParameter()`和 `getResult()`两个方法,分别代表设置 sql 问号占位符参数和获取列查询结果。 -#### 16、Mybatis 映射文件中,如果 A 标签通过 include 引用了 B 标签的内容,请问,B 标签能否定义在 A 标签的后面,还是说必须定义在 A 标签的前面? +#### 16、MyBatis 映射文件中,如果 A 标签通过 include 引用了 B 标签的内容,请问,B 标签能否定义在 A 标签的后面,还是说必须定义在 A 标签的前面? 注:我出的 -答:虽然 Mybatis 解析 Xml 映射文件是按照顺序解析的,但是,被引用的 B 标签依然可以定义在任何地方,Mybatis 都可以正确识别。 +答:虽然 MyBatis 解析 Xml 映射文件是按照顺序解析的,但是,被引用的 B 标签依然可以定义在任何地方,MyBatis 都可以正确识别。 -原理是,Mybatis 解析 A 标签,发现 A 标签引用了 B 标签,但是 B 标签尚未解析到,尚不存在,此时,Mybatis 会将 A 标签标记为未解析状态,然后继续解析余下的标签,包含 B 标签,待所有标签解析完毕,Mybatis 会重新解析那些被标记为未解析的标签,此时再解析 A 标签时,B 标签已经存在,A 标签也就可以正常解析完成了。 +原理是,MyBatis 解析 A 标签,发现 A 标签引用了 B 标签,但是 B 标签尚未解析到,尚不存在,此时,MyBatis 会将 A 标签标记为未解析状态,然后继续解析余下的标签,包含 B 标签,待所有标签解析完毕,MyBatis 会重新解析那些被标记为未解析的标签,此时再解析 A 标签时,B 标签已经存在,A 标签也就可以正常解析完成了。 -#### 17、简述 Mybatis 的 Xml 映射文件和 Mybatis 内部数据结构之间的映射关系? +#### 17、简述 MyBatis 的 Xml 映射文件和 MyBatis 内部数据结构之间的映射关系? 注:我出的 -答:Mybatis 将所有 Xml 配置信息都封装到 All-In-One 重量级对象 Configuration 内部。在 Xml 映射文件中,``标签会被解析为 `ParameterMap` 对象,其每个子元素会被解析为 ParameterMapping 对象。``标签会被解析为 `ResultMap` 对象,其每个子元素会被解析为 `ResultMapping` 对象。每一个``标签均会被解析为 `MappedStatement` 对象,标签内的 sql 会被解析为 BoundSql 对象。 -#### 18、为什么说 Mybatis 是半自动 ORM 映射工具?它与全自动的区别在哪里? +#### 18、为什么说 MyBatis 是半自动 ORM 映射工具?它与全自动的区别在哪里? 注:我出的 -答:Hibernate 属于全自动 ORM 映射工具,使用 Hibernate 查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。而 Mybatis 在查询关联对象或关联集合对象时,需要手动编写 sql 来完成,所以,称之为半自动 ORM 映射工具。 +答:Hibernate 属于全自动 ORM 映射工具,使用 Hibernate 查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。而 MyBatis 在查询关联对象或关联集合对象时,需要手动编写 sql 来完成,所以,称之为半自动 ORM 映射工具。 -面试题看似都很简单,但是想要能正确回答上来,必定是研究过源码且深入的人,而不是仅会使用的人或者用的很熟的人,以上所有面试题及其答案所涉及的内容,在我的 Mybatis 系列博客中都有详细讲解和原理分析。 \ No newline at end of file +面试题看似都很简单,但是想要能正确回答上来,必定是研究过源码且深入的人,而不是仅会使用的人或者用的很熟的人,以上所有面试题及其答案所涉及的内容,在我的 MyBatis 系列博客中都有详细讲解和原理分析。 diff --git a/docs/system-design/framework/spring/Spring-Design-Patterns.md b/docs/system-design/framework/spring/Spring-Design-Patterns.md index 968937f185d..c5d56ff6d37 100644 --- a/docs/system-design/framework/spring/Spring-Design-Patterns.md +++ b/docs/system-design/framework/spring/Spring-Design-Patterns.md @@ -30,7 +30,7 @@ Design Patterns(设计模式) 表示面向对象软件开发中最好的计算 ## 控制反转(IoC)和依赖注入(DI) -**IoC(Inversion of Control,控制翻转)** 是Spring 中一个非常非常重要的概念,它不是什么技术,而是一种解耦的设计思想。它的主要目的是借助于“第三方”(Spring 中的 IOC 容器) 实现具有依赖关系的对象之间的解耦(IOC容易管理对象,你只管使用即可),从而降低代码之间的耦合度。**IOC 是一个原则,而不是一个模式,以下模式(但不限于)实现了IoC原则。** +**IoC(Inversion of Control,控制反转)** 是Spring 中一个非常非常重要的概念,它不是什么技术,而是一种解耦的设计思想。它的主要目的是借助于“第三方”(Spring 中的 IOC 容器) 实现具有依赖关系的对象之间的解耦(IOC容器管理对象,你只管使用即可),从而降低代码之间的耦合度。**IOC 是一个原则,而不是一个模式,以下模式(但不限于)实现了IoC原则。** ![ioc-patterns](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/ioc-patterns.png) @@ -38,7 +38,7 @@ Design Patterns(设计模式) 表示面向对象软件开发中最好的计算 在实际项目中一个 Service 类如果有几百甚至上千个类作为它的底层,我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。如果利用 IOC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。关于Spring IOC 的理解,推荐看这一下知乎的一个回答: ,非常不错。 -**控制翻转怎么理解呢?** 举个例子:"对象a 依赖了对象 b,当对象 a 需要使用 对象 b的时候必须自己去创建。但是当系统引入了 IOC 容器后, 对象a 和对象 b 之前就失去了直接的联系。这个时候,当对象 a 需要使用 对象 b的时候, 我们可以指定 IOC 容器去创建一个对象b注入到对象 a 中"。 对象 a 获得依赖对象 b 的过程,由主动行为变为了被动行为,控制权翻转,这就是控制反转名字的由来。 +**控制反转怎么理解呢?** 举个例子:"对象a 依赖了对象 b,当对象 a 需要使用 对象 b的时候必须自己去创建。但是当系统引入了 IOC 容器后, 对象a 和对象 b 之前就失去了直接的联系。这个时候,当对象 a 需要使用 对象 b的时候, 我们可以指定 IOC 容器去创建一个对象b注入到对象 a 中"。 对象 a 获得依赖对象 b 的过程,由主动行为变为了被动行为,控制权反转,这就是控制反转名字的由来。 **DI(Dependecy Inject,依赖注入)是实现控制反转的一种设计模式,依赖注入就是将实例变量传入到一个对象中去。** @@ -154,8 +154,6 @@ AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无 模板方法模式是一种行为设计模式,它定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。 模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤的实现方式。 -![模板方法UML图](https://ws1.sinaimg.cn/large/006rNwoDgy1g3a73vdbojj30vo0iwdgc.jpg) - ```java public abstract class Template { //这是我们的模板方法 @@ -212,7 +210,7 @@ Spring 中默认存在以下事件,他们都是对 `ApplicationContextEvent` #### 事件监听者角色 -`ApplicationListener` 充当了事件监听者角色,它是一个接口,里面只定义了一个 `onApplicationEvent()`方法来处理`ApplicationEvent`。`ApplicationListener`接口类源码如下,可以看出接口定义看出接口中的事件只要实现了 `ApplicationEvent`就可以了。所以,在 Spring中我们只要实现 `ApplicationListener` 接口实现 `onApplicationEvent()` 方法即可完成监听事件 +`ApplicationListener` 充当了事件监听者角色,它是一个接口,里面只定义了一个 `onApplicationEvent()`方法来处理`ApplicationEvent`。`ApplicationListener`接口类源码如下,可以看出接口定义看出接口中的事件只要实现了 `ApplicationEvent`就可以了。所以,在 Spring中我们只要实现 `ApplicationListener` 接口的 `onApplicationEvent()` 方法即可完成监听事件 ```java package org.springframework.context; diff --git a/docs/system-design/framework/spring/SpringBean.md b/docs/system-design/framework/spring/SpringBean.md deleted file mode 100644 index 8968812a006..00000000000 --- a/docs/system-design/framework/spring/SpringBean.md +++ /dev/null @@ -1,451 +0,0 @@ - - -- [前言](#前言) -- [一 bean的作用域](#一-bean的作用域) - - [1. singleton——唯一 bean 实例](#1-singleton——唯一-bean-实例) - - [2. prototype——每次请求都会创建一个新的 bean 实例](#2-prototype——每次请求都会创建一个新的-bean-实例) - - [3. request——每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效](#3-request——每一次http请求都会产生一个新的bean,该bean仅在当前http-request内有效) - - [4. session——每一次HTTP请求都会产生一个新的 bean,该bean仅在当前 HTTP session 内有效](#4-session——每一次http请求都会产生一个新的-bean,该bean仅在当前-http-session-内有效) - - [5. globalSession](#5-globalsession) -- [二 bean的生命周期](#二-bean的生命周期) - - [initialization 和 destroy](#initialization-和-destroy) - - [实现*Aware接口 在Bean中使用Spring框架的一些对象](#实现aware接口-在bean中使用spring框架的一些对象) - - [BeanPostProcessor](#beanpostprocessor) - - [总结](#总结) - - [单例管理的对象](#单例管理的对象) - - [非单例管理的对象](#非单例管理的对象) -- [三 说明](#三-说明) - - - -# 前言 -在 Spring 中,那些组成应用程序的主体及由 Spring IOC 容器所管理的对象,被称之为 bean。简单地讲,bean 就是由 IOC 容器初始化、装配及管理的对象,除此之外,bean 就与应用程序中的其他对象没有什么区别了。而 bean 的定义以及 bean 相互间的依赖关系将通过配置元数据来描述。 - -**Spring中的bean默认都是单例的,这些单例Bean在多线程程序下如何保证线程安全呢?** 例如对于Web应用来说,Web容器对于每个用户请求都创建一个单独的Sevlet线程来处理请求,引入Spring框架之后,每个Action都是单例的,那么对于Spring托管的单例Service Bean,如何保证其安全呢? **Spring的单例是基于BeanFactory也就是Spring容器的,单例Bean在此容器内只有一个,Java的单例是基于 JVM,每个 JVM 内只有一个实例。** - -在大多数情况下。单例 bean 是很理想的方案。不过,有时候你可能会发现你所使用的类是易变的,它们会保持一些状态,因此重用是不安全的。在这种情况下,将 class 声明为单例的就不是那么明智了。因为对象会被污染,稍后重用的时候会出现意想不到的问题。所以 Spring 定义了多种作用域的bean。 - -# 一 bean的作用域 - -创建一个bean定义,其实质是用该bean定义对应的类来创建真正实例的“配方”。把bean定义看成一个配方很有意义,它与class很类似,只根据一张“处方”就可以创建多个实例。不仅可以控制注入到对象中的各种依赖和配置值,还可以控制该对象的作用域。这样可以灵活选择所建对象的作用域,而不必在Java Class级定义作用域。Spring Framework支持五种作用域,分别阐述如下表。 - - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-17/1188352.jpg) - -五种作用域中,**request、session** 和 **global session** 三种作用域仅在基于web的应用中使用(不必关心你所采用的是什么web应用框架),只能用在基于 web 的 Spring ApplicationContext 环境。 - - - -### 1. singleton——唯一 bean 实例 - -**当一个 bean 的作用域为 singleton,那么Spring IoC容器中只会存在一个共享的 bean 实例,并且所有对 bean 的请求,只要 id 与该 bean 定义相匹配,则只会返回bean的同一实例。** singleton 是单例类型(对应于单例模式),就是在创建起容器时就同时自动创建了一个bean的对象,不管你是否使用,但我们可以指定Bean节点的 `lazy-init=”true”` 来延迟初始化bean,这时候,只有在第一次获取bean时才会初始化bean,即第一次请求该bean时才初始化。 每次获取到的对象都是同一个对象。注意,singleton 作用域是Spring中的缺省作用域。要在XML中将 bean 定义成 singleton ,可以这样配置: - -```xml - -``` - -也可以通过 `@Scope` 注解(它可以显示指定bean的作用范围。)的方式 - -```java -@Service -@Scope("singleton") -public class ServiceImpl{ - -} -``` - -### 2. prototype——每次请求都会创建一个新的 bean 实例 - -**当一个bean的作用域为 prototype,表示一个 bean 定义对应多个对象实例。** **prototype 作用域的 bean 会导致在每次对该 bean 请求**(将其注入到另一个 bean 中,或者以程序的方式调用容器的 getBean() 方法**)时都会创建一个新的 bean 实例。prototype 是原型类型,它在我们创建容器的时候并没有实例化,而是当我们获取bean的时候才会去创建一个对象,而且我们每次获取到的对象都不是同一个对象。根据经验,对有状态的 bean 应该使用 prototype 作用域,而对无状态的 bean 则应该使用 singleton 作用域。** 在 XML 中将 bean 定义成 prototype ,可以这样配置: - -```java - - 或者 - -``` -通过 `@Scope` 注解的方式实现就不做演示了。 - -### 3. request——每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效 - -**request只适用于Web程序,每一次 HTTP 请求都会产生一个新的bean,同时该bean仅在当前HTTP request内有效,当请求结束后,该对象的生命周期即告结束。** 在 XML 中将 bean 定义成 request ,可以这样配置: - -```java - -``` - -### 4. session——每一次HTTP请求都会产生一个新的 bean,该bean仅在当前 HTTP session 内有效 - -**session只适用于Web程序,session 作用域表示该针对每一次 HTTP 请求都会产生一个新的 bean,同时该 bean 仅在当前 HTTP session 内有效.与request作用域一样,可以根据需要放心的更改所创建实例的内部状态,而别的 HTTP session 中根据 userPreferences 创建的实例,将不会看到这些特定于某个 HTTP session 的状态变化。当HTTP session最终被废弃的时候,在该HTTP session作用域内的bean也会被废弃掉。** - -```xml - -``` - -### 5. globalSession - -global session 作用域类似于标准的 HTTP session 作用域,不过仅仅在基于 portlet 的 web 应用中才有意义。Portlet 规范定义了全局 Session 的概念,它被所有构成某个 portlet web 应用的各种不同的 portle t所共享。在global session 作用域中定义的 bean 被限定于全局portlet Session的生命周期范围内。 - -```xml - -``` - -# 二 bean的生命周期 - -Spring Bean是Spring应用中最最重要的部分了。所以来看看Spring容器在初始化一个bean的时候会做那些事情,顺序是怎样的,在容器关闭的时候,又会做哪些事情。 - -> spring版本:4.2.3.RELEASE -鉴于Spring源码是用gradle构建的,我也决定舍弃我大maven,尝试下洪菊推荐过的gradle。运行beanLifeCycle模块下的junit test即可在控制台看到如下输出,可以清楚了解Spring容器在创建,初始化和销毁Bean的时候依次做了那些事情。 - -``` -Spring容器初始化 -===================================== -调用GiraffeService无参构造函数 -GiraffeService中利用set方法设置属性值 -调用setBeanName:: Bean Name defined in context=giraffeService -调用setBeanClassLoader,ClassLoader Name = sun.misc.Launcher$AppClassLoader -调用setBeanFactory,setBeanFactory:: giraffe bean singleton=true -调用setEnvironment -调用setResourceLoader:: Resource File Name=spring-beans.xml -调用setApplicationEventPublisher -调用setApplicationContext:: Bean Definition Names=[giraffeService, org.springframework.context.annotation.CommonAnnotationBeanPostProcessor#0, com.giraffe.spring.service.GiraffeServicePostProcessor#0] -执行BeanPostProcessor的postProcessBeforeInitialization方法,beanName=giraffeService -调用PostConstruct注解标注的方法 -执行InitializingBean接口的afterPropertiesSet方法 -执行配置的init-method -执行BeanPostProcessor的postProcessAfterInitialization方法,beanName=giraffeService -Spring容器初始化完毕 -===================================== -从容器中获取Bean -giraffe Name=李光洙 -===================================== -调用preDestroy注解标注的方法 -执行DisposableBean接口的destroy方法 -执行配置的destroy-method -Spring容器关闭 -``` - -先来看看,Spring在Bean从创建到销毁的生命周期中可能做得事情。 - - -### initialization 和 destroy - -有时我们需要在Bean属性值set好之后和Bean销毁之前做一些事情,比如检查Bean中某个属性是否被正常的设置好值了。Spring框架提供了多种方法让我们可以在Spring Bean的生命周期中执行initialization和pre-destroy方法。 - -**1.实现InitializingBean和DisposableBean接口** - -这两个接口都只包含一个方法。通过实现InitializingBean接口的afterPropertiesSet()方法可以在Bean属性值设置好之后做一些操作,实现DisposableBean接口的destroy()方法可以在销毁Bean之前做一些操作。 - -例子如下: - -```java -public class GiraffeService implements InitializingBean,DisposableBean { - @Override - public void afterPropertiesSet() throws Exception { - System.out.println("执行InitializingBean接口的afterPropertiesSet方法"); - } - @Override - public void destroy() throws Exception { - System.out.println("执行DisposableBean接口的destroy方法"); - } -} -``` -这种方法比较简单,但是不建议使用。因为这样会将Bean的实现和Spring框架耦合在一起。 - -**2.在bean的配置文件中指定init-method和destroy-method方法** - -Spring允许我们创建自己的 init 方法和 destroy 方法,只要在 Bean 的配置文件中指定 init-method 和 destroy-method 的值就可以在 Bean 初始化时和销毁之前执行一些操作。 - -例子如下: - -```java -public class GiraffeService { - //通过的destroy-method属性指定的销毁方法 - public void destroyMethod() throws Exception { - System.out.println("执行配置的destroy-method"); - } - //通过的init-method属性指定的初始化方法 - public void initMethod() throws Exception { - System.out.println("执行配置的init-method"); - } -} -``` - -配置文件中的配置: - -``` - - -``` - -需要注意的是自定义的init-method和post-method方法可以抛异常但是不能有参数。 - -这种方式比较推荐,因为可以自己创建方法,无需将Bean的实现直接依赖于spring的框架。 - -**3.使用@PostConstruct和@PreDestroy注解** - -除了xml配置的方式,Spring 也支持用 `@PostConstruct`和 `@PreDestroy`注解来指定 `init` 和 `destroy` 方法。这两个注解均在`javax.annotation` 包中。为了注解可以生效,需要在配置文件中定义org.springframework.context.annotation.CommonAnnotationBeanPostProcessor或context:annotation-config - -例子如下: - -```java -public class GiraffeService { - @PostConstruct - public void initPostConstruct(){ - System.out.println("执行PostConstruct注解标注的方法"); - } - @PreDestroy - public void preDestroy(){ - System.out.println("执行preDestroy注解标注的方法"); - } -} -``` - -配置文件: - -```xml - - - -``` - -### 实现*Aware接口 在Bean中使用Spring框架的一些对象 - -有些时候我们需要在 Bean 的初始化中使用 Spring 框架自身的一些对象来执行一些操作,比如获取 ServletContext 的一些参数,获取 ApplicaitionContext 中的 BeanDefinition 的名字,获取 Bean 在容器中的名字等等。为了让 Bean 可以获取到框架自身的一些对象,Spring 提供了一组名为*Aware的接口。 - -这些接口均继承于`org.springframework.beans.factory.Aware`标记接口,并提供一个将由 Bean 实现的set*方法,Spring通过基于setter的依赖注入方式使相应的对象可以被Bean使用。 -网上说,这些接口是利用观察者模式实现的,类似于servlet listeners,目前还不明白,不过这也不在本文的讨论范围内。 -介绍一些重要的Aware接口: - -- **ApplicationContextAware**: 获得ApplicationContext对象,可以用来获取所有Bean definition的名字。 -- **BeanFactoryAware**:获得BeanFactory对象,可以用来检测Bean的作用域。 -- **BeanNameAware**:获得Bean在配置文件中定义的名字。 -- **ResourceLoaderAware**:获得ResourceLoader对象,可以获得classpath中某个文件。 -- **ServletContextAware**:在一个MVC应用中可以获取ServletContext对象,可以读取context中的参数。 -- **ServletConfigAware**: 在一个MVC应用中可以获取ServletConfig对象,可以读取config中的参数。 - -```java -public class GiraffeService implements ApplicationContextAware, - ApplicationEventPublisherAware, BeanClassLoaderAware, BeanFactoryAware, - BeanNameAware, EnvironmentAware, ImportAware, ResourceLoaderAware{ - @Override - public void setBeanClassLoader(ClassLoader classLoader) { - System.out.println("执行setBeanClassLoader,ClassLoader Name = " + classLoader.getClass().getName()); - } - @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - System.out.println("执行setBeanFactory,setBeanFactory:: giraffe bean singleton=" + beanFactory.isSingleton("giraffeService")); - } - @Override - public void setBeanName(String s) { - System.out.println("执行setBeanName:: Bean Name defined in context=" - + s); - } - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - System.out.println("执行setApplicationContext:: Bean Definition Names=" - + Arrays.toString(applicationContext.getBeanDefinitionNames())); - } - @Override - public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { - System.out.println("执行setApplicationEventPublisher"); - } - @Override - public void setEnvironment(Environment environment) { - System.out.println("执行setEnvironment"); - } - @Override - public void setResourceLoader(ResourceLoader resourceLoader) { - Resource resource = resourceLoader.getResource("classpath:spring-beans.xml"); - System.out.println("执行setResourceLoader:: Resource File Name=" - + resource.getFilename()); - } - @Override - public void setImportMetadata(AnnotationMetadata annotationMetadata) { - System.out.println("执行setImportMetadata"); - } -} -``` - -### BeanPostProcessor - -上面的*Aware接口是针对某个实现这些接口的Bean定制初始化的过程, -Spring同样可以针对容器中的所有Bean,或者某些Bean定制初始化过程,只需提供一个实现BeanPostProcessor接口的类即可。 该接口中包含两个方法,postProcessBeforeInitialization和postProcessAfterInitialization。 postProcessBeforeInitialization方法会在容器中的Bean初始化之前执行, postProcessAfterInitialization方法在容器中的Bean初始化之后执行。 - -例子如下: - -```java -public class CustomerBeanPostProcessor implements BeanPostProcessor { - @Override - public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { - System.out.println("执行BeanPostProcessor的postProcessBeforeInitialization方法,beanName=" + beanName); - return bean; - } - @Override - public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { - System.out.println("执行BeanPostProcessor的postProcessAfterInitialization方法,beanName=" + beanName); - return bean; - } -} -``` - -要将BeanPostProcessor的Bean像其他Bean一样定义在配置文件中 - -```xml - -``` - -### 总结 - -所以。。。结合第一节控制台输出的内容,Spring Bean的生命周期是这样纸的: - -- Bean容器找到配置文件中 Spring Bean 的定义。 -- Bean容器利用Java Reflection API创建一个Bean的实例。 -- 如果涉及到一些属性值 利用set方法设置一些属性值。 -- 如果Bean实现了BeanNameAware接口,调用setBeanName()方法,传入Bean的名字。 -- 如果Bean实现了BeanClassLoaderAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例。 -- 如果Bean实现了BeanFactoryAware接口,调用setBeanFactory()方法,传入BeanFactory对象的实例。 -- 与上面的类似,如果实现了其他*Aware接口,就调用相应的方法。 -- 如果有和加载这个Bean的Spring容器相关的BeanPostProcessor对象,执行postProcessBeforeInitialization()方法 -- 如果Bean实现了InitializingBean接口,执行afterPropertiesSet()方法。 -- 如果Bean在配置文件中的定义包含init-method属性,执行指定的方法。 -- 如果有和加载这个Bean的Spring容器相关的BeanPostProcessor对象,执行postProcessAfterInitialization()方法 -- 当要销毁Bean的时候,如果Bean实现了DisposableBean接口,执行destroy()方法。 -- 当要销毁Bean的时候,如果Bean在配置文件中的定义包含destroy-method属性,执行指定的方法。 - -用图表示一下(图来源:http://www.jianshu.com/p/d00539babca5): - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-17/48376272.jpg) - -与之比较类似的中文版本: - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-17/5496407.jpg) - - -**其实很多时候我们并不会真的去实现上面说描述的那些接口,那么下面我们就除去那些接口,针对bean的单例和非单例来描述下bean的生命周期:** - -### 单例管理的对象 - -当scope=”singleton”,即默认情况下,会在启动容器时(即实例化容器时)时实例化。但我们可以指定Bean节点的lazy-init=”true”来延迟初始化bean,这时候,只有在第一次获取bean时才会初始化bean,即第一次请求该bean时才初始化。如下配置: - -```xml - -``` - -如果想对所有的默认单例bean都应用延迟初始化,可以在根节点beans设置default-lazy-init属性为true,如下所示: - -```xml - -``` - -默认情况下,Spring 在读取 xml 文件的时候,就会创建对象。在创建对象的时候先调用构造器,然后调用 init-method 属性值中所指定的方法。对象在被销毁的时候,会调用 destroy-method 属性值中所指定的方法(例如调用Container.destroy()方法的时候)。写一个测试类,代码如下: - -```java -public class LifeBean { - private String name; - - public LifeBean(){ - System.out.println("LifeBean()构造函数"); - } - public String getName() { - return name; - } - - public void setName(String name) { - System.out.println("setName()"); - this.name = name; - } - - public void init(){ - System.out.println("this is init of lifeBean"); - } - - public void destory(){ - System.out.println("this is destory of lifeBean " + this); - } -} -``` - life.xml配置如下: - -```xml - -``` - -测试代码: - -```java -public class LifeTest { - @Test - public void test() { - AbstractApplicationContext container = - new ClassPathXmlApplicationContext("life.xml"); - LifeBean life1 = (LifeBean)container.getBean("life"); - System.out.println(life1); - container.close(); - } -} -``` - -运行结果: - -``` -LifeBean()构造函数 -this is init of lifeBean -com.bean.LifeBean@573f2bb1 -…… -this is destory of lifeBean com.bean.LifeBean@573f2bb1 -``` - -### 非单例管理的对象 - -当`scope=”prototype”`时,容器也会延迟初始化 bean,Spring 读取xml 文件的时候,并不会立刻创建对象,而是在第一次请求该 bean 时才初始化(如调用getBean方法时)。在第一次请求每一个 prototype 的bean 时,Spring容器都会调用其构造器创建这个对象,然后调用`init-method`属性值中所指定的方法。对象销毁的时候,Spring 容器不会帮我们调用任何方法,因为是非单例,这个类型的对象有很多个,Spring容器一旦把这个对象交给你之后,就不再管理这个对象了。 - -为了测试prototype bean的生命周期life.xml配置如下: - -```xml - -``` - -测试程序: - -```java -public class LifeTest { - @Test - public void test() { - AbstractApplicationContext container = new ClassPathXmlApplicationContext("life.xml"); - LifeBean life1 = (LifeBean)container.getBean("life_singleton"); - System.out.println(life1); - - LifeBean life3 = (LifeBean)container.getBean("life_prototype"); - System.out.println(life3); - container.close(); - } -} -``` - -运行结果: - -``` -LifeBean()构造函数 -this is init of lifeBean -com.bean.LifeBean@573f2bb1 -LifeBean()构造函数 -this is init of lifeBean -com.bean.LifeBean@5ae9a829 -…… -this is destory of lifeBean com.bean.LifeBean@573f2bb1 -``` - -可以发现,对于作用域为 prototype 的 bean ,其`destroy`方法并没有被调用。**如果 bean 的 scope 设为prototype时,当容器关闭时,`destroy` 方法不会被调用。对于 prototype 作用域的 bean,有一点非常重要,那就是 Spring不能对一个 prototype bean 的整个生命周期负责:容器在初始化、配置、装饰或者是装配完一个prototype实例后,将它交给客户端,随后就对该prototype实例不闻不问了。** 不管何种作用域,容器都会调用所有对象的初始化生命周期回调方法。但对prototype而言,任何配置好的析构生命周期回调方法都将不会被调用。**清除prototype作用域的对象并释放任何prototype bean所持有的昂贵资源,都是客户端代码的职责**(让Spring容器释放被prototype作用域bean占用资源的一种可行方式是,通过使用bean的后置处理器,该处理器持有要被清除的bean的引用)。谈及prototype作用域的bean时,在某些方面你可以将Spring容器的角色看作是Java new操作的替代者,任何迟于该时间点的生命周期事宜都得交由客户端来处理。 - -**Spring 容器可以管理 singleton 作用域下 bean 的生命周期,在此作用域下,Spring 能够精确地知道bean何时被创建,何时初始化完成,以及何时被销毁。而对于 prototype 作用域的bean,Spring只负责创建,当容器创建了 bean 的实例后,bean 的实例就交给了客户端的代码管理,Spring容器将不再跟踪其生命周期,并且不会管理那些被配置成prototype作用域的bean的生命周期。** - - -# 三 说明 - -本文的完成结合了下面两篇文章,并做了相应修改: - -- https://blog.csdn.net/fuzhongmin05/article/details/73389779 -- https://yemengying.com/2016/07/14/spring-bean-life-cycle/ - -由于本文非本人独立原创,所以未声明为原创!在此说明! diff --git a/docs/system-design/framework/spring/spring-annotations.md "b/docs/system-design/framework/spring/SpringBoot+Spring\345\270\270\347\224\250\346\263\250\350\247\243\346\200\273\347\273\223.md" similarity index 98% rename from docs/system-design/framework/spring/spring-annotations.md rename to "docs/system-design/framework/spring/SpringBoot+Spring\345\270\270\347\224\250\346\263\250\350\247\243\346\200\273\347\273\223.md" index c83619a3144..0cdfb26073f 100644 --- a/docs/system-design/framework/spring/spring-annotations.md +++ "b/docs/system-design/framework/spring/SpringBoot+Spring\345\270\270\347\224\250\346\263\250\350\247\243\346\200\273\347\273\223.md" @@ -7,10 +7,10 @@ - [1. `@SpringBootApplication`](#1-springbootapplication) - [2. Spring Bean 相关](#2-spring-bean-%e7%9b%b8%e5%85%b3) - [2.1. `@Autowired`](#21-autowired) - - [2.2. `Component`,`@Repository`,`@Service`, `@Controller`](#22-componentrepositoryservice-controller) + - [2.2. `@Component`,`@Repository`,`@Service`, `@Controller`](#22-componentrepositoryservice-controller) - [2.3. `@RestController`](#23-restcontroller) - [2.4. `@Scope`](#24-scope) - - [2.5. `Configuration`](#25-configuration) + - [2.5. `@Configuration`](#25-configuration) - [3. 处理常见的 HTTP 请求类型](#3-%e5%a4%84%e7%90%86%e5%b8%b8%e8%a7%81%e7%9a%84-http-%e8%af%b7%e6%b1%82%e7%b1%bb%e5%9e%8b) - [3.1. GET 请求](#31-get-%e8%af%b7%e6%b1%82) - [3.2. POST 请求](#32-post-%e8%af%b7%e6%b1%82) @@ -130,14 +130,14 @@ public class UserController { } ``` -#### 2.2. `Component`,`@Repository`,`@Service`, `@Controller` +#### 2.2. `@Component`,`@Repository`,`@Service`, `@Controller` 我们一般使用 `@Autowired` 注解让 Spring 容器帮我们自动装配 bean。要想把类标识成可用于 `@Autowired` 注解自动装配的 bean 的类,可以采用以下注解实现: - `@Component` :通用的注解,可标注任意类为 `Spring` 组件。如果一个 Bean 不知道属于哪个层,可以使用`@Component` 注解标注。 - `@Repository` : 对应持久层即 Dao 层,主要用于数据库相关操作。 - `@Service` : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。 -- `@Controller` : 对应 Spring MVC 控制层,主要用户接受用户请求并调用 Service 层返回数据给前端页面。 +- `@Controller` : 对应 Spring MVC 控制层,主要用于接受用户请求并调用 Service 层返回数据给前端页面。 #### 2.3. `@RestController` @@ -168,9 +168,9 @@ public Person personSingleton() { - request : 每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP request 内有效。 - session : 每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP session 内有效。 -#### 2.5. `Configuration` +#### 2.5. `@Configuration` -一般用来声明配置类,可以使用 `@Component`注解替代,不过使用`Configuration`注解声明配置类更加语义化。 +一般用来声明配置类,可以使用 `@Component`注解替代,不过使用`@Configuration`注解声明配置类更加语义化。 ```java @Configuration @@ -302,7 +302,6 @@ public class UserRegisterRequest { private String userName; @NotBlank private String password; - @FullName @NotBlank private String fullName; } @@ -316,7 +315,7 @@ public class UserRegisterRequest { 这样我们的后端就可以直接把 json 格式的数据映射到我们的 `UserRegisterRequest` 类上。 -![](https://cdn.jsdelivr.net/gh/javaguide-tech/blog-images/2020-08/663d1ec1-7ebc-41ab-8431-159dc1ec6589.png) +![](images/spring-annotations/@RequestBody.png) 👉 需要注意的是:**一个请求方法只可以有一个`@RequestBody`,但是可以有多个`@RequestParam`和`@PathVariable`**。 如果你的方法必须要用两个 `@RequestBody`来接受数据的话,大概率是你的数据库设计或者系统设计出问题了! diff --git a/docs/system-design/framework/spring/SpringMVC-Principle.md b/docs/system-design/framework/spring/SpringMVC-Principle.md deleted file mode 100644 index 0efcd3f9534..00000000000 --- a/docs/system-design/framework/spring/SpringMVC-Principle.md +++ /dev/null @@ -1,269 +0,0 @@ -> 本文整理自网络,原文出处暂不知,对原文做了较大的改动,在此说明! - -### 先来看一下什么是 MVC 模式 - -MVC 是一种设计模式. - -**MVC 的原理图如下:** - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-10-11/60679444.jpg) - - - -### SpringMVC 简单介绍 - -SpringMVC 框架是以请求为驱动,围绕 Servlet 设计,将请求发给控制器,然后通过模型对象,分派器来展示请求结果视图。其中核心类是 DispatcherServlet,它是一个 Servlet,顶层是实现的Servlet接口。 - -### SpringMVC 使用 - -需要在 web.xml 中配置 DispatcherServlet 。并且需要配置 Spring 监听器ContextLoaderListener - -```xml - - - org.springframework.web.context.ContextLoaderListener - - - - springmvc - org.springframework.web.servlet.DispatcherServlet - - - - contextConfigLocation - classpath:spring/springmvc-servlet.xml - - 1 - - - springmvc - / - - -``` - -### SpringMVC 工作原理(重要) - -**简单来说:** - -客户端发送请求-> 前端控制器 DispatcherServlet 接受客户端请求 -> 找到处理器映射 HandlerMapping 解析请求对应的 Handler-> HandlerAdapter 会根据 Handler 来调用真正的处理器开处理请求,并处理相应的业务逻辑 -> 处理器返回一个模型视图 ModelAndView -> 视图解析器进行解析 -> 返回一个视图对象->前端控制器 DispatcherServlet 渲染数据(Moder)->将得到视图对象返回给用户 - - - -**如下图所示:** -![SpringMVC运行原理](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-10-11/49790288.jpg) - -上图的一个笔误的小问题:Spring MVC 的入口函数也就是前端控制器 DispatcherServlet 的作用是接收请求,响应结果。 - -**流程说明(重要):** - -(1)客户端(浏览器)发送请求,直接请求到 DispatcherServlet。 - -(2)DispatcherServlet 根据请求信息调用 HandlerMapping,解析请求对应的 Handler。 - -(3)解析到对应的 Handler(也就是我们平常说的 Controller 控制器)后,开始由 HandlerAdapter 适配器处理。 - -(4)HandlerAdapter 会根据 Handler 来调用真正的处理器开处理请求,并处理相应的业务逻辑。 - -(5)处理器处理完业务后,会返回一个 ModelAndView 对象,Model 是返回的数据对象,View 是个逻辑上的 View。 - -(6)ViewResolver 会根据逻辑 View 查找实际的 View。 - -(7)DispaterServlet 把返回的 Model 传给 View(视图渲染)。 - -(8)把 View 返回给请求者(浏览器) - - - -### SpringMVC 重要组件说明 - - -**1、前端控制器DispatcherServlet(不需要工程师开发),由框架提供(重要)** - -作用:**Spring MVC 的入口函数。接收请求,响应结果,相当于转发器,中央处理器。有了 DispatcherServlet 减少了其它组件之间的耦合度。用户请求到达前端控制器,它就相当于mvc模式中的c,DispatcherServlet是整个流程控制的中心,由它调用其它组件处理用户的请求,DispatcherServlet的存在降低了组件之间的耦合性。** - -**2、处理器映射器HandlerMapping(不需要工程师开发),由框架提供** - -作用:根据请求的url查找Handler。HandlerMapping负责根据用户请求找到Handler即处理器(Controller),SpringMVC提供了不同的映射器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等。 - -**3、处理器适配器HandlerAdapter** - -作用:按照特定规则(HandlerAdapter要求的规则)去执行Handler -通过HandlerAdapter对处理器进行执行,这是适配器模式的应用,通过扩展适配器可以对更多类型的处理器进行执行。 - -**4、处理器Handler(需要工程师开发)** - -注意:编写Handler时按照HandlerAdapter的要求去做,这样适配器才可以去正确执行Handler -Handler 是继DispatcherServlet前端控制器的后端控制器,在DispatcherServlet的控制下Handler对具体的用户请求进行处理。 -由于Handler涉及到具体的用户业务请求,所以一般情况需要工程师根据业务需求开发Handler。 - -**5、视图解析器View resolver(不需要工程师开发),由框架提供** - -作用:进行视图解析,根据逻辑视图名解析成真正的视图(view) -View Resolver负责将处理结果生成View视图,View Resolver首先根据逻辑视图名解析成物理视图名即具体的页面地址,再生成View视图对象,最后对View进行渲染将处理结果通过页面展示给用户。 springmvc框架提供了很多的View视图类型,包括:jstlView、freemarkerView、pdfView等。 -一般情况下需要通过页面标签或页面模版技术将模型数据通过页面展示给用户,需要由工程师根据业务需求开发具体的页面。 - -**6、视图View(需要工程师开发)** - -View是一个接口,实现类支持不同的View类型(jsp、freemarker、pdf...) - -**注意:处理器Handler(也就是我们平常说的Controller控制器)以及视图层view都是需要我们自己手动开发的。其他的一些组件比如:前端控制器DispatcherServlet、处理器映射器HandlerMapping、处理器适配器HandlerAdapter等等都是框架提供给我们的,不需要自己手动开发。** - -### DispatcherServlet详细解析 - -首先看下源码: - -```java -package org.springframework.web.servlet; - -@SuppressWarnings("serial") -public class DispatcherServlet extends FrameworkServlet { - - public static final String MULTIPART_RESOLVER_BEAN_NAME = "multipartResolver"; - public static final String LOCALE_RESOLVER_BEAN_NAME = "localeResolver"; - public static final String THEME_RESOLVER_BEAN_NAME = "themeResolver"; - public static final String HANDLER_MAPPING_BEAN_NAME = "handlerMapping"; - public static final String HANDLER_ADAPTER_BEAN_NAME = "handlerAdapter"; - public static final String HANDLER_EXCEPTION_RESOLVER_BEAN_NAME = "handlerExceptionResolver"; - public static final String REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME = "viewNameTranslator"; - public static final String VIEW_RESOLVER_BEAN_NAME = "viewResolver"; - public static final String FLASH_MAP_MANAGER_BEAN_NAME = "flashMapManager"; - public static final String WEB_APPLICATION_CONTEXT_ATTRIBUTE = DispatcherServlet.class.getName() + ".CONTEXT"; - public static final String LOCALE_RESOLVER_ATTRIBUTE = DispatcherServlet.class.getName() + ".LOCALE_RESOLVER"; - public static final String THEME_RESOLVER_ATTRIBUTE = DispatcherServlet.class.getName() + ".THEME_RESOLVER"; - public static final String THEME_SOURCE_ATTRIBUTE = DispatcherServlet.class.getName() + ".THEME_SOURCE"; - public static final String INPUT_FLASH_MAP_ATTRIBUTE = DispatcherServlet.class.getName() + ".INPUT_FLASH_MAP"; - public static final String OUTPUT_FLASH_MAP_ATTRIBUTE = DispatcherServlet.class.getName() + ".OUTPUT_FLASH_MAP"; - public static final String FLASH_MAP_MANAGER_ATTRIBUTE = DispatcherServlet.class.getName() + ".FLASH_MAP_MANAGER"; - public static final String EXCEPTION_ATTRIBUTE = DispatcherServlet.class.getName() + ".EXCEPTION"; - public static final String PAGE_NOT_FOUND_LOG_CATEGORY = "org.springframework.web.servlet.PageNotFound"; - private static final String DEFAULT_STRATEGIES_PATH = "DispatcherServlet.properties"; - protected static final Log pageNotFoundLogger = LogFactory.getLog(PAGE_NOT_FOUND_LOG_CATEGORY); - private static final Properties defaultStrategies; - static { - try { - ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class); - defaultStrategies = PropertiesLoaderUtils.loadProperties(resource); - } - catch (IOException ex) { - throw new IllegalStateException("Could not load 'DispatcherServlet.properties': " + ex.getMessage()); - } - } - - /** Detect all HandlerMappings or just expect "handlerMapping" bean? */ - private boolean detectAllHandlerMappings = true; - - /** Detect all HandlerAdapters or just expect "handlerAdapter" bean? */ - private boolean detectAllHandlerAdapters = true; - - /** Detect all HandlerExceptionResolvers or just expect "handlerExceptionResolver" bean? */ - private boolean detectAllHandlerExceptionResolvers = true; - - /** Detect all ViewResolvers or just expect "viewResolver" bean? */ - private boolean detectAllViewResolvers = true; - - /** Throw a NoHandlerFoundException if no Handler was found to process this request? **/ - private boolean throwExceptionIfNoHandlerFound = false; - - /** Perform cleanup of request attributes after include request? */ - private boolean cleanupAfterInclude = true; - - /** MultipartResolver used by this servlet */ - private MultipartResolver multipartResolver; - - /** LocaleResolver used by this servlet */ - private LocaleResolver localeResolver; - - /** ThemeResolver used by this servlet */ - private ThemeResolver themeResolver; - - /** List of HandlerMappings used by this servlet */ - private List handlerMappings; - - /** List of HandlerAdapters used by this servlet */ - private List handlerAdapters; - - /** List of HandlerExceptionResolvers used by this servlet */ - private List handlerExceptionResolvers; - - /** RequestToViewNameTranslator used by this servlet */ - private RequestToViewNameTranslator viewNameTranslator; - - private FlashMapManager flashMapManager; - - /** List of ViewResolvers used by this servlet */ - private List viewResolvers; - - public DispatcherServlet() { - super(); - } - - public DispatcherServlet(WebApplicationContext webApplicationContext) { - super(webApplicationContext); - } - @Override - protected void onRefresh(ApplicationContext context) { - initStrategies(context); - } - - protected void initStrategies(ApplicationContext context) { - initMultipartResolver(context); - initLocaleResolver(context); - initThemeResolver(context); - initHandlerMappings(context); - initHandlerAdapters(context); - initHandlerExceptionResolvers(context); - initRequestToViewNameTranslator(context); - initViewResolvers(context); - initFlashMapManager(context); - } -} - -``` - -DispatcherServlet类中的属性beans: - -- HandlerMapping:用于handlers映射请求和一系列的对于拦截器的前处理和后处理,大部分用@Controller注解。 -- HandlerAdapter:帮助DispatcherServlet处理映射请求处理程序的适配器,而不用考虑实际调用的是 哪个处理程序。- - - -- ViewResolver:根据实际配置解析实际的View类型。 -- ThemeResolver:解决Web应用程序可以使用的主题,例如提供个性化布局。 -- MultipartResolver:解析多部分请求,以支持从HTML表单上传文件。- -- FlashMapManager:存储并检索可用于将一个请求属性传递到另一个请求的input和output的FlashMap,通常用于重定向。 - -在Web MVC框架中,每个DispatcherServlet都拥自己的WebApplicationContext,它继承了ApplicationContext。WebApplicationContext包含了其上下文和Servlet实例之间共享的所有的基础框架beans。 - -**HandlerMapping** - -![HandlerMapping](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-10-11/96666164.jpg) - -HandlerMapping接口处理请求的映射HandlerMapping接口的实现类: - -- SimpleUrlHandlerMapping类通过配置文件把URL映射到Controller类。 -- DefaultAnnotationHandlerMapping类通过注解把URL映射到Controller类。 - -**HandlerAdapter** - - -![HandlerAdapter](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-10-11/91433100.jpg) - -HandlerAdapter接口-处理请求映射 - -AnnotationMethodHandlerAdapter:通过注解,把请求URL映射到Controller类的方法上。 - -**HandlerExceptionResolver** - - -![HandlerExceptionResolver](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-10-11/50343885.jpg) - -HandlerExceptionResolver接口-异常处理接口 - -- SimpleMappingExceptionResolver通过配置文件进行异常处理。 -- AnnotationMethodHandlerExceptionResolver:通过注解进行异常处理。 - -**ViewResolver** - -![ViewResolver](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-10-11/49497279.jpg) - -ViewResolver接口解析View视图。 - -UrlBasedViewResolver类 通过配置文件,把一个视图名交给到一个View来处理。 diff --git a/docs/system-design/framework/spring/spring-transaction.md "b/docs/system-design/framework/spring/Spring\344\272\213\345\212\241\346\200\273\347\273\223.md" similarity index 96% rename from docs/system-design/framework/spring/spring-transaction.md rename to "docs/system-design/framework/spring/Spring\344\272\213\345\212\241\346\200\273\347\273\223.md" index 8606166d34f..0bc45c8d63b 100644 --- a/docs/system-design/framework/spring/spring-transaction.md +++ "b/docs/system-design/framework/spring/Spring\344\272\213\345\212\241\346\200\273\347\273\223.md" @@ -51,14 +51,16 @@ public class OrdersService { 另外,数据库事务的 ACID 四大特性是事务的基础,下面简单来了解一下。 -## 2. 事物的特性(ACID)了解么? +## 2. 事务的特性(ACID)了解么? ![](images/spring-transaction/bda7231b-ab05-4e23-95ee-89ac90ac7fcf.png) -- **原子性:** 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用; -- **一致性:** 执行事务前后,数据保持一致; -- **隔离性:** 并发访问数据库时,一个用户的事物不被其他事务所干扰也就是说多个事务并发执行时,一个事务的执行不应影响其他事务的执行; -- **持久性:** 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。 +- **原子性(Atomicity):** 一个事务(transaction)中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。即,事务不可分割、不可约简。 +- **一致性(Consistency):** 在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设约束、触发器、级联回滚等。 +- **隔离性(Isolation):** 数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括未提交读(Read uncommitted)、提交读(read committed)、可重复读(repeatable read)和串行化(Serializable)。 +- **持久性(Durability):** 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。 + +[参考]https://zh.wikipedia.org/wiki/ACID ## 3. 详谈 Spring 对事务的支持 @@ -141,7 +143,7 @@ Spring 框架中,事务管理相关最重要的 3 个接口如下: - **`TransactionDefinition`**: 事务定义信息(事务隔离级别、传播行为、超时、只读、回滚规则)。 - **`TransactionStatus`**: 事务运行状态。 -我们可以把 **`PlatformTransactionManager`** 接口可以被看作是事务上层的管理者,而 **`TransactionDefinition`** 和 **`TransactionStatus`** 这两个接口可以看作是事物的描述。 +我们可以把 **`PlatformTransactionManager`** 接口可以被看作是事务上层的管理者,而 **`TransactionDefinition`** 和 **`TransactionStatus`** 这两个接口可以看作是事务的描述。 **`PlatformTransactionManager`** 会根据 **`TransactionDefinition`** 的定义比如事务超时时间、隔离级别、传播行为等来进行事务管理 ,而 **`TransactionStatus`** 接口则提供了一些方法来获取事务相应的状态比如是否新事务、是否可以回滚等等。 @@ -177,7 +179,7 @@ public interface PlatformTransactionManager { 主要是因为要将事务管理行为抽象出来,然后不同的平台去实现它,这样我们可以保证提供给外部的行为不变,方便我们扩展。我前段时间分享过:**“为什么我们要用接口?”** - +![](images/spring-transaction/接口使用原因.png) #### 3.2.2. TransactionDefinition:事务属性 @@ -236,7 +238,7 @@ public interface TransactionDefinition { ```java public interface TransactionStatus{ - boolean isNewTransaction(); // 是否是新的事物 + boolean isNewTransaction(); // 是否是新的事务 boolean hasSavepoint(); // 是否有恢复点 void setRollbackOnly(); // 设置为只回滚 boolean isRollbackOnly(); // 是否为只回滚 diff --git a/docs/system-design/framework/spring/SpringInterviewQuestions.md "b/docs/system-design/framework/spring/Spring\345\270\270\350\247\201\351\227\256\351\242\230\346\200\273\347\273\223.md" similarity index 98% rename from docs/system-design/framework/spring/SpringInterviewQuestions.md rename to "docs/system-design/framework/spring/Spring\345\270\270\350\247\201\351\227\256\351\242\230\346\200\273\347\273\223.md" index 77fdd374d1b..fb265f99ee5 100644 --- a/docs/system-design/framework/spring/SpringInterviewQuestions.md +++ "b/docs/system-design/framework/spring/Spring\345\270\270\350\247\201\351\227\256\351\242\230\346\200\273\347\273\223.md" @@ -54,7 +54,7 @@ Spring 官网列出的 Spring 的 6 个特征: Reference: -- https://dzone.com/articles/spring-framework-restcontroller-vs-controller(图片来源) +- https://dzone.com/articles/spring-framework-restcontroller-vs-controller (图片来源) - https://javarevisited.blogspot.com/2017/08/difference-between-restcontroller-and-controller-annotations-spring-mvc-rest.html?m=1 ## 4. Spring IOC & AOP @@ -170,7 +170,7 @@ public OneService getService(status) { - `@Component` :通用的注解,可标注任意类为 `Spring` 组件。如果一个Bean不知道属于哪个层,可以使用`@Component` 注解标注。 - `@Repository` : 对应持久层即 Dao 层,主要用于数据库相关操作。 - `@Service` : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao层。 -- `@Controller` : 对应 Spring MVC 控制层,主要用户接受用户请求并调用 Service 层返回数据给前端页面。 +- `@Controller` : 对应 Spring MVC 控制层,主要用于接受用户请求并调用 Service 层返回数据给前端页面。 ### 5.5 Spring 中的 bean 生命周期? @@ -203,7 +203,7 @@ public OneService getService(status) { 谈到这个问题,我们不得不提提之前 Model1 和 Model2 这两个没有 Spring MVC 的时代。 -- **Model1 时代** : 很多学 Java 后端比较晚的朋友可能并没有接触过 Model1 模式下的 JavaWeb 应用开发。在 Model1 模式下,整个 Web 应用几乎全部用 JSP 页面组成,只用少量的 JavaBean 来处理数据库连接、访问等操作。这个模式下 JSP 即是控制层又是表现层。显而易见,这种模式存在很多问题。比如①将控制逻辑和表现逻辑混杂在一起,导致代码重用率极低;②前端和后端相互依赖,难以进行测试并且开发效率极低; +- **Model1 时代** : 很多学 Java 后端比较晚的朋友可能并没有接触过 Model1 模式下的 JavaWeb 应用开发。在 Model1 模式下,整个 Web 应用几乎全部用 JSP 页面组成,只用少量的 JavaBean 来处理数据库连接、访问等操作。这个模式下 JSP 既是控制层又是表现层。显而易见,这种模式存在很多问题。比如①将控制逻辑和表现逻辑混杂在一起,导致代码重用率极低;②前端和后端相互依赖,难以进行测试并且开发效率极低; - **Model2 时代** :学过 Servlet 并做过相关 Demo 的朋友应该了解“Java Bean(Model)+ JSP(View,)+Servlet(Controller) ”这种开发模式,这就是早期的 JavaWeb MVC 开发模式。Model:系统涉及的数据,也就是 dao 和 bean。View:展示模型中的数据,只是用来展示。Controller:处理用户请求都发送给 ,返回数据给 JSP 并展示给用户。 Model2 模式下还存在很多问题,Model2的抽象和封装程度还远远不够,使用Model2进行开发时不可避免地会重复造轮子,这就大大降低了程序的可维护性和复用性。于是很多JavaWeb开发相关的 MVC 框架应运而生比如Struts2,但是 Struts2 比较笨重。随着 Spring 轻量级开发框架的流行,Spring 生态圈出现了 Spring MVC 框架, Spring MVC 是当前最优秀的 MVC 框架。相比于 Struts2 , Spring MVC 使用更加简单和方便,开发效率更高,并且 Spring MVC 运行速度更快。 @@ -226,7 +226,7 @@ MVC 是一种设计模式,Spring MVC 是一款很优秀的 MVC 框架。Spring M 1. 客户端(浏览器)发送请求,直接请求到 `DispatcherServlet`。 2. `DispatcherServlet` 根据请求信息调用 `HandlerMapping`,解析请求对应的 `Handler`。 3. 解析到对应的 `Handler`(也就是我们平常说的 `Controller` 控制器)后,开始由 `HandlerAdapter` 适配器处理。 -4. `HandlerAdapter` 会根据 `Handler `来调用真正的处理器开处理请求,并处理相应的业务逻辑。 +4. `HandlerAdapter` 会根据 `Handler `来调用真正的处理器来处理请求,并处理相应的业务逻辑。 5. 处理器处理完业务后,会返回一个 `ModelAndView` 对象,`Model` 是返回的数据对象,`View` 是个逻辑上的 `View`。 6. `ViewResolver` 会根据逻辑 `View` 查找实际的 `View`。 7. `DispaterServlet` 把返回的 `Model` 传给 `View`(视图渲染)。 @@ -291,7 +291,7 @@ MVC 是一种设计模式,Spring MVC 是一款很优秀的 MVC 框架。Spring M 当`@Transactional`注解作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。如果类或者方法加了这个注解,那么这个类里面的方法抛出异常,就会回滚,数据库里面的数据也会回滚。 -在`@Transactional`注解中如果不配置`rollbackFor`属性,那么事物只会在遇到`RuntimeException`的时候才会回滚,加上`rollbackFor=Exception.class`,可以让事物在遇到非运行时异常时也回滚。 +在`@Transactional`注解中如果不配置`rollbackFor`属性,那么事务只会在遇到`RuntimeException`的时候才会回滚,加上`rollbackFor=Exception.class`,可以让事务在遇到非运行时异常时也回滚。 关于 `@Transactional ` 注解推荐阅读的文章: diff --git a/docs/system-design/framework/spring/images/spring-annotations/@RequestBody.png b/docs/system-design/framework/spring/images/spring-annotations/@RequestBody.png new file mode 100644 index 00000000000..75d43a72be2 Binary files /dev/null and b/docs/system-design/framework/spring/images/spring-annotations/@RequestBody.png differ diff --git a/docs/system-design/framework/spring/images/spring-transaction/ed279f05-f5ad-443e-84e9-513a9e777139.png b/docs/system-design/framework/spring/images/spring-transaction/ed279f05-f5ad-443e-84e9-513a9e777139.png deleted file mode 100644 index d868e0515cb..00000000000 Binary files a/docs/system-design/framework/spring/images/spring-transaction/ed279f05-f5ad-443e-84e9-513a9e777139.png and /dev/null differ diff --git "a/docs/system-design/framework/spring/images/spring-transaction/\346\216\245\345\217\243\344\275\277\347\224\250\345\216\237\345\233\240.png" "b/docs/system-design/framework/spring/images/spring-transaction/\346\216\245\345\217\243\344\275\277\347\224\250\345\216\237\345\233\240.png" new file mode 100644 index 00000000000..eb7e6748411 Binary files /dev/null and "b/docs/system-design/framework/spring/images/spring-transaction/\346\216\245\345\217\243\344\275\277\347\224\250\345\216\237\345\233\240.png" differ diff --git a/docs/system-design/high-availability/images/cap-base/partition-tolerance.drawio b/docs/system-design/high-availability/images/cap-base/partition-tolerance.drawio new file mode 100644 index 00000000000..34112251a8d --- /dev/null +++ b/docs/system-design/high-availability/images/cap-base/partition-tolerance.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/docs/system-design/high-availability/images/cap-base/partition-tolerance.png b/docs/system-design/high-availability/images/cap-base/partition-tolerance.png new file mode 100644 index 00000000000..d523025fdc6 Binary files /dev/null and b/docs/system-design/high-availability/images/cap-base/partition-tolerance.png differ diff --git a/docs/system-design/micro-service/limit-request.md b/docs/system-design/high-availability/limit-request.md similarity index 66% rename from docs/system-design/micro-service/limit-request.md rename to docs/system-design/high-availability/limit-request.md index 3d42c29a146..e7e64accc04 100644 --- a/docs/system-design/micro-service/limit-request.md +++ b/docs/system-design/high-availability/limit-request.md @@ -6,7 +6,7 @@ #### 固定窗口计数器算法 -规定我们单位时间处理的请求数量。比如我们规定我们的一个接口一分钟只能访问10次的话。使用固定窗口计数器算法的话可以这样实现:给定一个变量counter来记录处理的请求数量,当1分钟之内处理一个请求之后counter+1,1分钟之内的如果counter=100的话,后续的请求就会被全部拒绝。等到 1分钟结束后,将counter回归成0,重新开始计数(ps:只要过了一个周期就讲counter回归成0)。 +该算法规定我们单位时间处理的请求数量。比如我们规定我们的一个接口一分钟只能访问10次的话。使用固定窗口计数器算法的话可以这样实现:给定一个变量counter来记录处理的请求数量,当1分钟之内处理一个请求之后counter+1,1分钟之内的如果counter=100的话,后续的请求就会被全部拒绝。等到 1分钟结束后,将counter回归成0,重新开始计数(ps:只要过了一个周期就讲counter回归成0)。 这种限流算法无法保证限流速率,因而无法保证突然激增的流量。比如我们限制一个接口一分钟只能访问10次的话,前半分钟一个请求没有接收,后半分钟接收了10个请求。 @@ -14,7 +14,7 @@ #### 滑动窗口计数器算法 -算的上是固定窗口计数器算法的升级版。滑动窗口计数器算法相比于固定窗口计数器算法的优化在于:它把时间以一定比例分片。例如我们的借口限流每分钟处理60个请求,我们可以把 1 分钟分为60个窗口。每隔1秒移动一次,每个窗口一秒只能处理 不大于 60(请求数)/60(窗口数) 的请求, 如果当前窗口的请求计数总和超过了限制的数量的话就不再处理其他请求。 +该算法算的上是固定窗口计数器算法的升级版。滑动窗口计数器算法相比于固定窗口计数器算法的优化在于:它把时间以一定比例分片。例如我们的接口限流每分钟处理60个请求,我们可以把 1 分钟分为60个窗口。每隔1秒移动一次,每个窗口一秒只能处理 不大于 60(请求数)/60(窗口数) 的请求, 如果当前窗口的请求计数总和超过了限制的数量的话就不再处理其他请求。 很显然:当滑动窗口的格子划分的越多,滑动窗口的滚动就越平滑,限流的统计就会越精确。 @@ -32,4 +32,4 @@ ![令牌桶算法](https://static001.infoq.cn/resource/image/ec/93/eca0e5eaa35dac938c673fecf2ec9a93.png) -### \ No newline at end of file +### diff --git "a/docs/system-design/website-architecture/\345\246\202\344\275\225\350\256\276\350\256\241\344\270\200\344\270\252\351\253\230\345\217\257\347\224\250\347\263\273\347\273\237\357\274\237\350\246\201\350\200\203\350\231\221\345\223\252\344\272\233\345\234\260\346\226\271\357\274\237.md" "b/docs/system-design/high-availability/\345\246\202\344\275\225\350\256\276\350\256\241\344\270\200\344\270\252\351\253\230\345\217\257\347\224\250\347\263\273\347\273\237\350\246\201\350\200\203\350\231\221\345\223\252\344\272\233\345\234\260\346\226\271.md" similarity index 100% rename from "docs/system-design/website-architecture/\345\246\202\344\275\225\350\256\276\350\256\241\344\270\200\344\270\252\351\253\230\345\217\257\347\224\250\347\263\273\347\273\237\357\274\237\350\246\201\350\200\203\350\231\221\345\223\252\344\272\233\345\234\260\346\226\271\357\274\237.md" rename to "docs/system-design/high-availability/\345\246\202\344\275\225\350\256\276\350\256\241\344\270\200\344\270\252\351\253\230\345\217\257\347\224\250\347\263\273\347\273\237\350\246\201\350\200\203\350\231\221\345\223\252\344\272\233\345\234\260\346\226\271.md" diff --git "a/docs/system-design/micro-service/\345\210\206\345\270\203\345\274\217id\347\224\237\346\210\220\346\226\271\346\241\210\346\200\273\347\273\223.md" "b/docs/system-design/micro-service/\345\210\206\345\270\203\345\274\217id\347\224\237\346\210\220\346\226\271\346\241\210\346\200\273\347\273\223.md" index bf27804785a..48f962d6c3c 100644 --- "a/docs/system-design/micro-service/\345\210\206\345\270\203\345\274\217id\347\224\237\346\210\220\346\226\271\346\241\210\346\200\273\347\273\223.md" +++ "b/docs/system-design/micro-service/\345\210\206\345\270\203\345\274\217id\347\224\237\346\210\220\346\226\271\346\241\210\346\200\273\347\273\223.md" @@ -8,8 +8,6 @@ ID是数据的唯一标识,传统的做法是利用UUID和数据库的自增ID 这篇文章并不会分析的特别详细,主要是做一些总结,以后再出一些详细某个方案的文章。 - - ## 数据库自增ID 第一种方案仍然还是基于数据库的自增ID,需要单独使用一个数据库实例,在这个实例中新建一个单独的表: @@ -101,7 +99,7 @@ update id_generator set current_max_id=#{newMaxId}, version=version+1 where vers 因为newMaxId是DistributIdService中根据oldMaxId+步长算出来的,只要上面的update更新成功了就表示号段获取成功了。 -为了提供数据库层的高可用,需要对数据库使用多主模式进行部署,对于每个数据库来说要保证生成的号段不重复,这就需要利用最开始的思路,再在刚刚的数据库表中增加起始值和步长,比如如果现在是两台Mysql,那么 mysql1将生成号段(1,1001],自增的时候序列为1,3,4,5,7.... mysql1将生成号段(2,1002],自增的时候序列为2,4,6,8,10... +为了提供数据库层的高可用,需要对数据库使用多主模式进行部署,对于每个数据库来说要保证生成的号段不重复,这就需要利用最开始的思路,再在刚刚的数据库表中增加起始值和步长,比如如果现在是两台Mysql,那么 mysql1将生成号段(1,1001],自增的时候序列为1,3,5,7.... mysql1将生成号段(2,1002],自增的时候序列为2,4,6,8,10... 更详细的可以参考滴滴开源的TinyId:[github.com/didi/tinyid…](https://github.com/didi/tinyid/wiki/tinyid原理介绍) @@ -183,4 +181,4 @@ AOF持久化相当于对每条写命令进行持久化,如果Redis挂掉了, **Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) \ No newline at end of file +![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) diff --git a/docs/system-design/naming.md b/docs/system-design/naming.md index aa4aee86571..1c16523c08e 100644 --- a/docs/system-design/naming.md +++ b/docs/system-design/naming.md @@ -209,5 +209,6 @@ Guide 制作了一个涵盖上面所有重要内容的思维导图,便于小 1. 《阿里巴巴 Java 开发手册》 2. 《Clean Code》 3. Google Java 代码指南:https://google.github.io/styleguide/javaguide.html#s5.1-identifier-name +4. 告别编码5分钟,命名2小时!史上最全的Java命名规范参考:https://www.cnblogs.com/liqiangchn/p/12000361.html diff --git a/docs/system-design/pictures/session-cookie-intro.png b/docs/system-design/pictures/session-cookie-intro.png deleted file mode 100644 index b4ef47acbd3..00000000000 Binary files a/docs/system-design/pictures/session-cookie-intro.png and /dev/null differ diff --git "a/docs/system-design/website-architecture/\345\210\206\345\270\203\345\274\217.md" "b/docs/system-design/website-architecture/\345\210\206\345\270\203\345\274\217.md" deleted file mode 100644 index 52a9d9f6665..00000000000 --- "a/docs/system-design/website-architecture/\345\210\206\345\270\203\345\274\217.md" +++ /dev/null @@ -1,39 +0,0 @@ -### 一 分布式系统的经典基础理论 - -[分布式系统的经典基础理论](https://blog.csdn.net/qq_34337272/article/details/80444032) -本文主要是简单的介绍了三个常见的概念: **分布式系统设计理念** 、 **CAP定理** 、 **BASE理论** ,关于分布式系统的还有很多很多东西。 - -![分布式系统的经典基础理论总结](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/1639234237ec9805.png) - -### 二 分布式事务 - -分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。以上是百度百科的解释,简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。 -- [深入理解分布式事务](http://www.codeceo.com/article/distributed-transaction.html) -- [聊聊分布式事务,再说说解决方案](https://www.cnblogs.com/savorboard/p/distributed-system-transaction-consistency.html) - - -### 三 分布式系统一致性 - -[分布式服务化系统一致性的“最佳实干”](https://www.jianshu.com/p/1156151e20c8) - -### 四 一致性协议/算法 - -早在1900年就诞生了著名的 **Paxos经典算法** (**Zookeeper就采用了Paxos算法的近亲兄弟Zab算法**),但由于Paxos算法非常难以理解、实现、排错。所以不断有人尝试简化这一算法,直到2013年才有了重大突破:斯坦福的Diego Ongaro、John Ousterhout以易懂性为目标设计了新的一致性算法—— **Raft算法** ,并发布了对应的论文《In Search of an Understandable Consensus Algorithm》,到现在有十多种语言实现的Raft算法框架,较为出名的有以Go语言实现的Etcd,它的功能类似于Zookeeper,但采用了更为主流的Rest接口。 - -* [图解 Paxos 一致性协议](https://mp.weixin.qq.com/s?__biz=MzI0NDI0MTgyOA==&mid=2652037784&idx=1&sn=d8c4f31a9cfb49ee91d05bb374e5cdd5&chksm=f2868653c5f10f45fc4a64d15a5f4163c3e66c00ed2ad334fa93edb46671f42db6752001f6c0#rd) -* [图解分布式协议-RAFT](http://ifeve.com/raft/) -* [Zookeeper ZAB 协议分析](https://dbaplus.cn/news-141-1875-1.html) - -### 五 分布式存储 - -**分布式存储系统将数据分散存储在多台独立的设备上**。传统的网络存储系统采用集中的存储服务器存放所有数据,存储服务器成为系统性能的瓶颈,也是可靠性和安全性的焦点,不能满足大规模存储应用的需要。分布式网络存储系统采用可扩展的系统结构,利用多台存储服务器分担存储负荷,利用位置服务器定位存储信息,它不但提高了系统的可靠性、可用性和存取效率,还易于扩展。 - -* [分布式存储系统概要](http://witchiman.top/2017/05/05/distributed-system/) - -### 六 分布式计算 - -**所谓分布式计算是一门计算机科学,它研究如何把一个需要非常巨大的计算能力才能解决的问题分成许多小的部分,然后把这些部分分配给许多计算机进行处理,最后把这些计算结果综合起来得到最终的结果。** -分布式网络存储技术是将数据分散的存储于多台独立的机器设备上。分布式网络存储系统采用可扩展的系统结构,利用多台存储服务器分担存储负荷,利用位置服务器定位存储信息,不但解决了传统集中式存储系统中单存储服务器的瓶颈问题,还提高了系统的可靠性、可用性和扩展性。 - -* [关于分布式计算的一些概念](https://blog.csdn.net/qq_34337272/article/details/80549020) - diff --git "a/docs/system-design/\350\256\276\350\256\241\346\250\241\345\274\217.md" "b/docs/system-design/\350\256\276\350\256\241\346\250\241\345\274\217.md" deleted file mode 100644 index c7a86716d97..00000000000 --- "a/docs/system-design/\350\256\276\350\256\241\346\250\241\345\274\217.md" +++ /dev/null @@ -1,84 +0,0 @@ -# Java 设计模式 - -下面是自己学习设计模式的时候做的总结,有些是自己的原创文章,有些是网上写的比较好的文章,保存下来细细消化吧! - -**系列文章推荐:** - -## 创建型模式 - -### 创建型模式概述 - -- 创建型模式(Creational Pattern)对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离。为了使软件的结构更加清晰,外界对于这些对象只需要知道它们共同的接口,而不清楚其具体的实现细节,使整个系统的设计更加符合单一职责原则。 -- 创建型模式在创建什么(What),由谁创建(Who),何时创建(When)等方面都为软件设计者提供了尽可能大的灵活性。创建型模式隐藏了类的实例的创建细节,通过隐藏对象如何被创建和组合在一起达到使整个系统独立的目的。 - -![创建型模式](https://user-gold-cdn.xitu.io/2018/6/16/1640641afcb7559b?w=491&h=241&f=png&s=51443) - -### 常见创建型模式详解 - -- **单例模式:** [深入理解单例模式——只有一个实例](https://blog.csdn.net/qq_34337272/article/details/80455972) -- **工厂模式:** [深入理解工厂模式——由对象工厂生成对象](https://blog.csdn.net/qq_34337272/article/details/80472071) -- **建造者模式:** [深入理解建造者模式 ——组装复杂的实例](http://blog.csdn.net/qq_34337272/article/details/80540059) -- **原型模式:** [深入理解原型模式 ——通过复制生成实例](https://blog.csdn.net/qq_34337272/article/details/80706444) - -## 结构型模式 - -### 结构型模式概述 - -- **结构型模式(Structural Pattern):** 描述如何将类或者对象结合在一起形成更大的结构,就像搭积木,可以通过简单积木的组合形成复杂的、功能更为强大的结构 -![结构型模式(Structural Pattern)](https://user-gold-cdn.xitu.io/2018/6/16/164064d6b3c205e3?w=719&h=233&f=png&s=270293) -- **结构型模式可以分为类结构型模式和对象结构型模式:** - - 类结构型模式关心类的组合,由多个类可以组合成一个更大的系统,在类结构型模式中一般只存在继承关系和实现关系。 - - 对象结构型模式关心类与对象的组合,通过关联关系使得在一个类中定义另一个类的实例对象,然后通过该对象调用其方法。根据“合成复用原则”,在系统中尽量使用关联关系来替代继承关系,因此大部分结构型模式都是对象结构型模式。 - -![结构型模式](https://user-gold-cdn.xitu.io/2018/6/16/1640655459d766d2?w=378&h=266&f=png&s=59652) - -### 常见结构型模式详解 - -- **适配器模式:** - - [深入理解适配器模式——加个“适配器”以便于复用](https://segmentfault.com/a/1190000011856448) https://blog.csdn.net/carson_ho/article/details/54910430 - - [适配器模式原理及实例介绍-IBM](https://www.ibm.com/developerworks/cn/java/j-lo-adapter-pattern/index.html) -- **桥接模式:** [设计模式笔记16:桥接模式(Bridge Pattern)](https://blog.csdn.net/yangzl2008/article/details/7670996) -- **组合模式:** [大话设计模式—组合模式](https://blog.csdn.net/lmb55/article/details/51039781) -- **装饰模式:** [java模式—装饰者模式](https://www.cnblogs.com/chenxing818/p/4705919.html)、[Java设计模式-装饰者模式](https://blog.csdn.net/cauchyweierstrass/article/details/48240147) -- **外观模式:** [java设计模式之外观模式(门面模式)](https://www.cnblogs.com/lthIU/p/5860607.html) -- **享元模式:** [享元模式](http://www.jasongj.com/design_pattern/flyweight/) -- **代理模式:** - - [代理模式原理及实例讲解 (IBM出品,很不错)](https://www.ibm.com/developerworks/cn/java/j-lo-proxy-pattern/index.html) - - [轻松学,Java 中的代理模式及动态代理](https://blog.csdn.net/briblue/article/details/73928350) - - [Java代理模式及其应用](https://blog.csdn.net/justloveyou_/article/details/74203025) - - -## 行为型模式 - -### 行为型模式概述 - -- 行为型模式(Behavioral Pattern)是对在不同的对象之间划分责任和算法的抽象化。 -- 行为型模式不仅仅关注类和对象的结构,而且重点关注它们之间的相互作用。 -- 通过行为型模式,可以更加清晰地划分类与对象的职责,并研究系统在运行时实例对象之间的交互。在系统运行时,对象并不是孤立的,它们可以通过相互通信与协作完成某些复杂功能,一个对象在运行时也将影响到其他对象的运行。 - -**行为型模式分为类行为型模式和对象行为型模式两种:** - -- **类行为型模式:** 类的行为型模式使用继承关系在几个类之间分配行为,类行为型模式主要通过多态等方式来分配父类与子类的职责。 -- **对象行为型模式:** 对象的行为型模式则使用对象的聚合关联关系来分配行为,对象行为型模式主要是通过对象关联等方式来分配两个或多个类的职责。根据“合成复用原则”,系统中要尽量使用关联关系来取代继承关系,因此大部分行为型设计模式都属于对象行为型设计模式。 - -![行为型模式](https://user-gold-cdn.xitu.io/2018/6/28/164467dd92c6172c?w=453&h=269&f=png&s=63270) - -- **职责链模式:** -- [Java设计模式之责任链模式、职责链模式](https://blog.csdn.net/jason0539/article/details/45091639) -- [责任链模式实现的三种方式](https://www.cnblogs.com/lizo/p/7503862.html) -- **命令模式:** 在软件设计中,我们经常需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个,我们只需在程序运行时指定具体的请求接收者即可,此时,可以使用命令模式来进行设计,使得请求发送者与请求接收者消除彼此之间的耦合,让对象之间的调用关系更加灵活。命令模式可以对发送者和接收者完全解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求。这就是命令模式的模式动机。 -- **解释器模式:** -- **迭代器模式:** -- **中介者模式:** -- **备忘录模式:** -- **观察者模式:** -- **状态模式:** -- **策略模式:** - -策略模式作为设计原则中开闭原则最典型的体现,也是经常使用的。下面这篇博客介绍了策略模式一般的组成部分和概念,并用了一个小demo去说明了策略模式的应用。 - -[java设计模式之策略模式](https://blog.csdn.net/zlj1217/article/details/81230077) - -- **模板方法模式:** -- **访问者模式:** - diff --git "a/docs/tools/Github\346\212\200\345\267\247.md" "b/docs/tools/Github\346\212\200\345\267\247.md" new file mode 100644 index 00000000000..deedc680e6e --- /dev/null +++ "b/docs/tools/Github\346\212\200\345\267\247.md" @@ -0,0 +1,121 @@ +我使用 Github 已经有 5 年多了,今天毫无保留地把自己觉得比较有用的 Gihub 小技巧送给关注 JavaGuide 的各位小伙伴。 + +这篇文章肝了很久,就挺用心的,大家看内容就知道了。 + +如果觉得有收获的话,不要白嫖!点个赞/在看就是对我最大的鼓励。你要是可以三连(点赞+在看+转发)的话,我就更爽了(_我在想屁吃?_)。 + +## 1. 一键生成 Github 简历 + +通过 [http://resume.github.io/](http://resume.github.io/) 这个网站你可以一键生成一个在线的 Github 简历。 + +当时我参加的校招的时候,个人信息那里就放了一个在线的 Github 简历。我觉得这样会让面试官感觉你是一个内行,会提高一些印象分。 + +但是,如果你的 Github 没有什么项目的话还是不要放在简历里面了。生成后的效果如下图所示。 + +![Github简历](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/image-20201108192205620.png) + +## 2. 个性化 Github 首页 + +Github 目前支持在个人主页自定义展示一些内容。展示效果如下图所示。 + +![个性化首页展示效果](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/1.png) + +想要做到这样非常简单,你只需要创建一个和你的 Github 账户同名的仓库,然后自定义`README.md`的内容即可。 + +展示在你主页的自定义内容就是`README.md`的内容(_不会 Markdown 语法的小伙伴自行面壁 5 分钟_)。 + +![创建一个和你的Github账户同名的仓库](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/image-20201107110309341.png) + +这个也是可以玩出花来的!比如说:通过 [github-readme-stats](https://hellogithub.com/periodical/statistics/click/?target=https://github.com/anuraghazra/github-readme-stats) 这个开源项目,你可以 README 中展示动态生成的 GitHub 统计信息。展示效果如下图所示。 + +![通过github-readme-stats动态生成GitHub统计信息 ](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/2.png) + +关于个性化首页这个就不多提了,感兴趣的小伙伴自行研究一下。 + +## 3. 自定义项目徽章 + +你在 Github 上看到的项目徽章都是通过 [https://shields.io/](https://shields.io/) 这个网站生成的。我的 JavaGuide 这个项目的徽章如下图所示。 + +![项目徽章](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/image-20201107143136559.png) + +并且,你不光可以生成静态徽章,shield.io 还可以动态读取你项目的状态并生成对应的徽章。 + +![自定义项目徽章](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/image-20201107143502356.png) + +生成的描述项目状态的徽章效果如下图所示。 + +![描述项目状态的徽章](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/image-20201107143752642.png) + +## 4. Github 表情 + +![Github表情](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/image-20201107162254582.png) + +如果你想要在 Github 使用表情的话,可以在这里找找 :[www.webfx.com/tools/emoji-cheat-sheet/ ](www.webfx.com/tools/emoji-cheat-sheet/)。 + +![在线Github表情](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/image-20201107162432941.png) + +## 5. 高效阅读 Github 项目的源代码 + +Github 前段时间推出的 Codespaces 可以提供类似 VS Code 的在线 IDE,不过目前还没还没完全开发使用。 + +简单介绍几种我最常用的阅读 Github 项目源代码的方式。 + +### 5.1. Chrome 插件 Octotree + +这个已经老生常谈了,是我最喜欢的一种方式。使用了 Octotree 之后网页侧边栏会按照树形结构展示项目,为我们带来 IDE 般的阅读源代码的感受。 + +![Chrome插件Octotree](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/image-20201107144944798.png) + +### 5.2. Chrome 插件 SourceGraph + +我不想将项目 clone 到本地的时候一般就会使用这种方式来阅读项目源代码。SourceGraph 不仅可以让我们在 Github 优雅的查看代码,它还支持一些骚操作,比如:类之间的跳转、代码搜索等功能。 + +当你下载了这个插件之后,你的项目主页会多出一个小图标如下图所示。点击这个小图标即可在线阅读项目源代码。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/image-20201107145749659.png) + +使用 SourceGraph 阅读代码的就像下面这样,同样是树形结构展示代码,但是我个人感觉没有 Octotree 的手感舒服。不过,SourceGraph 内置了很多插件,而且还支持类之间的跳转! + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/image-20201107150307314.png) + +### 5.3. 克隆项目到本地 + +先把项目克隆到本地,然后使用自己喜欢的 IDE 来阅读。可以说是最酸爽的方式了! + +如果你想要深入了解某个项目的话,首选这种方式。一个`git clone` 就完事了。 + +### 5.4. 其他 + +如果你要看的是前端项目的话,还可以考虑使用 [https://stackblitz.com/](https://stackblitz.com/) 这个网站。 + +这个网站会提供一个类似 VS Code 的在线 IDE。 + +## 6. 一键开启 Github 夜间模式 + +通过 **GitHub Dark Theme** 这个 Chrome 插件你可以将 Github 的主题变为夜间样式。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/image-20201107160240274.png) + +## 7. 扩展 Github 的功能 + +**Enhanced GitHub** 可以让你的 Github 更好用。这个 Chrome 插件可以可视化你的 Github 仓库大小,每个文件的大小并且可以让你快速下载单个文件。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/image-20201107160817672.png) + +## 8. 自动为 Markdown 文件生成目录 + +如果你想为 Github 上的 Markdown 文件生成目录的话,通过 VS Code 的 **Markdown Preview Enhanced** 这个插件就可以了。 + +生成的目录效果如下图所示。你直接点击目录中的链接即可跳转到文章对应的位置,可以优化阅读体验。 + +![]() + +## 9. 后记 + +这篇文章是我上周六的时候坐在窗台写的,花了一下午时间。 + +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-11/301604738120_.pic_hd.jpg) + +除了我提到的这些技巧之外,像 Github 搜索技巧、GitHub Actions 等内容的话,我这里没有提,感兴趣的小伙伴自行研究一下。 + +这里多说一句心里话: **Github 搜索技巧不必要记网上那些文章说的各种命令啥的,真没啥卵用。你会发现你用的最多的还是关键字搜索以及 Github 自带的筛选功能。** \ No newline at end of file diff --git a/docs/tools/github/github-star-ranking.md b/docs/tools/github/github-star-ranking.md deleted file mode 100644 index fa0c42ee094..00000000000 --- a/docs/tools/github/github-star-ranking.md +++ /dev/null @@ -1,84 +0,0 @@ - - -> 下面的 10 个项目还是很推荐的!JS 的项目占比挺大,其他基本都是文档/学习类型的仓库。 - -说明:数据统计于 2019-11-27。 - -### 1. freeCodeCamp - -- **Github地址**:[https://github.com/freeCodeCamp/freeCodeCamp](https://github.com/freeCodeCamp/freeCodeCamp) -- **star**: 307 k -- **介绍**: 开放源码代码库和课程。与数百万人一起免费学习编程。网站:[https://www.freeCodeCamp.org](https://www.freecodecamp.org/) (一个友好的社区,您可以在这里免费学习编码。它由捐助者支持、非营利组织运营,以帮助数百万忙碌的成年人学习编程技术。这个社区已经帮助10,000多人获得了第一份开发人员的工作。这里的全栈Web开发课程是完全免费的,并且可以自行调整进度。这里还有数以千计的交互式编码挑战,可帮助您扩展技能。) - -比如我想学习 ES6 的语法,学习界面是下面这样的,你可以很方便地边练习边学习: - -![Learn ES6](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/freecodemap-es6.jpg) - -### 2. 996.ICU - -- **Github地址**:[https://github.com/996icu/996.ICU](https://github.com/996icu/996.ICU) -- **star**: 248 k -- **介绍**: `996.ICU` 是指“工作 996, 生病 ICU” 。这是中国程序员之间的一种自嘲说法,意思是如果按照 996 的模式工作,那以后就得进 ICU 了。这个项目最早是某个中国程序员发起的,然后就火遍全网,甚至火到了全世界很多其他国家,其网站被翻译成了多种语言。网站地址:[https://996.icu](https://996.icu/)。 - -![996.ICU-website](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/996.icu.jpg) - -### 3. vue - -- **Github地址**:[https://github.com/vuejs/vue](https://github.com/vuejs/vue) -- **star**: 153 k -- **介绍**: 尤大的前端框架。国人用的最多(容易上手,文档比较丰富),所以 Star 数量比较多还是有道理的。Vue (读音 /vjuː/,类似于 **view**) 是一套用于构建用户界面的**渐进式框架**。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与[现代化的工具链](https://cn.vuejs.org/v2/guide/single-file-components.html)以及各种[支持类库](https://github.com/vuejs/awesome-vue#libraries--plugins)结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。 - -### 4. React - -- **Github地址**:[https://gitstar-ranking.com/facebook/react](https://gitstar-ranking.com/facebook/react) -- **star**: 140 k -- **介绍**: Facebook 开源的,大公司有保障。用于构建用户界面的声明式、基于组件开发,高效且灵活的JavaScript框架。我司大部分项目的前端都是 React ,我自己也用过一段时间,感觉还不错,但是也有一些小坑。 - -### 5. tensorflow - -- **Github地址**:[https://github.com/tensorflow/tensorflow](https://github.com/tensorflow/tensorflow) -- **star**: 138 k -- **介绍**: 适用于所有人的开源机器学习框架。[TensorFlow](https://www.tensorflow.org/)是用于机器学习的端到端开源平台。TensorFlow最初是由Google机器智能研究组织内Google Brain团队的研究人员和工程师开发的,用于进行机器学习和深度神经网络研究。该系统具有足够的通用性,也可以适用于多种其他领域。TensorFlow提供了稳定的[Python](https://www.tensorflow.org/api_docs/python) 和[C ++](https://www.tensorflow.org/api_docs/cc) API,以及[其他语言的](https://www.tensorflow.org/api_docs)非保证的向后兼容API 。 - -### 6. bootstrap - -- **Github地址**:[https://github.com/twbs/bootstrap](https://github.com/twbs/bootstrap) -- **star**: 137 k -- **介绍**: 相信初学前端的时候,大家一定或多或少地接触过这个框架。官网说它是最受欢迎的HTML,CSS和JavaScript框架,用于在网络上开发响应式,移动优先项目。 - -### 7. free-programming-books - -- **Github地址**:[https://github.com/EbookFoundation/free-programming-books](https://github.com/EbookFoundation/free-programming-books) -- **star**: 132 k -- **介绍**: 免费提供的编程书籍。我自己没太搞懂为啥这个项目 Star 数这么多,知道的麻烦评论区吱一声。 - -### 8. Awesome - -- **Github地址** : [https://github.com/sindresorhus/awesome](https://github.com/sindresorhus/awesome) -- **star**: 120 k -- **介绍**: github 上很多的各种 Awesome 系列合集。 - - 下面是这个开源仓库的目录,可以看出其涵盖了很多方面的内容。 - - - -举个例子,这个仓库里面就有两个让你的电脑更好用的开源仓库,Mac 和 Windows都有: - -- Awesome Mac:https://github.com/jaywcjlove/awesome-mac/blob/master/README-zh.m -- Awsome Windows: https://github.com/Awesome-Windows/Awesome/blob/master/README-cn.md - -### 9. You-Dont-Know-JS - -- **Github地址**:[https://github.com/getify/You-Dont-Know-JS](https://github.com/getify/You-Dont-Know-JS) -- **star**: 112 k -- **介绍**: 您还不认识JS(书籍系列)-第二版 - -### 10. oh-my-zsh - -- **Github地址**:[https://github.com/ohmyzsh/ohmyzsh](https://github.com/ohmyzsh/ohmyzsh) -- **star**: 99.4 k -- **介绍**: 一个令人愉快的社区驱动的框架(拥有近1500个贡献者),用于管理zsh配置。包括200多个可选插件(rails, git, OSX, hub, capistrano, brew, ant, php, python等),140多个主题,可为您的早晨增光添彩,以及一个自动更新工具,可让您轻松保持与来自社区的最新更新…… - -下面就是 oh-my-zsh 提供的一个花里胡哨的主题: - -![oh-my-zsh-theme](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/ohmyzsh-theme.png) diff --git "a/docs/tools/\351\230\277\351\207\214\344\272\221\346\234\215\345\212\241\345\231\250\344\275\277\347\224\250\347\273\217\351\252\214.md" "b/docs/tools/\351\230\277\351\207\214\344\272\221\346\234\215\345\212\241\345\231\250\344\275\277\347\224\250\347\273\217\351\252\214.md" deleted file mode 100644 index 55c89645362..00000000000 --- "a/docs/tools/\351\230\277\351\207\214\344\272\221\346\234\215\345\212\241\345\231\250\344\275\277\347\224\250\347\273\217\351\252\214.md" +++ /dev/null @@ -1,149 +0,0 @@ -最近很多阿里云双 11 做活动,优惠力度还挺大的,很多朋友都买以最低的价格买到了自己的云服务器。不论是作为学习机还是部署自己的小型网站或者服务来说都是很不错的! - -但是,很多朋友都不知道如何正确去使用。下面我简单分享一下自己的使用经验。 - -总结一下,主要涉及下面几个部分,对于新手以及没有这么使用过云服务的朋友还是比较友好的: - -1. 善用阿里云镜像市场节省安装 Java 环境的时间,相关说明都在根目录下的 readme.txt. 文件里面; -2. 本地通过 SSH 连接阿里云服务器很容易,配置好 Host地址,通过 root 用户加上实例密码直接连接即可。 -3. 本地连接 MySQL 数据库需要简单配置一下安全组和并且允许 root 用户在任何地方进行远程登录。 -4. 通过 Alibaba Cloud Toolkit 部署 Spring Boot 项目到阿里云服务器真的很方便。 - -**[活动地址](https://www.aliyun.com/1111/2019/group-buying-share?ptCode=32AE103FC8249634736194795A3477C4647C88CF896EF535&userCode=hf47liqn&share_source=copy_link)** (仅限新人,老用户可以考虑使用家人或者朋友账号购买,推荐799/3年 2核4G 这个性价比和适用面更广) - -### 善用阿里云镜像市场节省安装环境的时间 - -基本的购买流程这里就不多说了,另外这里需要注意的是:其实 Java 环境是不需要我们手动安装配置的,阿里云提供的镜像市场有一些常用的环境。 - -> 阿里云镜像市场是指阿里云建立的、由镜像服务商向用户提供其镜像及相关服务的网络平台。这些镜像在操作系统上整合了具体的软件环境和功能,比如Java、PHP运行环境、控制面板等,供有相关需求的用户开通实例时选用。 - -具体如何在购买云服务器的时候通过镜像创建实例或者已有ECS用户如何使用镜像可以查看官方详细的介绍,地址: - -https://help.aliyun.com/knowledge_detail/41987.html?spm=a2c4g.11186631.2.1.561e2098dIdCGZ - -### 当我们成功购买服务器之后如何通过 SSH 连接呢? - -创建好 ECS 后,你绑定的手机会收到短信,会告知你初始密码的。你可以登录管理控制台对密码进行修改,修改密码需要在管理控制台重启服务器才能生效。 - -你也可以在阿里云 ECS 控制台重置实例密码,如下图所示。 - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/Screen Shot 2019-10-30 at 10.51.15 AM.png) - -**第一种连接方式是直接在阿里云服务器管理的网页上连接**。如上图所示, 点击远程连接,然后输入远程连接密码,这个并不是你重置实例密码得到的密码,如果忘记了直接修改远程连接密码即可。 - -**第二种方式是在本地通过命令或者软件连接。** 推荐使用这种方式,更加方便。 - - **Windows 推荐使用 Xshell 连接,具体方式如下:** - -> Window电脑在家,这里直接用找到的一些图片给大家展示一个。 - -![](https://img2018.cnblogs.com/blog/1070438/201812/1070438-20181226165727765-1335537850.png) - -![](https://img2018.cnblogs.com/blog/1070438/201812/1070438-20181226170155651-1407670048.png) - -接着点开,输入账号:root,命名输入刚才设置的密码,点ok就可以了 - -![](https://img2018.cnblogs.com/blog/1070438/201812/1070438-20181226170444344-411355334.png) - -**Mac 或者 Linux 系统都可以直接使用 ssh 命令进行连接,非常方便。** - -成功连接之后,控制台会打印出如下消息。 - -```shell -➜ ~ ssh root@47.107.159.12 -p 22 -root@47.107.159.12's password: -Last login: Wed Oct 30 09:31:31 2019 from 220.249.123.170 - -Welcome to Alibaba Cloud Elastic Compute Service ! - - 欢迎使用 Tomcat8 JDK8 Mysql5.7 环境 - - 使用说明请参考 /root/readme.txt 文件 -``` - -我当时选择是阿里云提供好的 Java 环境,自动就提供了 Tomcat、 JDK8 、Mysql5.7,所以不需要我们再进行安装配置了,节省了很多时间。另外,需要注意的是:**一定要看 /readme.txt ,Tomcat、 JDK8 、Mysql5.7相关配置以及安装路径等说明都在里面。** - -### 如何连接数据库? - - **如需外网远程访问mysql 请参考以上网址 设置mysql及阿里云安全组**。 - -![开放安全组](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/开放安全组.png) - -Mysql为了安全性,在默认情况下用户只允许在本地登录,但是可以使用 SSH 方式连接。如果我们不想通过 SSH 方式连接的话就需要对 MySQL 进行简单的配置。 - -```shell -#允许root用户在任何地方进行远程登录,并具有所有库任何操作权限: -# *.*代表所有库表 “%”代表所有IP地址 -mysql> GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY "自定义密码" WITH GRANT OPTION; -Query OK, 0 rows affected, 1 warning (0.00 sec) -#刷新权限。  -mysql>flush privileges; -#退出mysql -mysql>exit -#重启MySQL生效 -[root@snailclimb]# systemctl restart mysql -``` - -这样的话,我们就能在本地进行连接了。Windows 推荐使用Navicat或者SQLyog。 - -> Window电脑在家,这里用 Mac 上的MySQL可视化工具Sequel Pro给大家演示一下。 - - - -### 如何把一个Spring Boot 项目部署到服务器上呢? - -默认大家都是用 IDEA 进行开发。另外,你要有一个简单的 Spring Boot Web 项目。如果还不了解 Spring Boot 的话,一个简单的 Spring Boot 版 "Hello World "项目,地址如下: - -https://github.com/Snailclimb/springboot-guide/blob/master/docs/start/springboot-hello-world.md 。 - -**1.下载一个叫做 Alibaba Cloud Toolkit 的插件。** - - - -**2.进入 Preference 配置一个 Access Key ID 和 Access Key Secret。** - - - -**3.部署项目到 ECS 上。** - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/deploy-to-ecs1.png) - - - -按照上面这样填写完基本配置之后,然后点击 run 运行即可。运行成功,控制台会打印出如下信息: - -```shell -[INFO] Deployment File is Uploading... -[INFO] IDE Version:IntelliJ IDEA 2019.2 -[INFO] Alibaba Cloud Toolkit Version:2019.9.1 -[INFO] Start upload hello-world-0.0.1-SNAPSHOT.jar -[INFO][##################################################] 100% (18609645/18609645) -[INFO] Succeed to upload, 18609645 bytes have been uploaded. -[INFO] Upload Deployment File to OSS Success -[INFO] Target Deploy ECS: { 172.18.245.148 / 47.107.159.12 } -[INFO] Command: { source /etc/profile; cd /springboot; } - Tip: The deployment package will be temporarily stored in Alibaba Cloud Security OSS and will be - deleted after the deployment is complete. Please be assured that no one can access it except you. - -[INFO] Create Deploy Directory Success. - -[INFO] Deployment File is Downloading... -[INFO] Download Deployment File from OSS Success - -[INFO] File Upload Total time: 16.676 s -``` - - 通过控制台答应出的信息可以看出:通过这个插件会自动把这个 Spring Boot 项目打包成一个 jar 包,然后上传到你的阿里云服务器中指定的文件夹中,你只需要登录你的阿里云服务器,然后通过 `java -jar hello-world-0.0.1-SNAPSHOT.jar`命令运行即可。 - -```shell -[root@snailclimb springboot]# ll -total 18176 --rw-r--r-- 1 root root 18609645 Oct 30 08:25 hello-world-0.0.1-SNAPSHOT.jar -[root@snailclimb springboot]# java -jar hello-world-0.0.1-SNAPSHOT.jar -``` - - 然后你就可以在本地访问访问部署在你的阿里云 ECS 上的服务了。 - - - -**[推荐一下阿里云双11的活动:云服务器1折起,仅86元/年,限量抢购!](https://www.aliyun.com/1111/2019/group-buying-share?ptCode=32AE103FC8249634736194795A3477C4647C88CF896EF535&userCode=hf47liqn&share_source=copy_link)** (仅限新人,老用户可以考虑使用家人或者朋友账号购买,推荐799/3年 2核4G 这个性价比和适用面更广) \ No newline at end of file diff --git a/index.html b/index.html index f154126a653..6b0768a5feb 100644 --- a/index.html +++ b/index.html @@ -8,11 +8,16 @@ - + + +

@@ -21,12 +26,12 @@ if (typeof navigator.serviceWorker !== 'undefined') { navigator.serviceWorker.register('sw.js') } - window.$docsify = { name: 'JavaGuide', repo: 'https://github.com/Snailclimb/JavaGuide', - maxLevel: 3,//最大支持渲染的标题层级 - coverpage: true,//封面,_coverpage.md + maxLevel: 4,//最大支持渲染的标题层级 + //封面,_coverpage.md + //coverpage: true, auto2top: true,//切换页面后是否自动跳转到页面顶部 //ga: 'UA-138586553-1', //logo: 'https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3logo-透明.png' , @@ -38,10 +43,16 @@ // 搜索标题的最大程级, 1 - 6 depth: 3, }, + // 字数统计 + count: { + countable: true, + fontsize: '0.9em', + color: 'rgb(90,90,90)', + language: 'chinese' + }, plugins: [ EditOnGithubPlugin.create('https://github.com/Snailclimb/JavaGuide/blob/master/') ], - } @@ -57,6 +68,10 @@ + + + + \ No newline at end of file diff --git a/media/pictures/java/my-lru-cache/ConcurrentLinkedQueue-Diagram.png b/media/pictures/java/my-lru-cache/ConcurrentLinkedQueue-Diagram.png deleted file mode 100644 index 26b68b1f7cd..00000000000 Binary files a/media/pictures/java/my-lru-cache/ConcurrentLinkedQueue-Diagram.png and /dev/null differ diff --git a/media/pictures/java/my-lru-cache/MyLRUCachePut.png b/media/pictures/java/my-lru-cache/MyLRUCachePut.png deleted file mode 100644 index 2ffc0b7b087..00000000000 Binary files a/media/pictures/java/my-lru-cache/MyLRUCachePut.png and /dev/null differ diff --git a/media/pictures/java/my-lru-cache/ScheduledThreadPoolExecutor-diagram.png b/media/pictures/java/my-lru-cache/ScheduledThreadPoolExecutor-diagram.png deleted file mode 100644 index 00a11dc7216..00000000000 Binary files a/media/pictures/java/my-lru-cache/ScheduledThreadPoolExecutor-diagram.png and /dev/null differ diff --git "a/media/pictures/kafka/Broker\345\222\214\351\233\206\347\276\244.png" "b/media/pictures/kafka/Broker\345\222\214\351\233\206\347\276\244.png" deleted file mode 100644 index edd70355200..00000000000 Binary files "a/media/pictures/kafka/Broker\345\222\214\351\233\206\347\276\244.png" and /dev/null differ diff --git "a/media/pictures/kafka/Partition\344\270\216\346\266\210\350\264\271\346\250\241\345\236\213.png" "b/media/pictures/kafka/Partition\344\270\216\346\266\210\350\264\271\346\250\241\345\236\213.png" deleted file mode 100644 index c87f44feace..00000000000 Binary files "a/media/pictures/kafka/Partition\344\270\216\346\266\210\350\264\271\346\250\241\345\236\213.png" and /dev/null differ diff --git "a/media/pictures/kafka/kafka\345\255\230\345\234\250\346\226\207\344\273\266\347\263\273\347\273\237\344\270\212.png" "b/media/pictures/kafka/kafka\345\255\230\345\234\250\346\226\207\344\273\266\347\263\273\347\273\237\344\270\212.png" deleted file mode 100644 index 3455f3adb6f..00000000000 Binary files "a/media/pictures/kafka/kafka\345\255\230\345\234\250\346\226\207\344\273\266\347\263\273\347\273\237\344\270\212.png" and /dev/null differ diff --git "a/media/pictures/kafka/segment\346\230\257kafka\346\226\207\344\273\266\345\255\230\345\202\250\347\232\204\346\234\200\345\260\217\345\215\225\344\275\215.png" "b/media/pictures/kafka/segment\346\230\257kafka\346\226\207\344\273\266\345\255\230\345\202\250\347\232\204\346\234\200\345\260\217\345\215\225\344\275\215.png" deleted file mode 100644 index fbf3088483e..00000000000 Binary files "a/media/pictures/kafka/segment\346\230\257kafka\346\226\207\344\273\266\345\255\230\345\202\250\347\232\204\346\234\200\345\260\217\345\215\225\344\275\215.png" and /dev/null differ diff --git "a/media/pictures/kafka/\344\270\273\351\242\230\344\270\216\345\210\206\345\214\272.png" "b/media/pictures/kafka/\344\270\273\351\242\230\344\270\216\345\210\206\345\214\272.png" deleted file mode 100644 index 5cf07722c4a..00000000000 Binary files "a/media/pictures/kafka/\344\270\273\351\242\230\344\270\216\345\210\206\345\214\272.png" and /dev/null differ diff --git "a/media/pictures/kafka/\345\211\215\350\250\200.md" "b/media/pictures/kafka/\345\211\215\350\250\200.md" deleted file mode 100644 index 24d6e370e5d..00000000000 --- "a/media/pictures/kafka/\345\211\215\350\250\200.md" +++ /dev/null @@ -1,202 +0,0 @@ -# 前言 - -谈到java的线程池最熟悉的莫过于ExecutorService接口了,jdk1.5新增的java.util.concurrent包下的这个api,大大的简化了多线程代码的开发。而不论你用FixedThreadPool还是CachedThreadPool其背后实现都是ThreadPoolExecutor。ThreadPoolExecutor是一个典型的缓存池化设计的产物,因为池子有大小,当池子体积不够承载时,就涉及到拒绝策略。JDK中已经预设了4种线程池拒绝策略,下面结合场景详细聊聊这些策略的使用场景,以及我们还能扩展哪些拒绝策略。 - -# 池化设计思想 - -池话设计应该不是一个新名词。我们常见的如java线程池、jdbc连接池、redis连接池等就是这类设计的代表实现。这种设计会初始预设资源,解决的问题就是抵消每次获取资源的消耗,如创建线程的开销,获取远程连接的开销等。就好比你去食堂打饭,打饭的大妈会先把饭盛好几份放那里,你来了就直接拿着饭盒加菜即可,不用再临时又盛饭又打菜,效率就高了。除了初始化资源,池化设计还包括如下这些特征:池子的初始值、池子的活跃值、池子的最大值等,这些特征可以直接映射到java线程池和数据库连接池的成员属性中。 - -# 线程池触发拒绝策略的时机 - -和数据源连接池不一样,线程池除了初始大小和池子最大值,还多了一个阻塞队列来缓冲。数据源连接池一般请求的连接数超过连接池的最大值的时候就会触发拒绝策略,策略一般是阻塞等待设置的时间或者直接抛异常。而线程池的触发时机如下图: - -![img](http://www.kailing.pub/Uploads/image/20190729/20190729193156_24469.png) - -如图,想要了解线程池什么时候触发拒绝粗略,需要明确上面三个参数的具体含义,是这三个参数总体协调的结果,而不是简单的超过最大线程数就会触发线程拒绝粗略,当提交的任务数大于corePoolSize时,会优先放到队列缓冲区,只有填满了缓冲区后,才会判断当前运行的任务是否大于maxPoolSize,小于时会新建线程处理。大于时就触发了拒绝策略,总结就是:当前提交任务数大于(maxPoolSize + queueCapacity)时就会触发线程池的拒绝策略了。 - -# JDK内置4种线程池拒绝策略 - -# 拒绝策略接口定义 - -在分析JDK自带的线程池拒绝策略前,先看下JDK定义的 拒绝策略接口,如下: - -```java -public interface RejectedExecutionHandler { - void rejectedExecution(Runnable r, ThreadPoolExecutor executor); -} -``` - -接口定义很明确,当触发拒绝策略时,线程池会调用你设置的具体的策略,将当前提交的任务以及线程池实例本身传递给你处理,具体作何处理,不同场景会有不同的考虑,下面看JDK为我们内置了哪些实现: - -# CallerRunsPolicy(调用者运行策略) - -```java - public static class CallerRunsPolicy implements RejectedExecutionHandler { - - public CallerRunsPolicy() { } - - public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { - if (!e.isShutdown()) { - r.run(); - } - } - } -``` - -功能:当触发拒绝策略时,只要线程池没有关闭,就由提交任务的当前线程处理。 - -使用场景:一般在不允许失败的、对性能要求不高、并发量较小的场景下使用,因为线程池一般情况下不会关闭,也就是提交的任务一定会被运行,但是由于是调用者线程自己执行的,当多次提交任务时,就会阻塞后续任务执行,性能和效率自然就慢了。 - -# AbortPolicy(中止策略) - -```java - public static class AbortPolicy implements RejectedExecutionHandler { - - public AbortPolicy() { } - - public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { - throw new RejectedExecutionException("Task " + r.toString() + - " rejected from " + - e.toString()); - } - } -``` - -功能:当触发拒绝策略时,直接抛出拒绝执行的异常,中止策略的意思也就是打断当前执行流程 - -使用场景:这个就没有特殊的场景了,但是一点要正确处理抛出的异常。ThreadPoolExecutor中默认的策略就是AbortPolicy,ExecutorService接口的系列ThreadPoolExecutor因为都没有显示的设置拒绝策略,所以默认的都是这个。但是请注意,ExecutorService中的线程池实例队列都是无界的,也就是说把内存撑爆了都不会触发拒绝策略。当自己自定义线程池实例时,使用这个策略一定要处理好触发策略时抛的异常,因为他会打断当前的执行流程。 - -# DiscardPolicy(丢弃策略) - -```java - public static class DiscardPolicy implements RejectedExecutionHandler { - - public DiscardPolicy() { } - - public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { - } - } -``` - -功能:如果线程池未关闭,就弹出队列头部的元素,然后尝试执行 - -使用场景:这个策略还是会丢弃任务,丢弃时也是毫无声息,但是特点是丢弃的是老的未执行的任务,而且是待执行优先级较高的任务。基于这个特性,我能想到的场景就是,发布消息,和修改消息,当消息发布出去后,还未执行,此时更新的消息又来了,这个时候未执行的消息的版本比现在提交的消息版本要低就可以被丢弃了。因为队列中还有可能存在消息版本更低的消息会排队执行,所以在真正处理消息的时候一定要做好消息的版本比较 - -# 第三方实现的拒绝策略 - -# dubbo中的线程拒绝策略 - -```java -public class AbortPolicyWithReport extends ThreadPoolExecutor.AbortPolicy { - - protected static final Logger logger = LoggerFactory.getLogger(AbortPolicyWithReport.class); - - private final String threadName; - - private final URL url; - - private static volatile long lastPrintTime = 0; - - private static Semaphore guard = new Semaphore(1); - - public AbortPolicyWithReport(String threadName, URL url) { - this.threadName = threadName; - this.url = url; - } - - @Override - public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { - String msg = String.format("Thread pool is EXHAUSTED!" + - " Thread Name: %s, Pool Size: %d (active: %d, core: %d, max: %d, largest: %d), Task: %d (completed: %d)," + - " Executor status:(isShutdown:%s, isTerminated:%s, isTerminating:%s), in %s://%s:%d!", - threadName, e.getPoolSize(), e.getActiveCount(), e.getCorePoolSize(), e.getMaximumPoolSize(), e.getLargestPoolSize(), - e.getTaskCount(), e.getCompletedTaskCount(), e.isShutdown(), e.isTerminated(), e.isTerminating(), - url.getProtocol(), url.getIp(), url.getPort()); - logger.warn(msg); - dumpJStack(); - throw new RejectedExecutionException(msg); - } - - private void dumpJStack() { - //省略实现 - } -} -``` - -可以看到,当dubbo的工作线程触发了线程拒绝后,主要做了三个事情,原则就是尽量让使用者清楚触发线程拒绝策略的真实原因 - -- 输出了一条警告级别的日志,日志内容为线程池的详细设置参数,以及线程池当前的状态,还有当前拒绝任务的一些详细信息。可以说,这条日志,使用dubbo的有过生产运维经验的或多或少是见过的,这个日志简直就是日志打印的典范,其他的日志打印的典范还有spring。得益于这么详细的日志,可以很容易定位到问题所在 -- 输出当前线程堆栈详情,这个太有用了,当你通过上面的日志信息还不能定位问题时,案发现场的dump线程上下文信息就是你发现问题的救命稻草。 -- 继续抛出拒绝执行异常,使本次任务失败,这个继承了JDK默认拒绝策略的特性 - -# Netty中的线程池拒绝策略 - -```java - private static final class NewThreadRunsPolicy implements RejectedExecutionHandler { - NewThreadRunsPolicy() { - super(); - } - - public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { - try { - final Thread t = new Thread(r, "Temporary task executor"); - t.start(); - } catch (Throwable e) { - throw new RejectedExecutionException( - "Failed to start a new thread", e); - } - } - } -``` - -Netty中的实现很像JDK中的CallerRunsPolicy,舍不得丢弃任务。不同的是,CallerRunsPolicy是直接在调用者线程执行的任务。而 Netty是新建了一个线程来处理的。所以,Netty的实现相较于调用者执行策略的使用面就可以扩展到支持高效率高性能的场景了。但是也要注意一点,Netty的实现里,在创建线程时未做任何的判断约束,也就是说只要系统还有资源就会创建新的线程来处理,直到new不出新的线程了,才会抛创建线程失败的异常 - -# ActiveMq中的线程池拒绝策略 - -```java -new RejectedExecutionHandler() { - @Override - public void rejectedExecution(final Runnable r, final ThreadPoolExecutor executor) { - try { - executor.getQueue().offer(r, 60, TimeUnit.SECONDS); - } catch (InterruptedException e) { - throw new RejectedExecutionException("Interrupted waiting for BrokerService.worker"); - } - - throw new RejectedExecutionException("Timed Out while attempting to enqueue Task."); - } - }); -``` - -ActiveMq中的策略属于最大努力执行任务型,当触发拒绝策略时,在尝试一分钟的时间重新将任务塞进任务队列,当一分钟超时还没成功时,就抛出异常 - -# pinpoint中的线程池拒绝策略 - -```java -public class RejectedExecutionHandlerChain implements RejectedExecutionHandler { - private final RejectedExecutionHandler[] handlerChain; - - public static RejectedExecutionHandler build(List chain) { - Objects.requireNonNull(chain, "handlerChain must not be null"); - RejectedExecutionHandler[] handlerChain = chain.toArray(new RejectedExecutionHandler[0]); - return new RejectedExecutionHandlerChain(handlerChain); - } - - private RejectedExecutionHandlerChain(RejectedExecutionHandler[] handlerChain) { - this.handlerChain = Objects.requireNonNull(handlerChain, "handlerChain must not be null"); - } - - @Override - public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { - for (RejectedExecutionHandler rejectedExecutionHandler : handlerChain) { - rejectedExecutionHandler.rejectedExecution(r, executor); - } - } -} -``` - -pinpoint的拒绝策略实现很有特点,和其他的实现都不同。他定义了一个拒绝策略链,包装了一个拒绝策略列表,当触发拒绝策略时,会将策略链中的rejectedExecution依次执行一遍 - -# 结语 - -前文从线程池设计思想,以及线程池触发拒绝策略的时机引出java线程池拒绝策略接口的定义。并辅以JDK内置4种以及四个第三方开源软件的拒绝策略定义描述了线程池拒绝策略实现的各种思路和使用场景。希望阅读此文后能让你对java线程池拒绝策略有更加深刻的认识,能够根据不同的使用场景更加灵活的应用。 \ No newline at end of file diff --git "a/media/pictures/kafka/\345\217\221\351\200\201\346\266\210\346\201\257.png" "b/media/pictures/kafka/\345\217\221\351\200\201\346\266\210\346\201\257.png" deleted file mode 100644 index 82cedad9dde..00000000000 Binary files "a/media/pictures/kafka/\345\217\221\351\200\201\346\266\210\346\201\257.png" and /dev/null differ diff --git "a/media/pictures/kafka/\345\220\257\345\212\250\346\234\215\345\212\241.png" "b/media/pictures/kafka/\345\220\257\345\212\250\346\234\215\345\212\241.png" deleted file mode 100644 index b7fe2423e02..00000000000 Binary files "a/media/pictures/kafka/\345\220\257\345\212\250\346\234\215\345\212\241.png" and /dev/null differ diff --git "a/media/pictures/kafka/\346\266\210\350\264\271\350\200\205\350\256\276\350\256\241\346\246\202\350\246\2011.png" "b/media/pictures/kafka/\346\266\210\350\264\271\350\200\205\350\256\276\350\256\241\346\246\202\350\246\2011.png" deleted file mode 100644 index 67ff1bf9f5e..00000000000 Binary files "a/media/pictures/kafka/\346\266\210\350\264\271\350\200\205\350\256\276\350\256\241\346\246\202\350\246\2011.png" and /dev/null differ diff --git "a/media/pictures/kafka/\346\266\210\350\264\271\350\200\205\350\256\276\350\256\241\346\246\202\350\246\2012.png" "b/media/pictures/kafka/\346\266\210\350\264\271\350\200\205\350\256\276\350\256\241\346\246\202\350\246\2012.png" deleted file mode 100644 index 5ea1f290c13..00000000000 Binary files "a/media/pictures/kafka/\346\266\210\350\264\271\350\200\205\350\256\276\350\256\241\346\246\202\350\246\2012.png" and /dev/null differ diff --git "a/media/pictures/kafka/\346\266\210\350\264\271\350\200\205\350\256\276\350\256\241\346\246\202\350\246\2013.png" "b/media/pictures/kafka/\346\266\210\350\264\271\350\200\205\350\256\276\350\256\241\346\246\202\350\246\2013.png" deleted file mode 100644 index 0c7383f9c92..00000000000 Binary files "a/media/pictures/kafka/\346\266\210\350\264\271\350\200\205\350\256\276\350\256\241\346\246\202\350\246\2013.png" and /dev/null differ diff --git "a/media/pictures/kafka/\346\266\210\350\264\271\350\200\205\350\256\276\350\256\241\346\246\202\350\246\2014.png" "b/media/pictures/kafka/\346\266\210\350\264\271\350\200\205\350\256\276\350\256\241\346\246\202\350\246\2014.png" deleted file mode 100644 index 0f4d51df8cc..00000000000 Binary files "a/media/pictures/kafka/\346\266\210\350\264\271\350\200\205\350\256\276\350\256\241\346\246\202\350\246\2014.png" and /dev/null differ diff --git "a/media/pictures/kafka/\346\266\210\350\264\271\350\200\205\350\256\276\350\256\241\346\246\202\350\246\2015.png" "b/media/pictures/kafka/\346\266\210\350\264\271\350\200\205\350\256\276\350\256\241\346\246\202\350\246\2015.png" deleted file mode 100644 index 741302da007..00000000000 Binary files "a/media/pictures/kafka/\346\266\210\350\264\271\350\200\205\350\256\276\350\256\241\346\246\202\350\246\2015.png" and /dev/null differ diff --git "a/media/pictures/kafka/\347\224\237\344\272\247\350\200\205\345\222\214\346\266\210\350\264\271\350\200\205.png" "b/media/pictures/kafka/\347\224\237\344\272\247\350\200\205\345\222\214\346\266\210\350\264\271\350\200\205.png" deleted file mode 100644 index 25c93e003fc..00000000000 Binary files "a/media/pictures/kafka/\347\224\237\344\272\247\350\200\205\345\222\214\346\266\210\350\264\271\350\200\205.png" and /dev/null differ diff --git "a/media/pictures/kafka/\347\224\237\344\272\247\350\200\205\350\256\276\350\256\241\346\246\202\350\246\201.png" "b/media/pictures/kafka/\347\224\237\344\272\247\350\200\205\350\256\276\350\256\241\346\246\202\350\246\201.png" deleted file mode 100644 index 58fe67bbabc..00000000000 Binary files "a/media/pictures/kafka/\347\224\237\344\272\247\350\200\205\350\256\276\350\256\241\346\246\202\350\246\201.png" and /dev/null differ diff --git "a/media/pictures/linux/Debian10\344\270\213IDEA\347\232\204Markdown\351\242\204\346\270\262\346\237\223\350\247\243\345\206\263\345\220\216.png" "b/media/pictures/linux/Debian10\344\270\213IDEA\347\232\204Markdown\351\242\204\346\270\262\346\237\223\350\247\243\345\206\263\345\220\216.png" deleted file mode 100644 index 303f3ee948f..00000000000 Binary files "a/media/pictures/linux/Debian10\344\270\213IDEA\347\232\204Markdown\351\242\204\346\270\262\346\237\223\350\247\243\345\206\263\345\220\216.png" and /dev/null differ diff --git "a/media/pictures/linux/Debian10\344\270\213IDEA\347\232\204Markdown\351\242\204\346\270\262\346\237\223\351\227\256\351\242\230.png" "b/media/pictures/linux/Debian10\344\270\213IDEA\347\232\204Markdown\351\242\204\346\270\262\346\237\223\351\227\256\351\242\230.png" deleted file mode 100644 index aaa25e62877..00000000000 Binary files "a/media/pictures/linux/Debian10\344\270\213IDEA\347\232\204Markdown\351\242\204\346\270\262\346\237\223\351\227\256\351\242\230.png" and /dev/null differ diff --git "a/media/pictures/linux/Fcitx\345\200\231\351\200\211\346\241\206\345\256\232\344\275\215\351\227\256\351\242\230.png" "b/media/pictures/linux/Fcitx\345\200\231\351\200\211\346\241\206\345\256\232\344\275\215\351\227\256\351\242\230.png" deleted file mode 100644 index 222193034b8..00000000000 Binary files "a/media/pictures/linux/Fcitx\345\200\231\351\200\211\346\241\206\345\256\232\344\275\215\351\227\256\351\242\230.png" and /dev/null differ diff --git "a/media/pictures/linux/Linux\347\233\256\345\275\225.png" "b/media/pictures/linux/Linux\347\233\256\345\275\225.png" deleted file mode 100644 index a7c05810d08..00000000000 Binary files "a/media/pictures/linux/Linux\347\233\256\345\275\225.png" and /dev/null differ diff --git "a/media/pictures/linux/\346\210\221\347\232\204\347\224\265\350\204\221\351\205\215\347\275\256.png" "b/media/pictures/linux/\346\210\221\347\232\204\347\224\265\350\204\221\351\205\215\347\275\256.png" deleted file mode 100644 index a07c78de656..00000000000 Binary files "a/media/pictures/linux/\346\210\221\347\232\204\347\224\265\350\204\221\351\205\215\347\275\256.png" and /dev/null differ diff --git "a/media/pictures/linux/\346\226\207\344\273\266inode\344\277\241\346\201\257.png" "b/media/pictures/linux/\346\226\207\344\273\266inode\344\277\241\346\201\257.png" deleted file mode 100644 index b47551e8314..00000000000 Binary files "a/media/pictures/linux/\346\226\207\344\273\266inode\344\277\241\346\201\257.png" and /dev/null differ diff --git "a/media/pictures/linux/\347\224\250\346\210\267\346\200\201\344\270\216\345\206\205\346\240\270\346\200\201.png" "b/media/pictures/linux/\347\224\250\346\210\267\346\200\201\344\270\216\345\206\205\346\240\270\346\200\201.png" deleted file mode 100644 index aa0dafc2f02..00000000000 Binary files "a/media/pictures/linux/\347\224\250\346\210\267\346\200\201\344\270\216\345\206\205\346\240\270\346\200\201.png" and /dev/null differ diff --git "a/media/pictures/linux/\347\233\256\345\275\225\346\226\207\344\273\266.png" "b/media/pictures/linux/\347\233\256\345\275\225\346\226\207\344\273\266.png" deleted file mode 100644 index 52e9397837b..00000000000 Binary files "a/media/pictures/linux/\347\233\256\345\275\225\346\226\207\344\273\266.png" and /dev/null differ diff --git "a/media/pictures/linux/\350\275\257\351\223\276\346\216\245\345\222\214\347\241\254\351\223\276\346\216\245.png" "b/media/pictures/linux/\350\275\257\351\223\276\346\216\245\345\222\214\347\241\254\351\223\276\346\216\245.png" deleted file mode 100644 index 214d436f31b..00000000000 Binary files "a/media/pictures/linux/\350\275\257\351\223\276\346\216\245\345\222\214\347\241\254\351\223\276\346\216\245.png" and /dev/null differ diff --git a/media/sponsor/kaikeba.png b/media/sponsor/kaikeba.png index 5f563bed7ba..9a5ce4877cc 100644 Binary files a/media/sponsor/kaikeba.png and b/media/sponsor/kaikeba.png differ diff --git a/media/sponsor/lagou-new.jpeg b/media/sponsor/lagou-new.jpeg index a093fd40d8c..a2acd797648 100644 Binary files a/media/sponsor/lagou-new.jpeg and b/media/sponsor/lagou-new.jpeg differ diff --git a/media/sponsor/wangyi.png b/media/sponsor/wangyi.png index 2c95d97c64d..eca0bd90a14 100644 Binary files a/media/sponsor/wangyi.png and b/media/sponsor/wangyi.png differ diff --git a/media/sponsor/xiangxue.png b/media/sponsor/xiangxue.png new file mode 100644 index 00000000000..16591c0a3f4 Binary files /dev/null and b/media/sponsor/xiangxue.png differ diff --git "a/\345\205\266\344\273\226/\345\274\200\346\272\220\351\241\271\347\233\256\346\272\220\347\240\201\351\230\205\350\257\273\346\214\207\345\215\227.md" "b/\345\205\266\344\273\226/\345\274\200\346\272\220\351\241\271\347\233\256\346\272\220\347\240\201\351\230\205\350\257\273\346\214\207\345\215\227.md" new file mode 100644 index 00000000000..b4ec32589a5 --- /dev/null +++ "b/\345\205\266\344\273\226/\345\274\200\346\272\220\351\241\271\347\233\256\346\272\220\347\240\201\351\230\205\350\257\273\346\214\207\345\215\227.md" @@ -0,0 +1,90 @@ +# 前言 +作为一个程序员,阅读大牛们优秀的开源项目源码是一个提升个人编程能力、扩展思维的重要途径。在实际工作中,相信并不是所有人接手的项目代码都很优雅和优秀,而且很大可能因为历史遗留、赶进度等原因,导致代码冗余、模块耦合严重、扩展性差和兼容性差等等, 这就有可能导致在工作中无法使个人能力得到很好的提高,并且会导致个人的思维和眼界有所局限。 + +其实一种想法的实现往往是多种的,而欠缺能力的人往往采用简单粗暴的方式,另一方面,而有能力的人总能使用优雅的方式,尽可能考虑各种可能的需求变动、适应各种使用途径和场景、想到未来扩展的方式来实现。 + +优秀的开源项目正是这种有能力的人用优雅方式实现想法的结晶!所以,阅读优秀的开源项目对个人编程的思考方式、知识扩展都是非常非常有帮助的。 + +作为经常阅读别人的优秀开源项目的人,想给大家分享下我的阅读经验,希望能对大家有所帮助。 + +# 步骤 +## 寻找驱动力 +当你开始阅读开源项目首先你得有目的性,工作需要?个人学习?这都是很好的驱动力。 + +没驱动力是很难坚持的,特别是开源项目涉及到很多你不怎么了解的知识点,很容易会觉得枯燥、晦涩。毕竟阅读别人的代码并不是一件快乐的事情,我们很难去完成理解代码作者当时的思路和想法,这个过程是很痛苦的。但如果你有目标、有意图地去阅读,就能在一定程度上减少这痛苦。每天给自己打下鸡血,未来的你等下会为这份坚持感到骄傲! +## 浏览官方文档,对开源项目的功能、架构有大概的印象 +好了,有了驱动力,先别急,看看官方文档,看看这个项目能完成什么事情和不能完成什么事情,还有官方对这个项目的定位。例如 Replugin 写得满满的十几页的 wiki ,官方定位: + +RePlugin是一套完整的、稳定的、适合全面使用的,占坑类插件化方案。 +完整的:让插件运行起来“像单品那样”,支持大部分特性 +稳定的:如此灵活完整的情况下,其框架崩溃率仅为业内很低的“万分之一” +适合全面使用的:其目的是让应用内的“所有功能皆为插件” +占坑类:以稳定为前提的Manifest占坑思路 +插件化方案:基于Android原生API和语言来开发,充分利用原生特性 + +可以看到,wiki很详细地介绍了Replugin定位和优点,这时相信对技术有追求的人都会冒出一个疑问:“他们如何做到的!?” 这又大大激起了你的好奇心,让你更有动力坚持下去。 + +很多人急功近利,马上就开始源码阅读之旅了,包括我。但经过多个项目源码的阅读的我,会告诉你,别急!我们还需要知道它怎么用。 +## 在工作中或实践中使用开源项目 +本节讲的不是怎么使用,这官网文档肯定会有说明的,而是讲为什么使用。一个东西你连使用都不会,就想去了解他的原理?就像你开车都不会,就去了解刹车怎么把车停下来。而事实是你开车都不会,你可能都分不清刹车、离合和油门是哪个跟哪个,这就去了解原理,往往会迷失方向。 + +所以,阅读前先使用吧。但我不建议在实际工作项目中立刻使用,因为你原理都不清楚,有问题不好排查,会影响线上用户,这就很糟糕了。我建议的是看官方demo,然后在自己的个人练手项目中使用。当对项目的使用有一定地理解了,ok,可以走下一步了。 +## 网上搜索针对该开源项目进行分析的优秀文章 +一个优秀的开源项目总是有很多人阅读并分析,然后整理写出总结文章。既然前人都帮我们分析好了,我们为什么不站在前人的肩膀上继续往上爬,这样就省了从脚到肩膀的力气了。但要注意我的字眼,是“优秀”的文章!现在很多人都写博客,很多都是潦潦而谈,只能说是笔记,而非总结。 + +像 Replugin 这样一个“巨型”的开源项目,老实说,对我这种菜鸡来说,很多知识点都只是略知一二,例如多进程通信、gradle编译脚本等,在实际工作中很少接触的难免会觉得难懂。另外,官方文档往往不会对实现细节讲得很细,这时,看前人的分析就很有必要了。这样可以让你对项目的实现有一定地了解,当你自己看时,你能很快懂得作者这样做的意图。 + +当然,如果你不想看别人的分析总结也未必不可,可能在自己阅读过程中多点磕磕碰碰,但你总不能跳过下一步! +## 对开源项目提出自己的疑问 +前面做了这么多准备,你总会产生疑问吧。什么?没有!好吧,这开源项目对你来说太简单,已经不值得你一读了。带着疑问去阅读是我认为最高效的阅读方式,当你有了目的,而不至于在阅读过程中迷失了方向,并且在阅读过程中针对性的看。对一个开源项目的疑问一般可以从以下方向提出: + +这块功能为什么这么做?有什么好处? +有没有另外一种实现方式? +我缺少哪些知识会阻碍我看源码(需要去补)? +例如我在阅读 Replugin 之前提出了几个疑问: + +如何做到一处hook?借助gradle? +查找坑位策略?如何替换真正的启动组件? +为什么需要声明这么多坑位? +为什么不用注入Service? +好了,当你有了好奇心、驱动力、目的,你已经准备好了。但开始阅读前还有一件事情先搞定:编译源码。 + +## 把开源项目下载到本地,并导入IDE,方便调试、测试 +工欲善其事,必先利其器。没有一个好的调试环境怎么能顺心地看源码。但幸亏GitHub让我们能简单地把源码download或clone下来,很多情况都是直接用IDE打开项目就搞定了。但也有像 Replugin 一样的,分为多个项目,每个项目都是单独编译的,这样我们就无法只打开一个窗口来调试,很不爽。这时就需要点导入技巧来搞定了。 +## 带着疑问阅读源码 +战争打响,在充满迷雾的大海中,我方对敌人的方位还不甚了解,但不怕,我们的指北针 —— 疑问 —— 会带领我们直达敌方腹地,我们终会揭开它的露出庐山真面目。 + +开源项目往往是庞大而复杂的,我们在阅读过程中真的非常容易会纠结于细节,而导致阅读混乱,迷失了方向,这对阅读的动力打击很沉重的,往往会使人放弃。 + +而有了疑问就不同了,你知道自己为何要看,你会思考,会有自己的目的,不拘泥于细节实现,能准确地找到源码的核心实现。 + +对于纠结细节是很多人在阅读源码犯的错误,有些细节我们根本不需要去搞清楚它怎么做的,知道它做什么就可以了。一些具体的实现可以放到当你使用过程中遇到问题,或者对该具体实现产生另一个疑问时才去深究,也就是说,还是带着疑问阅读代码。因为一个开源项目往往是多个优秀的人花了很多时间写出来的结晶,你想在短时间内把它完成消化,是不科学的。我们专注于最感兴趣的、最有参考价值和最核心的部分就可以了。 +## 阅读源码过程中多添加注释、多做笔记 +我得承认,我的记忆力不好,而我也不信我的记忆。好记性不如烂笔头,记忆终将遗忘,但所做的笔记除非被销毁,否则永远都会在那里,等着你去翻阅回顾。 + +我们把整个项目都下载下来了,首先当然是在阅读源码过程中添加下自己的注释了,写下自己的理解、疑惑,或者标记下值得借鉴参考的实现等等。另外,我们还需要做些简单的总结笔记。可以纸质或者网上很多的笔记类应用。对于我这种无法直视自己的手写字的,更倾向于用笔记类应用,这也是我推荐大家用的,多端同步,不能再省心。 + +##做阅读总结,吸收和再创造 +当你对开源项目阅读到一定程度了,对该项目有了深刻的理解,并有了自己的见解,你是不是有话要说?别憋着了,讲出来吧!跟大家分享!写篇博客总结下阅读经验、心得和成长等等,既能加深自己的印象,又能帮助到他人,何乐而不为呢?! + +阅读开源项目我们最终的目的是把其涉及到的知识点和设计实现思路吸收,并且转化为自己的功力。这个转化不是说你阅读完了就转化成功了,往往阅读是不够的,你还需要实践。 + +例如喜欢打球的我深知看NBA球星在球场上各种变向戏耍对手,对我的过人能力几乎没任何帮助,只是让我知道:“原来还能这么做呀!” 我还得自己去球场一招一式的练习,反复练习,或者我根据我的身体条件,做些简单的变种,直到这招转化为我的肌肉记忆,我才能在比赛中自然而然地使用出来。 + +所以我提倡再创造。所谓再创造不是让你重复造轮子,而是能根据自己的工作需求,把开源项目应用到工作中。这里的应用不一定是直接引用开源项目来使用,我是不建议这么做的,因为开源项目往往考虑全面,考虑到非常多的情况的,而你项目根本不存在这样的情况,这就是浪费。所以我建议的是:根据自己工作的需求,把开源项目的核心实现抽取出来,转化为能满足自己需求的库来使用。 + +而这个抽取的过程就是吸收的过程。在这个过程你遇到的问题并解决,会使你对开源项目有更深刻的理解。这个过程如果你对开源项目的某个实现不太认同,可以尝试改为自己的实现,这就是吸收。 + +# 总结 +非常感谢看到这里的童鞋,毕竟这些经验谈没什么干货,能耐心读到这里真的非常感谢!我们来总结一波阅读源码的步骤: + +寻找驱动力 +浏览官方文档,对开源项目的功能、架构有大概的印象 +在工作中或实践中使用开源项目 +网上搜索针对该开源项目进行分析的优秀文章 +对开源项目提出自己的疑问 +把开源项目下载到本地,并导入IDE,方便调试、测试 +带着疑问阅读源码 +阅读源码过程中多添加注释、多做笔记 +做阅读总结,吸收和再创造 +以上步骤有些可以根据实际情况跳过,程序员都是聪明人,总也会随机应变~ \ No newline at end of file