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 ef8d5cf..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------) -- [系统设计](#----) - * [秒杀系统相关](#------) - * [前后端分离](#-----) - * [单点登录](#----) - * [常用框架](#----) - + [Spring](#spring) - + [SpringBoot](#springboot) -- [分布式](#---) - * [dubbo](#dubbo) - * [zookeeper](#zookeeper) - * [RocketMQ](#rocketmq) - * [RabbitMQ](#rabbitmq) - * [kafka](#kafka) - * [消息中间件](#-----) - * [redis](#redis) - * [分布式系统](#-----) -- [线上问题调优(虚拟机,tomcat)](#-----------tomcat-) -- [面试指南](#----) -- [工具](#--) - * [Git](#git) - * [Docker](#docker) -- [其他](#--) - * [权限控制(设计、shiro)](#--------shiro-) -- [Java学习资源](#--) -- [Java书籍推荐](#java----) -- [实战项目推荐](#------) -- [说明](#--) - * [JavaInterview介绍](#javainterview--) - * [关于转载](#----) - * [如何对该开源文档进行贡献](#------------) - * [为什么要做这个开源文档?](#------------) - * [投稿](#--) - * [联系我](#---) - * [公众号](#---) - +- [数据结构与算法](#数据结构与算法) + - [数据结构](#数据结构) + - [算法](#算法) +- [数据库](#数据库) + - [MySQL](#mysql) + - [MySQL(优化思路)](#mysql优化思路) +- [系统设计](#系统设计) + - [秒杀系统相关](#秒杀系统相关) + - [前后端分离](#前后端分离) + - [单点登录](#单点登录) + - [常用框架](#常用框架) + - [Spring](#spring) + - [SpringBoot](#springboot) +- [分布式](#分布式) + - [dubbo](#dubbo) + - [zookeeper](#zookeeper) + - [RocketMQ](#rocketmq) + - [RabbitMQ](#rabbitmq) + - [kafka](#kafka) + - [消息中间件](#消息中间件) + - [redis](#redis) + - [分布式系统](#分布式系统) +- [线上问题调优(虚拟机,tomcat)](#线上问题调优虚拟机tomcat) +- [面试指南](#面试指南) +- [工具](#工具) + - [Git](#git) + - [Docker](#docker) +- [其他](#其他) + - [权限控制(设计、shiro)](#权限控制设计shiro) +- [Java学习资源](#java学习资源) +- [Java书籍推荐](#java书籍推荐) +- [实战项目推荐](#实战项目推荐) +- [程序人生](#程序人生) +- [说明](#说明) + - [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/Backtracking-NQueens.md b/docs/dataStructures-algorithms/Backtracking-NQueens.md deleted file mode 100644 index bac262d..0000000 --- a/docs/dataStructures-algorithms/Backtracking-NQueens.md +++ /dev/null @@ -1,145 +0,0 @@ -# N皇后 -[51. N皇后](https://leetcode-cn.com/problems/n-queens/) -### 题目描述 -> n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。 -> -![ANUzjA.png](https://s2.ax1x.com/2019/03/26/ANUzjA.png) -> -上图为 8 皇后问题的一种解法。 -> -给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。 -> -每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。 - -示例: - -``` -输入: 4 -输出: [ - [".Q..", // 解法 1 - "...Q", - "Q...", - "..Q."], - - ["..Q.", // 解法 2 - "Q...", - "...Q", - ".Q.."] -] -解释: 4 皇后问题存在两个不同的解法。 -``` - -### 问题分析 -约束条件为每个棋子所在的行、列、对角线都不能有另一个棋子。 - -使用一维数组表示一种解法,下标(index)表示行,值(value)表示该行的Q(皇后)在哪一列。 -每行只存储一个元素,然后递归到下一行,这样就不用判断行了,只需要判断列和对角线。 -### Solution1 -当result[row] = column时,即row行的棋子在column列。 - -对于[0, row-1]的任意一行(i 行),若 row 行的棋子和 i 行的棋子在同一列,则有result[i] == column; -若 row 行的棋子和 i 行的棋子在同一对角线,等腰直角三角形两直角边相等,即 row - i == Math.abs(result[i] - column) - -布尔类型变量 isValid 的作用是剪枝,减少不必要的递归。 -``` -public List> solveNQueens(int n) { - // 下标代表行,值代表列。如result[0] = 3 表示第1行的Q在第3列 - int[] result = new int[n]; - List> resultList = new LinkedList<>(); - dfs(resultList, result, 0, n); - return resultList; -} - -void dfs(List> resultList, int[] result, int row, int n) { - // 递归终止条件 - if (row == n) { - List list = new LinkedList<>(); - for (int x = 0; x < n; ++x) { - StringBuilder sb = new StringBuilder(); - for (int y = 0; y < n; ++y) - sb.append(result[x] == y ? "Q" : "."); - list.add(sb.toString()); - } - resultList.add(list); - return; - } - for (int column = 0; column < n; ++column) { - boolean isValid = true; - result[row] = column; - /* - * 逐行往下考察每一行。同列,result[i] == column - * 同对角线,row - i == Math.abs(result[i] - column) - */ - for (int i = row - 1; i >= 0; --i) { - if (result[i] == column || row - i == Math.abs(result[i] - column)) { - isValid = false; - break; - } - } - if (isValid) dfs(resultList, result, row + 1, n); - } -} -``` -### Solution2 -使用LinkedList表示一种解法,下标(index)表示行,值(value)表示该行的Q(皇后)在哪一列。 - -解法二和解法一的不同在于,相同列以及相同对角线的校验。 -将对角线抽象成【一次函数】这个简单的数学模型,根据一次函数的截距是常量这一特性进行校验。 - -这里,我将右上-左下对角线,简称为“\”对角线;左上-右下对角线简称为“/”对角线。 - -“/”对角线斜率为1,对应方程为y = x + b,其中b为截距。 -对于线上任意一点,均有y - x = b,即row - i = b; -定义一个布尔类型数组anti_diag,将b作为下标,当anti_diag[b] = true时,表示相应对角线上已经放置棋子。 -但row - i有可能为负数,负数不能作为数组下标,row - i 的最小值为-n(当row = 0,i = n时),可以加上n作为数组下标,即将row -i + n 作为数组下标。 -row - i + n 的最大值为 2n(当row = n,i = 0时),故anti_diag的容量设置为 2n 即可。 - -![ANXG79.png](https://s2.ax1x.com/2019/03/26/ANXG79.png) - -“\”对角线斜率为-1,对应方程为y = -x + b,其中b为截距。 -对于线上任意一点,均有y + x = b,即row + i = b; -同理,定义数组main_diag,将b作为下标,当main_diag[row + i] = true时,表示相应对角线上已经放置棋子。 - -有了两个校验对角线的数组,再来定义一个用于校验列的数组cols,这个太简单啦,不解释。 - -**解法二时间复杂度为O(n!),在校验相同列和相同对角线时,引入三个布尔类型数组进行判断。相比解法一,少了一层循环,用空间换时间。** - -``` -List> resultList = new LinkedList<>(); - -public List> solveNQueens(int n) { - boolean[] cols = new boolean[n]; - boolean[] main_diag = new boolean[2 * n]; - boolean[] anti_diag = new boolean[2 * n]; - LinkedList result = new LinkedList<>(); - dfs(result, 0, cols, main_diag, anti_diag, n); - return resultList; -} - -void dfs(LinkedList result, int row, boolean[] cols, boolean[] main_diag, boolean[] anti_diag, int n) { - if (row == n) { - List list = new LinkedList<>(); - for (int x = 0; x < n; ++x) { - StringBuilder sb = new StringBuilder(); - for (int y = 0; y < n; ++y) - sb.append(result.get(x) == y ? "Q" : "."); - list.add(sb.toString()); - } - resultList.add(list); - return; - } - for (int i = 0; i < n; ++i) { - if (cols[i] || main_diag[row + i] || anti_diag[row - i + n]) - continue; - result.add(i); - cols[i] = true; - main_diag[row + i] = true; - anti_diag[row - i + n] = true; - dfs(result, row + 1, cols, main_diag, anti_diag, n); - result.removeLast(); - cols[i] = false; - main_diag[row + i] = false; - anti_diag[row - i + n] = false; - } -} -``` \ No newline at end of file diff --git "a/docs/dataStructures-algorithms/\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\205\254\345\217\270\347\234\237\351\242\230.md" "b/docs/dataStructures-algorithms/\345\205\254\345\217\270\347\234\237\351\242\230.md" deleted file mode 100644 index c78ed8f..0000000 --- "a/docs/dataStructures-algorithms/\345\205\254\345\217\270\347\234\237\351\242\230.md" +++ /dev/null @@ -1,254 +0,0 @@ -# 网易 2018 - -下面三道编程题来自网易2018校招编程题,这三道应该来说是非常简单的编程题了,这些题目大家稍微有点编程和数学基础的话应该没什么问题。看答案之前一定要自己先想一下如果是自己做的话会怎么去做,然后再对照这我的答案看看,和你自己想的有什么区别?那一种方法更好? - -## 问题 - -### 一 获得特定数量硬币问题 - -小易准备去魔法王国采购魔法神器,购买魔法神器需要使用魔法币,但是小易现在一枚魔法币都没有,但是小易有两台魔法机器可以通过投入x(x可以为0)个魔法币产生更多的魔法币。 - -魔法机器1:如果投入x个魔法币,魔法机器会将其变为2x+1个魔法币 - -魔法机器2:如果投入x个魔法币,魔法机器会将其变为2x+2个魔法币 - -小易采购魔法神器总共需要n个魔法币,所以小易只能通过两台魔法机器产生恰好n个魔法币,小易需要你帮他设计一个投入方案使他最后恰好拥有n个魔法币。 - -**输入描述:** 输入包括一行,包括一个正整数n(1 ≤ n ≤ 10^9),表示小易需要的魔法币数量。 - -**输出描述:** 输出一个字符串,每个字符表示该次小易选取投入的魔法机器。其中只包含字符'1'和'2'。 - -**输入例子1:** 10 - -**输出例子1:** 122 - -### 二 求“相反数”问题 - -为了得到一个数的"相反数",我们将这个数的数字顺序颠倒,然后再加上原先的数得到"相反数"。例如,为了得到1325的"相反数",首先我们将该数的数字顺序颠倒,我们得到5231,之后再加上原先的数,我们得到5231+1325=6556.如果颠倒之后的数字有前缀零,前缀零将会被忽略。例如n = 100, 颠倒之后是1. - -**输入描述:** 输入包括一个整数n,(1 ≤ n ≤ 10^5) - -**输出描述:** 输出一个整数,表示n的相反数 - -**输入例子1:** 1325 - -**输出例子1:** 6556 - -### 三 字符串碎片的平均长度 - -一个由小写字母组成的字符串可以看成一些同一字母的最大碎片组成的。例如,"aaabbaaac"是由下面碎片组成的:'aaa','bb','c'。牛牛现在给定一个字符串,请你帮助计算这个字符串的所有碎片的平均长度是多少。 - -**输入描述:** 输入包括一个字符串s,字符串s的长度length(1 ≤ length ≤ 50),s只含小写字母('a'-'z') - -**输出描述:** 输出一个整数,表示所有碎片的平均长度,四舍五入保留两位小数。 - -**如样例所示:** s = "aaabbaaac" -所有碎片的平均长度 = (3 + 2 + 3 + 1) / 4 = 2.25 - -**输入例子1:** aaabbaaac - -**输出例子1:** 2.25 - -## 答案 - -### 一 获得特定数量硬币问题 - -#### 分析: - -作为该试卷的第一题,这道题应该只要思路正确就很简单了。 - -解题关键:明确魔法机器1只能产生奇数,魔法机器2只能产生偶数即可。我们从后往前一步一步推回去即可。 - -#### 示例代码 - -注意:由于用户的输入不确定性,一般是为了程序高可用性使需要将捕获用户输入异常然后友好提示用户输入类型错误并重新输入的。所以下面我给了两个版本,这两个版本都是正确的。这里只是给大家演示如何捕获输入类型异常,后面的题目中我给的代码没有异常处理的部分,参照下面两个示例代码,应该很容易添加。(PS:企业面试中没有明确就不用添加异常处理,当然你有的话也更好) - -**不带输入异常处理判断的版本:** - -```java -import java.util.Scanner; - -public class Main2 { - // 解题关键:明确魔法机器1只能产生奇数,魔法机器2只能产生偶数即可。我们从后往前一步一步推回去即可。 - - public static void main(String[] args) { - System.out.println("请输入要获得的硬币数量:"); - Scanner scanner = new Scanner(System.in); - int coincount = scanner.nextInt(); - StringBuilder sb = new StringBuilder(); - while (coincount >= 1) { - // 偶数的情况 - if (coincount % 2 == 0) { - coincount = (coincount - 2) / 2; - sb.append("2"); - // 奇数的情况 - } else { - coincount = (coincount - 1) / 2; - sb.append("1"); - } - } - // 输出反转后的字符串 - System.out.println(sb.reverse()); - - } -} -``` - -**带输入异常处理判断的版本(当输入的不是整数的时候会提示重新输入):** - -```java -import java.util.InputMismatchException; -import java.util.Scanner; - - -public class Main { - // 解题关键:明确魔法机器1只能产生奇数,魔法机器2只能产生偶数即可。我们从后往前一步一步推回去即可。 - - public static void main(String[] args) { - System.out.println("请输入要获得的硬币数量:"); - Scanner scanner = new Scanner(System.in); - boolean flag = true; - while (flag) { - try { - int coincount = scanner.nextInt(); - StringBuilder sb = new StringBuilder(); - while (coincount >= 1) { - // 偶数的情况 - if (coincount % 2 == 0) { - coincount = (coincount - 2) / 2; - sb.append("2"); - // 奇数的情况 - } else { - coincount = (coincount - 1) / 2; - sb.append("1"); - } - } - // 输出反转后的字符串 - System.out.println(sb.reverse()); - flag=false;//程序结束 - } catch (InputMismatchException e) { - System.out.println("输入数据类型不匹配,请您重新输入:"); - scanner.nextLine(); - continue; - } - } - - } -} - -``` - -### 二 求“相反数”问题 - -#### 分析: - -解决本道题有几种不同的方法,但是最快速的方法就是利用reverse()方法反转字符串然后再将字符串转换成int类型的整数,这个方法是快速解决本题关键。我们先来回顾一下下面两个知识点: - -**1)String转int;** - -在 Java 中要将 String 类型转化为 int 类型时,需要使用 Integer 类中的 parseInt() 方法或者 valueOf() 方法进行转换. - -```java - String str = "123"; - int a = Integer.parseInt(str); -``` - - 或 - -```java - String str = "123"; - int a = Integer.valueOf(str).intValue(); -``` - -**2)next()和nextLine()的区别** - -在Java中输入字符串有两种方法,就是next()和nextLine().两者的区别就是:nextLine()的输入是碰到回车就终止输入,而next()方法是碰到空格,回车,Tab键都会被视为终止符。所以next()不会得到带空格的字符串,而nextLine()可以得到带空格的字符串。 - -#### 示例代码: - -```java -import java.util.Scanner; - -/** - * 本题关键:①String转int;②next()和nextLine()的区别 - */ -public class Main { - - public static void main(String[] args) { - - System.out.println("请输入一个整数:"); - Scanner scanner = new Scanner(System.in); - String s=scanner.next(); - //将字符串转换成数字 - int number1=Integer.parseInt(s); - //将字符串倒序后转换成数字 - //因为Integer.parseInt()的参数类型必须是字符串所以必须加上toString() - int number2=Integer.parseInt(new StringBuilder(s).reverse().toString()); - System.out.println(number1+number2); - - } -} -``` - -### 三 字符串碎片的平均长度 - -#### 分析: - -这道题的意思也就是要求:(字符串的总长度)/(相同字母团构成的字符串的个数)。 - -这样就很简单了,就变成了字符串的字符之间的比较。如果需要比较字符串的字符的话,我们可以利用charAt(i)方法:取出特定位置的字符与后一个字符比较,或者利用toCharArray()方法将字符串转换成字符数组采用同样的方法做比较。 - -#### 示例代码 - -**利用charAt(i)方法:** - -```java -import java.util.Scanner; - -public class Main { - - public static void main(String[] args) { - - Scanner sc = new Scanner(System.in); - while (sc.hasNext()) { - String s = sc.next(); - //个数至少为一个 - float count = 1; - for (int i = 0; i < s.length() - 1; i++) { - if (s.charAt(i) != s.charAt(i + 1)) { - count++; - } - } - System.out.println(s.length() / count); - } - } - -} -``` - -**利用toCharArray()方法:** - -```java -import java.util.Scanner; - -public class Main2 { - - public static void main(String[] args) { - - Scanner sc = new Scanner(System.in); - while (sc.hasNext()) { - String s = sc.next(); - //个数至少为一个 - float count = 1; - char [] stringArr = s.toCharArray(); - for (int i = 0; i < stringArr.length - 1; i++) { - if (stringArr[i] != stringArr[i + 1]) { - count++; - } - } - System.out.println(s.length() / count); - } - } - -} -``` \ No newline at end of file diff --git "a/docs/dataStructures-algorithms/\345\207\240\351\201\223\345\270\270\350\247\201\347\232\204\345\255\220\347\254\246\344\270\262\347\256\227\346\263\225\351\242\230.md" "b/docs/dataStructures-algorithms/\345\207\240\351\201\223\345\270\270\350\247\201\347\232\204\345\255\220\347\254\246\344\270\262\347\256\227\346\263\225\351\242\230.md" deleted file mode 100644 index 45ea008..0000000 --- "a/docs/dataStructures-algorithms/\345\207\240\351\201\223\345\270\270\350\247\201\347\232\204\345\255\220\347\254\246\344\270\262\347\256\227\346\263\225\351\242\230.md" +++ /dev/null @@ -1,467 +0,0 @@ -[toc] - - -## 说明 - -- 本文作者:wwwxmu -- 原文地址:https://www.weiweiblog.cn/13string/ -- 作者的博客站点:https://www.weiweiblog.cn/ (推荐哦!) - -考虑到篇幅问题,我会分两次更新这个内容。本篇文章只是原文的一部分,我在原文的基础上增加了部分内容以及修改了部分代码和注释。另外,我增加了爱奇艺 2018 秋招 Java:`求给定合法括号序列的深度` 这道题。所有代码均编译成功,并带有注释,欢迎各位享用! - -## 1. KMP 算法 - -谈到字符串问题,不得不提的就是 KMP 算法,它是用来解决字符串查找的问题,可以在一个字符串(S)中查找一个子串(W)出现的位置。KMP 算法把字符匹配的时间复杂度缩小到 O(m+n) ,而空间复杂度也只有O(m)。因为“暴力搜索”的方法会反复回溯主串,导致效率低下,而KMP算法可以利用已经部分匹配这个有效信息,保持主串上的指针不回溯,通过修改子串的指针,让模式串尽量地移动到有效的位置。 - -具体算法细节请参考: - -- **字符串匹配的KMP算法:** http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html -- **从头到尾彻底理解KMP:** https://blog.csdn.net/v_july_v/article/details/7041827 -- **如何更好的理解和掌握 KMP 算法?:** https://www.zhihu.com/question/21923021 -- **KMP 算法详细解析:** https://blog.sengxian.com/algorithms/kmp -- **图解 KMP 算法:** http://blog.jobbole.com/76611/ -- **汪都能听懂的KMP字符串匹配算法【双语字幕】:** https://www.bilibili.com/video/av3246487/?from=search&seid=17173603269940723925 -- **KMP字符串匹配算法1:** https://www.bilibili.com/video/av11866460?from=search&seid=12730654434238709250 - -**除此之外,再来了解一下BM算法!** - -> BM算法也是一种精确字符串匹配算法,它采用从右向左比较的方法,同时应用到了两种启发式规则,即坏字符规则 和好后缀规则 ,来决定向右跳跃的距离。基本思路就是从右往左进行字符匹配,遇到不匹配的字符后从坏字符表和好后缀表找一个最大的右移值,将模式串右移继续匹配。 -《字符串匹配的KMP算法》:http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html - - -## 2. 替换空格 - -> 剑指offer:请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。 - -这里我提供了两种方法:①常规方法;②利用 API 解决。 - -```java -//https://www.weiweiblog.cn/replacespace/ -public class Solution { - - /** - * 第一种方法:常规方法。利用String.charAt(i)以及String.valueOf(char).equals(" " - * )遍历字符串并判断元素是否为空格。是则替换为"%20",否则不替换 - */ - public static String replaceSpace(StringBuffer str) { - - int length = str.length(); - // System.out.println("length=" + length); - StringBuffer result = new StringBuffer(); - for (int i = 0; i < length; i++) { - char b = str.charAt(i); - if (String.valueOf(b).equals(" ")) { - result.append("%20"); - } else { - result.append(b); - } - } - return result.toString(); - - } - - /** - * 第二种方法:利用API替换掉所用空格,一行代码解决问题 - */ - public static String replaceSpace2(StringBuffer str) { - - return str.toString().replaceAll("\\s", "%20"); - } -} - -``` - -## 3. 最长公共前缀 - -> Leetcode: 编写一个函数来查找字符串数组中的最长公共前缀。如果不存在公共前缀,返回空字符串 ""。 - -示例 1: - -``` -输入: ["flower","flow","flight"] -输出: "fl" -``` - -示例 2: - -``` -输入: ["dog","racecar","car"] -输出: "" -解释: 输入不存在公共前缀。 -``` - - -思路很简单!先利用Arrays.sort(strs)为数组排序,再将数组第一个元素和最后一个元素的字符从前往后对比即可! - -```java -public class Main { - public static String replaceSpace(String[] strs) { - - // 如果检查值不合法及就返回空串 - if (!checkStrs(strs)) { - return ""; - } - // 数组长度 - int len = strs.length; - // 用于保存结果 - StringBuilder res = new StringBuilder(); - // 给字符串数组的元素按照升序排序(包含数字的话,数字会排在前面) - Arrays.sort(strs); - int m = strs[0].length(); - int n = strs[len - 1].length(); - int num = Math.min(m, n); - for (int i = 0; i < num; i++) { - if (strs[0].charAt(i) == strs[len - 1].charAt(i)) { - res.append(strs[0].charAt(i)); - } else - break; - - } - return res.toString(); - - } - - private static boolean chechStrs(String[] strs) { - boolean flag = false; - if (strs != null) { - // 遍历strs检查元素值 - for (int i = 0; i < strs.length; i++) { - if (strs[i] != null && strs[i].length() != 0) { - flag = true; - } else { - flag = false; - break; - } - } - } - return flag; - } - - // 测试 - public static void main(String[] args) { - String[] strs = { "customer", "car", "cat" }; - // String[] strs = { "customer", "car", null };//空串 - // String[] strs = {};//空串 - // String[] strs = null;//空串 - System.out.println(Main.replaceSpace(strs));// c - } -} - -``` - -## 4. 回文串 - -### 4.1. 最长回文串 - -> LeetCode: 给定一个包含大写字母和小写字母的字符串,找到通过这些字母构造成的最长的回文串。在构造过程中,请注意区分大小写。比如`"Aa"`不能当做一个回文字符串。注 -意:假设字符串的长度不会超过 1010。 - - - -> 回文串:“回文串”是一个正读和反读都一样的字符串,比如“level”或者“noon”等等就是回文串。——百度百科 地址:https://baike.baidu.com/item/%E5%9B%9E%E6%96%87%E4%B8%B2/1274921?fr=aladdin - -示例 1: - -``` -输入: -"abccccdd" - -输出: -7 - -解释: -我们可以构造的最长的回文串是"dccaccd", 它的长度是 7。 -``` - -我们上面已经知道了什么是回文串?现在我们考虑一下可以构成回文串的两种情况: - -- 字符出现次数为双数的组合 -- 字符出现次数为双数的组合+一个只出现一次的字符 - -统计字符出现的次数即可,双数才能构成回文。因为允许中间一个数单独出现,比如“abcba”,所以如果最后有字母落单,总长度可以加 1。首先将字符串转变为字符数组。然后遍历该数组,判断对应字符是否在hashset中,如果不在就加进去,如果在就让count++,然后移除该字符!这样就能找到出现次数为双数的字符个数。 - -```java -//https://leetcode-cn.com/problems/longest-palindrome/description/ -class Solution { - public int longestPalindrome(String s) { - if (s.length() == 0) - return 0; - // 用于存放字符 - HashSet hashset = new HashSet(); - char[] chars = s.toCharArray(); - int count = 0; - for (int i = 0; i < chars.length; i++) { - if (!hashset.contains(chars[i])) {// 如果hashset没有该字符就保存进去 - hashset.add(chars[i]); - } else {// 如果有,就让count++(说明找到了一个成对的字符),然后把该字符移除 - hashset.remove(chars[i]); - count++; - } - } - return hashset.isEmpty() ? count * 2 : count * 2 + 1; - } -} -``` - - -### 4.2. 验证回文串 - -> LeetCode: 给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。 说明:本题中,我们将空字符串定义为有效的回文串。 - -示例 1: - -``` -输入: "A man, a plan, a canal: Panama" -输出: true -``` - -示例 2: - -``` -输入: "race a car" -输出: false -``` - -```java -//https://leetcode-cn.com/problems/valid-palindrome/description/ -class Solution { - public boolean isPalindrome(String s) { - if (s.length() == 0) - return true; - int l = 0, r = s.length() - 1; - while (l < r) { - // 从头和尾开始向中间遍历 - if (!Character.isLetterOrDigit(s.charAt(l))) {// 字符不是字母和数字的情况 - l++; - } else if (!Character.isLetterOrDigit(s.charAt(r))) {// 字符不是字母和数字的情况 - r--; - } else { - // 判断二者是否相等 - if (Character.toLowerCase(s.charAt(l)) != Character.toLowerCase(s.charAt(r))) - return false; - l++; - r--; - } - } - return true; - } -} -``` - - -### 4.3. 最长回文子串 - -> Leetcode: LeetCode: 最长回文子串 给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为1000。 - -示例 1: - -``` -输入: "babad" -输出: "bab" -注意: "aba"也是一个有效答案。 -``` - -示例 2: - -``` -输入: "cbbd" -输出: "bb" -``` - -以某个元素为中心,分别计算偶数长度的回文最大长度和奇数长度的回文最大长度。给大家大致花了个草图,不要嫌弃! - - -![](https://user-gold-cdn.xitu.io/2018/9/9/165bc32f6f1833ff?w=723&h=371&f=png&s=9305) - -```java -//https://leetcode-cn.com/problems/longest-palindromic-substring/description/ -class Solution { - private int index, len; - - public String longestPalindrome(String s) { - if (s.length() < 2) - return s; - for (int i = 0; i < s.length() - 1; i++) { - PalindromeHelper(s, i, i); - PalindromeHelper(s, i, i + 1); - } - return s.substring(index, index + len); - } - - public void PalindromeHelper(String s, int l, int r) { - while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) { - l--; - r++; - } - if (len < r - l - 1) { - index = l + 1; - len = r - l - 1; - } - } -} -``` - -### 4.4. 最长回文子序列 - -> LeetCode: 最长回文子序列 -给定一个字符串s,找到其中最长的回文子序列。可以假设s的最大长度为1000。 -**最长回文子序列和上一题最长回文子串的区别是,子串是字符串中连续的一个序列,而子序列是字符串中保持相对位置的字符序列,例如,"bbbb"可以是字符串"bbbab"的子序列但不是子串。** - -给定一个字符串s,找到其中最长的回文子序列。可以假设s的最大长度为1000。 - -示例 1: - -``` -输入: -"bbbab" -输出: -4 -``` -一个可能的最长回文子序列为 "bbbb"。 - -示例 2: - -``` -输入: -"cbbd" -输出: -2 -``` - -一个可能的最长回文子序列为 "bb"。 - -**动态规划:** dp[i][j] = dp[i+1][j-1] + 2 if s.charAt(i) == s.charAt(j) otherwise, dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]) - -```java -class Solution { - public int longestPalindromeSubseq(String s) { - int len = s.length(); - int [][] dp = new int[len][len]; - for(int i = len - 1; i>=0; i--){ - dp[i][i] = 1; - for(int j = i+1; j < len; j++){ - if(s.charAt(i) == s.charAt(j)) - dp[i][j] = dp[i+1][j-1] + 2; - else - dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]); - } - } - return dp[0][len-1]; - } -} -``` - -## 5. 括号匹配深度 - -> 爱奇艺 2018 秋招 Java: ->一个合法的括号匹配序列有以下定义: ->1. 空串""是一个合法的括号匹配序列 ->2. 如果"X"和"Y"都是合法的括号匹配序列,"XY"也是一个合法的括号匹配序列 ->3. 如果"X"是一个合法的括号匹配序列,那么"(X)"也是一个合法的括号匹配序列 ->4. 每个合法的括号序列都可以由以上规则生成。 - -> 例如: "","()","()()","((()))"都是合法的括号序列 ->对于一个合法的括号序列我们又有以下定义它的深度: ->1. 空串""的深度是0 ->2. 如果字符串"X"的深度是x,字符串"Y"的深度是y,那么字符串"XY"的深度为max(x,y) ->3. 如果"X"的深度是x,那么字符串"(X)"的深度是x+1 - -> 例如: "()()()"的深度是1,"((()))"的深度是3。牛牛现在给你一个合法的括号序列,需要你计算出其深度。 - -``` -输入描述: -输入包括一个合法的括号序列s,s长度length(2 ≤ length ≤ 50),序列中只包含'('和')'。 - -输出描述: -输出一个正整数,即这个序列的深度。 -``` - -示例: - -``` -输入: -(()) -输出: -2 -``` - -思路草图: - - -![](https://user-gold-cdn.xitu.io/2018/9/9/165bc6fca94ef278?w=792&h=324&f=png&s=15868) - -代码如下: - -```java -import java.util.Scanner; - -/** - * https://www.nowcoder.com/test/8246651/summary - * - * @author Snailclimb - * @date 2018年9月6日 - * @Description: TODO 求给定合法括号序列的深度 - */ -public class Main { - public static void main(String[] args) { - Scanner sc = new Scanner(System.in); - String s = sc.nextLine(); - int cnt = 0, max = 0, i; - for (i = 0; i < s.length(); ++i) { - if (s.charAt(i) == '(') - cnt++; - else - cnt--; - max = Math.max(max, cnt); - } - sc.close(); - System.out.println(max); - } -} - -``` - -## 6. 把字符串转换成整数 - -> 剑指offer: 将一个字符串转换成一个整数(实现Integer.valueOf(string)的功能,但是string不符合数字要求时返回0),要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0。 - -```java -//https://www.weiweiblog.cn/strtoint/ -public class Main { - - public static int StrToInt(String str) { - if (str.length() == 0) - return 0; - char[] chars = str.toCharArray(); - // 判断是否存在符号位 - int flag = 0; - if (chars[0] == '+') - flag = 1; - else if (chars[0] == '-') - flag = 2; - int start = flag > 0 ? 1 : 0; - int res = 0;// 保存结果 - for (int i = start; i < chars.length; i++) { - if (Character.isDigit(chars[i])) {// 调用Character.isDigit(char)方法判断是否是数字,是返回True,否则False - int temp = chars[i] - '0'; - res = res * 10 + temp; - } else { - return 0; - } - } - return flag != 2 ? res : -res; - - } - - public static void main(String[] args) { - // TODO Auto-generated method stub - String s = "-12312312"; - System.out.println("使用库函数转换:" + Integer.valueOf(s)); - int res = Main.StrToInt(s); - System.out.println("使用自己写的方法转换:" + res); - - } - -} - -``` diff --git "a/docs/dataStructures-algorithms/\345\207\240\351\201\223\345\270\270\350\247\201\347\232\204\351\223\276\350\241\250\347\256\227\346\263\225\351\242\230.md" "b/docs/dataStructures-algorithms/\345\207\240\351\201\223\345\270\270\350\247\201\347\232\204\351\223\276\350\241\250\347\256\227\346\263\225\351\242\230.md" deleted file mode 100644 index 85e2934..0000000 --- "a/docs/dataStructures-algorithms/\345\207\240\351\201\223\345\270\270\350\247\201\347\232\204\351\223\276\350\241\250\347\256\227\346\263\225\351\242\230.md" +++ /dev/null @@ -1,421 +0,0 @@ - - -- [1. 两数相加](#1-两数相加) - - [题目描述](#题目描述) - - [问题分析](#问题分析) - - [Solution](#solution) -- [2. 翻转链表](#2-翻转链表) - - [题目描述](#题目描述-1) - - [问题分析](#问题分析-1) - - [Solution](#solution-1) -- [3. 链表中倒数第k个节点](#3-链表中倒数第k个节点) - - [题目描述](#题目描述-2) - - [问题分析](#问题分析-2) - - [Solution](#solution-2) -- [4. 删除链表的倒数第N个节点](#4-删除链表的倒数第n个节点) - - [问题分析](#问题分析-3) - - [Solution](#solution-3) -- [5. 合并两个排序的链表](#5-合并两个排序的链表) - - [题目描述](#题目描述-3) - - [问题分析](#问题分析-4) - - [Solution](#solution-4) - - - - -# 1. 两数相加 - -### 题目描述 - -> Leetcode:给定两个非空链表来表示两个非负整数。位数按照逆序方式存储,它们的每个节点只存储单个数字。将两数相加返回一个新的链表。 -> ->你可以假设除了数字 0 之外,这两个数字都不会以零开头。 - -示例: - -``` -输入:(2 -> 4 -> 3) + (5 -> 6 -> 4) -输出:7 -> 0 -> 8 -原因:342 + 465 = 807 -``` - -### 问题分析 - -Leetcode官方详细解答地址: - - https://leetcode-cn.com/problems/add-two-numbers/solution/ - -> 要对头结点进行操作时,考虑创建哑节点dummy,使用dummy->next表示真正的头节点。这样可以避免处理头节点为空的边界问题。 - -我们使用变量来跟踪进位,并从包含最低有效位的表头开始模拟逐 -位相加的过程。 - -![图1,对两数相加方法的可视化: 342 + 465 = 807342+465=807, 每个结点都包含一个数字,并且数字按位逆序存储。](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-20/34910956.jpg) - -### Solution - -**我们首先从最低有效位也就是列表 l1和 l2 的表头开始相加。注意需要考虑到进位的情况!** - -```java -/** - * Definition for singly-linked list. - * public class ListNode { - * int val; - * ListNode next; - * ListNode(int x) { val = x; } - * } - */ - //https://leetcode-cn.com/problems/add-two-numbers/description/ -class Solution { -public ListNode addTwoNumbers(ListNode l1, ListNode l2) { - ListNode dummyHead = new ListNode(0); - ListNode p = l1, q = l2, curr = dummyHead; - //carry 表示进位数 - int carry = 0; - while (p != null || q != null) { - int x = (p != null) ? p.val : 0; - int y = (q != null) ? q.val : 0; - int sum = carry + x + y; - //进位数 - carry = sum / 10; - //新节点的数值为sum % 10 - curr.next = new ListNode(sum % 10); - curr = curr.next; - if (p != null) p = p.next; - if (q != null) q = q.next; - } - if (carry > 0) { - curr.next = new ListNode(carry); - } - return dummyHead.next; -} -} -``` - -# 2. 翻转链表 - - -### 题目描述 -> 剑指 offer:输入一个链表,反转链表后,输出链表的所有元素。 - -![翻转链表](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-20/81431871.jpg) - -### 问题分析 - -这道算法题,说直白点就是:如何让后一个节点指向前一个节点!在下面的代码中定义了一个 next 节点,该节点主要是保存要反转到头的那个节点,防止链表 “断裂”。 - -### Solution - - -```java -public class ListNode { - int val; - ListNode next = null; - - ListNode(int val) { - this.val = val; - } -} -``` - -```java -/** - * - * @author Snailclimb - * @date 2018年9月19日 - * @Description: TODO - */ -public class Solution { - - public ListNode ReverseList(ListNode head) { - - ListNode next = null; - ListNode pre = null; - - while (head != null) { - // 保存要反转到头的那个节点 - next = head.next; - // 要反转的那个节点指向已经反转的上一个节点(备注:第一次反转的时候会指向null) - head.next = pre; - // 上一个已经反转到头部的节点 - pre = head; - // 一直向链表尾走 - head = next; - } - return pre; - } - -} -``` - -测试方法: - -```java - public static void main(String[] args) { - - ListNode a = new ListNode(1); - ListNode b = new ListNode(2); - ListNode c = new ListNode(3); - ListNode d = new ListNode(4); - ListNode e = new ListNode(5); - a.next = b; - b.next = c; - c.next = d; - d.next = e; - new Solution().ReverseList(a); - while (e != null) { - System.out.println(e.val); - e = e.next; - } - } -``` - -输出: - -``` -5 -4 -3 -2 -1 -``` - -# 3. 链表中倒数第k个节点 - -### 题目描述 - -> 剑指offer: 输入一个链表,输出该链表中倒数第k个结点。 - -### 问题分析 - -> **链表中倒数第k个节点也就是正数第(L-K+1)个节点,知道了只一点,这一题基本就没问题!** - -首先两个节点/指针,一个节点 node1 先开始跑,指针 node1 跑到 k-1 个节点后,另一个节点 node2 开始跑,当 node1 跑到最后时,node2 所指的节点就是倒数第k个节点也就是正数第(L-K+1)个节点。 - - -### Solution - -```java -/* -public class ListNode { - int val; - ListNode next = null; - - ListNode(int val) { - this.val = val; - } -}*/ - -// 时间复杂度O(n),一次遍历即可 -// https://www.nowcoder.com/practice/529d3ae5a407492994ad2a246518148a?tpId=13&tqId=11167&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking -public class Solution { - public ListNode FindKthToTail(ListNode head, int k) { - // 如果链表为空或者k小于等于0 - if (head == null || k <= 0) { - return null; - } - // 声明两个指向头结点的节点 - ListNode node1 = head, node2 = head; - // 记录节点的个数 - int count = 0; - // 记录k值,后面要使用 - int index = k; - // p指针先跑,并且记录节点数,当node1节点跑了k-1个节点后,node2节点开始跑, - // 当node1节点跑到最后时,node2节点所指的节点就是倒数第k个节点 - while (node1 != null) { - node1 = node1.next; - count++; - if (k < 1) { - node2 = node2.next; - } - k--; - } - // 如果节点个数小于所求的倒数第k个节点,则返回空 - if (count < index) - return null; - return node2; - - } -} -``` - - -# 4. 删除链表的倒数第N个节点 - - -> Leetcode:给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。 - -**示例:** - -``` -给定一个链表: 1->2->3->4->5, 和 n = 2. - -当删除了倒数第二个节点后,链表变为 1->2->3->5. - -``` - -**说明:** - -给定的 n 保证是有效的。 - -**进阶:** - -你能尝试使用一趟扫描实现吗? - -该题在 leetcode 上有详细解答,具体可参考 Leetcode. - -### 问题分析 - - -我们注意到这个问题可以容易地简化成另一个问题:删除从列表开头数起的第 (L - n + 1)个结点,其中 L是列表的长度。只要我们找到列表的长度 L,这个问题就很容易解决。 - -![图 1. 删除列表中的第 L - n + 1 个元素](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-20/94354387.jpg) - -### Solution - -**两次遍历法** - -首先我们将添加一个 **哑结点** 作为辅助,该结点位于列表头部。哑结点用来简化某些极端情况,例如列表中只含有一个结点,或需要删除列表的头部。在第一次遍历中,我们找出列表的长度 L。然后设置一个指向哑结点的指针,并移动它遍历列表,直至它到达第 (L - n) 个结点那里。**我们把第 (L - n)个结点的 next 指针重新链接至第 (L - n + 2)个结点,完成这个算法。** - -```java -/** - * Definition for singly-linked list. - * public class ListNode { - * int val; - * ListNode next; - * ListNode(int x) { val = x; } - * } - */ -// https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/description/ -public class Solution { - public ListNode removeNthFromEnd(ListNode head, int n) { - // 哑结点,哑结点用来简化某些极端情况,例如列表中只含有一个结点,或需要删除列表的头部 - ListNode dummy = new ListNode(0); - // 哑结点指向头结点 - dummy.next = head; - // 保存链表长度 - int length = 0; - ListNode len = head; - while (len != null) { - length++; - len = len.next; - } - length = length - n; - ListNode target = dummy; - // 找到 L-n 位置的节点 - while (length > 0) { - target = target.next; - length--; - } - // 把第 (L - n)个结点的 next 指针重新链接至第 (L - n + 2)个结点 - target.next = target.next.next; - return dummy.next; - } -} -``` - -**复杂度分析:** - -- **时间复杂度 O(L)** :该算法对列表进行了两次遍历,首先计算了列表的长度 LL 其次找到第 (L - n)(L−n) 个结点。 操作执行了 2L-n2L−n 步,时间复杂度为 O(L)O(L)。 -- **空间复杂度 O(1)** :我们只用了常量级的额外空间。 - - - -**进阶——一次遍历法:** - - -> **链表中倒数第N个节点也就是正数第(L-N+1)个节点。 - -其实这种方法就和我们上面第四题找“链表中倒数第k个节点”所用的思想是一样的。**基本思路就是:** 定义两个节点 node1、node2;node1 节点先跑,node1节点 跑到第 n+1 个节点的时候,node2 节点开始跑.当node1 节点跑到最后一个节点时,node2 节点所在的位置就是第 (L-n ) 个节点(L代表总链表长度,也就是倒数第 n+1 个节点) - -```java -/** - * Definition for singly-linked list. - * public class ListNode { - * int val; - * ListNode next; - * ListNode(int x) { val = x; } - * } - */ -public class Solution { - public ListNode removeNthFromEnd(ListNode head, int n) { - - ListNode dummy = new ListNode(0); - dummy.next = head; - // 声明两个指向头结点的节点 - ListNode node1 = dummy, node2 = dummy; - - // node1 节点先跑,node1节点 跑到第 n 个节点的时候,node2 节点开始跑 - // 当node1 节点跑到最后一个节点时,node2 节点所在的位置就是第 (L-n ) 个节点,也就是倒数第 n+1(L代表总链表长度) - while (node1 != null) { - node1 = node1.next; - if (n < 1 && node1 != null) { - node2 = node2.next; - } - n--; - } - - node2.next = node2.next.next; - - return dummy.next; - - } -} -``` - - - - - -# 5. 合并两个排序的链表 - -### 题目描述 - -> 剑指offer:输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。 - -### 问题分析 - -我们可以这样分析: - -1. 假设我们有两个链表 A,B; -2. A的头节点A1的值与B的头结点B1的值比较,假设A1小,则A1为头节点; -3. A2再和B1比较,假设B1小,则,A1指向B1; -4. A2再和B2比较 -就这样循环往复就行了,应该还算好理解。 - -考虑通过递归的方式实现! - -### Solution - -**递归版本:** - -```java -/* -public class ListNode { - int val; - ListNode next = null; - - ListNode(int val) { - this.val = val; - } -}*/ -//https://www.nowcoder.com/practice/d8b6b4358f774294a89de2a6ac4d9337?tpId=13&tqId=11169&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking -public class Solution { -public ListNode Merge(ListNode list1,ListNode list2) { - if(list1 == null){ - return list2; - } - if(list2 == null){ - return list1; - } - if(list1.val <= list2.val){ - list1.next = Merge(list1.next, list2); - return list1; - }else{ - list2.next = Merge(list1, list2.next); - return list2; - } - } -} -``` - 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\203\250\345\210\206\347\274\226\347\250\213\351\242\230.md" "b/docs/dataStructures-algorithms/\345\211\221\346\214\207offer\351\203\250\345\210\206\347\274\226\347\250\213\351\242\230.md" deleted file mode 100644 index 51de35e..0000000 --- "a/docs/dataStructures-algorithms/\345\211\221\346\214\207offer\351\203\250\345\210\206\347\274\226\347\250\213\351\242\230.md" +++ /dev/null @@ -1,686 +0,0 @@ -### 一 斐波那契数列 - -#### **题目描述:** - -大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项。 -n<=39 - -#### **问题分析:** - -可以肯定的是这一题通过递归的方式是肯定能做出来,但是这样会有一个很大的问题,那就是递归大量的重复计算会导致内存溢出。另外可以使用迭代法,用fn1和fn2保存计算过程中的结果,并复用起来。下面我会把两个方法示例代码都给出来并给出两个方法的运行时间对比。 - -#### **示例代码:** - -**采用迭代法:** - -```java - int Fibonacci(int number) { - if (number <= 0) { - return 0; - } - if (number == 1 || number == 2) { - return 1; - } - int first = 1, second = 1, third = 0; - for (int i = 3; i <= number; i++) { - third = first + second; - first = second; - second = third; - } - return third; - } -``` - -**采用递归:** - -```java - public int Fibonacci(int n) { - - if (n <= 0) { - return 0; - } - if (n == 1||n==2) { - return 1; - } - - return Fibonacci(n - 2) + Fibonacci(n - 1); - - } -``` - -#### **运行时间对比:** - -假设n为40我们分别使用迭代法和递归法计算,计算结果如下: - -1. 迭代法 - ![迭代法](https://ws1.sinaimg.cn/large/006rNwoDgy1fpydt5as85j308a025dfl.jpg) -2. 递归法 - ![递归法](https://ws1.sinaimg.cn/large/006rNwoDgy1fpydt2d1k3j30ed02kt8i.jpg) - -### 二 跳台阶问题 - -#### **题目描述:** - -一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。 - -#### **问题分析:** - -**正常分析法:** -a.如果两种跳法,1阶或者2阶,那么假定第一次跳的是一阶,那么剩下的是n-1个台阶,跳法是f(n-1); -b.假定第一次跳的是2阶,那么剩下的是n-2个台阶,跳法是f(n-2) -c.由a,b假设可以得出总跳法为: f(n) = f(n-1) + f(n-2) -d.然后通过实际的情况可以得出:只有一阶的时候 f(1) = 1 ,只有两阶的时候可以有 f(2) = 2 -**找规律分析法:** -f(1) = 1, f(2) = 2, f(3) = 3, f(4) = 5, 可以总结出f(n) = f(n-1) + f(n-2)的规律。 -但是为什么会出现这样的规律呢?假设现在6个台阶,我们可以从第5跳一步到6,这样的话有多少种方案跳到5就有多少种方案跳到6,另外我们也可以从4跳两步跳到6,跳到4有多少种方案的话,就有多少种方案跳到6,其他的不能从3跳到6什么的啦,所以最后就是f(6) = f(5) + f(4);这样子也很好理解变态跳台阶的问题了。 - -**所以这道题其实就是斐波那契数列的问题。** -代码只需要在上一题的代码稍做修改即可。和上一题唯一不同的就是这一题的初始元素变为 1 2 3 5 8.....而上一题为1 1 2 3 5 .......。另外这一题也可以用递归做,但是递归效率太低,所以我这里只给出了迭代方式的代码。 - -#### **示例代码:** - -```java - int jumpFloor(int number) { - if (number <= 0) { - return 0; - } - if (number == 1) { - return 1; - } - if (number == 2) { - return 2; - } - int first = 1, second = 2, third = 0; - for (int i = 3; i <= number; i++) { - third = first + second; - first = second; - second = third; - } - return third; - } -``` - -### 三 变态跳台阶问题 - -#### **题目描述:** - -一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。 - -#### **问题分析:** - -假设n>=2,第一步有n种跳法:跳1级、跳2级、到跳n级 -跳1级,剩下n-1级,则剩下跳法是f(n-1) -跳2级,剩下n-2级,则剩下跳法是f(n-2) -...... -跳n-1级,剩下1级,则剩下跳法是f(1) -跳n级,剩下0级,则剩下跳法是f(0) -所以在n>=2的情况下: -f(n)=f(n-1)+f(n-2)+...+f(1) -因为f(n-1)=f(n-2)+f(n-3)+...+f(1) -所以f(n)=2*f(n-1) 又f(1)=1,所以可得**f(n)=2^(number-1)** - -#### **示例代码:** - -```java - int JumpFloorII(int number) { - return 1 << --number;//2^(number-1)用位移操作进行,更快 - } -``` - -#### **补充:** - -**java中有三种移位运算符:** - -1. “<<” : **左移运算符**,等同于乘2的n次方 -2. “>>”: **右移运算符**,等同于除2的n次方 -3. “>>>” **无符号右移运算符**,不管移动前最高位是0还是1,右移后左侧产生的空位部分都以0来填充。与>>类似。 - 例: - int a = 16; - int b = a << 2;//左移2,等同于16 * 2的2次方,也就是16 * 4 - int c = a >> 2;//右移2,等同于16 / 2的2次方,也就是16 / 4 - -### 四 二维数组查找 - -#### **题目描述:** - -在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。 - -#### **问题解析:** - -这一道题还是比较简单的,我们需要考虑的是如何做,效率最快。这里有一种很好理解的思路: - -> 矩阵是有序的,从左下角来看,向上数字递减,向右数字递增, -> 因此从左下角开始查找,当要查找数字比左下角数字大时。右移 -> 要查找数字比左下角数字小时,上移。这样找的速度最快。 - -#### **示例代码:** - -```java - public boolean Find(int target, int [][] array) { - //基本思路从左下角开始找,这样速度最快 - int row = array.length-1;//行 - int column = 0;//列 - //当行数大于0,当前列数小于总列数时循环条件成立 - while((row >= 0)&& (column< array[0].length)){ - if(array[row][column] > target){ - row--; - }else if(array[row][column] < target){ - column++; - }else{ - return true; - } - } - return false; - } -``` - -### 五 替换空格 - -#### **题目描述:** - -请实现一个函数,将一个字符串中的空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。 - -#### **问题分析:** - -这道题不难,我们可以通过循环判断字符串的字符是否为空格,是的话就利用append()方法添加追加“%20”,否则还是追加原字符。 - -或者最简单的方法就是利用: replaceAll(String regex,String replacement)方法了,一行代码就可以解决。 - -#### **示例代码:** - -**常规做法:** - -```java - public String replaceSpace(StringBuffer str) { - StringBuffer out=new StringBuffer(); - for (int i = 0; i < str.toString().length(); i++) { - char b=str.charAt(i); - if(String.valueOf(b).equals(" ")){ - out.append("%20"); - }else{ - out.append(b); - } - } - return out.toString(); - } -``` - -**一行代码解决:** - -```java - public String replaceSpace(StringBuffer str) { - //return str.toString().replaceAll(" ", "%20"); - //public String replaceAll(String regex,String replacement) - //用给定的替换替换与给定的regular expression匹配的此字符串的每个子字符串。 - //\ 转义字符. 如果你要使用 "\" 本身, 则应该使用 "\\". String类型中的空格用“\s”表示,所以我这里猜测"\\s"就是代表空格的意思 - return str.toString().replaceAll("\\s", "%20"); - } - -``` - -### 六 数值的整数次方 - -#### **题目描述:** - -给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。 - -#### **问题解析:** - -这道题算是比较麻烦和难一点的一个了。我这里采用的是**二分幂**思想,当然也可以采用**快速幂**。 -更具剑指offer书中细节,该题的解题思路如下: -1.当底数为0且指数<0时,会出现对0求倒数的情况,需进行错误处理,设置一个全局变量; -2.判断底数是否等于0,由于base为double型,所以不能直接用==判断 -3.优化求幂函数(二分幂)。 -当n为偶数,a^n =(a^n/2)*(a^n/2); -当n为奇数,a^n = a^[(n-1)/2] * a^[(n-1)/2] * a。时间复杂度O(logn) - -**时间复杂度**:O(logn) - -#### **示例代码:** - -```java -public class Solution { - boolean invalidInput=false; - public double Power(double base, int exponent) { - //如果底数等于0并且指数小于0 - //由于base为double型,不能直接用==判断 - if(equal(base,0.0)&&exponent<0){ - invalidInput=true; - return 0.0; - } - int absexponent=exponent; - //如果指数小于0,将指数转正 - if(exponent<0) - absexponent=-exponent; - //getPower方法求出base的exponent次方。 - double res=getPower(base,absexponent); - //如果指数小于0,所得结果为上面求的结果的倒数 - if(exponent<0) - res=1.0/res; - return res; - } - //比较两个double型变量是否相等的方法 - boolean equal(double num1,double num2){ - if(num1-num2>-0.000001&&num1-num2<0.000001) - return true; - else - return false; - } - //求出b的e次方的方法 - double getPower(double b,int e){ - //如果指数为0,返回1 - if(e==0) - return 1.0; - //如果指数为1,返回b - if(e==1) - return b; - //e>>1相等于e/2,这里就是求a^n =(a^n/2)*(a^n/2) - double result=getPower(b,e>>1); - result*=result; - //如果指数n为奇数,则要再乘一次底数base - if((e&1)==1) - result*=b; - return result; - } -} -``` - -当然这一题也可以采用笨方法:累乘。不过这种方法的时间复杂度为O(n),这样没有前一种方法效率高。 - -```java - // 使用累乘 - public double powerAnother(double base, int exponent) { - double result = 1.0; - for (int i = 0; i < Math.abs(exponent); i++) { - result *= base; - } - if (exponent >= 0) - return result; - else - return 1 / result; - } -``` - -### 七 调整数组顺序使奇数位于偶数前面 - -#### **题目描述:** - -输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。 - -#### **问题解析:** - -这道题有挺多种解法的,给大家介绍一种我觉得挺好理解的方法: -我们首先统计奇数的个数假设为n,然后新建一个等长数组,然后通过循环判断原数组中的元素为偶数还是奇数。如果是则从数组下标0的元素开始,把该奇数添加到新数组;如果是偶数则从数组下标为n的元素开始把该偶数添加到新数组中。 - -#### **示例代码:** - -时间复杂度为O(n),空间复杂度为O(n)的算法 - -```java -public class Solution { - public void reOrderArray(int [] array) { - //如果数组长度等于0或者等于1,什么都不做直接返回 - if(array.length==0||array.length==1) - return; - //oddCount:保存奇数个数 - //oddBegin:奇数从数组头部开始添加 - int oddCount=0,oddBegin=0; - //新建一个数组 - int[] newArray=new int[array.length]; - //计算出(数组中的奇数个数)开始添加元素 - for(int i=0;i stack1 = new Stack(); - Stack stack2 = new Stack(); - - //当执行push操作时,将元素添加到stack1 - public void push(int node) { - stack1.push(node); - } - - public int pop() { - //如果两个队列都为空则抛出异常,说明用户没有push进任何元素 - if(stack1.empty()&&stack2.empty()){ - throw new RuntimeException("Queue is empty!"); - } - //如果stack2不为空直接对stack2执行pop操作, - if(stack2.empty()){ - while(!stack1.empty()){ - //将stack1的元素按后进先出push进stack2里面 - stack2.push(stack1.pop()); - } - } - return stack2.pop(); - } -} -``` - -### 十二 栈的压入,弹出序列 - -#### **题目描述:** - -输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。(注意:这两个序列的长度是相等的) - -#### **题目分析:** - -这道题想了半天没有思路,参考了Alias的答案,他的思路写的也很详细应该很容易看懂。 -作者:Alias -https://www.nowcoder.com/questionTerminal/d77d11405cc7470d82554cb392585106 -来源:牛客网 - -【思路】借用一个辅助的栈,遍历压栈顺序,先讲第一个放入栈中,这里是1,然后判断栈顶元素是不是出栈顺序的第一个元素,这里是4,很显然1≠4,所以我们继续压栈,直到相等以后开始出栈,出栈一个元素,则将出栈顺序向后移动一位,直到不相等,这样循环等压栈顺序遍历完成,如果辅助栈还不为空,说明弹出序列不是该栈的弹出顺序。 - -举例: - -入栈1,2,3,4,5 - -出栈4,5,3,2,1 - -首先1入辅助栈,此时栈顶1≠4,继续入栈2 - -此时栈顶2≠4,继续入栈3 - -此时栈顶3≠4,继续入栈4 - -此时栈顶4=4,出栈4,弹出序列向后一位,此时为5,,辅助栈里面是1,2,3 - -此时栈顶3≠5,继续入栈5 - -此时栈顶5=5,出栈5,弹出序列向后一位,此时为3,,辅助栈里面是1,2,3 - -…. -依次执行,最后辅助栈为空。如果不为空说明弹出序列不是该栈的弹出顺序。 - - - -#### **考察内容:** - -栈 - -#### **示例代码:** - -```java -import java.util.ArrayList; -import java.util.Stack; -//这道题没想出来,参考了Alias同学的答案:https://www.nowcoder.com/questionTerminal/d77d11405cc7470d82554cb392585106 -public class Solution { - public boolean IsPopOrder(int [] pushA,int [] popA) { - if(pushA.length == 0 || popA.length == 0) - return false; - Stack s = new Stack(); - //用于标识弹出序列的位置 - int popIndex = 0; - for(int i = 0; i< pushA.length;i++){ - s.push(pushA[i]); - //如果栈不为空,且栈顶元素等于弹出序列 - while(!s.empty() &&s.peek() == popA[popIndex]){ - //出栈 - s.pop(); - //弹出序列向后一位 - popIndex++; - } - } - return s.empty(); - } -} -``` \ 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/\346\225\260\346\215\256\347\273\223\346\236\204.md" "b/docs/dataStructures-algorithms/\346\225\260\346\215\256\347\273\223\346\236\204.md" deleted file mode 100644 index dfb5bc1..0000000 --- "a/docs/dataStructures-algorithms/\346\225\260\346\215\256\347\273\223\346\236\204.md" +++ /dev/null @@ -1,192 +0,0 @@ -下面只是简单地总结,给了一些参考文章,后面会对这部分内容进行重构。 - - -- [Queue](#queue) - - [什么是队列](#什么是队列) - - [队列的种类](#队列的种类) - - [Java 集合框架中的队列 Queue](#java-集合框架中的队列-queue) - - [推荐文章](#推荐文章) -- [Set](#set) - - [什么是 Set](#什么是-set) - - [补充:有序集合与无序集合说明](#补充:有序集合与无序集合说明) - - [HashSet 和 TreeSet 底层数据结构](#hashset-和-treeset-底层数据结构) - - [推荐文章](#推荐文章-1) -- [List](#list) - - [什么是List](#什么是list) - - [List的常见实现类](#list的常见实现类) - - [ArrayList 和 LinkedList 源码学习](#arraylist-和-linkedlist-源码学习) - - [推荐阅读](#推荐阅读) -- [Map](#map) -- [树](#树) - - - - -## Queue - -### 什么是队列 -队列是数据结构中比较重要的一种类型,它支持 FIFO,尾部添加、头部删除(先进队列的元素先出队列),跟我们生活中的排队类似。 - -### 队列的种类 - -- **单队列**(单队列就是常见的队列, 每次添加元素时,都是添加到队尾,存在“假溢出”的问题也就是明明有位置却不能添加的情况) -- **循环队列**(避免了“假溢出”的问题) - -### Java 集合框架中的队列 Queue - -Java 集合中的 Queue 继承自 Collection 接口 ,Deque, LinkedList, PriorityQueue, BlockingQueue 等类都实现了它。 -Queue 用来存放 等待处理元素 的集合,这种场景一般用于缓冲、并发访问。 -除了继承 Collection 接口的一些方法,Queue 还添加了额外的 添加、删除、查询操作。 - -### 推荐文章 - -- [Java 集合深入理解(9):Queue 队列](https://blog.csdn.net/u011240877/article/details/52860924) - -## Set - -### 什么是 Set -Set 继承于 Collection 接口,是一个不允许出现重复元素,并且无序的集合,主要 HashSet 和 TreeSet 两大实现类。 - -在判断重复元素的时候,HashSet 集合会调用 hashCode()和 equal()方法来实现;TreeSet 集合会调用compareTo方法来实现。 - -### 补充:有序集合与无序集合说明 -- 有序集合:集合里的元素可以根据 key 或 index 访问 (List、Map) -- 无序集合:集合里的元素只能遍历。(Set) - - -### HashSet 和 TreeSet 底层数据结构 - -**HashSet** 是哈希表结构,主要利用 HashMap 的 key 来存储元素,计算插入元素的 hashCode 来获取元素在集合中的位置; - -**TreeSet** 是红黑树结构,每一个元素都是树中的一个节点,插入的元素都会进行排序; - - -### 推荐文章 - -- [Java集合--Set(基础)](https://www.jianshu.com/p/b48c47a42916) - -## List - -### 什么是List - -在 List 中,用户可以精确控制列表中每个元素的插入位置,另外用户可以通过整数索引(列表中的位置)访问元素,并搜索列表中的元素。 与 Set 不同,List 通常允许重复的元素。 另外 List 是有序集合而 Set 是无序集合。 - -### List的常见实现类 - -**ArrayList** 是一个数组队列,相当于动态数组。它由数组实现,随机访问效率高,随机插入、随机删除效率低。 - -**LinkedList** 是一个双向链表。它也可以被当作堆栈、队列或双端队列进行操作。LinkedList随机访问效率低,但随机插入、随机删除效率高。 - -**Vector** 是矢量队列,和ArrayList一样,它也是一个动态数组,由数组实现。但是ArrayList是非线程安全的,而Vector是线程安全的。 - -**Stack** 是栈,它继承于Vector。它的特性是:先进后出(FILO, First In Last Out)。相关阅读:[java数据结构与算法之栈(Stack)设计与实现](https://blog.csdn.net/javazejian/article/details/53362993) - -### ArrayList 和 LinkedList 源码学习 - -- [ArrayList 源码学习](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/ArrayList.md) -- [LinkedList 源码学习](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/LinkedList.md) - -### 推荐阅读 - -- [java 数据结构与算法之顺序表与链表深入分析](https://blog.csdn.net/javazejian/article/details/52953190) - - -## Map - - -- [集合框架源码学习之 HashMap(JDK1.8)](https://juejin.im/post/5ab0568b5188255580020e56) -- [ConcurrentHashMap 实现原理及源码分析](https://link.juejin.im/?target=http%3A%2F%2Fwww.cnblogs.com%2Fchengxiao%2Fp%2F6842045.html) - -## 树 - * ### 1 二叉树 - - [二叉树](https://baike.baidu.com/item/%E4%BA%8C%E5%8F%89%E6%A0%91)(百度百科) - - (1)[完全二叉树](https://baike.baidu.com/item/%E5%AE%8C%E5%85%A8%E4%BA%8C%E5%8F%89%E6%A0%91)——若设二叉树的高度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第h层有叶子结点,并且叶子结点都是从左到右依次排布,这就是完全二叉树。 - - (2)[满二叉树](https://baike.baidu.com/item/%E6%BB%A1%E4%BA%8C%E5%8F%89%E6%A0%91)——除了叶结点外每一个结点都有左右子叶且叶子结点都处在最底层的二叉树。 - - (3)[平衡二叉树](https://baike.baidu.com/item/%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%A0%91/10421057)——平衡二叉树又被称为AVL树(区别于AVL算法),它是一棵二叉排序树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。 - - * ### 2 完全二叉树 - - [完全二叉树](https://baike.baidu.com/item/%E5%AE%8C%E5%85%A8%E4%BA%8C%E5%8F%89%E6%A0%91)(百度百科) - - 完全二叉树:叶节点只能出现在最下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树。 - * ### 3 满二叉树 - - [满二叉树](https://baike.baidu.com/item/%E6%BB%A1%E4%BA%8C%E5%8F%89%E6%A0%91)(百度百科,国内外的定义不同) - - 国内教程定义:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。 - * ### 堆 - - [数据结构之堆的定义](https://blog.csdn.net/qq_33186366/article/details/51876191) - - 堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。 - * ### 4 二叉查找树(BST) - - [浅谈算法和数据结构: 七 二叉查找树](http://www.cnblogs.com/yangecnu/p/Introduce-Binary-Search-Tree.html) - - 二叉查找树的特点: - - 1. 若任意节点的左子树不空,则左子树上所有结点的 值均小于它的根结点的值; - 2. 若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值; - 3. 任意节点的左、右子树也分别为二叉查找树; - 4. 没有键值相等的节点(no duplicate nodes)。 - - * ### 5 平衡二叉树(Self-balancing binary search tree) - - [ 平衡二叉树](https://baike.baidu.com/item/%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%A0%91)(百度百科,平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等) - * ### 6 红黑树 - - - 红黑树特点: - 1. 每个节点非红即黑; - 2. 根节点总是黑色的; - 3. 每个叶子节点都是黑色的空节点(NIL节点); - 4. 如果节点是红色的,则它的子节点必须是黑色的(反之不一定); - 5. 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。 - - - 红黑树的应用: - - TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。 - - - 为什么要用红黑树 - - 简单来说红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。详细了解可以查看 [漫画:什么是红黑树?](https://juejin.im/post/5a27c6946fb9a04509096248#comment)(也介绍到了二叉查找树,非常推荐) - - - 推荐文章: - - [漫画:什么是红黑树?](https://juejin.im/post/5a27c6946fb9a04509096248#comment)(也介绍到了二叉查找树,非常推荐) - - [寻找红黑树的操作手册](http://dandanlove.com/2018/03/18/red-black-tree/)(文章排版以及思路真的不错) - - [红黑树深入剖析及Java实现](https://zhuanlan.zhihu.com/p/24367771)(美团点评技术团队) - * ### 7 B-,B+,B*树 - - [二叉树学习笔记之B树、B+树、B*树 ](https://yq.aliyun.com/articles/38345) - - [《B-树,B+树,B*树详解》](https://blog.csdn.net/aqzwss/article/details/53074186) - - [《B-树,B+树与B*树的优缺点比较》](https://blog.csdn.net/bigtree_3721/article/details/73632405) - - B-树(或B树)是一种平衡的多路查找(又称排序)树,在文件系统中有所应用。主要用作文件的索引。其中的B就表示平衡(Balance) - 1. B+ 树的叶子节点链表结构相比于 B- 树便于扫库,和范围检索。 - 2. B+树支持range-query(区间查询)非常方便,而B树不支持。这是数据库选用B+树的最主要原因。 - 3. B\*树 是B+树的变体,B\*树分配新结点的概率比B+树要低,空间使用率更高; - * ### 8 LSM 树 - - [[HBase] LSM树 VS B+树](https://blog.csdn.net/dbanote/article/details/8897599) - - B+树最大的性能问题是会产生大量的随机IO - - 为了克服B+树的弱点,HBase引入了LSM树的概念,即Log-Structured Merge-Trees。 - - [LSM树由来、设计思想以及应用到HBase的索引](http://www.cnblogs.com/yanghuahui/p/3483754.html) - - -## 图 - - - - -## BFS及DFS - -- [《使用BFS及DFS遍历树和图的思路及实现》](https://blog.csdn.net/Gene1994/article/details/85097507) - 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/\347\256\227\346\263\225\345\255\246\344\271\240\350\265\204\346\272\220\346\216\250\350\215\220.md" "b/docs/dataStructures-algorithms/\347\256\227\346\263\225\345\255\246\344\271\240\350\265\204\346\272\220\346\216\250\350\215\220.md" deleted file mode 100644 index 4c5df56..0000000 --- "a/docs/dataStructures-algorithms/\347\256\227\346\263\225\345\255\246\344\271\240\350\265\204\346\272\220\346\216\250\350\215\220.md" +++ /dev/null @@ -1,52 +0,0 @@ -我比较推荐大家可以刷一下 Leetcode ,我自己平时没事也会刷一下,我觉得刷 Leetcode 不仅是为了能让你更从容地面对面试中的手撕算法问题,更可以提高你的编程思维能力、解决问题的能力以及你对某门编程语言 API 的熟练度。当然牛客网也有一些算法题,我下面也整理了一些。 - -## LeetCode - -- [LeetCode(中国)官网](https://leetcode-cn.com/) - -- [如何高效地使用 LeetCode](https://leetcode-cn.com/articles/%E5%A6%82%E4%BD%95%E9%AB%98%E6%95%88%E5%9C%B0%E4%BD%BF%E7%94%A8-leetcode/) - - -## 牛客网 - -- [牛客网官网](https://www.nowcoder.com) -- [剑指offer编程题](https://www.nowcoder.com/ta/coding-interviews) - -- [2017校招真题](https://www.nowcoder.com/ta/2017test) -- [华为机试题](https://www.nowcoder.com/ta/huawei) - - -## 公司真题 - -- [ 网易2018校园招聘编程题真题集合](https://www.nowcoder.com/test/6910869/summary) -- [ 网易2018校招内推编程题集合](https://www.nowcoder.com/test/6291726/summary) -- [2017年校招全国统一模拟笔试(第五场)编程题集合](https://www.nowcoder.com/test/5986669/summary) -- [2017年校招全国统一模拟笔试(第四场)编程题集合](https://www.nowcoder.com/test/5507925/summary) -- [2017年校招全国统一模拟笔试(第三场)编程题集合](https://www.nowcoder.com/test/5217106/summary) -- [2017年校招全国统一模拟笔试(第二场)编程题集合](https://www.nowcoder.com/test/4546329/summary) -- [ 2017年校招全国统一模拟笔试(第一场)编程题集合](https://www.nowcoder.com/test/4236887/summary) -- [百度2017春招笔试真题编程题集合](https://www.nowcoder.com/test/4998655/summary) -- [网易2017春招笔试真题编程题集合](https://www.nowcoder.com/test/4575457/summary) -- [网易2017秋招编程题集合](https://www.nowcoder.com/test/2811407/summary) -- [网易有道2017内推编程题](https://www.nowcoder.com/test/2385858/summary) -- [ 滴滴出行2017秋招笔试真题-编程题汇总](https://www.nowcoder.com/test/3701760/summary) -- [腾讯2017暑期实习生编程题](https://www.nowcoder.com/test/1725829/summary) -- [今日头条2017客户端工程师实习生笔试题](https://www.nowcoder.com/test/1649301/summary) -- [今日头条2017后端工程师实习生笔试题](https://www.nowcoder.com/test/1649268/summary) - - - - - - - - - - - - - - - - - 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/essential-content-for-interview/BATJrealInterviewExperience/2019alipay-pinduoduo-toutiao.md b/docs/essential-content-for-interview/BATJrealInterviewExperience/2019alipay-pinduoduo-toutiao.md deleted file mode 100644 index 183a185..0000000 --- a/docs/essential-content-for-interview/BATJrealInterviewExperience/2019alipay-pinduoduo-toutiao.md +++ /dev/null @@ -1,294 +0,0 @@ -作者: rhwayfun,原文地址:https://mp.weixin.qq.com/s/msYty4vjjC0PvrwasRH5Bw ,JavaGuide 已经获得作者授权并对原文进行了重新排版。 - - -- [写在2019年后的蚂蚁、头条、拼多多的面试总结](#写在2019年后的蚂蚁头条拼多多的面试总结) - - [准备过程](#准备过程) - - [蚂蚁金服](#蚂蚁金服) - - [一面](#一面) - - [二面](#二面) - - [三面](#三面) - - [四面](#四面) - - [五面](#五面) - - [小结](#小结) - - [拼多多](#拼多多) - - [面试前](#面试前) - - [一面](#一面-1) - - [二面](#二面-1) - - [三面](#三面-1) - - [小结](#小结-1) - - [字节跳动](#字节跳动) - - [面试前](#面试前-1) - - [一面](#一面-2) - - [二面](#二面-2) - - [小结](#小结-2) - - [总结](#总结) - - - -# 2019年蚂蚁金服、头条、拼多多的面试总结 - -文章有点长,请耐心看完,绝对有收获!不想听我BB直接进入面试分享: - -- 准备过程 -- 蚂蚁金服面试分享 -- 拼多多面试分享 -- 字节跳动面试分享 -- 总结 - -说起来开始进行面试是年前倒数第二周,上午9点,我还在去公司的公交上,突然收到蚂蚁的面试电话,其实算不上真正的面试。面试官只是和我聊了下他们在做的事情(主要是做双十一这里大促的稳定性保障,偏中间件吧),说的很详细,然后和我沟通了下是否有兴趣,我表示有兴趣,后面就收到正式面试的通知,最后没选择去蚂蚁表示抱歉。 - -当时我自己也准备出去看看机会,顺便看看自己的实力。当时我其实挺纠结的,一方面现在部门也正需要我,还是可以有一番作为的,另一方面觉得近一年来进步缓慢,没有以前飞速进步的成就感了,而且业务和技术偏于稳定,加上自己也属于那种比较懒散的人,骨子里还是希望能够突破现状,持续在技术上有所精进。 - -在开始正式的总结之前,还是希望各位同仁能否听我继续发泄一会,抱拳! - -我翻开自己2018年初立的flag,觉得甚是惭愧。其中就有一条是保持一周写一篇博客,奈何中间因为各种原因没能坚持下去。细细想来,主要是自己没能真正静下来心认真投入到技术的研究和学习,那么为什么会这样?说白了还是因为没有确定目标或者目标不明确,没有目标或者目标不明确都可能导致行动的失败。 - -那么问题来了,目标是啥?就我而言,短期目标是深入研究某一项技术,比如最近在研究mysql,那么深入研究一定要动手实践并且有所产出,这就够了么?还需要我们能够举一反三,结合实际开发场景想一想日常开发要注意什么,这中间有没有什么坑?可以看出,要进步真的不是一件简单的事,这种反人类的行为需要我们克服自我的弱点,逐渐形成习惯。真正牛逼的人,从不觉得认真学习是一件多么难的事,因为这已经形成了他的习惯,就喝早上起床刷牙洗脸那么自然简单。 - -扯了那么多,开始进入正题,先后进行了蚂蚁、拼多多和字节跳动的面试。 - -## 准备过程 - -先说说我自己的情况,我2016先在蚂蚁实习了将近三个月,然后去了我现在的老东家,2.5年工作经验,可以说毕业后就一直老老实实在老东家打怪升级,虽说有蚂蚁的实习经历,但是因为时间太短,还是有点虚的。所以面试官看到我简历第一个问题绝对是这样的。 - -“哇,你在蚂蚁待过,不错啊”,面试官笑嘻嘻地问到。“是的,还好”,我说。“为啥才三个月?”,面试官脸色一沉问到。“哗啦啦解释一通。。。”,我解释道。“哦,原来如此,那我们开始面试吧”,面试官一本正经说到。 - -尼玛,早知道不写蚂蚁的实习经历了,后面仔细一想,当初写上蚂蚁不就给简历加点料嘛。 - -言归正传,准备过程其实很早开始了(当然这不是说我工作时老想着跳槽,因为我明白现在的老东家并不是终点,我还需要不断提升),具体可追溯到从蚂蚁离职的时候,当时出来也面了很多公司,没啥大公司,面了大概5家公司,都拿到offer了。 - -工作之余常常会去额外研究自己感兴趣的技术以及工作用到的技术,力求把原理搞明白,并且会自己实践一把。此外,买了N多书,基本有时间就会去看,补补基础,什么操作系统、数据结构与算法、MySQL、JDK之类的源码,基本都好好温习了(文末会列一下自己看过的书和一些好的资料)。**我深知基础就像“木桶效应”的短板,决定了能装多少水。** - -此外,在正式决定看机会之前,我给自己列了一个提纲,主要包括Java要掌握的核心要点,有不懂的就查资料搞懂。我给自己定位还是Java工程师,所以Java体系是一定要做到心中有数的,很多东西没有常年的积累面试的时候很容易露馅,学习要对得起自己,不要骗人。 - -剩下的就是找平台和内推了,除了蚂蚁,头条和拼多多都是找人内推的,感谢蚂蚁面试官对我的欣赏,以后说不定会去蚂蚁咯😄。 - -平台:脉脉、GitHub、v2 - -## 蚂蚁金服 - -![img](https://mmbiz.qpic.cn/mmbiz_jpg/zsXjkGNcic53JMPc0FUw1lBXl5iaibrEXvt9qal7lJSgfGJ8mq00yE1J4UQ9H1oo9t6RAL4T3whhx17TYlj1mjlXA/?wx_fmt=jpeg) - -- 一面 -- 二面 -- 三面 -- 四面 -- 五面 -- 小结 - -### 一面 - -一面就做了一道算法题,要求两小时内完成,给了长度为N的有重复元素的数组,要求输出第10大的数。典型的TopK问题,快排算法搞定。 - -算法题要注意的是合法性校验、边界条件以及异常的处理。另外,如果要写测试用例,一定要保证测试覆盖场景尽可能全。加上平时刷刷算法题,这种考核应该没问题的。 - -### 二面 - -- 自我介绍下呗 -- 开源项目贡献过代码么?(Dubbo提过一个打印accesslog的bug算么) -- 目前在部门做什么,业务简单介绍下,内部有哪些系统,作用和交互过程说下 -- Dubbo踩过哪些坑,分别是怎么解决的?(说了异常处理时业务异常捕获的问题,自定义了一个异常拦截器) -- 开始进入正题,说下你对线程安全的理解(多线程访问同一个对象,如果不需要考虑额外的同步,调用对象的行为就可以获得正确的结果就是线程安全) -- 事务有哪些特性?(ACID) -- 怎么理解原子性?(同一个事务下,多个操作要么成功要么失败,不存在部分成功或者部分失败的情况) -- 乐观锁和悲观锁的区别?(悲观锁假定会发生冲突,访问的时候都要先获得锁,保证同一个时刻只有线程获得锁,读读也会阻塞;乐观锁假设不会发生冲突,只有在提交操作的时候检查是否有冲突)这两种锁在Java和MySQL分别是怎么实现的?(Java乐观锁通过CAS实现,悲观锁通过synchronize实现。mysql乐观锁通过MVCC,也就是版本实现,悲观锁可以通过select... for update加上排它锁) -- HashMap为什么不是线程安全的?(多线程操作无并发控制,顺便说了在扩容的时候多线程访问时会造成死锁,会形成一个环,不过扩容时多线程操作形成环的问题再JDK1.8已经解决,但多线程下使用HashMap还会有一些其他问题比如数据丢失,所以多线程下不应该使用HashMap,而应该使用ConcurrentHashMap)怎么让HashMap变得线程安全?(Collections的synchronize方法包装一个线程安全的Map,或者直接用ConcurrentHashMap)两者的区别是什么?(前者直接在put和get方法加了synchronize同步,后者采用了分段锁以及CAS支持更高的并发) -- jdk1.8对ConcurrentHashMap做了哪些优化?(插入的时候如果数组元素使用了红黑树,取消了分段锁设计,synchronize替代了Lock锁)为什么这样优化?(避免冲突严重时链表多长,提高查询效率,时间复杂度从O(N)提高到O(logN)) -- redis主从机制了解么?怎么实现的? -- 有过GC调优的经历么?(有点虚,答得不是很好) -- 有什么想问的么? - -### 三面 - -- 简单自我介绍下 -- 监控系统怎么做的,分为哪些模块,模块之间怎么交互的?用的什么数据库?(MySQL)使用什么存储引擎,为什么使用InnnoDB?(支持事务、聚簇索引、MVCC) -- 订单表有做拆分么,怎么拆的?(垂直拆分和水平拆分) -- 水平拆分后查询过程描述下 -- 如果落到某个分片的数据很大怎么办?(按照某种规则,比如哈希取模、range,将单张表拆分为多张表) -- 哈希取模会有什么问题么?(有的,数据分布不均,扩容缩容相对复杂 ) -- 分库分表后怎么解决读写压力?(一主多从、多主多从) -- 拆分后主键怎么保证惟一?(UUID、Snowflake算法) -- Snowflake生成的ID是全局递增唯一么?(不是,只是全局唯一,单机递增) -- 怎么实现全局递增的唯一ID?(讲了TDDL的一次取一批ID,然后再本地慢慢分配的做法) -- Mysql的索引结构说下(说了B+树,B+树可以对叶子结点顺序查找,因为叶子结点存放了数据结点且有序) -- 主键索引和普通索引的区别(主键索引的叶子结点存放了整行记录,普通索引的叶子结点存放了主键ID,查询的时候需要做一次回表查询)一定要回表查询么?(不一定,当查询的字段刚好是索引的字段或者索引的一部分,就可以不用回表,这也是索引覆盖的原理) -- 你们系统目前的瓶颈在哪里? -- 你打算怎么优化?简要说下你的优化思路 -- 有什么想问我么? - -### 四面 - -- 介绍下自己 -- 为什么要做逆向? -- 怎么理解微服务? -- 服务治理怎么实现的?(说了限流、压测、监控等模块的实现) -- 这个不是中间件做的事么,为什么你们部门做?(当时没有单独的中间件团队,微服务刚搞不久,需要进行监控和性能优化) -- 说说Spring的生命周期吧 -- 说说GC的过程(说了young gc和full gc的触发条件和回收过程以及对象创建的过程) -- CMS GC有什么问题?(并发清除算法,浮动垃圾,短暂停顿) -- 怎么避免产生浮动垃圾?(记得有个VM参数设置可以让扫描新生代之前进行一次young gc,但是因为gc是虚拟机自动调度的,所以不保证一定执行。但是还有参数可以让虚拟机强制执行一次young gc) -- 强制young gc会有什么问题?(STW停顿时间变长) -- 知道G1么?(了解一点 ) -- 回收过程是怎么样的?(young gc、并发阶段、混合阶段、full gc,说了Remember Set) -- 你提到的Remember Set底层是怎么实现的? -- 有什么想问的么? - -### 五面 - -五面是HRBP面的,和我提前预约了时间,主要聊了之前在蚂蚁的实习经历、部门在做的事情、职业发展、福利待遇等。阿里面试官确实是具有一票否决权的,很看重你的价值观是否match,一般都比较喜欢皮实的候选人。HR面一定要诚实,不要说谎,只要你说谎HR都会去证实,直接cut了。 - -- 之前蚂蚁实习三个月怎么不留下来? -- 实习的时候主管是谁? -- 实习做了哪些事情?(尼玛这种也问?) -- 你对技术怎么看?平时使用什么技术栈?(阿里HR真的是既当爹又当妈,😂) -- 最近有在研究什么东西么 -- 你对SRE怎么看 -- 对待遇有什么预期么 - -最后HR还对我说目前稳定性保障部挺缺人的,希望我尽快回复。 - -### 小结 - -蚂蚁面试比较重视基础,所以Java那些基本功一定要扎实。蚂蚁的工作环境还是挺赞的,因为我面的是稳定性保障部门,还有许多单独的小组,什么三年1班,很有青春的感觉。面试官基本水平都比较高,基本都P7以上,除了基础还问了不少架构设计方面的问题,收获还是挺大的。 - -## 拼多多 - -![img](https://mmbiz.qpic.cn/mmbiz_jpg/zsXjkGNcic53JMPc0FUw1lBXl5iaibrEXvtsmoh9TdJcV0hwnrjtbWPdOacyj2uYe2qaI5jvlGIQHwYtknwnGTibbQ/?wx_fmt=jpeg) - -- 面试前 -- 一面 -- 二面 -- 三面 -- 小结 - -### 面试前 - -面完蚂蚁后,早就听闻拼多多这个独角兽,决定也去面一把。首先我在脉脉找了一个拼多多的HR,加了微信聊了下,发了简历便开始我的拼多多面试之旅。这里要非常感谢拼多多HR小姐姐,从面试内推到offer确认一直都在帮我,人真的很nice。 - -### 一面 - -- 为啥蚂蚁只待了三个月?没转正?(转正了,解释了一通。。。) -- Java中的HashMap、TreeMap解释下?(TreeMap红黑树,有序,HashMap无序,数组+链表) -- TreeMap查询写入的时间复杂度多少?(O(logN)) -- HashMap多线程有什么问题?(线程安全,死锁)怎么解决?( jdk1.8用了synchronize + CAS,扩容的时候通过CAS检查是否有修改,是则重试)重试会有什么问题么?(CAS(Compare And Swap)是比较和交换,不会导致线程阻塞,但是因为重试是通过自旋实现的,所以仍然会占用CPU时间,还有ABA的问题)怎么解决?(超时,限定自旋的次数,ABA可以通过原理变量AtomicStampedReference解决,原理利用版本号进行比较)超过重试次数如果仍然失败怎么办?(synchronize互斥锁) -- CAS和synchronize有什么区别?都用synchronize不行么?(CAS是乐观锁,不需要阻塞,硬件级别实现的原子性;synchronize会阻塞,JVM级别实现的原子性。使用场景不同,线程冲突严重时CAS会造成CPU压力过大,导致吞吐量下降,synchronize的原理是先自旋然后阻塞,线程冲突严重仍然有较高的吞吐量,因为线程都被阻塞了,不会占用CPU -) -- 如果要保证线程安全怎么办?(ConcurrentHashMap) -- ConcurrentHashMap怎么实现线程安全的?(分段锁) -- get需要加锁么,为什么?(不用,volatile关键字) -- volatile的作用是什么?(保证内存可见性) -- 底层怎么实现的?(说了主内存和工作内存,读写内存屏障,happen-before,并在纸上画了线程交互图) -- 在多核CPU下,可见性怎么保证?(思考了一会,总线嗅探技术) -- 聊项目,系统之间是怎么交互的? -- 系统并发多少,怎么优化? -- 给我一张纸,画了一个九方格,都填了数字,给一个M*N矩阵,从1开始逆时针打印这M*N个数,要求时间复杂度尽可能低(内心OS:之前貌似碰到过这题,最优解是怎么实现来着)思考中。。。 -- 可以先说下你的思路(想起来了,说了什么时候要变换方向的条件,向右、向下、向左、向上,依此循环) -- 有什么想问我的? - -### 二面 - -- 自我介绍下 -- 手上还有其他offer么?(拿了蚂蚁的offer) -- 部门组织结构是怎样的?(这轮不是技术面么,不过还是老老实实说了) -- 系统有哪些模块,每个模块用了哪些技术,数据怎么流转的?(面试官有点秃顶,一看级别就很高)给了我一张纸,我在上面简单画了下系统之间的流转情况 -- 链路追踪的信息是怎么传递的?(RpcContext的attachment,说了Span的结构:parentSpanId + curSpanId) -- SpanId怎么保证唯一性?(UUID,说了下内部的定制改动) -- RpcContext是在什么维度传递的?(线程) -- Dubbo的远程调用怎么实现的?(讲了读取配置、拼装url、创建Invoker、服务导出、服务注册以及消费者通过动态代理、filter、获取Invoker列表、负载均衡等过程(哗啦啦讲了10多分钟),我可以喝口水么) -- Spring的单例是怎么实现的?(单例注册表) -- 为什么要单独实现一个服务治理框架?(说了下内部刚搞微服务不久,主要对服务进行一些监控和性能优化) -- 谁主导的?内部还在使用么? -- 逆向有想过怎么做成通用么? -- 有什么想问的么? - -### 三面 - -二面老大面完后就直接HR面了,主要问了些职业发展、是否有其他offer、以及入职意向等问题,顺便说了下公司的福利待遇等,都比较常规啦。不过要说的是手上有其他offer或者大厂经历会有一定加分。 - -### 小结 - -拼多多的面试流程就简单许多,毕竟是一个成立三年多的公司。面试难度中规中矩,只要基础扎实应该不是问题。但不得不说工作强度很大,开始面试前HR就提前和我确认能否接受这样强度的工作,想来的老铁还是要做好准备 - -## 字节跳动 - -![img](https://mmbiz.qpic.cn/mmbiz_jpg/zsXjkGNcic53JMPc0FUw1lBXl5iaibrEXvtRoTSCMeUWramk7M4CekxE9ssH5DFGBxmDcw0x9hjzmbIGHVWenDK8w/?wx_fmt=jpeg) - -- 面试前 -- 一面 -- 二面 -- 小结 - -### 面试前 - -头条的面试是三家里最专业的,每次面试前有专门的HR和你约时间,确定OK后再进行面试。每次都是通过视频面试,因为都是之前都是电话面或现场面,所以视频面试还是有点不自然。也有人觉得视频面试体验很赞,当然萝卜青菜各有所爱。最坑的二面的时候对方面试官的网络老是掉线,最后很冤枉的挂了(当然有一些点答得不好也是原因之一)。所以还是有点遗憾的。 - -### 一面 - -- 先自我介绍下 -- 聊项目,逆向系统是什么意思 -- 聊项目,逆向系统用了哪些技术 -- 线程池的线程数怎么确定? -- 如果是IO操作为主怎么确定? -- 如果计算型操作又怎么确定? -- Redis熟悉么,了解哪些数据结构?(说了zset) zset底层怎么实现的?(跳表) -- 跳表的查询过程是怎么样的,查询和插入的时间复杂度?(说了先从第一层查找,不满足就下沉到第二层找,因为每一层都是有序的,写入和插入的时间复杂度都是O(logN)) -- 红黑树了解么,时间复杂度?(说了是N叉平衡树,O(logN)) -- 既然两个数据结构时间复杂度都是O(logN),zset为什么不用红黑树(跳表实现简单,踩坑成本低,红黑树每次插入都要通过旋转以维持平衡,实现复杂) -- 点了点头,说下Dubbo的原理?(说了服务注册与发布以及消费者调用的过程)踩过什么坑没有?(说了dubbo异常处理的和打印accesslog的问题) -- CAS了解么?(说了CAS的实现)还了解其他同步机制么?(说了synchronize以及两者的区别,一个乐观锁,一个悲观锁) -- 那我们做一道题吧,数组A,2*n个元素,n个奇数、n个偶数,设计一个算法,使得数组奇数下标位置放置的都是奇数,偶数下标位置放置的都是偶数 -- 先说下你的思路(从0下标开始遍历,如果是奇数下标判断该元素是否奇数,是则跳过,否则从该位置寻找下一个奇数) -- 下一个奇数?怎么找?(有点懵逼,思考中。。) -- 有思路么?(仍然是先遍历一次数组,并对下标进行判断,如果下标属性和该位置元素不匹配从当前下标的下一个遍历数组元素,然后替换) -- 你这样时间复杂度有点高,如果要求O(N)要怎么做(思考一会,答道“定义两个指针,分别从下标0和1开始遍历,遇见奇数位是是偶数和偶数位是奇数就停下,交换内容”) -- 时间差不多了,先到这吧。你有什么想问我的? - -### 二面 - -- 面试官和蔼很多,你先介绍下自己吧 -- 你对服务治理怎么理解的? -- 项目中的限流怎么实现的?(Guava ratelimiter,令牌桶算法) -- 具体怎么实现的?(要点是固定速率且令牌数有限) -- 如果突然很多线程同时请求令牌,有什么问题?(导致很多请求积压,线程阻塞) -- 怎么解决呢?(可以把积压的请求放到消息队列,然后异步处理) -- 如果不用消息队列怎么解决?(说了RateLimiter预消费的策略) -- 分布式追踪的上下文是怎么存储和传递的?(ThreadLocal + spanId,当前节点的spanId作为下个节点的父spanId) -- Dubbo的RpcContext是怎么传递的?(ThreadLocal)主线程的ThreadLocal怎么传递到线程池?(说了先在主线程通过ThreadLocal的get方法拿到上下文信息,在线程池创建新的ThreadLocal并把之前获取的上下文信息设置到ThreadLocal中。这里要注意的线程池创建的ThreadLocal要在finally中手动remove,不然会有内存泄漏的问题) -- 你说的内存泄漏具体是怎么产生的?(说了ThreadLocal的结构,主要分两种场景:主线程仍然对ThreadLocal有引用和主线程不存在对ThreadLocal的引用。第一种场景因为主线程仍然在运行,所以还是有对ThreadLocal的引用,那么ThreadLocal变量的引用和value是不会被回收的。第二种场景虽然主线程不存在对ThreadLocal的引用,且该引用是弱引用,所以会在gc的时候被回收,但是对用的value不是弱引用,不会被内存回收,仍然会造成内存泄漏) -- 线程池的线程是不是必须手动remove才可以回收value?(是的,因为线程池的核心线程是一直存在的,如果不清理,那么核心线程的threadLocals变量会一直持有ThreadLocal变量) -- 那你说的内存泄漏是指主线程还是线程池?(主线程 ) -- 可是主线程不是都退出了,引用的对象不应该会主动回收么?(面试官和内存泄漏杠上了),沉默了一会。。。 -- 那你说下SpringMVC不同用户登录的信息怎么保证线程安全的?(刚才解释的有点懵逼,一下没反应过来,居然回答成锁了。大脑有点晕了,此时已经一个小时过去了,感觉情况不妙。。。) -- 这个直接用ThreadLocal不就可以么,你见过SpringMVC有锁实现的代码么?(有点晕菜。。。) -- 我们聊聊mysql吧,说下索引结构(说了B+树) -- 为什么使用B+树?( 说了查询效率高,O(logN),可以充分利用磁盘预读的特性,多叉树,深度小,叶子结点有序且存储数据) -- 什么是索引覆盖?(忘记了。。。 ) -- Java为什么要设计双亲委派模型? -- 什么时候需要自定义类加载器? -- 我们做一道题吧,手写一个对象池 -- 有什么想问我的么?(感觉我很多点都没答好,是不是挂了(结果真的是) ) - -### 小结 - -头条的面试确实很专业,每次面试官会提前给你发一个视频链接,然后准点开始面试,而且考察的点都比较全。 - -面试官都有一个特点,会抓住一个值得深入的点或者你没说清楚的点深入下去直到你把这个点讲清楚,不然面试官会觉得你并没有真正理解。二面面试官给了我一点建议,研究技术的时候一定要去研究产生的背景,弄明白在什么场景解决什么特定的问题,其实很多技术内部都是相通的。很诚恳,还是很感谢这位面试官大大。 - -## 总结 - -从年前开始面试到头条面完大概一个多月的时间,真的有点身心俱疲的感觉。最后拿到了拼多多、蚂蚁的offer,还是蛮幸运的。头条的面试对我帮助很大,再次感谢面试官对我的诚恳建议,以及拼多多的HR对我的啰嗦的问题详细解答。 - -这里要说的是面试前要做好两件事:简历和自我介绍,简历要好好回顾下自己做的一些项目,然后挑几个亮点项目。自我介绍基本每轮面试都有,所以最好提前自己练习下,想好要讲哪些东西,分别怎么讲。此外,简历提到的技术一定是自己深入研究过的,没有深入研究也最好找点资料预热下,不打无准备的仗。 - -**这些年看过的书**: - -《Effective Java》、《现代操作系统》、《TCP/IP详解:卷一》、《代码整洁之道》、《重构》、《Java程序性能优化》、《Spring实战》、《Zookeeper》、《高性能MySQL》、《亿级网站架构核心技术》、《可伸缩服务架构》、《Java编程思想》 - -说实话这些书很多只看了一部分,我通常会带着问题看书,不然看着看着就睡着了,简直是催眠良药😅。 - - -最后,附一张自己面试前准备的脑图: - -链接:https://pan.baidu.com/s/1o2l1tuRakBEP0InKEh4Hzw 密码:300d - -全文完。 diff --git "a/docs/essential-content-for-interview/BATJrealInterviewExperience/5\351\235\242\351\230\277\351\207\214,\347\273\210\350\216\267offer.md" "b/docs/essential-content-for-interview/BATJrealInterviewExperience/5\351\235\242\351\230\277\351\207\214,\347\273\210\350\216\267offer.md" deleted file mode 100644 index 9efac14..0000000 --- "a/docs/essential-content-for-interview/BATJrealInterviewExperience/5\351\235\242\351\230\277\351\207\214,\347\273\210\350\216\267offer.md" +++ /dev/null @@ -1,96 +0,0 @@ -> 作者:ppxyn。本文来自读者投稿,同时也欢迎各位投稿,**对于不错的原创文章我根据你的选择给予现金(50-200)、付费专栏或者任选书籍进行奖励!所以,快提 pr 或者邮件的方式(邮件地址在主页)给我投稿吧!** 当然,我觉得奖励是次要的,最重要的是你可以从自己整理知识点的过程中学习到很多知识。 - -**目录** - - - -- [前言](#前言) -- [一面\(技术面\)](#一面技术面) -- [二面\(技术面\)](#二面技术面) -- [三面\(技术面\)](#三面技术面) -- [四面\(半个技术面\)](#四面半个技术面) -- [五面\(HR面\)](#五面hr面) -- [总结](#总结) - - - -### 前言 - -在接触 Java 之前我接触的比较多的是硬件方面,用的比较多的语言就是C和C++。到了大三我才正式选择 Java 方向,到目前为止使用Java到现在大概有一年多的时间,所以Java算不上很好。刚开始投递的时候,实习刚辞职,也没准备笔试面试,很多东西都忘记了。所以,刚开始我并没有直接就投递阿里,毕竟心里还是有一点点小害怕的。于是,我就先投递了几个不算大的公司来练手,就是想着刷刷经验而已或者说是练练手(ps:还是挺对不起那些公司的)。面了一个月其他公司后,我找了我实验室的学长内推我,后面就有了这5次面试。 - -下面简单的说一下我的这5次面试:4次技术面+1次HR面,希望我的经历能对你有所帮助。 - -### 一面(技术面) - -1. 自我介绍(主要讲自己会的技术细节,项目经验,经历那些就一语带过,后面面试官会问你的)。 -2. 聊聊项目(就是一个很普通的分布式商城,自己做了一些改进),让我画了整个项目的架构图,然后针对项目抛了一系列的提高性能的问题,还问了我做项目的过程中遇到了那些问题,如何解决的,差不读就这些吧。 -3. 可能是我前面说了我会数据库优化,然后面试官就开始问索引、事务隔离级别、悲观锁和乐观锁、索引、ACID、MVVC这些问题。 -4. 浏览器输入URL发生了什么? TCP和UDP区别? TCP如何保证传输可靠性? -5. 讲下跳表怎么实现的?哈夫曼编码是怎么回事?非递归且不用额外空间(不用栈),如何遍历二叉树 -6. 后面又问了很多JVM方面的问题,比如Java内存模型、常见的垃圾回收器、双亲委派模型这些 -7. 你有什么问题要问吗? - -### 二面(技术面) - -1. 自我介绍(主要讲自己会的技术细节,项目经验,经历那些就一语带过,后面面试官会问你的)。 -2. 操作系统的内存管理机制 -3. 进程和线程的区别 -4. 说下你对线程安全的理解 -5. volatile 有什么作用 ,sychronized和lock有什么区别 -6. ReentrantLock实现原理 -7. 用过CountDownLatch么?什么场景下用的? -8. AQS底层原理。 -9. 造成死锁的原因有哪些,如何预防? -10. 加锁会带来哪些性能问题。如何解决? -11. HashMap、ConcurrentHashMap源码。HashMap是线程安全的吗?Hashtable呢?ConcurrentHashMap有了解吗? -12. 是否可以实习? -13. 你有什么问题要问吗? - -### 三面(技术面) - -1. 有没有参加过 ACM 或者他竞赛,有没有拿过什么奖?( 我说我没参加过ACM,本科参加过数学建模竞赛,名次并不好,没拿过什么奖。面试官好像有点失望,然后我又赶紧补充说我和老师一起做过一个项目,目前已经投入使用。面试官还比较感兴趣,后面又和他聊了一下这个项目。) -2. 研究生期间,做过什么项目,发过论文吗?有什么成果吗? -3. 你觉得你有什么优点和缺点?你觉得你相比于那些比你更优秀的人欠缺什么? -4. 有读过什么源码吗?(我说我读过 Java 集合框架和 Netty 的,面试官说 Java 集合前几面一定问的差不多,就不问了,然后就问我 Netty的,我当时很慌啊!) -5. 介绍一下自己对 Netty 的认识,为什么要用。说说业务中,Netty 的使用场景。什么是TCP 粘包/拆包,解决办法。Netty线程模型。Dubbo 在使用 Netty 作为网络通讯时候是如何避免粘包与半包问题?讲讲Netty的零拷贝?巴拉巴拉问了好多,我记得有好几个我都没回答上来,心里想着凉凉了啊。 -6. 用到了那些开源技术、在开源领域做过贡献吗? -7. 常见的排序算法及其复杂度,现场写了快排。 -8. 红黑树,B树的一些问题。 -9. 讲讲算法及数据结构在实习项目中的用处。 -10. 自己的未来规划(就简单描述了一下自己未来的设想啊,说的还挺诚恳,面试官好像还挺满意的) -11. 你有什么问题要问吗? - -### 四面(半个技术面) - -三面面完当天,晚上9点接到面试电话,感觉像是部门或者项目主管。 这个和之前的面试不大相同,感觉面试官主要考察的是你解决问题的能力、学习能力和团队协作能力。 - -1. 让我讲一个自己觉得最不错的项目。然后就巴拉巴拉的聊,我记得主要是问了项目是如何进行协作的、遇到问题是如何解决的、与他人发生冲突是如何解决的这些。感觉聊了挺久。 -2. 出现 OOM 后你会怎么排查问题? -3. 自己平时是如何学习新技术的?除了 Java 还回去了解其他技术吗? -4. 上一段实习经历的收获。 -5. NginX如何做负载均衡、常见的负载均衡算法有哪些、一致性哈希的一致性是什么意思、一致性哈希是如何做哈希的 -6. 你有什么问题问我吗? -7. 还有一些其他的,想不起来了,感觉这一面不是偏向技术来问。 - -## 五面(HR面) - -1. 自我介绍(主要讲能突出自己的经历,会的编程技术一语带过)。 -2. 你觉得你有什么优点和缺点?如何克服这些缺点? -3. 说一件大学里你自己比较有成就感的一件事情,为此付出了那些努力。 -4. 你前面跟其他面试官讲过一些你做的项目吧?可以给我讲讲吗?你要考虑到我不是一个做技术的人,怎么让我也听得懂。项目中有什么问题,你怎么解决的?你最大的收获是什么? -5. 你目前有面试过其他公司吗?如果让你选,这些公司和阿里,你选哪个?(送分题,回答不好可能送命) -6. 你期望的工作地点是哪里? -7. 你有什么问题吗? - -### 总结 - -1. 可以看出面试官问我的很多问题都是比较常见的问题,所以记得一定要提前准备,还要深入准备,不要回答的太皮毛。很多时候一个问题可能会牵扯出很多问题,遇到不会的问题不要慌,冷静分析,如果你真的回答不上来,也不要担心自己是不是就要挂了,很可能这个问题本身就比较难。 -2. 表达能力和沟通能力太重要了,一定要提前练一下,我自身就是一个不太会说话的人,所以,面试前我对于自我介绍、项目介绍和一些常见问题都在脑子里练了好久,确保面试的时候能够很清晰和简洁的说出来。 -3. 等待面试的过程和面试的过程真的好熬人,那段时间我压力也比较大,好在我私下找到学长聊了很多,心情也好了很多。 -4. 面试之后及时总结,面的好的话,不要得意,尽快准备下一场面试吧! - -我觉得我还算是比较幸运的,最后也祝大家都能获得心仪的Offer。 - - - - diff --git "a/docs/essential-content-for-interview/BATJrealInterviewExperience/\350\232\202\350\232\201\351\207\221\346\234\215\345\256\236\344\271\240\347\224\237\351\235\242\347\273\217\346\200\273\347\273\223(\345\267\262\346\213\277\345\217\243\345\244\264offer).md" "b/docs/essential-content-for-interview/BATJrealInterviewExperience/\350\232\202\350\232\201\351\207\221\346\234\215\345\256\236\344\271\240\347\224\237\351\235\242\347\273\217\346\200\273\347\273\223(\345\267\262\346\213\277\345\217\243\345\244\264offer).md" deleted file mode 100644 index 2e2df23..0000000 --- "a/docs/essential-content-for-interview/BATJrealInterviewExperience/\350\232\202\350\232\201\351\207\221\346\234\215\345\256\236\344\271\240\347\224\237\351\235\242\347\273\217\346\200\273\347\273\223(\345\267\262\346\213\277\345\217\243\345\244\264offer).md" +++ /dev/null @@ -1,251 +0,0 @@ -本文来自 Anonymous 的投稿 ,JavaGuide 对原文进行了重新排版和一点完善。 - - - -- [一面 (37 分钟左右)](#一面-37-分钟左右) -- [二面 (33 分钟左右)](#二面-33-分钟左右) -- [三面 (46 分钟)](#三面-46-分钟) -- [HR 面](#hr-面) - - - -### 一面 (37 分钟左右) - -一面是上海的小哥打来的,3.12 号中午确认的内推,下午就打来约时间了,也是唯一一个约时间的面试官。约的晚上八点。紧张的一比,人生第一次面试就献给了阿里。 - -幸运的是一面的小哥特温柔。好像是个海归?口语中夹杂着英文。废话不多说,上干货: - -**面试官:** 先自我介绍下吧! - -**我:** 巴拉巴拉...。 - -> 关于自我介绍:从 HR 面、技术面到高管面/部门主管面,面试官一般会让你先自我介绍一下,所以好好准备自己的自我介绍真的非常重要。网上一般建议的是准备好两份自我介绍:一份对 HR 说的,主要讲能突出自己的经历,会的编程技术一语带过;另一份对技术面试官说的,主要讲自己会的技术细节,项目经验,经历那些就一语带过。 - -**面试官:** 我看你简历上写你做了个秒杀系统?我们就从这个项目开始吧,先介绍下你的项目。 - -> 关于项目介绍:如果有项目的话,技术面试第一步,面试官一般都是让你自己介绍一下你的项目。你可以从下面几个方向来考虑: -> -> 1. 对项目整体设计的一个感受(面试官可能会让你画系统的架构图) -> 2. 在这个项目中你负责了什么、做了什么、担任了什么角色 -> 3. 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用 -> 4. 另外项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个棘手的问题的时候你是如何解决的又或者说你在这个项目用了什么技术实现了什么功能比如:用 redis 做缓存提高访问速度和并发量、使用消息队列削峰和降流等等。 - -**我:** 我说了我是如何考虑它的需求(秒杀地址隐藏,记录订单,减库存),一开始简单的用 synchronized 锁住方法,出现了问题,后来乐观锁改进,又有瓶颈,再上缓存,出现了缓存雪崩,于是缓存预热,错开缓存失效时间。最后,发现先记录订单再减库存会减少行级锁等待时间。 - -> 一面面试官很耐心地听,并给了我一些指导,问了我乐观锁是怎么实现的,我说是基于 sql 语句,在减库存操作的 where 条件里加剩余库存数>0,他说这应该不算是一种乐观锁,应该先查库存,在减库存的时候判断当前库存是否与读到的库存一样(可这样不是多一次查询操作吗?不是很理解,不过我没有反驳,只是说理解您的意思。事实证明千万别怼面试官,即使你觉得他说的不对) - -**面试官:** 我缓存雪崩什么情况下会发生?如何避免? - -**我:** 当多个商品缓存同时失效时会雪崩,导致大量查询数据库。还有就是秒杀刚开始的时候缓存里没有数据。解决方案:缓存预热,错开缓存失效时间 - -**面试官:** 问我更新数据库的同时为什么不马上更新缓存,而是删除缓存? - -**我:** 因为考虑到更新数据库后更新缓存可能会因为多线程下导致写入脏数据(比如线程 A 先更新数据库成功,接下来要取更新缓存,接着线程 B 更新数据库,但 B 又更新了缓存,接着 B 的时间片用完了,线程 A 更新了缓存) - -逼逼了将近 30 分钟,面试官居然用周杰伦的语气对我说: - -![not bad](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3not-bad.jpg) - -我突然受宠若惊,连忙说谢谢,也正是因为第一次面试得到了面试官的肯定,才让我信心大增,二三面稳定发挥。 - -**面试官又曰:** 我看你还懂数据库是吧,答:略懂略懂。。。那我问个简单的吧! - -**我:** 因为这个问题太简单了,所以我忘记它是什么了。 - -**面试官:** 你还会啥数据库知识? - -**我:** 我一听,问的这么随意的吗。。。都让我选题了,我就说我了解索引,慢查询优化,巴拉巴拉 - -**面试官:** 等等,你说索引是吧,那你能说下索引的存储数据结构吗? - -**我:** 我心想这简单啊,我就说 B+树,还说了为什么用 B+树 - -**面试官:** 你简历上写的这个 J.U.C 包是什么啊?(他居然不知道 JUC) - -**我:** 就是 java 多线程的那个包啊。。。 - -**面试官:** 那你都了解里面的哪些东西呢? - -**我:** 哈哈哈!这可是我的强项,从 ConcurrentHashMap,ConcurrentLinkedQueue 说到 CountDownLatch,CyclicBarrier,又说到线程池,分别说了底层实现和项目中的应用。 - -**面试官:** 我觉得差不多了,那我再问个与技术无关的问题哈,虽然这个问题可能不应该我问,就是你是如何考虑你的项目架构的呢? - -**我:** 先用最简单的方式实现它,再去发掘系统的问题和瓶颈,于是查资料改进架构。。。 - -**面试官:** 好,那我给你介绍下我这边的情况吧 - -![chat-end](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3chat-end.jpg) - -**总结:** 一面可能是简历面吧,问的比较简单,我在讲项目中说出了我做项目时的学习历程和思考,赢得了面试官的好感,感觉他应该给我的评价很好。 - -### 二面 (33 分钟左右) - -然而开心了没一会,内推人问我面的怎么样啊?看我流程已经到大大 boss 那了。我一听二面不是主管吗???怎么直接跳了一面。于是瞬间慌了,赶紧(下床)学习准备二面。 - -隔了一天,3.14 的早上 10:56 分,杭州的大大 boss 给我打来了电话,卧槽我当时在上毛概课,万恶的毛概课每节课都点名,我还在最后一排不敢跑出去。于是接起电话来怂怂地说不好意思我在上课,晚上可以面试吗?大大 boss 看来很忙啊,跟我说晚上没时间啊,再说吧! - -于是又隔了一天,3.16 中午我收到了北京的电话,当时心里小失望,我的大大 boss 呢???接起电话来,就是一番狂轰乱炸。。。 - -第一步还是先自我介绍,这个就不多说了,提前准备好要说的重点就没问题! - -**面试官:** 我们还是从你的项目开始吧,说说你的秒杀系统。 - -**我:** 一面时的套路。。。我考虑到秒杀地址在开始前不应暴露给用户。。。 - -**面试官:** 等下啊,为什么要这样呢?暴露给用户会怎么样? - -**我:** 用户提前知道秒杀地址就可以写脚本来抢购了,这样不公平 - -**面试官:** 那比如说啊,我现在是个黑客,我在秒杀开始时写好了脚本,运行一万个线程获取秒杀地址,这样是不是也不公平呢? - -**我:** 我考虑到了这方面,于是我自己写了个 LRU 缓存(划重点,这么多好用的缓存我为啥不用偏要自己写?就是为了让面试官上钩问我是怎么写的,这样我就可以逼逼准备好的内容了!),用这个缓存存储请求的 ip 和用户名,一个 ip 和用户名只能同时透过 3 个请求。 - -**面试官:** 那我可不可以创建一个 ip 代理池和很多用户来抢购呢?假设我有很多手机号的账户。 - -**我:** 这就是在为难我胖虎啊,我说这种情况跟真实用户操作太像了。。。我没法区别,不过我觉得可以通过地理位置信息或者机器学习算法来做吧。。。 - -**面试官:** 好的这个问题就到这吧,你接着说 - -**我:** 我把生成订单和减库存两条 sql 语句放在一个事务里,都操作成功了则认为秒杀成功。 - -**面试官:** 等等,你这个订单表和商品库存表是在一个数据库的吧,那如果在不同的数据库中呢? - -**我:** 这面试官好变态啊,我只是个本科生?!?!我觉得应该要用分布式锁来实现吧。。。 - -**面试官:** 有没有更轻量级的做法? - -**我:** 不知道了。后来查资料发现可以用消息队列来实现。使用消息队列主要能带来两个好处:(1) 通过异步处理提高系统性能(削峰、减少响应所需时间);(2) 降低系统耦合性。关于消息队列的更多内容可以查看这篇文章: - -后来发现消息队列作用好大,于是现在在学手写一个消息队列。 - -**面试官:** 好的你接着说项目吧。 - -**我:** 我考虑到了缓存雪崩问题,于是。。。 - -**面试官:** 等等,你有没有考虑到一种情况,假如说你的缓存刚刚失效,大量流量就来查缓存,你的数据库会不会炸? - -**我:** 我不知道数据库会不会炸,反正我快炸了。当时说没考虑这么高的并发量,后来发现也是可以用消息队列来解决,对流量削峰填谷。 - -**面试官:** 好项目聊(怼)完了,我们来说说别的,操作系统了解吧,你能说说 NIO 吗? - -**我:** NIO 是。。。 - -**面试官:** 那你知道 NIO 的系统调用有哪些吗,具体是怎么实现的? - -**我:** 当时复习 NIO 的时候就知道是咋回事,不知道咋实现。最近在补这方面的知识,可见 NIO 还是很重要的! - -**面试官:** 说说进程切换时操作系统都会发生什么? - -**我:** 不如杀了我,我最讨厌操作系统了。简单说了下,可能不对,需要答案自行百度。 - -**面试官:** 说说线程池? - -**答:** 卧槽这我熟啊,把 Java 并发编程的艺术里讲的都说出来了,说了得有十分钟,自夸一波,毕竟这本书我看了五遍😂 - -**面试官:** 好问问计网吧如果设计一个聊天系统,应该用 TCP 还是 UDP?为什么 - -**我:** 当然是 TCP!原因如下: - -![TCP VS UDP](https://user-gold-cdn.xitu.io/2018/4/19/162db5e97e9a9e01?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -**面试官:** 好的,你有什么要问我的吗? - -**我:** 我还有下一次面试吗? - -**面试官:** 应该。应该有的,一周内吧。还告诉我居然转正前要实习三个月?wtf,一个大三满课的本科生让我如何在八月底前实习三个月? - -**我:** 面试官再见 - -![saygoodbye-smile](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3saygoodbye-smile.jpg) - -### 三面 (46 分钟) - -3.18 号,三面来了,这次又是那个大大 boss! - -第一步还是先自我介绍,这个就不多说了,提前准备好要说的重点就没问题! - -**面试官:** 聊聊你的项目? - -**我:** 经过二面的教训,我迅速学习了一下分布式的理论知识,并应用到了我的项目(吹牛逼)中。 - -**面试官:** 看你用到了 Spring 的事务机制,你能说下 Spring 的事务传播吗? - -**我:** 完了这个问题好像没准备,虽然之前刷知乎看到过。。。我就只说出来一条,面试官说其实这个有很多机制的,比如事务嵌套,内事务回滚外事务回滚都会有不同情况,你可以回去看看。 - -**面试官:** 说说你的分布式事务解决方案? - -**我:** 我叭叭的照着资料查到的解决方案说了一通,面试官怎么好像没大听懂??? - -> 阿里巴巴之前开源了一个分布式 Fescar(一种易于使用,高性能,基于 Java 的开源分布式事务解决方案),后来,Ant Financial 加入 Fescar,使其成为一个更加中立和开放的分布式交易社区,Fescar 重命名为 Seata。Github 地址: - -**面试官:** 好,我们聊聊其他项目,说说你这个 MapReduce 项目?MapReduce 原理了解过吗? - -**我:** 我叭叭地说了一通,面试官好像觉得这个项目太简单了。要不是没项目,我会把我的实验写上吗??? - -**面试官:** 你这个手写 BP 神经网络是干了啥? - -**我:** 这是我选修机器学习课程时的一个作业,我又对它进行了扩展。 - -**面试官:** 你能说说为什么调整权值时要沿着梯度下降的方向? - -**我:** 老大,你太厉害了,怎么什么都懂。我压根没准备这个项目。。。没想到会问,做过去好几个月了,加上当时一紧张就忘了,后来想起来大概是....。 - -**面试官:** 好我们问问基础知识吧,说说什么叫 xisuo? - -**我:**???xisuo,您说什么,不好意思我没听清。(这面试官有点口音。。。)就是 xisuo 啊!xisuo 你不知道吗?。。。尴尬了十几秒后我终于意识到,他在说死锁!!! - -**面试官:** 假如 A 账户给 B 账户转钱,会发生 xisuo 吗?能具体说说吗? - -**我:** 当时答的不好,后来发现面试官又是想问分布式,具体答案参考这个: - -**面试官:** 为什么不考研? - -**我:** 不喜欢学术氛围,巴拉巴拉。 - -**面试官:** 你有什么问题吗? - -**我:** 我还有下一面吗。。。面试官说让我等,一周内答复。 - ------- - -等了十天,一度以为我凉了,内推人说我流程到 HR 了,让我等着吧可能 HR 太忙了,3.28 号 HR 打来了电话,当时在教室,我直接飞了出去。 - -### HR 面 - -**面试官:** 你好啊,先自我介绍下吧 - -**我:** 巴拉巴拉....HR 面的技术面试和技术面的还是有所区别的! - -面试官人特别好,一听就是很会说话的小姐姐!说我这里给你悄悄透露下,你的评级是 A 哦! - -![panghu-knowledge](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3panghu-knowledge.jpg) - -接下来就是几个经典 HR 面挂人的问题,什么难给我来什么,我看别人的 HR 面怎么都是聊聊天。。。 - -**面试官:** 你为什么选择支付宝呢,你怎么看待支付宝? - -**我:** 我从个人情怀,公司理念,环境氛围,市场价值,趋势导向分析了一波(说白了就是疯狂夸支付宝,不过说实话我说的那些一点都没撒谎,阿里确实做到了。比如我举了个雷军和格力打赌 5 年 2000 亿销售额,大部分企业家关注的是利益,而马云更关注的是真的为人类为世界做一些事情,利益不是第一位的。) - -**面试官:** 明白了解,那你的优点我们都很明了了,你能说说你的缺点吗? - -> 缺点肯定不能是目标岗位需要的关键能力!!! -> -> 总之,记住一点,面试官问你这个问题的话,你可以说一些不影响你这个职位工作需要的一些缺点。比如你面试后端工程师,面试官问你的缺点是什么的话,你可以这样说:自己比较内向,平时不太爱与人交流,但是考虑到以后可能要和客户沟通,自己正在努力改。 - -**我:** 据说这是 HR 面最难的一个问题。。。我当时翻了好几天的知乎才找到一个合适的,也符合我的答案:我有时候会表现的不太自信,比如阿里的内推二月份就开始了,其实我当时已经复习了很久了,但是老是觉得自己还不行,不敢投简历,于是又把书看了一遍才投的,当时也是舍友怂恿一波才投的,面了之后发现其实自己也没有很差。(划重点,一定要把自己的缺点圆回来)。 - -**面试官:** HR 好像不太满意我的答案,继续问我还有缺点吗? - -**我:** 我说比较容易紧张吧,举了自己大一面实验室因为紧张没进去的例子,后来不断调整心态,现在已经好很多了。 - -接下来又是个好难的问题。 - -**面试官:** BAT 都给你 offer 了,你怎么选? - -其实我当时好想说,BT 是什么?不好意思我只知道阿里。 - -**我 :** 哈哈哈哈开玩笑,就说了阿里的文化,支付宝给我们带来很多便利,想加入支付宝为人类做贡献! - -最后 HR 问了我实习时间,现在大几之类的问题,说肯定会给我发 offer 的,让我等着就好了,希望过两天能收到好的结果。 - -![mengbi](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3mengbi.jpg) diff --git a/docs/essential-content-for-interview/PreparingForInterview/JavaInterviewLibrary.md b/docs/essential-content-for-interview/PreparingForInterview/JavaInterviewLibrary.md deleted file mode 100644 index 835b6a5..0000000 --- a/docs/essential-content-for-interview/PreparingForInterview/JavaInterviewLibrary.md +++ /dev/null @@ -1,89 +0,0 @@ -昨天我整理了公众号历史所有和面试相关的我觉得还不错的文章:[整理了一些有助于你拿Offer的文章]() 。今天分享一下最近逛Github看到了一些我觉得对于Java面试以及学习有帮助的仓库,这些仓库涉及Java核心知识点整理、Java常见面试题、算法、基础知识点比如网络和操作系统等等。 - -## 知识点相关 - -### 1.JavaGuide - -- Github地址: [https://github.com/Snailclimb/JavaGuide](https://github.com/Snailclimb/JavaGuide) -- star: 64.0k -- 介绍: 【Java学习+面试指南】 一份涵盖大部分Java程序员所需要掌握的核心知识。 - -### 2.CS-Notes - -- Github 地址: -- Star: 68.3k -- 介绍: 技术面试必备基础知识、Leetcode 题解、后端面试、Java 面试、春招、秋招、操作系统、计算机网络、系统设计。 - -### 3. advanced-java - -- Github地址:[https://github.com/doocs/advanced-java](https://github.com/doocs/advanced-java) -- star: 23.4k -- 介绍: 互联网 Java 工程师进阶知识完全扫盲:涵盖高并发、分布式、高可用、微服务等领域知识,后端同学必看,前端同学也可学习。 - -### 4.JCSprout - -- Github地址:[https://github.com/crossoverJie/JCSprout](https://github.com/crossoverJie/JCSprout) -- star: 21.2k -- 介绍: Java Core Sprout:处于萌芽阶段的 Java 核心知识库。 - -### 5.toBeTopJavaer - -- Github地址:[https://github.com/hollischuang/toBeTopJavaer](https://github.com/hollischuang/toBeTopJavaer) -- star: 4.0 k -- 介绍: Java工程师成神之路。 - -### 6.architect-awesome - -- Github地址:[https://github.com/xingshaocheng/architect-awesome](https://github.com/xingshaocheng/architect-awesome) -- star: 34.4 k -- 介绍:后端架构师技术图谱。 - -### 7.technology-talk - -- Github地址: [https://github.com/aalansehaiyang/technology-talk](https://github.com/aalansehaiyang/technology-talk) -- star: 6.1k -- 介绍: 汇总java生态圈常用技术框架、开源中间件,系统架构、项目管理、经典架构案例、数据库、常用三方库、线上运维等知识。 - -### 8.fullstack-tutorial - -- Github地址: [https://github.com/frank-lam/fullstack-tutorial](https://github.com/frank-lam/fullstack-tutorial) -- star: 4.0k -- 介绍: fullstack tutorial 2019,后台技术栈/架构师之路/全栈开发社区,春招/秋招/校招/面试。 - -### 9.3y - -- Github地址:[https://github.com/ZhongFuCheng3y/3y](https://github.com/ZhongFuCheng3y/3y) -- star: 1.9 k -- 介绍: Java 知识整合。 - -### 10.java-bible - -- Github地址:[https://github.com/biezhi/java-bible](https://github.com/biezhi/java-bible) -- star: 2.3k -- 介绍: 这里记录了一些技术摘要,部分文章来自网络,本项目的目的力求分享精品技术干货,以Java为主。 - -### 11.interviews - -- Github地址: [https://github.com/kdn251/interviews/blob/master/README-zh-cn.md](https://github.com/kdn251/interviews/blob/master/README-zh-cn.md) -- star: 35.3k -- 介绍: 软件工程技术面试个人指南(国外的一个项目,虽然有翻译版,但是不太推荐,因为很多内容并不适用于国内)。 - -## 算法相关 - -### 1.LeetCodeAnimation - -- Github 地址: -- Star: 33.4k -- 介绍: Demonstrate all the questions on LeetCode in the form of animation.(用动画的形式呈现解LeetCode题目的思路)。 - -### 2.awesome-java-leetcode - -- Github地址:[https://github.com/Blankj/awesome-java-leetcode](https://github.com/Blankj/awesome-java-leetcode) -- star: 6.1k -- 介绍: LeetCode 上 Facebook 的面试题目。 - -### 3.leetcode - -- Github地址:[https://github.com/azl397985856/leetcode](https://github.com/azl397985856/leetcode) -- star: 12.0k -- 介绍: LeetCode Solutions: A Record of My Problem Solving Journey.( leetcode题解,记录自己的leetcode解题之路。) \ No newline at end of file diff --git a/docs/essential-content-for-interview/PreparingForInterview/JavaProgrammerNeedKnow.md b/docs/essential-content-for-interview/PreparingForInterview/JavaProgrammerNeedKnow.md deleted file mode 100644 index d515693..0000000 --- a/docs/essential-content-for-interview/PreparingForInterview/JavaProgrammerNeedKnow.md +++ /dev/null @@ -1,81 +0,0 @@ -  身边的朋友或者公众号的粉丝很多人都向我询问过:“我是双非/三本/专科学校的,我有机会进入大厂吗?”、“非计算机专业的学生能学好吗?”、“如何学习Java?”、“Java学习该学哪些东西?”、“我该如何准备Java面试?”......这些方面的问题。我会根据自己的一点经验对大部分人关心的这些问题进行答疑解惑。现在又刚好赶上考研结束,这篇文章也算是给考研结束准备往Java后端方向发展的朋友们指明一条学习之路。道理懂了如果没有实际行动,那这篇文章对你或许没有任何意义。 - -### Question1:我是双非/三本/专科学校的,我有机会进入大厂吗? - -  我自己也是非985非211学校的,结合自己的经历以及一些朋友的经历,我觉得让我回答这个问题再好不过。 - -  首先,我觉得学校歧视很正常,真的太正常了,如果要抱怨的话,你只能抱怨自己没有进入名校。但是,千万不要动不动说自己学校差,动不动拿自己学校当做自己进不了大厂的借口,学历只是筛选简历的很多标准中的一个而已,如果你够优秀,简历够丰富,你也一样可以和名校同学一起同台竞争。 - -  企业HR肯定是更喜欢高学历的人,毕竟985、211优秀人才比例肯定比普通学校高很多,HR团队肯定会优先在这些学校里选。这就好比相亲,你是愿意在很多优秀的人中选一个优秀的,还是愿意在很多普通的人中选一个优秀的呢? -   -  双非本科甚至是二本、三本甚至是专科的同学也有很多进入大厂的,不过比率相比于名校的低很多而已。从大厂招聘的结果上看,高学历人才的数量占据大头,那些成功进入BAT、美团,京东,网易等大厂的双非本科甚至是二本、三本甚至是专科的同学往往是因为具备丰富的项目经历或者在某个含金量比较高的竞赛比如ACM中取得了不错的成绩。**一部分学历不突出但能力出众的面试者能够进入大厂并不是说明学历不重要,而是学历的软肋能够通过其他的优势来弥补。** 所以,如果你的学校不够好而你自己又想去大厂的话,建议你可以从这几点来做:**①尽量在面试前最好有一个可以拿的出手的项目;②有实习条件的话,尽早出去实习,实习经历也会是你的简历的一个亮点(有能力在大厂实习最佳!);③参加一些含金量比较高的比赛,拿不拿得到名次没关系,重在锻炼。** - - -### Question2:非计算机专业的学生能学好Java后台吗?我能进大厂吗? - -  当然可以!现在非科班的程序员很多,很大一部分原因是互联网行业的工资比较高。我们学校外面的培训班里面90%都是非科班,我觉得他们很多人学的都还不错。另外,我的一个朋友本科是机械专业,大一开始自学安卓,技术贼溜,在我看来他比大部分本科是计算机的同学学的还要好。参考Question1的回答,即使你是非科班程序员,如果你想进入大厂的话,你也可以通过自己的其他优势来弥补。 - -  我觉得我们不应该因为自己的专业给自己划界限或者贴标签,说实话,很多科班的同学可能并不如你,你以为科班的同学就会认真听讲吗?还不是几乎全靠自己课下自学!不过如果你是非科班的话,你想要学好,那么注定就要舍弃自己本专业的一些学习时间,这是无可厚非的。 - -  建议非科班的同学,首先要打好计算机基础知识基础:①计算机网络、②操作系统、③数据机构与算法,我个人觉得这3个对你最重要。这些东西就像是内功,对你以后的长远发展非常有用。当然,如果你想要进大厂的话,这些知识也是一定会被问到的。另外,“一定学好数据结构与算法!一定学好数据结构与算法!一定学好数据结构与算法!”,重要的东西说3遍。 - - - -### Question3: 我没有实习经历的话找工作是不是特别艰难? - -  没有实习经历没关系,只要你有拿得出手的项目或者大赛经历的话,你依然有可能拿到大厂的 offer 。笔主当时找工作的时候就没有实习经历以及大赛获奖经历,单纯就是凭借自己的项目经验撑起了整个面试。 - -  如果你既没有实习经历,又没有拿得出手的项目或者大赛经历的话,我觉得在简历关,除非你有其他特别的亮点,不然,你应该就会被刷。 - -### Question4: 我该如何准备面试呢?面试的注意事项有哪些呢? - -下面是我总结的一些准备面试的Tips以及面试必备的注意事项: - -1. **准备一份自己的自我介绍,面试的时候根据面试对象适当进行修改**(突出重点,突出自己的优势在哪里,切忌流水账); -2. **注意随身带上自己的成绩单和简历复印件;** (有的公司在面试前都会让你交一份成绩单和简历当做面试中的参考。) -3. **如果需要笔试就提前刷一些笔试题,大部分在线笔试的类型是选择题+编程题,有的还会有简答题。**(平时空闲时间多的可以刷一下笔试题目(牛客网上有很多),但是不要只刷面试题,不动手code,程序员不是为了考试而存在的。)另外,注意抓重点,因为题目太多了,但是有很多题目几乎次次遇到,像这样的题目一定要搞定。 -4. **提前准备技术面试。** 搞清楚自己面试中可能涉及哪些知识点、哪些知识点是重点。面试中哪些问题会被经常问到、自己该如何回答。(强烈不推荐背题,第一:通过背这种方式你能记住多少?能记住多久?第二:背题的方式的学习很难坚持下去!) -5. **面试之前做好定向复习。** 也就是专门针对你要面试的公司来复习。比如你在面试之前可以在网上找找有没有你要面试的公司的面经。 -6. **准备好自己的项目介绍。** 如果有项目的话,技术面试第一步,面试官一般都是让你自己介绍一下你的项目。你可以从下面几个方向来考虑:①对项目整体设计的一个感受(面试官可能会让你画系统的架构图);②在这个项目中你负责了什么、做了什么、担任了什么角色;③ 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用;④项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个棘手的问题的时候你是如何解决的又或者说你在这个项目用了什么技术实现了什么功能比如:用 redis 做缓存提高访问速度和并发量、使用消息队列削峰和降流等等。 -7. **面试之后记得复盘。** 面试遭遇失败是很正常的事情,所以善于总结自己的失败原因才是最重要的。如果失败,不要灰心;如果通过,切勿狂喜。 - - -**一些还算不错的 Java面试/学习相关的仓库,相信对大家准备面试一定有帮助:**[盘点一下Github上开源的Java面试/学习相关的仓库,看完弄懂薪资至少增加10k](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247484817&idx=1&sn=12f0c254a240c40c2ccab8314653216b&chksm=fd9853f0caefdae6d191e6bf085d44ab9c73f165e3323aa0362d830e420ccbfad93aa5901021&token=766994974&lang=zh_CN#rd) - -### Question5: 我该自学还是报培训班呢? - -  我本人更加赞同自学(你要知道去了公司可没人手把手教你了,而且几乎所有的公司都对培训班出生的有偏见。为什么有偏见,你学个东西还要去培训班,说明什么,同等水平下,你的自学能力以及自律能力一定是比不上自学的人的)。但是如果,你连每天在寝室坚持学上8个小时以上都坚持不了,或者总是容易半途而废的话,我还是推荐你去培训班。观望身边同学去培训班的,大多是非计算机专业或者是没有自律能力以及自学能力非常差的人。 - -  另外,如果自律能力不行,你也可以通过结伴学习、参加老师的项目等方式来督促自己学习。 - -  总结:去不去培训班主要还是看自己,如果自己能坚持自学就自学,坚持不下来就去培训班。 - -### Question6: 没有项目经历/博客/Github开源项目怎么办? - -  从现在开始做! - -  网上有很多非常不错的项目视频,你就跟着一步一步做,不光要做,还要改进,改善。另外,如果你的老师有相关 Java 后台项目的话,你也可以主动申请参与进来。 - -  如果有自己的博客,也算是简历上的一个亮点。建议可以在掘金、Segmentfault、CSDN等技术交流社区写博客,当然,你也可以自己搭建一个博客(采用 Hexo+Githu Pages 搭建非常简单)。写一些什么?学习笔记、实战内容、读书笔记等等都可以。 - -  多用 Github,用好 Github,上传自己不错的项目,写好 readme 文档,在其他技术社区做好宣传。相信你也会收获一个不错的开源项目! - - -### Question7: 大厂到底青睐什么样的应届生? - -  从阿里、腾讯等大厂招聘官网对于Java后端方向/后端方向的应届实习生的要求,我们大概可以总结归纳出下面这 4 点能给简历增加很多分数: - -- 参加过竞赛(含金量超高的是ACM); -- 对数据结构与算法非常熟练; -- 参与过实际项目(比如学校网站); -- 参与过某个知名的开源项目或者自己的某个开源项目很不错; - -  除了我上面说的这三点,在面试Java工程师的时候,下面几点也提升你的个人竞争力: - -- 熟悉Python、Shell、Perl等脚本语言; -- 熟悉 Java 优化,JVM调优; -- 熟悉 SOA 模式; -- 熟悉自己所用框架的底层知识比如Spring; -- 了解分布式一些常见的理论; -- 具备高并发开发经验;大数据开发经验等等。 - diff --git a/docs/essential-content-for-interview/PreparingForInterview/interviewPrepare.md b/docs/essential-content-for-interview/PreparingForInterview/interviewPrepare.md deleted file mode 100644 index 1ae36a3..0000000 --- a/docs/essential-content-for-interview/PreparingForInterview/interviewPrepare.md +++ /dev/null @@ -1,88 +0,0 @@ -不论是校招还是社招都避免不了各种面试、笔试,如何去准备这些东西就显得格外重要。不论是笔试还是面试都是有章可循的,我这个“有章可循”说的意思只是说应对技术面试是可以提前准备。 我其实特别不喜欢那种临近考试就提前背啊记啊各种题的行为,非常反对!我觉得这种方法特别极端,而且在稍有一点经验的面试官面前是根本没有用的。建议大家还是一步一个脚印踏踏实实地走。 - - - -- [1 如何获取大厂面试机会?](#1-如何获取大厂面试机会) -- [2 面试前的准备](#2--面试前的准备) - - [2.1 准备自己的自我介绍](#21-准备自己的自我介绍) - - [2.2 关于着装](#22-关于着装) - - [2.3 随身带上自己的成绩单和简历](#23-随身带上自己的成绩单和简历) - - [2.4 如果需要笔试就提前刷一些笔试题](#24-如果需要笔试就提前刷一些笔试题) - - [2.5 花时间一些逻辑题](#25-花时间一些逻辑题) - - [2.6 准备好自己的项目介绍](#26-准备好自己的项目介绍) - - [2.7 提前准备技术面试](#27-提前准备技术面试) - - [2.7 面试之前做好定向复习](#27-面试之前做好定向复习) -- [3 面试之后复盘](#3-面试之后复盘) - - - -## 1 如何获取大厂面试机会? - -**在讲如何获取大厂面试机会之前,先来给大家科普/对比一下两个校招非常常见的概念——春招和秋招。** - -1. **招聘人数** :秋招多于春招 ; -2. **招聘时间** : 秋招一般7月左右开始,大概一直持续到10月底。但是大厂(如BAT)都会早开始早结束,所以一定要把握好时间。春招最佳时间为3月,次佳时间为4月,进入5月基本就不会再有春招了(金三银四)。 -3. **应聘难度** :秋招略大于春招; -4. **招聘公司:** 秋招数量多,而春招数量较少,一般为秋招的补充。 - -**综上,一般来说,秋招的含金量明显是高于春招的。** - -**下面我就说一下我自己知道的一些方法,不过应该也涵盖了大部分获取面试机会的方法。** - -1. **关注大厂官网,随时投递简历(走流程的网申);** -2. **线下参加宣讲会,直接投递简历;** -3. **找到师兄师姐/认识的人,帮忙内推(能够让你避开网申简历筛选,笔试筛选,还是挺不错的,不过也还是需要你的简历够棒);** -4. **博客发文被看中/Github优秀开源项目作者,大厂内部人员邀请你面试;** -5. **求职类网站投递简历(不是太推荐,适合海投);** - - -除了这些方法,我也遇到过这样的经历:有些大公司的一些部门可能暂时没招够人,然后如果你的亲戚或者朋友刚好在这个公司,而你正好又在寻求offer,那么面试机会基本上是有了,而且这种面试的难度好像一般还普遍比其他正规面试低很多。 - -## 2 面试前的准备 - -### 2.1 准备自己的自我介绍 - -从HR面、技术面到高管面/部门主管面,面试官一般会让你先自我介绍一下,所以好好准备自己的自我介绍真的非常重要。网上一般建议的是准备好两份自我介绍:一份对hr说的,主要讲能突出自己的经历,会的编程技术一语带过;另一份对技术面试官说的,主要讲自己会的技术细节,项目经验,经历那些就一语带过。 - -我这里简单分享一下我自己的自我介绍的一个简单的模板吧: - -> 面试官,您好!我叫某某。大学时间我主要利用课外时间学习某某。在校期间参与过一个某某系统的开发,另外,自己学习过程中也写过很多系统比如某某系统。在学习之余,我比较喜欢通过博客整理分享自己所学知识。我现在是某某社区的认证作者,写过某某很不错的文章。另外,我获得过某某奖,我的Github上开源的某个项目已经有多少Star了。 - -### 2.2 关于着装 - -穿西装、打领带、小皮鞋?NO!NO!NO!这是互联网公司面试又不是去走红毯,所以你只需要穿的简单大方就好,不需要太正式。 - -### 2.3 随身带上自己的成绩单和简历 - -有的公司在面试前都会让你交一份成绩单和简历当做面试中的参考。 - -### 2.4 如果需要笔试就提前刷一些笔试题 - -平时空闲时间多的可以刷一下笔试题目(牛客网上有很多)。但是不要只刷面试题,不动手code,程序员不是为了考试而存在的。 - -### 2.5 花时间一些逻辑题 - -面试中发现有些公司都有逻辑题测试环节,并且都把逻辑笔试成绩作为很重要的一个参考。 - -### 2.6 准备好自己的项目介绍 - -如果有项目的话,技术面试第一步,面试官一般都是让你自己介绍一下你的项目。你可以从下面几个方向来考虑: - -1. 对项目整体设计的一个感受(面试官可能会让你画系统的架构图) -2. 在这个项目中你负责了什么、做了什么、担任了什么角色 -3. 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用 -4. 另外项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个棘手的问题的时候你是如何解决的又或者说你在这个项目用了什么技术实现了什么功能比如:用redis做缓存提高访问速度和并发量、使用消息队列削峰和降流等等。 - -### 2.7 提前准备技术面试 - -搞清楚自己面试中可能涉及哪些知识点、哪些知识点是重点。面试中哪些问题会被经常问到、自己该如何回答。(强烈不推荐背题,第一:通过背这种方式你能记住多少?能记住多久?第二:背题的方式的学习很难坚持下去!) - -### 2.7 面试之前做好定向复习 - -所谓定向复习就是专门针对你要面试的公司来复习。比如你在面试之前可以在网上找找有没有你要面试的公司的面经。 - -举个栗子:在我面试 ThoughtWorks 的前几天我就在网上找了一些关于 ThoughtWorks 的技术面的一些文章。然后知道了 ThoughtWorks 的技术面会让我们在之前做的作业的基础上增加一个或两个功能,所以我提前一天就把我之前做的程序重新重构了一下。然后在技术面的时候,简单的改了几行代码之后写个测试就完事了。如果没有提前准备,我觉得 20 分钟我很大几率会完不成这项任务。 - -## 3 面试之后复盘 - -如果失败,不要灰心;如果通过,切勿狂喜。面试和工作实际上是两回事,可能很多面试未通过的人,工作能力比你强的多,反之亦然。我个人觉得面试也像是一场全新的征程,失败和胜利都是平常之事。所以,劝各位不要因为面试失败而灰心、丧失斗志。也不要因为面试通过而沾沾自喜,等待你的将是更美好的未来,继续加油! diff --git "a/docs/essential-content-for-interview/PreparingForInterview/\345\272\224\345\261\212\347\224\237\351\235\242\350\257\225\346\234\200\347\210\261\351\227\256\347\232\204\345\207\240\351\201\223Java\345\237\272\347\241\200\351\227\256\351\242\230.md" "b/docs/essential-content-for-interview/PreparingForInterview/\345\272\224\345\261\212\347\224\237\351\235\242\350\257\225\346\234\200\347\210\261\351\227\256\347\232\204\345\207\240\351\201\223Java\345\237\272\347\241\200\351\227\256\351\242\230.md" deleted file mode 100644 index 2e93113..0000000 --- "a/docs/essential-content-for-interview/PreparingForInterview/\345\272\224\345\261\212\347\224\237\351\235\242\350\257\225\346\234\200\347\210\261\351\227\256\347\232\204\345\207\240\351\201\223Java\345\237\272\347\241\200\351\227\256\351\242\230.md" +++ /dev/null @@ -1,743 +0,0 @@ - - -- [一 为什么 Java 中只有值传递?](#一-为什么-java-中只有值传递) -- [二 ==与 equals(重要)](#二-与-equals重要) -- [三 hashCode 与 equals(重要)](#三-hashcode-与-equals重要) - - [3.1 hashCode()介绍](#31-hashcode介绍) - - [3.2 为什么要有 hashCode](#32-为什么要有-hashcode) - - [3.3 hashCode()与 equals()的相关规定](#33-hashcode与-equals的相关规定) - - [3.4 为什么两个对象有相同的 hashcode 值,它们也不一定是相等的?](#34-为什么两个对象有相同的-hashcode-值它们也不一定是相等的) -- [四 String 和 StringBuffer、StringBuilder 的区别是什么?String 为什么是不可变的?](#四-string-和-stringbufferstringbuilder-的区别是什么string-为什么是不可变的) - - [String 为什么是不可变的吗?](#string-为什么是不可变的吗) - - [String 真的是不可变的吗?](#string-真的是不可变的吗) -- [五 什么是反射机制?反射机制的应用场景有哪些?](#五-什么是反射机制反射机制的应用场景有哪些) - - [5.1 反射机制介绍](#51-反射机制介绍) - - [5.2 静态编译和动态编译](#52-静态编译和动态编译) - - [5.3 反射机制优缺点](#53-反射机制优缺点) - - [5.4 反射的应用场景](#54-反射的应用场景) -- [六 什么是 JDK?什么是 JRE?什么是 JVM?三者之间的联系与区别](#六-什么是-jdk什么是-jre什么是-jvm三者之间的联系与区别) - - [6.1 JVM](#61-jvm) - - [6.2 JDK 和 JRE](#62-jdk-和-jre) -- [七 什么是字节码?采用字节码的最大好处是什么?](#七-什么是字节码采用字节码的最大好处是什么) -- [八 接口和抽象类的区别是什么?](#八-接口和抽象类的区别是什么) -- [九 重载和重写的区别](#九-重载和重写的区别) - - [重载](#重载) - - [重写](#重写) -- [十. Java 面向对象编程三大特性: 封装 继承 多态](#十-java-面向对象编程三大特性-封装-继承-多态) - - [封装](#封装) - - [继承](#继承) - - [多态](#多态) -- [十一. 什么是线程和进程?](#十一-什么是线程和进程) - - [11.1 何为进程?](#111-何为进程) - - [11.2 何为线程?](#112-何为线程) -- [十二. 请简要描述线程与进程的关系,区别及优缺点?](#十二-请简要描述线程与进程的关系区别及优缺点) - - [12.1 图解进程和线程的关系](#121-图解进程和线程的关系) - - [12.2 程序计数器为什么是私有的?](#122-程序计数器为什么是私有的) - - [12.3 虚拟机栈和本地方法栈为什么是私有的?](#123-虚拟机栈和本地方法栈为什么是私有的) - - [12.4 一句话简单了解堆和方法区](#124-一句话简单了解堆和方法区) -- [十三. 说说并发与并行的区别?](#十三-说说并发与并行的区别) -- [十四. 什么是上下文切换?](#十四-什么是上下文切换) -- [十五. 什么是线程死锁?如何避免死锁?](#十五-什么是线程死锁如何避免死锁) - - [15.1. 认识线程死锁](#151-认识线程死锁) - - [15.2 如何避免线程死锁?](#152-如何避免线程死锁) -- [十六. 说说 sleep() 方法和 wait() 方法区别和共同点?](#十六-说说-sleep-方法和-wait-方法区别和共同点) -- [十七. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?](#十七-为什么我们调用-start-方法时会执行-run-方法为什么我们不能直接调用-run-方法) -- [参考](#参考) - - - -## 一 为什么 Java 中只有值传递? - -首先回顾一下在程序设计语言中有关将参数传递给方法(或函数)的一些专业术语。**按值调用(call by value)表示方法接收的是调用者提供的值,而按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。** 它用来描述各种程序设计语言(不只是 Java)中方法参数传递方式。 - -**Java 程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容。** - -**下面通过 3 个例子来给大家说明** - -> **example 1** - -```java -public static void main(String[] args) { - int num1 = 10; - int num2 = 20; - - swap(num1, num2); - - System.out.println("num1 = " + num1); - System.out.println("num2 = " + num2); -} - -public static void swap(int a, int b) { - int temp = a; - a = b; - b = temp; - - System.out.println("a = " + a); - System.out.println("b = " + b); -} -``` - -**结果:** - -``` -a = 20 -b = 10 -num1 = 10 -num2 = 20 -``` - -**解析:** - -![example 1 ](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-27/22191348.jpg) - -在 swap 方法中,a、b 的值进行交换,并不会影响到 num1、num2。因为,a、b 中的值,只是从 num1、num2 的复制过来的。也就是说,a、b 相当于 num1、num2 的副本,副本的内容无论怎么修改,都不会影响到原件本身。 - -**通过上面例子,我们已经知道了一个方法不能修改一个基本数据类型的参数,而对象引用作为参数就不一样,请看 example2.** - -> **example 2** - -```java - public static void main(String[] args) { - int[] arr = { 1, 2, 3, 4, 5 }; - System.out.println(arr[0]); - change(arr); - System.out.println(arr[0]); - } - - public static void change(int[] array) { - // 将数组的第一个元素变为0 - array[0] = 0; - } -``` - -**结果:** - -``` -1 -0 -``` - -**解析:** - -![example 2](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-27/3825204.jpg) - -array 被初始化 arr 的拷贝也就是一个对象的引用,也就是说 array 和 arr 指向的是同一个数组对象。 因此,外部对引用对象的改变会反映到所对应的对象上。 - -**通过 example2 我们已经看到,实现一个改变对象参数状态的方法并不是一件难事。理由很简单,方法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。** - -**很多程序设计语言(特别是,C++和 Pascal)提供了两种参数传递的方式:值调用和引用调用。有些程序员(甚至本书的作者)认为 Java 程序设计语言对对象采用的是引用调用,实际上,这种理解是不对的。由于这种误解具有一定的普遍性,所以下面给出一个反例来详细地阐述一下这个问题。** - -> **example 3** - -```java -public class Test { - - public static void main(String[] args) { - // TODO Auto-generated method stub - Student s1 = new Student("小张"); - Student s2 = new Student("小李"); - Test.swap(s1, s2); - System.out.println("s1:" + s1.getName()); - System.out.println("s2:" + s2.getName()); - } - - public static void swap(Student x, Student y) { - Student temp = x; - x = y; - y = temp; - System.out.println("x:" + x.getName()); - System.out.println("y:" + y.getName()); - } -} -``` - -**结果:** - -``` -x:小李 -y:小张 -s1:小张 -s2:小李 -``` - -**解析:** - -交换之前: - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-27/88729818.jpg) - -交换之后: - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-27/34384414.jpg) - -通过上面两张图可以很清晰的看出: **方法并没有改变存储在变量 s1 和 s2 中的对象引用。swap 方法的参数 x 和 y 被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝** - -> **总结** - -Java 程序设计语言对对象采用的不是引用调用,实际上,对象引用是按 -值传递的。 - -下面再总结一下 Java 中方法参数的使用情况: - -- 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。 -- 一个方法可以改变一个对象参数的状态。 -- 一个方法不能让对象参数引用一个新的对象。 - -**参考:** - -《Java 核心技术卷 Ⅰ》基础知识第十版第四章 4.5 小节 - -## 二 ==与 equals(重要) - -**==** : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型==比较的是值,引用数据类型==比较的是内存地址) - -**equals()** : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况: - -- 情况 1:类没有覆盖 equals()方法。则通过 equals()比较该类的两个对象时,等价于通过“==”比较这两个对象。 -- 情况 2:类覆盖了 equals()方法。一般,我们都覆盖 equals()方法来两个对象的内容相等;若它们的内容相等,则返回 true(即,认为这两个对象相等)。 - -**举个例子:** - -```java -public class test1 { - public static void main(String[] args) { - String a = new String("ab"); // a 为一个引用 - String b = new String("ab"); // b为另一个引用,对象的内容一样 - String aa = "ab"; // 放在常量池中 - String bb = "ab"; // 从常量池中查找 - if (aa == bb) // true - System.out.println("aa==bb"); - if (a == b) // false,非同一对象 - System.out.println("a==b"); - if (a.equals(b)) // true - System.out.println("aEQb"); - if (42 == 42.0) { // true - System.out.println("true"); - } - } -} -``` - -**说明:** - -- String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。 -- 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。 - -## 三 hashCode 与 equals(重要) - -面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写 equals 时必须重写 hashCode 方法?” - -### 3.1 hashCode()介绍 - -hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在 JDK 的 Object.java 中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。另外需要注意的是: Object 的 hashcode 方法是本地方法,也就是用 c 语言或 c++ 实现的,该方法通常用来将对象的 内存地址 转换为整数之后返回。 - -```java - /** - * Returns a hash code value for the object. This method is - * supported for the benefit of hash tables such as those provided by - * {@link java.util.HashMap}. - *

- * As much as is reasonably practical, the hashCode method defined by - * class {@code Object} does return distinct integers for distinct - * objects. (This is typically implemented by converting the internal - * address of the object into an integer, but this implementation - * technique is not required by the - * Java™ programming language.) - * - * @return a hash code value for this object. - * @see java.lang.Object#equals(java.lang.Object) - * @see java.lang.System#identityHashCode - */ - public native int hashCode(); -``` - -散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象) - -### 3.2 为什么要有 hashCode - -**我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:** - -当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的 Java 启蒙书《Head fist java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。 - -### 3.3 hashCode()与 equals()的相关规定 - -1. 如果两个对象相等,则 hashcode 一定也是相同的 -2. 两个对象相等,对两个对象分别调用 equals 方法都返回 true -3. 两个对象有相同的 hashcode 值,它们也不一定是相等的 -4. **因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖** -5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据) - -### 3.4 为什么两个对象有相同的 hashcode 值,它们也不一定是相等的? - -在这里解释一位小伙伴的问题。以下内容摘自《Head Fisrt Java》。 - -因为 hashCode() 所使用的杂凑算法也许刚好会让多个对象传回相同的杂凑值。越糟糕的杂凑算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同的 hashCode)。 - -我们刚刚也提到了 HashSet,如果 HashSet 在对比的时候,同样的 hashcode 有多个对象,它会使用 equals() 来判断是否真的相同。也就是说 hashcode 只是用来缩小查找成本。 - -## 四 String 和 StringBuffer、StringBuilder 的区别是什么?String 为什么是不可变的? - -**可变性** - -简单的来说:String 类中使用 final 关键字修饰字符数组来保存字符串,`private final char value[]`,所以 String 对象是不可变的。而 StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串`char[]value` 但是没有用 final 关键字修饰,所以这两种对象都是可变的。 - -StringBuilder 与 StringBuffer 的构造方法都是调用父类构造方法也就是 AbstractStringBuilder 实现的,大家可以自行查阅源码。 - -AbstractStringBuilder.java - -```java -abstract class AbstractStringBuilder implements Appendable, CharSequence { - char[] value; - int count; - AbstractStringBuilder() { - } - AbstractStringBuilder(int capacity) { - value = new char[capacity]; - } -``` - -**线程安全性** - -String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。 - -**性能** - -每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。 - -**对于三者使用的总结:** - -1. 操作少量的数据: 适用 String -2. 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder -3. 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer - -#### String 为什么是不可变的吗? - -简单来说就是 String 类利用了 final 修饰的 char 类型数组存储字符,源码如下图所以: - -```java - /** The value is used for character storage. */ - private final char value[]; -``` - -#### String 真的是不可变的吗? - -我觉得如果别人问这个问题的话,回答不可变就可以了。 -下面只是给大家看两个有代表性的例子: - -**1) String 不可变但不代表引用不可以变** - -```java - String str = "Hello"; - str = str + " World"; - System.out.println("str=" + str); -``` - -结果: - -``` -str=Hello World -``` - -解析: - -实际上,原来 String 的内容是不变的,只是 str 由原来指向"Hello"的内存地址转为指向"Hello World"的内存地址而已,也就是说多开辟了一块内存区域给"Hello World"字符串。 - -**2) 通过反射是可以修改所谓的“不可变”对象** - -```java - // 创建字符串"Hello World", 并赋给引用s - String s = "Hello World"; - - System.out.println("s = " + s); // Hello World - - // 获取String类中的value字段 - Field valueFieldOfString = String.class.getDeclaredField("value"); - - // 改变value属性的访问权限 - valueFieldOfString.setAccessible(true); - - // 获取s对象上的value属性的值 - char[] value = (char[]) valueFieldOfString.get(s); - - // 改变value所引用的数组中的第5个字符 - value[5] = '_'; - - System.out.println("s = " + s); // Hello_World -``` - -结果: - -``` -s = Hello World -s = Hello_World -``` - -解析: - -用反射可以访问私有成员, 然后反射出 String 对象中的 value 属性, 进而改变通过获得的 value 引用改变数组的结构。但是一般我们不会这么做,这里只是简单提一下有这个东西。 - -## 五 什么是反射机制?反射机制的应用场景有哪些? - -### 5.1 反射机制介绍 - -JAVA 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 java 语言的反射机制。 - -### 5.2 静态编译和动态编译 - -- **静态编译:**在编译时确定类型,绑定对象 -- **动态编译:**运行时确定类型,绑定对象 - -### 5.3 反射机制优缺点 - -- **优点:** 运行期类型的判断,动态加载类,提高代码灵活度。 -- **缺点:** 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的 java 代码要慢很多。 - -### 5.4 反射的应用场景 - -**反射是框架设计的灵魂。** - -在我们平时的项目开发过程中,基本上很少会直接使用到反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架也大量使用到了反射机制。 - -举例:① 我们在使用 JDBC 连接数据库时使用 `Class.forName()`通过反射加载数据库的驱动程序;②Spring 框架也用到很多反射机制,最经典的就是 xml 的配置模式。Spring 通过 XML 配置模式装载 Bean 的过程:1) 将程序内所有 XML 或 Properties 配置文件加载入内存中; -2)Java 类里面解析 xml 或 properties 里面的内容,得到对应实体类的字节码字符串以及相关的属性信息; 3)使用反射机制,根据这个字符串获得某个类的 Class 实例; 4)动态配置实例的属性 - -**推荐阅读:** - -- [Reflection:Java 反射机制的应用场景](https://segmentfault.com/a/1190000010162647?utm_source=tuicool&utm_medium=referral "Reflection:Java反射机制的应用场景") -- [Java 基础之—反射(非常重要)](https://blog.csdn.net/sinat_38259539/article/details/71799078 "Java基础之—反射(非常重要)") - -## 六 什么是 JDK?什么是 JRE?什么是 JVM?三者之间的联系与区别 - -### 6.1 JVM - -Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。 - -**什么是字节码?采用字节码的好处是什么?** - -> 在 Java 中,JVM 可以理解的代码就叫做`字节码`(即扩展名为 `.class` 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。 - -**Java 程序从源代码到运行一般有下面 3 步:** - -![Java程序运行过程](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/Java%20%E7%A8%8B%E5%BA%8F%E8%BF%90%E8%A1%8C%E8%BF%87%E7%A8%8B.png) - -我们需要格外注意的是 .class->机器码 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言。 - -> HotSpot 采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是 JIT 所需要编译的部分。JVM 会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。JDK 9 引入了一种新的编译模式 AOT(Ahead of Time Compilation),它是直接将字节码编译成机器码,这样就避免了 JIT 预热等各方面的开销。JDK 支持分层编译和 AOT 协作使用。但是 ,AOT 编译器的编译质量是肯定比不上 JIT 编译器的。 - -**总结:** - -Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。 - -### 6.2 JDK 和 JRE - -JDK 是 Java Development Kit,它是功能齐全的 Java SDK。它拥有 JRE 所拥有的一切,还有编译器(javac)和工具(如 javadoc 和 jdb)。它能够创建和编译程序。 - -JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。但是,它不能用于创建新程序。 - -如果你只是为了运行一下 Java 程序的话,那么你只需要安装 JRE 就可以了。如果你需要进行一些 Java 编程方面的工作,那么你就需要安装 JDK 了。但是,这不是绝对的。有时,即使您不打算在计算机上进行任何 Java 开发,仍然需要安装 JDK。例如,如果要使用 JSP 部署 Web 应用程序,那么从技术上讲,您只是在应用程序服务器中运行 Java 程序。那你为什么需要 JDK 呢?因为应用程序服务器会将 JSP 转换为 Java servlet,并且需要使用 JDK 来编译 servlet。 - -## 七 什么是字节码?采用字节码的最大好处是什么? - -**先看下 java 中的编译器和解释器:** - -Java 中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在 Java 中,这种供虚拟机理解的代码叫做`字节码`(即扩展名为`.class`的文件),它不面向任何特定的处理器,只面向虚拟机。每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java 源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行。这也就是解释了 Java 的编译与解释并存的特点。 - -Java 源代码---->编译器---->jvm 可执行的 Java 字节码(即虚拟指令)---->jvm---->jvm 中解释器----->机器可执行的二进制机器码---->程序运行。 - -**采用字节码的好处:** - -Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同的计算机上运行。 - -## 八 接口和抽象类的区别是什么? - -1. 接口的方法默认是 public,所有方法在接口中不能有实现,抽象类可以有非抽象的方法 -2. 接口中的实例变量默认是 final 类型的,而抽象类中则不一定 -3. 一个类可以实现多个接口,但最多只能实现一个抽象类 -4. 一个类实现接口的话要实现接口的所有方法,而抽象类不一定 -5. 接口不能用 new 实例化,但可以声明,但是必须引用一个实现该接口的对象 从设计层面来说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。 - -注意:Java8 后接口可以有默认实现( default )。 - -## 九 重载和重写的区别 - -### 重载 - -发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。 - -下面是《Java 核心技术》对重载这个概念的介绍: - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/bg/desktopjava核心技术-重载.jpg) - -### 重写 - -重写是子类对父类的允许访问的方法的实现过程进行重新编写,发生在子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。另外,如果父类方法访问修饰符为 private 则子类就不能重写该方法。**也就是说方法提供的行为改变,而方法的外貌并没有改变。** - -## 十. Java 面向对象编程三大特性: 封装 继承 多态 - -### 封装 - -封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。 - -### 继承 - -继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。 - -**关于继承如下 3 点请记住:** - -1. 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,**只是拥有**。 -2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。 -3. 子类可以用自己的方式实现父类的方法。(以后介绍)。 - -### 多态 - -所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。 - -在 Java 中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。 - -## 十一. 什么是线程和进程? - -### 11.1 何为进程? - -进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。 - -在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。 - -如下图所示,在 windows 中通过查看任务管理器的方式,我们就可以清楚看到 window 当前运行的进程(.exe 文件的运行)。 - -![进程示例图片-Windows](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/进程示例图片-Windows.png) - -### 11.2 何为线程? - -线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的**堆**和**方法区**资源,但每个线程有自己的**程序计数器**、**虚拟机栈**和**本地方法栈**,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。 - -Java 程序天生就是多线程程序,我们可以通过 JMX 来看一下一个普通的 Java 程序有哪些线程,代码如下。 - -```java -public class MultiThread { - public static void main(String[] args) { - // 获取 Java 线程管理 MXBean - ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); - // 不需要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息 - ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false); - // 遍历线程信息,仅打印线程 ID 和线程名称信息 - for (ThreadInfo threadInfo : threadInfos) { - System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName()); - } - } -} -``` - -上述程序输出如下(输出内容可能不同,不用太纠结下面每个线程的作用,只用知道 main 线程执行 main 方法即可): - -``` -[5] Attach Listener //添加事件 -[4] Signal Dispatcher // 分发处理给 JVM 信号的线程 -[3] Finalizer //调用对象 finalize 方法的线程 -[2] Reference Handler //清除 reference 线程 -[1] main //main 线程,程序入口 -``` - -从上面的输出内容可以看出:**一个 Java 程序的运行是 main 线程和多个其他线程同时运行**。 - -## 十二. 请简要描述线程与进程的关系,区别及优缺点? - -**从 JVM 角度说进程和线程之间的关系** - -### 12.1 图解进程和线程的关系 - -下图是 Java 内存区域,通过下图我们从 JVM 的角度来说一下线程和进程之间的关系。如果你对 Java 内存区域 (运行时数据区) 这部分知识不太了解的话可以阅读一下这篇文章:[《可能是把 Java 内存区域讲的最清楚的一篇文章》](https://github.com/Snailclimb/JavaGuide/blob/3965c02cc0f294b0bd3580df4868d5e396959e2e/Java%E7%9B%B8%E5%85%B3/%E5%8F%AF%E8%83%BD%E6%98%AF%E6%8A%8AJava%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F%E8%AE%B2%E7%9A%84%E6%9C%80%E6%B8%85%E6%A5%9A%E7%9A%84%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0.md "《可能是把 Java 内存区域讲的最清楚的一篇文章》") - -

- -
- -从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的**堆**和**方法区 (JDK1.8 之后的元空间)**资源,但是每个线程有自己的**程序计数器**、**虚拟机栈** 和 **本地方法栈**。 - -**总结:** 线程 是 进程 划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反 - -下面是该知识点的扩展内容! - -下面来思考这样一个问题:为什么**程序计数器**、**虚拟机栈**和**本地方法栈**是线程私有的呢?为什么堆和方法区是线程共享的呢? - -### 12.2 程序计数器为什么是私有的? - -程序计数器主要有下面两个作用: - -1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。 -2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。 - -需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。 - -所以,程序计数器私有主要是为了**线程切换后能恢复到正确的执行位置**。 - -### 12.3 虚拟机栈和本地方法栈为什么是私有的? - -- **虚拟机栈:** 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。 -- **本地方法栈:** 和虚拟机栈所发挥的作用非常相似,区别是: **虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。** 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。 - -所以,为了**保证线程中的局部变量不被别的线程访问到**,虚拟机栈和本地方法栈是线程私有的。 - -### 12.4 一句话简单了解堆和方法区 - -堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 - -## 十三. 说说并发与并行的区别? - -- **并发:** 同一时间段,多个任务都在执行 (单位时间内不一定同时执行); -- **并行:** 单位时间内,多个任务同时执行。 - -## 十四. 什么是上下文切换? - -多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。 - -概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。**任务从保存到再加载的过程就是一次上下文切换**。 - -上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。 - -Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。 - -## 十五. 什么是线程死锁?如何避免死锁? - -### 15.1. 认识线程死锁 - -多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。 - -如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。 - -![线程死锁示意图 ](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-4/2019-4%E6%AD%BB%E9%94%811.png) - -下面通过一个例子来说明线程死锁,代码模拟了上图的死锁的情况 (代码来源于《并发编程之美》): - -```java -public class DeadLockDemo { - private static Object resource1 = new Object();//资源 1 - private static Object resource2 = new Object();//资源 2 - - public static void main(String[] args) { - new Thread(() -> { - synchronized (resource1) { - System.out.println(Thread.currentThread() + "get resource1"); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println(Thread.currentThread() + "waiting get resource2"); - synchronized (resource2) { - System.out.println(Thread.currentThread() + "get resource2"); - } - } - }, "线程 1").start(); - - new Thread(() -> { - synchronized (resource2) { - System.out.println(Thread.currentThread() + "get resource2"); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println(Thread.currentThread() + "waiting get resource1"); - synchronized (resource1) { - System.out.println(Thread.currentThread() + "get resource1"); - } - } - }, "线程 2").start(); - } -} -``` - -Output - -``` -Thread[线程 1,5,main]get resource1 -Thread[线程 2,5,main]get resource2 -Thread[线程 1,5,main]waiting get resource2 -Thread[线程 2,5,main]waiting get resource1 -``` - -线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过`Thread.sleep(1000);`让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。 - -学过操作系统的朋友都知道产生死锁必须具备以下四个条件: - -1. 互斥条件:该资源任意一个时刻只由一个线程占用。 -2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。 -3. 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。 -4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。 - -### 15.2 如何避免线程死锁? - -我们只要破坏产生死锁的四个条件中的其中一个就可以了。 - -**破坏互斥条件** - -这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。 - -**破坏请求与保持条件** - -一次性申请所有的资源。 - -**破坏不剥夺条件** - -占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。 - -**破坏循环等待条件** - -靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。 - -我们对线程 2 的代码修改成下面这样就不会产生死锁了。 - -```java - new Thread(() -> { - synchronized (resource1) { - System.out.println(Thread.currentThread() + "get resource1"); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println(Thread.currentThread() + "waiting get resource2"); - synchronized (resource2) { - System.out.println(Thread.currentThread() + "get resource2"); - } - } - }, "线程 2").start(); -``` - -Output - -``` -Thread[线程 1,5,main]get resource1 -Thread[线程 1,5,main]waiting get resource2 -Thread[线程 1,5,main]get resource2 -Thread[线程 2,5,main]get resource1 -Thread[线程 2,5,main]waiting get resource2 -Thread[线程 2,5,main]get resource2 - -Process finished with exit code 0 -``` - -我们分析一下上面的代码为什么避免了死锁的发生? - -线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。 - -## 十六. 说说 sleep() 方法和 wait() 方法区别和共同点? - -- 两者最主要的区别在于:**sleep 方法没有释放锁,而 wait 方法释放了锁** 。 -- 两者都可以暂停线程的执行。 -- Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。 -- wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒。 - -## 十七. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法? - -这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来! - -new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。 - -**总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。** - -## 参考 - -- [https://blog.csdn.net/zhzhao999/article/details/53449504](https://blog.csdn.net/zhzhao999/article/details/53449504 "https://blog.csdn.net/zhzhao999/article/details/53449504") -- [https://www.cnblogs.com/skywang12345/p/3324958.html](https://www.cnblogs.com/skywang12345/p/3324958.html "https://www.cnblogs.com/skywang12345/p/3324958.html") -- [https://www.cnblogs.com/Eason-S/p/5524837.html](https://www.cnblogs.com/Eason-S/p/5524837.html "https://www.cnblogs.com/Eason-S/p/5524837.html") diff --git "a/docs/essential-content-for-interview/PreparingForInterview/\347\250\213\345\272\217\345\221\230\347\232\204\347\256\200\345\216\206\344\271\213\351\201\223.md" "b/docs/essential-content-for-interview/PreparingForInterview/\347\250\213\345\272\217\345\221\230\347\232\204\347\256\200\345\216\206\344\271\213\351\201\223.md" deleted file mode 100644 index 7feead7..0000000 --- "a/docs/essential-content-for-interview/PreparingForInterview/\347\250\213\345\272\217\345\221\230\347\232\204\347\256\200\345\216\206\344\271\213\351\201\223.md" +++ /dev/null @@ -1,121 +0,0 @@ - - -- [程序员简历就该这样写](#程序员简历就该这样写) - - [为什么说简历很重要?](#为什么说简历很重要) - - [先从面试前来说](#先从面试前来说) - - [再从面试中来说](#再从面试中来说) - - [下面这几点你必须知道](#下面这几点你必须知道) - - [必须了解的两大法则](#必须了解的两大法则) - - [STAR法则(Situation Task Action Result)](#star法则situation-task-action-result) - - [FAB 法则(Feature Advantage Benefit)](#fab-法则feature-advantage-benefit) - - [项目经历怎么写?](#项目经历怎么写) - - [专业技能该怎么写?](#专业技能该怎么写) - - [排版注意事项](#排版注意事项) - - [其他的一些小tips](#其他的一些小tips) - - [推荐的工具/网站](#推荐的工具网站) - - - -# 程序员简历就该这样写 - -本篇文章除了教大家用Markdown如何写一份程序员专属的简历,后面还会给大家推荐一些不错的用来写Markdown简历的软件或者网站,以及如何优雅的将Markdown格式转变为PDF格式或者其他格式。 - -推荐大家使用Markdown语法写简历,然后再将Markdown格式转换为PDF格式后进行简历投递。 - -如果你对Markdown语法不太了解的话,可以花半个小时简单看一下Markdown语法说明: http://www.markdown.cn 。 - -## 为什么说简历很重要? - -一份好的简历可以在整个申请面试以及面试过程中起到非常好的作用。 在不夸大自己能力的情况下,写出一份好的简历也是一项很棒的能力。为什么说简历很重要呢? - -### 先从面试前来说 - -- 假如你是网申,你的简历必然会经过HR的筛选,一张简历HR可能也就花费10秒钟看一下,然后HR就会决定你这一关是Fail还是Pass。 -- 假如你是内推,如果你的简历没有什么优势的话,就算是内推你的人再用心,也无能为力。 - -另外,就算你通过了筛选,后面的面试中,面试官也会根据你的简历来判断你究竟是否值得他花费很多时间去面试。 - -所以,简历就像是我们的一个门面一样,它在很大程度上决定了你能否进入到下一轮的面试中。 - -### 再从面试中来说 - -我发现大家比较喜欢看面经 ,这点无可厚非,但是大部分面经都没告诉你很多问题都是在特定条件下才问的。举个简单的例子:一般情况下你的简历上注明你会的东西才会被问到(Java、数据结构、网络、算法这些基础是每个人必问的),比如写了你会 redis,那面试官就很大概率会问你 redis 的一些问题。比如:redis的常见数据类型及应用场景、redis是单线程为什么还这么快、 redis 和 memcached 的区别、redis 内存淘汰机制等等。 - -所以,首先,你要明确的一点是:**你不会的东西就不要写在简历上**。另外,**你要考虑你该如何才能让你的亮点在简历中凸显出来**,比如:你在某某项目做了什么事情解决了什么问题(只要有项目就一定有要解决的问题)、你的某一个项目里使用了什么技术后整体性能和并发量提升了很多等等。 - -面试和工作是两回事,聪明的人会把面试官往自己擅长的领域领,其他人则被面试官牵着鼻子走。虽说面试和工作是两回事,但是你要想要获得自己满意的 offer ,你自身的实力必须要强。 - -## 下面这几点你必须知道 - -1. 大部分公司的HR都说我们不看重学历(骗你的!),但是如果你的学校不出众的话,很难在一堆简历中脱颖而出,除非你的简历上有特别的亮点,比如:某某大厂的实习经历、获得了某某大赛的奖等等。 -2. **大部分应届生找工作的硬伤是没有工作经验或实习经历,所以如果你是应届生就不要错过秋招和春招。一旦错过,你后面就极大可能会面临社招,这个时候没有工作经验的你可能就会面临各种碰壁,导致找不到一个好的工作** -3. **写在简历上的东西一定要慎重,这是面试官大量提问的地方;** -4. **将自己的项目经历完美的展示出来非常重要。** - -## 必须了解的两大法则 - -### STAR法则(Situation Task Action Result) - -- **Situation:** 事情是在什么情况下发生; -- **Task::** 你是如何明确你的任务的; -- **Action:** 针对这样的情况分析,你采用了什么行动方式; -- **Result:** 结果怎样,在这样的情况下你学习到了什么。 - -简而言之,STAR法则,就是一种讲述自己故事的方式,或者说,是一个清晰、条理的作文模板。不管是什么,合理熟练运用此法则,可以轻松的对面试官描述事物的逻辑方式,表现出自己分析阐述问题的清晰性、条理性和逻辑性。 - -### FAB 法则(Feature Advantage Benefit) - -- **Feature:** 是什么; -- **Advantage:** 比别人好在哪些地方; -- **Benefit:** 如果雇佣你,招聘方会得到什么好处。 - -简单来说,这个法则主要是让你的面试官知道你的优势、招了你之后对公司有什么帮助。 - -## 项目经历怎么写? - -简历上有一两个项目经历很正常,但是真正能把项目经历很好的展示给面试官的非常少。对于项目经历大家可以考虑从如下几点来写: - -1. 对项目整体设计的一个感受 -2. 在这个项目中你负责了什么、做了什么、担任了什么角色 -3. 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用 -4. 另外项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个棘手的问题的时候你是如何解决的又或者说你在这个项目用了什么技术实现了什么功能比如:用redis做缓存提高访问速度和并发量、使用消息队列削峰和降流等等。 - -## 专业技能该怎么写? - -先问一下你自己会什么,然后看看你意向的公司需要什么。一般HR可能并不太懂技术,所以他在筛选简历的时候可能就盯着你专业技能的关键词来看。对于公司有要求而你不会的技能,你可以花几天时间学习一下,然后在简历上可以写上自己了解这个技能。比如你可以这样写(下面这部分内容摘自我的简历,大家可以根据自己的情况做一些修改和完善): - -- 计算机网络、数据结构、算法、操作系统等课内基础知识:掌握 -- Java 基础知识:掌握 -- JVM 虚拟机(Java内存区域、虚拟机垃圾算法、虚拟垃圾收集器、JVM内存管理):掌握 -- 高并发、高可用、高性能系统开发:掌握 -- Struts2、Spring、Hibernate、Ajax、Mybatis、JQuery :掌握 -- SSH 整合、SSM 整合、 SOA 架构:掌握 -- Dubbo: 掌握 -- Zookeeper: 掌握 -- 常见消息队列: 掌握 -- Linux:掌握 -- MySQL常见优化手段:掌握 -- Spring Boot +Spring Cloud +Docker:了解 -- Hadoop 生态相关技术中的 HDFS、Storm、MapReduce、Hive、Hbase :了解 -- Python 基础、一些常见第三方库比如OpenCV、wxpy、wordcloud、matplotlib:熟悉 - -## 排版注意事项 - -1. 尽量简洁,不要太花里胡哨; -2. 一些技术名词不要弄错了大小写比如MySQL不要写成mysql,Java不要写成java。这个在我看来还是比较忌讳的,所以一定要注意这个细节; -3. 中文和数字英文之间加上空格的话看起来会舒服一点; - -## 其他的一些小tips - -1. 尽量避免主观表述,少一点语义模糊的形容词,尽量要简洁明了,逻辑结构清晰。 -2. 如果自己有博客或者个人技术栈点的话,写上去会为你加分很多。 -3. 如果自己的Github比较活跃的话,写上去也会为你加分很多。 -4. 注意简历真实性,一定不要写自己不会的东西,或者带有欺骗性的内容 -5. 项目经历建议以时间倒序排序,另外项目经历不在于多,而在于有亮点。 -6. 如果内容过多的话,不需要非把内容压缩到一页,保持排版干净整洁就可以了。 -7. 简历最后最好能加上:“感谢您花时间阅读我的简历,期待能有机会和您共事。”这句话,显的你会很有礼貌。 - -## 推荐的工具/网站 - -- 冷熊简历(MarkDown在线简历工具,可在线预览、编辑和生成PDF): -- Typora+[Java程序员简历模板](https://github.com/geekcompany/ResumeSample/blob/master/java.md) diff --git "a/docs/essential-content-for-interview/PreparingForInterview/\347\276\216\345\233\242\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\346\200\273\347\273\223.md" "b/docs/essential-content-for-interview/PreparingForInterview/\347\276\216\345\233\242\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\346\200\273\347\273\223.md" deleted file mode 100644 index fcbc75d..0000000 --- "a/docs/essential-content-for-interview/PreparingForInterview/\347\276\216\345\233\242\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230\346\200\273\347\273\223.md" +++ /dev/null @@ -1,925 +0,0 @@ - - -- [一 基础篇](#一-基础篇) - - [1. `System.out.println(3|9)`输出什么?](#1-systemoutprintln39输出什么) - - [2. 说一下转发(Forward)和重定向(Redirect)的区别](#2-说一下转发forward和重定向redirect的区别) - - [3. 在浏览器中输入 url 地址到显示主页的过程,整个过程会使用哪些协议](#3-在浏览器中输入-url-地址到显示主页的过程整个过程会使用哪些协议) - - [4. TCP 三次握手和四次挥手](#4-tcp-三次握手和四次挥手) - - [为什么要三次握手](#为什么要三次握手) - - [为什么要传回 SYN](#为什么要传回-syn) - - [传了 SYN,为啥还要传 ACK](#传了-syn为啥还要传-ack) - - [为什么要四次挥手](#为什么要四次挥手) - - [5. IP 地址与 MAC 地址的区别](#5-ip-地址与-mac-地址的区别) - - [6. HTTP 请求,响应报文格式](#6-http-请求响应报文格式) - - [7. 为什么要使用索引?索引这么多优点,为什么不对表中的每一个列创建一个索引呢?索引是如何提高查询速度的?说一下使用索引的注意事项?Mysql 索引主要使用的两种数据结构?什么是覆盖索引?](#7-为什么要使用索引索引这么多优点为什么不对表中的每一个列创建一个索引呢索引是如何提高查询速度的说一下使用索引的注意事项mysql-索引主要使用的两种数据结构什么是覆盖索引) - - [8. 进程与线程的区别是什么?进程间的几种通信方式说一下?线程间的几种通信方式知道不?](#8-进程与线程的区别是什么进程间的几种通信方式说一下线程间的几种通信方式知道不) - - [9. 为什么要用单例模式?手写几种线程安全的单例模式?](#9-为什么要用单例模式手写几种线程安全的单例模式) - - [10. 简单介绍一下 bean;知道 Spring 的 bean 的作用域与生命周期吗?](#10-简单介绍一下-bean知道-spring-的-bean-的作用域与生命周期吗) - - [11. Spring 中的事务传播行为了解吗?TransactionDefinition 接口中哪五个表示隔离级别的常量?](#11-spring-中的事务传播行为了解吗transactiondefinition-接口中哪五个表示隔离级别的常量) - - [事务传播行为](#事务传播行为) - - [隔离级别](#隔离级别) - - [12. SpringMVC 原理了解吗?](#12-springmvc-原理了解吗) - - [13. Spring AOP IOC 实现原理](#13-spring-aop-ioc-实现原理) -- [二 进阶篇](#二-进阶篇) - - [1 消息队列 MQ 的套路](#1-消息队列-mq-的套路) - - [1.1 介绍一下消息队列 MQ 的应用场景/使用消息队列的好处](#11-介绍一下消息队列-mq-的应用场景使用消息队列的好处) - - [1)通过异步处理提高系统性能](#1通过异步处理提高系统性能) - - [2)降低系统耦合性](#2降低系统耦合性) - - [1.2 那么使用消息队列会带来什么问题?考虑过这些问题吗?](#12-那么使用消息队列会带来什么问题考虑过这些问题吗) - - [1.3 介绍一下你知道哪几种消息队列,该如何选择呢?](#13-介绍一下你知道哪几种消息队列该如何选择呢) - - [1.4 关于消息队列其他一些常见的问题展望](#14-关于消息队列其他一些常见的问题展望) - - [2 谈谈 InnoDB 和 MyIsam 两者的区别](#2-谈谈-innodb-和-myisam-两者的区别) - - [2.1 两者的对比](#21-两者的对比) - - [2.2 关于两者的总结](#22-关于两者的总结) - - [3 聊聊 Java 中的集合吧!](#3-聊聊-java-中的集合吧) - - [3.1 Arraylist 与 LinkedList 有什么不同?(注意加上从数据结构分析的内容)](#31-arraylist-与-linkedlist-有什么不同注意加上从数据结构分析的内容) - - [3.2 HashMap 的底层实现](#32-hashmap-的底层实现) - - [1)JDK1.8 之前](#1jdk18-之前) - - [2)JDK1.8 之后](#2jdk18-之后) - - [3.3 既然谈到了红黑树,你给我手绘一个出来吧,然后简单讲一下自己对于红黑树的理解](#33-既然谈到了红黑树你给我手绘一个出来吧然后简单讲一下自己对于红黑树的理解) - - [3.4 红黑树这么优秀,为何不直接使用红黑树得了?](#34-红黑树这么优秀为何不直接使用红黑树得了) - - [3.5 HashMap 和 Hashtable 的区别/HashSet 和 HashMap 区别](#35-hashmap-和-hashtable-的区别hashset-和-hashmap-区别) -- [三 终结篇](#三-终结篇) - - [1. Object 类有哪些方法?](#1-object-类有哪些方法) - - [1.1 Object 类的常见方法总结](#11-object-类的常见方法总结) - - [1.2 hashCode 与 equals](#12-hashcode-与-equals) - - [1.2.1 hashCode()介绍](#121-hashcode介绍) - - [1.2.2 为什么要有 hashCode](#122-为什么要有-hashcode) - - [1.2.3 hashCode()与 equals()的相关规定](#123-hashcode与-equals的相关规定) - - [1.2.4 为什么两个对象有相同的 hashcode 值,它们也不一定是相等的?](#124-为什么两个对象有相同的-hashcode-值它们也不一定是相等的) - - [1.3 ==与 equals](#13-与-equals) - - [2 ConcurrentHashMap 相关问题](#2-concurrenthashmap-相关问题) - - [2.1 ConcurrentHashMap 和 Hashtable 的区别](#21-concurrenthashmap-和-hashtable-的区别) - - [2.2 ConcurrentHashMap 线程安全的具体实现方式/底层具体实现](#22-concurrenthashmap-线程安全的具体实现方式底层具体实现) - - [JDK1.7(上面有示意图)](#jdk17上面有示意图) - - [JDK1.8(上面有示意图)](#jdk18上面有示意图) - - [3 谈谈 synchronized 和 ReentrantLock 的区别](#3-谈谈-synchronized-和-reentrantlock-的区别) - - [4 线程池了解吗?](#4-线程池了解吗) - - [4.1 为什么要用线程池?](#41-为什么要用线程池) - - [4.2 Java 提供了哪几种线程池?他们各自的使用场景是什么?](#42-java-提供了哪几种线程池他们各自的使用场景是什么) - - [Java 主要提供了下面 4 种线程池](#java-主要提供了下面-4-种线程池) - - [各种线程池的适用场景介绍](#各种线程池的适用场景介绍) - - [4.3 创建的线程池的方式](#43-创建的线程池的方式) - - [5 Nginx](#5-nginx) - - [5.1 简单介绍一下 Nginx](#51-简单介绍一下-nginx) - - [反向代理](#反向代理) - - [负载均衡](#负载均衡) - - [动静分离](#动静分离) - - [5.2 为什么要用 Nginx?](#52-为什么要用-nginx) - - [5.3 Nginx 的四个主要组成部分了解吗?](#53-nginx-的四个主要组成部分了解吗) - - - -这些问题是 2018 年去美团面试的同学被问到的一些常见的问题,希望对你有帮助! - -# 一 基础篇 - -## 1. `System.out.println(3|9)`输出什么? - -正确答案:11。 - -**考察知识点:&和&&;|和||** - -**&和&&:** - -共同点:两者都可做逻辑运算符。它们都表示运算符的两边都是 true 时,结果为 true; - -不同点: &也是位运算符。& 表示在运算时两边都会计算,然后再判断;&&表示先运算符号左边的东西,然后判断是否为 true,是 true 就继续运算右边的然后判断并输出,是 false 就停下来直接输出不会再运行后面的东西。 - -**|和||:** - -共同点:两者都可做逻辑运算符。它们都表示运算符的两边任意一边为 true,结果为 true,两边都不是 true,结果就为 false; - -不同点:|也是位运算符。| 表示两边都会运算,然后再判断结果;|| 表示先运算符号左边的东西,然后判断是否为 true,是 true 就停下来直接输出不会再运行后面的东西,是 false 就继续运算右边的然后判断并输出。 - -**回到本题:** - -3 | 9=0011(二进制) | 1001(二进制)=1011(二进制)=11(十进制) - -## 2. 说一下转发(Forward)和重定向(Redirect)的区别 - -**转发是服务器行为,重定向是客户端行为。** - -**转发(Forword)** 通过 RequestDispatcher 对象的`forward(HttpServletRequest request,HttpServletResponse response)`方法实现的。`RequestDispatcher` 可以通过`HttpServletRequest` 的 `getRequestDispatcher()`方法获得。例如下面的代码就是跳转到 login_success.jsp 页面。 - -```java -request.getRequestDispatcher("login_success.jsp").forward(request, response); -``` - -**重定向(Redirect)** 是利用服务器返回的状态码来实现的。客户端浏览器请求服务器的时候,服务器会返回一个状态码。服务器通过 HttpServletRequestResponse 的 setStatus(int status)方法设置状态码。如果服务器返回 301 或者 302,则浏览器会到新的网址重新请求该资源。 - -1. **从地址栏显示来说**:forward 是服务器请求资源,服务器直接访问目标地址的 URL,把那个 URL 的响应内容读取过来,然后把这些内容再发给浏览器。浏览器根本不知道服务器发送的内容从哪里来的,所以它的地址栏还是原来的地址。redirect 是服务端根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址。所以地址栏显示的是新的 URL。 -2. **从数据共享来说**:forward:转发页面和转发到的页面可以共享 request 里面的数据。redirect:不能共享数据。 -3. **从运用地方来说**:forward:一般用于用户登陆的时候,根据角色转发到相应的模块。redirect:一般用于用户注销登陆时返回主页面和跳转到其它的网站等。 -4. **从效率来说**:forward:高。redirect:低。 - -## 3. 在浏览器中输入 url 地址到显示主页的过程,整个过程会使用哪些协议 - -图片来源:《图解 HTTP》: - -![各种网络请求用到的协议](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/各种网络请求用到的协议.jpg) - -总体来说分为以下几个过程: - -1. DNS 解析 -2. TCP 连接 -3. 发送 HTTP 请求 -4. 服务器处理请求并返回 HTTP 报文 -5. 浏览器解析渲染页面 -6. 连接结束 - -具体可以参考下面这篇文章: - -- [https://segmentfault.com/a/1190000006879700](https://segmentfault.com/a/1190000006879700 "https://segmentfault.com/a/1190000006879700") - -> 修正 [issue-568](https://github.com/Snailclimb/JavaGuide/issues/568 "issue-568"):上图中 IP 数据包在路由器之间使用的协议为 OPSF 协议错误,应该为 OSPF 协议 。 -> -> IP 数据包在路由器之间传播大致分为 IGP 和 BGP 协议,而 IGP 目前主流为 OSPF 协议,思科,华为和 H3C 等主流厂商都有各自实现并使用;BGP 协议为不同 AS(自治系统号)间路由传输,也分为 I-BGP 和 E-BGP,详细资料请查看《TCP/IP 卷一》 - -## 4. TCP 三次握手和四次挥手 - -为了准确无误地把数据送达目标处,TCP 协议采用了三次握手策略。 - -**漫画图解:** - -图片来源:《图解 HTTP》 - - -**简单示意图:** - - -- 客户端–发送带有 SYN 标志的数据包–一次握手–服务端 -- 服务端–发送带有 SYN/ACK 标志的数据包–二次握手–客户端 -- 客户端–发送带有带有 ACK 标志的数据包–三次握手–服务端 - -#### 为什么要三次握手 - -**三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。** - -第一次握手:Client 什么都不能确认;Server 确认了对方发送正常,自己接收正常。 - -第二次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己接收正常,对方发送正常 - -第三次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己发送、接收正常,对方发送、接收正常 - -所以三次握手就能确认双发收发功能都正常,缺一不可。 - -#### 为什么要传回 SYN - -接收端传回发送端所发送的 SYN 是为了告诉发送端,我接收到的信息确实就是你所发送的信号了。 - -> SYN 是 TCP/IP 建立连接时使用的握手信号。在客户机和服务器之间建立正常的 TCP 网络连接时,客户机首先发出一个 SYN 消息,服务器使用 SYN-ACK 应答表示接收到了这个消息,最后客户机再以 ACK(Acknowledgement[汉译:确认字符 ,在数据通信传输中,接收站发给发送站的一种传输控制字符。它表示确认发来的数据已经接受无误。 ])消息响应。这样在客户机和服务器之间才能建立起可靠的 TCP 连接,数据才可以在客户机和服务器之间传递。 - -#### 传了 SYN,为啥还要传 ACK - -双方通信无误必须是两者互相发送信息都无误。传了 SYN,证明发送方(主动关闭方)到接收方(被动关闭方)的通道没有问题,但是接收方到发送方的通道还需要 ACK 信号来进行验证。 - -断开一个 TCP 连接则需要“四次挥手”: - -- 客户端-发送一个 FIN,用来关闭客户端到服务器的数据传送 -- 服务器-收到这个 FIN,它发回一 个 ACK,确认序号为收到的序号加 1 。和 SYN 一样,一个 FIN 将占用一个序号 -- 服务器-关闭与客户端的连接,发送一个 FIN 给客户端 -- 客户端-发回 ACK 报文确认,并将确认序号设置为收到序号加 1 - -#### 为什么要四次挥手 - -任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后就完全关闭了 TCP 连接。 - -举个例子:A 和 B 打电话,通话即将结束后,A 说“我没啥要说的了”,B 回答“我知道了”,但是 B 可能还会有要说的话,A 不能要求 B 跟着自己的节奏结束通话,于是 B 可能又巴拉巴拉说了一通,最后 B 说“我说完了”,A 回答“知道了”,这样通话才算结束。 - -上面讲的比较概括,推荐一篇讲的比较细致的文章:[https://blog.csdn.net/qzcsu/article/details/72861891](https://blog.csdn.net/qzcsu/article/details/72861891 "https://blog.csdn.net/qzcsu/article/details/72861891") - -## 5. IP 地址与 MAC 地址的区别 - -参考:[https://blog.csdn.net/guoweimelon/article/details/50858597](https://blog.csdn.net/guoweimelon/article/details/50858597 "https://blog.csdn.net/guoweimelon/article/details/50858597") - -IP 地址是指互联网协议地址(Internet Protocol Address)IP Address 的缩写。IP 地址是 IP 协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。 - -MAC 地址又称为物理地址、硬件地址,用来定义网络设备的位置。网卡的物理地址通常是由网卡生产厂家写入网卡的,具有全球唯一性。MAC 地址用于在网络中唯一标示一个网卡,一台电脑会有一或多个网卡,每个网卡都需要有一个唯一的 MAC 地址。 - -## 6. HTTP 请求,响应报文格式 - -HTTP 请求报文主要由请求行、请求头部、请求正文 3 部分组成 - -HTTP 响应报文主要由状态行、响应头部、响应正文 3 部分组成 - -详细内容可以参考:[https://blog.csdn.net/a19881029/article/details/14002273](https://blog.csdn.net/a19881029/article/details/14002273 "https://blog.csdn.net/a19881029/article/details/14002273") - -## 7. 为什么要使用索引?索引这么多优点,为什么不对表中的每一个列创建一个索引呢?索引是如何提高查询速度的?说一下使用索引的注意事项?Mysql 索引主要使用的两种数据结构?什么是覆盖索引? - -**为什么要使用索引?** - -1. 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。 -2. 可以大大加快 数据的检索速度(大大减少的检索的数据量), 这也是创建索引的最主要的原因。 -3. 帮助服务器避免排序和临时表 -4. 将随机 IO 变为顺序 IO -5. 可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。 - -**索引这么多优点,为什么不对表中的每一个列创建一个索引呢?** - -1. 当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。 -2. 索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大。 -3. 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。 - -**索引是如何提高查询速度的?** - -将无序的数据变成相对有序的数据(就像查目录一样) - -**说一下使用索引的注意事项** - -1. 避免 where 子句中对字段施加函数,这会造成无法命中索引。 -2. 在使用 InnoDB 时使用与业务无关的自增主键作为主键,即使用逻辑主键,而不要使用业务主键。 -3. 将打算加索引的列设置为 NOT NULL ,否则将导致引擎放弃使用索引而进行全表扫描 -4. 删除长期未使用的索引,不用的索引的存在会造成不必要的性能损耗 MySQL 5.7 可以通过查询 sys 库的 schema_unused_indexes 视图来查询哪些索引从未被使用 -5. 在使用 limit offset 查询缓慢时,可以借助索引来提高性能 - -**Mysql 索引主要使用的哪两种数据结构?** - -- 哈希索引:对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择 BTree 索引。 -- BTree 索引:Mysql 的 BTree 索引使用的是 B 树中的 B+Tree。但对于主要的两种存储引擎(MyISAM 和 InnoDB)的实现方式是不同的。 - -更多关于索引的内容可以查看我的这篇文章:[【思维导图-索引篇】搞定数据库索引就是这么简单](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247484486&idx=1&sn=215450f11e042bca8a58eac9f4a97686&chksm=fd985227caefdb3117b8375f150676f5824aa20d1ebfdbcfb93ff06e23e26efbafae6cf6b48e&token=1990180468&lang=zh_CN#rd) - -**什么是覆盖索引?** - -如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称 -之为“覆盖索引”。我们知道在 InnoDB 存储引擎中,如果不是主键索引,叶子节点存储的是主键+列值。最终还是要“回表”,也就是要通过主键再查找一次,这样就会比较慢。覆盖索引就是把要查询出的列和索引是对应的,不做回表操作! - -## 8. 进程与线程的区别是什么?进程间的几种通信方式说一下?线程间的几种通信方式知道不? - -**进程与线程的区别是什么?** - -线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。另外,也正是因为共享资源,所以线程中执行时一般都要进行同步和互斥。总的来说,进程和线程的主要差别在于它们是不同的操作系统资源管理方式。 - -**进程间的几种通信方式说一下?** - -1. **管道(pipe)**:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有血缘关系的进程间使用。进程的血缘关系通常指父子进程关系。管道分为 pipe(无名管道)和 fifo(命名管道)两种,有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间通信。 -2. **信号量(semophore)**:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它通常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。 -3. **消息队列(message queue)**:消息队列是由消息组成的链表,存放在内核中 并由消息队列标识符标识。消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。消息队列与管道通信相比,其优势是对每个消息指定特定的消息类型,接收的时候不需要按照队列次序,而是可以根据自定义条件接收特定类型的消息。 -4. **信号(signal)**:信号是一种比较复杂的通信方式,用于通知接收进程某一事件已经发生。 -5. **共享内存(shared memory)**:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问,共享内存是最快的 IPC 方式,它是针对其他进程间的通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量配合使用,来实现进程间的同步和通信。 -6. **套接字(socket)**:socket,即套接字是一种通信机制,凭借这种机制,客户/服务器(即要进行通信的进程)系统的开发工作既可以在本地单机上进行,也可以跨网络进行。也就是说它可以让不在同一台计算机但通过网络连接计算机上的进程进行通信。也因为这样,套接字明确地将客户端和服务器区分开来。 - -**线程间的几种通信方式知道不?** - -1、锁机制 - -- 互斥锁:提供了以排它方式阻止数据结构被并发修改的方法。 -- 读写锁:允许多个线程同时读共享数据,而对写操作互斥。 -- 条件变量:可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。 - -2、信号量机制:包括无名线程信号量与有名线程信号量 - -3、信号机制:类似于进程间的信号处理。 - -线程间通信的主要目的是用于线程同步,所以线程没有象进程通信中用于数据交换的通信机制。 - -## 9. 为什么要用单例模式?手写几种线程安全的单例模式? - -**简单来说使用单例模式可以带来下面几个好处:** - -- 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销; -- 由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。 - -**懒汉式(双重检查加锁版本)** - -```java -public class Singleton { - - //volatile保证,当uniqueInstance变量被初始化成Singleton实例时,多个线程可以正确处理uniqueInstance变量 - private volatile static Singleton uniqueInstance; - private Singleton() { - } - public static Singleton getInstance() { - //检查实例,如果不存在,就进入同步代码块 - if (uniqueInstance == null) { - //只有第一次才彻底执行这里的代码 - synchronized(Singleton.class) { - //进入同步代码块后,再检查一次,如果仍是null,才创建实例 - if (uniqueInstance == null) { - uniqueInstance = new Singleton(); - } - } - } - return uniqueInstance; - } -} -``` - -**静态内部类方式** - -静态内部实现的单例是懒加载的且线程安全。 - -只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance(只有第一次使用这个单例的实例的时候才加载,同时不会有线程安全问题)。 - -```java -public class Singleton { - private static class SingletonHolder { - private static final Singleton INSTANCE = new Singleton(); - } - private Singleton (){} - public static final Singleton getInstance() { - return SingletonHolder.INSTANCE; - } -} -``` - -## 10. 简单介绍一下 bean;知道 Spring 的 bean 的作用域与生命周期吗? - -在 Spring 中,那些组成应用程序的主体及由 Spring IOC 容器所管理的对象,被称之为 bean。简单地讲,bean 就是由 IOC 容器初始化、装配及管理的对象,除此之外,bean 就与应用程序中的其他对象没有什么区别了。而 bean 的定义以及 bean 相互间的依赖关系将通过配置元数据来描述。 - -Spring 中的 bean 默认都是单例的,这些单例 Bean 在多线程程序下如何保证线程安全呢? 例如对于 Web 应用来说,Web 容器对于每个用户请求都创建一个单独的 Sevlet 线程来处理请求,引入 Spring 框架之后,每个 Action 都是单例的,那么对于 Spring 托管的单例 Service Bean,如何保证其安全呢? Spring 的单例是基于 BeanFactory 也就是 Spring 容器的,单例 Bean 在此容器内只有一个,Java 的单例是基于 JVM,每个 JVM 内只有一个实例。 - -![pring的bean的作用域](https://user-gold-cdn.xitu.io/2018/11/10/166fd45773d5dd2e?w=563&h=299&f=webp&s=27930) - -Spring 的 bean 的生命周期以及更多内容可以查看:[一文轻松搞懂 Spring 中 bean 的作用域与生命周期](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247484400&idx=2&sn=7201eb365102fce017f89cb3527fb0bc&chksm=fd985591caefdc872a2fac897288119f94c345e4e12150774f960bf5f816b79e4b9b46be3d7f&token=1990180468&lang=zh_CN#rd) - -## 11. Spring 中的事务传播行为了解吗?TransactionDefinition 接口中哪五个表示隔离级别的常量? - -#### 事务传播行为 - -事务传播行为(为了解决业务层方法之间互相调用的事务问题): -当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。在 TransactionDefinition 定义中包括了如下几个表示传播行为的常量: - -**支持当前事务的情况:** - -- TransactionDefinition.PROPAGATION_REQUIRED: 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。 -- TransactionDefinition.PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。 -- TransactionDefinition.PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性) - -**不支持当前事务的情况:** - -- TransactionDefinition.PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。 -- TransactionDefinition.PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。 -- TransactionDefinition.PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。 - -**其他情况:** - -- TransactionDefinition.PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 TransactionDefinition.PROPAGATION_REQUIRED。 - -#### 隔离级别 - -TransactionDefinition 接口中定义了五个表示隔离级别的常量: - -- **TransactionDefinition.ISOLATION_DEFAULT:** 使用后端数据库默认的隔离级别,Mysql 默认采用的 REPEATABLE_READ 隔离级别 Oracle 默认采用的 READ_COMMITTED 隔离级别. -- **TransactionDefinition.ISOLATION_READ_UNCOMMITTED:** 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读 -- **TransactionDefinition.ISOLATION_READ_COMMITTED:** 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生 -- **TransactionDefinition.ISOLATION_REPEATABLE_READ:** 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。 -- **TransactionDefinition.ISOLATION_SERIALIZABLE:** 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。 - -## 12. SpringMVC 原理了解吗? - -![SpringMVC 原理](https://user-gold-cdn.xitu.io/2018/11/10/166fd45787394192?w=1015&h=466&f=webp&s=35352) - -客户端发送请求-> 前端控制器 DispatcherServlet 接受客户端请求 -> 找到处理器映射 HandlerMapping 解析请求对应的 Handler-> HandlerAdapter 会根据 Handler 来调用真正的处理器处理请求,并处理相应的业务逻辑 -> 处理器返回一个模型视图 ModelAndView -> 视图解析器进行解析 -> 返回一个视图对象->前端控制器 DispatcherServlet 渲染数据(Model)->将得到视图对象返回给用户 - -关于 SpringMVC 原理更多内容可以查看我的这篇文章:[SpringMVC 工作原理详解](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247484496&idx=1&sn=5472ffa687fe4a05f8900d8ee6726de4&chksm=fd985231caefdb27fc75b44ecf76b6f43e4617e0b01b3c040f8b8fab32e51dfa5118eed1d6ad&token=1990180468&lang=zh_CN#rd) - -## 13. Spring AOP IOC 实现原理 - -过了秋招挺长一段时间了,说实话我自己也忘了如何简要概括 Spring AOP IOC 实现原理,就在网上找了一个较为简洁的答案,下面分享给各位。 - -**IOC:** 控制反转也叫依赖注入。IOC 利用 java 反射机制,AOP 利用代理模式。IOC 概念看似很抽象,但是很容易理解。说简单点就是将对象交给容器管理,你只需要在 spring 配置文件中配置对应的 bean 以及设置相关的属性,让 spring 容器来生成类的实例对象以及管理对象。在 spring 容器启动的时候,spring 会把你在配置文件中配置的 bean 都初始化好,然后在你需要调用的时候,就把它已经初始化好的那些 bean 分配给你需要调用这些 bean 的类。 - -**AOP:** 面向切面编程。(Aspect-Oriented Programming) 。AOP 可以说是对 OOP 的补充和完善。OOP 引入封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。实现 AOP 的技术,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码,属于静态代理。 - -# 二 进阶篇 - -## 1 消息队列 MQ 的套路 - -消息队列/消息中间件应该是 Java 程序员必备的一个技能了,如果你之前没接触过消息队列的话,建议先去百度一下某某消息队列入门,然后花 2 个小时就差不多可以学会任何一种消息队列的使用了。如果说仅仅学会使用是万万不够的,在实际生产环境还要考虑消息丢失等等情况。关于消息队列面试相关的问题,推荐大家也可以看一下视频《Java 工程师面试突击第 1 季-中华石杉老师》,如果大家没有资源的话,可以在我的公众号“Java 面试通关手册”后台回复关键字“1”即可! - -### 1.1 介绍一下消息队列 MQ 的应用场景/使用消息队列的好处 - -面试官一般会先问你这个问题,预热一下,看你知道消息队列不,一般在第一面的时候面试官可能只会问消息队列 MQ 的应用场景/使用消息队列的好处、使用消息队列会带来什么问题、消息队列的技术选型这几个问题,不会太深究下去,在后面的第二轮/第三轮技术面试中可能会深入问一下。 - -**《大型网站技术架构》第四章和第七章均有提到消息队列对应用性能及扩展性的提升。** - -#### 1)通过异步处理提高系统性能 - -![通过异步处理提高系统性能](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/通过异步处理提高系统性能.jpg) -如上图,**在不使用消息队列服务器的时候,用户的请求数据直接写入数据库,在高并发的情况下数据库压力剧增,使得响应速度变慢。但是在使用消息队列之后,用户的请求数据发送给消息队列之后立即 返回,再由消息队列的消费者进程从消息队列中获取数据,异步写入数据库。由于消息队列服务器处理速度快于数据库(消息队列也比数据库有更好的伸缩性),因此响应速度得到大幅改善。** - -通过以上分析我们可以得出**消息队列具有很好的削峰作用的功能**——即**通过异步处理,将短时间高并发产生的事务消息存储在消息队列中,从而削平高峰期的并发事务。** 举例:在电子商务一些秒杀、促销活动中,合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击。如下图所示: -![合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击.jpg) -因为**用户请求数据写入消息队列之后就立即返回给用户了,但是请求数据在后续的业务校验、写数据库等操作中可能失败**。因此使用消息队列进行异步处理之后,需要**适当修改业务流程进行配合**,比如**用户在提交订单之后,订单数据写入消息队列,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进程真正处理完该订单之后,甚至出库后,再通过电子邮件或短信通知用户订单成功**,以免交易纠纷。这就类似我们平时手机订火车票和电影票。 - -#### 2)降低系统耦合性 - -我们知道模块分布式部署以后聚合方式通常有两种:1.**分布式消息队列**和 2.**分布式服务**。 - -> **先来简单说一下分布式服务:** - -目前使用比较多的用来构建**SOA(Service Oriented Architecture 面向服务体系结构)**的**分布式服务框架**是阿里巴巴开源的**Dubbo**。如果想深入了解 Dubbo 的可以看我写的关于 Dubbo 的这一篇文章:**《高性能优秀的服务框架-dubbo 介绍》**:[https://juejin.im/post/5acadeb1f265da2375072f9c](https://juejin.im/post/5acadeb1f265da2375072f9c "https://juejin.im/post/5acadeb1f265da2375072f9c") - -> **再来谈我们的分布式消息队列:** - -我们知道如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性无疑更好一些。 - -我们最常见的**事件驱动架构**类似生产者消费者模式,在大型网站中通常用利用消息队列实现事件驱动结构。如下图所示: - -![利用消息队列实现事件驱动结构](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/利用消息队列实现事件驱动结构.jpg) -**消息队列使利用发布-订阅模式工作,消息发送者(生产者)发布消息,一个或多个消息接受者(消费者)订阅消息。** 从上图可以看到**消息发送者(生产者)和消息接受者(消费者)之间没有直接耦合**,消息发送者将消息发送至分布式消息队列即结束对消息的处理,消息接受者从分布式消息队列获取该消息后进行后续处理,并不需要知道该消息从何而来。**对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,从而实现网站业务的可扩展性设计**。 - -消息接受者对消息进行过滤、处理、包装后,构造成一个新的消息类型,将消息继续发送出去,等待其他消息接受者订阅该消息。因此基于事件(消息对象)驱动的业务架构可以是一系列流程。 - -**另外为了避免消息队列服务器宕机造成消息丢失,会将成功发送到消息队列的消息存储在消息生产者服务器上,等消息真正被消费者服务器处理后才删除消息。在消息队列服务器宕机后,生产者服务器会选择分布式消息队列服务器集群中的其他服务器发布消息。** - -**备注:** 不要认为消息队列只能利用发布-订阅模式工作,只不过在解耦这个特定业务环境下是使用发布-订阅模式的,**比如在我们的 ActiveMQ 消息队列中还有点对点工作模式**,具体的会在后面的文章给大家详细介绍,这一篇文章主要还是让大家对消息队列有一个更透彻的了解。 - -> 这个问题一般会在上一个问题问完之后,紧接着被问到。“使用消息队列会带来什么问题?”这个问题要引起重视,一般我们都会考虑使用消息队列会带来的好处而忽略它带来的问题! - -### 1.2 那么使用消息队列会带来什么问题?考虑过这些问题吗? - -- **系统可用性降低:** 系统可用性在某种程度上降低,为什么这样说呢?在加入 MQ 之前,你不用考虑消息丢失或者说 MQ 挂掉等等的情况,但是,引入 MQ 之后你就需要去考虑了! -- **系统复杂性提高:** 加入 MQ 之后,你需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺序性等等问题! -- **一致性问题:** 我上面讲了消息队列可以实现异步,消息队列带来的异步确实可以提高系统响应速度。但是,万一消息的真正消费者并没有正确消费消息怎么办?这样就会导致数据不一致的情况了! - -> 了解下面这个问题是为了我们更好的进行技术选型!该部分摘自:《Java 工程师面试突击第 1 季-中华石杉老师》,如果大家没有资源的话,可以在我的公众号“Java 面试通关手册”后台回复关键字“1”即可! - -### 1.3 介绍一下你知道哪几种消息队列,该如何选择呢? - -| 特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka | -| :----------------------- | -----------------------------------------------------------: | -----------------------------------------------------------: | -----------------------------------------------------------: | -----------------------------------------------------------: | -| 单机吞吐量 | 万级,吞吐量比 RocketMQ 和 Kafka 要低了一个数量级 | 万级,吞吐量比 RocketMQ 和 Kafka 要低了一个数量级 | 10 万级,RocketMQ 也是可以支撑高吞吐的一种 MQ | 10 万级别,这是 kafka 最大的优点,就是吞吐量高。一般配合大数据类的系统来进行实时数据计算、日志采集等场景 | -| topic 数量对吞吐量的影响 | | | topic 可以达到几百,几千个的级别,吞吐量会有较小幅度的下降这是 RocketMQ 的一大优势,在同等机器下,可以支撑大量的 topic | topic 从几十个到几百个的时候,吞吐量会大幅度下降。所以在同等机器下,kafka 尽量保证 topic 数量不要过多。如果要支撑大规模 topic,需要增加更多的机器资源 | -| 可用性 | 高,基于主从架构实现高可用性 | 高,基于主从架构实现高可用性 | 非常高,分布式架构 | 非常高,kafka 是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 | -| 消息可靠性 | 有较低的概率丢失数据 | | 经过参数优化配置,可以做到 0 丢失 | 经过参数优化配置,消息可以做到 0 丢失 | -| 时效性 | ms 级 | 微秒级,这是 rabbitmq 的一大特点,延迟是最低的 | ms 级 | 延迟在 ms 级以内 | -| 功能支持 | MQ 领域的功能极其完备 | 基于 erlang 开发,所以并发能力很强,性能极其好,延时很低 | MQ 功能较为完善,还是分布式的,扩展性好 | 功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用,是事实上的标准 | -| 优劣势总结 | 非常成熟,功能强大,在业内大量的公司以及项目中都有应用。偶尔会有较低概率丢失消息,而且现在社区以及国内应用都越来越少,官方社区现在对 ActiveMQ 5.x 维护越来越少,几个月才发布一个版本而且确实主要是基于解耦和异步来用的,较少在大规模吞吐的场景中使用 | erlang 语言开发,性能极其好,延时很低;吞吐量到万级,MQ 功能比较完备而且开源提供的管理界面非常棒,用起来很好用。社区相对比较活跃,几乎每个月都发布几个版本分在国内一些互联网公司近几年用 rabbitmq 也比较多一些但是问题也是显而易见的,RabbitMQ 确实吞吐量会低一些,这是因为他做的实现机制比较重。而且 erlang 开发,国内有几个公司有实力做 erlang 源码级别的研究和定制?如果说你没这个实力的话,确实偶尔会有一些问题,你很难去看懂源码,你公司对这个东西的掌控很弱,基本职能依赖于开源社区的快速维护和修复 bug。而且 rabbitmq 集群动态扩展会很麻烦,不过这个我觉得还好。其实主要是 erlang 语言本身带来的问题。很难读源码,很难定制和掌控。 | 接口简单易用,而且毕竟在阿里大规模应用过,有阿里品牌保障。日处理消息上百亿之多,可以做到大规模吞吐,性能也非常好,分布式扩展也很方便,社区维护还可以,可靠性和可用性都是 ok 的,还可以支撑大规模的 topic 数量,支持复杂 MQ 业务场景。而且一个很大的优势在于,阿里出品都是 java 系的,我们可以自己阅读源码,定制自己公司的 MQ,可以掌控。社区活跃度相对较为一般,不过也还可以,文档相对来说简单一些,然后接口这块不是按照标准 JMS 规范走的有些系统要迁移需要修改大量代码。还有就是阿里出台的技术,你得做好这个技术万一被抛弃,社区黄掉的风险,那如果你们公司有技术实力我觉得用 RocketMQ 挺好的 | kafka 的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms 级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展。同时 kafka 最好是支撑较少的 topic 数量即可,保证其超高吞吐量。而且 kafka 唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略这个特性天然适合大数据实时计算以及日志收集。 | - -> 这部分内容,我这里不给出答案,大家可以自行根据自己学习的消息队列查阅相关内容,我可能会在后面的文章中介绍到这部分内容。另外,下面这些问题在视频《Java 工程师面试突击第 1 季-中华石杉老师》中都有提到,如果大家没有资源的话,可以在我的公众号“Java 面试通关手册”后台回复关键字“1”即可! - -### 1.4 关于消息队列其他一些常见的问题展望 - -1. 引入消息队列之后如何保证高可用性? -2. 如何保证消息不被重复消费呢? -3. 如何保证消息的可靠性传输(如何处理消息丢失的问题)? -4. 我该怎么保证从消息队列里拿到的数据按顺序执行? -5. 如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,说说怎么解决? -6. 如果让你来开发一个消息队列中间件,你会怎么设计架构? - -## 2 谈谈 InnoDB 和 MyIsam 两者的区别 - -### 2.1 两者的对比 - -1. **count 运算上的区别:** 因为 MyISAM 缓存有表 meta-data(行数等),因此在做 COUNT(\*)时对于一个结构很好的查询是不需要消耗多少资源的。而对于 InnoDB 来说,则没有这种缓存 -2. **是否支持事务和崩溃后的安全恢复:** MyISAM 强调的是性能,每次查询具有原子性,其执行速度比 InnoDB 类型更快,但是不提供事务支持。但是 InnoDB 提供事务支持,外部键等高级数据库功能。 具有事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant))型表。 -3. **是否支持外键:** MyISAM 不支持,而 InnoDB 支持。 - -### 2.2 关于两者的总结 - -MyISAM 更适合读密集的表,而 InnoDB 更适合写密集的表。 在数据库做主从分离的情况下,经常选择 MyISAM 作为主库的存储引擎。 - -一般来说,如果需要事务支持,并且有较高的并发读取频率(MyISAM 的表锁的粒度太大,所以当该表写并发量较高时,要等待的查询就会很多了),InnoDB 是不错的选择。如果你的数据量很大(MyISAM 支持压缩特性可以减少磁盘的空间占用),而且不需要支持事务时,MyISAM 是最好的选择。 - -## 3 聊聊 Java 中的集合吧! - -### 3.1 Arraylist 与 LinkedList 有什么不同?(注意加上从数据结构分析的内容) - -- **1. 是否保证线程安全:** ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全; -- **2. 底层数据结构:** Arraylist 底层使用的是 Object 数组;LinkedList 底层使用的是双向链表数据结构(注意双向链表和双向循环链表的区别:); -- **3. 插入和删除是否受元素位置的影响:** ① **ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。** 比如:执行`add(E e)`方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(`add(int index, E element)`)时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② **LinkedList 采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1) 而数组为近似 O(n) 。** -- **4. 是否支持快速随机访问:** LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于`get(int index)`方法)。 -- **5. 内存空间占用:** ArrayList 的空 间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。 - -**补充内容:RandomAccess 接口** - -```java -public interface RandomAccess { -} -``` - -查看源码我们发现实际上 RandomAccess 接口中什么都没有定义。所以,在我看来 RandomAccess 接口不过是一个标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。 - -在 binarySearch() 方法中,它要判断传入的 list 是否 RamdomAccess 的实例,如果是,调用 indexedBinarySearch() 方法,如果不是,那么调用 iteratorBinarySearch() 方法 - -```java - public static - int binarySearch(List> list, T key) { - if (list instanceof RandomAccess || list.size() Java 中的集合这类问题几乎是面试必问的,问到这类问题的时候,HashMap 又是几乎必问的问题,所以大家一定要引起重视! - -### 3.2 HashMap 的底层实现 - -#### 1)JDK1.8 之前 - -JDK1.8 之前 HashMap 底层是 **数组和链表** 结合在一起使用也就是 **链表散列**。**HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 `(n - 1) & hash` 判断当前元素存放的位置(这里的 n 指的时数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。** - -**所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。** - -**JDK 1.8 HashMap 的 hash 方法源码:** - -JDK 1.8 的 hash 方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。 - -```java - static final int hash(Object key) { - int h; - // key.hashCode():返回散列值也就是hashcode - // ^ :按位异或 - // >>>:无符号右移,忽略符号位,空位都以0补齐 - return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); - } -``` - -对比一下 JDK1.7 的 HashMap 的 hash 方法源码. - -```java -static int hash(int h) { - // This function ensures that hashCodes that differ only by - // constant multiples at each bit position have a bounded - // number of collisions (approximately 8 at default load factor). - - h ^= (h >>> 20) ^ (h >>> 12); - return h ^ (h >>> 7) ^ (h >>> 4); -} -``` - -相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。 - -所谓 **“拉链法”** 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。 - -![jdk1.8之前的内部结构-HashMap](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/jdk1.8之前的内部结构-HashMap.jpg) - -#### 2)JDK1.8 之后 - -相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间。 - -![jdk1.8之后的内部结构-HashMap](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/JDK1.8之后的HashMap底层数据结构.jpg) - -TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。 - -> 问完 HashMap 的底层原理之后,面试官可能就会紧接着问你 HashMap 底层数据结构相关的问题! - -### 3.3 既然谈到了红黑树,你给我手绘一个出来吧,然后简单讲一下自己对于红黑树的理解 - -![红黑树](https://user-gold-cdn.xitu.io/2018/11/14/16711ac29c138cba?w=851&h=614&f=jpeg&s=34458) - -**红黑树特点:** - -1. 每个节点非红即黑; -2. 根节点总是黑色的; -3. 每个叶子节点都是黑色的空节点(NIL 节点); -4. 如果节点是红色的,则它的子节点必须是黑色的(反之不一定); -5. 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度) - -**红黑树的应用:** - -TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都用到了红黑树。 - -**为什么要用红黑树** - -简单来说红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。 - -### 3.4 红黑树这么优秀,为何不直接使用红黑树得了? - -说一下自己对于这个问题的看法:我们知道红黑树属于(自)平衡二叉树,但是为了保持“平衡”是需要付出代价的,红黑树在插入新数据后可能需要通过左旋,右旋、变色这些操作来保持平衡,这费事啊。你说说我们引入红黑树就是为了查找数据快,如果链表长度很短的话,根本不需要引入红黑树的,你引入之后还要付出代价维持它的平衡。但是链表过长就不一样了。至于为什么选 8 这个值呢?通过概率统计所得,这个值是综合查询成本和新增元素成本得出的最好的一个值。 - -### 3.5 HashMap 和 Hashtable 的区别/HashSet 和 HashMap 区别 - -**HashMap 和 Hashtable 的区别** - -1. **线程是否安全:** HashMap 是非线程安全的,Hashtable 是线程安全的;Hashtable 内部的方法基本都经过 `synchronized` 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!); -2. **效率:** 因为线程安全的问题,HashMap 要比 Hashtable 效率高一点。另外,Hashtable 基本被淘汰,不要在代码中使用它; -3. **对 Null key 和 Null value 的支持:** HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 Hashtable 中 put 进的键值只要有一个 null,直接抛出 NullPointerException。 -4. **初始容量大小和每次扩充容量大小的不同 :** ① 创建时如果不指定容量初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小(HashMap 中的`tableSizeFor()`方法保证,下面给出了源代码)。也就是说 HashMap 总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。 -5. **底层数据结构:** JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。 - -**HashSet 和 HashMap 区别** - -如果你看过 HashSet 源码的话就应该知道:HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常少,因为除了 clone() 方法、writeObject()方法、readObject()方法是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。) - -![HashSet 和 HashMap 区别](https://user-gold-cdn.xitu.io/2018/3/2/161e717d734f3b23?w=896&h=363&f=jpeg&s=205536) - -# 三 终结篇 - -## 1. Object 类有哪些方法? - -这个问题,面试中经常出现。我觉得不论是出于应付面试还是说更好地掌握 Java 这门编程语言,大家都要掌握! - -### 1.1 Object 类的常见方法总结 - -Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法: - -```java - -public final native Class getClass()//native方法,用于返回当前运行时对象的Class对象,使用了final关键字修饰,故不允许子类重写。 - -public native int hashCode() //native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的HashMap。 -public boolean equals(Object obj)//用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用户比较字符串的值是否相等。 - -protected native Object clone() throws CloneNotSupportedException//naitive方法,用于创建并返回当前对象的一份拷贝。一般情况下,对于任何对象 x,表达式 x.clone() != x 为true,x.clone().getClass() == x.getClass() 为true。Object本身没有实现Cloneable接口,所以不重写clone方法并且进行调用的话会发生CloneNotSupportedException异常。 - -public String toString()//返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方法。 - -public final native void notify()//native方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。 - -public final native void notifyAll()//native方法,并且不能重写。跟notify一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。 - -public final native void wait(long timeout) throws InterruptedException//native方法,并且不能重写。暂停线程的执行。注意:sleep方法没有释放锁,而wait方法释放了锁 。timeout是等待时间。 - -public final void wait(long timeout, int nanos) throws InterruptedException//多了nanos参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上nanos毫秒。 - -public final void wait() throws InterruptedException//跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念 - -protected void finalize() throws Throwable { }//实例被垃圾回收器回收的时候触发的操作 - -``` - -> 问完上面这个问题之后,面试官很可能紧接着就会问你“hashCode 与 equals”相关的问题。 - -### 1.2 hashCode 与 equals - -面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写 equals 时必须重写 hashCode 方法?” - -#### 1.2.1 hashCode()介绍 - -hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在 JDK 的 Object.java 中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。另外需要注意的是: Object 的 hashcode 方法是本地方法,也就是用 c 语言或 c++ 实现的,该方法通常用来将对象的 内存地址 转换为整数之后返回。 - -```java - public native int hashCode(); -``` - -散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象) - -#### 1.2.2 为什么要有 hashCode - -**我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:** - -当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的 Java 启蒙书《Head fist java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。 - -#### 1.2.3 hashCode()与 equals()的相关规定 - -1. 如果两个对象相等,则 hashcode 一定也是相同的 -2. 两个对象相等,对两个对象分别调用 equals 方法都返回 true -3. 两个对象有相同的 hashcode 值,它们也不一定是相等的 -4. **因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖** -5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据) - -#### 1.2.4 为什么两个对象有相同的 hashcode 值,它们也不一定是相等的? - -在这里解释一位小伙伴的问题。以下内容摘自《Head Fisrt Java》。 - -因为 hashCode() 所使用的杂凑算法也许刚好会让多个对象传回相同的杂凑值。越糟糕的杂凑算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同的 hashCode)。 - -我们刚刚也提到了 HashSet,如果 HashSet 在对比的时候,同样的 hashcode 有多个对象,它会使用 equals() 来判断是否真的相同。也就是说 hashcode 只是用来缩小查找成本。 - -> ==与 equals 的对比也是比较常问的基础问题之一! - -### 1.3 ==与 equals - -**==** : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型==比较的是值,引用数据类型==比较的是内存地址) - -**equals()** : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况: - -- 情况 1:类没有覆盖 equals()方法。则通过 equals()比较该类的两个对象时,等价于通过“==”比较这两个对象。 -- 情况 2:类覆盖了 equals()方法。一般,我们都覆盖 equals()方法来两个对象的内容相等;若它们的内容相等,则返回 true(即,认为这两个对象相等)。 - -**举个例子:** - -```java -public class test1 { - public static void main(String[] args) { - String a = new String("ab"); // a 为一个引用 - String b = new String("ab"); // b为另一个引用,对象的内容一样 - String aa = "ab"; // 放在常量池中 - String bb = "ab"; // 从常量池中查找 - if (aa == bb) // true - System.out.println("aa==bb"); - if (a == b) // false,非同一对象 - System.out.println("a==b"); - if (a.equals(b)) // true - System.out.println("aEQb"); - if (42 == 42.0) { // true - System.out.println("true"); - } - } -} -``` - -**说明:** - -- String 中的 equals()方法是被重写过的,因为 Object 的 equals()方法是比较的对象的内存地址,而 String 的 equals()方法比较的是对象的值。 -- 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。 - -> 在[【备战春招/秋招系列 5】美团面经总结进阶篇 (附详解答案)](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247484625&idx=1&sn=9c4fa1f7d4291a5fbd7daa44bac2b012&chksm=fd9852b0caefdba6edcf9a827aa4a17ddc97bf6ad2e5ee6f7e1aa1b443b54444d05d2b76732b&token=723699735&lang=zh_CN#rd) 这篇文章中,我们已经提到了一下关于 HashMap 在面试中常见的问题:HashMap 的底层实现、简单讲一下自己对于红黑树的理解、红黑树这么优秀,为何不直接使用红黑树得了、HashMap 和 Hashtable 的区别/HashSet 和 HashMap 区别。HashMap 和 ConcurrentHashMap 这俩兄弟在一般只要面试中问到集合相关的问题就一定会被问到,所以各位务必引起重视! - -## 2 ConcurrentHashMap 相关问题 - -### 2.1 ConcurrentHashMap 和 Hashtable 的区别 - -ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。 - -- **底层数据结构:** JDK1.7 的 ConcurrentHashMap 底层采用 **分段的数组+链表** 实现,JDK1.8 采用的数据结构跟 HashMap1.8 的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 **数组+链表** 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的; -- **实现线程安全的方式(重要):** ① **在 JDK1.7 的时候,ConcurrentHashMap(分段锁)** 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配 16 个 Segment,比 Hashtable 效率提高 16 倍。) **到了 JDK1.8 的时候已经摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6 以后 对 synchronized 锁做了很多优化)** 整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② **Hashtable(同一把锁)**:使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。 - -**两者的对比图:** - -图片来源:http://www.cnblogs.com/chengxiao/p/6842045.html - -Hashtable: -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-22/50656681.jpg) - -JDK1.7 的 ConcurrentHashMap: -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-22/33120488.jpg) -JDK1.8 的 ConcurrentHashMap(TreeBin: 红黑二叉树节点 -Node: 链表节点): -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-22/97739220.jpg) - -### 2.2 ConcurrentHashMap 线程安全的具体实现方式/底层具体实现 - -#### JDK1.7(上面有示意图) - -首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。 - -**ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成**。 - -Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。 - -```java -static class Segment extends ReentrantLock implements Serializable { -} -``` - -一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和 HashMap 类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 的锁。 - -#### JDK1.8(上面有示意图) - -ConcurrentHashMap 取消了 Segment 分段锁,采用 CAS 和 synchronized 来保证并发安全。数据结构跟 HashMap1.8 的结构类似,数组+链表/红黑二叉树。 - -synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。 - -## 3 谈谈 synchronized 和 ReentrantLock 的区别 - -**① 两者都是可重入锁** - -两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。 - -**② synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API** - -synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。 - -**③ ReentrantLock 比 synchronized 增加了一些高级功能** - -相比 synchronized,ReentrantLock 增加了一些高级功能。主要来说主要有三点:**① 等待可中断;② 可实现公平锁;③ 可实现选择性通知(锁可以绑定多个条件)** - -- **ReentrantLock 提供了一种能够中断等待锁的线程的机制**,通过 lock.lockInterruptibly() 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 -- **ReentrantLock 可以指定是公平锁还是非公平锁。而 synchronized 只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。** ReentrantLock 默认情况是非公平的,可以通过 ReentrantLock 类的`ReentrantLock(boolean fair)`构造方法来制定是否是公平的。 -- synchronized 关键字与 wait()和 notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock 类当然也可以实现,但是需要借助于 Condition 接口与 newCondition() 方法。Condition 是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个 Lock 对象中可以创建多个 Condition 实例(即对象监视器),**线程对象可以注册在指定的 Condition 中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用 notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用 ReentrantLock 类结合 Condition 实例可以实现“选择性通知”** ,这个功能非常重要,而且是 Condition 接口默认提供的。而 synchronized 关键字就相当于整个 Lock 对象中只有一个 Condition 实例,所有的线程都注册在它一个身上。如果执行 notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而 Condition 实例的 signalAll()方法 只会唤醒注册在该 Condition 实例中的所有等待线程。 - -如果你想使用上述功能,那么选择 ReentrantLock 是一个不错的选择。 - -**④ 两者的性能已经相差无几** - -在 JDK1.6 之前,synchronized 的性能是比 ReentrantLock 差很多。具体表示为:synchronized 关键字吞吐量岁线程数的增加,下降得非常严重。而 ReentrantLock 基本保持一个比较稳定的水平。我觉得这也侧面反映了, synchronized 关键字还有非常大的优化余地。后续的技术发展也证明了这一点,我们上面也讲了在 JDK1.6 之后 JVM 团队对 synchronized 关键字做了很多优化。JDK1.6 之后,synchronized 和 ReentrantLock 的性能基本是持平了。所以网上那些说因为性能才选择 ReentrantLock 的文章都是错的!JDK1.6 之后,性能已经不是选择 synchronized 和 ReentrantLock 的影响因素了!而且虚拟机在未来的性能改进中会更偏向于原生的 synchronized,所以还是提倡在 synchronized 能满足你的需求的情况下,优先考虑使用 synchronized 关键字来进行同步!优化后的 synchronized 和 ReentrantLock 一样,在很多地方都是用到了 CAS 操作。 - -## 4 线程池了解吗? - -### 4.1 为什么要用线程池? - -线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。 - -这里借用《Java 并发编程的艺术》提到的来说一下使用线程池的好处: - -- **降低资源消耗。** 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 -- **提高响应速度。** 当任务到达时,任务可以不需要的等到线程创建就能立即执行。 -- **提高线程的可管理性。** 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 - -### 4.2 Java 提供了哪几种线程池?他们各自的使用场景是什么? - -#### Java 主要提供了下面 4 种线程池 - -- **FixedThreadPool:** 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。 -- **SingleThreadExecutor:** 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。 -- **CachedThreadPool:** 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。 -- **ScheduledThreadPoolExecutor:** 主要用来在给定的延迟后运行任务,或者定期执行任务。ScheduledThreadPoolExecutor 又分为:ScheduledThreadPoolExecutor(包含多个线程)和 SingleThreadScheduledExecutor (只包含一个线程)两种。 - -#### 各种线程池的适用场景介绍 - -- **FixedThreadPool:** 适用于为了满足资源管理需求,而需要限制当前线程数量的应用场景。它适用于负载比较重的服务器; -- **SingleThreadExecutor:** 适用于需要保证顺序地执行各个任务并且在任意时间点,不会有多个线程是活动的应用场景; -- **CachedThreadPool:** 适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器; -- **ScheduledThreadPoolExecutor:** 适用于需要多个后台执行周期任务,同时为了满足资源管理需求而需要限制后台线程的数量的应用场景; -- **SingleThreadScheduledExecutor:** 适用于需要单个后台线程执行周期任务,同时保证顺序地执行各个任务的应用场景。 - -### 4.3 创建的线程池的方式 - -**(1) 使用 Executors 创建** - -我们上面刚刚提到了 Java 提供的几种线程池,通过 Executors 工具类我们可以很轻松的创建我们上面说的几种线程池。但是实际上我们一般都不是直接使用 Java 提供好的线程池,另外在《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 - -```java -Executors 返回线程池对象的弊端如下: - -FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。 -CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。 - -``` - -**(2) ThreadPoolExecutor 的构造函数创建** - -我们可以自己直接调用 ThreadPoolExecutor 的构造函数来自己创建线程池。在创建的同时,给 BlockQueue 指定容量就可以了。示例如下: - -```java -private static ExecutorService executor = new ThreadPoolExecutor(13, 13, - 60L, TimeUnit.SECONDS, - new ArrayBlockingQueue(13)); -``` - -这种情况下,一旦提交的线程数超过当前可用线程数时,就会抛出 java.util.concurrent.RejectedExecutionException,这是因为当前线程池使用的队列是有边界队列,队列已经满了便无法继续处理新的请求。但是异常(Exception)总比发生错误(Error)要好。 - -**(3) 使用开源类库** - -Hollis 大佬之前在他的文章中也提到了:“除了自己定义 ThreadPoolExecutor 外。还有其他方法。这个时候第一时间就应该想到开源类库,如 apache 和 guava 等。”他推荐使用 guava 提供的 ThreadFactoryBuilder 来创建线程池。下面是参考他的代码示例: - -```java -public class ExecutorsDemo { - - private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder() - .setNameFormat("demo-pool-%d").build(); - - private static ExecutorService pool = new ThreadPoolExecutor(5, 200, - 0L, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy()); - - public static void main(String[] args) { - - for (int i = 0; i < Integer.MAX_VALUE; i++) { - pool.execute(new SubThread()); - } - } -} -``` - -通过上述方式创建线程时,不仅可以避免 OOM 的问题,还可以自定义线程名称,更加方便的出错的时候溯源。 - -## 5 Nginx - -### 5.1 简单介绍一下 Nginx - -Nginx 是一款轻量级的 Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器。 Nginx 主要提供反向代理、负载均衡、动静分离(静态资源服务)等服务。下面我简单地介绍一下这些名词。 - -#### 反向代理 - -谈到反向代理,就不得不提一下正向代理。无论是正向代理,还是反向代理,说到底,就是代理模式的衍生版本罢了 - -- **正向代理:**某些情况下,代理我们用户去访问服务器,需要用户手动的设置代理服务器的 ip 和端口号。正向代理比较常见的一个例子就是 VPN 了。 -- **反向代理:** 是用来代理服务器的,代理我们要访问的目标服务器。代理服务器接受请求,然后将请求转发给内部网络的服务器,并将从服务器上得到的结果返回给客户端,此时代理服务器对外就表现为一个服务器。 - -通过下面两幅图,大家应该更好理解(图源:http://blog.720ui.com/2016/nginx_action_05_proxy/): - -![正向代理](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-15/60925795.jpg) - -![反向代理](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-11-15/62563930.jpg) - -所以,简单的理解,就是正向代理是为客户端做代理,代替客户端去访问服务器,而反向代理是为服务器做代理,代替服务器接受客户端请求。 - -#### 负载均衡 - -在高并发情况下需要使用,其原理就是将并发请求分摊到多个服务器执行,减轻每台服务器的压力,多台服务器(集群)共同完成工作任务,从而提高了数据的吞吐量。 - -Nginx 支持的 weight 轮询(默认)、ip_hash、fair、url_hash 这四种负载均衡调度算法,感兴趣的可以自行查阅。 - -负载均衡相比于反向代理更侧重的是将请求分担到多台服务器上去,所以谈论负载均衡只有在提供某服务的服务器大于两台时才有意义。 - -#### 动静分离 - -动静分离是让动态网站里的动态网页根据一定规则把不变的资源和经常变的资源区分开来,动静资源做好了拆分以后,我们就可以根据静态资源的特点将其做缓存操作,这就是网站静态化处理的核心思路。 - -### 5.2 为什么要用 Nginx? - -> 这部分内容参考极客时间—[Nginx 核心知识 100 讲的内容](https://time.geekbang.org/course/intro/138?code=AycjiiQk6uQRxnVJzBupFkrGkvZlmYELPRsZbWzaAHE= "Nginx核心知识100讲的内容")。 - -如果面试官问你这个问题,就一定想看你知道 Nginx 服务器的一些优点吗。 - -Nginx 有以下 5 个优点: - -1. 高并发、高性能(这是其他 web 服务器不具有的) -2. 可扩展性好(模块化设计,第三方插件生态圈丰富) -3. 高可靠性(可以在服务器行持续不间断的运行数年) -4. 热部署(这个功能对于 Nginx 来说特别重要,热部署指可以在不停止 Nginx 服务的情况下升级 Nginx) -5. BSD 许可证(意味着我们可以将源代码下载下来进行修改然后使用自己的版本) - -### 5.3 Nginx 的四个主要组成部分了解吗? - -> 这部分内容参考极客时间—[Nginx 核心知识 100 讲的内容](https://time.geekbang.org/course/intro/138?code=AycjiiQk6uQRxnVJzBupFkrGkvZlmYELPRsZbWzaAHE= "Nginx核心知识100讲的内容")。 - -- Nginx 二进制可执行文件:由各模块源码编译出一个文件 -- nginx.conf 配置文件:控制 Nginx 行为 -- acess.log 访问日志: 记录每一条 HTTP 请求信息 -- error.log 错误日志:定位问题 \ No newline at end of file diff --git "a/docs/essential-content-for-interview/PreparingForInterview/\351\235\242\350\257\225\345\256\230-\344\275\240\346\234\211\344\273\200\344\271\210\351\227\256\351\242\230\350\246\201\351\227\256\346\210\221.md" "b/docs/essential-content-for-interview/PreparingForInterview/\351\235\242\350\257\225\345\256\230-\344\275\240\346\234\211\344\273\200\344\271\210\351\227\256\351\242\230\350\246\201\351\227\256\346\210\221.md" deleted file mode 100644 index 7a55d53..0000000 --- "a/docs/essential-content-for-interview/PreparingForInterview/\351\235\242\350\257\225\345\256\230-\344\275\240\346\234\211\344\273\200\344\271\210\351\227\256\351\242\230\350\246\201\351\227\256\346\210\221.md" +++ /dev/null @@ -1,64 +0,0 @@ -我还记得当时我去参加面试的时候,几乎每一场面试,特别是HR面和高管面的时候,面试官总是会在结尾问我:“问了你这么多问题了,你有什么问题问我吗?”。这个时候很多人内心就会陷入短暂的纠结中:我该问吗?不问的话面试官会不会对我影响不好?问什么问题?问这个问题会不会让面试官对我的影响不好啊? - -![无奈](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-2/无奈.jpg) - -### 这个问题对最终面试结果的影响到底大不大? - -就技术面试而言,回答这个问题的时候,只要你不是触碰到你所面试的公司的雷区,那么我觉得这对你能不能拿到最终offer来说影响确实是不大的。我说这些并不代表你就可以直接对面试官说:“我没问题了。”,笔主当时面试的时候确实也说过挺多次“没问题要问了。”,最终也没有导致笔主被pass掉(可能是前面表现比较好,哈哈,自恋一下)。我现在回想起来,觉得自己当时做法其实挺不对的。面试本身就是一个双向选择的过程,你对这个问题的回答也会侧面反映出你对这次面试的上心程度,你的问题是否有价值,也影响了你最终的选择与公司是否选择你。 - -面试官在技术面试中主要考察的还是你这样个人到底有没有胜任这个工作的能力以及你是否适合公司未来的发展需要,很多公司还需要你认同它的文化,我觉得你只要不是太笨,应该不会栽在这里。除非你和另外一个人在能力上相同,但是只能在你们两个人中选一个,那么这个问题才对你能不能拿到offer至关重要。有准备总比没准备好,给面试官留一个好的影响总归是没错的。 - -但是,就非技术面试来说,我觉得好好回答这个问题对你最终的结果还是比较重要的。 - -总的来说不管是技术面试还是非技术面试,如果你想赢得公司的青睐和尊重,我觉得我们都应该重视这个问题。 - -### 真诚一点,不要问太 Low 的问题 - -回答这个问题很重要的一点就是你没有必要放低自己的姿态问一些很虚或者故意讨好面试官的问题,也不要把自己从面经上学到的东西照搬下来使用。面试官也不是傻子,特别是那种特别有经验的面试官,你是真心诚意的问问题,还是从别处照搬问题来讨好面试官,人家可能一听就听出来了。总的来说,还是要真诚。除此之外,不要问太 Low 的问题,会显得你整个人格局比较小或者说你根本没有准备(侧面反映你对这家公司不上心,既然你不上心,为什么要要你呢)。举例几个比较 Low 的问题,大家看看自己有没有问过其中的问题: - -- 贵公司的主要业务是什么?(面试之前自己不知道提前网上查一下吗?) -- 贵公司的男女比例如何?(考虑脱单?记住你是来工作的!) -- 贵公司一年搞几次外出旅游?(你是来工作的,这些娱乐活动先别放在心上!) -- ...... - -### 有哪些有价值的问题值得问? - -针对这个问题。笔主专门找了几个专门做HR工作的小哥哥小姐姐们询问并且查阅了挺多前辈们的回答,然后结合自己的实际经历,我概括了下面几个比较适合问的问题。 - -#### 面对HR或者其他Level比较低的面试官时 - -1. **能不能谈谈你作为一个公司老员工对公司的感受?** (这个问题比较容易回答,不会让面试官陷入无话可说的尴尬境地。另外,从面试官的回答中你可以加深对这个公司的了解,让你更加清楚这个公司到底是不是你想的那样或者说你是否能适应这个公司的文化。除此之外,这样的问题在某种程度上还可以拉进你与面试官的距离。) -2. **能不能问一下,你当时因为什么原因选择加入这家公司的呢或者说这家公司有哪些地方吸引你?有什么地方你觉得还不太好或者可以继续完善吗?** (类似第一个问题,都是问面试官个人对于公司的看法。) -3. **我觉得我这次表现的不是太好,你有什么建议或者评价给我吗?**(这个是我常问的。我觉得说自己表现不好只是这个语境需要这样来说,这样可以显的你比较谦虚好学上进。) -4. **接下来我会有一段空档期,有什么值得注意或者建议学习的吗?** (体现出你对工作比较上心,自助学习意识比较强。) -5. **这个岗位为什么还在招人?** (岗位真实性和价值咨询) -6. **大概什么时候能给我回复呢?** (终面的时候,如果面试官没有说的话,可以问一下) -7. ...... - - - -#### 面对部门领导 - -1. **部门的主要人员分配以及对应的主要工作能简单介绍一下吗?** -2. **未来如果我要加入这个团队,你对我的期望是什么?** (部门领导一般情况下是你的直属上级了,你以后和他打交道的机会应该是最多的。你问这个问题,会让他感觉你是一个对他的部门比较上心,比较有团体意识,并且愿意倾听的候选人。) -3. **公司对新入职的员工的培养机制是什么样的呢?** (正规的公司一般都有培养机制,提前问一下是对你自己的负责也会显的你比较上心) -4. **以您来看,这个岗位未来在公司内部的发展如何?** (在我看来,问这个问题也是对你自己的负责吧,谁不想发展前景更好的岗位呢?) -5. **团队现在面临的最大挑战是什么?** (这样的问题不会暴露你对公司的不了解,并且也能让你对未来工作的挑战或困难有一个提前的预期。) - - - -#### 面对Level比较高的(比如总裁,老板) - -1. **贵公司的发展目标和方向是什么?** (看下公司的发展是否满足自己的期望) -2. **与同行业的竞争者相比,贵公司的核心竞争优势在什么地方?** (充分了解自己的优势和劣势) -3. **公司现在面临的最大挑战是什么?** - -### 来个补充,顺便送个祝福给大家 - -薪酬待遇和相关福利问题一般在终面的时候(最好不要在前面几面的时候就问到这个问题),面试官会提出来或者在面试完之后以邮件的形式告知你。一般来说,如果面试官很愿意为你回答问题,对你的问题也比较上心的话,那他肯定是觉得你就是他们要招的人。 - -大家在面试的时候,可以根据自己对于公司或者岗位的了解程度,对上面提到的问题进行适当修饰或者修改。上面提到的一些问题只是给没有经验的朋友一个参考,如果你还有其他比较好的问题的话,那当然也更好啦! - -金三银四。过了二月就到了面试高峰期或者说是黄金期。几份惊喜几份愁,愿各位能始终不忘初心!每个人都有每个人的难处。引用一句《阿甘正传》里面的台词:“生活就像一盒巧克力,你永远不知道下一块是什么味道“。 - -![加油!彩虹就要来了](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-2/生活就像一盒巧克力你永远不知道下一块是什么味道.JPEG) \ No newline at end of file diff --git a/docs/essential-content-for-interview/real-interview-experience-analysis/alibaba-1.md b/docs/essential-content-for-interview/real-interview-experience-analysis/alibaba-1.md deleted file mode 100644 index a1f6101..0000000 --- a/docs/essential-content-for-interview/real-interview-experience-analysis/alibaba-1.md +++ /dev/null @@ -1,222 +0,0 @@ -本文的内容都是根据读者投稿的真实面试经历改编而来,首次尝试这种风格的文章,花了几天晚上才总算写完,希望对你有帮助。 - -本文主要涵盖下面的内容: - -1. 分布式商城系统:架构图讲解; -2. 消息队列相关:削峰和解耦; -3. Redis 相关:缓存穿透问题的解决; -4. 一些基础问题: - - 网络相关:1.浏览器输入 URL 发生了什么? 2.TCP 和 UDP 区别? 3.TCP 如何保证传输可靠性? - - Java 基础:1. 既然有了字节流,为什么还要有字符流? 2.深拷贝 和 浅拷贝有啥区别呢? - -下面是正文! - -面试开始,坐在我前面的就是这次我的面试官吗?这发量看着根本不像程序员啊?我心里正嘀咕着,只听见面试官说:“小伙,下午好,我今天就是你的面试官,咱们开始面试吧!”。 - -### 第一面开始 - -**面试官:** 我也不用多说了,你先自我介绍一下吧,简历上有的就不要再说了哈。 - -**我:** 内心 os:"果然如我所料,就知道会让我先自我介绍一下,还好我看了 [JavaGuide](https://github.com/Snailclimb/JavaGuide "JavaGuide") ,学到了一些套路。套路总结起来就是:**最好准备好两份自我介绍,一份对 hr 说的,主要讲能突出自己的经历,会的编程技术一语带过;另一份对技术面试官说的,主要讲自己会的技术细节,项目经验,经历那些就一语带过。** 所以,我按照这个套路准备了一个还算通用的模板,毕竟我懒嘛!不想多准备一个自我介绍,整个通用的多好! - -> 面试官,您好!我叫小李子。大学时间我主要利用课外时间学习 Java 相关的知识。在校期间参与过一个某某系统的开发,主要负责数据库设计和后端系统开发.,期间解决了什么问题,巴拉巴拉。另外,我自己在学习过程中也参照网上的教程写过一个电商系统的网站,写这个电商网站主要是为了能让自己接触到分布式系统的开发。在学习之余,我比较喜欢通过博客整理分享自己所学知识。我现在已经是某社区的认证作者,写过一系列关于 线程池使用以及源码分析的文章深受好评。另外,我获得过省级编程比赛二等奖,我将这个获奖项目开源到 Github 还收获了 2k 的 Star 呢? - -**面试官:** 你刚刚说参考网上的教程做了一个电商系统?你能画画这个电商系统的架构图吗? - -**我:** 内心 os: "这可难不倒我!早知道写在简历上的项目要重视了,提前都把这个系统的架构图画了好多遍了呢!" - - - -做过分布式电商系统的一定很熟悉上面的架构图(目前比较流行的是微服务架构,但是如果你有分布式开发经验也是非常加分的!)。 - -**面试官:** 简单介绍一下你做的这个系统吧! - -**我:** 我一本正经的对着我刚刚画的商城架构图开始了满嘴造火箭的讲起来: - -> 本系统主要分为展示层、服务层和持久层这三层。表现层顾名思义主要就是为了用来展示,比如我们的后台管理系统的页面、商城首页的页面、搜索系统的页面等等,这一层都只是作为展示,并没有提供任何服务。 -> -> 展示层和服务层一般是部署在不同的机器上来提高并发量和扩展性,那么展示层和服务层怎样才能交互呢?在本系统中我们使用 Dubbo 来进行服务治理。Dubbo 是一款高性能、轻量级的开源 Java RPC 框架。Dubbo 在本系统的主要作用就是提供远程 RPC 调用。在本系统中服务层的信息通过 Dubbo 注册给 ZooKeeper,表现层通过 Dubbo 去 ZooKeeper 中获取服务的相关信息。Zookeeper 的作用仅仅是存放提供服务的服务器的地址和一些服务的相关信息,实现 RPC 远程调用功能的还是 Dubbo。如果需要引用到某个服务的时候,我们只需要在配置文件中配置相关信息就可以在代码中直接使用了,就像调用本地方法一样。假如说某个服务的使用量增加时,我们只用为这单个服务增加服务器,而不需要为整个系统添加服务。 -> -> 另外,本系统的数据库使用的是常用的 MySQL,并且用到了数据库中间件 MyCat。另外,本系统还用到 redis 内存数据库来作为缓存来提高系统的反应速度。假如用户第一次访问数据库中的某些数据,这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。 -> -> 系统还用到了 Elasticsearch 来提供搜索功能。使用 Elasticsearch 我们可以非常方便的为我们的商城系统添加必备的搜索功能,并且使用 Elasticsearch 还能提供其它非常实用的功能,并且很容易扩展。 - -**面试官:** 我看你的系统里面还用到了消息队列,能说说为什么要用它吗? - -**我:** - -> 使用消息队列主要是为了: -> -> 1. 减少响应所需时间和削峰。 -> 2. 降低系统耦合性(解耦/提升系统可扩展性)。 - -**面试官:** 你这说的太简单了!能不能稍微详细一点,最好能画图给我解释一下。 - -**我:** 内心 os:"都 2019 年了,大部分面试者都能对消息队列的为系统带来的这两个好处倒背如流了,如果你想走的更远就要别别人懂的更深一点!" - -> 当我们不使用消息队列的时候,所有的用户的请求会直接落到服务器,然后通过数据库或者缓存响应。假如在高并发的场景下,如果没有缓存或者数据库承受不了这么大的压力的话,就会造成响应速度缓慢,甚至造成数据库宕机。但是,在使用消息队列之后,用户的请求数据发送给了消息队列之后就可以立即返回,再由消息队列的消费者进程从消息队列中获取数据,异步写入数据库,不过要确保消息不被重复消费还要考虑到消息丢失问题。由于消息队列服务器处理速度快于数据库,因此响应速度得到大幅改善。 -> -> 文字 is too 空洞,直接上图吧!下图展示了使用消息前后系统处理用户请求的对比(ps:我自己都被我画的这个图美到了,如果你也觉得这张图好看的话麻烦来个素质三连!)。 -> -> ![通过异步处理提高系统性能](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/Asynchronous-message-queue.png) -> -> 通过以上分析我们可以得出**消息队列具有很好的削峰作用的功能**——即**通过异步处理,将短时间高并发产生的事务消息存储在消息队列中,从而削平高峰期的并发事务。** 举例:在电子商务一些秒杀、促销活动中,合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击。如下图所示: -> -> ![削峰](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/削峰-消息队列.png) -> -> 使用消息队列还可以降低系统耦合性。我们知道如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性无疑更好一些。还是直接上图吧: -> -> ![解耦](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/消息队列-解耦.png) -> -> 生产者(客户端)发送消息到消息队列中去,接受者(服务端)处理消息,需要消费的系统直接去消息队列取消息进行消费即可而不需要和其他系统有耦合, 这显然也提高了系统的扩展性。 - -**面试官:** 你觉得它有什么缺点吗?或者说怎么考虑用不用消息队列? - -**我:** 内心 os: "面试官真鸡贼!这不是勾引我上钩么?还好我准备充分。" - -> 我觉得可以从下面几个方面来说: -> -> 1. **系统可用性降低:** 系统可用性在某种程度上降低,为什么这样说呢?在加入 MQ 之前,你不用考虑消息丢失或者说 MQ 挂掉等等的情况,但是,引入 MQ 之后你就需要去考虑了! -> 2. **系统复杂性提高:** 加入 MQ 之后,你需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺序性等等问题! -> 3. **一致性问题:** 我上面讲了消息队列可以实现异步,消息队列带来的异步确实可以提高系统响应速度。但是,万一消息的真正消费者并没有正确消费消息怎么办?这样就会导致数据不一致的情况了! - -**面试官**:做项目的过程中遇到了什么问题吗?解决了吗?如果解决的话是如何解决的呢? - -**我** : 内心 os: "做的过程中好像也没有遇到什么问题啊!怎么办?怎么办?突然想到可以说我在使用 Redis 过程中遇到的问题,毕竟我对 Redis 还算熟悉嘛,**把面试官往这个方向吸引**,准没错。" - -> 我在使用 Redis 对常用数据进行缓冲的过程中出现了缓存穿透问题。然后,我通过谷歌搜索相关的解决方案来解决的。 - -**面试官:** 你还知道缓存穿透啊?不错啊!来说说什么是缓存穿透以及你最后的解决办法。 - -**我:** 我先来谈谈什么是缓存穿透吧! - -> 缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。举个例子:某个黑客故意制造我们缓存中不存在的 key 发起大量请求,导致大量请求落到数据库。 -> -> 总结一下就是: -> -> 1. 缓存层不命中。 -> 2. 存储层不命中,不将空结果写回缓存。 -> 3. 返回空结果给客户端。 -> -> 一般 MySQL 默认的最大连接数在 150 左右,这个可以通过 `show variables like '%max_connections%';`命令来查看。最大连接数一个还只是一个指标,cpu,内存,磁盘,网络等物理条件都是其运行指标,这些指标都会限制其并发能力!所以,一般 3000 的并发请求就能打死大部分数据库了。 - -**面试官:** 小伙子不错啊!还准备问你:“为什么 3000 的并发能把支持最大连接数 4000 数据库压死?”想不到你自己就提前回答了!不错! - -**我:** 别夸了!别夸了!我再来说说我知道的一些解决办法以及我最后采用的方案吧!您帮忙看看有没有问题。 - -> 最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。 -> -> 参数校验通过的情况还是会出现缓存穿透,我们还可以通过以下几个方案来解决这个问题: -> -> **1)缓存无效 key** : 如果缓存和数据库都查不到某个 key 的数据就写一个到 redis 中去并设置过期时间,具体命令如下:`SET key value EX 10086`。这种方式可以解决请求的 key 变化不频繁的情况,如何黑客恶意攻击,每次构建的不同的请求 key,会导致 redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。 -> -> 另外,这里多说一嘴,一般情况下我们是这样设计 key 的: `表名:列名:主键名:主键值`。 -> -> **2)布隆过滤器:** 布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。我们需要的就是判断 key 是否合法,有没有感觉布隆过滤器就是我们想要找的那个“人”。 - -**面试官:** 不错不错!你还知道布隆过滤器啊!来给我谈一谈。 - -**我:** 内心 os:“如果你准备过海量数据处理的面试题,你一定对:“如何确定一个数字是否在于包含大量数字的数字集中(数字集很大,5 亿以上!)?”这个题目很了解了!解决这道题目就要用到布隆过滤器。” - -> 布隆过滤器在针对海量数据去重或者验证数据合法性的时候非常有用。**布隆过滤器的本质实际上是 “位(bit)数组”,也就是说每一个存入布隆过滤器的数据都只占一位。相比于我们平时常用的的 List、Map 、Set 等数据结构,它占用空间更少并且效率更高,但是缺点是其返回的结果是概率性的,而不是非常准确的。** -> -> **当一个元素加入布隆过滤器中的时候,会进行如下操作:** -> -> 1. 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。 -> 2. 根据得到的哈希值,在位数组中把对应下标的值置为 1。 -> -> **当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行如下操作:** -> -> 1. 对给定元素再次进行相同的哈希计算; -> 2. 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。 -> -> 举个简单的例子: -> -> ![布隆过滤器hash计算](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/布隆过滤器-hash运算.png) -> -> 如图所示,当字符串存储要加入到布隆过滤器中时,该字符串首先由多个哈希函数生成不同的哈希值,然后在对应的位数组的下表的元素设置为 1(当位数组初始化时 ,所有位置均为 0)。当第二次存储相同字符串时,因为先前的对应位置已设置为 1,所以很容易知道此值已经存在(去重非常方便)。 -> -> 如果我们需要判断某个字符串是否在布隆过滤器中时,只需要对给定字符串再次进行相同的哈希计算,得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。 -> -> **不同的字符串可能哈希出来的位置相同,这种情况我们可以适当增加位数组大小或者调整我们的哈希函数。** -> -> 综上,我们可以得出:**布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。** - -**面试官:** 看来你对布隆过滤器了解的还挺不错的嘛!那你快说说你最后是怎么利用它来解决缓存穿透的。 - -**我:** 知道了布隆过滤器的原理就之后就很容易做了。我是利用 Redis 布隆过滤器来做的。我把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,我会先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。总结一下就是下面这张图(这张图片不是我画的,为了省事直接在网上找的): - - - -更多关于布隆过滤器的内容可以看我的这篇原创:[《不了解布隆过滤器?一文给你整的明明白白!》](https://github.com/Snailclimb/JavaGuide/blob/master/docs/dataStructures-algorithms/data-structure/bloom-filter.md "《不了解布隆过滤器?一文给你整的明明白白!》") ,强烈推荐,个人感觉网上应该找不到总结的这么明明白白的文章了。 - -**面试官:** 好了好了。项目就暂时问到这里吧!下面有一些比较基础的问题我简单地问一下你。内心 os: 难不成这家伙满口高并发,连最基础的东西都不会吧! - -**我:** 好的好的!没问题! - -**面试官:** 浏览器输入 URL 发生了什么? - -**我:** 内心 os:“很常问的一个问题,建议拿小本本记好了!另外,百度好像最喜欢问这个问题,去百度面试可要提前备好这道题的功课哦!相似问题:打开一个网页,整个过程会使用哪些协议?”。 - -> 图解(图片来源:《图解 HTTP》): -> -> -> -> 总体来说分为以下几个过程: -> -> 1. DNS 解析 -> 2. TCP 连接 -> 3. 发送 HTTP 请求 -> 4. 服务器处理请求并返回 HTTP 报文 -> 5. 浏览器解析渲染页面 -> 6. 连接结束 -> -> 具体可以参考下面这篇文章: -> -> - [https://segmentfault.com/a/1190000006879700](https://segmentfault.com/a/1190000006879700 "https://segmentfault.com/a/1190000006879700") - -**面试官:** TCP 和 UDP 区别? - -**我:** - -> ![TCP、UDP协议的区别](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/tcp-vs-udp.jpg) -> -> UDP 在传送数据之前不需要先建立连接,远地主机在收到 UDP 报文后,不需要给出任何确认。虽然 UDP 不提供可靠交付,但在某些情况下 UDP 确是一种最有效的工作方式(一般用于即时通信),比如: QQ 语音、 QQ 视频 、直播等等 -> -> TCP 提供面向连接的服务。在传送数据之前必须先建立连接,数据传送结束后要释放连接。 TCP 不提供广播或多播服务。由于 TCP 要提供可靠的,面向连接的传输服务(TCP 的可靠体现在 TCP 在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完后,还会断开连接用来节约系统资源),这一难以避免增加了许多开销,如确认,流量控制,计时器以及连接管理等。这不仅使协议数据单元的首部增大很多,还要占用许多处理机资源。TCP 一般用于文件传输、发送和接收邮件、远程登录等场景。 - -**面试官:** TCP 如何保证传输可靠性? - -**我:** - -> 1. 应用数据被分割成 TCP 认为最适合发送的数据块。 -> 2. TCP 给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层。 -> 3. **校验和:** TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段。 -> 4. TCP 的接收端会丢弃重复的数据。 -> 5. **流量控制:** TCP 连接的每一方都有固定大小的缓冲空间,TCP 的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议。 (TCP 利用滑动窗口实现流量控制) -> 6. **拥塞控制:** 当网络拥塞时,减少数据的发送。 -> 7. **ARQ 协议:** 也是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组。 -> 8. **超时重传:** 当 TCP 发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。 - -**面试官:** 我再来问你一些 Java 基础的问题吧!小伙子。 - -**我:** 好的。(内心 os:“你尽管来!”) - -**面试官:** 既然有了字节流,为什么还要有字符流? - -我:内心 os :“问题本质想问:**不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?**” - -> 字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。 - -**面试官**:深拷贝 和 浅拷贝有啥区别呢? - -**我:** - -> 1. **浅拷贝**:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。 -> 2. **深拷贝**:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。 -> -> ![deep and shallow copy](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/java-deep-and-shallow-copy.jpg) - -**面试官:** 好的!面试结束。小伙子可以的!回家等通知吧! - -**我:** 好的好的!辛苦您了! diff --git "a/docs/essential-content-for-interview/\346\211\213\346\212\212\346\211\213\346\225\231\344\275\240\347\224\250Markdown\345\206\231\344\270\200\344\273\275\351\253\230\350\264\250\351\207\217\347\232\204\347\256\200\345\216\206.md" "b/docs/essential-content-for-interview/\346\211\213\346\212\212\346\211\213\346\225\231\344\275\240\347\224\250Markdown\345\206\231\344\270\200\344\273\275\351\253\230\350\264\250\351\207\217\347\232\204\347\256\200\345\216\206.md" deleted file mode 100644 index 9cb2811..0000000 --- "a/docs/essential-content-for-interview/\346\211\213\346\212\212\346\211\213\346\225\231\344\275\240\347\224\250Markdown\345\206\231\344\270\200\344\273\275\351\253\230\350\264\250\351\207\217\347\232\204\347\256\200\345\216\206.md" +++ /dev/null @@ -1,93 +0,0 @@ -## Markdown 简历模板样式一览 -![](https://user-gold-cdn.xitu.io/2018/9/3/1659f91e4843bd67?w=800&h=1737&f=png&s=97357) -**可以看到我把联系方式放在第一位,因为公司一般会与你联系,所以把联系方式放在第一位也是为了方便联系考虑。** - -## 为什么要用 Markdown 写简历? - -Markdown 语法简单,易于上手。使用正确的 Markdown 语言写出来的简历不论是在排版还是格式上都比较干净,易于阅读。另外,使用 Markdown 写简历也会给面试官一种你比较专业的感觉。 - -除了这些,我觉得使用 Markdown 写简历可以很方便将其与PDF、HTML、PNG格式之间转换。后面我会介绍到转换方法,只需要一条命令你就可以实现 Markdown 到 PDF、HTML 与 PNG之间的无缝切换。 - -> 下面的一些内容我在之前的一篇文章中已经提到过,这里再说一遍,最后会分享如何实现Markdown 到 PDF、HTML、PNG格式之间转换。 - -## 为什么说简历很重要? - -假如你是网申,你的简历必然会经过HR的筛选,一张简历HR可能也就花费10秒钟看一下,然后HR就会决定你这一关是Fail还是Pass。 - -假如你是内推,如果你的简历没有什么优势的话,就算是内推你的人再用心,也无能为力。 - -另外,就算你通过了筛选,后面的面试中,面试官也会根据你的简历来判断你究竟是否值得他花费很多时间去面试。 - -## 写简历的两大法则 - -目前写简历的方式有两种普遍被认可,一种是 STAR, 一种是 FAB。 - -**STAR法则(Situation Task Action Result):** - -- **Situation:** 事情是在什么情况下发生; -- **Task::** 你是如何明确你的任务的; -- **Action:** 针对这样的情况分析,你采用了什么行动方式; -- **Result:** 结果怎样,在这样的情况下你学习到了什么。 - -**FAB 法则(Feature Advantage Benefit):** - -- **Feature:** 是什么; -- **Advantage:** 比别人好在哪些地方; -- **Benefit:** 如果雇佣你,招聘方会得到什么好处。 - -## 项目经历怎么写? -简历上有一两个项目经历很正常,但是真正能把项目经历很好的展示给面试官的非常少。对于项目经历大家可以考虑从如下几点来写: - -1. 对项目整体设计的一个感受 -2. 在这个项目中你负责了什么、做了什么、担任了什么角色 -3. 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用 -4. 另外项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个棘手的问题的时候你是如何解决的。 - -## 专业技能该怎么写? -先问一下你自己会什么,然后看看你意向的公司需要什么。一般HR可能并不太懂技术,所以他在筛选简历的时候可能就盯着你专业技能的关键词来看。对于公司有要求而你不会的技能,你可以花几天时间学习一下,然后在简历上可以写上自己了解这个技能。比如你可以这样写: - -- Dubbo:精通 -- Spring:精通 -- Docker:掌握 -- SOA分布式开发 :掌握 -- Spring Cloud:了解 - -## 简历模板分享 - -**开源程序员简历模板**: [https://github.com/geekcompany/ResumeSample](https://github.com/geekcompany/ResumeSample)(包括PHP程序员简历模板、iOS程序员简历模板、Android程序员简历模板、Web前端程序员简历模板、Java程序员简历模板、C/C++程序员简历模板、NodeJS程序员简历模板、架构师简历模板以及通用程序员简历模板) - -**上述简历模板的改进版本:** [https://github.com/Snailclimb/Java-Guide/blob/master/面试必备/简历模板.md](https://github.com/Snailclimb/Java-Guide/blob/master/面试必备/简历模板.md) - -## 其他的一些小tips - -1. 尽量避免主观表述,少一点语义模糊的形容词,尽量要简洁明了,逻辑结构清晰。 -2. 注意排版(不需要花花绿绿的),尽量使用Markdown语法。 -3. 如果自己有博客或者个人技术栈点的话,写上去会为你加分很多。 -4. 如果自己的Github比较活跃的话,写上去也会为你加分很多。 -5. 注意简历真实性,一定不要写自己不会的东西,或者带有欺骗性的内容 -6. 项目经历建议以时间倒序排序,另外项目经历不在于多,而在于有亮点。 -7. 如果内容过多的话,不需要非把内容压缩到一页,保持排版干净整洁就可以了。 -8. 简历最后最好能加上:“感谢您花时间阅读我的简历,期待能有机会和您共事。”这句话,显的你会很有礼貌。 - - -> 我们刚刚讲了很多关于如何写简历的内容并且分享了一份 Markdown 格式的简历文档。下面我们来看看如何实现 Markdown 到 HTML格式、PNG格式之间转换。 -## Markdown 到 HTML格式、PNG格式之间转换 - -网上很难找到一个比较方便并且效果好的转换方法,最后我是通过 Visual Studio Code 的 Markdown PDF 插件完美解决了这个问题! - -### 安装 Markdown PDF 插件 - -**① 打开Visual Studio Code ,按快捷键 F1,选择安装扩展选项** - -![① 打开Visual Studio Code ,按快捷键 F1,选择安装扩展选项](https://user-gold-cdn.xitu.io/2018/9/3/1659f9a44103e551?w=1366&h=688&f=png&s=104435) - -**② 搜索 “Markdown PDF” 插件并安装 ,然后重启** - -![② 搜索 “Markdown PDF” 插件并安装 ,然后重启](https://user-gold-cdn.xitu.io/2018/9/3/1659f9dbef0d06fb?w=1280&h=420&f=png&s=70510) - -### 使用方法 - -随便打开一份 Markdown 文件 点击F1,然后输入export即可! - -![](https://user-gold-cdn.xitu.io/2018/9/3/1659fa0292906150?w=1289&h=468&f=png&s=72178) - diff --git "a/docs/essential-content-for-interview/\347\256\200\345\216\206\346\250\241\346\235\277.md" "b/docs/essential-content-for-interview/\347\256\200\345\216\206\346\250\241\346\235\277.md" deleted file mode 100644 index 2a7a043..0000000 --- "a/docs/essential-content-for-interview/\347\256\200\345\216\206\346\250\241\346\235\277.md" +++ /dev/null @@ -1,79 +0,0 @@ -# 联系方式 - -- 手机: -- Email: -- 微信: - -# 个人信息 - - - 姓名/性别/出生日期 - - 本科/xxx计算机系xxx专业/英语六级 - - 技术博客:[http://snailclimb.top/](http://snailclimb.top/) - - 荣誉奖励:获得了什么奖(获奖时间) - - Github:[https://github.com/Snailclimb ](https://github.com/Snailclimb) - - Github Resume: [http://resume.github.io/?Snailclimb](http://resume.github.io/?Snailclimb) - - 期望职位:Java 研发程序员/大数据工程师(Java后台开发为首选) - - 期望城市:xxx城市 - - -# 项目经历 - -## xxx项目 - -### 项目描述 - -介绍该项目是做什么的、使用到了什么技术以及你对项目整体设计的一个感受 - -### 责任描述 - -主要可以从下面三点来写: - -1. 在这个项目中你负责了什么、做了什么、担任了什么角色 -2. 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用 -3. 另外项目描述中,最好可以体现自己的综合素质,比如你是如何协调项目组成员协同开发的或者在遇到某一个棘手的问题的时候你是如何解决的。 - -# 开源项目和技术文章 - -## 开源项目 - -- [Java-Guide](https://github.com/Snailclimb/Java-Guide) :一份涵盖大部分Java程序员所需要掌握的核心知识。Star:3.9K; Fork:0.9k。 - - -## 技术文章推荐 - -- [可能是把Java内存区域讲的最清楚的一篇文章](https://juejin.im/post/5b7d69e4e51d4538ca5730cb) -- [搞定JVM垃圾回收就是这么简单](https://juejin.im/post/5b85ea54e51d4538dd08f601) -- [前端&后端程序员必备的Linux基础知识](https://juejin.im/post/5b3b19856fb9a04fa42f8c71) -- [可能是把Docker的概念讲的最清楚的一篇文章](https://juejin.im/post/5b260ec26fb9a00e8e4b031a) - - -# 校园经历(可选) - -## 2016-2017 - -担任学校社团-致深社副会长,主要负责团队每周活动的组建以及每周例会的主持。 - -## 2017-2018 - 担任学校传媒组织:“长江大学在线信息传媒”的副站长以及安卓组成员。主要负责每周例会主持、活动策划以及学校校园通APP的研发工作。 - - -# 技能清单 - -以下均为我熟练使用的技能 - -- Web开发:PHP/Hack/Node -- Web框架:ThinkPHP/Yaf/Yii/Lavarel/LazyPHP -- 前端框架:Bootstrap/AngularJS/EmberJS/HTML5/Cocos2dJS/ionic -- 前端工具:Bower/Gulp/SaSS/LeSS/PhoneGap -- 数据库相关:MySQL/PgSQL/PDO/SQLite -- 版本管理、文档和自动化部署工具:Svn/Git/PHPDoc/Phing/Composer -- 单元测试:PHPUnit/SimpleTest/Qunit -- 云和开放平台:SAE/BAE/AWS/微博开放平台/微信应用开发 - -# 自我评价(可选) - -自我发挥。切记不要过度自夸!!! - - -### 感谢您花时间阅读我的简历,期待能有机会和您共事。 - diff --git "a/docs/essential-content-for-interview/\351\235\242\350\257\225\345\277\205\345\244\207\344\271\213\344\271\220\350\247\202\351\224\201\344\270\216\346\202\262\350\247\202\351\224\201.md" "b/docs/essential-content-for-interview/\351\235\242\350\257\225\345\277\205\345\244\207\344\271\213\344\271\220\350\247\202\351\224\201\344\270\216\346\202\262\350\247\202\351\224\201.md" deleted file mode 100644 index 00aaecd..0000000 --- "a/docs/essential-content-for-interview/\351\235\242\350\257\225\345\277\205\345\244\207\344\271\213\344\271\220\350\247\202\351\224\201\344\270\216\346\202\262\350\247\202\351\224\201.md" +++ /dev/null @@ -1,115 +0,0 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 - - - -- [何谓悲观锁与乐观锁](#何谓悲观锁与乐观锁) - - [悲观锁](#悲观锁) - - [乐观锁](#乐观锁) - - [两种锁的使用场景](#两种锁的使用场景) -- [乐观锁常见的两种实现方式](#乐观锁常见的两种实现方式) - - [1. 版本号机制](#1-版本号机制) - - [2. CAS算法](#2-cas算法) -- [乐观锁的缺点](#乐观锁的缺点) - - [1 ABA 问题](#1-aba-问题) - - [2 循环时间长开销大](#2-循环时间长开销大) - - [3 只能保证一个共享变量的原子操作](#3-只能保证一个共享变量的原子操作) -- [CAS与synchronized的使用情景](#cas与synchronized的使用情景) - - - -### 何谓悲观锁与乐观锁 - -> 乐观锁对应于生活中乐观的人总是想着事情往好的方向发展,悲观锁对应于生活中悲观的人总是想着事情往坏的方向发展。这两种人各有优缺点,不能不以场景而定说一种人好于另外一种人。 - -#### 悲观锁 - -总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中`synchronized`和`ReentrantLock`等独占锁就是悲观锁思想的实现。 - - -#### 乐观锁 - -总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。**乐观锁适用于多读的应用类型,这样可以提高吞吐量**,像数据库提供的类似于**write_condition机制**,其实都是提供的乐观锁。在Java中`java.util.concurrent.atomic`包下面的原子变量类就是使用了乐观锁的一种实现方式**CAS**实现的。 - -#### 两种锁的使用场景 - -从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像**乐观锁适用于写比较少的情况下(多读场景)**,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以**一般多写的场景下用悲观锁就比较合适。** - - -### 乐观锁常见的两种实现方式 - -> **乐观锁一般会使用版本号机制或CAS算法实现。** - -#### 1. 版本号机制 - -一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。 - -**举一个简单的例子:** -假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。 - -1. 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。 -2. 在操作员 A 操作的过程中,操作员B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 )。 -3. 操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。 -4. 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。 - -这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能。 - -#### 2. CAS算法 - -即**compare and swap(比较与交换)**,是一种有名的**无锁算法**。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。**CAS算法**涉及到三个操作数 - -- 需要读写的内存值 V -- 进行比较的值 A -- 拟写入的新值 B - -当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个**自旋操作**,即**不断的重试**。 - -关于自旋锁,大家可以看一下这篇文章,非常不错:[《 -面试必备之深入理解自旋锁》](https://blog.csdn.net/qq_34337272/article/details/81252853) - -### 乐观锁的缺点 - -> ABA 问题是乐观锁一个常见的问题 - -#### 1 ABA 问题 - -如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 **"ABA"问题。** - -JDK 1.5 以后的 `AtomicStampedReference 类`就提供了此种能力,其中的 `compareAndSet 方法`就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。 - -#### 2 循环时间长开销大 -**自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。** 如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。 - -#### 3 只能保证一个共享变量的原子操作 - -CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了`AtomicReference类`来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用`AtomicReference类`把多个共享变量合并成一个共享变量来操作。 - -### CAS与synchronized的使用情景 - -> **简单的来说CAS适用于写比较少的情况下(多读场景,冲突一般较少),synchronized适用于写比较多的情况下(多写场景,冲突一般较多)** - -1. 对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。 -2. 对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。 - -补充: Java并发编程这个领域中synchronized关键字一直都是元老级的角色,很久之前很多人都会称它为 **“重量级锁”** 。但是,在JavaSE 1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的 **偏向锁** 和 **轻量级锁** 以及其它**各种优化**之后变得在某些情况下并不是那么重了。synchronized的底层实现主要依靠 **Lock-Free** 的队列,基本思路是 **自旋后阻塞**,**竞争切换后继续竞争锁**,**稍微牺牲了公平性,但获得了高吞吐量**。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。 - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) - - - - - - - - - - - - diff --git "a/docs/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/BIO-NIO-AIO.md b/docs/java/BIO-NIO-AIO.md deleted file mode 100644 index 36aac43..0000000 --- a/docs/java/BIO-NIO-AIO.md +++ /dev/null @@ -1,346 +0,0 @@ -熟练掌握 BIO,NIO,AIO 的基本概念以及一些常见问题是你准备面试的过程中不可或缺的一部分,另外这些知识点也是你学习 Netty 的基础。 - - - -- [BIO,NIO,AIO 总结](#bionioaio-总结) - - [1. BIO \(Blocking I/O\)](#1-bio-blocking-io) - - [1.1 传统 BIO](#11-传统-bio) - - [1.2 伪异步 IO](#12-伪异步-io) - - [1.3 代码示例](#13-代码示例) - - [1.4 总结](#14-总结) - - [2. NIO \(New I/O\)](#2-nio-new-io) - - [2.1 NIO 简介](#21-nio-简介) - - [2.2 NIO的特性/NIO与IO区别](#22-nio的特性nio与io区别) - - [1)Non-blocking IO(非阻塞IO)](#1non-blocking-io(非阻塞io)) - - [2)Buffer\(缓冲区\)](#2buffer缓冲区) - - [3)Channel \(通道\)](#3channel-通道) - - [4)Selectors\(选择器\)](#4selectors选择器) - - [2.3 NIO 读数据和写数据方式](#23-nio-读数据和写数据方式) - - [2.4 NIO核心组件简单介绍](#24-nio核心组件简单介绍) - - [2.5 代码示例](#25-代码示例) - - [3. AIO \(Asynchronous I/O\)](#3-aio-asynchronous-io) - - [参考](#参考) - - - - -# BIO,NIO,AIO 总结 - - Java 中的 BIO、NIO和 AIO 理解为是 Java 语言对操作系统的各种 IO 模型的封装。程序员在使用这些 API 的时候,不需要关心操作系统层面的知识,也不需要根据不同操作系统编写不同的代码。只需要使用Java的API就可以了。 - -在讲 BIO,NIO,AIO 之前先来回顾一下这样几个概念:同步与异步,阻塞与非阻塞。 - -**同步与异步** - -- **同步:** 同步就是发起一个调用后,被调用者未处理完请求之前,调用不返回。 -- **异步:** 异步就是发起一个调用后,立刻得到被调用者的回应表示已接收到请求,但是被调用者并没有返回结果,此时我们可以处理其他的请求,被调用者通常依靠事件,回调等机制来通知调用者其返回结果。 - -同步和异步的区别最大在于异步的话调用者不需要等待处理结果,被调用者会通过回调等机制来通知调用者其返回结果。 - -**阻塞和非阻塞** - -- **阻塞:** 阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。 -- **非阻塞:** 非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。 - -举个生活中简单的例子,你妈妈让你烧水,小时候你比较笨啊,在那里傻等着水开(**同步阻塞**)。等你稍微再长大一点,你知道每次烧水的空隙可以去干点其他事,然后只需要时不时来看看水开了没有(**同步非阻塞**)。后来,你们家用上了水开了会发出声音的壶,这样你就只需要听到响声后就知道水开了,在这期间你可以随便干自己的事情,你需要去倒水了(**异步非阻塞**)。 - - -## 1. BIO (Blocking I/O) - -同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。 - -### 1.1 传统 BIO - -BIO通信(一请求一应答)模型图如下(图源网络,原出处不明): - -![传统BIO通信模型图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2.png) - -采用 **BIO 通信模型** 的服务端,通常由一个独立的 Acceptor 线程负责监听客户端的连接。我们一般通过在`while(true)` 循环中服务端会调用 `accept()` 方法等待接收客户端的连接的方式监听请求,请求一旦接收到一个连接请求,就可以建立通信套接字在这个通信套接字上进行读写操作,此时不能再接收其他客户端连接请求,只能等待同当前连接的客户端的操作执行完成, 不过可以通过多线程来支持多个客户端的连接,如上图所示。 - -如果要让 **BIO 通信模型** 能够同时处理多个客户端请求,就必须使用多线程(主要原因是`socket.accept()`、`socket.read()`、`socket.write()` 涉及的三个主要函数都是同步阻塞的),也就是说它在接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的 **一请求一应答通信模型** 。我们可以设想一下如果这个连接不做任何事情的话就会造成不必要的线程开销,不过可以通过 **线程池机制** 改善,线程池还可以让线程的创建和回收成本相对较低。使用`FixedThreadPool` 可以有效的控制了线程的最大数量,保证了系统有限的资源的控制,实现了N(客户端请求数量):M(处理客户端请求的线程数量)的伪异步I/O模型(N 可以远远大于 M),下面一节"伪异步 BIO"中会详细介绍到。 - -**我们再设想一下当客户端并发访问量增加后这种模型会出现什么问题?** - -在 Java 虚拟机中,线程是宝贵的资源,线程的创建和销毁成本很高,除此之外,线程的切换成本也是很高的。尤其在 Linux 这样的操作系统中,线程本质上就是一个进程,创建和销毁线程都是重量级的系统函数。如果并发访问量增加会导致线程数急剧膨胀可能会导致线程堆栈溢出、创建新线程失败等问题,最终导致进程宕机或者僵死,不能对外提供服务。 - -### 1.2 伪异步 IO - -为了解决同步阻塞I/O面临的一个链路需要一个线程处理的问题,后来有人对它的线程模型进行了优化一一一后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数M:线程池最大线程数N的比例关系,其中M可以远远大于N.通过线程池可以灵活地调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。 - -伪异步IO模型图(图源网络,原出处不明): - -![伪异步IO模型图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/3.png) - -采用线程池和任务队列可以实现一种叫做伪异步的 I/O 通信框架,它的模型图如上图所示。当有新的客户端接入时,将客户端的 Socket 封装成一个Task(该任务实现java.lang.Runnable接口)投递到后端的线程池中进行处理,JDK 的线程池维护一个消息队列和 N 个活跃线程,对消息队列中的任务进行处理。由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。 - -伪异步I/O通信框架采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。不过因为它的底层仍然是同步阻塞的BIO模型,因此无法从根本上解决问题。 - -### 1.3 代码示例 - -下面代码中演示了BIO通信(一请求一应答)模型。我们会在客户端创建多个线程依次连接服务端并向其发送"当前时间+:hello world",服务端会为每个客户端线程创建一个线程来处理。代码示例出自闪电侠的博客,原地址如下: - -[https://www.jianshu.com/p/a4e03835921a](https://www.jianshu.com/p/a4e03835921a) - -**客户端** - -```java -/** - * - * @author 闪电侠 - * @date 2018年10月14日 - * @Description:客户端 - */ -public class IOClient { - - public static void main(String[] args) { - // TODO 创建多个线程,模拟多个客户端连接服务端 - new Thread(() -> { - try { - Socket socket = new Socket("127.0.0.1", 3333); - while (true) { - try { - socket.getOutputStream().write((new Date() + ": hello world").getBytes()); - Thread.sleep(2000); - } catch (Exception e) { - } - } - } catch (IOException e) { - } - }).start(); - - } - -} - -``` - -**服务端** - -```java -/** - * @author 闪电侠 - * @date 2018年10月14日 - * @Description: 服务端 - */ -public class IOServer { - - public static void main(String[] args) throws IOException { - // TODO 服务端处理客户端连接请求 - ServerSocket serverSocket = new ServerSocket(3333); - - // 接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理 - new Thread(() -> { - while (true) { - try { - // 阻塞方法获取新的连接 - Socket socket = serverSocket.accept(); - - // 每一个新的连接都创建一个线程,负责读取数据 - new Thread(() -> { - try { - int len; - byte[] data = new byte[1024]; - InputStream inputStream = socket.getInputStream(); - // 按字节流方式读取数据 - while ((len = inputStream.read(data)) != -1) { - System.out.println(new String(data, 0, len)); - } - } catch (IOException e) { - } - }).start(); - - } catch (IOException e) { - } - - } - }).start(); - - } - -} -``` - -### 1.4 总结 - -在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。 - - - -## 2. NIO (New I/O) - -### 2.1 NIO 简介 - - 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 的非阻塞模式来开发。 - -### 2.2 NIO的特性/NIO与IO区别 - -如果是在面试中回答这个问题,我觉得首先肯定要从 NIO 流是非阻塞 IO 而 IO 流是阻塞 IO 说起。然后,可以从 NIO 的3个核心组件/特性为 NIO 带来的一些改进来分析。如果,你把这些都回答上了我觉得你对于 NIO 就有了更为深入一点的认识,面试官问到你这个问题,你也能很轻松的回答上来了。 - -#### 1)Non-blocking IO(非阻塞IO) - -**IO流是阻塞的,NIO流是不阻塞的。** - -Java NIO使我们可以进行非阻塞IO操作。比如说,单线程中从通道读取数据到buffer,同时可以继续做别的事情,当数据读取到buffer中后,线程再继续处理数据。写数据也是一样的。另外,非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 - -Java IO的各种流是阻塞的。这意味着,当一个线程调用 `read()` 或 `write()` 时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了 - -#### 2)Buffer(缓冲区) - -**IO 面向流(Stream oriented),而 NIO 面向缓冲区(Buffer oriented)。** - -Buffer是一个对象,它包含一些要写入或者要读出的数据。在NIO类库中加入Buffer对象,体现了新库与原I/O的一个重要区别。在面向流的I/O中·可以将数据直接写入或者将数据直接读到 Stream 对象中。虽然 Stream 中也有 Buffer 开头的扩展类,但只是流的包装类,还是从流读到缓冲区,而 NIO 却是直接读到 Buffer 中进行操作。 - -在NIO厍中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。 - -最常用的缓冲区是 ByteBuffer,一个 ByteBuffer 提供了一组功能用于操作 byte 数组。除了ByteBuffer,还有其他的一些缓冲区,事实上,每一种Java基本类型(除了Boolean类型)都对应有一种缓冲区。 - -#### 3)Channel (通道) - -NIO 通过Channel(通道) 进行读写。 - -通道是双向的,可读也可写,而流的读写是单向的。无论读写,通道只能和Buffer交互。因为 Buffer,通道可以异步地读写。 - -#### 4)Selector (选择器) - -NIO有选择器,而IO没有。 - -选择器用于使用单个线程处理多个通道。因此,它需要较少的线程来处理这些通道。线程之间的切换对于操作系统来说是昂贵的。 因此,为了提高系统效率选择器是有用的。 - -![一个单线程中Selector维护3个Channel的示意图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-2/Slector.png) - -### 2.3 NIO 读数据和写数据方式 -通常来说NIO中的所有IO都是从 Channel(通道) 开始的。 - -- 从通道进行数据读取 :创建一个缓冲区,然后请求通道读取数据。 -- 从通道进行数据写入 :创建一个缓冲区,填充数据,并要求通道写入数据。 - -数据读取和写入操作图示: - -![NIO读写数据的方式](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-2/NIO读写数据的方式.png) - - -### 2.4 NIO核心组件简单介绍 - -NIO 包含下面几个核心的组件: - -- Channel(通道) -- Buffer(缓冲区) -- Selector(选择器) - -整个NIO体系包含的类远远不止这三个,只能说这三个是NIO体系的“核心API”。我们上面已经对这三个概念进行了基本的阐述,这里就不多做解释了。 - -### 2.5 代码示例 - -代码示例出自闪电侠的博客,原地址如下: - -[https://www.jianshu.com/p/a4e03835921a](https://www.jianshu.com/p/a4e03835921a) - -客户端 IOClient.java 的代码不变,我们对服务端使用 NIO 进行改造。以下代码较多而且逻辑比较复杂,大家看看就好。 - -```java -/** - * - * @author 闪电侠 - * @date 2019年2月21日 - * @Description: NIO 改造后的服务端 - */ -public class NIOServer { - public static void main(String[] args) throws IOException { - // 1. serverSelector负责轮询是否有新的连接,服务端监测到新的连接之后,不再创建一个新的线程, - // 而是直接将新连接绑定到clientSelector上,这样就不用 IO 模型中 1w 个 while 循环在死等 - Selector serverSelector = Selector.open(); - // 2. clientSelector负责轮询连接是否有数据可读 - Selector clientSelector = Selector.open(); - - new Thread(() -> { - try { - // 对应IO编程中服务端启动 - ServerSocketChannel listenerChannel = ServerSocketChannel.open(); - listenerChannel.socket().bind(new InetSocketAddress(3333)); - listenerChannel.configureBlocking(false); - listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT); - - while (true) { - // 监测是否有新的连接,这里的1指的是阻塞的时间为 1ms - if (serverSelector.select(1) > 0) { - Set set = serverSelector.selectedKeys(); - Iterator keyIterator = set.iterator(); - - while (keyIterator.hasNext()) { - SelectionKey key = keyIterator.next(); - - if (key.isAcceptable()) { - try { - // (1) 每来一个新连接,不需要创建一个线程,而是直接注册到clientSelector - SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept(); - clientChannel.configureBlocking(false); - clientChannel.register(clientSelector, SelectionKey.OP_READ); - } finally { - keyIterator.remove(); - } - } - - } - } - } - } catch (IOException ignored) { - } - }).start(); - new Thread(() -> { - try { - while (true) { - // (2) 批量轮询是否有哪些连接有数据可读,这里的1指的是阻塞的时间为 1ms - if (clientSelector.select(1) > 0) { - Set set = clientSelector.selectedKeys(); - Iterator keyIterator = set.iterator(); - - while (keyIterator.hasNext()) { - SelectionKey key = keyIterator.next(); - - if (key.isReadable()) { - try { - SocketChannel clientChannel = (SocketChannel) key.channel(); - ByteBuffer byteBuffer = ByteBuffer.allocate(1024); - // (3) 面向 Buffer - clientChannel.read(byteBuffer); - byteBuffer.flip(); - System.out.println( - Charset.defaultCharset().newDecoder().decode(byteBuffer).toString()); - } finally { - keyIterator.remove(); - key.interestOps(SelectionKey.OP_READ); - } - } - - } - } - } - } catch (IOException ignored) { - } - }).start(); - - } -} -``` - -为什么大家都不愿意用 JDK 原生 NIO 进行开发呢?从上面的代码中大家都可以看出来,是真的难用!除了编程复杂、编程模型难之外,它还有以下让人诟病的问题: - -- JDK 的 NIO 底层由 epoll 实现,该实现饱受诟病的空轮询 bug 会导致 cpu 飙升 100% -- 项目庞大之后,自行实现的 NIO 很容易出现各类 bug,维护成本较高,上面这一坨代码我都不能保证没有 bug - -Netty 的出现很大程度上改善了 JDK 原生 NIO 所存在的一些让人难以忍受的问题。 - -### 3. AIO (Asynchronous I/O) - -AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。 - -AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。(除了 AIO 其他的 IO 类型都是同步的,这一点可以从底层IO线程模型解释,推荐一篇文章:[《漫话:如何给女朋友解释什么是Linux的五种IO模型?》](https://mp.weixin.qq.com/s?__biz=Mzg3MjA4MTExMw==&mid=2247484746&idx=1&sn=c0a7f9129d780786cabfcac0a8aa6bb7&source=41#wechat_redirect) ) - -查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。 - -## 参考 - -- 《Netty 权威指南》第二版 -- https://zhuanlan.zhihu.com/p/23488863 (美团技术团队) diff --git a/docs/java/Basis/Arrays,CollectionsCommonMethods.md b/docs/java/Basis/Arrays,CollectionsCommonMethods.md deleted file mode 100644 index 0710de4..0000000 --- a/docs/java/Basis/Arrays,CollectionsCommonMethods.md +++ /dev/null @@ -1,383 +0,0 @@ - - -- [Collections 工具类和 Arrays 工具类常见方法](#collections-工具类和-arrays-工具类常见方法) - - [Collections](#collections) - - [排序操作](#排序操作) - - [查找,替换操作](#查找替换操作) - - [同步控制](#同步控制) - - [Arrays类的常见操作](#arrays类的常见操作) - - [排序 : `sort()`](#排序--sort) - - [查找 : `binarySearch()`](#查找--binarysearch) - - [比较: `equals()`](#比较-equals) - - [填充 : `fill()`](#填充--fill) - - [转列表 `asList()`](#转列表-aslist) - - [转字符串 `toString()`](#转字符串-tostring) - - [复制 `copyOf()`](#复制-copyof) - - -# Collections 工具类和 Arrays 工具类常见方法 - -## Collections - -Collections 工具类常用方法: - -1. 排序 -2. 查找,替换操作 -3. 同步控制(不推荐,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合) - -### 排序操作 - -```java -void reverse(List list)//反转 -void shuffle(List list)//随机排序 -void sort(List list)//按自然排序的升序排序 -void sort(List list, Comparator c)//定制排序,由Comparator控制排序逻辑 -void swap(List list, int i , int j)//交换两个索引位置的元素 -void rotate(List list, int distance)//旋转。当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将 list的前distance个元素整体移到后面。 -``` - -**示例代码:** - -```java - ArrayList arrayList = new ArrayList(); - arrayList.add(-1); - arrayList.add(3); - arrayList.add(3); - arrayList.add(-5); - arrayList.add(7); - arrayList.add(4); - arrayList.add(-9); - arrayList.add(-7); - System.out.println("原始数组:"); - System.out.println(arrayList); - // void reverse(List list):反转 - Collections.reverse(arrayList); - System.out.println("Collections.reverse(arrayList):"); - System.out.println(arrayList); - - - Collections.rotate(arrayList, 4); - System.out.println("Collections.rotate(arrayList, 4):"); - System.out.println(arrayList); - - // void sort(List list),按自然排序的升序排序 - Collections.sort(arrayList); - System.out.println("Collections.sort(arrayList):"); - System.out.println(arrayList); - - // void shuffle(List list),随机排序 - Collections.shuffle(arrayList); - System.out.println("Collections.shuffle(arrayList):"); - System.out.println(arrayList); - - // void swap(List list, int i , int j),交换两个索引位置的元素 - Collections.swap(arrayList, 2, 5); - System.out.println("Collections.swap(arrayList, 2, 5):"); - System.out.println(arrayList); - - // 定制排序的用法 - Collections.sort(arrayList, new Comparator() { - - @Override - public int compare(Integer o1, Integer o2) { - return o2.compareTo(o1); - } - }); - System.out.println("定制排序后:"); - System.out.println(arrayList); -``` - -### 查找,替换操作 - -```java -int binarySearch(List list, Object key)//对List进行二分查找,返回索引,注意List必须是有序的 -int max(Collection coll)//根据元素的自然顺序,返回最大的元素。 类比int min(Collection coll) -int max(Collection coll, Comparator c)//根据定制排序,返回最大元素,排序规则由Comparatator类控制。类比int min(Collection coll, Comparator c) -void fill(List list, Object obj)//用指定的元素代替指定list中的所有元素。 -int frequency(Collection c, Object o)//统计元素出现次数 -int indexOfSubList(List list, List target)//统计target在list中第一次出现的索引,找不到则返回-1,类比int lastIndexOfSubList(List source, list target). -boolean replaceAll(List list, Object oldVal, Object newVal), 用新元素替换旧元素 -``` - -**示例代码:** - -```java - ArrayList arrayList = new ArrayList(); - arrayList.add(-1); - arrayList.add(3); - arrayList.add(3); - arrayList.add(-5); - arrayList.add(7); - arrayList.add(4); - arrayList.add(-9); - arrayList.add(-7); - ArrayList arrayList2 = new ArrayList(); - arrayList2.add(-3); - arrayList2.add(-5); - arrayList2.add(7); - System.out.println("原始数组:"); - System.out.println(arrayList); - - System.out.println("Collections.max(arrayList):"); - System.out.println(Collections.max(arrayList)); - - System.out.println("Collections.min(arrayList):"); - System.out.println(Collections.min(arrayList)); - - System.out.println("Collections.replaceAll(arrayList, 3, -3):"); - Collections.replaceAll(arrayList, 3, -3); - System.out.println(arrayList); - - System.out.println("Collections.frequency(arrayList, -3):"); - System.out.println(Collections.frequency(arrayList, -3)); - - System.out.println("Collections.indexOfSubList(arrayList, arrayList2):"); - System.out.println(Collections.indexOfSubList(arrayList, arrayList2)); - - System.out.println("Collections.binarySearch(arrayList, 7):"); - // 对List进行二分查找,返回索引,List必须是有序的 - Collections.sort(arrayList); - System.out.println(Collections.binarySearch(arrayList, 7)); -``` - -### 同步控制 - -Collections提供了多个`synchronizedXxx()`方法·,该方法可以将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题。 - -我们知道 HashSet,TreeSet,ArrayList,LinkedList,HashMap,TreeMap 都是线程不安全的。Collections提供了多个静态方法可以把他们包装成线程同步的集合。 - -**最好不要用下面这些方法,效率非常低,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合。** - -方法如下: - -```java -synchronizedCollection(Collection c) //返回指定 collection 支持的同步(线程安全的)collection。 -synchronizedList(List list)//返回指定列表支持的同步(线程安全的)List。 -synchronizedMap(Map m) //返回由指定映射支持的同步(线程安全的)Map。 -synchronizedSet(Set s) //返回指定 set 支持的同步(线程安全的)set。 -``` - -### Collections还可以设置不可变集合,提供了如下三类方法: - -```java -emptyXxx(): 返回一个空的、不可变的集合对象,此处的集合既可以是List,也可以是Set,还可以是Map。 -singletonXxx(): 返回一个只包含指定对象(只有一个或一个元素)的不可变的集合对象,此处的集合可以是:List,Set,Map。 -unmodifiableXxx(): 返回指定集合对象的不可变视图,此处的集合可以是:List,Set,Map。 -上面三类方法的参数是原有的集合对象,返回值是该集合的”只读“版本。 -``` - -**示例代码:** - -```java - ArrayList arrayList = new ArrayList(); - arrayList.add(-1); - arrayList.add(3); - arrayList.add(3); - arrayList.add(-5); - arrayList.add(7); - arrayList.add(4); - arrayList.add(-9); - arrayList.add(-7); - HashSet integers1 = new HashSet<>(); - integers1.add(1); - integers1.add(3); - integers1.add(2); - Map scores = new HashMap(); - scores.put("语文" , 80); - scores.put("Java" , 82); - - //Collections.emptyXXX();创建一个空的、不可改变的XXX对象 - List list = Collections.emptyList(); - System.out.println(list);//[] - Set objects = Collections.emptySet(); - System.out.println(objects);//[] - Map objectObjectMap = Collections.emptyMap(); - System.out.println(objectObjectMap);//{} - - //Collections.singletonXXX(); - List> arrayLists = Collections.singletonList(arrayList); - System.out.println(arrayLists);//[[-1, 3, 3, -5, 7, 4, -9, -7]] - //创建一个只有一个元素,且不可改变的Set对象 - Set> singleton = Collections.singleton(arrayList); - System.out.println(singleton);//[[-1, 3, 3, -5, 7, 4, -9, -7]] - Map nihao = Collections.singletonMap("1", "nihao"); - System.out.println(nihao);//{1=nihao} - - //unmodifiableXXX();创建普通XXX对象对应的不可变版本 - List integers = Collections.unmodifiableList(arrayList); - System.out.println(integers);//[-1, 3, 3, -5, 7, 4, -9, -7] - Set integers2 = Collections.unmodifiableSet(integers1); - System.out.println(integers2);//[1, 2, 3] - Map objectObjectMap2 = Collections.unmodifiableMap(scores); - System.out.println(objectObjectMap2);//{Java=82, 语文=80} - - //添加出现异常:java.lang.UnsupportedOperationException -// list.add(1); -// arrayLists.add(arrayList); -// integers.add(1); -``` - -## Arrays类的常见操作 -1. 排序 : `sort()` -2. 查找 : `binarySearch()` -3. 比较: `equals()` -4. 填充 : `fill()` -5. 转列表: `asList()` -6. 转字符串 : `toString()` -7. 复制: `copyOf()` - - -### 排序 : `sort()` - -```java - // *************排序 sort**************** - int a[] = { 1, 3, 2, 7, 6, 5, 4, 9 }; - // sort(int[] a)方法按照数字顺序排列指定的数组。 - Arrays.sort(a); - System.out.println("Arrays.sort(a):"); - for (int i : a) { - System.out.print(i); - } - // 换行 - System.out.println(); - - // sort(int[] a,int fromIndex,int toIndex)按升序排列数组的指定范围 - int b[] = { 1, 3, 2, 7, 6, 5, 4, 9 }; - Arrays.sort(b, 2, 6); - System.out.println("Arrays.sort(b, 2, 6):"); - for (int i : b) { - System.out.print(i); - } - // 换行 - System.out.println(); - - int c[] = { 1, 3, 2, 7, 6, 5, 4, 9 }; - // parallelSort(int[] a) 按照数字顺序排列指定的数组(并行的)。同sort方法一样也有按范围的排序 - Arrays.parallelSort(c); - System.out.println("Arrays.parallelSort(c):"); - for (int i : c) { - System.out.print(i); - } - // 换行 - System.out.println(); - - // parallelSort给字符数组排序,sort也可以 - char d[] = { 'a', 'f', 'b', 'c', 'e', 'A', 'C', 'B' }; - Arrays.parallelSort(d); - System.out.println("Arrays.parallelSort(d):"); - for (char d2 : d) { - System.out.print(d2); - } - // 换行 - System.out.println(); - -``` - -在做算法面试题的时候,我们还可能会经常遇到对字符串排序的情况,`Arrays.sort()` 对每个字符串的特定位置进行比较,然后按照升序排序。 - -```java -String[] strs = { "abcdehg", "abcdefg", "abcdeag" }; -Arrays.sort(strs); -System.out.println(Arrays.toString(strs));//[abcdeag, abcdefg, abcdehg] -``` - -### 查找 : `binarySearch()` - -```java - // *************查找 binarySearch()**************** - char[] e = { 'a', 'f', 'b', 'c', 'e', 'A', 'C', 'B' }; - // 排序后再进行二分查找,否则找不到 - Arrays.sort(e); - System.out.println("Arrays.sort(e)" + Arrays.toString(e)); - System.out.println("Arrays.binarySearch(e, 'c'):"); - int s = Arrays.binarySearch(e, 'c'); - System.out.println("字符c在数组的位置:" + s); -``` - -### 比较: `equals()` - -```java - // *************比较 equals**************** - char[] e = { 'a', 'f', 'b', 'c', 'e', 'A', 'C', 'B' }; - char[] f = { 'a', 'f', 'b', 'c', 'e', 'A', 'C', 'B' }; - /* - * 元素数量相同,并且相同位置的元素相同。 另外,如果两个数组引用都是null,则它们被认为是相等的 。 - */ - // 输出true - System.out.println("Arrays.equals(e, f):" + Arrays.equals(e, f)); -``` - -### 填充 : `fill()` - -```java - // *************填充fill(批量初始化)**************** - int[] g = { 1, 2, 3, 3, 3, 3, 6, 6, 6 }; - // 数组中所有元素重新分配值 - Arrays.fill(g, 3); - System.out.println("Arrays.fill(g, 3):"); - // 输出结果:333333333 - for (int i : g) { - System.out.print(i); - } - // 换行 - System.out.println(); - - int[] h = { 1, 2, 3, 3, 3, 3, 6, 6, 6, }; - // 数组中指定范围元素重新分配值 - Arrays.fill(h, 0, 2, 9); - System.out.println("Arrays.fill(h, 0, 2, 9);:"); - // 输出结果:993333666 - for (int i : h) { - System.out.print(i); - } -``` - -### 转列表 `asList()` - -```java - // *************转列表 asList()**************** - /* - * 返回由指定数组支持的固定大小的列表。 - * (将返回的列表更改为“写入数组”。)该方法作为基于数组和基于集合的API之间的桥梁,与Collection.toArray()相结合 。 - * 返回的列表是可序列化的,并实现RandomAccess 。 - * 此方法还提供了一种方便的方式来创建一个初始化为包含几个元素的固定大小的列表如下: - */ - List stooges = Arrays.asList("Larry", "Moe", "Curly"); - System.out.println(stooges); -``` - -### 转字符串 `toString()` - -```java - // *************转字符串 toString()**************** - /* - * 返回指定数组的内容的字符串表示形式。 - */ - char[] k = { 'a', 'f', 'b', 'c', 'e', 'A', 'C', 'B' }; - System.out.println(Arrays.toString(k));// [a, f, b, c, e, A, C, B] -``` - -### 复制 `copyOf()` - -```java - // *************复制 copy**************** - // copyOf 方法实现数组复制,h为数组,6为复制的长度 - int[] h = { 1, 2, 3, 3, 3, 3, 6, 6, 6, }; - int i[] = Arrays.copyOf(h, 6); - System.out.println("Arrays.copyOf(h, 6);:"); - // 输出结果:123333 - for (int j : i) { - System.out.print(j); - } - // 换行 - System.out.println(); - // copyOfRange将指定数组的指定范围复制到新数组中 - int j[] = Arrays.copyOfRange(h, 6, 11); - System.out.println("Arrays.copyOfRange(h, 6, 11):"); - // 输出结果66600(h数组只有9个元素这里是从索引6到索引11复制所以不足的就为0) - for (int j2 : j) { - System.out.print(j2); - } - // 换行 - System.out.println(); -``` diff --git "a/docs/java/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/Basis/final\343\200\201static\343\200\201this\343\200\201super.md" "b/docs/java/Basis/final\343\200\201static\343\200\201this\343\200\201super.md" deleted file mode 100644 index 77e8b09..0000000 --- "a/docs/java/Basis/final\343\200\201static\343\200\201this\343\200\201super.md" +++ /dev/null @@ -1,342 +0,0 @@ - - -- [final,static,this,super 关键字总结](#finalstaticthissuper-关键字总结) - - [final 关键字](#final-关键字) - - [static 关键字](#static-关键字) - - [this 关键字](#this-关键字) - - [super 关键字](#super-关键字) - - [参考](#参考) -- [static 关键字详解](#static-关键字详解) - - [static 关键字主要有以下四种使用场景](#static-关键字主要有以下四种使用场景) - - [修饰成员变量和成员方法\(常用\)](#修饰成员变量和成员方法常用) - - [静态代码块](#静态代码块) - - [静态内部类](#静态内部类) - - [静态导包](#静态导包) - - [补充内容](#补充内容) - - [静态方法与非静态方法](#静态方法与非静态方法) - - [static{}静态代码块与{}非静态代码块\(构造代码块\)](#static静态代码块与非静态代码块构造代码块) - - [参考](#参考-1) - - - -# final,static,this,super 关键字总结 - -## final 关键字 - -**final关键字主要用在三个地方:变量、方法、类。** - -1. **对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。** - -2. **当用final修饰一个类时,表明这个类不能被继承。final类中的所有成员方法都会被隐式地指定为final方法。** - -3. 使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的Java版本已经不需要使用final方法进行这些优化了)。类中所有的private方法都隐式地指定为final。 - -## static 关键字 - -**static 关键字主要有以下四种使用场景:** - -1. **修饰成员变量和成员方法:** 被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。调用格式:`类名.静态变量名` `类名.静态方法名()` -2. **静态代码块:** 静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—>非静态代码块—>构造方法)。 该类不管创建多少对象,静态代码块只执行一次. -3. **静态内部类(static修饰类的话只能修饰内部类):** 静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:1. 它的创建是不需要依赖外围类的创建。2. 它不能使用任何外围类的非static成员变量和方法。 -4. **静态导包(用来导入类中的静态资源,1.5之后的新特性):** 格式为:`import static` 这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法。 - -## this 关键字 - -this关键字用于引用类的当前实例。 例如: - -```java -class Manager { - Employees[] employees; - - void manageEmployees() { - int totalEmp = this.employees.length; - System.out.println("Total employees: " + totalEmp); - this.report(); - } - - void report() { } -} -``` - -在上面的示例中,this关键字用于两个地方: - -- this.employees.length:访问类Manager的当前实例的变量。 -- this.report():调用类Manager的当前实例的方法。 - -此关键字是可选的,这意味着如果上面的示例在不使用此关键字的情况下表现相同。 但是,使用此关键字可能会使代码更易读或易懂。 - -## super 关键字 - -super关键字用于从子类访问父类的变量和方法。 例如: - -```java -public class Super { - protected int number; - - protected showNumber() { - System.out.println("number = " + number); - } -} - -public class Sub extends Super { - void bar() { - super.number = 10; - super.showNumber(); - } -} -``` - -在上面的例子中,Sub 类访问父类成员变量 number 并调用其其父类 Super 的 `showNumber()` 方法。 - -**使用 this 和 super 要注意的问题:** - -- 在构造器中使用 `super()` 调用父类中的其他构造方法时,该语句必须处于构造器的首行,否则编译器会报错。另外,this 调用本类中的其他构造方法时,也要放在首行。 -- this、super不能用在static方法中。 - -**简单解释一下:** - -被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享。而 this 代表对本类对象的引用,指向本类对象;而 super 代表对父类对象的引用,指向父类对象;所以, **this和super是属于对象范畴的东西,而静态方法是属于类范畴的东西**。 - -## 参考 - -- https://www.codejava.net/java-core/the-java-language/java-keywords -- https://blog.csdn.net/u013393958/article/details/79881037 - -# static 关键字详解 - -## static 关键字主要有以下四种使用场景 - -1. 修饰成员变量和成员方法 -2. 静态代码块 -3. 修饰类(只能修饰内部类) -4. 静态导包(用来导入类中的静态资源,1.5之后的新特性) - -### 修饰成员变量和成员方法(常用) - -被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。 - -方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。 - - HotSpot 虚拟机中方法区也常被称为 “永久代”,本质上两者并不等价。仅仅是因为 HotSpot 虚拟机设计团队用永久代来实现方法区而已,这样 HotSpot 虚拟机的垃圾收集器就可以像管理 Java 堆一样管理这部分内存了。但是这并不是一个好主意,因为这样更容易遇到内存溢出问题。 - -调用格式: - -- 类名.静态变量名 -- 类名.静态方法名() - -如果变量或者方法被 private 则代表该属性或者该方法只能在类的内部被访问而不能在类的外部被访问。 - -测试方法: - -```java -public class StaticBean { - - String name; - 静态变量 - static int age; - - public StaticBean(String name) { - this.name = name; - } - 静态方法 - static void SayHello() { - System.out.println(Hello i am java); - } - @Override - public String toString() { - return StaticBean{ + - name=' + name + ''' + age + age + - '}'; - } -} -``` - -```java -public class StaticDemo { - - public static void main(String[] args) { - StaticBean staticBean = new StaticBean(1); - StaticBean staticBean2 = new StaticBean(2); - StaticBean staticBean3 = new StaticBean(3); - StaticBean staticBean4 = new StaticBean(4); - StaticBean.age = 33; - StaticBean{name='1'age33} StaticBean{name='2'age33} StaticBean{name='3'age33} StaticBean{name='4'age33} - System.out.println(staticBean+ +staticBean2+ +staticBean3+ +staticBean4); - StaticBean.SayHello();Hello i am java - } - -} -``` - - -### 静态代码块 - -静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—非静态代码块—构造方法)。 该类不管创建多少对象,静态代码块只执行一次. - -静态代码块的格式是 - -``` -static { -语句体; -} -``` - - -一个类中的静态代码块可以有多个,位置可以随便放,它不在任何的方法体内,JVM加载类时会执行这些静态的代码块,如果静态代码块有多个,JVM将按照它们在类中出现的先后顺序依次执行它们,每个代码块只会被执行一次。 - -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-14/88531075.jpg) - -静态代码块对于定义在它之后的静态变量,可以赋值,但是不能访问. - - -### 静态内部类 - -静态内部类与非静态内部类之间存在一个最大的区别,我们知道非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着: - -1. 它的创建是不需要依赖外围类的创建。 -2. 它不能使用任何外围类的非static成员变量和方法。 - - -Example(静态内部类实现单例模式) - -```java -public class Singleton { - - 声明为 private 避免调用默认构造方法创建对象 - private Singleton() { - } - - 声明为 private 表明静态内部该类只能在该 Singleton 类中被访问 - private static class SingletonHolder { - private static final Singleton INSTANCE = new Singleton(); - } - - public static Singleton getUniqueInstance() { - return SingletonHolder.INSTANCE; - } -} -``` - -当 Singleton 类加载时,静态内部类 SingletonHolder 没有被加载进内存。只有当调用 `getUniqueInstance() `方法从而触发 `SingletonHolder.INSTANCE` 时 SingletonHolder 才会被加载,此时初始化 INSTANCE 实例,并且 JVM 能确保 INSTANCE 只被实例化一次。 - -这种方式不仅具有延迟初始化的好处,而且由 JVM 提供了对线程安全的支持。 - -### 静态导包 - -格式为:import static - -这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法 - -```java - - - Math. --- 将Math中的所有静态资源导入,这时候可以直接使用里面的静态方法,而不用通过类名进行调用 - 如果只想导入单一某个静态方法,只需要将换成对应的方法名即可 - -import static java.lang.Math.; - - 换成import static java.lang.Math.max;具有一样的效果 - -public class Demo { - public static void main(String[] args) { - - int max = max(1,2); - System.out.println(max); - } -} - -``` - - -## 补充内容 - -### 静态方法与非静态方法 - -静态方法属于类本身,非静态方法属于从该类生成的每个对象。 如果您的方法执行的操作不依赖于其类的各个变量和方法,请将其设置为静态(这将使程序的占用空间更小)。 否则,它应该是非静态的。 - -Example - -```java -class Foo { - int i; - public Foo(int i) { - this.i = i; - } - - public static String method1() { - return An example string that doesn't depend on i (an instance variable); - - } - - public int method2() { - return this.i + 1; Depends on i - } - -} -``` -你可以像这样调用静态方法:`Foo.method1()`。 如果您尝试使用这种方法调用 method2 将失败。 但这样可行:`Foo bar = new Foo(1);bar.method2();` - -总结: - -- 在外部调用静态方法时,可以使用”类名.方法名”的方式,也可以使用”对象名.方法名”的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。 -- 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制 - -### static{}静态代码块与{}非静态代码块(构造代码块) - -相同点: 都是在JVM加载类时且在构造方法执行之前执行,在类中都可以定义多个,定义多个时按定义的顺序执行,一般在代码块中对一些static变量进行赋值。 - -不同点: 静态代码块在非静态代码块之前执行(静态代码块—非静态代码块—构造方法)。静态代码块只在第一次new执行一次,之后不再执行,而非静态代码块在每new一次就执行一次。 非静态代码块可在普通方法中定义(不过作用不大);而静态代码块不行。 - -一般情况下,如果有些代码比如一些项目最常用的变量或对象必须在项目启动的时候就执行的时候,需要使用静态代码块,这种代码是主动执行的。如果我们想要设计不需要创建对象就可以调用类中的方法,例如:Arrays类,Character类,String类等,就需要使用静态方法, 两者的区别是 静态代码块是自动执行的而静态方法是被调用的时候才执行的. - -Example - -```java -public class Test { - public Test() { - System.out.print(默认构造方法!--); - } - - 非静态代码块 - { - System.out.print(非静态代码块!--); - } - 静态代码块 - static { - System.out.print(静态代码块!--); - } - - public static void test() { - System.out.print(静态方法中的内容! --); - { - System.out.print(静态方法中的代码块!--); - } - - } - public static void main(String[] args) { - - Test test = new Test(); - Test.test();静态代码块!--静态方法中的内容! --静态方法中的代码块!-- - } -``` - -当执行 `Test.test();` 时输出: - -``` -静态代码块!--静态方法中的内容! --静态方法中的代码块!-- -``` - -当执行 `Test test = new Test();` 时输出: - -``` -静态代码块!--非静态代码块!--默认构造方法!-- -``` - - -非静态代码块与构造函数的区别是: 非静态代码块是给所有对象进行统一初始化,而构造函数是给对应的对象初始化,因为构造函数是可以多个的,运行哪个构造函数就会建立什么样的对象,但无论建立哪个对象,都会先执行相同的构造代码块。也就是说,构造代码块中定义的是不同对象共性的初始化内容。 - -### 参考 - -- httpsblog.csdn.netchen13579867831articledetails78995480 -- httpwww.cnblogs.comchenssyp3388487.html -- httpwww.cnblogs.comQian123p5713440.html diff --git "a/docs/java/Basis/\347\224\250\345\245\275Java\344\270\255\347\232\204\346\236\232\344\270\276,\347\234\237\347\232\204\346\262\241\346\234\211\351\202\243\344\271\210\347\256\200\345\215\225!.md" "b/docs/java/Basis/\347\224\250\345\245\275Java\344\270\255\347\232\204\346\236\232\344\270\276,\347\234\237\347\232\204\346\262\241\346\234\211\351\202\243\344\271\210\347\256\200\345\215\225!.md" deleted file mode 100644 index 20b0229..0000000 --- "a/docs/java/Basis/\347\224\250\345\245\275Java\344\270\255\347\232\204\346\236\232\344\270\276,\347\234\237\347\232\204\346\262\241\346\234\211\351\202\243\344\271\210\347\256\200\345\215\225!.md" +++ /dev/null @@ -1,561 +0,0 @@ -> 最近重看 Java 枚举,看到这篇觉得还不错的文章,于是简单翻译和完善了一些内容,分享给大家,希望你们也能有所收获。另外,不要忘了文末还有补充哦! -> -> ps: 这里发一篇枚举的文章,也是因为后面要发一篇非常实用的关于 SpringBoot 全局异常处理的比较好的实践,里面就用到了枚举。 -> -> 这篇文章由 JavaGuide 翻译,公众号: JavaGuide,原文地址:https://www.baeldung.com/a-guide-to-java-enums 。 -> -> 转载请注明上面这段文字。 - -## 1.概览 - -在本文中,我们将看到什么是 Java 枚举,它们解决了哪些问题以及如何在实践中使用 Java 枚举实现一些设计模式。 - -enum关键字在 java5 中引入,表示一种特殊类型的类,其总是继承java.lang.Enum类,更多内容可以自行查看其[官方文档](https://docs.oracle.com/javase/6/docs/api/java/lang/Enum.html)。 - -枚举在很多时候会和常量拿来对比,可能因为本身我们大量实际使用枚举的地方就是为了替代常量。那么这种方式由什么优势呢? - -**以这种方式定义的常量使代码更具可读性,允许进行编译时检查,预先记录可接受值的列表,并避免由于传入无效值而引起的意外行为。** - -下面示例定义一个简单的枚举类型 pizza 订单的状态,共有三种 ORDERED, READY, DELIVERED状态: - -```java -package shuang.kou.enumdemo.enumtest; - -public enum PizzaStatus { - ORDERED, - READY, - DELIVERED; -} -``` - -**简单来说,我们通过上面的代码避免了定义常量,我们将所有和 pizza 订单的状态的常量都统一放到了一个枚举类型里面。** - -```java -System.out.println(PizzaStatus.ORDERED.name());//ORDERED -System.out.println(PizzaStatus.ORDERED);//ORDERED -System.out.println(PizzaStatus.ORDERED.name().getClass());//class java.lang.String -System.out.println(PizzaStatus.ORDERED.getClass());//class shuang.kou.enumdemo.enumtest.PizzaStatus -``` - -## 2.自定义枚举方法 - -现在我们对枚举是什么以及如何使用它们有了基本的了解,让我们通过在枚举上定义一些额外的API方法,将上一个示例提升到一个新的水平: - -```java -public class Pizza { - private PizzaStatus status; - public enum PizzaStatus { - ORDERED, - READY, - DELIVERED; - } - - public boolean isDeliverable() { - if (getStatus() == PizzaStatus.READY) { - return true; - } - return false; - } - - // Methods that set and get the status variable. -} -``` - -## 3.使用 == 比较枚举类型 - -由于枚举类型确保JVM中仅存在一个常量实例,因此我们可以安全地使用“ ==”运算符比较两个变量,如上例所示;此外,“ ==”运算符可提供编译时和运行时的安全性。 - -首先,让我们看一下以下代码段中的运行时安全性,其中“ ==”运算符用于比较状态,并且如果两个值均为null 都不会引发 NullPointerException。相反,如果使用equals方法,将抛出 NullPointerException: - -```java -if(testPz.getStatus().equals(Pizza.PizzaStatus.DELIVERED)); -if(testPz.getStatus() == Pizza.PizzaStatus.DELIVERED); -``` - -对于编译时安全性,我们看另一个示例,两个不同枚举类型进行比较,使用equal方法比较结果确定为true,因为getStatus方法的枚举值与另一个类型枚举值一致,但逻辑上应该为false。这个问题可以使用==操作符避免。因为编译器会表示类型不兼容错误: - -```java -if(testPz.getStatus().equals(TestColor.GREEN)); -if(testPz.getStatus() == TestColor.GREEN); -``` - -## 4.在 switch 语句中使用枚举类型 - -```java -public int getDeliveryTimeInDays() { - switch (status) { - case ORDERED: return 5; - case READY: return 2; - case DELIVERED: return 0; - } - return 0; -} -``` - -## 5.枚举类型的属性,方法和构造函数 - -> 文末有我(JavaGuide)的补充。 - -你可以通过在枚举类型中定义属性,方法和构造函数让它变得更加强大。 - -下面,让我们扩展上面的示例,实现从比萨的一个阶段到另一个阶段的过渡,并了解如何摆脱之前使用的if语句和switch语句: - -```java -public class Pizza { - - private PizzaStatus status; - public enum PizzaStatus { - ORDERED (5){ - @Override - public boolean isOrdered() { - return true; - } - }, - READY (2){ - @Override - public boolean isReady() { - return true; - } - }, - DELIVERED (0){ - @Override - public boolean isDelivered() { - return true; - } - }; - - private int timeToDelivery; - - public boolean isOrdered() {return false;} - - public boolean isReady() {return false;} - - public boolean isDelivered(){return false;} - - public int getTimeToDelivery() { - return timeToDelivery; - } - - PizzaStatus (int timeToDelivery) { - this.timeToDelivery = timeToDelivery; - } - } - - public boolean isDeliverable() { - return this.status.isReady(); - } - - public void printTimeToDeliver() { - System.out.println("Time to delivery is " + - this.getStatus().getTimeToDelivery()); - } - - // Methods that set and get the status variable. -} -``` - -下面这段代码展示它是如何 work 的: - -```java -@Test -public void givenPizaOrder_whenReady_thenDeliverable() { - Pizza testPz = new Pizza(); - testPz.setStatus(Pizza.PizzaStatus.READY); - assertTrue(testPz.isDeliverable()); -} -``` - -## 6.EnumSet and EnumMap - -### 6.1. EnumSet - -`EnumSet` 是一种专门为枚举类型所设计的 `Set` 类型。 - -与`HashSet`相比,由于使用了内部位向量表示,因此它是特定 `Enum` 常量集的非常有效且紧凑的表示形式。 - -它提供了类型安全的替代方法,以替代传统的基于int的“位标志”,使我们能够编写更易读和易于维护的简洁代码。 - -`EnumSet` 是抽象类,其有两个实现:`RegularEnumSet` 、`JumboEnumSet`,选择哪一个取决于实例化时枚举中常量的数量。 - -在很多场景中的枚举常量集合操作(如:取子集、增加、删除、`containsAll`和`removeAll`批操作)使用`EnumSet`非常合适;如果需要迭代所有可能的常量则使用`Enum.values()`。 - -```java -public class Pizza { - - private static EnumSet undeliveredPizzaStatuses = - EnumSet.of(PizzaStatus.ORDERED, PizzaStatus.READY); - - private PizzaStatus status; - - public enum PizzaStatus { - ... - } - - public boolean isDeliverable() { - return this.status.isReady(); - } - - public void printTimeToDeliver() { - System.out.println("Time to delivery is " + - this.getStatus().getTimeToDelivery() + " days"); - } - - public static List getAllUndeliveredPizzas(List input) { - return input.stream().filter( - (s) -> undeliveredPizzaStatuses.contains(s.getStatus())) - .collect(Collectors.toList()); - } - - public void deliver() { - if (isDeliverable()) { - PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy() - .deliver(this); - this.setStatus(PizzaStatus.DELIVERED); - } - } - - // Methods that set and get the status variable. -} -``` - - 下面的测试演示了展示了 `EnumSet` 在某些场景下的强大功能: - -```java -@Test -public void givenPizaOrders_whenRetrievingUnDeliveredPzs_thenCorrectlyRetrieved() { - List pzList = new ArrayList<>(); - Pizza pz1 = new Pizza(); - pz1.setStatus(Pizza.PizzaStatus.DELIVERED); - - Pizza pz2 = new Pizza(); - pz2.setStatus(Pizza.PizzaStatus.ORDERED); - - Pizza pz3 = new Pizza(); - pz3.setStatus(Pizza.PizzaStatus.ORDERED); - - Pizza pz4 = new Pizza(); - pz4.setStatus(Pizza.PizzaStatus.READY); - - pzList.add(pz1); - pzList.add(pz2); - pzList.add(pz3); - pzList.add(pz4); - - List undeliveredPzs = Pizza.getAllUndeliveredPizzas(pzList); - assertTrue(undeliveredPzs.size() == 3); -} -``` - -### 6.2. EnumMap - -`EnumMap`是一个专门化的映射实现,用于将枚举常量用作键。与对应的 `HashMap` 相比,它是一个高效紧凑的实现,并且在内部表示为一个数组: - -```java -EnumMap map; -``` - -让我们快速看一个真实的示例,该示例演示如何在实践中使用它: - -```java -public static EnumMap> - groupPizzaByStatus(List pizzaList) { - EnumMap> pzByStatus = - new EnumMap>(PizzaStatus.class); - - for (Pizza pz : pizzaList) { - PizzaStatus status = pz.getStatus(); - if (pzByStatus.containsKey(status)) { - pzByStatus.get(status).add(pz); - } else { - List newPzList = new ArrayList(); - newPzList.add(pz); - pzByStatus.put(status, newPzList); - } - } - return pzByStatus; -} -``` - - 下面的测试演示了展示了 `EnumMap` 在某些场景下的强大功能: - -```java -@Test -public void givenPizaOrders_whenGroupByStatusCalled_thenCorrectlyGrouped() { - List pzList = new ArrayList<>(); - Pizza pz1 = new Pizza(); - pz1.setStatus(Pizza.PizzaStatus.DELIVERED); - - Pizza pz2 = new Pizza(); - pz2.setStatus(Pizza.PizzaStatus.ORDERED); - - Pizza pz3 = new Pizza(); - pz3.setStatus(Pizza.PizzaStatus.ORDERED); - - Pizza pz4 = new Pizza(); - pz4.setStatus(Pizza.PizzaStatus.READY); - - pzList.add(pz1); - pzList.add(pz2); - pzList.add(pz3); - pzList.add(pz4); - - EnumMap> map = Pizza.groupPizzaByStatus(pzList); - assertTrue(map.get(Pizza.PizzaStatus.DELIVERED).size() == 1); - assertTrue(map.get(Pizza.PizzaStatus.ORDERED).size() == 2); - assertTrue(map.get(Pizza.PizzaStatus.READY).size() == 1); -} -``` - -## 7. 通过枚举实现一些设计模式 - -### 7.1 单例模式 - -通常,使用类实现 Singleton 模式并非易事,枚举提供了一种实现单例的简便方法。 - -《Effective Java 》和《Java与模式》都非常推荐这种方式,使用这种方式方式实现枚举可以有什么好处呢? - -《Effective Java》 - -> 这种方法在功能上与公有域方法相近,但是它更加简洁,无偿提供了序列化机制,绝对防止多次实例化,即使是在面对复杂序列化或者反射攻击的时候。虽然这种方法还没有广泛采用,但是单元素的枚举类型已经成为实现 Singleton的最佳方法。 —-《Effective Java 中文版 第二版》 - -《Java与模式》 - -> 《Java与模式》中,作者这样写道,使用枚举来实现单实例控制会更加简洁,而且无偿地提供了序列化机制,并由JVM从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式。 - -下面的代码段显示了如何使用枚举实现单例模式: - -```java -public enum PizzaDeliverySystemConfiguration { - INSTANCE; - PizzaDeliverySystemConfiguration() { - // Initialization configuration which involves - // overriding defaults like delivery strategy - } - - private PizzaDeliveryStrategy deliveryStrategy = PizzaDeliveryStrategy.NORMAL; - - public static PizzaDeliverySystemConfiguration getInstance() { - return INSTANCE; - } - - public PizzaDeliveryStrategy getDeliveryStrategy() { - return deliveryStrategy; - } -} -``` - -如何使用呢?请看下面的代码: - -```java -PizzaDeliveryStrategy deliveryStrategy = PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy(); -``` - -通过 `PizzaDeliverySystemConfiguration.getInstance()` 获取的就是单例的 `PizzaDeliverySystemConfiguration` - -### 7.2 策略模式 - -通常,策略模式由不同类实现同一个接口来实现的。 - - 这也就意味着添加新策略意味着添加新的实现类。使用枚举,可以轻松完成此任务,添加新的实现意味着只定义具有某个实现的另一个实例。 - -下面的代码段显示了如何使用枚举实现策略模式: - -```java -public enum PizzaDeliveryStrategy { - EXPRESS { - @Override - public void deliver(Pizza pz) { - System.out.println("Pizza will be delivered in express mode"); - } - }, - NORMAL { - @Override - public void deliver(Pizza pz) { - System.out.println("Pizza will be delivered in normal mode"); - } - }; - - public abstract void deliver(Pizza pz); -} -``` - -给 `Pizza `增加下面的方法: - -```java -public void deliver() { - if (isDeliverable()) { - PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy() - .deliver(this); - this.setStatus(PizzaStatus.DELIVERED); - } -} -``` - -如何使用呢?请看下面的代码: - -```java -@Test -public void givenPizaOrder_whenDelivered_thenPizzaGetsDeliveredAndStatusChanges() { - Pizza pz = new Pizza(); - pz.setStatus(Pizza.PizzaStatus.READY); - pz.deliver(); - assertTrue(pz.getStatus() == Pizza.PizzaStatus.DELIVERED); -} -``` - -## 8. Java 8 与枚举 - -Pizza 类可以用Java 8重写,您可以看到方法 lambda 和Stream API如何使 `getAllUndeliveredPizzas()`和`groupPizzaByStatus()`方法变得如此简洁: - -`getAllUndeliveredPizzas()`: - -```java -public static List getAllUndeliveredPizzas(List input) { - return input.stream().filter( - (s) -> !deliveredPizzaStatuses.contains(s.getStatus())) - .collect(Collectors.toList()); -} -``` - -`groupPizzaByStatus()` : - -```java -public static EnumMap> - groupPizzaByStatus(List pzList) { - EnumMap> map = pzList.stream().collect( - Collectors.groupingBy(Pizza::getStatus, - () -> new EnumMap<>(PizzaStatus.class), Collectors.toList())); - return map; -} -``` - -## 9. Enum 类型的 JSON 表现形式 - -使用Jackson库,可以将枚举类型的JSON表示为POJO。下面的代码段显示了可以用于同一目的的Jackson批注: - -```java -@JsonFormat(shape = JsonFormat.Shape.OBJECT) -public enum PizzaStatus { - ORDERED (5){ - @Override - public boolean isOrdered() { - return true; - } - }, - READY (2){ - @Override - public boolean isReady() { - return true; - } - }, - DELIVERED (0){ - @Override - public boolean isDelivered() { - return true; - } - }; - - private int timeToDelivery; - - public boolean isOrdered() {return false;} - - public boolean isReady() {return false;} - - public boolean isDelivered(){return false;} - - @JsonProperty("timeToDelivery") - public int getTimeToDelivery() { - return timeToDelivery; - } - - private PizzaStatus (int timeToDelivery) { - this.timeToDelivery = timeToDelivery; - } -} -``` - -我们可以按如下方式使用 `Pizza` 和 `PizzaStatus`: - -```java -Pizza pz = new Pizza(); -pz.setStatus(Pizza.PizzaStatus.READY); -System.out.println(Pizza.getJsonString(pz)); -``` - -生成 Pizza 状态以以下JSON展示: - -```json -{ - "status" : { - "timeToDelivery" : 2, - "ready" : true, - "ordered" : false, - "delivered" : false - }, - "deliverable" : true -} -``` - -有关枚举类型的JSON序列化/反序列化(包括自定义)的更多信息,请参阅[Jackson-将枚举序列化为JSON对象。](https://www.baeldung.com/jackson-serialize-enums) - -## 10.总结 - -本文我们讨论了Java枚举类型,从基础知识到高级应用以及实际应用场景,让我们感受到枚举的强大功能。 - -## 11. 补充 - -我们在上面讲到了,我们可以通过在枚举类型中定义属性,方法和构造函数让它变得更加强大。 - -下面我通过一个实际的例子展示一下,当我们调用短信验证码的时候可能有几种不同的用途,我们在下面这样定义: - -```java - -public enum PinType { - - REGISTER(100000, "注册使用"), - FORGET_PASSWORD(100001, "忘记密码使用"), - UPDATE_PHONE_NUMBER(100002, "更新手机号码使用"); - - private final int code; - private final String message; - - PinType(int code, String message) { - this.code = code; - this.message = message; - } - - public int getCode() { - return code; - } - - public String getMessage() { - return message; - } - - @Override - public String toString() { - return "PinType{" + - "code=" + code + - ", message='" + message + '\'' + - '}'; - } -} -``` - -实际使用: - - ```java -System.out.println(PinType.FORGET_PASSWORD.getCode()); -System.out.println(PinType.FORGET_PASSWORD.getMessage()); -System.out.println(PinType.FORGET_PASSWORD.toString()); - ``` - -Output: - -```java -100001 -忘记密码使用 -PinType{code=100001, message='忘记密码使用'} -``` - -这样的话,在实际使用起来就会非常灵活方便! \ 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/J2EE\345\237\272\347\241\200\347\237\245\350\257\206.md" "b/docs/java/J2EE\345\237\272\347\241\200\347\237\245\350\257\206.md" deleted file mode 100644 index 22ce691..0000000 --- "a/docs/java/J2EE\345\237\272\347\241\200\347\237\245\350\257\206.md" +++ /dev/null @@ -1,301 +0,0 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 - - - -- [Servlet总结](#servlet总结) -- [阐述Servlet和CGI的区别?](#阐述servlet和cgi的区别) - - [CGI的不足之处:](#cgi的不足之处) - - [Servlet的优点:](#servlet的优点) -- [Servlet接口中有哪些方法及Servlet生命周期探秘](#servlet接口中有哪些方法及servlet生命周期探秘) -- [get和post请求的区别](#get和post请求的区别) -- [什么情况下调用doGet\(\)和doPost\(\)](#什么情况下调用doget和dopost) -- [转发(Forward)和重定向(Redirect)的区别](#转发forward和重定向redirect的区别) -- [自动刷新\(Refresh\)](#自动刷新refresh) -- [Servlet与线程安全](#servlet与线程安全) -- [JSP和Servlet是什么关系](#jsp和servlet是什么关系) -- [JSP工作原理](#jsp工作原理) -- [JSP有哪些内置对象、作用分别是什么](#jsp有哪些内置对象、作用分别是什么) -- [Request对象的主要方法有哪些](#request对象的主要方法有哪些) -- [request.getAttribute\(\)和 request.getParameter\(\)有何区别](#requestgetattribute和-requestgetparameter有何区别) -- [include指令include的行为的区别](#include指令include的行为的区别) -- [JSP九大内置对象,七大动作,三大指令](#jsp九大内置对象,七大动作,三大指令) -- [讲解JSP中的四种作用域](#讲解jsp中的四种作用域) -- [如何实现JSP或Servlet的单线程模式](#如何实现jsp或servlet的单线程模式) -- [实现会话跟踪的技术有哪些](#实现会话跟踪的技术有哪些) -- [Cookie和Session的的区别](#cookie和session的的区别) - - - -## Servlet总结 - -在Java Web程序中,**Servlet**主要负责接收用户请求 `HttpServletRequest`,在`doGet()`,`doPost()`中做相应的处理,并将回应`HttpServletResponse`反馈给用户。**Servlet** 可以设置初始化参数,供Servlet内部使用。一个Servlet类只会有一个实例,在它初始化时调用`init()`方法,销毁时调用`destroy()`方法**。**Servlet需要在web.xml中配置(MyEclipse中创建Servlet会自动配置),**一个Servlet可以设置多个URL访问**。**Servlet不是线程安全**,因此要谨慎使用类变量。 - -## 阐述Servlet和CGI的区别? - -### CGI的不足之处: - -1,需要为每个请求启动一个操作CGI程序的系统进程。如果请求频繁,这将会带来很大的开销。 - -2,需要为每个请求加载和运行一个CGI程序,这将带来很大的开销 - -3,需要重复编写处理网络协议的代码以及编码,这些工作都是非常耗时的。 - -### Servlet的优点: - -1,只需要启动一个操作系统进程以及加载一个JVM,大大降低了系统的开销 - -2,如果多个请求需要做同样处理的时候,这时候只需要加载一个类,这也大大降低了开销 - -3,所有动态加载的类可以实现对网络协议以及请求解码的共享,大大降低了工作量。 - -4,Servlet能直接和Web服务器交互,而普通的CGI程序不能。Servlet还能在各个程序之间共享数据,使数据库连接池之类的功能很容易实现。 - -补充:Sun Microsystems公司在1996年发布Servlet技术就是为了和CGI进行竞争,Servlet是一个特殊的Java程序,一个基于Java的Web应用通常包含一个或多个Servlet类。Servlet不能够自行创建并执行,它是在Servlet容器中运行的,容器将用户的请求传递给Servlet程序,并将Servlet的响应回传给用户。通常一个Servlet会关联一个或多个JSP页面。以前CGI经常因为性能开销上的问题被诟病,然而Fast CGI早就已经解决了CGI效率上的问题,所以面试的时候大可不必信口开河的诟病CGI,事实上有很多你熟悉的网站都使用了CGI技术。 - -参考:《javaweb整合开发王者归来》P7 - -## Servlet接口中有哪些方法及Servlet生命周期探秘 -Servlet接口定义了5个方法,其中**前三个方法与Servlet生命周期相关**: - -- `void init(ServletConfig config) throws ServletException` -- `void service(ServletRequest req, ServletResponse resp) throws ServletException, java.io.IOException` -- `void destroy()` -- `java.lang.String getServletInfo()` -- `ServletConfig getServletConfig()` - -**生命周期:** **Web容器加载Servlet并将其实例化后,Servlet生命周期开始**,容器运行其**init()方法**进行Servlet的初始化;请求到达时调用Servlet的**service()方法**,service()方法会根据需要调用与请求对应的**doGet或doPost**等方法;当服务器关闭或项目被卸载时服务器会将Servlet实例销毁,此时会调用Servlet的**destroy()方法**。**init方法和destroy方法只会执行一次,service方法客户端每次请求Servlet都会执行**。Servlet中有时会用到一些需要初始化与销毁的资源,因此可以把初始化资源的代码放入init方法中,销毁资源的代码放入destroy方法中,这样就不需要每次处理客户端的请求都要初始化与销毁资源。 - -参考:《javaweb整合开发王者归来》P81 - -## get和post请求的区别 - -get和post请求实际上是没有区别,大家可以自行查询相关文章(参考文章:[https://www.cnblogs.com/logsharing/p/8448446.html](https://www.cnblogs.com/logsharing/p/8448446.html),知乎对应的问题链接:[get和post区别?](https://www.zhihu.com/question/28586791))! - -可以把 get 和 post 当作两个不同的行为,两者并没有什么本质区别,底层都是 TCP 连接。 get请求用来从服务器上获得资源,而post是用来向服务器提交数据。比如你要获取人员列表可以用 get 请求,你需要创建一个人员可以用 post 。这也是 Restful API 最基本的一个要求。 - -推荐阅读: - -- https://www.zhihu.com/question/28586791 -- https://mp.weixin.qq.com/s?__biz=MzI3NzIzMzg3Mw==&mid=100000054&idx=1&sn=71f6c214f3833d9ca20b9f7dcd9d33e4#rd - -## 什么情况下调用doGet()和doPost() -Form标签里的method的属性为get时调用doGet(),为post时调用doPost()。 - -## 转发(Forward)和重定向(Redirect)的区别 - -**转发是服务器行为,重定向是客户端行为。** - -**转发(Forward)** -通过RequestDispatcher对象的forward(HttpServletRequest request,HttpServletResponse response)方法实现的。RequestDispatcher可以通过HttpServletRequest 的getRequestDispatcher()方法获得。例如下面的代码就是跳转到login_success.jsp页面。 -```java - request.getRequestDispatcher("login_success.jsp").forward(request, response); -``` -**重定向(Redirect)** 是利用服务器返回的状态码来实现的。客户端浏览器请求服务器的时候,服务器会返回一个状态码。服务器通过 `HttpServletResponse` 的 `setStatus(int status)` 方法设置状态码。如果服务器返回301或者302,则浏览器会到新的网址重新请求该资源。 - -1. **从地址栏显示来说** - -forward是服务器请求资源,服务器直接访问目标地址的URL,把那个URL的响应内容读取过来,然后把这些内容再发给浏览器.浏览器根本不知道服务器发送的内容从哪里来的,所以它的地址栏还是原来的地址. -redirect是服务端根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址.所以地址栏显示的是新的URL. - -2. **从数据共享来说** - -forward:转发页面和转发到的页面可以共享request里面的数据. -redirect:不能共享数据. - -3. **从运用地方来说** - -forward:一般用于用户登陆的时候,根据角色转发到相应的模块. -redirect:一般用于用户注销登陆时返回主页面和跳转到其它的网站等 - -4. 从效率来说 - -forward:高. -redirect:低. - -## 自动刷新(Refresh) -自动刷新不仅可以实现一段时间之后自动跳转到另一个页面,还可以实现一段时间之后自动刷新本页面。Servlet中通过HttpServletResponse对象设置Header属性实现自动刷新例如: -```java -Response.setHeader("Refresh","5;URL=http://localhost:8080/servlet/example.htm"); -``` -其中5为时间,单位为秒。URL指定就是要跳转的页面(如果设置自己的路径,就会实现每过5秒自动刷新本页面一次) - - -## Servlet与线程安全 -**Servlet不是线程安全的,多线程并发的读写会导致数据不同步的问题。** 解决的办法是尽量不要定义name属性,而是要把name变量分别定义在doGet()和doPost()方法内。虽然使用synchronized(name){}语句块可以解决问题,但是会造成线程的等待,不是很科学的办法。 -注意:多线程的并发的读写Servlet类属性会导致数据不同步。但是如果只是并发地读取属性而不写入,则不存在数据不同步的问题。因此Servlet里的只读属性最好定义为final类型的。 - -参考:《javaweb整合开发王者归来》P92 - - - -## JSP和Servlet是什么关系 -其实这个问题在上面已经阐述过了,Servlet是一个特殊的Java程序,它运行于服务器的JVM中,能够依靠服务器的支持向浏览器提供显示内容。JSP本质上是Servlet的一种简易形式,JSP会被服务器处理成一个类似于Servlet的Java程序,可以简化页面内容的生成。Servlet和JSP最主要的不同点在于,Servlet的应用逻辑是在Java文件中,并且完全从表示层中的HTML分离开来。而JSP的情况是Java和HTML可以组合成一个扩展名为.jsp的文件。有人说,Servlet就是在Java中写HTML,而JSP就是在HTML中写Java代码,当然这个说法是很片面且不够准确的。JSP侧重于视图,Servlet更侧重于控制逻辑,在MVC架构模式中,JSP适合充当视图(view)而Servlet适合充当控制器(controller)。 - -## JSP工作原理 -JSP是一种Servlet,但是与HttpServlet的工作方式不太一样。HttpServlet是先由源代码编译为class文件后部署到服务器下,为先编译后部署。而JSP则是先部署后编译。JSP会在客户端第一次请求JSP文件时被编译为HttpJspPage类(接口Servlet的一个子类)。该类会被服务器临时存放在服务器工作目录里面。下面通过实例给大家介绍。 -工程JspLoginDemo下有一个名为login.jsp的Jsp文件,把工程第一次部署到服务器上后访问这个Jsp文件,我们发现这个目录下多了下图这两个东东。 -.class文件便是JSP对应的Servlet。编译完毕后再运行class文件来响应客户端请求。以后客户端访问login.jsp的时候,Tomcat将不再重新编译JSP文件,而是直接调用class文件来响应客户端请求。 -![JSP工作原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/1.png) -由于JSP只会在客户端第一次请求的时候被编译 ,因此第一次请求JSP时会感觉比较慢,之后就会感觉快很多。如果把服务器保存的class文件删除,服务器也会重新编译JSP。 - -开发Web程序时经常需要修改JSP。Tomcat能够自动检测到JSP程序的改动。如果检测到JSP源代码发生了改动。Tomcat会在下次客户端请求JSP时重新编译JSP,而不需要重启Tomcat。这种自动检测功能是默认开启的,检测改动会消耗少量的时间,在部署Web应用的时候可以在web.xml中将它关掉。 - -参考:《javaweb整合开发王者归来》P97 - -## JSP有哪些内置对象、作用分别是什么 -[JSP内置对象 - CSDN博客 ](http://blog.csdn.net/qq_34337272/article/details/64310849 ) - -JSP有9个内置对象: -- request:封装客户端的请求,其中包含来自GET或POST请求的参数; -- response:封装服务器对客户端的响应; -- pageContext:通过该对象可以获取其他对象; -- session:封装用户会话的对象; -- application:封装服务器运行环境的对象; -- out:输出服务器响应的输出流对象; -- config:Web应用的配置对象; -- page:JSP页面本身(相当于Java程序中的this); -- exception:封装页面抛出异常的对象。 - - -## Request对象的主要方法有哪些 -- setAttribute(String name,Object):设置名字为name的request 的参数值 -- getAttribute(String name):返回由name指定的属性值 -- getAttributeNames():返回request 对象所有属性的名字集合,结果是一个枚举的实例 -- getCookies():返回客户端的所有 Cookie 对象,结果是一个Cookie 数组 -- getCharacterEncoding() :返回请求中的字符编码方式 = getContentLength() :返回请求的 Body的长度 -- getHeader(String name) :获得HTTP协议定义的文件头信息 -- getHeaders(String name) :返回指定名字的request Header 的所有值,结果是一个枚举的实例 -- getHeaderNames() :返回所以request Header 的名字,结果是一个枚举的实例 -- getInputStream() :返回请求的输入流,用于获得请求中的数据 -- getMethod() :获得客户端向服务器端传送数据的方法 -- getParameter(String name) :获得客户端传送给服务器端的有 name指定的参数值 -- getParameterNames() :获得客户端传送给服务器端的所有参数的名字,结果是一个枚举的实例 -- getParameterValues(String name):获得有name指定的参数的所有值 -- getProtocol():获取客户端向服务器端传送数据所依据的协议名称 -- getQueryString() :获得查询字符串 -- getRequestURI() :获取发出请求字符串的客户端地址 -- getRemoteAddr():获取客户端的 IP 地址 -- getRemoteHost() :获取客户端的名字 -- getSession([Boolean create]) :返回和请求相关 Session -- getServerName() :获取服务器的名字 -- getServletPath():获取客户端所请求的脚本文件的路径 -- getServerPort():获取服务器的端口号 -- removeAttribute(String name):删除请求中的一个属性 - -## request.getAttribute()和 request.getParameter()有何区别 -**从获取方向来看:** - -`getParameter()`是获取 POST/GET 传递的参数值; - -`getAttribute()`是获取对象容器中的数据值; - -**从用途来看:** - -`getParameter()`用于客户端重定向时,即点击了链接或提交按扭时传值用,即用于在用表单或url重定向传值时接收数据用。 - -`getAttribute()` 用于服务器端重定向时,即在 sevlet 中使用了 forward 函数,或 struts 中使用了 -mapping.findForward。 getAttribute 只能收到程序用 setAttribute 传过来的值。 - -另外,可以用 `setAttribute()`,`getAttribute()` 发送接收对象.而 `getParameter()` 显然只能传字符串。 -`setAttribute()` 是应用服务器把这个对象放在该页面所对应的一块内存中去,当你的页面服务器重定向到另一个页面时,应用服务器会把这块内存拷贝另一个页面所对应的内存中。这样`getAttribute()`就能取得你所设下的值,当然这种方法可以传对象。session也一样,只是对象在内存中的生命周期不一样而已。`getParameter()`只是应用服务器在分析你送上来的 request页面的文本时,取得你设在表单或 url 重定向时的值。 - -**总结:** - -`getParameter()`返回的是String,用于读取提交的表单中的值;(获取之后会根据实际需要转换为自己需要的相应类型,比如整型,日期类型啊等等) - -`getAttribute()`返回的是Object,需进行转换,可用`setAttribute()`设置成任意对象,使用很灵活,可随时用 - -## include指令include的行为的区别 -**include指令:** JSP可以通过include指令来包含其他文件。被包含的文件可以是JSP文件、HTML文件或文本文件。包含的文件就好像是该JSP文件的一部分,会被同时编译执行。 语法格式如下: -<%@ include file="文件相对 url 地址" %> - -i**nclude动作:** ``动作元素用来包含静态和动态的文件。该动作把指定文件插入正在生成的页面。语法格式如下: - - -## JSP九大内置对象,七大动作,三大指令 -[JSP九大内置对象,七大动作,三大指令总结](http://blog.csdn.net/qq_34337272/article/details/64310849) - -## 讲解JSP中的四种作用域 -JSP中的四种作用域包括page、request、session和application,具体来说: -- **page**代表与一个页面相关的对象和属性。 -- **request**代表与Web客户机发出的一个请求相关的对象和属性。一个请求可能跨越多个页面,涉及多个Web组件;需要在页面显示的临时数据可以置于此作用域。 -- **session**代表与某个用户与服务器建立的一次会话相关的对象和属性。跟某个用户相关的数据应该放在用户自己的session中。 -- **application**代表与整个Web应用程序相关的对象和属性,它实质上是跨越整个Web应用程序,包括多个页面、请求和会话的一个全局作用域。 - -## 如何实现JSP或Servlet的单线程模式 -对于JSP页面,可以通过page指令进行设置。 -`<%@page isThreadSafe="false"%>` - -对于Servlet,可以让自定义的Servlet实现SingleThreadModel标识接口。 - -说明:如果将JSP或Servlet设置成单线程工作模式,会导致每个请求创建一个Servlet实例,这种实践将导致严重的性能问题(服务器的内存压力很大,还会导致频繁的垃圾回收),所以通常情况下并不会这么做。 - -## 实现会话跟踪的技术有哪些 -1. **使用Cookie** - -向客户端发送Cookie -```java -Cookie c =new Cookie("name","value"); //创建Cookie -c.setMaxAge(60*60*24); //设置最大时效,此处设置的最大时效为一天 -response.addCookie(c); //把Cookie放入到HTTP响应中 -``` -从客户端读取Cookie -```java -String name ="name"; -Cookie[]cookies =request.getCookies(); -if(cookies !=null){ - for(int i= 0;i -``` - -**优点:** Cookie被禁时可以使用 - -**缺点:** 所有页面必须是表单提交之后的结果。 - -4. HttpSession - - - 在所有会话跟踪技术中,HttpSession对象是最强大也是功能最多的。当一个用户第一次访问某个网站时会自动创建 HttpSession,每个用户可以访问他自己的HttpSession。可以通过HttpServletRequest对象的getSession方 法获得HttpSession,通过HttpSession的setAttribute方法可以将一个值放在HttpSession中,通过调用 HttpSession对象的getAttribute方法,同时传入属性名就可以获取保存在HttpSession中的对象。与上面三种方式不同的 是,HttpSession放在服务器的内存中,因此不要将过大的对象放在里面,即使目前的Servlet容器可以在内存将满时将HttpSession 中的对象移到其他存储设备中,但是这样势必影响性能。添加到HttpSession中的值可以是任意Java对象,这个对象最好实现了 Serializable接口,这样Servlet容器在必要的时候可以将其序列化到文件中,否则在序列化时就会出现异常。 -## Cookie和Session的的区别 - -Cookie 和 Session都是用来跟踪浏览器用户身份的会话方式,但是两者的应用场景不太一样。 - - **Cookie 一般用来保存用户信息** 比如①我们在 Cookie 中保存已经登录过得用户信息,下次访问网站的时候页面可以自动帮你登录的一些基本信息给填了;②一般的网站都会有保持登录也就是说下次你再访问网站的时候就不需要重新登录了,这是因为用户登录的时候我们可以存放了一个 Token 在 Cookie 中,下次登录的时候只需要根据 Token 值来查找用户即可(为了安全考虑,重新登录一般要将 Token 重写);③登录一次网站后访问网站其他页面不需要重新登录。**Session 的主要作用就是通过服务端记录用户的状态。** 典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,因为 HTTP 协议是无状态的。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了。 - -Cookie 数据保存在客户端(浏览器端),Session 数据保存在服务器端。 - -Cookie 存储在客户端中,而Session存储在服务器上,相对来说 Session 安全性更高。如果使用 Cookie 的一些敏感信息不要写入 Cookie 中,最好能将 Cookie 信息加密然后使用到的时候再去服务器端解密。 - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) diff --git "a/docs/java/Java IO\344\270\216NIO.md" "b/docs/java/Java IO\344\270\216NIO.md" deleted file mode 100644 index 74bd850..0000000 --- "a/docs/java/Java IO\344\270\216NIO.md" +++ /dev/null @@ -1,200 +0,0 @@ - - -- [IO流学习总结](#io流学习总结) - - [一 Java IO,硬骨头也能变软](#一-java-io,硬骨头也能变软) - - [二 java IO体系的学习总结](#二-java-io体系的学习总结) - - [三 Java IO面试题](#三-java-io面试题) -- [NIO与AIO学习总结](#nio与aio学习总结) - - [一 Java NIO 概览](#一-java-nio-概览) - - [二 Java NIO 之 Buffer\(缓冲区\)](#二-java-nio-之-buffer缓冲区) - - [三 Java NIO 之 Channel(通道)](#三-java-nio-之-channel(通道)) - - [四 Java NIO之Selector(选择器)](#四-java-nio之selector(选择器)) - - [五 Java NIO之拥抱Path和Files](#五-java-nio之拥抱path和files) - - [六 NIO学习总结以及NIO新特性介绍](#六-nio学习总结以及nio新特性介绍) - - [七 Java NIO AsynchronousFileChannel异步文件通](#七-java-nio-asynchronousfilechannel异步文件通) - - [八 高并发Java(8):NIO和AIO](#八-高并发java(8):nio和aio) -- [推荐阅读](#推荐阅读) - - [在 Java 7 中体会 NIO.2 异步执行的快乐](#在-java-7-中体会-nio2-异步执行的快乐) - - [Java AIO总结与示例](#java-aio总结与示例) - - - - - -## IO流学习总结 - -### [一 Java IO,硬骨头也能变软](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483981&idx=1&sn=6e5c682d76972c8d2cf271a85dcf09e2&chksm=fd98542ccaefdd3a70428e9549bc33e8165836855edaa748928d16c1ebde9648579d3acaac10#rd) - -**(1) 按操作方式分类结构图:** - -![IO-操作方式分类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/IO-操作方式分类.png) - - -**(2)按操作对象分类结构图** - -![IO-操作对象分类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/IO-操作对象分类.png) - -### [二 java IO体系的学习总结](https://blog.csdn.net/nightcurtis/article/details/51324105) -1. **IO流的分类:** - - 按照流的流向分,可以分为输入流和输出流; - - 按照操作单元划分,可以划分为字节流和字符流; - - 按照流的角色划分为节点流和处理流。 -2. **流的原理浅析:** - - java Io流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java Io流的40多个类都是从如下4个抽象类基类中派生出来的。 - - - **InputStream/Reader**: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。 - - **OutputStream/Writer**: 所有输出流的基类,前者是字节输出流,后者是字符输出流。 -3. **常用的io流的用法** - -### [三 Java IO面试题](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483985&idx=1&sn=38531c2cee7b87f125df7aef41637014&chksm=fd985430caefdd26b0506aa84fc26251877eccba24fac73169a4d6bd1eb5e3fbdf3c3b940261#rd) - -## NIO与AIO学习总结 - - -### [一 Java NIO 概览](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483956&idx=1&sn=57692bc5b7c2c6dfb812489baadc29c9&chksm=fd985455caefdd4331d828d8e89b22f19b304aa87d6da73c5d8c66fcef16e4c0b448b1a6f791#rd) - -1. **NIO简介**: - - Java NIO 是 java 1.4, 之后新出的一套IO接口NIO中的N可以理解为Non-blocking,不单纯是New。 - -2. **NIO的特性/NIO与IO区别:** - - 1)IO是面向流的,NIO是面向缓冲区的; - - 2)IO流是阻塞的,NIO流是不阻塞的; - - 3)NIO有选择器,而IO没有。 -3. **读数据和写数据方式:** - - 从通道进行数据读取 :创建一个缓冲区,然后请求通道读取数据。 - - - 从通道进行数据写入 :创建一个缓冲区,填充数据,并要求通道写入数据。 - -4. **NIO核心组件简单介绍** - - **Channels** - - **Buffers** - - **Selectors** - - -### [二 Java NIO 之 Buffer(缓冲区)](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483961&idx=1&sn=f67bef4c279e78043ff649b6b03fdcbc&chksm=fd985458caefdd4e3317ccbdb2d0a5a70a5024d3255eebf38183919ed9c25ade536017c0a6ba#rd) - -1. **Buffer(缓冲区)介绍:** - - Java NIO Buffers用于和NIO Channel交互。 我们从Channel中读取数据到buffers里,从Buffer把数据写入到Channels; - - Buffer本质上就是一块内存区; - - 一个Buffer有三个属性是必须掌握的,分别是:capacity容量、position位置、limit限制。 -2. **Buffer的常见方法** - - Buffer clear() - - Buffer flip() - - Buffer rewind() - - Buffer position(int newPosition) -3. **Buffer的使用方式/方法介绍:** - - 分配缓冲区(Allocating a Buffer): - ```java - ByteBuffer buf = ByteBuffer.allocate(28);//以ByteBuffer为例子 - ``` - - 写入数据到缓冲区(Writing Data to a Buffer) - - **写数据到Buffer有两种方法:** - - 1.从Channel中写数据到Buffer - ```java - int bytesRead = inChannel.read(buf); //read into buffer. - ``` - 2.通过put写数据: - ```java - buf.put(127); - ``` - -4. **Buffer常用方法测试** - - 说实话,NIO编程真的难,通过后面这个测试例子,你可能才能勉强理解前面说的Buffer方法的作用。 - - -### [三 Java NIO 之 Channel(通道)](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483966&idx=1&sn=d5cf18c69f5f9ec2aff149270422731f&chksm=fd98545fcaefdd49296e2c78000ce5da277435b90ba3c03b92b7cf54c6ccc71d61d13efbce63#rd) - - -1. **Channel(通道)介绍** - - 通常来说NIO中的所有IO都是从 Channel(通道) 开始的。 - - NIO Channel通道和流的区别: -2. **FileChannel的使用** -3. **SocketChannel和ServerSocketChannel的使用** -4. **️DatagramChannel的使用** -5. **Scatter / Gather** - - Scatter: 从一个Channel读取的信息分散到N个缓冲区中(Buufer). - - Gather: 将N个Buffer里面内容按照顺序发送到一个Channel. -6. **通道之间的数据传输** - - 在Java NIO中如果一个channel是FileChannel类型的,那么他可以直接把数据传输到另一个channel。 - - transferFrom() :transferFrom方法把数据从通道源传输到FileChannel - - transferTo() :transferTo方法把FileChannel数据传输到另一个channel - - -### [四 Java NIO之Selector(选择器)](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483970&idx=1&sn=d5e2b133313b1d0f32872d54fbdf0aa7&chksm=fd985423caefdd354b587e57ce6cf5f5a7bec48b9ab7554f39a8d13af47660cae793956e0f46#rd) - - -1. **Selector(选择器)介绍** - - Selector 一般称 为选择器 ,当然你也可以翻译为 多路复用器 。它是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel(通道)的状态是否处于可读、可写。如此可以实现单线程管理多个channels,也就是可以管理多个网络链接。 - - 使用Selector的好处在于: 使用更少的线程来就可以来处理通道了, 相比使用多个线程,避免了线程上下文切换带来的开销。 -2. **Selector(选择器)的使用方法介绍** - - Selector的创建 - ```java - Selector selector = Selector.open(); - ``` - - 注册Channel到Selector(Channel必须是非阻塞的) - ```java - channel.configureBlocking(false); - SelectionKey key = channel.register(selector, Selectionkey.OP_READ); - ``` - - SelectionKey介绍 - - 一个SelectionKey键表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系。 - - 从Selector中选择channel(Selecting Channels via a Selector) - - 选择器维护注册过的通道的集合,并且这种注册关系都被封装在SelectionKey当中. - - 停止选择的方法 - - wakeup()方法 和close()方法。 -3. **模板代码** - - 有了模板代码我们在编写程序时,大多数时间都是在模板代码中添加相应的业务代码。 -4. **客户端与服务端简单交互实例** - - - -### [五 Java NIO之拥抱Path和Files](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483976&idx=1&sn=2296c05fc1b840a64679e2ad7794c96d&chksm=fd985429caefdd3f48e2ee6fdd7b0f6fc419df90b3de46832b484d6d1ca4e74e7837689c8146&token=537240785&lang=zh_CN#rd) - -**一 文件I/O基石:Path:** -- 创建一个Path -- File和Path之间的转换,File和URI之间的转换 -- 获取Path的相关信息 -- 移除Path中的冗余项 - -**二 拥抱Files类:** -- Files.exists() 检测文件路径是否存在 -- Files.createFile() 创建文件 -- Files.createDirectories()和Files.createDirectory()创建文件夹 -- Files.delete()方法 可以删除一个文件或目录 -- Files.copy()方法可以吧一个文件从一个地址复制到另一个位置 -- 获取文件属性 -- 遍历一个文件夹 -- Files.walkFileTree()遍历整个目录 - -### [六 NIO学习总结以及NIO新特性介绍](https://blog.csdn.net/a953713428/article/details/64907250) - -- **内存映射:** - -这个功能主要是为了提高大文件的读写速度而设计的。内存映射文件(memory-mappedfile)能让你创建和修改那些大到无法读入内存的文件。有了内存映射文件,你就可以认为文件已经全部读进了内存,然后把它当成一个非常大的数组来访问了。将文件的一段区域映射到内存中,比传统的文件处理速度要快很多。内存映射文件它虽然最终也是要从磁盘读取数据,但是它并不需要将数据读取到OS内核缓冲区,而是直接将进程的用户私有地址空间中的一部分区域与文件对象建立起映射关系,就好像直接从内存中读、写文件一样,速度当然快了。 - -### [七 Java NIO AsynchronousFileChannel异步文件通](http://wiki.jikexueyuan.com/project/java-nio-zh/java-nio-asynchronousfilechannel.html) - -Java7中新增了AsynchronousFileChannel作为nio的一部分。AsynchronousFileChannel使得数据可以进行异步读写。 - -### [八 高并发Java(8):NIO和AIO](http://www.importnew.com/21341.html) - - - -## 推荐阅读 - -### [在 Java 7 中体会 NIO.2 异步执行的快乐](https://www.ibm.com/developerworks/cn/java/j-lo-nio2/index.html) - -### [Java AIO总结与示例](https://blog.csdn.net/x_i_y_u_e/article/details/52223406) -AIO是异步IO的缩写,虽然NIO在网络操作中,提供了非阻塞的方法,但是NIO的IO行为还是同步的。对于NIO来说,我们的业务线程是在IO操作准备好时,得到通知,接着就由这个线程自行进行IO操作,IO操作本身是同步的。 - - -**欢迎关注我的微信公众号:"Java面试通关手册"(一个有温度的微信公众号,期待与你共同进步~~~坚持原创,分享美文,分享各种Java学习资源):** diff --git "a/docs/java/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\237\272\347\241\200\347\237\245\350\257\206.md" "b/docs/java/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" deleted file mode 100644 index 2f214d0..0000000 --- "a/docs/java/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" +++ /dev/null @@ -1,556 +0,0 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 - - - -- [1. 面向对象和面向过程的区别](#1-面向对象和面向过程的区别) -- [2. Java 语言有哪些特点?](#2-java-语言有哪些特点) -- [3. 关于 JVM JDK 和 JRE 最详细通俗的解答](#3-关于-jvm-jdk-和-jre-最详细通俗的解答) - - [JVM](#jvm) - - [JDK 和 JRE](#jdk-和-jre) -- [4. Oracle JDK 和 OpenJDK 的对比](#4-oracle-jdk-和-openjdk-的对比) -- [5. Java和C++的区别?](#5-java和c的区别) -- [6. 什么是 Java 程序的主类 应用程序和小程序的主类有何不同?](#6-什么是-java-程序的主类-应用程序和小程序的主类有何不同) -- [7. Java 应用程序与小程序之间有哪些差别?](#7-java-应用程序与小程序之间有哪些差别) -- [8. 字符型常量和字符串常量的区别?](#8-字符型常量和字符串常量的区别) -- [9. 构造器 Constructor 是否可被 override?](#9-构造器-constructor-是否可被-override) -- [10. 重载和重写的区别](#10-重载和重写的区别) -- [11. Java 面向对象编程三大特性: 封装 继承 多态](#11-java-面向对象编程三大特性-封装-继承-多态) - - [封装](#封装) - - [继承](#继承) - - [多态](#多态) -- [12. String StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的?](#12-string-stringbuffer-和-stringbuilder-的区别是什么-string-为什么是不可变的) -- [13. 自动装箱与拆箱](#13-自动装箱与拆箱) -- [14. 在一个静态方法内调用一个非静态成员为什么是非法的?](#14-在一个静态方法内调用一个非静态成员为什么是非法的) -- [15. 在 Java 中定义一个不做事且没有参数的构造方法的作用](#15-在-java-中定义一个不做事且没有参数的构造方法的作用) -- [16. import java和javax有什么区别?](#16-import-java和javax有什么区别) -- [17. 接口和抽象类的区别是什么?](#17-接口和抽象类的区别是什么) -- [18. 成员变量与局部变量的区别有哪些?](#18-成员变量与局部变量的区别有哪些) -- [19. 创建一个对象用什么运算符?对象实体与对象引用有何不同?](#19-创建一个对象用什么运算符对象实体与对象引用有何不同) -- [20. 什么是方法的返回值?返回值在类的方法里的作用是什么?](#20-什么是方法的返回值返回值在类的方法里的作用是什么) -- [21. 一个类的构造方法的作用是什么? 若一个类没有声明构造方法,该程序能正确执行吗? 为什么?](#21-一个类的构造方法的作用是什么-若一个类没有声明构造方法该程序能正确执行吗-为什么) -- [22. 构造方法有哪些特性?](#22-构造方法有哪些特性) -- [23. 静态方法和实例方法有何不同](#23-静态方法和实例方法有何不同) -- [24. 对象的相等与指向他们的引用相等,两者有什么不同?](#24-对象的相等与指向他们的引用相等两者有什么不同) -- [25. 在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是?](#25-在调用子类构造方法之前会先调用父类没有参数的构造方法其目的是) -- [26. == 与 equals(重要)](#26--与-equals重要) -- [27. hashCode 与 equals (重要)](#27-hashcode-与-equals-重要) - - [hashCode()介绍](#hashcode介绍) - - [为什么要有 hashCode](#为什么要有-hashcode) - - [hashCode()与equals()的相关规定](#hashcode与equals的相关规定) -- [28. 为什么Java中只有值传递?](#28-为什么java中只有值传递) -- [29. 简述线程、程序、进程的基本概念。以及他们之间关系是什么?](#29-简述线程程序进程的基本概念以及他们之间关系是什么) -- [30. 线程有哪些基本状态?](#30-线程有哪些基本状态) -- [31 关于 final 关键字的一些总结](#31-关于-final-关键字的一些总结) -- [32 Java 中的异常处理](#32-java-中的异常处理) - - [Java异常类层次结构图](#java异常类层次结构图) - - [Throwable类常用方法](#throwable类常用方法) - - [异常处理总结](#异常处理总结) -- [33 Java序列化中如果有些字段不想进行序列化,怎么办?](#33-java序列化中如果有些字段不想进行序列化怎么办) -- [34 获取用键盘输入常用的两种方法](#34-获取用键盘输入常用的两种方法) -- [35 Java 中 IO 流](#35-java-中-io-流) - - [Java 中 IO 流分为几种?](#java-中-io-流分为几种) - - [既然有了字节流,为什么还要有字符流?](#既然有了字节流为什么还要有字符流) - - [BIO,NIO,AIO 有什么区别?](#bionioaio-有什么区别) -- [36. 常见关键字总结:static,final,this,super](#36-常见关键字总结staticfinalthissuper) -- [37. Collections 工具类和 Arrays 工具类常见方法总结](#37-collections-工具类和-arrays-工具类常见方法总结) -- [参考](#参考) -- [公众号](#公众号) - - - -## 1. 面向对象和面向过程的区别 - -- **面向过程** :**面向过程性能比面向对象高。** 因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发。但是,**面向过程没有面向对象易维护、易复用、易扩展。** -- **面向对象** :**面向对象易维护、易复用、易扩展。** 因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是,**面向对象性能比面向过程低**。 - -参见 issue : [面向过程 :面向过程性能比面向对象高??](https://github.com/Snailclimb/JavaGuide/issues/431) - -> 这个并不是根本原因,面向过程也需要分配内存,计算内存偏移量,Java性能差的主要原因并不是因为它是面向对象语言,而是Java是半编译语言,最终的执行代码并不是可以直接被CPU执行的二进制机械码。 -> -> 而面向过程语言大多都是直接编译成机械码在电脑上执行,并且其它一些面向过程的脚本语言性能也并不一定比Java好。 - -## 2. Java 语言有哪些特点? - -1. 简单易学; -2. 面向对象(封装,继承,多态); -3. 平台无关性( Java 虚拟机实现平台无关性); -4. 可靠性; -5. 安全性; -6. 支持多线程( C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持); -7. 支持网络编程并且很方便( Java 语言诞生本身就是为简化网络编程设计的,因此 Java 语言不仅支持网络编程而且很方便); -8. 编译与解释并存; - -> 修正(参见: [issue#544](https://github.com/Snailclimb/JavaGuide/issues/544)):C++11开始(2011年的时候),C++就引入了多线程库,在windows、linux、macos都可以使用`std::thread`和`std::async`来创建线程。参考链接:http://www.cplusplus.com/reference/thread/thread/?kw=thread - -## 3. 关于 JVM JDK 和 JRE 最详细通俗的解答 - -### JVM - -Java虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。 - -**什么是字节码?采用字节码的好处是什么?** - -> 在 Java 中,JVM可以理解的代码就叫做`字节码`(即扩展名为 `.class` 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java程序无须重新编译便可在多种不同操作系统的计算机上运行。 - -**Java 程序从源代码到运行一般有下面3步:** - -![Java程序运行过程](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/Java%20%E7%A8%8B%E5%BA%8F%E8%BF%90%E8%A1%8C%E8%BF%87%E7%A8%8B.png) - -我们需要格外注意的是 .class->机器码 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT 编译器,而JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言。 - -> HotSpot采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是JIT所需要编译的部分。JVM会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。JDK 9引入了一种新的编译模式AOT(Ahead of Time Compilation),它是直接将字节码编译成机器码,这样就避免了JIT预热等各方面的开销。JDK支持分层编译和AOT协作使用。但是 ,AOT 编译器的编译质量是肯定比不上 JIT 编译器的。 - -**总结:** - -Java虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。 - -### JDK 和 JRE - -JDK是Java Development Kit,它是功能齐全的Java SDK。它拥有JRE所拥有的一切,还有编译器(javac)和工具(如javadoc和jdb)。它能够创建和编译程序。 - -JRE 是 Java运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java虚拟机(JVM),Java类库,java命令和其他的一些基础构件。但是,它不能用于创建新程序。 - -如果你只是为了运行一下 Java 程序的话,那么你只需要安装 JRE 就可以了。如果你需要进行一些 Java 编程方面的工作,那么你就需要安装JDK了。但是,这不是绝对的。有时,即使您不打算在计算机上进行任何Java开发,仍然需要安装JDK。例如,如果要使用JSP部署Web应用程序,那么从技术上讲,您只是在应用程序服务器中运行Java程序。那你为什么需要JDK呢?因为应用程序服务器会将 JSP 转换为 Java servlet,并且需要使用 JDK 来编译 servlet。 - -## 4. Oracle JDK 和 OpenJDK 的对比 - -可能在看这个问题之前很多人和我一样并没有接触和使用过 OpenJDK 。那么Oracle和OpenJDK之间是否存在重大差异?下面我通过收集到的一些资料,为你解答这个被很多人忽视的问题。 - -对于Java 7,没什么关键的地方。OpenJDK项目主要基于Sun捐赠的HotSpot源代码。此外,OpenJDK被选为Java 7的参考实现,由Oracle工程师维护。关于JVM,JDK,JRE和OpenJDK之间的区别,Oracle博客帖子在2012年有一个更详细的答案: - -> 问:OpenJDK存储库中的源代码与用于构建Oracle JDK的代码之间有什么区别? -> -> 答:非常接近 - 我们的Oracle JDK版本构建过程基于OpenJDK 7构建,只添加了几个部分,例如部署代码,其中包括Oracle的Java插件和Java WebStart的实现,以及一些封闭的源代码派对组件,如图形光栅化器,一些开源的第三方组件,如Rhino,以及一些零碎的东西,如附加文档或第三方字体。展望未来,我们的目的是开源Oracle JDK的所有部分,除了我们考虑商业功能的部分。 - -**总结:** - -1. Oracle JDK大概每6个月发一次主要版本,而OpenJDK版本大概每三个月发布一次。但这不是固定的,我觉得了解这个没啥用处。详情参见:https://blogs.oracle.com/java-platform-group/update-and-faq-on-the-java-se-release-cadence。 -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许可获得许可。 - -## 5. Java和C++的区别? - -我知道很多人没学过 C++,但是面试官就是没事喜欢拿咱们 Java 和 C++ 比呀!没办法!!!就算没学过C++,也要记下来! - -- 都是面向对象的语言,都支持封装、继承和多态 -- Java 不提供指针来直接访问内存,程序内存更加安全 -- Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。 -- Java 有自动内存管理机制,不需要程序员手动释放无用内存 -- **在 C 语言中,字符串或字符数组最后都会有一个额外的字符‘\0’来表示结束。但是,Java 语言中没有结束符这一概念。** 这是一个值得深度思考的问题,具体原因推荐看这篇文章: [https://blog.csdn.net/sszgg2006/article/details/49148189]( https://blog.csdn.net/sszgg2006/article/details/49148189) - - -## 6. 什么是 Java 程序的主类 应用程序和小程序的主类有何不同? - -一个程序中可以有多个类,但只能有一个类是主类。在 Java 应用程序中,这个主类是指包含 main()方法的类。而在 Java 小程序中,这个主类是一个继承自系统类 JApplet 或 Applet 的子类。应用程序的主类不一定要求是 public 类,但小程序的主类要求必须是 public 类。主类是 Java 程序执行的入口点。 - -## 7. Java 应用程序与小程序之间有哪些差别? - -简单说应用程序是从主线程启动(也就是 `main()` 方法)。applet 小程序没有 `main()` 方法,主要是嵌在浏览器页面上运行(调用`init()`或者`run()`来启动),嵌入浏览器这点跟 flash 的小游戏类似。 - -## 8. 字符型常量和字符串常量的区别? - -1. 形式上: 字符常量是单引号引起的一个字符; 字符串常量是双引号引起的若干个字符 -2. 含义上: 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置) -3. 占内存大小 字符常量只占2个字节; 字符串常量占若干个字节 (**注意: char在Java中占两个字节**) - -> java编程思想第四版:2.2.2节 -![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-15/86735519.jpg) - -## 9. 构造器 Constructor 是否可被 override? - -Constructor 不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。 - -## 10. 重载和重写的区别 - -#### 重载 - -发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。 - -下面是《Java核心技术》对重载这个概念的介绍: - -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/bg/desktopjava核心技术-重载.jpg)  - -#### 重写 - - 重写是子类对父类的允许访问的方法的实现过程进行重新编写,发生在子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。另外,如果父类方法访问修饰符为 private 则子类就不能重写该方法。**也就是说方法提供的行为改变,而方法的外貌并没有改变。** - -## 11. Java 面向对象编程三大特性: 封装 继承 多态 - -### 封装 - -封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。 - - -### 继承 -继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。 - -**关于继承如下 3 点请记住:** - -1. 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,**只是拥有**。 -2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。 -3. 子类可以用自己的方式实现父类的方法。(以后介绍)。 - -### 多态 - -所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。 - -在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。 - -## 12. String StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的? - -**可变性** - -简单的来说:String 类中使用 final 关键字修饰字符数组来保存字符串,`private final char value[]`,所以 String 对象是不可变的。而StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串`char[]value` 但是没有用 final 关键字修饰,所以这两种对象都是可变的。 - -StringBuilder 与 StringBuffer 的构造方法都是调用父类构造方法也就是 AbstractStringBuilder 实现的,大家可以自行查阅源码。 - -AbstractStringBuilder.java - -```java -abstract class AbstractStringBuilder implements Appendable, CharSequence { - char[] value; - int count; - AbstractStringBuilder() { - } - AbstractStringBuilder(int capacity) { - value = new char[capacity]; - } -``` - - -**线程安全性** - -String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。  - -**性能** - -每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。 - -**对于三者使用的总结:** - -1. 操作少量的数据: 适用String -2. 单线程操作字符串缓冲区下操作大量数据: 适用StringBuilder -3. 多线程操作字符串缓冲区下操作大量数据: 适用StringBuffer - -## 13. 自动装箱与拆箱 - -- **装箱**:将基本类型用它们对应的引用类型包装起来; -- **拆箱**:将包装类型转换为基本数据类型; - -## 14. 在一个静态方法内调用一个非静态成员为什么是非法的? - -由于静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成员。 - -## 15. 在 Java 中定义一个不做事且没有参数的构造方法的作用 - -Java 程序在执行子类的构造方法之前,如果没有用 `super() `来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用 `super() `来调用父类中特定的构造方法,则编译时将发生错误,因为 Java 程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。 -  -## 16. import java和javax有什么区别? - -刚开始的时候 JavaAPI 所必需的包是 java 开头的包,javax 当时只是扩展 API 包来使用。然而随着时间的推移,javax 逐渐地扩展成为 Java API 的组成部分。但是,将扩展从 javax 包移动到 java 包确实太麻烦了,最终会破坏一堆现有的代码。因此,最终决定 javax 包将成为标准API的一部分。 - -所以,实际上java和javax没有区别。这都是一个名字。 - -## 17. 接口和抽象类的区别是什么? - -1. 接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),而抽象类可以有非抽象的方法。 -2. 接口中除了static、final变量,不能有其他变量,而抽象类中则不一定。 -3. 一个类可以实现多个接口,但只能实现一个抽象类。接口自己本身可以通过extends关键字扩展多个接口。 -4. 接口方法默认修饰符是public,抽象方法可以有public、protected和default这些修饰符(抽象方法就是为了被重写所以不能使用private关键字修饰!)。 -5. 从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。 - -备注:在JDK8中,接口也可以定义静态方法,可以直接用接口名调用。实现类和实现是不可以调用的。如果同时实现两个接口,接口中定义了一样的默认方法,则必须重写,不然会报错。(详见issue:[https://github.com/Snailclimb/JavaGuide/issues/146](https://github.com/Snailclimb/JavaGuide/issues/146)) - -## 18. 成员变量与局部变量的区别有哪些? - -1. 从语法形式上看:成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。 -2. 从变量在内存中的存储方式来看:如果成员变量是使用`static`修饰的,那么这个成员变量是属于类的,如果没有使用`static`修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。 -3. 从变量在内存中的生存时间上看:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。 -4. 成员变量如果没有被赋初值:则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。 - -## 19. 创建一个对象用什么运算符?对象实体与对象引用有何不同? - -new运算符,new创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。一个对象引用可以指向0个或1个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有n个引用指向它(可以用n条绳子系住一个气球)。 - -## 20. 什么是方法的返回值?返回值在类的方法里的作用是什么? - -方法的返回值是指我们获取到的某个方法体中的代码执行后产生的结果!(前提是该方法可能产生结果)。返回值的作用:接收出结果,使得它可以用于其他的操作! - -## 21. 一个类的构造方法的作用是什么? 若一个类没有声明构造方法,该程序能正确执行吗? 为什么? - -主要作用是完成对类对象的初始化工作。可以执行。因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。 - -## 22. 构造方法有哪些特性? - -1. 名字与类名相同。 -2. 没有返回值,但不能用void声明构造函数。 -3. 生成类的对象时自动执行,无需调用。 - -## 23. 静态方法和实例方法有何不同 - -1. 在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。 - -2. 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制。 - -## 24. 对象的相等与指向他们的引用相等,两者有什么不同? - -对象的相等,比的是内存中存放的内容是否相等。而引用相等,比较的是他们指向的内存地址是否相等。 - -## 25. 在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是? - -帮助子类做初始化工作。 - -## 26. == 与 equals(重要) - -**==** : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)。 - -**equals()** : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况: -- 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。 -- 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。 - - -**举个例子:** - -```java -public class test1 { - public static void main(String[] args) { - String a = new String("ab"); // a 为一个引用 - String b = new String("ab"); // b为另一个引用,对象的内容一样 - String aa = "ab"; // 放在常量池中 - String bb = "ab"; // 从常量池中查找 - if (aa == bb) // true - System.out.println("aa==bb"); - if (a == b) // false,非同一对象 - System.out.println("a==b"); - if (a.equals(b)) // true - System.out.println("aEQb"); - if (42 == 42.0) { // true - System.out.println("true"); - } - } -} -``` - -**说明:** - -- String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。 -- 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。 - -## 27. 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()` 的作用就是**获取哈希码**,也称为散列码;它实际上是返回一个int整数。这个**哈希码的作用**是确定该对象在哈希表中的索引位置。**`hashCode() `在散列表中才有用,在其它情况下没用**。在散列表中hashCode() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置。 - -### hashCode()与equals()的相关规定 - -1. 如果两个对象相等,则hashcode一定也是相同的 -2. 两个对象相等,对两个对象分别调用equals方法都返回true -3. 两个对象有相同的hashcode值,它们也不一定是相等的 -4. **因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖** -5. hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据) - -推荐阅读:[Java hashCode() 和 equals()的若干问题解答](https://www.cnblogs.com/skywang12345/p/3324958.html) - - -## 28. 为什么Java中只有值传递? - -[为什么Java中只有值传递?](https://juejin.im/post/5e18879e6fb9a02fc63602e2) - - -## 29. 简述线程、程序、进程的基本概念。以及他们之间关系是什么? - -**线程**与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。 - -**程序**是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。 - -**进程**是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 -线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。 - -## 30. 线程有哪些基本状态? - -Java 线程在运行的生命周期中的指定时刻只可能处于下面6种不同状态的其中一个状态(图源《Java 并发编程艺术》4.1.4节)。 - -![Java线程的状态](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/19-1-29/Java%E7%BA%BF%E7%A8%8B%E7%9A%84%E7%8A%B6%E6%80%81.png) - -线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示(图源《Java 并发编程艺术》4.1.4节): - -![Java线程状态变迁](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/19-1-29/Java%20%E7%BA%BF%E7%A8%8B%E7%8A%B6%E6%80%81%E5%8F%98%E8%BF%81.png) - - - -由上图可以看出: - -线程创建之后它将处于 **NEW(新建)** 状态,调用 `start()` 方法后开始运行,线程这时候处于 **READY(可运行)** 状态。可运行状态的线程获得了 cpu 时间片(timeslice)后就处于 **RUNNING(运行)** 状态。 - -> 操作系统隐藏 Java虚拟机(JVM)中的 READY 和 RUNNING 状态,它只能看到 RUNNABLE 状态(图源:[HowToDoInJava](https://howtodoinjava.com/):[Java Thread Life Cycle and Thread States](https://howtodoinjava.com/java/multi-threading/java-thread-life-cycle-and-thread-states/)),所以 Java 系统一般将这两个状态统称为 **RUNNABLE(运行中)** 状态 。 - -![RUNNABLE-VS-RUNNING](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3/RUNNABLE-VS-RUNNING.png) - -当线程执行 `wait()`方法之后,线程进入 **WAITING(等待)**状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 **TIME_WAITING(超时等待)** 状态相当于在等待状态的基础上增加了超时限制,比如通过 `sleep(long millis)`方法或 `wait(long millis)`方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 **BLOCKED(阻塞)** 状态。线程在执行 Runnable 的` run() `方法之后将会进入到 **TERMINATED(终止)** 状态。 - -## 31 关于 final 关键字的一些总结 - -final关键字主要用在三个地方:变量、方法、类。 - -1. 对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。 -2. 当用final修饰一个类时,表明这个类不能被继承。final类中的所有成员方法都会被隐式地指定为final方法。 -3. 使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的Java版本已经不需要使用final方法进行这些优化了)。类中所有的private方法都隐式地指定为final。 - -## 32 Java 中的异常处理 - -### Java异常类层次结构图 - -![Java异常类层次结构图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-2/Exception.png) - - - -在 Java 中,所有的异常都有一个共同的祖先java.lang包中的 **Throwable类**。Throwable: 有两个重要的子类:**Exception(异常)** 和 **Error(错误)** ,二者都是 Java 异常处理的重要子类,各自都包含大量子类。 - -**Error(错误):是程序无法处理的错误**,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。 - -这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java中,错误通过Error的子类描述。 - -**Exception(异常):是程序本身可以处理的异常**。Exception 类有一个重要的子类 **RuntimeException**。RuntimeException 异常由Java虚拟机抛出。**NullPointerException**(要访问的变量没有引用任何对象时,抛出该异常)、**ArithmeticException**(算术运算异常,一个整数除以0时,抛出该异常)和 **ArrayIndexOutOfBoundsException** (下标越界异常)。 - -**注意:异常和错误的区别:异常能被程序本身处理,错误是无法处理。** - -### Throwable类常用方法 - -- **public string getMessage()**:返回异常发生时的简要描述 -- **public string toString()**:返回异常发生时的详细信息 -- **public string getLocalizedMessage()**:返回异常对象的本地化信息。使用Throwable的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()返回的结果相同 -- **public void printStackTrace()**:在控制台上打印Throwable对象封装的异常信息 - -### 异常处理总结 - -- **try 块:** 用于捕获异常。其后可接零个或多个catch块,如果没有catch块,则必须跟一个finally块。 -- **catch 块:** 用于处理try捕获到的异常。 -- **finally 块:** 无论是否捕获或处理异常,finally块里的语句都会被执行。当在try块或catch块中遇到return -语句时,finally语句块将在方法返回之前被执行。 - -**在以下4种特殊情况下,finally块不会被执行:** - -1. 在finally语句块第一行发生了异常。 因为在其他行,finally块还是会得到执行 -2. 在前面的代码中用了System.exit(int)已退出程序。 exit是带参函数 ;若该语句在异常语句之后,finally会执行 -3. 程序所在的线程死亡。 -4. 关闭CPU。 - -下面这部分内容来自issue:。 - -**注意:** 当try语句和finally语句中都有return语句时,在方法返回之前,finally语句的内容将被执行,并且finally语句的返回值将会覆盖原始的返回值。如下: - -```java - public static int f(int value) { - try { - return value * value; - } finally { - if (value == 2) { - return 0; - } - } - } -``` - -如果调用 `f(2)`,返回值将是0,因为finally语句的返回值覆盖了try语句块的返回值。 - -## 33 Java序列化中如果有些字段不想进行序列化,怎么办? - -对于不想进行序列化的变量,使用transient关键字修饰。 - -transient关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被transient修饰的变量值不会被持久化和恢复。transient只能修饰变量,不能修饰类和方法。 - -## 34 获取用键盘输入常用的两种方法 - -方法1:通过 Scanner - -```java -Scanner input = new Scanner(System.in); -String s = input.nextLine(); -input.close(); -``` - -方法2:通过 BufferedReader - -```java -BufferedReader input = new BufferedReader(new InputStreamReader(System.in)); -String s = input.readLine(); -``` - -## 35 Java 中 IO 流 - -### Java 中 IO 流分为几种? - - - 按照流的流向分,可以分为输入流和输出流; - - 按照操作单元划分,可以划分为字节流和字符流; - - 按照流的角色划分为节点流和处理流。 - -Java Io流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java I0流的40多个类都是从如下4个抽象类基类中派生出来的。 - - - InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。 - - OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。 - -按操作方式分类结构图: - -![IO-操作方式分类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/IO-操作方式分类.png) - - -按操作对象分类结构图: - -![IO-操作对象分类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/IO-操作对象分类.png) - -### 既然有了字节流,为什么还要有字符流? - -问题本质想问:**不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?** - -回答:字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。 - -### BIO,NIO,AIO 有什么区别? - -- **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,不过又放弃了。 - -## 36. 常见关键字总结:static,final,this,super - -详见笔主的这篇文章: - -## 37. Collections 工具类和 Arrays 工具类常见方法总结 - -详见笔主的这篇文章: - -### 38. 深拷贝 vs 浅拷贝 - -1. **浅拷贝**:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。 -2. **深拷贝**:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。 - -![deep and shallow copy](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/java-deep-and-shallow-copy.jpg) - -## 参考 - -- https://stackoverflow.com/questions/1906445/what-is-the-difference-between-jdk-and-jre -- https://www.educba.com/oracle-vs-openjdk/ -- https://stackoverflow.com/questions/22358071/differences-between-oracle-jdk-and-openjdk?answertab=active#tab-top - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) - diff --git "a/docs/java/Java\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/Java\347\226\221\351\232\276\347\202\271.md" "b/docs/java/Java\347\226\221\351\232\276\347\202\271.md" deleted file mode 100644 index 1a10e95..0000000 --- "a/docs/java/Java\347\226\221\351\232\276\347\202\271.md" +++ /dev/null @@ -1,373 +0,0 @@ - - -- [1. 基础](#1-基础) - - [1.1. 正确使用 equals 方法](#11-正确使用-equals-方法) - - [1.2. 整型包装类值的比较](#12-整型包装类值的比较) - - [1.3. BigDecimal](#13-bigdecimal) - - [1.3.1. BigDecimal 的用处](#131-bigdecimal-的用处) - - [1.3.2. BigDecimal 的大小比较](#132-bigdecimal-的大小比较) - - [1.3.3. BigDecimal 保留几位小数](#133-bigdecimal-保留几位小数) - - [1.3.4. BigDecimal 的使用注意事项](#134-bigdecimal-的使用注意事项) - - [1.3.5. 总结](#135-总结) - - [1.4. 基本数据类型与包装数据类型的使用标准](#14-基本数据类型与包装数据类型的使用标准) -- [2. 集合](#2-集合) - - [2.1. Arrays.asList()使用指南](#21-arraysaslist使用指南) - - [2.1.1. 简介](#211-简介) - - [2.1.2. 《阿里巴巴Java 开发手册》对其的描述](#212-阿里巴巴java-开发手册对其的描述) - - [2.1.3. 使用时的注意事项总结](#213-使用时的注意事项总结) - - [2.1.4. 如何正确的将数组转换为ArrayList?](#214-如何正确的将数组转换为arraylist) - - [2.2. Collection.toArray()方法使用的坑&如何反转数组](#22-collectiontoarray方法使用的坑如何反转数组) - - [2.3. 不要在 foreach 循环里进行元素的 remove/add 操作](#23-不要在-foreach-循环里进行元素的-removeadd-操作) - - - -# 1. 基础 - -## 1.1. 正确使用 equals 方法 - -Object的equals方法容易抛空指针异常,应使用常量或确定有值的对象来调用 equals。 - -举个例子: - -```java -// 不能使用一个值为null的引用类型变量来调用非静态方法,否则会抛出异常 -String str = null; -if (str.equals("SnailClimb")) { - ... -} else { - .. -} -``` - -运行上面的程序会抛出空指针异常,但是我们把第二行的条件判断语句改为下面这样的话,就不会抛出空指针异常,else 语句块得到执行。: - -```java -"SnailClimb".equals(str);// false -``` -不过更推荐使用 `java.util.Objects#equals`(JDK7 引入的工具类)。 - -```java -Objects.equals(null,"SnailClimb");// false -``` -我们看一下`java.util.Objects#equals`的源码就知道原因了。 -```java -public static boolean equals(Object a, Object b) { - // 可以避免空指针异常。如果a==null的话此时a.equals(b)就不会得到执行,避免出现空指针异常。 - return (a == b) || (a != null && a.equals(b)); - } -``` - -**注意:** - -Reference:[Java中equals方法造成空指针异常的原因及解决方案](https://blog.csdn.net/tick_tock97/article/details/72824894) - -- 每种原始类型都有默认值一样,如int默认值为 0,boolean 的默认值为 false,null 是任何引用类型的默认值,不严格的说是所有 Object 类型的默认值。 -- 可以使用 == 或者 != 操作来比较null值,但是不能使用其他算法或者逻辑操作。在Java中`null == null`将返回true。 -- 不能使用一个值为null的引用类型变量来调用非静态方法,否则会抛出异常 - -## 1.2. 整型包装类值的比较 - -所有整型包装类对象值的比较必须使用equals方法。 - -先看下面这个例子: - -```java -Integer x = 3; -Integer y = 3; -System.out.println(x == y);// true -Integer a = new Integer(3); -Integer b = new Integer(3); -System.out.println(a == b);//false -System.out.println(a.equals(b));//true -``` - -当使用自动装箱方式创建一个Integer对象时,当数值在-128 ~127时,会将创建的 Integer 对象缓存起来,当下次再出现该数值时,直接从缓存中取出对应的Integer对象。所以上述代码中,x和y引用的是相同的Integer对象。 - -**注意:**如果你的IDE(IDEA/Eclipse)上安装了阿里巴巴的p3c插件,这个插件如果检测到你用 ==的话会报错提示,推荐安装一个这个插件,很不错。 - -## 1.3. BigDecimal - -### 1.3.1. BigDecimal 的用处 - -《阿里巴巴Java开发手册》中提到:**浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用 equals 来判断。** 具体原理和浮点数的编码方式有关,这里就不多提了,我们下面直接上实例: - -```java -float a = 1.0f - 0.9f; -float b = 0.9f - 0.8f; -System.out.println(a);// 0.100000024 -System.out.println(b);// 0.099999964 -System.out.println(a == b);// false -``` -具有基本数学知识的我们很清楚的知道输出并不是我们想要的结果(**精度丢失**),我们如何解决这个问题呢?一种很常用的方法是:**使用使用 BigDecimal 来定义浮点数的值,再进行浮点数的运算操作。** - -```java -BigDecimal a = new BigDecimal("1.0"); -BigDecimal b = new BigDecimal("0.9"); -BigDecimal c = new BigDecimal("0.8"); -BigDecimal x = a.subtract(b);// 0.1 -BigDecimal y = b.subtract(c);// 0.1 -System.out.println(x.equals(y));// true -``` - -### 1.3.2. BigDecimal 的大小比较 - -`a.compareTo(b)` : 返回 -1 表示小于,0 表示 等于, 1表示 大于。 - -```java -BigDecimal a = new BigDecimal("1.0"); -BigDecimal b = new BigDecimal("0.9"); -System.out.println(a.compareTo(b));// 1 -``` -### 1.3.3. BigDecimal 保留几位小数 - -通过 `setScale`方法设置保留几位小数以及保留规则。保留规则有挺多种,不需要记,IDEA会提示。 - -```java -BigDecimal m = new BigDecimal("1.255433"); -BigDecimal n = m.setScale(3,BigDecimal.ROUND_HALF_DOWN); -System.out.println(n);// 1.255 -``` - -### 1.3.4. BigDecimal 的使用注意事项 - -注意:我们在使用BigDecimal时,为了防止精度丢失,推荐使用它的 **BigDecimal(String)** 构造方法来创建对象。《阿里巴巴Java开发手册》对这部分内容也有提到如下图所示。 - -![《阿里巴巴Java开发手册》对这部分BigDecimal的描述](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019/7/BigDecimal.png) - -### 1.3.5. 总结 - -BigDecimal 主要用来操作(大)浮点数,BigInteger 主要用来操作大整数(超过 long 类型)。 - -BigDecimal 的实现利用到了 BigInteger, 所不同的是 BigDecimal 加入了小数位的概念 - -## 1.4. 基本数据类型与包装数据类型的使用标准 - -Reference:《阿里巴巴Java开发手册》 - -- 【强制】所有的 POJO 类属性必须使用包装数据类型。 -- 【强制】RPC 方法的返回值和参数必须使用包装数据类型。 -- 【推荐】所有的局部变量使用基本数据类型。 - -比如我们如果自定义了一个Student类,其中有一个属性是成绩score,如果用Integer而不用int定义,一次考试,学生可能没考,值是null,也可能考了,但考了0分,值是0,这两个表达的状态明显不一样. - -**说明** :POJO 类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何 NPE 问题,或者入库检查,都由使用者来保证。 - -**正例** : 数据库的查询结果可能是 null,因为自动拆箱,用基本数据类型接收有 NPE 风险。 - -**反例** : 比如显示成交总额涨跌情况,即正负 x%,x 为基本数据类型,调用的 RPC 服务,调用不成功时,返回的是默认值,页面显示为 0%,这是不合理的,应该显示成中划线。所以包装数据类型的 null 值,能够表示额外的信息,如:远程调用失败,异常退出。 - -# 2. 集合 - -## 2.1. Arrays.asList()使用指南 - -最近使用`Arrays.asList()`遇到了一些坑,然后在网上看到这篇文章:[Java Array to List Examples](http://javadevnotes.com/java-array-to-list-examples) 感觉挺不错的,但是还不是特别全面。所以,自己对于这块小知识点进行了简单的总结。 - -### 2.1.1. 简介 - -`Arrays.asList()`在平时开发中还是比较常见的,我们可以使用它将一个数组转换为一个List集合。 - -```java -String[] myArray = { "Apple", "Banana", "Orange" }; -List myList = Arrays.asList(myArray); -//上面两个语句等价于下面一条语句 -List myList = Arrays.asList("Apple","Banana", "Orange"); -``` - -JDK 源码对于这个方法的说明: - -```java -/** - *返回由指定数组支持的固定大小的列表。此方法作为基于数组和基于集合的API之间的桥梁,与 Collection.toArray()结合使用。返回的List是可序列化并实现RandomAccess接口。 - */ -public static List asList(T... a) { - return new ArrayList<>(a); -} -``` - -### 2.1.2. 《阿里巴巴Java 开发手册》对其的描述 - -`Arrays.asList()`将数组转换为集合后,底层其实还是数组,《阿里巴巴Java 开发手册》对于这个方法有如下描述: - -![阿里巴巴Java开发手-Arrays.asList()方法](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/阿里巴巴Java开发手-Arrays.asList()方法.png) - -### 2.1.3. 使用时的注意事项总结 - -**传递的数组必须是对象数组,而不是基本类型。** - -`Arrays.asList()`是泛型方法,传入的对象必须是对象数组。 - -```java -int[] myArray = { 1, 2, 3 }; -List myList = Arrays.asList(myArray); -System.out.println(myList.size());//1 -System.out.println(myList.get(0));//数组地址值 -System.out.println(myList.get(1));//报错:ArrayIndexOutOfBoundsException -int [] array=(int[]) myList.get(0); -System.out.println(array[0]);//1 -``` -当传入一个原生数据类型数组时,`Arrays.asList()` 的真正得到的参数就不是数组中的元素,而是数组对象本身!此时List 的唯一元素就是这个数组,这也就解释了上面的代码。 - -我们使用包装类型数组就可以解决这个问题。 - -```java -Integer[] myArray = { 1, 2, 3 }; -``` - -**使用集合的修改方法:`add()`、`remove()`、`clear()`会抛出异常。** - -```java -List myList = Arrays.asList(1, 2, 3); -myList.add(4);//运行时报错:UnsupportedOperationException -myList.remove(1);//运行时报错:UnsupportedOperationException -myList.clear();//运行时报错:UnsupportedOperationException -``` - -`Arrays.asList()` 方法返回的并不是 `java.util.ArrayList` ,而是 `java.util.Arrays` 的一个内部类,这个内部类并没有实现集合的修改方法或者说并没有重写这些方法。 - -```java -List myList = Arrays.asList(1, 2, 3); -System.out.println(myList.getClass());//class java.util.Arrays$ArrayList -``` - -下图是`java.util.Arrays$ArrayList`的简易源码,我们可以看到这个类重写的方法有哪些。 - -```java - private static class ArrayList extends AbstractList - implements RandomAccess, java.io.Serializable - { - ... - - @Override - public E get(int index) { - ... - } - - @Override - public E set(int index, E element) { - ... - } - - @Override - public int indexOf(Object o) { - ... - } - - @Override - public boolean contains(Object o) { - ... - } - - @Override - public void forEach(Consumer action) { - ... - } - - @Override - public void replaceAll(UnaryOperator operator) { - ... - } - - @Override - public void sort(Comparator c) { - ... - } - } -``` - -我们再看一下`java.util.AbstractList`的`remove()`方法,这样我们就明白为啥会抛出`UnsupportedOperationException`。 - -```java -public E remove(int index) { - throw new UnsupportedOperationException(); -} -``` - -### 2.1.4. 如何正确的将数组转换为ArrayList? - -stackoverflow:https://dwz.cn/vcBkTiTW - -**1. 自己动手实现(教育目的)** - -```java -//JDK1.5+ -static List arrayToList(final T[] array) { - final List l = new ArrayList(array.length); - - for (final T s : array) { - l.add(s); - } - return (l); -} -``` - -```java -Integer [] myArray = { 1, 2, 3 }; -System.out.println(arrayToList(myArray).getClass());//class java.util.ArrayList -``` - -**2. 最简便的方法(推荐)** - -```java -List list = new ArrayList<>(Arrays.asList("a", "b", "c")) -``` - -**3. 使用 Java8 的Stream(推荐)** - -```java -Integer [] myArray = { 1, 2, 3 }; -List myList = Arrays.stream(myArray).collect(Collectors.toList()); -//基本类型也可以实现转换(依赖boxed的装箱操作) -int [] myArray2 = { 1, 2, 3 }; -List myList = Arrays.stream(myArray2).boxed().collect(Collectors.toList()); -``` - -**4. 使用 Guava(推荐)** - -对于不可变集合,你可以使用[`ImmutableList`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/ImmutableList.java)类及其[`of()`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/ImmutableList.java#L101)与[`copyOf()`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/ImmutableList.java#L225)工厂方法:(参数不能为空) - -```java -List il = ImmutableList.of("string", "elements"); // from varargs -List il = ImmutableList.copyOf(aStringArray); // from array -``` -对于可变集合,你可以使用[`Lists`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/Lists.java)类及其[`newArrayList()`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/Lists.java#L87)工厂方法: - -```java -List l1 = Lists.newArrayList(anotherListOrCollection); // from collection -List l2 = Lists.newArrayList(aStringArray); // from array -List l3 = Lists.newArrayList("or", "string", "elements"); // from varargs -``` - -**5. 使用 Apache Commons Collections** - -```java -List list = new ArrayList(); -CollectionUtils.addAll(list, str); -``` - -## 2.2. Collection.toArray()方法使用的坑&如何反转数组 - -该方法是一个泛型方法:` T[] toArray(T[] a);` 如果`toArray`方法中没有传递任何参数的话返回的是`Object`类型数组。 - -```java -String [] s= new String[]{ - "dog", "lazy", "a", "over", "jumps", "fox", "brown", "quick", "A" -}; -List list = Arrays.asList(s); -Collections.reverse(list); -s=list.toArray(new String[0]);//没有指定类型的话会报错 -``` - -由于JVM优化,`new String[0]`作为`Collection.toArray()`方法的参数现在使用更好,`new String[0]`就是起一个模板的作用,指定了返回数组的类型,0是为了节省空间,因为它只是为了说明返回的类型。详见: - -## 2.3. 不要在 foreach 循环里进行元素的 remove/add 操作 - -如果要进行`remove`操作,可以调用迭代器的 `remove `方法而不是集合类的 remove 方法。因为如果列表在任何时间从结构上修改创建迭代器之后,以任何方式除非通过迭代器自身`remove/add`方法,迭代器都将抛出一个`ConcurrentModificationException`,这就是单线程状态下产生的 **fail-fast 机制**。 - -> **fail-fast 机制** :多个线程对 fail-fast 集合进行修改的时,可能会抛出ConcurrentModificationException,单线程下也会出现这种情况,上面已经提到过。 - -`java.util`包下面的所有的集合类都是fail-fast的,而`java.util.concurrent`包下面的所有的类都是fail-safe的。 - -![不要在 foreach 循环里进行元素的 remove/add 操作](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019/7/foreach-remove:add.png) - - - diff --git "a/docs/java/Java\347\250\213\345\272\217\350\256\276\350\256\241\351\242\230.md" "b/docs/java/Java\347\250\213\345\272\217\350\256\276\350\256\241\351\242\230.md" deleted file mode 100644 index 46c9c16..0000000 --- "a/docs/java/Java\347\250\213\345\272\217\350\256\276\350\256\241\351\242\230.md" +++ /dev/null @@ -1,125 +0,0 @@ -## 泛型的实际应用 - -### 实现最小值函数 - -自己设计一个泛型的获取数组最小值的函数.并且这个方法只能接受Number的子类并且实现了Comparable接口。 - -```java -//注意:Number并没有实现Comparable -private static > T min(T[] values) { - if (values == null || values.length == 0) return null; - T min = values[0]; - for (int i = 1; i < values.length; i++) { - if (min.compareTo(values[i]) > 0) min = values[i]; - } - return min; -} -``` - -测试: - -```java -int minInteger = min(new Integer[]{1, 2, 3});//result:1 -double minDouble = min(new Double[]{1.2, 2.2, -1d});//result:-1d -String typeError = min(new String[]{"1","3"});//报错 -``` - -## 数据结构 - -### 使用数组实现栈 - -**自己实现一个栈,要求这个栈具有`push()`、`pop()`(返回栈顶元素并出栈)、`peek()` (返回栈顶元素不出栈)、`isEmpty()`、`size()`这些基本的方法。** - -提示:每次入栈之前先判断栈的容量是否够用,如果不够用就用`Arrays.copyOf()`进行扩容; - -```java -public class MyStack { - private int[] storage;//存放栈中元素的数组 - private int capacity;//栈的容量 - private int count;//栈中元素数量 - private static final int GROW_FACTOR = 2; - - //TODO:不带初始容量的构造方法。默认容量为8 - public MyStack() { - this.capacity = 8; - this.storage=new int[8]; - this.count = 0; - } - - //TODO:带初始容量的构造方法 - public MyStack(int initialCapacity) { - if (initialCapacity < 1) - throw new IllegalArgumentException("Capacity too small."); - - this.capacity = initialCapacity; - this.storage = new int[initialCapacity]; - this.count = 0; - } - - //TODO:入栈 - public void push(int value) { - if (count == capacity) { - ensureCapacity(); - } - storage[count++] = value; - } - - //TODO:确保容量大小 - private void ensureCapacity() { - int newCapacity = capacity * GROW_FACTOR; - storage = Arrays.copyOf(storage, newCapacity); - capacity = newCapacity; - } - - //TODO:返回栈顶元素并出栈 - private int pop() { - count--; - if (count == -1) - throw new IllegalArgumentException("Stack is empty."); - - return storage[count]; - } - - //TODO:返回栈顶元素不出栈 - private int peek() { - if (count == 0){ - throw new IllegalArgumentException("Stack is empty."); - }else { - return storage[count-1]; - } - } - - //TODO:判断栈是否为空 - private boolean isEmpty() { - return count == 0; - } - - //TODO:返回栈中元素的个数 - private int size() { - return count; - } - -} - -``` - -验证 - -```java -MyStack myStack = new MyStack(3); -myStack.push(1); -myStack.push(2); -myStack.push(3); -myStack.push(4); -myStack.push(5); -myStack.push(6); -myStack.push(7); -myStack.push(8); -System.out.println(myStack.peek());//8 -System.out.println(myStack.size());//8 -for (int i = 0; i < 8; i++) { - System.out.println(myStack.pop()); -} -System.out.println(myStack.isEmpty());//true -myStack.pop();//报错:java.lang.IllegalArgumentException: Stack is empty. -``` \ No newline at end of file diff --git "a/docs/java/Java\347\274\226\347\250\213\350\247\204\350\214\203.md" "b/docs/java/Java\347\274\226\347\250\213\350\247\204\350\214\203.md" deleted file mode 100644 index 6b4731e..0000000 --- "a/docs/java/Java\347\274\226\347\250\213\350\247\204\350\214\203.md" +++ /dev/null @@ -1,30 +0,0 @@ -讲真的,下面推荐的文章或者资源建议阅读 3 遍以上。 - -### 团队 - -- **阿里巴巴Java开发手册(详尽版)** -- **Google Java编程风格指南:** - -### 个人 - -- **程序员你为什么这么累:** - -### 如何写出优雅的 Java 代码 - -1. 使用 IntelliJ IDEA 作为您的集成开发环境 (IDE) -1. 使用 JDK 8 或更高版本 -1. 使用 Maven/Gradle -1. 使用 Lombok -1. 编写单元测试 -1. 重构:常见,但也很慢 -1. 注意代码规范 -1. 定期联络客户,以获取他们的反馈 - -上述建议的详细内容:[八点建议助您写出优雅的Java代码](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485140&idx=1&sn=ecaeace613474f1859aaeed0282ae680&chksm=cea2491ff9d5c00982ffaece847ce1aead89fdb3fe190752d9837c075c79fc95db5940992c56&token=1328169465&lang=zh_CN&scene=21#wechat_redirect)。 - -更多代码优化相关内容推荐: - -- [业务复杂=if else?刚来的大神竟然用策略+工厂彻底干掉了他们!](https://juejin.im/post/5dad23685188251d2c4ea2b6) -- [一些不错的 Java 实践!推荐阅读3遍以上!](http://lrwinx.github.io/2017/03/04/%E7%BB%86%E6%80%9D%E6%9E%81%E6%81%90-%E4%BD%A0%E7%9C%9F%E7%9A%84%E4%BC%9A%E5%86%99java%E5%90%97/) -- [[解锁新姿势] 兄dei,你代码需要优化了](https://juejin.im/post/5dafbc02e51d4524a0060bdd) -- [消灭 Java 代码的“坏味道”](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485599&idx=1&sn=d83ff4e6b1ee951a0a33508a10980ea3&chksm=cea24754f9d5ce426d18b435a8c373ddc580c06c7d6a45cc51377361729c31c7301f1bbc3b78&token=1328169465&lang=zh_CN#rd) \ No newline at end of file diff --git a/docs/java/Multithread/AQS.md b/docs/java/Multithread/AQS.md deleted file mode 100644 index ab39908..0000000 --- a/docs/java/Multithread/AQS.md +++ /dev/null @@ -1,713 +0,0 @@ -点击关注[公众号](#公众号 "公众号")及时获取笔主最新更新文章,并可免费领取本文档配套的《Java 面试突击》以及 Java 工程师必备学习资源。 - - - -- [1 AQS 简单介绍](#1-aqs-简单介绍) -- [2 AQS 原理](#2-aqs-原理) - - [2.1 AQS 原理概览](#21-aqs-原理概览) - - [2.2 AQS 对资源的共享方式](#22-aqs-对资源的共享方式) - - [2.3 AQS 底层使用了模板方法模式](#23-aqs-底层使用了模板方法模式) -- [3 Semaphore(信号量)-允许多个线程同时访问](#3-semaphore信号量-允许多个线程同时访问) -- [4 CountDownLatch (倒计时器)](#4-countdownlatch-倒计时器) - - [4.1 CountDownLatch 的三种典型用法](#41-countdownlatch-的三种典型用法) - - [4.2 CountDownLatch 的使用示例](#42-countdownlatch-的使用示例) - - [4.3 CountDownLatch 的不足](#43-countdownlatch-的不足) - - [4.4 CountDownLatch 常见面试题](#44-countdownlatch-相常见面试题) -- [5 CyclicBarrier(循环栅栏)](#5-cyclicbarrier循环栅栏) - - [5.1 CyclicBarrier 的应用场景](#51-cyclicbarrier-的应用场景) - - [5.2 CyclicBarrier 的使用示例](#52-cyclicbarrier-的使用示例) - - [5.3 `CyclicBarrier`源码分析](#53-cyclicbarrier源码分析) - - [5.4 CyclicBarrier 和 CountDownLatch 的区别](#54-cyclicbarrier-和-countdownlatch-的区别) -- [6 ReentrantLock 和 ReentrantReadWriteLock](#6-reentrantlock-和-reentrantreadwritelock) -- [参考](#参考) -- [公众号](#公众号) - - - -> 常见问题:AQS 原理?;CountDownLatch 和 CyclicBarrier 了解吗,两者的区别是什么?用过 Semaphore 吗? - -### 1 AQS 简单介绍 - -AQS 的全称为(AbstractQueuedSynchronizer),这个类在 java.util.concurrent.locks 包下面。 - -![enter image description here](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/Java%20%E7%A8%8B%E5%BA%8F%E5%91%98%E5%BF%85%E5%A4%87%EF%BC%9A%E5%B9%B6%E5%8F%91%E7%9F%A5%E8%AF%86%E7%B3%BB%E7%BB%9F%E6%80%BB%E7%BB%93/AQS.png) - -AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask(jdk1.7) 等等皆是基于 AQS 的。当然,我们自己也能利用 AQS 非常轻松容易地构造出符合我们自己需求的同步器。 - -### 2 AQS 原理 - -> 在面试中被问到并发知识的时候,大多都会被问到“请你说一下自己对于 AQS 原理的理解”。下面给大家一个示例供大家参考,面试不是背题,大家一定要加入自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来。 - -下面大部分内容其实在 AQS 类注释上已经给出了,不过是英语看着比较吃力一点,感兴趣的话可以看看源码。 - -#### 2.1 AQS 原理概览 - -**AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。** - -> CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。 - -看个 AQS(AbstractQueuedSynchronizer)原理图: - -![enter image description here](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/Java%20%E7%A8%8B%E5%BA%8F%E5%91%98%E5%BF%85%E5%A4%87%EF%BC%9A%E5%B9%B6%E5%8F%91%E7%9F%A5%E8%AF%86%E7%B3%BB%E7%BB%9F%E6%80%BB%E7%BB%93/CLH.png) - -AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。 - -```java -private volatile int state;//共享变量,使用volatile修饰保证线程可见性 -``` - -状态信息通过 protected 类型的`getState`,`setState`,`compareAndSetState`进行操作 - -```java -//返回同步状态的当前值 -protected final int getState() { - return state; -} - // 设置同步状态的值 -protected final void setState(int newState) { - state = newState; -} -//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值) -protected final boolean compareAndSetState(int expect, int update) { - return unsafe.compareAndSwapInt(this, stateOffset, expect, update); -} -``` - -#### 2.2 AQS 对资源的共享方式 - -**AQS 定义两种资源共享方式** - -**1)Exclusive**(独占) - -只有一个线程能执行,如 ReentrantLock。又可分为公平锁和非公平锁,ReentrantLock 同时支持两种锁,下面以 ReentrantLock 对这两种锁的定义做介绍: - -- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁 -- 非公平锁:当线程要获取锁时,先通过两次 CAS 操作去抢锁,如果没抢到,当前线程再加入到队列中等待唤醒。 - -> 说明:下面这部分关于 `ReentrantLock` 源代码内容节选自:https://www.javadoop.com/post/AbstractQueuedSynchronizer-2,这是一篇很不错文章,推荐阅读。 - -**下面来看 ReentrantLock 中相关的源代码:** - -ReentrantLock 默认采用非公平锁,因为考虑获得更好的性能,通过 boolean 来决定是否用公平锁(传入 true 用公平锁)。 - -```java -/** Synchronizer providing all implementation mechanics */ -private final Sync sync; -public ReentrantLock() { - // 默认非公平锁 - sync = new NonfairSync(); -} -public ReentrantLock(boolean fair) { - sync = fair ? new FairSync() : new NonfairSync(); -} -``` - -ReentrantLock 中公平锁的 `lock` 方法 - -```java -static final class FairSync extends Sync { - final void lock() { - acquire(1); - } - // AbstractQueuedSynchronizer.acquire(int arg) - public final void acquire(int arg) { - if (!tryAcquire(arg) && - acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) - selfInterrupt(); - } - protected final boolean tryAcquire(int acquires) { - final Thread current = Thread.currentThread(); - int c = getState(); - if (c == 0) { - // 1. 和非公平锁相比,这里多了一个判断:是否有线程在等待 - if (!hasQueuedPredecessors() && - compareAndSetState(0, acquires)) { - setExclusiveOwnerThread(current); - return true; - } - } - else if (current == getExclusiveOwnerThread()) { - int nextc = c + acquires; - if (nextc < 0) - throw new Error("Maximum lock count exceeded"); - setState(nextc); - return true; - } - return false; - } -} -``` - -非公平锁的 lock 方法: - -```java -static final class NonfairSync extends Sync { - final void lock() { - // 2. 和公平锁相比,这里会直接先进行一次CAS,成功就返回了 - if (compareAndSetState(0, 1)) - setExclusiveOwnerThread(Thread.currentThread()); - else - acquire(1); - } - // AbstractQueuedSynchronizer.acquire(int arg) - public final void acquire(int arg) { - if (!tryAcquire(arg) && - acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) - selfInterrupt(); - } - protected final boolean tryAcquire(int acquires) { - return nonfairTryAcquire(acquires); - } -} -/** - * Performs non-fair tryLock. tryAcquire is implemented in - * subclasses, but both need nonfair try for trylock method. - */ -final boolean nonfairTryAcquire(int acquires) { - final Thread current = Thread.currentThread(); - int c = getState(); - if (c == 0) { - // 这里没有对阻塞队列进行判断 - if (compareAndSetState(0, acquires)) { - setExclusiveOwnerThread(current); - return true; - } - } - else if (current == getExclusiveOwnerThread()) { - int nextc = c + acquires; - if (nextc < 0) // overflow - throw new Error("Maximum lock count exceeded"); - setState(nextc); - return true; - } - return false; -} -``` - -总结:公平锁和非公平锁只有两处不同: - -1. 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。 -2. 非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。 - -公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。 - -相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。 - -**2)Share**(共享) - -多个线程可同时执行,如 Semaphore/CountDownLatch。Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。 - -ReentrantReadWriteLock 可以看成是组合式,因为 ReentrantReadWriteLock 也就是读写锁允许多个线程同时对某一资源进行读。 - -不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS 已经在上层已经帮我们实现好了。 - -#### 2.3 AQS 底层使用了模板方法模式 - -同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用): - -1. 使用者继承 AbstractQueuedSynchronizer 并重写指定的方法。(这些重写方法很简单,无非是对于共享资源 state 的获取和释放) -2. 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。 - -这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用,下面简单的给大家介绍一下模板方法模式,模板方法模式是一个很容易理解的设计模式之一。 - -> 模板方法模式是基于”继承“的,主要是为了在不改变模板结构的前提下在子类中重新定义模板中的内容以实现复用代码。举个很简单的例子假如我们要去一个地方的步骤是:购票`buyTicket()`->安检`securityCheck()`->乘坐某某工具回家`ride()`->到达目的地`arrive()`。我们可能乘坐不同的交通工具回家比如飞机或者火车,所以除了`ride()`方法,其他方法的实现几乎相同。我们可以定义一个包含了这些方法的抽象类,然后用户根据自己的需要继承该抽象类然后修改 `ride()`方法。 - -**AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的模板方法:** - -```java -isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。 -tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。 -tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。 -tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 -tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。 - -``` - -默认情况下,每个方法都抛出 `UnsupportedOperationException`。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS 类中的其他方法都是 final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。 - -以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程 lock()时,会调用 tryAcquire()独占该锁并将 state+1。此后,其他线程再 tryAcquire()时就会失败,直到 A 线程 unlock()到 state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。 - -再以 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown()一次,state 会 CAS(Compare and Swap)减 1。等到所有子线程都执行完后(即 state=0),会 unpark()主调用线程,然后主调用线程就会从 await()函数返回,继续后余动作。 - -一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现`tryAcquire-tryRelease`、`tryAcquireShared-tryReleaseShared`中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如`ReentrantReadWriteLock`。 - -推荐两篇 AQS 原理和相关源码分析的文章: - -- http://www.cnblogs.com/waterystone/p/4920797.html -- https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html - -### 3 Semaphore(信号量)-允许多个线程同时访问 - -**synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。** 示例代码如下: - -```java -/** - * - * @author Snailclimb - * @date 2018年9月30日 - * @Description: 需要一次性拿一个许可的情况 - */ -public class SemaphoreExample1 { - // 请求的数量 - private static final int threadCount = 550; - - public static void main(String[] args) throws InterruptedException { - // 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢) - ExecutorService threadPool = Executors.newFixedThreadPool(300); - // 一次只能允许执行的线程数量。 - final Semaphore semaphore = new Semaphore(20); - - for (int i = 0; i < threadCount; i++) { - final int threadnum = i; - threadPool.execute(() -> {// Lambda 表达式的运用 - try { - semaphore.acquire();// 获取一个许可,所以可运行线程数量为20/1=20 - test(threadnum); - semaphore.release();// 释放一个许可 - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - }); - } - threadPool.shutdown(); - System.out.println("finish"); - } - - public static void test(int threadnum) throws InterruptedException { - Thread.sleep(1000);// 模拟请求的耗时操作 - System.out.println("threadnum:" + threadnum); - Thread.sleep(1000);// 模拟请求的耗时操作 - } -} -``` - -执行 `acquire` 方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个 `release` 方法增加一个许可证,这可能会释放一个阻塞的 acquire 方法。然而,其实并没有实际的许可证这个对象,Semaphore 只是维持了一个可获得许可证的数量。 Semaphore 经常用于限制获取某种资源的线程数量。 - -当然一次也可以一次拿取和释放多个许可,不过一般没有必要这样做: - -```java - semaphore.acquire(5);// 获取5个许可,所以可运行线程数量为20/5=4 - test(threadnum); - semaphore.release(5);// 获取5个许可,所以可运行线程数量为20/5=4 -``` - -除了 `acquire`方法之外,另一个比较常用的与之对应的方法是`tryAcquire`方法,该方法如果获取不到许可就立即返回 false。 - -Semaphore 有两种模式,公平模式和非公平模式。 - -- **公平模式:** 调用 acquire 的顺序就是获取许可证的顺序,遵循 FIFO; -- **非公平模式:** 抢占式的。 - -**Semaphore 对应的两个构造方法如下:** - -```java - public Semaphore(int permits) { - sync = new NonfairSync(permits); - } - - public Semaphore(int permits, boolean fair) { - sync = fair ? new FairSync(permits) : new NonfairSync(permits); - } -``` - -**这两个构造方法,都必须提供许可的数量,第二个构造方法可以指定是公平模式还是非公平模式,默认非公平模式。** - -由于篇幅问题,如果对 Semaphore 源码感兴趣的朋友可以看下面这篇文章: - -- https://blog.csdn.net/qq_19431333/article/details/70212663 - -### 4 CountDownLatch (倒计时器) - -CountDownLatch 是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行。在 Java 并发中,countdownlatch 的概念是一个常见的面试题,所以一定要确保你很好的理解了它。 - -#### 4.1 CountDownLatch 的三种典型用法 - -① 某一线程在开始运行前等待 n 个线程执行完毕。将 CountDownLatch 的计数器初始化为 n :`new CountDownLatch(n)`,每当一个任务线程执行完毕,就将计数器减 1 `countdownlatch.countDown()`,当计数器的值变为 0 时,在`CountDownLatch上 await()` 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。 - -② 实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的 `CountDownLatch` 对象,将其计数器初始化为 1 :`new CountDownLatch(1)`,多个线程在开始执行任务前首先 `coundownlatch.await()`,当主线程调用 countDown() 时,计数器变为 0,多个线程同时被唤醒。 - -③ 死锁检测:一个非常方便的使用场景是,你可以使用 n 个线程访问共享资源,在每次测试阶段的线程数目是不同的,并尝试产生死锁。 - -#### 4.2 CountDownLatch 的使用示例 - -```java -/** - * - * @author SnailClimb - * @date 2018年10月1日 - * @Description: CountDownLatch 使用方法示例 - */ -public class CountDownLatchExample1 { - // 请求的数量 - private static final int threadCount = 550; - - public static void main(String[] args) throws InterruptedException { - // 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢) - ExecutorService threadPool = Executors.newFixedThreadPool(300); - final CountDownLatch countDownLatch = new CountDownLatch(threadCount); - for (int i = 0; i < threadCount; i++) { - final int threadnum = i; - threadPool.execute(() -> {// Lambda 表达式的运用 - try { - test(threadnum); - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } finally { - countDownLatch.countDown();// 表示一个请求已经被完成 - } - - }); - } - countDownLatch.await(); - threadPool.shutdown(); - System.out.println("finish"); - } - - public static void test(int threadnum) throws InterruptedException { - Thread.sleep(1000);// 模拟请求的耗时操作 - System.out.println("threadnum:" + threadnum); - Thread.sleep(1000);// 模拟请求的耗时操作 - } -} - -``` - -上面的代码中,我们定义了请求的数量为 550,当这 550 个请求被处理完成之后,才会执行`System.out.println("finish");`。 - -与 CountDownLatch 的第一次交互是主线程等待其他线程。主线程必须在启动其他线程后立即调用 CountDownLatch.await()方法。这样主线程的操作就会在这个方法上阻塞,直到其他线程完成各自的任务。 - -其他 N 个线程必须引用闭锁对象,因为他们需要通知 CountDownLatch 对象,他们已经完成了各自的任务。这种通知机制是通过 CountDownLatch.countDown()方法来完成的;每调用一次这个方法,在构造函数中初始化的 count 值就减 1。所以当 N 个线程都调 用了这个方法,count 的值等于 0,然后主线程就能通过 await()方法,恢复执行自己的任务。 - -#### 4.3 CountDownLatch 的不足 - -CountDownLatch 是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 CountDownLatch 使用完毕后,它不能再次被使用。 - -#### 4.4 CountDownLatch 相常见面试题: - -解释一下 CountDownLatch 概念? - -CountDownLatch 和 CyclicBarrier 的不同之处? - -给出一些 CountDownLatch 使用的例子? - -CountDownLatch 类中主要的方法? - -### 5 CyclicBarrier(循环栅栏) - -CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。 - -CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier 默认的构造方法是 `CyclicBarrier(int parties)`,其参数表示屏障拦截的线程数量,每个线程调用`await`方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。 - -再来看一下它的构造函数: - -```java -public CyclicBarrier(int parties) { - this(parties, null); -} - -public CyclicBarrier(int parties, Runnable barrierAction) { - if (parties <= 0) throw new IllegalArgumentException(); - this.parties = parties; - this.count = parties; - this.barrierCommand = barrierAction; -} -``` - -其中,parties 就代表了有拦截的线程的数量,当拦截的线程数量达到这个值的时候就打开栅栏,让所有线程通过。 - -#### 5.1 CyclicBarrier 的应用场景 - -CyclicBarrier 可以用于多线程计算数据,最后合并计算结果的应用场景。比如我们用一个 Excel 保存了用户所有银行流水,每个 Sheet 保存一个帐户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个 sheet 里的银行流水,都执行完之后,得到每个 sheet 的日均银行流水,最后,再用 barrierAction 用这些线程的计算结果,计算出整个 Excel 的日均银行流水。 - -#### 5.2 CyclicBarrier 的使用示例 - -示例 1: - -```java -/** - * - * @author Snailclimb - * @date 2018年10月1日 - * @Description: 测试 CyclicBarrier 类中带参数的 await() 方法 - */ -public class CyclicBarrierExample2 { - // 请求的数量 - private static final int threadCount = 550; - // 需要同步的线程数量 - private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5); - - public static void main(String[] args) throws InterruptedException { - // 创建线程池 - ExecutorService threadPool = Executors.newFixedThreadPool(10); - - for (int i = 0; i < threadCount; i++) { - final int threadNum = i; - Thread.sleep(1000); - threadPool.execute(() -> { - try { - test(threadNum); - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (BrokenBarrierException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - }); - } - threadPool.shutdown(); - } - - public static void test(int threadnum) throws InterruptedException, BrokenBarrierException { - System.out.println("threadnum:" + threadnum + "is ready"); - try { - /**等待60秒,保证子线程完全执行结束*/ - cyclicBarrier.await(60, TimeUnit.SECONDS); - } catch (Exception e) { - System.out.println("-----CyclicBarrierException------"); - } - System.out.println("threadnum:" + threadnum + "is finish"); - } - -} -``` - -运行结果,如下: - -``` -threadnum:0is ready -threadnum:1is ready -threadnum:2is ready -threadnum:3is ready -threadnum:4is ready -threadnum:4is finish -threadnum:0is finish -threadnum:1is finish -threadnum:2is finish -threadnum:3is finish -threadnum:5is ready -threadnum:6is ready -threadnum:7is ready -threadnum:8is ready -threadnum:9is ready -threadnum:9is finish -threadnum:5is finish -threadnum:8is finish -threadnum:7is finish -threadnum:6is finish -...... -``` - -可以看到当线程数量也就是请求数量达到我们定义的 5 个的时候, `await`方法之后的方法才被执行。 - -另外,CyclicBarrier 还提供一个更高级的构造函数`CyclicBarrier(int parties, Runnable barrierAction)`,用于在线程到达屏障时,优先执行`barrierAction`,方便处理更复杂的业务场景。示例代码如下: - -```java -/** - * - * @author SnailClimb - * @date 2018年10月1日 - * @Description: 新建 CyclicBarrier 的时候指定一个 Runnable - */ -public class CyclicBarrierExample3 { - // 请求的数量 - private static final int threadCount = 550; - // 需要同步的线程数量 - private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> { - System.out.println("------当线程数达到之后,优先执行------"); - }); - - public static void main(String[] args) throws InterruptedException { - // 创建线程池 - ExecutorService threadPool = Executors.newFixedThreadPool(10); - - for (int i = 0; i < threadCount; i++) { - final int threadNum = i; - Thread.sleep(1000); - threadPool.execute(() -> { - try { - test(threadNum); - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (BrokenBarrierException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - }); - } - threadPool.shutdown(); - } - - public static void test(int threadnum) throws InterruptedException, BrokenBarrierException { - System.out.println("threadnum:" + threadnum + "is ready"); - cyclicBarrier.await(); - System.out.println("threadnum:" + threadnum + "is finish"); - } - -} -``` - -运行结果,如下: - -``` -threadnum:0is ready -threadnum:1is ready -threadnum:2is ready -threadnum:3is ready -threadnum:4is ready -------当线程数达到之后,优先执行------ -threadnum:4is finish -threadnum:0is finish -threadnum:2is finish -threadnum:1is finish -threadnum:3is finish -threadnum:5is ready -threadnum:6is ready -threadnum:7is ready -threadnum:8is ready -threadnum:9is ready -------当线程数达到之后,优先执行------ -threadnum:9is finish -threadnum:5is finish -threadnum:6is finish -threadnum:8is finish -threadnum:7is finish -...... -``` - -#### 5.3 `CyclicBarrier`源码分析 - -当调用 `CyclicBarrier` 对象调用 `await()` 方法时,实际上调用的是`dowait(false, 0L)`方法。 `await()` 方法就像树立起一个栅栏的行为一样,将线程挡住了,当拦住的线程数量达到 parties 的值时,栅栏才会打开,线程才得以通过执行。 - -```java - public int await() throws InterruptedException, BrokenBarrierException { - try { - return dowait(false, 0L); - } catch (TimeoutException toe) { - throw new Error(toe); // cannot happen - } - } -``` - -`dowait(false, 0L)`: - -```java - // 当线程数量或者请求数量达到 count 时 await 之后的方法才会被执行。上面的示例中 count 的值就为 5。 - private int count; - /** - * Main barrier code, covering the various policies. - */ - private int dowait(boolean timed, long nanos) - throws InterruptedException, BrokenBarrierException, - TimeoutException { - final ReentrantLock lock = this.lock; - // 锁住 - lock.lock(); - try { - final Generation g = generation; - - if (g.broken) - throw new BrokenBarrierException(); - - // 如果线程中断了,抛出异常 - if (Thread.interrupted()) { - breakBarrier(); - throw new InterruptedException(); - } - // cout减1 - int index = --count; - // 当 count 数量减为 0 之后说明最后一个线程已经到达栅栏了,也就是达到了可以执行await 方法之后的条件 - if (index == 0) { // tripped - boolean ranAction = false; - try { - final Runnable command = barrierCommand; - if (command != null) - command.run(); - ranAction = true; - // 将 count 重置为 parties 属性的初始化值 - // 唤醒之前等待的线程 - // 下一波执行开始 - nextGeneration(); - return 0; - } finally { - if (!ranAction) - breakBarrier(); - } - } - - // loop until tripped, broken, interrupted, or timed out - for (;;) { - try { - if (!timed) - trip.await(); - else if (nanos > 0L) - nanos = trip.awaitNanos(nanos); - } catch (InterruptedException ie) { - if (g == generation && ! g.broken) { - breakBarrier(); - throw ie; - } else { - // We're about to finish waiting even if we had not - // been interrupted, so this interrupt is deemed to - // "belong" to subsequent execution. - Thread.currentThread().interrupt(); - } - } - - if (g.broken) - throw new BrokenBarrierException(); - - if (g != generation) - return index; - - if (timed && nanos <= 0L) { - breakBarrier(); - throw new TimeoutException(); - } - } - } finally { - lock.unlock(); - } - } - -``` - -总结:`CyclicBarrier` 内部通过一个 count 变量作为计数器,cout 的初始值为 parties 属性的初始化值,每当一个线程到了栅栏这里了,那么就将计数器减一。如果 count 值为 0 了,表示这是这一代最后一个线程到达栅栏,就尝试执行我们构造方法中输入的任务。 - -#### 5.4 CyclicBarrier 和 CountDownLatch 的区别 - -**下面这个是国外一个大佬的回答:** - -CountDownLatch 是计数器,只能使用一次,而 CyclicBarrier 的计数器提供 reset 功能,可以多次使用。但是我不那么认为它们之间的区别仅仅就是这么简单的一点。我们来从 jdk 作者设计的目的来看,javadoc 是这么描述它们的: - -> CountDownLatch: A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.(CountDownLatch: 一个或者多个线程,等待其他多个线程完成某件事情之后才能执行;) -> CyclicBarrier : A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point.(CyclicBarrier : 多个线程互相等待,直到到达同一个同步点,再继续一起执行。) - -对于 CountDownLatch 来说,重点是“一个线程(多个线程)等待”,而其他的 N 个线程在完成“某件事情”之后,可以终止,也可以等待。而对于 CyclicBarrier,重点是多个线程,在任意一个线程没有完成,所有的线程都必须等待。 - -CountDownLatch 是计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而 CyclicBarrier 更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行。 - -### 6 ReentrantLock 和 ReentrantReadWriteLock - -ReentrantLock 和 synchronized 的区别在上面已经讲过了这里就不多做讲解。另外,需要注意的是:读写锁 ReentrantReadWriteLock 可以保证多个线程可以同时读,所以在读操作远大于写操作的时候,读写锁就非常有用了。 - -### 参考 - -- https://juejin.im/post/5ae755256fb9a07ac3634067 -- https://blog.csdn.net/u010185262/article/details/54692886 -- https://blog.csdn.net/tolcf/article/details/50925145?utm_source=blogxgwz0 - -### 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java 面试突击》:** 由本文档衍生的专为面试而生的《Java 面试突击》V2.0 PDF 版本[公众号](#公众号 "公众号")后台回复 **"面试突击"** 即可免费领取! - -**Java 工程师必备学习资源:** 一些 Java 工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) - diff --git a/docs/java/Multithread/Atomic.md b/docs/java/Multithread/Atomic.md deleted file mode 100644 index 627ea3d..0000000 --- a/docs/java/Multithread/Atomic.md +++ /dev/null @@ -1,556 +0,0 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 - -> 个人觉得这一节掌握基本的使用即可! - - - -- [1 Atomic 原子类介绍](#1-atomic-原子类介绍) -- [2 基本类型原子类](#2-基本类型原子类) - - [2.1 基本类型原子类介绍](#21-基本类型原子类介绍) - - [2.2 AtomicInteger 常见方法使用](#22-atomicinteger-常见方法使用) - - [2.3 基本数据类型原子类的优势](#23-基本数据类型原子类的优势) - - [2.4 AtomicInteger 线程安全原理简单分析](#24-atomicinteger-线程安全原理简单分析) -- [3 数组类型原子类](#3-数组类型原子类) - - [3.1 数组类型原子类介绍](#31-数组类型原子类介绍) - - [3.2 AtomicIntegerArray 常见方法使用](#32-atomicintegerarray-常见方法使用) -- [4 引用类型原子类](#4-引用类型原子类) - - [4.1 引用类型原子类介绍](#41--引用类型原子类介绍) - - [4.2 AtomicReference 类使用示例](#42-atomicreference-类使用示例) - - [4.3 AtomicStampedReference 类使用示例](#43-atomicstampedreference-类使用示例) - - [4.4 AtomicMarkableReference 类使用示例](#44-atomicmarkablereference-类使用示例) -- [5 对象的属性修改类型原子类](#5-对象的属性修改类型原子类) - - [5.1 对象的属性修改类型原子类介绍](#51-对象的属性修改类型原子类介绍) - - [5.2 AtomicIntegerFieldUpdater 类使用示例](#52-atomicintegerfieldupdater-类使用示例) - - - -### 1 Atomic 原子类介绍 - -Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。 - -所以,所谓原子类说简单点就是具有原子/原子操作特征的类。 - -并发包 `java.util.concurrent` 的原子类都存放在`java.util.concurrent.atomic`下,如下图所示。 - -![JUC原子类概览](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/JUC原子类概览.png) - -根据操作的数据类型,可以将JUC包中的原子类分为4类 - -**基本类型** - -使用原子的方式更新基本类型 - -- AtomicInteger:整型原子类 -- AtomicLong:长整型原子类 -- AtomicBoolean :布尔型原子类 - -**数组类型** - -使用原子的方式更新数组里的某个元素 - - -- AtomicIntegerArray:整型数组原子类 -- AtomicLongArray:长整型数组原子类 -- AtomicReferenceArray :引用类型数组原子类 - -**引用类型** - -- AtomicReference:引用类型原子类 -- AtomicReferenceFieldUpdater:原子更新引用类型里的字段 -- AtomicMarkableReference :原子更新带有标记位的引用类型 - -**对象的属性修改类型** - -- AtomicIntegerFieldUpdater:原子更新整型字段的更新器 -- AtomicLongFieldUpdater:原子更新长整型字段的更新器 -- AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。 -- AtomicMarkableReference:原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来,也可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。 - -**CAS ABA 问题** -- 描述: 第一个线程取到了变量 x 的值 A,然后巴拉巴拉干别的事,总之就是只拿到了变量 x 的值 A。这段时间内第二个线程也取到了变量 x 的值 A,然后把变量 x 的值改为 B,然后巴拉巴拉干别的事,最后又把变量 x 的值变为 A (相当于还原了)。在这之后第一个线程终于进行了变量 x 的操作,但是此时变量 x 的值还是 A,所以 compareAndSet 操作是成功。 -- 例子描述(可能不太合适,但好理解): 年初,现金为零,然后通过正常劳动赚了三百万,之后正常消费了(比如买房子)三百万。年末,虽然现金零收入(可能变成其他形式了),但是赚了钱是事实,还是得交税的! -- 代码例子(以``` AtomicInteger ```为例) - -```java -import java.util.concurrent.atomic.AtomicInteger; - -public class AtomicIntegerDefectDemo { - public static void main(String[] args) { - defectOfABA(); - } - - static void defectOfABA() { - final AtomicInteger atomicInteger = new AtomicInteger(1); - - Thread coreThread = new Thread( - () -> { - final int currentValue = atomicInteger.get(); - System.out.println(Thread.currentThread().getName() + " ------ currentValue=" + currentValue); - - // 这段目的:模拟处理其他业务花费的时间 - try { - Thread.sleep(300); - } catch (InterruptedException e) { - e.printStackTrace(); - } - - boolean casResult = atomicInteger.compareAndSet(1, 2); - System.out.println(Thread.currentThread().getName() - + " ------ currentValue=" + currentValue - + ", finalValue=" + atomicInteger.get() - + ", compareAndSet Result=" + casResult); - } - ); - coreThread.start(); - - // 这段目的:为了让 coreThread 线程先跑起来 - try { - Thread.sleep(100); - } catch (InterruptedException e) { - e.printStackTrace(); - } - - Thread amateurThread = new Thread( - () -> { - int currentValue = atomicInteger.get(); - boolean casResult = atomicInteger.compareAndSet(1, 2); - System.out.println(Thread.currentThread().getName() - + " ------ currentValue=" + currentValue - + ", finalValue=" + atomicInteger.get() - + ", compareAndSet Result=" + casResult); - - currentValue = atomicInteger.get(); - casResult = atomicInteger.compareAndSet(2, 1); - System.out.println(Thread.currentThread().getName() - + " ------ currentValue=" + currentValue - + ", finalValue=" + atomicInteger.get() - + ", compareAndSet Result=" + casResult); - } - ); - amateurThread.start(); - } -} -``` - -输出内容如下: - -``` -Thread-0 ------ currentValue=1 -Thread-1 ------ currentValue=1, finalValue=2, compareAndSet Result=true -Thread-1 ------ currentValue=2, finalValue=1, compareAndSet Result=true -Thread-0 ------ currentValue=1, finalValue=2, compareAndSet Result=true -``` - -下面我们来详细介绍一下这些原子类。 - -### 2 基本类型原子类 - -#### 2.1 基本类型原子类介绍 - -使用原子的方式更新基本类型 - -- AtomicInteger:整型原子类 -- AtomicLong:长整型原子类 -- AtomicBoolean :布尔型原子类 - -上面三个类提供的方法几乎相同,所以我们这里以 AtomicInteger 为例子来介绍。 - - **AtomicInteger 类常用方法** - -```java -public final int get() //获取当前的值 -public final int getAndSet(int newValue)//获取当前的值,并设置新的值 -public final int getAndIncrement()//获取当前的值,并自增 -public final int getAndDecrement() //获取当前的值,并自减 -public final int getAndAdd(int delta) //获取当前的值,并加上预期的值 -boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update) -public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。 -``` - -#### 2.2 AtomicInteger 常见方法使用 - -```java -import java.util.concurrent.atomic.AtomicInteger; - -public class AtomicIntegerTest { - - public static void main(String[] args) { - // TODO Auto-generated method stub - int temvalue = 0; - AtomicInteger i = new AtomicInteger(0); - temvalue = i.getAndSet(3); - System.out.println("temvalue:" + temvalue + "; i:" + i);//temvalue:0; i:3 - temvalue = i.getAndIncrement(); - System.out.println("temvalue:" + temvalue + "; i:" + i);//temvalue:3; i:4 - temvalue = i.getAndAdd(5); - System.out.println("temvalue:" + temvalue + "; i:" + i);//temvalue:4; i:9 - } - -} -``` - -#### 2.3 基本数据类型原子类的优势 - -通过一个简单例子带大家看一下基本数据类型原子类的优势 - -**①多线程环境不使用原子类保证线程安全(基本数据类型)** - -```java -class Test { - private volatile int count = 0; - //若要线程安全执行执行count++,需要加锁 - public synchronized void increment() { - count++; - } - - public int getCount() { - return count; - } -} -``` -**②多线程环境使用原子类保证线程安全(基本数据类型)** - -```java -class Test2 { - private AtomicInteger count = new AtomicInteger(); - - public void increment() { - count.incrementAndGet(); - } - //使用AtomicInteger之后,不需要加锁,也可以实现线程安全。 - public int getCount() { - return count.get(); - } -} - -``` -#### 2.4 AtomicInteger 线程安全原理简单分析 - -AtomicInteger 类的部分源码: - -```java - // setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用) - private static final Unsafe unsafe = Unsafe.getUnsafe(); - private static final long valueOffset; - - static { - try { - valueOffset = unsafe.objectFieldOffset - (AtomicInteger.class.getDeclaredField("value")); - } catch (Exception ex) { throw new Error(ex); } - } - - private volatile int value; -``` - -AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。 - -CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址。另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。 - - -### 3 数组类型原子类 - -#### 3.1 数组类型原子类介绍 - -使用原子的方式更新数组里的某个元素 - - -- AtomicIntegerArray:整形数组原子类 -- AtomicLongArray:长整形数组原子类 -- AtomicReferenceArray :引用类型数组原子类 - -上面三个类提供的方法几乎相同,所以我们这里以 AtomicIntegerArray 为例子来介绍。 - -**AtomicIntegerArray 类常用方法** - -```java -public final int get(int i) //获取 index=i 位置元素的值 -public final int getAndSet(int i, int newValue)//返回 index=i 位置的当前的值,并将其设置为新值:newValue -public final int getAndIncrement(int i)//获取 index=i 位置元素的值,并让该位置的元素自增 -public final int getAndDecrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自减 -public final int getAndAdd(int delta) //获取 index=i 位置元素的值,并加上预期的值 -boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将 index=i 位置的元素值设置为输入值(update) -public final void lazySet(int i, int newValue)//最终 将index=i 位置的元素设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。 -``` -#### 3.2 AtomicIntegerArray 常见方法使用 - -```java - -import java.util.concurrent.atomic.AtomicIntegerArray; - -public class AtomicIntegerArrayTest { - - public static void main(String[] args) { - // TODO Auto-generated method stub - int temvalue = 0; - int[] nums = { 1, 2, 3, 4, 5, 6 }; - AtomicIntegerArray i = new AtomicIntegerArray(nums); - for (int j = 0; j < nums.length; j++) { - System.out.println(i.get(j)); - } - temvalue = i.getAndSet(0, 2); - System.out.println("temvalue:" + temvalue + "; i:" + i); - temvalue = i.getAndIncrement(0); - System.out.println("temvalue:" + temvalue + "; i:" + i); - temvalue = i.getAndAdd(0, 5); - System.out.println("temvalue:" + temvalue + "; i:" + i); - } - -} -``` - -### 4 引用类型原子类 - -#### 4.1 引用类型原子类介绍 - -基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用 引用类型原子类。 - -- AtomicReference:引用类型原子类 -- AtomicStampedReference:原子更新引用类型里的字段原子类 -- AtomicMarkableReference :原子更新带有标记位的引用类型 - -上面三个类提供的方法几乎相同,所以我们这里以 AtomicReference 为例子来介绍。 - -#### 4.2 AtomicReference 类使用示例 - -```java -import java.util.concurrent.atomic.AtomicReference; - -public class AtomicReferenceTest { - - public static void main(String[] args) { - AtomicReference ar = new AtomicReference(); - Person person = new Person("SnailClimb", 22); - ar.set(person); - Person updatePerson = new Person("Daisy", 20); - ar.compareAndSet(person, updatePerson); - - System.out.println(ar.get().getName()); - System.out.println(ar.get().getAge()); - } -} - -class Person { - private String name; - private int age; - - public Person(String name, int age) { - super(); - this.name = name; - this.age = age; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public int getAge() { - return age; - } - - public void setAge(int age) { - this.age = age; - } - -} -``` -上述代码首先创建了一个 Person 对象,然后把 Person 对象设置进 AtomicReference 对象中,然后调用 compareAndSet 方法,该方法就是通过 CAS 操作设置 ar。如果 ar 的值为 person 的话,则将其设置为 updatePerson。实现原理与 AtomicInteger 类中的 compareAndSet 方法相同。运行上面的代码后的输出结果如下: - -``` -Daisy -20 -``` -#### 4.3 AtomicStampedReference 类使用示例 - -```java -import java.util.concurrent.atomic.AtomicStampedReference; - -public class AtomicStampedReferenceDemo { - public static void main(String[] args) { - // 实例化、取当前值和 stamp 值 - final Integer initialRef = 0, initialStamp = 0; - final AtomicStampedReference asr = new AtomicStampedReference<>(initialRef, initialStamp); - System.out.println("currentValue=" + asr.getReference() + ", currentStamp=" + asr.getStamp()); - - // compare and set - final Integer newReference = 666, newStamp = 999; - final boolean casResult = asr.compareAndSet(initialRef, newReference, initialStamp, newStamp); - System.out.println("currentValue=" + asr.getReference() - + ", currentStamp=" + asr.getStamp() - + ", casResult=" + casResult); - - // 获取当前的值和当前的 stamp 值 - int[] arr = new int[1]; - final Integer currentValue = asr.get(arr); - final int currentStamp = arr[0]; - System.out.println("currentValue=" + currentValue + ", currentStamp=" + currentStamp); - - // 单独设置 stamp 值 - final boolean attemptStampResult = asr.attemptStamp(newReference, 88); - System.out.println("currentValue=" + asr.getReference() - + ", currentStamp=" + asr.getStamp() - + ", attemptStampResult=" + attemptStampResult); - - // 重新设置当前值和 stamp 值 - asr.set(initialRef, initialStamp); - System.out.println("currentValue=" + asr.getReference() + ", currentStamp=" + asr.getStamp()); - - // [不推荐使用,除非搞清楚注释的意思了] weak compare and set - // 困惑!weakCompareAndSet 这个方法最终还是调用 compareAndSet 方法。[版本: jdk-8u191] - // 但是注释上写着 "May fail spuriously and does not provide ordering guarantees, - // so is only rarely an appropriate alternative to compareAndSet." - // todo 感觉有可能是 jvm 通过方法名在 native 方法里面做了转发 - final boolean wCasResult = asr.weakCompareAndSet(initialRef, newReference, initialStamp, newStamp); - System.out.println("currentValue=" + asr.getReference() - + ", currentStamp=" + asr.getStamp() - + ", wCasResult=" + wCasResult); - } -} -``` - -输出结果如下: -``` -currentValue=0, currentStamp=0 -currentValue=666, currentStamp=999, casResult=true -currentValue=666, currentStamp=999 -currentValue=666, currentStamp=88, attemptStampResult=true -currentValue=0, currentStamp=0 -currentValue=666, currentStamp=999, wCasResult=true -``` - -#### 4.4 AtomicMarkableReference 类使用示例 - -``` java -import java.util.concurrent.atomic.AtomicMarkableReference; - -public class AtomicMarkableReferenceDemo { - public static void main(String[] args) { - // 实例化、取当前值和 mark 值 - final Boolean initialRef = null, initialMark = false; - final AtomicMarkableReference amr = new AtomicMarkableReference<>(initialRef, initialMark); - System.out.println("currentValue=" + amr.getReference() + ", currentMark=" + amr.isMarked()); - - // compare and set - final Boolean newReference1 = true, newMark1 = true; - final boolean casResult = amr.compareAndSet(initialRef, newReference1, initialMark, newMark1); - System.out.println("currentValue=" + amr.getReference() - + ", currentMark=" + amr.isMarked() - + ", casResult=" + casResult); - - // 获取当前的值和当前的 mark 值 - boolean[] arr = new boolean[1]; - final Boolean currentValue = amr.get(arr); - final boolean currentMark = arr[0]; - System.out.println("currentValue=" + currentValue + ", currentMark=" + currentMark); - - // 单独设置 mark 值 - final boolean attemptMarkResult = amr.attemptMark(newReference1, false); - System.out.println("currentValue=" + amr.getReference() - + ", currentMark=" + amr.isMarked() - + ", attemptMarkResult=" + attemptMarkResult); - - // 重新设置当前值和 mark 值 - amr.set(initialRef, initialMark); - System.out.println("currentValue=" + amr.getReference() + ", currentMark=" + amr.isMarked()); - - // [不推荐使用,除非搞清楚注释的意思了] weak compare and set - // 困惑!weakCompareAndSet 这个方法最终还是调用 compareAndSet 方法。[版本: jdk-8u191] - // 但是注释上写着 "May fail spuriously and does not provide ordering guarantees, - // so is only rarely an appropriate alternative to compareAndSet." - // todo 感觉有可能是 jvm 通过方法名在 native 方法里面做了转发 - final boolean wCasResult = amr.weakCompareAndSet(initialRef, newReference1, initialMark, newMark1); - System.out.println("currentValue=" + amr.getReference() - + ", currentMark=" + amr.isMarked() - + ", wCasResult=" + wCasResult); - } -} -``` - -输出结果如下: -``` -currentValue=null, currentMark=false -currentValue=true, currentMark=true, casResult=true -currentValue=true, currentMark=true -currentValue=true, currentMark=false, attemptMarkResult=true -currentValue=null, currentMark=false -currentValue=true, currentMark=true, wCasResult=true -``` - -### 5 对象的属性修改类型原子类 - -#### 5.1 对象的属性修改类型原子类介绍 - -如果需要原子更新某个类里的某个字段时,需要用到对象的属性修改类型原子类。 - -- AtomicIntegerFieldUpdater:原子更新整形字段的更新器 -- AtomicLongFieldUpdater:原子更新长整形字段的更新器 -- AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。 - -要想原子地更新对象的属性需要两步。第一步,因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新的对象属性必须使用 public volatile 修饰符。 - -上面三个类提供的方法几乎相同,所以我们这里以 `AtomicIntegerFieldUpdater`为例子来介绍。 - -#### 5.2 AtomicIntegerFieldUpdater 类使用示例 - -```java -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; - -public class AtomicIntegerFieldUpdaterTest { - public static void main(String[] args) { - AtomicIntegerFieldUpdater a = AtomicIntegerFieldUpdater.newUpdater(User.class, "age"); - - User user = new User("Java", 22); - System.out.println(a.getAndIncrement(user));// 22 - System.out.println(a.get(user));// 23 - } -} - -class User { - private String name; - public volatile int age; - - public User(String name, int age) { - super(); - this.name = name; - this.age = age; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public int getAge() { - return age; - } - - public void setAge(int age) { - this.age = age; - } - -} -``` - -输出结果: - -``` -22 -23 -``` - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) diff --git a/docs/java/Multithread/JavaConcurrencyAdvancedCommonInterviewQuestions.md b/docs/java/Multithread/JavaConcurrencyAdvancedCommonInterviewQuestions.md deleted file mode 100644 index 51b21fd..0000000 --- a/docs/java/Multithread/JavaConcurrencyAdvancedCommonInterviewQuestions.md +++ /dev/null @@ -1,929 +0,0 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 - - - -- [Java 并发进阶常见面试题总结](#java-并发进阶常见面试题总结) - - [1. synchronized 关键字](#1-synchronized-关键字) - - [1.1. 说一说自己对于 synchronized 关键字的了解](#11-说一说自己对于-synchronized-关键字的了解) - - [1.2. 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗](#12-说说自己是怎么使用-synchronized-关键字在项目中用到了吗) - - [1.3. 讲一下 synchronized 关键字的底层原理](#13-讲一下-synchronized-关键字的底层原理) - - [1.4. 说说 JDK1.6 之后的synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗](#14-说说-jdk16-之后的synchronized-关键字底层做了哪些优化可以详细介绍一下这些优化吗) - - [1.5. 谈谈 synchronized和ReentrantLock 的区别](#15-谈谈-synchronized和reentrantlock-的区别) - - [2. volatile关键字](#2-volatile关键字) - - [2.1. 讲一下Java内存模型](#21-讲一下java内存模型) - - [2.2. 说说 synchronized 关键字和 volatile 关键字的区别](#22-说说-synchronized-关键字和-volatile-关键字的区别) - - [3. ThreadLocal](#3-threadlocal) - - [3.1. ThreadLocal简介](#31-threadlocal简介) - - [3.2. ThreadLocal示例](#32-threadlocal示例) - - [3.3. ThreadLocal原理](#33-threadlocal原理) - - [3.4. ThreadLocal 内存泄露问题](#34-threadlocal-内存泄露问题) - - [4. 线程池](#4-线程池) - - [4.1. 为什么要用线程池?](#41-为什么要用线程池) - - [4.2. 实现Runnable接口和Callable接口的区别](#42-实现runnable接口和callable接口的区别) - - [4.3. 执行execute()方法和submit()方法的区别是什么呢?](#43-执行execute方法和submit方法的区别是什么呢) - - [4.4. 如何创建线程池](#44-如何创建线程池) - - [5. Atomic 原子类](#5-atomic-原子类) - - [5.1. 介绍一下Atomic 原子类](#51-介绍一下atomic-原子类) - - [5.2. JUC 包中的原子类是哪4类?](#52-juc-包中的原子类是哪4类) - - [5.3. 讲讲 AtomicInteger 的使用](#53-讲讲-atomicinteger-的使用) - - [5.4. 能不能给我简单介绍一下 AtomicInteger 类的原理](#54-能不能给我简单介绍一下-atomicinteger-类的原理) - - [6. AQS](#6-aqs) - - [6.1. AQS 介绍](#61-aqs-介绍) - - [6.2. AQS 原理分析](#62-aqs-原理分析) - - [6.2.1. AQS 原理概览](#621-aqs-原理概览) - - [6.2.2. AQS 对资源的共享方式](#622-aqs-对资源的共享方式) - - [6.2.3. AQS底层使用了模板方法模式](#623-aqs底层使用了模板方法模式) - - [6.3. AQS 组件总结](#63-aqs-组件总结) - - [7 Reference](#7-reference) - - - -# Java 并发进阶常见面试题总结 - -## 1. synchronized 关键字 - -### 1.1. 说一说自己对于 synchronized 关键字的了解 - -synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。 - -另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。 - - -### 1.2. 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗 - -**synchronized关键字最主要的三种使用方式:** - -- **修饰实例方法:** 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁 -- **修饰静态方法:** 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,**因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁**。 -- **修饰代码块:** 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。 - -**总结:** synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能! - -下面我以一个常见的面试题为例讲解一下 synchronized 关键字的具体使用。 - -面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理呗!” - -**双重校验锁实现对象单例(线程安全)** - -```java -public class Singleton { - - private volatile static Singleton uniqueInstance; - - private Singleton() { - } - - public static Singleton getUniqueInstance() { - //先判断对象是否已经实例过,没有实例化过才进入加锁代码 - if (uniqueInstance == null) { - //类对象加锁 - synchronized (Singleton.class) { - if (uniqueInstance == null) { - uniqueInstance = new Singleton(); - } - } - } - return uniqueInstance; - } -} -``` -另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。 - -uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行: - -1. 为 uniqueInstance 分配内存空间 -2. 初始化 uniqueInstance -3. 将 uniqueInstance 指向分配的内存地址 - -但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。 - -使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。 - -### 1.3. 讲一下 synchronized 关键字的底层原理 - -**synchronized 关键字底层原理属于 JVM 层面。** - -**① synchronized 同步语句块的情况** - -```java -public class SynchronizedDemo { - public void method() { - synchronized (this) { - System.out.println("synchronized 代码块"); - } - } -} - -``` - -通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 `javac SynchronizedDemo.java` 命令生成编译后的 .class 文件,然后执行`javap -c -s -v -l SynchronizedDemo.class`。 - -![synchronized关键字原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/synchronized关键字原理.png) - -从上面我们可以看出: - -**synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。** 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。 - -**② synchronized 修饰方法的的情况** - -```java -public class SynchronizedDemo2 { - public synchronized void method() { - System.out.println("synchronized 方法"); - } -} - -``` - -![synchronized关键字原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/synchronized关键字原理2.png) - -synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。 - - -### 1.4. 说说 JDK1.6 之后的synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗 - -JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。 - -锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。 - -关于这几种优化的详细信息可以查看笔主的这篇文章: - -### 1.5. 谈谈 synchronized和ReentrantLock 的区别 - - -**① 两者都是可重入锁** - -两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。 - -**② synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API** - -synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。 - -**③ ReentrantLock 比 synchronized 增加了一些高级功能** - -相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:**①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)** - -- **ReentrantLock提供了一种能够中断等待锁的线程的机制**,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 -- **ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。** ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的`ReentrantLock(boolean fair)`构造方法来制定是否是公平的。 -- synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),**线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知”** ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。 - -如果你想使用上述功能,那么选择ReentrantLock是一个不错的选择。 - -**④ 性能已不是选择标准** - -## 2. volatile关键字 - -### 2.1. 讲一下Java内存模型 - - -在 JDK1.2 之前,Java的内存模型实现总是从**主存**(即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变量保存**本地内存**(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成**数据的不一致**。 - -![数据不一致](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/数据不一致.png) - -要解决这个问题,就需要把变量声明为**volatile**,这就指示 JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。 - -说白了, **volatile** 关键字的主要作用就是保证变量的可见性然后还有一个作用是防止指令重排序。 - -![volatile关键字的可见性](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/volatile关键字的可见性.png) - - -### 2.2. 说说 synchronized 关键字和 volatile 关键字的区别 - - synchronized关键字和volatile关键字比较 - -- **volatile关键字**是线程同步的**轻量级实现**,所以**volatile性能肯定比synchronized关键字要好**。但是**volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块**。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,**实际开发中使用 synchronized 关键字的场景还是更多一些**。 -- **多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞** -- **volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。** -- **volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。** - -## 3. ThreadLocal - -### 3.1. ThreadLocal简介 - -通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。**如果想实现每一个线程都有自己的专属本地变量该如何解决呢?** JDK中提供的`ThreadLocal`类正是为了解决这样的问题。 **`ThreadLocal`类主要解决的就是让每个线程绑定自己的值,可以将`ThreadLocal`类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。** - -**如果你创建了一个`ThreadLocal`变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是`ThreadLocal`变量名的由来。他们可以使用 `get()` 和 `set()` 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。** - -再举个简单的例子: - -比如有两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么ThreadLocal就是用来避免这两个线程竞争的。 - -### 3.2. ThreadLocal示例 - -相信看了上面的解释,大家已经搞懂 ThreadLocal 类是个什么东西了。 - -```java -import java.text.SimpleDateFormat; -import java.util.Random; - -public class ThreadLocalExample implements Runnable{ - - // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本 - private static final ThreadLocal formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm")); - - public static void main(String[] args) throws InterruptedException { - ThreadLocalExample obj = new ThreadLocalExample(); - for(int i=0 ; i<10; i++){ - Thread t = new Thread(obj, ""+i); - Thread.sleep(new Random().nextInt(1000)); - t.start(); - } - } - - @Override - public void run() { - System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern()); - try { - Thread.sleep(new Random().nextInt(1000)); - } catch (InterruptedException e) { - e.printStackTrace(); - } - //formatter pattern is changed here by thread, but it won't reflect to other threads - formatter.set(new SimpleDateFormat()); - - System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern()); - } - -} - -``` - -Output: - -``` -Thread Name= 0 default Formatter = yyyyMMdd HHmm -Thread Name= 0 formatter = yy-M-d ah:mm -Thread Name= 1 default Formatter = yyyyMMdd HHmm -Thread Name= 2 default Formatter = yyyyMMdd HHmm -Thread Name= 1 formatter = yy-M-d ah:mm -Thread Name= 3 default Formatter = yyyyMMdd HHmm -Thread Name= 2 formatter = yy-M-d ah:mm -Thread Name= 4 default Formatter = yyyyMMdd HHmm -Thread Name= 3 formatter = yy-M-d ah:mm -Thread Name= 4 formatter = yy-M-d ah:mm -Thread Name= 5 default Formatter = yyyyMMdd HHmm -Thread Name= 5 formatter = yy-M-d ah:mm -Thread Name= 6 default Formatter = yyyyMMdd HHmm -Thread Name= 6 formatter = yy-M-d ah:mm -Thread Name= 7 default Formatter = yyyyMMdd HHmm -Thread Name= 7 formatter = yy-M-d ah:mm -Thread Name= 8 default Formatter = yyyyMMdd HHmm -Thread Name= 9 default Formatter = yyyyMMdd HHmm -Thread Name= 8 formatter = yy-M-d ah:mm -Thread Name= 9 formatter = yy-M-d ah:mm -``` - -从输出中可以看出,Thread-0已经改变了formatter的值,但仍然是thread-2默认格式化程序与初始化值相同,其他线程也一样。 - -上面有一段代码用到了创建 `ThreadLocal` 变量的那段代码用到了 Java8 的知识,它等于下面这段代码,如果你写了下面这段代码的话,IDEA会提示你转换为Java8的格式(IDEA真的不错!)。因为ThreadLocal类在Java 8中扩展,使用一个新的方法`withInitial()`,将Supplier功能接口作为参数。 - -```java - private static final ThreadLocal formatter = new ThreadLocal(){ - @Override - protected SimpleDateFormat initialValue() - { - return new SimpleDateFormat("yyyyMMdd HHmm"); - } - }; -``` - -### 3.3. ThreadLocal原理 - -从 `Thread`类源代码入手。 - -```java -public class Thread implements Runnable { - ...... -//与此线程有关的ThreadLocal值。由ThreadLocal类维护 -ThreadLocal.ThreadLocalMap threadLocals = null; - -//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护 -ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; - ...... -} -``` - -从上面`Thread`类 源代码可以看出`Thread` 类中有一个 `threadLocals` 和 一个 `inheritableThreadLocals` 变量,它们都是 `ThreadLocalMap` 类型的变量,我们可以把 `ThreadLocalMap` 理解为`ThreadLocal` 类实现的定制化的 `HashMap`。默认情况下这两个变量都是null,只有当前线程调用 `ThreadLocal` 类的 `set`或`get`方法时才创建它们,实际上调用这两个方法的时候,我们调用的是`ThreadLocalMap`类对应的 `get()`、`set() `方法。 - -`ThreadLocal`类的`set()`方法 - -```java - public void set(T value) { - Thread t = Thread.currentThread(); - ThreadLocalMap map = getMap(t); - if (map != null) - map.set(this, value); - else - createMap(t, value); - } - ThreadLocalMap getMap(Thread t) { - return t.threadLocals; - } -``` - -通过上面这些内容,我们足以通过猜测得出结论:**最终的变量是放在了当前线程的 `ThreadLocalMap` 中,并不是存在 `ThreadLocal` 上,`ThreadLocal` 可以理解为只是`ThreadLocalMap`的封装,传递了变量值。** `ThrealLocal` 类中可以通过`Thread.currentThread()`获取到当前线程对象后,直接通过`getMap(Thread t)`可以访问到该线程的`ThreadLocalMap`对象。 - -**每个`Thread`中都具备一个`ThreadLocalMap`,而`ThreadLocalMap`可以存储以`ThreadLocal`为key的键值对。** 比如我们在同一个线程中声明了两个 `ThreadLocal` 对象的话,会使用 `Thread`内部都是使用仅有那个`ThreadLocalMap` 存放数据的,`ThreadLocalMap`的 key 就是 `ThreadLocal`对象,value 就是 `ThreadLocal` 对象调用`set`方法设置的值。 - -`ThreadLocal` 内部维护的是一个类似 `Map` 的`ThreadLocalMap` 数据结构,`key` 为当前对象的 `Thread` 对象,值为 Object 对象。 - -![ThreadLocal数据结构](https://upload-images.jianshu.io/upload_images/7432604-ad2ff581127ba8cc.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/806) - -`ThreadLocalMap`是`ThreadLocal`的静态内部类。 - -![ThreadLocal内部类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/ThreadLocal内部类.png) - -### 3.4. ThreadLocal 内存泄露问题 - -`ThreadLocalMap` 中使用的 key 为 `ThreadLocal` 的弱引用,而 value 是强引用。所以,如果 `ThreadLocal` 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,`ThreadLocalMap` 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 `set()`、`get()`、`remove()` 方法的时候,会清理掉 key 为 null 的记录。使用完 `ThreadLocal`方法后 最好手动调用`remove()`方法 - -```java - static class Entry extends WeakReference> { - /** The value associated with this ThreadLocal. */ - Object value; - - Entry(ThreadLocal k, Object v) { - super(k); - value = v; - } - } -``` - -**弱引用介绍:** - -> 如果一个对象只具有弱引用,那就类似于**可有可无的生活用品**。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。 -> -> 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。 - -## 4. 线程池 - -### 4.1. 为什么要用线程池? - -> **池化技术相比大家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。** - -**线程池**提供了一种限制和管理资源(包括执行一个任务)。 每个**线程池**还维护一些基本统计信息,例如已完成任务的数量。 - -这里借用《Java 并发编程的艺术》提到的来说一下**使用线程池的好处**: - -- **降低资源消耗**。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 -- **提高响应速度**。当任务到达时,任务可以不需要的等到线程创建就能立即执行。 -- **提高线程的可管理性**。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 - -### 4.2. 实现Runnable接口和Callable接口的区别 - -`Runnable`自Java 1.0以来一直存在,但`Callable`仅在Java 1.5中引入,目的就是为了来处理`Runnable`不支持的用例。**`Runnable` 接口**不会返回结果或抛出检查异常,但是**`Callable` 接口**可以。所以,如果任务不需要返回结果或抛出异常推荐使用 **`Runnable` 接口**,这样代码看起来会更加简洁。 - -工具类 `Executors` 可以实现 `Runnable` 对象和 `Callable` 对象之间的相互转换。(`Executors.callable(Runnable task`)或 `Executors.callable(Runnable task,Object resule)`)。 - -`Runnable.java` - -```java -@FunctionalInterface -public interface Runnable { - /** - * 被线程执行,没有返回值也无法抛出异常 - */ - public abstract void run(); -} -``` - -`Callable.java` - -```java -@FunctionalInterface -public interface Callable { - /** - * 计算结果,或在无法这样做时抛出异常。 - * @return 计算得出的结果 - * @throws 如果无法计算结果,则抛出异常 - */ - V call() throws Exception; -} -``` - -### 4.3. 执行execute()方法和submit()方法的区别是什么呢? - -1. **`execute()`方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;** -2. **`submit()`方法用于提交需要返回值的任务。线程池会返回一个 `Future` 类型的对象,通过这个 `Future` 对象可以判断任务是否执行成功**,并且可以通过 `Future` 的 `get()`方法来获取返回值,`get()`方法会阻塞当前线程直到任务完成,而使用 `get(long timeout,TimeUnit unit)`方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。 - -我们以**`AbstractExecutorService`**接口中的一个 `submit` 方法为例子来看看源代码: - -```java - public Future submit(Runnable task) { - if (task == null) throw new NullPointerException(); - RunnableFuture ftask = newTaskFor(task, null); - execute(ftask); - return ftask; - } -``` - -上面方法调用的 `newTaskFor` 方法返回了一个 `FutureTask` 对象。 - -```java - protected RunnableFuture newTaskFor(Runnable runnable, T value) { - return new FutureTask(runnable, value); - } -``` - -我们再来看看`execute()`方法: - -```java - public void execute(Runnable command) { - ... - } -``` - -### 4.4. 如何创建线程池 - -《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险 - -> Executors 返回线程池对象的弊端如下: -> -> - **FixedThreadPool 和 SingleThreadExecutor** : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致OOM。 -> - **CachedThreadPool 和 ScheduledThreadPool** : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。 - -**方式一:通过构造方法实现** -![ThreadPoolExecutor构造方法](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/ThreadPoolExecutor构造方法.png) -**方式二:通过Executor 框架的工具类Executors来实现** -我们可以创建三种类型的ThreadPoolExecutor: - -- **FixedThreadPool** : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。 -- **SingleThreadExecutor:** 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。 -- **CachedThreadPool:** 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。 - -对应Executors工具类中的方法如图所示: -![Executor框架的工具类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/Executor框架的工具类.png) - -### 4.5 ThreadPoolExecutor 类分析 - -`ThreadPoolExecutor` 类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法的基础上产生(其他几个构造方法说白点都是给定某些默认参数的构造方法比如默认制定拒绝策略是什么),这里就不贴代码讲了,比较简单。 - -```java - /** - * 用给定的初始参数创建一个新的ThreadPoolExecutor。 - */ - public ThreadPoolExecutor(int corePoolSize, - int maximumPoolSize, - long keepAliveTime, - TimeUnit unit, - BlockingQueue workQueue, - ThreadFactory threadFactory, - RejectedExecutionHandler handler) { - if (corePoolSize < 0 || - maximumPoolSize <= 0 || - maximumPoolSize < corePoolSize || - keepAliveTime < 0) - throw new IllegalArgumentException(); - if (workQueue == null || threadFactory == null || handler == null) - throw new NullPointerException(); - this.corePoolSize = corePoolSize; - this.maximumPoolSize = maximumPoolSize; - this.workQueue = workQueue; - this.keepAliveTime = unit.toNanos(keepAliveTime); - this.threadFactory = threadFactory; - this.handler = handler; - } -``` - -**下面这些对创建 非常重要,在后面使用线程池的过程中你一定会用到!所以,务必拿着小本本记清楚。** - -#### 4.5.1 `ThreadPoolExecutor`构造函数重要参数分析 - -**`ThreadPoolExecutor` 3 个最重要的参数:** - -- **`corePoolSize` :** 核心线程数线程数定义了最小可以同时运行的线程数量。 -- **`maximumPoolSize` :** 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 -- **`workQueue`:** 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。 - -`ThreadPoolExecutor`其他常见参数: - -1. **`keepAliveTime`**:当线程池中的线程数量大于 `corePoolSize` 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 `keepAliveTime`才会被回收销毁; -2. **`unit`** : `keepAliveTime` 参数的时间单位。 -3. **`threadFactory`** :executor 创建新线程的时候会用到。 -4. **`handler`** :饱和策略。关于饱和策略下面单独介绍一下。 - -#### 4.5.2 `ThreadPoolExecutor` 饱和策略 - -**`ThreadPoolExecutor` 饱和策略定义:** - -如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,`ThreadPoolTaskExecutor` 定义一些策略: - -- **`ThreadPoolExecutor.AbortPolicy`**:抛出 `RejectedExecutionException`来拒绝新任务的处理。 -- **`ThreadPoolExecutor.CallerRunsPolicy`**:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。 -- **`ThreadPoolExecutor.DiscardPolicy`:** 不处理新任务,直接丢弃掉。 -- **`ThreadPoolExecutor.DiscardOldestPolicy`:** 此策略将丢弃最早的未处理的任务请求。 - -举个例子: Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecutor` 的构造函数创建线程池的时候,当我们不指定 `RejectedExecutionHandler` 饱和策略的话来配置线程池的时候默认使用的是 `ThreadPoolExecutor.AbortPolicy`。在默认情况下,`ThreadPoolExecutor` 将抛出 `RejectedExecutionException` 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 `ThreadPoolExecutor.CallerRunsPolicy`。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 `ThreadPoolExecutor` 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了) - -### 4.6 一个简单的线程池Demo:`Runnable`+`ThreadPoolExecutor` - -为了让大家更清楚上面的面试题中的一些概念,我写了一个简单的线程池 Demo。 - -首先创建一个 `Runnable` 接口的实现类(当然也可以是 `Callable` 接口,我们上面也说了两者的区别。) - -`MyRunnable.java` - -```java -import java.util.Date; - -/** - * 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。 - * @author shuang.kou - */ -public class MyRunnable implements Runnable { - - private String command; - - public MyRunnable(String s) { - this.command = s; - } - - @Override - public void run() { - System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date()); - processCommand(); - System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date()); - } - - private void processCommand() { - try { - Thread.sleep(5000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - @Override - public String toString() { - return this.command; - } -} - -``` - -编写测试程序,我们这里以阿里巴巴推荐的使用 `ThreadPoolExecutor` 构造函数自定义参数的方式来创建线程池。 - -`ThreadPoolExecutorDemo.java` - -```java -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -public class ThreadPoolExecutorDemo { - - private static final int CORE_POOL_SIZE = 5; - private static final int MAX_POOL_SIZE = 10; - private static final int QUEUE_CAPACITY = 100; - private static final Long KEEP_ALIVE_TIME = 1L; - public static void main(String[] args) { - - //使用阿里巴巴推荐的创建线程池的方式 - //通过ThreadPoolExecutor构造函数自定义参数创建 - ThreadPoolExecutor executor = new ThreadPoolExecutor( - CORE_POOL_SIZE, - MAX_POOL_SIZE, - KEEP_ALIVE_TIME, - TimeUnit.SECONDS, - new ArrayBlockingQueue<>(QUEUE_CAPACITY), - new ThreadPoolExecutor.CallerRunsPolicy()); - - for (int i = 0; i < 10; i++) { - //创建WorkerThread对象(WorkerThread类实现了Runnable 接口) - Runnable worker = new MyRunnable("" + i); - //执行Runnable - executor.execute(worker); - } - //终止线程池 - executor.shutdown(); - while (!executor.isTerminated()) { - } - System.out.println("Finished all threads"); - } -} - -``` - -可以看到我们上面的代码指定了: - -1. `corePoolSize`: 核心线程数为 5。 -2. `maximumPoolSize` :最大线程数 10 -3. `keepAliveTime` : 等待时间为 1L。 -4. `unit`: 等待时间的单位为 TimeUnit.SECONDS。 -5. `workQueue`:任务队列为 `ArrayBlockingQueue`,并且容量为 100; -6. `handler`:饱和策略为 `CallerRunsPolicy`。 - -**Output:** - -``` -pool-1-thread-2 Start. Time = Tue Nov 12 20:59:44 CST 2019 -pool-1-thread-5 Start. Time = Tue Nov 12 20:59:44 CST 2019 -pool-1-thread-4 Start. Time = Tue Nov 12 20:59:44 CST 2019 -pool-1-thread-1 Start. Time = Tue Nov 12 20:59:44 CST 2019 -pool-1-thread-3 Start. Time = Tue Nov 12 20:59:44 CST 2019 -pool-1-thread-5 End. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-3 End. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-2 End. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-4 End. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-1 End. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-2 Start. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-1 Start. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-4 Start. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-3 Start. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-5 Start. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-2 End. Time = Tue Nov 12 20:59:54 CST 2019 -pool-1-thread-3 End. Time = Tue Nov 12 20:59:54 CST 2019 -pool-1-thread-4 End. Time = Tue Nov 12 20:59:54 CST 2019 -pool-1-thread-5 End. Time = Tue Nov 12 20:59:54 CST 2019 -pool-1-thread-1 End. Time = Tue Nov 12 20:59:54 CST 2019 - -``` - -### 4.7 线程池原理分析 - -承接 4.6 节,我们通过代码输出结果可以看出:**线程池每次会同时执行 5 个任务,这 5 个任务执行完之后,剩余的 5 个任务才会被执行。** 大家可以先通过上面讲解的内容,分析一下到底是咋回事?(自己独立思考一会) - -现在,我们就分析上面的输出内容来简单分析一下线程池原理。 - -**为了搞懂线程池的原理,我们需要首先分析一下 `execute`方法。**在 4.6 节中的 Demo 中我们使用 `executor.execute(worker)`来提交一个任务到线程池中去,这个方法非常重要,下面我们来看看它的源码: - -```java - // 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount) - private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); - - private static int workerCountOf(int c) { - return c & CAPACITY; - } - - private final BlockingQueue workQueue; - - public void execute(Runnable command) { - // 如果任务为null,则抛出异常。 - if (command == null) - throw new NullPointerException(); - // ctl 中保存的线程池当前的一些状态信息 - int c = ctl.get(); - - // 下面会涉及到 3 步 操作 - // 1.首先判断当前线程池中之行的任务数量是否小于 corePoolSize - // 如果小于的话,通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。 - if (workerCountOf(c) < corePoolSize) { - if (addWorker(command, true)) - return; - c = ctl.get(); - } - // 2.如果当前之行的任务数量大于等于 corePoolSize 的时候就会走到这里 - // 通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态才会被并且队列可以加入任务,该任务才会被加入进去 - if (isRunning(c) && workQueue.offer(command)) { - int recheck = ctl.get(); - // 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。 - if (!isRunning(recheck) && remove(command)) - reject(command); - // 如果当前线程池为空就新创建一个线程并执行。 - else if (workerCountOf(recheck) == 0) - addWorker(null, false); - } - //3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。 - //如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。 - else if (!addWorker(command, false)) - reject(command); - } -``` - -通过下图可以更好的对上面这 3 步做一个展示,下图是我为了省事直接从网上找到,原地址不明。 - -![图解线程池实现原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/图解线程池实现原理.png) - -现在,让我们在回到 4.6 节我们写的 Demo, 现在应该是不是很容易就可以搞懂它的原理了呢? - -没搞懂的话,也没关系,可以看看我的分析: - -> 我们在代码中模拟了 10 个任务,我们配置的核心线程数为 5 、等待队列容量为 100 ,所以每次只可能存在 5 个任务同时执行,剩下的 5 个任务会被放到等待队列中去。当前的 5 个任务之行完成后,才会之行剩下的 5 个任务。 - -## 5. Atomic 原子类 - -### 5.1. 介绍一下Atomic 原子类 - -Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。 - -所以,所谓原子类说简单点就是具有原子/原子操作特征的类。 - - -并发包 `java.util.concurrent` 的原子类都存放在`java.util.concurrent.atomic`下,如下图所示。 - -![JUC原子类概览](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/JUC原子类概览.png) - -### 5.2. JUC 包中的原子类是哪4类? - -**基本类型** - -使用原子的方式更新基本类型 - -- AtomicInteger:整形原子类 -- AtomicLong:长整型原子类 -- AtomicBoolean:布尔型原子类 - -**数组类型** - -使用原子的方式更新数组里的某个元素 - - -- AtomicIntegerArray:整形数组原子类 -- AtomicLongArray:长整形数组原子类 -- AtomicReferenceArray:引用类型数组原子类 - -**引用类型** - -- AtomicReference:引用类型原子类 -- AtomicStampedReference:原子更新引用类型里的字段原子类 -- AtomicMarkableReference :原子更新带有标记位的引用类型 - -**对象的属性修改类型** - -- AtomicIntegerFieldUpdater:原子更新整形字段的更新器 -- AtomicLongFieldUpdater:原子更新长整形字段的更新器 -- AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。 - - -### 5.3. 讲讲 AtomicInteger 的使用 - - **AtomicInteger 类常用方法** - -```java -public final int get() //获取当前的值 -public final int getAndSet(int newValue)//获取当前的值,并设置新的值 -public final int getAndIncrement()//获取当前的值,并自增 -public final int getAndDecrement() //获取当前的值,并自减 -public final int getAndAdd(int delta) //获取当前的值,并加上预期的值 -boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update) -public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。 -``` - - **AtomicInteger 类的使用示例** - -使用 AtomicInteger 之后,不用对 increment() 方法加锁也可以保证线程安全。 -```java -class AtomicIntegerTest { - private AtomicInteger count = new AtomicInteger(); - //使用AtomicInteger之后,不需要对该方法加锁,也可以实现线程安全。 - public void increment() { - count.incrementAndGet(); - } - - public int getCount() { - return count.get(); - } -} - -``` - -### 5.4. 能不能给我简单介绍一下 AtomicInteger 类的原理 - -AtomicInteger 线程安全原理简单分析 - -AtomicInteger 类的部分源码: - -```java - // setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用) - private static final Unsafe unsafe = Unsafe.getUnsafe(); - private static final long valueOffset; - - static { - try { - valueOffset = unsafe.objectFieldOffset - (AtomicInteger.class.getDeclaredField("value")); - } catch (Exception ex) { throw new Error(ex); } - } - - private volatile int value; -``` - -AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。 - -CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。 - -关于 Atomic 原子类这部分更多内容可以查看我的这篇文章:并发编程面试必备:[JUC 中的 Atomic 原子类总结](https://mp.weixin.qq.com/s/joa-yOiTrYF67bElj8xqvg) - -## 6. AQS - -### 6.1. AQS 介绍 - -AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。 - -![AQS类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/AQS类.png) - -AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。 - -### 6.2. AQS 原理分析 - -AQS 原理这部分参考了部分博客,在5.2节末尾放了链接。 - -> 在面试中被问到并发知识的时候,大多都会被问到“请你说一下自己对于AQS原理的理解”。下面给大家一个示例供大家参加,面试不是背题,大家一定要加入自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来。 - -下面大部分内容其实在AQS类注释上已经给出了,不过是英语看着比较吃力一点,感兴趣的话可以看看源码。 - -#### 6.2.1. AQS 原理概览 - -**AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。** - -> CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。 - -看个AQS(AbstractQueuedSynchronizer)原理图: - - -![AQS原理图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/AQS原理图.png) - -AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。 - -```java -private volatile int state;//共享变量,使用volatile修饰保证线程可见性 -``` - -状态信息通过protected类型的getState,setState,compareAndSetState进行操作 - -```java - -//返回同步状态的当前值 -protected final int getState() { - return state; -} - // 设置同步状态的值 -protected final void setState(int newState) { - state = newState; -} -//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值) -protected final boolean compareAndSetState(int expect, int update) { - return unsafe.compareAndSwapInt(this, stateOffset, expect, update); -} -``` - -#### 6.2.2. AQS 对资源的共享方式 - -**AQS定义两种资源共享方式** - -- **Exclusive**(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁: - - 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁 - - 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的 -- **Share**(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。 - -ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。 - -不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。 - -#### 6.2.3. AQS底层使用了模板方法模式 - -同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用): - -1. 使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放) -2. 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。 - -这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。 - -**AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:** - -```java -isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。 -tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。 -tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。 -tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 -tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。 - -``` - -默认情况下,每个方法都抛出 `UnsupportedOperationException`。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS类中的其他方法都是final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。 - -以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。 - -再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS(Compare and Swap)减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。 - -一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现`tryAcquire-tryRelease`、`tryAcquireShared-tryReleaseShared`中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如`ReentrantReadWriteLock`。 - -推荐两篇 AQS 原理和相关源码分析的文章: - -- http://www.cnblogs.com/waterystone/p/4920797.html -- https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html - -### 6.3. AQS 组件总结 - -- **Semaphore(信号量)-允许多个线程同时访问:** synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。 -- **CountDownLatch (倒计时器):** CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。 -- **CyclicBarrier(循环栅栏):** CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await()方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。 - -## 7 Reference - -- 《深入理解 Java 虚拟机》 -- 《实战 Java 高并发程序设计》 -- 《Java并发编程的艺术》 -- http://www.cnblogs.com/waterystone/p/4920797.html -- https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html -- - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) diff --git a/docs/java/Multithread/JavaConcurrencyBasicsCommonInterviewQuestionsSummary.md b/docs/java/Multithread/JavaConcurrencyBasicsCommonInterviewQuestionsSummary.md deleted file mode 100644 index ca2518e..0000000 --- a/docs/java/Multithread/JavaConcurrencyBasicsCommonInterviewQuestionsSummary.md +++ /dev/null @@ -1,312 +0,0 @@ -点击关注[公众号](#公众号 "公众号")及时获取笔主最新更新文章,并可免费领取本文档配套的《Java 面试突击》以及 Java 工程师必备学习资源。 - - -- [Java 并发基础常见面试题总结](#java-并发基础常见面试题总结) - - [1. 什么是线程和进程?](#1-什么是线程和进程) - - [1.1. 何为进程?](#11-何为进程) - - [1.2. 何为线程?](#12-何为线程) - - [2. 请简要描述线程与进程的关系,区别及优缺点?](#2-请简要描述线程与进程的关系区别及优缺点) - - [2.1. 图解进程和线程的关系](#21-图解进程和线程的关系) - - [2.2. 程序计数器为什么是私有的?](#22-程序计数器为什么是私有的) - - [2.3. 虚拟机栈和本地方法栈为什么是私有的?](#23-虚拟机栈和本地方法栈为什么是私有的) - - [2.4. 一句话简单了解堆和方法区](#24-一句话简单了解堆和方法区) - - [3. 说说并发与并行的区别?](#3-说说并发与并行的区别) - - [4. 为什么要使用多线程呢?](#4-为什么要使用多线程呢) - - [5. 使用多线程可能带来什么问题?](#5-使用多线程可能带来什么问题) - - [6. 说说线程的生命周期和状态?](#6-说说线程的生命周期和状态) - - [7. 什么是上下文切换?](#7-什么是上下文切换) - - [8. 什么是线程死锁?如何避免死锁?](#8-什么是线程死锁如何避免死锁) - - [8.1. 认识线程死锁](#81-认识线程死锁) - - [8.2. 如何避免线程死锁?](#82-如何避免线程死锁) - - [9. 说说 sleep() 方法和 wait() 方法区别和共同点?](#9-说说-sleep-方法和-wait-方法区别和共同点) - - [10. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?](#10-为什么我们调用-start-方法时会执行-run-方法为什么我们不能直接调用-run-方法) - - [公众号](#公众号) - - -# Java 并发基础常见面试题总结 - -## 1. 什么是线程和进程? - -### 1.1. 何为进程? - -进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。 - -在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。 - -如下图所示,在 windows 中通过查看任务管理器的方式,我们就可以清楚看到 window 当前运行的进程(.exe 文件的运行)。 - -![进程示例图片-Windows](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/进程示例图片-Windows.png) - -### 1.2. 何为线程? - -线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的**堆**和**方法区**资源,但每个线程有自己的**程序计数器**、**虚拟机栈**和**本地方法栈**,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。 - -Java 程序天生就是多线程程序,我们可以通过 JMX 来看一下一个普通的 Java 程序有哪些线程,代码如下。 - -```java -public class MultiThread { - public static void main(String[] args) { - // 获取 Java 线程管理 MXBean - ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); - // 不需要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息 - ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false); - // 遍历线程信息,仅打印线程 ID 和线程名称信息 - for (ThreadInfo threadInfo : threadInfos) { - System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName()); - } - } -} -``` - -上述程序输出如下(输出内容可能不同,不用太纠结下面每个线程的作用,只用知道 main 线程执行 main 方法即可): - -``` -[5] Attach Listener //添加事件 -[4] Signal Dispatcher // 分发处理给 JVM 信号的线程 -[3] Finalizer //调用对象 finalize 方法的线程 -[2] Reference Handler //清除 reference 线程 -[1] main //main 线程,程序入口 -``` - -从上面的输出内容可以看出:**一个 Java 程序的运行是 main 线程和多个其他线程同时运行**。 - -## 2. 请简要描述线程与进程的关系,区别及优缺点? - -**从 JVM 角度说进程和线程之间的关系** - -### 2.1. 图解进程和线程的关系 - -下图是 Java 内存区域,通过下图我们从 JVM 的角度来说一下线程和进程之间的关系。如果你对 Java 内存区域 (运行时数据区) 这部分知识不太了解的话可以阅读一下这篇文章:[《可能是把 Java 内存区域讲的最清楚的一篇文章》](https://github.com/Snailclimb/JavaGuide/blob/3965c02cc0f294b0bd3580df4868d5e396959e2e/Java%E7%9B%B8%E5%85%B3/%E5%8F%AF%E8%83%BD%E6%98%AF%E6%8A%8AJava%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F%E8%AE%B2%E7%9A%84%E6%9C%80%E6%B8%85%E6%A5%9A%E7%9A%84%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0.md "《可能是把 Java 内存区域讲的最清楚的一篇文章》") - -
- -
- -从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的**堆**和**方法区 (JDK1.8 之后的元空间)**资源,但是每个线程有自己的**程序计数器**、**虚拟机栈** 和 **本地方法栈**。 - -**总结:** 线程 是 进程 划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反 - -下面是该知识点的扩展内容! - -下面来思考这样一个问题:为什么**程序计数器**、**虚拟机栈**和**本地方法栈**是线程私有的呢?为什么堆和方法区是线程共享的呢? - -### 2.2. 程序计数器为什么是私有的? - -程序计数器主要有下面两个作用: - -1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。 -2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。 - -需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。 - -所以,程序计数器私有主要是为了**线程切换后能恢复到正确的执行位置**。 - -### 2.3. 虚拟机栈和本地方法栈为什么是私有的? - -- **虚拟机栈:** 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。 -- **本地方法栈:** 和虚拟机栈所发挥的作用非常相似,区别是: **虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。** 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。 - -所以,为了**保证线程中的局部变量不被别的线程访问到**,虚拟机栈和本地方法栈是线程私有的。 - -### 2.4. 一句话简单了解堆和方法区 - -堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 - -## 3. 说说并发与并行的区别? - -- **并发:** 同一时间段,多个任务都在执行 (单位时间内不一定同时执行); -- **并行:** 单位时间内,多个任务同时执行。 - -## 4. 为什么要使用多线程呢? - -先从总体上来说: - -- **从计算机底层来说:** 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。 -- **从当代互联网发展趋势来说:** 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。 - -再深入到计算机底层来探讨: - -- **单核时代:** 在单核时代多线程主要是为了提高 CPU 和 IO 设备的综合利用率。举个例子:当只有一个线程的时候会导致 CPU 计算时,IO 设备空闲;进行 IO 操作时,CPU 空闲。我们可以简单地说这两者的利用率目前都是 50%左右。但是当有两个线程的时候就不一样了,当一个线程执行 CPU 计算时,另外一个线程可以进行 IO 操作,这样两个的利用率就可以在理想情况下达到 100%了。 -- **多核时代:** 多核时代多线程主要是为了提高 CPU 利用率。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,CPU 只会一个 CPU 核心被利用到,而创建多个线程就可以让多个 CPU 核心被利用到,这样就提高了 CPU 的利用率。 - -## 5. 使用多线程可能带来什么问题? - -并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、死锁还有受限于硬件和软件的资源闲置问题。 - -## 6. 说说线程的生命周期和状态? - -Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态(图源《Java 并发编程艺术》4.1.4 节)。 - -![Java 线程的状态 ](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/19-1-29/Java%E7%BA%BF%E7%A8%8B%E7%9A%84%E7%8A%B6%E6%80%81.png) - -线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示(图源《Java 并发编程艺术》4.1.4 节): - -![Java 线程状态变迁 ](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/19-1-29/Java+%E7%BA%BF%E7%A8%8B%E7%8A%B6%E6%80%81%E5%8F%98%E8%BF%81.png) - -由上图可以看出:线程创建之后它将处于 **NEW(新建)** 状态,调用 `start()` 方法后开始运行,线程这时候处于 **READY(可运行)** 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 **RUNNING(运行)** 状态。 - -> 操作系统隐藏 Java 虚拟机(JVM)中的 RUNNABLE 和 RUNNING 状态,它只能看到 RUNNABLE 状态(图源:[HowToDoInJava](https://howtodoinjava.com/ "HowToDoInJava"):[Java Thread Life Cycle and Thread States](https://howtodoinjava.com/java/multi-threading/java-thread-life-cycle-and-thread-states/ "Java Thread Life Cycle and Thread States")),所以 Java 系统一般将这两个状态统称为 **RUNNABLE(运行中)** 状态 。 - -![RUNNABLE-VS-RUNNING](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3/RUNNABLE-VS-RUNNING.png) - -当线程执行 `wait()`方法之后,线程进入 **WAITING(等待)** 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 **TIME_WAITING(超时等待)** 状态相当于在等待状态的基础上增加了超时限制,比如通过 `sleep(long millis)`方法或 `wait(long millis)`方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 **BLOCKED(阻塞)** 状态。线程在执行 Runnable 的`run()`方法之后将会进入到 **TERMINATED(终止)** 状态。 - -## 7. 什么是上下文切换? - -多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。 - -概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。**任务从保存到再加载的过程就是一次上下文切换**。 - -上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。 - -Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。 - -## 8. 什么是线程死锁?如何避免死锁? - -### 8.1. 认识线程死锁 - -多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。 - -如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。 - -![线程死锁示意图 ](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-4/2019-4%E6%AD%BB%E9%94%811.png) - -下面通过一个例子来说明线程死锁,代码模拟了上图的死锁的情况 (代码来源于《并发编程之美》): - -```java -public class DeadLockDemo { - private static Object resource1 = new Object();//资源 1 - private static Object resource2 = new Object();//资源 2 - - public static void main(String[] args) { - new Thread(() -> { - synchronized (resource1) { - System.out.println(Thread.currentThread() + "get resource1"); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println(Thread.currentThread() + "waiting get resource2"); - synchronized (resource2) { - System.out.println(Thread.currentThread() + "get resource2"); - } - } - }, "线程 1").start(); - - new Thread(() -> { - synchronized (resource2) { - System.out.println(Thread.currentThread() + "get resource2"); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println(Thread.currentThread() + "waiting get resource1"); - synchronized (resource1) { - System.out.println(Thread.currentThread() + "get resource1"); - } - } - }, "线程 2").start(); - } -} -``` - -Output - -``` -Thread[线程 1,5,main]get resource1 -Thread[线程 2,5,main]get resource2 -Thread[线程 1,5,main]waiting get resource2 -Thread[线程 2,5,main]waiting get resource1 -``` - -线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过`Thread.sleep(1000);`让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。 - -学过操作系统的朋友都知道产生死锁必须具备以下四个条件: - -1. 互斥条件:该资源任意一个时刻只由一个线程占用。 -2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。 -3. 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。 -4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。 - -### 8.2. 如何避免线程死锁? - -我们只要破坏产生死锁的四个条件中的其中一个就可以了。 - -**破坏互斥条件** - -这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。 - -**破坏请求与保持条件** - -一次性申请所有的资源。 - -**破坏不剥夺条件** - -占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。 - -**破坏循环等待条件** - -靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。 - -我们对线程 2 的代码修改成下面这样就不会产生死锁了。 - -```java - new Thread(() -> { - synchronized (resource1) { - System.out.println(Thread.currentThread() + "get resource1"); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println(Thread.currentThread() + "waiting get resource2"); - synchronized (resource2) { - System.out.println(Thread.currentThread() + "get resource2"); - } - } - }, "线程 2").start(); -``` - -Output - -``` -Thread[线程 1,5,main]get resource1 -Thread[线程 1,5,main]waiting get resource2 -Thread[线程 1,5,main]get resource2 -Thread[线程 2,5,main]get resource1 -Thread[线程 2,5,main]waiting get resource2 -Thread[线程 2,5,main]get resource2 - -Process finished with exit code 0 -``` - -我们分析一下上面的代码为什么避免了死锁的发生? - -线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。 - -## 9. 说说 sleep() 方法和 wait() 方法区别和共同点? - -- 两者最主要的区别在于:**sleep 方法没有释放锁,而 wait 方法释放了锁** 。 -- 两者都可以暂停线程的执行。 -- Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。 -- wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒。 - -## 10. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法? - -这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来! - -new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。 - -**总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。** - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java 面试突击》:** 由本文档衍生的专为面试而生的《Java 面试突击》V2.0 PDF 版本[公众号](#公众号 "公众号")后台回复 **"面试突击"** 即可免费领取! - -**Java 工程师必备学习资源:** 一些 Java 工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) diff --git "a/docs/java/Multithread/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/Multithread/ThreadLocal.md b/docs/java/Multithread/ThreadLocal.md deleted file mode 100644 index 06cdbea..0000000 --- a/docs/java/Multithread/ThreadLocal.md +++ /dev/null @@ -1,170 +0,0 @@ -[ThreadLocal造成OOM内存溢出案例演示与原理分析](https://blog.csdn.net/xlgen157387/article/details/78298840) - -[深入理解 Java 之 ThreadLocal 工作原理]() - -## ThreadLocal - -### ThreadLocal简介 - -通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。**如果想实现每一个线程都有自己的专属本地变量该如何解决呢?** JDK中提供的`ThreadLocal`类正是为了解决这样的问题。 **`ThreadLocal`类主要解决的就是让每个线程绑定自己的值,可以将`ThreadLocal`类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。** - -**如果你创建了一个`ThreadLocal`变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是`ThreadLocal`变量名的由来。他们可以使用 `get()` 和 `set()` 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。** - -再举个简单的例子: - -比如有两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么ThreadLocal就是用来这两个线程竞争的。 - -### ThreadLocal示例 - -相信看了上面的解释,大家已经搞懂 ThreadLocal 类是个什么东西了。 - -```java -import java.text.SimpleDateFormat; -import java.util.Random; - -public class ThreadLocalExample implements Runnable{ - - // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本 - private static final ThreadLocal formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm")); - - public static void main(String[] args) throws InterruptedException { - ThreadLocalExample obj = new ThreadLocalExample(); - for(int i=0 ; i<10; i++){ - Thread t = new Thread(obj, ""+i); - Thread.sleep(new Random().nextInt(1000)); - t.start(); - } - } - - @Override - public void run() { - System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern()); - try { - Thread.sleep(new Random().nextInt(1000)); - } catch (InterruptedException e) { - e.printStackTrace(); - } - //formatter pattern is changed here by thread, but it won't reflect to other threads - formatter.set(new SimpleDateFormat()); - - System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern()); - } - -} - -``` - -Output: - -``` -Thread Name= 0 default Formatter = yyyyMMdd HHmm -Thread Name= 0 formatter = yy-M-d ah:mm -Thread Name= 1 default Formatter = yyyyMMdd HHmm -Thread Name= 2 default Formatter = yyyyMMdd HHmm -Thread Name= 1 formatter = yy-M-d ah:mm -Thread Name= 3 default Formatter = yyyyMMdd HHmm -Thread Name= 2 formatter = yy-M-d ah:mm -Thread Name= 4 default Formatter = yyyyMMdd HHmm -Thread Name= 3 formatter = yy-M-d ah:mm -Thread Name= 4 formatter = yy-M-d ah:mm -Thread Name= 5 default Formatter = yyyyMMdd HHmm -Thread Name= 5 formatter = yy-M-d ah:mm -Thread Name= 6 default Formatter = yyyyMMdd HHmm -Thread Name= 6 formatter = yy-M-d ah:mm -Thread Name= 7 default Formatter = yyyyMMdd HHmm -Thread Name= 7 formatter = yy-M-d ah:mm -Thread Name= 8 default Formatter = yyyyMMdd HHmm -Thread Name= 9 default Formatter = yyyyMMdd HHmm -Thread Name= 8 formatter = yy-M-d ah:mm -Thread Name= 9 formatter = yy-M-d ah:mm -``` - -从输出中可以看出,Thread-0已经改变了formatter的值,但仍然是thread-2默认格式化程序与初始化值相同,其他线程也一样。 - -上面有一段代码用到了创建 `ThreadLocal` 变量的那段代码用到了 Java8 的知识,它等于下面这段代码,如果你写了下面这段代码的话,IDEA会提示你转换为Java8的格式(IDEA真的不错!)。因为ThreadLocal类在Java 8中扩展,使用一个新的方法`withInitial()`,将Supplier功能接口作为参数。 - -```java - private static final ThreadLocal formatter = new ThreadLocal(){ - @Override - protected SimpleDateFormat initialValue() - { - return new SimpleDateFormat("yyyyMMdd HHmm"); - } - }; -``` - -### ThreadLocal原理 - -从 `Thread`类源代码入手。 - -```java -public class Thread implements Runnable { - ...... -//与此线程有关的ThreadLocal值。由ThreadLocal类维护 -ThreadLocal.ThreadLocalMap threadLocals = null; - -//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护 -ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; - ...... -} -``` - -从上面`Thread`类 源代码可以看出`Thread` 类中有一个 `threadLocals` 和 一个 `inheritableThreadLocals` 变量,它们都是 `ThreadLocalMap` 类型的变量,我们可以把 `ThreadLocalMap` 理解为`ThreadLocal` 类实现的定制化的 `HashMap`。默认情况下这两个变量都是null,只有当前线程调用 `ThreadLocal` 类的 `set`或`get`方法时才创建它们,实际上调用这两个方法的时候,我们调用的是`ThreadLocalMap`类对应的 `get()`、`set() `方法。 - -`ThreadLocal`类的`set()`方法 - -```java - public void set(T value) { - Thread t = Thread.currentThread(); - ThreadLocalMap map = getMap(t); - if (map != null) - map.set(this, value); - else - createMap(t, value); - } - ThreadLocalMap getMap(Thread t) { - return t.threadLocals; - } -``` - -通过上面这些内容,我们足以通过猜测得出结论:**最终的变量是放在了当前线程的 `ThreadLocalMap` 中,并不是存在 `ThreadLocal` 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。** - -**每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为key的键值对。** 比如我们在同一个线程中声明了两个 `ThreadLocal` 对象的话,会使用 `Thread`内部都是使用仅有那个`ThreadLocalMap` 存放数据的,`ThreadLocalMap`的 key 就是 `ThreadLocal`对象,value 就是 `ThreadLocal` 对象调用`set`方法设置的值。`ThreadLocal` 是 map结构是为了让每个线程可以关联多个 `ThreadLocal`变量。这也就解释了ThreadLocal声明的变量为什么在每一个线程都有自己的专属本地变量。 - -```java -public class Thread implements Runnable { - ...... -//与此线程有关的ThreadLocal值。由ThreadLocal类维护 -ThreadLocal.ThreadLocalMap threadLocals = null; - -//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护 -ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; - ...... -} -``` - -`ThreadLocalMap`是`ThreadLocal`的静态内部类。 - -![ThreadLocal内部类](https://ws1.sinaimg.cn/large/006rNwoDgy1g2f47u9li2j30ka08cq43.jpg) - -### ThreadLocal 内存泄露问题 - -`ThreadLocalMap` 中使用的 key 为 `ThreadLocal` 的弱引用,而 value 是强引用。所以,如果 `ThreadLocal` 没有被外部强引用的情况下,在垃圾回收的时候会 key 会被清理掉,而 value 不会被清理掉。这样一来,`ThreadLocalMap` 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 `set()`、`get()`、`remove()` 方法的时候,会清理掉 key 为 null 的记录。使用完 `ThreadLocal`方法后 最好手动调用`remove()`方法 - -```java - static class Entry extends WeakReference> { - /** The value associated with this ThreadLocal. */ - Object value; - - Entry(ThreadLocal k, Object v) { - super(k); - value = v; - } - } -``` - -**弱引用介绍:** - -> 如果一个对象只具有弱引用,那就类似于**可有可无的生活用品**。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。 -> -> 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。 diff --git "a/docs/java/Multithread/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223.md" "b/docs/java/Multithread/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223.md" deleted file mode 100644 index bfbe99d..0000000 --- "a/docs/java/Multithread/java\347\272\277\347\250\213\346\261\240\345\255\246\344\271\240\346\200\273\347\273\223.md" +++ /dev/null @@ -1,781 +0,0 @@ - - -- [一 使用线程池的好处](#一-使用线程池的好处) -- [二 Executor 框架](#二-executor-框架) - - [2.1 简介](#21-简介) - - [2.2 Executor 框架结构(主要由三大部分组成)](#22-executor-框架结构主要由三大部分组成) - - [1) 任务(`Runnable` /`Callable`)](#1-任务runnable-callable) - - [2) 任务的执行(`Executor`)](#2-任务的执行executor) - - [3) 异步计算的结果(`Future`)](#3-异步计算的结果future) - - [2.3 Executor 框架的使用示意图](#23-executor-框架的使用示意图) -- [三 (重要)ThreadPoolExecutor 类简单介绍](#三-重要threadpoolexecutor-类简单介绍) - - [3.1 ThreadPoolExecutor 类分析](#31-threadpoolexecutor-类分析) - - [3.2 推荐使用 `ThreadPoolExecutor` 构造函数创建线程池](#32-推荐使用-threadpoolexecutor-构造函数创建线程池) -- [四 (重要)ThreadPoolExecutor 使用示例](#四-重要threadpoolexecutor-使用示例) - - [4.1 示例代码:`Runnable`+`ThreadPoolExecutor`](#41-示例代码runnablethreadpoolexecutor) - - [4.2 线程池原理分析](#42-线程池原理分析) - - [4.3 几个常见的对比](#43-几个常见的对比) - - [4.3.1 `Runnable` vs `Callable`](#431-runnable-vs-callable) - - [4.3.2 `execute()` vs `submit()`](#432-execute-vs-submit) - - [4.3.3 `shutdown()`VS`shutdownNow()`](#433-shutdownvsshutdownnow) - - [4.3.2 `isTerminated()` VS `isShutdown()`](#432-isterminated-vs-isshutdown) - - [4.4 加餐:`Callable`+`ThreadPoolExecutor`示例代码](#44-加餐callablethreadpoolexecutor示例代码) -- [五 几种常见的线程池详解](#五-几种常见的线程池详解) - - [5.1 FixedThreadPool](#51-fixedthreadpool) - - [5.1.1 介绍](#511-介绍) - - [5.1.2 执行任务过程介绍](#512-执行任务过程介绍) - - [5.1.3 为什么不推荐使用`FixedThreadPool`?](#513-为什么不推荐使用fixedthreadpool) - - [5.2 SingleThreadExecutor 详解](#52-singlethreadexecutor-详解) - - [5.2.1 介绍](#521-介绍) - - [5.2.2 执行任务过程介绍](#522-执行任务过程介绍) - - [5.2.3 为什么不推荐使用`FixedThreadPool`?](#523-为什么不推荐使用fixedthreadpool) - - [5.3 CachedThreadPool 详解](#53-cachedthreadpool-详解) - - [5.3.1 介绍](#531-介绍) - - [5.3.2 执行任务过程介绍](#532-执行任务过程介绍) - - [5.3.3 为什么不推荐使用`CachedThreadPool`?](#533-为什么不推荐使用cachedthreadpool) -- [六 ScheduledThreadPoolExecutor 详解](#六-scheduledthreadpoolexecutor-详解) - - [6.1 简介](#61-简介) - - [6.2 运行机制](#62-运行机制) - - [6.3 ScheduledThreadPoolExecutor 执行周期任务的步骤](#63-scheduledthreadpoolexecutor-执行周期任务的步骤) -- [七 线程池大小确定](#七-线程池大小确定) -- [八 参考](#八-参考) -- [九 其他推荐阅读](#九-其他推荐阅读) - - - -## 一 使用线程池的好处 - -> **池化技术相比大家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。** - -**线程池**提供了一种限制和管理资源(包括执行一个任务)。 每个**线程池**还维护一些基本统计信息,例如已完成任务的数量。 - -这里借用《Java 并发编程的艺术》提到的来说一下**使用线程池的好处**: - -- **降低资源消耗**。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 -- **提高响应速度**。当任务到达时,任务可以不需要的等到线程创建就能立即执行。 -- **提高线程的可管理性**。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 - -## 二 Executor 框架 - -### 2.1 简介 - -Executor 框架是 Java5 之后引进的,在 Java 5 之后,通过 Executor 来启动线程比使用 Thread 的 start 方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免 this 逃逸问题。 - -> 补充:this 逃逸是指在构造函数返回之前其他线程就持有该对象的引用. 调用尚未构造完全的对象的方法可能引发令人疑惑的错误。 - -Executor 框架不仅包括了线程池的管理,还提供了线程工厂、队列以及拒绝策略等,Executor 框架让并发编程变得更加简单。 - -### 2.2 Executor 框架结构(主要由三大部分组成) - -#### 1) 任务(`Runnable` /`Callable`) - -执行任务需要实现的 **`Runnable` 接口** 或 **`Callable`接口**。**`Runnable` 接口**或 **`Callable` 接口** 实现类都可以被 **`ThreadPoolExecutor`** 或 **`ScheduledThreadPoolExecutor`** 执行。 - -#### 2) 任务的执行(`Executor`) - -如下图所示,包括任务执行机制的核心接口 **`Executor`** ,以及继承自 `Executor` 接口的 **`ExecutorService` 接口。`ThreadPoolExecutor`** 和 **`ScheduledThreadPoolExecutor`** 这两个关键类实现了 **ExecutorService 接口**。 - -**这里提了很多底层的类关系,但是,实际上我们需要更多关注的是 `ThreadPoolExecutor` 这个类,这个类在我们实际使用线程池的过程中,使用频率还是非常高的。** - -> **注意:** 通过查看 `ScheduledThreadPoolExecutor` 源代码我们发现 `ScheduledThreadPoolExecutor` 实际上是继承了 `ThreadPoolExecutor` 并实现了 ScheduledExecutorService ,而 `ScheduledExecutorService` 又实现了 `ExecutorService`,正如我们下面给出的类关系图显示的一样。 - -**`ThreadPoolExecutor` 类描述:** - -```java -//AbstractExecutorService实现了ExecutorService接口 -public class ThreadPoolExecutor extends AbstractExecutorService -``` - -**`ScheduledThreadPoolExecutor` 类描述:** - -```java -//ScheduledExecutorService实现了ExecutorService接口 -public class ScheduledThreadPoolExecutor - extends ThreadPoolExecutor - implements ScheduledExecutorService -``` - -![任务的执行相关接口](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/任务的执行相关接口.png) - -#### 3) 异步计算的结果(`Future`) - -**`Future`** 接口以及 `Future` 接口的实现类 **`FutureTask`** 类都可以代表异步计算的结果。 - -当我们把 **`Runnable`接口** 或 **`Callable` 接口** 的实现类提交给 **`ThreadPoolExecutor`** 或 **`ScheduledThreadPoolExecutor`** 执行。(调用 `submit()` 方法时会返回一个 **`FutureTask`** 对象) - -### 2.3 Executor 框架的使用示意图 - -![Executor 框架的使用示意图](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC01LTMwLzg0ODIzMzMwLmpwZw?x-oss-process=image/format,png) - -1. **主线程首先要创建实现 `Runnable` 或者 `Callable` 接口的任务对象。** -2. **把创建完成的实现 `Runnable`/`Callable`接口的 对象直接交给 `ExecutorService` 执行**: `ExecutorService.execute(Runnable command)`)或者也可以把 `Runnable` 对象或`Callable` 对象提交给 `ExecutorService` 执行(`ExecutorService.submit(Runnable task)`或 `ExecutorService.submit(Callable task)`)。 -3. **如果执行 `ExecutorService.submit(…)`,`ExecutorService` 将返回一个实现`Future`接口的对象**(我们刚刚也提到过了执行 `execute()`方法和 `submit()`方法的区别,`submit()`会返回一个 `FutureTask 对象)。由于 FutureTask` 实现了 `Runnable`,我们也可以创建 `FutureTask`,然后直接交给 `ExecutorService` 执行。 -4. **最后,主线程可以执行 `FutureTask.get()`方法来等待任务执行完成。主线程也可以执行 `FutureTask.cancel(boolean mayInterruptIfRunning)`来取消此任务的执行。** - -## 三 (重要)ThreadPoolExecutor 类简单介绍 - -**线程池实现类 `ThreadPoolExecutor` 是 `Executor` 框架最核心的类。** - -### 3.1 ThreadPoolExecutor 类分析 - -`ThreadPoolExecutor` 类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法的基础上产生(其他几个构造方法说白点都是给定某些默认参数的构造方法比如默认制定拒绝策略是什么),这里就不贴代码讲了,比较简单。 - -```java - /** - * 用给定的初始参数创建一个新的ThreadPoolExecutor。 - */ - public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量 - int maximumPoolSize,//线程池的最大线程数 - long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间 - TimeUnit unit,//时间单位 - BlockingQueue workQueue,//任务队列,用来储存等待执行任务的队列 - ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可 - RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务 - ) { - if (corePoolSize < 0 || - maximumPoolSize <= 0 || - maximumPoolSize < corePoolSize || - keepAliveTime < 0) - throw new IllegalArgumentException(); - if (workQueue == null || threadFactory == null || handler == null) - throw new NullPointerException(); - this.corePoolSize = corePoolSize; - this.maximumPoolSize = maximumPoolSize; - this.workQueue = workQueue; - this.keepAliveTime = unit.toNanos(keepAliveTime); - this.threadFactory = threadFactory; - this.handler = handler; - } -``` - -**下面这些对创建 非常重要,在后面使用线程池的过程中你一定会用到!所以,务必拿着小本本记清楚。** - -**`ThreadPoolExecutor` 3 个最重要的参数:** - -- **`corePoolSize` :** 核心线程数线程数定义了最小可以同时运行的线程数量。 -- **`maximumPoolSize` :** 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 -- **`workQueue`:** 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,信任就会被存放在队列中。 - -`ThreadPoolExecutor`其他常见参数: - -1. **`keepAliveTime`**:当线程池中的线程数量大于 `corePoolSize` 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 `keepAliveTime`才会被回收销毁; -2. **`unit`** : `keepAliveTime` 参数的时间单位。 -3. **`threadFactory`** :executor 创建新线程的时候会用到。 -4. **`handler`** :饱和策略。关于饱和策略下面单独介绍一下。 - -下面这张图可以加深你对线程池中各个参数的相互关系的理解(图片来源:《Java性能调优实战》): - -![线程池各个参数的关系](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/线程池各个参数的关系.jpg) - -**`ThreadPoolExecutor` 饱和策略定义:** - -如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,`ThreadPoolTaskExecutor` 定义一些策略: - -- **`ThreadPoolExecutor.AbortPolicy`**:抛出 `RejectedExecutionException`来拒绝新任务的处理。 -- **`ThreadPoolExecutor.CallerRunsPolicy`**:调用执行自己的线程运行任务,也就是直接在调用`execute`方法的线程中运行(`run`)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。 -- **`ThreadPoolExecutor.DiscardPolicy`:** 不处理新任务,直接丢弃掉。 -- **`ThreadPoolExecutor.DiscardOldestPolicy`:** 此策略将丢弃最早的未处理的任务请求。 - -举个例子: - -> Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecutor` 的构造函数创建线程池的时候,当我们不指定 `RejectedExecutionHandler` 饱和策略的话来配置线程池的时候默认使用的是 `ThreadPoolExecutor.AbortPolicy`。在默认情况下,`ThreadPoolExecutor` 将抛出 `RejectedExecutionException` 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 `ThreadPoolExecutor.CallerRunsPolicy`。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 `ThreadPoolExecutor` 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了。) - -### 3.2 推荐使用 `ThreadPoolExecutor` 构造函数创建线程池 - -**在《阿里巴巴 Java 开发手册》“并发处理”这一章节,明确指出线程资源必须通过线程池提供,不允许在应用中自行显示创建线程。** - -**为什么呢?** - -> **使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题。如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。** - -**另外《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险** - -> Executors 返回线程池对象的弊端如下: -> -> - **`FixedThreadPool` 和 `SingleThreadExecutor`** : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。 -> - **CachedThreadPool 和 ScheduledThreadPool** : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。 - -**方式一:通过`ThreadPoolExecutor`构造函数实现(推荐)** -![通过构造方法实现](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC00LTE2LzE3ODU4MjMwLmpwZw?x-oss-process=image/format,png) -**方式二:通过 Executor 框架的工具类 Executors 来实现** -我们可以创建三种类型的 ThreadPoolExecutor: - -- **FixedThreadPool** -- **SingleThreadExecutor** -- **CachedThreadPool** - -对应 Executors 工具类中的方法如图所示: -![通过Executor 框架的工具类Executors来实现](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC00LTE2LzEzMjk2OTAxLmpwZw?x-oss-process=image/format,png) - -## 四 (重要)ThreadPoolExecutor 使用示例 - -我们上面讲解了 `Executor`框架以及 `ThreadPoolExecutor` 类,下面让我们实战一下,来通过写一个 `ThreadPoolExecutor` 的小 Demo 来回顾上面的内容。 - -### 4.1 示例代码:`Runnable`+`ThreadPoolExecutor` - -首先创建一个 `Runnable` 接口的实现类(当然也可以是 `Callable` 接口,我们上面也说了两者的区别。) - -`MyRunnable.java` - -```java -import java.util.Date; - -/** - * 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。 - * @author shuang.kou - */ -public class MyRunnable implements Runnable { - - private String command; - - public MyRunnable(String s) { - this.command = s; - } - - @Override - public void run() { - System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date()); - processCommand(); - System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date()); - } - - private void processCommand() { - try { - Thread.sleep(5000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - @Override - public String toString() { - return this.command; - } -} - -``` - -编写测试程序,我们这里以阿里巴巴推荐的使用 `ThreadPoolExecutor` 构造函数自定义参数的方式来创建线程池。 - -`ThreadPoolExecutorDemo.java` - -```java -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -public class ThreadPoolExecutorDemo { - - private static final int CORE_POOL_SIZE = 5; - private static final int MAX_POOL_SIZE = 10; - private static final int QUEUE_CAPACITY = 100; - private static final Long KEEP_ALIVE_TIME = 1L; - public static void main(String[] args) { - - //使用阿里巴巴推荐的创建线程池的方式 - //通过ThreadPoolExecutor构造函数自定义参数创建 - ThreadPoolExecutor executor = new ThreadPoolExecutor( - CORE_POOL_SIZE, - MAX_POOL_SIZE, - KEEP_ALIVE_TIME, - TimeUnit.SECONDS, - new ArrayBlockingQueue<>(QUEUE_CAPACITY), - new ThreadPoolExecutor.CallerRunsPolicy()); - - for (int i = 0; i < 10; i++) { - //创建WorkerThread对象(WorkerThread类实现了Runnable 接口) - Runnable worker = new MyRunnable("" + i); - //执行Runnable - executor.execute(worker); - } - //终止线程池 - executor.shutdown(); - while (!executor.isTerminated()) { - } - System.out.println("Finished all threads"); - } -} - -``` - -可以看到我们上面的代码指定了: - -1. `corePoolSize`: 核心线程数为 5。 -2. `maximumPoolSize` :最大线程数 10 -3. `keepAliveTime` : 等待时间为 1L。 -4. `unit`: 等待时间的单位为 TimeUnit.SECONDS。 -5. `workQueue`:任务队列为 `ArrayBlockingQueue`,并且容量为 100; -6. `handler`:饱和策略为 `CallerRunsPolicy`。 - -**Output:** - -``` -pool-1-thread-2 Start. Time = Tue Nov 12 20:59:44 CST 2019 -pool-1-thread-5 Start. Time = Tue Nov 12 20:59:44 CST 2019 -pool-1-thread-4 Start. Time = Tue Nov 12 20:59:44 CST 2019 -pool-1-thread-1 Start. Time = Tue Nov 12 20:59:44 CST 2019 -pool-1-thread-3 Start. Time = Tue Nov 12 20:59:44 CST 2019 -pool-1-thread-5 End. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-3 End. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-2 End. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-4 End. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-1 End. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-2 Start. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-1 Start. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-4 Start. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-3 Start. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-5 Start. Time = Tue Nov 12 20:59:49 CST 2019 -pool-1-thread-2 End. Time = Tue Nov 12 20:59:54 CST 2019 -pool-1-thread-3 End. Time = Tue Nov 12 20:59:54 CST 2019 -pool-1-thread-4 End. Time = Tue Nov 12 20:59:54 CST 2019 -pool-1-thread-5 End. Time = Tue Nov 12 20:59:54 CST 2019 -pool-1-thread-1 End. Time = Tue Nov 12 20:59:54 CST 2019 - -``` - -### 4.2 线程池原理分析 - -承接 4.1 节,我们通过代码输出结果可以看出:**线程池每次会同时执行 5 个任务,这 5 个任务执行完之后,剩余的 5 个任务才会被执行。** 大家可以先通过上面讲解的内容,分析一下到底是咋回事?(自己独立思考一会) - -现在,我们就分析上面的输出内容来简单分析一下线程池原理。 - -**为了搞懂线程池的原理,我们需要首先分析一下 `execute`方法。**在 4.1 节中的 Demo 中我们使用 `executor.execute(worker)`来提交一个任务到线程池中去,这个方法非常重要,下面我们来看看它的源码: - -```java - // 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount) - private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); - - private static int workerCountOf(int c) { - return c & CAPACITY; - } - - private final BlockingQueue workQueue; - - public void execute(Runnable command) { - // 如果任务为null,则抛出异常。 - if (command == null) - throw new NullPointerException(); - // ctl 中保存的线程池当前的一些状态信息 - int c = ctl.get(); - - // 下面会涉及到 3 步 操作 - // 1.首先判断当前线程池中之行的任务数量是否小于 corePoolSize - // 如果小于的话,通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。 - if (workerCountOf(c) < corePoolSize) { - if (addWorker(command, true)) - return; - c = ctl.get(); - } - // 2.如果当前之行的任务数量大于等于 corePoolSize 的时候就会走到这里 - // 通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态才会被并且队列可以加入任务,该任务才会被加入进去 - if (isRunning(c) && workQueue.offer(command)) { - int recheck = ctl.get(); - // 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。 - if (!isRunning(recheck) && remove(command)) - reject(command); - // 如果当前线程池为空就新创建一个线程并执行。 - else if (workerCountOf(recheck) == 0) - addWorker(null, false); - } - //3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。 - //如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。 - else if (!addWorker(command, false)) - reject(command); - } -``` - -通过下图可以更好的对上面这 3 步做一个展示,下图是我为了省事直接从网上找到,原地址不明。 - -![图解线程池实现原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/图解线程池实现原理.png) - -现在,让我们在回到 4.1 节我们写的 Demo, 现在应该是不是很容易就可以搞懂它的原理了呢? - -没搞懂的话,也没关系,可以看看我的分析: - -> 我们在代码中模拟了 10 个任务,我们配置的核心线程数为 5 、等待队列容量为 100 ,所以每次只可能存在 5 个任务同时执行,剩下的 5 个任务会被放到等待队列中去。当前的 5 个任务之行完成后,才会之行剩下的 5 个任务。 - -### 4.3 几个常见的对比 - -#### 4.3.1 `Runnable` vs `Callable` - -`Runnable`自 Java 1.0 以来一直存在,但`Callable`仅在 Java 1.5 中引入,目的就是为了来处理`Runnable`不支持的用例。**`Runnable` 接口**不会返回结果或抛出检查异常,但是**`Callable` 接口**可以。所以,如果任务不需要返回结果或抛出异常推荐使用 **`Runnable` 接口**,这样代码看起来会更加简洁。 - -工具类 `Executors` 可以实现 `Runnable` 对象和 `Callable` 对象之间的相互转换。(`Executors.callable(Runnable task`)或 `Executors.callable(Runnable task,Object resule)`)。 - -`Runnable.java` - -```java -@FunctionalInterface -public interface Runnable { - /** - * 被线程执行,没有返回值也无法抛出异常 - */ - public abstract void run(); -} -``` - -`Callable.java` - -```java -@FunctionalInterface -public interface Callable { - /** - * 计算结果,或在无法这样做时抛出异常。 - * @return 计算得出的结果 - * @throws 如果无法计算结果,则抛出异常 - */ - V call() throws Exception; -} - -``` - -#### 4.3.2 `execute()` vs `submit()` - -1. **`execute()`方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;** -2. **`submit()`方法用于提交需要返回值的任务。线程池会返回一个 `Future` 类型的对象,通过这个 `Future` 对象可以判断任务是否执行成功**,并且可以通过 `Future` 的 `get()`方法来获取返回值,`get()`方法会阻塞当前线程直到任务完成,而使用 `get(long timeout,TimeUnit unit)`方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。 - -我们以**`AbstractExecutorService`**接口中的一个 `submit` 方法为例子来看看源代码: - -```java - public Future submit(Runnable task) { - if (task == null) throw new NullPointerException(); - RunnableFuture ftask = newTaskFor(task, null); - execute(ftask); - return ftask; - } -``` - -上面方法调用的 `newTaskFor` 方法返回了一个 `FutureTask` 对象。 - -```java - protected RunnableFuture newTaskFor(Runnable runnable, T value) { - return new FutureTask(runnable, value); - } -``` - -我们再来看看`execute()`方法: - -```java - public void execute(Runnable command) { - ... - } -``` - -#### 4.3.3 `shutdown()`VS`shutdownNow()` - -- **`shutdown()`** :关闭线程池,线程池的状态变为 `SHUTDOWN`。线程池不再接受新任务了,但是队列里的任务得执行完毕。 -- **`shutdownNow()`** :关闭线程池,线程的状态变为 `STOP`。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。 - -#### 4.3.2 `isTerminated()` VS `isShutdown()` - -- **`isShutDown`** 当调用 `shutdown()` 方法后返回为 true。 -- **`isTerminated`** 当调用 `shutdown()` 方法后,并且所有提交的任务完成后返回为 true - -### 4.4 加餐:`Callable`+`ThreadPoolExecutor`示例代码 - -`MyCallable.java` - -```java - -import java.util.concurrent.Callable; - -public class MyCallable implements Callable { - @Override - public String call() throws Exception { - Thread.sleep(1000); - //返回执行当前 Callable 的线程名字 - return Thread.currentThread().getName(); - } -} -``` - -`CallableDemo.java` - -```java - -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -public class CallableDemo { - - private static final int CORE_POOL_SIZE = 5; - private static final int MAX_POOL_SIZE = 10; - private static final int QUEUE_CAPACITY = 100; - private static final Long KEEP_ALIVE_TIME = 1L; - - public static void main(String[] args) { - - //使用阿里巴巴推荐的创建线程池的方式 - //通过ThreadPoolExecutor构造函数自定义参数创建 - ThreadPoolExecutor executor = new ThreadPoolExecutor( - CORE_POOL_SIZE, - MAX_POOL_SIZE, - KEEP_ALIVE_TIME, - TimeUnit.SECONDS, - new ArrayBlockingQueue<>(QUEUE_CAPACITY), - new ThreadPoolExecutor.CallerRunsPolicy()); - - List> futureList = new ArrayList<>(); - Callable callable = new MyCallable(); - for (int i = 0; i < 10; i++) { - //提交任务到线程池 - Future future = executor.submit(callable); - //将返回值 future 添加到 list,我们可以通过 future 获得 执行 Callable 得到的返回值 - futureList.add(future); - } - for (Future fut : futureList) { - try { - System.out.println(new Date() + "::" + fut.get()); - } catch (InterruptedException | ExecutionException e) { - e.printStackTrace(); - } - } - //关闭线程池 - executor.shutdown(); - } -} -``` - -Output: - -``` -Wed Nov 13 13:40:41 CST 2019::pool-1-thread-1 -Wed Nov 13 13:40:42 CST 2019::pool-1-thread-2 -Wed Nov 13 13:40:42 CST 2019::pool-1-thread-3 -Wed Nov 13 13:40:42 CST 2019::pool-1-thread-4 -Wed Nov 13 13:40:42 CST 2019::pool-1-thread-5 -Wed Nov 13 13:40:42 CST 2019::pool-1-thread-3 -Wed Nov 13 13:40:43 CST 2019::pool-1-thread-2 -Wed Nov 13 13:40:43 CST 2019::pool-1-thread-1 -Wed Nov 13 13:40:43 CST 2019::pool-1-thread-4 -Wed Nov 13 13:40:43 CST 2019::pool-1-thread-5 -``` - -## 五 几种常见的线程池详解 - -### 5.1 FixedThreadPool - -#### 5.1.1 介绍 - -`FixedThreadPool` 被称为可重用固定线程数的线程池。通过 Executors 类中的相关源代码来看一下相关实现: - -```java - /** - * 创建一个可重用固定数量线程的线程池 - */ - public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) { - return new ThreadPoolExecutor(nThreads, nThreads, - 0L, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue(), - threadFactory); - } -``` - -另外还有一个 `FixedThreadPool` 的实现方法,和上面的类似,所以这里不多做阐述: - -```java - public static ExecutorService newFixedThreadPool(int nThreads) { - return new ThreadPoolExecutor(nThreads, nThreads, - 0L, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue()); - } -``` - -**从上面源代码可以看出新创建的 `FixedThreadPool` 的 `corePoolSize` 和 `maximumPoolSize` 都被设置为 nThreads,这个 nThreads 参数是我们使用的时候自己传递的。** - -#### 5.1.2 执行任务过程介绍 - -`FixedThreadPool` 的 `execute()` 方法运行示意图(该图片来源:《Java 并发编程的艺术》): - -![FixedThreadPool的execute()方法运行示意图](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC00LTE2LzcxMzc1OTYzLmpwZw?x-oss-process=image/format,png) - -**上图说明:** - -1. 如果当前运行的线程数小于 corePoolSize, 如果再来新任务的话,就创建新的线程来执行任务; -2. 当前运行的线程数等于 corePoolSize 后, 如果再来新任务的话,会将任务加入 `LinkedBlockingQueue`; -3. 线程池中的线程执行完 手头的任务后,会在循环中反复从 `LinkedBlockingQueue` 中获取任务来执行; - -#### 5.1.3 为什么不推荐使用`FixedThreadPool`? - -**`FixedThreadPool` 使用无界队列 `LinkedBlockingQueue`(队列的容量为 Intger.MAX_VALUE)作为线程池的工作队列会对线程池带来如下影响 :** - -1. 当线程池中的线程数达到 `corePoolSize` 后,新任务将在无界队列中等待,因此线程池中的线程数不会超过 corePoolSize; -2. 由于使用无界队列时 `maximumPoolSize` 将是一个无效参数,因为不可能存在任务队列满的情况。所以,通过创建 `FixedThreadPool`的源码可以看出创建的 `FixedThreadPool` 的 `corePoolSize` 和 `maximumPoolSize` 被设置为同一个值。 -3. 由于 1 和 2,使用无界队列时 `keepAliveTime` 将是一个无效参数; -4. 运行中的 `FixedThreadPool`(未执行 `shutdown()`或 `shutdownNow()`)不会拒绝任务,在任务比较多的时候会导致 OOM(内存溢出)。 - -### 5.2 SingleThreadExecutor 详解 - -#### 5.2.1 介绍 - -`SingleThreadExecutor` 是只有一个线程的线程池。下面看看**SingleThreadExecutor 的实现:** - -```java - /** - *返回只有一个线程的线程池 - */ - public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) { - return new FinalizableDelegatedExecutorService - (new ThreadPoolExecutor(1, 1, - 0L, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue(), - threadFactory)); - } -``` - -```java - public static ExecutorService newSingleThreadExecutor() { - return new FinalizableDelegatedExecutorService - (new ThreadPoolExecutor(1, 1, - 0L, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue())); - } -``` - -从上面源代码可以看出新创建的 `SingleThreadExecutor` 的 `corePoolSize` 和 `maximumPoolSize` 都被设置为 1.其他参数和 `FixedThreadPool` 相同。 - -#### 5.2.2 执行任务过程介绍 - -**`SingleThreadExecutor` 的运行示意图(该图片来源:《Java 并发编程的艺术》):** -![SingleThreadExecutor的运行示意图](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC00LTE2LzgyMjc2NDU4LmpwZw?x-oss-process=image/format,png) - -**上图说明;** - -1. 如果当前运行的线程数少于 corePoolSize,则创建一个新的线程执行任务; -2. 当前线程池中有一个运行的线程后,将任务加入 `LinkedBlockingQueue` -3. 线程执行完当前的任务后,会在循环中反复从` LinkedBlockingQueue` 中获取任务来执行; - -#### 5.2.3 为什么不推荐使用`SingleThreadExecutor`? - -`SingleThreadExecutor` 使用无界队列 `LinkedBlockingQueue` 作为线程池的工作队列(队列的容量为 Intger.MAX_VALUE)。`SingleThreadExecutor` 使用无界队列作为线程池的工作队列会对线程池带来的影响与 `FixedThreadPool` 相同。说简单点就是可能会导致 OOM, - -### 5.3 CachedThreadPool 详解 - -#### 5.3.1 介绍 - -`CachedThreadPool` 是一个会根据需要创建新线程的线程池。下面通过源码来看看 `CachedThreadPool` 的实现: - -```java - /** - * 创建一个线程池,根据需要创建新线程,但会在先前构建的线程可用时重用它。 - */ - public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) { - return new ThreadPoolExecutor(0, Integer.MAX_VALUE, - 60L, TimeUnit.SECONDS, - new SynchronousQueue(), - threadFactory); - } - -``` - -```java - public static ExecutorService newCachedThreadPool() { - return new ThreadPoolExecutor(0, Integer.MAX_VALUE, - 60L, TimeUnit.SECONDS, - new SynchronousQueue()); - } -``` - -`CachedThreadPool` 的` corePoolSize` 被设置为空(0),`maximumPoolSize `被设置为 Integer.MAX.VALUE,即它是无界的,这也就意味着如果主线程提交任务的速度高于 `maximumPool` 中线程处理任务的速度时,`CachedThreadPool` 会不断创建新的线程。极端情况下,这样会导致耗尽 cpu 和内存资源。 - -#### 5.3.2 执行任务过程介绍 - -**CachedThreadPool 的 execute()方法的执行示意图(该图片来源:《Java 并发编程的艺术》):** -![CachedThreadPool的execute()方法的执行示意图](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC00LTE2LzE4NjExNzY3LmpwZw?x-oss-process=image/format,png) - -**上图说明:** - -1. 首先执行 `SynchronousQueue.offer(Runnable task)` 提交任务到任务队列。如果当前 `maximumPool` 中有闲线程正在执行 `SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)`,那么主线程执行 offer 操作与空闲线程执行的 `poll` 操作配对成功,主线程把任务交给空闲线程执行,`execute()`方法执行完成,否则执行下面的步骤 2; -2. 当初始 `maximumPool` 为空,或者 `maximumPool` 中没有空闲线程时,将没有线程执行 `SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)`。这种情况下,步骤 1 将失败,此时 `CachedThreadPool` 会创建新线程执行任务,execute 方法执行完成; - -#### 5.3.3 为什么不推荐使用`CachedThreadPool`? - -`CachedThreadPool`允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。 - -## 六 ScheduledThreadPoolExecutor 详解 - -**`ScheduledThreadPoolExecutor` 主要用来在给定的延迟后运行任务,或者定期执行任务。** 这个在实际项目中基本不会被用到,所以对这部分大家只需要简单了解一下它的思想。关于如何在Spring Boot 中 实现定时任务,可以查看这篇文章[《5分钟搞懂如何在Spring Boot中Schedule Tasks》](https://github.com/Snailclimb/springboot-guide/blob/master/docs/advanced/SpringBoot-ScheduleTasks.md)。 - -### 6.1 简介 - -**`ScheduledThreadPoolExecutor` 使用的任务队列 `DelayQueue` 封装了一个 `PriorityQueue`,`PriorityQueue` 会对队列中的任务进行排序,执行所需时间短的放在前面先被执行(`ScheduledFutureTask` 的 `time` 变量小的先执行),如果执行所需时间相同则先提交的任务将被先执行(`ScheduledFutureTask` 的 `squenceNumber` 变量小的先执行)。** - -**`ScheduledThreadPoolExecutor` 和 `Timer` 的比较:** - -- `Timer` 对系统时钟的变化敏感,`ScheduledThreadPoolExecutor`不是; -- `Timer` 只有一个执行线程,因此长时间运行的任务可以延迟其他任务。 `ScheduledThreadPoolExecutor` 可以配置任意数量的线程。 此外,如果你想(通过提供 ThreadFactory),你可以完全控制创建的线程; -- 在`TimerTask` 中抛出的运行时异常会杀死一个线程,从而导致 `Timer` 死机:-( ...即计划任务将不再运行。`ScheduledThreadExecutor` 不仅捕获运行时异常,还允许您在需要时处理它们(通过重写 `afterExecute` 方法`ThreadPoolExecutor`)。抛出异常的任务将被取消,但其他任务将继续运行。 - -**综上,在 JDK1.5 之后,你没有理由再使用 Timer 进行任务调度了。** - -> **备注:** Quartz 是一个由 java 编写的任务调度库,由 OpenSymphony 组织开源出来。在实际项目开发中使用 Quartz 的还是居多,比较推荐使用 Quartz。因为 Quartz 理论上能够同时对上万个任务进行调度,拥有丰富的功能特性,包括任务调度、任务持久化、可集群化、插件等等。 - -### 6.2 运行机制 - -![ScheduledThreadPoolExecutor运行机制](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC00LTE2LzkyNTk0Njk4LmpwZw?x-oss-process=image/format,png) - -**`ScheduledThreadPoolExecutor` 的执行主要分为两大部分:** - -1. 当调用 `ScheduledThreadPoolExecutor` 的 **`scheduleAtFixedRate()`** 方法或者**`scheduleWirhFixedDelay()`** 方法时,会向 `ScheduledThreadPoolExecutor` 的 **`DelayQueue`** 添加一个实现了 **`RunnableScheduledFuture`** 接口的 **`ScheduledFutureTask`** 。 -2. 线程池中的线程从 `DelayQueue` 中获取 `ScheduledFutureTask`,然后执行任务。 - -**`ScheduledThreadPoolExecutor` 为了实现周期性的执行任务,对 `ThreadPoolExecutor `做了如下修改:** - -- 使用 **`DelayQueue`** 作为任务队列; -- 获取任务的方不同 -- 执行周期任务后,增加了额外的处理 - -### 6.3 ScheduledThreadPoolExecutor 执行周期任务的步骤 - -![ScheduledThreadPoolExecutor执行周期任务的步骤](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC01LTMwLzU5OTE2Mzg5LmpwZw?x-oss-process=image/format,png) - -1. 线程 1 从 `DelayQueue` 中获取已到期的 `ScheduledFutureTask(DelayQueue.take())`。到期任务是指 `ScheduledFutureTask `的 time 大于等于当前系统的时间; -2. 线程 1 执行这个 `ScheduledFutureTask`; -3. 线程 1 修改 `ScheduledFutureTask` 的 time 变量为下次将要被执行的时间; -4. 线程 1 把这个修改 time 之后的 `ScheduledFutureTask` 放回 `DelayQueue` 中(`DelayQueue.add()`)。 - -## 七 线程池大小确定 - -**线程池数量的确定一直是困扰着程序员的一个难题,大部分程序员在设定线程池大小的时候就是随心而定。我们并没有考虑过这样大小的配置是否会带来什么问题,我自己就是这大部分程序员中的一个代表。** - -由于笔主对如何确定线程池大小也没有什么实际经验,所以,这部分内容参考了网上很多文章/书籍。 - -**首先,可以肯定的一点是线程池大小设置过大或者过小都会有问题。合适的才是最好,貌似在 95 % 的场景下都是合适的。** - -如果阅读过我的上一篇关于线程池的文章的话,你一定知道: - -**如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的! CPU 根本没有得到充分利用。** - -**但是,如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。** - -> 上下文切换: -> -> 多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。**任务从保存到再加载的过程就是一次上下文切换**。 -> -> 上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。 -> -> Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。 - -有一个简单并且适用面比较广的公式: - -- **CPU 密集型任务(N+1):** 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。 -- **I/O 密集型任务(2N):** 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。 - -## 八 参考 - -- 《Java 并发编程的艺术》 -- [Java Scheduler ScheduledExecutorService ScheduledThreadPoolExecutor Example](https://www.journaldev.com/2340/java-scheduler-scheduledexecutorservice-scheduledthreadpoolexecutor-example "Java Scheduler ScheduledExecutorService ScheduledThreadPoolExecutor Example") -- [java.util.concurrent.ScheduledThreadPoolExecutor Example](https://examples.javacodegeeks.com/core-java/util/concurrent/scheduledthreadpoolexecutor/java-util-concurrent-scheduledthreadpoolexecutor-example/ "java.util.concurrent.ScheduledThreadPoolExecutor Example") -- [ThreadPoolExecutor – Java Thread Pool Example](https://www.journaldev.com/1069/threadpoolexecutor-java-thread-pool-example-executorservice "ThreadPoolExecutor – Java Thread Pool Example") - -## 九 其他推荐阅读 - -- [Java 并发(三)线程池原理](https://www.cnblogs.com/warehouse/p/10720781.html "Java并发(三)线程池原理") -- [如何优雅的使用和理解线程池](https://github.com/crossoverJie/JCSprout/blob/master/MD/ThreadPoolExecutor.md "如何优雅的使用和理解线程池") diff --git a/docs/java/Multithread/synchronized.md b/docs/java/Multithread/synchronized.md deleted file mode 100644 index 0a1f4f2..0000000 --- a/docs/java/Multithread/synchronized.md +++ /dev/null @@ -1,169 +0,0 @@ - - -![Synchronized 关键字使用、底层原理、JDK1.6 之后的底层优化以及 和ReenTrantLock 的对比](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/Java%20%E7%A8%8B%E5%BA%8F%E5%91%98%E5%BF%85%E5%A4%87%EF%BC%9A%E5%B9%B6%E5%8F%91%E7%9F%A5%E8%AF%86%E7%B3%BB%E7%BB%9F%E6%80%BB%E7%BB%93/%E4%BA%8C%20%20Synchronized%20%E5%85%B3%E9%94%AE%E5%AD%97%E4%BD%BF%E7%94%A8%E3%80%81%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86%E3%80%81JDK1.6%20%E4%B9%8B%E5%90%8E%E7%9A%84%E5%BA%95%E5%B1%82%E4%BC%98%E5%8C%96%E4%BB%A5%E5%8F%8A%20%E5%92%8CReenTrantLock%20%E7%9A%84%E5%AF%B9%E6%AF%94.png) - -### synchronized关键字最主要的三种使用方式的总结 - -- **修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁** -- **修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁** 。也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份,所以对该类的所有对象都加了锁)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,**因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁**。 -- **修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。** 和 synchronized 方法一样,synchronized(this)代码块也是锁定当前对象的。synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。这里再提一下:synchronized关键字加到非 static 静态方法上是给对象实例上锁。另外需要注意的是:尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓冲功能! - -下面我已一个常见的面试题为例讲解一下 synchronized 关键字的具体使用。 - -面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理呗!” - - - -**双重校验锁实现对象单例(线程安全)** - -```java -public class Singleton { - - private volatile static Singleton uniqueInstance; - - private Singleton() { - } - - public static Singleton getUniqueInstance() { - //先判断对象是否已经实例过,没有实例化过才进入加锁代码 - if (uniqueInstance == null) { - //类对象加锁 - synchronized (Singleton.class) { - if (uniqueInstance == null) { - uniqueInstance = new Singleton(); - } - } - } - return uniqueInstance; - } -} -``` -另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。 - -uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行: - -1. 为 uniqueInstance 分配内存空间 -2. 初始化 uniqueInstance -3. 将 uniqueInstance 指向分配的内存地址 - -但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。 - -使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。 - - -###synchronized 关键字底层原理总结 - - - -**synchronized 关键字底层原理属于 JVM 层面。** - -**① synchronized 同步语句块的情况** - -```java -public class SynchronizedDemo { - public void method() { - synchronized (this) { - System.out.println("synchronized 代码块"); - } - } -} - -``` - -通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 `javac SynchronizedDemo.java` 命令生成编译后的 .class 文件,然后执行`javap -c -s -v -l SynchronizedDemo.class`。 - -![synchronized 关键字原理](https://images.gitbook.cn/abc37c80-d21d-11e8-aab3-09d30029e0d5) - -从上面我们可以看出: - -**synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。** 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权.当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。 - -**② synchronized 修饰方法的的情况** - -```java -public class SynchronizedDemo2 { - public synchronized void method() { - System.out.println("synchronized 方法"); - } -} - -``` - -![synchronized 关键字原理](https://images.gitbook.cn/7d407bf0-d21e-11e8-b2d6-1188c7e0dd7e) - -synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。 - - -在 Java 早期版本中,synchronized 属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。 - - -### JDK1.6 之后的底层优化 - -JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。 - -锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。 - -**①偏向锁** - -**引入偏向锁的目的和引入轻量级锁的目的很像,他们都是为了没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。但是不同是:轻量级锁在无竞争的情况下使用 CAS 操作去代替使用互斥量。而偏向锁在无竞争的情况下会把整个同步都消除掉**。 - -偏向锁的“偏”就是偏心的偏,它的意思是会偏向于第一个获得它的线程,如果在接下来的执行中,该锁没有被其他线程获取,那么持有偏向锁的线程就不需要进行同步!关于偏向锁的原理可以查看《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版的13章第三节锁优化。 - -但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。 - -**② 轻量级锁** - -倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的)。**轻量级锁不是为了代替重量级锁,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,因为使用轻量级锁时,不需要申请互斥量。另外,轻量级锁的加锁和解锁都用到了CAS操作。** 关于轻量级锁的加锁和解锁的原理可以查看《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版的13章第三节锁优化。 - -**轻量级锁能够提升程序同步性能的依据是“对于绝大部分锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥操作的开销。但如果存在锁竞争,除了互斥量开销外,还会额外发生CAS操作,因此在有锁竞争的情况下,轻量级锁比传统的重量级锁更慢!如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁!** - -**③ 自旋锁和自适应自旋** - -轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。 - -互斥同步对性能最大的影响就是阻塞的实现,因为挂起线程/恢复线程的操作都需要转入内核态中完成(用户态转换到内核态会耗费时间)。 - -**一般线程持有锁的时间都不是太长,所以仅仅为了这一点时间去挂起线程/恢复线程是得不偿失的。** 所以,虚拟机的开发团队就这样去考虑:“我们能不能让后面来的请求获取锁的线程等待一会而不被挂起呢?看看持有锁的线程是否很快就会释放锁”。**为了让一个线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就叫做自旋**。 - -百度百科对自旋锁的解释: - -> 何谓自旋锁?它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。 - -自旋锁在 JDK1.6 之前其实就已经引入了,不过是默认关闭的,需要通过`--XX:+UseSpinning`参数来开启。JDK1.6及1.6之后,就改为默认开启的了。需要注意的是:自旋等待不能完全替代阻塞,因为它还是要占用处理器时间。如果锁被占用的时间短,那么效果当然就很好了!反之,相反!自旋等待的时间必须要有限度。如果自旋超过了限定次数任然没有获得锁,就应该挂起线程。**自旋次数的默认值是10次,用户可以修改`--XX:PreBlockSpin`来更改**。 - -另外,**在 JDK1.6 中引入了自适应的自旋锁。自适应的自旋锁带来的改进就是:自旋的时间不在固定了,而是和前一次同一个锁上的自旋时间以及锁的拥有者的状态来决定,虚拟机变得越来越“聪明”了**。 - -**④ 锁消除** - -锁消除理解起来很简单,它指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。 - -**⑤ 锁粗化** - -原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,——直在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。 - -大部分情况下,上面的原则都是没有问题的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,那么会带来很多不必要的性能消耗。 - -### Synchronized 和 ReenTrantLock 的对比 - - -**① 两者都是可重入锁** - -两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。 - -**② synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API** - -synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。 - -**③ ReenTrantLock 比 synchronized 增加了一些高级功能** - -相比synchronized,ReenTrantLock增加了一些高级功能。主要来说主要有三点:**①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)** - -- **ReenTrantLock提供了一种能够中断等待锁的线程的机制**,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 -- **ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。** ReenTrantLock默认情况是非公平的,可以通过 ReenTrantLock类的`ReentrantLock(boolean fair)`构造方法来制定是否是公平的。 -- synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),**线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知”** ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。 - -如果你想使用上述功能,那么选择ReenTrantLock是一个不错的选择。 - -**④ 性能已不是选择标准** - -在JDK1.6之前,synchronized 的性能是比 ReenTrantLock 差很多。具体表示为:synchronized 关键字吞吐量随线程数的增加,下降得非常严重。而ReenTrantLock 基本保持一个比较稳定的水平。我觉得这也侧面反映了, synchronized 关键字还有非常大的优化余地。后续的技术发展也证明了这一点,我们上面也讲了在 JDK1.6 之后 JVM 团队对 synchronized 关键字做了很多优化。**JDK1.6 之后,synchronized 和 ReenTrantLock 的性能基本是持平了。所以网上那些说因为性能才选择 ReenTrantLock 的文章都是错的!JDK1.6之后,性能已经不是选择synchronized和ReenTrantLock的影响因素了!而且虚拟机在未来的性能改进中会更偏向于原生的synchronized,所以还是提倡在synchronized能满足你的需求的情况下,优先考虑使用synchronized关键字来进行同步!优化后的synchronized和ReenTrantLock一样,在很多地方都是用到了CAS操作**。 diff --git "a/docs/java/Multithread/\345\271\266\345\217\221\345\256\271\345\231\250\346\200\273\347\273\223.md" "b/docs/java/Multithread/\345\271\266\345\217\221\345\256\271\345\231\250\346\200\273\347\273\223.md" deleted file mode 100644 index ed60634..0000000 --- "a/docs/java/Multithread/\345\271\266\345\217\221\345\256\271\345\231\250\346\200\273\347\273\223.md" +++ /dev/null @@ -1,229 +0,0 @@ -点击关注[公众号](#公众号 "公众号")及时获取笔主最新更新文章,并可免费领取本文档配套的《Java 面试突击》以及 Java 工程师必备学习资源。 - - - -- [一 JDK 提供的并发容器总结](#一-jdk-提供的并发容器总结 "一 JDK 提供的并发容器总结") -- [二 ConcurrentHashMap](#二-concurrenthashmap "二 ConcurrentHashMap") -- [三 CopyOnWriteArrayList](#三-copyonwritearraylist "三 CopyOnWriteArrayList") - - [3.1 CopyOnWriteArrayList 简介](#31-copyonwritearraylist-简介 "3.1 CopyOnWriteArrayList 简介") - - [3.2 CopyOnWriteArrayList 是如何做到的?](#32-copyonwritearraylist-是如何做到的? "3.2 CopyOnWriteArrayList 是如何做到的?") - - [3.3 CopyOnWriteArrayList 读取和写入源码简单分析](#33-copyonwritearraylist-读取和写入源码简单分析 "3.3 CopyOnWriteArrayList 读取和写入源码简单分析") - - [3.3.1 CopyOnWriteArrayList 读取操作的实现](#331-copyonwritearraylist-读取操作的实现 "3.3.1 CopyOnWriteArrayList 读取操作的实现") - - [3.3.2 CopyOnWriteArrayList 写入操作的实现](#332-copyonwritearraylist-写入操作的实现 "3.3.2 CopyOnWriteArrayList 写入操作的实现") -- [四 ConcurrentLinkedQueue](#四-concurrentlinkedqueue "四 ConcurrentLinkedQueue") -- [五 BlockingQueue](#五-blockingqueue "五 BlockingQueue") - - [5.1 BlockingQueue 简单介绍](#51-blockingqueue-简单介绍 "5.1 BlockingQueue 简单介绍") - - [5.2 ArrayBlockingQueue](#52-arrayblockingqueue "5.2 ArrayBlockingQueue") - - [5.3 LinkedBlockingQueue](#53-linkedblockingqueue "5.3 LinkedBlockingQueue") - - [5.4 PriorityBlockingQueue](#54-priorityblockingqueue "5.4 PriorityBlockingQueue") -- [六 ConcurrentSkipListMap](#六-concurrentskiplistmap "六 ConcurrentSkipListMap") -- [七 参考](#七-参考 "七 参考") - - - -## 一 JDK 提供的并发容器总结 - -JDK 提供的这些容器大部分在 `java.util.concurrent` 包中。 - -- **ConcurrentHashMap:** 线程安全的 HashMap -- **CopyOnWriteArrayList:** 线程安全的 List,在读多写少的场合性能非常好,远远好于 Vector. -- **ConcurrentLinkedQueue:** 高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList,这是一个非阻塞队列。 -- **BlockingQueue:** 这是一个接口,JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道。 -- **ConcurrentSkipListMap:** 跳表的实现。这是一个 Map,使用跳表的数据结构进行快速查找。 - -## 二 ConcurrentHashMap - -我们知道 HashMap 不是线程安全的,在并发场景下如果要保证一种可行的方式是使用 `Collections.synchronizedMap()` 方法来包装我们的 HashMap。但这是通过使用一个全局的锁来同步不同线程间的并发访问,因此会带来不可忽视的性能问题。 - -所以就有了 HashMap 的线程安全版本—— ConcurrentHashMap 的诞生。在 ConcurrentHashMap 中,无论是读操作还是写操作都能保证很高的性能:在进行读操作时(几乎)不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问。 - -关于 ConcurrentHashMap 相关问题,我在 [Java 集合框架常见面试题](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/Java%E9%9B%86%E5%90%88%E6%A1%86%E6%9E%B6%E5%B8%B8%E8%A7%81%E9%9D%A2%E8%AF%95%E9%A2%98.md "Java集合框架常见面试题") 这篇文章中已经提到过。下面梳理一下关于 ConcurrentHashMap 比较重要的问题: - -- [ConcurrentHashMap 和 Hashtable 的区别](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/Java%E9%9B%86%E5%90%88%E6%A1%86%E6%9E%B6%E5%B8%B8%E8%A7%81%E9%9D%A2%E8%AF%95%E9%A2%98.md#concurrenthashmap-%E5%92%8C-hashtable-%E7%9A%84%E5%8C%BA%E5%88%AB "ConcurrentHashMap 和 Hashtable 的区别") -- [ConcurrentHashMap 线程安全的具体实现方式/底层具体实现](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/Java%E9%9B%86%E5%90%88%E6%A1%86%E6%9E%B6%E5%B8%B8%E8%A7%81%E9%9D%A2%E8%AF%95%E9%A2%98.md#concurrenthashmap%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8%E7%9A%84%E5%85%B7%E4%BD%93%E5%AE%9E%E7%8E%B0%E6%96%B9%E5%BC%8F%E5%BA%95%E5%B1%82%E5%85%B7%E4%BD%93%E5%AE%9E%E7%8E%B0 "ConcurrentHashMap线程安全的具体实现方式/底层具体实现") - -## 三 CopyOnWriteArrayList - -### 3.1 CopyOnWriteArrayList 简介 - -```java -public class CopyOnWriteArrayList -extends Object -implements List, RandomAccess, Cloneable, Serializable -``` - -在很多应用场景中,读操作可能会远远大于写操作。由于读操作根本不会修改原有的数据,因此对于每次读取都进行加锁其实是一种资源浪费。我们应该允许多个线程同时访问 List 的内部数据,毕竟读取操作是安全的。 - -这和我们之前在多线程章节讲过 `ReentrantReadWriteLock` 读写锁的思想非常类似,也就是读读共享、写写互斥、读写互斥、写读互斥。JDK 中提供了 `CopyOnWriteArrayList` 类比相比于在读写锁的思想又更进一步。为了将读取的性能发挥到极致,`CopyOnWriteArrayList` 读取是完全不用加锁的,并且更厉害的是:写入也不会阻塞读取操作。只有写入和写入之间需要进行同步等待。这样一来,读操作的性能就会大幅度提升。**那它是怎么做的呢?** - -### 3.2 CopyOnWriteArrayList 是如何做到的? - -`CopyOnWriteArrayList` 类的所有可变操作(add,set 等等)都是通过创建底层数组的新副本来实现的。当 List 需要被修改的时候,我并不修改原有内容,而是对原有数据进行一次复制,将修改的内容写入副本。写完之后,再将修改完的副本替换原来的数据,这样就可以保证写操作不会影响读操作了。 - -从 `CopyOnWriteArrayList` 的名字就能看出`CopyOnWriteArrayList` 是满足`CopyOnWrite` 的 ArrayList,所谓`CopyOnWrite` 也就是说:在计算机,如果你想要对一块内存进行修改时,我们不在原有内存块中进行写操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后呢,就将指向原来内存指针指向新的内存,原来的内存就可以被回收掉了。 - -### 3.3 CopyOnWriteArrayList 读取和写入源码简单分析 - -#### 3.3.1 CopyOnWriteArrayList 读取操作的实现 - -读取操作没有任何同步控制和锁操作,理由就是内部数组 array 不会发生修改,只会被另外一个 array 替换,因此可以保证数据安全。 - -```java - /** The array, accessed only via getArray/setArray. */ - private transient volatile Object[] array; - public E get(int index) { - return get(getArray(), index); - } - @SuppressWarnings("unchecked") - private E get(Object[] a, int index) { - return (E) a[index]; - } - final Object[] getArray() { - return array; - } - -``` - -#### 3.3.2 CopyOnWriteArrayList 写入操作的实现 - -CopyOnWriteArrayList 写入操作 add() 方法在添加集合的时候加了锁,保证了同步,避免了多线程写的时候会 copy 出多个副本出来。 - -```java - /** - * Appends the specified element to the end of this list. - * - * @param e element to be appended to this list - * @return {@code true} (as specified by {@link Collection#add}) - */ - public boolean add(E e) { - final ReentrantLock lock = this.lock; - lock.lock();//加锁 - try { - Object[] elements = getArray(); - int len = elements.length; - Object[] newElements = Arrays.copyOf(elements, len + 1);//拷贝新数组 - newElements[len] = e; - setArray(newElements); - return true; - } finally { - lock.unlock();//释放锁 - } - } -``` - -## 四 ConcurrentLinkedQueue - -Java 提供的线程安全的 Queue 可以分为**阻塞队列**和**非阻塞队列**,其中阻塞队列的典型例子是 BlockingQueue,非阻塞队列的典型例子是 ConcurrentLinkedQueue,在实际应用中要根据实际需要选用阻塞队列或者非阻塞队列。 **阻塞队列可以通过加锁来实现,非阻塞队列可以通过 CAS 操作实现。** - -从名字可以看出,`ConcurrentLinkedQueue`这个队列使用链表作为其数据结构.ConcurrentLinkedQueue 应该算是在高并发环境中性能最好的队列了。它之所有能有很好的性能,是因为其内部复杂的实现。 - -ConcurrentLinkedQueue 内部代码我们就不分析了,大家知道 ConcurrentLinkedQueue 主要使用 CAS 非阻塞算法来实现线程安全就好了。 - -ConcurrentLinkedQueue 适合在对性能要求相对较高,同时对队列的读写存在多个线程同时进行的场景,即如果对队列加锁的成本较高则适合使用无锁的 ConcurrentLinkedQueue 来替代。 - -## 五 BlockingQueue - -### 5.1 BlockingQueue 简单介绍 - -上面我们己经提到了 ConcurrentLinkedQueue 作为高性能的非阻塞队列。下面我们要讲到的是阻塞队列——BlockingQueue。阻塞队列(BlockingQueue)被广泛使用在“生产者-消费者”问题中,其原因是 BlockingQueue 提供了可阻塞的插入和移除的方法。当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止。 - -BlockingQueue 是一个接口,继承自 Queue,所以其实现类也可以作为 Queue 的实现来使用,而 Queue 又继承自 Collection 接口。下面是 BlockingQueue 的相关实现类: - -![BlockingQueue 的实现类](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-9/51622268.jpg) - -**下面主要介绍一下:ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,这三个 BlockingQueue 的实现类。** - -### 5.2 ArrayBlockingQueue - -**ArrayBlockingQueue** 是 BlockingQueue 接口的有界队列实现类,底层采用**数组**来实现。ArrayBlockingQueue 一旦创建,容量不能改变。其并发控制采用可重入锁来控制,不管是插入操作还是读取操作,都需要获取到锁才能进行操作。当队列容量满时,尝试将元素放入队列将导致操作阻塞;尝试从一个空队列中取一个元素也会同样阻塞。 - -ArrayBlockingQueue 默认情况下不能保证线程访问队列的公平性,所谓公平性是指严格按照线程等待的绝对时间顺序,即最先等待的线程能够最先访问到 ArrayBlockingQueue。而非公平性则是指访问 ArrayBlockingQueue 的顺序不是遵守严格的时间顺序,有可能存在,当 ArrayBlockingQueue 可以被访问时,长时间阻塞的线程依然无法访问到 ArrayBlockingQueue。如果保证公平性,通常会降低吞吐量。如果需要获得公平性的 ArrayBlockingQueue,可采用如下代码: - -```java -private static ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue(10,true); -``` - -### 5.3 LinkedBlockingQueue - -**LinkedBlockingQueue** 底层基于**单向链表**实现的阻塞队列,可以当做无界队列也可以当做有界队列来使用,同样满足 FIFO 的特性,与 ArrayBlockingQueue 相比起来具有更高的吞吐量,为了防止 LinkedBlockingQueue 容量迅速增,损耗大量内存。通常在创建 LinkedBlockingQueue 对象时,会指定其大小,如果未指定,容量等于 Integer.MAX_VALUE。 - -**相关构造方法:** - -```java - /** - *某种意义上的无界队列 - * Creates a {@code LinkedBlockingQueue} with a capacity of - * {@link Integer#MAX_VALUE}. - */ - public LinkedBlockingQueue() { - this(Integer.MAX_VALUE); - } - - /** - *有界队列 - * Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity. - * - * @param capacity the capacity of this queue - * @throws IllegalArgumentException if {@code capacity} is not greater - * than zero - */ - public LinkedBlockingQueue(int capacity) { - if (capacity <= 0) throw new IllegalArgumentException(); - this.capacity = capacity; - last = head = new Node(null); - } -``` - -### 5.4 PriorityBlockingQueue - -**PriorityBlockingQueue** 是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序进行排序,也可以通过自定义类实现 `compareTo()` 方法来指定元素排序规则,或者初始化时通过构造器参数 `Comparator` 来指定排序规则。 - -PriorityBlockingQueue 并发控制采用的是 **ReentrantLock**,队列为无界队列(ArrayBlockingQueue 是有界队列,LinkedBlockingQueue 也可以通过在构造函数中传入 capacity 指定队列最大的容量,但是 PriorityBlockingQueue 只能指定初始的队列大小,后面插入元素的时候,**如果空间不够的话会自动扩容**)。 - -简单地说,它就是 PriorityQueue 的线程安全版本。不可以插入 null 值,同时,插入队列的对象必须是可比较大小的(comparable),否则报 ClassCastException 异常。它的插入操作 put 方法不会 block,因为它是无界队列(take 方法在队列为空的时候会阻塞)。 - -**推荐文章:** - -《解读 Java 并发队列 BlockingQueue》 - -[https://javadoop.com/post/java-concurrent-queue](https://javadoop.com/post/java-concurrent-queue "https://javadoop.com/post/java-concurrent-queue") - -## 六 ConcurrentSkipListMap - -下面这部分内容参考了极客时间专栏[《数据结构与算法之美》](https://time.geekbang.org/column/intro/126?code=zl3GYeAsRI4rEJIBNu5B/km7LSZsPDlGWQEpAYw5Vu0=&utm_term=SPoster "《数据结构与算法之美》")以及《实战 Java 高并发程序设计》。 - -**为了引出 ConcurrentSkipListMap,先带着大家简单理解一下跳表。** - -对于一个单链表,即使链表是有序的,如果我们想要在其中查找某个数据,也只能从头到尾遍历链表,这样效率自然就会很低,跳表就不一样了。跳表是一种可以用来快速查找的数据结构,有点类似于平衡树。它们都可以对元素进行快速的查找。但一个重要的区别是:对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整。而对跳表的插入和删除只需要对整个数据结构的局部进行操作即可。这样带来的好处是:在高并发的情况下,你会需要一个全局锁来保证整个平衡树的线程安全。而对于跳表,你只需要部分锁即可。这样,在高并发环境下,你就可以拥有更好的性能。而就查询的性能而言,跳表的时间复杂度也是 **O(logn)** 所以在并发数据结构中,JDK 使用跳表来实现一个 Map。 - -跳表的本质是同时维护了多个链表,并且链表是分层的, - -![2级索引跳表](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-9/93666217.jpg) - -最低层的链表维护了跳表内所有的元素,每上面一层链表都是下面一层的子集。 - -跳表内的所有链表的元素都是排序的。查找时,可以从顶级链表开始找。一旦发现被查找的元素大于当前链表中的取值,就会转入下一层链表继续找。这也就是说在查找过程中,搜索是跳跃式的。如上图所示,在跳表中查找元素 18。 - -![在跳表中查找元素18](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-9/32005738.jpg) - -查找 18 的时候原来需要遍历 18 次,现在只需要 7 次即可。针对链表长度比较大的时候,构建索引查找效率的提升就会非常明显。 - -从上面很容易看出,**跳表是一种利用空间换时间的算法。** - -使用跳表实现 Map 和使用哈希算法实现 Map 的另外一个不同之处是:哈希并不会保存元素的顺序,而跳表内所有的元素都是排序的。因此在对跳表进行遍历时,你会得到一个有序的结果。所以,如果你的应用需要有序性,那么跳表就是你不二的选择。JDK 中实现这一数据结构的类是 ConcurrentSkipListMap。 - -## 七 参考 - -- 《实战 Java 高并发程序设计》 -- https://javadoop.com/post/java-concurrent-queue -- https://juejin.im/post/5aeebd02518825672f19c546 - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java 面试突击》:** 由本文档衍生的专为面试而生的《Java 面试突击》V2.0 PDF 版本[公众号](#公众号 "公众号")后台回复 **"面试突击"** 即可免费领取! - -**Java 工程师必备学习资源:** 一些 Java 工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) diff --git "a/docs/java/Multithread/\345\271\266\345\217\221\347\274\226\347\250\213\345\237\272\347\241\200\347\237\245\350\257\206.md" "b/docs/java/Multithread/\345\271\266\345\217\221\347\274\226\347\250\213\345\237\272\347\241\200\347\237\245\350\257\206.md" deleted file mode 100644 index 68509cd..0000000 --- "a/docs/java/Multithread/\345\271\266\345\217\221\347\274\226\347\250\213\345\237\272\347\241\200\347\237\245\350\257\206.md" +++ /dev/null @@ -1,407 +0,0 @@ -# Java 并发基础知识 - -Java 并发的基础知识,可能会在笔试中遇到,技术面试中也可能以并发知识环节提问的第一个问题出现。比如面试官可能会问你:“谈谈自己对于进程和线程的理解,两者的区别是什么?” - -**本节思维导图:** - -## 一 进程和线程 - -进程和线程的对比这一知识点由于过于基础,所以在面试中很少碰到,但是极有可能会在笔试题中碰到。 - -常见的提问形式是这样的:**“什么是线程和进程?,请简要描述线程与进程的关系、区别及优缺点? ”**。 - -### 1.1. 何为进程? - -进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。 - -在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。 - -如下图所示,在 windows 中通过查看任务管理器的方式,我们就可以清楚看到 window 当前运行的进程(.exe 文件的运行)。 - -![进程 ](https://images.gitbook.cn/a0929b60-d133-11e8-88a4-5328c5b70145) - -### 1.2 何为线程? - -线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的**堆**和**方法区**资源,但每个线程有自己的**程序计数器**、**虚拟机栈**和**本地方法栈**,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。 - -Java 程序天生就是多线程程序,我们可以通过 JMX 来看一下一个普通的 Java 程序有哪些线程,代码如下。 - -```java -public class MultiThread { - public static void main(String[] args) { - // 获取 Java 线程管理 MXBean - ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); - // 不需要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息 - ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false); - // 遍历线程信息,仅打印线程 ID 和线程名称信息 - for (ThreadInfo threadInfo : threadInfos) { - System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName()); - } - } -} -``` - -上述程序输出如下(输出内容可能不同,不用太纠结下面每个线程的作用,只用知道 main 线程执行 main 方法即可): - -``` -[5] Attach Listener //添加事件 -[4] Signal Dispatcher // 分发处理给 JVM 信号的线程 -[3] Finalizer //调用对象 finalize 方法的线程 -[2] Reference Handler //清除 reference 线程 -[1] main //main 线程,程序入口 -``` - -从上面的输出内容可以看出:**一个 Java 程序的运行是 main 线程和多个其他线程同时运行**。 - -### 1.3 从 JVM 角度说进程和线程之间的关系(重要) - -#### 1.3.1 图解进程和线程的关系 - -下图是 Java 内存区域,通过下图我们从 JVM 的角度来说一下线程和进程之间的关系。如果你对 Java 内存区域 (运行时数据区) 这部分知识不太了解的话可以阅读一下我的这篇文章:[《可能是把 Java 内存区域讲的最清楚的一篇文章》]() - -
- -
- - -从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的**堆**和**方法区 (JDK1.8 之后的元空间)**资源,但是每个线程有自己的**程序计数器**、**虚拟机栈** 和 **本地方法栈**。 - -下面来思考这样一个问题:为什么**程序计数器**、**虚拟机栈**和**本地方法栈**是线程私有的呢?为什么堆和方法区是线程共享的呢? - -#### 1.3.2 程序计数器为什么是私有的? - -程序计数器主要有下面两个作用: - -1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。 -2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。 - -需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。 - -所以,程序计数器私有主要是为了**线程切换后能恢复到正确的执行位置**。 - -#### 1.3.3 虚拟机栈和本地方法栈为什么是私有的? - -- **虚拟机栈:**每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。 -- **本地方法栈:**和虚拟机栈所发挥的作用非常相似,区别是: **虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。** 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。 - -所以,为了**保证线程中的局部变量不被别的线程访问到**,虚拟机栈和本地方法栈是线程私有的。 - -#### 1.3.4 一句话简单了解堆和方法区 - -堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 - -## 二 多线程并发编程 - -### 2.1 并发与并行概念解读 - -- **并发:** 同一时间段,多个任务都在执行 (单位时间内不一定同时执行); -- **并行:**单位时间内,多个任务同时执行。 - -### 2.2 为什么要使用多线程? - -先从总体上来说: - -- **从计算机底层来说:**线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。 -- **从当代互联网发展趋势来说:**现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。 - -再深入到计算机底层来探讨: - -- **单核时代:** 在单核时代多线程主要是为了提高 CPU 和 IO 设备的综合利用率。举个例子:当只有一个线程的时候会导致 CPU 计算时,IO 设备空闲;进行 IO 操作时,CPU 空闲。我们可以简单地说这两者的利用率目前都是 50%左右。但是当有两个线程的时候就不一样了,当一个线程执行 CPU 计算时,另外一个线程可以进行 IO 操作,这样两个的利用率就可以在理想情况下达到 100%了。 -- **多核时代:** 多核时代多线程主要是为了提高 CPU 利用率。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,CPU 只会一个 CPU 核心被利用到,而创建多个线程就可以让多个 CPU 核心被利用到,这样就提高了 CPU 的利用率。 - -### 2.3 使用多线程可能带来的问题 - -并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、死锁还有受限于硬件和软件的资源闲置问题。 - -## 三 线程的创建与运行 - -前两种实际上很少使用,一般都是用线程池的方式比较多一点。 - -### 3.1 继承 Thread 类的方式 - - -```java -public class MyThread extends Thread { - @Override - public void run() { - super.run(); - System.out.println("MyThread"); - } -} -``` -Run.java - -```java -public class Run { - - public static void main(String[] args) { - MyThread mythread = new MyThread(); - mythread.start(); - System.out.println("运行结束"); - } - -} - -``` -运行结果: -![结果 ](https://user-gold-cdn.xitu.io/2018/3/20/16243e80f22a2d54?w=161&h=54&f=jpeg&s=7380) - -从上面的运行结果可以看出:线程是一个子任务,CPU 以不确定的方式,或者说是以随机的时间来调用线程中的 run 方法。 - -### 3.2 实现 Runnable 接口的方式 - -推荐实现 Runnable 接口方式开发多线程,因为 Java 单继承但是可以实现多个接口。 - -MyRunnable.java - -```java -public class MyRunnable implements Runnable { - @Override - public void run() { - System.out.println("MyRunnable"); - } -} -``` - -Run.java - -```java -public class Run { - - public static void main(String[] args) { - Runnable runnable=new MyRunnable(); - Thread thread=new Thread(runnable); - thread.start(); - System.out.println("运行结束!"); - } - -} -``` -运行结果: -![运行结果 ](https://user-gold-cdn.xitu.io/2018/3/20/16243f4373c6141a?w=137&h=46&f=jpeg&s=7316) - -### 3.3 使用线程池的方式 - -使用线程池的方式也是最推荐的一种方式,另外,《阿里巴巴 Java 开发手册》在第一章第六节并发处理这一部分也强调到“线程资源必须通过线程池提供,不允许在应用中自行显示创建线程”。这里就不给大家演示代码了,线程池这一节会详细介绍到这部分内容。 - -## 四 线程的生命周期和状态 - -Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态(图源《Java 并发编程艺术》4.1.4 节)。 - -![Java 线程的状态 ](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/19-1-29/Java%E7%BA%BF%E7%A8%8B%E7%9A%84%E7%8A%B6%E6%80%81.png) - -线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示(图源《Java 并发编程艺术》4.1.4 节): - -![Java 线程状态变迁 ](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/19-1-29/Java%20%E7%BA%BF%E7%A8%8B%E7%8A%B6%E6%80%81%E5%8F%98%E8%BF%81.png) - - - -由上图可以看出:线程创建之后它将处于 **NEW(新建)** 状态,调用 `start()` 方法后开始运行,线程这时候处于 **READY(可运行)** 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 **RUNNING(运行)** 状态。 - -> 操作系统隐藏 Java 虚拟机(JVM)中的 RUNNABLE 和 RUNNING 状态,它只能看到 RUNNABLE 状态(图源:[HowToDoInJava](https://howtodoinjava.com/):[Java Thread Life Cycle and Thread States](https://howtodoinjava.com/java/multi-threading/java-thread-life-cycle-and-thread-states/)),所以 Java 系统一般将这两个状态统称为 **RUNNABLE(运行中)** 状态 。 - -![RUNNABLE-VS-RUNNING](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3/RUNNABLE-VS-RUNNING.png) - -当线程执行 `wait()`方法之后,线程进入 **WAITING(等待)**状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 **TIME_WAITING(超时等待)** 状态相当于在等待状态的基础上增加了超时限制,比如通过 `sleep(long millis)`方法或 `wait(long millis)`方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 **BLOCKED(阻塞)** 状态。线程在执行 Runnable 的` run() `方法之后将会进入到 **TERMINATED(终止)** 状态。 - -## 五 线程优先级 - -**理论上**来说系统会根据优先级来决定首先使哪个线程进入运行状态。当 CPU 比较闲的时候,设置线程优先级几乎不会有任何作用,而且很多操作系统压根不会不会理会你设置的线程优先级,所以不要让业务过度依赖于线程的优先级。 - -另外,**线程优先级具有继承特性**比如 A 线程启动 B 线程,则 B 线程的优先级和 A 是一样的。**线程优先级还具有随机性** 也就是说线程优先级高的不一定每一次都先执行完。 - -Thread 类中包含的成员变量代表了线程的某些优先级。如**Thread.MIN_PRIORITY(常数 1)**,**Thread.NORM_PRIORITY(常数 5)**,**Thread.MAX_PRIORITY(常数 10)**。其中每个线程的优先级都在**1** 到**10** 之间,在默认情况下优先级都是**Thread.NORM_PRIORITY(常数 5)**。 - -**一般情况下,不会对线程设定优先级别,更不会让某些业务严重地依赖线程的优先级别,比如权重,借助优先级设定某个任务的权重,这种方式是不可取的,一般定义线程的时候使用默认的优先级就好了。** - -**相关方法:** - -```java -public final void setPriority(int newPriority) //为线程设定优先级 -public final int getPriority() //获取线程的优先级 -``` -**设置线程优先级方法源码:** - -```java - public final void setPriority(int newPriority) { - ThreadGroup g; - checkAccess(); - //线程游戏优先级不能小于 1 也不能大于 10,否则会抛出异常 - if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) { - throw new IllegalArgumentException(); - } - //如果指定的线程优先级大于该线程所在线程组的最大优先级,那么该线程的优先级将设为线程组的最大优先级 - if((g = getThreadGroup()) != null) { - if (newPriority > g.getMaxPriority()) { - newPriority = g.getMaxPriority(); - } - setPriority0(priority = newPriority); - } - } - -``` - -## 六 守护线程和用户线程 - -**守护线程和用户线程简介:** - -- **用户 (User) 线程:**运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程 -- **守护 (Daemon) 线程:**运行在后台,为其他前台线程服务.也可以说守护线程是 JVM 中非守护线程的 **“佣人”**。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作. - -main 函数所在的线程就是一个用户线程啊,main 函数启动的同时在 JVM 内部同时还启动了好多守护线程,比如垃圾回收线程。 - -**那么守护线程和用户线程有什么区别呢?** - -比较明显的区别之一是用户线程结束,JVM 退出,不管这个时候有没有守护线程运行。而守护线程不会影响 JVM 的退出。 - -**注意事项:** - -1. `setDaemon(true)`必须在`start()`方法前执行,否则会抛出 `IllegalThreadStateException` 异常 -2. 在守护线程中产生的新线程也是守护线程 -3. 不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑 -4. 守护 (Daemon) 线程中不能依靠 finally 块的内容来确保执行关闭或清理资源的逻辑。因为我们上面也说过了一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作,所以守护 (Daemon) 线程中的 finally 语句块可能无法被执行。 - -## 七 上下文切换 - -多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。 - -概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换会这个任务时,可以再加载这个任务的状态。**任务从保存到再加载的过程就是一次上下文切换**。 - -上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。 - -Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。 - -## 八 线程死锁 - -### 认识线程死锁 - -多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。 - -如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。 - -![线程死锁示意图 ](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-4/2019-4死锁1.png) - -下面通过一个例子来说明线程死锁,代码模拟了上图的死锁的情况 (代码来源于《并发编程之美》): - -```java -public class DeadLockDemo { - private static Object resource1 = new Object();//资源 1 - private static Object resource2 = new Object();//资源 2 - - public static void main(String[] args) { - new Thread(() -> { - synchronized (resource1) { - System.out.println(Thread.currentThread() + "get resource1"); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println(Thread.currentThread() + "waiting get resource2"); - synchronized (resource2) { - System.out.println(Thread.currentThread() + "get resource2"); - } - } - }, "线程 1").start(); - - new Thread(() -> { - synchronized (resource2) { - System.out.println(Thread.currentThread() + "get resource2"); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println(Thread.currentThread() + "waiting get resource1"); - synchronized (resource1) { - System.out.println(Thread.currentThread() + "get resource1"); - } - } - }, "线程 2").start(); - } -} -``` - -Output - -``` -Thread[线程 1,5,main]get resource1 -Thread[线程 2,5,main]get resource2 -Thread[线程 1,5,main]waiting get resource2 -Thread[线程 2,5,main]waiting get resource1 -``` - -线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过` Thread.sleep(1000);`让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。 - -学过操作系统的朋友都知道产生死锁必须具备以下四个条件: - -1. 互斥条件:该资源任意一个时刻只由一个线程占用。 -1. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。 -1. 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。 -1. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。 - -### 如何预防线程死锁? - -我们只要破坏产生死锁的四个条件中的其中一个就可以了。 - -**破坏互斥条件** - -这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。 - -**破坏请求与保持条件** - -一次性申请所有的资源。 - -**破坏不剥夺条件** - -占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。 - -**破坏循环等待条件** - -靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。 - -我们对线程 2 的代码修改成下面这样就不会产生死锁了。 - -```java - new Thread(() -> { - synchronized (resource1) { - System.out.println(Thread.currentThread() + "get resource1"); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println(Thread.currentThread() + "waiting get resource2"); - synchronized (resource2) { - System.out.println(Thread.currentThread() + "get resource2"); - } - } - }, "线程 2").start(); -``` - -Output - -``` -Thread[线程 1,5,main]get resource1 -Thread[线程 1,5,main]waiting get resource2 -Thread[线程 1,5,main]get resource2 -Thread[线程 2,5,main]get resource1 -Thread[线程 2,5,main]waiting get resource2 -Thread[线程 2,5,main]get resource2 - -Process finished with exit code 0 -``` - -我们分析一下上面的代码为什么避免了死锁的发生? - -线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。 - -## 参考 - -- 《Java 并发编程之美》 - -- 《Java 并发编程的艺术》 - -- https://howtodoinjava.com/java/multi-threading/java-thread-life-cycle-and-thread-states/ - - \ No newline at end of file diff --git a/docs/java/collection/ArrayList-Grow.md b/docs/java/collection/ArrayList-Grow.md deleted file mode 100644 index ab56b80..0000000 --- a/docs/java/collection/ArrayList-Grow.md +++ /dev/null @@ -1,358 +0,0 @@ - -## 一 先从 ArrayList 的构造函数说起 - -**ArrayList有三种方式来初始化,构造方法源码如下:** - -```java - /** - * 默认初始容量大小 - */ - private static final int DEFAULT_CAPACITY = 10; - - - private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; - - /** - *默认构造函数,使用初始容量10构造一个空列表(无参数构造) - */ - public ArrayList() { - this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; - } - - /** - * 带初始容量参数的构造函数。(用户自己指定容量) - */ - public ArrayList(int initialCapacity) { - if (initialCapacity > 0) {//初始容量大于0 - //创建initialCapacity大小的数组 - this.elementData = new Object[initialCapacity]; - } else if (initialCapacity == 0) {//初始容量等于0 - //创建空数组 - this.elementData = EMPTY_ELEMENTDATA; - } else {//初始容量小于0,抛出异常 - throw new IllegalArgumentException("Illegal Capacity: "+ - initialCapacity); - } - } - - - /** - *构造包含指定collection元素的列表,这些元素利用该集合的迭代器按顺序返回 - *如果指定的集合为null,throws NullPointerException。 - */ - public ArrayList(Collection c) { - elementData = c.toArray(); - if ((size = elementData.length) != 0) { - // c.toArray might (incorrectly) not return Object[] (see 6260652) - if (elementData.getClass() != Object[].class) - elementData = Arrays.copyOf(elementData, size, Object[].class); - } else { - // replace with empty array. - this.elementData = EMPTY_ELEMENTDATA; - } - } - -``` - -细心的同学一定会发现 :**以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为10。** 下面在我们分析 ArrayList 扩容时会讲到这一点内容! - -## 二 一步一步分析 ArrayList 扩容机制 - -这里以无参构造函数创建的 ArrayList 为例分析 - -### 1. 先来看 `add` 方法 - -```java - /** - * 将指定的元素追加到此列表的末尾。 - */ - public boolean add(E e) { - //添加元素之前,先调用ensureCapacityInternal方法 - ensureCapacityInternal(size + 1); // Increments modCount!! - //这里看到ArrayList添加元素的实质就相当于为数组赋值 - elementData[size++] = e; - return true; - } -``` -### 2. 再来看看 `ensureCapacityInternal()` 方法 - -可以看到 `add` 方法 首先调用了`ensureCapacityInternal(size + 1)` - -```java - //得到最小扩容量 - private void ensureCapacityInternal(int minCapacity) { - if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { - // 获取默认的容量和传入参数的较大值 - minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); - } - - ensureExplicitCapacity(minCapacity); - } -``` -**当 要 add 进第1个元素时,minCapacity为1,在Math.max()方法比较后,minCapacity 为10。** - -### 3. `ensureExplicitCapacity()` 方法 - -如果调用 `ensureCapacityInternal()` 方法就一定会进过(执行)这个方法,下面我们来研究一下这个方法的源码! - -```java - //判断是否需要扩容 - private void ensureExplicitCapacity(int minCapacity) { - modCount++; - - // overflow-conscious code - if (minCapacity - elementData.length > 0) - //调用grow方法进行扩容,调用此方法代表已经开始扩容了 - grow(minCapacity); - } - -``` - -我们来仔细分析一下: - -- 当我们要 add 进第1个元素到 ArrayList 时,elementData.length 为0 (因为还是一个空的 list),因为执行了 `ensureCapacityInternal()` 方法 ,所以 minCapacity 此时为10。此时,`minCapacity - elementData.length > 0 `成立,所以会进入 `grow(minCapacity)` 方法。 -- 当add第2个元素时,minCapacity 为2,此时e lementData.length(容量)在添加第一个元素后扩容成 10 了。此时,`minCapacity - elementData.length > 0 ` 不成立,所以不会进入 (执行)`grow(minCapacity)` 方法。 -- 添加第3、4···到第10个元素时,依然不会执行grow方法,数组容量都为10。 - -直到添加第11个元素,minCapacity(为11)比elementData.length(为10)要大。进入grow方法进行扩容。 - -### 4. `grow()` 方法 - -```java - /** - * 要分配的最大数组大小 - */ - private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; - - /** - * ArrayList扩容的核心方法。 - */ - private void grow(int minCapacity) { - // oldCapacity为旧容量,newCapacity为新容量 - int oldCapacity = elementData.length; - //将oldCapacity 右移一位,其效果相当于oldCapacity /2, - //我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍, - int newCapacity = oldCapacity + (oldCapacity >> 1); - //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量, - if (newCapacity - minCapacity < 0) - newCapacity = minCapacity; - // 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE, - //如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。 - if (newCapacity - MAX_ARRAY_SIZE > 0) - newCapacity = hugeCapacity(minCapacity); - // minCapacity is usually close to size, so this is a win: - elementData = Arrays.copyOf(elementData, newCapacity); - } -``` - -**int newCapacity = oldCapacity + (oldCapacity >> 1),所以 ArrayList 每次扩容之后容量都会变为原来的 1.5 倍!(JDK1.6版本以后)** JDk1.6版本时,扩容之后容量为 1.5 倍+1!详情请参考源码 - -> ">>"(移位运算符):>>1 右移一位相当于除2,右移n位相当于除以 2 的 n 次方。这里 oldCapacity 明显右移了1位所以相当于oldCapacity /2。对于大数据的2进制运算,位移运算符比那些普通运算符的运算要快很多,因为程序仅仅移动一下而已,不去计算,这样提高了效率,节省了资源   - -**我们再来通过例子探究一下`grow()` 方法 :** - -- 当add第1个元素时,oldCapacity 为0,经比较后第一个if判断成立,newCapacity = minCapacity(为10)。但是第二个if判断不会成立,即newCapacity 不比 MAX_ARRAY_SIZE大,则不会进入 `hugeCapacity` 方法。数组容量为10,add方法中 return true,size增为1。 -- 当add第11个元素进入grow方法时,newCapacity为15,比minCapacity(为11)大,第一个if判断不成立。新容量没有大于数组最大size,不会进入hugeCapacity方法。数组容量扩为15,add方法中return true,size增为11。 -- 以此类推······ - -**这里补充一点比较重要,但是容易被忽视掉的知识点:** - -- java 中的 `length `属性是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性. -- java 中的 `length()` 方法是针对字符串说的,如果想看这个字符串的长度则用到 `length()` 这个方法. -- java 中的 `size()` 方法是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看! - -### 5. `hugeCapacity()` 方法。 - -从上面 `grow()` 方法源码我们知道: 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。 - - -```java - private static int hugeCapacity(int minCapacity) { - if (minCapacity < 0) // overflow - throw new OutOfMemoryError(); - //对minCapacity和MAX_ARRAY_SIZE进行比较 - //若minCapacity大,将Integer.MAX_VALUE作为新数组的大小 - //若MAX_ARRAY_SIZE大,将MAX_ARRAY_SIZE作为新数组的大小 - //MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; - return (minCapacity > MAX_ARRAY_SIZE) ? - Integer.MAX_VALUE : - MAX_ARRAY_SIZE; - } -``` - - - -## 三 `System.arraycopy()` 和 `Arrays.copyOf()`方法 - - -阅读源码的话,我们就会发现 ArrayList 中大量调用了这两个方法。比如:我们上面讲的扩容操作以及`add(int index, E element)`、`toArray()` 等方法中都用到了该方法! - - -### 3.1 `System.arraycopy()` 方法 - -```java - /** - * 在此列表中的指定位置插入指定的元素。 - *先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大; - *再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。 - */ - public void add(int index, E element) { - rangeCheckForAdd(index); - - ensureCapacityInternal(size + 1); // Increments modCount!! - //arraycopy()方法实现数组自己复制自己 - //elementData:源数组;index:源数组中的起始位置;elementData:目标数组;index + 1:目标数组中的起始位置; size - index:要复制的数组元素的数量; - System.arraycopy(elementData, index, elementData, index + 1, size - index); - elementData[index] = element; - size++; - } -``` - -我们写一个简单的方法测试以下: - -```java -public class ArraycopyTest { - - public static void main(String[] args) { - // TODO Auto-generated method stub - int[] a = new int[10]; - a[0] = 0; - a[1] = 1; - a[2] = 2; - a[3] = 3; - System.arraycopy(a, 2, a, 3, 3); - a[2]=99; - for (int i = 0; i < a.length; i++) { - System.out.println(a[i]); - } - } - -} -``` - -结果: - -``` -0 1 99 2 3 0 0 0 0 0 -``` - -### 3.2 `Arrays.copyOf()`方法 - -```java - /** - 以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素); 返回的数组的运行时类型是指定数组的运行时类型。 - */ - public Object[] toArray() { - //elementData:要复制的数组;size:要复制的长度 - return Arrays.copyOf(elementData, size); - } -``` - -个人觉得使用 `Arrays.copyOf()`方法主要是为了给原有数组扩容,测试代码如下: - -```java -public class ArrayscopyOfTest { - - public static void main(String[] args) { - int[] a = new int[3]; - a[0] = 0; - a[1] = 1; - a[2] = 2; - int[] b = Arrays.copyOf(a, 10); - System.out.println("b.length"+b.length); - } -} -``` - -结果: - -``` -10 -``` - -### 3.3 两者联系和区别 - -**联系:** - -看两者源代码可以发现 copyOf() 内部实际调用了 `System.arraycopy()` 方法 - -**区别:** - -`arraycopy()` 需要目标数组,将原数组拷贝到你自己定义的数组里或者原数组,而且可以选择拷贝的起点和长度以及放入新数组中的位置 `copyOf()` 是系统自动在内部新建一个数组,并返回该数组。 - -## 四 `ensureCapacity`方法 - -ArrayList 源码中有一个 `ensureCapacity` 方法不知道大家注意到没有,这个方法 ArrayList 内部没有被调用过,所以很显然是提供给用户调用的,那么这个方法有什么作用呢? - -```java - /** - 如有必要,增加此 ArrayList 实例的容量,以确保它至少可以容纳由minimum capacity参数指定的元素数。 - * - * @param minCapacity 所需的最小容量 - */ - public void ensureCapacity(int minCapacity) { - int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) - // any size if not default element table - ? 0 - // larger than default for default empty table. It's already - // supposed to be at default size. - : DEFAULT_CAPACITY; - - if (minCapacity > minExpand) { - ensureExplicitCapacity(minCapacity); - } - } - -``` - -**最好在 add 大量元素之前用 `ensureCapacity` 方法,以减少增量重新分配的次数** - -我们通过下面的代码实际测试以下这个方法的效果: - -```java -public class EnsureCapacityTest { - public static void main(String[] args) { - ArrayList list = new ArrayList(); - final int N = 10000000; - long startTime = System.currentTimeMillis(); - for (int i = 0; i < N; i++) { - list.add(i); - } - long endTime = System.currentTimeMillis(); - System.out.println("使用ensureCapacity方法前:"+(endTime - startTime)); - - } -} -``` - -运行结果: - -``` -使用ensureCapacity方法前:2158 -``` - -```java -public class EnsureCapacityTest { - public static void main(String[] args) { - ArrayList list = new ArrayList(); - final int N = 10000000; - list = new ArrayList(); - long startTime1 = System.currentTimeMillis(); - list.ensureCapacity(N); - for (int i = 0; i < N; i++) { - list.add(i); - } - long endTime1 = System.currentTimeMillis(); - System.out.println("使用ensureCapacity方法后:"+(endTime1 - startTime1)); - } -} -``` - -运行结果: - -``` - -使用ensureCapacity方法前:1773 -``` - -通过运行结果,我们可以看出向 ArrayList 添加大量元素之前最好先使用`ensureCapacity` 方法,以减少增量重新分配的次数。 diff --git a/docs/java/collection/ArrayList.md b/docs/java/collection/ArrayList.md deleted file mode 100644 index f6578a7..0000000 --- a/docs/java/collection/ArrayList.md +++ /dev/null @@ -1,737 +0,0 @@ - - -- [ArrayList简介](#arraylist简介) -- [ArrayList核心源码](#arraylist核心源码) -- [ArrayList源码分析](#arraylist源码分析) - - [System.arraycopy\(\)和Arrays.copyOf\(\)方法](#systemarraycopy和arrayscopyof方法) - - [两者联系与区别](#两者联系与区别) - - [ArrayList核心扩容技术](#arraylist核心扩容技术) - - [内部类](#内部类) -- [ArrayList经典Demo](#arraylist经典demo) - - - - -### ArrayList简介 -  ArrayList 的底层是数组队列,相当于动态数组。与 Java 中的数组相比,它的容量能动态增长。在添加大量元素前,应用程序可以使用`ensureCapacity`操作来增加 ArrayList 实例的容量。这可以减少递增式再分配的数量。 - - 它继承于 **AbstractList**,实现了 **List**, **RandomAccess**, **Cloneable**, **java.io.Serializable** 这些接口。 - - 在我们学数据结构的时候就知道了线性表的顺序存储,插入删除元素的时间复杂度为**O(n)**,求表长以及增加元素,取第 i 元素的时间复杂度为**O(1)** - -  ArrayList 继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。 - -  ArrayList 实现了**RandomAccess 接口**, RandomAccess 是一个标志接口,表明实现这个这个接口的 List 集合是支持**快速随机访问**的。在 ArrayList 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。 - -  ArrayList 实现了**Cloneable 接口**,即覆盖了函数 clone(),**能被克隆**。 - -  ArrayList 实现**java.io.Serializable 接口**,这意味着ArrayList**支持序列化**,**能通过序列化去传输**。 - -  和 Vector 不同,**ArrayList 中的操作不是线程安全的**!所以,建议在单线程中才使用 ArrayList,而在多线程中可以选择 Vector 或者 CopyOnWriteArrayList。 -### ArrayList核心源码 - -```java -package java.util; - -import java.util.function.Consumer; -import java.util.function.Predicate; -import java.util.function.UnaryOperator; - - -public class ArrayList extends AbstractList - implements List, RandomAccess, Cloneable, java.io.Serializable -{ - private static final long serialVersionUID = 8683452581122892189L; - - /** - * 默认初始容量大小 - */ - private static final int DEFAULT_CAPACITY = 10; - - /** - * 空数组(用于空实例)。 - */ - private static final Object[] EMPTY_ELEMENTDATA = {}; - - //用于默认大小空实例的共享空数组实例。 - //我们把它从EMPTY_ELEMENTDATA数组中区分出来,以知道在添加第一个元素时容量需要增加多少。 - private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; - - /** - * 保存ArrayList数据的数组 - */ - transient Object[] elementData; // non-private to simplify nested class access - - /** - * ArrayList 所包含的元素个数 - */ - private int size; - - /** - * 带初始容量参数的构造函数。(用户自己指定容量) - */ - public ArrayList(int initialCapacity) { - if (initialCapacity > 0) { - //创建initialCapacity大小的数组 - this.elementData = new Object[initialCapacity]; - } else if (initialCapacity == 0) { - //创建空数组 - this.elementData = EMPTY_ELEMENTDATA; - } else { - throw new IllegalArgumentException("Illegal Capacity: "+ - initialCapacity); - } - } - - /** - *默认构造函数,DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为0.初始化为10,也就是说初始其实是空数组 当添加第一个元素的时候数组容量才变成10 - */ - public ArrayList() { - this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; - } - - /** - * 构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序。 - */ - public ArrayList(Collection c) { - // - elementData = c.toArray(); - //如果指定集合元素个数不为0 - if ((size = elementData.length) != 0) { - // c.toArray 可能返回的不是Object类型的数组所以加上下面的语句用于判断, - //这里用到了反射里面的getClass()方法 - if (elementData.getClass() != Object[].class) - elementData = Arrays.copyOf(elementData, size, Object[].class); - } else { - // 用空数组代替 - this.elementData = EMPTY_ELEMENTDATA; - } - } - - /** - * 修改这个ArrayList实例的容量是列表的当前大小。 应用程序可以使用此操作来最小化ArrayList实例的存储。 - */ - public void trimToSize() { - modCount++; - if (size < elementData.length) { - elementData = (size == 0) - ? EMPTY_ELEMENTDATA - : Arrays.copyOf(elementData, size); - } - } -//下面是ArrayList的扩容机制 -//ArrayList的扩容机制提高了性能,如果每次只扩充一个, -//那么频繁的插入会导致频繁的拷贝,降低性能,而ArrayList的扩容机制避免了这种情况。 - /** - * 如有必要,增加此ArrayList实例的容量,以确保它至少能容纳元素的数量 - * @param minCapacity 所需的最小容量 - */ - public void ensureCapacity(int minCapacity) { - int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) - // any size if not default element table - ? 0 - // larger than default for default empty table. It's already - // supposed to be at default size. - : DEFAULT_CAPACITY; - - if (minCapacity > minExpand) { - ensureExplicitCapacity(minCapacity); - } - } - //得到最小扩容量 - private void ensureCapacityInternal(int minCapacity) { - if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { - // 获取默认的容量和传入参数的较大值 - minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); - } - - ensureExplicitCapacity(minCapacity); - } - //判断是否需要扩容 - private void ensureExplicitCapacity(int minCapacity) { - modCount++; - - // overflow-conscious code - if (minCapacity - elementData.length > 0) - //调用grow方法进行扩容,调用此方法代表已经开始扩容了 - grow(minCapacity); - } - - /** - * 要分配的最大数组大小 - */ - private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; - - /** - * ArrayList扩容的核心方法。 - */ - private void grow(int minCapacity) { - // oldCapacity为旧容量,newCapacity为新容量 - int oldCapacity = elementData.length; - //将oldCapacity 右移一位,其效果相当于oldCapacity /2, - //我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍, - int newCapacity = oldCapacity + (oldCapacity >> 1); - //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量, - if (newCapacity - minCapacity < 0) - newCapacity = minCapacity; - //再检查新容量是否超出了ArrayList所定义的最大容量, - //若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE, - //如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Interger.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。 - if (newCapacity - MAX_ARRAY_SIZE > 0) - newCapacity = hugeCapacity(minCapacity); - // minCapacity is usually close to size, so this is a win: - elementData = Arrays.copyOf(elementData, newCapacity); - } - //比较minCapacity和 MAX_ARRAY_SIZE - private static int hugeCapacity(int minCapacity) { - if (minCapacity < 0) // overflow - throw new OutOfMemoryError(); - return (minCapacity > MAX_ARRAY_SIZE) ? - Integer.MAX_VALUE : - MAX_ARRAY_SIZE; - } - - /** - *返回此列表中的元素数。 - */ - public int size() { - return size; - } - - /** - * 如果此列表不包含元素,则返回 true 。 - */ - public boolean isEmpty() { - //注意=和==的区别 - return size == 0; - } - - /** - * 如果此列表包含指定的元素,则返回true 。 - */ - public boolean contains(Object o) { - //indexOf()方法:返回此列表中指定元素的首次出现的索引,如果此列表不包含此元素,则为-1 - return indexOf(o) >= 0; - } - - /** - *返回此列表中指定元素的首次出现的索引,如果此列表不包含此元素,则为-1 - */ - public int indexOf(Object o) { - if (o == null) { - for (int i = 0; i < size; i++) - if (elementData[i]==null) - return i; - } else { - for (int i = 0; i < size; i++) - //equals()方法比较 - if (o.equals(elementData[i])) - return i; - } - return -1; - } - - /** - * 返回此列表中指定元素的最后一次出现的索引,如果此列表不包含元素,则返回-1。. - */ - public int lastIndexOf(Object o) { - if (o == null) { - for (int i = size-1; i >= 0; i--) - if (elementData[i]==null) - return i; - } else { - for (int i = size-1; i >= 0; i--) - if (o.equals(elementData[i])) - return i; - } - return -1; - } - - /** - * 返回此ArrayList实例的浅拷贝。 (元素本身不被复制。) - */ - public Object clone() { - try { - ArrayList v = (ArrayList) super.clone(); - //Arrays.copyOf功能是实现数组的复制,返回复制后的数组。参数是被复制的数组和复制的长度 - v.elementData = Arrays.copyOf(elementData, size); - v.modCount = 0; - return v; - } catch (CloneNotSupportedException e) { - // 这不应该发生,因为我们是可以克隆的 - throw new InternalError(e); - } - } - - /** - *以正确的顺序(从第一个到最后一个元素)返回一个包含此列表中所有元素的数组。 - *返回的数组将是“安全的”,因为该列表不保留对它的引用。 (换句话说,这个方法必须分配一个新的数组)。 - *因此,调用者可以自由地修改返回的数组。 此方法充当基于阵列和基于集合的API之间的桥梁。 - */ - public Object[] toArray() { - return Arrays.copyOf(elementData, size); - } - - /** - * 以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素); - *返回的数组的运行时类型是指定数组的运行时类型。 如果列表适合指定的数组,则返回其中。 - *否则,将为指定数组的运行时类型和此列表的大小分配一个新数组。 - *如果列表适用于指定的数组,其余空间(即数组的列表数量多于此元素),则紧跟在集合结束后的数组中的元素设置为null 。 - *(这仅在调用者知道列表不包含任何空元素的情况下才能确定列表的长度。) - */ - @SuppressWarnings("unchecked") - public T[] toArray(T[] a) { - if (a.length < size) - // 新建一个运行时类型的数组,但是ArrayList数组的内容 - return (T[]) Arrays.copyOf(elementData, size, a.getClass()); - //调用System提供的arraycopy()方法实现数组之间的复制 - System.arraycopy(elementData, 0, a, 0, size); - if (a.length > size) - a[size] = null; - return a; - } - - // Positional Access Operations - - @SuppressWarnings("unchecked") - E elementData(int index) { - return (E) elementData[index]; - } - - /** - * 返回此列表中指定位置的元素。 - */ - public E get(int index) { - rangeCheck(index); - - return elementData(index); - } - - /** - * 用指定的元素替换此列表中指定位置的元素。 - */ - public E set(int index, E element) { - //对index进行界限检查 - rangeCheck(index); - - E oldValue = elementData(index); - elementData[index] = element; - //返回原来在这个位置的元素 - return oldValue; - } - - /** - * 将指定的元素追加到此列表的末尾。 - */ - public boolean add(E e) { - ensureCapacityInternal(size + 1); // Increments modCount!! - //这里看到ArrayList添加元素的实质就相当于为数组赋值 - elementData[size++] = e; - return true; - } - - /** - * 在此列表中的指定位置插入指定的元素。 - *先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大; - *再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。 - */ - public void add(int index, E element) { - rangeCheckForAdd(index); - - ensureCapacityInternal(size + 1); // Increments modCount!! - //arraycopy()这个实现数组之间复制的方法一定要看一下,下面就用到了arraycopy()方法实现数组自己复制自己 - System.arraycopy(elementData, index, elementData, index + 1, - size - index); - elementData[index] = element; - size++; - } - - /** - * 删除该列表中指定位置的元素。 将任何后续元素移动到左侧(从其索引中减去一个元素)。 - */ - public E remove(int index) { - rangeCheck(index); - - modCount++; - E oldValue = elementData(index); - - int numMoved = size - index - 1; - if (numMoved > 0) - System.arraycopy(elementData, index+1, elementData, index, - numMoved); - elementData[--size] = null; // clear to let GC do its work - //从列表中删除的元素 - return oldValue; - } - - /** - * 从列表中删除指定元素的第一个出现(如果存在)。 如果列表不包含该元素,则它不会更改。 - *返回true,如果此列表包含指定的元素 - */ - public boolean remove(Object o) { - if (o == null) { - for (int index = 0; index < size; index++) - if (elementData[index] == null) { - fastRemove(index); - return true; - } - } else { - for (int index = 0; index < size; index++) - if (o.equals(elementData[index])) { - fastRemove(index); - return true; - } - } - return false; - } - - /* - * Private remove method that skips bounds checking and does not - * return the value removed. - */ - private void fastRemove(int index) { - modCount++; - int numMoved = size - index - 1; - if (numMoved > 0) - System.arraycopy(elementData, index+1, elementData, index, - numMoved); - elementData[--size] = null; // clear to let GC do its work - } - - /** - * 从列表中删除所有元素。 - */ - public void clear() { - modCount++; - - // 把数组中所有的元素的值设为null - for (int i = 0; i < size; i++) - elementData[i] = null; - - size = 0; - } - - /** - * 按指定集合的Iterator返回的顺序将指定集合中的所有元素追加到此列表的末尾。 - */ - public boolean addAll(Collection c) { - Object[] a = c.toArray(); - int numNew = a.length; - ensureCapacityInternal(size + numNew); // Increments modCount - System.arraycopy(a, 0, elementData, size, numNew); - size += numNew; - return numNew != 0; - } - - /** - * 将指定集合中的所有元素插入到此列表中,从指定的位置开始。 - */ - public boolean addAll(int index, Collection c) { - rangeCheckForAdd(index); - - Object[] a = c.toArray(); - int numNew = a.length; - ensureCapacityInternal(size + numNew); // Increments modCount - - int numMoved = size - index; - if (numMoved > 0) - System.arraycopy(elementData, index, elementData, index + numNew, - numMoved); - - System.arraycopy(a, 0, elementData, index, numNew); - size += numNew; - return numNew != 0; - } - - /** - * 从此列表中删除所有索引为fromIndex (含)和toIndex之间的元素。 - *将任何后续元素移动到左侧(减少其索引)。 - */ - protected void removeRange(int fromIndex, int toIndex) { - modCount++; - int numMoved = size - toIndex; - System.arraycopy(elementData, toIndex, elementData, fromIndex, - numMoved); - - // clear to let GC do its work - int newSize = size - (toIndex-fromIndex); - for (int i = newSize; i < size; i++) { - elementData[i] = null; - } - size = newSize; - } - - /** - * 检查给定的索引是否在范围内。 - */ - private void rangeCheck(int index) { - if (index >= size) - throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); - } - - /** - * add和addAll使用的rangeCheck的一个版本 - */ - private void rangeCheckForAdd(int index) { - if (index > size || index < 0) - throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); - } - - /** - * 返回IndexOutOfBoundsException细节信息 - */ - private String outOfBoundsMsg(int index) { - return "Index: "+index+", Size: "+size; - } - - /** - * 从此列表中删除指定集合中包含的所有元素。 - */ - public boolean removeAll(Collection c) { - Objects.requireNonNull(c); - //如果此列表被修改则返回true - return batchRemove(c, false); - } - - /** - * 仅保留此列表中包含在指定集合中的元素。 - *换句话说,从此列表中删除其中不包含在指定集合中的所有元素。 - */ - public boolean retainAll(Collection c) { - Objects.requireNonNull(c); - return batchRemove(c, true); - } - - - /** - * 从列表中的指定位置开始,返回列表中的元素(按正确顺序)的列表迭代器。 - *指定的索引表示初始调用将返回的第一个元素为next 。 初始调用previous将返回指定索引减1的元素。 - *返回的列表迭代器是fail-fast 。 - */ - public ListIterator listIterator(int index) { - if (index < 0 || index > size) - throw new IndexOutOfBoundsException("Index: "+index); - return new ListItr(index); - } - - /** - *返回列表中的列表迭代器(按适当的顺序)。 - *返回的列表迭代器是fail-fast 。 - */ - public ListIterator listIterator() { - return new ListItr(0); - } - - /** - *以正确的顺序返回该列表中的元素的迭代器。 - *返回的迭代器是fail-fast 。 - */ - public Iterator iterator() { - return new Itr(); - } - - -``` -### ArrayList源码分析 -#### System.arraycopy()和Arrays.copyOf()方法 -  通过上面源码我们发现这两个实现数组复制的方法被广泛使用而且很多地方都特别巧妙。比如下面add(int index, E element)方法就很巧妙的用到了arraycopy()方法让数组自己复制自己实现让index开始之后的所有成员后移一个位置: -```java - /** - * 在此列表中的指定位置插入指定的元素。 - *先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大; - *再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。 - */ - public void add(int index, E element) { - rangeCheckForAdd(index); - - ensureCapacityInternal(size + 1); // Increments modCount!! - //arraycopy()方法实现数组自己复制自己 - //elementData:源数组;index:源数组中的起始位置;elementData:目标数组;index + 1:目标数组中的起始位置; size - index:要复制的数组元素的数量; - System.arraycopy(elementData, index, elementData, index + 1, size - index); - elementData[index] = element; - size++; - } -``` -又如toArray()方法中用到了copyOf()方法 -```java - - /** - *以正确的顺序(从第一个到最后一个元素)返回一个包含此列表中所有元素的数组。 - *返回的数组将是“安全的”,因为该列表不保留对它的引用。 (换句话说,这个方法必须分配一个新的数组)。 - *因此,调用者可以自由地修改返回的数组。 此方法充当基于阵列和基于集合的API之间的桥梁。 - */ - public Object[] toArray() { - //elementData:要复制的数组;size:要复制的长度 - return Arrays.copyOf(elementData, size); - } -``` -##### 两者联系与区别 -**联系:** -看两者源代码可以发现`copyOf()`内部调用了`System.arraycopy()`方法 -**区别:** -1. arraycopy()需要目标数组,将原数组拷贝到你自己定义的数组里,而且可以选择拷贝的起点和长度以及放入新数组中的位置 -2. copyOf()是系统自动在内部新建一个数组,并返回该数组。 -#### ArrayList 核心扩容技术 -```java -//下面是ArrayList的扩容机制 -//ArrayList的扩容机制提高了性能,如果每次只扩充一个, -//那么频繁的插入会导致频繁的拷贝,降低性能,而ArrayList的扩容机制避免了这种情况。 - /** - * 如有必要,增加此ArrayList实例的容量,以确保它至少能容纳元素的数量 - * @param minCapacity 所需的最小容量 - */ - public void ensureCapacity(int minCapacity) { - int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) - // any size if not default element table - ? 0 - // larger than default for default empty table. It's already - // supposed to be at default size. - : DEFAULT_CAPACITY; - - if (minCapacity > minExpand) { - ensureExplicitCapacity(minCapacity); - } - } - //得到最小扩容量 - private void ensureCapacityInternal(int minCapacity) { - if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { - // 获取默认的容量和传入参数的较大值 - minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); - } - - ensureExplicitCapacity(minCapacity); - } - //判断是否需要扩容,上面两个方法都要调用 - private void ensureExplicitCapacity(int minCapacity) { - modCount++; - - // 如果说minCapacity也就是所需的最小容量大于保存ArrayList数据的数组的长度的话,就需要调用grow(minCapacity)方法扩容。 - //这个minCapacity到底为多少呢?举个例子在添加元素(add)方法中这个minCapacity的大小就为现在数组的长度加1 - if (minCapacity - elementData.length > 0) - //调用grow方法进行扩容,调用此方法代表已经开始扩容了 - grow(minCapacity); - } - -``` -```java - /** - * ArrayList扩容的核心方法。 - */ - private void grow(int minCapacity) { - //elementData为保存ArrayList数据的数组 - ///elementData.length求数组长度elementData.size是求数组中的元素个数 - // oldCapacity为旧容量,newCapacity为新容量 - int oldCapacity = elementData.length; - //将oldCapacity 右移一位,其效果相当于oldCapacity /2, - //我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍, - int newCapacity = oldCapacity + (oldCapacity >> 1); - //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量, - if (newCapacity - minCapacity < 0) - newCapacity = minCapacity; - //再检查新容量是否超出了ArrayList所定义的最大容量, - //若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE, - //如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Interger.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。 - if (newCapacity - MAX_ARRAY_SIZE > 0) - newCapacity = hugeCapacity(minCapacity); - // minCapacity is usually close to size, so this is a win: - elementData = Arrays.copyOf(elementData, newCapacity); - } - -``` -  扩容机制代码已经做了详细的解释。另外值得注意的是大家很容易忽略的一个运算符:**移位运算符** -  **简介**:移位运算符就是在二进制的基础上对数字进行平移。按照平移的方向和填充数字的规则分为三种:<<(左移)>>(带符号右移)>>>(无符号右移)。 -  **作用**:**对于大数据的2进制运算,位移运算符比那些普通运算符的运算要快很多,因为程序仅仅移动一下而已,不去计算,这样提高了效率,节省了资源** -  比如这里:int newCapacity = oldCapacity + (oldCapacity >> 1); -右移一位相当于除2,右移n位相当于除以 2 的 n 次方。这里 oldCapacity 明显右移了1位所以相当于oldCapacity /2。 - -**另外需要注意的是:** - -1. java 中的**length 属性**是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性. - -2. java 中的**length()方法**是针对字 符串String说的,如果想看这个字符串的长度则用到 length()这个方法. - -3. .java 中的**size()方法**是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看! - - -#### 内部类 -```java - (1)private class Itr implements Iterator - (2)private class ListItr extends Itr implements ListIterator - (3)private class SubList extends AbstractList implements RandomAccess - (4)static final class ArrayListSpliterator implements Spliterator -``` -  ArrayList有四个内部类,其中的**Itr是实现了Iterator接口**,同时重写了里面的**hasNext()**, **next()**, **remove()** 等方法;其中的**ListItr** 继承 **Itr**,实现了**ListIterator接口**,同时重写了**hasPrevious()**, **nextIndex()**, **previousIndex()**, **previous()**, **set(E e)**, **add(E e)** 等方法,所以这也可以看出了 **Iterator和ListIterator的区别:** ListIterator在Iterator的基础上增加了添加对象,修改对象,逆向遍历等方法,这些是Iterator不能实现的。 -### ArrayList经典Demo - -```java -package list; -import java.util.ArrayList; -import java.util.Iterator; - -public class ArrayListDemo { - - public static void main(String[] srgs){ - ArrayList arrayList = new ArrayList(); - - System.out.printf("Before add:arrayList.size() = %d\n",arrayList.size()); - - arrayList.add(1); - arrayList.add(3); - arrayList.add(5); - arrayList.add(7); - arrayList.add(9); - System.out.printf("After add:arrayList.size() = %d\n",arrayList.size()); - - System.out.println("Printing elements of arrayList"); - // 三种遍历方式打印元素 - // 第一种:通过迭代器遍历 - System.out.print("通过迭代器遍历:"); - Iterator it = arrayList.iterator(); - while(it.hasNext()){ - System.out.print(it.next() + " "); - } - System.out.println(); - - // 第二种:通过索引值遍历 - System.out.print("通过索引值遍历:"); - for(int i = 0; i < arrayList.size(); i++){ - System.out.print(arrayList.get(i) + " "); - } - System.out.println(); - - // 第三种:for循环遍历 - System.out.print("for循环遍历:"); - for(Integer number : arrayList){ - System.out.print(number + " "); - } - - // toArray用法 - // 第一种方式(最常用) - Integer[] integer = arrayList.toArray(new Integer[0]); - - // 第二种方式(容易理解) - Integer[] integer1 = new Integer[arrayList.size()]; - arrayList.toArray(integer1); - - // 抛出异常,java不支持向下转型 - //Integer[] integer2 = new Integer[arrayList.size()]; - //integer2 = arrayList.toArray(); - System.out.println(); - - // 在指定位置添加元素 - arrayList.add(2,2); - // 删除指定位置上的元素 - arrayList.remove(2); - // 删除指定元素 - arrayList.remove((Object)3); - // 判断arrayList是否包含5 - System.out.println("ArrayList contains 5 is: " + arrayList.contains(5)); - - // 清空ArrayList - arrayList.clear(); - // 判断ArrayList是否为空 - System.out.println("ArrayList is empty: " + arrayList.isEmpty()); - } -} -``` - diff --git a/docs/java/collection/HashMap.md b/docs/java/collection/HashMap.md deleted file mode 100644 index c850cd5..0000000 --- a/docs/java/collection/HashMap.md +++ /dev/null @@ -1,542 +0,0 @@ - - -- [HashMap 简介](#hashmap-简介) -- [底层数据结构分析](#底层数据结构分析) - - [JDK1.8之前](#jdk18之前) - - [JDK1.8之后](#jdk18之后) -- [HashMap源码分析](#hashmap源码分析) - - [构造方法](#构造方法) - - [put方法](#put方法) - - [get方法](#get方法) - - [resize方法](#resize方法) -- [HashMap常用方法测试](#hashmap常用方法测试) - - - -> 感谢 [changfubai](https://github.com/changfubai) 对本文的改进做出的贡献! - -## HashMap 简介 -HashMap 主要用来存放键值对,它基于哈希表的Map接口实现,是常用的Java集合之一。 - -JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间,具体可以参考 `treeifyBin`方法。 - -## 底层数据结构分析 -### JDK1.8之前 -JDK1.8 之前 HashMap 底层是 **数组和链表** 结合在一起使用也就是 **链表散列**。**HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 `(n - 1) & hash` 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。** - -**所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。** - -**JDK 1.8 HashMap 的 hash 方法源码:** - -JDK 1.8 的 hash方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。 - - ```java - static final int hash(Object key) { - int h; - // key.hashCode():返回散列值也就是hashcode - // ^ :按位异或 - // >>>:无符号右移,忽略符号位,空位都以0补齐 - return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); - } - ``` -对比一下 JDK1.7的 HashMap 的 hash 方法源码. - -```java -static int hash(int h) { - // This function ensures that hashCodes that differ only by - // constant multiples at each bit position have a bounded - // number of collisions (approximately 8 at default load factor). - - h ^= (h >>> 20) ^ (h >>> 12); - return h ^ (h >>> 7) ^ (h >>> 4); -} -``` - -相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。 - -所谓 **“拉链法”** 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。 - -![jdk1.8之前的内部结构](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/jdk1.8之前的内部结构.png) - -### JDK1.8之后 -相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。 - -![JDK1.8之后的HashMap底层数据结构](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-8-22/67233764.jpg) - -**类的属性:** -```java -public class HashMap extends AbstractMap implements Map, Cloneable, Serializable { - // 序列号 - private static final long serialVersionUID = 362498820763181265L; - // 默认的初始容量是16 - static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; - // 最大容量 - static final int MAXIMUM_CAPACITY = 1 << 30; - // 默认的填充因子 - static final float DEFAULT_LOAD_FACTOR = 0.75f; - // 当桶(bucket)上的结点数大于这个值时会转成红黑树 - static final int TREEIFY_THRESHOLD = 8; - // 当桶(bucket)上的结点数小于这个值时树转链表 - static final int UNTREEIFY_THRESHOLD = 6; - // 桶中结构转化为红黑树对应的table的最小大小 - static final int MIN_TREEIFY_CAPACITY = 64; - // 存储元素的数组,总是2的幂次倍 - transient Node[] table; - // 存放具体元素的集 - transient Set> entrySet; - // 存放元素的个数,注意这个不等于数组的长度。 - transient int size; - // 每次扩容和更改map结构的计数器 - transient int modCount; - // 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容 - int threshold; - // 加载因子 - final float loadFactor; -} -``` -- **loadFactor加载因子** - - loadFactor加载因子是控制数组存放数据的疏密程度,loadFactor越趋近于1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor越小,也就是趋近于0,数组中存放的数据(entry)也就越少,也就越稀疏。 - - **loadFactor太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor的默认值为0.75f是官方给出的一个比较好的临界值**。 - - 给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。 - -- **threshold** - - **threshold = capacity * loadFactor**,**当Size>=threshold**的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是 **衡量数组是否需要扩增的一个标准**。 - -**Node节点类源码:** - -```java -// 继承自 Map.Entry -static class Node implements Map.Entry { - final int hash;// 哈希值,存放元素到hashmap中时用来与其他元素hash值比较 - final K key;//键 - V value;//值 - // 指向下一个节点 - Node next; - Node(int hash, K key, V value, Node next) { - this.hash = hash; - this.key = key; - this.value = value; - this.next = next; - } - public final K getKey() { return key; } - public final V getValue() { return value; } - public final String toString() { return key + "=" + value; } - // 重写hashCode()方法 - public final int hashCode() { - return Objects.hashCode(key) ^ Objects.hashCode(value); - } - - public final V setValue(V newValue) { - V oldValue = value; - value = newValue; - return oldValue; - } - // 重写 equals() 方法 - public final boolean equals(Object o) { - if (o == this) - return true; - if (o instanceof Map.Entry) { - Map.Entry e = (Map.Entry)o; - if (Objects.equals(key, e.getKey()) && - Objects.equals(value, e.getValue())) - return true; - } - return false; - } -} -``` -**树节点类源码:** -```java -static final class TreeNode extends LinkedHashMap.Entry { - TreeNode parent; // 父 - TreeNode left; // 左 - TreeNode right; // 右 - TreeNode prev; // needed to unlink next upon deletion - boolean red; // 判断颜色 - TreeNode(int hash, K key, V val, Node next) { - super(hash, key, val, next); - } - // 返回根节点 - final TreeNode root() { - for (TreeNode r = this, p;;) { - if ((p = r.parent) == null) - return r; - r = p; - } -``` -## HashMap源码分析 -### 构造方法 - -HashMap 中有四个构造方法,它们分别如下: - -```java - // 默认构造函数。 - public HashMap() { - this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted - } - - // 包含另一个“Map”的构造函数 - public HashMap(Map m) { - this.loadFactor = DEFAULT_LOAD_FACTOR; - putMapEntries(m, false);//下面会分析到这个方法 - } - - // 指定“容量大小”的构造函数 - public HashMap(int initialCapacity) { - this(initialCapacity, DEFAULT_LOAD_FACTOR); - } - - // 指定“容量大小”和“加载因子”的构造函数 - public HashMap(int initialCapacity, float loadFactor) { - if (initialCapacity < 0) - throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); - if (initialCapacity > MAXIMUM_CAPACITY) - initialCapacity = MAXIMUM_CAPACITY; - if (loadFactor <= 0 || Float.isNaN(loadFactor)) - throw new IllegalArgumentException("Illegal load factor: " + loadFactor); - this.loadFactor = loadFactor; - this.threshold = tableSizeFor(initialCapacity); - } -``` - -**putMapEntries方法:** - -```java -final void putMapEntries(Map m, boolean evict) { - int s = m.size(); - if (s > 0) { - // 判断table是否已经初始化 - if (table == null) { // pre-size - // 未初始化,s为m的实际元素个数 - float ft = ((float)s / loadFactor) + 1.0F; - int t = ((ft < (float)MAXIMUM_CAPACITY) ? - (int)ft : MAXIMUM_CAPACITY); - // 计算得到的t大于阈值,则初始化阈值 - if (t > threshold) - threshold = tableSizeFor(t); - } - // 已初始化,并且m元素个数大于阈值,进行扩容处理 - else if (s > threshold) - resize(); - // 将m中的所有元素添加至HashMap中 - for (Map.Entry e : m.entrySet()) { - K key = e.getKey(); - V value = e.getValue(); - putVal(hash(key), key, value, false, evict); - } - } -} -``` -### put方法 -HashMap只提供了put用于添加元素,putVal方法只是给put方法调用的一个方法,并没有提供给用户使用。 - -**对putVal方法添加元素的分析如下:** - -- ①如果定位到的数组位置没有元素 就直接插入。 -- ②如果定位到的数组位置有元素就和要插入的key比较,如果key相同就直接覆盖,如果key不相同,就判断p是否是一个树节点,如果是就调用`e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value)`将元素添加进入。如果不是就遍历链表插入(插入的是链表尾部)。 - -ps:下图有一个小问题,来自 [issue#608](https://github.com/Snailclimb/JavaGuide/issues/608)指出:直接覆盖之后应该就会 return,不会有后续操作。参考 JDK8 HashMap.java 658 行。 - -![put方法](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/put方法.png) - -```java -public V put(K key, V value) { - return putVal(hash(key), key, value, false, true); -} - -final V putVal(int hash, K key, V value, boolean onlyIfAbsent, - boolean evict) { - Node[] tab; Node p; int n, i; - // table未初始化或者长度为0,进行扩容 - if ((tab = table) == null || (n = tab.length) == 0) - n = (tab = resize()).length; - // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中) - if ((p = tab[i = (n - 1) & hash]) == null) - tab[i] = newNode(hash, key, value, null); - // 桶中已经存在元素 - else { - Node e; K k; - // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等 - if (p.hash == hash && - ((k = p.key) == key || (key != null && key.equals(k)))) - // 将第一个元素赋值给e,用e来记录 - e = p; - // hash值不相等,即key不相等;为红黑树结点 - 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); - // 结点数量达到阈值,转化为红黑树 - 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值与插入元素相等的结点 - 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; -} -``` - -**我们再来对比一下 JDK1.7 put方法的代码** - -**对于put方法的分析如下:** - -- ①如果定位到的数组位置没有元素 就直接插入。 -- ②如果定位到的数组位置有元素,遍历以这个元素为头结点的链表,依次和插入的key比较,如果key相同就直接覆盖,不同就采用头插法插入元素。 - -```java -public V put(K key, V value) - if (table == EMPTY_TABLE) { - inflateTable(threshold); -} - if (key == null) - return putForNullKey(value); - int hash = hash(key); - int i = indexFor(hash, table.length); - for (Entry e = table[i]; e != null; e = e.next) { // 先遍历 - Object k; - if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { - V oldValue = e.value; - e.value = value; - e.recordAccess(this); - return oldValue; - } - } - - modCount++; - addEntry(hash, key, value, i); // 再插入 - return null; -} -``` - - - -### get方法 -```java -public V get(Object key) { - Node e; - return (e = getNode(hash(key), key)) == null ? null : e.value; -} - -final Node getNode(int hash, Object key) { - Node[] tab; Node first, e; int n; K k; - if ((tab = table) != null && (n = tab.length) > 0 && - (first = tab[(n - 1) & hash]) != null) { - // 数组元素相等 - if (first.hash == hash && // always check first node - ((k = first.key) == key || (key != null && key.equals(k)))) - return first; - // 桶中不止一个节点 - if ((e = first.next) != null) { - // 在树中get - if (first instanceof TreeNode) - return ((TreeNode)first).getTreeNode(hash, key); - // 在链表中get - do { - if (e.hash == hash && - ((k = e.key) == key || (key != null && key.equals(k)))) - return e; - } while ((e = e.next) != null); - } - } - return null; -} -``` -### resize方法 -进行扩容,会伴随着一次重新hash分配,并且会遍历hash表中所有的元素,是非常耗时的。在编写程序中,要尽量避免resize。 -```java -final Node[] resize() { - Node[] oldTab = table; - int oldCap = (oldTab == null) ? 0 : oldTab.length; - int oldThr = threshold; - int newCap, newThr = 0; - if (oldCap > 0) { - // 超过最大值就不再扩充了,就只好随你碰撞去吧 - if (oldCap >= MAXIMUM_CAPACITY) { - threshold = Integer.MAX_VALUE; - return oldTab; - } - // 没超过最大值,就扩充为原来的2倍 - else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) - newThr = oldThr << 1; // double threshold - } - else if (oldThr > 0) // initial capacity was placed in threshold - newCap = oldThr; - else { - // signifies using defaults - newCap = DEFAULT_INITIAL_CAPACITY; - newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); - } - // 计算新的resize上限 - if (newThr == 0) { - float ft = (float)newCap * loadFactor; - newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); - } - threshold = newThr; - @SuppressWarnings({"rawtypes","unchecked"}) - Node[] newTab = (Node[])new Node[newCap]; - table = newTab; - if (oldTab != null) { - // 把每个bucket都移动到新的buckets中 - for (int j = 0; j < oldCap; ++j) { - Node e; - if ((e = oldTab[j]) != null) { - oldTab[j] = null; - if (e.next == null) - newTab[e.hash & (newCap - 1)] = e; - else if (e instanceof TreeNode) - ((TreeNode)e).split(this, newTab, j, oldCap); - else { - Node loHead = null, loTail = null; - Node hiHead = null, hiTail = null; - Node next; - do { - next = e.next; - // 原索引 - if ((e.hash & oldCap) == 0) { - if (loTail == null) - loHead = e; - else - loTail.next = e; - loTail = e; - } - // 原索引+oldCap - else { - if (hiTail == null) - hiHead = e; - else - hiTail.next = e; - hiTail = e; - } - } while ((e = next) != null); - // 原索引放到bucket里 - if (loTail != null) { - loTail.next = null; - newTab[j] = loHead; - } - // 原索引+oldCap放到bucket里 - if (hiTail != null) { - hiTail.next = null; - newTab[j + oldCap] = hiHead; - } - } - } - } - } - return newTab; -} -``` -## HashMap常用方法测试 -```java -package map; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Set; - -public class HashMapDemo { - - public static void main(String[] args) { - HashMap map = new HashMap(); - // 键不能重复,值可以重复 - map.put("san", "张三"); - map.put("si", "李四"); - map.put("wu", "王五"); - map.put("wang", "老王"); - map.put("wang", "老王2");// 老王被覆盖 - map.put("lao", "老王"); - System.out.println("-------直接输出hashmap:-------"); - System.out.println(map); - /** - * 遍历HashMap - */ - // 1.获取Map中的所有键 - System.out.println("-------foreach获取Map中所有的键:------"); - Set keys = map.keySet(); - for (String key : keys) { - System.out.print(key+" "); - } - System.out.println();//换行 - // 2.获取Map中所有值 - System.out.println("-------foreach获取Map中所有的值:------"); - Collection values = map.values(); - for (String value : values) { - System.out.print(value+" "); - } - System.out.println();//换行 - // 3.得到key的值的同时得到key所对应的值 - System.out.println("-------得到key的值的同时得到key所对应的值:-------"); - Set keys2 = map.keySet(); - for (String key : keys2) { - System.out.print(key + ":" + map.get(key)+" "); - - } - /** - * 另外一种不常用的遍历方式 - */ - // 当我调用put(key,value)方法的时候,首先会把key和value封装到 - // Entry这个静态内部类对象中,把Entry对象再添加到数组中,所以我们想获取 - // map中的所有键值对,我们只要获取数组中的所有Entry对象,接下来 - // 调用Entry对象中的getKey()和getValue()方法就能获取键值对了 - Set> entrys = map.entrySet(); - for (java.util.Map.Entry entry : entrys) { - System.out.println(entry.getKey() + "--" + entry.getValue()); - } - - /** - * HashMap其他常用方法 - */ - System.out.println("after map.size():"+map.size()); - System.out.println("after map.isEmpty():"+map.isEmpty()); - System.out.println(map.remove("san")); - System.out.println("after map.remove():"+map); - System.out.println("after map.get(si):"+map.get("si")); - System.out.println("after map.containsKey(si):"+map.containsKey("si")); - System.out.println("after containsValue(李四):"+map.containsValue("李四")); - System.out.println(map.replace("si", "李四2")); - System.out.println("after map.replace(si, 李四2):"+map); - } - -} - -``` diff --git "a/docs/java/collection/Java\351\233\206\345\220\210\346\241\206\346\236\266\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230.md" "b/docs/java/collection/Java\351\233\206\345\220\210\346\241\206\346\236\266\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230.md" deleted file mode 100644 index c5280d5..0000000 --- "a/docs/java/collection/Java\351\233\206\345\220\210\346\241\206\346\236\266\345\270\270\350\247\201\351\235\242\350\257\225\351\242\230.md" +++ /dev/null @@ -1,456 +0,0 @@ -点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 - - - -- [剖析面试最常见问题之Java集合框架](#剖析面试最常见问题之java集合框架) - - [说说List,Set,Map三者的区别?](#说说listsetmap三者的区别) - - [Arraylist 与 LinkedList 区别?](#arraylist-与-linkedlist-区别) - - [补充内容:RandomAccess接口](#补充内容randomaccess接口) - - [补充内容:双向链表和双向循环链表](#补充内容双向链表和双向循环链表) - - [ArrayList 与 Vector 区别呢?为什么要用Arraylist取代Vector呢?](#arraylist-与-vector-区别呢为什么要用arraylist取代vector呢) - - [说一说 ArrayList 的扩容机制吧](#说一说-arraylist-的扩容机制吧) - - [HashMap 和 Hashtable 的区别](#hashmap-和-hashtable-的区别) - - [HashMap 和 HashSet区别](#hashmap-和-hashset区别) - - [HashSet如何检查重复](#hashset如何检查重复) - - [HashMap的底层实现](#hashmap的底层实现) - - [JDK1.8之前](#jdk18之前) - - [JDK1.8之后](#jdk18之后) - - [HashMap 的长度为什么是2的幂次方](#hashmap-的长度为什么是2的幂次方) - - [HashMap 多线程操作导致死循环问题](#hashmap-多线程操作导致死循环问题) - - [ConcurrentHashMap 和 Hashtable 的区别](#concurrenthashmap-和-hashtable-的区别) - - [ConcurrentHashMap线程安全的具体实现方式/底层具体实现](#concurrenthashmap线程安全的具体实现方式底层具体实现) - - [JDK1.7(上面有示意图)](#jdk17上面有示意图) - - [JDK1.8 (上面有示意图)](#jdk18-上面有示意图) - - [comparable 和 Comparator的区别](#comparable-和-comparator的区别) - - [Comparator定制排序](#comparator定制排序) - - [重写compareTo方法实现按年龄来排序](#重写compareto方法实现按年龄来排序) - - [集合框架底层数据结构总结](#集合框架底层数据结构总结) - - [Collection](#collection) - - [1. List](#1-list) - - [2. Set](#2-set) - - [Map](#map) - - [如何选用集合?](#如何选用集合) - - - -# 剖析面试最常见问题之Java集合框架 - -## 说说List,Set,Map三者的区别? - -- **List(对付顺序的好帮手):** List接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象 -- **Set(注重独一无二的性质):** 不允许重复的集合。不会有多个元素引用相同的对象。 -- **Map(用Key来搜索的专家):** 使用键值对存储。Map会维护与Key有关联的值。两个Key可以引用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何对象。 - -## Arraylist 与 LinkedList 区别? - -- **1. 是否保证线程安全:** `ArrayList` 和 `LinkedList` 都是不同步的,也就是不保证线程安全; - -- **2. 底层数据结构:** `Arraylist` 底层使用的是 **`Object` 数组**;`LinkedList` 底层使用的是 **双向链表** 数据结构(JDK1.6之前为循环链表,JDK1.7取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!) - -- **3. 插入和删除是否受元素位置的影响:** ① **`ArrayList` 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。** 比如:执行`add(E e) `方法的时候, `ArrayList` 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话(`add(int index, E element) `)时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② **`LinkedList` 采用链表存储,所以对于`add(E e)`方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置`i`插入和删除元素的话(`(add(int index, E element)`) 时间复杂度近似为`o(n))`因为需要先移动到指定位置再插入。** - -- **4. 是否支持快速随机访问:** `LinkedList` 不支持高效的随机元素访问,而 `ArrayList` 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于`get(int index) `方法)。 - -- **5. 内存空间占用:** ArrayList的空 间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。 - -### **补充内容:RandomAccess接口** - -```java -public interface RandomAccess { -} -``` - -查看源码我们发现实际上 `RandomAccess` 接口中什么都没有定义。所以,在我看来 `RandomAccess` 接口不过是一个标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。 - -在 `binarySearch(`)方法中,它要判断传入的list 是否 `RamdomAccess` 的实例,如果是,调用`indexedBinarySearch()`方法,如果不是,那么调用`iteratorBinarySearch()`方法 - -```java - public static - int binarySearch(List> list, T key) { - if (list instanceof RandomAccess || list.size() MAXIMUM_CAPACITY) - initialCapacity = MAXIMUM_CAPACITY; - if (loadFactor <= 0 || Float.isNaN(loadFactor)) - throw new IllegalArgumentException("Illegal load factor: " + - loadFactor); - this.loadFactor = loadFactor; - this.threshold = tableSizeFor(initialCapacity); - } - public HashMap(int initialCapacity) { - this(initialCapacity, DEFAULT_LOAD_FACTOR); - } -``` - -下面这个方法保证了 HashMap 总是使用2的幂作为哈希表的大小。 - -```java - /** - * Returns a power of two size for the given target capacity. - */ - static final int tableSizeFor(int cap) { - int n = cap - 1; - n |= n >>> 1; - n |= n >>> 2; - n |= n >>> 4; - n |= n >>> 8; - n |= n >>> 16; - return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; - } -``` - -## HashMap 和 HashSet区别 - -如果你看过 `HashSet` 源码的话就应该知道:HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常少,因为除了 `clone() `、`writeObject()`、`readObject()`是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。 - -| HashMap | HashSet | -| :------------------------------: | :----------------------------------------------------------: | -| 实现了Map接口 | 实现Set接口 | -| 存储键值对 | 仅存储对象 | -| 调用 `put()`向map中添加元素 | 调用 `add()`方法向Set中添加元素 | -| HashMap使用键(Key)计算Hashcode | HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性, | - -## HashSet如何检查重复 - -当你把对象加入`HashSet`时,HashSet会先计算对象的`hashcode`值来判断对象加入的位置,同时也会与其他加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同hashcode值的对象,这时会调用`equals()`方法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让加入操作成功。(摘自我的Java启蒙书《Head fist java》第二版) - -**hashCode()与equals()的相关规定:** - -1. 如果两个对象相等,则hashcode一定也是相同的 -2. 两个对象相等,对两个equals方法返回true -3. 两个对象有相同的hashcode值,它们也不一定是相等的 -4. 综上,equals方法被覆盖过,则hashCode方法也必须被覆盖 -5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。 - -**==与equals的区别** - -1. ==是判断两个变量或实例是不是指向同一个内存空间 equals是判断两个变量或实例所指向的内存空间的值是不是相同 -2. ==是指对内存地址进行比较 equals()是对字符串的内容进行比较 -3. ==指引用是否相同 equals()指的是值是否相同 - -## HashMap的底层实现 - -### JDK1.8之前 - -JDK1.8 之前 `HashMap` 底层是 **数组和链表** 结合在一起使用也就是 **链表散列**。**HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。** - -**所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。** - -**JDK 1.8 HashMap 的 hash 方法源码:** - -JDK 1.8 的 hash方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。 - -```java - static final int hash(Object key) { - int h; - // key.hashCode():返回散列值也就是hashcode - // ^ :按位异或 - // >>>:无符号右移,忽略符号位,空位都以0补齐 - return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); - } -``` - -对比一下 JDK1.7的 HashMap 的 hash 方法源码. - -```java -static int hash(int h) { - // This function ensures that hashCodes that differ only by - // constant multiples at each bit position have a bounded - // number of collisions (approximately 8 at default load factor). - - h ^= (h >>> 20) ^ (h >>> 12); - return h ^ (h >>> 7) ^ (h >>> 4); -} -``` - -相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。 - -所谓 **“拉链法”** 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。 - -![jdk1.8之前的内部结构-HashMap](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/jdk1.8之前的内部结构-HashMap.jpg) - -### JDK1.8之后 - -相比于之前的版本, JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。 - -![jdk1.8之后的内部结构-HashMap](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/JDK1.8之后的HashMap底层数据结构.jpg) - -> TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。 - -**推荐阅读:** - -- 《Java 8系列之重新认识HashMap》 : - -## HashMap 的长度为什么是2的幂次方 - -为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ `(n - 1) & hash`”。(n代表数组长度)。这也就解释了 HashMap 的长度为什么是2的幂次方。 - -**这个算法应该如何设计呢?** - -我们首先可能会想到采用%取余的操作来实现。但是,重点来了:**“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。”** 并且 **采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。** - -## HashMap 多线程操作导致死循环问题 - -主要原因在于 并发下的Rehash 会造成元素之间会形成一个循环链表。不过,jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在其他问题比如数据丢失。并发环境下推荐使用 ConcurrentHashMap 。 - -详情请查看: - -## ConcurrentHashMap 和 Hashtable 的区别 - -ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。 - -- **底层数据结构:** JDK1.7的 ConcurrentHashMap 底层采用 **分段的数组+链表** 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 **数组+链表** 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的; -- **实现线程安全的方式(重要):** ① **在JDK1.7的时候,ConcurrentHashMap(分段锁)** 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 **到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化)** 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② **Hashtable(同一把锁)** :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。 - -**两者的对比图:** - -图片来源: - -**HashTable:** - -![HashTable全表锁](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/HashTable全表锁.png) - -**JDK1.7的ConcurrentHashMap:** - -![JDK1.7的ConcurrentHashMap](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/ConcurrentHashMap分段锁.jpg) - -**JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):** - -![JDK1.8的ConcurrentHashMap](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/JDK1.8-ConcurrentHashMap-Structure.jpg) - -## ConcurrentHashMap线程安全的具体实现方式/底层具体实现 - -### JDK1.7(上面有示意图) - -首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。 - -**ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成**。 - -Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。 - -```java -static class Segment extends ReentrantLock implements Serializable { -} -``` - -一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。 - -### JDK1.8 (上面有示意图) - -ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构类似,数组+链表/红黑二叉树。Java 8在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(log(N))) - -synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。 - -## comparable 和 Comparator的区别 - -- comparable接口实际上是出自java.lang包 它有一个 `compareTo(Object obj)`方法用来排序 -- comparator接口实际上是出自 java.util 包它有一个`compare(Object obj1, Object obj2)`方法用来排序 - -一般我们需要对一个集合使用自定义排序时,我们就要重写`compareTo()`方法或`compare()`方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写`compareTo()`方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的 `Collections.sort()`. - -### Comparator定制排序 - -```java - ArrayList arrayList = new ArrayList(); - arrayList.add(-1); - arrayList.add(3); - arrayList.add(3); - arrayList.add(-5); - arrayList.add(7); - arrayList.add(4); - arrayList.add(-9); - arrayList.add(-7); - System.out.println("原始数组:"); - System.out.println(arrayList); - // void reverse(List list):反转 - Collections.reverse(arrayList); - System.out.println("Collections.reverse(arrayList):"); - System.out.println(arrayList); - - // void sort(List list),按自然排序的升序排序 - Collections.sort(arrayList); - System.out.println("Collections.sort(arrayList):"); - System.out.println(arrayList); - // 定制排序的用法 - Collections.sort(arrayList, new Comparator() { - - @Override - public int compare(Integer o1, Integer o2) { - return o2.compareTo(o1); - } - }); - System.out.println("定制排序后:"); - System.out.println(arrayList); -``` - -Output: - -``` -原始数组: -[-1, 3, 3, -5, 7, 4, -9, -7] -Collections.reverse(arrayList): -[-7, -9, 4, 7, -5, 3, 3, -1] -Collections.sort(arrayList): -[-9, -7, -5, -1, 3, 3, 4, 7] -定制排序后: -[7, 4, 3, 3, -1, -5, -7, -9] -``` - -### 重写compareTo方法实现按年龄来排序 - -```java -// person对象没有实现Comparable接口,所以必须实现,这样才不会出错,才可以使treemap中的数据按顺序排列 -// 前面一个例子的String类已经默认实现了Comparable接口,详细可以查看String类的API文档,另外其他 -// 像Integer类等都已经实现了Comparable接口,所以不需要另外实现了 - -public class Person implements Comparable { - private String name; - private int age; - - public Person(String name, int age) { - super(); - this.name = name; - this.age = age; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public int getAge() { - return age; - } - - public void setAge(int age) { - this.age = age; - } - - /** - * TODO重写compareTo方法实现按年龄来排序 - */ - @Override - public int compareTo(Person o) { - // TODO Auto-generated method stub - if (this.age > o.getAge()) { - return 1; - } else if (this.age < o.getAge()) { - return -1; - } - return age; - } -} - -``` - -```java - public static void main(String[] args) { - TreeMap pdata = new TreeMap(); - pdata.put(new Person("张三", 30), "zhangsan"); - pdata.put(new Person("李四", 20), "lisi"); - pdata.put(new Person("王五", 10), "wangwu"); - pdata.put(new Person("小红", 5), "xiaohong"); - // 得到key的值的同时得到key所对应的值 - Set keys = pdata.keySet(); - for (Person key : keys) { - System.out.println(key.getAge() + "-" + key.getName()); - - } - } -``` - -Output: - -``` -5-小红 -10-王五 -20-李四 -30-张三 -``` - -## 集合框架底层数据结构总结 - -### Collection - -#### 1. List - -- **Arraylist:** Object数组 -- **Vector:** Object数组 -- **LinkedList:** 双向链表(JDK1.6之前为循环链表,JDK1.7取消了循环) - -#### 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 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看:[《LinkedHashMap 源码详细分析(JDK1.8)》](https://www.imooc.com/article/22931) -- **Hashtable:** 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的 -- **TreeMap:** 红黑树(自平衡的排序二叉树) - -## 如何选用集合? - -主要根据集合的特点来选用,比如我们需要根据键值获取到元素值时就选用Map接口下的集合,需要排序时选择TreeMap,不需要排序时就选择HashMap,需要保证线程安全就选用ConcurrentHashMap.当我们只需要存放元素值时,就选择实现Collection接口的集合,需要保证元素唯一时选择实现Set接口的集合比如TreeSet或HashSet,不需要就选择实现List接口的比如ArrayList或LinkedList,然后再根据实现这些接口的集合的特点来选用。 - -## 公众号 - -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 - -**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取! - -**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 - -![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) diff --git "a/docs/java/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/LinkedList.md b/docs/java/collection/LinkedList.md deleted file mode 100644 index d26bc75..0000000 --- a/docs/java/collection/LinkedList.md +++ /dev/null @@ -1,515 +0,0 @@ - - - -- [简介](#简介) -- [内部结构分析](#内部结构分析) -- [LinkedList源码分析](#linkedlist源码分析) - - [构造方法](#构造方法) - - [添加(add)方法](#add方法) - - [根据位置取数据的方法](#根据位置取数据的方法) - - [根据对象得到索引的方法](#根据对象得到索引的方法) - - [检查链表是否包含某对象的方法:](#检查链表是否包含某对象的方法:) - - [删除(remove/pop)方法](#删除方法) -- [LinkedList类常用方法测试:](#linkedlist类常用方法测试) - - - -## 简介 -LinkedList是一个实现了List接口Deque接口双端链表。 -LinkedList底层的链表结构使它支持高效的插入和删除操作,另外它实现了Deque接口,使得LinkedList类也具有队列的特性; -LinkedList不是线程安全的,如果想使LinkedList变成线程安全的,可以调用静态类Collections类中的synchronizedList方法: -```java -List list=Collections.synchronizedList(new LinkedList(...)); -``` -## 内部结构分析 -**如下图所示:** -![LinkedList内部结构](https://user-gold-cdn.xitu.io/2018/3/19/1623e363fe0450b0?w=600&h=481&f=jpeg&s=18502) -看完了图之后,我们再看LinkedList类中的一个**内部私有类Node**就很好理解了: -```java -private static class Node { - E item;//节点值 - Node next;//后继节点 - Node prev;//前驱节点 - - Node(Node prev, E element, Node next) { - this.item = element; - this.next = next; - this.prev = prev; - } - } -``` -这个类就代表双端链表的节点Node。这个类有三个属性,分别是前驱节点,本节点的值,后继结点。 - -## LinkedList源码分析 -### 构造方法 -**空构造方法:** -```java - public LinkedList() { - } -``` -**用已有的集合创建链表的构造方法:** -```java - public LinkedList(Collection c) { - this(); - addAll(c); - } -``` -### add方法 -**add(E e)** 方法:将元素添加到链表尾部 -```java -public boolean add(E e) { - linkLast(e);//这里就只调用了这一个方法 - return true; - } -``` - -```java - /** - * 链接使e作为最后一个元素。 - */ - void linkLast(E e) { - final Node l = last; - final Node newNode = new Node<>(l, e, null); - last = newNode;//新建节点 - if (l == null) - first = newNode; - else - l.next = newNode;//指向后继元素也就是指向下一个元素 - size++; - modCount++; - } -``` -**add(int index,E e)**:在指定位置添加元素 -```java -public void add(int index, E element) { - checkPositionIndex(index); //检查索引是否处于[0-size]之间 - - if (index == size)//添加在链表尾部 - linkLast(element); - else//添加在链表中间 - linkBefore(element, node(index)); - } -``` -linkBefore方法需要给定两个参数,一个插入节点的值,一个指定的node,所以我们又调用了Node(index)去找到index对应的node - -**addAll(Collection c ):将集合插入到链表尾部** - -```java -public boolean addAll(Collection c) { - return addAll(size, c); - } -``` -**addAll(int index, Collection c):** 将集合从指定位置开始插入 -```java -public boolean addAll(int index, Collection c) { - //1:检查index范围是否在size之内 - checkPositionIndex(index); - - //2:toArray()方法把集合的数据存到对象数组中 - Object[] a = c.toArray(); - int numNew = a.length; - if (numNew == 0) - return false; - - //3:得到插入位置的前驱节点和后继节点 - Node pred, succ; - //如果插入位置为尾部,前驱节点为last,后继节点为null - if (index == size) { - succ = null; - pred = last; - } - //否则,调用node()方法得到后继节点,再得到前驱节点 - else { - succ = node(index); - pred = succ.prev; - } - - // 4:遍历数据将数据插入 - for (Object o : a) { - @SuppressWarnings("unchecked") E e = (E) o; - //创建新节点 - Node newNode = new Node<>(pred, e, null); - //如果插入位置在链表头部 - if (pred == null) - first = newNode; - else - pred.next = newNode; - pred = newNode; - } - - //如果插入位置在尾部,重置last节点 - if (succ == null) { - last = pred; - } - //否则,将插入的链表与先前链表连接起来 - else { - pred.next = succ; - succ.prev = pred; - } - - size += numNew; - modCount++; - return true; - } -``` -上面可以看出addAll方法通常包括下面四个步骤: -1. 检查index范围是否在size之内 -2. toArray()方法把集合的数据存到对象数组中 -3. 得到插入位置的前驱和后继节点 -4. 遍历数据,将数据插入到指定位置 - -**addFirst(E e):** 将元素添加到链表头部 -```java - public void addFirst(E e) { - linkFirst(e); - } -``` -```java -private void linkFirst(E e) { - final Node f = first; - final Node newNode = new Node<>(null, e, f);//新建节点,以头节点为后继节点 - first = newNode; - //如果链表为空,last节点也指向该节点 - if (f == null) - last = newNode; - //否则,将头节点的前驱指针指向新节点,也就是指向前一个元素 - else - f.prev = newNode; - size++; - modCount++; - } -``` -**addLast(E e):** 将元素添加到链表尾部,与 **add(E e)** 方法一样 -```java -public void addLast(E e) { - linkLast(e); - } -``` -### 根据位置取数据的方法 -**get(int index):** 根据指定索引返回数据 -```java -public E get(int index) { - //检查index范围是否在size之内 - checkElementIndex(index); - //调用Node(index)去找到index对应的node然后返回它的值 - return node(index).item; - } -``` -**获取头节点(index=0)数据方法:** -```java -public E getFirst() { - final Node f = first; - if (f == null) - throw new NoSuchElementException(); - return f.item; - } -public E element() { - return getFirst(); - } -public E peek() { - final Node f = first; - return (f == null) ? null : f.item; - } - -public E peekFirst() { - final Node f = first; - return (f == null) ? null : f.item; - } -``` -**区别:** -getFirst(),element(),peek(),peekFirst() -这四个获取头结点方法的区别在于对链表为空时的处理,是抛出异常还是返回null,其中**getFirst()** 和**element()** 方法将会在链表为空时,抛出异常 - -element()方法的内部就是使用getFirst()实现的。它们会在链表为空时,抛出NoSuchElementException -**获取尾节点(index=-1)数据方法:** -```java - public E getLast() { - final Node l = last; - if (l == null) - throw new NoSuchElementException(); - return l.item; - } - public E peekLast() { - final Node l = last; - return (l == null) ? null : l.item; - } -``` -**两者区别:** -**getLast()** 方法在链表为空时,会抛出**NoSuchElementException**,而**peekLast()** 则不会,只是会返回 **null**。 -### 根据对象得到索引的方法 -**int indexOf(Object o):** 从头遍历找 -```java -public int indexOf(Object o) { - int index = 0; - if (o == null) { - //从头遍历 - for (Node x = first; x != null; x = x.next) { - if (x.item == null) - return index; - index++; - } - } else { - //从头遍历 - for (Node x = first; x != null; x = x.next) { - if (o.equals(x.item)) - return index; - index++; - } - } - return -1; - } -``` -**int lastIndexOf(Object o):** 从尾遍历找 -```java -public int lastIndexOf(Object o) { - int index = size; - if (o == null) { - //从尾遍历 - for (Node x = last; x != null; x = x.prev) { - index--; - if (x.item == null) - return index; - } - } else { - //从尾遍历 - for (Node x = last; x != null; x = x.prev) { - index--; - if (o.equals(x.item)) - return index; - } - } - return -1; - } -``` -### 检查链表是否包含某对象的方法: -**contains(Object o):** 检查对象o是否存在于链表中 -```java - public boolean contains(Object o) { - return indexOf(o) != -1; - } -``` -### 删除方法 -**remove()** ,**removeFirst(),pop():** 删除头节点 -``` -public E pop() { - return removeFirst(); - } -public E remove() { - return removeFirst(); - } -public E removeFirst() { - final Node f = first; - if (f == null) - throw new NoSuchElementException(); - return unlinkFirst(f); - } -``` -**removeLast(),pollLast():** 删除尾节点 -```java -public E removeLast() { - final Node l = last; - if (l == null) - throw new NoSuchElementException(); - return unlinkLast(l); - } -public E pollLast() { - final Node l = last; - return (l == null) ? null : unlinkLast(l); - } -``` -**区别:** removeLast()在链表为空时将抛出NoSuchElementException,而pollLast()方法返回null。 - -**remove(Object o):** 删除指定元素 -```java -public boolean remove(Object o) { - //如果删除对象为null - if (o == null) { - //从头开始遍历 - for (Node x = first; x != null; x = x.next) { - //找到元素 - if (x.item == null) { - //从链表中移除找到的元素 - unlink(x); - return true; - } - } - } else { - //从头开始遍历 - for (Node x = first; x != null; x = x.next) { - //找到元素 - if (o.equals(x.item)) { - //从链表中移除找到的元素 - unlink(x); - return true; - } - } - } - return false; - } -``` -当删除指定对象时,只需调用remove(Object o)即可,不过该方法一次只会删除一个匹配的对象,如果删除了匹配对象,返回true,否则false。 - -unlink(Node x) 方法: -```java -E unlink(Node x) { - // assert x != null; - final E element = x.item; - final Node next = x.next;//得到后继节点 - final Node prev = x.prev;//得到前驱节点 - - //删除前驱指针 - if (prev == null) { - first = next;//如果删除的节点是头节点,令头节点指向该节点的后继节点 - } else { - prev.next = next;//将前驱节点的后继节点指向后继节点 - x.prev = null; - } - - //删除后继指针 - if (next == null) { - last = prev;//如果删除的节点是尾节点,令尾节点指向该节点的前驱节点 - } else { - next.prev = prev; - x.next = null; - } - - x.item = null; - size--; - modCount++; - return element; - } -``` -**remove(int index)**:删除指定位置的元素 -```java -public E remove(int index) { - //检查index范围 - checkElementIndex(index); - //将节点删除 - return unlink(node(index)); - } -``` -## LinkedList类常用方法测试 - -```java -package list; - -import java.util.Iterator; -import java.util.LinkedList; - -public class LinkedListDemo { - public static void main(String[] srgs) { - //创建存放int类型的linkedList - LinkedList linkedList = new LinkedList<>(); - /************************** linkedList的基本操作 ************************/ - linkedList.addFirst(0); // 添加元素到列表开头 - linkedList.add(1); // 在列表结尾添加元素 - linkedList.add(2, 2); // 在指定位置添加元素 - linkedList.addLast(3); // 添加元素到列表结尾 - - System.out.println("LinkedList(直接输出的): " + linkedList); - - System.out.println("getFirst()获得第一个元素: " + linkedList.getFirst()); // 返回此列表的第一个元素 - System.out.println("getLast()获得第最后一个元素: " + linkedList.getLast()); // 返回此列表的最后一个元素 - System.out.println("removeFirst()删除第一个元素并返回: " + linkedList.removeFirst()); // 移除并返回此列表的第一个元素 - System.out.println("removeLast()删除最后一个元素并返回: " + linkedList.removeLast()); // 移除并返回此列表的最后一个元素 - System.out.println("After remove:" + linkedList); - System.out.println("contains()方法判断列表是否包含1这个元素:" + linkedList.contains(1)); // 判断此列表包含指定元素,如果是,则返回true - System.out.println("该linkedList的大小 : " + linkedList.size()); // 返回此列表的元素个数 - - /************************** 位置访问操作 ************************/ - System.out.println("-----------------------------------------"); - linkedList.set(1, 3); // 将此列表中指定位置的元素替换为指定的元素 - System.out.println("After set(1, 3):" + linkedList); - System.out.println("get(1)获得指定位置(这里为1)的元素: " + linkedList.get(1)); // 返回此列表中指定位置处的元素 - - /************************** Search操作 ************************/ - System.out.println("-----------------------------------------"); - linkedList.add(3); - System.out.println("indexOf(3): " + linkedList.indexOf(3)); // 返回此列表中首次出现的指定元素的索引 - System.out.println("lastIndexOf(3): " + linkedList.lastIndexOf(3));// 返回此列表中最后出现的指定元素的索引 - - /************************** Queue操作 ************************/ - System.out.println("-----------------------------------------"); - System.out.println("peek(): " + linkedList.peek()); // 获取但不移除此列表的头 - System.out.println("element(): " + linkedList.element()); // 获取但不移除此列表的头 - linkedList.poll(); // 获取并移除此列表的头 - System.out.println("After poll():" + linkedList); - linkedList.remove(); - System.out.println("After remove():" + linkedList); // 获取并移除此列表的头 - linkedList.offer(4); - System.out.println("After offer(4):" + linkedList); // 将指定元素添加到此列表的末尾 - - /************************** Deque操作 ************************/ - System.out.println("-----------------------------------------"); - linkedList.offerFirst(2); // 在此列表的开头插入指定的元素 - System.out.println("After offerFirst(2):" + linkedList); - linkedList.offerLast(5); // 在此列表末尾插入指定的元素 - System.out.println("After offerLast(5):" + linkedList); - System.out.println("peekFirst(): " + linkedList.peekFirst()); // 获取但不移除此列表的第一个元素 - System.out.println("peekLast(): " + linkedList.peekLast()); // 获取但不移除此列表的第一个元素 - linkedList.pollFirst(); // 获取并移除此列表的第一个元素 - System.out.println("After pollFirst():" + linkedList); - linkedList.pollLast(); // 获取并移除此列表的最后一个元素 - System.out.println("After pollLast():" + linkedList); - linkedList.push(2); // 将元素推入此列表所表示的堆栈(插入到列表的头) - System.out.println("After push(2):" + linkedList); - linkedList.pop(); // 从此列表所表示的堆栈处弹出一个元素(获取并移除列表第一个元素) - System.out.println("After pop():" + linkedList); - linkedList.add(3); - linkedList.removeFirstOccurrence(3); // 从此列表中移除第一次出现的指定元素(从头部到尾部遍历列表) - System.out.println("After removeFirstOccurrence(3):" + linkedList); - linkedList.removeLastOccurrence(3); // 从此列表中移除最后一次出现的指定元素(从尾部到头部遍历列表) - System.out.println("After removeFirstOccurrence(3):" + linkedList); - - /************************** 遍历操作 ************************/ - System.out.println("-----------------------------------------"); - linkedList.clear(); - for (int i = 0; i < 100000; i++) { - linkedList.add(i); - } - // 迭代器遍历 - long start = System.currentTimeMillis(); - Iterator iterator = linkedList.iterator(); - while (iterator.hasNext()) { - iterator.next(); - } - long end = System.currentTimeMillis(); - System.out.println("Iterator:" + (end - start) + " ms"); - - // 顺序遍历(随机遍历) - start = System.currentTimeMillis(); - for (int i = 0; i < linkedList.size(); i++) { - linkedList.get(i); - } - end = System.currentTimeMillis(); - System.out.println("for:" + (end - start) + " ms"); - - // 另一种for循环遍历 - start = System.currentTimeMillis(); - for (Integer i : linkedList) - ; - end = System.currentTimeMillis(); - System.out.println("for2:" + (end - start) + " ms"); - - // 通过pollFirst()或pollLast()来遍历LinkedList - LinkedList temp1 = new LinkedList<>(); - temp1.addAll(linkedList); - start = System.currentTimeMillis(); - while (temp1.size() != 0) { - temp1.pollFirst(); - } - end = System.currentTimeMillis(); - System.out.println("pollFirst()或pollLast():" + (end - start) + " ms"); - - // 通过removeFirst()或removeLast()来遍历LinkedList - LinkedList temp2 = new LinkedList<>(); - temp2.addAll(linkedList); - start = System.currentTimeMillis(); - while (temp2.size() != 0) { - temp2.removeFirst(); - } - end = System.currentTimeMillis(); - System.out.println("removeFirst()或removeLast():" + (end - start) + " ms"); - } -} -``` 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对象。每一个`