1
0
mirror of https://github.com/Snailclimb/JavaGuide synced 2025-06-20 22:17:09 +08:00

Merge pull request #1 from Snailclimb/master

merge my forks
This commit is contained in:
wjch 2019-11-30 18:26:58 +08:00 committed by GitHub
commit edd7932e00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1544 additions and 584 deletions

View File

@ -22,7 +22,7 @@
</a >
</p>
推荐使用 https://snailclimb.github.io/JavaGuide/ 在线阅读(访问速度慢的话,请使用 https://snailclimb.gitee.io/javaguide ),在线阅读内容本仓库同步一致。这种方式阅读的优势在于:有侧边栏阅读体验更好Gitee pages 的访问速度相对来说也比较快
推荐使用 https://snailclimb.gitee.io/javaguide 在线阅读,在线阅读内容本仓库同步一致。这种方式阅读的优势在于:阅读体验更好。
## 目录
@ -103,6 +103,8 @@
* [四 类文件结构](docs/java/jvm/类文件结构.md)
* **[五 类加载过程](docs/java/jvm/类加载过程.md)**
* [六 类加载器](docs/java/jvm/类加载器.md)
* **[【待完成】八 最重要的 JVM 参数指南(翻译完善了一半)](docs/java/jvm/最重要的JVM参数指南.md)**
* [九 JVM 配置常用参数和常用 GC 调优策略](docs/java/jvm/GC调优参数.md)
### I/O
@ -298,8 +300,9 @@
- [Github 上热门的 Spring Boot 项目实战推荐](docs/data/spring-boot-practical-projects.md)
### Github 历史榜单
### Github
- [Github 上 Star 数最多的 10 个项目,看完之后很意外!](docs/tools/github/github-star-ranking.md)
- [Java 项目月榜单](docs/github-trending/JavaGithubTrending.md)
***
@ -336,7 +339,7 @@ Markdown 格式参考:[Github Markdown格式](https://guides.github.com/featur
1. 笔记内容大多是手敲,所以难免会有笔误,你可以帮我找错别字。
2. 很多知识点我可能没有涉及到,所以你可以对其他知识点进行补充。
3. 现有的知识点难免存在不完善或者错误,所以你可以对已有知识点修改/补充。
3. 现有的知识点难免存在不完善或者错误,所以你可以对已有知识点进行修改/补充。
### 为什么要做这个开源文档?

View File

@ -0,0 +1,237 @@
最近,当我在做一个项目的时候需要过滤掉重复的 URL ,为了完成这个任务,我学到了一种称为 Bloom Filter (布隆过滤器)的东西,然后我学会了它并写下了这个博客。
下面我们将分为几个方面来介绍布隆过滤器:
1. 什么是布隆过滤器?
2. 布隆过滤器的原理介绍。
3. 布隆过滤器使用场景。
4. 通过 Java 编程手动实现布隆过滤器。
5. 利用Google开源的Guava中自带的布隆过滤器。
6. Redis 中的布隆过滤器。
7. 总结。
### 1.什么是布隆过滤器?
首先,我们需要了解布隆过滤器的概念。
布隆过滤器Bloom Filter是一个叫做 Bloom 的老哥于1970年提出的。我们可以把它看作由二进制向量或者说位数组和一系列随机映射函数哈希函数两部分组成的数据结构。相比于我们平时常用的的 List、Map 、Set 等数据结构,它占用空间更少并且效率更高,但是缺点是其返回的结果是概率性的,而不是非常准确的。理论情况下添加到集合中的元素越多,误报的可能性就越大。并且,存放在布隆过滤器的数据不容易删除。
![布隆过滤器示意图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/布隆过滤器-bit数组.png)
位数组中的每个元素都只占用 1 bit ,并且每个元素只能是 0 或者 1。这样申请一个 100w 个元素的位数组只占用 1000000 / 8 = 125000 B = 15625 byte ≈ 15.3kb 的空间。
总结:**一个名叫 Bloom 的人提出了一种来检索元素是否在给定大集合中的数据结构,这种数据结构是高效且性能很好的,但缺点是具有一定的错误识别率和删除难度。并且,理论情况下,添加到集合中的元素越多,误报的可能性就越大。**
### 2.布隆过滤器的原理介绍
**当一个元素加入布隆过滤器中的时候,会进行如下操作:**
1. 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。
2. 根据得到的哈希值,在位数组中把对应下标的值置为 1。
**当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行如下操作:**
1. 对给定元素再次进行相同的哈希计算;
2. 得到值之后判断位数组中的每个元素是否都为 1如果值都为 1那么说明这个值在布隆过滤器中如果存在一个值不为 1说明该元素不在布隆过滤器中。
举个简单的例子:
![布隆过滤器hash计算](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/布隆过滤器-hash运算.png)
如图所示,当字符串存储要加入到布隆过滤器中时,该字符串首先由多个哈希函数生成不同的哈希值,然后在对应的位数组的下表的元素设置为 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确定该元素不存在于过滤器中。
### 6.Redis 中的布隆过滤器
- https://juejin.im/post/5bc7446e5188255c791b3360
### 8.其他推荐阅读
1. 详解布隆过滤器的原理使用场景和注意事项https://zhuanlan.zhihu.com/p/43263751
2.

View File

@ -257,11 +257,15 @@ Redis 通过 MULTI、EXEC、WATCH 等命令来实现事务(transaction)功能。
### 缓存雪崩和缓存穿透问题解决方案
**缓存雪崩**
#### **缓存雪崩**
**什么是缓存雪崩?**
简介:缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决办法(中华石杉老师在他的视频中提到过,视频地址在最后一个问题中有提到):
**有哪些解决办法?**
(中华石杉老师在他的视频中提到过,视频地址在最后一个问题中有提到):
- 事前:尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略。
- 事中本地ehcache缓存 + hystrix限流&降级避免MySQL崩掉
@ -269,16 +273,46 @@ Redis 通过 MULTI、EXEC、WATCH 等命令来实现事务(transaction)功能。
![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-25/6078367.jpg)
#### **缓存穿透**
**缓存穿透**
**什么是缓存穿透**
简介:一般是黑客故意去请求缓存中不存在的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉
缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。举个例子:某个黑客故意制造我们缓存中不存在的 key 发起大量请求,导致大量请求落到数据库
解决办法: 有很多种方法可以有效地解决缓存穿透问题最常见的则是采用布隆过滤器将所有可能存在的数据哈希到一个足够大的bitmap中一个一定不存在的数据会被 这个bitmap拦截掉从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法我们采用的就是这种如果一个查询返回的数据为空不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
一般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;
}
```
参考:
- [https://blog.csdn.net/zeb_perfect/article/details/54135506](https://blog.csdn.net/zeb_perfect/article/details/54135506)
### 如何解决 Redis 的并发竞争 Key 问题
@ -308,6 +342,11 @@ Redis 通过 MULTI、EXEC、WATCH 等命令来实现事务(transaction)功能。
**参考:** Java工程师面试突击第1季可能是史上最好的Java面试突击课程-中华石杉老师公众号后台回复关键字“1”即可获取该视频内容。
### 参考
- 《Redis开发与运维》
- Redis 命令总结http://redisdoc.com/string/set.html
## 公众号
如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。

View File

@ -1,19 +1,68 @@
相关阅读:
- [史上最全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 集群以及应用
## 集群
### 主从复制
#### 主从链(拓扑结构)
![主从](https://user-images.githubusercontent.com/26766909/67539461-d1a26c00-f714-11e9-81ae-61fa89faf156.png)
![主从](https://user-images.githubusercontent.com/26766909/67539485-e0891e80-f714-11e9-8980-d253239fcd8b.png)
#### 复制模式
- 全量复制:master 全部同步到 slave
- 部分复制:slave 数据丢失进行备份
- 全量复制Master 全部同步到 Slave
- 部分复制Slave 数据丢失进行备份
#### 问题点
- 同步故障
@ -26,124 +75,194 @@
- 优化参数不一致:内存不一致.
- 避免全量复制
- 选择小主节点(分片)、低峰期间操作.
- 如果节点运行 id 不匹配(如主节点重启、运行 id 发送变化),此时要执行全量复制,应该配合哨兵和集群解决.
- 主从复制挤压缓冲区不足产生的问题(网络中断,部分复制无法满足),可增大复制缓冲区( rel_backlog_size 参数).
- 如果节点运行 id 不匹配(如主节点重启、运行 id 发送变化),此时要执行全量复制,应该配合哨兵和集群解决.
- 主从复制挤压缓冲区不足产生的问题(网络中断,部分复制无法满足)可增大复制缓冲区( rel_backlog_size 参数).
- 复制风暴
### 哨兵机制
#### 拓扑图
![image](https://user-images.githubusercontent.com/26766909/67539495-f0086780-f714-11e9-9eab-c11a163ac6c0.png)
#### 节点下线
- 客观下线
- 所有 Sentinel 节点对 Redis 节点失败要达成共识,即超过 quorum 个统一.
- 主观下线
- 即 Sentinel 节点对 Redis 节点失败的偏见,超出超时时间认为 Master 已经宕机.
#### leader选举
- 选举出一个 Sentinel 作为 Leader:集群中至少有三个 Sentinel 节点,但只有其中一个节点可完成故障转移.通过以下命令可以进行失败判定或领导者选举.
- 即 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 节点发送命令,要求设置它为领导者.
1. 收到命令的 Sentinel 节点如果没有同意通过其他 Sentinel 节点发送的命令,则同意该请求,否则拒绝.
1. 如果该 Sentinel 节点发现自己的票数已经超过 Sentinel 集合半数且超过 quorum,则它成为领导者.
1. 如果此过程有多个 Sentinel 节点成为领导者,则等待一段时间再重新进行选举.
1. 每个主观下线的 Sentinel 节点向其他 Sentinel 节点发送命令,要求设置它为领导者.
2. 收到命令的 Sentinel 节点如果没有同意通过其他 Sentinel 节点发送的命令,则同意该请求,否则拒绝。
3. 如果该 Sentinel 节点发现自己的票数已经超过 Sentinel 集合半数且超过 quorum则它成为领导者。
4. 如果此过程有多个 Sentinel 节点成为领导者,则等待一段时间再重新进行选举。
#### 故障转移
- 转移流程
1. Sentinel 选出一个合适的 Slave 作为新的 Master(slaveof no one 命令).
1. 向其余 Slave 发出通知,让它们成为新 Master 的 Slave( parallel-syncs 参数).
1. 等待旧 Master 复活,并使之称为新 Master 的 Slave.
1. 向客户端通知 Master 变化.
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 最高的节点.
1. 选择复制偏移量最大的节点(同步数据最多).
1. 选择 runId 最小的节点.
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 节点、确定主从关系.
- 每 1s 每个 Sentinel 对其他 Sentinel 和 Redis 执行 ping进行心跳检测。
- 每 2s 每个 Sentinel 通过 Master 的 Channel 交换信息(pub - sub)。
- 每 10s 每个 Sentinel 对 Master 和 Slave 执行 info目的是发现 Slave 节点、确定主从关系。
### 分布式集群(Cluster)
#### 拓扑图
![image](https://user-images.githubusercontent.com/26766909/67539510-f8f93900-f714-11e9-9d8d-08afdecff95a.png)
#### 通讯
##### 集中式
> 将集群元数据(节点信息、故障等等)几种存储在某个节点上.
> 将集群元数据(节点信息、故障等等)几种存储在某个节点上。
- 优势
1. 元数据的更新读取具有很强的时效性,元数据修改立即更新
1. 元数据的更新读取具有很强的时效性元数据修改立即更新
- 劣势
1. 数据集中存储
##### Gossip
![image](https://user-images.githubusercontent.com/26766909/67539546-16c69e00-f715-11e9-9891-1e81b6af624c.png)
- [Gossip 协议](https://www.jianshu.com/p/8279d6fd65bb)
#### 寻址分片
##### hash取模
- hash(key)%机器数量
- 问题
1. 机器宕机,造成数据丢失,数据读取失败
1. 机器宕机,造成数据丢失,数据读取失败
1. 伸缩性
##### 一致性hash
- ![image](https://user-images.githubusercontent.com/26766909/67539595-352c9980-f715-11e9-8e4a-9d9c04027785.png)
- 问题
1. 一致性哈希算法在节点太少时,容易因为节点分布不均匀而造成缓存热点的问题。
- 解决方案
- 可以通过引入虚拟节点机制解决:即对每一个节点计算多个 hash每个计算结果位置都放置一个虚拟节点。这样就实现了数据的均匀分布负载均衡。
##### hash槽
- CRC16(key)%16384
-
![image](https://user-images.githubusercontent.com/26766909/67539610-3fe72e80-f715-11e9-8e0d-ea58bc965795.png)
## 使用场景
### 热点数据
### 会话维持 session
存取数据优先从 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):一致性较差,维护成本低.
- 主动更新:代码层面控制生命周期,一致性最好,维护成本高.
- 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、或恶意攻击),同时后端数据库也没有查询到相应的记录、无法添加缓存.
这种状态会一直维持,流量一直打到存储层上,无法利用缓存、还会给存储层带来巨大压力.
>
> 当大量的请求无命中缓存、直接请求到后端数据库(业务代码的 bug、或恶意攻击),同时后端数据库也没有查询到相应的记录、无法添加缓存。
> 这种状态会一直维持,流量一直打到存储层上,无法利用缓存、还会给存储层带来巨大压力。
#### 解决方案
1. 请求无法命中缓存、同时数据库记录为空时在缓存添加该 key 的空对象(设置过期时间),缺点是可能会在缓存中添加大量的空值键(比如遭到恶意攻击或爬虫),而且缓存层和存储层数据短期内不一致;
1. 使用布隆过滤器在缓存层前拦截非法请求、自动为空值添加黑名单(同时可能要为误判的记录添加白名单).但需要考虑布隆过滤器的维护(离线生成/ 实时生成).
2. 使用布隆过滤器在缓存层前拦截非法请求、自动为空值添加黑名单(同时可能要为误判的记录添加白名单).但需要考虑布隆过滤器的维护(离线生成/ 实时生成)。
### 缓存雪崩
> 缓存崩溃时请求会直接落到数据库上,很可能由于无法承受大量的并发请求而崩溃,此时如果只重启数据库,或因为缓存重启后没有数据,新的流量进来很快又会把数据库击倒
>
> 缓存崩溃时请求会直接落到数据库上,很可能由于无法承受大量的并发请求而崩溃,此时如果只重启数据库,或因为缓存重启后没有数据,新的流量进来很快又会把数据库击倒。
#### 出现后应对
- 事前:Redis 高可用,主从 + 哨兵,Redis Cluster,避免全盘崩溃.
- 事中:本地 ehcache 缓存 + hystrix 限流 & 降级,避免数据库承受太多压力.
- 事后:Redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据.
- 事前Redis 高可用,主从 + 哨兵Redis Cluster避免全盘崩溃。
- 事中:本地 ehcache 缓存 + hystrix 限流 & 降级,避免数据库承受太多压力。
- 事后Redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
#### 请求过程
1. 用户请求先访问本地缓存,无命中后再访问 Redis,如果本地缓存和 Redis 都没有再查数据库,并把数据添加到本地缓存和 Redis
1. 由于设置了限流,一段时间范围内超出的请求走降级处理(返回默认值,或给出友情提示).
1. 用户请求先访问本地缓存,无命中后再访问 Redis如果本地缓存和 Redis 都没有再查数据库,并把数据添加到本地缓存和 Redis
2. 由于设置了限流,一段时间范围内超出的请求走降级处理(返回默认值,或给出友情提示)。
[1]: https://promotion.aliyun.com/ntms/act/redishybridstorage.html?spm=5176.54432.1380373.5.41921cf20pcZrZ&aly_as=ArH4VaEb

View File

@ -1,81 +1,79 @@
<!-- MarkdownTOC -->
<!-- TOC -->
- [一 基础篇](#一-基础篇)
- [1. `System.out.println(3|9)`输出什么?](#1-systemoutprintln39输出什么)
- [2. 说一下转发\(Forward\)和重定向\(Redirect\)的区别](#2-说一下转发forward和重定向redirect的区别)
- [3. 在浏览器中输入url地址到显示主页的过程,整个过程会使用哪些协议](#3-在浏览器中输入url地址到显示主页的过程整个过程会使用哪些协议)
- [4. TCP 三次握手和四次挥手](#4-tcp-三次握手和四次挥手)
- [为什么要三次握手](#为什么要三次握手)
- [为什么要传回 SYN](#为什么要传回-syn)
- [传了 SYN,为啥还要传 ACK](#传了-syn为啥还要传-ack)
- [为什么要四次挥手](#为什么要四次挥手)
- [5. IP地址与MAC地址的区别](#5-ip地址与mac地址的区别)
- [6. HTTP请求,响应报文格式](#6-http请求响应报文格式)
- [7. 为什么要使用索引?索引这么多优点,为什么不对表中的每一个列创建一个索引呢?索引是如何提高查询速度的?说一下使用索引的注意事项?Mysql索引主要使用的两种数据结构?什么是覆盖索引?](#7-为什么要使用索引索引这么多优点为什么不对表中的每一个列创建一个索引呢索引是如何提高查询速度的说一下使用索引的注意事项mysql索引主要使用的两种数据结构什么是覆盖索引)
- [8. 进程与线程的区别是什么?进程间的几种通信方式说一下?线程间的几种通信方式知道不?](#8-进程与线程的区别是什么进程间的几种通信方式说一下线程间的几种通信方式知道不)
- [9. 为什么要用单例模式?手写几种线程安全的单例模式?](#9-为什么要用单例模式手写几种线程安全的单例模式)
- [10. 简单介绍一下bean;知道Spring的bean的作用域与生命周期吗?](#10-简单介绍一下bean知道spring的bean的作用域与生命周期吗)
- [11. Spring 中的事务传播行为了解吗?TransactionDefinition 接口中哪五个表示隔离级别的常量?](#11-spring-中的事务传播行为了解吗transactiondefinition-接口中哪五个表示隔离级别的常量)
- [事务传播行为](#事务传播行为)
- [隔离级别](#隔离级别)
- [12. SpringMVC 原理了解吗?](#12-springmvc-原理了解吗)
- [13. Spring AOP IOC 实现原理](#13-spring-aop-ioc-实现原理)
- [1. `System.out.println(3|9)`输出什么?](#1-systemoutprintln39输出什么)
- [2. 说一下转发(Forward)和重定向(Redirect)的区别](#2-说一下转发forward和重定向redirect的区别)
- [3. 在浏览器中输入 url 地址到显示主页的过程,整个过程会使用哪些协议](#3-在浏览器中输入-url-地址到显示主页的过程整个过程会使用哪些协议)
- [4. TCP 三次握手和四次挥手](#4-tcp-三次握手和四次挥手)
- [为什么要三次握手](#为什么要三次握手)
- [为什么要传回 SYN](#为什么要传回-syn)
- [传了 SYN,为啥还要传 ACK](#传了-syn为啥还要传-ack)
- [为什么要四次挥手](#为什么要四次挥手)
- [5. IP 地址与 MAC 地址的区别](#5-ip-地址与-mac-地址的区别)
- [6. HTTP 请求,响应报文格式](#6-http-请求响应报文格式)
- [7. 为什么要使用索引?索引这么多优点,为什么不对表中的每一个列创建一个索引呢?索引是如何提高查询速度的?说一下使用索引的注意事项?Mysql 索引主要使用的两种数据结构?什么是覆盖索引?](#7-为什么要使用索引索引这么多优点为什么不对表中的每一个列创建一个索引呢索引是如何提高查询速度的说一下使用索引的注意事项mysql-索引主要使用的两种数据结构什么是覆盖索引)
- [8. 进程与线程的区别是什么?进程间的几种通信方式说一下?线程间的几种通信方式知道不?](#8-进程与线程的区别是什么进程间的几种通信方式说一下线程间的几种通信方式知道不)
- [9. 为什么要用单例模式?手写几种线程安全的单例模式?](#9-为什么要用单例模式手写几种线程安全的单例模式)
- [10. 简单介绍一下 bean;知道 Spring bean 的作用域与生命周期吗?](#10-简单介绍一下-bean知道-spring--bean-的作用域与生命周期吗)
- [11. Spring 中的事务传播行为了解吗?TransactionDefinition 接口中哪五个表示隔离级别的常量?](#11-spring-中的事务传播行为了解吗transactiondefinition-接口中哪五个表示隔离级别的常量)
- [事务传播行为](#事务传播行为)
- [隔离级别](#隔离级别)
- [12. SpringMVC 原理了解吗?](#12-springmvc-原理了解吗)
- [13. Spring AOP IOC 实现原理](#13-spring-aop-ioc-实现原理)
- [二 进阶篇](#二-进阶篇)
- [1 消息队列MQ的套路](#1-消息队列mq的套路)
- [1.1 介绍一下消息队列MQ的应用场景/使用消息队列的好处](#11-介绍一下消息队列mq的应用场景使用消息队列的好处)
- [1)通过异步处理提高系统性能](#1通过异步处理提高系统性能)
- [2)降低系统耦合性](#2降低系统耦合性)
- [1.2 那么使用消息队列会带来什么问题?考虑过这些问题吗?](#12-那么使用消息队列会带来什么问题考虑过这些问题吗)
- [1.3 介绍一下你知道哪几种消息队列,该如何选择呢?](#13-介绍一下你知道哪几种消息队列该如何选择呢)
- [1.4 关于消息队列其他一些常见的问题展望](#14-关于消息队列其他一些常见的问题展望)
- [2 谈谈 InnoDB 和 MyIsam 两者的区别](#2-谈谈-innodb-和-myisam-两者的区别)
- [2.1 两者的对比](#21-两者的对比)
- [2.2 关于两者的总结](#22-关于两者的总结)
- [3 聊聊 Java 中的集合吧!](#3-聊聊-java-中的集合吧)
- [3.1 ArrayList 与 LinkedList 有什么不同?\(注意加上从数据结构分析的内容\)](#31-arraylist-与-linkedlist-有什么不同注意加上从数据结构分析的内容)
- [3.2 HashMap的底层实现](#32-hashmap的底层实现)
- [1)JDK1.8之前](#1jdk18之前)
- [2)JDK1.8之后](#2jdk18之后)
- [3.3 既然谈到了红黑树,你给我手绘一个出来吧,然后简单讲一下自己对于红黑树的理解](#33-既然谈到了红黑树你给我手绘一个出来吧然后简单讲一下自己对于红黑树的理解)
- [3.4 红黑树这么优秀,为何不直接使用红黑树得了?](#34-红黑树这么优秀为何不直接使用红黑树得了)
- [3.5 HashMap 和 Hashtable 的区别/HashSet 和 HashMap 区别](#35-hashmap-和-hashtable-的区别hashset-和-hashmap-区别)
- [1 消息队列 MQ 的套路](#1-消息队列-mq-的套路)
- [1.1 介绍一下消息队列 MQ 的应用场景/使用消息队列的好处](#11-介绍一下消息队列-mq-的应用场景使用消息队列的好处)
- [1)通过异步处理提高系统性能](#1通过异步处理提高系统性能)
- [2)降低系统耦合性](#2降低系统耦合性)
- [1.2 那么使用消息队列会带来什么问题?考虑过这些问题吗?](#12-那么使用消息队列会带来什么问题考虑过这些问题吗)
- [1.3 介绍一下你知道哪几种消息队列,该如何选择呢?](#13-介绍一下你知道哪几种消息队列该如何选择呢)
- [1.4 关于消息队列其他一些常见的问题展望](#14-关于消息队列其他一些常见的问题展望)
- [2 谈谈 InnoDB 和 MyIsam 两者的区别](#2-谈谈-innodb-和-myisam-两者的区别)
- [2.1 两者的对比](#21-两者的对比)
- [2.2 关于两者的总结](#22-关于两者的总结)
- [3 聊聊 Java 中的集合吧!](#3-聊聊-java-中的集合吧)
- [3.1 Arraylist 与 LinkedList 有什么不同?(注意加上从数据结构分析的内容)](#31-arraylist-与-linkedlist-有什么不同注意加上从数据结构分析的内容)
- [3.2 HashMap 的底层实现](#32-hashmap-的底层实现)
- [1)JDK1.8 之前](#1jdk18-之前)
- [2)JDK1.8 之后](#2jdk18-之后)
- [3.3 既然谈到了红黑树,你给我手绘一个出来吧,然后简单讲一下自己对于红黑树的理解](#33-既然谈到了红黑树你给我手绘一个出来吧然后简单讲一下自己对于红黑树的理解)
- [3.4 红黑树这么优秀,为何不直接使用红黑树得了?](#34-红黑树这么优秀为何不直接使用红黑树得了)
- [3.5 HashMap 和 Hashtable 的区别/HashSet 和 HashMap 区别](#35-hashmap-和-hashtable-的区别hashset-和-hashmap-区别)
- [三 终结篇](#三-终结篇)
- [1. Object类有哪些方法?](#1-object类有哪些方法)
- [1.1 Object类的常见方法总结](#11-object类的常见方法总结)
- [1.2 hashCode与equals](#12-hashcode与equals)
- [1.2.1 hashCode\(\)介绍](#121-hashcode介绍)
- [1.2.2 为什么要有hashCode](#122-为什么要有hashcode)
- [1.2.3 hashCode\(\)与equals\(\)的相关规定](#123-hashcode与equals的相关规定)
- [1.2.4 为什么两个对象有相同的hashcode值,它们也不一定是相等的?](#124-为什么两个对象有相同的hashcode值它们也不一定是相等的)
- [1.3 ==与equals](#13-与equals)
- [2 ConcurrentHashMap 相关问题](#2-concurrenthashmap-相关问题)
- [2.1 ConcurrentHashMap 和 Hashtable 的区别](#21-concurrenthashmap-和-hashtable-的区别)
- [2.2 ConcurrentHashMap线程安全的具体实现方式/底层具体实现](#22-concurrenthashmap线程安全的具体实现方式底层具体实现)
- [JDK1.7\(上面有示意图\)](#jdk17上面有示意图)
- [JDK1.8\(上面有示意图\)](#jdk18上面有示意图)
- [3 谈谈 synchronized 和 ReenTrantLock 的区别](#3-谈谈-synchronized-和-reentrantlock-的区别)
- [4 线程池了解吗?](#4-线程池了解吗)
- [4.1 为什么要用线程池?](#41-为什么要用线程池)
- [4.2 Java 提供了哪几种线程池?他们各自的使用场景是什么?](#42-java-提供了哪几种线程池他们各自的使用场景是什么)
- [Java 主要提供了下面4种线程池](#java-主要提供了下面4种线程池)
- [各种线程池的适用场景介绍](#各种线程池的适用场景介绍)
- [4.3 创建的线程池的方式](#43-创建的线程池的方式)
- [5 Nginx](#5-nginx)
- [5.1 简单介绍一下Nginx](#51-简单介绍一下nginx)
- [反向代理](#反向代理)
- [负载均衡](#负载均衡)
- [动静分离](#动静分离)
- [5.2 为什么要用 Nginx?](#52-为什么要用-nginx)
- [5.3 Nginx 的四个主要组成部分了解吗?](#53-nginx-的四个主要组成部分了解吗)
- [1. Object 类有哪些方法?](#1-object-类有哪些方法)
- [1.1 Object 类的常见方法总结](#11-object-类的常见方法总结)
- [1.2 hashCode equals](#12-hashcode--equals)
- [1.2.1 hashCode()介绍](#121-hashcode介绍)
- [1.2.2 为什么要有 hashCode](#122-为什么要有-hashcode)
- [1.2.3 hashCode()与 equals()的相关规定](#123-hashcode与-equals的相关规定)
- [1.2.4 为什么两个对象有相同的 hashcode 值,它们也不一定是相等的?](#124-为什么两个对象有相同的-hashcode-值它们也不一定是相等的)
- [1.3 ==与 equals](#13-与-equals)
- [2 ConcurrentHashMap 相关问题](#2-concurrenthashmap-相关问题)
- [2.1 ConcurrentHashMap 和 Hashtable 的区别](#21-concurrenthashmap-和-hashtable-的区别)
- [2.2 ConcurrentHashMap 线程安全的具体实现方式/底层具体实现](#22-concurrenthashmap-线程安全的具体实现方式底层具体实现)
- [JDK1.7(上面有示意图)](#jdk17上面有示意图)
- [JDK1.8(上面有示意图)](#jdk18上面有示意图)
- [3 谈谈 synchronized 和 ReentrantLock 的区别](#3-谈谈-synchronized-和-reentrantlock-的区别)
- [4 线程池了解吗?](#4-线程池了解吗)
- [4.1 为什么要用线程池?](#41-为什么要用线程池)
- [4.2 Java 提供了哪几种线程池?他们各自的使用场景是什么?](#42-java-提供了哪几种线程池他们各自的使用场景是什么)
- [Java 主要提供了下面 4 种线程池](#java-主要提供了下面-4-种线程池)
- [各种线程池的适用场景介绍](#各种线程池的适用场景介绍)
- [4.3 创建的线程池的方式](#43-创建的线程池的方式)
- [5 Nginx](#5-nginx)
- [5.1 简单介绍一下 Nginx](#51-简单介绍一下-nginx)
- [反向代理](#反向代理)
- [负载均衡](#负载均衡)
- [动静分离](#动静分离)
- [5.2 为什么要用 Nginx?](#52-为什么要用-nginx)
- [5.3 Nginx 的四个主要组成部分了解吗?](#53-nginx-的四个主要组成部分了解吗)
<!-- /MarkdownTOC -->
<!-- /TOC -->
这些问题是2018年去美团面试的同学被问到的一些常见的问题希望对你有帮助
这些问题是 2018 年去美团面试的同学被问到的一些常见的问题,希望对你有帮助!
# 一 基础篇
## 1. `System.out.println(3|9)`输出什么?
正确答案11。
@ -84,15 +82,15 @@
**&&&**
共同点两者都可做逻辑运算符。它们都表示运算符的两边都是true时结果为true
共同点:两者都可做逻辑运算符。它们都表示运算符的两边都是 true 时,结果为 true
不同点: &也是位运算符。& 表示在运算时两边都会计算,然后再判断;&&表示先运算符号左边的东西然后判断是否为true是true就继续运算右边的然后判断并输出是false就停下来直接输出不会再运行后面的东西。
不同点: &也是位运算符。& 表示在运算时两边都会计算,然后再判断;&&表示先运算符号左边的东西,然后判断是否为 true true 就继续运算右边的然后判断并输出,是 false 就停下来直接输出不会再运行后面的东西。
**|和||**
共同点两者都可做逻辑运算符。它们都表示运算符的两边任意一边为true结果为true两边都不是true结果就为false
共同点:两者都可做逻辑运算符。它们都表示运算符的两边任意一边为 true结果为 true两边都不是 true结果就为 false
不同点:|也是位运算符。| 表示两边都会运算,然后再判断结果;|| 表示先运算符号左边的东西然后判断是否为true是true就停下来直接输出不会再运行后面的东西是false就继续运算右边的然后判断并输出。
不同点:|也是位运算符。| 表示两边都会运算,然后再判断结果;|| 表示先运算符号左边的东西,然后判断是否为 true true 就停下来直接输出不会再运行后面的东西,是 false 就继续运算右边的然后判断并输出。
**回到本题:**
@ -102,50 +100,53 @@
**转发是服务器行为,重定向是客户端行为。**
**转发Forword** 通过RequestDispatcher对象的`forwardHttpServletRequest request,HttpServletResponse response`方法实现的。`RequestDispatcher` 可以通过`HttpServletRequest``getRequestDispatcher()`方法获得。例如下面的代码就是跳转到 login_success.jsp 页面。
**转发Forword** 通过 RequestDispatcher 对象的`forwardHttpServletRequest request,HttpServletResponse response`方法实现的。`RequestDispatcher` 可以通过`HttpServletRequest``getRequestDispatcher()`方法获得。例如下面的代码就是跳转到 login_success.jsp 页面。
```java
request.getRequestDispatcher("login_success.jsp").forward(request, response);
```
**重定向Redirect** 是利用服务器返回的状态码来实现的。客户端浏览器请求服务器的时候服务器会返回一个状态码。服务器通过HttpServletRequestResponse的setStatus(int status)方法设置状态码。如果服务器返回301或者302则浏览器会到新的网址重新请求该资源。
**重定向Redirect** 是利用服务器返回的状态码来实现的。客户端浏览器请求服务器的时候,服务器会返回一个状态码。服务器通过 HttpServletRequestResponse setStatus(int status)方法设置状态码。如果服务器返回 301 或者 302则浏览器会到新的网址重新请求该资源。
1. **从地址栏显示来说**forward是服务器请求资源服务器直接访问目标地址的URL把那个URL的响应内容读取过来然后把这些内容再发给浏览器。浏览器根本不知道服务器发送的内容从哪里来的所以它的地址栏还是原来的地址。redirect是服务端根据逻辑发送一个状态码告诉浏览器重新去请求那个地址。所以地址栏显示的是新的URL。
2. **从数据共享来说**forward转发页面和转发到的页面可以共享request里面的数据。redirect不能共享数据。
1. **从地址栏显示来说**forward 是服务器请求资源,服务器直接访问目标地址的 URL把那个 URL 的响应内容读取过来然后把这些内容再发给浏览器。浏览器根本不知道服务器发送的内容从哪里来的所以它的地址栏还是原来的地址。redirect 是服务端根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址。所以地址栏显示的是新的 URL。
2. **从数据共享来说**forward转发页面和转发到的页面可以共享 request 里面的数据。redirect不能共享数据。
3. **从运用地方来说**forward一般用于用户登陆的时候根据角色转发到相应的模块。redirect一般用于用户注销登陆时返回主页面和跳转到其它的网站等。
4. **从效率来说**forward高。redirect低。
## 3. 在浏览器中输入 url 地址到显示主页的过程,整个过程会使用哪些协议
## 3. 在浏览器中输入url地址到显示主页的过程,整个过程会使用哪些协议
图片来源:《图解 HTTP》
图片来源《图解HTTP》
![状态码](https://user-gold-cdn.xitu.io/2018/4/19/162db5e985aabdbe?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)
![各种网络请求用到的协议](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/各种网络请求用到的协议.jpg)
总体来说分为以下几个过程:
1. DNS解析
2. TCP连接
3. 发送HTTP请求
4. 服务器处理请求并返回HTTP报文
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](https://segmentfault.com/a/1190000006879700 "https://segmentfault.com/a/1190000006879700")
> 修正 [issue-568](https://github.com/Snailclimb/JavaGuide/issues/568 "issue-568"):上图中 IP 数据包在路由器之间使用的协议为 OPSF 协议错误,应该为 OSPF 协议 。
>
> IP 数据包在路由器之间传播大致分为 IGP 和 BGP 协议,而 IGP 目前主流为 OSPF 协议,思科,华为和 H3C 等主流厂商都有各自实现并使用BGP 协议为不同 AS自治系统号间路由传输也分为 I-BGP 和 E-BGP详细资料请查看《TCP/IP 卷一》
## 4. TCP 三次握手和四次挥手
为了准确无误地把数据送达目标处TCP协议采用了三次握手策略。
为了准确无误地把数据送达目标处TCP 协议采用了三次握手策略。
**漫画图解:**
图片来源《图解HTTP》
![TCP三次握手](https://user-gold-cdn.xitu.io/2018/5/8/1633e127396541f1?w=864&h=439&f=png&s=226095)
图片来源:《图解 HTTP》
<img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/tcp三次握手.jpg" style="zoom:50%;" />
**简单示意图:**
![TCP三次握手](https://user-gold-cdn.xitu.io/2018/5/8/1633e14233d95972?w=542&h=427&f=jpeg&s=15088)
<img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/tcp三次握手2.jpg" style="zoom:50%;" />
- 客户端–发送带有 SYN 标志的数据包–一次握手–服务端
- 服务端–发送带有 SYN/ACK 标志的数据包–二次握手–客户端
@ -164,63 +165,54 @@ request.getRequestDispatcher("login_success.jsp").forward(request, response);
所以三次握手就能确认双发收发功能都正常,缺一不可。
#### 为什么要传回 SYN
接收端传回发送端所发送的 SYN 是为了告诉发送端,我接收到的信息确实就是你所发送的信号了。
> SYN 是 TCP/IP 建立连接时使用的握手信号。在客户机和服务器之间建立正常的 TCP 网络连接时,客户机首先发出一个 SYN 消息,服务器使用 SYN-ACK 应答表示接收到了这个消息,最后客户机再以 ACK(Acknowledgement[汉译:确认字符 ,在数据通信传输中,接收站发给发送站的一种传输控制字符。它表示确认发来的数据已经接受无误。 ]消息响应。这样在客户机和服务器之间才能建立起可靠的TCP连接数据才可以在客户机和服务器之间传递。
> SYN 是 TCP/IP 建立连接时使用的握手信号。在客户机和服务器之间建立正常的 TCP 网络连接时,客户机首先发出一个 SYN 消息,服务器使用 SYN-ACK 应答表示接收到了这个消息,最后客户机再以 ACK(Acknowledgement[汉译:确认字符 ,在数据通信传输中,接收站发给发送站的一种传输控制字符。它表示确认发来的数据已经接受无误。 ])消息响应。这样在客户机和服务器之间才能建立起可靠的 TCP 连接,数据才可以在客户机和服务器之间传递。
#### 传了 SYN,为啥还要传 ACK
双方通信无误必须是两者互相发送信息都无误。传了 SYN证明发送方主动关闭方到接收方被动关闭方的通道没有问题但是接收方到发送方的通道还需要 ACK 信号来进行验证。
![TCP四次挥手](https://user-gold-cdn.xitu.io/2018/5/8/1633e1676e2ac0a3?w=500&h=340&f=jpeg&s=13406)
断开一个 TCP 连接则需要“四次挥手”:
- 客户端-发送一个 FIN用来关闭客户端到服务器的数据传送
- 服务器-收到这个 FIN它发回一 个 ACK确认序号为收到的序号加1 。和 SYN 一样,一个 FIN 将占用一个序号
- 服务器-关闭与客户端的连接发送一个FIN给客户端
- 客户端-发回 ACK 报文确认并将确认序号设置为收到序号加1
- 服务器-收到这个 FIN它发回一 个 ACK确认序号为收到的序号加 1 。和 SYN 一样,一个 FIN 将占用一个序号
- 服务器-关闭与客户端的连接,发送一个 FIN 给客户端
- 客户端-发回 ACK 报文确认,并将确认序号设置为收到序号加 1
#### 为什么要四次挥手
#### 为什么要四次挥手
任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后就完全关闭了 TCP 连接。
任何一方都可以在数据传送结束后发出连接释放的通知待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候则发出连接释放通知对方确认后就完全关闭了TCP连接
举个例子A 和 B 打电话通话即将结束后A 说“我没啥要说的了”B 回答“我知道了”,但是 B 可能还会有要说的话A 不能要求 B 跟着自己的节奏结束通话,于是 B 可能又巴拉巴拉说了一通,最后 B 说“我说完了”A 回答“知道了”,这样通话才算结束
举个例子A 和 B 打电话通话即将结束后A 说“我没啥要说的了”B回答“我知道了”但是 B 可能还会有要说的话A 不能要求 B 跟着自己的节奏结束通话,于是 B 可能又巴拉巴拉说了一通,最后 B 说“我说完了”A 回答“知道了”,这样通话才算结束。
上面讲的比较概括,推荐一篇讲的比较细致的文章:[https://blog.csdn.net/qzcsu/article/details/72861891](https://blog.csdn.net/qzcsu/article/details/72861891 "https://blog.csdn.net/qzcsu/article/details/72861891")
上面讲的比较概括,推荐一篇讲的比较细致的文章:[https://blog.csdn.net/qzcsu/article/details/72861891](https://blog.csdn.net/qzcsu/article/details/72861891)
## 5. IP 地址与 MAC 地址的区别
参考:[https://blog.csdn.net/guoweimelon/article/details/50858597](https://blog.csdn.net/guoweimelon/article/details/50858597 "https://blog.csdn.net/guoweimelon/article/details/50858597")
IP 地址是指互联网协议地址Internet Protocol AddressIP Address 的缩写。IP 地址是 IP 协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。
## 5. IP地址与MAC地址的区别
MAC 地址又称为物理地址、硬件地址用来定义网络设备的位置。网卡的物理地址通常是由网卡生产厂家写入网卡的具有全球唯一性。MAC 地址用于在网络中唯一标示一个网卡,一台电脑会有一或多个网卡,每个网卡都需要有一个唯一的 MAC 地址。
参考:[https://blog.csdn.net/guoweimelon/article/details/50858597](https://blog.csdn.net/guoweimelon/article/details/50858597)
## 6. HTTP 请求,响应报文格式
IP地址是指互联网协议地址Internet Protocol AddressIP Address的缩写。IP地址是IP协议提供的一种统一的地址格式它为互联网上的每一个网络和每一台主机分配一个逻辑地址以此来屏蔽物理地址的差异。
HTTP 请求报文主要由请求行、请求头部、请求正文 3 部分组成
HTTP 响应报文主要由状态行、响应头部、响应正文 3 部分组成
详细内容可以参考:[https://blog.csdn.net/a19881029/article/details/14002273](https://blog.csdn.net/a19881029/article/details/14002273 "https://blog.csdn.net/a19881029/article/details/14002273")
MAC 地址又称为物理地址、硬件地址用来定义网络设备的位置。网卡的物理地址通常是由网卡生产厂家写入网卡的具有全球唯一性。MAC地址用于在网络中唯一标示一个网卡一台电脑会有一或多个网卡每个网卡都需要有一个唯一的MAC地址。
## 6. HTTP请求,响应报文格式
HTTP请求报文主要由请求行、请求头部、请求正文3部分组成
HTTP响应报文主要由状态行、响应头部、响应正文3部分组成
详细内容可以参考:[https://blog.csdn.net/a19881029/article/details/14002273](https://blog.csdn.net/a19881029/article/details/14002273)
## 7. 为什么要使用索引?索引这么多优点,为什么不对表中的每一个列创建一个索引呢?索引是如何提高查询速度的?说一下使用索引的注意事项?Mysql索引主要使用的两种数据结构?什么是覆盖索引?
## 7. 为什么要使用索引?索引这么多优点,为什么不对表中的每一个列创建一个索引呢?索引是如何提高查询速度的?说一下使用索引的注意事项?Mysql 索引主要使用的两种数据结构?什么是覆盖索引?
**为什么要使用索引?**
1. 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
2. 可以大大加快 数据的检索速度(大大减少的检索的数据量), 这也是创建索引的最主要的原因。
2. 可以大大加快 数据的检索速度(大大减少的检索的数据量), 这也是创建索引的最主要的原因。
3. 帮助服务器避免排序和临时表
4. 将随机IO变为顺序IO
4. 将随机 IO 变为顺序 IO
5. 可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。
**索引这么多优点,为什么不对表中的每一个列创建一个索引呢?**
@ -236,37 +228,36 @@ HTTP响应报文主要由状态行、响应头部、响应正文3部分组成
**说一下使用索引的注意事项**
1. 避免 where 子句中对字段施加函数,这会造成无法命中索引。
2. 在使用InnoDB时使用与业务无关的自增主键作为主键即使用逻辑主键而不要使用业务主键。
2. 在使用 InnoDB 时使用与业务无关的自增主键作为主键,即使用逻辑主键,而不要使用业务主键。
3. 将打算加索引的列设置为 NOT NULL ,否则将导致引擎放弃使用索引而进行全表扫描
4. 删除长期未使用的索引,不用的索引的存在会造成不必要的性能损耗 MySQL 5.7 可以通过查询 sys 库的 schema_unused_indexes 视图来查询哪些索引从未被使用
5. 在使用 limit offset 查询缓慢时,可以借助索引来提高性能
**Mysql索引主要使用的哪两种数据结构**
**Mysql 索引主要使用的哪两种数据结构?**
- 哈希索引对于哈希索引来说底层的数据结构就是哈希表因此在绝大多数需求为单条记录查询的时候可以选择哈希索引查询性能最快其余大部分场景建议选择BTree索引。
- BTree索引Mysql的BTree索引使用的是B树中的B+Tree。但对于主要的两种存储引擎MyISAM和InnoDB的实现方式是不同的。
- 哈希索引:对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择 BTree 索引。
- BTree 索引Mysql BTree 索引使用的是 B 树中的 B+Tree。但对于主要的两种存储引擎MyISAM InnoDB的实现方式是不同的。
更多关于索引的内容可以查看我的这篇文章:[【思维导图-索引篇】搞定数据库索引就是这么简单](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247484486&idx=1&sn=215450f11e042bca8a58eac9f4a97686&chksm=fd985227caefdb3117b8375f150676f5824aa20d1ebfdbcfb93ff06e23e26efbafae6cf6b48e&token=1990180468&lang=zh_CN#rd)
**什么是覆盖索引?**
如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称
之为“覆盖索引”。我们知道在InnoDB存储引擎中如果不是主键索引叶子节点存储的是主键+列值。最终还是要“回表”,也就是要通过主键再查找一次,这样就会比较慢。覆盖索引就是把要查询出的列和索引是对应的,不做回表操作!
之为“覆盖索引”。我们知道在 InnoDB 存储引擎中,如果不是主键索引,叶子节点存储的是主键+列值。最终还是要“回表”,也就是要通过主键再查找一次,这样就会比较慢。覆盖索引就是把要查询出的列和索引是对应的,不做回表操作!
## 8. 进程与线程的区别是什么?进程间的几种通信方式说一下?线程间的几种通信方式知道不?
**进程与线程的区别是什么?**
**进程与线程的区别是什么?**
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。另外,也正是因为共享资源,所以线程中执行时一般都要进行同步和互斥。总的来说,进程和线程的主要差别在于它们是不同的操作系统资源管理方式。
**进程间的几种通信方式说一下?**
1. **管道pipe**管道是一种半双工的通信方式数据只能单向流动而且只能在具有血缘关系的进程间使用。进程的血缘关系通常指父子进程关系。管道分为pipe无名管道和fifo命名管道两种有名管道也是半双工的通信方式但是它允许无亲缘关系进程间通信。
1. **管道pipe**:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有血缘关系的进程间使用。进程的血缘关系通常指父子进程关系。管道分为 pipe无名管道和 fifo命名管道两种有名管道也是半双工的通信方式但是它允许无亲缘关系进程间通信。
2. **信号量semophore**:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它通常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
3. **消息队列message queue**:消息队列是由消息组成的链表,存放在内核中 并由消息队列标识符标识。消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。消息队列与管道通信相比,其优势是对每个消息指定特定的消息类型,接收的时候不需要按照队列次序,而是可以根据自定义条件接收特定类型的消息。
4. **信号signal**:信号是一种比较复杂的通信方式,用于通知接收进程某一事件已经发生。
5. **共享内存shared memory**共享内存就是映射一段能被其他进程所访问的内存这段共享内存由一个进程创建但多个进程都可以访问共享内存是最快的IPC方式它是针对其他进程间的通信方式运行效率低而专门设计的。它往往与其他通信机制如信号量配合使用来实现进程间的同步和通信。
5. **共享内存shared memory**:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问,共享内存是最快的 IPC 方式,它是针对其他进程间的通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量配合使用,来实现进程间的同步和通信。
6. **套接字socket**socket即套接字是一种通信机制凭借这种机制客户/服务器(即要进行通信的进程)系统的开发工作既可以在本地单机上进行,也可以跨网络进行。也就是说它可以让不在同一台计算机但通过网络连接计算机上的进程进行通信。也因为这样,套接字明确地将客户端和服务器区分开来。
**线程间的几种通信方式知道不?**
@ -333,23 +324,22 @@ public class Singleton {
}
```
## 10. 简单介绍一下bean;知道Spring的bean的作用域与生命周期吗?
## 10. 简单介绍一下 bean;知道 Spring bean 的作用域与生命周期吗?
在 Spring 中,那些组成应用程序的主体及由 Spring IOC 容器所管理的对象,被称之为 bean。简单地讲bean 就是由 IOC 容器初始化、装配及管理的对象除此之外bean 就与应用程序中的其他对象没有什么区别了。而 bean 的定义以及 bean 相互间的依赖关系将通过配置元数据来描述。
Spring中的bean默认都是单例的这些单例Bean在多线程程序下如何保证线程安全呢 例如对于Web应用来说Web容器对于每个用户请求都创建一个单独的Sevlet线程来处理请求引入Spring框架之后每个Action都是单例的那么对于Spring托管的单例Service Bean如何保证其安全呢 Spring的单例是基于BeanFactory也就是Spring容器的单例Bean在此容器内只有一个Java的单例是基于 JVM每个 JVM 内只有一个实例。
Spring 中的 bean 默认都是单例的,这些单例 Bean 在多线程程序下如何保证线程安全呢? 例如对于 Web 应用来说Web 容器对于每个用户请求都创建一个单独的 Sevlet 线程来处理请求,引入 Spring 框架之后,每个 Action 都是单例的,那么对于 Spring 托管的单例 Service Bean如何保证其安全呢 Spring 的单例是基于 BeanFactory 也就是 Spring 容器的,单例 Bean 在此容器内只有一个Java 的单例是基于 JVM每个 JVM 内只有一个实例。
![pring的bean的作用域](https://user-gold-cdn.xitu.io/2018/11/10/166fd45773d5dd2e?w=563&h=299&f=webp&s=27930)
Spring的bean的生命周期以及更多内容可以查看[一文轻松搞懂Spring中bean的作用域与生命周期](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247484400&idx=2&sn=7201eb365102fce017f89cb3527fb0bc&chksm=fd985591caefdc872a2fac897288119f94c345e4e12150774f960bf5f816b79e4b9b46be3d7f&token=1990180468&lang=zh_CN#rd)
Spring 的 bean 的生命周期以及更多内容可以查看:[一文轻松搞懂 Spring 中 bean 的作用域与生命周期](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247484400&idx=2&sn=7201eb365102fce017f89cb3527fb0bc&chksm=fd985591caefdc872a2fac897288119f94c345e4e12150774f960bf5f816b79e4b9b46be3d7f&token=1990180468&lang=zh_CN#rd)
## 11. Spring 中的事务传播行为了解吗?TransactionDefinition 接口中哪五个表示隔离级别的常量?
#### 事务传播行为
事务传播行为(为了解决业务层方法之间互相调用的事务问题):
当事务方法被另一个事务方法调用时必须指定事务应该如何传播。例如方法可能继续在现有事务中运行也可能开启一个新事务并在自己的事务中运行。在TransactionDefinition定义中包括了如下几个表示传播行为的常量
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。在 TransactionDefinition 定义中包括了如下几个表示传播行为的常量:
**支持当前事务的情况:**
@ -365,18 +355,17 @@ Spring的bean的生命周期以及更多内容可以查看[一文轻松搞懂
**其他情况:**
- TransactionDefinition.PROPAGATION_NESTED 如果当前存在事务则创建一个事务作为当前事务的嵌套事务来运行如果当前没有事务则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。
- TransactionDefinition.PROPAGATION_NESTED 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 TransactionDefinition.PROPAGATION_REQUIRED。
#### 隔离级别
TransactionDefinition 接口中定义了五个表示隔离级别的常量:
- **TransactionDefinition.ISOLATION_DEFAULT:** 使用后端数据库默认的隔离级别Mysql 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别.
- **TransactionDefinition.ISOLATION_DEFAULT:** 使用后端数据库默认的隔离级别Mysql 默认采用的 REPEATABLE_READ 隔离级别 Oracle 默认采用的 READ_COMMITTED 隔离级别.
- **TransactionDefinition.ISOLATION_READ_UNCOMMITTED:** 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
- **TransactionDefinition.ISOLATION_READ_COMMITTED:** 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
- **TransactionDefinition.ISOLATION_REPEATABLE_READ:** 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
- **TransactionDefinition.ISOLATION_SERIALIZABLE:** 最高的隔离级别完全服从ACID的隔离级别。所有的事务依次逐个执行这样事务之间就完全不可能产生干扰也就是说该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
- **TransactionDefinition.ISOLATION_READ_COMMITTED:** 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
- **TransactionDefinition.ISOLATION_REPEATABLE_READ:** 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
- **TransactionDefinition.ISOLATION_SERIALIZABLE:** 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
## 12. SpringMVC 原理了解吗?
@ -390,77 +379,77 @@ TransactionDefinition 接口中定义了五个表示隔离级别的常量:
过了秋招挺长一段时间了,说实话我自己也忘了如何简要概括 Spring AOP IOC 实现原理,就在网上找了一个较为简洁的答案,下面分享给各位。
**IOC** 控制反转也叫依赖注入。IOC利用java反射机制AOP利用代理模式。IOC 概念看似很抽象但是很容易理解。说简单点就是将对象交给容器管理你只需要在spring配置文件中配置对应的bean以及设置相关的属性让spring容器来生成类的实例对象以及管理对象。在spring容器启动的时候spring会把你在配置文件中配置的bean都初始化好然后在你需要调用的时候就把它已经初始化好的那些bean分配给你需要调用这些bean的类。
**AOP** 面向切面编程。Aspect-Oriented Programming 。AOP可以说是对OOP的补充和完善。OOP引入封装、继承和多态性等概念来建立一种对象层次结构用以模拟公共行为的一个集合。实现AOP的技术主要分为两大类一是采用动态代理技术利用截取消息的方式对该消息进行装饰以取代原有对象行为的执行二是采用静态织入的方式引入特定的语法创建“方面”从而使得编译器可以在编译期间织入有关“方面”的代码属于静态代理。
**IOC** 控制反转也叫依赖注入。IOC 利用 java 反射机制AOP 利用代理模式。IOC 概念看似很抽象,但是很容易理解。说简单点就是将对象交给容器管理,你只需要在 spring 配置文件中配置对应的 bean 以及设置相关的属性,让 spring 容器来生成类的实例对象以及管理对象。在 spring 容器启动的时候spring 会把你在配置文件中配置的 bean 都初始化好,然后在你需要调用的时候,就把它已经初始化好的那些 bean 分配给你需要调用这些 bean 的类。
**AOP** 面向切面编程。Aspect-Oriented Programming 。AOP 可以说是对 OOP 的补充和完善。OOP 引入封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。实现 AOP 的技术,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码,属于静态代理。
# 二 进阶篇
## 1 消息队列MQ的套路
## 1 消息队列 MQ 的套路
消息队列/消息中间件应该是Java程序员必备的一个技能了如果你之前没接触过消息队列的话建议先去百度一下某某消息队列入门然后花2个小时就差不多可以学会任何一种消息队列的使用了。如果说仅仅学会使用是万万不够的在实际生产环境还要考虑消息丢失等等情况。关于消息队列面试相关的问题推荐大家也可以看一下视频《Java工程师面试突击第1季-中华石杉老师》如果大家没有资源的话可以在我的公众号“Java面试通关手册”后台回复关键字“1”即可
消息队列/消息中间件应该是 Java 程序员必备的一个技能了,如果你之前没接触过消息队列的话,建议先去百度一下某某消息队列入门,然后花 2 个小时就差不多可以学会任何一种消息队列的使用了。如果说仅仅学会使用是万万不够的在实际生产环境还要考虑消息丢失等等情况。关于消息队列面试相关的问题推荐大家也可以看一下视频《Java 工程师面试突击第 1 季-中华石杉老师》如果大家没有资源的话可以在我的公众号“Java 面试通关手册”后台回复关键字“1”即可
### 1.1 介绍一下消息队列MQ的应用场景/使用消息队列的好处
### 1.1 介绍一下消息队列 MQ 的应用场景/使用消息队列的好处
面试官一般会先问你这个问题预热一下看你知道消息队列不一般在第一面的时候面试官可能只会问消息队列MQ的应用场景/使用消息队列的好处、使用消息队列会带来什么问题、消息队列的技术选型这几个问题,不会太深究下去,在后面的第二轮/第三轮技术面试中可能会深入问一下。
面试官一般会先问你这个问题,预热一下,看你知道消息队列不,一般在第一面的时候面试官可能只会问消息队列 MQ 的应用场景/使用消息队列的好处、使用消息队列会带来什么问题、消息队列的技术选型这几个问题,不会太深究下去,在后面的第二轮/第三轮技术面试中可能会深入问一下。
**《大型网站技术架构》第四章和第七章均有提到消息队列对应用性能及扩展性的提升。**
#### 1)通过异步处理提高系统性能
![通过异步处理提高系统性能](https://user-gold-cdn.xitu.io/2018/4/21/162e63a8e34ba534?w=910&h=350&f=jpeg&s=29123)
![通过异步处理提高系统性能](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/通过异步处理提高系统性能.jpg)
如上图,**在不使用消息队列服务器的时候,用户的请求数据直接写入数据库,在高并发的情况下数据库压力剧增,使得响应速度变慢。但是在使用消息队列之后,用户的请求数据发送给消息队列之后立即 返回,再由消息队列的消费者进程从消息队列中获取数据,异步写入数据库。由于消息队列服务器处理速度快于数据库(消息队列也比数据库有更好的伸缩性),因此响应速度得到大幅改善。**
通过以上分析我们可以得出**消息队列具有很好的削峰作用的功能**——即**通过异步处理,将短时间高并发产生的事务消息存储在消息队列中,从而削平高峰期的并发事务。** 举例:在电子商务一些秒杀、促销活动中,合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击。如下图所示:
![合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击](https://user-gold-cdn.xitu.io/2018/4/21/162e64583dd3ed01?w=780&h=384&f=jpeg&s=13550)
![合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击.jpg)
因为**用户请求数据写入消息队列之后就立即返回给用户了,但是请求数据在后续的业务校验、写数据库等操作中可能失败**。因此使用消息队列进行异步处理之后,需要**适当修改业务流程进行配合**,比如**用户在提交订单之后,订单数据写入消息队列,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进程真正处理完该订单之后,甚至出库后,再通过电子邮件或短信通知用户订单成功**,以免交易纠纷。这就类似我们平时手机订火车票和电影票。
#### 2)降低系统耦合性
我们知道模块分布式部署以后聚合方式通常有两种1.**分布式消息队列**和2.**分布式服务**。
我们知道模块分布式部署以后聚合方式通常有两种1.**分布式消息队列**和 2.**分布式服务**。
> **先来简单说一下分布式服务:**
目前使用比较多的用来构建**SOAService Oriented Architecture面向服务体系结构**的**分布式服务框架**是阿里巴巴开源的**Dubbo**。如果想深入了解Dubbo的可以看我写的关于Dubbo的这一篇文章**《高性能优秀的服务框架-dubbo介绍》**[https://juejin.im/post/5acadeb1f265da2375072f9c](https://juejin.im/post/5acadeb1f265da2375072f9c)
目前使用比较多的用来构建**SOAService Oriented Architecture 面向服务体系结构)**的**分布式服务框架**是阿里巴巴开源的**Dubbo**。如果想深入了解 Dubbo 的可以看我写的关于 Dubbo 的这一篇文章:**《高性能优秀的服务框架-dubbo 介绍》**[https://juejin.im/post/5acadeb1f265da2375072f9c](https://juejin.im/post/5acadeb1f265da2375072f9c "https://juejin.im/post/5acadeb1f265da2375072f9c")
> **再来谈我们的分布式消息队列:**
我们知道如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性无疑更好一些。
我们最常见的**事件驱动架构**类似生产者消费者模式,在大型网站中通常用利用消息队列实现事件驱动结构。如下图所示:
![利用消息队列实现事件驱动结构](https://user-gold-cdn.xitu.io/2018/4/21/162e6665fa394b3b?w=790&h=290&f=jpeg&s=14946)
![利用消息队列实现事件驱动结构](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/利用消息队列实现事件驱动结构.jpg)
**消息队列使利用发布-订阅模式工作,消息发送者(生产者)发布消息,一个或多个消息接受者(消费者)订阅消息。** 从上图可以看到**消息发送者(生产者)和消息接受者(消费者)之间没有直接耦合**,消息发送者将消息发送至分布式消息队列即结束对消息的处理,消息接受者从分布式消息队列获取该消息后进行后续处理,并不需要知道该消息从何而来。**对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,从而实现网站业务的可扩展性设计**。
消息接受者对消息进行过滤、处理、包装后,构造成一个新的消息类型,将消息继续发送出去,等待其他消息接受者订阅该消息。因此基于事件(消息对象)驱动的业务架构可以是一系列流程。
**另外为了避免消息队列服务器宕机造成消息丢失,会将成功发送到消息队列的消息存储在消息生产者服务器上,等消息真正被消费者服务器处理后才删除消息。在消息队列服务器宕机后,生产者服务器会选择分布式消息队列服务器集群中的其他服务器发布消息。**
**备注:** 不要认为消息队列只能利用发布-订阅模式工作,只不过在解耦这个特定业务环境下是使用发布-订阅模式的,**比如在我们的ActiveMQ消息队列中还有点对点工作模式**,具体的会在后面的文章给大家详细介绍,这一篇文章主要还是让大家对消息队列有一个更透彻的了解。
**备注:** 不要认为消息队列只能利用发布-订阅模式工作,只不过在解耦这个特定业务环境下是使用发布-订阅模式的,**比如在我们的 ActiveMQ 消息队列中还有点对点工作模式**,具体的会在后面的文章给大家详细介绍,这一篇文章主要还是让大家对消息队列有一个更透彻的了解。
> 这个问题一般会在上一个问题问完之后,紧接着被问到。“使用消息队列会带来什么问题?”这个问题要引起重视,一般我们都会考虑使用消息队列会带来的好处而忽略它带来的问题!
### 1.2 那么使用消息队列会带来什么问题?考虑过这些问题吗?
- **系统可用性降低:** 系统可用性在某种程度上降低为什么这样说呢在加入MQ之前你不用考虑消息丢失或者说MQ挂掉等等的情况但是引入MQ之后你就需要去考虑了
- **系统复杂性提高:** 加入MQ之后你需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺序性等等问题
- **系统可用性降低:** 系统可用性在某种程度上降低,为什么这样说呢?在加入 MQ 之前,你不用考虑消息丢失或者说 MQ 挂掉等等的情况,但是,引入 MQ 之后你就需要去考虑了!
- **系统复杂性提高:** 加入 MQ 之后,你需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺序性等等问题!
- **一致性问题:** 我上面讲了消息队列可以实现异步,消息队列带来的异步确实可以提高系统响应速度。但是,万一消息的真正消费者并没有正确消费消息怎么办?这样就会导致数据不一致的情况了!
> 了解下面这个问题是为了我们更好的进行技术选型该部分摘自《Java工程师面试突击第1季-中华石杉老师》如果大家没有资源的话可以在我的公众号“Java面试通关手册”后台回复关键字“1”即可
> 了解下面这个问题是为了我们更好的进行技术选型该部分摘自《Java 工程师面试突击第 1 季-中华石杉老师》如果大家没有资源的话可以在我的公众号“Java 面试通关手册”后台回复关键字“1”即可
### 1.3 介绍一下你知道哪几种消息队列,该如何选择呢?
| 特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka |
| :----------------------- | -----------------------------------------------------------: | -----------------------------------------------------------: | -----------------------------------------------------------: | -----------------------------------------------------------: |
| 单机吞吐量 | 万级,吞吐量比 RocketMQ 和 Kafka 要低了一个数量级 | 万级,吞吐量比 RocketMQ 和 Kafka 要低了一个数量级 | 10 万级RocketMQ 也是可以支撑高吞吐的一种 MQ | 10 万级别,这是 kafka 最大的优点,就是吞吐量高。一般配合大数据类的系统来进行实时数据计算、日志采集等场景 |
| topic 数量对吞吐量的影响 | | | topic 可以达到几百,几千个的级别,吞吐量会有较小幅度的下降这是 RocketMQ 的一大优势,在同等机器下,可以支撑大量的 topic | topic 从几十个到几百个的时候吞吐量会大幅度下降。所以在同等机器下kafka 尽量保证 topic 数量不要过多。如果要支撑大规模 topic需要增加更多的机器资源 |
| 可用性 | 高,基于主从架构实现高可用性 | 高,基于主从架构实现高可用性 | 非常高,分布式架构 | 非常高kafka 是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 |
| 消息可靠性 | 有较低的概率丢失数据 | | 经过参数优化配置,可以做到 0 丢失 | 经过参数优化配置,消息可以做到 0 丢失 |
| 时效性 | ms 级 | 微秒级,这是 rabbitmq 的一大特点,延迟是最低的 | ms 级 | 延迟在 ms 级以内 |
| 功能支持 | MQ 领域的功能极其完备 | 基于 erlang 开发,所以并发能力很强,性能极其好,延时很低 | MQ 功能较为完善,还是分布式的,扩展性好 | 功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用,是事实上的标准 |
| 优劣势总结 | 非常成熟,功能强大,在业内大量的公司以及项目中都有应用。偶尔会有较低概率丢失消息,而且现在社区以及国内应用都越来越少,官方社区现在对 ActiveMQ 5.x 维护越来越少,几个月才发布一个版本而且确实主要是基于解耦和异步来用的,较少在大规模吞吐的场景中使用 | erlang 语言开发性能极其好延时很低吞吐量到万级MQ 功能比较完备而且开源提供的管理界面非常棒,用起来很好用。社区相对比较活跃,几乎每个月都发布几个版本分在国内一些互联网公司近几年用 rabbitmq 也比较多一些但是问题也是显而易见的RabbitMQ 确实吞吐量会低一些,这是因为他做的实现机制比较重。而且 erlang 开发,国内有几个公司有实力做 erlang 源码级别的研究和定制?如果说你没这个实力的话,确实偶尔会有一些问题,你很难去看懂源码,你公司对这个东西的掌控很弱,基本职能依赖于开源社区的快速维护和修复 bug。而且 rabbitmq 集群动态扩展会很麻烦,不过这个我觉得还好。其实主要是 erlang 语言本身带来的问题。很难读源码,很难定制和掌控。 | 接口简单易用,而且毕竟在阿里大规模应用过,有阿里品牌保障。日处理消息上百亿之多,可以做到大规模吞吐,性能也非常好,分布式扩展也很方便,社区维护还可以,可靠性和可用性都是 ok 的,还可以支撑大规模的 topic 数量,支持复杂 MQ 业务场景。而且一个很大的优势在于,阿里出品都是 java 系的,我们可以自己阅读源码,定制自己公司的 MQ可以掌控。社区活跃度相对较为一般不过也还可以文档相对来说简单一些然后接口这块不是按照标准 JMS 规范走的有些系统要迁移需要修改大量代码。还有就是阿里出台的技术,你得做好这个技术万一被抛弃,社区黄掉的风险,那如果你们公司有技术实力我觉得用 RocketMQ 挺好的 | kafka 的特点其实很明显就是仅仅提供较少的核心功能但是提供超高的吞吐量ms 级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展。同时 kafka 最好是支撑较少的 topic 数量即可,保证其超高吞吐量。而且 kafka 唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略这个特性天然适合大数据实时计算以及日志收集。 |
| 特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka |
| :---------------------- | -----------------------------------------------------------: | -----------------------------------------------------------: | -----------------------------------------------------------: | -----------------------------------------------------------: |
| 单机吞吐量 | 万级吞吐量比RocketMQ和Kafka要低了一个数量级 | 万级吞吐量比RocketMQ和Kafka要低了一个数量级 | 10万级RocketMQ也是可以支撑高吞吐的一种MQ | 10万级别这是kafka最大的优点就是吞吐量高。一般配合大数据类的系统来进行实时数据计算、日志采集等场景 |
| topic数量对吞吐量的影响 | | | topic可以达到几百几千个的级别吞吐量会有较小幅度的下降这是RocketMQ的一大优势在同等机器下可以支撑大量的topic | topic从几十个到几百个的时候吞吐量会大幅度下降。所以在同等机器下kafka尽量保证topic数量不要过多。如果要支撑大规模topic需要增加更多的机器资源 |
| 可用性 | 高,基于主从架构实现高可用性 | 高,基于主从架构实现高可用性 | 非常高,分布式架构 | 非常高kafka是分布式的一个数据多个副本少数机器宕机不会丢失数据不会导致不可用 |
| 消息可靠性 | 有较低的概率丢失数据 | | 经过参数优化配置可以做到0丢失 | 经过参数优化配置消息可以做到0丢失 |
| 时效性 | ms级 | 微秒级这是rabbitmq的一大特点延迟是最低的 | ms级 | 延迟在ms级以内 |
| 功能支持 | MQ领域的功能极其完备 | 基于erlang开发所以并发能力很强性能极其好延时很低 | MQ功能较为完善还是分布式的扩展性好 | 功能较为简单主要支持简单的MQ功能在大数据领域的实时计算以及日志采集被大规模使用是事实上的标准 |
| 优劣势总结 | 非常成熟功能强大在业内大量的公司以及项目中都有应用。偶尔会有较低概率丢失消息而且现在社区以及国内应用都越来越少官方社区现在对ActiveMQ 5.x维护越来越少几个月才发布一个版本而且确实主要是基于解耦和异步来用的较少在大规模吞吐的场景中使用 | erlang语言开发性能极其好延时很低吞吐量到万级MQ功能比较完备而且开源提供的管理界面非常棒用起来很好用。社区相对比较活跃几乎每个月都发布几个版本分在国内一些互联网公司近几年用rabbitmq也比较多一些但是问题也是显而易见的RabbitMQ确实吞吐量会低一些这是因为他做的实现机制比较重。而且erlang开发国内有几个公司有实力做erlang源码级别的研究和定制如果说你没这个实力的话确实偶尔会有一些问题你很难去看懂源码你公司对这个东西的掌控很弱基本职能依赖于开源社区的快速维护和修复bug。而且rabbitmq集群动态扩展会很麻烦不过这个我觉得还好。其实主要是erlang语言本身带来的问题。很难读源码很难定制和掌控。 | 接口简单易用而且毕竟在阿里大规模应用过有阿里品牌保障。日处理消息上百亿之多可以做到大规模吞吐性能也非常好分布式扩展也很方便社区维护还可以可靠性和可用性都是ok的还可以支撑大规模的topic数量支持复杂MQ业务场景。而且一个很大的优势在于阿里出品都是java系的我们可以自己阅读源码定制自己公司的MQ可以掌控。社区活跃度相对较为一般不过也还可以文档相对来说简单一些然后接口这块不是按照标准JMS规范走的有些系统要迁移需要修改大量代码。还有就是阿里出台的技术你得做好这个技术万一被抛弃社区黄掉的风险那如果你们公司有技术实力我觉得用RocketMQ挺好的 | kafka的特点其实很明显就是仅仅提供较少的核心功能但是提供超高的吞吐量ms级的延迟极高的可用性以及可靠性而且分布式可以任意扩展。同时kafka最好是支撑较少的topic数量即可保证其超高吞吐量。而且kafka唯一的一点劣势是有可能消息重复消费那么对数据准确性会造成极其轻微的影响在大数据领域中以及日志采集中这点轻微影响可以忽略这个特性天然适合大数据实时计算以及日志收集。 |
> 这部分内容我这里不给出答案大家可以自行根据自己学习的消息队列查阅相关内容我可能会在后面的文章中介绍到这部分内容。另外下面这些问题在视频《Java工程师面试突击第1季-中华石杉老师》中都有提到如果大家没有资源的话可以在我的公众号“Java面试通关手册”后台回复关键字“1”即可
> 这部分内容我这里不给出答案大家可以自行根据自己学习的消息队列查阅相关内容我可能会在后面的文章中介绍到这部分内容。另外下面这些问题在视频《Java 工程师面试突击第 1 季-中华石杉老师》中都有提到如果大家没有资源的话可以在我的公众号“Java 面试通关手册”后台回复关键字“1”即可
### 1.4 关于消息队列其他一些常见的问题展望
@ -471,35 +460,31 @@ TransactionDefinition 接口中定义了五个表示隔离级别的常量:
5. 如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,说说怎么解决?
6. 如果让你来开发一个消息队列中间件,你会怎么设计架构?
## 2 谈谈 InnoDB 和 MyIsam 两者的区别
### 2.1 两者的对比
1. **count运算上的区别** 因为MyISAM缓存有表meta-data行数等因此在做COUNT(*)时对于一个结构很好的查询是不需要消耗多少资源的。而对于InnoDB来说则没有这种缓存
2. **是否支持事务和崩溃后的安全恢复:** MyISAM 强调的是性能每次查询具有原子性其执行速度比InnoDB类型更快但是不提供事务支持。但是 InnoDB 提供事务支持,外部键等高级数据库功能。 具有事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant))型表。
3. **是否支持外键:** MyISAM不支持而InnoDB支持。
1. **count 运算上的区别:** 因为 MyISAM 缓存有表 meta-data行数等因此在做 COUNT(\*)时对于一个结构很好的查询是不需要消耗多少资源的。而对于 InnoDB 来说,则没有这种缓存
2. **是否支持事务和崩溃后的安全恢复:** MyISAM 强调的是性能,每次查询具有原子性,其执行速度比 InnoDB 类型更快,但是不提供事务支持。但是 InnoDB 提供事务支持,外部键等高级数据库功能。 具有事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant))型表。
3. **是否支持外键:** MyISAM 不支持,而 InnoDB 支持。
### 2.2 关于两者的总结
MyISAM更适合读密集的表而InnoDB更适合写密集的表。 在数据库做主从分离的情况下经常选择MyISAM作为主库的存储引擎。
一般来说,如果需要事务支持,并且有较高的并发读取频率(MyISAM的表锁的粒度太大所以当该表写并发量较高时要等待的查询就会很多了)InnoDB是不错的选择。如果你的数据量很大MyISAM支持压缩特性可以减少磁盘的空间占用而且不需要支持事务时MyISAM是最好的选择。
MyISAM 更适合读密集的表,而 InnoDB 更适合写密集的表。 在数据库做主从分离的情况下,经常选择 MyISAM 作为主库的存储引擎。
一般来说,如果需要事务支持,并且有较高的并发读取频率(MyISAM 的表锁的粒度太大,所以当该表写并发量较高时,要等待的查询就会很多了)InnoDB 是不错的选择。如果你的数据量很大MyISAM 支持压缩特性可以减少磁盘的空间占用而且不需要支持事务时MyISAM 是最好的选择。
## 3 聊聊 Java 中的集合吧!
### 3.1 Arraylist 与 LinkedList 有什么不同?(注意加上从数据结构分析的内容)
- **1. 是否保证线程安全:** ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
- **2. 底层数据结构:** Arraylist 底层使用的是Object数组LinkedList 底层使用的是双向链表数据结构(注意双向链表和双向循环链表的区别:);
- **3. 插入和删除是否受元素位置的影响:****ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。** 比如:执行`add(E e) `方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话(`add(int index, E element) `)时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② **LinkedList 采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1) 而数组为近似 O(n) 。**
- **4. 是否支持快速随机访问:** LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于`get(int index) `方法)。
- **5. 内存空间占用:** ArrayList的空 间浪费主要体现在在list列表的结尾会预留一定的容量空间而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间因为要存放直接后继和直接前驱以及数据
- **2. 底层数据结构:** Arraylist 底层使用的是 Object 数组LinkedList 底层使用的是双向链表数据结构(注意双向链表和双向循环链表的区别:);
- **3. 插入和删除是否受元素位置的影响:****ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。** 比如:执行`add(E e)`方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(`add(int index, E element)`)时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② **LinkedList 采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1) 而数组为近似 O(n) 。**
- **4. 是否支持快速随机访问:** LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于`get(int index)`方法)。
- **5. 内存空间占用:** ArrayList 的空 间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。
**补充内容:RandomAccess接口**
**补充内容:RandomAccess 接口**
```java
public interface RandomAccess {
@ -508,7 +493,7 @@ public interface RandomAccess {
查看源码我们发现实际上 RandomAccess 接口中什么都没有定义。所以,在我看来 RandomAccess 接口不过是一个标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。
在 binarySearch() 方法中,它要判断传入的 list 是否RamdomAccess的实例如果是调用 indexedBinarySearch() 方法,如果不是,那么调用 iteratorBinarySearch() 方法
在 binarySearch() 方法中,它要判断传入的 list 是否 RamdomAccess 的实例,如果是,调用 indexedBinarySearch() 方法,如果不是,那么调用 iteratorBinarySearch() 方法
```java
public static <T>
@ -520,28 +505,27 @@ public interface RandomAccess {
}
```
ArraysList 实现了 RandomAccess 接口, 而 LinkedList 没有实现。为什么呢我觉得还是和底层数据结构有关ArraysList 底层是数组,而 LinkedList 底层是链表。数组天然支持随机访问,时间复杂度为 O(1) ,所以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n) 所以不支持快速随机访问。ArraysList 实现了 RandomAccess 接口,就表明了他具有快速随机访问功能。 RandomAccess 接口只是标识,并不是说 ArraysList 实现 RandomAccess 接口才具有快速随机访问功能的!
**下面再总结一下 list 的遍历方式选择:**
- 实现了RandomAccess接口的list优先选择普通for循环 其次foreach,
- 未实现RandomAccess接口的ist 优先选择iterator遍历foreach遍历底层也是通过iterator实现的大size的数据千万不要使用普通for循环
- 实现了 RandomAccess 接口的 list优先选择普通 for 循环 ,其次 foreach,
- 未实现 RandomAccess 接口的 ist 优先选择 iterator 遍历foreach 遍历底层也是通过 iterator 实现的),大 size 的数据,千万不要使用普通 for 循环
> Java 中的集合这类问题几乎是面试必问的问到这类问题的时候HashMap 又是几乎必问的问题,所以大家一定要引起重视!
### 3.2 HashMap的底层实现
### 3.2 HashMap 的底层实现
#### 1)JDK1.8之前
#### 1)JDK1.8 之前
JDK1.8 之前 HashMap 底层是 **数组和链表** 结合在一起使用也就是 **链表散列**。**HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 `(n - 1) & hash` 判断当前元素存放的位置(这里的 n 指的时数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。**
JDK1.8 之前 HashMap 底层是 **数组和链表** 结合在一起使用也就是 **链表散列**。**HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 `(n - 1) & hash` 判断当前元素存放的位置(这里的 n 指的时数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。**
**所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。**
**JDK 1.8 HashMap 的 hash 方法源码:**
JDK 1.8 的 hash方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。
JDK 1.8 的 hash 方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。
```java
static final int hash(Object key) {
@ -552,7 +536,8 @@ JDK 1.8 的 hash方法 相比于 JDK 1.7 hash 方法更加简化,但是原理
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
```
对比一下 JDK1.7的 HashMap 的 hash 方法源码.
对比一下 JDK1.7 的 HashMap 的 hash 方法源码.
```java
static int hash(int h) {
@ -569,22 +554,19 @@ static int hash(int h) {
所谓 **“拉链法”** 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
![jdk1.8之前的内部结构-HashMap](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/jdk1.8之前的内部结构-HashMap.jpg)
#### 2)JDK1.8 之后
![jdk1.8之前的内部结构](https://user-gold-cdn.xitu.io/2018/3/20/16240dbcc303d872?w=348&h=427&f=png&s=10991)
相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8将链表转化为红黑树以减少搜索时间。
![jdk1.8之后的内部结构-HashMap](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/JDK1.8之后的HashMap底层数据结构.jpg)
#### 2)JDK1.8之后
相比于之前的版本, JDK1.8之后在解决哈希冲突时有了较大的变化当链表长度大于阈值默认为8将链表转化为红黑树以减少搜索时间。
![JDK1.8之后的HashMap底层数据结构](https://user-gold-cdn.xitu.io/2018/11/14/16711ac29c351da9?w=720&h=545&f=jpeg&s=23933)
TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷因为二叉查找树在某些情况下会退化成一个线性结构。
TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。
> 问完 HashMap 的底层原理之后,面试官可能就会紧接着问你 HashMap 底层数据结构相关的问题!
### 3.3 既然谈到了红黑树,你给我手绘一个出来吧,然后简单讲一下自己对于红黑树的理解
### 3.3 既然谈到了红黑树,你给我手绘一个出来吧,然后简单讲一下自己对于红黑树的理解
![红黑树](https://user-gold-cdn.xitu.io/2018/11/14/16711ac29c138cba?w=851&h=614&f=jpeg&s=34458)
@ -592,21 +574,19 @@ TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。红
1. 每个节点非红即黑;
2. 根节点总是黑色的;
3. 每个叶子节点都是黑色的空节点NIL节点
3. 每个叶子节点都是黑色的空节点NIL 节点);
4. 如果节点是红色的,则它的子节点必须是黑色的(反之不一定);
5. 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)
**红黑树的应用:**
TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。
TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都用到了红黑树。
**为什么要用红黑树**
简单来说红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。
### 3.4 红黑树这么优秀,为何不直接使用红黑树得了?
### 3.4 红黑树这么优秀,为何不直接使用红黑树得了?
说一下自己对于这个问题的看法:我们知道红黑树属于(自)平衡二叉树,但是为了保持“平衡”是需要付出代价的,红黑树在插入新数据后可能需要通过左旋,右旋、变色这些操作来保持平衡,这费事啊。你说说我们引入红黑树就是为了查找数据快,如果链表长度很短的话,根本不需要引入红黑树的,你引入之后还要付出代价维持它的平衡。但是链表过长就不一样了。至于为什么选 8 这个值呢?通过概率统计所得,这个值是综合查询成本和新增元素成本得出的最好的一个值。
@ -614,11 +594,11 @@ TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。
**HashMap 和 Hashtable 的区别**
1. **线程是否安全:** HashMap 是非线程安全的Hashtable 是线程安全的Hashtable 内部的方法基本都经过 `synchronized` 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
1. **线程是否安全:** HashMap 是非线程安全的Hashtable 是线程安全的Hashtable 内部的方法基本都经过 `synchronized` 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
2. **效率:** 因为线程安全的问题HashMap 要比 Hashtable 效率高一点。另外Hashtable 基本被淘汰,不要在代码中使用它;
3. **对Null key 和Null value的支持** HashMap 中null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 Hashtable 中 put 进的键值只要有一个 null直接抛出 NullPointerException。
4. **初始容量大小和每次扩充容量大小的不同 ** 创建时如果不指定容量初始值Hashtable 默认的初始大小为11之后每次扩充容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充容量变为原来的2倍。②创建时如果给定了容量初始值那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小HashMap 中的`tableSizeFor()`方法保证,下面给出了源代码)。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。
5. **底层数据结构:** JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化当链表长度大于阈值默认为8将链表转化为红黑树以减少搜索时间。Hashtable 没有这样的机制。
3. **对 Null key 和 Null value 的支持:** HashMap 中null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 Hashtable 中 put 进的键值只要有一个 null直接抛出 NullPointerException。
4. **初始容量大小和每次扩充容量大小的不同 ** 创建时如果不指定容量初始值Hashtable 默认的初始大小为 11之后每次扩充容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小HashMap 中的`tableSizeFor()`方法保证,下面给出了源代码)。也就是说 HashMap 总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。
5. **底层数据结构:** JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8将链表转化为红黑树以减少搜索时间。Hashtable 没有这样的机制。
**HashSet 和 HashMap 区别**
@ -628,13 +608,13 @@ TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。
# 三 终结篇
## 1. Object类有哪些方法?
## 1. Object 类有哪些方法?
这个问题面试中经常出现。我觉得不论是出于应付面试还是说更好地掌握Java这门编程语言大家都要掌握
这个问题,面试中经常出现。我觉得不论是出于应付面试还是说更好地掌握 Java 这门编程语言,大家都要掌握!
### 1.1 Object类的常见方法总结
### 1.1 Object 类的常见方法总结
Object类是一个特殊的类是所有类的父类。它主要提供了以下11个方法
Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法:
```java
@ -661,15 +641,15 @@ protected void finalize() throws Throwable { }//实例被垃圾回收器回收
```
> 问完上面这个问题之后面试官很可能紧接着就会问你“hashCode与equals”相关的问题。
> 问完上面这个问题之后面试官很可能紧接着就会问你“hashCode equals”相关的问题。
### 1.2 hashCode与equals
### 1.2 hashCode equals
面试官可能会问你:“你重写过 hashcode 和 equals 么为什么重写equals时必须重写hashCode方法
面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写 equals 时必须重写 hashCode 方法?”
#### 1.2.1 hashCode()介绍
hashCode() 的作用是获取哈希码也称为散列码它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中这就意味着Java中的任何类都包含有hashCode() 函数。另外需要注意的是: Object 的 hashcode 方法是本地方法,也就是用 c 语言或 c++ 实现的,该方法通常用来将对象的 内存地址 转换为整数之后返回。
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在 JDK Object.java 中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。另外需要注意的是: Object 的 hashcode 方法是本地方法,也就是用 c 语言或 c++ 实现的,该方法通常用来将对象的 内存地址 转换为整数之后返回。
```java
public native int hashCode();
@ -677,41 +657,38 @@ hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)
#### 1.2.2 为什么要有hashCode
#### 1.2.2 为什么要有 hashCode
**我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode**
**我们以“HashSet如何检查重复”为例子来说明为什么要有hashCode**
当你把对象加入 HashSet 时HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的 hashcodeHashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals方法来检查 hashcode 相等的对象是否真的相同。如果两者相同HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的 Java 启蒙书《Head fist java》第二版。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。
当你把对象加入HashSet时HashSet会先计算对象的hashcode值来判断对象加入的位置同时也会与其他已经加入的对象的hashcode值作比较如果没有相符的hashcodeHashSet会假设对象没有重复出现。但是如果发现有相同hashcode值的对象这时会调用equals方法来检查hashcode相等的对象是否真的相同。如果两者相同HashSet就不会让其加入操作成功。如果不同的话就会重新散列到其他位置。摘自我的Java启蒙书《Head fist java》第二版。这样我们就大大减少了equals的次数相应就大大提高了执行速度。
#### 1.2.3 hashCode()与 equals()的相关规定
1. 如果两个对象相等,则 hashcode 一定也是相同的
2. 两个对象相等,对两个对象分别调用 equals 方法都返回 true
3. 两个对象有相同的 hashcode 值,它们也不一定是相等的
4. **因此equals 方法被覆盖过,则 hashCode 方法也必须被覆盖**
5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
#### 1.2.3 hashCode()与equals()的相关规定
1. 如果两个对象相等则hashcode一定也是相同的
2. 两个对象相等对两个对象分别调用equals方法都返回true
3. 两个对象有相同的hashcode值它们也不一定是相等的
4. **因此equals方法被覆盖过则hashCode方法也必须被覆盖**
5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode()则该class的两个对象无论如何都不会相等即使这两个对象指向相同的数据
#### 1.2.4 为什么两个对象有相同的hashcode值,它们也不一定是相等的?
#### 1.2.4 为什么两个对象有相同的 hashcode 值,它们也不一定是相等的?
在这里解释一位小伙伴的问题。以下内容摘自《Head Fisrt Java》。
因为hashCode() 所使用的杂凑算法也许刚好会让多个对象传回相同的杂凑值。越糟糕的杂凑算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同的 hashCode
因为 hashCode() 所使用的杂凑算法也许刚好会让多个对象传回相同的杂凑值。越糟糕的杂凑算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同的 hashCode
我们刚刚也提到了 HashSet,如果 HashSet 在对比的时候,同样的 hashcode 有多个对象,它会使用 equals() 来判断是否真的相同。也就是说 hashcode 只是用来缩小查找成本。
> ==与equals 的对比也是比较常问的基础问题之一!
> ==与 equals 的对比也是比较常问的基础问题之一!
### 1.3 ==与equals
### 1.3 ==与 equals
**==** : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)
**equals()** : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
- 情况1类没有覆盖equals()方法。则通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象。
- 情况2类覆盖了equals()方法。一般我们都覆盖equals()方法来两个对象的内容相等若它们的内容相等则返回true(即,认为这两个对象相等)。
- 情况 1类没有覆盖 equals()方法。则通过 equals()比较该类的两个对象时,等价于通过“==”比较这两个对象。
- 情况 2类覆盖了 equals()方法。一般,我们都覆盖 equals()方法来两个对象的内容相等;若它们的内容相等,则返回 true(即,认为这两个对象相等)。
**举个例子:**
@ -737,10 +714,10 @@ public class test1 {
**说明:**
- String中的equals()方法是被重写过的因为Object的equals()方法是比较的对象的内存地址而String的equals()方法比较的是对象的值。
- 当创建String类型的对象时虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个String对象。
- String 中的 equals()方法是被重写过的,因为 Object equals()方法是比较的对象的内存地址,而 String equals()方法比较的是对象的值。
- 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。
> 在[【备战春招/秋招系列5】美团面经总结进阶篇 (附详解答案)](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247484625&idx=1&sn=9c4fa1f7d4291a5fbd7daa44bac2b012&chksm=fd9852b0caefdba6edcf9a827aa4a17ddc97bf6ad2e5ee6f7e1aa1b443b54444d05d2b76732b&token=723699735&lang=zh_CN#rd) 这篇文章中,我们已经提到了一下关于 HashMap 在面试中常见的问题HashMap 的底层实现、简单讲一下自己对于红黑树的理解、红黑树这么优秀为何不直接使用红黑树得了、HashMap 和 Hashtable 的区别/HashSet 和 HashMap 区别。HashMap 和 ConcurrentHashMap 这俩兄弟在一般只要面试中问到集合相关的问题就一定会被问到,所以各位务必引起重视!
> 在[【备战春招/秋招系列 5】美团面经总结进阶篇 (附详解答案)](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247484625&idx=1&sn=9c4fa1f7d4291a5fbd7daa44bac2b012&chksm=fd9852b0caefdba6edcf9a827aa4a17ddc97bf6ad2e5ee6f7e1aa1b443b54444d05d2b76732b&token=723699735&lang=zh_CN#rd) 这篇文章中,我们已经提到了一下关于 HashMap 在面试中常见的问题HashMap 的底层实现、简单讲一下自己对于红黑树的理解、红黑树这么优秀为何不直接使用红黑树得了、HashMap 和 Hashtable 的区别/HashSet 和 HashMap 区别。HashMap 和 ConcurrentHashMap 这俩兄弟在一般只要面试中问到集合相关的问题就一定会被问到,所以各位务必引起重视!
## 2 ConcurrentHashMap 相关问题
@ -748,8 +725,8 @@ public class test1 {
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
- **底层数据结构:** JDK1.7的 ConcurrentHashMap 底层采用 **分段的数组+链表** 实现JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 **数组+链表** 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
- **实现线程安全的方式(重要):****在JDK1.7的时候ConcurrentHashMap分段锁** 对整个桶数组进行了分割分段(Segment)每一把锁只锁容器其中一部分数据多线程访问容器里不同数据段的数据就不会存在锁竞争提高并发访问率。默认分配16个Segment比Hashtable效率提高16倍。 **到了 JDK1.8 的时候已经摒弃了Segment的概念而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。JDK1.6以后 对 synchronized锁做了很多优化** 整个看起来就像是优化过且线程安全的 HashMap虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② **Hashtable(同一把锁)**:使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get竞争会越来越激烈效率越低。
- **底层数据结构:** JDK1.7 的 ConcurrentHashMap 底层采用 **分段的数组+链表** 实现JDK1.8 采用的数据结构跟 HashMap1.8 的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 **数组+链表** 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
- **实现线程安全的方式(重要):****在 JDK1.7 的时候ConcurrentHashMap分段锁** 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配 16 Segment Hashtable 效率提高 16 倍。) **到了 JDK1.8 的时候已经摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。JDK1.6 以后 对 synchronized 锁做了很多优化)** 整个看起来就像是优化过且线程安全的 HashMap虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② **Hashtable(同一把锁)**:使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get竞争会越来越激烈效率越低。
**两者的对比图:**
@ -758,13 +735,13 @@ ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方
Hashtable
![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-22/50656681.jpg)
JDK1.7的ConcurrentHashMap
JDK1.7 ConcurrentHashMap
![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-22/33120488.jpg)
JDK1.8的ConcurrentHashMapTreeBin: 红黑二叉树节点
JDK1.8 ConcurrentHashMapTreeBin: 红黑二叉树节点
Node: 链表节点):
![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-22/97739220.jpg)
### 2.2 ConcurrentHashMap线程安全的具体实现方式/底层具体实现
### 2.2 ConcurrentHashMap 线程安全的具体实现方式/底层具体实现
#### JDK1.7(上面有示意图)
@ -779,19 +756,19 @@ static class Segment<K,V> extends ReentrantLock implements Serializable {
}
```
一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似是一种数组和链表结构一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。
一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和 HashMap 类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 的锁。
#### JDK1.8(上面有示意图)
ConcurrentHashMap取消了Segment分段锁采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构类似,数组+链表/红黑二叉树。
ConcurrentHashMap 取消了 Segment 分段锁,采用 CAS synchronized 来保证并发安全。数据结构跟 HashMap1.8 的结构类似,数组+链表/红黑二叉树。
synchronized只锁定当前链表或红黑二叉树的首节点这样只要hash不冲突就不会产生并发效率又提升N倍。
synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。
## 3 谈谈 synchronized 和 ReentrantLock 的区别
**① 两者都是可重入锁**
两者都是可重入锁。“可重入锁”概念是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁此时这个对象锁还没有释放当其再次想要获取这个对象的锁的时候还是可以获取的如果不可锁重入的话就会造成死锁。同一个线程每次获取锁锁的计数器都自增1所以要等到锁的计数器下降为0时才能释放锁。
两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1所以要等到锁的计数器下降为 0 时才能释放锁。
**② synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API**
@ -799,27 +776,25 @@ synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团
**③ ReentrantLock 比 synchronized 增加了一些高级功能**
相比synchronizedReentrantLock增加了一些高级功能。主要来说主要有三点**①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)**
相比 synchronizedReentrantLock 增加了一些高级功能。主要来说主要有三点:**① 等待可中断;② 可实现公平锁;③ 可实现选择性通知(锁可以绑定多个条件)**
- **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 提供了一种能够中断等待锁的线程的机制**,通过 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是一个不错的选择。
如果你想使用上述功能,那么选择 ReentrantLock 是一个不错的选择。
**④ 两者的性能已经相差无几**
在JDK1.6之前synchronized 的性能是比 ReentrantLock 差很多。具体表示为synchronized 关键字吞吐量岁线程数的增加下降得非常严重。而ReentrantLock 基本保持一个比较稳定的水平。我觉得这也侧面反映了, synchronized 关键字还有非常大的优化余地。后续的技术发展也证明了这一点,我们上面也讲了在 JDK1.6 之后 JVM 团队对 synchronized 关键字做了很多优化。JDK1.6 之后synchronized 和 ReentrantLock 的性能基本是持平了。所以网上那些说因为性能才选择 ReentrantLock 的文章都是错的JDK1.6之后性能已经不是选择synchronized和ReentrantLock的影响因素了而且虚拟机在未来的性能改进中会更偏向于原生的synchronized所以还是提倡在synchronized能满足你的需求的情况下优先考虑使用synchronized关键字来进行同步优化后的synchronized和ReentrantLock一样在很多地方都是用到了CAS操作。
在 JDK1.6 之前synchronized 的性能是比 ReentrantLock 差很多。具体表示为synchronized 关键字吞吐量岁线程数的增加,下降得非常严重。而 ReentrantLock 基本保持一个比较稳定的水平。我觉得这也侧面反映了, synchronized 关键字还有非常大的优化余地。后续的技术发展也证明了这一点,我们上面也讲了在 JDK1.6 之后 JVM 团队对 synchronized 关键字做了很多优化。JDK1.6 之后synchronized 和 ReentrantLock 的性能基本是持平了。所以网上那些说因为性能才选择 ReentrantLock 的文章都是错的JDK1.6 之后,性能已经不是选择 synchronized 和 ReentrantLock 的影响因素了!而且虚拟机在未来的性能改进中会更偏向于原生的 synchronized所以还是提倡在 synchronized 能满足你的需求的情况下,优先考虑使用 synchronized 关键字来进行同步!优化后的 synchronized 和 ReentrantLock 一样,在很多地方都是用到了 CAS 操作。
## 4 线程池了解吗?
### 4.1 为什么要用线程池?
线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。
这里借用《Java并发编程的艺术》提到的来说一下使用线程池的好处
这里借用《Java 并发编程的艺术》提到的来说一下使用线程池的好处:
- **降低资源消耗。** 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- **提高响应速度。** 当任务到达时,任务可以不需要的等到线程创建就能立即执行。
@ -827,12 +802,12 @@ synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团
### 4.2 Java 提供了哪几种线程池?他们各自的使用场景是什么?
#### Java 主要提供了下面4种线程池
#### Java 主要提供了下面 4 种线程池
- **FixedThreadPool** 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
- **SingleThreadExecutor** 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
- **CachedThreadPool** 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
- **ScheduledThreadPoolExecutor** 主要用来在给定的延迟后运行任务或者定期执行任务。ScheduledThreadPoolExecutor又分为ScheduledThreadPoolExecutor包含多个线程和SingleThreadScheduledExecutor (只包含一个线程)两种。
- **ScheduledThreadPoolExecutor** 主要用来在给定的延迟后运行任务或者定期执行任务。ScheduledThreadPoolExecutor 又分为ScheduledThreadPoolExecutor包含多个线程 SingleThreadScheduledExecutor (只包含一个线程)两种。
#### 各种线程池的适用场景介绍
@ -846,7 +821,7 @@ synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团
**1 使用 Executors 创建**
我们上面刚刚提到了 Java 提供的几种线程池,通过 Executors 工具类我们可以很轻松的创建我们上面说的几种线程池。但是实际上我们一般都不是直接使用Java提供好的线程池另外在《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
我们上面刚刚提到了 Java 提供的几种线程池,通过 Executors 工具类我们可以很轻松的创建我们上面说的几种线程池。但是实际上我们一般都不是直接使用 Java 提供好的线程池,另外在《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
```java
Executors 返回线程池对象的弊端如下:
@ -855,8 +830,8 @@ FixedThreadPool 和 SingleThreadExecutor 允许请求的队列长度为 Inte
CachedThreadPool 和 ScheduledThreadPool 允许创建的线程数量为 Integer.MAX_VALUE 可能会创建大量线程从而导致OOM。
```
**2 ThreadPoolExecutor的构造函数创建**
**2 ThreadPoolExecutor 的构造函数创建**
我们可以自己直接调用 ThreadPoolExecutor 的构造函数来自己创建线程池。在创建的同时,给 BlockQueue 指定容量就可以了。示例如下:
@ -866,11 +841,11 @@ private static ExecutorService executor = new ThreadPoolExecutor(13, 13,
new ArrayBlockingQueue(13));
```
这种情况下一旦提交的线程数超过当前可用线程数时就会抛出java.util.concurrent.RejectedExecutionException这是因为当前线程池使用的队列是有边界队列队列已经满了便无法继续处理新的请求。但是异常Exception总比发生错误Error要好。
这种情况下,一旦提交的线程数超过当前可用线程数时,就会抛出 java.util.concurrent.RejectedExecutionException这是因为当前线程池使用的队列是有边界队列队列已经满了便无法继续处理新的请求。但是异常Exception总比发生错误Error要好。
**3 使用开源类库**
Hollis 大佬之前在他的文章中也提到了“除了自己定义ThreadPoolExecutor外。还有其他方法。这个时候第一时间就应该想到开源类库如apache和guava等。”他推荐使用guava提供的ThreadFactoryBuilder来创建线程池。下面是参考他的代码示例
Hollis 大佬之前在他的文章中也提到了:“除了自己定义 ThreadPoolExecutor 外。还有其他方法。这个时候第一时间就应该想到开源类库,如 apache guava 等。”他推荐使用 guava 提供的 ThreadFactoryBuilder 来创建线程池。下面是参考他的代码示例:
```java
public class ExecutorsDemo {
@ -891,19 +866,19 @@ public class ExecutorsDemo {
}
```
通过上述方式创建线程时不仅可以避免OOM的问题还可以自定义线程名称更加方便的出错的时候溯源。
通过上述方式创建线程时,不仅可以避免 OOM 的问题,还可以自定义线程名称,更加方便的出错的时候溯源。
## 5 Nginx
### 5.1 简单介绍一下Nginx
### 5.1 简单介绍一下 Nginx
Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮件IMAP/POP3代理服务器。 Nginx 主要提供反向代理、负载均衡、动静分离(静态资源服务)等服务。下面我简单地介绍一下这些名词。
Nginx 是一款轻量级的 Web 服务器/反向代理服务器及电子邮件IMAP/POP3代理服务器。 Nginx 主要提供反向代理、负载均衡、动静分离(静态资源服务)等服务。下面我简单地介绍一下这些名词。
#### 反向代理
谈到反向代理,就不得不提一下正向代理。无论是正向代理,还是反向代理,说到底,就是代理模式的衍生版本罢了
- **正向代理:**某些情况下代理我们用户去访问服务器需要用户手动的设置代理服务器的ip和端口号。正向代理比较常见的一个例子就是 VPN 了。
- **正向代理:**某些情况下,代理我们用户去访问服务器,需要用户手动的设置代理服务器的 ip 和端口号。正向代理比较常见的一个例子就是 VPN 了。
- **反向代理:** 是用来代理服务器的,代理我们要访问的目标服务器。代理服务器接受请求,然后将请求转发给内部网络的服务器,并将从服务器上得到的结果返回给客户端,此时代理服务器对外就表现为一个服务器。
通过下面两幅图大家应该更好理解图源http://blog.720ui.com/2016/nginx_action_05_proxy/
@ -918,7 +893,7 @@ Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮件I
在高并发情况下需要使用,其原理就是将并发请求分摊到多个服务器执行,减轻每台服务器的压力,多台服务器(集群)共同完成工作任务,从而提高了数据的吞吐量。
Nginx支持的weight轮询默认、ip_hash、fair、url_hash这四种负载均衡调度算法感兴趣的可以自行查阅。
Nginx 支持的 weight 轮询默认、ip_hash、fair、url_hash 这四种负载均衡调度算法,感兴趣的可以自行查阅。
负载均衡相比于反向代理更侧重的是将请求分担到多台服务器上去,所以谈论负载均衡只有在提供某服务的服务器大于两台时才有意义。
@ -928,23 +903,23 @@ Nginx支持的weight轮询默认、ip_hash、fair、url_hash这四种负
### 5.2 为什么要用 Nginx?
> 这部分内容参考极客时间—[Nginx核心知识100讲的内容](https://time.geekbang.org/course/intro/138?code=AycjiiQk6uQRxnVJzBupFkrGkvZlmYELPRsZbWzaAHE=)。
> 这部分内容参考极客时间—[Nginx 核心知识 100 讲的内容](https://time.geekbang.org/course/intro/138?code=AycjiiQk6uQRxnVJzBupFkrGkvZlmYELPRsZbWzaAHE= "Nginx核心知识100讲的内容")。
如果面试官问你这个问题,就一定想看你知道 Nginx 服务器的一些优点吗。
Nginx 有以下5个优点
Nginx 有以下 5 个优点:
1. 高并发、高性能这是其他web服务器不具有的
1. 高并发、高性能(这是其他 web 服务器不具有的)
2. 可扩展性好(模块化设计,第三方插件生态圈丰富)
3. 高可靠性(可以在服务器行持续不间断的运行数年)
4. 热部署(这个功能对于 Nginx 来说特别重要,热部署指可以在不停止 Nginx服务的情况下升级 Nginx
5. BSD许可证意味着我们可以将源代码下载下来进行修改然后使用自己的版本
4. 热部署(这个功能对于 Nginx 来说特别重要,热部署指可以在不停止 Nginx 服务的情况下升级 Nginx
5. BSD 许可证(意味着我们可以将源代码下载下来进行修改然后使用自己的版本)
### 5.3 Nginx 的四个主要组成部分了解吗?
> 这部分内容参考极客时间—[Nginx核心知识100讲的内容](https://time.geekbang.org/course/intro/138?code=AycjiiQk6uQRxnVJzBupFkrGkvZlmYELPRsZbWzaAHE=)。
> 这部分内容参考极客时间—[Nginx 核心知识 100 讲的内容](https://time.geekbang.org/course/intro/138?code=AycjiiQk6uQRxnVJzBupFkrGkvZlmYELPRsZbWzaAHE= "Nginx核心知识100讲的内容")。
- Nginx 二进制可执行文件:由各模块源码编译出一个文件
- nginx.conf 配置文件控制Nginx 行为
- acess.log 访问日志: 记录每一条HTTP请求信息
- nginx.conf 配置文件:控制 Nginx 行为
- acess.log 访问日志: 记录每一条 HTTP 请求信息
- error.log 错误日志:定位问题

View File

@ -0,0 +1,141 @@
本文的内容都是根据读者投稿的真实面试经历改编而来,首次尝试这种风格的文章,花了几天晚上才总算写完,希望对你有帮助。本文主要涵盖下面的内容:
1. 分布式商城系统:架构图讲解;
2. 消息队列相关:削峰和解耦;
3. Redis 相关:缓存穿透问题的解决;
4. 一些 Java 基础问题;
面试开始,坐在我前面的就是这次我的面试官吗?这发量看着根本不像程序员啊?我心里正嘀咕着,只听见面试官说:“小伙,下午好,我今天就是你的面试官,咱们开始面试吧!”。
### 第一面开始
**面试官:** 我也不用多说了,你先自我介绍一下吧,简历上有的就不要再说了哈。
**我:** 内心 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:我自己都被我画的这个图美到了,如果你也觉得这张图好看的话麻烦来个素质三连!)。
>
> ![通过异步处理提高系统性能](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/Asynchronous-message-queue.png)
>
> 通过以上分析我们可以得出**消息队列具有很好的削峰作用的功能**——即**通过异步处理,将短时间高并发产生的事务消息存储在消息队列中,从而削平高峰期的并发事务。** 举例:在电子商务一些秒杀、促销活动中,合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击。如下图所示:
>
> ![削峰](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/削峰-消息队列.png)
>
> 使用消息队列还可以降低系统耦合性。我们知道如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性无疑更好一些。还是直接上图吧:
>
> ![解耦](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/消息队列-解耦.png)
>
> 生产者(客户端)发送消息到消息队列中去,接受者(服务端)处理消息,需要消费的系统直接去消息队列取消息进行消费即可而不需要和其他系统有耦合, 这显然也提高了系统的扩展性。
**面试官:** 你觉得它有什么缺点吗?或者说怎么考虑用不用消息队列?
**我:** 内心 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说明该元素不在布隆过滤器中。
>
> 举个简单的例子:
>
>
>
> ![布隆过滤器hash计算](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/布隆过滤器-hash运算.png)
>
> 如图所示,当字符串存储要加入到布隆过滤器中时,该字符串首先由多个哈希函数生成不同的哈希值,然后在对应的位数组的下表的元素设置为 1当位数组初始化时 所有位置均为0。当第二次存储相同字符串时因为先前的对应位置已设置为1所以很容易知道此值已经存在去重非常方便
>
> 如果我们需要判断某个字符串是否在布隆过滤器中时,只需要对给定字符串再次进行相同的哈希计算,得到值之后判断位数组中的每个元素是否都为 1如果值都为 1那么说明这个值在布隆过滤器中如果存在一个值不为 1说明该元素不在布隆过滤器中。
>
> **不同的字符串可能哈希出来的位置相同,这种情况我们可以适当增加位数组大小或者调整我们的哈希函数。**
>
> 综上,我们可以得出:**布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。**
**面试官:** 看来你对布隆过滤器了解的还挺不错的嘛!那你快说说你最后是怎么利用它来解决缓存穿透的。

View File

@ -164,8 +164,17 @@ JRE 是 Java运行时环境。它是运行已编译 Java 程序所需的所有
## 10. 重载和重写的区别
- **重载:** 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。   
- **重写:** 发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为 private 则子类就不能重写该方法。
#### 重载
发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。
下面是《Java核心技术》对重载这个概念的介绍
![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/bg/desktopjava核心技术-重载.jpg) 
#### 重写
重写是子类对父类的允许访问的方法的实现过程进行重新编写,发生在子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。另外,如果父类方法访问修饰符为 private 则子类就不能重写该方法。**也就是说方法提供的行为改变,而方法的外貌并没有改变。**
## 11. Java 面向对象编程三大特性: 封装 继承 多态

View File

@ -54,10 +54,9 @@ AQS使用一个int成员变量来表示同步状态通过内置的FIFO队列
private volatile int state;//共享变量使用volatile修饰保证线程可见性
```
状态信息通过protected类型的getStatesetStatecompareAndSetState进行操作
状态信息通过protected类型的`getState``setState``compareAndSetState`进行操作
```java
//返回同步状态的当前值
protected final int getState() {
return state;
@ -76,10 +75,125 @@ protected final boolean compareAndSetState(int expect, int update) {
**AQS定义两种资源共享方式**
- **Exclusive**独占只有一个线程能执行如ReentrantLock。又可分为公平锁和非公平锁
- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
- 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
- **Share**共享多个线程可同时执行如Semaphore/CountDownLatch。Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。
**1)Exclusive**(独占)
只有一个线程能执行如ReentrantLock。又可分为公平锁和非公平锁,ReentrantLock 同时支持两种锁,下面以 ReentrantLock 对这两种锁的定义做介绍:
- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
- 非公平锁:当线程要获取锁时,先通过两次 CAS 操作去抢锁,如果没抢到,当前线程再加入到队列中等待唤醒。
> 说明:下面这部分关于 `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也就是读写锁允许多个线程同时对某一资源进行读。

View File

@ -509,7 +509,192 @@ public interface Callable<V> {
- **`ThreadPoolExecutor.DiscardPolicy`** 不处理新任务,直接丢弃掉。
- **`ThreadPoolExecutor.DiscardOldestPolicy`** 此策略将丢弃最早的未处理的任务请求。
举个例子: Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecutor` 的构造函数创建线程池的时候,当我们不指定 `RejectedExecutionHandler` 饱和策略的话来配置线程池的时候默认使用的是 `ThreadPoolExecutor.AbortPolicy`。在默认情况下,`ThreadPoolExecutor` 将抛出 `RejectedExecutionException` 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 `ThreadPoolExecutor.CallerRunsPolicy`。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 `ThreadPoolExecutor` 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了。)
举个例子: Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecutor` 的构造函数创建线程池的时候,当我们不指定 `RejectedExecutionHandler` 饱和策略的话来配置线程池的时候默认使用的是 `ThreadPoolExecutor.AbortPolicy`。在默认情况下,`ThreadPoolExecutor` 将抛出 `RejectedExecutionException` 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 `ThreadPoolExecutor.CallerRunsPolicy`。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 `ThreadPoolExecutor` 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了)
### 4.6 一个简单的线程池Demo:`Runnable`+`ThreadPoolExecutor`
为了让大家更清楚上面的面试题中的一些概念,我写了一个简单的线程池 Demo。
首先创建一个 `Runnable` 接口的实现类(当然也可以是 `Callable` 接口,我们上面也说了两者的区别。)
`MyRunnable.java`
```java
import java.util.Date;
/**
* 这是一个简单的Runnable类需要大约5秒钟来执行其任务。
* @author shuang.kou
*/
public class MyRunnable implements Runnable {
private String command;
public MyRunnable(String s) {
this.command = s;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date());
processCommand();
System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date());
}
private void processCommand() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public String toString() {
return this.command;
}
}
```
编写测试程序,我们这里以阿里巴巴推荐的使用 `ThreadPoolExecutor` 构造函数自定义参数的方式来创建线程池。
`ThreadPoolExecutorDemo.java`
```java
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolExecutorDemo {
private static final int CORE_POOL_SIZE = 5;
private static final int MAX_POOL_SIZE = 10;
private static final int QUEUE_CAPACITY = 100;
private static final Long KEEP_ALIVE_TIME = 1L;
public static void main(String[] args) {
//使用阿里巴巴推荐的创建线程池的方式
//通过ThreadPoolExecutor构造函数自定义参数创建
ThreadPoolExecutor executor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 0; i < 10; i++) {
//创建WorkerThread对象WorkerThread类实现了Runnable 接口)
Runnable worker = new MyRunnable("" + i);
//执行Runnable
executor.execute(worker);
}
//终止线程池
executor.shutdown();
while (!executor.isTerminated()) {
}
System.out.println("Finished all threads");
}
}
```
可以看到我们上面的代码指定了:
1. `corePoolSize`: 核心线程数为 5。
2. `maximumPoolSize` :最大线程数 10
3. `keepAliveTime` : 等待时间为 1L。
4. `unit`: 等待时间的单位为 TimeUnit.SECONDS。
5. `workQueue`:任务队列为 `ArrayBlockingQueue`,并且容量为 100;
6. `handler`:饱和策略为 `CallerRunsPolicy`
**Output**
```
pool-1-thread-2 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-5 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-4 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-1 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-3 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-5 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-3 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-2 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-4 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-1 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-2 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-1 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-4 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-3 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-5 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-2 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-3 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-4 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-5 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-1 End. Time = Tue Nov 12 20:59:54 CST 2019
```
### 4.7 线程池原理分析
承接 4.6 节,我们通过代码输出结果可以看出:**线程池每次会同时执行 5 个任务,这 5 个任务执行完之后,剩余的 5 个任务才会被执行。** 大家可以先通过上面讲解的内容,分析一下到底是咋回事?(自己独立思考一会)
现在,我们就分析上面的输出内容来简单分析一下线程池原理。
**为了搞懂线程池的原理,我们需要首先分析一下 `execute`方法。**在 4.6 节中的 Demo 中我们使用 `executor.execute(worker)`来提交一个任务到线程池中去,这个方法非常重要,下面我们来看看它的源码:
```java
// 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount)
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static int workerCountOf(int c) {
return c & CAPACITY;
}
private final BlockingQueue<Runnable> workQueue;
public void execute(Runnable command) {
// 如果任务为null则抛出异常。
if (command == null)
throw new NullPointerException();
// ctl 中保存的线程池当前的一些状态信息
int c = ctl.get();
// 下面会涉及到 3 步 操作
// 1.首先判断当前线程池中之行的任务数量是否小于 corePoolSize
// 如果小于的话通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 2.如果当前之行的任务数量大于等于 corePoolSize 的时候就会走到这里
// 通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态才会被并且队列可以加入任务,该任务才会被加入进去
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。
if (!isRunning(recheck) && remove(command))
reject(command);
// 如果当前线程池为空就新创建一个线程并执行。
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
//如果addWorker(command, false)执行失败则通过reject()执行相应的拒绝策略的内容。
else if (!addWorker(command, false))
reject(command);
}
```
通过下图可以更好的对上面这 3 步做一个展示,下图是我为了省事直接从网上找到,原地址不明。
![图解线程池实现原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/图解线程池实现原理.png)
现在,让我们在回到 4.6 节我们写的 Demo 现在应该是不是很容易就可以搞懂它的原理了呢?
没搞懂的话,也没关系,可以看看我的分析:
> 我们在代码中模拟了 10 个任务,我们配置的核心线程数为 5 、等待队列容量为 100 ,所以每次只可能存在 5 个任务同时执行,剩下的 5 个任务会被放到等待队列中去。当前的 5 个任务之行完成后,才会之行剩下的 5 个任务。
## 5. Atomic 原子类

View File

@ -0,0 +1,58 @@
> 原文地址: https://juejin.im/post/5c94a123f265da610916081f。
## JVM 配置常用参数
1. 堆参数;
2. 回收器参数;
3. 项目中常用配置;
4. 常用组合;
### 堆参数
![img](https://ask.qcloudimg.com/http-save/yehe-1130324/975rk4d0wx.jpeg?imageView2/2/w/1620)
### 回收器参数
![img](https://ask.qcloudimg.com/http-save/yehe-1130324/34nzellt71.jpeg?imageView2/2/w/1620)
如上表所示,目前**主要有串行、并行和并发三种**,对于大内存的应用而言,串行的性能太低,因此使用到的主要是并行和并发两种。并行和并发 GC 的策略通过 `UseParallelGC `` UseConcMarkSweepGC` 来指定,还有一些细节的配置参数用来配置策略的执行方式。例如:`XX:ParallelGCThreads` `XX:CMSInitiatingOccupancyFraction` 等。 通常Young 区对象回收只可选择并行耗时间Old 区选择并发(耗 CPU
### 项目中常用配置
> 备注在Java8中永久代的参数`-XX:PermSize``-XXMaxPermSize`已经失效。
![img](https://ask.qcloudimg.com/http-save/yehe-1130324/urw285pczz.jpeg?imageView2/2/w/1620)
### 常用组合
![img](https://ask.qcloudimg.com/http-save/yehe-1130324/ff8ues5crb.jpeg?imageView2/2/w/1620)
## 常用 GC 调优策略
1. GC 调优原则;
2. GC 调优目的;
3. GC 调优策略;
### GC 调优原则
在调优之前,我们需要记住下面的原则:
> 多数的 Java 应用不需要在服务器上进行 GC 优化; 多数导致 GC 问题的 Java 应用,都不是因为我们参数设置错误,而是代码问题; 在应用上线之前,先考虑将机器的 JVM 参数设置到最优(最适合); 减少创建对象的数量; 减少使用全局变量和大对象; GC 优化是到最后不得已才采用的手段; 在实际使用中,分析 GC 情况优化代码比优化 GC 参数要多得多。
### GC 调优目的
将转移到老年代的对象数量降低到最小; 减少 GC 的执行时间。
### GC 调优策略
**策略 1**将新对象预留在新生代,由于 Full GC 的成本远高于 Minor GC因此尽可能将对象分配在新生代是明智的做法实际项目中根据 GC 日志分析新生代空间大小分配是否合理,适当通过“-Xmn”命令调节新生代大小最大限度降低新对象直接进入老年代的情况。
**策略 2**大对象进入老年代,虽然大部分情况下,将对象分配在新生代是合理的。但是对于大对象这种做法却值得商榷,大对象如果首次在新生代分配可能会出现空间不足导致很多年龄不够的小对象被分配的老年代,破坏新生代的对象结构,可能会出现频繁的 full gc。因此对于大对象可以设置直接进入老年代当然短命的大对象对于垃圾回收老说简直就是噩梦`-XX:PretenureSizeThreshold` 可以设置直接进入老年代的对象大小。
**策略 3**合理设置进入老年代对象的年龄,`-XX:MaxTenuringThreshold` 设置对象进入老年代的年龄大小,减少老年代的内存占用,降低 full gc 发生的频率。
**策略 4**设置稳定的堆大小,堆大小设置有两个参数:`-Xms` 初始化堆大小,`-Xmx` 最大堆大小。
**策略5**注意: 如果满足下面的指标,**则一般不需要进行 GC 优化:**
> MinorGC 执行时间不到50ms Minor GC 执行不频繁约10秒一次 Full GC 执行时间不到1s Full GC 执行频率不算频繁不低于10分钟1次。

View File

@ -136,15 +136,54 @@ Java 虚拟机所管理的内存中最大的一块Java 堆是所有线程共
Java 堆是垃圾收集器管理的主要区域,因此也被称作**GC 堆Garbage Collected Heap**.从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为新生代和老年代再细致一点有Eden 空间、From Survivor、To Survivor 空间等。**进一步划分的目的是更好地回收内存,或者更快地分配内存。**
<div align="center">
<img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3堆结构.png" width="400px"/>
</div>
在 JDK 7 版本及JDK 7 版本之前,堆内存被通常被分为下面三部分:
上图所示的 eden 区、s0 区、s1 区都属于新生代tentired 区属于老年代。大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。
1. 新生代内存(Young Ceneration)
2. 老生代(Old Generation)
3. 永生代(Permanent Generation)
![JVM堆内存结构-JDK7](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/JVM堆内存结构-JDK7.jpg)
JDK 8 版本之后方法区HotSpot 的永久代被彻底移除了JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。
![JVM堆内存结构-JDK8](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/JVM堆内存结构-jdk8.jpg)
**上图所示的 Eden 区、两个 Survivor 区都属于新生代(为了区分,这两个 Survivor 区域按照顺序被命名为 from 和 to中间一层属于老年代。**
大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。
> 修正([issue552](https://github.com/Snailclimb/JavaGuide/issues/552)“Hotspot遍历所有对象时按照年龄从小到大对其所占用的大小进行累积当累积的某个年龄大小超过了survivor区的一半时取这个年龄和MaxTenuringThreshold中更小的一个值作为新的晋升年龄阈值”。
>
> **动态年龄计算的代码如下**
>
> ```c++
> uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) {
> //survivor_capacity是survivor空间的大小
> size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100);
> size_t total = 0;
> uint age = 1;
> while (age < table_size) {
> total += sizes[age];//sizes数组是每个年龄段对象大小
> if (total > desired_survivor_size) break;
> age++;
> }
> uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;
> ...
> }
>
> ```
>
>
堆这里最容易出现的就是 OutOfMemoryError 异常,并且出现这种异常之后的表现形式还会有几种,比如:
1. **`OutOfMemoryError: GC Overhead Limit Exceeded`** 当JVM花太多时间执行垃圾回收并且只能回收很少的堆空间时就会发生此错误。
2. **`java.lang.OutOfMemoryError: Java heap space`** :假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发`java.lang.OutOfMemoryError: Java heap space` 错误。(和本机物理内存无关,和你配置的对内存大小有关!)
3. ......
### 2.5 方法区
方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 **Non-Heap非堆**,目的应该是与 Java 堆区分开来。
方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 **Java 虚拟机规范把方法区描述为堆的一个逻辑部分**,但是它却有一个别名叫做 **Non-Heap非堆**,目的应该是与 Java 堆区分开来。
方法区也被称为永久代。很多人都会分不清方法区和永久代的关系,为此我也查阅了文献。
@ -176,7 +215,7 @@ JDK 1.8 的时候方法区HotSpot 的永久代被彻底移除了JDK1
#### 2.5.3 为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?
整个永久代有一个 JVM 本身设置固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,并且永远不会得到 java.lang.OutOfMemoryError。你可以使用 `-XXMaxMetaspaceSize` 标志设置最大元空间大小,默认值为 unlimited这意味着它只受系统内存的限制。`-XXMetaspaceSize` 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。
整个永久代有一个 JVM 本身设置固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,并且永远不会得到 `java.lang.OutOfMemoryError`。你可以使用 `-XXMaxMetaspaceSize` 标志设置最大元空间大小,默认值为 unlimited这意味着它只受系统内存的限制。`-XXMetaspaceSize` 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。
当然这只是其中一个原因,还有很多底层的原因,这里就不提了。

View File

@ -0,0 +1,137 @@
> 本文由 JavaGuide 翻译自 https://www.baeldung.com/jvm-parameters并对文章进行了大量的完善补充。翻译不易如需转载请注明出处为 作者: 。
## 1.概述
在本篇文章中,你将掌握最常用的 JVM 参数配置。如果对于下面提到了一些概念比如堆、
## 2.堆内存相关
>Java 虚拟机所管理的内存中最大的一块Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。**此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。**
>
### 2.1.显式指定堆内存`Xms``-Xmx`
与性能有关的最常见实践之一是根据应用程序要求初始化堆内存。如果我们需要指定最小和最大堆大小(推荐显示指定大小),以下参数可以帮助你实现:
```
-Xms<heap size>[unit]
-Xmx<heap size>[unit]
```
- **heap size** 表示要初始化内存的具体大小。
- **unit** 表示要初始化内存的单位。单位为***“ g”*** (GB) 、***“ m”***MB、***“ k”***KB
举个栗子🌰如果我们要为JVM分配最小2 GB和最大5 GB的堆内存大小我们的参数应该这样来写
```
-Xms2G -Xmx5G
```
### 2.2.显式新生代内存(Young Ceneration)
根据[Oracle官方文档](https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/sizing.html),在堆总可用内存配置完成之后,第二大影响因素是为 `Young Generation` 在堆内存所占的比例。默认情况下YG 的最小大小为 1310 *MB*,最大大小为*无限制*。
一共有两种指定 新生代内存(Young Ceneration)大小的方法:
**1.通过`-XX:NewSize``-XX:MaxNewSize`指定**
```
-XX:NewSize=<young size>[unit]
-XX:MaxNewSize=<young size>[unit]
```
举个栗子🌰,如果我们要为 新生代分配 最小256m 的内存,最大 1024m的内存我们的参数应该这样来写
```
-XX:NewSize=256m
-XX:MaxNewSize=1024m
```
**2.通过`-Xmn<young size>[unit] `指定**
举个栗子🌰,如果我们要为 新生代分配256m的内存NewSize与MaxNewSize设为一致我们的参数应该这样来写
```
-Xmn256m
```
GC 调优策略中很重要的一条经验总结是这样说的:
> 将新对象预留在新生代,由于 Full GC 的成本远高于 Minor GC因此尽可能将对象分配在新生代是明智的做法实际项目中根据 GC 日志分析新生代空间大小分配是否合理,适当通过“-Xmn”命令调节新生代大小最大限度降低新对象直接进入老年代的情况。
另外,你还可以通过**`-XX:NewRatio=<int>`**来设置新生代和老年代内存的比值。
比如下面的参数就是设置新生代包括Eden和两个Survivor区与老年代的比值为1。也就是说新生代与老年代所占比值为11新生代占整个堆栈的 1/2。
```
-XX:NewRatio=1
```
### 2.3.显示指定永久代/元空间的大小
**从Java 8开始如果我们没有指定 Metaspace 的大小,随着更多类的创建,虚拟机会耗尽所有可用的系统内存(永久代并不会出现这种情况)。**
JDK 1.8 之前永久代还没被彻底移除的时候通常通过下面这些参数来调节方法区大小
```java
-XX:PermSize=N //方法区 (永久代) 初始大小
-XX:MaxPermSize=N //方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen
```
相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就“永久存在”了。
**JDK 1.8 的时候方法区HotSpot 的永久代被彻底移除了JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。**
下面是一些常用参数:
```java
-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小,如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存。
```
## 3.垃圾收集相关
### 3.1.垃圾回收器
为了提高应用程序的稳定性,选择正确的[垃圾收集](http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html)算法至关重要。
JVM具有四种类型的*GC*实现:
- 串行垃圾收集器
- 并行垃圾收集器
- CMS垃圾收集器
- G1垃圾收集器
可以使用以下参数声明这些实现:
```
-XX:+UseSerialGC
-XX:+UseParallelGC
-XX:+USeParNewGC
-XX:+UseG1GC
```
有关*垃圾回收*实施的更多详细信息,请参见[此处](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/jvm/JVM%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.md)。
### 3.2.GC记录
为了严格监控应用程序的运行状况我们应该始终检查JVM的*垃圾回收*性能。最简单的方法是以人类可读的格式记录*GC*活动。
使用以下参数,我们可以记录*GC*活动:
```
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=< number of log files >
-XX:GCLogFileSize=< file size >[ unit ]
-Xloggc:/path/to/gc.log
```
## 推荐阅读
- [CMS GC 默认新生代是多大?](https://www.jianshu.com/p/832fc4d4cb53)
- [CMS GC启动参数优化配置](https://www.cnblogs.com/hongdada/p/10277782.html)
- [从实际案例聊聊Java应用的GC优化-美团技术团队](https://tech.meituan.com/2017/12/29/jvm-optimize.html)
- [JVM性能调优详解](https://www.choupangxia.com/2019/11/11/interview-jvm-gc-08/) 2019-11-11
- [JVM参数使用手册](https://segmentfault.com/a/1190000010603813)

View File

@ -1,5 +1,3 @@
如遇链接无法打开,建议使用 https://github.com/Snailclimb/JavaGuide/blob/master/docs/questions/java-learning-path-and-methods.md 这个链接进行阅读。
到目前为止,我觉得不管是在公众号后台、知乎还是微信上面我被问的做多的就是:“大佬,有没有 Java 学习路线和方法”。所以,这部分单独就自己的学习经历来说点自己的看法。
## 前言

View File

@ -39,21 +39,22 @@
### (1) 通过异步处理提高系统性能(削峰、减少响应所需时间)
![通过异步处理提高系统性能](https://user-gold-cdn.xitu.io/2018/4/21/162e63a8e34ba534?w=910&h=350&f=jpeg&s=29123)
![通过异步处理提高系统性能](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/Asynchronous-message-queue.png)
  如上图,**在不使用消息队列服务器的时候,用户的请求数据直接写入数据库,在高并发的情况下数据库压力剧增,使得响应速度变慢。但是在使用消息队列之后,用户的请求数据发送给消息队列之后立即 返回,再由消息队列的消费者进程从消息队列中获取数据,异步写入数据库。由于消息队列服务器处理速度快于数据库(消息队列也比数据库有更好的伸缩性),因此响应速度得到大幅改善。**
  通过以上分析我们可以得出**消息队列具有很好的削峰作用的功能**——即**通过异步处理,将短时间高并发产生的事务消息存储在消息队列中,从而削平高峰期的并发事务。** 举例:在电子商务一些秒杀、促销活动中,合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击。如下图所示:
![合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击](https://user-gold-cdn.xitu.io/2018/4/21/162e64583dd3ed01?w=780&h=384&f=jpeg&s=13550)
![削峰](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/削峰-消息队列.png)
  因为**用户请求数据写入消息队列之后就立即返回给用户了,但是请求数据在后续的业务校验、写数据库等操作中可能失败**。因此使用消息队列进行异步处理之后,需要**适当修改业务流程进行配合**,比如**用户在提交订单之后,订单数据写入消息队列,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进程真正处理完该订单之后,甚至出库后,再通过电子邮件或短信通知用户订单成功**,以免交易纠纷。这就类似我们平时手机订火车票和电影票。
### (2) 降低系统耦合性
  我们知道如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性无疑更好一些。
  使用消息队列还可以降低系统耦合性。我们知道如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性无疑更好一些。还是直接上图吧:
  我们最常见的**事件驱动架构**类似生产者消费者模式,在大型网站中通常用利用消息队列实现事件驱动结构。如下图所示:
![解耦](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/消息队列-解耦.png)
![利用消息队列实现事件驱动结构](https://user-gold-cdn.xitu.io/2018/4/21/162e6665fa394b3b?w=790&h=290&f=jpeg&s=14946)
  生产者(客户端)发送消息到消息队列中去,接受者(服务端)处理消息,需要消费的系统直接去消息队列取消息进行消费即可而不需要和其他系统有耦合, 这显然也提高了系统的扩展性。
  **消息队列使利用发布-订阅模式工作,消息发送者(生产者)发布消息,一个或多个消息接受者(消费者)订阅消息。** 从上图可以看到**消息发送者(生产者)和消息接受者(消费者)之间没有直接耦合**,消息发送者将消息发送至分布式消息队列即结束对消息的处理,消息接受者从分布式消息队列获取该消息后进行后续处理,并不需要知道该消息从何而来。**对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,从而实现网站业务的可扩展性设计**。

View File

@ -1,202 +0,0 @@
> 本文由JavaGuide整理翻译自做了适当删减、修改和补充
>
> - https://www.javaguides.net/2018/11/spring-boot-interview-questions-and-answers.html
> - https://www.algrim.co/posts/101-spring-boot-interview-questions
### 1. 什么是 Spring Boot?
首先,重要的是要理解 Spring Boot 并不是一个框架,它是一种创建独立应用程序的更简单方法,只需要很少或没有配置(相比于 Spring 来说。Spring Boot最好的特性之一是它利用现有的 Spring 项目和第三方项目来开发适合生产的应用程序。
### 2. 说出使用Spring Boot的主要优点
1. 开发基于 Spring 的应用程序很容易。
2. Spring Boot 项目所需的开发或工程时间明显减少,通常会提高整体生产力。
3. Spring Boot不需要编写大量样板代码、XML配置和注释。
4. Spring引导应用程序可以很容易地与Spring生态系统集成如Spring JDBC、Spring ORM、Spring Data、Spring Security等。
5. Spring Boot遵循“固执己见的默认配置”以减少开发工作默认配置可以修改
6. Spring Boot 应用程序提供嵌入式HTTP服务器如Tomcat和Jetty可以轻松地开发和测试web应用程序。这点很赞普通运行Java程序的方式就能运行基于Spring Boot web 项目,省事很多)
7. Spring Boot提供命令行接口(CLI)工具用于开发和测试Spring Boot应用程序如Java或Groovy。
8. Spring Boot提供了多种插件可以使用内置工具(如Maven和Gradle)开发和测试Spring Boot应用程序。
### 3. 为什么需要Spring Boot?
Spring Framework旨在简化J2EE企业应用程序开发。Spring Boot Framework旨在简化Spring开发。
![why-we-need-springboot](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/why-we-need-springboot.png)
### 4. 什么是 Spring Boot Starters?
Spring Boot Starters 是一系列依赖关系的集合因为它的存在项目的依赖之间的关系对我们来说变的更加简单了。举个例子在没有Spring Boot Starters之前我们开发REST服务或Web应用程序时; 我们需要使用像Spring MVCTomcat和Jackson这样的库这些依赖我们需要手动一个一个添加。但是有了 Spring Boot Starters 我们只需要一个只需添加一个**spring-boot-starter-web**一个依赖就可以了这个依赖包含的字依赖中包含了我们开发REST 服务需要的所有依赖。
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
```
### 5. 如何在Spring Boot应用程序中使用Jetty而不是Tomcat?
Spring Boot Web starter使用Tomcat作为默认的嵌入式servlet容器, 如果你想使用 Jetty 的话只需要修改pom.xml(Maven)或者build.gradle(Gradle)就可以了。
**Maven:**
```xml
<!--从Web启动器依赖中排除Tomcat-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--添加Jetty依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
```
**Gradle:**
```groovy
compile("org.springframework.boot:spring-boot-starter-web") {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
}
compile("org.springframework.boot:spring-boot-starter-jetty")
```
说个题外话,从上面可以看出使用 Gradle 更加简洁明了,但是国内目前还是 Maven 使用的多一点,我个人觉得 Gradle 在很多方面都要好很多。
### 6. 介绍一下@SpringBootApplication注解
```java
package org.springframework.boot.autoconfigure;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
......
}
```
```java
package org.springframework.boot;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
}
```
可以看出大概可以把 `@SpringBootApplication `看作是 `@Configuration``@EnableAutoConfiguration``@ComponentScan ` 注解的集合。根据 SpringBoot官网这三个注解的作用分别是
- `@EnableAutoConfiguration`:启用 SpringBoot 的自动配置机制
- `@ComponentScan` 扫描被`@Component` (`@Service`,`@Controller`)注解的bean注解默认会扫描该类所在的包下所有的类。
- `@Configuration`允许在上下文中注册额外的bean或导入其他配置类
### 7. (重要)Spring Boot 的自动配置是如何实现的?
这个是因为`@SpringBootApplication `注解的原因,在上一个问题中已经提到了这个注解。我们知道 `@SpringBootApplication `看作是 `@Configuration``@EnableAutoConfiguration``@ComponentScan ` 注解的集合。
- `@EnableAutoConfiguration`:启用 SpringBoot 的自动配置机制
- `@ComponentScan` 扫描被`@Component` (`@Service`,`@Controller`)注解的bean注解默认会扫描该类所在的包下所有的类。
- `@Configuration`允许在上下文中注册额外的bean或导入其他配置类
`@EnableAutoConfiguration`是启动自动配置的关键,源码如下(建议自己打断点调试,走一遍基本的流程)
```java
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Import;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
```
`@EnableAutoConfiguration` 注解通过Spring 提供的 `@Import` 注解导入了`AutoConfigurationImportSelector`类(`@Import` 注解可以导入配置类或者Bean到当前类中
` ``AutoConfigurationImportSelector`类中`getCandidateConfigurations`方法会将所有自动配置类的信息以 List 的形式返回。这些配置信息会被 Spring 容器作 bean 来管理。
```java
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
```
自动配置信息有了,那么自动配置还差什么呢?
`@Conditional` 注解。`@ConditionalOnClass`(指定的类必须存在于类路径下),`@ConditionalOnBean`(容器中是否有指定的Bean)等等都是对`@Conditional`注解的扩展。拿 Spring Security 的自动配置举个例子:
`SecurityAutoConfiguration`中导入了`WebSecurityEnablerConfiguration`类,`WebSecurityEnablerConfiguration`源代码如下:
```java
@Configuration
@ConditionalOnBean(WebSecurityConfigurerAdapter.class)
@ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@EnableWebSecurity
public class WebSecurityEnablerConfiguration {
}
```
`WebSecurityEnablerConfiguration`类中使用`@ConditionalOnBean`指定了容器中必须还有`WebSecurityConfigurerAdapter` 类或其实现类。所以,一般情况下 Spring Security 配置类都会去实现 `WebSecurityConfigurerAdapter`,这样自动将配置就完成了。
更多内容可以参考这篇文章https://sylvanassun.github.io/2018/01/08/2018-01-08-spring_boot_auto_configure/
### 8. Spring Boot支持哪些嵌入式web容器
Spring Boot支持以下嵌入式servlet容器:
| **Name** | **Servlet Version** |
| ------------ | ------------------- |
| Tomcat 9.0 | 4.0 |
| Jetty 9.4 | 3.1 |
| Undertow 2.0 | 4.0 |
您还可以将Spring引导应用程序部署到任何Servlet 3.1+兼容的 Web 容器中。
这就是你为什么可以通过直接像运行 普通 Java 项目一样运行 SpringBoot 项目。这样的确省事了很多,方便了我们进行开发,降低了学习难度。
### 9. 什么是Spring Security ?
Spring Security 应该属于 Spring 全家桶中学习曲线比较陡峭的几个模块之一,下面我将从起源和定义这两个方面来简单介绍一下它。
- **起源:** Spring Security 实际上起源于 Acegi Security这个框架能为基于 Spring 的企业应用提供强大而灵活安全访问控制解决方案,并且框架这个充分利用 Spring 的 IoC 和 AOP 功能,提供声明式安全访问控制的功能。后面,随着这个项目发展, Acegi Security 成为了Spring官方子项目后来被命名为 “Spring Security”。
- **定义:**Spring Security 是一个功能强大且高度可以定制的框架侧重于为Java 应用程序提供身份验证和授权。——[官方介绍](https://spring.io/projects/spring-security)。
### 10. JPA 和 Hibernate 有哪些区别JPA 可以支持动态 SQL 吗?

View File

@ -13,6 +13,7 @@
- [Git 使用快速入门](#git-使用快速入门)
- [获取 Git 仓库](#获取-git-仓库)
- [记录每次更新到仓库](#记录每次更新到仓库)
- [一个好的 Git 提交消息](#一个好的-Git-提交消息)
- [推送改动到远程仓库](#推送改动到远程仓库)
- [远程仓库的移除与重命名](#远程仓库的移除与重命名)
- [查看提交历史](#查看提交历史)
@ -143,6 +144,17 @@ Git 有三种状态,你的文件可能处于其中之一:
6. **移除文件** `git rm filename` (从暂存区域移除,然后提交。)
7. **对文件重命名** `git mv README.md README`(这个命令相当于`mv README.md README``git rm README.md``git add README` 这三条命令的集合)
### 一个好的 Git 提交消息
一个好的 Git 提交消息如下:
标题行:用这一行来描述和解释你的这次提交
主体部分可以是很少的几行,来加入更多的细节来解释提交,最好是能给出一些相关的背景或者解释这个提交能修复和解决什么问题。
主体部分当然也可以有几段,但是一定要注意换行和句子不要太长。因为这样在使用 "git log" 的时候会有缩进比较好看。
提交的标题行描述应该尽量的清晰和尽量的一句话概括。这样就方便相关的 Git 日志查看工具显示和其他人的阅读。
### 推送改动到远程仓库
- 如果你还没有克隆现有仓库,并欲将你的仓库连接到某个远程服务器,你可以使用如下命令添加:·`git remote add origin <server>` ,比如我们要让本地的一个仓库和 Github 上创建的一个仓库关联可以这样`git remote add origin https://github.com/Snailclimb/test.git`
@ -257,3 +269,4 @@ git push origin
- [猴子都能懂得Git入门](https://backlog.com/git-tutorial/cn/intro/intro1_1.html)
- https://git-scm.com/book/en/v2
- [Generating a new SSH key and adding it to the ssh-agent](https://help.github.com/en/articles/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent)
- [一个好的 Git 提交消息,出自 Linus 之手](https://github.com/torvalds/subsurface-for-dirk/blob/a48494d2fbed58c751e9b7e8fbff88582f9b2d02/README#L88)

View File

@ -0,0 +1,94 @@
## 题外话
先来点题外话吧!如果想看正文的话可以直接看滑到下面正文。
来三亚旅行也有几天了,总体感觉很不错,后天就要返航回家了。偶尔出来散散心真的挺不错,放松一下自己的心情,感受一下大自然。个人感觉冬天的时候来三亚度假还是很不错的选择,但是不要 1 月份的时候过来(差不多就过年那会儿),那时候属于大旺季,各种东西特别是住宿都贵很多。而且,那时候的机票也很贵。很多人觉得来三亚会花很多钱,实际上你不是在大旺季来的话,花不了太多钱。我和我女朋友在这边玩的几天住的酒店都还不错(干净最重要!),价格差不多都在 200元左右有一天去西岛和天涯海角那边住的全海景房间也才要 200多不过过年那会儿可能会达到 1000+。
现在是晚上 7 点多,刚从外面玩耍完回来。女朋友拿着我的手机拼着图片,我一个只能玩玩电脑。这篇文章很早就想写了,毕竟不费什么事,所以逞着晚上有空写一下。
如果有读者想看去三亚拍的美照包括我和我女朋友的合照,可以在评论区扣个 “想看”,我可以整篇推文分享一下。
## 正文​
> 下面的 10 个项目还是很推荐的JS 的项目占比挺大,其他基本都是文档/学习类型的仓库。
说明:数据统计于 2019-11-27。
### 1. freeCodeCamp
- **Github地址**[https://github.com/freeCodeCamp/freeCodeCamp](https://github.com/freeCodeCamp/freeCodeCamp)
- **star**: 307 k
- **介绍**: 开放源码代码库和课程。与数百万人一起免费学习编程。网站:[https://www.freeCodeCamp.org](https://www.freecodecamp.org/) 一个友好的社区您可以在这里免费学习编码。它由捐助者支持、非营利组织运营以帮助数百万忙碌的成年人学习编程技术。这个社区已经帮助10,000多人获得了第一份开发人员的工作。这里的全栈Web开发课程是完全免费的并且可以自行调整进度。这里还有数以千计的交互式编码挑战可帮助您扩展技能。
比如我想学习 ES6 的语法,学习界面是下面这样的,你可以很方便地边练习边学习:
![Learn ES6](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/freecodemap-es6.jpg)
### 2. 996.ICU
- **Github地址**[https://github.com/996icu/996.ICU](https://github.com/996icu/996.ICU)
- **star**: 248 k
- **介绍**: `996.ICU` 是指“工作 996 生病 ICU” 。这是中国程序员之间的一种自嘲说法,意思是如果按照 996 的模式工作,那以后就得进 ICU 了。这个项目最早是某个中国程序员发起的,然后就火遍全网,甚至火到了全世界很多其他国家,其网站被翻译成了多种语言。网站地址:[https://996.icu](https://996.icu/)。
![996.ICU-website](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/996.icu.jpg)
### 3. vue
- **Github地址**[https://github.com/vuejs/vue](https://github.com/vuejs/vue)
- **star**: 153 k
- **介绍**: 尤大的前端框架。国人用的最多(容易上手,文档比较丰富),所以 Star 数量比较多还是有道理的。Vue (读音 /vjuː/,类似于 **view**) 是一套用于构建用户界面的**渐进式框架**。与其它大型框架不同的是Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与[现代化的工具链](https://cn.vuejs.org/v2/guide/single-file-components.html)以及各种[支持类库](https://github.com/vuejs/awesome-vue#libraries--plugins)结合使用时Vue 也完全能够为复杂的单页应用提供驱动。
### 4. React
- **Github地址**[https://gitstar-ranking.com/facebook/react](https://gitstar-ranking.com/facebook/react)
- **star**: 140 k
- **介绍**: Facebook 开源的大公司有保障。用于构建用户界面的声明式、基于组件开发高效且灵活的JavaScript框架。我司大部分项目的前端都是 React ,我自己也用过一段时间,感觉还不错,但是也有一些小坑。
### 5. tensorflow
- **Github地址**[https://github.com/tensorflow/tensorflow](https://github.com/tensorflow/tensorflow)
- **star**: 138 k
- **介绍**: 适用于所有人的开源机器学习框架。[TensorFlow](https://www.tensorflow.org/)是用于机器学习的端到端开源平台。TensorFlow最初是由Google机器智能研究组织内Google Brain团队的研究人员和工程师开发的用于进行机器学习和深度神经网络研究。该系统具有足够的通用性也可以适用于多种其他领域。TensorFlow提供了稳定的[Python](https://www.tensorflow.org/api_docs/python) 和[C ++](https://www.tensorflow.org/api_docs/cc) API以及[其他语言的](https://www.tensorflow.org/api_docs)非保证的向后兼容API 。
### 6. bootstrap
- **Github地址**[https://github.com/twbs/bootstrap](https://github.com/twbs/bootstrap)
- **star**: 137 k
- **介绍**: 相信初学前端的时候大家一定或多或少地接触过这个框架。官网说它是最受欢迎的HTMLCSS和JavaScript框架用于在网络上开发响应式移动优先项目。
### 7. free-programming-books
- **Github地址**[https://github.com/EbookFoundation/free-programming-books](https://github.com/EbookFoundation/free-programming-books)
- **star**: 132 k
- **介绍**: 免费提供的编程书籍。我自己没太搞懂为啥这个项目 Star 数这么多,知道的麻烦评论区吱一声。
### 8. Awesome
- **Github地址** [https://github.com/sindresorhus/awesome](https://github.com/sindresorhus/awesome)
- **star**: 120 k
- **介绍**: github 上很多的各种 Awesome 系列合集。
下面是这个开源仓库的目录,可以看出其涵盖了很多方面的内容。
<img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/awsome-contents.jpg" style="zoom:50%;" />
举个例子这个仓库里面就有两个让你的电脑更好用的开源仓库Mac 和 Windows都有
- Awesome Mac:https://github.com/jaywcjlove/awesome-mac/blob/master/README-zh.m
- Awsome Windows: https://github.com/Awesome-Windows/Awesome/blob/master/README-cn.md
### 9. You-Dont-Know-JS
- **Github地址**[https://github.com/getify/You-Dont-Know-JS](https://github.com/getify/You-Dont-Know-JS)
- **star**: 112 k
- **介绍**: 您还不认识JS书籍系列-第二版
### 10. oh-my-zsh
- **Github地址**[https://github.com/ohmyzsh/ohmyzsh](https://github.com/ohmyzsh/ohmyzsh)
- **star**: 99.4 k
- **介绍**: 一个令人愉快的社区驱动的框架拥有近1500个贡献者用于管理zsh配置。包括200多个可选插件rails, git, OSX, hub, capistrano, brew, ant, php, python等140多个主题可为您的早晨增光添彩以及一个自动更新工具可让您轻松保持与来自社区的最新更新……
下面就是 oh-my-zsh 提供的一个花里胡哨的主题:
![oh-my-zsh-theme](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/ohmyzsh-theme.png)