diff --git a/.gitignore b/.gitignore index 37d6ef0..fa43602 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ _config.yml .settings .springBeans .sts4-cache +.vscode ### IntelliJ IDEA ### .idea diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9a17302 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "qiniu.access_key": "5zQ0AuES-7MGr7y89zIyMIK7JsNUlpTLggi5JtGu", + "qiniu.bucket": "sihai", + "qiniu.domain": "image.ouyangsihai.cn", + "qiniu.enable": true, + "qiniu.secret_key": "Mq3GKXAmESb4QGcv8mY8-lNWt42G0AFHpTKgl5Yh", + "pasteImageToQiniu.access_key": "5zQ0AuES-7MGr7y89zIyMIK7JsNUlpTLggi5JtGu", + "pasteImageToQiniu.bucket": "sihai", + "pasteImageToQiniu.domain": "image.ouyangsihai.cn", + "pasteImageToQiniu.secret_key": "Mq3GKXAmESb4QGcv8mY8-lNWt42G0AFHpTKgl5Yh" +} \ No newline at end of file diff --git a/CNAME b/CNAME new file mode 100644 index 0000000..6efb3fd --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +github.ouyangsihai.cn \ No newline at end of file diff --git a/README.md b/README.md index b44b2eb..1c7180e 100644 --- a/README.md +++ b/README.md @@ -1,121 +1,201 @@ -**[JavaInterview](https://github.com/OUYANGSIHAI/JavaInterview)** 是本人在备战春招及这几年学习的知识沉淀,这里面有很多都是自己的原创文章,同时,也有很多是本在备战春招的过程中觉得对面试特别有帮助的文章,**[JavaInterview](https://github.com/OUYANGSIHAI/JavaInterview)** 不一定可以帮助你进入到 BAT 等大厂,但是,如果你认真研究,仔细思考,我相信你也可以跟我一样幸运的进入到腾讯等大厂。 +**[JavaInterview](https://github.com/OUYANGSIHAI/JavaInterview)** 是本人在备战春招及这几年学习的知识沉淀,这里面有很多都是自己的原创文章,同时,也有很多是本在备战春招的过程中觉得对面试特别有帮助的文章,**[JavaInterview](https://github.com/OUYANGSIHAI/JavaInterview)** 不一定可以帮助你进入到 BAT 等大厂,但是,如果你认真研究,仔细思考,我相信你也可以跟我一样幸运的进入到大厂。 -本人经常在 CSDN 写博客,累计**原创博客 400+**,拥有**访问量160W**,目前是 **CSDN 博客专家**,春招目前拿到了腾讯等大厂offer。 +本人经常在 CSDN 写博客,累计**原创博客 400+**,拥有**访问量251W+**,**CSDN 博客专家**,CSDN博客地址:[https://sihai.blog.csdn.net](https://sihai.blog.csdn.net),春招目前拿到了大厂offer。 如果觉得有帮助,给个 **star** 好不好,哈哈(目前还不是很完善,后面会一一补充)。 -**JavaInterview 最新地址**:https://github.com/OUYANGSIHAI/JavaInterview - **一起冲!!!** - -
-
+👉 如果你不知道该学习什么的话,请看 [Java 学习线路图是怎样的?](https://zhuanlan.zhihu.com/p/392712685) (原创不易,欢迎点赞),这是 2021 最新最完善的 Java 学习路线! + +👉 Java学习资源汇总(个人总结) + +- **Java基础到Java实战全套学习视频教程,包括多个企业级实战项目** + +- **面试算法资料,这是总结的算法资料,学完基本可以应付80%大厂** + +- **大厂面试资料,一年时间总结,覆盖Java所有技术点** + +- **面试思维导图,手打总结** + +👉 **Java各种电子书:各种技术相关的电子书** -
+👉 **Java面试思维导图(手打)**,我靠这些导图拿到了一线互联网公司的offer,关注公众号,回复:`思维导图`; -[![微信群](https://camo.githubusercontent.com/59d7f19ba1af85247e016858a63045f8fe9a8c19/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7765436861742de5beaee4bfa1e7bea42d626c75652e737667)](https://github.com/OUYANGSIHAI/JavaInterview#%E8%81%94%E7%B3%BB%E6%88%91) [![公众号](https://img.shields.io/badge/%E5%85%AC%E4%BC%97%E5%8F%B7-%E5%A5%BD%E5%A5%BD%E5%AD%A6java-orange)](https://github.com/OUYANGSIHAI/JavaInterview#%E5%85%AC%E4%BC%97%E5%8F%B7) [![公众号](https://camo.githubusercontent.com/6d206aa03f27a851cf994123ef7be1a8d3192d54/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6a75656a696e2de68e98e987912d626c75652e737667)](https://juejin.im/user/5a672822f265da3e55380f0b) [![投稿](https://camo.githubusercontent.com/85a04ac4953a80940570b5c86ce73a1d34ff1542/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6373646e2d4353444e2d7265642e737667)](https://blog.csdn.net/sihai12345) [![投稿](https://camo.githubusercontent.com/6efc9c83ef8e85b19ce2853b5f69d68255f0c037/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f62696c6962696c692de59394e593a9e59394e593a92d637269746963616c)](https://space.bilibili.com/441147490) -
-### 目录(ctrl + f 查找更香) +**划重点**:获取上面的资源,请关注我的公众号 `程序员的技术圈子`,**微信扫描下面二维码**,回复:`Java资料`,获取思维导图,绿色通道关注福利,等你拿。 +
+ + + +### 目录(ctrl + f 查找更香:不能点击的,还在写) + +- [个人经验](#个人经验) - [项目准备](#项目准备) - [面试知识点](#面试知识点) - [公司面经](#公司面经) - [Java](#java) - * [基础](#基础) - * [容器(包括juc)](#容器包括juc) - * [并发](#并发) - * [JVM](#jvm) - * [Java8](#java8) + - [基础](#基础) + - [容器(包括juc)](#容器包括juc) + - [基础容器](#基础容器) + - [阻塞容器](#阻塞容器) + - [并发](#并发) + - [JVM](#jvm) + - [Java8](#java8) - [计算机网络](#计算机网络) - [计算机操作系统](#计算机操作系统) - [Linux](#linux) - [数据结构与算法](#数据结构与算法) - * [数据结构](#数据结构) - * [算法](#算法) + - [数据结构](#数据结构) + - [算法](#算法) - [数据库](#数据库) - * [MySQL](#mysql) - + [mysql(优化思路)](#mysql优化思路) + - [MySQL](#mysql) + - [MySQL(优化思路)](#mysql优化思路) - [系统设计](#系统设计) - * [秒杀系统相关](#秒杀系统相关) - * [前后端分离](#前后端分离) - * [单点登录](#单点登录) - * [常用框架](#常用框架) - + [Spring](#Spring) - + [SpringBoot](#springboot) + - [秒杀系统相关](#秒杀系统相关) + - [前后端分离](#前后端分离) + - [单点登录](#单点登录) + - [常用框架](#常用框架) + - [Spring](#spring) + - [SpringBoot](#springboot) - [分布式](#分布式) - * [dubbo](#dubbo) - * [zookeeper](#zookeeper) - * [RocketMQ](#rocketmq) - * [RabbitMQ](#rabbitmq) - * [kafka](#kafka) - * [消息中间件](#消息中间件) - * [redis](#redis) - * [分布式系统](#分布式系统) -- [线上问题调优(虚拟机,tomcat)](#线上问题调优虚拟机,tomcat) + - [dubbo](#dubbo) + - [zookeeper](#zookeeper) + - [RocketMQ](#rocketmq) + - [RabbitMQ](#rabbitmq) + - [kafka](#kafka) + - [消息中间件](#消息中间件) + - [redis](#redis) + - [分布式系统](#分布式系统) +- [线上问题调优(虚拟机,tomcat)](#线上问题调优虚拟机tomcat) - [面试指南](#面试指南) - [工具](#工具) - * [Git](#git) - * [Docker](#docker) + - [Git](#git) + - [Docker](#docker) - [其他](#其他) - * [权限控制(设计、shiro)](#权限控制设计、shiro) -- [Java学习资源](#Java学习资源) -- [Java书籍推荐](#Java书籍推荐) + - [权限控制(设计、shiro)](#权限控制设计shiro) +- [Java学习资源](#java学习资源) +- [Java书籍推荐](#java书籍推荐) - [实战项目推荐](#实战项目推荐) +- [程序人生](#程序人生) - [说明](#说明) - * [JavaInterview介绍](#JavaInterview介绍) - * [关于转载](#关于转载) - * [如何对该开源文档进行贡献](#如何对该开源文档进行贡献) - * [为什么要做这个开源文档?](#为什么要做这个开源文档?) - * [投稿](#投稿) - * [联系我](#联系我) - * [公众号](#公众号) + - [JavaInterview介绍](#javainterview介绍) + - [关于转载](#关于转载) + - [如何对该开源文档进行贡献](#如何对该开源文档进行贡献) + - [为什么要做这个开源文档?](#为什么要做这个开源文档) + - [投稿](#投稿) + - [联系我](#联系我) + - [公众号](#公众号) +## 个人经验 -## 项目准备 +- [应届生如何准备校招,用我这一年的校招经历告诉你](https://sihai.blog.csdn.net/article/details/114258312?spm=1001.2014.3001.5502) +- [【大学到研究生自学Java的学习路线】这是一份最适合普通大众、非科班的路线,帮你快速找到一份满意的工作](https://sihai.blog.csdn.net/article/details/105964718?spm=1001.2014.3001.5502) +- [两个月的面试真实经历,告诉大家如何能够进入大厂工作?](https://sihai.blog.csdn.net/article/details/105807642) -- [如何进行项目的自我介绍呢?](docs/interview/自我介绍和项目介绍.md) +## 项目准备 -- [项目需要准备必备知识及方法](docs/project/秒杀项目总结.md) +- [我的个人项目介绍模板](docs/interview/自我介绍和项目介绍.md) +- [本人面试两个月真实经历:面试了20家大厂之后,发现这样介绍项目经验,显得项目很牛逼!](https://sihai.blog.csdn.net/article/details/105854760) +- [项目必备知识及解决方案](docs/project/秒杀项目总结.md) ## 面试知识点 -- [各公司面试知识点汇总](docs/interview-experience/面试常见知识.md) -- [面试常见问题分类汇总](docs/interview-experience/面试常见问题分类汇总.md) +- [各大公司面试知识点汇总](docs/interview-experience/各大公司面经.md) +- [Java后端面试常见问题分类汇总(高频考点)](docs/interview-experience/面试常见问题分类汇总.md) ## 公司面经 - [2020年各公司面试经验汇总](docs/interview-experience/各大公司面经.md) +- [最新!!招银网络科技Java面经,整理附答案](https://mp.weixin.qq.com/s/HAUOH-EYS_3Ho2XxYkTGXA) +- [拿了 30K 的 offer!](https://mp.weixin.qq.com/s/R4gZ8IuskxgxA1SZwfCOoA) +- [重磅面经!!四面美团最终拿到了 offer](https://mp.weixin.qq.com/s/P1mDcH5hEXqNp2Jpz5Qjmg) +- [十面阿里,七面头条](https://mp.weixin.qq.com/s/FErQnLvYnuZxiaDkYWPO5A) ## Java ### 基础 -- [Java基础系列文章](https://mp.weixin.qq.com/mp/homepage?__biz=MzI2OTQ4OTQ1NQ==&hid=1&sn=c455e51f87eaa9c12d6b45e0e4d33960&scene=1&devicetype=iOS13.3.1&version=17000c2b&lang=zh_CN&nettype=WIFI&ascene=7&session_us=gh_2bfc34fbf760&fontScale=100&wx_header=1) +这几篇文章虽然是基础,但是确实深入理解基础,如果你能很好的理解这些基础,那么对于Java基础面试题也是没有什么问题的,背面试题不如理解原理,很重要。 + +- [Java基础思维导图](http://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247483823&idx=1&sn=4588a874055e8ca54f2bbe1ede12cff4&scene=19#wechat_redirect) +- [Java基础(一) 深入解析基本类型](http://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247483948&idx=1&sn=cb0ae3d82a1629e3a0538b6f31e2473b&scene=19#wechat_redirect) +- [Java基础(二) 自增自减与贪心规则](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247483951&idx=1&sn=af5b54ed2e26d975f96643d9dfd66fab&scene=19#wechat_redirect) +- [Java基础(三) 加强型for循环与Iterator](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247483952&idx=1&sn=43130fdf815970e0e12347d057c6b24f&scene=19#wechat_redirect) +- [Java基础(四) java运算顺序的深入解析](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247483955&idx=1&sn=abfb3e8ac31cb84bb78216d9c953abc0&scene=19#wechat_redirect) +- [Java基础(五) String性质深入解析](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247483956&idx=1&sn=1c19164967621fa5449a7830d006c8f9&scene=19#wechat_redirect) +- [Java基础(六) switch语句的深入解析](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247483999&idx=1&sn=092ad983f87798360ef33b1485f3201b&scene=19#wechat_redirect) +- [Java基础(七) 深入解析java四种访问权限](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247484000&idx=1&sn=0b188c70ac54c65a0419ab0d5da14af4&scene=19#wechat_redirect) +- [Java基础(八) 深入解析常量池与装拆箱机制](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247484002&idx=1&sn=a1d9ec01c91537aca444408c989f5a50&scene=19#wechat_redirect) +- [Java基础(九) 可变参数列表介绍](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247484003&idx=1&sn=84366ed430c332d4b8e2b2d6b54280f4&scene=19#wechat_redirect) +- [Java基础(十) 深入理解数组类型](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247484004&idx=1&sn=9c58b6948f05bbeffea3552fec9ee9a6&scene=19#wechat_redirect) +- [Java基础(十一) 枚举类型](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247484005&idx=1&sn=5aaec133dca189fcabc86defcd54c5b8&scene=19#wechat_redirect) +- [类与接口(二)java的四种内部类详解](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247484075&idx=1&sn=e0fd37cc5c1eb5fb359ed3dc9c15af66&scene=19#wechat_redirect) +- [类与接口(三)java中的接口与嵌套接口](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247484076&idx=1&sn=1903edbc469b2660e51e1154a8b63a27&scene=19#wechat_redirect) +- [类与接口(四)方法重载解析](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247484078&idx=1&sn=db5f231dc64974057d4ee29af1649e8b&scene=19#wechat_redirect) +- [类与接口(五)java多态、方法重写、隐藏](https://mp.weixin.qq.com/s?__biz=MzI2OTQ4OTQ1NQ==&mid=2247484083&idx=1&sn=d5b3d1daca2eb4e8d9583d75e6f6ad6c&scene=19#wechat_redirect) + ### 容器(包括juc) +#### 基础容器 + +- [ArrayList源码分析及真实大厂面试题精讲](https://blog.csdn.net/sihai12345/article/details/138413307?spm=1001.2014.3001.5501) +- [LinkedList源码分析及真实大厂面试题精讲](https://blog.csdn.net/sihai12345/article/details/138413722?spm=1001.2014.3001.5501) +- [HashMap源码分析及真实大厂面试题精讲](https://blog.csdn.net/sihai12345/article/details/138416578?spm=1001.2014.3001.5501) +- TreeMap源码分析及真实大厂面试题精讲 +- TreeSet源码分析及真实大厂面试题精讲 +- LinkedHashMap源码分析及真实大厂面试题精讲 + +#### 阻塞容器 + +- [ConcurrentHashMap源码分析及真实大厂面试题精讲](https://blog.csdn.net/sihai12345/article/details/138420403) +- ArrayBlockingQueue源码分析及真实大厂面试题精讲 +- LinkedBlockingQueue源码分析及真实大厂面试题精讲 +- PriorityBlockingQueue源码分析及真实大厂面试题精讲 ### 并发 +- [Synchronized关键字精讲及真实大厂面试题解析](https://blog.csdn.net/sihai12345/article/details/138420474) +- [Volitale关键字精讲及真实大厂面试题解析](https://blog.csdn.net/sihai12345/article/details/138420521) +- 关于LRU的实现 +- [ThreadLocal面试中会怎么提问呢?](https://blog.csdn.net/sihai12345/article/details/138420558) +- [线程池的面试题,这篇文章帮你搞定它!](https://blog.csdn.net/sihai12345/article/details/138420591) ### JVM - [深入理解Java虚拟机系列](https://mp.weixin.qq.com/s/SZ87s3fmKL3Kc_tAMcOFQw) - [深入理解Java虚拟机系列--完全解决面试问题](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-xi-lie-jiao-cheng.html) +- [深入理解Java虚拟机-Java内存区域透彻分析](https://mp.weixin.qq.com/s/WuyxyelaXbU-lg-HVZ95TA) +- [深入理解Java虚拟机-JVM内存分配与回收策略原理,从此告别JVM内存分配文盲](https://mp.weixin.qq.com/s/IG_zU5xa7y4BB6PVP0Fmow) +- [深入理解Java虚拟机-常用vm参数分析](https://mp.weixin.qq.com/s/l8fsq07jI0svqBdBGxuOzA) +- [深入理解Java虚拟机-如何利用JDK自带的命令行工具监控上百万的高并发的虚拟机性能](https://mp.weixin.qq.com/s/wPgA5SDURCAqPsWkZGGX0g) +- [深入理解Java虚拟机-如何利用VisualVM对高并发项目进行性能分析](https://mp.weixin.qq.com/s/hhA9tI_rYNkJVbF-R45hbA) +- [深入理解Java虚拟机-你了解GC算法原理吗](https://mp.weixin.qq.com/s/SZ87s3fmKL3Kc_tAMcOFQw) +- [几个面试官常问的垃圾回收器,下次面试就拿这篇文章怼回去!](https://sihai.blog.csdn.net/article/details/105700527) +- [面试官100%会严刑拷打的 CMS 垃圾回收器,下次面试就拿这篇文章怼回去!](https://sihai.blog.csdn.net/article/details/105808878) +- [JVM 面试题 87 题详解](https://sihai.blog.csdn.net/article/details/118737581) ### Java8 -- [Java8快速学习教程](https://blog.ouyangsihai.cn/java8-zui-xin-jiao-cheng-bu-yi-yang-de-java8.html) -- [Java11的最新特性](https://blog.ouyangsihai.cn/java11-zheng-shi-fa-bu-liao-wo-men-gai-zen-me-ban.html) +- [Java8 Stream:2万字20个实例,玩转集合的筛选、归约、分组、聚合](https://mp.weixin.qq.com/s/u042M2Sw2glBlevIDVoSXg) +- [利用Java8新特征,重构传统设计模式,你学会了吗?](https://mp.weixin.qq.com/s/zZ6rWz_t_snYNiNyOtaGiQ) +- [Java8 之 lambda 表达式、方法引用、函数式接口、默认方式、静态方法](https://mp.weixin.qq.com/s/FdzNWIsEmHVe9Nehxvfa3w) +- [Java8之Consumer、Supplier、Predicate和Function攻略](https://sihai.blog.csdn.net/article/details/98193777) +- [Java8 的 Stream 流式操作之王者归来](https://sihai.blog.csdn.net/article/details/100434684) +- [Java11-17的最新特性](https://mp.weixin.qq.com/s/QPGdNn56mCCDIUS047_1cQ) ## 计算机网络 - [http面试问题全解析](docs/network/http面试问题全解析.md) +- [计算机网络常见面试题](https://sihai.blog.csdn.net/article/details/118737663) +- 关于tcp、udp网络模型的问题,这篇文章告诉你 +- http、https还不了解,别慌! +- 面试官问我计算机网络的问题,我一个问题给他讲半个小时 ## 计算机操作系统 @@ -123,28 +203,55 @@ ## Linux -- [java工程师linux命令,这篇文章就够了](https://blog.ouyangsihai.cn/java-gong-cheng-shi-linux-ming-ling-zhe-pian-wen-zhang-jiu-gou-liao.html) +- [java工程师linux命令,这篇文章就够了](https://mp.weixin.qq.com/s/bj28tvF9TwgwrH65OPjXZg) +- [linux常见面试题(基础版)](https://sihai.blog.csdn.net/article/details/118737736) +- [linux高频面试题](docs/operating-system/linux高频面试题.md) +- 常问的几个Linux面试题,通通解决它 ## 数据结构与算法 ### 数据结构 +- [跳表这种数据结构,你真的清楚吗,面试官可能会问这些问题!](https://blog.csdn.net/sihai12345/article/details/138419109) +- 红黑树你了解多少,不会肯定会被面试官怼坏 +- [B树,B+树,你了解多少,面试官问那些问题?](https://segmentfault.com/a/1190000020416577) +- [这篇文章带你彻底理解红黑树](https://sihai.blog.csdn.net/article/details/118738496) +- 二叉树、二叉搜索树、二叉平衡树、红黑树、B树、B+树 ### 算法 -- [2020年最新算法面试真题汇总](docs/dataStructures-algorithms/算法面试真题汇总.md) -- [2020年最新算法题型难点总结](docs/dataStructures-algorithms/算法题目难点题目总结.md) +- [从大学入门到研究生拿大厂offer,必须看的数据结构与算法书籍推荐,不好不推荐!](https://sihai.blog.csdn.net/article/details/106011624?spm=1001.2014.3001.5502) +- [2021年面试高频算法题题解](docs/dataStructures-algorithms/高频算法题目总结.md) +- [2021年最新剑指offer难题解析](docs/dataStructures-algorithms/剑指offer难点总结.md) +- [关于贪心算法的leetcode题目,这篇文章可以帮你解决80%](https://blog.ouyangsihai.cn/jie-shao-yi-xia-guan-yu-leetcode-de-tan-xin-suan-fa-de-jie-ti-fang-fa.html) +- [dfs题目这样去接题,秒杀leetcode题目](https://sihai.blog.csdn.net/article/details/106895319) +- [回溯算法不会,这篇文章一定得看](https://sihai.blog.csdn.net/article/details/106993339) +- 动态规划你了解多少,我来帮你入个们 +- 链表的题目真的不难,看了这篇文章你就知道有多简单了 +- 还在怕二叉树的题目吗? += 栈和队列的题目可以这样出题型,你掌握了吗 +- 数组中常用的几种leetcode解题技巧! ## 数据库 ### MySQL -- [MySQL深入理解教程-解决面试中的各种问题](https://blog.ouyangsihai.cn/mysql-shen-ru-li-jie-jiao-cheng-mysql-de-yi-zhu-shi-jie.html) +- [InnoDB与MyISAM等存储引擎对比](https://sihai.blog.csdn.net/article/details/100832158) +- [MySQL:从B树到B+树到索引再到存储引擎](https://mp.weixin.qq.com/s/QmG1FyWPp23klTVkTJvcUQ) +- [MySQL全文索引最强教程](https://blog.ouyangsihai.cn/mysql-quan-wen-suo-yin.html) +- [MySQL的又一神器-锁,MySQL面试必备](https://sihai.blog.csdn.net/article/details/102680104) +- [MySQL事务,这篇文章就够了](https://sihai.blog.csdn.net/article/details/102815801) +- [mysqldump工具命令参数大全](https://blog.ouyangsihai.cn/mysqldump-gong-ju-ming-ling-can-shu-da-quan.html) +- [看完这篇MySQL备份的文章,再也不用担心删库跑路了](https://blog.ouyangsihai.cn/kan-wan-zhe-pian-mysql-bei-fen-de-wen-zhang-zai-ye-bu-yong-dan-xin-shan-ku-pao-lu-liao.html) +- 关于MySQL索引,面试中面试官会怎么为难你,一定得注意 +- MySQL中的乐观锁、悲观锁,JDK中的乐观锁、悲观锁? -#### mysql(优化思路) +#### MySQL(优化思路) - [MySQL高频面试题](https://mp.weixin.qq.com/s/KFCkvfF84l6Eu43CH_TmXA) - [MySQL查询优化过程](https://mp.weixin.qq.com/s/jtuLb8uAIHJNvNpwcIZfpA) +- [面试官:MySQL 上亿大表,如何深度优化?](https://mp.weixin.qq.com/s/g-_Oz9CLJfBn_asJrzn6Yg) +- [老司机总结的12条 SQL 优化方案(非常实用)](https://mp.weixin.qq.com/s/7QuASKTpXOm54CgLiHqEJg) ## 系统设计 @@ -175,14 +282,14 @@ #### SpringBoot -- [springboot史上最全教程](https://blog.csdn.net/sihai12345/category_7779682.html) +- [springboot史上最全教程,11篇文章全解析](https://blog.csdn.net/sihai12345/category_7779682.html) - [微服务面试相关资料](docs/microservice/微服务相关资料.md) ## 分布式 ### dubbo -- [dubbo教程](https://blog.ouyangsihai.cn/dubbo-yi-pian-wen-zhang-jiu-gou-liao-dubbo-yu-dao-chu-lian.html) +- [dubbo入门实战教程,这篇文章真的再好不过了](https://segmentfault.com/a/1190000019896723) - [dubbo源码分析](http://cmsblogs.com/?p=5324) - [dubbo面试题](https://mp.weixin.qq.com/s/PdWRHgm83XwPYP08KnkIsw) - [dubbo面试题2](https://mp.weixin.qq.com/s/Kz0s9K3J9Lpvh37oP_CtCA) @@ -190,13 +297,7 @@ ### zookeeper - [什么是zookeeper?](https://mp.weixin.qq.com/s/i2_c4A0146B7Ev8QnofbfQ) - -- [Zookeeper教程](http://cmsblogs.com/?p=4139) - -- [zookeeper源码分析](http://cmsblogs.com/?p=4190) - - [zookeeeper面试题](https://segmentfault.com/a/1190000014479433) - - [zookeeper面试题2](https://juejin.im/post/5dbac7a0f265da4d2c5e9b3b) @@ -204,7 +305,6 @@ - [RocketMQ简单教程](https://juejin.im/post/5af02571f265da0b9e64fcfd) - [RocketMQ教程](https://mp.weixin.qq.com/s/VAZaU1DuKbpnaALjp_-9Qw) -- [RocketMQ源码分析](http://cmsblogs.com/?p=3236) - [RocketMQ面试题](https://blog.csdn.net/dingshuo168/article/details/102970988) ### RabbitMQ @@ -222,8 +322,6 @@ - [kafka面试题](https://blog.csdn.net/qq_28900249/article/details/90346599) - [kafka面试题2](http://trumandu.github.io/2019/04/13/Kafka%E9%9D%A2%E8%AF%95%E9%A2%98%E4%B8%8E%E7%AD%94%E6%A1%88%E5%85%A8%E5%A5%97%E6%95%B4%E7%90%86/) -- [分布式架构文章](https://blog.ouyangsihai.cn/fen-bu-shi-jia-gou-xi-lie-wen-zhang.html) - ### 消息中间件 - [消息中间件面试题总结](docs/project/消息中间件面试题.md) @@ -233,7 +331,6 @@ - [Redis设计与实现总结文章](https://blog.csdn.net/qq_41594698/category_9067680.html) - [Redis面试题必备:基础,面试题](https://mp.weixin.qq.com/s/3Fmv7h5p2QDtLxc9n1dp5A) - [Redis面试相关:其中包含redis知识](https://blog.csdn.net/qq_35190492/article/details/103105780) -- [Redis源码分析](http://cmsblogs.com/?p=4570) - [redis其他数据结构](https://blog.csdn.net/c_royi/article/details/82011208) ### 分布式系统 @@ -242,8 +339,8 @@ - [垃圾收集器ZGC](https://juejin.im/post/5dc361d3f265da4d1f51c670) - [jvm系列文章](https://crowhawk.github.io/tags/#JVM) - [一次JVM FullGC的背后,竟隐藏着惊心动魄的线上生产事故!](https://mp.weixin.qq.com/s/5SeGxKtwp6KZhUKn8jXi6A) -- [Java虚拟机调优文章](https://blog.ouyangsihai.cn/categories/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA/) -- [利用VisualVM对高并发项目进行性能分析](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-ru-he-li-yong-visualvm-dui-gao-bing-fa-xiang-mu-jin-xing-xing-neng-fen-xi.html#toc-heading-8) +- [深入理解Java虚拟机-如何利用JDK自带的命令行工具监控上百万的高并发的虚拟机性能](https://mp.weixin.qq.com/s/wPgA5SDURCAqPsWkZGGX0g) +- [深入理解Java虚拟机-如何利用VisualVM对高并发项目进行性能分析](https://mp.weixin.qq.com/s/hhA9tI_rYNkJVbF-R45hbA) - [JVM性能调优](https://www.iteye.com/blog/uule-2114697) - [百亿吞吐量服务的JVM性能调优实战](https://mp.weixin.qq.com/s?__biz=MzIwMzY1OTU1NQ==&mid=2247484236&idx=1&sn=b9743b2d7436f84e4617ff34e07abdd8&chksm=96cd4300a1baca1635a137294bc93c518c033ce01f843c9e012a1454b9f3ea3158fa1412e9da&scene=27&ascene=0&devicetype=android-24&version=26060638&nettype=WIFI&abtest_cookie=BAABAAoACwASABMABAAjlx4AUJkeAFmZHgBomR4AAAA%3D&lang=zh_CN&pass_ticket=%2F%2BLqr9N2EZtrEGLFo9vLA6Eqs89DSJ2CBKoAJFZ%2BBngphEP28dwmMQeSZcUB77qZ&wx_header=1) - [一次线上JVM调优实践,FullGC40次/天到10天一次的优化过程](https://blog.csdn.net/cml_blog/article/details/81057966) @@ -257,7 +354,7 @@ ### Git -- [实际开发中的git命令大全](https://www.jianshu.com/p/53a00fafbe99) +- [实际开发中的git命令大全](https://sihai.blog.csdn.net/article/details/106418135) ### Docker @@ -272,24 +369,28 @@ ## Java学习资源 -- [2020年Java学习资源,你想要的都在这里了](https://mp.weixin.qq.com/s/wLjjy7D57s3UOv4sr8Lxkg) - -**截图** - -![](http://image.ouyangsihai.cn/Fl0FhkpxLNw0_4-pe8_f8MwAyHzc) -![](http://image.ouyangsihai.cn/Fp3EtjR1FbKPJG2uPdGpMiFjHBNR) -![](http://image.ouyangsihai.cn/FqEKc4i6lsfLCRomFAIksG_rgveY) +- [2021年Java视频学习教程+项目实战](https://github.com/hello-go-maker/cs-learn-source) +- [2021 Java 1000G 最新学习资源大汇总](https://mp.weixin.qq.com/s/I0jimqziHqRNaIy0kXRCnw) ## Java书籍推荐 +- [从入门到拿大厂offer,必须看的数据结构与算法书籍推荐](https://blog.csdn.net/sihai12345/article/details/106011624) +- [全网最全电子书下载](https://github.com/hello-go-maker/cs-books) ## 实战项目推荐 >小心翼翼的告诉你,上面的资源当中就有很多**企业级项目**,没有项目一点不用怕,因为你看到了这个。 - [找工作,没有上的了台面的项目怎么办?](https://mp.weixin.qq.com/s/0oK43_z99pVY9dYVXyIeiw) +- [Java 实战项目推荐](https://github.com/hello-go-maker/cs-learn-source) +## 程序人生 + +- [我想是时候跟大学告别了](https://blog.csdn.net/sihai12345/article/details/86934341) +- [坚持,这两个字非常重要!](https://blog.csdn.net/sihai12345/article/details/89507366) +- [关于考研,这是我给大家的经验](https://blog.csdn.net/sihai12345/article/details/88548630) +- [从普通二本到研究生再到自媒体的年轻人,这是我的故事](https://segmentfault.com/a/1190000020317748) ## 说明 @@ -319,12 +420,12 @@ 添加我的微信备注 **github**, 即可入群。 -![](http://image.ouyangsihai.cn/FldnPFgz_u_3kt7YH_sHhAQL1kyt) - + ### 公众号 -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 +如果大家想要实时关注我更新的文章以及分享的干货的话,关注我的公众号 **程序员的技术圈子**。 + + -![](http://image.ouyangsihai.cn/FgUUPlQOlQtjbbdOs1RZK9gWxitV) diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000..c419263 --- /dev/null +++ b/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-cayman \ No newline at end of file diff --git a/assets/wx.jpg b/assets/wx.jpg new file mode 100644 index 0000000..3fd7f11 Binary files /dev/null and b/assets/wx.jpg differ diff --git "a/assets/\347\250\213\345\272\217\345\221\230\346\212\200\346\234\257\345\234\210\345\255\220.jpg" "b/assets/\347\250\213\345\272\217\345\221\230\346\212\200\346\234\257\345\234\210\345\255\220.jpg" new file mode 100644 index 0000000..8561507 Binary files /dev/null and "b/assets/\347\250\213\345\272\217\345\221\230\346\212\200\346\234\257\345\234\210\345\255\220.jpg" differ diff --git "a/docs/dataStructures-algorithms/\344\270\200\346\226\207\346\220\236\345\256\232\351\223\276\350\241\250\345\237\272\347\241\200\345\222\214\351\223\276\350\241\250\351\235\242\350\257\225\351\242\230.md" "b/docs/dataStructures-algorithms/\344\270\200\346\226\207\346\220\236\345\256\232\351\223\276\350\241\250\345\237\272\347\241\200\345\222\214\351\223\276\350\241\250\351\235\242\350\257\225\351\242\230.md" new file mode 100644 index 0000000..45fdc1a --- /dev/null +++ "b/docs/dataStructures-algorithms/\344\270\200\346\226\207\346\220\236\345\256\232\351\223\276\350\241\250\345\237\272\347\241\200\345\222\214\351\223\276\350\241\250\351\235\242\350\257\225\351\242\230.md" @@ -0,0 +1,6 @@ +### 链表基础结构 + +### 链表的常见操作 + +### 链表常见面试题 + diff --git "a/docs/dataStructures-algorithms/\345\211\221\346\214\207offer-Java\345\256\236\347\216\260\347\211\210\346\234\254.md" "b/docs/dataStructures-algorithms/\345\211\221\346\214\207offer-Java\345\256\236\347\216\260\347\211\210\346\234\254.md" new file mode 100644 index 0000000..256f99f --- /dev/null +++ "b/docs/dataStructures-algorithms/\345\211\221\346\214\207offer-Java\345\256\236\347\216\260\347\211\210\346\234\254.md" @@ -0,0 +1,3 @@ +https://blog.csdn.net/weixin_43774841/article/details/112912070 +https://zhuanlan.zhihu.com/p/84481303 +https://zhuanlan.zhihu.com/p/84481166 \ No newline at end of file diff --git "a/docs/dataStructures-algorithms/\345\211\221\346\214\207offer\351\232\276\347\202\271\346\200\273\347\273\223.md" "b/docs/dataStructures-algorithms/\345\211\221\346\214\207offer\351\232\276\347\202\271\346\200\273\347\273\223.md" new file mode 100644 index 0000000..9c32dde --- /dev/null +++ "b/docs/dataStructures-algorithms/\345\211\221\346\214\207offer\351\232\276\347\202\271\346\200\273\347\273\223.md" @@ -0,0 +1,788 @@ + + + + + +- [重构二叉树](#重构二叉树httpswwwnowcodercompractice8a19cbe657394eeaac2f6ea9b0f6fcf6tpid13tqid11157rp1rutacoding-interviewsqrutacoding-interviewsquestion-ranking) +- [旋转数组的最小数字](#旋转数组的最小数字httpswwwnowcodercompractice9f3231a991af4f55b95579b44b7a01batpid13rp1ru2fta2fcoding-interviewsqru2fta2fcoding-interviews2fquestion-ranking) +- [跳N级台阶](#跳n级台阶httpswwwnowcodercompractice22243d016f6b47f2a6928b4313c85387tpid13rp1ru2fta2fcoding-interviewsqru2fta2fcoding-interviews2fquestion-ranking) +- [栈的压入、弹出序列](#栈的压入-弹出序列httpswwwnowcodercompracticed77d11405cc7470d82554cb392585106tpid13rp1ru2fta2fcoding-interviewsqru2fta2fcoding-interviews2fquestion-ranking) +- [调整数组顺序使奇数位于偶数前面](#调整数组顺序使奇数位于偶数前面httpswwwnowcodercompracticebeb5aa231adc45b2a5dcc5b62c93f593tpid13rp1ru2fta2fcoding-interviewsqru2fta2fcoding-interviews2fquestion-ranking) +- [树的子结构](#树的子结构httpswwwnowcodercompractice6e196c44c7004d15b1610b9afca8bd88tpid13rp1ru2fta2fcoding-interviewsqru2fta2fcoding-interviews2fquestion-ranking) +- [二叉搜索树的后序遍历序列](#二叉搜索树的后序遍历序列httpswwwnowcodercompracticea861533d45854474ac791d90e447bafdtpid13rp1ru2fta2fcoding-interviewsqru2fta2fcoding-interviews2fquestion-ranking) +- [二叉树中和为某一值的路径](#二叉树中和为某一值的路径httpswwwnowcodercompracticeb736e784e3e34731af99065031301bcatpid13rp1ru2fta2fcoding-interviewsqru2fta2fcoding-interviews2fquestion-ranking) +- [二叉搜索树与双向链表](#二叉搜索树与双向链表httpswwwnowcodercompractice947f6eb80d944a84850b0538bf0ec3a5tpid13tqid11179rp1rutacoding-interviewsqrutacoding-interviewsquestion-ranking) +- [最小的k个数(partation)](#最小的k个数partationhttpswwwnowcodercompractice6a296eb82cf844ca8539b57c23e6e9bftpid13tqid11182rp1rutacoding-interviewsqrutacoding-interviewsquestion-ranking) +- [连续子数组的最大和(sum < 0置为0)](#连续子数组的最大和sum-0置为0httpswwwnowcodercompractice459bd355da1549fa8a49e350bf3df484tpid13tqid11183rp1rutacoding-interviewsqrutacoding-interviewsquestion-ranking) +- [整数中1出现的次数(从1到n整数中1出现的次数)](#整数中1出现的次数从1到n整数中1出现的次数httpswwwnowcodercompracticebd7f978302044eee894445e244c7eee6tpid13tqid11184rp1rutacoding-interviewsqrutacoding-interviewsquestion-ranking) +- [数组中的逆序对](#数组中的逆序对httpswwwnowcodercompractice96bd6684e04a44eb80e6a68efc0ec6c5tpid13tqid11188rp1rutacoding-interviewsqrutacoding-interviewsquestion-ranking) +- [两个链表的第一个公共结点](#两个链表的第一个公共结点httpswwwnowcodercompractice6ab1d9a29e88450685099d45c9e31e46tpid13tqid11189rp1rutacoding-interviewsqrutacoding-interviewsquestion-ranking) +- [数字在排序数组中出现的次数](#数字在排序数组中出现的次数httpswwwnowcodercompractice70610bf967994b22bb1c26f9ae901fa2tpid13tqid11190rp1rutacoding-interviewsqrutacoding-interviewsquestion-ranking) +- [和为S的连续正数序列](#和为s的连续正数序列httpswwwnowcodercompracticec451a3fd84b64cb19485dad758a55ebetpid13tqid11194rp1rutacoding-interviewsqrutacoding-interviewsquestion-ranking) +- [孩子们的游戏(圆圈中剩下的数)](#孩子们的游戏圆圈中剩下的数httpswwwnowcodercompracticef78a359491e64a50bce2d89cff857eb6tpid13tqid11199rp1rutacoding-interviewsqrutacoding-interviewsquestion-ranking) +- [不用加减乘除做加法](#不用加减乘除做加法httpswwwnowcodercompractice59ac416b4b944300b617d4f7f111b215tpid13tqid11201rp1rutacoding-interviewsqrutacoding-interviewsquestion-ranking) +- [数组中重复的数字](#数组中重复的数字httpswwwnowcodercompractice623a5ac0ea5b4e5f95552655361ae0a8tpid13tqid11203rp1rutacoding-interviewsqrutacoding-interviewsquestion-ranking) +- [正则表达式匹配](#正则表达式匹配) +- [表示数值的字符串](#表示数值的字符串) +- [删除链表中重复的结点](#删除链表中重复的结点) +- [二叉树的下一个结点](#二叉树的下一个结点) +- [对称的二叉树(序列化)](#对称的二叉树序列化) + + + + +### [重构二叉树](https://www.nowcoder.com/practice/8a19cbe657394eeaac2f6ea9b0f6fcf6?tpId=13&&tqId=11157&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +```java +/** + * Definition for binary tree + * public class TreeNode { + * int val; + * TreeNode left; + * TreeNode right; + * TreeNode(int x) { val = x; } + * } + */ +import java.util.*; +public class Solution { + public TreeNode reConstructBinaryTree(int [] pre,int [] in) { + if(pre.length == 0||in.length == 0){ + return null; + } + + // 前序遍历的第一个节点是根结点 + TreeNode node = new TreeNode(pre[0]); + for(int i = 0; i < in.length; i++){ + // 如果找到中序遍历和根结点一样, + // 那么中序遍历的左边是左子树,右边是右子树 + if(pre[0] == in[i]){ + node.left = reConstructBinaryTree(Arrays.copyOfRange(pre, 1, i+1), + Arrays.copyOfRange(in, 0, i)); + node.right = reConstructBinaryTree(Arrays.copyOfRange(pre, + i+1, pre.length), Arrays.copyOfRange(in, i+1,in.length)); + } + } + return node; + } +} +``` + +### [旋转数组的最小数字](https://www.nowcoder.com/practice/9f3231a991af4f55b95579b44b7a01ba?tpId=13&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +```java +import java.util.ArrayList; + +public class Solution { + public int minNumberInRotateArray(int [] array) { + int low = 0 ; int high = array.length - 1; + while(low < high){ + int mid = low + (high - low) / 2; + // 出现这种情况的array类似[3,4,5,6,0,1,2], + // 此时最小数字一定在mid的右边。 + if(array[mid] > array[high]){ + low = mid + 1; + // 出现这种情况的array类似 [1,0,1,1,1] 或者[1,1,1,0,1], + // 此时最小数字不好判断在mid左边还是右边,这时只好一个一个试 + }else if(array[mid] == array[high]){ + high = high - 1; + // 出现这种情况的array类似[2,2,3,4,5,6,6], + // 此时最小数字一定就是array[mid]或者在mid的左边 + }else{ + high = mid; + } + } + return array[low]; + } +} +``` + +### [跳N级台阶](https://www.nowcoder.com/practice/22243d016f6b47f2a6928b4313c85387?tpId=13&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +```java +// 每个台阶都有跳与不跳两种情况(除了最后一个台阶), +// 最后一个台阶必须跳。所以共用2^(n-1)中情况 +public int JumpFloorII(int target) { + return 1 << (target - 1); +} + +public int JumpFloorII(int target) { + if(target <= 0){ + return 0; + } + + int arr[] = new int[target + 1]; + arr[0] = 1; + int preSum = arr[0]; + for(int i = 1 ; i < arr.length ; i++){ + arr[i] = preSum; + preSum += arr[i]; + } + + return arr[target]; +} +``` + + +### [栈的压入、弹出序列](https://www.nowcoder.com/practice/d77d11405cc7470d82554cb392585106?tpId=13&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +```java +public boolean IsPopOrder(int [] arr1,int [] arr2) { + //input check + if(arr1 == null || arr2 == null + || arr1.length != arr2.length + || arr1.length == 0){ + return false; + } + Stack stack = new Stack(); + int length = arr1.length; + int i = 0, j = 0; + while(i < length && j < length){ + if(arr1[i] != arr2[j]){ + // 不相等,压入栈中 + stack.push(arr1[i++]); + }else{ + // 相等,同时弹出 + i++; + j++; + } + } + + // 栈中为空时,才满足条件 + while(j < length){ + if(arr2[j] != stack.peek()){ + return false; + }else{ + stack.pop(); + j++; + } + } + + return stack.empty() && j == length; +} + +``` + +### [调整数组顺序使奇数位于偶数前面](https://www.nowcoder.com/practice/beb5aa231adc45b2a5dcc5b62c93f593?tpId=13&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +```java +public void reOrderArray(int [] array) { + //相对位置不变,稳定性 + //插入排序的思想 + int m = array.length; + int k = 0;//记录已经摆好位置的奇数的个数 + for (int i = 0; i < m; i++) { + if (array[i] % 2 == 1) { + int j = i; + while (j > k) {//j >= k+1 + int tmp = array[j]; + array[j] = array[j-1]; + array[j-1] = tmp; + j--; + } + k++; + } + } +} + +// 空间换时间 +public class Solution { + public void reOrderArray(int [] array) { + if (array != null) { + int[] even = new int[array.length]; + int indexOdd = 0; + int indexEven = 0; + for (int num : array) { + if ((num & 1) == 1) { + array[indexOdd++] = num; + } else { + even[indexEven++] = num; + } + } + + for (int i = 0; i < indexEven; i++) { + array[indexOdd + i] = even[i]; + } + } + } +} + +``` + +### [树的子结构](https://www.nowcoder.com/practice/6e196c44c7004d15b1610b9afca8bd88?tpId=13&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +```java + +// 解法一 +public boolean HasSubtree(TreeNode root1,TreeNode root2) { + if(root1==null || root2==null) return false; + return doesTree1HasTree2(root1, root2)|| HasSubtree(root1.left, root2) + ||HasSubtree(root1.right, root2); +} + +private boolean doesTree1HasTree2(TreeNode root1,TreeNode root2) { + if(root2==null) return true; + if(root1==null) return false; + return root1.val==root2.val && doesTree1HasTree2(root1.left, root2.left) + && doesTree1HasTree2(root1.right, root2.right); +} + +// 解法二:先序遍历判断是否是子串 +public boolean HasSubtree(TreeNode root1,TreeNode root2) { + if(root2 == null){ + return false; + } + String str1 = pre(root1); + String str2 = pre(root2); + return str1.indexOf(str2) != -1; +} + +public String pre(TreeNode root){ + if(root == null){ + return ""; + } + String str = root.val + ""; + str += pre(root.left); + str += pre(root.right); + return str; +} +``` + +### [二叉搜索树的后序遍历序列](https://www.nowcoder.com/practice/a861533d45854474ac791d90e447bafd?tpId=13&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +BST的后序序列的合法序列是,对于一个序列S,最后一个元素是x (也就是根),如果去掉最后一个元素的序列为T,那么T满足:T可以分成两段,前一段(左子树)小于x,后一段(右子树)大于x,且这两段(子树)都是合法的后序序列。 +```java +public class Solution { + public boolean VerifySquenceOfBST(int [] sequence) { + if(sequence.length==0) + return false; + if(sequence.length==1) + return true; + return ju(sequence, 0, sequence.length-1); + + } + + public boolean ju(int[] a,int star,int root){ + if(star>=root) + return true; + int i = root; + //从后面开始找 + while(i>star&&a[i-1]>a[root]) + i--;//找到比根小的坐标 + //从前面开始找 star到i-1应该比根小 + for(int j = star;ja[root]) + return false;; + return ju(a,star,i-1)&&ju(a, i, root-1); + } +} +``` + +### [二叉树中和为某一值的路径](https://www.nowcoder.com/practice/b736e784e3e34731af99065031301bca?tpId=13&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) + +```java +public class Solution { + private ArrayList> listAll = + new ArrayList>(); + private ArrayList list = new ArrayList(); + public ArrayList> FindPath(TreeNode root,int target) { + if(root == null) return listAll; + list.add(root.val); + target -= root.val; + if(target == 0 && root.left == null + && root.right == null) + listAll.add(new ArrayList(list)); + FindPath(root.left, target); + FindPath(root.right, target); + list.remove(list.size()-1); + return listAll; + } +} +``` + +### [二叉搜索树与双向链表](https://www.nowcoder.com/practice/947f6eb80d944a84850b0538bf0ec3a5?tpId=13&&tqId=11179&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +```java +// 直接使用中序遍历 +public class Solution { + public TreeNode head = null; + public TreeNode dummy = null; + public TreeNode Convert(TreeNode pRootOfTree) { + ConvertSub(pRootOfTree); + return dummy; + } + + public void ConvertSub(TreeNode root){ + if(root == null) return; + ConvertSub(root.left); + + if(head == null){ + head = root; + dummy = root; + } else { + head.right = root; + root.left = head; + head = root; + } + + ConvertSub(root.right); + } +} +``` + +### [最小的k个数(partation)](https://www.nowcoder.com/practice/6a296eb82cf844ca8539b57c23e6e9bf?tpId=13&&tqId=11182&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +```java +// 方法一:最大堆 +public class Solution { + public ArrayList GetLeastNumbers_Solution(int[] input, int k) { + ArrayList result = new ArrayList(); + int length = input.length; + if(k > length || k == 0){ + return result; + } + PriorityQueue maxHeap = + new PriorityQueue(k, new Comparator() { + + @Override + public int compare(Integer o1, Integer o2) { + return o2.compareTo(o1); + } + }); + for (int i = 0; i < length; i++) { + if (maxHeap.size() != k) { + maxHeap.offer(input[i]); + } else if (maxHeap.peek() > input[i]) { + Integer temp = maxHeap.poll(); + temp = null; + maxHeap.offer(input[i]); + } + } + for (Integer integer : maxHeap) { + result.add(integer); + } + return result; + } +} + +// 全排序 +public class Solution { + public ArrayList GetLeastNumbers_Solution(int [] input, int k) { + ArrayList list = new ArrayList<>(); + if(k > input.length){ + return list; + } + Arrays.sort(input); + for(int i = 0; i < k; i++){ + list.add(input[i]); + } + return list; + } +} +``` + +### [连续子数组的最大和(sum < 0置为0)](https://www.nowcoder.com/practice/459bd355da1549fa8a49e350bf3df484?tpId=13&&tqId=11183&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +```java + +// dp +public int FindGreatestSumOfSubArray(int[] array) { + if(array.length == 0){ + return 0; + } + + int max = Integer.MIN_VALUE; + int[] dp = new int[array.length]; + for(int i = 0; i < array.length; i++){ + dp[i] = array[i]; + } + for(int i = 1; i < array.length; i++){ + dp[i] = Math.max(dp[i-1] + array[i],dp[i]); + } + + for(int i = 0; i < dp.length; i++){ + if(dp[i] > max){ + max = dp[i]; + } + } + return max; +} + +// sum < 0置为0 +public int FindGreatestSumOfSubArray(int[] array) { + if(array.length == 0){ + return 0; + } + + int max = Integer.MIN_VALUE; + int cur = 0; + for(int i = 0; i < array.length; i++){ + cur += array[i]; + max = Math.max(max,cur); + cur = cur < 0 ? 0 : cur; + } + return max; +} + +``` + +### [整数中1出现的次数(从1到n整数中1出现的次数)](https://www.nowcoder.com/practice/bd7f978302044eee894445e244c7eee6?tpId=13&&tqId=11184&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +```java +public int NumberOf1Between1AndN_Solution(int n) { + int count=0; + StringBuffer s=new StringBuffer(); + for(int i=1;i= end) + return; + //计算中间值,注意溢出 + int mid = start + (end - start)/2; + + //递归分 + divide(arr,start,mid); + divide(arr,mid+1,end); + + //治 + merge(arr,start,mid,end); + } + + private void merge(int[] arr,int start,int mid,int end){ + int[] temp = new int[end-start+1]; + + //存一下变量 + int i=start,j=mid+1,k=0; + //下面就开始两两进行比较,若前面的数大于后面的数,就构成逆序对 + while(i<=mid && j<=end){ + //若前面小于后面,直接存进去,并且移动前面数所在的数组的指针即可 + if(arr[i] <= arr[j]){ + temp[k++] = arr[i++]; + }else{ + temp[k++] = arr[j++]; + //a[i]>a[j]了,那么这一次,从a[i]开始到a[mid]必定都是大于这个a[j]的,因为此时分治的两边已经是各自有序了 + cnt = (cnt+mid-i+1)%1000000007; + } + } + //各自还有剩余的没比完,直接赋值即可 + while(i<=mid) + temp[k++] = arr[i++]; + while(j<=end) + temp[k++] = arr[j++]; + //覆盖原数组 + for (k = 0; k < temp.length; k++) + arr[start + k] = temp[k]; + } +} +``` + +### [两个链表的第一个公共结点](https://www.nowcoder.com/practice/6ab1d9a29e88450685099d45c9e31e46?tpId=13&&tqId=11189&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +```java +public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) { + if(pHead1 == null || pHead2 == null){ + return null; + } + + ListNode p1 = pHead1; + ListNode p2 = pHead2; + + while(p1 != p2){ + p1 = p1.next; + p2 = p2.next; + if(p1 != p2){ + if(p1 == null) p1 = pHead2; + if(p2 == null) p2 = pHead1; + } + } + + return p1; + +} +``` + +### [数字在排序数组中出现的次数](https://www.nowcoder.com/practice/70610bf967994b22bb1c26f9ae901fa2?tpId=13&&tqId=11190&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +```java +public class Solution { + public static int GetNumberOfK(int [] array , int k) { + int length = array.length; + if(length == 0){ + return 0; + } + int lastK = getLastK(array,k); + int firstK = getFirstK(array,k); + if(firstK != -1 && lastK != -1){ + return lastK - firstK + 1; + } + return 0; + } + + public static int getLastK(int [] array, int k){ + int start = 0, end = array.length - 1; + int mid = start + (end - start) / 2; + while(start <= end){ + if(array[mid] > k){ + end = mid - 1; + } else if(array[mid] < k){ + start = mid + 1; + } else if(mid + 1 < array.length && array[mid+1] == k){ + start = mid + 1; + }else { + return mid; + } + mid = start + (end - start) / 2; + } + return -1; + } + + public static int getFirstK(int [] array, int k){ + int start = 0, end = array.length - 1; + int mid = start + (end - start) / 2; + while(start <= end){ + if(array[mid] > k){ + end = mid - 1; + } else if(array[mid] < k){ + start = mid + 1; + } else if(mid - 1 >= 0 && array[mid-1] == k){ + end = mid - 1; + }else { + return mid; + } + mid = start + (end - start) / 2; + } + + return -1; + } +} +``` + +### [和为S的连续正数序列](https://www.nowcoder.com/practice/c451a3fd84b64cb19485dad758a55ebe?tpId=13&&tqId=11194&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +```java +import java.util.ArrayList; +public class Solution { + public ArrayList > FindContinuousSequence(int sum) { + ArrayList> list = new ArrayList<>(); + int left = 1; + int right = 2; + int res = 0; + while(left < right){ + ArrayList temp = new ArrayList<>(); + res = 0; + int index = left; + while(index <= right){ + //收集临时值 + temp.add(index); + res+=index; + index++; + } + + if(res < sum){ + right++; + } else if(res > sum){ + left++; + } else { + //收集结果 + list.add(temp); + left++;//如果找到一个结果,left右移,继续找其他结果 + } + + } + return list; + } +} +``` + +### [孩子们的游戏(圆圈中剩下的数)](https://www.nowcoder.com/practice/f78a359491e64a50bce2d89cff857eb6?tpId=13&&tqId=11199&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +```java +import java.util.*; +public class Solution { + public int LastRemaining_Solution(int n, int m) { + LinkedList list = new LinkedList<>(); + for(int i = 0; i < n; i++){ + list.add(i); + } + int bt = 0; + while(list.size() > 1){ + bt = (bt + m - 1) % list.size(); + list.remove(bt); + } + + return list.size() == 1 ? list.get(0) : -1; + } +} +``` + +### [不用加减乘除做加法](https://www.nowcoder.com/practice/59ac416b4b944300b617d4f7f111b215?tpId=13&&tqId=11201&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +```java +public class Solution { + public int Add(int num1,int num2) { + while(num2 != 0){ + int sum = num1 ^ num2;//不进位求值 + int carry = (num1 & num2) << 1;//求进位值 + num1 = sum; + num2 = carry; + } + return num1; + } +} +``` + +### [数组中重复的数字](https://www.nowcoder.com/practice/623a5ac0ea5b4e5f95552655361ae0a8?tpId=13&&tqId=11203&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) + +```java +//boolean只占一位,所以还是比较省的 +public boolean duplicate(int numbers[], int length, int[] duplication) { + boolean[] k = new boolean[length]; + for (int i = 0; i < k.length; i++) { + if (k[numbers[i]] == true) { + duplication[0] = numbers[i]; + return true; + } + k[numbers[i]] = true; + } + return false; +} +``` + +### 正则表达式匹配 + +```java + +``` + +### 表示数值的字符串 + +```java +// 统一回复 .2 在 Java、Python 中都是数字 +public class Solution { + public boolean isNumeric(char[] str) { + String string = String.valueOf(str); + return string.matches("[\\+-]?[0-9]*(\\.[0-9]*)?([eE][\\+-]?[0-9]+)?"); + } +} +``` + +### 删除链表中重复的结点 + +```java +public ListNode deleteDuplication(ListNode pHead){ + if(pHead == null){ + return null; + } + ListNode node = new ListNode(Integer.MIN_VALUE); + node.next = pHead; + ListNode pre = node, p = pHead; + boolean deletedMode = false; + while(p != null){ + if(p.next != null && p.next.val == p.val){ + p.next = p.next.next; + deletedMode = true; + }else if(deletedMode){ + pre.next = p.next; + p = pre.next; + deletedMode = false; + }else{ + pre = p; + p = p.next; + } + } + + return node.next; +} + +``` + +### 二叉树的下一个结点 + +```java +public TreeLinkNode GetNext(TreeLinkNode pNode){ + if(pNode == null){ + return null; + } + //如果有右子树,后继结点是右子树上最左的结点 + if(pNode.right != null){ + TreeLinkNode p = pNode.right; + while(p.left != null){ + p = p.left; + } + return p; + }else{ + //如果没有右子树,向上查找第一个当前结点是父结点的左孩子的结点 + TreeLinkNode p = pNode.next; + while(p != null && pNode != p.left){ + pNode = p; + p = p.next; + } + + if(p != null && pNode == p.left){ + return p; + } + return null; + } +} + +``` + +### 对称的二叉树(序列化) + +```java +boolean isSymmetrical(TreeNode pRoot){ + if(pRoot == null){ + return true; + } + StringBuffer str1 = new StringBuffer(""); + StringBuffer str2 = new StringBuffer(""); + preOrder(pRoot, str1); + preOrder2(pRoot, str2); + return str1.toString().equals(str2.toString()); +} + +public void preOrder(TreeNode root, StringBuffer str){ + if(root == null){ + str.append("#"); + return; + } + str.append(String.valueOf(root.val)); + preOrder(root.left, str); + preOrder(root.right, str); +} + +public void preOrder2(TreeNode root, StringBuffer str){ + if(root == null){ + str.append("#"); + return; + } + str.append(String.valueOf(root.val)); + preOrder2(root.right, str); + preOrder2(root.left, str); +} + +``` \ No newline at end of file diff --git "a/docs/dataStructures-algorithms/\345\267\246\347\245\236\347\233\264\351\200\232 BAT \347\256\227\346\263\225\345\256\236\347\216\260.md" "b/docs/dataStructures-algorithms/\345\267\246\347\245\236\347\233\264\351\200\232 BAT \347\256\227\346\263\225\345\256\236\347\216\260.md" new file mode 100644 index 0000000..8c53251 --- /dev/null +++ "b/docs/dataStructures-algorithms/\345\267\246\347\245\236\347\233\264\351\200\232 BAT \347\256\227\346\263\225\345\256\236\347\216\260.md" @@ -0,0 +1,4 @@ +https://juejin.cn/post/6844903779289006094 +https://juejin.cn/post/6844903779289022478 +https://juejin.cn/post/6844903779289006093 +https://juejin.cn/post/6844903779289022471 \ No newline at end of file diff --git "a/docs/dataStructures-algorithms/\347\250\213\345\272\217\345\221\230\344\273\243\347\240\201\351\235\242\350\257\225\346\214\207\345\215\227-Java\345\256\236\347\216\260.md" "b/docs/dataStructures-algorithms/\347\250\213\345\272\217\345\221\230\344\273\243\347\240\201\351\235\242\350\257\225\346\214\207\345\215\227-Java\345\256\236\347\216\260.md" new file mode 100644 index 0000000..6e99702 --- /dev/null +++ "b/docs/dataStructures-algorithms/\347\250\213\345\272\217\345\221\230\344\273\243\347\240\201\351\235\242\350\257\225\346\214\207\345\215\227-Java\345\256\236\347\216\260.md" @@ -0,0 +1 @@ +https://github.com/LyricYang/Internet-Recruiting-Algorithm-Problems/blob/master/CodeInterviewGuide/README.md \ No newline at end of file diff --git "a/docs/dataStructures-algorithms/\351\253\230\351\242\221\347\256\227\346\263\225\351\242\230\347\233\256\346\200\273\347\273\223.md" "b/docs/dataStructures-algorithms/\351\253\230\351\242\221\347\256\227\346\263\225\351\242\230\347\233\256\346\200\273\347\273\223.md" new file mode 100644 index 0000000..3c616a3 --- /dev/null +++ "b/docs/dataStructures-algorithms/\351\253\230\351\242\221\347\256\227\346\263\225\351\242\230\347\233\256\346\200\273\347\273\223.md" @@ -0,0 +1,6885 @@ +Ctrl+Shift+P(MacOS:cmd+shift+p)呼出命令面板,输入Markdown Preview Enhanced: Create Toc会生成一段类似,保存生成目录。 + + + + + + +- [Java API整理](#java-api整理) +- [Go API整理](#go-api整理) +- [链表](#链表) + - [排序](#排序) + - [翻转链表, NC78](#翻转链表-nc78) + - [LFU缓存结构设计](#lfu缓存结构设计) + - [设计LRU缓存结构, NC93](#设计lru缓存结构-nc93) + - [合并有序链表, NC33](#合并有序链表-nc33) + - [链表中的节点每K个一组翻转](#链表中的节点每k个一组翻转) + - [判断链表中是否有环](#判断链表中是否有环) + - [链表中环的入口结点](#链表中环的入口结点) + - [删除链表的倒数第n个节点](#删除链表的倒数第n个节点) + - [两个链表的第一个公共结点](#两个链表的第一个公共结点) + - [两个链表生成相加链表](#两个链表生成相加链表) + - [合并k个已排序的链表](#合并k个已排序的链表) + - [单链表的排序,NC70](#单链表的排序nc70) + - [判断链表是否为回文结构](#判断链表是否为回文结构) + - [链表内指定区间反转](#链表内指定区间反转) + - [删除有序链表中重复出现的元素](#删除有序链表中重复出现的元素) + - [环形链表的约瑟夫问题](#环形链表的约瑟夫问题) + - [链表的奇偶重排](#链表的奇偶重排) + - [重排链表(1->n->2->n-1)](#重排链表1-n-2-n-1) + - [二叉搜索树与双向链表](#二叉搜索树与双向链表) +- [队列、栈](#队列栈) + - [用两个栈实现队列](#用两个栈实现队列) + - [有效括号序列](#有效括号序列) + - [包含 min 函数的栈](#包含-min-函数的栈) + - [表达式求值](#表达式求值) + - [最长括号子串](#最长括号子串) + - [括号生成](#括号生成) +- [二叉树](#二叉树) + - [实现二叉树先序,中序和后序遍历](#实现二叉树先序中序和后序遍历) + - [二叉树的层序遍历](#二叉树的层序遍历) + - [二叉树的之字形层序遍历](#二叉树的之字形层序遍历) + - [在二叉树中找到两个节点的最近公共祖先](#在二叉树中找到两个节点的最近公共祖先) + - [重建二叉树](#重建二叉树) + - [输出二叉树的右视图(先重建,再输出右视图)](#输出二叉树的右视图先重建再输出右视图) + - [二叉树的最大深度](#二叉树的最大深度) + - [判断是不是平衡二叉树](#判断是不是平衡二叉树) + - [二叉树根节点到叶子节点的所有路径和](#二叉树根节点到叶子节点的所有路径和) + - [二叉树中和为某一值的路径,返回所有路径](#二叉树中和为某一值的路径返回所有路径) + - [判断一棵二叉树是否为搜索二叉树和完全二叉树](#判断一棵二叉树是否为搜索二叉树和完全二叉树) + - [二叉树的最大路径和](#二叉树的最大路径和) + - [判断二叉树是否对称](#判断二叉树是否对称) + - [二叉树中是否存在节点和为指定值的路径](#二叉树中是否存在节点和为指定值的路径) + - [序列化二叉树](#序列化二叉树) + - [二叉搜索树的第k个结点](#二叉搜索树的第k个结点) + - [把二叉树打印成多行](#把二叉树打印成多行) + - [二叉树的镜像](#二叉树的镜像) + - [判断t1树中是否有与t2树拓扑结构完全相同的子树](#判断t1树中是否有与t2树拓扑结构完全相同的子树) + - [合并二叉树](#合并二叉树) + - [字典树的实现](#字典树的实现) + - [找到二叉搜索树中的两个错误节点](#找到二叉搜索树中的两个错误节点) +- [堆](#堆) + - [最小的K个数](#最小的k个数) + - [字符串出现次数的TopK问题](#字符串出现次数的topk问题) + - [寻找第K大](#寻找第k大) +- [双指针](#双指针) + - [最长无重复子数组的长度](#最长无重复子数组的长度) + - [滑动窗口的最大值](#滑动窗口的最大值) + - [合并区间(区间重叠)](#合并区间区间重叠) + - [反转字符串](#反转字符串) + - [数组中相加和为0的三元组](#数组中相加和为0的三元组) + - [接雨水问题](#接雨水问题) + - [最小覆盖子串(T包含S的最小子串)](#最小覆盖子串t包含s的最小子串) + - [两数之和](#两数之和) + - [最长重复子串(连续两个相同的字符串)](#最长重复子串连续两个相同的字符串) +- [动态规划](#动态规划) + - [跳台阶](#跳台阶) + - [连续子数组的最大和(sum < 0置为0)](#连续子数组的最大和sum--0置为0) + - [最长公共子串(返回具体字符串/长度)](#最长公共子串返回具体字符串长度) + - [斐波那契数列](#斐波那契数列) + - [最长回文子串的长度](#最长回文子串的长度) + - [最长递增子序列](#最长递增子序列) + - [买卖股票的最佳时机](#买卖股票的最佳时机) + - [矩阵的最小路径和](#矩阵的最小路径和) + - [编辑距离](#编辑距离) + - [不同路径的数目](#不同路径的数目) + - [最长公共子序列](#最长公共子序列) + - [最长的括号子串](#最长的括号子串) + - [高空扔鸡蛋](#高空扔鸡蛋) + - [兑换零钱](#兑换零钱) + - [最大正方形](#最大正方形) + - [通配符匹配](#通配符匹配) + - [正则表达式匹配](#正则表达式匹配) + - [矩阵最长递增路径](#矩阵最长递增路径) + - [最长上升子序列](#最长上升子序列) + - [目标和(完全背包)](#目标和完全背包) + - [打家劫舍](#打家劫舍) + - [带权值的最小路径和](#带权值的最小路径和) + - [最长不含重复字符的子字符串](#最长不含重复字符的子字符串) + - [把数字翻译成字符串](#把数字翻译成字符串) +- [二分](#二分) + - [求平方根](#求平方根) + - [在旋转过的有序数组中寻找目标值](#在旋转过的有序数组中寻找目标值) + - [在两个长度相等的排序数组中找到上中位数](#在两个长度相等的排序数组中找到上中位数) + - [有序矩阵元素查找](#有序矩阵元素查找) + - [二分查找](#二分查找) + - [旋转数组的最小数字](#旋转数组的最小数字) + - [数字在升序数组中出现的次数](#数字在升序数组中出现的次数) + - [峰值](#峰值) +- [数组](#数组) + - [数组中只出现一次的数字](#数组中只出现一次的数字) + - [合并两个有序的数组](#合并两个有序的数组) + - [子数组最大乘积](#子数组最大乘积) + - [数组中最长连续子序列](#数组中最长连续子序列) + - [数组中未出现的最小正整数](#数组中未出现的最小正整数) + - [顺时针旋转数组](#顺时针旋转数组) + - [旋转数组](#旋转数组) + - [逆序对](#逆序对) + - [调整数组顺序使奇数位于偶数前面](#调整数组顺序使奇数位于偶数前面) + - [矩阵乘法](#矩阵乘法) +- [回溯](#回溯) + - [字符串的全排列](#字符串的全排列) + - [岛屿的数量](#岛屿的数量) + - [没有重复项数字的所有排列(全排列)](#没有重复项数字的所有排列全排列) + - [集合的所有子集](#集合的所有子集) + - [重复项数字的所有排列](#重复项数字的所有排列) + - [N皇后问题](#n皇后问题) + - [把数组字符串转换为 ip 地址](#把数组字符串转换为-ip-地址) + - [加起来和为目标值的组合](#加起来和为目标值的组合) +- [其他](#其他) + - [螺旋矩阵](#螺旋矩阵) + - [顺时针旋转矩阵](#顺时针旋转矩阵) + - [进制转换](#进制转换) + - [反转数字](#反转数字) + - [大数加法](#大数加法) + - [把字符串转换成整数(atoi)](#把字符串转换成整数atoi) + - [最长公共前缀](#最长公共前缀) + - [回文数字](#回文数字) + - [字符串变形(反序,大写)](#字符串变形反序大写) + - [最大值(数组拼接最大数)](#最大值数组拼接最大数) + - [验证ip地址](#验证ip地址) + - [二进制中1的个数](#二进制中1的个数) + - [第一个只出现一次的字符](#第一个只出现一次的字符) +- [其他编程题(golang、java)](#其他编程题golangjava) + - [单例模式](#单例模式) + - [实现线程安全的生产者消费者](#实现线程安全的生产者消费者) + - [一个10G的文件,里面全部是自然数,一行一个,乱序排列,对其排序。在32位机器上面完成,内存限制为 2G(bitmap原理知道吗?)](#一个10g的文件里面全部是自然数一行一个乱序排列对其排序在32位机器上面完成内存限制为-2gbitmap原理知道吗) + - [实现使用字符串函数名,调用函数](#实现使用字符串函数名调用函数) + - [负载均衡算法。(一致性哈希)](#负载均衡算法一致性哈希) + - [(Goroutine)有三个函数,分别打印"cat", "fish","dog"要求每一个函数都用一个goroutine,按照顺序打印100次](#goroutine有三个函数分别打印cat-fishdog要求每一个函数都用一个goroutine按照顺序打印100次) + - [两个协程交替打印10个字母和数字](#两个协程交替打印10个字母和数字) + - [启动 2个groutine 2秒后取消, 第一个协程1秒执行完,第二个协程3秒执行完。](#启动-2个groutine-2秒后取消-第一个协程1秒执行完第二个协程3秒执行完) + - [当select监控多个chan同时到达就绪态时,如何先执行某个任务?](#当select监控多个chan同时到达就绪态时如何先执行某个任务) + + + +## Java API整理 + +- api + +https://blog.csdn.net/qq_34756156/article/details/120713595 + +## Go API整理 + +- api + +https://www.pseudoyu.com/zh/2021/05/29/algorithm_data_structure_go/ +https://greyireland.gitbook.io/algorithm-pattern/ru-men-pian/golang + +- 刷题模板 + +https://greyireland.gitbook.io/algorithm-pattern/ + +## 链表 + +### 排序 + +```java +import java.util.*; + + +public class Solution { + + public int[] MySort(int[] arr) { +// 选择排序 +// return selectSort(arr); +// 冒泡排序 +// return bubbleSort(arr); +// 插入排序 +// return insertSort(arr); +// 希尔排序 +// return shellSort(arr); +// 归并排序 +// return mergeSort(arr,0,arr.length-1); +// 快速排序 +// quickSort(arr,0,arr.length-1); +// return arr; +// 计数排序 +// return countSort(arr); +// 基数排序 +// return radixSort(arr); +// 桶排序 + return bucketSort(arr); + } + // 选择排序---选择最小的数与当前数交换 + public int[] selectSort(int[] arr){ + if(arr.length<2)return arr; + for(int i=0;iarr[j])swap(arr,i,j); + } + } + return arr; + } + + // 插入排序---与当前位置之前的所有元素比较,交换元素 + public int[] insertSort(int[] arr){ + if(arr.length<2)return arr; + for(int i=1;i0;j--){ + if(arr[j]0;gap=(gap-1)/3){ + for(int i=gap;i=0;j=j-gap){ + if(arr[j]= right) return ; + int pivot = arr[left]; + int i = left,j = right; + while(i < j){ + while(arr[j] >= pivot && j>i){ + j--; + } + while(arr[i] <= pivot && i0){count++;temp = temp/10;} + if(count>max)max = count; + } + + for(int m=0;m0){temp=temp/10;} + int result = temp%10; + for(int k=0;k0){ + arr[k++] = countArr[i][j]; + } + } + } + return arr; + } + + // 桶排序---给定n个桶,找到最大数与最小数, + // 计算出每个桶能装的数的范围,将数分别放入符合条件的桶中, + // 对每个桶进行快速排序,最后合并 + public int[] bucketSort(int[] arr){ + // 设置桶的个数 + int bucket = 4; + // 找到数组中的最大最小值 + int min = arr[0],max=arr[0]; + for(int i=0;imax)max=arr[i]; + if(arr[i]= min && temp < min+range){ + for(int k =0;k= min+range && temp < min+2*range){ + for(int k =0;k= min+2*range && temp < max - range){ + for(int k =0;k= max - range && temp <= max){ + for(int k =0;k cache; // 存储缓存的内容 + Map> freqMap; // 存储每个频次对应的双向链表 + int size; + int capacity; + int min; // 存储当前最小频次 + + public LFUCache(int capacity) { + cache = new HashMap<> (capacity); + freqMap = new HashMap<>(); + this.capacity = capacity; + } + + public int get(int key) { + Node node = cache.get(key); + if (node == null) { + return -1; + } + freqInc(node); + return node.value; + } + + public void put(int key, int value) { + if (capacity == 0) { + return; + } + Node node = cache.get(key); + if (node != null) { + node.value = value; + freqInc(node); + } else { + if (size == capacity) { + Node deadNode = removeNode(); + cache.remove(deadNode.key); + size--; + } + Node newNode = new Node(key, value); + cache.put(key, newNode); + addNode(newNode); + size++; + } + } + + void freqInc(Node node) { + // 从原freq对应的链表里移除, 并更新min + int freq = node.freq; + LinkedHashSet set = freqMap.get(freq); + set.remove(node); + if (freq == min && set.size() == 0) { + min = freq + 1; + } + // 加入新freq对应的链表 + node.freq++; + LinkedHashSet newSet = freqMap.get(freq + 1); + if (newSet == null) { + newSet = new LinkedHashSet<>(); + freqMap.put(freq + 1, newSet); + } + newSet.add(node); + } + + void addNode(Node node) { + LinkedHashSet set = freqMap.get(1); + if (set == null) { + set = new LinkedHashSet<>(); + freqMap.put(1, set); + } + set.add(node); + min = 1; + } + + Node removeNode() { + LinkedHashSet set = freqMap.get(min); + Node deadNode = set.iterator().next(); + set.remove(deadNode); + return deadNode; + } +} + +class Node { + int key; + int value; + int freq = 1; + + public Node() {} + + public Node(int key, int value) { + this.key = key; + this.value = value; + } +} +``` + +### 设计LRU缓存结构, NC93 + +- lru-k算法:https://blog.csdn.net/love254443233/article/details/82598381 + +```java +import java.util.*; + +public class Solution { + /** + * lru design + * @param operators int整型二维数组 the ops + * @param k int整型 the k + * @return int整型一维数组 + */ + public int[] LRU (int[][] operators, int k) { + // write code here + ArrayList list = new ArrayList(); + LRUCache cache = new LRUCache(k); + for(int[] op : operators){ + if(op[0]==1){ + cache.put(op[1],op[2]); + }else{ + int val = cache.get(op[1]); + list.add(val); + } + } + int[] ans = new int[list.size()]; + for(int i=0;i map; + public LinkedList list; + public int capacity; + + public LRUCache(int capacity){ + this.capacity = capacity; + map = new HashMap<>(); + list = new LinkedList<>(); + } + + public int get(int key){ + if(!map.containsKey(key)){ + return -1; + } + Node temp = map.get(key); + put(key,temp.value); + return temp.value; + } + + public void put(int key, int value){ + Node node = new Node(key,value); + if(map.containsKey(key)){ + Node temp = map.get(key); + list.remove(temp); + list.addFirst(node); + map.put(key,node); + } else { + if(map.size() == capacity){ + Node last = list.removeLast(); + map.remove(last.key); + } + list.addFirst(node); + map.put(key,node); + } + } + +} +``` + +```go +type LRUCache struct { + capacity int + m map[int]*Node + head, tail *Node +} + +type Node struct { + Key int + Value int + Pre, Next *Node +} + +func (this *LRUCache) Get(key int) int { + if v, ok := this.m[key]; ok { + this.moveToHead(v) + return v.Value + } + return -1 +} + +func (this *LRUCache) moveToHead(node *Node) { + this.deleteNode(node) + this.addToHead(node) +} + +func (this *LRUCache) deleteNode(node *Node) { + node.Pre.Next = node.Next + node.Next.Pre = node.Pre +} + +func (this *LRUCache) removeTail() int { + node := this.tail.Pre + this.deleteNode(node) + return node.Key +} + +func (this *LRUCache) addToHead(node *Node) { + this.head.Next.Pre = node + node.Next = this.head.Next + node.Pre = this.head + this.head.Next = node +} + +func (this *LRUCache) Put(key int, value int) { + if v, ok := this.m[key]; ok { + v.Value = value + this.moveToHead(v) + return + } + + if this.capacity == len(this.m) { + rmKey := this.removeTail() + delete(this.m, rmKey) + } + + newNode := &Node{Key: key, Value: value} + this.addToHead(newNode) + this.m[key] = newNode +} + +func Constructor(capacity int) LRUCache { + head, tail := &Node{}, &Node{} + head.Next = tail + tail.Pre = head + return LRUCache{ + capacity: capacity, + m: map[int]*Node{}, + head: head, + tail: tail, + } +} +``` + +### 合并有序链表, NC33 + +```java +import java.util.*; + +/* + * public class ListNode { + * int val; + * ListNode next = null; + * } + */ + +public class Solution { + /** + * + * @param l1 ListNode类 + * @param l2 ListNode类 + * @return ListNode类 + */ + public ListNode mergeTwoLists (ListNode l1, ListNode l2) { + ListNode node = new ListNode(0); + ListNode res = node; + while(l1 != null && l2 != null){ + if(l1.val > l2.val){ + node.next = l2; + l2 = l2.next; + } else { + node.next = l1; + l1 = l1.next; + } + node = node.next; + } + + if(l1 != null){ + node.next = l1; + } + + if(l2 != null){ + node.next = l2; + } + + return res.next; + } +} +``` + +### 链表中的节点每K个一组翻转 + +```java +//明显递归解决,翻转第一组之后,以第二组的开头为头节点,继续翻转,转翻到最后,返回。 +public ListNode reverseKGroup(ListNode head, int k) { + if(head==null||head.next==null) + return head; + ListNode h=new ListNode(0); + h.next=head; + ListNode next=null,tmp=head,cur=head; + for(int i=1;i lists) { + if(lists == null || lists.size() == 0){ + return null; + } + + return mergeList(lists,0,lists.size()-1); + } + + public ListNode mergeList(ArrayList lists, int low, int high){ + if(low >= high){ + return lists.get(low); + } + + int mid = low + (high - low)/2; + ListNode left = mergeList(lists,low,mid); + ListNode right = mergeList(lists,mid+1,high); + return merge(left,right); + } + + public ListNode merge(ListNode left, ListNode right){ + ListNode h = new ListNode(-1); + ListNode tmp = h; + while(left != null && right != null){ + if(left.val < right.val){ + tmp.next = left; + left = left.next; + } else { + tmp.next = right; + right = right.next; + } + tmp = tmp.next; + } + + if(left != null){ + tmp.next = left; + } + + if(right != null){ + tmp.next = right; + } + + return h.next; + } +} +``` + +### 单链表的排序,NC70 + +- 堆排序 + +```java +import java.util.*; + +public class Solution { + /** + * + * @param head ListNode类 the head node + * @return ListNode类 + */ + public ListNode sortInList (ListNode head) { + // write code here + PriorityQueue heap = new PriorityQueue<>((n1, n2) -> n1.val - n2.val); + while (head != null) { + heap.add(head); + head = head.next; + } + ListNode dummy = new ListNode(-1); + ListNode cur = dummy; + while (!heap.isEmpty()) { + cur.next = heap.poll(); + cur = cur.next; + } + cur.next = null; + return dummy.next; + } +} +``` + +- 归并排序 + +```java +import java.util.*; +public class Solution { + //合并两段有序链表 + ListNode merge(ListNode pHead1, ListNode pHead2) { + //一个已经为空了,直接返回另一个 + if(pHead1 == null) + return pHead2; + if(pHead2 == null) + return pHead1; + //加一个表头 + ListNode head = new ListNode(0); + ListNode cur = head; + //两个链表都要不为空 + while(pHead1 != null && pHead2 != null){ + //取较小值的节点 + if(pHead1.val <= pHead2.val){ + cur.next = pHead1; + //只移动取值的指针 + pHead1 = pHead1.next; + }else{ + cur.next = pHead2; + //只移动取值的指针 + pHead2 = pHead2.next; + } + //指针后移 + cur = cur.next; + } + //哪个链表还有剩,直接连在后面 + if(pHead1 != null) + cur.next = pHead1; + else + cur.next = pHead2; + //返回值去掉表头 + return head.next; + } + + public ListNode sortInList (ListNode head) { + //链表为空或者只有一个元素,直接就是有序的 + if(head == null || head.next == null) + return head; + ListNode left = head; + ListNode mid = head.next; + ListNode right = head.next.next; + //右边的指针到达末尾时,中间的指针指向该段链表的中间 + while(right != null && right.next != null){ + left = left.next; + mid = mid.next; + right = right.next.next; + } + //左边指针指向左段的左右一个节点,从这里断开 + left.next = null; + //分成两段排序,合并排好序的两段 + return merge(sortInList(head), sortInList(mid)); + } +} +``` + +### 判断链表是否为回文结构 + +```java +import java.util.*; + +public class Solution { + /** + * + * @param head ListNode类 the head + * @return bool布尔型 + */ + public boolean isPail (ListNode head) { + ListNode slow = head; + ListNode fast = head; + while(fast != null && fast.next != null){ + fast = fast.next.next; + slow = slow.next; + } + + Stack stack = new Stack<>(); + while(slow != null){ + stack.add(slow.val); + slow = slow.next; + } + + while(!stack.isEmpty()){ + if(stack.pop() != head.val){ + return false; + } + + head = head.next; + } + + return true; + } +} +``` + +### 链表内指定区间反转 + +```java +public class Solution { + /** + * + * @param head ListNode类 + * @param m int整型 + * @param n int整型 + * @return ListNode类 + */ + public ListNode reverseBetween (ListNode head, int m, int n) { + // write code here + if(head == null || n == m){ + return head; + } + + ListNode dummy = new ListNode(0); + dummy.next = head; + + ListNode cur = dummy; + for(int i = 1; i < m; i++){ + cur = cur.next; + } + + // t1代表头,t2代表尾 + ListNode t1 = cur; + ListNode t2 = cur.next; + cur = t2; + + ListNode pre = null; + // 翻转链表 + for(int i = 0; i <= n - m; i++){ + ListNode temp = cur.next; + cur.next = pre; + pre = cur; + cur = temp; + } + + t1.next = pre; + t2.next = cur; + + return dummy.next; + } +} +``` + +### 删除有序链表中重复出现的元素 + +```java +public ListNode deleteDuplicates (ListNode head) { + ListNode dummy=new ListNode(0); + dummy.next=head; + ListNode pre=dummy; + ListNode p=head; + while(p!=null&&p.next!=null){ + if(p.val==p.next.val){ + while(p.next!=null&&p.val==p.next.val){ + p=p.next; + } + pre.next=p.next; + p=p.next; + } + else{ + pre=p; + p=p.next; + } + } + return dummy.next; +} +``` + +### 环形链表的约瑟夫问题 + +```java +public int ysf (int n, int m) { + // write code here + ListNode head = new ListNode(1) ,p1=head; + for(int i=2;i<=n;i++){ + ListNode temp = new ListNode(i); + p1.next=temp; + p1=p1.next; + } + p1.next=head; + while(n-->1){ + int num=m; + while(num-->1){ + p1=p1.next; + } + p1.next=p1.next.next; + } + return p1.val; +} +``` + +### 链表的奇偶重排 + +```java +import java.util.*; + +/* + * public class ListNode { + * int val; + * ListNode next = null; + * } + */ + +public class Solution { + /** + * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可 + * + * @param head ListNode类 + * @return ListNode类 + */ + public ListNode oddEvenList (ListNode head) { + // write code here + if (head == null || head.next == null) return head; + /* + odd 指向奇数节点的指针 + oddHead 指向初始奇数节点的头指针 + even 指向偶数节点的指针 + evenHead 指向初始偶数节点的头指针 + */ + ListNode odd = head, oddHead = head, even = head.next, evenHead = head.next; + + while (even != null && even.next != null) { + // 奇数节点指向偶数节点的 next + odd.next = even.next; + // 奇数节点指针后移 + odd = odd.next; + // 偶数节点指向奇数节点的 next + even.next = odd.next; + // 偶数节点指针后移 + even = even.next; + } + // 将奇数节点的 next 指向 偶数节点的初始头指针 + odd.next = evenHead; + // 返回奇数节点的初始头指针 + return oddHead; + } +} +``` + +### 重排链表(1->n->2->n-1) + +```java +import java.util.*; +public class Solution { + public void reorderList(ListNode head) { + if (head == null || head.next == null) return; + List list = new ArrayList<>(); + ListNode cur = head; + while (cur != null) { + list.add(cur); + cur = cur.next; + } + int l = 0, r = list.size() - 1; + while (l < r) { + list.get(l).next = list.get(r); + l++; + list.get(r).next = list.get(l); + r--; + } + list.get(l).next = null; + } +} +``` + +### 二叉搜索树与双向链表 + +```java +/** +public class TreeNode { + int val = 0; + TreeNode left = null; + TreeNode right = null; + + public TreeNode(int val) { + this.val = val; + + } +} +*/ +public class Solution { + public TreeNode head = null; + public TreeNode dummy = null; + public TreeNode Convert(TreeNode pRootOfTree) { + ConvertSub(pRootOfTree); + return dummy; + } + + public void ConvertSub(TreeNode root){ + if(root == null) return; + ConvertSub(root.left); + + if(head == null){ + head = root; + dummy = root; + } else { + head.right = root; + root.left = head; + head = root; + } + + ConvertSub(root.right); + } +} +``` + +## 队列、栈 + +### 用两个栈实现队列 + +```java +import java.util.Stack; +public class Solution { + Stack stack1 = new Stack(); + Stack stack2 = new Stack(); + + public void push(int node) { + stack1.add(node); + } + + public void pushToPop(){ + if(stack2.isEmpty()){ + while(!stack1.isEmpty()){ + stack2.add(stack1.pop()); + } + } + } + + public int pop() { + pushToPop(); + return stack2.pop(); + } +} +``` + +### 有效括号序列 + +```java +import java.util.*; +public class Solution { + + public boolean isValid (String s) { + // write code here + Stack stack = new Stack<>(); + char[] chs = s.toCharArray(); + for(int i = 0; i < chs.length; i++){ + if(stack.isEmpty()){ + stack.push(chs[i]); + } else if(chs[i] == '{' || chs[i] == '[' + || chs[i] == '('){ + stack.push(chs[i]); + } else if((chs[i] == '}' && stack.peek() == '{') || + (chs[i] == ']' && stack.peek() == '[') || + (chs[i] == ')' && stack.peek() == '(')){ + stack.pop(); + } + } + + return stack.isEmpty() ? true : false; + } +} +``` + +### 包含 min 函数的栈 + +```java +import java.util.Stack; + +public class Solution { + Stack minStack = new Stack<>(); + Stack stack = new Stack<>(); + + public void push(int node) { + if(minStack.isEmpty()){ + minStack.push(node); + } + + if(node < minStack.peek().intValue()){ + minStack.push(node); + } else { + minStack.push(minStack.peek()); + } + + stack.push(node); + } + + public void pop() { + if(stack.isEmpty()){ + return; + } + stack.pop(); + minStack.pop(); + } + + public int top() { + return minStack.peek(); + } + + public int min() { + return minStack.peek(); + } +} +``` + +### 表达式求值 + +step 1:使用栈辅助处理优先级,默认符号为加号。 +step 2:遍历字符串,遇到数字,则将连续的数字字符部分转化为int型数字。 +step 3:遇到左括号,则将括号后的部分送入递归,处理子问题; +遇到右括号代表已经到了这个子问题的结尾,结束继续遍历字符串,将子问题的加法部分相加为一个数字,返回。 +step 4:当遇到符号的时候如果是+,得到的数字正常入栈,如果是-,则将其相反数入栈, +如果是*,则将栈中内容弹出与后一个元素相乘再入栈。 +step 5:最后将栈中剩余的所有元素,进行一次全部相加。 + + +```java +import java.util.*; +public class Solution { + public ArrayList function(String s, int index){ + Stack stack = new Stack(); + int num = 0; + char op = '+'; + int i; + for(i = index; i < s.length(); i++){ + //数字转换成int数字 + //判断是否为数字 + if(s.charAt(i) >= '0' && s.charAt(i) <= '9'){ + num = num * 10 + s.charAt(i) - '0'; + if(i != s.length() - 1) + continue; + } + //碰到'('时,把整个括号内的当成一个数字处理 + if(s.charAt(i) == '('){ + //递归处理括号 + ArrayList res = function(s, i + 1); + num = res.get(0); + i = res.get(1); + if(i != s.length() - 1) + continue; + } + switch(op){ + //加减号先入栈 + case '+': + stack.push(num); + break; + case '-': + //相反数 + stack.push(-num); + break; + //优先计算乘号 + case '*': + int temp = stack.pop(); + stack.push(temp * num); + break; + } + num = 0; + //右括号结束递归 + if(s.charAt(i) == ')') + break; + else + op = s.charAt(i); + } + int sum = 0; + //栈中元素相加 + while(!stack.isEmpty()) + sum += stack.pop(); + ArrayList temp = new ArrayList(); + temp.add(sum); + temp.add(i); + return temp; + } + public int solve (String s) { + ArrayList res = function(s, 0); + return res.get(0); + } +} +``` + +### 最长括号子串 + +step 1:可以使用栈来记录左括号下标。 +step 2:遍历字符串,左括号入栈,每次遇到右括号则弹出左括号的下标。 +step 3:然后长度则更新为当前下标与栈顶下标的距离。 +step 4:遇到不符合的括号,可能会使栈为空,因此需要使用start记录上一次结束的位置, +这样用当前下标减去start即可获取长度,即得到子串。 +step 5:循环中最后维护子串长度最大值。 + +```java +import java.util.*; +public class Solution { + public int longestValidParentheses(String s) { + if (s == null || s.length() == 0) + return 0; + int[] dp = new int[s.length()]; + int ans = 0; + for (int i = 1; i < s.length(); i++) { + // 如果是'('直接跳过,默认为0 + if (s.charAt(i) == ')') { + if (s.charAt(i - 1) == '(') + dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2; + // 说明s.charAt(i - 1)==')' + else if (i - dp[i - 1] > 0 + && s.charAt(i - dp[i - 1] - 1) == '(') { + dp[i] = (i - dp[i - 1] > 1 + ? dp[i - dp[i - 1] - 2] : 0) + dp[i - 1] + 2; + // 因为加了一个左括号和一个右括号,所以是加2 + } + } + ans = Math.max(ans, dp[i]); + } + return ans; + } +} +``` + +```java +import java.util.*; +public class Solution { + public int longestValidParentheses (String s) { + int res = 0; + //记录上一次连续括号结束的位置 + int start = -1; + Stack st = new Stack(); + for(int i = 0; i < s.length(); i++){ + //左括号入栈 + if(s.charAt(i) == '(') + st.push(i); + //右括号 + else{ + //如果右括号时栈为空,不合法,设置为结束位置 + if(st.isEmpty()) + start = i; + else{ + //弹出左括号 + st.pop(); + //栈中还有左括号,说明右括号不够,减去栈顶位置就是长度 + if(!st.empty()) + res = Math.max(res, i - st.peek()); + //栈中没有括号,说明左右括号行号,减去上一次结束的位置就是长度 + else + res = Math.max(res, i - start); + } + } + } + return res; + } +} +``` + +### 括号生成 + +对于括号的题,核心基本都是: +"一个字符串是合法的括号组合"的*充分必要*条件是: + +1. 字符串中开口数等于闭口数 (这是废话) +2. 字符串的所有prefix都满足: 开口数>=闭口数 +举个栗子,比如 "()(())": +prefix: "(", "()", "()(", "()((", "()(()", "()(())". + +那么对与这道题,为满足1,2, 每一个位置可以有的permutation就是: + +1. 如果有多余的开口 -> 可以选开口 +2. 如果有多余未闭合的开口 -> 可以选闭口 + +剩下的就是正常的递归+回溯了 +时间: O(2^n), 每一位最多2个permutation +空间: O(n), 栈高是n + +```java +import java.util.*; + +public class Solution { + ArrayList ans = new ArrayList<>(); + + public ArrayList generateParenthesis (int n) { + permute(n, n, 0, new StringBuilder()); + return ans; + } + + void permute(int open, int close, int unclosedOpen, StringBuilder sb) { + // base case,开口闭口都用完了 + if (open == 0 && close == 0) { + ans.add(sb.toString()); + return; + } + + // always ok to pick an open bracket if there are any open-bracket + if (open > 0) { + sb.append("("); + permute(open-1, close, unclosedOpen+1, sb); + sb.deleteCharAt(sb.length()-1); + } + // can pick close bracket if there is any unclosed open-bracket + if (unclosedOpen > 0) { + sb.append(")"); + permute(open, close-1, unclosedOpen-1, sb); + sb.deleteCharAt(sb.length()-1); + } + } +} +``` + +## 二叉树 + +### 实现二叉树先序,中序和后序遍历 + +```java +import java.util.*; + +/* + * public class TreeNode { + * int val = 0; + * TreeNode left = null; + * TreeNode right = null; + * } + */ + +public class Solution { + /** + * + * @param root TreeNode类 the root of binary tree + * @return int整型二维数组 + */ + public int[][] threeOrders (TreeNode root) { + // write code here + ArrayList list1 = new ArrayList<>(); + ArrayList list2 = new ArrayList<>(); + ArrayList list3 = new ArrayList<>(); + front(root,list1,list2,list3); + int[][] ints = new int[3][list1.size()]; + for (int i = 0; i < list1.size(); i++) { + ints[0][i] = list1.get(i); + ints[1][i] = list2.get(i); + ints[2][i] = list3.get(i); + } + return ints; + } + + public void front(TreeNode root,ArrayList list1, + ArrayList list2,ArrayList list3){ + if(root == null){ + return; + } + + list1.add(root.val); + front(root.left,list1,list2,list3); + list2.add(root.val); + front(root.right,list1,list2,list3); + list3.add(root.val); + } +} +``` + +- 非递归遍历 + +- 前序遍历 + +用栈来保存信息,但是遍历的时候,是:**先输出根节点信息,然后压入右节点信息,然后再压入左节点信息。** + +```java +public void pre(Node head){ + if(head == null){ + return; + } + Stack stack = new Stack<>(); + stack.push(head); + while(!stack.isEmpty()){ + head = stack.poll(); + System.out.println(head.value + " "); + if(head.right != null){ + stack.push(head.right); + } + if(head.left != null){ + stack.push(head.left); + } + } + System.out.println(); +} +``` + +- 中序遍历 + +中序遍历的顺序是**左中右**,先一直左节点遍历,并压入栈中,当做节点为空时,输出当前节点,往右节点遍历。 + +```java +public void inorder(Node head){ + if(head == null){ + return; + } + Stack stack = new Stack<>(); + stack.push(head); + while(!stack.isEmpty() || head != null){ + if(head != null){ + stack.push(head); + head = head.left + } else { + head = stack.poll(); + System.out.println(head.value + " "); + head = head.right; + } + } + System.out.println(); +} +``` + +- 后序遍历 + +用两个栈来实现,压入栈1的时候为**先左后右**,栈1弹出来就是**中右左**,栈2收集起来就是**左右中**。 + +```java +// 后序遍历-迭代 +public void postIteOrders(TreeNode root, List postList) { + if (root == null) { + return; + } + // 用两个栈来实现 + // 通过 stack1 和 stack2 来配合可以实现 左 - 右 - 中的顺序 + Stack stack1 = new Stack<>(); + Stack stack2 = new Stack<>(); + stack1.push(root); + while (!stack1.isEmpty()) { + TreeNode node = stack1.pop(); + stack2.push(node); + // 先入左节点 + if (node.left != null) { + stack1.push(node.left); + } + // 在入右节点 + if (node.right != null) { + stack1.push(node.right); + } + + } + // 弹出元素 + while (!stack2.isEmpty()) { + postList.add(stack2.pop().val); + } +} +``` + +### 二叉树的层序遍历 + +```java +public ArrayList> levelOrder (TreeNode root) { + // write code here + ArrayList> result = new ArrayList<>(); + if (root == null) { + return result; + } + // 队列,用于存储元素 + Queue queue = new LinkedList<>(); + // 根节点先入队 + queue.offer(root); + // 当队列不为空的时候 + while(!queue.isEmpty()) { + // 队列的大小就是这一层的元素数量 + int size = queue.size(); + ArrayList list = new ArrayList<>(); + // 开始遍历这一层的所有元素 + for (int i = 0; i < size; i ++) { + TreeNode node = queue.poll(); + // 如果左节点不为空,则入队,作为下一层来遍历 + if(node.left != null) { + queue.offer(node.left); + } + // 同上 + if (node.right != null) { + queue.offer(node.right); + } + // 存储一层的节点 + list.add(node.val); + } + // 将一层所有的节点汇入到总的结果集中 + result.add(list); + } + return result; +} +``` + +### 二叉树的之字形层序遍历 + +```java +import java.util.Queue; +import java.util.LinkedList; + +public class Solution { + + public ArrayList> Print(TreeNode root) { + ArrayList> res = new ArrayList<>(); + if (root == null) + return res; + Queue queue = new LinkedList<>(); + queue.add(root); + boolean leftToRight = true; + while (!queue.isEmpty()) { + ArrayList level = new ArrayList<>(); + //统计这一行有多少个节点 + int count = queue.size(); + //遍历这一行的所有节点 + for (int i = 0; i < count; i++) { + //poll移除队列头部元素(队列在头部移除,尾部添加) + TreeNode node = queue.poll(); + //判断是从左往右打印还是从右往左打印。 + if (leftToRight) { + level.add(node.val); + } else { + level.add(0, node.val); + } + //左右子节点如果不为空会被加入到队列中 + if (node.left != null) + queue.add(node.left); + if (node.right != null) + queue.add(node.right); + } + res.add(level); + leftToRight = !leftToRight; + } + return res; + } +} +``` + +### 在二叉树中找到两个节点的最近公共祖先 + +```java +import java.util.*; + +public class Solution { + public int lowestCommonAncestor (TreeNode root, int o1, int o2) { + // root为空则说明越过了叶子节点 + if(root == null) return -1; + // 如果root为o1或o2中任意一个,则root就是公共祖先 + if(root.val == o1 || root.val == o2) return root.val; + + //root不为o1或o2 + int left = lowestCommonAncestor(root.left, o1, o2); + int right = lowestCommonAncestor(root.right, o1, o2); + //如果left=-1,说明在左子树中一直找到叶子节点,也没找到最近公共祖先 + //所以最近公共祖先,必在右子树中,right即为最近公共祖先 + if(left == -1) return right; + //同理,最近公共祖先必在左子树中,left即为最近公共祖先 + else if(right == -1) return left; + //若left和right都不为-1,则说明o1,o2节点在root的异侧,则root为最近公共祖先 + else return root.val; + } +} +``` + +### 重建二叉树 + +```java + +import java.util.*; +public class Solution { + public TreeNode reConstructBinaryTree(int [] pre,int [] in) { + if(pre.length == 0||in.length == 0){ + return null; + } + TreeNode node = new TreeNode(pre[0]); + for(int i = 0; i < in.length; i++){ + if(pre[0] == in[i]){ + node.left = reConstructBinaryTree( + Arrays.copyOfRange(pre, 1, i+1), + Arrays.copyOfRange(in, 0, i)); + node.right = reConstructBinaryTree( + Arrays.copyOfRange(pre, i+1, pre.length), + Arrays.copyOfRange(in, i+1,in.length)); + } + } + return node; + } +} +``` + +```java +public TreeNode reConstructBinaryTree(int [] pre, int [] in) { + TreeNode root = rebuild(pre, 0, pre.length - 1, + in, 0, in.length - 1); + return root; +} + +public TreeNode rebuild(int[] preorder, int preStart, + int preEnd, int[] inorder, int inStart, int inEnd) { + if (preStart < 0 || inStart < 0 || + preStart > preEnd || inStart > inEnd) { + return null; + } + TreeNode root = new TreeNode(preorder[preStart]); + + int index = 0; + for (int i = 0; i < inorder.length; i++) { + if (inorder[i] == root.val) { + index = i; + break; + } + } + int leftLen = index - inStart; + int rightLen = inEnd - index; + root.left = rebuild(preorder, preStart + 1, + leftLen + preStart, inorder, inStart, index - 1); + root.right = rebuild(preorder, leftLen + preStart + 1, + preEnd, inorder, index + 1, inEnd); + return root; +} +``` + +### 输出二叉树的右视图(先重建,再输出右视图) + +```java +public class Solution { + /** + * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可 + * 求二叉树的右视图 + * @param xianxu int整型一维数组 先序遍历 + * @param zhongxu int整型一维数组 中序遍历 + * @return int整型一维数组 + */ + public int[] solve (int[] preorder, int[] inorder) { + // write code here + if (preorder == null || preorder.length < 1 + || inorder == null || inorder.length < 1) { + return new int[0]; + } + TreeNode root = rebuild(preorder, 0, preorder.length - 1, + inorder, 0, inorder.length - 1); + LinkedList queue = new LinkedList<>(); + TreeNode cur = root; + queue.offer(cur); + List list = new ArrayList<>(); + while (!queue.isEmpty()) { + int size = queue.size(); + list.add(queue.peekLast().val); + for (int i = 0; i < size; i++) { + cur = queue.poll(); + if (cur.left != null) { + queue.offer(cur.left); + } + if (cur.right != null) { + queue.offer(cur.right); + } + } + } + int[] res = new int[list.size()]; + for (int i = 0; i < res.length; i++) { + res[i] = list.get(i); + } + return res; + } + + public TreeNode rebuild(int[] preorder, int preStart, + int preEnd, int[] inorder, int inStart, int inEnd) { + if (preStart < 0 || inStart < 0 || + preStart > preEnd || inStart > inEnd) { + return null; + } + TreeNode root = new TreeNode(preorder[preStart]); + + int index = 0; + for (int i = 0; i < inorder.length; i++) { + if (inorder[i] == root.val) { + index = i; + break; + } + } + int leftLen = index - inStart; + int rightLen = inEnd - index; + root.left = rebuild(preorder, preStart + 1, + leftLen + preStart, inorder, inStart, index - 1); + root.right = rebuild(preorder, leftLen + preStart + 1, + preEnd, inorder, index + 1, inEnd); + return root; + } + + public static class TreeNode { + public int val; + public TreeNode left; + public TreeNode right; + + public TreeNode(int val) { + this.val = val; + } + } +} +``` + +### 二叉树的最大深度 + +```java +public int maxDepth(TreeNode root) { + return root==null? 0 : + Math.max(maxDepth(root.left), maxDepth(root.right))+1; +} +``` + +### 判断是不是平衡二叉树 + +```java +import java.util.*; +public class Solution { + public boolean IsBalanced_Solution(TreeNode root) { + //可以分别求出左右子树的高度,然后进行对比 + return TreeDepth(root) >= 0; + } + //求二叉树深度的方法 + public int TreeDepth(TreeNode root) { + if (root == null) { + return 0; + } + int leftHeight = TreeDepth(root.left); + int rightHeight = TreeDepth(root.right); + if (leftHeight == -1 || rightHeight == -1 + || Math.abs(leftHeight - rightHeight) > 1) { + return -1; + } else { + return Math.max(leftHeight, rightHeight) + 1; + } + } +} +``` + +### 二叉树根节点到叶子节点的所有路径和 + +```java +public class Solution { + /** + * + * @param root TreeNode类 + * @return int整型 + */ + public int sumNumbers (TreeNode root) { + // 调用dfs + return dfs(root,0); + } + //深度优先搜索 + public int dfs(TreeNode root,int sum){ + if(root==null){ + return 0; + } + int total = sum*10+root.val; + //已达到叶子节点,返回结果 + if(root.left==null && root.right==null){ + return total; + }else{ + //递归调用 + return dfs(root.left,total)+dfs(root.right,total); + } + + } +} +``` + +### 二叉树中和为某一值的路径,返回所有路径 + +```java +import java.util.ArrayList; +public class Solution { + ArrayList> lists = new ArrayList<>(); + ArrayList list = new ArrayList<>(); + public ArrayList> FindPath(TreeNode root,int target) { + if(root == null){ + return lists; + } + list.add(root.val); + target -= root.val; + if(target == 0 && root.left == null + && root.right == null){ + lists.add(new ArrayList(list)); + } + FindPath(root.left,target); + FindPath(root.right,target); + list.remove(list.size() - 1); + return lists; + } +} +``` + +### 判断一棵二叉树是否为搜索二叉树和完全二叉树 + +```java +int num=-1; +boolean flag = false; +public boolean[] judgeIt (TreeNode root) { + // write code here + return new boolean[]{isSearch(root) ,isFull(root)}; +} + +public boolean isSearch(TreeNode root){ + if(root==null){ + return true; + } + boolean left = isSearch(root.left); + if(num>=root.val){ + return false; + } + num=root.val; + boolean right = isSearch(root.right); + return left && right; +} + +public boolean isFull(TreeNode root){ + Queue queue = new LinkedList<>(); + queue.add(root); + while(!queue.isEmpty()){ + TreeNode node = queue.poll(); + if(node==null){ + flag=true; + }else{ + if(flag){ + return false; + }else{ + queue.add(node.left); + queue.add(node.right); + } + } + } + return true; +} +``` + +### 二叉树的最大路径和 + +```java +public class Solution { + int max = Integer.MIN_VALUE; + /** + * + * @param root TreeNode类 + * @return int整型 + */ + public int maxPathSum (TreeNode root) { + // write code here + maxSum(root); + return max; + } + + public int maxSum(TreeNode root){ + if(root == null){ + return 0; + } + + //三种情况:1.包含一个子树和顶点,2.仅包含顶点,3.包含左子树和右子树以及顶点。 + int left = Math.max(maxSum(root.left),0); + int right = Math.max(maxSum(root.right),0); + + max = Math.max(max,left+right+root.val); + + //对于每一个子树,返回包含该子树顶点的深度方向的路径和的最大值。 + return root.val + Math.max(left,right); + } +} +``` + +### 判断二叉树是否对称 + +```java +public class Solution { + /** + * + * @param root TreeNode类 + * @return bool布尔型 + */ + public boolean isSymmetric (TreeNode root) { + // write code here + return isSymmetricNode(root,root); + } + + public boolean isSymmetricNode(TreeNode node1, TreeNode node2){ + if(node1 == null && node2 == null){ + return true; + } + if(node1 == null || node2 == null){ + return false; + } + if(node1.val != node2.val){ + return false; + } + return isSymmetricNode(node1.left,node2.right) + && isSymmetricNode(node1.right,node2.left); + } +} +``` + +### 二叉树中是否存在节点和为指定值的路径 + +```java +public class Solution { + /** + * + * @param root TreeNode类 + * @param sum int整型 + * @return bool布尔型 + */ + public boolean hasPathSum (TreeNode root, int sum) { + // write code here + if(root == null){ + return false; + } + + if(root.left == null && root.right == null){ + return sum - root.val == 0; + } + + return hasPathSum(root.left,sum - root.val) || + hasPathSum(root.right,sum - root.val); + } +} +``` + +### 序列化二叉树 + +```java +import java.util.*; +/* +public class TreeNode { + int val = 0; + TreeNode left = null; + TreeNode right = null; + + public TreeNode(int val) { + this.val = val; + + } + +} +*/ +public class Solution { + private int index = -1; + + public String Serialize(TreeNode root) { + StringBuilder builder = new StringBuilder(); + intervalSerialize(builder, root); + return builder.toString(); + } + + private void intervalSerialize(StringBuilder builder, TreeNode root) { + if (root == null) { + builder.append("#,"); + return; + } + builder.append(root.val).append(","); + intervalSerialize(builder, root.left); + intervalSerialize(builder, root.right); + } + + public TreeNode Deserialize(String str) { + if (str == null || str.isEmpty()) { + return null; + } + String[] words = str.split(","); + return internalDeserialize(words); + } + + private TreeNode internalDeserialize(String[] words) { + index++; + if (index == words.length) { + return null; + } + if ("#".equals(words[index])) { + return null; + } + TreeNode root = new TreeNode(Integer.parseInt(words[index])); + root.left = internalDeserialize(words); + root.right = internalDeserialize(words); + return root; + } +} +``` + +### 二叉搜索树的第k个结点 + +```java +public class Solution { + int index = 0; + TreeNode target = null; + TreeNode KthNode(TreeNode pRoot, int k){ + getKthNode(pRoot,k); + return target; + } + + public void getKthNode(TreeNode pRoot, int k){ + if(pRoot == null){ + return; + } + getKthNode(pRoot.left,k); + index++; + if(index == k){ + target = pRoot; + return; + } + getKthNode(pRoot.right,k); + } +} +``` + +### 把二叉树打印成多行 + +```java +import java.util.*; + +public class Solution { + ArrayList > Print(TreeNode pRoot) { + if(pRoot == null){ + return new ArrayList>(); + } + ArrayList> list = new ArrayList<>(); + + Queue queue = new LinkedList<>(); + queue.add(pRoot); + while(!queue.isEmpty()){ + ArrayList temp = new ArrayList<>(); + for(int i = queue.size(); i > 0; i--){ + TreeNode node = queue.poll(); + temp.add(node.val); + if(node.left != null){ + queue.add(node.left); + } + if(node.right != null){ + queue.add(node.right); + } + } + list.add(temp); + } + + return list; + } + +} +``` + +### 二叉树的镜像 + +```java +public class Solution { + public void Mirror(TreeNode root) { + if(root == null){ + return; + } + if(root.left == null && root.right == null){ + return; + } + Stack stack = new Stack<>(); + stack.push(root); + while(!stack.isEmpty()){ + TreeNode node = stack.pop(); + + if(node.left != null || node.right != null){ + TreeNode temp = node.left; + node.left = node.right; + node.right = temp; + } + + if(node.left != null){ + stack.push(node.left); + } + + if(node.right != null){ + stack.push(node.right); + } + } + } +} + +/* +public class Solution { + public void Mirror(TreeNode root) { + if(root == null){ + return; + } + if(root.left == null && root.right == null){ + return; + } + + TreeNode temp = root.left; + root.left = root.right; + root.right = temp; + + if(root.left != null){ + Mirror(root.left); + } + + if(root.right != null){ + Mirror(root.right); + } + } +} +*/ + +``` + +### 判断t1树中是否有与t2树拓扑结构完全相同的子树 + +```java +/** + * + * @param root1 TreeNode类 + * @param root2 TreeNode类 + * @return bool布尔型 + */ +public boolean isContains (TreeNode root1, TreeNode root2) { + // write code here + if(root1 == null || root2 == null){ + return false; + } + return recur(root1, root2) || isContains(root1.left,root2) + || isContains(root1.right,root2); +} + +public boolean recur(TreeNode root1, TreeNode root2){ + if(root2 == null){ + return true; + } + if(root1 == null || root1.val != root2.val){ + return false; + } + return recur(root1.left,root2.left) && + recur(root1.right,root2.right); +} +``` + +### 合并二叉树 + +```java +import java.util.*; + +public class Solution { + /** + * + * @param t1 TreeNode类 + * @param t2 TreeNode类 + * @return TreeNode类 + */ + public TreeNode mergeTrees (TreeNode t1, TreeNode t2) { + if(t1==null) return t2; + if(t2==null) return t1; + TreeNode temp=new TreeNode(t1.val+t2.val); + temp.left=mergeTrees(t1.left,t2.left); + temp.right=mergeTrees(t1.right,t2.right); + return temp; + } +} +``` + +### 字典树的实现 + +![](http://image.ouyangsihai.cn/Fm_RgyVyr-Q8xgXGWHyrkrfRBukX) + +```java +import java.util.*; + +public class Solution { + /** + * + * @param operators string字符串二维数组 the ops + * @return string字符串一维数组 + */ + public String[] trieU (String[][] operators) { + //计算结果集长度,并进行初始化 + int len=0; + for(String[] opera:operators){ + if(opera[0].equals("3")||opera[0].equals("4")){ + len++; + } + } + String[] res=new String[len]; + Trie trie=new Trie(); + int id=0; + + for(String[] opera:operators){ + if(opera[0].equals("1")){ + //添加单词 + trie.insert(opera[1]); + } + else if(opera[0].equals("2")){ + //删除单词 + trie.delete(opera[1]); + } + else if(opera[0].equals("3")){ + //查询单词是否存在 + res[id++]=trie.search(opera[1])?"YES":"NO"; + } + else if(opera[0].equals("4")){ + //查找以word为前缀的单词数量 + String preNumber=String.valueOf(trie.prefixNumber(opera[1])); + res[id++]=preNumber; + } + } + return res; + } + + class Trie{ + //构建字典树节点 + class TrieNode{ + //child数组记录所有子节点 + TrieNode[] child; + //pre_number表示插入单词时,当前节点被访问次数 + int pre_number; + //end表示当前节点是否是某个单词的末尾 + boolean end; + TrieNode(){ + child=new TrieNode[26]; + pre_number=0; + end=false; + } + } + + Trie(){} + + //初始化根节点 + TrieNode root=new TrieNode(); + + //添加单词 + void insert(String word){ + TrieNode node=root; + char[] arr=word.toCharArray(); + for(char c:arr){ + //如果子节点不存在,则新建 + if(node.child[c-'a']==null){ + node.child[c-'a']=new TrieNode(); + } + //往子节点方向移动 + node=node.child[c-'a']; + node.pre_number++; + } + node.end=true; + } + + void delete(String word){ + TrieNode node=root; + char[] arr=word.toCharArray(); + for(char c:arr){ + //往子节点方向移动,将访问次数减一 + node=node.child[c-'a']; + node.pre_number--; + } + //如果访问次数为0,说明不存在该单词为前缀的单词,以及该单词 + if(node.pre_number==0){ + node.end=false; + } + } + + boolean search(String word){ + TrieNode node=root; + char[] arr=word.toCharArray(); + for(char c:arr){ + //如果子节点不存在,说明不存在该单词 + if(node.child[c-'a']==null){ + return false; + } + node=node.child[c-'a']; + } + + //如果前面的节点都存在,并且该节点末尾标识为true,则存在该单词 + return node.end; + } + + int prefixNumber(String pre){ + TrieNode node=root; + char[] arr=pre.toCharArray(); + for(char c:arr){ + //如果子节点不存在,说明不存在该前缀 + if(node.child[c-'a']==null){ + return 0; + } + node=node.child[c-'a']; + } + + //返回以该单词为前缀的数量 + return node.pre_number; + } + } +} +``` + +### 找到二叉搜索树中的两个错误节点 + +![](http://image.ouyangsihai.cn/FgMj1e8uJv5aZqSy__ZFJ2aN4cCZ) + +```java +import java.util.*; +public class Solution { + /** + * + * @param root TreeNode类 the root + * @return int整型一维数组 + */ + + //存储结果集的二维数组 + int[] result = new int[2]; + int index = 1; + TreeNode preNode; + public int[] findError (TreeNode root) { + // 特判 + if(root == null) { + return result; + } + // 递归左子树,寻找该树符合条件的节点 + findError(root.left); + if(preNode == null) { + preNode = root; + } + // 判断是否是出错的节点 + if(index == 1 && root.val < preNode.val) { + result[index] = preNode.val; + index--; + } + if(index == 0 && root.val < preNode.val) { + result[index] = root.val; + } + preNode = root; + // 递归右子树,寻找该树符合条件的节点 + findError(root.right); + return result; + } +} +``` + +## 堆 + +### 最小的K个数 + +```java +import java.util.*; +public class Solution { + public ArrayList GetLeastNumbers_Solution(int [] input, int k) { + ArrayList res = new ArrayList(); + //排除特殊情况 + if(k == 0 || input.length == 0) + return res; + //大根堆 + PriorityQueue q = + new PriorityQueue<>((o1, o2)->o2.compareTo(o1)); + //构建一个k个大小的堆 + for(int i = 0; i < k; i++) + q.offer(input[i]); + for(int i = k; i < input.length; i++){ + //较小元素入堆 + if(q.peek() > input[i]){ + q.poll(); + q.offer(input[i]); + } + } + //堆中元素取出入数组 + for(int i = 0; i < k; i++) + res.add(q.poll()); + return res; + } +} + +// 自己实现堆排序 +public class Solution { + public ArrayList + GetLeastNumbers_Solution(int [] input, int k) { + ArrayList list = new ArrayList<>(); + if (input == null || input.length == 0 + || k > input.length || k == 0) + return list; + int[] arr = new int[k + 1];//数组下标0的位置作为哨兵,不存储数据 + //初始化数组 + for (int i = 1; i < k + 1; i++) + arr[i] = input[i - 1]; + buildMaxHeap(arr, k + 1);//构造大根堆 + for (int i = k; i < input.length; i++) { + if (input[i] < arr[1]) { + arr[1] = input[i]; + adjustDown(arr, 1, k + 1);//将改变了根节点的二叉树继续调整为大根堆 + } + } + for (int i = 1; i < arr.length; i++) { + list.add(arr[i]); + } + return list; + } + /** + * @Author: ZwZ + * @Description: 构造大根堆 + * @Param: [arr, length] length:数组长度 作为是否跳出循环的条件 + * @return: void + * @Date: 2020/1/30-22:06 + */ + public void buildMaxHeap(int[] arr, int length) { + if (arr == null || arr.length == 0 || arr.length == 1) + return; + for (int i = (length - 1) / 2; i > 0; i--) { + adjustDown(arr, i, arr.length); + } + } + /** + * @Author: ZwZ + * @Description: 堆排序中对一个子二叉树进行堆排序 + * @Param: [arr, k, length] + * @return: + * @Date: 2020/1/30-21:55 + */ + public void adjustDown(int[] arr, int k, int length) { + arr[0] = arr[k];//哨兵 + for (int i = 2 * k; i <= length; i *= 2) { + if (i < length - 1 && arr[i] < arr[i + 1]) + i++;//取k较大的子结点的下标 + if (i > length - 1 || arr[0] >= arr[i]) + break; + else { + arr[k] = arr[i]; + k = i; //向下筛选 + } + } + arr[k] = arr[0]; + } +} +``` + +### 字符串出现次数的TopK问题 + +```java +public class Solution { + public String[][] topKstrings (String[] strings, int k) { + String[][] res = new String[k][2]; + //记录字符出现次数 + HashMap map = new HashMap<>(); + for(int i = 0; i < strings.length; i++){ + if(map.containsKey(strings[i])){ + map.put(strings[i], map.get(strings[i]) + 1); + }else{ + map.put(strings[i], 1); + } + } + //建立小根堆,自定义比较器(次数值value相同, + // 比较key的字典序,不相同直接比较次数值value) + PriorityQueue> pq = + new PriorityQueue<>((o1,o2) -> o1.getValue().equals(o2.getValue()) + ? o2.getKey().compareTo(o1.getKey()) : o1.getValue()-o2.getValue()); + int size = 0; + //维护size为k的小根堆 + for(Map.Entry m : map.entrySet()){ + if(size < k){ + pq.offer(m); + size++; + } + //大于堆顶元素插入 + else if((m.getValue().equals(pq.peek().getValue()) + ? pq.peek().getKey().compareTo(m.getKey()) + : m.getValue() - pq.peek().getValue()) > 0){ + pq.poll(); + pq.offer(m); + } + } + //取出堆中元素,从后向前放置 + for(int i = k - 1; i >= 0; i--){ + Map.Entry entry =(Map.Entry)pq.poll(); + res[i][0] = entry.getKey(); + res[i][1] = String.valueOf(entry.getValue()); + } + return res; + } +} +``` + +### 寻找第K大 + +```java +public int findKth(int[] a, int n, int K){ + // 暂存K个较大的值,优先队列默认是自然排序(升序), + // 队头元素(根)是堆内的最小元素,也就是小根堆 + PriorityQueue queue = new PriorityQueue<>(K); + // 遍历每一个元素,调整小根堆 + for (int num : a) { + // 对于小根堆来说,只要没满就可以加入(不需要比较); + // 如果满了,才判断是否需要替换第一个元素 + if (queue.size() < K) { + queue.add(num); + } else { + // 在小根堆内,存储着K个较大的元素,根是这K个中最小的, + // 如果出现比根还要大的元素,说明可以替换根 + if (num > queue.peek()) { + queue.poll(); // 高个中挑矮个,矮个淘汰 + queue.add(num); + } + } + } + return queue.isEmpty() ? 0 : queue.peek(); +} +``` + +```java +import java.util.*; + +public class Finder { + public int findKth(int[] a, int n, int K) { + // write code here + return find(a, 0, n-1, K); + } + + public int find(int[] a, int low, int high, int K){ + int pivot = partition(a, low, high); + + if(pivot + 1 < K){ + return find(a, pivot + 1, high, K); + } else if(pivot + 1 > K){ + return find(a, low, pivot - 1, K); + } else { + return a[pivot]; + } + } + + int partition(int arr[], int startIndex, int endIndex){ + int small = startIndex - 1; + for (int i = startIndex; i < endIndex; ++i) { + if(arr[i] > arr[endIndex]) { + swap(arr,++small, i); + } + } + swap(arr,++small,endIndex); + return small; + } + + public void swap(int[] arr, int i, int j){ + int temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + } +} + +``` + +## 双指针 + +### 最长无重复子数组的长度 + +```java +// 方法1 +public int maxLength (int[] arr) { + int left = 0, right = 0; + Set set = new HashSet<>(); + int res = 1; + while(right < arr.length){ + if(!set.contains(arr[right])){ + set.add(arr[right]); + right++; + }else{ + set.remove(arr[left]); + left++; + } + res = Math.max(res, set.size()); + } + return res; +} + +// 方法2 +public int maxLength(int[] arr) { + if (arr.length == 0) + return 0; + HashMap map = new HashMap<>(); + int max = 0; + for (int i = 0, j = 0; i < arr.length; ++i) { + if (map.containsKey(arr[i])) { + j = Math.max(j, map.get(arr[i]) + 1); + } + map.put(arr[i], i); + max = Math.max(max, i - j + 1); + } + return max; +} +``` + +### 滑动窗口的最大值 + +```java +import java.util.*; +/** +用一个双端队列,队列第一个位置保存当前窗口的最大值,当窗口滑动一次 +1.判断当前最大值是否过期 +2.新增加的值从队尾开始比较,把所有比他小的值丢掉 +*/ +public class Solution { + public ArrayList maxInWindows(int [] num, int size) + { + ArrayList res = new ArrayList<>(); + if(size == 0) return res; + int begin; + ArrayDeque q = new ArrayDeque<>(); + for(int i = 0; i < num.length; i++){ + begin = i - size + 1; + if(q.isEmpty()) + q.add(i); + else if(begin > q.peekFirst()) + q.pollFirst(); + + while((!q.isEmpty()) && num[q.peekLast()] <= num[i]) + q.pollLast(); + q.add(i); + if(begin >= 0) + res.add(num[q.peekFirst()]); + } + return res; + } +} +``` + +### 合并区间(区间重叠) + +首先将各个区间进行排序,排序规则为首先根据start进行排序, +如果start相等则根据end从小到大排序new一个新的List result存放结果, +遍历给定的intervals,比较当前interval的start是否大于result中最后一个元素的end, +若大于,说明从开了一个区间,若区间有重叠,则更新result中最后一个元素的end。 + +```java +import java.util.*; +/** + * Definition for an interval. + * public class Interval { + * int start; + * int end; + * Interval() { start = 0; end = 0; } + * Interval(int s, int e) { start = s; end = e; } + * } + */ +public class Solution { + public ArrayList merge(ArrayList intervals) { + // 首先根据start排序,如果start相等,根据end排序 + Collections.sort(intervals, + (o1, o2) -> (o1.start != o2.start + ? o1.start - o2.start : o1.end - o2.end)); + ArrayList result = new ArrayList<>(); + if(intervals.size() == 0) { + return result; + } + // 放入第一个区间 + result.add(intervals.get(0)); + int count = 0; + // 遍历后续区间,查看是否与末尾有重叠 + for(int i = 1; i < intervals.size(); i++) { + Interval o1 = intervals.get(i); + Interval origin = result.get(result.size() - 1); + // 如果当前Interval的start比List里面最后一个元素的end大,说明从开一个区间 + if(o1.start > origin.end) { + result.add(o1); + } else { // 区间有重叠,更新结尾 + if(o1.end > origin.end) { + result.get(result.size() - 1).end = o1.end; + } + } + } + return result; + } +} +``` + +### 反转字符串 + +```java +import java.util.*; +public class Solution { + /** + * 反转字符串 + * @param str string字符串 + * @return string字符串 + */ + public String solve (String str) { + // write code here + if(str == null){ + return null; + } + char[] c = new char[str.length()]; + int left = 0, right = str.length() - 1; + + while(left <= right){ + c[left] = str.charAt(right); + c[right] = str.charAt(left); + left++; + right--; + } + return new String(c); + } +} +``` + +### 数组中相加和为0的三元组 + +```java +import java.util.*; + +public class Solution { + public ArrayList> threeSum(int[] num) { + ArrayList> list = new ArrayList<>(); + Arrays.sort(num); + int left,right,sum; + for(int i = 0; i < num.length - 2; i++){ + if(i > 0 && num[i] == num[i-1]) continue; + left = i + 1; + right = num.length - 1; + while(left < right){ + sum = num[i] + num[left] + num[right]; + if(sum == 0){ + ArrayList temp = new ArrayList<>(); + temp.add(num[i]); + temp.add(num[left]); + temp.add(num[right]); + list.add(temp); + right--; + left++; + while(left < right && num[left] == num[left-1]){ + left++; + } + while(left < right && num[right] == num[right+1]){ + right--; + } + } else if(sum < 0){ + left++; + } else { + right--; + } + } + } + return list; + } +} +``` + +### 接雨水问题 + +```java + public long maxWater(int[] arr) { + if (arr.length <= 2) + return 0; + //找到最高的柱子的下标 + int max = Integer.MIN_VALUE; + int maxIndex = -1; + for (int i = 0; i < arr.length; i++) { + if (arr[i] > max) { + max = arr[i]; + maxIndex = i; + } + } + + //统计最高柱子左边能接的雨水数量 + int left = arr[0]; + int right = 0; + long water = 0; + for (int i = 1; i < maxIndex; i++) { + right = arr[i]; + if (right > left) { + left = right; + } else { + water += left - right; + } + } + + //统计最高柱子右边能接的雨水数量 + right = arr[arr.length - 1]; + for (int i = arr.length - 2; i > maxIndex; i--) { + left = arr[i]; + if (arr[i] > right) { + right = left; + } else { + water += right - left; + } + } + + //返回盛水量 + return water; + } +``` + +```java +public long maxWater (int[] arr) { + int l = 0, r = arr.length-1; + int maxL = 0, maxR = 0; + long res = 0; + while(l < r){ + maxL = Math.max(arr[l],maxL); // 求出左边界的最大值 + maxR = Math.max(arr[r],maxR); // 求出右边界的最大值 + if(maxR > maxL){ // 如果 + res += maxL - arr[l++]; + }else{ + res += maxR - arr[r--]; + } + } + return res; +} +``` + +### 最小覆盖子串(T包含S的最小子串) + +```java +import java.util.*; + +public class Solution { + /** + * + * @param S string字符串 + * @param T string字符串 + * @return string字符串 + */ + public String minWindow (String s, String t) { + HashMap window = new HashMap<>(); + HashMap need = new HashMap<>(); + for (int i = 0; i < t.length(); i++) { + Integer count = need.get(t.charAt(i)); + count = count == null ? 1 : ++count; + need.put(t.charAt(i),count); + } + int left =0 , right = 0; + int vaild = 0; + int len = Integer.MAX_VALUE,start = 0; + //最小覆盖字串起始索引 + while (right < s.length()){ + char c = s.charAt(right); + right++; + if (need.containsKey(c)){ + Integer count = window.get(c); + count = count == null ? 1 : ++count; + window.put(c,count); + if (window.get(c).equals(need.get(c))){ + vaild++; + } + } + + //都包含了,right找到了,可以考虑收缩 + while (vaild == need.size()){ + if (right -left < len){ + start = left; + len = right - left; + } + //d是将要移出窗口的字符 + char d = s.charAt(left); + //左移窗口 + left++; + //数据更新 + if (need.containsKey(d)){ + if (window.get(d).equals(need.get(d))){ + vaild--; + } + window.put(d,window.get(d)-1); + } + } + } + return len == Integer.MAX_VALUE + ? "" : s.substring(start,start+len); + } +} +``` + +### 两数之和 + +```java +import java.util.HashMap; +public class Solution { + public int[] twoSum(int[] nums, int target) { + HashMap map = new HashMap<>(); + for (int i = 0; i < nums.length; i++) { + if (map.containsKey(nums[i])){ + return new int[]{map.get(nums[i])+1,i+1}; + } + map.put(target - nums[i],i); + } + return null; + } +} +``` + +### 最长重复子串(连续两个相同的字符串) + +```java +import java.util.*; +public class Solution { + // 使用滑动窗口 滑动窗口中的字符串是重复字符串的一半 + public int solve (String a) { + //枚举长度为i的窗口(按窗口大小倒序枚举),找到第一个满足条件的窗口(窗口为重复子串) + char[] cs = a.toCharArray(); + int len = cs.length; + int cnt = 0; + // 滑动窗口的大小不可能大于数组长度的一半 + for (int i=len/2; i>0; i--) { //枚举一半窗口大小 + //窗口右侧节点范围len-i + for (int j=0; j max){ + max = dp[i]; + } + } + return max; +} + +// sum < 0置为0 +public int FindGreatestSumOfSubArray(int[] array) { + if(array.length == 0){ + return 0; + } + + int max = Integer.MIN_VALUE; + int cur = 0; + for(int i = 0; i < array.length; i++){ + cur += array[i]; + max = Math.max(max,cur); + cur = cur < 0 ? 0 : cur; + } + return max; +} +``` + +### 最长公共子串(返回具体字符串/长度) + +```java +// 返回字符串 +public class Solution { + public String LCS (String str1, String str2) { + // write code here + int m = str1.length(); + int n = str2.length(); + int[][] dp = new int[m][n]; + int maxLength = 0; + int lastIndex = 0; + //用来记录str1中最长公共串的最后一个字符的下标 + for(int i = 0; i < m; i++){ + for(int j = 0; j < n; j++){ + if(str1.charAt(i) == str2.charAt(j)){ + //判断str1中第i个字符是否和str2中第j个字符相等 + if(i == 0 || j == 0){ + dp[i][j] = 1; + }else{ + dp[i][j] = dp[i - 1][j - 1] + 1; + } + + if(dp[i][j] > maxLength){ + //判断是否需要更新最长公共子串 + maxLength = dp[i][j]; + lastIndex = i; + } + } + } + } + + //通过str1来截取长度为maxLength, 最后字符坐标为lastIndex的子串 + return str1.substring(lastIndex - maxLength + 1, + lastIndex + 1); + } +} + +// 返回长度 +public class Solution { + public int LCS (String s1, String s2) { + // write code here + int mLength = s1.length(); + int nLength = s2.length(); + int[][] dp = new int[mLength + 1][nLength + 1]; + char[] c1 = s1.toCharArray(); + char[] c2 = s2.toCharArray(); + for (int i = 0; i <= mLength; i++) { + dp[i][0] = 0; + } + for (int j = 0; j <= nLength; j++) { + dp[0][j] = 0; + } + for (int i = 1; i <= mLength; i++) { + for (int j = 1; j <= nLength; j++) { + if (c1[i - 1] != c2[j - 1]) { + dp[i][j] = + Math.max(dp[i- 1][j], dp[i][j - 1]); + } else { + dp[i][j] = dp[i - 1][j - 1] + 1; + } + } + } + return dp[mLength][nLength]; + } +} +``` + +### 斐波那契数列 + +```java +public class Solution { + public int Fibonacci(int n) { + if(n == 0){ + return 0; + } + if(n == 1 || n == 2){ + return 1; + } + return Fibonacci(n-1) + Fibonacci(n-2); + } +} +``` + +### 最长回文子串的长度 + +```java +public String longestPalindrome1(String s) { + if (s == null || s.length() == 0) { + return ""; + } + int strLen = s.length(); + int left = 0; + int right = 0; + int len = 1; + int maxStart = 0; + int maxLen = 0; + + for (int i = 0; i < strLen; i++) { + left = i - 1; + right = i + 1; + while (left >= 0 && + s.charAt(left) == s.charAt(i)) { + len++; + left--; + } + while (right < strLen && + s.charAt(right) == s.charAt(i)) { + len++; + right++; + } + while (left >= 0 && right < strLen + && s.charAt(right) == s.charAt(left)) { + len = len + 2; + left--; + right++; + } + if (len > maxLen) { + maxLen = len; + maxStart = left; + } + len = 1; + } + return s.substring(maxStart + 1, + maxStart + maxLen + 1); + +} +``` + +### 最长递增子序列 + +```java +// 求长度 +class Solution { + public int lengthOfLIS(int[] nums) { + if(nums.length == 0) return 0; + int[] dp = new int[nums.length]; + int res = 0; + Arrays.fill(dp, 1); + for(int i = 0; i < nums.length; i++) { + for(int j = 0; j < i; j++) { + if(nums[j] < nums[i]) + dp[i] = Math.max(dp[i], dp[j] + 1); + } + res = Math.max(res, dp[i]); + } + return res; + } +} +``` + +```java +public int[] LIS (int[] arr) { + // write code here + if(arr == null || arr.length <= 0){ + return null; + } + + int len = arr.length; + int[] count = new int[len]; // 存长度 + int[] end = new int[len]; // 存最长递增子序列 + + //init + int index = 0; // end 数组下标 + end[index] = arr[0]; + count[0] = 1; + + for(int i = 0; i < len; i++){ + if(end[index] < arr[i]){ + end[++index] = arr[i]; + count[i] = index; + } + else{ + int left = 0, right = index; + while(left <= right){ + int mid = (left + right) >> 1; + if(end[mid] >= arr[i]){ + right = mid - 1; + } + else{ + left = mid + 1; + } + } + end[left] = arr[i]; + count[i] = left; + } + } + + //因为返回的数组要求是字典序,所以从后向前遍历 + int[] res = new int[index + 1]; + for(int i = len - 1; i >= 0; i--){ + if(count[i] == index){ + res[index--] = arr[i]; + } + } + return res; +} +``` + +### 买卖股票的最佳时机 + +base case: +dp[-1][k][0] = dp[i][0][0] = 0 +dp[-1][k][1] = dp[i][0][1] = -infinity + +状态转移⽅程: +dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]) +dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]) + +- k == 1 + +```java +public class Solution { + /** + * + * @param prices int整型一维数组 + * @return int整型 + */ + public int maxProfit (int[] prices) { + if(prices.length == 0) return 0; + // write code here + int n = prices.length; + int[][] dp = new int[n][2]; + for(int i = 0; i < n; i++){ + if(i - 1 == -1){ + dp[i][0] = 0; + dp[i][1] = -prices[i]; + continue; + } + dp[i][0] = Math.max(dp[i-1][0], + dp[i-1][1] + prices[i]); + dp[i][1] = Math.max(dp[i-1][1],-prices[i]); + } + + return dp[n-1][0]; + } +} + +// 空间复杂度优化版本 +int maxProfit(int[] prices) { + int n = prices.length; + // base case: dp[-1][0] = 0, dp[-1][1] = -infinity + int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE; + for (int i = 0; i < n; i++) { + // dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i]) + dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]); + // dp[i][1] = max(dp[i-1][1], -prices[i]) + dp_i_1 = Math.max(dp_i_1, -prices[i]); + } + return dp_i_0; +} +``` + +- k 为正无穷 + +```java +// 原始版本 +int maxProfit_k_inf(int[] prices) { + int n = prices.length; + int[][] dp = new int[n][2]; + for (int i = 0; i < n; i++) { + if (i - 1 == -1) { + // base case + dp[i][0] = 0; + dp[i][1] = -prices[i]; + continue; + } + dp[i][0] = Math.max(dp[i-1][0], + dp[i-1][1] + prices[i]); + dp[i][1] = Math.max(dp[i-1][1], + dp[i-1][0] - prices[i]); + } + return dp[n - 1][0]; +} + +// 空间复杂度优化版本 +int maxProfit_k_inf(int[] prices) { + int n = prices.length; + int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE; + for (int i = 0; i < n; i++) { + int temp = dp_i_0; + dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]); + dp_i_1 = Math.max(dp_i_1, temp - prices[i]); + } + return dp_i_0; +} +``` + +- k == 2 + +```java +// 原始版本 +int maxProfit(int[] prices) { + int max_k = 2, n = prices.length; + int[][][] dp = new int[n][max_k + 1][2]; + for (int i = 0; i < n; i++) { + for (int k = max_k; k >= 1; k--) { + if (i - 1 == -1) { + // 处理 base case + dp[i][k][0] = 0; + dp[i][k][1] = -prices[i]; + continue; + } + dp[i][k][0] = Math.max(dp[i-1][k][0], + dp[i-1][k][1] + prices[i]); + dp[i][k][1] = Math.max(dp[i-1][k][1], + dp[i-1][k-1][0] - prices[i]); + } + } + // 穷举了 n × max_k × 2 个状态,正确。 + return dp[n - 1][max_k][0]; +} + +// 空间复杂度优化版本 +int maxProfit_k_2(int[] prices) { + // base case + int dp_i10 = 0, dp_i11 = Integer.MIN_VALUE; + int dp_i20 = 0, dp_i21 = Integer.MIN_VALUE; + for (int price : prices) { + dp_i20 = Math.max(dp_i20, dp_i21 + price); + dp_i21 = Math.max(dp_i21, dp_i10 - price); + dp_i10 = Math.max(dp_i10, dp_i11 + price); + dp_i11 = Math.max(dp_i11, -price); + } + return dp_i20; +} +``` + +- k 为正无穷,但含有交易冷冻期 + +```java +// 原始版本 +int maxProfit_with_cool(int[] prices) { + int n = prices.length; + int[][] dp = new int[n][2]; + for (int i = 0; i < n; i++) { + if (i - 1 == -1) { + // base case 1 + dp[i][0] = 0; + dp[i][1] = -prices[i]; + continue; + } + if (i - 2 == -1) { + // base case 2 + dp[i][0] = Math.max(dp[i-1][0], + dp[i-1][1] + prices[i]); + // i - 2 小于 0 时根据状态转移方程推出对应 base case + dp[i][1] = Math.max(dp[i-1][1], -prices[i]); + // dp[i][1] + // = max(dp[i-1][1], dp[-1][0] - prices[i]) + // = max(dp[i-1][1], 0 - prices[i]) + // = max(dp[i-1][1], -prices[i]) + continue; + } + dp[i][0] = Math.max(dp[i-1][0], + dp[i-1][1] + prices[i]); + dp[i][1] = Math.max(dp[i-1][1], + dp[i-2][0] - prices[i]); + } + return dp[n - 1][0]; +} + +// 空间复杂度优化版本 +int maxProfit_with_cool(int[] prices) { + int n = prices.length; + int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE; + int dp_pre_0 = 0; // 代表 dp[i-2][0] + for (int i = 0; i < n; i++) { + int temp = dp_i_0; + dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]); + dp_i_1 = Math.max(dp_i_1, dp_pre_0 - prices[i]); + dp_pre_0 = temp; + } + return dp_i_0; +} +``` + +- k 为正无穷且考虑交易手续费 + +```java +// 原始版本 +int maxProfit_with_fee(int[] prices, int fee) { + int n = prices.length; + int[][] dp = new int[n][2]; + for (int i = 0; i < n; i++) { + if (i - 1 == -1) { + // base case + dp[i][0] = 0; + dp[i][1] = -prices[i] - fee; + // dp[i][1] + // = max(dp[i - 1][1], dp[i - 1][0] - prices[i] - fee) + // = max(dp[-1][1], dp[-1][0] - prices[i] - fee) + // = max(-inf, 0 - prices[i] - fee) + // = -prices[i] - fee + continue; + } + dp[i][0] = Math.max(dp[i - 1][0], + dp[i - 1][1] + prices[i]); + dp[i][1] = Math.max(dp[i - 1][1], + dp[i - 1][0] - prices[i] - fee); + } + return dp[n - 1][0]; +} + +// 空间复杂度优化版本 +int maxProfit_with_fee(int[] prices, int fee) { + int n = prices.length; + int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE; + for (int i = 0; i < n; i++) { + int temp = dp_i_0; + dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]); + dp_i_1 = Math.max(dp_i_1, temp - prices[i] - fee); + } + return dp_i_0; +} +``` + +- 指定 k + +```java +int maxProfit_k_any(int max_k, int[] prices) { + int n = prices.length; + if (n <= 0) { + return 0; + } + if (max_k > n / 2) { + // 复用之前交易次数 k 没有限制的情况 + return maxProfit_k_inf(prices); + } + + // base case: + // dp[-1][...][0] = dp[...][0][0] = 0 + // dp[-1][...][1] = dp[...][0][1] = -infinity + int[][][] dp = new int[n][max_k + 1][2]; + // k = 0 时的 base case + for (int i = 0; i < n; i++) { + dp[i][0][1] = Integer.MIN_VALUE; + dp[i][0][0] = 0; + } + + for (int i = 0; i < n; i++) + for (int k = max_k; k >= 1; k--) { + if (i - 1 == -1) { + // 处理 i = -1 时的 base case + dp[i][k][0] = 0; + dp[i][k][1] = -prices[i]; + continue; + } + dp[i][k][0] = Math.max(dp[i-1][k][0], + dp[i-1][k][1] + prices[i]); + dp[i][k][1] = Math.max(dp[i-1][k][1], + dp[i-1][k-1][0] - prices[i]); + } + return dp[n - 1][max_k][0]; +} +``` + +### 矩阵的最小路径和 + +```java +import java.util.*; +public class Solution { + /** + * + * @param matrix int整型二维数组 the matrix + * @return int整型 + */ + public int minPathSum (int[][] matrix) { + // write code here + int m = matrix.length, n = matrix[0].length; + if (m == 0 || n == 0) return 0; + + for (int i = 1; i < m; i++) + matrix[i][0] += matrix[i-1][0]; + for (int i = 1; i < n; i++) + matrix[0][i] += matrix[0][i-1]; + + for (int i = 1; i < m; i++) { + for (int j = 1; j < n; j++) { + matrix[i][j] += + Math.min(matrix[i-1][j], matrix[i][j-1]); + } + } + return matrix[m-1][n-1]; + } +} +``` + +### 编辑距离 + +```java +import java.util.*; + + +public class Solution { + /** + * min edit cost + * @param str1 string字符串 the string + * @param str2 string字符串 the string + * @param ic int整型 insert cost + * @param dc int整型 delete cost + * @param rc int整型 replace cost + * @return int整型 + */ + public int minEditCost (String str1, String str2, + int ic, int dc, int rc) { + // write code here + int len1=str1.length(); + int len2=str2.length(); + char[] char1=str1.toCharArray(); + char[] char2=str2.toCharArray(); + int[][] dp=new int[len1+1][len2+1]; + for(int i=1;i<=len1;i++){ + dp[i][0]=dp[i-1][0]+dc; + } + for(int i=1;i<=len2;i++){ + dp[0][i]=dp[0][i-1]+ic; + } + for(int i=0;i stack = new Stack<>(); + int last = -1; + int maxLen = 0; + for(int i = 0; i < s.length(); i++){ + if(s.charAt(i) == '('){ + stack.push(i); + } else { + if(stack.isEmpty()){ + last = i; + } else { + stack.pop(); + if(stack.isEmpty()){ + maxLen = + Math.max(maxLen, i - last); + } else { + maxLen = + Math.max(maxLen, i - stack.peek()); + } + } + } + } + + return maxLen; + } +} + +// 动态规划 +public int longestValidParentheses2(String s) { + if (s == null || s.length() == 0) + return 0; + int[] dp = new int[s.length()]; + int ans = 0; + for (int i = 1; i < s.length(); i++) { + // 如果是'('直接跳过,默认为0 + if (s.charAt(i) == ')') { + if (s.charAt(i - 1) == '(') + dp[i] = + (i >= 2 ? dp[i - 2] : 0) + 2; + // 说明s.charAt(i - 1)==')' + else if (i - dp[i - 1] > 0 && + s.charAt(i - dp[i - 1] - 1) == '(') { + dp[i] = + (i - dp[i - 1] > 1 ? + dp[i - dp[i - 1] - 2] : 0) + dp[i - 1] + 2; + // 因为加了一个左括号和一个右括号,所以是加2 + } + } + ans = Math.max(ans, dp[i]); + } + return ans; +} +``` + +### 高空扔鸡蛋 + +```java +// 如果棋子碎了,那么棋子的个数K应该减一, +// 搜索的楼层区间应该从[1..N]变为[1..i-1]共i-1层楼; +// 如果棋子没碎,那么棋子的个数K不变, +// 搜索的楼层区间应该从 [1..N]变为[i+1..N]共N-i层楼。 +import java.util.*; +public class Solution { + /** + * 返回最差情况下扔棋子的最小次数 + * @param k int整型 棋子数 + * @param n int整型 楼层数 + * @return int整型 + */ + int[][] memo; + public int solve (int n, int k) { + memo = new int[k + 1][n + 1]; + for(int[] m : memo) { + Arrays.fill(m, -1); + } + return dp(k, n); + } + + // 定义:有K个棋子面对N层楼,最少需要扔 dp(K, N) 次 + int dp(int k, int n) { + // 状态:棋子数k,需要测试的楼层n + if(k == 1) { + return n; + } + // 尝试到底层 + if(n == 0) { + return 0; + } + if(memo[k][n] != -1) { + return memo[k][n]; + } + int res = Integer.MAX_VALUE; + // 寻找第一层到第n层的最少扔的次数 + for(int i = 1; i <= n; i++) { + res = Math.min(res, + // 取决于最差情况(碎了,没碎) + Math.max(dp(k-1, i-1), dp(k, n-i)) + 1); + } + memo[k][n] = res; + return res; + } +} +``` + +### 兑换零钱 + +```java +public class Solution { + /** + * 最少货币数 + * @param arr int整型一维数组 the array + * @param aim int整型 the target + * @return int整型 + */ + public int minMoney (int[] arr, int aim) { + // write code here + //如何使用最少arr元素 构成 aim值 + //dp[i] 代表给定钱数为i的时候最少货币数 + // 就是凑成 i 元钱,需要dp[i] 张arr中面值纸币 + //没办法兑换 arr[i] dp[i] = dp[i] + //可以dp[i] = dp[i - arr[i]] + 1 + //dp[i] = min(dp[i], dp[i-a[j]]) + if(arr == null || arr.length == 0){ + return -1; + } + int[] dp = new int[aim+1]; + for(int i = 0;i<=aim;i++){ + dp[i] = aim+1; + } + + dp[0] = 0; + for(int i = 1;i<=aim;i++){ + for(int j = 0;j< arr.length;j++){ + if(arr[j] <= i){ + //给了一张 3 元钱,小于 需要找零的4 元钱, + // 那 就等于 1 + 需要找零剩下的钱dp[i -arr[j]] 4 - 3 + dp[i] = + Math.min(dp[i], dp[i-arr[j]] +1); + } + } + } + return (dp[aim] > aim) ?-1 : dp[aim]; + } +} +``` + +### 最大正方形 + +1.确定dp[][]数组的含义 + +此题的dp[i][j],代表以坐标为(i,j)的元素为右下角的正方形的边长。 + +2.状态转移方程 + +dp[i][j]的值取决于dp[i-1][j],dp[i-1][j-1],dp[i][j-1]的最小值 +即左方正方形的边长,左上方正方形的边长,上方正方形的边长三者的最小值。 + +```java +import java.util.*; + +// dp[i][j],代表以坐标为(i,j)的元素为右下角的正方形的边长 +public class Solution { + /** + * 最大正方形 + * @param matrix char字符型二维数组 + * @return int整型 + */ + public int solve (char[][] matrix) { + // write code here + if(matrix.length ==0 || matrix[0].length == 0) return 0; + int rows = matrix.length; + int cols = matrix[0].length; + int[][] dp = new int[rows][cols]; + int maxSquareLength = 0; + for(int i = 0; i < rows; i++){ + if(matrix[i][0] == '1') dp[i][0] = 1; + } + for(int i = 0; i < cols; i++){ + if(matrix[0][i] == '1') dp[0][i] = 1; + } + for(int i =1; i < rows; i++){ + for(int j = 1; j < cols; j++){ + if(matrix[i][j] == '1'){ + dp[i][j] = + Math.min(Math.min(dp[i-1][j-1], + dp[i-1][j]),dp[i][j-1])+1; + if(dp[i][j] > maxSquareLength) + maxSquareLength = dp[i][j]; + } + } + } + return maxSquareLength*maxSquareLength; + } +} +``` + +### 通配符匹配 + +![](http://image.ouyangsihai.cn/FtLlLxIBxM4nN1yyIi7VqfxsAXMB) + +```java +import java.util.*; +public class Solution { + public boolean isMatch(String s, String p) { + if (p == null || p.isEmpty())return s == null || s.isEmpty(); + int slen = s.length(), plen = p.length(); + boolean[][] dp = new boolean[slen + 1][plen + 1]; + //初始化dp数组,dp[1][0]~dp[s.length][0]默认值flase不需要显式初始化为false + dp[0][0] = true; + //dp[0][1]~dp[0][p.length]只有p的j字符以及前面所有字符都为'*'才为true + for (int j = 1; j <= plen; j++)dp[0][j] = p.charAt(j - 1) == '*' && + dp[0][j - 1]; + //填写dp数组剩余部分 + for (int i = 1; i <= slen; i++) { + for (int j = 1; j <= plen; j++) { + char si = s.charAt(i - 1), pj = p.charAt(j - 1); + if (si == pj || pj == '?') { + dp[i][j] = dp[i - 1][j - 1]; + } else if (pj == '*') { + dp[i][j] = dp[i - 1][j] || dp[i][j - 1]; + } + } + } + return dp[slen][plen]; + } +} +``` + +### 正则表达式匹配 + +![](http://image.ouyangsihai.cn/FsEkqX8hk4n_JgGhpWxZgFGtV5Qs) + +```java +import java.util.*; +public class Solution { + public boolean match (String str, String pattern) { + int n=str.length(); + int m=pattern.length(); + boolean[][] dp=new boolean[n+1][m+1]; + + //初始化 + dp[0][0]=true; + for(int i=1;i<=n;i++){ + dp[i][0]=false; + } + + //分模式串的后一个位置是否为*进行讨论,为*时, + // 将*与前一个位置合并起来进行考虑 + for(int i=0;i<=n;i++){ + for(int j=1;j<=m;j++){ + if(pattern.charAt(j-1)!='*'){ + //当前模式串字符和原串字符匹配 + if(i>0 + &&(str.charAt(i-1)==pattern.charAt(j-1) + ||(pattern.charAt(j-1)=='.'))){ + dp[i][j]=dp[i-1][j-1]; + } + } + else{ + if(j>=2){ + //不管是否匹配,都可以将当前字符绑定上*匹配原串字符0次 + dp[i][j]=dp[i][j-2]; + //当前模式串字符和原串字符匹配 + if(i>0 + &&(str.charAt(i-1)==pattern.charAt(j-2) + ||(pattern.charAt(j-2)=='.'))){ + dp[i][j]=dp[i-1][j]||dp[i][j-2]; + } + } + } + } + } + return dp[n][m]; + } +} +``` + +### 矩阵最长递增路径 + +```java +class Solution { + public int longestIncreasingPath(int[][] matrix) { + int m = matrix.length; + int n = matrix[0].length; + int ans = 0; + int[][] dp = new int[m][n];//储存当前节点最长路径 + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (dp[i][j] == 0) { + ans = Math.max(ans, dfs(matrix, dp, i, j)); + } + } + } + return ans; + } + public int dfs(int[][] matrix , int[][] dp, int i , int j){ + if (dp[i][j] == 0) { + int dir1 = 0, dir2 = 0, dir3 = 0, dir4 = 0; + if (i > 0 && matrix[i - 1][j] > matrix[i][j]) { + dir1 = dfs(matrix, dp, i - 1, j); + } + if (j > 0 && matrix[i][j - 1] > matrix[i][j]) { + dir2 = dfs(matrix, dp, i, j - 1); + } + if (i < matrix.length - 1 + && matrix[i + 1][j] > matrix[i][j]) { + dir3 = dfs(matrix, dp, i + 1, j); + } + if (j < matrix[0].length - 1 + && matrix[i][j + 1] > matrix[i][j]) { + dir4 = dfs(matrix, dp, i, j + 1); + } + //选出四个方向的最长子串,加1后赋值给当前节点 + dp[i][j] = 1 + Math.max(dir1, + Math.max(dir2, Math.max(dir3, dir4))); + } + return dp[i][j]; + } +} +``` + +### 最长上升子序列 + +- 返回长度 + +```java +import java.util.*; + + +public class Solution { + /** + * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可 + * + * 给定数组的最长严格上升子序列的长度。 + * @param arr int整型一维数组 给定的数组 + * @return int整型 + */ + public int LIS (int[] arr) { + // write code here + int len = arr.length; + if (arr == null && len == 0){ + return 0; + } + int maxLen = 0; + int[] dp = new int[len]; + Arrays.fill(dp, 1); + for (int i = 1; i < len; i++) { + for (int j = i-1; j >=0 ; j--) { + if (arr[i]> arr[j]){ + dp[i] = Math.max(dp[i],dp[j]+1); + } + } + maxLen = Math.max(maxLen,dp[i]); + } + return maxLen; + } +} +``` + +- 返回字符串 + +// 方法一:dp +状态定义:dp[i]表示以i位置元素结尾的最长上升子序列长度。 +状态初始化:以每个位置结尾的上升子序列长度至少为1。 +状态转移:遍历arr数组,假设当前位置为i, +比较当前位置i之前的每一个元素与当前位置元素的大小, +如果小于当前位置元素arr[i],说明可以接在arr前面。 +即 dp[i]=Math.max(dp[i],dp[j]+1)。 + +// 方法二:dp+数组 +与方法一相同的是,也需要建立一个dp数组找到每一个位置对应的最长上升子序列长度, +最后再通过逆序遍历arr数组的方式,找到每一个长度对应的那个元素,赋值给结果序列。 +不过确定dp数组的方式有所不同。 + +为了找到最长的上升子序列,我们可以维护一个单调递增的数组tail记录当前的最长上升子序列, +如果后面出现的数arr[i]比tail数组末尾元素大,则可以直接加在后面; +如果不是,则找到tail数组中第一个大于等于arr[i]的元素位置, +并将该位置的元素替换为arr[i],因为在长度相同的情况下,当前值越小,则后面出现更长子序列的概率越大。 + +```java +// 方法一:dp +public class Solution { + public int[] LIS (int[] arr) { + int n=arr.length; + //dp[i]表示以i位置元素结尾的最长上升子序列长度 + int[] dp=new int[n+1]; + //初始化为1 + Arrays.fill(dp,1); + //记录最长子序列的长度 + int len=0; + for(int i=0;i=0;i--){ + if(dp[i]==len){ + res[--len]=arr[i]; + } + return res; + } +} + +// 方法二:dp+二分 +public class Solution { + public int[] LIS (int[] arr) { + int n=arr.length; + //维护一个单调递增tail数组 + int[] tail=new int[n]; + //dp[i]表示以i位置结尾的最长上升子序列长度 + int[] dp=new int[n]; + //最长上升子序列长度 + int len=0; + for(int i=0;i=0;i--){ + if(dp[i]==len){ + res[--len]=arr[i]; + } + } + return res; + } + + //二分法找tail数组中第一个大于等于arr[i]的元素位置 + private int search(int[] nums,int len,int k){ + int low=0,high=len-1; + while(low=k){ + high=mid; + } + //否则排除mid以及mid往左的所有元素 + else{ + low=mid+1; + } + } + return low; + } +} +``` + +### 目标和(完全背包) + +```java +public class Solution { + public int findTargetSumWays (int[] nums, int target) { + //边界情况判断 + int n=nums.length; + if(n==0) return 0; + //记录累加和 + int sum=0; + //遍历nums数组 + for(int num:nums){ + sum+=num; + } + //计算背包容量 + int V=(sum+target)/2; + //如果为奇数,说明nums数组中找不打和为(sum+target)/2的若干数字 + if((sum+target)%2==1) return 0; + + //dp[j]表示有多少种不同的组合,其累加和为j + int[] dp=new int[V+1]; + //初始化 + dp[0]=1; + for(int i=0;i=nums[i];j--){ + dp[j]+=dp[j-nums[i]]; + } + } + return dp[V]; + } +} +``` + +### 打家劫舍 + +```java +public class Solution { + public int rob (int[] nums) { + // write code here + + // 一些特殊情况的处理 + if (1 == nums.length) { + return nums[0]; + } + if (2 == nums.length) { + return Math.max(nums[0], nums[1]); + } + + int l = nums.length; + int[] dp = new int[l]; + + dp[0] = nums[0]; + dp[1] = Math.max(nums[0], nums[1]); + for (int i = 2; i < l; i++) { + dp[i] = Math.max(dp[i - 2] + + nums[i], dp[i - 1]); + } + return dp[l - 1]; + } +} + +// 圆形情况 +public class Solution { + public int rob (int[] nums) { + int n=nums.length; + //在0到n-2范围内找 + int rob1=getRob(Arrays.copyOfRange(nums,0,n-1)); + //在1到n-1范围内找 + int rob2=getRob(Arrays.copyOfRange(nums,1,n)); + + return Math.max(rob1,rob2); + } + + private int getRob(int[] nums){ + int n=nums.length; + //边界情况处理 + if(n==0) return 0; + if(n==1) return nums[0]; + if(n==2) return Math.max(nums[0],nums[1]); + //定义dp数组 + int[] dp=new int[n]; + //初始化 + dp[0]=nums[0]; + dp[1]=Math.max(nums[0],nums[1]); + for(int i=2;i +如果map中没有当前这个元素,那么dp[i]=dp[i-1]+1 +如果map中存在当前的元素,一开始的想法是 dp[i]=i-map.get(array[i]), +但是这样想有点问题,如果当前的字符串是abba的时候,按照刚才的思路dp[0]=1 dp[1]=2 dp[2]=1 dp[3]=3 +但是dp[3]是错误的,因为中间存在了重复的字符。所以要加一种情况。 +dp[i]=Math.min(dp[i-1]+1,i-map.get(array[i])) + +public class Solution { + /** + * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可 + * + * + * @param s string字符串 + * @return int整型 + */ + public int lengthOfLongestSubstring (String s) { + if(s==null) return 0; + char[]array=s.toCharArray(); + if(array.length==1){ + return 1; + } + int[]dp=new int[array.length]; + int maxLength=1; + HashMapmap=new HashMap<>(); + dp[0]=1; + map.put(array[0],0); + for(int i=1;i= 10 && num <= 26){ + if(i == 1){ + dp[i] += 1; + }else{ + dp[i] += dp[i-2]; + } + } + } + return dp[nums.length()-1]; + + } +``` + +## 二分 + +### 求平方根 + +```java +import java.util.*; + + +public class Solution { + /** + * + * @param x int整型 + * @return int整型 + */ + public int sqrt (int x) { + // write code here + if(x < 2){ + return x; + } + int left = 1; + int right = x / 2; + while(left <= right){ + int mid = left + (right - left) / 2; + if(x / mid == mid){ + return mid; + } else if(x / mid < mid){ + right = mid - 1; + } else if(x / mid > mid){ + left = mid + 1; + } + } + + return right; + } +} +``` + +### 在旋转过的有序数组中寻找目标值 + +```java +public class Solution { + public int search (int[] nums, int target) { + // write code here + if (nums == null || nums.length < 1) { + return -1; + } + if (nums.length == 1) { + return nums[0] == target ? 0 : -1; + } + + int start = 0, end = nums.length - 1; + while (end >= start) { + // 找到 左右指针中间位置 + int mid = (end + start) >> 1; + if (nums[mid] == target) { + return mid; + } + // 在左侧升序数组中 + if (nums[0] <= nums[mid]) { + // 在开头和 mid 之间,那么 右指针则为 mid -1 + if (target >= nums[0] + && target < nums[mid]) { + end = mid -1; + } else { + start = mid + 1; + } + } else { + // 如果在 mid 和end 之间,更新 start 为 mid = 1 + if (target > nums[mid] + && target <= nums[end]) { + start = mid + 1; + } else { + end = mid - 1; + } + } + } + return -1; + } +} +``` + +### 在两个长度相等的排序数组中找到上中位数 + +```java +// 普通 +public class Solution { + public int findMedianinTwoSortedAray (int[] arr1, int[] arr2) { + // write code here + int length1 = arr1.length; + int length2 = arr2.length; + int i=0; + int j=0; + int[] arr = new int[length1+length2]; + int index = 0; + //合并数组 + for(;i>1); + //arr2中位数 + mid2 = l2+((r2-l2)>>1); + int k = r1-l1+1; + if(arr1[mid1] == arr2[mid2]){ + //若两数组中位数相等,整体中位数也是这个 + return arr1[mid1]; + } + else if(arr1[mid1] > arr2[mid2]){ + if(k%2 == 0){//区间元素个数为偶数 + r1 = mid1; //整体中位数在arr1左区间,包括mid1 + l2 = mid2+1; //整体中位数在arr2右区间,不包括mid2 + } + else if(k%2 == 1){ //区间元素个数为奇数 + r1 = mid1; //整体中位数在arr1左区间,包括mid1 + l2 = mid2; //整体中位数在arr2右区间,包括mid2 + } + } + else if (arr1[mid1] < arr2[mid2]){ + if(k%2 == 0){//区间元素个数为偶数 + r2 = mid2; //整体中位数在arr2左区间,包括mid2 + l1 = mid1+1; //整体中位数在arr1右区间,不包括mid1 + } + else if(k%2 == 1){ //区间元素个数为奇数 + r2 = mid2; //整体中位数在arr2左区间,包括mid2 + l1 = mid1; //整体中位数在arr1右区间,包括mid1 + } + } + } + //当区间内只有一个元素时,两个区间中最小值即为整体中位数 + return Math.min(arr1[l1],arr2[l2]); +} +``` + +### 有序矩阵元素查找 + +```java +public class Solution { + public int[] findElement(int[][] mat, int n, int m, int x) { + // write code here + int[] result = new int[2]; + int row = 0; + int col = m - 1; + while(row < n && col >= 0) { + if(mat[row][col] == x) { + result[0] = row; + result[1] = col; + break; + } + if(x > mat[row][col]) { + row ++; + } else { + col --; + } + } + return result; + } +} +``` + +### 二分查找 + +```java +public int binarySearch(int target, int[] nums) { + int left = 0; + int right = nums.length-1; //取最后一个下标 + int mid = 0; + //左下标大于右下标,直接返回-1 + if (left > right || nums.length == 0 || nums == null) { + return -1; + } + // 初始化 right 的赋值是 nums.length - 1, + // 即最后一个元素的索引,而不是 nums.length + while (left <= right) { + mid = left + (right - left) / 2; //如果下标之和除以2有小数,则直接去掉 + if (target == nums[mid]) { + return mid; //找到目标值,然后返回 + } else if (target > nums[mid]) { //目标值大于中间值,向右遍历 + left = mid + 1; //所以向右遍历的第一个下标是:中间下标+1 + } else if (target < nums[mid]) { //目标值小于中间值,向左遍历 + right = mid - 1; //所以向左遍历的最后一个下标是:中间下标-1 + } + } + return -1; //找不到对应目标值,直接返回-1 +} +``` + +```java +// 查找最右值 +public class Solution { + public int upper_bound_search (int n, int v, int[] a) { + // write code here + int left = 0, right = n; + while(left < right){ + int mid = left + (right - left) / 2; + if(a[mid] == v){ + right = mid; + } else if(a[mid] > v){ + right = mid; + } else { + left = mid + 1; + } + } + return left+1; + } +} +``` + +```java +// 查找最左值 +public class Solution { + public int left_bound_search (int[] nums, int target) { + // write code here + if (nums == null || nums.length == 0) { + return -1; + } + int left = 0, right = nums.length - 1; + + while (left < right) { + int mid = (left + right) / 2; + if (nums[mid] >= target) { + right = mid; + } else { + left = mid + 1; + } + } + return nums[right] == target ? right : -1; + } +} +``` + +### 旋转数组的最小数字 + +1.如果mid>right,说明mid-right之间存在被旋转数组,left = mid+1 +2.如果mid array[right]){ + left = mid + 1; + } + else{ + right = right - 1; + } + } + return array[left]; + } +} +``` + +### 数字在升序数组中出现的次数 + +```java +public class Solution { + public int GetNumberOfK(int [] array , int k) { + if(array.length == 0 || k < array[0] + || k > array[array.length-1]){ + return 0; + } + int left = 0; + int right = array.length -1; + int count = 0; + int found = 0; + int mid = -1; + while(left < right){ + mid = (left+right)/2; + if(array[mid] > k){ + right = mid-1; + }else if(array[mid] < k){ + left = mid+1; + }else{ + count++; + found = mid; + break; + } + } + + int prev = mid-1; + int foll = mid+1; + while(prev >= left){ + if(array[prev] == k){ + count++; + prev--; + }else{ + break; + } + } + + while(foll <= right){ + if(array[foll] == k){ + count++; + foll++; + }else{ + break; + } + } + return count; + } +} +``` + +### 峰值 + +本题之所以可以使用二分,使复杂度讲到lgn,是因为题目中的nums[i] != nums[i + 1]条件, +当中间元素mid不是峰时,一定有一边比mid中间值大, +假设右边的值,即mid+1位置的值大于mid的值,则右边一定存在峰, +因为右边的值从mid开始要么是 /\ 这个单调性,要么是 / 这种单调性,两种都一定存在峰 + +```java +import java.util.*; +public class Solution { + public int findPeakElement (int[] nums) { + int left = 0; + int right = nums.length - 1; + //二分法 + while(left < right){ + int mid = (left + right) / 2; + //右边是往下,不一定有坡峰 + if(nums[mid] > nums[mid + 1]) + right = mid; + //右边是往上,一定能找到波峰 + else + left = mid + 1; + } + //其中一个波峰 + return right; + } +} +``` + +## 数组 + +### 数组中只出现一次的数字 + +```java +public class Solution { + + public void FindNumsAppearOnce(int [] array, + int num1[] , int num2[]) { + int num = 0; + for(int i = 0; i < array.length; i++){ + num^=array[i]; + } + + int count = 0; + // 标志位,记录num中的第一个1出现的位置 + for(;count < array.length; count++){ + if((num&(1< set = new HashSet<>(); + for(int i = 0; i < array.length; i++){ + if(!set.add(array[i])){ + set.remove(array[i]); + } + } + + Object[] temp = set.toArray(); + num1[0] = (int)temp[0]; + num2[0] = (int)temp[1]; + }*/ +} + +``` + +### 合并两个有序的数组 + +```java +public class Solution { + public void merge(int A[], int m, int B[], int n) { + int i = m-1, j = n-1, k = m+n-1; + while(i >= 0 && j >= 0){ + if(A[i] > B[j]){ + A[k--] = A[i--]; + } else { + A[k--] = B[j--]; + } + } + + while(j >= 0){ + A[k--] = B[j--]; + } + } +} +``` + +### 子数组最大乘积 + +```java +public class Solution { + public double maxProduct(double[] arr) { + if(arr.length == 0 || arr == null){ + return 0.0; + } + double[] max = new double[arr.length]; + double[] min = new double[arr.length]; + max[0] = min[0] = arr[0]; + for(int i = 1; i < arr.length; i++){ + max[i] = + Math.max(Math.max(max[i-1]*arr[i], + min[i-1]*arr[i]),arr[i]); + min[i] = + Math.min(Math.min(max[i-1]*arr[i], + min[i-1]*arr[i]),arr[i]); + } + + double ans = max[0]; + for(int i = 0; i < max.length; i++){ + if(max[i] > ans){ + ans = max[i]; + } + } + return ans; + } +} + +public int maxProduct (int[] nums) { + int max = nums[0]; + int min = nums[0]; + int result = nums[0]; + for (int i = 1; i < nums.length; i++) { + int t = max; + max = Math.max(nums[i], + Math.max(max * nums[i], min * nums[i])); + min = Math.min(nums[i], + Math.min(t * nums[i], min * nums[i])); + result = Math.max(result,max); + } + return result; +} +``` + +### 数组中最长连续子序列 + +```java +public int MLS(int[] arr) { + if (arr == null || arr.length == 0) + return 0; + int longest = 1;//记录最长的有序序列 + int count = 1;//目前有序序列的长度 + //先对数组进行排序 + Arrays.sort(arr); + for (int i = 1; i < arr.length; i++) { + //跳过重复的 + if (arr[i] == arr[i - 1]) + continue; + //比前一个大1,可以构成连续的序列,count++ + if ((arr[i] - arr[i - 1]) == 1) { + count++; + } else { + //没有比前一个大1,不可能构成连续的, + //count重置为1 + count = 1; + } + //记录最长的序列长度 + longest = Math.max(longest, count); + } + return longest; +} +``` + +### 数组中未出现的最小正整数 + +时间复杂度O(n),空间复杂度O(n)的做法:开辟一个新的数组arr,长度为nums.length+1,遍历nums数组, +如果非负且值小于nums的长度,则把arr[nums[i]]置1。然后遍历辅助数组,找到下标不为1的第一个元素即可。 + +```java +public class Solution { + public int minNumberDisappeared (int[] nums) { + int []arr = new int [nums.length+1]; + for(int i=0;i0){ + arr[nums[i]] =1; + } + } + for(int i=1;i= end){ + return; + } + int mid = start + (end - start)/2; + + mergeSort(array,start,mid); + mergeSort(array,mid + 1,end); + + merge(array,start,mid,end); + } + + public void merge(int[] array,int start,int mid,int end){ + int[] temp = new int[end-start+1]; + int k = 0; + int i = start; + int j = mid + 1; + while(i <= mid && j <= end){ + if(array[i] < array[j]){ + temp[k++] = array[i++]; + } else { + temp[k++] = array[j++]; + res = (res + mid - i + 1) % 1000000007; + } + } + + while(i <= mid){ + temp[k++] = array[i++]; + } + + while(j <= end){ + temp[k++] = array[j++]; + } + + for(k = 0; k < temp.length; k++){ + array[start + k] = temp[k]; + } + } + + + /* + public int InversePairs(int [] array) { + int sum = 0; + for(int i = 0; i < array.length - 1; i++){ + for(int j = i + 1; j < array.length; j++){ + if(array[i] > array[j]){ + sum ++; + } + } + } + return sum % 1000000007; + } + */ +} +``` + +### 调整数组顺序使奇数位于偶数前面 + +```java +// 方法一:先记录下技术的个数,然后遍历数组,用另外一个数组接收奇数和偶数 +import java.util.*; +public class Solution { + public int[] reOrderArray (int[] array) { + // write code here + int[] arr=new int[array.length]; + int num=0; + for(int a:array){ + if((a&1)==1) num++;//奇数 + } + int i=0; + for(int a:array){ + if((a&1)==1){ //奇数 + arr[i++]=a; + }else{ + arr[num++]=a; + } + } + return arr; + } +} + +// 方法二:记录已经是奇数的位置下标(视作为有序区域),然后向后遍历, +// 一经发现是奇数则进行“插入排序”,然后有序区下标加1。 +public class Solution { + public int[] reOrderArray (int[] array) { + // 首先是对数值长度进行特判 + if(array==null||array.length==0) return array; + //记录已经是奇数的位置 + int j=0; + int temp = 0; + for(int i =0;ij){ + //这区间整体向后移动一位 + array[k] = array[k-1]; + k--; + } + //移位之后将对应的值赋值 + array[k] = temp; + j++; + } + } + //返回结果数数组 + return array; + } +} +``` + +### 矩阵乘法 + +```java +import java.util.*; +public class Solution { + public int[][] solve (int[][] a, int[][] b) { + // write code here + //矩阵相乘条件:a的列和b的行必须相等 + //记录a的行、a的列(b的行)、b的列 + int aRow = a.length; + int aColumn = a[0].length; + int bColumn = b[0].length; + //新矩阵行为a的行,列为b的列 + int[][] res = new int[aRow][bColumn]; + for(int i=0; i> result = new ArrayList<>(); + //暂存结果 + List path = new ArrayList<>(); + + public List> permuteUnique(int[] nums) { + boolean[] used = new boolean[nums.length]; + Arrays.fill(used, false); + Arrays.sort(nums); + backTrack(nums, used); + return result; + } + + private void backTrack(int[] nums, boolean[] used) { + if (path.size() == nums.length) { + result.add(new ArrayList<>(path)); + return; + } + for (int i = 0; i < nums.length; i++) { + // used[i - 1] == true,说明同⼀树⽀nums[i - 1]使⽤过 + // used[i - 1] == false,说明同⼀树层nums[i - 1]使⽤过 + // 如果同⼀树层nums[i - 1]使⽤过则直接跳过 + if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) { + continue; + } + //如果同⼀树⽀nums[i]没使⽤过开始处理 + if (used[i] == false) { + used[i] = true;//标记同⼀树⽀nums[i]使⽤过,防止同一树支重复使用 + path.add(nums[i]); + backTrack(nums, used); + //回溯,说明同⼀树层nums[i]使⽤过,防止下一树层重复 + path.remove(path.size() - 1); + used[i] = false;//回溯 + } + } + } +} +``` + +### 岛屿的数量 + +```java +class Solution { + public int numIslands(char[][] grid) { + int count = 0; + for(int i = 0; i < grid.length; i++) { + for(int j = 0; j < grid[0].length; j++) { + if(grid[i][j] == '1'){ + bfs(grid, i, j); + count++; + } + } + } + return count; + } + + public void bfs(char[][] grid, int i, int j){ + Queue list = new LinkedList<>(); + list.add(new int[] { i, j }); + while(!list.isEmpty()){ + int[] cur = list.remove(); + i = cur[0]; j = cur[1]; + if(inArea(i,j,grid) + && grid[i][j] == '1') { + grid[i][j] = '0'; + list.add(new int[] { i + 1, j }); + list.add(new int[] { i - 1, j }); + list.add(new int[] { i, j + 1 }); + list.add(new int[] { i, j - 1 }); + } + } + } + + public boolean inArea(int i, int j, char[][] grid){ + return i >=0 && j >= 0 + && i < grid.length + && j < grid[0].length; + } +} +``` + +### 没有重复项数字的所有排列(全排列) + +```java +public class Demo1 { + ArrayList> res; + + public ArrayList> permute(int[] nums) { + res = new ArrayList>(); + if (nums == null || nums.length < 1) + return res; + //对数组元素进行从小到大排序 + Arrays.sort(nums); + ArrayList list = new ArrayList(); + + solve(list, nums); + + return res; + } + + private void solve(ArrayList list, int[] nums) { + if (list.size() == nums.length) { + res.add(new ArrayList(list)); + return; + } + for (int i = 0; i < nums.length; i++) { + if (!list.contains(nums[i])) { + list.add(nums[i]); + solve(list, nums); + list.remove(list.size() - 1); + } + } + } +} +``` + +### 集合的所有子集 + +```java +import java.util.*; +public class Solution { + public ArrayList> subsets(int[] S) { + ArrayList> res=new ArrayList<>(); + Arrays.sort(S); + LinkedList list=new LinkedList<>(); + dfs(res,list,0,S); + return res; + } + public void dfs(ArrayList> res, + LinkedList list, int k, int[] S){ + res.add(new ArrayList<>(list)); + for(int i=k;i> res; + private boolean[] visited; + + public ArrayList> permute(int[] nums) { + res = new ArrayList<>(); + visited = new boolean[nums.length]; + List list = new ArrayList<>(); + backtrace(nums, list); + return res; + } + + private void backtrace(int[] nums, List list) { + if (list.size() == nums.length) { + res.add(new ArrayList<>(list)); + } + + for (int i = 0; i < nums.length; i++) { + if (visited[i]) continue; + visited[i] = true; + list.add(nums[i]); + backtrace(nums, list); + visited[i] = false; + list.remove(list.size() - 1); + } + } +} +``` + +### N皇后问题 + +```java +public class Solution { + /** + * + * @param n int整型 the n + * @return int整型 + */ + public int Nqueen (int n) { + // write code here + List res=new ArrayList<>(); + char[][] chess=new char[n][n]; + for(int i=0;i res){ + if(row==chess.length){ + res.add(1); + return; + } + for(int col=0;col=0&&j=0&&i>=0;i--,j--){ + if(chess[i][j]=='Q'){ + return false; + } + } + return true; + } +} +``` + +### 把数组字符串转换为 ip 地址 + +回溯法插入'.',每次可以插入到1个,2个或者3个字符后面,插入3次之后对得到的字符串进行验证 + +```java +public class Solution { + ArrayList res = new ArrayList<>(); + public ArrayList restoreIpAddresses (String s) { + // write code here + if(s.length() == 0) + return res; + //表示当前字符串s,可以从第0个位置开始插入'.' ,还有3个'.'可以插入 + backTrack(s, 0, 3); + return res; + } + + public void backTrack(String s, int start, int cnt){ + if(cnt == 0){ + String[] splits = s.split("\\."); + //没有插入4个合法的小数点 + if(splits.length < 4) + return; + //判断每一位是否合法 + for(String str:splits){ + if(str.length() > 1 && str.charAt(0) == '0') return; //最前面的数字不能为0 + if(Integer.valueOf(str) > 255) return; //每一位都不能大于255 + } + res.add(s); + return; + } + + if(start >= s.length()) return; //没有插完全部的点 就已经超出字符串的范围了 + int len = s.length(); + //每次将一个字符作为一位 + backTrack(s.substring(0,start+1)+'.' + +s.substring(start+1,len), start+2, cnt-1); + //每次将两位字符作为一位 + if(start < len-2) + backTrack(s.substring(0,start+2)+'.' + +s.substring(start+2,len), start+3, cnt-1); + //每次将三位字符作为一位 + if(start < len-3) + backTrack(s.substring(0,start+3)+'.' + +s.substring(start+3,len), start+4, cnt-1); + } +} +``` + +### 加起来和为目标值的组合 + +```java +import java.util.* ; +public class Solution { + public ArrayList> combinationSum2(int[] num, int target) { + Arrays.sort(num) ; + ArrayList> res = new ArrayList<>() ; + help(target , num , 0 , res , new ArrayList()) ; + return res ; + } + public void help(int target , int[] num , int idx , + ArrayList> res , ArrayList tmp) { + if(target == 0) { + res.add(new ArrayList(tmp)) ; + return ; + } + for(int i = idx ; i < num.length ; i ++) { + if(num[i] > target) return ;//剪枝 + if((i > idx && num[i] == num[i-1])) continue ;//去重 + tmp.add(num[i]) ; + help(target-num[i] , num , i + 1 , res , tmp) ;//递归 + tmp.remove(tmp.size() - 1) ;//回溯 + } + } +} +``` + +## 其他 + +### 螺旋矩阵 + +给定一个m x n大小的矩阵(m行,n列),按螺旋的顺序返回矩阵中的所有元素 + +```java +import java.util.*; +public class Solution { + public ArrayList spiralOrder(int[][] matrix) { + ArrayList list = new ArrayList<>(); + + if(matrix.length == 0) { + return list; + } + + int left = 0; + int right = matrix[0].length - 1; + int top = 0; + int bottom = matrix.length - 1; + int x = 0; + + + while(true) { + for(int i = left; i <= right; i++) { //从左到右 + list.add(matrix[top][i]) ; + } + + if(++top > bottom){ + break; + } + for(int i = top; i <= bottom; i++){ + list.add( matrix[i][right]); //从上到下 + } + + if(left > --right){ + break; + } + for(int i = right; i >= left; i--){ + list.add(matrix[bottom][i]); //从右到左 + } + + if(top > --bottom){ + break; + } + for(int i = bottom; i >= top; i--){ + list.add(matrix[i][left]); //从下到上 + } + + if(++left > right){ + break; + } + } + return list; + } +} +``` + +### 顺时针旋转矩阵 + +原矩阵元素的列数变成新矩阵元素的行数: 原矩阵元素的行数是第2行,旋转后元素的列数是从右往左倒数第2列。 +因此对于原矩阵mat[i][j],旋转后该值应该在新矩阵ans[j][n-i-1]的位置。 + +```java +class Solution { +public: + vector > rotateMatrix(vector > mat, int n) { + // write code here + vector> ans(n,vector(n)); + for(int i=0;i=10){ + sb.append(arr[temp-10]); + } + //小于10,直接加到sb + else{ + sb.append(temp); + } + M/=N; + } + //负数要多加一个负号 + if(f){ + sb.append('-'); + } + //反转后转为字符串返回 + return sb.reverse().toString(); + } +} +``` + +### 反转数字 + +```java +public class Solution { + /** + * + * @param x int整型 + * @return int整型 + */ + public int reverse (int x) { + // write code here + int res = 0; + while(x != 0){ + // 获取最后一位 + int tail = x % 10; + int newRes = res * 10 + tail; + // 如果不等于,说明溢出 + if((newRes - tail) / 10 != res){ + return 0; + } + res = newRes; + x /= 10; + } + + return res; + } +} +``` + +### 大数加法 + +```java +public class Solution { + public String solve (String s, String t){ + int i = s.length() - 1, j = t.length() - 1; + int temp = 0; + StringBuilder out = new Stringbuilder(); + while (i >= 0 || j >= 0 || temp != 0) { + temp += i >= 0 ? s.charAt(i--) - '0' : 0; + temp += j >= 0 ? t.charAt(j--) - '0' : 0; + out.append(temp % 10); + temp = temp / 10; + } + return out.reverse().toString(); + } +} + +public class Solution { + public String solveByJava(String s, String t){ + BigInteger num1 = new BigInteger(s); + BigInteger num2 = new BigInteger(t); + return num1.add(num2).toString(); + } +} +``` + +### 把字符串转换成整数(atoi) + +1、首位空格:通过trim()函数即可处理 +2、正负:通过判断第一位,使用变量储存符号即可 +3、非数字字符:对每一位进行判断,非数字则结束 +4、越界:通过提前预判,判断拼接后是否大于阈值,进行处理 + +```java +public class Solution { + public int StrToInt (String s) { + // write code here + char[] array = s.trim().toCharArray(); + if(array.length==0){ + return 0; + } + int sign = 1; + int res = 0; + int i = 0; + + if(array[i] == '+' || array[i] == '-'){ + sign = array[i++] == '+' ? 1 : -1; + } + while(i < array.length){ + char cur = array[i]; + if(cur < '0' || cur>'9'){ + break; + } + if (res >= Integer.MAX_VALUE / 10) { + if(res > Integer.MAX_VALUE / 10){ + return sign==1 + ? Integer.MAX_VALUE : Integer.MIN_VALUE; + } + if(res == Integer.MAX_VALUE / 10){ + if(sign == 1 && (cur - '0') > 7){ + return Integer.MAX_VALUE; + }else if(sign == -1 && (cur - '0') > 8){ + return Integer.MIN_VALUE; + } + } + } + res = res * 10 + (cur - '0'); + i++; + } + return sign * res; + } +} +``` + +### 最长公共前缀 + +```java +// 方法一:先取第一个字符串当做他们的公共前缀 +// 然后找出他和第2个字符串的公共前缀,然后再用这个找出的公共前缀分别和第3个,第4个……判断 +public String longestCommonPrefix(String[] strs) { + //边界条件判断 + if (strs == null || strs.length == 0) + return ""; + //默认第一个字符串是他们的公共前缀 + String pre = strs[0]; + int i = 1; + while (i < strs.length) { + //不断的截取 + while (strs[i].indexOf(pre) != 0) + pre = pre.substring(0, pre.length() - 1); + i++; + } + return pre; +} + +// 方法二:按照字典序排序之后比较字典序最小的子串和字典序最大的子串的相同部分, +// 得到的最长公共前缀就是所有字符串的最长公共前缀 +public class Solution { + public String longestCommonPrefix (String[] strs) { + int len = strs.length; + if(len==0) return ""; + Arrays.sort(strs); + //枚举第一个最小的子串和最后一个最大的子串 + int i = 0; + String a = strs[0]; + String b = strs[len-1]; + for(i = 0;i < a.length()&&a.charAt(i)==b.charAt(i);i++); + return a.substring(0,i); + } +} +``` + +### 回文数字 + +```java +// 方法一:双指针 +import java.util.*; +public class Solution { + /** + * + * @param x int整型 + * @return bool布尔型 + */ + public boolean isPalindrome (int x) { + if(x<0) return false; + // 转换成字符串 + String xs = String.valueOf(x); + // 利用双指针 + int left = 0; + int right = xs.length()-1; + // 比较字符串的头部和尾部是否相同 + while(left < right){ + // 不相同直接返回 + if(xs.charAt(left) != xs.charAt(right)) return false; + left++; + right--; + } + return true; + } +} + +// 方法二:翻转数字 +public class Solution { + /** + * + * @param x int整型 + * @return bool布尔型 + */ + public boolean isPalindrome (int x) { + // write code here + if(x<0) return false; + int reverse = 0; + int tmp = x; + while(tmp>0){ + int div = tmp%10; + // 判断是否会溢出 + if(reverse >= Integer.MAX_VALUE/10 && div > 7) return false; + // 获得反向数字 + reverse = reverse*10 + div; + tmp = tmp/10; + } + return x == reverse; + } +} +``` + +### 字符串变形(反序,大写) + +利用split切割为String数组 +String数组从后往前遍历,拿到具体的String从0到str.length()遍历,并判断大小写然后转换 +需要注意的地方就是s.split(" ",-1),limit需要设置为-1来不舍弃最后的空串 + +```java +import java.util.*; + +public class Solution { + public String trans(String s, int n) { + // write code here + String[] strArr = s.split(" ",-1); // 注意这里limit为-1,不舍弃最后的空串 + StringBuilder sb = new StringBuilder(); + for(int i = strArr.length - 1; i >= 0; i--) { + for(int j = 0; j < strArr[i].length(); j++) { + if(Character.isUpperCase(strArr[i].charAt(j))) { + sb.append(Character.toLowerCase(strArr[i].charAt(j))); + } else { + sb.append(Character.toUpperCase(strArr[i].charAt(j))); + } + } + if(i != 0) { + sb.append(" "); + } + } + return sb.toString(); + } +} +``` + +### 最大值(数组拼接最大数) + +```java +import java.util.*; +public class Solution { + /** + * 最大数 + * @param nums int整型一维数组 + * @return string字符串 + */ + public String solve (int[] nums) { + String[] strArr = new String[nums.length]; + for (int i = 0 ; i < nums.length ; i++) { + strArr[i] = String.valueOf(nums[i]); + } + Arrays.sort(strArr, + (o1, o2) -> Integer.parseInt(o2 + o1) - Integer.parseInt(o1 + o2)); + StringBuilder maxString = new StringBuilder(); + if (strArr[0].equals( "0")) { + return "0"; + } + for (int i = 0 ; i < strArr.length; i++) { + maxString.append(strArr[i]); + } + return maxString.toString(); + } +} +``` + +### 验证ip地址 + +```java +public String validIPAddress(String IP) { + return validIPv4(IP) + ? "IPv4" : (validIPv6(IP) ? "IPv6" : "Neither"); +} + +private boolean validIPv4(String IP) { + String[] strs = IP.split("\\.", -1); + if (strs.length != 4) { + return false; + } + + for (String str : strs) { + if (str.length() > 1 && str.startsWith("0")) { + return false; + } + try { + int val = Integer.parseInt(str); + if (!(val >= 0 && val <= 255)) { + return false; + } + } catch (NumberFormatException numberFormatException) { + return false; + } + } + return true; +} + +private boolean validIPv6(String IP) { + String[] strs = IP.split(":", -1); + if (strs.length != 8) { + return false; + } + + for (String str : strs) { + if (str.length() > 4 || str.length() == 0) { + return false; + } + try { + int val = Integer.parseInt(str, 16); + } catch (NumberFormatException numberFormatException) { + return false; + } + } + return true; +} +``` + +### 二进制中1的个数 + +将数字与1进行与运算,返回结果为1则表明数字二进制最后一位是1,通过对不断数字右移运算判断有多少个1。 + +```java +public class Solution { + // replace替换 + public int NumberOf1(int n) { + return Integer.toBinaryString(n).replace("0","").length(); + } + + // 遍历字符串记录 + public int NumberOf1(int n) { + String str = Integer.toBinaryString(n); + int length = str.length(); + int count = 0; + for (int i=0; i>>= 1; + } + return res; + } +} +``` + +### 第一个只出现一次的字符 + +```java +import java.util.*; +public class Solution { + public static int FirstNotRepeatingChar(String str) { + int[] map = new int[58]; + char[] chs = str.toCharArray(); + for(int i = 0; i < chs.length; i++){ + map[chs[i] - 'A'] ++; + } + int res = -1; + for(int i = 0; i < chs.length; i++){ + if(map[chs[i] - 'A'] == 1){ + res = i; + break; + } + } + return res; + } +} +``` + +## 其他编程题(golang、java) + +### 单例模式 + +双重否定单例模式:即可以保证线程的安全性 +(避免两个线程同时进入到 synchronized (Singleton.class)时, +线程1先获取到了锁,释放后,线程2执行,如果没有第二次空的判断,会导致多次创建对象), +也可以实现只有第一次创建new的时候才会执行到同步代码块中的代码,提高了效率。 + +- java + +```java +// 方法一 +//优点:线程安全,volatile关键词主要是保证多线程之间的可见性, +// 保证线程在每次取值volatile变量都是最新值 +//volatile关键字主要是禁止命令重排序的,但是volatile不是原子性的 +public class Singleton { + private static volatile Singleton instance = null; + private Singleton() {} + public static Singleton getInstance() { + if (null == instance) { + synchronized (Singleton.class) { + if (null == instance) { + instance = new Singleton(); + } + } + } + return instance; + } +} + +// 方法二 +public class Singleton { + private Singleton() {} + private static Singleton getInstance() { + return SingletonHolder.instance; + } + private static class SingletonHolder { + //静态变量值会初始化一次 + private static Singleton instance = new Singleton(); + } +} +``` + +- go + +```go +package main + +import ( + "fmt" + "sync" +) + +var lock = &sync.Mutex{} // 创建互锁 +type Singleton struct {} // 创建结构体 + +var singletonInstance *Singleton // 创建指针 + +func getInstance() *single { + if singletonInstance == nil { //!!!注意这里check nil了两次 + lock.Lock() + defer lock.Unlock() + if singletonInstance == nil { + fmt.Println("创建单例") + singletonInstance = &Singleton{} + } else { + fmt.Println("单例对象已创建") + } + } else { + fmt.Println("单例对象已创建") + } + return singletonInstance +} +``` + +### 实现线程安全的生产者消费者 + +- java + +https://blog.csdn.net/u010983881/article/details/78554671 + +- golang + +https://blog.csdn.net/weixin_50005436/article/details/123065703 + +### 一个10G的文件,里面全部是自然数,一行一个,乱序排列,对其排序。在32位机器上面完成,内存限制为 2G(bitmap原理知道吗?) + +首先,10G文件是不可能一次性放到内存里的。这类问题一般有两种解决方案: + +- 将10G文件分成多个小文件,分别排序,最后合并一个文件; +- 采用bitmap + +如果面试大数据类岗位,可能面试官就想考察你对Mapreduce熟悉程度,要采用第一种merge and sort。 + +如果是算法类岗位,就要考虑bitmap,但需要注意的是bitmap**不能对重复数据进行排序**。这里我们详细介绍一下: + +定量分析一下,32位机器自然数有2^32个,用一个bit来存放一个整数,那么所需的内存是, +`2^32/(8<<20) = 512MB` ,这些数存放在文件中,一行一个,需要20G容量, +所以题目问10G文件,只需要256MB内存就可以完成。 + +bitmap实现具体分为两步:插入一个数,和排序。 + +```go +type BitMap struct { + vec []byte + size int +} + +func New(size int) *BitMap { + return &BitMap{ + size: size, + vec: make([]byte, size), + } +} + +func (bm *BitMap) Set(num int) (ok bool, err error) { + if num/8 >= bm.size { + return false, errors.New("the num overflows the size of bitmap") + } + bm.vec[num/8] |= 1 << (num % 8) + return true, nil +} + +func (bm *BitMap) Exist(num int) bool { + if num/8 >= bm.size { + return false + } + return bm.vec[num/8]&(1<<(num%8)) > 0 +} + +func (bm *BitMap) Sort() (ret []int) { + ret = make([]int, 0) + for i := 0; i < (8 * bm.size); i++ { + if bm.Exist(i) { + ret = append(ret, i) + } + } + return +} +``` + +### 实现使用字符串函数名,调用函数 + +思路:采用反射的Call方法实现。 + +```go +package main +import ( + "fmt" + "reflect" +) + +type Animal struct{ + +} + +func (a *Animal) Eat(){ + fmt.Println("Eat") +} + +func main(){ + a := Animal{} + reflect.ValueOf(&a).MethodByName("Eat").Call([]reflect.Value{}) + +} +``` + +### 负载均衡算法。(一致性哈希) + +```go +package main + +import ( + "fmt" + "sort" + "strconv" +) + +type HashFunc func(key []byte) uint32 + +type ConsistentHash struct { + hash HashFunc + hashvals []int + hashToKey map[int]string + virtualNum int +} + +func NewConsistentHash(virtualNum int, fn HashFunc) *ConsistentHash { + return &ConsistentHash{ + hash: fn, + virtualNum: virtualNum, + hashToKey: make(map[int]string), + } +} + +func (ch *ConsistentHash) AddNode(keys ...string) { + for _, k := range keys { + for i := 0; i < ch.virtualNum; i++ { + conv := strconv.Itoa(i) + hashval := int(ch.hash([]byte(conv + k))) + ch.hashvals = append(ch.hashvals, hashval) + ch.hashToKey[hashval] = k + } + } + sort.Ints(ch.hashvals) +} + +func (ch *ConsistentHash) GetNode(key string) string { + if len(ch.hashToKey) == 0 { + return "" + } + keyhash := int(ch.hash([]byte(key))) + id := sort.Search(len(ch.hashToKey), func(i int) bool { + return ch.hashvals[i] >= keyhash + }) + return ch.hashToKey[ch.hashvals[id%len(ch.hashvals)]] +} + +func main() { + ch := NewConsistentHash(3, func(key []byte) uint32 { + ret, _ := strconv.Atoi(string(key)) + return uint32(ret) + }) + ch.AddNode("1", "3", "5", "7") + testkeys := []string{"12", "4", "7", "8"} + for _, k := range testkeys { + fmt.Printf("k:%s,node:%s\n", k, ch.GetNode(k)) + } +} +``` + +### (Goroutine)有三个函数,分别打印"cat", "fish","dog"要求每一个函数都用一个goroutine,按照顺序打印100次 + +此题目考察channel,用三个无缓冲channel,如果一个channel收到信号则通知下一个。 + +```go +package main + +import ( + "fmt" + "time" +) + +var dog = make(chan struct{}) +var cat = make(chan struct{}) +var fish = make(chan struct{}) + +func Dog() { + <-fish + fmt.Println("dog") + dog <- struct{}{} +} + +func Cat() { + <-dog + fmt.Println("cat") + cat <- struct{}{} +} + +func Fish() { + <-cat + fmt.Println("fish") + fish <- struct{}{} +} + +func main() { + for i := 0; i < 100; i++ { + go Dog() + go Cat() + go Fish() + } + fish <- struct{}{} + + time.Sleep(10 * time.Second) +} +``` + +- Java 实现 + +https://www.cnblogs.com/jyx140521/p/6747750.html + +### 两个协程交替打印10个字母和数字 + +思路:采用channel来协调goroutine之间顺序。 + +主线程一般要waitGroup等待协程退出,这里简化了一下直接sleep。 + +```go +package main + +import ( + "fmt" + "time" +) + +var word = make(chan struct{}, 1) +var num = make(chan struct{}, 1) + +func printNums() { + for i := 0; i < 10; i++ { + <-word + fmt.Println(1) + num <- struct{}{} + } +} +func printWords() { + for i := 0; i < 10; i++ { + <-num + fmt.Println("a") + word <- struct{}{} + } +} + +func main() { + num <- struct{}{} + go printNums() + go printWords() + time.Sleep(time.Second * 1) +} +``` + +- Java 实现 + +```java +package com.lutongnet.util; + +import org.junit.Test; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedTransferQueue; +import java.util.concurrent.TransferQueue; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.LockSupport; +import java.util.concurrent.locks.ReentrantLock; + +/** + * @author mifei + * @version 1.0.0 + * @description 多线程测试 + * @date 2020-11-28 15:18 + */ +public class CommonThreadTest { + + Thread t1 = null; + Thread t2 = null; + + /** + * 测试Synchronized的wait和notify写法 + */ + @Test + public void testSynchronized() { + Object o = new Object(); + char [] letterArray = "ABCDEFGHIJ".toCharArray(); + char [] numbberArray = "1234567890".toCharArray(); + + t1 = new Thread(()->{ + synchronized (o) { + for (char c: letterArray) { + System.out.println("字母:" + c); + try { + o.notify(); + o.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + o.notify(); + } + }); + + t2 = new Thread(()->{ + synchronized (o) { + for (char c: numbberArray) { + System.out.println("数字:" + c); + try { + o.notify(); + o.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + o.notify(); + } + }); + + t1.start(); + t2.start(); + } + + /** + * 测试ReenTrantLock写法 + */ + @Test + public void testReenTrantlock() { + char [] letterArray = "ABCDEFGHIJ".toCharArray(); + char [] numberArray = "1234567890".toCharArray(); + + Lock lock = new ReentrantLock(); + Condition letterCondition = lock.newCondition(); + Condition numberCondition = lock.newCondition(); + + new Thread(()->{ + try { + lock.lock(); + for (char c: letterArray) { + System.out.println("字母:" + c); + numberCondition.signal(); + letterCondition.await(); + } + numberCondition.signal(); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + }, "t1").start(); + + new Thread(()->{ + try { + lock.lock(); + for (char c: numberArray) { + System.out.println("数字:" + c); + letterCondition.signal(); + numberCondition.await(); + } + letterCondition.signal(); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + }, "t2").start(); + } + + /** + * 测试LockSupport写法 + */ + @Test + public void testLockSupport() { + char [] letterArray = "ABCDEFGHIJ".toCharArray(); + char [] numberArray = "1234567890".toCharArray(); + + t1 = new Thread(()->{ + for (char c: letterArray) { + System.out.println("字母:" + c); + LockSupport.unpark(t2); + LockSupport.park(); + } + }); + + t2 = new Thread(()->{ + for (char c: numberArray) { + LockSupport.park(); + System.out.println("数字:" + c); + LockSupport.unpark(t1); + } + }); + + t1.start(); + t2.start(); + } + + /** + * 测试BlockingQueue写法 + */ + @Test + public void testBlockingQueue() { + char [] letterArray = "ABCDEFGHIJ".toCharArray(); + char [] numberArray = "1234567890".toCharArray(); + + BlockingQueue q1 = new ArrayBlockingQueue(1); + BlockingQueue q2 = new ArrayBlockingQueue(1); + + new Thread(()->{ + for (char c: letterArray) { + System.out.println("字母:" + c); + try { + q1.put("ok"); + q2.take(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + } + }, "t1").start(); + + new Thread(()->{ + for (char c: numberArray) { + try { + q1.take(); + System.out.println("数字:" + c); + q2.put("ok"); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }, "t2").start(); + } + + /** + * 测试AtomicInteger写法 + */ + @Test + public void testAtomicInteger() { + AtomicInteger threadNo = new AtomicInteger(1); + + char [] letterArray = "ABCDEFGHIJ".toCharArray(); + char [] numberArray = "1234567890".toCharArray(); + + new Thread(()->{ + for (char c: letterArray) { + while (threadNo.get() != 1) {} + System.out.println("字母:" + c); + threadNo.set(2); + } + }, "t1").start(); + + new Thread(()->{ + for (char c: numberArray) { + while (threadNo.get() != 2) {} + System.out.println("数字:" + c); + threadNo.set(1); + } + }, "t2").start(); + } + + /** + * 测试TransferQueue写法 + */ + @Test + public void testTransferQueue() { + char [] letterArray = "ABCDEFGHIJ".toCharArray(); + char [] numberArray = "1234567890".toCharArray(); + + TransferQueue queue = new LinkedTransferQueue<>(); + new Thread(()->{ + try { + for (char c : letterArray) { + System.out.println("数字:" + queue.take()); + queue.transfer(c); + } + + } catch (InterruptedException e) { + e.printStackTrace(); + } + }, "t1").start(); + + new Thread(()->{ + try { + for (char c : numberArray) { + queue.transfer(c); + System.out.println("字母:" + queue.take()); + } + + } catch (InterruptedException e) { + e.printStackTrace(); + } + }, "t2").start(); + + } +} +``` + +### 启动 2个groutine 2秒后取消, 第一个协程1秒执行完,第二个协程3秒执行完。 + +思路:采用`ctx, _ := context.WithTimeout(context.Background(), time.Second*2)`实现2s取消。 +协程执行完后通过channel通知,是否超时。 + +```go +package main + +import ( + "context" + "fmt" + "time" +) + +func f1(in chan struct{}) { + + time.Sleep(1 * time.Second) + in <- struct{}{} + +} + +func f2(in chan struct{}) { + time.Sleep(3 * time.Second) + in <- struct{}{} +} + +func main() { + ch1 := make(chan struct{}) + ch2 := make(chan struct{}) + ctx, _ := context.WithTimeout(context.Background(), 2*time.Second) + + go func() { + go f1(ch1) + select { + case <-ctx.Done(): + fmt.Println("f1 timeout") + break + case <-ch1: + fmt.Println("f1 done") + } + }() + + go func() { + go f2(ch2) + select { + case <-ctx.Done(): + fmt.Println("f2 timeout") + break + case <-ch2: + fmt.Println("f2 done") + } + }() + time.Sleep(time.Second * 5) +} +``` + + +### 当select监控多个chan同时到达就绪态时,如何先执行某个任务? + +可以在子case再加一个for select语句。 + +```go +func priority_select(ch1, ch2 <-chan string) { + for { + select { + case val := <-ch1: + fmt.Println(val) + case val2 := <-ch2: + priority: + for { + select { + case val1 := <-ch1: + fmt.Println(val1) + + default: + break priority + } + } + fmt.Println(val2) + } + } + +} +``` \ No newline at end of file diff --git "a/docs/dataStructures-algorithms/\351\253\230\351\242\221\347\256\227\346\263\225\351\242\230\347\233\256\346\200\273\347\273\223.pdf" "b/docs/dataStructures-algorithms/\351\253\230\351\242\221\347\256\227\346\263\225\351\242\230\347\233\256\346\200\273\347\273\223.pdf" new file mode 100644 index 0000000..ddc6e28 Binary files /dev/null and "b/docs/dataStructures-algorithms/\351\253\230\351\242\221\347\256\227\346\263\225\351\242\230\347\233\256\346\200\273\347\273\223.pdf" differ diff --git "a/docs/database/MySQL\351\235\242\350\257\225\351\242\230\344\270\200.md" "b/docs/database/MySQL\351\235\242\350\257\225\351\242\230\344\270\200.md" new file mode 100644 index 0000000..aa25c45 --- /dev/null +++ "b/docs/database/MySQL\351\235\242\350\257\225\351\242\230\344\270\200.md" @@ -0,0 +1,783 @@ +### 基础问题 + +> truncate和delete有什么区别? + +truncate table:删除内容、不删除定义、释放空间 +delete table:删除内容、不删除定义、不释放空间 +drop table:删除内容、删除定义、释放空间 + +具体来说,有以下区别: +- truncate table 只能删除表中全部数据,delete from 可以删除表中的全部数据也可以部分删除。 +- delete from记录是一条条删除的,删除的每一行记录都会进日志,而truncate一次性删除整个页,日志只会记录页的释放。 +- truncate删除后不能回滚,delete则可以。 +- truncate的删除速度比delete快。 +- delete删除后,删除的数据占用的存储空间还存在,可以恢复数据;truncate则空间不存在,同时不能恢复数据。 + +总的来说,其差别在于,truncate删除是不可恢复的,同时空间也不存在,不支持回滚,而delete删除正好相反。 + +> select Count(*)、select Count(数字)和select Count(column)的区别? + +count(*)和count(1)返回的结果是记录的总行数,包括对NULL的统计;而count(column)是不会记录NULL的统计。 + +> EXISTS关键词的使用方法 + +EXISTS表示是否存在,使用EXISTS时,如果内存查询语句查询到符合条件的值,就返回一个true,否则,将返回false。 + +例如: +```sql +SELECT * FROM user + where EXISTS + (SELECT name FROM employee WHERE id=100) +``` + +如果employee表中存在id为100的员工,内层查询就会返回一个true,外层查询接收到true后,开始查询user表中的数据,因为where没有设置其他查询条件,所以,将查询出user的全部数据。 + +> 内连接(inner join)和外连接的区别? + +- 内连接:只会查询出两表连接符合条件的记录。 + +```sql +SELECT * FROM user1 u1 INNER JOIN user2 u2 ON u1.id = u2.id; +``` + +如上sql所示,只会查询到user1和user2关联符合条件的记录。 + +外连接分为左外连接、右外连接和全外连接。 + +- 左外连接 + +它的原理是:以左表为基准,去右表匹配数据,找不到匹配的用NULL补齐。其显示左表的全部记录和右表符合连接条件的记录。 + +- 右外连接 + +它的原理是:以右表为基准,去左表匹配数据,找不到匹配的用NULL补齐。其显示右表的全部记录和左表符合连接条件的记录。这正好与左外连接相反。 + +- 全外连接 + +除了显示符合连接条件的记录之外,两个表的其他数据也会显示出来。 + +> inner join 和 left join性能谁优谁劣? + +![](http://image.ouyangsihai.cn/Ft-S3YZNbH0Il2x0K6wbxSUcZ2yE) + +如上图,是mysql的执行顺序,我们可以看出,外查询是在内查询的基础上,进而进行查询操作的。因此,我们从理论上可以得出,内连接的执行效率是更好一些的。 + +但是,外连接也是会进行优化操作的,在编译优化阶段,如果左连接的结果和内连接一样,左连接查询会转换成内连接查询,但这也表明编译优化器也认为内连接的效率是更高的。 + +虽然从查询的结果来看一般不会有太大的区别,但是,如果左右表之间的数据差别很大,内连接的效率是明显更高的,因为左连接以左表为基准,并且会进行回表操作。 + +最后,给出一个结论:在外连接和内连接都可以实现需求时,建议使用内连接进行操作。 + +> 存储过程是什么,优势是什么,为什么不建议使用? + +存储过程:为了完成特定功能的SQL语句集,存储在数据库中,一次编译后永久有效,用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。存储过程是数据库中的一个重要对象。在数据量特别庞大的情况下利用存储过程能达到倍速的效率提升。 + +**优势** +- 存储过程是预编译的,因此执行速度较快; +- 存储过程在服务端执行,减少客户端的压力; +- 减少网络流量,客户端只需要传递存储过程名称和参数即可进行调用,减少了传输的数据量; +- 一次编写,任意次执行,复用的思想简单便捷; +- 安全性较高,因为在服务端执行,管理员可以对存储过程进行权限限制,能够避免非法的访问,保证数据的安全性。 + +**缺点** + +- 调试麻烦,可移植性查。 + +这一个缺点也是存储过程在实际开发中用的不多的原因,现在开发的理念是简便。 + +> 数据库的三级范式 + +1NF:原子性,字段不可再分; +2NF:在满足第一范式的基础上,一个表只能说明一个事物,非主键属性必须玩去哪依赖于主键属性; +3NF:在满足第二范式的基础上,每列都与主键有直接关系,不存在传递依赖,任何非主键属性不依赖于其他非主键属性。 + +> sql语句应该考虑哪些安全性 + +- 防止sql注入,对sql语句尽量进行过滤同时使用预编译的sql语句绑定变量 +- 查询错误信息不要返回给用户,将错误记录到日志 +- 最小用户权限设置,最好不要使用root用户连接数据库 +- 定期做数据备份,避免数据丢失 + +> 什么叫sql注入,如何防止? + +SQL注入是一种将SQL代码添加到输入参数中,传递到服务器解析并执行的一种攻击手法。 + +**SQL注入攻击**是输入参数未经过滤,然后直接拼接到SQL语句当中解析,执行达到预想之外的一种行为,称之为SQL注入攻击。 + +常见的sql注入,有下列几种: +- 数字注入 + +例如,查询的sql语句为:`SELECT * FROM user WHERE id = 1`,正常是没有问题的,如果我们进行sql注入,写成`SELECT * FROM user WHERE id = 1 or 1=1`,那么,这个语句永远都是成立的,这就有了问题,也就是sql注入。 + +- 字符串注入 + +字符串注入是因为注释的原因,导致sql错误的被执行,例如字符`#`、`--`。 + +例如,`SELECT * FROM user WHERE username = 'sihai'#'ADN password = '123456'`,这个sql语句'#'后面都被注释掉了,相当于`SELECT * FROM user WHERE username = 'sihai' `。 + +这种情况我们在mybatis中也是会存在的,所以在服务端写sql时,需要特别注意此类情况。 + +该如何防范此类问题呢? +- 严格检查输入变量的类型和格式,也就是对相关传入的参数进行验证,尽可能降低风险。 +- 过滤和转义特殊字符。 +- 利用mysql的预编译机制,在Java中mybatis也是有预编译的方法的,所以可以采用这种方式避免。 + +> MySQL中InnoDB和MyISAM的区别? + +- 事务处理方面:MyISAM强调的是性能,查询的速度比InnoDB更快,但是,不支持事务,InnoDB是支持事务的。 +- 外键:InnoDB支持外键,MyISAM不支持。 +- 锁:InnoDB支持行锁和表锁,默认会使用行锁,而MyISAM只是支持表锁。由于行锁是能更好的支持并发操作,因此,InnoDB更加适合插入和更新操作较多的情况,而MyISAM适用于频繁查询操作。 +- 全文索引:MyISAM支持全文索引,InnoDB不支持。但是,在5.6版本开始InnoDB也开始支持全文索引。 +- 表主键:MyISAM允许没有主键的表存在,而InnoDB如果不存在主键,会自动生成一个6字节的主键。 +- 查询表的行数差异:InnoDB不保存表的函数信息,因此,select count(*)时会扫描整个表来进行计算;MyISAM内置了计数器,只需要简单的读取保存好的行数即可。 + + +### 事务相关 + +> 数据库事务的四个基本要素? + +事务是指一组SQL语句组成的逻辑处理单元。 + +ACID:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability) + + +- 原子性(Atomicity) + +原子性指的是整个数据库的事务是一个不可分割的工作单位,每一个都应该是一个原子操作。 + +当我们执行一个事务的时候,如果一系列的操作中,有一个操作失败了,那么,需要将这一个事务中的所有操作恢复到执行事务之前的状态,这就是事务的原子性。 + +下面举个简单的例子。 +```java +i++; +``` +上面这个最简单不过的代码经常也会被问到,这是一个原子操作吗?那肯定不是,如果我们把这个代码放到一个事务中来说,当i+1出现问题的时候,回滚的就是整个代码i++(i = i + 1)了,所以回滚之后,i的值也是不会改变的。 + +以上就是原子性的概念。 + +- 一致性(consistency) + +一致性是指事务将数据库从一种状态转变为下一种一致性的状态,也就是说在事务执行前后,这两种状态应该是一样的,也就是数据库的完整性约束不会被破坏。 + +另外,需要注意的是一致性是不关注中间状态的,比如银行转账的过程,你转账给别人,至于中间的状态,你少了500 ,他多了500,这些中间状态不关注,如果分多次转账中间状态也是不可见的,只有最后的成功或者失败的状态是可见的。 + +如果到分布式的一致性问题,又可以分为强一致性、弱一致性和最终一致性,关于这些概念,可以自己查查,还是很有意思的。 + +- 隔离性(isolation) + +事务我们是可以开启很多的,MySQL数据库中可以同时启动很多的事务,但是,事务和事务之间他们是相互分离的,也就是互不影响的,这就是事务的隔离性。 + +- 持久性(durability) + +事务的持久性是指事务一旦提交,就是永久的了,就是发生问题,数据库也是可以恢复的。因此,持久性保证事务的高可靠性。 + +> 并发事务会带来什么问题 + +#### 脏读 + +**脏读:** 在不同的事务下,当前事务可以读到另外事务未提交的数据。另外我们需要注意的是默认的MySQL隔离级别是`REPEATABLE READ`是不会发生脏读的,脏读发生的条件是需要事务的隔离级别为`READ UNCOMMITTED`,所以如果出现脏读,可能就是这种隔离级别导致的。 + +下面我们通过一个例子看一下。 +![](http://image.ouyangsihai.cn/Fh7WDJf8NpXn0QY0sA-vpVyGtGhs) + +从上面这个例子可以看出,当我们的事务的隔离级别为`READ UNCOMMITTED`的时候,在会话A还没有提交时,会话B就能够查询到会话A没有提交的数据。 + + +#### 不可重复读 + +**不可重复读:** 是指在一个事务内多次读取同一集合的数据,但是多次读到的数据是不一样的,这就违反了数据库事务的一致性的原则。但是,这跟脏读还是有区别的,脏读的数据是没有提交的,但是不可重复读的数据是已经提交的数据。 + +我们通过下面的例子来看一下这种问题的发生。 + +![](http://image.ouyangsihai.cn/FuqDKDktSrQTMPP8fAm0cmWdNsR8) + +从上面的例子可以看出,在A的一次会话中,由于会话B插入了数据,导致两次查询的结果不一致,所以就出现了不可重复读的问题。 + +我们需要注意的是不可重复读读取的数据是已经提交的数据,事务的隔离级别为`READ COMMITTED`,这种问题我们是可以接受的。 + +如果我们需要避免不可重复读的问题的发生,那么我们可以使用**Next-Key Lock算法**(设置事务的隔离级别为`READ REPEATABLE`)来避免,在MySQL中,不可重复读问题就是Phantom Problem,也就是**幻像问题**。 + +#### 幻读 + +幻读本质上也属于不可重复读的情况,会话 A 读取某个范围的数据,会话 B 在这个范围内**插入**新的数据,会话 A 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。 + +#### 丢失更新 + +**丢失更新:** 指的是一个事务的更新操作会被另外一个事务的更新操作所覆盖,从而导致数据的不一致。在当前数据库的任何隔离级别下都不会导致丢失更新问题,要出现这个问题,在多用户计算机系统环境下有可能出现这种问题。 + +如何避免丢失更新的问题呢,我们只需要让事务的操作变成串行化,不要并行执行就可以。 + +我们一般使用`SELECT ... FOR UPDATE`语句,给操作加上一个排他X锁。 + +> 数据库事务的隔离级别 + +数据库提供了四种隔离级别。 + +- 读未提交数据(READ UNCOMMITTED) + +允许事务读取未被其他事务提交的变更,可能有脏读、不可重复读和幻读的问题。 + +例如,某个时刻会话a修改了一个数据,但是未提交,此时,会话b读取了该数据,此时,a回滚了事务,这就会出现a、b数据不一致,这就是**脏读**。 + +- 读已提交数据(READ COMMITTED) + +允许事务读取已经被其他事务提交的变更,可能不可重复读和幻读的问题。 + +例如,某个时刻会话a修改了一个数据,提交了,此时的结果是10,此时b对该数据进行了修改为20,并提交了,此时会话a再次读取该数据,发现结果是20了,因此,在同一事务中,出现了两次读取的结果不一致的现象,这就是**不可重复读**。 + +- 可重复读(REPEATABLE READ,默认隔离级别) + +可重复读,从字面上的意思也能明白,就是在同一事务中读取多次,确保每次读取到的数据都是一样的,可以避免脏读和不可重复读,但是可能会出现幻读。 + +- 可串行化(SERIALIZABLE) + +可串行化是指所有的事务都是一个接一个执行,可以避免所有的问题,但是,效果太低。 + +最后,再用一张图来总结一下。 + +![](http://image.ouyangsihai.cn/Fh1arOgS78BnrjBXKwGw8NSP27uM) + +### MVCC 实现原理 + +在理解 MVCC 的实现原理之前,需要先带大家了解一下 **版本链**。 + +我们都知道,在 InnoDB 每个事务都有一个唯一的事务 ID(transaction id),该 ID 是在启动一个事务时申请的并且严格顺序递增。 + +另外,数据表中的每行数据都是有多个版本的,每次事务更新都会生成新的版本,并且把本次事务的 transaction id 赋值给这个数据版本的事务 ID(row trx_id)。 + +除此之外,还有一个 roll_pointer指针,该指针 ROLL_PTR 把一个数据行的所有快照版本记录连接起来。 + +undo log 的回滚机制也是依靠这个版本链,每次对记录进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer属性(INSERT操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些undo日志都连起来,串成一个链表,所以现在的情况就像下图一样: + +![](http://image.ouyangsihai.cn/Fidy__nsyaUj1N3MGfGlu1hjbYMM) + +有了上面的知识储备,所谓的 MVCC(Multi-Version Concurrency Control ,多版本并发控制)指的就是在使用**读已提交(READ COMMITTD)**、**可重复读(REPEATABLE READ)** 这两种隔离级别的事务在执行普通的 SELECT 操作时访问记录的版本链的过程,这样子可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。 + +这两个隔离级别的一个很大不同就是:生成 ReadView 的时机不同,READ COMMITTD 在每一次进行普通 SELECT 操作前都会生成一个 ReadView,而 REPEATABLE READ 只在第一次进行普通 SELECT 操作前生成一个ReadView,数据的可重复读其实就是 ReadView 的重复使用。 + +#### **ReadView** + +MVCC 维护了一个 ReadView 结构,主要包含了当前系统未提交的事务列表 TRX_IDs {TRX_ID_1, TRX_ID_2, ...},还有该列表的最小值 TRX_ID_MIN 和 TRX_ID_MAX。 + +在进行 SELECT 操作时,根据数据行快照的 TRX_ID 与 TRX_ID_MIN 和 TRX_ID_MAX 之间的关系,从而判断数据行快照是否可以使用: + +- TRX_ID < TRX_ID_MIN,表示该数据行快照时在当前所有未提交事务之前进行更改的,因此可以使用。 +- TRX_ID > TRX_ID_MAX,表示该数据行快照是在事务启动之后被更改的,因此不可使用。 +- TRX_ID_MIN <= TRX_ID <= TRX_ID_MAX,需要根据隔离级别再进行判断: + - 提交读:如果 TRX_ID 在 TRX_IDs 列表中,表示该数据行快照对应的事务还未提交,则该快照不可使用。否则表示已经提交,可以使用。 + - 可重复读:都不可以使用。因为如果可以使用的话,那么其它事务也可以读到这个数据行快照并进行修改,那么当前事务再去读这个数据行得到的值就会发生改变,也就是出现了不可重复读问题。 + +在数据行快照不可使用的情况下,需要沿着 Undo Log 的回滚指针 ROLL_PTR 找到下一个快照,再进行上面的判断。 +### 锁相关 + +> 数据库中锁机制,说说数据库中锁的类型 + +对于MySQL来说,锁是一个很重要的特性,数据库的锁是为了支持对共享资源进行并发访问,提供数据的完整性和一致性,这样才能保证在高并发的情况下,访问数据库的时候,数据不会出现问题。 + +在数据库中,lock和latch都可以称为锁,但是意义却不同。 + +**Latch**一般称为`闩锁`(轻量级的锁),因为其要求锁定的时间必须非常短。若持续的时间长,则应用的性能会非常差,在InnoDB引擎中,Latch又可以分为`mutex`(互斥量)和`rwlock`(读写锁)。其目的是用来保证并发线程操作临界资源的正确性,并且通常没有死锁检测的机制。 + +**Lock**的对象是`事务`,用来锁定的是数据库中的对象,如表、页、行。并且一般lock的对象仅在事务commit或rollback后进行释放(不同事务隔离级别释放的时间可能不同)。 + +InnoDB存储引擎中存在着不同类型的锁,下面一一介绍一下。 + +**S or X (共享锁、排他锁)** + +数据的操作其实只有两种,也就是读和写,而数据库在实现锁时,也会对这两种操作使用不同的锁;InnoDB 实现了标准的**行级锁**,也就是**共享锁(Shared Lock)和互斥锁(Exclusive Lock)**。 +- 共享锁(读锁)(S Lock),允许事务读一行数据。 +- 排他锁(写锁)(X Lock),允许事务删除或更新一行数据。 + +**IS or IX (共享、排他)意向锁** + +为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB存储引擎支持一种额外的锁方式,就称为**意向锁**,意向锁在 InnoDB 中是**表级锁**,意向锁分为: + +- 意向共享锁:表达一个事务想要获取一张表中某几行的共享锁。 +- 意向排他锁:表达一个事务想要获取一张表中某几行的排他锁。 + +在存在行级锁和表级锁的情况下,事务 T 想要对表 A 加 X 锁,就需要先检测是否有其它事务对表 A 或者表 A 中的任意一行加了锁,那么就需要对表 A 的每一行都检测一次,这是非常耗时的。 + +意向锁在原来的 X/S 锁之上引入了 IX/IS,IX/IS 都是表锁,用来表示一个事务想要在表中的某个数据行上加 X 锁或 S 锁。有以下两个规定: + +- 一个事务在获得某个数据行对象的 S 锁之前,必须先获得表的 IS 锁或者更强的锁; +- 一个事务在获得某个数据行对象的 X 锁之前,必须先获得表的 IX 锁。 + +通过引入意向锁,事务 T 想要对表 A 加 X 锁,只需要先检测是否有其它事务对表 A 加了 X/IX/S/IS 锁,如果加了就表示有其它事务正在使用这个表或者表中某一行的锁,因此事务 T 加 X 锁失败。 + +另外,这些锁之间的并不是一定可以共存的,有些锁之间是不兼容的,所谓**兼容性**就是指事务 A 获得一个某行某种锁之后,事务 B 同样的在这个行上尝试获取某种锁,如果能立即获取,则称锁兼容,反之叫冲突。 + +下面我们再看一下这两种锁的兼容性。 + +- S or X (共享锁、排他锁)的兼容性 + +![](https://img-blog.csdnimg.cn/20191022121422475.png) + + +- IS or IX (共享、排他)意向锁的兼容性 + +![](https://img-blog.csdnimg.cn/20191022121422644.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3NpaGFpMTIzNDU=,size_16,color_FFFFFF,t_70) + +**注意:** 任意 IS/IX 锁之间都是兼容的,因为它们只表示想要对表加锁,而不是真正加锁。 + +> MySQL中锁的粒度 + +在数据库中,锁的粒度的不同可以分为表锁、页锁、行锁,这些锁的粒度之间也是会发生升级的,**锁升级**的意思就是讲当前锁的粒度降低,数据库可以把一个表的1000个行锁升级为一个页锁,或者将页锁升级为表锁,下面分别介绍一下这三种锁的粒度(参考自博客:https://blog.csdn.net/baolingye/article/details/102506072)。 + +##### 表锁 + +表级别的锁定是MySQL各存储引擎中最大颗粒度的锁定机制。该锁定机制最大的特点是实现逻辑非常简单,带来的系统负面影响最小。所以获取锁和释放锁的速度很快。由于表级锁一次会将整个表锁定,所以可以很好的避免困扰我们的死锁问题。 + +当然,锁定颗粒度大所带来最大的负面影响就是出现锁定资源争用的概率也会最高,致使并大度大打折扣。 + +使用表级锁定的主要是MyISAM,MEMORY,CSV等一些非事务性存储引擎。 + +**特点:** 开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。 + +##### 页锁 + +页级锁定是MySQL中比较独特的一种锁定级别,在其他数据库管理软件中也并不是太常见。页级锁定的特点是锁定颗粒度介于行级锁定与表级锁之间,所以获取锁定所需要的资源开销,以及所能提供的并发处理能力也同样是介于上面二者之间。另外,页级锁定和行级锁定一样,会发生死锁。 +在数据库实现资源锁定的过程中,随着锁定资源颗粒度的减小,锁定相同数据量的数据所需要消耗的内存数量是越来越多的,实现算法也会越来越复杂。不过,随着锁定资源 颗粒度的减小,应用程序的访问请求遇到锁等待的可能性也会随之降低,系统整体并发度也随之提升。 +使用页级锁定的主要是BerkeleyDB存储引擎。 + +**特点:** 开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。 + +##### 行锁 + +行级锁定最大的特点就是锁定对象的粒度很小,也是目前各大数据库管理软件所实现的锁定颗粒度最小的。由于锁定颗粒度很小,所以发生锁定资源争用的概率也最小,能够给予应用程序尽可能大的并发处理能力而提高一些需要高并发应用系统的整体性能。 + +虽然能够在并发处理能力上面有较大的优势,但是行级锁定也因此带来了不少弊端。由于锁定资源的颗粒度很小,所以每次获取锁和释放锁需要做的事情也更多,带来的消耗自然也就更大了。此外,行级锁定也最容易发生死锁。 + +**特点:** 开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。 + +比较表锁我们可以发现,这两种锁的特点基本都是相反的,而从锁的角度来说,**表级锁**更适合于以查询为主,只有少量按索引条件更新数据的应用,如Web应用;而**行级锁**则更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用,如一些在线事务处理(OLTP)系统。 + +##### MySQL 不同引擎支持的锁的粒度 + +![](https://img-blog.csdnimg.cn/20191022121422292.png) + +> 了解一致性非锁定读和一致性锁定读吗? + +#### 一致性锁定读(Locking Reads) + +在一个事务中查询数据时,普通的SELECT语句不会对查询的数据进行加锁,其他事务仍可以对查询的数据执行更新和删除操作。因此,InnoDB提供了两种类型的锁定读来保证额外的安全性: + + - `SELECT ... LOCK IN SHARE MODE` + - `SELECT ... FOR UPDATE` + + `SELECT ... LOCK IN SHARE MODE`: 对读取的行添加S锁,其他事物可以对这些行添加S锁,若添加X锁,则会被阻塞。 + + `SELECT ... FOR UPDATE`: 会对查询的行及相关联的索引记录加X锁,其他事务请求的S锁或X锁都会被阻塞。 当事务提交或回滚后,通过这两个语句添加的锁都会被释放。 注意:只有在自动提交被禁用时,SELECT FOR UPDATE才可以锁定行,若开启自动提交,则匹配的行不会被锁定。 + + #### 一致性非锁定读 + + **一致性非锁定读(consistent nonlocking read)** 是指InnoDB存储引擎通过多版本控制(MVVC)读取当前数据库中行数据的方式。如果读取的行正在执行DELETE或UPDATE操作,这时读取操作不会因此去等待行上锁的释放。相反地,InnoDB会去读取行的一个快照。所以,非锁定读机制大大提高了数据库的并发性。 + + ![来自网络:侵权删](https://img-blog.csdnimg.cn/2019102212142395.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3NpaGFpMTIzNDU=,size_16,color_FFFFFF,t_70) + +一致性非锁定读是InnoDB默认的读取方式,即读取不会占用和等待行上的锁。在事务隔离级别`READ COMMITTED`和`REPEATABLE READ`下,InnoDB使用一致性非锁定读。 + +然而,对于快照数据的定义却不同。在`READ COMMITTED`事务隔离级别下,一致性非锁定读总是**读取被锁定行的最新一份快照数据**。而在`REPEATABLE READ`事务隔离级别下,则**读取事务开始时的行数据版本**。 + +下面我们通过一个简单的例子来说明一下这两种方式的区别。 + +首先创建一张表; + +![](https://img-blog.csdnimg.cn/20191022121423315.png) + +插入一条数据; + +``` +insert into lock_test values(1); +``` + +查看隔离级别; + +``` +select @@tx_isolation; +``` + +![](https://img-blog.csdnimg.cn/20191022121423531.png) + +下面分为两种事务进行操作。 + +在`REPEATABLE READ`事务隔离级别下; + +![](https://img-blog.csdnimg.cn/20191022121423748.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3NpaGFpMTIzNDU=,size_16,color_FFFFFF,t_70) + +在`REPEATABLE READ`事务隔离级别下,读取事务开始时的行数据,所以当会话B修改了数据之后,通过以前的查询,还是可以查询到数据的。 + +在`READ COMMITTED`事务隔离级别下; + +![](https://img-blog.csdnimg.cn/20191022121423939.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3NpaGFpMTIzNDU=,size_16,color_FFFFFF,t_70) + +在`READ COMMITTED`事务隔离级别下,读取该行版本最新的一个快照数据,所以,由于B会话修改了数据,并且提交了事务,所以,A读取不到数据了。 + +> InnoDB存储引擎行锁的算法了解吗? + +InnoDB存储引擎有3种行锁的算法,其分别是: + +- Record Lock:单个行记录上的锁。 +- Gap Lock:间隙锁,锁定一个范围,但不包含记录本身。 +- Next-Key Lock:Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身。 + +**Record Lock**:总是会去锁住索引记录,如果InnoDB存储引擎表在建立的时候没有设置任何一个索引,那么这时InnoDB存储引擎会使用隐式的主键来进行锁定。 + +**Next-Key Lock**:结合了Gap Lock和Record Lock的一种锁定算法,在Next-Key Lock算法下,InnoDB对于行的查询都是采用这种锁定算法。举个例子10,20,30,那么该索引可能被Next-Key Locking的区间为: +![](https://img-blog.csdnimg.cn/20191022121424137.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3NpaGFpMTIzNDU=,size_16,color_FFFFFF,t_70) + +除了Next-Key Locking,还有**Previous-Key Locking**技术,这种技术跟Next-Key Lock正好相反,锁定的区间是区间范围和前一个值。同样上述的值,使用Previous-Key Locking技术,那么可锁定的区间为: +![](https://img-blog.csdnimg.cn/20191022121424338.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3NpaGFpMTIzNDU=,size_16,color_FFFFFF,t_70) + +不是所有索引都会加上Next-key Lock的,这里有一种**特殊的情况**,在查询的列是唯一索引(包含主键索引)的情况下,`Next-key Lock`会降级为`Record Lock`。 + +接下来,我们来通过一个例子解释一下。 +```java +CREATE TABLE test ( + x INT, + y INT, + PRIMARY KEY(x), // x是主键索引 + KEY(y) // y是普通索引 +); +INSERT INTO test select 3, 2; +INSERT INTO test select 5, 3; +INSERT INTO test select 7, 6; +INSERT INTO test select 10, 8; +``` +我们现在会话A中执行如下语句; +```java +SELECT * FROM test WHERE y = 3 FOR UPDATE +``` + +我们分析一下这时候的加锁情况。 + +- 对于主键x + +![](https://img-blog.csdnimg.cn/20191022121424525.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3NpaGFpMTIzNDU=,size_16,color_FFFFFF,t_70) + + +- 辅助索引y + +![](https://img-blog.csdnimg.cn/20191022121424732.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3NpaGFpMTIzNDU=,size_16,color_FFFFFF,t_70) + + +用户可以通过以下两种方式来显示的关闭Gap Lock: + +- 将事务的隔离级别设为 READ COMMITED。 +- 将参数innodb_locks_unsafe_for_binlog设置为1。 + +**Gap Lock的作用**:是为了阻止多个事务将记录插入到同一个范围内,设计它的目的是用来解决**Phontom Problem(幻读问题)**。在MySQL默认的隔离级别(Repeatable Read)下,InnoDB就是使用它来解决幻读问题。 + +>**幻读**:是指在同一事务下,连续执行两次同样的SQL语句可能导致不同的结果,第二次的SQL可能会返回之前不存在的行,也就是第一次执行和第二次执行期间有其他事务往里插入了新的行。 + +> 说说悲观锁和乐观锁 + +##### 悲观锁 + +悲观锁是指在数据处理过程中,从一开始就使数据处于锁定状态,知道更改完成才释放。 + +MySQL中悲观锁使用以下方式:`select...for update` + +例如: +```sql +select name from item where id = 200 for update; +insert into orders(id,item_id) values(null,100) +update item set count = count - 1 where id = 100; +``` +我们使用`select name from item where id = 200 for update;`对id为200的数据进行了锁定,其他要对该条数据进行修改,必须等到该事务提交之后,否则无法修改。这样就保证了并发的安全性。 + +需要注意的是:`select...for update`语句必须在事务中使用。 + +悲观锁虽然能够解决并发安全的问题,但是,这种锁定会导致性能降低,加锁时间过长,并发性不好,影响系统的整体性能。因此,这种方式在实际的开发中用的很少。 + +##### 乐观锁 + +乐观锁是相对悲观锁而言的,认为数据一般情况下不会出现冲突,所以在数据进行更新的时候,才会将数据锁定。 + +**乐观锁的实现方式** + +- 使用数据版本(version)机制实现 + +该方式是在每次更新数据的时候,都要对更新的数据进行version版本+1操作。 + +其具体原理是:读取数据时,将此版本一同读出,之后,更新数据时,对此版本+1,每次提交数据时,如果提交的数据版本大于或等于数据库表中的版本,则可以更新,说明是最新数据,否则,不予更新,说明数据已经过期。 + +- 使用时间戳实现 + +该机制和version是类似的,也是需要再表中增加一个字段,类型使用时间类型即可。 + +原理:在更新数据时,检查数据库中当前的时间戳和更新前取到的时间戳,如果对比一致,就予以更新,否则不更新。 + +##### 使用场景分析 + +悲观锁可以在并发量不大的情况下使用,并发量大的情况下,使用乐观锁,大多数情况下都建议使用乐观锁。 + +### 索引相关 + +> 索引有哪些类型? + +索引有很多中类型:普通索引、唯一索引、主键索引、组合索引、全文索引,下面我们看看如何创建和删除下面这些类型的索引。 + +- 唯一索引:是在表上一个或者多个字段组合建立的索引,这些字段组合的值在表中不可重复。 +- 非唯一索引:是在表上一个或者多个字段组合建立的索引,这些字段组合的值在表中可重复。 +- 主键索引:是唯一索引的特定类型。表中创建主键时,会自动创建主键索引且只有一个。 +- 组合索引:基于多个字段而创建的索引。 + +下面再看看索引的创建和删除的方法。 + +#### 索引的创建方式 + +索引的创建是可以在很多种情况下进行的。 + +- 直接创建索引 + +``` +CREATE [UNIQUE|FULLLTEXT] INDEX index_name ON table_name(column_name(length)) +``` +`[UNIQUE|FULLLTEXT]`:表示可选择的索引类型,唯一索引还是全文索引,不加话就是普通索引。 +`table_name`:表的名称,表示为哪个表添加索引。 +`column_name(length)`:column_name是表的列名,length表示为这一列的前length行记录添加索引。 + +- 修改表结构的方式添加索引 + +``` +ALTER TABLE table_name ADD [UNIQUE|FULLLTEXT] INDEX index_name (column(length)) +``` + +- 创建表的时候同时创建索引 + +``` +CREATE TABLE `table` ( + `id` int(11) NOT NULL AUTO_INCREMENT , + `title` char(255) CHARACTER NOT NULL , + PRIMARY KEY (`id`), + [UNIQUE|FULLLTEXT] INDEX index_name (title(length)) +) +``` + +#### 主键索引和组合索引创建的方式 + +前面讲的都是**普通索引、唯一索引和全文索引**创建的方式,但是,**主键索引和组合索引**创建的方式却是有点不一样的,所以单独拿出来讲一下。 + +**组合索引创建方式** + +- 创建表的时候同时创建索引 + + +``` +CREATE TABLE `table` ( + `id` int(11) NOT NULL AUTO_INCREMENT , + `title` char(255) CHARACTER NOT NULL , + PRIMARY KEY (`id`), + INDEX index_name(id,title) +) +``` + +- 修改表结构的方式添加索引 + +``` +ALTER TABLE table_name ADD INDEX name_city_age (name,city,age); +``` + +**主键索引创建方式** +主键索引是一种特殊的唯一索引,一个表只能有一个主键,不允许有空值。一般是在建表的时候同时创建主键索引。 + +``` +CREATE TABLE `table` ( + `id` int(11) NOT NULL AUTO_INCREMENT , + `title` char(255) CHARACTER NOT NULL , + PRIMARY KEY (`id`) +) +``` + +#### 删除索引 + +删除索引可利用`ALTER TABLE`或`DROP INDEX`语句来删除索引。类似于`CREATE INDEX`语句,`DROP INDEX`可以在`ALTER TABLE`内部作为一条语句处理,语法如下。 + +(1)`DROP INDEX index_name ON talbe_name` +(2)`ALTER TABLE table_name DROP INDEX index_name` +(3)`ALTER TABLE table_name DROP PRIMARY KEY` + +第3条语句只在删除`PRIMARY KEY`索引时使用,因为一个表只可能有一个`PRIMARY KEY`索引,因此不需要指定索引名。 + +> 数据库索引的实现原理 + +在讲解本题之前,建议大家先了解一下B+树的原理,这对后面的讲解的理解有很大的帮助,大家可以阅读一些这篇文章:[面试官问你B树和B+树,就把这篇文章丢给他](https://www.java1000.com/mian-shi-guan-wen-ni-b-shu-he-b-shu-jiu-ba-zhe-pian-wen-zhang-diu-gei-ta.html) + +基于MySQL数据库的引擎不同,索引的实现原理也是不相同的,这里主要以最主流的InnoDB和MyISAM搜索引擎举例,来说明索引的实现原理。 + +##### MyISAM索引实现原理 + +首先,我们需要明白一点,MyISAM 引擎的整理结构是采用主键索引和辅助索引构成的。MyISAM 引擎使用 B+ 树作为索引结构,叶节点的 data 域存放的是数据记录的地址。如下图所示。 + +![](http://image.ouyangsihai.cn/FhIggfScI0BhtoxY4xOCh6wLTJD0) + +由上图可知,MyISAM 引擎的叶子节点存放的是**数据记录的地址**。 + +接下来,再来看一下辅助索引。 + +![](http://image.ouyangsihai.cn/Fk9XGH0vXWUgUOqniAqBpkm8ETAf) + +在 MyISAM 中,主索引和辅助索引(Secondary key)在结构上没有任何区别,只是**主索引要求 key 是唯一的,而辅助索引的 key 可以重复。** + +辅助索引也是一颗B+树,data域保存数据记录的地址。 + +**基于MyISAM引擎的索引检索算法:** 按照B+树算法搜索索引,假若指定搜索的key存在,则可以直接取出值,然后以data域的值为地址,使用地址获取对应的数据记录。 + +##### InnoDB索引实现原理 + +基于MyISAM引擎实现的索引原理与基于InnoDB实现的索引原理总是分不开的,可以说是一对欢喜冤家,而两者之间最大的区别就在于,**InnoDB的数据文件本身就是索引文件**,怎么理解这句话呢?其实是相对于MyISAM引擎实现的索引而言的。从上分析我们可以知道,**MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址**,而在InnoDB中,表数据文件本身就是按B+Tree组织的一个索引结构,这棵树的叶节点data域保存了完整的数据记录,而保存这个索引的key就是数据表的主键,因此InnoDB引擎表数据文件本身就是**主索引**。 + +总结一下,意思就是说InnoDB实现的索引叶子节点保存的是数据记录,而MyISAM引擎实现的索引的叶子节点保存的是数据记录的地址,还需要通过地址去索引对应的数据。 + +![](http://image.ouyangsihai.cn/Fp7rcVb2jKuNnpqcZY58zQYmWman) + +另外,由于基于InnoDB实现的索引的数据文件本身要按主键聚集,因此,基于InnoDB实现的索引是必须有主键存在的。 + +**其主键策略**:如果没有显式指定,则MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键;如果没有找到上面符合条件的列,则会生成6个字节的bigint unsigned值作为其主键。 + +**考点:尽量在InnoDB引擎上采用自增字段做表的主键** + +InnoDB引擎数据文件本身是一棵B+树,非自增的主键会造成在插入新记录时数据文件为了维持B+树的特性而频繁的分裂调整,十分低效,而使用自增字段作为主键则是一个很好的选择。如果不了解以上B+树的原理,建议阅读上面的B+树的文章。如果表使用自增主键,那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的页。 + +因此,采用在我们平时的开发当中,我们通常会采用自增主键,因为,MySQL的常用的版本中,默认的搜索引擎就是InnoDB,所以,采用自增主键,其实是可以保证比较好的效率的。 + + +**辅助索引** + +在上述MyISAM引擎索引的讲解中提到了辅助索引,MyISAM引擎中的主索引和辅助索引都是指向了同一数据记录的,而在InnoDB引擎中的表现却不一样。 + +**InnoDB的辅助索引data域存储相应记录主键索引的值而不是地址**。搜索辅助索引需要先根据辅助索引获取到主键值,再根据主键到主索引中获取到对应的数据记录。 +![](http://image.ouyangsihai.cn/FjeFUL7Iq4iIXtVOVZqBe4hJmE7G) + + +> 谈谈聚簇索引和非聚簇索引 + +其实,在上面对索引实现原理的分析当中,已经对这两个概念有了很好的讲解了,只是没有明显的指出而已。 + +InnoDB引擎采用的是**聚簇索引**,而MyISAM引擎采用的是**非聚簇索引**。这两个概念的区别就在于**叶节点是否存放一整行记录**,我们都知道,InnoDB引擎叶子节点存放的是数据记录,而MyISAM引擎的叶子节点存放的是数据记录的地址,所以说,只要理解了基于InnoDB引擎和基于MyISAM引擎实现的索引原理,就理解了以上这两个概念。这么说是不是就很容易理解的,不就是对应了两种不同的引擎吗,是不是很简单。 + +如果还不是很理解,再放一张图。 + +![](http://image.ouyangsihai.cn/Ft6BKzPJQ-Le30BjRM44ICGTjAtU) + +左边的是聚集索引(聚簇索引),右边的是非聚集索引(非聚簇索引),这两个是不是就是**基于InnoDB引擎和基于MyISAM引擎实现的索引原理**。 + +**聚簇索引的优势** + +- 当你需要取出一定范围内的数据时,用聚簇索引也比用非聚簇索引好。 +- 当通过聚簇索引查找目标数据时理论上比非聚簇索引要快,原因在于非聚簇索引叶子节点存放的是数据记录的地址,索引定位到对应主键时还要多一次目标记录寻址,即多一次I/O。 +- 采用覆盖索引扫描的查询可以直接使用页节点中的主键值。 + +**聚簇索引的不足** + +- 插入速度严重依赖于插入顺序,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能。 +- 更新主键的代价很高,因为将会导致被更新的行移动。因此,对于InnoDB表,我们一般定义主键为不可更新。 +- 二级索引访问需要两次索引查找,第一次找到主键值,第二次根据主键值找到行数据。二级索引的叶节点存储的是主键值,而不是行指针(非聚簇索引存储的是指针或者说是地址),这是为了减少当出现行移动或数据页分裂时二级索引的维护工作,但会让二级索引占用更多的空间。 +- 采用聚簇索引插入新值比采用非聚簇索引插入新值的速度要慢很多,因为插入要保证主键不能重复,判断主键不能重复,采用的方式在不同的索引下面会有很大的性能差距,聚簇索引遍历所有的叶子节点,非聚簇索引也判断所有的叶子节点,但是聚簇索引的叶子节点除了带有主键还有记录值,记录的大小往往比主键要大的多。这样就会导致聚簇索引在判定新记录携带的主键是否重复时进行昂贵的I/O代价。 + +最后,说明一下,如果你很好的理解了索引的原理,这上面的就会很好理解,如果理解不到位,就会发现这都是什么东西?因此,看这个的时候,先看一下上面那一题的解答。 + +> 覆盖索引 + +覆盖索引(covering index)指一个查询语句的执行只用从索引中就能够取得,不必从数据表中读取。也可以称之为实现了索引覆盖。 + +举个例子 + +```sql +select * from user where name = "sihai"; +``` +以上语句查询是会从数据表中进行查询的,因为,没有对user表中的name字段增加索引的操作。 + +```sql +alter table user add index name_index(name); +``` +我们对user表中的name字段添加了索引。我们再使用sql语句`select * from user where name = "sihai";`进行查询是,就会使用覆盖索引。 + +因此,我们就可以非常清楚的明白,覆盖索引就是如果发现可以走索引的方式得到数据,就不采用回表查询的操作了,从而提高了查询的效率。 + +另外,从上面的索引原理的介绍也可以得到另外一个结论:使用覆盖索引InnoDB引擎比MyISAM引擎效果更佳,原因在于InnoDB采用聚集索引,如果二级索引中包含查询所需的数据,就不再需要在聚集索引中查找了。 + +最后一点,想要使用覆盖索引,就必须要使得查询能够用到索引,因此,也需要注意索引失效的场景。 + +> 建立索引的原则 + +- 最左前缀匹配原则,MySQL会遇到范围查询停止匹配,所以会导致组合索引失效。 +- 索引列不进行函数运算。 +- 注意一个表建立索引的数量,不是索引建的越多越好,维护索引也会有很大的开销。 +- 尽量选择区分度高的字段作为索引。 +- where语句中,经常使用的字段应该考虑建立索引。 +- 分组和排序语句中,经常使用的字段应该考虑建立索引。 +- 两个表关联字段考虑建立索引。 +- like模糊查询中,只有右模糊查询才会使用索引。 +- 在varchar 字段上建立索引时,必须指定索引长度。 +- 禁止建立超过3个字段的联合索引。 +- 尽量采用覆盖索引,避免回表查询。 +- 索引优化的目标:至少要达到range级别,要求是ref级别,如果可以是consts最好。 + +> 索引失效的情况 + +- 使用组合索引,没有满足最左匹配原则,导致失效。 +- or语句所有字段必须都有索引,否则失效。 +- like以%开头,索引失效。 +- 需要类型转换。 +- where中索引列有运算。 +- where中索引列使用了函数。 +- 如果mysql觉得全表扫描更快时(数据少)。 + +### 其他问题 + +> 一张有自增id的表,当数据记录到了20之后,删除了第18,19,20条记录,再把MySQL重启,再插入一条记录,这条记录的id是21还是18呢? + +当表的引擎采用MyISAM时,是21,当表的引擎采用InnoDB时是18。 + +MyISAM引擎会把表自增主键的最大id记录到数据文件中,做了持久化,重启后也不会消失,而InnoDB是将自增主键的最大id记录在内存中,重启后,会丢失。 + +> 关系型数据库和非关系型数据库的区别 + +非关系型数据库的优势在于性能和可扩展性,非关系型数据库一般都是基于键值对的,当然也支持文档形式、图片形式等等,文档形式、图片形式等等,使用灵活,应用场景广泛,同时,也是基于内存进行相关的操作,同时,底层也有较好的数据结构的支持,保证了其操作的效率,性能较高;另外,同样也是因为基于键值对,数据之间没有耦合性,所以非常容易水平扩展。成本低:非关系型数据库部署简单,基本都是开源软件。 + + +关系型数据库的优势在于支持更加复杂的sql操作、事务支持和使用表结构更加易于维护。 + +> binlog、redo log和undo log + +redo log(重做日志)是InnoDB存储引擎独有的,它让MySQL拥有了崩溃恢复能力。 + +比如 MySQL 实例挂了或宕机了,重启时,InnoDB存储引擎会使用redo log恢复数据,保证数据的**持久性与完整性**。 + +redo log 记录的是数据的物理变化。 + + +binlog 记录了数据库表结构和表数据变更,比如update/delete/insert/truncate/create。它不会记录select(因为这没有对表没有进行变更),存储着每条变更的SQL语句(当然从下面的图看来看,不止SQL,还有XID「事务Id」等等)。 + +主要有两个作用:**复制和恢复数据** + +- MySQL在公司使用的时候往往都是一主多从结构的,从服务器需要与主服务器的数据保持一致,这就是通过binlog来实现的 + +- 数据库的数据被干掉了,我们可以通过binlog来对数据进行恢复。 + +undo log主要有两个作用:**回滚和多版本控制(MVCC)** + +在数据修改的时候,不仅记录了redo log,还记录undo log,如果因为某些原因导致事务失败或回滚了,可以用undo log进行回滚 + +undo log 主要存储的也是**逻辑日志**,比如我们要insert一条数据了,那undo log会记录的一条对应的delete日志。我们要update一条记录时,它会记录一条对应相反的update记录。 + +这也应该容易理解,毕竟回滚嘛,跟需要修改的操作相反就好,这样就能达到回滚的目的。因为支持回滚操作,所以我们就能保证**原子性**。 + +> mysql 优化思路 + +https://mp.weixin.qq.com/s/jtuLb8uAIHJNvNpwcIZfpA + +https://www.cnblogs.com/jay-huaxiao/p/12995510.html + +> mysql 语法和复杂语句练习题 + +- 常用语法 + +https://www.jb51.net/article/156898.htm +www.cyc2018.xyz/算法/基础/算法 - 排序.html + +- 练习题 + +https://www.jianshu.com/p/476b52ee4f1b +www.cyc2018.xyz/算法/基础/算法 - 排序.html diff --git "a/docs/database/\346\225\260\346\215\256\345\272\223\344\274\230\345\214\226.md" "b/docs/database/\346\225\260\346\215\256\345\272\223\344\274\230\345\214\226.md" new file mode 100644 index 0000000..32cea00 --- /dev/null +++ "b/docs/database/\346\225\260\346\215\256\345\272\223\344\274\230\345\214\226.md" @@ -0,0 +1,12 @@ +## 数据库优化 + +### 为什么要做优化 + + +### 从哪几个方面考虑优化 + + +### MySQL语句优化 + + +### 其他 \ No newline at end of file diff --git "a/docs/golang/\351\235\242\350\257\225\351\242\230/golang\351\235\242\350\257\225\351\242\230\346\225\264\347\220\206-\345\276\256\344\277\241\345\256\214\346\225\264.md" "b/docs/golang/\351\235\242\350\257\225\351\242\230/golang\351\235\242\350\257\225\351\242\230\346\225\264\347\220\206-\345\276\256\344\277\241\345\256\214\346\225\264.md" new file mode 100644 index 0000000..e65120c --- /dev/null +++ "b/docs/golang/\351\235\242\350\257\225\351\242\230/golang\351\235\242\350\257\225\351\242\230\346\225\264\347\220\206-\345\276\256\344\277\241\345\256\214\346\225\264.md" @@ -0,0 +1,836 @@ +> ❝ +> +> 原文: https://zhuanlan.zhihu.com/p/519979757,作者:沪猿小韩。为适于阅读,部分样式及内容有做简单修改。 +> +> ❞ + +# 前言 + +文章部分题目来源于网络,答案系个人结合5月份面试了近30家公司整理所得,最后附录参考原文链接,如有遗漏的原文出处请联系本人。不对之处望批评指正,答案需要加上自己的思考,最好是代码实践下。 + +参与过面试的企业有:zg人寿,睿科lun,qi猫,yun汉商城,zi节跳动,特斯la,虾P,chuan音,qi安信,ai立信等大大小小企业近30家,BAT简历都过不了。 + +## 面试建议 + +### 技术部分 + +1)算法部分,刷LeetCode就完事了,这是一个长期的过程,短期突击没啥效果,因为题目太多了。 + +2)语言基础,细分为: + +* golang基础及原理,就是本文主要内容了; +* mysql基础及原理; +* redis基础及原理; +* kafka或其他消息中间件(如果没用过,需要了解大概的底层原理及结构); +* linux常用的命令,比如定时脚本几个参数时间分别代表啥,文件权限需要搞清楚,进程内存占用命令;小公司还要懂一些前端的知识,因为他们希望你什么都会。 + +3)项目经验,可以搞一个基于gin的后端接口服务的web框架,一般会问你怎么实现的;以及微服务了解一下。 + +### 非技术部分 + +1)因为上海5月份居家办公,远程面试,这些题目准备一份,遇到卡壳的题目完全可以参考你准备好的答案,因为视频面试你眼睛是看着面试官还是题目是不太容易区分的(把题目窗口置顶)。 + +2)HR面也可以完全准备一份可能问到的问题的答案,并不是说你不会回答,而是会让你的表达更顺畅,其次也说明你是有备而来的,我在某拉公司面试就吃了这个亏,技术通过,HR说我的表达能力不行(后续我也会把这个模板分享出来,感谢我媳妇充当面试官,以及指导如何高情商的回答HR的问题)。 + +3)可以自己录音面试回答,看看自己的语气、音量,顺畅度,如果自己听了都不舒服,面试官可能也不舒服。 + +## 一、基础部分 + +#### 1、golang 中 make 和 new 的区别?(基本必问) + +共同点:给变量分配内存 + +不同点: + +* 作用变量类型不同,new给string,int和数组分配内存,make给切片,map,channel分配内存; + +* 返回类型不一样,new返回指向变量的指针,make返回变量本身; + +* new 分配的空间被清零。make 分配空间后,会进行初始化; + +* 字节的面试官还说了另外一个区别,就是分配的位置,在堆上还是在栈上?这块我比较模糊,大家可以自己探究下,我搜索出来的答案是golang会弱化分配的位置的概念,因为编译的时候会自动内存逃逸处理,懂的大佬帮忙补充下:make、new内存分配是在堆上还是在栈上? + +#### string 底层数据结构 + +```go +// from: string.go 在GoLand IDE中双击shift快速找到 +type stringStruct struct { + array unsafe.Pointer // 指向一个 [len]byte 的数组 + length int // 长度 +} +``` + +#### []string 和 []byte 的区别 + +string + +1 .是一个指针,指向某个数组的首地址 +[]byte + +1 .是一个切片slice。一个封装了数组的结构体 +2 .slice结构 +```go +type slice struct { + array unsafe.Pointer + len int + cap int +``` +使用场景 + +1 .想要在本身原地修改,就只能使用[]byte +2 .string不能为nil,想要返回nil表达特殊含义,只能使用[]byte +3 .string可以直接比较,而[]byte不可以,所以[]byte不可以当map的key值。 +4 .因为无法修改string中的某个字符,需要粒度小到操作一个字符时,用[]byte +5 .[]byte切片这么灵活,想要用切片的特性就用[]byte +6 .需要大量字符串处理的时候用[]byte,性能好很多 +区别 + +1 .string的指针指向的内容是不可以改变的,所以每次更改一次字符串,都需要重新分配内存。之前的内存还需要GC收回,这是导致string效率底下的根本原因 +2 .如果我们保存的字符在 ASCII 表的,比如[0-1, a-z,A-Z..]直接可以保存到 byte +3 .如果我们保存的字符对应码值大于 255,这时我们可以考虑使用 int 类型保存 + +#### 2、数组和切片的区别 (基本必问) + +相同点: + +* 只能存储一组相同类型的数据结构 + +* 都是通过下标来访问,并且有容量长度,长度通过 len 获取,容量通过 cap 获取 + +区别: + +* 数组是定长,访问和复制不能超过数组定义的长度,否则就会下标越界,切片长度和容量可以自动扩容 + +* 数组是值类型,切片是引用类型,每个切片都引用了一个底层数组,切片本身不能存储任何数据,都是这底层数组存储数据,所以修改切片的时候修改的是底层数组中的数据。切片一旦扩容,指向一个新的底层数组,内存地址也就随之改变 + +简洁的回答: + +1)定义方式不一样 2)初始化方式不一样,数组需要指定大小,大小不改变 3)函数的传递方式不一样,数组传递的是值,切片传的是地址。 + +数组的定义 + +```go +var a1 [3]int + +var a2 [...]int{1,2,3} +``` + +切片的定义 + +```go +var a1 []int + +var a2 :=make([]int,3,5) +``` + +数组的初始化 + +```go +a1 := [...]int{1,2,3} + +a2 := [5]int{1,2,3} +``` + +切片的初始化 + +```go +b:= make([]int,3,5) +``` +#### 3、for range 的时候它的地址会发生变化么? + +答:在 `for a,b := range c` 遍历中, a 和 b 在内存中只会存在一份,即之后每次循环时遍历到的数据都是以值覆盖的方式赋给 a 和 b,a,b 的内存地址始终不变。由于有这个特性,**「for 循环里面如果开协程,不要直接把 a 或者 b 的地址传给协程」**。解决办法:在每次循环时,创建一个临时变量。 + +#### 4、go defer,多个 defer 的顺序,defer 在什么时机会修改返回值? + +作用:defer延迟函数,释放资源,收尾工作;如释放锁,关闭文件,关闭链接;捕获panic; + +避坑指南:defer函数紧跟在资源打开后面,否则defer可能得不到执行,导致内存泄露。 + +多个 defer 调用顺序是 LIFO(后入先出),defer后的操作可以理解为压入栈中 + +解析:函数的 return 语句并不是原子级的,实际上 return 语句只代理汇编指令 ret。defer 语句是在返回前执行,所以返回过程是:「设置返回值—>执行defer—>ret」。defer可以修改函数最终返回值,修改时机:有名返回值或者函数返回指针 参考:[Go高阶指南07,一文搞懂 defer 实现原理)](https://mp.weixin.qq.com/s?__biz=Mzk0NzI3Mjk1Mg==&mid=2247484683&idx=1&sn=262b205caeef06489e5ac8cf84d44112&chksm=c378289cf40fa18aef1d26e1437232b6b20a2f1f7129951d3b094eefa04007ee3fcd0af4655f&token=1319045915&lang=zh_CN&scene=21#wechat_redirect) + +有名返回值 + +```go +func b() (i int) { + defer func() { + i++ + fmt.Println("defer2:", i) + }() + defer func() { + i++ + fmt.Println("defer1:", i) + }() + return i //或者直接写成return +} +func main() { + fmt.Println("return:", b()) +} + +//运行结果: +//defer1: 1 +//defer2: 2 +//return: 2 +``` + +函数返回指针 + +```go +func c() *int { + var i int + defer func() { + i++ + fmt.Println("defer2:", i) + }() + defer func() { + i++ + fmt.Println("defer1:", i) + }() + return &i +} +func main() { + fmt.Println("return:", *(c())) +} + +//运行结果: +//defer1: 1 +//defer2: 2 +//return: 2 +``` +#### 5、uint 类型溢出问题 + +超过最大存储值如uint8最大是255 + +var a uint8 =255 + +var b uint8 =1 + +a+b = 0总之类型溢出会出现难以意料的事 + +![图片](data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==) +#### 6、能介绍下 rune 类型吗? + +相当int32 + +golang中的字符串底层实现是通过byte数组的,中文字符在unicode下占2个字节,在utf-8编码下占3个字节,而golang默认编码正好是utf-8 + +byte 等同于int8,常用来处理ascii字符 + +rune 等同于int32,常用来处理unicode或utf-8字符 + +![图片](https://mmbiz.qpic.cn/mmbiz_png/3wgqfEribn6fXH7I19WrA9zDKjjmfnh6uvufYzu884pVVTs93HTicKncwIbXWzxftNmicFZDkkP6bqdojwN5LicfXw/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1) +#### 7、 golang 中解析 tag 是怎么实现的?反射原理是什么?(中高级肯定会问,比较难,需要自己多去总结) + +参考如下连接golang中struct关于反射tag (https://blog.csdn.net/paladinosment/article/details/42570937) + +```go +type User struct { + name string `json:name-field` + age int +} +func main() { + user := &User{"John Doe The Fourth", 20} + + field, ok := reflect.TypeOf(user).Elem().FieldByName("name") + if !ok { + panic("Field not found") + } + fmt.Println(getStructTag(field)) +} + +func getStructTag(f reflect.StructField) string { + return string(f.Tag) +} +``` + +o 中解析的 tag 是通过反射实现的,反射是指计算机程序在运行时(Run time)可以访问、检测和修改它本身状态或行为的一种能力或动态知道给定数据对象的类型和结构,并有机会修改它。反射将接口变量转换成反射对象 Type 和 Value;反射可以通过反射对象 Value 还原成原先的接口变量;反射可以用来修改一个变量的值,前提是这个值可以被修改;tag是啥:结构体支持标记,name string `json:name-field` 就是 `json:name-field` 这部分 + +gorm json yaml gRPC protobuf gin.Bind()都是通过反射来实现的 + +#### 8、调用函数传入结构体时,应该传值还是指针?(Golang 都是传值) + +Go 的函数参数传递都是值传递。所谓值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。参数传递还有引用传递,所谓引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数 + +因为 Go 里面的 map,slice,chan 是引用类型。变量区分值类型和引用类型。 +所谓值类型:变量和变量的值存在同一个位置。 +所谓引用类型:变量和变量的值是不同的位置,变量的值存储的是对值的引用。 +但并不是 map,slice,chan 的所有的变量在函数内都能被修改,不同数据类型的底层存储结构和实现可能不太一样,情况也就不一样。 + +#### 9、讲讲 Go 的 slice 底层数据结构和一些特性? + +答:Go 的 slice 底层数据结构是由一个 array 指针指向底层数组,len 表示切片长度,cap 表示切片容量。slice 的主要实现是扩容。对于 append 向 slice 添加元素时,假如 slice 容量够用,则追加新元素进去,slice.len++,返回原来的 slice。当原容量不够,则 slice 先扩容,扩容之后 slice 得到新的 slice,将元素追加进新的 slice,slice.len++,返回新的 slice。对于切片的扩容规则:当切片比较小时(容量小于 1024),则采用较大的扩容倍速进行扩容(新的扩容会是原来的 2 倍),避免频繁扩容,从而减少内存分配的次数和数据拷贝的代价。当切片较大的时(原来的 slice 的容量大于或者等于 1024),采用较小的扩容倍速(新的扩容将扩大大于或者等于原来 1.25 倍),主要避免空间浪费,网上其实很多总结的是 1.25 倍,那是在不考虑内存对齐的情况下,实际上还要考虑内存对齐,扩容是大于或者等于 1.25 倍。 + +(关于刚才问的 slice 为什么传到函数内可能被修改,如果 slice 在函数内没有出现扩容,函数外和函数内 slice 变量指向是同一个数组,则函数内复制的 slice 变量值出现更改,函数外这个 slice 变量值也会被修改。如果 slice 在函数内出现扩容,则函数内变量的值会新生成一个数组(也就是新的 slice,而函数外的 slice 指向的还是原来的 slice,则函数内的修改不会影响函数外的 slice。) + +#### 10、讲讲 Go 的 select 底层数据结构和一些特性?(难点,没有项目经常可能说不清,面试一般会问你项目中怎么使用select) + +答:go 的 select 为 golang 提供了多路 IO 复用机制,和其他 IO 复用一样,用于检测是否有读写事件是否 ready。linux 的系统 IO 模型有 select,poll,epoll,go 的 select 和 linux 系统 select 非常相似。 + +select 结构组成主要是由 case 语句和执行的函数组成 select 实现的多路复用是:每个线程或者进程都先到注册和接受的 channel(装置)注册,然后阻塞,然后只有一个线程在运输,当注册的线程和进程准备好数据后,装置会根据注册的信息得到相应的数据。 + +select 的特性 + +* select 操作至少要有一个 case 语句,出现读写 nil 的 channel 该分支会忽略,在 nil 的 channel 上操作则会报错。 +* select 仅支持管道,而且是单协程操作。 +* 每个 case 语句仅能处理一个管道,要么读要么写。 +* 多个 case 语句的执行顺序是随机的。 +* 存在 default 语句,select 将不会阻塞,但是存在 default 会影响性能。 + +- select 和 channel 的使用场景:https://blog.csdn.net/u011240877/article/details/123611525 + +#### 11、讲讲 Go 的 defer 底层数据结构和一些特性? + +答:每个 defer 语句都对应一个_defer 实例,多个实例使用指针连接起来形成一个单连表,保存在 gotoutine 数据结构中,每次插入_defer 实例,均插入到链表的头部,函数结束再一次从头部取出,从而形成后进先出的效果。 + +defer 的规则总结: + +* 延迟函数的参数是 defer 语句出现的时候就已经确定了的。 +* 延迟函数执行按照后进先出的顺序执行,即先出现的 defer 最后执行。 +* 延迟函数可能操作主函数的返回值。 +* 申请资源后立即使用 defer 关闭资源是个好习惯。 + +#### 12、单引号,双引号,反引号的区别? + +单引号,表示byte类型或rune类型,对应 uint8和int32类型,默认是 rune 类型。byte用来强调数据是raw data,而不是数字;而rune用来表示Unicode的code point。 + +双引号,才是字符串,实际上是字符数组。可以用索引号访问某字节,也可以用len()函数来获取字符串所占的字节长度。 + +反引号,表示字符串字面量,但不支持任何转义序列。字面量 raw literal string 的意思是,你定义时写的啥样,它就啥样,你有换行,它就换行。你写转义字符,它也就展示转义字符。 + +# 二、map相关 + +#### 1、map 使用注意的点,是否并发安全? + +map的类型是map[key],key类型的ke必须是可比较的,通常情况,会选择内建的基本类型,比如整数、字符串做key的类型。如果要使用struct作为key,要保证struct对象在逻辑上是不可变的。在Go语言中,map[key]函数返回结果可以是一个值,也可以是两个值。map是无序的,如果我们想要保证遍历map时元素有序,可以使用辅助的数据结构,例如orderedmap。 + +第一,一定要先初始化,否则panic + +第二,map类型是容易发生并发访问问题的。不注意就容易发生程序运行时并发读写导致的panic。Go语言内建的map对象不是线程安全的,并发读写的时候运行时会有检查,遇到并发问题就会导致panic。 + +#### 2、map 循环是有序的还是无序的? + +无序的, map 因扩张⽽重新哈希时,各键值项存储位置都可能会发生改变,顺序自然也没法保证了,所以官方避免大家依赖顺序,直接打乱处理。就是 for range map 在开始处理循环逻辑的时候,就做了随机播种 + +#### 3、 map 中删除一个 key,它的内存会释放么?(常问) + +如果删除的元素是值类型,如int,float,bool,string以及数组和struct,map的内存不会自动释放 + +如果删除的元素是引用类型,如指针,slice,map,chan等,map的内存会自动释放,但释放的内存是子元素应用类型的内存占用 + +将map设置为nil后,内存被回收。 + +这个问题还需要大家去搜索下答案,我记得有不一样的说法,谨慎采用本题答案。 + +#### 4、怎么处理对 map 进行并发访问?有没有其他方案?区别是什么? + +方式一、使用内置sync.Map,详细参考#### golang sync.map 原理和使用 #### (https://yebd1h.smartapps.cn/pages/blog/index?_swebFromHost=baiduboxapp&blogId=114628932&_swebfr=1) + +方式二、使用读写锁实现并发安全mapgolang线程安全map (https://yebd1h.smartapps.cn/pages/blog/index?_swebFromHost=baiduboxapp&blogId=123259483&_swebfr=1) + +#### 5、 nil map 和空 map 有何不同? + +1. 可以对未初始化的map进行取值,但取出来的东西是空: + +```go +var m1 map[string]string + +fmt.Println(m1["1"]) +``` + +1. 不能对未初始化的map进行赋值,这样将会抛出一个异常: + +```go +var m1 map[string]string + +m1["1"] = "1" + +panic: assignment to entry in nil map +``` + +1. 通过fmt打印map时,空map和nil map结果是一样的,都为map[]。所以,这个时候别断定map是空还是nil,而应该通过map == nil来判断。 + nil map 未初始化,空map是长度为空 + +#### 6、map 的数据结构是什么?是怎么实现扩容? + +答:golang 中 map 是一个 kv 对集合。底层使用 hash table,用链表来解决冲突 ,出现冲突时,不是每一个 key 都申请一个结构通过链表串起来,而是以 bmap 为最小粒度挂载,一个 bmap 可以放 8 个 kv。在哈希函数的选择上,会在程序启动时,检测 cpu 是否支持 aes,如果支持,则使用 aes hash,否则使用 memhash。每个 map 的底层结构是 hmap,是有若干个结构为 bmap 的 bucket 组成的数组。每个 bucket 底层都采用链表结构。 + +hmap 的结构如下: + +```go +type hmap struct { + count int // 元素个数 + flags uint8 + B uint8 // 扩容常量相关字段B是buckets数组的长度的对数 2^B + noverflow uint16 // 溢出的bucket个数 + hash0 uint32 // hash seed + buckets unsafe.Pointer // buckets 数组指针 + oldbuckets unsafe.Pointer // 结构扩容的时候用于赋值的buckets数组 + nevacuate uintptr // 搬迁进度 + extra *mapextra // 用于扩容的指针 +} +``` + +map 的容量大小 + +底层调用 makemap 函数,计算得到合适的 B,map 容量最多可容纳 6.52^B 个元素,6.5 为装载因子阈值常量。装载因子的计算公式是:装载因子=填入表中的元素个数/散列表的长度,装载因子越大,说明空闲位置越少,冲突越多,散列表的性能会下降。底层调用 makemap 函数,计算得到合适的 B,map 容量最多可容纳 6.52^B 个元素,6.5 为装载因子阈值常量。装载因子的计算公式是:装载因子=填入表中的元素个数/散列表的长度,装载因子越大,说明空闲位置越少,冲突越多,散列表的性能会下降。 + +触发 map 扩容的条件 + +* 装载因子超过阈值,源码里定义的阈值是 6.5。 +* overflow 的 bucket 数量过多 map 的 bucket 定位和 key 的定位高八位用于定位 bucket,低八位用于定位 key,快速试错后再进行完整对比。 + +#### 7、slices能作为map类型的key吗? + +当时被问得一脸懵逼,其实是这个问题的变种:golang 哪些类型可以作为map key? + +答案是:在golang规范中,可比较的类型都可以作为map key;这个问题又延伸到在:golang规范中,哪些数据类型可以比较? + +不能作为map key 的类型包括: + +slices maps functions + +# 三、context相关 + +#### 1、context 结构是什么样的?context 使用场景和用途? + +(难,也常常问你项目中怎么用,光靠记答案很难让面试官满意,反正有各种结合实际的问题) + +参考链接:go context详解 (https://www.cnblogs.com/juanmaofeifei/p/14439957.html) + +答:Go 的 Context 的数据结构包含 Deadline,Done,Err,Value,Deadline 方法返回一个 time.Time,表示当前 Context 应该结束的时间,ok 则表示有结束时间,Done 方法当 Context 被取消或者超时时候返回的一个 close 的 channel,告诉给 context 相关的函数要停止当前工作然后返回了,Err 表示 context 被取消的原因,Value 方法表示 context 实现共享数据存储的地方,是协程安全的。context 在业务中是经常被使用的, + +其主要的应用 : + +1:上下文控制,2:多个 goroutine 之间的数据交互等,3:超时控制:到某个时间点超时,过多久超时。 + +# 四、channel相关 + +#### 1、channel 是否线程安全?锁用在什么地方? + +![图片](data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==) + +#### 2、go channel 的底层实现原理 (数据结构) + +![图片](https://mmbiz.qpic.cn/mmbiz_png/3wgqfEribn6fXH7I19WrA9zDKjjmfnh6uAhBkuiacRMG4zqoayxUrPtUZQHoNxic9GOvXljTCre35EUTwzrp88LaQ/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1) + +底层结构需要描述出来,这个简单,buf,发送队列,接收队列,lock。 + +#### 3、nil、关闭的 channel、有数据的 channel,再进行读、写、关闭会怎么样?(各类变种题型,重要) + +![图片](https://mmbiz.qpic.cn/mmbiz_png/3wgqfEribn6fXH7I19WrA9zDKjjmfnh6ucsjGfTGOQjmefjjU9H1cVmvqKCNniaszG72rlBlJsl5yKic2kDG0WVcA/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1) + +还要去了解一下单向channel,如只读或者只写通道常见的异常问题,这块还需要大家自己总结总结,有懂的大佬也可以评论发送答案。 + +#### 4、向 channel 发送数据和从 channel 读数据的流程是什么样的? + +发送流程: + +![图片](data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==) + +接收流程:![图片](data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==) + +这个没啥好说的,底层原理,1、2、3描述出来,保证面试官满意。具体的文字描述下面一题有,channel的概念多且复杂,脑海中有个总分的概念,否则你说的再多,面试官也抓不住你说的重点,等于白说。问题5已经为大家总结好了。 + +#### 5、讲讲 Go 的 chan 底层数据结构和主要使用场景 + +答:channel 的数据结构包含 qccount 当前队列中剩余元素个数,dataqsiz 环形队列长度,即可以存放的元素个数,buf 环形队列指针,elemsize 每个元素的大小,closed 标识关闭状态,elemtype 元素类型,sendx 队列下表,指示元素写入时存放到队列中的位置,recv 队列下表,指示元素从队列的该位置读出。recvq 等待读消息的 goroutine 队列,sendq 等待写消息的 goroutine 队列,lock 互斥锁,chan 不允许并发读写。 + +无缓冲和有缓冲区别:管道没有缓冲区,从管道读数据会阻塞,直到有协程向管道中写入数据。同样,向管道写入数据也会阻塞,直到有协程从管道读取数据。管道有缓冲区但缓冲区没有数据,从管道读取数据也会阻塞,直到协程写入数据,如果管道满了,写数据也会阻塞,直到协程从缓冲区读取数据。 + +channel 的一些特点 1)、读写值 nil 管道会永久阻塞 2)、关闭的管道读数据仍然可以读数据 3)、往关闭的管道写数据会 panic 4)、关闭为 nil 的管道 panic 5)、关闭已经关闭的管道 panic + +向 channel 写数据的流程:如果等待接收队列 recvq 不为空,说明缓冲区中没有数据或者没有缓冲区,此时直接从 recvq 取出 G,并把数据写入,最后把该 G 唤醒,结束发送过程;如果缓冲区中有空余位置,将数据写入缓冲区,结束发送过程;如果缓冲区中没有空余位置,将待发送数据写入 G,将当前 G 加入 sendq,进入睡眠,等待被读 goroutine 唤醒; + +向 channel 读数据的流程:如果等待发送队列 sendq 不为空,且没有缓冲区,直接从 sendq 中取出 G,把 G 中数据读出,最后把 G 唤醒,结束读取过程;如果等待发送队列 sendq 不为空,此时说明缓冲区已满,从缓冲区中首部读出数据,把 G 中数据写入缓冲区尾部,把 G 唤醒,结束读取过程;如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程;将当前 goroutine 加入 recvq,进入睡眠,等待被写 goroutine 唤醒; + +使用场景:消息传递、消息过滤,信号广播,事件订阅与广播,请求、响应转发,任务分发,结果汇总,并发控制,限流,同步与异步 + +# 五、GMP相关 + +#### 1、什么是 GMP?(必问) + +答:G 代表着 goroutine,P 代表着上下文处理器,M 代表 thread 线程,在 GPM 模型,有一个全局队列(Global Queue):存放等待运行的 G,还有一个 P 的本地队列:也是存放等待运行的 G,但数量有限,不超过 256 个。GPM 的调度流程从 go func()开始创建一个 goroutine,新建的 goroutine 优先保存在 P 的本地队列中,如果 P 的本地队列已经满了,则会保存到全局队列中。M 会从 P 的队列中取一个可执行状态的 G 来执行,如果 P 的本地队列为空,就会从其他的 MP 组合偷取一个可执行的 G 来执行,当 M 执行某一个 G 时候发生系统调用或者阻塞,M 阻塞,如果这个时候 G 在执行,runtime 会把这个线程 M 从 P 中摘除,然后创建一个新的操作系统线程来服务于这个 P,当 M 系统调用结束时,这个 G 会尝试获取一个空闲的 P 来执行,并放入到这个 P 的本地队列,如果这个线程 M 变成休眠状态,加入到空闲线程中,然后整个 G 就会被放入到全局队列中。 + +关于 G,P,M 的个数问题,G 的个数理论上是无限制的,但是受内存限制,P 的数量一般建议是逻辑 CPU 数量的 2 倍,M 的数据默认启动的时候是 10000,内核很难支持这么多线程数,所以整个限制客户忽略,M 一般不做设置,设置好 P,M 一般都是要大于 P。 + +#### 2、进程、线程、协程有什么区别?(必问) + +进程:是应用程序的启动实例,每个进程都有独立的内存空间,不同的进程通过进程间的通信方式来通信。 + +线程:从属于进程,每个进程至少包含一个线程,线程是 CPU 调度的基本单位,多个线程之间可以共享进程的资源并通过共享内存等线程间的通信方式来通信。 + +协程:为轻量级线程,与线程相比,协程不受操作系统的调度,协程的调度器由用户应用程序提供,协程调度器按照调度策略把协程调度到线程中运行 + +#### 3、抢占式调度是如何抢占的? + +基于协作式抢占 + +基于信号量抢占 + +就像操作系统要负责线程的调度一样,Go的runtime要负责goroutine的调度。现代操作系统调度线程都是抢占式的,我们不能依赖用户代码主动让出CPU,或者因为IO、锁等待而让出,这样会造成调度的不公平。基于经典的时间片算法,当线程的时间片用完之后,会被时钟中断给打断,调度器会将当前线程的执行上下文进行保存,然后恢复下一个线程的上下文,分配新的时间片令其开始执行。这种抢占对于线程本身是无感知的,系统底层支持,不需要开发人员特殊处理。 + +基于时间片的抢占式调度有个明显的优点,能够避免CPU资源持续被少数线程占用,从而使其他线程长时间处于饥饿状态。goroutine的调度器也用到了时间片算法,但是和操作系统的线程调度还是有些区别的,因为整个Go程序都是运行在用户态的,所以不能像操作系统那样利用时钟中断来打断运行中的goroutine。也得益于完全在用户态实现,goroutine的调度切换更加轻量。 + +上面这两段文字只是对调度的一个概括,具体的协作式调度、信号量调度大家还需要去详细了解,这偏底层了,大厂或者中高级开发会问。(字节就问了) + +#### 4、M 和 P 的数量问题? + +p默认cpu内核数 + +M与P的数量没有绝对关系,一个M阻塞,P就会去创建或者切换另一个M,所以,即使P的默认数量是1,也有可能会创建很多个M出来 + +【Go语言调度模型G、M、P的数量多少合适?】 + +详细参考这篇文章Go语言调度模型G、M、P的数量多少合适? (https://zoyi14.smartapps.cn/pages/note/index?_swebFromHost=baiduboxapp&origin=share&slug=1a50330adf1b&_swebfr=1) + +GMP数量这一块,结论很好记,没用项目经验的话,问了项目中怎么用可能容易卡壳。 + +# 六、锁相关 + +#### 1、除了 mutex 以外还有哪些方式安全读写共享变量? + +* 将共享变量的读写放到一个 goroutine 中,其它 goroutine 通过 channel 进行读写操作。 + +* 可以用个数为 1 的信号量(semaphore)实现互斥 + +* 通过 Mutex 锁实现 + +#### 2、Go 如何实现原子操作? + +答:原子操作就是不可中断的操作,外界是看不到原子操作的中间状态,要么看到原子操作已经完成,要么看到原子操作已经结束。在某个值的原子操作执行的过程中,CPU 绝对不会再去执行其他针对该值的操作,那么其他操作也是原子操作。 + +Go 语言的标准库代码包 sync/atomic 提供了原子的读取(Load 为前缀的函数)或写入(Store 为前缀的函数)某个值(这里细节还要多去查查资料)。 + +原子操作与互斥锁的区别 + +* 互斥锁是一种数据结构,用来让一个线程执行程序的关键部分,完成互斥的多个操作。 +* 原子操作是针对某个值的单个互斥操作。 +* Mutex 是悲观锁还是乐观锁?悲观锁、乐观锁是什么? + +悲观锁 + +悲观锁:当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制【Pessimistic Concurrency Control,缩写“PCC”,又名“悲观锁”】。 + +乐观锁 + +乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果冲突,则返回给用户异常信息,让用户决定如何去做。乐观锁适用于读多写少的场景,这样可以提高程序的吞吐量 + +#### 4、Mutex 有几种模式? + +1. 正常模式 + +当前的mutex只有一个goruntine来获取,那么没有竞争,直接返回。新的goruntine进来,如果当前mutex已经被获取了,则该goruntine进入一个先入先出的waiter队列,在mutex被释放后,waiter按照先进先出的方式获取锁。该goruntine会处于自旋状态(不挂起,继续占有cpu)。新的goruntine进来,mutex处于空闲状态,将参与竞争。新来的 goroutine 有先天的优势,它们正在 CPU 中运行,可能它们的数量还不少,所以,在高并发情况下,被唤醒的 waiter 可能比较悲剧地获取不到锁,这时,它会被插入到队列的前面。如果 waiter 获取不到锁的时间超过阈值 1 毫秒,那么,这个 Mutex 就进入到了饥饿模式。 + +1. 饥饿模式 + +在饥饿模式下,Mutex 的拥有者将直接把锁交给队列最前面的 waiter。新来的 goroutine 不会尝试获取锁,即使看起来锁没有被持有,它也不会去抢,也不会 spin(自旋),它会乖乖地加入到等待队列的尾部。如果拥有 Mutex 的 waiter 发现下面两种情况的其中之一,它就会把这个 Mutex 转换成正常模式: + +此 waiter 已经是队列中的最后一个 waiter 了,没有其它的等待锁的 goroutine 了;此 waiter 的等待时间小于 1 毫秒。 + +#### 5、goroutine 的自旋占用资源如何解决 + +自旋锁是指当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断地判断是否能够被成功获取,直到获取到锁才会退出循环。 + +自旋的条件如下: + +* 还没自旋超过 4 次, +* 多核处理器, +* GOMAXPROCS > 1, +* p 上本地 goroutine 队列为空。 + +mutex 会让当前的 goroutine 去空转 CPU,在空转完后再次调用 CAS 方法去尝试性的占有锁资源,直到不满足自旋条件,则最终会加入到等待队列里。 + +# 七、并发相关 + +#### 1、怎么控制并发数? + +1. 有缓冲通道 + +根据通道中没有数据时读取操作陷入阻塞和通道已满时继续写入操作陷入阻塞的特性,正好实现控制并发数量。 + +```go +func main() { + count := 10 // 最大支持并发 + sum := 100 // 任务总数 + wg := sync.WaitGroup{} //控制主协程等待所有子协程执行完之后再退出。 + + c := make(chan struct{}, count) // 控制任务并发的chan + defer close(c) + + for i:=0; i接口,自动垃圾回收和goroutine等让人**拍案叫绝**的设计。 + +有许多基于Go的优秀项目。Docker,Kubernetes,etcd,deis,flynn,lime,revel等等。Go无疑是**云时代的最好语言**! + +题外话到此为止,在面试中,我们需要深入了解Go**语言特性**,并适当辅以**源码阅读**(Go源码非常**人性化,注释非常详细,**基本上只要你学过Go就能看懂)来提升能力。常考的点包括:切片,通道,异常处理,Goroutine,GMP模型,字符串高效拼接,指针,反射,接口,sync,go test和相关工具链。 + +一切问题的最权威的回答一定来自**官方**,这里力荐golang官方FAQ,虽然是英文的,但是也希望你花3-4天看完。**从使用者的角度去提问题, 从设计者的角度回答问题**。 + +![](https://pic3.zhimg.com/80/v2-1b6d660f2d2fc3599a2d82ef1245e3ea_1440w.jpg)官方FAQ问题 + [https://golang.org/doc/faq​golang.org/doc/faq](https://link.zhihu.com/?target=https%3A//golang.org/doc/faq) + +**面试题都是来源于网上和自己平时遇到的,但是很少有解答的版本,所以我专门回答了一下,放在专栏。** + +【所有试题已注明来源,侵删】 + +* * * + +## **面试题1** + +来源:[geektutu](https://link.zhihu.com/?target=https%3A//geektutu.com/post/qa-golang.html) + +### 基础语法 + +### 01 `=` 和 `:=` 的区别? + +=是赋值变量,:=是定义变量。 + +### 02 指针的作用 + +一个指针可以指向任意变量的地址,它所指向的地址在32位或64位机器上分别**固定**占4或8个字节。指针的作用有: + +* 获取变量的值 + + ```go +import fmt + ​ + func main(){ + a := 1 + p := &a//取址& + fmt.Printf("%d\n", *p);//取值* + } +``` + +* 改变变量的值 + + ```go +// 交换函数 + func swap(a, b *int) { + *a, *b = *b, *a + } +``` + +* 用指针替代值传入函数,比如类的接收器就是这样的。 + + ```go +type A struct{} + ​ + func (a *A) fun(){} +``` + +### 03 Go 允许多个返回值吗? + +可以。通常函数除了一般返回值还会返回一个error。 + +### 04 Go 有异常类型吗? + +有。Go用error类型代替try...catch语句,这样可以节省资源。同时增加代码可读性: + + ```go +_,err := errorDemo() + if err!=nil{ + fmt.Println(err) + return + } +``` + +也可以用errors.New()来定义自己的异常。errors.Error()会返回异常的字符串表示。只要实现error接口就可以定义自己的异常, + + ```go +type errorString struct { + s string + } + ​ + func (e *errorString) Error() string { + return e.s + } + ​ + // 多一个函数当作构造函数 + func New(text string) error { + return &errorString{text} + } +``` + +### 05 什么是协程(Goroutine) + +协程是**用户态轻量级线程**,它是**线程调度的基本单位**。通常在函数前加上go关键字就能实现并发。一个Goroutine会以一个很小的栈启动2KB或4KB,当遇到栈空间不足时,栈会**自动伸缩**, 因此可以轻易实现成千上万个goroutine同时启动。 + +### 06 ❤ 如何高效地拼接字符串 + +拼接字符串的方式有:"+", fmt.Sprintf, strings.Builder, bytes.Buffer, strings.Join + +1 "+" + +使用`+`操作符进行拼接时,会对字符串进行遍历,计算并开辟一个新的空间来存储原来的两个字符串。 + +2 fmt.Sprintf + +由于采用了接口参数,必须要用反射获取值,因此有性能损耗。 + +3 strings.Builder: + +用WriteString()进行拼接,内部实现是指针+切片,同时String()返回拼接后的字符串,它是直接把[]byte转换为string,从而避免变量拷贝。 + +`strings.builder`的实现原理很简单,结构如下: + +```go +type Builder struct { + addr *Builder // of receiver, to detect copies by value + buf []byte // 1 + } +``` + +`addr`字段主要是做`copycheck`,`buf`字段是一个`byte`类型的切片,这个就是用来存放字符串内容的,提供的`writeString()`方法就是像切片`buf`中追加数据: + +```go +func (b *Builder) WriteString(s string) (int, error) { + b.copyCheck() + b.buf = append(b.buf, s...) + return len(s), nil + } +``` + +提供的`String`方法就是将`[]byte`转换为`string`类型,这里为了避免内存拷贝的问题,使用了强制转换来避免内存拷贝: + +```go +func (b *Builder) String() string { + return *(*string)(unsafe.Pointer(&b.buf)) + } +``` + +4 bytes.Buffer + +`bytes.Buffer`是一个一个缓冲`byte`类型的缓冲器,这个缓冲器里存放着都是`byte`。使用方式如下: + +`bytes.buffer`底层也是一个`[]byte`切片,结构体如下: + +```go +type Buffer struct { + buf []byte // contents are the bytes buf[off : len(buf)] + off int // read at &buf[off], write at &buf[len(buf)] + lastRead readOp // last read operation, so that Unread* can work correctly. +} +``` + +因为`bytes.Buffer`可以持续向`Buffer`尾部写入数据,从`Buffer`头部读取数据,所以`off`字段用来记录读取位置,再利用切片的`cap`特性来知道写入位置,这个不是本次的重点,重点看一下`WriteString`方法是如何拼接字符串的: + +```go +func (b *Buffer) WriteString(s string) (n int, err error) { + b.lastRead = opInvalid + m, ok := b.tryGrowByReslice(len(s)) + if !ok { + m = b.grow(len(s)) + } + return copy(b.buf[m:], s), nil +} +``` + +切片在创建时并不会申请内存块,只有在往里写数据时才会申请,首次申请的大小即为写入数据的大小。如果写入的数据小于64字节,则按64字节申请。采用动态扩展`slice`的机制,字符串追加采用`copy`的方式将追加的部分拷贝到尾部,`copy`是内置的拷贝函数,可以减少内存分配。 + +但是在将`[]byte`转换为`string`类型依旧使用了标准类型,所以会发生内存分配: + +```go +func (b *Buffer) String() string { + if b == nil { + // Special case, useful in debugging. + return "" + } + return string(b.buf[b.off:]) +} +``` + +5 strings.join + +`strings.join`也是基于`strings.builder`来实现的,并且可以自定义分隔符,代码如下: + +```go +func Join(elems []string, sep string) string { + switch len(elems) { + case 0: + return "" + case 1: + return elems[0] + } + n := len(sep) * (len(elems) - 1) + for i := 0; i < len(elems); i++ { + n += len(elems[i]) + } + + var b Builder + b.Grow(n) + b.WriteString(elems[0]) + for _, s := range elems[1:] { + b.WriteString(sep) + b.WriteString(s) + } + return b.String() +} +``` + +唯一不同在于在`join`方法内调用了`b.Grow(n)`方法,这个是进行初步的容量分配,而前面计算的n的长度就是我们要拼接的slice的长度,因为我们传入切片长度固定,所以提前进行容量分配可以减少内存分配,很高效。 + +```go +func main(){ + a := []string{"a", "b", "c"} + //方式1: ret := a[0] + a[1] + a[2] + //方式2: ret := fmt.Sprintf("%s%s%s", a[0],a[1],a[2]) + //方式3: var sb strings.Builder + sb.WriteString(a[0]) + sb.WriteString(a[1]) + sb.WriteString(a[2]) + ret := sb.String() + //方式4: buf := new(bytes.Buffer) + buf.Write(a[0]) + buf.Write(a[1]) + buf.Write(a[2]) + ret := buf.String() + //方式5: ret := strings.Join(a,"") +} +``` + +总结: + +strings.Join ≈ strings.Builder > bytes.Buffer > "+" > fmt.Sprintf + +> 参考资料:[字符串拼接性能及原理 | Go 语言高性能编程 | 极客兔兔](https://link.zhihu.com/?target=https%3A//geektutu.com/post/hpg-string-concat.html) + +### 07 什么是 rune 类型 + +ASCII 码只需要 7 bit 就可以完整地表示,但只能表示英文字母在内的128个字符,为了表示世界上大部分的文字系统,发明了 Unicode, 它是ASCII的超集,包含世界上书写系统中存在的所有字符,并为每个代码分配一个标准编号(称为Unicode CodePoint),在 Go 语言中称之为 rune,是 int32 类型的别名。 + +Go 语言中,字符串的底层表示是 byte (8 bit) 序列,而非 rune (32 bit) 序列。 + +```go +sample := "我爱GO" +runeSamp := []rune(sample) +runeSamp[0] = '你' +fmt.Println(string(runeSamp)) // "你爱GO" fmt.Println(len(runeSamp)) // 4 +``` + +### 08 如何判断 map 中是否包含某个 key ? + +```go +var sample map[int]int +if _, ok := sample[10];ok{ + +}else{ + +} +``` + +### 09 Go 支持默认参数或可选参数吗? + +不支持。但是可以利用结构体参数,或者...传入参数切片。 + +### 10 defer 的执行顺序 + +defer执行顺序和调用顺序相反,类似于栈后进先出(LIFO)。 + +defer在return之后执行,但在函数退出之前,defer可以修改返回值。下面是一个例子: + +```go +func test() int { + i := 0 + defer func() { + fmt.Println("defer1") + }() + defer func() { + i += 1 + fmt.Println("defer2") + }() + return i +} + +func main() { + fmt.Println("return", test()) +} +// defer2 // defer1 // return 0 +``` + +上面这个例子中,test返回值并没有修改,这是由于Go的返回机制决定的,执行Return语句后,Go会创建一个临时变量保存返回值。如果是有名返回(也就是指明返回值functest()(i int)) + +```go +func test() (i int) { + i = 0 + defer func() { + i += 1 + fmt.Println("defer2") + }() + return i +} + +func main() { + fmt.Println("return", test()) +} +// defer2 // return 1 +``` + +这个例子中,返回值被修改了。对于有名返回值的函数,执行 return 语句时,并不会再创建临时变量保存,因此,defer 语句修改了 i,即对返回值产生了影响。 + +### 11 如何交换 2 个变量的值? + +对于变量而言`a,b = b,a`; 对于指针而言`*a,*b = *b, *a` + +### 12 Go 语言 tag 的用处? + +tag可以为结构体成员提供属性。常见的: + +1. json序列化或反序列化时字段的名称 +2. db: sqlx模块中对应的数据库字段名 +3. form: gin框架中对应的前端的数据字段名 +4. binding: 搭配 form 使用, 默认如果没查找到结构体中的某个字段则不报错值为空, binding为 required 代表没找到返回错误给前端 + +### 13 如何获取一个结构体的所有tag? + +利用反射: + +```go +import reflect +type Author struct { + Name int `json:Name` + Publications []string `json:Publication,omitempty` +} + +func main() { + t := reflect.TypeOf(Author{}) + for i := 0; i < t.NumField(); i++ { + name := t.Field(i).Name + s, _ := t.FieldByName(name) + fmt.Println(name, s.Tag) + } +} +``` + +上述例子中,`reflect.TypeOf`方法获取对象的类型,之后`NumField()`获取结构体成员的数量。 通过`Field(i)`获取第i个成员的名字。 再通过其`Tag` 方法获得标签。 + +### 14 如何判断 2 个字符串切片(slice) 是相等的? + +reflect.DeepEqual(), 但反射非常影响性能。 + +### 15 结构体打印时,`%v` 和 `%+v` 的区别 + +`%v`输出结构体各成员的值; + +`%+v`输出结构体各成员的**名称**和**值**; + +`%#v`输出结构体名称和结构体各成员的名称和值 + +### 16 Go 语言中如何表示枚举值(enums)? + +在常量中用iota可以表示枚举。iota从0开始。 + +```go +const ( + B = 1 << (10 * iota) + KiB + MiB + GiB + TiB + PiB + EiB +) +``` + +### 17 空 struct{} 的用途 + +* 用map模拟一个set,那么就要把值置为struct{},struct{}本身不占任何空间,可以避免任何多余的内存分配。 + +```go +type Set map[string]struct{} + +func main() { + set := make(Set) + + for _, item := range []string{"A", "A", "B", "C"} { + set[item] = struct{}{} + } + fmt.Println(len(set)) // 3 + if _, ok := set["A"]; ok { + fmt.Println("A exists") // A exists + } +} +``` + +* 有时候给通道发送一个空结构体,channel<-struct{}{},也是节省了空间。 + +```go +func main() { + ch := make(chan struct{}, 1) + go func() { + <-ch + // do something + }() + ch <- struct{}{} + // ... +} +``` + +* 仅有方法的结构体 + +```go +type Lamp struct{} +``` + +### **18 go里面的int和int32是同一个概念吗?** + +不是一个概念!千万不能混淆。go语言中的int的大小是和操作系统位数相关的,如果是32位操作系统,int类型的大小就是4字节。如果是64位操作系统,int类型的大小就是8个字节。除此之外uint也与操作系统有关。 + +int8占1个字节,int16占2个字节,int32占4个字节,int64占8个字节。 + +### 实现原理 + +### 01 init() 函数是什么时候执行的? + +**简答**: 在main函数之前执行。 + +**详细**:init()函数是go初始化的一部分,由runtime初始化每个导入的包,初始化不是按照从上到下的导入顺序,而是按照解析的依赖关系,没有依赖的包最先初始化。 + +每个包首先初始化包作用域的常量和变量(常量优先于变量),然后执行包的`init()`函数。同一个包,甚至是同一个源文件可以有多个`init()`函数。`init()`函数没有入参和返回值,不能被其他函数调用,同一个包内多个`init()`函数的执行顺序不作保证。 + +执行顺序:import –> const –> var –>`init()`–>`main()` + +一个文件可以有多个`init()`函数! + +### 02 ❤如何知道一个对象是分配在栈上还是堆上? + +Go和C++不同,Go局部变量会进行**逃逸分析**。如果**变量离开作用域后没有被引用**,则**优先**分配到栈上,否则分配到堆上。那么如何判断是否发生了逃逸呢? + +`go build -gcflags '-m -m -l' xxx.go`. + +关于逃逸的可能情况:变量大小不确定,变量类型不确定,变量分配的内存超过用户栈最大值,暴露给了外部指针。 + +### 03 2 个 interface 可以比较吗 ? + +Go 语言中,interface 的内部实现包含了 2 个字段,类型 `T` 和 值 `V`,interface 可以使用 `==` 或 `!=` 比较。2 个 interface 相等有以下 2 种情况 + +1. 两个 interface 均等于 nil(此时 V 和 T 都处于 unset 状态) +2. 类型 T 相同,且对应的值 V 相等。 + +看下面的例子: + +```go +type Stu struct { + Name string +} + +type StuInt interface{} + +func main() { + var stu1, stu2 StuInt = &Stu{"Tom"}, &Stu{"Tom"} + var stu3, stu4 StuInt = Stu{"Tom"}, Stu{"Tom"} + fmt.Println(stu1 == stu2) // false + fmt.Println(stu3 == stu4) // true } +``` + +`stu1` 和 `stu2` 对应的类型是 `*Stu`,值是 Stu 结构体的地址,两个地址不同,因此结果为 false。 +`stu3` 和 `stu4` 对应的类型是 `Stu`,值是 Stu 结构体,且各字段相等,因此结果为 true。 + +### 04 2 个 nil 可能不相等吗? + +可能不等。interface在运行时绑定值,只有值为nil接口值才为nil,但是与指针的nil不相等。举个例子: + +```go +var p *int = nil +var i interface{} = nil +if(p == i){ + fmt.Println("Equal") +} +``` + +两者并不相同。总结:**两个nil只有在类型相同时才相等**。 + +### 05 ❤简述 Go 语言GC(垃圾回收)的工作原理 + +垃圾回收机制是Go一大特(nan)色(dian)。Go1.3采用**标记清除法**, Go1.5采用**三色标记法**,Go1.8采用**三色标记法+混合写屏障**。 + +**_标记清除法_** + +分为两个阶段:标记和清除 + +标记阶段:从根对象出发寻找并标记所有存活的对象。 + +清除阶段:遍历堆中的对象,回收未标记的对象,并加入空闲链表。 + +缺点是需要暂停程序STW。 + +**_三色标记法_**: + +将对象标记为白色,灰色或黑色。 + +白色:不确定对象(默认色);黑色:存活对象。灰色:存活对象,子对象待处理。 + +标记开始时,先将所有对象加入白色集合(需要STW)。首先将根对象标记为灰色,然后将一个对象从灰色集合取出,遍历其子对象,放入灰色集合。同时将取出的对象放入黑色集合,直到灰色集合为空。最后的白色集合对象就是需要清理的对象。 + +这种方法有一个缺陷,如果对象的引用被用户修改了,那么之前的标记就无效了。因此Go采用了**写屏障技术**,当对象新增或者更新会将其着色为灰色。 + +一次完整的GC分为四个阶段: + +1. 准备标记(需要STW),开启写屏障。 +2. 开始标记 +3. 标记结束(STW),关闭写屏障 +4. 清理(并发) + +基于插入写屏障和删除写屏障在结束时需要STW来重新扫描栈,带来性能瓶颈。**混合写屏障**分为以下四步: + +1. GC开始时,将栈上的全部对象标记为黑色(不需要二次扫描,无需STW); +2. GC期间,任何栈上创建的新对象均为黑色 +3. 被删除引用的对象标记为灰色 +4. 被添加引用的对象标记为灰色 + +总而言之就是确保黑色对象不能引用白色对象,这个改进直接使得GC时间从 2s降低到2us。 + +### 06 函数返回局部变量的指针是否安全? + +这一点和C++不同,在Go里面返回局部变量的指针是安全的。因为Go会进行**逃逸分析**,如果发现局部变量的作用域超过该函数则会**把指针分配到堆区**,避免内存泄漏。 + +### 07 非接口的任意类型 T() 都能够调用 `*T` 的方法吗?反过来呢? + +一个T类型的值可以调用*T类型声明的方法,当且仅当T是**可寻址的**。 + +反之:*T 可以调用T()的方法,因为指针可以解引用。 + +### 08 go slice是怎么扩容的? + +如果当前容量小于1024,则判断所需容量是否大于原来容量2倍,如果大于,当前容量加上所需容量;否则当前容量乘2。 + +如果当前容量大于1024,则每次按照1.25倍速度递增容量,也就是每次加上cap/4。 + +### [并发编程](https://link.zhihu.com/?target=https%3A//geektutu.com/post/qa-golang-3.html) + +### 01 ❤无缓冲的 channel 和有缓冲的 channel 的区别? + +(这个问题笔者也纠结了很久,直到看到一篇文章,阻塞与否是分别针对发送接收方而言的,才茅塞顿开) + +对于无缓冲区channel: + +发送的数据如果没有被接收方接收,那么**发送方阻塞;**如果一直接收不到发送方的数据,**接收方阻塞**; + +有缓冲的channel: + +发送方在缓冲区满的时候阻塞,接收方不阻塞;接收方在缓冲区为空的时候阻塞,发送方不阻塞。 + +可以类比生产者与消费者问题。 + +![](https://pic3.zhimg.com/80/v2-b770e5632874d40780ecfe79701324f2_1440w.jpg) +### 02 为什么有协程泄露(Goroutine Leak)? + +协程泄漏是指协程创建之后没有得到释放。主要原因有: + +1. 缺少接收器,导致发送阻塞 +2. 缺少发送器,导致接收阻塞 +3. 死锁。多个协程由于竞争资源导致死锁。 +4. WaitGroup Add()和Done()不相等,前者更大。 + +### 03 Go 可以限制运行时操作系统线程的数量吗? 常见的goroutine操作函数有哪些? + +可以,使用runtime.GOMAXPROCS(num int)可以设置线程数目。该值默认为CPU逻辑核数,如果设的太大,会引起频繁的线程切换,降低性能。 + +runtime.Gosched(),用于让出CPU时间片,让出当前goroutine的执行权限,调度器安排其它等待的任务运行,并在下次某个时候从该位置恢复执行。 +runtime.Goexit(),调用此函数会立即使当前的goroutine的运行终止(终止协程),而其它的goroutine并不会受此影响。runtime.Goexit在终止当前goroutine前会先执行此goroutine的还未执行的defer语句。请注意千万别在主函数调用runtime.Goexit,因为会引发panic。 + +### 04 如何控制协程数目。 + +> The GOMAXPROCS variable limits the number of operating system threads that can execute user-level Go code simultaneously. There is no limit to the number of threads that can be blocked in system calls on behalf of Go code; those do not count against the GOMAXPROCS limit. + +可以使用环境变量 `GOMAXPROCS` 或 `runtime.GOMAXPROCS(num int)` 设置,例如: + +```go +runtime.GOMAXPROCS(1) // 限制同时执行Go代码的操作系统线程数为 1 +``` + +从官方文档的解释可以看到,`GOMAXPROCS` 限制的是同时执行用户态 Go 代码的操作系统线程的数量,但是对于被系统调用阻塞的线程数量是没有限制的。`GOMAXPROCS` 的默认值等于 CPU 的逻辑核数,同一时间,一个核只能绑定一个线程,然后运行被调度的协程。因此对于 CPU 密集型的任务,若该值过大,例如设置为 CPU 逻辑核数的 2 倍,会增加线程切换的开销,降低性能。对于 I/O 密集型应用,适当地调大该值,可以提高 I/O 吞吐率。 + +另外对于协程,可以用带缓冲区的channel来控制,下面的例子是协程数为1024的例子 + +```go +var wg sync.WaitGroup +ch := make(chan struct{}, 1024) +for i:=0; i<20000; i++{ + wg.Add(1) + ch<-struct{}{} + go func(){ + defer wg.Done() + <-ch + } +} +wg.Wait() +``` + +此外还可以用**协程池**:其原理无外乎是将上述代码中通道和协程函数解耦,并封装成单独的结构体。常见第三方协程池库,比如[tunny](https://link.zhihu.com/?target=http%3A//github.com/Jeffail/tunny)等。 + +面试题评价:★★★☆☆。偏容易和基础。❤表示需要重点关注。 + +* * * + +## **面试题2** + +来源:Durant Thorvalds + +### ❤new和make的区别? + +* new只用于分配内存,返回一个指向地址的**指针**。它为每个新类型分配一片内存,初始化为0且返回类型*T的内存地址,它相当于&T{} +* make只可用于**slice,map,channel**的初始化,返回的是**引用**。 + +```go +a := new(int) +*a = 46 +fmt.Println(*a) +``` + +### 请你讲一下Go面向对象是如何实现的? + +Go实现面向对象的两个关键是struct和interface。 + +封装:对于同一个包,对象对包内的文件可见;对不同的包,需要将对象以大写开头才是可见的。 + +继承:继承是编译时特征,在struct内加入所需要继承的类即可: + +```go +type A struct{} +type B struct{ +A +} +``` + +多态:多态是运行时特征,Go多态通过interface来实现。类型和接口是松耦合的,某个类型的实例可以赋给它所实现的任意接口类型的变量。 + +Go支持多重继承,就是在类型中嵌入所有必要的父类型。 + +### 二维切片如何初始化 + +一种方式是对每一个维度都初始化。 + +另一种方式是用一个单独的一维切片初始化。 + +```go +// Allocate the top-level slice. +picture := make([][]uint8, YSize) // One row per unit of y. +// Loop over the rows, allocating the slice for each row. +for i := range picture { + picture[i] = make([]uint8, XSize) +} +// Allocate the top-level slice, the same as before. +picture := make([][]uint8, YSize) // One row per unit of y. +// Allocate one large slice to hold all the pixels. +pixels := make([]uint8, XSize*YSize) // Has type []uint8 even though picture is [][]uint8. +// Loop over the rows, slicingog each row from the front of the remaining pixels slice. +for i := range picture { + picture[i], pixels = pixels[:XSize], pixels[XSize:] +``` + +### uint型变量值分别为 1,2,它们相减的结果是多少? + + ```go +var a uint = 1 + var b uint = 2 + fmt.Println(a - b) +``` + +答案,结果会溢出,如果是32位系统,结果是2^32-1,如果是64位系统,结果2^64-1. + +### 讲一下go有没有函数在main之前执行?怎么用? + +go的init函数在main函数之前执行,它有如下特点: + +* 初始化不能采用初始化表达式初始化的变量; +* 程序运行前执行注册 +* 实现sync.Once功能 +* 不能被其它函数调用 +* init函数没有入口参数和返回值: + +```go +func init(){ + register... +} +``` + +* 每个包可以有多个init函数,**每个源文件也可以有多个init函数**。 +* 同一个包的init执行顺序,golang没有明确定义,编程时要注意程序不要依赖这个执行顺序。 +* 不同包的init函数按照包导入的依赖关系决定执行顺序。 + +### 下面这句代码是什么作用,为什么要定义一个空值? + +```go +var _ Codec = (*GobCodec)(nil) +type GobCodec struct{ + conn io.ReadWriteCloser + buf *bufio.Writer + dec *gob.Decoder + enc *gob.Encoder +} + +type Codec interface { + io.Closer + ReadHeader(*Header) error + ReadBody(interface{}) error + Write(*Header, interface{}) error +} +``` + +答:将nil转换为*GobCodec类型,然后再转换为Codec接口,如果转换失败,说明*GobCodec没有实现Codec接口的所有方法。 + +### ❤golang的内存管理的原理清楚吗?简述go内存管理机制。 + +golang内存管理基本是参考tcmalloc来进行的。go内存管理本质上是一个内存池,只不过内部做了很多优化:自动伸缩内存池大小,合理的切割内存块。 + +> 一些基本概念: +> 页Page:一块8K大小的内存空间。Go向操作系统申请和释放内存都是以页为单位的。 +> span : 内存块,一个或多个连续的 page 组成一个 span 。如果把 page 比喻成工人, span 可看成是小队,工人被分成若干个队伍,不同的队伍干不同的活。 +> sizeclass : 空间规格,每个 span 都带有一个 sizeclass ,标记着该 span 中的 page 应该如何使用。使用上面的比喻,就是 sizeclass 标志着 span 是一个什么样的队伍。 +> object : 对象,用来存储一个变量数据内存空间,一个 span 在初始化时,会被切割成一堆等大的 object 。假设 object 的大小是 16B , span 大小是 8K ,那么就会把 span 中的 page 就会被初始化 8K / 16B = 512 个 object 。所谓内存分配,就是分配一个 object 出去。 + +1. **mheap** + +一开始go从操作系统索取一大块内存作为内存池,并放在一个叫mheap的内存池进行管理,mheap将一整块内存切割为不同的区域,并将一部分内存切割为合适的大小。 + +![](https://pic3.zhimg.com/80/v2-05f622a5c88a9a9456d43ee301622582_1440w.jpg) + +mheap.spans :用来存储 page 和 span 信息,比如一个 span 的起始地址是多少,有几个 page,已使用了多大等等。 + +mheap.bitmap 存储着各个 span 中对象的标记信息,比如对象是否可回收等等。 + +mheap.arena_start : 将要分配给应用程序使用的空间。 + +1. **mcentral** + +用途相同的span会以链表的形式组织在一起存放在mcentral中。这里用途用**sizeclass**来表示,就是该span存储哪种大小的对象。 + +找到合适的 span 后,会从中取一个 object 返回给上层使用。 + +1. **mcache** + +为了提高内存并发申请效率,加入缓存层mcache。每一个mcache和处理器P对应。Go申请内存首先从P的mcache中分配,如果没有可用的span再从mcentral中获取。 + +> 参考资料:[Go 语言内存管理(二):Go 内存管理](https://link.zhihu.com/?target=https%3A//cloud.tencent.com/developer/article/1422392) + +### ❤mutex有几种模式? + +mutex有两种模式:**normal** 和 **starvation** + +正常模式 + +所有goroutine按照FIFO的顺序进行锁获取,被唤醒的goroutine和新请求锁的goroutine同时进行锁获取,通常**新请求锁的goroutine更容易获取锁**(持续占有cpu),被唤醒的goroutine则不容易获取到锁。公平性:否。 + +饥饿模式 + +所有尝试获取锁的goroutine进行等待排队,**新请求锁的goroutine不会进行锁获取**(禁用自旋),而是加入队列尾部等待获取锁。公平性:是。 + +> 参考链接:[Go Mutex 饥饿模式](https://link.zhihu.com/?target=https%3A//blog.csdn.net/qq_37102984/article/details/115322706),[GO 互斥锁(Mutex)原理](https://link.zhihu.com/?target=https%3A//blog.csdn.net/baolingye/article/details/111357407%23%3A~%3Atext%3D%25E6%25AF%258F%25E4%25B8%25AAMutex%25E9%2583%25BD%2Ctarving%25E3%2580%2582) + +* * * + +## **面试题3** + +来源**:**[如果你是一个Golang面试官,你会问哪些问题?](https://www.zhihu.com/question/67846139/answer/1983588716) + +### ❤go如何进行调度的。GMP中状态流转。 + +Go里面GMP分别代表:G:goroutine,M:线程(真正在CPU上跑的),P:调度器。 + +![](https://pic3.zhimg.com/80/v2-63a317972091b6d43863c5144a6badce_1440w.jpg)GMP模型 + +调度器是M和G之间桥梁。 + +go进行调度过程: + +* 某个线程尝试创建一个新的G,那么这个G就会被安排到这个线程的G本地队列LRQ中,如果LRQ满了,就会分配到全局队列GRQ中; +* 尝试获取当前线程的M,如果无法获取,就会从空闲的M列表中找一个,如果空闲列表也没有,那么就创建一个M,然后绑定G与P运行。 +* 进入调度循环: + +* 找到一个合适的G +* 执行G,完成以后退出 + +### Go什么时候发生阻塞?阻塞时,调度器会怎么做。 + +* 用于**原子、互斥量或通道**操作导致goroutine阻塞,调度器将把当前阻塞的goroutine从本地运行队列**LRQ换出**,并重新调度其它goroutine; +* 由于**网络请求**和**IO**导致的阻塞,Go提供了网络轮询器(Netpoller)来处理,后台用epoll等技术实现IO多路复用。 + +其它回答: + +* **channel阻塞**:当goroutine读写channel发生阻塞时,会调用gopark函数,该G脱离当前的M和P,调度器将新的G放入当前M。 +* **系统调用**:当某个G由于系统调用陷入内核态,该P就会脱离当前M,此时P会更新自己的状态为Psyscall,M与G相互绑定,进行系统调用。结束以后,若该P状态还是Psyscall,则直接关联该M和G,否则使用闲置的处理器处理该G。 +* **系统监控**:当某个G在P上运行的时间超过10ms时候,或者P处于Psyscall状态过长等情况就会调用retake函数,触发新的调度。 +* **主动让出**:由于是协作式调度,该G会主动让出当前的P(通过GoSched),更新状态为Grunnable,该P会调度队列中的G运行。 + +> 更多关于netpoller的内容可以参看:[https://strikefreedom.top/go-netpoll-io-multiplexing-reactor](https://link.zhihu.com/?target=https%3A//strikefreedom.top/go-netpoll-io-multiplexing-reactor) + +### ❤Go中GMP有哪些状态? + +G的状态: + +**_Gidle**:刚刚被分配并且还没有被初始化,值为0,为创建goroutine后的默认值 + +**_Grunnable**: 没有执行代码,没有栈的所有权,存储在运行队列中,可能在某个P的本地队列或全局队列中(如上图)。 + +**_Grunning**: 正在执行代码的goroutine,拥有栈的所有权(如上图)。 + +**_Gsyscall**:正在执行系统调用,拥有栈的所有权,与P脱离,但是与某个M绑定,会在调用结束后被分配到运行队列(如上图)。 + +**_Gwaiting**:被阻塞的goroutine,阻塞在某个channel的发送或者接收队列(如上图)。 + +**_Gdead**: 当前goroutine未被使用,没有执行代码,可能有分配的栈,分布在空闲列表gFree,可能是一个刚刚初始化的goroutine,也可能是执行了goexit退出的goroutine(如上图)。 + +**_Gcopystac**:栈正在被拷贝,没有执行代码,不在运行队列上,执行权在 + +**_Gscan** : GC 正在扫描栈空间,没有执行代码,可以与其他状态同时存在。 + +P的状态: + +**_Pidle** :处理器没有运行用户代码或者调度器,被空闲队列或者改变其状态的结构持有,运行队列为空 + +**_Prunning** :被线程 M 持有,并且正在执行用户代码或者调度器(如上图) + +**_Psyscall**:没有执行用户代码,当前线程陷入系统调用(如上图) + +**_Pgcstop** :被线程 M 持有,当前处理器由于垃圾回收被停止 + +**_Pdead** :当前处理器已经不被使用 + +M的状态: + +**自旋线程**:处于运行状态但是没有可执行goroutine的线程,数量最多为GOMAXPROC,若是数量大于GOMAXPROC就会进入休眠。 + +**非自旋线程**:处于运行状态有可执行goroutine的线程。 + +下面一张图很好的展示了Goroutine状态流转: + +![](https://pic4.zhimg.com/80/v2-3312a9b7852f67257a1266bd56e2aa1f_1440w.jpg)Goroutine状态流转 +### GMP能不能去掉P层?会怎么样? + +去掉p会导致,当G进行**系统调用时候,会一直阻塞**,其它G无法获得M。 + +### 如果有一个G一直占用资源怎么办。 + +如果有个goroutine一直占用资源,那么GMP模型会**从正常模式转变为饥饿模式**(类似于mutex),允许其它goroutine抢占(禁用自旋锁)。 + +### 若干线程一个线程发生OOM(Out of memory)会怎么办。 + +对于线程而言:发生内存溢出的线程会被kill,其它线程不受影响。 + +### goroutine什么情况会发生内存泄漏?如何避免。 + +在Go中内存泄露分为暂时性内存泄露和永久性内存泄露。 + +**暂时性内存泄露** + +* 获取长字符串中的一段导致长字符串未释放 +* 获取长slice中的一段导致长slice未释放 +* 在长slice新建slice导致泄漏 + +string相比切片少了一个容量的cap字段,可以把string当成一个只读的切片类型。获取长string或者切片中的一段内容,由于新生成的对象和老的string或者切片共用一个内存空间,会导致老的string和切片资源暂时得不到释放,造成短暂的内存泄漏 + +**永久性内存泄露** + +* goroutine永久阻塞而导致泄漏 +* time.Ticker未关闭导致泄漏 +* 不正确使用Finalizer导致泄漏 + +### 怎么调试go? + +在vscode设置mode为debug。需要go-dlv插件。 + +### Go GC有几个阶段 + +目前的go GC采用**三色标记法**和**混合写屏障**技术。 + +Go GC有**四**个阶段: + +* STW,开启混合写屏障,扫描栈对象; +* 将所有对象加入白色集合,从根对象开始,将其放入灰色集合。每次从灰色集合取出一个对象标记为黑色,然后遍历其子对象,标记为灰色,放入灰色集合; +* 如此循环直到灰色集合为空。剩余的白色对象就是需要清理的对象。 +* STW,关闭混合写屏障; +* 在后台进行GC(并发)。 + +### go竞态条件了解吗? + +所谓竞态竞争,就是当**两个或以上的goroutine访问相同资源时候,对资源进行读/写。** + +比如`var a int = 0`,有两个协程分别对a+=1,我们发现最后a不一定为2.这就是竞态竞争。 + +通常我们可以用`go run -race xx.go`来进行检测。 + +解决方法是,对临界区资源上锁,或者使用原子操作(atomics),原子操作的开销小于上锁。 + +### 如果若干个goroutine,有一个panic会怎么做? + +有一个panic,那么剩余goroutine也会退出。 + +> 参考理解:[goroutine配上panic会怎样](https://link.zhihu.com/?target=https%3A//blog.csdn.net/huorongbj/article/details/123013273)。 + +### defer可以捕获goroutine的子goroutine吗? + +不可以。它们处于不同的调度器P中。对于子goroutine,正确的做法是: + +1. 必须通过 defer 关键字来调用 recover()。 +2. 当通过 goroutine 调用某个方法,一定要确保内部有 recover() 机制。 + +### ❤gRPC是什么? + +基于go的**远程过程调用**。RPC 框架的目标就是让远程服务调用更加简单、透明,RPC 框架负责屏蔽底层的传输方式(TCP 或者 UDP)、序列化方式(XML/Json/ 二进制)和通信细节。服务调用者可以像调用本地接口一样调用远程的服务提供者,而不需要关心底层通信细节和调用过程。 + +![](https://pic3.zhimg.com/80/v2-53fcdf2682c027f2d16292c4b4ba20d6_1440w.jpg)gRPC框架图 +## 面试题4 + +需要面试者有一定的大型项目经验经验,了解使用**微服务,etcd,gin,gorm,gRPC**等典型框架等模型或框架。 + +### 微服务了解吗? + +微服务是一种开发软件的架构和组织方法,其中软件由通过明确定义的 API 进行通信的小型独立服务组成。微服务架构使应用程序更易于扩展和更快地开发,从而加速创新并缩短新功能的上市时间。 + +![](https://pic3.zhimg.com/80/v2-56601175dc48fbec496c79284488ecee_1440w.jpg)微服务示意图 + +微服务有着自主,专用,灵活性等优点。 + +> 参考资料:[什么是微服务?| AWS](https://link.zhihu.com/?target=https%3A//aws.amazon.com/cn/microservices/) + +### 服务发现是怎么做的? + +主要有两种服务发现机制:**客户端发现**和**服务端发现**。 + +**客户端发现模式**:当我们使用客户端发现的时候,客户端负责决定可用服务实例的网络地址并且在集群中对请求负载均衡, 客户端访问**服务登记表**,也就是一个可用服务的数据库,然后客户端使用一种**负载均衡算法**选择一个可用的服务实例然后发起请求。该模式如下图所示: + +![](https://pic2.zhimg.com/80/v2-915e057bb7b6783393cdf1bfd2d0d745_1440w.jpg)客户端发现模式 + +**服务端发现模式**:客户端通过**负载均衡器**向某个服务提出请求,负载均衡器查询服务注册表,并将请求转发到可用的服务实例。如同客户端发现,服务实例在服务注册表中注册或注销。 + +![](https://pic3.zhimg.com/80/v2-fe7926e3a7007f985a87e102743a842e_1440w.jpg)服务端发现模式 + +参考资料:[「Chris Richardson 微服务系列」服务发现的可行方案以及实践案例](https://link.zhihu.com/?target=http%3A//blog.daocloud.io/3289.html) + +### ETCD用过吗? + +**etcd**是一个**高度一致**的**分布式键值存储**,它提供了一种可靠的方式来存储需要由分布式系统或机器集群访问的数据。它可以优雅地处理网络分区期间的领导者**选举**,即使在领导者节点中也可以容忍机器故障。 + +etcd 是用**Go语言**编写的,它具有出色的跨平台支持,小的二进制文件和强大的社区。etcd机器之间的通信通过**Raft共识算法**处理。 + +关于文档可以参考:[v3.5 docs](https://link.zhihu.com/?target=https%3A//etcd.io/docs/v3.5/) + +### GIN怎么做参数校验? + +go采用validator作参数校验。 + +它具有以下独特功能: + +* 使用验证tag或自定义validator进行跨字段Field和跨结构体验证。 +* 允许切片、数组和哈希表,多维字段的任何或所有级别进行校验。 +* 能够对哈希表key和value进行验证 +* 通过在验证之前确定它的基础类型来处理类型接口。 +* 别名验证标签,允许将多个验证映射到单个标签,以便更轻松地定义结构体上的验证 +* gin web 框架的默认验证器; + +参考资料:[validator package - pkg.go.dev](https://link.zhihu.com/?target=https%3A//pkg.go.dev/github.com/go-playground/validator%23section-readme) + +### 中间件用过吗? + +Middleware是Web的重要组成部分,中间件(通常)是一小段代码,它们接受一个请求,对其进行处理,每个中间件只处理一件事情,完成后将其传递给另一个中间件或最终处理程序,这样就做到了程序的解耦。 + +### Go解析Tag是怎么实现的? + +Go解析tag采用的是**反射**。 + +具体来说使用reflect.ValueOf方法获取其反射值,然后获取其Type属性,之后再通过Field(i)获取第i+1个field,再.Tag获得Tag。 + +反射实现的原理在: `src/reflect/type.go`中 + +### 你项目有优雅的启停吗? + +所谓「优雅」启停就是在启动退出服务时要满足以下几个条件: + +* **不可以关闭现有连接**(进程) +* 新的进程启动并「**接管**」旧进程 +* 连接要**随时响应用户请求**,不可以出现拒绝请求的情况 +* 停止的时候,必须**处理完既有连接**,并且**停止接收新的连接**。 + +为此我们必须引用**信号**来完成这些目的: + +启动: + +* 监听SIGHUP(在用户终端连接(正常或非正常)结束时发出); +* 收到信号后将服务监听的文件描述符传递给新的子进程,此时新老进程同时接收请求; + +退出: + +* 监听SIGINT和SIGSTP和SIGQUIT等。 +* 父进程停止接收新请求,等待旧请求完成(或超时); +* 父进程退出。 + +实现:go1.8采用Http.Server内置的Shutdown方法支持优雅关机。 然后[fvbock/endless](https://link.zhihu.com/?target=http%3A//github.com/fvbock/endless)可以实现优雅重启。 + +> 参考资料:[gin框架实践连载八 | 如何优雅重启和停止 - 掘金](https://link.zhihu.com/?target=https%3A//juejin.cn/post/6867074626427502600%23heading-3),[优雅地关闭或重启 go web 项目](https://link.zhihu.com/?target=http%3A//www.phpxs.com/post/7186/) + +### 持久化怎么做的? + +所谓持久化就是将要保存的字符串写到硬盘等设备。 + +* 最简单的方式就是采用ioutil的WriteFile()方法将字符串写到磁盘上,这种方法面临**格式化**方面的问题。 +* 更好的做法是将数据按照**固定协议**进行组织再进行读写,比如JSON,XML,Gob,csv等。 +* 如果要考虑**高并发**和**高可用**,必须把数据放入到数据库中,比如MySQL,PostgreDB,MongoDB等。 + +参考链接:[Golang 持久化](https://link.zhihu.com/?target=https%3A//www.jianshu.com/p/015aca3e11ae) + +* * * + +## **面试题5** + +作者:Dylan2333 链接: + + [测开转Go开发-面经&总结_笔经面经_牛客网​www.nowcoder.com/discuss/826193?type=post&order=recall&pos=&page=1&ncTraceId=&channel=-1&source_id=search_post_nctrack&gio_id=9C5DC1FFB3FC3BE29281D7CCFC420365-1645173894793![](https://pic1.zhimg.com/v2-ed411b6288d53218e5e9dd056d1df020_ipico.jpg)](https://link.zhihu.com/?target=https%3A//www.nowcoder.com/discuss/826193%3Ftype%3Dpost%26order%3Drecall%26pos%3D%26page%3D1%26ncTraceId%3D%26channel%3D-1%26source_id%3Dsearch_post_nctrack%26gio_id%3D9C5DC1FFB3FC3BE29281D7CCFC420365-1645173894793) + +该试题需要面试者有非常丰富的项目阅历和底层原理经验,熟练使用**微服务,etcd,gin,gorm,gRPC**等典型框架等模型或框架。 + +### channel 死锁的场景 + +* 当一个`channel`中没有数据,而直接读取时,会发生死锁: + +```go +q := make(chan int,2) +<-q +``` + +解决方案是采用select语句,再default放默认处理方式: + +```go +q := make(chan int,2) +select{ + case val:=<-q: + default: + ... + +} +``` + +* 当channel数据满了,再尝试写数据会造成死锁: + +```go +q := make(chan int,2) +q<-1 +q<-2 +q<-3 +``` + +解决方法,采用select + +```go +func main() { + q := make(chan int, 2) + q <- 1 + q <- 2 + select { + case q <- 3: + fmt.Println("ok") + default: + fmt.Println("wrong") + } + +} +``` + +* 向一个关闭的channel写数据。 + +注意:一个已经关闭的channel,只能读数据,不能写数据。 + +参考资料:[Golang关于channel死锁情况的汇总以及解决方案](https://link.zhihu.com/?target=https%3A//blog.csdn.net/qq_35976351/article/details/81984117) + +### 读写channel应该先关哪个? + +应该写channel先关。因为对于已经关闭的channel只能读,不能写。 + +### 对已经关闭的chan进行读写会怎么样? + +* 读已经关闭的chan能一直读到东西,但是读到的内容根据通道内关闭前是否有元素而不同。 + +* 如果chan关闭前,buffer内有元素还未读,会正确读到chan内的值,且返回的第二个bool值(是否读成功)为true。 +* 如果chan关闭前,buffer内有元素已经被读完,chan内无值,接下来所有接收的值都会非阻塞直接成功,返回 channel 元素的零值,但是第二个bool值一直为false。 + +写已经关闭的chan会panic。 + +### 说说 atomic底层怎么实现的. + +atomic源码位于`sync\atomic`。通过阅读源码可知,atomic采用**CAS**(CompareAndSwap)的方式实现的。所谓CAS就是使用了CPU中的原子性操作。在操作共享变量的时候,CAS不需要对其进行加锁,而是通过类似于乐观锁的方式进行检测,总是假设被操作的值未曾改变(即与旧值相等),并一旦确认这个假设的真实性就立即进行值替换。本质上是**不断占用CPU资源来避免加锁的开销**。 + +> 参考资料:[Go语言的原子操作atomic - 编程猎人](https://link.zhihu.com/?target=https%3A//www.programminghunter.com/article/37392193442/) + +### channel底层实现?是否线程安全。 + +channel底层实现在`src/runtime/chan.go`中 + +channel内部是一个循环链表。内部包含buf, sendx, recvx, lock ,recvq, sendq几个部分; + +buf是有缓冲的channel所特有的结构,用来存储缓存数据。是个循环链表; + +* sendx和recvx用于记录buf这个循环链表中的发送或者接收的index; +* lock是个互斥锁; +* recvq和sendq分别是接收(<-channel)或者发送(channel <- xxx)的goroutine抽象出来的结构体(sudog)的队列。是个双向链表。 + +channel是**线程安全**的。 + +> 参考资料:[Kitou:Golang 深度剖析 -- channel的底层实现](https://zhuanlan.zhihu.com/p/264305133) + +### map的底层实现。 + +源码位于`src\runtime\map.go` 中。 + +go的map和C++map不一样,底层实现是哈希表,包括两个部分:**hmap**和**bucket**。 + +hmap结构体如图: + +```go +type hmap struct { + count int //map元素的个数,调用len()直接返回此值 + + // map标记: + // 1\. key和value是否包指针 + // 2\. 是否正在扩容 + // 3\. 是否是同样大小的扩容 + // 4\. 是否正在 `range`方式访问当前的buckets + // 5\. 是否有 `range`方式访问旧的bucket + flags uint8 + + B uint8 // buckets 的对数 log_2, buckets 数组的长度就是 2^B + noverflow uint16 // overflow 的 bucket 近似数 + hash0 uint32 // hash种子 计算 key 的哈希的时候会传入哈希函数 + buckets unsafe.Pointer // 指向 buckets 数组,大小为 2^B 如果元素个数为0,就为 nil + + // 扩容的时候,buckets 长度会是 oldbuckets 的两倍 + oldbuckets unsafe.Pointer // bucket slice指针,仅当在扩容的时候不为nil + + nevacuate uintptr // 扩容时已经移到新的map中的bucket数量 + extra *mapextra // optional fields +} +``` + +里面最重要的是buckets(桶)。buckets是一个指针,最终它指向的是一个结构体: + +```go +// A bucket for a Go map. +type bmap struct { + tophash [bucketCnt]uint8 +} +``` + +每个bucket固定包含8个key和value(可以查看源码bucketCnt=8).实现上面是一个固定的大小连续内存块,分成四部分:每个条目的状态,8个key值,8个value值,指向下个bucket的指针。 + +创建哈希表使用的是`makemap`函数.map 的一个关键点在于,**哈希函数**的选择。在程序启动时,会检测 cpu 是否支持 aes,如果支持,则使用 aes hash,否则使用 memhash。这是在函数 alginit() 中完成,位于路径:`src/runtime/alg.go` 下。 + +map查找就是将key哈希后得到64位(64位机)用最后B个比特位计算在哪个桶。在 bucket 中,从前往后找到第一个空位。这样,在查找某个 key 时,先找到对应的桶,再去遍历 bucket 中的 key。 + +关于map的查找和扩容可以参考[map的用法到map底层实现分析](https://link.zhihu.com/?target=https%3A//blog.csdn.net/chenxun_2010/article/details/103768011%3Futm_medium%3Ddistribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-0.pc_relevant_aa%26spm%3D1001.2101.3001.4242.1%26utm_relevant_index%3D3)。 + +### select的实现原理? + +select源码位于`src\runtime\select.go`,最重要的`scase` 数据结构为: + +```go +type scase struct { + c *hchan // chan + elem unsafe.Pointer // data element +} +``` + +scase.c为当前case语句所操作的channel指针,这也说明了一个case语句只能操作一个channel。 + +scase.elem表示缓冲区地址: + +* caseRecv : scase.elem表示读出channel的数据存放地址; +* caseSend : scase.elem表示将要写入channel的数据存放地址; + +select的主要实现位于:`selectgo`函数:其主要功能如下: + +```go +//1\. 锁定scase语句中所有的channel + //2\. 按照随机顺序检测scase中的channel是否ready + // 2.1 如果case可读,则读取channel中数据,解锁所有的channel,然后返回(case index, true) + // 2.2 如果case可写,则将数据写入channel,解锁所有的channel,然后返回(case index, false) + // 2.3 所有case都未ready,则解锁所有的channel,然后返回(default index, false) + //3\. 所有case都未ready,且没有default语句 + // 3.1 将当前协程加入到所有channel的等待队列 + // 3.2 当将协程转入阻塞,等待被唤醒 + //4\. 唤醒后返回channel对应的case index + // 4.1 如果是读操作,解锁所有的channel,然后返回(case index, true) + // 4.2 如果是写操作,解锁所有的channel,然后返回(case index, false) +``` + +参考资料:[Go select的使用和实现原理](https://link.zhihu.com/?target=https%3A//www.cnblogs.com/wuyepeng/p/13910678.html%23%3A~%3Atext%3D%25E4%25B8%2580%25E3%2580%2581select%25E7%25AE%2580%25E4%25BB%258B.%25201.Go%25E7%259A%2584select%25E8%25AF%25AD%25E5%258F%25A5%25E6%2598%25AF%25E4%25B8%2580%25E7%25A7%258D%25E4%25BB%2585%25E8%2583%25BD%25E7%2594%25A8%25E4%25BA%258Echannl%25E5%258F%2591%25E9%2580%2581%25E5%2592%258C%25E6%258E%25A5%25E6%2594%25B6%25E6%25B6%2588%25E6%2581%25AF%25E7%259A%2584%25E4%25B8%2593%25E7%2594%25A8%25E8%25AF%25AD%25E5%258F%25A5%25EF%25BC%258C%25E6%25AD%25A4%25E8%25AF%25AD%25E5%258F%25A5%25E8%25BF%2590%25E8%25A1%258C%25E6%259C%259F%25E9%2597%25B4%25E6%2598%25AF%25E9%2598%25BB%25E5%25A1%259E%25E7%259A%2584%25EF%25BC%259B%25E5%25BD%2593select%25E4%25B8%25AD%25E6%25B2%25A1%25E6%259C%2589case%25E8%25AF%25AD%25E5%258F%25A5%25E7%259A%2584%25E6%2597%25B6%25E5%2580%2599%25EF%25BC%258C%25E4%25BC%259A%25E9%2598%25BB%25E5%25A1%259E%25E5%25BD%2593%25E5%2589%258Dgroutine%25E3%2580%2582.%25202.select%25E6%2598%25AFGolang%25E5%259C%25A8%25E8%25AF%25AD%25E8%25A8%2580%25E5%25B1%2582%25E9%259D%25A2%25E6%258F%2590%25E4%25BE%259B%25E7%259A%2584I%252FO%25E5%25A4%259A%25E8%25B7%25AF%25E5%25A4%258D%25E7%2594%25A8%25E7%259A%2584%25E6%259C%25BA%25E5%2588%25B6%25EF%25BC%258C%25E5%2585%25B6%25E4%25B8%2593%25E9%2597%25A8%25E7%2594%25A8%25E6%259D%25A5%25E6%25A3%2580%25E6%25B5%258B%25E5%25A4%259A%25E4%25B8%25AAchannel%25E6%2598%25AF%25E5%2590%25A6%25E5%2587%2586%25E5%25A4%2587%25E5%25AE%258C%25E6%25AF%2595%25EF%25BC%259A%25E5%258F%25AF%25E8%25AF%25BB%25E6%2588%2596%25E5%258F%25AF%25E5%2586%2599%25E3%2580%2582.%2C3.select%25E8%25AF%25AD%25E5%258F%25A5%25E4%25B8%25AD%25E9%2599%25A4default%25E5%25A4%2596%25EF%25BC%258C%25E6%25AF%258F%25E4%25B8%25AAcase%25E6%2593%258D%25E4%25BD%259C%25E4%25B8%2580%25E4%25B8%25AAchannel%25EF%25BC%258C%25E8%25A6%2581%25E4%25B9%2588%25E8%25AF%25BB%25E8%25A6%2581%25E4%25B9%2588%25E5%2586%2599.%25204.select%25E8%25AF%25AD%25E5%258F%25A5%25E4%25B8%25AD%25E9%2599%25A4default%25E5%25A4%2596%25EF%25BC%258C%25E5%2590%2584case%25E6%2589%25A7%25E8%25A1%258C%25E9%25A1%25BA%25E5%25BA%258F%25E6%2598%25AF%25E9%259A%258F%25E6%259C%25BA%25E7%259A%2584.%25205.select%25E8%25AF%25AD%25E5%258F%25A5%25E4%25B8%25AD%25E5%25A6%2582%25E6%259E%259C%25E6%25B2%25A1%25E6%259C%2589default%25E8%25AF%25AD%25E5%258F%25A5%25EF%25BC%258C%25E5%2588%2599%25E4%25BC%259A%25E9%2598%25BB%25E5%25A1%259E%25E7%25AD%2589%25E5%25BE%2585%25E4%25BB%25BB%25E4%25B8%2580case.%25206.select%25E8%25AF%25AD%25E5%258F%25A5%25E4%25B8%25AD%25E8%25AF%25BB%25E6%2593%258D%25E4%25BD%259C%25E8%25A6%2581%25E5%2588%25A4%25E6%2596%25AD%25E6%2598%25AF%25E5%2590%25A6%25E6%2588%2590%25E5%258A%259F%25E8%25AF%25BB%25E5%258F%2596%25EF%25BC%258C%25E5%2585%25B3%25E9%2597%25AD%25E7%259A%2584channel%25E4%25B9%259F%25E5%258F%25AF%25E4%25BB%25A5%25E8%25AF%25BB%25E5%258F%2596). + +### go的interface怎么实现的? + +go interface源码在`runtime\iface.go`中。 + +go的接口由两种类型实现`iface`和`eface`。iface是包含方法的接口,而eface不包含方法。 + +* `iface` + +对应的数据结构是(位于`src\runtime\runtime2.go`): + +```go +type iface struct { + tab *itab + data unsafe.Pointer +} +``` + +可以简单理解为,tab表示接口的具体结构类型,而data是接口的值。 + +itab: + +```go +type itab struct { + inter *interfacetype //此属性用于定位到具体interface + _type *_type //此属性用于定位到具体interface + hash uint32 // copy of _type.hash. Used for type switches. + _ [4]byte + fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter. +} +``` + +属性`interfacetype`类似于`_type`,其作用就是interface的公共描述,类似的还有`maptype`、`arraytype`、`chantype`…其都是各个结构的公共描述,可以理解为一种外在的表现信息。interfaetype和type唯一确定了接口类型,而hash用于查询和类型判断。fun表示方法集。 + +* `eface` + +与iface基本一致,但是用`_type`直接表示类型,这样的话就无法使用方法。 + +```go +type eface struct { + _type *_type + data unsafe.Pointer +} +``` + +这里篇幅有限,深入讨论可以看:[深入研究 Go interface 底层实现](https://link.zhihu.com/?target=https%3A//halfrost.com/go_interface/%23toc-1) + +### go的reflect 底层实现 + +go reflect源码位于`src\reflect\`下面,作为一个库独立存在。反射是基于**接口**实现的。 + +Go反射有三大法则: + +* 反射从**接口**映射到**反射对象;** + +![](https://pic2.zhimg.com/80/v2-350518add3d5e2757a8bc98f3c6fc15d_1440w.jpg)法则1 + +* 反射从**反射对象**映射到**接口值**; + +![](https://pic3.zhimg.com/80/v2-c2354d13a1514a482efa60e3d8cff816_1440w.jpg)法则2 + +* 只有**值可以修改**(settable),才可以**修改**反射对象。 + +Go反射基于上述三点实现。我们先从最核心的两个源文件入手`type.go`和`value.go`. + +type用于获取当前值的类型。value用于获取当前的值。 + +> 参考资料:[The Laws of Reflection](https://link.zhihu.com/?target=https%3A//go.dev/blog/laws-of-reflection), [图解go反射实现原理](https://link.zhihu.com/?target=https%3A//i6448038.github.io/2020/02/15/golang-reflection/) + +### go GC的原理知道吗? + +如果需要从源码角度解释GC,推荐阅读(非常详细,图文并茂): + +[https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-garbage-collector/](https://link.zhihu.com/?target=https%3A//draveness.me/golang/docs/part3-runtime/ch07-memory/golang-garbage-collector/) + +### go里用过哪些设计模式 。[Go 语言垃圾收集器的实现原理](https://link.zhihu.com/?target=https%3A//draveness.me/golang/docs/part3-runtime/ch07-memory/golang-garbage-collector/)go里用过哪些设计模式 。 + +工厂模式:比如New()方法返回一个对象实例。 + +单例模式:比如日志系统,只需要一个实例就可以完成日志记录了。 + +更多的可以模式可以参考:[Go语言设计模式汇总](https://link.zhihu.com/?target=https%3A//www.cnblogs.com/Survivalist/p/11207789.html) + +### go的调试/分析工具用过哪些。 + +go的自带工具链相当丰富, + +* go cover : 测试代码覆盖率; +* godoc: 用于生成go文档; +* pprof:用于性能调优,针对cpu,内存和并发; +* race:用于竞争检测; + +### 进程被kill,如何保证所有goroutine顺利退出 + +goroutine监听SIGKILL信号,一旦接收到SIGKILL,则立刻退出。可采用select方法。 + +### 说说context包的作用?你用过哪些,原理知道吗? + +`context`可以用来在`goroutine`之间传递上下文信息,相同的`context`可以传递给运行在不同`goroutine`中的函数,上下文对于多个`goroutine`同时使用是安全的,`context`包定义了上下文类型,可以使用`background`、`TODO`创建一个上下文,在函数调用链之间传播`context`,也可以使用`WithDeadline`、`WithTimeout`、`WithCancel` 或 `WithValue` 创建的修改副本替换它,听起来有点绕,其实总结起就是一句话:`context`的作用就是在不同的`goroutine`之间同步请求特定的数据、取消信号以及处理请求的截止日期。 + +关于context原理,可以参看:[小白也能看懂的context包详解:从入门到精通](https://link.zhihu.com/?target=https%3A//cloud.tencent.com/developer/article/1900658) + +### grpc为啥好,基本原理是什么,和http比呢 + +官方介绍:gRPC 是一个现代开源的**高性能远程过程调用** (RPC) 框架,可以在**任何环境**中运行。它可以通过对负载平衡、跟踪、健康检查和身份验证的可插拔支持有效地连接数据中心内和跨数据中心的服务。它也适用于分布式计算的最后一英里,将设备、移动应用程序和浏览器连接到后端服务。 + +区别: +- rpc是远程过程调用,就是本地去调用一个远程的函数,而http是通过 url和符合restful风格的数据包去发送和获取数据; +- rpc的一般使用的编解码协议更加高效,比如grpc使用protobuf编解码。而http的一般使用json进行编解码,数据相比rpc更加直观,但是数据包也更大,效率低下; +- rpc一般用在服务内部的相互调用,而http则用于和用户交互; +相似点: +都有类似的机制,例如grpc的metadata机制和http的头机制作用相似,而且web框架,和rpc框架中都有拦截器的概念。grpc使用的是http2.0协议。 +官网:[gRPC](https://link.zhihu.com/?target=https%3A//grpc.io/) + +### etcd怎么搭建的,具体怎么用的 + +### 熔断怎么做的 + +### 服务降级怎么搞 + +### 1亿条数据动态增长,取top10,怎么实现 + +### 进程挂了怎么办 + +### nginx配置过吗,有哪些注意的点 + +### 设计一个阻塞队列 + +### mq消费阻塞怎么办 + +### 性能没达到预期,有什么解决方案 + +* * * + +## 编程系列 + +### 一个10G的文件,里面全部是自然数,一行一个,乱序排列,对其排序。在32位机器上面完成,内存限制为 2G(bitmap原理知道吗?) + +首先,10G文件是不可能一次性放到内存里的。这类问题一般有两种解决方案: + +* 将10G文件分成多个小文件,分别排序,最后合并一个文件; +* 采用bitmap + +如果面试大数据类岗位,可能面试官就想考察你对Mapreduce熟悉程度,要采用第一种merge and sort。 + +如果是算法类岗位,就要考虑bitmap,但需要注意的是bitmap**不能对重复数据进行排序**。这里我们详细介绍一下: + +定量分析一下,32位机器自然数有2^32个,用一个bit来存放一个整数,那么所需的内存是,`2^32/(8<<20) = 512MB` ,这些数存放在文件中,一行一个,需要20G容量,所以题目问10G文件,只需要256MB内存就可以完成。 + +bitmap实现具体分为两步:插入一个数,和排序。 + +```go +type BitMap struct { + vec []byte + size int +} + +func New(size int) *BitMap { + return &BitMap{ + size: size, + vec: make([]byte, size), + } +} + +func (bm *BitMap) Set(num int) (ok bool, err error) { + if num/8 >= bm.size { + return false, errors.New("the num overflows the size of bitmap") + } + bm.vec[num/8] |= 1 << (num % 8) + return true, nil +} + +func (bm *BitMap) Exist(num int) bool { + if num/8 >= bm.size { + return false + } + return bm.vec[num/8]&(1<<(num%8)) > 0 +} + +func (bm *BitMap) Sort() (ret []int) { + ret = make([]int, 0) + for i := 0; i < (8 * bm.size); i++ { + if bm.Exist(i) { + ret = append(ret, i) + } + } + return +} +``` + +### 实现使用字符串函数名,调用函数。 + +思路:采用反射的Call方法实现。 + +```go +package main +import ( + "fmt" + "reflect" +) + +type Animal struct{ + +} + +func (a *Animal) Eat(){ + fmt.Println("Eat") +} + +func main(){ + a := Animal{} + reflect.ValueOf(&a).MethodByName("Eat").Call([]reflect.Value{}) + +} +``` + +### 负载均衡算法。(一致性哈希) + +```go +package main + +import ( + "fmt" + "sort" + "strconv" +) + +type HashFunc func(key []byte) uint32 + +type ConsistentHash struct { + hash HashFunc + hashvals []int + hashToKey map[int]string + virtualNum int +} + +func NewConsistentHash(virtualNum int, fn HashFunc) *ConsistentHash { + return &ConsistentHash{ + hash: fn, + virtualNum: virtualNum, + hashToKey: make(map[int]string), + } +} + +func (ch *ConsistentHash) AddNode(keys ...string) { + for _, k := range keys { + for i := 0; i < ch.virtualNum; i++ { + conv := strconv.Itoa(i) + hashval := int(ch.hash([]byte(conv + k))) + ch.hashvals = append(ch.hashvals, hashval) + ch.hashToKey[hashval] = k + } + } + sort.Ints(ch.hashvals) +} + +func (ch *ConsistentHash) GetNode(key string) string { + if len(ch.hashToKey) == 0 { + return "" + } + keyhash := int(ch.hash([]byte(key))) + id := sort.Search(len(ch.hashToKey), func(i int) bool { + return ch.hashvals[i] >= keyhash + }) + return ch.hashToKey[ch.hashvals[id%len(ch.hashvals)]] +} + +func main() { + ch := NewConsistentHash(3, func(key []byte) uint32 { + ret, _ := strconv.Atoi(string(key)) + return uint32(ret) + }) + ch.AddNode("1", "3", "5", "7") + testkeys := []string{"12", "4", "7", "8"} + for _, k := range testkeys { + fmt.Printf("k:%s,node:%s\n", k, ch.GetNode(k)) + } +} +``` + +### (Goroutine)有三个函数,分别打印"cat", "fish","dog"要求每一个函数都用一个goroutine,按照顺序打印100次。 + +此题目考察channel,用三个无缓冲channel,如果一个channel收到信号则通知下一个。 + +```go +package main + +import ( + "fmt" + "time" +) + +var dog = make(chan struct{}) +var cat = make(chan struct{}) +var fish = make(chan struct{}) + +func Dog() { + <-fish + fmt.Println("dog") + dog <- struct{}{} +} + +func Cat() { + <-dog + fmt.Println("cat") + cat <- struct{}{} +} + +func Fish() { + <-cat + fmt.Println("fish") + fish <- struct{}{} +} + +func main() { + for i := 0; i < 100; i++ { + go Dog() + go Cat() + go Fish() + } + fish <- struct{}{} + + time.Sleep(10 * time.Second) +} +``` + +### 两个协程交替打印10个字母和数字 + +思路:采用channel来协调goroutine之间顺序。 + +主线程一般要waitGroup等待协程退出,这里简化了一下直接sleep。 + +```go +package main + +import ( + "fmt" + "time" +) + +var word = make(chan struct{}, 1) +var num = make(chan struct{}, 1) + +func printNums() { + for i := 0; i < 10; i++ { + <-word + fmt.Println(1) + num <- struct{}{} + } +} +func printWords() { + for i := 0; i < 10; i++ { + <-num + fmt.Println("a") + word <- struct{}{} + } +} + +func main() { + num <- struct{}{} + go printNums() + go printWords() + time.Sleep(time.Second * 1) +} +``` + +代码: + + [@中二的灰太狼](https://www.zhihu.com/people/2d6eba22144ae58788cef3ed60485e9d) + +### 启动 2个groutine 2秒后取消, 第一个协程1秒执行完,第二个协程3秒执行完。 + +思路:采用`ctx, _ := context.WithTimeout(context.Background(), time.Second*2)`实现2s取消。协程执行完后通过channel通知,是否超时。 + +```go +package main + +import ( + "context" + "fmt" + "time" +) + +func f1(in chan struct{}) { + + time.Sleep(1 * time.Second) + in <- struct{}{} + +} + +func f2(in chan struct{}) { + time.Sleep(3 * time.Second) + in <- struct{}{} +} + +func main() { + ch1 := make(chan struct{}) + ch2 := make(chan struct{}) + ctx, _ := context.WithTimeout(context.Background(), 2*time.Second) + + go func() { + go f1(ch1) + select { + case <-ctx.Done(): + fmt.Println("f1 timeout") + break + case <-ch1: + fmt.Println("f1 done") + } + }() + + go func() { + go f2(ch2) + select { + case <-ctx.Done(): + fmt.Println("f2 timeout") + break + case <-ch2: + fmt.Println("f2 done") + } + }() + time.Sleep(time.Second * 5) +} +``` + +代码: + + [@中二的灰太狼](https://www.zhihu.com/people/2d6eba22144ae58788cef3ed60485e9d) + +### 当select监控多个chan同时到达就绪态时,如何先执行某个任务? + +可以在子case再加一个for select语句。 + +```go +func priority_select(ch1, ch2 <-chan string) { + for { + select { + case val := <-ch1: + fmt.Println(val) + case val2 := <-ch2: + priority: + for { + select { + case val1 := <-ch1: + fmt.Println(val1) + + default: + break priority + } + } + fmt.Println(val2) + } + } + +} +``` \ No newline at end of file diff --git "a/docs/golang/\351\235\242\350\257\225\351\242\230/test.md" "b/docs/golang/\351\235\242\350\257\225\351\242\230/test.md" new file mode 100644 index 0000000..34eb5a3 --- /dev/null +++ "b/docs/golang/\351\235\242\350\257\225\351\242\230/test.md" @@ -0,0 +1,4279 @@ + +### 文章目录 + +* [SpringBoot](https://www.pudn.com/news/62d0d14055398e076b87721e.html#SpringBoot_0) +* * [1、SpringBoot入门](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1SpringBoot_7) + * * [1.1、Spring程序与SpringBoot程序对比](https://www.pudn.com/news/62d0d14055398e076b87721e.html#11SpringSpringBoot_9) + * [1.2、parent](https://www.pudn.com/news/62d0d14055398e076b87721e.html#12parent_13) + * [1.3、引导类](https://www.pudn.com/news/62d0d14055398e076b87721e.html#13_36) + * [1.4、内置Tomcat](https://www.pudn.com/news/62d0d14055398e076b87721e.html#14Tomcat_59) + * [2、Rest风格](https://www.pudn.com/news/62d0d14055398e076b87721e.html#2Rest_75) + * * [2.1、什么是Rest](https://www.pudn.com/news/62d0d14055398e076b87721e.html#21Rest_77) + * [2.2、Rest入门案例](https://www.pudn.com/news/62d0d14055398e076b87721e.html#22Rest_114) + * [2.3、Restful快速开发](https://www.pudn.com/news/62d0d14055398e076b87721e.html#23Restful_153) + * [3、配置文档](https://www.pudn.com/news/62d0d14055398e076b87721e.html#3_163) + * [3.1、基础配置](https://www.pudn.com/news/62d0d14055398e076b87721e.html#31_165) + * [3.2、配置文件类型](https://www.pudn.com/news/62d0d14055398e076b87721e.html#32_191) + * [3.3、配置文件加载优先级](https://www.pudn.com/news/62d0d14055398e076b87721e.html#33_221) + * [3.4、yaml数据格式](https://www.pudn.com/news/62d0d14055398e076b87721e.html#34yaml_230) + * [数据类型](https://www.pudn.com/news/62d0d14055398e076b87721e.html#_262) + * [3.5、读取yaml单一属性数据](https://www.pudn.com/news/62d0d14055398e076b87721e.html#35yaml_321) + * [3.6、yaml文件中的变量应用](https://www.pudn.com/news/62d0d14055398e076b87721e.html#36yaml_353) + * [3.7、读取yaml全部属性数据](https://www.pudn.com/news/62d0d14055398e076b87721e.html#37yaml_367) + * [3.8、读取yaml应用类型属性数据](https://www.pudn.com/news/62d0d14055398e076b87721e.html#38yaml_376) + * [4、SpringBoot整合JUnit](https://www.pudn.com/news/62d0d14055398e076b87721e.html#4SpringBootJUnit_430) + * * [4.1、整合JUnit](https://www.pudn.com/news/62d0d14055398e076b87721e.html#41JUnit_432) + * [4.2、整合JUnit——classes属性](https://www.pudn.com/news/62d0d14055398e076b87721e.html#42JUnitclasses_473) + * [5、SpringBoot整合MyBatis、MyBatisPlus](https://www.pudn.com/news/62d0d14055398e076b87721e.html#5SpringBootMyBatisMyBatisPlus_499) + * * [5.1、整合MyBatis](https://www.pudn.com/news/62d0d14055398e076b87721e.html#51MyBatis_501) + * [5.2、常见问题处理](https://www.pudn.com/news/62d0d14055398e076b87721e.html#52_597) + * [5.3、整合MyBatisPlus](https://www.pudn.com/news/62d0d14055398e076b87721e.html#53MyBatisPlus_614) + * [6、SpringBoot整合Druid](https://www.pudn.com/news/62d0d14055398e076b87721e.html#6SpringBootDruid_674) + * [7、SSMP](https://www.pudn.com/news/62d0d14055398e076b87721e.html#7SSMP_715) + * * [7.1、数据配置](https://www.pudn.com/news/62d0d14055398e076b87721e.html#71_717) + * [7.2、分页](https://www.pudn.com/news/62d0d14055398e076b87721e.html#72_934) + * [7.3、数据层标准开发](https://www.pudn.com/news/62d0d14055398e076b87721e.html#73_994) + * [7.4、业务层标准开发(基础CRUD)](https://www.pudn.com/news/62d0d14055398e076b87721e.html#74CRUD_1031) + * [7.5、业务层快速开发(基于MyBatisPlus构建)](https://www.pudn.com/news/62d0d14055398e076b87721e.html#75MyBatisPlus_1158) + * [7.7、表现层标准开发](https://www.pudn.com/news/62d0d14055398e076b87721e.html#77_1285) + * [7.8、表现层数据一致性处理(R对象)](https://www.pudn.com/news/62d0d14055398e076b87721e.html#78R_1357) + * [8、Springboot工程打包与运行](https://www.pudn.com/news/62d0d14055398e076b87721e.html#8Springboot_1448) + * * [8.1、程序为什么要打包](https://www.pudn.com/news/62d0d14055398e076b87721e.html#81_1450) + * [8.2、SpringBoot项目快速启动(Windows版)](https://www.pudn.com/news/62d0d14055398e076b87721e.html#82SpringBootWindows_1456) + * [8.3、打包插件](https://www.pudn.com/news/62d0d14055398e076b87721e.html#83_1511) + * [8.4、Boot工程快速启动](https://www.pudn.com/news/62d0d14055398e076b87721e.html#84Boot_1580) + * [8.6、临时属性](https://www.pudn.com/news/62d0d14055398e076b87721e.html#86_1635) + * * [8.6.1、临时属性](https://www.pudn.com/news/62d0d14055398e076b87721e.html#861_1637) + * [8.6.2、开发环境](https://www.pudn.com/news/62d0d14055398e076b87721e.html#862_1657) + * [8.7、配置环境](https://www.pudn.com/news/62d0d14055398e076b87721e.html#87_1688) + * * [8.7.1、配置文件分类](https://www.pudn.com/news/62d0d14055398e076b87721e.html#871_1691) + * [8.7.2、自定义配置文件](https://www.pudn.com/news/62d0d14055398e076b87721e.html#872_1711) + * [8.7.3、多环境开发(yaml)](https://www.pudn.com/news/62d0d14055398e076b87721e.html#873yaml_1745) + * [8.7.4、多环境开发文件(yaml)](https://www.pudn.com/news/62d0d14055398e076b87721e.html#874yaml_1783) + * [8.7.5、多环境分组管理](https://www.pudn.com/news/62d0d14055398e076b87721e.html#875_1821) + * [8.7.6、多环境开发控制](https://www.pudn.com/news/62d0d14055398e076b87721e.html#876_1871) + * [9、日志](https://www.pudn.com/news/62d0d14055398e076b87721e.html#9_1923) + * * [9.1、日志基础操作](https://www.pudn.com/news/62d0d14055398e076b87721e.html#91_1925) + * [9.2、快速创建日志对象](https://www.pudn.com/news/62d0d14055398e076b87721e.html#92_2006) + * [9.3、日志输出格式控制](https://www.pudn.com/news/62d0d14055398e076b87721e.html#93_2034) + * [9.4、文件记录日志](https://www.pudn.com/news/62d0d14055398e076b87721e.html#94_2061) + * [10、热部署](https://www.pudn.com/news/62d0d14055398e076b87721e.html#10_2084) + * [11、属性绑定](https://www.pudn.com/news/62d0d14055398e076b87721e.html#11_2113) + * [12、常用计量单位应用](https://www.pudn.com/news/62d0d14055398e076b87721e.html#12_2158) + * [13、测试类](https://www.pudn.com/news/62d0d14055398e076b87721e.html#13_2230) + * * [13.1、加载专用属性](https://www.pudn.com/news/62d0d14055398e076b87721e.html#131_2232) + * [13.2、加载专用类](https://www.pudn.com/news/62d0d14055398e076b87721e.html#132_2262) + * [13.3、业务层测试事务回滚](https://www.pudn.com/news/62d0d14055398e076b87721e.html#133_2381) + * [13.4、测试用例数据设定](https://www.pudn.com/news/62d0d14055398e076b87721e.html#134_2404) + * [14、数据层解决方案](https://www.pudn.com/news/62d0d14055398e076b87721e.html#14_2431) + * * [14.1、SQL](https://www.pudn.com/news/62d0d14055398e076b87721e.html#141SQL_2433) + * [14.2、MongoDB](https://www.pudn.com/news/62d0d14055398e076b87721e.html#142MongoDB_2641) + * * [14.2.1、MongoDB的使用](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1421MongoDB_2647) + * [14.2.2、MongoDB可视化客户端](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1422MongoDB_2671) + * [14.2.3、Springboot集成MongoDB](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1423SpringbootMongoDB_2689) + * [14.3、ElasticSearch(ES)](https://www.pudn.com/news/62d0d14055398e076b87721e.html#143ElasticSearchES_2730) + * * [14.3.1、ES下载](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1431ES_2738) + * [14.3.2、ES索引、分词器](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1432ES_2750) + * [14.3.3、文档操作(增删改查)](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1433_2792) + * [14.3.4、Springboot集成ES](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1434SpringbootES_2850) + * [14.3.5、索引](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1435_2939) + * [15、缓存](https://www.pudn.com/news/62d0d14055398e076b87721e.html#15_3057) + * * [15.1、缓存简介](https://www.pudn.com/news/62d0d14055398e076b87721e.html#151_3059) + * [15.2、缓存使用](https://www.pudn.com/news/62d0d14055398e076b87721e.html#152_3073) + * [15.3、其他缓存](https://www.pudn.com/news/62d0d14055398e076b87721e.html#153_3115) + * [15.4、缓存使用案例——手机验证码](https://www.pudn.com/news/62d0d14055398e076b87721e.html#154_3132) + * * [15.4.1、Cache](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1541Cache_3146) + * [15.4.2、Ehcache](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1542Ehcache_3217) + * [15.4.3、Redis](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1543Redis_3276) + * [15.4.4、memcached](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1544memcached_3316) + * [15.4.5、jetcache](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1545jetcache_3445) + * [15.4.6、j2cache](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1546j2cache_3652) + * [16、定时](https://www.pudn.com/news/62d0d14055398e076b87721e.html#16_3730) + * * [16.1、SpringBoot整合Quartz](https://www.pudn.com/news/62d0d14055398e076b87721e.html#161SpringBootQuartz_3745) + * [16.2、Spring Task](https://www.pudn.com/news/62d0d14055398e076b87721e.html#162Spring_Task_3795) + * [16.3、SpringBoot整合JavaMail](https://www.pudn.com/news/62d0d14055398e076b87721e.html#163SpringBootJavaMail_3841) + * [17、消息](https://www.pudn.com/news/62d0d14055398e076b87721e.html#17_3925) + * * [17.1、JMS](https://www.pudn.com/news/62d0d14055398e076b87721e.html#171JMS_3935) + * [17.2、AMQP](https://www.pudn.com/news/62d0d14055398e076b87721e.html#172AMQP_3954) + * [17.3、MQTT](https://www.pudn.com/news/62d0d14055398e076b87721e.html#173MQTT_3970) + * [17.4、Kafka](https://www.pudn.com/news/62d0d14055398e076b87721e.html#174Kafka_3976) + * [17.5、消息案例](https://www.pudn.com/news/62d0d14055398e076b87721e.html#175_3982) + * [17.6、ActiveMQ](https://www.pudn.com/news/62d0d14055398e076b87721e.html#176ActiveMQ_4000) + * * [17.6.1、SpringBoot整合ActiveMQ](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1761SpringBootActiveMQ_4018) + * [17.7、RabbitMQ](https://www.pudn.com/news/62d0d14055398e076b87721e.html#177RabbitMQ_4104) + * * [17.7.1、SpringBoot整合RabbitMQ](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1771SpringBootRabbitMQ_4148) + * [17.8、RocketMQ](https://www.pudn.com/news/62d0d14055398e076b87721e.html#178RocketMQ_4323) + * * [17.8.1、SpringBoot整合RocketMQ](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1781SpringBootRocketMQ_4358) + * [17.9、Kafka](https://www.pudn.com/news/62d0d14055398e076b87721e.html#179Kafka_4434) + * * [17.9.1、SpringBoot整合Kafka](https://www.pudn.com/news/62d0d14055398e076b87721e.html#1791SpringBootKafka_4476) + * [18、监控](https://www.pudn.com/news/62d0d14055398e076b87721e.html#18_4525) + * * [18.1、简介](https://www.pudn.com/news/62d0d14055398e076b87721e.html#181_4527) + * [18.2、可视化监控平台](https://www.pudn.com/news/62d0d14055398e076b87721e.html#182_4545) + * [18.3、监控原理](https://www.pudn.com/news/62d0d14055398e076b87721e.html#183_4658) + * [18.4、自定义监控指标](https://www.pudn.com/news/62d0d14055398e076b87721e.html#184_4703) + * [18.3、监控原理](https://www.pudn.com/news/62d0d14055398e076b87721e.html#183_4879) + * [18.4、自定义监控指标](https://www.pudn.com/news/62d0d14055398e076b87721e.html#184_4928) + +## 1、SpringBoot入门 + +### 1.1、Spring程序与SpringBoot程序对比 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/43e3ba472de049f9a489b0773ecf88a7.png) + +### 1.2、parent + +![在这里插入图片描述](https://img-blog.csdnimg.cn/51971e3edda741028356f6cd3947d4a2.png) + +* starter + + SpringBoot中常见项目名称,定义了当前项目使用的所有依赖坐标,以达到减少依赖配置的目的 + +* parent + + 所有SpringBoot项目要继承的项目,定义了若干个坐标版本号(依赖管理,而非依赖),以达到减少依赖冲突的目的 + + spring-boot-starter-parent各版本间存在着诸多坐标版本不同 + +* 实际开发 + + 使用任意坐标时,仅书写GAV(groupId, artifactId, version)中的G和A,V由SpringBoot提供,除非SpringBoot未提供对应版本V + + 如发生坐标错误,再指定Version(要小心版本冲突) + +### 1.3、引导类 + +* 启动方式 + + @SpringBootApplication + public class Springboot0101QuickstartApplication { + + public static void main(String[] args) { + ConfigurableApplicationContext ctx = SpringApplication.run(Springboot0101QuickstartApplication.class, args); + //获取bean对象 + BookController bean = ctx.getBean(BookController.class); + System.out.println("bean======>" + bean); + } + } + + SpringBoot的引导类是Boot工程的执行入口,运行main方法就可以启动项目 + + SpringBoot工程运行后初始化Spring容器,扫描引导类所在包加载bean + +### 1.4、内置Tomcat + +![在这里插入图片描述](https://img-blog.csdnimg.cn/17e3b9fb27e745dab90d4f73c4168c27.png) + +* Jetty比Tomcat更轻量级,可扩展性更强(相较于Tomcat),谷歌应用引擎(GAE)已经全面切换为Jetty + +* 内置服务器 + + tomcat(默认) apache出品,粉丝多,应用面广,负载了若干较重的组件 + + jetty 更轻量级,负载性能远不及tomcat + + undertow undertow,负载性能勉强跑赢tomcat + +## 2、Rest风格 + +### 2.1、什么是Rest + +1. 什么是 rest : + + REST(Representational State Transfer)表现形式状态转换 + + 传统风格资源描述形式 + http://localhost/user/getById?id=1 (得到id为1的用户) + http://localhost/user/saveUser (保存用户) + + REST风格描述形式 + http://localhost/user/1 (得到id为1的用户) + http://localhost/user (保存用户) + +2. 优点: + + 隐藏资源的访问行为, 无法通过地址得知对资源是何种操作 + 书写简化 + +3. 按照REST风格访问资源时使用行为动作区分对资源进行了何种操作 + + GET 用来获取资源,POST 用来新建资源,PUT 用来更新资源,DELETE 用来删除资源 + + http://localhost/users 查询全部用户信息 GET (查询) + http://localhost/users/1 查询指定用户信息 GET (查询) + http://localhost/users 添加用户信息 POST (新增/保存) + http://localhost/users 修改用户信息 PUT (修改/更新) + http://localhost/users/1 删除用户信息 DELETE (删除) + +注意: + +上述行为是约定方式,约定不是规范,可以打破,所以称REST风格,而不是REST规范 +描述模块的名称通常使用复数,也就是加s的格式描述,表示此类资源,而非单个资源,例如: users、 + +1. 根据REST风格对资源进行访问称为**RESTful** + +### 2.2、Rest入门案例 + +步骤: + +①设定http请求动作(动词) + +使用 @RequestMapping 注解的 method 属性声明请求的方式 + +使用 @RequestBody 注解 获取请求体内容。直接使用得到是 key=value&key=value…结构的数据。get 请求方式不适用。 + +使用@ResponseBody 注解实现将 controller 方法返回对象转换为 json 响应给客户端。 + +@RequestMapping(value=“/users”,method=RequestMethod.POST) + +![在这里插入图片描述](https://img-blog.csdnimg.cn/0a4ded7bb220415bbcd779c230f2d94f.png) + +②:设定请求参数(路径变量) + +使用`@PathVariable` 用于绑定 url 中的占位符。例如:请求 url 中 /delete/`{id}`,这个`{id}`就是 url 占位符。 +![在这里插入图片描述](https://img-blog.csdnimg.cn/fd8f1fc9f45b429ebabf148df521d3e0.png) + +@RequestMapping + +![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HhzZsWGO-1657811363433)(SpringBoot.assets/image-20220312164532116.png)]](https://img-blog.csdnimg.cn/7fb5465b71a346d4b2e8920493c5e36c.png) + +@PathVariable + +![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yaepxdng-1657811363434)(SpringBoot.assets/image-20220312164552778.png)]](https://img-blog.csdnimg.cn/a9c22e3367a7480ea219baaa21de868e.png) + +@RequestBody @RequestParam @PathVariable +![在这里插入图片描述](https://img-blog.csdnimg.cn/1ce13dc308fe4147981ce26622eec1ca.png) + +### 2.3、Restful快速开发 + +使用 `@RestController` 注解开发 RESTful 风格 +![在这里插入图片描述](https://img-blog.csdnimg.cn/9e782ebc0c8b458ea30bd3132f9e7f57.png) + +使用 @GetMapping @PostMapping @PutMapping @DeleteMapping 简化 `@RequestMapping` 注解开发 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/4b55439a2e094dae84f37f2f59f7b0f2.png) + +### 3、配置文档 + +### 3.1、基础配置 + +1. 修改配置 + 修改服务器端口 + server.port=80 + 关闭运行日志图标(banner) + spring.main.banner-mode=off + 设置日志相关 + logging.level.root=debug + +# 服务器端口配置 +server.port=80 + +# 修改banner +# spring.main.banner-mode=off +# spring.banner.image.location=logo.png + +# 日志 +logging.level.root=info + +1. SpringBoot内置属性查询 + https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#application-properties + 官方文档中参考文档第一项:Application Propertie + +### 3.2、配置文件类型 + +* 配置文件格式 + ![在这里插入图片描述](https://img-blog.csdnimg.cn/cf1d1024f92142d69b1b1e30038a9434.png) + +* SpringBoot提供了多种属性配置方式 + + application.properties + + server.port=80 + + application.yml + + server: + port: 81 + + application.yaml + + server: + port: 82 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/aa8415068c074278ad3085414efc96c8.png) + +### 3.3、配置文件加载优先级 + +* SpringBoot配置文件加载顺序 + application.properties > application.yml > application.yaml +* 常用配置文件种类 + application.yml + +### 3.4、yaml数据格式 + +yaml + +YAML(YAML Ain’t Markup Language),一种数据序列化格式 + +优点: + 容易阅读 + 容易与脚本语言交互 + 以数据为核心,重数据轻格式 + +YAML文件扩展名 + .yml(主流) + .yaml + +yaml语法规则 +基本语法 + +key: value -> value 前面一定要有空格 +大小写敏感 +属性层级关系使用多行描述,每行结尾使用冒号结束 +使用缩进表示层级关系,同层级左侧对齐,只允许使用空格(不允许使用Tab键) +属性值前面添加空格(属性名与属性值之间使用冒号+空格作为分隔) +# 表示注释 +核心规则:数据前面要加空格与冒号隔开 server: + servlet: + context-path: /hello + port: 82 +### 数据类型 + +* 字面值表示方式 + ![在这里插入图片描述](https://img-blog.csdnimg.cn/8f1261fad4f842d8ab8e8bd591d4aaf6.png) + +# 字面值表示方式 + +boolean: TRUE #TRUE,true,True,FALSE,false , False 均可 +float: 3.14 #6.8523015e+5 # 支持科学计数法 +int: 123 #0b1010_0111_0100_1010_1110 # 支持二进制、八进制、十六进制 +# null: ~ # 使用 ~ 表示 null +string: HelloWorld # 字符串可以直接书写 +string2: "Hello World" # 可以使用双引号包裹特殊字符 +date: 2018-02-17 # 日期必须使用 yyyy-MM-dd 格式 +datetime: 2018-02-17T15:02:31+08:00 # 时间和日期之间使用 T 连接,最后使用 + 代表时区 + +* 数组表示方式:在属性名书写位置的下方使用减号作为数据开始符号,每行书写一个数据,减号与数据间空格分隔 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/450bae74c7904daf8c26854788698b18.png) + +subject: + - Java + - 前端 + - 大数据 + +enterprise: + name: zhangsan + age: 16 + +subject2: + - Java + - 前端 + - 大数据 +likes: [王者荣耀,刺激战场] # 数组书写缩略格式 + +users: # 对象数组格式 + - name: Tom + age: 4 + + - name: Jerry + age: 5 +users2: # 对象数组格式二 + - + name: Tom + age: 4 + - + name: Jerry + age: 5 + +# 对象数组缩略格式 +users3: [ { name:Tom , age:4 } , { name:Jerry , age:5 } ] +### 3.5、读取yaml单一属性数据 + +* 使用@Value读取单个数据,属性名引用方式:${一级属性名.二级属性名……} + ![在这里插入图片描述](https://img-blog.csdnimg.cn/332e7c2fc82449e29984c7caaa3f8c31.png) + + @Value("${country}") + private String country1; + + @Value("${user.age}") + private String age1; + + @Value("${likes[1]}") + private String likes1; + + @Value("${users[1].name}") + private String name1; + + @GetMapping + public String getById() { + System.out.println("springboot is running2..."); + System.out.println("country1=>" + country1); + System.out.println("age1=>" + age1); + System.out.println("likes1=>" + likes1); + System.out.println("name1=>" + name1); + return "springboot is running2..."; + } +### 3.6、yaml文件中的变量应用 + +* 在配置文件中可以使用属性名引用方式引用属性 + ![在这里插入图片描述](https://img-blog.csdnimg.cn/519e7e02e15b4ae996a53faa03bfb44b.png) + ![在这里插入图片描述](https://img-blog.csdnimg.cn/d20085f899a84d9c81365897c8c01775.png) + +* 属性值中如果出现转移字符,需要使用双引号包裹 + + lesson: "Spring\tboot\nlesson" + +### 3.7、读取yaml全部属性数据 + +* 封装全部数据到Environment对象 +* 注意 要导这个 包 +* **import org.springframework.core.env.Environment** + ![在这里插入图片描述](https://img-blog.csdnimg.cn/e8e576fdf9244d9692f6dd8d92c18602.png) + ![在这里插入图片描述](https://img-blog.csdnimg.cn/0d64b9e6621a424999400a063743cb6d.png) + +### 3.8、读取yaml应用类型属性数据 + +* 自定义对象封装指定数据 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/feb05e9432c0461e87474bde8cbf5db1.png) + +* 自定义对象封装指定数据的作用 + ![在这里插入图片描述](https://img-blog.csdnimg.cn/4ffb01bae1ba4e088750c5882ca71094.png) + +# 创建类,用于封装下面的数据 +# 由spring帮我们去加载数据到对象中,一定要告诉spring加载这组信息 +# 使用时候从spring中直接获取信息使用 + +datasource: + driver: com.mysql.jdbc.Driver + url: jdbc:mysql://localhost/springboot_db + username: root + password: root666123 //1.定义数据模型封装yaml文件中对应的数据 +//2.定义为spring管控的bean +@Component +//3.指定加载的数据 +@ConfigurationProperties(prefix = "datasource") +public class MyDataSource { + + private String driver; + private String url; + private String username; + private String password; + + //省略get/set/tostring 方法 +} + +使用自动装配封装指定数据 + + @Autowired + private MyDataSource myDataSource; + +输出查看 + +System.out.println(myDataSource); +## 4、SpringBoot整合JUnit + +### 4.1、整合JUnit + +* 添加Junit的起步依赖 Spring Initializr 创建时自带 + + + + org.springframework.boot + spring-boot-starter-test + test + +* SpringBoot整合JUnit + + @SpringBootTest + class Springboot07JunitApplicationTests { + @Autowired + private BookService bookService; + @Test + public void testSave(){ + bookService.save(); + } + } +* @SpringBootTest + 名称:@SpringBootTest + 类型:测试类注解 + 位置:测试类定义上方 + 作用:设置JUnit加载的SpringBoot启动类 + 范例: + + @SpringBootTest + class Springboot05JUnitApplicationTests {} + +### 4.2、整合JUnit——classes属性 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/11b8072d4158454d829dfaece9287625.png) + +@SpringBootTest(classes = Springboot04JunitApplication.class) +//@ContextConfiguration(classes = Springboot04JunitApplication.class) +class Springboot04JunitApplicationTests { + //1.注入你要测试的对象 + @Autowired + private BookDao bookDao; + + @Test + void contextLoads() { + //2.执行要测试的对象对应的方法 + bookDao.save(); + System.out.println("two..."); + } +} + +注意: + +* 如果测试类在SpringBoot启动类的包或子包中,可以省略启动类的设置,也就是省略classes的设定 + +## 5、SpringBoot整合MyBatis、MyBatisPlus + +### 5.1、整合MyBatis + +①:创建新模块,选择Spring初始化,并配置模块相关基础信息 + +②:选择当前模块需要使用的技术集(MyBatis、MySQL) + +③:设置数据源参数 + +#DB Configuration: +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/springboot_db + username: root + password: 123456 + +④:创建user表 +在 springboot_db 数据库中创建 user 表 + +-- ---------------------------- +-- Table structure for `user` +-- ---------------------------- +DROP TABLE IF EXISTS `user`; +CREATE TABLE `user` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `username` varchar(50) DEFAULT NULL, + `password` varchar(50) DEFAULT NULL, + `name` varchar(50) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8; + +-- ---------------------------- +-- Records of user +-- ---------------------------- +INSERT INTO `user` VALUES ('1', 'zhangsan', '123', '张三'); +INSERT INTO `user` VALUES ('2', 'lisi', '123', '李四'); + +⑤:创建实体Bean + +public class User { + // 主键 + private Long id; + // 用户名 + private String username; + // 密码 + private String password; + // 姓名 + private String name; + + //此处省略getter,setter,toString方法 .. .. + +} + +⑥: 定义数据层接口与映射配置 + +@Mapper +public interface UserDao { + + @Select("select * from user") + public List getAll(); +} + +⑦:测试类中注入dao接口,测试功能 + +@SpringBootTest +class Springboot05MybatisApplicationTests { + + @Autowired + private UserDao userDao; + + @Test + void contextLoads() { + List userList = userDao.getAll(); + System.out.println(userList); + } + +} + +⑧:运行如下 + +[User{id=1, username='zhangsan', password='123', name='张三'}, User{id=2, username='lisi', password='123', name='李四'}] +### 5.2、常见问题处理 + +SpringBoot版本低于2.4.3(不含),Mysql驱动版本大于8.0时,需要在url连接串中配置时区 + +jdbc:mysql://localhost:3306/springboot_db?serverTimezone=UTC + +或在MySQL数据库端配置时区解决此问题 + +1.MySQL 8.X驱动强制要求设置时区 + +修改url,添加serverTimezone设定 +修改MySQL数据库配置(略) + +2.驱动类过时,提醒更换为com.mysql.cj.jdbc.Driver + +### 5.3、整合MyBatisPlus + +①:手动添加SpringBoot整合MyBatis-Plus的坐标,可以通过mvnrepository获取 + + + com.baomidou + mybatis-plus-boot-starter + 3.4.3 + + +注意事项: 由于SpringBoot中未收录MyBatis-Plus的坐标版本,需要指定对应的Version + +②:定义数据层接口与映射配置,继承BaseMapper + +@Mapper +public interface UserDao extends BaseMapper { + +} + +③:其他同SpringBoot整合MyBatis +(略) + +④:测试类中注入dao接口,测试功能 + +@SpringBootTest +class Springboot06MybatisPlusApplicationTests { + + @Autowired + private UserDao userDao; + + @Test + void contextLoads() { + List users = userDao.selectList(null); + System.out.println(users); + } + +} + +⑤: 运行如下: + +[User{id=1, username='zhangsan', password='123', name='张三'}, User{id=2, username='lisi', password='123', name='李四'}] + +注意: 如果你的数据库表有前缀要在 application.yml 添加如下配制 + +#设置Mp相关的配置 +mybatis-plus: + global-config: + db-config: + table-prefix: tbl_ +### 6、SpringBoot整合Druid + +①: 导入Druid对应的starter + + + com.alibaba + druid-spring-boot-starter + 1.2.6 + #DB Configuration: +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/springboot_db?serverTimezone=UTC + username: root + password: Lemon + type: com.alibaba.druid.pool.DruidDataSource + +②: 指定数据源类型 (这种方式只需导入一个 Druid 的坐标) + +spring: + datasource: + druid: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/springboot_db?serverTimezone=UTC + username: root + password: 123456 + +或者 变更Druid的配置方式(推荐) 这种方式需要导入 Druid对应的starter + +## 7、SSMP + +### 7.1、数据配置 + +1\. 案例实现方案分析 + 实体类开发————使用Lombok快速制作实体类 + Dao开发————整合MyBatisPlus,制作数据层测试类 + Service开发————基于MyBatisPlus进行增量开发,制作业务层测试类 + Controller开发————基于Restful开发,使用PostMan测试接口功能 + Controller开发————前后端开发协议制作 + 页面开发————基于VUE+ElementUI制作,前后端联调,页面数据处理,页面消息处理 + 列表、新增、修改、删除、分页、查询 + 项目异常处理 + 按条件查询————页面功能调整、Controller修正功能、Service修正功能 +2\. SSMP案例制作流程解析 + 先开发基础CRUD功能,做一层测一层 + 调通页面,确认异步提交成功后,制作所有功能 + 添加分页功能与查询功能 DROP TABLE IF EXISTS `tbl_book`; +CREATE TABLE `tbl_book` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `type` varchar(20) DEFAULT NULL, + `name` varchar(50) DEFAULT NULL, + `description` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8; + +-- ---------------------------- +-- Records of tbl_book +-- ---------------------------- +INSERT INTO `tbl_book` VALUES ('1', '计算机理论', 'Spring实战第5版', 'Spring入门经典教程,深入理解Spring原理技术内幕'); +INSERT INTO `tbl_book` VALUES ('2', '计算机理论', 'Spring 5核心原理与30个类手写实战', '十年沉淀之作,写Spring精华思想'); +INSERT INTO `tbl_book` VALUES ('3', '计算机理论', 'Spring 5设计模式', '深入Spring源码剖析Spring源码中蕴含的10大设计模式'); +INSERT INTO `tbl_book` VALUES ('4', '计算机理论', 'Spring MVC+ MyBatis开发从入门到项目实战', '全方位解析面向Web应用的轻量级框架,带你成为Spring MVC开发高手'); +INSERT INTO `tbl_book` VALUES ('5', '计算机理论', '轻量级Java Web企业应用实战', '源码级剖析Spring框架,适合已掌握Java基础的读者'); +INSERT INTO `tbl_book` VALUES ('6', '计算机理论', 'Java核心技术卷|基础知识(原书第11版)', 'Core Java第11版,Jolt大奖获奖作品,针对Java SE9、10、 11全面更新'); +INSERT INTO `tbl_book` VALUES ('7', '计算机理论', '深入理解Java虚拟机', '5个维度全面剖析JVM,面试知识点全覆盖'); +INSERT INTO `tbl_book` VALUES ('8', '计算机理论', 'Java编程思想(第4版)', 'Java学习必读经典殿堂级著作!赢得了全球程序员的广泛赞誉'); +INSERT INTO `tbl_book` VALUES ('9', '计算机理论', '零基础学Java (全彩版)', '零基础自学编程的入门]图书,由浅入深,详解Java语言的编程思想和核心技术'); +INSERT INTO `tbl_book` VALUES ('10', '市场营销', '直播就该这么做:主播高效沟通实战指南', '李子柒、李佳琦、薇娅成长为网红的秘密都在书中'); +INSERT INTO `tbl_book` VALUES ('11', '市场营销', '直播销讲实战一本通', '和秋叶一起学系列网络营销书籍'); +INSERT INTO `tbl_book` VALUES ('12', '市场营销', '直播带货:淘宝、天猫直播从新手到高手', '一本教你如何玩转直播的书, 10堂课轻松实现带货月入3W+'); + + org.springframework.boot + spring-boot-starter-web + + + + mysql + mysql-connector-java + runtime + + + + com.baomidou + mybatis-plus-boot-starter + 3.4.3 + + + + com.alibaba + druid-spring-boot-starter + 1.2.6 + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.projectlombok + lombok + + + + com.baomidou + mybatis-plus-boot-starter + 3.4.3 + + + + com.alibaba + druid-spring-boot-starter + 1.2.6 + + + server: + port: 80 +# druid 数据源配制 +spring: + datasource: + druid: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/springboot?serverTimezone=UTC + username: root + password: Lemon + +# mybatis-plus +mybatis-plus: + global-config: + db-config: + table-prefix: tbl_ + id-type: auto # 主键策略 + +Book.class + +@Data +public class Book { + private Integer id; + private String type; + private String name; + private String description; +} + +BookDao.class + +@Mapper +public interface BookDao extends BaseMapper { + + /** + * 查询一个 + * 这是 Mybatis 开发 + * @param id + * @return + */ + @Select("select * from tbl_book where id = #{id}") + Book getById(Integer id); +} + +测试类 + +@SpringBootTest +public class BookDaoTestCase { + + @Autowired + private BookDao bookDao; + + @Test + void testGetById() { + System.out.println(bookDao.getById(1)); + System.out.println(bookDao.selectById(1)); + } + + @Test + void testSave() { + Book book = new Book(); + book.setType("测试数据123"); + book.setName("测试数据123"); + book.setDescription("测试数据123"); + bookDao.insert(book); + } + + @Test + void testUpdate() { + Book book = new Book(); + book.setId(13); + book.setType("测试数据asfd"); + book.setName("测试数据123"); + book.setDescription("测试数据123"); + bookDao.updateById(book); + } + + @Test + void testDelete() { + bookDao.deleteById(13); + } + + @Test + void testGetAll() { + System.out.println(bookDao.selectList(null)); + } + + @Test + void testGetPage() { + } + + @Test + void testGetBy() { + } +} + +开启MybatisPlus运行日志 + +# mybatis-plus +mybatis-plus: + global-config: + db-config: + table-prefix: tbl_ + id-type: auto # 主键策略 + configuration: + # 开启MyBatisPlus的日志 + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl +### 7.2、分页 + +分页操作需要设定分页对象IPage + +@Test +void testGetPage() { + IPage page = new Page(1, 5); + bookDao.selectPage(page, null); +} + +* IPage对象中封装了分页操作中的所有数据 + + 数据 + + 当前页码值 + + 每页数据总量 + + 最大页码值 + + 数据总量 + +* 分页操作是在MyBatisPlus的常规操作基础上增强得到,内部是动态的拼写SQL语句,因此需要增强对应的功能 + +使用MyBatisPlus拦截器实现 + +@Configuration +public class MybatisPlusConfig { + + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + //1\. 定义 Mp 拦截器 + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + //2\. 添加具体的拦截器 分页拦截器 + interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); + return interceptor; + } +} + +测试 + +@Test +void testGetPage() { + IPage page = new Page(1, 5); + bookDao.selectPage(page, null); + System.out.println(page.getCurrent()); + System.out.println(page.getSize()); + System.out.println(page.getPages()); + System.out.println(page.getTotal()); + System.out.println(page.getRecords()); +} +### 7.3、数据层标准开发 + +* 使用QueryWrapper对象封装查询条件,推荐使用LambdaQueryWrapper对象,所有查询操作封装成方法调用 + +@Test +void testGetBy2() { + LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>(); + lambdaQueryWrapper.like(Book::getName, "Spring"); + bookDao.selectList(lambdaQueryWrapper); +} @Test +void testGetBy() { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.like("name", "Spring"); + bookDao.selectList(queryWrapper); +} + +* 支持动态拼写查询条件 + +@Test +void testGetBy2() { + String name = "1"; + LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>(); + //if (name != null) lambdaQueryWrapper.like(Book::getName,name); + lambdaQueryWrapper.like(Strings.isNotEmpty(name), Book::getName, name); + bookDao.selectList(lambdaQueryWrapper); +} +### 7.4、业务层标准开发(基础CRUD) + +* Service层接口定义与数据层接口定义具有较大区别,不要混用 + selectByUserNameAndPassword(String username,String password); 数据层接口 + login(String username,String password); Service层接口 + +* 接口定义 + + public interface BookService { + + Boolean save(Book book); + + Boolean update(Book book); + + Boolean delete(Integer id); + + Book getById(Integer id); + + List getAll(); + + IPage getPage(int currentPage,int pageSize); + } +* 实现类定义 + +@Service +public class BookServiceImpl implements BookService { + + @Autowired + private BookDao bookDao; + + @Override + public Boolean save(Book book) { + return bookDao.insert(book) > 0; + } + + @Override + public Boolean update(Book book) { + return bookDao.updateById(book) > 0; + } + + @Override + public Boolean delete(Integer id) { + return bookDao.deleteById(id) > 0; + } + + @Override + public Book getById(Integer id) { + return bookDao.selectById(id); + } + + @Override + public List getAll() { + return bookDao.selectList(null); + } + + @Override + public IPage getPage(int currentPage, int pageSize) { + IPage page = new Page(currentPage, pageSize); + bookDao.selectPage(page, null); + return page; + } +} + +* 测试类定义 + +@SpringBootTest +public class BookServiceTestCase { + + @Autowired + private BookService bookService; + + @Test + void testGetById() { + System.out.println(bookService.getById(4)); + } + + @Test + void testSave() { + Book book = new Book(); + book.setType("测试数据123"); + book.setName("测试数据123"); + book.setDescription("测试数据123"); + bookService.save(book); + } + + @Test + void testUpdate() { + Book book = new Book(); + book.setId(14); + book.setType("测试数据asfd"); + book.setName("测试数据123"); + book.setDescription("测试数据123"); + bookService.update(book); + } + + @Test + void testDelete() { + bookService.delete(14); + } + + @Test + void testGetAll() { + System.out.println(bookService.getAll()); + } + + @Test + void testGetPage() { + IPage page = bookService.getPage(2, 5); + System.out.println(page.getCurrent()); + System.out.println(page.getSize()); + System.out.println(page.getPages()); + System.out.println(page.getTotal()); + System.out.println(page.getRecords()); + } +} +### 7.5、业务层快速开发(基于MyBatisPlus构建) + +* 快速开发方案 + + 使用MyBatisPlus提供有业务层通用接口(ISerivce)与业务层通用实现类(ServiceImpl) + + 在通用类基础上做功能重载或功能追加 + + 注意重载时不要覆盖原始操作,避免原始提供的功能丢失 + +* 接口定义 + +public interface IBookService extends IService { +} + +* 接口追加功能 + +public interface IBookService extends IService { + + // 追加的操作与原始操作通过名称区分,功能类似 + Boolean delete(Integer id); + + Boolean insert(Book book); + + Boolean modify(Book book); + + Book get(Integer id); +} + +![在这里插入图片描述](https://img-blog.csdnimg.cn/ac8db44e41344a03964803b29491c6a6.png) + +* 实现类定义 + +@Service +public class BookServiceImpl extends ServiceImpl implements IBookService { +} + +* 实现类追加功能 + +@Service +public class BookServiceImpl extends ServiceImpl implements IBookService { + + @Autowired + private BookDao bookDao; + + public Boolean insert(Book book) { + return bookDao.insert(book) > 0; + } + + public Boolean modify(Book book) { + return bookDao.updateById(book) > 0; + } + + public Boolean delete(Integer id) { + return bookDao.deleteById(id) > 0; + } + + public Book get(Integer id) { + return bookDao.selectById(id); + } +} + +* 测试类定义 + +@SpringBootTest +public class BookServiceTest { + + @Autowired + private IBookService bookService; + + @Test + void testGetById() { + System.out.println(bookService.getById(4)); + } + + @Test + void testSave() { + Book book = new Book(); + book.setType("测试数据123"); + book.setName("测试数据123"); + book.setDescription("测试数据123"); + bookService.save(book); + } + + @Test + void testUpdate() { + Book book = new Book(); + book.setId(14); + book.setType("==========="); + book.setName("测试数据123"); + book.setDescription("测试数据123"); + bookService.updateById(book); + } + + @Test + void testDelete() { + bookService.removeById(14); + } + + @Test + void testGetAll() { + System.out.println(bookService.list()); + } + + @Test + void testGetPage() { + IPage page = new Page<>(2, 5); + bookService.page(page); + System.out.println(page.getCurrent()); + System.out.println(page.getSize()); + System.out.println(page.getPages()); + System.out.println(page.getTotal()); + System.out.println(page.getRecords()); + } +} +### 7.7、表现层标准开发 + +* 基于Restful进行表现层接口开发 +* 使用Postman测试表现层接口功能 + +@RestController +@RequestMapping("/books") +public class BookController { + + @Autowired + private IBookService bookService; + + @GetMapping + public List getAll() { + return bookService.list(); + } + + @PostMapping + public Boolean save(@RequestBody Book book) { + return bookService.save(book); + } + + @PutMapping + public Boolean update(@RequestBody Book book) { + return bookService.modify(book); + } + + @DeleteMapping("{id}") + public Boolean delete(@PathVariable Integer id) { + return bookService.delete(id); + } + + @GetMapping("{id}") + public Book getById(@PathVariable Integer id) { + return bookService.getById(id); + } + + @GetMapping("{currentPage}/{pageSize}") + public IPage getPage(@PathVariable Integer currentPage, @PathVariable int pageSize) { + return bookService.getPage(currentPage, pageSize); + } + +} + +添加 分页的业务层方法 + +IBookService + + IPage getPage(int currentPage,int pageSize); + +BookServiceImpl + +@Override +public IPage getPage(int currentPage, int pageSize) { + + IPage page = new Page(currentPage, pageSize); + bookDao.selectPage(page, null); + + return page; +} + +![在这里插入图片描述](https://img-blog.csdnimg.cn/6f1ad431b53e47229e88f1898ff062de.png) +![在这里插入图片描述](https://img-blog.csdnimg.cn/2fc40d92ea0846c7b38142f9e755ed77.png) +![在这里插入图片描述](https://img-blog.csdnimg.cn/f1c3c746038b49619af5de1b04e7935f.png) + +### 7.8、表现层数据一致性处理(R对象) + +统一格式 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/a1ca89547dba4aeb9b164e195e572e6a.png) +![在这里插入图片描述](https://img-blog.csdnimg.cn/ee0fa35fce3542b4991d3be29736a2bb.png) + +* 设计表现层返回结果的模型类,用于后端与前端进行数据格式统一,也称为**前后端数据协议** + +@Data +public class R { + private Boolean flag; + private Object data; + + public R() { + } + + /** + * 不返回数据的构造方法 + * + * @param flag + */ + public R(Boolean flag) { + this.flag = flag; + } + + /** + * 返回数据的构造方法 + * + * @param flag + * @param data + */ + public R(Boolean flag, Object data) { + this.flag = flag; + this.data = data; + } +} + +* 表现层接口统一返回值类型结果 + +@RestController +@RequestMapping("/books") +public class BookController { + + @Autowired + private IBookService bookService; + + @GetMapping + public R getAll() { + return new R(true, bookService.list()); + } + + @PostMapping + public R save(@RequestBody Book book) { + return new R(bookService.save(book)); + + } + + @PutMapping + public R update(@RequestBody Book book) { + return new R(bookService.modify(book)); + } + + @DeleteMapping("{id}") + public R delete(@PathVariable Integer id) { + return new R(bookService.delete(id)); + } + + @GetMapping("{id}") + public R getById(@PathVariable Integer id) { + return new R(true, bookService.getById(id)); + } + + @GetMapping("{currentPage}/{pageSize}") + public R getPage(@PathVariable Integer currentPage, @PathVariable int pageSize) { + return new R(true, bookService.getPage(currentPage, pageSize)); + } + +} + +![在这里插入图片描述](https://img-blog.csdnimg.cn/c0c4ac51439b4d418abc0dcbbea2862c.png) + +**前端部分省略** + +## 8、Springboot工程打包与运行 + +### 8.1、程序为什么要打包 + +将程序部署在独立的服务器上 +![在这里插入图片描述](https://img-blog.csdnimg.cn/cf0d396d48bb49fa9c55847904a5f0b1.png) + +### 8.2、SpringBoot项目快速启动(Windows版) + +步骤 + +①:对SpringBoot项目打包(执行[Maven](https://so.csdn.net/so/search?q=Maven&spm=1001.2101.3001.7020)构建指令package) +执行 package 打包命令之前 先执行 **mvn clean** 删除 target 目录及内容 + +mvn package + +![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Eg4DFXqd-1657811363451)(SpringBoot.assets/image-20220317212408717.png)]](https://img-blog.csdnimg.cn/a19f1b3a110349cd9e16dbbd229ded5e.png) + +打包完成 生成对应的 jar 文件 +![在这里插入图片描述](https://img-blog.csdnimg.cn/9b4a4c84a82a44328fced7242662b4b8.png) + +可能出现的问题: IDEA下 执行 Maven 命令控制台中文乱码 +Ctr+Alt+S 打开设置,在Build,Execution ,Deployment找到Build Tools下Maven项下的Runner ,在VM Options 添加 +-Dfile.encoding=GB2312 ,点击OK。 +![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z5rHQGqK-1657811363452)(SpringBoot.assets/image-20220317212514627.png)]](https://img-blog.csdnimg.cn/74c0cddee08a4766bf1390fb3f7fac3f.png) + +②:运行项目(执行启动指令) java -jar <打包文件名> + +java –jar springboot.jar + +注意事项: +jar支持命令行启动需要依赖maven插件支持,请确认打包时是否具有SpringBoot对应的maven插件 + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + +![在这里插入图片描述](https://img-blog.csdnimg.cn/bbd941c93bed424e88de917ee0f8f406.png) + +地址栏输入 cmd 回车 +![在这里插入图片描述](https://img-blog.csdnimg.cn/db84686fb76149ce85ed43d38a91b783.png) + +执行 java -jar springboot_08_ssmp-0.0.1-SNAPSHOT.jar +![在这里插入图片描述](https://img-blog.csdnimg.cn/e2c9fa1457f44b62affb67d232ae9bf4.png) + +打包优化:跳过 test 生命周期 +![在这里插入图片描述](https://img-blog.csdnimg.cn/3ab5f5b1493346c6876ae477f1930402.png) +![在这里插入图片描述](https://img-blog.csdnimg.cn/1c91e19c0c1e4e57b8a313c93141249d.png) + +### 8.3、打包插件 + +如果没有配制spring boot 打包插件可能遇到下面的问题: +![在这里插入图片描述](https://img-blog.csdnimg.cn/7e7f2f26252240a5b0997570a230b3f1.png) + +使用SpringBoot提供的maven插件可以将工程打包成可执行jar包 + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + +可执行jar包目录 +![在这里插入图片描述](https://img-blog.csdnimg.cn/ab51e088e01643a384a434f8961dde04.png) + +ar包描述文件(MANIFEST.MF) + +普通工程 + +Manifest-Version: 1.0 +Implementation-Title: springboot_08_ssmp +Implementation-Version: 0.0.1-SNAPSHOT +Build-Jdk-Spec: 1.8 +Created-By: Maven Jar Plugin 3.2.0 + +基于spring-boot-maven-plugin打包的工程 + +Manifest-Version: 1.0 +Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx +Implementation-Title: springboot_08_ssmp +Implementation-Version: 0.0.1-SNAPSHOT +Spring-Boot-Layers-Index: BOOT-INF/layers.idx +Start-Class: com.example.SSMPApplication 启动类 +Spring-Boot-Classes: BOOT-INF/classes/ +Spring-Boot-Lib: BOOT-INF/lib/ +Build-Jdk-Spec: 1.8 +Spring-Boot-Version: 2.5.6 +Created-By: Maven Jar Plugin 3.2.0 +Main-Class: org.springframework.boot.loader.JarLauncher jar启动器 + +命令行启动常见问题及解决方案 + +* Windonws端口被占用 + +# 查询端口 +netstat -ano +# 查询指定端口 +netstat -ano |findstr "端口号" +# 根据进程PID查询进程名称 +tasklist |findstr "进程PID号" +# 根据PID杀死任务 +taskkill /F /PID "进程PID号" +# 根据进程名称杀死任务 +taskkill -f -t -im "进程名称" +### 8.4、Boot工程快速启动 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/a23a18d50a0246b89bf02a2ffeb549e2.png) + +* 基于Linux(CenterOS7) + +* 安装JDK,且版本不低于打包时使用的JDK版本 + + * 可以使用 yum 安装 +* 安装 MySQL + + * 可以参考: https://blog.csdn.net/qq_42324086/article/details/120579197 +* 安装包保存在/usr/local/自定义目录中或$HOME下 + +* 其他操作参照Windows版进行 + +**启动成功无法访问** + +添加 80 端口 + +* 添加 端口 + +firewall-cmd --zone=public --permanent --add-port=80/tcp + +重启 + +systemctl restart firewalld + +后台启动命令 + +nohup java -jar springboot_08_ssmp-0.0.1-SNAPSHOT.jar > server.log 2>&1 & + +停止服务 + +* ps -ef | grep “java -jar” +* kill -9 PID +* cat server.log (查看日志) + +[root@cjbCentos01 app]# ps -ef | grep "java -jar" +UID PID PPID C STIME TTY TIME CMD +root 6848 6021 7 14:45 pts/2 00:00:19 java -jar springboot_08_ssmp-0.0.1-SNAPSHOT.jar +root 6919 6021 0 14:49 pts/2 00:00:00 grep --color=auto java -jar +[root@cjbCentos01 app]# kill -9 6848 +[root@cjbCentos01 app]# ps -ef | grep "java -jar" +root 7016 6021 0 14:52 pts/2 00:00:00 grep --color=auto java -jar +[1]+ 已杀死 nohup java -jar springboot_08_ssmp-0.0.1-SNAPSHOT.jar > server.log 2>&1 +[root@cjbCentos01 app]# +### 8.6、临时属性 + +#### 8.6.1、临时属性 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/5b127fc48b0a44639d06f08bc0bd977c.png) + +* 带属性数启动SpringBoot + +java -jar springboot_08_ssmp-0.0.1-SNAPSHOT.jar --server.port=8080 + +* 携带多个属性启动SpringBoot,属性间使用空格分隔 + +属性加载优先顺序 + +1. 参看 https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config + ![在这里插入图片描述](https://img-blog.csdnimg.cn/945e0efcbcad4926a418bdad28e1be08.png) + +#### 8.6.2、开发环境 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/29a1d15b010747c0977800bea5d0e007.png) + +* 带属性启动SpringBoot程序,为程序添加运行属性 + ![在这里插入图片描述](https://img-blog.csdnimg.cn/13411376a16645bf8bae8fbcf6fc4964.png) + ![在这里插入图片描述](https://img-blog.csdnimg.cn/57e571fe9ce641bba055af96dce44e5d.png) + +在启动类中 main 可以通过 System.out.println(Arrays.toString(args)); 查看配制的属性 + +通过编程形式带参数启动SpringBoot程序,为程序添加运行参数 + +public static void main(String[] args) { + String[] arg = new String[1]; + arg[0] = "--server.port=8080"; + SpringApplication.run(SSMPApplication.class, arg); +} + +不携带参数启动SpringBoot程序 + +public static void main(String[] args) { + //可以在启动boot程序时断开读取外部临时配置对应的入口,也就是去掉读取外部参数的形参 + SpringApplication.run(SSMPApplication.class); +} +### 8.7、配置环境 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/205956af5f974a54a1e4c10015dae30d.png) + +#### 8.7.1、配置文件分类 + +* SpringBoot中4级配置文件 + + * 1级:file :config/application.yml 【最高】 + + * 2级:file :application.yml + + * 3级:classpath:config/application.yml + + * 4级:classpath:application.yml 【最低】 + +* 作用: + + * 1级与2级留做系统打包后设置通用属性,1级常用于运维经理进行线上整体项目部署方案调控 + + * 3级与4级用于系统开发阶段设置通用属性,3级常用于项目经理进行整体项目属性调控 + +#### 8.7.2、自定义配置文件 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/de7f3627fec54dd2852e44890f192b1d.png) + +通过启动参数加载配置文件(无需书写配置文件扩展名) --spring.config.name=eban +![在这里插入图片描述](https://img-blog.csdnimg.cn/70cebcbbd6c84a8eac3f9e997dca9a38.png) + +properties与yml文件格式均支持 + +* 通过启动参数加载指定文件路径下的配置文件 --spring.config.location=classpath:/ebank.yml + ![在这里插入图片描述](https://img-blog.csdnimg.cn/135c2375c371451e9bd86eee59f64668.png) + +properties与yml文件格式均支持 + +* 通过启动参数加载指定文件路径下的配置文件时可以加载多个配置,后面的会覆盖前面的 + +--spring.config.location=classpath:/ebank.yml,classpath:/ebank-server.yml + +![在这里插入图片描述](https://img-blog.csdnimg.cn/dc8e74a2c8c74d379f0869260493e458.png) + +注意事项: +多配置文件常用于将配置进行分类,进行独立管理,或将可选配置单独制作便于上线更新维护 + +自定义配置文件——重要说明 + +* 单服务器项目:使用自定义配置文件需求较低 + +* 多服务器项目:使用自定义配置文件需求较高,将所有配置放置在一个目录中,统一管理 + +* 基于SpringCloud技术,所有的服务器将不再设置配置文件,而是通过配置中心进行设定,动态加载配置信息 + +#### 8.7.3、多环境开发(yaml) + +![在这里插入图片描述](https://img-blog.csdnimg.cn/6aa91cdd68fd4e74aefce1746f502c5c.png) +![在这里插入图片描述](https://img-blog.csdnimg.cn/a286e356b075446e852abbaeea3b02bd.png) +![在这里插入图片描述](https://img-blog.csdnimg.cn/f331e0b270dd4a23afe009c231694d9e.png) + +#应用环境 +#公共配制 +spring: + profiles: + active: dev + +#设置环境 +#开发环境 +--- +spring: + config: + activate: + on-profile: dev +server: + port: 81 + +#生产环境 +--- +spring: + profiles: pro +server: + port: 80 + +#测试环境 +--- +spring: + profiles: test +server: + port: 82 +#### 8.7.4、多环境开发文件(yaml) + +![在这里插入图片描述](https://img-blog.csdnimg.cn/31e2fdc7804242488e594f9a04352d29.png) + +多环境开发(YAML版)多配置文件格式 +主启动配置文件application.yml + +#应用环境 +#公共配制 +spring: + profiles: + active: test + +环境分类配置文件application-pro.yml + +server: + port: 81 + +环境分类配置文件application-dev.yml + +server: + port: 82 + +环境分类配置文件application-test.yml + +server: + port: 83 +#### 8.7.5、多环境分组管理 + +* 根据功能对配置文件中的信息进行拆分,并制作成独立的配置文件,命名规则如下 + + * application-devDB.yml + + * application-devRedis.yml + + * application-devMVC.yml + +* 使用include属性在激活指定环境的情况下,同时对多个环境进行加载使其生效,多个环境间使用逗号分隔 + + spring: + profiles: + active: dev + include: devDB,devMVC + + 注意事项: + **当主环境dev与其他环境有相同属性时,主环境属性生效;其他环境中有相同属性时,最后加载的环境属性生效** + + The following profiles are active: devDB,devMVC,dev +* 从Spring2.4版开始使用group属性替代include属性,降低了配置书写量 + +* 使用**group**属性定义多种主环境与子环境的包含关系 + +spring: + profiles: + active: dev + group: + "dev": devDB,devMVC + "pro": proDB,proMVC + "test": testDB,testRedis,testMVC + +注意事项: +**使用group属性,会覆盖 主环境dev (active) 的内容,最后加载的环境属性生效** + +The following profiles are active: dev,devDB,devMVC +#### 8.7.6、多环境开发控制 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/7cac87f3998c403db8301a601178c9e8.png) +Maven与SpringBoot多环境兼容 +①:Maven中设置多环境属性 + + + + + dev_env + + dev + + + true + + + + pro_env + + pro + + + + + test_env + + test + + + + +②:SpringBoot中引用Maven属性 + +spring: + profiles: + active: @profile.active@ + group: + "dev": devDB,devMVC + "pro": proDB,proMVC + +![在这里插入图片描述](https://img-blog.csdnimg.cn/706db57f170d4b42ab1c71e4fd02888e.png) + +③:执行Maven打包指令,并在生成的boot打包文件.jar文件中查看对应信息 +问题:**修改pom.xml 文件后,启动没有生效 手动 compile 即可** +![在这里插入图片描述](https://img-blog.csdnimg.cn/41ea64df2c1b4e5393eaabfb1410ea4c.png) + +或者 设置 IDEA进行自动编译 +![在这里插入图片描述](https://img-blog.csdnimg.cn/281978a65c304f43a811da62b26ac626.png) + +## 9、日志 + +### 9.1、日志基础操作 + +日志(log)作用 + +1. 编程期调试代码 +2. 运营期记录信息 + * 记录日常运营重要信息(峰值流量、平均响应时长……) + * 记录应用报错信息(错误堆栈) + * 记录运维过程数据(扩容、宕机、报警……) + +代码中使用日志工具记录日志 + +* 先引入 Lombok 工具类 + + + + org.projectlombok + lombok + + + ①:添加日志记录操作 + + @RestController + @RequestMapping("/books") + public class BookController { + private static final Logger log = LoggerFactory.getLogger(BookController.class); + + @GetMapping + public String getById() { + System.out.println("springboot is running..."); + log.debug("debug ..."); + log.info("info ..."); + log.warn("warn ..."); + log.error("error ..."); + return "springboot is running..."; + } + } +* 日志级别 + +TRACE:运行堆栈信息,使用率低 +DEBUG:程序员调试代码使用 +INFO:记录运维过程数据 +WARN:记录运维过程报警数据 +ERROR:记录错误堆栈信息 +FATAL:灾难信息,合并计入ERRO + +②:设置日志输出级别 + +# 开启 debug 模式,输出调试信息,常用于检查系统运行状况 +debug: true +# 设置日志级别, root 表示根节点,即整体应用日志级别 +logging: + level: + root: debug + +③:设置日志组,控制指定包对应的日志输出级别,也可以直接控制指定包对应的日志输出级别 + +logging: + # 设置分组 + group: + # 自定义组名,设置当前组中所包含的包 + ebank: com.example.controller,com.example.service,com.example.dao + iservice: com.alibaba + level: + root: info + # 设置某个包的日志级别 +# com.example.controller: debug + # 为对应组设置日志级别 + ebank: warn +### 9.2、快速创建日志对象 + +* 使用lombok提供的注解@Slf4j简化开发,减少日志对象的声明操作 + + @Slf4j + //Rest模式 + @RestController + @RequestMapping("/books") + public class BookController { + + @GetMapping + public String getById(){ + System.out.println("springboot is running...2"); + + log.debug("debug..."); + log.info("info..."); + log.warn("warn..."); + log.error("error..."); + + return "springboot is running...2"; + } + + } + +### 9.3、日志输出格式控制 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/7b1ae90e16854890af7661f94570bd5d.png) + +* PID:进程ID,用于表明当前操作所处的进程,当多服务同时记录日志时,该值可用于协助程序员调试程序 +* 所属类/接口名:当前显示信息为SpringBoot重写后的信息,名称过长时,简化包名书写为首字母,甚至直接删除 +* 设置日志输出格式 + +logging: + pattern: + console: "%d - %m%n" + +%d:日期 +%m:消息 +%n:换行 +![在这里插入图片描述](https://img-blog.csdnimg.cn/91223acd27344b03ace835d17d35dd95.png) + +logging: +pattern: +# console: "%d - %m%n" +console: "%d %clr(%5p) --- [%16t] %clr(%-40.40c){cyan} : %m %n" +### 9.4、文件记录日志 + +* 设置日志文件 + +logging: + file: + name: server.log + +* 日志文件详细配置 + +logging: + file: + name: server.log + logback: + rollingpolicy: + max-file-size: 4KB + file-name-pattern: server.%d{yyyy-MM-dd}.%i.log + +![在这里插入图片描述](https://img-blog.csdnimg.cn/756bf00cf4f44ea286b6fac6f31ccf44.png) + +## 10、热部署 + + + org.springframework.boot + spring-boot-devtools + true + spring: + # 热部署范围配置 + devtools: + restart: + # 设置不参与热部署的文件和文件夹(即修改后不重启) + exclude: static/**,public/**,config/application.yml + #是否可用 + enabled: false + +如果配置文件比较多的时候找热部署对应配置比较麻烦,可以在`springboot`启动类的main方法中设置,此处设置的优先级将比配置文件高,一定会生效。 + +System.setProperty("spring.devtools.restart.enabled", "false"); +## 11、属性绑定 + +1. 先在要配置的类上面加@Component注解将该类交由spring容器管理; +2. `@ConfigurationProperties(prefix="xxx")`,xxx跟application.yml配置文件中的属性对应; +3. 如果多个配置类想统一管理也可以通过`@EnableConfigurationProperties({xxx.class, yyy.class})`的方式完成配置,不过该注解会与@Component配置发生冲突,二选一即可; +4. 第三方类对象想通过配置进行属性注入,可以通过创建一个方法,在方法体上加@Bean和`@ConfigurationProperties(prefix="xxx")`注解,然后方法返回这个第三方对象的方式。 +5. 使用`@ConfigurationProperties(prefix="xxx")`注解后idea工具会报一个警告Spring Boot Configuration Annotation Processor not configured + +@ConfigurationProperties(prefix="xxx") +@Data +public class ServerConfig { + private String inAddress; + private int port; + private long timeout; +} + +`@ConfigurationProperties`绑定属性支持属性名宽松绑定,又叫松散绑定。 + +比如要将`ServerConfig.class`作为配置类,并通过配置文件`application.yml`绑定属性 + +ServerConfig.class serverConfig: + # ipAddress: 192.168.0.1 # 驼峰模式 + # ipaddress: 192.168.0.1 + # IPADDRESS: 192.168.0.1 + ip-address: 192.168.0.1 # 主流配置方式,烤肉串模式 + # ip_address: 192.168.0.1 # 下划线模式 + # IP_ADDRESS: 192.168.0.1 # 常量模式 + # ip_Add_rEss: 192.168.0.1 + # ipaddress: 192.168.0.1 + port: 8888 + timeout: -1 + +以ipAddress属性为例,上面的多种配置方式皆可生效,这就是松散绑定。而@Value不支持松散绑定,必须一一对应。 + +`@ConfigurationProperties(prefix="serverconfig")`中的prefix的值为serverconfig或者server-config,如果是serverConfig就会报错,这与松散绑定的前缀命名规范有关:仅能使用纯小写字母、数字、中划线作为合法的字符 + +## 12、常用计量单位应用 + +//@Component +@ConfigurationProperties(prefix = "server-config") +@Data +public class ServerConfig { + private String ipAddress; + private int port; + @DurationUnit(ChronoUnit.MINUTES) + private Duration timeout; + + @DataSizeUnit(DataUnit.MEGABYTES) + private DataSize dataSize; +} + +引入`Bean`属性校验框架的步骤: + +1. 在`pom.xml`中添加`JSR303`规范和`hibernate`校验框架的依赖: + + + + javax.validation + validation-api + + + + org.hibernate.validator + hibernate-validator + + +1. 在要校验的类上加`@Validated`注解 + +2. 设置具体的校验规则,如:`@Max(value=8888, message="最大值不能超过8888")` + +@ConfigurationProperties(prefix = "server-config") +@Data +// 2.开启对当前bean的属性注入校验 +@Validated +public class ServerConfig { + private String ipAddress; + // 设置具体的规则 + @Max(value = 8888, message = "最大值不能超过8888") + @Min(value = 1000, message = "最小值不能低于1000") + private int port; + @DurationUnit(ChronoUnit.MINUTES) + private Duration timeout; + + @DataSizeUnit(DataUnit.MEGABYTES) + private DataSize dataSize; +} + +进制转换中的一些问题: + +如`application.yml`文件中对数据库有如下配置: + +datasource: + driverClassName: com.mysql.cj.jdbc.Driver123 + # 不加引号读取的时候默认解析为了8进制数,转成十进制就是87 + # 所以想让这里正确识别,需要加上引号 + # password: 0127 + password: "0127" +## 13、测试类 + +### 13.1、加载专用属性 + +@SpringBootTest注解中可以设置properties和args属性,这里的args属性的作用跟idea工具中自带的程序参数类似,只不过这里的配置是源码级别的,会随着源码的移动而跟随,而idea中的程序参数的配置会丢失。并且这里的args属性的配置的作用范围比较小,仅在当前测试类生效。 + +application.yml + +test: + prop: testValue // properties属性可以为当前测试用例添加临时的属性配置 +//@SpringBootTest(properties = {"test.prop=testValue1"}) +// args属性可以为当前测试用例添加临时的命令行参数 +//@SpringBootTest(args = {"--test.prop=testValue2"}) +// 优先级排序: args > properties > 配置文件 +@SpringBootTest(args = {"--test.prop=testValue2"}, properties = {"test.prop=testValue1"}) +class PropertiesAndArgsTest { + @Value("${test.prop}") + private String prop; + @Test + public void testProperties() { + System.out.println("prop = " + prop); + } +} +### 13.2、加载专用类 + +某些测试类中需要用到第三方的类,而其他测试类则不需要用到,这里可以在类上加载`@Import({xxx.class, yyy.class})` + +@RestController +@RequestMapping("/books") +public class BookController { + /*@GetMapping("/{id}") + public String getById(@PathVariable int id) { + System.out.println("id = " + id); + return "getById..."; + }*/ + + @GetMapping("/{id}") + public Book getById(@PathVariable int id) { + System.out.println("id = " + id); + Book book = new Book(); + book.setId(5); + book.setName("springboot"); + book.setType("springboot"); + book.setDescription("springboot"); + return book; + } +} + +相应测试类 + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +// 开启虚拟mvc调用 +@AutoConfigureMockMvc +public class WebTest { + @Test + public void testRandomPort() { + } + + @Test + public void testWeb(@Autowired MockMvc mvc) throws Exception { + // 创建虚拟请求,当前访问 /books + MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books/5"); + mvc.perform(builder); + } + + @Test + public void testStatus(@Autowired MockMvc mvc) throws Exception { + MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books1/6"); + ResultActions action = mvc.perform(builder); + // 设定预期值,与真实值进行比较,成功测试通过,失败测试不通过 + // 定义本次调用的预期值 + StatusResultMatchers srm = MockMvcResultMatchers.status(); + // 预计本次调用成功的状态码:200 + ResultMatcher ok = srm.isOk(); + // 添加预计值到本次调用过程中进行匹配 + action.andExpect(ok); + } + + @Test + public void testBody(@Autowired MockMvc mvc) throws Exception { + MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books/6"); + ResultActions action = mvc.perform(builder); + // 设定预期值,与真实值进行比较,成功测试通过,失败测试不通过 + // 定义本次调用的预期值 + ContentResultMatchers crm = MockMvcResultMatchers.content(); + // 预计本次调用成功的状态码:200 + ResultMatcher rm = crm.string("getById..."); + // 添加预计值到本次调用过程中进行匹配 + action.andExpect(rm); + } + + @Test + public void testJson(@Autowired MockMvc mvc) throws Exception { + MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books/7"); + ResultActions action = mvc.perform(builder); + // 设定预期值,与真实值进行比较,成功测试通过,失败测试不通过 + // 定义本次调用的预期值 + ContentResultMatchers jsonMatcher = MockMvcResultMatchers.content(); + ResultMatcher rm = jsonMatcher.json("{\"id\":5,\"name\":\"springboot\",\"type\":\"springboot\",\"description\":\"springboot1\"}"); + action.andExpect(rm); + } + + @Test + public void testContentType(@Autowired MockMvc mvc) throws Exception { + MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books/7"); + ResultActions action = mvc.perform(builder); + // 设定预期值,与真实值进行比较,成功测试通过,失败测试不通过 + // 定义本次调用的预期值 + HeaderResultMatchers hrm = MockMvcResultMatchers.header(); + ResultMatcher rm = hrm.string("Content-Type", "application/json"); + action.andExpect(rm); + } + + @Test + // 完整测试 + public void testGetById(@Autowired MockMvc mvc) throws Exception { + MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books/8"); + ResultActions action = mvc.perform(builder); + + // 1、比较状态码 + StatusResultMatchers statusResultMatchers = MockMvcResultMatchers.status(); + ResultMatcher statusResultMatcher = statusResultMatchers.isOk(); + action.andExpect(statusResultMatcher); + + // 2、比较返回值类型 + HeaderResultMatchers headerResultMatchers = MockMvcResultMatchers.header(); + ResultMatcher headerResultMatcher = headerResultMatchers.string("Content-Type", "application/json"); + action.andExpect(headerResultMatcher); + + /// 3、比较json返回值 + ContentResultMatchers contentResultMatchers = MockMvcResultMatchers.content(); + ResultMatcher jsonResultMatcher = contentResultMatchers.json("{\"id\":5,\"name\":\"springboot\",\"type\":\"springboot\",\"description\":\"springboot\"}"); + action.andExpect(jsonResultMatcher); + } +} +### 13.3、业务层测试事务回滚 + +* 为测试用例添加事务,SpringBoot会对测试用例对应的事务提交操作进行回滚 + + @SpringBootTest + @Transactional public class DaoTest { + @Autowired + private BookService bookService; } +* l如果想在测试用例中提交事务,可以通过@Rollback注解设置 + + @SpringBootTest + @Transactional + @Rollback(false) + public class DaoTest { + } + +### 13.4、测试用例数据设定 + +* 测试用例数据通常采用随机值进行测试,使用SpringBoot提供的随机数为其赋值 + +testcast: + book: + id: ${random.int} # 随机整数 + id2: ${random.int(10)} # 10以内随机数 + type: ${random.int(10,20)} # 10到20随机数 + uuid: ${random.uuid} # 随机uuid + name: ${random.value} # 随机字符串,MD5字符串,32位 + publishTime: ${random.long} # 随机整数(long范围) u${random.int}表示随机整数 + +u${random.int(10)}表示10以内的随机数 + +u${random.int(10,20)}表示10到20的随机数 + +u其中()可以是任意字符,例如[],!!均可 +## 14、数据层解决方案 + +### 14.1、SQL + +现有数据层解决方案技术选型 + +Druid + MyBatis-Plus + MySQL + +* 数据源:DruidDataSource +* 持久化技术:MyBatis-Plus / MyBatis +* 数据库:MySQL + +格式一: + +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC + username: root + password: root + +格式二: + +spring: + datasource: + druid: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC + username: root + password: root + +* SpringBoot提供了3种内嵌的数据源对象供开发者选择 + * HikariCP + * Tomcat提供DataSource + * Commons DBCP + +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC + username: root + password: root spring: + datasource: + druid: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC + username: root + password: root + +* SpringBoot提供了3种内嵌的数据源对象供开发者选择 + + * HikariCP:默认内置数据源对象 + + * Tomcat提供DataSource:HikariCP不可用的情况下,且在web环境中,将使用tomcat服务器配置的数据源对象 + + * Commons DBCP:Hikari不可用,tomcat数据源也不可用,将使用dbcp数据源 + +* 通用配置无法设置具体的数据源配置信息,仅提供基本的连接相关配置,如需配置,在下一级配置中设置具体设定 + +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/ssm_db + username: root + password: root + hikari: + maximum-pool-size: 50 + +* 内置持久化解决方案——JdbcTemplate + +@SpringBootTest +class Springboot15SqlApplicationTests { + @Autowired + private JdbcTemplate jdbcTemplate; + @Test + void testJdbc(){ + String sql = "select * from tbl_book where id = 1"; + List query = jdbcTemplate.query(sql, new RowMapper() { + @Override + public Book mapRow(ResultSet rs, int rowNum) throws SQLException { + Book temp = new Book(); + temp.setId(rs.getInt("id")); + temp.setName(rs.getString("name")); + temp.setType(rs.getString("type")); + temp.setDescription(rs.getString("description")); + return temp; + } + }); + System.out.println(query); + } +} + org.springframework.boot + spring-boot-starter-jdbc + spring: + jdbc: + template: + query-timeout: -1 # 查询超时时间 + max-rows: 500 # 最大行数 + fetch-size: -1 # 缓存行数 + +* SpringBoot提供了3种内嵌数据库供开发者选择,提高开发测试效率 + + * H2 + * HSQL + * Derby +* 导入H2相关坐标 + + + org.springframework.boot + spring-boot-starter-data-jpa + + + com.h2database + h2 + runtime + +* 设置当前项目为web工程,并配置H2管理控制台参数 + +server: + port: 80 +spring: + h2: + console: + path: /h2 + enabled: true + +访问用户名sa,默认密码123456 + +操作数据库(创建表) + +create table tbl_book (id int,name varchar,type varchar,description varchar) #设置访问数据源 +server: + port: 80 +spring: + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:~/test + username: sa + password: 123456 +h2: + console: + path: /h2 + enabled: true + +H2数据库控制台仅用于开发阶段,线上项目请务必关闭控制台功能 + +server: + port: 80 +spring: + h2: + console: + path: /h2 + enabled: false + +SpringBoot可以根据url地址自动识别数据库种类,在保障驱动类存在的情况下,可以省略配置 + +server: + port: 80 +spring: + datasource: +# driver-class-name: org.h2.Driver + url: jdbc:h2:~/test + username: sa + password: 123456 + h2: + console: + path: /h2 + enabled: true + +![在这里插入图片描述](https://img-blog.csdnimg.cn/cb7f9780f7884e9bb8b993ee2363dd6e.png) + +### 14.2、MongoDB + +MongoDB是一个开源、高性能、无模式的文档型数据库。NoSQL数据库产品中的一种,是最像关系型数据库的非关系型数据库 +![在这里插入图片描述](https://img-blog.csdnimg.cn/bc3313adaf3a471da66e8a9a062f552b.png) + +#### 14.2.1、MongoDB的使用 + +* Windows版Mongo下载 + +https://www.mongodb.com/try/download + +* Windows版Mongo安装 + +解压缩后设置数据目录 + +* Windows版Mongo启动 + +服务端启动 + +在bin目录下 + +`mongod --dbpath=..\data\db` + +客户端启动 + +`mongo --host=127.0.0.1 --port=27017` + +#### 14.2.2、MongoDB可视化客户端 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/48f4a3f143be40cbaf09e018df91dc5e.png) + +* 新增 + + `db.集合名称.insert/save/insertOne(文档)` + +* 修改 + + `db.集合名称.remove(条件)` + +* 删除 + + `db.集合名称.update(条件,{操作种类:{文档}})` + ![在这里插入图片描述](https://img-blog.csdnimg.cn/349eb759436043b88ec23d8541c41340.png) + ![在这里插入图片描述](https://img-blog.csdnimg.cn/f5140aed83ee407db4a4652f81fe7d9a.png) + +#### 14.2.3、Springboot集成MongoDB + +导入MongoDB驱动 + + + org.springframework.boot + spring-boot-starter-data-mongodb + + +配置客户端 + +spring: + data: + mongodb: + uri: mongodb://localhost/itheima + +客户端读写MongoDB + +@Test +void testSave(@Autowired MongoTemplate mongoTemplate){ + Book book = new Book(); + book.setId(1); + book.setType("springboot"); + book.setName("springboot"); + book.setDescription("springboot"); + mongoTemplate.save(book); +} +@Test +void testFind(@Autowired MongoTemplate mongoTemplate){ + List all = mongoTemplate.findAll(Book.class); + System.out.println(all); +} +### 14.3、ElasticSearch(ES) + +Elasticsearch是一个分布式全文搜索引擎 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/a5bb0c141ef24ed8bfedf270201dae1e.png) + +![在这里插入图片描述](https://img-blog.csdnimg.cn/a17cd8d3332d42c4a1161c8541f1fbbb.png) + +#### 14.3.1、ES下载 + +* Windows版ES下载 + +[https://](https://www.elastic.co/cn/downloads/elasticsearch)[www.elastic.co/cn/downloads/elasticsearch](https://www.elastic.co/cn/downloads/elasticsearch) + +* Windows版ES安装与启动 + +运行:`elasticsearch.bat` + +#### 14.3.2、ES索引、分词器 + +* 创建/查询/删除索引 + +put:`http://localhost:9200/books` + +get:`http://localhost:9200/books` + +delete:`http://localhost:9200/books` + +* IK分词器 + + 下载:https://github.com/medcl/elasticsearch-analysis-ik/releases + +* 创建索引并指定规则 + +{ + "mappings":{ + "properties":{ + "id":{ + "type":"keyword" + }, + "name":{ + "type":"text", "analyzer":"ik_max_word", "copy_to":"all" + }, + "type":{ + "type":"keyword" + }, + "description":{ + "type":"text", "analyzer":"ik_max_word", "copy_to":"all" + }, + "all":{ + "type":"text", "analyzer":"ik_max_word" + } + } + } +} +#### 14.3.3、文档操作(增删改查) + +* 创建文档 + + post:`http://localhost:9200/books/_doc`(使用系统生成的id) + + post:`http://localhost:9200/books/_create/1`(使用指定id) + + post:`http://localhost:9200/books/_doc/1`(使用指定id,不存在创建,存在更新,版本递增) + + { + "name":"springboot", + "type":"springboot", + "description":"springboot" + } +* 查询文档 + + get:`http://localhost:9200/books/_doc/1` + + get:`http://localhost:9200/books/_search` + +* 条件查询 + + get:`http://localhost:9200/books/_search?q=name:springboot` + +* 删除文档 + + delete:`http://localhost:9200/books/_doc/1` + +* 修改文档(全量修改) + + put:`http://localhost:9200/books/_doc/1` + + { + "name":"springboot", + "type":"springboot", + "description":"springboot" + } +* 修改文档(部分修改) + + post:`http://localhost:9200/books/_update/1` + + { + "doc":{ + "name":"springboot" + } + } + +#### 14.3.4、Springboot集成ES + +* 导入坐标 + + + org.springframework.boot + spring-boot-starter-data-elasticsearch + +* 配置 + + spring: + elasticsearch: + rest: + uris: http://localhost:9200 +* 客户端 + + @SpringBootTest + class Springboot18EsApplicationTests { + @Autowired + private ElasticsearchRestTemplate template; + } +* SpringBoot平台并没有跟随ES的更新速度进行同步更新,ES提供了High Level Client操作ES + + 导入坐标 + + + org.elasticsearch.client + elasticsearch-rest-high-level-client + + + 无需配置 + +* 客户端 + + @Test + void test() throws IOException { + HttpHost host = HttpHost.create("http://localhost:9200"); + RestClientBuilder builder = RestClient.builder(host); + RestHighLevelClient client = new RestHighLevelClient(builder); + //客户端操作 + CreateIndexRequest request = new CreateIndexRequest("books"); + //获取操作索引的客户端对象,调用创建索引操作 + client.indices().create(request, RequestOptions.DEFAULT); + //关闭客户端 + client.close(); + } +* 客户端改进 + + @SpringBootTest + class Springboot18EsApplicationTests { + @Autowired + private BookDao bookDao; + @Autowired + RestHighLevelClient client; + @BeforeEach + void setUp() { + this.client = new RestHighLevelClient(RestClient.builder(HttpHost.create("http://localhost:9200"))); + } + @AfterEach + void tearDown() throws IOException { + this.client.close(); + } + @Test + void test() throws IOException { + //客户端操作 + CreateIndexRequest request = new CreateIndexRequest("books"); + //获取操作索引的客户端对象,调用创建索引操作 + client.indices().create(request, RequestOptions.DEFAULT); + } + } + +#### 14.3.5、索引 + +* 创建索引 + + //创建索引 + @Test + void testCreateIndexByIK() throws IOException { + HttpHost host = HttpHost.create("http://localhost:9200"); + RestClientBuilder builder = RestClient.builder(host); + RestHighLevelClient client = new RestHighLevelClient(builder); + //客户端操作 + CreateIndexRequest request = new CreateIndexRequest("books"); + //设置要执行操作 + String json = ""; + //设置请求参数,参数类型json数据 + request.source(json,XContentType.JSON); + //获取操作索引的客户端对象,调用创建索引操作 + client.indices().create(request, RequestOptions.DEFAULT); + //关闭客户端 + client.close(); + } String json = "{\n" + + " \"mappings\":{\n" + + " \"properties\":{\n" + + " \"id\":{\n" + + " \"type\":\"keyword\"\n" + + " },\n" + + " \"name\":{\n" + + " \"type\":\"text\",\n" + + " \"analyzer\":\"ik_max_word\",\n" + + " \"copy_to\":\"all\"\n" + + " },\n" + + " \"type\":{\n" + + " \"type\":\"keyword\"\n" + + " },\n" + + " \"description\":{\n" + + " \"type\":\"text\",\n" + + " \"analyzer\":\"ik_max_word\",\n" + + " \"copy_to\":\"all\"\n" + + " },\n" + + " \"all\":{\n" + + " \"type\":\"text\",\n" + + " \"analyzer\":\"ik_max_word\"\n" + + " }\n" + + " }\n" + + " }\n" + + "}"; +* 添加文档 + + //添加文档 + @Test + void testCreateDoc() throws IOException { + Book book = bookDao.selectById(1); + IndexRequest request = new IndexRequest("books").id(book.getId().toString()); + String json = JSON.toJSONString(book); + request.source(json,XContentType.JSON); + client.index(request, RequestOptions.DEFAULT); + } +* 批量添加文档 + + //批量添加文档 + @Test + void testCreateDocAll() throws IOException { + List bookList = bookDao.selectList(null); + BulkRequest bulk = new BulkRequest(); + for (Book book : bookList) { + IndexRequest request = new IndexRequest("books").id(book.getId().toString()); + String json = JSON.toJSONString(book); + request.source(json,XContentType.JSON); + bulk.add(request); + } + client.bulk(bulk,RequestOptions.DEFAULT); + } +* 按id查询文档 + + @Test + void testGet() throws IOException { + GetRequest request = new GetRequest("books","1"); + GetResponse response = client.get(request, RequestOptions.DEFAULT); + String json = response.getSourceAsString(); + System.out.println(json); + } +* 按条件查询文档 + +@Test +void testSearch() throws IOException { + SearchRequest request = new SearchRequest("books"); + SearchSourceBuilder builder = new SearchSourceBuilder(); + builder.query(QueryBuilders.termQuery("all",“java")); + request.source(builder); + SearchResponse response = client.search(request, RequestOptions.DEFAULT); + SearchHits hits = response.getHits(); + for (SearchHit hit : hits) { + String source = hit.getSourceAsString(); + Book book = JSON.parseObject(source, Book.class); + System.out.println(book); + } +} +## 15、缓存 + +### 15.1、缓存简介 + +缓存是一种介于数据永久存储介质与数据应用之间的数据临时存储介质 +![在这里插入图片描述](https://img-blog.csdnimg.cn/be3b3d385ea7423cb66544139b28591d.png) + +* 缓存是一种介于数据永久存储介质与数据应用之间的数据临时存储介质 + +* 使用缓存可以有效的减少低速数据读取过程的次数(例如磁盘IO),提高系统性能 + +* 缓存不仅可以用于提高永久性存储介质的数据读取效率,还可以提供临时的数据存储空间 + ![在这里插入图片描述](https://img-blog.csdnimg.cn/340efee416e64d83957abf3c421c3575.png) + +* SpringBoot提供了缓存技术,方便缓存使用 + +### 15.2、缓存使用 + +* 启用缓存 + +* 设置进入缓存的数据 + +* 设置读取缓存的数据 + +* 导入缓存技术对应的starter + + + org.springframework.boot + spring-boot-starter-cache + +* 启用缓存 + + @SpringBootApplication + @EnableCaching + public class Springboot19CacheApplication { + public static void main(String[] args) { + SpringApplication.run(Springboot19CacheApplication.class, args); + } + } +* 设置当前操作的结果数据进入缓存 + +@Cacheable(value="cacheSpace",key="#id") +public Book getById(Integer id) { + return bookDao.selectById(id); +} +### 15.3、其他缓存 + +* SpringBoot提供的缓存技术除了提供默认的缓存方案,还可以对其他缓存技术进行整合,统一接口,方便缓存技术的开发与管理 + * Generic + * JCache + * **Ehcache** + * Hazelcast + * Infinispan + * Couchbase + * **Redis** + * Caffeine + * Simple(默认) + * **memcached** + * jetcache(阿里) + +### 15.4、缓存使用案例——手机验证码 + +* 需求 + + * 输入手机号获取验证码,组织文档以短信形式发送给用户(页面模拟) + + * 输入手机号和验证码验证结果 + +* 需求分析 + + * 提供controller,传入手机号,业务层通过手机号计算出独有的6位验证码数据,存入缓存后返回此数据 + + * 提供controller,传入手机号与验证码,业务层通过手机号从缓存中读取验证码与输入验证码进行比对,返回比对结果 + +#### 15.4.1、Cache + +* 开启缓存 + + @SpringBootApplication + @EnableCaching + public class Springboot19CacheApplication { + public static void main(String[] args) { + SpringApplication.run(Springboot19CacheApplication.class, args); + } + } +* 业务层接口 + + public interface SMSCodeService { + /** + * 传入手机号获取验证码,存入缓存 + * @param tele + * @return + */ + String sendCodeToSMS(String tele); + + /** + * 传入手机号与验证码,校验匹配是否成功 + * @param smsCode + * @return + */ + boolean checkCode(SMSCode smsCode); + } +* 业务层设置获取验证码操作,并存储缓存,手机号为key,验证码为value + + @Autowired + private CodeUtils codeUtils; + @CachePut(value = "smsCode",key="#tele") + public String sendCodeToSMS(String tele) { + String code = codeUtils.generator(tele); + return code; + } +* 业务层设置校验验证码操作,校验码通过缓存读取,返回校验结果 + + @Autowired + private CodeUtils codeUtils; + public boolean checkCode(SMSCode smsCode) { + //取出内存中的验证码与传递过来的验证码比对,如果相同,返回true + String code = smsCode.getCode(); + String cacheCode = codeUtils.get(smsCode.getTele()); + return code.equals(cacheCode); + } @Component + public class CodeUtils { + @Cacheable(value = "smsCode",key="#tele") + public String get(String tele){ + return null; + } + } + +#### 15.4.2、Ehcache + +* 加入Ehcache坐标(缓存供应商实现) + + + net.sf.ehcache + ehcache + +* 缓存设定为使用Ehcache + + spring: + cache: + type: ehcache + ehcache: + config: ehcache.xml +* 提供ehcache配置文件ehcache.xml + + + + + + + + + + + + + + + + +#### 15.4.3、Redis + +* 加入Redis坐标(缓存供应商实现) + + + org.springframework.boot + spring-boot-starter-data-redis + + +* 配置Redis服务器,缓存设定为使用Redis + + spring: + redis: + host: localhost + port: 6379 + cache: + type: redis +* 设置Redis相关配置 + + spring: + redis: + host: localhost + port: 6379 + cache: + type: redis + redis: + use-key-prefix: true # 是否使用前缀名(系统定义前缀名) + key-prefix: sms_ # 追加自定义前缀名 + time-to-live: 10s # 有效时长 + cache-null-values: false # 是否允许存储空值 + +#### 15.4.4、memcached + +* 下载memcached + +* 地址:https://www.runoob.com/memcached/window-install-memcached.html + +* 安装memcached + + * 使用管理员身份运行cmd指令 + + * 安装 + + `memcached.exe -d install` + +* 运行 + + * 启动服务 + + `memcached.exe -d start` + + * 定制服务 + + `memcached.exe -d stop` + +* memcached客户端选择 + + * Memcached Client for Java:最早期客户端,稳定可靠,用户群广 + * SpyMemcached:效率更高 + * Xmemcached:并发处理更好 +* SpringBoot未提供对memcached的整合,需要使用硬编码方式实现客户端初始化管理 + +* 加入Xmemcached坐标(缓存供应商实现) + + + com.googlecode.xmemcached + xmemcached + 2.4.7 + +* 配置memcached服务器必要属性 + + memcached: + # memcached服务器地址 + servers: localhost:11211 + # 连接池的数量 + poolSize: 10 + # 设置默认操作超时 + opTimeout: 3000 +* 创建读取属性配置信息类,加载配置 + + @Component + @ConfigurationProperties(prefix = "memcached") + @Data + public class XMemcachedProperties { + private String servers; + private Integer poolSize; + private Long opTimeout; + } +* 创建客户端配置类 + + @Configuration + public class XMemcachedConfig { + @Autowired + private XMemcachedProperties xMemcachedProperties; + @Bean + public MemcachedClient getMemcachedClinet() throws IOException { + MemcachedClientBuilder builder = new XMemcachedClientBuilder(xMemcachedProperties.getServers()); + MemcachedClient memcachedClient = builder.build(); + return memcachedClient; + } + } +* 配置memcached属性 + + @Service + public class SMSCodeServiceMemcacheImpl implements SMSCodeService { + @Autowired + private CodeUtils codeUtils; + @Autowired + private MemcachedClient memcachedClient; + @Override + public String sendCodeToSMS(String tele) { + String code = this.codeUtils.generator(tele); + //将数据加入memcache + try { + memcachedClient.set(tele,0,code); // key,timeout,value + } catch (Exception e) { + e.printStackTrace(); + } + return code; + } + } @Service + public class SMSCodeServiceMemcacheImpl implements SMSCodeService { + @Autowired + private CodeUtils codeUtils; + @Autowired + private MemcachedClient memcachedClient; + @Override + public boolean checkCode(CodeMsg codeMsg) { + String value = null; + try { + value = memcachedClient.get(codeMsg.getTele()).toString(); + } catch (Exception e) { + e.printStackTrace(); + } + return codeMsg.getCode().equals(value); + } + } + +#### 15.4.5、jetcache + +* jetCache对SpringCache进行了封装,在原有功能基础上实现了多级缓存、缓存统计、自动刷新、异步调用、数据报表等功能 + +* jetCache设定了本地缓存与远程缓存的多级缓存解决方案 + + * 本地缓存(local) + + * LinkedHashMap + * Caffeine + * 远程缓存(remote) + + * Redis + * Tair +* 加入jetcache坐标 + + + com.alicp.jetcache + jetcache-starter-redis + 2.6.2 + +* 配置**远程**缓存必要属性 + + jetcache: + remote: + default: + type: redis + host: localhost + port: 6379 + poolConfig: + maxTotal: 50 jetcache: + remote: + default: + type: redis + host: localhost + port: 6379 + poolConfig: + maxTotal: 50 + sms: + type: redis + host: localhost + port: 6379 + poolConfig: + maxTotal: 50 +* 配置**本地**缓存必要属性 + + jetcache: + local: + default: + type: linkedhashmap + keyConvertor: fastjson +* 配置范例 + + jetcache: + statIntervalMinutes: 15 + areaInCacheName: false + local: + default: + type: linkedhashmap + keyConvertor: fastjson + limit: 100 + remote: + default: + host: localhost + port: 6379 + type: redis + keyConvertor: fastjson + valueEncoder: java + valueDecoder: java + poolConfig: + minIdle: 5 + maxIdle: 20 + maxTotal: 50 +* 配置属性说明 + ![在这里插入图片描述](https://img-blog.csdnimg.cn/bb8230091ee84b91828bf74555e8d64b.png) + +* 开启jetcache注解支持 + + @SpringBootApplication + @EnableCreateCacheAnnotation + public class Springboot19CacheApplication { + public static void main(String[] args) { + SpringApplication.run(Springboot19CacheApplication.class, args); + } + } +* 声明缓存对象 + + @Service + public class SMSCodeServiceImpl implements SMSCodeService { + @Autowired + private CodeUtils codeUtils; + @CreateCache(name = "smsCache", expire = 3600) + private Cache jetSMSCache; + } +* 操作缓存 + + @Service + public class SMSCodeServiceImpl implements SMSCodeService { + @Override + public String sendCodeToSMS(String tele) { + String code = this.codeUtils.generator(tele); + jetSMSCache.put(tele,code); + return code; + } + @Override + public boolean checkCode(CodeMsg codeMsg) { + String value = jetSMSCache.get(codeMsg.getTele()); + return codeMsg.getCode().equals(value); + } + } +* 启用方法注解 + + @SpringBootApplication + @EnableCreateCacheAnnotation + @EnableMethodCache(basePackages = "com.itheima") + public class Springboot20JetCacheApplication { + public static void main(String[] args) { + SpringApplication.run(Springboot20JetCacheApplication.class, args); + } + } +* 使用方法注解操作缓存 + + @Service + public class BookServiceImpl implements BookService { + @Autowired + private BookDao bookDao; + @Cached(name = "smsCache_", key = "#id", expire = 3600) + @CacheRefresh(refresh = 10,timeUnit = TimeUnit.SECONDS) + public Book getById(Integer id) { + return bookDao.selectById(id); + } + } @Service + public class BookServiceImpl implements BookService { + + @CacheUpdate(name = "smsCache_", key = "#book.id", value = "#book") + public boolean update(Book book) { + return bookDao.updateById(book) > 0; + } + + @CacheInvalidate(name = "smsCache_", key = "#id") + public boolean delete(Integer id) { + return bookDao.deleteById(id) > 0; + } + } +* 缓存对象必须保障可序列化 + + @Data + public class Book implements Serializable { + } jetcache: + remote: + default: + type: redis + keyConvertor: fastjson + valueEncoder: java + valueDecoder: java +* 查看缓存统计报告 + + jetcache: + statIntervalMinutes: 15 + +#### 15.4.6、j2cache + +* j2cache是一个缓存整合框架,可以提供缓存的整合方案,使各种缓存搭配使用,自身不提供缓存功能 + +* 基于 ehcache + redis 进行整合 + +* 加入j2cache坐标,加入整合缓存的坐标 + + + net.oschina.j2cache + j2cache-spring-boot2-starter + 2.8.0-release + + + net.oschina.j2cache + j2cache-core + 2.8.4-release + + + net.sf.ehcache + ehcache + +* 配置使用j2cache(application.yml) + + j2cache: + config-location: j2cache.properties +* 配置一级缓存与二级缓存以及一级缓存数据到二级缓存的发送方式(j2cache.properties) + + # 配置1级缓存 + j2cache.L1.provider_class = ehcache + ehcache.configXml = ehcache.xml + + # 配置1级缓存数据到2级缓存的广播方式:可以使用redis提供的消息订阅模式,也可以使用jgroups多播实现 + j2cache.broadcast = net.oschina.j2cache.cache.support.redis.SpringRedisPubSubPolicy + + # 配置2级缓存 + j2cache.L2.provider_class = net.oschina.j2cache.cache.support.redis.SpringRedisProvider + j2cache.L2.config_section = redis + redis.hosts = localhost:6379 +* 设置使用缓存 + + @Service + public class SMSCodeServiceImpl implements SMSCodeService { + @Autowired + private CodeUtils codeUtils; + @Autowired + private CacheChannel cacheChannel; + } @Service + public class SMSCodeServiceImpl implements SMSCodeService { + @Override + public String sendCodeToSMS(String tele) { + String code = codeUtils.generator(tele); + cacheChannel.set("sms",tele,code); + return code; + } + @Override + public boolean checkCode(SMSCode smsCode) { + String code = cacheChannel.get("sms",smsCode.getTele()).asString(); + return smsCode.getCode().equals(code); + } + } + +## 16、定时 + +任务 + +* 定时任务是企业级应用中的常见操作 + + * 年度报表 + * 缓存统计报告 + * … … +* 市面上流行的定时任务技术 + + * Quartz + * Spring Task + +### 16.1、SpringBoot整合Quartz + +* 相关概念 + + * 工作(Job):用于定义具体执行的工作 + * 工作明细(JobDetail):用于描述定时工作相关的信息 + * 触发器(Trigger):用于描述触发工作的规则,通常使用cron表达式定义调度规则 + * 调度器(Scheduler):描述了工作明细与触发器的对应关系 +* 导入SpringBoot整合quartz的坐标 + + + org.springframework.boot + spring-boot-starter-quartz + +* 定义具体要执行的任务,继承QuartzJobBean + + public class QuartzTaskBean extends QuartzJobBean { + @Override + protected void executeInternal(JobExecutionContext context) throws JobExecutionException { + System.out.println(“quartz job run... "); + } + } +* 定义工作明细与触发器,并绑定对应关系 + + @Configuration + public class QuartzConfig { + @Bean + public JobDetail printJobDetail(){ + return JobBuilder.newJob(QuartzTaskBean.class).storeDurably().build(); + } + @Bean + public Trigger printJobTrigger() { + CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule("0/3 * * * * ?"); + return TriggerBuilder.newTrigger().forJob(printJobDetail()) + .withSchedule(cronScheduleBuilder).build(); + } + } + +### 16.2、Spring Task + +* 开启定时任务功能 + + @SpringBootApplication + @EnableScheduling + public class Springboot22TaskApplication { + public static void main(String[] args) { + SpringApplication.run(Springboot22TaskApplication.class, args); + } + } +* 设置定时执行的任务,并设定执行周期 + + @Component + public class ScheduledBean { + @Scheduled(cron = "0/5 * * * * ?") + public void printLog(){ + System.out.println(Thread.currentThread().getName()+":run..."); + } + } +* 定时任务相关配置 + + spring: + task: + scheduling: + # 任务调度线程池大小 默认 1 + pool: + size: 1 + # 调度线程名称前缀 默认 scheduling- + thread-name-prefix: ssm_ + shutdown: + # 线程池关闭时等待所有任务完成 + await-termination: false + # 调度线程关闭前最大等待时间,确保最后一定关闭 + await-termination-period: 10s + +### 16.3、SpringBoot整合JavaMail + +* SMTP(Simple Mail Transfer Protocol):简单邮件传输协议,用于**发送**电子邮件的传输协议 + +* POP3(Post Office Protocol - Version 3):用于**接收**电子邮件的标准协议 + +* IMAP(Internet Mail Access Protocol):互联网消息协议,是POP3的替代协议 + +* 导入SpringBoot整合JavaMail的坐标 + + + org.springframework.boot + spring-boot-starter-mail + +* 配置JavaMail + + spring: + mail: + host: smtp.qq.com + username: *********@qq.com + password: ********* + +![在这里插入图片描述](https://img-blog.csdnimg.cn/8c4baf0029074afaaca6ff4c670edd56.png) + +* 开启定时任务功能 + + @Service + public class SendMailServiceImpl implements SendMailService { + private String from = “********@qq.com"; // 发送人 + private String to = "********@126.com"; // 接收人 + private String subject = "测试邮件"; // 邮件主题 + private String text = "测试邮件正文"; // 邮件内容 + } +* 开启定时任务功能 + + @Service + public class SendMailServiceImpl implements SendMailService { + @Autowired + private JavaMailSender javaMailSender; + @Override + public void sendMail() { + SimpleMailMessage mailMessage = new SimpleMailMessage(); + mailMessage.setFrom(from); + mailMessage.setTo(to); + mailMessage.setSubject(subject); + mailMessage.setText(text); + javaMailSender.send(mailMessage); + } + } +* 附件与HTML文本支持 + + private String text = "传智教育"; + @Override + public void sendMail() { + try { + MimeMessage mimeMessage = javaMailSender.createMimeMessage(); + MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage,true); + mimeMessageHelper.setFrom(from); + mimeMessageHelper.setTo(to); + mimeMessageHelper.setSubject(subject); + mimeMessageHelper.setText(text,true); + File file = new File("logo.png"); + mimeMessageHelper.addAttachment("美图.png",file); + javaMailSender.send(mimeMessage); + } catch (Exception e) { + e.printStackTrace(); + } + } + +## 17、消息 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/fbb603a59b2b417fab3a7e26c48b2987.png) + +* 企业级应用中广泛使用的三种异步消息传递技术 + * JMS + * AMQP + * MQTT + +### 17.1、JMS + +* JMS(Java Message Service):一个规范,等同于JDBC规范,提供了与消息服务相关的API接口 + +* JMS消息模型 + + * peer-2-peer:点对点模型,消息发送到一个队列中,队列保存消息。队列的消息只能被一个消费者消费,或超时 + * **pub**lish-**sub**scribe:发布订阅模型,消息可以被多个消费者消费,生产者和消费者完全独立,不需要感知对方的存在 +* JMS消息种类 + + * TextMessage + * MapMessage + * **BytesMessage** + * StreamMessage + * ObjectMessage + * Message (只有消息头和属性) +* JMS实现:ActiveMQ、Redis、HornetMQ、RabbitMQ、RocketMQ(没有完全遵守JMS规范 + +### 17.2、AMQP + +* AMQP(advanced message queuing protocol):一种协议(高级消息队列协议,也是消息代理规范),规范了网络交换的数据格式,兼容JMS + +* 优点:具有跨平台性,服务器供应商,生产者,消费者可以使用不同的语言来实现 + +* AMQP消息模型 + + * direct exchange + * fanout exchange + * topic exchange + * headers exchange + * system exchange +* AMQP消息种类:byte[] + +* AMQP实现:RabbitMQ、StormMQ、RocketMQ + +### 17.3、MQTT + +* MQTT(Message Queueing Telemetry Transport)消息队列遥测传输,专为小设备设计,是物联网(IOT)生态系统中主要成分之一 + +### 17.4、Kafka + +* Kafka,一种高吞吐量的分布式发布订阅消息系统,提供实时消息功能。 + +### 17.5、消息案例 + +* 购物订单业务 + * 登录状态检测 + * 生成主单 + * 生成子单 + * 库存检测与变更 + * 积分变更 + * 支付 + * 短信通知(异步) + * 购物车维护 + * 运单信息初始化 + * 商品库存维护 + * 会员维护 + * … + +### 17.6、ActiveMQ + +* 下载地址:[https://activemq.apache.org/components/classic/download](https://activemq.apache.org/components/classic/download/)[/](https://activemq.apache.org/components/classic/download/) + +* 安装:解压缩 + +* 启动服务 + + `activemq.bat` + +* 访问服务器 + + `http://127.0.0.1:8161/` + + * 服务端口:61616,管理后台端口:8161 + * 用户名&密码:**admin** + +#### 17.6.1、SpringBoot整合ActiveMQ + +* 导入SpringBoot整合ActiveMQ坐标 + + + org.springframework.boot + spring-boot-starter-activemq + +* 配置ActiveMQ(采用默认配置) + + spring: + activemq: + broker-url: tcp://localhost:61616 + jms: + pub-sub-domain: true + template: + default-destination: itheima +* 生产与消费消息(使用默认消息存储队列) + + @Service + public class MessageServiceActivemqImpl implements MessageService { + @Autowired + private JmsMessagingTemplate jmsMessagingTemplate; + public void sendMessage(String id) { + System.out.println("使用Active将待发送短信的订单纳入处理队列,id:"+id); + jmsMessagingTemplate.convertAndSend(id); + } + public String doMessage() { + return jmsMessagingTemplate.receiveAndConvert(String.class); + } + } +* 生产与消费消息(指定消息存储队列) + + @Service + public class MessageServiceActivemqImpl implements MessageService { + @Autowired + private JmsMessagingTemplate jmsMessagingTemplate; + + public void sendMessage(String id) { + System.out.println("使用Active将待发送短信的订单纳入处理队列,id:"+id); + jmsMessagingTemplate.convertAndSend("order.sm.queue.id",id); + } + public String doMessage() { + return jmsMessagingTemplate.receiveAndConvert("order.sm.queue.id",String.class); + } + } +* 使用消息监听器对消息队列监听 + + @Component + public class MessageListener { + @JmsListener(destination = "order.sm.queue.id") + public void receive(String id){ + System.out.println("已完成短信发送业务,id:"+id); + } + } +* 流程性业务消息消费完转入下一个消息队列 + + @Component + public class MessageListener { + @JmsListener(destination = "order.sm.queue.id") + @SendTo("order.other.queue.id") + public String receive(String id){ + System.out.println("已完成短信发送业务,id:"+id); + return "new:"+id; + } + } + +### 17.7、RabbitMQ + +* RabbitMQ基于Erlang语言编写,需要安装Erlang + +* Erlang + + * 下载地址:[https](https://www.erlang.org/downloads)[😕/www.erlang.org/downloads](https://www.erlang.org/downloads) + * 安装:一键傻瓜式安装,安装完毕需要重启,需要依赖Windows组件 + * 环境变量配置 + * ERLANG_HOME + * PATH +* 下载地址:[https://](https://rabbitmq.com/install-windows.html)[rabbitmq.com/install-windows.html](https://rabbitmq.com/install-windows.html) + +* 安装:一键傻瓜式安装 + +* 启动服务 + + `rabbitmq-service.bat start` + +* 关闭服务 + + `rabbitmq-service.bat stop` + +* 查看服务状态 + + `rabbitmqctl status` + +* 服务管理可视化(插件形式) + +* 查看已安装的插件列表 + +* 开启服务管理插件 + + `rabbitmq-plugins.bat enable rabbitmq_management` + +* 访问服务器 + + `http://localhost:15672` + + * 服务端口:5672,管理后台端口:15672 + * 用户名&密码:**guest** + +#### 17.7.1、SpringBoot整合RabbitMQ + +* 导入SpringBoot整合RabbitMQ坐标 + + + org.springframework.boot + spring-boot-starter-amqp + +* 配置RabbitMQ (采用默认配置) + + spring: + rabbitmq: + host: localhost + port: 5672 +* 定义消息队列(direct) + + @Configuration + public class RabbitDirectConfig { + @Bean + public Queue queue(){ + return new Queue("simple_queue"); + } + } @Configuration + public class RabbitDirectConfig { + @Bean + public Queue queue(){ + // durable:是否持久化,默认false + // exclusive:是否当前连接专用,默认false,连接关闭后队列即被删除 + // autoDelete:是否自动删除,当生产者或消费者不再使用此队列,自动删除 + return new Queue("simple_queue",true,false,false); + } + } @Configuration + public class RabbitDirectConfig { + @Bean + public Queue directQueue(){ + return new Queue("direct_queue"); + } + @Bean + public Queue directQueue2(){ + return new Queue("direct_queue2"); + } + @Bean + public DirectExchange directExchange(){ + return new DirectExchange("directExchange"); + } + @Bean + public Binding bindingDirect(){ + return BindingBuilder.bind(directQueue()).to(directExchange()).with("direct"); + } + @Bean + public Binding bindingDirect2(){ + return BindingBuilder.bind(directQueue2()).to(directExchange()).with("direct2"); + } + } +* 生产与消费消息(direct) + + @Service + public class MessageServiceRabbitmqDirectImpl implements MessageService { + @Autowired + private AmqpTemplate amqpTemplate; + @Override + public void sendMessage(String id) { + System.out.println("使用Rabbitmq将待发送短信的订单纳入处理队列,id:"+id); + amqpTemplate.convertAndSend("directExchange","direct",id); + } + } +* 使用消息监听器对消息队列监听(direct) + + @Component + public class RabbitMessageListener { + @RabbitListener(queues = "direct_queue") + public void receive(String id){ + System.out.println("已完成短信发送业务,id:"+id); + } + } +* 使用多消息监听器对消息队列监听进行消息轮循处理(direct) + + @Component + public class RabbitMessageListener2 { + @RabbitListener(queues = "direct_queue") + public void receive(String id){ + System.out.println("已完成短信发送业务(two),id:"+id); + } + } +* 定义消息队列(topic) + + @Configuration + public class RabbitTopicConfig { + @Bean + public Queue topicQueue(){ return new Queue("topic_queue"); } + @Bean + public Queue topicQueue2(){ return new Queue("topic_queue2"); } + @Bean + public TopicExchange topicExchange(){ + return new TopicExchange("topicExchange"); + } + @Bean + public Binding bindingTopic(){ + return BindingBuilder.bind(topicQueue()).to(topicExchange()).with("topic.*.*"); + } + @Bean + public Binding bindingTopic2(){ + return BindingBuilder.bind(topicQueue2()).to(topicExchange()).with("topic.#"); + } + } +* 绑定键匹配规则 + + * `*`(星号): 用来表示一个单词 ,且该单词是必须出现的 + * `#`(井号): 用来表示任意数量 + +![在这里插入图片描述](https://img-blog.csdnimg.cn/bcfc4cb557714c3da413337a052de3ff.png) + +* 生产与消费消息(topic) + + @Service + public class MessageServiceRabbitmqTopicmpl implements MessageService { + @Autowired + private AmqpTemplate amqpTemplate; + @Override + public void sendMessage(String id) { + System.out.println("使用Rabbitmq将待发送短信的订单纳入处理队列,id:"+id); + amqpTemplate.convertAndSend("topicExchange","topic.order.id",id); + } + } +* 使用消息监听器对消息队列监听(topic) + + @Component + public class RabbitTopicMessageListener { + @RabbitListener(queues = "topic_queue") + public void receive(String id){ + System.out.println("已完成短信发送业务,id:"+id); + } + @RabbitListener(queues = "topic_queue2") + public void receive2(String id){ + System.out.println("已完成短信发送业务(two),id:"+id); + } + } + +### 17.8、RocketMQ + +* 下载地址:[https://rocketmq.apache.org](https://rocketmq.apache.org/)[/](https://rocketmq.apache.org/) + +* 安装:解压缩 + + * 默认服务端口:9876 +* 环境变量配置 + +* ROCKETMQ_HOME + +* PATH + +* NAMESRV_ADDR (建议): 127.0.0.1:9876 + +* 命名服务器与broker + ![在这里插入图片描述](https://img-blog.csdnimg.cn/c9ec39af08124eb7904e5cdc2abc3b43.png) + +* 启动命名服务器 + + `mqnamesrv` + +* 启动broker + + `mqbroker` + +* 服务器功能测试:生产者 + + `tools org.apache.rocketmq.example.quickstart.Producer` + +* 服务器功能测试:消费者 + + `tools org.apache.rocketmq.example.quickstart.Consumer` + +#### 17.8.1、SpringBoot整合RocketMQ + +* 导入SpringBoot整合RocketMQ坐标 + + + org.apache.rocketmq + rocketmq-spring-boot-starter + 2.2.1 + + +* 配置RocketMQ (采用默认配置) + + rocketmq: + name-server: localhost:9876 + producer: + group: group_rocketmq +* 生产消息 + + @Service + public class MessageServiceRocketmqImpl implements MessageService { + @Autowired + private RocketMQTemplate rocketMQTemplate; + @Override + public void sendMessage(String id) { + rocketMQTemplate.convertAndSend("order_sm_id",id); + System.out.println("使用Rabbitmq将待发送短信的订单纳入处理队列,id:"+id); + } + } +* 生产异步消息 + + @Service + public class MessageServiceRocketmqImpl implements MessageService { + @Autowired + private RocketMQTemplate rocketMQTemplate; + @Override + public void sendMessage(String id) { + SendCallback callback = new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + System.out.println("消息发送成功"); + } + @Override + public void onException(Throwable throwable) { + System.out.println("消息发送失败!!!!!!!!!!!"); + } + }; + System.out.println("使用Rabbitmq将待发送短信的订单纳入处理队列,id:"+id); + rocketMQTemplate.asyncSend("order_sm_id",id,callback); + } + } +* 使用消息监听器对消息队列监听 + + @Component + @RocketMQMessageListener(topic="order_sm_id",consumerGroup = "group_rocketmq") + public class RocketmqMessageListener implements RocketMQListener { + @Override + public void onMessage(String id) { + System.out.println("已完成短信发送业务,id:"+id); + } + } + +### 17.9、Kafka + +* 下载地址:[https://](https://kafka.apache.org/downloads)[kafka.apache.org/downloads](https://kafka.apache.org/downloads) + +* windows 系统下3.0.0版本存在BUG,建议使用2.X版本 + +* 安装:解压缩 + +* 启动zookeeper + + `zookeeper-server-start.bat ..\..\config\zookeeper.properties` + + * 默认端口:2181 +* 启动kafka + + `kafka-server-start.bat ..\..\config\server.properties` + + * 默认端口:9092 +* 创建topic + + `kafka-topics.bat --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic itheima` + +* 查看topic + + `kafka-topics.bat --zookeeper 127.0.0.1:2181 --list` + +* 删除topic + + `kafka-topics.bat --delete --zookeeper localhost:2181 --topic itheima` + +* 生产者功能测试 + + `kafka-console-producer.bat --broker-list localhost:9092 --topic itheima` + +* 消费者功能测试 + + `kafka-console-consumer.bat --bootstrap-server localhost:9092 --topic itheima --from-beginning` + +#### 17.9.1、SpringBoot整合Kafka + +* 导入SpringBoot整合Kafka坐标 + + + org.springframework.kafka + spring-kafka + +* 配置Kafka (采用默认配置) + + spring: + kafka: + bootstrap-servers: localhost:9092 + consumer: + group-id: order +* 生产消息 + + @Service + public class MessageServiceKafkaImpl implements MessageService { + @Autowired + private KafkaTemplate kafkaTemplate; + @Override + public void sendMessage(String id) { + System.out.println("使用Kafka将待发送短信的订单纳入处理队列,id:"+id); + kafkaTemplate.send("kafka_topic",id); } + } +* 使用消息监听器对消息队列监听 + + @Component + public class KafkaMessageListener{ + @KafkaListener(topics = {"kafka_topic"}) + public void onMessage(ConsumerRecord record) { + System.out.println("已完成短信发送业务,id:"+record.value()); + } + } + +## 18、监控 + +### 18.1、简介 + +监控的意义: + +* 监控服务状态是否宕机 +* 监控服务运行指标(内存、虚拟机、线程、请求等) +* 监控日志 +* 管理服务(服务下线) + +监控的实施方式: + +* 显示监控信息的服务器:用于获取服务信息,并显示对应的信息 +* 运行的服务:启动时主动上报,告知监控服务器自己需要受到监控 + ![在这里插入图片描述](https://img-blog.csdnimg.cn/394df97f929849a9ae91c4510b5f27ee.png) + +### 18.2、可视化监控平台 + +* Spring Boot Admin,开源社区项目,用于管理和监控SpringBoot应用程序。 客户端注册到服务端后,通过HTTP请求方式,服务端定期从客户端获取对应的信息,并通过UI界面展示对应信息。 + +* Admin服务端 + + + 2.5.4 + + + + de.codecentric + spring-boot-admin-starter-server + + + + + + de.codecentric + spring-boot-admin-dependencies + ${spring-boot-admin.version} + pom + import + + + +* Admin客户端 + + + 2.5.4 + + + + de.codecentric + spring-boot-admin-starter-client + + + + + + de.codecentric + spring-boot-admin-dependencies + ${spring-boot-admin.version} + pom + import + + + +* Admin服务端 + + + de.codecentric + spring-boot-admin-starter-server + 2.5.4 + +* Admin客户端 + + + de.codecentric + spring-boot-admin-starter-client + 2.5.4 + +* Admin服务端 + + server: + port: 8080 +* 设置启用Spring-Admin + + @SpringBootApplication + @EnableAdminServer + public class Springboot25ActuatorServerApplication { + public static void main(String[] args) { + SpringApplication.run(Springboot25ActuatorServerApplication.class, args); + } + } +* Admin客户端 + + spring: + boot: + admin: + client: + url: http://localhost:8080 + management: + endpoint: + health: + show-details: always + endpoints: + web: + exposure: + include: "*" + +![在这里插入图片描述](https://img-blog.csdnimg.cn/7102e30a17524139b1bb8f20c1776756.png) + +### 18.3、监控原理 + +* Actuator提供了SpringBoot生产就绪功能,通过端点的配置与访问,获取端点信息 + +* 端点描述了一组监控信息,SpringBoot提供了多个内置端点,也可以根据需要自定义端点信息 + +* 访问当前应用所有端点信息:**/actuator** + +* 访问端点详细信息:/actuator**/****端点名称** + ![在这里插入图片描述](https://img-blog.csdnimg.cn/342450058b5940738d2dfe692fa63a10.png) + +![在这里插入图片描述](https://img-blog.csdnimg.cn/8c62c1bf3be0479d85f2ddcb18c34422.png) + +* Web程序专用端点 + ![在这里插入图片描述](https://img-blog.csdnimg.cn/13dfbc9b4b3e4cd6b91cc22519c4e2a4.png) + +* 启用指定端点 + + management: + endpoint: + health: # 端点名称 + enabled: true show-details: always beans: # 端点名称 enabled: true +* 启用所有端点 + + management: + endpoints: + enabled-by-default: true +* 暴露端点功能 + + * 端点中包含的信息存在敏感信息,需要对外暴露端点功能时手动设定指定端点信息 + ![在这里插入图片描述](https://img-blog.csdnimg.cn/cf157f09d9274dd68ffe9b83f6efc106.png) + ![在这里插入图片描述](https://img-blog.csdnimg.cn/1d82e3fd9f254fd381acc37c05dfc09f.png) + ![在这里插入图片描述](https://img-blog.csdnimg.cn/76cc94690520440fa27368ebb195eee9.png) + +### 18.4、自定义监控指标 + +* 为info端点添加自定义指标 + + info: + appName: @project.artifactId@ + version: @project.version@ + author: itheima @Component + public class AppInfoContributor implements InfoContributor { + @Override + public void contribute(Info.Builder builder) { + Map infoMap = new HashMap<>(); + infoMap.put("buildTime","2006"); + builder.withDetail("runTime",System.currentTimeMillis()) + .withDetail("company","传智教育"); + builder.withDetails(infoMap); + } + } +* 为Health端点添加自定义指标 + + @Component + public class AppHealthContributor extends AbstractHealthIndicator { + @Override + protected void doHealthCheck(Health.Builder builder) throws Exception { + boolean condition = true; + if(condition){ + Map infoMap = new HashMap<>(); + infoMap.put("buildTime","2006"); + builder.withDetail("runTime",System.currentTimeMillis()) + .withDetail("company","传智教育"); + builder.withDetails(infoMap); + builder.status(Status.UP); + }else{ + builder.status(Status.DOWN); + } + } + } +* 为Metrics端点添加自定义指标 + + @Service + public class BookServiceImpl extends ServiceImpl implements IBookService { + private Counter counter; + public BookServiceImpl(MeterRegistry meterRegistry){ + counter = meterRegistry.counter("用户付费操作次数:"); + } + @Override + public boolean delete(Integer id) { + counter.increment(); + return bookDao.deleteById(id) > 0; + } + } +* 自定义端点 + + @Component + @Endpoint(id="pay") + public class PayEndPoint { + @ReadOperation + public Object getPay(){ + //调用业务操作,获取支付相关信息结果,最终return出去 + Map payMap = new HashMap(); + payMap.put("level 1",103); + payMap.put("level 2",315); + payMap.put("level 3",666); + return payMap; + } + } + +pom +import + + - Admin客户端 + +```xml + + 2.5.4 + + + + de.codecentric + spring-boot-admin-starter-client + + + + + + de.codecentric + spring-boot-admin-dependencies + ${spring-boot-admin.version} + pom + import + + + + +* Admin服务端 + + + de.codecentric + spring-boot-admin-starter-server + 2.5.4 + +* Admin客户端 + + + de.codecentric + spring-boot-admin-starter-client + 2.5.4 + +* Admin服务端 + + server: + port: 8080 +* 设置启用Spring-Admin + + @SpringBootApplication + @EnableAdminServer + public class Springboot25ActuatorServerApplication { + public static void main(String[] args) { + SpringApplication.run(Springboot25ActuatorServerApplication.class, args); + } + } +* Admin客户端 + + spring: + boot: + admin: + client: + url: http://localhost:8080 + management: + endpoint: + health: + show-details: always + endpoints: + web: + exposure: + include: "*" + + [外链图片转存中…(img-cXWfQSEx-1657811363485)] + +### 18.3、监控原理 + +* Actuator提供了SpringBoot生产就绪功能,通过端点的配置与访问,获取端点信息 + +* 端点描述了一组监控信息,SpringBoot提供了多个内置端点,也可以根据需要自定义端点信息 + +* 访问当前应用所有端点信息:**/actuator** + +* 访问端点详细信息:/actuator**/****端点名称** + + [外链图片转存中…(img-KSdMaD18-1657811363486)] + + [外链图片转存中…(img-UlUbALwe-1657811363487)] + +* Web程序专用端点 + + [外链图片转存中…(img-maGlhCAb-1657811363487)] + +* 启用指定端点 + + management: + endpoint: + health: # 端点名称 + enabled: true show-details: always beans: # 端点名称 enabled: true +* 启用所有端点 + + management: + endpoints: + enabled-by-default: true +* 暴露端点功能 + + * 端点中包含的信息存在敏感信息,需要对外暴露端点功能时手动设定指定端点信息 + + [外链图片转存中…(img-6UeTKYgJ-1657811363488)] + +[外链图片转存中…(img-bLPYJP4S-1657811363489)] + +[外链图片转存中…(img-RwCI70cm-1657811363490)] + +### 18.4、自定义监控指标 + +* 为info端点添加自定义指标 + + info: + appName: @project.artifactId@ + version: @project.version@ + author: itheima @Component + public class AppInfoContributor implements InfoContributor { + @Override + public void contribute(Info.Builder builder) { + Map infoMap = new HashMap<>(); + infoMap.put("buildTime","2006"); + builder.withDetail("runTime",System.currentTimeMillis()) + .withDetail("company","传智教育"); + builder.withDetails(infoMap); + } + } +* 为Health端点添加自定义指标 + + @Component + public class AppHealthContributor extends AbstractHealthIndicator { + @Override + protected void doHealthCheck(Health.Builder builder) throws Exception { + boolean condition = true; + if(condition){ + Map infoMap = new HashMap<>(); + infoMap.put("buildTime","2006"); + builder.withDetail("runTime",System.currentTimeMillis()) + .withDetail("company","传智教育"); + builder.withDetails(infoMap); + builder.status(Status.UP); + }else{ + builder.status(Status.DOWN); + } + } + } +* 为Metrics端点添加自定义指标 + + @Service + public class BookServiceImpl extends ServiceImpl implements IBookService { + private Counter counter; + public BookServiceImpl(MeterRegistry meterRegistry){ + counter = meterRegistry.counter("用户付费操作次数:"); + } + @Override + public boolean delete(Integer id) { + counter.increment(); + return bookDao.deleteById(id) > 0; + } + } +* 自定义端点 + + @Component + @Endpoint(id="pay") + public class PayEndPoint { + @ReadOperation + public Object getPay(){ + //调用业务操作,获取支付相关信息结果,最终return出去 + Map payMap = new HashMap(); + payMap.put("level 1",103); + payMap.put("level 2",315); + payMap.put("level 3",666); + return payMap; + } + } \ No newline at end of file diff --git "a/docs/interview-experience/\345\220\204\345\244\247\345\205\254\345\217\270\351\235\242\347\273\217.md" "b/docs/interview-experience/\345\220\204\345\244\247\345\205\254\345\217\270\351\235\242\347\273\217.md" index 03ebf3e..1c0fbd0 100644 --- "a/docs/interview-experience/\345\220\204\345\244\247\345\205\254\345\217\270\351\235\242\347\273\217.md" +++ "b/docs/interview-experience/\345\220\204\345\244\247\345\205\254\345\217\270\351\235\242\347\273\217.md" @@ -1,3 +1,49 @@ +# 京东 + +### 京东一面 + +1、Java中的乐观锁悲观锁 + +https://segmentfault.com/a/1190000016611415 + +2、单点登录 +3、集合的问题 +4、反转链表 +5、二分查找 +6、堆排序 +7、JUC +8、Java中如何实现线程安全 +9、ArrayList和linkedList区别 +10、数组和链表区别 +11、volitale + +### 秋招一面 + +1、数据库死锁及解决方案 + +https://blog.csdn.net/cbjcry/article/details/84920174 + + +2、数据库分库分表后的分页查询及相关操作怎么解决 + +https://crossoverjie.top/2019/07/24/framework-design/sharding-db-03/ +https://juejin.im/entry/6844903478171533320 +https://tech.meituan.com/2016/11/18/dianping-order-db-sharding.html + + +### 秋招二面 + +1、UML +https://www.jianshu.com/p/28200121a33d +2、超时确认,快速重传原理,快速重传重传几次,3次 +https://blog.csdn.net/u010710458/article/details/79968648 +https://www.cnblogs.com/postw/p/9678454.html +3、singleThreadThreadPool相对于ThreadPoolExecutor的优势 +4、可重复读机制 +https://www.pianshen.com/article/11361872925/ + + + # 阿里 - [阿里社招四面(分布式)](https://www.nowcoder.com/discuss/349542) @@ -19,6 +65,18 @@ 8、maven:如何查找重复的jar包问题 9、单点登录如何做的,如果保证安全性 +**缺点**:linux实战的不懂 + +### 钉钉二面 + +1、项目有什么难点:应该不管是什么项目都是数据库优化跟JVM问题排查 +2、操作系统内存交换方式 +3、tcp7层模型 +4、平时学习方法 +5、自己博客写的好的进行介绍 + +**缺点**:重点不突出、操作系统不清楚。 + # 腾讯 @@ -77,6 +135,36 @@ # 头条 +### 一面 + +1、多线程调度原理 +https://zhuanlan.zhihu.com/p/58846439 +2、select、poll、epoll +3、多线程原理与操作系统 +https://www.zhihu.com/question/25527028 +4、redis的单线程模型:单核会有多线程切换吗 +https://blog.csdn.net/qq_27185561/article/details/82900426 +5、算法、算法、算法、算法。 +6、线程池用到的数据结构 +7、最长上升子序列的个数 +8、为什么myisam快:非聚集索引,B+树,为什么用B+树和B树的区别。为什么B+树IO次数少 +9、MyISAM与InnoDB 的区别 +https://www.cnblogs.com/fwqblogs/p/6645274.html +10、事务隔离级别:可重复读会发生幻读吗?会 + +### 二面 + +1、hashmap解决冲突方法 +2、hashmap扩容机制 +3、jvm的jstack作用 +4、最长不重复子串 +5、https + + + + + + # 拼多多 @@ -133,6 +221,14 @@ https://www.cnblogs.com/JackPn/p/9386182.html # 快手 +1、项目 +2、两数之和等于target +3、http介绍 +4、输入网址的过程 +5、dubbo原理 +6、http和dubbo协议的区别 + +**缺点**:写算法还不熟练 #### Tip:本来有很多我准备的资料的,但是都是外链,或者不合适的分享方式,所以大家去公众号回复【资料】好了。 diff --git "a/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\347\237\245\350\257\206.md" "b/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\347\237\245\350\257\206.md" deleted file mode 100644 index d5c41a0..0000000 --- "a/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\347\237\245\350\257\206.md" +++ /dev/null @@ -1,90 +0,0 @@ -# 阿里 - -linux进程结构 -linux文件系统 -linux统计文件数量 -spingioc,springaop中的cglib动态 -shell脚本 -webLogic知道不? 与tomcat区别? -webservice -HashMap链表转红黑树是怎么实现的 -进程通信 -散列表(哈希表) -Java中pv操作实现时用的类 -说一下TCP/IP的状态转移 -说一下NIO -线程通信 -写过异步的代码吗 -拜占庭算法的理解? -红黑树么,在插入上有什么优化? -resetful风格 -分布式rpc调度过程中要注意的问题 -hashmap并发读写死循环问题 -快排时间复杂度?最好什么情况,最坏什么情况?有什么改进方案? -HashMap get和put源码,为什么红黑而非平衡树? -分布式系统CAP理论,重点解释分区容错性的意义 -对虚拟内存的理解 -三个线程如何实现交替打印ABC -线程池中LinkedBlockingQueue满了的话,线程会怎么样 -HashMap和ConcurrentHashMap哪个效率更高?为什么? -mybatis如何进行类型转换 -myql间歇锁的实现原理 -future的底层实现异步原理 -rpc原理 -多个服务端上下线怎么感知 -降级处理hystrix了解过么 -redis的热点key问题 -一致性哈希 -ClassLoader原理和应用 -注解的原理 - - - -# 腾讯 - - - -# 百度 - - - -# 滴滴 - - - -# 头条 - - - -# 拼多多 - - - -# 美团 - - - -# 小米 - - - -# 网易 - - - -# 华为 - - -#### Tip:本来有很多我准备的资料的,但是都是外链,或者不合适的分享方式,所以大家去公众号回复【资料】好了。 - -![](http://image.ouyangsihai.cn/FszE5cIon6eHHexBEgOSBGBWeoyP) - -现在免费送给大家,在我的公众号 **好好学java** 回复 **资料** 即可获取。 - -有收获?希望老铁们来个三连击,给更多的人看到这篇文章 - -1、老铁们,关注我的原创微信公众号「**好好学java**」,专注于Java、数据结构和算法、微服务、中间件等技术分享,保证你看完有所收获。 - -![](http://image.ouyangsihai.cn/FgUUPlQOlQtjbbdOs1RZK9gWxitV) - -2、给俺一个 **star** 呗,可以让更多的人看到这篇文章,顺便激励下我继续写作,嘻嘻。 \ No newline at end of file diff --git "a/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\345\210\206\347\261\273\346\261\207\346\200\273.md" "b/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\345\210\206\347\261\273\346\261\207\346\200\273.md" index 7857062..5c926ef 100644 --- "a/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\345\210\206\347\261\273\346\261\207\346\200\273.md" +++ "b/docs/interview-experience/\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\345\210\206\347\261\273\346\261\207\346\200\273.md" @@ -1,96 +1,398 @@ -# Java基础 -#### 反射在jvm层面的实现 + + + + +- [一 Java基础](#一-java基础) + - [一致性hash算法](#一致性hash算法) + - [sleep和wait](#sleep和wait) + - [强软弱虚引用](#强软弱虚引用) + - [Arrays.sort原理](#arrayssort原理) + - [创建对象的方式](#创建对象的方式) + - [若hashcode方法永远返回1会产生什么结果](#若hashcode方法永远返回1会产生什么结果) + - [解决hash冲突的三种方法](#解决hash冲突的三种方法) + - [为什么要重写hashCode()方法和equals()方法以及如何进行重写](#为什么要重写hashcode方法和equals方法以及如何进行重写) + - [动态代理](#动态代理) + - [sleep和wait的区别](#sleep和wait的区别) + - [java 地址和值传递的例子](#java-地址和值传递的例子) + - [Java序列化](#java序列化) + - [java NIO,java 多线程、线程池,java 网络编程解决并发量](#java-niojava-多线程-线程池java-网络编程解决并发量) + - [JDBC 连接的过程 ,手写 jdbc 连接过程](#jdbc-连接的过程-手写-jdbc-连接过程) + - [说出三个遇到过的程序报异常的情况](#说出三个遇到过的程序报异常的情况) + - [socket 是靠什么协议支持的](#socket-是靠什么协议支持的) + - [java io 用到什么设计模式](#java-io-用到什么设计模式) + - [serviable 的序列化,其中 uuid 的作用](#serviable-的序列化其中-uuid-的作用) + - [什么情景下会用到反射](#什么情景下会用到反射) + - [浅克隆与深克隆有什么区别,如何实现深克隆](#浅克隆与深克隆有什么区别如何实现深克隆) + - [反射能够使用私有的方法属性吗和底层原理?](#反射能够使用私有的方法属性吗和底层原理) + - [处理器指令优化有些什么考虑?](#处理器指令优化有些什么考虑) + - [object 对象的常用方法](#object-对象的常用方法) + - [Stack 和 ArrayList 的区别](#stack-和-arraylist-的区别) + - [statement 和 prestatement 的区别](#statement-和-prestatement-的区别) + - [手写模拟实现一个阻塞队列](#手写模拟实现一个阻塞队列) + - [util 包下有哪几种接口](#util-包下有哪几种接口) + - [很常见的 Nullpointerexception ,你是怎么排查的,怎么解决的;](#很常见的-nullpointerexception-你是怎么排查的怎么解决的) + - [静态内部类和非静态内部类的区别是什么?](#静态内部类和非静态内部类的区别是什么) + - [怎么创建静态内部类和非静态内部类?](#怎么创建静态内部类和非静态内部类) + - [Xml 解析方式,原理优缺点](#xml-解析方式原理优缺点) + - [静态变量和全局变量的区别](#静态变量和全局变量的区别) +- [二 Java集合](#二-java集合) + - [hashmap的jdk1.7和jdk1.8区别](#hashmap的jdk17和jdk18区别) + - [concurrenthashmap的jdk1.7和jdk1.8区别](#concurrenthashmap的jdk17和jdk18区别) + - [HashMap 实现原理,扩容因子过大过小的缺点,扩容过程 采用什么方法能保证每个 bucket 中的数据更均匀 解决冲突的方式,还有没有其他方式(全域哈希)](#hashmap-实现原理扩容因子过大过小的缺点扩容过程-采用什么方法能保证每个-bucket-中的数据更均匀-解决冲突的方式还有没有其他方式全域哈希) + - [Collection 集合类中只能在 Iterator 中删除元素的原因](#collection-集合类中只能在-iterator-中删除元素的原因) + - [ArrayList、LinkedList、Vector](#arraylist-linkedlist-vector) + - [还了解除 util 其他包下的 List 吗?](#还了解除-util-其他包下的-list-吗) + - [CopyOnWriteArrayList](#copyonwritearraylist) + - [ConcurrentHashMap 和 LinkedHashMap 差异和适用情形](#concurrenthashmap-和-linkedhashmap-差异和适用情形) + - [ConcurrentHashMap分段锁是如何实现,ConcurrentHashmap jdk1.8 访问的时候是怎么加锁的,插入的时候是怎么加锁的 访问不加 锁插入的时候对头结点加锁](#concurrenthashmap分段锁是如何实现concurrenthashmap-jdk18-访问的时候是怎么加锁的插入的时候是怎么加锁的-访问不加-锁插入的时候对头结点加锁) + - [ArrayDeque 的使用场景](#arraydeque-的使用场景) + - [ArrayBlockingQueue 源码](#arrayblockingqueue-源码) + - [hashmap 和 treemap 的区别](#hashmap-和-treemap-的区别) + - [hashmap](#hashmap) + - [treemap](#treemap) + - [rehash 过程](#rehash-过程) + - [HashMap 的负载因子,为什么容量为2^n](#hashmap-的负载因子为什么容量为2n) + - [list,map,set 之间的区别](#listmapset-之间的区别) + - [什么时候会用到 HashMap](#什么时候会用到-hashmap) + - [常见的线程安全的集合类](#常见的线程安全的集合类) +- [三 JVM](#三-jvm) + - [反射在jvm层面的实现](#反射在jvm层面的实现) + - [jvm的方法区存什么?](#jvm的方法区存什么) + - [JDK1.8 JVM方法区变成了什么,为什么这样做](#jdk18-jvm方法区变成了什么为什么这样做) + - [oom出现的原因](#oom出现的原因) + - [Class.forName和ClassLoader的区别](#classforname和classloader的区别) + - [java对象信息分配](#java对象信息分配) + - [java虚拟机ZGC详解](#java虚拟机zgc详解) + - [java虚拟机CMS详解](#java虚拟机cms详解) + - [java虚拟机G1详解](#java虚拟机g1详解) + - [JVM tomcat 容器启动,jvm 加载情况描述](#jvm-tomcat-容器启动jvm-加载情况描述) +- [四 多线程并发](#四-多线程并发) + - [volitale使用场景](#volitale使用场景) + - [可重入锁,实现原理](#可重入锁实现原理) + - [Java 无锁原理](#java-无锁原理) + - [讲讲多线程,多线程的同步方法](#讲讲多线程多线程的同步方法) + - [synchronized原理](#synchronized原理) + - [reetrantlock](#reetrantlock) + - [java 线程安全都体现在哪些方面](#java-线程安全都体现在哪些方面) + - [如果维护线程安全 如果想实现一个线程安全的队列,可以怎么实现?](#如果维护线程安全-如果想实现一个线程安全的队列可以怎么实现) + - [Java多线程通信方式](#java多线程通信方式) + - [CountDownLatch、CyclicBarrier、Semaphore 用法总结](#countdownlatch-cyclicbarrier-semaphore-用法总结) + - [juc下的内容](#juc下的内容) + - [AOS等并发相关面试题](#aos等并发相关面试题) + - [threadlocal](#threadlocal) + - [java 线程池达到提交上限的具体情况 ,线程池用法,Java 多线程,线程池有哪几类,每一类的差别](#java-线程池达到提交上限的具体情况-线程池用法java-多线程线程池有哪几类每一类的差别) + - [要你设计的话,如何实现一个线程池](#要你设计的话如何实现一个线程池) + - [线程池的类型,固定大小的线程池内部是如何实现的,等待队列是用了哪一个队列实现 线程池种类和工作流程](#线程池的类型固定大小的线程池内部是如何实现的等待队列是用了哪一个队列实现-线程池种类和工作流程) + - [线程池使用了什么设计模式](#线程池使用了什么设计模式) + - [线程池使用时一般要考虑哪些问题](#线程池使用时一般要考虑哪些问题) + - [线程池的配置](#线程池的配置) + - [Excutor 以及 Connector 的配置](#excutor-以及-connector-的配置) +- [五 Java框架(ssm)](#五-java框架ssm) +- [hibernate](#hibernate) + - [Hibernate 的生成策略](#hibernate-的生成策略) + - [Hibernate 与 Mybatis 区别](#hibernate-与-mybatis-区别) + - [Mybatis原理](#mybatis原理) + - [mybatis执行select的过程](#mybatis执行select的过程) + - [mybatis有哪些executors](#mybatis有哪些executors) + - [mybatis插件原理](#mybatis插件原理) + - [mybatis二级缓存](#mybatis二级缓存) +- [spring&springmvc](#springspringmvc) + - [spring中的设计模式](#spring中的设计模式) + - [spring中bean的作用域](#spring中bean的作用域) + - [BeanFactory和FactoryBean区别](#beanfactory和factorybean区别) + - [aspect的种类](#aspect的种类) + - [spring aop的实际应用](#spring-aop的实际应用) + - [spring实现多线程安全](#spring实现多线程安全) + - [spring的bean的高并发安全问题](#spring的bean的高并发安全问题) + - [ioc aop总结(概述性)](#ioc-aop总结概述性) + - [Spring 的加载流程,Spring 的源码中 Bean 的构造的流程](#spring-的加载流程spring-的源码中-bean-的构造的流程) + - [Spring 事务源码,IOC 源码,AOP 源码](#spring-事务源码ioc-源码aop-源码) + - [spring 的作用及理解 事务怎么配置](#spring-的作用及理解-事务怎么配置) + - [spring事务失效情况](#spring事务失效情况) + - [Spring 的 annotation 如何实现](#spring-的-annotation-如何实现) + - [SpringMVC 工作原理](#springmvc-工作原理) + - [了解 SpringMVC 与 Struct2 区别](#了解-springmvc-与-struct2-区别) + - [springMVC 和 spring 是什么关系](#springmvc-和-spring-是什么关系) + - [项目中 Spring 的 IOC 和 AOP 具体怎么使用的](#项目中-spring-的-ioc-和-aop-具体怎么使用的) + - [spring mvc 底层实现原理](#spring-mvc-底层实现原理) + - [动态代理的原理](#动态代理的原理) + - [如果使用 spring mvc,那 post 请求跟 put 请求有什么区别啊; 然后开始问 springmvc:描述从 tomcat 开始到 springmvc 返回到前端显示的整个流程,接着问 springmvc 中的 handlerMapping 的内部实现,然后又问 spring 中从载入 xml 文件到 getbean 整个流程,描述一遍](#如果使用-spring-mvc那-post-请求跟-put-请求有什么区别啊-然后开始问-springmvc描述从-tomcat-开始到-springmvc-返回到前端显示的整个流程接着问-springmvc-中的-handlermapping-的内部实现然后又问-spring-中从载入-xml-文件到-getbean-整个流程描述一遍) +- [六 微服务(springboot等)](#六-微服务springboot等) + - [springboot](#springboot) + - [springcloud](#springcloud) +- [七 数据结构](#七-数据结构) + - [二叉树相关](#二叉树相关) + - [红黑树](#红黑树) +- [八 数据库](#八-数据库) +- [MySQL](#mysql) + - [数据库死锁问题](#数据库死锁问题) + - [hash索引和B+树索引的区别](#hash索引和b树索引的区别) + - [可重复的原理MVCC](#可重复的原理mvcc) + - [count(1)、count(*)、count(列名)](#count1-count-count列名) + - [mysql的undo、redo、binlog的区别](#mysql的undo-redo-binlog的区别) + - [explain解释](#explain解释) + - [mysql分页查询优化](#mysql分页查询优化) + - [sql注入](#sql注入) + - [为什么用B+树](#为什么用b树) + - [sql执行流程](#sql执行流程) + - [聚集索引与非聚集索引](#聚集索引与非聚集索引) + - [覆盖索引](#覆盖索引) + - [sql总结](#sql总结) + - [有人建议给每张表都建一个自增主键,这样做有什么优点跟缺点](#有人建议给每张表都建一个自增主键这样做有什么优点跟缺点) + - [对 MySQL 的了解,和 oracle 的区别](#对-mysql-的了解和-oracle-的区别) + - [500万数字排序,内存只能容纳5万个,如何排序,如何优化?](#500万数字排序内存只能容纳5万个如何排序如何优化) + - [平时怎么写数据库的模糊查询(由字典树扯到模糊查询,前缀查询,例如“abc%”,还是索引策略的问题)](#平时怎么写数据库的模糊查询由字典树扯到模糊查询前缀查询例如abc还是索引策略的问题) + - [数据库里有 10000000 条用户信息,需要给每位用户发送信息(必须发送成功),要求节省内存](#数据库里有-10000000-条用户信息需要给每位用户发送信息必须发送成功要求节省内存) + - [项目中如何实现事务](#项目中如何实现事务) + - [数据库设计一般设计成第几范式](#数据库设计一般设计成第几范式) + - [mysql 用的什么版本 5.7 跟 5.6 有啥区别](#mysql-用的什么版本-57-跟-56-有啥区别) + - [提升 MySQL 安全性](#提升-mysql-安全性) + - [问了一个这样的表(三个字段:姓名,id,分数)要求查出平均分大于 80 的 id 然后分数降序排序,然后经过提示用聚合函数 avg。](#问了一个这样的表三个字段姓名id分数要求查出平均分大于-80-的-id-然后分数降序排序然后经过提示用聚合函数-avg) + - [为什么 mysql 事务能保证失败回滚](#为什么-mysql-事务能保证失败回滚) + - [主键索引底层的实现原理](#主键索引底层的实现原理) + - [经典的01索引问题?](#经典的01索引问题) + - [如何在长文本中快捷的筛选出你的名字?](#如何在长文本中快捷的筛选出你的名字) + - [多列索引及最左前缀原则和其他使用场景](#多列索引及最左前缀原则和其他使用场景) + - [事务隔离级别](#事务隔离级别) + - [索引的最左前缀原则](#索引的最左前缀原则) + - [数据库悲观锁怎么实现的](#数据库悲观锁怎么实现的) + - [建表的原则](#建表的原则) + - [索引的内涵和用法](#索引的内涵和用法) + - [给了两条 SQL 语句,让根据这两条语句建索引(个人想法:主要考虑复合索引只能匹配前缀列的特点)](#给了两条-sql-语句让根据这两条语句建索引个人想法主要考虑复合索引只能匹配前缀列的特点) + - [那么我们来聊一下数据库。A 和 B 两个表做等值连接(Inner join) 怎么优化](#那么我们来聊一下数据库a-和-b-两个表做等值连接inner-join-怎么优化) + - [数据库连接池的理解和优化](#数据库连接池的理解和优化) + - [Sql语句分组排序](#sql语句分组排序) + - [SQL语句的5个连接概念](#sql语句的5个连接概念) + - [数据库优化和架构(主要是主从分离和分库分表相关)](#数据库优化和架构主要是主从分离和分库分表相关) + - [分库分表](#分库分表) + - [跨库join实现](#跨库join实现) + - [探讨主从分离和分库分表相关](#探讨主从分离和分库分表相关) + - [数据库中间件](#数据库中间件) + - [读写分离在中间件的实现](#读写分离在中间件的实现) + - [限流 and 熔断](#限流-and-熔断) + - [行锁适用场景](#行锁适用场景) +- [Redis](#redis) + - [redis为什么快?](#redis为什么快) + - [Redis 数据结构原理](#redis-数据结构原理) + - [Redis 持久化机制](#redis-持久化机制) + - [Redis 的一致性哈希算法](#redis-的一致性哈希算法) + - [redis了解多少](#redis了解多少) + - [redis五种数据类型,当散列类型的 value 值非常大的时候怎么进行压缩](#redis五种数据类型当散列类型的-value-值非常大的时候怎么进行压缩) + - [用redis怎么实现摇一摇与附近的人功能](#用redis怎么实现摇一摇与附近的人功能) + - [redis 主从复制过程](#redis-主从复制过程) + - [Redis 如何解决 key 冲突](#redis-如何解决-key-冲突) + - [redis 是怎么存储数据的](#redis-是怎么存储数据的) + - [redis 使用场景](#redis-使用场景) +- [九 计算机网络](#九-计算机网络) + - [cookie 禁用怎么办](#cookie-禁用怎么办) + - [Netty new 实例化过程](#netty-new-实例化过程) + - [socket 实现过程,具体用的方法;怎么实现异步 socket.](#socket-实现过程具体用的方法怎么实现异步-socket) + - [浏览器的缓存机制](#浏览器的缓存机制) + - [http相关问题](#http相关问题) + - [TCP三次握手第三次握手时ACK丢失怎么办](#tcp三次握手第三次握手时ack丢失怎么办) + - [dns属于udp还是tcp,原因](#dns属于udp还是tcp原因) + - [http的幂等性](#http的幂等性) + - [建立连接的过程客户端跟服务端会交换什么信息(参考 TCP 报文结构)](#建立连接的过程客户端跟服务端会交换什么信息参考-tcp-报文结构) + - [丢包如何解决重传的消耗](#丢包如何解决重传的消耗) + - [traceroute 实现原理](#traceroute-实现原理) + - [IO多路复用](#io多路复用) + - [select 和 poll 区别?](#select-和-poll-区别) + - [在不使用 WebSocket 情况下怎么实现服务器推送的一种方法](#在不使用-websocket-情况下怎么实现服务器推送的一种方法) + - [可以使用客户端定时刷新请求或者和 TCP 保持心跳连接实现。](#可以使用客户端定时刷新请求或者和-tcp-保持心跳连接实现) + - [查看磁盘读写吞吐量?](#查看磁盘读写吞吐量) + - [PING 位于哪一层](#ping-位于哪一层) + - [网络重定向,说下流程](#网络重定向说下流程) + - [controller 怎么处理的请求:路由](#controller-怎么处理的请求路由) + - [IP 地址分为几类,每类都代表什么,私网是哪些](#ip-地址分为几类每类都代表什么私网是哪些) +- [十 操作系统](#十-操作系统) + - [Java I/O 底层细节,注意是底层细节,而不是怎么用](#java-io-底层细节注意是底层细节而不是怎么用) + - [Java IO 模型(BIO,NIO 等) ,Tomcat 用的哪一种模型](#java-io-模型bionio-等-tomcat-用的哪一种模型) + - [当获取第一个获取锁之后,条件不满足需要释放锁应当怎么做?](#当获取第一个获取锁之后条件不满足需要释放锁应当怎么做) + - [手写一个线程安全的生产者与消费者。](#手写一个线程安全的生产者与消费者) + - [进程和线程调度方法](#进程和线程调度方法) +- [linux](#linux) + - [linux查找命令](#linux查找命令) + - [项目部署常见linux命令](#项目部署常见linux命令) + - [进程文件里有哪些信息](#进程文件里有哪些信息) + - [sed 和 awk 的区别](#sed-和-awk-的区别) + - [linux查看进程并杀死的命令](#linux查看进程并杀死的命令) + - [有一个文件被锁住,如何查看锁住它的线程?](#有一个文件被锁住如何查看锁住它的线程) + - [如何查看一个文件第100行到150行的内容](#如何查看一个文件第100行到150行的内容) + - [如何查看进程消耗的资源](#如何查看进程消耗的资源) + - [如何查看每个进程下的线程?](#如何查看每个进程下的线程) + - [linux 如何查找文件](#linux-如何查找文件) + - [select epoll等问题](#select-epoll等问题) +- [十一 框架其他](#十一-框架其他) + - [Servlet 的 Filter 用的什么设计模式](#servlet-的-filter-用的什么设计模式) + - [zookeeper 的常用功能,自己用它来做什么](#zookeeper-的常用功能自己用它来做什么) + - [redis 的操作是不是原子操作](#redis-的操作是不是原子操作) + - [秒杀业务场景设计](#秒杀业务场景设计) + - [如何设计淘宝秒杀系统(重点关注架构,比如数据一致性,数据库集群一致性哈希,缓存, 分库分表等等)](#如何设计淘宝秒杀系统重点关注架构比如数据一致性数据库集群一致性哈希缓存-分库分表等等) + - [对后台的优化有了解吗?比如负载均衡](#对后台的优化有了解吗比如负载均衡) + - [对 Restful 了解 Restful 的认识,优点,以及和 soap 的区别](#对-restful-了解-restful-的认识优点以及和-soap-的区别) + - [lrucache 的基本原理](#lrucache-的基本原理) +- [十二 设计模式](#十二-设计模式) + - [Java常见设计模式](#java常见设计模式) +- [十三 分布式](#十三-分布式) + - [dubbo中的dubbo协议和http协议有什么区别?](#dubbo中的dubbo协议和http协议有什么区别) + - [负载均衡](#负载均衡) + - [分布式锁的实现方式及优缺点](#分布式锁的实现方式及优缺点) + - [CAP](#cap) + - [如何实现分布式缓存](#如何实现分布式缓存) +- [十四 其他](#十四-其他) + - [Java 8 函数式编程 回调函数](#java-8-函数式编程-回调函数) + - [函数式编程,面向对象之间区别](#函数式编程面向对象之间区别) + - [Java 8 中 stream 迭代的优势和区别?](#java-8-中-stream-迭代的优势和区别) + - [同步等于可见性吗?](#同步等于可见性吗) + - [git底层数据结构](#git底层数据结构) + - [安全加密](#安全加密) + - [web安全问题](#web安全问题) + + + + +### 一 Java基础 + +#### 一致性hash算法 + +https://blog.csdn.net/qq_40551994/article/details/100991581 + +#### sleep和wait + +https://blog.csdn.net/qq_20009015/article/details/89980966 +https://blog.csdn.net/lengyue309/article/details/79639245 + +#### 强软弱虚引用 + +https://blog.csdn.net/junjunba2689/article/details/80601729 + +#### Arrays.sort原理 + +https://www.cnblogs.com/baichunyu/p/11935995.html + +#### 创建对象的方式 + +https://blog.csdn.net/w410589502/article/details/56489294 -https://www.jianshu.com/p/b6cb4c694951 +#### 若hashcode方法永远返回1会产生什么结果 -#### mysql语句分别会加什么锁 +https://blog.csdn.net/cnq2328/article/details/50436175 -https://blog.csdn.net/iceman1952/article/details/85504278 +#### 解决hash冲突的三种方法 -#### 若hashcode方法永远返回1会产生什么结果 +https://blog.csdn.net/qq_32595453/article/details/80660676 -https://blog.csdn.net/cnq2328/article/details/50436175 +#### 为什么要重写hashCode()方法和equals()方法以及如何进行重写 -#### jvm的方法区存什么? +https://blog.csdn.net/xlgen157387/article/details/63683882 -https://www.jianshu.com/p/10584345b10a +#### 动态代理 -#### Class.forName和ClassLoader的区别 +https://segmentfault.com/a/1190000011291179 -https://blog.csdn.net/qq_27093465/article/details/52262340 +#### sleep和wait的区别 -#### java对象信息分配 +https://blog.csdn.net/u012050154/article/details/50903326 -https://blog.csdn.net/u014520047/article/details/81940447 +#### java 地址和值传递的例子 -#### java虚拟机ZGC详解 +https://www.cnblogs.com/zhangyu317/p/11226105.html -https://vimsky.com/article/4162.html +#### Java序列化 -#### java虚拟机CMS详解 +https://juejin.im/post/5ce3cdc8e51d45777b1a3cdf -https://juejin.im/post/5c7262a15188252f30484351 +#### java NIO,java 多线程、线程池,java 网络编程解决并发量 -#### java虚拟机G1详解 +Java Nio使用:https://blog.csdn.net/forezp/article/details/88414741 +Java Nio原理:https://www.cnblogs.com/crazymakercircle/p/10225159.html +线程池:http://cmsblogs.com/?p=2448 +为什么nio快:https://blog.csdn.net/yaogao000/article/details/47972143 -https://zhuanlan.zhihu.com/p/59861022 +#### JDBC 连接的过程 ,手写 jdbc 连接过程 -#### 解决hash冲突的三种方法 +https://blog.csdn.net/qq_44971038/article/details/103204217 -https://blog.csdn.net/qq_32595453/article/details/80660676 +#### 说出三个遇到过的程序报异常的情况 -#### 为什么要重写hashCode()方法和equals()方法以及如何进行重写 +https://www.cnblogs.com/winnie-man/p/10471338.html -https://blog.csdn.net/xlgen157387/article/details/63683882 +#### socket 是靠什么协议支持的 -#### 动态代理 +TCP/IP,协议。socket用于 通信,在实际应用中有im等,因此需要可靠的网络协议,UDP则是不可靠的协议,且服务端与客户端不链接,UDP用于广播,视频流等 -- https://segmentfault.com/a/1190000011291179 +#### java io 用到什么设计模式 -#### 红黑树 +装饰模式和适配器模式 -- https://zhuanlan.zhihu.com/p/31805309 +#### serviable 的序列化,其中 uuid 的作用 -#### hashmap的jdk1.7和jdk1.8区别 +相当于快递的打包和拆包,里面的东西要保持一致,不能人为的去改变他,不然就交易不成功。序列化与反序列化也是一样,而版本号的存在就是要是里面内容要是不一致,不然就报错。像一个防伪码一样。 -- https://juejin.im/post/5aa5d8d26fb9a028d2079264 +#### 什么情景下会用到反射 -- https://blog.csdn.net/qq_36520235/article/details/82417949 +注解、Spring 配置文件、动态代理、jdbc -#### concurrenthashmap的jdk1.7和jdk1.8区别 +#### 浅克隆与深克隆有什么区别,如何实现深克隆 -- [面试题](https://juejin.im/post/5df8d7346fb9a015ff64eaf9) +浅拷贝:仅仅克隆基本类型变量,而不克隆引用类型的变量 +深克隆:既克隆基本类型变量,也克隆引用类型变量 +1.浅克隆:只复制基本类型的数据,引用类型的数据只复制了引用的地址,引用的对象并没有复制,在新的对象中修改引用类型的数据会影响原对象中的引用。直接使用clone方法,再嵌套的还是浅克隆,因为有些引用类型不能直接克隆。 +2.深克隆:是在引用类型的类中也实现了clone,是clone的嵌套,并且在clone方法中又对没有clone方法的引用类型又做差异化复制,克隆后的对象与原对象之间完全不会影响,但是内容完全相同。 -#### Java I/O 底层细节,注意是底层细节,而不是怎么用 +#### 反射能够使用私有的方法属性吗和底层原理? -可以从Java IO底层、JavaIO模型(阻塞、异步等) +https://blog.51cto.com/4247649/2109128 -https://www.cnblogs.com/crazymakercircle/p/10225159.html +#### 处理器指令优化有些什么考虑? -#### 如何实现分布式缓存 +禁止重排序 -redis如何实现分布式缓存 -https://stor.51cto.com/art/201912/607229.htm +#### object 对象的常用方法 -#### 浏览器的缓存机制 +#### Stack 和 ArrayList 的区别 -说明计算机网络的知识还没有记住 +#### statement 和 prestatement 的区别 -https://www.cnblogs.com/yangyangxxb/p/10218871.html +1、Statement用于执行静态SQL语句,在执行时,必须指定一个事先准备好的SQL语句。 +2、PrepareStatement是预编译的SQL语句对象,sql语句被预编译并保存在对象中。被封装的sql语句代表某一类操作,语句中可以包含动态参数“?”,在执行时可以为“?”动态设置参数值。 +3、使用PrepareStatement对象执行sql时,sql被数据库进行解析和编译,然后被放到命令缓冲区,每当执行同一个PrepareStatement对象时,它就会被解析一次,但不会被再次编译。在缓冲区可以发现预编译的命令,并且可以重用。 +4、PrepareStatement可以减少编译次数提高数据库性能。 -#### JVM tomcat 容器启动,jvm 加载情况描述 +#### 手写模拟实现一个阻塞队列 -- tomcat请求流程:http://objcoding.com/2017/06/12/Tomcat-structure-and-processing-request-process/ +https://www.cnblogs.com/keeya/p/9713686.html -其实就是jvm的类加载情况,非常相似 -- https://blog.csdn.net/lduzhenlin/article/details/83013143 -- https://blog.csdn.net/xlgen157387/article/details/53521928 +#### util 包下有哪几种接口 -#### 当获取第一个获取锁之后,条件不满足需要释放锁应当怎么做? +#### 很常见的 Nullpointerexception ,你是怎么排查的,怎么解决的; -https://www.jianshu.com/p/eb112b25b848 +#### 静态内部类和非静态内部类的区别是什么? + +#### 怎么创建静态内部类和非静态内部类? + +https://blog.csdn.net/qq_38366777/article/details/78088386 + +#### Xml 解析方式,原理优缺点 + +https://segmentfault.com/a/1190000013504078?utm_source=tag-newest + +#### 静态变量和全局变量的区别 + + +### 二 Java集合 + +#### hashmap的jdk1.7和jdk1.8区别 + +https://juejin.im/post/5aa5d8d26fb9a028d2079264 + +https://blog.csdn.net/qq_36520235/article/details/82417949 + +#### concurrenthashmap的jdk1.7和jdk1.8区别 + +https://juejin.im/post/5df8d7346fb9a015ff64eaf9 #### HashMap 实现原理,扩容因子过大过小的缺点,扩容过程 采用什么方法能保证每个 bucket 中的数据更均匀 解决冲突的方式,还有没有其他方式(全域哈希) @@ -109,49 +411,35 @@ https://www.cnblogs.com/peizhe123/p/5790252.html 更加详细的解释 https://blog.csdn.net/yanshuanche3765/article/details/78917507 -#### java 地址和值传递的例子 - -https://www.cnblogs.com/zhangyu317/p/11226105.html - -#### java NIO,java 多线程、线程池,java 网络编程解决并发量 +#### ArrayList、LinkedList、Vector -- Java Nio使用:https://blog.csdn.net/forezp/article/details/88414741 -- Java Nio原理:https://www.cnblogs.com/crazymakercircle/p/10225159.html -- 线程池:http://cmsblogs.com/?p=2448 -- 为什么nio快:https://blog.csdn.net/yaogao000/article/details/47972143 +https://blog.csdn.net/zhangqiluGrubby/article/details/72870493 -#### 手写一个线程安全的生产者与消费者。 +##### 还了解除 util 其他包下的 List 吗? -- https://www.cnblogs.com/jun-ma/p/11843394.html -- https://blog.csdn.net/Virgil_K2017/article/details/89283946 +##### CopyOnWriteArrayList +(1)CopyOnWriteArrayList使用ReentrantLock重入锁加锁,保证线程安全; +(2)CopyOnWriteArrayList的写操作都要先拷贝一份新数组,在新数组中做修改,修改完了再用新数组替换老数组,所以空间复杂度是O(n),性能比较低下; +(3)CopyOnWriteArrayList的读操作支持随机访问,时间复杂度为O(1); +(4)CopyOnWriteArrayList采用读写分离的思想,读操作不加锁,写操作加锁,且写操作占用较大内存空间,所以适用于读多写少的场合; +(5)CopyOnWriteArrayList只保证最终一致性,不保证实时一致性; #### ConcurrentHashMap 和 LinkedHashMap 差异和适用情形 哈希表的原理:https://blog.csdn.net/yyyljw/article/details/80903391 可以以下方面进行回答 - (1)使用的数据结构? - (2)添加元素、删除元素的基本逻辑? - (3)是否是fail-fast的? - (4)是否需要扩容?扩容规则? - (5)是否有序?是按插入顺序还是自然顺序还是访问顺序? - (6)是否线程安全? - (7)使用的锁? - (8)优点?缺点? - (9)适用的场景? - (10)时间复杂度? - (11)空间复杂度? #### ConcurrentHashMap分段锁是如何实现,ConcurrentHashmap jdk1.8 访问的时候是怎么加锁的,插入的时候是怎么加锁的 访问不加 锁插入的时候对头结点加锁 @@ -164,18 +452,6 @@ jdk1.8;https://blog.csdn.net/weixin_42130471/article/details/89813248 2、线程不是安全的 3、可以用来实现栈 -#### JDBC 连接的过程 ,手写 jdbc 连接过程 - -https://blog.csdn.net/qq_44971038/article/details/103204217 - -#### 可重入锁,实现原理 - -ReetrantLock:https://www.jianshu.com/p/f8f6ac49830e - -#### Java IO 模型(BIO,NIO 等) ,Tomcat 用的哪一种模型 - -tomcat支持:https://blog.csdn.net/fd2025/article/details/80007435 - #### ArrayBlockingQueue 源码 http://cmsblogs.com/?p=4755 @@ -185,16 +461,6 @@ http://cmsblogs.com/?p=4755 (3)入队和出队各定义了四组方法为满足不同的用途; (4)利用重入锁和两个条件保证并发安全:lock、notEmpty、notFull -#### 多进程和多线程的区别 - -#### 说出三个遇到过的程序报异常的情况 - -https://www.cnblogs.com/winnie-man/p/10471338.html - -#### Java 无锁原理 - -https://blog.csdn.net/qq_39291929/article/details/81501829 - #### hashmap 和 treemap 的区别 http://cmsblogs.com/?p=4743 @@ -223,151 +489,111 @@ http://cmsblogs.com/?p=4743 https://www.jianshu.com/p/dde9b12343c1 -#### 网络编程的 accept 和 connect - #### HashMap 的负载因子,为什么容量为2^n HashMap为了存取高效,要尽量较少碰撞,就是要尽量把数据分配均匀,每个链表长度大致相同,这个实现就在把数据存到哪个链表中的算法; 这个算法实际就是取模,hash%length,计算机中直接求余效率不如位移运算,源码中做了优化hash&(length-1), hash%length==hash&(length-1)的前提是length是2的n次方; 为什么这样能均匀分布减少碰撞呢?2的n次方实际就是1后面n个0,2的n次方-1 实际就是n个1; 例如长度为9时候,3&(9-1)=0 2&(9-1)=0 ,都在0上,碰撞了; 例如长度为8时候,3&(8-1)=3 2&(8-1)=2 ,不同位置上,不碰撞; 其实就是按位“与”的时候,每一位都能 &1 ,也就是和1111……1111111进行与运算 -#### try catch finally 可不可以没有 catch(try return,finally return) - -#### mapreduce 流程,如何保证 reduce 接受的数据没有丢失,数据如何去重,mapreduce 原理,partion 发生在什么阶段 - -#### 直接写一个 java 程序,统计 IP 地址的次数 - -#### 讲讲多线程,多线程的同步方法 - -1、synchronized -2、reetrantlock - #### list,map,set 之间的区别 https://blog.csdn.net/u012102104/article/details/79235938 -#### socket 是靠什么协议支持的 - -TCP/IP,协议。socket用于 通信,在实际应用中有im等,因此需要可靠的网络协议,UDP则是不可靠的协议,且服务端与客户端不链接,UDP用于广播,视频流等 - -#### java io 用到什么设计模式 - -装饰模式和适配器模式 - -#### serviable 的序列化,其中 uuid 的作用 - -相当于快递的打包和拆包,里面的东西要保持一致,不能人为的去改变他,不然就交易不成功。序列化与反序列化也是一样,而版本号的存在就是要是里面内容要是不一致,不然就报错。像一个防伪码一样。 - #### 什么时候会用到 HashMap -#### 什么情景下会用到反射 +#### 常见的线程安全的集合类 -注解、Spring 配置文件、动态代理、jdbc -#### 浅克隆与深克隆有什么区别,如何实现深克隆 +### 三 JVM -浅拷贝:仅仅克隆基本类型变量,而不克隆引用类型的变量 -深克隆:既克隆基本类型变量,也克隆引用类型变量 +#### 反射在jvm层面的实现 -1.浅克隆:只复制基本类型的数据,引用类型的数据只复制了引用的地址,引用的对象并没有复制,在新的对象中修改引用类型的数据会影响原对象中的引用。直接使用clone方法,再嵌套的还是浅克隆,因为有些引用类型不能直接克隆。 -2.深克隆:是在引用类型的类中也实现了clone,是clone的嵌套,并且在clone方法中又对没有clone方法的引用类型又做差异化复制,克隆后的对象与原对象之间完全不会影响,但是内容完全相同。 +https://www.jianshu.com/p/b6cb4c694951 +#### jvm的方法区存什么? -##### 常见的线程安全的集合类 +https://www.jianshu.com/p/10584345b10a -##### Java 8 函数式编程 回调函数 +#### JDK1.8 JVM方法区变成了什么,为什么这样做 -#### 函数式编程,面向对象之间区别 +https://blog.csdn.net/u011665991/article/details/107141348/ -#### Java 8 中 stream 迭代的优势和区别? +#### oom出现的原因 -#### 同步等于可见性吗? +https://blog.csdn.net/iteye_9584/article/details/82583093 -保证了可见性不等于正确同步,因为还有原子性没考虑。 +#### Class.forName和ClassLoader的区别 -#### 还了解除 util 其他包下的 List 吗? +https://blog.csdn.net/qq_27093465/article/details/52262340 -##### CopyOnWriteArrayList +#### java对象信息分配 -(1)CopyOnWriteArrayList使用ReentrantLock重入锁加锁,保证线程安全; -(2)CopyOnWriteArrayList的写操作都要先拷贝一份新数组,在新数组中做修改,修改完了再用新数组替换老数组,所以空间复杂度是O(n),性能比较低下; -(3)CopyOnWriteArrayList的读操作支持随机访问,时间复杂度为O(1); -(4)CopyOnWriteArrayList采用读写分离的思想,读操作不加锁,写操作加锁,且写操作占用较大内存空间,所以适用于读多写少的场合; -(5)CopyOnWriteArrayList只保证最终一致性,不保证实时一致性; +https://blog.csdn.net/u014520047/article/details/81940447 -#### 反射能够使用私有的方法属性吗和底层原理? +#### java虚拟机ZGC详解 -https://blog.51cto.com/4247649/2109128 +https://vimsky.com/article/4162.html -#### 处理器指令优化有些什么考虑? +#### java虚拟机CMS详解 -禁止重排序 +https://juejin.im/post/5c7262a15188252f30484351 -#### object 对象的常用方法 +#### java虚拟机G1详解 -#### Stack 和 ArrayList 的区别 +https://zhuanlan.zhihu.com/p/59861022 -#### statement 和 prestatement 的区别 +#### JVM tomcat 容器启动,jvm 加载情况描述 -1、Statement用于执行静态SQL语句,在执行时,必须指定一个事先准备好的SQL语句。 -2、PrepareStatement是预编译的SQL语句对象,sql语句被预编译并保存在对象中。被封装的sql语句代表某一类操作,语句中可以包含动态参数“?”,在执行时可以为“?”动态设置参数值。 -3、使用PrepareStatement对象执行sql时,sql被数据库进行解析和编译,然后被放到命令缓冲区,每当执行同一个PrepareStatement对象时,它就会被解析一次,但不会被再次编译。在缓冲区可以发现预编译的命令,并且可以重用。 -4、PrepareStatement可以减少编译次数提高数据库性能。 +tomcat请求流程:http://objcoding.com/2017/06/12/Tomcat-structure-and-processing-request-process/ -#### 手写模拟实现一个阻塞队列 +其实就是jvm的类加载情况,非常相似 +https://blog.csdn.net/lduzhenlin/article/details/83013143 +https://blog.csdn.net/xlgen157387/article/details/53521928 -https://www.cnblogs.com/keeya/p/9713686.html +### 四 多线程并发 -#### 怎么使用父类的方法 +#### volitale使用场景 -#### util 包下有哪几种接口 +https://blog.csdn.net/vking_wang/article/details/9982709 -#### cookie 禁用怎么办 +#### 可重入锁,实现原理 -https://segmentfault.com/q/1010000007715137 +ReetrantLock:https://www.jianshu.com/p/f8f6ac49830e -- Netty new 实例化过程 +#### Java 无锁原理 -#### socket 实现过程,具体用的方法;怎么实现异步 socket. +https://blog.csdn.net/qq_39291929/article/details/81501829 -https://blog.csdn.net/charjay_lin/article/details/81810922 +#### 讲讲多线程,多线程的同步方法 + +#### synchronized原理 -- 很常见的 Nullpointerexception ,你是怎么排查的,怎么解决的; +https://www.jianshu.com/p/d53bf830fa09 -#### Binder 的原理 +#### reetrantlock #### java 线程安全都体现在哪些方面 #### 如果维护线程安全 如果想实现一个线程安全的队列,可以怎么实现? -JUC 包里的 ArrayBlockingQueue 还有 LinkedBlockingQueue 啥的又结合源码说了一 通。 - -#### 静态内部类和非静态内部类的区别是什么? - -#### 怎么创建静态内部类和非静态内部类? +JUC 包里的 ArrayBlockingQueue 还有 LinkedBlockingQueue 啥的又结合源码说了一通。 -https://blog.csdn.net/qq_38366777/article/details/78088386 - -#### 断点续传的原理 - -#### Xml 解析方式,原理优缺点 +#### Java多线程通信方式 -#### - -https://segmentfault.com/a/1190000013504078?utm_source=tag-newest - -#### 静态变量和全局变量的区别 +https://blog.csdn.net/u011514810/article/details/77131296 +https://blog.csdn.net/xiaokang123456kao/article/details/77331878 +#### CountDownLatch、CyclicBarrier、Semaphore 用法总结 -### Java多线程 +https://segmentfault.com/a/1190000012234469 -#### CountDownLatch、CyclicBarrier、Semaphore 用法总结 +#### juc下的内容 -- https://segmentfault.com/a/1190000012234469 +https://blog.csdn.net/sixabs/article/details/98471709 #### AOS等并发相关面试题 -- https://cloud.tencent.com/developer/article/1471770 -- https://zhuanlan.zhihu.com/p/96544118 -- https://zhuanlan.zhihu.com/p/48295486 +https://cloud.tencent.com/developer/article/1471770 +https://zhuanlan.zhihu.com/p/96544118 +https://zhuanlan.zhihu.com/p/48295486 #### threadlocal https://juejin.im/post/5ac2eb52518825555e5e06ee @@ -399,263 +625,216 @@ https://juejin.im/post/5d1882b1f265da1ba84aa676#heading-14 https://www.cnblogs.com/kismetv/p/7806063.html -### spring&springmvc - -面试题: -https://mp.weixin.qq.com/s/2Y5X11TycreHgO0R3agK2A -https://mp.weixin.qq.com/s/IdjCxumDleLqdU8MgQnrLQ - -#### ioc aop总结(概述性) - -https://juejin.im/post/5b040cf66fb9a07ab7748c8b -https://juejin.im/post/5b06bf2df265da0de2574ee1 - -#### Spring 的加载流程,Spring 的源码中 Bean 的构造的流程 - -spring ioc系列文章:http://cmsblogs.com/?p=2806 -- 加载流程(概述):https://www.jianshu.com/p/5fd1922ccab1 -- 循环依赖问题:https://blog.csdn.net/u010853261/article/details/77940767 - - -#### Spring 事务源码,IOC 源码,AOP 源码 - -https://juejin.im/post/5c525968e51d453f5e6b744b - -ioc、aop系列源码: -https://segmentfault.com/a/1190000015319623 -http://www.tianxiaobo.com/2018/05/30/Spring-IOC-%E5%AE%B9%E5%99%A8%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E7%B3%BB%E5%88%97%E6%96%87%E7%AB%A0%E5%AF%BC%E8%AF%BB/ +### 五 Java框架(ssm) -#### spring 的作用及理解 事务怎么配置 - -https://www.jianshu.com/p/e7d59ebf41a3 - -#### Spring 的 annotation 如何实现 -https://segmentfault.com/a/1190000013258647 - -#### SpringMVC 工作原理 - -https://blog.csdn.net/cswhale/article/details/16941281 - -#### 了解 SpringMVC 与 Struct2 区别 - -https://blog.csdn.net/chenleixing/article/details/44570681 - -#### springMVC 和 spring 是什么关系 - - -#### 项目中 Spring 的 IOC 和 AOP 具体怎么使用的 - -- https://www.cnblogs.com/xdp-gacl/p/4249939.html -- https://juejin.im/post/5b06bf2df265da0de2574ee1 - -#### spring mvc 底层实现原理 - -https://blog.csdn.net/weixin_42323802/article/details/84038765 - -#### 动态代理的原理 - -https://juejin.im/post/5a3284a75188252970793195 - -#### 如果使用 spring mvc,那 post 请求跟 put 请求有什么区别啊; 然后开始问 springmvc:描述从 tomcat 开始到 springmvc 返回到前端显示的整个流程,接着问 springmvc 中的 handlerMapping 的内部实现,然后又问 spring 中从载入 xml 文件到 getbean 整个流程,描述一遍 - -### springboot & springcloud - -#### springboot - -- [springboot面试题](https://mp.weixin.qq.com/s/id0Ga1OC4D3Hu6lkzc9hRg) -- [springboot面试题2](https://mp.weixin.qq.com/s/XGIErbCx2i6Y8vBgw1gq_Q) - -#### springcloud +### hibernate -- [springcloudm面试题](https://mp.weixin.qq.com/s/CYfLA9s9zhwcIwJjMFXhQQ) +#### Hibernate 的生成策略 +主要说了 native 、uuid +https://blog.csdn.net/itmyhome1990/article/details/54863822 -### servlet +#### Hibernate 与 Mybatis 区别 -#### Servlet 知道是做什么的吗?和 JSP 有什么联系? +https://blog.csdn.net/wangpeng047/article/details/17038659 -jsp就是在html里面写java代码,servlet就是在java里面写html代码…其实jsp经过容器解释之后就是servlet.只是我们自己写代码的时候尽量能让它们各司其职,jsp更注重前端显示,servlet更注重模型和业务逻辑。不要写出万能的jsp或servlet来即可。 +#### Mybatis原理 -作者:知乎用户 -链接:https://www.zhihu.com/question/37962386/answer/74906895 +https://www.javazhiyin.com/34438.html +#### mybatis执行select的过程 -#### JSP 的运行原理? +https://www.jianshu.com/p/ae2bda8f9d84 +https://blog.csdn.net/qwesxd/article/details/90049863 -jsp/servlet原理:https://www.jianshu.com/p/93736c3b448b +#### mybatis有哪些executors -#### JSP 属于 Java 中 的吗? +https://blog.csdn.net/weixin_42495773/article/details/106799280 +https://blog.csdn.net/weixin_34025051/article/details/92405286 -#### Servlet 是线程安全 +#### mybatis插件原理 -https://blog.csdn.net/qq_24145735/article/details/52433096 -https://www.cnblogs.com/chanshuyi/p/5052426.html +https://www.cnblogs.com/qdhxhz/p/11390778.html -#### servlet 是单例 -#### servlet 和 filter 的区别。 +#### mybatis二级缓存 -https://blog.csdn.net/weixin_42669555/article/details/81049423 +https://blog.csdn.net/csdnliuxin123524/article/details/78874261 -#### servlet jsp tomcat常见面试题 -https://juejin.im/post/5a75ab4b6fb9a063592ba9db -https://blog.csdn.net/shxz130/article/details/39735373 +### spring&springmvc +面试题: +https://mp.weixin.qq.com/s/2Y5X11TycreHgO0R3agK2A +https://mp.weixin.qq.com/s/IdjCxumDleLqdU8MgQnrLQ +#### spring中的设计模式 -### hibernate +https://juejin.im/post/5ce69379e51d455d877e0ca0 -#### Hibernate 的生成策略 +#### spring中bean的作用域 -主要说了 native 、uuid +https://blog.csdn.net/weidaoyouwen/article/details/80503575 -https://blog.csdn.net/itmyhome1990/article/details/54863822 +#### BeanFactory和FactoryBean区别 -#### Hibernate 与 Mybatis 区别 +https://blog.csdn.net/weixin_38361347/article/details/92852611 -- https://blog.csdn.net/wangpeng047/article/details/17038659 +#### aspect的种类 -#### Mybatis原理 +https://blog.csdn.net/StubbornAccepted/article/details/70767014 -- https://www.javazhiyin.com/34438.html +#### spring aop的实际应用 -### Redis +https://blog.csdn.net/zzh_spring/article/details/107207025 -- Redis 数据结构 +#### spring实现多线程安全 -- Redis 持久化机制 +https://www.cnblogs.com/tiancai/p/9627109.html -- Redis 的一致性哈希算法 +#### spring的bean的高并发安全问题 -- redis了解多少 +https://blog.csdn.net/songzehao/article/details/103365494/ -- redis五种数据类型,当散列类型的 value 值非常大的时候怎么进行压缩 +#### ioc aop总结(概述性) -#### 用redis怎么实现摇一摇与附近的人功能 +https://juejin.im/post/5b040cf66fb9a07ab7748c8b +https://juejin.im/post/5b06bf2df265da0de2574ee1 -https://blog.csdn.net/smartwu_sir/article/details/80254733 +#### Spring 的加载流程,Spring 的源码中 Bean 的构造的流程 -- redis 主从复制过程 +spring ioc系列文章:http://cmsblogs.com/?p=2806 +加载流程(概述):https://www.jianshu.com/p/5fd1922ccab1 +循环依赖问题:https://blog.csdn.net/u010853261/article/details/77940767 +https://blog.csdn.net/a15119273009/article/details/108007864 -- Redis 如何解决 key 冲突 -- redis 是怎么存储数据的 +#### Spring 事务源码,IOC 源码,AOP 源码 -- redis 使用场景 +https://juejin.im/post/5c525968e51d453f5e6b744b -### 框架其他 +ioc、aop系列源码: +https://segmentfault.com/a/1190000015319623 +http://www.tianxiaobo.com/2018/05/30/Spring-IOC-%E5%AE%B9%E5%99%A8%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E7%B3%BB%E5%88%97%E6%96%87%E7%AB%A0%E5%AF%BC%E8%AF%BB/ -#### Servlet 的 Filter 用的什么设计模式 +#### spring 的作用及理解 事务怎么配置 -https://www.jianshu.com/p/e4197a54828d +https://www.jianshu.com/p/e7d59ebf41a3 -- zookeeper 的常用功能,自己用它来做什么 +#### spring事务失效情况 +https://blog.csdn.net/luo4105/article/details/79733338 -#### ibatis 是怎么实现映射的,它的映射原理是什么 +#### Spring 的 annotation 如何实现 +https://segmentfault.com/a/1190000013258647 -mybatis面试题:https://zhuanlan.zhihu.com/p/44464109 +#### SpringMVC 工作原理 -#### redis 的操作是不是原子操作 +https://blog.csdn.net/cswhale/article/details/16941281 -https://juejin.im/entry/58f9e22044d9040069d40dca +#### 了解 SpringMVC 与 Struct2 区别 -#### 秒杀业务场景设计 +https://blog.csdn.net/chenleixing/article/details/44570681 -- WebSocket 长连接问题 +#### springMVC 和 spring 是什么关系 -- 如何设计淘宝秒杀系统(重点关注架构,比如数据一致性,数据库集群一致性哈希,缓存, 分库分表等等) -- List 接口去实例化一个它的实现类(ArrayList)以及直接用 ArrayList 去 new 一个该类的对 象,这两种方式有什么区别 +#### 项目中 Spring 的 IOC 和 AOP 具体怎么使用的 -#### Tomcat 关注哪些参数 (tomcat调优) +https://www.cnblogs.com/xdp-gacl/p/4249939.html +https://juejin.im/post/5b06bf2df265da0de2574ee1 -https://juejin.im/post/5ac034f351882548fe4a4383 +#### spring mvc 底层实现原理 -https://testerhome.com/topics/16082 +https://blog.csdn.net/weixin_42323802/article/details/84038765 +#### 动态代理的原理 -- 对后台的优化有了解吗?比如负载均衡 +https://juejin.im/post/5a3284a75188252970793195 -我给面试官说了 Ngix+Tomcat 负载均 衡,异步处理(消息缓冲服务器),缓存(Redis, Memcache), NoSQL,数据库优化,存储索引优化 +#### 如果使用 spring mvc,那 post 请求跟 put 请求有什么区别啊; 然后开始问 springmvc:描述从 tomcat 开始到 springmvc 返回到前端显示的整个流程,接着问 springmvc 中的 handlerMapping 的内部实现,然后又问 spring 中从载入 xml 文件到 getbean 整个流程,描述一遍 -#### 对 Restful 了解 Restful 的认识,优点,以及和 soap 的区别 +### 六 微服务(springboot等) -https://www.ruanyifeng.com/blog/2011/09/restful.html +#### springboot -- lrucache 的基本原理 +[springboot面试题](https://mp.weixin.qq.com/s/id0Ga1OC4D3Hu6lkzc9hRg) +[springboot面试题2](https://mp.weixin.qq.com/s/XGIErbCx2i6Y8vBgw1gq_Q) +#### springcloud -### 设计模式 +[springcloud面试题](https://mp.weixin.qq.com/s/CYfLA9s9zhwcIwJjMFXhQQ) -#### Java常见设计模式 -- https://www.jianshu.com/p/61b67ca754a3 +### 七 数据结构 -- 单例模式(双检锁模式)、简单工厂、观察者模式、适配器模式、职责链模式等等 +#### 二叉树相关 -- 享元模式模式 选两个画下 UML 图 +https://www.jianshu.com/p/655d83f9ba7b +https://www.jianshu.com/p/ff4b93b088eb -- 手写单例 +#### 红黑树 -写的是静态内部类的单例,然后他问我这个地方为什么用 private,这儿为啥用 static, 这就考察你的基本功啦 +https://www.jianshu.com/p/e136ec79235c +https://zhuanlan.zhihu.com/p/31805309 -- 静态类与单例模式的区别 -- 单例模式 double check 单例模式都有什么,都是否线程安全,怎么改进(从 synchronized 到 双重检验锁 到 枚举 Enum) -- 基本的设计模式及其核心思想 +### 八 数据库 -- 来,我们写一个单例模式的实现 +### MySQL -这里有一个深坑,详情请见《 JVM 》 第 370 页 +#### 数据库死锁问题 +https://blog.csdn.net/cbjcry/article/details/84920174 -- 基本的设计原则 +#### hash索引和B+树索引的区别 -如果有人问你接口里的属性为什么都是 final static 的,记得和他聊一聊设计原则。 +https://www.cnblogs.com/heiming/p/5865101.html +#### 可重复的原理MVCC -### 数据库 +https://www.cnblogs.com/wade-luffy/p/8686883.html +https://blog.csdn.net/weixin_42041027/article/details/100587435 +https://www.jianshu.com/p/8845ddca3b23 #### count(1)、count(*)、count(列名) -- https://blog.csdn.net/iFuMI/article/details/77920767 +https://blog.csdn.net/iFuMI/article/details/77920767 #### mysql的undo、redo、binlog的区别 -- https://mp.weixin.qq.com/s/0z6GmUp0Lb1hDUo0EyYiUg +https://mp.weixin.qq.com/s/0z6GmUp0Lb1hDUo0EyYiUg #### explain解释 -- https://segmentfault.com/a/1190000010293791 +https://segmentfault.com/a/1190000010293791 #### mysql分页查询优化 -- https://blog.csdn.net/hanchao5272/article/details/102790490 +https://blog.csdn.net/hanchao5272/article/details/102790490 #### sql注入 -- https://blog.csdn.net/github_36032947/article/details/78442189 +https://blog.csdn.net/github_36032947/article/details/78442189 #### 为什么用B+树 -- https://blog.csdn.net/xlgen157387/article/details/79450295 +https://blog.csdn.net/xlgen157387/article/details/79450295 #### sql执行流程 -- https://juejin.im/post/5b7036de6fb9a009c40997eb +https://juejin.im/post/5b7036de6fb9a009c40997eb #### 聚集索引与非聚集索引 -- https://juejin.im/post/5cdd701ee51d453a36384939 +https://juejin.im/post/5cdd701ee51d453a36384939 #### 覆盖索引 -- https://www.jianshu.com/p/77eaad62f974 +https://www.jianshu.com/p/77eaad62f974 #### sql总结 @@ -669,62 +848,55 @@ https://blog.csdn.net/yixuandong9010/article/details/72286029 https://juejin.im/post/5cbdbb455188250ab224802d - #### 500万数字排序,内存只能容纳5万个,如何排序,如何优化? 参考文章:https://juejin.im/entry/5a27cb796fb9a045104a5e8c -- 平时怎么写数据库的模糊查询(由字典树扯到模糊查询,前缀查询,例如“abc%”,还是索引策略的问题) +#### 平时怎么写数据库的模糊查询(由字典树扯到模糊查询,前缀查询,例如“abc%”,还是索引策略的问题) -- 数据库里有 10000000 条用户信息,需要给每位用户发送信息(必须发送成功),要求节省内存 +#### 数据库里有 10000000 条用户信息,需要给每位用户发送信息(必须发送成功),要求节省内存 -- 项目中如何实现事务 +#### 项目中如何实现事务 #### 数据库设计一般设计成第几范式 https://blog.csdn.net/hsd2012/article/details/51018631 -- mysql 用的什么版本 5.7 跟 5.6 有啥区别 +#### mysql 用的什么版本 5.7 跟 5.6 有啥区别 #### 提升 MySQL 安全性 https://blog.csdn.net/listen_for/article/details/53907270 -- 问了一个这样的表(三个字段:姓名,id,分数)要求查出平均分大于 80 的 id 然后分数降序排序,然后经过提示用聚合函数 avg。 +#### 问了一个这样的表(三个字段:姓名,id,分数)要求查出平均分大于 80 的 id 然后分数降序排序,然后经过提示用聚合函数 avg。 select id from table group by id having avg(score) > 80 order by avg(score) desc。 -- 为什么 mysql 事务能保证失败回滚 - - -#### 一道算法题,在一个整形数组中,找出第三大的数,注意时间效率 (使用堆) - +#### 为什么 mysql 事务能保证失败回滚 - -- 主键索引底层的实现原理 +#### 主键索引底层的实现原理 B+树 -- 经典的01索引问题? +#### 经典的01索引问题? -- 如何在长文本中快捷的筛选出你的名字? +#### 如何在长文本中快捷的筛选出你的名字? 全文索引 -- 多列索引及最左前缀原则和其他使用场景 +#### 多列索引及最左前缀原则和其他使用场景 -- 事务隔离级别 +#### 事务隔离级别 -- 索引的最左前缀原则 +#### 索引的最左前缀原则 -- 数据库悲观锁怎么实现的 +#### 数据库悲观锁怎么实现的 https://www.jianshu.com/p/f5ff017db62a -- 建表的原则 - -- 索引的内涵和用法 +#### 建表的原则 +#### 索引的内涵和用法 -- 给了两条 SQL 语句,让根据这两条语句建索引(个人想法:主要考虑复合索引只能匹配前缀列的特点) +#### 给了两条 SQL 语句,让根据这两条语句建索引(个人想法:主要考虑复合索引只能匹配前缀列的特点) #### 那么我们来聊一下数据库。A 和 B 两个表做等值连接(Inner join) 怎么优化 @@ -735,29 +907,80 @@ https://blog.csdn.net/hguisu/article/details/5731880 #### 数据库连接池的理解和优化 -- Sql 语句 分组排序 +#### Sql语句分组排序 -- SQL 语句的 5 个连接概念 +#### SQL语句的5个连接概念 -- 数据库优化和架构(主要是主从分离和分库分表相关) +#### 数据库优化和架构(主要是主从分离和分库分表相关) -分库分表 +#### 分库分表 -- 跨库join实现 +#### 跨库join实现 -- 探讨主从分离和分库分表相关 +#### 探讨主从分离和分库分表相关 -- 数据库中间件 +#### 数据库中间件 -- 读写分离在中间件的实现 +#### 读写分离在中间件的实现 -- 限流 and 熔断 +#### 限流 and 熔断 #### 行锁适用场景 https://cloud.tencent.com/developer/article/1104098 -### 计算机网络 + +### Redis + +#### redis为什么快? + +https://zhuanlan.zhihu.com/p/57089960 + +#### Redis 数据结构原理 + +https://blog.csdn.net/jackFXX/article/details/82318080 + +#### Redis 持久化机制 + +#### Redis 的一致性哈希算法 + +#### redis了解多少 + +#### redis五种数据类型,当散列类型的 value 值非常大的时候怎么进行压缩 + +#### 用redis怎么实现摇一摇与附近的人功能 + +https://blog.csdn.net/smartwu_sir/article/details/80254733 + +#### redis 主从复制过程 + +#### Redis 如何解决 key 冲突 + +#### redis 是怎么存储数据的 + +#### redis 使用场景 + +### 九 计算机网络 + +#### cookie 禁用怎么办 + +https://segmentfault.com/q/1010000007715137 + +#### Netty new 实例化过程 + +#### socket 实现过程,具体用的方法;怎么实现异步 socket. + +https://blog.csdn.net/charjay_lin/article/details/81810922 + +#### 浏览器的缓存机制 + +说明计算机网络的知识还没有记住 + +https://www.cnblogs.com/yangyangxxb/p/10218871.html + +#### http相关问题 + +https://mp.weixin.qq.com/s/xSGD3rWdboeeQvaS8jEchw #### TCP三次握手第三次握手时ACK丢失怎么办 @@ -765,13 +988,13 @@ https://www.cnblogs.com/wuyepeng/p/9801470.html #### dns属于udp还是tcp,原因 -- https://www.zhihu.com/question/310145373 +https://www.zhihu.com/question/310145373 #### http的幂等性 -- https://www.cnblogs.com/weidagang2046/archive/2011/06/04/idempotence.html +https://www.cnblogs.com/weidagang2046/archive/2011/06/04/idempotence.html -- 建立连接的过程客户端跟服务端会交换什么信息(参考 TCP 报文结构) +#### 建立连接的过程客户端跟服务端会交换什么信息(参考 TCP 报文结构) #### 丢包如何解决重传的消耗 @@ -783,7 +1006,7 @@ https://zhuanlan.zhihu.com/p/36811672 #### IO多路复用 -- https://sanyuesha.com/python-server-tutorial/book/ch05.html +https://sanyuesha.com/python-server-tutorial/book/ch05.html #### select 和 poll 区别? @@ -793,7 +1016,7 @@ https://zhuanlan.zhihu.com/p/36811672 服务器推送:https://juejin.im/post/5c20e5766fb9a049b13e387b -可以使用客户端定时刷新请求或者和 TCP 保持心跳连接实现。 +#### 可以使用客户端定时刷新请求或者和 TCP 保持心跳连接实现。 #### 查看磁盘读写吞吐量? @@ -807,27 +1030,51 @@ https://www.cnblogs.com/ggjucheng/archive/2013/01/13/2858810.html https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Redirections -- controller 怎么处理的请求:路由 +#### controller 怎么处理的请求:路由 #### IP 地址分为几类,每类都代表什么,私网是哪些 https://zhuanlan.zhihu.com/p/54593244 + +### 十 操作系统 + +#### Java I/O 底层细节,注意是底层细节,而不是怎么用 + +可以从Java IO底层、JavaIO模型(阻塞、异步等) +https://www.cnblogs.com/crazymakercircle/p/10225159.html + +#### Java IO 模型(BIO,NIO 等) ,Tomcat 用的哪一种模型 + +tomcat支持:https://blog.csdn.net/fd2025/article/details/80007435 + +#### 当获取第一个获取锁之后,条件不满足需要释放锁应当怎么做? + +https://www.jianshu.com/p/eb112b25b848 + +#### 手写一个线程安全的生产者与消费者。 + +https://blog.csdn.net/u010983881/article/details/78554671 + +#### 进程和线程调度方法 + +https://www.jianshu.com/p/91c8600cb2ae + ### linux #### linux查找命令 -- https://blog.51cto.com/whylinux/2043871 +https://blog.51cto.com/whylinux/2043871 #### 项目部署常见linux命令 -- https://blog.csdn.net/u010938610/article/details/79625988 +https://blog.csdn.net/u010938610/article/details/79625988 -- 进程文件里有哪些信息 +#### 进程文件里有哪些信息 #### sed 和 awk 的区别 -- awk用法:https://www.cnblogs.com/isykw/p/6258781.html +awk用法:https://www.cnblogs.com/isykw/p/6258781.html 其实sed和awk都是每次读入一行来处理的,区别是:sed 适合简单的文本替换和搜索;而awk除了自动给你分列之外,里面丰富的函数大大增强了awk的功能。数据统计,正则表达式搜索,逻辑处理,前后置脚本等。因此基本上sed能做的,awk可以全部完成并且做的更好。 @@ -838,8 +1085,6 @@ https://zhuanlan.zhihu.com/p/54593244 https://blog.csdn.net/qingmu0803/article/details/38271077 - - #### 有一个文件被锁住,如何查看锁住它的线程? #### 如何查看一个文件第100行到150行的内容 @@ -852,7 +1097,7 @@ https://www.cnblogs.com/freeweb/p/5407105.html https://blog.csdn.net/inuyashaw/article/details/55095545 -- linux 如何查找文件 +#### linux 如何查找文件 linux命令:https://juejin.im/post/5d3857eaf265da1bd04f2437 @@ -860,91 +1105,93 @@ linux命令:https://juejin.im/post/5d3857eaf265da1bd04f2437 https://juejin.im/post/5b624f4d518825068302aee9#heading-13 -### 安全加密 - -- http://www.ruanyifeng.com/blog/2011/08/what_is_a_digital_signature.html -- https://yq.aliyun.com/articles/54155 +### 十一 框架其他 -#### web安全问题 +#### Servlet 的 Filter 用的什么设计模式 -https://juejin.im/post/5da44c5de51d45783a772a22 +https://www.jianshu.com/p/e4197a54828d -### 分布式 +#### zookeeper 的常用功能,自己用它来做什么 -#### dubbo中的dubbo协议和http协议有什么区别? +#### redis 的操作是不是原子操作 -- https://blog.csdn.net/wjw_77/article/details/99696757 +https://juejin.im/entry/58f9e22044d9040069d40dca -### 项目及规划 +#### 秒杀业务场景设计 -1. 对你来说影响最大的一个项目(该面试中有关项目问题都针对该项目展开)? +#### 如何设计淘宝秒杀系统(重点关注架构,比如数据一致性,数据库集群一致性哈希,缓存, 分库分表等等) -2. 项目哪一部分最难攻克?如何攻克? -个人建议:大家一定要选自己印象最深的项目回答,首先按模块,然后组成 人员,最后你在项目中的角色和发挥 的作用。全程组织好语言,最好不要有停顿,面试官可以 看出你对项目的熟悉程度 +#### 对后台的优化有了解吗?比如负载均衡 -3. 你觉得你在项目运行过程中作为组长是否最大限度发挥了组员的优势?具体事例? +我给面试官说了 Ngix+Tomcat 负载均 衡,异步处理(消息缓冲服务器),缓存(Redis, Memcache), NoSQL,数据库优化,存储索引优化 -4. 职业规划,今天想发展的工作方向 -5. 项目里我遇到过的最大的困难是什么 +#### 对 Restful 了解 Restful 的认识,优点,以及和 soap 的区别 -6. 实验室的新来的研一,你会给他们什么学习上的建议,例如对于内核源码的枯 燥如何克服 +https://www.ruanyifeng.com/blog/2011/09/restful.html -7. 如何协调团队中多人的工作 +#### lrucache 的基本原理 -8. 当团队中有某人的任务没有完成的很好,如何处理 -9. 平时看些什么书,技术 综合 +### 十二 设计模式 -10. 项目解决的什么问题 用到了哪些技术 +#### Java常见设计模式 -11. 怎么预防 bug 日志 jvm 异常信息 如何找问题的根源(统计表格) +- https://www.jianshu.com/p/61b67ca754a3 +- 单例模式(双检锁模式)、简单工厂、观察者模式、适配器模式、职责链模式等等 +- 享元模式模式 选两个画下 UML 图 +- 手写单例 +写的是静态内部类的单例,然后他问我这个地方为什么用 private,这儿为啥用 static, 这就考察你的基本功啦 +- 静态类与单例模式的区别 +- 单例模式 double check 单例模式都有什么,都是否线程安全,怎么改进(从 synchronized 到 双重检验锁 到 枚举 Enum) +- 基本的设计模式及其核心思想 +- 来,我们写一个单例模式的实现 -12. 你是怎么学习的,说完会让举个例子 +### 十三 分布式 -13. 实习投了哪几个公司?为什么,原因 +#### dubbo中的dubbo协议和http协议有什么区别? -14. 最得意的项目是什么?为什么?(回答因为项目对实际作用大,并得到认可) +https://blog.csdn.net/wjw_77/article/details/99696757 -15. 最得意的项目内容,讲了会 +#### 负载均衡 -16. 你简历上写的是最想去的部门不是我们部门,来我们部门的话对你有影响麽? +https://juejin.im/post/5b39eea0e51d4558c1010e36 -17. 你除了在学校还有哪些方式去获取知识和技术? +#### 分布式锁的实现方式及优缺点 -18. 你了解阿里文化和阿里开源吗? +https://zhuanlan.zhihu.com/p/62158041 -19. 遇到困难解决问题的思路? +#### CAP -20. 我觉得最成功的一件事了 +https://www.jianshu.com/p/8025e3346734 -我说能说几件吗,说了我大学明白明白了 自己想干什么,选择了自己喜欢的事,大学里学会了和自己相处,自己一个人的 时候也不会感觉无聊,精神世界比较丰富,坚持锻炼,健身,有个很不错的身体, 然后顿了顿笑着说,说,有一个对我很好的女朋友算吗? +#### 如何实现分布式缓存 -21. 压力大的时候怎么调整?多个任务冲突了你怎么协调的? +redis如何实现分布式缓存 +https://stor.51cto.com/art/201912/607229.htm -22. 家里有几个孩子,父母对你来北京有什么看法? +### 十四 其他 -23. 职业生涯规划 +##### Java 8 函数式编程 回调函数 -24. 你在什么情况下可能会离职 +#### 函数式编程,面向对象之间区别 -25. 对你影响最大的人 +#### Java 8 中 stream 迭代的优势和区别? -26. 1. 优点 3 个,以及缺点 2. 说说你应聘这个岗位的优势 3. 说说家庭 4. 为什么 想来网易,用过网易的哪些产品,对比下有什么好的地方 5. 投递了哪些公司,对第一份工 作怎么看待 +#### 同步等于可见性吗? -27. 为什么要选择互联网(楼主偏底层的) +保证了可见性不等于正确同步,因为还有原子性没考虑。 -28. 为什么来网易(看你如何夸) +#### git底层数据结构 -29. 在校期间怎样学习 +https://blog.csdn.net/leo187/article/details/106233706 -30. 经常逛的技术性网站有哪些? +#### 安全加密 -31. 举出你在开发过程中遇到的原先不知道的 bug, 通过各种方式定位 bug 并最终 成功解决的例子 +http://www.ruanyifeng.com/blog/2011/08/what_is_a_digital_signature.html +https://yq.aliyun.com/articles/54155 -32. 举出一个例子说明你的自学能力 7 次面试记录,除了京东基本上也都走到了很后面的阶段。硬要说经验可能有三点: +#### web安全问题 -- 不会就不会。我比较爽快,如果遇到的不会的甚至是不确定的,都直接说:“对不起, 我答不上来”之类的。 -- 一技之长。中间件和架构相关的实习经历,让我基本上和面试官都可以聊的很多, 也可以看到,我整个过程没有多少算法题。是因为面试官和你聊完项目就知道你能 做事了。其实,面试官很不愿意出算法题的(BAT 那个档次除外),你能和他扯技 术他当然高兴了。关键很多人只会算法(逃)。 -- 基础非常重要。面试官只要问 Java 相关的基础,我都有自信让一般的面试官感觉 惊讶,甚至学到新知识 \ No newline at end of file +https://juejin.im/post/5da44c5de51d45783a772a22 diff --git "a/docs/interview/\345\267\262\346\212\225\345\205\254\345\217\270\346\203\205\345\206\265.md" "b/docs/interview/\345\267\262\346\212\225\345\205\254\345\217\270\346\203\205\345\206\265.md" index c1bb8fe..3ca1b9c 100644 --- "a/docs/interview/\345\267\262\346\212\225\345\205\254\345\217\270\346\203\205\345\206\265.md" +++ "b/docs/interview/\345\267\262\346\212\225\345\205\254\345\217\270\346\203\205\345\206\265.md" @@ -5,7 +5,7 @@ |蘑菇街|http://job.mogujie.com/#/candidate/perfectInfo | | offer | |虎牙| | | 不匹配 | |远景|https://campus.envisioncn.com/ | | 笔试 | -|阿里钉钉| https://campus.alibaba.com/myJobApply.htm?saveResume=yes&t=1584782560963 |https://www.nowcoder.com/discuss/368915?type=0&order=0&pos=25&page=3| 一面 | +|阿里钉钉| https://campus.alibaba.com/myJobApply.htm?saveResume=yes&t=1584782560963 |https://www.nowcoder.com/discuss/368915?type=0&order=0&pos=25&page=3| 二面 | |阿里新零售| |https://www.nowcoder.com/discuss/374171?type=0&order=0&pos=35&page=1 https://www.nowcoder.com/discuss/372118?type=0&order=0&pos=80&page=2| | |深信服| |https://www.nowcoder.com/discuss/369399?type=0&order=0&pos=40&page=6| | |CVTE| |https://www.nowcoder.com/discuss/368463?type=0&order=0&pos=87&page=3| 已投 | @@ -15,13 +15,13 @@ |拼多多| https://pinduoduo.zhiye.com/Portal/Apply/Index | https://www.nowcoder.com/discuss/393350?type=post&order=time&pos=&page=8 | 已投 | |腾讯| https://join.qq.com/center.php |https://www.nowcoder.com/discuss/377813?type=post&order=time&pos=&page=1| offer | |猿辅导| https://app.mokahr.com/m/candidate/applications/deliver-query/fenbi |https://www.nowcoder.com/discuss/375610?type=0&order=0&pos=95&page=2| 已投 | -|斗鱼| https://app.mokahr.com/m/candidate/applications/deliver-query/douyu |https://www.nowcoder.com/discuss/375180?type=0&order=0&pos=158&page=1| 笔试 | +|斗鱼| https://app.mokahr.com/m/candidate/applications/deliver-query/douyu |https://www.nowcoder.com/discuss/375180?type=0&order=0&pos=158&page=1| 一面挂 | |淘宝技术部| |https://www.nowcoder.com/discuss/374655?type=0&order=0&pos=165&page=6| | |字节跳动| https://job.bytedance.com/user https://job.bytedance.com/referral/pc/position/application?lightning=1&token=MzsxNTg0MTU2NDIxMDIzOzY2ODgyMjg1NzI1Mjk3MjI4ODM7MA/profile/ |https://www.nowcoder.com/discuss/381888?type=post&order=time&pos=&page=2| 已投 | |陌陌| | 来自内推军 |已投| |网易| http://gzgame.campus.163.com/applyPosition.do?&lan=zh |https://www.nowcoder.com/discuss/373132?type=post&order=create&pos=&page=1 一姐| 已投| |百度| https://talent.baidu.com/external/baidu/index.html#/individualCenter |https://www.nowcoder.com/discuss/376515?type=post&order=time&pos=&page=1| 已投 | -|京东| http://campus.jd.com/web/resume/resume_index?fxType=0 |https://www.nowcoder.com/discuss/372978?type=post&order=time&pos=&page=4| 已投 | +|京东| http://campus.jd.com/web/resume/resume_index?fxType=0 |https://www.nowcoder.com/discuss/372978?type=post&order=time&pos=&page=4| 二面挂 | |爱奇艺| | | | |科大讯飞| | | | |度小满| | https://www.nowcoder.com/discuss/387950?type=post&order=time&pos=&page=13 | 已投 | \ No newline at end of file diff --git "a/docs/interview/\350\207\252\346\210\221\344\273\213\347\273\215\345\222\214\351\241\271\347\233\256\344\273\213\347\273\215.md" "b/docs/interview/\350\207\252\346\210\221\344\273\213\347\273\215\345\222\214\351\241\271\347\233\256\344\273\213\347\273\215.md" index 374650b..3825b52 100644 --- "a/docs/interview/\350\207\252\346\210\221\344\273\213\347\273\215\345\222\214\351\241\271\347\233\256\344\273\213\347\273\215.md" +++ "b/docs/interview/\350\207\252\346\210\221\344\273\213\347\273\215\345\222\214\351\241\271\347\233\256\344\273\213\347\273\215.md" @@ -35,20 +35,8 @@ 4、对数据库性能进行调优 5、开发抢购活动功能模块(业务需求) -### 北京项目介绍 -这个项目的背景是,现在深空探索的研究越来越热,这个项目就是研究及开发脉冲星导航的相关问题,而脉冲星导航的能够解决深空探索航天器的位置问题,这个就是北京项目的研究目标。 - -脉冲星是一颗稳定的中子星,能够发出稳定的脉冲波,通过接收脉冲星发出来的脉冲波,可以确定航天器在太空中的位置。 - -而我在北京的工作是: -1、开发一个软件 -2、实现相关算法对航天器上的部件精度进行控制 -3、航天器相关的数据进行数据显示 -4、近地卫星轨道5星编队仿真实现 - - -2019年5月–2019年9月,在####实习,参加了脉冲星导航的项目开发工作。项目的背景是,现在深空探索的研究越来越热,这个项目就是研究及开发脉冲星导航的相关问题,而脉冲星导航的能够解决深空探索航天器的位置问题,这个就是项目的研究目标。 +2019年5月–2019年9月,在中国空间技术研究院钱学森空间技术实验室实习,参加了脉冲星导航的项目开发工作。项目的背景是,现在深空探索的研究越来越热,这个项目就是研究及开发脉冲星导航的相关问题,而脉冲星导航的能够解决深空探索航天器的位置问题,这个就是项目的研究目标。 负责工作: 1、开发一个脉冲星导航仿真软件 2、实现相关算法对航天器上的部件精度进行控制 @@ -69,4 +57,6 @@ ![](http://image.ouyangsihai.cn/FgUUPlQOlQtjbbdOs1RZK9gWxitV) -2、给俺一个 **star** 呗,可以让更多的人看到这篇文章,顺便激励下我继续写作,嘻嘻。 \ No newline at end of file +2、给俺一个 **star** 呗,可以让更多的人看到这篇文章,顺便激励下我继续写作,嘻嘻。 + + diff --git "a/docs/java/Basis/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" "b/docs/java/Basis/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" new file mode 100644 index 0000000..5a97f4b --- /dev/null +++ "b/docs/java/Basis/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" @@ -0,0 +1 @@ +https://juejin.cn/post/6844904127059738631 \ No newline at end of file diff --git a/docs/java/IO/java IO.md b/docs/java/IO/java IO.md new file mode 100644 index 0000000..c197d76 --- /dev/null +++ b/docs/java/IO/java IO.md @@ -0,0 +1 @@ +https://juejin.cn/post/6844904125700784136 \ No newline at end of file diff --git "a/docs/java/JavaFamily/\351\235\242\350\257\225\347\237\245\350\257\206\347\202\271\346\200\273\347\273\223.md" "b/docs/java/JavaFamily/\351\235\242\350\257\225\347\237\245\350\257\206\347\202\271\346\200\273\347\273\223.md" new file mode 100644 index 0000000..459cd09 --- /dev/null +++ "b/docs/java/JavaFamily/\351\235\242\350\257\225\347\237\245\350\257\206\347\202\271\346\200\273\347\273\223.md" @@ -0,0 +1 @@ +https://github.com/AobingJava/JavaFamily \ No newline at end of file diff --git "a/docs/java/Java\345\267\245\347\250\213\345\270\210\351\235\242\350\257\225\347\252\201\345\207\273/\347\254\254\344\270\200\345\255\243\347\254\224\350\256\260.md" "b/docs/java/Java\345\267\245\347\250\213\345\270\210\351\235\242\350\257\225\347\252\201\345\207\273/\347\254\254\344\270\200\345\255\243\347\254\224\350\256\260.md" new file mode 100644 index 0000000..6052a3f --- /dev/null +++ "b/docs/java/Java\345\267\245\347\250\213\345\270\210\351\235\242\350\257\225\347\252\201\345\207\273/\347\254\254\344\270\200\345\255\243\347\254\224\350\256\260.md" @@ -0,0 +1,2 @@ +https://www.yuque.com/books/share/327d9543-85d2-418f-9315-41c3e19d2768/0dca325c876a0e85f0ba4ea48042e61d +https://github.com/shishan100/Java-Interview-Advanced#%E5%88%86%E5%B8%83%E5%BC%8F%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97 \ No newline at end of file diff --git "a/docs/java/Java\345\267\245\347\250\213\345\270\210\351\235\242\350\257\225\347\252\201\345\207\273/\347\254\254\344\270\211\345\255\243\347\254\224\350\256\260.md" "b/docs/java/Java\345\267\245\347\250\213\345\270\210\351\235\242\350\257\225\347\252\201\345\207\273/\347\254\254\344\270\211\345\255\243\347\254\224\350\256\260.md" new file mode 100644 index 0000000..42773c7 --- /dev/null +++ "b/docs/java/Java\345\267\245\347\250\213\345\270\210\351\235\242\350\257\225\347\252\201\345\207\273/\347\254\254\344\270\211\345\255\243\347\254\224\350\256\260.md" @@ -0,0 +1 @@ +https://blog.csdn.net/u013073869/article/details/105271345 \ No newline at end of file diff --git "a/docs/java/Java\345\267\245\347\250\213\345\270\210\351\235\242\350\257\225\347\252\201\345\207\273/\347\254\254\344\272\214\345\255\243\347\254\224\350\256\260.md" "b/docs/java/Java\345\267\245\347\250\213\345\270\210\351\235\242\350\257\225\347\252\201\345\207\273/\347\254\254\344\272\214\345\255\243\347\254\224\350\256\260.md" new file mode 100644 index 0000000..e69de29 diff --git "a/docs/java/Multithread/Java\345\271\266\345\217\221.md" "b/docs/java/Multithread/Java\345\271\266\345\217\221.md" new file mode 100644 index 0000000..4a068da --- /dev/null +++ "b/docs/java/Multithread/Java\345\271\266\345\217\221.md" @@ -0,0 +1,2 @@ +参考:https://juejin.cn/post/6844904063687983111 +参考:https://www.cmsblogs.com/category/1391296887813967872 \ No newline at end of file diff --git "a/docs/java/collection/Java\351\233\206\345\220\210\351\235\242\350\257\225\351\242\230.md" "b/docs/java/collection/Java\351\233\206\345\220\210\351\235\242\350\257\225\351\242\230.md" new file mode 100644 index 0000000..85a698b --- /dev/null +++ "b/docs/java/collection/Java\351\233\206\345\220\210\351\235\242\350\257\225\351\242\230.md" @@ -0,0 +1,114 @@ +### 集合面试题 + +> ArrayList、LinkedList和Vector的区别和实现原理 + +#### 数据结构实现 + +ArrayList和Vector都是基于可改变大小的数据实现的,而LinkedList是基于双链表实现的。 + +#### 增删改查效率对比 + +ArrayList和Vector都是基于可改变大小的数据实现的,因此,从指定的位置检索对象时,或在集合的末尾插入对象、删除一个对象的时间都是O(1),但是如果在其他位置增加或者删除对象,花费的时间是O(n); + +而LinkedList是基于双链表实现的,因此,在插入、删除集合中的任何位置上的对象,所花费的时间都是O(1),但基于链表的数据结构在查找元素时的效率是更低的,花费的时间为O(n)。 + +因此,从以上分析我们可以知道,查找特定的对象或者在集合末端增加或者删除对象,ArrayList和Vector的效率是ok的,如果在指定的位置删除或者插入,LinkedList的效率则更高。 + +#### 线程安全 + +ArrayList、LinkedList不具有线程安全性,在多线程的问题下是不能使用的,如果想要在多线程的环境下使用怎么办呢?我们可以采用Collections的静态方法synchronizedList包装一下,就可以保证线程安全了,但是在实际情况下,并不会使用这种方式,而是会采用更高级的集合进行线程安全的操作。 + +Vector是线程安全的,其保证线程安全的机制是采用synchronized关键字,我们都知道,这个关键字的效率是不高的,在后续的很多版本中,线程安全的机制都不会采用这种方式,因此,Vector的效率是比ArrayList、LinkedList更低效的。 + +#### 扩容机制 + +ArrayList和Vector都是基于数据这种数据结构实现的,因此,在集合的容量满了时,是需要进行扩容操作的。 + +在扩容时,ArrayList扩容后的容量是原先的1.5倍,扩容后,再将原先的数组中的数据拷贝到新建的数组中。 + +Vector默认情况下,扩容后的容量是原先的2倍,除此之外,Vector还有一种可以设置**容量增量**的机制,在Vector中有capacityIncrement变量用于控制扩容时的增量,具体的规则是:当capacityIncrement大于0时,扩容时增加的大小就是capacityIncrement的大小,如果capacityIncrement小于等于0时,则将容量增加为之前的2倍。 + +> HashMap原理分析 + +在分析HashMap的原理之前,先说明一下,大家应该都知道HashMap在JDK1.7和1.8的实现上是有较大的区别的,而面试官也是非常喜欢考察这一个点,因此,这里也是采用这两个JDK版本对比来进行分析,这样也可以印象更加深刻一些。 + +#### 数据结构 + +在数据结构的实现上,大家应该都知道,JDK1.7是数组+单链表的形式,而1.8采用的是数组+单链表+红黑树,具体的表现如下: + +|版本|数据结构|数组+链表的实现形式|红黑树实现形式| +|-|-|-|-| +|JDK1.8|数组+单链表+红黑树|Node|TreeNode| +|JDK1.7|数组+单链表|Entry|-| + +为了更好的让大家理解后续的讲解,这里先讲解一下HashMap中实现的一些重要参数。 + +- 容量(capacity): HashMap中数组的长度 + - 容量范围:必须是 2 的幂 + - 初始容量 = 哈希表创建时的容量 + - 默认容量 = 16 = 1<<4 = 00001中的1向左移4位 = 10000 = 十进制的 2^4 = 16 + `static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;` + - 最大容量 = 2的30次方 + `static final int MAXIMUM_CAPACITY = 1 << 30;` + +- 加载因子(Load factor):HashMap在其容量自动增加时,会设置加载因子,当达到设置的值时,就会触发自动扩容。 + - 加载因子越大、填满的元素越多,也就是说,空间利用率高、但冲突的机会加大、查找效率变低 + - 加载因子越小、填满的元素越少,也就是说,空间利用率小、冲突的机会减小、查找效率高 + // 实际加载因子 + `final float loadFactor;` + // 默认加载因子 = 0.75 + `static final float DEFAULT_LOAD_FACTOR = 0.75f;` + +- 扩容阈值(threshold):当哈希表的大小 ≥ 扩容阈值时,就会扩容哈希表(即扩充HashMap的容量)。 + - 扩容 = 对哈希表进行resize操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数 + - 扩容阈值 = 容量 x 加载因子 + +#### 获取数据(get) + +HashMap的获取数据的过程大致如下: + +- 首先,根据key判断是否为空值; +- 如果为空,则到hashmap数组的第1个位置,寻找对应key为null的键; +- 如果不为空,则根据key计算hash值; +- 根据得到的hash值采用`hash & (length - 1)`的计算方式得到key在数组中的位置; +- 结束。 + +以上就是大致的数据获取流程,接下来,我们再对JDK1.7和1.8获取数据的细节做一个对比。 + +|版本|hash值的计算方式| +|-|-| +|JDK1.8|1、hash = (key == null) ? 0 : hash(key);
2、扰动处理 = 2次扰动 = 1次位运算+1次异或运算| +|JDK1.7|1、hash = (key == null) ? 0 : hash(key);
2、扰动处理 = 9次扰动 = 4次位运算+5次异或运算| + +#### 保存数据(put) + +HashMap的保存数据的过程大致如下: + +- 判读HashMap是否初始化,如果没有则进行初始化; +- 判断key是否为null,如果为null,则将key-value的数据存储在数组的第1个位置,这里与获取数据时对应的;否则,进行后续操作; +- 根据key计算数据存放的位置; +- 根据位置判断key是否存在,如果存在,则用新值替换旧值;如果不存在,则直接设置; + +这里也对保存数据的过程进行一个更加细致的对比。 + +|版本|hash值的计算方式|存放数据方式|插入数据方式| +|-|-|-|-| +|JDK1.8|1. hash = (key == null) ? 0 : hash(key);
2. 扰动处理 = 2次扰动 = 1次位运算+1次异或运算|数组+单链表+红黑树
- 无冲突,直接保存数据
- 冲突时,当链表长度小于8时,存放到单链表,当长度大于8时,存到到红黑树|尾插法| +|JDK1.7|1、hash = (key == null) ? 0 : hash(key);
2、扰动处理 = 9次扰动 = 4次位运算+5次异或运算|数组+单链表
- 无冲突,直接保存数据
- 冲突时,存放到单链表|头插法| + +#### 扩容机制 + +HashMap的扩容的过程大致如下: + +- 当发现容量不足时,开始扩容机制; +- 首先,保存旧数组,再根据旧容量的2倍新建数组; +- 遍历旧数组的每个元素,采用头插法的方式,将每个元素保存到新数组; +- 将新数组引用到hashmap的table属性上; +- 重新设置扩容阀值,完成扩容操作。 + +最后,也对扩容的过程进行一个更加细致的对比。 + +|版本|扩容后的位置计算方式|数据转移方式| +|-|-|-| +|JDK1.8|扩容后的位置 = 原位置 or 原位置+旧容量|尾插法| +|JDK1.7|扩容后的位置 = hashCode() -> 扰动处理 -> h & (length - 1)|头插法| diff --git "a/docs/java/collection/\351\233\206\345\220\210\351\235\242\350\257\225\350\265\204\346\226\231\346\261\207\346\200\273.md" "b/docs/java/collection/\351\233\206\345\220\210\351\235\242\350\257\225\350\265\204\346\226\231\346\261\207\346\200\273.md" new file mode 100644 index 0000000..e69961f --- /dev/null +++ "b/docs/java/collection/\351\233\206\345\220\210\351\235\242\350\257\225\350\265\204\346\226\231\346\261\207\346\200\273.md" @@ -0,0 +1,25 @@ +## Java 集合 + +参考:https://www.cmsblogs.com/article/1391291996752187392 +参考:https://juejin.cn/post/6844904125939843079 + +- ArrayList + +- LinkedList + +- HashMap + +- TreeMap + +- TreeSet + +- LinkedHashMap + +- ConcurrentHashMap + +- ArrayBlockingQueue + +- LinkedBlockingQueue + +- PriorityBlockingQueue + diff --git "a/docs/java/jvm/Java\350\231\232\346\213\237\346\234\272\351\235\242\350\257\225.md" "b/docs/java/jvm/Java\350\231\232\346\213\237\346\234\272\351\235\242\350\257\225.md" new file mode 100644 index 0000000..9e46964 --- /dev/null +++ "b/docs/java/jvm/Java\350\231\232\346\213\237\346\234\272\351\235\242\350\257\225.md" @@ -0,0 +1,1513 @@ +## 简单的说一下Java的垃圾回收机制,解决了什么问题? + +这个题目其实我是不太想写的,原因在于太笼统了,容易让面试者陷入误区,难以正确的回答,但是,如何加上一个解决了什么问题,那么,这个面试题还是有意义的,可以看看面试者的理解。 + +在c语言和c++中,对于内存的管理是非常的让人头疼的,也是很多人放弃的原因,因此,在后面的一些高级语言中,设计者就想解决这一个问题,让开发者专注于自己的业务开发即可,因此,在Java中也引入了垃圾回收机制,它使得开发者在编写程序的时候不再需要考虑内存管理,由于有个垃圾回收机制,Java中的对象不再有“作用域”的概念,只有对象的引用才有“作用域”。垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存。 + +## 了解JVM的内存模型吗? + +JVM载执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。 + +Java 虚拟机所管理的内存一共分为Method Area(方法区)、VM Stack(虚拟机栈)、Native Method Stack(本地方法栈)、Heap(堆)、Program Counter Register(程序计数器)五个区域。 + +这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。具体如下图所示: + +![](http://image.ouyangsihai.cn/FhxTjHOieWt_ugomW5L33YNkJlJQ) + +上图介绍的是JDK1.8 JVM运行时内存数据区域划分。1.8同1.7比,最大的差别就是:**元数据区取代了永久代**。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:**元数据空间并不在虚拟机中,而是使用本地内存**。 + + +#### 1 程序计数器(Program Counter Register) + +**程序计数器(Program Counter Register)**是一块较小的内存空间,可以看作是当前线程所执行的字节码的**行号指示器**。在虚拟机概念模型中,**字节码解释器**工作时就是通过改变计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。 + +程序计数器是一块 **“线程私有”** 的内存,每条线程都有一个独立的程序计数器,能够将切换后的线程恢复到正确的执行位置。 + +- 执行的是一个**Java方法** + +计数器记录的是正在执行的**虚拟机字节码指令的地址**。 + +- 执行的是**Native方法** + +**计数器为空(Undefined)**,因为native方法是java通过JNI直接调用本地C/C++库,可以近似的认为native方法相当于C/C++暴露给java的一个接口,java通过调用这个接口从而调用到C/C++方法。由于该方法是通过C/C++而不是java进行实现。那么自然无法产生相应的字节码,并且C/C++执行时的内存分配是由自己语言决定的,而不是由JVM决定的。 + +- 程序计数器也是唯一一个在Java虚拟机规范中没有规定任何**OutOfMemoryError**情况的内存区域。 + +其实,我感觉这块区域,作为我们开发人员来说是不能过多的干预的,我们只需要了解有这个区域的存在就可以,并且也没有虚拟机相应的参数可以进行设置及控制。 + +#### 2 Java虚拟机栈(Java Virtual Machine Stacks) + +![](http://image.ouyangsihai.cn/FksaMoFlAkSPkTB84bV4cK7xa8L3) + + +**Java虚拟机栈(Java Virtual Machine Stacks)**描述的是**Java方法执行的内存模型**:每个方法在执行的同时都会创建一个**栈帧(Stack Frame)**,从上图中可以看出,栈帧中存储着**局部变量表**、**操作数栈**、**动态链接**、**方法出口**等信息。每一个方法从调用直至执行完成的过程,会对应一个栈帧在虚拟机栈中入栈到出栈的过程。 + +与程序计数器一样,Java虚拟机栈也是**线程私有**的。 + +而**局部变量表**中存放了编译期可知的各种: + +* **基本数据类型**(boolen、byte、char、short、int、 float、 long、double) +* **对象引用**(reference类型,它不等于对象本身,可能是一个指向对象起始地址的指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置) +* **returnAddress类型**(指向了一条字节码指令的地址) + +其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余数据类型只占用1个。**局部变量表所需的内存空间在编译期间完成分配**,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。 + +Java虚拟机规范中对这个区域规定了两种异常状况: + +* **StackOverflowError**:线程请求的栈深度大于虚拟机所允许的深度,将会抛出此异常。 +* **OutOfMemoryError**:当可动态扩展的虚拟机栈在扩展时无法申请到足够的内存,就会抛出该异常。 + +一直觉得上面的概念性的知识还是比较抽象的,下面我们通过JVM参数的方式来控制栈的内存容量,模拟StackOverflowError异常现象。 + + +#### 3 本地方法栈(Native Method Stack) + +**本地方法栈(Native Method Stack)** 与Java虚拟机栈作用很相似,它们的区别在于虚拟机栈为虚拟机执行Java方法(即字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。 + +在虚拟机规范中对本地方法栈中使用的语言、方式和数据结构并无强制规定,因此具体的虚拟机可实现它。甚至**有的虚拟机(Sun HotSpot虚拟机)直接把本地方法栈和虚拟机栈合二为一**。与虚拟机一样,本地方法栈会抛出**StackOverflowError**和**OutOfMemoryError**异常。 + +- 使用-Xss参数减少栈内存容量(更多的JVM参数可以参考这篇文章:[深入理解Java虚拟机-常用vm参数分析](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-chang-yong-vm-can-shu-fen-xi.html)) + +这个例子中,我们将栈内存的容量设置为`256K`(默认1M),并且再定义一个变量查看栈递归的深度。 + +```java +/** + * @ClassName Test_02 + * @Description 设置Jvm参数:-Xss256k + * @Author 欧阳思海 + * @Date 2019/9/30 11:05 + * @Version 1.0 + **/ +public class Test_02 { + + private int len = 1; + + public void stackTest() { + len++; + System.out.println("stack len:" + len); + stackTest(); + } + + public static void main(String[] args) { + Test_02 test = new Test_02(); + try { + test.stackTest(); + } catch (Throwable e) { + e.printStackTrace(); + } + } +} + +``` +运行时设置JVM参数 + +![](http://image.ouyangsihai.cn/Fp8Z9xGi-AN7k7laSOHupU7htMg9) + +输出结果: + +![](http://image.ouyangsihai.cn/FuHgFTcCaWlFjEtorqUbRF3RI_Cx) + + + +#### 4 Java堆(Heap) + + + +对于大多数应用而言,**Java堆(Heap)**是Java虚拟机所管理的内存中最大的一块,它**被所有线程共享的**,在虚拟机启动时创建。此内存区域**唯一的目的**是**存放对象实例**,几乎所有的对象实例都在这里分配内存,且每次分配的空间是**不定长**的。在Heap 中分配一定的内存来保存对象实例,实际上只是保存**对象实例的属性值**,**属性的类型**和**对象本身的类型标记**等,**并不保存对象的方法(方法是指令,保存在Stack中)**,在Heap 中分配一定的内存保存对象实例和对象的序列化比较类似。 + + +Java堆是垃圾收集器管理的主要区域,因此也被称为 **“GC堆(Garbage Collected Heap)”** 。从内存回收的角度看内存空间可如下划分: + +![图片摘自https://blog.csdn.net/bruce128/article/details/79357870](http://image.ouyangsihai.cn/FvwbMlmR_k5r4xwnpH5LXDN-4qok) + + +* **新生代(Young)**: 新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低。在新生代中,常规应用进行一次垃圾收集一般可以回收70% ~ 95% 的空间,回收效率很高。 + +如果把新生代再分的细致一点,新生代又可细分为**Eden空间**、**From Survivor空间**、**To Survivor空间**,默认比例为8:1:1。 + +* **老年代(Tenured/Old)**:在新生代中经历了多次(具体看虚拟机配置的阀值)GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。 +* **永久代(Perm)**:永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,Java虚拟机规范指出可以不进行垃圾收集,一般而言不会进行垃圾回收。 + + +其中**新生代和老年代组成了Java堆的全部内存区域**,而**永久代不属于堆空间,它在JDK 1.8以前被Sun HotSpot虚拟机用作方法区的实现** + +另外,再强调一下堆空间内存分配的大体情况,这对于后面一些Jvm优化的技巧还是有帮助的。 + +- 老年代 : 三分之二的堆空间 +- 年轻代 : 三分之一的堆空间 + eden区: 8/10 的年轻代空间 + survivor0 : 1/10 的年轻代空间 + survivor1 : 1/10 的年轻代空间 + +最后,我们再通过一个简单的例子更加形象化的展示一下**堆溢出**的情况。 + +- JVM参数设置:-Xms10m -Xmx10m + +这里将堆的最小值和最大值都设置为10m,如果不了解这些参数的含义,可以参考这篇文章:[深入理解Java虚拟机-常用vm参数分析](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-chang-yong-vm-can-shu-fen-xi.html) + +```java +/** + * VM Args:-Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError + * @author zzm + */ +public class HeapTest { + + static class HeapObject { + } + + public static void main(String[] args) { + List list = new ArrayList(); + + //不断的向堆中添加对象 + while (true) { + list.add(new HeapObject()); + } + } +} + +``` + +输出结果: +![](http://image.ouyangsihai.cn/Fhky14SMLxHjx9R9ZCcY0jAJ8ljg) + +图中出现了`java.lang.OutOfMemoryError`,并且提示了`Java heap space`,这就说明是Java堆内存溢出的情况。 + +**堆的Dump文件分析** + +我的使用的是VisualVM工具进行分析,关于如何使用这个工具查看这篇文章([深入理解Java虚拟机-如何利用VisualVM对高并发项目进行性能分析 ](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-ru-he-li-yong-visualvm-dui-gao-bing-fa-xiang-mu-jin-xing-xing-neng-fen-xi.html))。在运行程序之后,会同时打开VisualVM工具,查看堆内存的变化情况。 + +![](http://image.ouyangsihai.cn/Fhdj0VggJwgP-qAOdWBBrWmO5XrM) + +在上图中,可以看到,堆的最大值是30m,但是使用的堆的容量也快接近30m了,所以很容易发生堆内存溢出的情况。 + +接着查看dump文件。 + +![](http://image.ouyangsihai.cn/FpYV2YbCGR3ByPy3vFNJbVNpoLdW) + +如上图,堆中的大部分的对象都是HeapObject,所以,就是因为这个对象的一直产生,所以导致堆内存不够分配,所以出现内存溢出。 + +我们再看GC情况。 + +![](http://image.ouyangsihai.cn/FpB0KxmFAtZOx7g95dlfyAp8mLEV) + +如上图,Eden新生代总共48次minor gc,耗时1.168s,基本满足要求,但是survivor却没有,这不正常,同时Old Gen老年代总共27次full gc,耗时4.266s,耗时长,gc多,这正是因为大量的大对象进入到老年代导致的,所以,导致full gc频繁。 + +#### 5 方法区(Method Area) + +**方法区(Method Area)** 与Java堆一样,是各个线程共享的内存区域。它用于存储一杯`虚拟机加载`的**类信息、常量、静态变量、及时编译器编译后的代码**等数据。正因为方法区所存储的数据与堆有一种类比关系,所以它还被称为 **Non-Heap**。 + + + +**运行时常量池(Runtime Constant Pool)** + +**运行时常量池(Runtime Constant Pool)**是方法区的一部分。**Class文件**中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是**常量池(Constant Pool Table)**,用于存放编译期生成的各种字面量和符号引用,**这部分内容将在类加载后进入方法区的运行时常量池存放**。 + +Java虚拟机对Class文件每一部分(自然包括常量池)的格式有严格规定,每一个字节用于存储那种数据都必须符合规范上的要求才会被虚拟机认可、装载和执行。但**对于运行时常量池,Java虚拟机规范没有做任何有关细节的要求**,不同的提供商实现的虚拟机可以按照自己的需求来实现此内存区域。不过一般而言,除了保存**Class文件中的描述符号引用**外,还会把**翻译出的直接引用**也存储在运行时常量池中。 + +运行时常量池相对于Class文件常量池的另外一个重要特征是具备**动态性**,Java语言并不要求常量一定只有编译器才能产生,也就是**并非置入Class文件中的常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中**。 + +#### 运行时常量池举例 + +上面的**动态性**在开发中用的比较多的便是String类的`intern()` 方法。所以,我们以`intern()` 方法举例,讲解一下**运行时常量池**。 + +`String.intern()`是一个`native`方法,作用是:如果字符串常量池中已经包含有一个等于此String对象的字符串,则直接返回池中的字符串;否则,加入到池中,并返回。 + +```java +/** + * @ClassName MethodTest + * @Description vm参数设置:-Xms512m -Xmx512m -Xmn128m -XX:PermSize=10M -XX:MaxPermSize=10M -XX:NewRatio=4 -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -XX:-HeapDumpOnOutOfMemoryError -XX:+UseParNewGC -XX:+UseConcMarkSweepGC + * @Author 欧阳思海 + * @Date 2019/11/25 20:06 + * @Version 1.0 + **/ + +public class MethodTest { + + public static void main(String[] args) { + List list = new ArrayList(); + long i = 0; + while (i < 1000000000) { + System.out.println(i); + list.add(String.valueOf(i++).intern()); + } + } +} +``` + +vm参数介绍: +>-Xms512m -Xmx512m -Xmn128m -XX:PermSize=10M -XX:MaxPermSize=10M -XX:NewRatio=4 -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -XX:-HeapDumpOnOutOfMemoryError -XX:+UseParNewGC -XX:+UseConcMarkSweepGC +开始堆内存和最大堆内存都是512m,永久代大小10m,新生代和老年代1:4,E:S1:S2=8:1:1,最大经过15次survivor进入老年代,使用的,垃圾收集器是新生代ParNew,老年代CMS。 + +通过这样的设置之后,查看运行结果: +![](http://image.ouyangsihai.cn/FlpAIczI0f-h26J5QGYXdUkW2hbw) + +首先堆内存耗完,然后看看GC情况,设置这些参数之后,GC情况应该会不错,拭目以待。 + +![](http://image.ouyangsihai.cn/FvkHjwUTJcMK0j7KvrgLVWJOHODY) + +上图是GC情况,我们可以看到**新生代** 21 次minor gc,用了1.179秒,平均不到50ms一次,性能不错,**老年代** 117 次full gc,用了45.308s,平均一次不到1s,性能也不错,说明jvm运行是不错的。 + +>**注意:** 在JDK1.6及以前的版本中运行以上代码,因为我们通过`-XX:PermSize=10M -XX:MaxPermSize=10M`设置了方法区的大小,所以也就是设置了常量池的容量,所以运行之后,会报错:`java.lang.OutOfMemoryError:PermGen space`,这说明常量池溢出;在JDK1.7及以后的版本中,将会一直运行下去,不会报错,在前面也说到,JDK1.7及以后,去掉了永久代。 + +#### 6 直接内存 + +**直接内存(Direct Memory)**并不是虚拟机**运行时数据区**的一部分,也不是Java虚拟机规范中定义的内存区域。但这部分内存也被频繁运用,而却可能导致**OutOfMemoryError**异常出现。 + +这个我们实际中主要接触到的就是NIO,在NIO中,我们为了能够加快IO操作,采用了一种直接内存的方式,使得相比于传统的IO快了很多。 + +在NIO引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配**堆外内存**,然后通过一个存储在Java堆中的`DirectByteBuffer`对象作为这块内存的引用进行操作。这样能避免在Java堆和Native堆中来回复制数据,在一些场景里显著提高性能。 + +在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统的限制),从而导致动态扩展时出现**OutOfMemoryError**异常。 + +## JVM的四种引用? + +Java把对象的引用分为四种级别,从而使程序能更加灵活的控制对象的生命周期。这四种级别由高到低依次为:强引用、软引用、弱引用和虚引用。 + +- 强引用 + +以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。 + +```java + String str = "sihai"; + List list = new Arraylist(); + list.add(str); +``` +以上就是一个强引用的例子,及时内存不足,该对象也不会被回收。 + +- 软引用 + +如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。 + +软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。 + +当内存足够大时可以把数组存入软引用,取数据时就可从内存里取数据,提高运行效率。 + +- 弱引用 + +如果一个对象只具有弱引用,那就类似于可有可物的生活用品。弱引用与软引用的区别在于:**只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存**。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。 + +弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。 + +- 虚引用 + +如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:**虚引用必须和引用队列(ReferenceQueue)联合使用。** + +当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 + +**注意:** 在实际程序设计中一般很少使用弱引用与虚引用,使用软用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。 + +## GC用的可达性分析算法中,哪些对象可以作为GC Roots对象? + +- 虚拟机栈(栈帧中的局部变量表,Local Variable Table)中引用的对象。 +- 方法区中类静态属性引用的对象。 +- 方法区中常量引用的对象。 +- 本地方法栈中JNI(即一般说的Native方法)引用的对象。 + +## 如何判断对象是不是垃圾? + +#### 引用计数算法 + +先讲讲第一个算法:**引用计数算法**。 + +其实,这个算法的思想非常的简单,一句话就是:**给对象中添加一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器减1;任何时刻计数器为0的对象就是不可能再被使用的。** + +这些简单的算法现在是否还被大量的使用呢,其实,现在用的已经不多,没有被使用的最主要的原因是他有一个很大的**缺点**:**很难解决对象之间循环引用的问题**。 + +**循环引用**:当A有B的引用,B又有A的引用的时候,这个时候,即使A和B对象都为null,这个时候,引用计数算法也不会将他们进行垃圾回收。 + +```java +/** + * @ClassName Test_02 + * @Description + * @Author 欧阳思海 + * @Date 2019/12/5 16:59 + * @Version 1.0 + **/ +public class Test_02 { + + public static void main(String[] args) { + Instance instanceA = new Instance(); + Instance instanceB = new Instance(); + + instanceA.instance = instanceB; + instanceB.instance = instanceA; + + instanceA = null; + instanceB = null; + + System.gc(); + + Scanner scanner = new Scanner(System.in); + scanner.next(); + } +} + +class Instance{ + public Object instance = null; +} + +``` + +如果使用的是**引用计数算法**,这是不能被回收的,当然,现在的JVM是可以被回收的。 + +#### 可达性分析算法 + +这个算法的思想也是很简单的,这里有一个概念叫做**可达性分析**,如果知道图的数据结构,这里可以把每一个对象当做图中的一个节点,我们把一个节点叫做**GC Roots**,如果一个节点到**GC Roots**没有任何的相连的路径,那么就说明这个节点不可达,也就是这个节点可以被回收。 + +![](http://image.ouyangsihai.cn/FqyjRBThJ5HhXAIEH4UE7znJhOuk) + +上面图中,虽然obj7、8、9相互引用,但是到GC Roots不可达,所以,这种对象也是会被当做垃圾收集的。 + +在Java中,可以作为`GC Roots`的对象包括以下几种: + +- 虚拟机栈(栈帧中的局部变量表,Local Variable Table)中引用的对象。 +- 方法区中类静态属性引用的对象。 +- 方法区中常量引用的对象。 +- 本地方法栈中JNI(即一般说的Native方法)引用的对象。 + +## 介绍一下JVM的堆 + +这个面试题其实和上面的有点重合,但是,这里单独拿出来再介绍一下,因为这个确实是比较常见的,这样大家也都有印象。 + +对于大多数应用而言,**Java堆(Heap)**是Java虚拟机所管理的内存中最大的一块,它**被所有线程共享的**,在虚拟机启动时创建。此内存区域**唯一的目的**是**存放对象实例**,几乎所有的对象实例都在这里分配内存,且每次分配的空间是**不定长**的。在Heap 中分配一定的内存来保存对象实例,实际上只是保存**对象实例的属性值**,**属性的类型**和**对象本身的类型标记**等,**并不保存对象的方法(方法是指令,保存在Stack中)**,在Heap 中分配一定的内存保存对象实例和对象的序列化比较类似。 + +Java堆是垃圾收集器管理的主要区域,因此也被称为 **“GC堆(Garbage Collected Heap)”** 。从内存回收的角度看内存空间可如下划分: + +![图片摘自https://blog.csdn.net/bruce128/article/details/79357870](http://image.ouyangsihai.cn/FvwbMlmR_k5r4xwnpH5LXDN-4qok) + + +* **新生代(Young)**: 新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低。在新生代中,常规应用进行一次垃圾收集一般可以回收70% ~ 95% 的空间,回收效率很高。 + +如果把新生代再分的细致一点,新生代又可细分为**Eden空间**、**From Survivor空间**、**To Survivor空间**,默认比例为8:1:1。 + +* **老年代(Tenured/Old)**:在新生代中经历了多次(具体看虚拟机配置的阀值)GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。 +* **永久代(Perm)**:永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,Java虚拟机规范指出可以不进行垃圾收集,一般而言不会进行垃圾回收。 + + +其中**新生代和老年代组成了Java堆的全部内存区域**,而**永久代不属于堆空间,它在JDK 1.8以前被Sun HotSpot虚拟机用作方法区的实现** + +另外,再强调一下堆空间内存分配的大体情况,这对于后面一些Jvm优化的技巧还是有帮助的。 + +- 老年代 : 三分之二的堆空间 +- 年轻代 : 三分之一的堆空间 + eden区: 8/10 的年轻代空间 + survivor0 : 1/10 的年轻代空间 + survivor1 : 1/10 的年轻代空间 + +最后,我们再通过一个简单的例子更加形象化的展示一下**堆溢出**的情况。 + +- JVM参数设置:-Xms10m -Xmx10m + +这里将堆的最小值和最大值都设置为10m,如果不了解这些参数的含义,可以参考这篇文章:[深入理解Java虚拟机-常用vm参数分析](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-chang-yong-vm-can-shu-fen-xi.html) + +```java +/** + * VM Args:-Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError + * @author zzm + */ +public class HeapTest { + + static class HeapObject { + } + + public static void main(String[] args) { + List list = new ArrayList(); + + //不断的向堆中添加对象 + while (true) { + list.add(new HeapObject()); + } + } +} + +``` + +输出结果: +![](http://image.ouyangsihai.cn/Fhky14SMLxHjx9R9ZCcY0jAJ8ljg) + +图中出现了`java.lang.OutOfMemoryError`,并且提示了`Java heap space`,这就说明是Java堆内存溢出的情况。 + +## 介绍一下Minor GC和Full GC + +这个概念首先我们要了解JVM的内存分区,在上面的面试题中已经做了介绍,这里就不再介绍了。 + +新生代 GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快。 + +老年代 GC(Major GC/Full GC):指发生在老年代的 GC,出现了 Major GC 经常会伴随至少一次的 Minor GC(并非绝对),Major GC 的速度一般会比 Minor GC 的慢 10 倍以上。 + +**内存分配规则** + +- 对象优先分配到Eden区,如果Eden区空间不够,则执行一次minor GC; +- 大对象直接进入到老年代; +- 长期存活的对象可以进入到老年代; +- 动态判断对象年龄。如果在Survivor空间中相同年龄所有对象的大小的总和大于Survivor空间的一半,年龄大于等于该年龄的对象直接进入到老年代。 + +在这里更加详细的规则介绍可以参考这篇文章:[深入理解Java虚拟机-JVM内存分配与回收策略原理,从此告别JVM内存分配文盲](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-jvm-nei-cun-fen-pei-yu-hui-shou-ce-lue-yuan-li-cong-ci-gao-bie-jvm-nei-cun-fen-pei-wen-mang.html) + +## 说一下Java对象创建的方法 + +这个问题其实很简单,但是很多人却只知道new的方式。 + +- new的方法,最常见; +- 调用对象的clone方法; +- 使用反射,Class.forName(); +- 运用反序列化机制,java.io.ObjectInputStream对象的readObject()方法。 + +## 介绍一下几种垃圾收集算法? + +#### 标记-清除(Mark-Sweep)算法 + +**标记-清除(Mark-Sweep)** 算法是最基础的垃圾收集算法,后续的收集算法都是基于它的思路并对其不足进行改进而得到的。顾名思义,算法分成“标记”、“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,标记过程在前一节讲述对象标记判定时已经讲过了。 + +标记-清除算法的不足主要有以下两点: + +* **空间问题**,标记清除之后会产生大量不连续的**内存碎片**,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不触发另一次垃圾收集动作。 +* **效率问题**,因为内存碎片的存在,操作会变得更加费时,因为查找下一个可用空闲块已不再是一个简单操作。 + +标记-清除算法的执行过程如下图所示: + +![](http://image.ouyangsihai.cn/Fn_nJ2vQKuX47-8rBn7IZrV5LfoH) + + +#### 复制(Copying)算法 + +为了解决标记-清除算法的效率问题,一种称为**“复制”(Copying)**的收集算法出现了,思想为:它**将可用内存按容量分成大小相等的两块**,每次只使用其中的一块。**当这一块内存用完,就将还存活着的对象复制到另一块上面**,然后再把已使用过的内存空间一次清理掉。 + +这样做使得**每次都是对整个半区进行内存回收**,内存分配时也就**不用考虑内存碎片**等复杂情况,只要**移动堆顶指针,按顺序分配内存**即可,实现简单,运行高效。只是这种算法的代价是**将内存缩小为原来的一半**,代价可能过高了。复制算法的执行过程如下图所示: + +![](http://image.ouyangsihai.cn/FoyNQk9dft20afZSCIzC7oVoJIHQ) + + + +##### 标记-整理(Mark-Compact)算法 + +复制算法在对象存活率较高时要进行较多的复制操作,效率将会变低。更关键的是:如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在**老年代一般不能直接选用复制算法**。 + +根据老年代的特点,**标记-整理(Mark-Compact)**算法被提出来,主要思想为:此算法的标记过程与**标记-清除**算法一样,但后续步骤不是直接对可回收对象进行清理,而是**让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。** 具体示意图如下所示: + +![](http://image.ouyangsihai.cn/Fov_rN7qL6R_DbGUNChUUOC4lqHS) + + +##### 分代收集(Generational Collection)算法 + +当前商业虚拟机的垃圾收集都采用**分代收集(Generational Collection)算法**,此算法相较于前几种没有什么新的特征,主要思想为:根据对象存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适合的收集算法: + +* **新生代** 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用**复制算法**,只需要付出少量存活对象的复制成本就可以完成收集。 + +* **老年代** 在老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须使用**标记-清除**或**标记-整理**算法来进行回收。 + +## 如何减少gc出现的次数 + +上面的面试题已经讲解到了,从年轻代空间(包括Eden和 Survivor 区域)回收内存被称为Minor GC,对老年代GC称为Major GC,而Full GC是对整个堆来说的,在最近几个版本的JDK里默认包括了对永生带即方法区的回收(JDK8中无永生带了),出现Full GC的时候经常伴随至少一次的Minor GC,但非绝对的。Major GC的速度一般会比Minor GC慢10倍以上。 + +GC会stop the world。会暂停程序的执行,带来延迟的代价。所以在开发中,我们不希望GC的次数过多。 + +- 对象不用时最好显式置为 Null + +一般而言,为 Null 的对象都会被作为垃圾处理,所以将不用的对象显式地设为 Null,有利于 GC 收集器判定垃圾,从而提高了 GC 的效率。 +- 尽量少用 System.gc() + +此函数建议 JVM 进行主 GC,虽然只是建议而非一定,但很多情况下它会触发主 GC,从而增加主 GC 的频率,也即增加了间歇性停顿的次数。 +- 尽量少用静态变量 + +静态变量属于全局变量,不会被 GC 回收,它们会一直占用内存。 +- 尽量使用 StringBuffer,而不用 String 来累加字符串 + +由于 String 是固定长的字符串对象。累加 String 对象时,并非在一个 String对象中扩增,而是重新创建新的 String 对象,如 Str5=Str1+Str2+Str3+Str4,这条语句执行过程中会产生多个垃圾对象,因为对次作“+”操作时都必须创建新的 String 对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。 避免这种情况可以改用 StringBuffer 来累加字符串,因 StringBuffer是可变长的,它在原有基础上进行扩增,不会产生中间对象 +- 分散对象创建或删除的时间 + +集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM 在面临这种情况时,只能进行主 GC,以回收内存或整合内存碎片,从而增加主 GC 的频率。 + +集中删除对象,道理也是一样的。 它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主 GC 的机会。 +- 尽量少用 finalize 函数 + +因为它会加大 GC 的工作量,因此尽量少用finalize 方式回收资源。 +- 使用软引用类型 + +如果需要使用经常用到的图片,可以使用软引用类型,它可以尽可能将图片保存在内存中,供程序调用,而不引起 OutOfMemory。 + +- 尽量少用 finalize 函数。 + +因为它会加大 GC 的工作量,因此尽量少用finalize 方式回收资源。 + +如果需要使用经常用到的图片,可以使用软引用类型,它可以尽可能将图片保存在内存中,供程序调用,而不引起 OutOfMemory。 + +- 能用基本类型如 int,long,就不用 Integer,Long 对象 + +基本类型变量占用的内存资源比相应包装类对象占用的少得多,如果没有必要,最好使用基本变量。 + +- 增大-Xmx + +- 老年代代空间不足 + +老年代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误: +```java +java.lang.OutOfMemoryError: Java heap space +``` +为避免以上两种状况引起的Full GC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。 + +- 永生区空间不足 + +JVM规范中运行时数据区域中的方法区,在HotSpot虚拟机中又被习惯称为永生代或者永生区,`Permanet Generation`中存放的为一些class的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,`Permanet Generation`可能会被占满,在未配置为采用CMS GC的情况下也会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息: +```java +java.lang.OutOfMemoryError: PermGen space +``` +为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。 + +- CMS GC时出现`promotion failed`和`concurrent mode failure` + +对于采用CMS进行老年代GC的程序而言,尤其要注意GC日志中是否有`promotion failed`和`concurrent mode failure`两种状况,当这两种状况出现时可能会触发Full GC。 + +promotion failed是在进行Minor GC时,`survivor space`放不下、对象只能放入老年代,而此时老年代也放不下造成的;`concurrent mode failure`是在执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足造成的(有时候“空间不足”是CMS GC时当前的浮动垃圾过多导致暂时性的空间不足触发Full GC)。 + +对措施为:增大survivor space、老年代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会由于JDK的bug29导致CMS在remark完毕后很久才触发sweeping动作。对于这种状况,可通过设置`-XX: CMSMaxAbortablePrecleanTime=5`(单位为ms)来避免。 + +- 统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间 + +这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。 + +例如程序第一次触发Minor GC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。 + +当新生代采用PS GC时,方式稍有不同,PS GC是在Minor GC后也会检查,例如上面的例子中第一次Minor GC后,PS GC会检查此时旧生代的剩余空间是否大于6MB,如小于,则触发对旧生代的回收。 + +除了以上4种状况外,对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。可通过在启动时通过`java -Dsun.rmi.dgc.client.gcInterval=3600000`来设置Full GC执行的间隔时间或通过`-XX:+ DisableExplicitGC`来禁止RMI调用`System.gc`。 + +- 堆中分配很大的对象 + +所谓大对象,是指需要大量连续内存空间的java对象,例如很长的数组,此种对象会直接进入老年代,而老年代虽然有很大的剩余空间,但是无法找到足够大的连续空间来分配给当前对象,此种情况就会触发JVM进行Full GC。 + +为了解决这个问题,CMS垃圾收集器提供了一个可配置的参数,即`-XX:+UseCMSCompactAtFullCollection`开关参数,用于在“享受”完Full GC服务之后额外免费赠送一个碎片整理的过程,内存整理的过程无法并发的,空间碎片问题没有了,但提顿时间不得不变长了,JVM设计者们还提供了另外一个参数 `-XX:CMSFullGCsBeforeCompaction`,这个参数用于设置在执行多少次不压缩的Full GC后,跟着来一次带压缩的。 + +## 数组多大会放在JVM老年代,永久代对象如何GC?如果想不被GC怎么办?如果想在GC中生存一次怎么办? + +虚拟机提供了一个`-XX:PretenureSizeThreshold`参数(通常是3MB),令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制。(新生代采用复制算法收集内存) + +垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完成垃圾回收(Full GC)。如果仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。 + +让对象实现finalize()方法,一次对象的自我拯救。 + +## JVM 常见的参数有哪些,介绍几个常见的,并说说在你工作中实际用到的地方? + +首先,JVM 中的参数是非常多的,而且可以分为不同的类型,主要可以分为以下三种类型: +- `标准参数(-)`,所有的JVM实现都必须实现这些参数的功能,而且向后兼容。 +- `非标准参数(-X)`,默认JVM实现这些参数的功能,但是并不保证所有JVM实现都满足,且不保证向后兼容。 +- `非Stable参数(-XX)`,此类参数各个JVM实现会有所不同,将来可能会随时取消,需要慎重使用。 + +虽然是这么分类的,实际上呢,非标准参数和非稳定的参数实际的使用中还是用的非常的多的。 + +#### 标准参数 +这一类参数可以说是我们刚刚开始Java是就用的非常多的参数了,比如`java -version`、`java -jar`等等,我们在CMD中输入`java -help`就可以获得Java当前版本的所有标准参数了。 + +![](http://image.ouyangsihai.cn/FsGtetpK2vpkQRFke44AyrDqbsl2) + +如上图就是JDK1.8的所有标准参数了,下面我们将介绍一些我们会用的比较多的参数。 + +- -client + +以client模式启动JVM,这种方式启动速度快,但运行时性能和内存管理效率不高,适合客户端程序或者开发调试。 + +- -server + +以server模式启动JVM,与client情况恰好相反。适合生产环境,适用于服务器。64位的JVM自动以server模式启动。 + +- -classpath或者-cp + +通知JVM类搜索路径。如果指定了`-classpath`,则JVM就忽略`CLASSPATH`中指定的路径。各路径之间以分号隔开。如果`-classpath`和`CLASSPATH`都没有指定,则JVM从当前路径寻找class。 + +JVM搜索路径的顺序: + +**1.先搜索JVM自带的jar或zip包。** + +Bootstrap,搜索路径可以用`System.getProperty("sun.boot.class.path")`获得; + +**2.搜索`JRE_HOME/lib/ext`下的jar包。** + +Extension,搜索路径可以用`System.getProperty("java.ext.dirs")`获得; + +**3.搜索用户自定义目录,顺序为:当前目录(.),CLASSPATH,-cp。** + +搜索路径用`System.getProperty("java.class.path")`获得。 + +```java +System.out.println(System.getProperty("sun.boot.class.path")); +System.out.println(System.getProperty("java.ext.dirs")); +System.out.println(System.getProperty("java.class.path")); +``` +![](http://image.ouyangsihai.cn/FnHtFC8cd9UucBd39nAiLcCrT5mY) + +如上就是我电脑的JVM的路径。 + +- -DpropertyName=value + +定义系统的全局属性值,如配置文件地址等,如果value有空格,则需要使用双引号。 + +另外用`System.getProperty("hello")`可以获得这些定义的属性值,在代码中也可以用`System.setProperty("hello","world")`的形式来定义属性。 + +如键值对设置为hello=world。 +![](http://image.ouyangsihai.cn/Fp74h47lvnSt4LAq4S7bKH_Qcilr) + +```java +System.out.println(System.getProperty("hello")); +``` +运行结果就是: +![](http://image.ouyangsihai.cn/Fu6bGzdxxsaJ_bOzjDbd1RUxFKIM) + +- -verbose + +查询GC问题最常用的命令之一,参数如下: +**-verbose:class** +输出JVM载入类的相关信息,当JVM报告说找不到类或者类冲突时可此进行诊断。 +**-verbose:gc** +输出每次GC的相关情况。 +**-verbose:jni** +输出native方法调用的相关情况,一般用于诊断jni调用错误信息。 + +另外,控制台**输出GC信息**还可以使用如下命令: + +在JVM的启动参数中加入`-XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCApplicationStoppedTime`,按照参数的顺序分别输出GC的简要信息,GC的详细信息、GC的时间信息及GC造成的应用暂停的时间。 + +#### 非标准参数 + +非标准的参数主要是关于Java内存区域的设置参数,所以在看这些参数之前,应该先查看Java内存区域的基础知识,可以查看这篇文章:[深入理解Java虚拟机-Java内存区域透彻分析](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-java-nei-cun-qu-yu-tou-che-fen-xi.html)。 + +非标准参数实在标准参数的基础上的一些扩充参数,可以输入`java -X`,获得当前JVM支持的非标准参数。 + +![](http://image.ouyangsihai.cn/FkMW25ImmKNr7dZDD0pyysui2wYl) + +从图片中可以看出来,这些非标准的参数其实不多的,下面我们再 讲解一些比较常用的参数。 + +- -Xmn + +新生代内存大小的最大值,包括E区和两个S区的总和。设置方法:`-Xmn512m、-Xmn2g`。 + +- -Xms + +初始堆的大小,也是堆大小的最小值,默认值是总共的物理内存/64(且小于1G)。默认情况下,当堆中可用内存小于40%,堆内存会开始增加,一直增加到-Xmx的大小。 + +- -Xmx + +堆的最大值,默认值是总共的物理内存/64(且小于1G),默认情况下,当堆中可用内存大于70%,堆内存会开始减少,一直减小到-Xms的大小。 + +因此,为了避免这种浮动,所以在设置`-Xms`和`-Xmx`参数时,一般会设置成一样的,能够提高性能。 + +另外,官方默认的配置为**年老代大小:年轻代大小=2:1**左右,使用`-XX:NewRatio`可以设置年老代和年轻代之比,例如,`-XX:NewRatio=4`,表示**年老代:年轻代=4:1** + +- -Xss + +设置每个线程的栈内存,默认1M,一般来说是不需要改的。 + +- -Xprof + +跟踪正运行的程序,并将跟踪数据在标准输出输出;适合于开发环境调试。 + +- -Xnoclassgc + +禁用类垃圾收集,关闭针对class的gc功能;因为其阻止内存回收,所以可能会导致OutOfMemoryError错误,慎用。 + +- -Xincgc + +开启增量gc(默认为关闭);这有助于减少长时间GC时应用程序出现的停顿;但由于可能和应用程序并发执行,所以会降低CPU对应用的处理能力。 + +- -Xloggc:file + +与`-verbose:gc`功能类似,只是将每次GC事件的相关情况记录到一个文件中,文件的位置最好在本地,以避免网络的潜在问题。 + 若与verbose命令同时出现在命令行中,则以-Xloggc为准。 + +#### 4 非Stable参数 + +这类参数你一看官网以为不能使用呢,官网给你的建议就是这些参数不稳定,慎用,其实这主要的原因还是因为每个公司的实现都是不一样的,所以就是导致不稳定。但是呢,在实际的使用中却是非常的多的,而且这部分的参数很重要。 + +这些参数大致可以分为三类: + +- 性能参数(Performance Options):用于JVM的性能调优和内存分配控制,如初始化内存大小的设置; +- 行为参数(Behavioral Options):用于改变JVM的基础行为,如GC的方式和算法的选择; +- 调试参数(Debugging Options):用于监控、打印、输出等jvm参数,用于显示jvm更加详细的信息; + +**注意:以下参数都是JDK1.7及以下可以使用。** + +- 性能参数 + +|参数及其默认值| 描述| +|-----|-----| +|-XX:LargePageSizeInBytes=4m |设置用于Java堆的大页面尺寸| +|-XX:MaxHeapFreeRatio=70 |GC后java堆中空闲量占的最大比例| +|-XX:MinHeapFreeRatio=40 | GC后java堆中空闲量占的最小比例| +|**-XX:MaxNewSize=size** | 新生成对象能占用内存的最大值| +|**-XX:MaxPermSize=64m** |老生代对象能占用内存的最大值| +|**-XX:NewRatio=2** |新生代内存容量与老生代内存容量的比例| +|**-XX:NewSize=2.125m** | 新生代对象生成时占用内存的默认值| +|-XX:ReservedCodeCacheSize=32m |保留代码占用的内存容量| +|-XX:ThreadStackSize=512 |设置线程栈大小,若为0则使用系统默认值| +|-XX:+UseLargePages |使用大页面内存| + +- 行为参数 + + +|参数及其默认值 |描述| +|-----|-----| +|-XX:+ScavengeBeforeFullGC |新生代GC优先于Full GC执行| +|-XX:+UseGCOverheadLimit |在抛出OOM之前限制jvm耗费在GC上的时间比例| +|**-XX:-UseParNewGC**| 打开此开关,使用`ParNew+Serial Old`收集器| +|**-XX:-UseConcMarkSweepGC** |使用`ParNew+CMS+Serial Old`收集器对老生代采用并发标记交换算法进行GC| +|**-XX:-UseParallelGC** |启用并行GC,使用`ParallelScavenge+Serial Old`收集器| +|**-XX:-UseParallelOldGC** |对Full GC启用并行,当`-XX:-UseParallelGC`启用时该项自动启用,`ParallelScavenge+Parallel Old`收集器| +|**-XX:-UseSerialGC** |启用串行GC| +|**-XX:+UseG1GC**| 使用垃圾优先(G1)收集器| +|-XX:SurvivorRatio=n |Eden区域与Survivor区域大小之比。预设值为8| +|-XX:PretenureSizeThreshold=n|直接晋升到老年代的**对象大小**,设置这个参数之后,大于这个参数的对象直接进入到老年代分配| +|-XX:MaxTenuringThreshold=n|晋升到老年代的**对象年龄**,每个对象在坚持过一次Minor GC之后,年龄加1,当超过这个值之后就进入老年代。预设值为15| +|-XX:+UseAdaptiveSizePolicy|动态调整Java堆中各个区域的大小以及进入老年代的年龄| +| -XX:ParallelGCThreads=n|设置并行收集器收集时使用的CPU数。并行收集线程数| +|-XX:MaxGCPauseMillis=n|设置并行收集最大暂停时间| +|-XX:GCTimeRatio=n|设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+N)| +|-XX:+UseThreadPriorities |启用本地线程优先级| +|-XX:-DisableExplicitGC |禁止调用`System.gc()`;但jvm的gc仍然有效| +|-XX:+MaxFDLimit |最大化文件描述符的数量限制| + +前面6个参数都是关于**垃圾收集器**的行为参数,也是经常会用到的参数。 + +- 调试参数 + +|参数及其默认值| 描述| +|-----|-----| +|-XX:-CITime | 打印消耗在JIT编译的时间| +|-XX:ErrorFile=./hs_err_pid\.log |保存错误日志或者数据到文件中| +|-XX:HeapDumpPath=./java_pid\.hprof |指定导出堆信息时的路径或文件名| +|**-XX:-HeapDumpOnOutOfMemoryError** |当首次遭遇OOM时导出此时堆中相关信息| +|-XX:OnError="\;\" |出现致命ERROR之后运行自定义命令| +|-XX:OnOutOfMemoryError="\;\" |当首次遭遇OOM时执行自定义命令| +|-XX:-PrintClassHistogram |遇到Ctrl-Break后打印类实例的柱状信息,与`jmap -histo`功能相同| +|-XX:-PrintConcurrentLocks |遇到Ctrl-Break后打印并发锁的相关信息,与`jstack -l`功能相同| +|-XX:-PrintCommandLineFlags |打印在命令行中出现过的标记| +|-XX:-PrintCompilation |当一个方法被编译时打印相关信息| +|**-XX:-PrintGC** |每次GC时打印相关信息| +|**-XX:-PrintGCDetails** |每次GC时打印详细信息| +|-XX:-PrintGCTimeStamps |打印每次GC的时间戳| +|-XX:-TraceClassLoading |跟踪类的加载信息| +|-XX:-TraceClassLoadingPreorder |跟踪被引用到的所有类的加载信息| +|-XX:-TraceClassResolution | 跟踪常量池| +|-XX:-TraceClassUnloading | 跟踪类的卸载信息| +|-XX:-TraceLoaderConstraints | 跟踪类加载器约束的相关信息| + +以上标黑的就是常用的一些参数。 + +最后,给大家一个实例,看看在工作中是怎么去运用这些参数的,怎么在工作中去解决这些问题的。 + +#### **参数实例** + +设置`-Xms`、`-Xmn`和`-Xmx`参数分别为`-Xms512m -Xmx512m -Xmn128m`。同时设置新生代和老生代之比为1:4,E:S0:S1=8:1:1。除此之外,当然,你还可以设置一些其他的参数,比如,前面说到的,性能参数 `-XX:MaxNewSize`、 `-XX:NewRatio=`、,行为参数 `-XX:-UseParNewGC`,调试参数 `-XX:-PrintGCDetails`。 + +这些参数都是可以在 IDEA 中启动时直接设置的。 + +``` +** + * @ClassName MethodTest + * @Description vm参数设置:-Xms512m -Xmx512m -Xmn128m -XX:NewRatio=4 -XX:SurvivorRatio=8 + * @Author 欧阳思海 + * @Date 2019/11/25 20:06 + * @Version 1.0 + **/ + +public class MethodTest { + + public static void main(String[] args) { + List list = new ArrayList(); + long i = 0; + while (i < 1000000000) { + System.out.println(i); + list.add(String.valueOf(i++).intern()); + } + } +} +``` + +运行之后,用VisualVM查看相关信息是否正确。 + +当我们**没有设置**`-XX:NewRatio=4 -XX:SurvivorRatio=8`时,使用官方默认的情况如下: + +![](http://image.ouyangsihai.cn/FgIr8Niw2F9Cs1IK6ABn74oEi66T) + +上图可以看出,**新生代(Eden Space + Survivor 0 + Survivor 1):老年代(Old Gen)≈ 1:2**。 + +当我们**设置了**`-XX:NewRatio=4 -XX:SurvivorRatio=8`时,情况如下: + +![](http://image.ouyangsihai.cn/Fkc4DTkISF1LA9fpO54VEinaYvlc) + +变成了**新生代(Eden Space + Survivor 0 + Survivor 1):老年代(Old Gen)≈ 1:4**,Eden Space:Survivor 0: Survivor 1 = 8:1:1。 + +![](http://image.ouyangsihai.cn/FphOpfRSOQYu-pcR6xf6rDPcQAF9) + +从上图可知,堆的信息是正确的。 + +更多的使用方法可以参考这篇文章 [如何利用VisualVM对高并发项目进行性能分析](http://www.java1000.com/shen-ru-li-jie-java-xu-ni-ji-ru-he-li-yong-visualvm-dui-gao-bing-fa-xiang-mu-jin-xing-xing-neng-fen-xi.html),有更加细致的介绍。 + +## 说说你了解的常见的内存调试工具:jps、jmap、jhat等。 + +|名称|解释| +|-----|-----| +|jps|显示指定系统内所有的HotSpot虚拟机的进程| +|jstat|用于收集HotSpot虚拟机各方面的运行数据| +|jinfo|显示虚拟机配置信息| +|jmap|生成虚拟机的内存转存储快照(heapdump文件),利用这个文件就可以分析内存等情况| +|jhat|用于分析上面jmap生成的heapdump文件,它会建立一个HTTP/HTML服务器,让用户可以在浏览器上查看分析结果| +|jstack|显示虚拟机的线程快照| + +#### 1 jps:虚拟机进程状况工具 + +这个工具使用的频率还是非常高的,因为你需要查看你的程序的运行情况的时候,首先需要知道的就是程序所运行的**进程本地虚拟机唯一ID(LVMID)**,知道这个ID之后,才可以进行其他监控的操作。 + +- 命令格式: + +``` +jps [选项] [主机id] +``` + +- jps主要选项: + +|选项|解释| +|-----|-----| +|-q|只输出LVMID,省略主类名称| +|-m|输出虚拟机进程启动时传递给主类main()函数的参数| +|-l|输出主类的全名| +|-v|输出虚拟机进程启动时的 JVM 参数| + + + +- 实例 + +``` +jps -l +jps -v +``` +![](http://image.ouyangsihai.cn/Fq2nwxo46q4EpQ08Ba_qKIU2JN-J) + +![](http://image.ouyangsihai.cn/FmEvrlfb6kTEk8u_Hc-NcGHg8wW9) + +#### 2 jinfo:Java配置信息工具 + +jinfo的功能很简单,主要就是显示和查看调整虚拟机的各种参数。 + +- jinfo命令格式: + +``` +jinfo [选项] pid +``` + +- 相关选项 + +![](http://image.ouyangsihai.cn/FpSz04ZVzS6K6FJ3zVtnoYqMdLt7) + + +- 实例 + +我们在启动程序的时候设置一些JVM参数:`-Xms512m -Xmx512m -Xmn128m -XX:NewRatio=4 -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15`。 + +我们先使用`jps -l`查看虚拟机进程ID; +![](http://image.ouyangsihai.cn/FtsaKW9WE0lT-Vn_OH4CjpqWXqwz) + +再使用pid为1584进行查询参数; + +![](http://image.ouyangsihai.cn/FgOjSz1U0kS2yt2V-k_huM-f0zJ_) + +#### 3 jmap:Java内存映射工具 + +jmap的主要功能就是生成**堆转存储快照**,之后,我们再利用这个快照文件进行分析。 + +- jmap命令格式: + +``` +jmap [选项] vmid +``` + +- 相关选项 + +|选项|解释| +|-----|-----| +|-dump|生成Java堆转存储快照,格式:`-dump:[live,]format=b,file=`,其中`live`子参数说明是否只dump出存活的对象| +|-finalizerinfo|显示在F-Queue中等待Finalizer线程执行finalize方法的对象,**只在linux/Solaris有效**| +|-heap|显示Java堆详细信息,如使用哪种回收期、参数配置、分代状况等等。**只在Linux/Solaris有效**| +|-histo|显示堆中对象统计信息,包括类、实例数量、合计容量| +|-permstat|以ClassLoader为统计口径显示永久代内存状态,**只在Linux/Solaris有效**| +|-F|当虚拟机进程对-dump选项没有响应时,可使用这个选项强制生成dump快照,**只在Linux/Solaris有效**| + +- 实例 + +首先还是查看虚拟机ID; + +![](http://image.ouyangsihai.cn/FtuNhdGkitnyOGWFegoT2ETIOidf) + +然后再运行下面命令行; +![](http://image.ouyangsihai.cn/Fvi9c3BD9bSt4Qi1ptDkHaVH6jA1) + +打开这个dump文件如下; +![](http://image.ouyangsihai.cn/Fvd2a3KTLCykCyBX7fAosH65tvCw) + +ok,现在已经有了生成的dump文件,所以,我们就需要对这个文件进行解析,看看**jhat命令**。 + +#### 4 jhat:虚拟机堆转存储快照分析工具 + +虽然好像这个命令挺牛逼,但是,其实,由于这个工具的功能不太够,分析也得不到太多的结果,所以我们一般可以会将得到的dump文件用其他的工具进行分析,比如,可视化工具VisualVM等等。 + +所以,这里简单的做一个例子。 +- 实例 + +``` +jhat D:\dump.bin +``` + +![](http://image.ouyangsihai.cn/FsLOf6D2M9Ta8YiA9DCJ-zj6I-_X) + +如果有兴趣查看如何利用VisualVM进行查看,可以查看这篇文章:[深入理解Java虚拟机-如何利用VisualVM对高并发项目进行性能分析](https://blog.ouyangsihai.cn/shen-ru-li-jie-java-xu-ni-ji-ru-he-li-yong-visualvm-dui-gao-bing-fa-xiang-mu-jin-xing-xing-neng-fen-xi.html)。 + +#### 5 jstack:Java堆栈跟踪工具 + +`jstack` 命令主要用于生成虚拟机当前时刻的**线程快照**。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的**主要目的**:定位线程出现长时间停顿的原因、请求外部资源导致的长时间等待等这些原因。 + +- jstack命令格式: + +``` +jstack [选项] vmid +``` + +- 相关选项 + +|选项|解释| +|-----|-----| +|-F|当正常输出的请求不被响应时,强制输出线程堆栈| +|-l|除堆栈外,显示关于锁的附加信息| +|-m|如果调用到本地方法的话,可以显示C/C++的堆栈| + +- 实例 + +``` +jstack -l 6708 +``` +![](http://image.ouyangsihai.cn/FrcFS7iWpybtIg48OoDjrdmsSDJa) + + +#### 6 jstat:虚拟机统计信息监视工具 + +jstat这个工具还是很有作用的,他可以显示本地或者远程**虚拟机进程中的类装载、内存、垃圾收集、JIT编译**等运行数据,在服务器上,他是运行期定位虚拟机性能问题的首选工具。 + +- jstat命令格式: + +``` +jstat [选项 vmid [interval[s|ms] [count]]] +``` + +- 相关选项 + +|选项|解释| +|-----|-----| +|**-class**|监视类装载、卸载数量、总空间以及类装载所耗费的时间| +|**-gc**|监视Java堆状况,包括Eden区、两个Survivor区、老年代、永久代等容量、已用空间、GC时间合计等信息| +|-gccapacity|监视内容与-gc基本相同,但输出主要关注Java堆各个区域使用到的最大、最小空间| +|**-gcutil**|监视内容与-gc基本相同,但输出主要关注已使用空间占总空间的百分比| +|-gccause|与-gcutil功能一样,但是会输出额外导致上一次GC产生的原因| +|-gcnew|监视新生代GC状况| +|-gcnewcapacity|监控内容与-gcnew一样,输出主要关注使用到的最大最小空间| +|-gcold|监控老年代GC状况| +|-gcoldcapacity|监控内容与-gcold一样,输出主要关注使用到的最大最小空间| +|-gcpermcapacity|输出永久代使用到的最大最小空间| +|-compiler|输出JIT编译器编译过的方法、耗时等信息| +|-printcompilation|输出已经被JIT编译过的方法| + +- 实例 + + +我们这里还关注一下虚拟机GC的状况。 + +``` +jstat -gcutil 9676 +``` +![](http://image.ouyangsihai.cn/FkelCR8JIup_jpEFY9UQvqD2l0EC) + +上面参数的含义: +>S0:第一个Survivor区的空间使用(%)大小 +S1:第二个Survivor区的空间使用(%)大小 +E:Eden区的空间使用(%)大小 +O:老年代空间使用(%)大小 +M:方法区空间使用(%)大小 +CCS:压缩类空间使用(%)大小 +YGC:年轻代垃圾回收次数 +YGCT:年轻代垃圾回收消耗时间 +FGC:老年代垃圾回收次数 +FGCT:老年代垃圾回收消耗时间 +GCT:垃圾回收消耗总时间 + +了解了这些参数的意义之后,我们就可以对虚拟机GC状况进行分析。我们发现年轻代回收次数`12`次,使用时间`1.672`s,老年代回收`0`次,使用时间`0`s,所有GC总耗时`1.672`s。 + +通过以上参数分析,发现老年代Full GC没有,说明没有大对象进入到老年代,整个老年代的GC情况还是不错的。另外,年轻代回收次数`12`次,使用时间`1.672`s,每次用时100ms左右,这个时间稍微长了一点,可以将新生代的空间调低一点,以降低每一次的GC时间。 + +## 常见的垃圾回收器有哪些? + +先上一张图,这张图是Java虚拟机的jdk1.7及以前版本的所有垃圾回收器,也可以说是比较成熟的垃圾回收器,除了这些垃圾回收器,面试的时候最多也就再怼怼G1和ZGC了。 + + + +上面的表示是年轻代的垃圾回收器:Serial、ParNew、Parallel Scavenge,下面表示是老年代的垃圾回收器:CMS、Parallel Old、Serial Old,以及不分老年代和年轻代的G1。之间的相互的连线表示可以相互配合使用。 + +说完是不是一篇明朗,其实也就是那么回事。 + +#### 新生代垃圾回收器 + +##### Serial + +Serial(串行)收集器是**最基本、发展历史最悠久**的收集器,它是采用**复制算法**的新生代收集器,曾经(JDK 1.3.1之前)是虚拟机新生代收集的唯一选择。它是一个**单线程收集器**,只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直至Serial收集器收集结束为止(“Stop The World”)。 + +其实对于这个垃圾回收器,你只要记住是一个**单线程、采用复制算法的,会进行“Stop The World”** 即可,因为面试官一般不问这个,为什么,因为太简单了,没什么可问的呗。 + +好了,再放一张图好吧,说明一下Serial的回收过程,完事。 + + + +说明:这张图的意思就是**单线程,新生代使用复制算法标记、老年代使用标记整理算法标记**,就是这么简单。 + +##### ParNew + +**ParNew收集器就是Serial收集器的**多线程**版本**,它也是一个新生代收集器。除了使用多线程进行垃圾收集外,其余行为包括Serial收集器可用的所有控制参数、收集算法(复制算法)、Stop The World、对象分配规则、回收策略等与Serial收集器完全相同。 + +需要注意一点是:**除了Serial收集器外,目前只有它能和CMS收集器(Concurrent Mark Sweep)配合工作。** + +最后再放一张回收过程图; + +![](http://image.ouyangsihai.cn/FvsKnXGzEQd6WYdUmIgLcWoSHG4H) + +*** 是不是很简单,我在这里讲这些知识点并不是为了深入去了解这些原理,基本的知道对于工作已经够了,其实,主要还是应付面试官,哈哈。 + +##### Parallel Scavenge + +Parallel Scavenge收集器也是一个**并行**的**多线程**新生代收集器,它也使用**复制算法**。 + +Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。 + +这里需要注意的唯一的区别是:Parallel Scavenge收集器的目标是**达到一个可控制的吞吐量(Throughput)**。 + +我们知道,停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而**高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务**。 + +#### 老年代垃圾回收器 + +##### Serial Old + +Serial Old 是Serial收集器的老年代版本,它同样是一个**单线程**收集器,使用“**标记-整理**”(Mark-Compact)算法。 + +在这里就可以出一个面试题了。 +- 为什么Serial使用的是**复制算法**,而Serial Old使用是**标记-整理**算法? +同一个爸爸,儿子长的天差地别,当然也有啊,哈哈。 + +> + 其实,看了我前面的文章你可能就知道了,因为在新生代绝大多数的内存都是会被回收的,所以留下来的需要回收的垃圾就很少了,所以复制算法更合适,你可以发现,基本的老年代的都是使用标记整理算法,当然,CMS是个杂种哈。 + + +它的工作流程与Serial收集器相同,下图是Serial/Serial Old配合使用的工作流程图: + + + +##### Parallel Old + +Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用**多线程**和“**标记-整理**”算法,是不是前面说的,老年代出了杂种CMS不是“**标记-整理**”算法,其他都是。 + +另外,有了Parallel Old垃圾回收器后,就出现了以“**吞吐量优先**”著称的“男女朋友”收集器了,这就是:**Parallel Old和Parallel Scavenge收集器的组合**。 + +Parallel Old收集器的工作流程与Parallel Scavenge相同,这里给出Parallel Scavenge/Parallel Old收集器配合使用的流程图: + + + +你是不是以为我还要讲CMS和G1,我任性,我就是要接着讲,哈哈哈。 + + +#### CMS + +##### 小伙子,你说一下 CMS 垃圾回收器吧! + +这个题目一来,吓出一身冷汗,差点就没有复习这个CMS,还好昨晚抱佛脚看了一下哈。 + +于是我。。。一顿操作猛如虎。 + +CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它是基于“标记-清除”算法实现的,并且常见的应用场景是**互联网站或者B/S系统的服务端上的Java应用**。 + +结果就一紧张就记得这么多,面试官肯定不满意了,这个时候,面试官的常规操作是,**继续严刑拷打,他想,你可能忘记了,我来提醒提醒你!** + +##### CMS收集器工作的整个流程是怎么样的,你能给我讲讲吗? + +这个时候,面试官还会安慰你说不用紧张,但是,安慰归安慰,最后挂不挂可是另一回事。 + +于是,我又开始回答问题。 + +CMS 处理过程有七个步骤: + +- **初始标记**,会导致stw; +- **并发标记**,与用户线程同时运行; +- **预清理**,与用户线程同时运行; +- **可被终止的预清理**,与用户线程同时运行; +- **重新标记** ,会导致swt; +- **并发清除**,与用户线程同时运行; + +其实,只要回答四个就差不多了,是这几个。 + +- **初始标记**:仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。 +- **并发标记**:进行GC Roots Tracing的过程,在整个过程中耗时最长。 +- **重新标记**:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。此阶段也需要“Stop The World”。 +- **并发清除**。 + +你以为这样子就可以了,面试官就会说可以了,如果可以了,那估计你凉了! + +##### 面试官说:CMS这么好,那有没有什么缺点呢? + +我。。。好吧,谁怪我这么强呢,对吧。 + +其实,CMS虽然经过这么些年的考验,已经是一个值得信赖的GC回收器了,但是,其实也是有一些他的不足的, + +第一,**垃圾碎片的问题**,我们都知道CMS是使用的是**标记-清除**算法的,所以不可避免的就是会出现垃圾碎片的问题。 +第二,**一般CMS的GC耗时80%都在remark阶段,remark阶段停顿时间会很长**,在CMS的这四个主要的阶段中,最费时间的就是重新标记阶段。 +第三,**concurrent mode failure**,说出这个的时候,面试官就会觉得,小伙子,哎呦,不错哟,掌握的比较清楚,那这个是什么意思呢,其实是说: + +>这个异常发生在cms正在回收的时候。执行CMS GC的过程中,同时业务线程也在运行,当年轻带空间满了,执行ygc时,需要将存活的对象放入到老年代,而此时老年代空间不足,这时CMS还没有机会回收老年带产生的,或者在做Minor GC的时候,新生代救助空间放不下,需要放入老年代,而老年代也放不下而产生的。 + +第四,**promotion failed**,这个问题是指,在进行Minor GC时,Survivor空间不足,对象只能放入老年代,而此时老年代也放不下造成的,多数是由于老年代有足够的空闲空间,但是由于碎片较多,新生代要转移到老年带的对象比较大,找不到一段连续区域存放这个对象导致的。 + +面试官看到你掌握的这么好,心里已经给你竖起来大拇指,但是,面试官觉得你优秀啊,就还想看看你到底还有多少东西。 + +##### 既然你知道有这么多的缺点,那么你知道怎么解决这些问题吗? + +这个真的被问蒙了,你以为我什么都会吗!!!! + +但是,我还是得给大家讲讲,不然下次被问到,可能会把锅甩给我。 + +- **垃圾碎片的问题**:针对这个问题,这时候我们需要用到这个参数:`-XX:CMSFullGCsBeforeCompaction=n` 意思是说在上一次CMS并发GC执行过后,到底还要再执行多少次`full GC`才会做压缩。默认是0,也就是在默认配置下每次CMS GC顶不住了而要转入full GC的时候都会做压缩。 + +- **concurrent mode failure** + +解决这个问题其实很简单,只需要设置两个参数即可 + +`-XX:+UseCMSInitiatingOccupancyOnly` +`-XX:CMSInitiatingOccupancyFraction=60`:是指设定CMS在对内存占用率达到60%的时候开始GC。 + +为什么设置这两个参数呢?由于在垃圾收集阶段用户线程还需要运行,那也就还需要**预留有足够的内存空间给用户线程使用**,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集。 + +当然也不能设置过高,比如90%,这时候虽然GC次数少,但是,却会导致用于用户线程空间小,效率不高,太低10%,你自己想想会怎么样,体会体会! + +**哈哈,万事大吉,这一点说出了,估计面试官已经爱上我了吧,赶紧把我招进去干活吧。** + +- **remark阶段停顿时间会很长的问题**:解决这个问题巨简单,加入`-XX:+CMSScavengeBeforeRemark`。在执行remark操作之前先做一次`Young GC`,目的在于减少年轻代对老年代的无效引用,降低remark时的开销。 + +#### G1 (Garbage-First) + +G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器。以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。 + +- 可以像CMS收集器一样, GC 操作与应用的线程一起并发执行 +- 紧凑的空闲内存区间且没有很长的 GC 停顿时间 +- 需要可预测的GC暂停耗时 +- 不想牺牲太多吞吐量性能. +- 启动后不需要请求更大的Java堆 + +那么 G1 相对于 CMS 的区别在: + +- G1 在压缩空间方面有优势 + +- G1 通过将内存空间分成区域(Region)的方式避免内存碎片问题 + +- Eden, Survivor, Old 区不再固定、在内存使用效率上来说更灵活 + +- G1 可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象 + +- G1 在回收内存后会马上同时做合并空闲内存的工作、而 CMS 默认是在STW(stop the world)的时候做 + +- G1 会在 Young GC 中使用,而 CMS 只能在O区使用 + +就目前而言、CMS 还是默认首选的 GC 策略、可能在以下场景下 G1 更适合: + +- 服务端多核 CPU、JVM 内存占用较大的应用(至少大于4G) + +- 应用在运行过程中会产生大量内存碎片、需要经常压缩空间 + +- 想要更可控、可预期的 GC 停顿周期;防止高并发下应用雪崩现象 + +G1的内存使用示意图: +![](http://image.ouyangsihai.cn/FgFGLjZMCGmjKscUAj3hBfK1HLKM) + +G1在运行过程中主要包含如下4种操作方式: + +- YGC(不同于CMS) + +- 并发阶段 + +- 混合模式 + +- full GC (一般是G1出现问题时发生) + +##### YGC(年轻代GC) +下面是一次 YGC 前后内存区域是示意图: +![](http://image.ouyangsihai.cn/FjX3b5ZidxSGhDRF-qiJ4SXm-ji5) + +图中每个小区块都代表 G1 的一个区域(Region),区块里面的字母代表不同的分代内存空间类型(如[E]Eden,[O]Old,[S]Survivor)空白的区块不属于任何一个分区;G1 可以在需要的时候任意指定这个区域属于 Eden 或是 O 区之类的。 + +YoungGC 在 Eden 充满时触发,在回收之后所有之前属于 Eden 的区块全变成空白。然后至少有一个区块是属于 S 区的(如图半满的那个区域),同时可能有一些数据移到了 O 区。 + +目前大都使用 PrintGCDetails 参数打出GC日志、这个参数对G1同样有效、但日志内容颇为不同。 + +下面是一个Young GC的例子: +``` +23.430: [GC pause (young), 0.23094400 secs] +... +[Eden: 1286M(1286M)->0B(1212M) +Survivors: 78M->152M Heap: 1454M(4096M)->242M(4096M)] +[Times: user=0.85 sys=0.05, real=0.23 secs] +``` +上面日志的内容解析:Young GC实际占用230毫秒、其中GC线程占用850毫秒的CPU时间 + +- E:内存占用从 1286MB 变成 0、都被移出 +- S:从 78M 增长到了 152M、说明从 Eden 移过来 74M +- Heap: 占用从 1454 变成 242M、说明这次 Young GC 一共释放了 1212M 内存空间 + +很多情况下,S 区的对象会有部分晋升到 Old 区,另外如果 S 区已满、Eden 存活的对象会直接晋升到 Old 区,这种情况下 Old 的空间就会涨。 + +##### 并发阶段 +一个并发G1回收周期前后内存占用情况如下图所示: +![](http://image.ouyangsihai.cn/Flo526oI2G_UIbkT-Jibo5ZoX27u) + +从上面的图表可以看出以下几点: + +- Young 区发生了变化、这意味着在 G1 并发阶段内至少发生了一次 YGC(这点和 CMS 就有区别),Eden 在标记之前已经被完全清空,因为在并发阶段应用线程同时在工作、所以可以看到 Eden 又有新的占用 +- 一些区域被X标记,这些区域属于 O 区,此时仍然有数据存放、不同之处在 G1 已标记出这些区域包含的垃圾最多、也就是回收收益最高的区域 +- 在并发阶段完成之后实际上 O 区的容量变得更大了(O+X 的方块)。这时因为这个过程中发生了 YGC 有新的对象进入所致。此外,这个阶段在 O 区没有回收任何对象:它的作用主要是标记出垃圾最多的区块出来。对象实际上是在后面的阶段真正开始被回收 + +G1 并发标记周期可以分成几个阶段、其中有些需要暂停应用线程。第一个阶段是初始标记阶段。这个阶段会暂停所有应用线程-部分原因是这个过程会执行一次 YGC、下面是一个日志示例: +``` +50.541: [GC pause (young) (initial-mark), 0.27767100 secs] +[Eden: 1220M(1220M)->0B(1220M) +Survivors: 144M->144M Heap: 3242M(4096M)->2093M(4096M)] +[Times: user=1.02 sys=0.04, real=0.28 secs] +``` +上面的日志表明发生了 YGC 、应用线程为此暂停了 280 毫秒,Eden 区被清空(71MB 从 Young 区移到了 O 区)。 + +日志里面 initial-mark 的字样表明后台的并发 GC 阶段开始了。因为初始标记阶段本身也是要暂停应用线程的,G1 正好在 YGC 的过程中把这个事情也一起干了。为此带来的额外开销不是很大、增加了 20% 的 CPU ,暂停时间相应的略微变长了些。 + +接下来,G1 开始扫描根区域、日志示例: +``` +50.819: [GC concurrent-root-region-scan-start] +51.408: [GC concurrent-root-region-scan-end, 0.5890230] +``` +一共花了 580 毫秒,这个过程没有暂停应用线程;是后台线程并行处理的。这个阶段不能被 YGC 所打断、因此后台线程有足够的 CPU 时间很关键。如果 Young 区空间恰好在 Root 扫描的时候满了、YGC 必须等待 root 扫描之后才能进行。带来的影响是 YGC 暂停时间会相应的增加。这时的 GC 日志是这样的: +``` +350.994: [GC pause (young) +351.093: [GC concurrent-root-region-scan-end, 0.6100090] +351.093: [GC concurrent-mark-start],0.37559600 secs] +``` +GC 暂停这里可以看出在 root 扫描结束之前就发生了,表明 YGC 发生了等待,等待时间大概是100毫秒。 + +在 root 扫描完成后,G1 进入了一个并发标记阶段。这个阶段也是完全后台进行的;GC 日志里面下面的信息代表这个阶段的开始和结束: +``` +111.382: [GC concurrent-mark-start] +.... +120.905: [GC concurrent-mark-end, 9.5225160 sec] +``` +并发标记阶段是可以被打断的,比如这个过程中发生了 YGC 就会。这个阶段之后会有一个二次标记阶段和清理阶段: +``` +120.910: [GC remark 120.959: +[GC ref-PRC, 0.0000890 secs], 0.0718990 secs] +[Times: user=0.23 sys=0.01, real=0.08 secs] +120.985: [GC cleanup 3510M->3434M(4096M), 0.0111040 secs] +[Times: user=0.04 sys=0.00, real=0.01 secs] +``` +这两个阶段同样会暂停应用线程,但时间很短。接下来还有额外的一次并发清理阶段: +``` +120.996: [GC concurrent-cleanup-start] +120.996: [GC concurrent-cleanup-end, 0.0004520] +``` +到此为止,正常的一个 G1 周期已完成–这个周期主要做的是发现哪些区域包含可回收的垃圾最多(标记为 X ),实际空间释放较少。 + +##### 混合 GC + +接下来 G1 执行一系列的混合 GC。这个时期因为会同时进行 YGC 和清理上面已标记为 X 的区域,所以称之为混合阶段,下面是一个混合 GC 执行的前后示意图: +![](http://image.ouyangsihai.cn/Flo526oI2G_UIbkT-Jibo5ZoX27u) + +像普通的 YGC 那样、G1 完全清空掉 Eden 同时调整 survivor 区。另外,两个标记也被回收了,他们有个共同的特点是包含最多可回收的对象,因此这两个区域绝对部分空间都被释放了。这两个区域任何存活的对象都被移到了其他区域(和 YGC 存活对象晋升到 O 区类似)。这就是为什么 G1 的堆比 CMS 内存碎片要少很多的原因——移动这些对象的同时也就是在压缩对内存。下面是一个混合GC的日志: +``` +79.826: [GC pause (mixed), 0.26161600 secs] +.... +[Eden: 1222M(1222M)->0B(1220M) +Survivors: 142M->144M Heap: 3200M(4096M)->1964M(4096M)] +[Times: user=1.01 sys=0.00, real=0.26 secs] +``` +上面的日志可以注意到 Eden 释放了 1222 MB、但整个堆的空间释放内存要大于这个数目。数量相差看起来比较少、只有 16 MB,但是要考虑同时有 survivor 区的对象晋升到 O 区;另外,每次混合 GC 只是清理一部分的 O 区内存,整个 GC 会一直持续到几乎所有的标记区域垃圾对象都被回收,这个阶段完了之后 G1 会重新回到正常的 YGC 阶段。周期性的,当O区内存占用达到一定数量之后 G1 又会开启一次新的并行 GC 阶段. + +G1来源:https://blog.51cto.com/lqding/1770055 + +### 总结 + +这里把上面的这些垃圾回收器做个总结,看完这个,面试给面试官讲的时候思路就非常清晰了。 + +|收集器|串行、并行or并发|新生代/老年代|算法|目标|适用场景| +|------|------|------|------|------|------| +|**Serial**|串行|新生代|复制算法|响应速度优先|单CPU环境下的Client模式 +|**Serial Old**|串行|老年代|标记-整理|响应速度优先|单CPU环境下的Client模式、CMS的后备预案 +|**ParNew**|并行|新生代|复制算法|响应速度优先|多CPU环境时在Server模式下与CMS配合 +|**Parallel Scavenge**|并行|新生代|复制算法|吞吐量优先|在后台运算而不需要太多交互的任务 +|**Parallel Old**|并行|老年代|标记-整理|吞吐量优先|在后台运算而不需要太多交互的任务 +|**CMS**|并发|老年代|标记-清除|一种以获取最短回收停顿时间为目标的收集器|互联网站或者B/S系统的服务端上的Java应用 +|**G1**|并发|老年代|标记-整理|高吞吐量|面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器 + + +## 内存泄漏和内存溢出,什么时候会出现,怎么解决? + +内存泄漏:(Memory Leak) 不再会被使用的对象的内存不能被回收,就是内存泄露。 + +强引用所指向的对象不会被回收,可能导致内存泄漏,虚拟机宁愿抛出OOM也不会去回收他指向的对象。 + +意思就是你用资源的时候为他开辟了一块空间,当你用完时忘记释放资源了,这时内存还被占用着,一次没关系,但是内存泄漏次数多了就会导致内存溢出。 + +内存溢出:(Out Of Memory —— OOM) 指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储 int 类型数据的存储空间,但是你却存储 long 类型的数据,那么结果就是内存不够用,此时就会报错 OOM ,即所谓的内存溢出,简单来说就是自己所需要使用的空间比我们拥有的内存大内存不够使用所造成的内存溢出。 + +内存的释放:即清理那些不可达的对象,是由 GC 决定和执行的,所以 GC 会监控每一个对象的状态,包括申请、引用、被引用和赋值等。释放对象的根本原则就是对象不会再被使用。 + +#### 内存泄漏的原因 + +- 静态集合类引起内存泄漏; + +- 当集合里面的对象属性被修改后,再调用remove()方法时不起作用。JDK1.8 貌似修正了引用对象修改参数,导致hashCode变更的问题; + +- 监听器 Listener 各种连接 Connection,没有及时关闭; + +- 内部类和外部模块的引用(尽量使用静态内部类); + +- 单例模式(静态类持有引用,导致对象不可回收); + +#### 解决方法 + +- 尽早释放无用对象的引用,及时关闭使用的资源,数据库连接等; +- 特别注意一些像 HashMap 、ArrayList 的集合对象,它们经常会引发内存泄漏。当它们被声明为 static 时,它们的生命周期就会和应用程序一样长。 +- 注意 事件监听 和 回调函数 。当一个监听器在使用的时候被注册,但不再使用之后却未被反注册。 + +#### 内存溢出的情况和解决方法 + +- OOM for Heap (java.lang.OutOfMemoryError: Java heap space) + +此 OOM 是由于 JVM 中 heap 的最大值不满足需要,将设置 heap 的最大值调高即可,如,-Xmx8G。 + +- OOM for StackOverflowError (Exception in thread "main" java.lang.StackOverflowError) + +如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出 StackOverflowError 异常。 + +如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出 OutOfMemoryError 异常。 + +检查程序是否有深度递归。 + +- OOM for Perm (java.lang.OutOfMemoryError: PermGen space) + +调高 Perm 的最大值,即 -XX:MaxPermSize 的值调大。 + +- OOM for GC (java.lang.OutOfMemoryError: GC overhead limit exceeded) + +此 OOM 是由于 JVM 在 GC 时,对象过多,导致内存溢出,建议调整 GC 的策略,在一定比例下开始 GC 而不要使用默认的策略,或者将新代和老代设置合适的大小,需要进行微调存活率。 + +改变 GC 策略,在老代 80% 时就是开始 GC ,并且将`-XX:SurvivorRatio(-XX:SurvivorRatio=8)`和`-XX:NewRatio(-XX:NewRatio=4)`设置的更合理。 + +- OOM for native thread created (java.lang.OutOfMemoryError: unable to create new native thread) + +将 heap 及 perm 的最大值下调,并将线程栈调小,即 -Xss 调小,如:-Xss128k。 + +在 JVM 内存不能调小的前提下,将 -Xss 设置较小,如:-Xss:128k。 + +- OOM for allocate huge array (Exception in thread "main": java.lang.OutOfMemoryError: Requested array size exceeds VM limit) + +此类信息表明应用程序试图分配一个大于堆大小的数组。例如,如果应用程序 new 一个数组对象,大小为 512M,但是最大堆大小为 256M,因此 OutOfMemoryError 会抛出,因为数组的大小超过虚拟机的限制。 +1) 首先检查 heap 的 -Xmx 是不是设置的过小; +2) 如果 heap 的 -Xmx 已经足够大,那么请检查应用程序是不是存在 bug,例如:应用程序可能在计算数组的大小时,存在算法错误,导致数组的 size 很大,从而导致巨大的数组被分配。 + +- OOM for small swap (Exception in thread "main": java.lang.OutOfMemoryError: request bytes for . Out of swap space? ) + +由于从 native 堆中分配内存失败,并且堆内存可能接近耗尽。 + +1) 检查 os 的 swap 是不是没有设置或者设置的过小; +2) 检查是否有其他进程在消耗大量的内存,从而导致当前的 JVM 内存不够分配。 + +- OOM for exhausted native memory (java.lang.OutOfMemoryErr java.io.FileInputStream.readBytes(Native Method)) + +从错误日志来看,在 OOM 后面没有提示引起 OOM 的原因,进一步查看 stack trace 发现,导致 OOM 的原因是由Native Method 的调用引起的,另外检查 Java heap ,发现 heap 的使用正常,因而需要考虑问题的发生是由于 Native memory 被耗尽导致的。 + +从根本上来说,解决此问题的方法应该通过检测发生问题时的环境下,native memory 为什么被占用或者说为什么 native memory 越来越小,从而去解决引起 Native memory 减小的问题。但是如果此问题不容易分析时,可以通过以下方法或者结合起来处理。 + +1) cpu 和 os 保证是 64 位的,并且 jdk 也换为 64 位的。 +2) 将 java heap 的 -Xmx 尽量调小,但是保证在不影响应用使用的前提下。 +3) 限制对 native memory 的消耗,比如:将 thread 的 -Xss 调小,并且限制产生大量的线程;限制文件的 io 操作次数和数量;限制网络的使用等等。 + +## Java 的类加载过程 + +JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程。其中加载、检验、准备、初始化和卸载这个五个阶段的顺序是固定的,而解析则未必。为了支持动态绑定,解析这个过程可以发生在初始化阶段之后。 + +![](http://image.ouyangsihai.cn/FpontTFCT65kRlJvGhuMsP9lkRkZ) + + +#### 加载 + +加载过程主要完成三件事情: + +1. 通过类的全限定名来获取定义此类的二进制字节流 +2. 将这个类字节流代表的静态存储结构转为方法区的运行时数据结构 +3. 在堆中生成一个代表此类的 java.lang.Class 对象,作为访问方法区这些数据结构的入口。 + +#### 校验 + +此阶段主要确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的自身安全。 + +1. 文件格式验证:基于字节流验证。 +2. 元数据验证:基于方法区的存储结构验证。 +3. 字节码验证:基于方法区的存储结构验证。 +4. 符号引用验证:基于方法区的存储结构验证。 + +#### 准备 + +为类变量分配内存,并将其初始化为默认值。(此时为默认值,在初始化的时候才会给变量赋值)即在方法区中分配这些变量所使用的内存空间。例如: + +```java +public static int value = 123; +``` + +此时在准备阶段过后的初始值为0而不是 123 ;将 value 赋值为 123 的 putstatic 指令是程序被编译后,存放于类构造器方法之中。特例: + +```java +public static final int value = 123; +``` + +此时 value 的值在准备阶段过后就是 123 。 + +#### 解析 + +把类型中的符号引用转换为直接引用。 + +* 符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的 Class 文件格式中。 +* 直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在 + +主要有以下四种: + +1. 类或接口的解析 +2. 字段解析 +3. 类方法解析 +4. 接口方法解析 + +#### 初始化 + +初始化阶段是执行类构造器方法的过程。方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证方法执行之前,父类的方法已经执行完毕。如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成()方法。 + +Java 中,对于初始化阶段,有且只有以下五种情况才会对要求类立刻“初始化”(加载,验证,准备,自然需要在此之前开始): + +1. 使用 new 关键字实例化对象、访问或者设置一个类的静态字段(被 final 修饰、编译器优化时已经放入常量池的例外)、调用类方法,都会初始化该静态字段或者静态方法所在的类。 +2. 初始化类的时候,如果其父类没有被初始化过,则要先触发其父类初始化。 +3. 使用 java.lang.reflect 包的方法进行反射调用的时候,如果类没有被初始化,则要先初始化。 +4. 虚拟机启动时,用户会先初始化要执行的主类(含有main) +5. jdk 1.7后,如果 java.lang.invoke.MethodHandle的实例最后对应的解析结果是 REF_getStatic、REF_putStatic、REF_invokeStatic 方法句柄,并且这个方法所在类没有初始化,则先初始化。 + +## 聊聊双亲委派机制 + +![](http://image.ouyangsihai.cn/FoFkvh_XGVUdHkp3F6dYBpwE8tgb) + +**工作过程** + +如果一个类加载器收到了类加载器的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父加载器去完成,每个层次的类加载器都是如此,因此,所有的加载请求最终都会传送到 Bootstrap 类加载器(启动类加载器)中,只有父类加载反馈自己无法加载这个请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。 + +**优点** + +Java 类随着它的加载器一起具备了一种带有优先级的层次关系. + +例如类 java.lang.Object ,它存放在 rt.jar 之中,无论哪一个类加载器都要加载这个类,最终都是双亲委派模型最顶端的 Bootstrap 类加载器去加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户编写了一个称为 “java.lang.Object” 的类,并存放在程序的 ClassPath 中,那系统中将会出现多个不同的 Object 类,java类型体系中最基础的行为也就无法保证,应用程序也将会一片混乱。 diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Dubbo.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Dubbo.md" new file mode 100644 index 0000000..ab40459 --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Dubbo.md" @@ -0,0 +1,516 @@ + + +## 基础知识 + +### 为什么要用 Dubbo? + +* 随着服务化的进一步发展,服务越来越多,服务之间的调用和依赖关系也越来越复杂,诞生了面向服务的架构体系(SOA),也因此衍生出了一系列相应的技术,如对服务提供、服务调用、连接处理、通信协议、序列化方式、服务发现、服务路由、日志输出等行为进行封装的服务框架。就这样为分布式系统的服务治理框架就出现了,Dubbo 也就这样产生了。 + +### Dubbo 是什么? + +* Dubbo 是一款高性能、轻量级的开源 RPC 框架,提供服务自动注册、自动发现等高效服务治理方案, 可以和 Spring 框架无缝集成。 + +### Dubbo 的使用场景有哪些? + +* 透明化的远程方法调用:就像调用本地方法一样调用远程方法,只需简单配置,没有任何API侵入。 +* 软负载均衡及容错机制:可在内网替代 F5 等硬件负载均衡器,降低成本,减少单点。 +* 服务自动注册与发现:不再需要写死服务提供方地址,注册中心基于接口名查询服务提供者的IP地址,并且能够平滑添加或删除服务提供者。 + +### Dubbo 核心功能有哪些? + +* Remoting:网络通信框架,提供对多种NIO框架抽象封装,包括“同步转异步”和“请求-响应”模式的信息交换方式。 +* Cluster:服务框架,提供基于接口方法的透明远程过程调用,包括多协议支持,以及软负载均衡,失败容错,地址路由,动态配置等集群支持。 +* Registry:服务注册,基于注册中心目录服务,使服务消费方能动态的查找服务提供方,使地址透明,使服务提供方可以平滑增加或减少机器。 + +### Dubbo 核心组件有哪些? + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/1717841c2bd87ef0~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* Provider:暴露服务的服务提供方 +* Consumer:调用远程服务消费方 +* Registry:服务注册与发现注册中心 +* Monitor:监控中心和访问调用统计 +* Container:服务运行容器 + +### Dubbo 服务器注册与发现的流程? + +* 服务容器Container负责启动,加载,运行服务提供者。 +* 服务提供者Provider在启动时,向注册中心注册自己提供的服务。 +* 服务消费者Consumer在启动时,向注册中心订阅自己所需的服务。 +* 注册中心Registry返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。 +* 服务消费者Consumer,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。 +* 服务消费者Consumer和提供者Provider,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心Monitor。 + +## 架构设计 + +### Dubbo 的整体架构设计有哪些分层? + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/1717841c2d11703a~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 接口服务层(Service):该层与业务逻辑相关,根据 provider 和 consumer 的业务设计对应的接口和实现 +* 配置层(Config):对外配置接口,以 ServiceConfig 和 ReferenceConfig 为中心 +* 服务代理层(Proxy):服务接口透明代理,生成服务的客户端 Stub 和 服务端的 Skeleton,以 ServiceProxy 为中心,扩展接口为 ProxyFactory +* 服务注册层(Registry):封装服务地址的注册和发现,以服务 URL 为中心,扩展接口为 RegistryFactory、Registry、RegistryService +* 路由层(Cluster):封装多个提供者的路由和负载均衡,并桥接注册中心,以Invoker 为中心,扩展接口为 Cluster、Directory、Router 和 LoadBlancce +* 监控层(Monitor):RPC 调用次数和调用时间监控,以 Statistics 为中心,扩展接口为 MonitorFactory、Monitor 和 MonitorService +* 远程调用层(Protocal):封装 RPC 调用,以 Invocation 和 Result 为中心,扩展接口为 Protocal、Invoker 和 Exporter +* 信息交换层(Exchange):封装请求响应模式,同步转异步。以 Request 和Response 为中心,扩展接口为 Exchanger、ExchangeChannel、ExchangeClient 和 ExchangeServer +* 网络 传输 层(Transport):抽象 mina 和 netty 为统一接口,以 Message 为中心,扩展接口为 Channel、Transporter、Client、Server 和 Codec +* 数据序列化层(Serialize):可复用的一些工具,扩展接口为 Serialization、ObjectInput、ObjectOutput 和 ThreadPool + +### Dubbo Monitor 实现原理? + +* Consumer 端在发起调用之前会先走 filter 链;provider 端在接收到请求时也是先走 filter 链,然后才进行真正的业务逻辑处理。默认情况下,在 consumer 和 provider 的 filter 链中都会有 Monitorfilter。 + +1. MonitorFilter 向 DubboMonitor 发送数据 + +2. DubboMonitor 将数据进行聚合后(默认聚合 1min 中的统计数据)暂存到ConcurrentMap statisticsMap,然后使用一个含有 3 个线程(线程名字:DubboMonitorSendTimer)的线程池每隔 1min 钟,调用 SimpleMonitorService 遍历发送 statisticsMap 中的统计数据,每发送完毕一个,就重置当前的 Statistics 的 AtomicReference + +3. SimpleMonitorService 将这些聚合数据塞入 BlockingQueue queue 中(队列大写为 100000) + +4. SimpleMonitorService 使用一个后台线程(线程名为:DubboMonitorAsyncWriteLogThread)将 queue 中的数据写入文件(该线程以死循环的形式来写) + +5. SimpleMonitorService 还会使用一个含有 1 个线程(线程名字:DubboMonitorTimer)的线程池每隔 5min 钟,将文件中的统计数据画成图表 + +## 分布式框架 + +### Dubbo 类似的分布式框架还有哪些? + +* 比较著名的就是 Spring Cloud。 + +### Dubbo 和 Spring Cloud 有什么关系? + +* Dubbo 是 SOA 时代的产物,它的关注点主要在于服务的调用,流量分发、流量监控和熔断。而 Spring Cloud 诞生于微服务架构时代,考虑的是微服务治理的方方面面,另外由于依托了 Spring、Spring Boot 的优势之上,两个框架在开始目标就不一致,Dubbo 定位服务治理、Spring Cloud 是打造一个生态。 + +#### Dubbo 和 Spring Cloud 有什么哪些区别? + +* Dubbo 底层是使用 Netty 这样的 NIO 框架,是基于 TCP 协议传输的,配合以 Hession 序列化完成 RPC 通信。 + +* Spring Cloud 是基于 Http 协议 Rest 接口调用远程过程的通信,相对来说 Http 请求会有更大的报文,占的带宽也会更多。但是 REST 相比 RPC 更为灵活,服务提供方和调用方的依赖只依靠一纸契约,不存在代码级别的强依赖,这在强调快速演化的微服务环境下,显得更为合适,至于注重通信速度还是方便灵活性,具体情况具体考虑。 + +### Dubbo 和 Dubbox 之间的区别? + +* Dubbox 是继 Dubbo 停止维护后,当当网基于 Dubbo 做的一个扩展项目,如加了服务可 Restful 调用,更新了开源组件等。 + +## 注册中心 + +### Dubbo 有哪些注册中心? + +* Multicast 注册中心:Multicast 注册中心不需要任何中心节点,只要广播地址,就能进行服务注册和发现,基于网络中组播传输实现。 +* Zookeeper 注册中心:基于分布式协调系统 Zookeeper 实现,采用 Zookeeper 的 watch 机制实现数据变更。 +* Redis 注册中心:基于 Redis 实现,采用 key/map 存储,key 存储服务名和类型,map 中 key 存储服务 url,value 服务过期时间。基于 Redis 的发布/订阅模式通知数据变更。 +* Simple 注册中心。 +* 推荐使用 Zookeeper 作为注册中心 + +### Dubbo 的注册中心集群挂掉,发布者和订阅者之间还能通信么? + +* 可以通讯。启动 Dubbo 时,消费者会从 Zookeeper 拉取注册的生产者的地址接口等数据,缓存在本地。每次调用时,按照本地存储的地址进行调用。 + +## 集群 + +### Dubbo集群提供了哪些负载均衡策略? + +* Random LoadBalance: 随机选取提供者策略,有利于动态调整提供者权重。截面碰撞率高,调用次数越多,分布越均匀。 + +* RoundRobin LoadBalance: 轮循选取提供者策略,平均分布,但是存在请求累积的问题。 + +* LeastActive LoadBalance: 最少活跃调用策略,解决慢提供者接收更少的请求。 + +* ConstantHash LoadBalance: 一致性 Hash 策略,使相同参数请求总是发到同一提供者,一台机器宕机,可以基于虚拟节点,分摊至其他提供者,避免引起提供者的剧烈变动。 + +`默认为 Random 随机调用。` + +### Dubbo的集群容错方案有哪些? + +* Failover Cluster:失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。 +* Failfast Cluster:快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。 +* Failsafe Cluster:失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。 +* Failback Cluster:失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。 +* Forking Cluster:并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks=”2″ 来设置最大并行数。 +* Broadcast Cluster:广播调用所有提供者,逐个调用,任意一台报错则报错 。通常用于通知所有提供者更新缓存或日志等本地资源信息。 + +`默认的容错方案是 Failover Cluster。` + +## 配置 + +### Dubbo 配置文件是如何加载到 Spring 中的? + +* Spring 容器在启动的时候,会读取到 Spring 默认的一些 schema 以及 Dubbo 自定义的 schema,每个 schema 都会对应一个自己的 NamespaceHandler,NamespaceHandler 里面通过 BeanDefinitionParser 来解析配置信息并转化为需要加载的 bean 对象! + +### 说说核心的配置有哪些? + +| 标签 | 用途 | 解释 | +| --- | --- | --- | +| | 服务配置 | 用于暴露一个服务,定义服务的元信息,一个服务可以用多个协议暴露,一个服务也可以注册到多个注册中心 | +| | 引用配置 | 用于创建一个远程服务代理,一个引用可以指向多个注册中心 | +| | 协议配置 | 用于配置提供服务的协议信息,协议由提供方指定,消费方被动接受 | +| | 应用配置 | 用于配置当前应用信息,不管该应用是提供者还是消费者 | +| | 模块配置 | 用于配置当前模块信息,可选 | +| | 注册中心配置 | 用于配置连接注册中心相关信息 | +| | 监控中心配置 | 用于配置连接监控中心相关信息,可选 | +| | 提供方配置 | 当 ProtocolConfig 和 ServiceConfig 某属性没有配置时,采用此缺省值,可选 | +| | 消费方配置 | 当 ReferenceConfig 某属性没有配置时,采用此缺省值,可选 | +| | 方法配置 | 用于 ServiceConfig 和 ReferenceConfig 指定方法级的配置信息 | +| | 参数配置 | 用于指定方法参数配置 | + +`如果是SpringBoot项目就只需要注解,或者开Application配置文件!!!` + +### Dubbo 超时设置有哪些方式? + +**Dubbo 超时设置有两种方式:** + +* 服务提供者端设置超时时间,在Dubbo的用户文档中,推荐如果能在服务端多配置就尽量多配置,因为服务提供者比消费者更清楚自己提供的服务特性。 +* 服务消费者端设置超时时间,如果在消费者端设置了超时时间,以消费者端为主,即优先级更高。因为服务调用方设置超时时间控制性更灵活。如果消费方超时,服务端线程不会定制,会产生警告。 + +### 服务调用超时会怎么样? + +* dubbo 在调用服务不成功时,默认是会重试两次。 + +## 通信协议 + +### Dubbo 使用的是什么通信框架? + +* 默认使用 Netty 作为通讯框架。 + +### Dubbo 支持哪些协议,它们的优缺点有哪些? + +* Dubbo: 单一长连接和 NIO 异步通讯,适合大并发小数据量的服务调用,以及消费者远大于提供者。传输协议 TCP,异步 Hessian 序列化。Dubbo推荐使用dubbo协议。 + +* RMI: 采用 JDK 标准的 RMI 协议实现,传输参数和返回参数对象需要实现 Serializable 接口,使用 Java 标准序列化机制,使用阻塞式短连接,传输数据包大小混合,消费者和提供者个数差不多,可传文件,传输协议 TCP。 多个短连接 TCP 协议传输,同步传输,适用常规的远程服务调用和 RMI 互操作。在依赖低版本的 Common-Collections 包,Java 序列化存在安全漏洞。 + +* WebService:基于 WebService 的远程调用协议,集成 CXF 实现,提供和原生 WebService 的互操作。多个短连接,基于 HTTP 传输,同步传输,适用系统集成和跨语言调用。 + +* HTTP: 基于 Http 表单提交的远程调用协议,使用 Spring 的 HttpInvoke 实现。多个短连接,传输协议 HTTP,传入参数大小混合,提供者个数多于消费者,需要给应用程序和浏览器 JS 调用。 + +* Hessian:集成 Hessian 服务,基于 HTTP 通讯,采用 Servlet 暴露服务,Dubbo 内嵌 Jetty 作为服务器时默认实现,提供与 Hession 服务互操作。多个短连接,同步 HTTP 传输,Hessian 序列化,传入参数较大,提供者大于消费者,提供者压力较大,可传文件。 + +* Memcache:基于 Memcache实现的 RPC 协议。 + +* Redis:基于 Redis 实现的RPC协议。 + +## 设计模式 + +### Dubbo 用到哪些设计模式? + +`Dubbo 框架在初始化和通信过程中使用了多种设计模式,可灵活控制类加载、权限控制等功能。` + +* **工厂模式** + + Provider 在 export 服务时,会调用 ServiceConfig 的 export 方法。ServiceConfig中有个字段: + +private static final Protocol protocol = +ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtensi +on(); +复制代码 + +* 工厂模式 + + Provider 在 export 服务时,会调用 ServiceConfig 的 export 方法。ServiceConfig中有个字段: + +private static final Protocol protocol = +ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtensi +on(); +复制代码 + +Dubbo 里有很多这种代码。这也是一种工厂模式,只是实现类的获取采用了 JDKSPI 的机制。这么实现的优点是可扩展性强,想要扩展实现,只需要在 classpath下增加个文件就可以了,代码零侵入。另外,像上面的 Adaptive 实现,可以做到调用时动态决定调用哪个实现,但是由于这种实现采用了动态代理,会造成代码调试比较麻烦,需要分析出实际调用的实现类。 + +* **装饰器模式** + + Dubbo 在启动和调用阶段都大量使用了装饰器模式。以 Provider 提供的调用链为例,具体的调用链代码是在 ProtocolFilterWrapper 的 buildInvokerChain 完成的,具体是将注解中含有 group=provider 的 Filter 实现,按照 order 排序,最后的调用顺序是: + +EchoFilter -> ClassLoaderFilter -> GenericFilter -> ContextFilter -> +ExecuteLimitFilter -> TraceFilter -> TimeoutFilter -> MonitorFilter -> +ExceptionFilter +复制代码 + +更确切地说,这里是装饰器和责任链模式的混合使用。例如,EchoFilter 的作用是判断是否是回声测试请求,是的话直接返回内容,这是一种责任链的体现。而像ClassLoaderFilter 则只是在主功能上添加了功能,更改当前线程的 ClassLoader,这是典型的装饰器模式。 + +* **观察者模式** + + Dubbo 的 Provider 启动时,需要与注册中心交互,先注册自己的服务,再订阅自己的服务,订阅时,采用了观察者模式,开启一个 listener。注册中心会每 5 秒定时检查是否有服务更新,如果有更新,向该服务的提供者发送一个 notify 消息,provider 接受到 notify 消息后,运行 NotifyListener 的 notify 方法,执行监听器方法。 + +* **动态代理模式** + + Dubbo 扩展 JDK SPI 的类 ExtensionLoader 的 Adaptive 实现是典型的动态代理实现。Dubbo 需要灵活地控制实现类,即在调用阶段动态地根据参数决定调用哪个实现类,所以采用先生成代理类的方法,能够做到灵活的调用。生成代理类的代码是 ExtensionLoader 的 createAdaptiveExtensionClassCode 方法。代理类主要逻辑是,获取 URL 参数中指定参数的值作为获取实现类的 key。 + +## 运维管理 + +### 服务上线怎么兼容旧版本? + +* 可以用版本号(version)过渡,多个不同版本的服务注册到注册中心,版本号不同的服务相互间不引用。这个和服务分组的概念有一点类似。 + +### Dubbo telnet 命令能做什么? + +* dubbo 服务发布之后,我们可以利用 telnet 命令进行调试、管理。Dubbo2.0.5 以上版本服务提供端口支持 telnet 命令 + +### Dubbo 支持服务降级吗? + +* 以通过 dubbo:reference 中设置 mock=“return null”。mock 的值也可以修改为 true,然后再跟接口同一个路径下实现一个 Mock 类,命名规则是 “接口名称+Mock” 后缀。然后在 Mock 类里实现自己的降级逻辑 + +### Dubbo 如何优雅停机? + +* Dubbo 是通过 JDK 的 ShutdownHook 来完成优雅停机的,所以如果使用kill -9 PID 等强制关闭指令,是不会执行优雅停机的,只有通过 kill PID 时,才会执行。 + +## SPI + +### Dubbo SPI 和 Java SPI 区别? + +* JDK SPI: + + JDK 标准的 SPI 会一次性加载所有的扩展实现,如果有的扩展很耗时,但也没用上,很浪费资源。所以只希望加载某个的实现,就不现实了 + +* DUBBO SPI: + + 1、对 Dubbo 进行扩展,不需要改动 Dubbo 的源码 + + 2、延迟加载,可以一次只加载自己想要加载的扩展实现。 + + 3、增加了对扩展点 IOC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。 + + 4、Dubbo 的扩展机制能很好的支持第三方 IoC 容器,默认支持 Spring Bean。 + +## 其他 + +### Dubbo 支持分布式事务吗? + +* 目前暂时不支持,可与通过 tcc-transaction 框架实现 + +* 介绍:tcc-transaction 是开源的 TCC 补偿性分布式事务框架 + +* TCC-Transaction 通过 Dubbo 隐式传参的功能,避免自己对业务代码的入侵。 + +### Dubbo 可以对结果进行缓存吗? + +* 为了提高数据访问的速度。Dubbo 提供了声明式缓存,以减少用户加缓存的工作量 +* 其实比普通的配置文件就多了一个标签 cache=“true” + +### Dubbo 必须依赖的包有哪些? + +* Dubbo 必须依赖 JDK,其他为可选。 + +### Dubbo 支持哪些序列化方式? + +* 默认使用 Hessian 序列化,还有 Duddo、FastJson、Java 自带序列化。 + +### Dubbo 在安全方面有哪些措施? + +* Dubbo 通过 Token 令牌防止用户绕过注册中心直连,然后在注册中心上管理授权。 +* Dubbo 还提供服务黑白名单,来控制服务所允许的调用方。 + +### 服务调用是阻塞的吗? + +* 默认是阻塞的,可以异步调用,没有返回值的可以这么做。Dubbo 是基于 NIO 的非阻塞实现并行调用,客户端不需要启动多线程即可完成并行调用多个远程服务,相对多线程开销较小,异步调用会返回一个 Future 对象。 + +### 服务提供者能实现失效踢出是什么原理? + +* 服务失效踢出基于 zookeeper 的临时节点原理。 + +### 同一个服务多个注册的情况下可以直连某一个服务吗? + +* 可以点对点直连,修改配置即可,也可以通过 telnet 直接某个服务。 + +### Dubbo 服务降级,失败重试怎么做? + +* 可以通过 dubbo:reference 中设置 mock=“return null”。mock 的值也可以修改为 true,然后再跟接口同一个路径下实现一个 Mock 类,命名规则是 “接口名称+Mock” 后缀。然后在 Mock 类里实现自己的降级逻辑 + +### Dubbo 使用过程中都遇到了些什么问题? + +* 在注册中心找不到对应的服务,检查 service 实现类是否添加了@service 注解无法连接到注册中心,检查配置文件中的对应的测试 ip 是否正确 + +## RPC + +### 为什么要有RPC + +* http接口是在接口不多、系统与系统交互较少的情况下,解决信息孤岛初期常使用的一种通信手段;优点就是简单、直接、开发方便。利用现成的http协议进行传输。但是如果是一个大型的网站,内部子系统较多、接口非常多的情况下,RPC框架的好处就显示出来了,首先就是长链接,不必每次通信都要像http一样去3次握手什么的,减少了网络开销;其次就是RPC框架一般都有注册中心,有丰富的监控管理;发布、下线接口、动态扩展等,对调用方来说是无感知、统一化的操作。第三个来说就是安全性。最后就是最近流行的服务化架构、服务化治理,RPC框架是一个强力的支撑。 + +* socket只是一个简单的网络通信方式,只是创建通信双方的通信通道,而要实现rpc的功能,还需要对其进行封装,以实现更多的功能。 + +* RPC一般配合netty框架、spring自定义注解来编写轻量级框架,其实netty内部是封装了socket的,较新的jdk的IO一般是NIO,即非阻塞IO,在高并发网站中,RPC的优势会很明显 + +### 什么是RPC + +* RPC(Remote Procedure Call Protocol)远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。简言之,RPC使得程序能够像访问本地系统资源一样,去访问远端系统资源。比较关键的一些方面包括:通讯协议、序列化、资源(接口)描述、服务框架、性能、语言支持等。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/1717841c2ec735fd~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 简单的说,RPC就是从一台机器(客户端)上通过参数传递的方式调用另一台机器(服务器)上的一个函数或方法(可以统称为服务)并得到返回的结果。 + +### PRC架构组件 + +* 一个基本的RPC架构里面应该至少包含以下4个组件: + + 1、客户端(Client):服务调用方(服务消费者) + + 2、客户端存根(Client Stub):存放服务端地址信息,将客户端的请求参数数据信息打包成网络消息,再通过网络传输发送给服务端 + + 3、服务端存根(Server Stub):接收客户端发送过来的请求消息并进行解包,然后再调用本地服务进行处理4、服务端(Server):服务的真正提供者 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/1717841c2fb08565~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 具体调用过程: + + 1、服务消费者(client客户端)通过调用本地服务的方式调用需要消费的服务; + + 2、客户端存根(client stub)接收到调用请求后负责将方法、入参等信息序列化(组装)成能够进行网络传输的消息体; + + 3、客户端存根(client stub)找到远程的服务地址,并且将消息通过网络发送给服务端; + + 4、服务端存根(server stub)收到消息后进行解码(反序列化操作); + + 5、服务端存根(server stub)根据解码结果调用本地的服务进行相关处理; + + 6、本地服务执行具体业务逻辑并将处理结果返回给服务端存根(server stub); + + 7、服务端存根(server stub)将返回结果重新打包成消息(序列化)并通过网络发送至消费方; + + 8、客户端存根(client stub)接收到消息,并进行解码(反序列化); + + 9、服务消费方得到最终结果; + +`而RPC框架的实现目标则是将上面的第2-10步完好地封装起来,也就是把调用、编码/解码的过程给封装起来,让用户感觉上像调用本地服务一样的调用远程服务。` + +### RPC和SOA、SOAP、REST的区别 + +* 1、REST + + 可以看着是HTTP协议的一种直接应用,默认基于JSON作为传输格式,使用简单,学习成本低效率高,但是安全性较低。 + +* 2、SOAP + + SOAP是一种数据交换协议规范,是一种轻量的、简单的、基于XML的协议的规范。而SOAP可以看着是一个重量级的协议,基于XML、SOAP在安全方面是通过使用XML-Security和XML-Signature两个规范组成了WS-Security来实现安全控制的,当前已经得到了各个厂商的支持 。 + + 它有什么优点?简单总结为:易用、灵活、跨语言、跨平台。 + +* 3、SOA + + 面向服务架构,它可以根据需求通过网络对松散耦合的粗粒度应用组件进行分布式部署、组合和使用。服务层是SOA的基础,可以直接被应用调用,从而有效控制系统中与软件代理交互的人为依赖性。 + + SOA是一种粗粒度、松耦合服务架构,服务之间通过简单、精确定义接口进行通讯,不涉及底层编程接口和通讯模型。SOA可以看作是B/S模型、XML(标准通用标记语言的子集)/Web Service技术之后的自然延伸。 + +* 4、REST 和 SOAP、RPC 有何区别呢? + + 没什么太大区别,他们的本质都是提供可支持分布式的基础服务,最大的区别在于他们各自的的特点所带来的不同应用场景 。 + +### RPC框架需要解决的问题? + +* 1、如何确定客户端和服务端之间的通信协议? +* 2、如何更高效地进行网络通信? +* 3、服务端提供的服务如何暴露给客户端? +* 4、客户端如何发现这些暴露的服务? +* 5、如何更高效地对请求对象和响应结果进行序列化和反序列化操作? + +### RPC的实现基础? + +* 1、需要有非常高效的网络通信,比如一般选择Netty作为网络通信框架; +* 2、需要有比较高效的序列化框架,比如谷歌的Protobuf序列化框架; +* 3、可靠的寻址方式(主要是提供服务的发现),比如可以使用Zookeeper来注册服务等等; +* 4、如果是带会话(状态)的RPC调用,还需要有会话和状态保持的功能; + +### RPC使用了哪些关键技术? + +* 1、动态代理 + + 生成Client Stub(客户端存根)和Server Stub(服务端存根)的时候需要用到Java动态代理技术,可以使用JDK提供的原生的动态代理机制,也可以使用开源的:CGLib代理,Javassist字节码生成技术。 + +* 2、序列化和反序列化 + + 在网络中,所有的数据都将会被转化为字节进行传送,所以为了能够使参数对象在网络中进行传输,需要对这些参数进行序列化和反序列化操作。 + + * 序列化:把对象转换为字节序列的过程称为对象的序列化,也就是编码的过程。反序列化:把字节序列恢复为对象的过程称为对象的反序列化,也就是解码的过程。 目前比较高效的开源序列化框架:如Kryo、FastJson和Protobuf等。 + * 反序列化:把字节序列恢复为对象的过程称为对象的反序列化,也就是解码的过程。 目前比较高效的开源序列化框架:如Kryo、FastJson和Protobuf等。 +* 3、NIO通信 + + 出于并发性能的考虑,传统的阻塞式 IO 显然不太合适,因此我们需要异步的 IO,即 NIO。Java 提供了 NIO 的解决方案,Java 7 也提供了更优秀的 NIO.2 支持。可以选择Netty或者MINA来解决NIO数据传输的问题。 + +* 4、服务注册中心 + + 可选:Redis、Zookeeper、Consul 、Etcd。一般使用ZooKeeper提供服务注册与发现功能,解决单点故障以及分布式部署的问题(注册中心)。 + +### 主流RPC框架有哪些 + +* 1、RMI + + 利用java.rmi包实现,基于Java远程方法协议(Java Remote Method Protocol) 和java的原生序列化。 + +* 2、Hessian + + 是一个轻量级的remoting onhttp工具,使用简单的方法提供了RMI的功能。 基于HTTP协议,采用二进制编解码。 + +* 3、protobuf-rpc-pro + + 是一个Java类库,提供了基于 Google 的 Protocol Buffers 协议的远程方法调用的框架。基于 Netty 底层的 NIO 技术。支持 TCP 重用/ keep-alive、SSL加密、RPC 调用取消操作、嵌入式日志等功能。 + +* 4、Thrift + + 是一种可伸缩的跨语言服务的软件框架。它拥有功能强大的代码生成引擎,无缝地支持C + +,C#,Java,Python和PHP和Ruby。thrift允许你定义一个描述文件,描述数据类型和服务接口。依据该文件,编译器方便地生成RPC客户端和服务器通信代码。 + + 最初由facebook开发用做系统内个语言之间的RPC通信,2007年由facebook贡献到apache基金 ,现在是apache下的opensource之一 。支持多种语言之间的RPC方式的通信:php语言client可以构造一个对象,调用相应的服务方法来调用java语言的服务,跨越语言的C/S RPC调用。底层通讯基于SOCKET。 + +* 5、Avro + + 出自Hadoop之父Doug Cutting, 在Thrift已经相当流行的情况下推出Avro的目标不仅是提供一套类似Thrift的通讯中间件,更是要建立一个新的,标准性的云计算的数据交换和存储的Protocol。支持HTTP,TCP两种协议。 + +* 6、Dubbo + + Dubbo是 阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和 Spring框架无缝集成。 + +### RPC的实现原理架构图 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/1717841c300aefca~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。 + +比如说,A服务器想调用B服务器上的一个方法: + +* 1、建立通信 + + 首先要解决通讯的问题:即A机器想要调用B机器,首先得建立起通信连接。 + + 主要是通过在客户端和服务器之间建立TCP连接,远程过程调用的所有交换的数据都在这个连接里传输。连接可以是按需连接,调用结束后就断掉,也可以是长连接,多个远程过程调用共享同一个连接。 + + 通常这个连接可以是按需连接(需要调用的时候就先建立连接,调用结束后就立马断掉),也可以是长连接(客户端和服务器建立起连接之后保持长期持有,不管此时有无数据包的发送,可以配合心跳检测机制定期检测建立的连接是否存活有效),多个远程过程调用共享同一个连接。 + +* 2、服务寻址 + + 要解决寻址的问题,也就是说,A服务器上的应用怎么告诉底层的RPC框架,如何连接到B服务器(如主机或IP地址)以及特定的端口,方法的名称名称是什么。 + + 通常情况下我们需要提供B机器(主机名或IP地址)以及特定的端口,然后指定调用的方法或者函数的名称以及入参出参等信息,这样才能完成服务的一个调用。 + + 可靠的寻址方式(主要是提供服务的发现)是RPC的实现基石,比如可以采用Redis或者Zookeeper来注册服务等等。 + + * 2.1、从服务提供者的角度看: + + 当服务提供者启动的时候,需要将自己提供的服务注册到指定的注册中心,以便服务消费者能够通过服务注册中心进行查找; + + 当服务提供者由于各种原因致使提供的服务停止时,需要向注册中心注销停止的服务; + + 服务的提供者需要定期向服务注册中心发送心跳检测,服务注册中心如果一段时间未收到来自服务提供者的心跳后,认为该服务提供者已经停止服务,则将该服务从注册中心上去掉。 + + * 2.2、从调用者的角度看: + + 服务的调用者启动的时候根据自己订阅的服务向服务注册中心查找服务提供者的地址等信息; + + 当服务调用者消费的服务上线或者下线的时候,注册中心会告知该服务的调用者; + + 服务调用者下线的时候,则取消订阅。 + +* 3、网络传输 + + * 3.1、序列化 + + 当A机器上的应用发起一个RPC调用时,调用方法和其入参等信息需要通过底层的网络协议如TCP传输到B机器,由于网络协议是基于二进制的,所有我们传输的参数数据都需要先进行序列化(Serialize)或者编组(marshal)成二进制的形式才能在网络中进行传输。然后通过寻址操作和网络传输将序列化或者编组之后的二进制数据发送给B机器。 + + * 3.2、反序列化 + + 当B机器接收到A机器的应用发来的请求之后,又需要对接收到的参数等信息进行反序列化操作(序列化的逆操作),即将二进制信息恢复为内存中的表达方式,然后再找到对应的方法(寻址的一部分)进行本地调用(一般是通过生成代理Proxy去调用, 通常会有JDK动态代理、CGLIB动态代理、Javassist生成字节码技术等),之后得到调用的返回值。 + +* 4、服务调用 + + B机器进行本地调用(通过代理Proxy和反射调用)之后得到了返回值,此时还需要再把返回值发送回A机器,同样也需要经过序列化操作,然后再经过网络传输将二进制数据发送回A机器,而当A机器接收到这些返回值之后,则再次进行反序列化操作,恢复为内存中的表达方式,最后再交给A机器上的应用进行相关处理(一般是业务逻辑处理操作)。 + +`通常,经过以上四个步骤之后,一次完整的RPC调用算是完成了,另外可能因为网络抖动等原因需要重试等。` + +作者:小杰要吃蛋 +链接:https://juejin.cn/post/6844904127076499463 +来源:稀土掘金 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 \ No newline at end of file diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/JavaIO.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/JavaIO.md" new file mode 100644 index 0000000..d28772b --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/JavaIO.md" @@ -0,0 +1,949 @@ + + +## BIO、NIO、AIO、Netty + +#### 什么是IO + +* Java中I/O是以流为基础进行数据的输入输出的,所有数据被串行化(所谓串行化就是数据要按顺序进行输入输出)写入输出流。简单来说就是java通过io流方式和外部设备进行交互。 + +* 在Java类库中,IO部分的内容是很庞大的,因为它涉及的领域很广泛:标准输入输出,文件的操作,**网络上的数据传输流**,字符串流,对象流等等等。 + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2b746125c6~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +* 比如程序从服务器上下载图片,就是通过流的方式从网络上以流的方式到程序中,在到硬盘中 + +#### 在了解不同的IO之前先了解:同步与异步,阻塞与非阻塞的区别 + +* 同步,一个任务的完成之前不能做其他操作,必须等待(等于在打电话) +* 异步,一个任务的完成之前,可以进行其他操作(等于在聊QQ) +* 阻塞,是相对于CPU来说的, 挂起当前线程,不能做其他操作只能等待 +* 非阻塞,,无须挂起当前线程,可以去执行其他操作 + +#### 什么是BIO + +* BIO:同步并阻塞,服务器实现一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,没处理完之前此线程不能做其他操作(如果是单线程的情况下,我传输的文件很大呢?),当然可以通过线程池机制改善。BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。 + +#### 什么是NIO + +* NIO:同步非阻塞,服务器实现一个连接一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4之后开始支持。 + +#### 什么是AIO + +* AIO:异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由操作系统先完成了再通知服务器应用去启动线程进行处理,AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用操作系统参与并发操作,编程比较复杂,JDK1.7之后开始支持。. + +* AIO属于NIO包中的类实现,其实IO主要分为BIO和NIO,AIO只是附加品,解决IO不能异步的实现 + +* 在以前很少有Linux系统支持AIO,Windows的IOCP就是该AIO模型。但是现在的服务器一般都是支持AIO操作 + +#### 什么Netty + +* Netty是由JBOSS提供的一个Java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。 + +* Netty 是一个基于NIO的客户、服务器端编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户,服务端应用。Netty相当简化和流线化了网络应用的编程开发过程,例如,TCP和UDP的socket服务开发。 + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2b74fff5c8~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)Netty是由NIO演进而来,使用过NIO编程的用户就知道NIO编程非常繁重,Netty是能够能跟好的使用NIO + +#### BIO和NIO、AIO的区别 + +* BIO是阻塞的,NIO是非阻塞的. +* BIO是面向流的,只能单向读写,NIO是面向缓冲的, 可以双向读写 +* 使用BIO做Socket连接时,由于单向读写,当没有数据时,会挂起当前线程,阻塞等待,为防止影响其它连接,,需要为每个连接新建线程处理.,然而系统资源是有限的,,不能过多的新建线程,线程过多带来线程上下文的切换,从来带来更大的性能损耗,因此需要使用NIO进行BIO多路复用,使用一个线程来监听所有Socket连接,使用本线程或者其他线程处理连接 +* AIO是非阻塞 以异步方式发起 I/O 操作。当 I/O 操作进行时可以去做其他操作,由操作系统内核空间提醒IO操作已完成(不懂的可以往下看) + +#### IO流的分类 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2b771fca9c~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)**按照读写的单位大小来分:** + +* `字符流`:以字符为单位,每次次读入或读出是16位数据。其只能读取字符类型数据。 (Java代码接收数据为一般为`char数组,也可以是别的`) +* 字节流:以字节为单位,每次次读入或读出是8位数据。可以读任何类型数据,图片、文件、音乐视频等。 (Java代码接收数据只能为`byte数组`) + +**按照实际IO操作来分:** + +* 输出流:从内存读出到文件。只能进行写操作。 +* 输入流:从文件读入到内存。只能进行读操作。 +* **注意**:输出流可以帮助我们创建文件,而输入流不会。 + +**按照读写时是否直接与硬盘,内存等节点连接分:** + +* 节点流:直接与数据源相连,读入或读出。 +* 处理流:也叫包装流,是对一个对于已存在的流的连接进行封装,通过所封装的流的功能调用实现数据读写。如添加个Buffering缓冲区。(意思就是有个缓存区,等于软件和mysql中的redis) +* **注意**:为什么要有处理流?主要作用是在读入或写出时,对数据进行缓存,以减少I/O的次数,以便下次更好更快的读写文件,才有了处理流。 + +#### 什么是内核空间 + +* 我们的应用程序是不能直接访问硬盘的,我们程序没有权限直接访问,但是操作系统(Windows、Linux......)会给我们一部分权限较高的内存空间,他叫内核空间,和我们的实际硬盘空间是有区别的 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2b7790530d~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### 五种IO模型 + +* **注意:我这里的用户空间就是应用程序空间** + +##### 1.阻塞BIO(blocking I/O) + +* A拿着一支鱼竿在河边钓鱼,并且一直在鱼竿前等,在等的时候不做其他的事情,十分专心。只有鱼上钩的时,才结束掉等的动作,把鱼钓上来。 + +* 在内核将数据准备好之前,系统调用会一直等待所有的套接字,默认的是阻塞方式。 + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2b82fb2f64~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +##### 2.非阻塞NIO(noblocking I/O) + +* B也在河边钓鱼,但是B不想将自己的所有时间都花费在钓鱼上,在等鱼上钩这个时间段中,B也在做其他的事情(一会看看书,一会读读报纸,一会又去看其他人的钓鱼等),但B在做这些事情的时候,每隔一个固定的时间检查鱼是否上钩。一旦检查到有鱼上钩,就停下手中的事情,把鱼钓上来。 **B在检查鱼竿是否有鱼,是一个轮询的过程。** + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2b830f1cd3~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +##### 3.异步AIO(asynchronous I/O) + +* C也想钓鱼,但C有事情,于是他雇来了D、E、F,让他们帮他等待鱼上钩,一旦有鱼上钩,就打电话给C,C就会将鱼钓上去。![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2ba0e18c2d~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)当应用程序请求数据时,内核一方面去取数据报内容返回,另一方面将程序控制权还给应用进程,应用进程继续处理其他事情,是一种非阻塞的状态。 + +##### 4.信号驱动IO(signal blocking I/O) + +* G也在河边钓鱼,但与A、B、C不同的是,G比较聪明,他给鱼竿上挂一个铃铛,当有鱼上钩的时候,这个铃铛就会被碰响,G就会将鱼钓上来。![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2ba21e5d95~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)信号驱动IO模型,应用进程告诉内核:当数据报准备好的时候,给我发送一个信号,对SIGIO信号进行捕捉,并且调用我的信号处理函数来获取数据报。 + +##### 5.IO多路转接(I/O multiplexing) + +* H同样也在河边钓鱼,但是H生活水平比较好,H拿了很多的鱼竿,一次性有很多鱼竿在等,H不断的查看每个鱼竿是否有鱼上钩。增加了效率,减少了等待的时间。 + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2ba9450627~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)IO多路转接是多了一个select函数,select函数有一个参数是文件描述符集合,对这些文件描述符进行循环监听,当某个文件描述符就绪时,就对这个文件描述符进行处理。 +* IO多路转接是属于阻塞IO,但可以对多个文件描述符进行阻塞监听,所以效率较阻塞IO的高。 + +#### 什么是比特(Bit),什么是字节(Byte),什么是字符(Char),它们长度是多少,各有什么区别 + +* Bit最小的二进制单位 ,是计算机的操作部分取值0或者1 +* Byte是计算机中存储数据的单元,是一个8位的二进制数,(计算机内部,一个字节可表示一个英文字母,两个字节可表示一个汉字。) `取值(-128-127)` +* Char是用户的可读写的最小单位,他只是抽象意义上的一个符号。如‘5’,‘中’,‘¥’ 等等等等。在java里面由16位bit组成Char 取值`(0-65535)` +* Bit 是最小单位 计算机他只能认识0或者1 +* Byte是8个字节 是给计算机看的 +* 字符 是看到的东西 一个字符=二个字节 + +#### 什么叫对象序列化,什么是反序列化,实现对象序列化需要做哪些工作 + +* 对象序列化,将对象以二进制的形式保存在硬盘上 +* 反序列化;将二进制的文件转化为对象读取 +* 实现serializable接口,不想让字段放在硬盘上就加transient + +#### 在实现序列化接口是时候一般要生成一个serialVersionUID字段,它叫做什么,一般有什么用 + +* 如果用户没有自己声明一个serialVersionUID,接口会默认生成一个serialVersionUID +* 但是强烈建议用户自定义一个serialVersionUID,因为默认的serialVersinUID对于class的细节非常敏感,反序列化时可能会导致InvalidClassException这个异常。 +* (比如说先进行序列化,然后在反序列化之前修改了类,那么就会报错。因为修改了类,对应的SerialversionUID也变化了,而序列化和反序列化就是通过对比其SerialversionUID来进行的,一旦SerialversionUID不匹配,反序列化就无法成功。 + +#### 怎么生成SerialversionUID + +* 可序列化类可以通过声明名为 "serialVersionUID" 的字段(该字段**必须是静态 (static)、最终 (final) 的 long 型字段**)显式声明其自己的 serialVersionUID + +* 两种显示的生成方式(当你一个类实现了Serializable接口,如果没有显示的定义serialVersionUID,Eclipse会提供这个提示功能告诉你去定义 。在Eclipse中点击类中warning的图标一下,Eclipse就会自动给定两种生成的方式。 + +#### BufferedReader属于哪种流,它主要是用来做什么的,它里面有那些经典的方法 + +* 属于处理流中的缓冲流,可以将读取的内容存在内存里面,有readLine()方法 + +#### Java中流类的超类主要有那些? + +* 超类代表顶端的父类(都是抽象类) + +* java.io.InputStream + +* java.io.OutputStream + +* java.io.Reader + +* java.io.Writer + +#### 为什么图片、视频、音乐、文件等 都是要字节流来读取 + +* 这个很基础,你看看你电脑文件的属性就好了,CPU规定了计算机存储文件都是按字节算的 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2bae24bd8b~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### IO的常用类和方法,以及如何使用 + +[注意,如果懂IO的普通文件读写操作可以直接点击此处跳过,直接看网络操作IO编程,那个才是重点,点击即会跳转](#Mark "#Mark") + +前面讲了那么多废话,现在我们开始进入主题,后面很长,从开始的文件操作到后面的**网络IO操作**都会有例子: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2bb4bd288c~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)[注意,如果懂IO的普通文件读写操作可以直接点击此处跳过,直接看网络操作IO编程,那个才是重点,点击即会跳转](#Mark "#Mark") +#### IO基本操作讲解 + +* `这里的基本操作就是普通的读取操作,如果想要跟深入的了解不同的IO开发场景必须先了解IO的基本操作` + +##### 1 按`字符`流读取文件 + +###### 1.1 按字符流的·节点流方式读取 + +* 如果我们要取的数据基本单位是字符,那么用(**字符流**)这种方法读取文件就比较适合。比如:读取test.txt文件 + +**注释:** + +* `字符流`:以字符为单位,每次次读入或读出是16位数据。其只能读取字符类型数据。 (Java代码接收数据为一般为`char数组,也可以是别的`) + +* 字节流:以字节为单位,每次次读入或读出是8位数据。可以读任何类型数据,图片、文件、音乐视频等。 (Java代码接收数据只能为`byte数组`) + +* **FileReader 类:**(字符输入流) 注意:new FileReader("D:\test.txt");//文件必须存在 + +```java +package com.test.io; + +import java.io.FileReader; +import java.io.IOException; + +public class TestFileReader { + public static void main(String[] args) throws IOException { + int num=0; + //字符流接收使用的char数组 + char[] buf=new char[1024]; + //字符流、节点流打开文件类 + FileReader fr = new FileReader("D:\\test.txt");//文件必须存在 + //FileReader.read():取出字符存到buf数组中,如果读取为-1代表为空即结束读取。 + //FileReader.read():读取的是一个字符,但是java虚拟机会自动将char类型数据转换为int数据, + //如果你读取的是字符A,java虚拟机会自动将其转换成97,如果你想看到字符可以在返回的字符数前加(char)强制转换如 + while((num=fr.read(buf))!=-1) { } + //检测一下是否取到相应的数据 + for(int i=0;i 0) { + System.out.println(new String(buffer, 0, len)); + } + //向客户端写数据 + out = socket.getOutputStream(); + out.write("hello!".getBytes()); + } + } catch (IOException e) { + e.printStackTrace(); + } + } +} +``` + +* TCP协议Socket使用BIO进行通信:客户端(第二执行) + +```java +package com.test.io; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; +import java.util.Scanner; + +//TCP协议Socket使用BIO进行通信:客户端 +public class Client01 { + public static void main(String[] args) throws IOException { + //创建套接字对象socket并封装ip与port + Socket socket = new Socket("127.0.0.1", 8000); + //根据创建的socket对象获得一个输出流 + //基于字节流 + OutputStream outputStream = socket.getOutputStream(); + //控制台输入以IO的形式发送到服务器 + System.out.println("TCP连接成功 \n请输入:"); + String str = new Scanner(System.in).nextLine(); + byte[] car = str.getBytes(); + outputStream.write(car); + System.out.println("TCP协议的Socket发送成功"); + //刷新缓冲区 + outputStream.flush(); + //关闭连接 + socket.close(); + } +} +``` + +* TCP协议Socket使用BIO进行通信:客户端(第三执行) + +```java +package com.test.io; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; +import java.util.Scanner; + +//TCP协议Socket:客户端 +public class Client02 { + public static void main(String[] args) throws IOException { + //创建套接字对象socket并封装ip与port + Socket socket = new Socket("127.0.0.1", 8000); + //根据创建的socket对象获得一个输出流 + //基于字节流 + OutputStream outputStream = socket.getOutputStream(); + //控制台输入以IO的形式发送到服务器 + System.out.println("TCP连接成功 \n请输入:"); + String str = new Scanner(System.in).nextLine(); + byte[] car = str.getBytes(); + outputStream.write(car); + System.out.println("TCP协议的Socket发送成功"); + //刷新缓冲区 + outputStream.flush(); + //关闭连接 + socket.close(); + } +} +``` + +`为了解决堵塞问题,可以使用多线程,请看下面` + +##### 2 多线程解决BIO编程会出现的问题 + +**这时有人就会说,我多线程不就解决了吗?** + +* 使用多线程是可以解决堵塞等待时间很长的问题,因为他可以充分发挥CPU +* 然而系统资源是有限的,不能过多的新建线程,线程过多带来线程上下文的切换,从来带来更大的性能损耗![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2dd68f7c10~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +**万一请求越来越多,线程越来越多那我CPU不就炸了?** + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2d696381b8~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)**多线程BIO代码示例:** + +* 四个客户端,这次我多复制了俩个一样客户端类![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2d7a9bf162~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)`先启动服务端,在启动所有客户端,测试`,发现连接成功(`后面有代码`)![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2d94da3441~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)在所有客户端输入消息(`Client01、Client02这些是我在客户端输入的消息`):发现没有问题![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2d94251e4b~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +**多线程BIO通信代码:** + +* `服务端的代码,客户端的代码还是上面之前的代码` + +```java +package com.test.io; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; + +//TCP协议Socket使用多线程BIO进行通行:服务端 +public class BIOThreadService { + public static void main(String[] args) { + try { + ServerSocket server = new ServerSocket(8000); + System.out.println("服务端启动成功,监听端口为8000,等待客户端连接... "); + while (true) { + Socket socket = server.accept();//等待客户连接 + System.out.println("客户连接成功,客户信息为:" + socket.getRemoteSocketAddress()); + //针对每个连接创建一个线程, 去处理I0操作 + //创建多线程创建开始 + Thread thread = new Thread(new Runnable() { + public void run() { + try { + InputStream in = socket.getInputStream(); + byte[] buffer = new byte[1024]; + int len = 0; + //读取客户端的数据 + while ((len = in.read(buffer)) > 0) { + System.out.println(new String(buffer, 0, len)); + } + //向客户端写数据 + OutputStream out = socket.getOutputStream(); + out.write("hello".getBytes()); + } catch (IOException e) { + e.printStackTrace(); + } + } + }); + thread.start(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } +} +``` + +`为了解决线程太多,这时又来了,线程池` + +##### 3 线程池解决多线程BIO编程会出现的问题 + +**这时有人就会说,我TM用线程池?** + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2dab69e263~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 线程池固然可以解决这个问题,万一需求量还不够还要扩大线程池。当是这是我们自己靠着自己的思想完成的IO操作,Socket 上来了就去创建线程去抢夺CPU资源,MD,线程都TM做IO去了,CPU也不舒服呀 + +* 这时呢:Jdk官方坐不住了,兄弟BIO的问题交给我,我来给你解决:`NIO的诞生` + +**线程池BIO代码示例:** + +* 四个客户端![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2da7389a42~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)`先启动服务端,在启动所有客户端,测试`,(`后面有代码`)![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2dc163e25a~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)在所有客户端输入消息(`Client01、Client02这些是我在客户端输入的消息`):发现没有问题![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2dc2080d3a~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +**线程池BIO通信代码:** + +* `服务端的代码,客户端的代码还是上面的代码` + +```java +package com.test.io; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +//TCP协议Socket使用线程池BIO进行通行:服务端 +public class BIOThreadPoolService { + public static void main(String[] args) { + //创建线程池 + ExecutorService executorService = Executors.newFixedThreadPool(30); + try { + ServerSocket server = new ServerSocket(8000); + System.out.println("服务端启动成功,监听端口为8000,等待客户端连接..."); + while (true) { + Socket socket = server.accept();//等待客户连接 + System.out.println("客户连接成功,客户信息为:" + socket.getRemoteSocketAddress()); + //使用线程池中的线程去执行每个对应的任务 + executorService.execute(new Thread(new Runnable() { + public void run() { + try { + InputStream in = socket.getInputStream(); + byte[] buffer = new byte[1024]; + int len = 0; + //读取客户端的数据 + while ((len = in.read(buffer)) > 0) { + System.out.println(new String(buffer, 0, len)); + } + //向客户端写数据 + OutputStream out = socket.getOutputStream(); + out.write("hello".getBytes()); + } catch (IOException e) { + e.printStackTrace(); + } + } + }) + ); + } + } catch (IOException e) { + e.printStackTrace(); + } + } +} +``` + +##### 4 使用NIO实现网络通信 + +* NIO是JDK1.4提供的操作,他的流还是流,没有改变,服务器实现的还是一个连接一个线程,当是:`客户端发送的连接请求都会注册到多路复用器上`,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4之后开始支持。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2dc3e76709~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)`看不懂介绍可以认真看看代码实例,其实不难` +###### 什么是通道(Channel) + +* Channel是一个对象,可以通过它读取和写入数据。 通常我们都是将数据写入包含一个或者多个字节的缓冲区,然后再将缓存区的数据写入到通道中,将数据从通道读入缓冲区,再从缓冲区获取数据。 + +* Channel 类似于原I/O中的流(Stream),但有所区别: + + * 流是单向的,通道是双向的,可读可写。 + * 流读写是阻塞的,通道可以异步读写。 + +###### 什么是选择器(Selector) + +* Selector可以称他为通道的集合,每次客户端来了之后我们会把Channel注册到Selector中并且我们给他一个状态,在用死循环来环判断(`判断是否做完某个操作,完成某个操作后改变不一样的状态`)状态是否发生变化,知道IO操作完成后在退出死循环 + +###### 什么是Buffer(缓冲区) + +* Buffer 是一个缓冲数据的对象, 它包含一些要写入或者刚读出的数据。 + +* 在普通的面向流的 I/O 中,一般将数据直接写入或直接读到 Stream 对象中。当是有了Buffer(缓冲区)后,数据第一步到达的是Buffer(缓冲区)中 + +* 缓冲区实质上是一个数组(`底层完全是数组实现的,感兴趣可以去看一下`)。通常它是一个字节数组,内部维护几个状态变量,可以实现在同一块缓冲区上反复读写(不用清空数据再写)。 + +###### 代码实例: + +* 目录结构![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2dd02ee59c~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +* 运行示例,先运行服务端,在运行所有客户端控制台输入消息就好了。:`我这客户端和服务端代码有些修该变,后面有代码`![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2de8321be5~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +* `服务端示例,先运行,想要搞定NIO请认真看代码示例,真的很清楚` + +```java +package com.test.io; + +import com.lijie.iob.RequestHandler; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.Iterator; +import java.util.Set; + +public class NIOServer { + public static void main(String[] args) throws IOException { + //111111111 + //Service端的Channel,监听端口的 + ServerSocketChannel serverChannel = ServerSocketChannel.open(); + //设置为非阻塞 + serverChannel.configureBlocking(false); + //nio的api规定这样赋值端口 + serverChannel.bind(new InetSocketAddress(8000)); + //显示Channel是否已经启动成功,包括绑定在哪个地址上 + System.out.println("服务端启动成功,监听端口为8000,等待客户端连接..."+ serverChannel.getLocalAddress()); + + //22222222 + //声明selector选择器 + Selector selector = Selector.open(); + //这句话的含义,是把selector注册到Channel上面, + //每个客户端来了之后,就把客户端注册到Selector选择器上,默认状态是Accepted + serverChannel.register(selector, SelectionKey.OP_ACCEPT); + + //33333333 + //创建buffer缓冲区,声明大小是1024,底层使用数组来实现的 + ByteBuffer buffer = ByteBuffer.allocate(1024); + RequestHandler requestHandler = new RequestHandler(); + + //444444444 + //轮询,服务端不断轮询,等待客户端的连接 + //如果有客户端轮询上来就取出对应的Channel,没有就一直轮询 + while (true) { + int select = selector.select(); + if (select == 0) { + continue; + } + //有可能有很多,使用Set保存Channel + Set selectionKeys = selector.selectedKeys(); + Iterator iterator = selectionKeys.iterator(); + while (iterator.hasNext()) { + //使用SelectionKey来获取连接了客户端和服务端的Channel + SelectionKey key = iterator.next(); + //判断SelectionKey中的Channel状态如何,如果是OP_ACCEPT就进入 + if (key.isAcceptable()) { + //从判断SelectionKey中取出Channel + ServerSocketChannel channel = (ServerSocketChannel) key.channel(); + //拿到对应客户端的Channel + SocketChannel clientChannel = channel.accept(); + //把客户端的Channel打印出来 + System.out.println("客户端通道信息打印:" + clientChannel.getRemoteAddress()); + //设置客户端的Channel设置为非阻塞 + clientChannel.configureBlocking(false); + //操作完了改变SelectionKey中的Channel的状态OP_READ + clientChannel.register(selector, SelectionKey.OP_READ); + } + //到此轮训到的时候,发现状态是read,开始进行数据交互 + if (key.isReadable()) { + //以buffer作为数据桥梁 + SocketChannel channel = (SocketChannel) key.channel(); + //数据要想读要先写,必须先读取到buffer里面进行操作 + channel.read(buffer); + //进行读取 + String request = new String(buffer.array()).trim(); + buffer.clear(); + //进行打印buffer中的数据 + System.out.println(String.format("客户端发来的消息: %s : %s", channel.getRemoteAddress(), request)); + //要返回数据的话也要先返回buffer里面进行返回 + String response = requestHandler.handle(request); + //然后返回出去 + channel.write(ByteBuffer.wrap(response.getBytes())); + } + iterator.remove(); + } + } + } +} +``` + +* 客户端示例:(`我这用的不是之前的了,有修改`)运行起来客户端控制台输入消息就好了。 `要模拟测试,请复制粘贴改一下,修改客户端的类名就行了,四个客户端代码一样的`,![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2dee3a5661~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +```java +package com.test.io; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; +import java.util.Scanner; + +//TCP协议Socket:客户端 +public class Client01 { + public static void main(String[] args) throws IOException { + //创建套接字对象socket并封装ip与port + Socket socket = new Socket("127.0.0.1", 8000); + //根据创建的socket对象获得一个输出流 + OutputStream outputStream = socket.getOutputStream(); + //控制台输入以IO的形式发送到服务器 + System.out.println("TCP连接成功 \n请输入:"); + while(true){ + byte[] car = new Scanner(System.in).nextLine().getBytes(); + outputStream.write(car); + System.out.println("TCP协议的Socket发送成功"); + //刷新缓冲区 + outputStream.flush(); + } + } +} +``` + +##### 5 使用Netty实现网络通信 + +* Netty是由JBOSS提供的一个Java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。 + +* Netty 是一个基于NIO的客户、服务器端编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户,服务端应用。Netty相当简化和流线化了网络应用的编程开发过程,例如,TCP和UDP的Socket服务开发。 + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2e0284e9ca~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)Netty是由NIO演进而来,使用过NIO编程的用户就知道NIO编程非常繁重,Netty是能够能跟好的使用NIO +* Netty的原里就是NIO,他是基于NIO的一个完美的封装,并且优化了NIO,使用他非常方便,简单快捷 + +* 我直接上代码: + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2e0e2ef4ca~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +* 1、先添加依赖: + + + io.netty + netty-all + 4.1.16.Final + +``` + +* 2、NettyServer 模板,看起来代码那么多,`其实只需要添加一行消息就好了` +* `请认真看中间的代码` + +```java +package com.lijie.iob; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.*; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.serialization.ClassResolvers; +import io.netty.handler.codec.serialization.ObjectEncoder; +import io.netty.handler.codec.string.StringDecoder; + +public class NettyServer { + public static void main(String[] args) throws InterruptedException { + EventLoopGroup bossGroup = new NioEventLoopGroup(); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel socketChannel) throws Exception { + ChannelPipeline pipeline = socketChannel.pipeline(); + pipeline.addLast(new StringDecoder()); + pipeline.addLast("encoder", new ObjectEncoder()); + pipeline.addLast(" decoder", new io.netty.handler.codec.serialization.ObjectDecoder(Integer.MAX_VALUE, ClassResolvers.cacheDisabled(null))); + + //重点,其他的都是复用的 + //这是真正的I0的业务代码,把他封装成一个个的个Hand1e类就行了 + //把他当成 SpringMVC的Controller + pipeline.addLast(new NettyServerHandler()); + + } + }) + .option(ChannelOption.SO_BACKLOG, 128) + .childOption(ChannelOption.SO_KEEPALIVE, true); + ChannelFuture f = b.bind(8000).sync(); + System.out.println("服务端启动成功,端口号为:" + 8000); + f.channel().closeFuture().sync(); + } finally { + workerGroup.shutdownGracefully(); + bossGroup.shutdownGracefully(); + } + } +} +``` + +* 3、需要做的IO操作,重点是继承ChannelInboundHandlerAdapter类就好了 + +```java +package com.lijie.iob; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; + +public class NettyServerHandler extends ChannelInboundHandlerAdapter { + RequestHandler requestHandler = new RequestHandler(); + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + Channel channel = ctx.channel(); + System.out.println(String.format("客户端信息: %s", channel.remoteAddress())); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + Channel channel = ctx.channel(); + String request = (String) msg; + System.out.println(String.format("客户端发送的消息 %s : %s", channel.remoteAddress(), request)); + String response = requestHandler.handle(request); + ctx.write(response); + ctx.flush(); + } +} +``` + +* 4 客户的代码还是之前NIO的代码,我在复制下来一下吧 + +```java +package com.test.io; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; +import java.util.Scanner; + +//TCP协议Socket:客户端 +public class Client01 { + public static void main(String[] args) throws IOException { + //创建套接字对象socket并封装ip与port + Socket socket = new Socket("127.0.0.1", 8000); + //根据创建的socket对象获得一个输出流 + OutputStream outputStream = socket.getOutputStream(); + //控制台输入以IO的形式发送到服务器 + System.out.println("TCP连接成功 \n请输入:"); + while(true){ + byte[] car = new Scanner(System.in).nextLine().getBytes(); + outputStream.write(car); + System.out.println("TCP协议的Socket发送成功"); + //刷新缓冲区 + outputStream.flush(); + } + } +} +``` + +* 运行测试,还是之前那样,启动服务端,在启动所有客户端控制台输入就好了:![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172a2e4e740749~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +作者:小杰要吃蛋 +链接:https://juejin.cn/post/6844904125700784136 +来源:稀土掘金 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 \ No newline at end of file diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\345\237\272\347\241\200.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\345\237\272\347\241\200.md" new file mode 100644 index 0000000..e4df817 --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\345\237\272\347\241\200.md" @@ -0,0 +1,1313 @@ + + +## Java概述 + +### 何为编程 + +* 编程就是让计算机为解决某个问题而使用某种程序设计语言编写程序代码,并最终得到结果的过程。 + +* 为了使计算机能够理解人的意图,人类就必须要将需解决的问题的思路、方法、和手段通过计算机能够理解的形式告诉计算机,使得计算机能够根据人的指令一步一步去工作,完成某种特定的任务。这种人和计算机之间交流的过程就是编程。 + +### 什么是Java + +* Java是一门面向对象编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承、指针等概念,因此Java语言具有功能强大和简单易用两个特征。Java语言作为静态面向对象编程语言的代表,极好地实现了面向对象理论,允许程序员以优雅的思维方式进行复杂的编程 。 + +### jdk1.5之后的三大版本 + +* Java SE(J2SE,Java 2 Platform Standard Edition,标准版) + Java SE 以前称为 J2SE。它允许开发和部署在桌面、服务器、嵌入式环境和实时环境中使用的 Java 应用程序。Java SE 包含了支持 Java Web 服务开发的类,并为Java EE和Java ME提供基础。 +* Java EE(J2EE,Java 2 Platform Enterprise Edition,企业版) + Java EE 以前称为 J2EE。企业版本帮助开发和部署可移植、健壮、可伸缩且安全的服务器端Java 应用程序。Java EE 是在 Java SE 的基础上构建的,它提供 Web 服务、组件模型、管理和通信 API,可以用来实现企业级的面向服务体系结构(service-oriented architecture,SOA)和 Web2.0应用程序。2018年2月,Eclipse 宣布正式将 JavaEE 更名为 JakartaEE +* Java ME(J2ME,Java 2 Platform Micro Edition,微型版) + Java ME 以前称为 J2ME。Java ME 为在移动设备和嵌入式设备(比如手机、PDA、电视机顶盒和打印机)上运行的应用程序提供一个健壮且灵活的环境。Java ME 包括灵活的用户界面、健壮的安全模型、许多内置的网络协议以及对可以动态下载的连网和离线应用程序的丰富支持。基于 Java ME 规范的应用程序只需编写一次,就可以用于许多设备,而且可以利用每个设备的本机功能。 + +### 3 Jdk和Jre和JVM的区别 + +`看Java官方的图片,Jdk中包括了Jre,Jre中包括了JVM` + +* JDK :Jdk还包括了一些Jre之外的东西 ,就是这些东西帮我们编译Java代码的, 还有就是监控Jvm的一些工具 Java Development Kit是提供给Java开发人员使用的,其中包含了Java的开发工具,也包括了JRE。所以安装了JDK,就无需再单独安装JRE了。其中的开发工具:编译工具(javac.exe),打包工具(jar.exe)等 + +* JRE :Jre大部分都是 C 和 C++ 语言编写的,他是我们在编译java时所需要的基础的类库 Java Runtime Environment包括Java虚拟机和Java程序所需的核心类库等。核心类库主要是java.lang包:包含了运行Java程序必不可少的系统类,如基本数据类型、基本数学函数、字符串处理、线程、异常处理类等,系统缺省加载这个包 + + 如果想要运行一个开发好的Java程序,计算机中只需要安装JRE即可。 + +* Jvm:在倒数第二层 由他可以在(最后一层的)各种平台上运行 Java Virtual Machine是Java虚拟机,Java程序需要运行在虚拟机上,不同的平台有自己的虚拟机,因此Java语言可以实现跨平台。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744c434318a82~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +### 什么是跨平台性?原理是什么 + +* 所谓跨平台性,是指java语言编写的程序,一次编译后,可以在多个系统平台上运行。 + +* 实现原理:Java程序是通过java虚拟机在系统平台上运行的,只要该系统可以安装相应的java虚拟机,该系统就可以运行java程序。 + +### Java语言有哪些特点 + +* 简单易学(Java语言的语法与C语言和C++语言很接近) + +* 面向对象(封装,继承,多态) + +* 平台无关性(Java虚拟机实现平台无关性) + +* 支持网络编程并且很方便(Java语言诞生本身就是为简化网络编程设计的) + +* 支持多线程(多线程机制使应用程序在同一时间并行执行多项任) + +* 健壮性(Java语言的强类型机制、异常处理、垃圾的自动收集等) + +* 安全性好 + +### 什么是字节码?采用字节码的最大好处是什么 + +* **字节码**:Java源代码经过虚拟机编译器编译后产生的文件(即扩展为.class的文件),它不面向任何特定的处理器,只面向虚拟机。 + +* **采用字节码的好处**: + + Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。 + +* **先看下java中的编译器和解释器**: + + Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚拟机理解的代码叫做字节码(即扩展为.class的文件),它不面向任何特定的处理器,只面向虚拟机。每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行,这就是上面提到的Java的特点的编译与解释并存的解释。 + + Java源代码---->编译器---->jvm可执行的Java字节码(即虚拟指令)---->jvm---->jvm中解释器----->机器可执行的二进制机器码---->程序运行。 + +### 什么是Java程序的主类?应用程序和小程序的主类有何不同? + +* 一个程序中可以有多个类,但只能有一个类是主类。在Java应用程序中,这个主类是指包含main()方法的类。而在Java小程序中,这个主类是一个继承自系统类JApplet或Applet的子类。应用程序的主类不一定要求是public类,但小程序的主类要求必须是public类。主类是Java程序执行的入口点。 + +### Java应用程序与小程序之间有那些差别? + +* 简单说应用程序是从主线程启动(也就是main()方法)。applet小程序没有main方法,主要是嵌在浏览器页面上运行(调用init()线程或者run()来启动),嵌入浏览器这点跟flash的小游戏类似。 + +### Java和C++的区别 + +`我知道很多人没学过C++,但是面试官就是没事喜欢拿咱们Java和C++比呀!没办法!!!就算没学过C++,也要记下来!` + +* 都是面向对象的语言,都支持封装、继承和多态 +* Java不提供指针来直接访问内存,程序内存更加安全 +* Java的类是单继承的,C++支持多重继承;虽然Java的类不可以多继承,但是接口可以多继承。 +* Java有自动内存管理机制,不需要程序员手动释放无用内存 + +### Oracle JDK 和 OpenJDK 的对比 + +1. Oracle JDK版本将每三年发布一次,而OpenJDK版本每三个月发布一次; + +2. OpenJDK 是一个参考模型并且是完全开源的,而Oracle JDK是OpenJDK的一个实现,并不是完全开源的; + +3. Oracle JDK 比 OpenJDK 更稳定。OpenJDK和Oracle JDK的代码几乎相同,但Oracle JDK有更多的类和一些错误修复。因此,如果您想开发企业/商业软件,我建议您选择Oracle JDK,因为它经过了彻底的测试和稳定。某些情况下,有些人提到在使用OpenJDK 可能会遇到了许多应用程序崩溃的问题,但是,只需切换到Oracle JDK就可以解决问题; + +4. 在响应性和JVM性能方面,Oracle JDK与OpenJDK相比提供了更好的性能; + +5. Oracle JDK不会为即将发布的版本提供长期支持,用户每次都必须通过更新到最新版本获得支持来获取最新版本; + +6. Oracle JDK根据二进制代码许可协议获得许可,而OpenJDK根据GPL v2许可获得许可。 + +## 基础语法 + +### 数据类型 + +#### Java有哪些数据类型 + +**定义**:Java语言是强类型语言,对于每一种数据都定义了明确的具体的数据类型,在内存中分配了不同大小的内存空间。 + +**分类** + +* 基本数据类型 + * 数值型 + * 整数类型(byte,short,int,long) + * 浮点类型(float,double) + * 字符型(char) + * 布尔型(boolean) +* 引用数据类型 + * 类(class) + * 接口(interface) + * 数组([]) + +**Java基本数据类型图** + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744c434465b69~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### switch 是否能作用在 byte 上,是否能作用在 long 上,是否能作用在 String 上 + +* 在 Java 5 以前,switch(expr)中,expr 只能是 byte、short、char、int。从 Java5 开始,Java 中引入了枚举类型,expr 也可以是 enum 类型,从 Java 7 开始,expr 还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。 + +#### 用最有效率的方法计算 2 乘以 8 + +* 2 << 3(左移 3 位相当于乘以 2 的 3 次方,右移 3 位相当于除以 2 的 3 次方)。 + +#### Math.round(11.5) 等于多少?Math.round(-11.5)等于多少 + +* Math.round(11.5)的返回值是 12,Math.round(-11.5)的返回值是-11。四舍五入的原理是在参数上加 0.5 然后进行下取整。 + +#### float f=3.4;是否正确 + +* 不正确。3.4 是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换float f =(float)3.4; 或者写成 float f =3.4F;。 + +#### short s1 = 1; s1 = s1 + 1;有错吗?short s1 = 1; s1 += 1;有错吗 + +* 对于 short s1 = 1; s1 = s1 + 1;由于 1 是 int 类型,因此 s1+1 运算结果也是 int型,需要强制转换类型才能赋值给 short 型。 + +* 而 short s1 = 1; s1 += 1;可以正确编译,因为 s1+= 1;相当于 s1 = (short(s1 + 1);其中有隐含的强制类型转换。 + +### 编码 + +#### Java语言采用何种编码方案?有何特点? + +* Java语言采用Unicode编码标准,Unicode(标准码),它为每个字符制订了一个唯一的数值,因此在任何的语言,平台,程序都可以放心的使用。 + +### 注释 + +#### 什么Java注释 + +**定义**:用于解释说明程序的文字 + +**分类** + +* 单行注释 + 格式: // 注释文字 +* 多行注释 + 格式: /* 注释文字 */ +* 文档注释 + 格式:/** 注释文字 */ + +**作用** + +* 在程序中,尤其是复杂的程序中,适当地加入注释可以增加程序的可读性,有利于程序的修改、调试和交流。注释的内容在程序编译的时候会被忽视,不会产生目标代码,注释的部分不会对程序的执行结果产生任何影响。 + +`注意事项:多行和文档注释都不能嵌套使用。` + +### 访问修饰符 + +#### 访问修饰符 public,private,protected,以及不写(默认)时的区别 + +* **定义**:Java中,可以使用访问修饰符来保护对类、变量、方法和构造方法的访问。Java 支持 4 种不同的访问权限。 + +* **分类** + + * private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类) + + * default (即缺省,什么也不写,不使用任何关键字): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。 + + * protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。 + + * public : 对所有类可见。使用对象:类、接口、变量、方法 + +**访问修饰符图** + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744c433bcfd38~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +### 运算符 + +#### &和&&的区别 + +* &运算符有两种用法:(1)按位与;(2)逻辑与。 + +* &&运算符是短路与运算。逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是true 整个表达式的值才是 true。&&之所以称为短路运算,是因为如果&&左边的表达式的值是 false,右边的表达式会被直接短路掉,不会进行运算。 + +`注意:逻辑或运算符(|)和短路或运算符(||)的差别也是如此。` + +### 关键字 + +#### Java 有没有 goto + +* goto 是 Java 中的保留字,在目前版本的 Java 中没有使用。 + +#### final 有什么用? + +`用于修饰类、属性和方法;` + +* 被final修饰的类不可以被继承 +* 被final修饰的方法不可以被重写 +* 被final修饰的变量不可以被改变,被final修饰不可变的是变量的引用,而不是引用指向的内容,引用指向的内容是可以改变的 + +#### final finally finalize区别 + +* final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表 示该变量是一个常量不能被重新赋值。 +* finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块 中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。 +* finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调 用,当我们调用System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的 最后判断。 + +#### this关键字的用法 + +* this是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。 + +* this的用法在java中大体可以分为3种: + + * 1.普通的直接引用,this相当于是指向当前对象本身。 + + * 2.形参与成员名字重名,用this来区分: + + public Person(String name, int age) { + this.name = name; + this.age = age; + } + 复制代码 + * 3.引用本类的构造函数 + + class Person{ + private String name; + private int age; + + public Person() { + } + + public Person(String name) { + this.name = name; + } + public Person(String name, int age) { + this(name); + this.age = age; + } + } + 复制代码 + +#### super关键字的用法 + +* super可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。 + +* super也有三种用法: + + * 1.普通的直接引用 + + 与this类似,super相当于是指向当前对象的父类的引用,这样就可以用super.xxx来引用父类的成员。 + + * 2.子类中的成员变量或方法与父类中的成员变量或方法同名时,用super进行区分 + + class Person{ + protected String name; + + public Person(String name) { + this.name = name; + } + + } + + class Student extends Person{ + private String name; + + public Student(String name, String name1) { + super(name); + this.name = name1; + } + + public void getInfo(){ + System.out.println(this.name); //Child + System.out.println(super.name); //Father + } + + } + + public class Test { + public static void main(String[] args) { + Student s1 = new Student("Father","Child"); + s1.getInfo(); + + } + } + 复制代码 + * 3.引用父类构造函数 + + * super(参数):调用父类中的某一个构造函数(应该为构造函数中的第一条语句)。 + * this(参数):调用本类中另一种形式的构造函数(应该为构造函数中的第一条语句)。 + +#### this与super的区别 + +* super: 它引用当前对象的直接父类中的成员(用来访问直接父类中被隐藏的父类中成员数据或函数,基类与派生类中有相同成员定义时如:super.变量名 super.成员函数据名(实参) +* this:它代表当前对象名(在程序中易产生二义性之处,应使用this来指明当前对象;如果函数的形参与类中的成员数据同名,这时需用this来指明成员变量名) +* super()和this()类似,区别是,super()在子类中调用父类的构造方法,this()在本类内调用本类的其它构造方法。 +* super()和this()均需放在构造方法内第一行。 +* 尽管可以用this调用一个构造器,但却不能调用两个。 +* this和super不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数,其它的构造函数必然也会有super语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。 +* this()和super()都指的是对象,所以,均不可以在static环境中使用。包括:static变量,static方法,static语句块。 +* 从本质上讲,this是一个指向本对象的指针, 然而super是一个Java关键字。 + +#### static存在的主要意义 + +* static的主要意义是在于创建独立于具体对象的域变量或者方法。**以致于即使没有创建对象,也能使用属性和调用方法**! + +* static关键字还有一个比较关键的作用就是 **用来形成静态代码块以优化程序性能**。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。 + +* 为什么说static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次。因此,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行。 + +#### static的独特之处 + +* 1、被static修饰的变量或者方法是独立于该类的任何对象,也就是说,这些变量和方法**不属于任何一个实例对象,而是被类的实例对象所共享**。 + +> 怎么理解 “被类的实例对象所共享” 这句话呢?就是说,一个类的静态成员,它是属于大伙的【大伙指的是这个类的多个对象实例,我们都知道一个类可以创建多个实例!】,所有的类对象共享的,不像成员变量是自个的【自个指的是这个类的单个实例对象】…我觉得我已经讲的很通俗了,你明白了咩? + +* 2、在该类被第一次加载的时候,就会去加载被static修饰的部分,而且只在类第一次使用时加载并进行初始化,注意这是第一次用就要初始化,后面根据需要是可以再次赋值的。 + +* 3、static变量值在类加载的时候分配空间,以后创建类对象的时候不会重新分配。赋值的话,是可以任意赋值的! + +* 4、被static修饰的变量或者方法是优先于对象存在的,也就是说当一个类加载完毕之后,即便没有创建对象,也可以去访问。 + +#### static应用场景 + +* 因为static是被类的实例对象所共享,因此如果**某个成员变量是被所有对象所共享的,那么这个成员变量就应该定义为静态变量**。 + +* 因此比较常见的static应用场景有: + +> 1、修饰成员变量 2、修饰成员方法 3、静态代码块 4、修饰类【只能修饰内部类也就是静态内部类】 5、静态导包 + +#### static注意事项 + +* 1、静态只能访问静态。 +* 2、非静态既可以访问非静态的,也可以访问静态的。 + +### 流程控制语句 + +#### break ,continue ,return 的区别及作用 + +* break 跳出总上一层循环,不再执行循环(结束当前的循环体) + +* continue 跳出本次循环,继续执行下次循环(结束正在执行的循环 进入下一个循环条件) + +* return 程序返回,不再执行下面的代码(结束当前的方法 直接返回) + +#### 在 Java 中,如何跳出当前的多重嵌套循环 + +* 在Java中,要想跳出多重循环,可以在外面的循环语句前定义一个标号,然后在里层循环体的代码中使用带有标号的break 语句,即可跳出外层循环。例如: + + public static void main(String[] args) { + ok: + for (int i = 0; i < 10; i++) { + for (int j = 0; j < 10; j++) { + System.out.println("i=" + i + ",j=" + j); + if (j == 5) { + break ok; + } + } + } + } + 复制代码 + +## 面向对象 + +### 面向对象概述 + +#### 面向对象和面向过程的区别 + +* **面向过程**: + + * 优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。 + + * 缺点:没有面向对象易维护、易复用、易扩展 + +* **面向对象**: + + * 优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护 + + * 缺点:性能比面向过程低 + +`面向过程是具体化的,流程化的,解决一个问题,你需要一步一步的分析,一步一步的实现。` + +`面向对象是模型化的,你只需抽象出一个类,这是一个封闭的盒子,在这里你拥有数据也拥有解决问题的方法。需要什么功能直接使用就可以了,不必去一步一步的实现,至于这个功能是如何实现的,管我们什么事?我们会用就可以了。` + +`面向对象的底层其实还是面向过程,把面向过程抽象成类,然后封装,方便我们使用的就是面向对象了。` + +### 面向对象三大特性 + +#### 面向对象的特征有哪些方面 + +**面向对象的特征主要有以下几个方面**: + +* **抽象**:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。 + +* **封装**把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。 + +* **继承**是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。 + + * 关于继承如下 3 点请记住: + + * 子类拥有父类非 private 的属性和方法。 + + * 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。 + + * 子类可以用自己的方式实现父类的方法。(以后介绍)。 + +* **多态**:父类或接口定义的引用变量可以指向子类或具体实现类的实例对象。提高了程序的拓展性。在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。 + +#### 什么是多态机制?Java语言是如何实现多态的? + +* 所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。 + +* 多态分为编译时多态和运行时多态。其中编辑时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的函数,通过编辑之后会变成两个不同的函数,在运行时谈不上多态。而运行时多态是动态的,它是通过动态绑定来实现的,也就是我们所说的多态性。 + +**多态的实现** + +* Java实现多态有三个必要条件:继承、重写、向上转型。 + + * 继承:在多态中必须存在有继承关系的子类和父类。 + + * 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。 + + * 向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。 + +`只有满足了上述三个条件,我们才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而达到执行不同的行为。` + +`对于Java而言,它多态的实现机制遵循一个原则:当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。` + +#### 面向对象五大基本原则是什么(可选) + +* 单一职责原则SRP(Single Responsibility Principle) + 类的功能要单一,不能包罗万象,跟杂货铺似的。 +* 开放封闭原则OCP(Open-Close Principle) + 一个模块对于拓展是开放的,对于修改是封闭的,想要增加功能热烈欢迎,想要修改,哼,一万个不乐意。 +* 里式替换原则LSP(the Liskov Substitution Principle LSP) + 子类可以替换父类出现在父类能够出现的任何地方。比如你能代表你爸去你姥姥家干活。哈哈~~ +* 依赖倒置原则DIP(the Dependency Inversion Principle DIP) + 高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象。就是你出国要说你是中国人,而不能说你是哪个村子的。比如说中国人是抽象的,下面有具体的xx省,xx市,xx县。你要依赖的抽象是中国人,而不是你是xx村的。 +* 接口分离原则ISP(the Interface Segregation Principle ISP) + 设计时采用多个与特定客户类有关的接口比采用一个通用的接口要好。就比如一个手机拥有打电话,看视频,玩游戏等功能,把这几个功能拆分成不同的接口,比在一个接口里要好的多。 + +### 类与接口 + +#### 抽象类和接口的对比 + +* 抽象类是用来捕捉子类的通用特性的。接口是抽象方法的集合。 + +* 从设计层面来说,抽象类是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。 + +**相同点** + +* 接口和抽象类都不能实例化 +* 都位于继承的顶端,用于被其他实现或继承 +* 都包含抽象方法,其子类都必须覆写这些抽象方法 + +**不同点** + +| 参数 | 抽象类 | 接口 | +| --- | --- | --- | +| 声明 | 抽象类使用abstract关键字声明 | 接口使用interface关键字声明 | +| 实现 | 子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现 | 子类使用implements关键字来实现接口。它需要提供接口中所有声明的方法的实现 | +| 构造器 | 抽象类可以有构造器 | 接口不能有构造器 | +| 访问修饰符 | 抽象类中的方法可以是任意访问修饰符 | 接口方法默认修饰符是public。并且不允许定义为 private 或者 protected | +| 多继承 | 一个类最多只能继承一个抽象类 | 一个类可以实现多个接口 | +| 字段声明 | 抽象类的字段声明可以是任意的 | 接口的字段默认都是 static 和 final 的 | + +**备注**:Java8中接口中引入默认方法和静态方法,以此来减少抽象类和接口之间的差异。 + +`现在,我们可以为接口提供默认实现的方法了,并且不用强制子类来实现它。` + +* 接口和抽象类各有优缺点,在接口和抽象类的选择上,必须遵守这样一个原则: + * 行为模型应该总是通过接口而不是抽象类定义,所以通常是优先选用接口,尽量少用抽象类。 + * 选择抽象类的时候通常是如下情况:需要定义子类的行为,又要为子类提供通用的功能。 + +#### 普通类和抽象类有哪些区别? + +* 普通类不能包含抽象方法,抽象类可以包含抽象方法。 +* 抽象类不能直接实例化,普通类可以直接实例化。 + +#### 抽象类能使用 final 修饰吗? + +* 不能,定义抽象类就是让其他类继承的,如果定义为 final 该类就不能被继承,这样彼此就会产生矛盾,所以 final 不能修饰抽象类 + +#### 创建一个对象用什么关键字?对象实例与对象引用有何不同? + +* new关键字,new创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。一个对象引用可以指向0个或1个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有n个引用指向它(可以用n条绳子系住一个气球) + +### 变量与方法 + +#### 成员变量与局部变量的区别有哪些 + +* 变量:在程序执行的过程中,在某个范围内其值可以发生改变的量。从本质上讲,变量其实是内存中的一小块区域 + +* 成员变量:方法外部,类内部定义的变量 + +* 局部变量:类的方法中的变量。 + +* 成员变量和局部变量的区别 + +**作用域** + +* 成员变量:针对整个类有效。 +* 局部变量:只在某个范围内有效。(一般指的就是方法,语句体内) + +**存储位置** + +* 成员变量:随着对象的创建而存在,随着对象的消失而消失,存储在堆内存中。 +* 局部变量:在方法被调用,或者语句被执行的时候存在,存储在栈内存中。当方法调用完,或者语句结束后,就自动释放。 + +**生命周期** + +* 成员变量:随着对象的创建而存在,随着对象的消失而消失 +* 局部变量:当方法调用完,或者语句结束后,就自动释放。 + +**初始值** + +* 成员变量:有默认初始值。 +* 局部变量:没有默认初始值,使用前必须赋值。 + +#### 在Java中定义一个不做事且没有参数的构造方法的作用 + +* Java程序在执行子类的构造方法之前,如果没有用super()来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用super()来调用父类中特定的构造方法,则编译时将发生错误,因为Java程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。 + +#### 在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是? + +* 帮助子类做初始化工作。 + +#### 一个类的构造方法的作用是什么?若一个类没有声明构造方法,改程序能正确执行吗?为什么? + +* 主要作用是完成对类对象的初始化工作。可以执行。因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。 + +#### 构造方法有哪些特性? + +* 名字与类名相同; + +* 没有返回值,但不能用void声明构造函数; + +* 生成类的对象时自动执行,无需调用。 + +#### 静态变量和实例变量区别 + +* 静态变量: 静态变量由于不属于任何实例对象,属于类的,所以在内存中只会有一份,在类的加载过程中,JVM只为静态变量分配一次内存空间。 + +* 实例变量: 每次创建对象,都会为每个对象分配成员变量内存空间,实例变量是属于实例对象的,在内存中,创建几次对象,就有几份成员变量。 + +#### 静态变量与普通变量区别 + +* static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。 + +* 还有一点就是static成员变量的初始化顺序按照定义的顺序进行初始化。 + +#### 静态方法和实例方法有何不同? + +`静态方法和实例方法的区别主要体现在两个方面:` + +* 在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。 + +* 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制 + +#### 在一个静态方法内调用一个非静态成员为什么是非法的? + +* 由于静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成员。 + +#### 什么是方法的返回值?返回值的作用是什么? + +* 方法的返回值是指我们获取到的某个方法体中的代码执行后产生的结果!(前提是该方法可能产生结果)。返回值的作用:接收出结果,使得它可以用于其他的操作! + +### 内部类 + +#### 什么是内部类? + +* 在Java中,可以将一个类的定义放在另外一个类的定义内部,这就是**内部类**。内部类本身就是类的一个属性,与其他属性定义方式一致。 + +#### 内部类的分类有哪些 + +`内部类可以分为四种:**成员内部类、局部内部类、匿名内部类和静态内部类**。` + +##### 静态内部类 + +* 定义在类内部的静态类,就是静态内部类。 + + public class Outer { + + private static int radius = 1; + + static class StaticInner { + public void visit() { + System.out.println("visit outer static variable:" + radius); + } + } + } + 复制代码 +* 静态内部类可以访问外部类所有的静态变量,而不可访问外部类的非静态变量;静态内部类的创建方式,`new 外部类.静态内部类()`,如下: + + Outer.StaticInner inner = new Outer.StaticInner(); + inner.visit(); + 复制代码 + +##### 成员内部类 + +* 定义在类内部,成员位置上的非静态类,就是成员内部类。 + + public class Outer { + + private static int radius = 1; + private int count =2; + + class Inner { + public void visit() { + System.out.println("visit outer static variable:" + radius); + System.out.println("visit outer variable:" + count); + } + } + } + 复制代码 +* 成员内部类可以访问外部类所有的变量和方法,包括静态和非静态,私有和公有。成员内部类依赖于外部类的实例,它的创建方式`外部类实例.new 内部类()`,如下: + + Outer outer = new Outer(); + Outer.Inner inner = outer.new Inner(); + inner.visit(); + 复制代码 + +##### 局部内部类 + +* 定义在方法中的内部类,就是局部内部类。 + + public class Outer { + + private int out_a = 1; + private static int STATIC_b = 2; + + public void testFunctionClass(){ + int inner_c =3; + class Inner { + private void fun(){ + System.out.println(out_a); + System.out.println(STATIC_b); + System.out.println(inner_c); + } + } + Inner inner = new Inner(); + inner.fun(); + } + public static void testStaticFunctionClass(){ + int d =3; + class Inner { + private void fun(){ + // System.out.println(out_a); 编译错误,定义在静态方法中的局部类不可以访问外部类的实例变量 + System.out.println(STATIC_b); + System.out.println(d); + } + } + Inner inner = new Inner(); + inner.fun(); + } + } + 复制代码 +* 定义在实例方法中的局部类可以访问外部类的所有变量和方法,定义在静态方法中的局部类只能访问外部类的静态变量和方法。局部内部类的创建方式,在对应方法内,`new 内部类()`,如下: + + public static void testStaticFunctionClass(){ + class Inner { + } + Inner inner = new Inner(); + } + 复制代码 + +##### 匿名内部类 + +* 匿名内部类就是没有名字的内部类,日常开发中使用的比较多。 + + public class Outer { + + private void test(final int i) { + new Service() { + public void method() { + for (int j = 0; j < i; j++) { + System.out.println("匿名内部类" ); + } + } + }.method(); + } + } + //匿名内部类必须继承或实现一个已有的接口 + interface Service{ + void method(); + } + 复制代码 +* 除了没有名字,匿名内部类还有以下特点: + + * 匿名内部类必须继承一个抽象类或者实现一个接口。 + * 匿名内部类不能定义任何静态成员和静态方法。 + * 当所在的方法的形参需要被匿名内部类使用时,必须声明为 final。 + * 匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。 +* 匿名内部类创建方式: + + new 类/接口{ + //匿名内部类实现部分 + } + 复制代码 + +#### 内部类的优点 + +`我们为什么要使用内部类呢?因为它有以下优点:` + +* 一个内部类对象可以访问创建它的外部类对象的内容,包括私有数据! +* 内部类不为同一包的其他类所见,具有很好的封装性; +* 内部类有效实现了“多重继承”,优化 java 单继承的缺陷。 +* 匿名内部类可以很方便的定义回调。 + +#### 内部类有哪些应用场景 + +1. 一些多算法场合 +2. 解决一些非面向对象的语句块。 +3. 适当使用内部类,使得代码更加灵活和富有扩展性。 +4. 当某个类除了它的外部类,不再被其他的类使用时。 + +#### 局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final? + +* 局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final呢?它内部原理是什么呢?先看这段代码: + + public class Outer { + + void outMethod(){ + final int a =10; + class Inner { + void innerMethod(){ + System.out.println(a); + } + } + } + } + 复制代码 +* 以上例子,为什么要加final呢?是因为**生命周期不一致**, 局部变量直接存储在栈中,当方法执行结束后,非final的局部变量就被销毁。而局部内部类对局部变量的引用依然存在,如果局部内部类要调用局部变量时,就会出错。加了final,可以确保局部内部类使用的变量与外层的局部变量区分开,解决了这个问题。 + +#### 内部类相关,看程序说出运行结果 + +public class Outer { + private int age = 12; + + class Inner { + private int age = 13; + public void print() { + int age = 14; + System.out.println("局部变量:" + age); + System.out.println("内部类变量:" + this.age); + System.out.println("外部类变量:" + Outer.this.age); + } + } + + public static void main(String[] args) { + Outer.Inner in = new Outer().new Inner(); + in.print(); + } + +} +复制代码 + +运行结果: + +局部变量:14 +内部类变量:13 +外部类变量:12 +复制代码 +### 重写与重载 + +#### 构造器(constructor)是否可被重写(override) + +* 构造器不能被继承,因此不能被重写,但可以被重载。 + +#### 重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分? + +* 方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。 + +* 重载:发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分 + +* 重写:发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛出的异常小于等于父类,访问修饰符大于等于父类(里氏代换原则);如果父类方法访问修饰符为private则子类中就不是重写。 + +### 对象相等判断 + +#### == 和 equals 的区别是什么 + +* **==** : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型 == 比较的是值,引用数据类型 == 比较的是内存地址) + +* **equals()** : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况: + + * 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。 + + * 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。 + + * **举个例子:** + + public class test1 { + public static void main(String[] args) { + String a = new String("ab"); // a 为一个引用 + String b = new String("ab"); // b为另一个引用,对象的内容一样 + String aa = "ab"; // 放在常量池中 + String bb = "ab"; // 从常量池中查找 + if (aa == bb) // true + System.out.println("aa==bb"); + if (a == b) // false,非同一对象 + System.out.println("a==b"); + if (a.equals(b)) // true + System.out.println("aEQb"); + if (42 == 42.0) { // true + System.out.println("true"); + } + } + } + 复制代码 +* **说明:** + + * String中的equals方法是被重写过的,因为object的equals方法是比较的对象的内存地址,而String的equals方法比较的是对象的值。 + * 当创建String类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个String对象。 + +#### hashCode 与 equals (重要) + +* HashSet如何检查重复 + +* 两个对象的 hashCode() 相同,则 equals() 也一定为 true,对吗? + +* hashCode和equals方法的关系 + +* 面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写equals时必须重写hashCode方法?” + +**hashCode()介绍** + +* hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode()函数。 + +* 散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象) + +**为什么要有 hashCode** + +`我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:` + +* 当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的Java启蒙书《Head first java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。 + +**hashCode()与equals()的相关规定** + +* 如果两个对象相等,则hashcode一定也是相同的 + +* 两个对象相等,对两个对象分别调用equals方法都返回true + +* 两个对象有相同的hashcode值,它们也不一定是相等的 + +`因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖` + +`hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)` + +#### 对象的相等与指向他们的引用相等,两者有什么不同? + +* 对象的相等 比的是内存中存放的内容是否相等而 引用相等 比较的是他们指向的内存地址是否相等。 + +### 值传递 + +#### 当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递 + +* 是值传递。Java 语言的方法调用只支持参数的值传递。当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的引用。对象的属性可以在被调用过程中被改变,但对对象引用的改变是不会影响到调用者的 + +#### 为什么 Java 中只有值传递 + +* 首先回顾一下在程序设计语言中有关将参数传递给方法(或函数)的一些专业术语。**按值调用(call by value)表示方法接收的是调用者提供的值,而按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。** 它用来描述各种程序设计语言(不只是Java)中方法参数传递方式。 + +* **Java程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容。** + + * **下面通过 3 个例子来给大家说明** + +##### example 1 + +public static void main(String[] args) { + int num1 = 10; + int num2 = 20; + + swap(num1, num2); + + System.out.println("num1 = " + num1); + System.out.println("num2 = " + num2); +} + +public static void swap(int a, int b) { + int temp = a; + a = b; + b = temp; + + System.out.println("a = " + a); + System.out.println("b = " + b); +} +复制代码 + +* 结果: + + a = 20 b = 10 num1 = 10 num2 = 20 + +* 解析: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744c436af3af1~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 在swap方法中,a、b的值进行交换,并不会影响到 num1、num2。因为,a、b中的值,只是从 num1、num2 的复制过来的。也就是说,a、b相当于num1、num2 的副本,副本的内容无论怎么修改,都不会影响到原件本身。 + +`通过上面例子,我们已经知道了一个方法不能修改一个基本数据类型的参数,而对象引用作为参数就不一样,请看 example.` + +##### example 2 + + public static void main(String[] args) { + int[] arr = { 1, 2, 3, 4, 5 }; + System.out.println(arr[0]); + change(arr); + System.out.println(arr[0]); + } + + public static void change(int[] array) { + // 将数组的第一个元素变为0 + array[0] = 0; + } +复制代码 + +* 结果: + + 1 0 + +* 解析: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744c4372f6ac8~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* array 被初始化 arr 的拷贝也就是一个对象的引用,也就是说 array 和 arr 指向的时同一个数组对象。 因此,外部对引用对象的改变会反映到所对应的对象上。 + +`通过 example2 我们已经看到,实现一个改变对象参数状态的方法并不是一件难事。理由很简单,方法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。` + +`很多程序设计语言(特别是,C++和Pascal)提供了两种参数传递的方式:值调用和引用调用。有些程序员(甚至本书的作者)认为Java程序设计语言对对象采用的是引用调用,实际上,这种理解是不对的。由于这种误解具有一定的普遍性,所以下面给出一个反例来详细地阐述一下这个问题。` + +##### example 3 + +public class Test { + + public static void main(String[] args) { + // TODO Auto-generated method stub + Student s1 = new Student("小张"); + Student s2 = new Student("小李"); + Test.swap(s1, s2); + System.out.println("s1:" + s1.getName()); + System.out.println("s2:" + s2.getName()); + } + + public static void swap(Student x, Student y) { + Student temp = x; + x = y; + y = temp; + System.out.println("x:" + x.getName()); + System.out.println("y:" + y.getName()); + } +} +复制代码 + +* 结果: + + x:小李 y:小张 s1:小张 s2:小李 + +* 解析: + +* 交换之前: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744c445af6270~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 交换之后: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744c45facc688~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 通过上面两张图可以很清晰的看出:`方法并没有改变存储在变量 s1 和 s2 中的对象引用。swap方法的参数x和y被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝` + +* 总结 + + * `Java程序设计语言对对象采用的不是引用调用,实际上,对象引用是按值传递的。` +* 下面再总结一下Java中方法参数的使用情况: + + * 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型》 + * 一个方法可以改变一个对象参数的状态。 + * 一个方法不能让对象参数引用一个新的对象。 + +#### 值传递和引用传递有什么区别 + +* 值传递:指的是在方法调用时,传递的参数是按值的拷贝传递,传递的是值的拷贝,也就是说传递后就互不相关了。 + +* 引用传递:指的是在方法调用时,传递的参数是按引用进行传递,其实传递的引用的地址,也就是变量所对应的内存空间的地址。传递的是值的引用,也就是说传递前和传递后都指向同一个引用(也就是同一个内存空间)。 + +### Java包 + +#### JDK 中常用的包有哪些 + +* java.lang:这个是系统的基础类; +* java.io:这里面是所有输入输出有关的类,比如文件操作等; +* java.nio:为了完善 io 包中的功能,提高 io 包中性能而写的一个新包; +* java.net:这里面是与网络有关的类; +* java.util:这个是系统辅助类,特别是集合类; +* java.sql:这个是数据库操作的类。 + +#### import java和javax有什么区别 + +* 刚开始的时候 JavaAPI 所必需的包是 java 开头的包,javax 当时只是扩展 API 包来说使用。然而随着时间的推移,javax 逐渐的扩展成为 Java API 的组成部分。但是,将扩展从 javax 包移动到 java 包将是太麻烦了,最终会破坏一堆现有的代码。因此,最终决定 javax 包将成为标准API的一部分。 + +`所以,实际上java和javax没有区别。这都是一个名字。` + +## IO流 + +### java 中 IO 流分为几种? + +* 按照流的流向分,可以分为输入流和输出流; +* 按照操作单元划分,可以划分为字节流和字符流; +* 按照流的角色划分为节点流和处理流。 + +`Java Io流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java I0流的40多个类都是从如下4个抽象类基类中派生出来的。` + +* InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。 +* OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。 + +`按操作方式分类结构图:` + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744c4799a7a74~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +`按操作对象分类结构图:` + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744c479a04121~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +### BIO,NIO,AIO 有什么区别? + +* 简答 + + * BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。 + * NIO:Non IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。 + * AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。 +* 详细回答 + + * **BIO (Blocking I/O):** 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。 + * **NIO (New I/O):** NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 `Socket` 和 `ServerSocket` 相对应的 `SocketChannel` 和 `ServerSocketChannel` 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发 + * **AIO (Asynchronous I/O):** AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。 + +### Files的常用方法都有哪些? + +* Files. exists():检测文件路径是否存在。 +* Files. createFile():创建文件。 +* Files. createDirectory():创建文件夹。 +* Files. delete():删除一个文件或目录。 +* Files. copy():复制文件。 +* Files. move():移动文件。 +* Files. size():查看文件个数。 +* Files. read():读取文件。 +* Files. write():写入文件。 + +## 反射 + +### 什么是反射机制? + +* JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。 + +* 静态编译和动态编译 + + * 静态编译:在编译时确定类型,绑定对象 + + * 动态编译:运行时确定类型,绑定对象 + +### 反射机制优缺点 + +* **优点:** 运行期类型的判断,动态加载类,提高代码灵活度。 +* **缺点:** 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的java代码要慢很多。 + +### 反射机制的应用场景有哪些? + +* 反射是框架设计的灵魂。 + +* 在我们平时的项目开发过程中,基本上很少会直接使用到反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架也大量使用到了反射机制。 + +* 举例:①我们在使用JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序;②Spring框架也用到很多反射机制,最经典的就是xml的配置模式。Spring 通过 XML 配置模式装载 Bean 的过程:1) 将程序内所有 XML 或 Properties 配置文件加载入内存中; 2)Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息; 3)使用反射机制,根据这个字符串获得某个类的Class实例; 4)动态配置实例的属性 + +### Java获取反射的三种方法 + +1.通过new对象实现反射机制 2.通过路径实现反射机制 3.通过类名实现反射机制 + +public class Student { + private int id; + String name; + protected boolean sex; + public float score; +} + +public class Get { + //获取反射机制三种方式 + public static void main(String[] args) throws ClassNotFoundException { + //方式一(通过建立对象) + Student stu = new Student(); + Class classobj1 = stu.getClass(); + System.out.println(classobj1.getName()); + //方式二(所在通过路径-相对路径) + Class classobj2 = Class.forName("fanshe.Student"); + System.out.println(classobj2.getName()); + //方式三(通过类名) + Class classobj3 = Student.class; + System.out.println(classobj3.getName()); + } +} +复制代码 +## 常用API + +### String相关 + +#### 字符型常量和字符串常量的区别 + +1. 形式上: 字符常量是单引号引起的一个字符 字符串常量是双引号引起的若干个字符 +2. 含义上: 字符常量相当于一个整形值(ASCII值),可以参加表达式运算 字符串常量代表一个地址值(该字符串在内存中存放位置) +3. 占内存大小 字符常量只占一个字节 字符串常量占若干个字节(至少一个字符结束标志) + +#### 什么是字符串常量池? + +* 字符串常量池位于堆内存中,专门用来存储字符串常量,可以提高内存的使用率,避免开辟多块空间存储相同的字符串,在创建字符串时 JVM 会首先检查字符串常量池,如果该字符串已经存在池中,则返回它的引用,如果不存在,则实例化一个字符串放到池中,并返回其引用。 + +#### String 是最基本的数据类型吗 + +* 不是。Java 中的基本数据类型只有 8 个 :byte、short、int、long、float、double、char、boolean;除了基本类型(primitive type),剩下的都是引用类型(referencetype),Java 5 以后引入的枚举类型也算是一种比较特殊的引用类型。 + +`这是很基础的东西,但是很多初学者却容易忽视,Java 的 8 种基本数据类型中不包括 String,基本数据类型中用来描述文本数据的是 char,但是它只能表示单个字符,比如 ‘a’,‘好’ 之类的,如果要描述一段文本,就需要用多个 char 类型的变量,也就是一个 char 类型数组,比如“你好” 就是长度为2的数组 char\[\] chars = {‘你’,‘好’};` + +`但是使用数组过于麻烦,所以就有了 String,String 底层就是一个 char 类型的数组,只是使用的时候开发者不需要直接操作底层数组,用更加简便的方式即可完成对字符串的使用。` + +#### String有哪些特性 + +* 不变性:String 是只读字符串,是一个典型的 immutable 对象,对它进行任何操作,其实都是创建一个新的对象,再把引用指向该对象。不变模式的主要作用在于当一个对象需要被多线程共享并频繁访问时,可以保证数据的一致性。 + +* 常量池优化:String 对象创建之后,会在字符串常量池中进行缓存,如果下次创建同样的对象时,会直接返回缓存的引用。 + +* final:使用 final 来定义 String 类,表示 String 类不能被继承,提高了系统的安全性。 + +#### String为什么是不可变的吗? + +* 简单来说就是String类利用了final修饰的char类型数组存储字符,源码如下图所以: + + /** The value is used for character storage. */ private final char value[]; + +#### String真的是不可变的吗? + +* 我觉得如果别人问这个问题的话,回答不可变就可以了。 下面只是给大家看两个有代表性的例子: + +**1 String不可变但不代表引用不可以变** + +String str = "Hello"; +str = str + " World"; +System.out.println("str=" + str); +复制代码 + +* 结果: + + str=Hello World + +* 解析: + +* 实际上,原来String的内容是不变的,只是str由原来指向"Hello"的内存地址转为指向"Hello World"的内存地址而已,也就是说多开辟了一块内存区域给"Hello World"字符串。 + +**2.通过反射是可以修改所谓的“不可变”对象** + +// 创建字符串"Hello World", 并赋给引用s +String s = "Hello World"; + +System.out.println("s = " + s); // Hello World + +// 获取String类中的value字段 +Field valueFieldOfString = String.class.getDeclaredField("value"); + +// 改变value属性的访问权限 +valueFieldOfString.setAccessible(true); + +// 获取s对象上的value属性的值 +char[] value = (char[]) valueFieldOfString.get(s); + +// 改变value所引用的数组中的第5个字符 +value[5] = '_'; + +System.out.println("s = " + s); // Hello_World +复制代码 + +* 结果: + + s = Hello World s = Hello_World + +* 解析: + +* 用反射可以访问私有成员, 然后反射出String对象中的value属性, 进而改变通过获得的value引用改变数组的结构。但是一般我们不会这么做,这里只是简单提一下有这个东西。 + +#### 是否可以继承 String 类 + +* String 类是 final 类,不可以被继承。 + +#### String str="i"与 String str=new String(“i”)一样吗? + +* 不一样,因为内存的分配方式不一样。String str="i"的方式,java 虚拟机会将其分配到常量池中;而 String str=new String(“i”) 则会被分到堆内存中。 + +#### String s = new String(“xyz”);创建了几个字符串对象 + +* 两个对象,一个是静态区的"xyz",一个是用new创建在堆上的对象。 + + String str1 = "hello"; //str1指向静态区 String str2 = new String("hello"); //str2指向堆上的对象 String str3 = "hello"; String str4 = new String("hello"); System.out.println(str1.equals(str2)); //true System.out.println(str2.equals(str4)); //true System.out.println(str1 == str3); //true System.out.println(str1 == str2); //false System.out.println(str2 == str4); //false System.out.println(str2 == "hello"); //false str2 = str1; System.out.println(str2 == "hello"); //true + +#### 如何将字符串反转? + +* 使用 StringBuilder 或者 stringBuffer 的 reverse() 方法。 + +* 示例代码: + + // StringBuffer reverse StringBuffer stringBuffer = new StringBuffer(); stringBuffer. append("abcdefg"); System. out. println(stringBuffer. reverse()); // gfedcba // StringBuilder reverse StringBuilder stringBuilder = new StringBuilder(); stringBuilder. append("abcdefg"); System. out. println(stringBuilder. reverse()); // gfedcba + +#### 数组有没有 length()方法?String 有没有 length()方法 + +* 数组没有 length()方法 ,有 length 的属性。String 有 length()方法。JavaScript中,获得字符串的长度是通过 length 属性得到的,这一点容易和 Java 混淆。 + +#### String 类的常用方法都有那些? + +* indexOf():返回指定字符的索引。 +* charAt():返回指定索引处的字符。 +* replace():字符串替换。 +* trim():去除字符串两端空白。 +* split():分割字符串,返回一个分割后的字符串数组。 +* getBytes():返回字符串的 byte 类型数组。 +* length():返回字符串长度。 +* toLowerCase():将字符串转成小写字母。 +* toUpperCase():将字符串转成大写字符。 +* substring():截取字符串。 +* equals():字符串比较。 + +#### 在使用 HashMap 的时候,用 String 做 key 有什么好处? + +* HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置,因为字符串是不可变的,所以当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。 + +#### String和StringBuffer、StringBuilder的区别是什么?String为什么是不可变的 + +**可变性** + +* String类中使用字符数组保存字符串,private final char value[],所以string对象是不可变的。StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,char[] value,这两种对象都是可变的。 + +**线程安全性** + +* String中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder是StringBuilder与StringBuffer的公共父类,定义了一些字符串的基本操作,如expandCapacity、append、insert、indexOf等公共方法。StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。 + +**性能** + +* 每次对String 类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String 对象。StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用StirngBuilder 相比使用StringBuffer 仅能获得10%~15% 左右的性能提升,但却要冒多线程不安全的风险。 + +**对于三者使用的总结** + +* 如果要操作少量的数据用 = String + +* 单线程操作字符串缓冲区 下操作大量数据 = StringBuilder + +* 多线程操作字符串缓冲区 下操作大量数据 = StringBuffer + +### Date相关 + +### 包装类相关 + +#### 自动装箱与拆箱 + +* **装箱**:将基本类型用它们对应的引用类型包装起来; + +* **拆箱**:将包装类型转换为基本数据类型; + +#### int 和 Integer 有什么区别 + +* Java 是一个近乎纯洁的面向对象编程语言,但是为了编程的方便还是引入了基本数据类型,但是为了能够将这些基本数据类型当成对象操作,Java 为每一个基本数据类型都引入了对应的包装类型(wrapper class),int 的包装类就是 Integer,从 Java 5 开始引入了自动装箱/拆箱机制,使得二者可以相互转换。 + +* Java 为每个原始类型提供了包装类型: + + * 原始类型: boolean,char,byte,short,int,long,float,double + + * 包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double + +#### Integer a= 127 与 Integer b = 127相等吗 + +* 对于对象引用类型:==比较的是对象的内存地址。 +* 对于基本数据类型:==比较的是值。 + +`如果整型字面量的值在-128到127之间,那么自动装箱时不会new新的Integer对象,而是直接引用常量池中的Integer对象,超过范围 a1==b1的结果是false` + +public static void main(String[] args) { + Integer a = new Integer(3); + Integer b = 3; // 将3自动装箱成Integer类型 + int c = 3; + System.out.println(a == b); // false 两个引用没有引用同一对象 + System.out.println(a == c); // true a自动拆箱成int类型再和c比较 + System.out.println(b == c); // true + + Integer a1 = 128; + Integer b1 = 128; + System.out.println(a1 == b1); // false + + Integer a2 = 127; + Integer b2 = 127; + System.out.println(a2 == b2); // true +} + +作者:小杰要吃蛋 +链接:https://juejin.cn/post/6844904127059738631 +来源:稀土掘金 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 \ No newline at end of file diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\345\271\266\345\217\221.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\345\271\266\345\217\221.md" new file mode 100644 index 0000000..50d9a6a --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\345\271\266\345\217\221.md" @@ -0,0 +1,1478 @@ +https://juejin.cn/post/6844904119338024974#heading-17 + +## 基础知识 + +#### 为什么要使用并发编程 + +* 提升多核CPU的利用率:一般来说一台主机上的会有多个CPU核心,我们可以创建多个线程,理论上讲操作系统可以将多个线程分配给不同的CPU去执行,每个CPU执行一个线程,这样就提高了CPU的使用效率,如果使用单线程就只能有一个CPU核心被使用。 + +* 比如当我们在网上购物时,为了提升响应速度,需要拆分,减库存,生成订单等等这些操作,就可以进行拆分利用多线程的技术完成。面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分 。 + +* 简单来说就是: + + * 充分利用多核CPU的计算能力; + * 方便进行业务拆分,提升应用性能 + +#### 多线程应用场景 + +* 例如: 迅雷多线程下载、数据库连接池、分批发送短信等。 + +#### 并发编程有什么缺点 + +* 并发编程的目的就是为了能提高程序的执行效率,提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、线程安全、死锁等问题。 + +#### 并发编程三个必要因素是什么? + +* 原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。 +* 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。(synchronized,volatile) +* 有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序) + +#### 在 Java 程序中怎么保证多线程的运行安全? + +* 出现线程安全问题的原因一般都是三个原因: + + * 线程切换带来的原子性问题 解决办法:使用多线程之间同步synchronized或使用锁(lock)。 + + * 缓存导致的可见性问题 解决办法:synchronized、volatile、LOCK,可以解决可见性问题 + + * 编译优化带来的有序性问题 解决办法:Happens-Before 规则可以解决有序性问题 + +#### 并行和并发有什么区别? + +* 并发:多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。 +* 并行:单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”。 +* 串行:有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题。 + +**做一个形象的比喻:** + +* 并发 = 俩个人用一台电脑。 + +* 并行 = 俩个人分配了俩台电脑。 + +* 串行 = 俩个人排队使用一台电脑。 + +#### 什么是多线程 + +* 多线程:多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务。 + +#### 多线程的好处 + +* 可以提高 CPU 的利用率。在多线程程序中,一个线程必须等待的时候,CPU 可以运行其它的线程而不是等待,这样就大大提高了程序的效率。也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。 + +#### 多线程的劣势: + +* 线程也是程序,所以线程需要占用内存,线程越多占用内存也越多; + +* 多线程需要协调和管理,所以需要 CPU 时间跟踪线程; + +* 线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。 + +#### 线程和进程区别 + +* 什么是线程和进程? + + * 进程 + + 一个在内存中运行的应用程序。 每个正在系统上运行的程序都是一个进程 + + * 线程 + + 进程中的一个执行任务(控制单元), 它负责在程序里独立执行。 + +`一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。` + +* 进程与线程的区别 + + * 根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位 + + * 资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。 + + * 包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。 + + * 内存分配:同一进程的线程共享本进程的地址空间和资源,而进程与进程之间的地址空间和资源是相互独立的 + + * 影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃有可能导致整个进程都死掉。所以多进程要比多线程健壮。 + + * 执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行 + +#### 什么是上下文切换? + +* 多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。 + +* 概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。 + +* 上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。 + +* Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。 + +#### 守护线程和用户线程有什么区别呢? + +* 用户 (User) 线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程 +* 守护 (Daemon) 线程:运行在后台,为其他前台线程服务。也可以说守护线程是 JVM 中非守护线程的 “佣人”。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作 + +#### 如何在 Windows 和 Linux 上查找哪个线程cpu利用率最高? + +* windows上面用任务管理器看,linux下可以用 top 这个工具看。 + * 找出cpu耗用厉害的进程pid, 终端执行top命令,然后按下shift+p (shift+m是找出消耗内存最高)查找出cpu利用最厉害的pid号 + * 根据上面第一步拿到的pid号,top -H -p pid 。然后按下shift+p,查找出cpu利用率最厉害的线程号,比如top -H -p 1328 + * 将获取到的线程号转换成16进制,去百度转换一下就行 + * 使用jstack工具将进程信息打印输出,jstack pid号 > /tmp/t.dat,比如jstack 31365 > /tmp/t.dat + * 编辑/tmp/t.dat文件,查找线程号对应的信息 + +`或者直接使用JDK自带的工具查看“jconsole” 、“visualVm”,这都是JDK自带的,可以直接在JDK的bin目录下找到直接使用` + +#### 什么是线程死锁 + +* 死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)。 +* 多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。 +* 如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bab440e1912~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### 形成死锁的四个必要条件是什么 + +* 互斥条件:在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,就只能等待,直至占有资源的进程用毕释放。 +* 占有且等待条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。 +* 不可抢占条件:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。 +* 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。(比如一个进程集合,A在等B,B在等C,C在等A) + +#### 如何避免线程死锁 + +1. 避免一个线程同时获得多个锁 +2. 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源 +3. 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制 + +#### 创建线程的四种方式 + +* 继承 Thread 类; + +```java +public class MyThread extends Thread { + @Override + public void run() { + System.out.println(Thread.currentThread().getName() + " run()方法正在执行..."); + } +``` +* 实现 Runnable 接口; + +```java +public class MyRunnable implements Runnable { + @Override + public void run() { + System.out.println(Thread.currentThread().getName() + " run()方法执行中..."); + } +``` +* 实现 Callable 接口; + +```java +public class MyCallable implements Callable { + @Override + public Integer call() { + System.out.println(Thread.currentThread().getName() + " call()方法执行中..."); + return 1; + } +``` +* 使用匿名内部类方式 + +```java +public class CreateRunnable { + public static void main(String[] args) { + //创建多线程创建开始 + Thread thread = new Thread(new Runnable() { + public void run() { + for (int i = 0; i < 10; i++) { + System.out.println("i:" + i); + } + } + }); + thread.start(); + } + } +``` + +#### 说一下 runnable 和 callable 有什么区别 + +**相同点:** + +* 都是接口 +* 都可以编写多线程程序 +* 都采用Thread.start()启动线程 + +**主要区别:** + +* Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果 +* Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息 注:Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。 + +#### 线程的 run()和 start()有什么区别? + +* 每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,run()方法称为线程体。通过调用Thread类的start()方法来启动一个线程。 + +* start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。 + +* start()方法来启动一个线程,真正实现了多线程运行。调用start()方法无需等待run方法体代码执行完毕,可以直接继续执行其他的代码; 此时线程是处于就绪状态,并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, run()方法运行结束, 此线程终止。然后CPU再调度其它线程。 + +* run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。 + +#### 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法? + +这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来! + +* new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到`时间片`后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 + +* 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。 + +总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。 + +#### 什么是 Callable 和 Future? + +* Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。 + +* Future 接口表示异步任务,是一个可能还没有完成的异步任务的结果。所以说 Callable用于产生结果,Future 用于获取结果。 + +#### 什么是 FutureTask + +* FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是Runnable 接口的实现类,所以 FutureTask 也可以放入线程池中。 + +#### 线程的状态 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bab4672a149~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 新建(new):新创建了一个线程对象。 + +* 就绪(可运行状态)(runnable):线程对象创建后,当调用线程对象的 start()方法,该线程处于就绪状态,等待被线程调度选中,获取cpu的使用权。 + +* 运行(running):可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中; + +* 阻塞(block):处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到运行状态。 + + * 阻塞的情况分三种: + * (一). 等待阻塞:运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waitting queue)中,使本线程进入到等待阻塞状态; + * (二). 同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),,则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态; + * (三). 其他阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。 +* 死亡(dead)(结束):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。 + +#### Java 中用到的线程调度算法是什么? + +* 计算机通常只有一个 CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU 的使用权才能执行指令。所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得 CPU 的使用权,分别执行各自的任务。在运行池中,会有多个处于就绪状态的线程在等待 CPU,JAVA 虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配 CPU 的使用权。(Java是由JVM中的线程计数器来实现线程调度) + +* 有两种调度模型:分时调度模型和抢占式调度模型。 + + * 分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU 的时间片这个也比较好理解。 + + * Java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU。 + +#### 线程的调度策略 + +`线程调度器选择优先级最高的线程运行,但是,如果发生以下情况,就会终止线程的运行:` + +* (1)线程体中调用了 yield 方法让出了对 cpu 的占用权利 + +* (2)线程体中调用了 sleep 方法使线程进入睡眠状态 + +* (3)线程由于 IO 操作受到阻塞 + +* (4)另外一个更高优先级线程出现 + +* (5)在支持时间片的系统中,该线程的时间片用完 + +#### 什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing )? + +* 线程调度器是一个操作系统服务,它负责为 Runnable 状态的线程分配 CPU 时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。 + +* 时间分片是指将可用的 CPU 时间分配给可用的 Runnable 线程的过程。分配 CPU 时间可以基于线程优先级或者线程等待的时间。 + +* 线程调度并不受到 Java 虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级)。 + +#### 请说出与线程同步以及线程调度相关的方法。 + +* (1) wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁; + +* (2)sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常; + +* (3)notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关; + +* (4)notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态; + +#### sleep() 和 wait() 有什么区别? + +`两者都可以暂停线程的执行` + +* 类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。 +* 是否释放锁:sleep() 不释放锁;wait() 释放锁。 +* 用途不同:Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。 +* 用法不同:wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。 + +#### 你是如何调用 wait() 方法的?使用 if 块还是循环?为什么? + +* 处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。 + +* wait() 方法应该在循环调用,因为当线程获取到 CPU 开始执行的时候,其他条件可能还没有满足,所以在处理前,循环检测条件是否满足会更好。下面是一段标准的使用 wait 和 notify 方法的代码: + +synchronized (monitor) { + // 判断条件谓词是否得到满足 + while(!locked) { + // 等待唤醒 + monitor.wait(); + } + // 处理其他的业务逻辑 +} +复制代码 +#### 为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在 Object 类里? + +* 因为Java所有类的都继承了Object,Java想让任何对象都可以作为锁,并且 wait(),notify()等方法用于等待对象的锁或者唤醒线程,在 Java 的线程中并没有可供任何对象使用的锁,所以任意对象调用方法一定定义在Object类中。 + +* 有的人会说,既然是线程放弃对象锁,那也可以把wait()定义在Thread类里面啊,新定义的线程继承于Thread类,也不需要重新定义wait()方法的实现。然而,这样做有一个非常大的问题,一个线程完全可以持有很多锁,你一个线程放弃锁的时候,到底要放弃哪个锁?当然了,这种设计并不是不能实现,只是管理起来更加复杂。 + +#### 为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用? + +* 当一个线程需要调用对象的 wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的 notify()方法。同样的,当一个线程需要调用对象的 notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。 + +#### Thread 类中的 yield 方法有什么作用? + +* 使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。 + +* 当前线程到了就绪状态,那么接下来哪个线程会从就绪状态变成执行状态呢?可能是当前线程,也可能是其他线程,看系统的分配了。 + +#### 为什么 Thread 类的 sleep()和 yield ()方法是静态的? + +* Thread 类的 sleep()和 yield()方法将在当前正在执行的线程上运行。所以在其他处于等待状态的线程上调用这些方法是没有意义的。这就是为什么这些方法是静态的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这些方法。 + +#### 线程的 sleep()方法和 yield()方法有什么区别? + +* (1) sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会; + +* (2) 线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态; + +* (3)sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常; + +* (4)sleep()方法比 yield()方法(跟操作系统 CPU 调度相关)具有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。 + +#### 如何停止一个正在运行的线程? + +* 在java中有以下3种方法可以终止正在运行的线程: + * 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。 + * 使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法。 + * 使用interrupt方法中断线程。 + +#### Java 中 interrupted 和 isInterrupted 方法的区别? + +* interrupt:用于中断线程。调用该方法的线程的状态为将被置为”中断”状态。 + + 注意:线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException 的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。 + +* interrupted:是静态方法,查看当前中断信号是true还是false并且清除中断信号。如果一个线程被中断了,第一次调用 interrupted 则返回 true,第二次和后面的就返回 false 了。 + +* isInterrupted:是可以返回当前中断信号是true还是false,与interrupt最大的差别 + +#### 什么是阻塞式方法? + +* 阻塞式方法是指程序会一直等待该方法完成期间不做其他事情,ServerSocket 的accept()方法就是一直等待客户端连接。这里的阻塞是指调用结果返回之前,当前线程会被挂起,直到得到结果之后才会返回。此外,还有异步和非阻塞式方法在任务完成前就返回。 + +#### Java 中你怎样唤醒一个阻塞的线程? + +* 首先 ,wait()、notify() 方法是针对对象的,调用任意对象的 wait()方法都将导致线程阻塞,阻塞的同时也将释放该对象的锁,相应地,调用任意对象的 notify()方法则将随机解除该对象阻塞的线程,但它需要重新获取该对象的锁,直到获取成功才能往下执行; + +* 其次,wait、notify 方法必须在 synchronized 块或方法中被调用,并且要保证同步块或方法的锁对象与调用 wait、notify 方法的对象是同一个,如此一来在调用 wait 之前当前线程就已经成功获取某对象的锁,执行 wait 阻塞后当前线程就将之前获取的对象锁释放。 + +#### notify() 和 notifyAll() 有什么区别? + +* 如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。 + +* notifyAll() 会唤醒所有的线程,notify() 只会唤醒一个线程。 + +* notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而 notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。 + +#### 如何在两个线程间共享数据? + +* 在两个线程间共享变量即可实现共享。 + +`一般来说,共享变量要求变量本身是线程安全的,然后在线程内使用的时候,如果有对共享变量的复合操作,那么也得保证复合操作的线程安全性。` + +#### Java 如何实现多线程之间的通讯和协作? + +* 可以通过中断 和 共享变量的方式实现线程间的通讯和协作 + +* 比如说最经典的生产者-消费者模型:当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界资源(即队列)的占用权。因为生产者如果不释放对临界资源的占用权,那么消费者就无法消费队列中的商品,就不会让队列有空间,那么生产者就会一直无限等待下去。因此,一般情况下,当队列满时,会让生产者交出对临界资源的占用权,并进入挂起状态。然后等待消费者消费了商品,然后消费者通知生产者队列有空间了。同样地,当队列空时,消费者也必须等待,等待生产者通知它队列中有商品了。这种互相通信的过程就是线程间的协作。 + +* Java中线程通信协作的最常见方式: + + * 一.syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll() + + * 二.ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll() + +* 线程间直接的数据交换: + + * 三.通过管道进行线程间通信:字节流、字符流 + +#### 同步方法和同步块,哪个是更好的选择? + +* 同步块是更好的选择,因为它不会锁住整个对象(当然你也可以让它锁住整个对象)。同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停止执行并需要等待获得这个对象上的锁。 + +* 同步块更要符合开放调用的原则,只在需要锁住的代码块锁住相应的对象,这样从侧面来说也可以避免死锁。 + +`请知道一条原则:同步的范围越小越好。` + +#### 什么是线程同步和线程互斥,有哪几种实现方式? + +* 当一个线程对共享的数据进行操作时,应使之成为一个”原子操作“,即在没有完成相关操作之前,不允许其他线程打断它,否则,就会破坏数据的完整性,必然会得到错误的处理结果,这就是线程的同步。 + +* 在多线程应用中,考虑不同线程之间的数据同步和防止死锁。当两个或多个线程之间同时等待对方释放资源的时候就会形成线程之间的死锁。为了防止死锁的发生,需要通过同步来实现线程安全。 + +* 线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。 + +* 线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。 + +* 用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。内核模式下的方法有:事件,信号量,互斥量。 + +* 实现线程同步的方法 + + * 同步代码方法:sychronized 关键字修饰的方法 + + * 同步代码块:sychronized 关键字修饰的代码块 + + * 使用特殊变量域volatile实现线程同步:volatile关键字为域变量的访问提供了一种免锁机制 + + * 使用重入锁实现线程同步:reentrantlock类是可冲入、互斥、实现了lock接口的锁他与sychronized方法具有相同的基本行为和语义 + +#### 在监视器(Monitor)内部,是如何做线程同步的?程序应该做哪种级别的同步? + +* 在 java 虚拟机中,监视器和锁在Java虚拟机中是一块使用的。监视器监视一块同步代码块,确保一次只有一个线程执行同步代码块。每一个监视器都和一个对象引用相关联。线程在获取锁之前不允许执行同步代码。 + +* 一旦方法或者代码块被 synchronized 修饰,那么这个部分就放入了监视器的监视区域,确保一次只能有一个线程执行该部分的代码,线程在获取锁之前不允许执行该部分的代码 + +* 另外 java 还提供了显式监视器( Lock )和隐式监视器( synchronized )两种锁方案 + +#### 如果你提交任务时,线程池队列已满,这时会发生什么 + +* 有俩种可能: + + (1)如果使用的是无界队列 LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务 + + (2)如果使用的是有界队列比如 ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue 中,ArrayBlockingQueue 满了,会根据maximumPoolSize 的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue 继续满,那么则会使用拒绝策略RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy + +#### 什么叫线程安全?servlet 是线程安全吗? + +* 线程安全是编程中的术语,指某个方法在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。 + +* Servlet 不是线程安全的,servlet 是单实例多线程的,当多个线程同时访问同一个方法,是不能保证共享变量的线程安全性的。 + +* Struts2 的 action 是多实例多线程的,是线程安全的,每个请求过来都会 new 一个新的 action 分配给这个请求,请求完成后销毁。 + +* SpringMVC 的 Controller 是线程安全的吗?不是的,和 Servlet 类似的处理流程。 + +* Struts2 好处是不用考虑线程安全问题;Servlet 和 SpringMVC 需要考虑线程安全问题,但是性能可以提升不用处理太多的 gc,可以使用 ThreadLocal 来处理多线程的问题。 + +#### 在 Java 程序中怎么保证多线程的运行安全? + +* 方法一:使用安全类,比如 java.util.concurrent 下的类,使用原子类AtomicInteger + +* 方法二:使用自动锁 synchronized。 + +* 方法三:使用手动锁 Lock。 + +* 手动锁 Java 示例代码如下: + +```java +Lock lock = new ReentrantLock(); + lock. lock(); + try { + System. out. println("获得锁"); + } catch (Exception e) { + // TODO: handle exception + } finally { + System. out. println("释放锁"); + lock. unlock(); + } +``` + +#### 你对线程优先级的理解是什么? + +* 每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,但这依赖于线程调度的实现,这个实现是和操作系统相关的(OS dependent)。我们可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程优先级是一个 int 变量(从 1-10),1 代表最低优先级,10 代表最高优先级。 + +* Java 的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优先级有关,如非特别需要,一般无需设置线程优先级。 + +* 当然,如果你真的想设置优先级可以通过setPriority()方法设置,但是设置了不一定会该变,这个是不准确的 + +#### 线程类的构造方法、静态块是被哪个线程调用的 + +* 这是一个非常刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被 new这个线程类所在的线程所调用的,而 run 方法里面的代码才是被线程自身所调用的。 + +* 如果说上面的说法让你感到困惑,那么我举个例子,假设 Thread2 中 new 了Thread1,main 函数中 new 了 Thread2,那么: + +(1)Thread2 的构造方法、静态块是 main 线程调用的,Thread2 的 run()方法是Thread2 自己调用的 + +(2)Thread1 的构造方法、静态块是 Thread2 调用的,Thread1 的 run()方法是Thread1 自己调用的 + +#### Java 中怎么获取一份线程 dump 文件?你如何在 Java 中获取线程堆栈? + +* Dump文件是进程的内存镜像。可以把程序的执行状态通过调试器保存到dump文件中。 + +* 在 Linux 下,你可以通过命令 kill -3 PID (Java 进程的进程 ID)来获取 Java应用的 dump 文件。 + +* 在 Windows 下,你可以按下 Ctrl + Break 来获取。这样 JVM 就会将线程的 dump 文件打印到标准输出或错误文件中,它可能打印在控制台或者日志文件中,具体位置依赖应用的配置。 + +#### 一个线程运行时发生异常会怎样? + +* 如果异常没有被捕获该线程将会停止执行。Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候,JVM 会使用 Thread.getUncaughtExceptionHandler()来查询线程的 UncaughtExceptionHandler 并将线程和异常作为参数传递给 handler 的 uncaughtException()方法进行处理。 + +#### Java 线程数过多会造成什么异常? + +* 线程的生命周期开销非常高 + +* 消耗过多的 CPU + + 资源如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争 CPU资源时还将产生其他性能的开销。 + +* 降低稳定性JVM + + 在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括 JVM 的启动参数、Thread 构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出OutOfMemoryError 异常。 + +#### 多线程的常用方法 + +| 方法 名 | 描述 | +| --- | --- | +| sleep() | 强迫一个线程睡眠N毫秒 | +| isAlive() | 判断一个线程是否存活。 | +| join() | 等待线程终止。 | +| activeCount() | 程序中活跃的线程数。 | +| enumerate() | 枚举程序中的线程。 | +| currentThread() | 得到当前线程。 | +| isDaemon() | 一个线程是否为守护线程。 | +| setDaemon() | 设置一个线程为守护线程。 | +| setName() | 为线程设置一个名称。 | +| wait() | 强迫一个线程等待。 | +| notify() | 通知一个线程继续运行。 | +| setPriority() | 设置一个线程的优先级。 | + +## 并发理论 + +#### Java中垃圾回收有什么目的?什么时候进行垃圾回收? + +* 垃圾回收是在内存中存在没有引用的对象或超过作用域的对象时进行的。 + +* 垃圾回收的目的是识别并且丢弃应用不再使用的对象来释放和重用资源。 + +#### 线程之间如何通信及线程之间如何同步 + +* 在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步。通信是指线程之间以如何来交换信息。一般线程之间的通信机制有两种:共享内存和消息传递。 + +* Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。如果编写多线程程序的Java程序员不理解隐式进行的线程之间通信的工作机制,很可能会遇到各种奇怪的内存可见性问题。 + +#### Java内存模型 + +* 共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入时,能对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bab46986d99~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤: + 1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。 + 2. 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。 + +**下面通过示意图来说明线程之间的通信** + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bab7ff494ac~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 总结:什么是Java内存模型:java内存模型简称jmm,定义了一个线程对另一个线程可见。共享变量存放在主内存中,每个线程都有自己的本地内存,当多个线程同时访问一个数据的时候,可能本地内存没有及时刷新到主内存,所以就会发生线程安全问题。 + +#### 如果对象的引用被置为null,垃圾收集器是否会立即释放对象占用的内存? + +* 不会,在下一个垃圾回调周期中,这个对象将是被可回收的。 + +* 也就是说并不会立即被垃圾收集器立刻回收,而是在下一次垃圾回收时才会释放其占用的内存。 + +#### finalize()方法什么时候被调用?析构函数(finalization)的目的是什么? + +* 1.垃圾回收器(garbage colector)决定回收某对象时,就会运行该对象的finalize()方法; finalize是Object类的一个方法,该方法在Object类中的声明protected void finalize() throws Throwable { } 在垃圾回收器执行时会调用被回收对象的finalize()方法,可以覆盖此方法来实现对其资源的回收。注意:一旦垃圾回收器准备释放对象占用的内存,将首先调用该对象的finalize()方法,并且下一次垃圾回收动作发生时,才真正回收对象占用的内存空间 + +* 1. GC本来就是内存回收了,应用还需要在finalization做什么呢? 答案是大部分时候,什么都不用做(也就是不需要重载)。只有在某些很特殊的情况下,比如你调用了一些native的方法(一般是C写的),可以要在finaliztion里去调用C的释放函数。 + * Finalizetion主要用来释放被对象占用的资源(不是指内存,而是指其他资源,比如文件(File Handle)、端口(ports)、数据库连接(DB Connection)等)。然而,它不能真正有效地工作。 + +#### 什么是重排序 + +* 程序执行的顺序按照代码的先后顺序执行。 +* 一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,进行重新排序(重排序),它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。 + +int a = 5; //语句1 +int r = 3; //语句2 +a = a + 2; //语句3 +r = a*a; //语句4 +复制代码 + +* 则因为重排序,他还可能执行顺序为(这里标注的是语句的执行顺序) 2-1-3-4,1-3-2-4 但绝不可能 2-1-4-3,因为这打破了依赖关系。 +* 显然重排序对单线程运行是不会有任何问题,但是多线程就不一定了,所以我们在多线程编程时就得考虑这个问题了。 + +#### 重排序实际执行的指令步骤 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bab5818fe21~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。 +2. 指令级并行的重排序。现代处理器采用了指令级并行技术(ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。 +3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。 + +* 这些重排序对于单线程没问题,但是多线程都可能会导致多线程程序出现内存可见性问题。 + +#### 重排序遵守的规则 + +* as-if-serial: + 1. 不管怎么排序,结果不能改变 + 2. 不存在数据依赖的可以被编译器和处理器重排序 + 3. 一个操作依赖两个操作,这两个操作如果不存在依赖可以重排序 + 4. 单线程根据此规则不会有问题,但是重排序后多线程会有问题 + +#### as-if-serial规则和happens-before规则的区别 + +* as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。 + +* as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。 + +* as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。 + +#### 并发关键字 synchronized ? + +* 在 Java 中,synchronized 关键字是用来控制线程同步的,就是在多线程的环境下,控制 synchronized 代码段不被多个线程同时执行。synchronized 可以修饰类、方法、变量。 + +* 另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。 + +#### 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗 + +**synchronized关键字最主要的三种使用方式:** + +* 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁 +* 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。 +* 修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。 + +`总结: synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!` + +#### 单例模式了解吗?给我解释一下双重检验锁方式实现单例模式!” + +**双重校验锁实现对象单例(线程安全)** + +**说明:** + +* 双锁机制的出现是为了解决前面同步问题和性能问题,看下面的代码,简单分析下确实是解决了多线程并行进来不会出现重复new对象,而且也实现了懒加载 + +```java + public class Singleton { + private volatile static Singleton uniqueInstance; + private Singleton() {} + + public static Singleton getUniqueInstance() { + //先判断对象是否已经实例过,没有实例化过才进入加锁代码 + if (uniqueInstance == null) { + //类对象加锁 + synchronized (Singleton.class) { + if (uniqueInstance == null) { + uniqueInstance = new Singleton(); + } + } + } + return uniqueInstance; + } + } +``` +`另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。` + +* uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行: + +1. 为 uniqueInstance 分配内存空间 +2. 初始化 uniqueInstance +3. 将 uniqueInstance 指向分配的内存地址 + +`但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。` + +`使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。` + +#### 说一下 synchronized 底层实现原理? + +* Synchronized的语义底层是通过一个monitor(监视器锁)的对象来完成, + +* 每个对象有一个监视器锁(monitor)。每个Synchronized修饰过的代码当它的monitor被占用时就会处于锁定状态并且尝试获取monitor的所有权 ,过程: + + 1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。 + + 2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1. + + 3、如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。 + +`synchronized是可以通过 反汇编指令 javap命令,查看相应的字节码文件。` + +#### synchronized可重入的原理 + +* 重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁。底层原理维护一个计数器,当线程获取该锁时,计数器加一,再次获得该锁时继续加一,释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁。 + +#### 什么是自旋 + +* 很多 synchronized 里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然 synchronized 里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在 synchronized 的边界做忙循环,这就是自旋。如果做了多次循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。 +* 忙循环:就是程序员用循环让一个线程等待,不像传统方法wait(), sleep() 或 yield() 它们都放弃了CPU控制,而忙循环不会放弃CPU,它就是在运行一个空循环。这么做的目的是为了保留CPU缓存,在多核系统中,一个等待线程醒来的时候可能会在另一个内核运行,这样会重建缓存。为了避免重建缓存和减少等待重建的时间就可以使用它了。 + +#### 多线程中 synchronized 锁升级的原理是什么? + +* synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。 + +`锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。` + +* 偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,减少加锁/解锁的一些CAS操作(比如等待队列的一些CAS操作),这种情况下,就会给线程加一个偏向锁。 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。 + +* 轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,轻量级锁就会升级为重量级锁; + +* 重量级锁是synchronized ,是 Java 虚拟机中最为基础的锁实现。在这种状态下,Java 虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程。 + +#### 线程 B 怎么知道线程 A 修改了变量 + +* (1)volatile 修饰变量 + +* (2)synchronized 修饰修改变量的方法 + +* (3)wait/notify + +* (4)while 轮询 + +#### 当一个线程进入一个对象的 synchronized 方法 A 之后,其它线程是否可进入此对象的 synchronized 方法 B? + +* 不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。因为非静态方法上的 synchronized 修饰符要求执行方法时要获得对象的锁,如果已经进入A 方法说明对象锁已经被取走,那么试图进入 B 方法的线程就只能在等锁池(注意不是等待池哦)中等待对象的锁。 + +#### synchronized、volatile、CAS 比较 + +* (1)synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。 + +* (2)volatile 提供多线程共享变量可见性和禁止指令重排序优化。 + +* (3)CAS 是基于冲突检测的乐观锁(非阻塞) + +#### synchronized 和 Lock 有什么区别? + +* 首先synchronized是Java内置关键字,在JVM层面,Lock是个Java类; +* synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。 +* synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。 +* 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。 + +#### synchronized 和 ReentrantLock 区别是什么? + +* synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量 + +* synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在 Java 6 中对 synchronized 进行了非常多的改进。 + +* 相同点:两者都是可重入锁 + + 两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。 + +* 主要区别如下: + + * ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作; + * ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁; + * ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等。 + * 二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的park 方法加锁,synchronized 操作的应该是对象头中 mark word +* Java中每一个对象都可以作为锁,这是synchronized实现同步的基础: + + * 普通同步方法,锁是当前实例对象 + * 静态同步方法,锁是当前类的class对象 + * 同步方法块,锁是括号里面的对象 + +#### volatile 关键字的作用 + +* 对于可见性,Java 提供了 volatile 关键字来保证可见性和禁止指令重排。 volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主内存中,当有其他线程需要读取时,它会去内存中读取新值。 + +* 从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详细的可以参见 java.util.concurrent.atomic 包下的类,比如 AtomicInteger。 + +* volatile 常用于多线程环境下的单次操作(单次读或者单次写)。 + +#### Java 中能创建 volatile 数组吗? + +* 能,Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。意思是,如果改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线程同时改变数组的元素,volatile 标示符就不能起到之前的保护作用了。 + +#### volatile 变量和 atomic 变量有什么不同? + +* volatile 变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用 volatile 修饰 count 变量,那么 count++ 操作就不是原子性的。 + +* 而 AtomicInteger 类提供的 atomic 方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。 + +#### volatile 能使得一个非原子操作变成原子操作吗? + +* 关键字volatile的主要作用是使变量在多个线程间可见,但无法保证原子性,对于多个线程访问同一个实例变量需要加锁进行同步。 + +* 虽然volatile只能保证可见性不能保证原子性,但用volatile修饰long和double可以保证其操作原子性。 + +**所以从Oracle Java Spec里面可以看到:** + +* 对于64位的long和double,如果没有被volatile修饰,那么对其操作可以不是原子的。在操作的时候,可以分成两步,每次对32位操作。 +* 如果使用volatile修饰long和double,那么其读写都是原子操作 +* 对于64位的引用地址的读写,都是原子操作 +* 在实现JVM时,可以自由选择是否把读写long和double作为原子操作 +* 推荐JVM实现为原子操作 + +#### synchronized 和 volatile 的区别是什么? + +* synchronized 表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程。 + +* volatile 表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序。 + +**区别** + +* volatile 是变量修饰符;synchronized 可以修饰类、方法、变量。 + +* volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。 + +* volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。 + +* volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。 + +* volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。 + +#### final不可变对象,它对写并发应用有什么帮助? + +* 不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,反之即为可变对象(Mutable Objects)。 + +* 不可变对象的类即为不可变类(Immutable Class)。Java 平台类库中包含许多不可变类,如 String、基本类型的包装类、BigInteger 和 BigDecimal 等。 + +* 只有满足如下状态,一个对象才是不可变的; + + * 它的状态不能在创建后再被修改; + + * 所有域都是 final 类型;并且,它被正确创建(创建期间没有发生 this 引用的逸出)。 + +`不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。` + +#### Lock 接口和synchronized 对比同步它有什么优势? + +* Lock 接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。 + +* 它的优势有: + + * (1)可以使锁更公平 + + * (2)可以使线程在等待锁的时候响应中断 + + * (3)可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间 + + * (4)可以在不同的范围,以不同的顺序获取和释放锁 + +* 整体上来说 Lock 是 synchronized 的扩展版,Lock 提供了无条件的、可轮询的(tryLock 方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition 方法)锁操作。另外 Lock 的实现类基本都支持非公平锁(默认)和公平锁,synchronized 只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择。 + +#### 乐观锁和悲观锁的理解及如何实现,有哪些实现方式? + +* 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。 + +* 乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。在 Java中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。 + +#### 什么是 CAS + +* CAS 是 compare and swap 的缩写,即我们所说的比较交换。 + +* cas 是一种基于锁的操作,而且是乐观锁。在 java 中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加 version 来获取数据,性能较悲观锁有很大的提高。 + +* CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和 A 的值是一样的,那么就将内存里面的值更新成 B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a 线程获取地址里面的值被b 线程修改了,那么 a 线程需要自旋,到下次循环才有可能机会执行。 + +`java.util.concurrent.atomic 包下的类大多是使用 CAS 操作来实现的(AtomicInteger,AtomicBoolean,AtomicLong)` + +#### CAS 的会产生什么问题? + +* 1、ABA 问题: + + 比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但可能存在潜藏的问题。从 Java1.5 开始 JDK 的 atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。 + +* 2、循环时间长开销大: + + 对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。 + +* 3、只能保证一个共享变量的原子操作: + + 当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。 + +#### 什么是原子类 + +* java.util.concurrent.atomic包:是原子类的小工具包,支持在单个变量上解除锁的线程安全编程 原子变量类相当于一种泛化的 volatile 变量,能够支持原子的和有条件的读-改-写操作。 + +* 比如:AtomicInteger 表示一个int类型的值,并提供了 get 和 set 方法,这些 Volatile 类型的int变量在读取和写入上有着相同的内存语义。它还提供了一个原子的 compareAndSet 方法(如果该方法成功执行,那么将实现与读取/写入一个 volatile 变量相同的内存效果),以及原子的添加、递增和递减等方法。AtomicInteger 表面上非常像一个扩展的 Counter 类,但在发生竞争的情况下能提供更高的可伸缩性,因为它直接利用了硬件对并发的支持。 + +`简单来说就是原子类来实现CAS无锁模式的算法` + +#### 原子类的常用类 + +* AtomicBoolean +* AtomicInteger +* AtomicLong +* AtomicReference + +#### 说一下 Atomic的原理? + +* Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。 + +#### 死锁与活锁的区别,死锁与饥饿的区别? + +* 死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。 + +* 活锁:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。 + +* 活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,这就是所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。 + +* 饥饿:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。 + + Java 中导致饥饿的原因: + + * 1、高优先级线程吞噬所有的低优先级线程的 CPU 时间。 + + * 2、线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。 + + * 3、线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法),因为其他线程总是被持续地获得唤醒。 + +## 线程池 + +#### 什么是线程池? + +* Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来许多好处。 + * 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 + * 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。 + * 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用 + +#### 线程池作用? + +* 线程池是为突然大量爆发的线程设计的,通过有限的几个固定线程为大量的操作服务,减少了创建和销毁线程所需的时间,从而提高效率。 + +* 如果一个线程所需要执行的时间非常长的话,就没必要用线程池了(不是不能作长时间操作,而是不宜。本来降低线程创建和销毁,结果你那么久我还不好控制还不如直接创建线程),况且我们还不能控制线程池中线程的开始、挂起、和中止。 + +#### 线程池有什么优点? + +* 降低资源消耗:重用存在的线程,减少对象创建销毁的开销。 + +* 提高响应速度。可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。当任务到达时,任务可以不需要的等到线程创建就能立即执行。 + +* 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 + +* 附加功能:提供定时执行、定期执行、单线程、并发数控制等功能。 + +#### 什么是ThreadPoolExecutor? + +* **ThreadPoolExecutor就是线程池** + + ThreadPoolExecutor其实也是JAVA的一个类,我们一般通过Executors工厂类的方法,通过传入不同的参数,就可以构造出适用于不同应用场景下的ThreadPoolExecutor(线程池) + +构造参数图: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172babf4c562f1~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)`构造参数参数介绍:`corePoolSize 核心线程数量 +maximumPoolSize 最大线程数量 +keepAliveTime 线程保持时间,N个时间单位 +unit 时间单位(比如秒,分) +workQueue 阻塞队列 +threadFactory 线程工厂 +handler 线程池拒绝策略 +复制代码 +#### 什么是Executors? + +* **Executors框架实现的就是线程池的功能。** + + Executors工厂类中提供的newCachedThreadPool、newFixedThreadPool 、newScheduledThreadPool 、newSingleThreadExecutor 等方法其实也只是ThreadPoolExecutor的构造函数参数不同而已。通过传入不同的参数,就可以构造出适用于不同应用场景下的线程池, + +Executor工厂类如何创建线程池图: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bab8fc18fd3~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### 线程池四种创建方式? + +* **Java通过Executors(jdk1.5并发包)提供四种线程池,分别为:** + 1. newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。 + 2. newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。 + 3. newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。 + 4. newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。 + +#### 在 Java 中 Executor 和 Executors 的区别? + +* Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。 + +* Executor 接口对象能执行我们的线程任务。 + +* ExecutorService 接口继承了 Executor 接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。 + +* 使用 ThreadPoolExecutor 可以创建自定义线程池。 + +#### 四种构建线程池的区别及特点? + +##### 1\. newCachedThreadPool + +* **特点**:newCachedThreadPool创建一个可缓存线程池,如果当前线程池的长度超过了处理的需要时,它可以灵活的回收空闲的线程,当需要增加时, 它可以灵活的添加新的线程,而不会对池的长度作任何限制 + +* **缺点**:他虽然可以无线的新建线程,但是容易造成堆外内存溢出,因为它的最大值是在初始化的时候设置为 Integer.MAX_VALUE,一般来说机器都没那么大内存给它不断使用。当然知道可能出问题的点,就可以去重写一个方法限制一下这个最大值 + +* **总结**:线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。 + +* **代码示例:** + +```java +package com.lijie; + + import java.util.concurrent.ExecutorService; + import java.util.concurrent.Executors; + + public class TestNewCachedThreadPool { + public static void main(String[] args) { + // 创建无限大小线程池,由jvm自动回收 + ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(); + for (int i = 0; i < 10; i++) { + final int temp = i; + newCachedThreadPool.execute(new Runnable() { + public void run() { + try { + Thread.sleep(100); + } catch (Exception e) { + } + System.out.println(Thread.currentThread().getName() + ",i==" + temp); + } + }); + } + } + } + +``` + +##### 2.newFixedThreadPool + +* **特点**:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。定长线程池的大小最好根据系统资源进行设置。 + +* **缺点**:线程数量是固定的,但是阻塞队列是无界队列。如果有很多请求积压,阻塞队列越来越长,容易导致OOM(超出内存空间) + +* **总结**:请求的挤压一定要和分配的线程池大小匹配,定线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors() + +`Runtime.getRuntime().availableProcessors()方法是查看电脑CPU核心数量)` + +* **代码示例:** + +```java +package com.lijie; + + import java.util.concurrent.ExecutorService; + import java.util.concurrent.Executors; + + public class TestNewFixedThreadPool { + public static void main(String[] args) { + ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(3); + for (int i = 0; i < 10; i++) { + final int temp = i; + newFixedThreadPool.execute(new Runnable() { + public void run() { + System.out.println(Thread.currentThread().getName() + ",i==" + temp); + } + }); + } + } + } + +``` + +##### 3.newScheduledThreadPool + +* **特点**:创建一个固定长度的线程池,而且支持定时的以及周期性的任务执行,类似于Timer(Timer是Java的一个定时器类) + +* **缺点**:由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务(比如:一个任务出错,以后的任务都无法继续)。 + +* **代码示例:** + +```java +package com.lijie; + + import java.util.concurrent.Executors; + import java.util.concurrent.ScheduledExecutorService; + import java.util.concurrent.TimeUnit; + + public class TestNewScheduledThreadPool { + public static void main(String[] args) { + //定义线程池大小为3 + ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(3); + for (int i = 0; i < 10; i++) { + final int temp = i; + newScheduledThreadPool.schedule(new Runnable() { + public void run() { + System.out.println("i:" + temp); + } + }, 3, TimeUnit.SECONDS);//这里表示延迟3秒执行。 + } + } + } + +``` + +##### 4.newSingleThreadExecutor + +* **特点**:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它,他必须保证前一项任务执行完毕才能执行后一项。保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。 + +* **缺点**:缺点的话,很明显,他是单线程的,高并发业务下有点无力 + +* **总结**:保证所有任务按照指定顺序执行的,如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它 + +* **代码示例:** + +```java +package com.lijie; + + import java.util.concurrent.ExecutorService; + import java.util.concurrent.Executors; + + public class TestNewSingleThreadExecutor { + public static void main(String[] args) { + ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor(); + for (int i = 0; i < 10; i++) { + final int index = i; + newSingleThreadExecutor.execute(new Runnable() { + public void run() { + System.out.println(Thread.currentThread().getName() + " index:" + index); + try { + Thread.sleep(200); + } catch (Exception e) { + } + } + }); + } + } + } + +``` + +#### 线程池都有哪些状态? + +* RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。 +* SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。 +* STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。 +* TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。 +* TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。 + +#### 线程池中 submit() 和 execute() 方法有什么区别? + +* 相同点: + * 相同点就是都可以开启线程执行池中的任务。 +* 不同点: + * 接收参数:execute()只能执行 Runnable 类型的任务。submit()可以执行 Runnable 和 Callable 类型的任务。 + * 返回值:submit()方法可以返回持有计算结果的 Future 对象,而execute()没有 + * 异常处理:submit()方便Exception处理 + +#### 什么是线程组,为什么在 Java 中不推荐使用? + +* ThreadGroup 类,可以把线程归属到某一个线程组中,线程组中可以有线程对象,也可以有线程组,组中还可以有线程,这样的组织结构有点类似于树的形式。 + +* 线程组和线程池是两个不同的概念,他们的作用完全不同,前者是为了方便线程的管理,后者是为了管理线程的生命周期,复用线程,减少创建销毁线程的开销。 + +* 为什么不推荐使用线程组?因为使用有很多的安全隐患吧,没有具体追究,如果需要使用,推荐使用线程池。 + +#### ThreadPoolExecutor饱和策略有哪些? + +`如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,ThreadPoolTaskExecutor 定义一些策略:` + +* ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。 +* ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。 +* ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。 +* ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。 + +#### 如何自定义线程线程池? + +* 先看ThreadPoolExecutor(线程池)这个类的构造参数 + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bab8f7d464b~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)构造参数参数介绍: +```java + corePoolSize 核心线程数量 + maximumPoolSize 最大线程数量 + keepAliveTime 线程保持时间,N个时间单位 + unit 时间单位(比如秒,分) + workQueue 阻塞队列 + threadFactory 线程工厂 + handler 线程池拒绝策略 + +``` +* 代码示例: + +```java +package com.lijie; + + import java.util.concurrent.ArrayBlockingQueue; + import java.util.concurrent.ThreadPoolExecutor; + import java.util.concurrent.TimeUnit; + + public class Test001 { + public static void main(String[] args) { + //创建线程池 + ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 2, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3)); + for (int i = 1; i <= 6; i++) { + TaskThred t1 = new TaskThred("任务" + i); + //executor.execute(t1);是执行线程方法 + executor.execute(t1); + } + //executor.shutdown()不再接受新的任务,并且等待之前提交的任务都执行完再关闭,阻塞队列中的任务不会再执行。 + executor.shutdown(); + } + } + + class TaskThred implements Runnable { + private String taskName; + + public TaskThred(String taskName) { + this.taskName = taskName; + } + public void run() { + System.out.println(Thread.currentThread().getName() + taskName); + } + } + +``` + +#### 线程池的执行原理? + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bac2446a113~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 提交一个任务到线程池中,线程池的处理流程如下: + + 1. 判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。 + + 2. 线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。 + + 3. 判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。 + +#### 如何合理分配线程池大小? + +* 要合理的分配线程池的大小要根据实际情况来定,简单的来说的话就是根据CPU密集和IO密集来分配 + +##### 什么是CPU密集 + +* CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。 + +* CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程,该任务都不可能得到加速,因为CPU总的运算能力就那样。 + +##### 什么是IO密集 + +* IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。所以在IO密集型任务中使用多线程可以大大的加速程序运行,即时在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。 + +##### 分配CPU和IO密集: + +1. CPU密集型时,任务可以少配置线程数,大概和机器的cpu核数相当,这样可以使得每个线程都在执行任务 + +2. IO密集型时,大部分线程都阻塞,故需要多配置线程数,2*cpu核数 + +##### 精确来说的话的话: + +* 从以下几个角度分析任务的特性: + + * 任务的性质:CPU密集型任务、IO密集型任务、混合型任务。 + + * 任务的优先级:高、中、低。 + + * 任务的执行时间:长、中、短。 + + * 任务的依赖性:是否依赖其他系统资源,如数据库连接等。 + +**可以得出一个结论:** + +* 线程等待时间比CPU执行时间比例越高,需要越多线程。 +* 线程CPU执行时间比等待时间比例越高,需要越少线程。 + +## 并发容器 + +#### 你经常使用什么并发容器,为什么? + +* 答:Vector、ConcurrentHashMap、HasTable + +* 一般软件开发中容器用的最多的就是HashMap、ArrayList,LinkedList ,等等 + +* 但是在多线程开发中就不能乱用容器,如果使用了未加锁(非同步)的的集合,你的数据就会非常的混乱。由此在多线程开发中需要使用的容器必须是加锁(同步)的容器。 + +#### 什么是Vector + +* Vector与ArrayList一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,访问它比访问ArrayList慢很多 + + (`ArrayList是最常用的List实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。当从ArrayList的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。ArrayList的缺点是每个元素之间不能有间隔。`) + +#### ArrayList和Vector有什么不同之处? + +* Vector方法带上了synchronized关键字,是线程同步的 + +1. ArrayList添加方法源码![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bac57d8b760~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +2. Vector添加源码(加锁了synchronized关键字)![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bac5bc35f22~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +#### 为什么HashTable是线程安全的? + +* 因为HasTable的内部方法都被synchronized修饰了,所以是线程安全的。其他的都和HashMap一样 + +1. HashMap添加方法的源码![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bac5c47d65f~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +2. HashTable添加方法的源码![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bac599568da~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +#### 用过ConcurrentHashMap,讲一下他和HashTable的不同之处? + +* ConcurrentHashMap是Java5中支持高并发、高吞吐量的线程安全HashMap实现。它由Segment数组结构和HashEntry数组结构组成。Segment数组在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键-值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构;一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素;每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。 + +* 看不懂???很正常,我也看不懂 + +* 总结: + + 1. HashTable就是实现了HashMap加上了synchronized,而ConcurrentHashMap底层采用分段的数组+链表实现,线程安全 + 2. ConcurrentHashMap通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。 + 3. 并且读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。 + 4. Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术 + 5. 扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容 + +#### Collections.synchronized * 是什么? + +`注意:* 号代表后面是还有内容的` + +* 此方法是干什么的呢,他完完全全的可以把List、Map、Set接口底下的集合变成线程安全的集合 + +* Collections.synchronized * :原理是什么,我猜的话是代理模式:[Java代理模式理解](https://link.juejin.cn?target=https%3A%2F%2Fblog.csdn.net%2Fweixin_43122090%2Farticle%2Fdetails%2F104883274 "https://blog.csdn.net/weixin_43122090/article/details/104883274") + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172bac6e2aff60~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### Java 中 ConcurrentHashMap 的并发度是什么? + +* ConcurrentHashMap 把实际 map 划分成若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度获得的,它是 ConcurrentHashMap 类构造函数的一个可选参数,默认值为 16,这样在多线程情况下就能避免争用。 + +* 在 JDK8 后,它摒弃了 Segment(锁段)的概念,而是启用了一种全新的方式实现,利用 CAS 算法。同时加入了更多的辅助变量来提高并发度,具体内容还是查看源码吧。 + +#### 什么是并发容器的实现? + +* 何为同步容器:可以简单地理解为通过 synchronized 来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。比如 Vector,Hashtable,以及 Collections.synchronizedSet,synchronizedList 等方法返回的容器。可以通过查看 Vector,Hashtable 等这些同步容器的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并在需要同步的方法上加上关键字 synchronized。 + +* 并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性,例如在 ConcurrentHashMap 中采用了一种粒度更细的加锁机制,可以称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问 map,并且执行读操作的线程和写操作的线程也可以并发的访问 map,同时允许一定数量的写操作线程并发地修改 map,所以它可以在并发环境下实现更高的吞吐量。 + +#### Java 中的同步集合与并发集合有什么区别? + +* 同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。在 Java1.5 之前程序员们只有同步集合来用且在多线程并发的时候会导致争用,阻碍了系统的扩展性。Java5 介绍了并发集合像ConcurrentHashMap,不仅提供线程安全还用锁分离和内部分区等现代技术提高了可扩展性。 + +#### SynchronizedMap 和 ConcurrentHashMap 有什么区别? + +* SynchronizedMap 一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为 map。 + +* ConcurrentHashMap 使用分段锁来保证在多线程下的性能。 + +* ConcurrentHashMap 中则是一次锁住一个桶。ConcurrentHashMap 默认将hash 表分为 16 个桶,诸如 get,put,remove 等常用操作只锁当前需要用到的桶。 + +* 这样,原来只能一个线程进入,现在却能同时有 16 个写线程执行,并发性能的提升是显而易见的。 + +* 另外 ConcurrentHashMap 使用了一种不同的迭代方式。在这种迭代方式中,当iterator 被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时 new 新的数据从而不影响原有的数据,iterator 完成后再将头指针替换为新的数据 ,这样 iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。 + +#### CopyOnWriteArrayList 是什么? + +* CopyOnWriteArrayList 是一个并发容器。有很多人称它是线程安全的,我认为这句话不严谨,缺少一个前提条件,那就是非复合场景下操作它是线程安全的。 + +* CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出 ConcurrentModificationException。在CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。 + +#### CopyOnWriteArrayList 的使用场景? + +* 合适读多写少的场景。 + +#### CopyOnWriteArrayList 的缺点? + +* 由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致 young gc 或者 full gc。 +* 不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求。 +* 由于实际使用中可能没法保证 CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次 add/set 都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。 + +#### CopyOnWriteArrayList 的设计思想? + +* 读写分离,读和写分开 +* 最终一致性 +* 使用另外开辟空间的思路,来解决并发冲突 + +## 并发队列 + +#### 什么是并发队列: + +* 消息队列很多人知道:消息队列是分布式系统中重要的组件,是系统与系统直接的通信 + +* 并发队列是什么:并发队列多个线程以有次序共享数据的重要组件 + +#### 并发队列和并发集合的区别: + +`那就有可能要说了,我们并发集合不是也可以实现多线程之间的数据共享吗,其实也是有区别的:` + +* 队列遵循“先进先出”的规则,可以想象成排队检票,队列一般用来解决大数据量采集处理和显示的。 + +* 并发集合就是在多个线程中共享数据的 + +#### 怎么判断并发队列是阻塞队列还是非阻塞队列 + +* 在并发队列上JDK提供了Queue接口,一个是以Queue接口下的BlockingQueue接口为代表的阻塞队列,另一个是高性能(无堵塞)队列。 + +#### 阻塞队列和非阻塞队列区别 + +* 当队列阻塞队列为空的时,从队列中获取元素的操作将会被阻塞。 + +* 或者当阻塞队列是满时,往队列里添加元素的操作会被阻塞。 + +* 或者试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。 + +* 试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程使队列重新变得空闲起来 + +#### 常用并发列队的介绍: + +1. **非堵塞队列:** + + 1. **ArrayDeque, (数组双端队列)** + + ArrayDeque (非堵塞队列)是JDK容器中的一个双端队列实现,内部使用数组进行元素存储,不允许存储null值,可以高效的进行元素查找和尾部插入取出,是用作队列、双端队列、栈的绝佳选择,性能比LinkedList还要好。 + + 2. **PriorityQueue, (优先级队列)** + + PriorityQueue (非堵塞队列) 一个基于优先级的无界优先级队列。优先级队列的元素按照其自然顺序进行排序,或者根据构造队列时提供的 Comparator 进行排序,具体取决于所使用的构造方法。该队列不允许使用 null 元素也不允许插入不可比较的对象 + + 3. **ConcurrentLinkedQueue, (基于链表的并发队列)** + + ConcurrentLinkedQueue (非堵塞队列): 是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能。ConcurrentLinkedQueue的性能要好于BlockingQueue接口,它是一个基于链接节点的无界线程安全队列。该队列的元素遵循先进先出的原则。该队列不允许null元素。 + +2. **堵塞队列:** + + 1. **DelayQueue, (基于时间优先级的队列,延期阻塞队列)** + + DelayQueue是一个没有边界BlockingQueue实现,加入其中的元素必需实现Delayed接口。当生产者线程调用put之类的方法加入元素时,会触发Delayed接口中的compareTo方法进行排序,也就是说队列中元素的顺序是按到期时间排序的,而非它们进入队列的顺序。排在队列头部的元素是最早到期的,越往后到期时间赿晚。 + + 2. **ArrayBlockingQueue, (基于数组的并发阻塞队列)** + + ArrayBlockingQueue是一个有边界的阻塞队列,它的内部实现是一个数组。有边界的意思是它的容量是有限的,我们必须在其初始化的时候指定它的容量大小,容量大小一旦指定就不可改变。ArrayBlockingQueue是以先进先出的方式存储数据 + + 3. **LinkedBlockingQueue, (基于链表的FIFO阻塞队列)** + + LinkedBlockingQueue阻塞队列大小的配置是可选的,如果我们初始化时指定一个大小,它就是有边界的,如果不指定,它就是无边界的。说是无边界,其实是采用了默认大小为Integer.MAX_VALUE的容量 。它的内部实现是一个链表。 + + 4. **LinkedBlockingDeque, (基于链表的FIFO双端阻塞队列)** + + LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列,即可以从队列的两端插入和移除元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。 + + 相比于其他阻塞队列,LinkedBlockingDeque多了addFirst、addLast、peekFirst、peekLast等方法,以first结尾的方法,表示插入、获取获移除双端队列的第一个元素。以last结尾的方法,表示插入、获取获移除双端队列的最后一个元素。 + + LinkedBlockingDeque是可选容量的,在初始化时可以设置容量防止其过度膨胀,如果不设置,默认容量大小为Integer.MAX_VALUE。 + + 5. **PriorityBlockingQueue, (带优先级的无界阻塞队列)** + + priorityBlockingQueue是一个无界队列,它没有限制,在内存允许的情况下可以无限添加元素;它又是具有优先级的队列,是通过构造函数传入的对象来判断,传入的对象必须实现comparable接口。 + + 6. **SynchronousQueue (并发同步阻塞队列)** + + SynchronousQueue是一个内部只能包含一个元素的队列。插入元素到队列的线程被阻塞,直到另一个线程从队列中获取了队列中存储的元素。同样,如果线程尝试获取元素并且当前不存在任何元素,则该线程将被阻塞,直到线程将元素插入队列。 + + 将这个类称为队列有点夸大其词。这更像是一个点。 + +#### 并发队列的常用方法 + +`不管是那种列队,是那个类,当是他们使用的方法都是差不多的` + +| 方法名 | 描述 | +| --- | --- | +| add() | 在不超出队列长度的情况下插入元素,可以立即执行,成功返回true,如果队列满了就抛出异常。 | +| offer() | 在不超出队列长度的情况下插入元素的时候则可以立即在队列的尾部插入指定元素,成功时返回true,如果此队列已满,则返回false。 | +| put() | 插入元素的时候,如果队列满了就进行等待,直到队列可用。 | +| take() | 从队列中获取值,如果队列中没有值,线程会一直阻塞,直到队列中有值,并且该方法取得了该值。 | +| poll(long timeout, TimeUnit unit) | 在给定的时间里,从队列中获取值,如果没有取到会抛出异常。 | +| remainingCapacity() | 获取队列中剩余的空间。 | +| remove(Object o) | 从队列中移除指定的值。 | +| contains(Object o) | 判断队列中是否拥有该值。 | +| drainTo(Collection c) | 将队列中值,全部移除,并发设置到给定的集合中。 | + +## 并发工具类 + +#### 常用的并发工具类有哪些? + +* CountDownLatch + + CountDownLatch 类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他3个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。 + +* CyclicBarrier (回环栅栏) CyclicBarrier它的作用就是会让所有线程都等待完成后才会继续下一步行动。 + + CyclicBarrier初始化时规定一个数目,然后计算调用了CyclicBarrier.await()进入等待的线程数。当线程数达到了这个数目时,所有进入等待状态的线程被唤醒并继续。 + + CyclicBarrier初始时还可带一个Runnable的参数, 此Runnable任务在CyclicBarrier的数目达到后,所有其它线程被唤醒前被执行。 + +* Semaphore (信号量) Semaphore 是 synchronized 的加强版,作用是控制线程的并发数量(允许自定义多少线程同时访问)。就这一点而言,单纯的synchronized 关键字是实现不了的。 + + Semaphore是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程竞争获取许可信号,做自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。Semaphore可以用来构建一些对象池,资源池之类的,比如数据库连接池,我们也可以创建计数为1的Semaphore,将其作为一种类似互斥锁的机制,这也叫二元信号量,表示两种互斥状态。它的用法如下: + +作者:小杰要吃蛋 +链接:https://juejin.cn/post/6844904125755293710 +来源:稀土掘金 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 \ No newline at end of file diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\345\274\202\345\270\270.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\345\274\202\345\270\270.md" new file mode 100644 index 0000000..566eeaa --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\345\274\202\345\270\270.md" @@ -0,0 +1,739 @@ + + +## Java异常架构与异常关键字 + +### Java异常简介 + +* Java异常是Java提供的一种识别及响应错误的一致性机制。 + Java异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅,并提高程序健壮性。在有效使用异常的情况下,异常能清晰的回答what, where, why这3个问题:异常类型回答了“什么”被抛出,异常堆栈跟踪回答了“在哪”抛出,异常信息回答了“为什么”会抛出。 + +### Java异常架构 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/1717840de0260d11~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### 1\. Throwable + +* Throwable 是 Java 语言中所有错误与异常的超类。 + +* Throwable 包含两个子类:Error(错误)和 Exception(异常),它们通常用于指示发生了异常情况。 + +* Throwable 包含了其线程创建时线程执行堆栈的快照,它提供了 printStackTrace() 等接口用于获取堆栈跟踪数据等信息。 + +#### 2\. Error(错误) + +* **定义**:Error 类及其子类。程序中无法处理的错误,表示运行应用程序中出现了严重的错误。 + +* **特点**:此类错误一般表示代码运行时 JVM 出现问题。通常有 Virtual MachineError(虚拟机运行错误)、NoClassDefFoundError(类定义错误)等。比如 OutOfMemoryError:内存不足错误;StackOverflowError:栈溢出错误。此类错误发生时,JVM 将终止线程。 + +* 这些错误是不受检异常,非代码性错误。因此,当此类错误发生时,应用程序不应该去处理此类错误。按照Java惯例,我们是不应该实现任何新的Error子类的! + +#### 3\. Exception(异常) + +* 程序本身可以捕获并且可以处理的异常。Exception 这种异常又分为两类:运行时异常和编译时异常。 + +##### 运行时异常 + +* **定义**:RuntimeException 类及其子类,表示 JVM 在运行期间可能出现的异常。 + +* **特点**:Java 编译器不会检查它。也就是说,当程序中可能出现这类异常时,倘若既"没有通过throws声明抛出它",也"没有用try-catch语句捕获它",还是会编译通过。比如NullPointerException空指针异常、ArrayIndexOutBoundException数组下标越界异常、ClassCastException类型转换异常、ArithmeticExecption算术异常。此类异常属于不受检异常,一般是由程序逻辑错误引起的,在程序中可以选择捕获处理,也可以不处理。虽然 Java 编译器不会检查运行时异常,但是我们也可以通过 throws 进行声明抛出,也可以通过 try-catch 对它进行捕获处理。如果产生运行时异常,则需要通过修改代码来进行避免。例如,若会发生除数为零的情况,则需要通过代码避免该情况的发生! + +* RuntimeException 异常会由 Java 虚拟机自动抛出并自动捕获(**就算我们没写异常捕获语句运行时也会抛出错误**!!),此类异常的出现绝大数情况是代码本身有问题应该从逻辑上去解决并改进代码。 + +##### 编译时异常 + +* **定义**: Exception 中除 RuntimeException 及其子类之外的异常。 + +* **特点**: Java 编译器会检查它。如果程序中出现此类异常,比如 ClassNotFoundException(没有找到指定的类异常),IOException(IO流异常),要么通过throws进行声明抛出,要么通过try-catch进行捕获处理,否则不能通过编译。在程序中,通常不会自定义该类异常,而是直接使用系统提供的异常类。**该异常我们必须手动在代码里添加捕获语句来处理该异常**。 + +#### 4\. 受检异常与非受检异常 + +* Java 的所有异常可以分为受检异常(checked exception)和非受检异常(unchecked exception)。 + +##### 受检异常 + +* 编译器要求必须处理的异常。正确的程序在运行过程中,经常容易出现的、符合预期的异常情况。一旦发生此类异常,就必须采用某种方式进行处理。**除 RuntimeException 及其子类外,其他的 Exception 异常都属于受检异常**。编译器会检查此类异常,也就是说当编译器检查到应用中的某处可能会此类异常时,将会提示你处理本异常——要么使用try-catch捕获,要么使用方法签名中用 throws 关键字抛出,否则编译不通过。 + +##### 非受检异常 + +* 编译器不会进行检查并且不要求必须处理的异常,也就说当程序中出现此类异常时,即使我们没有try-catch捕获它,也没有使用throws抛出该异常,编译也会正常通过。**该类异常包括运行时异常(RuntimeException极其子类)和错误(Error)。** + +### Java异常关键字 + +* **try** – 用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。 +* **catch** – 用于捕获异常。catch用来捕获try语句块中发生的异常。 +* **finally** – finally语句块总是会被执行。它主要用于回收在try块里打开的物力资源(如数据库连接、网络连接和磁盘文件)。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。 +* **throw** – 用于抛出异常。 +* **throws** – 用在方法签名中,用于声明该方法可能抛出的异常。 + +## Java异常处理 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/1717840de2de5ccf~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* Java 通过面向对象的方法进行异常处理,一旦方法抛出异常,系统自动根据该异常对象寻找合适异常处理器(Exception Handler)来处理该异常,把各种不同的异常进行分类,并提供了良好的接口。在 Java 中,每个异常都是一个对象,它是 Throwable 类或其子类的实例。当一个方法出现异常后便抛出一个异常对象,该对象中包含有异常信息,调用这个对象的方法可以捕获到这个异常并可以对其进行处理。Java 的异常处理是通过 5 个关键词来实现的:try、catch、throw、throws 和 finally。 + +* 在Java应用中,异常的处理机制分为声明异常,抛出异常和捕获异常。 + +### 声明异常 + +* 通常,应该捕获那些知道如何处理的异常,将不知道如何处理的异常继续传递下去。传递异常可以在方法签名处使用 **throws** 关键字声明可能会抛出的异常。 + +**注意** + +* 非检查异常(Error、RuntimeException 或它们的子类)不可使用 throws 关键字来声明要抛出的异常。 +* 一个方法出现编译时异常,就需要 try-catch/ throws 处理,否则会导致编译错误。 + +### 抛出异常 + +* 如果你觉得解决不了某些异常问题,且不需要调用者处理,那么你可以抛出异常。 + +* throw关键字作用是在方法内部抛出一个`Throwable`类型的异常。任何Java代码都可以通过throw语句抛出异常。 + +### 捕获异常 + +* 程序通常在运行之前不报错,但是运行后可能会出现某些未知的错误,但是还不想直接抛出到上一级,那么就需要通过try…catch…的形式进行异常捕获,之后根据不同的异常情况来进行相应的处理。 + +### 如何选择异常类型 + +* 可以根据下图来选择是捕获异常,声明异常还是抛出异常 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/1717840de32f26f7~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +### 常见异常处理方式 + +#### 直接抛出异常 + +* 通常,应该捕获那些知道如何处理的异常,将不知道如何处理的异常继续传递下去。传递异常可以在方法签名处使用 **throws** 关键字声明可能会抛出的异常。 + +private static void readFile(String filePath) throws IOException { + File file = new File(filePath); + String result; + BufferedReader reader = new BufferedReader(new FileReader(file)); + while((result = reader.readLine())!=null) { + System.out.println(result); + } + reader.close(); +} +复制代码 +#### 封装异常再抛出 + +* 有时我们会从 catch 中抛出一个异常,目的是为了改变异常的类型。多用于在多系统集成时,当某个子系统故障,异常类型可能有多种,可以用统一的异常类型向外暴露,不需暴露太多内部异常细节。 + +private static void readFile(String filePath) throws MyException { + try { + // code + } catch (IOException e) { + MyException ex = new MyException("read file failed."); + ex.initCause(e); + throw ex; + } +} +复制代码 +#### 捕获异常 + +* 在一个 try-catch 语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理 + +private static void readFile(String filePath) { + try { + // code + } catch (FileNotFoundException e) { + // handle FileNotFoundException + } catch (IOException e){ + // handle IOException + } +} +复制代码 + +* 同一个 catch 也可以捕获多种类型异常,用 | 隔开 + +private static void readFile(String filePath) { + try { + // code + } catch (FileNotFoundException | UnknownHostException e) { + // handle FileNotFoundException or UnknownHostException + } catch (IOException e){ + // handle IOException + } +} +复制代码 +#### 自定义异常 + +* 习惯上,定义一个异常类应包含两个构造函数,一个无参构造函数和一个带有详细描述信息的构造函数(Throwable 的 toString 方法会打印这些详细信息,调试时很有用) + +public class MyException extends Exception { + public MyException(){ } + public MyException(String msg){ + super(msg); + } + // ... +} +复制代码 +#### try-catch-finally + +* 当方法中发生异常,异常处之后的代码不会再执行,如果之前获取了一些本地资源需要释放,则需要在方法正常结束时和 catch 语句中都调用释放本地资源的代码,显得代码比较繁琐,finally 语句可以解决这个问题。 + +private static void readFile(String filePath) throws MyException { + File file = new File(filePath); + String result; + BufferedReader reader = null; + try { + reader = new BufferedReader(new FileReader(file)); + while((result = reader.readLine())!=null) { + System.out.println(result); + } + } catch (IOException e) { + System.out.println("readFile method catch block."); + MyException ex = new MyException("read file failed."); + ex.initCause(e); + throw ex; + } finally { + System.out.println("readFile method finally block."); + if (null != reader) { + try { + reader.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} +复制代码 + +* 调用该方法时,读取文件时若发生异常,代码会进入 catch 代码块,之后进入 finally 代码块;若读取文件时未发生异常,则会跳过 catch 代码块直接进入 finally 代码块。所以无论代码中是否发生异常,fianlly 中的代码都会执行。 + +* 若 catch 代码块中包含 return 语句,finally 中的代码还会执行吗?将以上代码中的 catch 子句修改如下: + +catch (IOException e) { + System.out.println("readFile method catch block."); + return; +} +复制代码 + +* 调用 readFile 方法,观察当 catch 子句中调用 return 语句时,finally 子句是否执行 + +readFile method catch block. +readFile method finally block. +复制代码 + +* 可见,即使 catch 中包含了 return 语句,finally 子句依然会执行。若 finally 中也包含 return 语句,finally 中的 return 会覆盖前面的 return. + +#### try-with-resource + +* 上面例子中,finally 中的 close 方法也可能抛出 IOException, 从而覆盖了原始异常。JAVA 7 提供了更优雅的方式来实现资源的自动释放,自动释放的资源需要是实现了 AutoCloseable 接口的类。 + +private static void tryWithResourceTest(){ + try (Scanner scanner = new Scanner(new FileInputStream("c:/abc"),"UTF-8")){ + // code + } catch (IOException e){ + // handle exception + } +} +复制代码 + +* try 代码块退出时,会自动调用 scanner.close 方法,和把 scanner.close 方法放在 finally 代码块中不同的是,若 scanner.close 抛出异常,则会被抑制,抛出的仍然为原始异常。被抑制的异常会由 addSusppressed 方法添加到原来的异常,如果想要获取被抑制的异常列表,可以调用 getSuppressed 方法来获取。 + +## Java异常常见面试题 + +### 1\. Error 和 Exception 区别是什么? + +* Error 类型的错误通常为虚拟机相关错误,如系统崩溃,内存不足,堆栈溢出等,编译器不会对这类错误进行检测,JAVA 应用程序也不应对这类错误进行捕获,一旦这类错误发生,通常应用程序会被终止,仅靠应用程序本身无法恢复; + +* Exception 类的错误是可以在应用程序中进行捕获并处理的,通常遇到这种错误,应对其进行处理,使应用程序可以继续正常运行。 + +### 2\. 运行时异常和一般异常(受检异常)区别是什么? + +* 运行时异常包括 RuntimeException 类及其子类,表示 JVM 在运行期间可能出现的异常。 Java 编译器不会检查运行时异常。 + +* 受检异常是Exception 中除 RuntimeException 及其子类之外的异常。 Java 编译器会检查受检异常。 + +* **RuntimeException异常和受检异常之间的区别**:是否强制要求调用者必须处理此异常,如果强制要求调用者必须进行处理,那么就使用受检异常,否则就选择非受检异常(RuntimeException)。一般来讲,如果没有特殊的要求,我们建议使用RuntimeException异常。 + +### 3\. JVM 是如何处理异常的? + +* 在一个方法中如果发生异常,这个方法会创建一个异常对象,并转交给 JVM,该异常对象包含异常名称,异常描述以及异常发生时应用程序的状态。创建异常对象并转交给 JVM 的过程称为抛出异常。可能有一系列的方法调用,最终才进入抛出异常的方法,这一系列方法调用的有序列表叫做调用栈。 + +* JVM 会顺着调用栈去查找看是否有可以处理异常的代码,如果有,则调用异常处理代码。当 JVM 发现可以处理异常的代码时,会把发生的异常传递给它。如果 JVM 没有找到可以处理该异常的代码块,JVM 就会将该异常转交给默认的异常处理器(默认处理器为 JVM 的一部分),默认异常处理器打印出异常信息并终止应用程序。 + +### 4\. throw 和 throws 的区别是什么? + +* Java 中的异常处理除了包括捕获异常和处理异常之外,还包括声明异常和拋出异常,可以通过 throws 关键字在方法上声明该方法要拋出的异常,或者在方法内部通过 throw 拋出异常对象。 + +**throws 关键字和 throw 关键字在使用上的几点区别如下**: + +* throw 关键字用在方法内部,只能用于抛出一种异常,用来抛出方法或代码块中的异常,受查异常和非受查异常都可以被抛出。 +* throws 关键字用在方法声明上,可以抛出多个异常,用来标识该方法可能抛出的异常列表。一个方法用 throws 标识了可能抛出的异常列表,调用该方法的方法中必须包含可处理异常的代码,否则也要在方法签名中用 throws 关键字声明相应的异常。 + +### 5\. final、finally、finalize 有什么区别? + +* final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。 +* finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。 +* finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,Java 中允许使用 finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。 + +### 6\. NoClassDefFoundError 和 ClassNotFoundException 区别? + +* NoClassDefFoundError 是一个 Error 类型的异常,是由 JVM 引起的,不应该尝试捕获这个异常。 + +* 引起该异常的原因是 JVM 或 ClassLoader 尝试加载某类时在内存中找不到该类的定义,该动作发生在运行期间,即编译时该类存在,但是在运行时却找不到了,可能是变异后被删除了等原因导致; + +* ClassNotFoundException 是一个受查异常,需要显式地使用 try-catch 对其进行捕获和处理,或在方法签名中用 throws 关键字进行声明。当使用 Class.forName, ClassLoader.loadClass 或 ClassLoader.findSystemClass 动态加载类到内存的时候,通过传入的类路径参数没有找到该类,就会抛出该异常;另一种抛出该异常的可能原因是某个类已经由一个类加载器加载至内存中,另一个加载器又尝试去加载它。 + +### 7\. try-catch-finally 中哪个部分可以省略? + +* 答:catch 可以省略 + +**原因** + +* 更为严格的说法其实是:try只适合处理运行时异常,try+catch适合处理运行时异常+普通异常。也就是说,如果你只用try去处理普通异常却不加以catch处理,编译是通不过的,因为编译器硬性规定,普通异常如果选择捕获,则必须用catch显示声明以便进一步处理。而运行时异常在编译时没有如此规定,所以catch可以省略,你加上catch编译器也觉得无可厚非。 + +* 理论上,编译器看任何代码都不顺眼,都觉得可能有潜在的问题,所以你即使对所有代码加上try,代码在运行期时也只不过是在正常运行的基础上加一层皮。但是你一旦对一段代码加上try,就等于显示地承诺编译器,对这段代码可能抛出的异常进行捕获而非向上抛出处理。如果是普通异常,编译器要求必须用catch捕获以便进一步处理;如果运行时异常,捕获然后丢弃并且+finally扫尾处理,或者加上catch捕获以便进一步处理。 + +* 至于加上finally,则是在不管有没捕获异常,都要进行的“扫尾”处理。 + +### 8\. try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗? + +* 答:会执行,在 return 前执行。 + +* **注意**:在 finally 中改变返回值的做法是不好的,因为如果存在 finally 代码块,try中的 return 语句不会立马返回调用者,而是记录下返回值待 finally 代码块执行完毕之后再向调用者返回其值,然后如果在 finally 中修改了返回值,就会返回修改后的值。显然,在 finally 中返回或者修改返回值会对程序造成很大的困扰,C#中直接用编译错误的方式来阻止程序员干这种龌龊的事情,Java 中也可以通过提升编译器的语法检查级别来产生警告或错误。 + +**代码示例1:** + +public static int getInt() { + int a = 10; + try { + System.out.println(a / 0); + a = 20; + } catch (ArithmeticException e) { + a = 30; + return a; + /* + * return a 在程序执行到这一步的时候,这里不是return a 而是 return 30;这个返回路径就形成了 + * 但是呢,它发现后面还有finally,所以继续执行finally的内容,a=40 + * 再次回到以前的路径,继续走return 30,形成返回路径之后,这里的a就不是a变量了,而是常量30 + */ + } finally { + a = 40; + } + return a; +} +复制代码 + +* 执行结果:30 + +**代码示例2:** + +public static int getInt() { + int a = 10; + try { + System.out.println(a / 0); + a = 20; + } catch (ArithmeticException e) { + a = 30; + return a; + } finally { + a = 40; + //如果这样,就又重新形成了一条返回路径,由于只能通过1个return返回,所以这里直接返回40 + return a; + } + +} +复制代码 + +* 执行结果:40 + +### 9\. 类 ExampleA 继承 Exception,类 ExampleB 继承ExampleA。 + +* 有如下代码片断: + +try { + throw new ExampleB("b") +} catch(ExampleA e){ + System.out.println("ExampleA"); +} catch(Exception e){ + System.out.println("Exception"); +} +复制代码 + +* 请问执行此段代码的输出是什么? + +* **答**:输出:ExampleA。(根据里氏代换原则[能使用父类型的地方一定能使用子类型],抓取 ExampleA 类型异常的 catch 块能够抓住 try 块中抛出的 ExampleB 类型的异常) + +* 面试题 - 说出下面代码的运行结果。(此题的出处是《Java 编程思想》一书) + +class Annoyance extends Exception { +} +class Sneeze extends Annoyance { +} +class Human { + public static void main(String[] args) + throws Exception { + try { + try { + throw new Sneeze(); + } catch ( Annoyance a ) { + System.out.println("Caught Annoyance"); + throw a; + } + } catch ( Sneeze s ) { + System.out.println("Caught Sneeze"); + return ; + } finally { + System.out.println("Hello World!"); + } + } +} +复制代码 + +* 结果 + +Caught Annoyance +Caught Sneeze +Hello World! +复制代码 +### 10\. 常见的 RuntimeException 有哪些? + +* ClassCastException(类转换异常) +* IndexOutOfBoundsException(数组越界) +* NullPointerException(空指针) +* ArrayStoreException(数据存储异常,操作数组时类型不一致) +* 还有IO操作的BufferOverflowException异常 + +### 11\. Java常见异常有哪些 + +* java.lang.IllegalAccessError:违法访问错误。当一个应用试图访问、修改某个类的域(Field)或者调用其方法,但是又违反域或方法的可见性声明,则抛出该异常。 + +* java.lang.InstantiationError:实例化错误。当一个应用试图通过Java的new操作符构造一个抽象类或者接口时抛出该异常. + +* java.lang.OutOfMemoryError:内存不足错误。当可用内存不足以让Java虚拟机分配给一个对象时抛出该错误。 + +* java.lang.StackOverflowError:堆栈溢出错误。当一个应用递归调用的层次太深而导致堆栈溢出或者陷入死循环时抛出该错误。 + +* java.lang.ClassCastException:类造型异常。假设有类A和B(A不是B的父类或子类),O是A的实例,那么当强制将O构造为类B的实例时抛出该异常。该异常经常被称为强制类型转换异常。 + +* java.lang.ClassNotFoundException:找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常。 + +* java.lang.ArithmeticException:算术条件异常。譬如:整数除零等。 + +* java.lang.ArrayIndexOutOfBoundsException:数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。 + +* java.lang.IndexOutOfBoundsException:索引越界异常。当访问某个序列的索引值小于0或大于等于序列大小时,抛出该异常。 + +* java.lang.InstantiationException:实例化异常。当试图通过newInstance()方法创建某个类的实例,而该类是一个抽象类或接口时,抛出该异常。 + +* java.lang.NoSuchFieldException:属性不存在异常。当访问某个类的不存在的属性时抛出该异常。 + +* java.lang.NoSuchMethodException:方法不存在异常。当访问某个类的不存在的方法时抛出该异常。- + +* java.lang.NullPointerException:空指针异常。当应用试图在要求使用对象的地方使用了null时,抛出该异常。譬如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等。 + +* java.lang.NumberFormatException:数字格式异常。当试图将一个String转换为指定的数字类型,而该字符串确不满足数字类型要求的格式时,抛出该异常。 + +* java.lang.StringIndexOutOfBoundsException:字符串索引越界异常。当使用索引值访问某个字符串中的字符,而该索引值小于0或大于等于序列大小时,抛出该异常。 + +## Java异常处理最佳实践 + +* 在 Java 中处理异常并不是一个简单的事情。不仅仅初学者很难理解,即使一些有经验的开发者也需要花费很多时间来思考如何处理异常,包括需要处理哪些异常,怎样处理等等。这也是绝大多数开发团队都会制定一些规则来规范进行异常处理的原因。而团队之间的这些规范往往是截然不同的。 + +* 本文给出几个被很多团队使用的异常处理最佳实践。 + +### 1\. 在 finally 块中清理资源或者使用 try-with-resource 语句 + +* 当使用类似InputStream这种需要使用后关闭的资源时,一个常见的错误就是在try块的最后关闭资源。 + +public void doNotCloseResourceInTry() { + FileInputStream inputStream = null; + try { + File file = new File("./tmp.txt"); + inputStream = new FileInputStream(file); + // use the inputStream to read a file + // do NOT do this + inputStream.close(); + } catch (FileNotFoundException e) { + log.error(e); + } catch (IOException e) { + log.error(e); + } +} +复制代码 + +* 问题就是,只有没有异常抛出的时候,这段代码才可以正常工作。try 代码块内代码会正常执行,并且资源可以正常关闭。但是,使用 try 代码块是有原因的,一般调用一个或多个可能抛出异常的方法,而且,你自己也可能会抛出一个异常,这意味着代码可能不会执行到 try 代码块的最后部分。结果就是,你并没有关闭资源。 + +所以,你应该把清理工作的代码放到 finally 里去,或者使用 try-with-resource 特性。 + +#### 1.1 使用 finally 代码块 + +* 与前面几行 try 代码块不同,finally 代码块总是会被执行。不管 try 代码块成功执行之后还是你在 catch 代码块中处理完异常后都会执行。因此,你可以确保你清理了所有打开的资源。 + +public void closeResourceInFinally() { + FileInputStream inputStream = null; + try { + File file = new File("./tmp.txt"); + inputStream = new FileInputStream(file); + // use the inputStream to read a file + } catch (FileNotFoundException e) { + log.error(e); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + log.error(e); + } + } + } +} +复制代码 +#### 1.2 Java 7 的 try-with-resource 语法 + +* 如果你的资源实现了 AutoCloseable 接口,你可以使用这个语法。大多数的 Java 标准资源都继承了这个接口。当你在 try 子句中打开资源,资源会在 try 代码块执行后或异常处理后自动关闭。 + +public void automaticallyCloseResource() { + File file = new File("./tmp.txt"); + try (FileInputStream inputStream = new FileInputStream(file);) { + // use the inputStream to read a file + } catch (FileNotFoundException e) { + log.error(e); + } catch (IOException e) { + log.error(e); + } +} + +复制代码 + +​ + +### 2\. 优先明确的异常 + +* 你抛出的异常越明确越好,永远记住,你的同事或者几个月之后的你,将会调用你的方法并且处理异常。 + +* 因此需要保证提供给他们尽可能多的信息。这样你的 API 更容易被理解。你的方法的调用者能够更好的处理异常并且避免额外的检查。 + +* 因此,总是尝试寻找最适合你的异常事件的类,例如,抛出一个 NumberFormatException 来替换一个 IllegalArgumentException 。避免抛出一个不明确的异常。 + +public void doNotDoThis() throws Exception { + ... +} +public void doThis() throws NumberFormatException { + ... +} + +复制代码 + +​ + +### 3\. 对异常进行文档说明 + +* 当在方法上声明抛出异常时,也需要进行文档说明。目的是为了给调用者提供尽可能多的信息,从而可以更好地避免或处理异常。 + 在 Javadoc 添加 @throws 声明,并且描述抛出异常的场景。 + +public void doSomething(String input) throws MyBusinessException { + ... +} + +复制代码 +### 4\. 使用描述性消息抛出异常 + +* 在抛出异常时,需要尽可能精确地描述问题和相关信息,这样无论是打印到日志中还是在监控工具中,都能够更容易被人阅读,从而可以更好地定位具体错误信息、错误的严重程度等。 + +* 但这里并不是说要对错误信息长篇大论,因为本来 Exception 的类名就能够反映错误的原因,因此只需要用一到两句话描述即可。 + +* 如果抛出一个特定的异常,它的类名很可能已经描述了这种错误。所以,你不需要提供很多额外的信息。一个很好的例子是 NumberFormatException 。当你以错误的格式提供 String 时,它将被 java.lang.Long 类的构造函数抛出。 + +try { + new Long("xyz"); +} catch (NumberFormatException e) { + log.error(e); +} + +复制代码 +### 5\. 优先捕获最具体的异常 + +* 大多数 IDE 都可以帮助你实现这个最佳实践。当你尝试首先捕获较不具体的异常时,它们会报告无法访问的代码块。 + +* 但问题在于,只有匹配异常的第一个 catch 块会被执行。 因此,如果首先捕获 IllegalArgumentException ,则永远不会到达应该处理更具体的 NumberFormatException 的 catch 块,因为它是 IllegalArgumentException 的子类。 + +* 总是优先捕获最具体的异常类,并将不太具体的 catch 块添加到列表的末尾。 + +* 你可以在下面的代码片断中看到这样一个 try-catch 语句的例子。 第一个 catch 块处理所有 NumberFormatException 异常,第二个处理所有非 NumberFormatException 异常的IllegalArgumentException 异常。 + +public void catchMostSpecificExceptionFirst() { + try { + doSomething("A message"); + } catch (NumberFormatException e) { + log.error(e); + } catch (IllegalArgumentException e) { + log.error(e) + } +} + +复制代码 + +​ + +### 6\. 不要捕获 Throwable 类 + +* Throwable 是所有异常和错误的超类。你可以在 catch 子句中使用它,但是你永远不应该这样做! + +* 如果在 catch 子句中使用 Throwable ,它不仅会捕获所有异常,也将捕获所有的错误。JVM 抛出错误,指出不应该由应用程序处理的严重问题。 典型的例子是 OutOfMemoryError 或者 StackOverflowError 。两者都是由应用程序控制之外的情况引起的,无法处理。 + +* 所以,最好不要捕获 Throwable ,除非你确定自己处于一种特殊的情况下能够处理错误。 + +public void doNotCatchThrowable() { + try { + // do something + } catch (Throwable t) { + // don't do this! + } +} + +复制代码 +### 7\. 不要忽略异常 + +* 很多时候,开发者很有自信不会抛出异常,因此写了一个catch块,但是没有做任何处理或者记录日志。 + +public void doNotIgnoreExceptions() { + try { + // do something + } catch (NumberFormatException e) { + // this will never happen + } +} + +复制代码 + +* 但现实是经常会出现无法预料的异常,或者无法确定这里的代码未来是不是会改动(删除了阻止异常抛出的代码),而此时由于异常被捕获,使得无法拿到足够的错误信息来定位问题。 + +* 合理的做法是至少要记录异常的信息。 + +public void logAnException() { + try { + // do something + } catch (NumberFormatException e) { + log.error("This should never happen: " + e); + } +} + +复制代码 +### 8\. 不要记录并抛出异常 + +* 这可能是本文中最常被忽略的最佳实践。可以发现很多代码甚至类库中都会有捕获异常、记录日志并再次抛出的逻辑。如下: + +try { + new Long("xyz"); +} catch (NumberFormatException e) { + log.error(e); + throw e; +} + +复制代码 + +* 这个处理逻辑看着是合理的。但这经常会给同一个异常输出多条日志。如下: + +17:44:28,945 ERROR TestExceptionHandling:65 - java.lang.NumberFormatException: For input string: "xyz" +Exception in thread "main" java.lang.NumberFormatException: For input string: "xyz" +at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) +at java.lang.Long.parseLong(Long.java:589) +at java.lang.Long.(Long.java:965) +at com.stackify.example.TestExceptionHandling.logAndThrowException(TestExceptionHandling.java:63) +at com.stackify.example.TestExceptionHandling.main(TestExceptionHandling.java:58) + +复制代码 + +* 如上所示,后面的日志也没有附加更有用的信息。如果想要提供更加有用的信息,那么可以将异常包装为自定义异常。 + +public void wrapException(String input) throws MyBusinessException { + try { + // do something + } catch (NumberFormatException e) { + throw new MyBusinessException("A message that describes the error.", e); + } +} + +复制代码 + +* 因此,仅仅当想要处理异常时才去捕获,否则只需要在方法签名中声明让调用者去处理。 + +### 9\. 包装异常时不要抛弃原始的异常 + +* 捕获标准异常并包装为自定义异常是一个很常见的做法。这样可以添加更为具体的异常信息并能够做针对的异常处理。 + 在你这样做时,请确保将原始异常设置为原因(注:参考下方代码 NumberFormatException e 中的原始异常 e )。Exception 类提供了特殊的构造函数方法,它接受一个 Throwable 作为参数。否则,你将会丢失堆栈跟踪和原始异常的消息,这将会使分析导致异常的异常事件变得困难。 + +public void wrapException(String input) throws MyBusinessException { + try { + // do something + } catch (NumberFormatException e) { + throw new MyBusinessException("A message that describes the error.", e); + } +} + +复制代码 + +​ + +### 10\. 不要使用异常控制程序的流程 + +* 不应该使用异常控制应用的执行流程,例如,本应该使用if语句进行条件判断的情况下,你却使用异常处理,这是非常不好的习惯,会严重影响应用的性能。 + +### 11\. 使用标准异常 + +* 如果使用内建的异常可以解决问题,就不要定义自己的异常。Java API 提供了上百种针对不同情况的异常类型,在开发中首先尽可能使用 Java API 提供的异常,如果标准的异常不能满足你的要求,这时候创建自己的定制异常。尽可能得使用标准异常有利于新加入的开发者看懂项目代码。 + +### 12\. 异常会影响性能 + +* 异常处理的性能成本非常高,每个 Java 程序员在开发时都应牢记这句话。创建一个异常非常慢,抛出一个异常又会消耗1~5ms,当一个异常在应用的多个层级之间传递时,会拖累整个应用的性能。 + + * 仅在异常情况下使用异常; + * 在可恢复的异常情况下使用异常; +* 尽管使用异常有利于 Java 开发,但是在应用中最好不要捕获太多的调用栈,因为在很多情况下都不需要打印调用栈就知道哪里出错了。因此,异常消息应该提供恰到好处的信息。 + +### 13\. 总结 + +* 综上所述,当你抛出或捕获异常的时候,有很多不同的情况需要考虑,而且大部分事情都是为了改善代码的可读性或者 API 的可用性。 + +* 异常不仅仅是一个错误控制机制,也是一个通信媒介。因此,为了和同事更好的合作,一个团队必须要制定出一个最佳实践和规则,只有这样,团队成员才能理解这些通用概念,同时在工作中使用它。 + +### 异常处理-阿里巴巴Java开发手册 + +1. 【强制】Java 类库中定义的可以通过预检查方式规避的RuntimeException异常不应该通过catch 的方式来处理,比如:NullPointerException,IndexOutOfBoundsException等等。 说明:无法通过预检查的异常除外,比如,在解析字符串形式的数字时,可能存在数字格式错误,不得不通过catch NumberFormatException来实现。 正例:if (obj != null) {…} 反例:try { obj.method(); } catch (NullPointerException e) {…} + +2. 【强制】异常不要用来做流程控制,条件控制。 说明:异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。 + +3. 【强制】catch时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的catch尽可能进行区分异常类型,再做对应的异常处理。 说明:对大段代码进行try-catch,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题,这是一种不负责任的表现。 正例:用户注册的场景中,如果用户输入非法字符,或用户名称已存在,或用户输入密码过于简单,在程序上作出分门别类的判断,并提示给用户。 + +4. 【强制】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。 + +5. 【强制】有try块放到了事务代码中,catch异常后,如果需要回滚事务,一定要注意手动回滚事务。 + +6. 【强制】finally块必须对资源对象、流对象进行关闭,有异常也要做try-catch。 说明:如果JDK7及以上,可以使用try-with-resources方式。 + +7. 【强制】不要在finally块中使用return。 说明:try块中的return语句执行成功后,并不马上返回,而是继续执行finally块中的语句,如果此处存在return语句,则在此直接返回,无情丢弃掉try块中的返回点。 反例: + + private int x = 0; + public int checkReturn() { + try { + // x等于1,此处不返回 + return ++x; + } finally { + // 返回的结果是2 + return ++x; + } + } + +复制代码 + +1. 【强制】捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。 说明:如果预期对方抛的是绣球,实际接到的是铅球,就会产生意外情况。 + +2. 【强制】在调用RPC、二方包、或动态生成类的相关方法时,捕捉异常必须使用Throwable类来进行拦截。 说明:通过反射机制来调用方法,如果找不到方法,抛出NoSuchMethodException。什么情况会抛出NoSuchMethodError呢?二方包在类冲突时,仲裁机制可能导致引入非预期的版本使类的方法签名不匹配,或者在字节码修改框架(比如:ASM)动态创建或修改类时,修改了相应的方法签名。这些情况,即使代码编译期是正确的,但在代码运行期时,会抛出NoSuchMethodError。 + +3. 【推荐】方法的返回值可以为null,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回null值。 说明:本手册明确防止NPE是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败、序列化失败、运行时异常等场景返回null的情况。 + +4. 【推荐】防止NPE,是程序员的基本修养,注意NPE产生的场景: 1) 返回类型为基本数据类型,return包装数据类型的对象时,自动拆箱有可能产生NPE。 反例:public int f() { return Integer对象}, 如果为null,自动解箱抛NPE。 2) 数据库的查询结果可能为null。 3) 集合里的元素即使isNotEmpty,取出的数据元素也可能为null。 4) 远程调用返回对象时,一律要求进行空指针判断,防止NPE。 5) 对于Session中获取的数据,建议进行NPE检查,避免空指针。 6) 级联调用obj.getA().getB().getC();一连串调用,易产生NPE。 + 正例:使用JDK8的Optional类来防止NPE问题。 + +5. 【推荐】定义时区分unchecked / checked 异常,避免直接抛出new RuntimeException(),更不允许抛出Exception或者Throwable,应使用有业务含义的自定义异常。推荐业界已定义过的自定义异常,如:DAOException / ServiceException等。 + +6. 【参考】对于公司外的http/api开放接口必须使用“错误码”;而应用内部推荐异常抛出;跨应用间RPC调用优先考虑使用Result方式,封装isSuccess()方法、“错误码”、“错误简短信息”。 说明:关于RPC方法返回方式使用Result方式的理由: 1)使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。 2)如果不加栈信息,只是new自定义异常,加入自己的理解的error message,对于调用端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题。 + +7. 【参考】避免出现重复的代码(Don’t Repeat Yourself),即DRY原则。 说明:随意复制和粘贴代码,必然会导致代码的重复,在以后需要修改时,需要修改所有的副本,容易遗漏。必要时抽取共性方法,或者抽象公共类,甚至是组件化。 正例:一个类中有多个public方法,都需要进行数行相同的参数校验操作,这个时候请抽取: + private boolean checkParam(DTO dto) {…} + +作者:小杰要吃蛋 +链接:https://juejin.cn/post/6844904128959741965 +来源:稀土掘金 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 \ No newline at end of file diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\350\231\232\346\213\237\346\234\272.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\350\231\232\346\213\237\346\234\272.md" new file mode 100644 index 0000000..382dac4 --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\350\231\232\346\213\237\346\234\272.md" @@ -0,0 +1,627 @@ + + +## Java内存模型 + +### 我们开发人员编写的Java代码是怎么让电脑认识的 + +* 首先先了解电脑是二进制的系统,他只认识 01010101 + +* 比如我们经常要编写 HelloWord.java 电脑是怎么认识运行的 + +* HelloWord.java是我们程序员编写的,我们人可以认识,但是电脑不认识 + +**Java文件编译的过程** + +1. 程序员编写的.java文件 +2. 由javac编译成字节码文件.class:(为什么编译成class文件,因为JVM只认识.class文件) +3. 在由JVM编译成电脑认识的文件 (对于电脑系统来说 文件代表一切) + +`(这是一个大概的观念 抽象画的概念)` + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fc7554d11b~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +### 为什么说java是跨平台语言 + +* 这个夸平台是中间语言(JVM)实现的夸平台 +* Java有JVM从软件层面屏蔽了底层硬件、指令层面的细节让他兼容各种系统 + +`难道 C 和 C++ 不能夸平台吗 其实也可以` `C和C++需要在编译器层面去兼容不同操作系统的不同层面,写过C和C++的就知道不同操作系统的有些代码是不一样` + +### Jdk和Jre和JVM的区别 + +* Jdk包括了Jre和Jvm,Jre包括了Jvm + +* Jdk是我们编写代码使用的开发工具包 + +* Jre 是Java的运行时环境,他大部分都是 C 和 C++ 语言编写的,他是我们在编译java时所需要的基础的类库 + +* Jvm俗称Java虚拟机,他是java运行环境的一部分,它虚构出来的一台计算机,在通过在实际的计算机上仿真模拟各种计算机功能来实现Java应用程序 + +* 看Java官方的图片,Jdk中包括了Jre,Jre中包括了JVM + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fc8693d47b~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +### 说一下 JVM由那些部分组成,运行流程是什么? + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fc868d44b7~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* JVM包含两个子系统和两个组件: 两个子系统为Class loader(类装载)、Execution engine(执行引擎); 两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。 + + * Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area。 + + * Execution engine(执行引擎):执行classes中的指令。 + + * Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。 + + * Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。 + +* **流程** :首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。 + +### 说一下 JVM 运行时数据区 + +* Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存区域划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有些区域随着虚拟机进程的启动而存在,有些区域则是依赖线程的启动和结束而建立和销毁。Java 虚拟机所管理的内存被划分为如下几个区域: + +`简单的说就是我们java运行时的东西是放在那里的` + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fc8784541e~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成; + + `为什么要线程计数器?因为线程是不具备记忆功能` + +* Java 虚拟机栈(Java Virtual Machine Stacks):每个方法在执行的同时都会在Java 虚拟机栈中创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息; + + `栈帧就是Java虚拟机栈中的下一个单位` + +* 本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的; + + `Native 关键字修饰的方法是看不到的,Native 方法的源码大部分都是 C和C++ 的代码` + +* Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存; + +* 方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。 + +`后面有详细的说明JVM 运行时数据区` + +### 详细的介绍下程序计数器?(重点理解) + +1. 程序计数器是一块较小的内存空间,它可以看作是:保存当前线程所正在执行的字节码指令的地址(行号) + +2. 由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储。称之为“线程私有”的内存。程序计数器内存区域是虚拟机中唯一没有规定OutOfMemoryError情况的区域。 + + `总结:也可以把它叫做线程计数器` + +* **例子**:在java中最小的执行单位是线程,线程是要执行指令的,执行的指令最终操作的就是我们的电脑,就是 CPU。在CPU上面去运行,有个非常不稳定的因素,叫做调度策略,这个调度策略是时基于时间片的,也就是当前的这一纳秒是分配给那个指令的。 + +* **假如**: + + * 线程A在看直播 ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fc9acf8957~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + + * 突然,线程B来了一个视频电话,就会抢夺线程A的时间片,就会打断了线程A,线程A就会挂起 ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fcc70da181~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + + * 然后,视频电话结束,这时线程A究竟该干什么? (线程是最小的执行单位,他不具备记忆功能,他只负责去干,那这个记忆就由:**程序计数器来记录**) ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fcc90c8a88~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +### 详细介绍下Java虚拟机栈?(重点理解) + +1. Java虚拟机是线程私有的,它的生命周期和线程相同。 +2. 虚拟机栈描述的是Java方法执行的内存模型:`每个方法在执行的同时`都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。 + +* **解释**:虚拟机栈中是有单位的,单位就是**栈帧**,一个方法一个**栈帧**。一个**栈帧**中他又要存储,局部变量,操作数栈,动态链接,出口等。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fccadd7f8b~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)**解析栈帧:** + +1. 局部变量表:是用来存储我们临时8个基本数据类型、对象引用地址、returnAddress类型。(returnAddress中保存的是return后要执行的字节码的指令地址。) +2. 操作数栈:操作数栈就是用来操作的,例如代码中有个 i = 6*6,他在一开始的时候就会进行操作,读取我们的代码,进行计算后再放入局部变量表中去 +3. 动态链接:假如我方法中,有个 service.add()方法,要链接到别的方法中去,这就是动态链接,存储链接的地方。 +4. 出口:出口是什呢,出口正常的话就是return 不正常的话就是抛出异常落 + +#### 一个方法调用另一个方法,会创建很多栈帧吗? + +* 答:会创建。如果一个栈中有动态链接调用别的方法,就会去创建新的栈帧,栈中是由顺序的,一个栈帧调用另一个栈帧,另一个栈帧就会排在调用者下面 + +#### 栈指向堆是什么意思? + +* 栈指向堆是什么意思,就是栈中要使用成员变量怎么办,栈中不会存储成员变量,只会存储一个应用地址 + +#### 递归的调用自己会创建很多栈帧吗? + +* 答:递归的话也会创建多个栈帧,就是在栈中一直从上往下排下去 + +### 你能给我详细的介绍Java堆吗?(重点理解) + +* java堆(Java Heap)是java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。 +* 在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。 +* java堆是垃圾收集器管理的主要区域,因此也被成为“GC堆”。 +* 从内存回收角度来看java堆可分为:新生代和老生代。 +* 从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。 +* 无论怎么划分,都与存放内容无关,无论哪个区域,存储的都是对象实例,进一步的划分都是为了更好的回收内存,或者更快的分配内存。 +* 根据Java虚拟机规范的规定,java堆可以处于物理上不连续的内存空间中。当前主流的虚拟机都是可扩展的(通过 -Xmx 和 -Xms 控制)。如果堆中没有内存可以完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。 + +### 能不能解释一下本地方法栈? + +1. 本地方法栈很好理解,他很栈很像,只不过方法上带了 native 关键字的栈字 +2. 它是虚拟机栈为虚拟机执行Java方法(也就是字节码)的服务方法 +3. native关键字的方法是看不到的,必须要去oracle官网去下载才可以看的到,而且native关键字修饰的大部分源码都是C和C++的代码。 +4. 同理可得,本地方法栈中就是C和C++的代码 + +### 能不能解释一下方法区(重点理解) + +1. 方法区是所有线程共享的内存区域,它用于存储已被Java虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 +2. 它有个别命叫Non-Heap(非堆)。当方法区无法满足内存分配需求时,抛出OutOfMemoryError异常。 + +### 什么是JVM字节码执行引擎 + +* 虚拟机核心的组件就是执行引擎,它负责执行虚拟机的字节码,一般户先进行编译成机器码后执行。 + +* “虚拟机”是一个相对于“物理机”的概念,虚拟机的字节码是不能直接在物理机上运行的,需要JVM字节码执行引擎- 编译成机器码后才可在物理机上执行。 + +### 你听过直接内存吗? + +* 直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致 OutOfMemoryError 异常出现,所以我们放到这里一起讲解。 +* 我的理解就是直接内存是基于物理内存和Java虚拟机内存的中间内存 + +### 知道垃圾收集系统吗? + +* 程序在运行过程中,会产生大量的内存垃圾(一些没有引用指向的内存对象都属于内存垃圾,因为这些对象已经无法访问,程序用不了它们了,对程序而言它们已经死亡),为了确保程序运行时的性能,java虚拟机在程序运行的过程中不断地进行自动的垃圾回收(GC)。 + +* 垃圾收集系统是Java的核心,也是不可少的,Java有一套自己进行垃圾清理的机制,开发人员无需手工清理 + +* 有一部分原因就是因为Java垃圾回收系统的强大导致Java领先市场 + +### 堆栈的区别是什么? + +> | 对比 | JVM堆 | JVM栈 | +> | --- | --- | --- | +> | 物理地址 | 堆的物理地址分配对对象是不连续的。因此性能慢些。在GC的时候也要考虑到不连续的分配,所以有各种算法。比如,标记-消除,复制,标记-压缩,分代(即新生代使用复制算法,老年代使用标记——压缩) | 栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。 | +> | 内存分别 | 堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆大小远远大于栈。 | 栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。 | +> | 存放的内容 | 堆存放的是对象的实例和数组。因此该区更关注的是数据的存储 | 栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。 | +> | 程序的可见度 | 堆对于整个应用程序都是共享、可见的。 | 栈只对于线程是可见的。所以也是线程私有。他的生命周期和线程相同。 | + +* 注意: + * 静态变量放在方法区 + * 静态的对象还是放在堆。 + +### 深拷贝和浅拷贝 + +* 浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址, +* 深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存, +* 浅复制:仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变。 +* 深复制:在计算机中开辟一块**新的内存地址**用于存放复制的对象。 + +### Java会存在内存泄漏吗?请说明为什么? + +* 内存泄漏是指不再被使用的对象或者变量一直被占据在内存中。理论上来说,Java是有GC垃圾回收机制的,也就是说,不再被使用的对象,会被GC自动回收掉,自动从内存中清除。 + +* 但是,即使这样,Java也还是存在着内存泄漏的情况,java导致内存泄露的原因很明确:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,`尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收`,这就是java中内存泄露的发生场景。 + +## 垃圾回收机制及算法 + +### 简述Java垃圾回收机制 + +* 在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。 + +### GC是什么?为什么要GC + +* GC 是垃圾收集的意思(Gabage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java 语言没有提供释放已分配内存的显示操作方法。 + +### 垃圾回收的优点和缺点 + +* 优点:JVM的垃圾回收器都不需要我们手动处理无引用的对象了,这个就是最大的优点 + +* 缺点:程序员不能实时的对某个对象或所有对象调用垃圾回收器进行垃圾回收。 + +### 垃圾回收器的原理是什么?有什么办法手动进行垃圾回收? + +* 对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。 + +* 通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。 + +* 可以。程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。 + +### JVM 中都有哪些引用类型? + +* 强引用:发生 gc 的时候不会被回收。 +* 软引用:有用但不是必须的对象,在发生内存溢出之前会被回收。 +* 弱引用:有用但不是必须的对象,在下一次GC时会被回收。 +* 虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用 PhantomReference 实现虚引用,虚引用的用途是在 gc 时返回一个通知。 + +### 怎么判断对象是否可以被回收? + +* 垃圾收集器在做垃圾回收的时候,首先需要判定的就是哪些内存是需要被回收的,哪些对象是存活的,是不可以被回收的;哪些对象已经死掉了,需要被回收。 + +* 一般有两种方法来判断: + + * 引用计数器法:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;(这个已经淘汰了) + * 可达性分析算法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。(市场上用的非常非常广泛) + +### Full GC是什么 + +* 清理整个堆空间—包括年轻代和老年代和永久代 +* 因为Full GC是清理整个堆空间所以Full GC执行速度非常慢,在Java开发中最好保证少触发Full GC + +### 对象什么时候可以被垃圾器回收 + +* 当对象对当前使用这个对象的应用程序变得不可触及的时候,这个对象就可以被回收了。 +* 垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。 + +### JVM 垃圾回收算法有哪些? + +* 标记-清除算法:标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。 +* 复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。 +* 标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。 +* 分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。 + +#### 标记-清除算法 + +* 标记无用对象,然后进行清除回收。 + +* 标记-清除算法(Mark-Sweep)是一种常见的基础垃圾收集算法,它将垃圾收集分为两个阶段: + + * 标记阶段:标记出可以回收的对象。 + * 清除阶段:回收被标记的对象所占用的空间。 +* 标记-清除算法之所以是基础的,是因为后面讲到的垃圾收集算法都是在此算法的基础上进行改进的。 + +* **优点**:实现简单,不需要对象进行移动。 + +* **缺点**:标记、清除过程效率低,产生大量不连续的内存碎片,提高了垃圾回收的频率。 + +* 标记-清除算法的执行的过程如下图所示 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fce4d05e44~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### 复制算法 + +* 为了解决标记-清除算法的效率不高的问题,产生了复制算法。它把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾收集时,遍历当前使用的区域,把存活对象复制到另外一个区域中,最后将当前使用的区域的可回收的对象进行回收。 + +* **优点**:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。 + +* **缺点**:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。 + +* 复制算法的执行过程如下图所示 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fcd22194ad~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### 标记-整理算法 + +* 在新生代中可以使用复制算法,但是在老年代就不能选择复制算法了,因为老年代的对象存活率会较高,这样会有较多的复制操作,导致效率变低。标记-清除算法可以应用在老年代中,但是它效率不高,在内存回收后容易产生大量内存碎片。因此就出现了一种标记-整理算法(Mark-Compact)算法,与标记-整理算法不同的是,在标记可回收的对象后将所有存活的对象压缩到内存的一端,使他们紧凑的排列在一起,然后对端边界以外的内存进行回收。回收后,已用和未用的内存都各自一边。 + +* **优点**:解决了标记-清理算法存在的内存碎片问题。 + +* **缺点**:仍需要进行局部对象移动,一定程度上降低了效率。 + +* 标记-整理算法的执行过程如下图所示 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fce7bb60b1~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### 分代收集算法 + +* 当前商业虚拟机都采用 `分代收集`的垃圾收集算法。分代收集算法,顾名思义是根据对象的`存活周期`将内存划分为几块。一般包括`年轻代`、`老年代`和 `永久代`,如图所示:`(后面有重点讲解)` + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fcf6f94e92~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +### JVM中的永久代中会发生垃圾回收吗 + +* 垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。请参考下Java8:从永久代到元数据区 + (注:Java8中已经移除了永久代,新加了一个叫做元数据区的native内存区) + +## 垃圾收集器以及新生代、老年代、永久代 + +### 讲一下新生代、老年代、永久代的区别 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fd069db773~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。而新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。 + +* 新生代中一般保存新出现的对象,所以每次垃圾收集时都发现大批对象死去,只有少量对象存活,便采用了`复制算法`,只需要付出少量存活对象的复制成本就可以完成收集。 + +* 老年代中一般保存存活了很久的对象,他们存活率高、没有额外空间对它进行分配担保,就必须采用`“标记-清理”或者“标记-整理”`算法。 + +* 永久代就是JVM的方法区。在这里都是放着一些被虚拟机加载的类信息,静态变量,常量等数据。这个区中的东西比老年代和新生代更不容易回收。 + +### Minor GC、Major GC、Full GC是什么 + +1. Minor GC是新生代GC,指的是发生在新生代的垃圾收集动作。由于java对象大都是朝生夕死的,所以Minor GC非常频繁,一般回收速度也比较快。(一般采用复制算法回收垃圾) +2. Major GC是老年代GC,指的是发生在老年代的GC,通常执行Major GC会连着Minor GC一起执行。Major GC的速度要比Minor GC慢的多。(可采用标记清楚法和标记整理法) +3. Full GC是清理整个堆空间,包括年轻代和老年代 + +### Minor GC、Major GC、Full GC区别及触发条件 + +* **Minor GC 触发条件一般为:** + + 1. eden区满时,触发MinorGC。即申请一个对象时,发现eden区不够用,则触发一次MinorGC。 + 2. 新创建的对象大小 > Eden所剩空间时触发Minor GC +* **Major GC和Full GC 触发条件一般为:** `Major GC通常是跟full GC是等价的` + + 1. 每次晋升到老年代的对象平均大小>老年代剩余空间 + + 2. MinorGC后存活的对象超过了老年代剩余空间 + + 3. 永久代空间不足 + + 4. 执行System.gc() + + 5. CMS GC异常 + + 6. 堆内存分配很大的对象 + +### 为什么新生代要分Eden和两个 Survivor 区域? + +* 如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC.老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多,所以需要分为Eden和Survivor。 +* Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历15次Minor GC还能在新生代中存活的对象,才会被送到老年代。 +* 设置两个Survivor区最大的好处就是解决了碎片化,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生) + +### Java堆老年代( Old ) 和新生代 ( Young ) 的默认比例? + +* 默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。 + +* 其中,新生代 ( Young ) 被细分为 Eden 和 **两个 Survivor 区域**,Edem 和俩个Survivor 区域比例是 = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ), + +* 但是JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。 + +### 为什么要这样分代: + +* 其实主要原因就是可以根据各个年代的特点进行对象分区存储,更便于回收,采用最适当的收集算法: + + * 新生代中,每次垃圾收集时都发现大批对象死去,只有少量对象存活,便采用了复制算法,只需要付出少量存活对象的复制成本就可以完成收集。 + + * 而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须采用“标记-清理”或者“标记-整理”算法。 + +* 新生代又分为Eden和Survivor (From与To,这里简称一个区)两个区。加上老年代就这三个区。数据会首先分配到Eden区当中(当然也有特殊情况,如果是大对象那么会直接放入到老年代(大对象是指需要大量连续内存空间的java对象)。当Eden没有足够空间的时候就会触发jvm发起一次Minor GC,。如果对象经过一次Minor-GC还存活,并且又能被Survivor空间接受,那么将被移动到Survivor空间当中。并将其年龄设为1,对象在Survivor每熬过一次Minor GC,年龄就加1,当年龄达到一定的程度(默认为15)时,就会被晋升到老年代中了,当然晋升老年代的年龄是可以设置的。 + +### 什么是垃圾回收器他和垃圾算法有什么区别 + +* 垃圾收集器是垃圾回收算法(标记清楚法、标记整理法、复制算法、分代算法)的具体实现,不同垃圾收集器、不同版本的JVM所提供的垃圾收集器可能会有很在差别。 + +### 说一下 JVM 有哪些垃圾回收器? + +* 如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。下图展示了7种作用于不同分代的收集器,其中用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,还有用于回收整个Java堆的G1收集器。不同收集器之间的连线表示它们可以搭配使用。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fd022875b5~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +> | 垃圾回收器 | 工作区域 | 回收算法 | 工作线程 | 用户线程并行 | 描述 | +> | --- | --- | --- | --- | --- | --- | +> | Serial | 新生带 | 复制算法 | 单线程 | 否 | Client模式下默认新生代收集器。简单高效 | +> | ParNew | 新生带 | 复制算法 | 多线程 | 否 | Serial的多线程版本,Server模式下首选, 可搭配CMS的新生代收集器 | +> | Parallel Scavenge | 新生带 | 复制算法 | 多线程 | 否 | 目标是达到可控制的吞吐量 | +> | Serial Old | 老年带 | 标记-整理 | 单线程 | 否 | Serial老年代版本,给Client模式下的虚拟机使用 | +> | Parallel Old | 老年带 | 标记-整理 | 多线程 | 否 | Parallel Scavenge老年代版本,吞吐量优先 | +> | | | | | | | +> | G1 | 新生带 + 老年带 | 标记-整理 + 复制算法 | 多线程 | 是 | JDK1.9默认垃圾收集器 | + +* Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效; +* ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现; +* Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景; +* Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本; +* Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本; +* CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。 +* G1(Garbage First)收集器 ( `标记整理 + 复制算法来回收垃圾` ): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。 + +### 收集器可以这么分配?(了解就好了) + +Serial / Serial Old +Serial / CMS +ParNew / Serial Old +ParNew / CMS +Parallel Scavenge / Serial Old +Parallel Scavenge / Parallel Old +G1 +复制代码 +### 新生代垃圾回收器和老年代垃圾回收器都有哪些?有什么区别? + +* 新生代回收器:Serial、ParNew、Parallel Scavenge +* 老年代回收器:Serial Old、Parallel Old、CMS +* 整堆回收器:G1 + +新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃圾回收。 + +### 简述分代垃圾回收器是怎么工作的? + +* 分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。 + +* 新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下: + + * 把 Eden + From Survivor 存活的对象放入 To Survivor 区; + * 清空 Eden 和 From Survivor 分区; + * From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。 +* 每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。 + +* 老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。 + +## 内存分配策略 + +### 简述java内存分配与回收策率以及Minor GC和Major GC + +* 所谓自动内存管理,最终要解决的也就是内存分配和内存回收两个问题。前面我们介绍了内存回收,这里我们再来聊聊内存分配。 + +* 对象的内存分配通常是在 Java 堆上分配(随着虚拟机优化技术的诞生,某些场景下也会在栈上分配,后面会详细介绍),对象主要分配在新生代的 Eden 区,如果启动了本地线程缓冲,将按照线程优先在 TLAB 上分配。少数情况下也会直接在老年代上分配。总的来说分配规则不是百分百固定的,其细节取决于哪一种垃圾收集器组合以及虚拟机相关参数有关,但是虚拟机对于内存的分配还是会遵循以下几种「普世」规则: + +#### 对象优先在 Eden 区分配 + +* 多数情况,对象都在新生代 Eden 区分配。当 Eden 区分配没有足够的空间进行分配时,虚拟机将会发起一次 Minor GC。如果本次 GC 后还是没有足够的空间,则将启用分配担保机制在老年代中分配内存。 + * 这里我们提到 Minor GC,如果你仔细观察过 GC 日常,通常我们还能从日志中发现 Major GC/Full GC。 + * **Minor GC** 是指发生在新生代的 GC,因为 Java 对象大多都是朝生夕死,所有 Minor GC 非常频繁,一般回收速度也非常快; + * **Major GC/Full GC** 是指发生在老年代的 GC,出现了 Major GC 通常会伴随至少一次 Minor GC。Major GC 的速度通常会比 Minor GC 慢 10 倍以上。 + +#### 为什么大对象直接进入老年代 + +* 所谓大对象是指需要大量连续内存空间的对象,频繁出现大对象是致命的,会导致在内存还有不少空间的情况下提前触发 GC 以获取足够的连续空间来安置新对象。 + +* 前面我们介绍过新生代使用的是标记-清除算法来处理垃圾回收的,如果大对象直接在新生代分配就会导致 Eden 区和两个 Survivor 区之间发生大量的内存复制。因此对于大对象都会直接在老年代进行分配。 + +#### 长期存活对象将进入老年代 + +* 虚拟机采用分代收集的思想来管理内存,那么内存回收时就必须判断哪些对象应该放在新生代,哪些对象应该放在老年代。因此虚拟机给每个对象定义了一个对象年龄的计数器,如果对象在 Eden 区出生,并且能够被 Survivor 容纳,将被移动到 Survivor 空间中,这时设置对象年龄为 1。对象在 Survivor 区中每「熬过」一次 Minor GC 年龄就加 1,当年龄达到一定程度(默认 15) 就会被晋升到老年代。 + +## 虚拟机类加载机制 + +### 简述java类加载机制? + +* 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的java类型。 + +### 类加载的机制及过程 + +* 程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化3个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fd24770998~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +##### 1、加载 + +* 加载指的是将类的class文件读入到内存,并将这些静态数据转换成方法区中的运行时数据结构,并在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口,这个过程需要类加载器参与。 + +* Java类加载器由JVM提供,是所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。 + +* 类加载器,可以从不同来源加载类的二进制数据,比如:本地Class文件、Jar包Class文件、网络Class文件等等等。 + +* 类加载的最终产物就是位于堆中的Class对象(注意不是目标类对象),该对象封装了类在方法区中的数据结构,并且向用户提供了访问方法区数据结构的接口,即Java反射的接口 + +##### 2、连接过程 + +* 当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中(意思就是将java类的二进制代码合并到JVM的运行状态之中)。类连接又可分为如下3个阶段。 + +1. 验证:确保加载的类信息符合JVM规范,没有安全方面的问题。主要验证是否符合Class文件格式规范,并且是否能被当前的虚拟机加载处理。 + +2. 准备:正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配 + +3. 解析:虚拟机常量池的符号引用替换为字节引用过程 + +##### 3、初始化 + +* 初始化阶段是执行类构造器``() 方法的过程。类构造器``()方法是由编译器自动收藏类中的`所有类变量的赋值动作和静态语句块(static块)中的语句合并产生,代码从上往下执行。` + +* 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化 + +* 虚拟机会保证一个类的``() 方法在多线程环境中被正确加锁和同步 + +`初始化的总结就是:初始化是为类的静态变量赋予正确的初始值` + +### 描述一下JVM加载Class文件的原理机制 + +* Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。 + +* 类装载方式,有两种 : + + * 1.隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中, + + * 2.显式装载, 通过class.forname()等方法,显式加载需要的类 + +* Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。 + +### 什么是类加载器,类加载器有哪些? + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fd30195e5b~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。 + +* 主要有一下四种类加载器: + + 1. 启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。 + 2. 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。 + 3. 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。 + 4. 用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。 + +### 说一下类装载的执行过程? + +* 类装载分为以下 5 个步骤: + * 加载:根据查找路径找到相应的 class 文件然后导入; + * 验证:检查加载的 class 文件的正确性; + * 准备:给类中的静态变量分配内存空间; + * 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址; + * 初始化:对静态变量和静态代码块执行初始化工作。 + +### 什么是双亲委派模型? + +* 在介绍双亲委派模型之前先说下类加载器。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立在 JVM 中的唯一性,每一个类加载器,都有一个独立的类名称空间。类加载器就是根据指定全限定名称将 class 文件加载到 JVM 内存,然后再转化为 class 对象。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fd30195e5b~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 类加载器分类: + + * 启动类加载器(Bootstrap ClassLoader),是虚拟机自身的一部分,用来加载Java_HOME/lib/目录中的,或者被 -Xbootclasspath 参数所指定的路径中并且被虚拟机识别的类库; + * 其他类加载器: + * 扩展类加载器(Extension ClassLoader):负责加载\lib\ext目录或Java. ext. dirs系统变量指定的路径中的所有类库; + * 应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。 +* 双亲委派模型:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。 + +* 总结就是:`当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。` + +## JVM调优 + +### JVM 调优的参数可以在那设置参数值 + +* 可以在IDEA,Eclipse,工具里设置 + +* 如果上线了是WAR包的话可以在Tomcat设置 + +* 如果是Jar包直接 :java -jar 是直接插入JVM命令就好了 + + java -Xms1024m -Xmx1024m ...等等等 JVM参数 -jar springboot_app.jar & + 复制代码 + +### 说一下 JVM 调优的工具? + +* JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。 + + * jconsole:用于对 JVM 中的内存、线程和类等进行监控; ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fd4667d98b~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + + * jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。 ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171729fd4d2a3018~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +### 常用的 JVM 调优的参数都有哪些? + +#常用的设置 +-Xms:初始堆大小,JVM 启动的时候,给定堆空间大小。 + +-Xmx:最大堆大小,JVM 运行过程中,如果初始堆空间不足的时候,最大可以扩展到多少。 + +-Xmn:设置堆中年轻代大小。整个堆大小=年轻代大小+年老代大小+持久代大小。 + +-XX:NewSize=n 设置年轻代初始化大小大小 + +-XX:MaxNewSize=n 设置年轻代最大值 + +-XX:NewRatio=n 设置年轻代和年老代的比值。如: -XX:NewRatio=3,表示年轻代与年老代比值为 1:3,年轻代占整个年轻代+年老代和的 1/4 + +-XX:SurvivorRatio=n 年轻代中 Eden 区与两个 Survivor 区的比值。注意 Survivor 区有两个。8表示两个Survivor :eden=2:8 ,即一个Survivor占年轻代的1/10,默认就为8 + +-Xss:设置每个线程的堆栈大小。JDK5后每个线程 Java 栈大小为 1M,以前每个线程堆栈大小为 256K。 + +-XX:ThreadStackSize=n 线程堆栈大小 + +-XX:PermSize=n 设置持久代初始值 + +-XX:MaxPermSize=n 设置持久代大小 + +-XX:MaxTenuringThreshold=n 设置年轻带垃圾对象最大年龄。如果设置为 0 的话,则年轻代对象不经过 Survivor 区,直接进入年老代。 + +#下面是一些不常用的 + +-XX:LargePageSizeInBytes=n 设置堆内存的内存页大小 + +-XX:+UseFastAccessorMethods 优化原始类型的getter方法性能 + +-XX:+DisableExplicitGC 禁止在运行期显式地调用System.gc(),默认启用 + +-XX:+AggressiveOpts 是否启用JVM开发团队最新的调优成果。例如编译优化,偏向锁,并行年老代收集等,jdk6纸之后默认启动 + +-XX:+UseBiasedLocking 是否启用偏向锁,JDK6默认启用 + +-Xnoclassgc 是否禁用垃圾回收 + +-XX:+UseThreadPriorities 使用本地线程的优先级,默认启用 + +等等等...... +复制代码 +### JVM的GC收集器设置 + +* -xx:+Use xxx GC + * xxx 代表垃圾收集器名称 + +-XX:+UseSerialGC:设置串行收集器,年轻带收集器 + +-XX:+UseParNewGC:设置年轻代为并行收集。可与 CMS 收集同时使用。JDK5.0 以上,JVM 会根据系统配置自行设置,所以无需再设置此值。 + +-XX:+UseParallelGC:设置并行收集器,目标是目标是达到可控制的吞吐量 + +-XX:+UseParallelOldGC:设置并行年老代收集器,JDK6.0 支持对年老代并行收集。 + +-XX:+UseConcMarkSweepGC:设置年老代并发收集器 + +-XX:+UseG1GC:设置 G1 收集器,JDK1.9默认垃圾收集器 + +作者:小杰要吃蛋 +链接:https://juejin.cn/post/6844904125696573448 +来源:稀土掘金 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 \ No newline at end of file diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\351\233\206\345\220\210.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\351\233\206\345\220\210.md" new file mode 100644 index 0000000..217da0b --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Java\351\233\206\345\220\210.md" @@ -0,0 +1,930 @@ + +假设你是一名资深的 Java 开发工程师,有 5-10 年的大厂开发经验,现在你正在面试,需要你回答下面的一些问题,并且答案需要满足下列要求: +1、用中文回答; +2、以 markdown 的格式回答,中英文左右有空格,同时,对你的答案进行重点的突出标注等; +3、对于特别有需要解释的,难以理解的、有深度的内容,加以代码进行解释; +4、对相关实现的底层原理进行对比和分析; +你可以帮助我完成吗? + + +假设你是一名资深的 Java 开发工程师,有 5-10 年的大厂开发经验,现在你是一名面试官,现在你正在面试一名有着 5 年大厂经验的 Java 开发工程师。 + + +ConcurrentHashMap 和 Hashtable 的区别是什么? +需要你详细的回答,对底层的实现原理进行分析。 +然后,用 markdown 格式,重点突出,同时,如果 pdf 的内容有不完善的地方,结合你的理解补充完整。 + + +ConcurrentHashMap 和 Hashtable 的区别,需要你详细的回答,对底层的实现原理进行分析。 +然后,用 markdown 格式,重点突出。 + + + +## 集合容器概述 + +### 什么是集合 + +简单来说,集合就是一个放数据容器,它主要包括 Collection 和 Map 集合 + +- 集合只能存放对象,Java中每一种基本数据类型都有对应的引用类型。例如在集合中存储一个int型数据时,要先自动转换成Integer类后再存入; +- 集合存放的是对对象的引用,对象本身还是存放在堆内存中; +- 集合可以存放不同类型、不限数量的数据类型。 + +### 集合和数组的区别 + +* 数组是固定长度的;集合可变长度的。 + +* 数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。 + +* 数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。 + + +### 常用的集合类有哪些? + +常用的Java集合主要由三大体系:Set、List和Map。其中Set和List是基于Collection接口的实现类,Set中常用的有HashSet和TreeSet,List中常用的有ArrayList,基于Map接口的常用实现类有HashMap和TreeMap。 + +1. Collection接口的子接口包括:Set接口和List接口 +2. Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等 +3. Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等 +4. List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等 + +### List,Set,Map三者的区别? + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17173551e70de4bd~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* Java 容器分为 Collection 和 Map 两大类,Collection集合的子接口有Set、List、Queue三种子接口。我们比较常用的是Set、List,Map接口不是collection的子接口。 + +* Collection集合主要有List和Set两大接口 + + * List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。 + * Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。 +* Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。 + + * Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap + +### 集合框架底层数据结构 + +* Collection + + 1. List + + * Arraylist: Object数组 + + * Vector: Object数组 + + * LinkedList: 双向循环链表 + + 2. Set + + * HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素 + * LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的。 + * TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。) +* Map + + * HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间 + * LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。 + * HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的 + * TreeMap: 红黑树(自平衡的排序二叉树) + +### 哪些集合类是线程安全的? + +* Vector:就比Arraylist多了个 synchronized (线程安全),因为效率较低,现在已经不太建议使用。 +* hashTable:就比hashMap多了个synchronized (线程安全),不建议使用。 +* ConcurrentHashMap:是Java5中支持高并发、高吞吐量的线程安全HashMap实现。它由Segment数组结构和HashEntry数组结构组成。Segment数组在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键-值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构;一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素;每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。(推荐使用) +* ... + +### Java集合的快速失败机制 “fail-fast”? + +* 是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。 + +* 例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。 + +* 原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。 + +* 解决办法: + + 1. 在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。 + + 2. 使用CopyOnWriteArrayList来替换ArrayList + +### 怎么确保一个集合不能被修改? + +* 可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常。 + +* 示例代码如下: + + List list = new ArrayList<>(); + list. add("x"); + Collection clist = Collections. unmodifiableCollection(list); + clist. add("y"); // 运行时此行报错 + System. out. println(list. size()); + 复制代码 + +## Collection接口 + +### List接口 + +#### 迭代器 Iterator 是什么? + +* Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例。迭代器取代了 Java 集合框架中的 Enumeration,迭代器允许调用者在迭代过程中移除元素。 +* 因为所有Collection接继承了Iterator迭代器 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17173551e6f6342b~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### Iterator 怎么使用?有什么特点? + +* Iterator 使用代码如下: + + List list = new ArrayList<>(); + Iterator it = list. iterator(); + while(it. hasNext()){ + String obj = it. next(); + System. out. println(obj); + } + 复制代码 +* Iterator 的特点是只能单向遍历,但是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModificationException 异常。 + +#### 如何边遍历边移除 Collection 中的元素? + +* 边遍历边修改 Collection 的唯一正确方式是使用 Iterator.remove() 方法,如下: + + Iterator it = list.iterator(); + while(it.hasNext()){ + *// do something* + it.remove(); + } + 复制代码 + +一种最常见的**错误**代码如下: + +for(Integer i : list){ + list.remove(i) +} +复制代码 + +* 运行以上错误代码会报 **ConcurrentModificationException 异常**。这是因为当使用 foreach(for(Integer i : list)) 语句时,会自动生成一个iterator 来遍历该 list,但同时该 list 正在被 Iterator.remove() 修改。Java 一般不允许一个线程在遍历 Collection 时另一个线程修改它。 + +#### Iterator 和 ListIterator 有什么区别? + +* Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。 +* Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。 +* ListIterator 实现 Iterator 接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。 + +#### 遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java 中 List 遍历的最佳实践是什么? + +* 遍历方式有以下几种: + + 1. for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止。 + 2. 迭代器遍历,Iterator。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式。 + 3. foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明 Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。 +* 最佳实践:Java Collections 框架中提供了一个 RandomAccess 接口,用来标记 List 实现是否支持 Random Access。 + + * 如果一个数据集合实现了该接口,就意味着它支持 Random Access,按位置读取元素的平均时间复杂度为 O(1),如ArrayList。 + + * 如果没有实现该接口,表示不支持 Random Access,如LinkedList。 + + * 推荐的做法就是,支持 Random Access 的列表可用 for 循环遍历,否则建议用 Iterator 或 foreach 遍历。 + +#### 说一下 ArrayList 的优缺点 + +* ArrayList的优点如下: + + * ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口,因此查找的时候非常快。 + * ArrayList 在顺序添加一个元素的时候非常方便。 +* ArrayList 的缺点如下: + + * 删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能。 + * 插入元素的时候,也需要做一次元素复制操作,缺点同上。 +* ArrayList 比较适合顺序添加、随机访问的场景。 + +#### 如何实现数组和 List 之间的转换? + +* 数组转 List:使用 Arrays. asList(array) 进行转换。 +* List 转数组:使用 List 自带的 toArray() 方法。 + +* 代码示例: + + // list to array + List list = new ArrayList(); + list.add("123"); + list.add("456"); + list.toArray(); + + // array to list + String[] array = new String[]{"123","456"}; + Arrays.asList(array); + 复制代码 + +#### ArrayList 和 LinkedList 的区别是什么? + +* 数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。 +* 随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。 +* 增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。 +* 内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。 +* 线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全; + +* 综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。 + +* LinkedList 的双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。 + +#### ArrayList 和 Vector 的区别是什么? + +* 这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合 + + * 线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。 + * 性能:ArrayList 在性能方面要优于 Vector。 + * 扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%。 +* Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。 + +* Arraylist不是同步的,所以在不需要保证线程安全时时建议使用Arraylist。 + +#### 插入数据时,ArrayList、LinkedList、Vector谁速度较快?阐述 ArrayList、Vector、LinkedList 的存储性能和特性? + +* ArrayList和Vector 底层的实现都是使用数组方式存储数据。数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢。 + +* Vector 中的方法由于加了 synchronized 修饰,因此 **Vector** **是线程安全容器,但性能上较ArrayList差**。 + +* LinkedList 使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但插入数据时只需要记录当前项的前后项即可,所以 **LinkedList** **插入速度较快**。 + +#### 多线程场景下如何使用 ArrayList? + +* ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。例如像下面这样: + + List synchronizedList = Collections.synchronizedList(list); + synchronizedList.add("aaa"); + synchronizedList.add("bbb"); + + for (int i = 0; i < synchronizedList.size(); i++) { + System.out.println(synchronizedList.get(i)); + } + 复制代码 + +#### 为什么 ArrayList 的 elementData 加上 transient 修饰? + +* ArrayList 中的数组定义如下: + + private transient Object[] elementData; + +* 再看一下 ArrayList 的定义: + + public class ArrayList extends AbstractList + implements List, RandomAccess, Cloneable, java.io.Serializable + 复制代码 +* 可以看到 ArrayList 实现了 Serializable 接口,这意味着 ArrayList 支持序列化。transient 的作用是说不希望 elementData 数组被序列化,重写了 writeObject 实现: + + private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{ + *// Write out element count, and any hidden stuff* + int expectedModCount = modCount; + s.defaultWriteObject(); + *// Write out array length* + s.writeInt(elementData.length); + *// Write out all elements in the proper order.* + for (int i=0; i +* 每次序列化时,先调用 defaultWriteObject() 方法序列化 ArrayList 中的非 transient 元素,然后遍历 elementData,只序列化已存入的元素,这样既加快了序列化的速度,又减小了序列化之后的文件大小。 + +#### List 和 Set 的区别 + +* List , Set 都是继承自Collection 接口 + +* List 特点:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。 + +* Set 特点:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。 + +* 另外 List 支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。 + +* Set和List对比 + + * Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。 + * List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变 + +### Set接口 + +#### 说一下 HashSet 的实现原理? + +* HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为present,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,HashSet 不允许重复的值。 + +#### HashSet如何检查重复?HashSet是如何保证数据不可重复的? + +* 向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles 方法比较。 + +* HashSet 中的add ()方法会使用HashMap 的put()方法。 + +* HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为HashMap 的key,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复( HashMap 比较key是否相等是先比较hashcode 再比较equals )。 + +* 以下是HashSet 部分源码: + + private static final Object PRESENT = new Object(); + private transient HashMap map; + + public HashSet() { + map = new HashMap<>(); + } + + public boolean add(E e) { + // 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值 + return map.put(e, PRESENT)==null; + } + 复制代码 + +**hashCode()与equals()的相关规定**: + +1. 如果两个对象相等,则hashcode一定也是相同的 + * hashCode是jdk根据对象的地址或者字符串或者数字算出来的int类型的数值 +2. 两个对象相等,对两个equals方法返回true +3. 两个对象有相同的hashcode值,它们也不一定是相等的 +4. 综上,equals方法被覆盖过,则hashCode方法也必须被覆盖 +5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。 + +**==与equals的区别** + +1. ==是判断两个变量或实例是不是指向同一个内存空间 equals是判断两个变量或实例所指向的内存空间的值是不是相同 +2. ==是指对内存地址进行比较 equals()是对字符串的内容进行比较 + +#### HashSet与HashMap的区别 + +> | HashMap | HashSet | +> | --- | --- | +> | 实现了Map接口 | 实现Set接口 | +> | 存储键值对 | 仅存储对象 | +> | 调用put()向map中添加元素 | 调用add()方法向Set中添加元素 | +> | HashMap使用键(Key)计算Hashcode | HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false | +> | HashMap相对于HashSet较快,因为它是使用唯一的键获取对象 | HashSet较HashMap来说比较慢 | + +## Map接口 + +### 什么是Hash算法 + +* 哈希算法是指把任意长度的二进制映射为固定长度的较小的二进制值,这个较小的二进制值叫做哈希值。 + +### 什么是链表 + +* 链表是可以将物理地址上不连续的数据连接起来,通过指针来对物理地址进行操作,实现增删改查等功能。 + +* 链表大致分为单链表和双向链表 + + 1. 单链表:每个节点包含两部分,一部分存放数据变量的data,另一部分是指向下一节点的next指针 + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17173551e72891e5~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + 2. 双向链表:除了包含单链表的部分,还增加的pre前一个节点的指针 + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17173551e73f80b0~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +* 链表的优点 + + * 插入删除速度快(因为有next指针指向其下一个节点,通过改变指针的指向可以方便的增加删除元素) + * 内存利用率高,不会浪费内存(可以使用内存中细小的不连续空间(大于node节点的大小),并且在需要空间的时候才创建空间) + * 大小没有固定,拓展很灵活。 +* 链表的缺点 + + * 不能随机查找,必须从第一个开始遍历,查找效率低 + +### 说一下HashMap的实现原理? + +* HashMap概述: HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 + +* HashMap的数据结构: 在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。 + +* HashMap 基于 Hash 算法实现的 + + 1. 当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标 + + 2. 存储时,如果出现hash值相同的key,此时有两种情况。 + + ​ (1)如果key相同,则覆盖原始值; + + ​ (2)如果key不同(出现冲突),则将当前的key-value放入链表中 + + 3. 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。 + + 4. 理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。 + +* 需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn) + +### HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现 + +* 在Java中,保存数据有两种比较简单的数据结构:数组和链表。**数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做拉链法**的方式可以解决哈希冲突。 + +#### HashMap JDK1.8之前 + +* JDK1.8之前采用的是拉链法。**拉链法**:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17173551e78f59a7~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### HashMap JDK1.8之后 + +* 相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17173551e7c6af15~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +#### JDK1.7 VS JDK1.8 比较 + +* JDK1.8主要解决或优化了一下问题: + 1. resize 扩容优化 + 2. 引入了红黑树,目的是避免单条链表过长而影响查询效率,红黑树算法请参考 + 3. 解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。 + +> | 不同 | JDK 1.7 | JDK 1.8 | +> | --- | --- | --- | +> | 存储结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 | +> | 初始化方式 | 单独函数:`inflateTable()` | 直接集成到了扩容函数`resize()`中 | +> | hash值计算方式 | 扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算 | 扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算 | +> | 存放数据的规则 | 无冲突时,存放数组;冲突时,存放链表 | 无冲突时,存放数组;冲突 & 链表长度 < 8:存放单链表;冲突 & 链表长度 > 8:树化并存放红黑树 | +> | 插入数据方式 | 头插法(先讲原位置的数据移到后1位,再插入数据到该位置) | 尾插法(直接插入到链表尾部/红黑树) | +> | 扩容后存储位置的计算方式 | 全部按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1)) | 按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量) | + +### 什么是红黑树 + +#### 说道红黑树先讲什么是二叉树 + +* 二叉树简单来说就是 每一个节上可以关联俩个子节点 + + * 大概就是这样子: + a + / \ + b c + / \ / \ + d e f g + / \ / \ / \ / \ + h i j k l m n o + 复制代码 + +#### 红黑树 + +* 红黑树是一种特殊的二叉查找树。红黑树的每个结点上都有存储位表示结点的颜色,可以是红(Red)或黑(Black)。 ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17173552173a8a0c~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 红黑树的每个结点是黑色或者红色。当是不管怎么样他的根结点是黑色。每个叶子结点(叶子结点代表终结、结尾的节点)也是黑色 [注意:这里叶子结点,是指为空(NIL或NULL)的叶子结点!]。 + +* 如果一个结点是红色的,则它的子结点必须是黑色的。 + +* 每个结点到叶子结点NIL所经过的黑色结点的个数一样的。[确保没有一条路径会比其他路径长出俩倍,所以红黑树是相对接近平衡的二叉树的!] + +* 红黑树的基本操作是**添加、删除**。在对红黑树进行添加或删除之后,都会用到旋转方法。为什么呢?道理很简单,添加或删除红黑树中的结点之后,红黑树的结构就发生了变化,可能不满足上面三条性质,也就不再是一颗红黑树了,而是一颗普通的树。而通过旋转和变色,可以使这颗树重新成为红黑树。简单点说,旋转和变色的目的是让树保持红黑树的特性。 + +### HashMap的put方法的具体流程? + +* 当我们put的时候,首先计算 `key`的`hash`值,这里调用了 `hash`方法,`hash`方法实际是让`key.hashCode()`与`key.hashCode()>>>16`进行异或操作,高16bit补0,一个数和0异或不变,所以 hash 函数大概的作用就是:**高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞**。按照函数注释,因为bucket数组大小是2的幂,计算下标`index = (table.length - 1) & hash`,如果不做 hash 处理,相当于散列生效的只有几个低 bit 位,为了减少散列的碰撞,设计者综合考虑了速度、作用、质量之后,使用高16bit和低16bit异或来简单处理减少碰撞,而且JDK8中用了复杂度 O(logn)的树结构来提升碰撞下的性能。 + +* putVal方法执行流程图 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/1717355218a84ee7~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)public V put(K key, V value) { + return putVal(hash(key), key, value, false, true); +} + +static final int hash(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); +} + +//实现Map.put和相关方法 +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { + Node[] tab; Node p; int n, i; + // 步骤①:tab为空则创建 + // table未初始化或者长度为0,进行扩容 + if ((tab = table) == null || (n = tab.length) == 0) + n = (tab = resize()).length; + // 步骤②:计算index,并对null做处理 + // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中) + if ((p = tab[i = (n - 1) & hash]) == null) + tab[i] = newNode(hash, key, value, null); + // 桶中已经存在元素 + else { + Node e; K k; + // 步骤③:节点key存在,直接覆盖value + // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等 + if (p.hash == hash && + ((k = p.key) == key || (key != null && key.equals(k)))) + // 将第一个元素赋值给e,用e来记录 + e = p; + // 步骤④:判断该链为红黑树 + // hash值不相等,即key不相等;为红黑树结点 + // 如果当前元素类型为TreeNode,表示为红黑树,putTreeVal返回待存放的node, e可能为null + else if (p instanceof TreeNode) + // 放入树中 + e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); + // 步骤⑤:该链为链表 + // 为链表结点 + else { + // 在链表最末插入结点 + for (int binCount = 0; ; ++binCount) { + // 到达链表的尾部 + + //判断该链表尾部指针是不是空的 + if ((e = p.next) == null) { + // 在尾部插入新结点 + p.next = newNode(hash, key, value, null); + //判断链表的长度是否达到转化红黑树的临界值,临界值为8 + if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st + //链表结构转树形结构 + treeifyBin(tab, hash); + // 跳出循环 + break; + } + // 判断链表中结点的key值与插入的元素的key值是否相等 + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + // 相等,跳出循环 + break; + // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表 + p = e; + } + } + //判断当前的key已经存在的情况下,再来一个相同的hash值、key值时,返回新来的value这个值 + if (e != null) { + // 记录e的value + V oldValue = e.value; + // onlyIfAbsent为false或者旧值为null + if (!onlyIfAbsent || oldValue == null) + //用新值替换旧值 + e.value = value; + // 访问后回调 + afterNodeAccess(e); + // 返回旧值 + return oldValue; + } + } + // 结构性修改 + ++modCount; + // 步骤⑥:超过最大容量就扩容 + // 实际大小大于阈值则扩容 + if (++size > threshold) + resize(); + // 插入后回调 + afterNodeInsertion(evict); + return null; +} +复制代码 + +1. 判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容; +2. 根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③; +3. 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals; +4. 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向5; +5. 遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可; +6. 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。 + +### HashMap的扩容操作是怎么实现的? + +1. 在jdk1.8中,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容; + +2. 每次扩展的时候,都是扩展2倍; + +3. 扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。 + +* 在putVal()中,我们看到在这个函数里面使用到了2次resize()方法,resize()方法表示的在进行第一次初始化时会对其进行扩容,或者当该数组的实际大小大于其临界值值(第一次为12),这个时候在扩容的同时也会伴随的桶上面的元素进行重新分发,这也是JDK1.8版本的一个优化的地方,在1.7中,扩容之后需要重新去计算其Hash值,根据Hash值对其进行分发,但在1.8版本中,则是根据在同一个桶的位置中进行判断(e.hash & oldCap)是否为0,重新进行hash分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上 + + final Node[] resize() { + Node[] oldTab = table;//oldTab指向hash桶数组 + int oldCap = (oldTab == null) ? 0 : oldTab.length; + int oldThr = threshold; + int newCap, newThr = 0; + if (oldCap > 0) {//如果oldCap不为空的话,就是hash桶数组不为空 + if (oldCap >= MAXIMUM_CAPACITY) {//如果大于最大容量了,就赋值为整数最大的阀值 + threshold = Integer.MAX_VALUE; + return oldTab;//返回 + }//如果当前hash桶数组的长度在扩容后仍然小于最大容量 并且oldCap大于默认值16 + else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && + oldCap >= DEFAULT_INITIAL_CAPACITY) + newThr = oldThr << 1; // double threshold 双倍扩容阀值threshold + } + // 旧的容量为0,但threshold大于零,代表有参构造有cap传入,threshold已经被初始化成最小2的n次幂 + // 直接将该值赋给新的容量 + else if (oldThr > 0) // initial capacity was placed in threshold + newCap = oldThr; + // 无参构造创建的map,给出默认容量和threshold 16, 16*0.75 + else { // zero initial threshold signifies using defaults + newCap = DEFAULT_INITIAL_CAPACITY; + newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); + } + // 新的threshold = 新的cap * 0.75 + if (newThr == 0) { + float ft = (float)newCap * loadFactor; + newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? + (int)ft : Integer.MAX_VALUE); + } + threshold = newThr; + // 计算出新的数组长度后赋给当前成员变量table + @SuppressWarnings({"rawtypes","unchecked"}) + Node[] newTab = (Node[])new Node[newCap];//新建hash桶数组 + table = newTab;//将新数组的值复制给旧的hash桶数组 + // 如果原先的数组没有初始化,那么resize的初始化工作到此结束,否则进入扩容元素重排逻辑,使其均匀的分散 + if (oldTab != null) { + // 遍历新数组的所有桶下标 + for (int j = 0; j < oldCap; ++j) { + Node e; + if ((e = oldTab[j]) != null) { + // 旧数组的桶下标赋给临时变量e,并且解除旧数组中的引用,否则就数组无法被GC回收 + oldTab[j] = null; + // 如果e.next==null,代表桶中就一个元素,不存在链表或者红黑树 + if (e.next == null) + // 用同样的hash映射算法把该元素加入新的数组 + newTab[e.hash & (newCap - 1)] = e; + // 如果e是TreeNode并且e.next!=null,那么处理树中元素的重排 + else if (e instanceof TreeNode) + ((TreeNode)e).split(this, newTab, j, oldCap); + // e是链表的头并且e.next!=null,那么处理链表中元素重排 + else { // preserve order + // loHead,loTail 代表扩容后不用变换下标,见注1 + Node loHead = null, loTail = null; + // hiHead,hiTail 代表扩容后变换下标,见注1 + Node hiHead = null, hiTail = null; + Node next; + // 遍历链表 + do { + next = e.next; + if ((e.hash & oldCap) == 0) { + if (loTail == null) + // 初始化head指向链表当前元素e,e不一定是链表的第一个元素,初始化后loHead + // 代表下标保持不变的链表的头元素 + loHead = e; + else + // loTail.next指向当前e + loTail.next = e; + // loTail指向当前的元素e + // 初始化后,loTail和loHead指向相同的内存,所以当loTail.next指向下一个元素时, + // 底层数组中的元素的next引用也相应发生变化,造成lowHead.next.next..... + // 跟随loTail同步,使得lowHead可以链接到所有属于该链表的元素。 + loTail = e; + } + else { + if (hiTail == null) + // 初始化head指向链表当前元素e, 初始化后hiHead代表下标更改的链表头元素 + hiHead = e; + else + hiTail.next = e; + hiTail = e; + } + } while ((e = next) != null); + // 遍历结束, 将tail指向null,并把链表头放入新数组的相应下标,形成新的映射。 + if (loTail != null) { + loTail.next = null; + newTab[j] = loHead; + } + if (hiTail != null) { + hiTail.next = null; + newTab[j + oldCap] = hiHead; + } + } + } + } + } + return newTab; + } + 复制代码 + +### HashMap是怎么解决哈希冲突的? + +* 答:在解决这个问题之前,我们首先需要知道**什么是哈希冲突**,而在了解哈希冲突之前我们还要知道**什么是哈希**才行; + +#### 什么是哈希? + +* Hash,一般翻译为“散列”,也有直接音译为“哈希”的, Hash就是指使用哈希算法是指把任意长度的二进制映射为固定长度的较小的二进制值,这个较小的二进制值叫做哈希值。 + +#### 什么是哈希冲突? + +* **当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)**。 + +#### HashMap的数据结构 + +* 在Java中,保存数据有两种比较简单的数据结构:数组和链表。 + * 数组的特点是:寻址容易,插入和删除困难; + * 链表的特点是:寻址困难,但插入和删除容易; +* 所以我们将数组和链表结合在一起,发挥两者各自的优势,就可以使用俩种方式:链地址法和开放地址法可以解决哈希冲突: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171735521c92dc84~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位; +* 开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位。 +* **但相比于hashCode返回的int类型,我们HashMap初始的容量大小`DEFAULT_INITIAL_CAPACITY = 1 << 4`(即2的四次方16)要远小于int类型的范围,所以我们如果只是单纯的用hashCode取余来获取对应的bucket这将会大大增加哈希碰撞的概率,并且最坏情况下还会将HashMap变成一个单链表**,所以我们还需要对hashCode作一定的优化 + +#### hash()函数 + +* 上面提到的问题,主要是因为如果使用hashCode取余,那么相当于**参与运算的只有hashCode的低位**,高位是没有起到任何作用的,所以我们的思路就是让hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为**扰动**,在**JDK 1.8**中的hash()函数如下: + + static final int hash(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与自己右移16位进行异或运算(高低位异或) + } + 复制代码 +* 这比在**JDK 1.7**中,更为简洁,**相比在1.7中的4次位运算,5次异或运算(9次扰动),在1.8中,只进行了1次位运算和1次异或运算(2次扰动)**; + +#### 总结 + +* 简单总结一下HashMap是使用了哪些方法来有效解决哈希冲突的: + * 链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位; + * 开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位。 + +### 能否使用任何类作为 Map 的 key? + +可以使用任何类作为 Map 的 key,然而在使用之前,需要考虑以下几点: + +* 如果类重写了 equals() 方法,也应该重写 hashCode() 方法。 + +* 类的所有实例需要遵循与 equals() 和 hashCode() 相关的规则。 + +* 如果一个类没有使用 equals(),不应该在 hashCode() 中使用它。 + +* 用户自定义 Key 类最佳实践是使之为不可变的,这样 hashCode() 值可以被缓存起来,拥有更好的性能。不可变的类也可以确保 hashCode() 和 equals() 在未来不会改变,这样就会解决与可变相关的问题了。 + +### 为什么HashMap中String、Integer这样的包装类适合作为K? + +* 答:String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率 + * 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况 + * 内部已重写了`equals()`、`hashCode()`等方法,遵守了HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况; + +### 如果使用Object作为HashMap的Key,应该怎么办呢? + +* 答:重写`hashCode()`和`equals()`方法 + 1. **重写`hashCode()`是因为需要计算存储数据的存储位置**,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞; + 2. **重写`equals()`方法**,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,**目的是为了保证key在哈希表中的唯一性**; + +### HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标? + +* 答:`hashCode()`方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过`hashCode()`计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置; + +* **那怎么解决呢?** + + 1. HashMap自己实现了自己的`hash()`方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均; + + 2. 在保证数组长度为2的幂次方的时候,使用`hash()`运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,三来解决了“哈希值与数组大小范围不匹配”的问题; + +### HashMap 的长度为什么是2的幂次方 + +* 为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。 + +* **这个算法应该如何设计呢?** + + * 我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。 +* **那为什么是两次扰动呢?** + + * 答:这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的; + +### HashMap 与 HashTable 有什么区别? + +1. **线程安全**: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过 `synchronized` 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap ); +2. **效率**: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;(如果你要保证线程安全的话就使用 ConcurrentHashMap ); +3. **对Null key 和Null value的支持**: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛NullPointerException。 +4. **初始容量大小和每次扩充容量大小的不同** : + 1. 创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。 + 2. 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。 +5. **底层数据结构**: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。 +6. 推荐使用:在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代。 + +### 什么是TreeMap 简介 + +* TreeMap 是一个**有序的key-value集合**,它是通过红黑树实现的。 +* TreeMap基于**红黑树(Red-Black tree)实现**。该映射根据**其键的自然顺序进行排序**,或者根据**创建映射时提供的 Comparator 进行排序**,具体取决于使用的构造方法。 +* TreeMap是线程**非同步**的。 + +### 如何决定使用 HashMap 还是 TreeMap? + +* 对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历。 + +### HashMap 和 ConcurrentHashMap 的区别 + +1. ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的synchronized锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。(JDK1.8之后ConcurrentHashMap启用了一种全新的方式实现,利用CAS算法。) +2. HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。 + +### ConcurrentHashMap 和 Hashtable 的区别? + +* ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。 + + * **底层数据结构**: JDK1.7的 ConcurrentHashMap 底层采用 **分段的数组+链表** 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 **数组+链表** 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的; + * **实现线程安全的方式**: + 1. **在JDK1.7的时候,ConcurrentHashMap(分段锁)** 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。) **到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化)** 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本; + 2. ② **Hashtable(同一把锁)** :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。 +* **两者的对比图**: + +##### 1、HashTable: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171735521ca71b79~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +##### 2、 JDK1.7的ConcurrentHashMap: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171735521de4886d~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +##### 3、JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点): + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171735522b19186a~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 答:ConcurrentHashMap 结合了 HashMap 和 HashTable 二者的优势。HashMap 没有考虑同步,HashTable 考虑了同步的问题使用了synchronized 关键字,所以 HashTable 在每次同步执行时都要锁住整个结构。 ConcurrentHashMap 锁的方式是稍微细粒度的。 + +### ConcurrentHashMap 底层具体实现知道吗?实现原理是什么? + +#### JDK1.7 + +* 首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。 + +* 在JDK1.7中,ConcurrentHashMap采用Segment + HashEntry的方式进行实现,结构如下: + +* 一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/171735524c5089b8~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +1. 该类包含两个静态内部类 HashEntry 和 Segment ;前者用来封装映射表的键值对,后者用来充当锁的角色; +2. Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁。 + +#### JDK1.8 + +* 在**JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现**,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。 + +* 结构如下: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17173552564c22be~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* **附加源码,有需要的可以看看** + +* 插入元素过程(建议去看看源码): + +* 如果相应位置的Node还没有初始化,则调用CAS插入相应的数据; + + else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { + if (casTabAt(tab, i, null, new Node(hash, key, value, null))) + break; // no lock when adding to empty bin + } + 复制代码 +* 如果相应位置的Node不为空,且当前该节点不处于移动状态,则对该节点加synchronized锁,如果该节点的hash不小于0,则遍历链表更新节点或插入新节点; + + if (fh >= 0) { + binCount = 1; + for (Node e = f;; ++binCount) { + K ek; + if (e.hash == hash && + ((ek = e.key) == key || + (ek != null && key.equals(ek)))) { + oldVal = e.val; + if (!onlyIfAbsent) + e.val = value; + break; + } + Node pred = e; + if ((e = e.next) == null) { + pred.next = new Node(hash, key, value, null); + break; + } + } + } + 复制代码 + +1. 如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树中插入节点;如果binCount不为0,说明put操作对数据产生了影响,如果当前链表的个数达到8个,则通过treeifyBin方法转化为红黑树,如果oldVal不为空,说明是一次更新操作,没有对元素个数产生影响,则直接返回旧值; +2. 如果插入的是一个新节点,则执行addCount()方法尝试更新元素个数baseCount; + +## 辅助工具类 + +### Array 和 ArrayList 有何区别? + +* Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。 +* Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。 +* Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList 有。 + +`对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型的时候,这种方式相对比较慢。` + +### 如何实现 Array 和 List 之间的转换? + +* Array 转 List: Arrays. asList(array) ; +* List 转 Array:List 的 toArray() 方法。 + +### comparable 和 comparator的区别? + +* comparable接口实际上是出自java.lang包,它有一个 compareTo(Object obj)方法用来排序 +* comparator接口实际上是出自 java.util 包,它有一个compare(Object obj1, Object obj2)方法用来排序 + +* 一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo方法或compare方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的Collections.sort(). + +### Collection 和 Collections 有什么区别? + +* java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。 +* Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。 + +### TreeMap 和 TreeSet 在排序时如何比较元素?Collections 工具类中的 sort()方法如何比较元素? + +* TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素的 compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元素进 行排 序。 + +* Collections 工具类的 sort 方法有两种重载的形式, + +* 第一种要求传入的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比较; + +? + +* comparable接口实际上是出自java.lang包,它有一个 compareTo(Object obj)方法用来排序 +* comparator接口实际上是出自 java.util 包,它有一个compare(Object obj1, Object obj2)方法用来排序 + +* 一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo方法或compare方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的Collections.sort(). + +### Collection 和 Collections 有什么区别? + +* java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。 +* Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。 + +### TreeMap 和 TreeSet 在排序时如何比较元素?Collections 工具类中的 sort()方法如何比较元素? + +* TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素的 compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元素进 行排 序。 + +* Collections 工具类的 sort 方法有两种重载的形式, + +* 第一种要求传入的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比较; + +* 第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator 接口的子类型(需要重写 compare 方法实现元素的比较),相当于一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对回调模式的应用(Java 中对函数式编程的支持)。 + +作者:小杰要吃蛋 +链接:https://juejin.cn/post/6844904125939843079 +来源:稀土掘金 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 \ No newline at end of file diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Linux.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Linux.md" new file mode 100644 index 0000000..e799b7b --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Linux.md" @@ -0,0 +1,676 @@ + + +## Linux 概述 + +### 什么是Linux + +* Linux是一套免费使用和自由传播的类似Unix操作系统,一般的WEB项目都是部署都是放在Linux操作系统上面。 Linux是一个基于POSIX和Unix的多用户、多任务、支持多线程和多CPU的操作系统。它能运行主要的Unix工具软件、应用程序和网络协议。它支持32位和64位硬件。Linux继承了Unix以网络为核心的设计思想,是一个性能稳定的多用户网络操作系统。 + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744a2d148acc2~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +### Windows和Linux的区别 + +* Windows是微软开发的操作系统,民用操作系统,可用于娱乐、影音、上网。 Windows操作系统具有强大的日志记录系统和强大的桌面应用。好处是它可以帮我们实现非常多绚丽多彩的效果,可以非常方便去进行娱乐、影音、上网。 +* Linux的应用相对单纯很多,没有什么绚丽多彩的效果,因此Linux的性能是非常出色的,可以完全针对机器的配置有针对性的优化, +* 简单来说Windows适合普通用户进行娱乐办公使用,Linux适合软件开发部署 + +### Unix和Linux有什么区别? + +* Linux和Unix都是功能强大的操作系统,都是应用广泛的服务器操作系统,有很多相似之处,甚至有一部分人错误地认为Unix和Linux操作系统是一样的,然而,事实并非如此,以下是两者的区别。 + 1. 开源性 + Linux是一款开源操作系统,不需要付费,即可使用;Unix是一款对源码实行知识产权保护的传统商业软件,使用需要付费授权使用。 + 2. 跨平台性 + Linux操作系统具有良好的跨平台性能,可运行在多种硬件平台上;Unix操作系统跨平台性能较弱,大多需与硬件配套使用。 + 3. 可视化界面 + Linux除了进行命令行操作,还有窗体管理系统;Unix只是命令行下的系统。 + 4. 硬件环境 + Linux操作系统对硬件的要求较低,安装方法更易掌握;Unix对硬件要求比较苛刻,按照难度较大。 + 5. 用户群体 + Linux的用户群体很广泛,个人和企业均可使用;Unix的用户群体比较窄,多是安全性要求高的大型企业使用,如银行、电信部门等,或者Unix硬件厂商使用,如Sun等。 + 相比于Unix操作系统,Linux操作系统更受广大计算机爱好者的喜爱,主要原因是Linux操作系统具有Unix操作系统的全部功能,并且能够在普通PC计算机上实现全部的Unix特性,开源免费的特性,更容易普及使用! + +### 什么是 Linux 内核? + +* Linux 系统的核心是内核。内核控制着计算机系统上的所有硬件和软件,在必要时分配硬件,并根据需要执行软件。 + 1. 系统内存管理 + 2. 应用程序管理 + 3. 硬件设备管理 + 4. 文件系统管理 + +### Linux的基本组件是什么? + +* 就像任何其他典型的操作系统一样,Linux拥有所有这些组件:内核,shell和GUI,系统实用程序和应用程序。Linux比其他操作系统更具优势的是每个方面都附带其他功能,所有代码都可以免费下载。 + +### Linux 的体系结构 + +* 从大的方面讲,Linux 体系结构可以分为两块: + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744a2d1cc127a~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 用户空间(User Space) :用户空间又包括用户的应用程序(User Applications)、C 库(C Library) 。 +* 内核空间(Kernel Space) :内核空间又包括系统调用接口(System Call Interface)、内核(Kernel)、平台架构相关的代码(Architecture-Dependent Kernel Code) 。 + +**为什么 Linux 体系结构要分为用户空间和内核空间的原因?** + +* 1、现代 CPU 实现了不同的工作模式,不同模式下 CPU 可以执行的指令和访问的寄存器不同。 +* 2、Linux 从 CPU 的角度出发,为了保护内核的安全,把系统分成了两部分。 + +* 用户空间和内核空间是程序执行的**两种不同的状态**,我们可以通过两种方式完成用户空间到内核空间的转移: + * 系统调用; + * 硬件中断。 + +### BASH和DOS之间的基本区别是什么? + +* BASH和DOS控制台之间的主要区别在于3个方面: + * BASH命令区分大小写,而DOS命令则不区分; + * 在BASH下,/ character是目录分隔符,\作为转义字符。在DOS下,/用作命令参数分隔符,\是目录分隔符 + * DOS遵循命名文件中的约定,即8个字符的文件名后跟一个点,扩展名为3个字符。BASH没有遵循这样的惯例。 + +### Linux 开机启动过程? + +> 了解即可。 + +* 1、主机加电自检,加载 BIOS 硬件信息。 + +* 2、读取 MBR 的引导文件(GRUB、LILO)。 + +* 3、引导 Linux 内核。 + +* 4、运行第一个进程 init (进程号永远为 1 )。 + +* 5、进入相应的运行级别。 + +* 6、运行终端,输入用户名和密码。 + +### Linux系统缺省的运行级别? + +* 关机。 +* 单机用户模式。 +* 字符界面的多用户模式(不支持网络)。 +* 字符界面的多用户模式。 +* 未分配使用。 +* 图形界面的多用户模式。 +* 重启。 + +### Linux 使用的进程间通信方式? + +> 了解即可,不需要太深入。 + +* 1、管道(pipe)、流管道(s_pipe)、有名管道(FIFO)。 +* 2、信号(signal) 。 +* 3、消息队列。 +* 4、共享内存。 +* 5、信号量。 +* 6、套接字(socket) 。 + +### Linux 有哪些系统日志文件? + +* 比较重要的是 `/var/log/messages` 日志文件。 + +> 该日志文件是许多进程日志文件的汇总,从该文件可以看出任何入侵企图或成功的入侵。 +> +> 另外,如果胖友的系统里有 ELK 日志集中收集,它也会被收集进去。 + +### Linux系统安装多个桌面环境有帮助吗? + +* 通常,一个桌面环境,如KDE或Gnome,足以在没有问题的情况下运行。尽管系统允许从一个环境切换到另一个环境,但这对用户来说都是优先考虑的问题。有些程序在一个环境中工作而在另一个环境中无法工作,因此它也可以被视为选择使用哪个环境的一个因素。 + +### 什么是交换空间? + +* 交换空间是Linux使用的一定空间,用于临时保存一些并发运行的程序。当RAM没有足够的内存来容纳正在执行的所有程序时,就会发生这种情况。 + +### 什么是root帐户 + +* root帐户就像一个系统管理员帐户,允许你完全控制系统。你可以在此处创建和维护用户帐户,为每个帐户分配不同的权限。每次安装Linux时都是默认帐户。 + +### 什么是LILO? + +* LILO是Linux的引导加载程序。它主要用于将Linux操作系统加载到主内存中,以便它可以开始运行。 + +### 什么是BASH? + +* BASH是Bourne Again SHell的缩写。它由Steve Bourne编写,作为原始Bourne Shell(由/ bin / sh表示)的替代品。它结合了原始版本的Bourne Shell的所有功能,以及其他功能,使其更容易使用。从那以后,它已被改编为运行Linux的大多数系统的默认shell。 + +### 什么是CLI? + +* **命令行界面**(英语**:command-line interface**,缩写]**:CLI**)是在图形用户界面得到普及之前使用最为广泛的用户界面,它通常不支持鼠标,用户通过键盘输入指令,计算机接收到指令后,予以执行。也有人称之为**字符用户界面**(CUI)。 + +* 通常认为,命令行界面(CLI)没有图形用户界面(GUI)那么方便用户操作。因为,命令行界面的软件通常需要用户记忆操作的命令,但是,由于其本身的特点,命令行界面要较图形用户界面节约计算机系统的资源。在熟记命令的前提下,使用命令行界面往往要较使用图形用户界面的操作速度要快。所以,图形用户界面的操作系统中,都保留着可选的命令行界面。 + +### 什么是GUI? + +* 图形用户界面(Graphical User Interface,简称 GUI,又称图形用户接口)是指采用图形方式显示的计算机操作用户界面。 + +* 图形用户界面是一种人与计算机通信的界面显示格式,允许用户使用鼠标等输入设备操纵屏幕上的图标或菜单选项,以选择命令、调用文件、启动程序或执行其它一些日常任务。与通过键盘输入文本或字符命令来完成例行任务的字符界面相比,图形用户界面有许多优点。 + +### 开源的优势是什么? + +* 开源允许你将软件(包括源代码)免费分发给任何感兴趣的人。然后,人们可以添加功能,甚至可以调试和更正源代码中的错误。它们甚至可以让它运行得更好,然后再次自由地重新分配这些增强的源代码。这最终使社区中的每个人受益。 + +### GNU项目的重要性是什么? + +* 这种所谓的自由软件运动具有多种优势,例如可以自由地运行程序以及根据你的需要自由学习和修改程序。它还允许你将软件副本重新分发给其他人,以及自由改进软件并将其发布给公众。 + +## 磁盘、目录、文件 + +### 简单 Linux 文件系统? + +**在 Linux 操作系统中,所有被操作系统管理的资源,例如网络接口卡、磁盘驱动器、打印机、输入输出设备、普通文件或是目录都被看作是一个文件。** + +* 也就是说在 Linux 系统中有一个重要的概念**:一切都是文件**。其实这是 Unix 哲学的一个体现,而 Linux 是重写 Unix 而来,所以这个概念也就传承了下来。在 Unix 系统中,把一切资源都看作是文件,包括硬件设备。UNIX系统把每个硬件都看成是一个文件,通常称为设备文件,这样用户就可以用读写文件的方式实现对硬件的访问。 + +* Linux 支持 5 种文件类型,如下图所示: + + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744a2d70c1faf~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +### Linux 的目录结构是怎样的? + +> 这个问题,一般不会问。更多是实际使用时,需要知道。 + +* Linux 文件系统的结构层次鲜明,就像一棵倒立的树,最顶层是其根目录: + ![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/14/171744a2d6e0c867~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp)**常见目录说明**: + +> | 目录 | 介绍 | +> | --- | --- | +> | /bin | 存放二进制可执行文件(ls,cat,mkdir等),常用命令一般都在这里; | +> | /etc | 存放系统管理和配置文件; | +> | /home | 存放所有用户文件的根目录,是用户主目录的基点,比如用户user的主目录就是/home/user,可以用~user表示; | +> | /usr | 用于存放系统应用程序; | +> | /opt | 额外安装的可选应用程序包所放置的位置。一般情况下,我们可以把tomcat等都安装到这里; | +> | /proc | 虚拟文件系统目录,是系统内存的映射。可直接访问这个目录来获取系统信息; | +> | /root | 超级用户(系统管理员)的主目录(特权阶级); | +> | /sbin | 存放二进制可执行文件,只有root才能访问。这里存放的是系统管理员使用的系统级别的管理命令和程序。如ifconfig等; | +> | /dev | 用于存放设备文件; | +> | /mnt | 系统管理员安装临时文件系统的安装点,系统提供这个目录是让用户临时挂载其他的文件系统; | +> | /boot | 存放用于系统引导时使用的各种文件; | +> | /lib | 存放着和系统运行相关的库文件 ; | +> | /tmp | 用于存放各种临时文件,是公用的临时文件存储点; | +> | /var | 用于存放运行时需要改变数据的文件,也是某些大文件的溢出区,比方说各种服务的日志文件(系统启动日志等。)等; | +> | /lost+found | 这个目录平时是空的,系统非正常关机而留下“无家可归”的文件(windows下叫什么.chk)就在这里 | + +### 什么是 inode ? + +> 一般来说,面试不会问 inode 。但是 inode 是一个重要概念,是理解 Unix/Linux 文件系统和硬盘储存的基础。 + +* 理解inode,要从文件储存说起。 + +* 文件储存在硬盘上,硬盘的最小存储单位叫做"扇区"(Sector)。每个扇区储存512字节(相当于0.5KB)。 + +* 操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个"块"(block)。这种由多个扇区组成的"块",是文件存取的最小单位。"块"的大小,最常见的是4KB,即连续八个 sector组成一个 block。 + +* 文件数据都储存在"块"中,那么很显然,我们还必须找到一个地方储存文件的元信息,比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做inode,中文译名为"索引节点"。 + +* 每一个文件都有对应的inode,里面包含了与该文件有关的一些信息。 + +**简述 Linux 文件系统通过 i 节点把文件的逻辑结构和物理结构转换的工作过程?** + +> 如果看的一脸懵逼,也没关系。一般来说,面试官不太会问这个题目。 + +* Linux 通过 inode 节点表将文件的逻辑结构和物理结构进行转换。 + * inode 节点是一个 64 字节长的表,表中包含了文件的相关信息,其中有文件的大小、文件所有者、文件的存取许可方式以及文件的类型等重要信息。在 inode 节点表中最重要的内容是磁盘地址表。在磁盘地址表中有 13 个块号,文件将以块号在磁盘地址表中出现的顺序依次读取相应的块。 + * Linux 文件系统通过把 inode 节点和文件名进行连接,当需要读取该文件时,文件系统在当前目录表中查找该文件名对应的项,由此得到该文件相对应的 inode 节点号,通过该 inode 节点的磁盘地址表把分散存放的文件物理块连接成文件的逻辑结构。 + +### 什么是硬链接和软链接? + +* **硬链接**:由于 Linux 下的文件是通过索引节点(inode)来识别文件,硬链接可以认为是一个指针,指向文件索引节点的指针,系统并不为它重新分配 inode 。每添加一个一个硬链接,文件的链接数就加 1 。 + + * 不足: + 1. 不可以在不同文件系统的文件间建立链接; + 2. 只有超级用户才可以为目录创建硬链接。 +* **软链接**:软链接克服了硬链接的不足,没有任何文件系统的限制,任何用户可以创建指向目录的符号链接。因而现在更为广泛使用,它具有更大的灵活性,甚至可以跨越不同机器、不同网络对文件进行链接。 + + * 不足:因为链接文件包含有原文件的路径信息,所以当原文件从一个目录下移到其他目录中,再访问链接文件,系统就找不到了,而硬链接就没有这个缺陷,你想怎么移就怎么移;还有它要系统分配额外的空间用于建立新的索引节点和保存原文件的路径。 +* **实际场景下,基本是使用软链接**。总结区别如下: + + * 硬链接不可以跨分区,软件链可以跨分区。 + * 硬链接指向一个 inode 节点,而软链接则是创建一个新的 inode 节点。 + * 删除硬链接文件,不会删除原文件,删除软链接文件,会把原文件删除。 + +### RAID 是什么? + +> RAID 全称为独立磁盘冗余阵列(Redundant Array of Independent Disks),基本思想就是把多个相对便宜的硬盘组合起来,成为一个硬盘阵列组,使性能达到甚至超过一个价格昂贵、 容量巨大的硬盘。RAID 通常被用在服务器电脑上,使用完全相同的硬盘组成一个逻辑扇区,因此操作系统只会把它当做一个硬盘。 +> +> RAID 分为不同的等级,各个不同的等级均在数据可靠性及读写性能上做了不同的权衡。在实际应用中,可以依据自己的实际需求选择不同的 RAID 方案。 + +* 当然,因为很多公司都使用云服务,大家很难接触到 RAID 这个概念,更多的可能是普通云盘、SSD 云盘酱紫的概念。 + +## 安全 + +### 一台 Linux 系统初始化环境后需要做一些什么安全工作? + +* 1、添加普通用户登陆,禁止 root 用户登陆,更改 SSH 端口号。 + + > 修改 SSH 端口不一定绝对哈。当然,如果要暴露在外网,建议改下。l + +* 2、服务器使用密钥登陆,禁止密码登陆。 + +* 3、开启防火墙,关闭 SElinux ,根据业务需求设置相应的防火墙规则。 + +* 4、装 fail2ban 这种防止 SSH 暴力破击的软件。 + +* 5、设置只允许公司办公网出口 IP 能登陆服务器(看公司实际需要) + + > 也可以安装 VPN 等软件,只允许连接 VPN 到服务器上。 + +* 6、修改历史命令记录的条数为 10 条。 + +* 7、只允许有需要的服务器可以访问外网,其它全部禁止。 + +* 8、做好软件层面的防护。 + + * 8.1 设置 nginx_waf 模块防止 SQL 注入。 + * 8.2 把 Web 服务使用 www 用户启动,更改网站目录的所有者和所属组为 www 。 + +### 什么叫 CC 攻击?什么叫 DDOS 攻击? + +* CC 攻击,主要是用来攻击页面的,模拟多个用户不停的对你的页面进行访问,从而使你的系统资源消耗殆尽。 + +* DDOS 攻击,中文名叫分布式拒绝服务攻击,指借助服务器技术将多个计算机联合起来作为攻击平台,来对一个或多个目标发动 DDOS 攻击。 + + > 攻击,即是通过大量合法的请求占用大量网络资源,以达到瘫痪网络的目的。 + +**怎么预防 CC 攻击和 DDOS 攻击?** + +* 防 CC、DDOS 攻击,这些只能是用硬件防火墙做流量清洗,将攻击流量引入黑洞。 + +> 流量清洗这一块,主要是买 ISP 服务商的防攻击的服务就可以,机房一般有空余流量,我们一般是买服务,毕竟攻击不会是持续长时间。 + +### 什么是网站数据库注入? + +* 由于程序员的水平及经验参差不齐,大部分程序员在编写代码的时候,没有对用户输入数据的合法性进行判断。 +* 应用程序存在安全隐患。用户可以提交一段数据库查询代码,根据程序返回的结果,获得某些他想得知的数据,这就是所谓的 SQL 注入。 +* SQL注入,是从正常的 WWW 端口访问,而且表面看起来跟一般的 Web 页面访问没什么区别,如果管理员没查看日志的习惯,可能被入侵很长时间都不会发觉。 + +**如何过滤与预防?** + +* 数据库网页端注入这种,可以考虑使用 nginx_waf 做过滤与预防。 + +### Shell 脚本是什么? + +* 一个 Shell 脚本是一个文本文件,包含一个或多个命令。作为系统管理员,我们经常需要使用多个命令来完成一项任务,我们可以添加这些所有命令在一个文本文件(Shell 脚本)来完成这些日常工作任务。 + +## 实战 + +### 如何选择 Linux 操作系统版本? + +**一般来讲,桌面用户首选 Ubuntu ;服务器首选 RHEL 或 CentOS ,两者中首选 CentOS 。** + +* 根据具体要求: + + * 安全性要求较高,则选择 Debian 或者 FreeBSD 。 + + * 需要使用数据库高级服务和电子邮件网络应用的用户可以选择 SUSE 。 + + * 想要新技术新功能可以选择 Feddora ,Feddora 是 RHEL 和 CentOS 的一个测试版和预发布版本。 + + * 【重点】**根据现有状况,绝大多数互联网公司选择 CentOS 。现在比较常用的是 6 系列,现在市场占有大概一半左右。另外的原因是 CentOS 更侧重服务器领域,并且无版权约束**。 + + > CentOS 7 系列,也慢慢使用的会比较多了。 + +### 如何规划一台 Linux 主机,步骤是怎样? + +* 1、确定机器是做什么用的,比如是做 WEB 、DB、还是游戏服务器。 + + > 不同的用途,机器的配置会有所不同。 + +* 2、确定好之后,就要定系统需要怎么安装,默认安装哪些系统、分区怎么做。 + +* 3、需要优化系统的哪些参数,需要创建哪些用户等等的。 + +### 请问当用户反馈网站访问慢,你会如何处理? + +**有哪些方面的因素会导致网站网站访问慢?** + +* 1、服务器出口带宽不够用 + + > * 本身服务器购买的出口带宽比较小。一旦并发量大的话,就会造成分给每个用户的出口带宽就小,访问速度自然就会慢。 + > * 跨运营商网络导致带宽缩减。例如,公司网站放在电信的网络上,那么客户这边对接是长城宽带或联通,这也可能导致带宽的缩减。 + +* 2、服务器负载过大,导致响应不过来 + + > 可以从两个方面入手分析: + > + > * 分析系统负载,使用 w 命令或者 uptime 命令查看系统负载。如果负载很高,则使用 top 命令查看 CPU ,MEM 等占用情况,要么是 CPU 繁忙,要么是内存不够。 + > * 如果这二者都正常,再去使用 sar 命令分析网卡流量,分析是不是遭到了攻击。一旦分析出问题的原因,采取对应的措施解决,如决定要不要杀死一些进程,或者禁止一些访问等。 + +* 3、数据库瓶颈 + + > * 如果慢查询比较多。那么就要开发人员或 DBA 协助进行 SQL 语句的优化。 + > * 如果数据库响应慢,考虑可以加一个数据库缓存,如 Redis 等。然后,也可以搭建 MySQL 主从,一台 MySQL 服务器负责写,其他几台从数据库负责读。 + +* 4、网站开发代码没有优化好 + + > * 例如 SQL 语句没有优化,导致数据库读写相当耗时。 + +**针对网站访问慢,怎么去排查?** + +* 1、首先要确定是用户端还是服务端的问题。当接到用户反馈访问慢,那边自己立即访问网站看看,如果自己这边访问快,基本断定是用户端问题,就需要耐心跟客户解释,协助客户解决问题。 + + > 不要上来就看服务端的问题。一定要从源头开始,逐步逐步往下。 + +* 2、如果访问也慢,那么可以利用浏览器的调试功能,看看加载那一项数据消耗时间过多,是图片加载慢,还是某些数据加载慢。 + +* 3、针对服务器负载情况。查看服务器硬件(网络、CPU、内存)的消耗情况。如果是购买的云主机,比如阿里云,可以登录阿里云平台提供各方面的监控,比如 CPU、内存、带宽的使用情况。 + +* 4、如果发现硬件资源消耗都不高,那么就需要通过查日志,比如看看 MySQL慢查询的日志,看看是不是某条 SQL 语句查询慢,导致网站访问慢。 + +**怎么去解决?** + +* 1、如果是出口带宽问题,那么久申请加大出口带宽。 +* 2、如果慢查询比较多,那么就要开发人员或 DBA 协助进行 SQL 语句的优化。 +* 3、如果数据库响应慢,考虑可以加一个数据库缓存,如 Redis 等等。然后也可以搭建MySQL 主从,一台 MySQL 服务器负责写,其他几台从数据库负责读。 +* 4、申请购买 CDN 服务,加载用户的访问。 +* 5、如果访问还比较慢,那就需要从整体架构上进行优化咯。做到专角色专用,多台服务器提供同一个服务。 + +### Linux 性能调优都有哪几种方法? + +* 1、Disabling daemons (关闭 daemons)。 +* 2、Shutting down the GUI (关闭 GUI)。 +* 3、Changing kernel parameters (改变内核参数)。 +* 4、Kernel parameters (内核参数)。 +* 5、Tuning the processor subsystem (处理器子系统调优)。 +* 6、Tuning the memory subsystem (内存子系统调优)。 +* 7、Tuning the file system (文件系统子系统调优)。 +* 8、Tuning the network subsystem(网络子系统调优)。 + +## 基本命令 + +###### cd (change directory:英文释义是改变目录)切换目录 + +cd ../ ;跳到上级目录 +cd /opt ;不管现在到那直接跳到指定的opt文件夹中 +cd ~ ;切换当前用户的家目录。root用户的家目录就是root目录。 +复制代码 + +###### pwd (print working directory:显示当前工作目录的绝对路径) + +pwd +显示当前的绝对路劲 +复制代码 + +###### ls (ls:list的缩写,查看列表)查看当前目录下的所有文件夹(ls 只列出文件名或目录名) + +ls -a ;显示所有文件夹,隐藏文件也显示出来 +ls -R ;连同子目录一起列出来 +复制代码 + +###### ll (ll:list的缩写,查看列表详情)查看当前目录下的所有详细信息和文件夹(ll 结果是详细,有时间,是否可读写等信息) + +ll -a ;显示所有文件,隐藏文件也显示出来 +ll -R ;连同子目录内容一起列出来 +ll -h ;友好展示详情信息,可以看大小 +ll -al ;即能显示隐藏文件又能显示详细列表。 +复制代码 + +###### touch (touch:创建文件)创建文件 + +touch test.txt ;创建test.txt文件 +touch /opt/java/test.java ;在指定目录创建test.java文件 +复制代码 + +###### mkdir (mkdir:创建目录) 创建目录 + +mkdir 文件夹名称 ;在此目录创建文件夹 +mkdir /opt/java/jdk ;在指定目录创建文件夹 +复制代码 + +###### cat (concatenate:显示或把多个文本文件连接起来)查看文件命令(可以快捷查看当前文件的内容)(不能快速定位到最后一页) + +cat lj.log ;快捷查看文件命令 +Ctrl + c ;暂停显示文件 +Ctrl + d ;退出查看文件命令 +复制代码 + +###### more (more:更多的意思)分页查看文件命令(不能快速定位到最后一页) + +回车:向下n行,需要定义,默认为1行。 +空格键:向下滚动一屏或Ctrl+F +B:返回上一层或Ctrl+B +q:退出more +复制代码 + +###### less (lese:较少的意思)分页查看文件命令(可以快速定位到最后一页) + +less -m 显示类似于more命令的百分比。 +less -N 显示每行的行号。(大写的N) +两参数一起使用如:less -mN 文件名,如此可分页并显示行号。 + +空格键:前下一页或page down。 +回车:向下一行。 +b:后退一页 或 page up。 +q:退出。 +d:前进半页。 +u:后退半页 +复制代码 + +###### tail(尾巴) 查看文件命令(看最后多少行) + +tail -10 ;文件名 看最后10行 +复制代码 + +###### cp(copy单词缩写,复制功能) + +cp /opt/java/java.log /opt/logs/ ;把java.log 复制到/opt/logs/下 +cp /opt/java/java.log /opt/logs/aaa.log ;把java.log 复制到/opt/logs/下并且改名为aaa.log +cp -r /opt/java /opt/logs ;把文件夹及内容复制到logs文件中 +复制代码 + +###### mv(move单词缩写,移动功能,该文件名称功能) + +mv /opt/java/java.log /opt/mysql/ ;移动文件到mysql目录下 +mv java.log mysql.log ;把java.log改名为mysql.log +复制代码 + +###### rm(remove:移除的意思)删除文件,或文件夹 + +-f或--force 强制删除文件或目录。删除文件不包括文件夹的文件 +-r或-R或--recursive 递归处理,将指定目录下的所有文件及子目录一并删除。 +-rf 强制删除文件夹及内容 + +rm 文件名 ;安全删除命令 (yes删除 no取消) +rm -rf 强制删除文件夹及内容 +rm -rf * 删除当前目录下的所有内容。 +rm -rf /* 删除Linux系统根目录下所有的内容。系统将完蛋。 +复制代码 + +###### find (find:找到的意思)查找指定文件或目录 + +* 表示0~多个任意字符。 + +find -name 文件名;按照指定名称查找在当前目录下查找文件 +find / -name 文件名按照指定名称全局查找文件 +find -name '*文件名' ;任意前缀加上文件名在当前目录下查找文件 +find / -name '*文件名*' ;全局进行模糊查询带文件名的文件 +复制代码 + +###### vi (VIsual:视觉)文本编辑器 类似win的记事本 (操作类似于地下的vim命令,看底下vim 的操作) + +###### vim (VI IMproved:改进版视觉)改进版文本编辑器 (不管是文件查看还是文件编辑 按 Shift + 上或者下可以上下移动查看视角) + +输入”vim 文件名” 打开文件,刚刚时是”一般模式”。 + +一般模式:可以浏览文件内容,可以进行文本快捷操作。如单行复制,多行复制,单行删除,多行删除,(退出)等。 +插入模式:可以编辑文件内容。 +底行模式:可以进行强制退出操作,不保存 :q! + 可以进行保存并退出操作 :wq + +按下”i”或”a”或”o”键,从”一般模式”,进入”插入模式(编辑模式)”。 +在编辑模式下按”Esc” 即可到一般模式 +在一般模式下按”:”,冒号进入底行模式。 + +在一般模式下的快捷键 + dd ;删除一整行 + X ;向前删除 等同于windowns系统中的删除键 + x ;向后删除和大写x相反方向 + Ctrl + f ;向后看一页 + Ctrl + b ;向前看一页 + u ;撤销上一步操作 + /word ;向下查找word关键字 输入:n查找下一个,N查找上一个(不管是哪个查找都是全局查找 只不过n的方向相反) + ?log ;向上查找log关键字 输入:n查找上一个,N查找下一个 + :1,90s/redis/Redis/g ;把1-90行的redis替换为Redis。语法n1,n2s/原关键字/新关键字/g,n1代表其实行,n2代表结尾行,g是必须要的 + :0 ;光标移动到第一行 + :$ ;光标移动到最后一行 + :300 ;光标移动到300行,输入多少数字移动到多少行 + :w ;保存 + :w! ;强制保存 + :q ;退出 + :q! ;强制退出 + 5dd ;删除后面5行,打一个参数为自己填写 + 5x ;删除此光标后面5个字符 + d1G ;删除此光标之前的所有 + d0 ;从光标当前位置删除到此行的第一个位置 + yy ;复制 + p ;在光标的下面进行粘贴 + P ;在光标的上门进行粘贴 +复制代码 + +###### | 管道命令(把多个命令组合起来使用) + +管道命令的语法:命令1 | 命令2 | 命令3。 +复制代码 + +###### grep (grep :正则表达式)正则表达式,用于字符串的搜索工作(模糊查询)。不懂可以先过 + +单独使用: +grep String test.java ;在test.java文件中查找String的位置,返回整行 +一般此命令不会单独使用下面列几个常用的命令(地下通过管道命令组合起来使用) + +ps aux|grep java ;查找带java关键字的进程 +ll |grep java ;查找带java关键字的文件夹及文件 +复制代码 + +###### yum install -y lrzsz 命令(实现win到Linux文件互相简单上传文件) + +#(实际上就是在Linux系统中下载了一个插件)下了了此安装包后就可以实现win系统到linux之间拉文件拉文件 +#等待下载完了就可以输入: + +rz 从win系统中选择文件上传到Linux系统中 + +sz 文件名 选择Linux系统的文件复制到win系统中 + +复制代码 + +###### tar (解压 压缩 命令) + +常用的组合命令: +-z 是否需要用gzip压缩。 +-c 建立一个压缩文件的参数指令(create) –压缩 + -x 解开一个压缩文件的参数指令(extract) –解压 + -v 压缩的过程中显示文件(verbose) + -f 使用档名,在f之后要立即接档中(file) + 常用解压参数组合:zxvf + 常用压缩参数组合:zcvf + +解压命令: +tar -zxvf redis-3.2.8.tar.gz ;解压到当前文件夹 +tar -zxvf redis-3.2.8.tar.gz -C /opt/java/ ;解压到指定目录 + +压缩命令:(注意 语法有点反了,我反正每次都搞反) +tar -zcvf redis-3.2.8.tar.gz redis-3.2.8/ ;语法 tar -zcvf 压缩后的名称 要压缩的文件 +tar -zcvf 压缩后的文件(可指定目录) 要压缩的文件(可指定目录) +复制代码 + +###### ps (process status:进程状态,类似于windows的任务管理器) + +常用组合:ps -ef 标准的格式查看系统进程 + ps -aux BSD格式查看系统进程 + ps -aux|grep redis BSD格式查看进程名称带有redis的系统进程(常用技巧) +//显示进程的一些属性,需要了解(ps aux) +USER //用户名 +PID //进程ID号,用来杀死进程的 +%CPU //进程占用的CPU的百分比 +%MEM //占用内存的的百分比 +VSZ //该进程使用的虚拟內存量(KB) +RSS //该进程占用的固定內存量(KB) +STAT //进程的状态 +START //该进程被触发启动时间 +TIME //该进程实际使用CPU运行的时间 +复制代码 + +###### clear 清屏命令。(强迫症患者使用) + +kill 命令用来中止一个进程。(要配合ps命令使用,配合pid关闭进程) +(ps类似于打开任务管理器,kill类似于关闭进程) + kill -5 进程的PID ;推荐,和平关闭进程 + kill -9 PID ;不推荐,强制杀死进程 +复制代码 + +###### ifconfig命令 + +用于查看和更改网络接口的地址和参数,包括IP地址、网络掩码、广播地址,使用权限是超级用户。(一般是用来查看的,很少更改) +如果此命令输入无效,先输入yum -y install net-tools +ifconfig +复制代码 + +###### ping (用于检测与目标的连通性)语法:ping ip地址 + +测试: +1、在Windows操作系统中cmdipconfig,查看本机IP地址: +2、再到LInux系统中输入 ping ip地址 +(公司电脑,我就不暴露Ip了,没图片 自己去试) +按Ctrl + C 可以停止测试。 +复制代码 + +###### free 命令 (显示系统内存) + +#显示系统内存使用情况,包括物理内存、交互区内存(swap)和内核缓冲区内存。 +-b 以Byte显示内存使用情况 +-k 以kb为单位显示内存使用情况 +-m 以mb为单位显示内存使用情况 +-g 以gb为单位显示内存使用情况 +-s<间隔秒数> 持续显示内存 +-t 显示内存使用总合 +复制代码 + +###### top 命令 + +#显示当前系统正在执行的进程的相关信息,包括进程 ID、内存占用率、CPU 占用率等 +-c 显示完整的进程命令 +-s 保密模式 +-p <进程号> 指定进程显示 +-n <次数>循环显示次数 +复制代码 + +###### netstat 命令 + +#Linux netstat命令用于显示网络状态。 +#利用netstat指令可让你得知整个Linux系统的网络情况。 +#语法: +netstat [-acCeFghilMnNoprstuvVwx][-A<网络类型>][--ip] +复制代码 + +###### file (可查看文件类型) + +file 文件名 +复制代码 + +###### 重启linux + +Linux centos 重启命令:reboot +复制代码 + +###### 关机linux + +Linux centos 关机命令:halt + +复制代码 + +###### 同步时间命令 + +ntpdate ntp1.aliyun.com +复制代码 + +###### 更改为北京时间命令 + +rm -rf /etc/localtime +ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime +复制代码 + +###### 查看时间命令: + +date + +作者:小杰要吃蛋 +链接:https://juejin.cn/post/6844904127059738637 +来源:稀土掘金 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 \ No newline at end of file diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Nginx.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Nginx.md" new file mode 100644 index 0000000..a1d00e1 --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/Nginx.md" @@ -0,0 +1,516 @@ + + +### 什么是Nginx? + +* Nginx是一个 轻量级/高性能的反向代理Web服务器,他实现非常高效的反向代理、负载平衡,他可以处理2-3万并发连接数,官方监测能支持5万并发,现在中国使用nginx网站用户有很多,例如:新浪、网易、 腾讯等。 + +### 为什么要用Nginx? + +* 跨平台、配置简单、方向代理、高并发连接:处理2-3万并发连接数,官方监测能支持5万并发,内存消耗小:开启10个nginx才占150M内存 ,nginx处理静态文件好,耗费内存少, + +* 而且Nginx内置的健康检查功能:如果有一个服务器宕机,会做一个健康检查,再发送的请求就不会发送到宕机的服务器了。重新将请求提交到其他的节点上。 + +* 使用Nginx的话还能: + + 1. 节省宽带:支持GZIP压缩,可以添加浏览器本地缓存 + 2. 稳定性高:宕机的概率非常小 + 3. 接收用户请求是异步的 + +### 为什么Nginx性能这么高? + +* 因为他的事件处理机制:异步非阻塞事件处理机制:运用了epoll模型,提供了一个队列,排队解决 + +### Nginx怎么处理请求的? + +* nginx接收一个请求后,首先由listen和server_name指令匹配server模块,再匹配server模块里的location,location就是实际地址 + +```java +server { # 第一个Server区块开始,表示一个独立的虚拟主机站点 + listen 80; # 提供服务的端口,默认80 + server_name localhost; # 提供服务的域名主机名 + location / { # 第一个location区块开始 + root html; # 站点的根目录,相当于Nginx的安装目录 + index index.html index.htm; # 默认的首页文件,多个用空格分开 + } # 第一个location区块结果 + } +``` +### 什么是正向代理和反向代理? + +1. 正向代理就是一个人发送一个请求直接就到达了目标的服务器 +2. 反方代理就是请求统一被Nginx接收,nginx反向代理服务器接收到之后,按照一定的规 则分发给了后端的业务处理服务器进行处理了 + +### 使用“反向代理服务器的优点是什么? + +* 反向代理服务器可以隐藏源服务器的存在和特征。它充当互联网云和web服务器之间的中间层。这对于安全方面来说是很好的,特别是当您使用web托管服务时。 + +### Nginx的优缺点? + +* 优点: + + 1. 占内存小,可实现高并发连接,处理响应快 + 2. 可实现http服务器、虚拟主机、方向代理、负载均衡 + 3. Nginx配置简单 + 4. 可以不暴露正式的服务器IP地址 +* 缺点: 动态处理差:nginx处理静态文件好,耗费内存少,但是处理动态页面则很鸡肋,现在一般前端用nginx作为反向代理抗住压力, + +### Nginx应用场景? + +1. http服务器。Nginx是一个http服务可以独立提供http服务。可以做网页静态服务器。 +2. 虚拟主机。可以实现在一台服务器虚拟出多个网站,例如个人网站使用的虚拟机。 +3. 反向代理,负载均衡。当网站的访问量达到一定程度后,单台服务器不能满足用户的请求时,需要用多台服务器集群可以使用nginx做反向代理。并且多台服务器可以平均分担负载,不会应为某台服务器负载高宕机而某台服务器闲置的情况。 +4. nginz 中也可以配置安全管理、比如可以使用Nginx搭建API接口网关,对每个接口服务进行拦截。 + +### Nginx目录结构有哪些? + +```java +[root@localhost ~]# tree /usr/local/nginx +/usr/local/nginx +├── client_body_temp +├── conf # Nginx所有配置文件的目录 +│ ├── fastcgi.conf # fastcgi相关参数的配置文件 +│ ├── fastcgi.conf.default # fastcgi.conf的原始备份文件 +│ ├── fastcgi_params # fastcgi的参数文件 +│ ├── fastcgi_params.default +│ ├── koi-utf +│ ├── koi-win +│ ├── mime.types # 媒体类型 +│ ├── mime.types.default +│ ├── nginx.conf # Nginx主配置文件 +│ ├── nginx.conf.default +│ ├── scgi_params # scgi相关参数文件 +│ ├── scgi_params.default +│ ├── uwsgi_params # uwsgi相关参数文件 +│ ├── uwsgi_params.default +│ └── win-utf +├── fastcgi_temp # fastcgi临时数据目录 +├── html # Nginx默认站点目录 +│ ├── 50x.html # 错误页面优雅替代显示文件,例如当出现502错误时会调用此页面 +│ └── index.html # 默认的首页文件 +├── logs # Nginx日志目录 +│ ├── access.log # 访问日志文件 +│ ├── error.log # 错误日志文件 +│ └── nginx.pid # pid文件,Nginx进程启动后,会把所有进程的ID号写到此文件 +├── proxy_temp # 临时目录 +├── sbin # Nginx命令目录 +│ └── nginx # Nginx的启动命令 +├── scgi_temp # 临时目录 +└── uwsgi_temp # 临时目录 +``` +### Nginx配置文件nginx.conf有哪些属性模块? + +```java +worker_processes 1; # worker进程的数量 +events { # 事件区块开始 + worker_connections 1024; # 每个worker进程支持的最大连接数 +} # 事件区块结束 +http { # HTTP区块开始 + include mime.types; # Nginx支持的媒体类型库文件 + default_type application/octet-stream; # 默认的媒体类型 + sendfile on; # 开启高效传输模式 + keepalive_timeout 65; # 连接超时 + server { # 第一个Server区块开始,表示一个独立的虚拟主机站点 + listen 80; # 提供服务的端口,默认80 + server_name localhost; # 提供服务的域名主机名 + location / { # 第一个location区块开始 + root html; # 站点的根目录,相当于Nginx的安装目录 + index index.html index.htm; # 默认的首页文件,多个用空格分开 + } # 第一个location区块结果 + error_page 500502503504 /50x.html; # 出现对应的http状态码时,使用50x.html回应客户 + location = /50x.html { # location区块开始,访问50x.html + root html; # 指定对应的站点目录为html + } + } + ...... +``` +### Nginx静态资源? + +* 静态资源访问,就是存放在nginx的html页面,我们可以自己编写 + +### 如何用Nginx解决前端跨域问题? + +* 使用Nginx转发请求。把跨域的接口写成调本域的接口,然后将这些接口转发到真正的请求地址。 + +### Nginx虚拟主机怎么配置? + +* 1、基于域名的虚拟主机,通过域名来区分虚拟主机——应用:外部网站 + +* 2、基于端口的虚拟主机,通过端口来区分虚拟主机——应用:公司内部网站,外部网站的管理后台 + +* 3、基于ip的虚拟主机。 + +#### 基于虚拟主机配置域名 + +* 需要建立/data/www /data/bbs目录,windows本地hosts添加虚拟机ip地址对应的域名解析;对应域名网站目录下新增index.html文件; + +```java +#当客户端访问www.lijie.com,监听端口号为80,直接跳转到data/www目录下文件 + server { + listen 80; + server_name www.lijie.com; + location / { + root data/www; + index index.html index.htm; + } + } + + #当客户端访问www.lijie.com,监听端口号为80,直接跳转到data/bbs目录下文件 + server { + listen 80; + server_name bbs.lijie.com; + location / { + root data/bbs; + index index.html index.htm; + } + } +``` +#### 基于端口的虚拟主机 + +* 使用端口来区分,浏览器使用域名或ip地址:端口号 访问 + +```java +#当客户端访问www.lijie.com,监听端口号为8080,直接跳转到data/www目录下文件 + server { + listen 8080; + server_name 8080.lijie.com; + location / { + root data/www; + index index.html index.htm; + } + } + + #当客户端访问www.lijie.com,监听端口号为80直接跳转到真实ip服务器地址 127.0.0.1:8080 + server { + listen 80; + server_name www.lijie.com; + location / { + proxy_pass http://127.0.0.1:8080; + index index.html index.htm; + } + } +``` +### location的作用是什么? + +* location指令的作用是根据用户请求的URI来执行不同的应用,也就是根据用户请求的网站URL进行匹配,匹配成功即进行相关的操作。 + +#### location的语法能说出来吗? + +> 注意:~ 代表自己输入的英文字母 +> +> | 匹配符 | 匹配规则 | 优先级 | +> | --- | --- | --- | +> | = | 精确匹配 | 1 | +> | ^~ | 以某个字符串开头 | 2 | +> | ~ | 区分大小写的正则匹配 | 3 | +> | ~* | 不区分大小写的正则匹配 | 4 | +> | !~ | 区分大小写不匹配的正则 | 5 | +> | !~* | 不区分大小写不匹配的正则 | 6 | +> | / | 通用匹配,任何请求都会匹配到 | 7 | + +#### Location正则案例 + +* 示例: + +```java +#优先级1,精确匹配,根路径 + location =/ { + return 400; + } + + #优先级2,以某个字符串开头,以av开头的,优先匹配这里,区分大小写 + location ^~ /av { + root /data/av/; + } + + #优先级3,区分大小写的正则匹配,匹配/media*****路径 + location ~ /media { + alias /data/static/; + } + + #优先级4 ,不区分大小写的正则匹配,所有的****.jpg|gif|png 都走这里 + location ~* .*\.(jpg|gif|png|js|css)$ { + root /data/av/; + } + + #优先7,通用匹配 + location / { + return 403; + } +``` +### 限流怎么做的? + +* Nginx限流就是限制用户请求速度,防止服务器受不了 + +* 限流有3种 + + 1. 正常限制访问频率(正常流量) + 2. 突发限制访问频率(突发流量) + 3. 限制并发连接数 +* Nginx的限流都是基于漏桶流算法,底下会说道什么是桶铜流 + +**实现三种限流算法** + +##### 1、正常限制访问频率(正常流量): + +* 限制一个用户发送的请求,我Nginx多久接收一个请求。 + +* Nginx中使用ngx_http_limit_req_module模块来限制的访问频率,限制的原理实质是基于漏桶算法原理来实现的。在nginx.conf配置文件中可以使用limit_req_zone命令及limit_req命令限制单个IP的请求处理频率。 + +```java +#定义限流维度,一个用户一分钟一个请求进来,多余的全部漏掉 + limit_req_zone $binary_remote_addr zone=one:10m rate=1r/m; + + #绑定限流维度 + server{ + + location/seckill.html{ + limit_req zone=zone; + proxy_pass http://lj_seckill; + } + + } +``` + +* 1r/s代表1秒一个请求,1r/m一分钟接收一个请求, 如果Nginx这时还有别人的请求没有处理完,Nginx就会拒绝处理该用户请求。 + +##### 2、突发限制访问频率(突发流量): + +* 限制一个用户发送的请求,我Nginx多久接收一个。 + +* 上面的配置一定程度可以限制访问频率,但是也存在着一个问题:如果突发流量超出请求被拒绝处理,无法处理活动时候的突发流量,这时候应该如何进一步处理呢?Nginx提供burst参数结合nodelay参数可以解决流量突发的问题,可以设置能处理的超过设置的请求数外能额外处理的请求数。我们可以将之前的例子添加burst参数以及nodelay参数: + +```java +#定义限流维度,一个用户一分钟一个请求进来,多余的全部漏掉 + limit_req_zone $binary_remote_addr zone=one:10m rate=1r/m; + + #绑定限流维度 + server{ + + location/seckill.html{ + limit_req zone=zone burst=5 nodelay; + proxy_pass http://lj_seckill; + } + + } +``` + +* 为什么就多了一个 burst=5 nodelay; 呢,多了这个可以代表Nginx对于一个用户的请求会立即处理前五个,多余的就慢慢来落,没有其他用户的请求我就处理你的,有其他的请求的话我Nginx就漏掉不接受你的请求 + +##### 3、 限制并发连接数 + +* Nginx中的ngx_http_limit_conn_module模块提供了限制并发连接数的功能,可以使用limit_conn_zone指令以及limit_conn执行进行配置。接下来我们可以通过一个简单的例子来看下: + +```java +http { + limit_conn_zone $binary_remote_addr zone=myip:10m; + limit_conn_zone $server_name zone=myServerName:10m; + } + + server { + location / { + limit_conn myip 10; + limit_conn myServerName 100; + rewrite / http://www.lijie.net permanent; + } + } +``` + +* 上面配置了单个IP同时并发连接数最多只能10个连接,并且设置了整个虚拟服务器同时最大并发数最多只能100个链接。当然,只有当请求的header被服务器处理后,虚拟服务器的连接数才会计数。刚才有提到过Nginx是基于漏桶算法原理实现的,实际上限流一般都是基于漏桶算法和令牌桶算法实现的。接下来我们来看看两个算法的介绍: + +### 漏桶流算法和令牌桶算法知道? + +#### 漏桶算法 + +* 漏桶算法是网络世界中流量整形或速率限制时经常使用的一种算法,它的主要目的是控制数据注入到网络的速率,平滑网络上的突发流量。漏桶算法提供了一种机制,通过它,突发流量可以被整形以便为网络提供一个稳定的流量。也就是我们刚才所讲的情况。漏桶算法提供的机制实际上就是刚才的案例:**突发流量会进入到一个漏桶,漏桶会按照我们定义的速率依次处理请求,如果水流过大也就是突发流量过大就会直接溢出,则多余的请求会被拒绝。所以漏桶算法能控制数据的传输速率。**![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172646dbb8b696~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +#### 令牌桶算法 + +* 令牌桶算法是网络流量整形和速率限制中最常使用的一种算法。典型情况下,令牌桶算法用来控制发送到网络上的数据的数目,并允许突发数据的发送。Google开源项目Guava中的RateLimiter使用的就是令牌桶控制算法。**令牌桶算法的机制如下:存在一个大小固定的令牌桶,会以恒定的速率源源不断产生令牌。如果令牌消耗速率小于生产令牌的速度,令牌就会一直产生直至装满整个令牌桶。** + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/17172646dbc20c88~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +### 为什么要做动静分离? + +* Nginx是当下最热的Web容器,网站优化的重要点在于静态化网站,网站静态化的关键点则是是动静分离,动静分离是让动态网站里的动态网页根据一定规则把不变的资源和经常变的资源区分开来,动静资源做好了拆分以后,我们则根据静态资源的特点将其做缓存操作。 + +* 让静态的资源只走静态资源服务器,动态的走动态的服务器 + +* Nginx的静态处理能力很强,但是动态处理能力不足,因此,在企业中常用动静分离技术。 + +* 对于静态资源比如图片,js,css等文件,我们则在反向代理服务器nginx中进行缓存。这样浏览器在请求一个静态资源时,代理服务器nginx就可以直接处理,无需将请求转发给后端服务器tomcat。 若用户请求的动态文件,比如servlet,jsp则转发给Tomcat服务器处理,从而实现动静分离。这也是反向代理服务器的一个重要的作用。 + +### Nginx怎么做的动静分离? + +* 只需要指定路径对应的目录。location/可以使用正则表达式匹配。并指定对应的硬盘中的目录。如下:(操作都是在Linux上) + +```java +location /image/ { + root /usr/local/static/; + autoindex on; + } +``` + +1. 创建目录 + +```java +mkdir /usr/local/static/image +``` +1. 进入目录 + +```java +cd /usr/local/static/image +``` +1. 放一张照片上去# + +```java +1.jpg +``` +1. 重启 nginx + +```java +sudo nginx -s reload +``` +1. 打开浏览器 输入 server_name/image/1.jpg 就可以访问该静态图片了 + +### Nginx负载均衡的算法怎么实现的?策略有哪些? + +* 为了避免服务器崩溃,大家会通过负载均衡的方式来分担服务器压力。将对台服务器组成一个集群,当用户访问时,先访问到一个转发服务器,再由转发服务器将访问分发到压力更小的服务器。 + +* Nginx负载均衡实现的策略有以下五种: + +#### 1 轮询(默认) + +* 每个请求按时间顺序逐一分配到不同的后端服务器,如果后端某个服务器宕机,能自动剔除故障系统。 + +```java +upstream backserver { + server 192.168.0.12; + server 192.168.0.13; +} +``` +#### 2 权重 weight + +* weight的值越大分配 + +* 到的访问概率越高,主要用于后端每台服务器性能不均衡的情况下。其次是为在主从的情况下设置不同的权值,达到合理有效的地利用主机资源。 + +```java +upstream backserver { + server 192.168.0.12 weight=2; + server 192.168.0.13 weight=8; +} +``` + +* 权重越高,在被访问的概率越大,如上例,分别是20%,80%。 + +#### 3 ip_hash( IP绑定) + +* 每个请求按访问IP的哈希结果分配,使来自同一个IP的访客固定访问一台后端服务器,`并且可以有效解决动态网页存在的session共享问题` + +```java +upstream backserver { + ip_hash; + server 192.168.0.12:88; + server 192.168.0.13:80; +} +``` +#### 4 fair(第三方插件) + +* 必须安装upstream_fair模块。 + +* 对比 weight、ip_hash更加智能的负载均衡算法,fair算法可以根据页面大小和加载时间长短智能地进行负载均衡,响应时间短的优先分配。 + +```java +upstream backserver { + server server1; + server server2; + fair; +} + +``` + +* 哪个服务器的响应速度快,就将请求分配到那个服务器上。 + +#### 5、url_hash(第三方插件) + +* 必须安装Nginx的hash软件包 + +* 按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,可以进一步提高后端缓存服务器的效率。 + +```java +upstream backserver { + server squid1:3128; + server squid2:3128; + hash $request_uri; + hash_method crc32; +} + +``` +### Nginx配置高可用性怎么配置? + +* 当上游服务器(真实访问服务器),一旦出现故障或者是没有及时相应的话,应该直接轮训到下一台服务器,保证服务器的高可用 + +* Nginx配置代码: + +```java +server { + listen 80; + server_name www.lijie.com; + location / { + ### 指定上游服务器负载均衡服务器 + proxy_pass http://backServer; + ###nginx与上游服务器(真实访问的服务器)超时时间 后端服务器连接的超时时间_发起握手等候响应超时时间 + proxy_connect_timeout 1s; + ###nginx发送给上游服务器(真实访问的服务器)超时时间 + proxy_send_timeout 1s; + ### nginx接受上游服务器(真实访问的服务器)超时时间 + proxy_read_timeout 1s; + index index.html index.htm; + } + } + +``` +### Nginx怎么判断别IP不可访问? + +```java +# 如果访问的ip地址为192.168.9.115,则返回403 +if ($remote_addr = 192.168.9.115) { + return 403; +} +``` +### 怎么限制浏览器访问? + +```java +## 不允许谷歌浏览器访问 如果是谷歌浏览器返回500 +if ($http_user_agent ~ Chrome) { + return 500; +} +``` +### Rewrite全局变量是什么? + +> | 变量 | 含义 | +> | --- | --- | +> | $args | 这个变量等于请求行中的参数,同$query_string | +> | $content length | 请求头中的Content-length字段。 | +> | $content_type | 请求头中的Content-Type字段。 | +> | $document_root | 当前请求在root指令中指定的值。 | +> | $host | 请求主机头字段,否则为服务器名称。 | +> | $http_user_agent | 客户端agent信息 | +> | $http_cookie | 客户端cookie信息 | +> | $limit_rate | 这个变量可以限制连接速率。 | +> | $request_method | 客户端请求的动作,通常为GET或POST。 | +> | $remote_addr | 客户端的IP地址。 | +> | $remote_port | 客户端的端口。 | +> | $remote_user | 已经经过Auth Basic Module验证的用户名。 | +> | $request_filename | 当前请求的文件路径,由root或alias指令与URI请求生成。 | +> | $scheme | HTTP方法(如http,https)。 | +> | $server_protocol | 请求使用的协议,通常是HTTP/1.0或HTTP/1.1。 | +> | $server_addr | 服务器地址,在完成一次系统调用后可以确定这个值。 | +> | $server_name | 服务器名称。 | +> | $server_port | 请求到达服务器的端口号。 | +> | $request_uri | 包含请求参数的原始URI,不包含主机名,如”/foo/bar.php?arg=baz”。 | +> | $uri | 不带请求参数的当前URI,$uri不包含主机名,如”/foo/bar.html”。 | +> | $document_uri | 与$uri相同。 | + +作者:小杰要吃蛋 +链接:https://juejin.cn/post/6844904125784653837 +来源:稀土掘金 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 \ No newline at end of file diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/elasticSearch.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/elasticSearch.md" new file mode 100644 index 0000000..8c213a4 --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/elasticSearch.md" @@ -0,0 +1,296 @@ +https://juejin.cn/post/6844904032083902471 + +## 前言 + + ElasticSearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。Elasticsearch是用Java语言开发的,并作为Apache许可条款下的开放源码发布,是一种流行的企业级搜索引擎。ElasticSearch用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。官方客户端在Java、.NET(C#)、PHP、Python、Apache Groovy、Ruby和许多其他语言中都是可用的。根据DB-Engines的排名显示,Elasticsearch是最受欢迎的企业搜索引擎,其次是Apache Solr,也是基于Lucene。 + + ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/25/16f3cd47cdfa683a~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +## Elasticsearch 面试题 + + 1、elasticsearch 了解多少,说说你们公司 es 的集群架构,索引数据大小,分片有多少,以及一些调优手段 。 + + 2、elasticsearch 的倒排索引是什么 + + 3、elasticsearch 索引数据多了怎么办,如何调优,部署 + + 4、elasticsearch 是如何实现 master 选举的 + + 5、详细描述一下 Elasticsearch 索引文档的过程 + + 6、详细描述一下 Elasticsearch 搜索的过程? + + 7、Elasticsearch 在部署时,对 Linux 的设置有哪些优化方法 + + 8、lucence 内部结构是什么? + + 9、Elasticsearch 是如何实现 Master 选举的? + + 10、Elasticsearch 中的节点(比如共 20 个),其中的 10 个选了一个master,另外 10 个选了另一个 master,怎么办? + + 11、客户端在和集群连接时,如何选择特定的节点执行请求的? + + 12、详细描述一下 Elasticsearch 索引文档的过程。 + + ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/25/16f3cd47cb162405~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +## 1、elasticsearch 了解多少,说说你们公司 es 的集群架构,索引数据大小,分片有多少,以及一些调优手段 。 + + 面试官:想了解应聘者之前公司接触的 ES 使用场景、规模,有没有做过比较大规模的索引设计、规划、调优。 + + 解答:如实结合自己的实践场景回答即可。 + + 比如:ES 集群架构 13 个节点,索引根据通道不同共 20+索引,根据日期,每日递增 20+,索引:10 分片,每日递增 1 亿+数据,每个通道每天索引大小控制:150GB 之内。 + + 仅索引层面调优手段: + +### 1.1、设计阶段调优 + + (1)根据业务增量需求,采取基于日期模板创建索引,通过 roll over API 滚动索引; + + (2)使用别名进行索引管理; + + (3)每天凌晨定时对索引做 force_merge 操作,以释放空间; + + (4)采取冷热分离机制,热数据存储到 SSD,提高检索效率;冷数据定期进行 shrink操作,以缩减存储; + + (5)采取 curator 进行索引的生命周期管理; + + (6)仅针对需要分词的字段,合理的设置分词器; + + (7)Mapping 阶段充分结合各个字段的属性,是否需要检索、是否需要存储等。…….. + +### 1.2、写入调优 + + (1)写入前副本数设置为 0; + + (2)写入前关闭 refresh_interval 设置为-1,禁用刷新机制; + + (3)写入过程中:采取 bulk 批量写入; + + (4)写入后恢复副本数和刷新间隔; + + (5)尽量使用自动生成的 id。 + +### 1.3、查询调优 + + (1)禁用 wildcard; + + (2)禁用批量 terms(成百上千的场景); + + (3)充分利用倒排索引机制,能 keyword 类型尽量 keyword; + + (4)数据量大时候,可以先基于时间敲定索引再检索; + + (5)设置合理的路由机制。 + +### 1.4、其他调优 + + 部署调优,业务调优等。 + + 上面的提及一部分,面试者就基本对你之前的实践或者运维经验有所评估了。 + +## 2、elasticsearch 的倒排索引是什么 + + 面试官:想了解你对基础概念的认知。 + + 解答:通俗解释一下就可以。 + + 传统的我们的检索是通过文章,逐个遍历找到对应关键词的位置。 + + 而倒排索引,是通过分词策略,形成了词和文章的映射关系表,这种词典+映射表即为倒排索引。有了倒排索引,就能实现 o(1)时间复杂度的效率检索文章了,极大的提高了检索效率。 + + ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/25/16f3cd47d11e0d11~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + + 学术的解答方式: + + 倒排索引,相反于一篇文章包含了哪些词,它从词出发,记载了这个词在哪些文档中出现过,由两部分组成——词典和倒排表。 + + 加分项:倒排索引的底层实现是基于:FST(Finite State Transducer)数据结构。 + + lucene 从 4+版本后开始大量使用的数据结构是 FST。FST 有两个优点: + + (1)空间占用小。通过对词典中单词前缀和后缀的重复利用,压缩了存储空间; + + (2)查询速度快。O(len(str))的查询时间复杂度。 + +## 3、elasticsearch 索引数据多了怎么办,如何调优,部署 + + 面试官:想了解大数据量的运维能力。 + + 解答:索引数据的规划,应在前期做好规划,正所谓“设计先行,编码在后”,这样才能有效的避免突如其来的数据激增导致集群处理能力不足引发的线上客户检索或者其他业务受到影响。 + + 如何调优,正如问题 1 所说,这里细化一下: + +### 3.1 动态索引层面 + + 基于模板+时间+rollover api 滚动创建索引,举例:设计阶段定义:blog 索引的模板格式为:blog_index_时间戳的形式,每天递增数据。这样做的好处:不至于数据量激增导致单个索引数据量非常大,接近于上线 2 的32 次幂-1,索引存储达到了 TB+甚至更大。 + + 一旦单个索引很大,存储等各种风险也随之而来,所以要提前考虑+及早避免。 + +### 3.2 存储层面 + + 冷热数据分离存储,热数据(比如最近 3 天或者一周的数据),其余为冷数据。 + + 对于冷数据不会再写入新数据,可以考虑定期 force_merge 加 shrink 压缩操作,节省存储空间和检索效率。 + +### 3.3 部署层面 + + 一旦之前没有规划,这里就属于应急策略。 + + 结合 ES 自身的支持动态扩展的特点,动态新增机器的方式可以缓解集群压力,注意:如果之前主节点等规划合理,不需要重启集群也能完成动态新增的。 + +## 4、elasticsearch 是如何实现 master 选举的 + + 面试官:想了解 ES 集群的底层原理,不再只关注业务层面了。 + + 解答: + + 前置前提: + + (1)只有候选主节点(master:true)的节点才能成为主节点。 + + (2)最小主节点数(min_master_nodes)的目的是防止脑裂。 + + 核对了一下代码,核心入口为 findMaster,选择主节点成功返回对应 Master,否则返回 null。选举流程大致描述如下: + + 第一步:确认候选主节点数达标,elasticsearch.yml 设置的值 + + discovery.zen.minimum_master_nodes; + + 第二步:比较:先判定是否具备 master 资格,具备候选主节点资格的优先返回; + + 若两节点都为候选主节点,则 id 小的值会主节点。注意这里的 id 为 string 类型。 + + 题外话:获取节点 id 的方法。 + +1GET /_cat/nodes?v&h=ip,port,heapPercent,heapMax,id,name + +2ip port heapPercent heapMax id name复制代码 +## 5、详细描述一下 Elasticsearch 索引文档的过程 + + 面试官:想了解 ES 的底层原理,不再只关注业务层面了。 + + 解答: + + 这里的索引文档应该理解为文档写入 ES,创建索引的过程。 + + 文档写入包含:单文档写入和批量 bulk 写入,这里只解释一下:单文档写入流程。 + + 记住官方文档中的这个图。 + + ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/25/16f3cd47d2b0df73~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + + 第一步:客户写集群某节点写入数据,发送请求。(如果没有指定路由/协调节点,请求的节点扮演路由节点的角色。) + + 第二步:节点 1 接受到请求后,使用文档_id 来确定文档属于分片 0。请求会被转到另外的节点,假定节点 3。因此分片 0 的主分片分配到节点 3 上。 + + 第三步:节点 3 在主分片上执行写操作,如果成功,则将请求并行转发到节点 1和节点 2 的副本分片上,等待结果返回。所有的副本分片都报告成功,节点 3 将向协调节点(节点 1)报告成功,节点 1 向请求客户端报告写入成功。 + + 如果面试官再问:第二步中的文档获取分片的过程? + + 回答:借助路由算法获取,路由算法就是根据路由和文档 id 计算目标的分片 id 的过程。 + +1shard = hash(_routing) % (num_of_primary_shards)复制代码 +## 6、详细描述一下 Elasticsearch 搜索的过程? + + 面试官:想了解 ES 搜索的底层原理,不再只关注业务层面了。 + + 解答: + + 搜索拆解为“query then fetch” 两个阶段。 + + query 阶段的目的:定位到位置,但不取。 + + 步骤拆解如下: + + (1)假设一个索引数据有 5 主+1 副本 共 10 分片,一次请求会命中(主或者副本分片中)的一个。 + + (2)每个分片在本地进行查询,结果返回到本地有序的优先队列中。 + + (3)第 2)步骤的结果发送到协调节点,协调节点产生一个全局的排序列表。 + + fetch 阶段的目的:取数据。 + + 路由节点获取所有文档,返回给客户端。 + + ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/25/16f3cd47d2edca5f~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +## 7、Elasticsearch 在部署时,对 Linux 的设置有哪些优化方法 + + 面试官:想了解对 ES 集群的运维能力。 + + 解答: + + (1)关闭缓存 swap; + + (2)堆内存设置为:Min(节点内存/2, 32GB); + + (3)设置最大文件句柄数; + + (4)线程池+队列大小根据业务需要做调整; + + (5)磁盘存储 raid 方式——存储有条件使用 RAID10,增加单节点性能以及避免单节点存储故障。 + +## 8、lucence 内部结构是什么? + + 面试官:想了解你的知识面的广度和深度。 + + 解答: + + ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/25/16f3cd47d3590ee5~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + + Lucene 是有索引和搜索的两个过程,包含索引创建,索引,搜索三个要点。可以基于这个脉络展开一些。 + +## 9、Elasticsearch 是如何实现 Master 选举的? + + (1)Elasticsearch 的选主是 ZenDiscovery 模块负责的,主要包含 Ping(节点之间通过这个 RPC 来发现彼此)和 Unicast(单播模块包含一个主机列表以控制哪些节点需要 ping 通)这两部分; + + (2)对所有可以成为 master 的节点(node.master: true)根据 nodeId 字典排序,每次选举每个节点都把自己所知道节点排一次序,然后选出第一个(第 0 位)节点,暂且认为它是 master 节点。 + + (3)如果对某个节点的投票数达到一定的值(可以成为 master 节点数 n/2+1)并且该节点自己也选举自己,那这个节点就是 master。否则重新选举一直到满足上述条件。 + + (4)补充:master 节点的职责主要包括集群、节点和索引的管理,不负责文档级别的管理;data 节点可以关闭 http 功能*。 + +## 10、Elasticsearch 中的节点(比如共 20 个),其中的 10 个 + + 选了一个 master,另外 10 个选了另一个 master,怎么办? + + (1)当集群 master 候选数量不小于 3 个时,可以通过设置最少投票通过数量(discovery.zen.minimum_master_nodes)超过所有候选节点一半以上来解决脑裂问题; + + (3)当候选数量为两个时,只能修改为唯一的一个 master 候选,其他作为 data节点,避免脑裂问题。 + +## 11、客户端在和集群连接时,如何选择特定的节点执行请求的? + + TransportClient 利用 transport 模块远程连接一个 elasticsearch 集群。它并不加入到集群中,只是简单的获得一个或者多个初始化的 transport 地址,并以 轮询 的方式与这些地址进行通信。 + +## 12、详细描述一下 Elasticsearch 索引文档的过程。 + + 协调节点默认使用文档 ID 参与计算(也支持通过 routing),以便为路由提供合适的分片。 + +shard = hash(document_id) % (num_of_primary_shards)复制代码 + + (1)当分片所在的节点接收到来自协调节点的请求后,会将请求写入到 MemoryBuffer,然后定时(默认是每隔 1 秒)写入到 Filesystem Cache,这个从 MomeryBuffer 到 Filesystem Cache 的过程就叫做 refresh; + + (2)当然在某些情况下,存在 Momery Buffer 和 Filesystem Cache 的数据可能会丢失,ES 是通过 translog 的机制来保证数据的可靠性的。其实现机制是接收到请求后,同时也会写入到 translog 中 ,当 Filesystem cache 中的数据写入到磁盘中时,才会清除掉,这个过程叫做 flush; + + (3)在 flush 过程中,内存中的缓冲将被清除,内容被写入一个新段,段的 fsync将创建一个新的提交点,并将内容刷新到磁盘,旧的 translog 将被删除并开始一个新的 translog。 + + (4)flush 触发的时机是定时触发(默认 30 分钟)或者 translog 变得太大(默认为 512M)时; + + ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/25/16f3cd48799304d6~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + + 补充:关于 Lucene 的 Segement: + + (1)Lucene 索引是由多个段组成,段本身是一个功能齐全的倒排索引。 + + (2)段是不可变的,允许 Lucene 将新的文档增量地添加到索引中,而不用从头重建索引。 + + (3)对于每一个搜索请求而言,索引中的所有段都会被搜索,并且每个段会消耗CPU 的时钟周、文件句柄和内存。这意味着段的数量越多,搜索性能会越低。 + + (4)为了解决这个问题,Elasticsearch 会合并小段到一个较大的段,提交新的合并段到磁盘,并删除那些旧的小段。 + +作者:程序员追风 +链接:https://juejin.cn/post/6844904031555420167 +来源:稀土掘金 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 \ No newline at end of file diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/kafka.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/kafka.md" new file mode 100644 index 0000000..677057b --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/kafka.md" @@ -0,0 +1 @@ +https://juejin.cn/post/6844904025805029383 \ No newline at end of file diff --git "a/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/mybatis.md" "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/mybatis.md" new file mode 100644 index 0000000..b648d26 --- /dev/null +++ "b/docs/java/\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230/mybatis.md" @@ -0,0 +1,565 @@ + + +## MyBatis简介 + +### MyBatis是什么? + +* Mybatis 是一个半 ORM(对象关系映射)框架,它内部封装了 JDBC,开发时只需要关注 SQL 语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement 等繁杂的过程。程序员直接编写原生态 sql,可以严格控制 sql 执行性能,灵活度高。 + +* MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO 映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。 + +### Mybatis优缺点 + +**优点** + +`与传统的数据库访问技术相比,ORM有以下优点:` + +* 基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用 +* 与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接 +* 很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持) +* 提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护 +* 能够与Spring很好的集成 + +**缺点** + +* SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求 +* SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库 + +### Hibernate 和 MyBatis 的区别 + +**相同点** + +* 都是对jdbc的封装,都是持久层的框架,都用于dao层的开发。 + +**不同点** + +* 映射关系 + * MyBatis 是一个半自动映射的框架,配置Java对象与sql语句执行结果的对应关系,多表关联关系配置简单 + * Hibernate 是一个全表映射的框架,配置Java对象与数据库表的对应关系,多表关联关系配置复杂 + +**SQL优化和移植性** + +* Hibernate 对SQL语句封装,提供了日志、缓存、级联(级联比 MyBatis 强大)等特性,此外还提供 HQL(Hibernate Query Language)操作数据库,数据库无关性支持好,但会多消耗性能。如果项目需要支持多种数据库,代码开发量少,但SQL语句优化困难。 +* MyBatis 需要手动编写 SQL,支持动态 SQL、处理列表、动态生成表名、支持存储过程。开发工作量相对大些。直接使用SQL语句操作数据库,不支持数据库无关性,但sql语句优化容易。 + +### ORM是什么 + +* ORM(Object Relational Mapping),对象关系映射,是一种为了解决关系型数据库数据与简单Java对象(POJO)的映射关系的技术。简单的说,ORM是通过使用描述对象和数据库之间映射的元数据,将程序中的对象自动持久化到关系型数据库中。 + +### 为什么说Mybatis是半自动ORM映射工具?它与全自动的区别在哪里? + +* Hibernate属于全自动ORM映射工具,使用Hibernate查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。 + +* 而Mybatis在查询关联对象或关联集合对象时,需要手动编写sql来完成,所以,称之为半自动ORM映射工具。 + +### 传统JDBC开发存在什么问题? + +* 频繁创建数据库连接对象、释放,容易造成系统资源浪费,影响系统性能。可以使用连接池解决这个问题。但是使用jdbc需要自己实现连接池。 +* sql语句定义、参数设置、结果集处理存在硬编码。实际项目中sql语句变化的可能性较大,一旦发生变化,需要修改java代码,系统需要重新编译,重新发布。不好维护。 +* 使用preparedStatement向占有位符号传参数存在硬编码,因为sql语句的where条件不一定,可能多也可能少,修改sql还要修改代码,系统不易维护。 +* 结果集处理存在重复代码,处理麻烦。如果可以映射成Java对象会比较方便。 + +### JDBC编程有哪些不足之处,MyBatis是如何解决的? + +* 1、数据库链接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库连接池可解决此问题。 + + * 解决:在mybatis-config.xml中配置数据链接池,使用连接池管理数据库连接。 +* 2、Sql语句写在代码中造成代码不易维护,实际应用sql变化的可能较大,sql变动需要改变java代码。- + + * 解决:将Sql语句配置在XXXXmapper.xml文件中与java代码分离。 +* 3、向sql语句传参数麻烦,因为sql语句的where条件不一定,可能多也可能少,占位符需要和参数一一对应。 + + * 解决: Mybatis自动将java对象映射至sql语句。 +* 4、对结果集解析麻烦,sql变化导致解析代码变化,且解析前需要遍历,如果能将数据库记录封装成pojo对象解析比较方便。 + + * 解决:Mybatis自动将sql执行结果映射至java对象。 + +### MyBatis和Hibernate的适用场景? + +* MyBatis专注于SQL本身,是一个足够灵活的DAO层解决方案。 +* 对性能的要求很高,或者需求变化较多的项目,如互联网项目,MyBatis将是不错的选择。 + +**开发难易程度和学习成本** + +* Hibernate 是重量级框架,学习使用门槛高,适合于需求相对稳定,中小型的项目,比如:办公自动化系统 + +* MyBatis 是轻量级框架,学习使用门槛低,适合于需求变化频繁,大型的项目,比如:互联网电子商务系统 + +**总结** + +* MyBatis 是一个小巧、方便、高效、简单、直接、半自动化的持久层框架, + +* Hibernate 是一个强大、方便、高效、复杂、间接、全自动化的持久层框架。 + +## MyBatis的架构 + +### MyBatis编程步骤是什么样的? + +* 1、 创建SqlSessionFactory + +* 2、 通过SqlSessionFactory创建SqlSession + +* 3、 通过sqlsession执行数据库操作 + +* 4、 调用session.commit()提交事务 + +* 5、 调用session.close()关闭会话 + +### 请说说MyBatis的工作原理 + +* 在学习 MyBatis 程序之前,需要了解一下 MyBatis 工作原理,以便于理解程序。MyBatis 的工作原理如下图 ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/1717343a66d9566c~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +1. 读取 MyBatis 配置文件:mybatis-config.xml 为 MyBatis 的全局配置文件,配置了 MyBatis 的运行环境等信息,例如数据库连接信息。 +2. 加载映射文件。映射文件即 SQL 映射文件,该文件中配置了操作数据库的 SQL 语句,需要在 MyBatis 配置文件 mybatis-config.xml 中加载。mybatis-config.xml 文件可以加载多个映射文件,每个文件对应数据库中的一张表。 +3. 构造会话工厂:通过 MyBatis 的环境等配置信息构建会话工厂 SqlSessionFactory。 +4. 创建会话对象:由会话工厂创建 SqlSession 对象,该对象中包含了执行 SQL 语句的所有方法。 +5. Executor 执行器:MyBatis 底层定义了一个 Executor 接口来操作数据库,它将根据 SqlSession 传递的参数动态地生成需要执行的 SQL 语句,同时负责查询缓存的维护。 +6. MappedStatement 对象:在 Executor 接口的执行方法中有一个 MappedStatement 类型的参数,该参数是对映射信息的封装,用于存储要映射的 SQL 语句的 id、参数等信息。 +7. 输入参数映射:输入参数类型可以是 Map、List 等集合类型,也可以是基本数据类型和 POJO 类型。输入参数映射过程类似于 JDBC 对 preparedStatement 对象设置参数的过程。 +8. 输出结果映射:输出结果类型可以是 Map、 List 等集合类型,也可以是基本数据类型和 POJO 类型。输出结果映射过程类似于 JDBC 对结果集的解析过程。 + +### MyBatis的功能架构是怎样的 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/1717343a5a426a4d~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 我们把Mybatis的功能架构分为三层: + * API接口层:提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。 + * 数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。 + * 基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。 + +### MyBatis的框架架构设计是怎么样的 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/1717343a5ab904a6~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) + +* 这张图从上往下看。MyBatis的初始化,会从mybatis-config.xml配置文件,解析构造成Configuration这个类,就是图中的红框。 + +1. 加载配置:配置来源于两个地方,一处是配置文件,一处是Java代码的注解,将SQL的配置信息加载成为一个个MappedStatement对象(包括了传入参数映射配置、执行的SQL语句、结果映射配置),存储在内存中。 + +2. SQL解析:当API接口层接收到调用请求时,会接收到传入SQL的ID和传入对象(可以是Map、JavaBean或者基本数据类型),Mybatis会根据SQL的ID找到对应的MappedStatement,然后根据传入参数对象对MappedStatement进行解析,解析后可以得到最终要执行的SQL语句和参数。 + +3. SQL执行:将最终得到的SQL和参数拿到数据库进行执行,得到操作数据库的结果。 + +4. 结果映射:将操作数据库的结果按照映射的配置进行转换,可以转换成HashMap、JavaBean或者基本数据类型,并将最终结果返回。 + +### 什么是DBMS + +* DBMS:数据库管理系统(database management system)是一种操纵和管理数据库的大型软件,用于建立、使用和维护数zd据库,简称dbms。它对数据库进行统一的管理和控制,以保证数据库的安全性和完整性。用户通过dbms访问数据库中的数据,数据库管理员也通过dbms进行数据库的维护工作。它可使多个应用程序和用户用不同的方法在同时版或不同时刻去建立,修改和询问数据库。DBMS提供数据定义语言[DDL](https://link.juejin.cn?target=https%3A%2F%2Fwww.baidu.com%2Fs%3Fwd%3DDDL%26tn%3DSE_PcZhidaonwhc_ngpagmjz%26rsv_dl%3Dgh_pc_zhidao "https://www.baidu.com/s?wd=DDL&tn=SE_PcZhidaonwhc_ngpagmjz&rsv_dl=gh_pc_zhidao")(Data Definition Language)与数据操作语言[DML](https://link.juejin.cn?target=https%3A%2F%2Fwww.baidu.com%2Fs%3Fwd%3DDML%26tn%3DSE_PcZhidaonwhc_ngpagmjz%26rsv_dl%3Dgh_pc_zhidao "https://www.baidu.com/s?wd=DML&tn=SE_PcZhidaonwhc_ngpagmjz&rsv_dl=gh_pc_zhidao")(Data Manipulation Language),供用户定义数据库的模式结构与权限约束,实现对数据的追加权、删除等操作。 + +### 为什么需要预编译 + +* 定义: + + * SQL 预编译指的是数据库驱动在发送 SQL 语句和参数给 DBMS 之前对 SQL 语句进行编译,这样 DBMS 执行 SQL 时,就不需要重新编译。 +* 为什么需要预编译 + + * JDBC 中使用对象 PreparedStatement 来抽象预编译语句,使用预编译。预编译阶段可以优化 SQL 的执行。预编译之后的 SQL 多数情况下可以直接执行,DBMS 不需要再次编译,越复杂的SQL,编译的复杂度将越大,预编译阶段可以合并多次操作为一个操作。同时预编译语句对象可以重复利用。把一个 SQL 预编译后产生的 PreparedStatement 对象缓存下来,下次对于同一个SQL,可以直接使用这个缓存的 PreparedState 对象。Mybatis默认情况下,将对所有的 SQL 进行预编译。 + * 还有一个重要的原因,复制SQL注入 + +### Mybatis都有哪些Executor执行器?它们之间的区别是什么? + +* Mybatis有三种基本的Executor执行器,SimpleExecutor、ReuseExecutor、BatchExecutor。 + +* **SimpleExecutor**:每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。 + +* **ReuseExecutor**:执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map内,供下一次使用。简言之,就是重复使用Statement对象。 + +* **BatchExecutor**:执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。 + +`作用范围:Executor的这些特点,都严格限制在SqlSession生命周期范围内。` + +### Mybatis中如何指定使用哪一种Executor执行器? + +* 在Mybatis配置文件中,在设置(settings)可以指定默认的ExecutorType执行器类型,也可以手动给DefaultSqlSessionFactory的创建SqlSession的方法传递ExecutorType类型参数,如SqlSession openSession(ExecutorType execType)。 + +* 配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(prepared statements); BATCH 执行器将重用语句并执行批量更新。 + +### Mybatis是否支持延迟加载?如果支持,它的实现原理是什么? + +* Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。 + +* 它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。 + +* 当然了,不光是Mybatis,几乎所有的包括Hibernate,支持延迟加载的原理都是一样的。 + +## 映射器 + +### #{}和${}的区别 + +* #{}是占位符,预编译处理;${}是拼接符,字符串替换,没有预编译处理。 + +* Mybatis在处理#{}时,#{}传入参数是以字符串传入,会将SQL中的#{}替换为?号,调用PreparedStatement的set方法来赋值。 + +* #{} 可以有效的防止SQL注入,提高系统安全性;${} 不能防止SQL 注入 + +* #{} 的变量替换是在DBMS 中;${} 的变量替换是在 DBMS 外 + +### 模糊查询like语句该怎么写 + +* 1 ’%${question}%’ 可能引起SQL注入,不推荐 +* 2 "%"#{question}"%" 注意:因为#{…}解析成sql语句时候,会在变量外侧自动加单引号’ ',所以这里 % 需要使用双引号" ",不能使用单引号 ’ ',不然会查不到任何结果。 +* 3 CONCAT(’%’,#{question},’%’) 使用CONCAT()函数,(推荐) +* 4 使用bind标签(不推荐) + + +复制代码 +### 在mapper中如何传递多个参数 + +**方法1:顺序传参法** + +public User selectUser(String name, int deptId); + + +复制代码 + +* #{}里面的数字代表传入参数的顺序。 + +* 这种方法不建议使用,sql层表达不直观,且一旦顺序调整容易出错。 + +**方法2:@Param注解传参法** + +public User selectUser(@Param("userName") String name, int @Param("deptId") deptId); + + +复制代码 + +* #{}里面的名称对应的是注解@Param括号里面修饰的名称。 + +* 这种方法在参数不多的情况还是比较直观的,(推荐使用)。 + +**方法3:Map传参法** + +public User selectUser(Map params); + + +复制代码 + +* #{}里面的名称对应的是Map里面的key名称。 + +* 这种方法适合传递多个参数,且参数易变能灵活传递的情况。(推荐使用)。 + +**方法4:Java Bean传参法** + +public User selectUser(User user); + + +复制代码 + +* #{}里面的名称对应的是User类里面的成员属性。 + +* 这种方法直观,需要建一个实体类,扩展不容易,需要加属性,但代码可读性强,业务逻辑处理方便,推荐使用。(推荐使用)。 + +### Mybatis如何执行批量操作 + +* **使用foreach标签** +* foreach的主要用在构建in条件中,它可以在SQL语句中进行迭代一个集合。foreach标签的属性主要有item,index,collection,open,separator,close。 + * item   表示集合中每一个元素进行迭代时的别名,随便起的变量名; + * index   指定一个名字,用于表示在迭代过程中,每次迭代到的位置,不常用; + * open   表示该语句以什么开始,常用“(”; + * separator 表示在每次进行迭代之间以什么符号作为分隔符,常用“,”; + * close   表示以什么结束,常用“)”。 +* 在使用foreach的时候最关键的也是最容易出错的就是collection属性,该属性是必须指定的,但是在不同情况下,该属性的值是不一样的,主要有一下3种情况: + 1. 如果传入的是单参数且参数类型是一个List的时候,collection属性值为list + 2. 如果传入的是单参数且参数类型是一个array数组的时候,collection的属性值为array + 3. 如果传入的参数是多个的时候,我们就需要把它们封装成一个Map了,当然单参数也可以封装成map,实际上如果你在传入参数的时候,在MyBatis里面也是会把它封装成一个Map的, + map的key就是参数名,所以这个时候collection属性值就是传入的List或array对象在自己封装的map里面的key +* 具体用法如下: + + + //推荐使用 + + + INSERT INTO emp(ename,gender,email,did) + VALUES + + (#{emp.eName},#{emp.gender},#{emp.email},#{emp.dept.id}) + + +复制代码 + + + + INSERT INTO emp(ename,gender,email,did) + VALUES(#{emp.eName},#{emp.gender},#{emp.email},#{emp.dept.id}) + + +复制代码 + +* **使用ExecutorType.BATCH** + + * Mybatis内置的ExecutorType有3种,默认为simple,该模式下它为每个语句的执行创建一个新的预处理语句,单条提交sql;而batch模式重复使用已经预处理的语句,并且批量执行所有更新语句,显然batch性能将更优; 但batch模式也有自己的问题,比如在Insert操作时,在事务没有提交之前,是没有办法获取到自增的id,这在某型情形下是不符合业务要求的 + + * 具体用法如下: + + //批量保存方法测试 + @Test + public void testBatch() throws IOException{ + SqlSessionFactory sqlSessionFactory = getSqlSessionFactory(); + //可以执行批量操作的sqlSession + SqlSession openSession = sqlSessionFactory.openSession(ExecutorType.BATCH); + + //批量保存执行前时间 + long start = System.currentTimeMillis(); + try { + EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class); + for (int i = 0; i < 1000; i++) { + mapper.addEmp(new Employee(UUID.randomUUID().toString().substring(0, 5), "b", "1")); + } + + openSession.commit(); + long end = System.currentTimeMillis(); + //批量保存执行后的时间 + System.out.println("执行时长" + (end - start)); + //批量 预编译sql一次==》设置参数==》10000次==》执行1次 677 + //非批量 (预编译=设置参数=执行 )==》10000次 1121 + + } finally { + openSession.close(); + } + } + 复制代码 + * mapper和mapper.xml如下 + + public interface EmployeeMapper { + //批量保存员工 + Long addEmp(Employee employee); + } + + 复制代码 + + insert into employee(lastName,email,gender) + values(#{lastName},#{email},#{gender}) + + + + 复制代码 + +### 如何获取生成的主键 + +* 新增标签中添加:keyProperty=" ID " 即可 + + + insert into user( + user_name, user_password, create_time) + values(#{userName}, #{userPassword} , #{createTime, jdbcType= TIMESTAMP}) + + + 复制代码 + +![在这里插入图片描述](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/4/13/1717343a5d83efe4~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.awebp) +### 当实体类中的属性名和表中的字段名不一样 ,怎么办 + +* 第1种: 通过在查询的SQL语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。 + + + + 复制代码 +* 第2种: 通过``来映射字段名和实体类属性名的一一对应的关系。 + + + + + + + + + + + + + 复制代码 + +### Mapper 编写有哪几种方式? + +* 第一种:接口实现类继承 SqlSessionDaoSupport:使用此种方法需要编写mapper 接口,mapper 接口实现类、mapper.xml 文件。 + + 1. 在 sqlMapConfig.xml 中配置 mapper.xml 的位置 + + + + + + + 复制代码 + 2. 定义 mapper 接口 + + 3. 实现类集成 SqlSessionDaoSupport + + mapper 方法中可以 this.getSqlSession()进行数据增删改查。 + + 4. spring 配置 + + + + + + 复制代码 +* 第二种:使用 org.mybatis.spring.mapper.MapperFactoryBean: + + 1. 在 sqlMapConfig.xml 中配置 mapper.xml 的位置,如果 mapper.xml 和mappre 接口的名称相同且在同一个目录,这里可以不用配置 + + 2. 定义 mapper 接口: + + + + + + + 复制代码 + 3. mapper.xml 中的 namespace 为 mapper 接口的地址 + + 4. mapper 接口中的方法名和 mapper.xml 中的定义的 statement 的 id 保持一致 + + 5. Spring 中定义 + + + + + + + 复制代码 +* 第三种:使用 mapper 扫描器: + + 1. mapper.xml 文件编写: + + mapper.xml 中的 namespace 为 mapper 接口的地址; + + mapper 接口中的方法名和 mapper.xml 中的定义的 statement 的 id 保持一致; + + 如果将 mapper.xml 和 mapper 接口的名称保持一致则不用在 sqlMapConfig.xml中进行配置。 + + 2. 定义 mapper 接口: + + 注意 mapper.xml 的文件名和 mapper 的接口名称保持一致,且放在同一个目录 + + 3. 配置 mapper 扫描器: + + + + + + + 复制代码 + 4. 使用扫描器后从 spring 容器中获取 mapper 的实现对象。 + +### 什么是MyBatis的接口绑定?有哪些实现方式? + +* 接口绑定,就是在MyBatis中任意定义接口,然后把接口里面的方法和SQL语句绑定,我们直接调用接口方法就可以,这样比起原来了SqlSession提供的方法我们可以有更加灵活的选择和设置。 + +* 接口绑定有两种实现方式 + + 1. 通过注解绑定,就是在接口的方法上面加上 @Select、@Update等注解,里面包含Sql语句来绑定; + + 2. 通过xml里面写SQL来绑定, 在这种情况下,要指定xml映射文件里面的namespace必须为接口的全路径名。当Sql语句比较简单时候,用注解绑定, 当SQL语句比较复杂时候,用xml绑定,一般用xml绑定的比较多。 + +### 使用MyBatis的mapper接口调用时有哪些要求? + +1. Mapper接口方法名和mapper.xml中定义的每个sql的id相同。 +2. Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql 的parameterType的类型相同。 +3. Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同。 +4. Mapper.xml文件中的namespace即是mapper接口的类路径。 + +### 这个Dao接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗 + +* Dao接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Dao接口生成代理proxy对象,代理对象proxy会拦截接口方法,转而执行MappedStatement所代表的sql,然后将sql执行结果返回。 + +* Dao接口里的方法,是不能重载的,因为是全限名+方法名的保存和寻找策略。 + +### Mybatis的Xml映射文件中,不同的Xml映射文件,id是否可以重复? + +* 不同的Xml映射文件,如果配置了namespace,那么id可以重复;如果没有配置namespace,那么id不能重复;毕竟namespace不是必须的,只是最佳实践而已。 + +* 原因就是namespace+id是作为Map的key使用的,如果没有namespace,就剩下id,那么,id重复会导致数据互相覆盖。有了namespace,自然id就可以重复,namespace不同,namespace+id自然也就不同。 + +### 简述Mybatis的Xml映射文件和Mybatis内部数据结构之间的映射关系? + +* 答:Mybatis将所有Xml配置信息都封装到All-In-One重量级对象Configuration内部。在Xml映射文件中,``标签会被解析为ParameterMap对象,其每个子元素会被解析为ParameterMapping对象。``标签会被解析为ResultMap对象,其每个子元素会被解析为ResultMapping对象。每一个`