mirror of
https://github.com/Snailclimb/JavaGuide
synced 2025-06-20 22:17:09 +08:00
commit
ee60e5e42b
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
.gradle
|
||||||
|
/build/
|
||||||
|
/**/build/
|
||||||
|
|
||||||
|
### STS ###
|
||||||
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
|
||||||
|
### IntelliJ IDEA ###
|
||||||
|
.idea
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
/out/
|
||||||
|
/**/out/
|
||||||
|
.shelf/
|
||||||
|
.ideaDataSources/
|
||||||
|
dataSources/
|
||||||
|
|
||||||
|
### NetBeans ###
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
/node_modules/
|
||||||
|
|
||||||
|
### OS ###
|
||||||
|
.DS_Store
|
236
HomePage.md
Normal file
236
HomePage.md
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
点击订阅[Java面试进阶指南](https://xiaozhuanlan.com/javainterview?rel=javaguide)(专为Java面试方向准备)。[为什么要弄这个专栏?](https://shimo.im/./9BJjNsNg7S4dCnz3/)
|
||||||
|
|
||||||
|
<h1 align="center">Java 学习/面试指南</h1>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/Snailclimb/JavaGuide" target="_blank">
|
||||||
|
<img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3/logo - 副本.png" width=""/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
|
||||||
|
- [Java](#java)
|
||||||
|
- [基础](#基础)
|
||||||
|
- [容器](#容器)
|
||||||
|
- [并发](#并发)
|
||||||
|
- [JVM](#jvm)
|
||||||
|
- [I/O](#io)
|
||||||
|
- [Java 8](#java-8)
|
||||||
|
- [编程规范](#编程规范)
|
||||||
|
- [网络](#网络)
|
||||||
|
- [操作系统](#操作系统)
|
||||||
|
- [Linux相关](#linux相关)
|
||||||
|
- [数据结构与算法](#数据结构与算法)
|
||||||
|
- [数据结构](#数据结构)
|
||||||
|
- [算法](#算法)
|
||||||
|
- [数据库](#数据库)
|
||||||
|
- [MySQL](#mysql)
|
||||||
|
- [Redis](#redis)
|
||||||
|
- [系统设计](#系统设计)
|
||||||
|
- [设计模式(工厂模式、单例模式 ... )](#设计模式)
|
||||||
|
- [常用框架(Spring、Zookeeper ... )](#常用框架)
|
||||||
|
- [数据通信(消息队列、Dubbo ... )](#数据通信)
|
||||||
|
- [网站架构](#网站架构)
|
||||||
|
- [面试指南](#面试指南)
|
||||||
|
- [备战面试](#备战面试)
|
||||||
|
- [常见面试题总结](#常见面试题总结)
|
||||||
|
- [面经](#面经)
|
||||||
|
- [工具](#工具)
|
||||||
|
- [Git](#git)
|
||||||
|
- [Docker](#Docker)
|
||||||
|
- [资料](#资料)
|
||||||
|
- [书单](#书单)
|
||||||
|
- [Github榜单](#Github榜单)
|
||||||
|
- [待办](#待办)
|
||||||
|
- [说明](#说明)
|
||||||
|
|
||||||
|
## Java
|
||||||
|
|
||||||
|
### 基础
|
||||||
|
|
||||||
|
* [Java 基础知识回顾](java/Java基础知识.md)
|
||||||
|
* [Java 基础知识疑难点总结](java/Java疑难点.md)
|
||||||
|
* [J2EE 基础知识回顾](java/J2EE基础知识.md)
|
||||||
|
|
||||||
|
### 容器
|
||||||
|
|
||||||
|
* [Java容器常见面试题/知识点总结](java/collection/Java集合框架常见面试题.md)
|
||||||
|
* [ArrayList 源码学习](java/collection/ArrayList.md)
|
||||||
|
* [LinkedList 源码学习](java/collection/LinkedList.md)
|
||||||
|
* [HashMap(JDK1.8)源码学习](java/collection/HashMap.md)
|
||||||
|
|
||||||
|
### 并发
|
||||||
|
|
||||||
|
* [Java 并发基础常见面试题总结](java/Multithread/JavaConcurrencyBasicsCommonInterviewQuestionsSummary.md)
|
||||||
|
* [Java 并发进阶常见面试题总结](java/Multithread/JavaConcurrencyAdvancedCommonInterviewQuestions.md)
|
||||||
|
* [并发容器总结](java/Multithread/并发容器总结.md)
|
||||||
|
* [乐观锁与悲观锁](essential-content-for-interview/面试必备之乐观锁与悲观锁.md)
|
||||||
|
* [JUC 中的 Atomic 原子类总结](java/Multithread/Atomic.md)
|
||||||
|
* [AQS 原理以及 AQS 同步组件总结](java/Multithread/AQS.md)
|
||||||
|
|
||||||
|
### JVM
|
||||||
|
* [一 Java内存区域](java/jvm/Java内存区域.md)
|
||||||
|
* [二 JVM垃圾回收](java/jvm/JVM垃圾回收.md)
|
||||||
|
* [三 JDK 监控和故障处理工具](java/jvm/JDK监控和故障处理工具总结.md)
|
||||||
|
* [四 类文件结构](java/jvm/类文件结构.md)
|
||||||
|
* [五 类加载过程](java/jvm/类加载过程.md)
|
||||||
|
* [六 类加载器](java/jvm/类加载器.md)
|
||||||
|
|
||||||
|
### I/O
|
||||||
|
|
||||||
|
* [BIO,NIO,AIO 总结 ](java/BIO-NIO-AIO.md)
|
||||||
|
* [Java IO 与 NIO系列文章](java/Java%20IO与NIO.md)
|
||||||
|
|
||||||
|
### Java 8
|
||||||
|
|
||||||
|
* [Java 8 新特性总结](java/What's%20New%20in%20JDK8/Java8Tutorial.md)
|
||||||
|
* [Java 8 学习资源推荐](java/What's%20New%20in%20JDK8/Java8教程推荐.md)
|
||||||
|
|
||||||
|
### 编程规范
|
||||||
|
|
||||||
|
- [Java 编程规范](java/Java编程规范.md)
|
||||||
|
|
||||||
|
## 网络
|
||||||
|
|
||||||
|
* [计算机网络常见面试题](network/计算机网络.md)
|
||||||
|
* [计算机网络基础知识总结](network/干货:计算机网络知识总结.md)
|
||||||
|
* [HTTPS中的TLS](network/HTTPS中的TLS.md)
|
||||||
|
|
||||||
|
## 操作系统
|
||||||
|
|
||||||
|
### Linux相关
|
||||||
|
|
||||||
|
* [后端程序员必备的 Linux 基础知识](operating-system/后端程序员必备的Linux基础知识.md)
|
||||||
|
* [Shell 编程入门](operating-system/Shell.md)
|
||||||
|
|
||||||
|
## 数据结构与算法
|
||||||
|
|
||||||
|
### 数据结构
|
||||||
|
|
||||||
|
- [数据结构知识学习与面试](dataStructures-algorithms/数据结构.md)
|
||||||
|
|
||||||
|
### 算法
|
||||||
|
|
||||||
|
- [算法学习资源推荐](dataStructures-algorithms/算法学习资源推荐.md)
|
||||||
|
- [几道常见的字符串算法题总结 ](dataStructures-algorithms/几道常见的子符串算法题.md)
|
||||||
|
- [几道常见的链表算法题总结 ](dataStructures-algorithms/几道常见的链表算法题.md)
|
||||||
|
- [剑指offer部分编程题](dataStructures-algorithms/剑指offer部分编程题.md)
|
||||||
|
- [公司真题](dataStructures-algorithms/公司真题.md)
|
||||||
|
- [回溯算法经典案例之N皇后问题](dataStructures-algorithms/Backtracking-NQueens.md)
|
||||||
|
|
||||||
|
## 数据库
|
||||||
|
|
||||||
|
### MySQL
|
||||||
|
|
||||||
|
* [MySQL 学习与面试](database/MySQL.md)
|
||||||
|
* [一千行MySQL学习笔记](database/一千行MySQL命令.md)
|
||||||
|
* [MySQL高性能优化规范建议](database/MySQL高性能优化规范建议.md)
|
||||||
|
* [数据库索引总结](database/MySQL%20Index.md)
|
||||||
|
* [事务隔离级别(图文详解)](database/事务隔离级别(图文详解).md)
|
||||||
|
* [一条SQL语句在MySQL中如何执行的](database/一条sql语句在mysql中如何执行的.md)
|
||||||
|
|
||||||
|
### Redis
|
||||||
|
|
||||||
|
* [Redis 总结](database/Redis/Redis.md)
|
||||||
|
* [Redlock分布式锁](database/Redis/Redlock分布式锁.md)
|
||||||
|
* [如何做可靠的分布式锁,Redlock真的可行么](database/Redis/如何做可靠的分布式锁,Redlock真的可行么.md)
|
||||||
|
|
||||||
|
## 系统设计
|
||||||
|
|
||||||
|
### 设计模式
|
||||||
|
|
||||||
|
- [设计模式系列文章](system-design/设计模式.md)
|
||||||
|
|
||||||
|
### 常用框架
|
||||||
|
|
||||||
|
#### Spring
|
||||||
|
|
||||||
|
- [Spring 学习与面试](system-design/framework/spring/Spring.md)
|
||||||
|
- [Spring 常见问题总结](system-design/framework/spring/SpringInterviewQuestions.md)
|
||||||
|
- [Spring中bean的作用域与生命周期](system-design/framework/spring/SpringBean.md)
|
||||||
|
- [SpringMVC 工作原理详解](system-design/framework/spring/SpringMVC-Principle.md)
|
||||||
|
- [Spring中都用到了那些设计模式?](system-design/framework/spring/Spring-Design-Patterns.md)
|
||||||
|
|
||||||
|
#### ZooKeeper
|
||||||
|
|
||||||
|
- [ZooKeeper 相关概念总结](system-design/framework/ZooKeeper.md)
|
||||||
|
- [ZooKeeper 数据模型和常见命令](system-design/framework/ZooKeeper数据模型和常见命令.md)
|
||||||
|
|
||||||
|
### 数据通信
|
||||||
|
|
||||||
|
- [数据通信(RESTful、RPC、消息队列)相关知识点总结](system-design/data-communication/summary.md)
|
||||||
|
- [Dubbo 总结:关于 Dubbo 的重要知识点](system-design/data-communication/dubbo.md)
|
||||||
|
- [消息队列总结](system-design/data-communication/message-queue.md)
|
||||||
|
- [RabbitMQ 入门](system-design/data-communication/rabbitmq.md)
|
||||||
|
- [RocketMQ的几个简单问题与答案](system-design/data-communication/RocketMQ-Questions.md)
|
||||||
|
|
||||||
|
### 网站架构
|
||||||
|
|
||||||
|
- [一文读懂分布式应该学什么](system-design/website-architecture/分布式.md)
|
||||||
|
- [8 张图读懂大型网站技术架构](system-design/website-architecture/8%20张图读懂大型网站技术架构.md)
|
||||||
|
- [【面试精选】关于大型网站系统架构你不得不懂的10个问题](system-design/website-architecture/【面试精选】关于大型网站系统架构你不得不懂的10个问题.md)
|
||||||
|
|
||||||
|
## 面试指南
|
||||||
|
|
||||||
|
### 备战面试
|
||||||
|
|
||||||
|
* [【备战面试1】程序员的简历就该这样写](essential-content-for-interview/PreparingForInterview/程序员的简历之道.md)
|
||||||
|
* [【备战面试2】初出茅庐的程序员该如何准备面试?](essential-content-for-interview/PreparingForInterview/interviewPrepare.md)
|
||||||
|
* [【备战面试3】7个大部分程序员在面试前很关心的问题](essential-content-for-interview/PreparingForInterview/JavaProgrammerNeedKnow.md)
|
||||||
|
* [【备战面试4】Github上开源的Java面试/学习相关的仓库推荐](essential-content-for-interview/PreparingForInterview/JavaInterviewLibrary.md)
|
||||||
|
* [【备战面试5】如果面试官问你“你有什么问题问我吗?”时,你该如何回答](essential-content-for-interview/PreparingForInterview/如果面试官问你“你有什么问题问我吗?”时,你该如何回答.md)
|
||||||
|
* [【备战面试6】美团面试常见问题总结(附详解答案)](essential-content-for-interview/PreparingForInterview/美团面试常见问题总结.md)
|
||||||
|
|
||||||
|
### 常见面试题总结
|
||||||
|
|
||||||
|
* [第一周(2018-8-7)](essential-content-for-interview/MostCommonJavaInterviewQuestions/第一周(2018-8-7).md) (为什么 Java 中只有值传递、==与equals、 hashCode与equals)
|
||||||
|
* [第二周(2018-8-13)](essential-content-for-interview/MostCommonJavaInterviewQuestions/第二周(2018-8-13).md)(String和StringBuffer、StringBuilder的区别是什么?String为什么是不可变的?、什么是反射机制?反射机制的应用场景有哪些?......)
|
||||||
|
* [第三周(2018-08-22)](java/collection/Java集合框架常见面试题.md) (Arraylist 与 LinkedList 异同、ArrayList 与 Vector 区别、HashMap的底层实现、HashMap 和 Hashtable 的区别、HashMap 的长度为什么是2的幂次方、HashSet 和 HashMap 区别、ConcurrentHashMap 和 Hashtable 的区别、ConcurrentHashMap线程安全的具体实现方式/底层具体实现、集合框架底层数据结构总结)
|
||||||
|
* [第四周(2018-8-30).md](essential-content-for-interview/MostCommonJavaInterviewQuestions/第四周(2018-8-30).md) (主要内容是几道面试常问的多线程基础题。)
|
||||||
|
|
||||||
|
### 面经
|
||||||
|
|
||||||
|
- [5面阿里,终获offer(2018年秋招)](essential-content-for-interview/BATJrealInterviewExperience/5面阿里,终获offer.md)
|
||||||
|
- [蚂蚁金服2019实习生面经总结(已拿口头offer)](essential-content-for-interview/BATJrealInterviewExperience/蚂蚁金服实习生面经总结(已拿口头offer).md)
|
||||||
|
- [2019年蚂蚁金服、头条、拼多多的面试总结](essential-content-for-interview/BATJrealInterviewExperience/2019alipay-pinduoduo-toutiao.md)
|
||||||
|
|
||||||
|
## 工具
|
||||||
|
|
||||||
|
### Git
|
||||||
|
|
||||||
|
* [Git入门](tools/Git.md)
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
* [Docker 入门](tools/Docker.md)
|
||||||
|
* [一文搞懂 Docker 镜像的常用操作!](tools/Docker-Image.md)
|
||||||
|
|
||||||
|
## 资料
|
||||||
|
|
||||||
|
### 书单
|
||||||
|
|
||||||
|
- [Java程序员必备书单](data/java-recommended-books.md)
|
||||||
|
|
||||||
|
### Github榜单
|
||||||
|
|
||||||
|
- [Java 项目月榜单](github-trending/JavaGithubTrending.md)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
## 待办
|
||||||
|
|
||||||
|
- [x] [Java 8 新特性总结](./java/What's%20New%20in%20JDK8/Java8Tutorial.md)
|
||||||
|
- [x] [Java 8 新特性详解](./java/What's%20New%20in%20JDK8/Java8教程推荐.md)
|
||||||
|
- [ ] Java 多线程类别知识重构(---正在进行中---)
|
||||||
|
- [x] [BIO,NIO,AIO 总结 ](./java/BIO-NIO-AIO.md)
|
||||||
|
- [ ] Netty 总结(---正在进行中---)
|
||||||
|
- [ ] 数据结构总结重构(---正在进行中---)
|
||||||
|
|
||||||
|
## 公众号
|
||||||
|
|
||||||
|
- 如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。
|
||||||
|
- 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本公众号后台回复 **"Java面试突击"** 即可免费领取!
|
||||||
|
- 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="https://user-gold-cdn.xitu.io/2018/11/28/167598cd2e17b8ec?w=258&h=258&f=jpeg&s=27334" width=""/>
|
||||||
|
</p>
|
201
LICENSE
Normal file
201
LICENSE
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
478
README.md
478
README.md
@ -1,27 +1,39 @@
|
|||||||
<h1 align="center">Java 学习/面试指南</h1>
|
> JavaGuide 的Star数量虽然比较多,但是它的价值和含金量一定是不能和 Dubbo、Nacos这些优秀的国产开源项目比的。希望国内可以出更多优秀的开源项目!
|
||||||
|
>
|
||||||
|
> 另外,希望大家对面试不要抱有侥幸的心理,打铁还需自身硬! 我希望这个文档是为你学习 Java 指明方向,而不是用来应付面试用的。加油!奥利给!
|
||||||
|
|
||||||
|
**开始阅读之前必看** :
|
||||||
|
|
||||||
|
1. [完结撒花!JavaGuide面试突击版来啦!](./docs/javaguide面试突击版.md)
|
||||||
|
2. [JavaGuide重大更新记录](./docs/update-history.md)
|
||||||
|
|
||||||
|
更多原创内容和干货分享:
|
||||||
|
|
||||||
|
1. [公众号—JavaGuide](#公众号) : 最新原创文章+免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源)
|
||||||
|
|
||||||
|
Github用户如果访问速度缓慢的话,可以转移到[码云](https://gitee.com/SnailClimb/JavaGuide )查看,或者[在线阅读](https://snailclimb.gitee.io/javaguide )。
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3/logo - 副本.png" width=""/>
|
<a href="https://github.com/Snailclimb/JavaGuide" target="_blank">
|
||||||
|
<img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3/logo - 副本.png" width=""/>
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://snailclimb.gitee.io/javaguide"><img src="https://img.shields.io/badge/阅读-read-brightgreen.svg" alt="阅读"></a>
|
<a href="https://snailclimb.gitee.io/javaguide"><img src="https://img.shields.io/badge/阅读-read-brightgreen.svg" alt="阅读"></a>
|
||||||
<a href="#联系我"><img src="https://img.shields.io/badge/chat-微信群-blue.svg" alt="微信群"></a>
|
|
||||||
<a href="#公众号"><img src="https://img.shields.io/badge/%E5%85%AC%E4%BC%97%E5%8F%B7-JavaGuide-lightgrey.svg" alt="公众号"></a>
|
<a href="#公众号"><img src="https://img.shields.io/badge/%E5%85%AC%E4%BC%97%E5%8F%B7-JavaGuide-lightgrey.svg" alt="公众号"></a>
|
||||||
<a href="#公众号"><img src="https://img.shields.io/badge/PDF-Java面试突击-important.svg" alt="公众号"></a>
|
<a href="#公众号"><img src="https://img.shields.io/badge/PDF-Java面试突击-important.svg" alt="公众号"></a>
|
||||||
<a href="#投稿"><img src="https://img.shields.io/badge/support-投稿-critical.svg" alt="投稿"></a>
|
<a href="#投稿"><img src="https://img.shields.io/badge/support-投稿-critical.svg" alt="投稿"></a>
|
||||||
<h2 align="center">Special Sponsors</h2>
|
<a href="https://xiaozhuanlan.com/javainterview?rel=javaguide"><img src="https://img.shields.io/badge/Java-面试指南-important" alt="投稿"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<h3 align="center">Sponsor</h3>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://e.coding.net/?utm_source=JavaGuide" target="_blank">
|
<a href="https://mp.weixin.qq.com/s/li9_YXNVxan6Qgt3Q9FYqA">
|
||||||
<img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3/Coding Devops.png" width=""/>
|
<img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/WechatIMG1.png" style="margin: 0 auto;width:400px"/>
|
||||||
</a>
|
</a >
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
由于对文件目录结构进行了大幅度修改,所以如果遇到文章中有 Github 404 链接请 [联系我](#联系我)
|
|
||||||
|
|
||||||
推荐使用 <https://snailclimb.top/JavaGuide/> 在线阅读(访问速度慢的话,请使用 <https://snailclimb.gitee.io/javaguide> ),在线阅读内容本仓库同步一致。这种方式阅读的优势在于:有侧边栏阅读体验更好,Gitee pages 的访问速度相对来说也比较快。
|
|
||||||
|
|
||||||
## 目录
|
## 目录
|
||||||
|
|
||||||
- [Java](#java)
|
- [Java](#java)
|
||||||
@ -29,186 +41,307 @@
|
|||||||
- [容器](#容器)
|
- [容器](#容器)
|
||||||
- [并发](#并发)
|
- [并发](#并发)
|
||||||
- [JVM](#jvm)
|
- [JVM](#jvm)
|
||||||
- [I/O](#io)
|
- [其他](#其他)
|
||||||
- [Java 8](#java-8)
|
|
||||||
- [编程规范](#编程规范)
|
|
||||||
- [网络](#网络)
|
- [网络](#网络)
|
||||||
- [操作系统](#操作系统)
|
- [操作系统](#操作系统)
|
||||||
- [Linux相关](#linux相关)
|
- [Linux](#linux)
|
||||||
- [数据结构与算法](#数据结构与算法)
|
- **[数据结构与算法](#数据结构与算法)**
|
||||||
- [数据结构](#数据结构)
|
- [数据结构](#数据结构)
|
||||||
- [算法](#算法)
|
- [算法](#算法)
|
||||||
- [数据库](#数据库)
|
- [数据库](#数据库)
|
||||||
- [MySQL](#mysql)
|
- [MySQL](#mysql)
|
||||||
- [Redis](#redis)
|
- [Redis](#redis)
|
||||||
- [系统设计](#系统设计)
|
- [系统设计](#系统设计)
|
||||||
- [设计模式](#设计模式)
|
- [必知](#必知)
|
||||||
- [常用框架](#常用框架)
|
- [常用框架](#常用框架)
|
||||||
- [数据通信](#数据通信)
|
- [Spring](#spring)
|
||||||
- [网站架构](#网站架构)
|
- [SpringBoot](#springboot)
|
||||||
- [面试指南](#面试指南)
|
- [MyBatis](#mybatis)
|
||||||
- [备战面试](#备战面试)
|
- [认证授权(JWT、SSO)](#认证授权)
|
||||||
- [常见面试题总结](#常见面试题总结)
|
- [分布式](#分布式)
|
||||||
- [面经](#面经)
|
- [Elasticsearch(分布式搜索引擎)](#elasticsearch分布式搜索引擎)
|
||||||
- [工具](#工具)
|
- [RPC](#rpc)
|
||||||
|
- [消息队列](#消息队列)
|
||||||
|
- [API 网关](#api-网关)
|
||||||
|
- [分布式id](#分布式id)
|
||||||
|
- [分布式限流](#分布式限流)
|
||||||
|
- [分布式接口幂等性](#分布式接口幂等性)
|
||||||
|
- [数据库扩展](#数据库扩展)
|
||||||
|
- [ZooKeeper](#zookeeper)
|
||||||
|
- [大型网站架构](#大型网站架构)
|
||||||
|
- [性能测试](#性能测试)
|
||||||
|
- [高并发](#高并发)
|
||||||
|
- [高可用](#高可用)
|
||||||
|
- [微服务](#微服务)
|
||||||
|
- [Spring Cloud](#spring-cloud)
|
||||||
|
- [必会工具](#必会工具)
|
||||||
- [Git](#git)
|
- [Git](#git)
|
||||||
- [Docker](#Docker)
|
- [Docker](#docker)
|
||||||
- [资料](#资料)
|
- [面试指南](#面试指南)
|
||||||
- [书单](#书单)
|
- [Java学习常见问题汇总](#java学习常见问题汇总)
|
||||||
- [Github榜单](#Github榜单)
|
- [资源](#资源)
|
||||||
- [闲谈](#闲谈)
|
- [书单推荐](#书单推荐)
|
||||||
|
- [实战项目推荐](#实战项目推荐)
|
||||||
- [待办](#待办)
|
- [待办](#待办)
|
||||||
- [说明](#说明)
|
- [说明](#说明)
|
||||||
|
|
||||||
|
|
||||||
## Java
|
## Java
|
||||||
|
|
||||||
### 基础
|
### 基础
|
||||||
|
|
||||||
* [Java 基础知识回顾](docs/java/Java基础知识.md)
|
**基础知识系统总结:**
|
||||||
* [J2EE 基础知识回顾](docs/java/J2EE基础知识.md)
|
|
||||||
* [Collections 工具类和 Arrays 工具类常见方法](docs/java/Basis/Arrays%2CCollectionsCommonMethods.md)
|
1. **[Java 基础知识](docs/java/Java基础知识.md)**
|
||||||
* [Java常见关键字总结:static、final、this、super](docs/java/Basis/final、static、this、super.md)
|
2. **[Java 基础知识疑难点/易错点](docs/java/Java疑难点.md)**
|
||||||
|
3. [【选看】J2EE 基础知识](docs/java/J2EE基础知识.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)
|
||||||
|
|
||||||
|
**其他:**
|
||||||
|
|
||||||
|
1. [JAD反编译](docs/java/JAD反编译tricks.md)
|
||||||
|
|
||||||
### 容器
|
### 容器
|
||||||
|
|
||||||
* **常见问题总结:**
|
1. **[Java容器常见面试题/知识点总结](docs/java/collection/Java集合框架常见面试题.md)**
|
||||||
* [这几道Java集合框架面试题几乎必问](docs/java/这几道Java集合框架面试题几乎必问.md)
|
2. [ArrayList 源码](docs/java/collection/ArrayList.md) 、[LinkedList 源码](docs/java/collection/LinkedList.md) 、[HashMap(JDK1.8)源码](docs/java/collection/HashMap.md)
|
||||||
* [Java 集合框架常见面试题总结](docs/java/Java集合框架常见面试题总结.md)
|
|
||||||
* **源码分析:**
|
|
||||||
* [ArrayList 源码学习](docs/java/ArrayList.md)
|
|
||||||
* [【面试必备】透过源码角度一步一步带你分析 ArrayList 扩容机制](docs/java/ArrayList-Grow.md)
|
|
||||||
* [LinkedList 源码学习](docs/java/LinkedList.md)
|
|
||||||
* [HashMap(JDK1.8)源码学习](docs/java/HashMap.md)
|
|
||||||
|
|
||||||
### 并发
|
### 并发
|
||||||
|
|
||||||
* [并发编程面试必备:synchronized 关键字使用、底层原理、JDK1.6 之后的底层优化以及 和ReenTrantLock 的对比](docs/java/synchronized.md)
|
**[多线程学习指南](./docs/java/Multithread/多线程学习指南.md)**
|
||||||
* [并发编程面试必备:乐观锁与悲观锁](docs/essential-content-for-interview/面试必备之乐观锁与悲观锁.md)
|
|
||||||
* [并发编程面试必备:JUC 中的 Atomic 原子类总结](docs/java/Multithread/Atomic.md)
|
**面试题总结:**
|
||||||
* [并发编程面试必备:AQS 原理以及 AQS 同步组件总结](docs/java/Multithread/AQS.md)
|
|
||||||
* [BATJ都爱问的多线程面试题](docs/java/Multithread/BATJ都爱问的多线程面试题.md)
|
1. **[Java 并发基础常见面试题总结](docs/java/Multithread/JavaConcurrencyBasicsCommonInterviewQuestionsSummary.md)**
|
||||||
* [并发容器总结](docs/java/Multithread/并发容器总结.md)
|
2. **[Java 并发进阶常见面试题总结](docs/java/Multithread/JavaConcurrencyAdvancedCommonInterviewQuestions.md)**
|
||||||
|
|
||||||
|
**必备知识点:**
|
||||||
|
|
||||||
|
1. [并发容器总结](docs/java/Multithread/并发容器总结.md)
|
||||||
|
2. **[Java线程池学习总结](./docs/java/Multithread/java线程池学习总结.md)**
|
||||||
|
3. [乐观锁与悲观锁](docs/essential-content-for-interview/面试必备之乐观锁与悲观锁.md)
|
||||||
|
4. [JUC 中的 Atomic 原子类总结](docs/java/Multithread/Atomic.md)
|
||||||
|
5. [AQS 原理以及 AQS 同步组件总结](docs/java/Multithread/AQS.md)
|
||||||
|
|
||||||
### JVM
|
### JVM
|
||||||
|
|
||||||
* [可能是把Java内存区域讲的最清楚的一篇文章](docs/java/可能是把Java内存区域讲的最清楚的一篇文章.md)
|
1. **[Java内存区域](docs/java/jvm/Java内存区域.md)**
|
||||||
* [搞定JVM垃圾回收就是这么简单](docs/java/搞定JVM垃圾回收就是这么简单.md)
|
2. **[JVM垃圾回收](docs/java/jvm/JVM垃圾回收.md)**
|
||||||
* [《深入理解Java虚拟机》第2版学习笔记](docs/java/Java虚拟机(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)**
|
||||||
|
|
||||||
### I/O
|
### 其他
|
||||||
|
|
||||||
* [BIO,NIO,AIO 总结 ](docs/java/BIO-NIO-AIO.md)
|
1. **I/O** :[BIO,NIO,AIO 总结 ](docs/java/BIO-NIO-AIO.md)
|
||||||
* [Java IO 与 NIO系列文章](docs/java/Java%20IO与NIO.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)
|
||||||
### Java 8
|
4. Java编程规范:**[Java 编程规范以及优雅 Java 代码实践总结](docs/java/Java编程规范.md)** 、[告别编码5分钟,命名2小时!史上最全的Java命名规范参考!](docs/java/java-naming-conventions.md)
|
||||||
|
5. 设计模式 :[设计模式系列文章](docs/system-design/设计模式.md)
|
||||||
* [Java 8 新特性总结](docs/java/What's%20New%20in%20JDK8/Java8Tutorial.md)
|
|
||||||
* [Java 8 学习资源推荐](docs/java/What's%20New%20in%20JDK8/Java8教程推荐.md)
|
|
||||||
|
|
||||||
### 编程规范
|
|
||||||
|
|
||||||
- [Java 编程规范](docs/java/Java编程规范.md)
|
|
||||||
|
|
||||||
## 网络
|
## 网络
|
||||||
|
|
||||||
* [计算机网络常见面试题](docs/network/计算机网络.md)
|
1. [计算机网络常见面试题](docs/network/计算机网络.md)
|
||||||
* [计算机网络基础知识总结](docs/network/干货:计算机网络知识总结.md)
|
2. [计算机网络基础知识总结](docs/network/干货:计算机网络知识总结.md)
|
||||||
* [HTTPS中的TLS](docs/network/HTTPS中的TLS.md)
|
3. [HTTPS中的TLS](docs/network/HTTPS中的TLS.md)
|
||||||
|
|
||||||
## 操作系统
|
## 操作系统
|
||||||
|
|
||||||
### Linux相关
|
[最硬核的操作系统常见问题总结!](docs/operating-system/basis.md)
|
||||||
|
|
||||||
* [后端程序员必备的 Linux 基础知识](docs/operating-system/后端程序员必备的Linux基础知识.md)
|
### Linux
|
||||||
* [Shell 编程入门](docs/operating-system/Shell.md)
|
|
||||||
|
* [后端程序员必备的 Linux 基础知识](docs/operating-system/linux.md)
|
||||||
|
* [Shell 编程入门](docs/operating-system/Shell.md)
|
||||||
|
|
||||||
## 数据结构与算法
|
## 数据结构与算法
|
||||||
|
|
||||||
### 数据结构
|
### 数据结构
|
||||||
|
|
||||||
|
- [不了解布隆过滤器?一文给你整的明明白白!](docs/dataStructures-algorithms/data-structure/bloom-filter.md)
|
||||||
- [数据结构知识学习与面试](docs/dataStructures-algorithms/数据结构.md)
|
- [数据结构知识学习与面试](docs/dataStructures-algorithms/数据结构.md)
|
||||||
|
|
||||||
### 算法
|
### 算法
|
||||||
|
|
||||||
- [算法学习资源推荐](docs/dataStructures-algorithms/算法学习资源推荐.md)
|
- [硬核的算法学习书籍+资源推荐](docs/dataStructures-algorithms/算法学习资源推荐.md)
|
||||||
- [算法总结——几道常见的子符串算法题 ](docs/dataStructures-algorithms/几道常见的子符串算法题.md)
|
- 常见算法问题总结:
|
||||||
- [算法总结——几道常见的链表算法题 ](docs/dataStructures-algorithms/几道常见的链表算法题.md)
|
- [几道常见的字符串算法题总结 ](docs/dataStructures-algorithms/几道常见的子符串算法题.md)
|
||||||
- [剑指offer部分编程题](docs/dataStructures-algorithms/剑指offer部分编程题.md)
|
- [几道常见的链表算法题总结 ](docs/dataStructures-algorithms/几道常见的链表算法题.md)
|
||||||
- [公司真题](docs/dataStructures-algorithms/公司真题.md)
|
- [剑指offer部分编程题](docs/dataStructures-algorithms/剑指offer部分编程题.md)
|
||||||
- [回溯算法经典案例之N皇后问题](./dataStructures-algorithms/Backtracking-NQueens.md)
|
- [公司真题](docs/dataStructures-algorithms/公司真题.md)
|
||||||
|
- [回溯算法经典案例之N皇后问题](docs/dataStructures-algorithms/Backtracking-NQueens.md)
|
||||||
|
|
||||||
## 数据库
|
## 数据库
|
||||||
|
|
||||||
### MySQL
|
### MySQL
|
||||||
|
|
||||||
* [MySQL 学习与面试](docs/database/MySQL.md)
|
**总结:**
|
||||||
* [一千行MySQL学习笔记](docs/database/一千行MySQL命令.md)
|
|
||||||
* [【思维导图-索引篇】搞定数据库索引就是这么简单](docs/database/MySQL%20Index.md)
|
1. **[【推荐】MySQL/数据库 知识点总结](docs/database/MySQL.md)**
|
||||||
* [事务隔离级别(图文详解)](docs/database/事务隔离级别(图文详解).md)
|
2. **[阿里巴巴开发手册数据库部分的一些最佳实践](docs/database/阿里巴巴开发手册数据库部分的一些最佳实践.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)
|
||||||
|
4. **[关于数据库中如何存储时间的一点思考](docs/database/关于数据库存储时间的一点思考.md)**
|
||||||
|
|
||||||
### Redis
|
### Redis
|
||||||
|
|
||||||
* [Redis 总结](docs/database/Redis/Redis.md)
|
* [Redis 常见问题总结](docs/database/Redis/Redis.md)
|
||||||
* [Redlock分布式锁](docs/database/Redis/Redlock分布式锁.md)
|
* **Redis 系列文章合集:**
|
||||||
* [如何做可靠的分布式锁,Redlock真的可行么](docs/database/Redis/如何做可靠的分布式锁,Redlock真的可行么.md)
|
1. [5种基本数据结构](docs/database/Redis/redis-collection/Redis(1)——5种基本数据结构.md)
|
||||||
|
2. [跳跃表](docs/database/Redis/redis-collection/Redis(2)——跳跃表.md)
|
||||||
|
3. [分布式锁深入探究](docs/database/Redis/redis-collection/Redis(3)——分布式锁深入探究.md) 、 [Redlock分布式锁](docs/database/Redis/Redlock分布式锁.md) 、[如何做可靠的分布式锁,Redlock真的可行么](docs/database/Redis/如何做可靠的分布式锁,Redlock真的可行么.md)
|
||||||
|
4. [神奇的HyperLoglog解决统计问题](docs/database/Redis/redis-collection/Reids(4)——神奇的HyperLoglog解决统计问题.md)
|
||||||
|
5. [亿级数据过滤和布隆过滤器](docs/database/Redis/redis-collection/Redis(5)——亿级数据过滤和布隆过滤器.md)
|
||||||
|
6. [GeoHash查找附近的人](docs/database/Redis/redis-collection/Redis(6)——GeoHash查找附近的人.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)
|
||||||
|
|
||||||
## 系统设计
|
## 系统设计
|
||||||
|
|
||||||
### 设计模式
|
### 必知
|
||||||
|
|
||||||
- [设计模式系列文章](docs/system-design/设计模式.md)
|
1. **[RestFul API 简明教程](docs/system-design/restful-api.md)**
|
||||||
|
|
||||||
### 常用框架
|
### 常用框架
|
||||||
|
|
||||||
#### Spring
|
#### Spring/SpringBoot
|
||||||
|
|
||||||
- [Spring 学习与面试](docs/system-design/framework/Spring学习与面试.md)
|
1. [Spring 学习与面试(待重构)](docs/system-design/framework/spring/Spring.md)
|
||||||
- [Spring中bean的作用域与生命周期](docs/system-design/framework/SpringBean.md)
|
2. **[Spring 常见问题总结](docs/system-design/framework/spring/SpringInterviewQuestions.md)**
|
||||||
- [SpringMVC 工作原理详解](docs/system-design/framework/SpringMVC%20%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86%E8%AF%A6%E8%A7%A3.md)
|
3. **[Spring/Spring常用注解总结!安排!](./docs/system-design/framework/spring/spring-annotations.md)**
|
||||||
|
4. **[SpringBoot 指南/常见面试题总结](https://github.com/Snailclimb/springboot-guide)**
|
||||||
|
5. [Spring中 Bean 的作用域与生命周期](docs/system-design/framework/spring/SpringBean.md)
|
||||||
|
6. [SpringMVC 工作原理详解](docs/system-design/framework/spring/SpringMVC-Principle.md)
|
||||||
|
7. [Spring中都用到了那些设计模式?](docs/system-design/framework/spring/Spring-Design-Patterns.md)
|
||||||
|
|
||||||
|
#### MyBatis
|
||||||
|
|
||||||
|
- [MyBatis常见面试题总结](docs/system-design/framework/mybatis/mybatis-interview.md)
|
||||||
|
|
||||||
|
### 认证授权
|
||||||
|
|
||||||
|
**[认证授权基础:搞清Authentication,Authorization以及Cookie、Session、Token、OAuth 2、SSO](docs/system-design/authority-certification/basis-of-authority-certification.md)**
|
||||||
|
|
||||||
|
#### 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)**
|
||||||
|
|
||||||
|
#### SSO(单点登录)
|
||||||
|
|
||||||
|
SSO(Single Sign On)即单点登录说的是用户登陆多个子系统的其中一个就有权访问与其相关的其他系统。举个例子我们在登陆了京东金融之后,我们同时也成功登陆京东的京东超市、京东家电等子系统。相关阅读:**[SSO 单点登录看这篇就够了!](docs/system-design/authority-certification/sso.md)**
|
||||||
|
|
||||||
|
### 分布式
|
||||||
|
|
||||||
|
[分布式相关概念入门](docs/system-design/website-architecture/分布式.md)
|
||||||
|
|
||||||
|
#### Elasticsearch(分布式搜索引擎)
|
||||||
|
|
||||||
|
提高搜索效率。常见于电商购物网站的商品搜索于分类。
|
||||||
|
|
||||||
|
代办......
|
||||||
|
|
||||||
|
#### RPC
|
||||||
|
|
||||||
|
让调用远程服务调用像调用本地方法那样简单。
|
||||||
|
|
||||||
|
- [Dubbo 总结:关于 Dubbo 的重要知识点](docs/system-design/data-communication/dubbo.md)
|
||||||
|
- [服务之间的调用为啥不直接用 HTTP 而用 RPC?](docs/system-design/data-communication/why-use-rpc.md)
|
||||||
|
|
||||||
|
#### 消息队列
|
||||||
|
|
||||||
|
消息队列在分布式系统中主要是为了接耦和削峰。相关阅读: **[消息队列总结](docs/system-design/data-communication/message-queue.md)** 。
|
||||||
|
|
||||||
|
**RabbitMQ:**
|
||||||
|
|
||||||
|
1. [RabbitMQ 入门](docs/system-design/data-communication/rabbitmq.md)
|
||||||
|
|
||||||
|
**RocketMQ:**
|
||||||
|
|
||||||
|
1. [RocketMQ 入门](docs/system-design/data-communication/RocketMQ.md)
|
||||||
|
2. [RocketMQ的几个简单问题与答案](docs/system-design/data-communication/RocketMQ-Questions.md)
|
||||||
|
|
||||||
|
**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)
|
||||||
|
|
||||||
|
#### API 网关
|
||||||
|
|
||||||
|
网关主要用于请求转发、安全认证、协议转换、容灾。
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
#### 分布式接口幂等性
|
||||||
|
|
||||||
#### ZooKeeper
|
#### ZooKeeper
|
||||||
|
|
||||||
- [可能是把 ZooKeeper 概念讲的最清楚的一篇文章](docs/system-design/framework/ZooKeeper.md)
|
> 前两篇文章可能有内容重合部分,推荐都看一遍。
|
||||||
- [ZooKeeper 数据模型和常见命令了解一下,速度收藏!](docs/system-design/framework/ZooKeeper数据模型和常见命令.md)
|
|
||||||
|
|
||||||
### 数据通信
|
1. [【入门】ZooKeeper 相关概念总结](docs/system-design/framework/ZooKeeper.md)
|
||||||
|
2. [【进阶】Zookeeper 原理简单入门!](docs/system-design/framework/ZooKeeper-plus.md)
|
||||||
|
3. [【拓展】ZooKeeper 数据模型和常见命令](docs/system-design/framework/ZooKeeper数据模型和常见命令.md)
|
||||||
|
|
||||||
- [数据通信(RESTful、RPC、消息队列)相关知识点总结](docs/system-design/data-communication/数据通信(RESTful、RPC、消息队列).md)
|
#### 其他
|
||||||
- [Dubbo 总结:关于 Dubbo 的重要知识点](docs/system-design/data-communication/dubbo.md)
|
|
||||||
- [消息队列总结:新手也能看懂,消息队列其实很简单](docs/system-design/data-communication/message-queue.md)
|
|
||||||
- [一文搞懂 RabbitMQ 的重要概念以及安装](docs/system-design/data-communication/rabbitmq.md)
|
|
||||||
|
|
||||||
### 网站架构
|
- 接口幂等性(代办):分布式系统必须要考虑接口的幂等性。
|
||||||
|
|
||||||
|
#### 数据库扩展
|
||||||
|
|
||||||
|
读写分离、分库分表。
|
||||||
|
|
||||||
|
代办.....
|
||||||
|
|
||||||
|
### 大型网站架构
|
||||||
|
|
||||||
- [一文读懂分布式应该学什么](docs/system-design/website-architecture/分布式.md)
|
|
||||||
- [8 张图读懂大型网站技术架构](docs/system-design/website-architecture/8%20张图读懂大型网站技术架构.md)
|
- [8 张图读懂大型网站技术架构](docs/system-design/website-architecture/8%20张图读懂大型网站技术架构.md)
|
||||||
- [【面试精选】关于大型网站系统架构你不得不懂的10个问题](docs/system-design/website-architecture/【面试精选】关于大型网站系统架构你不得不懂的10个问题.md)
|
- [关于大型网站系统架构你不得不懂的10个问题](docs/system-design/website-architecture/关于大型网站系统架构你不得不懂的10个问题.md)
|
||||||
|
|
||||||
## 面试指南
|
#### 性能测试
|
||||||
|
|
||||||
### 备战面试
|
- [后端程序员也要懂的性能测试知识](https://articles.zsxq.com/id_lwl39teglv3d.html) (知识星球)
|
||||||
|
|
||||||
* [【备战面试1】程序员的简历就该这样写](docs/essential-content-for-interview/PreparingForInterview/程序员的简历之道.md)
|
#### 高并发
|
||||||
* [【备战面试2】初出茅庐的程序员该如何准备面试?](docs/essential-content-for-interview/PreparingForInterview/interviewPrepare.md)
|
|
||||||
* [【备战面试3】7个大部分程序员在面试前很关心的问题](docs/essential-content-for-interview/PreparingForInterview/JavaProgrammerNeedKnow.md)
|
|
||||||
* [【备战面试4】Java程序员必备书单](docs/essential-content-for-interview/PreparingForInterview/books.md)
|
|
||||||
* [【备战面试5】Github上开源的Java面试/学习相关的仓库推荐](docs/essential-content-for-interview/PreparingForInterview/JavaInterviewLibrary.md)
|
|
||||||
* [【备战面试6】如果面试官问你“你有什么问题问我吗?”时,你该如何回答](docs/essential-content-for-interview/PreparingForInterview/如果面试官问你“你有什么问题问我吗?”时,你该如何回答.md)
|
|
||||||
* [【备战面试7】美团面试常见问题总结(附详解答案)](docs/essential-content-for-interview/PreparingForInterview/美团面试常见问题总结.md)
|
|
||||||
|
|
||||||
### 常见面试题总结
|
待办......
|
||||||
|
|
||||||
* [第一周(2018-8-7)](docs/essential-content-for-interview/MostCommonJavaInterviewQuestions/第一周(2018-8-7).md) (为什么 Java 中只有值传递、==与equals、 hashCode与equals)
|
#### 高可用
|
||||||
* [第二周(2018-8-13)](docs/essential-content-for-interview/MostCommonJavaInterviewQuestions/第二周(2018-8-13).md)(String和StringBuffer、StringBuilder的区别是什么?String为什么是不可变的?、什么是反射机制?反射机制的应用场景有哪些?......)
|
|
||||||
* [第三周(2018-08-22)](docs/java/这几道Java集合框架面试题几乎必问.md) (Arraylist 与 LinkedList 异同、ArrayList 与 Vector 区别、HashMap的底层实现、HashMap 和 Hashtable 的区别、HashMap 的长度为什么是2的幂次方、HashSet 和 HashMap 区别、ConcurrentHashMap 和 Hashtable 的区别、ConcurrentHashMap线程安全的具体实现方式/底层具体实现、集合框架底层数据结构总结)
|
|
||||||
* [第四周(2018-8-30).md](docs/essential-content-for-interview/MostCommonJavaInterviewQuestions/第四周(2018-8-30).md) (主要内容是几道面试常问的多线程基础题。)
|
|
||||||
|
|
||||||
### 面经
|
高可用描述的是一个系统在大部分时间都是可用的,可以为我们提供服务的。高可用代表系统即使在发生硬件故障或者系统升级的时候,服务仍然是可用的 。相关阅读: **《[如何设计一个高可用系统?要考虑哪些地方?](docs/system-design/website-architecture/如何设计一个高可用系统?要考虑哪些地方?.md)》** 。
|
||||||
|
|
||||||
- [5面阿里,终获offer(2018年秋招)](docs/essential-content-for-interview/BATJrealInterviewExperience/5面阿里,终获offer.md)
|
### 微服务
|
||||||
|
|
||||||
## 工具
|
#### Spring Cloud
|
||||||
|
|
||||||
|
- [ 大白话入门 Spring Cloud](docs/system-design/micro-service/spring-cloud.md)
|
||||||
|
|
||||||
|
## 必会工具
|
||||||
|
|
||||||
### Git
|
### Git
|
||||||
|
|
||||||
@ -216,38 +349,64 @@
|
|||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
|
|
||||||
* [Docker 入门](docs/tools/Docker.md)
|
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)
|
||||||
|
|
||||||
- [Java程序员必备书单](docs/essential-content-for-interview/PreparingForInterview/books.md)
|
## 面试指南
|
||||||
|
|
||||||
### Github榜单
|
> 这部分很多内容比如大厂面经、真实面经分析被移除,详见[完结撒花!JavaGuide面试突击版来啦!](./docs/javaguide面试突击版.md)。
|
||||||
|
|
||||||
- [Java 项目月榜单](docs/github-trending/JavaGithubTrending.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学习常见问题汇总
|
||||||
|
|
||||||
* [如何提问](docs/chat/如何提问.md)
|
1. [Java学习路线和方法推荐](docs/questions/java-learning-path-and-methods.md)
|
||||||
* [选择技术方向都要考虑哪些因素](docs/chat/选择技术方向都要考虑哪些因素.md)
|
2. [Java培训四个月能学会吗?](docs/questions/java-training-4-month.md)
|
||||||
* [结束了我短暂的秋招,说点自己的感受](docs/chat/2018%20%E7%A7%8B%E6%8B%9B.md)
|
3. [新手学习Java,有哪些Java相关的博客,专栏,和技术学习网站推荐?](docs/questions/java-learning-website-blog.md)
|
||||||
|
4. [Java 还是大数据,你需要了解这些东西!](docs/questions/java-big-data)
|
||||||
|
5. [Java 后台开发/大数据?你需要了解这些东西!](https://articles.zsxq.com/id_wto1iwd5g72o.html)(知识星球)
|
||||||
|
|
||||||
|
## 资源
|
||||||
|
|
||||||
|
### 书单推荐
|
||||||
|
|
||||||
|
- **[Java程序员必备书单](docs/books/java.md)**
|
||||||
|
|
||||||
|
### 实战项目推荐
|
||||||
|
|
||||||
|
- **[Java、SpringBoot实战项目推荐](https://github.com/Snailclimb/awesome-java#实战项目)**
|
||||||
|
|
||||||
|
### 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)
|
||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
## 待办
|
## 待办
|
||||||
|
|
||||||
- [x] [Java 8 新特性总结](docs/java/What's%20New%20in%20JDK8/Java8Tutorial.md)
|
- [ ] Netty 总结(---正在进行中---)
|
||||||
- [ ] Java 8 新特性详解
|
- [ ] 数据结构总结重构(---正在进行中---)
|
||||||
- [ ] Java 多线程类别知识重构
|
|
||||||
- [x] [BIO,NIO,AIO 总结 ](docs/java/BIO-NIO-AIO.md)
|
|
||||||
- [ ] Netty 总结
|
|
||||||
- [ ] 数据结构总结重构
|
|
||||||
|
|
||||||
## 说明
|
## 说明
|
||||||
|
|
||||||
### 介绍
|
开源项目在于大家的参与,这才使得它的价值得到提升。感谢🙏有你!
|
||||||
|
|
||||||
|
### JavaGuide介绍
|
||||||
|
|
||||||
|
开源 JavaGuide 初始想法源于自己的个人那一段比较迷茫的学习经历。主要目的是为了通过这个开源平台来帮助一些在学习 Java 或者面试过程中遇到问题的小伙伴。
|
||||||
|
|
||||||
* **对于 Java 初学者来说:** 本文档倾向于给你提供一个比较详细的学习路径,让你对于Java整体的知识体系有一个初步认识。另外,本文的一些文章
|
* **对于 Java 初学者来说:** 本文档倾向于给你提供一个比较详细的学习路径,让你对于Java整体的知识体系有一个初步认识。另外,本文的一些文章
|
||||||
也是你学习和复习 Java 知识不错的实践;
|
也是你学习和复习 Java 知识不错的实践;
|
||||||
@ -255,7 +414,7 @@
|
|||||||
|
|
||||||
Markdown 格式参考:[Github Markdown格式](https://guides.github.com/features/mastering-markdown/),表情素材来自:[EMOJI CHEAT SHEET](https://www.webpagefx.com/tools/emoji-cheat-sheet/)。
|
Markdown 格式参考:[Github Markdown格式](https://guides.github.com/features/mastering-markdown/),表情素材来自:[EMOJI CHEAT SHEET](https://www.webpagefx.com/tools/emoji-cheat-sheet/)。
|
||||||
|
|
||||||
利用 docsify 生成文档部署在 Github pages: [docsify 官网介绍](https://docsify.js.org/#/)
|
利用 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) 。
|
||||||
|
|
||||||
### 关于转载
|
### 关于转载
|
||||||
|
|
||||||
@ -265,40 +424,39 @@ Markdown 格式参考:[Github Markdown格式](https://guides.github.com/featur
|
|||||||
|
|
||||||
1. 笔记内容大多是手敲,所以难免会有笔误,你可以帮我找错别字。
|
1. 笔记内容大多是手敲,所以难免会有笔误,你可以帮我找错别字。
|
||||||
2. 很多知识点我可能没有涉及到,所以你可以对其他知识点进行补充。
|
2. 很多知识点我可能没有涉及到,所以你可以对其他知识点进行补充。
|
||||||
3. 现有的知识点难免存在不完善或者错误,所以你可以对已有知识点的修改/补充。
|
3. 现有的知识点难免存在不完善或者错误,所以你可以对已有知识点进行修改/补充。
|
||||||
|
|
||||||
### 为什么要做这个开源文档?
|
|
||||||
|
|
||||||
初始想法源于自己的个人那一段比较迷茫的学习经历。主要目的是为了通过这个开源平台来帮助一些在学习 Java 或者面试过程中遇到问题的小伙伴。
|
|
||||||
|
|
||||||
### 投稿
|
|
||||||
|
|
||||||
由于我个人能力有限,很多知识点我可能没有涉及到,所以你可以对其他知识点进行补充。大家也可以对自己的文章进行自荐,对于不错的文章不仅可以成功在本仓库展示出来更可以获得作者送出的 50 元左右的任意书籍进行奖励(当然你也可以直接折现50元)。
|
|
||||||
|
|
||||||
### 联系我
|
### 联系我
|
||||||
|
|
||||||
添加我的微信备注“Github”,回复关键字 **“加群”** 即可入群。
|

|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### Contributor
|
### Contributor
|
||||||
|
|
||||||
下面是笔主收集的一些对本仓库提过有价值的pr或者issue的朋友,人数较多,如果你也对本仓库提过不错的pr或者issue的话,你可以加我的微信与我联系。下面的排名不分先后!
|
下面是笔主收集的一些对本仓库提过有价值的pr或者issue的朋友,人数较多,如果你也对本仓库提过不错的pr或者issue的话,你可以加我的微信与我联系。下面的排名不分先后!
|
||||||
|
|
||||||
|
|
||||||
<a href="https://github.com/spikesp">
|
|
||||||
<img src="https://avatars0.githubusercontent.com/u/12581996?s=460&v=4" width="45px"></a>
|
|
||||||
<a href="https://github.com/fanofxiaofeng">
|
<a href="https://github.com/fanofxiaofeng">
|
||||||
<img src="https://avatars0.githubusercontent.com/u/3983683?s=460&v=4" width="45px"></a>
|
<img src="https://avatars0.githubusercontent.com/u/3983683?s=460&v=4" width="45px">
|
||||||
<a href="https://github.com/Gene1994">
|
|
||||||
<img src="https://avatars3.githubusercontent.com/u/24930369?s=460&v=4" width="45px">
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/illusorycloud">
|
|
||||||
<img src="https://avatars3.githubusercontent.com/u/31980412?s=460&v=4" width="45px">
|
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/LiWenGu">
|
<a href="https://github.com/LiWenGu">
|
||||||
<img src="https://avatars0.githubusercontent.com/u/15909210?s=460&v=4" width="45px">
|
<img src="https://avatars0.githubusercontent.com/u/15909210?s=460&v=4" width="45px">
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://github.com/fanchenggang">
|
||||||
|
<img src="https://avatars2.githubusercontent.com/u/8225921?s=460&v=4" width="45px">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/Rustin-Liu">
|
||||||
|
<img src="https://avatars2.githubusercontent.com/u/29879298?s=400&v=4" width="45px">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="https://github.com/ipofss">
|
||||||
|
<img src="https://avatars1.githubusercontent.com/u/5917359?s=460&v=4" width="45px"></a>
|
||||||
|
<a href="https://github.com/Gene1994">
|
||||||
|
<img src="https://avatars3.githubusercontent.com/u/24930369?s=460&v=4" width="45px">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/spikesp">
|
||||||
|
<img src="https://avatars0.githubusercontent.com/u/12581996?s=460&v=4" width="45px"></a>
|
||||||
|
<a href="https://github.com/illusorycloud">
|
||||||
|
<img src="https://avatars3.githubusercontent.com/u/31980412?s=460&v=4" width="45px">
|
||||||
|
</a>
|
||||||
<a href="https://github.com/kinglaw1204">
|
<a href="https://github.com/kinglaw1204">
|
||||||
<img src="https://avatars1.githubusercontent.com/u/20039931?s=460&v=4" width="45px">
|
<img src="https://avatars1.githubusercontent.com/u/20039931?s=460&v=4" width="45px">
|
||||||
</a>
|
</a>
|
||||||
@ -320,6 +478,12 @@ Markdown 格式参考:[Github Markdown格式](https://guides.github.com/featur
|
|||||||
<a href="https://github.com/yuechuanx">
|
<a href="https://github.com/yuechuanx">
|
||||||
<img src="https://avatars3.githubusercontent.com/u/19339293?s=460&v=4" width="45px">
|
<img src="https://avatars3.githubusercontent.com/u/19339293?s=460&v=4" width="45px">
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://github.com/cnLGMing">
|
||||||
|
<img src="https://avatars2.githubusercontent.com/u/15910705?s=460&v=4" width="45px">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/fanchenggang">
|
||||||
|
<img src="https://avatars0.githubusercontent.com/u/20358122?s=460&v=4" width="45px">
|
||||||
|
</a>
|
||||||
|
|
||||||
### 公众号
|
### 公众号
|
||||||
|
|
||||||
@ -327,6 +491,8 @@ Markdown 格式参考:[Github Markdown格式](https://guides.github.com/featur
|
|||||||
|
|
||||||
**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取!
|
**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取!
|
||||||
|
|
||||||
**Java工程师必备学习资源:** 一些Java工程师常用学习资源[公众号](#公众号)后台回复关键字 **“1”** 即可免费无套路获取。
|
**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||

|
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3logo-透明.png" width=""/>
|
<img src="./media/pictures/logo.png" width="200" height="200"/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
<h1 align="center">Java 学习/面试指南</h1>
|
<h1 align="center">Java 学习/面试指南</h1>
|
||||||
|
|
||||||
[常用资源](https://shimo.im/docs/MuiACIg1HlYfVxrj/)
|
[常用资源](https://shimo.im/docs/MuiACIg1HlYfVxrj/)
|
||||||
[GitHub](<https://github.com/Snailclimb/JavaGuide>)
|
[GitHub](<https://github.com/Snailclimb/JavaGuide>)
|
||||||
[开始阅读](#java)
|
[开始阅读](#java)
|
||||||
|
|
||||||
|
|
2
code/java/ThreadPoolExecutorDemo/.idea/.gitignore
generated
vendored
Normal file
2
code/java/ThreadPoolExecutorDemo/.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/workspace.xml
|
16
code/java/ThreadPoolExecutorDemo/.idea/checkstyle-idea.xml
generated
Normal file
16
code/java/ThreadPoolExecutorDemo/.idea/checkstyle-idea.xml
generated
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CheckStyle-IDEA">
|
||||||
|
<option name="configuration">
|
||||||
|
<map>
|
||||||
|
<entry key="checkstyle-version" value="8.25" />
|
||||||
|
<entry key="copy-libs" value="false" />
|
||||||
|
<entry key="location-0" value="BUNDLED:(bundled):Sun Checks" />
|
||||||
|
<entry key="location-1" value="BUNDLED:(bundled):Google Checks" />
|
||||||
|
<entry key="scan-before-checkin" value="false" />
|
||||||
|
<entry key="scanscope" value="JavaOnly" />
|
||||||
|
<entry key="suppress-errors" value="false" />
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
36
code/java/ThreadPoolExecutorDemo/.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
36
code/java/ThreadPoolExecutorDemo/.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="JavaDoc" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="TOP_LEVEL_CLASS_OPTIONS">
|
||||||
|
<value>
|
||||||
|
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
|
||||||
|
<option name="REQUIRED_TAGS" value="" />
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
<option name="INNER_CLASS_OPTIONS">
|
||||||
|
<value>
|
||||||
|
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
|
||||||
|
<option name="REQUIRED_TAGS" value="" />
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
<option name="METHOD_OPTIONS">
|
||||||
|
<value>
|
||||||
|
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
|
||||||
|
<option name="REQUIRED_TAGS" value="@return@param@throws or @exception" />
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
<option name="FIELD_OPTIONS">
|
||||||
|
<value>
|
||||||
|
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
|
||||||
|
<option name="REQUIRED_TAGS" value="" />
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
<option name="IGNORE_DEPRECATED" value="false" />
|
||||||
|
<option name="IGNORE_JAVADOC_PERIOD" value="true" />
|
||||||
|
<option name="IGNORE_DUPLICATED_THROWS" value="false" />
|
||||||
|
<option name="IGNORE_POINT_TO_ITSELF" value="false" />
|
||||||
|
<option name="myAdditionalJavadocTags" value="date" />
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
6
code/java/ThreadPoolExecutorDemo/.idea/misc.xml
generated
Normal file
6
code/java/ThreadPoolExecutorDemo/.idea/misc.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||||
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
|
</component>
|
||||||
|
</project>
|
8
code/java/ThreadPoolExecutorDemo/.idea/modules.xml
generated
Normal file
8
code/java/ThreadPoolExecutorDemo/.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/ThreadPoolExecutorDemo.iml" filepath="$PROJECT_DIR$/ThreadPoolExecutorDemo.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
124
code/java/ThreadPoolExecutorDemo/.idea/uiDesigner.xml
generated
Normal file
124
code/java/ThreadPoolExecutorDemo/.idea/uiDesigner.xml
generated
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Palette2">
|
||||||
|
<group name="Swing">
|
||||||
|
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.png" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
|
||||||
|
</item>
|
||||||
|
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.png" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.png" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.png" removable="false" auto-create-binding="false" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.png" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
|
||||||
|
<initial-values>
|
||||||
|
<property name="text" value="Button" />
|
||||||
|
</initial-values>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.png" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
|
||||||
|
<initial-values>
|
||||||
|
<property name="text" value="RadioButton" />
|
||||||
|
</initial-values>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.png" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
|
||||||
|
<initial-values>
|
||||||
|
<property name="text" value="CheckBox" />
|
||||||
|
</initial-values>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.png" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
|
||||||
|
<initial-values>
|
||||||
|
<property name="text" value="Label" />
|
||||||
|
</initial-values>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.png" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
|
||||||
|
<preferred-size width="150" height="-1" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.png" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
|
||||||
|
<preferred-size width="150" height="-1" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.png" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
|
||||||
|
<preferred-size width="150" height="-1" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.png" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||||
|
<preferred-size width="150" height="50" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.png" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||||
|
<preferred-size width="150" height="50" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.png" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||||
|
<preferred-size width="150" height="50" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.png" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.png" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||||
|
<preferred-size width="150" height="50" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.png" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
|
||||||
|
<preferred-size width="150" height="50" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.png" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||||
|
<preferred-size width="150" height="50" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.png" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
|
||||||
|
<preferred-size width="200" height="200" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.png" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
|
||||||
|
<preferred-size width="200" height="200" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.png" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.png" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.png" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.png" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.png" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
|
||||||
|
<preferred-size width="-1" height="20" />
|
||||||
|
</default-constraints>
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.png" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
|
||||||
|
</item>
|
||||||
|
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.png" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||||
|
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
|
||||||
|
</item>
|
||||||
|
</group>
|
||||||
|
</component>
|
||||||
|
</project>
|
6
code/java/ThreadPoolExecutorDemo/.idea/vcs.xml
generated
Normal file
6
code/java/ThreadPoolExecutorDemo/.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$/../../.." vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
11
code/java/ThreadPoolExecutorDemo/ThreadPoolExecutorDemo.iml
Normal file
11
code/java/ThreadPoolExecutorDemo/ThreadPoolExecutorDemo.iml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="JAVA_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,49 @@
|
|||||||
|
package callable;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static common.ThreadPoolConstants.CORE_POOL_SIZE;
|
||||||
|
import static common.ThreadPoolConstants.KEEP_ALIVE_TIME;
|
||||||
|
import static common.ThreadPoolConstants.MAX_POOL_SIZE;
|
||||||
|
import static common.ThreadPoolConstants.QUEUE_CAPACITY;
|
||||||
|
|
||||||
|
public class CallableDemo {
|
||||||
|
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());
|
||||||
|
|
||||||
|
List<Future<String>> futureList = new ArrayList<>();
|
||||||
|
Callable<String> callable = new MyCallable();
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
//提交任务到线程池
|
||||||
|
Future<String> future = executor.submit(callable);
|
||||||
|
//将返回值 future 添加到 list,我们可以通过 future 获得 执行 Callable 得到的返回值
|
||||||
|
futureList.add(future);
|
||||||
|
}
|
||||||
|
for (Future<String> fut : futureList) {
|
||||||
|
try {
|
||||||
|
System.out.println(new Date() + "::" + fut.get());
|
||||||
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//关闭线程池
|
||||||
|
executor.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
|||||||
|
package callable;
|
||||||
|
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
|
||||||
|
public class MyCallable implements Callable<String> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String call() throws Exception {
|
||||||
|
Thread.sleep(1000);
|
||||||
|
//返回执行当前 Callable 的线程名字
|
||||||
|
return Thread.currentThread().getName();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package common;
|
||||||
|
|
||||||
|
public class ThreadPoolConstants {
|
||||||
|
public static final int CORE_POOL_SIZE = 5;
|
||||||
|
public static final int MAX_POOL_SIZE = 10;
|
||||||
|
public static final int QUEUE_CAPACITY = 100;
|
||||||
|
public static final Long KEEP_ALIVE_TIME = 1L;
|
||||||
|
private ThreadPoolConstants(){
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
package threadPoolExecutor;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
package threadPoolExecutor;
|
||||||
|
|
||||||
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static common.ThreadPoolConstants.CORE_POOL_SIZE;
|
||||||
|
import static common.ThreadPoolConstants.KEEP_ALIVE_TIME;
|
||||||
|
import static common.ThreadPoolConstants.MAX_POOL_SIZE;
|
||||||
|
import static common.ThreadPoolConstants.QUEUE_CAPACITY;
|
||||||
|
|
||||||
|
|
||||||
|
public class ThreadPoolExecutorDemo {
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
201
docs/HomePage.md
201
docs/HomePage.md
@ -1,201 +0,0 @@
|
|||||||
|
|
||||||
对于复习 Linux 的朋友,推荐一下刘超(网易杭州研究院云计算技术部首席架构师)老师的《趣谈Linux操作系统——像故事一样的操作系统入门课》,这门课程是刚上新的,目前正在优惠,看过这位老师的《趣谈网络协议》的朋友应该都知道他,非常厉害,课程内容非常棒。[点击了解详情](https://shimo.im/docs/Jp998jwxhHwTp3sq/)。
|
|
||||||
|
|
||||||
<h2 align="center">Special Sponsors</h2>
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://e.coding.net/?utm_source=JavaGuide" target="_blank">
|
|
||||||
<img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3/Coding Devops.png" width=""/>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
## Java
|
|
||||||
|
|
||||||
### 基础
|
|
||||||
|
|
||||||
* [Java 基础知识回顾](./java/Java基础知识.md)
|
|
||||||
* [J2EE 基础知识回顾](./java/J2EE基础知识.md)
|
|
||||||
* [Collections 工具类和 Arrays 工具类常见方法](./java/Basis/Arrays%2CCollectionsCommonMethods.md)
|
|
||||||
* [Java常见关键字总结:static、final、this、super](./java/Basis/final、static、this、super.md)
|
|
||||||
|
|
||||||
### 容器
|
|
||||||
|
|
||||||
* **常见问题总结:**
|
|
||||||
* [这几道Java集合框架面试题几乎必问](./java/这几道Java集合框架面试题几乎必问.md)
|
|
||||||
* [Java 集合框架常见面试题总结](./java/Java集合框架常见面试题总结.md)
|
|
||||||
* **源码分析:**
|
|
||||||
* [ArrayList 源码学习](./java/ArrayList.md)
|
|
||||||
* [【面试必备】透过源码角度一步一步带你分析 ArrayList 扩容机制](./java/ArrayList-Grow.md)
|
|
||||||
* [LinkedList 源码学习](./java/LinkedList.md)
|
|
||||||
* [HashMap(JDK1.8)源码学习](./java/HashMap.md)
|
|
||||||
|
|
||||||
### 并发
|
|
||||||
|
|
||||||
* [并发编程面试必备:synchronized 关键字使用、底层原理、JDK1.6 之后的底层优化以及 和ReenTrantLock 的对比](./java/synchronized.md)
|
|
||||||
* [并发编程面试必备:乐观锁与悲观锁](./essential-content-for-interview/面试必备之乐观锁与悲观锁.md)
|
|
||||||
* [并发编程面试必备:JUC 中的 Atomic 原子类总结](./java/Multithread/Atomic.md)
|
|
||||||
* [并发编程面试必备:AQS 原理以及 AQS 同步组件总结](./java/Multithread/AQS.md)
|
|
||||||
* [BATJ都爱问的多线程面试题](./java/Multithread/BATJ都爱问的多线程面试题.md)
|
|
||||||
* [并发容器总结](./java/Multithread/并发容器总结.md)
|
|
||||||
|
|
||||||
### JVM
|
|
||||||
|
|
||||||
* [可能是把Java内存区域讲的最清楚的一篇文章](./java/可能是把Java内存区域讲的最清楚的一篇文章.md)
|
|
||||||
* [搞定JVM垃圾回收就是这么简单](./java/搞定JVM垃圾回收就是这么简单.md)
|
|
||||||
* [《深入理解Java虚拟机》第2版学习笔记](./java/Java虚拟机(jvm).md)
|
|
||||||
|
|
||||||
### I/O
|
|
||||||
|
|
||||||
* [BIO,NIO,AIO 总结 ](./java/BIO-NIO-AIO.md)
|
|
||||||
* [Java IO 与 NIO系列文章](./java/Java%20IO与NIO.md)
|
|
||||||
|
|
||||||
### Java 8
|
|
||||||
|
|
||||||
* [Java 8 新特性总结](./java/What's%20New%20in%20JDK8/Java8Tutorial.md)
|
|
||||||
* [Java 8 学习资源推荐](./java/What's%20New%20in%20JDK8/Java8教程推荐.md)
|
|
||||||
|
|
||||||
### 编程规范
|
|
||||||
|
|
||||||
- [Java 编程规范](./java/Java编程规范.md)
|
|
||||||
|
|
||||||
## 网络
|
|
||||||
|
|
||||||
* [计算机网络常见面试题](./network/计算机网络.md)
|
|
||||||
* [计算机网络基础知识总结](./network/干货:计算机网络知识总结.md)
|
|
||||||
* [HTTPS中的TLS](./network/HTTPS中的TLS.md)
|
|
||||||
|
|
||||||
## 操作系统
|
|
||||||
|
|
||||||
### Linux相关
|
|
||||||
|
|
||||||
* [后端程序员必备的 Linux 基础知识](./operating-system/后端程序员必备的Linux基础知识.md)
|
|
||||||
* [Shell 编程入门](./operating-system/Shell.md)
|
|
||||||
|
|
||||||
## 数据结构与算法
|
|
||||||
|
|
||||||
### 数据结构
|
|
||||||
|
|
||||||
- [数据结构知识学习与面试](./dataStructures-algorithms/数据结构.md)
|
|
||||||
|
|
||||||
### 算法
|
|
||||||
|
|
||||||
- [算法学习资源推荐](./dataStructures-algorithms/算法学习资源推荐.md)
|
|
||||||
- [算法总结——几道常见的子符串算法题 ](./dataStructures-algorithms/几道常见的子符串算法题.md)
|
|
||||||
- [算法总结——几道常见的链表算法题 ](./dataStructures-algorithms/几道常见的链表算法题.md)
|
|
||||||
- [剑指offer部分编程题](./dataStructures-algorithms/剑指offer部分编程题.md)
|
|
||||||
- [公司真题](./dataStructures-algorithms/公司真题.md)
|
|
||||||
- [回溯算法经典案例之N皇后问题](./dataStructures-algorithms/Backtracking-NQueens.md)
|
|
||||||
|
|
||||||
## 数据库
|
|
||||||
|
|
||||||
### MySQL
|
|
||||||
|
|
||||||
* [MySQL 学习与面试](./database/MySQL.md)
|
|
||||||
* [【思维导图-索引篇】搞定数据库索引就是这么简单](./database/MySQL%20Index.md)
|
|
||||||
* [一千行MySQL学习笔记](./database/一千行MySQL命令.md)
|
|
||||||
|
|
||||||
### Redis
|
|
||||||
|
|
||||||
* [Redis 总结](./database/Redis/Redis.md)
|
|
||||||
* [Redlock分布式锁](./database/Redis/Redlock分布式锁.md)
|
|
||||||
* [如何做可靠的分布式锁,Redlock真的可行么](./database/Redis/如何做可靠的分布式锁,Redlock真的可行么.md)
|
|
||||||
|
|
||||||
## 系统设计
|
|
||||||
|
|
||||||
### 设计模式
|
|
||||||
|
|
||||||
- [设计模式系列文章](./system-design/设计模式.md)
|
|
||||||
|
|
||||||
### 常用框架
|
|
||||||
|
|
||||||
#### Spring
|
|
||||||
|
|
||||||
- [Spring 学习与面试](./system-design/framework/Spring学习与面试.md)
|
|
||||||
- [Spring中bean的作用域与生命周期](./system-design/framework/SpringBean.md)
|
|
||||||
- [SpringMVC 工作原理详解](./system-design/framework/SpringMVC%20%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86%E8%AF%A6%E8%A7%A3.md)
|
|
||||||
|
|
||||||
#### ZooKeeper
|
|
||||||
|
|
||||||
- [可能是把 ZooKeeper 概念讲的最清楚的一篇文章](./system-design/framework/ZooKeeper.md)
|
|
||||||
- [ZooKeeper 数据模型和常见命令了解一下,速度收藏!](./system-design/framework/ZooKeeper数据模型和常见命令.md)
|
|
||||||
|
|
||||||
### 数据通信
|
|
||||||
|
|
||||||
- [数据通信(RESTful、RPC、消息队列)相关知识点总结](./system-design/data-communication/数据通信(RESTful、RPC、消息队列).md)
|
|
||||||
- [Dubbo 总结:关于 Dubbo 的重要知识点](./system-design/data-communication/dubbo.md)
|
|
||||||
- [消息队列总结:新手也能看懂,消息队列其实很简单](./system-design/data-communication/message-queue.md)
|
|
||||||
- [一文搞懂 RabbitMQ 的重要概念以及安装](./system-design/data-communication/rabbitmq.md)
|
|
||||||
|
|
||||||
### 网站架构
|
|
||||||
|
|
||||||
- [一文读懂分布式应该学什么](./system-design/website-architecture/分布式.md)
|
|
||||||
- [8 张图读懂大型网站技术架构](./system-design/website-architecture/8%20张图读懂大型网站技术架构.md)
|
|
||||||
- [【面试精选】关于大型网站系统架构你不得不懂的10个问题](./system-design/website-architecture/【面试精选】关于大型网站系统架构你不得不懂的10个问题.md)
|
|
||||||
|
|
||||||
## 面试指南
|
|
||||||
|
|
||||||
### 备战面试
|
|
||||||
|
|
||||||
* [【备战面试1】程序员的简历就该这样写](./essential-content-for-interview/PreparingForInterview/程序员的简历之道.md)
|
|
||||||
* [【备战面试2】初出茅庐的程序员该如何准备面试?](./essential-content-for-interview/PreparingForInterview/interviewPrepare.md)
|
|
||||||
* [【备战面试3】7个大部分程序员在面试前很关心的问题](./essential-content-for-interview/PreparingForInterview/JavaProgrammerNeedKnow.md)
|
|
||||||
* [【备战面试4】Java程序员必备书单](./essential-content-for-interview/PreparingForInterview/books.md)
|
|
||||||
* [【备战面试5】Github上开源的Java面试/学习相关的仓库推荐](./essential-content-for-interview/PreparingForInterview/JavaInterviewLibrary.md)
|
|
||||||
* [【备战面试6】如果面试官问你“你有什么问题问我吗?”时,你该如何回答](./essential-content-for-interview/PreparingForInterview/如果面试官问你“你有什么问题问我吗?”时,你该如何回答.md)
|
|
||||||
* [【备战面试7】美团面试常见问题总结(附详解答案)](./essential-content-for-interview/PreparingForInterview/美团面试常见问题总结.md)
|
|
||||||
|
|
||||||
### 常见面试题总结
|
|
||||||
|
|
||||||
* [第一周(2018-8-7)](./essential-content-for-interview/MostCommonJavaInterviewQuestions/第一周(2018-8-7).md) (为什么 Java 中只有值传递、==与equals、 hashCode与equals)
|
|
||||||
* [第二周(2018-8-13)](./essential-content-for-interview/MostCommonJavaInterviewQuestions/第二周(2018-8-13).md)(String和StringBuffer、StringBuilder的区别是什么?String为什么是不可变的?、什么是反射机制?反射机制的应用场景有哪些?......)
|
|
||||||
* [第三周(2018-08-22)](./java/这几道Java集合框架面试题几乎必问.md) (Arraylist 与 LinkedList 异同、ArrayList 与 Vector 区别、HashMap的底层实现、HashMap 和 Hashtable 的区别、HashMap 的长度为什么是2的幂次方、HashSet 和 HashMap 区别、ConcurrentHashMap 和 Hashtable 的区别、ConcurrentHashMap线程安全的具体实现方式/底层具体实现、集合框架底层数据结构总结)
|
|
||||||
* [第四周(2018-8-30).md](./essential-content-for-interview/MostCommonJavaInterviewQuestions/第四周(2018-8-30).md) (主要内容是几道面试常问的多线程基础题。)
|
|
||||||
|
|
||||||
### 面经
|
|
||||||
|
|
||||||
- [5面阿里,终获offer(2018年秋招)](./essential-content-for-interview/BATJrealInterviewExperience/5面阿里,终获offer.md)
|
|
||||||
|
|
||||||
## 工具
|
|
||||||
|
|
||||||
### Git
|
|
||||||
|
|
||||||
* [Git入门](./tools/Git.md)
|
|
||||||
|
|
||||||
### Docker
|
|
||||||
|
|
||||||
* [Docker 入门](./tools/Docker.md)
|
|
||||||
|
|
||||||
## 资料
|
|
||||||
|
|
||||||
### 书单
|
|
||||||
|
|
||||||
- [Java程序员必备书单](./essential-content-for-interview/PreparingForInterview/books.md)
|
|
||||||
|
|
||||||
### Github榜单
|
|
||||||
|
|
||||||
- [Java 项目月榜单](./github-trending/JavaGithubTrending.md)
|
|
||||||
|
|
||||||
## 闲谈
|
|
||||||
|
|
||||||
* [选择技术方向都要考虑哪些因素](./chat/选择技术方向都要考虑哪些因素.md)
|
|
||||||
* [结束了我短暂的秋招,说点自己的感受](./chat/2018%20%E7%A7%8B%E6%8B%9B.md)
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
## 待办
|
|
||||||
|
|
||||||
- [x] [Java 8 新特性总结](./java/What's%20New%20in%20JDK8/Java8Tutorial.md)
|
|
||||||
- [ ] Java 8 新特性详解
|
|
||||||
- [ ] Java 多线程类别知识重构
|
|
||||||
- [x] [BIO,NIO,AIO 总结 ](./java/BIO-NIO-AIO.md)
|
|
||||||
- [ ] Netty 总结
|
|
||||||
- [ ] 数据结构总结重构
|
|
||||||
|
|
||||||
## 公众号
|
|
||||||
|
|
||||||
- 如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。
|
|
||||||
- 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本公众号后台回复 **"Java面试突击"** 即可免费领取!
|
|
||||||
- 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<img src="https://user-gold-cdn.xitu.io/2018/11/28/167598cd2e17b8ec?w=258&h=258&f=jpeg&s=27334" width=""/>
|
|
||||||
</p>
|
|
176
docs/books/java.md
Normal file
176
docs/books/java.md
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
<!-- TOC -->
|
||||||
|
|
||||||
|
- [Java](#java)
|
||||||
|
- [基础](#基础)
|
||||||
|
- [并发](#并发)
|
||||||
|
- [JVM](#jvm)
|
||||||
|
- [Java8 新特性](#java8-新特性)
|
||||||
|
- [代码优化](#代码优化)
|
||||||
|
- [网络](#网络)
|
||||||
|
- [操作系统](#操作系统)
|
||||||
|
- [数据结构](#数据结构)
|
||||||
|
- [算法](#算法)
|
||||||
|
- [入门](#入门)
|
||||||
|
- [经典](#经典)
|
||||||
|
- [面试](#面试)
|
||||||
|
- [数据库](#数据库)
|
||||||
|
- [系统设计](#系统设计)
|
||||||
|
- [设计模式](#设计模式)
|
||||||
|
- [常用框架](#常用框架)
|
||||||
|
- [Spring/SpringBoot](#springspringboot)
|
||||||
|
- [Netty](#netty)
|
||||||
|
- [分布式](#分布式)
|
||||||
|
- [网站架构](#网站架构)
|
||||||
|
- [软件底层](#软件底层)
|
||||||
|
- [其他](#其他)
|
||||||
|
- [其他](#其他-1)
|
||||||
|
|
||||||
|
<!-- /TOC -->
|
||||||
|
|
||||||
|
## Java
|
||||||
|
|
||||||
|
### 基础
|
||||||
|
|
||||||
|
- **[《Head First Java》](https://book.douban.com/subject/2000732/)** : 可以说是我的 Java 启蒙书籍了,特别适合新手读当然也适合我们用来温故 Java 知识点。
|
||||||
|
- **[《Java 核心技术卷 1+卷 2》](https://book.douban.com/subject/25762168/)**: 很棒的两本书,建议有点 Java 基础之后再读,介绍的还是比较深入的,非常推荐。这两本书我一般也会用来巩固知识点或者当做工具书参考,是两本适合放在自己身边的好书。
|
||||||
|
- **[《Java 编程思想 (第 4 版)》](https://book.douban.com/subject/2130190/)**(推荐,豆瓣评分 9.1,3.2K+人评价):大部分人称之为Java领域的圣经,但我不推荐初学者阅读,有点劝退的味道。稍微有点基础后阅读更好。
|
||||||
|
- **[《JAVA 网络编程 第 4 版》](https://book.douban.com/subject/26259017/)**: 可以系统的学习一下网络的一些概念以及网络编程在 Java 中的使用。
|
||||||
|
- **[《Java性能权威指南》](https://book.douban.com/subject/26740520/)**:O'Reilly 家族书,性能调优的入门书,我个人觉得性能调优是每个 Java 从业者必备知识,这本书的缺点就是太老了,但是这本书可以作为一个实战书,尤其是 JVM 调优!不适合初学者。前置书籍:《深入理解 Java 虚拟机》
|
||||||
|
|
||||||
|
### 并发
|
||||||
|
|
||||||
|
- **[《Java 并发编程之美》](<https://book.douban.com/subject/30351286/>)** :**我觉得这本书还是非常适合我们用来学习 Java 多线程的。这本书的讲解非常通俗易懂,作者从并发编程基础到实战都是信手拈来。** 另外,这本书的作者加多自身也会经常在网上发布各种技术文章。我觉得这本书也是加多大佬这么多年在多线程领域的沉淀所得的结果吧!他书中的内容基本都是结合代码讲解,非常有说服力!
|
||||||
|
- **[《实战 Java 高并发程序设计》](https://book.douban.com/subject/26663605/)**: 这个是我第二本要推荐的书籍,比较适合作为多线程入门/进阶书籍来看。这本书内容同样是理论结合实战,对于每个知识点的讲解也比较通俗易懂,整体结构也比较清。
|
||||||
|
- **[《深入浅出 Java 多线程》](https://github.com/RedSpider1/concurrent)**:这本书是几位大厂(如阿里)的大佬开源的,Github 地址:[https://github.com/RedSpider1/concurrent](https://github.com/RedSpider1/concurrent)几位作者为了写好《深入浅出 Java 多线程》这本书阅读了大量的 Java 多线程方面的书籍和博客,然后再加上他们的经验总结、Demo 实例、源码解析,最终才形成了这本书。这本书的质量也是非常过硬!给作者们点个赞!这本书有统一的排版规则和语言风格、清晰的表达方式和逻辑。并且每篇文章初稿写完后,作者们就会互相审校,合并到主分支时所有成员会再次审校,最后再通篇修订了三遍。
|
||||||
|
- **《Java 并发编程的艺术》** :这本书不是很适合作为 Java 多线程入门书籍,需要具备一定的 JVM 基础,有些东西讲的还是挺深入的。另外,就我自己阅读这本书的感觉来说,我觉得这本书的章节规划有点杂乱,但是,具体到某个知识点又很棒!这可能也和这本书由三名作者共同编写完成有关系吧!
|
||||||
|
- ......
|
||||||
|
|
||||||
|
### JVM
|
||||||
|
|
||||||
|
- **[《深入理解 Java 虚拟机(第 3 版)》](https://book.douban.com/subject/24722612/))**:必读!必读!必读!神书,建议多刷几篇。里面不光有丰富地JVM理论知识,还有JVM实战案例!必读!
|
||||||
|
- **[《实战 JAVA 虚拟机》](https://book.douban.com/subject/26354292/)**:作为入门的了解 Java 虚拟机的知识还是不错的。
|
||||||
|
|
||||||
|
### Java8 新特性
|
||||||
|
|
||||||
|
- **[《Java 8 实战》](https://book.douban.com/subject/26772632/)**:面向 Java 8 的技能升级,包括 Lambdas、流和函数式编程特性。实战系列的一贯风格让自己快速上手应用起来。Java 8 支持的 Lambda 是精简表达在语法上提供的支持。Java 8 提供了 Stream,学习和使用可以建立流式编程的认知。
|
||||||
|
- **[《Java 8 编程参考官方教程》](https://book.douban.com/subject/26556574/)**:建议当做工具书来用!哪里不会翻哪里!
|
||||||
|
|
||||||
|
### 代码优化
|
||||||
|
|
||||||
|
- **[《重构_改善既有代码的设计》](https://book.douban.com/subject/4262627/)**:豆瓣 9.1 分,重构书籍的开山鼻祖。
|
||||||
|
- **[《Effective java 》](https://book.douban.com/subject/3360807/)**:本书介绍了在 Java 编程中很多极具实用价值的经验规则,这些经验规则涵盖了大多数开发人员每天所面临的问题的解决方案。这篇文章能够非常实际地帮助你写出更加清晰、健壮和高效的代码。本书中的每条规则都以简短、独立的小文章形式出现,并通过例子代码加以进一步说明。
|
||||||
|
- **[《代码整洁之道》](https://book.douban.com/subject/5442024/)**:虽然是用 Java 语言作为例子,全篇都是在阐述 Java 面向对象的思想,但是其中大部分内容其它语言也能应用到。
|
||||||
|
- **阿里巴巴 Java 开发手册** :[https://github.com/alibaba/p3c](https://github.com/alibaba/p3c)
|
||||||
|
- **Google Java 编程风格指南:** <http://www.hawstein.com/posts/google-java-style.html>
|
||||||
|
|
||||||
|
|
||||||
|
## 网络
|
||||||
|
|
||||||
|
- **[《图解 HTTP》](https://book.douban.com/subject/25863515/)**: 讲漫画一样的讲 HTTP,很有意思,不会觉得枯燥,大概也涵盖也 HTTP 常见的知识点。因为篇幅问题,内容可能不太全面。不过,如果不是专门做网络方向研究的小伙伴想研究 HTTP 相关知识的话,读这本书的话应该来说就差不多了。
|
||||||
|
- **[《HTTP 权威指南》](https://book.douban.com/subject/10746113/)**:如果要全面了解 HTTP 非此书不可!
|
||||||
|
|
||||||
|
## 操作系统
|
||||||
|
|
||||||
|
- **[《鸟哥的 Linux 私房菜》](https://book.douban.com/subject/4889838/)**:本书是最具知名度的 Linux 入门书《鸟哥的 Linux 私房菜基础学习篇》的最新版,全面而详细地介绍了 Linux 操作系统。
|
||||||
|
|
||||||
|
## 数据结构
|
||||||
|
|
||||||
|
- **[《大话数据结构》](https://book.douban.com/subject/6424904/)**:入门类型的书籍,读起来比较浅显易懂,适合没有数据结构基础或者说数据结构没学好的小伙伴用来入门数据结构。
|
||||||
|
|
||||||
|
## 算法
|
||||||
|
|
||||||
|
### 入门
|
||||||
|
|
||||||
|
- **[《我的第一本算法书》](https://book.douban.com/subject/30357170/) (豆瓣评分 7.1,0.2K+人评价)** 一本不那么“专业”的算法书籍。和下面两本推荐的算法书籍都是比较通俗易懂,“不那么深入”的算法书籍。我个人非常推荐,配图和讲解都非常不错!
|
||||||
|
- **[《算法图解》](https://book.douban.com/subject/26979890/)(豆瓣评分 8.4,1.5K+人评价)** :入门类型的书籍,读起来比较浅显易懂,非常适合没有算法基础或者说算法没学好的小伙伴用来入门。示例丰富,图文并茂,以让人容易理解的方式阐释了算法.读起来比较快,内容不枯燥!
|
||||||
|
- **[《啊哈!算法》](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+人评价)**
|
||||||
|
- **[《计算机程序设计艺术(第1卷)》](https://book.douban.com/subject/1130500/)(豆瓣评分 9.4,0.4K+人评价)**
|
||||||
|
|
||||||
|
### 面试
|
||||||
|
|
||||||
|
1. **[《剑指Offer》](https://book.douban.com/subject/6966465/)(豆瓣评分 8.3,0.7K+人评价)**这本面试宝典上面涵盖了很多经典的算法面试题,如果你要准备大厂面试的话一定不要错过这本书。《剑指Offer》 对应的算法编程题部分的开源项目解析:[CodingInterviews](https://github.com/gatieme/CodingInterviews)
|
||||||
|
2. **[程序员代码面试指南:IT名企算法与数据结构题目最优解(第2版)](https://book.douban.com/subject/30422021/) (豆瓣评分 8.7,0.2K+人评价)** :题目相比于《剑指 offer》 来说要难很多,题目涵盖面相比于《剑指 offer》也更加全面。全书一共有将近300道真实出现过的经典代码面试题。
|
||||||
|
3. **[编程之美](https://book.douban.com/subject/3004255/)(豆瓣评分 8.4,3K+人评价)**:这本书收集了约60道算法和程序设计题目,这些题目大部分在近年的笔试、面试中出现过,或者是被微软员工热烈讨论过。作者试图从书中各种有趣的问题出发,引导读者发现问题,分析问题,解决问题,寻找更优的解法。
|
||||||
|
|
||||||
|
## 数据库
|
||||||
|
|
||||||
|
**MySQL:**
|
||||||
|
|
||||||
|
- **[《高性能 MySQL》](https://book.douban.com/subject/23008813/)**:这本书不用多说了把!MySQL 领域的经典之作,拥有广泛的影响力。不但适合数据库管理员(dba)阅读,也适合开发人员参考学习。不管是数据库新手还是专家,相信都能从本书有所收获。如果你的时间不够的话,第5章关于索引的内容和第6章关于查询的内容是必读的!
|
||||||
|
- [《MySQL 技术内幕-InnoDB 存储引擎》](<https://book.douban.com/subject/24708143/>)(推荐,豆瓣评分 8.7):了解 InnoDB 存储引擎底层原理必备的一本书,比较深入。
|
||||||
|
|
||||||
|
**Redis:**
|
||||||
|
|
||||||
|
- **[《Redis 实战》](https://book.douban.com/subject/26612779/)**:如果你想了解 Redis 的一些概念性知识的话,这本书真的非常不错。
|
||||||
|
- **[《Redis 设计与实现》](https://book.douban.com/subject/25900156/)**:也还行吧!
|
||||||
|
|
||||||
|
## 系统设计
|
||||||
|
|
||||||
|
### 设计模式
|
||||||
|
|
||||||
|
- **[《设计模式 : 可复用面向对象软件的基础》](https://book.douban.com/subject/1052241/)** :设计模式的经典!
|
||||||
|
- **[《Head First 设计模式(中文版)》](https://book.douban.com/subject/2243615/)** :相当赞的一本设计模式入门书籍。用实际的编程案例讲解算法设计中会遇到的各种问题和需求变更(对的,连需求变更都考虑到了!),并以此逐步推导出良好的设计模式解决办法。
|
||||||
|
- **[《大话设计模式》](https://book.douban.com/subject/2334288/)** :本书通篇都是以情景对话的形式,用多个小故事或编程示例来组织讲解GOF(即《设计模式 : 可复用面向对象软件的基础》这本书)),但是不像《设计模式 : 可复用面向对象软件的基础》难懂。但是设计模式只看书是不够的,还是需要在实际项目中运用,在实战中体会。
|
||||||
|
|
||||||
|
### 常用框架
|
||||||
|
|
||||||
|
#### Spring/SpringBoot
|
||||||
|
|
||||||
|
- **[《Spring 实战(第 4 版)》](https://book.douban.com/subject/26767354/)** :不建议当做入门书籍读,入门的话可以找点国人的书或者视频看。这本定位就相当于是关于 Spring 的新华字典,只有一些基本概念的介绍和示例,涵盖了 Spring 的各个方面,但都不够深入。就像作者在最后一页写的那样:“学习 Spring,这才刚刚开始”。
|
||||||
|
- **《[Spring源码深度解析 第2版](https://book.douban.com/subject/30452948/)》** :读Spring源码必备的一本书籍。市面上关于Spring源码分析的书籍太少了。
|
||||||
|
- **[《Spring 5高级编程(第5版)》](https://book.douban.com/subject/30452637/)** :推荐阅读,对于Spring5的新特性介绍的很好!不过内容比较多,可以作为工具书参考。
|
||||||
|
- **[《精通Spring4.x企业应用开发实战》](https://read.douban.com/ebook/58113975/?dcs=subject-rec&dcm=douban&dct=26767354)** :通过实战讲解,比较适合作为Spring入门书籍来看。
|
||||||
|
- **[《Spring入门经典》](https://book.douban.com/subject/26652876/)** :适合入门,也有很多示例!
|
||||||
|
- **[《Spring Boot实战派》](https://book.douban.com/subject/34894533/)** :这本书使用的Spring Boot 2.0+的版本,还算比较新。整本书采用“知识点+实例”的形式编写。本书通过“58个基于知识的实例+2个综合性的项目”,深入地讲解Spring Boot的技术原理、知识点和具体应用;把晦涩难懂的理论用实例展现出来,使得读者对知识的理解变得非常容易,同时也立即学会如何使用它。说实话,我还是比较推荐这本书的。
|
||||||
|
- **[《Spring Boot编程思想(核心篇)》](https://book.douban.com/subject/33390560/)** :SpringBoot深入书,不适合初学者。书尤其的厚,这本书的缺点是书的很多知识点的讲解过于啰嗦和拖沓,优点是书中对SpringBoot内部原理讲解很清楚。
|
||||||
|
|
||||||
|
#### Netty
|
||||||
|
|
||||||
|
- **[《Netty进阶之路:跟着案例学Netty》](https://book.douban.com/subject/30381214/)** : 这本书的优点是有不少实际的案例的讲解,通过案例来学习是很不错的!
|
||||||
|
- **[《Netty 4.x 用户指南》](https://waylau.gitbooks.io/netty-4-user-guide/content/)** :《Netty 4.x 用户指南》中文翻译(包含了官方文档以及其他文章)。
|
||||||
|
- **[《Netty 入门与实战:仿写微信 IM 即时通讯系统》](https://juejin.im/book/5b4bc28bf265da0f60130116?referrer=59fbb2daf265da4319559f3a)** :基于 Netty 框架实现 IM 核心系统,带你深入学习 Netty 网络编程核心知识
|
||||||
|
- **[《Netty 实战》](https://book.douban.com/subject/27038538/)** :可以作为工具书参考!
|
||||||
|
|
||||||
|
### 分布式
|
||||||
|
|
||||||
|
- **[《从 Paxos 到 Zookeeper》](https://book.douban.com/subject/26292004/)**:简要介绍几种典型的分布式一致性协议,以及解决分布式一致性问题的思路,其中重点讲解了 Paxos 和 ZAB 协议。同时,本书深入介绍了分布式一致性问题的工业解决方案——ZooKeeper,并着重向读者展示这一分布式协调框架的使用方法、内部实现及运维技巧,旨在帮助读者全面了解 ZooKeeper,并更好地使用和运维 ZooKeeper。
|
||||||
|
- **[《RabbitMQ 实战指南》](https://book.douban.com/subject/27591386/)**:《RabbitMQ 实战指南》从消息中间件的概念和 RabbitMQ 的历史切入,主要阐述 RabbitMQ 的安装、使用、配置、管理、运维、原理、扩展等方面的细节。如果你想浅尝 RabbitMQ 的使用,这本书是你最好的选择;如果你想深入 RabbitMQ 的原理,这本书也是你最好的选择;总之,如果你想玩转 RabbitMQ,这本书一定是最值得看的书之一
|
||||||
|
- **[《Spring Cloud 微服务实战》](https://book.douban.com/subject/27025912/)**:从时下流行的微服务架构概念出发,详细介绍了 Spring Cloud 针对微服务架构中几大核心要素的解决方案和基础组件。对于各个组件的介绍,《Spring Cloud 微服务实战》主要以示例与源码结合的方式来帮助读者更好地理解这些组件的使用方法以及运行原理。同时,在介绍的过程中,还包含了作者在实践中所遇到的一些问题和解决思路,可供读者在实践中作为参考。
|
||||||
|
|
||||||
|
|
||||||
|
### 网站架构
|
||||||
|
|
||||||
|
- **[《大型网站技术架构:核心原理与案例分析+李智慧》](https://book.douban.com/subject/25723064/)**:这本书我读过,基本不需要你有什么基础啊~读起来特别轻松,但是却可以学到很多东西,非常推荐了。另外我写过这本书的思维导图,关注我的微信公众号:“Java 面试通关手册”回复“大型网站技术架构”即可领取思维导图。
|
||||||
|
- **[《亿级流量网站架构核心技术》](https://book.douban.com/subject/26999243/)**:一书总结并梳理了亿级流量网站高可用和高并发原则,通过实例详细介绍了如何落地这些原则。本书分为四部分:概述、高可用原则、高并发原则、案例实战。从负载均衡、限流、降级、隔离、超时与重试、回滚机制、压测与预案、缓存、池化、异步化、扩容、队列等多方面详细介绍了亿级流量网站的架构核心技术,让读者看后能快速运用到实践项目中。
|
||||||
|
|
||||||
|
### 软件底层
|
||||||
|
|
||||||
|
- **[《深入剖析 Tomcat》](https://book.douban.com/subject/10426640/)**:本书深入剖析 Tomcat 4 和 Tomcat 5 中的每个组件,并揭示其内部工作原理。通过学习本书,你将可以自行开发 Tomcat 组件,或者扩展已有的组件。 读完这本书,基本可以摆脱背诵面试题的尴尬。
|
||||||
|
- **[《深入理解 Nginx(第 2 版)》](https://book.douban.com/subject/26745255/)**:作者讲的非常细致,注释都写的都很工整,对于 Nginx 的开发人员非常有帮助。优点是细致,缺点是过于细致,到处都是代码片段,缺少一些抽象。
|
||||||
|
|
||||||
|
### 其他
|
||||||
|
|
||||||
|
- **[《深入分析 Java Web 技术内幕》](https://book.douban.com/subject/25953851/)**: 感觉还行,涉及的东西也蛮多。
|
||||||
|
|
||||||
|
## 其他
|
||||||
|
|
||||||
|
- **[《黑客与画家》](https://read.douban.com/ebook/387525/?dcs=subject-rec&dcm=douban&dct=2243615)**:这本书是硅谷创业之父,Y Combinator 创始人 Paul Graham 的文集。之所以叫这个名字,是因为作者认为黑客(并非负面的那个意思)与画家有着极大的相似性,他们都是在创造,而不是完成某个任务。
|
||||||
|
|
||||||
|
- **[《图解密码技术》](https://book.douban.com/subject/26265544/)**:本书以**图配文**的形式,第一部分讲述了密码技术的历史沿革、对称密码、分组密码模式(包括ECB、CBC、CFB、OFB、CTR)、公钥、混合密码系统。第二部分重点介绍了认证方面的内容,涉及单向散列函数、消息认证码、数字签名、证书等。第三部分讲述了密钥、随机数、PGP、SSL/TLS 以及密码技术在现实生活中的应用。关键字:JWT 前置知识、区块链密码技术前置知识。属于密码知识入门书籍。
|
||||||
|
|
||||||
|
- 《人月神话》 、《程序开发心理学》 、《程序员修炼之道,从小工道专家》、 《高效程序员的45个习惯,敏捷开发修炼之道》 、《高效能程序员的修炼》 、《软技能,代码之外的生存之道》 、《程序员的职业素养》 、《程序员的思维修炼》
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
# 秋招历程流水账总结
|
|
||||||
|
|
||||||
笔主大四准毕业生,在秋招末流比较幸运地进入了一家自己非常喜欢一家公司——ThoughtWorks.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
从9-6号投递出去第一份简历,到10-18号左右拿到第一份 offer ,中间差不多有 1 个半月的时间了。可能自己比较随缘,而且自己所在的大学所处的位置并不是互联网比较发达的城市的原因。所以,很少会有公司愿意跑到我们学校那边来宣讲,来的公司也大多是一些自己没听过或者不太喜欢的公司。所以,在前期,我仅仅能够通过网上投递简历的方式来找工作。
|
|
||||||
|
|
||||||
零零总总算了一下,自己在网上投了大概有 10 份左右的简历,都是些自己还算喜欢的公司。简单说一下自己投递的一些公司:网上投递的公司有:ThoughtWorks、网易、小米、携程、爱奇艺、知乎、小红书、搜狐、欢聚时代、京东;直接邮箱投递的有:烽火、中电数据、蚂蚁金服花呗部门、今日头条;线下宣讲会投递的有:玄武科技。
|
|
||||||
|
|
||||||
网上投递的大部分简历都是在做完笔试之后就没有了下文了,即使有几场笔试自我感觉做的很不错的情况下,还是没有收到后续的面试邀请。还有些邮箱投递的简历,后面也都没了回应。所以,我总共也只参加了3个公司的面试,ThoughtWorks、玄武科技和中电数据,都算是拿到了 offer。拿到 ThoughtWorks 的 offer之后,后面的一些笔试和少部分面试都拒了。决定去 ThoughtWorks 了,春招的大部队会没有我的存在。
|
|
||||||
|
|
||||||
|
|
||||||
我个人对 ThoughtWorks 最有好感,ThoughtWorks 也是我自己之前很想去的一家公司。不光是因为我投递简历的时候可以不用重新填一遍表格可以直接发送我已经编辑好的PDF格式简历的友好,这个公司的文化也让我很喜欢。每次投递一家公司几乎都要重新填写一遍简历真的很让人头疼,即使是用牛客网的简历助手也还是有很多东西需要自己重新填写。
|
|
||||||
|
|
||||||
说句实话,自己在拿到第一份 offer 之前心里还是比较空的,虽然说对自己还是比较自信。包括自己当时来到武汉的原因,也是因为自己没有 offer ,就感觉心里空空的,我相信很多人在这个时候与我也有一样的感觉。然后,我就想到武汉参加一下别的学校宣讲会。现在看来,这个决定也是不必要的,因为我最后去的公司 ThoughtWorks,虽然就在我租的房子的附近,但之前投递的时候,选择的还是远程面试。来到武汉,简单的修整了一下之后,我就去参加了玄武科技在武理工的宣讲会,顺便做了笔试,然后接着就是技术面、HR面、高管面。总体来说,玄武科技的 HR 真的很热情,为他们点个赞,虽然自己最后没能去玄武科技,然后就是技术面非常简单,HR面和高管面也都还好,不会有压抑的感觉,总体聊得很愉快。需要注意的是 玄武科技和很多公司一样都有笔试中有逻辑题,我之前没有做过类似的题,所以当时第一次做有点懵逼。高管面的时候,高管还专门在我做的逻辑题上聊了一会,让我重新做了一些做错的题,并且给他讲一些题的思路,可以看出高层对于应聘者的这项能力还是比较看重的。
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
中电数据的技术面试是电话进行的,花了1个多小时一点,个人感觉问的还是比较深的,感觉自己总体回答的还是比较不错的。
|
|
||||||
|
|
||||||
这里我着重说一下 ThoughtWorks,也算是给想去 ThoughtWorks 的同学一点小小的提示。我是 9.11 号在官网:https://join.thoughtworks.cn/ 投递的简历,9.20 日邮件通知官网下载作业,作业总体来说不难,9.21 号花了半天多的时间做完,然后就直接在9.21 号下午提交了。然后等了挺长时间的,可能是因为 ThoughtWorks 在管理方面比较扁平化的原因,所以总体来说效率可能不算高。因为我选的是远程面试,所以直接下载好 zoom 之后,等HR打电话过来告诉你一个房间号,你就可以直接进去面试就好,一般技术面试有几个人看着你。技术面试的内容,首先就是在面试官让你在你之前做的作业的基础上新增加一个或者两个功能(20分钟)。所以,你在技术面试之前一定要保证你的程序的扩展性是不错的,另外就是你在技术面试之前最好能重构一下自己写的程序。重构本身就是你自己对你写的程序的理解加强很好的一种方式,另外重构也能让你发现你的程序的一些小问题。然后,这一步完成之后,面试官可能会问你一些基础问题,比较简单,所以我觉得 ThoughtWorks 可能更看重你的代码质量。ThoughtWorks 的 HR 面和其他公司的唯一不同可能在于,他会让你用英语介绍一下自己或者说自己的技术栈啊这些。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
# 关于面试一些重要的问题总结
|
|
||||||
另外,再给大家总结一些我个人想到一些关于面试非常重要的一些问题。
|
|
||||||
|
|
||||||
### 面试前
|
|
||||||
|
|
||||||
**如何准备**
|
|
||||||
|
|
||||||
|
|
||||||
运筹帷幄之后,决胜千里之外!不打毫无准备的仗,我觉得大家可以先从下面几个方面来准备面试:
|
|
||||||
|
|
||||||
1. 自我介绍。(你可千万这样介绍:“我叫某某,性别,来自哪里,学校是那个,自己爱干什么”,记住:多说点简历上没有的,多说点自己哪里比别人强!)
|
|
||||||
2. 自己面试中可能涉及哪些知识点、那些知识点是重点。
|
|
||||||
3. 面试中哪些问题会被经常问到、面试中自己改如何回答。(强烈不推荐背题,第一:通过背这种方式你能记住多少?能记住多久?第二:背题的方式的学习很难坚持下去!)
|
|
||||||
4. 自己的简历该如何写。
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
另外,如果你想去类似阿里巴巴、腾讯这种比较大的互联网公司的话,一定要尽早做打算。像阿里巴巴在7月份左右就开始了提前批招聘,到了9月份差不多就已经招聘完毕了。所以,秋招没有参加到阿里的面试还是很遗憾的,毕竟面试即使失败了,也能从阿里难度Max的面试中学到很多东西。
|
|
||||||
|
|
||||||
**关于着装**
|
|
||||||
|
|
||||||
穿西装、打领带、小皮鞋?NO!NO!NO!这是互联网公司面试又不是去走红毯,所以你只需要穿的简单大方就好,不需要太正式。
|
|
||||||
|
|
||||||
**关于自我介绍**
|
|
||||||
|
|
||||||
如果你简历上写的基本信息就不要说了,比如性别、年龄、学校。另外,你也不要一上来就说自己爱好什么这方面内容。因为,面试官根本不关心这些东西。你直接挑和你岗位相关的重要经历和自己最突出的特点讲就好了。
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
**提前准备**
|
|
||||||
|
|
||||||
面试之前可以在网上找找有没有你要面试的公司的面经。在我面试 ThoughtWorks 的前几天我就在网上找了一些关于 ThoughtWorks 的技术面的一些文章。然后知道了 ThoughtWorks 的技术面会让我们在之前做的作业的基础上增加一个或两个功能,所以我提前一天就把我之前做的程序重新重构了一下。然后在技术面的时候,简单的改了几行代码之后写个测试就完事了。如果没有提前准备,我觉得 20 分钟我很大几率会完不成这项任务。
|
|
||||||
|
|
||||||
|
|
||||||
### 面试中
|
|
||||||
|
|
||||||
面试的时候一定要自信,千万不要怕自己哪里会答不出来,或者说某个问题自己忘记怎么回答了。面试过程中,很多问题可能是你之前没有碰到过的,这个时候你就要通过自己构建的知识体系来思考这些问题。如果某些问题你回答不上来,你也可以让面试官给你简单的提示一下。总之,你要自信,你自信的前提是自己要做好充分的准备。下面给大家总结一些面试非常常见的问题:
|
|
||||||
|
|
||||||
- SpringMVC 工作原理
|
|
||||||
- 说一下自己对 IOC 、AOP 的理解
|
|
||||||
- Spring 中用到了那些设计模式,讲一下自己对于这些设计模式的理解
|
|
||||||
- Spring Bean 的作用域和生命周期了解吗
|
|
||||||
- Spring 事务中的隔离级别
|
|
||||||
- Spring 事务中的事务传播行为
|
|
||||||
- 手写一个 LRU 算法
|
|
||||||
- 知道那些排序算法,简单介绍一下快排的原理,能不能手写一下快排
|
|
||||||
- String 为什么是不可变的?String为啥要设计为不可变的?
|
|
||||||
- Arraylist 与 LinkedList 异同
|
|
||||||
- HashMap的底层实现
|
|
||||||
- HashMap 的长度为什么是2的幂次方
|
|
||||||
- ConcurrentHashMap 和 Hashtable 的区别
|
|
||||||
- ConcurrentHashMap线程安全的具体实现方式/底层具体实现
|
|
||||||
- 如果你的简历写了redis 、dubbo、zookeeper、docker的话,面试官还会问一下这些东西。比如redis可能会问你:为什么要用 redis、为什么要用 redis 而不用 map/guava 做缓存、redis 常见数据结构以及使用场景分析、 redis 设置过期时间、redis 内存淘汰机制、 redis 持久化机制、 缓存雪崩和缓存穿透问题、如何解决 Redis 的并发竞争 Key 问题、如何保证缓存与数据库双写时的数据一致性。
|
|
||||||
- 一些简单的 Linux 命令。
|
|
||||||
- 为什么要用 消息队列
|
|
||||||
- 关于 Java多线程,在面试的时候,问的比较多的就是①悲观锁和乐观锁②synchronized 和 ReenTrantLock 区别以及 volatile 和 synchronized 的区别,③可重入锁与非可重入锁的区别、④多线程是解决什么问题的、⑤线程池解决什么问题,为什么要用线程池 ⑥Synchronized 关键字使用、底层原理、JDK1.6 之后的底层优化以及 ReenTrantLock 对比;⑦线程池使用时的注意事项、⑧AQS 原理以及 AQS 同步组件:Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock、⑨ReentranLock源码,设计原理,整体过程 等等问题。
|
|
||||||
- 关于 Java 虚拟机问的比较多的是:①Java内存区域、②虚拟机垃圾算法、③虚拟机垃圾收集器、④JVM内存管理、⑤JVM调优这些问题。
|
|
||||||
|
|
||||||
|
|
||||||
### 面试后
|
|
||||||
|
|
||||||
如果失败,不要灰心;如果通过,切勿狂喜。面试和工作实际上是两回事,可能很多面试未通过的人,工作能力比你强的多,反之亦然。我个人觉得面试也像是一场全新的征程,失败和胜利都是平常之事。所以,劝各位不要因为面试失败而灰心、丧失斗志。也不要因为面试通过而沾沾自喜,等待你的将是更美好的未来,继续加油!
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,89 +0,0 @@
|
|||||||
下面是个人阅读书籍的部分清单,我比较建议阅读的书籍前都加上了:thumbsup: 表情。
|
|
||||||
> ### 核心基础知识
|
|
||||||
|
|
||||||
- :thumbsup: [《图解HTTP》](https://book.douban.com/subject/25863515/)
|
|
||||||
|
|
||||||
讲漫画一样的讲HTTP,很有意思,不会觉得枯燥,大概也涵盖也HTTP常见的知识点。因为篇幅问题,内容可能不太全面。不过,如果不是专门做网络方向研究的小伙伴想研究HTTP相关知识的话,读这本书的话应该来说就差不多了。
|
|
||||||
|
|
||||||
> ### Java相关
|
|
||||||
|
|
||||||
- :thumbsup: [《Head First Java.第二版》](https://book.douban.com/subject/2000732/)
|
|
||||||
|
|
||||||
可以说是我的Java启蒙书籍了,特别适合新手读当然也适合我们用来温故Java知识点。
|
|
||||||
|
|
||||||
- [《Java多线程编程核心技术》](https://book.douban.com/subject/26555197/)
|
|
||||||
|
|
||||||
Java多线程入门级书籍还不错,但是说实话,质量不是很高,很快就可以阅读完。
|
|
||||||
|
|
||||||
- [《JAVA网络编程 第4版》](https://book.douban.com/subject/26259017/)
|
|
||||||
|
|
||||||
可以系统的学习一下网络的一些概念以及网络编程在Java中的使用。
|
|
||||||
|
|
||||||
- :thumbsup: [《Java核心技术卷1+卷2》](https://book.douban.com/subject/25762168/)
|
|
||||||
|
|
||||||
很棒的两本书,建议有点Java基础之后再读,介绍的还是比较深入的,非常推荐。这两本书我一般也会用来巩固知识点,是两本适合放在自己身边的好书。
|
|
||||||
|
|
||||||
- :thumbsup: [《Java编程思想(第4版)》](https://book.douban.com/subject/2130190/)
|
|
||||||
|
|
||||||
这本书要常读,初学者可以快速概览,中等程序员可以深入看看java,老鸟还可以用之回顾java的体系。这本书之所以厉害,因为它在无形中整合了设计模式,这本书之所以难读,也恰恰在于他对设计模式的整合是无形的。
|
|
||||||
|
|
||||||
- :thumbsup: [《Java并发编程的艺术》](https://book.douban.com/subject/26591326/)
|
|
||||||
|
|
||||||
这本书不是很适合作为Java并发入门书籍,需要具备一定的JVM基础。我感觉有些东西讲的还是挺深入的,推荐阅读。
|
|
||||||
- :thumbsup: [《实战Java高并发程序设计》](https://book.douban.com/subject/26663605/)
|
|
||||||
|
|
||||||
豆瓣评分 8.3 ,书的质量没的说,推荐大家好好看一下。
|
|
||||||
|
|
||||||
- [《Java程序员修炼之道》](https://book.douban.com/subject/24841235/)
|
|
||||||
|
|
||||||
很杂,我只看了前面几章,不太推荐阅读。
|
|
||||||
|
|
||||||
- :thumbsup: [《深入理解Java虚拟机(第2版)周志明》](https://book.douban.com/subject/24722612/)
|
|
||||||
|
|
||||||
神书!神书!神书!建议多刷几遍,书中的所有知识点可以通过JAVA运行时区域和JAVA的内存模型与线程两个大模块罗列完全。
|
|
||||||
|
|
||||||
> ### JavaWeb相关
|
|
||||||
|
|
||||||
- :thumbsup: [《深入分析Java Web技术内幕》](https://book.douban.com/subject/25953851/)
|
|
||||||
|
|
||||||
感觉还行,涉及的东西也蛮多,推荐阅读。
|
|
||||||
|
|
||||||
- :thumbsup: [《Spring实战(第4版)》](https://book.douban.com/subject/26767354/)
|
|
||||||
|
|
||||||
不建议当做入门书籍读,入门的话可以找点国人的书或者视频看。这本定位就相当于是关于Spring的新华字典,只有一些基本概念的介绍和示例,涵盖了Spring的各个方面,但都不够深入。就像作者在最后一页写的那样:“学习Spring,这才刚刚开始”。
|
|
||||||
|
|
||||||
- [《Java Web整合开发王者归来》](https://book.douban.com/subject/4189495/)
|
|
||||||
|
|
||||||
当时刚开始学的时候就是开的这本书,基本上是完完整整的看完了。不过,我不是很推荐大家看。这本书比较老了,里面很多东西都已经算是过时了。不过,这本书的一个很大优点是:基础知识点概括全面。
|
|
||||||
|
|
||||||
- :thumbsup: [《Redis实战》](https://book.douban.com/subject/26612779/)
|
|
||||||
|
|
||||||
如果你想了解Redis的一些概念性知识的话,这本书真的非常不错。
|
|
||||||
|
|
||||||
> ### 架构相关
|
|
||||||
|
|
||||||
- :thumbsup: [《大型网站技术架构:核心原理与案例分析+李智慧》](https://book.douban.com/subject/25723064/)
|
|
||||||
|
|
||||||
这本书我读过,基本不需要你有什么基础啊~读起来特别轻松,但是却可以学到很多东西,非常推荐了。另外我写过这本书的思维导图,关注我的微信公众号:“Java面试通关手册”回复“大型网站技术架构”即可领取思维导图。
|
|
||||||
|
|
||||||
- [《架构解密从分布式到微服务(Leaderus著)》](https://book.douban.com/subject/27081188/)
|
|
||||||
|
|
||||||
很一般的书籍,我就是当做课后图书来阅读的。
|
|
||||||
|
|
||||||
> ### 代码优化
|
|
||||||
|
|
||||||
- :thumbsup: [《重构_改善既有代码的设计》](https://book.douban.com/subject/4262627/)
|
|
||||||
|
|
||||||
豆瓣 9.1 分,重构书籍的开山鼻祖。
|
|
||||||
> ### linux操作系统相关
|
|
||||||
- :thumbsup:[<<unix环境编程>>](https://book.douban.com/subject/25900403/) :thumbsup: [<<unix网络编程>>](https://book.douban.com/subject/1500149/)
|
|
||||||
|
|
||||||
对于理解linux操作系统原理非常有用,同时可以打好个人的基本功力,面试中很多公司也会问到linux知识。
|
|
||||||
> ### 课外书籍
|
|
||||||
|
|
||||||
《技术奇点》 :thumbsup:《追风筝的人》 :thumbsup:《穆斯林的葬礼》 :thumbsup:《三体》 《人工智能——李开复》
|
|
||||||
:thumbsup:《活着——余华》
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
|||||||
上几周公司例会培训有一个讲座是关于 **“如何提问”** 的,听完讲座再联想到自己的一些实际经历,我觉得学会提问对一个来说真的太特么重要了。
|
|
||||||
|
|
||||||
就拿我自己平时的情况来说,随着我自己业余写的一些文章被越来越人看到,越来越多人认识和了解到我。也正因为如此,我每天几乎都面临来自读者至少 10 个以上的问题。我挺高兴有这么多人愿意问我问题的。我看到一些读者的问题,能回答的我都会尽力去好好解决对方的疑惑,我觉得这也算是一种信任,一种陌生人之间建立起来的信任。如果我没及时回答或者忘记回答你的问题的话,可能是我当时比较忙忘记了或者说我自己也不会!我也请各位也对没有按时回答你问题的一些人一些宽容,换句话说也没有人有义务非要去回答你的问题,而且你的的提问方式真的很大影响了别人回答你问题的欲望。所以,大家在提问题之前可以先这样想:“别人如果回答我的问题是情分,如果没能解决我的问题也很正常,如果忘记或者不想回答我的问题也没毛病”。至少我每次问别人问题前都是这样想的,这样别人很久或者没回答我问题,我也不至于纠结半天!**于****我而言,你所提的问题质量,决定了我是否愿意去帮你解答。甚至在某些情况下,你提出了一些很有价值的问题的话,会让我对你产生一种好感,觉得你这个人还挺有见解“**。
|
|
||||||
|
|
||||||
我遇到过很多让我无语或者头疼的问题,也遇到让我很欢喜想要去耐心解答的问题,总的来说,会提问的人还是太少了。我不知道我是不是一个会提问的人,为此我也查阅了网上的一些相关资料,下面给大家分享一下我对如何提问的看法。**下面只是代表了我个人的看法,欢迎各位在评论区说出自己的见解,我会抽出一位综合起来最好的朋友送一本50元左右的任意书籍。**
|
|
||||||
|
|
||||||
下面我总结了一些经常被问到的一些问题,我暂且将它们分为:“稍微正常”和“不那么好”这两类。
|
|
||||||
|
|
||||||
**我觉得稍微正常点的问题(还算正常的问题,但提问方式有待改善):**
|
|
||||||
|
|
||||||
1. 如何学习什么?
|
|
||||||
2. 什么该如何入门?
|
|
||||||
3. 什么问题如何解决?
|
|
||||||
4. 什么内容你能给我解释一下吗?
|
|
||||||
5. 如何找到一个让自己满意的工作?
|
|
||||||
6. 简介该如何写?
|
|
||||||
7. 初学xxx有哪些书籍推荐呢?
|
|
||||||
8. ......
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
**我觉得觉得不那么好的问题(让人讨厌的问题):**
|
|
||||||
|
|
||||||
1. 什么软件可以发一下、我能在哪找到 X 程序或 X 资源?(一般被提问者内心OS:难道不会 Google?最不济应该也会百度吧!)
|
|
||||||
2. 什么环境变量怎么配置啊( Google?百度?)
|
|
||||||
3. 随便截个bug图,然后扔下一句话:“这是什么题”(一般被提问者内心OS:我滴个乖乖,你随便截个图问我,我特么哪有闲心思给你解决这种问题,我自己不就是从这个时候过来的吗,是不是应该把 stackoverflow 推荐给他!
|
|
||||||
4. 我怎么才能破解 root 帐号/窃取 OP 特权/读别人的邮件呢?(一般被提问者内心OS:想要这样做,说明了你是个卑鄙小人;想找个别人帮你,说明你是个白痴!)
|
|
||||||
5. ......
|
|
||||||
|
|
||||||
分享一个这两天遇到的一个典型的例子,当然,之前也遇到了很多这样的例子,我觉得下面这位同学的问题以及提问方式都不太好,至少我自己真的不太喜欢。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
前面的聊天的我这里就不贴了,总结来说,我觉得他的提问存在很明显的问题就是:没有把自己的问题描述清楚,问一些过于”低级“的问题,另外,最重要是我觉得他态度也不是那么好。所以,后面我就直接给他说:”这些问题你直接百度/Google 最好“。我是真的讨厌这种问问题方式,我也知道你可能是刚入门,需要别人帮助你回答一些疑问,但是请你问问题之前自己先做下功课可好?
|
|
||||||
|
|
||||||
说了这么多废话,其实也是自己心里话,不光是想让大家意识到会提问真的很重要,同时也是告诫自己以后要注意自己的提问方式。**下面说一下我觉得比较好的提问方式或者说是高效提问方式:**
|
|
||||||
|
|
||||||
1. 最重要的就是遇到问题之前首先 Google!很多时候你花半个小时到处问问题,你 Google 一下可能 10 分钟就解决了。
|
|
||||||
2. 有问题直接问,不要给别人来句“在吗”或者“有时间吗”这类话(我觉得我还算脾气很好的,每天都会遇到这类人,每天都不耐烦的回答,但直接说明自己的问题或者请求不是更好吗?)。
|
|
||||||
3. 问别人问题之前自己先做一些功课,不要一上来就问一下很 Low 的问题,让别人对你的印象不好;
|
|
||||||
4. 问问题的时候尽量添加一些上下文信息,比如说:你为什么问这些问题,这些问题出现在什么情况下等等。
|
|
||||||
5. 你可以先说明一下自己对于这些问题的看法,你准备如何解决,你做过哪些尝试,你期待对方给你什么样的回答。
|
|
||||||
6. 缩小你的问题的范围,越是范围小而清晰的问题越容易回答。
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
最后,再分享一下有些我觉得比较好的提问网站:
|
|
||||||
|
|
||||||
**国内:** segmentfault、知乎
|
|
||||||
|
|
||||||
**国外:**stackoverflow (感觉和知乎很像,但是 stackoverflow 不光可以给回答打分还可以给问题本身打分,我觉得这点很不错,最重要的是 stackoverflow 主要是程序员问答,你遇到的很多程序问题在这里应该都有其他人遇到过 )
|
|
||||||
|
|
||||||
更多关于如何提问的内容,详见 github 上开源版『提问的智慧』 <https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/master/README-zh_CN.md>
|
|
@ -1,63 +0,0 @@
|
|||||||
本文主要是作者读安晓辉老师的《程序员程序员职场进阶 32 讲 》中关于“选择技术方向都要考虑哪些因素”这部分做的一些笔记和自己的思考。在这里分享给各位!
|
|
||||||
|
|
||||||
### 选择一种技术可能会考虑到的决定因素
|
|
||||||
|
|
||||||
1. 就业机会
|
|
||||||
|
|
||||||
选择一门就业面广的技术还是比较重要的。我的很多学PHP的同学现在都在培训班学Java,真的!!!
|
|
||||||
2. 难易程度
|
|
||||||
|
|
||||||
我当时是在C/C++语言与Java中选择了Java,因为我感觉Java学起来确实要比C++简单一些。
|
|
||||||
3. 个人兴趣
|
|
||||||
|
|
||||||
兴趣是你能坚持下来的一个很重要的条件。
|
|
||||||
4. 薪资水平
|
|
||||||
|
|
||||||
薪资虽然不是人的唯一追求,但是一定是必备的追求。
|
|
||||||
5. 发展前景
|
|
||||||
|
|
||||||
你肯定不愿意看到这种情况发生:选择了一门技术,结果一年后它就没人用、没市场了。所以我们在选择时就要考虑这一点,做一些预判。
|
|
||||||
|
|
||||||
选择技术时存在两种考虑:一种是选择稳定的、经典的技术;一种是卡位将来的市场缺口,选择将来可能需要用到的技术。
|
|
||||||
6. 他人推荐
|
|
||||||
|
|
||||||
我们在懵懵懂懂的时候,往往最容易听从别人的推荐,然后选择某种技术。
|
|
||||||
7. 相近原则
|
|
||||||
|
|
||||||
当我们已经掌握了一些技术,要学习新技术时,就可以根据一种新技术是否和自己已经掌握的技术比较接近来判断选择。相近的技术,学起来会更容易上手。
|
|
||||||
8. 互补原则
|
|
||||||
|
|
||||||
和相近性类似,互补性也常用在拓展我们技术能力的情景下。它指的是,有一些技术可以和你已经掌握的技术互相补充,组合在一起,形成更完整、更系统的技术图谱,给你带来更大的竞争力。关于相近原则与互补原则,我们也会在后面的文章里具体解读。
|
|
||||||
9. 团队技术图谱
|
|
||||||
|
|
||||||
我觉得这个可能就是团队开发过程中的需要。比如在做一个项目的时候,这个项目需要你去学习一下某个你没有接触过的新技术。
|
|
||||||
|
|
||||||
### 入行时如何选择技术方向
|
|
||||||
|
|
||||||
为了明确自己的求职目标,可以问问自己下面的问题:
|
|
||||||
- 我想在哪个城市工作?
|
|
||||||
- 我想在哪些行业、领域发展?
|
|
||||||
- 我想去什么样的公司?
|
|
||||||
- 我想做什么样的产品?
|
|
||||||
|
|
||||||
另外你要知道的是热门技术会有更多机会,相应竞争压力也会更大,并不能保证你找到合适的工作。
|
|
||||||
冷门技术,机会相对较少,而且机会相对确定 。
|
|
||||||
|
|
||||||
### 构建技能树时如何选择技术方向
|
|
||||||
|
|
||||||
当我们过了专项能力提升的初级阶段之后,就应该开始构建自己的技能体系了。在为搭建技能树而选择技术时,通常考虑下面两个原则:
|
|
||||||
- 相近原则
|
|
||||||
- 互补原则
|
|
||||||
|
|
||||||
“学习技术时一定要学对自己以后发展有用的技术”是我经常对自己强调的,另外我觉得很误导人同时也很错误的一个思想是:“只要是技术学了就会有用的”,这句话在我刚学编程时经常听到有人对我说。希望大家不要被误导,很多技术过时了就是过时了,没有必要再去花时间学。
|
|
||||||
|
|
||||||
我觉得相近原则和互补原则互补原则就是你主精和自己技术方向相同的的东西或者对自己技术领域有提升的东西。比如我目前暂时选择了Java为我的主要发展语言,所以我就要求自己大部分时间还是搞和Java相关的东西比如:Spring、SpingBoot、Dubbo、Mybatis等等。但是千万不要被语言所束缚,在业余时间我学的比较多的就是Python以及JS、C/C++/C#也会偶尔接触。因为我经常会接触前端另外我自己偶尔有爬虫需求或者需要用Python的一些第三库解决一些问题,所以我业余学Pyton以及JS就比较多一点,我觉得这两门技术也是对我现有技术的一个补充了。
|
|
||||||
|
|
||||||
|
|
||||||
### 技术转型时的方向选择
|
|
||||||
|
|
||||||
我觉得对于技术转型主要有一下几点建议
|
|
||||||
|
|
||||||
- 与自己当前技术栈跨度不太大的领域,比如你做安卓的话转型可以选择做Java后端。
|
|
||||||
- 真正适合自己去做的,并不是一味看着这个领域火了(比如人工智能),然后自己就不考虑实际的去转型到这个领域里去。
|
|
||||||
- 技术转型方向尽量对自己以后的发展需要有帮助。
|
|
@ -41,7 +41,7 @@
|
|||||||
若 row 行的棋子和 i 行的棋子在同一对角线,等腰直角三角形两直角边相等,即 row - i == Math.abs(result[i] - column)
|
若 row 行的棋子和 i 行的棋子在同一对角线,等腰直角三角形两直角边相等,即 row - i == Math.abs(result[i] - column)
|
||||||
|
|
||||||
布尔类型变量 isValid 的作用是剪枝,减少不必要的递归。
|
布尔类型变量 isValid 的作用是剪枝,减少不必要的递归。
|
||||||
```
|
```java
|
||||||
public List<List<String>> solveNQueens(int n) {
|
public List<List<String>> solveNQueens(int n) {
|
||||||
// 下标代表行,值代表列。如result[0] = 3 表示第1行的Q在第3列
|
// 下标代表行,值代表列。如result[0] = 3 表示第1行的Q在第3列
|
||||||
int[] result = new int[n];
|
int[] result = new int[n];
|
||||||
@ -104,7 +104,7 @@ row - i + n 的最大值为 2n(当row = n,i = 0时),故anti_diag的容
|
|||||||
|
|
||||||
**解法二时间复杂度为O(n!),在校验相同列和相同对角线时,引入三个布尔类型数组进行判断。相比解法一,少了一层循环,用空间换时间。**
|
**解法二时间复杂度为O(n!),在校验相同列和相同对角线时,引入三个布尔类型数组进行判断。相比解法一,少了一层循环,用空间换时间。**
|
||||||
|
|
||||||
```
|
```java
|
||||||
List<List<String>> resultList = new LinkedList<>();
|
List<List<String>> resultList = new LinkedList<>();
|
||||||
|
|
||||||
public List<List<String>> solveNQueens(int n) {
|
public List<List<String>> solveNQueens(int n) {
|
||||||
|
297
docs/dataStructures-algorithms/data-structure/bloom-filter.md
Normal file
297
docs/dataStructures-algorithms/data-structure/bloom-filter.md
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
海量数据处理以及缓存穿透这两个场景让我认识了 布隆过滤器 ,我查阅了一些资料来了解它,但是很多现成资料并不满足我的需求,所以就决定自己总结一篇关于布隆过滤器的文章。希望通过这篇文章让更多人了解布隆过滤器,并且会实际去使用它!
|
||||||
|
|
||||||
|
下面我们将分为几个方面来介绍布隆过滤器:
|
||||||
|
|
||||||
|
1. 什么是布隆过滤器?
|
||||||
|
2. 布隆过滤器的原理介绍。
|
||||||
|
3. 布隆过滤器使用场景。
|
||||||
|
4. 通过 Java 编程手动实现布隆过滤器。
|
||||||
|
5. 利用Google开源的Guava中自带的布隆过滤器。
|
||||||
|
6. Redis 中的布隆过滤器。
|
||||||
|
|
||||||
|
### 1.什么是布隆过滤器?
|
||||||
|
|
||||||
|
首先,我们需要了解布隆过滤器的概念。
|
||||||
|
|
||||||
|
布隆过滤器(Bloom Filter)是一个叫做 Bloom 的老哥于1970年提出的。我们可以把它看作由二进制向量(或者说位数组)和一系列随机映射函数(哈希函数)两部分组成的数据结构。相比于我们平时常用的的 List、Map 、Set 等数据结构,它占用空间更少并且效率更高,但是缺点是其返回的结果是概率性的,而不是非常准确的。理论情况下添加到集合中的元素越多,误报的可能性就越大。并且,存放在布隆过滤器的数据不容易删除。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
位数组中的每个元素都只占用 1 bit ,并且每个元素只能是 0 或者 1。这样申请一个 100w 个元素的位数组只占用 1000000Bit / 8 = 125000 Byte = 125000/1024 kb ≈ 122kb 的空间。
|
||||||
|
|
||||||
|
总结:**一个名叫 Bloom 的人提出了一种来检索元素是否在给定大集合中的数据结构,这种数据结构是高效且性能很好的,但缺点是具有一定的错误识别率和删除难度。并且,理论情况下,添加到集合中的元素越多,误报的可能性就越大。**
|
||||||
|
|
||||||
|
### 2.布隆过滤器的原理介绍
|
||||||
|
|
||||||
|
**当一个元素加入布隆过滤器中的时候,会进行如下操作:**
|
||||||
|
|
||||||
|
1. 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。
|
||||||
|
2. 根据得到的哈希值,在位数组中把对应下标的值置为 1。
|
||||||
|
|
||||||
|
**当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行如下操作:**
|
||||||
|
|
||||||
|
1. 对给定元素再次进行相同的哈希计算;
|
||||||
|
2. 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。
|
||||||
|
|
||||||
|
举个简单的例子:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
如图所示,当字符串存储要加入到布隆过滤器中时,该字符串首先由多个哈希函数生成不同的哈希值,然后在对应的位数组的下表的元素设置为 1(当位数组初始化时 ,所有位置均为0)。当第二次存储相同字符串时,因为先前的对应位置已设置为1,所以很容易知道此值已经存在(去重非常方便)。
|
||||||
|
|
||||||
|
如果我们需要判断某个字符串是否在布隆过滤器中时,只需要对给定字符串再次进行相同的哈希计算,得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。
|
||||||
|
|
||||||
|
**不同的字符串可能哈希出来的位置相同,这种情况我们可以适当增加位数组大小或者调整我们的哈希函数。**
|
||||||
|
|
||||||
|
综上,我们可以得出:**布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。**
|
||||||
|
|
||||||
|
### 3.布隆过滤器使用场景
|
||||||
|
|
||||||
|
1. 判断给定数据是否存在:比如判断一个数字是否在于包含大量数字的数字集中(数字集很大,5亿以上!)、 防止缓存穿透(判断请求的数据是否有效避免直接绕过缓存请求数据库)等等、邮箱的垃圾邮件过滤、黑名单功能等等。
|
||||||
|
2. 去重:比如爬给定网址的时候对已经爬取过的 URL 去重。
|
||||||
|
|
||||||
|
### 4.通过 Java 编程手动实现布隆过滤器
|
||||||
|
|
||||||
|
我们上面已经说了布隆过滤器的原理,知道了布隆过滤器的原理之后就可以自己手动实现一个了。
|
||||||
|
|
||||||
|
如果你想要手动实现一个的话,你需要:
|
||||||
|
|
||||||
|
1. 一个合适大小的位数组保存数据
|
||||||
|
2. 几个不同的哈希函数
|
||||||
|
3. 添加元素到位数组(布隆过滤器)的方法实现
|
||||||
|
4. 判断给定元素是否存在于位数组(布隆过滤器)的方法实现。
|
||||||
|
|
||||||
|
下面给出一个我觉得写的还算不错的代码(参考网上已有代码改进得到,对于所有类型对象皆适用):
|
||||||
|
|
||||||
|
```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)));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
测试:
|
||||||
|
|
||||||
|
```java
|
||||||
|
String value1 = "https://javaguide.cn/";
|
||||||
|
String value2 = "https://github.com/Snailclimb";
|
||||||
|
MyBloomFilter filter = new MyBloomFilter();
|
||||||
|
System.out.println(filter.contains(value1));
|
||||||
|
System.out.println(filter.contains(value2));
|
||||||
|
filter.add(value1);
|
||||||
|
filter.add(value2);
|
||||||
|
System.out.println(filter.contains(value1));
|
||||||
|
System.out.println(filter.contains(value2));
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
|
||||||
|
```
|
||||||
|
false
|
||||||
|
false
|
||||||
|
true
|
||||||
|
true
|
||||||
|
```
|
||||||
|
|
||||||
|
测试:
|
||||||
|
|
||||||
|
```java
|
||||||
|
Integer value1 = 13423;
|
||||||
|
Integer value2 = 22131;
|
||||||
|
MyBloomFilter filter = new MyBloomFilter();
|
||||||
|
System.out.println(filter.contains(value1));
|
||||||
|
System.out.println(filter.contains(value2));
|
||||||
|
filter.add(value1);
|
||||||
|
filter.add(value2);
|
||||||
|
System.out.println(filter.contains(value1));
|
||||||
|
System.out.println(filter.contains(value2));
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
|
||||||
|
```java
|
||||||
|
false
|
||||||
|
false
|
||||||
|
true
|
||||||
|
true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.利用Google开源的 Guava中自带的布隆过滤器
|
||||||
|
|
||||||
|
自己实现的目的主要是为了让自己搞懂布隆过滤器的原理,Guava 中布隆过滤器的实现算是比较权威的,所以实际项目中我们不需要手动实现一个布隆过滤器。
|
||||||
|
|
||||||
|
首先我们需要在项目中引入 Guava 的依赖:
|
||||||
|
|
||||||
|
```java
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.guava</groupId>
|
||||||
|
<artifactId>guava</artifactId>
|
||||||
|
<version>28.0-jre</version>
|
||||||
|
</dependency>
|
||||||
|
```
|
||||||
|
|
||||||
|
实际使用如下:
|
||||||
|
|
||||||
|
我们创建了一个最多存放 最多 1500个整数的布隆过滤器,并且我们可以容忍误判的概率为百分之(0.01)
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 创建布隆过滤器对象
|
||||||
|
BloomFilter<Integer> 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 中的布隆过滤器了。**
|
||||||
|
|
||||||
|
### 6.Redis 中的布隆过滤器
|
||||||
|
|
||||||
|
#### 6.1介绍
|
||||||
|
|
||||||
|
Redis v4.0 之后有了 Module(模块/插件) 功能,Redis Modules 让 Redis 可以使用外部模块扩展其功能 。布隆过滤器就是其中的 Module。详情可以查看 Redis 官方对 Redis Modules 的介绍 :https://redis.io/modules。
|
||||||
|
|
||||||
|
另外,官网推荐了一个 RedisBloom 作为 Redis 布隆过滤器的 Module,地址:https://github.com/RedisBloom/RedisBloom。其他还有:
|
||||||
|
|
||||||
|
- redis-lua-scaling-bloom-filter (lua 脚本实现):https://github.com/erikdubbelboer/redis-lua-scaling-bloom-filter
|
||||||
|
- pyreBloom(Python中的快速Redis 布隆过滤器) :https://github.com/seomoz/pyreBloom
|
||||||
|
- ......
|
||||||
|
|
||||||
|
RedisBloom 提供了多种语言的客户端支持,包括:Python、Java、JavaScript 和 PHP。
|
||||||
|
|
||||||
|
#### 6.2使用Docker安装
|
||||||
|
|
||||||
|
如果我们需要体验 Redis 中的布隆过滤器非常简单,通过 Docker 就可以了!我们直接在 Google 搜索**docker redis bloomfilter** 然后在排除广告的第一条搜素结果就找到了我们想要的答案(这是我平常解决问题的一种方式,分享一下),具体地址:https://hub.docker.com/r/redislabs/rebloom/ (介绍的很详细 )。
|
||||||
|
|
||||||
|
**具体操作如下:**
|
||||||
|
|
||||||
|
```
|
||||||
|
➜ ~ docker run -p 6379:6379 --name redis-redisbloom redislabs/rebloom:latest
|
||||||
|
➜ ~ docker exec -it redis-redisbloom bash
|
||||||
|
root@21396d02c252:/data# redis-cli
|
||||||
|
127.0.0.1:6379>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6.3常用命令一览
|
||||||
|
|
||||||
|
> 注意: key:布隆过滤器的名称,item : 添加的元素。
|
||||||
|
|
||||||
|
1. **`BF.ADD `**:将元素添加到布隆过滤器中,如果该过滤器尚不存在,则创建该过滤器。格式:`BF.ADD {key} {item}`。
|
||||||
|
2. **`BF.MADD `** : 将一个或多个元素添加到“布隆过滤器”中,并创建一个尚不存在的过滤器。该命令的操作方式`BF.ADD`与之相同,只不过它允许多个输入并返回多个值。格式:`BF.MADD {key} {item} [item ...]` 。
|
||||||
|
3. **`BF.EXISTS` ** : 确定元素是否在布隆过滤器中存在。格式:`BF.EXISTS {key} {item}`。
|
||||||
|
4. **`BF.MEXISTS`** : 确定一个或者多个元素是否在布隆过滤器中存在格式:`BF.MEXISTS {key} {item} [item ...]`。
|
||||||
|
|
||||||
|
另外,`BF.RESERVE` 命令需要单独介绍一下:
|
||||||
|
|
||||||
|
这个命令的格式如下:
|
||||||
|
|
||||||
|
`BF.RESERVE {key} {error_rate} {capacity} [EXPANSION expansion] `。
|
||||||
|
|
||||||
|
下面简单介绍一下每个参数的具体含义:
|
||||||
|
|
||||||
|
1. key:布隆过滤器的名称
|
||||||
|
2. error_rate :误报的期望概率。这应该是介于0到1之间的十进制值。例如,对于期望的误报率0.1%(1000中为1),error_rate应该设置为0.001。该数字越接近零,则每个项目的内存消耗越大,并且每个操作的CPU使用率越高。
|
||||||
|
3. capacity: 过滤器的容量。当实际存储的元素个数超过这个值之后,性能将开始下降。实际的降级将取决于超出限制的程度。随着过滤器元素数量呈指数增长,性能将线性下降。
|
||||||
|
|
||||||
|
可选参数:
|
||||||
|
|
||||||
|
- expansion:如果创建了一个新的子过滤器,则其大小将是当前过滤器的大小乘以`expansion`。默认扩展值为2。这意味着每个后续子过滤器将是前一个子过滤器的两倍。
|
||||||
|
|
||||||
|
#### 6.4实际使用
|
||||||
|
|
||||||
|
```shell
|
||||||
|
127.0.0.1:6379> BF.ADD myFilter java
|
||||||
|
(integer) 1
|
||||||
|
127.0.0.1:6379> BF.ADD myFilter javaguide
|
||||||
|
(integer) 1
|
||||||
|
127.0.0.1:6379> BF.EXISTS myFilter java
|
||||||
|
(integer) 1
|
||||||
|
127.0.0.1:6379> BF.EXISTS myFilter javaguide
|
||||||
|
(integer) 1
|
||||||
|
127.0.0.1:6379> BF.EXISTS myFilter github
|
||||||
|
(integer) 0
|
||||||
|
```
|
||||||
|
|
@ -15,11 +15,13 @@
|
|||||||
<!-- /MarkdownTOC -->
|
<!-- /MarkdownTOC -->
|
||||||
|
|
||||||
|
|
||||||
## 说明
|
|
||||||
|
|
||||||
- 本文作者:wwwxmu
|
> 授权转载!
|
||||||
- 原文地址:https://www.weiweiblog.cn/13string/
|
>
|
||||||
- 作者的博客站点:https://www.weiweiblog.cn/ (推荐哦!)
|
> - 本文作者:wwwxmu
|
||||||
|
> - 原文地址:https://www.weiweiblog.cn/13string/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
考虑到篇幅问题,我会分两次更新这个内容。本篇文章只是原文的一部分,我在原文的基础上增加了部分内容以及修改了部分代码和注释。另外,我增加了爱奇艺 2018 秋招 Java:`求给定合法括号序列的深度` 这道题。所有代码均编译成功,并带有注释,欢迎各位享用!
|
考虑到篇幅问题,我会分两次更新这个内容。本篇文章只是原文的一部分,我在原文的基础上增加了部分内容以及修改了部分代码和注释。另外,我增加了爱奇艺 2018 秋招 Java:`求给定合法括号序列的深度` 这道题。所有代码均编译成功,并带有注释,欢迎各位享用!
|
||||||
|
|
||||||
@ -112,7 +114,7 @@ public class Main {
|
|||||||
public static String replaceSpace(String[] strs) {
|
public static String replaceSpace(String[] strs) {
|
||||||
|
|
||||||
// 如果检查值不合法及就返回空串
|
// 如果检查值不合法及就返回空串
|
||||||
if (!chechStrs(strs)) {
|
if (!checkStrs(strs)) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
// 数组长度
|
// 数组长度
|
||||||
@ -137,7 +139,6 @@ public class Main {
|
|||||||
|
|
||||||
private static boolean chechStrs(String[] strs) {
|
private static boolean chechStrs(String[] strs) {
|
||||||
boolean flag = false;
|
boolean flag = false;
|
||||||
// 注意:=是赋值,==是判断
|
|
||||||
if (strs != null) {
|
if (strs != null) {
|
||||||
// 遍历strs检查元素值
|
// 遍历strs检查元素值
|
||||||
for (int i = 0; i < strs.length; i++) {
|
for (int i = 0; i < strs.length; i++) {
|
||||||
@ -145,6 +146,7 @@ public class Main {
|
|||||||
flag = true;
|
flag = true;
|
||||||
} else {
|
} else {
|
||||||
flag = false;
|
flag = false;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -190,7 +192,7 @@ public class Main {
|
|||||||
我们上面已经知道了什么是回文串?现在我们考虑一下可以构成回文串的两种情况:
|
我们上面已经知道了什么是回文串?现在我们考虑一下可以构成回文串的两种情况:
|
||||||
|
|
||||||
- 字符出现次数为双数的组合
|
- 字符出现次数为双数的组合
|
||||||
- 字符出现次数为双数的组合+一个只出现一次的字符
|
- **字符出现次数为偶数的组合+单个字符中出现次数最多且为奇数次的字符** (参见 **[issue665](https://github.com/Snailclimb/JavaGuide/issues/665)** )
|
||||||
|
|
||||||
统计字符出现的次数即可,双数才能构成回文。因为允许中间一个数单独出现,比如“abcba”,所以如果最后有字母落单,总长度可以加 1。首先将字符串转变为字符数组。然后遍历该数组,判断对应字符是否在hashset中,如果不在就加进去,如果在就让count++,然后移除该字符!这样就能找到出现次数为双数的字符个数。
|
统计字符出现的次数即可,双数才能构成回文。因为允许中间一个数单独出现,比如“abcba”,所以如果最后有字母落单,总长度可以加 1。首先将字符串转变为字符数组。然后遍历该数组,判断对应字符是否在hashset中,如果不在就加进去,如果在就让count++,然后移除该字符!这样就能找到出现次数为双数的字符个数。
|
||||||
|
|
||||||
@ -463,7 +465,7 @@ public class Main {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return flag == 1 ? res : -res;
|
return flag != 2 ? res : -res;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,7 +225,7 @@ public class Solution {
|
|||||||
while (node1 != null) {
|
while (node1 != null) {
|
||||||
node1 = node1.next;
|
node1 = node1.next;
|
||||||
count++;
|
count++;
|
||||||
if (k < 1 && node1 != null) {
|
if (k < 1) {
|
||||||
node2 = node2.next;
|
node2 = node2.next;
|
||||||
}
|
}
|
||||||
k--;
|
k--;
|
||||||
|
@ -48,15 +48,6 @@ n<=39
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### **运行时间对比:**
|
|
||||||
|
|
||||||
假设n为40我们分别使用迭代法和递归法计算,计算结果如下:
|
|
||||||
|
|
||||||
1. 迭代法
|
|
||||||

|
|
||||||
2. 递归法
|
|
||||||

|
|
||||||
|
|
||||||
### 二 跳台阶问题
|
### 二 跳台阶问题
|
||||||
|
|
||||||
#### **题目描述:**
|
#### **题目描述:**
|
||||||
|
@ -47,7 +47,7 @@ Queue 用来存放 等待处理元素 的集合,这种场景一般用于缓冲
|
|||||||
### 什么是 Set
|
### 什么是 Set
|
||||||
Set 继承于 Collection 接口,是一个不允许出现重复元素,并且无序的集合,主要 HashSet 和 TreeSet 两大实现类。
|
Set 继承于 Collection 接口,是一个不允许出现重复元素,并且无序的集合,主要 HashSet 和 TreeSet 两大实现类。
|
||||||
|
|
||||||
在判断重复元素的时候,Set 集合会调用 hashCode()和 equal()方法来实现。
|
在判断重复元素的时候,HashSet 集合会调用 hashCode()和 equal()方法来实现;TreeSet 集合会调用compareTo方法来实现。
|
||||||
|
|
||||||
### 补充:有序集合与无序集合说明
|
### 补充:有序集合与无序集合说明
|
||||||
- 有序集合:集合里的元素可以根据 key 或 index 访问 (List、Map)
|
- 有序集合:集合里的元素可以根据 key 或 index 访问 (List、Map)
|
||||||
@ -83,8 +83,8 @@ Set 继承于 Collection 接口,是一个不允许出现重复元素,并且
|
|||||||
|
|
||||||
### ArrayList 和 LinkedList 源码学习
|
### ArrayList 和 LinkedList 源码学习
|
||||||
|
|
||||||
- [ArrayList 源码学习](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/ArrayList.md)
|
- [ArrayList 源码学习](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/ArrayList.md)
|
||||||
- [LinkedList 源码学习](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/LinkedList.md)
|
- [LinkedList 源码学习](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/LinkedList.md)
|
||||||
|
|
||||||
### 推荐阅读
|
### 推荐阅读
|
||||||
|
|
||||||
@ -98,87 +98,99 @@ Set 继承于 Collection 接口,是一个不允许出现重复元素,并且
|
|||||||
- [ConcurrentHashMap 实现原理及源码分析](https://link.juejin.im/?target=http%3A%2F%2Fwww.cnblogs.com%2Fchengxiao%2Fp%2F6842045.html)
|
- [ConcurrentHashMap 实现原理及源码分析](https://link.juejin.im/?target=http%3A%2F%2Fwww.cnblogs.com%2Fchengxiao%2Fp%2F6842045.html)
|
||||||
|
|
||||||
## 树
|
## 树
|
||||||
* ### 1 二叉树
|
|
||||||
|
|
||||||
[二叉树](https://baike.baidu.com/item/%E4%BA%8C%E5%8F%89%E6%A0%91)(百度百科)
|
|
||||||
|
|
||||||
(1)[完全二叉树](https://baike.baidu.com/item/%E5%AE%8C%E5%85%A8%E4%BA%8C%E5%8F%89%E6%A0%91)——若设二叉树的高度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第h层有叶子结点,并且叶子结点都是从左到右依次排布,这就是完全二叉树。
|
### 1 二叉树
|
||||||
|
|
||||||
(2)[满二叉树](https://baike.baidu.com/item/%E5%AE%8C%E5%85%A8%E4%BA%8C%E5%8F%89%E6%A0%91)——除了叶结点外每一个结点都有左右子叶且叶子结点都处在最底层的二叉树。
|
|
||||||
|
|
||||||
(3)[平衡二叉树](https://baike.baidu.com/item/%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%A0%91/10421057)——平衡二叉树又被称为AVL树(区别于AVL算法),它是一棵二叉排序树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
|
|
||||||
|
|
||||||
* ### 2 完全二叉树
|
[二叉树](https://baike.baidu.com/item/%E4%BA%8C%E5%8F%89%E6%A0%91)(百度百科)
|
||||||
|
|
||||||
[完全二叉树](https://baike.baidu.com/item/%E5%AE%8C%E5%85%A8%E4%BA%8C%E5%8F%89%E6%A0%91)(百度百科)
|
(1)[完全二叉树](https://baike.baidu.com/item/%E5%AE%8C%E5%85%A8%E4%BA%8C%E5%8F%89%E6%A0%91)——若设二叉树的高度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第h层有叶子结点,并且叶子结点都是从左到右依次排布,这就是完全二叉树。
|
||||||
|
|
||||||
完全二叉树:叶节点只能出现在最下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树
|
(2)[满二叉树](https://baike.baidu.com/item/%E6%BB%A1%E4%BA%8C%E5%8F%89%E6%A0%91)——除了叶结点外每一个结点都有左右子叶且叶子结点都处在最底层的二叉树。
|
||||||
* ### 3 满二叉树
|
|
||||||
|
|
||||||
[满二叉树](https://baike.baidu.com/item/%E6%BB%A1%E4%BA%8C%E5%8F%89%E6%A0%91)(百度百科,国内外的定义不同)
|
(3)[平衡二叉树](https://baike.baidu.com/item/%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%A0%91/10421057)——平衡二叉树又被称为AVL树(区别于AVL算法),它是一棵二叉排序树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
|
||||||
|
|
||||||
国内教程定义:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。
|
### 2 完全二叉树
|
||||||
* ### 堆
|
|
||||||
|
|
||||||
[数据结构之堆的定义](https://blog.csdn.net/qq_33186366/article/details/51876191)
|
|
||||||
|
|
||||||
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆
|
[完全二叉树](https://baike.baidu.com/item/%E5%AE%8C%E5%85%A8%E4%BA%8C%E5%8F%89%E6%A0%91)(百度百科)
|
||||||
* ### 4 二叉查找树(BST)
|
|
||||||
|
|
||||||
[浅谈算法和数据结构: 七 二叉查找树](http://www.cnblogs.com/yangecnu/p/Introduce-Binary-Search-Tree.html)
|
完全二叉树:叶节点只能出现在最下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树。
|
||||||
|
|
||||||
二叉查找树的特点:
|
### 3 满二叉树
|
||||||
|
|
||||||
1. 若任意节点的左子树不空,则左子树上所有结点的 值均小于它的根结点的值;
|
[满二叉树](https://baike.baidu.com/item/%E6%BB%A1%E4%BA%8C%E5%8F%89%E6%A0%91)(百度百科,国内外的定义不同)
|
||||||
2. 若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
|
|
||||||
3. 任意节点的左、右子树也分别为二叉查找树。
|
|
||||||
4. 没有键值相等的节点(no duplicate nodes)。
|
|
||||||
|
|
||||||
* ### 5 平衡二叉树(Self-balancing binary search tree)
|
国内教程定义:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。
|
||||||
|
|
||||||
[ 平衡二叉树](https://baike.baidu.com/item/%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%A0%91)(百度百科,平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等)
|
|
||||||
* ### 6 红黑树
|
|
||||||
|
|
||||||
- 红黑树特点:
|
|
||||||
1. 每个节点非红即黑;
|
|
||||||
2. 根节点总是黑色的;
|
|
||||||
3. 每个叶子节点都是黑色的空节点(NIL节点);
|
|
||||||
4. 如果节点是红色的,则它的子节点必须是黑色的(反之不一定);
|
|
||||||
5. 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)
|
|
||||||
|
|
||||||
- 红黑树的应用:
|
|
||||||
|
|
||||||
TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。
|
|
||||||
|
|
||||||
- 为什么要用红黑树
|
|
||||||
|
|
||||||
简单来说红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。详细了解可以查看 [漫画:什么是红黑树?](https://juejin.im/post/5a27c6946fb9a04509096248#comment)(也介绍到了二叉查找树,非常推荐)
|
|
||||||
|
|
||||||
- 推荐文章:
|
|
||||||
- [漫画:什么是红黑树?](https://juejin.im/post/5a27c6946fb9a04509096248#comment)(也介绍到了二叉查找树,非常推荐)
|
|
||||||
- [寻找红黑树的操作手册](http://dandanlove.com/2018/03/18/red-black-tree/)(文章排版以及思路真的不错)
|
|
||||||
- [红黑树深入剖析及Java实现](https://zhuanlan.zhihu.com/p/24367771)(美团点评技术团队)
|
|
||||||
* ### 7 B-,B+,B*树
|
|
||||||
|
|
||||||
[二叉树学习笔记之B树、B+树、B*树 ](https://yq.aliyun.com/articles/38345)
|
|
||||||
|
|
||||||
[《B-树,B+树,B*树详解》](https://blog.csdn.net/aqzwss/article/details/53074186)
|
### 堆
|
||||||
|
|
||||||
[《B-树,B+树与B*树的优缺点比较》](https://blog.csdn.net/bigtree_3721/article/details/73632405)
|
[数据结构之堆的定义](https://blog.csdn.net/qq_33186366/article/details/51876191)
|
||||||
|
|
||||||
B-树(或B树)是一种平衡的多路查找(又称排序)树,在文件系统中有所应用。主要用作文件的索引。其中的B就表示平衡(Balance)
|
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
|
||||||
1. B+ 树的叶子节点链表结构相比于 B- 树便于扫库,和范围检索。
|
|
||||||
2. B+树支持range-query(区间查询)非常方便,而B树不支持。这是数据库选用B+树的最主要原因。
|
### 4 二叉查找树(BST)
|
||||||
3. B\*树 是B+树的变体,B\*树分配新结点的概率比B+树要低,空间使用率更高;
|
|
||||||
* ### 8 LSM 树
|
[浅谈算法和数据结构: 七 二叉查找树](http://www.cnblogs.com/yangecnu/p/Introduce-Binary-Search-Tree.html)
|
||||||
|
|
||||||
|
二叉查找树的特点:
|
||||||
|
|
||||||
|
1. 若任意节点的左子树不空,则左子树上所有结点的 值均小于它的根结点的值;
|
||||||
|
2. 若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
|
||||||
|
3. 任意节点的左、右子树也分别为二叉查找树;
|
||||||
|
4. 没有键值相等的节点(no duplicate nodes)。
|
||||||
|
|
||||||
|
### 5 平衡二叉树(Self-balancing binary search tree)
|
||||||
|
|
||||||
|
[ 平衡二叉树](https://baike.baidu.com/item/%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%A0%91)(百度百科,平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等)
|
||||||
|
|
||||||
|
### 6 红黑树
|
||||||
|
|
||||||
|
红黑树特点:
|
||||||
|
|
||||||
|
1. 每个节点非红即黑;
|
||||||
|
2. 根节点总是黑色的;
|
||||||
|
3. 每个叶子节点都是黑色的空节点(NIL节点);
|
||||||
|
4. 如果节点是红色的,则它的子节点必须是黑色的(反之不一定);
|
||||||
|
5. 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。
|
||||||
|
|
||||||
|
|
||||||
|
红黑树的应用:
|
||||||
|
|
||||||
[[HBase] LSM树 VS B+树](https://blog.csdn.net/dbanote/article/details/8897599)
|
TreeMap、TreeSet以及JDK1.8的HashMap底层都用到了红黑树。
|
||||||
|
|
||||||
|
**为什么要用红黑树?**
|
||||||
|
|
||||||
|
|
||||||
|
简单来说红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。详细了解可以查看 [漫画:什么是红黑树?](https://juejin.im/post/5a27c6946fb9a04509096248#comment)(也介绍到了二叉查找树,非常推荐)
|
||||||
|
|
||||||
|
推荐文章:
|
||||||
|
|
||||||
|
- [漫画:什么是红黑树?](https://juejin.im/post/5a27c6946fb9a04509096248#comment)(也介绍到了二叉查找树,非常推荐)
|
||||||
|
- [寻找红黑树的操作手册](http://dandanlove.com/2018/03/18/red-black-tree/)(文章排版以及思路真的不错)
|
||||||
|
- [红黑树深入剖析及Java实现](https://zhuanlan.zhihu.com/p/24367771)(美团点评技术团队)
|
||||||
|
|
||||||
|
### 7 B-,B+,B*树
|
||||||
|
|
||||||
|
[二叉树学习笔记之B树、B+树、B*树 ](https://yq.aliyun.com/articles/38345)
|
||||||
|
|
||||||
|
[《B-树,B+树,B*树详解》](https://blog.csdn.net/aqzwss/article/details/53074186)
|
||||||
|
|
||||||
|
[《B-树,B+树与B*树的优缺点比较》](https://blog.csdn.net/bigtree_3721/article/details/73632405)
|
||||||
|
|
||||||
|
B-树(或B树)是一种平衡的多路查找(又称排序)树,在文件系统中有所应用。主要用作文件的索引。其中的B就表示平衡(Balance)
|
||||||
|
|
||||||
|
1. B+ 树的叶子节点链表结构相比于 B- 树便于扫库,和范围检索。
|
||||||
|
2. B+树支持range-query(区间查询)非常方便,而B树不支持。这是数据库选用B+树的最主要原因。
|
||||||
|
3. B\*树 是B+树的变体,B\*树分配新结点的概率比B+树要低,空间使用率更高;
|
||||||
|
|
||||||
|
### 8 LSM 树
|
||||||
|
|
||||||
|
[[HBase] LSM树 VS B+树](https://blog.csdn.net/dbanote/article/details/8897599)
|
||||||
|
|
||||||
B+树最大的性能问题是会产生大量的随机IO
|
B+树最大的性能问题是会产生大量的随机IO
|
||||||
|
|
||||||
为了克服B+树的弱点,HBase引入了LSM树的概念,即Log-Structured Merge-Trees。
|
为了克服B+树的弱点,HBase引入了LSM树的概念,即Log-Structured Merge-Trees。
|
||||||
|
|
||||||
[LSM树由来、设计思想以及应用到HBase的索引](http://www.cnblogs.com/yanghuahui/p/3483754.html)
|
[LSM树由来、设计思想以及应用到HBase的索引](http://www.cnblogs.com/yanghuahui/p/3483754.html)
|
||||||
|
|
||||||
|
|
||||||
## 图
|
## 图
|
||||||
|
@ -1,38 +1,128 @@
|
|||||||
|
先占个坑,说一下我觉得算法这部分学习比较好的规划:
|
||||||
|
|
||||||
|
1. 未入门(对算法和基本数据结构不了解)之前建议先找一本入门书籍看;
|
||||||
|
2. 如果时间比较多可以看一下我推荐的经典部分的书籍,《算法》这本书是首要要看的,其他推荐的神书看自己时间和心情就好,不要太纠结。
|
||||||
|
3. 如果要准备面试,时间比较紧的话,就不需要再去看《算法》这本书了,时间来不及,当然你也可以选取其特定的章节查看。我也推荐了几本不错的专门为算法面试准备的书籍比如《剑指offer》和《程序员代码面试指南》。除了这两本书籍的话,我在下面推荐了 Leetcode 和牛客网这两个常用的刷题网站以及一些比较好的题目资源。
|
||||||
|
|
||||||
|
## 书籍推荐
|
||||||
|
|
||||||
|
> 以下提到的部分书籍的 PDF 高清阅读版本在我的公众号“JavaGuide”后台回复“书籍”即可获取。
|
||||||
|
|
||||||
|
先来看三本入门书籍,这三本入门书籍中的任何一本拿来作为入门学习都非常好。我个人比较倾向于 **《我的第一本算法书》** 这本书籍,虽然它相比于其他两本书集它的豆瓣评分略低一点。我觉得它的配图以及讲解是这三本书中最优秀,唯一比较明显的问题就是没有代码示例。但是,我觉得这不影响它是一本好的算法书籍。因为本身下面这三本入门书籍的目的就不是通过代码来让你的算法有多厉害,只是作为一本很好的入门书籍让你进入算法学习的大门。
|
||||||
|
|
||||||
|
### 入门
|
||||||
|
|
||||||
|
<img src="https://imgkr.cn-bj.ufileos.com/bcf73ee2-ca03-4985-b620-ebe36cc3e791.jpg" style="zoom:33%;" />
|
||||||
|
|
||||||
|
**[我的第一本算法书](https://book.douban.com/subject/30357170/) (豆瓣评分 7.1,0.2K+人评价)**
|
||||||
|
|
||||||
|
一本不那么“专业”的算法书籍。和下面两本推荐的算法书籍都是比较通俗易懂,“不那么深入”的算法书籍。我个人非常推荐,配图和讲解都非常不错!
|
||||||
|
|
||||||
|
<img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/q2790p435q88491n967nqo15077ss401.jpg" alt="img" style="zoom:50%;" />
|
||||||
|
|
||||||
|
**[《算法图解》](https://book.douban.com/subject/26979890/)(豆瓣评分 8.4,1.5K+人评价)**
|
||||||
|
|
||||||
|
入门类型的书籍,读起来比较浅显易懂,非常适合没有算法基础或者说算法没学好的小伙伴用来入门。示例丰富,图文并茂,以让人容易理解的方式阐释了算法.读起来比较快,内容不枯燥!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**[啊哈!算法](https://book.douban.com/subject/25894685/) (豆瓣评分 7.7,0.5K+人评价)**
|
||||||
|
|
||||||
|
和《算法图解》类似的算法趣味入门书籍。
|
||||||
|
|
||||||
|
### 经典
|
||||||
|
|
||||||
|
<img src="https://imgkr.cn-bj.ufileos.com/b5a5a9b0-db43-4c04-9fd7-5fd6998a2491.jpg" style="zoom:50%;" />
|
||||||
|
|
||||||
|
**[《算法 第四版》](https://book.douban.com/subject/10432347/)(豆瓣评分 9.3,0.4K+人评价)**
|
||||||
|
|
||||||
|
我在大二的时候被我们的一个老师强烈安利过!自己也在当时购买了一本放在宿舍,到离开大学的时候自己大概看了一半多一点。因为内容实在太多了!另外,这本书还提供了详细的Java代码,非常适合学习 Java 的朋友来看,可以说是 Java 程序员的必备书籍之一了。
|
||||||
|
|
||||||
|
再来介绍一下这本书籍吧!这本书籍算的上是算法领域经典的参考书,全面介绍了关于算法和数据结构的必备知识,并特别针对排序、搜索、图处理和字符串处理进行了论述。
|
||||||
|
|
||||||
|
> **下面这些书籍都是经典中的经典,但是阅读起来难度也比较大,不做太多阐述,神书就完事了!推荐先看 《算法》,然后再选下面的书籍进行进一步阅读。不需要都看,找一本好好看或者找某本书的某一个章节知识点好好看。**
|
||||||
|
|
||||||
|
<img src="https://imgkr.cn-bj.ufileos.com/08dcc4fa-5b79-4761-848e-50f47bc31cd0.jpg" style="zoom:67%;" />
|
||||||
|
|
||||||
|
**[编程珠玑](https://book.douban.com/subject/3227098/)(豆瓣评分 9.1,2K+人评价)**
|
||||||
|
|
||||||
|
经典名著,被无数读者强烈推荐的书籍,几乎是顶级程序员必看的书籍之一了。这本书的作者也非常厉害,Java之父 James Gosling 就是他的学生。
|
||||||
|
|
||||||
|
很多人都说这本书不是教你具体的算法,而是教你一种编程的思考方式。这种思考方式不仅仅在编程领域适用,在其他同样适用。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<img src="https://imgkr.cn-bj.ufileos.com/2734e31f-433b-456c-98e2-39652ac97c86.png" style="zoom:50%;" />
|
||||||
|
|
||||||
|
**[《算法设计手册》](https://book.douban.com/subject/4048566/)(豆瓣评分9.1 , 45人评价)**
|
||||||
|
|
||||||
|
被 [Teach Yourself Computer Science](https://teachyourselfcs.com/) 强烈推荐的一本算法书籍。
|
||||||
|
|
||||||
|
<img src="https://imgkr.cn-bj.ufileos.com/1981a809-3828-4cfb-af0c-b636dd5c53bf.jpg" style="zoom:48%;" />
|
||||||
|
|
||||||
|
**[《算法导论》](https://book.douban.com/subject/20432061/) (豆瓣评分 9.2,0.4K+人评价)**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**[《计算机程序设计艺术(第1卷)》](https://book.douban.com/subject/1130500/)(豆瓣评分 9.4,0.4K+人评价)**
|
||||||
|
|
||||||
|
### 面试
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**[《剑指Offer》](https://book.douban.com/subject/6966465/)(豆瓣评分 8.3,0.7K+人评价)**
|
||||||
|
|
||||||
|
这本面试宝典上面涵盖了很多经典的算法面试题,如果你要准备大厂面试的话一定不要错过这本书。
|
||||||
|
|
||||||
|
《剑指Offer》 对应的算法编程题部分的开源项目解析:[CodingInterviews](https://github.com/gatieme/CodingInterviews)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<img src="https://imgkr.cn-bj.ufileos.com/bee9a056-a6ba-4387-bd9b-c9a35a178dcf.jpg" style="zoom:50%;" />
|
||||||
|
|
||||||
|
**[程序员代码面试指南:IT名企算法与数据结构题目最优解(第2版)](https://book.douban.com/subject/30422021/) (豆瓣评分 8.7,0.2K+人评价)**
|
||||||
|
|
||||||
|
题目相比于《剑指 offer》 来说要难很多,题目涵盖面相比于《剑指 offer》也更加全面。全书一共有将近300道真实出现过的经典代码面试题。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<img src="https://imgkr.cn-bj.ufileos.com/cea5161f-cd7b-48c7-a9b0-55674f7dadcc.jpg" style="zoom:55%;" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**[编程之美](https://book.douban.com/subject/3004255/)(豆瓣评分 8.4,3K+人评价)**
|
||||||
|
|
||||||
|
这本书收集了约60道算法和程序设计题目,这些题目大部分在近年的笔试、面试中出现过,或者是被微软员工热烈讨论过。作者试图从书中各种有趣的问题出发,引导读者发现问题,分析问题,解决问题,寻找更优的解法。
|
||||||
|
|
||||||
|
## 网站推荐
|
||||||
|
|
||||||
我比较推荐大家可以刷一下 Leetcode ,我自己平时没事也会刷一下,我觉得刷 Leetcode 不仅是为了能让你更从容地面对面试中的手撕算法问题,更可以提高你的编程思维能力、解决问题的能力以及你对某门编程语言 API 的熟练度。当然牛客网也有一些算法题,我下面也整理了一些。
|
我比较推荐大家可以刷一下 Leetcode ,我自己平时没事也会刷一下,我觉得刷 Leetcode 不仅是为了能让你更从容地面对面试中的手撕算法问题,更可以提高你的编程思维能力、解决问题的能力以及你对某门编程语言 API 的熟练度。当然牛客网也有一些算法题,我下面也整理了一些。
|
||||||
|
|
||||||
## LeetCode
|
### [LeetCode](https://leetcode-cn.com/)
|
||||||
|
|
||||||
- [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/)
|
||||||
|
|
||||||
- [如何高效地使用 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)**
|
||||||
|
|
||||||
|
|
||||||
- [牛客网官网](https://www.nowcoder.com)
|
|
||||||
- [剑指offer编程题](https://www.nowcoder.com/ta/coding-interviews)
|
|
||||||
|
|
||||||
- [2017校招真题](https://www.nowcoder.com/ta/2017test)
|
|
||||||
- [华为机试题](https://www.nowcoder.com/ta/huawei)
|
|
||||||
|
|
||||||
|
|
||||||
## 公司真题
|
|
||||||
|
|
||||||
- [ 网易2018校园招聘编程题真题集合](https://www.nowcoder.com/test/6910869/summary)
|
|
||||||
- [ 网易2018校招内推编程题集合](https://www.nowcoder.com/test/6291726/summary)
|
|
||||||
- [2017年校招全国统一模拟笔试(第五场)编程题集合](https://www.nowcoder.com/test/5986669/summary)
|
|
||||||
- [2017年校招全国统一模拟笔试(第四场)编程题集合](https://www.nowcoder.com/test/5507925/summary)
|
|
||||||
- [2017年校招全国统一模拟笔试(第三场)编程题集合](https://www.nowcoder.com/test/5217106/summary)
|
|
||||||
- [2017年校招全国统一模拟笔试(第二场)编程题集合](https://www.nowcoder.com/test/4546329/summary)
|
|
||||||
- [ 2017年校招全国统一模拟笔试(第一场)编程题集合](https://www.nowcoder.com/test/4236887/summary)
|
|
||||||
- [百度2017春招笔试真题编程题集合](https://www.nowcoder.com/test/4998655/summary)
|
|
||||||
- [网易2017春招笔试真题编程题集合](https://www.nowcoder.com/test/4575457/summary)
|
|
||||||
- [网易2017秋招编程题集合](https://www.nowcoder.com/test/2811407/summary)
|
|
||||||
- [网易有道2017内推编程题](https://www.nowcoder.com/test/2385858/summary)
|
|
||||||
- [ 滴滴出行2017秋招笔试真题-编程题汇总](https://www.nowcoder.com/test/3701760/summary)
|
|
||||||
- [腾讯2017暑期实习生编程题](https://www.nowcoder.com/test/1725829/summary)
|
|
||||||
- [今日头条2017客户端工程师实习生笔试题](https://www.nowcoder.com/test/1649301/summary)
|
|
||||||
- [今日头条2017后端工程师实习生笔试题](https://www.nowcoder.com/test/1649268/summary)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,13 +1,73 @@
|
|||||||
|
## 为什么要使用索引?
|
||||||
|
|
||||||
# 思维导图-索引篇
|
1. 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
|
||||||
|
2. 可以大大加快 数据的检索速度(大大减少的检索的数据量), 这也是创建索引的最主要的原因。
|
||||||
|
3. 帮助服务器避免排序和临时表。
|
||||||
|
4. 将随机IO变为顺序IO
|
||||||
|
5. 可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。
|
||||||
|
|
||||||
> 系列思维导图源文件(数据库+架构)以及思维导图制作软件—XMind8 破解安装,公众号后台回复:**“思维导图”** 免费领取!(下面的图片不是很清楚,原图非常清晰,另外提供给大家源文件也是为了大家根据自己需要进行修改)
|
## 索引这么多优点,为什么不对表中的每一个列创建一个索引呢?
|
||||||
|
|
||||||

|
1. 当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。
|
||||||
|
2. 索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大。
|
||||||
|
3. 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。
|
||||||
|
|
||||||
> **下面是我补充的一些内容**
|
## 使用索引的注意事项?
|
||||||
|
|
||||||
# 为什么索引能提高查询速度
|
1. 在经常需要搜索的列上,可以加快搜索的速度;
|
||||||
|
2. 在经常使用在WHERE子句中的列上面创建索引,加快条件的判断速度。
|
||||||
|
3. 在经常需要排序的列上创 建索引,因为索引已经排序,这样查询可以利用索引的排序,加快排序查询时间;
|
||||||
|
4. 对于中到大型表索引都是非常有效的,但是特大型表的话维护开销会很大,不适合建索引
|
||||||
|
5. 在经常用在连接的列上,这 些列主要是一些外键,可以加快连接的速度;
|
||||||
|
6. 避免 where 子句中对宇段施加函数,这会造成无法命中索引。
|
||||||
|
7. 在使用InnoDB时使用与业务无关的自增主键作为主键,即使用逻辑主键,而不要使用业务主键。
|
||||||
|
8. 将打算加索引的列设置为 NOT NULL ,否则将导致引擎放弃使用索引而进行全表扫描
|
||||||
|
9. 删除长期未使用的索引,不用的索引的存在会造成不必要的性能损耗 MySQL 5.7 可以通过查询 sys 库的 chema_unused_indexes 视图来查询哪些索引从未被使用
|
||||||
|
10. 在使用 limit offset 查询缓慢时,可以借助索引来提高性能
|
||||||
|
|
||||||
|
## Mysql索引主要使用的两种数据结构
|
||||||
|
|
||||||
|
### 哈希索引
|
||||||
|
|
||||||
|
对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择BTree索引。
|
||||||
|
|
||||||
|
### BTree索引
|
||||||
|
|
||||||
|
## MyISAM和InnoDB实现BTree索引方式的区别
|
||||||
|
|
||||||
|
### MyISAM
|
||||||
|
|
||||||
|
B+Tree叶节点的data域存放的是数据记录的地址。在索引检索的时候,首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其 data 域的值,然后以 data 域的值为地址读取相应的数据记录。这被称为“非聚簇索引”。
|
||||||
|
|
||||||
|
### InnoDB
|
||||||
|
|
||||||
|
其数据文件本身就是索引文件。相比MyISAM,索引文件和数据文件是分离的,其表数据文件本身就是按B+Tree组织的一个索引结构,树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。这被称为“聚簇索引(或聚集索引)”,而其余的索引都作为辅助索引,辅助索引的data域存储相应记录主键的值而不是地址,这也是和MyISAM不同的地方。在根据主索引搜索时,直接找到key所在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,在走一遍主索引。 因此,在设计表的时候,不建议使用过长的字段作为主键,也不建议使用非单调的字段作为主键,这样会造成主索引频繁分裂。 PS:整理自《Java工程师修炼之道》
|
||||||
|
|
||||||
|
## 覆盖索引介绍
|
||||||
|
|
||||||
|
### 什么是覆盖索引
|
||||||
|
|
||||||
|
如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为“覆盖索引”。我们知道InnoDB存储引擎中,如果不是主键索引,叶子节点存储的是主键+列值。最终还是要“回表”,也就是要通过主键再查找一次。这样就会比较慢覆盖索引就是把要查询出的列和索引是对应的,不做回表操作!
|
||||||
|
|
||||||
|
### 覆盖索引使用实例
|
||||||
|
|
||||||
|
现在我创建了索引(username,age),我们执行下面的 sql 语句
|
||||||
|
|
||||||
|
```sql
|
||||||
|
select username , age from user where username = 'Java' and age = 22
|
||||||
|
```
|
||||||
|
|
||||||
|
在查询数据的时候:要查询出的列在叶子节点都存在!所以,就不用回表。
|
||||||
|
|
||||||
|
## 选择索引和编写利用这些索引的查询的3个原则
|
||||||
|
|
||||||
|
1. 单行访问是很慢的。特别是在机械硬盘存储中(SSD的随机I/O要快很多,不过这一点仍然成立)。如果服务器从存储中读取一个数据块只是为了获取其中一行,那么就浪费了很多工作。最好读取的块中能包含尽可能多所需要的行。使用索引可以创建位置引,用以提升效率。
|
||||||
|
2. 按顺序访问范围数据是很快的,这有两个原因。第一,顺序1/0不需要多次磁盘寻道,所以比随机I/O要快很多(特别是对机械硬盘)。第二,如果服务器能够按需要顺序读取数据,那么就不再需要额外的排序操作,并且GROUPBY查询也无须再做排序和将行按组进行聚合计算了。
|
||||||
|
3. 索引覆盖查询是很快的。如果一个索引包含了查询需要的所有列,那么存储引擎就
|
||||||
|
不需要再回表查找行。这避免了大量的单行访问,而上面的第1点已经写明单行访
|
||||||
|
问是很慢的。
|
||||||
|
|
||||||
|
## 为什么索引能提高查询速度
|
||||||
|
|
||||||
> 以下内容整理自:
|
> 以下内容整理自:
|
||||||
> 地址: https://juejin.im/post/5b55b842f265da0f9e589e79
|
> 地址: https://juejin.im/post/5b55b842f265da0f9e589e79
|
||||||
@ -28,8 +88,8 @@ MySQL的基本存储结构是页(记录都存在页里边):
|
|||||||
|
|
||||||
所以说,如果我们写select * from user where indexname = 'xxx'这样没有进行任何优化的sql语句,默认会这样做:
|
所以说,如果我们写select * from user where indexname = 'xxx'这样没有进行任何优化的sql语句,默认会这样做:
|
||||||
|
|
||||||
1. **定位到记录所在的页:需要遍历双向链表,找到所在的页**
|
1. **定位到记录所在的页:需要遍历双向链表,找到所在的页**
|
||||||
2. **从所在的页内中查找相应的记录:由于不是根据主键查询,只能遍历所在页的单链表了**
|
2. **从所在的页内中查找相应的记录:由于不是根据主键查询,只能遍历所在页的单链表了**
|
||||||
|
|
||||||
很明显,在数据量很大的情况下这样查找会很慢!这样的时间复杂度为O(n)。
|
很明显,在数据量很大的情况下这样查找会很慢!这样的时间复杂度为O(n)。
|
||||||
|
|
||||||
@ -48,7 +108,7 @@ MySQL的基本存储结构是页(记录都存在页里边):
|
|||||||
|
|
||||||
其实底层结构就是B+树,B+树作为树的一种实现,能够让我们很快地查找出对应的记录。
|
其实底层结构就是B+树,B+树作为树的一种实现,能够让我们很快地查找出对应的记录。
|
||||||
|
|
||||||
# 关于索引其他重要的内容补充
|
## 关于索引其他重要的内容补充
|
||||||
|
|
||||||
> 以下内容整理自:《Java工程师修炼之道》
|
> 以下内容整理自:《Java工程师修炼之道》
|
||||||
|
|
||||||
@ -60,17 +120,17 @@ MySQL中的索引可以以一定顺序引用多列,这种索引叫作联合索
|
|||||||
```
|
```
|
||||||
select * from user where name=xx and city=xx ; //可以命中索引
|
select * from user where name=xx and city=xx ; //可以命中索引
|
||||||
select * from user where name=xx ; // 可以命中索引
|
select * from user where name=xx ; // 可以命中索引
|
||||||
select * from user where city=xx; // 无法命中索引
|
select * from user where city=xx ; // 无法命中索引
|
||||||
```
|
```
|
||||||
这里需要注意的是,查询的时候如果两个条件都用上了,但是顺序不同,如 `city= xx and name =xx`,那么现在的查询引擎会自动优化为匹配联合索引的顺序,这样是能够命中索引的.
|
这里需要注意的是,查询的时候如果两个条件都用上了,但是顺序不同,如 `city= xx and name =xx`,那么现在的查询引擎会自动优化为匹配联合索引的顺序,这样是能够命中索引的。
|
||||||
|
|
||||||
由于最左前缀原则,在创建联合索引时,索引字段的顺序需要考虑字段值去重之后的个数,较多的放前面。ORDERBY子句也遵循此规则。
|
由于最左前缀原则,在创建联合索引时,索引字段的顺序需要考虑字段值去重之后的个数,较多的放前面。ORDER BY子句也遵循此规则。
|
||||||
|
|
||||||
### 注意避免冗余索引
|
### 注意避免冗余索引
|
||||||
|
|
||||||
冗余索引指的是索引的功能相同,能够命中 就肯定能命中 ,那么 就是冗余索引如(name,city )和(name )这两个索引就是冗余索引,能够命中后者的查询肯定是能够命中前者的 在大多数情况下,都应该尽量扩展已有的索引而不是创建新索引。
|
冗余索引指的是索引的功能相同,能够命中 就肯定能命中 ,那么 就是冗余索引如(name,city )和(name )这两个索引就是冗余索引,能够命中后者的查询肯定是能够命中前者的 在大多数情况下,都应该尽量扩展已有的索引而不是创建新索引。
|
||||||
|
|
||||||
MySQLS.7 版本后,可以通过查询 sys 库的 `schema_redundant_indexes` 表来查看冗余索引
|
MySQL 5.7 版本后,可以通过查询 sys 库的 `schema_redundant_indexes` 表来查看冗余索引
|
||||||
|
|
||||||
### Mysql如何为表字段添加索引???
|
### Mysql如何为表字段添加索引???
|
||||||
|
|
||||||
@ -84,19 +144,19 @@ ALTER TABLE `table_name` ADD PRIMARY KEY ( `column` )
|
|||||||
```
|
```
|
||||||
ALTER TABLE `table_name` ADD UNIQUE ( `column` )
|
ALTER TABLE `table_name` ADD UNIQUE ( `column` )
|
||||||
```
|
```
|
||||||
|
|
||||||
3.添加INDEX(普通索引)
|
3.添加INDEX(普通索引)
|
||||||
|
|
||||||
```
|
```
|
||||||
ALTER TABLE `table_name` ADD INDEX index_name ( `column` )
|
ALTER TABLE `table_name` ADD INDEX index_name ( `column` )
|
||||||
```
|
```
|
||||||
|
|
||||||
4.添加FULLTEXT(全文索引)
|
4.添加FULLTEXT(全文索引)
|
||||||
|
|
||||||
```
|
```
|
||||||
ALTER TABLE `table_name` ADD FULLTEXT ( `column`)
|
ALTER TABLE `table_name` ADD FULLTEXT ( `column`)
|
||||||
```
|
```
|
||||||
|
|
||||||
5.添加多列索引
|
5.添加多列索引
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -104,7 +164,7 @@ ALTER TABLE `table_name` ADD INDEX index_name ( `column1`, `column2`, `column3`
|
|||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
# 参考
|
## 参考
|
||||||
|
|
||||||
- 《Java工程师修炼之道》
|
- 《Java工程师修炼之道》
|
||||||
- 《MySQL高性能书籍_第3版》
|
- 《MySQL高性能书籍_第3版》
|
||||||
|
@ -1,171 +1,328 @@
|
|||||||
|
点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。
|
||||||
|
|
||||||
|
- [书籍推荐](#书籍推荐)
|
||||||
|
- [文字教程推荐](#文字教程推荐)
|
||||||
|
- [视频教程推荐](#视频教程推荐)
|
||||||
|
- [常见问题总结](#常见问题总结)
|
||||||
|
- [什么是MySQL?](#什么是mysql)
|
||||||
|
- [存储引擎](#存储引擎)
|
||||||
|
- [一些常用命令](#一些常用命令)
|
||||||
|
- [MyISAM和InnoDB区别](#myisam和innodb区别)
|
||||||
|
- [字符集及校对规则](#字符集及校对规则)
|
||||||
|
- [索引](#索引)
|
||||||
|
- [查询缓存的使用](#查询缓存的使用)
|
||||||
|
- [什么是事务?](#什么是事务)
|
||||||
|
- [事物的四大特性(ACID)](#事物的四大特性acid)
|
||||||
|
- [并发事务带来哪些问题?](#并发事务带来哪些问题)
|
||||||
|
- [事务隔离级别有哪些?MySQL的默认隔离级别是?](#事务隔离级别有哪些mysql的默认隔离级别是)
|
||||||
|
- [锁机制与InnoDB锁算法](#锁机制与innodb锁算法)
|
||||||
|
- [大表优化](#大表优化)
|
||||||
|
- [1. 限定数据的范围](#1-限定数据的范围)
|
||||||
|
- [2. 读/写分离](#2-读写分离)
|
||||||
|
- [3. 垂直分区](#3-垂直分区)
|
||||||
|
- [4. 水平分区](#4-水平分区)
|
||||||
|
- [一条SQL语句在MySQL中如何执行的](#一条sql语句在mysql中如何执行的)
|
||||||
|
- [MySQL高性能优化规范建议](#mysql高性能优化规范建议)
|
||||||
|
- [一条SQL语句执行得很慢的原因有哪些?](#一条sql语句执行得很慢的原因有哪些)
|
||||||
|
|
||||||
Java面试通关手册(Java学习指南,欢迎Star,会一直完善下去,欢迎建议和指导):[https://github.com/Snailclimb/Java_Guide](https://github.com/Snailclimb/Java_Guide)
|
<!-- /TOC -->
|
||||||
|
|
||||||
> ## 书籍推荐
|
## 书籍推荐
|
||||||
|
|
||||||
**《高性能MySQL : 第3版》**
|
- 《SQL基础教程(第2版)》 (入门级)
|
||||||
|
- 《高性能MySQL : 第3版》 (进阶)
|
||||||
|
|
||||||
> ## 文字教程推荐
|
## 文字教程推荐
|
||||||
|
|
||||||
[MySQL 教程(菜鸟教程)](http://www.runoob.com/mysql/mysql-tutorial.html)
|
- [SQL Tutorial](https://www.w3schools.com/sql/default.asp) (SQL语句学习,英文)、[SQL Tutorial](https://www.w3school.com.cn/sql/index.asp)(SQL语句学习,中文)、[SQL语句在线练习](https://www.w3schools.com/sql/exercise.asp) (非常不错)
|
||||||
|
- [Github-MySQL入门教程(MySQL tutorial book)](https://github.com/jaywcjlove/mysql-tutorial) (从零开始学习MySQL,主要是面向MySQL数据库管理系统初学者)
|
||||||
|
- [官方教程](https://dev.mysql.com/doc/refman/5.7/)
|
||||||
|
- [MySQL 教程(菜鸟教程)](http://www.runoob.com/MySQL/MySQL-tutorial.html)
|
||||||
|
|
||||||
[MySQL教程(易百教程)](https://www.yiibai.com/mysql/)
|
## 相关资源推荐
|
||||||
|
|
||||||
> ## 视频教程推荐
|
- [中国5级行政区域mysql库](https://github.com/kakuilan/china_area_mysql)
|
||||||
|
|
||||||
|
## 视频教程推荐
|
||||||
|
|
||||||
**基础入门:** [与MySQL的零距离接触-慕课网](https://www.imooc.com/learn/122)
|
**基础入门:** [与MySQL的零距离接触-慕课网](https://www.imooc.com/learn/122)
|
||||||
|
|
||||||
**Mysql开发技巧:** [MySQL开发技巧(一)](https://www.imooc.com/learn/398) [MySQL开发技巧(二)](https://www.imooc.com/learn/427) [MySQL开发技巧(三)](https://www.imooc.com/learn/449)
|
**MySQL开发技巧:** [MySQL开发技巧(一)](https://www.imooc.com/learn/398) [MySQL开发技巧(二)](https://www.imooc.com/learn/427) [MySQL开发技巧(三)](https://www.imooc.com/learn/449)
|
||||||
|
|
||||||
**Mysql5.7新特性及相关优化技巧:** [MySQL5.7版本新特性](https://www.imooc.com/learn/533) [性能优化之MySQL优化](https://www.imooc.com/learn/194)
|
**MySQL5.7新特性及相关优化技巧:** [MySQL5.7版本新特性](https://www.imooc.com/learn/533) [性能优化之MySQL优化](https://www.imooc.com/learn/194)
|
||||||
|
|
||||||
[MySQL集群(PXC)入门](https://www.imooc.com/learn/993) [MyCAT入门及应用](https://www.imooc.com/learn/951)
|
[MySQL集群(PXC)入门](https://www.imooc.com/learn/993) [MyCAT入门及应用](https://www.imooc.com/learn/951)
|
||||||
|
|
||||||
|
## 常见问题总结
|
||||||
|
|
||||||
|
### 什么是MySQL?
|
||||||
|
|
||||||
> ## 常见问题总结
|
MySQL 是一种关系型数据库,在Java企业级开发中非常常用,因为 MySQL 是开源免费的,并且方便扩展。阿里巴巴数据库系统也大量用到了 MySQL,因此它的稳定性是有保障的。MySQL是开放源代码的,因此任何人都可以在 GPL(General Public License) 的许可下下载并根据个性化的需要对其进行修改。MySQL的默认端口号是**3306**。
|
||||||
|
|
||||||
- ### ①存储引擎
|
### 存储引擎
|
||||||
|
|
||||||
[MySQL常见的两种存储引擎:MyISAM与InnoDB的爱恨情仇](https://juejin.im/post/5b1685bef265da6e5c3c1c34)
|
#### 一些常用命令
|
||||||
|
|
||||||
- ### ②字符集及校对规则
|
|
||||||
|
|
||||||
字符集指的是一种从二进制编码到某类字符符号的映射。校对规则则是指某种字符集下的排序规则。Mysql中每一种字符集都会对应一系列的校对规则。
|
**查看MySQL提供的所有存储引擎**
|
||||||
|
|
||||||
Mysql采用的是类似继承的方式指定字符集的默认值,每个数据库以及每张数据表都有自己的默认值,他们逐层继承。比如:某个库中所有表的默认字符集将是该数据库所指定的字符集(这些表在没有指定字符集的情况下,才会采用默认字符集) PS:整理自《Java工程师修炼之道》
|
```sql
|
||||||
|
mysql> show engines;
|
||||||
详细内容可以参考: [MySQL字符集及校对规则的理解](https://www.cnblogs.com/geaozhang/p/6724393.html#mysqlyuzifuji)
|
```
|
||||||
|
|
||||||
- ### ③索引相关的内容(数据库使用中非常关键的技术,合理正确的使用索引可以大大提高数据库的查询性能)
|

|
||||||
|
|
||||||
Mysql索引使用的数据结构主要有**BTree索引** 和 **哈希索引** 。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择BTree索引。
|
|
||||||
|
|
||||||
Mysql的BTree索引使用的是B数中的B+Tree,但对于主要的两种存储引擎的实现方式是不同的。
|
|
||||||
|
|
||||||
**MyISAM:** B+Tree叶节点的data域存放的是数据记录的地址。在索引检索的时候,首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其 data 域的值,然后以 data 域的值为地址读取相应的数据记录。这被称为“非聚簇索引”。
|
从上图我们可以查看出 MySQL 当前默认的存储引擎是InnoDB,并且在5.7版本所有的存储引擎中只有 InnoDB 是事务性存储引擎,也就是说只有 InnoDB 支持事务。
|
||||||
|
|
||||||
**InnoDB:** 其数据文件本身就是索引文件。相比MyISAM,索引文件和数据文件是分离的,其表数据文件本身就是按B+Tree组织的一个索引结构,树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。这被称为“聚簇索引(或聚集索引)”。而其余的索引都作为辅助索引,辅助索引的data域存储相应记录主键的值而不是地址,这也是和MyISAM不同的地方。**在根据主索引搜索时,直接找到key所在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,再走一遍主索引。** **因此,在设计表的时候,不建议使用过长的字段作为主键,也不建议使用非单调的字段作为主键,这样会造成主索引频繁分裂。** PS:整理自《Java工程师修炼之道》
|
|
||||||
|
|
||||||
详细内容可以参考:
|
|
||||||
|
|
||||||
[干货:mysql索引的数据结构](https://www.jianshu.com/p/1775b4ff123a)
|
|
||||||
|
|
||||||
[MySQL优化系列(三)--索引的使用、原理和设计优化](https://blog.csdn.net/Jack__Frost/article/details/72571540)
|
|
||||||
|
|
||||||
[数据库两大神器【索引和锁】](https://juejin.im/post/5b55b842f265da0f9e589e79#comment)
|
|
||||||
|
|
||||||
- ### ④查询缓存的使用
|
|
||||||
|
|
||||||
my.cnf加入以下配置,重启Mysql开启查询缓存
|
**查看MySQL当前默认的存储引擎**
|
||||||
```
|
|
||||||
query_cache_type=1
|
|
||||||
query_cache_size=600000
|
|
||||||
```
|
|
||||||
|
|
||||||
Mysql执行以下命令也可以开启查询缓存
|
|
||||||
|
|
||||||
```
|
|
||||||
set global query_cache_type=1;
|
|
||||||
set global query_cache_size=600000;
|
|
||||||
```
|
|
||||||
如上,**开启查询缓存后在同样的查询条件以及数据情况下,会直接在缓存中返回结果**。这里的查询条件包括查询本身、当前要查询的数据库、客户端协议版本号等一些可能影响结果的信息。因此任何两个查询在任何字符上的不同都会导致缓存不命中。此外,如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、Mysql库中的系统表,其查询结果也不会被缓存。
|
|
||||||
|
|
||||||
缓存建立之后,Mysql的查询缓存系统会跟踪查询中涉及的每张表,如果这些表(数据或结构)发生变化,那么和这张表相关的所有缓存数据都将失效。
|
|
||||||
|
|
||||||
**缓存虽然能够提升数据库的查询性能,但是缓存同时也带来了额外的开销,每次查询后都要做一次缓存操作,失效后还要销毁。** 因此,开启缓存查询要谨慎,尤其对于写密集的应用来说更是如此。如果开启,要注意合理控制缓存空间大小,一般来说其大小设置为几十MB比较合适。此外,**还可以通过sql_cache和sql_no_cache来控制某个查询语句是否需要缓存:**
|
|
||||||
```
|
|
||||||
select sql_no_cache count(*) from usr;
|
|
||||||
```
|
|
||||||
|
|
||||||
- ### ⑤事务机制
|
|
||||||
|
|
||||||
**关系性数据库需要遵循ACID规则,具体内容如下:**
|
|
||||||
|
|
||||||

|
我们也可以通过下面的命令查看默认的存储引擎。
|
||||||
|
|
||||||
1. **原子性:** 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
|
```sql
|
||||||
2. **一致性:** 执行事务前后,数据库从一个一致性状态转换到另一个一致性状态。
|
mysql> show variables like '%storage_engine%';
|
||||||
3. **隔离性:** 并发访问数据库时,一个用户的事物不被其他事务所干扰,各并发事务之间数据库是独立的;
|
```
|
||||||
4. **持久性:** 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库 发生故障也不应该对其有任何影响。
|
|
||||||
|
|
||||||
**为了达到上述事务特性,数据库定义了几种不同的事务隔离级别:**
|
|
||||||
|
|
||||||
- **READ_UNCOMMITTED(未提交读):** 最低的隔离级别,允许读取尚未提交的数据变更,**可能会导致脏读、幻读或不可重复读**
|
**查看表的存储引擎**
|
||||||
- **READ_COMMITTED(提交读):** 允许读取并发事务已经提交的数据,**可以阻止脏读,但是幻读或不可重复读仍有可能发生**
|
|
||||||
- **REPEATABLE_READ(可重复读):** 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,**可以阻止脏读和不可重复读,但幻读仍有可能发生。**
|
|
||||||
- **SERIALIZABLE(串行):** 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,**该级别可以防止脏读、不可重复读以及幻读**。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
|
|
||||||
|
|
||||||
这里需要注意的是:**Mysql 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别.**
|
```sql
|
||||||
|
show table status like "table_name" ;
|
||||||
|
```
|
||||||
|
|
||||||
事务隔离机制的实现基于锁机制和并发调度。其中并发调度使用的是MVCC(多版本并发控制),通过行的创建时间和行的过期时间来支持并发一致性读和回滚等特性。
|

|
||||||
|
|
||||||
详细内容可以参考: [可能是最漂亮的Spring事务管理详解](https://blog.csdn.net/qq_34337272/article/details/80394121)
|
#### MyISAM和InnoDB区别
|
||||||
|
|
||||||
- ### ⑥锁机制与InnoDB锁算法
|
MyISAM是MySQL的默认数据库引擎(5.5版之前)。虽然性能极佳,而且提供了大量的特性,包括全文索引、压缩、空间函数等,但MyISAM不支持事务和行级锁,而且最大的缺陷就是崩溃后无法安全恢复。不过,5.5版本之后,MySQL引入了InnoDB(事务性数据库引擎),MySQL 5.5版本后默认的存储引擎为InnoDB。
|
||||||
**MyISAM和InnoDB存储引擎使用的锁:**
|
|
||||||
|
|
||||||
- MyISAM采用表级锁(table-level locking)。
|
大多数时候我们使用的都是 InnoDB 存储引擎,但是在某些情况下使用 MyISAM 也是合适的比如读密集的情况下。(如果你不介意 MyISAM 崩溃恢复问题的话)。
|
||||||
- InnoDB支持行级锁(row-level locking)和表级锁,默认为行级锁
|
|
||||||
|
|
||||||
**表级锁和行级锁对比:**
|
**两者的对比:**
|
||||||
|
|
||||||
- **表级锁:** Mysql中锁定 **粒度最大** 的一种锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。其锁定粒度最大,触发锁冲突的概率最高,并发度最低,MyISAM和 InnoDB引擎都支持表级锁。
|
1. **是否支持行级锁** : MyISAM 只有表级锁(table-level locking),而InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。
|
||||||
- **行级锁:** Mysql中锁定 **粒度最小** 的一种锁,只针对当前操作的行进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。
|
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. ......
|
||||||
|
|
||||||
详细内容可以参考:
|
《MySQL高性能》上面有一句话这样写到:
|
||||||
[Mysql锁机制简单了解一下](https://blog.csdn.net/qq_34337272/article/details/80611486)
|
|
||||||
|
|
||||||
**InnoDB存储引擎的锁的算法有三种:**
|
|
||||||
- Record lock:单个行记录上的锁
|
|
||||||
- Gap lock:间隙锁,锁定一个范围,不包括记录本身
|
|
||||||
- Next-key lock:record+gap 锁定一个范围,包含记录本身
|
|
||||||
|
|
||||||
**相关知识点:**
|
|
||||||
1. innodb对于行的查询使用next-key lock
|
|
||||||
2. Next-locking keying为了解决Phantom Problem幻读问题
|
|
||||||
3. 当查询的索引含有唯一属性时,将next-key lock降级为record key
|
|
||||||
4. Gap锁设计的目的是为了阻止多个事务将记录插入到同一范围内,而这会导致幻读问题的产生
|
|
||||||
5. 有两种方式显式关闭gap锁:(除了外键约束和唯一性检查外,其余情况仅使用record lock) A. 将事务隔离级别设置为RC B. 将参数innodb_locks_unsafe_for_binlog设置为1
|
|
||||||
|
|
||||||
- ### ⑦大表优化
|
> 不要轻易相信“MyISAM比InnoDB快”之类的经验之谈,这个结论往往不是绝对的。在很多我们已知场景中,InnoDB的速度都可以让MyISAM望尘莫及,尤其是用到了聚簇索引,或者需要访问的数据都可以放入内存的应用。
|
||||||
|
|
||||||
当MySQL单表记录数过大时,数据库的CRUD性能会明显下降,一些常见的优化措施如下:
|
一般情况下我们选择 InnoDB 都是没有问题的,但是某些情况下你并不在乎可扩展能力和并发能力,也不需要事务支持,也不在乎崩溃后的安全恢复问题的话,选择MyISAM也是一个不错的选择。但是一般情况下,我们都是需要考虑到这些问题的。
|
||||||
|
|
||||||
1. **限定数据的范围:** 务必禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时候,我们可以控制在一个月的范围内。;
|
|
||||||
2. **读/写分离:** 经典的数据库拆分方案,主库负责写,从库负责读;
|
|
||||||
3 . **垂直分区:**
|
|
||||||
|
|
||||||
**根据数据库里面数据表的相关性进行拆分。** 例如,用户表中既有用户的登录信息又有用户的基本信息,可以将用户表拆分成两个单独的表,甚至放到单独的库做分库。
|
|
||||||
|
|
||||||
**简单来说垂直拆分是指数据表列的拆分,把一张列比较多的表拆分为多张表。** 如下图所示,这样来说大家应该就更容易理解了。
|
### 字符集及校对规则
|
||||||

|
|
||||||
|
|
||||||
**垂直拆分的优点:** 可以使得行数据变小,在查询时减少读取的Block数,减少I/O次数。此外,垂直分区可以简化表的结构,易于维护。
|
|
||||||
|
|
||||||
**垂直拆分的缺点:** 主键会出现冗余,需要管理冗余列,并会引起Join操作,可以通过在应用层进行Join来解决。此外,垂直分区会让事务变得更加复杂;
|
字符集指的是一种从二进制编码到某类字符符号的映射。校对规则则是指某种字符集下的排序规则。MySQL中每一种字符集都会对应一系列的校对规则。
|
||||||
|
|
||||||
4. **水平分区:**
|
MySQL采用的是类似继承的方式指定字符集的默认值,每个数据库以及每张数据表都有自己的默认值,他们逐层继承。比如:某个库中所有表的默认字符集将是该数据库所指定的字符集(这些表在没有指定字符集的情况下,才会采用默认字符集) PS:整理自《Java工程师修炼之道》
|
||||||
|
|
||||||
|
|
||||||
**保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表或者库中,达到了分布式的目的。 水平拆分可以支撑非常大的数据量。**
|
|
||||||
|
|
||||||
水平拆分是指数据表行的拆分,表的行数超过200万行时,就会变慢,这时可以把一张的表的数据拆成多张表来存放。举个例子:我们可以将用户信息表拆分成多个用户信息表,这样就可以避免单一表数据量过大对性能造成影响。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
水平拆分可以支持非常大的数据量。需要注意的一点是:分表仅仅是解决了单一表数据过大的问题,但由于表的数据还是在同一台机器上,其实对于提升MySQL并发能力没有什么意义,所以 **水平拆分最好分库** 。
|
|
||||||
|
|
||||||
水平拆分能够 **支持非常大的数据量存储,应用端改造也少**,但 **分片事务难以解决** ,跨界点Join性能较差,逻辑复杂。《Java工程师修炼之道》的作者推荐 **尽量不要对数据进行分片,因为拆分会带来逻辑、部署、运维的各种复杂度** ,一般的数据表在优化得当的情况下支撑千万以下的数据量是没有太大问题的。如果实在要分片,尽量选择客户端分片架构,这样可以减少一次和中间件的网络I/O。
|
|
||||||
|
|
||||||
**下面补充一下数据库分片的两种常见方案:**
|
|
||||||
- **客户端代理:** **分片逻辑在应用端,封装在jar包中,通过修改或者封装JDBC层来实现。** 当当网的 **Sharding-JDBC** 、阿里的TDDL是两种比较常用的实现。
|
|
||||||
- **中间件代理:** **在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。** 我们现在谈的 **Mycat** 、360的Atlas、网易的DDB等等都是这种架构的实现。
|
|
||||||
|
|
||||||
|
|
||||||
详细内容可以参考:
|
|
||||||
[MySQL大表优化方案](https://segmentfault.com/a/1190000006158186)
|
|
||||||
|
|
||||||
|
|
||||||
|
详细内容可以参考: [MySQL字符集及校对规则的理解](https://www.cnblogs.com/geaozhang/p/6724393.html#MySQLyuzifuji)
|
||||||
|
|
||||||
|
### 索引
|
||||||
|
|
||||||
|
MySQL索引使用的数据结构主要有**BTree索引** 和 **哈希索引** 。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择BTree索引。
|
||||||
|
|
||||||
|
MySQL的BTree索引使用的是B树中的B+Tree,但对于主要的两种存储引擎的实现方式是不同的。
|
||||||
|
|
||||||
|
- **MyISAM:** B+Tree叶节点的data域存放的是数据记录的地址。在索引检索的时候,首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其 data 域的值,然后以 data 域的值为地址读取相应的数据记录。这被称为“非聚簇索引”。
|
||||||
|
- **InnoDB:** 其数据文件本身就是索引文件。相比MyISAM,索引文件和数据文件是分离的,其表数据文件本身就是按B+Tree组织的一个索引结构,树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。这被称为“聚簇索引(或聚集索引)”。而其余的索引都作为辅助索引,辅助索引的data域存储相应记录主键的值而不是地址,这也是和MyISAM不同的地方。**在根据主索引搜索时,直接找到key所在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,再走一遍主索引。** **因此,在设计表的时候,不建议使用过长的字段作为主键,也不建议使用非单调的字段作为主键,这样会造成主索引频繁分裂。** PS:整理自《Java工程师修炼之道》
|
||||||
|
|
||||||
|
**更多关于索引的内容可以查看文档首页MySQL目录下关于索引的详细总结。**
|
||||||
|
|
||||||
|
### 查询缓存的使用
|
||||||
|
|
||||||
|
> 执行查询语句的时候,会先查询缓存。不过,MySQL 8.0 版本后移除,因为这个功能不太实用
|
||||||
|
|
||||||
|
my.cnf加入以下配置,重启MySQL开启查询缓存
|
||||||
|
```properties
|
||||||
|
query_cache_type=1
|
||||||
|
query_cache_size=600000
|
||||||
|
```
|
||||||
|
|
||||||
|
MySQL执行以下命令也可以开启查询缓存
|
||||||
|
|
||||||
|
```properties
|
||||||
|
set global query_cache_type=1;
|
||||||
|
set global query_cache_size=600000;
|
||||||
|
```
|
||||||
|
如上,**开启查询缓存后在同样的查询条件以及数据情况下,会直接在缓存中返回结果**。这里的查询条件包括查询本身、当前要查询的数据库、客户端协议版本号等一些可能影响结果的信息。因此任何两个查询在任何字符上的不同都会导致缓存不命中。此外,如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、MySQL库中的系统表,其查询结果也不会被缓存。
|
||||||
|
|
||||||
|
缓存建立之后,MySQL的查询缓存系统会跟踪查询中涉及的每张表,如果这些表(数据或结构)发生变化,那么和这张表相关的所有缓存数据都将失效。
|
||||||
|
|
||||||
|
**缓存虽然能够提升数据库的查询性能,但是缓存同时也带来了额外的开销,每次查询后都要做一次缓存操作,失效后还要销毁。** 因此,开启缓存查询要谨慎,尤其对于写密集的应用来说更是如此。如果开启,要注意合理控制缓存空间大小,一般来说其大小设置为几十MB比较合适。此外,**还可以通过sql_cache和sql_no_cache来控制某个查询语句是否需要缓存:**
|
||||||
|
```sql
|
||||||
|
select sql_no_cache count(*) from usr;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 什么是事务?
|
||||||
|
|
||||||
|
**事务是逻辑上的一组操作,要么都执行,要么都不执行。**
|
||||||
|
|
||||||
|
事务最经典也经常被拿出来说例子就是转账了。假如小明要给小红转账1000元,这个转账会涉及到两个关键操作就是:将小明的余额减少1000元,将小红的余额增加1000元。万一在这两个操作之间突然出现错误比如银行系统崩溃,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。
|
||||||
|
|
||||||
|
### 事物的四大特性(ACID)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
1. **原子性(Atomicity):** 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
|
||||||
|
2. **一致性(Consistency):** 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
|
||||||
|
3. **隔离性(Isolation):** 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
|
||||||
|
4. **持久性(Durability):** 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
|
||||||
|
|
||||||
|
### 并发事务带来哪些问题?
|
||||||
|
|
||||||
|
在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对同一数据进行操作)。并发虽然是必须的,但可能会导致以下的问题。
|
||||||
|
|
||||||
|
- **脏读(Dirty read):** 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
|
||||||
|
- **丢失修改(Lost to modify):** 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。
|
||||||
|
- **不可重复读(Unrepeatableread):** 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
|
||||||
|
- **幻读(Phantom read):** 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
|
||||||
|
|
||||||
|
**不可重复读和幻读区别:**
|
||||||
|
|
||||||
|
不可重复读的重点是修改比如多次读取一条记录发现其中某些列的值被修改,幻读的重点在于新增或者删除比如多次读取一条记录发现记录增多或减少了。
|
||||||
|
|
||||||
|
### 事务隔离级别有哪些?MySQL的默认隔离级别是?
|
||||||
|
|
||||||
|
**SQL 标准定义了四个隔离级别:**
|
||||||
|
|
||||||
|
- **READ-UNCOMMITTED(读取未提交):** 最低的隔离级别,允许读取尚未提交的数据变更,**可能会导致脏读、幻读或不可重复读**。
|
||||||
|
- **READ-COMMITTED(读取已提交):** 允许读取并发事务已经提交的数据,**可以阻止脏读,但是幻读或不可重复读仍有可能发生**。
|
||||||
|
- **REPEATABLE-READ(可重复读):** 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,**可以阻止脏读和不可重复读,但幻读仍有可能发生**。
|
||||||
|
- **SERIALIZABLE(可串行化):** 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,**该级别可以防止脏读、不可重复读以及幻读**。
|
||||||
|
|
||||||
|
------
|
||||||
|
|
||||||
|
| 隔离级别 | 脏读 | 不可重复读 | 幻影读 |
|
||||||
|
| :--------------: | :--: | :--------: | :----: |
|
||||||
|
| READ-UNCOMMITTED | √ | √ | √ |
|
||||||
|
| READ-COMMITTED | × | √ | √ |
|
||||||
|
| REPEATABLE-READ | × | × | √ |
|
||||||
|
| SERIALIZABLE | × | × | × |
|
||||||
|
|
||||||
|
MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)**。我们可以通过`SELECT @@tx_isolation;`命令来查看
|
||||||
|
|
||||||
|
```sql
|
||||||
|
mysql> SELECT @@tx_isolation;
|
||||||
|
+-----------------+
|
||||||
|
| @@tx_isolation |
|
||||||
|
+-----------------+
|
||||||
|
| REPEATABLE-READ |
|
||||||
|
+-----------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
这里需要注意的是:与 SQL 标准不同的地方在于 InnoDB 存储引擎在 **REPEATABLE-READ(可重读)**
|
||||||
|
事务隔离级别下使用的是Next-Key Lock 锁算法,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server)
|
||||||
|
是不同的。所以说InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)** 已经可以完全保证事务的隔离性要求,即达到了
|
||||||
|
SQL标准的 **SERIALIZABLE(可串行化)** 隔离级别。因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是 **READ-COMMITTED(读取提交内容)** ,但是你要知道的是InnoDB 存储引擎默认使用 **REPEAaTABLE-READ(可重读)** 并不会有任何性能损失。
|
||||||
|
|
||||||
|
InnoDB 存储引擎在 **分布式事务** 的情况下一般会用到 **SERIALIZABLE(可串行化)** 隔离级别。
|
||||||
|
|
||||||
|
### 锁机制与InnoDB锁算法
|
||||||
|
|
||||||
|
**MyISAM和InnoDB存储引擎使用的锁:**
|
||||||
|
|
||||||
|
- MyISAM采用表级锁(table-level locking)。
|
||||||
|
- InnoDB支持行级锁(row-level locking)和表级锁,默认为行级锁
|
||||||
|
|
||||||
|
**表级锁和行级锁对比:**
|
||||||
|
|
||||||
|
- **表级锁:** MySQL中锁定 **粒度最大** 的一种锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。其锁定粒度最大,触发锁冲突的概率最高,并发度最低,MyISAM和 InnoDB引擎都支持表级锁。
|
||||||
|
- **行级锁:** MySQL中锁定 **粒度最小** 的一种锁,只针对当前操作的行进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。
|
||||||
|
|
||||||
|
详细内容可以参考: MySQL锁机制简单了解一下:[https://blog.csdn.net/qq_34337272/article/details/80611486](https://blog.csdn.net/qq_34337272/article/details/80611486)
|
||||||
|
|
||||||
|
**InnoDB存储引擎的锁的算法有三种:**
|
||||||
|
|
||||||
|
- Record lock:单个行记录上的锁
|
||||||
|
- Gap lock:间隙锁,锁定一个范围,不包括记录本身
|
||||||
|
- Next-key lock:record+gap 锁定一个范围,包含记录本身
|
||||||
|
|
||||||
|
**相关知识点:**
|
||||||
|
|
||||||
|
1. innodb对于行的查询使用next-key lock
|
||||||
|
2. Next-locking keying为了解决Phantom Problem幻读问题
|
||||||
|
3. 当查询的索引含有唯一属性时,将next-key lock降级为record key
|
||||||
|
4. Gap锁设计的目的是为了阻止多个事务将记录插入到同一范围内,而这会导致幻读问题的产生
|
||||||
|
5. 有两种方式显式关闭gap锁:(除了外键约束和唯一性检查外,其余情况仅使用record lock) A. 将事务隔离级别设置为RC B. 将参数innodb_locks_unsafe_for_binlog设置为1
|
||||||
|
|
||||||
|
### 大表优化
|
||||||
|
|
||||||
|
当MySQL单表记录数过大时,数据库的CRUD性能会明显下降,一些常见的优化措施如下:
|
||||||
|
|
||||||
|
#### 1. 限定数据的范围
|
||||||
|
|
||||||
|
务必禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时候,我们可以控制在一个月的范围内;
|
||||||
|
|
||||||
|
#### 2. 读/写分离
|
||||||
|
|
||||||
|
经典的数据库拆分方案,主库负责写,从库负责读;
|
||||||
|
|
||||||
|
#### 3. 垂直分区
|
||||||
|
|
||||||
|
**根据数据库里面数据表的相关性进行拆分。** 例如,用户表中既有用户的登录信息又有用户的基本信息,可以将用户表拆分成两个单独的表,甚至放到单独的库做分库。
|
||||||
|
|
||||||
|
**简单来说垂直拆分是指数据表列的拆分,把一张列比较多的表拆分为多张表。** 如下图所示,这样来说大家应该就更容易理解了。
|
||||||
|

|
||||||
|
|
||||||
|
- **垂直拆分的优点:** 可以使得列数据变小,在查询时减少读取的Block数,减少I/O次数。此外,垂直分区可以简化表的结构,易于维护。
|
||||||
|
- **垂直拆分的缺点:** 主键会出现冗余,需要管理冗余列,并会引起Join操作,可以通过在应用层进行Join来解决。此外,垂直分区会让事务变得更加复杂;
|
||||||
|
|
||||||
|
#### 4. 水平分区
|
||||||
|
|
||||||
|
**保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表或者库中,达到了分布式的目的。 水平拆分可以支撑非常大的数据量。**
|
||||||
|
|
||||||
|
水平拆分是指数据表行的拆分,表的行数超过200万行时,就会变慢,这时可以把一张的表的数据拆成多张表来存放。举个例子:我们可以将用户信息表拆分成多个用户信息表,这样就可以避免单一表数据量过大对性能造成影响。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
水平拆分可以支持非常大的数据量。需要注意的一点是:分表仅仅是解决了单一表数据过大的问题,但由于表的数据还是在同一台机器上,其实对于提升MySQL并发能力没有什么意义,所以 **水平拆分最好分库** 。
|
||||||
|
|
||||||
|
水平拆分能够 **支持非常大的数据量存储,应用端改造也少**,但 **分片事务难以解决** ,跨节点Join性能较差,逻辑复杂。《Java工程师修炼之道》的作者推荐 **尽量不要对数据进行分片,因为拆分会带来逻辑、部署、运维的各种复杂度** ,一般的数据表在优化得当的情况下支撑千万以下的数据量是没有太大问题的。如果实在要分片,尽量选择客户端分片架构,这样可以减少一次和中间件的网络I/O。
|
||||||
|
|
||||||
|
**下面补充一下数据库分片的两种常见方案:**
|
||||||
|
|
||||||
|
- **客户端代理:** **分片逻辑在应用端,封装在jar包中,通过修改或者封装JDBC层来实现。** 当当网的 **Sharding-JDBC** 、阿里的TDDL是两种比较常用的实现。
|
||||||
|
- **中间件代理:** **在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。** 我们现在谈的 **Mycat** 、360的Atlas、网易的DDB等等都是这种架构的实现。
|
||||||
|
|
||||||
|
详细内容可以参考: MySQL大表优化方案: [https://segmentfault.com/a/1190000006158186](https://segmentfault.com/a/1190000006158186)
|
||||||
|
|
||||||
|
### 解释一下什么是池化设计思想。什么是数据库连接池?为什么需要数据库连接池?
|
||||||
|
|
||||||
|
池化设计应该不是一个新名词。我们常见的如java线程池、jdbc连接池、redis连接池等就是这类设计的代表实现。这种设计会初始预设资源,解决的问题就是抵消每次获取资源的消耗,如创建线程的开销,获取远程连接的开销等。就好比你去食堂打饭,打饭的大妈会先把饭盛好几份放那里,你来了就直接拿着饭盒加菜即可,不用再临时又盛饭又打菜,效率就高了。除了初始化资源,池化设计还包括如下这些特征:池子的初始值、池子的活跃值、池子的最大值等,这些特征可以直接映射到java线程池和数据库连接池的成员属性中。这篇文章对[池化设计思想](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485679&idx=1&sn=57dbca8c9ad49e1f3968ecff04a4f735&chksm=cea24724f9d5ce3212292fac291234a760c99c0960b5430d714269efe33554730b5f71208582&token=1141994790&lang=zh_CN#rd)介绍的还不错,直接复制过来,避免重复造轮子了。
|
||||||
|
|
||||||
|
数据库连接本质就是一个 socket 的连接。数据库服务端还要维护一些缓存和用户权限信息之类的 所以占用了一些内存。我们可以把数据库连接池是看做是维护的数据库连接的缓存,以便将来需要对数据库的请求时可以重用这些连接。为每个用户打开和维护数据库连接,尤其是对动态数据库驱动的网站应用程序的请求,既昂贵又浪费资源。**在连接池中,创建连接后,将其放置在池中,并再次使用它,因此不必建立新的连接。如果使用了所有连接,则会建立一个新连接并将其添加到池中**。 连接池还减少了用户必须等待建立与数据库的连接的时间。
|
||||||
|
|
||||||
|
### 分库分表之后,id 主键如何处理?
|
||||||
|
|
||||||
|
因为要是分成多个表之后,每个表都是从 1 开始累加,这样是不对的,我们需要一个全局唯一的 id 来支持。
|
||||||
|
|
||||||
|
生成全局 id 有下面这几种方式:
|
||||||
|
|
||||||
|
- **UUID**:不适合作为主键,因为太长了,并且无序不可读,查询效率低。比较适合用于生成唯一的名字的标示比如文件的名字。
|
||||||
|
- **数据库自增 id** : 两台数据库分别设置不同步长,生成不重复ID的策略来实现高可用。这种方式生成的 id 有序,但是需要独立部署数据库实例,成本高,还会有性能瓶颈。
|
||||||
|
- **利用 redis 生成 id :** 性能比较好,灵活方便,不依赖于数据库。但是,引入了新的组件造成系统更加复杂,可用性降低,编码更加复杂,增加了系统成本。
|
||||||
|
- **Twitter的snowflake算法** :Github 地址:https://github.com/twitter-archive/snowflake。
|
||||||
|
- **美团的[Leaf](https://tech.meituan.com/2017/04/21/mt-leaf.html)分布式ID生成系统** :Leaf 是美团开源的分布式ID生成器,能保证全局唯一性、趋势递增、单调递增、信息安全,里面也提到了几种分布式方案的对比,但也需要依赖关系数据库、Zookeeper等中间件。感觉还不错。美团技术团队的一篇文章:https://tech.meituan.com/2017/04/21/mt-leaf.html 。
|
||||||
|
- ......
|
||||||
|
|
||||||
|
### 一条SQL语句在MySQL中如何执行的
|
||||||
|
|
||||||
|
[一条SQL语句在MySQL中如何执行的](<https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485097&idx=1&sn=84c89da477b1338bdf3e9fcd65514ac1&chksm=cea24962f9d5c074d8d3ff1ab04ee8f0d6486e3d015cfd783503685986485c11738ccb542ba7&token=79317275&lang=zh_CN#rd>)
|
||||||
|
|
||||||
|
### MySQL高性能优化规范建议
|
||||||
|
|
||||||
|
[MySQL高性能优化规范建议](<https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485117&idx=1&sn=92361755b7c3de488b415ec4c5f46d73&chksm=cea24976f9d5c060babe50c3747616cce63df5d50947903a262704988143c2eeb4069ae45420&token=79317275&lang=zh_CN#rd>)
|
||||||
|
|
||||||
|
### 一条SQL语句执行得很慢的原因有哪些?
|
||||||
|
|
||||||
|
[腾讯面试:一条SQL语句执行得很慢的原因有哪些?---不看后悔系列](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485185&idx=1&sn=66ef08b4ab6af5757792223a83fc0d45&chksm=cea248caf9d5c1dc72ec8a281ec16aa3ec3e8066dbb252e27362438a26c33fbe842b0e0adf47&token=79317275&lang=zh_CN#rd)
|
||||||
|
|
||||||
|
## 公众号
|
||||||
|
|
||||||
|
如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。
|
||||||
|
|
||||||
|
**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取!
|
||||||
|
|
||||||
|
**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。
|
||||||
|
|
||||||
|

|
||||||
|
441
docs/database/MySQL高性能优化规范建议.md
Normal file
441
docs/database/MySQL高性能优化规范建议.md
Normal file
@ -0,0 +1,441 @@
|
|||||||
|
> 作者: 听风,原文地址: <https://www.cnblogs.com/huchong/p/10219318.html>。JavaGuide 已获得作者授权。
|
||||||
|
|
||||||
|
<!-- TOC -->
|
||||||
|
|
||||||
|
- [数据库命令规范](#数据库命令规范)
|
||||||
|
- [数据库基本设计规范](#数据库基本设计规范)
|
||||||
|
- [1. 所有表必须使用 Innodb 存储引擎](#1-所有表必须使用-innodb-存储引擎)
|
||||||
|
- [2. 数据库和表的字符集统一使用 UTF8](#2-数据库和表的字符集统一使用-utf8)
|
||||||
|
- [3. 所有表和字段都需要添加注释](#3-所有表和字段都需要添加注释)
|
||||||
|
- [4. 尽量控制单表数据量的大小,建议控制在 500 万以内。](#4-尽量控制单表数据量的大小建议控制在-500-万以内)
|
||||||
|
- [5. 谨慎使用 MySQL 分区表](#5-谨慎使用-mysql-分区表)
|
||||||
|
- [6.尽量做到冷热数据分离,减小表的宽度](#6尽量做到冷热数据分离减小表的宽度)
|
||||||
|
- [7. 禁止在表中建立预留字段](#7-禁止在表中建立预留字段)
|
||||||
|
- [8. 禁止在数据库中存储图片,文件等大的二进制数据](#8-禁止在数据库中存储图片文件等大的二进制数据)
|
||||||
|
- [9. 禁止在线上做数据库压力测试](#9-禁止在线上做数据库压力测试)
|
||||||
|
- [10. 禁止从开发环境,测试环境直接连接生成环境数据库](#10-禁止从开发环境测试环境直接连接生成环境数据库)
|
||||||
|
- [数据库字段设计规范](#数据库字段设计规范)
|
||||||
|
- [1. 优先选择符合存储需要的最小的数据类型](#1-优先选择符合存储需要的最小的数据类型)
|
||||||
|
- [2. 避免使用 TEXT,BLOB 数据类型,最常见的 TEXT 类型可以存储 64k 的数据](#2-避免使用-textblob-数据类型最常见的-text-类型可以存储-64k-的数据)
|
||||||
|
- [3. 避免使用 ENUM 类型](#3-避免使用-enum-类型)
|
||||||
|
- [4. 尽可能把所有列定义为 NOT NULL](#4-尽可能把所有列定义为-not-null)
|
||||||
|
- [5. 使用 TIMESTAMP(4 个字节) 或 DATETIME 类型 (8 个字节) 存储时间](#5-使用-timestamp4-个字节-或-datetime-类型-8-个字节-存储时间)
|
||||||
|
- [6. 同财务相关的金额类数据必须使用 decimal 类型](#6-同财务相关的金额类数据必须使用-decimal-类型)
|
||||||
|
- [索引设计规范](#索引设计规范)
|
||||||
|
- [1. 限制每张表上的索引数量,建议单张表索引不超过 5 个](#1-限制每张表上的索引数量建议单张表索引不超过-5-个)
|
||||||
|
- [2. 禁止给表中的每一列都建立单独的索引](#2-禁止给表中的每一列都建立单独的索引)
|
||||||
|
- [3. 每个 Innodb 表必须有个主键](#3-每个-innodb-表必须有个主键)
|
||||||
|
- [4. 常见索引列建议](#4-常见索引列建议)
|
||||||
|
- [5.如何选择索引列的顺序](#5如何选择索引列的顺序)
|
||||||
|
- [6. 避免建立冗余索引和重复索引(增加了查询优化器生成执行计划的时间)](#6-避免建立冗余索引和重复索引增加了查询优化器生成执行计划的时间)
|
||||||
|
- [7. 对于频繁的查询优先考虑使用覆盖索引](#7-对于频繁的查询优先考虑使用覆盖索引)
|
||||||
|
- [8.索引 SET 规范](#8索引-set-规范)
|
||||||
|
- [数据库 SQL 开发规范](#数据库-sql-开发规范)
|
||||||
|
- [1. 建议使用预编译语句进行数据库操作](#1-建议使用预编译语句进行数据库操作)
|
||||||
|
- [2. 避免数据类型的隐式转换](#2-避免数据类型的隐式转换)
|
||||||
|
- [3. 充分利用表上已经存在的索引](#3-充分利用表上已经存在的索引)
|
||||||
|
- [4. 数据库设计时,应该要对以后扩展进行考虑](#4-数据库设计时应该要对以后扩展进行考虑)
|
||||||
|
- [5. 程序连接不同的数据库使用不同的账号,禁止跨库查询](#5-程序连接不同的数据库使用不同的账号禁止跨库查询)
|
||||||
|
- [6. 禁止使用 SELECT * 必须使用 SELECT <字段列表> 查询](#6-禁止使用-select--必须使用-select-字段列表-查询)
|
||||||
|
- [7. 禁止使用不含字段列表的 INSERT 语句](#7-禁止使用不含字段列表的-insert-语句)
|
||||||
|
- [8. 避免使用子查询,可以把子查询优化为 join 操作](#8-避免使用子查询可以把子查询优化为-join-操作)
|
||||||
|
- [9. 避免使用 JOIN 关联太多的表](#9-避免使用-join-关联太多的表)
|
||||||
|
- [10. 减少同数据库的交互次数](#10-减少同数据库的交互次数)
|
||||||
|
- [11. 对应同一列进行 or 判断时,使用 in 代替 or](#11-对应同一列进行-or-判断时使用-in-代替-or)
|
||||||
|
- [12. 禁止使用 order by rand() 进行随机排序](#12-禁止使用-order-by-rand-进行随机排序)
|
||||||
|
- [13. WHERE 从句中禁止对列进行函数转换和计算](#13-where-从句中禁止对列进行函数转换和计算)
|
||||||
|
- [14. 在明显不会有重复值时使用 UNION ALL 而不是 UNION](#14-在明显不会有重复值时使用-union-all-而不是-union)
|
||||||
|
- [15. 拆分复杂的大 SQL 为多个小 SQL](#15-拆分复杂的大-sql-为多个小-sql)
|
||||||
|
- [数据库操作行为规范](#数据库操作行为规范)
|
||||||
|
- [1. 超 100 万行的批量写 (UPDATE,DELETE,INSERT) 操作,要分批多次进行操作](#1-超-100-万行的批量写-updatedeleteinsert-操作要分批多次进行操作)
|
||||||
|
- [2. 对于大表使用 pt-online-schema-change 修改表结构](#2-对于大表使用-pt-online-schema-change-修改表结构)
|
||||||
|
- [3. 禁止为程序使用的账号赋予 super 权限](#3-禁止为程序使用的账号赋予-super-权限)
|
||||||
|
- [4. 对于程序连接数据库账号,遵循权限最小原则](#4-对于程序连接数据库账号遵循权限最小原则)
|
||||||
|
|
||||||
|
<!-- /TOC -->
|
||||||
|
|
||||||
|
## 数据库命令规范
|
||||||
|
|
||||||
|
- 所有数据库对象名称必须使用小写字母并用下划线分割
|
||||||
|
- 所有数据库对象名称禁止使用 MySQL 保留关键字(如果表名中包含关键字查询时,需要将其用单引号括起来)
|
||||||
|
- 数据库对象的命名要能做到见名识意,并且最后不要超过 32 个字符
|
||||||
|
- 临时库表必须以 tmp_为前缀并以日期为后缀,备份表必须以 bak_为前缀并以日期 (时间戳) 为后缀
|
||||||
|
- 所有存储相同数据的列名和列类型必须一致(一般作为关联列,如果查询时关联列类型不一致会自动进行数据类型隐式转换,会造成列上的索引失效,导致查询效率降低)
|
||||||
|
|
||||||
|
------
|
||||||
|
|
||||||
|
## 数据库基本设计规范
|
||||||
|
|
||||||
|
### 1. 所有表必须使用 Innodb 存储引擎
|
||||||
|
|
||||||
|
没有特殊要求(即 Innodb 无法满足的功能如:列存储,存储空间数据等)的情况下,所有表必须使用 Innodb 存储引擎(MySQL5.5 之前默认使用 Myisam,5.6 以后默认的为 Innodb)。
|
||||||
|
|
||||||
|
Innodb 支持事务,支持行级锁,更好的恢复性,高并发下性能更好。
|
||||||
|
|
||||||
|
### 2. 数据库和表的字符集统一使用 UTF8
|
||||||
|
|
||||||
|
兼容性更好,统一字符集可以避免由于字符集转换产生的乱码,不同的字符集进行比较前需要进行转换会造成索引失效,如果数据库中有存储 emoji 表情的需要,字符集需要采用 utf8mb4 字符集。
|
||||||
|
|
||||||
|
### 3. 所有表和字段都需要添加注释
|
||||||
|
|
||||||
|
使用 comment 从句添加表和列的备注,从一开始就进行数据字典的维护
|
||||||
|
|
||||||
|
### 4. 尽量控制单表数据量的大小,建议控制在 500 万以内。
|
||||||
|
|
||||||
|
500 万并不是 MySQL 数据库的限制,过大会造成修改表结构,备份,恢复都会有很大的问题。
|
||||||
|
|
||||||
|
可以用历史数据归档(应用于日志数据),分库分表(应用于业务数据)等手段来控制数据量大小
|
||||||
|
|
||||||
|
### 5. 谨慎使用 MySQL 分区表
|
||||||
|
|
||||||
|
分区表在物理上表现为多个文件,在逻辑上表现为一个表;
|
||||||
|
|
||||||
|
谨慎选择分区键,跨分区查询效率可能更低;
|
||||||
|
|
||||||
|
建议采用物理分表的方式管理大数据。
|
||||||
|
|
||||||
|
### 6.尽量做到冷热数据分离,减小表的宽度
|
||||||
|
|
||||||
|
> MySQL 限制每个表最多存储 4096 列,并且每一行数据的大小不能超过 65535 字节。
|
||||||
|
|
||||||
|
减少磁盘 IO,保证热数据的内存缓存命中率(表越宽,把表装载进内存缓冲池时所占用的内存也就越大,也会消耗更多的 IO);
|
||||||
|
|
||||||
|
更有效的利用缓存,避免读入无用的冷数据;
|
||||||
|
|
||||||
|
经常一起使用的列放到一个表中(避免更多的关联操作)。
|
||||||
|
|
||||||
|
### 7. 禁止在表中建立预留字段
|
||||||
|
|
||||||
|
预留字段的命名很难做到见名识义。
|
||||||
|
|
||||||
|
预留字段无法确认存储的数据类型,所以无法选择合适的类型。
|
||||||
|
|
||||||
|
对预留字段类型的修改,会对表进行锁定。
|
||||||
|
|
||||||
|
### 8. 禁止在数据库中存储图片,文件等大的二进制数据
|
||||||
|
|
||||||
|
通常文件很大,会短时间内造成数据量快速增长,数据库进行数据库读取时,通常会进行大量的随机 IO 操作,文件很大时,IO 操作很耗时。
|
||||||
|
|
||||||
|
通常存储于文件服务器,数据库只存储文件地址信息
|
||||||
|
|
||||||
|
### 9. 禁止在线上做数据库压力测试
|
||||||
|
|
||||||
|
### 10. 禁止从开发环境,测试环境直接连接生产环境数据库
|
||||||
|
|
||||||
|
------
|
||||||
|
|
||||||
|
## 数据库字段设计规范
|
||||||
|
|
||||||
|
### 1. 优先选择符合存储需要的最小的数据类型
|
||||||
|
|
||||||
|
**原因:**
|
||||||
|
|
||||||
|
列的字段越大,建立索引时所需要的空间也就越大,这样一页中所能存储的索引节点的数量也就越少也越少,在遍历时所需要的 IO 次数也就越多,索引的性能也就越差。
|
||||||
|
|
||||||
|
**方法:**
|
||||||
|
|
||||||
|
**a.将字符串转换成数字类型存储,如:将 IP 地址转换成整形数据**
|
||||||
|
|
||||||
|
MySQL 提供了两个方法来处理 ip 地址
|
||||||
|
|
||||||
|
- inet_aton 把 ip 转为无符号整型 (4-8 位)
|
||||||
|
- inet_ntoa 把整型的 ip 转为地址
|
||||||
|
|
||||||
|
插入数据前,先用 inet_aton 把 ip 地址转为整型,可以节省空间,显示数据时,使用 inet_ntoa 把整型的 ip 地址转为地址显示即可。
|
||||||
|
|
||||||
|
**b.对于非负型的数据 (如自增 ID,整型 IP) 来说,要优先使用无符号整型来存储**
|
||||||
|
|
||||||
|
**原因:**
|
||||||
|
|
||||||
|
无符号相对于有符号可以多出一倍的存储空间
|
||||||
|
|
||||||
|
```
|
||||||
|
SIGNED INT -2147483648~2147483647
|
||||||
|
UNSIGNED INT 0~4294967295
|
||||||
|
```
|
||||||
|
|
||||||
|
VARCHAR(N) 中的 N 代表的是字符数,而不是字节数,使用 UTF8 存储 255 个汉字 Varchar(255)=765 个字节。**过大的长度会消耗更多的内存。**
|
||||||
|
|
||||||
|
### 2. 避免使用 TEXT,BLOB 数据类型,最常见的 TEXT 类型可以存储 64k 的数据
|
||||||
|
|
||||||
|
**a. 建议把 BLOB 或是 TEXT 列分离到单独的扩展表中**
|
||||||
|
|
||||||
|
MySQL 内存临时表不支持 TEXT、BLOB 这样的大数据类型,如果查询中包含这样的数据,在排序等操作时,就不能使用内存临时表,必须使用磁盘临时表进行。而且对于这种数据,MySQL 还是要进行二次查询,会使 sql 性能变得很差,但是不是说一定不能使用这样的数据类型。
|
||||||
|
|
||||||
|
如果一定要使用,建议把 BLOB 或是 TEXT 列分离到单独的扩展表中,查询时一定不要使用 select * 而只需要取出必要的列,不需要 TEXT 列的数据时不要对该列进行查询。
|
||||||
|
|
||||||
|
**2、TEXT 或 BLOB 类型只能使用前缀索引**
|
||||||
|
|
||||||
|
因为[MySQL](http://mp.weixin.qq.com/s?__biz=MzI4Njc5NjM1NQ==&mid=2247487885&idx=1&sn=65b1bf5f7d4505502620179669a9c2df&chksm=ebd62ea1dca1a7b7bf884bcd9d538d78ba064ee03c09436ca8e57873b1d98a55afd6d7884cfc&scene=21#wechat_redirect) 对索引字段长度是有限制的,所以 TEXT 类型只能使用前缀索引,并且 TEXT 列上是不能有默认值的
|
||||||
|
|
||||||
|
### 3. 避免使用 ENUM 类型
|
||||||
|
|
||||||
|
修改 ENUM 值需要使用 ALTER 语句
|
||||||
|
|
||||||
|
ENUM 类型的 ORDER BY 操作效率低,需要额外操作
|
||||||
|
|
||||||
|
禁止使用数值作为 ENUM 的枚举值
|
||||||
|
|
||||||
|
### 4. 尽可能把所有列定义为 NOT NULL
|
||||||
|
|
||||||
|
**原因:**
|
||||||
|
|
||||||
|
索引 NULL 列需要额外的空间来保存,所以要占用更多的空间
|
||||||
|
|
||||||
|
进行比较和计算时要对 NULL 值做特别的处理
|
||||||
|
|
||||||
|
### 5. 使用 TIMESTAMP(4 个字节) 或 DATETIME 类型 (8 个字节) 存储时间
|
||||||
|
|
||||||
|
TIMESTAMP 存储的时间范围 1970-01-01 00:00:01 ~ 2038-01-19-03:14:07
|
||||||
|
|
||||||
|
TIMESTAMP 占用 4 字节和 INT 相同,但比 INT 可读性高
|
||||||
|
|
||||||
|
超出 TIMESTAMP 取值范围的使用 DATETIME 类型存储
|
||||||
|
|
||||||
|
**经常会有人用字符串存储日期型的数据(不正确的做法)**
|
||||||
|
|
||||||
|
- 缺点 1:无法用日期函数进行计算和比较
|
||||||
|
- 缺点 2:用字符串存储日期要占用更多的空间
|
||||||
|
|
||||||
|
### 6. 同财务相关的金额类数据必须使用 decimal 类型
|
||||||
|
|
||||||
|
- 非精准浮点:float,double
|
||||||
|
- 精准浮点:decimal
|
||||||
|
|
||||||
|
Decimal 类型为精准浮点数,在计算时不会丢失精度
|
||||||
|
|
||||||
|
占用空间由定义的宽度决定,每 4 个字节可以存储 9 位数字,并且小数点要占用一个字节
|
||||||
|
|
||||||
|
可用于存储比 bigint 更大的整型数据
|
||||||
|
|
||||||
|
------
|
||||||
|
|
||||||
|
## 索引设计规范
|
||||||
|
|
||||||
|
### 1. 限制每张表上的索引数量,建议单张表索引不超过 5 个
|
||||||
|
|
||||||
|
索引并不是越多越好!索引可以提高效率同样可以降低效率。
|
||||||
|
|
||||||
|
索引可以增加查询效率,但同样也会降低插入和更新的效率,甚至有些情况下会降低查询效率。
|
||||||
|
|
||||||
|
因为 MySQL 优化器在选择如何优化查询时,会根据统一信息,对每一个可以用到的索引来进行评估,以生成出一个最好的执行计划,如果同时有很多个索引都可以用于查询,就会增加 MySQL 优化器生成执行计划的时间,同样会降低查询性能。
|
||||||
|
|
||||||
|
### 2. 禁止给表中的每一列都建立单独的索引
|
||||||
|
|
||||||
|
5.6 版本之前,一个 sql 只能使用到一个表中的一个索引,5.6 以后,虽然有了合并索引的优化方式,但是还是远远没有使用一个联合索引的查询方式好。
|
||||||
|
|
||||||
|
### 3. 每个 Innodb 表必须有个主键
|
||||||
|
|
||||||
|
Innodb 是一种索引组织表:数据的存储的逻辑顺序和索引的顺序是相同的。每个表都可以有多个索引,但是表的存储顺序只能有一种。
|
||||||
|
|
||||||
|
Innodb 是按照主键索引的顺序来组织表的
|
||||||
|
|
||||||
|
- 不要使用更新频繁的列作为主键,不适用多列主键(相当于联合索引)
|
||||||
|
- 不要使用 UUID,MD5,HASH,字符串列作为主键(无法保证数据的顺序增长)
|
||||||
|
- 主键建议使用自增 ID 值
|
||||||
|
|
||||||
|
------
|
||||||
|
|
||||||
|
### 4. 常见索引列建议
|
||||||
|
|
||||||
|
- 出现在 SELECT、UPDATE、DELETE 语句的 WHERE 从句中的列
|
||||||
|
- 包含在 ORDER BY、GROUP BY、DISTINCT 中的字段
|
||||||
|
- 并不要将符合 1 和 2 中的字段的列都建立一个索引, 通常将 1、2 中的字段建立联合索引效果更好
|
||||||
|
- 多表 join 的关联列
|
||||||
|
|
||||||
|
------
|
||||||
|
|
||||||
|
### 5.如何选择索引列的顺序
|
||||||
|
|
||||||
|
建立索引的目的是:希望通过索引进行数据查找,减少随机 IO,增加查询性能 ,索引能过滤出越少的数据,则从磁盘中读入的数据也就越少。
|
||||||
|
|
||||||
|
- 区分度最高的放在联合索引的最左侧(区分度=列中不同值的数量/列的总行数)
|
||||||
|
- 尽量把字段长度小的列放在联合索引的最左侧(因为字段长度越小,一页能存储的数据量越大,IO 性能也就越好)
|
||||||
|
- 使用最频繁的列放到联合索引的左侧(这样可以比较少的建立一些索引)
|
||||||
|
|
||||||
|
------
|
||||||
|
|
||||||
|
### 6. 避免建立冗余索引和重复索引(增加了查询优化器生成执行计划的时间)
|
||||||
|
|
||||||
|
- 重复索引示例:primary key(id)、index(id)、unique index(id)
|
||||||
|
- 冗余索引示例:index(a,b,c)、index(a,b)、index(a)
|
||||||
|
|
||||||
|
------
|
||||||
|
|
||||||
|
### 7. 对于频繁的查询优先考虑使用覆盖索引
|
||||||
|
|
||||||
|
> 覆盖索引:就是包含了所有查询字段 (where,select,ordery by,group by 包含的字段) 的索引
|
||||||
|
|
||||||
|
**覆盖索引的好处:**
|
||||||
|
|
||||||
|
- **避免 Innodb 表进行索引的二次查询:** Innodb 是以聚集索引的顺序来存储的,对于 Innodb 来说,二级索引在叶子节点中所保存的是行的主键信息,如果是用二级索引查询数据的话,在查找到相应的键值后,还要通过主键进行二次查询才能获取我们真实所需要的数据。而在覆盖索引中,二级索引的键值中可以获取所有的数据,避免了对主键的二次查询 ,减少了 IO 操作,提升了查询效率。
|
||||||
|
- **可以把随机 IO 变成顺序 IO 加快查询效率:** 由于覆盖索引是按键值的顺序存储的,对于 IO 密集型的范围查找来说,对比随机从磁盘读取每一行的数据 IO 要少的多,因此利用覆盖索引在访问时也可以把磁盘的随机读取的 IO 转变成索引查找的顺序 IO。
|
||||||
|
|
||||||
|
------
|
||||||
|
|
||||||
|
### 8.索引 SET 规范
|
||||||
|
|
||||||
|
**尽量避免使用外键约束**
|
||||||
|
|
||||||
|
- 不建议使用外键约束(foreign key),但一定要在表与表之间的关联键上建立索引
|
||||||
|
- 外键可用于保证数据的参照完整性,但建议在业务端实现
|
||||||
|
- 外键会影响父表和子表的写操作从而降低性能
|
||||||
|
|
||||||
|
------
|
||||||
|
|
||||||
|
## 数据库 SQL 开发规范
|
||||||
|
|
||||||
|
### 1. 建议使用预编译语句进行数据库操作
|
||||||
|
|
||||||
|
预编译语句可以重复使用这些计划,减少 SQL 编译所需要的时间,还可以解决动态 SQL 所带来的 SQL 注入的问题。
|
||||||
|
|
||||||
|
只传参数,比传递 SQL 语句更高效。
|
||||||
|
|
||||||
|
相同语句可以一次解析,多次使用,提高处理效率。
|
||||||
|
|
||||||
|
### 2. 避免数据类型的隐式转换
|
||||||
|
|
||||||
|
隐式转换会导致索引失效如:
|
||||||
|
|
||||||
|
```
|
||||||
|
select name,phone from customer where id = '111';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 充分利用表上已经存在的索引
|
||||||
|
|
||||||
|
避免使用双%号的查询条件。如:`a like '%123%'`,(如果无前置%,只有后置%,是可以用到列上的索引的)
|
||||||
|
|
||||||
|
一个 SQL 只能利用到复合索引中的一列进行范围查询。如:有 a,b,c 列的联合索引,在查询条件中有 a 列的范围查询,则在 b,c 列上的索引将不会被用到。
|
||||||
|
|
||||||
|
在定义联合索引时,如果 a 列要用到范围查找的话,就要把 a 列放到联合索引的右侧,使用 left join 或 not exists 来优化 not in 操作,因为 not in 也通常会使用索引失效。
|
||||||
|
|
||||||
|
### 4. 数据库设计时,应该要对以后扩展进行考虑
|
||||||
|
|
||||||
|
### 5. 程序连接不同的数据库使用不同的账号,禁止跨库查询
|
||||||
|
|
||||||
|
- 为数据库迁移和分库分表留出余地
|
||||||
|
- 降低业务耦合度
|
||||||
|
- 避免权限过大而产生的安全风险
|
||||||
|
|
||||||
|
### 6. 禁止使用 SELECT * 必须使用 SELECT <字段列表> 查询
|
||||||
|
|
||||||
|
**原因:**
|
||||||
|
|
||||||
|
- 消耗更多的 CPU 和 IO 以网络带宽资源
|
||||||
|
- 无法使用覆盖索引
|
||||||
|
- 可减少表结构变更带来的影响
|
||||||
|
|
||||||
|
### 7. 禁止使用不含字段列表的 INSERT 语句
|
||||||
|
|
||||||
|
如:
|
||||||
|
|
||||||
|
```
|
||||||
|
insert into values ('a','b','c');
|
||||||
|
```
|
||||||
|
|
||||||
|
应使用:
|
||||||
|
|
||||||
|
```
|
||||||
|
insert into t(c1,c2,c3) values ('a','b','c');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. 避免使用子查询,可以把子查询优化为 join 操作
|
||||||
|
|
||||||
|
通常子查询在 in 子句中,且子查询中为简单 SQL(不包含 union、group by、order by、limit 从句) 时,才可以把子查询转化为关联查询进行优化。
|
||||||
|
|
||||||
|
**子查询性能差的原因:**
|
||||||
|
|
||||||
|
子查询的结果集无法使用索引,通常子查询的结果集会被存储到临时表中,不论是内存临时表还是磁盘临时表都不会存在索引,所以查询性能会受到一定的影响。特别是对于返回结果集比较大的子查询,其对查询性能的影响也就越大。
|
||||||
|
|
||||||
|
由于子查询会产生大量的临时表也没有索引,所以会消耗过多的 CPU 和 IO 资源,产生大量的慢查询。
|
||||||
|
|
||||||
|
### 9. 避免使用 JOIN 关联太多的表
|
||||||
|
|
||||||
|
对于 MySQL 来说,是存在关联缓存的,缓存的大小可以由 join_buffer_size 参数进行设置。
|
||||||
|
|
||||||
|
在 MySQL 中,对于同一个 SQL 多关联(join)一个表,就会多分配一个关联缓存,如果在一个 SQL 中关联的表越多,所占用的内存也就越大。
|
||||||
|
|
||||||
|
如果程序中大量的使用了多表关联的操作,同时 join_buffer_size 设置的也不合理的情况下,就容易造成服务器内存溢出的情况,就会影响到服务器数据库性能的稳定性。
|
||||||
|
|
||||||
|
同时对于关联操作来说,会产生临时表操作,影响查询效率,MySQL 最多允许关联 61 个表,建议不超过 5 个。
|
||||||
|
|
||||||
|
### 10. 减少同数据库的交互次数
|
||||||
|
|
||||||
|
数据库更适合处理批量操作,合并多个相同的操作到一起,可以提高处理效率。
|
||||||
|
|
||||||
|
### 11. 对应同一列进行 or 判断时,使用 in 代替 or
|
||||||
|
|
||||||
|
in 的值不要超过 500 个,in 操作可以更有效的利用索引,or 大多数情况下很少能利用到索引。
|
||||||
|
|
||||||
|
### 12. 禁止使用 order by rand() 进行随机排序
|
||||||
|
|
||||||
|
order by rand() 会把表中所有符合条件的数据装载到内存中,然后在内存中对所有数据根据随机生成的值进行排序,并且可能会对每一行都生成一个随机值,如果满足条件的数据集非常大,就会消耗大量的 CPU 和 IO 及内存资源。
|
||||||
|
|
||||||
|
推荐在程序中获取一个随机值,然后从数据库中获取数据的方式。
|
||||||
|
|
||||||
|
### 13. WHERE 从句中禁止对列进行函数转换和计算
|
||||||
|
|
||||||
|
对列进行函数转换或计算时会导致无法使用索引
|
||||||
|
|
||||||
|
**不推荐:**
|
||||||
|
|
||||||
|
```
|
||||||
|
where date(create_time)='20190101'
|
||||||
|
```
|
||||||
|
|
||||||
|
**推荐:**
|
||||||
|
|
||||||
|
```
|
||||||
|
where create_time >= '20190101' and create_time < '20190102'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 14. 在明显不会有重复值时使用 UNION ALL 而不是 UNION
|
||||||
|
|
||||||
|
- UNION 会把两个结果集的所有数据放到临时表中后再进行去重操作
|
||||||
|
- UNION ALL 不会再对结果集进行去重操作
|
||||||
|
|
||||||
|
### 15. 拆分复杂的大 SQL 为多个小 SQL
|
||||||
|
|
||||||
|
- 大 SQL 逻辑上比较复杂,需要占用大量 CPU 进行计算的 SQL
|
||||||
|
- MySQL 中,一个 SQL 只能使用一个 CPU 进行计算
|
||||||
|
- SQL 拆分后可以通过并行执行来提高处理效率
|
||||||
|
|
||||||
|
------
|
||||||
|
|
||||||
|
## 数据库操作行为规范
|
||||||
|
|
||||||
|
### 1. 超 100 万行的批量写 (UPDATE,DELETE,INSERT) 操作,要分批多次进行操作
|
||||||
|
|
||||||
|
**大批量操作可能会造成严重的主从延迟**
|
||||||
|
|
||||||
|
主从环境中,大批量操作可能会造成严重的主从延迟,大批量的写操作一般都需要执行一定长的时间,
|
||||||
|
而只有当主库上执行完成后,才会在其他从库上执行,所以会造成主库与从库长时间的延迟情况
|
||||||
|
|
||||||
|
**binlog 日志为 row 格式时会产生大量的日志**
|
||||||
|
|
||||||
|
大批量写操作会产生大量日志,特别是对于 row 格式二进制数据而言,由于在 row 格式中会记录每一行数据的修改,我们一次修改的数据越多,产生的日志量也就会越多,日志的传输和恢复所需要的时间也就越长,这也是造成主从延迟的一个原因
|
||||||
|
|
||||||
|
**避免产生大事务操作**
|
||||||
|
|
||||||
|
大批量修改数据,一定是在一个事务中进行的,这就会造成表中大批量数据进行锁定,从而导致大量的阻塞,阻塞会对 MySQL 的性能产生非常大的影响。
|
||||||
|
|
||||||
|
特别是长时间的阻塞会占满所有数据库的可用连接,这会使生产环境中的其他应用无法连接到数据库,因此一定要注意大批量写操作要进行分批
|
||||||
|
|
||||||
|
### 2. 对于大表使用 pt-online-schema-change 修改表结构
|
||||||
|
|
||||||
|
- 避免大表修改产生的主从延迟
|
||||||
|
- 避免在对表字段进行修改时进行锁表
|
||||||
|
|
||||||
|
对大表数据结构的修改一定要谨慎,会造成严重的锁表操作,尤其是生产环境,是不能容忍的。
|
||||||
|
|
||||||
|
pt-online-schema-change 它会首先建立一个与原表结构相同的新表,并且在新表上进行表结构的修改,然后再把原表中的数据复制到新表中,并在原表中增加一些触发器。把原表中新增的数据也复制到新表中,在行所有数据复制完成之后,把新表命名成原表,并把原来的表删除掉。把原来一个 DDL 操作,分解成多个小的批次进行。
|
||||||
|
|
||||||
|
### 3. 禁止为程序使用的账号赋予 super 权限
|
||||||
|
|
||||||
|
- 当达到最大连接数限制时,还运行 1 个有 super 权限的用户连接
|
||||||
|
- super 权限只能留给 DBA 处理问题的账号使用
|
||||||
|
|
||||||
|
### 4. 对于程序连接数据库账号,遵循权限最小原则
|
||||||
|
|
||||||
|
- 程序使用数据库账号只能在一个 DB 下使用,不准跨库
|
||||||
|
- 程序使用的账号原则上不准有 drop 权限
|
@ -1,38 +1,38 @@
|
|||||||
<!-- MarkdownTOC -->
|
点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。
|
||||||
|
|
||||||
|
<!-- TOC -->
|
||||||
|
|
||||||
- [redis 简介](#redis-简介)
|
- [redis 简介](#redis-简介)
|
||||||
- [为什么要用 redis /为什么要用缓存](#为什么要用-redis-为什么要用缓存)
|
- [为什么要用 redis/为什么要用缓存](#为什么要用-redis为什么要用缓存)
|
||||||
- [为什么要用 redis 而不用 map/guava 做缓存?](#为什么要用-redis-而不用-mapguava-做缓存)
|
- [为什么要用 redis 而不用 map/guava 做缓存?](#为什么要用-redis-而不用-mapguava-做缓存)
|
||||||
- [redis 和 memcached 的区别](#redis-和-memcached-的区别)
|
- [redis 和 memcached 的区别](#redis-和-memcached-的区别)
|
||||||
- [redis 常见数据结构以及使用场景分析](#redis-常见数据结构以及使用场景分析)
|
- [redis 常见数据结构以及使用场景分析](#redis-常见数据结构以及使用场景分析)
|
||||||
- [1. String](#1-string)
|
- [1.String](#1string)
|
||||||
- [2.Hash](#2hash)
|
- [2.Hash](#2hash)
|
||||||
- [3.List](#3list)
|
- [3.List](#3list)
|
||||||
- [4.Set](#4set)
|
- [4.Set](#4set)
|
||||||
- [5.Sorted Set](#5sorted-set)
|
- [5.Sorted Set](#5sorted-set)
|
||||||
- [redis 设置过期时间](#redis-设置过期时间)
|
- [redis 设置过期时间](#redis-设置过期时间)
|
||||||
- [redis 内存淘汰机制(MySQL里有2000w数据,Redis中只存20w的数据,如何保证Redis中的数据都是热点数据?)](#redis-内存淘汰机制(mysql里有2000w数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据?))
|
- [redis 内存淘汰机制(MySQL里有2000w数据,Redis中只存20w的数据,如何保证Redis中的数据都是热点数据?)](#redis-内存淘汰机制mysql里有2000w数据redis中只存20w的数据如何保证redis中的数据都是热点数据)
|
||||||
- [redis 持久化机制(怎么保证 redis 挂掉之后再重启数据可以进行恢复)](#redis-持久化机制(怎么保证-redis-挂掉之后再重启数据可以进行恢复))
|
- [redis 持久化机制(怎么保证 redis 挂掉之后再重启数据可以进行恢复)](#redis-持久化机制怎么保证-redis-挂掉之后再重启数据可以进行恢复)
|
||||||
- [redis 事务](#redis-事务)
|
- [redis 事务](#redis-事务)
|
||||||
- [缓存雪崩和缓存穿透问题解决方案](#缓存雪崩和缓存穿透问题解决方案)
|
- [缓存雪崩和缓存穿透问题解决方案](#缓存雪崩和缓存穿透问题解决方案)
|
||||||
- [如何解决 Redis 的并发竞争 Key 问题](#如何解决-redis-的并发竞争-key-问题)
|
- [如何解决 Redis 的并发竞争 Key 问题](#如何解决-redis-的并发竞争-key-问题)
|
||||||
- [如何保证缓存与数据库双写时的数据一致性?](#如何保证缓存与数据库双写时的数据一致性?)
|
- [如何保证缓存与数据库双写时的数据一致性?](#如何保证缓存与数据库双写时的数据一致性)
|
||||||
- [参考:](#参考:)
|
|
||||||
|
|
||||||
<!-- /MarkdownTOC -->
|
|
||||||
|
|
||||||
|
<!-- /TOC -->
|
||||||
|
|
||||||
### redis 简介
|
### redis 简介
|
||||||
|
|
||||||
简单来说 redis 就是一个数据库,不过与传统数据库不同的是 redis 的数据是存在内存中的,所以存写速度非常快,因此 redis 被广泛应用于缓存方向。另外,redis 也经常用来做分布式锁。redis 提供了多种数据类型来支持不同的业务场景。除此之外,redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。
|
简单来说 redis 就是一个数据库,不过与传统数据库不同的是 redis 的数据是存在内存中的,所以读写速度非常快,因此 redis 被广泛应用于缓存方向。另外,redis 也经常用来做分布式锁。redis 提供了多种数据类型来支持不同的业务场景。除此之外,redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。
|
||||||
|
|
||||||
### 为什么要用 redis /为什么要用缓存
|
### 为什么要用 redis/为什么要用缓存
|
||||||
|
|
||||||
主要从“高性能”和“高并发”这两点来看待这个问题。
|
主要从“高性能”和“高并发”这两点来看待这个问题。
|
||||||
|
|
||||||
**高性能:**
|
**高性能:**
|
||||||
|
|
||||||
假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
|
假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@ -54,6 +54,21 @@
|
|||||||
|
|
||||||
使用 redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持 redis 或 memcached服务的高可用,整个程序架构上较为复杂。
|
使用 redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持 redis 或 memcached服务的高可用,整个程序架构上较为复杂。
|
||||||
|
|
||||||
|
### redis 的线程模型
|
||||||
|
|
||||||
|
> 参考地址:https://www.javazhiyin.com/22943.html
|
||||||
|
|
||||||
|
redis 内部使用文件事件处理器 `file event handler`,这个文件事件处理器是单线程的,所以 redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。
|
||||||
|
|
||||||
|
文件事件处理器的结构包含 4 个部分:
|
||||||
|
|
||||||
|
- 多个 socket
|
||||||
|
- IO 多路复用程序
|
||||||
|
- 文件事件分派器
|
||||||
|
- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
|
||||||
|
|
||||||
|
多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。
|
||||||
|
|
||||||
|
|
||||||
### redis 和 memcached 的区别
|
### redis 和 memcached 的区别
|
||||||
|
|
||||||
@ -72,7 +87,7 @@
|
|||||||
|
|
||||||
### redis 常见数据结构以及使用场景分析
|
### redis 常见数据结构以及使用场景分析
|
||||||
|
|
||||||
#### 1. String
|
#### 1.String
|
||||||
|
|
||||||
> **常用命令:** set,get,decr,incr,mget 等。
|
> **常用命令:** set,get,decr,incr,mget 等。
|
||||||
|
|
||||||
@ -84,7 +99,7 @@ String数据结构是简单的key-value类型,value其实不仅可以是String
|
|||||||
#### 2.Hash
|
#### 2.Hash
|
||||||
> **常用命令:** hget,hset,hgetall 等。
|
> **常用命令:** hget,hset,hgetall 等。
|
||||||
|
|
||||||
Hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以Hash数据结构来存储用户信息,商品信息等等。比如下面我就用 hash 类型存放了我本人的一些信息:
|
hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以 hash 数据结构来存储用户信息,商品信息等等。比如下面我就用 hash 类型存放了我本人的一些信息:
|
||||||
|
|
||||||
```
|
```
|
||||||
key=JavaUser293847
|
key=JavaUser293847
|
||||||
@ -128,7 +143,7 @@ sinterstore key1 key2 key3 将交集存在key1内
|
|||||||
|
|
||||||
和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。
|
和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。
|
||||||
|
|
||||||
**举例:** 在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息,适合使用 Redis 中的 SortedSet 结构进行存储。
|
**举例:** 在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息,适合使用 Redis 中的 Sorted Set 结构进行存储。
|
||||||
|
|
||||||
|
|
||||||
### redis 设置过期时间
|
### redis 设置过期时间
|
||||||
@ -147,11 +162,9 @@ Redis中有个设置时间过期的功能,即对存储在 redis 数据库中
|
|||||||
- **惰性删除** :定期删除可能会导致很多过期 key 到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被redis给删除掉。这就是所谓的惰性删除,也是够懒的哈!
|
- **惰性删除** :定期删除可能会导致很多过期 key 到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被redis给删除掉。这就是所谓的惰性删除,也是够懒的哈!
|
||||||
|
|
||||||
|
|
||||||
但是仅仅通过设置过期时间还是有问题的。我们想一下:如果定期删除漏掉了很多过期 key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期key堆积在内存里,导致redis内存块耗尽了。怎么解决这个问题呢?
|
但是仅仅通过设置过期时间还是有问题的。我们想一下:如果定期删除漏掉了很多过期 key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期key堆积在内存里,导致redis内存块耗尽了。怎么解决这个问题呢? **redis 内存淘汰机制。**
|
||||||
|
|
||||||
**redis 内存淘汰机制。**
|
### redis 内存淘汰机制(MySQL里有2000w数据,Redis中只存20w的数据,如何保证Redis中的数据都是热点数据?)
|
||||||
|
|
||||||
### redis 内存淘汰机制(MySQL里有2000w数据,Redis中只存20w的数据,如何保证Redis中的数据都是热点数据?)
|
|
||||||
|
|
||||||
redis 配置文件 redis.conf 中有相关注释,我这里就不贴了,大家可以自行查阅或者通过这个网址查看: [http://download.redis.io/redis-stable/redis.conf](http://download.redis.io/redis-stable/redis.conf)
|
redis 配置文件 redis.conf 中有相关注释,我这里就不贴了,大家可以自行查阅或者通过这个网址查看: [http://download.redis.io/redis-stable/redis.conf](http://download.redis.io/redis-stable/redis.conf)
|
||||||
|
|
||||||
@ -160,19 +173,23 @@ redis 配置文件 redis.conf 中有相关注释,我这里就不贴了,大
|
|||||||
1. **volatile-lru**:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
|
1. **volatile-lru**:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
|
||||||
2. **volatile-ttl**:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
|
2. **volatile-ttl**:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
|
||||||
3. **volatile-random**:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
|
3. **volatile-random**:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
|
||||||
4. **allkeys-lru**:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的).
|
4. **allkeys-lru**:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)
|
||||||
5. **allkeys-random**:从数据集(server.db[i].dict)中任意选择数据淘汰
|
5. **allkeys-random**:从数据集(server.db[i].dict)中任意选择数据淘汰
|
||||||
6. **no-eviction**:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!
|
6. **no-eviction**:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!
|
||||||
|
|
||||||
|
4.0版本后增加以下两种:
|
||||||
|
|
||||||
|
7. **volatile-lfu**:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
|
||||||
|
8. **allkeys-lfu**:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的key
|
||||||
|
|
||||||
**备注: 关于 redis 设置过期时间以及内存淘汰机制,我这里只是简单的总结一下,后面会专门写一篇文章来总结!**
|
**备注: 关于 redis 设置过期时间以及内存淘汰机制,我这里只是简单的总结一下,后面会专门写一篇文章来总结!**
|
||||||
|
|
||||||
|
|
||||||
### redis 持久化机制(怎么保证 redis 挂掉之后再重启数据可以进行恢复)
|
### redis 持久化机制(怎么保证 redis 挂掉之后再重启数据可以进行恢复)
|
||||||
|
|
||||||
很多时候我们需要持久化数据也就是将内存中的数据写入到硬盘里面,大部分原因是为了之后重用数据(比如重启机器、机器故障之后回复数据),或者是为了防止系统故障而将数据备份到一个远程位置。
|
很多时候我们需要持久化数据也就是将内存中的数据写入到硬盘里面,大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了防止系统故障而将数据备份到一个远程位置。
|
||||||
|
|
||||||
Redis不同于Memcached的很重一点就是,Redis支持持久化,而且支持两种不同的持久化操作。**Redis的一种持久化方式叫快照(snapshotting,RDB),另一种方式是只追加文件(append-only file,AOF)**.这两种方法各有千秋,下面我会详细这两种持久化方法是什么,怎么用,如何选择适合自己的持久化方法。
|
Redis不同于Memcached的很重一点就是,Redis支持持久化,而且支持两种不同的持久化操作。**Redis的一种持久化方式叫快照(snapshotting,RDB),另一种方式是只追加文件(append-only file,AOF)**。这两种方法各有千秋,下面我会详细这两种持久化方法是什么,怎么用,如何选择适合自己的持久化方法。
|
||||||
|
|
||||||
**快照(snapshotting)持久化(RDB)**
|
**快照(snapshotting)持久化(RDB)**
|
||||||
|
|
||||||
@ -182,14 +199,13 @@ Redis可以通过创建快照来获得存储在内存里面的数据在某个时
|
|||||||
|
|
||||||
```conf
|
```conf
|
||||||
|
|
||||||
save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
|
save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
|
||||||
|
|
||||||
save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
|
save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
|
||||||
|
|
||||||
save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
|
save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
**AOF(append-only file)持久化**
|
**AOF(append-only file)持久化**
|
||||||
|
|
||||||
与快照持久化相比,AOF持久化 的实时性更好,因此已成为主流的持久化方案。默认情况下Redis没有开启AOF(append only file)方式的持久化,可以通过appendonly参数开启:
|
与快照持久化相比,AOF持久化 的实时性更好,因此已成为主流的持久化方案。默认情况下Redis没有开启AOF(append only file)方式的持久化,可以通过appendonly参数开启:
|
||||||
@ -203,49 +219,53 @@ appendonly yes
|
|||||||
在Redis的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:
|
在Redis的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:
|
||||||
|
|
||||||
```conf
|
```conf
|
||||||
appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
|
appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
|
||||||
appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘
|
appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘
|
||||||
appendfsync no #让操作系统决定何时进行同步
|
appendfsync no #让操作系统决定何时进行同步
|
||||||
```
|
```
|
||||||
|
|
||||||
为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec选项 ,让Redis每秒同步一次AOF文件,Redis性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。
|
为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec选项 ,让Redis每秒同步一次AOF文件,Redis性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。
|
||||||
|
|
||||||
|
|
||||||
**Redis 4.0 对于持久化机制的优化**
|
**Redis 4.0 对于持久化机制的优化**
|
||||||
|
|
||||||
Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 `aof-use-rdb-preamble` 开启)。
|
Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 `aof-use-rdb-preamble` 开启)。
|
||||||
|
|
||||||
如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。
|
如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
**补充内容:AOF 重写**
|
**补充内容:AOF 重写**
|
||||||
|
|
||||||
AOF重写可以产生一个新的AOF文件,这个新的AOF文件和原有的AOF文件所保存的数据库状态一样,但体积更小。
|
AOF重写可以产生一个新的AOF文件,这个新的AOF文件和原有的AOF文件所保存的数据库状态一样,但体积更小。
|
||||||
|
|
||||||
AOF重写是一个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无须对现有AOF文件进行任伺读入、分析或者写入操作。
|
AOF重写是一个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无须对现有AOF文件进行任何读入、分析或者写入操作。
|
||||||
|
|
||||||
在执行 BGREWRITEAOF 命令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子进程创建新AOF文件期间,记录服务器执行的所有写命令。当子进程完成创建新AOF文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新AOF文件的末尾,使得新旧两个AOF文件所保存的数据库状态一致。最后,服务器用新的AOF文件替换旧的AOF文件,以此来完成AOF文件重写操作
|
在执行 BGREWRITEAOF 命令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子进程创建新AOF文件期间,记录服务器执行的所有写命令。当子进程完成创建新AOF文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新AOF文件的末尾,使得新旧两个AOF文件所保存的数据库状态一致。最后,服务器用新的AOF文件替换旧的AOF文件,以此来完成AOF文件重写操作
|
||||||
|
|
||||||
|
|
||||||
**更多内容可以查看我的这篇文章:**
|
**更多内容可以查看我的这篇文章:**
|
||||||
|
|
||||||
- [https://github.com/Snailclimb/JavaGuide/blob/master/数据存储/Redis/Redis持久化.md](https://github.com/Snailclimb/JavaGuide/blob/master/数据存储/Redis/Redis持久化.md)
|
- [Redis持久化](Redis持久化.md)
|
||||||
|
|
||||||
|
|
||||||
### redis 事务
|
### redis 事务
|
||||||
|
|
||||||
Redis 通过 MULTI、EXEC、WATCH 等命令来实现事务(transaction)功能。事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。
|
Redis 通过 MULTI、EXEC、WATCH 等命令来实现事务(transaction)功能。事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。
|
||||||
|
|
||||||
在传统的关系式数据库中,常常用 ACID 性质来检验事务功能的可靠性和安全性。在 Redis 中,事务总是具有原子性(Atomicity)、一致性(Consistency)和隔离性(Isolation),并且当 Redis 运行在某种特定的持久化模式下时,事务也具有持久性(Durability)。
|
在传统的关系式数据库中,常常用 ACID 性质来检验事务功能的可靠性和安全性。在 Redis 中,事务总是具有原子性(Atomicity)、一致性(Consistency)和隔离性(Isolation),并且当 Redis 运行在某种特定的持久化模式下时,事务也具有持久性(Durability)。
|
||||||
|
|
||||||
|
补充内容:
|
||||||
|
|
||||||
|
> 1. redis同一个事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚。(来自[issue:关于Redis事务不是原子性问题](https://github.com/Snailclimb/JavaGuide/issues/452) )
|
||||||
|
|
||||||
### 缓存雪崩和缓存穿透问题解决方案
|
### 缓存雪崩和缓存穿透问题解决方案
|
||||||
|
|
||||||
**缓存雪崩**
|
#### **缓存雪崩**
|
||||||
|
|
||||||
|
**什么是缓存雪崩?**
|
||||||
|
|
||||||
简介:缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
|
简介:缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
|
||||||
|
|
||||||
解决办法(中华石杉老师在他的视频中提到过,视频地址在最后一个问题中有提到):
|
**有哪些解决办法?**
|
||||||
|
|
||||||
|
(中华石杉老师在他的视频中提到过,视频地址在最后一个问题中有提到):
|
||||||
|
|
||||||
- 事前:尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略。
|
- 事前:尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略。
|
||||||
- 事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉
|
- 事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉
|
||||||
@ -253,16 +273,58 @@ Redis 通过 MULTI、EXEC、WATCH 等命令来实现事务(transaction)功能。
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
#### **缓存穿透**
|
||||||
|
|
||||||
**缓存穿透**
|
**什么是缓存穿透?**
|
||||||
|
|
||||||
简介:一般是黑客故意去请求缓存中不存在的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
|
缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。举个例子:某个黑客故意制造我们缓存中不存在的 key 发起大量请求,导致大量请求落到数据库。下面用图片展示一下(这两张图片不是我画的,为了省事直接在网上找的,这里说明一下):
|
||||||
|
|
||||||
解决办法: 有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
|
**正常缓存处理流程:**
|
||||||
|
|
||||||
参考:
|
<img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/正常缓存处理流程-redis.png" style="zoom:50%;" />
|
||||||
|
|
||||||
- [https://blog.csdn.net/zeb_perfect/article/details/54135506](https://blog.csdn.net/zeb_perfect/article/details/54135506)
|
**缓存穿透情况处理流程:**
|
||||||
|
|
||||||
|
<img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/缓存穿透处理流程-redis.png" style="zoom:50%;" />
|
||||||
|
|
||||||
|
一般MySQL 默认的最大连接数在 150 左右,这个可以通过 `show variables like '%max_connections%'; `命令来查看。最大连接数一个还只是一个指标,cpu,内存,磁盘,网络等无力条件都是其运行指标,这些指标都会限制其并发能力!所以,一般 3000 个并发请求就能打死大部分数据库了。
|
||||||
|
|
||||||
|
**有哪些解决办法?**
|
||||||
|
|
||||||
|
最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。
|
||||||
|
|
||||||
|
**1)缓存无效 key** : 如果缓存和数据库都查不到某个 key 的数据就写一个到 redis 中去并设置过期时间,具体命令如下:`SET key value EX 10086`。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求key,会导致 redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。
|
||||||
|
|
||||||
|
另外,这里多说一嘴,一般情况下我们是这样设计 key 的: `表名:列名:主键名:主键值`。
|
||||||
|
|
||||||
|
如果用 Java 代码展示的话,差不多是下面这样的:
|
||||||
|
|
||||||
|
```java
|
||||||
|
public Object getObjectInclNullById(Integer id) {
|
||||||
|
// 从缓存中获取数据
|
||||||
|
Object cacheValue = cache.get(id);
|
||||||
|
// 缓存为空
|
||||||
|
if (cacheValue == null) {
|
||||||
|
// 从数据库中获取
|
||||||
|
Object storageValue = storage.get(key);
|
||||||
|
// 缓存空对象
|
||||||
|
cache.set(key, storageValue);
|
||||||
|
// 如果存储数据为空,需要设置一个过期时间(300秒)
|
||||||
|
if (storageValue == null) {
|
||||||
|
// 必须设置过期时间,否则有被攻击的风险
|
||||||
|
cache.expire(key, 60 * 5);
|
||||||
|
}
|
||||||
|
return storageValue;
|
||||||
|
}
|
||||||
|
return cacheValue;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**2)布隆过滤器:**布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在与海量数据中。我们需要的就是判断 key 是否合法,有没有感觉布隆过滤器就是我们想要找的那个“人”。具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,我会先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。总结一下就是下面这张图(这张图片不是我画的,为了省事直接在网上找的):
|
||||||
|
|
||||||
|
<img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/布隆过滤器-缓存穿透-redis.png" style="zoom:50%;" />
|
||||||
|
|
||||||
|
更多关于布隆过滤器的内容可以看我的这篇原创:[《不了解布隆过滤器?一文给你整的明明白白!》](https://github.com/Snailclimb/JavaGuide/blob/master/docs/dataStructures-algorithms/data-structure/bloom-filter.md) ,强烈推荐,个人感觉网上应该找不到总结的这么明明白白的文章了。
|
||||||
|
|
||||||
### 如何解决 Redis 的并发竞争 Key 问题
|
### 如何解决 Redis 的并发竞争 Key 问题
|
||||||
|
|
||||||
@ -278,9 +340,9 @@ Redis 通过 MULTI、EXEC、WATCH 等命令来实现事务(transaction)功能。
|
|||||||
|
|
||||||
- https://www.jianshu.com/p/8bddd381de06
|
- https://www.jianshu.com/p/8bddd381de06
|
||||||
|
|
||||||
|
### 如何保证缓存与数据库双写时的数据一致性?
|
||||||
|
|
||||||
### 如何保证缓存与数据库双写时的数据一致性?
|
> 一般情况下我们都是这样使用缓存的:先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。这种方式很明显会存在缓存和数据库的数据不一致的情况。
|
||||||
|
|
||||||
|
|
||||||
你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?
|
你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?
|
||||||
|
|
||||||
@ -288,13 +350,21 @@ Redis 通过 MULTI、EXEC、WATCH 等命令来实现事务(transaction)功能。
|
|||||||
|
|
||||||
串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。
|
串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。
|
||||||
|
|
||||||
**参考:**
|
更多内容可以查看:https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/redis-consistence.md
|
||||||
|
|
||||||
- Java工程师面试突击第1季(可能是史上最好的Java面试突击课程)-中华石杉老师。视频地址见下面!
|
**参考:** Java工程师面试突击第1季(可能是史上最好的Java面试突击课程)-中华石杉老师!公众号后台回复关键字“1”即可获取该视频内容。
|
||||||
- 链接: https://pan.baidu.com/s/18pp6g1xKVGCfUATf_nMrOA
|
|
||||||
- 密码:5i58
|
|
||||||
|
|
||||||
### 参考:
|
### 参考
|
||||||
|
|
||||||
- redis设计与实现(第二版)
|
- 《Redis开发与运维》
|
||||||
|
- Redis 命令总结:http://redisdoc.com/string/set.html
|
||||||
|
|
||||||
|
## 公众号
|
||||||
|
|
||||||
|
如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。
|
||||||
|
|
||||||
|
**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取!
|
||||||
|
|
||||||
|
**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。
|
||||||
|
|
||||||
|

|
||||||
|
@ -28,7 +28,7 @@ end
|
|||||||
|
|
||||||
算法很易懂,起 5 个 master 节点,分布在不同的机房尽量保证可用性。为了获得锁,client 会进行如下操作:
|
算法很易懂,起 5 个 master 节点,分布在不同的机房尽量保证可用性。为了获得锁,client 会进行如下操作:
|
||||||
|
|
||||||
1. 得到当前的时间,微妙单位
|
1. 得到当前的时间,微秒单位
|
||||||
2. 尝试顺序地在 5 个实例上申请锁,当然需要使用相同的 key 和 random value,这里一个 client 需要合理设置与 master 节点沟通的 timeout 大小,避免长时间和一个 fail 了的节点浪费时间
|
2. 尝试顺序地在 5 个实例上申请锁,当然需要使用相同的 key 和 random value,这里一个 client 需要合理设置与 master 节点沟通的 timeout 大小,避免长时间和一个 fail 了的节点浪费时间
|
||||||
3. 当 client 在大于等于 3 个 master 上成功申请到锁的时候,且它会计算申请锁消耗了多少时间,这部分消耗的时间采用获得锁的当下时间减去第一步获得的时间戳得到,如果锁的持续时长(lock validity time)比流逝的时间多的话,那么锁就真正获取到了。
|
3. 当 client 在大于等于 3 个 master 上成功申请到锁的时候,且它会计算申请锁消耗了多少时间,这部分消耗的时间采用获得锁的当下时间减去第一步获得的时间戳得到,如果锁的持续时长(lock validity time)比流逝的时间多的话,那么锁就真正获取到了。
|
||||||
4. 如果锁申请到了,那么锁真正的 lock validity time 应该是 origin(lock validity time) - 申请锁期间流逝的时间
|
4. 如果锁申请到了,那么锁真正的 lock validity time 应该是 origin(lock validity time) - 申请锁期间流逝的时间
|
||||||
|
515
docs/database/Redis/redis-collection/Redis(1)——5种基本数据结构.md
Normal file
515
docs/database/Redis/redis-collection/Redis(1)——5种基本数据结构.md
Normal file
@ -0,0 +1,515 @@
|
|||||||
|
> 授权转载自: https://github.com/wmyskxz/MoreThanJava#part3-redis
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# 一、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 万个请求时的性能:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
当然不同电脑之间由于各方面的原因会存在性能差距,这个测试您可以权当是一种 **「乐趣」** 就好。
|
||||||
|
|
||||||
|
# 二、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" 的一个字符数组)*
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
这样简单的数据结构可能会造成以下一些问题:
|
||||||
|
|
||||||
|
- **获取字符串长度为 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` 指针组成双向链表:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
虽然仅仅使用多个 listNode 结构就可以组成链表,但是使用 `adlist.h/list` 结构来持有链表的话,操作起来会更加方便:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 链表的基本操作
|
||||||
|
|
||||||
|
- `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** 小步搬迁:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
渐进式 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 值,用来代表排序的权重。
|
||||||
|
|
||||||
|
它的内部实现用的是一种叫做 **「跳跃表」** 的数据结构,由于比较复杂,所以在这里简单提一下原理就好了:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
想象你是一家创业公司的老板,刚开始只有几个人,大家都平起平坐。后来随着公司的发展,人数越来越多,团队沟通成本逐渐增加,渐渐地引入了组长制,对团队进行划分,于是有一些人**又是员工又有组长的身份**。
|
||||||
|
|
||||||
|
再后来,公司规模进一步扩大,公司需要再进入一个层级:部门。于是每个部门又会从组长中推举一位选出部长。
|
||||||
|
|
||||||
|
跳跃表就类似于这样的机制,最下面一层所有的元素都会串起来,都是员工,然后每隔几个元素就会挑选出一个代表,再把这几个代表使用另外一级指针串起来。然后再在这些代表里面挑出二级代表,再串起来。**最终形成了一个金字塔的结构。**
|
||||||
|
|
||||||
|
想一下你目前所在的地理位置:亚洲 > 中国 > 某省 > 某市 > ....,**就是这样一个结构!**
|
||||||
|
|
||||||
|
### 有序列表 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/)
|
||||||
|
|
@ -0,0 +1,599 @@
|
|||||||
|
## 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属性来决定
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
+ 每种类型的对象至少使用了两种不同的编码,而这些编码对用户是完全透明的。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
+ 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字节的字符串
|
||||||
|
|
||||||
|
<font color="red">int用来保存整数值,raw用来保存长字符串,embstr用来保存短字符串。embstr编码是用来专门保存短字符串的一种优化编码。</font>
|
||||||
|
|
||||||
|
<font color="red">Redis中对于浮点型也是作为字符串保存的,在需要时再将其转换成浮点数类型</font>
|
||||||
|
|
||||||
|
+ 编码的转换
|
||||||
|
+ 当 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
|
||||||
|
<font color ="red">Redis的共享对象目前只支持整数值的字符串对象。之所以如此,实际上是对内存和CPU(时间)的平衡:共享对象虽然会降低内存消耗,但是判断两个对象是否相等却需要消耗额外的时间。对于整数值,判断操作复杂度为o(1),对于普通字符串,判断复杂度为o(n);而对于哈希,列表,集合和有序集合,判断的复杂度为o(n^2).虽然共享的对象只能是整数值的字符串对象,但是5种类型都可能使用共享对象。</font>
|
||||||
|
|
||||||
|
|
||||||
|
|
392
docs/database/Redis/redis-collection/Redis(2)——跳跃表.md
Normal file
392
docs/database/Redis/redis-collection/Redis(2)——跳跃表.md
Normal file
@ -0,0 +1,392 @@
|
|||||||
|
> 授权转载自: https://github.com/wmyskxz/MoreThanJava#part3-redis
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# 一、跳跃表简介
|
||||||
|
|
||||||
|
跳跃表(skiplist)是一种随机化的数据结构,由 **William Pugh** 在论文[《Skip lists: a probabilistic alternative to balanced trees》](https://www.cl.cam.ac.uk/teaching/0506/Algorithms/skiplists.pdf)中提出,是一种可以于平衡树媲美的层次化链表结构——查找、删除、添加等操作都可以在对数期望时间下完成,以下是一个典型的跳跃表例子:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
我们在上一篇中提到了 Redis 的五种基本结构中,有一个叫做 **有序列表 zset** 的数据结构,它类似于 Java 中的 **SortedSet** 和 **HashMap** 的结合体,一方面它是一个 set 保证了内部 value 的唯一性,另一方面又可以给每个 value 赋予一个排序的权重值 score,来达到 **排序** 的目的。
|
||||||
|
|
||||||
|
它的内部实现就依赖了一种叫做 **「跳跃列表」** 的数据结构。
|
||||||
|
|
||||||
|
## 为什么使用跳跃表
|
||||||
|
|
||||||
|
首先,因为 zset 要支持随机的插入和删除,所以它 **不宜使用数组来实现**,关于排序问题,我们也很容易就想到 **红黑树/ 平衡树** 这样的树形结构,为什么 Redis 不使用这样一些结构呢?
|
||||||
|
|
||||||
|
1. **性能考虑:** 在高并发的情况下,树形结构需要执行一些类似于 rebalance 这样的可能涉及整棵树的操作,相对来说跳跃表的变化只涉及局部 _(下面详细说)_;
|
||||||
|
2. **实现考虑:** 在复杂度与红黑树相同的情况下,跳跃表实现起来更简单,看起来也更加直观;
|
||||||
|
|
||||||
|
基于以上的一些考虑,Redis 基于 **William Pugh** 的论文做出一些改进后采用了 **跳跃表** 这样的结构。
|
||||||
|
|
||||||
|
## 本质是解决查找问题
|
||||||
|
|
||||||
|
我们先来看一个普通的链表结构:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
我们需要这个链表按照 score 值进行排序,这也就意味着,当我们需要添加新的元素时,我们需要定位到插入点,这样才可以继续保证链表是有序的,通常我们会使用 **二分查找法**,但二分查找是有序数组的,链表没办法进行位置定位,我们除了遍历整个找到第一个比给定数据大的节点为止 _(时间复杂度 O(n))_ 似乎没有更好的办法。
|
||||||
|
|
||||||
|
但假如我们每相邻两个节点之间就增加一个指针,让指针指向下一个节点,如下图:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
这样所有新增的指针连成了一个新的链表,但它包含的数据却只有原来的一半 _(图中的为 3,11)_。
|
||||||
|
|
||||||
|
现在假设我们想要查找数据时,可以根据这条新的链表查找,如果碰到比待查找数据大的节点时,再回到原来的链表中进行查找,比如,我们想要查找 7,查找的路径则是沿着下图中标注出的红色指针所指向的方向进行的:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
这是一个略微极端的例子,但我们仍然可以看到,通过新增加的指针查找,我们不再需要与链表上的每一个节点逐一进行比较,这样改进之后需要比较的节点数大概只有原来的一半。
|
||||||
|
|
||||||
|
利用同样的方式,我们可以在新产生的链表上,继续为每两个相邻的节点增加一个指针,从而产生第三层链表:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
在这个新的三层链表结构中,我们试着 **查找 13**,那么沿着最上层链表首先比较的是 11,发现 11 比 13 小,于是我们就知道只需要到 11 后面继续查找,**从而一下子跳过了 11 前面的所有节点。**
|
||||||
|
|
||||||
|
可以想象,当链表足够长,这样的多层链表结构可以帮助我们跳过很多下层节点,从而加快查找的效率。
|
||||||
|
|
||||||
|
## 更进一步的跳跃表
|
||||||
|
|
||||||
|
**跳跃表 skiplist** 就是受到这种多层链表结构的启发而设计出来的。按照上面生成链表的方式,上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就非常类似于一个二分查找,使得查找的时间复杂度可以降低到 _O(logn)_。
|
||||||
|
|
||||||
|
但是,这种方法在插入数据的时候有很大的问题。新插入一个节点之后,就会打乱上下相邻两层链表上节点个数严格的 2:1 的对应关系。如果要维持这种对应关系,就必须把新插入的节点后面的所有节点 _(也包括新插入的节点)_ 重新进行调整,这会让时间复杂度重新蜕化成 _O(n)_。删除数据也有同样的问题。
|
||||||
|
|
||||||
|
**skiplist** 为了避免这一问题,它不要求上下相邻两层链表之间的节点个数有严格的对应关系,而是 **为每个节点随机出一个层数(level)**。比如,一个节点随机出的层数是 3,那么就把它链入到第 1 层到第 3 层这三层链表中。为了表达清楚,下图展示了如何通过一步步的插入操作从而形成一个 skiplist 的过程:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
从上面的创建和插入的过程中可以看出,每一个节点的层数(level)是随机出来的,而且新插入一个节点并不会影响到其他节点的层数,因此,**插入操作只需要修改节点前后的指针,而不需要对多个节点都进行调整**,这就降低了插入操作的复杂度。
|
||||||
|
|
||||||
|
现在我们假设从我们刚才创建的这个结构中查找 23 这个不存在的数,那么查找路径会如下图:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# 二、跳跃表的实现
|
||||||
|
|
||||||
|
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<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
直观上期望的目标是 50% 的概率被分配到 `Level 1`,25% 的概率被分配到 `Level 2`,12.5% 的概率被分配到 `Level 3`,以此类推...有 2<sup>-63</sup> 的概率被分配到最顶层,因为这里每一层的晋升率都是 50%。
|
||||||
|
|
||||||
|
**Redis 跳跃表默认允许最大的层数是 32**,被源码中 `ZSKIPLIST_MAXLEVEL` 定义,当 `Level[0]` 有 2<sup>64</sup> 个元素时,才能达到 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
即执行完之后创建了如下结构的初始化跳跃表:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
## 插入节点实现
|
||||||
|
|
||||||
|
这几乎是最重要的一段代码了,但总体思路也比较清晰简单,如果理解了上面所说的跳跃表的原理,那么很容易理清楚插入节点时发生的几个动作 *(几乎跟链表类似)*:
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
228
docs/database/Redis/redis-collection/Redis(3)——分布式锁深入探究.md
Normal file
228
docs/database/Redis/redis-collection/Redis(3)——分布式锁深入探究.md
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
> 授权转载自: https://github.com/wmyskxz/MoreThanJava#part3-redis
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# 一、分布式锁简介
|
||||||
|
|
||||||
|
**锁** 是一种用来解决多个执行线程 **访问共享资源** 错误或数据不一致问题的工具。
|
||||||
|
|
||||||
|
如果 *把一台服务器比作一个房子*,那么 *线程就好比里面的住户*,当他们想要共同访问一个共享资源,例如厕所的时候,如果厕所门上没有锁...更甚者厕所没装门...这是会出原则性的问题的..
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
装上了锁,大家用起来就安心多了,本质也就是 **同一时间只允许一个住户使用**。
|
||||||
|
|
||||||
|
而随着互联网世界的发展,单体应用已经越来越无法满足复杂互联网的高并发需求,转而慢慢朝着分布式方向发展,慢慢进化成了 **更大一些的住户**。所以同样,我们需要引入分布式锁来解决分布式应用之间访问共享资源的并发问题。
|
||||||
|
|
||||||
|
## 为何需要分布式锁
|
||||||
|
|
||||||
|
一般情况下,我们使用分布式锁主要有两个场景:
|
||||||
|
|
||||||
|
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 服务就永远无法获取到锁了:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
所以我们需要额外设置一个超时时间,来保证服务的可用性。
|
||||||
|
|
||||||
|
但是另一个问题随即而来:**如果在加锁和释放锁之间的逻辑执行得太长,以至于超出了锁的超时限制**,也会出现问题。因为这时候第一个线程持有锁过期了,而临界区的逻辑还没有执行完,与此同时第二个线程就提前拥有了这把锁,导致临界区的代码不能得到严格的串行执行。
|
||||||
|
|
||||||
|
为了避免这个问题,**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)**,这本身是为了保障垃圾回收器的正常执行,但可能会引发如下的问题:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
服务 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 设置为 <current Unix time + lock timeout + 1>**,这样在其他客户端访问时就能够自己判断是否能够获取下一个 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 深度历险》 - 钱文品/ 著
|
||||||
|
|
361
docs/database/Redis/redis-collection/Redis(5)——亿级数据过滤和布隆过滤器.md
Normal file
361
docs/database/Redis/redis-collection/Redis(5)——亿级数据过滤和布隆过滤器.md
Normal file
@ -0,0 +1,361 @@
|
|||||||
|
> 授权转载自: https://github.com/wmyskxz/MoreThanJava#part3-redis
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# 一、布隆过滤器简介
|
||||||
|
|
||||||
|
[上一次](https://www.wmyskxz.com/2020/03/02/reids-4-shen-qi-de-hyperloglog-jie-jue-tong-ji-wen-ti/) 我们学会了使用 **HyperLogLog** 来对大数据进行一个估算,它非常有价值,可以解决很多精确度不高的统计需求。但是如果我们想知道某一个值是不是已经在 **HyperLogLog** 结构里面了,它就无能为力了,它只提供了 `pfadd` 和 `pfcount` 方法,没有提供类似于 `contains` 的这种方法。
|
||||||
|
|
||||||
|
就举一个场景吧,比如你 **刷抖音**:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
你有 **刷到过重复的推荐内容** 吗?这么多的推荐内容要推荐给这么多的用户,它是怎么保证每个用户在看推荐内容时,保证不会出现之前已经看过的推荐视频呢?也就是说,抖音是如何实现 **推送去重** 的呢?
|
||||||
|
|
||||||
|
你会想到服务器 **记录** 了用户看过的 **所有历史记录**,当推荐系统推荐短视频时会从每个用户的历史记录里进行 **筛选**,过滤掉那些已经存在的记录。问题是当 **用户量很大**,每个用户看过的短视频又很多的情况下,这种方式,推荐系统的去重工作 **在性能上跟的上么?**
|
||||||
|
|
||||||
|
实际上,如果历史记录存储在关系数据库里,去重就需要频繁地对数据库进行 `exists` 查询,当系统并发量很高时,数据库是很难抗住压力的。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
你可能又想到了 **缓存**,但是这么多用户这么多的历史记录,如果全部缓存起来,那得需要 **浪费多大的空间** 啊.. *(可能老板看一眼账单,看一眼你..)* 并且这个存储空间会随着时间呈线性增长,就算你用缓存撑得住一个月,但是又能继续撑多久呢?不缓存性能又跟不上,咋办呢?
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
如上图所示,**布隆过滤器(Bloom Filter)** 就是这样一种专门用来解决去重问题的高级数据结构。但是跟 **HyperLogLog** 一样,它也一样有那么一点点不精确,也存在一定的误判概率,但它能在解决去重的同时,在 **空间上能节省 90%** 以上,也是非常值得的。
|
||||||
|
|
||||||
|
## 布隆过滤器是什么
|
||||||
|
|
||||||
|
**布隆过滤器(Bloom Filter)** 是 1970 年由布隆提出的。它 **实际上** 是一个很长的二进制向量和一系列随机映射函数 *(下面详细说)*,实际上你也可以把它 **简单理解** 为一个不怎么精确的 **set** 结构,当你使用它的 `contains` 方法判断某个对象是否存在时,它可能会误判。但是布隆过滤器也不是特别不精确,只要参数设置的合理,它的精确度可以控制的相对足够精确,只会有小小的误判概率。
|
||||||
|
|
||||||
|
当布隆过滤器说某个值存在时,这个值 **可能不存在**;当它说不存在时,那么 **一定不存在**。打个比方,当它说不认识你时,那就是真的不认识,但是当它说认识你的时候,可能是因为你长得像它认识的另外一个朋友 *(脸长得有些相似)*,所以误判认识你。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 布隆过滤器的使用场景
|
||||||
|
|
||||||
|
基于上述的功能,我们大致可以把布隆过滤器用于以下的场景之中:
|
||||||
|
|
||||||
|
- **大数据判断是否存在**:这就可以实现出上述的去重功能,如果你的服务器内存足够大的话,那么使用 HashMap 可能是一个不错的解决方案,理论上时间复杂度可以达到 O(1 的级别,但是当数据量起来之后,还是只能考虑布隆过滤器。
|
||||||
|
- **解决缓存穿透**:我们经常会把一些热点数据放在 Redis 中当作缓存,例如产品详情。 通常一个请求过来之后我们会先查询缓存,而不用直接读取数据库,这是提升性能最简单也是最普遍的做法,但是 **如果一直请求一个不存在的缓存**,那么此时一定不存在缓存,那就会有 **大量请求直接打到数据库** 上,造成 **缓存穿透**,布隆过滤器也可以用来解决此类问题。
|
||||||
|
- **爬虫/ 邮箱等系统的过滤**:平时不知道你有没有注意到有一些正常的邮件也会被放进垃圾邮件目录中,这就是使用布隆过滤器 **误判** 导致的。
|
||||||
|
|
||||||
|
# 二、布隆过滤器原理解析
|
||||||
|
|
||||||
|
布隆过滤器 **本质上** 是由长度为 `m` 的位向量或位列表(仅包含 `0` 或 `1` 位值的列表)组成,最初所有的值均设置为 `0`,所以我们先来创建一个稍微长一些的位向量用作展示:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
当我们向布隆过滤器中添加数据时,会使用 **多个** `hash` 函数对 `key` 进行运算,算得一个证书索引值,然后对位数组长度进行取模运算得到一个位置,每个 `hash` 函数都会算得一个不同的位置。再把位数组的这几个位置都置为 `1` 就完成了 `add` 操作,例如,我们添加一个 `wmyskxz`:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
向布隆过滤器查查询 `key` 是否存在时,跟 `add` 操作一样,会把这个 `key` 通过相同的多个 `hash` 函数进行运算,查看 **对应的位置** 是否 **都** 为 `1`,**只要有一个位为 `0`**,那么说明布隆过滤器中这个 `key` 不存在。如果这几个位置都是 `1`,并不能说明这个 `key` 一定存在,只能说极有可能存在,因为这些位置的 `1` 可能是因为其他的 `key` 存在导致的。
|
||||||
|
|
||||||
|
就比如我们在 `add` 了一定的数据之后,查询一个 **不存在** 的 `key`:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
很明显,`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<Integer> 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
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.guava</groupId>
|
||||||
|
<artifactId>guava</artifactId>
|
||||||
|
<version>28.0-jre</version>
|
||||||
|
</dependency>
|
||||||
|
```
|
||||||
|
|
||||||
|
实际使用如下:
|
||||||
|
|
||||||
|
我们创建了一个最多存放 最多 1500 个整数的布隆过滤器,并且我们可以容忍误判的概率为百分之(0.01)
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 创建布隆过滤器对象
|
||||||
|
BloomFilter<Integer> 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)
|
||||||
|
|
227
docs/database/Redis/redis-collection/Redis(6)——GeoHash查找附近的人.md
Normal file
227
docs/database/Redis/redis-collection/Redis(6)——GeoHash查找附近的人.md
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
> 授权转载自: https://github.com/wmyskxz/MoreThanJava#part3-redis
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
像微信 **"附近的人"**,美团 **"附近的餐厅"**,支付宝共享单车 **"附近的车"** 是怎么设计实现的呢?
|
||||||
|
|
||||||
|
# 一、使用数据库实现查找附近的人
|
||||||
|
|
||||||
|
我们都知道,地球上的任何一个位置都可以使用二维的 **经纬度** 来表示,经度范围 *[-180, 180]*,纬度范围 *[-90, 90]*,纬度正负以赤道为界,北正南负,经度正负以本初子午线 *(英国格林尼治天文台)* 为界,东正西负。比如说,北京人民英雄纪念碑的经纬度坐标就是 *(39.904610, 116.397724)*,都是正数,因为中国位于东北半球。
|
||||||
|
|
||||||
|
所以,当我们使用数据库存储了所有人的 **经纬度** 信息之后,我们就可以基于当前的坐标节点,来划分出一个矩形的范围,来得知附近的人,如下图:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
所以,我们很容易写出下列的伪 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 算法将 **二维的经纬度** 数据映射到 **一维** 的整数,这样所有的元素都将在挂载到一条线上,距离靠近的二维坐标映射到一维后的点之间距离也会很接近。当我们想要计算 **「附近的人时」**,首先将目标位置映射到这条线上,然后在这个一维的线上获取附近的点就行了。
|
||||||
|
|
||||||
|
它的核心思想就是把整个地球看成是一个 **二维的平面**,然后把这个平面不断地等分成一个一个小的方格,**每一个** 坐标元素都位于其中的 **唯一一个方格** 中,等分之后的 **方格越小**,那么坐标也就 **越精确**,类似下图:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
经过划分的地球,我们需要对其进行编码:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
经过这样顺序的编码之后,如果你仔细观察一会儿,你就会发现一些规律:
|
||||||
|
|
||||||
|
- 横着的所有编码中,**第 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`,观察地图指向的位置是否正确:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
很好,就是这个位置,非常准确。
|
||||||
|
|
||||||
|
## 附近的公司
|
||||||
|
`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,与您共同成长!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
非常感谢各位人才能 **看到这里**,如果觉得本篇文章写得不错,觉得 **「我没有三颗心脏」有点东西** 的话,**求点赞,求关注,求分享,求留言!**
|
||||||
|
|
||||||
|
创作不易,各位的支持和认可,就是我创作的最大动力,我们下篇文章见!
|
215
docs/database/Redis/redis-collection/Redis(7)——持久化.md
Normal file
215
docs/database/Redis/redis-collection/Redis(7)——持久化.md
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
> 授权转载自: https://github.com/wmyskxz/MoreThanJava#part3-redis
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# 一、持久化简介
|
||||||
|
|
||||||
|
**Redis** 的数据 **全部存储** 在 **内存** 中,如果 **突然宕机**,数据就会全部丢失,因此必须有一套机制来保证 Redis 的数据不会因为故障而丢失,这种机制就是 Redis 的 **持久化机制**,它会将内存中的数据库状态 **保存到磁盘** 中。
|
||||||
|
|
||||||
|
## 持久化发生了什么 | 从内存到磁盘
|
||||||
|
|
||||||
|
我们来稍微考虑一下 **Redis** 作为一个 **"内存数据库"** 要做的关于持久化的事情。通常来说,从客户端发起请求开始,到服务器真实地写入磁盘,需要发生如下几件事情:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**详细版** 的文字描述大概就是下面这样:
|
||||||
|
|
||||||
|
1. 客户端向数据库 **发送写命令** *(数据在客户端的内存中)*
|
||||||
|
2. 数据库 **接收** 到客户端的 **写请求** *(数据在服务器的内存中)*
|
||||||
|
3. 数据库 **调用系统 API** 将数据写入磁盘 *(数据在内核缓冲区中)*
|
||||||
|
4. 操作系统将 **写缓冲区** 传输到 **磁盘控控制器** *(数据在磁盘缓存中)*
|
||||||
|
5. 操作系统的磁盘控制器将数据 **写入实际的物理媒介** 中 *(数据在磁盘中)*
|
||||||
|
|
||||||
|
**注意:** 上面的过程其实是 **极度精简** 的,在实际的操作系统中,**缓存** 和 **缓冲区** 会比这 **多得多**...
|
||||||
|
|
||||||
|
## 如何尽可能保证持久化的安全
|
||||||
|
|
||||||
|
如果我们故障仅仅涉及到 **软件层面** *(该进程被管理员终止或程序崩溃)* 并且没有接触到内核,那么在 *上述步骤 3* 成功返回之后,我们就认为成功了。即使进程崩溃,操作系统仍然会帮助我们把数据正确地写入磁盘。
|
||||||
|
|
||||||
|
如果我们考虑 **停电/ 火灾** 等 **更具灾难性** 的事情,那么只有在完成了第 **5** 步之后,才是安全的。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
所以我们可以总结得出数据安全最重要的阶段是:**步骤三、四、五**,即:
|
||||||
|
|
||||||
|
- 数据库软件调用写操作将用户空间的缓冲区转移到内核缓冲区的频率是多少?
|
||||||
|
- 内核多久从缓冲区取数据刷新到磁盘控制器?
|
||||||
|
- 磁盘控制器多久把数据写入物理媒介一次?
|
||||||
|
- **注意:** 如果真的发生灾难性的事件,我们可以从上图的过程中看到,任何一步都可能被意外打断丢失,所以只能 **尽可能地保证** 数据的安全,这对于所有数据库来说都是一样的。
|
||||||
|
|
||||||
|
我们从 **第三步** 开始。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 秒** 后实际提交写入。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
但是很明显,**30 秒** 并不是 Redis 能够承受的,这意味着,如果发生故障,那么最近 30 秒内写入的所有数据都可能会丢失。幸好 `PROSIX API` 提供了另一个解决方案:`fsync`,该命令会 **强制** 内核将 **缓冲区** 写入 **磁盘**,但这是一个非常消耗性能的操作,每次调用都会 **阻塞等待** 直到设备报告 IO 完成,所以一般在生产环境的服务器中,**Redis** 通常是每隔 1s 左右执行一次 `fsync` 操作。
|
||||||
|
|
||||||
|
到目前为止,我们了解到了如何控制 `第三步` 和 `第四步`,但是对于 **第五步**,我们 **完全无法控制**。也许一些内核实现将试图告诉驱动实际提交物理介质上的数据,或者控制器可能会为了提高速度而重新排序写操作,不会尽快将数据真正写到磁盘上,而是会等待几个多毫秒。这完全是我们无法控制的。
|
||||||
|
|
||||||
|
|
||||||
|
# 二、Redis 中的两种持久化方式
|
||||||
|
|
||||||
|
## 方式一:快照
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Redis 快照** 是最简单的 Redis 持久性模式。当满足特定条件时,它将生成数据集的时间点快照,例如,如果先前的快照是在2分钟前创建的,并且现在已经至少有 *100* 次新写入,则将创建一个新的快照。此条件可以由用户配置 Redis 实例来控制,也可以在运行时修改而无需重新启动服务器。快照作为包含整个数据集的单个 `.rdb` 文件生成。
|
||||||
|
|
||||||
|
但我们知道,Redis 是一个 **单线程** 的程序,这意味着,我们不仅仅要响应用户的请求,还需要进行内存快照。而后者要求 Redis 必须进行 IO 操作,这会严重拖累服务器的性能。
|
||||||
|
|
||||||
|
还有一个重要的问题是,我们在 **持久化的同时**,**内存数据结构** 还可能在 **变化**,比如一个大型的 hash 字典正在持久化,结果一个请求过来把它删除了,可是这才刚持久化结束,咋办?
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 使用系统多进程 COW(Copy On Write) 机制 | fork 函数
|
||||||
|
|
||||||
|
操作系统多进程 **COW(Copy On Write) 机制** 拯救了我们。**Redis** 在持久化时会调用 `glibc` 的函数 `fork` 产生一个子进程,简单理解也就是基于当前进程 **复制** 了一个进程,主进程和子进程会共享内存里面的代码块和数据段:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
这里多说一点,**为什么 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
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**快照不是很持久**。如果运行 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 重写
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Redis** 在长期运行的过程中,AOF 的日志会越变越长。如果实例宕机重启,重放整个 AOF 日志会非常耗时,导致长时间 Redis 无法对外提供服务。所以需要对 **AOF 日志 "瘦身"**。
|
||||||
|
|
||||||
|
**Redis** 提供了 `bgrewriteaof` 指令用于对 AOF 日志进行瘦身。其 **原理** 就是 **开辟一个子进程** 对内存进行 **遍历** 转换成一系列 Redis 的操作指令,**序列化到一个新的 AOF 日志文件** 中。序列化完毕后再将操作期间发生的 **增量 AOF 日志** 追加到这个新的 AOF 日志文件中,追加完毕后就立即替代旧的 AOF 日志文件了,瘦身工作就完成了。
|
||||||
|
|
||||||
|
### fsync
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
AOF 日志是以文件的形式存在的,当程序对 AOF 日志文件进行写操作时,实际上是将内容写到了内核为文件描述符分配的一个内存缓存中,然后内核会异步将脏数据刷回到磁盘的。
|
||||||
|
|
||||||
|
就像我们 *上方第四步* 描述的那样,我们需要借助 `glibc` 提供的 `fsync(int fd)` 函数来讲指定的文件内容 **强制从内核缓存刷到磁盘**。但 **"强制开车"** 仍然是一个很消耗资源的一个过程,需要 **"节制"**!通常来说,生产环境的服务器,Redis 每隔 1s 左右执行一次 `fsync` 操作就可以了。
|
||||||
|
|
||||||
|
Redis 同样也提供了另外两种策略,一个是 **永不 `fsync`**,来让操作系统来决定合适同步磁盘,很不安全,另一个是 **来一个指令就 `fsync` 一次**,非常慢。但是在生产环境基本不会使用,了解一下即可。
|
||||||
|
|
||||||
|
## Redis 4.0 混合持久化
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
重启 Redis 时,我们很少使用 `rdb` 来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 `rdb` 来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。
|
||||||
|
|
||||||
|
**Redis 4.0** 为了解决这个问题,带来了一个新的持久化选项——**混合持久化**。将 `rdb` 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是 **自持久化开始到持久化结束** 的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
于是在 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)
|
||||||
|
|
531
docs/database/Redis/redis-collection/Redis(8)——发布订阅与Stream.md
Normal file
531
docs/database/Redis/redis-collection/Redis(8)——发布订阅与Stream.md
Normal file
@ -0,0 +1,531 @@
|
|||||||
|
> 授权转载自: https://github.com/wmyskxz/MoreThanJava#part3-redis
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# 一、Redis 中的发布/订阅功能
|
||||||
|
|
||||||
|
**发布/ 订阅系统** 是 Web 系统中比较常用的一个功能。简单点说就是 **发布者发布消息,订阅者接受消息**,这有点类似于我们的报纸/ 杂志社之类的: *(借用前边的一张图)*
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- 图片引用自:「消息队列」看过来! - [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` 来实现消息队列的功能,但是似乎很难实现实现 **消息多播** 的功能:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
为了支持消息多播,**Redis** 不能再依赖于那 5 种基础的数据结构了,它单独使用了一个模块来支持消息多播,这个模块就是 **PubSub**,也就是 **PublisherSubscriber** *(发布者/ 订阅者模式)*。
|
||||||
|
|
||||||
|
## PubSub 简介
|
||||||
|
|
||||||
|
我们从 *上面的图* 中可以看到,基于 `list` 结构的消息队列,是一种 `Publisher` 与 `Consumer` 点对点的强关联关系,**Redis** 为了消除这样的强关联,引入了另一种概念:**频道** *(channel)*:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
当 `Publisher` 往 `channel` 中发布消息时,关注了指定 `channel` 的 `Consumer` 就能够同时受到消息。但这里的 **问题** 是,消费者订阅一个频道是必须 **明确指定频道名称** 的,这意味着,如果我们想要 **订阅多个** 频道,那么就必须 **显式地关注多个** 名称。
|
||||||
|
|
||||||
|
为了简化订阅的繁琐操作,**Redis** 提供了 **模式订阅** 的功能 **Pattern Subscribe**,这样就可以 **一次性关注多个频道** 了,即使生产者新增了同模式的频道,消费者也可以立即受到消息:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
例如上图中,**所有** 位于图片下方的 **`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**:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
具体步骤如下:
|
||||||
|
|
||||||
|
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`,而其他频道也分别被其他客户端订阅:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### 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` 字典的给定频道(键)中,删除关于当前客户端的信息,这样被退订频道的信息就不会再发送给这个客户端。
|
||||||
|
|
||||||
|
### 订阅模式原理
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
正如我们上面说到了,当发送一条消息到 `wmyskxz.chat` 这个频道时,Redis 不仅仅会发送到当前的频道,还会发送到匹配于当前模式的所有频道,实际上,`pubsub_patterns` 背后还维护了一个 `redis.h/pubsubPattern` 结构:
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct pubsubPattern {
|
||||||
|
redisClient *client; // 订阅模式的客户端
|
||||||
|
robj *pattern; // 订阅的模式
|
||||||
|
} pubsubPattern;
|
||||||
|
```
|
||||||
|
|
||||||
|
每当调用 `PSUBSCRIBE` 命令订阅一个模式时,程序就创建一个包含客户端信息和被订阅模式的 `pubsubPattern` 结构,并将该结构添加到 `redisServer.pubsub_patterns` 链表中。
|
||||||
|
|
||||||
|
我们来看一个 `pusub_patterns` 链表的示例:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
这个时候客户端 `client 3` 执行 `PSUBSCRIBE wmyskxz.java.*`,那么 `pubsub_patterns` 链表就会被更新成这样:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
通过遍历整个 `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 作为消息队列的功能可以说是就消失了..
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# 二、更为强大的 Stream | 持久化的发布/订阅系统
|
||||||
|
|
||||||
|
**Redis Stream** 从概念上来说,就像是一个 **仅追加内容** 的 **消息链表**,把所有加入的消息都一个一个串起来,每个消息都有一个唯一的 ID 和内容,这很简单,让它复杂的是从 Kafka 借鉴的另一种概念:**消费者组(Consumer Group)** *(思路一致,实现不同)*:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
上图就展示了一个典型的 **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/)
|
648
docs/database/Redis/redis-collection/Redis(9)——集群入门实践教程.md
Normal file
648
docs/database/Redis/redis-collection/Redis(9)——集群入门实践教程.md
Normal file
@ -0,0 +1,648 @@
|
|||||||
|
> 授权转载自: https://github.com/wmyskxz/MoreThanJava#part3-redis
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# 一、Redis 集群概述
|
||||||
|
|
||||||
|
#### Redis 主从复制
|
||||||
|
|
||||||
|
到 [目前](#相关阅读) 为止,我们所学习的 Redis 都是 **单机版** 的,这也就意味着一旦我们所依赖的 Redis 服务宕机了,我们的主流程也会受到一定的影响,这当然是我们不能够接受的。
|
||||||
|
|
||||||
|
所以一开始我们的想法是:搞一台备用机。这样我们就可以在一台服务器出现问题的时候切换动态地到另一台去:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
幸运的是,两个节点数据的同步我们可以使用 Redis 的 **主从同步** 功能帮助到我们,这样一来,有个备份,心里就踏实多了。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Redis 哨兵
|
||||||
|
|
||||||
|
后来因为某种神秘力量,Redis 老会在莫名其妙的时间点出问题 *(比如半夜 2 点)*,我总不能 24 小时时刻守在电脑旁边切换节点吧,于是另一个想法又开始了:给所有的节点找一个 **"管家"**,自动帮我监听照顾节点的状态并切换:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
这大概就是 **Redis 哨兵** *(Sentinel)* 的简单理解啦。什么?管家宕机了怎么办?相较于有大量请求的 Redis 服务来说,管家宕机的概率就要小得多啦.. 如果真的宕机了,我们也可以直接切换成当前可用的节点保证可用..
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Redis 集群化
|
||||||
|
|
||||||
|
好了,通过上面的一些解决方案我们对 Redis 的 **稳定性** 稍微有了一些底气了,但单台节点的计算能力始终有限,所谓人多力量大,如果我们把 **多个节点组合** 成 **一个可用的工作节点**,那就大大增加了 Redis 的 **高可用、可扩展、分布式、容错** 等特性:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# 二、主从复制
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**主从复制**,是指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器。前者称为 **主节点(master)**,后者称为 **从节点(slave)**。且数据的复制是 **单向** 的,只能由主节点到从节点。Redis 主从复制支持 **主从同步** 和 **从从同步** 两种,后者是 Redis 后续版本新增的功能,以减轻主节点的同步负担。
|
||||||
|
|
||||||
|
#### 主从复制主要的作用
|
||||||
|
|
||||||
|
- **数据冗余:** 主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
|
||||||
|
- **故障恢复:** 当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复 *(实际上是一种服务的冗余)*。
|
||||||
|
- **负载均衡:** 在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务 *(即写 Redis 数据时应用连接主节点,读 Redis 数据时应用连接从节点)*,分担服务器负载。尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高 Redis 服务器的并发量。
|
||||||
|
- **高可用基石:** 除了上述作用以外,主从复制还是哨兵和集群能够实施的 **基础**,因此说主从复制是 Redis 高可用的基础。
|
||||||
|
|
||||||
|
## 快速体验
|
||||||
|
|
||||||
|
在 **Redis** 中,用户可以通过执行 `SLAVEOF` 命令或者设置 `slaveof` 选项,让一个服务器去复制另一个服务器,以下三种方式是 **完全等效** 的:
|
||||||
|
|
||||||
|
- **配置文件**:在从服务器的配置文件中加入:`slaveof <masterip> <masterport>`
|
||||||
|
- **启动命令**:redis-server 启动命令后加入 `--slaveof <masterip> <masterport>`
|
||||||
|
- **客户端命令**:Redis 服务器启动后,直接通过客户端执行命令:`slaveof <masterip> <masterport>`,让该 Redis 实例成为从节点。
|
||||||
|
|
||||||
|
需要注意的是:**主从复制的开启,完全是在从节点发起的,不需要我们在主节点做任何事情。**
|
||||||
|
|
||||||
|
#### 第一步:本地启动两个节点
|
||||||
|
|
||||||
|
在正确安装好 Redis 之后,我们可以使用 `redis-server --port <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 <masterip> <masterport>` 命令建立主从复制关系以后,可以通过 `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.
|
||||||
|
```
|
||||||
|
|
||||||
|
## 实现原理简析
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
为了节省篇幅,我把主要的步骤都 **浓缩** 在了上图中,其实也可以 **简化成三个阶段:准备阶段-数据同步阶段-命令传播阶段**。下面我们来进行一些必要的说明。
|
||||||
|
|
||||||
|
#### 身份验证 | 主从复制安全问题
|
||||||
|
|
||||||
|
在上面的 **快速体验** 过程中,你会发现 `slaveof` 这个命令居然不需要验证?这意味着只要知道了 ip 和端口就可以随意拷贝服务器上的数据了?
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
那当然不能够了,我们可以通过在 **主节点** 配置 `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 哨兵
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
*上图* 展示了一个典型的哨兵架构图,它由两部分组成,哨兵节点和数据节点:
|
||||||
|
|
||||||
|
- **哨兵节点:** 哨兵系统由一个或多个哨兵节点组成,哨兵节点是特殊的 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` 文件的位置)*
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
打开可以看到这个 `.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 <config file path>` 来根据配置文件启动不同的 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` 检查一下主从状态是否正常:*(可以看到下方正确地显示了两个从节点)*
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### 第二步:创建哨兵节点配置文件并启动
|
||||||
|
|
||||||
|
按照上面同样的方法,我们给哨兵节点也创建三个配置文件。*(哨兵节点本质上是特殊的 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<String> 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` 命令,将这个从服务器转换为主服务器。但是这个从服务器是怎么样被挑选出来的呢?
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
简单来说 Sentinel 使用以下规则来选择新的主服务器:
|
||||||
|
|
||||||
|
1. 在失效主服务器属下的从服务器当中, 那些被标记为主观下线、已断线、或者最后一次回复 PING 命令的时间大于五秒钟的从服务器都会被 **淘汰**。
|
||||||
|
2. 在失效主服务器属下的从服务器当中, 那些与失效主服务器连接断开的时长超过 down-after 选项指定的时长十倍的从服务器都会被 **淘汰**。
|
||||||
|
3. 在 **经历了以上两轮淘汰之后** 剩下来的从服务器中, 我们选出 **复制偏移量(replication offset)最大** 的那个 **从服务器** 作为新的主服务器;如果复制偏移量不可用,或者从服务器的复制偏移量相同,那么 **带有最小运行 ID** 的那个从服务器成为新的主服务器。
|
||||||
|
|
||||||
|
# 四、Redis 集群
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
*上图* 展示了 **Redis Cluster** 典型的架构图,集群中的每一个 Redis 节点都 **互相两两相连**,客户端任意 **直连** 到集群中的 **任意一台**,就可以对其他 Redis 节点进行 **读写** 的操作。
|
||||||
|
|
||||||
|
#### 基本原理
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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` 查看是否启动成功:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
可以看到 `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` 的意思是:我们希望为集群中的每个主节点创建一个从节点。
|
||||||
|
|
||||||
|
观察控制台输出:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
看到 `[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 , 2<sup>32</sup>-1]*,对于每一个数据,根据 `key` 计算 hash 值,确数据在环上的位置,然后从此位置沿顺时针行走,找到的第一台服务器就是其应该映射到的服务器:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
与哈希取余分区相比,一致性哈希分区将 **增减节点的影响限制在相邻节点**。以上图为例,如果在 `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 <ip> <port>` 命令发送 `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,与您共同成长!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
非常感谢各位人才能 **看到这里**,如果觉得本篇文章写得不错,觉得 **「我没有三颗心脏」有点东西** 的话,**求点赞,求关注,求分享,求留言!**
|
||||||
|
|
||||||
|
创作不易,各位的支持和认可,就是我创作的最大动力,我们下篇文章见!
|
@ -0,0 +1,469 @@
|
|||||||
|
> 授权转载自: https://github.com/wmyskxz/MoreThanJava#part3-redis
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# 一、HyperLogLog 简介
|
||||||
|
|
||||||
|
**HyperLogLog** 是最早由 [Flajolet](http://algo.inria.fr/flajolet/Publications/FlFuGaMe07.pdf) 及其同事在 2007 年提出的一种 **估算基数的近似最优算法**。但跟原版论文不同的是,好像很多书包括 Redis 作者都把它称为一种 **新的数据结构(new datastruct)** *(算法实现确实需要一种特定的数据结构来实现)*。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 关于基数统计
|
||||||
|
|
||||||
|
**基数统计(Cardinality Counting)** 通常是用来统计一个集合中不重复的元素个数。
|
||||||
|
|
||||||
|
**思考这样的一个场景:** 如果你负责开发维护一个大型的网站,有一天老板找产品经理要网站上每个网页的 **UV(独立访客,每个用户每天只记录一次)**,然后让你来开发这个统计模块,你会如何实现?
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
如果统计 **PV(浏览量,用户没点一次记录一次)**,那非常好办,给每个页面配置一个独立的 Redis 计数器就可以了,把这个计数器的 key 后缀加上当天的日期。这样每来一个请求,就执行 `INCRBY` 指令一次,最终就可以统计出所有的 **PV** 数据了。
|
||||||
|
|
||||||
|
但是 **UV** 不同,它要去重,**同一个用户一天之内的多次访问请求只能计数一次**。这就要求了每一个网页请求都需要带上用户的 ID,无论是登录用户还是未登录的用户,都需要一个唯一 ID 来标识。
|
||||||
|
|
||||||
|
你也许马上就想到了一个 *简单的解决方案*:那就是 **为每一个页面设置一个独立的 set 集合** 来存储所有当天访问过此页面的用户 ID。但这样的 **问题** 就是:
|
||||||
|
|
||||||
|
1. **存储空间巨大:** 如果网站访问量一大,你需要用来存储的 set 集合就会非常大,如果页面再一多.. 为了一个去重功能耗费的资源就可以直接让你 **老板打死你**;
|
||||||
|
2. **统计复杂:** 这么多 set 集合如果要聚合统计一下,又是一个复杂的事情;
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
## 基数统计的常用方法
|
||||||
|
|
||||||
|
对于上述这样需要 **基数统计** 的事情,通常来说有两种比 set 集合更好的解决方案:
|
||||||
|
|
||||||
|
### 第一种:B 树
|
||||||
|
|
||||||
|
**B 树最大的优势就是插入和查找效率很高**,如果用 B 树存储要统计的数据,可以快速判断新来的数据是否存在,并快速将元素插入 B 树。要计算基础值,只需要计算 B 树的节点个数就行了。
|
||||||
|
|
||||||
|
不过将 B 树结构维护到内存中,能够解决统计和计算的问题,但是 **并没有节省内存**。
|
||||||
|
|
||||||
|
### 第二种:bitmap
|
||||||
|
|
||||||
|
**bitmap** 可以理解为通过一个 bit 数组来存储特定数据的一种数据结构,**每一个 bit 位都能独立包含信息**,bit 是数据的最小存储单位,因此能大量节省空间,也可以将整个 bit 数据一次性 load 到内存计算。如果定义一个很大的 bit 数组,基础统计中 **每一个元素对应到 bit 数组中的一位**,例如:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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`,对于大数据的场景仍然不适用。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 概率算法
|
||||||
|
|
||||||
|
实际上目前还没有发现更好的在 **大数据场景** 中 **准确计算** 基数的高效算法,因此在不追求绝对精确的情况下,使用概率算法算是一个不错的解决方案。
|
||||||
|
|
||||||
|
概率算法 **不直接存储** 数据集合本身,通过一定的 **概率统计方法预估基数值**,这种方法可以大大节省内存,同时保证误差控制在一定范围内。目前用于基数计数的概率算法包括:
|
||||||
|
|
||||||
|
- **Linear Counting(LC)**:早期的基数估计算法,LC 在空间复杂度方面并不算优秀,实际上 LC 的空间复杂度与上文中简单 bitmap 方法是一样的(但是有个常数项级别的降低),都是 O(N<sub>max</sub>)
|
||||||
|
- **LogLog Counting(LLC)**:LogLog Counting 相比于 LC 更加节省内存,空间复杂度只有 O(log<sub>2</sub>(log<sub>2</sub>(N<sub>max</sub>)))
|
||||||
|
- **HyperLogLog Counting(HLL)**:HyperLogLog Counting 是基于 LLC 的优化和改进,在同样空间复杂度情况下,能够比 LLC 的基数估计误差更小
|
||||||
|
|
||||||
|
其中,**HyperLogLog** 的表现是惊人的,上面我们简单计算过用 **bitmap** 存储 **1 个亿** 统计数据大概需要 `12 M` 内存,而在 **HyperLoglog** 中,只需要不到 **1 K** 内存就能够做到!在 Redis 中实现的 **HyperLoglog** 也只需要 **12 K** 内存,在 **标准误差 0.81%** 的前提下,**能够统计 2<sup>64</sup> 个数据**!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**这是怎么做到的?!** 下面赶紧来了解一下!
|
||||||
|
|
||||||
|
# 二、HyperLogLog 原理
|
||||||
|
|
||||||
|
我们来思考一个抛硬币的游戏:你连续掷 n 次硬币,然后说出其中**连续掷为正面的最大次数,我来猜你一共抛了多少次**。
|
||||||
|
|
||||||
|
这很容易理解吧,例如:你说你这一次 *最多连续出现了 2 次* 正面,那么我就可以知道你这一次投掷的次数并不多,所以 *我可能会猜是 5* 或者是其他小一些的数字,但如果你说你这一次 *最多连续出现了 20 次* 正面,虽然我觉得不可能,但我仍然知道你花了特别多的时间,所以 *我说 GUN...*。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
这期间我可能会要求你重复实验,然后我得到了更多的数据之后就会估计得更准。**我们来把刚才的游戏换一种说法**:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
这张图的意思是,我们给定一系列的随机整数,**记录下低位连续零位的最大长度 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 约等于 2<sup>k</sup>**
|
||||||
|
|
||||||
|
## 更近一步:分桶平均
|
||||||
|
|
||||||
|
**如果 `N` 介于 2<sup>k</sup> 和 2<sup>k+1</sup> 之间,用这种方式估计的值都等于 2<sup>k</sup>,这明显是不合理的**,所以我们可以使用多个 `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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这个过程有点 **类似于选秀节目里面的打分**,一堆专业评委打分,但是有一些评委因为自己特别喜欢所以给高了,一些评委又打低了,所以一般都要 **屏蔽最高分和最低分**,然后 **再计算平均值**,这样的出来的分数就差不多是公平公正的了。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
上述代码就有 **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)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
其中的一些概念这里稍微解释一下,您就可以自行去点击 `step` 来观察了:
|
||||||
|
|
||||||
|
- **m 表示分桶个数:** 从图中可以看到,这里分成了 64 个桶;
|
||||||
|
- **蓝色的 bit 表示在桶中的位置:** 例如图中的 `101110` 实则表示二进制的 `46`,所以该元素被统计在中间大表格 `Register Values` 中标红的第 46 个桶之中;
|
||||||
|
- **绿色的 bit 表示第一个 1 出现的位置**: 从图中可以看到标绿的 bit 中,从右往左数,第一位就是 1,所以在 `Register Values` 第 46 个桶中写入 1;
|
||||||
|
- **红色 bit 表示绿色 bit 的值的累加:** 下一个出现在第 46 个桶的元素值会被累加;
|
||||||
|
|
||||||
|
|
||||||
|
### 为什么要统计 Hash 值中第一个 1 出现的位置?
|
||||||
|
|
||||||
|
因为第一个 1 出现的位置可以同我们抛硬币的游戏中第一次抛到正面的抛掷次数对应起来,根据上面掷硬币实验的结论,记录每个数据的第一个出现的位置 `K`,就可以通过其中最大值 K<sub>max</sub> 来推导出数据集合中的基数:**N = 2<sup>K<sub>max</sub></sup>**
|
||||||
|
|
||||||
|
### PF 的内存占用为什么是 12 KB?
|
||||||
|
|
||||||
|
我们上面的算法中使用了 **1024** 个桶,网站演示也只有 **64** 个桶,不过在 Redis 的 HyperLogLog 实现中,用的是 **16384** 个桶,即:2<sup>14</sup>,也就是说,就像上面网站中间那个 `Register Values` 大表格有 **16384** 格。
|
||||||
|
|
||||||
|
**而Redis 最大能够统计的数据量是 2<sup>64</sup>**,即每个桶的 `maxbit` 需要 **6** 个 bit 来存储,最大可以表示 `maxbit = 63`,于是总共占用内存就是:**(2<sup>14</sup>) x 6 / 8** *(每个桶 6 bit,而这么多桶本身要占用 16384 bit,再除以 8 转换成 KB)*,算出来的结果就是 `12 KB`。
|
||||||
|
|
||||||
|
# 三、Redis 中的 HyperLogLog 实现
|
||||||
|
|
||||||
|
从上面我们算是对 **HyperLogLog** 的算法和思想有了一定的了解,并且知道了一个 **HyperLogLog** 实际占用的空间大约是 `12 KB`,但 Redis 对于内存的优化非常变态,当 **计数比较小** 的时候,大多数桶的计数值都是 **零**,这个时候 Redis 就会适当节约空间,转换成另外一种 **稀疏存储方式**,与之相对的,正常的存储模式叫做 **密集存储**,这种方式会恒定地占用 `12 KB`。
|
||||||
|
|
||||||
|
## 密集型存储结构
|
||||||
|
|
||||||
|
密集型的存储结构非常简单,就是 **16384 个 6 bit 连续串成** 的字符串位图:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
我们都知道,一个字节是由 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)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 稀疏存储结构
|
||||||
|
|
||||||
|
稀疏存储适用于很多计数值都是零的情况。下图表示了一般稀疏存储计数值的状态:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
当 **多个连续桶的计数值都是零** 时,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,与您共同成长!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
非常感谢各位人才能 **看到这里**,如果觉得本篇文章写得不错,觉得 **「我没有三颗心脏」有点东西** 的话,**求点赞,求关注,求分享,求留言!**
|
||||||
|
|
||||||
|
创作不易,各位的支持和认可,就是我创作的最大动力,我们下篇文章见!
|
269
docs/database/Redis/redis集群以及应用场景.md
Normal file
269
docs/database/Redis/redis集群以及应用场景.md
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
相关阅读:
|
||||||
|
|
||||||
|
- [史上最全Redis高可用技术解决方案大全](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247484850&idx=1&sn=3238360bfa8105cf758dcf7354af2814&chksm=cea24a79f9d5c36fb2399aafa91d7fb2699b5006d8d037fe8aaf2e5577ff20ae322868b04a87&token=1082669959&lang=zh_CN&scene=21#wechat_redirect)
|
||||||
|
- [Raft协议实战之Redis Sentinel的选举Leader源码解析](http://weizijun.cn/2015/04/30/Raft%E5%8D%8F%E8%AE%AE%E5%AE%9E%E6%88%98%E4%B9%8BRedis%20Sentinel%E7%9A%84%E9%80%89%E4%B8%BELeader%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/)
|
||||||
|
|
||||||
|
目录:
|
||||||
|
|
||||||
|
<!-- TOC -->
|
||||||
|
|
||||||
|
- [Redis 集群以及应用](#redis-集群以及应用)
|
||||||
|
- [集群](#集群)
|
||||||
|
- [主从复制](#主从复制)
|
||||||
|
- [主从链(拓扑结构)](#主从链拓扑结构)
|
||||||
|
- [复制模式](#复制模式)
|
||||||
|
- [问题点](#问题点)
|
||||||
|
- [哨兵机制](#哨兵机制)
|
||||||
|
- [拓扑图](#拓扑图)
|
||||||
|
- [节点下线](#节点下线)
|
||||||
|
- [Leader选举](#Leader选举)
|
||||||
|
- [故障转移](#故障转移)
|
||||||
|
- [读写分离](#读写分离)
|
||||||
|
- [定时任务](#定时任务)
|
||||||
|
- [分布式集群(Cluster)](#分布式集群cluster)
|
||||||
|
- [拓扑图](#拓扑图)
|
||||||
|
- [通讯](#通讯)
|
||||||
|
- [集中式](#集中式)
|
||||||
|
- [Gossip](#gossip)
|
||||||
|
- [寻址分片](#寻址分片)
|
||||||
|
- [hash取模](#hash取模)
|
||||||
|
- [一致性hash](#一致性hash)
|
||||||
|
- [hash槽](#hash槽)
|
||||||
|
- [使用场景](#使用场景)
|
||||||
|
- [热点数据](#热点数据)
|
||||||
|
- [会话维持 Session](#会话维持-session)
|
||||||
|
- [分布式锁 SETNX](#分布式锁-setnx)
|
||||||
|
- [表缓存](#表缓存)
|
||||||
|
- [消息队列 list](#消息队列-list)
|
||||||
|
- [计数器 string](#计数器-string)
|
||||||
|
- [缓存设计](#缓存设计)
|
||||||
|
- [更新策略](#更新策略)
|
||||||
|
- [更新一致性](#更新一致性)
|
||||||
|
- [缓存粒度](#缓存粒度)
|
||||||
|
- [缓存穿透](#缓存穿透)
|
||||||
|
- [解决方案](#解决方案)
|
||||||
|
- [缓存雪崩](#缓存雪崩)
|
||||||
|
- [出现后应对](#出现后应对)
|
||||||
|
- [请求过程](#请求过程)
|
||||||
|
|
||||||
|
<!-- /MarkdownTOC -->
|
||||||
|
|
||||||
|
# Redis 集群以及应用
|
||||||
|
|
||||||
|
## 集群
|
||||||
|
|
||||||
|
### 主从复制
|
||||||
|
|
||||||
|
#### 主从链(拓扑结构)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### 复制模式
|
||||||
|
- 全量复制:Master 全部同步到 Slave
|
||||||
|
- 部分复制:Slave 数据丢失进行备份
|
||||||
|
|
||||||
|
#### 问题点
|
||||||
|
- 同步故障
|
||||||
|
- 复制数据延迟(不一致)
|
||||||
|
- 读取过期数据(Slave 不能删除数据)
|
||||||
|
- 从节点故障
|
||||||
|
- 主节点故障
|
||||||
|
- 配置不一致
|
||||||
|
- maxmemory 不一致:丢失数据
|
||||||
|
- 优化参数不一致:内存不一致.
|
||||||
|
- 避免全量复制
|
||||||
|
- 选择小主节点(分片)、低峰期间操作.
|
||||||
|
- 如果节点运行 id 不匹配(如主节点重启、运行 id 发送变化),此时要执行全量复制,应该配合哨兵和集群解决.
|
||||||
|
- 主从复制挤压缓冲区不足产生的问题(网络中断,部分复制无法满足),可增大复制缓冲区( rel_backlog_size 参数).
|
||||||
|
- 复制风暴
|
||||||
|
|
||||||
|
### 哨兵机制
|
||||||
|
|
||||||
|
#### 拓扑图
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### 节点下线
|
||||||
|
|
||||||
|
- 主观下线
|
||||||
|
- 即 Sentinel 节点对 Redis 节点失败的偏见,超出超时时间认为 Master 已经宕机。
|
||||||
|
- Sentinel 集群的每一个 Sentinel 节点会定时对 Redis 集群的所有节点发心跳包检测节点是否正常。如果一个节点在 `down-after-milliseconds` 时间内没有回复 Sentinel 节点的心跳包,则该 Redis 节点被该 Sentinel 节点主观下线。
|
||||||
|
- 客观下线
|
||||||
|
- 所有 Sentinel 节点对 Redis 节点失败要达成共识,即超过 quorum 个统一。
|
||||||
|
- 当节点被一个 Sentinel 节点记为主观下线时,并不意味着该节点肯定故障了,还需要 Sentinel 集群的其他 Sentinel 节点共同判断为主观下线才行。
|
||||||
|
- 该 Sentinel 节点会询问其它 Sentinel 节点,如果 Sentinel 集群中超过 quorum 数量的 Sentinel 节点认为该 Redis 节点主观下线,则该 Redis 客观下线。
|
||||||
|
|
||||||
|
#### Leader选举
|
||||||
|
|
||||||
|
- 选举出一个 Sentinel 作为 Leader:集群中至少有三个 Sentinel 节点,但只有其中一个节点可完成故障转移.通过以下命令可以进行失败判定或领导者选举。
|
||||||
|
- 选举流程
|
||||||
|
1. 每个主观下线的 Sentinel 节点向其他 Sentinel 节点发送命令,要求设置它为领导者.
|
||||||
|
2. 收到命令的 Sentinel 节点如果没有同意通过其他 Sentinel 节点发送的命令,则同意该请求,否则拒绝。
|
||||||
|
3. 如果该 Sentinel 节点发现自己的票数已经超过 Sentinel 集合半数且超过 quorum,则它成为领导者。
|
||||||
|
4. 如果此过程有多个 Sentinel 节点成为领导者,则等待一段时间再重新进行选举。
|
||||||
|
|
||||||
|
#### 故障转移
|
||||||
|
|
||||||
|
- 转移流程
|
||||||
|
1. Sentinel 选出一个合适的 Slave 作为新的 Master(slaveof no one 命令)。
|
||||||
|
2. 向其余 Slave 发出通知,让它们成为新 Master 的 Slave( parallel-syncs 参数)。
|
||||||
|
3. 等待旧 Master 复活,并使之称为新 Master 的 Slave。
|
||||||
|
4. 向客户端通知 Master 变化。
|
||||||
|
- 从 Slave 中选择新 Master 节点的规则(slave 升级成 master 之后)
|
||||||
|
1. 选择 slave-priority 最高的节点。
|
||||||
|
2. 选择复制偏移量最大的节点(同步数据最多)。
|
||||||
|
3. 选择 runId 最小的节点。
|
||||||
|
|
||||||
|
>Sentinel 集群运行过程中故障转移完成,所有 Sentinel 又会恢复平等。Leader 仅仅是故障转移操作出现的角色。
|
||||||
|
|
||||||
|
#### 读写分离
|
||||||
|
|
||||||
|
#### 定时任务
|
||||||
|
|
||||||
|
- 每 1s 每个 Sentinel 对其他 Sentinel 和 Redis 执行 ping,进行心跳检测。
|
||||||
|
- 每 2s 每个 Sentinel 通过 Master 的 Channel 交换信息(pub - sub)。
|
||||||
|
- 每 10s 每个 Sentinel 对 Master 和 Slave 执行 info,目的是发现 Slave 节点、确定主从关系。
|
||||||
|
|
||||||
|
### 分布式集群(Cluster)
|
||||||
|
|
||||||
|
#### 拓扑图
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### 通讯
|
||||||
|
|
||||||
|
##### 集中式
|
||||||
|
|
||||||
|
> 将集群元数据(节点信息、故障等等)几种存储在某个节点上。
|
||||||
|
- 优势
|
||||||
|
1. 元数据的更新读取具有很强的时效性,元数据修改立即更新
|
||||||
|
- 劣势
|
||||||
|
1. 数据集中存储
|
||||||
|
|
||||||
|
##### Gossip
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- [Gossip 协议](https://www.jianshu.com/p/8279d6fd65bb)
|
||||||
|
|
||||||
|
#### 寻址分片
|
||||||
|
|
||||||
|
##### hash取模
|
||||||
|
|
||||||
|
- hash(key)%机器数量
|
||||||
|
- 问题
|
||||||
|
1. 机器宕机,造成数据丢失,数据读取失败
|
||||||
|
1. 伸缩性
|
||||||
|
|
||||||
|
##### 一致性hash
|
||||||
|
|
||||||
|
- 
|
||||||
|
|
||||||
|
- 问题
|
||||||
|
1. 一致性哈希算法在节点太少时,容易因为节点分布不均匀而造成缓存热点的问题。
|
||||||
|
- 解决方案
|
||||||
|
- 可以通过引入虚拟节点机制解决:即对每一个节点计算多个 hash,每个计算结果位置都放置一个虚拟节点。这样就实现了数据的均匀分布,负载均衡。
|
||||||
|
|
||||||
|
##### hash槽
|
||||||
|
|
||||||
|
- CRC16(key)%16384
|
||||||
|
-
|
||||||
|

|
||||||
|
|
||||||
|
## 使用场景
|
||||||
|
|
||||||
|
### 热点数据
|
||||||
|
|
||||||
|
存取数据优先从 Redis 操作,如果不存在再从文件(例如 MySQL)中操作,从文件操作完后将数据存储到 Redis 中并返回。同时有个定时任务后台定时扫描 Redis 的 key,根据业务规则进行淘汰,防止某些只访问一两次的数据一直存在 Redis 中。
|
||||||
|
>例如使用 Zset 数据结构,存储 Key 的访问次数/最后访问时间作为 Score,最后做排序,来淘汰那些最少访问的 Key。
|
||||||
|
|
||||||
|
如果企业级应用,可以参考:[阿里云的 Redis 混合存储版][1]
|
||||||
|
|
||||||
|
### 会话维持 Session
|
||||||
|
|
||||||
|
会话维持 Session 场景,即使用 Redis 作为分布式场景下的登录中心存储应用。每次不同的服务在登录的时候,都会去统一的 Redis 去验证 Session 是否正确。但是在微服务场景,一般会考虑 Redis + JWT 做 Oauth2 模块。
|
||||||
|
>其中 Redis 存储 JWT 的相关信息主要是留出口子,方便以后做统一的防刷接口,或者做登录设备限制等。
|
||||||
|
|
||||||
|
### 分布式锁 SETNX
|
||||||
|
|
||||||
|
命令格式:`SETNX key value`:当且仅当 key 不存在,将 key 的值设为 value。若给定的 key 已经存在,则 SETNX 不做任何动作。
|
||||||
|
|
||||||
|
1. 超时时间设置:获取锁的同时,启动守护线程,使用 expire 进行定时更新超时时间。如果该业务机器宕机,守护线程也挂掉,这样也会自动过期。如果该业务不是宕机,而是真的需要这么久的操作时间,那么增加超时时间在业务上也是可以接受的,但是肯定有个最大的阈值。
|
||||||
|
2. 但是为了增加高可用,需要使用多台 Redis,就增加了复杂性,就可以参考 Redlock:[Redlock分布式锁](Redlock分布式锁.md#怎么在单节点上实现分布式锁)
|
||||||
|
|
||||||
|
### 表缓存
|
||||||
|
|
||||||
|
Redis 缓存表的场景有黑名单、禁言表等。访问频率较高,即读高。根据业务需求,可以使用后台定时任务定时刷新 Redis 的缓存表数据。
|
||||||
|
|
||||||
|
### 消息队列 list
|
||||||
|
|
||||||
|
主要使用了 List 数据结构。
|
||||||
|
List 支持在头部和尾部操作,因此可以实现简单的消息队列。
|
||||||
|
1. 发消息:在 List 尾部塞入数据。
|
||||||
|
2. 消费消息:在 List 头部拿出数据。
|
||||||
|
|
||||||
|
同时可以使用多个 List,来实现多个队列,根据不同的业务消息,塞入不同的 List,来增加吞吐量。
|
||||||
|
|
||||||
|
### 计数器 string
|
||||||
|
|
||||||
|
主要使用了 INCR、DECR、INCRBY、DECRBY 方法。
|
||||||
|
|
||||||
|
INCR key:给 key 的 value 值增加一
|
||||||
|
DECR key:给 key 的 value 值减去一
|
||||||
|
|
||||||
|
## 缓存设计
|
||||||
|
|
||||||
|
### 更新策略
|
||||||
|
|
||||||
|
- LRU、LFU、FIFO 算法自动清除:一致性最差,维护成本低。
|
||||||
|
- 超时自动清除(key expire):一致性较差,维护成本低。
|
||||||
|
- 主动更新:代码层面控制生命周期,一致性最好,维护成本高。
|
||||||
|
|
||||||
|
在 Redis 根据在 redis.conf 的参数 `maxmemory` 来做更新淘汰策略:
|
||||||
|
1. noeviction: 不删除策略, 达到最大内存限制时, 如果需要更多内存, 直接返回错误信息。大多数写命令都会导致占用更多的内存(有极少数会例外, 如 DEL 命令)。
|
||||||
|
2. allkeys-lru: 所有 key 通用; 优先删除最近最少使用(less recently used ,LRU) 的 key。
|
||||||
|
3. volatile-lru: 只限于设置了 expire 的部分; 优先删除最近最少使用(less recently used ,LRU) 的 key。
|
||||||
|
4. allkeys-random: 所有key通用; 随机删除一部分 key。
|
||||||
|
5. volatile-random: 只限于设置了 expire 的部分; 随机删除一部分 key。
|
||||||
|
6. volatile-ttl: 只限于设置了 expire 的部分; 优先删除剩余时间(time to live,TTL) 短的key。
|
||||||
|
|
||||||
|
### 更新一致性
|
||||||
|
|
||||||
|
- 读请求:先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
|
||||||
|
- 写请求:先删除缓存,然后再更新数据库(避免大量地写、却又不经常读的数据导致缓存频繁更新)。
|
||||||
|
|
||||||
|
### 缓存粒度
|
||||||
|
|
||||||
|
- 通用性:全量属性更好。
|
||||||
|
- 占用空间:部分属性更好。
|
||||||
|
- 代码维护成本。
|
||||||
|
|
||||||
|
### 缓存穿透
|
||||||
|
|
||||||
|
> 当大量的请求无命中缓存、直接请求到后端数据库(业务代码的 bug、或恶意攻击),同时后端数据库也没有查询到相应的记录、无法添加缓存。
|
||||||
|
> 这种状态会一直维持,流量一直打到存储层上,无法利用缓存、还会给存储层带来巨大压力。
|
||||||
|
|
||||||
|
#### 解决方案
|
||||||
|
|
||||||
|
1. 请求无法命中缓存、同时数据库记录为空时在缓存添加该 key 的空对象(设置过期时间),缺点是可能会在缓存中添加大量的空值键(比如遭到恶意攻击或爬虫),而且缓存层和存储层数据短期内不一致;
|
||||||
|
2. 使用布隆过滤器在缓存层前拦截非法请求、自动为空值添加黑名单(同时可能要为误判的记录添加白名单).但需要考虑布隆过滤器的维护(离线生成/ 实时生成)。
|
||||||
|
|
||||||
|
### 缓存雪崩
|
||||||
|
|
||||||
|
> 缓存崩溃时请求会直接落到数据库上,很可能由于无法承受大量的并发请求而崩溃,此时如果只重启数据库,或因为缓存重启后没有数据,新的流量进来很快又会把数据库击倒。
|
||||||
|
|
||||||
|
#### 出现后应对
|
||||||
|
|
||||||
|
- 事前:Redis 高可用,主从 + 哨兵,Redis Cluster,避免全盘崩溃。
|
||||||
|
- 事中:本地 ehcache 缓存 + hystrix 限流 & 降级,避免数据库承受太多压力。
|
||||||
|
- 事后:Redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
|
||||||
|
|
||||||
|
#### 请求过程
|
||||||
|
|
||||||
|
1. 用户请求先访问本地缓存,无命中后再访问 Redis,如果本地缓存和 Redis 都没有再查数据库,并把数据添加到本地缓存和 Redis;
|
||||||
|
2. 由于设置了限流,一段时间范围内超出的请求走降级处理(返回默认值,或给出友情提示)。
|
||||||
|
|
@ -104,7 +104,7 @@ SHOW VARIABLES -- 显示系统变量信息
|
|||||||
-- 查看所有表
|
-- 查看所有表
|
||||||
SHOW TABLES[ LIKE 'pattern']
|
SHOW TABLES[ LIKE 'pattern']
|
||||||
SHOW TABLES FROM 库名
|
SHOW TABLES FROM 库名
|
||||||
-- 查看表机构
|
-- 查看表结构
|
||||||
SHOW CREATE TABLE 表名 (信息更详细)
|
SHOW CREATE TABLE 表名 (信息更详细)
|
||||||
DESC 表名 / DESCRIBE 表名 / EXPLAIN 表名 / SHOW COLUMNS FROM 表名 [LIKE 'PATTERN']
|
DESC 表名 / DESCRIBE 表名 / EXPLAIN 表名 / SHOW COLUMNS FROM 表名 [LIKE 'PATTERN']
|
||||||
SHOW TABLE STATUS [FROM db_name] [LIKE 'pattern']
|
SHOW TABLE STATUS [FROM db_name] [LIKE 'pattern']
|
||||||
@ -242,7 +242,7 @@ SET NAMES GBK; -- 相当于完成以上三个设置
|
|||||||
utf8 最大为21844个字符,gbk 最大为32766个字符,latin1 最大为65532个字符
|
utf8 最大为21844个字符,gbk 最大为32766个字符,latin1 最大为65532个字符
|
||||||
varchar 是变长的,需要利用存储空间保存 varchar 的长度,如果数据小于255个字节,则采用一个字节来保存长度,反之需要两个字节来保存。
|
varchar 是变长的,需要利用存储空间保存 varchar 的长度,如果数据小于255个字节,则采用一个字节来保存长度,反之需要两个字节来保存。
|
||||||
varchar 的最大有效长度由最大行大小和使用的字符集确定。
|
varchar 的最大有效长度由最大行大小和使用的字符集确定。
|
||||||
最大有效长度是65532字节,因为在varchar存字符串时,第一个字节是空的,不存在任何数据,然后还需两个字节来存放字符串的长度,所以有效长度是64432-1-2=65532字节。
|
最大有效长度是65532字节,因为在varchar存字符串时,第一个字节是空的,不存在任何数据,然后还需两个字节来存放字符串的长度,所以有效长度是65535-1-2=65532字节。
|
||||||
例:若一个表定义为 CREATE TABLE tb(c1 int, c2 char(30), c3 varchar(N)) charset=utf8; 问N的最大值是多少? 答:(65535-1-2-4-30*3)/3
|
例:若一个表定义为 CREATE TABLE tb(c1 int, c2 char(30), c3 varchar(N)) charset=utf8; 问N的最大值是多少? 答:(65535-1-2-4-30*3)/3
|
||||||
-- b. blob, text ----------
|
-- b. blob, text ----------
|
||||||
blob 二进制字符串(字节字符串)
|
blob 二进制字符串(字节字符串)
|
||||||
@ -363,7 +363,7 @@ set(val1, val2, val3...)
|
|||||||
字段不能再分,就满足第一范式。
|
字段不能再分,就满足第一范式。
|
||||||
-- 2NF, 第二范式
|
-- 2NF, 第二范式
|
||||||
满足第一范式的前提下,不能出现部分依赖。
|
满足第一范式的前提下,不能出现部分依赖。
|
||||||
消除符合主键就可以避免部分依赖。增加单列关键字。
|
消除复合主键就可以避免部分依赖。增加单列关键字。
|
||||||
-- 3NF, 第三范式
|
-- 3NF, 第三范式
|
||||||
满足第二范式的前提下,不能出现传递依赖。
|
满足第二范式的前提下,不能出现传递依赖。
|
||||||
某个字段依赖于主键,而有其他字段依赖于该字段。这就是传递依赖。
|
某个字段依赖于主键,而有其他字段依赖于该字段。这就是传递依赖。
|
||||||
@ -590,7 +590,7 @@ CREATE [OR REPLACE] [ALGORITHM = {UNDEFINED | MERGE | TEMPTABLE}] VIEW view_name
|
|||||||
```mysql
|
```mysql
|
||||||
事务是指逻辑上的一组操作,组成这组操作的各个单元,要不全成功要不全失败。
|
事务是指逻辑上的一组操作,组成这组操作的各个单元,要不全成功要不全失败。
|
||||||
- 支持连续SQL的集体成功或集体撤销。
|
- 支持连续SQL的集体成功或集体撤销。
|
||||||
- 事务是数据库在数据晚自习方面的一个功能。
|
- 事务是数据库在数据完整性方面的一个功能。
|
||||||
- 需要利用 InnoDB 或 BDB 存储引擎,对自动提交的特性支持完成。
|
- 需要利用 InnoDB 或 BDB 存储引擎,对自动提交的特性支持完成。
|
||||||
- InnoDB被称为事务安全型引擎。
|
- InnoDB被称为事务安全型引擎。
|
||||||
-- 事务开启
|
-- 事务开启
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
本文来自[木木匠](https://github.com/kinglaw1204)投稿,[SnailClimb](https://github.com/Snailclimb) 对本文进行了内容和排版进行了修改完善。
|
本文来自[木木匠](https://github.com/kinglaw1204)投稿。
|
||||||
|
|
||||||
<!-- TOC -->
|
<!-- TOC -->
|
||||||
|
|
||||||
@ -138,12 +138,12 @@ update tb_student A set A.age='19' where A.name=' 张三 ';
|
|||||||
|
|
||||||
## 三 总结
|
## 三 总结
|
||||||
|
|
||||||
* MySQL 主要分为 Server 曾和引擎层,Server 层主要包括连接器、查询缓存、分析器、优化器、执行器,同时还有一个日志模块(binlog),这个日志模块所有执行引擎都可以共用,redolog 只有 InnoDB 有。
|
* MySQL 主要分为 Server 层和引擎层,Server 层主要包括连接器、查询缓存、分析器、优化器、执行器,同时还有一个日志模块(binlog),这个日志模块所有执行引擎都可以共用,redolog 只有 InnoDB 有。
|
||||||
* 引擎层是插件式的,目前主要包括,MyISAM,InnoDB,Memory 等。
|
* 引擎层是插件式的,目前主要包括,MyISAM,InnoDB,Memory 等。
|
||||||
* 查询语句的执行流程如下:权限校验(如果命中缓存)---》查询缓存---》分析器---》优化器---》权限校验---》执行器---》引擎
|
* 查询语句的执行流程如下:权限校验(如果命中缓存)---》查询缓存---》分析器---》优化器---》权限校验---》执行器---》引擎
|
||||||
* 更新语句执行流程如下:分析器----》权限校验----》执行器---》引擎---redo log(prepare 状态---》binlog---》redo log(commit状态)
|
* 更新语句执行流程如下:分析器----》权限校验----》执行器---》引擎---redo log(prepare 状态---》binlog---》redo log(commit状态)
|
||||||
|
|
||||||
## 四 参考
|
## 四 参考
|
||||||
|
|
||||||
* 《一起构建 MySQL 知识网络》
|
* 《MySQL 实战45讲》
|
||||||
* MySQL 5.6参考手册:<https://dev.MySQL.com/doc/refman/5.6/en/>
|
* MySQL 5.6参考手册:<https://dev.MySQL.com/doc/refman/5.6/en/>
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
> 本文由 [SnailClimb](https://github.com/Snailclimb) 和 [BugSpeak](https://github.com/BugSpeak) 共同完成。
|
> 本文由 [SnailClimb](https://github.com/Snailclimb) 和 [guang19](https://github.com/guang19) 共同完成。
|
||||||
<!-- TOC -->
|
<!-- TOC -->
|
||||||
|
|
||||||
- [事务隔离级别(图文详解)](#事务隔离级别图文详解)
|
- [事务隔离级别(图文详解)](#事务隔离级别图文详解)
|
||||||
- [什么是事务?](#什么是事务)
|
- [什么是事务?](#什么是事务)
|
||||||
- [事物的特性(ACID)](#事物的特性acid)
|
- [事务的特性(ACID)](#事务的特性acid)
|
||||||
- [并发事务带来的问题](#并发事务带来的问题)
|
- [并发事务带来的问题](#并发事务带来的问题)
|
||||||
- [事务隔离级别](#事务隔离级别)
|
- [事务隔离级别](#事务隔离级别)
|
||||||
- [实际情况演示](#实际情况演示)
|
- [实际情况演示](#实际情况演示)
|
||||||
@ -24,20 +24,19 @@
|
|||||||
|
|
||||||
事务最经典也经常被拿出来说例子就是转账了。假如小明要给小红转账1000元,这个转账会涉及到两个关键操作就是:将小明的余额减少1000元,将小红的余额增加1000元。万一在这两个操作之间突然出现错误比如银行系统崩溃,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。
|
事务最经典也经常被拿出来说例子就是转账了。假如小明要给小红转账1000元,这个转账会涉及到两个关键操作就是:将小明的余额减少1000元,将小红的余额增加1000元。万一在这两个操作之间突然出现错误比如银行系统崩溃,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。
|
||||||
|
|
||||||
### 事物的特性(ACID)
|
### 事务的特性(ACID)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
<div align="center">
|
|
||||||
<img src="https://user-gold-cdn.xitu.io/2018/5/20/1637b08b98619455?w=312&h=305&f=png&s=22430" width="200px"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
1. **原子性:** 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
|
1. **原子性:** 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
|
||||||
2. **一致性:** 执行事务前后,数据保持一致;
|
2. **一致性:** 执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的;
|
||||||
3. **隔离性:** 并发访问数据库时,一个用户的事物不被其他事物所干扰,各并发事务之间数据库是独立的;
|
3. **隔离性:** 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
|
||||||
4. **持久性:** 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
|
4. **持久性:** 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
|
||||||
|
|
||||||
### 并发事务带来的问题
|
### 并发事务带来的问题
|
||||||
|
|
||||||
在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对统一数据进行操作)。并发虽然是必须的,但可能会导致一下的问题。
|
在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对统一数据进行操作)。并发虽然是必须的,但可能会导致以下的问题。
|
||||||
|
|
||||||
- **脏读(Dirty read):** 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
|
- **脏读(Dirty read):** 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
|
||||||
- **丢失修改(Lost to modify):** 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。
|
- **丢失修改(Lost to modify):** 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。
|
||||||
@ -56,12 +55,21 @@
|
|||||||
|
|
||||||
**SQL 标准定义了四个隔离级别:**
|
**SQL 标准定义了四个隔离级别:**
|
||||||
|
|
||||||
- **READ-UNCOMMITTED(读取未提交):** 最低的隔离级别,允许读取尚未提交的数据变更,**可能会导致脏读、幻读或不可重复读**
|
- **READ-UNCOMMITTED(读取未提交):** 最低的隔离级别,允许读取尚未提交的数据变更,**可能会导致脏读、幻读或不可重复读**。
|
||||||
- **READ-COMMITTED(读取已提交):** 允许读取并发事务已经提交的数据,**可以阻止脏读,但是幻读或不可重复读仍有可能发生**
|
- **READ-COMMITTED(读取已提交):** 允许读取并发事务已经提交的数据,**可以阻止脏读,但是幻读或不可重复读仍有可能发生**。
|
||||||
- **REPEATABLE-READ(可重读):** 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,**可以阻止脏读和不可重复读,但幻读仍有可能发生。**
|
- **REPEATABLE-READ(可重复读):** 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,**可以阻止脏读和不可重复读,但幻读仍有可能发生**。
|
||||||
- **SERIALIZABLE(可串行化):** 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,**该级别可以防止脏读、不可重复读以及幻读**。
|
- **SERIALIZABLE(可串行化):** 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,**该级别可以防止脏读、不可重复读以及幻读**。
|
||||||
|
|
||||||
MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)**。我们可以通过`SELECT @@tx_isolation;`命令来查看
|
----
|
||||||
|
|
||||||
|
| 隔离级别 | 脏读 | 不可重复读 | 幻影读 |
|
||||||
|
| :---: | :---: | :---:| :---: |
|
||||||
|
| READ-UNCOMMITTED | √ | √ | √ |
|
||||||
|
| READ-COMMITTED | × | √ | √ |
|
||||||
|
| REPEATABLE-READ | × | × | √ |
|
||||||
|
| SERIALIZABLE | × | × | × |
|
||||||
|
|
||||||
|
MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)**。我们可以通过`SELECT @@tx_isolation;`命令来查看,MySQL 8.0 该命令改为`SELECT @@transaction_isolation;`
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
mysql> SELECT @@tx_isolation;
|
mysql> SELECT @@tx_isolation;
|
||||||
@ -92,9 +100,9 @@ SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL [READ UNCOMMITTED|READ COMMITTE
|
|||||||
|
|
||||||
我们再来看一下我们在下面实际操作中使用到的一些并发控制语句:
|
我们再来看一下我们在下面实际操作中使用到的一些并发控制语句:
|
||||||
|
|
||||||
- `START TARNSACTION` |`BEGIN`:显式地开启一个事务。
|
- `START TARNSACTION` |`BEGIN`:显式地开启一个事务。
|
||||||
- `COMMIT`:提交事务,使得对数据库做的所有修改成为永久性。
|
- `COMMIT`:提交事务,使得对数据库做的所有修改成为永久性。
|
||||||
- `ROLLBACK` 回滚会结束用户的事务,并撤销正在进行的所有未提交的修改。
|
- `ROLLBACK`:回滚会结束用户的事务,并撤销正在进行的所有未提交的修改。
|
||||||
|
|
||||||
#### 脏读(读未提交)
|
#### 脏读(读未提交)
|
||||||
|
|
||||||
@ -136,3 +144,5 @@ SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL [READ UNCOMMITTED|READ COMMITTE
|
|||||||
|
|
||||||
- 《MySQL技术内幕:InnoDB存储引擎》
|
- 《MySQL技术内幕:InnoDB存储引擎》
|
||||||
- <https://dev.mysql.com/doc/refman/5.7/en/>
|
- <https://dev.mysql.com/doc/refman/5.7/en/>
|
||||||
|
- [Mysql 锁:灵魂七拷问](https://tech.youzan.com/seven-questions-about-the-lock-of-mysql/)
|
||||||
|
- [Innodb 中的事务隔离级别和锁的关系](https://tech.meituan.com/2014/08/20/innodb-lock.html)
|
||||||
|
160
docs/database/关于数据库存储时间的一点思考.md
Normal file
160
docs/database/关于数据库存储时间的一点思考.md
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
我们平时开发中不可避免的就是要存储时间,比如我们要记录操作表中这条记录的时间、记录转账的交易时间、记录出发时间等等。你会发现这个时间这个东西与我们开发的联系还是非常紧密的,用的好与不好会给我们的业务甚至功能带来很大的影响。所以,我们有必要重新出发,好好认识一下这个东西。
|
||||||
|
|
||||||
|
这是一篇短小精悍的文章,仔细阅读一定能学到不少东西!
|
||||||
|
|
||||||
|
### 1.切记不要用字符串存储日期
|
||||||
|
|
||||||
|
我记得我在大学的时候就这样干过,而且现在很多对数据库不太了解的新手也会这样干,可见,这种存储日期的方式的优点还是有的,就是简单直白,容易上手。
|
||||||
|
|
||||||
|
但是,这是不正确的做法,主要会有下面两个问题:
|
||||||
|
|
||||||
|
1. 字符串占用的空间更大!
|
||||||
|
2. 字符串存储的日期比较效率比较低(逐个字符进行比对),无法用日期相关的 API 进行计算和比较。
|
||||||
|
|
||||||
|
### 2.Datetime 和 Timestamp 之间抉择
|
||||||
|
|
||||||
|
Datetime 和 Timestamp 是 MySQL 提供的两种比较相似的保存时间的数据类型。他们两者究竟该如何选择呢?
|
||||||
|
|
||||||
|
**通常我们都会首选 Timestamp。** 下面说一下为什么这样做!
|
||||||
|
|
||||||
|
#### 2.1 DateTime 类型没有时区信息的
|
||||||
|
|
||||||
|
**DateTime 类型是没有时区信息的(时区无关)** ,DateTime 类型保存的时间都是当前会话所设置的时区对应的时间。这样就会有什么问题呢?当你的时区更换之后,比如你的服务器更换地址或者更换客户端连接时区设置的话,就会导致你从数据库中读出的时间错误。不要小看这个问题,很多系统就是因为这个问题闹出了很多笑话。
|
||||||
|
|
||||||
|
**Timestamp 和时区有关**。Timestamp 类型字段的值会随着服务器时区的变化而变化,自动换算成相应的时间,说简单点就是在不同时区,查询到同一个条记录此字段的值会不一样。
|
||||||
|
|
||||||
|
下面实际演示一下!
|
||||||
|
|
||||||
|
建表 SQL 语句:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE `time_zone_test` (
|
||||||
|
`id` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||||
|
`date_time` datetime DEFAULT NULL,
|
||||||
|
`time_stamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||||
|
```
|
||||||
|
|
||||||
|
插入数据:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO time_zone_test(date_time,time_stamp) VALUES(NOW(),NOW());
|
||||||
|
```
|
||||||
|
|
||||||
|
查看数据:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
select date_time,time_stamp from time_zone_test;
|
||||||
|
```
|
||||||
|
|
||||||
|
结果:
|
||||||
|
|
||||||
|
```
|
||||||
|
+---------------------+---------------------+
|
||||||
|
| date_time | time_stamp |
|
||||||
|
+---------------------+---------------------+
|
||||||
|
| 2020-01-11 09:53:32 | 2020-01-11 09:53:32 |
|
||||||
|
+---------------------+---------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
现在我们运行
|
||||||
|
|
||||||
|
修改当前会话的时区:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
set time_zone='+8:00';
|
||||||
|
```
|
||||||
|
|
||||||
|
再次查看数据:
|
||||||
|
|
||||||
|
```
|
||||||
|
+---------------------+---------------------+
|
||||||
|
| date_time | time_stamp |
|
||||||
|
+---------------------+---------------------+
|
||||||
|
| 2020-01-11 09:53:32 | 2020-01-11 17:53:32 |
|
||||||
|
+---------------------+---------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
**扩展:一些关于 MySQL 时区设置的一个常用 sql 命令**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
# 查看当前会话时区
|
||||||
|
SELECT @@session.time_zone;
|
||||||
|
# 设置当前会话时区
|
||||||
|
SET time_zone = 'Europe/Helsinki';
|
||||||
|
SET time_zone = "+00:00";
|
||||||
|
# 数据库全局时区设置
|
||||||
|
SELECT @@global.time_zone;
|
||||||
|
# 设置全局时区
|
||||||
|
SET GLOBAL time_zone = '+8:00';
|
||||||
|
SET GLOBAL time_zone = 'Europe/Helsinki';
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2 DateTime 类型耗费空间更大
|
||||||
|
|
||||||
|
Timestamp 只需要使用 4 个字节的存储空间,但是 DateTime 需要耗费 8 个字节的存储空间。但是,这样同样造成了一个问题,Timestamp 表示的时间范围更小。
|
||||||
|
|
||||||
|
- DateTime :1000-01-01 00:00:00 ~ 9999-12-31 23:59:59
|
||||||
|
- Timestamp: 1970-01-01 00:00:01 ~ 2037-12-31 23:59:59
|
||||||
|
|
||||||
|
> Timestamp 在不同版本的 MySQL 中有细微差别。
|
||||||
|
|
||||||
|
### 3 再看 MySQL 日期类型存储空间
|
||||||
|
|
||||||
|
下图是 MySQL 5.6 版本中日期类型所占的存储空间:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
可以看出 5.6.4 之后的 MySQL 多出了一个需要 0 ~ 3 字节的小数位。Datatime 和 Timestamp 会有几种不同的存储空间占用。
|
||||||
|
|
||||||
|
为了方便,本文我们还是默认 Timestamp 只需要使用 4 个字节的存储空间,但是 DateTime 需要耗费 8 个字节的存储空间。
|
||||||
|
|
||||||
|
### 4.数值型时间戳是更好的选择吗?
|
||||||
|
|
||||||
|
很多时候,我们也会使用 int 或者 bigint 类型的数值也就是时间戳来表示时间。
|
||||||
|
|
||||||
|
这种存储方式的具有 Timestamp 类型的所具有一些优点,并且使用它的进行日期排序以及对比等操作的效率会更高,跨系统也很方便,毕竟只是存放的数值。缺点也很明显,就是数据的可读性太差了,你无法直观的看到具体时间。
|
||||||
|
|
||||||
|
时间戳的定义如下:
|
||||||
|
|
||||||
|
> 时间戳的定义是从一个基准时间开始算起,这个基准时间是「1970-1-1 00:00:00 +0:00」,从这个时间开始,用整数表示,以秒计时,随着时间的流逝这个时间整数不断增加。这样一来,我只需要一个数值,就可以完美地表示时间了,而且这个数值是一个绝对数值,即无论的身处地球的任何角落,这个表示时间的时间戳,都是一样的,生成的数值都是一样的,并且没有时区的概念,所以在系统的中时间的传输中,都不需要进行额外的转换了,只有在显示给用户的时候,才转换为字符串格式的本地时间。
|
||||||
|
|
||||||
|
数据库中实际操作:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
mysql> select UNIX_TIMESTAMP('2020-01-11 09:53:32');
|
||||||
|
+---------------------------------------+
|
||||||
|
| UNIX_TIMESTAMP('2020-01-11 09:53:32') |
|
||||||
|
+---------------------------------------+
|
||||||
|
| 1578707612 |
|
||||||
|
+---------------------------------------+
|
||||||
|
1 row in set (0.00 sec)
|
||||||
|
|
||||||
|
mysql> select FROM_UNIXTIME(1578707612);
|
||||||
|
+---------------------------+
|
||||||
|
| FROM_UNIXTIME(1578707612) |
|
||||||
|
+---------------------------+
|
||||||
|
| 2020-01-11 09:53:32 |
|
||||||
|
+---------------------------+
|
||||||
|
1 row in set (0.01 sec)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.总结
|
||||||
|
|
||||||
|
MySQL 中时间到底怎么存储才好?Datetime?Timestamp? 数值保存的时间戳?
|
||||||
|
|
||||||
|
好像并没有一个银弹,很多程序员会觉得数值型时间戳是真的好,效率又高还各种兼容,但是很多人又觉得它表现的不够直观。这里插一嘴,《高性能 MySQL 》这本神书的作者就是推荐 Timestamp,原因是数值表示时间不够直观。下面是原文:
|
||||||
|
|
||||||
|
<img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/高性能mysql-不推荐用数值时间戳.jpg" style="zoom:50%;" />
|
||||||
|
|
||||||
|
每种方式都有各自的优势,根据实际场景才是王道。下面再对这三种方式做一个简单的对比,以供大家实际开发中选择正确的存放时间的数据类型:
|
||||||
|
|
||||||
|
<img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/总结-常用日期存储方式.jpg" style="zoom:50%;" />
|
||||||
|
|
||||||
|
如果还有什么问题欢迎给我留言!如果文章有什么问题的话,也劳烦指出,Guide 哥感激不尽!
|
||||||
|
|
||||||
|
后面的文章我会介绍:
|
||||||
|
|
||||||
|
- [ ] Java8 对日期的支持以及为啥不能用 SimpleDateFormat。
|
||||||
|
- [ ] SpringBoot 中如何实际使用(JPA 为例)
|
225
docs/database/数据库索引.md
Normal file
225
docs/database/数据库索引.md
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
## 什么是索引?
|
||||||
|
**索引是一种用于快速查询和检索数据的数据结构。常见的索引结构有: B树, B+树和Hash。**
|
||||||
|
|
||||||
|
索引的作用就相当于目录的作用。打个比方: 我们在查字典的时候,如果没有目录,那我们就只能一页一页的去找我们需要查的那个字,速度很慢。如果有目录了,我们只需要先去目录里查找字的位置,然后直接翻到那一页就行了。
|
||||||
|
|
||||||
|
## 为什么要用索引?索引的优缺点分析
|
||||||
|
|
||||||
|
### 索引的优点
|
||||||
|
**可以大大加快 数据的检索速度(大大减少的检索的数据量), 这也是创建索引的最主要的原因。毕竟大部分系统的读请求总是大于写请求的。 ** 另外,通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
|
||||||
|
|
||||||
|
### 索引的缺点
|
||||||
|
1. **创建索引和维护索引需要耗费许多时间**:当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态的修改,会降低SQL执行效率。
|
||||||
|
2. **占用物理存储空间** :索引需要使用物理文件存储,也会耗费一定空间。
|
||||||
|
|
||||||
|
## B树和B+树区别
|
||||||
|
|
||||||
|
* B树的所有节点既存放 键(key) 也存放 数据(data);而B+树只有叶子节点存放 key 和 data,其他内节点只存放key。
|
||||||
|
* B树的叶子节点都是独立的;B+树的叶子节点有一条引用链指向与它相邻的叶子节点。
|
||||||
|
* B树的检索的过程相当于对范围内的每个节点的关键字做二分查找,可能还没有到达叶子节点,检索就结束了。而B+树的检索效率就很稳定了,任何查找都是从根节点到叶子节点的过程,叶子节点的顺序检索很明显。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Hash索引和 B+树索引优劣分析
|
||||||
|
|
||||||
|
**Hash索引定位快**
|
||||||
|
|
||||||
|
Hash索引指的就是Hash表,最大的优点就是能够在很短的时间内,根据Hash函数定位到数据所在的位置,这是B+树所不能比的。
|
||||||
|
|
||||||
|
**Hash冲突问题**
|
||||||
|
|
||||||
|
知道HashMap或HashTable的同学,相信都知道它们最大的缺点就是Hash冲突了。不过对于数据库来说这还不算最大的缺点。
|
||||||
|
|
||||||
|
**Hash索引不支持顺序和范围查询(Hash索引不支持顺序和范围查询是它最大的缺点。**
|
||||||
|
|
||||||
|
试想一种情况:
|
||||||
|
|
||||||
|
````text
|
||||||
|
SELECT * FROM tb1 WHERE id < 500;
|
||||||
|
````
|
||||||
|
|
||||||
|
B+树是有序的,在这种范围查询中,优势非常大,直接遍历比500小的叶子节点就够了。而Hash索引是根据hash算法来定位的,难不成还要把 1 - 499的数据,每个都进行一次hash计算来定位吗?这就是Hash最大的缺点了。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 索引类型
|
||||||
|
|
||||||
|
### 主键索引(Primary Key)
|
||||||
|
**数据表的主键列使用的就是主键索引。**
|
||||||
|
|
||||||
|
**一张数据表有只能有一个主键,并且主键不能为null,不能重复。**
|
||||||
|
|
||||||
|
**在mysql的InnoDB的表中,当没有显示的指定表的主键时,InnoDB会自动先检查表中是否有唯一索引的字段,如果有,则选择该字段为默认的主键,否则InnoDB将会自动创建一个6Byte的自增主键。**
|
||||||
|
|
||||||
|
### 二级索引(辅助索引)
|
||||||
|
**二级索引又称为辅助索引,是因为二级索引的叶子节点存储的数据是主键。也就是说,通过二级索引,可以定位主键的位置。**
|
||||||
|
|
||||||
|
唯一索引,普通索引,前缀索引等索引属于二级索引。
|
||||||
|
|
||||||
|
**PS:不懂的同学可以暂存疑,慢慢往下看,后面会有答案的,也可以自行搜索。**
|
||||||
|
|
||||||
|
1. **唯一索引(Unique Key)** :唯一索引也是一种约束。**唯一索引的属性列不能出现重复的数据,但是允许数据为NULL,一张表允许创建多个唯一索引。**建立唯一索引的目的大部分时候都是为了该属性列的数据的唯一性,而不是为了查询效率。
|
||||||
|
2. **普通索引(Index)** :**普通索引的唯一作用就是为了快速查询数据,一张表允许创建多个普通索引,并允许数据重复和NULL。**
|
||||||
|
3. **前缀索引(Prefix)** :前缀索引只适用于字符串类型的数据。前缀索引是对文本的前几个字符创建索引,相比普通索引建立的数据更小,
|
||||||
|
因为只取前几个字符。
|
||||||
|
4. **全文索引(Full Text)** :全文索引主要是为了检索大文本数据中的关键字的信息,是目前搜索引擎数据库使用的一种技术。Mysql5.6之前只有MYISAM引擎支持全文索引,5.6之后InnoDB也支持了全文索引。
|
||||||
|
|
||||||
|
二级索引:
|
||||||
|
.png)
|
||||||
|
|
||||||
|
## 聚集索引与非聚集索引
|
||||||
|
|
||||||
|
### 聚集索引
|
||||||
|
**聚集索引即索引结构和数据一起存放的索引。主键索引属于聚集索引。**
|
||||||
|
|
||||||
|
在 Mysql 中,InnoDB引擎的表的 `.ibd`文件就包含了该表的索引和数据,对于 InnoDB 引擎表来说,该表的索引(B+树)的每个非叶子节点存储索引,叶子节点存储索引和索引对应的数据。
|
||||||
|
|
||||||
|
#### 聚集索引的优点
|
||||||
|
聚集索引的查询速度非常的快,因为整个B+树本身就是一颗多叉平衡树,叶子节点也都是有序的,定位到索引的节点,就相当于定位到了数据。
|
||||||
|
|
||||||
|
#### 聚集索引的缺点
|
||||||
|
1. **依赖于有序的数据** :因为B+树是多路平衡树,如果索引的数据不是有序的,那么就需要在插入时排序,如果数据是整型还好,否则类似于字符串或UUID这种又长又难比较的数据,插入或查找的速度肯定比较慢。
|
||||||
|
2. **更新代价大** : 如果对索引列的数据被修改时,那么对应的索引也将会被修改,
|
||||||
|
而且况聚集索引的叶子节点还存放着数据,修改代价肯定是较大的,
|
||||||
|
所以对于主键索引来说,主键一般都是不可被修改的。
|
||||||
|
|
||||||
|
### 非聚集索引
|
||||||
|
|
||||||
|
**非聚集索引即索引结构和数据分开存放的索引。**
|
||||||
|
|
||||||
|
**二级索引属于非聚集索引。**
|
||||||
|
|
||||||
|
>MYISAM引擎的表的.MYI文件包含了表的索引,
|
||||||
|
>该表的索引(B+树)的每个叶子非叶子节点存储索引,
|
||||||
|
>叶子节点存储索引和索引对应数据的指针,指向.MYD文件的数据。
|
||||||
|
>
|
||||||
|
**非聚集索引的叶子节点并不一定存放数据的指针,
|
||||||
|
因为二级索引的叶子节点就存放的是主键,根据主键再回表查数据。**
|
||||||
|
|
||||||
|
#### 非聚集索引的优点
|
||||||
|
**更新代价比聚集索引要小** 。非聚集索引的更新代价就没有聚集索引那么大了,非聚集索引的叶子节点是不存放数据的
|
||||||
|
|
||||||
|
#### 非聚集索引的缺点
|
||||||
|
1. 跟聚集索引一样,非聚集索引也依赖于有序的数据
|
||||||
|
2. **可能会二次查询(回表)** :这应该是非聚集索引最大的缺点了。 当查到索引对应的指针或主键后,可能还需要根据指针或主键再到数据文件或表中查询。
|
||||||
|
|
||||||
|
这是Mysql的表的文件截图:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
聚集索引和非聚集索引:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 非聚集索引一定回表查询吗(覆盖索引)?
|
||||||
|
**非聚集索引不一定回表查询。**
|
||||||
|
|
||||||
|
>试想一种情况,用户准备使用SQL查询用户名,而用户名字段正好建立了索引。
|
||||||
|
|
||||||
|
````text
|
||||||
|
SELECT name FROM table WHERE username='guang19';
|
||||||
|
````
|
||||||
|
|
||||||
|
>那么这个索引的key本身就是name,查到对应的name直接返回就行了,无需回表查询。
|
||||||
|
|
||||||
|
**即使是MYISAM也是这样,虽然MYISAM的主键索引确实需要回表,
|
||||||
|
因为它的主键索引的叶子节点存放的是指针。但是如果SQL查的就是主键呢?**
|
||||||
|
|
||||||
|
```text
|
||||||
|
SELECT id FROM table WHERE id=1;
|
||||||
|
```
|
||||||
|
|
||||||
|
主键索引本身的key就是主键,查到返回就行了。这种情况就称之为覆盖索引了。
|
||||||
|
|
||||||
|
## 覆盖索引
|
||||||
|
|
||||||
|
如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为“覆盖索引”。我们知道在InnoDB存储引擎中,如果不是主键索引,叶子节点存储的是主键+列值。最终还是要“回表”,也就是要通过主键再查找一次。这样就会比较慢覆盖索引就是把要查询出的列和索引是对应的,不做回表操作!
|
||||||
|
|
||||||
|
**覆盖索引即需要查询的字段正好是索引的字段,那么直接根据该索引,就可以查到数据了,
|
||||||
|
而无需回表查询。**
|
||||||
|
|
||||||
|
>如主键索引,如果一条SQL需要查询主键,那么正好根据主键索引就可以查到主键。
|
||||||
|
>
|
||||||
|
>再如普通索引,如果一条SQL需要查询name,name字段正好有索引,
|
||||||
|
>那么直接根据这个索引就可以查到数据,也无需回表。
|
||||||
|
|
||||||
|
覆盖索引:
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 索引创建原则
|
||||||
|
|
||||||
|
### 单列索引
|
||||||
|
单列索引即由一列属性组成的索引。
|
||||||
|
|
||||||
|
### 联合索引(多列索引)
|
||||||
|
联合索引即由多列属性组成索引。
|
||||||
|
|
||||||
|
### 最左前缀原则
|
||||||
|
|
||||||
|
假设创建的联合索引由三个字段组成:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ALTER TABLE table ADD INDEX index_name (num,name,age)
|
||||||
|
```
|
||||||
|
|
||||||
|
那么当查询的条件有为:num / (num AND name) / (num AND name AND age)时,索引才生效。所以在创建联合索引时,尽量把查询最频繁的那个字段作为最左(第一个)字段。查询的时候也尽量以这个字段为第一条件。
|
||||||
|
|
||||||
|
> 但可能由于版本原因(我的mysql版本为8.0.x),我创建的联合索引,相当于在联合索引的每个字段上都创建了相同的索引:
|
||||||
|
|
||||||
|
.png)
|
||||||
|
|
||||||
|
无论是否符合最左前缀原则,每个字段的索引都生效:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 索引创建注意点
|
||||||
|
|
||||||
|
### 最左前缀原则
|
||||||
|
|
||||||
|
虽然我目前的Mysql版本较高,好像不遵守最左前缀原则,索引也会生效。
|
||||||
|
但是我们仍应遵守最左前缀原则,以免版本更迭带来的麻烦。
|
||||||
|
|
||||||
|
### 选择合适的字段
|
||||||
|
|
||||||
|
#### 1.不为NULL的字段
|
||||||
|
|
||||||
|
索引字段的数据应该尽量不为NULL,因为对于数据为NULL的字段,数据库较难优化。如果字段频繁被查询,但又避免不了为NULL,建议使用0,1,true,false这样语义较为清晰的短值或短字符作为替代。
|
||||||
|
|
||||||
|
#### 2.被频繁查询的字段
|
||||||
|
|
||||||
|
我们创建索引的字段应该是查询操作非常频繁的字段。
|
||||||
|
|
||||||
|
#### 3.被作为条件查询的字段
|
||||||
|
|
||||||
|
被作为WHERE条件查询的字段,应该被考虑建立索引。
|
||||||
|
|
||||||
|
#### 4.被经常频繁用于连接的字段
|
||||||
|
|
||||||
|
经常用于连接的字段可能是一些外键列,对于外键列并不一定要建立外键,只是说该列涉及到表与表的关系。对于频繁被连接查询的字段,可以考虑建立索引,提高多表连接查询的效率。
|
||||||
|
|
||||||
|
### 不合适创建索引的字段
|
||||||
|
|
||||||
|
#### 1.被频繁更新的字段应该慎重建立索引
|
||||||
|
|
||||||
|
虽然索引能带来查询上的效率,但是维护索引的成本也是不小的。
|
||||||
|
如果一个字段不被经常查询,反而被经常修改,那么就更不应该在这种字段上建立索引了。
|
||||||
|
|
||||||
|
#### 2.不被经常查询的字段没有必要建立索引
|
||||||
|
|
||||||
|
#### 3.尽可能的考虑建立联合索引而不是单列索引
|
||||||
|
|
||||||
|
因为索引是需要占用磁盘空间的,可以简单理解为每个索引都对应着一颗B+树。如果一个表的字段过多,索引过多,那么当这个表的数据达到一个体量后,索引占用的空间也是很多的,且修改索引时,耗费的时间也是较多的。如果是联合索引,多个字段在一个索引上,那么将会节约很大磁盘空间,且修改数据的操作效率也会提升。
|
||||||
|
|
||||||
|
#### 4.注意避免冗余索引
|
||||||
|
|
||||||
|
冗余索引指的是索引的功能相同,能够命中 就肯定能命中 ,那么 就是冗余索引如(name,city )和(name )这两个索引就是冗余索引,能够命中后者的查询肯定是能够命中前者的 在大多数情况下,都应该尽量扩展已有的索引而不是创建新索引。
|
||||||
|
|
||||||
|
#### 5.考虑在字符串类型的字段上使用前缀索引代替普通索引
|
||||||
|
|
||||||
|
前缀索引仅限于字符串类型,较普通索引会占用更小的空间,所以可以考虑使用前缀索引带替普通索引。
|
||||||
|
|
||||||
|
### 使用索引一定能提高查询性能吗?
|
||||||
|
|
||||||
|
大多数情况下,索引查询都是比全表扫描要快的。但是如果数据库的数据量不大,那么使用索引也不一定能够带来很大提升。
|
21
docs/database/数据库连接池.md
Normal file
21
docs/database/数据库连接池.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
- 公众号和Github待发文章:[数据库:数据库连接池原理详解与自定义连接池实现](https://www.fangzhipeng.com/javainterview/2019/07/15/mysql-connector-pool.html)
|
||||||
|
- [基于JDBC的数据库连接池技术研究与应用](http://blog.itpub.net/9403012/viewspace-111794/)
|
||||||
|
- [数据库连接池技术详解](https://juejin.im/post/5b7944c6e51d4538c86cf195)
|
||||||
|
|
||||||
|
数据库连接本质就是一个 socket 的连接。数据库服务端还要维护一些缓存和用户权限信息之类的 所以占用了一些内存
|
||||||
|
|
||||||
|
连接池是维护的数据库连接的缓存,以便将来需要对数据库的请求时可以重用这些连接。为每个用户打开和维护数据库连接,尤其是对动态数据库驱动的网站应用程序的请求,既昂贵又浪费资源。**在连接池中,创建连接后,将其放置在池中,并再次使用它,因此不必建立新的连接。如果使用了所有连接,则会建立一个新连接并将其添加到池中。**连接池还减少了用户必须等待建立与数据库的连接的时间。
|
||||||
|
|
||||||
|
操作过数据库的朋友应该都知道数据库连接池这个概念,它几乎每天都在和我们打交道,但是你真的了解 **数据库连接池** 吗?
|
||||||
|
|
||||||
|
### 没有数据库连接池之前
|
||||||
|
|
||||||
|
我相信你一定听过这样一句话:**Java语言中,JDBC(Java DataBase Connection)是应用程序与数据库沟通的桥梁**。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
41
docs/database/阿里巴巴开发手册数据库部分的一些最佳实践.md
Normal file
41
docs/database/阿里巴巴开发手册数据库部分的一些最佳实践.md
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# 阿里巴巴Java开发手册数据库部分的一些最佳实践总结
|
||||||
|
|
||||||
|
## 模糊查询
|
||||||
|
|
||||||
|
对于模糊查询阿里巴巴开发手册这样说到:
|
||||||
|
|
||||||
|
> 【强制】页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。
|
||||||
|
>
|
||||||
|
> 说明:索引文件具有 B-Tree 的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。
|
||||||
|
|
||||||
|
## 外键和级联
|
||||||
|
|
||||||
|
对于外键和级联,阿里巴巴开发手册这样说到:
|
||||||
|
|
||||||
|
>【强制】不得使用外键与级联,一切外键概念必须在应用层解决。
|
||||||
|
>
|
||||||
|
>说明:以学生和成绩的关系为例,学生表中的 student_id 是主键,那么成绩表中的 student_id 则为外键。如果更新学生表中的 student_id,同时触发成绩表中的 student_id 更新,即为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风 险;外键影响数据库的插入速度
|
||||||
|
|
||||||
|
为什么不要用外键呢?大部分人可能会这样回答:
|
||||||
|
|
||||||
|
> 1. **增加了复杂性:** a.每次做DELETE 或者UPDATE都必须考虑外键约束,会导致开发的时候很痛苦,测试数据极为不方便;b.外键的主从关系是定的,假如那天需求有变化,数据库中的这个字段根本不需要和其他表有关联的话就会增加很多麻烦。
|
||||||
|
> 2. **增加了额外工作**: 数据库需要增加维护外键的工作,比如当我们做一些涉及外键字段的增,删,更新操作之后,需要触发相关操作去检查,保证数据的的一致性和正确性,这样会不得不消耗资源;(个人觉得这个不是不用外键的原因,因为即使你不使用外键,你在应用层面也还是要保证的。所以,我觉得这个影响可以忽略不计。)
|
||||||
|
> 3. 外键还会因为需要请求对其他表内部加锁而容易出现死锁情况;
|
||||||
|
> 4. **对分不分表不友好** :因为分库分表下外键是无法生效的。
|
||||||
|
> 5. ......
|
||||||
|
|
||||||
|
我个人觉得上面这种回答不是特别的全面,只是说了外键存在的一个常见的问题。实际上,我们知道外键也是有很多好处的,比如:
|
||||||
|
|
||||||
|
1. 保证了数据库数据的一致性和完整性;
|
||||||
|
2. 级联操作方便,减轻了程序代码量;
|
||||||
|
3. ......
|
||||||
|
|
||||||
|
所以说,不要一股脑的就抛弃了外键这个概念,既然它存在就有它存在的道理,如果系统不涉及分不分表,并发量不是很高的情况还是可以考虑使用外键的。
|
||||||
|
|
||||||
|
我个人是不太喜欢外键约束,比较喜欢在应用层去进行相关操作。
|
||||||
|
|
||||||
|
## 关于@Transactional注解
|
||||||
|
|
||||||
|
对于`@Transactional`事务注解,阿里巴巴开发手册这样说到:
|
||||||
|
|
||||||
|
>【参考】@Transactional事务不要滥用。事务会影响数据库的QPS,另外使用事务的地方需要考虑各方面的回滚方案,包括缓存回滚、搜索引擎回滚、消息补偿、统计修正等。
|
@ -0,0 +1,294 @@
|
|||||||
|
作者: rhwayfun,原文地址:https://mp.weixin.qq.com/s/msYty4vjjC0PvrwasRH5Bw ,JavaGuide 已经获得作者授权并对原文进行了重新排版。
|
||||||
|
<!-- TOC -->
|
||||||
|
|
||||||
|
- [写在2019年后的蚂蚁、头条、拼多多的面试总结](#写在2019年后的蚂蚁头条拼多多的面试总结)
|
||||||
|
- [准备过程](#准备过程)
|
||||||
|
- [蚂蚁金服](#蚂蚁金服)
|
||||||
|
- [一面](#一面)
|
||||||
|
- [二面](#二面)
|
||||||
|
- [三面](#三面)
|
||||||
|
- [四面](#四面)
|
||||||
|
- [五面](#五面)
|
||||||
|
- [小结](#小结)
|
||||||
|
- [拼多多](#拼多多)
|
||||||
|
- [面试前](#面试前)
|
||||||
|
- [一面](#一面-1)
|
||||||
|
- [二面](#二面-1)
|
||||||
|
- [三面](#三面-1)
|
||||||
|
- [小结](#小结-1)
|
||||||
|
- [字节跳动](#字节跳动)
|
||||||
|
- [面试前](#面试前-1)
|
||||||
|
- [一面](#一面-2)
|
||||||
|
- [二面](#二面-2)
|
||||||
|
- [小结](#小结-2)
|
||||||
|
- [总结](#总结)
|
||||||
|
|
||||||
|
<!-- /TOC -->
|
||||||
|
|
||||||
|
# 2019年蚂蚁金服、头条、拼多多的面试总结
|
||||||
|
|
||||||
|
文章有点长,请耐心看完,绝对有收获!不想听我BB直接进入面试分享:
|
||||||
|
|
||||||
|
- 准备过程
|
||||||
|
- 蚂蚁金服面试分享
|
||||||
|
- 拼多多面试分享
|
||||||
|
- 字节跳动面试分享
|
||||||
|
- 总结
|
||||||
|
|
||||||
|
说起来开始进行面试是年前倒数第二周,上午9点,我还在去公司的公交上,突然收到蚂蚁的面试电话,其实算不上真正的面试。面试官只是和我聊了下他们在做的事情(主要是做双十一这里大促的稳定性保障,偏中间件吧),说的很详细,然后和我沟通了下是否有兴趣,我表示有兴趣,后面就收到正式面试的通知,最后没选择去蚂蚁表示抱歉。
|
||||||
|
|
||||||
|
当时我自己也准备出去看看机会,顺便看看自己的实力。当时我其实挺纠结的,一方面现在部门也正需要我,还是可以有一番作为的,另一方面觉得近一年来进步缓慢,没有以前飞速进步的成就感了,而且业务和技术偏于稳定,加上自己也属于那种比较懒散的人,骨子里还是希望能够突破现状,持续在技术上有所精进。
|
||||||
|
|
||||||
|
在开始正式的总结之前,还是希望各位同仁能否听我继续发泄一会,抱拳!
|
||||||
|
|
||||||
|
我翻开自己2018年初立的flag,觉得甚是惭愧。其中就有一条是保持一周写一篇博客,奈何中间因为各种原因没能坚持下去。细细想来,主要是自己没能真正静下来心认真投入到技术的研究和学习,那么为什么会这样?说白了还是因为没有确定目标或者目标不明确,没有目标或者目标不明确都可能导致行动的失败。
|
||||||
|
|
||||||
|
那么问题来了,目标是啥?就我而言,短期目标是深入研究某一项技术,比如最近在研究mysql,那么深入研究一定要动手实践并且有所产出,这就够了么?还需要我们能够举一反三,结合实际开发场景想一想日常开发要注意什么,这中间有没有什么坑?可以看出,要进步真的不是一件简单的事,这种反人类的行为需要我们克服自我的弱点,逐渐形成习惯。真正牛逼的人,从不觉得认真学习是一件多么难的事,因为这已经形成了他的习惯,就喝早上起床刷牙洗脸那么自然简单。
|
||||||
|
|
||||||
|
扯了那么多,开始进入正题,先后进行了蚂蚁、拼多多和字节跳动的面试。
|
||||||
|
|
||||||
|
## 准备过程
|
||||||
|
|
||||||
|
先说说我自己的情况,我2016先在蚂蚁实习了将近三个月,然后去了我现在的老东家,2.5年工作经验,可以说毕业后就一直老老实实在老东家打怪升级,虽说有蚂蚁的实习经历,但是因为时间太短,还是有点虚的。所以面试官看到我简历第一个问题绝对是这样的。
|
||||||
|
|
||||||
|
“哇,你在蚂蚁待过,不错啊”,面试官笑嘻嘻地问到。“是的,还好”,我说。“为啥才三个月?”,面试官脸色一沉问到。“哗啦啦解释一通。。。”,我解释道。“哦,原来如此,那我们开始面试吧”,面试官一本正经说到。
|
||||||
|
|
||||||
|
尼玛,早知道不写蚂蚁的实习经历了,后面仔细一想,当初写上蚂蚁不就给简历加点料嘛。
|
||||||
|
|
||||||
|
言归正传,准备过程其实很早开始了(当然这不是说我工作时老想着跳槽,因为我明白现在的老东家并不是终点,我还需要不断提升),具体可追溯到从蚂蚁离职的时候,当时出来也面了很多公司,没啥大公司,面了大概5家公司,都拿到offer了。
|
||||||
|
|
||||||
|
工作之余常常会去额外研究自己感兴趣的技术以及工作用到的技术,力求把原理搞明白,并且会自己实践一把。此外,买了N多书,基本有时间就会去看,补补基础,什么操作系统、数据结构与算法、MySQL、JDK之类的源码,基本都好好温习了(文末会列一下自己看过的书和一些好的资料)。**我深知基础就像“木桶效应”的短板,决定了能装多少水。**
|
||||||
|
|
||||||
|
此外,在正式决定看机会之前,我给自己列了一个提纲,主要包括Java要掌握的核心要点,有不懂的就查资料搞懂。我给自己定位还是Java工程师,所以Java体系是一定要做到心中有数的,很多东西没有常年的积累面试的时候很容易露馅,学习要对得起自己,不要骗人。
|
||||||
|
|
||||||
|
剩下的就是找平台和内推了,除了蚂蚁,头条和拼多多都是找人内推的,感谢蚂蚁面试官对我的欣赏,以后说不定会去蚂蚁咯😄。
|
||||||
|
|
||||||
|
平台:脉脉、GitHub、v2
|
||||||
|
|
||||||
|
## 蚂蚁金服
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- 一面
|
||||||
|
- 二面
|
||||||
|
- 三面
|
||||||
|
- 四面
|
||||||
|
- 五面
|
||||||
|
- 小结
|
||||||
|
|
||||||
|
### 一面
|
||||||
|
|
||||||
|
一面就做了一道算法题,要求两小时内完成,给了长度为N的有重复元素的数组,要求输出第10大的数。典型的TopK问题,快排算法搞定。
|
||||||
|
|
||||||
|
算法题要注意的是合法性校验、边界条件以及异常的处理。另外,如果要写测试用例,一定要保证测试覆盖场景尽可能全。加上平时刷刷算法题,这种考核应该没问题的。
|
||||||
|
|
||||||
|
### 二面
|
||||||
|
|
||||||
|
- 自我介绍下呗
|
||||||
|
- 开源项目贡献过代码么?(Dubbo提过一个打印accesslog的bug算么)
|
||||||
|
- 目前在部门做什么,业务简单介绍下,内部有哪些系统,作用和交互过程说下
|
||||||
|
- Dubbo踩过哪些坑,分别是怎么解决的?(说了异常处理时业务异常捕获的问题,自定义了一个异常拦截器)
|
||||||
|
- 开始进入正题,说下你对线程安全的理解(多线程访问同一个对象,如果不需要考虑额外的同步,调用对象的行为就可以获得正确的结果就是线程安全)
|
||||||
|
- 事务有哪些特性?(ACID)
|
||||||
|
- 怎么理解原子性?(同一个事务下,多个操作要么成功要么失败,不存在部分成功或者部分失败的情况)
|
||||||
|
- 乐观锁和悲观锁的区别?(悲观锁假定会发生冲突,访问的时候都要先获得锁,保证同一个时刻只有线程获得锁,读读也会阻塞;乐观锁假设不会发生冲突,只有在提交操作的时候检查是否有冲突)这两种锁在Java和MySQL分别是怎么实现的?(Java乐观锁通过CAS实现,悲观锁通过synchronize实现。mysql乐观锁通过MVCC,也就是版本实现,悲观锁可以通过select... for update加上排它锁)
|
||||||
|
- HashMap为什么不是线程安全的?(多线程操作无并发控制,顺便说了在扩容的时候多线程访问时会造成死锁,会形成一个环,不过扩容时多线程操作形成环的问题再JDK1.8已经解决,但多线程下使用HashMap还会有一些其他问题比如数据丢失,所以多线程下不应该使用HashMap,而应该使用ConcurrentHashMap)怎么让HashMap变得线程安全?(Collections的synchronize方法包装一个线程安全的Map,或者直接用ConcurrentHashMap)两者的区别是什么?(前者直接在put和get方法加了synchronize同步,后者采用了分段锁以及CAS支持更高的并发)
|
||||||
|
- jdk1.8对ConcurrentHashMap做了哪些优化?(插入的时候如果数组元素使用了红黑树,取消了分段锁设计,synchronize替代了Lock锁)为什么这样优化?(避免冲突严重时链表多长,提高查询效率,时间复杂度从O(N)提高到O(logN))
|
||||||
|
- redis主从机制了解么?怎么实现的?
|
||||||
|
- 有过GC调优的经历么?(有点虚,答得不是很好)
|
||||||
|
- 有什么想问的么?
|
||||||
|
|
||||||
|
### 三面
|
||||||
|
|
||||||
|
- 简单自我介绍下
|
||||||
|
- 监控系统怎么做的,分为哪些模块,模块之间怎么交互的?用的什么数据库?(MySQL)使用什么存储引擎,为什么使用InnnoDB?(支持事务、聚簇索引、MVCC)
|
||||||
|
- 订单表有做拆分么,怎么拆的?(垂直拆分和水平拆分)
|
||||||
|
- 水平拆分后查询过程描述下
|
||||||
|
- 如果落到某个分片的数据很大怎么办?(按照某种规则,比如哈希取模、range,将单张表拆分为多张表)
|
||||||
|
- 哈希取模会有什么问题么?(有的,数据分布不均,扩容缩容相对复杂 )
|
||||||
|
- 分库分表后怎么解决读写压力?(一主多从、多主多从)
|
||||||
|
- 拆分后主键怎么保证惟一?(UUID、Snowflake算法)
|
||||||
|
- Snowflake生成的ID是全局递增唯一么?(不是,只是全局唯一,单机递增)
|
||||||
|
- 怎么实现全局递增的唯一ID?(讲了TDDL的一次取一批ID,然后再本地慢慢分配的做法)
|
||||||
|
- Mysql的索引结构说下(说了B+树,B+树可以对叶子结点顺序查找,因为叶子结点存放了数据结点且有序)
|
||||||
|
- 主键索引和普通索引的区别(主键索引的叶子结点存放了整行记录,普通索引的叶子结点存放了主键ID,查询的时候需要做一次回表查询)一定要回表查询么?(不一定,当查询的字段刚好是索引的字段或者索引的一部分,就可以不用回表,这也是索引覆盖的原理)
|
||||||
|
- 你们系统目前的瓶颈在哪里?
|
||||||
|
- 你打算怎么优化?简要说下你的优化思路
|
||||||
|
- 有什么想问我么?
|
||||||
|
|
||||||
|
### 四面
|
||||||
|
|
||||||
|
- 介绍下自己
|
||||||
|
- 为什么要做逆向?
|
||||||
|
- 怎么理解微服务?
|
||||||
|
- 服务治理怎么实现的?(说了限流、压测、监控等模块的实现)
|
||||||
|
- 这个不是中间件做的事么,为什么你们部门做?(当时没有单独的中间件团队,微服务刚搞不久,需要进行监控和性能优化)
|
||||||
|
- 说说Spring的生命周期吧
|
||||||
|
- 说说GC的过程(说了young gc和full gc的触发条件和回收过程以及对象创建的过程)
|
||||||
|
- CMS GC有什么问题?(并发清除算法,浮动垃圾,短暂停顿)
|
||||||
|
- 怎么避免产生浮动垃圾?(记得有个VM参数设置可以让扫描新生代之前进行一次young gc,但是因为gc是虚拟机自动调度的,所以不保证一定执行。但是还有参数可以让虚拟机强制执行一次young gc)
|
||||||
|
- 强制young gc会有什么问题?(STW停顿时间变长)
|
||||||
|
- 知道G1么?(了解一点 )
|
||||||
|
- 回收过程是怎么样的?(young gc、并发阶段、混合阶段、full gc,说了Remember Set)
|
||||||
|
- 你提到的Remember Set底层是怎么实现的?
|
||||||
|
- 有什么想问的么?
|
||||||
|
|
||||||
|
### 五面
|
||||||
|
|
||||||
|
五面是HRBP面的,和我提前预约了时间,主要聊了之前在蚂蚁的实习经历、部门在做的事情、职业发展、福利待遇等。阿里面试官确实是具有一票否决权的,很看重你的价值观是否match,一般都比较喜欢皮实的候选人。HR面一定要诚实,不要说谎,只要你说谎HR都会去证实,直接cut了。
|
||||||
|
|
||||||
|
- 之前蚂蚁实习三个月怎么不留下来?
|
||||||
|
- 实习的时候主管是谁?
|
||||||
|
- 实习做了哪些事情?(尼玛这种也问?)
|
||||||
|
- 你对技术怎么看?平时使用什么技术栈?(阿里HR真的是既当爹又当妈,😂)
|
||||||
|
- 最近有在研究什么东西么
|
||||||
|
- 你对SRE怎么看
|
||||||
|
- 对待遇有什么预期么
|
||||||
|
|
||||||
|
最后HR还对我说目前稳定性保障部挺缺人的,希望我尽快回复。
|
||||||
|
|
||||||
|
### 小结
|
||||||
|
|
||||||
|
蚂蚁面试比较重视基础,所以Java那些基本功一定要扎实。蚂蚁的工作环境还是挺赞的,因为我面的是稳定性保障部门,还有许多单独的小组,什么三年1班,很有青春的感觉。面试官基本水平都比较高,基本都P7以上,除了基础还问了不少架构设计方面的问题,收获还是挺大的。
|
||||||
|
|
||||||
|
## 拼多多
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- 面试前
|
||||||
|
- 一面
|
||||||
|
- 二面
|
||||||
|
- 三面
|
||||||
|
- 小结
|
||||||
|
|
||||||
|
### 面试前
|
||||||
|
|
||||||
|
面完蚂蚁后,早就听闻拼多多这个独角兽,决定也去面一把。首先我在脉脉找了一个拼多多的HR,加了微信聊了下,发了简历便开始我的拼多多面试之旅。这里要非常感谢拼多多HR小姐姐,从面试内推到offer确认一直都在帮我,人真的很nice。
|
||||||
|
|
||||||
|
### 一面
|
||||||
|
|
||||||
|
- 为啥蚂蚁只待了三个月?没转正?(转正了,解释了一通。。。)
|
||||||
|
- Java中的HashMap、TreeMap解释下?(TreeMap红黑树,有序,HashMap无序,数组+链表)
|
||||||
|
- TreeMap查询写入的时间复杂度多少?(O(logN))
|
||||||
|
- HashMap多线程有什么问题?(线程安全,死锁)怎么解决?( jdk1.8用了synchronize + CAS,扩容的时候通过CAS检查是否有修改,是则重试)重试会有什么问题么?(CAS(Compare And Swap)是比较和交换,不会导致线程阻塞,但是因为重试是通过自旋实现的,所以仍然会占用CPU时间,还有ABA的问题)怎么解决?(超时,限定自旋的次数,ABA可以通过原理变量AtomicStampedReference解决,原理利用版本号进行比较)超过重试次数如果仍然失败怎么办?(synchronize互斥锁)
|
||||||
|
- CAS和synchronize有什么区别?都用synchronize不行么?(CAS是乐观锁,不需要阻塞,硬件级别实现的原子性;synchronize会阻塞,JVM级别实现的原子性。使用场景不同,线程冲突严重时CAS会造成CPU压力过大,导致吞吐量下降,synchronize的原理是先自旋然后阻塞,线程冲突严重仍然有较高的吞吐量,因为线程都被阻塞了,不会占用CPU
|
||||||
|
)
|
||||||
|
- 如果要保证线程安全怎么办?(ConcurrentHashMap)
|
||||||
|
- ConcurrentHashMap怎么实现线程安全的?(分段锁)
|
||||||
|
- get需要加锁么,为什么?(不用,volatile关键字)
|
||||||
|
- volatile的作用是什么?(保证内存可见性)
|
||||||
|
- 底层怎么实现的?(说了主内存和工作内存,读写内存屏障,happen-before,并在纸上画了线程交互图)
|
||||||
|
- 在多核CPU下,可见性怎么保证?(思考了一会,总线嗅探技术)
|
||||||
|
- 聊项目,系统之间是怎么交互的?
|
||||||
|
- 系统并发多少,怎么优化?
|
||||||
|
- 给我一张纸,画了一个九方格,都填了数字,给一个M*N矩阵,从1开始逆时针打印这M*N个数,要求时间复杂度尽可能低(内心OS:之前貌似碰到过这题,最优解是怎么实现来着)思考中。。。
|
||||||
|
- 可以先说下你的思路(想起来了,说了什么时候要变换方向的条件,向右、向下、向左、向上,依此循环)
|
||||||
|
- 有什么想问我的?
|
||||||
|
|
||||||
|
### 二面
|
||||||
|
|
||||||
|
- 自我介绍下
|
||||||
|
- 手上还有其他offer么?(拿了蚂蚁的offer)
|
||||||
|
- 部门组织结构是怎样的?(这轮不是技术面么,不过还是老老实实说了)
|
||||||
|
- 系统有哪些模块,每个模块用了哪些技术,数据怎么流转的?(面试官有点秃顶,一看级别就很高)给了我一张纸,我在上面简单画了下系统之间的流转情况
|
||||||
|
- 链路追踪的信息是怎么传递的?(RpcContext的attachment,说了Span的结构:parentSpanId + curSpanId)
|
||||||
|
- SpanId怎么保证唯一性?(UUID,说了下内部的定制改动)
|
||||||
|
- RpcContext是在什么维度传递的?(线程)
|
||||||
|
- Dubbo的远程调用怎么实现的?(讲了读取配置、拼装url、创建Invoker、服务导出、服务注册以及消费者通过动态代理、filter、获取Invoker列表、负载均衡等过程(哗啦啦讲了10多分钟),我可以喝口水么)
|
||||||
|
- Spring的单例是怎么实现的?(单例注册表)
|
||||||
|
- 为什么要单独实现一个服务治理框架?(说了下内部刚搞微服务不久,主要对服务进行一些监控和性能优化)
|
||||||
|
- 谁主导的?内部还在使用么?
|
||||||
|
- 逆向有想过怎么做成通用么?
|
||||||
|
- 有什么想问的么?
|
||||||
|
|
||||||
|
### 三面
|
||||||
|
|
||||||
|
二面老大面完后就直接HR面了,主要问了些职业发展、是否有其他offer、以及入职意向等问题,顺便说了下公司的福利待遇等,都比较常规啦。不过要说的是手上有其他offer或者大厂经历会有一定加分。
|
||||||
|
|
||||||
|
### 小结
|
||||||
|
|
||||||
|
拼多多的面试流程就简单许多,毕竟是一个成立三年多的公司。面试难度中规中矩,只要基础扎实应该不是问题。但不得不说工作强度很大,开始面试前HR就提前和我确认能否接受这样强度的工作,想来的老铁还是要做好准备
|
||||||
|
|
||||||
|
## 字节跳动
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- 面试前
|
||||||
|
- 一面
|
||||||
|
- 二面
|
||||||
|
- 小结
|
||||||
|
|
||||||
|
### 面试前
|
||||||
|
|
||||||
|
头条的面试是三家里最专业的,每次面试前有专门的HR和你约时间,确定OK后再进行面试。每次都是通过视频面试,因为都是之前都是电话面或现场面,所以视频面试还是有点不自然。也有人觉得视频面试体验很赞,当然萝卜青菜各有所爱。最坑的二面的时候对方面试官的网络老是掉线,最后很冤枉的挂了(当然有一些点答得不好也是原因之一)。所以还是有点遗憾的。
|
||||||
|
|
||||||
|
### 一面
|
||||||
|
|
||||||
|
- 先自我介绍下
|
||||||
|
- 聊项目,逆向系统是什么意思
|
||||||
|
- 聊项目,逆向系统用了哪些技术
|
||||||
|
- 线程池的线程数怎么确定?
|
||||||
|
- 如果是IO操作为主怎么确定?
|
||||||
|
- 如果计算型操作又怎么确定?
|
||||||
|
- Redis熟悉么,了解哪些数据结构?(说了zset) zset底层怎么实现的?(跳表)
|
||||||
|
- 跳表的查询过程是怎么样的,查询和插入的时间复杂度?(说了先从第一层查找,不满足就下沉到第二层找,因为每一层都是有序的,写入和插入的时间复杂度都是O(logN))
|
||||||
|
- 红黑树了解么,时间复杂度?(说了是N叉平衡树,O(logN))
|
||||||
|
- 既然两个数据结构时间复杂度都是O(logN),zset为什么不用红黑树(跳表实现简单,踩坑成本低,红黑树每次插入都要通过旋转以维持平衡,实现复杂)
|
||||||
|
- 点了点头,说下Dubbo的原理?(说了服务注册与发布以及消费者调用的过程)踩过什么坑没有?(说了dubbo异常处理的和打印accesslog的问题)
|
||||||
|
- CAS了解么?(说了CAS的实现)还了解其他同步机制么?(说了synchronize以及两者的区别,一个乐观锁,一个悲观锁)
|
||||||
|
- 那我们做一道题吧,数组A,2*n个元素,n个奇数、n个偶数,设计一个算法,使得数组奇数下标位置放置的都是奇数,偶数下标位置放置的都是偶数
|
||||||
|
- 先说下你的思路(从0下标开始遍历,如果是奇数下标判断该元素是否奇数,是则跳过,否则从该位置寻找下一个奇数)
|
||||||
|
- 下一个奇数?怎么找?(有点懵逼,思考中。。)
|
||||||
|
- 有思路么?(仍然是先遍历一次数组,并对下标进行判断,如果下标属性和该位置元素不匹配从当前下标的下一个遍历数组元素,然后替换)
|
||||||
|
- 你这样时间复杂度有点高,如果要求O(N)要怎么做(思考一会,答道“定义两个指针,分别从下标0和1开始遍历,遇见奇数位是是偶数和偶数位是奇数就停下,交换内容”)
|
||||||
|
- 时间差不多了,先到这吧。你有什么想问我的?
|
||||||
|
|
||||||
|
### 二面
|
||||||
|
|
||||||
|
- 面试官和蔼很多,你先介绍下自己吧
|
||||||
|
- 你对服务治理怎么理解的?
|
||||||
|
- 项目中的限流怎么实现的?(Guava ratelimiter,令牌桶算法)
|
||||||
|
- 具体怎么实现的?(要点是固定速率且令牌数有限)
|
||||||
|
- 如果突然很多线程同时请求令牌,有什么问题?(导致很多请求积压,线程阻塞)
|
||||||
|
- 怎么解决呢?(可以把积压的请求放到消息队列,然后异步处理)
|
||||||
|
- 如果不用消息队列怎么解决?(说了RateLimiter预消费的策略)
|
||||||
|
- 分布式追踪的上下文是怎么存储和传递的?(ThreadLocal + spanId,当前节点的spanId作为下个节点的父spanId)
|
||||||
|
- Dubbo的RpcContext是怎么传递的?(ThreadLocal)主线程的ThreadLocal怎么传递到线程池?(说了先在主线程通过ThreadLocal的get方法拿到上下文信息,在线程池创建新的ThreadLocal并把之前获取的上下文信息设置到ThreadLocal中。这里要注意的线程池创建的ThreadLocal要在finally中手动remove,不然会有内存泄漏的问题)
|
||||||
|
- 你说的内存泄漏具体是怎么产生的?(说了ThreadLocal的结构,主要分两种场景:主线程仍然对ThreadLocal有引用和主线程不存在对ThreadLocal的引用。第一种场景因为主线程仍然在运行,所以还是有对ThreadLocal的引用,那么ThreadLocal变量的引用和value是不会被回收的。第二种场景虽然主线程不存在对ThreadLocal的引用,且该引用是弱引用,所以会在gc的时候被回收,但是对用的value不是弱引用,不会被内存回收,仍然会造成内存泄漏)
|
||||||
|
- 线程池的线程是不是必须手动remove才可以回收value?(是的,因为线程池的核心线程是一直存在的,如果不清理,那么核心线程的threadLocals变量会一直持有ThreadLocal变量)
|
||||||
|
- 那你说的内存泄漏是指主线程还是线程池?(主线程 )
|
||||||
|
- 可是主线程不是都退出了,引用的对象不应该会主动回收么?(面试官和内存泄漏杠上了),沉默了一会。。。
|
||||||
|
- 那你说下SpringMVC不同用户登录的信息怎么保证线程安全的?(刚才解释的有点懵逼,一下没反应过来,居然回答成锁了。大脑有点晕了,此时已经一个小时过去了,感觉情况不妙。。。)
|
||||||
|
- 这个直接用ThreadLocal不就可以么,你见过SpringMVC有锁实现的代码么?(有点晕菜。。。)
|
||||||
|
- 我们聊聊mysql吧,说下索引结构(说了B+树)
|
||||||
|
- 为什么使用B+树?( 说了查询效率高,O(logN),可以充分利用磁盘预读的特性,多叉树,深度小,叶子结点有序且存储数据)
|
||||||
|
- 什么是索引覆盖?(忘记了。。。 )
|
||||||
|
- Java为什么要设计双亲委派模型?
|
||||||
|
- 什么时候需要自定义类加载器?
|
||||||
|
- 我们做一道题吧,手写一个对象池
|
||||||
|
- 有什么想问我的么?(感觉我很多点都没答好,是不是挂了(结果真的是) )
|
||||||
|
|
||||||
|
### 小结
|
||||||
|
|
||||||
|
头条的面试确实很专业,每次面试官会提前给你发一个视频链接,然后准点开始面试,而且考察的点都比较全。
|
||||||
|
|
||||||
|
面试官都有一个特点,会抓住一个值得深入的点或者你没说清楚的点深入下去直到你把这个点讲清楚,不然面试官会觉得你并没有真正理解。二面面试官给了我一点建议,研究技术的时候一定要去研究产生的背景,弄明白在什么场景解决什么特定的问题,其实很多技术内部都是相通的。很诚恳,还是很感谢这位面试官大大。
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
从年前开始面试到头条面完大概一个多月的时间,真的有点身心俱疲的感觉。最后拿到了拼多多、蚂蚁的offer,还是蛮幸运的。头条的面试对我帮助很大,再次感谢面试官对我的诚恳建议,以及拼多多的HR对我的啰嗦的问题详细解答。
|
||||||
|
|
||||||
|
这里要说的是面试前要做好两件事:简历和自我介绍,简历要好好回顾下自己做的一些项目,然后挑几个亮点项目。自我介绍基本每轮面试都有,所以最好提前自己练习下,想好要讲哪些东西,分别怎么讲。此外,简历提到的技术一定是自己深入研究过的,没有深入研究也最好找点资料预热下,不打无准备的仗。
|
||||||
|
|
||||||
|
**这些年看过的书**:
|
||||||
|
|
||||||
|
《Effective Java》、《现代操作系统》、《TCP/IP详解:卷一》、《代码整洁之道》、《重构》、《Java程序性能优化》、《Spring实战》、《Zookeeper》、《高性能MySQL》、《亿级网站架构核心技术》、《可伸缩服务架构》、《Java编程思想》
|
||||||
|
|
||||||
|
说实话这些书很多只看了一部分,我通常会带着问题看书,不然看着看着就睡着了,简直是催眠良药😅。
|
||||||
|
|
||||||
|
|
||||||
|
最后,附一张自己面试前准备的脑图:
|
||||||
|
|
||||||
|
链接:https://pan.baidu.com/s/1o2l1tuRakBEP0InKEh4Hzw 密码:300d
|
||||||
|
|
||||||
|
全文完。
|
@ -0,0 +1,61 @@
|
|||||||
|
|
||||||
|
|
||||||
|
> 本文来自读者 Boyn 投稿!恭喜这位粉丝拿到了含金量极高的字节跳动实习 offer!赞!
|
||||||
|
|
||||||
|
## 基本条件
|
||||||
|
|
||||||
|
本人是底层 211 本科,现在大三,无科研经历,但是有一些项目经历,在国内监控行业某头部企业做过一段时间的实习。想着投一下字节,可以积累一下面试经验和为春招做准备.投了简历之后,过了一段时间,HR 就打电话跟我约时间,在年后进行远程面。
|
||||||
|
|
||||||
|
说明一下,我投的是北京 office。
|
||||||
|
|
||||||
|
## 一面
|
||||||
|
|
||||||
|
面试官很和蔼,由于疫情的原因,大家都在家里面进行远程面试
|
||||||
|
|
||||||
|
开头没有自我介绍,直接开始问项目了,问了比如
|
||||||
|
|
||||||
|
- 常用的 Web 组件有哪些(回答了自己经常用到的 SpringBoot,Redis,Mysql 等等,字节这边基本没有用 Java 的后台,所以感觉面试官不大会问 Spring,Java 这些东西,反倒是对数据库和中间件比较感兴趣)
|
||||||
|
- Kafka 相关,如何保证不会重复消费,Kafka 消费组结构等等(这个只是凭着感觉和面试官说了,因为 Kafka 自己确实准备得不充分,但是心态稳住了)
|
||||||
|
- **Mysql 索引,B+树(必考嗷同学们)**
|
||||||
|
|
||||||
|
还有一些项目中的细节,这些因人而异,就不放上来了,提示一点就是要在项目中介绍一些亮眼的地方,比如用了什么牛逼的数据结构,架构上有什么特点,并发量大小还有怎么去 hold 住并发量
|
||||||
|
|
||||||
|
后面就是算法题了,一共做了两道
|
||||||
|
|
||||||
|
1. 判断平衡二叉树(这道题总体来说并不难,但是面试官在中间穿插了垃圾回收的知识,这就很难受了,具体的就是大家要判断一下对象在什么时候会回收,可达性分析什么时候对这个对象来说是不可达的,还有在递归函数中内存如何变化,这个是让我们来对这个函数进行执行过程的建模,只看栈帧大小变化的话,应该有是两个峰值,中间会有抖动的情况)
|
||||||
|
2. 二分查找法的变种题,给定`target`和一个升序的数组,寻找下一个比数组大的数.这道题也不难,靠大家对二分查找法的熟悉程度,当然,这边还有一个优化的点,可以看看[我的博客](https://boyn.top/2019/11/09/%E7%AE%97%E6%B3%95%E4%B8%8E%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE%E6%B3%95/)找找灵感
|
||||||
|
|
||||||
|
完成了之后,面试官让我等一会有二面,大概 10 分钟左右吧,休息了一会就继续了
|
||||||
|
|
||||||
|
## 二面
|
||||||
|
|
||||||
|
二面一上来就是先让我自我介绍,当然还是同样的套路,同样的香脆
|
||||||
|
|
||||||
|
然后问了我一些关于 Redis 的问题,比如 **zset 的实现(跳表,这个高频)** ,键的过期策略,持久化等等,这些在大多数 Redis 的介绍中都可以找到,就不细说了
|
||||||
|
|
||||||
|
还有一些数据结构的问题,比如说问了哈希表是什么,给面试官详细说了一下`java.util.HashMap`是怎么实现(当然里面就穿插着红黑树了,多看看红黑树是有什么特点之类的)的,包括说为什么要用链地址法来避免冲突,探测法有哪些,链地址法和探测法的优劣对比
|
||||||
|
|
||||||
|
后面还跟我讨论了很久的项目,所以说大家的项目一定要做好,要有亮点的地方,在这里跟面试官讨论了很多项目优化的地方,还有什么不足,还有什么地方可以新增功能等等,同样不细说了
|
||||||
|
|
||||||
|
一边讨论的时候劈里啪啦敲了很多,应该是对个人的面试评价一类的
|
||||||
|
|
||||||
|
后面就是字节的传统艺能手撕算法了,一共做了三道
|
||||||
|
|
||||||
|
- 一二道是连在一起的.给定一个规则`S_0 = {1} S_1={1,2,1} S_2 = {1,2,1,3,1,2,1} S_n = {S_n-1 , n + 1, S_n-1}`.第一个问题是他们的个数有什么关系(1 3 7 15... 2 的 n 次方-1,用位运算解决).第二个问题是给定数组个数下标 n 和索引 k,让我们求出 S_n(k)所指的数,假如`S_2(2) = 1`,我在做的时候没有什么好的思路,如果有的话大家可以分享一下
|
||||||
|
- 第三道是下一个排列:[https://leetcode-cn.com/problems/next-permutation](https://leetcode-cn.com/problems/next-permutation) 的题型,不过做了一些修改,数组大小`10000<n<100000`,不能用暴力法,还有数字是在 1-9 之间会有重复
|
||||||
|
|
||||||
|
## hr 面
|
||||||
|
|
||||||
|
一些偏职业规划的话题了,实习时间,项目经历,实习经历这些。
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
基础很重要!这次准备到的 Redis,Mysql,JVM 原理等等都有问到了,(网络这一块没问,但是也是要好好准备的,对于后台来说,网络知识不仅仅是面试,还是以后工作的知识基础).当然自己也有准备不足的地方,比如 Kafka 等中间件,只会用不会原理是万万不行的.并且这些基础知识不能只靠背,面试官还会融合在项目里面进行串问
|
||||||
|
|
||||||
|
问到了不会的不要慌,因为面试官是在试探你的技术深度,有可能会针对某一个问题,问到你不会为止,所以你出现不会的问题是很正常的,心态把控住就行.
|
||||||
|
|
||||||
|
无论是做题,还是回答问题的时候,牢记你不是在考试,而是在交流,和面试官有互动和沟通是很重要的,你说的一些疏漏的地方,如果你及时跟面试官反馈,还是可以补救一下的
|
||||||
|
|
||||||
|
最重要的一点字节的面试就是算法一定要牢固,每一轮都会有手撕算法的,这个不用想,LeetCode+剑指 Offer 走起来就对了,心态很重要,算法题不一定都是你会的,要有一定的心理准备,遇到难题可以先冷静分析一波.而且写出`Bug free`的代码也是很重要的,我前面的几题算法因为在牛客网上进行面试,所以要运行出来.
|
||||||
|
|
||||||
|
最后祝大家在春招取得好的 Offer,_奥力给_!
|
@ -0,0 +1,125 @@
|
|||||||
|
> 本文是鄙人薛某这位老哥的投稿,虽然面试最后挂了,但是老哥本身还是挺优秀的,而且通过这次面试学到了很多东西,我想这就足够了!加油!不要畏惧面试失败,好好修炼自己,多准备一下,后面一定会找到让自己满意的工作。
|
||||||
|
>
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
前段时间家里出了点事,辞职回家待了一段时间,处理完老家的事情后就回到广州这边继续找工作,大概是国庆前几天我去面试了一家叫做Bigo(YY的子公司),面试的职位是面向3-5年的Java开发,最终自己倒在了第三轮的技术面上。虽然有些遗憾和泄气,但想着还是写篇博客来记录一下自己的面试过程好了,也算是对广大程序员同胞们的分享,希望对你们以后的学习和面试能有所帮助。
|
||||||
|
|
||||||
|
## 个人情况
|
||||||
|
|
||||||
|
先说下LZ的个人情况。
|
||||||
|
|
||||||
|
17年毕业,二本,目前位于广州,是一个非常普通的Java开发程序员,算起来有两年多的开发经验。
|
||||||
|
|
||||||
|
其实这个阶段有点尴尬,高不成低不就,比初级程序员稍微好点,但也达不到高级的程度。加上现如今IT行业接近饱和,很多岗位都是要求至少3-5年以上开发经验,所以对于两年左右开发经验的需求其实是比较小的,这点在LZ找工作的过程中深有体会。最可悲的是,今年的大环境不好,很多公司不断的在裁员,更别说招人了,残酷的形势对于求职者来说更是雪上加霜,相信很多求职的同学也有所体会。所以,不到万不得已的情况下,建议不要裸辞!
|
||||||
|
|
||||||
|
## Bigo面试
|
||||||
|
|
||||||
|
面试岗位:Java后台开发
|
||||||
|
|
||||||
|
经验要求:3-5年
|
||||||
|
|
||||||
|
由于是国庆前去面试Bigo的,到现在也有一个多月的时间了,虽然仍有印象,但也有不少面试题忘了,所以我只能尽量按照自己的回忆来描述面试的过程,不明白之处还请见谅!
|
||||||
|
|
||||||
|
### 一面(微信电话面)
|
||||||
|
|
||||||
|
bigo的第一面是微信电话面试,本来是想直接电话面,但面试官说需要手写算法题,就改成微信电话面。
|
||||||
|
|
||||||
|
- 自我介绍
|
||||||
|
- 先了解一下Java基础吧,什么是内存泄漏和内存溢出?(溢出是指创建太多对象导致内存空间不足,泄漏是无用对象没有回收)
|
||||||
|
- JVM怎么判断对象是无用对象?(根搜索算法,从GC Root出发,对象没有引用,就判定为无用对象)
|
||||||
|
- 根搜索算法中的根节点可以是哪些对象?(类对象,虚拟机栈的对象,常量引用的对象)
|
||||||
|
- 重载和重写的区别?(重载发生在同个类,方法名相同,参数列表不同;重写是父子类之间的行为,方法名好参数列表都相同,方法体内的程序不同)
|
||||||
|
- 重写有什么限制没有?
|
||||||
|
- Java有哪些同步工具?(synchronized和Lock)
|
||||||
|
- 这两者有什么区别?
|
||||||
|
- ArrayList和LinkedList的区别?(ArrayList基于数组,搜索快,增删元素慢,LinkedList基于链表,增删快,搜索因为要遍历元素所以效率低)
|
||||||
|
- 这两种集合哪个比较占内存?(看情况的,ArrayList如果有扩容并且元素没占满数组的话,浪费的内存空间也是比较多的,但一般情况下,LinkedList占用的内存会相对多点,因为每个元素都包含了指向前后节点的指针)
|
||||||
|
- 说一下HashMap的底层结构(数组 + 链表,链表过长变成红黑树)
|
||||||
|
- HashMap为什么线程不安全,1.7版本之前HashMap有什么问题(扩容时多线程操作可能会导致链表成环的出现,然后调用get方法会死循环)
|
||||||
|
- 了解ConcurrentHashMap吗?说一下它为什么能线程安全(用了分段锁)
|
||||||
|
- 哪些方法需要锁住整个集合的?(读取size的时候)
|
||||||
|
- 看你简历写着你了解RPC啊,那你说下RPC的整个过程?(从客户端发起请求,到socket传输,然后服务端处理消息,以及怎么序列化之类的都大概讲了一下)
|
||||||
|
- 服务端获取客户端要调用的接口信息后,怎么找到对应的实现类的?(反射 + 注解吧,这里也不是很懂)
|
||||||
|
- dubbo的负载均衡有几种算法?(随机,轮询,最少活跃请求数,一致性hash)
|
||||||
|
- 你说的最少活跃数算法是怎么回事?(服务提供者有一个计数器,记录当前同时请求个数,值越小说明该服务器负载越小,路由器会优先选择该服务器)
|
||||||
|
- 服务端怎么知道客户端要调用的算法的?(socket传递消息过来的时候会把算法策略传递给服务端)
|
||||||
|
- 你用过redis做分布式锁是吧,你们是自己写的工具类吗?(不是,我们用redission做分布式锁)
|
||||||
|
- 线程拿到key后是怎么保证不死锁的呢?(给这个key加上一个过期时间)
|
||||||
|
- 如果这个过期时间到了,但是业务程序还没处理完,该怎么办?(额......可以在业务逻辑上保证幂等性吧)
|
||||||
|
- 那如果多个业务都用到分布式锁的话,每个业务都要保证幂等性了,有没有更好的方法?(额......思考了下暂时没有头绪,面试官就说那先跳过吧。事后我了解到redission本身是有个看门狗的监控线程的,如果检测到key被持有的话就会再次重置过期时间)
|
||||||
|
- 你那边有纸和笔吧,写一道算法,用两个栈模拟一个队列的入队和出队。(因为之前复习的时候对这道题有印象,写的时候也比较快,大概是用了五分钟,然后就拍成图片发给了面试官,对方看完后表示没问题就结束了面试。)
|
||||||
|
|
||||||
|
第一面问的不算难,问题也都是偏基础之类的,虽然答得不算完美,但过程还是比较顺利的。几天之后,Bigo的hr就邀请我去他们公司参加现场面试。
|
||||||
|
|
||||||
|
### 二面
|
||||||
|
|
||||||
|
到Bigo公司后,一位hr小姐姐招待我到了一个会议室,等了大概半个小时,一位中年男子走了进来,非常的客气,说不好意思让我等那么久了,并且介绍了自己是技术经理,然后就开始了我们的交谈。
|
||||||
|
|
||||||
|
- 依照惯例,让我简单做下自我介绍,这个过程他也在边看我的简历。
|
||||||
|
- 说下你最熟悉的项目吧。(我就拿我上家公司最近做的一个电商项目开始介绍,从简单的项目描述,到项目的主要功能,以及我主要负责的功能模块,吧啦吧啦..............)
|
||||||
|
- 你对这个项目这么熟悉,那你根据你的理解画一下你的项目架构图,还有说下你具体参与了哪部分。(这个题目还是比较麻烦的,毕竟我当时离职的时间也挺长了,对这个项目的架构也是有些模糊。当然,最后还是硬着头皮还是画了个大概,从前端开始访问,然后通过nginx网关层,最后到具体的服务等等,并且把自己参与的服务模块也标示了出来)
|
||||||
|
- 你的项目用到了Spring Cloud GateWay,既然你已经有nginx做网关了,为什么还要用gateWay呢?(nginx是做负载均衡,还有针对客户端的访问做网关用的,gateWay是接入业务层做的网关,而且还整合了熔断器Hystrix)
|
||||||
|
- 熔断器Hystrix最主要的作用是什么?(防止服务调用失败导致的服务雪崩,能降级)
|
||||||
|
- 你的项目用到了redis,你们的redis是怎么部署的?(额。。。。好像是哨兵模式部署的吧。)
|
||||||
|
- 说一下你对哨兵模式的理解?(我对哨兵模式了解的不多,就大概说了下Sentinel监控之类的,还有类似ping命令的心跳机制,以及怎么判断一个master是下线之类。。。。。)
|
||||||
|
- 那你们为什么要用哨兵模式呢?怎么不用集群的方式部署呢?一开始get不到他的点,就说哨兵本身就是多实例部署的,他解释了一下,说的是redis-cluster的部署方案。(额......redis的环境搭建有专门的运维人员部署的,应该是优先考虑高可用吧..........开始有点心慌了,因为我也不知道为什么)
|
||||||
|
- 哦,那你是觉得集群没有办法实现高可用吗?(不....不是啊,只是觉得哨兵模式可能比较保证主从复制安全性吧........我也不知道自己在说什么)
|
||||||
|
- 集群也是能保证高可用的,你知道它又是怎么保证主从一致性的吗?(好吧,这里真的不知道了,只能跳过)
|
||||||
|
- 你肯定有微信吧,如果让你来设计微信朋友圈的话,你会怎么设计它的属性成员呢?(嗯......需要有用户表,朋友圈的表,好友表之类的吧)
|
||||||
|
- 嗯,好,你也知道微信用户有接近10亿之多,那肯定要涉及到分库分表,如果是你的话,怎么设计分库分表呢?(这个问题考察的点比较大,我答的其实一般,而且这个过程面试官还不断的进行连环炮发问,导致这个话题说了有将近20分钟,限于篇幅,这里就不再详述了)
|
||||||
|
- 这边差不多了,最后你写一道算法吧,有一组未排序的整形数组,你设计一个算法,对数组的元素两两配对,然后输出最大的绝对值差和最小的绝对值差的"对数"。(听到这道题,我第一想法就是用HashMap来保存,key是两个元素的绝对值差,value是配对的数量,如果有相同的就加1,没有就赋值为1,然后最后对map做排序,输出最大和最小的value值,写完后面试官说结果虽然是正确的,但是不够效率,因为遍历的时间复杂度成了O(n^2),然后提醒了我往排序这方面想。我灵机一动,可以先对数组做排序,然后首元素与第二个元素做绝对值差,记为num,然后首元素循环和后面的元素做计算,直到绝对值差不等于num位置,这样效率比起O(n^2)快多了。)
|
||||||
|
|
||||||
|
面试完后,技术官就问我有什么要问他的,我就针对这个岗位的职责和项目所用的技术栈做了询问,然后就让我先等下,等他去通知三面的技术官。说实话,二面给我的感觉是最舒服的,因为面试官很亲切,面试的过程一直积极的引导我,而且在职业规划方面给了我很多的建议,让我受益匪浅,虽然面试时间有一个半小时,但却丝毫不觉得长,整个面试过程聊得挺舒服的,不过因为时间比较久了,很多问题我也记不清了。
|
||||||
|
|
||||||
|
### 三面
|
||||||
|
|
||||||
|
二面结束后半个小时,三面的技术面试官就开始进来了,从他的额头发量分布情况就能猜想是个大牛,人狠话不多,坐下后也没让我做自我介绍,直接开问,整个过程我答的也不好,而且面试官的问题表述有些不太清晰,经常需要跟他重复确认清楚。
|
||||||
|
|
||||||
|
- 对事务了解吗?说一下事务的隔离级别有哪些(我以比较了解的Spring来说,把Spring的四种事务隔离级别都叙述了一遍)
|
||||||
|
|
||||||
|
- 你做过电商,那应该知道下单的时候需要减库存对吧,假设现在有两个服务A和B,分别操作订单和库存表,A保存订单后,调用B减库存的时候失败了,这个时候A也要回滚,这个事务要怎么设计?(B服务的减库存方法不抛异常,由调用方也就是A服务来抛异常)
|
||||||
|
|
||||||
|
- 了解过读写分离吗?(额。。。大概了解一点,就是写的时候进主库,读的时候读从库)
|
||||||
|
|
||||||
|
- 你说读的时候读从库,现在假设有一张表User做了读写分离,然后有个线程在**一个事务范围内**对User表先做了写的处理,然后又做了读的处理,这时候数据还没同步到从库,怎么保证读的时候能读到最新的数据呢?(听完顿时有点懵圈,一时间答不上来,后来面试官说想办法保证一个事务中读写都是同一个库才行)
|
||||||
|
|
||||||
|
- 你的项目里用到了rabbitmq,那你说下mq的消费端是怎么处理的?(就是消费端接收到消息之后,会先把消息存到数据库中,然后再从数据库中定时跑消息)
|
||||||
|
|
||||||
|
- 也就是说你的mq是先保存到数据库中,然后业务逻辑就是从mq中读取消息然后再处理的是吧?(是的)
|
||||||
|
|
||||||
|
- 那你的消息是唯一的吗?(是的,用了唯一约束)
|
||||||
|
|
||||||
|
- 你怎么保证消息一定能被消费?或者说怎么保证一定能存到数据库中?(这里开始慌了,因为mq接入那一块我只是看过部分逻辑,但没有亲自参与,凭着自己对mq的了解就答道,应该是靠rabbitmq的ack确认机制)
|
||||||
|
|
||||||
|
- 好,那你整理一下你的消费端的整个处理逻辑流程,然后说说你的ack是在哪里返回的(听到这里我的心凉了一截,mq接入这部分我确实没有参与,硬着头皮按照自己的理解画了一下流程,但其实漏洞百出)
|
||||||
|
|
||||||
|
- 按照你这样画的话,如果数据库突然宕机,你的消息该怎么确认已经接收?(额.....那发送消息的时候就存放消息可以吧.........回答的时候心里千万只草泥马路过........行了吧,没玩没了了。)
|
||||||
|
|
||||||
|
- 那如果发送端的服务是多台部署呢?你保存消息的时候数据库就一直报唯一性的错误?(好吧,你赢了。。。最后硬是憋出了一句,您说的是,这样设计确实不好。。。。)
|
||||||
|
|
||||||
|
- 算了,跳过吧,现在你来设计一个map,然后有两个线程对这个map进行操作,主线程高速增加和删除map的元素,然后有个异步线程定时去删除map中主线程5秒内没有删除的数据,你会怎么设计?
|
||||||
|
|
||||||
|
(这道题我答得并不好,做了下简单的思考就说可以把map的key加上时间戳的标志,遍历的时候发现小于当前时间戳5秒前的元素就进行删除,面试官对这样的回答明显不太满意,说这样遍历会影响效率,ps:对这道题,大佬们如果有什么高见可以在评论区说下!)
|
||||||
|
|
||||||
|
......还有其他问题,但我只记住了这么多,就这样吧。
|
||||||
|
|
||||||
|
面完最后一道题后,面试官就表示这次面试过程结束了,让我回去等消息。听到这里,我知道基本上算是宣告结果了。回想起来,自己这一轮面试确实表现的很一般,加上时间拖得很长,从当天的2点半一直面试到6点多,精神上也尽显疲态。果然,几天之后,hr微信通知了我,说我第三轮技术面试没有通过,这一次面试以失败告终。
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
以上就是面试的大概过程,不得不说,大厂的面试还是非常有技术水平的,这个过程中我学到了很多,这里分享下个人的一些心得:
|
||||||
|
|
||||||
|
1、**基础**!**基础**!**基础**!重要的事情说三遍,无论是什么阶段的程序员,基础都是最重要的。每个公司的面试一定会涉及到基础知识的提问,如果你的基础不扎实,往往第一面就可能被淘汰。
|
||||||
|
|
||||||
|
2、**简历需要适当的包装**。老实说,我的简历肯定是经过包装的,这也是我的工作年限不够,但却能获取Bigo面试机会的重要原因,所以适当的包装一下简历很有必要,不过切记一点,就是**不能脱离现实**,比如明明只有两年经验,却硬是写到三年。小厂还可能蒙混过关,但大厂基本很难,因为很多公司会在入职前做背景调查。
|
||||||
|
|
||||||
|
3、**要对简历上的技术点很熟悉**。简历包装可以,但一定要对简历上的技术点很熟悉,比如只是简单写过rabbitmq的demo的话,就不要写“熟悉”等字眼,因为很多的面试官会针对一个技能点问的很深入,像连环炮一样的深耕你对这个技能点的理解程度。
|
||||||
|
|
||||||
|
4、**简历上的项目要非常熟悉**。一般我们写简历都是需要对自己的项目做一定程序的包装和美化,项目写得好能给简历加很多分。但一定要对项目非常的熟悉,不熟悉的模块最好不要写上去。笔者这次就吃了大亏,我的简历上有个电商项目就写到了用rabbitmq处理下单,虽然稍微了解过那部分下单的处理逻辑,但由于没有亲自参与就没有做深入的了解,面试时在这一块内容上被Bigo三面的面试官逼得最后哑口无言。
|
||||||
|
|
||||||
|
5、**提升自己的架构思维**。对于初中级程序员来说,日常的工作就是基本的增删改查,把功能实现就完事了,这种思维不能说不好,只是想更上一层楼的话,业务时间需要提升下自己的架构思维能力,比如说如果让你接手一个项目的话,你会怎么考虑设计这个项目,从整体架构,到引入一些组件,再到设计具体的业务服务,这些都是设计一个项目必须要考虑的环节,对于提升我们的架构思维是一种很好的锻炼,这也是很多大厂面试高级程序员时的重要考察部分。
|
||||||
|
|
||||||
|
6、**不要裸辞**。这也是我最朴实的建议了,大环境不好,且行且珍惜吧,唉~~~~
|
||||||
|
|
||||||
|
总的来说,这次面试Bigo还是收获颇丰的,虽然有点遗憾,但也没什么后悔的,毕竟自己面试之前也是准备的很充分了,有些题目答得不好说明我还有很多技术盲区,不懂就是不懂,再这么吹也吹不出来。这也算是给我提了个醒,你还嫩着呢,好好修炼内功吧,毕竟菜可是原罪啊。
|
@ -0,0 +1,251 @@
|
|||||||
|
本文来自 Anonymous 的投稿 ,Guide哥 对原文进行了重新排版和一点完善。
|
||||||
|
|
||||||
|
<!-- TOC -->
|
||||||
|
|
||||||
|
- [一面 (37 分钟左右)](#一面-37-分钟左右)
|
||||||
|
- [二面 (33 分钟左右)](#二面-33-分钟左右)
|
||||||
|
- [三面 (46 分钟)](#三面-46-分钟)
|
||||||
|
- [HR 面](#hr-面)
|
||||||
|
|
||||||
|
<!-- /TOC -->
|
||||||
|
|
||||||
|
### 一面 (37 分钟左右)
|
||||||
|
|
||||||
|
一面是上海的小哥打来的,3.12 号中午确认的内推,下午就打来约时间了,也是唯一一个约时间的面试官。约的晚上八点。紧张的一比,人生第一次面试就献给了阿里。
|
||||||
|
|
||||||
|
幸运的是一面的小哥特温柔。好像是个海归?口语中夹杂着英文。废话不多说,上干货:
|
||||||
|
|
||||||
|
**面试官:** 先自我介绍下吧!
|
||||||
|
|
||||||
|
**我:** 巴拉巴拉...。
|
||||||
|
|
||||||
|
> 关于自我介绍:从 HR 面、技术面到高管面/部门主管面,面试官一般会让你先自我介绍一下,所以好好准备自己的自我介绍真的非常重要。网上一般建议的是准备好两份自我介绍:一份对 HR 说的,主要讲能突出自己的经历,会的编程技术一语带过;另一份对技术面试官说的,主要讲自己会的技术细节,项目经验,经历那些就一语带过。
|
||||||
|
|
||||||
|
**面试官:** 我看你简历上写你做了个秒杀系统?我们就从这个项目开始吧,先介绍下你的项目。
|
||||||
|
|
||||||
|
> 关于项目介绍:如果有项目的话,技术面试第一步,面试官一般都是让你自己介绍一下你的项目。你可以从下面几个方向来考虑:
|
||||||
|
>
|
||||||
|
> 1. 对项目整体设计的一个感受(面试官可能会让你画系统的架构图)
|
||||||
|
> 2. 在这个项目中你负责了什么、做了什么、担任了什么角色
|
||||||
|
> 3. 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用
|
||||||
|
> 4. 另外项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个棘手的问题的时候你是如何解决的又或者说你在这个项目用了什么技术实现了什么功能比如:用 redis 做缓存提高访问速度和并发量、使用消息队列削峰和降流等等。
|
||||||
|
|
||||||
|
**我:** 我说了我是如何考虑它的需求(秒杀地址隐藏,记录订单,减库存),一开始简单的用 synchronized 锁住方法,出现了问题,后来乐观锁改进,又有瓶颈,再上缓存,出现了缓存雪崩,于是缓存预热,错开缓存失效时间。最后,发现先记录订单再减库存会减少行级锁等待时间。
|
||||||
|
|
||||||
|
> 一面面试官很耐心地听,并给了我一些指导,问了我乐观锁是怎么实现的,我说是基于 sql 语句,在减库存操作的 where 条件里加剩余库存数>0,他说这应该不算是一种乐观锁,应该先查库存,在减库存的时候判断当前库存是否与读到的库存一样(可这样不是多一次查询操作吗?不是很理解,不过我没有反驳,只是说理解您的意思。事实证明千万别怼面试官,即使你觉得他说的不对)
|
||||||
|
|
||||||
|
**面试官:** 我缓存雪崩什么情况下会发生?如何避免?
|
||||||
|
|
||||||
|
**我:** 当多个商品缓存同时失效时会雪崩,导致大量查询数据库。还有就是秒杀刚开始的时候缓存里没有数据。解决方案:缓存预热,错开缓存失效时间
|
||||||
|
|
||||||
|
**面试官:** 问我更新数据库的同时为什么不马上更新缓存,而是删除缓存?
|
||||||
|
|
||||||
|
**我:** 因为考虑到更新数据库后更新缓存可能会因为多线程下导致写入脏数据(比如线程 A 先更新数据库成功,接下来要取更新缓存,接着线程 B 更新数据库,但 B 又更新了缓存,接着 B 的时间片用完了,线程 A 更新了缓存)
|
||||||
|
|
||||||
|
逼逼了将近 30 分钟,面试官居然用周杰伦的语气对我说:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
我突然受宠若惊,连忙说谢谢,也正是因为第一次面试得到了面试官的肯定,才让我信心大增,二三面稳定发挥。
|
||||||
|
|
||||||
|
**面试官又曰:** 我看你还懂数据库是吧,答:略懂略懂。。。那我问个简单的吧!
|
||||||
|
|
||||||
|
**我:** 因为这个问题太简单了,所以我忘记它是什么了。
|
||||||
|
|
||||||
|
**面试官:** 你还会啥数据库知识?
|
||||||
|
|
||||||
|
**我:** 我一听,问的这么随意的吗。。。都让我选题了,我就说我了解索引,慢查询优化,巴拉巴拉
|
||||||
|
|
||||||
|
**面试官:** 等等,你说索引是吧,那你能说下索引的存储数据结构吗?
|
||||||
|
|
||||||
|
**我:** 我心想这简单啊,我就说 B+树,还说了为什么用 B+树
|
||||||
|
|
||||||
|
**面试官:** 你简历上写的这个 J.U.C 包是什么啊?(他居然不知道 JUC)
|
||||||
|
|
||||||
|
**我:** 就是 java 多线程的那个包啊。。。
|
||||||
|
|
||||||
|
**面试官:** 那你都了解里面的哪些东西呢?
|
||||||
|
|
||||||
|
**我:** 哈哈哈!这可是我的强项,从 ConcurrentHashMap,ConcurrentLinkedQueue 说到 CountDownLatch,CyclicBarrier,又说到线程池,分别说了底层实现和项目中的应用。
|
||||||
|
|
||||||
|
**面试官:** 我觉得差不多了,那我再问个与技术无关的问题哈,虽然这个问题可能不应该我问,就是你是如何考虑你的项目架构的呢?
|
||||||
|
|
||||||
|
**我:** 先用最简单的方式实现它,再去发掘系统的问题和瓶颈,于是查资料改进架构。。。
|
||||||
|
|
||||||
|
**面试官:** 好,那我给你介绍下我这边的情况吧
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**总结:** 一面可能是简历面吧,问的比较简单,我在讲项目中说出了我做项目时的学习历程和思考,赢得了面试官的好感,感觉他应该给我的评价很好。
|
||||||
|
|
||||||
|
### 二面 (33 分钟左右)
|
||||||
|
|
||||||
|
然而开心了没一会,内推人问我面的怎么样啊?看我流程已经到大大 boss 那了。我一听二面不是主管吗???怎么直接跳了一面。于是瞬间慌了,赶紧(下床)学习准备二面。
|
||||||
|
|
||||||
|
隔了一天,3.14 的早上 10:56 分,杭州的大大 boss 给我打来了电话,卧槽我当时在上毛概课,万恶的毛概课每节课都点名,我还在最后一排不敢跑出去。于是接起电话来怂怂地说不好意思我在上课,晚上可以面试吗?大大 boss 看来很忙啊,跟我说晚上没时间啊,再说吧!
|
||||||
|
|
||||||
|
于是又隔了一天,3.16 中午我收到了北京的电话,当时心里小失望,我的大大 boss 呢???接起电话来,就是一番狂轰乱炸。。。
|
||||||
|
|
||||||
|
第一步还是先自我介绍,这个就不多说了,提前准备好要说的重点就没问题!
|
||||||
|
|
||||||
|
**面试官:** 我们还是从你的项目开始吧,说说你的秒杀系统。
|
||||||
|
|
||||||
|
**我:** 一面时的套路。。。我考虑到秒杀地址在开始前不应暴露给用户。。。
|
||||||
|
|
||||||
|
**面试官:** 等下啊,为什么要这样呢?暴露给用户会怎么样?
|
||||||
|
|
||||||
|
**我:** 用户提前知道秒杀地址就可以写脚本来抢购了,这样不公平
|
||||||
|
|
||||||
|
**面试官:** 那比如说啊,我现在是个黑客,我在秒杀开始时写好了脚本,运行一万个线程获取秒杀地址,这样是不是也不公平呢?
|
||||||
|
|
||||||
|
**我:** 我考虑到了这方面,于是我自己写了个 LRU 缓存(划重点,这么多好用的缓存我为啥不用偏要自己写?就是为了让面试官上钩问我是怎么写的,这样我就可以逼逼准备好的内容了!),用这个缓存存储请求的 ip 和用户名,一个 ip 和用户名只能同时透过 3 个请求。
|
||||||
|
|
||||||
|
**面试官:** 那我可不可以创建一个 ip 代理池和很多用户来抢购呢?假设我有很多手机号的账户。
|
||||||
|
|
||||||
|
**我:** 这就是在为难我胖虎啊,我说这种情况跟真实用户操作太像了。。。我没法区别,不过我觉得可以通过地理位置信息或者机器学习算法来做吧。。。
|
||||||
|
|
||||||
|
**面试官:** 好的这个问题就到这吧,你接着说
|
||||||
|
|
||||||
|
**我:** 我把生成订单和减库存两条 sql 语句放在一个事务里,都操作成功了则认为秒杀成功。
|
||||||
|
|
||||||
|
**面试官:** 等等,你这个订单表和商品库存表是在一个数据库的吧,那如果在不同的数据库中呢?
|
||||||
|
|
||||||
|
**我:** 这面试官好变态啊,我只是个本科生?!?!我觉得应该要用分布式锁来实现吧。。。
|
||||||
|
|
||||||
|
**面试官:** 有没有更轻量级的做法?
|
||||||
|
|
||||||
|
**我:** 不知道了。后来查资料发现可以用消息队列来实现。使用消息队列主要能带来两个好处:(1) 通过异步处理提高系统性能(削峰、减少响应所需时间);(2) 降低系统耦合性。关于消息队列的更多内容可以查看这篇文章:<https://snailclimb.gitee.io/javaguide/#/./system-design/data-communication/message-queue>
|
||||||
|
|
||||||
|
后来发现消息队列作用好大,于是现在在学手写一个消息队列。
|
||||||
|
|
||||||
|
**面试官:** 好的你接着说项目吧。
|
||||||
|
|
||||||
|
**我:** 我考虑到了缓存雪崩问题,于是。。。
|
||||||
|
|
||||||
|
**面试官:** 等等,你有没有考虑到一种情况,假如说你的缓存刚刚失效,大量流量就来查缓存,你的数据库会不会炸?
|
||||||
|
|
||||||
|
**我:** 我不知道数据库会不会炸,反正我快炸了。当时说没考虑这么高的并发量,后来发现也是可以用消息队列来解决,对流量削峰填谷。
|
||||||
|
|
||||||
|
**面试官:** 好项目聊(怼)完了,我们来说说别的,操作系统了解吧,你能说说 NIO 吗?
|
||||||
|
|
||||||
|
**我:** NIO 是。。。
|
||||||
|
|
||||||
|
**面试官:** 那你知道 NIO 的系统调用有哪些吗,具体是怎么实现的?
|
||||||
|
|
||||||
|
**我:** 当时复习 NIO 的时候就知道是咋回事,不知道咋实现。最近在补这方面的知识,可见 NIO 还是很重要的!
|
||||||
|
|
||||||
|
**面试官:** 说说进程切换时操作系统都会发生什么?
|
||||||
|
|
||||||
|
**我:** 不如杀了我,我最讨厌操作系统了。简单说了下,可能不对,需要答案自行百度。
|
||||||
|
|
||||||
|
**面试官:** 说说线程池?
|
||||||
|
|
||||||
|
**答:** 卧槽这我熟啊,把 Java 并发编程的艺术里讲的都说出来了,说了得有十分钟,自夸一波,毕竟这本书我看了五遍😂
|
||||||
|
|
||||||
|
**面试官:** 好问问计网吧如果设计一个聊天系统,应该用 TCP 还是 UDP?为什么
|
||||||
|
|
||||||
|
**我:** 当然是 TCP!原因如下:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**面试官:** 好的,你有什么要问我的吗?
|
||||||
|
|
||||||
|
**我:** 我还有下一次面试吗?
|
||||||
|
|
||||||
|
**面试官:** 应该。应该有的,一周内吧。还告诉我居然转正前要实习三个月?wtf,一个大三满课的本科生让我如何在八月底前实习三个月?
|
||||||
|
|
||||||
|
**我:** 面试官再见
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 三面 (46 分钟)
|
||||||
|
|
||||||
|
3.18 号,三面来了,这次又是那个大大 boss!
|
||||||
|
|
||||||
|
第一步还是先自我介绍,这个就不多说了,提前准备好要说的重点就没问题!
|
||||||
|
|
||||||
|
**面试官:** 聊聊你的项目?
|
||||||
|
|
||||||
|
**我:** 经过二面的教训,我迅速学习了一下分布式的理论知识,并应用到了我的项目(吹牛逼)中。
|
||||||
|
|
||||||
|
**面试官:** 看你用到了 Spring 的事务机制,你能说下 Spring 的事务传播吗?
|
||||||
|
|
||||||
|
**我:** 完了这个问题好像没准备,虽然之前刷知乎看到过。。。我就只说出来一条,面试官说其实这个有很多机制的,比如事务嵌套,内事务回滚外事务回滚都会有不同情况,你可以回去看看。
|
||||||
|
|
||||||
|
**面试官:** 说说你的分布式事务解决方案?
|
||||||
|
|
||||||
|
**我:** 我叭叭的照着资料查到的解决方案说了一通,面试官怎么好像没大听懂???
|
||||||
|
|
||||||
|
> 阿里巴巴之前开源了一个分布式 Fescar(一种易于使用,高性能,基于 Java 的开源分布式事务解决方案),后来,Ant Financial 加入 Fescar,使其成为一个更加中立和开放的分布式交易社区,Fescar 重命名为 Seata。Github 地址:<https://github.com/seata/seata>
|
||||||
|
|
||||||
|
**面试官:** 好,我们聊聊其他项目,说说你这个 MapReduce 项目?MapReduce 原理了解过吗?
|
||||||
|
|
||||||
|
**我:** 我叭叭地说了一通,面试官好像觉得这个项目太简单了。要不是没项目,我会把我的实验写上吗???
|
||||||
|
|
||||||
|
**面试官:** 你这个手写 BP 神经网络是干了啥?
|
||||||
|
|
||||||
|
**我:** 这是我选修机器学习课程时的一个作业,我又对它进行了扩展。
|
||||||
|
|
||||||
|
**面试官:** 你能说说为什么调整权值时要沿着梯度下降的方向?
|
||||||
|
|
||||||
|
**我:** 老大,你太厉害了,怎么什么都懂。我压根没准备这个项目。。。没想到会问,做过去好几个月了,加上当时一紧张就忘了,后来想起来大概是....。
|
||||||
|
|
||||||
|
**面试官:** 好我们问问基础知识吧,说说什么叫 xisuo?
|
||||||
|
|
||||||
|
**我:**???xisuo,您说什么,不好意思我没听清。(这面试官有点口音。。。)就是 xisuo 啊!xisuo 你不知道吗?。。。尴尬了十几秒后我终于意识到,他在说死锁!!!
|
||||||
|
|
||||||
|
**面试官:** 假如 A 账户给 B 账户转钱,会发生 xisuo 吗?能具体说说吗?
|
||||||
|
|
||||||
|
**我:** 当时答的不好,后来发现面试官又是想问分布式,具体答案参考这个:<https://blog.csdn.net/taylorchan2016/article/details/51039362>
|
||||||
|
|
||||||
|
**面试官:** 为什么不考研?
|
||||||
|
|
||||||
|
**我:** 不喜欢学术氛围,巴拉巴拉。
|
||||||
|
|
||||||
|
**面试官:** 你有什么问题吗?
|
||||||
|
|
||||||
|
**我:** 我还有下一面吗。。。面试官说让我等,一周内答复。
|
||||||
|
|
||||||
|
------
|
||||||
|
|
||||||
|
等了十天,一度以为我凉了,内推人说我流程到 HR 了,让我等着吧可能 HR 太忙了,3.28 号 HR 打来了电话,当时在教室,我直接飞了出去。
|
||||||
|
|
||||||
|
### HR 面
|
||||||
|
|
||||||
|
**面试官:** 你好啊,先自我介绍下吧
|
||||||
|
|
||||||
|
**我:** 巴拉巴拉....HR 面的技术面试和技术面的还是有所区别的!
|
||||||
|
|
||||||
|
面试官人特别好,一听就是很会说话的小姐姐!说我这里给你悄悄透露下,你的评级是 A 哦!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
接下来就是几个经典 HR 面挂人的问题,什么难给我来什么,我看别人的 HR 面怎么都是聊聊天。。。
|
||||||
|
|
||||||
|
**面试官:** 你为什么选择支付宝呢,你怎么看待支付宝?
|
||||||
|
|
||||||
|
**我:** 我从个人情怀,公司理念,环境氛围,市场价值,趋势导向分析了一波(说白了就是疯狂夸支付宝,不过说实话我说的那些一点都没撒谎,阿里确实做到了。比如我举了个雷军和格力打赌 5 年 2000 亿销售额,大部分企业家关注的是利益,而马云更关注的是真的为人类为世界做一些事情,利益不是第一位的。)
|
||||||
|
|
||||||
|
**面试官:** 明白了解,那你的优点我们都很明了了,你能说说你的缺点吗?
|
||||||
|
|
||||||
|
> 缺点肯定不能是目标岗位需要的关键能力!!!
|
||||||
|
>
|
||||||
|
> 总之,记住一点,面试官问你这个问题的话,你可以说一些不影响你这个职位工作需要的一些缺点。比如你面试后端工程师,面试官问你的缺点是什么的话,你可以这样说:自己比较内向,平时不太爱与人交流,但是考虑到以后可能要和客户沟通,自己正在努力改。
|
||||||
|
|
||||||
|
**我:** 据说这是 HR 面最难的一个问题。。。我当时翻了好几天的知乎才找到一个合适的,也符合我的答案:我有时候会表现的不太自信,比如阿里的内推二月份就开始了,其实我当时已经复习了很久了,但是老是觉得自己还不行,不敢投简历,于是又把书看了一遍才投的,当时也是舍友怂恿一波才投的,面了之后发现其实自己也没有很差。(划重点,一定要把自己的缺点圆回来)。
|
||||||
|
|
||||||
|
**面试官:** HR 好像不太满意我的答案,继续问我还有缺点吗?
|
||||||
|
|
||||||
|
**我:** 我说比较容易紧张吧,举了自己大一面实验室因为紧张没进去的例子,后来不断调整心态,现在已经好很多了。
|
||||||
|
|
||||||
|
接下来又是个好难的问题。
|
||||||
|
|
||||||
|
**面试官:** BAT 都给你 offer 了,你怎么选?
|
||||||
|
|
||||||
|
其实我当时好想说,BT 是什么?不好意思我只知道阿里。
|
||||||
|
|
||||||
|
**我 :** 哈哈哈哈开玩笑,就说了阿里的文化,支付宝给我们带来很多便利,想加入支付宝为人类做贡献!
|
||||||
|
|
||||||
|
最后 HR 问了我实习时间,现在大几之类的问题,说肯定会给我发 offer 的,让我等着就好了,希望过两天能收到好的结果。
|
||||||
|
|
||||||
|

|
@ -1,253 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
## 一 为什么 Java 中只有值传递?
|
|
||||||
|
|
||||||
|
|
||||||
首先回顾一下在程序设计语言中有关将参数传递给方法(或函数)的一些专业术语。**按值调用(call by value)表示方法接收的是调用者提供的值,而按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。** 它用来描述各种程序设计语言(不只是Java)中方法参数传递方式。
|
|
||||||
|
|
||||||
**Java程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容。**
|
|
||||||
|
|
||||||
**下面通过 3 个例子来给大家说明**
|
|
||||||
|
|
||||||
### example 1
|
|
||||||
|
|
||||||
|
|
||||||
```java
|
|
||||||
public static void main(String[] args) {
|
|
||||||
int num1 = 10;
|
|
||||||
int num2 = 20;
|
|
||||||
|
|
||||||
swap(num1, num2);
|
|
||||||
|
|
||||||
System.out.println("num1 = " + num1);
|
|
||||||
System.out.println("num2 = " + num2);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void swap(int a, int b) {
|
|
||||||
int temp = a;
|
|
||||||
a = b;
|
|
||||||
b = temp;
|
|
||||||
|
|
||||||
System.out.println("a = " + a);
|
|
||||||
System.out.println("b = " + b);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**结果:**
|
|
||||||
|
|
||||||
```
|
|
||||||
a = 20
|
|
||||||
b = 10
|
|
||||||
num1 = 10
|
|
||||||
num2 = 20
|
|
||||||
```
|
|
||||||
|
|
||||||
**解析:**
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
在swap方法中,a、b的值进行交换,并不会影响到 num1、num2。因为,a、b中的值,只是从 num1、num2 的复制过来的。也就是说,a、b相当于num1、num2 的副本,副本的内容无论怎么修改,都不会影响到原件本身。
|
|
||||||
|
|
||||||
**通过上面例子,我们已经知道了一个方法不能修改一个基本数据类型的参数,而对象引用作为参数就不一样,请看 example2.**
|
|
||||||
|
|
||||||
|
|
||||||
### example 2
|
|
||||||
|
|
||||||
```java
|
|
||||||
public static void main(String[] args) {
|
|
||||||
int[] arr = { 1, 2, 3, 4, 5 };
|
|
||||||
System.out.println(arr[0]);
|
|
||||||
change(arr);
|
|
||||||
System.out.println(arr[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void change(int[] array) {
|
|
||||||
// 将数组的第一个元素变为0
|
|
||||||
array[0] = 0;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**结果:**
|
|
||||||
|
|
||||||
```
|
|
||||||
1
|
|
||||||
0
|
|
||||||
```
|
|
||||||
|
|
||||||
**解析:**
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
array 被初始化 arr 的拷贝也就是一个对象的引用,也就是说 array 和 arr 指向的时同一个数组对象。 因此,外部对引用对象的改变会反映到所对应的对象上。
|
|
||||||
|
|
||||||
|
|
||||||
**通过 example2 我们已经看到,实现一个改变对象参数状态的方法并不是一件难事。理由很简单,方法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。**
|
|
||||||
|
|
||||||
**很多程序设计语言(特别是,C++和Pascal)提供了两种参数传递的方式:值调用和引用调用。有些程序员(甚至本书的作者)认为Java程序设计语言对对象采用的是引用调用,实际上,这种理解是不对的。由于这种误解具有一定的普遍性,所以下面给出一个反例来详细地阐述一下这个问题。**
|
|
||||||
|
|
||||||
|
|
||||||
### example 3
|
|
||||||
|
|
||||||
```java
|
|
||||||
public class Test {
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
|
||||||
// TODO Auto-generated method stub
|
|
||||||
Student s1 = new Student("小张");
|
|
||||||
Student s2 = new Student("小李");
|
|
||||||
Test.swap(s1, s2);
|
|
||||||
System.out.println("s1:" + s1.getName());
|
|
||||||
System.out.println("s2:" + s2.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void swap(Student x, Student y) {
|
|
||||||
Student temp = x;
|
|
||||||
x = y;
|
|
||||||
y = temp;
|
|
||||||
System.out.println("x:" + x.getName());
|
|
||||||
System.out.println("y:" + y.getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**结果:**
|
|
||||||
|
|
||||||
```
|
|
||||||
x:小李
|
|
||||||
y:小张
|
|
||||||
s1:小张
|
|
||||||
s2:小李
|
|
||||||
```
|
|
||||||
|
|
||||||
**解析:**
|
|
||||||
|
|
||||||
交换之前:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
交换之后:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
通过上面两张图可以很清晰的看出: **方法并没有改变存储在变量 s1 和 s2 中的对象引用。swap方法的参数x和y被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝**
|
|
||||||
|
|
||||||
### 总结
|
|
||||||
|
|
||||||
Java程序设计语言对对象采用的不是引用调用,实际上,对象引用是按
|
|
||||||
值传递的。
|
|
||||||
|
|
||||||
下面再总结一下Java中方法参数的使用情况:
|
|
||||||
|
|
||||||
- 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型》
|
|
||||||
- 一个方法可以改变一个对象参数的状态。
|
|
||||||
- 一个方法不能让对象参数引用一个新的对象。
|
|
||||||
|
|
||||||
|
|
||||||
### 参考:
|
|
||||||
|
|
||||||
《Java核心技术卷Ⅰ》基础知识第十版第四章4.5小节
|
|
||||||
|
|
||||||
## 二 ==与equals(重要)
|
|
||||||
|
|
||||||
**==** : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)
|
|
||||||
|
|
||||||
**equals()** : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
|
|
||||||
|
|
||||||
- 情况1:类没有覆盖equals()方法。则通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象。
|
|
||||||
- 情况2:类覆盖了equals()方法。一般,我们都覆盖equals()方法来两个对象的内容相等;若它们的内容相等,则返回true(即,认为这两个对象相等)。
|
|
||||||
|
|
||||||
|
|
||||||
**举个例子:**
|
|
||||||
|
|
||||||
```java
|
|
||||||
public class test1 {
|
|
||||||
public static void main(String[] args) {
|
|
||||||
String a = new String("ab"); // a 为一个引用
|
|
||||||
String b = new String("ab"); // b为另一个引用,对象的内容一样
|
|
||||||
String aa = "ab"; // 放在常量池中
|
|
||||||
String bb = "ab"; // 从常量池中查找
|
|
||||||
if (aa == bb) // true
|
|
||||||
System.out.println("aa==bb");
|
|
||||||
if (a == b) // false,非同一对象
|
|
||||||
System.out.println("a==b");
|
|
||||||
if (a.equals(b)) // true
|
|
||||||
System.out.println("aEQb");
|
|
||||||
if (42 == 42.0) { // true
|
|
||||||
System.out.println("true");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**说明:**
|
|
||||||
|
|
||||||
- String中的equals方法是被重写过的,因为object的equals方法是比较的对象的内存地址,而String的equals方法比较的是对象的值。
|
|
||||||
- 当创建String类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个String对象。
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 三 hashCode与equals(重要)
|
|
||||||
|
|
||||||
面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写equals时必须重写hashCode方法?”
|
|
||||||
|
|
||||||
### hashCode()介绍
|
|
||||||
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode() 函数。另外需要注意的是: Object 的 hashcode 方法是本地方法,也就是用 c 语言或 c++ 实现的,该方法通常用来将对象的 内存地址 转换为整数之后返回。
|
|
||||||
|
|
||||||
```java
|
|
||||||
/**
|
|
||||||
* Returns a hash code value for the object. This method is
|
|
||||||
* supported for the benefit of hash tables such as those provided by
|
|
||||||
* {@link java.util.HashMap}.
|
|
||||||
* <p>
|
|
||||||
* As much as is reasonably practical, the hashCode method defined by
|
|
||||||
* class {@code Object} does return distinct integers for distinct
|
|
||||||
* objects. (This is typically implemented by converting the internal
|
|
||||||
* address of the object into an integer, but this implementation
|
|
||||||
* technique is not required by the
|
|
||||||
* Java™ programming language.)
|
|
||||||
*
|
|
||||||
* @return a hash code value for this object.
|
|
||||||
* @see java.lang.Object#equals(java.lang.Object)
|
|
||||||
* @see java.lang.System#identityHashCode
|
|
||||||
*/
|
|
||||||
public native int hashCode();
|
|
||||||
```
|
|
||||||
|
|
||||||
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)
|
|
||||||
|
|
||||||
### 为什么要有hashCode
|
|
||||||
|
|
||||||
|
|
||||||
**我们以“HashSet如何检查重复”为例子来说明为什么要有hashCode:**
|
|
||||||
|
|
||||||
当你把对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他已经加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同hashcode值的对象,这时会调用equals()方法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的Java启蒙书《Head fist java》第二版)。这样我们就大大减少了equals的次数,相应就大大提高了执行速度。
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### hashCode()与equals()的相关规定
|
|
||||||
|
|
||||||
1. 如果两个对象相等,则hashcode一定也是相同的
|
|
||||||
2. 两个对象相等,对两个对象分别调用equals方法都返回true
|
|
||||||
3. 两个对象有相同的hashcode值,它们也不一定是相等的
|
|
||||||
4. **因此,equals方法被覆盖过,则hashCode方法也必须被覆盖**
|
|
||||||
5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
|
|
||||||
|
|
||||||
### 为什么两个对象有相同的hashcode值,它们也不一定是相等的?
|
|
||||||
|
|
||||||
在这里解释一位小伙伴的问题。以下内容摘自《Head Fisrt Java》。
|
|
||||||
|
|
||||||
因为hashCode() 所使用的杂凑算法也许刚好会让多个对象传回相同的杂凑值。越糟糕的杂凑算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同的 hashCode)。
|
|
||||||
|
|
||||||
我们刚刚也提到了 HashSet,如果 HashSet 在对比的时候,同样的 hashcode 有多个对象,它会使用 equals() 来判断是否真的相同。也就是说 hashcode 只是用来缩小查找成本。
|
|
||||||
|
|
||||||
参考:
|
|
||||||
|
|
||||||
[https://blog.csdn.net/zhzhao999/article/details/53449504](https://blog.csdn.net/zhzhao999/article/details/53449504)
|
|
||||||
|
|
||||||
[https://www.cnblogs.com/skywang12345/p/3324958.html](https://www.cnblogs.com/skywang12345/p/3324958.html)
|
|
||||||
|
|
||||||
[https://www.cnblogs.com/skywang12345/p/3324958.html](https://www.cnblogs.com/skywang12345/p/3324958.html)
|
|
||||||
|
|
||||||
[https://www.cnblogs.com/Eason-S/p/5524837.html](https://www.cnblogs.com/Eason-S/p/5524837.html)
|
|
||||||
|
|
@ -1,200 +0,0 @@
|
|||||||
|
|
||||||
### String和StringBuffer、StringBuilder的区别是什么?String为什么是不可变的?
|
|
||||||
|
|
||||||
#### String和StringBuffer、StringBuilder的区别
|
|
||||||
|
|
||||||
**可变性**
|
|
||||||
|
|
||||||
|
|
||||||
简单的来说:String 类中使用 final 关键字字符数组保存字符串,`private final char value[]`,所以 String 对象是不可变的。而StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串`char[]value` 但是没有用 final 关键字修饰,所以这两种对象都是可变的。
|
|
||||||
|
|
||||||
StringBuilder 与 StringBuffer 的构造方法都是调用父类构造方法也就是 AbstractStringBuilder 实现的,大家可以自行查阅源码。
|
|
||||||
|
|
||||||
AbstractStringBuilder.java
|
|
||||||
|
|
||||||
```java
|
|
||||||
abstract class AbstractStringBuilder implements Appendable, CharSequence {
|
|
||||||
char[] value;
|
|
||||||
int count;
|
|
||||||
AbstractStringBuilder() {
|
|
||||||
}
|
|
||||||
AbstractStringBuilder(int capacity) {
|
|
||||||
value = new char[capacity];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
**线程安全性**
|
|
||||||
|
|
||||||
String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
|
|
||||||
|
|
||||||
|
|
||||||
**性能**
|
|
||||||
|
|
||||||
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StirngBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
|
|
||||||
|
|
||||||
**对于三者使用的总结:**
|
|
||||||
1. 操作少量的数据 = String
|
|
||||||
2. 单线程操作字符串缓冲区下操作大量数据 = StringBuilder
|
|
||||||
3. 多线程操作字符串缓冲区下操作大量数据 = StringBuffer
|
|
||||||
|
|
||||||
#### String为什么是不可变的吗?
|
|
||||||
简单来说就是String类利用了final修饰的char类型数组存储字符,源码如下图所以:
|
|
||||||
|
|
||||||
```java
|
|
||||||
/** The value is used for character storage. */
|
|
||||||
private final char value[];
|
|
||||||
```
|
|
||||||
|
|
||||||
#### String真的是不可变的吗?
|
|
||||||
我觉得如果别人问这个问题的话,回答不可变就可以了。
|
|
||||||
下面只是给大家看两个有代表性的例子:
|
|
||||||
|
|
||||||
**1) String不可变但不代表引用不可以变**
|
|
||||||
```java
|
|
||||||
String str = "Hello";
|
|
||||||
str = str + " World";
|
|
||||||
System.out.println("str=" + str);
|
|
||||||
```
|
|
||||||
结果:
|
|
||||||
```
|
|
||||||
str=Hello World
|
|
||||||
```
|
|
||||||
解析:
|
|
||||||
|
|
||||||
实际上,原来String的内容是不变的,只是str由原来指向"Hello"的内存地址转为指向"Hello World"的内存地址而已,也就是说多开辟了一块内存区域给"Hello World"字符串。
|
|
||||||
|
|
||||||
**2) 通过反射是可以修改所谓的“不可变”对象**
|
|
||||||
|
|
||||||
```java
|
|
||||||
// 创建字符串"Hello World", 并赋给引用s
|
|
||||||
String s = "Hello World";
|
|
||||||
|
|
||||||
System.out.println("s = " + s); // Hello World
|
|
||||||
|
|
||||||
// 获取String类中的value字段
|
|
||||||
Field valueFieldOfString = String.class.getDeclaredField("value");
|
|
||||||
|
|
||||||
// 改变value属性的访问权限
|
|
||||||
valueFieldOfString.setAccessible(true);
|
|
||||||
|
|
||||||
// 获取s对象上的value属性的值
|
|
||||||
char[] value = (char[]) valueFieldOfString.get(s);
|
|
||||||
|
|
||||||
// 改变value所引用的数组中的第5个字符
|
|
||||||
value[5] = '_';
|
|
||||||
|
|
||||||
System.out.println("s = " + s); // Hello_World
|
|
||||||
```
|
|
||||||
|
|
||||||
结果:
|
|
||||||
|
|
||||||
```
|
|
||||||
s = Hello World
|
|
||||||
s = Hello_World
|
|
||||||
```
|
|
||||||
|
|
||||||
解析:
|
|
||||||
|
|
||||||
用反射可以访问私有成员, 然后反射出String对象中的value属性, 进而改变通过获得的value引用改变数组的结构。但是一般我们不会这么做,这里只是简单提一下有这个东西。
|
|
||||||
|
|
||||||
### 什么是反射机制?反射机制的应用场景有哪些?
|
|
||||||
|
|
||||||
#### 反射机制介绍
|
|
||||||
|
|
||||||
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
|
|
||||||
|
|
||||||
#### 静态编译和动态编译
|
|
||||||
|
|
||||||
- **静态编译:**在编译时确定类型,绑定对象
|
|
||||||
- **动态编译:**运行时确定类型,绑定对象
|
|
||||||
|
|
||||||
#### 反射机制优缺点
|
|
||||||
|
|
||||||
- **优点:** 运行期类型的判断,动态加载类,提高代码灵活度。
|
|
||||||
- **缺点:** 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的java代码要慢很多。
|
|
||||||
|
|
||||||
#### 反射的应用场景
|
|
||||||
|
|
||||||
反射是框架设计的灵魂。
|
|
||||||
|
|
||||||
在我们平时的项目开发过程中,基本上很少会直接使用到反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架也大量使用到了反射机制。
|
|
||||||
|
|
||||||
举例:①我们在使用JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序;②Spring框架也用到很多反射机制,最经典的就是xml的配置模式。Spring 通过 XML 配置模式装载 Bean 的过程:1) 将程序内所有 XML 或 Properties 配置文件加载入内存中;
|
|
||||||
2)Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息; 3)使用反射机制,根据这个字符串获得某个类的Class实例; 4)动态配置实例的属性
|
|
||||||
|
|
||||||
**推荐阅读:**
|
|
||||||
|
|
||||||
- [Reflection:Java反射机制的应用场景](https://segmentfault.com/a/1190000010162647?utm_source=tuicool&utm_medium=referral)
|
|
||||||
- [Java基础之—反射(非常重要)](https://blog.csdn.net/sinat_38259539/article/details/71799078)
|
|
||||||
### 什么是JDK?什么是JRE?什么是JVM?三者之间的联系与区别
|
|
||||||
|
|
||||||
这几个是Java中很基本很基本的东西,但是我相信一定还有很多人搞不清楚!为什么呢?因为我们大多数时候在使用现成的编译工具以及环境的时候,并没有去考虑这些东西。
|
|
||||||
|
|
||||||
**JDK:** 顾名思义它是给开发者提供的开发工具箱,是给程序开发者用的。它除了包括完整的JRE(Java Runtime Environment),Java运行环境,还包含了其他供开发者使用的工具包。
|
|
||||||
|
|
||||||
**JRE:** 普通用户而只需要安装JRE(Java Runtime Environment)来运行Java程序。而程序开发者必须安装JDK来编译、调试程序。
|
|
||||||
|
|
||||||
**JVM:** 当我们运行一个程序时,JVM负责将字节码转换为特定机器代码,JVM提供了内存管理/垃圾回收和安全机制等。这种独立于硬件和操作系统,正是java程序可以一次编写多处执行的原因。
|
|
||||||
|
|
||||||
**区别与联系:**
|
|
||||||
|
|
||||||
1. JDK用于开发,JRE用于运行java程序 ;
|
|
||||||
2. JDK和JRE中都包含JVM ;
|
|
||||||
3. JVM是java编程语言的核心并且具有平台独立性。
|
|
||||||
|
|
||||||
### 什么是字节码?采用字节码的最大好处是什么?
|
|
||||||
|
|
||||||
**先看下java中的编译器和解释器:**
|
|
||||||
|
|
||||||
Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚拟机理解的代码叫做`字节码`(即扩展名为`.class`的文件),它不面向任何特定的处理器,只面向虚拟机。每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行。这也就是解释了Java的编译与解释并存的特点。
|
|
||||||
|
|
||||||
Java源代码---->编译器---->jvm可执行的Java字节码(即虚拟指令)---->jvm---->jvm中解释器----->机器可执行的二进制机器码---->程序运行。
|
|
||||||
|
|
||||||
**采用字节码的好处:**
|
|
||||||
|
|
||||||
Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。
|
|
||||||
|
|
||||||
### Java和C++的区别
|
|
||||||
|
|
||||||
我知道很多人没学过C++,但是面试官就是没事喜欢拿咱们Java和C++比呀!没办法!!!就算没学过C++,也要记下来!
|
|
||||||
|
|
||||||
- 都是面向对象的语言,都支持封装、继承和多态
|
|
||||||
- Java不提供指针来直接访问内存,程序内存更加安全
|
|
||||||
- Java的类是单继承的,C++支持多重继承;虽然Java的类不可以多继承,但是接口可以多继承。
|
|
||||||
- Java有自动内存管理机制,不需要程序员手动释放无用内存
|
|
||||||
|
|
||||||
|
|
||||||
### 接口和抽象类的区别是什么?
|
|
||||||
|
|
||||||
1. 接口的方法默认是public,所有方法在接口中不能有实现,抽象类可以有非抽象的方法
|
|
||||||
2. 接口中的实例变量默认是final类型的,而抽象类中则不一定
|
|
||||||
3. 一个类可以实现多个接口,但最多只能实现一个抽象类
|
|
||||||
4. 一个类实现接口的话要实现接口的所有方法,而抽象类不一定
|
|
||||||
5. 接口不能用new实例化,但可以声明,但是必须引用一个实现该接口的对象 从设计层面来说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。
|
|
||||||
|
|
||||||
注意:Java8 后接口可以有默认实现( default )。
|
|
||||||
|
|
||||||
### 成员变量与局部变量的区别有那些?
|
|
||||||
|
|
||||||
1. 从语法形式上,看成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;成员变量可以被public,private,static等修饰符所修饰,而局部变量不能被访问控制修饰符及static所修饰;但是,成员变量和局部变量都能被final所修饰;
|
|
||||||
2. 从变量在内存中的存储方式来看,成员变量是对象的一部分,而对象存在于堆内存,局部变量存在于栈内存
|
|
||||||
3. 从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。
|
|
||||||
4. 成员变量如果没有被赋初值,则会自动以类型的默认值而赋值(一种情况例外被final修饰但没有被static修饰的成员变量必须显示地赋值);而局部变量则不会自动赋值。
|
|
||||||
|
|
||||||
### 重载和重写的区别
|
|
||||||
|
|
||||||
**重载:** 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。
|
|
||||||
|
|
||||||
**重写:** 发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为private则子类就不能重写该方法。
|
|
||||||
|
|
||||||
### 字符型常量和字符串常量的区别
|
|
||||||
1) 形式上:
|
|
||||||
字符常量是单引号引起的一个字符
|
|
||||||
字符串常量是双引号引起的若干个字符
|
|
||||||
2) 含义上:
|
|
||||||
字符常量相当于一个整形值(ASCII值),可以参加表达式运算
|
|
||||||
字符串常量代表一个地址值(该字符串在内存中存放位置)
|
|
||||||
3) 占内存大小
|
|
||||||
字符常量只占一个字节
|
|
||||||
字符串常量占若干个字节(至少一个字符结束标志)
|
|
@ -1,195 +0,0 @@
|
|||||||
|
|
||||||
## 1. 简述线程,程序、进程的基本概念。以及他们之间关系是什么?
|
|
||||||
|
|
||||||
**线程**与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
|
|
||||||
|
|
||||||
**程序**是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
|
|
||||||
|
|
||||||
**进程**是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。
|
|
||||||
|
|
||||||
**线程** 是 **进程** 划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。
|
|
||||||
|
|
||||||
**线程上下文的切换比进程上下文切换要快很多**
|
|
||||||
|
|
||||||
- 进程切换时,涉及到当前进程的CPU环境的保存和新被调度运行进程的CPU环境的设置。
|
|
||||||
- 线程切换仅需要保存和设置少量的寄存器内容,不涉及存储管理方面的操作。
|
|
||||||
|
|
||||||
## 2. 线程有哪些基本状态?这些状态是如何定义的?
|
|
||||||
|
|
||||||
1. **新建(new)**:新创建了一个线程对象。
|
|
||||||
2. **可运行(runnable)**:线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获 取cpu的使用权。
|
|
||||||
3. **运行(running)**:可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。
|
|
||||||
4. **阻塞(block)**:阻塞状态是指线程因为某种原因放弃了cpu使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有 机会再次获得cpu timeslice转到运行(running)状态。阻塞的情况分三种:
|
|
||||||
- **(一). 等待阻塞**:运行(running)的线程执行o.wait()方法,JVM会把该线程放 入等待队列(waiting queue)中。
|
|
||||||
- **(二). 同步阻塞**:运行(running)的线程在获取对象的同步锁时,若该同步 锁 被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
|
|
||||||
- **(三). 其他阻塞**: 运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
|
|
||||||
5. **死亡(dead)**:线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
备注: 可以用早起坐地铁来比喻这个过程(下面参考自牛客网某位同学的回答):
|
|
||||||
|
|
||||||
1. 还没起床:sleeping
|
|
||||||
2. 起床收拾好了,随时可以坐地铁出发:Runnable
|
|
||||||
3. 等地铁来:Waiting
|
|
||||||
4. 地铁来了,但要排队上地铁:I/O阻塞
|
|
||||||
5. 上了地铁,发现暂时没座位:synchronized阻塞
|
|
||||||
6. 地铁上找到座位:Running
|
|
||||||
7. 到达目的地:Dead
|
|
||||||
|
|
||||||
|
|
||||||
## 3. 何为多线程?
|
|
||||||
|
|
||||||
多线程就是多个线程同时运行或交替运行。单核CPU的话是顺序执行,也就是交替运行。多核CPU的话,因为每个CPU有自己的运算器,所以在多个CPU中可以同时运行。
|
|
||||||
|
|
||||||
|
|
||||||
## 4. 为什么多线程是必要的?
|
|
||||||
|
|
||||||
1. 使用线程可以把占据长时间的程序中的任务放到后台去处理。
|
|
||||||
2. 用户界面可以更加吸引人,这样比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度。
|
|
||||||
3. 程序的运行速度可能加快。
|
|
||||||
|
|
||||||
## 5 使用多线程常见的三种方式
|
|
||||||
|
|
||||||
### ①继承Thread类
|
|
||||||
|
|
||||||
MyThread.java
|
|
||||||
|
|
||||||
```java
|
|
||||||
public class MyThread extends Thread {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
super.run();
|
|
||||||
System.out.println("MyThread");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Run.java
|
|
||||||
|
|
||||||
```java
|
|
||||||
public class Run {
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
|
||||||
MyThread mythread = new MyThread();
|
|
||||||
mythread.start();
|
|
||||||
System.out.println("运行结束");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
运行结果:
|
|
||||||

|
|
||||||
从上面的运行结果可以看出:线程是一个子任务,CPU以不确定的方式,或者说是以随机的时间来调用线程中的run方法。
|
|
||||||
|
|
||||||
### ②实现Runnable接口
|
|
||||||
推荐实现Runnable接口方式开发多线程,因为Java单继承但是可以实现多个接口。
|
|
||||||
|
|
||||||
MyRunnable.java
|
|
||||||
|
|
||||||
```java
|
|
||||||
public class MyRunnable implements Runnable {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
System.out.println("MyRunnable");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Run.java
|
|
||||||
|
|
||||||
```java
|
|
||||||
public class Run {
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
|
||||||
Runnable runnable=new MyRunnable();
|
|
||||||
Thread thread=new Thread(runnable);
|
|
||||||
thread.start();
|
|
||||||
System.out.println("运行结束!");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
运行结果:
|
|
||||||

|
|
||||||
|
|
||||||
### ③使用线程池
|
|
||||||
|
|
||||||
**在《阿里巴巴Java开发手册》“并发处理”这一章节,明确指出线程资源必须通过线程池提供,不允许在应用中自行显示创建线程。**
|
|
||||||
|
|
||||||
**为什么呢?**
|
|
||||||
|
|
||||||
> **使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题。如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。**
|
|
||||||
|
|
||||||
**另外《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险**
|
|
||||||
|
|
||||||
> Executors 返回线程池对象的弊端如下:
|
|
||||||
>
|
|
||||||
> - **FixedThreadPool 和 SingleThreadExecutor** : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。
|
|
||||||
> - **CachedThreadPool 和 ScheduledThreadPool** : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。
|
|
||||||
|
|
||||||
对于线程池感兴趣的可以查看我的这篇文章:[《Java多线程学习(八)线程池与Executor 框架》](http://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247484042&idx=1&sn=541dbf2cb969a151d79f4a4f837ee1bd&chksm=fd9854ebcaefddfd1876bb96ab218be3ae7b12546695a403075d4ed22e5e17ff30ebdabc8bbf#rd) 点击阅读原文即可查看到该文章的最新版。
|
|
||||||
|
|
||||||
|
|
||||||
## 6 线程的优先级
|
|
||||||
|
|
||||||
每个线程都具有各自的优先级,**线程的优先级可以在程序中表明该线程的重要性,如果有很多线程处于就绪状态,系统会根据优先级来决定首先使哪个线程进入运行状态**。但这个并不意味着低
|
|
||||||
优先级的线程得不到运行,而只是它运行的几率比较小,如垃圾回收机制线程的优先级就比较低。所以很多垃圾得不到及时的回收处理。
|
|
||||||
|
|
||||||
**线程优先级具有继承特性。** 比如A线程启动B线程,则B线程的优先级和A是一样的。
|
|
||||||
|
|
||||||
**线程优先级具有随机性。** 也就是说线程优先级高的不一定每一次都先执行完。
|
|
||||||
|
|
||||||
Thread类中包含的成员变量代表了线程的某些优先级。如**Thread.MIN_PRIORITY(常数1)**,**Thread.NORM_PRIORITY(常数5)**,
|
|
||||||
**Thread.MAX_PRIORITY(常数10)**。其中每个线程的优先级都在**Thread.MIN_PRIORITY(常数1)** 到**Thread.MAX_PRIORITY(常数10)** 之间,在默认情况下优先级都是**Thread.NORM_PRIORITY(常数5)**。
|
|
||||||
|
|
||||||
学过操作系统这门课程的话,我们可以发现多线程优先级或多或少借鉴了操作系统对进程的管理。
|
|
||||||
|
|
||||||
|
|
||||||
## 7 Java多线程分类
|
|
||||||
|
|
||||||
### 用户线程
|
|
||||||
|
|
||||||
运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程
|
|
||||||
|
|
||||||
### 守护线程
|
|
||||||
|
|
||||||
运行在后台,为其他前台线程服务.也可以说守护线程是JVM中非守护线程的 **“佣人”**。
|
|
||||||
|
|
||||||
|
|
||||||
- **特点:** 一旦所有用户线程都结束运行,守护线程会随JVM一起结束工作
|
|
||||||
- **应用:** 数据库连接池中的检测线程,JVM虚拟机启动后的检测线程
|
|
||||||
- **最常见的守护线程:** 垃圾回收线程
|
|
||||||
|
|
||||||
|
|
||||||
**如何设置守护线程?**
|
|
||||||
|
|
||||||
可以通过调用 Thead 类的 `setDaemon(true)` 方法设置当前的线程为守护线程。
|
|
||||||
|
|
||||||
注意事项:
|
|
||||||
|
|
||||||
1. setDaemon(true)必须在start()方法前执行,否则会抛出IllegalThreadStateException异常
|
|
||||||
2. 在守护线程中产生的新线程也是守护线程
|
|
||||||
3. 不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑
|
|
||||||
|
|
||||||
|
|
||||||
## 8 sleep()方法和wait()方法简单对比
|
|
||||||
|
|
||||||
- 两者最主要的区别在于:**sleep方法没有释放锁,而wait方法释放了锁** 。
|
|
||||||
- 两者都可以暂停线程的执行。
|
|
||||||
- Wait通常被用于线程间交互/通信,sleep通常被用于暂停执行。
|
|
||||||
- wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll()方法。sleep()方法执行完成后,线程会自动苏醒。
|
|
||||||
|
|
||||||
|
|
||||||
## 9 为什么我们调用start()方法时会执行run()方法,为什么我们不能直接调用run()方法?
|
|
||||||
|
|
||||||
这是另一个非常经典的java多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!
|
|
||||||
|
|
||||||
new一个Thread,线程进入了新建状态;调用start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。
|
|
||||||
start()会执行线程的相应准备工作,然后自动执行run()方法的内容,这是真正的多线程工作。 而直接执行run()方法,会把run方法当成一个mian线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
|
|
||||||
|
|
||||||
**总结: 调用start方法方可启动线程并使线程进入就绪状态,而run方法只是thread的一个普通方法调用,还是在主线程里执行。**
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,78 +1,89 @@
|
|||||||
最近浏览 Github ,收藏了一些还算不错的 Java面试/学习相关的仓库,分享给大家,希望对你有帮助。我暂且按照目前的 Star 数量来排序。
|
昨天我整理了公众号历史所有和面试相关的我觉得还不错的文章:[整理了一些有助于你拿Offer的文章](<https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485434&idx=1&sn=f6bdf19d2594bf719e149e48d1384340&chksm=cea24831f9d5c1278617d347238f65f0481f36291675f05fabb382b69ea0ff3adae7ee6e6524&token=1452779379&lang=zh_CN#rd>) 。今天分享一下最近逛Github看到了一些我觉得对于Java面试以及学习有帮助的仓库,这些仓库涉及Java核心知识点整理、Java常见面试题、算法、基础知识点比如网络和操作系统等等。
|
||||||
|
|
||||||
本文由 SnailClimb 整理,如需转载请联系作者。
|
## 知识点相关
|
||||||
|
|
||||||
### 1. interviews
|
### 1.JavaGuide
|
||||||
|
|
||||||
- Github地址: [https://github.com/kdn251/interviews/blob/master/README-zh-cn.md](https://github.com/kdn251/interviews/blob/master/README-zh-cn.md)
|
|
||||||
- star: 31k
|
|
||||||
- 介绍: 软件工程技术面试个人指南。
|
|
||||||
- 概览:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### 2. JCSprout
|
|
||||||
|
|
||||||
- Github地址:[https://github.com/crossoverJie/JCSprout](https://github.com/crossoverJie/JCSprout)
|
|
||||||
- star: 17.7k
|
|
||||||
- 介绍: Java Core Sprout:处于萌芽阶段的 Java 核心知识库。
|
|
||||||
- 概览:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### 3. JavaGuide
|
|
||||||
|
|
||||||
- Github地址: [https://github.com/Snailclimb/JavaGuide](https://github.com/Snailclimb/JavaGuide)
|
- Github地址: [https://github.com/Snailclimb/JavaGuide](https://github.com/Snailclimb/JavaGuide)
|
||||||
- star: 17.4k
|
- star: 64.0k
|
||||||
- 介绍: 【Java学习+面试指南】 一份涵盖大部分Java程序员所需要掌握的核心知识。
|
- 介绍: 【Java学习+面试指南】 一份涵盖大部分Java程序员所需要掌握的核心知识。
|
||||||
- 概览:
|
|
||||||
|
|
||||||

|
### 2.CS-Notes
|
||||||
|
|
||||||
### 4. technology-talk
|
- Github 地址:<https://github.com/CyC2018/CS-Notes>
|
||||||
|
- Star: 68.3k
|
||||||
|
- 介绍: 技术面试必备基础知识、Leetcode 题解、后端面试、Java 面试、春招、秋招、操作系统、计算机网络、系统设计。
|
||||||
|
|
||||||
- Github地址: [https://github.com/aalansehaiyang/technology-talk](https://github.com/aalansehaiyang/technology-talk)
|
### 3. advanced-java
|
||||||
- star: 4.2k
|
|
||||||
- 介绍: 汇总java生态圈常用技术框架、开源中间件,系统架构、项目管理、经典架构案例、数据库、常用三方库、线上运维等知识。
|
|
||||||
|
|
||||||
### 5. fullstack-tutorial
|
|
||||||
|
|
||||||
- Github地址: [https://github.com/frank-lam/fullstack-tutorial](https://github.com/frank-lam/fullstack-tutorial)
|
|
||||||
- star: 2.8k
|
|
||||||
- 介绍: Full Stack Developer Tutorial,后台技术栈/全栈开发/架构师之路,秋招/春招/校招/面试。 from zero to hero。
|
|
||||||
- 概览:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### 6. java-bible
|
|
||||||
|
|
||||||
- Github地址:[https://github.com/biezhi/java-bible](https://github.com/biezhi/java-bible)
|
|
||||||
- star: 1.9k
|
|
||||||
- 介绍: 这里记录了一些技术摘要,部分文章来自网络,本项目的目的力求分享精品技术干货,以Java为主。
|
|
||||||
- 概览:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### 7. EasyJob
|
|
||||||
|
|
||||||
- Github地址:[https://github.com/it-interview/EasyJob](https://github.com/it-interview/EasyJob)
|
|
||||||
- star: 1.9k
|
|
||||||
- 介绍: 互联网求职面试题、知识点和面经整理。
|
|
||||||
|
|
||||||
### 8. advanced-java
|
|
||||||
|
|
||||||
- Github地址:[https://github.com/doocs/advanced-java](https://github.com/doocs/advanced-java)
|
- Github地址:[https://github.com/doocs/advanced-java](https://github.com/doocs/advanced-java)
|
||||||
- star: 1k
|
- star: 23.4k
|
||||||
- 介绍: 互联网 Java 工程师进阶知识完全扫盲
|
- 介绍: 互联网 Java 工程师进阶知识完全扫盲:涵盖高并发、分布式、高可用、微服务等领域知识,后端同学必看,前端同学也可学习。
|
||||||
|
|
||||||
### 9. 3y
|
### 4.JCSprout
|
||||||
|
|
||||||
|
- Github地址:[https://github.com/crossoverJie/JCSprout](https://github.com/crossoverJie/JCSprout)
|
||||||
|
- star: 21.2k
|
||||||
|
- 介绍: Java Core Sprout:处于萌芽阶段的 Java 核心知识库。
|
||||||
|
|
||||||
|
### 5.toBeTopJavaer
|
||||||
|
|
||||||
|
- Github地址:[https://github.com/hollischuang/toBeTopJavaer](https://github.com/hollischuang/toBeTopJavaer)
|
||||||
|
- star: 4.0 k
|
||||||
|
- 介绍: Java工程师成神之路。
|
||||||
|
|
||||||
|
### 6.architect-awesome
|
||||||
|
|
||||||
|
- Github地址:[https://github.com/xingshaocheng/architect-awesome](https://github.com/xingshaocheng/architect-awesome)
|
||||||
|
- star: 34.4 k
|
||||||
|
- 介绍:后端架构师技术图谱。
|
||||||
|
|
||||||
|
### 7.technology-talk
|
||||||
|
|
||||||
|
- Github地址: [https://github.com/aalansehaiyang/technology-talk](https://github.com/aalansehaiyang/technology-talk)
|
||||||
|
- star: 6.1k
|
||||||
|
- 介绍: 汇总java生态圈常用技术框架、开源中间件,系统架构、项目管理、经典架构案例、数据库、常用三方库、线上运维等知识。
|
||||||
|
|
||||||
|
### 8.fullstack-tutorial
|
||||||
|
|
||||||
|
- Github地址: [https://github.com/frank-lam/fullstack-tutorial](https://github.com/frank-lam/fullstack-tutorial)
|
||||||
|
- star: 4.0k
|
||||||
|
- 介绍: fullstack tutorial 2019,后台技术栈/架构师之路/全栈开发社区,春招/秋招/校招/面试。
|
||||||
|
|
||||||
|
### 9.3y
|
||||||
|
|
||||||
- Github地址:[https://github.com/ZhongFuCheng3y/3y](https://github.com/ZhongFuCheng3y/3y)
|
- Github地址:[https://github.com/ZhongFuCheng3y/3y](https://github.com/ZhongFuCheng3y/3y)
|
||||||
- star: 0.4 k
|
- star: 1.9 k
|
||||||
- 介绍: Java 知识整合。
|
- 介绍: Java 知识整合。
|
||||||
|
|
||||||
除了这九个仓库,再推荐几个不错的学习方向的仓库给大家。
|
### 10.java-bible
|
||||||
|
|
||||||
1. Star 数高达 4w+的 CS 笔记-CS-Notes:[https://github.com/CyC2018/CS-Notes](https://github.com/CyC2018/CS-Notes)
|
- Github地址:[https://github.com/biezhi/java-bible](https://github.com/biezhi/java-bible)
|
||||||
2. 后端(尤其是Java)程序员的 Linux 学习仓库-Linux-Tutorial:[https://github.com/judasn/Linux-Tutorial](https://github.com/judasn/Linux-Tutorial)( Star:4.6k)
|
- star: 2.3k
|
||||||
3. 两个算法相关的仓库,刷 Leetcode 的小伙伴必备:①awesome-java-leetcode:[https://github.com/Blankj/awesome-java-leetcode](https://github.com/Blankj/awesome-java-leetcode);②LintCode:[https://github.com/awangdev/LintCode](https://github.com/awangdev/LintCode)
|
- 介绍: 这里记录了一些技术摘要,部分文章来自网络,本项目的目的力求分享精品技术干货,以Java为主。
|
||||||
|
|
||||||
|
### 11.interviews
|
||||||
|
|
||||||
|
- Github地址: [https://github.com/kdn251/interviews/blob/master/README-zh-cn.md](https://github.com/kdn251/interviews/blob/master/README-zh-cn.md)
|
||||||
|
- star: 35.3k
|
||||||
|
- 介绍: 软件工程技术面试个人指南(国外的一个项目,虽然有翻译版,但是不太推荐,因为很多内容并不适用于国内)。
|
||||||
|
|
||||||
|
## 算法相关
|
||||||
|
|
||||||
|
### 1.LeetCodeAnimation
|
||||||
|
|
||||||
|
- Github 地址: <https://github.com/MisterBooo/LeetCodeAnimation>
|
||||||
|
- Star: 33.4k
|
||||||
|
- 介绍: Demonstrate all the questions on LeetCode in the form of animation.(用动画的形式呈现解LeetCode题目的思路)。
|
||||||
|
|
||||||
|
### 2.awesome-java-leetcode
|
||||||
|
|
||||||
|
- Github地址:[https://github.com/Blankj/awesome-java-leetcode](https://github.com/Blankj/awesome-java-leetcode)
|
||||||
|
- star: 6.1k
|
||||||
|
- 介绍: LeetCode 上 Facebook 的面试题目。
|
||||||
|
|
||||||
|
### 3.leetcode
|
||||||
|
|
||||||
|
- Github地址:[https://github.com/azl397985856/leetcode](https://github.com/azl397985856/leetcode)
|
||||||
|
- star: 12.0k
|
||||||
|
- 介绍: LeetCode Solutions: A Record of My Problem Solving Journey.( leetcode题解,记录自己的leetcode解题之路。)
|
@ -1,81 +1,101 @@
|
|||||||
身边的朋友或者公众号的粉丝很多人都向我询问过:“我是双非/三本/专科学校的,我有机会进入大厂吗?”、“非计算机专业的学生能学好吗?”、“如何学习Java?”、“Java学习该学那些东西?”、“我该如何准备Java面试?”......这些方面的问题。我会根据自己的一点经验对大部分人关心的这些问题进行答疑解惑。现在又刚好赶上考研结束,这篇文章也算是给考研结束准备往Java后端方向发展的朋友们指名一条学习之路。道理懂了如果没有实际行动,那这篇文章对你或许没有任何意义。
|
身边的朋友或者公众号的粉丝很多人都向我询问过:“我是双非/三本/专科学校的,我有机会进入大厂吗?”、“非计算机专业的学生能学好吗?”、“如何学习 Java?”、“Java 学习该学哪些东西?”、“我该如何准备 Java 面试?”......这些方面的问题。我会根据自己的一点经验对大部分人关心的这些问题进行答疑解惑。现在又刚好赶上考研结束,这篇文章也算是给考研结束准备往 Java 后端方向发展的朋友们指明一条学习之路。道理懂了如果没有实际行动,那这篇文章对你或许没有任何意义。
|
||||||
|
|
||||||
|
<!-- TOC -->
|
||||||
|
|
||||||
|
- [Question1:我是双非/三本/专科学校的,我有机会进入大厂吗?](#question1我是双非三本专科学校的我有机会进入大厂吗)
|
||||||
|
- [Question2:非计算机专业的学生能学好 Java 后台吗?我能进大厂吗?](#question2非计算机专业的学生能学好-java-后台吗我能进大厂吗)
|
||||||
|
- [Question3: 我没有实习经历的话找工作是不是特别艰难?](#question3-我没有实习经历的话找工作是不是特别艰难)
|
||||||
|
- [Question4: 我该如何准备面试呢?面试的注意事项有哪些呢?](#question4-我该如何准备面试呢面试的注意事项有哪些呢)
|
||||||
|
- [Question5: 我该自学还是报培训班呢?](#question5-我该自学还是报培训班呢)
|
||||||
|
- [Question6: 没有项目经历/博客/Github 开源项目怎么办?](#question6-没有项目经历博客github-开源项目怎么办)
|
||||||
|
- [Question7: 大厂青睐什么样的人?](#question7-大厂青睐什么样的人)
|
||||||
|
|
||||||
|
<!-- /TOC -->
|
||||||
|
|
||||||
### Question1:我是双非/三本/专科学校的,我有机会进入大厂吗?
|
### Question1:我是双非/三本/专科学校的,我有机会进入大厂吗?
|
||||||
|
|
||||||
我自己也是非985非211学校的,结合自己的经历以及一些朋友的经历,我觉得让我回答这个问题再好不过。
|
我自己也是非 985 非 211 学校的,结合自己的经历以及一些朋友的经历,我觉得让我回答这个问题再好不过。
|
||||||
|
|
||||||
首先,我觉得学校歧视很正常,真的太正常了,如果要抱怨的话,你只能抱怨自己没有进入名校。但是,千万不要动不动说自己学校差,动不动拿自己学校当做自己进不了大厂的借口,学历只是筛选简历的很多标准中的一个而已,如果你够优秀,简历够丰富,你也一样可以和名校同学一起同台竞争。
|
首先,我觉得学校歧视很正常,真的太正常了,如果要抱怨的话,你只能抱怨自己没有进入名校。但是,千万不要动不动说自己学校差,动不动拿自己学校当做自己进不了大厂的借口,学历只是筛选简历的很多标准中的一个而已,如果你够优秀,简历够丰富,你也一样可以和名校同学一起同台竞争。
|
||||||
|
|
||||||
企业HR肯定是更喜欢高学历的人,毕竟985,211优秀人才比例肯定比普通学校高很多,HR团队肯定会优先在这些学校里选。这就好比相亲,你是愿意在很多优秀的人中选一个优秀的,还是愿意在很多普通的人中选一个优秀的呢?
|
企业 HR 肯定是更喜欢高学历的人,毕竟 985、211 优秀人才比例肯定比普通学校高很多,HR 团队肯定会优先在这些学校里选。这就好比相亲,你是愿意在很多优秀的人中选一个优秀的,还是愿意在很多普通的人中选一个优秀的呢?
|
||||||
|
|
||||||
双非本科甚至是二本、三本甚至是专科的同学也有很多进入大厂的,不过比率相比于名校的低很多而已。从大厂招聘的结果上看,高学历人才的数量占据大头,那些成功进入BAT、美团,京东,网易等大厂的双非本科甚至是二本、三本甚至是专科的同学往往是因为具备丰富的项目经历或者在某个含金量比较高的竞赛比如ACM中取得了不错的成绩。**一部分学历不突出但能力出众的面试者能够进入大厂并不是说明学历不重要,而是学历的软肋能够通过其他的优势来弥补。** 所以,如果你的学校不够好而你自己又想去大厂的话,建议你可以从这几点来做:**①尽量在面试前最好有一个可以拿的出手的项目;②有实习条件的话,尽早出去实习,实习经历也会是你的简历的一个亮点(有能力在大厂实习最佳!);③参加一些含金量比较高的比赛,拿不拿得到名次没关系,重在锻炼。**
|
|
||||||
|
|
||||||
|
双非本科甚至是二本、三本甚至是专科的同学也有很多进入大厂的,不过比率相比于名校的低很多而已。从大厂招聘的结果上看,高学历人才的数量占据大头,那些成功进入 BAT、美团,京东,网易等大厂的双非本科甚至是二本、三本甚至是专科的同学往往是因为具备丰富的项目经历或者在某个含金量比较高的竞赛比如 ACM 中取得了不错的成绩。**一部分学历不突出但能力出众的面试者能够进入大厂并不是说明学历不重要,而是学历的软肋能够通过其他的优势来弥补。** 所以,如果你的学校不够好而你自己又想去大厂的话,建议你可以从这几点来做:**① 尽量在面试前最好有一个可以拿的出手的项目;② 有实习条件的话,尽早出去实习,实习经历也会是你的简历的一个亮点(有能力在大厂实习最佳!);③ 参加一些含金量比较高的比赛,拿不拿得到名次没关系,重在锻炼。**
|
||||||
|
|
||||||
### Question2:非计算机专业的学生能学好Java后台吗?我能进大厂吗?
|
### Question2:非计算机专业的学生能学好 Java 后台吗?我能进大厂吗?
|
||||||
|
|
||||||
当然可以!现在非科班的程序员很多,很大一部分原因是互联网行业的工资比较高。我们学校外面的培训班里面90%都是非科班,我觉得他们很多人学的都还不错。另外,我的一个朋友本科是机械专业,大一开始自学安卓,技术贼溜,在我看来他比大部分本科是计算机的同学学的还要好。参考Question1的回答,即使你是非科班程序员,如果你想进入大厂的话,你也可以通过自己的其他优势来弥补。
|
当然可以!现在非科班的程序员很多,很大一部分原因是互联网行业的工资比较高。我们学校外面的培训班里面 90%都是非科班,我觉得他们很多人学的都还不错。另外,我的一个朋友本科是机械专业,大一开始自学安卓,技术贼溜,在我看来他比大部分本科是计算机的同学学的还要好。参考 Question1 的回答,即使你是非科班程序员,如果你想进入大厂的话,你也可以通过自己的其他优势来弥补。
|
||||||
|
|
||||||
我觉得我们不应该因为自己的专业给自己划界限或者贴标签,说实话,很多科班的同学可能并不如你,你以为科班的同学就会认真听讲吗?还不是几乎全靠自己课下自学!不过如果你是非科班的话,你想要学好,那么注定就要舍弃自己本专业的一些学习时间,这是无可厚非的。
|
|
||||||
|
|
||||||
建议非科班的同学,首先要打好计算机基础知识基础:①计算机网络、②操作系统、③数据机构与算法,我个人觉得这3个对你最重要。这些东西就像是内功,对你以后的长远发展非常有用。当然,如果你想要进大厂的话,这些知识也是一定会被问到的。另外,“一定学好数据机构与算法!一定学好数据机构与算法!一定学好数据机构与算法!”,重要的东西说3遍。
|
|
||||||
|
|
||||||
|
我觉得我们不应该因为自己的专业给自己划界限或者贴标签,说实话,很多科班的同学可能并不如你,你以为科班的同学就会认真听讲吗?还不是几乎全靠自己课下自学!不过如果你是非科班的话,你想要学好,那么注定就要舍弃自己本专业的一些学习时间,这是无可厚非的。
|
||||||
|
|
||||||
|
建议非科班的同学,首先要打好计算机基础知识基础:① 计算机网络、② 操作系统、③ 数据机构与算法,我个人觉得这 3 个对你最重要。这些东西就像是内功,对你以后的长远发展非常有用。当然,如果你想要进大厂的话,这些知识也是一定会被问到的。另外,“一定学好数据结构与算法!一定学好数据结构与算法!一定学好数据结构与算法!”,重要的东西说 3 遍。
|
||||||
|
|
||||||
### Question3: 我没有实习经历的话找工作是不是特别艰难?
|
### Question3: 我没有实习经历的话找工作是不是特别艰难?
|
||||||
|
|
||||||
没有实习经历没关系,只要你有拿得出手的项目或者大赛经历的话,你依然有可能拿到大厂的 offer 。笔主当时找工作的时候就没有实习经历以及大赛获奖经历,单纯就是凭借自己的项目经验撑起了整个面试。
|
没有实习经历没关系,只要你有拿得出手的项目或者大赛经历的话,你依然有可能拿到大厂的 offer 。笔主当时找工作的时候就没有实习经历以及大赛获奖经历,单纯就是凭借自己的项目经验撑起了整个面试。
|
||||||
|
|
||||||
如果你既没有实习经历,又没有拿得出手的项目或者大赛经历的话,我觉得在简历关,除非你有其他特别的亮点,不然,你应该就会被刷。
|
如果你既没有实习经历,又没有拿得出手的项目或者大赛经历的话,我觉得在简历关,除非你有其他特别的亮点,不然,你应该就会被刷。
|
||||||
|
|
||||||
### Question4: 我该如何准备面试呢?面试的注意事项有哪些呢?
|
### Question4: 我该如何准备面试呢?面试的注意事项有哪些呢?
|
||||||
|
|
||||||
下面是我总结的一些准备面试的Tips以及面试必备的注意事项:
|
下面是我总结的一些准备面试的 Tips 以及面试必备的注意事项:
|
||||||
|
|
||||||
1. **准备一份自己的自我介绍,面试的时候根据面试对象适当进行修改**(突出重点,突出自己的优势在哪里,切忌流水账);
|
1. **准备一份自己的自我介绍,面试的时候根据面试对象适当进行修改**(突出重点,突出自己的优势在哪里,切忌流水账);
|
||||||
2. **注意随身带上自己的成绩单和简历复印件;** (有的公司在面试前都会让你交一份成绩单和简历当做面试中的参考。)
|
2. **注意随身带上自己的成绩单和简历复印件;** (有的公司在面试前都会让你交一份成绩单和简历当做面试中的参考。)
|
||||||
3. **如果需要笔试就提前刷一些笔试题,大部分在线笔试的类型是选择题+编程题,有的还会有简答题。**(平时空闲时间多的可以刷一下笔试题目(牛客网上有很多),但是不要只刷面试题,不动手code,程序员不是为了考试而存在的。)另外,注意抓重点,因为题目太多了,但是有很多题目几乎次次遇到,像这样的题目一定要搞定。
|
3. **如果需要笔试就提前刷一些笔试题,大部分在线笔试的类型是选择题+编程题,有的还会有简答题。**(平时空闲时间多的可以刷一下笔试题目(牛客网上有很多),但是不要只刷面试题,不动手 code,程序员不是为了考试而存在的。)另外,注意抓重点,因为题目太多了,但是有很多题目几乎次次遇到,像这样的题目一定要搞定。
|
||||||
4. **提前准备技术面试。** 搞清楚自己面试中可能涉及哪些知识点、那些知识点是重点。面试中哪些问题会被经常问到、自己改如何回答。(强烈不推荐背题,第一:通过背这种方式你能记住多少?能记住多久?第二:背题的方式的学习很难坚持下去!)
|
4. **提前准备技术面试。** 搞清楚自己面试中可能涉及哪些知识点、哪些知识点是重点。面试中哪些问题会被经常问到、自己该如何回答。(强烈不推荐背题,第一:通过背这种方式你能记住多少?能记住多久?第二:背题的方式的学习很难坚持下去!)
|
||||||
5. **面试之前做好定向复习。** 也就是专门针对你要面试的公司来复习。比如你在面试之前可以在网上找找有没有你要面试的公司的面经。
|
5. **面试之前做好定向复习。** 也就是专门针对你要面试的公司来复习。比如你在面试之前可以在网上找找有没有你要面试的公司的面经。
|
||||||
6. **准备好自己的项目介绍。** 如果有项目的话,技术面试第一步,面试官一般都是让你自己介绍一下你的项目。你可以从下面几个方向来考虑:①对项目整体设计的一个感受(面试官可能会让你画系统的架构图;②在这个项目中你负责了什么、做了什么、担任了什么角色;③ 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用;④项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个棘手的问题的时候你是如何解决的又或者说你在这个项目用了什么技术实现了什么功能比如:用redis做缓存提高访问速度和并发量、使用消息队列削峰和降流等等。
|
6. **准备好自己的项目介绍。** 如果有项目的话,技术面试第一步,面试官一般都是让你自己介绍一下你的项目。你可以从下面几个方向来考虑:① 对项目整体设计的一个感受(面试官可能会让你画系统的架构图);② 在这个项目中你负责了什么、做了什么、担任了什么角色;③ 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用;④ 项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个棘手的问题的时候你是如何解决的又或者说你在这个项目用了什么技术实现了什么功能比如:用 redis 做缓存提高访问速度和并发量、使用消息队列削峰和降流等等。
|
||||||
7. **面试之后记得复盘。** 面试遭遇失败是很正常的事情,所以善于总结自己的失败原因才是最重要的。如果失败,不要灰心;如果通过,切勿狂喜。
|
7. **面试之后记得复盘。** 面试遭遇失败是很正常的事情,所以善于总结自己的失败原因才是最重要的。如果失败,不要灰心;如果通过,切勿狂喜。
|
||||||
|
|
||||||
|
**一些还算不错的 Java 面试/学习相关的仓库,相信对大家准备面试一定有帮助:**[盘点一下 Github 上开源的 Java 面试/学习相关的仓库,看完弄懂薪资至少增加 10k](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247484817&idx=1&sn=12f0c254a240c40c2ccab8314653216b&chksm=fd9853f0caefdae6d191e6bf085d44ab9c73f165e3323aa0362d830e420ccbfad93aa5901021&token=766994974&lang=zh_CN#rd)
|
||||||
**一些还算不错的 Java面试/学习相关的仓库,相信对大家准备面试一定有帮助:**[盘点一下Github上开源的Java面试/学习相关的仓库,看完弄懂薪资至少增加10k](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247484817&idx=1&sn=12f0c254a240c40c2ccab8314653216b&chksm=fd9853f0caefdae6d191e6bf085d44ab9c73f165e3323aa0362d830e420ccbfad93aa5901021&token=766994974&lang=zh_CN#rd)
|
|
||||||
|
|
||||||
### Question5: 我该自学还是报培训班呢?
|
### Question5: 我该自学还是报培训班呢?
|
||||||
|
|
||||||
我本人更加赞同自学(你要知道去了公司可没人手把手教你了,而且几乎所有的公司都对培训班出生的有偏见。为什么有偏见,你学个东西还要去培训班,说明什么,同等水平下,你的自学能力以及自律能力一定是比不上自学的人的)。但是如果,你连每天在寝室坚持学上8个小时以上都坚持不了,或者总是容易半途而废的话,我还是推荐你去培训班。观望身边同学去培训班的,大多是非计算机专业或者是没有自律能力以及自学能力非常差的人。
|
我本人更加赞同自学(你要知道去了公司可没人手把手教你了,而且几乎所有的公司都对培训班出生的有偏见。为什么有偏见,你学个东西还要去培训班,说明什么,同等水平下,你的自学能力以及自律能力一定是比不上自学的人的)。但是如果,你连每天在寝室坚持学上 8 个小时以上都坚持不了,或者总是容易半途而废的话,我还是推荐你去培训班。观望身边同学去培训班的,大多是非计算机专业或者是没有自律能力以及自学能力非常差的人。
|
||||||
|
|
||||||
另外,如果自律能力不行,你也可以通过结伴学习、参加老师的项目等方式来督促自己学习。
|
另外,如果自律能力不行,你也可以通过结伴学习、参加老师的项目等方式来督促自己学习。
|
||||||
|
|
||||||
总结:去不去培训班主要还是看自己,如果自己能坚持自学就自学,坚持不下来就去培训班。
|
总结:去不去培训班主要还是看自己,如果自己能坚持自学就自学,坚持不下来就去培训班。
|
||||||
|
|
||||||
### Question6: 没有项目经历/博客/Github开源项目怎么办?
|
### Question6: 没有项目经历/博客/Github 开源项目怎么办?
|
||||||
|
|
||||||
从现在开始做!
|
从现在开始做!
|
||||||
|
|
||||||
网上有很多非常不错的项目视频,你就跟着一步一步做,不光要做,还要改进,改善。另外,如果你的老师有相关 Java 后台项目的话,你也可以主动申请参与进来。
|
网上有很多非常不错的项目视频,你就跟着一步一步做,不光要做,还要改进,改善。另外,如果你的老师有相关 Java 后台项目的话,你也可以主动申请参与进来。
|
||||||
|
|
||||||
如果有自己的博客,也算是简历上的一个亮点。建议可以在掘金、Segmentfault、CSDN等技术交流社区写博客,当然,你也可以自己搭建一个博客(采用 Hexo+Githu Pages 搭建非常简单)。写一些什么?学习笔记、实战内容、读书笔记等等都可以。
|
如果有自己的博客,也算是简历上的一个亮点。建议可以在掘金、Segmentfault、CSDN 等技术交流社区写博客,当然,你也可以自己搭建一个博客(采用 Hexo+Githu Pages 搭建非常简单)。写一些什么?学习笔记、实战内容、读书笔记等等都可以。
|
||||||
|
|
||||||
多用 Github,用好 Github,上传自己不错的项目,写好 readme 文档,在其他技术社区做好宣传。相信你也会收获一个不错的开源项目!
|
多用 Github,用好 Github,上传自己不错的项目,写好 readme 文档,在其他技术社区做好宣传。相信你也会收获一个不错的开源项目!
|
||||||
|
|
||||||
|
### Question7: 大厂青睐什么样的人?
|
||||||
|
|
||||||
### Question7: 大厂到底青睐什么样的应届生?
|
**先从已经有两年左右开发经验的工程师角度来看:** 我们来看一下阿里官网支付宝 Java 高级开发工程师的招聘要求,从下面的招聘信息可以看出,除去 Java 基础/集合/多线程这些,这些能力格外重要:
|
||||||
|
|
||||||
从阿里、腾讯等大厂招聘官网对于Java后端方向/后端方向的应届实习生的要求,我们大概可以总结归纳出下面这 4 点能给简历增加很多分数:
|
1. **底层知识比如 jvm** :不只是懂理论更会实操;
|
||||||
|
2. 面**向对象编程能力** :我理解这个不仅包括“面向对象编程”,还有 SOLID 软件设计原则,相关阅读:[《写了这么多年代码,你真的了解 SOLID 吗?》](https://insights.thoughtworks.cn/do-you-really-know-solid/)(我司大佬的一篇文章)
|
||||||
|
3. **框架能力** :不只是使用那么简单,更要搞懂原理和机制!搞懂原理和机制的基础是要学会看源码。
|
||||||
|
4. **分布式系统开发能力** :缓存、消息队列等等都要掌握,关键是还要能使用这些技术解决实际问题而不是纸上谈兵。
|
||||||
|
5. **不错的 sense** :喜欢和尝试新技术、追求编写优雅的代码等等。
|
||||||
|
|
||||||
- 参加过竞赛(含金量超高的是ACM);
|

|
||||||
- 对数据结构与算法非常熟练;
|
|
||||||
- 参与过实际项目(比如学校网站);
|
|
||||||
- 参与过某个知名的开源项目或者自己的某个开源项目很不错;
|
|
||||||
|
|
||||||
除了我上面说的这三点,在面试Java工程师的时候,下面几点也提升你的个人竞争力:
|
**再从应届生的角度来看:** 我们还是看阿里巴巴的官网相关应届生 Java 工程师招聘岗位的相关要求。
|
||||||
|
|
||||||
- 熟悉Python、Shell、Perl等脚本语言;
|

|
||||||
- 熟悉 Java 优化,JVM调优;
|
|
||||||
- 熟悉 SOA 模式;
|
|
||||||
- 熟悉自己所用框架的底层知识比如Spring;
|
|
||||||
- 了解分布式一些常见的理论;
|
|
||||||
- 具备高并发开发经验;大数据开发经验等等。
|
|
||||||
|
|
||||||
|
结合阿里、腾讯等大厂招聘官网对于 Java 后端方向/后端方向的应届实习生的要求下面几点也提升你的个人竞争力:
|
||||||
|
|
||||||
|
1. 参加过竞赛( 含金量超高的是 ACM );
|
||||||
|
2. 对数据结构与算法非常熟练;
|
||||||
|
3. 参与过实际项目(比如学校网站)
|
||||||
|
4. 熟悉 Python、Shell、Perl 其中一门脚本语言;
|
||||||
|
5. 熟悉如何优化 Java 代码、有写出质量更高的代码的意识;
|
||||||
|
6. 熟悉 SOA 分布式相关的知识尤其是理论知识;
|
||||||
|
7. 熟悉自己所用框架的底层知识比如 Spring;
|
||||||
|
8. 有高并发开发经验;
|
||||||
|
9. 有大数据开发经验等等。
|
||||||
|
|
||||||
|
从来到大学之后,我的好多阅历非常深的老师经常就会告诫我们:“ 一定要有一门自己的特长,不管是技术还好还是其他能力 ” 。我觉得这句话真的非常有道理!
|
||||||
|
|
||||||
|
刚刚也提到了要有一门特长,所以在这里再强调一点:公司不需要你什么都会,但是在某一方面你一定要有过于常人的优点。换言之就是我们不需要去掌握每一门技术(你也没精力去掌握这么多技术),而是需要去深入研究某一门技术,对于其他技术我们可以简单了解一下。
|
||||||
|
@ -1,66 +0,0 @@
|
|||||||
|
|
||||||
### 核心基础知识
|
|
||||||
|
|
||||||
- [《图解HTTP》](https://book.douban.com/subject/25863515/)(推荐,豆瓣评分 8.1 , 1.6K+人评价): 讲漫画一样的讲HTTP,很有意思,不会觉得枯燥,大概也涵盖也HTTP常见的知识点。因为篇幅问题,内容可能不太全面。不过,如果不是专门做网络方向研究的小伙伴想研究HTTP相关知识的话,读这本书的话应该来说就差不多了。
|
|
||||||
- [《大话数据结构》](https://book.douban.com/subject/6424904/)(推荐,豆瓣评分 7.9 , 1K+人评价):入门类型的书籍,读起来比较浅显易懂,适合没有数据结构基础或者说数据结构没学好的小伙伴用来入门数据结构。
|
|
||||||
- [《数据结构与算法分析:C语言描述》](https://book.douban.com/subject/1139426/)(推荐,豆瓣评分 8.9,1.6K+人评价):本书是《Data Structures and Algorithm Analysis in C》一书第2版的简体中译本。原书曾被评为20世纪顶尖的30部计算机著作之一,作者Mark Allen Weiss在数据结构和算法分析方面卓有建树,他的数据结构和算法分析的著作尤其畅销,并受到广泛好评.已被世界500余所大学用作教材。
|
|
||||||
- [《算法图解》](https://book.douban.com/subject/26979890/)(推荐,豆瓣评分 8.4,0.6K+人评价):入门类型的书籍,读起来比较浅显易懂,适合没有算法基础或者说算法没学好的小伙伴用来入门。示例丰富,图文并茂,以让人容易理解的方式阐释了算法.读起来比较快,内容不枯燥!
|
|
||||||
- [《算法 第四版》](https://book.douban.com/subject/10432347/)(推荐,豆瓣评分 9.3,0.4K+人评价):Java语言描述,算法领域经典的参考书,全面介绍了关于算法和数据结构的必备知识,并特别针对排序、搜索、图处理和字符串处理进行了论述。书的内容非常多,可以说是Java程序员的必备书籍之一了。
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Java相关
|
|
||||||
|
|
||||||
- [《Effective java 》](https://book.douban.com/subject/3360807/)(推荐,豆瓣评分 9.0,1.4K+人评价):本书介绍了在Java编程中78条极具实用价值的经验规则,这些经验规则涵盖了大多数开发人员每天所面临的问题的解决方案。通过对Java平台设计专家所使用的技术的全面描述,揭示了应该做什么,不应该做什么才能产生清晰、健壮和高效的代码。本书中的每条规则都以简短、独立的小文章形式出现,并通过例子代码加以进一步说明。本书内容全面,结构清晰,讲解详细。可作为技术人员的参考用书。
|
|
||||||
- [《Head First Java.第二版》](https://book.douban.com/subject/2000732/)(推荐,豆瓣评分 8.7,1.0K+人评价): 可以说是我的Java启蒙书籍了,特别适合新手读当然也适合我们用来温故Java知识点。
|
|
||||||
- [《Java多线程编程核心技术》](https://book.douban.com/subject/26555197/): Java多线程入门级书籍还不错,但是说实话,质量不是很高,很快就可以阅读完。
|
|
||||||
- [《JAVA网络编程 第4版》](https://book.douban.com/subject/26259017/): 可以系统的学习一下网络的一些概念以及网络编程在Java中的使用。
|
|
||||||
- [《Java核心技术卷1+卷2》](https://book.douban.com/subject/25762168/)(推荐): 很棒的两本书,建议有点Java基础之后再读,介绍的还是比较深入的,非常推荐。这两本书我一般也会用来巩固知识点,是两本适合放在自己身边的好书。
|
|
||||||
- [《Java编程思想(第4版)》](https://book.douban.com/subject/2130190/)(推荐,豆瓣评分 9.1,3.2K+人评价):这本书要常读,初学者可以快速概览,中等程序员可以深入看看java,老鸟还可以用之回顾java的体系。这本书之所以厉害,因为它在无形中整合了设计模式,这本书之所以难读,也恰恰在于他对设计模式的整合是无形的。
|
|
||||||
- [《Java并发编程的艺术》](https://book.douban.com/subject/26591326/)(推荐,豆瓣评分 7.2,0.2K+人评价): 这本书不是很适合作为Java并发入门书籍,需要具备一定的JVM基础。我感觉有些东西讲的还是挺深入的,推荐阅读。
|
|
||||||
- [《实战Java高并发程序设计》](https://book.douban.com/subject/26663605/)(推荐):豆瓣评分 8.3 ,书的质量没的说,推荐大家好好看一下。
|
|
||||||
- [《Java程序员修炼之道》](https://book.douban.com/subject/24841235/): 很杂,我只看了前面几章,不太推荐阅读。
|
|
||||||
- [《深入理解Java虚拟机(第2版)周志明》](https://book.douban.com/subject/24722612/)(推荐,豆瓣评分 8.9,1.0K+人评价):建议多刷几遍,书中的所有知识点可以通过JAVA运行时区域和JAVA的内存模型与线程两个大模块罗列完全。
|
|
||||||
- [《Netty实战》](https://book.douban.com/subject/27038538/)(推荐,豆瓣评分 7.8,92人评价):内容很细,如果想学Netty的话,推荐阅读这本书!
|
|
||||||
- [《从Paxos到Zookeeper》](https://book.douban.com/subject/26292004/)(推荐,豆瓣评分 7.8,0.3K人评价):简要介绍几种典型的分布式一致性协议,以及解决分布式一致性问题的思路,其中重点讲解了Paxos和ZAB协议。同时,本书深入介绍了分布式一致性问题的工业解决方案——ZooKeeper,并着重向读者展示这一分布式协调框架的使用方法、内部实现及运维技巧,旨在帮助读者全面了解ZooKeeper,并更好地使用和运维ZooKeeper。
|
|
||||||
|
|
||||||
### JavaWeb相关
|
|
||||||
|
|
||||||
- [《深入分析Java Web技术内幕》](https://book.douban.com/subject/25953851/): 感觉还行,涉及的东西也蛮多。
|
|
||||||
- [《Spring实战(第4版)》](https://book.douban.com/subject/26767354/)(推荐,豆瓣评分 8.3
|
|
||||||
,0.3K+人评价):不建议当做入门书籍读,入门的话可以找点国人的书或者视频看。这本定位就相当于是关于Spring的新华字典,只有一些基本概念的介绍和示例,涵盖了Spring的各个方面,但都不够深入。就像作者在最后一页写的那样:“学习Spring,这才刚刚开始”。
|
|
||||||
- [《Java Web整合开发王者归来》](https://book.douban.com/subject/4189495/)(已过时):当时刚开始学的时候就是开的这本书,基本上是完完整整的看完了。不过,我不是很推荐大家看。这本书比较老了,里面很多东西都已经算是过时了。不过,这本书的一个很大优点是:基础知识点概括全面。
|
|
||||||
- [《Redis实战》](https://book.douban.com/subject/26612779/):如果你想了解Redis的一些概念性知识的话,这本书真的非常不错。
|
|
||||||
- [《Redis设计与实现》](https://book.douban.com/subject/25900156/)(推荐,豆瓣评分 8.5,0.5K+人评价)
|
|
||||||
- [《深入剖析Tomcat》](https://book.douban.com/subject/10426640/)(推荐,豆瓣评分 8.4,0.2K+人评价):本书深入剖析Tomcat 4和Tomcat 5中的每个组件,并揭示其内部工作原理。通过学习本书,你将可以自行开发Tomcat组件,或者扩展已有的组件。 读完这本书,基本可以摆脱背诵面试题的尴尬。
|
|
||||||
- [《高性能MySQL》](https://book.douban.com/subject/23008813/)(推荐,豆瓣评分 9.3,0.4K+人评价):mysql 领域的经典之作,拥有广泛的影响力。不但适合数据库管理员(dba)阅读,也适合开发人员参考学习。不管是数据库新手还是专家,相信都能从本书有所收获。
|
|
||||||
- [深入理解Nginx(第2版)](https://book.douban.com/subject/26745255/):作者讲的非常细致,注释都写的都很工整,对于 Nginx 的开发人员非常有帮助。优点是细致,缺点是过于细致,到处都是代码片段,缺少一些抽象。
|
|
||||||
- [《RabbitMQ实战指南》](https://book.douban.com/subject/27591386/):《RabbitMQ实战指南》从消息中间件的概念和RabbitMQ的历史切入,主要阐述RabbitMQ的安装、使用、配置、管理、运维、原理、扩展等方面的细节。如果你想浅尝RabbitMQ的使用,这本书是你最好的选择;如果你想深入RabbitMQ的原理,这本书也是你最好的选择;总之,如果你想玩转RabbitMQ,这本书一定是最值得看的书之一
|
|
||||||
- [《Spring Cloud微服务实战》](https://book.douban.com/subject/27025912/):从时下流行的微服务架构概念出发,详细介绍了Spring Cloud针对微服务架构中几大核心要素的解决方案和基础组件。对于各个组件的介绍,《Spring Cloud微服务实战》主要以示例与源码结合的方式来帮助读者更好地理解这些组件的使用方法以及运行原理。同时,在介绍的过程中,还包含了作者在实践中所遇到的一些问题和解决思路,可供读者在实践中作为参考。
|
|
||||||
- [《第一本Docker书》](https://book.douban.com/subject/26780404/):Docker入门书籍!
|
|
||||||
|
|
||||||
### 操作系统
|
|
||||||
|
|
||||||
- [《鸟哥的Linux私房菜》](https://book.douban.com/subject/4889838/)(推荐,,豆瓣评分 9.1,0.3K+人评价):本书是最具知名度的Linux入门书《鸟哥的Linux私房菜基础学习篇》的最新版,全面而详细地介绍了Linux操作系统。全书分为5个部分:第一部分着重说明Linux的起源及功能,如何规划和安装Linux主机;第二部分介绍Linux的文件系统、文件、目录与磁盘的管理;第三部分介绍文字模式接口 shell和管理系统的好帮手shell脚本,另外还介绍了文字编辑器vi和vim的使用方法;第四部分介绍了对于系统安全非常重要的Linux账号的管理,以及主机系统与程序的管理,如查看进程、任务分配和作业管理;第五部分介绍了系统管理员(root)的管理事项,如了解系统运行状况、系统服务,针对登录文件进行解析,对系统进行备份以及核心的管理等。
|
|
||||||
|
|
||||||
### 架构相关
|
|
||||||
|
|
||||||
- [《大型网站技术架构:核心原理与案例分析+李智慧》](https://book.douban.com/subject/25723064/)(推荐):这本书我读过,基本不需要你有什么基础啊~读起来特别轻松,但是却可以学到很多东西,非常推荐了。另外我写过这本书的思维导图,关注我的微信公众号:“Java面试通关手册”回复“大型网站技术架构”即可领取思维导图。
|
|
||||||
- [《亿级流量网站架构核心技术》](https://book.douban.com/subject/26999243/)(推荐):一书总结并梳理了亿级流量网站高可用和高并发原则,通过实例详细介绍了如何落地这些原则。本书分为四部分:概述、高可用原则、高并发原则、案例实战。从负载均衡、限流、降级、隔离、超时与重试、回滚机制、压测与预案、缓存、池化、异步化、扩容、队列等多方面详细介绍了亿级流量网站的架构核心技术,让读者看后能快速运用到实践项目中。
|
|
||||||
- [《架构解密从分布式到微服务(Leaderus著)》](https://book.douban.com/subject/27081188/):很一般的书籍,我就是当做课后图书来阅读的。
|
|
||||||
|
|
||||||
### 代码优化
|
|
||||||
|
|
||||||
- [《重构_改善既有代码的设计》](https://book.douban.com/subject/4262627/)(推荐):豆瓣 9.1 分,重构书籍的开山鼻祖。
|
|
||||||
|
|
||||||
### 课外书籍
|
|
||||||
|
|
||||||
- 《追风筝的人》(推荐)
|
|
||||||
- 《穆斯林的葬礼》 (推荐)
|
|
||||||
- 《三体》 (推荐)
|
|
||||||
- 《活着——余华》 (推荐)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,8 +1,34 @@
|
|||||||
这是【备战春招/秋招系列】的第二篇文章,主要是简单地介绍如何去准备面试。
|
不论是笔试还是面试都是有章可循的,但是,一定要不要想着如何去应付面试,糊弄面试官,这样做终究是欺骗自己。这篇文章的目的也主要想让大家知道自己应该从哪些方向去准备面试,有哪些可以提高的方向。
|
||||||
|
|
||||||
不论是校招还是社招都避免不了各种面试、笔试,如何去准备这些东西就显得格外重要。不论是笔试还是面试都是有章可循的,我这个“有章可循”说的意思只是说应对技术面试是可以提前准备。 我其实特别不喜欢那种临近考试就提前背啊记啊各种题的行为,非常反对!我觉得这种方法特别极端,而且在稍有一点经验的面试官面前是根本没有用的。建议大家还是一步一个脚印踏踏实实地走。
|
网上已经有很多面经了,但是我认为网上的各种面经仅仅只能作为参考,你的实际面试与之还是有一些区别的。另外如果要在网上看别人的面经的话,建议即要看别人成功的案例也要适当看看别人失败的案例。**看面经没问题,不论是你要找工作还是平时学习,这都是一种比较好地检验自己水平的一种方式。但是,一定不要过分寄希望于各种面经,试着去提高自己的综合能力。**
|
||||||
|
|
||||||
### 1 如何获取大厂面试机会?
|
“ 80% 的 offer 掌握在 20% 的人手 ” 中这句话也不是不无道理的。决定你面试能否成功的因素中实力固然占有很大一部分比例,但是如果你的心态或者说运气不好的话,依然无法拿到满意的 offer。
|
||||||
|
|
||||||
|
运气暂且不谈,就拿心态来说,千万不要因为面试失败而气馁或者说怀疑自己的能力,面试失败之后多总结一下失败的原因,后面你就会发现自己会越来越强大。
|
||||||
|
|
||||||
|
另外,笔主只是在这里分享一下自己对于 “ 如何备战大厂面试 ” 的一个看法,以下大部分理论/言辞都经过过反复推敲验证,如果有不对的地方或者和你想法不同的地方,请您敬请雅正、不舍赐教。
|
||||||
|
|
||||||
|
<!-- TOC -->
|
||||||
|
|
||||||
|
- [1 如何获取大厂面试机会?](#1-如何获取大厂面试机会)
|
||||||
|
- [2 面试前的准备](#2--面试前的准备)
|
||||||
|
- [2.1 准备自己的自我介绍](#21-准备自己的自我介绍)
|
||||||
|
- [2.2 搞清楚技术面可能会问哪些方向的问题](#22-搞清楚技术面可能会问哪些方向的问题)
|
||||||
|
- [2.2 休闲着装即可](#22-休闲着装即可)
|
||||||
|
- [2.3 随身带上自己的成绩单和简历](#23-随身带上自己的成绩单和简历)
|
||||||
|
- [2.4 如果需要笔试就提前刷一些笔试题](#24-如果需要笔试就提前刷一些笔试题)
|
||||||
|
- [2.5 花时间一些逻辑题](#25-花时间一些逻辑题)
|
||||||
|
- [2.6 准备好自己的项目介绍](#26-准备好自己的项目介绍)
|
||||||
|
- [2.7 提前准备技术面试](#27-提前准备技术面试)
|
||||||
|
- [2.7 面试之前做好定向复习](#27-面试之前做好定向复习)
|
||||||
|
- [3 面试之后复盘](#3-面试之后复盘)
|
||||||
|
- [4 如何学习?学会各种框架有必要吗?](#4-如何学习学会各种框架有必要吗)
|
||||||
|
- [4.1 我该如何学习?](#41-我该如何学习)
|
||||||
|
- [4.2 学会各种框架有必要吗?](#42-学会各种框架有必要吗)
|
||||||
|
|
||||||
|
<!-- /TOC -->
|
||||||
|
|
||||||
|
## 1 如何获取大厂面试机会?
|
||||||
|
|
||||||
**在讲如何获取大厂面试机会之前,先来给大家科普/对比一下两个校招非常常见的概念——春招和秋招。**
|
**在讲如何获取大厂面试机会之前,先来给大家科普/对比一下两个校招非常常见的概念——春招和秋招。**
|
||||||
|
|
||||||
@ -24,17 +50,39 @@
|
|||||||
|
|
||||||
除了这些方法,我也遇到过这样的经历:有些大公司的一些部门可能暂时没招够人,然后如果你的亲戚或者朋友刚好在这个公司,而你正好又在寻求offer,那么面试机会基本上是有了,而且这种面试的难度好像一般还普遍比其他正规面试低很多。
|
除了这些方法,我也遇到过这样的经历:有些大公司的一些部门可能暂时没招够人,然后如果你的亲戚或者朋友刚好在这个公司,而你正好又在寻求offer,那么面试机会基本上是有了,而且这种面试的难度好像一般还普遍比其他正规面试低很多。
|
||||||
|
|
||||||
### 2 面试前的准备
|
## 2 面试前的准备
|
||||||
|
|
||||||
### 2.1 准备自己的自我介绍
|
### 2.1 准备自己的自我介绍
|
||||||
|
|
||||||
从HR面、技术面到高管面/部门主管面,面试官一般会让你先自我介绍一下,所以好好准备自己的自我介绍真的非常重要。网上一般建议的是准备好两份自我介绍:一份对hr说的,主要讲能突出自己的经历,会的编程技术一语带过;另一份对技术面试官说的,主要讲自己会的技术细节,项目经验,经历那些就一语带过。
|
自我介绍一般是你和面试官的第一次面对面正式交流,换位思考一下,假如你是面试官的话,你想听到被你面试的人如何介绍自己呢?一定不是客套地说说自己喜欢编程、平时花了很多时间来学习、自己的兴趣爱好是打球吧?
|
||||||
|
|
||||||
我这里简单分享一下我自己的自我介绍的一个简单的模板吧:
|
我觉得一个好的自我介绍应该包含这几点要素:
|
||||||
|
|
||||||
> 面试官,您好!我叫某某。大学时间我主要利用课外时间学习某某。在校期间参与过一个某某系统的开发,另外,自己学习过程中也写过很多系统比如某某系统。在学习之余,我比较喜欢通过博客整理分享自己所学知识。我现在是某某社区的认证作者,写过某某很不错的文章。另外,我获得过某某奖,我的Github上开源的某个项目已经有多少Star了。
|
1. 用简单的话说清楚自己主要的技术栈于擅长的领域;
|
||||||
|
2. 把重点放在自己在行的地方以及自己的优势之处;
|
||||||
|
3. 重点突出自己的能力比如自己的定位的bug的能力特别厉害;
|
||||||
|
|
||||||
### 2.2 关于着装
|
从社招和校招两个角度来举例子吧!我下面的两个例子仅供参考,自我介绍并不需要死记硬背,记住要说的要点,面试的时候根据公司的情况临场发挥也是没问题的。另外,网上一般建议的是准备好两份自我介绍:一份对hr说的,主要讲能突出自己的经历,会的编程技术一语带过;另一份对技术面试官说的,主要讲自己会的技术细节和项目经验。
|
||||||
|
|
||||||
|
**社招:**
|
||||||
|
|
||||||
|
> 面试官,您好!我叫独秀儿。我目前有1年半的工作经验,熟练使用Spring、MyBatis等框架、了解 Java 底层原理比如JVM调优并且有着丰富的分布式开发经验。离开上一家公司是因为我想在技术上得到更多的锻炼。在上一个公司我参与了一个分布式电子交易系统的开发,负责搭建了整个项目的基础架构并且通过分库分表解决了原始数据库以及一些相关表过于庞大的问题,目前这个网站最高支持 10 万人同时访问。工作之余,我利用自己的业余时间写了一个简单的 RPC 框架,这个框架用到了Netty进行网络通信, 目前我已经将这个项目开源,在 Github 上收获了 2k的 Star! 说到业余爱好的话,我比较喜欢通过博客整理分享自己所学知识,现在已经是多个博客平台的认证作者。 生活中我是一个比较积极乐观的人,一般会通过运动打球的方式来放松。我一直都非常想加入贵公司,我觉得贵公司的文化和技术氛围我都非常喜欢,期待能与你共事!
|
||||||
|
|
||||||
|
**校招:**
|
||||||
|
|
||||||
|
> 面试官,您好!我叫秀儿。大学时间我主要利用课外时间学习了 Java 以及 Spring、MyBatis等框架 。在校期间参与过一个考试系统的开发,这个系统的主要用了 Spring、MyBatis 和 shiro 这三种框架。我在其中主要担任后端开发,主要负责了权限管理功能模块的搭建。另外,我在大学的时候参加过一次软件编程大赛,我和我的团队做的在线订餐系统成功获得了第二名的成绩。我还利用自己的业余时间写了一个简单的 RPC 框架,这个框架用到了Netty进行网络通信, 目前我已经将这个项目开源,在 Github 上收获了 2k的 Star! 说到业余爱好的话,我比较喜欢通过博客整理分享自己所学知识,现在已经是多个博客平台的认证作者。 生活中我是一个比较积极乐观的人,一般会通过运动打球的方式来放松。我一直都非常想加入贵公司,我觉得贵公司的文化和技术氛围我都非常喜欢,期待能与你共事!
|
||||||
|
|
||||||
|
### 2.2 搞清楚技术面可能会问哪些方向的问题
|
||||||
|
|
||||||
|
你准备面试的话首先要搞清技术面可能会被问哪些方向的问题吧!
|
||||||
|
|
||||||
|
**我直接用思维导图的形式展示出来吧!这样更加直观形象一点,细化到某个知识点的话这张图没有介绍到,留个悬念,下篇文章会详细介绍。**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**上面思维导图大概涵盖了技术面试可能会设计的技术,但是你不需要把上面的每一个知识点都搞得很熟悉,要分清主次,对于自己不熟悉的技术不要写在简历上,对于自己简单了解的技术不要说自己熟练掌握!**
|
||||||
|
|
||||||
|
### 2.2 休闲着装即可
|
||||||
|
|
||||||
穿西装、打领带、小皮鞋?NO!NO!NO!这是互联网公司面试又不是去走红毯,所以你只需要穿的简单大方就好,不需要太正式。
|
穿西装、打领带、小皮鞋?NO!NO!NO!这是互联网公司面试又不是去走红毯,所以你只需要穿的简单大方就好,不需要太正式。
|
||||||
|
|
||||||
@ -57,11 +105,11 @@
|
|||||||
1. 对项目整体设计的一个感受(面试官可能会让你画系统的架构图)
|
1. 对项目整体设计的一个感受(面试官可能会让你画系统的架构图)
|
||||||
2. 在这个项目中你负责了什么、做了什么、担任了什么角色
|
2. 在这个项目中你负责了什么、做了什么、担任了什么角色
|
||||||
3. 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用
|
3. 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用
|
||||||
4. 另外项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个棘手的问题的时候你是如何解决的又或者说你在这个项目用了什么技术实现了什么功能比如:用redis做缓存提高访问速度和并发量、使用消息队列削峰和降流等等。
|
4. 另外项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个棘手的问题的时候你是如何解决的又或者说你在这个项目用了什么技术实现了什么功能比如:用redis做缓存提高访问速度和并发量、使用消息队列削峰和降流等等。
|
||||||
|
|
||||||
### 2.7 提前准备技术面试
|
### 2.7 提前准备技术面试
|
||||||
|
|
||||||
搞清楚自己面试中可能涉及哪些知识点、那些知识点是重点。面试中哪些问题会被经常问到、自己改如何回答。(强烈不推荐背题,第一:通过背这种方式你能记住多少?能记住多久?第二:背题的方式的学习很难坚持下去!)
|
搞清楚自己面试中可能涉及哪些知识点、哪些知识点是重点。面试中哪些问题会被经常问到、自己该如何回答。(强烈不推荐背题,第一:通过背这种方式你能记住多少?能记住多久?第二:背题的方式的学习很难坚持下去!)
|
||||||
|
|
||||||
### 2.7 面试之前做好定向复习
|
### 2.7 面试之前做好定向复习
|
||||||
|
|
||||||
@ -69,6 +117,35 @@
|
|||||||
|
|
||||||
举个栗子:在我面试 ThoughtWorks 的前几天我就在网上找了一些关于 ThoughtWorks 的技术面的一些文章。然后知道了 ThoughtWorks 的技术面会让我们在之前做的作业的基础上增加一个或两个功能,所以我提前一天就把我之前做的程序重新重构了一下。然后在技术面的时候,简单的改了几行代码之后写个测试就完事了。如果没有提前准备,我觉得 20 分钟我很大几率会完不成这项任务。
|
举个栗子:在我面试 ThoughtWorks 的前几天我就在网上找了一些关于 ThoughtWorks 的技术面的一些文章。然后知道了 ThoughtWorks 的技术面会让我们在之前做的作业的基础上增加一个或两个功能,所以我提前一天就把我之前做的程序重新重构了一下。然后在技术面的时候,简单的改了几行代码之后写个测试就完事了。如果没有提前准备,我觉得 20 分钟我很大几率会完不成这项任务。
|
||||||
|
|
||||||
# 3 面试之后复盘
|
## 3 面试之后复盘
|
||||||
|
|
||||||
如果失败,不要灰心;如果通过,切勿狂喜。面试和工作实际上是两回事,可能很多面试未通过的人,工作能力比你强的多,反之亦然。我个人觉得面试也像是一场全新的征程,失败和胜利都是平常之事。所以,劝各位不要因为面试失败而灰心、丧失斗志。也不要因为面试通过而沾沾自喜,等待你的将是更美好的未来,继续加油!
|
如果失败,不要灰心;如果通过,切勿狂喜。面试和工作实际上是两回事,可能很多面试未通过的人,工作能力比你强的多,反之亦然。我个人觉得面试也像是一场全新的征程,失败和胜利都是平常之事。所以,劝各位不要因为面试失败而灰心、丧失斗志。也不要因为面试通过而沾沾自喜,等待你的将是更美好的未来,继续加油!
|
||||||
|
|
||||||
|
## 4 如何学习?学会各种框架有必要吗?
|
||||||
|
|
||||||
|
### 4.1 我该如何学习?
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
最最最关键也是对自己最最最重要的就是学习!看看别人分享的面经,看看我写的这篇文章估计你只需要10分钟不到。但这些东西终究是空洞的理论,最主要的还是自己平时的学习!
|
||||||
|
|
||||||
|
如何去学呢?我觉得学习每个知识点可以考虑这样去入手:
|
||||||
|
|
||||||
|
1. **官网(大概率是英文,不推荐初学者看)**。
|
||||||
|
2. **书籍(知识更加系统完全,推荐)**。
|
||||||
|
3. **视频(比较容易理解,推荐,特别是初学的时候。慕课网和哔哩哔哩上面有挺多学习视频可以看,只直接在上面搜索关键词就可以了)**。
|
||||||
|
4. **网上博客(解决某一知识点的问题的时候可以看看)**。
|
||||||
|
|
||||||
|
这里给各位一个建议,**看视频的过程中最好跟着一起练,要做笔记!!!**
|
||||||
|
|
||||||
|
**最好可以边看视频边找一本书籍看,看视频没弄懂的知识点一定要尽快解决,如何解决?**
|
||||||
|
|
||||||
|
首先百度/Google,通过搜索引擎解决不了的话就找身边的朋友或者认识的一些人。
|
||||||
|
|
||||||
|
#### 4.2 学会各种框架有必要吗?
|
||||||
|
|
||||||
|
**一定要学会分配自己时间,要学的东西很多,真的很多,搞清楚哪些东西是重点,哪些东西仅仅了解就够了。一定不要把精力都花在了学各种框架上,算法、数据结构还有计算机网络真的很重要!**
|
||||||
|
|
||||||
|
另外,**学习的过程中有一个可以参考的文档很重要,非常有助于自己的学习**。我当初弄 JavaGuide: https://github.com/Snailclimb/JavaGuide 的很大一部分目的就是因为这个。**客观来说,相比于博客,JavaGuide 里面的内容因为更多人的参与变得更加准确和完善。**
|
||||||
|
|
||||||
|
如果大家觉得这篇文章不错的话,欢迎给我来个三连(评论+转发+在看)!我会在下一篇文章中介绍如何从技术面时的角度准备面试?
|
@ -0,0 +1,743 @@
|
|||||||
|
<!-- TOC -->
|
||||||
|
|
||||||
|
- [一 为什么 Java 中只有值传递?](#一-为什么-java-中只有值传递)
|
||||||
|
- [二 ==与 equals(重要)](#二-与-equals重要)
|
||||||
|
- [三 hashCode 与 equals(重要)](#三-hashcode-与-equals重要)
|
||||||
|
- [3.1 hashCode()介绍](#31-hashcode介绍)
|
||||||
|
- [3.2 为什么要有 hashCode](#32-为什么要有-hashcode)
|
||||||
|
- [3.3 hashCode()与 equals()的相关规定](#33-hashcode与-equals的相关规定)
|
||||||
|
- [3.4 为什么两个对象有相同的 hashcode 值,它们也不一定是相等的?](#34-为什么两个对象有相同的-hashcode-值它们也不一定是相等的)
|
||||||
|
- [四 String 和 StringBuffer、StringBuilder 的区别是什么?String 为什么是不可变的?](#四-string-和-stringbufferstringbuilder-的区别是什么string-为什么是不可变的)
|
||||||
|
- [String 为什么是不可变的吗?](#string-为什么是不可变的吗)
|
||||||
|
- [String 真的是不可变的吗?](#string-真的是不可变的吗)
|
||||||
|
- [五 什么是反射机制?反射机制的应用场景有哪些?](#五-什么是反射机制反射机制的应用场景有哪些)
|
||||||
|
- [5.1 反射机制介绍](#51-反射机制介绍)
|
||||||
|
- [5.2 静态编译和动态编译](#52-静态编译和动态编译)
|
||||||
|
- [5.3 反射机制优缺点](#53-反射机制优缺点)
|
||||||
|
- [5.4 反射的应用场景](#54-反射的应用场景)
|
||||||
|
- [六 什么是 JDK?什么是 JRE?什么是 JVM?三者之间的联系与区别](#六-什么是-jdk什么是-jre什么是-jvm三者之间的联系与区别)
|
||||||
|
- [6.1 JVM](#61-jvm)
|
||||||
|
- [6.2 JDK 和 JRE](#62-jdk-和-jre)
|
||||||
|
- [七 什么是字节码?采用字节码的最大好处是什么?](#七-什么是字节码采用字节码的最大好处是什么)
|
||||||
|
- [八 接口和抽象类的区别是什么?](#八-接口和抽象类的区别是什么)
|
||||||
|
- [九 重载和重写的区别](#九-重载和重写的区别)
|
||||||
|
- [重载](#重载)
|
||||||
|
- [重写](#重写)
|
||||||
|
- [十. Java 面向对象编程三大特性: 封装 继承 多态](#十-java-面向对象编程三大特性-封装-继承-多态)
|
||||||
|
- [封装](#封装)
|
||||||
|
- [继承](#继承)
|
||||||
|
- [多态](#多态)
|
||||||
|
- [十一. 什么是线程和进程?](#十一-什么是线程和进程)
|
||||||
|
- [11.1 何为进程?](#111-何为进程)
|
||||||
|
- [11.2 何为线程?](#112-何为线程)
|
||||||
|
- [十二. 请简要描述线程与进程的关系,区别及优缺点?](#十二-请简要描述线程与进程的关系区别及优缺点)
|
||||||
|
- [12.1 图解进程和线程的关系](#121-图解进程和线程的关系)
|
||||||
|
- [12.2 程序计数器为什么是私有的?](#122-程序计数器为什么是私有的)
|
||||||
|
- [12.3 虚拟机栈和本地方法栈为什么是私有的?](#123-虚拟机栈和本地方法栈为什么是私有的)
|
||||||
|
- [12.4 一句话简单了解堆和方法区](#124-一句话简单了解堆和方法区)
|
||||||
|
- [十三. 说说并发与并行的区别?](#十三-说说并发与并行的区别)
|
||||||
|
- [十四. 什么是上下文切换?](#十四-什么是上下文切换)
|
||||||
|
- [十五. 什么是线程死锁?如何避免死锁?](#十五-什么是线程死锁如何避免死锁)
|
||||||
|
- [15.1. 认识线程死锁](#151-认识线程死锁)
|
||||||
|
- [15.2 如何避免线程死锁?](#152-如何避免线程死锁)
|
||||||
|
- [十六. 说说 sleep() 方法和 wait() 方法区别和共同点?](#十六-说说-sleep-方法和-wait-方法区别和共同点)
|
||||||
|
- [十七. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?](#十七-为什么我们调用-start-方法时会执行-run-方法为什么我们不能直接调用-run-方法)
|
||||||
|
- [参考](#参考)
|
||||||
|
|
||||||
|
<!-- /TOC -->
|
||||||
|
|
||||||
|
## 一 为什么 Java 中只有值传递?
|
||||||
|
|
||||||
|
首先回顾一下在程序设计语言中有关将参数传递给方法(或函数)的一些专业术语。**按值调用(call by value)表示方法接收的是调用者提供的值,而按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。** 它用来描述各种程序设计语言(不只是 Java)中方法参数传递方式。
|
||||||
|
|
||||||
|
**Java 程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容。**
|
||||||
|
|
||||||
|
**下面通过 3 个例子来给大家说明**
|
||||||
|
|
||||||
|
> **example 1**
|
||||||
|
|
||||||
|
```java
|
||||||
|
public static void main(String[] args) {
|
||||||
|
int num1 = 10;
|
||||||
|
int num2 = 20;
|
||||||
|
|
||||||
|
swap(num1, num2);
|
||||||
|
|
||||||
|
System.out.println("num1 = " + num1);
|
||||||
|
System.out.println("num2 = " + num2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void swap(int a, int b) {
|
||||||
|
int temp = a;
|
||||||
|
a = b;
|
||||||
|
b = temp;
|
||||||
|
|
||||||
|
System.out.println("a = " + a);
|
||||||
|
System.out.println("b = " + b);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**结果:**
|
||||||
|
|
||||||
|
```
|
||||||
|
a = 20
|
||||||
|
b = 10
|
||||||
|
num1 = 10
|
||||||
|
num2 = 20
|
||||||
|
```
|
||||||
|
|
||||||
|
**解析:**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
在 swap 方法中,a、b 的值进行交换,并不会影响到 num1、num2。因为,a、b 中的值,只是从 num1、num2 的复制过来的。也就是说,a、b 相当于 num1、num2 的副本,副本的内容无论怎么修改,都不会影响到原件本身。
|
||||||
|
|
||||||
|
**通过上面例子,我们已经知道了一个方法不能修改一个基本数据类型的参数,而对象引用作为参数就不一样,请看 example2.**
|
||||||
|
|
||||||
|
> **example 2**
|
||||||
|
|
||||||
|
```java
|
||||||
|
public static void main(String[] args) {
|
||||||
|
int[] arr = { 1, 2, 3, 4, 5 };
|
||||||
|
System.out.println(arr[0]);
|
||||||
|
change(arr);
|
||||||
|
System.out.println(arr[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void change(int[] array) {
|
||||||
|
// 将数组的第一个元素变为0
|
||||||
|
array[0] = 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**结果:**
|
||||||
|
|
||||||
|
```
|
||||||
|
1
|
||||||
|
0
|
||||||
|
```
|
||||||
|
|
||||||
|
**解析:**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
array 被初始化 arr 的拷贝也就是一个对象的引用,也就是说 array 和 arr 指向的是同一个数组对象。 因此,外部对引用对象的改变会反映到所对应的对象上。
|
||||||
|
|
||||||
|
**通过 example2 我们已经看到,实现一个改变对象参数状态的方法并不是一件难事。理由很简单,方法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。**
|
||||||
|
|
||||||
|
**很多程序设计语言(特别是,C++和 Pascal)提供了两种参数传递的方式:值调用和引用调用。有些程序员(甚至本书的作者)认为 Java 程序设计语言对对象采用的是引用调用,实际上,这种理解是不对的。由于这种误解具有一定的普遍性,所以下面给出一个反例来详细地阐述一下这个问题。**
|
||||||
|
|
||||||
|
> **example 3**
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class Test {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
Student s1 = new Student("小张");
|
||||||
|
Student s2 = new Student("小李");
|
||||||
|
Test.swap(s1, s2);
|
||||||
|
System.out.println("s1:" + s1.getName());
|
||||||
|
System.out.println("s2:" + s2.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void swap(Student x, Student y) {
|
||||||
|
Student temp = x;
|
||||||
|
x = y;
|
||||||
|
y = temp;
|
||||||
|
System.out.println("x:" + x.getName());
|
||||||
|
System.out.println("y:" + y.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**结果:**
|
||||||
|
|
||||||
|
```
|
||||||
|
x:小李
|
||||||
|
y:小张
|
||||||
|
s1:小张
|
||||||
|
s2:小李
|
||||||
|
```
|
||||||
|
|
||||||
|
**解析:**
|
||||||
|
|
||||||
|
交换之前:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
交换之后:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
通过上面两张图可以很清晰的看出: **方法并没有改变存储在变量 s1 和 s2 中的对象引用。swap 方法的参数 x 和 y 被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝**
|
||||||
|
|
||||||
|
> **总结**
|
||||||
|
|
||||||
|
Java 程序设计语言对对象采用的不是引用调用,实际上,对象引用是按
|
||||||
|
值传递的。
|
||||||
|
|
||||||
|
下面再总结一下 Java 中方法参数的使用情况:
|
||||||
|
|
||||||
|
- 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。
|
||||||
|
- 一个方法可以改变一个对象参数的状态。
|
||||||
|
- 一个方法不能让对象参数引用一个新的对象。
|
||||||
|
|
||||||
|
**参考:**
|
||||||
|
|
||||||
|
《Java 核心技术卷 Ⅰ》基础知识第十版第四章 4.5 小节
|
||||||
|
|
||||||
|
## 二 ==与 equals(重要)
|
||||||
|
|
||||||
|
**==** : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)
|
||||||
|
|
||||||
|
**equals()** : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
|
||||||
|
|
||||||
|
- 情况 1:类没有覆盖 equals()方法。则通过 equals()比较该类的两个对象时,等价于通过“==”比较这两个对象。
|
||||||
|
- 情况 2:类覆盖了 equals()方法。一般,我们都覆盖 equals()方法来两个对象的内容相等;若它们的内容相等,则返回 true(即,认为这两个对象相等)。
|
||||||
|
|
||||||
|
**举个例子:**
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class test1 {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
String a = new String("ab"); // a 为一个引用
|
||||||
|
String b = new String("ab"); // b为另一个引用,对象的内容一样
|
||||||
|
String aa = "ab"; // 放在常量池中
|
||||||
|
String bb = "ab"; // 从常量池中查找
|
||||||
|
if (aa == bb) // true
|
||||||
|
System.out.println("aa==bb");
|
||||||
|
if (a == b) // false,非同一对象
|
||||||
|
System.out.println("a==b");
|
||||||
|
if (a.equals(b)) // true
|
||||||
|
System.out.println("aEQb");
|
||||||
|
if (42 == 42.0) { // true
|
||||||
|
System.out.println("true");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明:**
|
||||||
|
|
||||||
|
- String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。
|
||||||
|
- 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。
|
||||||
|
|
||||||
|
## 三 hashCode 与 equals(重要)
|
||||||
|
|
||||||
|
面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写 equals 时必须重写 hashCode 方法?”
|
||||||
|
|
||||||
|
### 3.1 hashCode()介绍
|
||||||
|
|
||||||
|
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在 JDK 的 Object.java 中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。另外需要注意的是: Object 的 hashcode 方法是本地方法,也就是用 c 语言或 c++ 实现的,该方法通常用来将对象的 内存地址 转换为整数之后返回。
|
||||||
|
|
||||||
|
```java
|
||||||
|
/**
|
||||||
|
* Returns a hash code value for the object. This method is
|
||||||
|
* supported for the benefit of hash tables such as those provided by
|
||||||
|
* {@link java.util.HashMap}.
|
||||||
|
* <p>
|
||||||
|
* As much as is reasonably practical, the hashCode method defined by
|
||||||
|
* class {@code Object} does return distinct integers for distinct
|
||||||
|
* objects. (This is typically implemented by converting the internal
|
||||||
|
* address of the object into an integer, but this implementation
|
||||||
|
* technique is not required by the
|
||||||
|
* Java™ programming language.)
|
||||||
|
*
|
||||||
|
* @return a hash code value for this object.
|
||||||
|
* @see java.lang.Object#equals(java.lang.Object)
|
||||||
|
* @see java.lang.System#identityHashCode
|
||||||
|
*/
|
||||||
|
public native int hashCode();
|
||||||
|
```
|
||||||
|
|
||||||
|
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)
|
||||||
|
|
||||||
|
### 3.2 为什么要有 hashCode
|
||||||
|
|
||||||
|
**我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:**
|
||||||
|
|
||||||
|
当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的 Java 启蒙书《Head fist java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。
|
||||||
|
|
||||||
|
### 3.3 hashCode()与 equals()的相关规定
|
||||||
|
|
||||||
|
1. 如果两个对象相等,则 hashcode 一定也是相同的
|
||||||
|
2. 两个对象相等,对两个对象分别调用 equals 方法都返回 true
|
||||||
|
3. 两个对象有相同的 hashcode 值,它们也不一定是相等的
|
||||||
|
4. **因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖**
|
||||||
|
5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
|
||||||
|
|
||||||
|
### 3.4 为什么两个对象有相同的 hashcode 值,它们也不一定是相等的?
|
||||||
|
|
||||||
|
在这里解释一位小伙伴的问题。以下内容摘自《Head Fisrt Java》。
|
||||||
|
|
||||||
|
因为 hashCode() 所使用的杂凑算法也许刚好会让多个对象传回相同的杂凑值。越糟糕的杂凑算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同的 hashCode)。
|
||||||
|
|
||||||
|
我们刚刚也提到了 HashSet,如果 HashSet 在对比的时候,同样的 hashcode 有多个对象,它会使用 equals() 来判断是否真的相同。也就是说 hashcode 只是用来缩小查找成本。
|
||||||
|
|
||||||
|
## 四 String 和 StringBuffer、StringBuilder 的区别是什么?String 为什么是不可变的?
|
||||||
|
|
||||||
|
**可变性**
|
||||||
|
|
||||||
|
简单的来说:String 类中使用 final 关键字修饰字符数组来保存字符串,`private final char value[]`,所以 String 对象是不可变的。而 StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串`char[]value` 但是没有用 final 关键字修饰,所以这两种对象都是可变的。
|
||||||
|
|
||||||
|
StringBuilder 与 StringBuffer 的构造方法都是调用父类构造方法也就是 AbstractStringBuilder 实现的,大家可以自行查阅源码。
|
||||||
|
|
||||||
|
AbstractStringBuilder.java
|
||||||
|
|
||||||
|
```java
|
||||||
|
abstract class AbstractStringBuilder implements Appendable, CharSequence {
|
||||||
|
char[] value;
|
||||||
|
int count;
|
||||||
|
AbstractStringBuilder() {
|
||||||
|
}
|
||||||
|
AbstractStringBuilder(int capacity) {
|
||||||
|
value = new char[capacity];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**线程安全性**
|
||||||
|
|
||||||
|
String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
|
||||||
|
|
||||||
|
**性能**
|
||||||
|
|
||||||
|
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
|
||||||
|
|
||||||
|
**对于三者使用的总结:**
|
||||||
|
|
||||||
|
1. 操作少量的数据: 适用 String
|
||||||
|
2. 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
|
||||||
|
3. 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer
|
||||||
|
|
||||||
|
#### String 为什么是不可变的吗?
|
||||||
|
|
||||||
|
简单来说就是 String 类利用了 final 修饰的 char 类型数组存储字符,源码如下图所以:
|
||||||
|
|
||||||
|
```java
|
||||||
|
/** The value is used for character storage. */
|
||||||
|
private final char value[];
|
||||||
|
```
|
||||||
|
|
||||||
|
#### String 真的是不可变的吗?
|
||||||
|
|
||||||
|
我觉得如果别人问这个问题的话,回答不可变就可以了。
|
||||||
|
下面只是给大家看两个有代表性的例子:
|
||||||
|
|
||||||
|
**1) String 不可变但不代表引用不可以变**
|
||||||
|
|
||||||
|
```java
|
||||||
|
String str = "Hello";
|
||||||
|
str = str + " World";
|
||||||
|
System.out.println("str=" + str);
|
||||||
|
```
|
||||||
|
|
||||||
|
结果:
|
||||||
|
|
||||||
|
```
|
||||||
|
str=Hello World
|
||||||
|
```
|
||||||
|
|
||||||
|
解析:
|
||||||
|
|
||||||
|
实际上,原来 String 的内容是不变的,只是 str 由原来指向"Hello"的内存地址转为指向"Hello World"的内存地址而已,也就是说多开辟了一块内存区域给"Hello World"字符串。
|
||||||
|
|
||||||
|
**2) 通过反射是可以修改所谓的“不可变”对象**
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 创建字符串"Hello World", 并赋给引用s
|
||||||
|
String s = "Hello World";
|
||||||
|
|
||||||
|
System.out.println("s = " + s); // Hello World
|
||||||
|
|
||||||
|
// 获取String类中的value字段
|
||||||
|
Field valueFieldOfString = String.class.getDeclaredField("value");
|
||||||
|
|
||||||
|
// 改变value属性的访问权限
|
||||||
|
valueFieldOfString.setAccessible(true);
|
||||||
|
|
||||||
|
// 获取s对象上的value属性的值
|
||||||
|
char[] value = (char[]) valueFieldOfString.get(s);
|
||||||
|
|
||||||
|
// 改变value所引用的数组中的第5个字符
|
||||||
|
value[5] = '_';
|
||||||
|
|
||||||
|
System.out.println("s = " + s); // Hello_World
|
||||||
|
```
|
||||||
|
|
||||||
|
结果:
|
||||||
|
|
||||||
|
```
|
||||||
|
s = Hello World
|
||||||
|
s = Hello_World
|
||||||
|
```
|
||||||
|
|
||||||
|
解析:
|
||||||
|
|
||||||
|
用反射可以访问私有成员, 然后反射出 String 对象中的 value 属性, 进而改变通过获得的 value 引用改变数组的结构。但是一般我们不会这么做,这里只是简单提一下有这个东西。
|
||||||
|
|
||||||
|
## 五 什么是反射机制?反射机制的应用场景有哪些?
|
||||||
|
|
||||||
|
### 5.1 反射机制介绍
|
||||||
|
|
||||||
|
JAVA 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 java 语言的反射机制。
|
||||||
|
|
||||||
|
### 5.2 静态编译和动态编译
|
||||||
|
|
||||||
|
- **静态编译:**在编译时确定类型,绑定对象
|
||||||
|
- **动态编译:**运行时确定类型,绑定对象
|
||||||
|
|
||||||
|
### 5.3 反射机制优缺点
|
||||||
|
|
||||||
|
- **优点:** 运行期类型的判断,动态加载类,提高代码灵活度。
|
||||||
|
- **缺点:** 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的 java 代码要慢很多。
|
||||||
|
|
||||||
|
### 5.4 反射的应用场景
|
||||||
|
|
||||||
|
**反射是框架设计的灵魂。**
|
||||||
|
|
||||||
|
在我们平时的项目开发过程中,基本上很少会直接使用到反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架也大量使用到了反射机制。
|
||||||
|
|
||||||
|
举例:① 我们在使用 JDBC 连接数据库时使用 `Class.forName()`通过反射加载数据库的驱动程序;②Spring 框架也用到很多反射机制,最经典的就是 xml 的配置模式。Spring 通过 XML 配置模式装载 Bean 的过程:1) 将程序内所有 XML 或 Properties 配置文件加载入内存中;
|
||||||
|
2)Java 类里面解析 xml 或 properties 里面的内容,得到对应实体类的字节码字符串以及相关的属性信息; 3)使用反射机制,根据这个字符串获得某个类的 Class 实例; 4)动态配置实例的属性
|
||||||
|
|
||||||
|
**推荐阅读:**
|
||||||
|
|
||||||
|
- [Reflection:Java 反射机制的应用场景](https://segmentfault.com/a/1190000010162647?utm_source=tuicool&utm_medium=referral "Reflection:Java反射机制的应用场景")
|
||||||
|
- [Java 基础之—反射(非常重要)](https://blog.csdn.net/sinat_38259539/article/details/71799078 "Java基础之—反射(非常重要)")
|
||||||
|
|
||||||
|
## 六 什么是 JDK?什么是 JRE?什么是 JVM?三者之间的联系与区别
|
||||||
|
|
||||||
|
### 6.1 JVM
|
||||||
|
|
||||||
|
Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。
|
||||||
|
|
||||||
|
**什么是字节码?采用字节码的好处是什么?**
|
||||||
|
|
||||||
|
> 在 Java 中,JVM 可以理解的代码就叫做`字节码`(即扩展名为 `.class` 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。
|
||||||
|
|
||||||
|
**Java 程序从源代码到运行一般有下面 3 步:**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
我们需要格外注意的是 .class->机器码 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言。
|
||||||
|
|
||||||
|
> HotSpot 采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是 JIT 所需要编译的部分。JVM 会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。JDK 9 引入了一种新的编译模式 AOT(Ahead of Time Compilation),它是直接将字节码编译成机器码,这样就避免了 JIT 预热等各方面的开销。JDK 支持分层编译和 AOT 协作使用。但是 ,AOT 编译器的编译质量是肯定比不上 JIT 编译器的。
|
||||||
|
|
||||||
|
**总结:**
|
||||||
|
|
||||||
|
Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。
|
||||||
|
|
||||||
|
### 6.2 JDK 和 JRE
|
||||||
|
|
||||||
|
JDK 是 Java Development Kit,它是功能齐全的 Java SDK。它拥有 JRE 所拥有的一切,还有编译器(javac)和工具(如 javadoc 和 jdb)。它能够创建和编译程序。
|
||||||
|
|
||||||
|
JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。但是,它不能用于创建新程序。
|
||||||
|
|
||||||
|
如果你只是为了运行一下 Java 程序的话,那么你只需要安装 JRE 就可以了。如果你需要进行一些 Java 编程方面的工作,那么你就需要安装 JDK 了。但是,这不是绝对的。有时,即使您不打算在计算机上进行任何 Java 开发,仍然需要安装 JDK。例如,如果要使用 JSP 部署 Web 应用程序,那么从技术上讲,您只是在应用程序服务器中运行 Java 程序。那你为什么需要 JDK 呢?因为应用程序服务器会将 JSP 转换为 Java servlet,并且需要使用 JDK 来编译 servlet。
|
||||||
|
|
||||||
|
## 七 什么是字节码?采用字节码的最大好处是什么?
|
||||||
|
|
||||||
|
**先看下 java 中的编译器和解释器:**
|
||||||
|
|
||||||
|
Java 中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在 Java 中,这种供虚拟机理解的代码叫做`字节码`(即扩展名为`.class`的文件),它不面向任何特定的处理器,只面向虚拟机。每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java 源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行。这也就是解释了 Java 的编译与解释并存的特点。
|
||||||
|
|
||||||
|
Java 源代码---->编译器---->jvm 可执行的 Java 字节码(即虚拟指令)---->jvm---->jvm 中解释器----->机器可执行的二进制机器码---->程序运行。
|
||||||
|
|
||||||
|
**采用字节码的好处:**
|
||||||
|
|
||||||
|
Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同的计算机上运行。
|
||||||
|
|
||||||
|
## 八 接口和抽象类的区别是什么?
|
||||||
|
|
||||||
|
1. 接口的方法默认是 public,所有方法在接口中不能有实现,抽象类可以有非抽象的方法
|
||||||
|
2. 接口中的实例变量默认是 final 类型的,而抽象类中则不一定
|
||||||
|
3. 一个类可以实现多个接口,但最多只能实现一个抽象类
|
||||||
|
4. 一个类实现接口的话要实现接口的所有方法,而抽象类不一定
|
||||||
|
5. 接口不能用 new 实例化,但可以声明,但是必须引用一个实现该接口的对象 从设计层面来说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。
|
||||||
|
|
||||||
|
注意:Java8 后接口可以有默认实现( default )。
|
||||||
|
|
||||||
|
## 九 重载和重写的区别
|
||||||
|
|
||||||
|
### 重载
|
||||||
|
|
||||||
|
发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。
|
||||||
|
|
||||||
|
下面是《Java 核心技术》对重载这个概念的介绍:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 重写
|
||||||
|
|
||||||
|
重写是子类对父类的允许访问的方法的实现过程进行重新编写,发生在子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。另外,如果父类方法访问修饰符为 private 则子类就不能重写该方法。**也就是说方法提供的行为改变,而方法的外貌并没有改变。**
|
||||||
|
|
||||||
|
## 十. Java 面向对象编程三大特性: 封装 继承 多态
|
||||||
|
|
||||||
|
### 封装
|
||||||
|
|
||||||
|
封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。
|
||||||
|
|
||||||
|
### 继承
|
||||||
|
|
||||||
|
继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。
|
||||||
|
|
||||||
|
**关于继承如下 3 点请记住:**
|
||||||
|
|
||||||
|
1. 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,**只是拥有**。
|
||||||
|
2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
|
||||||
|
3. 子类可以用自己的方式实现父类的方法。(以后介绍)。
|
||||||
|
|
||||||
|
### 多态
|
||||||
|
|
||||||
|
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
|
||||||
|
|
||||||
|
在 Java 中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。
|
||||||
|
|
||||||
|
## 十一. 什么是线程和进程?
|
||||||
|
|
||||||
|
### 11.1 何为进程?
|
||||||
|
|
||||||
|
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
|
||||||
|
|
||||||
|
在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。
|
||||||
|
|
||||||
|
如下图所示,在 windows 中通过查看任务管理器的方式,我们就可以清楚看到 window 当前运行的进程(.exe 文件的运行)。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 11.2 何为线程?
|
||||||
|
|
||||||
|
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的**堆**和**方法区**资源,但每个线程有自己的**程序计数器**、**虚拟机栈**和**本地方法栈**,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
|
||||||
|
|
||||||
|
Java 程序天生就是多线程程序,我们可以通过 JMX 来看一下一个普通的 Java 程序有哪些线程,代码如下。
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class MultiThread {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
// 获取 Java 线程管理 MXBean
|
||||||
|
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
|
||||||
|
// 不需要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息
|
||||||
|
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
|
||||||
|
// 遍历线程信息,仅打印线程 ID 和线程名称信息
|
||||||
|
for (ThreadInfo threadInfo : threadInfos) {
|
||||||
|
System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
上述程序输出如下(输出内容可能不同,不用太纠结下面每个线程的作用,只用知道 main 线程执行 main 方法即可):
|
||||||
|
|
||||||
|
```
|
||||||
|
[5] Attach Listener //添加事件
|
||||||
|
[4] Signal Dispatcher // 分发处理给 JVM 信号的线程
|
||||||
|
[3] Finalizer //调用对象 finalize 方法的线程
|
||||||
|
[2] Reference Handler //清除 reference 线程
|
||||||
|
[1] main //main 线程,程序入口
|
||||||
|
```
|
||||||
|
|
||||||
|
从上面的输出内容可以看出:**一个 Java 程序的运行是 main 线程和多个其他线程同时运行**。
|
||||||
|
|
||||||
|
## 十二. 请简要描述线程与进程的关系,区别及优缺点?
|
||||||
|
|
||||||
|
**从 JVM 角度说进程和线程之间的关系**
|
||||||
|
|
||||||
|
### 12.1 图解进程和线程的关系
|
||||||
|
|
||||||
|
下图是 Java 内存区域,通过下图我们从 JVM 的角度来说一下线程和进程之间的关系。如果你对 Java 内存区域 (运行时数据区) 这部分知识不太了解的话可以阅读一下这篇文章:[《可能是把 Java 内存区域讲的最清楚的一篇文章》](https://github.com/Snailclimb/JavaGuide/blob/3965c02cc0f294b0bd3580df4868d5e396959e2e/Java%E7%9B%B8%E5%85%B3/%E5%8F%AF%E8%83%BD%E6%98%AF%E6%8A%8AJava%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F%E8%AE%B2%E7%9A%84%E6%9C%80%E6%B8%85%E6%A5%9A%E7%9A%84%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0.md "《可能是把 Java 内存区域讲的最清楚的一篇文章》")
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3/JVM运行时数据区域.png" width="600px"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的**堆**和**方法区 (JDK1.8 之后的元空间)**资源,但是每个线程有自己的**程序计数器**、**虚拟机栈** 和 **本地方法栈**。
|
||||||
|
|
||||||
|
**总结:** 线程 是 进程 划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反
|
||||||
|
|
||||||
|
下面是该知识点的扩展内容!
|
||||||
|
|
||||||
|
下面来思考这样一个问题:为什么**程序计数器**、**虚拟机栈**和**本地方法栈**是线程私有的呢?为什么堆和方法区是线程共享的呢?
|
||||||
|
|
||||||
|
### 12.2 程序计数器为什么是私有的?
|
||||||
|
|
||||||
|
程序计数器主要有下面两个作用:
|
||||||
|
|
||||||
|
1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
|
||||||
|
2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
|
||||||
|
|
||||||
|
需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。
|
||||||
|
|
||||||
|
所以,程序计数器私有主要是为了**线程切换后能恢复到正确的执行位置**。
|
||||||
|
|
||||||
|
### 12.3 虚拟机栈和本地方法栈为什么是私有的?
|
||||||
|
|
||||||
|
- **虚拟机栈:** 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
|
||||||
|
- **本地方法栈:** 和虚拟机栈所发挥的作用非常相似,区别是: **虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。** 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
|
||||||
|
|
||||||
|
所以,为了**保证线程中的局部变量不被别的线程访问到**,虚拟机栈和本地方法栈是线程私有的。
|
||||||
|
|
||||||
|
### 12.4 一句话简单了解堆和方法区
|
||||||
|
|
||||||
|
堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
|
||||||
|
|
||||||
|
## 十三. 说说并发与并行的区别?
|
||||||
|
|
||||||
|
- **并发:** 同一时间段,多个任务都在执行 (单位时间内不一定同时执行);
|
||||||
|
- **并行:** 单位时间内,多个任务同时执行。
|
||||||
|
|
||||||
|
## 十四. 什么是上下文切换?
|
||||||
|
|
||||||
|
多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
|
||||||
|
|
||||||
|
概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。**任务从保存到再加载的过程就是一次上下文切换**。
|
||||||
|
|
||||||
|
上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
|
||||||
|
|
||||||
|
Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。
|
||||||
|
|
||||||
|
## 十五. 什么是线程死锁?如何避免死锁?
|
||||||
|
|
||||||
|
### 15.1. 认识线程死锁
|
||||||
|
|
||||||
|
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
|
||||||
|
|
||||||
|
如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
下面通过一个例子来说明线程死锁,代码模拟了上图的死锁的情况 (代码来源于《并发编程之美》):
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class DeadLockDemo {
|
||||||
|
private static Object resource1 = new Object();//资源 1
|
||||||
|
private static Object resource2 = new Object();//资源 2
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
new Thread(() -> {
|
||||||
|
synchronized (resource1) {
|
||||||
|
System.out.println(Thread.currentThread() + "get resource1");
|
||||||
|
try {
|
||||||
|
Thread.sleep(1000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
System.out.println(Thread.currentThread() + "waiting get resource2");
|
||||||
|
synchronized (resource2) {
|
||||||
|
System.out.println(Thread.currentThread() + "get resource2");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, "线程 1").start();
|
||||||
|
|
||||||
|
new Thread(() -> {
|
||||||
|
synchronized (resource2) {
|
||||||
|
System.out.println(Thread.currentThread() + "get resource2");
|
||||||
|
try {
|
||||||
|
Thread.sleep(1000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
System.out.println(Thread.currentThread() + "waiting get resource1");
|
||||||
|
synchronized (resource1) {
|
||||||
|
System.out.println(Thread.currentThread() + "get resource1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, "线程 2").start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Output
|
||||||
|
|
||||||
|
```
|
||||||
|
Thread[线程 1,5,main]get resource1
|
||||||
|
Thread[线程 2,5,main]get resource2
|
||||||
|
Thread[线程 1,5,main]waiting get resource2
|
||||||
|
Thread[线程 2,5,main]waiting get resource1
|
||||||
|
```
|
||||||
|
|
||||||
|
线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过`Thread.sleep(1000);`让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。
|
||||||
|
|
||||||
|
学过操作系统的朋友都知道产生死锁必须具备以下四个条件:
|
||||||
|
|
||||||
|
1. 互斥条件:该资源任意一个时刻只由一个线程占用。
|
||||||
|
2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
|
||||||
|
3. 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
|
||||||
|
4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
|
||||||
|
|
||||||
|
### 15.2 如何避免线程死锁?
|
||||||
|
|
||||||
|
我们只要破坏产生死锁的四个条件中的其中一个就可以了。
|
||||||
|
|
||||||
|
**破坏互斥条件**
|
||||||
|
|
||||||
|
这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
|
||||||
|
|
||||||
|
**破坏请求与保持条件**
|
||||||
|
|
||||||
|
一次性申请所有的资源。
|
||||||
|
|
||||||
|
**破坏不剥夺条件**
|
||||||
|
|
||||||
|
占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
|
||||||
|
|
||||||
|
**破坏循环等待条件**
|
||||||
|
|
||||||
|
靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
|
||||||
|
|
||||||
|
我们对线程 2 的代码修改成下面这样就不会产生死锁了。
|
||||||
|
|
||||||
|
```java
|
||||||
|
new Thread(() -> {
|
||||||
|
synchronized (resource1) {
|
||||||
|
System.out.println(Thread.currentThread() + "get resource1");
|
||||||
|
try {
|
||||||
|
Thread.sleep(1000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
System.out.println(Thread.currentThread() + "waiting get resource2");
|
||||||
|
synchronized (resource2) {
|
||||||
|
System.out.println(Thread.currentThread() + "get resource2");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, "线程 2").start();
|
||||||
|
```
|
||||||
|
|
||||||
|
Output
|
||||||
|
|
||||||
|
```
|
||||||
|
Thread[线程 1,5,main]get resource1
|
||||||
|
Thread[线程 1,5,main]waiting get resource2
|
||||||
|
Thread[线程 1,5,main]get resource2
|
||||||
|
Thread[线程 2,5,main]get resource1
|
||||||
|
Thread[线程 2,5,main]waiting get resource2
|
||||||
|
Thread[线程 2,5,main]get resource2
|
||||||
|
|
||||||
|
Process finished with exit code 0
|
||||||
|
```
|
||||||
|
|
||||||
|
我们分析一下上面的代码为什么避免了死锁的发生?
|
||||||
|
|
||||||
|
线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。
|
||||||
|
|
||||||
|
## 十六. 说说 sleep() 方法和 wait() 方法区别和共同点?
|
||||||
|
|
||||||
|
- 两者最主要的区别在于:**sleep 方法没有释放锁,而 wait 方法释放了锁** 。
|
||||||
|
- 两者都可以暂停线程的执行。
|
||||||
|
- Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
|
||||||
|
- wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒。
|
||||||
|
|
||||||
|
## 十七. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
|
||||||
|
|
||||||
|
这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!
|
||||||
|
|
||||||
|
new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
|
||||||
|
|
||||||
|
**总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。**
|
||||||
|
|
||||||
|
## 参考
|
||||||
|
|
||||||
|
- [https://blog.csdn.net/zhzhao999/article/details/53449504](https://blog.csdn.net/zhzhao999/article/details/53449504 "https://blog.csdn.net/zhzhao999/article/details/53449504")
|
||||||
|
- [https://www.cnblogs.com/skywang12345/p/3324958.html](https://www.cnblogs.com/skywang12345/p/3324958.html "https://www.cnblogs.com/skywang12345/p/3324958.html")
|
||||||
|
- [https://www.cnblogs.com/Eason-S/p/5524837.html](https://www.cnblogs.com/Eason-S/p/5524837.html "https://www.cnblogs.com/Eason-S/p/5524837.html")
|
@ -1,21 +1,43 @@
|
|||||||
# 程序员的简历就该这样写
|
<!-- TOC -->
|
||||||
|
|
||||||
### 1 前言
|
- [程序员简历就该这样写](#程序员简历就该这样写)
|
||||||
<font color="red">一份好的简历可以在整个申请面试以及面试过程中起到非常好的作用。</font> 在不夸大自己能力的情况下,写出一份好的简历也是一项很棒的能力。
|
- [为什么说简历很重要?](#为什么说简历很重要)
|
||||||
|
- [先从面试前来说](#先从面试前来说)
|
||||||
|
- [再从面试中来说](#再从面试中来说)
|
||||||
|
- [下面这几点你必须知道](#下面这几点你必须知道)
|
||||||
|
- [必须了解的两大法则](#必须了解的两大法则)
|
||||||
|
- [STAR法则(Situation Task Action Result)](#star法则situation-task-action-result)
|
||||||
|
- [FAB 法则(Feature Advantage Benefit)](#fab-法则feature-advantage-benefit)
|
||||||
|
- [项目经历怎么写?](#项目经历怎么写)
|
||||||
|
- [专业技能该怎么写?](#专业技能该怎么写)
|
||||||
|
- [排版注意事项](#排版注意事项)
|
||||||
|
- [其他的一些小tips](#其他的一些小tips)
|
||||||
|
- [推荐的工具/网站](#推荐的工具网站)
|
||||||
|
|
||||||
### 2 为什么说简历很重要?
|
<!-- /TOC -->
|
||||||
|
|
||||||
#### 2.1 先从面试前来说
|
# 程序员简历就该这样写
|
||||||
|
|
||||||
假如你是网申,你的简历必然会经过HR的筛选,一张简历HR可能也就花费10秒钟看一下,然后HR就会决定你这一关是Fail还是Pass。
|
本篇文章除了教大家用Markdown如何写一份程序员专属的简历,后面还会给大家推荐一些不错的用来写Markdown简历的软件或者网站,以及如何优雅的将Markdown格式转变为PDF格式或者其他格式。
|
||||||
|
|
||||||
假如你是内推,如果你的简历没有什么优势的话,就算是内推你的人再用心,也无能为力。
|
推荐大家使用Markdown语法写简历,然后再将Markdown格式转换为PDF格式后进行简历投递。
|
||||||
|
|
||||||
|
如果你对Markdown语法不太了解的话,可以花半个小时简单看一下Markdown语法说明: http://www.markdown.cn 。
|
||||||
|
|
||||||
|
## 为什么说简历很重要?
|
||||||
|
|
||||||
|
一份好的简历可以在整个申请面试以及面试过程中起到非常好的作用。 在不夸大自己能力的情况下,写出一份好的简历也是一项很棒的能力。为什么说简历很重要呢?
|
||||||
|
|
||||||
|
### 先从面试前来说
|
||||||
|
|
||||||
|
- 假如你是网申,你的简历必然会经过HR的筛选,一张简历HR可能也就花费10秒钟看一下,然后HR就会决定你这一关是Fail还是Pass。
|
||||||
|
- 假如你是内推,如果你的简历没有什么优势的话,就算是内推你的人再用心,也无能为力。
|
||||||
|
|
||||||
另外,就算你通过了筛选,后面的面试中,面试官也会根据你的简历来判断你究竟是否值得他花费很多时间去面试。
|
另外,就算你通过了筛选,后面的面试中,面试官也会根据你的简历来判断你究竟是否值得他花费很多时间去面试。
|
||||||
|
|
||||||
所以,简历就像是我们的一个门面一样,它在很大程度上决定了你能否进入到下一轮的面试中。
|
所以,简历就像是我们的一个门面一样,它在很大程度上决定了你能否进入到下一轮的面试中。
|
||||||
|
|
||||||
#### 2.2 再从面试中来说
|
### 再从面试中来说
|
||||||
|
|
||||||
我发现大家比较喜欢看面经 ,这点无可厚非,但是大部分面经都没告诉你很多问题都是在特定条件下才问的。举个简单的例子:一般情况下你的简历上注明你会的东西才会被问到(Java、数据结构、网络、算法这些基础是每个人必问的),比如写了你会 redis,那面试官就很大概率会问你 redis 的一些问题。比如:redis的常见数据类型及应用场景、redis是单线程为什么还这么快、 redis 和 memcached 的区别、redis 内存淘汰机制等等。
|
我发现大家比较喜欢看面经 ,这点无可厚非,但是大部分面经都没告诉你很多问题都是在特定条件下才问的。举个简单的例子:一般情况下你的简历上注明你会的东西才会被问到(Java、数据结构、网络、算法这些基础是每个人必问的),比如写了你会 redis,那面试官就很大概率会问你 redis 的一些问题。比如:redis的常见数据类型及应用场景、redis是单线程为什么还这么快、 redis 和 memcached 的区别、redis 内存淘汰机制等等。
|
||||||
|
|
||||||
@ -23,17 +45,16 @@
|
|||||||
|
|
||||||
面试和工作是两回事,聪明的人会把面试官往自己擅长的领域领,其他人则被面试官牵着鼻子走。虽说面试和工作是两回事,但是你要想要获得自己满意的 offer ,你自身的实力必须要强。
|
面试和工作是两回事,聪明的人会把面试官往自己擅长的领域领,其他人则被面试官牵着鼻子走。虽说面试和工作是两回事,但是你要想要获得自己满意的 offer ,你自身的实力必须要强。
|
||||||
|
|
||||||
### 3 下面这几点你必须知道
|
## 下面这几点你必须知道
|
||||||
|
|
||||||
1. 大部分公司的HR都说我们不看重学历(骗你的!),但是如果你的学校不出众的话,很难在一堆简历中脱颖而出,除非你的简历上有特别的亮点,比如:某某大厂的实习经历、获得了某某大赛的奖等等。
|
1. 大部分公司的HR都说我们不看重学历(骗你的!),但是如果你的学校不出众的话,很难在一堆简历中脱颖而出,除非你的简历上有特别的亮点,比如:某某大厂的实习经历、获得了某某大赛的奖等等。
|
||||||
2. **大部分应届生找工作的硬伤是没有工作经验或实习经历,所以如果你是应届生就不要错过秋招和春招。一旦错过,你后面就极大可能会面临社招,这个时候没有工作经验的你可能就会面临各种碰壁,导致找不到一个好的工作**
|
2. **大部分应届生找工作的硬伤是没有工作经验或实习经历,所以如果你是应届生就不要错过秋招和春招。一旦错过,你后面就极大可能会面临社招,这个时候没有工作经验的你可能就会面临各种碰壁,导致找不到一个好的工作**
|
||||||
3. **写在简历上的东西一定要慎重,这是面试官大量提问的地方;**
|
3. **写在简历上的东西一定要慎重,这是面试官大量提问的地方;**
|
||||||
4. **将自己的项目经历完美的展示出来非常重要。**
|
4. **将自己的项目经历完美的展示出来非常重要。**
|
||||||
|
|
||||||
### 4 必须了解的两大法则
|
## 必须了解的两大法则
|
||||||
|
|
||||||
|
### STAR法则(Situation Task Action Result)
|
||||||
**①STAR法则(Situation Task Action Result):**
|
|
||||||
|
|
||||||
- **Situation:** 事情是在什么情况下发生;
|
- **Situation:** 事情是在什么情况下发生;
|
||||||
- **Task::** 你是如何明确你的任务的;
|
- **Task::** 你是如何明确你的任务的;
|
||||||
@ -42,14 +63,7 @@
|
|||||||
|
|
||||||
简而言之,STAR法则,就是一种讲述自己故事的方式,或者说,是一个清晰、条理的作文模板。不管是什么,合理熟练运用此法则,可以轻松的对面试官描述事物的逻辑方式,表现出自己分析阐述问题的清晰性、条理性和逻辑性。
|
简而言之,STAR法则,就是一种讲述自己故事的方式,或者说,是一个清晰、条理的作文模板。不管是什么,合理熟练运用此法则,可以轻松的对面试官描述事物的逻辑方式,表现出自己分析阐述问题的清晰性、条理性和逻辑性。
|
||||||
|
|
||||||
下面这段内容摘自百度百科,我觉得写的非常不错:
|
### FAB 法则(Feature Advantage Benefit)
|
||||||
|
|
||||||
> STAR法则,500强面试题回答时的技巧法则,备受面试者成功者和500强HR的推崇。
|
|
||||||
由于这个法则被广泛应用于面试问题的回答,尽管我们还在写简历阶段,但是,写简历时能把面试的问题就想好,会使自己更加主动和自信,做到简历,面试关联性,逻辑性强,不至于在一个月后去面试,却把简历里的东西都忘掉了(更何况有些朋友会稍微夸大简历内容)
|
|
||||||
在我们写简历时,每个人都要写上自己的工作经历,活动经历,想必每一个同学,都会起码花上半天甚至更长的时间去搜寻脑海里所有有关的经历,争取找出最好的东西写在简历上。
|
|
||||||
但是此时,我们要注意了,简历上的任何一个信息点都有可能成为日后面试时的重点提问对象,所以说,不能只管写上让自己感觉最牛的经历就完事了,要想到今后,在面试中,你所写的经历万一被面试官问到,你真的能回答得流利,顺畅,且能通过这段经历,证明自己正是适合这个职位的人吗?
|
|
||||||
|
|
||||||
**②FAB 法则(Feature Advantage Benefit):**
|
|
||||||
|
|
||||||
- **Feature:** 是什么;
|
- **Feature:** 是什么;
|
||||||
- **Advantage:** 比别人好在哪些地方;
|
- **Advantage:** 比别人好在哪些地方;
|
||||||
@ -57,7 +71,7 @@
|
|||||||
|
|
||||||
简单来说,这个法则主要是让你的面试官知道你的优势、招了你之后对公司有什么帮助。
|
简单来说,这个法则主要是让你的面试官知道你的优势、招了你之后对公司有什么帮助。
|
||||||
|
|
||||||
### 5 项目经历怎么写?
|
## 项目经历怎么写?
|
||||||
|
|
||||||
简历上有一两个项目经历很正常,但是真正能把项目经历很好的展示给面试官的非常少。对于项目经历大家可以考虑从如下几点来写:
|
简历上有一两个项目经历很正常,但是真正能把项目经历很好的展示给面试官的非常少。对于项目经历大家可以考虑从如下几点来写:
|
||||||
|
|
||||||
@ -66,7 +80,8 @@
|
|||||||
3. 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用
|
3. 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用
|
||||||
4. 另外项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个棘手的问题的时候你是如何解决的又或者说你在这个项目用了什么技术实现了什么功能比如:用redis做缓存提高访问速度和并发量、使用消息队列削峰和降流等等。
|
4. 另外项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个棘手的问题的时候你是如何解决的又或者说你在这个项目用了什么技术实现了什么功能比如:用redis做缓存提高访问速度和并发量、使用消息队列削峰和降流等等。
|
||||||
|
|
||||||
### 6 专业技能该怎么写?
|
## 专业技能该怎么写?
|
||||||
|
|
||||||
先问一下你自己会什么,然后看看你意向的公司需要什么。一般HR可能并不太懂技术,所以他在筛选简历的时候可能就盯着你专业技能的关键词来看。对于公司有要求而你不会的技能,你可以花几天时间学习一下,然后在简历上可以写上自己了解这个技能。比如你可以这样写(下面这部分内容摘自我的简历,大家可以根据自己的情况做一些修改和完善):
|
先问一下你自己会什么,然后看看你意向的公司需要什么。一般HR可能并不太懂技术,所以他在筛选简历的时候可能就盯着你专业技能的关键词来看。对于公司有要求而你不会的技能,你可以花几天时间学习一下,然后在简历上可以写上自己了解这个技能。比如你可以这样写(下面这部分内容摘自我的简历,大家可以根据自己的情况做一些修改和完善):
|
||||||
|
|
||||||
- 计算机网络、数据结构、算法、操作系统等课内基础知识:掌握
|
- 计算机网络、数据结构、算法、操作系统等课内基础知识:掌握
|
||||||
@ -79,28 +94,29 @@
|
|||||||
- Zookeeper: 掌握
|
- Zookeeper: 掌握
|
||||||
- 常见消息队列: 掌握
|
- 常见消息队列: 掌握
|
||||||
- Linux:掌握
|
- Linux:掌握
|
||||||
- MySQL常见优化手段:掌握
|
- MySQL常见优化手段:掌握
|
||||||
- Spring Boot +Spring Cloud +Docker:了解
|
- Spring Boot +Spring Cloud +Docker:了解
|
||||||
- Hadoop 生态相关技术中的 HDFS、Storm、MapReduce、Hive、Hbase :了解
|
- Hadoop 生态相关技术中的 HDFS、Storm、MapReduce、Hive、Hbase :了解
|
||||||
- Python 基础、一些常见第三方库比如OpenCV、wxpy、wordcloud、matplotlib:熟悉
|
- Python 基础、一些常见第三方库比如OpenCV、wxpy、wordcloud、matplotlib:熟悉
|
||||||
|
|
||||||
### 7 开源程序员Markdown格式简历模板分享
|
## 排版注意事项
|
||||||
|
|
||||||
分享一个Github上开源的程序员简历模板。包括PHP程序员简历模板、iOS程序员简历模板、Android程序员简历模板、Web前端程序员简历模板、Java程序员简历模板、C/C++程序员简历模板、NodeJS程序员简历模板、架构师简历模板以及通用程序员简历模板 。
|
1. 尽量简洁,不要太花里胡哨;
|
||||||
Github地址:[https://github.com/geekcompany/ResumeSample](https://github.com/geekcompany/ResumeSample)
|
2. 一些技术名词不要弄错了大小写比如MySQL不要写成mysql,Java不要写成java。这个在我看来还是比较忌讳的,所以一定要注意这个细节;
|
||||||
|
3. 中文和数字英文之间加上空格的话看起来会舒服一点;
|
||||||
|
|
||||||
|
## 其他的一些小tips
|
||||||
我的下面这篇文章讲了如何写一份Markdown格式的简历,另外,文中还提到了一种实现 Markdown 格式到PDF、HTML、JPEG这几种格式的转换方法。
|
|
||||||
|
|
||||||
[手把手教你用Markdown写一份高质量的简历](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247484347&idx=1&sn=a986ea7e199871999a5257bd3ed78be1&chksm=fd9855dacaefdccc2c5d5f8f79c4aa1b608ad5b42936bccaefb99a850a2e6e8e2e910e1b3153&token=719595858&lang=zh_CN#rd)
|
|
||||||
|
|
||||||
### 8 其他的一些小tips
|
|
||||||
|
|
||||||
1. 尽量避免主观表述,少一点语义模糊的形容词,尽量要简洁明了,逻辑结构清晰。
|
1. 尽量避免主观表述,少一点语义模糊的形容词,尽量要简洁明了,逻辑结构清晰。
|
||||||
2. 注意排版(不需要花花绿绿的),尽量使用Markdown语法。
|
2. 如果自己有博客或者个人技术栈点的话,写上去会为你加分很多。
|
||||||
3. 如果自己有博客或者个人技术栈点的话,写上去会为你加分很多。
|
3. 如果自己的Github比较活跃的话,写上去也会为你加分很多。
|
||||||
4. 如果自己的Github比较活跃的话,写上去也会为你加分很多。
|
4. 注意简历真实性,一定不要写自己不会的东西,或者带有欺骗性的内容
|
||||||
5. 注意简历真实性,一定不要写自己不会的东西,或者带有欺骗性的内容
|
5. 项目经历建议以时间倒序排序,另外项目经历不在于多,而在于有亮点。
|
||||||
6. 项目经历建议以时间倒序排序,另外项目经历不在于多,而在于有亮点。
|
6. 如果内容过多的话,不需要非把内容压缩到一页,保持排版干净整洁就可以了。
|
||||||
7. 如果内容过多的话,不需要非把内容压缩到一页,保持排版干净整洁就可以了。
|
7. 简历最后最好能加上:“感谢您花时间阅读我的简历,期待能有机会和您共事。”这句话,显的你会很有礼貌。
|
||||||
8. 简历最后最好能加上:“感谢您花时间阅读我的简历,期待能有机会和您共事。”这句话,显的你会很有礼貌。
|
|
||||||
|
## 推荐的工具/网站
|
||||||
|
|
||||||
|
- 冷熊简历(MarkDown在线简历工具,可在线预览、编辑和生成PDF):<http://cv.ftqq.com/>
|
||||||
|
- Typora+[Java程序员简历模板](https://github.com/geekcompany/ResumeSample/blob/master/java.md)
|
||||||
|
- Guide哥自己写的Markdown模板:[https://github.com/Snailclimb/typora-markdown-resume](https://github.com/Snailclimb/typora-markdown-resume)
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,4 @@
|
|||||||
我还记得当时我去参加面试的时候,几乎每一场面试,特别是HR面和高管面的时候,面试官总是会在结尾问我:“问了你这么多问题了,你有什么问题问我吗?”。这个时候很多人内心就会陷入短暂的纠结中:我该问吗?不问的话面试官会不会对我影响不好?问什么问题?问这个问题会不会让面试官对我的影响不好啊?
|
我还记得当时我去参加面试的时候,几乎每一场面试,特别是HR面和高管面的时候,面试官总是会在结尾问我:“问了你这么多问题了,你有什么问题问我吗?”。这个时候很多人内心就会陷入短暂的纠结中:我该问吗?不问的话面试官会不会对我影响不好?问什么问题?问这个问题会不会让面试官对我的影响不好啊?
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
### 真诚一点,不要问太 Low 的问题
|
### 真诚一点,不要问太 Low 的问题
|
||||||
|
|
||||||
回答这个问题很重要的一点就是你没有必要放低自己的姿态问一些很虚或者故意讨好面试官的问题,也不要把自己从面经上学到的东西照搬下来使用。面试官也不是傻子,特别是那种特别有经验的面试官,你是真心诚意的问问题,还是从别处照搬问题来讨好面试官,人家可能一听就听出来了。总的来说,还是要真诚。除此之外,不要问太Low的问题,会显得你整个人格局比较小或者说你根本没有准备(侧面反映你对这家公司不伤心,既然你不上心,为什么要要你呢)。举例几个比较 Low 的问题,大家看看自己有没有问过其中的问题:
|
回答这个问题很重要的一点就是你没有必要放低自己的姿态问一些很虚或者故意讨好面试官的问题,也不要把自己从面经上学到的东西照搬下来使用。面试官也不是傻子,特别是那种特别有经验的面试官,你是真心诚意的问问题,还是从别处照搬问题来讨好面试官,人家可能一听就听出来了。总的来说,还是要真诚。除此之外,不要问太 Low 的问题,会显得你整个人格局比较小或者说你根本没有准备(侧面反映你对这家公司不上心,既然你不上心,为什么要要你呢)。举例几个比较 Low 的问题,大家看看自己有没有问过其中的问题:
|
||||||
|
|
||||||
- 贵公司的主要业务是什么?(面试之前自己不知道提前网上查一下吗?)
|
- 贵公司的主要业务是什么?(面试之前自己不知道提前网上查一下吗?)
|
||||||
- 贵公司的男女比例如何?(考虑脱单?记住你是来工作的!)
|
- 贵公司的男女比例如何?(考虑脱单?记住你是来工作的!)
|
||||||
@ -28,9 +28,9 @@
|
|||||||
#### 面对HR或者其他Level比较低的面试官时
|
#### 面对HR或者其他Level比较低的面试官时
|
||||||
|
|
||||||
1. **能不能谈谈你作为一个公司老员工对公司的感受?** (这个问题比较容易回答,不会让面试官陷入无话可说的尴尬境地。另外,从面试官的回答中你可以加深对这个公司的了解,让你更加清楚这个公司到底是不是你想的那样或者说你是否能适应这个公司的文化。除此之外,这样的问题在某种程度上还可以拉进你与面试官的距离。)
|
1. **能不能谈谈你作为一个公司老员工对公司的感受?** (这个问题比较容易回答,不会让面试官陷入无话可说的尴尬境地。另外,从面试官的回答中你可以加深对这个公司的了解,让你更加清楚这个公司到底是不是你想的那样或者说你是否能适应这个公司的文化。除此之外,这样的问题在某种程度上还可以拉进你与面试官的距离。)
|
||||||
2. **能不能问一下,你当时因为什么原因选择加入这家公司的呢或者说这家公司有哪些地方吸引你?有什么地方你觉得还不太好或者可以继续完善吗?** (类似第一个问题,都是问面试官个人对于公司的看法,)
|
2. **能不能问一下,你当时因为什么原因选择加入这家公司的呢或者说这家公司有哪些地方吸引你?有什么地方你觉得还不太好或者可以继续完善吗?** (类似第一个问题,都是问面试官个人对于公司的看法。)
|
||||||
3. **我觉得我这次表现的不是太好,你有什么建议或者评价给我吗?**(这个是我常问的。我觉得说自己表现不好只是这个语境需要这样来说,这样可以显的你比较谦虚好学上进。)
|
3. **我觉得我这次表现的不是太好,你有什么建议或者评价给我吗?**(这个是我常问的。我觉得说自己表现不好只是这个语境需要这样来说,这样可以显的你比较谦虚好学上进。)
|
||||||
4. **接下来我会有一段空档期,有什么值得注意或者建议学习的吗?** (体现出你对工作比较上心,自助学习意识比较强。)
|
4. **接下来我会有一段空档期,有什么值得注意或者建议学习的吗?** (体现出你对工作比较上心,自助学习意识比较强。)
|
||||||
5. **这个岗位为什么还在招人?** (岗位真实性和价值咨询)
|
5. **这个岗位为什么还在招人?** (岗位真实性和价值咨询)
|
||||||
6. **大概什么时候能给我回复呢?** (终面的时候,如果面试官没有说的话,可以问一下)
|
6. **大概什么时候能给我回复呢?** (终面的时候,如果面试官没有说的话,可以问一下)
|
||||||
7. ......
|
7. ......
|
@ -0,0 +1,222 @@
|
|||||||
|
本文的内容都是根据读者投稿的真实面试经历改编而来,首次尝试这种风格的文章,花了几天晚上才总算写完,希望对你有帮助。
|
||||||
|
|
||||||
|
本文主要涵盖下面的内容:
|
||||||
|
|
||||||
|
1. 分布式商城系统:架构图讲解;
|
||||||
|
2. 消息队列相关:削峰和解耦;
|
||||||
|
3. Redis 相关:缓存穿透问题的解决;
|
||||||
|
4. 一些基础问题:
|
||||||
|
- 网络相关:1.浏览器输入 URL 发生了什么? 2.TCP 和 UDP 区别? 3.TCP 如何保证传输可靠性?
|
||||||
|
- Java 基础:1. 既然有了字节流,为什么还要有字符流? 2.深拷贝 和 浅拷贝有啥区别呢?
|
||||||
|
|
||||||
|
下面是正文!
|
||||||
|
|
||||||
|
面试开始,坐在我前面的就是这次我的面试官吗?这发量看着根本不像程序员啊?我心里正嘀咕着,只听见面试官说:“小伙,下午好,我今天就是你的面试官,咱们开始面试吧!”。
|
||||||
|
|
||||||
|
### 第一面开始
|
||||||
|
|
||||||
|
**面试官:** 我也不用多说了,你先自我介绍一下吧,简历上有的就不要再说了哈。
|
||||||
|
|
||||||
|
**我:** 内心 os:"果然如我所料,就知道会让我先自我介绍一下,还好我看了 [JavaGuide](https://github.com/Snailclimb/JavaGuide "JavaGuide") ,学到了一些套路。套路总结起来就是:**最好准备好两份自我介绍,一份对 hr 说的,主要讲能突出自己的经历,会的编程技术一语带过;另一份对技术面试官说的,主要讲自己会的技术细节,项目经验,经历那些就一语带过。** 所以,我按照这个套路准备了一个还算通用的模板,毕竟我懒嘛!不想多准备一个自我介绍,整个通用的多好!
|
||||||
|
|
||||||
|
> 面试官,您好!我叫小李子。大学时间我主要利用课外时间学习 Java 相关的知识。在校期间参与过一个某某系统的开发,主要负责数据库设计和后端系统开发.,期间解决了什么问题,巴拉巴拉。另外,我自己在学习过程中也参照网上的教程写过一个电商系统的网站,写这个电商网站主要是为了能让自己接触到分布式系统的开发。在学习之余,我比较喜欢通过博客整理分享自己所学知识。我现在已经是某社区的认证作者,写过一系列关于 线程池使用以及源码分析的文章深受好评。另外,我获得过省级编程比赛二等奖,我将这个获奖项目开源到 Github 还收获了 2k 的 Star 呢?
|
||||||
|
|
||||||
|
**面试官:** 你刚刚说参考网上的教程做了一个电商系统?你能画画这个电商系统的架构图吗?
|
||||||
|
|
||||||
|
**我:** 内心 os: "这可难不倒我!早知道写在简历上的项目要重视了,提前都把这个系统的架构图画了好多遍了呢!"
|
||||||
|
|
||||||
|
<img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/商城系统-架构图plus.png" style="zoom:50%;" />
|
||||||
|
|
||||||
|
做过分布式电商系统的一定很熟悉上面的架构图(目前比较流行的是微服务架构,但是如果你有分布式开发经验也是非常加分的!)。
|
||||||
|
|
||||||
|
**面试官:** 简单介绍一下你做的这个系统吧!
|
||||||
|
|
||||||
|
**我:** 我一本正经的对着我刚刚画的商城架构图开始了满嘴造火箭的讲起来:
|
||||||
|
|
||||||
|
> 本系统主要分为展示层、服务层和持久层这三层。表现层顾名思义主要就是为了用来展示,比如我们的后台管理系统的页面、商城首页的页面、搜索系统的页面等等,这一层都只是作为展示,并没有提供任何服务。
|
||||||
|
>
|
||||||
|
> 展示层和服务层一般是部署在不同的机器上来提高并发量和扩展性,那么展示层和服务层怎样才能交互呢?在本系统中我们使用 Dubbo 来进行服务治理。Dubbo 是一款高性能、轻量级的开源 Java RPC 框架。Dubbo 在本系统的主要作用就是提供远程 RPC 调用。在本系统中服务层的信息通过 Dubbo 注册给 ZooKeeper,表现层通过 Dubbo 去 ZooKeeper 中获取服务的相关信息。Zookeeper 的作用仅仅是存放提供服务的服务器的地址和一些服务的相关信息,实现 RPC 远程调用功能的还是 Dubbo。如果需要引用到某个服务的时候,我们只需要在配置文件中配置相关信息就可以在代码中直接使用了,就像调用本地方法一样。假如说某个服务的使用量增加时,我们只用为这单个服务增加服务器,而不需要为整个系统添加服务。
|
||||||
|
>
|
||||||
|
> 另外,本系统的数据库使用的是常用的 MySQL,并且用到了数据库中间件 MyCat。另外,本系统还用到 redis 内存数据库来作为缓存来提高系统的反应速度。假如用户第一次访问数据库中的某些数据,这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。
|
||||||
|
>
|
||||||
|
> 系统还用到了 Elasticsearch 来提供搜索功能。使用 Elasticsearch 我们可以非常方便的为我们的商城系统添加必备的搜索功能,并且使用 Elasticsearch 还能提供其它非常实用的功能,并且很容易扩展。
|
||||||
|
|
||||||
|
**面试官:** 我看你的系统里面还用到了消息队列,能说说为什么要用它吗?
|
||||||
|
|
||||||
|
**我:**
|
||||||
|
|
||||||
|
> 使用消息队列主要是为了:
|
||||||
|
>
|
||||||
|
> 1. 减少响应所需时间和削峰。
|
||||||
|
> 2. 降低系统耦合性(解耦/提升系统可扩展性)。
|
||||||
|
|
||||||
|
**面试官:** 你这说的太简单了!能不能稍微详细一点,最好能画图给我解释一下。
|
||||||
|
|
||||||
|
**我:** 内心 os:"都 2019 年了,大部分面试者都能对消息队列的为系统带来的这两个好处倒背如流了,如果你想走的更远就要别别人懂的更深一点!"
|
||||||
|
|
||||||
|
> 当我们不使用消息队列的时候,所有的用户的请求会直接落到服务器,然后通过数据库或者缓存响应。假如在高并发的场景下,如果没有缓存或者数据库承受不了这么大的压力的话,就会造成响应速度缓慢,甚至造成数据库宕机。但是,在使用消息队列之后,用户的请求数据发送给了消息队列之后就可以立即返回,再由消息队列的消费者进程从消息队列中获取数据,异步写入数据库,不过要确保消息不被重复消费还要考虑到消息丢失问题。由于消息队列服务器处理速度快于数据库,因此响应速度得到大幅改善。
|
||||||
|
>
|
||||||
|
> 文字 is too 空洞,直接上图吧!下图展示了使用消息前后系统处理用户请求的对比(ps:我自己都被我画的这个图美到了,如果你也觉得这张图好看的话麻烦来个素质三连!)。
|
||||||
|
>
|
||||||
|
> 
|
||||||
|
>
|
||||||
|
> 通过以上分析我们可以得出**消息队列具有很好的削峰作用的功能**——即**通过异步处理,将短时间高并发产生的事务消息存储在消息队列中,从而削平高峰期的并发事务。** 举例:在电子商务一些秒杀、促销活动中,合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击。如下图所示:
|
||||||
|
>
|
||||||
|
> 
|
||||||
|
>
|
||||||
|
> 使用消息队列还可以降低系统耦合性。我们知道如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性无疑更好一些。还是直接上图吧:
|
||||||
|
>
|
||||||
|
> 
|
||||||
|
>
|
||||||
|
> 生产者(客户端)发送消息到消息队列中去,接受者(服务端)处理消息,需要消费的系统直接去消息队列取消息进行消费即可而不需要和其他系统有耦合, 这显然也提高了系统的扩展性。
|
||||||
|
|
||||||
|
**面试官:** 你觉得它有什么缺点吗?或者说怎么考虑用不用消息队列?
|
||||||
|
|
||||||
|
**我:** 内心 os: "面试官真鸡贼!这不是勾引我上钩么?还好我准备充分。"
|
||||||
|
|
||||||
|
> 我觉得可以从下面几个方面来说:
|
||||||
|
>
|
||||||
|
> 1. **系统可用性降低:** 系统可用性在某种程度上降低,为什么这样说呢?在加入 MQ 之前,你不用考虑消息丢失或者说 MQ 挂掉等等的情况,但是,引入 MQ 之后你就需要去考虑了!
|
||||||
|
> 2. **系统复杂性提高:** 加入 MQ 之后,你需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺序性等等问题!
|
||||||
|
> 3. **一致性问题:** 我上面讲了消息队列可以实现异步,消息队列带来的异步确实可以提高系统响应速度。但是,万一消息的真正消费者并没有正确消费消息怎么办?这样就会导致数据不一致的情况了!
|
||||||
|
|
||||||
|
**面试官**:做项目的过程中遇到了什么问题吗?解决了吗?如果解决的话是如何解决的呢?
|
||||||
|
|
||||||
|
**我** : 内心 os: "做的过程中好像也没有遇到什么问题啊!怎么办?怎么办?突然想到可以说我在使用 Redis 过程中遇到的问题,毕竟我对 Redis 还算熟悉嘛,**把面试官往这个方向吸引**,准没错。"
|
||||||
|
|
||||||
|
> 我在使用 Redis 对常用数据进行缓冲的过程中出现了缓存穿透问题。然后,我通过谷歌搜索相关的解决方案来解决的。
|
||||||
|
|
||||||
|
**面试官:** 你还知道缓存穿透啊?不错啊!来说说什么是缓存穿透以及你最后的解决办法。
|
||||||
|
|
||||||
|
**我:** 我先来谈谈什么是缓存穿透吧!
|
||||||
|
|
||||||
|
> 缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。举个例子:某个黑客故意制造我们缓存中不存在的 key 发起大量请求,导致大量请求落到数据库。
|
||||||
|
>
|
||||||
|
> 总结一下就是:
|
||||||
|
>
|
||||||
|
> 1. 缓存层不命中。
|
||||||
|
> 2. 存储层不命中,不将空结果写回缓存。
|
||||||
|
> 3. 返回空结果给客户端。
|
||||||
|
>
|
||||||
|
> 一般 MySQL 默认的最大连接数在 150 左右,这个可以通过 `show variables like '%max_connections%';`命令来查看。最大连接数一个还只是一个指标,cpu,内存,磁盘,网络等物理条件都是其运行指标,这些指标都会限制其并发能力!所以,一般 3000 的并发请求就能打死大部分数据库了。
|
||||||
|
|
||||||
|
**面试官:** 小伙子不错啊!还准备问你:“为什么 3000 的并发能把支持最大连接数 4000 数据库压死?”想不到你自己就提前回答了!不错!
|
||||||
|
|
||||||
|
**我:** 别夸了!别夸了!我再来说说我知道的一些解决办法以及我最后采用的方案吧!您帮忙看看有没有问题。
|
||||||
|
|
||||||
|
> 最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。
|
||||||
|
>
|
||||||
|
> 参数校验通过的情况还是会出现缓存穿透,我们还可以通过以下几个方案来解决这个问题:
|
||||||
|
>
|
||||||
|
> **1)缓存无效 key** : 如果缓存和数据库都查不到某个 key 的数据就写一个到 redis 中去并设置过期时间,具体命令如下:`SET key value EX 10086`。这种方式可以解决请求的 key 变化不频繁的情况,如何黑客恶意攻击,每次构建的不同的请求 key,会导致 redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。
|
||||||
|
>
|
||||||
|
> 另外,这里多说一嘴,一般情况下我们是这样设计 key 的: `表名:列名:主键名:主键值`。
|
||||||
|
>
|
||||||
|
> **2)布隆过滤器:** 布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。我们需要的就是判断 key 是否合法,有没有感觉布隆过滤器就是我们想要找的那个“人”。
|
||||||
|
|
||||||
|
**面试官:** 不错不错!你还知道布隆过滤器啊!来给我谈一谈。
|
||||||
|
|
||||||
|
**我:** 内心 os:“如果你准备过海量数据处理的面试题,你一定对:“如何确定一个数字是否在于包含大量数字的数字集中(数字集很大,5 亿以上!)?”这个题目很了解了!解决这道题目就要用到布隆过滤器。”
|
||||||
|
|
||||||
|
> 布隆过滤器在针对海量数据去重或者验证数据合法性的时候非常有用。**布隆过滤器的本质实际上是 “位(bit)数组”,也就是说每一个存入布隆过滤器的数据都只占一位。相比于我们平时常用的的 List、Map 、Set 等数据结构,它占用空间更少并且效率更高,但是缺点是其返回的结果是概率性的,而不是非常准确的。**
|
||||||
|
>
|
||||||
|
> **当一个元素加入布隆过滤器中的时候,会进行如下操作:**
|
||||||
|
>
|
||||||
|
> 1. 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。
|
||||||
|
> 2. 根据得到的哈希值,在位数组中把对应下标的值置为 1。
|
||||||
|
>
|
||||||
|
> **当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行如下操作:**
|
||||||
|
>
|
||||||
|
> 1. 对给定元素再次进行相同的哈希计算;
|
||||||
|
> 2. 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。
|
||||||
|
>
|
||||||
|
> 举个简单的例子:
|
||||||
|
>
|
||||||
|
> 
|
||||||
|
>
|
||||||
|
> 如图所示,当字符串存储要加入到布隆过滤器中时,该字符串首先由多个哈希函数生成不同的哈希值,然后在对应的位数组的下表的元素设置为 1(当位数组初始化时 ,所有位置均为 0)。当第二次存储相同字符串时,因为先前的对应位置已设置为 1,所以很容易知道此值已经存在(去重非常方便)。
|
||||||
|
>
|
||||||
|
> 如果我们需要判断某个字符串是否在布隆过滤器中时,只需要对给定字符串再次进行相同的哈希计算,得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。
|
||||||
|
>
|
||||||
|
> **不同的字符串可能哈希出来的位置相同,这种情况我们可以适当增加位数组大小或者调整我们的哈希函数。**
|
||||||
|
>
|
||||||
|
> 综上,我们可以得出:**布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。**
|
||||||
|
|
||||||
|
**面试官:** 看来你对布隆过滤器了解的还挺不错的嘛!那你快说说你最后是怎么利用它来解决缓存穿透的。
|
||||||
|
|
||||||
|
**我:** 知道了布隆过滤器的原理就之后就很容易做了。我是利用 Redis 布隆过滤器来做的。我把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,我会先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。总结一下就是下面这张图(这张图片不是我画的,为了省事直接在网上找的):
|
||||||
|
|
||||||
|
<img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/布隆过滤器-缓存穿透-redis.png" style="zoom:50%;" />
|
||||||
|
|
||||||
|
更多关于布隆过滤器的内容可以看我的这篇原创:[《不了解布隆过滤器?一文给你整的明明白白!》](https://github.com/Snailclimb/JavaGuide/blob/master/docs/dataStructures-algorithms/data-structure/bloom-filter.md "《不了解布隆过滤器?一文给你整的明明白白!》") ,强烈推荐,个人感觉网上应该找不到总结的这么明明白白的文章了。
|
||||||
|
|
||||||
|
**面试官:** 好了好了。项目就暂时问到这里吧!下面有一些比较基础的问题我简单地问一下你。内心 os: 难不成这家伙满口高并发,连最基础的东西都不会吧!
|
||||||
|
|
||||||
|
**我:** 好的好的!没问题!
|
||||||
|
|
||||||
|
**面试官:** 浏览器输入 URL 发生了什么?
|
||||||
|
|
||||||
|
**我:** 内心 os:“很常问的一个问题,建议拿小本本记好了!另外,百度好像最喜欢问这个问题,去百度面试可要提前备好这道题的功课哦!相似问题:打开一个网页,整个过程会使用哪些协议?”。
|
||||||
|
|
||||||
|
> 图解(图片来源:《图解 HTTP》):
|
||||||
|
>
|
||||||
|
> <img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/url输入到展示出来的过程.jpg" style="zoom:50%;" />
|
||||||
|
>
|
||||||
|
> 总体来说分为以下几个过程:
|
||||||
|
>
|
||||||
|
> 1. DNS 解析
|
||||||
|
> 2. TCP 连接
|
||||||
|
> 3. 发送 HTTP 请求
|
||||||
|
> 4. 服务器处理请求并返回 HTTP 报文
|
||||||
|
> 5. 浏览器解析渲染页面
|
||||||
|
> 6. 连接结束
|
||||||
|
>
|
||||||
|
> 具体可以参考下面这篇文章:
|
||||||
|
>
|
||||||
|
> - [https://segmentfault.com/a/1190000006879700](https://segmentfault.com/a/1190000006879700 "https://segmentfault.com/a/1190000006879700")
|
||||||
|
|
||||||
|
**面试官:** TCP 和 UDP 区别?
|
||||||
|
|
||||||
|
**我:**
|
||||||
|
|
||||||
|
> 
|
||||||
|
>
|
||||||
|
> UDP 在传送数据之前不需要先建立连接,远地主机在收到 UDP 报文后,不需要给出任何确认。虽然 UDP 不提供可靠交付,但在某些情况下 UDP 确是一种最有效的工作方式(一般用于即时通信),比如: QQ 语音、 QQ 视频 、直播等等
|
||||||
|
>
|
||||||
|
> TCP 提供面向连接的服务。在传送数据之前必须先建立连接,数据传送结束后要释放连接。 TCP 不提供广播或多播服务。由于 TCP 要提供可靠的,面向连接的传输服务(TCP 的可靠体现在 TCP 在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完后,还会断开连接用来节约系统资源),这一难以避免增加了许多开销,如确认,流量控制,计时器以及连接管理等。这不仅使协议数据单元的首部增大很多,还要占用许多处理机资源。TCP 一般用于文件传输、发送和接收邮件、远程登录等场景。
|
||||||
|
|
||||||
|
**面试官:** TCP 如何保证传输可靠性?
|
||||||
|
|
||||||
|
**我:**
|
||||||
|
|
||||||
|
> 1. 应用数据被分割成 TCP 认为最适合发送的数据块。
|
||||||
|
> 2. TCP 给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层。
|
||||||
|
> 3. **校验和:** TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段。
|
||||||
|
> 4. TCP 的接收端会丢弃重复的数据。
|
||||||
|
> 5. **流量控制:** TCP 连接的每一方都有固定大小的缓冲空间,TCP 的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议。 (TCP 利用滑动窗口实现流量控制)
|
||||||
|
> 6. **拥塞控制:** 当网络拥塞时,减少数据的发送。
|
||||||
|
> 7. **ARQ 协议:** 也是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组。
|
||||||
|
> 8. **超时重传:** 当 TCP 发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。
|
||||||
|
|
||||||
|
**面试官:** 我再来问你一些 Java 基础的问题吧!小伙子。
|
||||||
|
|
||||||
|
**我:** 好的。(内心 os:“你尽管来!”)
|
||||||
|
|
||||||
|
**面试官:** 既然有了字节流,为什么还要有字符流?
|
||||||
|
|
||||||
|
我:内心 os :“问题本质想问:**不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?**”
|
||||||
|
|
||||||
|
> 字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。
|
||||||
|
|
||||||
|
**面试官**:深拷贝 和 浅拷贝有啥区别呢?
|
||||||
|
|
||||||
|
**我:**
|
||||||
|
|
||||||
|
> 1. **浅拷贝**:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。
|
||||||
|
> 2. **深拷贝**:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。
|
||||||
|
>
|
||||||
|
> 
|
||||||
|
|
||||||
|
**面试官:** 好的!面试结束。小伙子可以的!回家等通知吧!
|
||||||
|
|
||||||
|
**我:** 好的好的!辛苦您了!
|
@ -1,3 +1,22 @@
|
|||||||
|
点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。
|
||||||
|
|
||||||
|
<!-- TOC -->
|
||||||
|
|
||||||
|
- [何谓悲观锁与乐观锁](#何谓悲观锁与乐观锁)
|
||||||
|
- [悲观锁](#悲观锁)
|
||||||
|
- [乐观锁](#乐观锁)
|
||||||
|
- [两种锁的使用场景](#两种锁的使用场景)
|
||||||
|
- [乐观锁常见的两种实现方式](#乐观锁常见的两种实现方式)
|
||||||
|
- [1. 版本号机制](#1-版本号机制)
|
||||||
|
- [2. CAS算法](#2-cas算法)
|
||||||
|
- [乐观锁的缺点](#乐观锁的缺点)
|
||||||
|
- [1 ABA 问题](#1-aba-问题)
|
||||||
|
- [2 循环时间长开销大](#2-循环时间长开销大)
|
||||||
|
- [3 只能保证一个共享变量的原子操作](#3-只能保证一个共享变量的原子操作)
|
||||||
|
- [CAS与synchronized的使用情景](#cas与synchronized的使用情景)
|
||||||
|
|
||||||
|
<!-- /TOC -->
|
||||||
|
|
||||||
### 何谓悲观锁与乐观锁
|
### 何谓悲观锁与乐观锁
|
||||||
|
|
||||||
> 乐观锁对应于生活中乐观的人总是想着事情往好的方向发展,悲观锁对应于生活中悲观的人总是想着事情往坏的方向发展。这两种人各有优缺点,不能不以场景而定说一种人好于另外一种人。
|
> 乐观锁对应于生活中乐观的人总是想着事情往好的方向发展,悲观锁对应于生活中悲观的人总是想着事情往坏的方向发展。这两种人各有优缺点,不能不以场景而定说一种人好于另外一种人。
|
||||||
@ -64,8 +83,6 @@ JDK 1.5 以后的 `AtomicStampedReference 类`就提供了此种能力,其中
|
|||||||
|
|
||||||
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了`AtomicReference类`来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用`AtomicReference类`把多个共享变量合并成一个共享变量来操作。
|
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了`AtomicReference类`来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用`AtomicReference类`把多个共享变量合并成一个共享变量来操作。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### CAS与synchronized的使用情景
|
### CAS与synchronized的使用情景
|
||||||
|
|
||||||
> **简单的来说CAS适用于写比较少的情况下(多读场景,冲突一般较少),synchronized适用于写比较多的情况下(多写场景,冲突一般较多)**
|
> **简单的来说CAS适用于写比较少的情况下(多读场景,冲突一般较少),synchronized适用于写比较多的情况下(多写场景,冲突一般较多)**
|
||||||
@ -73,10 +90,17 @@ CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS
|
|||||||
1. 对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
|
1. 对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
|
||||||
2. 对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。
|
2. 对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。
|
||||||
|
|
||||||
|
|
||||||
补充: Java并发编程这个领域中synchronized关键字一直都是元老级的角色,很久之前很多人都会称它为 **“重量级锁”** 。但是,在JavaSE 1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的 **偏向锁** 和 **轻量级锁** 以及其它**各种优化**之后变得在某些情况下并不是那么重了。synchronized的底层实现主要依靠 **Lock-Free** 的队列,基本思路是 **自旋后阻塞**,**竞争切换后继续竞争锁**,**稍微牺牲了公平性,但获得了高吞吐量**。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。
|
补充: Java并发编程这个领域中synchronized关键字一直都是元老级的角色,很久之前很多人都会称它为 **“重量级锁”** 。但是,在JavaSE 1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的 **偏向锁** 和 **轻量级锁** 以及其它**各种优化**之后变得在某些情况下并不是那么重了。synchronized的底层实现主要依靠 **Lock-Free** 的队列,基本思路是 **自旋后阻塞**,**竞争切换后继续竞争锁**,**稍微牺牲了公平性,但获得了高吞吐量**。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。
|
||||||
|
|
||||||
|
## 公众号
|
||||||
|
|
||||||
|
如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。
|
||||||
|
|
||||||
|
**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"面试突击"** 即可免费领取!
|
||||||
|
|
||||||
|
**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
144
docs/github-trending/2019-12.md
Normal file
144
docs/github-trending/2019-12.md
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
# 年末将至,值得你关注的16个Java 开源项目!
|
||||||
|
|
||||||
|
Star 的数量统计于 2019-12-29。
|
||||||
|
|
||||||
|
### 1.JavaGuide
|
||||||
|
|
||||||
|
Guide 哥大三开始维护的,目前算是纯 Java 类型项目中 Star 数量最多的项目了。但是,本仓库的价值远远(+N次 )比不上像 Spring Boot、Elasticsearch 等等这样非常非常非常优秀的项目。希望以后我也有能力为这些项目贡献一些有价值的代码。
|
||||||
|
|
||||||
|
- **Github 地址**:<https://github.com/Snailclimb/JavaGuide>
|
||||||
|
- **Star**: 66.3k
|
||||||
|
- **介绍**: 【Java 学习+面试指南】 一份涵盖大部分 Java 程序员所需要掌握的核心知识。
|
||||||
|
|
||||||
|
### 2.java-design-patterns
|
||||||
|
|
||||||
|
感觉还不错。根据官网介绍:
|
||||||
|
|
||||||
|
> 设计模式是程序员在设计应用程序或系统时可以用来解决常见问题的最佳形式化实践。 设计模式可以通过提供经过测试的,经过验证的开发范例来加快开发过程。 重用设计模式有助于防止引起重大问题的细微问题,并且还可以提高熟悉模式的编码人员和架构师的代码可读性。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- **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 开源项目集合。
|
||||||
|
|
||||||
|
|
||||||
|
|
60
docs/github-trending/2019-3.md
Normal file
60
docs/github-trending/2019-3.md
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
### 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 地址**: <https://github.com/didi/DoraemonKit>
|
||||||
|
- **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。
|
||||||
|
|
98
docs/github-trending/2019-4.md
Normal file
98
docs/github-trending/2019-4.md
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
以下涉及到的数据统计与 2019 年 5 月 1 日 12 点,数据来源:<https://github.com/trending/java?since=monthly> 。
|
||||||
|
|
||||||
|
下面的内容从 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 地址**:<https://github.com/CyC2018/CS-Notes>
|
||||||
|
- **Star**: 59.2k (4,012 stars this month)
|
||||||
|
- **介绍**: 技术面试必备基础知识。
|
||||||
|
|
||||||
|
### 4. ghidra
|
||||||
|
|
||||||
|
- **Github 地址**:<https://github.com/NationalSecurityAgency/ghidra>
|
||||||
|
- **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 地址**: <https://github.com/ZXZxin/ZXBlog>
|
||||||
|
- **star**: 2.1 k (2,086 stars this month)
|
||||||
|
- **介绍**: 记录各种学习笔记(算法、Java、数据库、并发......)。
|
||||||
|
|
||||||
|
### 7.DoraemonKit
|
||||||
|
|
||||||
|
- **Github地址**: <https://github.com/didi/DoraemonKit>
|
||||||
|
- **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 地址**:<https://github.com/apache/skywalking>
|
||||||
|
- **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地址**:<https://github.com/looly/hutool>
|
||||||
|
- **star**: 4.5 k (1,031 stars this month)
|
||||||
|
- **介绍**: Hutool是一个Java工具包,也只是一个工具包,它帮助我们简化每一行代码,减少每一个方法,让Java语言也可以“甜甜的”。Hutool最初是我项目中“util”包的一个整理,后来慢慢积累并加入更多非业务相关功能,并广泛学习其它开源项目精髓,经过自己整理修改,最终形成丰富的开源工具集。官网:<https://www.hutool.cn/> 。
|
125
docs/github-trending/2019-5.md
Normal file
125
docs/github-trending/2019-5.md
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
以下涉及到的数据统计与 2019 年 6 月 1 日 18 点,数据来源:<https://github.com/trending/java?since=monthly> 。下面推荐的内容从 Java 学习文档到最热门的框架再到热门的工具应有尽有,建议收藏+在看!
|
||||||
|
|
||||||
|
### 1.LeetCodeAnimation
|
||||||
|
|
||||||
|
- **Github 地址**: <https://github.com/MisterBooo/LeetCodeAnimation>
|
||||||
|
- **Star**: 29.0k (11,492 stars this month)
|
||||||
|
- **介绍**: Demonstrate all the questions on LeetCode in the form of animation.(用动画的形式呈现解LeetCode题目的思路)。
|
||||||
|
|
||||||
|
### 2.CS-Notes
|
||||||
|
|
||||||
|
- **Github 地址**:<https://github.com/CyC2018/CS-Notes>
|
||||||
|
- **Star**: 64.4k (5513 stars this month)
|
||||||
|
- **介绍**: 技术面试必备基础知识、Leetcode 题解、后端面试、Java 面试、春招、秋招、操作系统、计算机网络、系统设计。
|
||||||
|
|
||||||
|
### 3.JavaGuide
|
||||||
|
|
||||||
|
- **Github 地址**:<https://github.com/Snailclimb/JavaGuide>
|
||||||
|
- **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 地址**:<https://github.com/TheAlgorithms/Java>
|
||||||
|
- **Star**:14.3k (1,334 stars this month)
|
||||||
|
- **介绍**: All Algorithms implemented in Java。
|
||||||
|
|
||||||
|
### 8.server
|
||||||
|
|
||||||
|
- **Github 地址**:<https://github.com/wildfirechat/server>
|
||||||
|
- **star**: 2.2 k (1,275 stars this month)
|
||||||
|
- **介绍**: 全开源即时通讯(IM)系统。
|
||||||
|
|
||||||
|
### 9.litemall
|
||||||
|
|
||||||
|
- **Github 地址**:<https://github.com/linlinjava/litemall>
|
||||||
|
- **Star**: 7.1k (1,114 stars this month)
|
||||||
|
- **介绍**: 又一个小商城。litemall = Spring Boot后端 + Vue管理员前端 + 微信小程序用户前端 + Vue用户移动端。
|
||||||
|
|
||||||
|
### 10.Linkage-RecyclerView
|
||||||
|
|
||||||
|
- **Github 地址**:<https://github.com/KunMinX/Linkage-RecyclerView>
|
||||||
|
- **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 地址** : <https://github.com/hollischuang/toBeTopJavaer>
|
||||||
|
- **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 地址** : <https://github.com/iluwatar/java-design-patterns>
|
||||||
|
- **Star**: 41.5k (955 stars this month)
|
||||||
|
- **介绍**: Design patterns implemented in Java。
|
||||||
|
|
||||||
|
### 14.apollo
|
||||||
|
|
||||||
|
- **Github 地址** : <https://github.com/ctripcorp/apollo>
|
||||||
|
- **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地址**:<https://github.com/apache/dubbo>
|
||||||
|
- **star**: 26.9 k (769 stars this month)
|
||||||
|
- **介绍**: Apache Dubbo是一个基于Java的高性能开源RPC框架。
|
||||||
|
|
||||||
|
### 17.DoraemonKit
|
||||||
|
|
||||||
|
- **Github地址**: <https://github.com/didi/DoraemonKit>
|
||||||
|
- **Star**: 8.5k (909 stars this month)
|
||||||
|
- **介绍**: 简称 "DoKit" 。一款功能齐全的客户端( iOS 、Android )研发助手,你值得拥有。
|
||||||
|
|
||||||
|
### 18.halo
|
||||||
|
|
||||||
|
- **Github地址**: <https://github.com/halo-dev/halo>
|
||||||
|
- **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地址**:<https://github.com/looly/hutool>
|
||||||
|
- **star**: 5,3 k (812 stars this month)
|
||||||
|
- **介绍**: Hutool是一个Java工具包,也只是一个工具包,它帮助我们简化每一行代码,减少每一个方法,让Java语言也可以“甜甜的”。Hutool最初是我项目中“util”包的一个整理,后来慢慢积累并加入更多非业务相关功能,并广泛学习其它开源项目精髓,经过自己整理修改,最终形成丰富的开源工具集。官网:<https://www.hutool.cn/> 。
|
119
docs/github-trending/2019-6.md
Normal file
119
docs/github-trending/2019-6.md
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
### 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地址**:<https://github.com/apache/dubbo>
|
||||||
|
- **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 地址**:<https://github.com/Snailclimb/JavaGuide>
|
||||||
|
- **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的设备信息。
|
@ -1,4 +1,8 @@
|
|||||||
- [2018 年 12 月](https://github.com/Snailclimb/JavaGuide/blob/master/docs/github-trending/2018-12.md)
|
- [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 年 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 年 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)
|
||||||
|
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>JavaGuide</title>
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
|
||||||
<meta name="description" content="Description">
|
|
||||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
|
||||||
<link rel="stylesheet" href="//unpkg.com/docsify/lib/themes/vue.css">
|
|
||||||
<link rel="stylesheet" href="//unpkg.com/docsify/lib/themes/prism.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script>
|
|
||||||
window.$docsify = {
|
|
||||||
name: 'JavaGuide',
|
|
||||||
repo: 'https://github.com/Snailclimb/JavaGuide',
|
|
||||||
maxLevel: 3,//最大支持渲染的标题层级
|
|
||||||
homepage: 'HomePage.md',
|
|
||||||
coverpage: true,//封面,_coverpage.md
|
|
||||||
auto2top: true,//切换页面后是否自动跳转到页面顶部
|
|
||||||
//logo: 'https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3logo-透明.png' ,
|
|
||||||
search: {
|
|
||||||
//maxAge: 86400000, // 过期时间,单位毫秒,默认一天
|
|
||||||
paths: 'auto',
|
|
||||||
placeholder: '搜索',
|
|
||||||
noData: '找不到结果',
|
|
||||||
// 搜索标题的最大程级, 1 - 6
|
|
||||||
depth: 3,
|
|
||||||
},
|
|
||||||
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<script src="//unpkg.com/docsify/lib/docsify.min.js"></script>
|
|
||||||
<!--Java代码高亮-->
|
|
||||||
<script src="//unpkg.com/prismjs/components/prism-java.min.js"></script>
|
|
||||||
<!--全文搜索,直接用官方提供的无法生效-->
|
|
||||||
<script src="https://cdn.bootcss.com/docsify/4.5.9/plugins/search.min.js"></script>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -30,20 +30,23 @@
|
|||||||
|
|
||||||
在讲 BIO,NIO,AIO 之前先来回顾一下这样几个概念:同步与异步,阻塞与非阻塞。
|
在讲 BIO,NIO,AIO 之前先来回顾一下这样几个概念:同步与异步,阻塞与非阻塞。
|
||||||
|
|
||||||
**同步与异步**
|
关于同步和异步的概念解读困扰着很多程序员,大部分的解读都会带有自己的一点偏见。参考了 [Stackoverflow](https://stackoverflow.com/questions/748175/asynchronous-vs-synchronous-execution-what-does-it-really-mean)相关问题后对原有答案进行了进一步完善:
|
||||||
|
|
||||||
- **同步:** 同步就是发起一个调用后,被调用者未处理完请求之前,调用不返回。
|
> When you execute something synchronously, you wait for it to finish before moving on to another task. When you execute something asynchronously, you can move on to another task before it finishes.
|
||||||
- **异步:** 异步就是发起一个调用后,立刻得到被调用者的回应表示已接收到请求,但是被调用者并没有返回结果,此时我们可以处理其他的请求,被调用者通常依靠事件,回调等机制来通知调用者其返回结果。
|
>
|
||||||
|
> 当你同步执行某项任务时,你需要等待其完成才能继续执行其他任务。当你异步执行某些操作时,你可以在完成另一个任务之前继续进行。
|
||||||
|
|
||||||
同步和异步的区别最大在于异步的话调用者不需要等待处理结果,被调用者会通过回调等机制来通知调用者其返回结果。
|
- **同步** :两个同步任务相互依赖,并且一个任务必须以依赖于另一任务的某种方式执行。 比如在`A->B`事件模型中,你需要先完成 A 才能执行B。 再换句话说,同步调用种被调用者未处理完请求之前,调用不返回,调用者会一直等待结果的返回。
|
||||||
|
- **异步**: 两个异步的任务完全独立的,一方的执行不需要等待另外一方的执行。再换句话说,异步调用种一调用就返回结果不需要等待结果返回,当结果返回的时候通过回调函数或者其他方式拿着结果再做相关事情,
|
||||||
|
|
||||||
**阻塞和非阻塞**
|
**阻塞和非阻塞**
|
||||||
|
|
||||||
- **阻塞:** 阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。
|
- **阻塞:** 阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。
|
||||||
- **非阻塞:** 非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。
|
- **非阻塞:** 非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。
|
||||||
|
|
||||||
举个生活中简单的例子,你妈妈让你烧水,小时候你比较笨啊,在哪里傻等着水开(**同步阻塞**)。等你稍微再长大一点,你知道每次烧水的空隙可以去干点其他事,然后只需要时不时来看看水开了没有(**同步非阻塞**)。后来,你们家用上了水开了会发出声音的壶,这样你就只需要听到响声后就知道水开了,在这期间你可以随便干自己的事情,你需要去倒水了(**异步非阻塞**)。
|
**如何区分 “同步/异步 ”和 “阻塞/非阻塞” 呢?**
|
||||||
|
|
||||||
|
同步/异步是从行为角度描述事物的,而阻塞和非阻塞描述的当前事物的状态(等待调用结果时的状态)。
|
||||||
|
|
||||||
## 1. BIO (Blocking I/O)
|
## 1. BIO (Blocking I/O)
|
||||||
|
|
||||||
@ -73,7 +76,7 @@ BIO通信(一请求一应答)模型图如下(图源网络,原出处不明)
|
|||||||
|
|
||||||
采用线程池和任务队列可以实现一种叫做伪异步的 I/O 通信框架,它的模型图如上图所示。当有新的客户端接入时,将客户端的 Socket 封装成一个Task(该任务实现java.lang.Runnable接口)投递到后端的线程池中进行处理,JDK 的线程池维护一个消息队列和 N 个活跃线程,对消息队列中的任务进行处理。由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。
|
采用线程池和任务队列可以实现一种叫做伪异步的 I/O 通信框架,它的模型图如上图所示。当有新的客户端接入时,将客户端的 Socket 封装成一个Task(该任务实现java.lang.Runnable接口)投递到后端的线程池中进行处理,JDK 的线程池维护一个消息队列和 N 个活跃线程,对消息队列中的任务进行处理。由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。
|
||||||
|
|
||||||
伪异步I/O通信框架采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。不过因为它的底层任然是同步阻塞的BIO模型,因此无法从根本上解决问题。
|
伪异步I/O通信框架采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。不过因为它的底层仍然是同步阻塞的BIO模型,因此无法从根本上解决问题。
|
||||||
|
|
||||||
### 1.3 代码示例
|
### 1.3 代码示例
|
||||||
|
|
||||||
@ -164,14 +167,12 @@ public class IOServer {
|
|||||||
|
|
||||||
在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
|
在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 2. NIO (New I/O)
|
## 2. NIO (New I/O)
|
||||||
|
|
||||||
### 2.1 NIO 简介
|
### 2.1 NIO 简介
|
||||||
|
|
||||||
NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。
|
NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了 NIO 框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。
|
||||||
|
|
||||||
NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 `Socket` 和 `ServerSocket` 相对应的 `SocketChannel` 和 `ServerSocketChannel` 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。
|
NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 `Socket` 和 `ServerSocket` 相对应的 `SocketChannel` 和 `ServerSocketChannel` 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。
|
||||||
|
|
||||||
### 2.2 NIO的特性/NIO与IO区别
|
### 2.2 NIO的特性/NIO与IO区别
|
||||||
@ -202,13 +203,13 @@ NIO 通过Channel(通道) 进行读写。
|
|||||||
|
|
||||||
通道是双向的,可读也可写,而流的读写是单向的。无论读写,通道只能和Buffer交互。因为 Buffer,通道可以异步地读写。
|
通道是双向的,可读也可写,而流的读写是单向的。无论读写,通道只能和Buffer交互。因为 Buffer,通道可以异步地读写。
|
||||||
|
|
||||||
#### 4)Selectors(选择器)
|
#### 4)Selector (选择器)
|
||||||
|
|
||||||
NIO有选择器,而IO没有。
|
NIO有选择器,而IO没有。
|
||||||
|
|
||||||
选择器用于使用单个线程处理多个通道。因此,它需要较少的线程来处理这些通道。线程之间的切换对于操作系统来说是昂贵的。 因此,为了提高系统效率选择器是有用的。
|
选择器用于使用单个线程处理多个通道。因此,它需要较少的线程来处理这些通道。线程之间的切换对于操作系统来说是昂贵的。 因此,为了提高系统效率选择器是有用的。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### 2.3 NIO 读数据和写数据方式
|
### 2.3 NIO 读数据和写数据方式
|
||||||
通常来说NIO中的所有IO都是从 Channel(通道) 开始的。
|
通常来说NIO中的所有IO都是从 Channel(通道) 开始的。
|
||||||
@ -273,8 +274,7 @@ public class NIOServer {
|
|||||||
|
|
||||||
if (key.isAcceptable()) {
|
if (key.isAcceptable()) {
|
||||||
try {
|
try {
|
||||||
// (1)
|
// (1) 每来一个新连接,不需要创建一个线程,而是直接注册到clientSelector
|
||||||
// 每来一个新连接,不需要创建一个线程,而是直接注册到clientSelector
|
|
||||||
SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
|
SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
|
||||||
clientChannel.configureBlocking(false);
|
clientChannel.configureBlocking(false);
|
||||||
clientChannel.register(clientSelector, SelectionKey.OP_READ);
|
clientChannel.register(clientSelector, SelectionKey.OP_READ);
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。
|
||||||
|
|
||||||
<!-- MarkdownTOC -->
|
<!-- MarkdownTOC -->
|
||||||
|
|
||||||
- [Servlet总结](#servlet总结)
|
- [Servlet总结](#servlet总结)
|
||||||
@ -26,7 +28,7 @@
|
|||||||
|
|
||||||
## Servlet总结
|
## Servlet总结
|
||||||
|
|
||||||
在Java Web程序中,**Servlet**主要负责接收用户请求**HttpServletRequest**,在**doGet()**,**doPost()**中做相应的处理,并将回应**HttpServletResponse**反馈给用户。Servlet可以设置初始化参数,供Servlet内部使用。一个Servlet类只会有一个实例,在它初始化时调用**init()方法**,销毁时调用**destroy()方法**。**Servlet需要在web.xml中配置**(MyEclipse中创建Servlet会自动配置),**一个Servlet可以设置多个URL访问**。**Servlet不是线程安全**,因此要谨慎使用类变量。
|
在Java Web程序中,**Servlet**主要负责接收用户请求 `HttpServletRequest`,在`doGet()`,`doPost()`中做相应的处理,并将回应`HttpServletResponse`反馈给用户。**Servlet** 可以设置初始化参数,供Servlet内部使用。一个Servlet类只会有一个实例,在它初始化时调用`init()`方法,销毁时调用`destroy()`方法**。**Servlet需要在web.xml中配置(MyEclipse中创建Servlet会自动配置),**一个Servlet可以设置多个URL访问**。**Servlet不是线程安全**,因此要谨慎使用类变量。
|
||||||
|
|
||||||
## 阐述Servlet和CGI的区别?
|
## 阐述Servlet和CGI的区别?
|
||||||
|
|
||||||
@ -55,11 +57,11 @@
|
|||||||
## Servlet接口中有哪些方法及Servlet生命周期探秘
|
## Servlet接口中有哪些方法及Servlet生命周期探秘
|
||||||
Servlet接口定义了5个方法,其中**前三个方法与Servlet生命周期相关**:
|
Servlet接口定义了5个方法,其中**前三个方法与Servlet生命周期相关**:
|
||||||
|
|
||||||
- **void init(ServletConfig config) throws ServletException**
|
- `void init(ServletConfig config) throws ServletException`
|
||||||
- **void service(ServletRequest req, ServletResponse resp) throws ServletException, java.io.IOException**
|
- `void service(ServletRequest req, ServletResponse resp) throws ServletException, java.io.IOException`
|
||||||
- **void destory()**
|
- `void destroy()`
|
||||||
- java.lang.String getServletInfo()
|
- `java.lang.String getServletInfo()`
|
||||||
- ServletConfig getServletConfig()
|
- `ServletConfig getServletConfig()`
|
||||||
|
|
||||||
**生命周期:** **Web容器加载Servlet并将其实例化后,Servlet生命周期开始**,容器运行其**init()方法**进行Servlet的初始化;请求到达时调用Servlet的**service()方法**,service()方法会根据需要调用与请求对应的**doGet或doPost**等方法;当服务器关闭或项目被卸载时服务器会将Servlet实例销毁,此时会调用Servlet的**destroy()方法**。**init方法和destroy方法只会执行一次,service方法客户端每次请求Servlet都会执行**。Servlet中有时会用到一些需要初始化与销毁的资源,因此可以把初始化资源的代码放入init方法中,销毁资源的代码放入destroy方法中,这样就不需要每次处理客户端的请求都要初始化与销毁资源。
|
**生命周期:** **Web容器加载Servlet并将其实例化后,Servlet生命周期开始**,容器运行其**init()方法**进行Servlet的初始化;请求到达时调用Servlet的**service()方法**,service()方法会根据需要调用与请求对应的**doGet或doPost**等方法;当服务器关闭或项目被卸载时服务器会将Servlet实例销毁,此时会调用Servlet的**destroy()方法**。**init方法和destroy方法只会执行一次,service方法客户端每次请求Servlet都会执行**。Servlet中有时会用到一些需要初始化与销毁的资源,因此可以把初始化资源的代码放入init方法中,销毁资源的代码放入destroy方法中,这样就不需要每次处理客户端的请求都要初始化与销毁资源。
|
||||||
|
|
||||||
@ -67,21 +69,11 @@ Servlet接口定义了5个方法,其中**前三个方法与Servlet生命周期
|
|||||||
|
|
||||||
## get和post请求的区别
|
## get和post请求的区别
|
||||||
|
|
||||||
> 网上也有文章说:get和post请求实际上是没有区别,大家可以自行查询相关文章(参考文章:[https://www.cnblogs.com/logsharing/p/8448446.html](https://www.cnblogs.com/logsharing/p/8448446.html),知乎对应的问题链接:[get和post区别?](https://www.zhihu.com/question/28586791))!我下面给出的只是一种常见的答案。
|
get和post请求实际上是没有区别,大家可以自行查询相关文章(参考文章:[https://www.cnblogs.com/logsharing/p/8448446.html](https://www.cnblogs.com/logsharing/p/8448446.html),知乎对应的问题链接:[get和post区别?](https://www.zhihu.com/question/28586791))!
|
||||||
|
|
||||||
①get请求用来从服务器上获得资源,而post是用来向服务器提交数据;
|
可以把 get 和 post 当作两个不同的行为,两者并没有什么本质区别,底层都是 TCP 连接。 get请求用来从服务器上获得资源,而post是用来向服务器提交数据。比如你要获取人员列表可以用 get 请求,你需要创建一个人员可以用 post 。这也是 Restful API 最基本的一个要求。
|
||||||
|
|
||||||
②get将表单中数据按照name=value的形式,添加到action 所指向的URL 后面,并且两者使用"?"连接,而各个变量之间使用"&"连接;post是将表单中的数据放在HTTP协议的请求头或消息体中,传递到action所指向URL;
|
推荐阅读:
|
||||||
|
|
||||||
③get传输的数据要受到URL长度限制(最大长度是 2048 个字符);而post可以传输大量的数据,上传文件通常要使用post方式;
|
|
||||||
|
|
||||||
④使用get时参数会显示在地址栏上,如果这些数据不是敏感数据,那么可以使用get;对于敏感数据还是应用使用post;
|
|
||||||
|
|
||||||
⑤get使用MIME类型application/x-www-form-urlencoded的URL编码(也叫百分号编码)文本的格式传递参数,保证被传送的参数由遵循规范的文本组成,例如一个空格的编码是"%20"。
|
|
||||||
|
|
||||||
补充:GET方式提交表单的典型应用是搜索引擎。GET方式就是被设计为查询用的。
|
|
||||||
|
|
||||||
还有另外一种回答。推荐大家看一下:
|
|
||||||
|
|
||||||
- https://www.zhihu.com/question/28586791
|
- https://www.zhihu.com/question/28586791
|
||||||
- https://mp.weixin.qq.com/s?__biz=MzI3NzIzMzg3Mw==&mid=100000054&idx=1&sn=71f6c214f3833d9ca20b9f7dcd9d33e4#rd
|
- https://mp.weixin.qq.com/s?__biz=MzI3NzIzMzg3Mw==&mid=100000054&idx=1&sn=71f6c214f3833d9ca20b9f7dcd9d33e4#rd
|
||||||
@ -93,7 +85,7 @@ Form标签里的method的属性为get时调用doGet(),为post时调用doPost()
|
|||||||
|
|
||||||
**转发是服务器行为,重定向是客户端行为。**
|
**转发是服务器行为,重定向是客户端行为。**
|
||||||
|
|
||||||
**转发(Forword)**
|
**转发(Forward)**
|
||||||
通过RequestDispatcher对象的forward(HttpServletRequest request,HttpServletResponse response)方法实现的。RequestDispatcher可以通过HttpServletRequest 的getRequestDispatcher()方法获得。例如下面的代码就是跳转到login_success.jsp页面。
|
通过RequestDispatcher对象的forward(HttpServletRequest request,HttpServletResponse response)方法实现的。RequestDispatcher可以通过HttpServletRequest 的getRequestDispatcher()方法获得。例如下面的代码就是跳转到login_success.jsp页面。
|
||||||
```java
|
```java
|
||||||
request.getRequestDispatcher("login_success.jsp").forward(request, response);
|
request.getRequestDispatcher("login_success.jsp").forward(request, response);
|
||||||
@ -143,13 +135,11 @@ Response.setHeader("Refresh","5;URL=http://localhost:8080/servlet/example.htm");
|
|||||||
JSP是一种Servlet,但是与HttpServlet的工作方式不太一样。HttpServlet是先由源代码编译为class文件后部署到服务器下,为先编译后部署。而JSP则是先部署后编译。JSP会在客户端第一次请求JSP文件时被编译为HttpJspPage类(接口Servlet的一个子类)。该类会被服务器临时存放在服务器工作目录里面。下面通过实例给大家介绍。
|
JSP是一种Servlet,但是与HttpServlet的工作方式不太一样。HttpServlet是先由源代码编译为class文件后部署到服务器下,为先编译后部署。而JSP则是先部署后编译。JSP会在客户端第一次请求JSP文件时被编译为HttpJspPage类(接口Servlet的一个子类)。该类会被服务器临时存放在服务器工作目录里面。下面通过实例给大家介绍。
|
||||||
工程JspLoginDemo下有一个名为login.jsp的Jsp文件,把工程第一次部署到服务器上后访问这个Jsp文件,我们发现这个目录下多了下图这两个东东。
|
工程JspLoginDemo下有一个名为login.jsp的Jsp文件,把工程第一次部署到服务器上后访问这个Jsp文件,我们发现这个目录下多了下图这两个东东。
|
||||||
.class文件便是JSP对应的Servlet。编译完毕后再运行class文件来响应客户端请求。以后客户端访问login.jsp的时候,Tomcat将不再重新编译JSP文件,而是直接调用class文件来响应客户端请求。
|
.class文件便是JSP对应的Servlet。编译完毕后再运行class文件来响应客户端请求。以后客户端访问login.jsp的时候,Tomcat将不再重新编译JSP文件,而是直接调用class文件来响应客户端请求。
|
||||||

|

|
||||||
由于JSP只会在客户端第一次请求的时候被编译 ,因此第一次请求JSP时会感觉比较慢,之后就会感觉快很多。如果把服务器保存的class文件删除,服务器也会重新编译JSP。
|
由于JSP只会在客户端第一次请求的时候被编译 ,因此第一次请求JSP时会感觉比较慢,之后就会感觉快很多。如果把服务器保存的class文件删除,服务器也会重新编译JSP。
|
||||||
|
|
||||||
开发Web程序时经常需要修改JSP。Tomcat能够自动检测到JSP程序的改动。如果检测到JSP源代码发生了改动。Tomcat会在下次客户端请求JSP时重新编译JSP,而不需要重启Tomcat。这种自动检测功能是默认开启的,检测改动会消耗少量的时间,在部署Web应用的时候可以在web.xml中将它关掉。
|
开发Web程序时经常需要修改JSP。Tomcat能够自动检测到JSP程序的改动。如果检测到JSP源代码发生了改动。Tomcat会在下次客户端请求JSP时重新编译JSP,而不需要重启Tomcat。这种自动检测功能是默认开启的,检测改动会消耗少量的时间,在部署Web应用的时候可以在web.xml中将它关掉。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
参考:《javaweb整合开发王者归来》P97
|
参考:《javaweb整合开发王者归来》P97
|
||||||
|
|
||||||
## JSP有哪些内置对象、作用分别是什么
|
## JSP有哪些内置对象、作用分别是什么
|
||||||
@ -195,31 +185,31 @@ JSP有9个内置对象:
|
|||||||
## request.getAttribute()和 request.getParameter()有何区别
|
## request.getAttribute()和 request.getParameter()有何区别
|
||||||
**从获取方向来看:**
|
**从获取方向来看:**
|
||||||
|
|
||||||
getParameter()是获取 POST/GET 传递的参数值;
|
`getParameter()`是获取 POST/GET 传递的参数值;
|
||||||
|
|
||||||
getAttribute()是获取对象容器中的数据值;
|
`getAttribute()`是获取对象容器中的数据值;
|
||||||
|
|
||||||
**从用途来看:**
|
**从用途来看:**
|
||||||
|
|
||||||
getParameter用于客户端重定向时,即点击了链接或提交按扭时传值用,即用于在用表单或url重定向传值时接收数据用。
|
`getParameter()`用于客户端重定向时,即点击了链接或提交按扭时传值用,即用于在用表单或url重定向传值时接收数据用。
|
||||||
|
|
||||||
getAttribute用于服务器端重定向时,即在 sevlet 中使用了 forward 函数,或 struts 中使用了
|
`getAttribute()` 用于服务器端重定向时,即在 sevlet 中使用了 forward 函数,或 struts 中使用了
|
||||||
mapping.findForward。 getAttribute 只能收到程序用 setAttribute 传过来的值。
|
mapping.findForward。 getAttribute 只能收到程序用 setAttribute 传过来的值。
|
||||||
|
|
||||||
另外,可以用 setAttribute,getAttribute 发送接收对象.而 getParameter 显然只能传字符串。
|
另外,可以用 `setAttribute()`,`getAttribute()` 发送接收对象.而 `getParameter()` 显然只能传字符串。
|
||||||
setAttribute 是应用服务器把这个对象放在该页面所对应的一块内存中去,当你的页面服务器重定向到另一个页面时,应用服务器会把这块内存拷贝另一个页面所对应的内存中。这样getAttribute就能取得你所设下的值,当然这种方法可以传对象。session也一样,只是对象在内存中的生命周期不一样而已。getParameter只是应用服务器在分析你送上来的 request页面的文本时,取得你设在表单或 url 重定向时的值。
|
`setAttribute()` 是应用服务器把这个对象放在该页面所对应的一块内存中去,当你的页面服务器重定向到另一个页面时,应用服务器会把这块内存拷贝另一个页面所对应的内存中。这样`getAttribute()`就能取得你所设下的值,当然这种方法可以传对象。session也一样,只是对象在内存中的生命周期不一样而已。`getParameter()`只是应用服务器在分析你送上来的 request页面的文本时,取得你设在表单或 url 重定向时的值。
|
||||||
|
|
||||||
**总结:**
|
**总结:**
|
||||||
|
|
||||||
getParameter 返回的是String,用于读取提交的表单中的值;(获取之后会根据实际需要转换为自己需要的相应类型,比如整型,日期类型啊等等)
|
`getParameter()`返回的是String,用于读取提交的表单中的值;(获取之后会根据实际需要转换为自己需要的相应类型,比如整型,日期类型啊等等)
|
||||||
|
|
||||||
getAttribute 返回的是Object,需进行转换,可用setAttribute 设置成任意对象,使用很灵活,可随时用
|
`getAttribute()`返回的是Object,需进行转换,可用`setAttribute()`设置成任意对象,使用很灵活,可随时用
|
||||||
|
|
||||||
## include指令include的行为的区别
|
## include指令include的行为的区别
|
||||||
**include指令:** JSP可以通过include指令来包含其他文件。被包含的文件可以是JSP文件、HTML文件或文本文件。包含的文件就好像是该JSP文件的一部分,会被同时编译执行。 语法格式如下:
|
**include指令:** JSP可以通过include指令来包含其他文件。被包含的文件可以是JSP文件、HTML文件或文本文件。包含的文件就好像是该JSP文件的一部分,会被同时编译执行。 语法格式如下:
|
||||||
<%@ include file="文件相对 url 地址" %>
|
<%@ include file="文件相对 url 地址" %>
|
||||||
|
|
||||||
i**nclude动作:** <jsp:include>动作元素用来包含静态和动态的文件。该动作把指定文件插入正在生成的页面。语法格式如下:
|
i**nclude动作:** `<jsp:include>`动作元素用来包含静态和动态的文件。该动作把指定文件插入正在生成的页面。语法格式如下:
|
||||||
<jsp:include page="相对 URL 地址" flush="true" />
|
<jsp:include page="相对 URL 地址" flush="true" />
|
||||||
|
|
||||||
## JSP九大内置对象,七大动作,三大指令
|
## JSP九大内置对象,七大动作,三大指令
|
||||||
@ -232,11 +222,9 @@ JSP中的四种作用域包括page、request、session和application,具体来
|
|||||||
- **session**代表与某个用户与服务器建立的一次会话相关的对象和属性。跟某个用户相关的数据应该放在用户自己的session中。
|
- **session**代表与某个用户与服务器建立的一次会话相关的对象和属性。跟某个用户相关的数据应该放在用户自己的session中。
|
||||||
- **application**代表与整个Web应用程序相关的对象和属性,它实质上是跨越整个Web应用程序,包括多个页面、请求和会话的一个全局作用域。
|
- **application**代表与整个Web应用程序相关的对象和属性,它实质上是跨越整个Web应用程序,包括多个页面、请求和会话的一个全局作用域。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 如何实现JSP或Servlet的单线程模式
|
## 如何实现JSP或Servlet的单线程模式
|
||||||
对于JSP页面,可以通过page指令进行设置。
|
对于JSP页面,可以通过page指令进行设置。
|
||||||
<%@page isThreadSafe=”false”%>
|
`<%@page isThreadSafe="false"%>`
|
||||||
|
|
||||||
对于Servlet,可以让自定义的Servlet实现SingleThreadModel标识接口。
|
对于Servlet,可以让自定义的Servlet实现SingleThreadModel标识接口。
|
||||||
|
|
||||||
@ -294,12 +282,20 @@ if(cookies !=null){
|
|||||||
在所有会话跟踪技术中,HttpSession对象是最强大也是功能最多的。当一个用户第一次访问某个网站时会自动创建 HttpSession,每个用户可以访问他自己的HttpSession。可以通过HttpServletRequest对象的getSession方 法获得HttpSession,通过HttpSession的setAttribute方法可以将一个值放在HttpSession中,通过调用 HttpSession对象的getAttribute方法,同时传入属性名就可以获取保存在HttpSession中的对象。与上面三种方式不同的 是,HttpSession放在服务器的内存中,因此不要将过大的对象放在里面,即使目前的Servlet容器可以在内存将满时将HttpSession 中的对象移到其他存储设备中,但是这样势必影响性能。添加到HttpSession中的值可以是任意Java对象,这个对象最好实现了 Serializable接口,这样Servlet容器在必要的时候可以将其序列化到文件中,否则在序列化时就会出现异常。
|
在所有会话跟踪技术中,HttpSession对象是最强大也是功能最多的。当一个用户第一次访问某个网站时会自动创建 HttpSession,每个用户可以访问他自己的HttpSession。可以通过HttpServletRequest对象的getSession方 法获得HttpSession,通过HttpSession的setAttribute方法可以将一个值放在HttpSession中,通过调用 HttpSession对象的getAttribute方法,同时传入属性名就可以获取保存在HttpSession中的对象。与上面三种方式不同的 是,HttpSession放在服务器的内存中,因此不要将过大的对象放在里面,即使目前的Servlet容器可以在内存将满时将HttpSession 中的对象移到其他存储设备中,但是这样势必影响性能。添加到HttpSession中的值可以是任意Java对象,这个对象最好实现了 Serializable接口,这样Servlet容器在必要的时候可以将其序列化到文件中,否则在序列化时就会出现异常。
|
||||||
## Cookie和Session的的区别
|
## Cookie和Session的的区别
|
||||||
|
|
||||||
1. 由于HTTP协议是无状态的协议,所以服务端需要记录用户的状态时,就需要用某种机制来识具体的用户,这个机制就是Session.典型的场景比如购物车,当你点击下单按钮时,由于HTTP协议无状态,所以并不知道是哪个用户操作的,所以服务端要为特定的用户创建了特定的Session,用用于标识这个用户,并且跟踪用户,这样才知道购物车里面有几本书。这个Session是保存在服务端的,有一个唯一标识。在服务端保存Session的方法很多,内存、数据库、文件都有。集群的时候也要考虑Session的转移,在大型的网站,一般会有专门的Session服务器集群,用来保存用户会话,这个时候 Session 信息都是放在内存的,使用一些缓存服务比如Memcached之类的来放 Session。
|
Cookie 和 Session都是用来跟踪浏览器用户身份的会话方式,但是两者的应用场景不太一样。
|
||||||
2. 思考一下服务端如何识别特定的客户?这个时候Cookie就登场了。每次HTTP请求的时候,客户端都会发送相应的Cookie信息到服务端。实际上大多数的应用都是用 Cookie 来实现Session跟踪的,第一次创建Session的时候,服务端会在HTTP协议中告诉客户端,需要在 Cookie 里面记录一个Session ID,以后每次请求把这个会话ID发送到服务器,我就知道你是谁了。有人问,如果客户端的浏览器禁用了 Cookie 怎么办?一般这种情况下,会使用一种叫做URL重写的技术来进行会话跟踪,即每次HTTP交互,URL后面都会被附加上一个诸如 sid=xxxxx 这样的参数,服务端据此来识别用户。
|
|
||||||
3. Cookie其实还可以用在一些方便用户的场景下,设想你某次登陆过一个网站,下次登录的时候不想再次输入账号了,怎么办?这个信息可以写到Cookie里面,访问网站的时候,网站页面的脚本可以读取这个信息,就自动帮你把用户名给填了,能够方便一下用户。这也是Cookie名称的由来,给用户的一点甜头。所以,总结一下:Session是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中;Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现Session的一种方式。
|
|
||||||
|
|
||||||
参考:
|
**Cookie 一般用来保存用户信息** 比如①我们在 Cookie 中保存已经登录过得用户信息,下次访问网站的时候页面可以自动帮你登录的一些基本信息给填了;②一般的网站都会有保持登录也就是说下次你再访问网站的时候就不需要重新登录了,这是因为用户登录的时候我们可以存放了一个 Token 在 Cookie 中,下次登录的时候只需要根据 Token 值来查找用户即可(为了安全考虑,重新登录一般要将 Token 重写);③登录一次网站后访问网站其他页面不需要重新登录。**Session 的主要作用就是通过服务端记录用户的状态。** 典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,因为 HTTP 协议是无状态的。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了。
|
||||||
|
|
||||||
https://www.zhihu.com/question/19786827/answer/28752144
|
Cookie 数据保存在客户端(浏览器端),Session 数据保存在服务器端。
|
||||||
|
|
||||||
《javaweb整合开发王者归来》P158 Cookie和Session的比较
|
Cookie 存储在客户端中,而Session存储在服务器上,相对来说 Session 安全性更高。如果使用 Cookie 的一些敏感信息不要写入 Cookie 中,最好能将 Cookie 信息加密然后使用到的时候再去服务器端解密。
|
||||||
|
|
||||||
|
## 公众号
|
||||||
|
|
||||||
|
如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。
|
||||||
|
|
||||||
|
**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取!
|
||||||
|
|
||||||
|
**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。
|
||||||
|
|
||||||
|

|
||||||
|
375
docs/java/JAD反编译tricks.md
Normal file
375
docs/java/JAD反编译tricks.md
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
[jad](https://varaneckas.com/jad/)反编译工具,已经不再更新,且只支持JDK1.4,但并不影响其强大的功能。
|
||||||
|
|
||||||
|
基本用法:`jad xxx.class`,会生成直接可读的xxx.jad文件。
|
||||||
|
|
||||||
|
## 自动拆装箱
|
||||||
|
|
||||||
|
对于基本类型和包装类型之间的转换,通过xxxValue()和valueOf()两个方法完成自动拆装箱,使用jad进行反编译可以看到该过程:
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class Demo {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
int x = new Integer(10); // 自动拆箱
|
||||||
|
Integer y = x; // 自动装箱
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
反编译后结果:
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class Demo
|
||||||
|
{
|
||||||
|
public Demo(){}
|
||||||
|
|
||||||
|
public static void main(String args[])
|
||||||
|
{
|
||||||
|
int i = (new Integer(10)).intValue(); // intValue()拆箱
|
||||||
|
Integer integer = Integer.valueOf(i); // valueOf()装箱
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## foreach语法糖
|
||||||
|
|
||||||
|
在遍历迭代时可以foreach语法糖,对于数组类型直接转换成for循环:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 原始代码
|
||||||
|
int[] arr = {1, 2, 3, 4, 5};
|
||||||
|
for(int item: arr) {
|
||||||
|
System.out.println(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 反编译后代码
|
||||||
|
int ai[] = {
|
||||||
|
1, 2, 3, 4, 5
|
||||||
|
};
|
||||||
|
int ai1[] = ai;
|
||||||
|
int i = ai1.length;
|
||||||
|
// 转换成for循环
|
||||||
|
for(int j = 0; j < i; j++)
|
||||||
|
{
|
||||||
|
int k = ai1[j];
|
||||||
|
System.out.println(k);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
对于容器类的遍历会使用iterator进行迭代:
|
||||||
|
|
||||||
|
```java
|
||||||
|
import java.io.PrintStream;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class Demo
|
||||||
|
{
|
||||||
|
public Demo() {}
|
||||||
|
public static void main(String args[])
|
||||||
|
{
|
||||||
|
ArrayList arraylist = new ArrayList();
|
||||||
|
arraylist.add(Integer.valueOf(1));
|
||||||
|
arraylist.add(Integer.valueOf(2));
|
||||||
|
arraylist.add(Integer.valueOf(3));
|
||||||
|
Integer integer;
|
||||||
|
// 使用的for循环+Iterator,类似于链表迭代:
|
||||||
|
// for (ListNode cur = head; cur != null; System.out.println(cur.val)){
|
||||||
|
// cur = cur.next;
|
||||||
|
// }
|
||||||
|
for(Iterator iterator = arraylist.iterator(); iterator.hasNext(); System.out.println(integer))
|
||||||
|
integer = (Integer)iterator.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Arrays.asList(T...)
|
||||||
|
|
||||||
|
熟悉Arrays.asList(T...)用法的小伙伴都应该知道,asList()方法传入的参数不能是基本类型的数组,必须包装成包装类型再使用,否则对应生成的列表的大小永远是1:
|
||||||
|
|
||||||
|
```java
|
||||||
|
import java.util.*;
|
||||||
|
public class Demo {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
int[] arr1 = {1, 2, 3};
|
||||||
|
Integer[] arr2 = {1, 2, 3};
|
||||||
|
List lists1 = Arrays.asList(arr1);
|
||||||
|
List lists2 = Arrays.asList(arr2);
|
||||||
|
System.out.println(lists1.size()); // 1
|
||||||
|
System.out.println(lists2.size()); // 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
从反编译结果来解释,为什么传入基本类型的数组后,返回的List大小是1:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 反编译后文件
|
||||||
|
import java.io.PrintStream;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class Demo
|
||||||
|
{
|
||||||
|
public Demo() {}
|
||||||
|
|
||||||
|
public static void main(String args[])
|
||||||
|
{
|
||||||
|
int ai[] = {
|
||||||
|
1, 2, 3
|
||||||
|
};
|
||||||
|
// 使用包装类型,全部元素由int包装为Integer
|
||||||
|
Integer ainteger[] = {
|
||||||
|
Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3)
|
||||||
|
};
|
||||||
|
|
||||||
|
// 注意这里被反编译成二维数组,而且是一个1行三列的二维数组
|
||||||
|
// list.size()当然返回1
|
||||||
|
List list = Arrays.asList(new int[][] { ai });
|
||||||
|
List list1 = Arrays.asList(ainteger);
|
||||||
|
System.out.println(list.size());
|
||||||
|
System.out.println(list1.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
从上面结果可以看到,传入基本类型的数组后,会被转换成一个二维数组,而且是**new int\[1]\[arr.length]**这样的数组,调用list.size()当然返回1。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 注解
|
||||||
|
|
||||||
|
Java中的类、接口、枚举、注解都可以看做是类类型。使用jad来看一下@interface被转换成什么:
|
||||||
|
|
||||||
|
```java
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface Foo{
|
||||||
|
String[] value();
|
||||||
|
boolean bar();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
查看反编译代码可以看出:
|
||||||
|
|
||||||
|
- 自定义的注解类Foo被转换成接口Foo,并且继承Annotation接口
|
||||||
|
- 原来自定义接口中的value()和bar()被转换成抽象方法
|
||||||
|
|
||||||
|
```java
|
||||||
|
import java.lang.annotation.Annotation;
|
||||||
|
|
||||||
|
public interface Foo
|
||||||
|
extends Annotation
|
||||||
|
{
|
||||||
|
public abstract String[] value();
|
||||||
|
|
||||||
|
public abstract boolean bar();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
注解通常和反射配合使用,而且既然自定义的注解最终被转换成接口,注解中的属性被转换成接口中的抽象方法,那么通过反射之后拿到接口实例,在通过接口实例自然能够调用对应的抽象方法:
|
||||||
|
```java
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
@Foo(value={"sherman", "decompiler"}, bar=true)
|
||||||
|
public class Demo{
|
||||||
|
public static void main(String[] args) {
|
||||||
|
Foo foo = Demo.class.getAnnotation(Foo.class);
|
||||||
|
System.out.println(Arrays.toString(foo.value())); // [sherman, decompiler]
|
||||||
|
System.out.println(foo.bar()); // true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 枚举
|
||||||
|
|
||||||
|
通过jad反编译可以很好地理解枚举类。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 空枚举
|
||||||
|
|
||||||
|
先定义一个空的枚举类:
|
||||||
|
|
||||||
|
```java
|
||||||
|
public enum DummyEnum {
|
||||||
|
}
|
||||||
|
```
|
||||||
|
使用jad反编译查看结果:
|
||||||
|
|
||||||
|
- 自定义枚举类被转换成final类,并且继承Enum
|
||||||
|
- 提供了两个参数(name,odinal)的私有构造器,并且调用了父类的构造器。注意即使没有提供任何参数,也会有该该构造器,其中name就是枚举实例的名称,odinal是枚举实例的索引号
|
||||||
|
- 初始化了一个private static final自定义类型的空数组 **$VALUES**
|
||||||
|
- 提供了两个public static方法:
|
||||||
|
- values()方法通过clone()方法返回内部$VALUES的浅拷贝。这个方法结合私有构造器可以完美实现单例模式,想一想values()方法是不是和单例模式中getInstance()方法功能类似
|
||||||
|
- valueOf(String s):调用父类Enum的valueOf方法并强转返回
|
||||||
|
|
||||||
|
```java
|
||||||
|
public final class DummyEnum extends Enum
|
||||||
|
{
|
||||||
|
// 功能和单例模式的getInstance()方法相同
|
||||||
|
public static DummyEnum[] values()
|
||||||
|
{
|
||||||
|
return (DummyEnum[])$VALUES.clone();
|
||||||
|
}
|
||||||
|
// 调用父类的valueOf方法,并墙砖返回
|
||||||
|
public static DummyEnum valueOf(String s)
|
||||||
|
{
|
||||||
|
return (DummyEnum)Enum.valueOf(DummyEnum, s);
|
||||||
|
}
|
||||||
|
// 默认提供一个私有的私有两个参数的构造器,并调用父类Enum的构造器
|
||||||
|
private DummyEnum(String s, int i)
|
||||||
|
{
|
||||||
|
super(s, i);
|
||||||
|
}
|
||||||
|
// 初始化一个private static final的本类空数组
|
||||||
|
private static final DummyEnum $VALUES[] = new DummyEnum[0];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
### 包含抽象方法的枚举
|
||||||
|
|
||||||
|
枚举类中也可以包含抽象方法,但是必须定义枚举实例并且立即重写抽象方法,就像下面这样:
|
||||||
|
|
||||||
|
```java
|
||||||
|
public enum DummyEnum {
|
||||||
|
DUMMY1 {
|
||||||
|
public void dummyMethod() {
|
||||||
|
System.out.println("[1]: implements abstract method in enum class");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
DUMMY2 {
|
||||||
|
public void dummyMethod() {
|
||||||
|
System.out.println("[2]: implements abstract method in enum class");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
abstract void dummyMethod();
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
再来反编译看看有哪些变化:
|
||||||
|
|
||||||
|
- 原来final class变成了abstract class:这很好理解,有抽象方法的类自然是抽象类
|
||||||
|
- 多了两个public static final的成员DUMMY1、DUMMY2,这两个实例的初始化过程被放到了static代码块中,并且实例过程中直接重写了抽象方法,类似于匿名内部类的形式。
|
||||||
|
- 数组**$VALUES[]**初始化时放入枚举实例
|
||||||
|
|
||||||
|
还有其它变化么?
|
||||||
|
|
||||||
|
在反编译后的DummyEnum类中,是存在抽象方法的,而枚举实例在静态代码块中初始化过程中重写了抽象方法。在Java中,抽象方法和抽象方法重写同时放在一个类中,只能通过内部类形式完成。因此上面第二点应该说成就是以内部类形式初始化。
|
||||||
|
|
||||||
|
可以看一下DummyEnum.class存放的位置,应该多了两个文件:
|
||||||
|
|
||||||
|
- DummyEnum$1.class
|
||||||
|
- DummyEnum$2.class
|
||||||
|
|
||||||
|
Java中.class文件出现$符号表示有内部类存在,就像OutClass$InnerClass,这两个文件出现也应证了上面的匿名内部类初始化的说法。
|
||||||
|
|
||||||
|
```java
|
||||||
|
import java.io.PrintStream;
|
||||||
|
|
||||||
|
public abstract class DummyEnum extends Enum
|
||||||
|
{
|
||||||
|
public static DummyEnum[] values()
|
||||||
|
{
|
||||||
|
return (DummyEnum[])$VALUES.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DummyEnum valueOf(String s)
|
||||||
|
{
|
||||||
|
return (DummyEnum)Enum.valueOf(DummyEnum, s);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DummyEnum(String s, int i)
|
||||||
|
{
|
||||||
|
super(s, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 抽象方法
|
||||||
|
abstract void dummyMethod();
|
||||||
|
|
||||||
|
// 两个pubic static final实例
|
||||||
|
public static final DummyEnum DUMMY1;
|
||||||
|
public static final DummyEnum DUMMY2;
|
||||||
|
private static final DummyEnum $VALUES[];
|
||||||
|
|
||||||
|
// static代码块进行初始化
|
||||||
|
static
|
||||||
|
{
|
||||||
|
DUMMY1 = new DummyEnum("DUMMY1", 0) {
|
||||||
|
public void dummyMethod()
|
||||||
|
{
|
||||||
|
System.out.println("[1]: implements abstract method in enum class");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
;
|
||||||
|
DUMMY2 = new DummyEnum("DUMMY2", 1) {
|
||||||
|
public void dummyMethod()
|
||||||
|
{
|
||||||
|
System.out.println("[2]: implements abstract method in enum class");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
;
|
||||||
|
// 对本类数组进行初始化
|
||||||
|
$VALUES = (new DummyEnum[] {
|
||||||
|
DUMMY1, DUMMY2
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 正常的枚举类
|
||||||
|
|
||||||
|
实际开发中,枚举类通常的形式是有两个参数(int code,Sring msg)的构造器,可以作为状态码进行返回。Enum类实际上也是提供了包含两个参数且是protected的构造器,这里为了避免歧义,将枚举类的构造器设置为三个,使用jad反编译:
|
||||||
|
|
||||||
|
最大的变化是:现在的private构造器从2个参数变成5个,而且在内部仍然将前两个参数通过super传递给父类,剩余的三个参数才是真正自己提供的参数。可以想象,如果自定义的枚举类只提供了一个参数,最终生成底层代码中private构造器应该有三个参数,前两个依然通过super传递给父类。
|
||||||
|
|
||||||
|
```java
|
||||||
|
public final class CustomEnum extends Enum
|
||||||
|
{
|
||||||
|
public static CustomEnum[] values()
|
||||||
|
{
|
||||||
|
return (CustomEnum[])$VALUES.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CustomEnum valueOf(String s)
|
||||||
|
{
|
||||||
|
return (CustomEnum)Enum.valueOf(CustomEnum, s);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CustomEnum(String s, int i, int j, String s1, Object obj)
|
||||||
|
{
|
||||||
|
super(s, i);
|
||||||
|
code = j;
|
||||||
|
msg = s1;
|
||||||
|
data = obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final CustomEnum FIRST;
|
||||||
|
public static final CustomEnum SECOND;
|
||||||
|
public static final CustomEnum THIRD;
|
||||||
|
private int code;
|
||||||
|
private String msg;
|
||||||
|
private Object data;
|
||||||
|
private static final CustomEnum $VALUES[];
|
||||||
|
|
||||||
|
static
|
||||||
|
{
|
||||||
|
FIRST = new CustomEnum("FIRST", 0, 10010, "first", Long.valueOf(100L));
|
||||||
|
SECOND = new CustomEnum("SECOND", 1, 10020, "second", "Foo");
|
||||||
|
THIRD = new CustomEnum("THIRD", 2, 10030, "third", new Object());
|
||||||
|
$VALUES = (new CustomEnum[] {
|
||||||
|
FIRST, SECOND, THIRD
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
@ -27,12 +27,12 @@
|
|||||||
|
|
||||||
**(1) 按操作方式分类结构图:**
|
**(1) 按操作方式分类结构图:**
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
**(2)按操作对象分类结构图**
|
**(2)按操作对象分类结构图**
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### [二 java IO体系的学习总结](https://blog.csdn.net/nightcurtis/article/details/51324105)
|
### [二 java IO体系的学习总结](https://blog.csdn.net/nightcurtis/article/details/51324105)
|
||||||
1. **IO流的分类:**
|
1. **IO流的分类:**
|
||||||
@ -92,7 +92,7 @@
|
|||||||
- 写入数据到缓冲区(Writing Data to a Buffer)
|
- 写入数据到缓冲区(Writing Data to a Buffer)
|
||||||
|
|
||||||
**写数据到Buffer有两种方法:**
|
**写数据到Buffer有两种方法:**
|
||||||
|
|
||||||
1.从Channel中写数据到Buffer
|
1.从Channel中写数据到Buffer
|
||||||
```java
|
```java
|
||||||
int bytesRead = inChannel.read(buf); //read into buffer.
|
int bytesRead = inChannel.read(buf); //read into buffer.
|
||||||
@ -103,7 +103,7 @@
|
|||||||
```
|
```
|
||||||
|
|
||||||
4. **Buffer常用方法测试**
|
4. **Buffer常用方法测试**
|
||||||
|
|
||||||
说实话,NIO编程真的难,通过后面这个测试例子,你可能才能勉强理解前面说的Buffer方法的作用。
|
说实话,NIO编程真的难,通过后面这个测试例子,你可能才能勉强理解前面说的Buffer方法的作用。
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,73 +1,76 @@
|
|||||||
|
点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java 面试突击》以及 Java 工程师必备学习资源。
|
||||||
|
<!-- TOC -->
|
||||||
<!-- MarkdownTOC -->
|
|
||||||
|
|
||||||
- [1. 面向对象和面向过程的区别](#1-面向对象和面向过程的区别)
|
- [1. 面向对象和面向过程的区别](#1-面向对象和面向过程的区别)
|
||||||
- [面向过程](#面向过程)
|
- [2. Java 语言有哪些特点?](#2-java-语言有哪些特点)
|
||||||
- [面向对象](#面向对象)
|
|
||||||
- [2. Java 语言有哪些特点](#2-java-语言有哪些特点)
|
|
||||||
- [3. 关于 JVM JDK 和 JRE 最详细通俗的解答](#3-关于-jvm-jdk-和-jre-最详细通俗的解答)
|
- [3. 关于 JVM JDK 和 JRE 最详细通俗的解答](#3-关于-jvm-jdk-和-jre-最详细通俗的解答)
|
||||||
- [JVM](#jvm)
|
- [JVM](#jvm)
|
||||||
- [JDK 和 JRE](#jdk-和-jre)
|
- [JDK 和 JRE](#jdk-和-jre)
|
||||||
- [4. Oracle JDK 和 OpenJDK 的对比](#4-oracle-jdk-和-openjdk-的对比)
|
- [4. Oracle JDK 和 OpenJDK 的对比](#4-oracle-jdk-和-openjdk-的对比)
|
||||||
- [5. Java和C++的区别](#5-java和c的区别)
|
- [5. Java 和 C++的区别?](#5-java-和-c的区别)
|
||||||
- [6. 什么是 Java 程序的主类 应用程序和小程序的主类有何不同](#6-什么是-java-程序的主类-应用程序和小程序的主类有何不同)
|
- [6. 什么是 Java 程序的主类 应用程序和小程序的主类有何不同?](#6-什么是-java-程序的主类-应用程序和小程序的主类有何不同)
|
||||||
- [7. Java 应用程序与小程序之间有那些差别](#7-java-应用程序与小程序之间有那些差别)
|
- [7. Java 应用程序与小程序之间有哪些差别?](#7-java-应用程序与小程序之间有哪些差别)
|
||||||
- [8. 字符型常量和字符串常量的区别](#8-字符型常量和字符串常量的区别)
|
- [8. 字符型常量和字符串常量的区别?](#8-字符型常量和字符串常量的区别)
|
||||||
- [9. 构造器 Constructor 是否可被 override](#9-构造器-constructor-是否可被-override)
|
- [9. 构造器 Constructor 是否可被 override?](#9-构造器-constructor-是否可被-override)
|
||||||
- [10. 重载和重写的区别](#10-重载和重写的区别)
|
- [10. 重载和重写的区别](#10-重载和重写的区别)
|
||||||
|
- [重载](#重载)
|
||||||
|
- [重写](#重写)
|
||||||
- [11. Java 面向对象编程三大特性: 封装 继承 多态](#11-java-面向对象编程三大特性-封装-继承-多态)
|
- [11. Java 面向对象编程三大特性: 封装 继承 多态](#11-java-面向对象编程三大特性-封装-继承-多态)
|
||||||
- [封装](#封装)
|
- [封装](#封装)
|
||||||
- [继承](#继承)
|
- [继承](#继承)
|
||||||
- [多态](#多态)
|
- [多态](#多态)
|
||||||
- [12. String StringBuffer 和 StringBuilder 的区别是什么 String 为什么是不可变的](#12-string-stringbuffer-和-stringbuilder-的区别是什么-string-为什么是不可变的)
|
- [12. String StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的?](#12-string-stringbuffer-和-stringbuilder-的区别是什么-string-为什么是不可变的)
|
||||||
- [13. 自动装箱与拆箱](#13-自动装箱与拆箱)
|
- [13. 自动装箱与拆箱](#13-自动装箱与拆箱)
|
||||||
- [14. 在一个静态方法内调用一个非静态成员为什么是非法的](#14-在一个静态方法内调用一个非静态成员为什么是非法的)
|
- [14. 在一个静态方法内调用一个非静态成员为什么是非法的?](#14-在一个静态方法内调用一个非静态成员为什么是非法的)
|
||||||
- [15. 在 Java 中定义一个不做事且没有参数的构造方法的作用](#15-在-java-中定义一个不做事且没有参数的构造方法的作用)
|
- [15. 在 Java 中定义一个不做事且没有参数的构造方法的作用](#15-在-java-中定义一个不做事且没有参数的构造方法的作用)
|
||||||
- [16. import java和javax有什么区别](#16-import-java和javax有什么区别)
|
- [16. import java 和 javax 有什么区别?](#16-import-java-和-javax-有什么区别)
|
||||||
- [17. 接口和抽象类的区别是什么](#17-接口和抽象类的区别是什么)
|
- [17. 接口和抽象类的区别是什么?](#17-接口和抽象类的区别是什么)
|
||||||
- [18. 成员变量与局部变量的区别有那些](#18-成员变量与局部变量的区别有那些)
|
- [18. 成员变量与局部变量的区别有哪些?](#18-成员变量与局部变量的区别有哪些)
|
||||||
- [19. 创建一个对象用什么运算符?对象实体与对象引用有何不同?](#19-创建一个对象用什么运算符对象实体与对象引用有何不同)
|
- [19. 创建一个对象用什么运算符?对象实体与对象引用有何不同?](#19-创建一个对象用什么运算符对象实体与对象引用有何不同)
|
||||||
- [20. 什么是方法的返回值?返回值在类的方法里的作用是什么?](#20-什么是方法的返回值返回值在类的方法里的作用是什么)
|
- [20. 什么是方法的返回值?返回值在类的方法里的作用是什么?](#20-什么是方法的返回值返回值在类的方法里的作用是什么)
|
||||||
- [21. 一个类的构造方法的作用是什么 若一个类没有声明构造方法,该程序能正确执行吗 ?为什么?](#21-一个类的构造方法的作用是什么-若一个类没有声明构造方法该程序能正确执行吗-为什么)
|
- [21. 一个类的构造方法的作用是什么? 若一个类没有声明构造方法,该程序能正确执行吗? 为什么?](#21-一个类的构造方法的作用是什么-若一个类没有声明构造方法该程序能正确执行吗-为什么)
|
||||||
- [22. 构造方法有哪些特性](#22-构造方法有哪些特性)
|
- [22. 构造方法有哪些特性?](#22-构造方法有哪些特性)
|
||||||
- [23. 静态方法和实例方法有何不同](#23-静态方法和实例方法有何不同)
|
- [23. 静态方法和实例方法有何不同](#23-静态方法和实例方法有何不同)
|
||||||
- [24. 对象的相等与指向他们的引用相等,两者有什么不同?](#24-对象的相等与指向他们的引用相等两者有什么不同)
|
- [24. 对象的相等与指向他们的引用相等,两者有什么不同?](#24-对象的相等与指向他们的引用相等两者有什么不同)
|
||||||
- [25. 在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是?](#25-在调用子类构造方法之前会先调用父类没有参数的构造方法其目的是)
|
- [25. 在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是?](#25-在调用子类构造方法之前会先调用父类没有参数的构造方法其目的是)
|
||||||
- [26. == 与 equals\(重要\)](#26--与-equals重要)
|
- [26. == 与 equals(重要)](#26--与-equals重要)
|
||||||
- [27. hashCode 与 equals \(重要\)](#27-hashcode-与-equals-重要)
|
- [27. hashCode 与 equals (重要)](#27-hashcode-与-equals-重要)
|
||||||
- [hashCode()介绍](#hashcode()介绍)
|
- [hashCode()介绍](#hashcode介绍)
|
||||||
- [为什么要有 hashCode](#为什么要有-hashcode)
|
- [为什么要有 hashCode](#为什么要有-hashcode)
|
||||||
- [hashCode()与equals()的相关规定](#hashcode()与equals()的相关规定)
|
- [hashCode()与 equals()的相关规定](#hashcode与-equals的相关规定)
|
||||||
- [28. 为什么Java中只有值传递](#28-为什么java中只有值传递)
|
- [28. 为什么 Java 中只有值传递?](#28-为什么-java-中只有值传递)
|
||||||
- [29. 简述线程,程序、进程的基本概念。以及他们之间关系是什么](#29-简述线程程序进程的基本概念以及他们之间关系是什么)
|
- [29. 简述线程、程序、进程的基本概念。以及他们之间关系是什么?](#29-简述线程程序进程的基本概念以及他们之间关系是什么)
|
||||||
- [30. 线程有哪些基本状态?](#30-线程有哪些基本状态)
|
- [30. 线程有哪些基本状态?](#30-线程有哪些基本状态)
|
||||||
- [31 关于 final 关键字的一些总结](#31-关于-final-关键字的一些总结)
|
- [31 关于 final 关键字的一些总结](#31-关于-final-关键字的一些总结)
|
||||||
- [32 Java 中的异常处理](#32-java-中的异常处理)
|
- [32 Java 中的异常处理](#32-java-中的异常处理)
|
||||||
- [Java异常类层次结构图](#java异常类层次结构图)
|
- [Java 异常类层次结构图](#java-异常类层次结构图)
|
||||||
- [Throwable类常用方法](#throwable类常用方法)
|
- [Throwable 类常用方法](#throwable-类常用方法)
|
||||||
- [异常处理总结](#异常处理总结)
|
- [异常处理总结](#异常处理总结)
|
||||||
- [33 Java序列化中如果有些字段不想进行序列化 怎么办](#33-java序列化中如果有些字段不想进行序列化-怎么办)
|
- [33 Java 序列化中如果有些字段不想进行序列化,怎么办?](#33-java-序列化中如果有些字段不想进行序列化怎么办)
|
||||||
- [34 获取用键盘输入常用的的两种方法](#34-获取用键盘输入常用的的两种方法)
|
- [34 获取用键盘输入常用的两种方法](#34-获取用键盘输入常用的两种方法)
|
||||||
|
- [35 Java 中 IO 流](#35-java-中-io-流)
|
||||||
|
- [Java 中 IO 流分为几种?](#java-中-io-流分为几种)
|
||||||
|
- [既然有了字节流,为什么还要有字符流?](#既然有了字节流为什么还要有字符流)
|
||||||
|
- [BIO,NIO,AIO 有什么区别?](#bionioaio-有什么区别)
|
||||||
|
- [36. 常见关键字总结:static,final,this,super](#36-常见关键字总结staticfinalthissuper)
|
||||||
|
- [37. Collections 工具类和 Arrays 工具类常见方法总结](#37-collections-工具类和-arrays-工具类常见方法总结)
|
||||||
|
- [38. 深拷贝 vs 浅拷贝](#38-深拷贝-vs-浅拷贝)
|
||||||
- [参考](#参考)
|
- [参考](#参考)
|
||||||
|
- [公众号](#公众号)
|
||||||
|
|
||||||
<!-- /MarkdownTOC -->
|
<!-- /TOC -->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 1. 面向对象和面向过程的区别
|
## 1. 面向对象和面向过程的区别
|
||||||
|
|
||||||
### 面向过程
|
- **面向过程** :**面向过程性能比面向对象高。** 因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix 等一般采用面向过程开发。但是,**面向过程没有面向对象易维护、易复用、易扩展。**
|
||||||
|
- **面向对象** :**面向对象易维护、易复用、易扩展。** 因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是,**面向对象性能比面向过程低**。
|
||||||
|
|
||||||
**优点:** 性能比面向对象高。因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发
|
参见 issue : [面向过程 :面向过程性能比面向对象高??](https://github.com/Snailclimb/JavaGuide/issues/431)
|
||||||
|
|
||||||
**缺点:** 没有面向对象易维护、易复用、易扩展
|
> 这个并不是根本原因,面向过程也需要分配内存,计算内存偏移量,Java 性能差的主要原因并不是因为它是面向对象语言,而是 Java 是半编译语言,最终的执行代码并不是可以直接被 CPU 执行的二进制机械码。
|
||||||
|
>
|
||||||
### 面向对象
|
> 而面向过程语言大多都是直接编译成机械码在电脑上执行,并且其它一些面向过程的脚本语言性能也并不一定比 Java 好。
|
||||||
|
|
||||||
**优点:** 易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护
|
|
||||||
|
|
||||||
**缺点:** 性能比面向过程低
|
|
||||||
|
|
||||||
## 2. Java 语言有哪些特点?
|
## 2. Java 语言有哪些特点?
|
||||||
|
|
||||||
@ -80,89 +83,124 @@
|
|||||||
7. 支持网络编程并且很方便( Java 语言诞生本身就是为简化网络编程设计的,因此 Java 语言不仅支持网络编程而且很方便);
|
7. 支持网络编程并且很方便( Java 语言诞生本身就是为简化网络编程设计的,因此 Java 语言不仅支持网络编程而且很方便);
|
||||||
8. 编译与解释并存;
|
8. 编译与解释并存;
|
||||||
|
|
||||||
|
> 修正(参见: [issue#544](https://github.com/Snailclimb/JavaGuide/issues/544)):C++11 开始(2011 年的时候),C++就引入了多线程库,在 windows、linux、macos 都可以使用`std::thread`和`std::async`来创建线程。参考链接:http://www.cplusplus.com/reference/thread/thread/?kw=thread
|
||||||
|
|
||||||
## 3. 关于 JVM JDK 和 JRE 最详细通俗的解答
|
## 3. 关于 JVM JDK 和 JRE 最详细通俗的解答
|
||||||
|
|
||||||
### JVM
|
### JVM
|
||||||
|
|
||||||
Java虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。
|
Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。
|
||||||
|
|
||||||
**什么是字节码?采用字节码的好处是什么?**
|
**什么是字节码?采用字节码的好处是什么?**
|
||||||
|
|
||||||
> 在 Java 中,JVM可以理解的代码就叫做`字节码`(即扩展名为 `.class` 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java程序无须重新编译便可在多种不同操作系统的计算机上运行。
|
> 在 Java 中,JVM 可以理解的代码就叫做`字节码`(即扩展名为 `.class` 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。
|
||||||
|
|
||||||
**Java 程序从源代码到运行一般有下面3步:**
|
**Java 程序从源代码到运行一般有下面 3 步:**
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
我们需要格外注意的是 .class->机器码 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT 编译器,而JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言。
|
我们需要格外注意的是 .class->机器码 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言。
|
||||||
|
|
||||||
> HotSpot采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是JIT所需要编译的部分。JVM会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。JDK 9引入了一种新的编译模式AOT(Ahead of Time Compilation),它是直接将字节码编译成机器码,这样就避免了JIT预热等各方面的开销。JDK支持分层编译和AOT协作使用。但是 ,AOT 编译器的编译质量是肯定比不上 JIT 编译器的。
|
> HotSpot 采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是 JIT 所需要编译的部分。JVM 会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。JDK 9 引入了一种新的编译模式 AOT(Ahead of Time Compilation),它是直接将字节码编译成机器码,这样就避免了 JIT 预热等各方面的开销。JDK 支持分层编译和 AOT 协作使用。但是 ,AOT 编译器的编译质量是肯定比不上 JIT 编译器的。
|
||||||
|
|
||||||
总结:Java虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。
|
**总结:**
|
||||||
|
|
||||||
|
Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。
|
||||||
|
|
||||||
### JDK 和 JRE
|
### 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命令和其他的一些基础构件。但是,它不能用于创建新程序。
|
JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。但是,它不能用于创建新程序。
|
||||||
|
|
||||||
如果你只是为了运行一下 Java 程序的话,那么你只需要安装 JRE 就可以了。如果你需要进行一些 Java 编程方面的工作,那么你就需要安装JDK了。但是,这不是绝对的。有时,即使您不打算在计算机上进行任何Java开发,仍然需要安装JDK。例如,如果要使用JSP部署Web应用程序,那么从技术上讲,您只是在应用程序服务器中运行Java程序。那你为什么需要JDK呢?因为应用程序服务器会将 JSP 转换为 Java servlet,并且需要使用 JDK 来编译 servlet。
|
如果你只是为了运行一下 Java 程序的话,那么你只需要安装 JRE 就可以了。如果你需要进行一些 Java 编程方面的工作,那么你就需要安装 JDK 了。但是,这不是绝对的。有时,即使您不打算在计算机上进行任何 Java 开发,仍然需要安装 JDK。例如,如果要使用 JSP 部署 Web 应用程序,那么从技术上讲,您只是在应用程序服务器中运行 Java 程序。那你为什么需要 JDK 呢?因为应用程序服务器会将 JSP 转换为 Java servlet,并且需要使用 JDK 来编译 servlet。
|
||||||
|
|
||||||
## 4. Oracle JDK 和 OpenJDK 的对比
|
## 4. Oracle JDK 和 OpenJDK 的对比
|
||||||
|
|
||||||
可能在看这个问题之前很多人和我一样并没有接触和使用过 OpenJDK 。那么Oracle和OpenJDK之间是否存在重大差异?下面我通过收集到的一些资料,为你解答这个被很多人忽视的问题。
|
可能在看这个问题之前很多人和我一样并没有接触和使用过 OpenJDK 。那么 Oracle 和 OpenJDK 之间是否存在重大差异?下面我通过收集到的一些资料,为你解答这个被很多人忽视的问题。
|
||||||
|
|
||||||
对于Java 7,没什么关键的地方。OpenJDK项目主要基于Sun捐赠的HotSpot源代码。此外,OpenJDK被选为Java 7的参考实现,由Oracle工程师维护。关于JVM,JDK,JRE和OpenJDK之间的区别,Oracle博客帖子在2012年有一个更详细的答案:
|
对于 Java 7,没什么关键的地方。OpenJDK 项目主要基于 Sun 捐赠的 HotSpot 源代码。此外,OpenJDK 被选为 Java 7 的参考实现,由 Oracle 工程师维护。关于 JVM,JDK,JRE 和 OpenJDK 之间的区别,Oracle 博客帖子在 2012 年有一个更详细的答案:
|
||||||
|
|
||||||
> 问:OpenJDK存储库中的源代码与用于构建Oracle JDK的代码之间有什么区别?
|
> 问:OpenJDK 存储库中的源代码与用于构建 Oracle JDK 的代码之间有什么区别?
|
||||||
>
|
>
|
||||||
> 答:非常接近 - 我们的Oracle JDK版本构建过程基于OpenJDK 7构建,只添加了几个部分,例如部署代码,其中包括Oracle的Java插件和Java WebStart的实现,以及一些封闭的源代码派对组件,如图形光栅化器,一些开源的第三方组件,如Rhino,以及一些零碎的东西,如附加文档或第三方字体。展望未来,我们的目的是开源Oracle JDK的所有部分,除了我们考虑商业功能的部分。
|
> 答:非常接近 - 我们的 Oracle JDK 版本构建过程基于 OpenJDK 7 构建,只添加了几个部分,例如部署代码,其中包括 Oracle 的 Java 插件和 Java WebStart 的实现,以及一些封闭的源代码派对组件,如图形光栅化器,一些开源的第三方组件,如 Rhino,以及一些零碎的东西,如附加文档或第三方字体。展望未来,我们的目的是开源 Oracle JDK 的所有部分,除了我们考虑商业功能的部分。
|
||||||
|
|
||||||
总结:
|
**总结:**
|
||||||
|
|
||||||
1. Oracle JDK版本将每三年发布一次,而OpenJDK版本每三个月发布一次;
|
1. Oracle JDK 大概每 6 个月发一次主要版本,而 OpenJDK 版本大概每三个月发布一次。但这不是固定的,我觉得了解这个没啥用处。详情参见:[https://blogs.oracle.com/java-platform-group/update-and-faq-on-the-java-se-release-cadence](https://blogs.oracle.com/java-platform-group/update-and-faq-on-the-java-se-release-cadence) 。
|
||||||
2. OpenJDK 是一个参考模型并且是完全开源的,而Oracle JDK是OpenJDK的一个实现,并不是完全开源的;
|
2. OpenJDK 是一个参考模型并且是完全开源的,而 Oracle JDK 是 OpenJDK 的一个实现,并不是完全开源的;
|
||||||
3. Oracle JDK 比 OpenJDK 更稳定。OpenJDK和Oracle JDK的代码几乎相同,但Oracle JDK有更多的类和一些错误修复。因此,如果您想开发企业/商业软件,我建议您选择Oracle JDK,因为它经过了彻底的测试和稳定。某些情况下,有些人提到在使用OpenJDK 可能会遇到了许多应用程序崩溃的问题,但是,只需切换到Oracle JDK就可以解决问题;
|
3. Oracle JDK 比 OpenJDK 更稳定。OpenJDK 和 Oracle JDK 的代码几乎相同,但 Oracle JDK 有更多的类和一些错误修复。因此,如果您想开发企业/商业软件,我建议您选择 Oracle JDK,因为它经过了彻底的测试和稳定。某些情况下,有些人提到在使用 OpenJDK 可能会遇到了许多应用程序崩溃的问题,但是,只需切换到 Oracle JDK 就可以解决问题;
|
||||||
4. 在响应性和JVM性能方面,Oracle JDK与OpenJDK相比提供了更好的性能;
|
4. 在响应性和 JVM 性能方面,Oracle JDK 与 OpenJDK 相比提供了更好的性能;
|
||||||
5. Oracle JDK不会为即将发布的版本提供长期支持,用户每次都必须通过更新到最新版本获得支持来获取最新版本;
|
5. Oracle JDK 不会为即将发布的版本提供长期支持,用户每次都必须通过更新到最新版本获得支持来获取最新版本;
|
||||||
6. Oracle JDK根据二进制代码许可协议获得许可,而OpenJDK根据GPL v2许可获得许可。
|
6. Oracle JDK 根据二进制代码许可协议获得许可,而 OpenJDK 根据 GPL v2 许可获得许可。
|
||||||
|
|
||||||
## 5. Java和C++的区别?
|
## 5. Java 和 C++的区别?
|
||||||
|
|
||||||
我知道很多人没学过 C++,但是面试官就是没事喜欢拿咱们 Java 和 C++ 比呀!没办法!!!就算没学过C++,也要记下来!
|
我知道很多人没学过 C++,但是面试官就是没事喜欢拿咱们 Java 和 C++ 比呀!没办法!!!就算没学过 C++,也要记下来!
|
||||||
|
|
||||||
- 都是面向对象的语言,都支持封装、继承和多态
|
- 都是面向对象的语言,都支持封装、继承和多态
|
||||||
- Java 不提供指针来直接访问内存,程序内存更加安全
|
- Java 不提供指针来直接访问内存,程序内存更加安全
|
||||||
- Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。
|
- Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。
|
||||||
- Java 有自动内存管理机制,不需要程序员手动释放无用内存
|
- Java 有自动内存管理机制,不需要程序员手动释放无用内存
|
||||||
|
- **在 C 语言中,字符串或字符数组最后都会有一个额外的字符‘\0’来表示结束。但是,Java 语言中没有结束符这一概念。** 这是一个值得深度思考的问题,具体原因推荐看这篇文章: [https://blog.csdn.net/sszgg2006/article/details/49148189](https://blog.csdn.net/sszgg2006/article/details/49148189)
|
||||||
|
|
||||||
## 6. 什么是 Java 程序的主类 应用程序和小程序的主类有何不同?
|
## 6. 什么是 Java 程序的主类 应用程序和小程序的主类有何不同?
|
||||||
|
|
||||||
一个程序中可以有多个类,但只能有一个类是主类。在 Java 应用程序中,这个主类是指包含 main()方法的类。而在 Java 小程序中,这个主类是一个继承自系统类 JApplet 或 Applet 的子类。应用程序的主类不一定要求是 public 类,但小程序的主类要求必须是 public 类。主类是 Java 程序执行的入口点。
|
一个程序中可以有多个类,但只能有一个类是主类。在 Java 应用程序中,这个主类是指包含 main()方法的类。而在 Java 小程序中,这个主类是一个继承自系统类 JApplet 或 Applet 的子类。应用程序的主类不一定要求是 public 类,但小程序的主类要求必须是 public 类。主类是 Java 程序执行的入口点。
|
||||||
|
|
||||||
## 7. Java 应用程序与小程序之间有那些差别?
|
## 7. Java 应用程序与小程序之间有哪些差别?
|
||||||
|
|
||||||
简单说应用程序是从主线程启动(也就是 main() 方法)。applet 小程序没有main方法,主要是嵌在浏览器页面上运行(调用init()线程或者run()来启动),嵌入浏览器这点跟 flash 的小游戏类似。
|
简单说应用程序是从主线程启动(也就是 `main()` 方法)。applet 小程序没有 `main()` 方法,主要是嵌在浏览器页面上运行(调用`init()`或者`run()`来启动),嵌入浏览器这点跟 flash 的小游戏类似。
|
||||||
|
|
||||||
## 8. 字符型常量和字符串常量的区别?
|
## 8. 字符型常量和字符串常量的区别?
|
||||||
|
|
||||||
1. 形式上: 字符常量是单引号引起的一个字符; 字符串常量是双引号引起的若干个字符
|
1. 形式上: 字符常量是单引号引起的一个字符; 字符串常量是双引号引起的若干个字符
|
||||||
2. 含义上: 字符常量相当于一个整形值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)
|
2. 含义上: 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)
|
||||||
3. 占内存大小 字符常量只占2个字节; 字符串常量占若干个字节(至少一个字符结束标志) (**注意: char在Java中占两个字节**)
|
3. 占内存大小 字符常量只占 2 个字节; 字符串常量占若干个字节 (**注意: char 在 Java 中占两个字节**)
|
||||||
|
|
||||||
> java编程思想第四版:2.2.2节
|
> java 编程思想第四版:2.2.2 节
|
||||||

|
> 
|
||||||
|
|
||||||
## 9. 构造器 Constructor 是否可被 override?
|
## 9. 构造器 Constructor 是否可被 override?
|
||||||
|
|
||||||
在讲继承的时候我们就知道父类的私有属性和构造方法并不能被继承,所以 Constructor 也就不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。
|
Constructor 不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。
|
||||||
|
|
||||||
## 10. 重载和重写的区别
|
## 10. 重载和重写的区别
|
||||||
|
|
||||||
**重载:** 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。
|
> 重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理
|
||||||
|
>
|
||||||
|
> 重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法
|
||||||
|
|
||||||
**重写:** 发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为 private 则子类就不能重写该方法。
|
#### 重载
|
||||||
|
|
||||||
|
发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。
|
||||||
|
|
||||||
|
下面是《Java 核心技术》对重载这个概念的介绍:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**综上:重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理。**
|
||||||
|
|
||||||
|
#### 重写
|
||||||
|
|
||||||
|
重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。
|
||||||
|
|
||||||
|
1. 返回值类型、方法名、参数列表必须相同,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
|
||||||
|
2. 如果父类方法访问修饰符为 private/final/static 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。
|
||||||
|
3. 构造方法无法被重写
|
||||||
|
|
||||||
|
**综上:重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变**
|
||||||
|
|
||||||
|
**暖心的 Guide 哥最后再来个图标总结一下!**
|
||||||
|
|
||||||
|
| 区别点 | 重载方法 | 重写方法 |
|
||||||
|
| :--------- | :------- | :--------------------------------------------- |
|
||||||
|
| 发生范围 | 同一个类 | 子类 中 |
|
||||||
|
| 参数列表 | 必须修改 | 一定不能修改 |
|
||||||
|
| 返回类型 | 可修改 | 一定不能修改 |
|
||||||
|
| 异常 | 可修改 | 可以减少或删除,一定不能抛出新的或者更广的异常 |
|
||||||
|
| 访问修饰符 | 可修改 | 一定不能做更严格的限制(可以降低限制) |
|
||||||
|
| 发生阶段 | 编译期 | 运行期 |
|
||||||
|
|
||||||
## 11. Java 面向对象编程三大特性: 封装 继承 多态
|
## 11. Java 面向对象编程三大特性: 封装 继承 多态
|
||||||
|
|
||||||
@ -170,13 +208,13 @@ JRE 是 Java运行时环境。它是运行已编译 Java 程序所需的所有
|
|||||||
|
|
||||||
封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。
|
封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。
|
||||||
|
|
||||||
|
|
||||||
### 继承
|
### 继承
|
||||||
|
|
||||||
继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。
|
继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。
|
||||||
|
|
||||||
**关于继承如下 3 点请记住:**
|
**关于继承如下 3 点请记住:**
|
||||||
|
|
||||||
1. 子类拥有父类非 private 的属性和方法。
|
1. 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,**只是拥有**。
|
||||||
2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
|
2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
|
||||||
3. 子类可以用自己的方式实现父类的方法。(以后介绍)。
|
3. 子类可以用自己的方式实现父类的方法。(以后介绍)。
|
||||||
|
|
||||||
@ -184,83 +222,103 @@ JRE 是 Java运行时环境。它是运行已编译 Java 程序所需的所有
|
|||||||
|
|
||||||
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
|
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
|
||||||
|
|
||||||
在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。
|
在 Java 中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。
|
||||||
|
|
||||||
## 12. String StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的?
|
## 12. String StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的?
|
||||||
|
|
||||||
**可变性**
|
**可变性**
|
||||||
|
|
||||||
|
|
||||||
简单的来说:String 类中使用 final 关键字修饰字符数组来保存字符串,`private final char value[]`,所以 String 对象是不可变的。而StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串`char[]value` 但是没有用 final 关键字修饰,所以这两种对象都是可变的。
|
简单的来说:String 类中使用 final 关键字修饰字符数组来保存字符串,`private final char value[]`,所以 String 对象是不可变的。
|
||||||
|
|
||||||
|
> 补充(来自[issue 675](https://github.com/Snailclimb/JavaGuide/issues/675)):在 Java 9 之后,String 类的实现改用 byte 数组存储字符串 `private final byte[] value`;
|
||||||
|
|
||||||
|
而 StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串`char[]value` 但是没有用 final 关键字修饰,所以这两种对象都是可变的。
|
||||||
|
|
||||||
StringBuilder 与 StringBuffer 的构造方法都是调用父类构造方法也就是 AbstractStringBuilder 实现的,大家可以自行查阅源码。
|
StringBuilder 与 StringBuffer 的构造方法都是调用父类构造方法也就是 AbstractStringBuilder 实现的,大家可以自行查阅源码。
|
||||||
|
|
||||||
AbstractStringBuilder.java
|
`AbstractStringBuilder.java`
|
||||||
|
|
||||||
```java
|
```java
|
||||||
abstract class AbstractStringBuilder implements Appendable, CharSequence {
|
abstract class AbstractStringBuilder implements Appendable, CharSequence {
|
||||||
|
/**
|
||||||
|
* The value is used for character storage.
|
||||||
|
*/
|
||||||
char[] value;
|
char[] value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The count is the number of characters used.
|
||||||
|
*/
|
||||||
int count;
|
int count;
|
||||||
AbstractStringBuilder() {
|
|
||||||
}
|
|
||||||
AbstractStringBuilder(int capacity) {
|
AbstractStringBuilder(int capacity) {
|
||||||
value = new char[capacity];
|
value = new char[capacity];
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
**线程安全性**
|
**线程安全性**
|
||||||
|
|
||||||
String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
|
String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
|
||||||
|
|
||||||
|
|
||||||
**性能**
|
**性能**
|
||||||
|
|
||||||
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
|
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
|
||||||
|
|
||||||
**对于三者使用的总结:**
|
**对于三者使用的总结:**
|
||||||
1. 操作少量的数据: 适用String
|
|
||||||
2. 单线程操作字符串缓冲区下操作大量数据: 适用StringBuilder
|
1. 操作少量的数据: 适用 String
|
||||||
3. 多线程操作字符串缓冲区下操作大量数据: 适用StringBuffer
|
2. 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
|
||||||
|
3. 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer
|
||||||
|
|
||||||
## 13. 自动装箱与拆箱
|
## 13. 自动装箱与拆箱
|
||||||
**装箱**:将基本类型用它们对应的引用类型包装起来;
|
|
||||||
|
|
||||||
**拆箱**:将包装类型转换为基本数据类型;
|
- **装箱**:将基本类型用它们对应的引用类型包装起来;
|
||||||
|
- **拆箱**:将包装类型转换为基本数据类型;
|
||||||
|
|
||||||
|
更多内容见:[深入剖析 Java 中的装箱和拆箱](https://www.cnblogs.com/dolphin0520/p/3780005.html)
|
||||||
|
|
||||||
## 14. 在一个静态方法内调用一个非静态成员为什么是非法的?
|
## 14. 在一个静态方法内调用一个非静态成员为什么是非法的?
|
||||||
|
|
||||||
由于静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成员。
|
由于静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成员。
|
||||||
|
|
||||||
## 15. 在 Java 中定义一个不做事且没有参数的构造方法的作用
|
## 15. 在 Java 中定义一个不做事且没有参数的构造方法的作用
|
||||||
Java 程序在执行子类的构造方法之前,如果没有用 super() 来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用 super() 来调用父类中特定的构造方法,则编译时将发生错误,因为 Java 程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。
|
|
||||||
|
|
||||||
## 16. import java和javax有什么区别?
|
|
||||||
|
|
||||||
刚开始的时候 JavaAPI 所必需的包是 java 开头的包,javax 当时只是扩展 API 包来使用。然而随着时间的推移,javax 逐渐地扩展成为 Java API 的组成部分。但是,但是,将扩展从 javax 包移动到 java 包确实太麻烦了,最终会破坏一堆现有的代码。因此,最终决定 javax 包将成为标准API的一部分。
|
Java 程序在执行子类的构造方法之前,如果没有用 `super()`来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用 `super()`来调用父类中特定的构造方法,则编译时将发生错误,因为 Java 程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。
|
||||||
|
|
||||||
所以,实际上java和javax没有区别。这都是一个名字。
|
## 16. import java 和 javax 有什么区别?
|
||||||
|
|
||||||
|
刚开始的时候 JavaAPI 所必需的包是 java 开头的包,javax 当时只是扩展 API 包来使用。然而随着时间的推移,javax 逐渐地扩展成为 Java API 的组成部分。但是,将扩展从 javax 包移动到 java 包确实太麻烦了,最终会破坏一堆现有的代码。因此,最终决定 javax 包将成为标准 API 的一部分。
|
||||||
|
|
||||||
|
所以,实际上 java 和 javax 没有区别。这都是一个名字。
|
||||||
|
|
||||||
## 17. 接口和抽象类的区别是什么?
|
## 17. 接口和抽象类的区别是什么?
|
||||||
|
|
||||||
1. 接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),而抽象类可以有非抽象的方法。
|
1. 接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),而抽象类可以有非抽象的方法。
|
||||||
2. 接口中的实例变量默认是 final 类型的,而抽象类中则不一定。
|
2. 接口中除了 static、final 变量,不能有其他变量,而抽象类中则不一定。
|
||||||
3. 一个类可以实现多个接口,但最多只能实现一个抽象类。
|
3. 一个类可以实现多个接口,但只能实现一个抽象类。接口自己本身可以通过 extends 关键字扩展多个接口。
|
||||||
4. 一个类实现接口的话要实现接口的所有方法,而抽象类不一定。
|
4. 接口方法默认修饰符是 public,抽象方法可以有 public、protected 和 default 这些修饰符(抽象方法就是为了被重写所以不能使用 private 关键字修饰!)。
|
||||||
5. 接口不能用 new 实例化,但可以声明,但是必须引用一个实现该接口的对象。从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。
|
5. 从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。
|
||||||
|
|
||||||
备注:在JDK8中,接口也可以定义静态方法,可以直接用接口名调用。实现类和实现是不可以调用的。如果同时实现两个接口,接口中定义了一样的默认方法,则必须重写,不然会报错。(详见issue:[https://github.com/Snailclimb/JavaGuide/issues/146](https://github.com/Snailclimb/JavaGuide/issues/146))
|
> 备注:
|
||||||
|
>
|
||||||
|
> 1. 在 JDK8 中,接口也可以定义静态方法,可以直接用接口名调用。实现类和实现是不可以调用的。如果同时实现两个接口,接口中定义了一样的默认方法,则必须重写,不然会报错。(详见 issue:[https://github.com/Snailclimb/JavaGuide/issues/146](https://github.com/Snailclimb/JavaGuide/issues/146)。
|
||||||
|
> 2. jdk9 的接口被允许定义私有方法 。
|
||||||
|
|
||||||
## 18. 成员变量与局部变量的区别有那些?
|
总结一下 jdk7~jdk9 Java 中接口概念的变化([相关阅读](https://www.geeksforgeeks.org/private-methods-java-9-interfaces/)):
|
||||||
|
|
||||||
|
1. 在 jdk 7 或更早版本中,接口里面只能有常量变量和抽象方法。这些接口方法必须由选择实现接口的类实现。
|
||||||
|
2. jdk8 的时候接口可以有默认方法和静态方法功能。
|
||||||
|
3. Jdk 9 在接口中引入了私有方法和私有静态方法。
|
||||||
|
|
||||||
|
## 18. 成员变量与局部变量的区别有哪些?
|
||||||
|
|
||||||
1. 从语法形式上看:成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
|
1. 从语法形式上看:成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
|
||||||
2. 从变量在内存中的存储方式来看:如果成员变量是使用`static`修饰的,那么这个成员变量是属于类的,如果没有使用使用`static`修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
|
2. 从变量在内存中的存储方式来看:如果成员变量是使用`static`修饰的,那么这个成员变量是属于类的,如果没有使用`static`修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
|
||||||
3. 从变量在内存中的生存时间上看:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。
|
3. 从变量在内存中的生存时间上看:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。
|
||||||
4. 成员变量如果没有被赋初值:则会自动以类型的默认值而赋值(一种情况例外被 final 修饰的成员变量也必须显示地赋值),而局部变量则不会自动赋值。
|
4. 成员变量如果没有被赋初值:则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。
|
||||||
|
|
||||||
## 19. 创建一个对象用什么运算符?对象实体与对象引用有何不同?
|
## 19. 创建一个对象用什么运算符?对象实体与对象引用有何不同?
|
||||||
|
|
||||||
new运算符,new创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。一个对象引用可以指向0个或1个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有n个引用指向它(可以用n条绳子系住一个气球)。
|
new 运算符,new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有 n 个引用指向它(可以用 n 条绳子系住一个气球)。
|
||||||
|
|
||||||
## 20. 什么是方法的返回值?返回值在类的方法里的作用是什么?
|
## 20. 什么是方法的返回值?返回值在类的方法里的作用是什么?
|
||||||
|
|
||||||
@ -273,12 +331,12 @@ new运算符,new创建对象实例(对象实例在堆内存中),对象
|
|||||||
## 22. 构造方法有哪些特性?
|
## 22. 构造方法有哪些特性?
|
||||||
|
|
||||||
1. 名字与类名相同。
|
1. 名字与类名相同。
|
||||||
2. 没有返回值,但不能用void声明构造函数。
|
2. 没有返回值,但不能用 void 声明构造函数。
|
||||||
3. 生成类的对象时自动执行,无需调用。
|
3. 生成类的对象时自动执行,无需调用。
|
||||||
|
|
||||||
## 23. 静态方法和实例方法有何不同
|
## 23. 静态方法和实例方法有何不同
|
||||||
|
|
||||||
1. 在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。
|
1. 在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。
|
||||||
|
|
||||||
2. 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制。
|
2. 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制。
|
||||||
|
|
||||||
@ -295,9 +353,9 @@ new运算符,new创建对象实例(对象实例在堆内存中),对象
|
|||||||
**==** : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)。
|
**==** : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)。
|
||||||
|
|
||||||
**equals()** : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
|
**equals()** : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
|
||||||
- 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
|
|
||||||
- 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
|
|
||||||
|
|
||||||
|
- 情况 1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
|
||||||
|
- 情况 2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
|
||||||
|
|
||||||
**举个例子:**
|
**举个例子:**
|
||||||
|
|
||||||
@ -322,138 +380,149 @@ public class test1 {
|
|||||||
```
|
```
|
||||||
|
|
||||||
**说明:**
|
**说明:**
|
||||||
|
|
||||||
- String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。
|
- String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。
|
||||||
- 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。
|
- 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。
|
||||||
|
|
||||||
|
## 27. hashCode 与 equals (重要)
|
||||||
|
|
||||||
|
面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写 equals 时必须重写 hashCode 方法?”
|
||||||
## 27. hashCode 与 equals (重要)
|
|
||||||
|
|
||||||
面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写equals时必须重写hashCode方法?”
|
|
||||||
|
|
||||||
### hashCode()介绍
|
### hashCode()介绍
|
||||||
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode() 函数。
|
|
||||||
|
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在 JDK 的 Object.java 中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。
|
||||||
|
|
||||||
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)
|
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)
|
||||||
|
|
||||||
### 为什么要有 hashCode
|
### 为什么要有 hashCode
|
||||||
|
|
||||||
|
**我们先以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:** 当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与该位置其他已经加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 `equals()`方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的 Java 启蒙书《Head first java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。
|
||||||
|
|
||||||
**我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:**
|
通过我们可以看出:`hashCode()` 的作用就是**获取哈希码**,也称为散列码;它实际上是返回一个 int 整数。这个**哈希码的作用**是确定该对象在哈希表中的索引位置。**`hashCode()`在散列表中才有用,在其它情况下没用**。在散列表中 hashCode() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置。
|
||||||
|
|
||||||
当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的Java启蒙书《Head first java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。
|
### hashCode()与 equals()的相关规定
|
||||||
|
|
||||||
|
1. 如果两个对象相等,则 hashcode 一定也是相同的
|
||||||
|
2. 两个对象相等,对两个对象分别调用 equals 方法都返回 true
|
||||||
### hashCode()与equals()的相关规定
|
3. 两个对象有相同的 hashcode 值,它们也不一定是相等的
|
||||||
|
|
||||||
1. 如果两个对象相等,则hashcode一定也是相同的
|
|
||||||
2. 两个对象相等,对两个对象分别调用equals方法都返回true
|
|
||||||
3. 两个对象有相同的hashcode值,它们也不一定是相等的
|
|
||||||
4. **因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖**
|
4. **因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖**
|
||||||
5. hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
|
5. hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
|
||||||
|
|
||||||
|
推荐阅读:[Java hashCode() 和 equals()的若干问题解答](https://www.cnblogs.com/skywang12345/p/3324958.html)
|
||||||
|
|
||||||
## 28. 为什么Java中只有值传递?
|
## 28. 为什么 Java 中只有值传递?
|
||||||
|
|
||||||
[为什么Java中只有值传递?](https://github.com/Snailclimb/JavaGuide/blob/master/docs/essential-content-for-interview/MostCommonJavaInterviewQuestions/%E7%AC%AC%E4%B8%80%E5%91%A8%EF%BC%882018-8-7%EF%BC%89.md)
|
|
||||||
|
|
||||||
|
[为什么 Java 中只有值传递?](https://juejin.im/post/5e18879e6fb9a02fc63602e2)
|
||||||
|
|
||||||
## 29. 简述线程、程序、进程的基本概念。以及他们之间关系是什么?
|
## 29. 简述线程、程序、进程的基本概念。以及他们之间关系是什么?
|
||||||
|
|
||||||
**线程**与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
|
**线程**与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
|
||||||
|
|
||||||
**程序**是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
|
**程序**是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
|
||||||
|
|
||||||
**进程**是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。
|
**进程**是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如 CPU 时间,内存空间,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。
|
||||||
线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。
|
线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。
|
||||||
|
|
||||||
## 30. 线程有哪些基本状态?
|
## 30. 线程有哪些基本状态?
|
||||||
|
|
||||||
Java 线程在运行的生命周期中的指定时刻只可能处于下面6种不同状态的其中一个状态(图源《Java 并发编程艺术》4.1.4节)。
|
Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态(图源《Java 并发编程艺术》4.1.4 节)。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示(图源《Java 并发编程艺术》4.1.4节):
|
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示(图源《Java 并发编程艺术》4.1.4 节):
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
由上图可以看出:
|
由上图可以看出:
|
||||||
|
|
||||||
线程创建之后它将处于 **NEW(新建)** 状态,调用 `start()` 方法后开始运行,线程这时候处于 **READY(可运行)** 状态。可运行状态的线程获得了 cpu 时间片(timeslice)后就处于 **RUNNING(运行)** 状态。
|
线程创建之后它将处于 **NEW(新建)** 状态,调用 `start()` 方法后开始运行,线程这时候处于 **READY(可运行)** 状态。可运行状态的线程获得了 cpu 时间片(timeslice)后就处于 **RUNNING(运行)** 状态。
|
||||||
|
|
||||||
> 操作系统隐藏 Java虚拟机(JVM)中的 RUNNABLE 和 RUNNING 状态,它只能看到 RUNNABLE 状态(图源:[HowToDoInJava](https://howtodoinjava.com/):[Java Thread Life Cycle and Thread States](https://howtodoinjava.com/java/multi-threading/java-thread-life-cycle-and-thread-states/)),所以 Java 系统一般将这两个状态统称为 **RUNNABLE(运行中)** 状态 。
|
> 操作系统隐藏 Java 虚拟机(JVM)中的 READY 和 RUNNING 状态,它只能看到 RUNNABLE 状态(图源:[HowToDoInJava](https://howtodoinjava.com/):[Java Thread Life Cycle and Thread States](https://howtodoinjava.com/java/multi-threading/java-thread-life-cycle-and-thread-states/)),所以 Java 系统一般将这两个状态统称为 **RUNNABLE(运行中)** 状态 。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
当线程执行 `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(终止)** 状态。
|
||||||
|
|
||||||
## 31 关于 final 关键字的一些总结
|
## 31 关于 final 关键字的一些总结
|
||||||
|
|
||||||
final关键字主要用在三个地方:变量、方法、类。
|
final 关键字主要用在三个地方:变量、方法、类。
|
||||||
|
|
||||||
1. 对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。
|
1. 对于一个 final 变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。
|
||||||
2. 当用final修饰一个类时,表明这个类不能被继承。final类中的所有成员方法都会被隐式地指定为final方法。
|
2. 当用 final 修饰一个类时,表明这个类不能被继承。final 类中的所有成员方法都会被隐式地指定为 final 方法。
|
||||||
3. 使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的Java版本已经不需要使用final方法进行这些优化了)。类中所有的private方法都隐式地指定为final。
|
3. 使用 final 方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的 Java 实现版本中,会将 final 方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的 Java 版本已经不需要使用 final 方法进行这些优化了)。类中所有的 private 方法都隐式地指定为 final。
|
||||||
|
|
||||||
## 32 Java 中的异常处理
|
## 32 Java 中的异常处理
|
||||||
|
|
||||||
### Java异常类层次结构图
|
### Java 异常类层次结构图
|
||||||
|
|
||||||

|

|
||||||
在 Java 中,所有的异常都有一个共同的祖先java.lang包中的 **Throwable类**。Throwable: 有两个重要的子类:**Exception(异常)** 和 **Error(错误)** ,二者都是 Java 异常处理的重要子类,各自都包含大量子类。
|
|
||||||
|
|
||||||
**Error(错误):是程序无法处理的错误**,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。
|
<p style="font-size:13px;text-align:right">图片来自:https://simplesnippets.tech/exception-handling-in-java-part-1/</p>
|
||||||
|
|
||||||
这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java中,错误通过Error的子类描述。
|
|
||||||
|
|
||||||
**Exception(异常):是程序本身可以处理的异常**。</font>Exception 类有一个重要的子类 **RuntimeException**。RuntimeException 异常由Java虚拟机抛出。**NullPointerException**(要访问的变量没有引用任何对象时,抛出该异常)、**ArithmeticException**(算术运算异常,一个整数除以0时,抛出该异常)和 **ArrayIndexOutOfBoundsException** (下标越界异常)。
|

|
||||||
|
|
||||||
**注意:异常和错误的区别:异常能被程序本身可以处理,错误是无法处理。**
|
<p style="font-size:13px;text-align:right">图片来自:https://chercher.tech/java-programming/exceptions-java</p>
|
||||||
|
|
||||||
### Throwable类常用方法
|
|
||||||
|
|
||||||
- **public string getMessage()**:返回异常发生时的详细信息
|
在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 **Throwable 类**。Throwable: 有两个重要的子类:**Exception(异常)** 和 **Error(错误)** ,二者都是 Java 异常处理的重要子类,各自都包含大量子类。
|
||||||
- **public string toString()**:返回异常发生时的简要描述
|
|
||||||
- **public string getLocalizedMessage()**:返回异常对象的本地化信息。使用Throwable的子类覆盖这个方法,可以声称本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()返回的结果相同
|
**Error(错误):是程序无法处理的错误**,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java 虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
|
||||||
- **public void printStackTrace()**:在控制台上打印Throwable对象封装的异常信息
|
|
||||||
|
这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如 Java 虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java 中,错误通过 Error 的子类描述。
|
||||||
|
|
||||||
|
**Exception(异常):是程序本身可以处理的异常**。</font>Exception 类有一个重要的子类 **RuntimeException**。RuntimeException 异常由 Java 虚拟机抛出。**NullPointerException**(要访问的变量没有引用任何对象时,抛出该异常)、**ArithmeticException**(算术运算异常,一个整数除以 0 时,抛出该异常)和 **ArrayIndexOutOfBoundsException** (下标越界异常)。
|
||||||
|
|
||||||
|
**注意:异常和错误的区别:异常能被程序本身处理,错误是无法处理。**
|
||||||
|
|
||||||
|
### Throwable 类常用方法
|
||||||
|
|
||||||
|
- **public string getMessage()**:返回异常发生时的简要描述
|
||||||
|
- **public string toString()**:返回异常发生时的详细信息
|
||||||
|
- **public string getLocalizedMessage()**:返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同
|
||||||
|
- **public void printStackTrace()**:在控制台上打印 Throwable 对象封装的异常信息
|
||||||
|
|
||||||
### 异常处理总结
|
### 异常处理总结
|
||||||
|
|
||||||
- **try 块:**用于捕获异常。其后可接零个或多个catch块,如果没有catch块,则必须跟一个finally块。
|
- **try 块:** 用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
|
||||||
- **catch 块:**用于处理try捕获到的异常。
|
- **catch 块:** 用于处理 try 捕获到的异常。
|
||||||
- **finally 块:**无论是否捕获或处理异常,finally块里的语句都会被执行。当在try块或catch块中遇到return语句时,finally语句块将在方法返回之前被执行。
|
- **finally 块:** 无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return
|
||||||
|
语句时,finally 语句块将在方法返回之前被执行。
|
||||||
|
|
||||||
**在以下4种特殊情况下,finally块不会被执行:**
|
**在以下 4 种特殊情况下,finally 块不会被执行:**
|
||||||
|
|
||||||
1. 在finally语句块第一行发生了异常。 因为在其他行,finally块还是会得到执行
|
1. 在 finally 语句块第一行发生了异常。 因为在其他行,finally 块还是会得到执行
|
||||||
2. 在前面的代码中用了System.exit(int)已退出程序。 exit是带参函数 ;若该语句在异常语句之后,finally会执行
|
2. 在前面的代码中用了 System.exit(int)已退出程序。 exit 是带参函数 ;若该语句在异常语句之后,finally 会执行
|
||||||
3. 程序所在的线程死亡。
|
3. 程序所在的线程死亡。
|
||||||
4. 关闭CPU。
|
4. 关闭 CPU。
|
||||||
|
|
||||||
下面这部分内容来自issue:<https://github.com/Snailclimb/JavaGuide/issues/190>。
|
下面这部分内容来自 issue:<https://github.com/Snailclimb/JavaGuide/issues/190>。
|
||||||
|
|
||||||
**关于返回值:**
|
**注意:** 当 try 语句和 finally 语句中都有 return 语句时,在方法返回之前,finally 语句的内容将被执行,并且 finally 语句的返回值将会覆盖原始的返回值。如下:
|
||||||
|
|
||||||
如果try语句里有return,返回的是try语句块中变量值。
|
```java
|
||||||
详细执行过程如下:
|
public static int f(int value) {
|
||||||
|
try {
|
||||||
|
return value * value;
|
||||||
|
} finally {
|
||||||
|
if (value == 2) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
1. 如果有返回值,就把返回值保存到局部变量中;
|
如果调用 `f(2)`,返回值将是 0,因为 finally 语句的返回值覆盖了 try 语句块的返回值。
|
||||||
2. 执行jsr指令跳到finally语句里执行;
|
|
||||||
3. 执行完finally语句后,返回之前保存在局部变量表里的值。
|
|
||||||
4. 如果try,finally语句里均有return,忽略try的return,而使用finally的return.
|
|
||||||
|
|
||||||
## 33 Java序列化中如果有些字段不想进行序列化,怎么办?
|
## 33 Java 序列化中如果有些字段不想进行序列化,怎么办?
|
||||||
|
|
||||||
对于不想进行序列化的变量,使用transient关键字修饰。
|
对于不想进行序列化的变量,使用 transient 关键字修饰。
|
||||||
|
|
||||||
transient关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被transient修饰的变量值不会被持久化和恢复。transient只能修饰变量,不能修饰类和方法。
|
transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。transient 只能修饰变量,不能修饰类和方法。
|
||||||
|
|
||||||
## 34 获取用键盘输入常用的的两种方法
|
## 34 获取用键盘输入常用的两种方法
|
||||||
|
|
||||||
方法1:通过 Scanner
|
方法 1:通过 Scanner
|
||||||
|
|
||||||
```java
|
```java
|
||||||
Scanner input = new Scanner(System.in);
|
Scanner input = new Scanner(System.in);
|
||||||
@ -461,15 +530,73 @@ String s = input.nextLine();
|
|||||||
input.close();
|
input.close();
|
||||||
```
|
```
|
||||||
|
|
||||||
方法2:通过 BufferedReader
|
方法 2:通过 BufferedReader
|
||||||
|
|
||||||
```java
|
```java
|
||||||
BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
|
BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
|
||||||
String s = input.readLine();
|
String s = input.readLine();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 35 Java 中 IO 流
|
||||||
|
|
||||||
|
### Java 中 IO 流分为几种?
|
||||||
|
|
||||||
|
- 按照流的流向分,可以分为输入流和输出流;
|
||||||
|
- 按照操作单元划分,可以划分为字节流和字符流;
|
||||||
|
- 按照流的角色划分为节点流和处理流。
|
||||||
|
|
||||||
|
Java Io 流共涉及 40 多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java I0 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。
|
||||||
|
|
||||||
|
- InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
|
||||||
|
- OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
|
||||||
|
|
||||||
|
按操作方式分类结构图:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
按操作对象分类结构图:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 既然有了字节流,为什么还要有字符流?
|
||||||
|
|
||||||
|
问题本质想问:**不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?**
|
||||||
|
|
||||||
|
回答:字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。
|
||||||
|
|
||||||
|
### BIO,NIO,AIO 有什么区别?
|
||||||
|
|
||||||
|
- **BIO (Blocking I/O):** 同步阻塞 I/O 模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机 1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
|
||||||
|
- **NIO (Non-blocking/New I/O):** NIO 是一种同步非阻塞的 I/O 模型,在 Java 1.4 中引入了 NIO 框架,对应 java.nio 包,提供了 Channel , Selector,Buffer 等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它支持面向缓冲的,基于通道的 I/O 操作方法。 NIO 提供了与传统 BIO 模型中的 `Socket` 和 `ServerSocket` 相对应的 `SocketChannel` 和 `ServerSocketChannel` 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞 I/O 来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发
|
||||||
|
- **AIO (Asynchronous I/O):** AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的 IO 模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步 IO 的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO 操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。
|
||||||
|
|
||||||
|
## 36. 常见关键字总结:static,final,this,super
|
||||||
|
|
||||||
|
详见笔主的这篇文章: https://snailclimb.gitee.io/javaguide/#/docs/java/basic/final,static,this,super
|
||||||
|
|
||||||
|
## 37. Collections 工具类和 Arrays 工具类常见方法总结
|
||||||
|
|
||||||
|
详见笔主的这篇文章: https://gitee.com/SnailClimb/JavaGuide/blob/master/docs/java/basic/Arrays,CollectionsCommonMethods.md
|
||||||
|
|
||||||
|
## 38. 深拷贝 vs 浅拷贝
|
||||||
|
|
||||||
|
1. **浅拷贝**:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。
|
||||||
|
2. **深拷贝**:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## 参考
|
## 参考
|
||||||
|
|
||||||
- https://stackoverflow.com/questions/1906445/what-is-the-difference-between-jdk-and-jre
|
- https://stackoverflow.com/questions/1906445/what-is-the-difference-between-jdk-and-jre
|
||||||
- https://www.educba.com/oracle-vs-openjdk/
|
- https://www.educba.com/oracle-vs-openjdk/
|
||||||
- https://stackoverflow.com/questions/22358071/differences-between-oracle-jdk-and-openjdk?answertab=active#tab-top
|
- https://stackoverflow.com/questions/22358071/differences-between-oracle-jdk-and-openjdk?answertab=active#tab-top
|
||||||
|
|
||||||
|
## 公众号
|
||||||
|
|
||||||
|
如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。
|
||||||
|
|
||||||
|
**《Java 面试突击》:** 由本文档衍生的专为面试而生的《Java 面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java 面试突击"** 即可免费领取!
|
||||||
|
|
||||||
|
**Java 工程师必备学习资源:** 一些 Java 工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。
|
||||||
|
|
||||||
|

|
||||||
|
373
docs/java/Java疑难点.md
Normal file
373
docs/java/Java疑难点.md
Normal file
@ -0,0 +1,373 @@
|
|||||||
|
<!-- TOC -->
|
||||||
|
|
||||||
|
- [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-操作)
|
||||||
|
|
||||||
|
<!-- /TOC -->
|
||||||
|
|
||||||
|
# 1. 基础
|
||||||
|
|
||||||
|
## 1.1. 正确使用 equals 方法
|
||||||
|
|
||||||
|
Object的equals方法容易抛空指针异常,应使用常量或确定有值的对象来调用 equals。
|
||||||
|
|
||||||
|
举个例子:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 不能使用一个值为null的引用类型变量来调用非静态方法,否则会抛出异常
|
||||||
|
String str = null;
|
||||||
|
if (str.equals("SnailClimb")) {
|
||||||
|
...
|
||||||
|
} else {
|
||||||
|
..
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
运行上面的程序会抛出空指针异常,但是我们把第二行的条件判断语句改为下面这样的话,就不会抛出空指针异常,else 语句块得到执行。:
|
||||||
|
|
||||||
|
```java
|
||||||
|
"SnailClimb".equals(str);// false
|
||||||
|
```
|
||||||
|
不过更推荐使用 `java.util.Objects#equals`(JDK7 引入的工具类)。
|
||||||
|
|
||||||
|
```java
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意:**
|
||||||
|
|
||||||
|
Reference:[Java中equals方法造成空指针异常的原因及解决方案](https://blog.csdn.net/tick_tock97/article/details/72824894)
|
||||||
|
|
||||||
|
- 每种原始类型都有默认值一样,如int默认值为 0,boolean 的默认值为 false,null 是任何引用类型的默认值,不严格的说是所有 Object 类型的默认值。
|
||||||
|
- 可以使用 == 或者 != 操作来比较null值,但是不能使用其他算法或者逻辑操作。在Java中`null == null`将返回true。
|
||||||
|
- 不能使用一个值为null的引用类型变量来调用非静态方法,否则会抛出异常
|
||||||
|
|
||||||
|
## 1.2. 整型包装类值的比较
|
||||||
|
|
||||||
|
所有整型包装类对象值的比较必须使用equals方法。
|
||||||
|
|
||||||
|
先看下面这个例子:
|
||||||
|
|
||||||
|
```java
|
||||||
|
Integer x = 3;
|
||||||
|
Integer y = 3;
|
||||||
|
System.out.println(x == y);// true
|
||||||
|
Integer a = new Integer(3);
|
||||||
|
Integer b = new Integer(3);
|
||||||
|
System.out.println(a == b);//false
|
||||||
|
System.out.println(a.equals(b));//true
|
||||||
|
```
|
||||||
|
|
||||||
|
当使用自动装箱方式创建一个Integer对象时,当数值在-128 ~127时,会将创建的 Integer 对象缓存起来,当下次再出现该数值时,直接从缓存中取出对应的Integer对象。所以上述代码中,x和y引用的是相同的Integer对象。
|
||||||
|
|
||||||
|
**注意:**如果你的IDE(IDEA/Eclipse)上安装了阿里巴巴的p3c插件,这个插件如果检测到你用 ==的话会报错提示,推荐安装一个这个插件,很不错。
|
||||||
|
|
||||||
|
## 1.3. BigDecimal
|
||||||
|
|
||||||
|
### 1.3.1. BigDecimal 的用处
|
||||||
|
|
||||||
|
《阿里巴巴Java开发手册》中提到:**浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用 equals 来判断。** 具体原理和浮点数的编码方式有关,这里就不多提了,我们下面直接上实例:
|
||||||
|
|
||||||
|
```java
|
||||||
|
float a = 1.0f - 0.9f;
|
||||||
|
float b = 0.9f - 0.8f;
|
||||||
|
System.out.println(a);// 0.100000024
|
||||||
|
System.out.println(b);// 0.099999964
|
||||||
|
System.out.println(a == b);// false
|
||||||
|
```
|
||||||
|
具有基本数学知识的我们很清楚的知道输出并不是我们想要的结果(**精度丢失**),我们如何解决这个问题呢?一种很常用的方法是:**使用使用 BigDecimal 来定义浮点数的值,再进行浮点数的运算操作。**
|
||||||
|
|
||||||
|
```java
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3.2. BigDecimal 的大小比较
|
||||||
|
|
||||||
|
`a.compareTo(b)` : 返回 -1 表示小于,0 表示 等于, 1表示 大于。
|
||||||
|
|
||||||
|
```java
|
||||||
|
BigDecimal a = new BigDecimal("1.0");
|
||||||
|
BigDecimal b = new BigDecimal("0.9");
|
||||||
|
System.out.println(a.compareTo(b));// 1
|
||||||
|
```
|
||||||
|
### 1.3.3. BigDecimal 保留几位小数
|
||||||
|
|
||||||
|
通过 `setScale`方法设置保留几位小数以及保留规则。保留规则有挺多种,不需要记,IDEA会提示。
|
||||||
|
|
||||||
|
```java
|
||||||
|
BigDecimal m = new BigDecimal("1.255433");
|
||||||
|
BigDecimal n = m.setScale(3,BigDecimal.ROUND_HALF_DOWN);
|
||||||
|
System.out.println(n);// 1.255
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3.4. BigDecimal 的使用注意事项
|
||||||
|
|
||||||
|
注意:我们在使用BigDecimal时,为了防止精度丢失,推荐使用它的 **BigDecimal(String)** 构造方法来创建对象。《阿里巴巴Java开发手册》对这部分内容也有提到如下图所示。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 1.3.5. 总结
|
||||||
|
|
||||||
|
BigDecimal 主要用来操作(大)浮点数,BigInteger 主要用来操作大整数(超过 long 类型)。
|
||||||
|
|
||||||
|
BigDecimal 的实现利用到了 BigInteger, 所不同的是 BigDecimal 加入了小数位的概念
|
||||||
|
|
||||||
|
## 1.4. 基本数据类型与包装数据类型的使用标准
|
||||||
|
|
||||||
|
Reference:《阿里巴巴Java开发手册》
|
||||||
|
|
||||||
|
- 【强制】所有的 POJO 类属性必须使用包装数据类型。
|
||||||
|
- 【强制】RPC 方法的返回值和参数必须使用包装数据类型。
|
||||||
|
- 【推荐】所有的局部变量使用基本数据类型。
|
||||||
|
|
||||||
|
比如我们如果自定义了一个Student类,其中有一个属性是成绩score,如果用Integer而不用int定义,一次考试,学生可能没考,值是null,也可能考了,但考了0分,值是0,这两个表达的状态明显不一样.
|
||||||
|
|
||||||
|
**说明** :POJO 类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何 NPE 问题,或者入库检查,都由使用者来保证。
|
||||||
|
|
||||||
|
**正例** : 数据库的查询结果可能是 null,因为自动拆箱,用基本数据类型接收有 NPE 风险。
|
||||||
|
|
||||||
|
**反例** : 比如显示成交总额涨跌情况,即正负 x%,x 为基本数据类型,调用的 RPC 服务,调用不成功时,返回的是默认值,页面显示为 0%,这是不合理的,应该显示成中划线。所以包装数据类型的 null 值,能够表示额外的信息,如:远程调用失败,异常退出。
|
||||||
|
|
||||||
|
# 2. 集合
|
||||||
|
|
||||||
|
## 2.1. Arrays.asList()使用指南
|
||||||
|
|
||||||
|
最近使用`Arrays.asList()`遇到了一些坑,然后在网上看到这篇文章:[Java Array to List Examples](http://javadevnotes.com/java-array-to-list-examples) 感觉挺不错的,但是还不是特别全面。所以,自己对于这块小知识点进行了简单的总结。
|
||||||
|
|
||||||
|
### 2.1.1. 简介
|
||||||
|
|
||||||
|
`Arrays.asList()`在平时开发中还是比较常见的,我们可以使用它将一个数组转换为一个List集合。
|
||||||
|
|
||||||
|
```java
|
||||||
|
String[] myArray = { "Apple", "Banana", "Orange" };
|
||||||
|
List<String> myList = Arrays.asList(myArray);
|
||||||
|
//上面两个语句等价于下面一条语句
|
||||||
|
List<String> myList = Arrays.asList("Apple","Banana", "Orange");
|
||||||
|
```
|
||||||
|
|
||||||
|
JDK 源码对于这个方法的说明:
|
||||||
|
|
||||||
|
```java
|
||||||
|
/**
|
||||||
|
*返回由指定数组支持的固定大小的列表。此方法作为基于数组和基于集合的API之间的桥梁,与 Collection.toArray()结合使用。返回的List是可序列化并实现RandomAccess接口。
|
||||||
|
*/
|
||||||
|
public static <T> List<T> asList(T... a) {
|
||||||
|
return new ArrayList<>(a);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.1.2. 《阿里巴巴Java 开发手册》对其的描述
|
||||||
|
|
||||||
|
`Arrays.asList()`将数组转换为集合后,底层其实还是数组,《阿里巴巴Java 开发手册》对于这个方法有如下描述:
|
||||||
|
|
||||||
|
方法.png)
|
||||||
|
|
||||||
|
### 2.1.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<E> extends AbstractList<E>
|
||||||
|
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<? super E> action) {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void replaceAll(UnaryOperator<E> operator) {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sort(Comparator<? super E> c) {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
我们再看一下`java.util.AbstractList`的`remove()`方法,这样我们就明白为啥会抛出`UnsupportedOperationException`。
|
||||||
|
|
||||||
|
```java
|
||||||
|
public E remove(int index) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.1.4. 如何正确的将数组转换为ArrayList?
|
||||||
|
|
||||||
|
stackoverflow:https://dwz.cn/vcBkTiTW
|
||||||
|
|
||||||
|
**1. 自己动手实现(教育目的)**
|
||||||
|
|
||||||
|
```java
|
||||||
|
//JDK1.5+
|
||||||
|
static <T> List<T> arrayToList(final T[] array) {
|
||||||
|
final List<T> l = new ArrayList<T>(array.length);
|
||||||
|
|
||||||
|
for (final T s : array) {
|
||||||
|
l.add(s);
|
||||||
|
}
|
||||||
|
return (l);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```java
|
||||||
|
Integer [] myArray = { 1, 2, 3 };
|
||||||
|
System.out.println(arrayToList(myArray).getClass());//class java.util.ArrayList
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. 最简便的方法(推荐)**
|
||||||
|
|
||||||
|
```java
|
||||||
|
List list = new ArrayList<>(Arrays.asList("a", "b", "c"))
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. 使用 Java8 的Stream(推荐)**
|
||||||
|
|
||||||
|
```java
|
||||||
|
Integer [] myArray = { 1, 2, 3 };
|
||||||
|
List myList = Arrays.stream(myArray).collect(Collectors.toList());
|
||||||
|
//基本类型也可以实现转换(依赖boxed的装箱操作)
|
||||||
|
int [] myArray2 = { 1, 2, 3 };
|
||||||
|
List myList = Arrays.stream(myArray2).boxed().collect(Collectors.toList());
|
||||||
|
```
|
||||||
|
|
||||||
|
**4. 使用 Guava(推荐)**
|
||||||
|
|
||||||
|
对于不可变集合,你可以使用[`ImmutableList`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/ImmutableList.java)类及其[`of()`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/ImmutableList.java#L101)与[`copyOf()`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/ImmutableList.java#L225)工厂方法:(参数不能为空)
|
||||||
|
|
||||||
|
```java
|
||||||
|
List<String> il = ImmutableList.of("string", "elements"); // from varargs
|
||||||
|
List<String> il = ImmutableList.copyOf(aStringArray); // from array
|
||||||
|
```
|
||||||
|
对于可变集合,你可以使用[`Lists`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/Lists.java)类及其[`newArrayList()`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/Lists.java#L87)工厂方法:
|
||||||
|
|
||||||
|
```java
|
||||||
|
List<String> l1 = Lists.newArrayList(anotherListOrCollection); // from collection
|
||||||
|
List<String> l2 = Lists.newArrayList(aStringArray); // from array
|
||||||
|
List<String> l3 = Lists.newArrayList("or", "string", "elements"); // from varargs
|
||||||
|
```
|
||||||
|
|
||||||
|
**5. 使用 Apache Commons Collections**
|
||||||
|
|
||||||
|
```java
|
||||||
|
List<String> list = new ArrayList<String>();
|
||||||
|
CollectionUtils.addAll(list, str);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2.2. Collection.toArray()方法使用的坑&如何反转数组
|
||||||
|
|
||||||
|
该方法是一个泛型方法:`<T> T[] toArray(T[] a);` 如果`toArray`方法中没有传递任何参数的话返回的是`Object`类型数组。
|
||||||
|
|
||||||
|
```java
|
||||||
|
String [] s= new String[]{
|
||||||
|
"dog", "lazy", "a", "over", "jumps", "fox", "brown", "quick", "A"
|
||||||
|
};
|
||||||
|
List<String> list = Arrays.asList(s);
|
||||||
|
Collections.reverse(list);
|
||||||
|
s=list.toArray(new String[0]);//没有指定类型的话会报错
|
||||||
|
```
|
||||||
|
|
||||||
|
由于JVM优化,`new String[0]`作为`Collection.toArray()`方法的参数现在使用更好,`new String[0]`就是起一个模板的作用,指定了返回数组的类型,0是为了节省空间,因为它只是为了说明返回的类型。详见:<https://shipilev.net/blog/2016/arrays-wisdom-ancients/>
|
||||||
|
|
||||||
|
## 2.3. 不要在 foreach 循环里进行元素的 remove/add 操作
|
||||||
|
|
||||||
|
如果要进行`remove`操作,可以调用迭代器的 `remove `方法而不是集合类的 remove 方法。因为如果列表在任何时间从结构上修改创建迭代器之后,以任何方式除非通过迭代器自身`remove/add`方法,迭代器都将抛出一个`ConcurrentModificationException`,这就是单线程状态下产生的 **fail-fast 机制**。
|
||||||
|
|
||||||
|
> **fail-fast 机制** :多个线程对 fail-fast 集合进行修改的时,可能会抛出ConcurrentModificationException,单线程下也会出现这种情况,上面已经提到过。
|
||||||
|
|
||||||
|
`java.util`包下面的所有的集合类都是fail-fast的,而`java.util.concurrent`包下面的所有的类都是fail-safe的。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,6 +1,30 @@
|
|||||||
|
讲真的,下面推荐的文章或者资源建议阅读 3 遍以上。
|
||||||
|
|
||||||
|
### 团队
|
||||||
|
|
||||||
根据各位建议加上了这部分内容,我暂时只是给出了两个资源,后续可能会对重要的点进行总结,然后更新在这里,如果你总结过这类东西,欢迎与我联系!
|
- **阿里巴巴Java开发手册(详尽版)** <https://github.com/alibaba/p3c/blob/master/阿里巴巴Java开发手册(华山版).pdf>
|
||||||
|
- **Google Java编程风格指南:** <http://hawstein.com/2014/01/20/google-java-style/>
|
||||||
|
|
||||||
- **阿里巴巴Java开发手册(详尽版)** <https://github.com/alibaba/p3c/blob/master/阿里巴巴Java开发手册(详尽版).pdf>
|
### 个人
|
||||||
- **Google Java编程风格指南:** <http://www.hawstein.com/posts/google-java-style.html>
|
|
||||||
|
- **程序员你为什么这么累:** <https://xwjie.github.io/rule/>
|
||||||
|
|
||||||
|
### 如何写出优雅的 Java 代码
|
||||||
|
|
||||||
|
1. 使用 IntelliJ IDEA 作为您的集成开发环境 (IDE)
|
||||||
|
1. 使用 JDK 8 或更高版本
|
||||||
|
1. 使用 Maven/Gradle
|
||||||
|
1. 使用 Lombok
|
||||||
|
1. 编写单元测试
|
||||||
|
1. 重构:常见,但也很慢
|
||||||
|
1. 注意代码规范
|
||||||
|
1. 定期联络客户,以获取他们的反馈
|
||||||
|
|
||||||
|
上述建议的详细内容:[八点建议助您写出优雅的Java代码](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485140&idx=1&sn=ecaeace613474f1859aaeed0282ae680&chksm=cea2491ff9d5c00982ffaece847ce1aead89fdb3fe190752d9837c075c79fc95db5940992c56&token=1328169465&lang=zh_CN&scene=21#wechat_redirect)。
|
||||||
|
|
||||||
|
更多代码优化相关内容推荐:
|
||||||
|
|
||||||
|
- [业务复杂=if else?刚来的大神竟然用策略+工厂彻底干掉了他们!](https://juejin.im/post/5dad23685188251d2c4ea2b6)
|
||||||
|
- [一些不错的 Java 实践!推荐阅读3遍以上!](http://lrwinx.github.io/2017/03/04/%E7%BB%86%E6%80%9D%E6%9E%81%E6%81%90-%E4%BD%A0%E7%9C%9F%E7%9A%84%E4%BC%9A%E5%86%99java%E5%90%97/)
|
||||||
|
- [[解锁新姿势] 兄dei,你代码需要优化了](https://juejin.im/post/5dafbc02e51d4524a0060bdd)
|
||||||
|
- [消灭 Java 代码的“坏味道”](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485599&idx=1&sn=d83ff4e6b1ee951a0a33508a10980ea3&chksm=cea24754f9d5ce426d18b435a8c373ddc580c06c7d6a45cc51377361729c31c7301f1bbc3b78&token=1328169465&lang=zh_CN#rd)
|
@ -1,59 +0,0 @@
|
|||||||
|
|
||||||
下面是按jvm虚拟机知识点分章节总结的一些jvm学习与面试相关的一些东西。一般作为Java程序员在面试的时候一般会问的大多就是**Java内存区域、虚拟机垃圾算法、虚拟垃圾收集器、JVM内存管理**这些问题了。这些内容参考周的《深入理解Java虚拟机》中第二章和第三章就足够了对应下面的[深入理解虚拟机之Java内存区域:](https://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s%3F__biz%3DMzU4NDQ4MzU5OA%3D%3D%26mid%3D2247483910%26idx%3D1%26sn%3D246f39051a85fc312577499691fba89f%26chksm%3Dfd985467caefdd71f9a7c275952be34484b14f9e092723c19bd4ef557c324169ed084f868bdb%23rd)和[深入理解虚拟机之垃圾回收](https://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s%3F__biz%3DMzU4NDQ4MzU5OA%3D%3D%26mid%3D2247483914%26idx%3D1%26sn%3D9aa157d4a1570962c39783cdeec7e539%26chksm%3Dfd98546bcaefdd7d9f61cd356e5584e56b64e234c3a403ed93cb6d4dde07a505e3000fd0c427%23rd)这两篇文章。
|
|
||||||
|
|
||||||
|
|
||||||
> ### 常见面试题
|
|
||||||
|
|
||||||
[深入理解虚拟机之Java内存区域](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247484960&idx=1&sn=ff3739fe849030178346bef28a4556c3&chksm=cea249ebf9d5c0fdbde7c86155d0d7ac8925153742aff472bcb79e5e9d400534a855bad38375&token=1082669959&lang=zh_CN#rd)
|
|
||||||
|
|
||||||
1. 介绍下Java内存区域(运行时数据区)。
|
|
||||||
|
|
||||||
2. 对象的访问定位的两种方式。
|
|
||||||
|
|
||||||
|
|
||||||
[深入理解虚拟机之垃圾回收](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247484959&idx=1&sn=9ac740edba59981b7c89482043776280&chksm=cea249d4f9d5c0c21703382510a47d4bb387932bd814ac891fd214b92cead5d2cf0ee2dff797&token=1082669959&lang=zh_CN#rd)
|
|
||||||
|
|
||||||
1. 如何判断对象是否死亡(两种方法)。
|
|
||||||
|
|
||||||
2. 简单的介绍一下强引用、软引用、弱引用、虚引用(虚引用与软引用和弱引用的区别、使用软引用能带来的好处)。
|
|
||||||
|
|
||||||
3. 垃圾收集有哪些算法,各自的特点?
|
|
||||||
|
|
||||||
4. HotSpot为什么要分为新生代和老年代?
|
|
||||||
|
|
||||||
5. 常见的垃圾回收器有那些?
|
|
||||||
|
|
||||||
6. 介绍一下CMS,G1收集器。
|
|
||||||
|
|
||||||
7. Minor Gc和Full GC 有什么不同呢?
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[虚拟机性能监控和故障处理工具](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247484957&idx=1&sn=713ed6003d23ef883ded14cb43e9ebb7&chksm=cea249d6f9d5c0c0ce0854a03f0d02fcacc8a46e29c2fd4f085a375b00e1cd1b632937a9895e&token=1082669959&lang=zh_CN#rd)
|
|
||||||
|
|
||||||
1. JVM调优的常见命令行工具有哪些?
|
|
||||||
|
|
||||||
[深入理解虚拟机之类文件结构](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247484956&idx=1&sn=05f46ccacacdbce7c43de594d3fe93db&chksm=cea249d7f9d5c0c1ef6d29b0fbbf0701acd28490deb0974ae71b4d23ae793bec0b0993a4c829&token=1082669959&lang=zh_CN#rd)
|
|
||||||
|
|
||||||
1. 简单介绍一下Class类文件结构(常量池主要存放的是那两大常量?Class文件的继承关系是如何确定的?字段表、方法表、属性表主要包含那些信息?)
|
|
||||||
|
|
||||||
[深入理解虚拟机之虚拟机字节码执行引擎](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247484952&idx=1&sn=d0ec9443600dc5b2a81782b7ae0691d5&chksm=cea249d3f9d5c0c50642f1829fd6fe9e35d155bbbb6718611330c7c46c7158279275b533181e&token=1082669959&lang=zh_CN#rd)
|
|
||||||
|
|
||||||
1. 简单说说类加载过程,里面执行了哪些操作?
|
|
||||||
|
|
||||||
2. 对类加载器有了解吗?
|
|
||||||
|
|
||||||
3. 什么是双亲委派模型?
|
|
||||||
|
|
||||||
4. 双亲委派模型的工作过程以及使用它的好处。
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
> ### 推荐阅读
|
|
||||||
|
|
||||||
[《深入理解 Java 内存模型》读书笔记](http://www.54tianzhisheng.cn/2018/02/28/Java-Memory-Model/) (非常不错的文章)
|
|
||||||
[全面理解Java内存模型(JMM)及volatile关键字 ](https://blog.csdn.net/javazejian/article/details/72772461)
|
|
||||||
|
|
||||||
|
|
@ -1,353 +0,0 @@
|
|||||||
<!-- MarkdownTOC -->
|
|
||||||
|
|
||||||
1. [List,Set,Map三者的区别及总结](#list,setmap三者的区别及总结)
|
|
||||||
1. [Arraylist 与 LinkedList 区别](#arraylist-与-linkedlist-区别)
|
|
||||||
1. [ArrayList 与 Vector 区别(为什么要用Arraylist取代Vector呢?)](#arraylist-与-vector-区别)
|
|
||||||
1. [HashMap 和 Hashtable 的区别](#hashmap-和-hashtable-的区别)
|
|
||||||
1. [HashSet 和 HashMap 区别](#hashset-和-hashmap-区别)
|
|
||||||
1. [HashMap 和 ConcurrentHashMap 的区别](#hashmap-和-concurrenthashmap-的区别)
|
|
||||||
1. [HashSet如何检查重复](#hashset如何检查重复)
|
|
||||||
1. [comparable 和 comparator的区别](#comparable-和-comparator的区别)
|
|
||||||
1. [Comparator定制排序](#comparator定制排序)
|
|
||||||
1. [重写compareTo方法实现按年龄来排序](#重写compareto方法实现按年龄来排序)
|
|
||||||
1. [如何对Object的list排序?](#如何对object的list排序)
|
|
||||||
1. [如何实现数组与List的相互转换?](#如何实现数组与list的相互转换)
|
|
||||||
1. [如何求ArrayList集合的交集 并集 差集 去重复并集](#如何求arraylist集合的交集-并集-差集-去重复并集)
|
|
||||||
1. [HashMap 的工作原理及代码实现](#hashmap-的工作原理及代码实现)
|
|
||||||
1. [ConcurrentHashMap 的工作原理及代码实现](#concurrenthashmap-的工作原理及代码实现)
|
|
||||||
1. [集合框架底层数据结构总结](#集合框架底层数据结构总结)
|
|
||||||
1. [- Collection](#--collection)
|
|
||||||
1. [1. List](#1-list)
|
|
||||||
1. [2. Set](#2-set)
|
|
||||||
1. [- Map](#--map)
|
|
||||||
1. [集合的选用](#集合的选用)
|
|
||||||
1. [集合的常用方法](#集合的常用方法)
|
|
||||||
|
|
||||||
<!-- /MarkdownTOC -->
|
|
||||||
|
|
||||||
|
|
||||||
## <font face="楷体">List,Set,Map三者的区别及总结</font>
|
|
||||||
- **List:对付顺序的好帮手**
|
|
||||||
|
|
||||||
List接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象
|
|
||||||
- **Set:注重独一无二的性质**
|
|
||||||
|
|
||||||
不允许重复的集合。不会有多个元素引用相同的对象。
|
|
||||||
|
|
||||||
- **Map:用Key来搜索的专家**
|
|
||||||
|
|
||||||
使用键值对存储。Map会维护与Key有关联的值。两个Key可以引用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何对象。
|
|
||||||
|
|
||||||
|
|
||||||
## <font face="楷体">Arraylist 与 LinkedList 区别</font>
|
|
||||||
Arraylist底层使用的是数组(存读数据效率高,插入删除特定位置效率低),LinkedList 底层使用的是双向链表数据结构(插入,删除效率特别高)(JDK1.6之前为循环链表,JDK1.7取消了循环。注意双向链表和双向循环链表的区别:); 详细可阅读JDK1.7-LinkedList循环链表优化。学过数据结构这门课后我们就知道采用链表存储,插入,删除元素时间复杂度不受元素位置的影响,都是近似O(1)而数组为近似O(n),因此当数据特别多,而且经常需要插入删除元素时建议选用LinkedList.一般程序只用Arraylist就够用了,因为一般数据量都不会蛮大,Arraylist是使用最多的集合类。
|
|
||||||
|
|
||||||
## <font face="楷体">ArrayList 与 Vector 区别</font>
|
|
||||||
Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector
|
|
||||||
,代码要在同步操作上耗费大量的时间。Arraylist不是同步的,所以在不需要同步时建议使用Arraylist。
|
|
||||||
|
|
||||||
## <font face="楷体">HashMap 和 Hashtable 的区别</font>
|
|
||||||
1. HashMap是非线程安全的,HashTable是线程安全的;HashTable内部的方法基本都经过synchronized修饰。
|
|
||||||
|
|
||||||
2. 因为线程安全的问题,HashMap要比HashTable效率高一点,HashTable基本被淘汰。
|
|
||||||
3. HashMap允许有null值的存在,而在HashTable中put进的键值只要有一个null,直接抛出NullPointerException。
|
|
||||||
|
|
||||||
Hashtable和HashMap有几个主要的不同:线程安全以及速度。仅在你需要完全的线程安全的时候使用Hashtable,而如果你使用Java5或以上的话,请使用ConcurrentHashMap吧
|
|
||||||
|
|
||||||
## <font face="楷体">HashSet 和 HashMap 区别</font>
|
|
||||||

|
|
||||||
|
|
||||||
## <font face="楷体">HashMap 和 ConcurrentHashMap 的区别</font>
|
|
||||||
[HashMap与ConcurrentHashMap的区别](https://blog.csdn.net/xuefeng0707/article/details/40834595)
|
|
||||||
|
|
||||||
1. ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的synchronized锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。(JDK1.8之后ConcurrentHashMap启用了一种全新的方式实现,利用CAS算法。)
|
|
||||||
2. HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。
|
|
||||||
|
|
||||||
## <font face="楷体">HashSet如何检查重复</font>
|
|
||||||
当你把对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同hashcode值的对象,这时会调用equals()方法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让加入操作成功。(摘自我的Java启蒙书《Head fist java》第二版)
|
|
||||||
|
|
||||||
**hashCode()与equals()的相关规定:**
|
|
||||||
1. 如果两个对象相等,则hashcode一定也是相同的
|
|
||||||
2. 两个对象相等,对两个equals方法返回true
|
|
||||||
3. 两个对象有相同的hashcode值,它们也不一定是相等的
|
|
||||||
4. 综上,equals方法被覆盖过,则hashCode方法也必须被覆盖
|
|
||||||
5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。
|
|
||||||
|
|
||||||
**==与equals的区别**
|
|
||||||
|
|
||||||
1. ==是判断两个变量或实例是不是指向同一个内存空间 equals是判断两个变量或实例所指向的内存空间的值是不是相同
|
|
||||||
2. ==是指对内存地址进行比较 equals()是对字符串的内容进行比较
|
|
||||||
3. ==指引用是否相同 equals()指的是值是否相同
|
|
||||||
|
|
||||||
## <font face="楷体">comparable 和 comparator的区别</font>
|
|
||||||
- comparable接口实际上是出自java.lang包 它有一个 compareTo(Object obj)方法用来排序
|
|
||||||
- comparator接口实际上是出自 java.util 包它有一个compare(Object obj1, Object obj2)方法用来排序
|
|
||||||
|
|
||||||
一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo方法或compare方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的Collections.sort().
|
|
||||||
|
|
||||||
### <font face="楷体">Comparator定制排序<font face="楷体">
|
|
||||||
```java
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO Collections类方法测试之排序
|
|
||||||
* @author 寇爽
|
|
||||||
* @date 2017年11月20日
|
|
||||||
* @version 1.8
|
|
||||||
*/
|
|
||||||
public class CollectionsSort {
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
|
||||||
|
|
||||||
ArrayList<Integer> arrayList = new ArrayList<Integer>();
|
|
||||||
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);
|
|
||||||
/*
|
|
||||||
* void rotate(List list, int distance),旋转。
|
|
||||||
* 当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将
|
|
||||||
* list的前distance个元素整体移到后面。
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
// 定制排序的用法
|
|
||||||
Collections.sort(arrayList, new Comparator<Integer>() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int compare(Integer o1, Integer o2) {
|
|
||||||
return o2.compareTo(o1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
System.out.println("定制排序后:");
|
|
||||||
System.out.println(arrayList);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
### <font face="楷体">重写compareTo方法实现按年龄来排序</font>
|
|
||||||
```java
|
|
||||||
package map;
|
|
||||||
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.TreeMap;
|
|
||||||
|
|
||||||
public class TreeMap2 {
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
|
||||||
// TODO Auto-generated method stub
|
|
||||||
TreeMap<Person, String> pdata = new TreeMap<Person, String>();
|
|
||||||
pdata.put(new Person("张三", 30), "zhangsan");
|
|
||||||
pdata.put(new Person("李四", 20), "lisi");
|
|
||||||
pdata.put(new Person("王五", 10), "wangwu");
|
|
||||||
pdata.put(new Person("小红", 5), "xiaohong");
|
|
||||||
// 得到key的值的同时得到key所对应的值
|
|
||||||
Set<Person> keys = pdata.keySet();
|
|
||||||
for (Person key : keys) {
|
|
||||||
System.out.println(key.getAge() + "-" + key.getName());
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// person对象没有实现Comparable接口,所以必须实现,这样才不会出错,才可以使treemap中的数据按顺序排列
|
|
||||||
// 前面一个例子的String类已经默认实现了Comparable接口,详细可以查看String类的API文档,另外其他
|
|
||||||
// 像Integer类等都已经实现了Comparable接口,所以不需要另外实现了
|
|
||||||
|
|
||||||
class Person implements Comparable<Person> {
|
|
||||||
private String name;
|
|
||||||
private int age;
|
|
||||||
|
|
||||||
public Person(String name, int age) {
|
|
||||||
super();
|
|
||||||
this.name = name;
|
|
||||||
this.age = age;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setName(String name) {
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getAge() {
|
|
||||||
return age;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAge(int age) {
|
|
||||||
this.age = age;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO重写compareTo方法实现按年龄来排序
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public int compareTo(Person o) {
|
|
||||||
// TODO Auto-generated method stub
|
|
||||||
if (this.age > o.getAge()) {
|
|
||||||
return 1;
|
|
||||||
} else if (this.age < o.getAge()) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return age;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## <font face="楷体">如何对Object的list排序</font>
|
|
||||||
- 对objects数组进行排序,我们可以用Arrays.sort()方法
|
|
||||||
- 对objects的集合进行排序,需要使用Collections.sort()方法
|
|
||||||
|
|
||||||
|
|
||||||
## <font face="楷体">如何实现数组与List的相互转换</font>
|
|
||||||
List转数组:toArray(arraylist.size()方法;数组转List:Arrays的asList(a)方法
|
|
||||||
```java
|
|
||||||
List<String> arrayList = new ArrayList<String>();
|
|
||||||
arrayList.add("s");
|
|
||||||
arrayList.add("e");
|
|
||||||
arrayList.add("n");
|
|
||||||
/**
|
|
||||||
* ArrayList转数组
|
|
||||||
*/
|
|
||||||
int size=arrayList.size();
|
|
||||||
String[] a = arrayList.toArray(new String[size]);
|
|
||||||
//输出第二个元素
|
|
||||||
System.out.println(a[1]);//结果:e
|
|
||||||
//输出整个数组
|
|
||||||
System.out.println(Arrays.toString(a));//结果:[s, e, n]
|
|
||||||
/**
|
|
||||||
* 数组转list
|
|
||||||
*/
|
|
||||||
List<String> list=Arrays.asList(a);
|
|
||||||
/**
|
|
||||||
* list转Arraylist
|
|
||||||
*/
|
|
||||||
List<String> arrayList2 = new ArrayList<String>();
|
|
||||||
arrayList2.addAll(list);
|
|
||||||
System.out.println(list);
|
|
||||||
```
|
|
||||||
## <font face="楷体">如何求ArrayList集合的交集 并集 差集 去重复并集</font>
|
|
||||||
需要用到List接口中定义的几个方法:
|
|
||||||
|
|
||||||
- addAll(Collection<? extends E> c) :按指定集合的Iterator返回的顺序将指定集合中的所有元素追加到此列表的末尾
|
|
||||||
实例代码:
|
|
||||||
- retainAll(Collection<?> c): 仅保留此列表中包含在指定集合中的元素。
|
|
||||||
- removeAll(Collection<?> c) :从此列表中删除指定集合中包含的所有元素。
|
|
||||||
```java
|
|
||||||
package list;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*TODO 两个集合之间求交集 并集 差集 去重复并集
|
|
||||||
* @author 寇爽
|
|
||||||
* @date 2017年11月21日
|
|
||||||
* @version 1.8
|
|
||||||
*/
|
|
||||||
public class MethodDemo {
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
|
||||||
// TODO Auto-generated method stub
|
|
||||||
List<Integer> list1 = new ArrayList<Integer>();
|
|
||||||
list1.add(1);
|
|
||||||
list1.add(2);
|
|
||||||
list1.add(3);
|
|
||||||
list1.add(4);
|
|
||||||
|
|
||||||
List<Integer> list2 = new ArrayList<Integer>();
|
|
||||||
list2.add(2);
|
|
||||||
list2.add(3);
|
|
||||||
list2.add(4);
|
|
||||||
list2.add(5);
|
|
||||||
// 并集
|
|
||||||
// list1.addAll(list2);
|
|
||||||
// 交集
|
|
||||||
//list1.retainAll(list2);
|
|
||||||
// 差集
|
|
||||||
// list1.removeAll(list2);
|
|
||||||
// 无重复并集
|
|
||||||
list2.removeAll(list1);
|
|
||||||
list1.addAll(list2);
|
|
||||||
for (Integer i : list1) {
|
|
||||||
System.out.println(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## <font face="楷体">HashMap 的工作原理及代码实现</font>
|
|
||||||
|
|
||||||
[集合框架源码学习之HashMap(JDK1.8)](https://juejin.im/post/5ab0568b5188255580020e56)
|
|
||||||
|
|
||||||
## <font face="楷体">ConcurrentHashMap 的工作原理及代码实现</font>
|
|
||||||
|
|
||||||
[ConcurrentHashMap实现原理及源码分析](http://www.cnblogs.com/chengxiao/p/6842045.html)
|
|
||||||
|
|
||||||
|
|
||||||
## <font face="楷体">集合框架底层数据结构总结</font>
|
|
||||||
### - Collection
|
|
||||||
|
|
||||||
#### 1. List
|
|
||||||
- Arraylist:数组(查询快,增删慢 线程不安全,效率高 )
|
|
||||||
- Vector:数组(查询快,增删慢 线程安全,效率低 )
|
|
||||||
- LinkedList:链表(查询慢,增删快 线程不安全,效率高 )
|
|
||||||
|
|
||||||
#### 2. Set
|
|
||||||
- HashSet(无序,唯一):哈希表或者叫散列集(hash table)
|
|
||||||
- LinkedHashSet:链表和哈希表组成 。 由链表保证元素的排序 , 由哈希表证元素的唯一性
|
|
||||||
- TreeSet(有序,唯一):红黑树(自平衡的排序二叉树。)
|
|
||||||
|
|
||||||
### - Map
|
|
||||||
- HashMap:基于哈希表的Map接口实现(哈希表对键进行散列,Map结构即映射表存放键值对)
|
|
||||||
- LinkedHashMap:HashMap 的基础上加上了链表数据结构
|
|
||||||
- HashTable:哈希表
|
|
||||||
- TreeMap:红黑树(自平衡的排序二叉树)
|
|
||||||
|
|
||||||
|
|
||||||
## <font face="楷体">集合的选用</font>
|
|
||||||
主要根据集合的特点来选用,比如我们需要根据键值获取到元素值时就选用Map接口下的集合,需要排序时选择TreeMap,不需要排序时就选择HashMap,需要保证线程安全就选用ConcurrentHashMap.当我们只需要存放元素值时,就选择实现Collection接口的集合,需要保证元素唯一时选择实现Set接口的集合比如TreeSet或HashSet,不需要就选择实现List接口的比如ArrayList或LinkedList,然后再根据实现这些接口的集合的特点来选用。
|
|
||||||
|
|
||||||
2018/3/11更新
|
|
||||||
## <font face="楷体">集合的常用方法</font>
|
|
||||||
今天下午无意看见一道某大厂的面试题,面试题的内容就是问你某一个集合常见的方法有哪些。虽然平时也经常见到这些集合,但是猛一下让我想某一个集合的常用的方法难免会有遗漏或者与其他集合搞混,所以建议大家还是照着API文档把常见的那几个集合的常用方法看一看。
|
|
||||||
|
|
||||||
会持续更新。。。
|
|
||||||
|
|
||||||
**参考书籍:**
|
|
||||||
|
|
||||||
《Head first java 》第二版 推荐阅读真心不错 (适合基础较差的)
|
|
||||||
|
|
||||||
《Java核心技术卷1》推荐阅读真心不错 (适合基础较好的)
|
|
||||||
|
|
||||||
《算法》第四版 (适合想对数据结构的Java实现感兴趣的)
|
|
||||||
|
|
@ -1,73 +1,70 @@
|
|||||||
|
点击关注[公众号](#公众号 "公众号")及时获取笔主最新更新文章,并可免费领取本文档配套的《Java 面试突击》以及 Java 工程师必备学习资源。
|
||||||
|
|
||||||
**目录:**
|
<!-- TOC -->
|
||||||
<!-- MarkdownTOC -->
|
|
||||||
|
|
||||||
- [1 AQS 简单介绍](#1-aqs-简单介绍)
|
- [1 AQS 简单介绍](#1-aqs-简单介绍)
|
||||||
- [2 AQS 原理](#2-aqs-原理)
|
- [2 AQS 原理](#2-aqs-原理)
|
||||||
- [2.1 AQS 原理概览](#21-aqs-原理概览)
|
- [2.1 AQS 原理概览](#21-aqs-原理概览)
|
||||||
- [2.2 AQS 对资源的共享方式](#22-aqs-对资源的共享方式)
|
- [2.2 AQS 对资源的共享方式](#22-aqs-对资源的共享方式)
|
||||||
- [2.3 AQS底层使用了模板方法模式](#23-aqs底层使用了模板方法模式)
|
- [2.3 AQS 底层使用了模板方法模式](#23-aqs-底层使用了模板方法模式)
|
||||||
- [3 Semaphore\(信号量\)-允许多个线程同时访问](#3-semaphore信号量-允许多个线程同时访问)
|
- [3 Semaphore(信号量)-允许多个线程同时访问](#3-semaphore信号量-允许多个线程同时访问)
|
||||||
- [4 CountDownLatch (倒计时器)](#4-countdownlatch-倒计时器)
|
- [4 CountDownLatch (倒计时器)](#4-countdownlatch-倒计时器)
|
||||||
- [4.1 CountDownLatch 的三种典型用法](#41-countdownlatch-的三种典型用法)
|
- [4.1 CountDownLatch 的三种典型用法](#41-countdownlatch-的三种典型用法)
|
||||||
- [4.2 CountDownLatch 的使用示例](#42-countdownlatch-的使用示例)
|
- [4.2 CountDownLatch 的使用示例](#42-countdownlatch-的使用示例)
|
||||||
- [4.3 CountDownLatch 的不足](#43-countdownlatch-的不足)
|
- [4.3 CountDownLatch 的不足](#43-countdownlatch-的不足)
|
||||||
- [4.4 CountDownLatch相常见面试题:](#44-countdownlatch相常见面试题)
|
- [4.4 CountDownLatch 常见面试题](#44-countdownlatch-相常见面试题)
|
||||||
- [5 CyclicBarrier\(循环栅栏\)](#5-cyclicbarrier循环栅栏)
|
- [5 CyclicBarrier(循环栅栏)](#5-cyclicbarrier循环栅栏)
|
||||||
- [5.1 CyclicBarrier 的应用场景](#51-cyclicbarrier-的应用场景)
|
- [5.1 CyclicBarrier 的应用场景](#51-cyclicbarrier-的应用场景)
|
||||||
- [5.2 CyclicBarrier 的使用示例](#52-cyclicbarrier-的使用示例)
|
- [5.2 CyclicBarrier 的使用示例](#52-cyclicbarrier-的使用示例)
|
||||||
- [5.3 CyclicBarrier和CountDownLatch的区别](#53-cyclicbarrier和countdownlatch的区别)
|
- [5.3 `CyclicBarrier`源码分析](#53-cyclicbarrier源码分析)
|
||||||
|
- [5.4 CyclicBarrier 和 CountDownLatch 的区别](#54-cyclicbarrier-和-countdownlatch-的区别)
|
||||||
- [6 ReentrantLock 和 ReentrantReadWriteLock](#6-reentrantlock-和-reentrantreadwritelock)
|
- [6 ReentrantLock 和 ReentrantReadWriteLock](#6-reentrantlock-和-reentrantreadwritelock)
|
||||||
|
- [参考](#参考)
|
||||||
|
- [公众号](#公众号)
|
||||||
|
|
||||||
<!-- /MarkdownTOC -->
|
<!-- /TOC -->
|
||||||
|
|
||||||
> 常见问题:AQS原理?;CountDownLatch和CyclicBarrier了解吗,两者的区别是什么?用过Semaphore吗?
|
|
||||||
|
|
||||||
**本节思维导图:**
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
> 常见问题:AQS 原理?;CountDownLatch 和 CyclicBarrier 了解吗,两者的区别是什么?用过 Semaphore 吗?
|
||||||
|
|
||||||
### 1 AQS 简单介绍
|
### 1 AQS 简单介绍
|
||||||
AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。
|
|
||||||
|
AQS 的全称为(AbstractQueuedSynchronizer),这个类在 java.util.concurrent.locks 包下面。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。
|
AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask(jdk1.7) 等等皆是基于 AQS 的。当然,我们自己也能利用 AQS 非常轻松容易地构造出符合我们自己需求的同步器。
|
||||||
|
|
||||||
### 2 AQS 原理
|
### 2 AQS 原理
|
||||||
|
|
||||||
> 在面试中被问到并发知识的时候,大多都会被问到“请你说一下自己对于AQS原理的理解”。下面给大家一个示例供大家参加,面试不是背题,大家一定要加入自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来。
|
> 在面试中被问到并发知识的时候,大多都会被问到“请你说一下自己对于 AQS 原理的理解”。下面给大家一个示例供大家参考,面试不是背题,大家一定要加入自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来。
|
||||||
|
|
||||||
下面大部分内容其实在AQS类注释上已经给出了,不过是英语看着比较吃力一点,感兴趣的话可以看看源码。
|
下面大部分内容其实在 AQS 类注释上已经给出了,不过是英语看着比较吃力一点,感兴趣的话可以看看源码。
|
||||||
|
|
||||||
#### 2.1 AQS 原理概览
|
#### 2.1 AQS 原理概览
|
||||||
|
|
||||||
**AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。**
|
**AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。**
|
||||||
|
|
||||||
> CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
|
> CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。
|
||||||
|
|
||||||
看个AQS(AbstractQueuedSynchronizer)原理图:
|
|
||||||
|
|
||||||
|
看个 AQS(AbstractQueuedSynchronizer)原理图:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。
|
AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。
|
||||||
|
|
||||||
```java
|
```java
|
||||||
private volatile int state;//共享变量,使用volatile修饰保证线程可见性
|
private volatile int state;//共享变量,使用volatile修饰保证线程可见性
|
||||||
```
|
```
|
||||||
|
|
||||||
状态信息通过protected类型的getState,setState,compareAndSetState进行操作
|
状态信息通过 protected 类型的`getState`,`setState`,`compareAndSetState`进行操作
|
||||||
|
|
||||||
```java
|
```java
|
||||||
|
|
||||||
//返回同步状态的当前值
|
//返回同步状态的当前值
|
||||||
protected final int getState() {
|
protected final int getState() {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
// 设置同步状态的值
|
// 设置同步状态的值
|
||||||
protected final void setState(int newState) {
|
protected final void setState(int newState) {
|
||||||
state = newState;
|
state = newState;
|
||||||
}
|
}
|
||||||
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
|
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
|
||||||
@ -78,29 +75,144 @@ protected final boolean compareAndSetState(int expect, int update) {
|
|||||||
|
|
||||||
#### 2.2 AQS 对资源的共享方式
|
#### 2.2 AQS 对资源的共享方式
|
||||||
|
|
||||||
**AQS定义两种资源共享方式**
|
**AQS 定义两种资源共享方式**
|
||||||
|
|
||||||
- **Exclusive**(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
|
**1)Exclusive**(独占)
|
||||||
- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
|
|
||||||
- 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
|
|
||||||
- **Share**(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。
|
|
||||||
|
|
||||||
ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。
|
只有一个线程能执行,如 ReentrantLock。又可分为公平锁和非公平锁,ReentrantLock 同时支持两种锁,下面以 ReentrantLock 对这两种锁的定义做介绍:
|
||||||
|
|
||||||
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在上层已经帮我们实现好了。
|
- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
|
||||||
|
- 非公平锁:当线程要获取锁时,先通过两次 CAS 操作去抢锁,如果没抢到,当前线程再加入到队列中等待唤醒。
|
||||||
|
|
||||||
#### 2.3 AQS底层使用了模板方法模式
|
> 说明:下面这部分关于 `ReentrantLock` 源代码内容节选自:https://www.javadoop.com/post/AbstractQueuedSynchronizer-2 ,这是一篇很不错文章,推荐阅读。
|
||||||
|
|
||||||
|
**下面来看 ReentrantLock 中相关的源代码:**
|
||||||
|
|
||||||
|
ReentrantLock 默认采用非公平锁,因为考虑获得更好的性能,通过 boolean 来决定是否用公平锁(传入 true 用公平锁)。
|
||||||
|
|
||||||
|
```java
|
||||||
|
/** Synchronizer providing all implementation mechanics */
|
||||||
|
private final Sync sync;
|
||||||
|
public ReentrantLock() {
|
||||||
|
// 默认非公平锁
|
||||||
|
sync = new NonfairSync();
|
||||||
|
}
|
||||||
|
public ReentrantLock(boolean fair) {
|
||||||
|
sync = fair ? new FairSync() : new NonfairSync();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
ReentrantLock 中公平锁的 `lock` 方法
|
||||||
|
|
||||||
|
```java
|
||||||
|
static final class FairSync extends Sync {
|
||||||
|
final void lock() {
|
||||||
|
acquire(1);
|
||||||
|
}
|
||||||
|
// AbstractQueuedSynchronizer.acquire(int arg)
|
||||||
|
public final void acquire(int arg) {
|
||||||
|
if (!tryAcquire(arg) &&
|
||||||
|
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
|
||||||
|
selfInterrupt();
|
||||||
|
}
|
||||||
|
protected final boolean tryAcquire(int acquires) {
|
||||||
|
final Thread current = Thread.currentThread();
|
||||||
|
int c = getState();
|
||||||
|
if (c == 0) {
|
||||||
|
// 1. 和非公平锁相比,这里多了一个判断:是否有线程在等待
|
||||||
|
if (!hasQueuedPredecessors() &&
|
||||||
|
compareAndSetState(0, acquires)) {
|
||||||
|
setExclusiveOwnerThread(current);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (current == getExclusiveOwnerThread()) {
|
||||||
|
int nextc = c + acquires;
|
||||||
|
if (nextc < 0)
|
||||||
|
throw new Error("Maximum lock count exceeded");
|
||||||
|
setState(nextc);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
非公平锁的 lock 方法:
|
||||||
|
|
||||||
|
```java
|
||||||
|
static final class NonfairSync extends Sync {
|
||||||
|
final void lock() {
|
||||||
|
// 2. 和公平锁相比,这里会直接先进行一次CAS,成功就返回了
|
||||||
|
if (compareAndSetState(0, 1))
|
||||||
|
setExclusiveOwnerThread(Thread.currentThread());
|
||||||
|
else
|
||||||
|
acquire(1);
|
||||||
|
}
|
||||||
|
// AbstractQueuedSynchronizer.acquire(int arg)
|
||||||
|
public final void acquire(int arg) {
|
||||||
|
if (!tryAcquire(arg) &&
|
||||||
|
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
|
||||||
|
selfInterrupt();
|
||||||
|
}
|
||||||
|
protected final boolean tryAcquire(int acquires) {
|
||||||
|
return nonfairTryAcquire(acquires);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Performs non-fair tryLock. tryAcquire is implemented in
|
||||||
|
* subclasses, but both need nonfair try for trylock method.
|
||||||
|
*/
|
||||||
|
final boolean nonfairTryAcquire(int acquires) {
|
||||||
|
final Thread current = Thread.currentThread();
|
||||||
|
int c = getState();
|
||||||
|
if (c == 0) {
|
||||||
|
// 这里没有对阻塞队列进行判断
|
||||||
|
if (compareAndSetState(0, acquires)) {
|
||||||
|
setExclusiveOwnerThread(current);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (current == getExclusiveOwnerThread()) {
|
||||||
|
int nextc = c + acquires;
|
||||||
|
if (nextc < 0) // overflow
|
||||||
|
throw new Error("Maximum lock count exceeded");
|
||||||
|
setState(nextc);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
总结:公平锁和非公平锁只有两处不同:
|
||||||
|
|
||||||
|
1. 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
|
||||||
|
2. 非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。
|
||||||
|
|
||||||
|
公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。
|
||||||
|
|
||||||
|
相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。
|
||||||
|
|
||||||
|
**2)Share**(共享)
|
||||||
|
|
||||||
|
多个线程可同时执行,如 Semaphore/CountDownLatch。Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。
|
||||||
|
|
||||||
|
ReentrantReadWriteLock 可以看成是组合式,因为 ReentrantReadWriteLock 也就是读写锁允许多个线程同时对某一资源进行读。
|
||||||
|
|
||||||
|
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS 已经在上层已经帮我们实现好了。
|
||||||
|
|
||||||
|
#### 2.3 AQS 底层使用了模板方法模式
|
||||||
|
|
||||||
同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):
|
同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):
|
||||||
|
|
||||||
1. 使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)
|
1. 使用者继承 AbstractQueuedSynchronizer 并重写指定的方法。(这些重写方法很简单,无非是对于共享资源 state 的获取和释放)
|
||||||
2. 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
|
2. 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
|
||||||
|
|
||||||
这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用,下面简单的给大家介绍一下模板方法模式,模板方法模式是一个很容易理解的设计模式之一。
|
这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用,下面简单的给大家介绍一下模板方法模式,模板方法模式是一个很容易理解的设计模式之一。
|
||||||
|
|
||||||
> 模板方法模式是基于”继承“的,主要是为了在不改变模板结构的前提下在子类中重新定义模板中的内容以实现复用代码。举个很简单的例子假如我们要去一个地方的步骤是:购票`buyTicket()`->安检`securityCheck()`->乘坐某某工具回家`ride()`->到达目的地`arrive()`。我们可能乘坐不同的交通工具回家比如飞机或者火车,所以除了`ride()`方法,其他方法的实现几乎相同。我们可以定义一个包含了这些方法的抽象类,然后用户根据自己的需要继承该抽象类然后修改 `ride()`方法。
|
> 模板方法模式是基于”继承“的,主要是为了在不改变模板结构的前提下在子类中重新定义模板中的内容以实现复用代码。举个很简单的例子假如我们要去一个地方的步骤是:购票`buyTicket()`->安检`securityCheck()`->乘坐某某工具回家`ride()`->到达目的地`arrive()`。我们可能乘坐不同的交通工具回家比如飞机或者火车,所以除了`ride()`方法,其他方法的实现几乎相同。我们可以定义一个包含了这些方法的抽象类,然后用户根据自己的需要继承该抽象类然后修改 `ride()`方法。
|
||||||
|
|
||||||
**AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:**
|
**AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的模板方法:**
|
||||||
|
|
||||||
```java
|
```java
|
||||||
isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
|
isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
|
||||||
@ -111,28 +223,28 @@ tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
默认情况下,每个方法都抛出 `UnsupportedOperationException`。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS类中的其他方法都是final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。
|
默认情况下,每个方法都抛出 `UnsupportedOperationException`。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS 类中的其他方法都是 final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。
|
||||||
|
|
||||||
以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
|
以 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()函数返回,继续后余动作。
|
再以 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown()一次,state 会 CAS(Compare and Swap)减 1。等到所有子线程都执行完后(即 state=0),会 unpark()主调用线程,然后主调用线程就会从 await()函数返回,继续后余动作。
|
||||||
|
|
||||||
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现`tryAcquire-tryRelease`、`tryAcquireShared-tryReleaseShared`中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如`ReentrantReadWriteLock`。
|
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现`tryAcquire-tryRelease`、`tryAcquireShared-tryReleaseShared`中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如`ReentrantReadWriteLock`。
|
||||||
|
|
||||||
推荐两篇 AQS 原理和相关源码分析的文章:
|
推荐两篇 AQS 原理和相关源码分析的文章:
|
||||||
|
|
||||||
- http://www.cnblogs.com/waterystone/p/4920797.html
|
- http://www.cnblogs.com/waterystone/p/4920797.html
|
||||||
- https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html
|
- https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### 3 Semaphore(信号量)-允许多个线程同时访问
|
### 3 Semaphore(信号量)-允许多个线程同时访问
|
||||||
|
|
||||||
**synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。**示例代码如下:
|
**synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。**
|
||||||
|
|
||||||
|
示例代码如下:
|
||||||
|
|
||||||
```java
|
```java
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @author Snailclimb
|
* @author Snailclimb
|
||||||
* @date 2018年9月30日
|
* @date 2018年9月30日
|
||||||
* @Description: 需要一次性拿一个许可的情况
|
* @Description: 需要一次性拿一个许可的情况
|
||||||
@ -173,22 +285,21 @@ public class SemaphoreExample1 {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
执行 `acquire` 方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个 `release` 方法增加一个许可证,这可能会释放一个阻塞的acquire方法。然而,其实并没有实际的许可证这个对象,Semaphore只是维持了一个可获得许可证的数量。 Semaphore经常用于限制获取某种资源的线程数量。
|
执行 `acquire` 方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个 `release` 方法增加一个许可证,这可能会释放一个阻塞的 acquire 方法。然而,其实并没有实际的许可证这个对象,Semaphore 只是维持了一个可获得许可证的数量。 Semaphore 经常用于限制获取某种资源的线程数量。
|
||||||
|
|
||||||
当然一次也可以一次拿取和释放多个许可,不过一般没有必要这样做:
|
当然一次也可以一次拿取和释放多个许可,不过一般没有必要这样做:
|
||||||
|
|
||||||
```java
|
```java
|
||||||
semaphore.acquire(5);// 获取5个许可,所以可运行线程数量为20/5=4
|
semaphore.acquire(5);// 获取5个许可,所以可运行线程数量为20/5=4
|
||||||
test(threadnum);
|
test(threadnum);
|
||||||
semaphore.release(5);// 获取5个许可,所以可运行线程数量为20/5=4
|
semaphore.release(5);// 获取5个许可,所以可运行线程数量为20/5=4
|
||||||
```
|
```
|
||||||
|
|
||||||
除了 `acquire`方法之外,另一个比较常用的与之对应的方法是`tryAcquire`方法,该方法如果获取不到许可就立即返回false。
|
除了 `acquire`方法之外,另一个比较常用的与之对应的方法是`tryAcquire`方法,该方法如果获取不到许可就立即返回 false。
|
||||||
|
|
||||||
|
|
||||||
Semaphore 有两种模式,公平模式和非公平模式。
|
Semaphore 有两种模式,公平模式和非公平模式。
|
||||||
|
|
||||||
- **公平模式:** 调用acquire的顺序就是获取许可证的顺序,遵循FIFO;
|
- **公平模式:** 调用 acquire 的顺序就是获取许可证的顺序,遵循 FIFO;
|
||||||
- **非公平模式:** 抢占式的。
|
- **非公平模式:** 抢占式的。
|
||||||
|
|
||||||
**Semaphore 对应的两个构造方法如下:**
|
**Semaphore 对应的两个构造方法如下:**
|
||||||
@ -202,29 +313,30 @@ Semaphore 有两种模式,公平模式和非公平模式。
|
|||||||
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
|
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
**这两个构造方法,都必须提供许可的数量,第二个构造方法可以指定是公平模式还是非公平模式,默认非公平模式。**
|
|
||||||
|
|
||||||
由于篇幅问题,如果对 Semaphore 源码感兴趣的朋友可以看下面这篇文章:
|
**这两个构造方法,都必须提供许可的数量,第二个构造方法可以指定是公平模式还是非公平模式,默认非公平模式。**
|
||||||
|
|
||||||
- https://blog.csdn.net/qq_19431333/article/details/70212663
|
[issue645补充内容](https://github.com/Snailclimb/JavaGuide/issues/645) :Semaphore与CountDownLatch一样,也是共享锁的一种实现。它默认构造AQS的state为permits。当执行任务的线程数量超出permits,那么多余的线程将会被放入阻塞队列Park,并自旋判断state是否大于0。只有当state大于0的时候,阻塞的线程才能继续执行,此时先前执行任务的线程继续执行release方法,release方法使得state的变量会加1,那么自旋的线程便会判断成功。
|
||||||
|
如此,每次只有最多不超过permits数量的线程能自旋成功,便限制了执行任务线程的数量。
|
||||||
|
|
||||||
|
由于篇幅问题,如果对 Semaphore 源码感兴趣的朋友可以看下这篇文章:https://juejin.im/post/5ae755366fb9a07ab508adc6
|
||||||
|
|
||||||
### 4 CountDownLatch (倒计时器)
|
### 4 CountDownLatch (倒计时器)
|
||||||
|
|
||||||
CountDownLatch是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行。在Java并发中,countdownlatch的概念是一个常见的面试题,所以一定要确保你很好的理解了它。
|
CountDownLatch允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。在 Java 并发中,countdownlatch 的概念是一个常见的面试题,所以一定要确保你很好的理解了它。
|
||||||
|
|
||||||
#### 4.1 CountDownLatch 的三种典型用法
|
CountDownLatch是共享锁的一种实现,它默认构造 AQS 的 state 值为 count。当线程使用countDown方法时,其实使用了`tryReleaseShared`方法以CAS的操作来减少state,直至state为0就代表所有的线程都调用了countDown方法。当调用await方法的时候,如果state不为0,就代表仍然有线程没有调用countDown方法,那么就把已经调用过countDown的线程都放入阻塞队列Park,并自旋CAS判断state == 0,直至最后一个线程调用了countDown,使得state == 0,于是阻塞的线程便判断成功,全部往下执行。
|
||||||
|
|
||||||
①某一线程在开始运行前等待n个线程执行完毕。将 CountDownLatch 的计数器初始化为n :`new CountDownLatch(n) `,每当一个任务线程执行完毕,就将计数器减1 `countdownlatch.countDown()`,当计数器的值变为0时,在`CountDownLatch上 await()` 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。
|
#### 4.1 CountDownLatch 的两种典型用法
|
||||||
|
|
||||||
②实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的 `CountDownLatch` 对象,将其计数器初始化为 1 :`new CountDownLatch(1) `,多个线程在开始执行任务前首先 `coundownlatch.await()`,当主线程调用 countDown() 时,计数器变为0,多个线程同时被唤醒。
|
1. 某一线程在开始运行前等待 n 个线程执行完毕。将 CountDownLatch 的计数器初始化为 n :`new CountDownLatch(n)`,每当一个任务线程执行完毕,就将计数器减 1 `countdownlatch.countDown()`,当计数器的值变为 0 时,在`CountDownLatch上 await()` 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。
|
||||||
|
2. 实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的 `CountDownLatch` 对象,将其计数器初始化为 1 :`new CountDownLatch(1)`,多个线程在开始执行任务前首先 `coundownlatch.await()`,当主线程调用 countDown() 时,计数器变为 0,多个线程同时被唤醒。
|
||||||
③死锁检测:一个非常方便的使用场景是,你可以使用n个线程访问共享资源,在每次测试阶段的线程数目是不同的,并尝试产生死锁。
|
|
||||||
|
|
||||||
#### 4.2 CountDownLatch 的使用示例
|
#### 4.2 CountDownLatch 的使用示例
|
||||||
|
|
||||||
```java
|
```java
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @author SnailClimb
|
* @author SnailClimb
|
||||||
* @date 2018年10月1日
|
* @date 2018年10月1日
|
||||||
* @Description: CountDownLatch 使用方法示例
|
* @Description: CountDownLatch 使用方法示例
|
||||||
@ -264,23 +376,36 @@ public class CountDownLatchExample1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
上面的代码中,我们定义了请求的数量为550,当这550个请求被处理完成之后,才会执行`System.out.println("finish");`。
|
|
||||||
|
|
||||||
与CountDownLatch的第一次交互是主线程等待其他线程。主线程必须在启动其他线程后立即调用CountDownLatch.await()方法。这样主线程的操作就会在这个方法上阻塞,直到其他线程完成各自的任务。
|
上面的代码中,我们定义了请求的数量为 550,当这 550 个请求被处理完成之后,才会执行`System.out.println("finish");`。
|
||||||
|
|
||||||
其他N个线程必须引用闭锁对象,因为他们需要通知CountDownLatch对象,他们已经完成了各自的任务。这种通知机制是通过 CountDownLatch.countDown()方法来完成的;每调用一次这个方法,在构造函数中初始化的count值就减1。所以当N个线程都调 用了这个方法,count的值等于0,然后主线程就能通过await()方法,恢复执行自己的任务。
|
与 CountDownLatch 的第一次交互是主线程等待其他线程。主线程必须在启动其他线程后立即调用 `CountDownLatch.await()` 方法。这样主线程的操作就会在这个方法上阻塞,直到其他线程完成各自的任务。
|
||||||
|
|
||||||
|
其他 N 个线程必须引用闭锁对象,因为他们需要通知 `CountDownLatch` 对象,他们已经完成了各自的任务。这种通知机制是通过 `CountDownLatch.countDown()`方法来完成的;每调用一次这个方法,在构造函数中初始化的 count 值就减 1。所以当 N 个线程都调 用了这个方法,count 的值等于 0,然后主线程就能通过 `await()`方法,恢复执行自己的任务。
|
||||||
|
|
||||||
|
再插一嘴:`CountDownLatch` 的 `await()` 方法使用不当很容易产生死锁,比如我们上面代码中的 for 循环改为:
|
||||||
|
|
||||||
|
```java
|
||||||
|
for (int i = 0; i < threadCount-1; i++) {
|
||||||
|
.......
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这样就导致 `count` 的值没办法等于 0,然后就会导致一直等待。
|
||||||
|
|
||||||
|
如果对CountDownLatch源码感兴趣的朋友,可以查看: [【JUC】JDK1.8源码分析之CountDownLatch(五)](https://www.cnblogs.com/leesf456/p/5406191.html)
|
||||||
|
|
||||||
#### 4.3 CountDownLatch 的不足
|
#### 4.3 CountDownLatch 的不足
|
||||||
|
|
||||||
CountDownLatch是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch使用完毕后,它不能再次被使用。
|
CountDownLatch 是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 CountDownLatch 使用完毕后,它不能再次被使用。
|
||||||
|
|
||||||
#### 4.4 CountDownLatch相常见面试题:
|
#### 4.4 CountDownLatch 相常见面试题
|
||||||
|
|
||||||
解释一下CountDownLatch概念?
|
解释一下 CountDownLatch 概念?
|
||||||
|
|
||||||
CountDownLatch 和CyclicBarrier的不同之处?
|
CountDownLatch 和 CyclicBarrier 的不同之处?
|
||||||
|
|
||||||
给出一些CountDownLatch使用的例子?
|
给出一些 CountDownLatch 使用的例子?
|
||||||
|
|
||||||
CountDownLatch 类中主要的方法?
|
CountDownLatch 类中主要的方法?
|
||||||
|
|
||||||
@ -288,19 +413,38 @@ CountDownLatch 类中主要的方法?
|
|||||||
|
|
||||||
CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。
|
CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。
|
||||||
|
|
||||||
CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 `CyclicBarrier(int parties)`,其参数表示屏障拦截的线程数量,每个线程调用`await`方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。
|
> CountDownLatch的实现是基于AQS的,而CycliBarrier是基于 ReentrantLock(ReentrantLock也属于AQS同步器)和 Condition 的.
|
||||||
|
|
||||||
|
CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier 默认的构造方法是 `CyclicBarrier(int parties)`,其参数表示屏障拦截的线程数量,每个线程调用`await`方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。
|
||||||
|
|
||||||
|
再来看一下它的构造函数:
|
||||||
|
|
||||||
|
```java
|
||||||
|
public CyclicBarrier(int parties) {
|
||||||
|
this(parties, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CyclicBarrier(int parties, Runnable barrierAction) {
|
||||||
|
if (parties <= 0) throw new IllegalArgumentException();
|
||||||
|
this.parties = parties;
|
||||||
|
this.count = parties;
|
||||||
|
this.barrierCommand = barrierAction;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
其中,parties 就代表了有拦截的线程的数量,当拦截的线程数量达到这个值的时候就打开栅栏,让所有线程通过。
|
||||||
|
|
||||||
#### 5.1 CyclicBarrier 的应用场景
|
#### 5.1 CyclicBarrier 的应用场景
|
||||||
|
|
||||||
CyclicBarrier 可以用于多线程计算数据,最后合并计算结果的应用场景。比如我们用一个Excel保存了用户所有银行流水,每个Sheet保存一个帐户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个sheet里的银行流水,都执行完之后,得到每个sheet的日均银行流水,最后,再用barrierAction用这些线程的计算结果,计算出整个Excel的日均银行流水。
|
CyclicBarrier 可以用于多线程计算数据,最后合并计算结果的应用场景。比如我们用一个 Excel 保存了用户所有银行流水,每个 Sheet 保存一个帐户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个 sheet 里的银行流水,都执行完之后,得到每个 sheet 的日均银行流水,最后,再用 barrierAction 用这些线程的计算结果,计算出整个 Excel 的日均银行流水。
|
||||||
|
|
||||||
#### 5.2 CyclicBarrier 的使用示例
|
#### 5.2 CyclicBarrier 的使用示例
|
||||||
|
|
||||||
示例1:
|
示例 1:
|
||||||
|
|
||||||
```java
|
```java
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @author Snailclimb
|
* @author Snailclimb
|
||||||
* @date 2018年10月1日
|
* @date 2018年10月1日
|
||||||
* @Description: 测试 CyclicBarrier 类中带参数的 await() 方法
|
* @Description: 测试 CyclicBarrier 类中带参数的 await() 方法
|
||||||
@ -336,7 +480,7 @@ public class CyclicBarrierExample2 {
|
|||||||
public static void test(int threadnum) throws InterruptedException, BrokenBarrierException {
|
public static void test(int threadnum) throws InterruptedException, BrokenBarrierException {
|
||||||
System.out.println("threadnum:" + threadnum + "is ready");
|
System.out.println("threadnum:" + threadnum + "is ready");
|
||||||
try {
|
try {
|
||||||
/**等待60秒,保证子线程完全执行结束*/
|
/**等待60秒,保证子线程完全执行结束*/
|
||||||
cyclicBarrier.await(60, TimeUnit.SECONDS);
|
cyclicBarrier.await(60, TimeUnit.SECONDS);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
System.out.println("-----CyclicBarrierException------");
|
System.out.println("-----CyclicBarrierException------");
|
||||||
@ -372,13 +516,14 @@ threadnum:7is finish
|
|||||||
threadnum:6is finish
|
threadnum:6is finish
|
||||||
......
|
......
|
||||||
```
|
```
|
||||||
可以看到当线程数量也就是请求数量达到我们定义的 5 个的时候, `await`方法之后的方法才被执行。
|
|
||||||
|
|
||||||
另外,CyclicBarrier还提供一个更高级的构造函数`CyclicBarrier(int parties, Runnable barrierAction)`,用于在线程到达屏障时,优先执行`barrierAction`,方便处理更复杂的业务场景。示例代码如下:
|
可以看到当线程数量也就是请求数量达到我们定义的 5 个的时候, `await`方法之后的方法才被执行。
|
||||||
|
|
||||||
|
另外,CyclicBarrier 还提供一个更高级的构造函数`CyclicBarrier(int parties, Runnable barrierAction)`,用于在线程到达屏障时,优先执行`barrierAction`,方便处理更复杂的业务场景。示例代码如下:
|
||||||
|
|
||||||
```java
|
```java
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @author SnailClimb
|
* @author SnailClimb
|
||||||
* @date 2018年10月1日
|
* @date 2018年10月1日
|
||||||
* @Description: 新建 CyclicBarrier 的时候指定一个 Runnable
|
* @Description: 新建 CyclicBarrier 的时候指定一个 Runnable
|
||||||
@ -449,28 +594,136 @@ threadnum:8is finish
|
|||||||
threadnum:7is finish
|
threadnum:7is finish
|
||||||
......
|
......
|
||||||
```
|
```
|
||||||
#### 5.3 CyclicBarrier和CountDownLatch的区别
|
|
||||||
|
|
||||||
CountDownLatch是计数器,只能使用一次,而CyclicBarrier的计数器提供reset功能,可以多次使用。但是我不那么认为它们之间的区别仅仅就是这么简单的一点。我们来从jdk作者设计的目的来看,javadoc是这么描述它们的:
|
#### 5.3 `CyclicBarrier`源码分析
|
||||||
|
|
||||||
|
当调用 `CyclicBarrier` 对象调用 `await()` 方法时,实际上调用的是`dowait(false, 0L)`方法。 `await()` 方法就像树立起一个栅栏的行为一样,将线程挡住了,当拦住的线程数量达到 parties 的值时,栅栏才会打开,线程才得以通过执行。
|
||||||
|
|
||||||
|
```java
|
||||||
|
public int await() throws InterruptedException, BrokenBarrierException {
|
||||||
|
try {
|
||||||
|
return dowait(false, 0L);
|
||||||
|
} catch (TimeoutException toe) {
|
||||||
|
throw new Error(toe); // cannot happen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`dowait(false, 0L)`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 当线程数量或者请求数量达到 count 时 await 之后的方法才会被执行。上面的示例中 count 的值就为 5。
|
||||||
|
private int count;
|
||||||
|
/**
|
||||||
|
* Main barrier code, covering the various policies.
|
||||||
|
*/
|
||||||
|
private int dowait(boolean timed, long nanos)
|
||||||
|
throws InterruptedException, BrokenBarrierException,
|
||||||
|
TimeoutException {
|
||||||
|
final ReentrantLock lock = this.lock;
|
||||||
|
// 锁住
|
||||||
|
lock.lock();
|
||||||
|
try {
|
||||||
|
final Generation g = generation;
|
||||||
|
|
||||||
|
if (g.broken)
|
||||||
|
throw new BrokenBarrierException();
|
||||||
|
|
||||||
|
// 如果线程中断了,抛出异常
|
||||||
|
if (Thread.interrupted()) {
|
||||||
|
breakBarrier();
|
||||||
|
throw new InterruptedException();
|
||||||
|
}
|
||||||
|
// cout减1
|
||||||
|
int index = --count;
|
||||||
|
// 当 count 数量减为 0 之后说明最后一个线程已经到达栅栏了,也就是达到了可以执行await 方法之后的条件
|
||||||
|
if (index == 0) { // tripped
|
||||||
|
boolean ranAction = false;
|
||||||
|
try {
|
||||||
|
final Runnable command = barrierCommand;
|
||||||
|
if (command != null)
|
||||||
|
command.run();
|
||||||
|
ranAction = true;
|
||||||
|
// 将 count 重置为 parties 属性的初始化值
|
||||||
|
// 唤醒之前等待的线程
|
||||||
|
// 下一波执行开始
|
||||||
|
nextGeneration();
|
||||||
|
return 0;
|
||||||
|
} finally {
|
||||||
|
if (!ranAction)
|
||||||
|
breakBarrier();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loop until tripped, broken, interrupted, or timed out
|
||||||
|
for (;;) {
|
||||||
|
try {
|
||||||
|
if (!timed)
|
||||||
|
trip.await();
|
||||||
|
else if (nanos > 0L)
|
||||||
|
nanos = trip.awaitNanos(nanos);
|
||||||
|
} catch (InterruptedException ie) {
|
||||||
|
if (g == generation && ! g.broken) {
|
||||||
|
breakBarrier();
|
||||||
|
throw ie;
|
||||||
|
} else {
|
||||||
|
// We're about to finish waiting even if we had not
|
||||||
|
// been interrupted, so this interrupt is deemed to
|
||||||
|
// "belong" to subsequent execution.
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (g.broken)
|
||||||
|
throw new BrokenBarrierException();
|
||||||
|
|
||||||
|
if (g != generation)
|
||||||
|
return index;
|
||||||
|
|
||||||
|
if (timed && nanos <= 0L) {
|
||||||
|
breakBarrier();
|
||||||
|
throw new TimeoutException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
总结:`CyclicBarrier` 内部通过一个 count 变量作为计数器,cout 的初始值为 parties 属性的初始化值,每当一个线程到了栅栏这里了,那么就将计数器减一。如果 count 值为 0 了,表示这是这一代最后一个线程到达栅栏,就尝试执行我们构造方法中输入的任务。
|
||||||
|
|
||||||
|
#### 5.4 CyclicBarrier 和 CountDownLatch 的区别
|
||||||
|
|
||||||
|
**下面这个是国外一个大佬的回答:**
|
||||||
|
|
||||||
|
CountDownLatch 是计数器,只能使用一次,而 CyclicBarrier 的计数器提供 reset 功能,可以多次使用。但是我不那么认为它们之间的区别仅仅就是这么简单的一点。我们来从 jdk 作者设计的目的来看,javadoc 是这么描述它们的:
|
||||||
|
|
||||||
> CountDownLatch: A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.(CountDownLatch: 一个或者多个线程,等待其他多个线程完成某件事情之后才能执行;)
|
> CountDownLatch: A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.(CountDownLatch: 一个或者多个线程,等待其他多个线程完成某件事情之后才能执行;)
|
||||||
> CyclicBarrier : A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point.(CyclicBarrier : 多个线程互相等待,直到到达同一个同步点,再继续一起执行。)
|
> CyclicBarrier : A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point.(CyclicBarrier : 多个线程互相等待,直到到达同一个同步点,再继续一起执行。)
|
||||||
|
|
||||||
对于CountDownLatch来说,重点是“一个线程(多个线程)等待”,而其他的N个线程在完成“某件事情”之后,可以终止,也可以等待。而对于CyclicBarrier,重点是多个线程,在任意一个线程没有完成,所有的线程都必须等待。
|
对于 CountDownLatch 来说,重点是“一个线程(多个线程)等待”,而其他的 N 个线程在完成“某件事情”之后,可以终止,也可以等待。而对于 CyclicBarrier,重点是多个线程,在任意一个线程没有完成,所有的线程都必须等待。
|
||||||
|
|
||||||
CountDownLatch是计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而CyclicBarrier更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行。
|
CountDownLatch 是计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而 CyclicBarrier 更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行。
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
CyclicBarrier和CountDownLatch的区别这部分内容参考了如下两篇文章:
|
|
||||||
|
|
||||||
- https://blog.csdn.net/u010185262/article/details/54692886
|
|
||||||
- https://blog.csdn.net/tolcf/article/details/50925145?utm_source=blogxgwz0
|
|
||||||
|
|
||||||
### 6 ReentrantLock 和 ReentrantReadWriteLock
|
### 6 ReentrantLock 和 ReentrantReadWriteLock
|
||||||
|
|
||||||
ReentrantLock 和 synchronized 的区别在上面已经讲过了这里就不多做讲解。另外,需要注意的是:读写锁 ReentrantReadWriteLock 可以保证多个线程可以同时读,所以在读操作远大于写操作的时候,读写锁就非常有用了。
|
ReentrantLock 和 synchronized 的区别在上面已经讲过了这里就不多做讲解。另外,需要注意的是:读写锁 ReentrantReadWriteLock 可以保证多个线程可以同时读,所以在读操作远大于写操作的时候,读写锁就非常有用了。
|
||||||
|
|
||||||
由于篇幅问题,关于 ReentrantLock 和 ReentrantReadWriteLock 详细内容可以查看我的这篇原创文章。
|
### 参考
|
||||||
|
|
||||||
|
- https://juejin.im/post/5ae755256fb9a07ac3634067
|
||||||
|
- https://blog.csdn.net/u010185262/article/details/54692886
|
||||||
|
- https://blog.csdn.net/tolcf/article/details/50925145?utm_source=blogxgwz0
|
||||||
|
|
||||||
|
### 公众号
|
||||||
|
|
||||||
|
如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。
|
||||||
|
|
||||||
|
**《Java 面试突击》:** 由本文档衍生的专为面试而生的《Java 面试突击》V2.0 PDF 版本[公众号](#公众号 "公众号")后台回复 **"面试突击"** 即可免费领取!
|
||||||
|
|
||||||
|
**Java 工程师必备学习资源:** 一些 Java 工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
- [ReentrantLock 和 ReentrantReadWriteLock](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483745&idx=2&sn=6778ee954a19816310df54ef9a3c2f8a&chksm=fd985700caefde16b9970f5e093b0c140d3121fb3a8458b11871e5e9723c5fd1b5a961fd2228&token=1829606453&lang=zh_CN#rd)
|
|
||||||
|
@ -1,8 +1,28 @@
|
|||||||
|
点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。
|
||||||
|
|
||||||
> 个人觉得这一节掌握基本的使用即可!
|
> 个人觉得这一节掌握基本的使用即可!
|
||||||
|
|
||||||
**本节思维导图:**
|
<!-- TOC -->
|
||||||
|
|
||||||

|
- [1 Atomic 原子类介绍](#1-atomic-原子类介绍)
|
||||||
|
- [2 基本类型原子类](#2-基本类型原子类)
|
||||||
|
- [2.1 基本类型原子类介绍](#21-基本类型原子类介绍)
|
||||||
|
- [2.2 AtomicInteger 常见方法使用](#22-atomicinteger-常见方法使用)
|
||||||
|
- [2.3 基本数据类型原子类的优势](#23-基本数据类型原子类的优势)
|
||||||
|
- [2.4 AtomicInteger 线程安全原理简单分析](#24-atomicinteger-线程安全原理简单分析)
|
||||||
|
- [3 数组类型原子类](#3-数组类型原子类)
|
||||||
|
- [3.1 数组类型原子类介绍](#31-数组类型原子类介绍)
|
||||||
|
- [3.2 AtomicIntegerArray 常见方法使用](#32-atomicintegerarray-常见方法使用)
|
||||||
|
- [4 引用类型原子类](#4-引用类型原子类)
|
||||||
|
- [4.1 引用类型原子类介绍](#41--引用类型原子类介绍)
|
||||||
|
- [4.2 AtomicReference 类使用示例](#42-atomicreference-类使用示例)
|
||||||
|
- [4.3 AtomicStampedReference 类使用示例](#43-atomicstampedreference-类使用示例)
|
||||||
|
- [4.4 AtomicMarkableReference 类使用示例](#44-atomicmarkablereference-类使用示例)
|
||||||
|
- [5 对象的属性修改类型原子类](#5-对象的属性修改类型原子类)
|
||||||
|
- [5.1 对象的属性修改类型原子类介绍](#51-对象的属性修改类型原子类介绍)
|
||||||
|
- [5.2 AtomicIntegerFieldUpdater 类使用示例](#52-atomicintegerfieldupdater-类使用示例)
|
||||||
|
|
||||||
|
<!-- /TOC -->
|
||||||
|
|
||||||
### 1 Atomic 原子类介绍
|
### 1 Atomic 原子类介绍
|
||||||
|
|
||||||
@ -12,7 +32,7 @@ Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是
|
|||||||
|
|
||||||
并发包 `java.util.concurrent` 的原子类都存放在`java.util.concurrent.atomic`下,如下图所示。
|
并发包 `java.util.concurrent` 的原子类都存放在`java.util.concurrent.atomic`下,如下图所示。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
根据操作的数据类型,可以将JUC包中的原子类分为4类
|
根据操作的数据类型,可以将JUC包中的原子类分为4类
|
||||||
|
|
||||||
@ -36,14 +56,136 @@ Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是
|
|||||||
**引用类型**
|
**引用类型**
|
||||||
|
|
||||||
- AtomicReference:引用类型原子类
|
- AtomicReference:引用类型原子类
|
||||||
- AtomicStampedRerence:原子更新引用类型里的字段原子类
|
- AtomicMarkableReference:原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来,~~也可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。~~
|
||||||
- AtomicMarkableReference :原子更新带有标记位的引用类型
|
- AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
|
||||||
|
|
||||||
**对象的属性修改类型**
|
**对象的属性修改类型**
|
||||||
|
|
||||||
- AtomicIntegerFieldUpdater:原子更新整型字段的更新器
|
- AtomicIntegerFieldUpdater:原子更新整型字段的更新器
|
||||||
- AtomicLongFieldUpdater:原子更新长整型字段的更新器
|
- AtomicLongFieldUpdater:原子更新长整型字段的更新器
|
||||||
- AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
|
- AtomicReferenceFieldUpdater:原子更新引用类型里的字段
|
||||||
|
|
||||||
|
> 修正: **AtomicMarkableReference 不能解决ABA问题** **[issue#626](https://github.com/Snailclimb/JavaGuide/issues/626)**
|
||||||
|
|
||||||
|
```java
|
||||||
|
/**
|
||||||
|
|
||||||
|
AtomicMarkableReference是将一个boolean值作是否有更改的标记,本质就是它的版本号只有两个,true和false,
|
||||||
|
|
||||||
|
修改的时候在这两个版本号之间来回切换,这样做并不能解决ABA的问题,只是会降低ABA问题发生的几率而已
|
||||||
|
|
||||||
|
@author : mazh
|
||||||
|
|
||||||
|
@Date : 2020/1/17 14:41
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class SolveABAByAtomicMarkableReference {
|
||||||
|
|
||||||
|
private static AtomicMarkableReference atomicMarkableReference = new AtomicMarkableReference(100, false);
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
|
||||||
|
Thread refT1 = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
TimeUnit.SECONDS.sleep(1);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
atomicMarkableReference.compareAndSet(100, 101, atomicMarkableReference.isMarked(), !atomicMarkableReference.isMarked());
|
||||||
|
atomicMarkableReference.compareAndSet(101, 100, atomicMarkableReference.isMarked(), !atomicMarkableReference.isMarked());
|
||||||
|
});
|
||||||
|
|
||||||
|
Thread refT2 = new Thread(() -> {
|
||||||
|
boolean marked = atomicMarkableReference.isMarked();
|
||||||
|
try {
|
||||||
|
TimeUnit.SECONDS.sleep(2);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
boolean c3 = atomicMarkableReference.compareAndSet(100, 101, marked, !marked);
|
||||||
|
System.out.println(c3); // 返回true,实际应该返回false
|
||||||
|
});
|
||||||
|
|
||||||
|
refT1.start();
|
||||||
|
refT2.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**CAS ABA 问题**
|
||||||
|
- 描述: 第一个线程取到了变量 x 的值 A,然后巴拉巴拉干别的事,总之就是只拿到了变量 x 的值 A。这段时间内第二个线程也取到了变量 x 的值 A,然后把变量 x 的值改为 B,然后巴拉巴拉干别的事,最后又把变量 x 的值变为 A (相当于还原了)。在这之后第一个线程终于进行了变量 x 的操作,但是此时变量 x 的值还是 A,所以 compareAndSet 操作是成功。
|
||||||
|
- 例子描述(可能不太合适,但好理解): 年初,现金为零,然后通过正常劳动赚了三百万,之后正常消费了(比如买房子)三百万。年末,虽然现金零收入(可能变成其他形式了),但是赚了钱是事实,还是得交税的!
|
||||||
|
- 代码例子(以``` AtomicInteger ```为例)
|
||||||
|
|
||||||
|
```java
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
public class AtomicIntegerDefectDemo {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
defectOfABA();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void defectOfABA() {
|
||||||
|
final AtomicInteger atomicInteger = new AtomicInteger(1);
|
||||||
|
|
||||||
|
Thread coreThread = new Thread(
|
||||||
|
() -> {
|
||||||
|
final int currentValue = atomicInteger.get();
|
||||||
|
System.out.println(Thread.currentThread().getName() + " ------ currentValue=" + currentValue);
|
||||||
|
|
||||||
|
// 这段目的:模拟处理其他业务花费的时间
|
||||||
|
try {
|
||||||
|
Thread.sleep(300);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean casResult = atomicInteger.compareAndSet(1, 2);
|
||||||
|
System.out.println(Thread.currentThread().getName()
|
||||||
|
+ " ------ currentValue=" + currentValue
|
||||||
|
+ ", finalValue=" + atomicInteger.get()
|
||||||
|
+ ", compareAndSet Result=" + casResult);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
coreThread.start();
|
||||||
|
|
||||||
|
// 这段目的:为了让 coreThread 线程先跑起来
|
||||||
|
try {
|
||||||
|
Thread.sleep(100);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread amateurThread = new Thread(
|
||||||
|
() -> {
|
||||||
|
int currentValue = atomicInteger.get();
|
||||||
|
boolean casResult = atomicInteger.compareAndSet(1, 2);
|
||||||
|
System.out.println(Thread.currentThread().getName()
|
||||||
|
+ " ------ currentValue=" + currentValue
|
||||||
|
+ ", finalValue=" + atomicInteger.get()
|
||||||
|
+ ", compareAndSet Result=" + casResult);
|
||||||
|
|
||||||
|
currentValue = atomicInteger.get();
|
||||||
|
casResult = atomicInteger.compareAndSet(2, 1);
|
||||||
|
System.out.println(Thread.currentThread().getName()
|
||||||
|
+ " ------ currentValue=" + currentValue
|
||||||
|
+ ", finalValue=" + atomicInteger.get()
|
||||||
|
+ ", compareAndSet Result=" + casResult);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
amateurThread.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
输出内容如下:
|
||||||
|
|
||||||
|
```
|
||||||
|
Thread-0 ------ currentValue=1
|
||||||
|
Thread-1 ------ currentValue=1, finalValue=2, compareAndSet Result=true
|
||||||
|
Thread-1 ------ currentValue=2, finalValue=1, compareAndSet Result=true
|
||||||
|
Thread-0 ------ currentValue=1, finalValue=2, compareAndSet Result=true
|
||||||
|
```
|
||||||
|
|
||||||
下面我们来详细介绍一下这些原子类。
|
下面我们来详细介绍一下这些原子类。
|
||||||
|
|
||||||
@ -60,7 +202,7 @@ Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是
|
|||||||
上面三个类提供的方法几乎相同,所以我们这里以 AtomicInteger 为例子来介绍。
|
上面三个类提供的方法几乎相同,所以我们这里以 AtomicInteger 为例子来介绍。
|
||||||
|
|
||||||
**AtomicInteger 类常用方法**
|
**AtomicInteger 类常用方法**
|
||||||
|
|
||||||
```java
|
```java
|
||||||
public final int get() //获取当前的值
|
public final int get() //获取当前的值
|
||||||
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
|
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
|
||||||
@ -149,7 +291,7 @@ AtomicInteger 类的部分源码:
|
|||||||
|
|
||||||
AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
|
AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
|
||||||
|
|
||||||
CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。
|
CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址。另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。
|
||||||
|
|
||||||
|
|
||||||
### 3 数组类型原子类
|
### 3 数组类型原子类
|
||||||
@ -210,8 +352,8 @@ public class AtomicIntegerArrayTest {
|
|||||||
基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用 引用类型原子类。
|
基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用 引用类型原子类。
|
||||||
|
|
||||||
- AtomicReference:引用类型原子类
|
- AtomicReference:引用类型原子类
|
||||||
- AtomicStampedRerence:原子更新引用类型里的字段原子类
|
- AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
|
||||||
- AtomicMarkableReference :原子更新带有标记位的引用类型
|
- AtomicMarkableReference :原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来,~~也可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。~~
|
||||||
|
|
||||||
上面三个类提供的方法几乎相同,所以我们这里以 AtomicReference 为例子来介绍。
|
上面三个类提供的方法几乎相同,所以我们这里以 AtomicReference 为例子来介绍。
|
||||||
|
|
||||||
@ -262,13 +404,127 @@ class Person {
|
|||||||
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
上述代码首先创建了一个 Person 对象,然后把 Person 对象设置进 AtomicReference 对象中,然后调用 compareAndSet 方法,该方法就是通过通过 CAS 操作设置 ar。如果 ar 的值为 person 的话,则将其设置为 updatePerson。实现原理与 AtomicInteger 类中的 compareAndSet 方法相同。运行上面的代码后的输出结果如下:
|
上述代码首先创建了一个 Person 对象,然后把 Person 对象设置进 AtomicReference 对象中,然后调用 compareAndSet 方法,该方法就是通过 CAS 操作设置 ar。如果 ar 的值为 person 的话,则将其设置为 updatePerson。实现原理与 AtomicInteger 类中的 compareAndSet 方法相同。运行上面的代码后的输出结果如下:
|
||||||
|
|
||||||
```
|
```
|
||||||
Daisy
|
Daisy
|
||||||
20
|
20
|
||||||
```
|
```
|
||||||
|
#### 4.3 AtomicStampedReference 类使用示例
|
||||||
|
|
||||||
|
```java
|
||||||
|
import java.util.concurrent.atomic.AtomicStampedReference;
|
||||||
|
|
||||||
|
public class AtomicStampedReferenceDemo {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
// 实例化、取当前值和 stamp 值
|
||||||
|
final Integer initialRef = 0, initialStamp = 0;
|
||||||
|
final AtomicStampedReference<Integer> asr = new AtomicStampedReference<>(initialRef, initialStamp);
|
||||||
|
System.out.println("currentValue=" + asr.getReference() + ", currentStamp=" + asr.getStamp());
|
||||||
|
|
||||||
|
// compare and set
|
||||||
|
final Integer newReference = 666, newStamp = 999;
|
||||||
|
final boolean casResult = asr.compareAndSet(initialRef, newReference, initialStamp, newStamp);
|
||||||
|
System.out.println("currentValue=" + asr.getReference()
|
||||||
|
+ ", currentStamp=" + asr.getStamp()
|
||||||
|
+ ", casResult=" + casResult);
|
||||||
|
|
||||||
|
// 获取当前的值和当前的 stamp 值
|
||||||
|
int[] arr = new int[1];
|
||||||
|
final Integer currentValue = asr.get(arr);
|
||||||
|
final int currentStamp = arr[0];
|
||||||
|
System.out.println("currentValue=" + currentValue + ", currentStamp=" + currentStamp);
|
||||||
|
|
||||||
|
// 单独设置 stamp 值
|
||||||
|
final boolean attemptStampResult = asr.attemptStamp(newReference, 88);
|
||||||
|
System.out.println("currentValue=" + asr.getReference()
|
||||||
|
+ ", currentStamp=" + asr.getStamp()
|
||||||
|
+ ", attemptStampResult=" + attemptStampResult);
|
||||||
|
|
||||||
|
// 重新设置当前值和 stamp 值
|
||||||
|
asr.set(initialRef, initialStamp);
|
||||||
|
System.out.println("currentValue=" + asr.getReference() + ", currentStamp=" + asr.getStamp());
|
||||||
|
|
||||||
|
// [不推荐使用,除非搞清楚注释的意思了] weak compare and set
|
||||||
|
// 困惑!weakCompareAndSet 这个方法最终还是调用 compareAndSet 方法。[版本: jdk-8u191]
|
||||||
|
// 但是注释上写着 "May fail spuriously and does not provide ordering guarantees,
|
||||||
|
// so is only rarely an appropriate alternative to compareAndSet."
|
||||||
|
// todo 感觉有可能是 jvm 通过方法名在 native 方法里面做了转发
|
||||||
|
final boolean wCasResult = asr.weakCompareAndSet(initialRef, newReference, initialStamp, newStamp);
|
||||||
|
System.out.println("currentValue=" + asr.getReference()
|
||||||
|
+ ", currentStamp=" + asr.getStamp()
|
||||||
|
+ ", wCasResult=" + wCasResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
输出结果如下:
|
||||||
|
```
|
||||||
|
currentValue=0, currentStamp=0
|
||||||
|
currentValue=666, currentStamp=999, casResult=true
|
||||||
|
currentValue=666, currentStamp=999
|
||||||
|
currentValue=666, currentStamp=88, attemptStampResult=true
|
||||||
|
currentValue=0, currentStamp=0
|
||||||
|
currentValue=666, currentStamp=999, wCasResult=true
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.4 AtomicMarkableReference 类使用示例
|
||||||
|
|
||||||
|
``` java
|
||||||
|
import java.util.concurrent.atomic.AtomicMarkableReference;
|
||||||
|
|
||||||
|
public class AtomicMarkableReferenceDemo {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
// 实例化、取当前值和 mark 值
|
||||||
|
final Boolean initialRef = null, initialMark = false;
|
||||||
|
final AtomicMarkableReference<Boolean> amr = new AtomicMarkableReference<>(initialRef, initialMark);
|
||||||
|
System.out.println("currentValue=" + amr.getReference() + ", currentMark=" + amr.isMarked());
|
||||||
|
|
||||||
|
// compare and set
|
||||||
|
final Boolean newReference1 = true, newMark1 = true;
|
||||||
|
final boolean casResult = amr.compareAndSet(initialRef, newReference1, initialMark, newMark1);
|
||||||
|
System.out.println("currentValue=" + amr.getReference()
|
||||||
|
+ ", currentMark=" + amr.isMarked()
|
||||||
|
+ ", casResult=" + casResult);
|
||||||
|
|
||||||
|
// 获取当前的值和当前的 mark 值
|
||||||
|
boolean[] arr = new boolean[1];
|
||||||
|
final Boolean currentValue = amr.get(arr);
|
||||||
|
final boolean currentMark = arr[0];
|
||||||
|
System.out.println("currentValue=" + currentValue + ", currentMark=" + currentMark);
|
||||||
|
|
||||||
|
// 单独设置 mark 值
|
||||||
|
final boolean attemptMarkResult = amr.attemptMark(newReference1, false);
|
||||||
|
System.out.println("currentValue=" + amr.getReference()
|
||||||
|
+ ", currentMark=" + amr.isMarked()
|
||||||
|
+ ", attemptMarkResult=" + attemptMarkResult);
|
||||||
|
|
||||||
|
// 重新设置当前值和 mark 值
|
||||||
|
amr.set(initialRef, initialMark);
|
||||||
|
System.out.println("currentValue=" + amr.getReference() + ", currentMark=" + amr.isMarked());
|
||||||
|
|
||||||
|
// [不推荐使用,除非搞清楚注释的意思了] weak compare and set
|
||||||
|
// 困惑!weakCompareAndSet 这个方法最终还是调用 compareAndSet 方法。[版本: jdk-8u191]
|
||||||
|
// 但是注释上写着 "May fail spuriously and does not provide ordering guarantees,
|
||||||
|
// so is only rarely an appropriate alternative to compareAndSet."
|
||||||
|
// todo 感觉有可能是 jvm 通过方法名在 native 方法里面做了转发
|
||||||
|
final boolean wCasResult = amr.weakCompareAndSet(initialRef, newReference1, initialMark, newMark1);
|
||||||
|
System.out.println("currentValue=" + amr.getReference()
|
||||||
|
+ ", currentMark=" + amr.isMarked()
|
||||||
|
+ ", wCasResult=" + wCasResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
输出结果如下:
|
||||||
|
```
|
||||||
|
currentValue=null, currentMark=false
|
||||||
|
currentValue=true, currentMark=true, casResult=true
|
||||||
|
currentValue=true, currentMark=true
|
||||||
|
currentValue=true, currentMark=false, attemptMarkResult=true
|
||||||
|
currentValue=null, currentMark=false
|
||||||
|
currentValue=true, currentMark=true, wCasResult=true
|
||||||
|
```
|
||||||
|
|
||||||
### 5 对象的属性修改类型原子类
|
### 5 对象的属性修改类型原子类
|
||||||
|
|
||||||
@ -278,7 +534,7 @@ Daisy
|
|||||||
|
|
||||||
- AtomicIntegerFieldUpdater:原子更新整形字段的更新器
|
- AtomicIntegerFieldUpdater:原子更新整形字段的更新器
|
||||||
- AtomicLongFieldUpdater:原子更新长整形字段的更新器
|
- AtomicLongFieldUpdater:原子更新长整形字段的更新器
|
||||||
- AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
|
- AtomicReferenceFieldUpdater :原子更新引用类型里的字段的更新器
|
||||||
|
|
||||||
要想原子地更新对象的属性需要两步。第一步,因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新的对象属性必须使用 public volatile 修饰符。
|
要想原子地更新对象的属性需要两步。第一步,因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新的对象属性必须使用 public volatile 修饰符。
|
||||||
|
|
||||||
@ -335,3 +591,16 @@ class User {
|
|||||||
23
|
23
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- 《Java并发编程的艺术》
|
||||||
|
|
||||||
|
## 公众号
|
||||||
|
|
||||||
|
如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。
|
||||||
|
|
||||||
|
**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"面试突击"** 即可免费领取!
|
||||||
|
|
||||||
|
**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。
|
||||||
|
|
||||||
|

|
@ -1,429 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 一 面试中关于 synchronized 关键字的 5 连击
|
|
||||||
|
|
||||||
### 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 方法一样,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 的指令重排,保证在多线程环境下也能正常运行。
|
|
||||||
|
|
||||||
### 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 同步语句块的实现使用的是 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 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
|
|
||||||
|
|
||||||
|
|
||||||
### 1.4 说说 JDK1.6 之后的synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗
|
|
||||||
|
|
||||||
JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
|
|
||||||
|
|
||||||
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
|
|
||||||
|
|
||||||
关于这几种优化的详细信息可以查看:[synchronized 关键字使用、底层原理、JDK1.6 之后的底层优化以及 和ReenTrantLock 的对比](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247484539&idx=1&sn=3500cdcd5188bdc253fb19a1bfa805e6&chksm=fd98521acaefdb0c5167247a1fa903a1a53bb4e050b558da574f894f9feda5378ec9d0fa1ac7&token=1604028915&lang=zh_CN#rd)
|
|
||||||
|
|
||||||
### 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是一个不错的选择。
|
|
||||||
|
|
||||||
**④ 性能已不是选择标准**
|
|
||||||
|
|
||||||
# 二 面试中关于线程池的 4 连击
|
|
||||||
|
|
||||||
### 2.1 讲一下Java内存模型
|
|
||||||
|
|
||||||
|
|
||||||
在 JDK1.2 之前,Java的内存模型实现总是从<font color="red">**主存**(即共享内存)读取变量</font>,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变量保存<font color="red">**本地内存**</font>(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成<font color="red">**数据的不一致**</font>。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
要解决这个问题,就需要把变量声明为<font color="red"> **volatile**</font>,这就指示 JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。
|
|
||||||
|
|
||||||
说白了,<font color="red"> **volatile**</font> 关键字的主要作用就是保证变量的可见性然后还有一个作用是防止指令重排序。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
### 2.2 说说 synchronized 关键字和 volatile 关键字的区别
|
|
||||||
|
|
||||||
synchronized关键字和volatile关键字比较
|
|
||||||
|
|
||||||
- **volatile关键字**是线程同步的**轻量级实现**,所以**volatile性能肯定比synchronized关键字要好**。但是**volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块**。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,**实际开发中使用 synchronized 关键字的场景还是更多一些**。
|
|
||||||
- **多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞**
|
|
||||||
- **volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。**
|
|
||||||
- **volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。**
|
|
||||||
|
|
||||||
|
|
||||||
# 三 面试中关于 线程池的 2 连击
|
|
||||||
|
|
||||||
|
|
||||||
### 3.1 为什么要用线程池?
|
|
||||||
|
|
||||||
线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。
|
|
||||||
|
|
||||||
这里借用《Java并发编程的艺术》提到的来说一下使用线程池的好处:
|
|
||||||
|
|
||||||
- **降低资源消耗。** 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
|
|
||||||
- **提高响应速度。** 当任务到达时,任务可以不需要的等到线程创建就能立即执行。
|
|
||||||
- **提高线程的可管理性。** 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
|
|
||||||
|
|
||||||
|
|
||||||
### 3.2 实现Runnable接口和Callable接口的区别
|
|
||||||
|
|
||||||
如果想让线程池执行任务的话需要实现的Runnable接口或Callable接口。 Runnable接口或Callable接口实现类都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor执行。两者的区别在于 Runnable 接口不会返回结果但是 Callable 接口可以返回结果。
|
|
||||||
|
|
||||||
**备注:** 工具类`Executors`可以实现`Runnable`对象和`Callable`对象之间的相互转换。(`Executors.callable(Runnable task)`或`Executors.callable(Runnable task,Object resule)`)。
|
|
||||||
|
|
||||||
### 3.3 执行execute()方法和submit()方法的区别是什么呢?
|
|
||||||
|
|
||||||
1)**`execute()` 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;**
|
|
||||||
|
|
||||||
2)**submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功**,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 `get(long timeout,TimeUnit unit)`方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
|
|
||||||
|
|
||||||
|
|
||||||
### 3.4 如何创建线程池
|
|
||||||
|
|
||||||
《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
|
|
||||||
|
|
||||||
> Executors 返回线程池对象的弊端如下:
|
|
||||||
>
|
|
||||||
> - **FixedThreadPool 和 SingleThreadExecutor** : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。
|
|
||||||
> - **CachedThreadPool 和 ScheduledThreadPool** : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。
|
|
||||||
|
|
||||||
**方式一:通过构造方法实现**
|
|
||||||

|
|
||||||
**方式二:通过Executor 框架的工具类Executors来实现**
|
|
||||||
我们可以创建三种类型的ThreadPoolExecutor:
|
|
||||||
|
|
||||||
- **FixedThreadPool** : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
|
|
||||||
- **SingleThreadExecutor:** 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
|
|
||||||
- **CachedThreadPool:** 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
|
|
||||||
|
|
||||||
对应Executors工具类中的方法如图所示:
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
# 四 面试中关于 Atomic 原子类的 4 连击
|
|
||||||
|
|
||||||
### 4.1 介绍一下Atomic 原子类
|
|
||||||
|
|
||||||
Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
|
|
||||||
|
|
||||||
所以,所谓原子类说简单点就是具有原子/原子操作特征的类。
|
|
||||||
|
|
||||||
|
|
||||||
并发包 `java.util.concurrent` 的原子类都存放在`java.util.concurrent.atomic`下,如下图所示。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### 4.2 JUC 包中的原子类是哪4类?
|
|
||||||
|
|
||||||
**基本类型**
|
|
||||||
|
|
||||||
使用原子的方式更新基本类型
|
|
||||||
|
|
||||||
- AtomicInteger:整形原子类
|
|
||||||
- AtomicLong:长整型原子类
|
|
||||||
- AtomicBoolean :布尔型原子类
|
|
||||||
|
|
||||||
**数组类型**
|
|
||||||
|
|
||||||
使用原子的方式更新数组里的某个元素
|
|
||||||
|
|
||||||
|
|
||||||
- AtomicIntegerArray:整形数组原子类
|
|
||||||
- AtomicLongArray:长整形数组原子类
|
|
||||||
- AtomicReferenceArray :引用类型数组原子类
|
|
||||||
|
|
||||||
**引用类型**
|
|
||||||
|
|
||||||
- AtomicReference:引用类型原子类
|
|
||||||
- AtomicStampedRerence:原子更新引用类型里的字段原子类
|
|
||||||
- AtomicMarkableReference :原子更新带有标记位的引用类型
|
|
||||||
|
|
||||||
**对象的属性修改类型**
|
|
||||||
|
|
||||||
- AtomicIntegerFieldUpdater:原子更新整形字段的更新器
|
|
||||||
- AtomicLongFieldUpdater:原子更新长整形字段的更新器
|
|
||||||
- AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
|
|
||||||
|
|
||||||
|
|
||||||
### 4.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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.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)
|
|
||||||
|
|
||||||
# 五 AQS
|
|
||||||
|
|
||||||
### 5.1 AQS 介绍
|
|
||||||
|
|
||||||
AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。
|
|
||||||
|
|
||||||
### 5.2 AQS 原理分析
|
|
||||||
|
|
||||||
AQS 原理这部分参考了部分博客,在5.2节末尾放了链接。
|
|
||||||
|
|
||||||
> 在面试中被问到并发知识的时候,大多都会被问到“请你说一下自己对于AQS原理的理解”。下面给大家一个示例供大家参加,面试不是背题,大家一定要假如自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来。
|
|
||||||
|
|
||||||
下面大部分内容其实在AQS类注释上已经给出了,不过是英语看着比较吃力一点,感兴趣的话可以看看源码。
|
|
||||||
|
|
||||||
#### 5.2.1 AQS 原理概览
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
**AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。**
|
|
||||||
|
|
||||||
> CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
|
|
||||||
|
|
||||||
看个AQS(AbstractQueuedSynchronizer)原理图:
|
|
||||||
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。
|
|
||||||
|
|
||||||
```java
|
|
||||||
private volatile int state;//共享变量,使用volatile修饰保证线程可见性
|
|
||||||
```
|
|
||||||
|
|
||||||
状态信息通过procted类型的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);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 5.2.2 AQS 对资源的共享方式
|
|
||||||
|
|
||||||
**AQS定义两种资源共享方式**
|
|
||||||
|
|
||||||
- **Exclusive**(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
|
|
||||||
- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
|
|
||||||
- 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
|
|
||||||
- **Share**(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。
|
|
||||||
|
|
||||||
ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。
|
|
||||||
|
|
||||||
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。
|
|
||||||
|
|
||||||
#### 5.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
|
|
||||||
|
|
||||||
### 5.3 AQS 组件总结
|
|
||||||
|
|
||||||
- **Semaphore(信号量)-允许多个线程同时访问:** synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。
|
|
||||||
- **CountDownLatch (倒计时器):** CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。
|
|
||||||
- **CyclicBarrier(循环栅栏):** CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。
|
|
||||||
|
|
||||||
关于AQS这部分的更多内容可以查看我的这篇文章:[并发编程面试必备:AQS 原理以及 AQS 同步组件总结](https://mp.weixin.qq.com/s/joa-yOiTrYF67bElj8xqvg)
|
|
||||||
|
|
||||||
# Reference
|
|
||||||
|
|
||||||
- 《深入理解 Java 虚拟机》
|
|
||||||
- 《实战 Java 高并发程序设计》
|
|
||||||
- 《Java并发编程的艺术》
|
|
||||||
- http://www.cnblogs.com/waterystone/p/4920797.html
|
|
||||||
- https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html
|
|
@ -1,269 +0,0 @@
|
|||||||
# Java 并发基础知识
|
|
||||||
|
|
||||||
Java 并发的基础知识,可能会在笔试中遇到,技术面试中也可能以并发知识环节提问的第一个问题出现。比如面试官可能会问你:“谈谈自己对于进程和线程的理解,两者的区别是什么?”
|
|
||||||
|
|
||||||
**本节思维导图:**
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## 一 进程和线程
|
|
||||||
|
|
||||||
进程和线程的对比这一知识点由于过于基础,所以在面试中很少碰到,但是极有可能会在笔试题中碰到。
|
|
||||||
|
|
||||||
常见的提问形式是这样的:**“什么是线程和进程?,请简要描述线程与进程的关系、区别及优缺点? ”**。
|
|
||||||
|
|
||||||
### 1.1. 何为进程?
|
|
||||||
|
|
||||||
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
|
|
||||||
|
|
||||||
在Java中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。
|
|
||||||
|
|
||||||
如下图所示,在 windows 中通过查看任务管理器的方式,我们就可以清楚看到 window 当前运行的进程(.exe文件的运行)。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### 1.2 何为线程?
|
|
||||||
|
|
||||||
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的**堆**和**方法区**资源,但每个线程有自己的**程序计数器**、**虚拟机栈**和**本地方法栈**,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
|
|
||||||
|
|
||||||
Java 程序天生就是多线程程序,我们可以通过 JMX 来看一下一个普通的 Java 程序有哪些线程,代码如下。
|
|
||||||
|
|
||||||
```java
|
|
||||||
public class MultiThread {
|
|
||||||
public static void main(String[] args) {
|
|
||||||
// 获取Java线程管理MXBean
|
|
||||||
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
|
|
||||||
// 不需要获取同步的monitor和synchronizer信息,仅获取线程和线程堆栈信息
|
|
||||||
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
|
|
||||||
// 遍历线程信息,仅打印线程ID和线程名称信息
|
|
||||||
for (ThreadInfo threadInfo : threadInfos) {
|
|
||||||
System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
上述程序输出如下(输出内容可能不同,不用太纠结下面每个线程的作用,只用知道 main 线程执行main方法即可):
|
|
||||||
|
|
||||||
```
|
|
||||||
[5] Attach Listener //添加事件
|
|
||||||
[4] Signal Dispatcher // 分发处理给JVM信号的线程
|
|
||||||
[3] Finalizer //调用对象finalize方法的线程
|
|
||||||
[2] Reference Handler //清除reference线程
|
|
||||||
[1] main //main线程,程序入口
|
|
||||||
```
|
|
||||||
|
|
||||||
从上面的输出内容可以看出:**一个 Java 程序的运行是 main 线程和多个其他线程同时运行**。
|
|
||||||
|
|
||||||
### 1.3 从 JVM 角度说进程和线程之间的关系(重要)
|
|
||||||
|
|
||||||
#### 1.3.1 图解进程和线程的关系
|
|
||||||
|
|
||||||
下图是 Java 内存区域,通过下图我们从 JVM 的角度来说一下线程和进程之间的关系。如果你对 Java 内存区域(运行时数据区)这部分知识不太了解的话可以阅读一下我的这篇文章:[《可能是把Java内存区域讲的最清楚的一篇文章》](https://github.com/Snailclimb/JavaGuide/blob/master/Java相关/可能是把Java内存区域讲的最清楚的一篇文章.md)
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的**堆**和**方法区**资源,但是每个线程有自己的**程序计数器**、**虚拟机栈** 和 **本地方法栈**。
|
|
||||||
|
|
||||||
下面来思考这样一个问题:为什么**程序计数器**、**虚拟机栈**和**本地方法栈**是线程私有的呢?为什么堆和方法区是线程共享的呢?
|
|
||||||
|
|
||||||
#### 1.3.2 程序计数器为什么是私有的?
|
|
||||||
|
|
||||||
程序计数器主要有下面两个作用:
|
|
||||||
|
|
||||||
1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
|
|
||||||
2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
|
|
||||||
|
|
||||||
需要注意的是,如果执行的是native方法,那么程序计数器记录的是undefined地址,只有执行的是Java代码时程序计数器记录的才是下一条指令的地址。
|
|
||||||
|
|
||||||
所以,程序计数器私有主要是为了**线程切换后能恢复到正确的执行位置**。
|
|
||||||
|
|
||||||
#### 1.3.3 虚拟机栈和本地方法栈为什么是私有的?
|
|
||||||
|
|
||||||
- **虚拟机栈:**每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
|
|
||||||
- **本地方法栈:**和虚拟机栈所发挥的作用非常相似,区别是: **虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。** 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
|
|
||||||
|
|
||||||
所以,为了**保证线程中的局部变量不被别的线程访问到**,虚拟机栈和本地方法栈是线程私有的。
|
|
||||||
|
|
||||||
#### 1.3.4 一句话简单了解堆和方法区
|
|
||||||
|
|
||||||
堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象(所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
|
|
||||||
|
|
||||||
## 二 多线程并发编程
|
|
||||||
|
|
||||||
### 2.1 并发与并行
|
|
||||||
|
|
||||||
- **并发:** 同一时间段,多个任务都在执行(单位时间内不一定同时执行);
|
|
||||||
- **并行:**单位时间内,多个任务同时执行。
|
|
||||||
|
|
||||||
### 2.1 多线程并发编程详解
|
|
||||||
|
|
||||||
单CPU时代多个任务共享一个CPU,某一特定时刻只能有一个任务被执行,CPU会分配时间片给当前要执行的任务。当一个任务占用CPU时,其他任务就会被挂起。当占用CPU的任务的时间片用完后,才会由 CPU 选择下一个需要执行的任务。所以说,在单核CPU时代,多线程编程没有太大意义,反而会因为线程间频繁的上下文切换而带来额外开销。
|
|
||||||
|
|
||||||
但现在 CPU 一般都是多核,如果这个CPU是多核的话,那么进程中的不同线程可以使用不同核心,实现了真正意义上的并行运行。**那为什么我们不直接叫做多线程并行编程呢?**
|
|
||||||
|
|
||||||
**这是因为多线程在实际开发使用中,线程的个数往往多于CPU的个数,所以一般都称多线程并发编程而不是多线程并行编程。`**
|
|
||||||
|
|
||||||
### 2.2 为什么要多线程并发编程?
|
|
||||||
|
|
||||||
- **从计算机底层来说:**线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
|
|
||||||
|
|
||||||
- **从当代互联网发展趋势来说:**现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
|
|
||||||
|
|
||||||
## 三 线程的创建与运行
|
|
||||||
|
|
||||||
前两种实际上很少使用,一般都是用线程池的方式比较多一点。
|
|
||||||
|
|
||||||
### 3.1 继承 Thread 类的方式
|
|
||||||
|
|
||||||
|
|
||||||
```java
|
|
||||||
public class MyThread extends Thread {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
super.run();
|
|
||||||
System.out.println("MyThread");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Run.java
|
|
||||||
|
|
||||||
```java
|
|
||||||
public class Run {
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
|
||||||
MyThread mythread = new MyThread();
|
|
||||||
mythread.start();
|
|
||||||
System.out.println("运行结束");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
运行结果:
|
|
||||||

|
|
||||||
|
|
||||||
从上面的运行结果可以看出:线程是一个子任务,CPU以不确定的方式,或者说是以随机的时间来调用线程中的run方法。
|
|
||||||
|
|
||||||
### 3.2 实现Runnable接口的方式
|
|
||||||
|
|
||||||
推荐实现Runnable接口方式开发多线程,因为Java单继承但是可以实现多个接口。
|
|
||||||
|
|
||||||
MyRunnable.java
|
|
||||||
|
|
||||||
```java
|
|
||||||
public class MyRunnable implements Runnable {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
System.out.println("MyRunnable");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Run.java
|
|
||||||
|
|
||||||
```java
|
|
||||||
public class Run {
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
|
||||||
Runnable runnable=new MyRunnable();
|
|
||||||
Thread thread=new Thread(runnable);
|
|
||||||
thread.start();
|
|
||||||
System.out.println("运行结束!");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
运行结果:
|
|
||||||

|
|
||||||
|
|
||||||
### 3.3 使用线程池的方式
|
|
||||||
|
|
||||||
使用线程池的方式也是最推荐的一种方式,另外,《阿里巴巴Java开发手册》在第一章第六节并发处理这一部分也强调到“线程资源必须通过线程池提供,不允许在应用中自行显示创建线程”。这里就不给大家演示代码了,线程池这一节会详细介绍到这部分内容。
|
|
||||||
|
|
||||||
## 四 线程的生命周期和状态
|
|
||||||
|
|
||||||
Java 线程在运行的生命周期中的指定时刻只可能处于下面6种不同状态的其中一个状态(图源《Java 并发编程艺术》4.1.4节)。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示(图源《Java 并发编程艺术》4.1.4节):
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
由上图可以看出:
|
|
||||||
|
|
||||||
线程创建之后它将处于 **NEW(新建)** 状态,调用 `start()` 方法后开始运行,线程这时候处于 **READY(可运行)** 状态。可运行状态的线程获得了 cpu 时间片(timeslice)后就处于 **RUNNING(运行)** 状态。
|
|
||||||
|
|
||||||
> 操作系统隐藏 Java虚拟机(JVM)中的 RUNNABLE 和 RUNNING 状态,它只能看到 RUNNABLE 状态(图源:[HowToDoInJava](https://howtodoinjava.com/):[Java Thread Life Cycle and Thread States](https://howtodoinjava.com/java/multi-threading/java-thread-life-cycle-and-thread-states/)),所以 Java 系统一般将这两个状态统称为 **RUNNABLE(运行中)** 状态 。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
当线程执行 `wait()`方法之后,线程进入 **WAITING(等待)**状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 **TIME_WAITING(超时等待)** 状态相当于在等待状态的基础上增加了超时限制,比如通过 `sleep(long millis)`方法或 `wait(long millis)`方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 **BLOCKED(阻塞)** 状态。线程在执行 Runnable 的` run() `方法之后将会进入到 **TERMINATED(终止)** 状态。
|
|
||||||
|
|
||||||
## 五 线程优先级
|
|
||||||
|
|
||||||
**理论上**来说系统会根据优先级来决定首先使哪个线程进入运行状态。当 CPU 比较闲的时候,设置线程优先级几乎不会有任何作用,而且很多操作系统压根不会不会理会你设置的线程优先级,所以不要让业务过度依赖于线程的优先级。
|
|
||||||
|
|
||||||
另外,**线程优先级具有继承特性**比如A线程启动B线程,则B线程的优先级和A是一样的。**线程优先级还具有随机性** 也就是说线程优先级高的不一定每一次都先执行完。
|
|
||||||
|
|
||||||
Thread类中包含的成员变量代表了线程的某些优先级。如**Thread.MIN_PRIORITY(常数1)**,**Thread.NORM_PRIORITY(常数5)**,**Thread.MAX_PRIORITY(常数10)**。其中每个线程的优先级都在**1** 到**10** 之间,在默认情况下优先级都是**Thread.NORM_PRIORITY(常数5)**。
|
|
||||||
|
|
||||||
**一般情况下,不会对线程设定优先级别,更不会让某些业务严重地依赖线程的优先级别,比如权重,借助优先级设定某个任务的权重,这种方式是不可取的,一般定义线程的时候使用默认的优先级就好了。**
|
|
||||||
|
|
||||||
**相关方法:**
|
|
||||||
|
|
||||||
```java
|
|
||||||
public final void setPriority(int newPriority) //为线程设定优先级
|
|
||||||
public final int getPriority() //获取线程的优先级
|
|
||||||
```
|
|
||||||
**设置线程优先级方法源码:**
|
|
||||||
|
|
||||||
```java
|
|
||||||
public final void setPriority(int newPriority) {
|
|
||||||
ThreadGroup g;
|
|
||||||
checkAccess();
|
|
||||||
//线程游戏优先级不能小于1也不能大于10,否则会抛出异常
|
|
||||||
if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
|
|
||||||
throw new IllegalArgumentException();
|
|
||||||
}
|
|
||||||
//如果指定的线程优先级大于该线程所在线程组的最大优先级,那么该线程的优先级将设为线程组的最大优先级
|
|
||||||
if((g = getThreadGroup()) != null) {
|
|
||||||
if (newPriority > g.getMaxPriority()) {
|
|
||||||
newPriority = g.getMaxPriority();
|
|
||||||
}
|
|
||||||
setPriority0(priority = newPriority);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## 六 守护线程和用户线程
|
|
||||||
|
|
||||||
**守护线程和用户线程简介:**
|
|
||||||
|
|
||||||
- **用户(User)线程:**运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程
|
|
||||||
- **守护(Daemon)线程:**运行在后台,为其他前台线程服务.也可以说守护线程是JVM中非守护线程的 **“佣人”**。一旦所有用户线程都结束运行,守护线程会随JVM一起结束工作.
|
|
||||||
|
|
||||||
main 函数所在的线程就是一个用户线程啊,main函数启动的同时在JVM内部同时还启动了好多守护线程,比如垃圾回收线程。
|
|
||||||
|
|
||||||
**那么守护线程和用户线程有什么区别呢?**
|
|
||||||
|
|
||||||
比较明显的区别之一是用户线程结束,JVM退出,不管这个时候有没有守护线程运行。而守护线程不会影响 JVM 的退出。
|
|
||||||
|
|
||||||
**注意事项:**
|
|
||||||
|
|
||||||
1. `setDaemon(true)`必须在`start()`方法前执行,否则会抛出 `IllegalThreadStateException` 异常
|
|
||||||
2. 在守护线程中产生的新线程也是守护线程
|
|
||||||
3. 不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑
|
|
||||||
4. 守护(Daemon)线程中不能依靠 finally 块的内容来确保执行关闭或清理资源的逻辑。因为我们上面也说过了一旦所有用户线程都结束运行,守护线程会随JVM一起结束工作,所以守护(Daemon)线程中的finally语句块可能无法被执行。
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 参考
|
|
||||||
|
|
||||||
- 《Java并发编程之美》
|
|
||||||
- 《Java并发编程的艺术》
|
|
||||||
- https://howtodoinjava.com/java/multi-threading/java-thread-life-cycle-and-thread-states/
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user