+ + 唯手熟尔 + +
++ +
+ + + + + +diff --git "a/blog-site/content/posts/annex/xmind/\345\211\215\347\253\257\345\255\246\344\271\240\350\267\257\347\272\277.xmind" "b/blog-site/content/posts/annex/xmind/\345\211\215\347\253\257\345\255\246\344\271\240\350\267\257\347\272\277.xmind" new file mode 100644 index 00000000..190d9a9e Binary files /dev/null and "b/blog-site/content/posts/annex/xmind/\345\211\215\347\253\257\345\255\246\344\271\240\350\267\257\347\272\277.xmind" differ diff --git a/blog-site/content/posts/books/JavaBooks.md b/blog-site/content/posts/books/JavaBooks.md index 93687508..20d64723 100644 --- a/blog-site/content/posts/books/JavaBooks.md +++ b/blog-site/content/posts/books/JavaBooks.md @@ -6,15 +6,17 @@ tags: ["书籍"] slug: "java-books" --- -- [HeadFirst设计模式](/iblog/posts/annex/pdf/books/HeadFirst设计模式.pdf) -- [Java数据结构和算法](/iblog/posts/annex/pdf/books/Java数据结构和算法.pdf) -- [Java核心技术卷I基础知识](/iblog/posts/annex/pdf/books/Java核心技术卷I基础知识.pdf) -- [Java编程思想](/iblog/posts/annex/pdf/books/Java编程思想.pdf) -- [代码整洁之道](/iblog/posts/annex/pdf/books/代码整洁之道.pdf) -- [大型网站技术架构](/iblog/posts/annex/pdf/books/大型网站技术架构.pdf) -- [大话数据结构](/iblog/posts/annex/pdf/books/大话数据结构.pdf) -- [深入分析JavaWeb技术内幕](/iblog/posts/annex/pdf/books/深入分析JavaWeb技术内幕.pdf) -- [疯狂Java讲义](/iblog/posts/annex/pdf/books/疯狂Java讲义.pdf) -- [重构:改善既有代码的设计](/iblog/posts/annex/pdf/books/重构:改善既有代码的设计.pdf) -- [领域驱动设计](/iblog/posts/annex/pdf/books/领域驱动设计.pdf) - +- [HeadFirst设计模式](https://pan.baidu.com/s/1mXGLnTJqGtGeGlrePzx3fA?pwd=8888) +- [Java数据结构和算法](https://pan.baidu.com/s/1mXGLnTJqGtGeGlrePzx3fA?pwd=8888) +- [Java核心技术卷I基础知识](https://pan.baidu.com/s/1mXGLnTJqGtGeGlrePzx3fA?pwd=8888) +- [Java编程思想](https://pan.baidu.com/s/1mXGLnTJqGtGeGlrePzx3fA?pwd=8888) +- [代码整洁之道](https://pan.baidu.com/s/1mXGLnTJqGtGeGlrePzx3fA?pwd=8888) +- [大型网站技术架构](https://pan.baidu.com/s/1mXGLnTJqGtGeGlrePzx3fA?pwd=8888) +- [大话数据结构](https://pan.baidu.com/s/1mXGLnTJqGtGeGlrePzx3fA?pwd=8888) +- [深入分析JavaWeb技术内幕](https://pan.baidu.com/s/1mXGLnTJqGtGeGlrePzx3fA?pwd=8888) +- [疯狂Java讲义](https://pan.baidu.com/s/1mXGLnTJqGtGeGlrePzx3fA?pwd=8888) +- [重构:改善既有代码的设计](https://pan.baidu.com/s/1mXGLnTJqGtGeGlrePzx3fA?pwd=8888) +- [领域驱动设计](https://pan.baidu.com/s/1mXGLnTJqGtGeGlrePzx3fA?pwd=8888) +- [深入理解计算机系统](https://pan.baidu.com/s/1mXGLnTJqGtGeGlrePzx3fA?pwd=8888) +- [Java并发编程的艺术](https://pan.baidu.com/s/1mXGLnTJqGtGeGlrePzx3fA?pwd=8888) +- [...](https://www.jiumodiary.com) \ No newline at end of file diff --git "a/blog-site/content/posts/essays/\345\211\215\347\253\257\345\255\246\344\271\240\350\267\257\347\272\277.md" "b/blog-site/content/posts/essays/\345\211\215\347\253\257\345\255\246\344\271\240\350\267\257\347\272\277.md" new file mode 100644 index 00000000..4513a8ea --- /dev/null +++ "b/blog-site/content/posts/essays/\345\211\215\347\253\257\345\255\246\344\271\240\350\267\257\347\272\277.md" @@ -0,0 +1,95 @@ +--- +title: "前端学习路线" +date: 2024-02-29 +draft: false +tags: ["学习路线"] +slug: "front-learning-route" +--- + + +## 基础知识 +### 网络知识 +#### HTTP +#### DNS +#### 域名 +#### 云服务 +#### 网络安全 +- HTTPS +- CORS +- 网络渗透 +- OWASP +### HTML +### CSS +### JavaScript +### JQuery +### Ajax +### ES6-ES11 +### 综合应用 + +## 工程化体系 +### 代码规范 +### CSS预处理器 +- Less +- Sass +- PostCSS +### Node +### Promise +### Axios +### 工具 +#### 包管理工具 +- Npm +- Yarn +#### 打包工具 +- Webpack +- Parcel +#### 代码格式化工具 +- ESLint +- Prettier +#### 调试工具 +- Chrome +- IETest +- Postman +#### 版本管理工具 +- Git +- GitLab +- GitHub +#### 部署发布工具 +- Jenkins +- CICD +### 主流技术 +- TypeScript +- Vue +- React +- Angular +- 综合应用 +## 静态站点生成器 +- Next +- GatsbyJS +- Nuxt +- Vuepress +- Hugo +## 性能优化和监控 +### 性能优化概览 +### 浏览器及工作方式 +### SEO +### 资源管理 +- 延迟加载 +- 按需加载 +- 缓存复用 +- CDN部署 +- 请求合并 +- 异步同步 +## 移动端 +### Native App +- 安卓原生 +- IOS原生 +- 鸿蒙原生 +### Web App +- Uni-App +- Taro +- React Native +- Flutter + 1. 基础 + 2. 实战 +### 微信小程序 + diff --git "a/blog-site/content/posts/exam/\345\244\207\350\200\203\346\210\220\344\272\272\346\234\254\347\247\221\345\255\246\344\275\215\350\213\261\350\257\255.md" "b/blog-site/content/posts/exam/\345\244\207\350\200\203\346\210\220\344\272\272\346\234\254\347\247\221\345\255\246\344\275\215\350\213\261\350\257\255.md" index 57a43d63..0b177d39 100644 --- "a/blog-site/content/posts/exam/\345\244\207\350\200\203\346\210\220\344\272\272\346\234\254\347\247\221\345\255\246\344\275\215\350\213\261\350\257\255.md" +++ "b/blog-site/content/posts/exam/\345\244\207\350\200\203\346\210\220\344\272\272\346\234\254\347\247\221\345\255\246\344\275\215\350\213\261\350\257\255.md" @@ -1,5 +1,6 @@ --- title: "备考成人本科学位英语" +password: 2022englishexam date: 2022-03-24 hidden: true draft: true diff --git "a/blog-site/content/posts/resume/20201124\347\256\200\345\216\206.md" "b/blog-site/content/posts/resume/20201124\347\256\200\345\216\206.md" index 24860038..34e717de 100644 --- "a/blog-site/content/posts/resume/20201124\347\256\200\345\216\206.md" +++ "b/blog-site/content/posts/resume/20201124\347\256\200\345\216\206.md" @@ -1,8 +1,9 @@ --- title: "20201124简历" +password: 20201124resume date: 2020-11-24 -draft: true -tags: ["简历"] +draft: false +tags: ["简历","求职"] slug: "interview-resume-20201124" --- diff --git "a/blog-site/content/posts/resume/20220422\347\256\200\345\216\206.md" "b/blog-site/content/posts/resume/20220422\347\256\200\345\216\206.md" index 4eca3af1..b02c2e34 100644 --- "a/blog-site/content/posts/resume/20220422\347\256\200\345\216\206.md" +++ "b/blog-site/content/posts/resume/20220422\347\256\200\345\216\206.md" @@ -1,8 +1,9 @@ --- title: "20220422简历" +password: 20220422resume date: 2022-04-22 -draft: true -tags: ["简历"] +draft: false +tags: ["简历","求职"] slug: "interview-resume-20220422" --- diff --git "a/blog-site/content/posts/resume/20230915\347\256\200\345\216\206.md" "b/blog-site/content/posts/resume/20230915\347\256\200\345\216\206.md" index f1ce8179..3ea569b4 100644 --- "a/blog-site/content/posts/resume/20230915\347\256\200\345\216\206.md" +++ "b/blog-site/content/posts/resume/20230915\347\256\200\345\216\206.md" @@ -1,8 +1,9 @@ --- title: "20230915简历" +password: 20230915resume date: 2023-09-15 -draft: true -tags: ["简历"] +draft: false +tags: ["简历","求职"] slug: "interview-resume-20230915" --- diff --git "a/blog-site/content/posts/resume/\351\235\242\350\257\225Java\345\217\257\350\203\275\344\274\232\350\242\253\351\227\256\345\210\260\347\232\204\351\227\256\351\242\230.md" "b/blog-site/content/posts/resume/\351\235\242\350\257\225Java\345\217\257\350\203\275\344\274\232\350\242\253\351\227\256\345\210\260\347\232\204\351\227\256\351\242\230.md" index 8b5194fd..f88ccd95 100644 --- "a/blog-site/content/posts/resume/\351\235\242\350\257\225Java\345\217\257\350\203\275\344\274\232\350\242\253\351\227\256\345\210\260\347\232\204\351\227\256\351\242\230.md" +++ "b/blog-site/content/posts/resume/\351\235\242\350\257\225Java\345\217\257\350\203\275\344\274\232\350\242\253\351\227\256\345\210\260\347\232\204\351\227\256\351\242\230.md" @@ -2,7 +2,7 @@ title: "面试Java可能会被问到的问题" date: 2021-05-11 draft: false -tags: ["Java", "求职"] +tags: ["求职"] slug: "interview-junior-javaer" --- diff --git "a/blog-site/content/posts/resume/\351\235\242\350\257\225\344\270\255\345\270\270\350\247\201\347\232\204\351\227\256\351\242\230.md" "b/blog-site/content/posts/resume/\351\235\242\350\257\225\344\270\255\345\270\270\350\247\201\347\232\204\351\227\256\351\242\230.md" index b2b7827d..f6acf883 100644 --- "a/blog-site/content/posts/resume/\351\235\242\350\257\225\344\270\255\345\270\270\350\247\201\347\232\204\351\227\256\351\242\230.md" +++ "b/blog-site/content/posts/resume/\351\235\242\350\257\225\344\270\255\345\270\270\350\247\201\347\232\204\351\227\256\351\242\230.md" @@ -2,7 +2,7 @@ title: "面试中常见的问题" date: 2021-04-23 draft: false -tags: ["Java","求职"] +tags: ["求职"] slug: "Interview-questions-and-answers" --- diff --git "a/blog-site/content/posts/worksummary/2019\345\267\245\344\275\234\346\200\273\347\273\223.md" "b/blog-site/content/posts/worksummary/2019\345\267\245\344\275\234\346\200\273\347\273\223.md" index 8ec3af55..43012f99 100644 --- "a/blog-site/content/posts/worksummary/2019\345\267\245\344\275\234\346\200\273\347\273\223.md" +++ "b/blog-site/content/posts/worksummary/2019\345\267\245\344\275\234\346\200\273\347\273\223.md" @@ -1,22 +1,19 @@ --- title: "2019工作总结" +password: 2019summary date: 2019-12-01 -draft: true +draft: false tags: ["工作总结"] slug: "work-summary-2019" --- 本人在进入公司起,期间一直对自己要求严谨,遵守公司的相应制度. 在过去的一个月时间里,我参与了贵州银行的电子验印系统的开发,一直努力完成和完善分配给我的任务,在这一个月发现了自身还有很多的不足,所以抱着虚心学习的态度,学习公司的开发流程,了解公司的产品架构,主要技术,主动和同事沟通,学习经验,希望能快速融入公司,能够全心的投入工作. - 试用期完成的工作有限,主要负责验印系统的统一门开发,学习了一些新技术,因为自己在经验上不足,对于技术的学习和掌握还不够深入,发现问题的能力还不够,所以拖慢了自己的开发进度,简单列一些. 通过开发的过程中,学习并掌握了vue框架的使用,学习到了Oracle数据库的使用..... 使我认识到了一个称职的开发人员应当具备良好的语言表达能力,较强的逻辑能力,灵活的处理应变能力,有效的对外联系能力. 在参与项目的开发过程中,发现很多看似简单的工作,其实里面有很多技巧 - 今后,我会多注意在这些方面的学习和积累,努力做好开发人员的本职工作,注重工作态度,把自己的工作做好做扎实,为项目的开发及公司的发展贡献自己的一份力量.在工作的这段时间里.我得到了同事的帮助,经常与我交流,指出技术上的问题,传授了很多开发经验,在生活上也给与快了我很大的帮助,使得我很快就适应了这里的生活. - 整个工作学习过程中,我认为自己工作比较认真负责.具有较强的责任心和进取心,能完成领导交付的工作,但也存在着许多缺点与不足,对工作的专业性还不够,业务经验不够丰富,对于发现问题的处理还不是很全面,我会在以后的工作中不断实践和总结,并积极学习新知识,弥补自身不足,来提高自己的综合素质. 总之,认真的回顾了这段时间的工作,发现了一些不足之处,这都是我在接下的工作中需要完善的.同时,也会尽最大努力的学习和积累经验,逐步发展成一个全面的技术开发人员,更好的完成工作. - 以上是我对2019年的工作总结及2020年工作计划,可能还不是很成熟,希望领导指正.展望2020年,我会更加努力,认真负责的去对待工作,相信自己会完成新的任务,能迎接新的挑战。为公司的发展做出自己的贡献. diff --git "a/blog-site/content/posts/worksummary/2023\345\267\245\344\275\234\346\200\273\347\273\223.md" "b/blog-site/content/posts/worksummary/2023\345\267\245\344\275\234\346\200\273\347\273\223.md" index 14b4cb45..6aa007f1 100644 --- "a/blog-site/content/posts/worksummary/2023\345\267\245\344\275\234\346\200\273\347\273\223.md" +++ "b/blog-site/content/posts/worksummary/2023\345\267\245\344\275\234\346\200\273\347\273\223.md" @@ -1,7 +1,8 @@ --- title: "2023工作总结" +password: 2023summary date: 2023-12-01 -draft: true +draft: false tags: ["工作总结"] slug: "work-summary-2023" --- diff --git a/blog-site/public/404.html b/blog-site/public/404.html new file mode 100644 index 00000000..0d747144 --- /dev/null +++ b/blog-site/public/404.html @@ -0,0 +1,166 @@ + + +
+ + + + + + + ++ +
+ + + + + ++ +
+ + + + + +本博客使用 hugo + GitHubPage 进行搭建,使用的主题为 zozo Designed by VarKai。
+如果你想要使用 hugo 搭建博客,可以参考以下相关资料:
+目前只有面试Java开发的相关资料,包括从网上收集和自己整理的一些,希望可以帮助到一些人吧。
+因为本人目前主要是做Java开发相关的工作,所以这里的博客文章、参考资料大部分都和Java相关,如果您不想看到Java可以直接关闭本页面。
++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + +在职期间,我主要负责耐材项目的开发与维护,共迭代171个版本。通过与团队成员的紧密合作,我们按时完成了项目中的需求。在这段时间里,我不断提升自己的专业技能和知识,增强了自己的专业能力。我始终认为团队合作是成功的关键。在工作中,我积极与同事沟......
+自我介绍 1998 · 李济芝 河北唐山 15176733539 m15176733539@163.com 本人有严谨的工作态度与高质量意识;能查阅各种开发技术手册,具有独立解决问题的能力。具备扎实的Java基础和四年开发经验,有良好的编程风格,独立熟练使用Spring全家桶等常用类库开发Java服务端程序、对Jav......
+代码实现 代码结构 pom <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> </dependency> 库表结构 -- ---------------------------- -- 定时任务调度表 -- ---------------------------- drop table if exists sys_job; create table sys_job ( job_id bigint(20) not null auto_increment comment '任务ID', job_name varchar(64) default '' comment '任务名称', job_group varchar(64) default 'DEFAULT' comment '任务组名', invoke_target varchar(500) not null comment......
+nacos nacos下载 下载地址 一键傻瓜试安装即可,官网写的很清楚这里不在赘述 http://nacos.io/zh-cn/docs/v2/quickstart/quick-start.html nacos启动 将模式改为单机模式 启动成功 nacos相关配置 demo-dev.yaml server: port: 8001 config: info: "config info for dev from nacos config center" demo-test.yaml server: port: 3333 config: info: "config info for test from nacos config center" user.yaml user: name: zs1112222 age: 10 address: 测试地址 代码 整合nacos配置中心,注册......
+结构 pom.xml fastdfs-client-java-1.27.jar:点击下载 <dependencies> <!-- fastdfs --> <dependency> <groupId>org.csource</groupId> <artifactId>fastdfs-client-java</artifactId> <version>1.27</version> <systemPath>${project.basedir}/lib/fastdfs-client-java-1.27.jar</systemPath> <scope>system</scope> </dependency> <!--aliyun oss 依赖--> <dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.11</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.11.0</version> </dependency> </dependencies> application.yml server: port: 80 公共部分 FileManagement public interface FileManagement { /** * 设置下一个bean的对象 * * @param nextFileManagement 下一个......
+结构 pom.xml <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.11</version> </dependency> <dependency> <groupId>com.alipay.sdk</groupId> <artifactId>alipay-sdk-java</artifactId> <version>4.9.9</version> </dependency> <dependency> <groupId>com.github.binarywang</groupId> <artifactId>weixin-java-pay</artifactId> <version>4.5.0</version> </dependency> </dependencies> application.yml server: port: 8080 pay: wechat: #微信公众号或者小程序等的appid appId: "" #微信支付商户号 mchId: "" #微信支付商户密钥 mchKey: "" #服务商模式下的子商户公众账号ID subAppId: #服务商模式下的子商户号 subMchId: # p12证书的位......
+常见参数校验 在日常的接口开发中,为了防止非法参数对业务造成影响,经常需要对接口的参数做校验,最简单就是用if条件语句来判断,但是随着参数越来越多,业务越来越复杂,判断参数代码语句显得尤为冗长. 或者有些程序会将if封装起来,例如spring中......
+流程图 代码实现 pom <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.plugin</groupId> <artifactId>spring-plugin-core</artifactId> <version>${spring.plugin.core.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>${hutool.version}</version> </dependency> </dependencies> context EventContext public interface EventContext { /** * 是否继续调用链 */ boolean continueChain(); /** * 获取当前过滤器选择器 */ FilterSelector getFilterSelector(); } BizType public interface BizType { /** * 获取业务类型码值 */ Integer getCode(); /** * 业务类型名称 * */ String getName(); } AbstractEventContext public abstract class AbstractEventContext implements EventContext{ private final BizType businessType; private final FilterSelector filterSelector; protected AbstractEventContext(BizType businessType, FilterSelector filterSelector) {......
+什么是重构 摘自《重构:改善既有代码的设计》 重构(名词形式): 对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。 重构(动词形式): 使用一些列重构手法,在不改变软件可观察行为的前提下,调整其结构。 重......
+Spring MVC Spring Web MVC是建立在Servlet API上的原始Web框架,从一开始就包含在Spring框架中。正式名称 “Spring Web MVC “来自其源模块的名称(spring-webmvc),但它更常被称为 “Spring MVC”。 SpringMVC是基于S......
+逻辑架构 主要分为:连接层,服务层,引擎层,存储层。 客户端执行一条select命令的流程如下: 连接层:最上层是一些客户端和连接服务,包含本地sock通信和大多数基于客户端/服务端工具实现的类似于tcplip的通信。主要完成一些类似于连接处理、......
+bug的起源: 1945年,一只小飞蛾钻进了计算机电路里,导致系统无法工作,一位名叫格蕾丝·赫柏的人把飞蛾拍死在工作日志上,写道:就是这个 bug(虫子),害我们今天的工作无法完成——于是,bug一词成了电脑系统程序的专业术语,形容那些系统中的......
+概览 Elasticsearch,简称为 ES, ES是一个开源的高扩展的分布式全文搜索引擎, 是整个 ElasticStack 技术栈的核心。它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上百台服务器,处理 PB 级别的数据。 Elastic Stack, 包括 Elasticsearch、 Ki......
+QPS 即 Queries Per Second的缩写,每秒能处理查询数目。是一台服务器每秒能够相应的查询次数,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。 TPS 即 Transactions Per Second的缩写,每秒处理的事务数目。一个事务是指一个客户机向服务器发送请求然后服务......
+接口优化 线上接口很慢,线上生产问题,我们绝对不能马虎放过抱着侥幸心理,必须要找到根本原因及时处理,防止下次留下更大的坑.大致思路要定位接口问题,然后具体问题具体分析,讨论不同解决方案. 定位问题 要快速定位接口哪一个环节比较慢,性能瓶颈在哪里,......
+始计 兵者,国之大事,死生之地,存亡之道,不可不察也。 故经之以五事,校之以计,而索其情:一曰道,二曰天,三曰地,四曰将,五曰法。道者,令民于上同意,可与之死,可与之生,而不危也;天者,阴阳、寒暑、时制也;地者,远近、险易、广狭、死生也;将者,......
+下载 工欲善其事必先利其器,一个好的开发工具,能极大提高开发效率. 新UI很漂亮。IDEA 2022.2.3 官方下载地址: https://www.jetbrains.com/zh-cn/idea/download/other.html 激活工具 百度云下载. 链接:https://pan.baidu.com/s/19sCUTCBXvwXgEQc8vX-SYQ?pwd=gwu......
+为什么要写整洁的代码 为什么要写整洁的代码,回答这个问题之前,也许应该想想写糟糕的代码的原因 是想快点完成吗?还是要赶时间吗?有可能.或许你觉得自己要干好所需要的时间不够;假使花时间清理代码,老板就会大发雷霆.或许你只是不耐烦再搞这套程序,期望......
+HeadFirst设计模式 Java数据结构和算法 Java核心技术卷I基础知识 Java编程思想 代码整洁之道 大型网站技术架构 大话数据结构 深入分析JavaWeb技术内幕 疯狂Java讲义 重构:改善既有代码的设计 领域驱动设计......
+产品需求澄清、PN排期及任务分解 开发设计评审 功能设计流程图 与外部系统交互、本系统模块之间流程,比较好用的画圈软件draw .io或在线的process on 数据库设计 从DDD角度界限上下文、ER图、评审表结构设计是否合理,表的关联关系是否合理、是......
+{{ERROR}}
+ +
+ + + + + +自我介绍 1998 · 李济芝 河北唐山 15176733539 m15176733539@163.com 本人有严谨的工作态度与高质量意识;能查阅各种开发技术手册,具有独立解决问题的能力。具备扎实的Java基础和三年开发经验,有良好的编程风格,独立熟练使用Spring全家桶等常用类库开发Java服务端程序、对SQL......
+写在前面 本文中所涉及的程序均为Java开发,如果您想要直接使用这些工具需要提前配置Java环境。所涉及到的程序均提供完整代码,如果您有兴趣可以尝试运行。 使用java -jar命令启动 某些程序功能并不是很完善,但是也可以凑合着用,写这些程序的主......
+数据结构 数据结构是一门研究组织数据方式的学科,有了编程语言也就有了数据结构,学好数据结构可以编写出更有效率的代码。数据结构是算法的基础,想要学好算法,就必须把数据结构学到位。 数据结构包括:线性结构、非线性结构。 线性结构作为最常用的数据结构,......
+编码规范 我们为什么要遵守规范来编码? 是因为通常在编码过程中我们不只自己进行开发,通常需要一个团队来进行,开发好之后还需要维护,所以编码规范就显的尤为重要。 代码维护时间比较长,那么保证代码可读性就显得很重要。作为一个程序员,咱们得有点追求和信......
+网络协议 以下内容摘自百度百科: https://baike.baidu.com/item/网络协议/328636 https://baike.baidu.com/item/网络七层协议/6056879 网络协议指的是计算机网络中互相通信的对等实体......
+概念 MQ 即 messagequeue 消息队列,是分布式系统的重要组件,主要解决异步消息,应用解耦,消峰等问题。从而实现高可用,高性能,可伸缩和最终一致性的架构。使用较多的MQ有:activeMQ,rabbitMQ,kafka,metaMQ。 MQ优点 异步消息处理:可以......
+概述 Java中的集合主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。 如果你看过ArrayList类源码,就知道ArrayList底层是通过数组来存储元素的,所以如果严格来说,数组也算集合的一种......
+概述 什么是反射 在运行状态中,对于任意一个实体类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。 反射是Java语言的一个特性,它允许程序......
+故障排查基础 收录Linux常用命令,以下命令来自https://www.bilibili.com/video/BV14A411378a 关机/重启/注销 常用命令 作用 shutdown -h now 即刻关机 shutdown -h 10 10分钟后关机 shutdown -h 11:00 11:00关机 shutdown -h +10 预定时间关机(10......
+基础概念 什么是事务 什么是事务?举个例子:你去超市买东西,“一手交钱,一手交货"就是一个事务的例子,交钱和交货必须同时成功,事务才算成功,其中有一个环节失败,事务将会撤销所有已成功的活动。 所以事务可以看作是一次重大的活动......
+概览 Object 类位于 java.lang 包中,编译时会自动导入,我们创建一个类时,如果没有明确继承一个父类,那么它就会自动继承Object,成为Object的子类。 Object类可以显示继承,也可以隐式继承,效果都是一样的。 class A extends Object{ // to do } class A { // to do } Java Objec......
+什么是微服务架构 In short, the microservice architectural style is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API。 These services are built around business capabilities and independently deployable by fully automated deployment machinery。 There is a bare minimum of centralized management of these services, which may be written in different programming......
+Redis概述 参考文章: https://www.runoob.com/redis/redis-intro.html https://www.redis.com.cn/redis-interview-questions.html 什么是Redis Redis(Remote Dictionary Server) Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API 的非关系型数据库。 简而言之,Redis是一个可基于内存亦可持久......
+概览 Spring是一个轻量级的Java开源框架,为了解决企业应用开发的复杂性而创建的。Spring的核心是控制反转(IOC)和面向切面(AOP)。 简单来说,Spring是一个分层的JavaSE/EE 一站式轻量级开源框架。在每一层都提供支持。......
+面试必问 自我介绍一下 你有什么职业规划 你为什么要离职 说一下你的优缺点 你的期望薪资是多少 你为什么要选择我们公司 你能否接受加班 你有对象了吗 你还有什么问题要问的吗 基础 说一下UDP、TCP及http与https 如何保证线程安全 线程池工作原理 如何避免死......
+垃圾回收器分类 垃圾收集器没有在规范中进行过多的规定,可以由不同的厂商、不同版本的JVM来实现。 由于JDK的版本处于高速迭代过程中,因此Java发展至今已经衍生了众多的GC版本。 Java不同版本新特性学习思路: 语法层面:Lambda表达式、s......
+相关概念 线程与进程 进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。例如,一个正在运行的程序的实例就是一个进程。 线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。 一条线程指的是进程中一个单一......
+相关概念 capacity: 容量,默认16; loadFactor: 负载因子,表示HashMap满的程度,默认值为0.75f,也就是说默认情况下,当HashMap中元素个数达到了容量的3/4的时候就会进行自动扩容; threshold: 阈值;阈值......
+内存溢出 内存溢出(Out Of Memory,简称OOM)是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。 官方文档中对内存溢出的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。 由于GC一......
+面试常见问题 自我介绍 个人经历可以进行适当包装,但是不能造假,一方面如果一旦被人拆穿,后果就不用我说了吧,另一方面如果你说谎,说了一些你自己不感兴趣的项目,在入职之后可能会被分配到该项目上; 在介绍的时候要说明你对面试的公司有什么用,根据不同类......
++ +
+ + + + + +垃圾回收 垃圾收集,不是Java语言的伴生产物。早在1960年,第一门开始使用内存动态分配和垃圾收集技术的Lisp语言诞生。 垃圾收集机制是Java的招牌能力,极大地提高了开发效率。 如今,垃圾收集几乎成为现代语言的标配,即使经过如此长时间的发展......
+概述 执行引擎是Java虚拟机核心的组成部分之一,属于JVM的下层,里面包括 解释器、及时编译器、垃圾回收器。 “虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力, 其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统......
+直接内存不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。 直接内存是在Java堆外的、直接向系统申请的内存区间。 操作直接内存演示代码: public class MainTest { public static void main(String[] args) { ByteBuffer allocate = ByteBuffer.allocate(1024 * 1024 * 1024); System.out.println(......
+对象实例化 对象的创建方式 使用new关键字创建:最常见的方式、单例类中调用getInstance的静态类方法,XXXFactory的静态方法; 使用反射方式创建: 使用Class的newInstance方法:在JDK9里面被标记为过时的方法,因为......
+原文地址:https://www.jianshu.com/p/0f967298a5d7 语法糖 语法糖(Syntactic Sugar),也称糖衣语法,是由英国计算机学家 Peter.J.Landin 发明的一个术语,指在计算机语言中添加的某种语法, 这种语法对语言的功能并没有......
+概念 Java IO通过数据流、序列化和文件系统提供系统输入和输出。 IO,即 in 和 out,也就是输入和输出,指应用程序和外部设备之间的数据传递,常见的外部设备包括文件、管道、网络连接。 传统的 IO 是通过流技术来处理的。 流(Stream),是一个抽象的概念,......
+Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚......
+Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚......
+概念 简单地讲,一个Native Methodt是一个Java调用非Java代码的接囗。 一个Native Method是这样一个Java方法:该方法的实现由非Java语言实现,比如C。 这个特征并非Java所特有,很多其它的编程语言都有这一机制,比......
+Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚......
+Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚......
+Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚......
+为什么要学习JVM 大部分Java开发人员,除了会在项目中使用到与Java平台相关的各种高精尖技术,对于Java技术的核心Java虚拟机了解甚少。 一些有一定工作经验的开发人员,打心眼儿里觉得SSM、微服务等上层技术才是重点,基础技术并不重要,......
+Nginx介绍 Nginx (“engine x”)是一个高性能的HTTP和反向代理服务器,特点是占有内存少,并发能力强,事实上Nginx的并发能力确实在同类型的网页服务器中表现较好. Nginx专为性能优化而开发,性能是其最重要的考量,实现上非常注重效率,......
+第一章 道可道,非常道。名可名,非常名。 无名天地之始﹔有名万物之母。 故常无,欲以观其妙﹔常有,欲以观其徼。 此两者,同出而异名,同谓之玄。 玄之又玄,众妙之门。 第二章 天下皆知美之为美,斯恶已。 皆知善之为善,斯不善已。 有无相生,难易相成,长短相形,......
+面向对象是一种编程思想,包括三大特性和六大原则,其中,三大特性指的是封装、继承和多态;六大原则指的是单一职责原则、开放封闭原则、迪米特原则、里氏替换原则、依赖倒置原则以及接口隔离原则,其中,单一职责原则是指一个类应该是一组相关性很高的函数和......
+类加载过程 在Java中,类加载器把一个类装入JVM中,要经过以下步骤: 加载、验证、准备、解析和初始化。其中验证,准备,解析统称为连接。 这5个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段发生在初始化阶段之后。 类加载器只负责class文......
+运算符与表达式 运算符 运算符指明对操作数的运算方式。组成表达式的Java操作符有很多种。运算符按照其要求的操作数数目来分,可以有单目运算符、双目运算符和三目运算符,它们分别对应于1个、2个、3个操作数。 种类 运算符按其功能来分:有算术运算符、赋......
+基本类型 Java语言提供了八种基本类型。六种数值类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型. 俗称4类8种 这里只介绍称4类8种.实际上,JAVA中还存在另外一种基本类型 void,它也有对应的包装类java.lang.Void......
+异常类型 Throwable 可以用来表示任何可以作为异常抛出的类,分为两种:Error 和 Exception。 其中 Error 用来表示Java程序无法处理的错误;这类错误一般与硬件有关,与程序本身无关,通常由系统进行处理,程序本身无法捕获和处理。是不可控制的。 Exception 分为两种......
++ +
+ + + + + +自我介绍 1998 · 李济芝 河北唐山 15176733539 m15176733539@163.com 专业技能 熟练使用 SSM,SpringBoot等框架技术; 熟练使用HTML,CSS等相关技术; 有Redis,VUE相关使用经验; 有对接第三方系统,调用外系统相关经验; 熟悉 MySQL,ORACLE.基本操作,熟练使......
+MacOS上安装docker 下载 国内下载网站: http://get.daocloud.io 不推荐下载docker版本太旧了 官网下载: https://docs.docker.com/get-started/#download-and-install-docker 或用homebrew进行下载安装 brew install --cask --appdir=/Applications docker 配置镜像 由于网速原因,可以配置一下国内的镜像加速器 中科大镜像: https://docker.mirrors.ustc.edu.cn 网易: https://hub-mirror.c.163.com 阿里云: https://<你......
+kafka介绍 kafka官网: http://kafka.apache.org kafka中文官网: https://kafka.apachecn.org Kafka是一种分布式的,基于发布/订阅的消息系统。主要特点如下: 以时间复杂度为O(1)的方式提供消息持久化能力,并保证即使对TB级以上数据也能保证常数时间的访问性能 高吞吐率。即使在非常......
+线程状态及转换 线程状态共包含6种,6中状态又可以互相的转换。 新建状态(New): 创建了线程后尚未启动; 可运行状态(Runnable): 可能正在运行,也可能正在等待 CPU 时间片。包含了运行中(Running)和 就绪(Ready)状态; 就绪(Rea......
+docker是什么 Docker 属于 Linux 容器的一种封装,提供简单易用的容器使用接口。它是目前流行的 Linux 容器解决方案。 Docker 将应用程序与该程序的依赖,打包在一个文件里面。运行这个文件,就会生成一个虚拟容器。程序在这个虚拟容器里运行,就好像在真实的物理机上运行一样......
+公平锁 指多个线程按照申请锁的顺序来获取锁类似排队打饭 先来后到 优点: 所有的线程都能得到资源,不会饿死在队列中。 缺点: 吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。 非公平锁 指在多线程获取锁的顺序并......
+ArrayList ArrayList线程不安全示例: public static void main(String[] args) { ArrayList<String> arrayList = new ArrayList<>(); for(int i=0; i< 3; i++) { new Thread(() -> { arrayList.add(UUID.randomUUID().toString()); System.out.println(arrayList); },String.valueOf(i)).start(); } } // ConcurrentModificationException 同步修改异常 Exception in thread "8" java.util.ConcurrentModificationException [null, 2041b613-8068-4ddd-9d01-305f5680d377] [null, 2041b613-8068-4ddd-9d01-305f5680d377, b3e0296d-e263-4632-a023-4267cdec5e25] [null, 2041b613-8068-4ddd-9d01-305f5680d377] 原因分析: 当某个线程正在执行 add()方法时,被某个线程打断,添加到一半被打断,没有被添加完 解决方案: 使用Vec......
+CAS CAS全称为Compare and Swap被译为比较并交换。是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。 java.util.concurrent.atomic 并发包下的所有原子类都是基于 CAS 来实现的。 以 AtomicInteger 原子整型类为例。 public class MainTest { public static void main(String[] args) { new AtomicInteger().compareAndSet(1,2); } } 以上面的代码为例......
+Redis介绍 redis是开源的一个高性能的 key-value 数据库。 主要特点 Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用 Redis支持数据的备份,即master-slave模式的数据备份 Redis 可以存储键与5种不同......
+安装elasticsearch 要注意导入依赖的版本和安装elasticsearch的版本与springboot的兼容问题 用 docker 安装 elasticsearch 本例用elasticsearch-6.5.3和springboot-2.1.0.RELEASE版本 下载镜像: docker......
+本人在进入公司起,期间一直对自己要求严谨,遵守公司的相应制度. 在过去的一个月时间里,我参与了贵州银行的电子验印系统的开发,一直努力完成和完善分配给我的任务,在这一个月发现了自身还有很多的不足,所以抱着虚心学习的态度,学习公司的开发流程,了解......
+参考资料 vue官方文档: https://cn.vuejs.org/v2/guide vue参考视频资料: https://www.bilibili.com/video/av50680998 vue菜鸟教程文档: https://www.runoob.com/vue2/vue-tutorial.html vue-组件 参考资料: https://cn.vuejs.org/v2/guide/components.html#ad 组件是可复用的 Vue 实例,且带有一个名字. 组件的出现是为了拆分vue实例的代码量,能够让我们以不同的组件,来划分不同的功能模块,将来我们需要什么样的功......
+index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>snow</title> </head> <style> html { width: 100%; } body { margin: 0; padding: 0; overflow-y: hidden; width: 100%; } .header { width: 100%; height: 315px; background: url("images/header-bg.png") repeat; } .snow { position: relative; height: inherit; width: 960px; background: url("images/con-bg.png") no-repeat 0 204px, url("images/snow-bg.png") no-repeat 0 0;; margin: 0 auto; animation: auto 10s linear infinite; } /* 下雪动画 插入两个背景图片*/ @keyframes auto { from { background: url("images/con-bg.png") no-repeat 0 204px, url("images/snow-bg.png") repeat 0 0; } to { background: url("images/con-bg.png") no-repeat 0 204px, url("images/snow-bg.png") repeat 0 1000px; } } tree, snow { position: absolute; } tree { width: 112px; height: 137px; background: url("images/tree.png");......
+index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title> rain </title> <style> html { width: 100%; } body { width: 100%; margin: 0; padding: 0; background-color: #000; } .rain { display: block; } embed { display: block; } </style> </head> <body> <!-- 2、使用hidden="true"表示隐藏音乐播放按钮,相反使用hidden="false"表示开启音乐播放按钮。 3、使用a......
+index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>换肤特效</title> <style type="text/css"> body { margin: 0; background-image: url("images/1.jpg"); background-size: cover; } ul { margin: 0; padding: 0; list-style-type: none; } .bg-list { display: none; margin: 0; width: 100%; height: 200px; background: rgba(0, 0, 0, 0.5); } .img-wrap { height: 200px; display: flex; justify-content: space-around; align-items: center; } .tab-btn { background-image: url("images/upseek.png"); height: 50px; width: 50px; position: fixed; top: 0; right: 0; } .tab-btn:hover { background-position-y: -63.6px; } </style> </head> <body> <div class="bg-list"> <ul class="img-wrap"> <li class="img-item" data-src="images/1.jpg"> <img src="images/1-1.jpg" width="160px"/> </li>......
+index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>折纸导航栏</title> </head> <style> *{ margin: 0; padding: 0; } .content{ position: relative; width: 400px; height: 30px; margin: 50px auto; /*-webkit-perspective: 1000px; -moz-perspective: 1000px; -ms-perspective: 1000px;*/ perspective: 1000px;/*景深相当于眼睛距离元素的位置距离*/ } .content .open{ transform: rotateX(0); animation: open 1s linear; } @keyframes open { 0%{ transform: rotateX(-90deg); } 20%{ transform:rotateX(30deg); } 40%{ transform:rotateX(-60deg); } 60%{ transform:rotateX(60deg); } 80%{ transform:rotateX(-30deg);......
+index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>love</title> <style> *{ margin: 0; padding: 0; } body{ background-color: #000; background-size: cover; overflow-y: hidden; } .love{ width: 400px; height: 400px; /*background-color: #7c7c7c;*/ margin: 130px auto; animation: move 1s infinite alternate; } @keyframes move { 100%{ transform: scale(1.5); } } .left{ float: left; width: 150px; height: 250px; background-color: #FF0000; border-radius: 75px 75px 0 5px; -webkit-transform: rotate(-45deg); -moz-transform: rotate(-45deg); -ms-transform: rotate(-45deg); -o-transform: rotate(-45deg); transform: rotate(-45deg); margin-left: 85px; box-shadow: 0 0 20px #FF0000; animation: shadow 1s infinite alternate; } @keyframes shadow { 100%{ box-shadow: 0 0 100px #FF0000; } } .right{ float: left; width: 150px; height: 250px; background-color: #FF0000; border-radius: 75px 75px 5px 0; -webkit-transform: rotate(45deg); -moz-transform: rotate(45deg); -ms-transform: rotate(45deg); -o-transform: rotate(45deg); transform: rotate(45deg);......
+index.html <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="Generator" content="EditPlus®"> <meta name="Author" content=""> <meta name="Keywords" content=""> <meta name="Description" content=""> <title>懒加载技术</title> <style> *{ margin: 0; padding:0; } body{ background: rgb(0,0,0); } .box{ overflow: hidden; width: 948px; background-color: #7c7c7c; margin: 50px auto; -webkit-border-radius: 10px; -moz-border-radius: 10px; border-radius: 10px; } .box img{ float: left; display: block; width: 300px; height: 150px; margin:......
+index.html <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>五子棋</title> <meta name="viewport" content="device-width; initial-scale=1.0;" /> <style> #c1 { display: block; margin: 60px auto; box-shadow: 1px 1px 5px #000000; } </style> <script src="js/index.js"></script> </head> <body> <canvas id="c1" width="450px" height="450px"></canvas> </body> </html> index.js window.onload = function(){ var oC = document.getElementById('c1'); var oGc = oC.getContext('2d'); var over = false; oGc.strokeStyle = "#bfbfbf"; //绘制棋盘 for(var i=0;i<15;i++){ oGc.moveTo(15+i*30,15); oGc.lineTo(15+i*30,435); oGc.stroke(); oGc.moveTo(15,15+i*30); oGc.lineTo(435,15+i*30); oGc.stroke(); } /* AI难点解析 赢法......
+index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>滑块拖拽</title> </head> <style> body { margin: 0; padding: 0; user-select: none; } .content { position: relative; width: 300px; height: 40px; margin: 50px auto; background-color: #E8E8EB; text-align: center; line-height: 40px; } .rect { position: absolute; width: 100%; height: 100%; } .rect .bg { position: absolute; left: 0; top: 0; z-index: 1; width: 0; height: 100%; background: rgba(122,194,60,.4); } .rect .move { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; width: 45px; height: 40px; position: absolute; top: 0; left: 0; background-color: #fff; border: 1px solid #cccccc;......
++ +
+ + + + + +index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>card</title> <style> body,html{ width: 100%; height: 100%; } body{ display: flex;/*弹性盒模型*/ justify-content: center;/*水平对齐 盒子位于中心*/ align-items: center;/*竖直对齐 居中对齐*/ background-color: yellow; perspective: 1000px;/*景深:眼到屏幕的距离*/ } body,h1,p{ margin: 0; } .card{ width: 520px; height: 350px; border-radius: 15px; background: linear-gradient(#020333 70%,#fff 75%);/*......
++ +
+ + + + + +道可道,非常道。名可名,非常名。 +无名天地之始﹔有名万物之母。 +故常无,欲以观其妙﹔常有,欲以观其徼。 +此两者,同出而异名,同谓之玄。 +玄之又玄,众妙之门。
+天下皆知美之为美,斯恶已。 +皆知善之为善,斯不善已。 +有无相生,难易相成,长短相形,高下相盈,音声相和,前后相随。恒也。 +是以圣人处无为之事,行不言之教﹔ +万物作而弗始,生而弗有,为而弗恃,功成而不居。 +夫唯弗居,是以不去。
+不尚贤,使民不争 +不贵难得之货,使民不为盗﹔ +不见可欲,使民心不乱。 +是以圣人之治, +虚其心,实其腹, +弱其志,强其骨。 +常使民无知无欲。 +使夫智者不敢为也。 +为无为,则无不治。
+道冲,而用之或不盈。 +渊兮,似万物之宗﹔湛兮,似或存。 +吾不知谁之子,象帝之先。
+天地不仁,以万物为刍狗﹔ +圣人不仁,以百姓为刍狗。 +天地之间,其犹橐龠乎。 +虚而不屈,动而愈出。 +多言数穷,不如守中。
+谷神不死,是谓玄牝。 +玄牝之门,是谓天地根。 +绵绵若存,用之不勤。
+天长地久。 +天地所以能长且久者, +以其不自生,故能长生。 +是以圣人后其身而身先﹔外其身而身存。 +非以其无私邪。故能成其私。
+上善若水。 +水善利万物而不争, +处众人之所恶,故几于道。 +居善地,心善渊, +与善仁,言善信, +政善治,事善能,动善时。 +夫唯不争,故无尤。
+持而盈之,不如其已﹔ +揣而锐之,不可长保。 +金玉满堂,莫之能守﹔ +富贵而骄,自遗其咎。 +功遂身退,天之道也。
+载营魄抱一,能无离乎。 +专气致柔,能如婴儿乎。 +涤除玄鉴,能无疵乎。 +爱国治民,能无为乎。 +天门开阖,能为雌乎。 +明白四达,能无知乎。
+三十辐,共一毂,当其无,有车之用。 +埏埴以为器,当其无,有器之用。 +凿户牖以为室,当其无,有室之用。 +故有之以为利,无之以为用。
+五色令人目盲﹔五音令人耳聋﹔五味令人口爽﹔ +驰骋畋猎,令人心发狂﹔难得之货,令人行妨。 +是以圣人为腹不为目,故去彼取此。
+宠辱若惊,贵大患若身。 +何谓宠辱若惊。 +宠为下,得之若惊,失之若惊,是谓宠辱若惊。 +何谓贵大患若身。 +吾所以有大患者,为吾有身, +及吾无身,吾有何患。 +故贵以身为天下,若可寄天下﹔ +爱以身为天下,若可托天下。
+视之不见,名曰夷﹔ +听之不闻,名曰希﹔ +搏之不得,名曰微。 +此三者不可致诘,故混而为一。 +其上不皦,其下不昧。 +绳绳兮不可名,复归于无物。 +是谓无状之状,无物之象,是谓惚恍。 +迎之不见其首,随之不见其后。 +执古之道,以御今之有。 +能知古始,是谓道纪。
+古之善为道者,微妙玄通,深不可识。 +夫唯不可识,故强为之容: +豫兮若冬涉川﹔ +犹兮若畏四邻﹔ +俨兮其若容﹔ +涣兮若冰之将释﹔ +敦兮其若朴﹔ +旷兮其若谷﹔ +混兮其若浊﹔ +澹兮其若海﹔ +飂兮若无止。 +孰能浊以静之徐清。 +孰能安以动之徐生。 +保此道者,不欲盈。 +夫唯不盈,故能蔽而新成。
+致虚极,守静笃。 +万物并作,吾以观复。 +夫物芸芸,各复归其根。 +归根曰静,静曰复命。 +复命曰常,知常曰明。 +不知常,妄作凶。 +知常容,容乃公, +公乃全,全乃天, +天乃道,道乃久,没身不殆。
+太上,不知有之﹔ +其次,亲而誉之﹔ +其次,畏之﹔ +其次,侮之。 +信不足焉,有不信焉。 +悠兮其贵言。 +功成事遂,百姓皆谓:「我自然」。
+大道废,有仁义﹔ +智慧出,有大伪﹔ +六亲不和,有孝慈﹔ +国家昏乱,有忠臣。
+绝圣弃智,民利百倍﹔ +绝仁弃义,民复孝慈﹔ +绝巧弃利,盗贼无有。 +此三者以为文不足,故令有所属。 +见素抱朴,少思寡欲,绝学无忧。
+唯之与阿,相去几何。 +善之与恶,相去若何。 +人之所畏,不可不畏。 +荒兮,其未央哉。 +众人熙熙,如享太牢,如春登台。 +我独泊兮,其未兆﹔ +沌沌兮,如婴儿之未孩﹔ +儽儽兮,若无所归。 +众人皆有余,而我独若遗。我愚人之心也哉。 +俗人昭昭,我独昏昏。 +俗人察察,我独闷闷。 +众人皆有以,而我独顽且鄙。 +我独异于人,而贵食母。
+孔德之容,惟道是从。 +道之为物,惟恍惟惚。 +惚兮恍兮,其中有象﹔ +恍兮惚兮,其中有物。 +窈兮冥兮,其中有精﹔ +其精甚真,其中有信。 +自今及古,其名不去,以阅众甫。 +吾何以知众甫之状哉。以此。
+曲则全,枉则直,洼则盈, +敝则新,少则得,多则惑。 +是以圣人抱一为天下式。 +不自见,故明﹔ +不自是,故彰﹔ +不自伐,故有功﹔ +不自矜,故长。 +夫唯不争,故天下莫能与之争。 +古之所谓「曲则全」者,岂虚言哉。 +诚全而归之。
+希言自然。 +故飘风不终朝,骤雨不终日。 +孰为此者。 +天地。天地尚不能久,而况于人乎。 +故从事于道者,同于道﹔ +德者,同于德﹔失者,同于失。 +同于道者,道亦乐得之﹔ +同于德者,德亦乐得之﹔ +同于失者,失亦乐得之。 +信不足焉,有不信焉。
+企者不立﹔跨者不行﹔ +自见者不明﹔自是者不彰﹔ +自伐者无功﹔自矜者不长。 +其在道也,曰:余食赘形。 +物或恶之,故有道者不处。
+有物混成,先天地生。 +寂兮寥兮,独立而不改, +周行而不殆,可以为天地母。 +吾不知其名,强字之曰道,强为之名曰大。 +大曰逝,逝曰远,远曰反。 +故道大,天大,地大,人亦大。 +域中有四大,而人居其一焉。 +人法地,地法天,天法道,道法自然。
+重为轻根,静为躁君。 +是以君子终日行不离辎重。 +虽有荣观,燕处超然。 +奈何万乘之主,而以身轻天下。 +轻则失根,躁则失君。
+善行无辙迹,善言无瑕谪﹔ +善数不用筹策﹔ +善闭无关楗而不可开, +善结无绳约而不可解。 +是以圣人常善救人,故无弃人﹔ +常善救物,故无弃物。 +是谓袭明。 +故善人者,不善人之师﹔ +不善人者,善人之资。 +不贵其师,不爱其资, +虽智大迷,是谓要妙。
+知其雄,守其雌,为天下溪。 +为天下溪,常德不离,复归于婴儿。 +知其白,守其黑,为天下式。 +为天下式,常德不忒,复归于无极。 +知其荣,守其辱,为天下谷。 +为天下谷,常德乃足。 +复归於朴,朴散则为器。 +圣人用之,则为官长,故大制不割。
+将欲取天下而为之,吾见其不得已。 +天下神器,不可为也,不可执也。 +为者败之,执者失之。 +是以圣人无为,故无败﹔ +无执,故无失。 +夫物或行或随﹔或嘘或吹﹔ +或强或羸﹔或挫或隳。 +是以圣人去甚,去奢,去泰。
+以道佐人主者,不以兵强天下。 +其事好远。 +师之所处,荆棘生焉。 +大军之后,必有凶年。 +善有果而已,不以取强。 +果而勿矜,果而勿伐,果而勿骄。 +果而不得已,果而勿强。 +物壮则老,是谓不道,不道早已。
+夫兵者,不祥之器, +物或恶之,故有道者不处。 +君子居则贵左,用兵则贵右。 +兵者不祥之器,非君子之器, +不得已而用之,恬淡为上。 +胜而不美,而美之者,是乐杀人。 +夫乐杀人者,则不可得志于天下矣。 +吉事尚左,凶事尚右。 +偏将军居左,上将军居右,言以丧礼处之。 +杀人之众,以悲哀泣之,战胜以丧礼处之。
+道常无名。 +朴虽小,天下莫能臣。 +侯王若能守之,万物将自宾。 +天地相合,以降甘露,民莫之令而自均。 +始制有名,名亦既有, +夫亦将知止,知止可以不殆。 +譬道之在天下,犹川谷之于江海。
+知人者智,自知者明。 +胜人者有力, +自胜者强,知足者富。 +强行者有志。 +不失其所者久。 +死而不亡者寿。
+大道泛兮,其可左右。 +万物恃之以生而不辞,功成而不有。 +衣养万物而不为主。常无欲可名于小﹔ +万物归焉而不为主,可名为大。 +以其终不自为大,故能成其大。
+执大象,天下往。 +往而不害,安平泰。 +乐与饵,过客止。 +道之出口,淡乎其无味, +视之不足见,听之不足闻,用之不足既。
+将欲歙之,必故张之﹔ +将欲弱之,必故强之﹔ +将欲废之,必故兴之﹔ +将欲取之,必故与之。 +是谓微明。 +柔弱胜刚强。 +鱼不可脱于渊,国之利器不可以示人。
+道常无为而无不为。 +侯王若能守之,万物将自化。 +化而欲作,吾将镇之以无名之朴。 +无名之朴,夫亦将不欲。 +不欲以静,天下将自定。
+上德不德,是以有德﹔ +下德不失德,是以无德。 +上德无为而无以为﹔ +下德无为而有以为。 +上仁为之而无以为﹔ +上义为之而有以为。 +上礼为之而莫之应, +则攘臂而扔之。 +故失道而后德,失德而后仁, +失仁而后义,失义而后礼。 +夫礼者,忠信之薄,而乱之首。 +前识者,道之华,而愚之始。 +是以大丈夫处其厚,不居其薄﹔ +处其实,不居其华。故去彼取此。
+昔之得一者: +天得一以清﹔ +地得一以宁﹔ +神得一以灵﹔ +谷得一以生﹔ +侯王得一以为天下贞。 +其致之也,谓天无以清,将恐裂﹔ +地无以宁,将恐废﹔ +神无以灵,将恐歇﹔ +谷无以盈,将恐竭﹔ +万物无以生,将恐灭﹔ +侯王无以贞,将恐蹶。 +故贵以贱为本,高以下为基。 +是以侯王自称孤、寡、不谷。 +此非以贱为本邪。非乎。故致誉无誉。 +是故不欲琭琭如玉,珞珞如石。
+反者道之动﹔弱者道之用。 +天下万物生于有,有生于无。
+上士闻道,勤而行之﹔ +中士闻道,若存若亡﹔ +下士闻道,大笑之。 +不笑不足以为道。 +故建言有之: +明道若昧﹔进道若退﹔夷道若颣﹔ +上德若谷﹔广德若不足﹔ +建德若偷﹔质真若渝﹔ +大白若辱﹔大方无隅﹔ +大器晚成﹔大音希声﹔ +大象无形﹔道隐无名。 +夫唯道,善贷且成。
+道生一,一生二,二生三,三生万物。 +万物负阴而抱阳,冲气以为和。 +人之所恶,唯孤、寡、不谷,而王公以为称。 +故物或损之而益,或益之而损。 +人之所教,我亦教之。 +强梁者不得其死,吾将以为教父。
+天下之至柔,驰骋天下之至坚。 +无有入无间,吾是以知无为之有益。 +不言之教,无为之益,天下希及之。
+名与身孰亲。身与货孰多。得与亡孰病。 +甚爱必大费﹔多藏必厚亡。 +故知足不辱,知止不殆,可以长久。
+大成若缺,其用不弊。 +大盈若冲,其用不穷。 +大直若屈,大巧若拙,大辩若讷。 +静胜躁,寒胜热。清静为天下正。
+天下有道,却走马以粪。 +天下无道,戎马生于郊。 +祸莫大于不知足﹔咎莫大于欲得。 +故知足之足,常足矣。
+不出户,知天下﹔不窥牖,见天道。 +其出弥远,其知弥少。 +是以圣人不行而知,不见而明,不为而成。
+为学日益,为道日损。 +损之又损,以至于无为。 +无为而无不为。 +取天下常以无事,及其有事,不足以取天下。
+圣人常无心,以百姓心为心。 +善者,吾善之﹔不善者,吾亦善之﹔德善。 +信者,吾信之﹔不信者,吾亦信之﹔德信。 +圣人在天下,歙歙焉,为天下浑其心, +百姓皆注其耳目,圣人皆孩之。
+出生入死。 +生之徒,十有三﹔ +死之徒,十有三﹔ +人之生,动之于死地,亦十有三。 +夫何故,以其生之厚。 +盖闻善摄生者,路行不遇兕虎,入军不被甲兵﹔ +兕无所投其角,虎无所用其爪,兵无所容其刃。 +夫何故,以其无死地。
+道生之,德畜之, +物形之,势成之。 +是以万物莫不尊道而贵德。 +道之尊,德之贵,夫莫之命而常自然。 +故道生之,德畜之﹔ +长之育之﹔成之熟之﹔养之覆之。 +生而不有,为而不恃, +长而不宰。是谓玄德。
+天下有始,以为天下母。 +既得其母,以知其子, +复守其母,没身不殆。 +塞其兑,闭其门,终身不勤。 +开其兑,济其事,终身不救。 +见小曰明,守柔曰强。 +用其光,复归其明, +无遗身殃﹔是为袭常。
+使我介然有知,行于大道,唯施是畏。 +大道甚夷,而人好径。 +朝甚除,田甚芜,仓甚虚﹔ +服文采,带利剑,厌饮食, +财货有余﹔是为盗夸。 +非道也哉。
+善建者不拔, +善抱者不脱,子孙以祭祀不辍。 +修之于身,其德乃真﹔ +修之于家,其德乃余﹔ +修之于乡,其德乃长﹔ +修之于邦,其德乃丰﹔ +修之于天下,其德乃普。 +故以身观身,以家观家,以乡观乡,以邦观邦,以天下观天下。 +吾何以知天下然哉。以此。
+含「德」之厚,比于赤子。 +毒虫不螫,猛兽不据,攫鸟不搏。 +骨弱筋柔而握固。 +未知牝牡之合而峻作,精之至也。 +终日号而不嗄,和之至也。 +知和曰「常」,知常曰「明」。 +益生曰祥。心使气曰强。 +物壮则老,谓之不道,不道早已。
+知者不言,言者不知。 +挫其锐,解其纷。 +和其光,同其尘,是谓「玄同」。 +故不可得而亲,不可得而疏﹔ +不可得而利,不可得而害﹔ +不可得而贵,不可得而贱。故为天下贵。
+以正治国,以奇用兵,以无事取天下。 +吾何以知其然哉。以此: +天下多忌讳,而民弥贫﹔ +人多利器,国家滋昏﹔ +人多伎巧,奇物滋起﹔ +法令滋彰,盗贼多有。 +故圣人云: +「我无为,而民自化﹔ +我好静,而民自正﹔ +我无事,而民自富﹔ +我无欲,而民自朴。」
+其政闷闷,其民淳淳﹔ +其政察察,其民缺缺。 +祸兮福之所倚,福兮祸之所伏。 +孰知其极。其无正也。 +正复为奇,善复为妖。 +人之迷,其日固久。 +是以圣人方而不割,廉而不刿,直而不肆,光而不耀。
+治人事天,莫若啬。 +夫唯啬,是谓早服﹔ +早服谓之重积德﹔重积德则无不克﹔ +无不克则莫知其极﹔莫知其极,可以有国﹔ +有国之母,可以长久﹔ +是谓深根固柢,长生久视之道。
+治大国,若烹小鲜。 +以道莅天下,其鬼不神﹔ +非其鬼不神,其神不伤人﹔ +非其神不伤人,圣人亦不伤人。 +夫两不相伤,故德交归焉。
+大邦者下流,天下之交,天下之牝。 +牝常以静胜牡,以静为下。 +故大邦以下小邦,则取小邦﹔ +小邦以下大邦,则取大邦。 +故或下以取,或下而取。 +大邦不过欲兼畜人, +小邦不过欲入事人。 +夫两者各得所欲,大者宜为下。
+道者万物之奥。善人之宝,不善人之所保。 +美言可以市尊,美行可以加人。 +人之不善,何弃之有。 +故立天子,置三公, +虽有拱璧以先驷马,不如坐进此道。 +古之所以贵此道者何。 +不曰:求以得,有罪以免邪。故为天下贵。
+为无为,事无事,味无味。 +图难于其易,为大于其细﹔ +天下难事,必作于易, +天下大事,必作于细。 +是以圣人终不为大,故能成其大。 +夫轻诺必寡信,多易必多难。 +是以圣人犹难之,故终无难矣。
+其安易持,其未兆易谋。 +其脆易泮,其微易散。 +为之于未有,治之于未乱。 +合抱之木,生于毫末﹔ +九层之台,起于累土﹔ +千里之行,始于足下。 +民之从事,常于几成而败之。 +慎终如始,则无败事。
+古之善为道者,非以明民,将以愚之。 +民之难治,以其智多。 +故以智治国,国之贼﹔ +不以智治国,国之福。 +知此两者亦稽式。 +常知稽式,是谓「玄德」。 +「玄德」深矣,远矣,与物反矣,然后乃至大顺。
+江海之所以能为百谷王者, +以其善下之,故能为百谷王。 +是以圣人欲上民,必以言下之﹔ +欲先民,必以身后之。 +是以圣人处上而民不重,处前而民不害。 +是以天下乐推而不厌。 +以其不争,故天下莫能与之争。
+天下皆谓我道大,似不肖。 +夫唯大,故似不肖。 +若肖,久矣其细也夫。 +我有三宝,持而保之。 +一曰慈,二曰俭, +三曰不敢为天下先。 +慈故能勇﹔俭故能广﹔ +不敢为天下先,故能成器长。 +今舍慈且勇﹔舍俭且广﹔ +舍后且先﹔死矣。 +夫慈以战则胜,以守则固。 +天将救之,以慈卫之。
+善为士者,不武﹔ +善战者,不怒﹔ +善胜敌者,不与﹔ +善用人者,为之下。 +是谓不争之德, +是谓用人之力, +是谓配天古之极。
+用兵有言: +「吾不敢为主,而为客﹔ +不敢进寸,而退尺。」 +是谓行无行﹔攘无臂﹔ +扔无敌﹔执无兵。 +祸莫大于轻敌,轻敌几丧吾宝。 +故抗兵相若,哀者胜矣。
+吾言甚易知,甚易行。 +天下莫能知,莫能行。 +言有宗,事有君。 +夫唯无知,是以不我知。 +知我者希,则我者贵。 +是以圣人被褐而怀玉。
+知不知,尚矣﹔ +不知知,病也。 +圣人不病,以其病病。 +夫唯病病,是以不病。
+民不畏威,则大威至。 +无狎其所居,无厌其所生。 +夫唯不厌,是以不厌。 +是以圣人自知不自见﹔ +自爱不自贵。故去彼取此。
+勇于敢则杀,勇于不敢则活。 +此两者,或利或害。 +天之所恶,孰知其故。 +天之道,不争而善胜,不言而善应,不召而自来,繟然而善谋。 +天网恢恢,疏而不失。
+民不畏死,奈何以死惧之。 +若使民常畏死,而为奇者, +吾得执而杀之,孰敢。 +常有司杀者杀。 +夫代司杀者杀,是谓代大匠斲, +夫代大匠斲者,希有不伤其手矣。
+民之饥,以其上食税之多,是以饥。 +民之难治,以其上之有为,是以难治。 +民之轻死,以其上求生之厚,是以轻死。 +夫唯无以生为者,是贤于贵生。
+人之生也柔弱,其死也坚强。 +草木之生也柔脆,其死也枯槁。 +故坚强者死之徒,柔弱者生之徒。 +是以兵强则灭,木强则折。 +强大处下,柔弱处上。
+天之道,其犹张弓欤。 +高者抑之,下者举之﹔ +有余者损之,不足者补之。 +天之道,损有余而补不足。 +人之道,则不然,损不足以奉有余。 +孰能有余以奉天下,唯有道者。 +是以圣人为而不恃,功成而不处,其不欲见贤。
+天下莫柔弱于水,而攻坚强者莫之能胜,以其无以易之。 +弱之胜强,柔之胜刚, +天下莫不知,莫能行。 +是以圣人云: +「受国之垢,是谓社稷主﹔ +受国不祥,是为天下王。」 +正言若反。
+和大怨,必有余怨﹔ +报怨以德,安可以为善。 +是以圣人执左契,而不责于人。 +有德司契,无德司彻。 +天道无亲,常与善人。
+小国寡民。 +使有什伯之器而不用﹔ +使民重死而不远徙。 +虽有舟舆,无所乘之, +虽有甲兵,无所陈之。 +使民复结绳而用之。 +甘其食,美其服,安其居,乐其俗。 +邻国相望,鸡犬之声相闻, +民至老死,不相往来。
+信言不美,美言不信。 +善者不辩,辩者不善。 +知者不博,博者不知。 +圣人不积,既以为人己愈有, +既以与人己愈多。 +天之道,利而不害﹔ +圣人之道,为而不争。
++ +
+ + + + + +兵者,国之大事,死生之地,存亡之道,不可不察也。
+故经之以五事,校之以计,而索其情:一曰道,二曰天,三曰地,四曰将,五曰法。道者,令民于上同意,可与之死,可与之生,而不危也;天者,阴阳、寒暑、时制也;地者,远近、险易、广狭、死生也;将者,智、信、仁、勇、严也;法者,曲制、官道、主用也。凡此五者,将莫不闻,知之者胜,不知之者不胜。故校之以计,而索其情,曰:主孰有道?将孰有能?天地孰得?法令孰行?兵众孰强?士卒孰练?赏罚孰明?吾以此知胜负矣。将听吾计,用之必胜,留之;将不听吾计,用之必败,去之。
+计利以听,乃为之势,以佐其外。势者,因利而制权也。兵者,诡道也。故能而示之不能,用而示之不用,近而示之远,远而示之近。利而诱之,乱而取之,实而备之,强而避之,怒而挠之,卑而骄之,佚而劳之,亲而离之,攻其无备,出其不意。此兵家之胜,不可先传也。
+夫未战而庙算胜者,得算多也;未战而庙算不胜者,得算少也。多算胜少算,而况于无算乎!吾以此观之,胜负见矣。
+凡用兵之法,驰车千驷,革车千乘,带甲十万,千里馈粮。则内外之费,宾客之用,胶漆之材,车甲之奉,日费千金,然后十万之师举矣。
+其用战也,胜久则钝兵挫锐,攻城则力屈,久暴师则国用不足。夫钝兵挫锐,屈力殚货,则诸侯乘其弊而起,虽有智者不能善其后矣。故兵闻拙速,未睹巧之久也。夫兵久而国利者,未之有也。故不尽知用兵之害者,则不能尽知用兵之利也。
+善用兵者,役不再籍,粮不三载,取用于国,因粮于敌,故军食可足也。国之贫于师者远输,远输则百姓贫;近师者贵卖,贵卖则百姓财竭,财竭则急于丘役。力屈中原、内虚于家,百姓之费,十去其七;公家之费,破军罢马,甲胄矢弓,戟盾矛橹,丘牛大车,十去其六。故智将务食于敌,食敌一钟,当吾二十钟;萁杆一石,当吾二十石。故杀敌者,怒也;取敌之利者,货也。车战得车十乘以上,赏其先得者而更其旌旗。车杂而乘之,卒善而养之,是谓胜敌而益强。
+故兵贵胜,不贵久。
+故知兵之将,民之司命。国家安危之主也。
+夫用兵之法,全国为上,破国次之;全军为上,破军次之;全旅为上,破旅次之;全卒为上,破卒次之;全伍为上,破伍次之。
+是故百战百胜,非善之善也;不战而屈人之兵,善之善者也。故上兵伐谋,其次伐交,其次伐兵,其下攻城。攻城之法,为不得已。修橹轒辒,具器械,三月而后成;距堙,又三月而后已。将不胜其忿而蚁附之,杀士卒三分之一,而城不拔者,此攻之灾也。故善用兵者,屈人之兵而非战也,拔人之城而非攻也,毁人之国而非久也,必以全争于天下,故兵不顿而利可全,此谋攻之法也。
+故用兵之法,十则围之,五则攻之,倍则分之,敌则能战之,少则能逃之,不若则能避之。故小敌之坚,大敌之擒也。
+夫将者,国之辅也。辅周则国必强,辅隙则国必弱。故君之所以患于军者三:不知军之不可以进而谓之进,不知军之不可以退而谓之退,是谓縻军;不知三军之事而同三军之政,则军士惑矣;不知三军之权而同三军之任,则军士疑矣。三军既惑且疑,则诸侯之难至矣。是谓乱军引胜。
+故知胜有五:知可以战与不可以战者胜,识众寡之用者胜,上下同欲者胜,以虞待不虞者胜,将能而君不御者胜。此五者,知胜之道也。故曰:知己知彼,百战不贻;不知彼而知己,一胜一负;不知彼不知己,每战必败。
+昔之善战者,先为不可胜,以待敌之可胜。不可胜在己,可胜在敌。故善战者,能为不可胜,不能使敌之必可胜。故曰:胜可知,而不可为。不可胜者,守也;可胜者,攻也。守则不足,攻则有余。善守者藏于九地之下,善攻者动于九天之上,故能自保而全胜也。见胜不过众人之所知,非善之善者也;战胜而天下曰善,非善之善者也。故举秋毫不为多力,见日月不为明目,闻雷霆不为聪耳。古之所谓善战者,胜于易胜者也。故善战者之胜也,无智名,无勇功,故其战胜不忒。不忒者,其所措胜,胜已败者也。故善战者,立于不败之地,而不失敌之败也。是故胜兵先胜而后求战,败兵先战而后求胜。善用兵者,修道而保法,故能为胜败之政。
+兵法:一曰度,二曰量,三曰数,四曰称,五曰胜。地生度,度生量,量生数,数生称,称生胜。故胜兵若以镒称铢,败兵若以铢称镒。
+称胜者之战民也,若决积水于千仞之溪者,形也。
+凡治众如治寡,分数是也;斗众如斗寡,形名是也;三军之众,可使必受敌而无败者,奇正是也;兵之所加,如以碫投卵者,虚实是也。
+凡战者,以正合,以奇胜。故善出奇者,无穷如天地,不竭如江海。终而复始,日月是也。死而更生,四时是也。声不过五,五声之变,不可胜听也;色不过五,五色之变,不可胜观也;味不过五,五味之变,不可胜尝也;战势不过奇正,奇正之变,不可胜穷也。奇正相生,如循环之无端,孰能穷之哉!
+激水之疾,至于漂石者,势也;鸷鸟之疾,至于毁折者,节也。故善战者,其势险,其节短。势如扩弩,节如发机。纷纷纭纭,斗乱而不可乱;浑浑沌沌,形圆而不可败。乱生于治,怯生于勇,弱生于强。治乱,数也;勇怯,势也;强弱,形也。
+故善动敌者,形之,敌必从之;予之,敌必取之。以利动之,以卒待之。故善战者,求之于势,不责于人故能择人而任势。任势者,其战人也,如转木石。木石之性,安则静,危则动,方则止,圆则行。
+故善战人之势,如转圆石于千仞之山者,势也。
+凡先处战地而待敌者佚,后处战地而趋战者劳。故善战者,致人而不致于人。能使敌人自至者,利之也;能使敌人不得至者,害之也。故敌佚能劳之,饱能饥之,安能动之。出其所必趋,趋其所不意。
+行千里而不劳者,行于无人之地也;攻而必取者,攻其所不守也。守而必固者,守其所必攻也。故善攻者,敌不知其所守;善守者,敌不知其所攻。微乎微乎,至于无形;神乎神乎,至于无声,故能为敌之司命。进而不可御者,冲其虚也;退而不可追者,速而不可及也。故我欲战,敌虽高垒深沟,不得不与我战者,攻其所必救也;我不欲战,虽画地而守之,敌不得与我战者,乖其所之也。故形人而我无形,则我专而敌分。我专为一,敌分为十,是以十攻其一也。则我众敌寡,能以众击寡者,则吾之所与战者约矣。吾所与战之地不可知,不可知则敌所备者多,敌所备者多,则吾所与战者寡矣。故备前则后寡,备后则前寡,备左则右寡,备右则左寡,无所不备,则无所不寡。寡者,备人者也;众者,使人备己者也。故知战之地,知战之日,则可千里而会战;不知战之地,不知战日,则左不能救右,右不能救左,前不能救后,后不能救前,而况远者数十里,近者数里乎!
+以吾度之,越人之兵虽多,亦奚益于胜哉!
+故曰:胜可为也。敌虽众,可使无斗。故策之而知得失之计,候之而知动静之理,形之而知死生之地,角之而知有余不足之处。故形兵之极,至于无形。无形则深间不能窥,智者不能谋。因形而措胜于众,众不能知。人皆知我所以胜之形,而莫知吾所以制胜之形。故其战胜不复,而应形于无穷。
+夫兵形象水,水之行避高而趋下,兵之形避实而击虚;水因地而制流,兵因敌而制胜。故兵无常势,水无常形。能因敌变化而取胜者,谓之神。故五行无常胜,四时无常位,日有短长,月有死生。
+凡用兵之法,将受命于君,合军聚众,交和而舍,莫难于军争。军争之难者,以迂为直,以患为利。
+故迂其途,而诱之以利,后人发,先人至,此知迂直之计者也。军争为利,军争为危。举军而争利则不及,委军而争利则辎重捐。是故卷甲而趋,日夜不处,倍道兼行,百里而争利,则擒三将军,劲者先,疲者后,其法十一而至;五十里而争利,则蹶上将军,其法半至;三十里而争利,则三分之二至。是故军无辎重则亡,无粮食则亡,无委积则亡。故不知诸侯之谋者,不能豫交;不知山林、险阻、沮泽之形者,不能行军;不用乡导者,不能得地利。故兵以诈立,以利动,以分和为变者也。故其疾如风,其徐如林,侵掠如火,不动如山,难知如阴,动如雷震。掠乡分众,廓地分利,悬权而动。先知迂直之计者胜,此军争之法也。
+《军政》曰:“言不相闻,故为之金鼓;视不相见,故为之旌旗。”夫金鼓旌旗者,所以一民之耳目也。民既专一,则勇者不得独进,怯者不得独退,此用众之法也。故夜战多金鼓,昼战多旌旗,所以变人之耳目也。
+三军可夺气,将军可夺心。是故朝气锐,昼气惰,暮气归。善用兵者,避其锐气,击其惰归,此治气者也。以治待乱,以静待哗,此治心者也。以近待远,以佚待劳,以饱待饥,此治力者也。无邀正正之旗,无击堂堂之陈,此治变者也。
+故用兵之法,高陵勿向,背丘勿逆,佯北勿从,锐卒勿攻,饵兵勿食,归师勿遏,围师遗阙,穷寇勿迫,此用兵之法也。
+凡用兵之法,将受命于君,合军聚合。泛地无舍,衢地合交,绝地无留,围地则谋,死地则战,途有所不由,军有所不击,城有所不攻,地有所不争,君命有所不受。
+故将通于九变之利者,知用兵矣;将不通九变之利,虽知地形,不能得地之利矣;治兵不知九变之术,虽知五利,不能得人之用矣。
+是故智者之虑,必杂于利害,杂于利而务可信也,杂于害而患可解也。是故屈诸侯者以害,役诸侯者以业,趋诸侯者以利。故用兵之法,无恃其不来,恃吾有以待之;无恃其不攻,恃吾有所不可攻也。
+故将有五危,必死可杀,必生可虏,忿速可侮,廉洁可辱,爱民可烦。凡此五者,将之过也,用兵之灾也。覆军杀将,必以五危,不可不察也。
+凡处军相敌,绝山依谷,视生处高,战隆无登,此处山之军也。绝水必远水,客绝水而来,勿迎之于水内,令半渡而击之利,欲战者,无附于水而迎客,视生处高,无迎水流,此处水上之军也。绝斥泽,唯亟去无留,若交军于斥泽之中,必依水草而背众树,此处斥泽之军也。平陆处易,右背高,前死后生,此处平陆之军也。凡此四军之利,黄帝之所以胜四帝也。凡军好高而恶下,贵阳而贱阴,养生而处实,军无百疾,是谓必胜。丘陵堤防,必处其阳而右背之,此兵之利,地之助也。上雨水流至,欲涉者,待其定也。凡地有绝涧、天井、天牢、天罗、天陷、天隙,必亟去之,勿近也。吾远之,敌近之;吾迎之,敌背之。军旁有险阻、潢井、蒹葭、小林、蘙荟者,必谨覆索之,此伏奸之所处也。
+敌近而静者,恃其险也;远而挑战者,欲人之进也;其所居易者,利也;众树动者,来也;众草多障者,疑也;鸟起者,伏也;兽骇者,覆也;尘高而锐者,车来也;卑而广者,徒来也;散而条达者,樵采也;少而往来者,营军也;辞卑而备者,进也;辞强而进驱者,退也;轻车先出居其侧者,陈也;无约而请和者,谋也;奔走而陈兵者,期也;半进半退者,诱也;杖而立者,饥也;汲而先饮者,渴也;见利而不进者,劳也;鸟集者,虚也;夜呼者,恐也;军扰者,将不重也;旌旗动者,乱也;吏怒者,倦也;杀马肉食者,军无粮也;悬甀不返其舍者,穷寇也;谆谆𧬈𧬈,徐与人言者,失众也;数赏者,窘也;数罚者,困也;先暴而后畏其众者,不精之至也;来委谢者,欲休息也。兵怒而相迎,久而不合,又不相去,必谨察之。
+兵非贵益多也,惟无武进,足以并力料敌取人而已。夫惟无虑而易敌者,必擒于人。卒未亲而罚之,则不服,不服则难用。卒已亲附而罚不行,则不可用。故合之以文,齐之以武,是谓必取。令素行以教其民,则民服;令素不行以教其民,则民不服。令素行者,与众相得也。
+地形有通者、有挂者、有支者、有隘者、有险者、有远者。我可以往,彼可以来,曰通。通形者,先居高阳,利粮道,以战则利。可以往,难以返,曰挂。挂形者,敌无备,出而胜之,敌若有备,出而不胜,难以返,不利。我出而不利,彼出而不利,曰支。支形者,敌虽利我,我无出也,引而去之,令敌半出而击之利。隘形者,我先居之,必盈之以待敌。若敌先居之,盈而勿从,不盈而从之。险形者,我先居之,必居高阳以待敌;若敌先居之,引而去之,勿从也。远形者,势均难以挑战,战而不利。凡此六者,地之道也,将之至任,不可不察也。
+凡兵有走者、有驰者、有陷者、有崩者、有乱者、有北者。凡此六者,非天地之灾,将之过也。夫势均,以一击十,曰走;卒强吏弱,曰驰;吏强卒弱,曰陷;大吏怒而不服,遇敌怼而自战,将不知其能,曰崩;将弱不严,教道不明,吏卒无常,陈兵纵横,曰乱;将不能料敌,以少合众,以弱击强,兵无选锋,曰北。凡此六者,败之道也,将之至任,不可不察也。
+夫地形者,兵之助也。料敌制胜,计险隘远近,上将之道也。知此而用战者必胜,不知此而用战者必败。故战道必胜,主曰无战,必战可也;战道不胜,主曰必战,无战可也。故进不求名,退不避罪,唯民是保,而利于主,国之宝也。
+视卒如婴儿,故可以与之赴深溪;视卒如爱子,故可与之俱死。厚而不能使,爱而不能令,乱而不能治,譬若骄子,不可用也。
+知吾卒之可以击,而不知敌之不可击,胜之半也;知敌之可击,而不知吾卒之不可以击,胜之半也;知敌之可击,知吾卒之可以击,而不知地形之不可以战,胜之半也。故知兵者,动而不迷,举而不穷。故曰:知彼知己,胜乃不殆;知天知地,胜乃可全。
+用兵之法,有散地,有轻地,有争地,有交地,有衢地,有重地,有泛地,有围地,有死地。诸侯自战其地者,为散地;入人之地不深者,为轻地;我得亦利,彼得亦利者,为争地;我可以往,彼可以来者,为交地;诸侯之地三属,先至而得天下众者,为衢地;入人之地深,背城邑多者,为重地;山林、险阻、沮泽,凡难行之道者,为泛地;所由入者隘,所从归者迂,彼寡可以击吾之众者,为围地;疾战则存,不疾战则亡者,为死地。是故散地则无战,轻地则无止,争地则无攻,交地则无绝,衢地则合交,重地则掠,泛地则行,围地则谋,死地则战。
+古之善用兵者,能使敌人前后不相及,众寡不相恃,贵贱不相救,上下不相收,卒离而不集,兵合而不齐。合于利而动,不合于利而止。敢问敌众而整将来,待之若何曰:先夺其所爱则听矣。兵之情主速,乘人之不及。由不虞之道,攻其所不戒也。
+凡为客之道,深入则专。主人不克,掠于饶野,三军足食。谨养而勿劳,并气积力,运兵计谋,为不可测。
+投之无所往,死且不北。死焉不得,士人尽力。兵士甚陷则不惧,无所往则固,深入则拘,不得已则斗。是故其兵不修而戒,不求而得,不约而亲,不令而信,禁祥去疑,至死无所之。
+吾士无余财,非恶货也;无余命,非恶寿也。令发之日,士卒坐者涕沾襟,偃卧者涕交颐,投之无所往,诸、刿之勇也。故善用兵者,譬如率然。率然者,常山之蛇也。击其首则尾至,击其尾则首至,击其中则首尾俱至。敢问兵可使如率然乎?曰可。夫吴人与越人相恶也,当其同舟而济而遇风,其相救也如左右手。是故方马埋轮,未足恃也;齐勇如一,政之道也;刚柔皆得,地之理也。故善用兵者,携手若使一人,不得已也。
+将军之事,静以幽,正以治,能愚士卒之耳目,使之无知;易其事,革其谋,使人无识;易其居,迂其途,使民不得虑。帅与之期,如登高而去其梯;帅与之深入诸侯之地,而发其机。若驱群羊,驱而往,驱而来,莫知所之。聚三军之众,投之于险,此谓将军之事也。
+九地之变,屈伸之力,人情之理,不可不察也。
+凡为客之道,深则专,浅则散。去国越境而师者,绝地也;四彻者,衢地也;入深者,重地也;入浅者,轻地也;背固前隘者,围地也;无所往者,死地也。
+是故散地吾将一其志,轻地吾将使之属,争地吾将趋其后,交地吾将谨其守,交地吾将固其结,衢地吾将谨其恃,重地吾将继其食,泛地吾将进其途,围地吾将塞其阙,死地吾将示之以不活。
+故兵之情:围则御,不得已则斗,过则从。
+是故不知诸侯之谋者,不能预交;不知山林、险阻、沮泽之形者,不能行军;不用乡导,不能得地利。四五者,一不知,非霸王之兵也。夫霸王之兵,伐大国,则其众不得聚;威加于敌,则其交不得合。是故不争天下之交,不养天下之权,信己之私,威加于敌,则其城可拔,其国可隳。
+施无法之赏,悬无政之令。犯三军之众,若使一人。犯之以事,勿告以言;犯之以害,勿告以利。投之亡地然后存,陷之死地然后生。夫众陷于害,然后能为胜败。
+故为兵之事,在顺详敌之意,并敌一向,千里杀将,是谓巧能成事。是故政举之日,夷关折符,无通其使,厉于廊庙之上,以诛其事。敌人开阖,必亟入之,先其所爱,微与之期,践墨随敌,以决战事。是故始如处女,敌人开户;后如脱兔,敌不及拒。
+凡火攻有五:一曰火人,二曰火积,三曰火辎,四曰火库,五曰火队。
+行火必有因,因必素具。发火有时,起火有日。时者,天之燥也。日者,月在箕、壁、翼、轸也。凡此四宿者,风起之日也。凡火攻,必因五火之变而应之:火发于内,则早应之于外;火发而其兵静者,待而勿攻,极其火力,可从而从之,不可从则上。火可发于外,无待于内,以时发之,火发上风,无攻下风,昼风久,夜风止。凡军必知五火之变,以数守之。
+故以火佐攻者明,以水佐攻者强。水可以绝,不可以夺。
+夫战胜攻取而不惰其功者凶,命曰“费留”。故曰:明主虑之,良将惰之,非利不动,非得不用,非危不战。主不可以怒而兴师,将不可以愠而攻战。合于利而动,不合于利而上。怒可以复喜,愠可以复说,亡国不可以复存,死者不可以复生。故明主慎之,良将警之。此安国全军之道也。
+凡兴师十万,出征千里,百姓之费,公家之奉,日费千金,内外骚动,怠于道路,不得操事者,七十万家。相守数年,以争一日之胜,而爱爵禄百金,不知敌之情者,不仁之至也,非民之将也,非主之佐也,非胜之主也。故明君贤将所以动而胜人,成功出于众者,先知也。先知者,不可取于鬼神,不可象于事,不可验于度,必取于人,知敌之情者也。
+故用间有五:有因间,有内间,有反间,有死间,有生间。五间俱起,莫知其道,是谓神纪,人君之宝也。乡间者,因其乡人而用之;内间者,因其官人而用之;反间者,因其敌间而用之;死间者,为诳事于外,令吾闻知之而传于敌间也;生间者,反报也。故三军之事,莫亲于间,赏莫厚于间,事莫密于间,非圣贤不能用间,非仁义不能使间,非微妙不能得间之实。微哉微哉!无所不用间也。间事未发而先闻者,间与所告者兼死。凡军之所欲击,城之所欲攻,人之所欲杀,必先知其守将、左右、谒者、门者、舍人之姓名,令吾间必索知之。敌间之来间我者,因而利之,导而舍之,故反间可得而用也;因是而知之,故乡间、内间可得而使也;因是而知之,故死间为诳事,可使告敌;因是而知之,故生间可使如期。五间之事,主必知之,知之必在于反间,故反间不可不厚也。
+昔殷之兴也,伊挚在夏;周之兴也,吕牙在殷。故明君贤将,能以上智为间者,必成大功。此兵之要,三军之所恃而动也。
++ +
+ + + + + ++ +
+ + + + + +CAS全称为Compare and Swap
被译为比较并交换。是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。
+java.util.concurrent.atomic
并发包下的所有原子类都是基于 CAS
来实现的。
以 AtomicInteger
原子整型类为例。
public class MainTest {
+ public static void main(String[] args) {
+ new AtomicInteger().compareAndSet(1,2);
+ }
+}
+
以上面的代码为例,调用栈如下:
+compareAndSet --> unsafe.compareAndSwapInt ---> unsafe.compareAndSwapInt --> (C++) cmpxchg
+
AtomicInteger
内部方法都是基于 Unsafe
类实现的。
public final boolean compareAndSet(int expect, int update) {
+ return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
+}
+
参数:
+this
: Unsafe
对象本身,需要通过这个类来获取 value
的内存偏移地址;valueOffset
: value
变量的内存偏移地址;expect
: 期望更新的值;update
: 要更新的最新值;偏移量valueOffset
// 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;
+
Unsafe
是CAS的核心类,Java无法直接访问底层操作系统,而是通过 native
方法来访问。不过尽管如此,JVM还是开了一个后门,JDK中有一个类 Unsafe
,它提供了硬件级别的原子操作。valueOffset
表示的是变量值在内存中的偏移地址,因为 Unsafe
就是根据内存偏移地址获取数据的原值的。value
是用 volatile
修饰的,保证了多线程之间看到的 value
值是同一份。继续向底层深入,就会看到Unsafe
类中的一些方法,同时也是CAS的核心方法:
public final class Unsafe {
+
+ // ...
+
+ public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
+
+ public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
+
+ public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
+
+ // ...
+}
+
上面的三个方法可以对应去查看 openjdk
的 hotspot
源码:src/share/vm/prims/unsafe.cpp
#define FN_PTR(f) CAST_FROM_FN_PTR(void*, &f)
+
+{CC"compareAndSwapObject", CC"("OBJ"J"OBJ""OBJ")Z", FN_PTR(Unsafe_CompareAndSwapObject)},
+
+{CC"compareAndSwapInt", CC"("OBJ"J""I""I"")Z", FN_PTR(Unsafe_CompareAndSwapInt)},
+
+{CC"compareAndSwapLong", CC"("OBJ"J""J""J"")Z", FN_PTR(Unsafe_CompareAndSwapLong)},
+
最终在 hotspot
源码实现中都会调用统一的 cmpxchg
函数,/src/share/vm/runtime/Atomic.cpp
jbyte Atomic::cmpxchg(jbyte exchange_value, volatile jbyte*dest, jbyte compare_value) {
+ assert (sizeof(jbyte) == 1,"assumption.");
+ uintptr_t dest_addr = (uintptr_t) dest;
+ uintptr_t offset = dest_addr % sizeof(jint);
+ volatile jint*dest_int = ( volatile jint*)(dest_addr - offset);
+ // 对象当前值
+ jint cur = *dest_int;
+ // 当前值cur的地址
+ jbyte * cur_as_bytes = (jbyte *) ( & cur);
+ // new_val地址
+ jint new_val = cur;
+ jbyte * new_val_as_bytes = (jbyte *) ( & new_val);
+ // new_val存exchange_value,后面修改则直接从new_val中取值
+ new_val_as_bytes[offset] = exchange_value;
+ // 比较当前值与期望值,如果相同则更新,不同则直接返回
+ while (cur_as_bytes[offset] == compare_value) {
+ // 调用汇编指令cmpxchg执行CAS操作,期望值为cur,更新值为new_val
+ jint res = cmpxchg(new_val, dest_int, cur);
+ if (res == cur) break;
+ cur = res;
+ new_val = cur;
+ new_val_as_bytes[offset] = exchange_value;
+ }
+ // 返回当前值
+ return cur_as_bytes[offset];
+}
+
从上述源码可以看出CAS的原理就是调用了汇编指令 cmpxchg
,最终其实也就调用了CPU的某些指令。
CAS作用也一目了然,在多线程环境中,就是比较当前线程工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较,直到主内存和工作内存中的值一直为止。例如代码:
+public final int getAndAddInt(Object var1, long var2, int var4) {
+ int var5;
+ do {
+ var5 = this.getIntVolatile(var1, var2);
+ } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
+ return var5;
+}
+
从源码可以看出,是通过CPU指令进行调用,当CPU中某个处理器对缓存中的共享变量进行了操作,其他处理器会有个嗅探机制,将其他处理器的该共享变量的缓存失效,待其他线程读取时会重新从主内存中读取最新的数据,基于 MESI
缓存一致性协议来实现的。
简述,就是通过CPU的缓存一致性协议来保证线程之间的数据一致性的。
+++ +CPU 处理器速度远远大于在主内存中的,为了解决速度差异,在他们之间架设了多级缓存,如 L1、L2、L3 级别的缓存,这些缓存离CPU越近就越快,将频繁操作的数据缓存到这里,加快访问速度。
+
CAS的作用是比较并交换,就是先拿这个期望值,与主内存的值比较,判断主内存中该位置是否存在期望值, +如果存在,则改为新的值,这个修改的过程是具有原子性的. +因为CAS是cpu并发源语,并发源语体现在Java sun.misc.Unsafa类上. +调用Unsafe类中的CAS方法,JVM会帮我们实现CAS汇编指令。这是一种完全依赖于硬件的功能,通过他实现了原子操作。 +由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成数据不一致问题。
+++PS Unsafe类 +CAS其实是调用了
+Unsafe
类的方法Unsafa
类是CAS核心类,由于Java方法无法直接访问底层系统,需要通过本地(native
)方法来访问,Unsafe
相当于一个后门,基于该类可以直接操作特定内存数据。 +Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针(内存地址)一样直接操作内存,因此Java中CAS操作的执行依赖于Unsafe类的方法。 +Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务。
因为是采用自旋锁的方式来实现所以,自然有自旋锁的缺点,循环时间长开销大,例如:getAndAddInt
方法执行,有个do while
循环,如果CAS失败,一直会进行尝试,如果CAS长时间不成功,可能会给CPU带来很大的开销。
public final int getAndAddInt(Object var1, long var2, int var4) {
+ int var5;
+ do {
+ var5 = this.getIntVolatile(var1, var2);
+ } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
+ return var5;
+}
+
只能保证一个共享变量的原子操作,对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
+ABA问题。
+ABA问题示例代码:
+public class MainTest {
+ static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
+ public static void main(String[] args) {
+ new Thread(() -> {
+ // 先改到101在改回来,CAS会认为value没有被修改过
+ atomicReference.compareAndSet(100, 101);
+ atomicReference.compareAndSet(101, 100);
+ }, "Thread 1").start();
+
+ new Thread(() -> {
+ try {
+ //保证线程1完成一次ABA操作
+ TimeUnit.SECONDS.sleep(1);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
+ }, "Thread 2").start();
+ try {
+ TimeUnit.SECONDS.sleep(2);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+}
+
CAS算法实现一个重要前提是,需要去除内存中某个时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
+比如,线程1从内存位置V取出A,线程2同时也从内存取出A,并且线程2进行一些操作将值改为B,然后线程2又将V位置数据改成A,这时候线程1进行CAS操作发现内存中的值依然时A,然后线程1操作成功。 +尽管线程1的CAS操作成功,但是不代表这个过程没有问题。
+简单说,如果一个线程改了一个值,最后又改回到初始值了,这时候CAS会认为它没有被修改过。简而言之就是只比较结果,不比较过程。
+3.1 ABA问题解决
+3.1.1 利用 AtomicReference
类进行原子引用
public class AtomicRefrenceDemo {
+ public static void main(String[] args) {
+ User z3 = new User("张三", 22);
+ User l4 = new User("李四", 23);
+ AtomicReference<User> atomicReference = new AtomicReference<>();
+ atomicReference.set(z3);
+ System.out.println(atomicReference.compareAndSet(z3, l4) + "\t" + atomicReference.get().toString());
+ System.out.println(atomicReference.compareAndSet(z3, l4) + "\t" + atomicReference.get().toString());
+ }
+}
+
+@Getter
+@ToString
+@AllArgsConstructor
+class User {
+ String userName;
+ int age;
+}
+
// 输出结果
+true User(userName=李四, age=23)
+false User(userName=李四, age=23)
+
3.1.2 使用时间戳的原子引用AtomicStampedReference
修改版本号。主要是在对象中额外再增加一个标记来标识对象是否有过变更
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
+
+public static void main(String[] args) {
+ new Thread(() -> {
+ int stamp = atomicStampedReference.getStamp();
+ System.out.println(Thread.currentThread().getName() + "\t第1次版本号" + stamp);
+ try {
+ TimeUnit.SECONDS.sleep(2);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
+ System.out.println(Thread.currentThread().getName() + "\t第2次版本号" + atomicStampedReference.getStamp());
+ atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
+ System.out.println(Thread.currentThread().getName() + "\t第3次版本号" + atomicStampedReference.getStamp());
+ }, "Thread 3").start();
+
+ new Thread(() -> {
+ int stamp = atomicStampedReference.getStamp();
+ System.out.println(Thread.currentThread().getName() + "\t第1次版本号" + stamp);
+ try {
+ TimeUnit.SECONDS.sleep(4);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
+
+ System.out.println(Thread.currentThread().getName() + "\t修改是否成功" + result + "\t当前最新实际版本号:" + atomicStampedReference.getStamp());
+ System.out.println(Thread.currentThread().getName() + "\t当前最新实际值:" + atomicStampedReference.getReference());
+ }, "Thread 4").start();
+}
+
Thread 3 第1次版本号1
+Thread 4 第1次版本号1
+Thread 3 第2次版本号2
+Thread 3 第3次版本号3
+Thread 4 修改是否成功false 当前最新实际版本号:3
+Thread 4 当前最新实际值:100
+
+ +
+ + + + + +为什么要写整洁的代码,回答这个问题之前,也许应该想想写糟糕的代码的原因
+是想快点完成吗?还是要赶时间吗?有可能.或许你觉得自己要干好所需要的时间不够;假使花时间清理代码,老板就会大发雷霆.或许你只是不耐烦再搞这套程序,期望早点结束.或许你看了看,自己承诺要做的其他事情,意识到得赶紧弄完手上的东西,好接着做下一件工作.这种事情我们都干过.
+“只要你干过编程,就有可能曾经被某人的糟糕代码绊倒过.如果你编程不止两三年,有可能被这种代码拖过腿.进度延缓的程度非常严重.有些团队在项目初期进展迅速,但是有那么一两年的时间却慢如蜗行.如对代码的每次修改都影响到了其他两三处代码.修改无小事.每次修改或添加代码都对那对扭纹柴了然于心,这样才能网上扔更多的扭纹柴.这团乱麻越来越大,在也无法清理,最后束手无策.
+随着混乱的增加,团队生产力也持续下降,趋势余零.当生产力下降时,管理层就只有一件事情可做了:增加更多的人手到项目中,期望提高生产力.可新人不熟悉系统的设计.他们搞不清什么样的修改符合设计的意图,什么样的修改违背设计意图.而且,他们以及团队中的其他人都背负着提高生产力的压力.于是,他们制造了更多的混乱,驱动生产力向零的那端不断下降.
+最后,开发团队造反了,他们告诉管理层,再也无法在这令人生厌的代码基础上做开发.他们要求全新设计.管理层不愿意投入资源完全重启炉灶,他们也不能否认生产力低得可怕.他们只好同意开发者的要求,授权去做一套看上去很美的华丽新设计.
+于是就组建了一只新军.谁都想加入这个团队,因为它是张白纸.他们可以重新来过,搞出点真正漂亮的东西来.但只有最优秀,最聪明的家伙被选中.其余人则继续维护现有的系统.
+现在有两支队伍在竞赛.新团队必须搭建一套新系统,要求实现旧系统的所有功能.另外,还得跟得上旧系统的持续改动.在新系统功能足以对抗旧系统之前,管理层就不会替换掉旧的系统.
+竞赛可能会持续极长的时间.到了完成的时候,新团队的老成员已不知去向,而现有成员则需求重新设计一套新系统,因为这套系统太烂了.
+假如你经历过哪怕是一小段我谈到的这种事,那么你一定知道,花时间保持整洁的代码不但有关于效率,还有关于生存. "
+有时我们抱怨需求变化背离了初期设计.哀叹进度太紧张,没法好好干活.我们把问题归咎于那些愚蠢的经理,苛刻的用户,没用的营销方式.
+经理和营销人员指望从我们这里得到必须的信息,然后才能做出承诺和保证;即便他们没开口问,我们也不该羞于告知自己的想法。用户指望我们验证需求是否都在系统中实现了。项目经理指望我们遵守进度。我们与项目的规划脱不了干系,对失败负有极大的责任:特别是当失败与糟糕的代码有关时尤为如此! +“且慢!”你说。“不听经理的,我就会被炒鱿鱼。”多半不会。多数经理想要知道实情,即便他们看起来不喜欢实情。多数经理想要好代码,即便他们总是痴缠于进度。他们会奋力卫护进度和需求;那是他们该干的。你则当以同等的热情卫护代码。
+再说明白些,假使你是位医生,病人请求你在给他做手术前别洗手,因为那会花太多时间,你会照办吗?本该是病人说了算;但医生却绝对应该拒绝遵从。为什么?因为医生比病人更了解疾病和感染的风险。医生如果按病人说的办,就是一种不专业的态度,更别说是犯罪了。同理,程序员遵从不了解混乱风险的经理的意愿,也是不专业的做法。我们应该加强这方面的意识。
+程序员面临着一种基础价值谜题。有那么几年经验的开发者都知道,之前的混乱拖了自己的后腿。但开发者们背负期限的压力,只好制造混乱。简言之,他们没花时间让自己做得更快!真正的专业人士明白,这道谜题的第二部分说错了。制造混乱无助于赶上期限。混乱只会立刻拖慢你,叫你错过期限。赶上期限的唯一方法一做得快的唯一方法就是始终尽可能保持代码整洁。
+“如果你不明白整洁对于代码有何意义,尝试去写整洁代码就会毫无所益.”
+截取自《代码整洁之道》
+每个人对于整洁的代码理解肯定不同,在我看来,满足业务场景的情况下,可读性强,运行效率高,细节处理好,易扩展的代码就是整洁代码. 抛开业务场景不谈,只谈所谓的"整洁代码"就是所谓的耍流氓。
+整洁的代码总是看起来总是像是某位特别在意它的人写的.几乎没有改进的余地.代码的作者什么都想到了.如果你想改进它,总会回到原点。
+代码的作用是为了解决人们的某种需求,什么语言不重要(但是总有人非要争个高低),问题解决了才重要.在规定的业务场景下,写出能解决用户需求的代码就是程序员的日常工作,而需求并不是一成不变的,需求会变代码也会改变,所以我们就需要再这个特定的业务场景中尽量把代码变得灵活起来,之后增加需求或者修改需求时,会变的容易一些。
+至于方法的规范命名,可读性,注释等.这些也很重要,毕竟开发的时候一般都是团队一起来开发,代码不止你自己看,还需要别人看,说简单一些就是别人好接手你的代码,即代码的维护
+在一个产品的周期中,开发其实只占了一小段时间,绝大多数时间都在维护代码.代码写出来首先是给人看的,其次是给电脑看,所以代码的可读性至关重要.所以,如果我非要在代码的可读性和运行效率之间选择一个,非可读性莫属.一般来说,要权衡代码的可读性和运行效率,如果差距太大,要看实际的业务场景来决定,毕竟写程序的最终目的是为了解决用户的某些问题.
+可读性通常表现在代码易于理解
+如果一个方法的行数过多也会影响代码的可读性,一般控制在80行左右。过多无用的注释、API,只会加重使用者的认知负担,过多无用的信息读起来只会浪费时间,所以要尽量保持API的精简,代码注释的合理,保持规范的命名,使注释看起来没那么臃肿。要知道代码有人维护,可注释没有人维护。
+代码依赖性导致了变化的放大和高认知负荷。模糊性造成了未知的不可知性,导致了认知负荷。从而使得代码更加难以理解,从而不能很好的维护. 所以整洁的代码总是复杂性低的。
+运行效率即代码的运行效率,包括运行所占用的时间和空间。如果数据量不是很大(单表在300w左右)可能几乎不用考虑这个问题,空间就更不用说了,现在大多数公司都是用空间来换时间的,即通过增加服务器的配置或数量来提高程序运算速度。所以很多人并不关心程序运行的效率。
+诚然,我也不是很关心软件的运行效率,因为软件的运行效率主要还是取决与硬件的发展水平,现在硬件发展比软件发展快了一个档次,不然现在也不能一下子涌起那么多的软件公司。
+但是,如果业务量非常大,电脑的运行效率也是有限的,当服务器达到一定数量后,企业就会考虑成本毕竟不能一直毫无节制的增加下去,这时候就需要考虑程序的运行效率了。
+作为一个好的程序员,你不得不具备这项技能。
+怎样提高程序的运行效率,有没有想过?程序是算法和数据结构组成的,数据结构决定一个程序的空间复杂度,算法则决定一个程序的时间复杂度。 想要程序跑的更快,空间占用更少,可以从这两个维度来进行探索。
+一个好的算法离不开一个好的想法,这对于一个程序来说是至关重要的,因为它是决定了程序运行速度的关键原因。可能很多人都有一个误区,就是代码越少执行效率就越高,在改进算法的时候会通过删减代码来进行。举个例子:匹配字符串,在数据量很大的情况下,暴力匹配的方式无论你怎么改,都会比那些运用了好的算法的程序慢。
+不整洁的代码是混乱的,代码混乱到一定程度就会对程序的运行速度产生影响。所以,代码的整洁程度一定程度上影响了代码的运行速度。
+整洁的代码除了是可读性强、运行效率高还有最重要的一点是它是容易扩展的。扩展性可理解为易于修改的代码。程序的扩展性代表了维护该程序程度的难易,当然可读性也是,二者都很重要。
+在所有的设计模式中,几乎所有的设计模式都是为了符合开闭原则,即保持程序的扩展性,重要程度可见一斑。
+代码都是为了一定的需求服务的,但是这些需求并不是一成不变的,当需求变更了,如果我们代码的扩展性很好,我们可能只需要简单的添加或者删除模块就行了,如果扩展性不好,可能所有代码都需要重写,那就是一场灾难了,所以提高代码的扩展性是很重要的。
+衡量代码扩展性可以从高内聚,低耦合这两个方面来衡量。
+++高内聚:一个软件单位内部的关联紧密程度;有关联的事务应该放在一起。 +低耦合:两个或多个软件单位之间的关联紧密程度;软件单位之间尽可能的不要相互影响。
+
好的代码是不断的迭代出来的,没有人能一下子写完整 ,需求会变代码也会改变 第一次迭代可能写的代码很糟糕,这时一定要再次回头去看之前的代码,去优化,重构,让代码变得易于维护.
+如何写出整洁的代码,那就看你怎么理解整洁的代码,理解的不一样写出来的肯定就不一样.下面是我的几点建议
+注释只是二手的信息,它和代码所传达的信息并不等价.所以,不要写没有意义的注释(冗余注释,废弃注释,等一些没有意义的信息和不恰当的信息),要知道代码有人维护,可注释没有人维护,最好的办法就是规范变量,方法的命名,做到见名知意; 如果你的方法命名足够明确就可以不用写注释了,当然一段好的注释一定是包含代码中没有体现的信息。
+如果要编写一条注释,就花时间尽量保证写出最好的注释 不要画蛇添足,要保持注释的简洁。比如,无用的代码直接删掉,不要注释它,不用担心会丢,版本服务会有记录能找回。编写注释和迭代代码是一样的道理。但是一般注释是没有人来维护的,因为它不会影响程序的正常运行。
+同样的,命名也不会影响程序的正常运行。注释和命名是不会影响程序的执行的,但是这两个因素是会影响到开发者编写代码的。它会导致开发者的认知负荷增加,从而降低编写代码的效率。
+“ 好的代码本身就是注释,我们要尽量规范和美化自己的代码来减少不必要的注释。若编程语言足够有表达力,就不需要注释,尽量通过代码来阐述。”编程语言的表达力很大程度上就取决于方法的命名。
+那什么是好的命名呢?说实话这也是我编程一直头疼的原因,总觉得命名不够好。在网络上看到一种方法觉得很不错: 把你的变量名拎出来,问别人,你看到这个名字会想到什么,他说的和你想的一致,就用,否则就改。改到基本上不懂程序的人都能大概看懂你写的是什么,那大概就是好的命名了。
+程序的命名我认为是约定俗成的,最初开始编程的开发者们,他们一定会遇到这个问题的,久而久之就会建立一套规则将这些命名进行统一。到了现在一定是有很多成熟的命名规则的,所以我们可以踩着前辈们的肩膀前行。
+虽然是约定俗成的,但是事物的发展一定不是一成不变的,我并不知道在我写下这么多文字后是否适用于未来,或许大概只有思想会适用,而那些具体的方法是一定不会适用的,但是现在我们需要具体的解决方案,或许我能给你提点建议,如果能帮到你我会很高兴的.
+注释
+命名
+最好的方法入参参数是0个,其次是1,最多建议不超过3个,大于三个建议封装成对象,这样做的好处是方便扩展管理;在一个方法中声明变量应该在方法的最前面,我们应该降低方法的复杂度,避免出现if..else多层嵌套的情况,当然,如果一个方法过于复杂可将该方法进行拆分;还应当注意方法中异常块的处理,一般情况下是不会写的,因为有全局异常处理,如果非要写那么可能代码看起来不是那么简洁清晰.注意方法的封装.在方法调用本地方法的时候,本地的方法尽量使用基础类型作为返回值,好处是避免空指针判断和隐式拆包和打包.当我们写方法返回值的时候,如果返回值类型是数组或集合,我们应该避免返回null,否则调用方就有可能出现空指针.可以避免不必要的空指针判断。
+或许我应该更加细致的标出
+除了上述的几点以外,在写函数的时候还要遵循单一职责原则,即每个方法只做一件事,好处是方便管理,代码可读性会提高,复杂度降低 易于维护。也要遵循开闭原则,这会使你的代码更加灵活。当然这些代码肯定不是一次就写出来的,好的代码需要迭代,需要打磨。在你写完几个函数之后,可能会发现重复的地方,这时候就需要将他们抽象出来。
+许许多多的函数组成了类,当函数之间相互调用的时候,就会存在多个类之间的联系,所以在编写类的时候要注意类之间的依赖关系,使它们别那么耦合,一般会遵循迪米特法则。
+属性的存在使类中的元素更加丰富,一般情况下属性在类中都是私有的,会对外提供set,get方法供外部调用修改;这样做的好处是方便控制外部调用,假如你想公共处理某个属性给它加个前缀,就可以通过调用该类中涉及到该属性的方法进行修改,如果你直接修改属性那么改起来会很麻烦。
+极少数情况是公共的,比如定义一个常量类,公共的资源属性。还有多数情况下是受保护的,该种情况一般是用来给子类使用的,当然同一个包下也能访问得到。
+高内聚低耦合,这是我们写代码应该遵循的标准。内聚代表着职责的专一,这是整洁的一个很重要准则。从大的方面来说,系统的使用与构造需要明确的区分开,才不会将整个结构混杂在一起。与此同时,决定了系统的数据流走向也是决定了整个系统的层级划分,不同的层级也需要明确的区分开来。
+那么应该怎么划分代码的结构?最简单的应该同类型的相关联的表需要放在一个类中或一个包中,写一些方法对外提供api,供其他方法调用,而不是跨层调用。
+一个好的结构使代码看上去更加清晰,更加容易维护,其实它更像是对系统架构的拆分。最常见的系统分层应该是MVC结构,即模型层、视图层、控制层。我们应当更加详细的将MVC进行划分,最常见的我们将控制层又分为业务层(service)和持久层(dao)。 划分的目的是规划软件系统的逻辑结构便于开发维护。但是,随着微服务的演变和不同研发的编码习惯,往往导致了代码分层不彻底导致引入了“坏味道”。
+划分代码我认为最重要的作用是使结构单一,减少代码之间的依赖性,降低耦合度,从而提高代码的可维护性。
++ +
+ + + + + +数据结构是一门研究组织数据方式的学科,有了编程语言也就有了数据结构,学好数据结构可以编写出更有效率的代码。数据结构是算法的基础,想要学好算法,就必须把数据结构学到位。
+数据结构包括:线性结构、非线性结构。
+线性结构作为最常用的数据结构,其特点是数据元素之间存在一对一的线性存储关系。线性结构有两种不同的存储结构,即顺序存储和链式存储。顺序存储的线性表被称为顺序表,顺序表中存储元素的地址是连续的;链式存储的线性表被称为链表,链表中存储元素的地址不一定是连续的,元素结点中存放数据元素以及相邻元素地址信息。
+常见的线性结构有:数组、链表、队列、栈;常见非线性结构有:多维数组、广义表、树结构、图结构。
+使用稀疏数组可以用来压缩数据。稀疏数组的第一行依次记录原数组一共有几行几列,有多少个不为零的值,之后的每行记录原数组中不为零的值所在的行数、列数以及数组中元素该值。如图所示:
+ +二维数组转稀疏数组
+class TwoDimensionArrayDemo {
+ // 将二维数组转换为稀疏数组
+ public static int[][] twoDimensionArrayToSparseArray(int[][] array) {
+ // 记录棋盘中有效值的数量
+ int sum = 0;
+ int row = array.length;
+ int column = 0;
+ for (int[] ints : array) {
+ column = ints.length;
+ for (int item : ints) {
+ if (item != 0) {
+ sum++;
+ }
+ }
+ }
+
+ // 创建稀疏数组
+ int[][] sparseArray = new int[sum + 1][3];
+ sparseArray[0][0] = row;
+ sparseArray[0][1] = column;
+ sparseArray[0][2] = sum;
+
+ // 给稀疏数组赋值
+ int count = 0;
+ for (int i = 0; i < array.length; i++) {
+ for (int j = 0; j < array[i].length; j++) {
+ if (array[i][j] != 0) {
+ count++;
+ sparseArray[count][0] = i;
+ sparseArray[count][1] = j;
+ sparseArray[count][2] = array[i][j];
+ }
+ }
+ }
+
+ System.out.println("稀疏数组====》");
+ for (int i = 0; i < sparseArray.length; i++) {
+ System.out.printf("%d\t%d\t%d\t\n", sparseArray[i][0], sparseArray[i][1], sparseArray[i][2]);
+ }
+ return sparseArray;
+ }
+}
+
稀疏数组转二维数组
+class TwoDimensionArrayDemo {
+ // 稀疏数组转二维数组
+ public static int[][] sparseArrayToTwoDimensionArray(int[][] sparseArray) {
+ int[][] toTwoDimensionArray = new int[sparseArray[0][0]][sparseArray[0][1]];
+ // 给二维数组赋值
+ for (int i = 1; i < sparseArray.length; i++) {
+ toTwoDimensionArray[sparseArray[i][0]][sparseArray[i][1]] = sparseArray[i][2];
+ }
+
+ System.out.println("二维数组====》");
+ for (int[] row : toTwoDimensionArray) {
+ for (int data : row) {
+ System.out.printf("%d\t", data);
+ }
+ System.out.println();
+ }
+ return toTwoDimensionArray;
+ }
+}
+
队列是一个有序列表,可以使用数组或链表来实现。队列遵循先入先出的原则。即,先存入队列的数据,要先取出。后存入的要后取出。
+使用数组模拟队列示意图:
+ +数组模拟单向队列
+ public class ArrayQueue{
+
+ // 队列容量
+ private int capacity;
+
+ // 保存队列中的数据
+ private int[] arr;
+
+ // 头部指针
+ private int front;
+
+ // 尾部指针
+ private int rear;
+
+ public ArrayQueue(int capacity) {
+ this.capacity = capacity;
+ arr = new int[capacity];
+ front = -1;
+ rear = -1;
+ }
+
+ public boolean isEmpty() {
+ return front == rear;
+ }
+
+ public boolean isFull() {
+ return capacity - 1 == rear;
+ }
+
+ public void add(int data) {
+ if (isFull()) {
+ System.out.println("队列已经满了,不能在继续添加");
+ return;
+ }
+ arr[++rear] = data;
+ }
+
+ public int get() {
+ if (isEmpty()) {
+ System.out.println("队列为空,不能获取元素");
+ return -1;
+ }
+ return arr[++front];
+ }
+
+ // 显示队列的所有数据
+ public void show() {
+ if (isEmpty()) {
+ System.out.println("队列空的,没有数据~~");
+ return;
+ }
+ System.out.println("开始遍历队列:");
+ for (int i = front + 1; i <= rear; i++) {
+ System.out.printf("arr[%d]=%d\n", i, arr[i]);
+ }
+ }
+
+
+ // 显示队列的头数据, 注意不是取出数据
+ public int peek() {
+ if (isEmpty()) {
+ throw new RuntimeException("队列空的,没有数据~~");
+ }
+ return arr[front + 1];
+ }
+
+ }
+
数组模拟环形队列
+ public class CircleArrayQueue{
+
+ // 队列容量
+ private int capacity;
+
+ // 保存队列中的数据
+ private int[] arr;
+
+ // 头部指针
+ private int front;
+
+ // 尾部指针
+ private int rear;
+
+ public CircleArrayQueue(int capacity) {
+ this.capacity = capacity;
+ arr = new int[capacity];
+ }
+
+ public boolean isEmpty() {
+ return front == rear;
+ }
+
+ public boolean isFull() {
+ // 此处+1 是因为存储元素从0算起
+ return (rear + 1) % capacity == front;
+ }
+
+ public void add(int data) {
+ if (isFull()) {
+ System.out.println("队列已经满了,不能在继续添加");
+ return;
+ }
+ arr[rear] = data;
+ rear = (rear + 1) % capacity;
+ }
+
+ public int get() {
+ if (isEmpty()) {
+ System.out.println("队列为空,不能获取元素");
+ return -1;
+ }
+ int value = arr[front];
+ front = (front + 1) % capacity;
+ return value;
+ }
+
+ // 显示队列的所有数据
+ public void show() {
+ if (isEmpty()) {
+ System.out.println("队列空的,没有数据~~");
+ return;
+ }
+ System.out.println("开始遍历队列:");
+ for (int i = front % capacity; i < front + ((rear + capacity - front) % capacity); i++) {
+ System.out.printf("arr[%d]=%d\n", i, arr[i]);
+ }
+ }
+
+ // 显示队列的头数据, 注意不是取出数据
+ public int peek() {
+ if (isEmpty()) {
+ throw new RuntimeException("队列空的,没有数据~~");
+ }
+ return arr[front];
+ }
+
+ }
+
链表属于线性结构,存储空间不连续。
+链表特点:
+操作单向链表:对于插入、删除操作,只能定位至待操作节点的前一个节点,如果定位至当前节点,那么其上一个节点的信息便会丢失。
+单向链表,链表的增、删、查、改
+class SingleLinkedList{
+
+ // 头结点
+ private Node headNode = new Node(0,"");
+
+ // 添加方法
+ public void add(Node node){
+ Node tmpNode = headNode;
+
+ while (tmpNode.next != null){
+ // 指向下一个结点
+ tmpNode = tmpNode.next;
+ }
+ // 退出循环意味着tmpNode.next == null 即找到最后一个结点了
+ tmpNode.next = node;
+ }
+
+ // 顺序添加
+ public void addByOrder(Node node){
+ boolean flag = false;
+ Node tmp = headNode;
+ while (true){
+ if (tmp.next == null) {
+ break;
+ }
+
+ // 将新插入的结点num跟链表中已经存在的num进行比较,如果 < 链表中的结点 则说明找到了该位置
+ if (node.num < tmp.next.num){
+ break;
+ }
+ // 如果num相同则不能添加
+ if (node.num == tmp.next.num){
+ flag = true;
+ break;
+ }
+ tmp = tmp.next;
+ }
+
+ if (!flag){
+ node.next = tmp.next;
+ tmp.next = node;
+ return;
+ }
+ System.out.printf("需要添加的结点编号:%d已经存在了",node.num);
+ }
+
+ // 遍历链表
+ public void list() {
+ // 遍历除了头结点外的所有结点
+ Node tmpNode = headNode.next;
+ if (tmpNode == null){
+ System.out.println("链表为空!");
+ return;
+ }
+
+ while (tmpNode != null){
+ System.out.println(tmpNode);
+ // 指向下一个结点
+ tmpNode = tmpNode.next;
+ }
+ }
+
+
+}
+
+
+class Node {
+
+ int num;
+ String name;
+ Node next;
+
+ public Node(int num,String name){
+ this.num = num;
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ return "Node{" +
+ "num=" + num +
+ ", name='" + name + '\'' +
+ '}';
+ }
+}
+
反转单向链表
+ +class LinkedListDemo{
+ // 反转链表
+ public void reserve(Node head) {
+ if (head.next == null || head.next.next == null) {
+ return;
+ }
+
+ Node reserve = new Node(0, "");
+ Node cur = head.next;
+ Node next = null;
+ // 遍历链表
+ // 遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表reverseHead 的最前端
+ while (cur != null) {
+
+ // 保存当前结点的下一个结点
+ next = cur.next;
+
+ // 将cur的下一个节点指向新的链表的最前端(覆盖掉)保证将最新的结点放到reseve的最前面
+ cur.next = reserve.next;
+
+ // 将cur 连接到新的链表上
+ reserve.next = cur;
+
+ // 将之前保存好的结点赋值给当前结点
+ cur = next;
+ }
+ }
+}
+
利用栈逆序打印单向链表
+class LinkedListDemo {
+ public void reservePrint(Node head) {
+ if (head.next == null || head.next.next == null) {
+ return;
+ }
+
+ Stack<Node> nodes = new Stack<>();
+ Node tmp = head.next;
+ while (tmp != null) {
+ nodes.push(tmp);
+ tmp = tmp.next;
+ }
+
+ // 从stack中取出结点
+ while (nodes.size() > 0) {
+ System.out.println(nodes.pop());
+ }
+ }
+}
+
对比单向链表:
+双向链表,增、删、改、查
+class DoubleLinkedList{
+
+ // 头结点
+ private Node headNode = new Node(0,"");
+
+
+ public Node getHeadNode() {
+ return headNode;
+ }
+
+
+ // 添加方法
+ public void add(Node node){
+ Node tmpNode = headNode;
+
+ while (tmpNode.next != null){
+ // 指向下一个结点
+ tmpNode = tmpNode.next;
+ }
+ // 退出循环意味着tmpNode.next == null 即找到最后一个结点了
+ tmpNode.next = node;
+ node.prev = tmpNode;
+ }
+
+
+ // 双向链表修改
+ public void update(Node node){
+ if (headNode == null) {
+ return;
+ }
+
+ Node tmp = headNode.next;
+ while (true){
+ if (tmp == null){
+ break;
+ }
+ if (node.num == tmp.num){
+ tmp.name = node.name;
+ break;
+ }
+ tmp = tmp.next;
+ }
+ }
+
+ // 双向链表删除
+ public void remove(int num){
+ if (headNode.next == null){
+ System.out.println("链表为空,无法删除!");
+ return;
+ }
+ Node tmp = headNode.next;
+ while (tmp != null){
+ if (num == tmp.num){
+ tmp.prev.next = tmp.next;
+
+ // 最后一个结点的next 为null null.pre会出现空指针异常
+ if(tmp.next != null) {
+ tmp.next.prev = tmp.prev;
+ }
+ break;
+ }
+ tmp = tmp.next;
+ }
+ }
+
+ // 遍历链表
+ public void list() {
+ // 遍历除了头结点外的所有结点
+ Node tmpNode = headNode.next;
+ if (tmpNode == null){
+ System.out.println("链表为空!");
+ return;
+ }
+
+ while (tmpNode != null){
+ System.out.println(tmpNode);
+ // 指向下一个结点
+ tmpNode = tmpNode.next;
+ }
+ }
+
+
+}
+
+
+class Node {
+
+ int num;
+ String name;
+ Node next;
+ Node prev;
+
+ public Node(int num,String name){
+ this.num = num;
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ return "Node{" +
+ "num=" + num +
+ ", name='" + name + '\'' +
+ '}';
+ }
+}
+
++约瑟夫问题为:设编号为 1,2,…n的n个人围坐一圈,约定编号为 k(1<=k<=n) 的人从 1 开始报数,数到 m 的那个人出列,它的下一位又从 1 开始报数,数到 m 的那个人又出列, 依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
+
用一个不带头结点的循环链表来处理约瑟夫问题:先构成一个有 n 个结点的单循环链表,然后由 k 结点起从 1 开始计数,计到 m 时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从 1 开始计数,直到最后一个结点从链表中删除算法结束。
+ +class JoesphSingletonLinkedList {
+
+ private Node first = null;
+
+
+ // 向单向链表添加数据
+ public void add(int nums) {
+ if (nums < 1) {
+ System.out.println("nums的值不正确");
+ return;
+ }
+ Node cur = null;
+ for (int i = 1; i <= nums; i++) {
+ Node node = new Node(i);
+ if (i == 1) {
+ first = node;
+ first.next = first;
+ cur = first;
+ } else {
+ cur.next = node;
+ node.next = first;
+ cur = node;
+ }
+ }
+ }
+
+
+ // 遍历单向循环链表
+ public void list() {
+ Node tmp = first;
+ while (true){
+ System.out.printf("当前结点为:%d\n",tmp.num);
+ if (tmp.next == first){
+ break;
+ }
+ tmp = tmp.next;
+ }
+ }
+
+
+ // 约瑟夫问题
+ public void joseph(int startNum,int countNum,int sum){
+ if (startNum > sum || startNum < 0 || countNum < 0) {
+ System.out.println("输入的参数不正确!");
+ return;
+ }
+ // 创建辅助指针,将该指针指向 first 的前一个
+ Node helper = first;
+ while (helper.next != first) {
+ helper = helper.next;
+ }
+
+ // 将first 和 help指针循环 (startNum - 1)次;因为从startNum开始,需要减一
+ for (int i = 0; i < startNum - 1; i++) {
+ first = first.next;
+ helper = helper.next;
+ }
+
+ while (true){
+ // 当环形链表中只存在一个结点
+ if (first == helper){
+ break;
+ }
+
+ // 因为是环形链表,所以需要循环挨个出链表
+ for (int i = 0; i < countNum - 1; i++) {
+ first = first.next;
+ helper = helper.next;
+ }
+
+ // 当前 first 就是出圈的结点
+ System.out.printf("当前出队列的结点编号为:%d\n",first.num);
+ first = first.next;
+ helper.next = first;
+ }
+ System.out.printf("最后的结点为:%d\n",first.num);
+ }
+
+}
+
+
+class Node {
+
+ int num;
+ Node next;
+
+ public Node(int num){
+ this.num = num;
+ }
+
+ @Override
+ public String toString() {
+ return "Node{" +
+ "num=" + num +
+ '}';
+ }
+}
+
栈的英文为stack,是一个先入后出(FILO-First In Last Out)的有序列表,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除。
+栈是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)。
+下图是出栈和入栈
+ +栈的应用场景:
+数组模拟栈
+class ArrayStack<T>{
+
+ private int size;
+ private int top;
+ private Object[] stack;
+
+ public ArrayStack(int size) {
+ this.size = size;
+ this.stack = new String[size];
+ top = -1;
+ }
+
+ public boolean isFull(){
+ return top == size -1;
+ }
+
+ public boolean isEmpty() {
+ return top == -1;
+ }
+
+ // 入栈
+ public void push(T item) {
+ if (isFull()) {
+ System.out.println("栈已经满了,不能继续添加!");
+ return;
+ }
+ top++;
+ this.stack[top] = item;
+ }
+
+ // 出栈操作
+ public T pop() {
+ if (isEmpty()) {
+ throw new RuntimeException("栈已经为空,不能继续pop");
+ }
+ T val = (T)this.stack[top];
+ top--;
+ return val;
+ }
+
+ // 遍历栈
+ public void list() {
+ if (isEmpty()) {
+ System.out.println("栈为空不能继续遍历!");
+ return;
+ }
+
+ System.out.println("遍历栈==》");
+ for (int i = top; i >=0; i--){
+ System.out.println(this.stack[i]);
+ }
+ }
+
+}
+
class CalcInfixExpressions {
+
+
+ public int calcInfixExpressions(String expression) {
+ // 定义变量
+ char[] chars = expression.toCharArray();
+ int len = chars.length;
+ Stack<Integer> numStack = new Stack<>();
+ Stack<Character> oprStack = new Stack<>();
+ int index = 0;
+
+ for (int j = 0; j < len; j++) {
+
+ char ch = chars[j];
+
+ index++;
+
+ // 判断字符是否为数字,如果是数字就放入数栈中
+ if (Character.isDigit(ch)) {
+
+ // 接收多位数
+ int num = ch;
+ boolean flag = false;
+
+ // 从当前字符开始遍历,如果下一位字符不是数字,则将该数字压入栈中并退出循环,如果是数字,则需要拼接起来
+ for (int i = index; i < len; i++) {
+ if (Character.isDigit(expression.charAt(i))) {
+ String strNum = String.valueOf(ch) + expression.charAt(i);
+ num = Integer.parseInt(strNum);
+ flag = true;
+ index++;
+ j++;
+ }else {
+ break;
+ }
+ }
+ if (!flag) {
+ num -= 48;
+ }
+ numStack.push(num);
+ continue;
+ }
+
+ // 非数字,即运算符,如果为空直接加入栈中
+ if (oprStack.isEmpty()) {
+ oprStack.push(ch);
+ continue;
+ }
+
+ // 如果运算符栈不为空,需要比较运算符的优先级,如果当前运算符的优先级 <= 栈顶的运算符的优先级,需要计算在压入栈中
+ if (oprPriority(oprStack.peek()) >= oprPriority(ch)) {
+ numStack.push(calc(numStack.pop(), numStack.pop(), oprStack.pop()));
+ }
+
+ // 将字符压入操作符栈中
+ oprStack.push(ch);
+ }
+
+
+ // 将处理好的数据按照顺序弹出,进行计算,得到数栈中最后一个数就是最终的结果
+ while (!oprStack.isEmpty()){
+ numStack.push(calc(numStack.pop(), numStack.pop(), oprStack.pop()));
+ }
+
+ return numStack.pop();
+ }
+
+
+ // 获取字符的优先级
+ private int oprPriority(int ch) {
+ if (ch == '*' || ch == '/') {
+ return 2;
+ }
+ if (ch == '+' || ch == '-') {
+ return 1;
+ }
+ return -1;
+ }
+
+
+ // 计算
+ private int calc(int num1, int num2, int opr) {
+ int res = 0;
+ switch (opr) {
+ case '+':
+ res = num1 + num2;
+ break;
+ case '-':
+ res = num2 - num1;
+ break;
+ case '*':
+ res = num1 * num2;
+ break;
+ case '/':
+ res = num2 / num1;
+ break;
+ }
+ return res;
+ }
+
+}
+
后缀表达式又称逆波兰表达式,与前缀表达式相似,只是运算符位于操作数之后。例如: (3+4)×5-6 对应的后缀表达式就是 3 4 + 5 × 6 –
+思路:从左至右扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(次顶元素 和 栈顶元素),并将结果入栈;重复上述过程直到表达式最右端,最后运算得出的值即为表达式的结果。
+class PolandNotation{
+
+ // 计算后缀表达式
+ public int calcSuffixExceptions(String suffixExpres) {
+ char[] chars = suffixExpres.toCharArray();
+ Stack<Integer> stack = new Stack<>();
+ int res =0;
+
+ for (int i = 0; i < chars.length; i++) {
+ char ch = suffixExpres.charAt(i);
+ if (!Character.isDigit(ch)) {
+ res = calc(stack.pop(),stack.pop(),ch);
+ stack.push(res);
+ }else {
+ stack.push(ch - 48);
+ }
+ }
+ return res;
+ }
+
+ // 计算
+ private int calc(int num1, int num2, int opr) {
+ int res = 0;
+ switch (opr) {
+ case '+':
+ res = num1 + num2;
+ break;
+ case '-':
+ res = num2 - num1;
+ break;
+ case '*':
+ res = num1 * num2;
+ break;
+ case '/':
+ res = num2 / num1;
+ break;
+ default:
+ throw new RuntimeException("运算符有误");
+ }
+ return res;
+ }
+
+}
+
中缀表达式转后缀表达式代码实现
+class InfixToPolandNotation{
+
+ // 根据ASCII 判断是否为数字
+ private boolean isNumber(char ch){
+ return ch >=48 && ch <= 57;
+ }
+
+ // 获取字符的优先级
+ private int oprPriority(String opr) {
+ if (opr.equals("*") || opr.equals("/")) {
+ return 2;
+ }
+ if (opr.equals("+") || opr.equals("-")) {
+ return 2;
+ }
+ return -1;
+ }
+
+ // 将中缀表达式字符串转成中缀表达式集合
+ public List<String> toInfixExceptionList(String str) {
+ ArrayList<String> list = new ArrayList<>();
+ int index = 0;
+ StringBuilder number;
+ char c;
+
+ while (index < str.length()){
+ if (!isNumber((c = str.charAt(index)))){
+ list.add(String.valueOf(c));
+ index++;
+ }else {
+ number = new StringBuilder();
+ while (index < str.length() && isNumber((c = str.charAt(index)))){
+ index++;
+ number.append(c);
+ }
+ list.add(number.toString());
+ }
+ }
+ return list;
+ }
+
+
+ // 将中缀表达式转为后缀表达式
+ public List<String> infixExpressionToSuffixExpress(List<String> list) {
+ Stack<String> stack = new Stack<>();
+ ArrayList<String> finalList = new ArrayList<>();
+
+ for (String item : list) {
+ // 如果是数字或者为( 将该值压入栈中
+ if (item.matches("\\d+")){
+ finalList.add(item);
+ continue;
+ }
+
+ if (item.equals("(")){
+ stack.push(item);
+ continue;
+ }
+
+ // 如果是 )则将 ()中间的数重新压入list中,最后将 ) 移除掉
+ if (item.equals(")")){
+ while (!stack.peek().equals("(")) {
+ finalList.add(stack.pop());
+ }
+ stack.pop();
+ }else {
+ // 如果不是 )则判断运算符的优先级,如果符号栈栈顶的优先级 >= 当前的优先级,则将该运算符加入数字栈中
+ while (stack.size() > 0 && oprPriority(stack.peek()) >= oprPriority(item)){
+ finalList.add(stack.pop());
+ }
+ stack.push(item);
+ }
+ }
+
+ // 将operStack中剩余的运算符依次弹出并加入tempList
+ while (stack.size() != 0) {
+ finalList.add(stack.pop());
+ }
+ return finalList;
+ }
+
+}
+
完整逆波兰表达式代码,支持小数、支持消除空格
+public class ReversePolishMultiCalc {
+
+ /**
+ * 匹配 + - * / ( ) 运算符
+ */
+ static final String SYMBOL = "\\+|-|\\*|/|\\(|\\)";
+
+ static final String LEFT = "(";
+ static final String RIGHT = ")";
+ static final String ADD = "+";
+ static final String MINUS= "-";
+ static final String TIMES = "*";
+ static final String DIVISION = "/";
+
+ /**
+ * 加減 + -
+ */
+ static final int LEVEL_01 = 1;
+ /**
+ * 乘除 * /
+ */
+ static final int LEVEL_02 = 2;
+
+ /**
+ * 括号
+ */
+ static final int LEVEL_HIGH = Integer.MAX_VALUE;
+
+
+ static Stack<String> stack = new Stack<>();
+ static List<String> data = Collections.synchronizedList(new ArrayList<String>());
+
+ /**
+ * 去除所有空白符
+ * @param s
+ * @return
+ */
+ public static String replaceAllBlank(String s ){
+ // \\s+ 匹配任何空白字符,包括空格、制表符、换页符等等, 等价于[ \f\n\r\t\v]
+ return s.replaceAll("\\s+","");
+ }
+
+ /**
+ * 判断是不是数字 int double long float
+ * @param s
+ * @return
+ */
+ public static boolean isNumber(String s){
+ Pattern pattern = Pattern.compile("^[-\\+]?[.\\d]*$");
+ return pattern.matcher(s).matches();
+ }
+
+ /**
+ * 判断是不是运算符
+ * @param s
+ * @return
+ */
+ public static boolean isSymbol(String s){
+ return s.matches(SYMBOL);
+ }
+
+ /**
+ * 匹配运算等级
+ * @param s
+ * @return
+ */
+ public static int calcLevel(String s){
+ if("+".equals(s) || "-".equals(s)){
+ return LEVEL_01;
+ } else if("*".equals(s) || "/".equals(s)){
+ return LEVEL_02;
+ }
+ return LEVEL_HIGH;
+ }
+
+ /**
+ * 匹配
+ * @param s
+ */
+ public static List<String> doMatch (String s) throws Exception{
+ if(s == null || "".equals(s.trim())) throw new RuntimeException("data is empty");
+ if(!isNumber(s.charAt(0)+"")) throw new RuntimeException("data illeagle,start not with a number");
+
+ s = replaceAllBlank(s);
+
+ String each;
+ int start = 0;
+
+ for (int i = 0; i < s.length(); i++) {
+ if(isSymbol(s.charAt(i)+"")){
+ each = s.charAt(i)+"";
+ //栈为空,(操作符,或者 操作符优先级大于栈顶优先级 && 操作符优先级不是( )的优先级 及是 ) 不能直接入栈
+ if(stack.isEmpty() || LEFT.equals(each)
+ || ((calcLevel(each) > calcLevel(stack.peek())) && calcLevel(each) < LEVEL_HIGH)){
+ stack.push(each);
+ }else if( !stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek())){
+ //栈非空,操作符优先级小于等于栈顶优先级时出栈入列,直到栈为空,或者遇到了(,最后操作符入栈
+ while (!stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek()) ){
+ if(calcLevel(stack.peek()) == LEVEL_HIGH){
+ break;
+ }
+ data.add(stack.pop());
+ }
+ stack.push(each);
+ }else if(RIGHT.equals(each)){
+ // ) 操作符,依次出栈入列直到空栈或者遇到了第一个)操作符,此时)出栈
+ while (!stack.isEmpty() && LEVEL_HIGH >= calcLevel(stack.peek())){
+ if(LEVEL_HIGH == calcLevel(stack.peek())){
+ stack.pop();
+ break;
+ }
+ data.add(stack.pop());
+ }
+ }
+ start = i ; //前一个运算符的位置
+ }else if( i == s.length()-1 || isSymbol(s.charAt(i+1)+"") ){
+ each = start == 0 ? s.substring(start,i+1) : s.substring(start+1,i+1);
+ if(isNumber(each)) {
+ data.add(each);
+ continue;
+ }
+ throw new RuntimeException("data not match number");
+ }
+ }
+ //如果栈里还有元素,此时元素需要依次出栈入列,可以想象栈里剩下栈顶为/,栈底为+,应该依次出栈入列,可以直接翻转整个stack 添加到队列
+ Collections.reverse(stack);
+ data.addAll(new ArrayList<>(stack));
+
+ System.out.println(data);
+ return data;
+ }
+
+ /**
+ * 算出结果
+ * @param list
+ * @return
+ */
+ public static Double doCalc(List<String> list){
+ Double d = 0d;
+ if(list == null || list.isEmpty()){
+ return null;
+ }
+ if (list.size() == 1){
+ System.out.println(list);
+ d = Double.valueOf(list.get(0));
+ return d;
+ }
+ ArrayList<String> list1 = new ArrayList<>();
+ for (int i = 0; i < list.size(); i++) {
+ list1.add(list.get(i));
+ if(isSymbol(list.get(i))){
+ Double d1 = doTheMath(list.get(i - 2), list.get(i - 1), list.get(i));
+ list1.remove(i);
+ list1.remove(i-1);
+ list1.set(i-2,d1+"");
+ list1.addAll(list.subList(i+1,list.size()));
+ break;
+ }
+ }
+ doCalc(list1);
+ return d;
+ }
+
+ /**
+ * 运算
+ * @param s1
+ * @param s2
+ * @param symbol
+ * @return
+ */
+ public static Double doTheMath(String s1,String s2,String symbol){
+ Double result ;
+ switch (symbol){
+ case ADD : result = Double.valueOf(s1) + Double.valueOf(s2); break;
+ case MINUS : result = Double.valueOf(s1) - Double.valueOf(s2); break;
+ case TIMES : result = Double.valueOf(s1) * Double.valueOf(s2); break;
+ case DIVISION : result = Double.valueOf(s1) / Double.valueOf(s2); break;
+ default : result = null;
+ }
+ return result;
+
+ }
+
+ public static void main(String[] args) {
+ //String math = "9+(3-1)*3+10/2";
+ String math = "12.8 + (2 - 3.55)*4+10/5.0";
+ try {
+ doCalc(doMatch(math));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+}
+
哈希表也叫散列表,是根据关键码值(Key value)而直接进行访问的数据结构。
+它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
+手写模拟哈希表
+public class HashTableDemo {
+
+ public static void main(String[] args) {
+ HashTab hashTab = new HashTab(3);
+ Node node1 = new Node(1, "zs");
+ Node node2 = new Node(2, "lx");
+ Node node3 = new Node(3, "ex");
+ Node node4 = new Node(4, "as");
+ Node node5 = new Node(7, "we");
+
+ hashTab.put(node1);
+ hashTab.put(node2);
+ hashTab.put(node3);
+ hashTab.put(node4);
+ hashTab.put(node5);
+
+ System.out.println("添加元素后===》");
+ System.out.println(hashTab.toString());
+
+ System.out.println("删除后===》");
+ hashTab.remove(4);
+ System.out.println(hashTab.toString());
+ }
+}
+
+class HashTab {
+
+ private NodeList[] nodes;
+ private int size;
+
+ public HashTab(int size) {
+ this.size = size;
+ nodes = new NodeList[size];
+ for (int i = 0; i < size; i++) {
+ nodes[i] = new NodeList();
+ }
+ }
+
+ public void put(Node node) {
+ // 放入hash表的位置
+ nodes[getPosition(node.id)].add(node);
+ }
+
+ public void remove(int id) {
+ nodes[getPosition(id)].delete(id);
+ }
+
+
+ private int getPosition(int id) {
+ return id % size;
+ }
+
+ @Override
+ public String toString() {
+ return "HashTab{" +
+ "nodes=\n" + Arrays.toString(nodes) +
+ "}";
+ }
+}
+
+class NodeList {
+ // 头结点
+ Node head = null;
+
+ // 添加结点方法
+ public void add(Node node) {
+ if (head == null) {
+ head = node;
+ return;
+ }
+
+ // 头结点不要动,将添加的结点放到链表的最后一个位置
+ Node tmp = head;
+ // 当下一个结点等于null时,找到最后一个结点
+ while (tmp.next != null) {
+ tmp = tmp.next;
+ }
+ tmp.next = node;
+ }
+
+
+ // 展示当前链表
+ public void list() {
+ if (head == null) {
+ System.out.println("当前链表为空");
+ return;
+ }
+ // 辅助结点
+ Node tmp = head;
+ while (true) {
+ System.out.println(tmp);
+ if (tmp.next == null) {
+ break;
+ }
+ tmp = tmp.next;
+ }
+ }
+
+ // 根据ID删除链表中的某个结点
+ public void delete(int id) {
+ if (head == null) {
+ System.out.println("当前链表为空");
+ return;
+ }
+ // 判断删除的是否是头结点
+ if (head.id == id) {
+ head = head.next;
+ return;
+ }
+
+ Node preNode = head;
+ Node curNode = preNode.next;
+ while (curNode != null) {
+ if (curNode.id == id) {
+ preNode.next = curNode.next;
+ System.out.println("删除成功,删除的是: " + curNode.id + "," + curNode.name);
+ curNode = null;
+ return;
+ }
+ preNode = preNode.next;
+ curNode = curNode.next;
+ }
+ System.out.println("删除失败,节点不存在");
+ }
+
+
+ @Override
+ public String toString() {
+ return "NodeList{" +
+ "head=" + head +
+ "}\n";
+ }
+}
+
+class Node {
+ int id;
+ String name;
+ Node next;
+
+ public Node(int id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ return "Node{" +
+ "id=" + id +
+ ", name='" + name + '\'' +
+ ", next=" + next +
+ "}";
+ }
+}
+
++二叉树(Binary tree)是树形结构的一个重要类型。许多实际问题抽象出来的数据结构往往是二叉树形式,即使是一般的树也能简单地转换为二叉树,而且二叉树的存储结构及其算法都较为简单,因此二叉树显得特别重要。二叉树特点是每个结点最多只能有两棵子树,且有左右之分。
+
二叉树是n个有限元素的集合,该集合或者为空、或者由一个称为根的元素及两个不相交的、被分别称为左子树和右子树的二叉树组成,是有序树。当集合为空时,称该二叉树为空二叉树。在二叉树中,一个元素也称作一个结点。
+如果该二叉树的所有叶子节点都在最后一层,并且结点总数为 2^n -1, n 为层数,则称为满二叉树。
+如果该二叉树的所有叶子节点都在最后一层或者倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续,我们称为完全二叉树。
+++ +PS: 二叉树中结点等价于节点
+
对于二叉树来讲最主要、最基本的运算是遍历。遍历二叉树 是指以一定的次序访问二叉树中的每个结点。所谓访问结点是指对结点进行各种操作的简称。
+例如,查询结点数据域的内容,或输出它的值,或找出结点位置,或是执行对结点的其他操作。遍历二叉树的过程实质是把二叉树的结点进行线性排列的过程。
+二叉树的遍历方法:
+public class BinaryTreeDemo {
+
+ public static void main(String[] args) {
+ BinaryTreeObj root = new BinaryTreeObj(1,"zs");
+ BinaryTreeObj binaryTreeObj2 = new BinaryTreeObj(2,"ls");
+ BinaryTreeObj binaryTreeObj3 = new BinaryTreeObj(3,"ww");
+ BinaryTreeObj binaryTreeObj4 = new BinaryTreeObj(4,"zq");
+ BinaryTreeObj binaryTreeObj5 = new BinaryTreeObj(5,"111");
+
+ root.setLeft(binaryTreeObj2);
+ root.setRight(binaryTreeObj3);
+ binaryTreeObj3.setLeft(binaryTreeObj4);
+ binaryTreeObj3.setRight(binaryTreeObj5);
+
+ BinaryTree binaryTree = new BinaryTree();
+ binaryTree.setRoot(root);
+ binaryTree.preOrderShow();
+
+ BinaryTreeObj binaryTreeObj = binaryTree.preOrderSearch(11);
+ if (binaryTreeObj == null) {
+ System.out.println("没有找到该结点~");
+ return;
+ }
+ System.out.printf("找到当前结点:id: %d, name: %s",binaryTreeObj.getId(),binaryTreeObj.getName());
+ }
+}
+
+
+class BinaryTree {
+
+ private BinaryTreeObj root;
+
+ public void setRoot(BinaryTreeObj root) {
+ this.root = root;
+ }
+
+ public void preOrderShow() {
+ if (this.root != null) {
+ this.root.preOrder();
+ }
+ }
+
+
+ public void postOrderShow() {
+ if (this.root != null) {
+ this.root.postOrder();
+ }
+ }
+
+
+ public void midOrderShow() {
+ if (this.root != null) {
+ this.root.midOrder();
+ }
+ }
+
+ public BinaryTreeObj preOrderSearch(int id){
+ if (this.root != null) {
+ return this.root.preOrderSearch(id);
+ }
+ return null;
+ }
+
+ public BinaryTreeObj infixOrderSearch(int id){
+ if (this.root != null) {
+ return this.root.infixOrderSearch(id);
+ }
+ return null;
+ }
+
+ public BinaryTreeObj postOrderSearch(int id){
+ if (this.root != null) {
+ return this.root.postOrderSearch(id);
+ }
+ return null;
+ }
+
+}
+
+
+class BinaryTreeObj {
+
+ private Integer id;
+ private String name;
+
+ public BinaryTreeObj(Integer id,String name){
+ this.id = id;
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Integer getId() {
+ return id;
+ }
+
+ private BinaryTreeObj left;
+ private BinaryTreeObj right;
+
+ public void setLeft(BinaryTreeObj left) {
+ this.left = left;
+ }
+
+ public void setRight(BinaryTreeObj right) {
+ this.right = right;
+ }
+
+ // 前序遍历:根结点 =》 左结点 =》 右结点
+ public void preOrder(){
+ System.out.println(this);
+ if (this.left != null){
+ this.left.preOrder();
+ }
+
+ if (this.right != null){
+ this.right.preOrder();
+ }
+ }
+
+ // 后序遍历: 左结点 =》 右结点 =》 根结点
+ public void postOrder(){
+
+ if (this.left != null){
+ this.left.postOrder();
+ }
+
+ if (this.right != null){
+ this.right.postOrder();
+ }
+ System.out.println(this);
+ }
+
+ // 中序遍历: 左结点 =》 根结点 =》 右结点
+ public void midOrder(){
+
+ if (this.left != null){
+ this.left.midOrder();
+ }
+ System.out.println(this);
+
+ if (this.right != null){
+ this.right.midOrder();
+ }
+ }
+
+ // 前序查找
+ public BinaryTreeObj preOrderSearch(int id) {
+ if (id == this.id){
+ return this;
+ }
+
+ BinaryTreeObj binaryTreeObj = null;
+ if (this.left != null){
+ binaryTreeObj = this.left.preOrderSearch(id);
+ }
+
+ if (binaryTreeObj != null){
+ return binaryTreeObj;
+ }
+
+ if (this.right != null){
+ binaryTreeObj = this.right.preOrderSearch(id);
+ }
+ return binaryTreeObj;
+ }
+
+ // 中序查找
+ public BinaryTreeObj infixOrderSearch(int id) {
+
+ BinaryTreeObj binaryTreeObj = null;
+ if (this.left != null){
+ binaryTreeObj = this.left.infixOrderSearch(id);
+ }
+
+ if (id == this.id){
+ return this;
+ }
+
+ if (binaryTreeObj != null){
+ return binaryTreeObj;
+ }
+
+ if (this.right != null){
+ binaryTreeObj = this.right.infixOrderSearch(id);
+ }
+ return binaryTreeObj;
+ }
+
+ // 后序查找
+ public BinaryTreeObj postOrderSearch(int id) {
+
+ BinaryTreeObj binaryTreeObj = null;
+ if (this.left != null){
+ binaryTreeObj = this.left.postOrderSearch(id);
+ }
+
+ if (binaryTreeObj != null){
+ return binaryTreeObj;
+ }
+
+ if (this.right != null){
+ binaryTreeObj = this.right.postOrderSearch(id);
+ }
+ if (id == this.id){
+ return this;
+ }
+ return binaryTreeObj;
+ }
+
+ @Override
+ public String toString() {
+ return "BinaryTreeObj{" +
+ "id=" + id +
+ ", name='" + name + '\'' +
+ '}';
+ }
+}
+
class BinaryTree {
+ public void del(int id){
+ if (this.root == null){
+ return;
+ }
+ if (this.root.getId() == id){
+ this.root = null;
+ return;
+ }
+ this.root.delNo(id);
+ }
+}
+
class BinaryTreeObj {
+ // 根据ID删除结点
+ public void delNo(int id){
+ // 找到当前结点的左子树结点是否为指定结点,如果是则将其置空
+ if (this.left != null && this.left.id == id){
+ this.left = null;
+ return;
+ }
+
+ // 与上面同理删除右子树结点
+ if (this.right != null && this.right.id == id){
+ this.right = null;
+ return;
+ }
+
+
+ if (this.left == null && this.right == null){
+ return;
+ }
+
+ // 如果当前左结点或右结点 不是要删除的结点 则进行递归删除
+ if (this.left != null){
+ this.left.delNo(id);
+ }
+
+ if (this.right != null) {
+ this.right.delNo(id);
+ }
+ }
+}
+
二叉树的顺序存储,指的是使用顺序表(数组)存储二叉树。
+需要注意的是,顺序存储只适用于完全二叉树。换句话说,只有完全二叉树才可以使用顺序表存储。因此,如果我们想顺序存储普通二叉树,需要提前将普通二叉树转化为完全二叉树。
+++顺序存储二叉树应用实例:八大排序算法中的堆排序,就会使用到顺序存储二叉树。
+
public class ArrBinaryTreeDemo {
+ public static void main(String[] args) {
+ int[] arr = {1, 2, 3, 4, 5, 6, 7};
+ ArrBinaryTree arrBinaryTree = new ArrBinaryTree();
+ arrBinaryTree.setArrayTree(arr);
+ arrBinaryTree.preArrBinaryTree(0);
+ }
+}
+
+class ArrBinaryTree {
+
+ private int[] arr = null;
+
+ public void setArrayTree(int[] arr) {
+ this.arr = arr;
+ }
+
+ public void preArrBinaryTree(int index) {
+ if (arr == null || arr.length == 0) {
+ return;
+ }
+ System.out.println(arr[index]);
+
+ int nextLeftIndex = (index << 1) + 1;
+ int nextRightIndex = (index << 1) + 2;
+
+ if (nextLeftIndex < arr.length) {
+ preArrBinaryTree(nextLeftIndex);
+ }
+
+ if (nextRightIndex < arr.length) {
+ preArrBinaryTree(nextRightIndex);
+ }
+ }
+
+}
+
n 个结点的二叉链表中含有 n+1【公式 2n-(n-1)=n+1】个空指针域。利用二叉链表中的空指针域,存放指向该结点在 某种遍历次序 下的前驱和后继结点的指针,这种附加的指针称为"线索"。这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树。
+根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种。
+如图,按照前序遍历可以得到数组 {1, 2, 4, 5, 3, 6} 其中
+中序线索化二叉树示意图
+ +public class ThreadedBinaryTreeDemo {
+
+ public static void main(String[] args) {
+ BinaryTreeObj root = new BinaryTreeObj(1,"zs");
+ BinaryTreeObj binaryTreeObj2 = new BinaryTreeObj(2,"ls");
+ BinaryTreeObj binaryTreeObj3 = new BinaryTreeObj(3,"ww");
+ BinaryTreeObj binaryTreeObj4 = new BinaryTreeObj(4,"zq");
+ BinaryTreeObj binaryTreeObj5 = new BinaryTreeObj(5,"111");
+
+ root.setLeft(binaryTreeObj2);
+ root.setRight(binaryTreeObj3);
+ binaryTreeObj3.setLeft(binaryTreeObj4);
+ binaryTreeObj3.setRight(binaryTreeObj5);
+
+ // 中序线索化二叉树
+ ThreadedBinaryTree threadedBinaryTree = new ThreadedBinaryTree();
+ threadedBinaryTree.infixThreadedBinaryTree(root);
+
+ // 遍历中序线索化二叉树
+ threadedBinaryTree.forEachInfixThreadedBinaryTree(root); // 2,1,4,3,5
+ }
+}
+
+
+class ThreadedBinaryTree{
+
+ private BinaryTreeObj pre;
+
+ // 中序线索化二叉树
+ public void infixThreadedBinaryTree(BinaryTreeObj node){
+ if (node == null){
+ return;
+ }
+ // 左递归线索化二叉树
+ infixThreadedBinaryTree(node.getLeft());
+
+ // 线索化核心代码 将左结点线索化
+ if (node.getLeft() == null){
+ node.setLeft(pre);
+ node.setLeftType(1);
+ }
+ // 将右结点线索化
+ if (pre != null && pre.getRight() == null) {
+ pre.setRight(node);
+ pre.setRightType(1);
+ }
+ // 使辅助结点指针指向当前结点
+ pre = node;
+
+ // 右递归线索化二叉树
+ infixThreadedBinaryTree(node.getRight());
+ }
+
+ // 遍历中序线索二叉树
+ public void forEachInfixThreadedBinaryTree(BinaryTreeObj root) {
+ // 用辅助结点保存根结点
+ BinaryTreeObj node = root;
+
+ while (node != null){
+
+ // 向左子树遍历,直到找到 leftType=1 的结点,等于1代表该结点为前驱结点
+ while (node.getLeftType() == 0){
+ node = node.getLeft();
+ }
+ System.out.println(node);
+
+ // 向右子树遍历,直到找到 rightType=0 的结点, 等于0代表该结点为右子树
+ while (node.getRightType() == 1){
+ node = node.getRight();
+ System.out.println(node);
+ }
+
+ // node 结点向右边找
+ node = node.getRight();
+ }
+ }
+}
+
+class BinaryTreeObj {
+
+ private Integer id;
+ private String name;
+
+ private BinaryTreeObj left;
+ private BinaryTreeObj right;
+
+ // 如果leftType == 0 表示指向的是左子树, 如果 1 则表示指向前驱结点
+ // 如果rightType == 0 表示指向是右子树, 如果 1 表示指向后继结点
+ private int leftType;
+ private int rightType;
+
+ public BinaryTreeObj(Integer id,String name){
+ this.id = id;
+ this.name = name;
+ }
+ public String getName() {
+ return name;
+ }
+ public Integer getId() {
+ return id;
+ }
+ public BinaryTreeObj getLeft() {
+ return left;
+ }
+ public BinaryTreeObj getRight() {
+ return right;
+ }
+ public void setLeft(BinaryTreeObj left) {
+ this.left = left;
+ }
+ public void setRight(BinaryTreeObj right) {
+ this.right = right;
+ }
+ public void setLeftType(int leftType) {
+ this.leftType = leftType;
+ }
+ public void setRightType(int rightType) {
+ this.rightType = rightType;
+ }
+ public int getLeftType() {
+ return leftType;
+ }
+ public int getRightType() {
+ return rightType;
+ }
+}
+
哈夫曼树重要概念:
+给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,称为哈夫曼树,有些资料也译为赫夫曼树。
+ +WPL最小的就是哈夫曼树,如上图,中间的树就是哈夫曼树。
+public class HuffmanTreeDemo {
+ public static void main(String[] args) {
+ HuffmanTree huffmanTree = new HuffmanTree();
+ HuffmanTreeNode root = huffmanTree.buildHuffmanTree(new int[]{13, 7, 8, 3, 29, 6, 1});
+ System.out.println("前序遍历huffman树:");
+ huffmanTree.preOrder(root);
+ }
+}
+
+class HuffmanTree{
+
+ public void preOrder(HuffmanTreeNode root){
+ if (root != null){
+ root.preOrder();
+ }
+ }
+
+ public HuffmanTreeNode buildHuffmanTree(int[] arr){
+
+ List<HuffmanTreeNode> list = new ArrayList<>();
+ for (int value : arr) {
+ list.add(new HuffmanTreeNode(value));
+ }
+
+ // 如果集合中的元素大于1则继续循环
+ while (list.size() > 1){
+ // 从大到小进行排序
+ Collections.sort(list);
+
+ // 获取集合中两个较小的元素进行构建 huffman 树
+ HuffmanTreeNode leftNode = list.get(0);
+ HuffmanTreeNode rightNode = list.get(1);
+ HuffmanTreeNode parentNode = new HuffmanTreeNode(leftNode.value + rightNode.value);
+ parentNode.left = leftNode;
+ parentNode.right = rightNode;
+
+ // 构建后将 leftNode, rightNode 移除集合;将parentNode加入集合;然后重新排序
+ list.remove(leftNode);
+ list.remove(rightNode);
+ list.add(parentNode);
+ }
+ return list.get(0);
+ }
+
+}
+
+
+class HuffmanTreeNode implements Comparable<HuffmanTreeNode>{
+ int value;
+ HuffmanTreeNode left;
+ HuffmanTreeNode right;
+
+ public HuffmanTreeNode(int value){
+ this.value = value;
+ }
+
+ public void preOrder(){
+ System.out.println(this);
+ if (this.left != null){
+ this.left.preOrder();
+ }
+ if (this.right != null){
+ this.right.preOrder();
+ }
+ }
+
+ @Override
+ public int compareTo(HuffmanTreeNode o) {
+ // 从小到大排序
+ return this.value - o.value;
+ }
+
+ @Override
+ public String toString() {
+ return "HuffmanTreeNode{" +
+ "value=" + value +
+ '}';
+ }
+}
+
二叉排序树(Binary Sort Tree),又称二叉查找树(Binary Search Tree),亦称二叉搜索树。是数据结构中的一类。在一般情况下,查询效率比链表结构要高。
+对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。如果有相同的值,可以将该节点放在左子节点或右子节点,二叉排序树的中序遍历为有序数列。
+ +class BinarySortTree {
+
+ private Node root;
+
+ public void setRoot(Node root) {
+ this.root = root;
+ }
+
+ // 添加结点
+ public void add(Node node) {
+ if (this.root == null) {
+ this.root = node;
+ } else {
+ this.root.add(node);
+ }
+ }
+
+ // 中序遍历
+ public void infixOrder() {
+ if (this.root != null) {
+ this.root.infixOrder();
+ }
+ }
+
+ // 查找结点
+ public Node search(int target) {
+ if (this.root == null) {
+ return null;
+ }
+ return this.root.search(target);
+ }
+
+ // 查找当前结点的父结点
+ public Node searchParentNode(int target) {
+ if (this.root == null) {
+ return null;
+ }
+ return this.root.searchParentNode(target);
+ }
+
+ // 删除结点
+ public void delNode(int target) {
+ if (this.root == null) {
+ return;
+ }
+ // 如果删除的结点是根结点
+ if (this.root.left == null && this.root.right == null) {
+ this.root = null;
+ return;
+ }
+ Node targetNode = search(target);
+ if (targetNode == null) {
+ return;
+ }
+ // 获取当前结点的父结点
+ Node parentNode = searchParentNode(target);
+ // 删除的结点是叶子结点
+ if (targetNode.left == null && targetNode.right == null) {
+ // 判断是左结点还是右结点
+ if (parentNode.left != null && parentNode.left.val == target) {
+ parentNode.left = null;
+ } else {
+ parentNode.right = null;
+ }
+ return;
+ }
+
+ // 删除的结点有两个结点
+ if (targetNode.left != null && targetNode.right != null) {
+ // 从右子树找到最小的值并删除,将该值赋值给targetNode
+ targetNode.val = delRightTreeMin(targetNode.right);
+ return;
+ }
+
+ // 删除只有一颗子树的结点
+ if (targetNode.left != null) {
+ if(parentNode == null){
+ root = targetNode.left;
+ return;
+ }
+ // 当前结点存在左子树
+ if (parentNode.left.val == target) {
+ parentNode.left = targetNode.left;
+ } else {
+ parentNode.right = targetNode.left;
+ }
+ }
+ if (targetNode.right != null) {
+ if(parentNode == null){
+ root = targetNode.right;
+ return;
+ }
+ // 当前结点存在右子树
+ if (parentNode.right.val == target) {
+ parentNode.left = targetNode.right;
+ } else {
+ parentNode.right = targetNode.right;
+ }
+ }
+ }
+
+ private int delRightTreeMin(Node node) {
+ Node target = node;
+ // 循环的查找左子节点,就会找到最小值
+ while (target.left != null) {
+ target = target.left;
+ }
+ // 此时 target就指向了最小结点 删除最小结点(该节点肯定是左叶子节点)
+ delNode(target.val);
+ return target.val;
+ }
+
+}
+
+class Node {
+ int val;
+ Node left;
+ Node right;
+
+ public Node(int val) {
+ this.val = val;
+ }
+
+ // 查找结点
+ public Node search(int target) {
+ if (this.val == target) {
+ return this;
+ } else if (this.val > target) {
+ if (this.left == null) {
+ return null;
+ }
+ return this.left.search(target);
+ } else {
+ if (this.right == null) {
+ return null;
+ }
+ return this.right.search(target);
+ }
+ }
+
+ // 查找当前结点的父结点
+ public Node searchParentNode(int target) {
+ if ((this.left != null && this.left.val == target) || (this.right != null && this.right.val == target)) {
+ return this;
+ } else if (this.left != null && this.val > target) {
+ return this.left.searchParentNode(target);
+ } else if (this.right != null && this.val <= target) {
+ return this.right.searchParentNode(target);
+ } else {
+ return null;
+ }
+ }
+
+ // 添加结点
+ public void add(Node node) {
+ if (node == null) {
+ return;
+ }
+ // 如果当前待插入结点的值小于当前结点,将其插入在左子树中
+ if (node.val < this.val) {
+ if (this.left == null) {
+ this.left = node;
+ } else {
+ this.left.add(node);
+ }
+ } else {
+ // 将当前结点插入右子树
+ if (this.right == null) {
+ this.right = node;
+ } else {
+ this.right.add(node);
+ }
+ }
+ }
+
+ // 中序遍历
+ public void infixOrder() {
+ if (this.left != null) {
+ this.left.infixOrder();
+ }
+ System.out.println(this);
+ if (this.right != null) {
+ this.right.infixOrder();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "Node{" +
+ "val=" + val +
+ '}';
+ }
+}
+
二叉搜索树一定程度上可以提高搜索效率,但是当序列构造二叉搜索树,可能会将二叉树退化成单链表,从而降低搜索效率。
+平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree)又被称为AVL树,可以保证查询效率较高。
+平衡二叉树的特点:
+将有序二叉树变为平衡二叉树代码
+public class AVLTreeDemo {
+ public static void main(String[] args) {
+ AVLTree avlTree = new AVLTree();
+ int[] arr = {10, 11, 7, 6, 8, 9 };
+ for (int i = 0; i < arr.length; i++) {
+ Node node = new Node(arr[i]);
+ avlTree.add(node);
+ }
+ System.out.println("当前树高度:"+ avlTree.height());
+ System.out.println("当前根结点:" + avlTree.getRoot());
+ System.out.println("当前左子树高度:"+ avlTree.getRoot().leftHeight());
+ System.out.println("当前右子树高度:"+ avlTree.getRoot().rightHeight());
+ }
+}
+
+class AVLTree{
+ private Node root;
+
+ public void setRoot(Node root) {
+ this.root = root;
+ }
+
+ public Node getRoot() {
+ return root;
+ }
+
+ public int height(){
+ if (root == null){
+ return 0;
+ }
+ return this.root.height();
+ }
+
+ // 添加结点
+ public void add(Node node) {
+ if (this.root == null) {
+ this.root = node;
+ } else {
+ this.root.add(node);
+ }
+ }
+
+ // 中序遍历
+ public void infixOrder() {
+ if (this.root != null) {
+ this.root.infixOrder();
+ }
+ }
+
+}
+
+class Node {
+
+ int value;
+ Node left;
+ Node right;
+
+ public Node(int value) {
+ this.value = value;
+ }
+
+ // 获取当前左子树的高度
+ public int leftHeight(){
+ if (this.left == null){
+ return 0;
+ }
+ return this.left.height();
+ }
+
+ // 获取当前结点右子树的高度
+ public int rightHeight(){
+ if (this.right == null){
+ return 0;
+ }
+ return this.right.height();
+ }
+
+ // 获取当前结点的高度
+ public int height() {
+ return Math.max(
+ (this.left == null ? 0 : this.left.height()),
+ (this.right == null ? 0 : this.right.height())
+ ) + 1;
+ }
+
+ // 左旋转
+ public void leftRote(){
+ // 创建一个新结点,并设置值等于当前结点的值
+ Node newNode = new Node(value);
+ // 使新结点的左结点指向当前结点的左结点
+ newNode.left = left;
+ // 新结点的右结点指向当前结点的右结点的左结点
+ newNode.right = right.left;
+ // 使当前结点的值指向新结点
+ value = right.value;
+ // 使当前结点的右结点指向当前结点的右结点的右结点
+ right = right.right;
+ // 使当前结点的左结点指向新结点
+ left = newNode;
+ }
+
+ // 右旋转
+ public void rightRote(){
+ Node newNode = new Node(value);
+ newNode.right = right;
+ newNode.left = left.right;
+ value = left.value;
+ left = left.left;
+ right = newNode;
+ }
+
+ // 添加结点
+ public void add(Node node) {
+ if (node == null) {
+ return;
+ }
+ // 如果当前待插入结点的值小于当前结点,将其插入在左子树中
+ if (node.value < this.value) {
+ if (this.left == null) {
+ this.left = node;
+ } else {
+ this.left.add(node);
+ }
+ } else {
+ // 将当前结点插入右子树
+ if (this.right == null) {
+ this.right = node;
+ } else {
+ this.right.add(node);
+ }
+ }
+ // 如果左子树的高度-右子树的高度 > 1 进行右旋转 反之进行左旋转
+ if (this.leftHeight() - this.rightHeight() > 1){
+ // 如果当前结点的左子树的右子树的高度>当前结点左子树的左子树的高度 则进行左旋转
+ if (this.left != null && this.left.rightHeight() > this.left.leftHeight()){
+ // 对当前结点的左结点进行左旋转
+ this.left.leftRote();
+ // 对当前结点右旋转
+ this.rightRote();
+ }else {
+ this.rightRote();
+ }
+ return;
+ }
+ if (this.rightHeight() - this.leftHeight() > 1) {
+ if (this.right != null && this.right.leftHeight() > this.right.rightHeight()){
+ // 对当前结点的右结点进行右旋转
+ this.right.rightRote();
+ // 对当前结点进行左旋转
+ this.leftRote();
+ }else {
+ this.leftRote();
+ }
+ }
+ }
+
+ // 中序遍历
+ public void infixOrder() {
+ if (this.left != null) {
+ this.left.infixOrder();
+ }
+ System.out.println(this);
+ if (this.right != null) {
+ this.right.infixOrder();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "Node{" +
+ "value=" + value +
+ '}';
+ }
+}
+
在二叉树中,每个节点有数据项,最多有两个子节点。如果允许每个节点可以有更多的数据项和更多的子节点,就是多叉树(multiway tree)。多叉树通过重新组织节点,减少树的高度,能对二叉树进行优化。典型的多叉树有:2-3树、2-3-4树、红黑树和B树。
+多叉树的前提是有序二叉树。
+2-3树是由二节点和三节点构成的树,是最简单的B树结构,2-3树的所有叶子节点都在同一层(只要是B树都满足这个条件)。
+2-3-4树,与2-3树类似。
+ +B-tree 树即 B 树,B 即 Balanced ,平衡的意思。 B树通过重新组织节点,降低树的高度,并且减少i/o读写次数来提升效率。
+文件系统及数据库系统的设计者利用了磁盘预读原理,将一个节点的大小设为等于一个页(页的大小通常为4k),这样每个节点只需要一次I/O就可以完全载入。
+++B树的阶(度):节点的最多子节点个数。比如2-3树的阶是3,2-3-4树的阶是4。
+
B树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点。
+B树的关键字集合分布在整颗树中,即叶子节点和非叶子节点都存放数据,搜索有可能在非叶子结点结束,其搜索性能等价于在关键字全集内做一次二分查找。
+ +B+树是B树的变体,也是一种多路搜索树。
+B+树的搜索与B树也基本相同,区别是B+树只有达到叶子结点才命中(B树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找。
+B+树所有关键字都出现在叶子结点的链表中(即数据只能在叶子节点【也叫稠密索引】),且链表中的关键字(数据)恰好是有序的。所以不可能在非叶子结点命中。
+B+树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录, B+树更适合文件索引系统,B树和B+树各有自己的应用场景,不能说B+树完全比B树好,反之亦然。
+ +B* 树是B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针。
+ +图是一种数据结构,其中结点可以具有零个或多个相邻元素。两个结点之间的连接称为边。结点也可以称为顶点。
+如果给图的每条边规定一个方向,那么得到的图称为有向图。在有向图中,与一个节点相关联的边有出边和入边之分。相反,边没有方向的图称为无向图。
+ +可用二维数组表示图(邻接矩阵);或链表表示(邻接表)。
+ + +用java模拟图,包括图的深度遍历,广度遍历。
+ + +public class GraphDemo {
+ public static void main(String[] args) {
+ String[] vertexes = {"A", "B", "C", "D", "E"};
+ int n = vertexes.length;
+ Graph graph = new Graph(n);
+ for (String vertex : vertexes) {
+ graph.addVertex(vertex);
+ }
+ graph.addEdge(0, 1, 1); // A-B
+ graph.addEdge(0, 2, 1); // A-C
+ graph.addEdge(1, 2, 1); // B-C
+ graph.addEdge(1, 3, 1); // B-D
+ graph.addEdge(1, 4, 1); // B-E
+ // 展示图转换的矩阵
+ graph.showEdges();
+ // 图的深度遍历
+ graph.dfs();
+ System.out.println();
+ // 图的广度遍历
+ graph.bfs();
+ }
+}
+
+class Graph {
+
+ // 保存顶点
+ private List<String> vertexList;
+
+ // 保存边的数量
+ private int sideNums;
+
+ // 保存图的矩阵
+ private int[][] edges;
+
+ private boolean[] isVisited;
+
+ public Graph(int n) {
+ vertexList = new ArrayList<>(n);
+ edges = new int[n][n];
+ sideNums = 0;
+ }
+
+ // 获取第一个结点的下一个结点
+ private int getFirstNeighbor(int index) {
+ for (int i = 0; i < vertexList.size(); i++) {
+ if (edges[index][i] > 1) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ // 获取当前结点的下一个结点
+ private int getNextNeighbor(int vertex, int index) {
+ for (int i = vertex + 1; i < vertexList.size(); i++) {
+ if (edges[index][i] > 1) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ // 图的深度遍历
+ private void dfs(boolean[] isVisited, int i) {
+ System.out.print(getVertexValByIndex(i) + "->");
+ // 将当前遍历后的顶点标记为true
+ isVisited[i] = true;
+ // 获取当前结点的下一个结点的索引位置
+ int firstNeighborIndex = getFirstNeighbor(i);
+ // 如果 != -1 代表当前结点没有找到下一个结点,需要向下移动
+ while (firstNeighborIndex != -1) {
+ // 判断该结点是否被遍历过
+ if (!isVisited[firstNeighborIndex]) {
+ dfs(isVisited, firstNeighborIndex);
+ }
+ // 当前结点向后移动,否则是死循环
+ firstNeighborIndex = getNextNeighbor(firstNeighborIndex, i);
+ }
+ }
+
+ public void dfs() {
+ isVisited = new boolean[vertexList.size()];
+ for (int i = 0; i < getVertexCount(); i++) {
+ if (!isVisited[i]) {
+ dfs(isVisited, i);
+ }
+ }
+ }
+
+ // 一个结点的广度优先遍历
+ private void bfs(boolean[] isVisited, int i) {
+ // 队列头结点下标索引
+ int headIndex;
+ // 相邻结点下标索引
+ int neighborIndex;
+ LinkedList<Integer> queue = new LinkedList<>();
+
+ System.out.print(getVertexValByIndex(i) + "->");
+ isVisited[i] = true;
+ queue.addLast(i);
+
+ // 如果队列不等于空 则需要遍历循环查找
+ while (!queue.isEmpty()) {
+ headIndex = queue.removeFirst();
+ // 得到第一个邻接结点的下标
+ neighborIndex = getFirstNeighbor(headIndex);
+ while (neighborIndex != -1) {
+ // 是否访问过
+ if (!isVisited[neighborIndex]) {
+ System.out.print(getVertexValByIndex(neighborIndex) + "->");
+ isVisited[neighborIndex] = true;
+ queue.addLast(neighborIndex);
+ }
+ // neighborIndex 向下找
+ neighborIndex = getNextNeighbor(headIndex, neighborIndex);
+ }
+ }
+ }
+
+ // 广度优先遍历
+ public void bfs() {
+ isVisited = new boolean[vertexList.size()];
+ for (int i = 0; i < getVertexCount(); i++) {
+ if (!isVisited[i]) {
+ bfs(isVisited, i);
+ }
+ }
+ }
+
+ // 添加顶点
+ public void addVertex(String vertex) {
+ vertexList.add(vertex);
+ }
+
+ // 添加边
+ public void addEdge(int vertex1, int vertex2, int weight) {
+ edges[vertex1][vertex2] = weight;
+ edges[vertex2][vertex1] = weight;
+ sideNums++;
+ }
+
+ // 获取边的数量
+ public int getSideNums() {
+ return sideNums;
+ }
+
+ // 遍历矩阵
+ public void showEdges() {
+ for (int[] edge : edges) {
+ System.out.println(Arrays.toString(edge));
+ }
+ }
+
+ // 获取顶点数量
+ public int getVertexCount() {
+ return vertexList.size();
+ }
+
+ // 获取边之间的权值
+ public int getVertexWeight(int vertex1, int vertex2) {
+ return edges[vertex1][vertex2];
+ }
+
+ // 根据下标获取结点的值
+ public String getVertexValByIndex(int index) {
+ return vertexList.get(index);
+ }
+
+}
+
英文对应的单词是Algorithm,它的本意为:解决问题的方法,所以算法的直接理解就是解决问题的方法。在计算机领域定义的话就是:一系列解决问题的、清晰、可执行的计算机指令。
+一个算法的优劣可以用空间复杂度与时间复杂度来衡量。
+度量一个算法执行时间的两种方法:
+事后统计法:即直接运行程序,统计需要的时间和空间。但是,这种方法有两个问题:
+所以,就需要有一种不用具体测试数据,也能估计算法执行效率的方法,就是算法复杂度分析,包括时间、空间复杂度分析。
+事前估算法:通过分析某个算法的时间复杂度来判断那个算法更优;
+一般情况下,算法中的基本操作语句的重复执行次数是问题规模 n 的某个函数, 用T(n)
表示,若有某个辅助函数f(n)
,使得当 n 趋近于无穷大时,T(n)/f(n)
的极限值为不等于零的常数,则称f(n)
是T(n)
的同数量级函数。记作 T(n)=O(f(n))
, 称O(f(n))
为算法的渐进时间复杂度,简称时间复杂度。
例如,T(n) = n + 1 与 T(n) = n 就是同数量级函数,因为 n+1/n 的极限值为不等于零的常数。 +T(n) 不同,但时间复杂度可能相同: T(n)=n²+7n+6 与 T(n)=3n²+2n+2 它们的 T(n) 不同, 但时间复杂度相同,都为 O(n²)。
+计算时间复杂度的方法:
+时间频度
+一个算法花费的时间与算法中的语句的执行次数成正比例,哪个算法执行次数多,他花费的时间就多.一个算法中的语句执行次数称为语句频度或时间频度.记为T(n)。
+随着时间的推移,一些复杂度花费时间无限接近:
+常见的时间复杂度
+常数阶 O(1): 无论代码执行了多少行,只要是没有循环等复杂结构,这个代码的时间复杂度就都是O(1);
+int i = 1;
+int j = 2;
+++i;
+j++;
+int m = i + j;
+
对数阶 O(log2n)
+ +int i = 1;
+while(i<n){
+ i = i*2;
+}
+
线性阶 O(n): 它消耗的时间是随着n的变化而变化的,与n成正比或反比;
+for(int i=1; i<=n; ++i){
+ j = i;
+ j++;
+}
+
线性对数阶 O(nlog2n): 将时间复杂度为O(logn)的代码循环n遍,那么它的时间复杂度就是 n * O(logN),也就是了O(nlogN);
+for(int m=1; m<=n; ++m){
+ int i = 1;
+ while(i<n){
+ i = i*2;
+ }
+}
+
平方阶 O(n^2): 如果把 O(n) 的代码再嵌套循环一遍,它的时间复杂度就是 O(n²),这段代码其实就是嵌套了2层n循环,它的时间复杂度就是 O(n*n)
,即 O(n²) 如果将其中一层循环的n改成m,那它的时间复杂度就变成了 O(m*n)
;
for(int i=1; i<=n; ++i){
+ for(int j=1; j<=n; ++j){
+ j=i;
+ }
+}
+
立方阶 O(n^3)
+for(int i=1; i<=n; ++i){
+ for(int j=1; j<=n; ++j){
+ for(int x=1; x<=n; ++x){
+ int m = 0;
+ i = x+j;
+ }
+ }
+}
+
k 次方阶 O(n^k)
+指数阶 O(2^n)
+常见的算法时间复杂度由小到大依次为:
+Ο (1)<Ο (log2n)<Ο (n)<Ο (nlog2n)<Ο (n2)<Ο (n3)< Ο (nk) < Ο (2n)
+
随着问题规模 n 的不断增大,上述时间复杂度不断增大,算法的执行效率越低。
+类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储空间,它也是问题规模 n 的函数。
+空间复杂度全称为渐进空间复杂度,是对一个算法在运行过程中临时占用存储空间大小的量度。 有的算法需要占用的临时工作单元数与解决问题的规模 n 有关, 它随着 n 的增大而增大, 当 n 较大时, 将占用较多的存储单元, 例如快速排序和归并排序算法, 基数排序就属于这种情况 +在做算法分析时, 主要讨论的是时间复杂度。 从用户使用体验上看, 更看重的程序执行的速度。 一些缓存产品(redis, memcache)和算法(基数排序)本质就是用空间换时间
+空间复杂度较为简单,常见的空间复杂度为 O(1),O(n) 和 O(n ^ 2)。
+递归就是方法自己调用自己,每次调用时传入不同的变量,递归有助于编程者解决复杂的问题,同时可以让代码变得简洁。
+递归能解决什么问题?
+使用递归遵守的规则:
+迷宫回溯问题,寻找最短路径可以通过改变策略,将每个策略都经过的路保存在集合中,最后看哪个集合最小即最小路径。
+public class MyTest {
+
+ public static void main(String[] args) {
+ Mazeback mazeback = new Mazeback();
+
+ int[][] map = mazeback.createMap();
+ mazeback.list(map);
+
+ System.out.println("自动寻找路线:");
+
+ mazeback.setWay(map, 1, 1);
+ mazeback.list(map);
+ }
+
+}
+
+
+class Mazeback {
+
+ // 创建地图
+ public int[][] createMap() {
+ // 地图
+ int[][] map = new int[7][8];
+
+ for (int i = 0; i < 7; i++) {
+ map[i][0] = 1;
+ map[i][7] = 1;
+ map[6][i] = 1;
+ map[0][i] = 1;
+ }
+ map[3][1] = 1;
+ map[3][2] = 1;
+ return map;
+ }
+
+ // 遍历地图
+ public void list(int[][] map) {
+ for (int i = 0; i < map.length; i++) {
+ for (int j = 0; j < map[i].length; j++) {
+ System.out.print(map[i][j] + "");
+ }
+ System.out.println();
+ }
+ }
+
+ // 寻找路径
+ // 1:墙;2:通路,3:死路
+ public boolean setWay(int[][] map, int row, int column) {
+ if (map[5][6] == 2) {
+ return true;
+ }
+ if (map[row][column] == 0) {
+
+ // 先假设是通路
+ map[row][column] = 2;
+
+ // 寻找路径顺序:下,右,上,左
+ if (setWay(map, row + 1, column)) {
+ return true;
+ } else if (setWay(map, row, column + 1)) {
+ return true;
+ } else if (setWay(map, row - 1, column)) {
+ return true;
+ } else if (setWay(map, row, column - 1)) {
+ return true;
+ } else {
+ // 当标记为3时,说明是死路走不通
+ map[row][column] = 3;
+ return false;
+ }
+ }
+ return false;
+ }
+
+}
+
八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848 年提出:在 8 × 8 格的国际象棋上摆放八个皇后,使其不能互相攻击, 即: 任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。
+++ +八皇后问题共92中解法
+
public class MyTest {
+
+ public static void main(String[] args) {
+ EightQueens eightQueens = new EightQueens();
+ eightQueens.exec(0);
+ }
+
+}
+
+
+class EightQueens {
+
+ private int[] arr;
+
+ private int max;
+
+ public EightQueens() {
+ this.max = 8;
+ this.arr = new int[max];
+ }
+
+
+ // 算法
+ public void exec(int position) {
+ // 如果当前位置等于max说明解法成立,需要回溯
+ if (position == max) {
+ print();
+ return;
+ }
+ for (int i = 0; i < max; i++) {
+ arr[position] = i;
+ if (check(position)){
+ exec(position + 1);
+ }
+ }
+ }
+
+
+ // 判断皇后位置是否冲突
+ private boolean check(int position) {
+ for (int j = 0; j < position; j++) {
+ // 判断是否在同一列或在同一斜线上
+ if (arr[position] == arr[j] || Math.abs(position - j) == Math.abs(arr[position] - arr[j])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+
+ // 打印数组
+ private void print() {
+ for (int i = 0; i < arr.length; i++) {
+ System.out.print(arr[i] + "");
+ }
+ System.out.println();
+ }
+
+}
+
排序也称排序算法,是将一组数据,依指定的顺序进行排列的过程。
+排序算法分类:
+常见内排序算法复杂度比较
+ +名词解释:
+冒泡排序的基本思想是:通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序(当前值大于比较的值)则交换,使值较大的元素逐渐从前移向后部,就像水底下的气泡一样逐渐向上冒。
+ +优化:因为排序的过程中, 各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个标志 flag 判断元素是否进行过交换。 从而减少不必要的比较。
+class BubbleSorting {
+
+ public int[] sort(int[] data) {
+ int len = data.length - 1;
+ int tmp;
+ boolean flag = false;
+
+ for (int i = 0; i <len; i++){
+ for (int j = 0; j < len - i; j++) {
+ // 将当前值与next值进行比较,如果当前值大于next值则交换两者之间的位置
+ if (data[j] > data[j+1]){
+ flag = true;
+ tmp = data[j];
+ data[j] = data[j+1];
+ data[j+1] = tmp;
+ }
+ }
+
+ // 加入标志为进行判断,如果整个循环下啦都没有交换位置,说明该数组是有序的,所以直接退出循环
+ if (!flag){
+ break;
+ }else {
+ flag = false;
+ }
+ }
+ return data;
+ }
+}
+
选择式排序也属于内部排序法,是从欲排序的数据中,按指定的规则选出某一元素,再依规定交换位置后达到排序的目的。
+首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小元素,然后放到排序序列末尾。以此类推,直到所有元素均排序完毕。
+ +class SelectSorting{
+
+ public int[] sort(int[] data) {
+ for (int i = 0; i < data.length; i++){
+
+ int min = i;
+
+ for (int j = i+1; j < data.length; j++) {
+ // 如果next值大于当前值,则记录该值和该值的位置,等全部比较完毕后,将最大的一个与数据的末尾进行替换
+ if (data[i] > data[j]){
+ min = j;
+ }
+ }
+
+ // 将每次循环中的最小的值,调整到最前面
+ if (min != i){
+ int tmp = data[i];
+ data[i] = data[min];
+ data[min] = tmp;
+ }
+ }
+ return data;
+ }
+}
+
插入排序(Insertion Sorting) 的基本思想是:把 n 个待排序的元素看成为一个有序表和一个无序表;
+开始时有序表中只包含一个元素,无序表中包含有 n-1 个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较, 将它插入到有序表中的适当位置,使之成为新的有序表。
+对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
+ +class InsertSorting{
+
+ public int[] sort(int[] data) {
+ for (int i = 1; i < data.length; i++){
+ // 如果当前要插入的值 data[i] > 有序队列中最后一个,则将其直接插入到最后一个
+ if (data[i] > data[i - 1]){
+ continue;
+ }
+
+ int tmp = data[i];
+ int index = i - 1;
+ // 如果当前位置的值【tmp】小于 上一个位置的值【data[index]】说明当前值需要插入到有序队列中
+ while (index >= 0 && tmp < data[index]){
+ data[index + 1] = data[index];
+ index--;
+ }
+ data[index + 1] = tmp;
+ }
+ return data;
+ }
+
+}
+
希尔排序是希尔(Donald Shell) 于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序。
+希尔排序按照增量将数组进行分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
+ +class ShellSorting{
+
+ // 希尔排序,交换法
+ public void sortSwap(int[] data){
+ int size = data.length;
+ int tmp = 0;
+
+ // 将数据分组,分组数量:data.length/2
+ for (int gap = size >> 1; gap > 0; gap >>= 1){
+
+ for (int i = gap; i < size; i++) {
+ // 遍历每组中的元素
+ for (int j = i - gap; j >= 0; j -= gap) {
+ // 将每组中的元素进行排序(交换元素)
+ if (data[j] > data[j + gap]){
+ tmp = data[j];
+ data[j] = data[j+gap];
+ data[j+gap] = tmp;
+ }
+ }
+ }
+ }
+ }
+
+ // 插入法,融入 插入排序 思想
+ public void sort(int[] data){
+ int size = data.length;
+ int tmp = 0;
+
+ // 将数据分组,分组数量:data.length/2
+ for (int gap = size >> 1; gap > 0; gap >>= 1) {
+ for (int i = gap; i < size; i++) {
+
+ tmp = data[i];
+ int index = i;
+
+ // 如果当前位置的值【tmp】小于 上一个位置的值【data[index]】说明当前值需要插入到有序队列中
+ while (index - gap >= 0 && tmp < data[index - gap]){
+ data[index] = data[index - gap];
+ index -= gap;
+ }
+ data[index] = tmp;
+ }
+ }
+ }
+
+}
+
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序n个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环可以在大部分的架构上很有效率地被实现出来。
+快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。快速排序思路:
+class QuickSorting {
+
+ public void sort(int[] data, int l, int r) {
+
+ // 如果开始的位置大于等于结束的位置则不用进行比较直接退出
+ if (l >= r){
+ return;
+ }
+
+ int left = l;
+ int right = r;
+
+ // 基准数值,将小于该数值的放在该数字的左边,大于该数值的放在右边
+ int pivot = data[left];
+
+ while (left < right) {
+
+ // 从右向左开始比较,如果此数大于等于基准数则将right索引向前移动,否则,将该值覆盖到对应的 data[left] 中
+ while (left < right && data[right] >= pivot) {
+ --right;
+ }
+ data[left] = data[right];
+
+ // 从左向右开始比较,如果此数小于等于基准数则将left索引向后移动,否则,将该值覆盖到对应的 data[right] 中
+ while (left < right && data[left] <= pivot) {
+ ++left;
+ }
+ data[right] = data[left];
+ }
+
+ // 此时left与right指向重合的位置为基准所在的位置,需要将该位置覆盖掉为基准的值
+ data[left] = pivot;
+
+ // 递归排序
+ sort(data, l, left);
+ sort(data, right+1,r);
+ }
+}
+
归并排序是利用归并的思想实现的排序方法, 该算法采用经典的分治(divide-and-conquer)策略;分治法将问题分成一些小的问题然后递归求解, 而治的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之。
+归并排序算法思路:采用分治算法思想,首先将序列使用递归进行拆分,然后进行合并;合并思路,将两个有序队列中的元素分别按顺序进行比较,将结果保存在一个临时数组中,最后将临时数组合并到最后的队列中。
+ +class MergeSorting {
+
+ // 递归拆分算法
+ public void divide(int[] arr, int start, int end, int[] tmpArr) {
+ if (start >= end) {
+ return;
+ }
+ int mid = (start + end) >> 1;
+
+ // 分别向左向右递归
+ divide(arr, start, mid, tmpArr);
+ divide(arr, mid + 1, end, tmpArr);
+
+ // 拆分一次合并一次
+ merge(arr, start, mid, end, tmpArr);
+
+ }
+
+ // 合并算法
+ public void merge(int[] arr, int start, int mid, int end, int[] tmpArr) {
+ int leftIndex = start;
+ int rightIndex = mid + 1;
+ int tmpArrIndex = 0;
+
+ // 判断是否超出范围
+ while (leftIndex <= mid && rightIndex <= end) {
+
+ // 将两组数据进行比较,按照从小到大的顺序将两组数据填入 tmpArr 中
+ if (arr[leftIndex] <= arr[rightIndex]) {
+ tmpArr[tmpArrIndex] = arr[leftIndex];
+ ++leftIndex;
+ } else {
+ tmpArr[tmpArrIndex] = arr[rightIndex];
+ ++rightIndex;
+ }
+ ++tmpArrIndex;
+ }
+
+ // 判断两组数据是否还有剩余,如果有剩余数据,则直接将数据追加到 tmpArr 数组后边
+ while (leftIndex <= mid) {
+ tmpArr[tmpArrIndex] = arr[leftIndex];
+ ++leftIndex;
+ ++tmpArrIndex;
+ }
+
+ while (rightIndex <= end) {
+ tmpArr[tmpArrIndex] = arr[rightIndex];
+ ++rightIndex;
+ ++tmpArrIndex;
+ }
+
+ // 将两组数据进行合并
+ tmpArrIndex = 0;
+ int tmpLeftIndex = leftIndex;
+ while (tmpLeftIndex <= end) {
+ arr[tmpLeftIndex] = tmpArr[tmpArrIndex];
+ ++tmpLeftIndex;
+ ++tmpArrIndex;
+ }
+ }
+
+}
+
基数排序是 1887 年赫尔曼·何乐礼发明的。基数排序属于“分配式排序”,又称“桶子法”或 bin sort,顾名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用,基数排序法是属于稳定性的排序, 基数排序法的是效率高的稳定性排序法。
+基数排序是桶排序的扩展,它是这样实现的: 将所有待比较数值统一为同样的数位长度,数位较短的数前面补零;然后, 从最低位开始, 依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
+ +class BucketSorting {
+
+ public void sort(int[] arr) {
+ // 求数组中最大数的长度
+ int maxNum = arr[0];
+ for (int i = 0; i < arr.length; i++) {
+ if (arr[i] > maxNum) {
+ maxNum = arr[i];
+ }
+ }
+ int maxNumLen = String.valueOf(maxNum).length();
+
+ // 桶,用于保存数据
+ int[][] buckets = new int[10][arr.length];
+
+ // 存放每个桶的保存数据的索引
+ int[] bucketElementIndex = new int[10];
+
+ // 将数组中的元素 按照 个、十、百、千 …… 的顺序依次放入桶中
+ for (int i = 0, n = 1; i < maxNumLen; i++, n *= 10) {
+
+ // 遍历二维数组
+ for (int j = 0; j < arr.length; j++) {
+ // 计算放入的桶的下标
+ int index = arr[j] / n % 10;
+ buckets[index][bucketElementIndex[index]] = arr[j];
+ bucketElementIndex[index]++;
+ }
+
+ // 从桶中依次取出元素并放入原数组中
+ int index = 0;
+ for (int f = 0; f < bucketElementIndex.length; f++) {
+ // 判断桶中是否保存数据
+ if (bucketElementIndex[f] == 0) {
+ continue;
+ }
+ for (int h = 0; h < bucketElementIndex[f]; h++) {
+ arr[index] = buckets[f][h];
+ index++;
+ }
+ bucketElementIndex[f] = 0;
+ }
+ }
+
+ }
+}
+
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏、最好、平均时间复杂度均为O(nlogn),它也是不稳定排序。
+堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。
+++ + +大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列; +小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
+
class HeapSorting {
+
+ public void sort(int[] arr) {
+ // 计算树中的叶子结点位置
+ int leafNode = (arr.length >> 1) - 1;
+
+ // 构建大顶堆,此处需要注意 i >= 0 需要算根结点
+ for (int i = leafNode; i >= 0; i--) {
+ buildMaxHeap(arr, i, arr.length);
+ }
+
+ // 构建大顶堆后,将根元素与树中的最后一个进行交换,再循环构建大顶堆
+ int tmp;
+ for (int i = arr.length - 1; i > 0; i--) {
+ tmp = arr[i];
+ arr[i] = arr[0];
+ arr[0] = tmp;
+ buildMaxHeap(arr, 0, i);
+ }
+ }
+
+ /**
+ * 堆排序核心代码 构建大顶堆
+ *
+ * @param arr 需要调整的数组
+ * @param i 非叶子结点的索引位置
+ * @param len 每次调整的长度
+ */
+ public void buildMaxHeap(int[] arr, int i, int len) {
+ // 保存非叶子结点的位置如果该结点的值小于子结点的值,则需要进行交换
+ int tmp = arr[i];
+
+ // 从上至下,从左至右 遍历. 从第一个左子结点开始遍历
+ for (int n = (i << 1) + 1; n < len; n = (n << 1) + 1) {
+ // 如果左子结点 < 右结点,则需要将 n 指向右结点,即后移
+ if (n + 1 < len && arr[n] < arr[n + 1]) {
+ n++;
+ }
+ // 当前非叶子结点 < 当前子结点
+ if (arr[n] > tmp) {
+ // 将当前非叶子结点指向叶子结点
+ arr[i] = arr[n];
+ // 将i指向当前叶子结点,待最后将其变为 非叶子结点的值 即tmp的值
+ i = n;
+ } else {
+ break;
+ }
+ }
+ // 与前面互相呼应
+ arr[i] = tmp;
+ }
+}
+
线性查找又称顺序查找,是一种最简单的查找方法,它的基本思想是从第一个记录开始,逐个比较记录的关键字,直到和给定的K值相等,则查找成功;若比较结果与文件中n个记录的关键字都不等,则查找失败。
+class LinearSearch{
+
+ public int search(int[] arr, int value){
+ for (int i = 0; i < arr.length; i++){
+ if (arr[i] == value) {
+ return i;
+ }
+ }
+ return - 1;
+ }
+
+}
+
二分查找也称折半查找,它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。
+二分查找算法的前提,数组必须是有序数组,如果没有有序列表,请使用排序算法对列表进行排序。
+递归实现二分查找
+class BinarySearch {
+
+ // 使用二分查找时,arr必须为有序列表
+ public int search(int start, int end, int[] arr, int value) {
+ // 在 {@param arr} 中 没有查到 {@param value}
+ if (start > end || value > arr[end] || value < arr[start]) {
+ return -1;
+ }
+
+ // 获取中间值,用于分割列表
+ int mid = (start + end) >> 1;
+ int midVal = arr[mid];
+
+ // 如果 查找的值< 中间值,说明该值可能在mid的左边
+ if (value < midVal) {
+ return search(start, mid - 1, arr, value);
+ }
+
+ // 相反如果 查找的值 > 中间值,说明该值可能在mid的右边
+ if (value > midVal) {
+ return search(mid + 1, end, arr, value);
+ }
+
+ // 使用递归不停的向下细分,当 value == arr[mid] 时 返回该值,说明此时已经找到了
+ return mid;
+ }
+}
+
非递归实现二分查找
+public class BinarySearchDemo {
+ public static void main(String[] args) {
+ int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
+ System.out.println(new BinarySearch().search(arr, 3));
+ }
+}
+
+class BinarySearch {
+
+ // 使用二分查找时,arr必须为有序列表
+ public int search(int[] arr, int target) {
+ int left = 0;
+ int right = arr.length - 1;
+ while (left <= right) {
+ int mid = (left + right) >> 1;
+ if (arr[mid] == target) {
+ return mid;
+ }
+ // 如果目标值小于中间值则向左边找,反之向右边找
+ if (target < arr[mid]) {
+ right = mid - 1;
+ }
+
+ if (target > arr[mid]) {
+ left = mid + 1;
+ }
+ }
+ return -1;
+ }
+
+}
+
插值查找算法类似于二分查找,与二分查找不同的是插值查找每次从自适应 mid 处开始查找,而不是像二分查找那样每次都从中间开始找。
+ +注意:对于数据量较大,关键字分布比较均匀(最好是线性分布)的查找表来说,采用插值查找,速度较快;对于关键字分布不均匀的情况下,该方法不一定比二分查找要好。
+class InsertValueSearch {
+
+ // 与二分查找基本相同,只是查找 mid 值发生了变动
+ public int search(int start, int end, int[] arr, int value) {
+ // 在 {@param arr} 中 没有查到 {@param value}
+ if (start > end || value > arr[end] || value < arr[start]) {
+ return -1;
+ }
+
+ int mid = start + (value - arr[start]) / (arr[end] - arr[start]) * (end - start);
+ int midVal = arr[mid];
+
+ if (value < midVal) {
+ return search(start, mid - 1, arr, value);
+ }
+
+ if (value > midVal) {
+ return search(mid + 1, end, arr, value);
+ }
+ return mid;
+ }
+
+}
+
斐波那契查找是基于【黄金分割】的二分查找。即在斐波那契队列中,将二分查找中的分割点替换为黄金分割点,来查找。
+++黄金分割是指将整体一分为二,较大部分与整体部分的比值等于较小部分与较大部分的比值,其 比值 约为 0.618。这个比例被公认为是最能引起美感的 比例,因此被称为黄金分割。
+
斐波那契查找特点:
+class FibonacciSearch{
+ // lookupTable,需要传入斐波那契数列,例如:{1,1,2,3,5,8,13,21,34,55};
+ public static int search(int[] lookupTable,int[] f,int target){
+
+ int low = 0;
+ int high = lookupTable.length - 1;
+
+ // k 是 Fibonacci 分割数组下标
+ int k = 0;
+ int middle = 0;
+
+ while (f[k] < high){
+ k ++;
+ }
+
+ //利用 java 工具类构造 f[k] 长度的查找表,解决原有查找表元素不够的问题
+ int[] temp = Arrays.copyOf(lookupTable,f[k]);
+ while (low <= high){
+ middle = low + f[k - 1];
+ if (target < lookupTable[middle]){
+ high = middle -1;
+ k --;
+ }else if (target > lookupTable[middle]){
+ low = middle + 1;
+ k -= 2;
+ }else{
+ return Math.min(middle,high);
+ }
+ }
+ return -1;
+ }
+}
+
赫夫曼编码也翻译为哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,属于一种程序算法。
+哈夫曼编码是哈夫曼树在电讯通信中的经典的应用之一。哈夫曼编码广泛地用于数据文件压缩。其压缩率通常在20%~90%之间。哈夫曼码是可变字长编码的一种。Huffman于1952年提出一种编码方法,称之为最佳编码。
+++定长编码与变长编码,以字符串like like为例:
++
+- 定长编码:
++
+- 将上述字符串转换对应的ASCII: 108 105 107 101 32 108 105 107 101
+- ASCII转换为二进制:01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 +
++
+- 变长编码:
++
+- 统计上述字符串出现的各字符出现的次数:l:2 i:2 k:2 e:2 :1
+- 按照各个字符出现的次数进行编码,原则是出现次数越多的,则编码越小:0=l 1=i 10=k 11=e 100=
+- 最终转换为变长编码为:011011100011011
+
上述的变长编码 011011100011011 在解码的时候会出现多意现象,比如当匹配到数字1,是把1解成i还是按照10来进行解码。因为这种现象的存在,所以在进行变长编码时,编码要符合前缀编码。
+++字符的编码都不能是其他字符编码的前缀,符合此要求的编码叫做前缀编码, 即不能匹配到重复的编码。
+
构建哈夫曼编码思路:
+假如一段信息里只有A,B,C,D,E,F这6个字符,他们出现的次数依次是2次,3次,7次,9次,18次,25次,最终构建成哈夫曼编树为下图所示:
+ +得到哈夫曼编码:
+A=11100 B=11101 C=1111 D=110 E=10 F=0
+
利用哈夫曼编码,压缩解压文件:
+public class HuffmanCodeTest {
+
+ public static void main(String[] args) {
+
+ // 测试压缩文件
+ String srcFile = "/Users/whitepure/Desktop/1.png";
+ String dstFile = "/Users/whitepure/Desktop/1.zip";
+
+ zipFile(srcFile, dstFile);
+ System.out.println("压缩文件成功");
+
+ // 测试解压文件
+ srcFile = "/Users/whitepure/Desktop/1.zip";
+ dstFile = "/Users/whitepure/Desktop/1copy.png";
+ unZipFile(srcFile, dstFile);
+ System.out.println("解压成功!");
+ }
+
+
+ // 将一个文件进行压缩
+ public static void zipFile(String srcFile, String dstFile) {
+ // 创建输出流
+ OutputStream os = null;
+ ObjectOutputStream oos = null;
+ // 创建文件的输入流
+ FileInputStream is = null;
+ try {
+ // 创建文件的输入流
+ is = new FileInputStream(srcFile);
+ // 创建一个和源文件大小一样的byte[]
+ byte[] b = new byte[is.available()];
+ // 读取文件
+ is.read(b);
+ HuffmanCode huffmanCode = new HuffmanCode();
+ // 直接对源文件压缩
+ byte[] huffmanBytes = huffmanCode.encode(b);
+ // 创建文件的输出流, 存放压缩文件
+ os = new FileOutputStream(dstFile);
+ // 创建一个和文件输出流关联的ObjectOutputStream
+ oos = new ObjectOutputStream(os);
+ // 把 赫夫曼编码后的字节数组写入压缩文件
+ oos.writeObject(huffmanBytes); // 我们是把
+ // 这里我们以对象流的方式写入 赫夫曼编码,是为了以后我们恢复源文件时使用
+ // 注意一定要把赫夫曼编码 写入压缩文件
+ oos.writeObject(huffmanCode.getHuffmanCodes());
+
+ } catch (Exception e) {
+ // TODO: handle exception
+ System.out.println(e.getMessage());
+ } finally {
+ try {
+ is.close();
+ oos.close();
+ os.close();
+ } catch (Exception e) {
+ // TODO: handle exception
+ System.out.println(e.getMessage());
+ }
+ }
+
+ }
+
+
+ // 完成对压缩文件的解压
+ public static void unZipFile(String zipFile, String dstFile) {
+
+ // 定义文件输入流
+ InputStream is = null;
+ // 定义一个对象输入流
+ ObjectInputStream ois = null;
+ // 定义文件的输出流
+ OutputStream os = null;
+ try {
+ // 创建文件输入流
+ is = new FileInputStream(zipFile);
+ // 创建一个和 is关联的对象输入流
+ ois = new ObjectInputStream(is);
+ // 读取byte数组 huffmanBytes
+ byte[] huffmanBytes = (byte[]) ois.readObject();
+ // 读取赫夫曼编码表
+ Map<Byte, String> huffmanCodes = (Map<Byte, String>) ois.readObject();
+ HuffmanCode huffmanCode = new HuffmanCode();
+ // 解码
+ byte[] bytes = huffmanCode.decode(huffmanCodes, huffmanBytes);
+ // 将bytes 数组写入到目标文件
+ os = new FileOutputStream(dstFile);
+ // 写数据到 dstFile 文件
+ os.write(bytes);
+ } catch (Exception e) {
+ // TODO: handle exception
+ System.out.println(e.getMessage());
+ } finally {
+
+ try {
+ os.close();
+ ois.close();
+ is.close();
+ } catch (Exception e2) {
+ // TODO: handle exception
+ System.out.println(e2.getMessage());
+ }
+
+ }
+ }
+}
+
+class HuffmanCode {
+
+ private final Map<Byte, String> huffmanCodes = new HashMap<>();
+
+ public Map<Byte, String> getHuffmanCodes() {
+ return huffmanCodes;
+ }
+
+ // 生成 huffman 编码 压缩
+ public byte[] encode(byte[] bytes) {
+ List<Node> nodes = buildHuffmanNodes(bytes);
+ Node huffmanTreeRoot = buildHuffmanTree(nodes);
+ Map<Byte, String> huffmanCodes = buildHuffmanCodeTab(huffmanTreeRoot);
+ return zip(bytes, huffmanCodes);
+ }
+
+ // 将 huffman编码 解码 解压缩
+ public byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
+ StringBuilder stringBuilder = new StringBuilder();
+
+ // 将byte数组转成二进制的字符串
+ for (int i = 0; i < huffmanBytes.length - 1; i++) {
+ byte b = huffmanBytes[i];
+ String strToAppend = byteToBitString(b);
+ // 判断是不是最后一个字节
+ boolean isLastByte = (i == huffmanBytes.length - 2);
+ if (isLastByte) {
+ // 得到最后一个字节的有效位数
+ byte validBits = huffmanBytes[huffmanBytes.length - 1];
+ strToAppend = strToAppend.substring(0, validBits);
+ }
+ stringBuilder.append(strToAppend);
+ }
+
+ // 把字符串按照指定的赫夫曼编码进行解码
+ // 把赫夫曼编码表进行调换,因为反向查询 a->100 100->a
+ Map<String, Byte> map = new HashMap<>();
+ huffmanCodes.forEach((key, value) -> map.put(value, key));
+
+ // 创建要给集合,存放byte
+ List<Byte> list = new ArrayList<>();
+ // i 可以理解成就是索引,扫描 stringBuilder
+ for (int i = 0; i < stringBuilder.length(); ) {
+ int count = 1;
+ boolean flag = true;
+ Byte b = null;
+
+ while (flag) {
+ // 递增的取出 key
+ String key = stringBuilder.substring(i, i + count);
+ b = map.get(key);
+ if (b == null) {
+ // 没有匹配到
+ count++;
+ } else {
+ // 匹配到
+ flag = false;
+ }
+ }
+ list.add(b);
+ i += count;
+ }
+ byte[] b = new byte[list.size()];
+ IntStream.range(0, b.length).forEach(i -> b[i] = list.get(i));
+ return b;
+ }
+
+ // 计算字符串中每个字符出现的次数
+ private List<Node> buildHuffmanNodes(byte[] bytes) {
+ ArrayList<Node> nodes = new ArrayList<>();
+
+ // 利用map记录集合中元素出现的次数
+ Map<Byte, Integer> counts = new HashMap<>();
+ for (byte b : bytes) {
+ counts.merge(b, 1, Integer::sum);
+ }
+
+ // 把每一个键值对转成一个Node 对象,并加入到nodes集合
+ counts.forEach((key, value) -> nodes.add(new Node(key, value)));
+ return nodes;
+ }
+
+ // 构建Huffman树
+ private Node buildHuffmanTree(List<Node> nodes) {
+ while (nodes.size() > 1) {
+ // 排序, 从小到大
+ Collections.sort(nodes);
+ // 取出第一颗最小的二叉树
+ Node leftNode = nodes.get(0);
+ // 取出第二颗最小的二叉树
+ Node rightNode = nodes.get(1);
+ // 创建一颗新的二叉树,它的根节点 没有data, 只有权值
+ Node parent = new Node(null, leftNode.weight + rightNode.weight);
+ parent.left = leftNode;
+ parent.right = rightNode;
+
+ // 将已经处理的两颗二叉树从nodes删除
+ nodes.remove(leftNode);
+ nodes.remove(rightNode);
+ // 将新的二叉树,加入到nodes
+ nodes.add(parent);
+ }
+ // nodes 最后的结点,就是赫夫曼树的根结点
+ return nodes.get(0);
+ }
+
+ // 重载 getCodes
+ private Map<Byte, String> buildHuffmanCodeTab(Node root) {
+ if (root == null) {
+ return null;
+ }
+ // 处理root的左子树
+ buildHuffmanCodeTab(root.left, "0", new StringBuilder());
+ // 处理root的右子树
+ buildHuffmanCodeTab(root.right, "1", new StringBuilder());
+ return huffmanCodes;
+ }
+
+ // 获取huffman编码表
+ private void buildHuffmanCodeTab(Node node, String code, StringBuilder stringBuilder) {
+ StringBuilder curNodeCode = new StringBuilder(stringBuilder);
+ curNodeCode.append(code);
+ if (node == null) {
+ return;
+ }
+ // 判断当前node 是叶子结点还是非叶子结点
+ if (node.data == null) { // 非叶子结点
+ // 向左递归
+ buildHuffmanCodeTab(node.left, "0", curNodeCode);
+ // 向右递归
+ buildHuffmanCodeTab(node.right, "1", curNodeCode);
+ } else {
+ // 表示找到某个叶子结点的最后
+ huffmanCodes.put(node.data, curNodeCode.toString());
+ }
+ }
+
+ // 压缩传入字节(将传入字符串转成字节类型)将待压缩字节转换为字节数组
+ private byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
+ // 利用 huffmanCodes 将 bytes 转成 赫夫曼编码对应的字符串
+ StringBuilder stringBuilder = new StringBuilder();
+ // 遍历bytes 数组
+ for (byte b : bytes) {
+ stringBuilder.append(huffmanCodes.get(b));
+ }
+
+ // 统计返回 byte[] huffmanCodeBytes 长度
+ int len;
+ // 等同于 int len = (stringBuilder.length() + 7) / 8;
+ byte countToEight = (byte) (stringBuilder.length() & 7);
+ if (countToEight == 0) {
+ len = stringBuilder.length() >> 3;
+ } else {
+ len = (stringBuilder.length() >> 3) + 1;
+ // 后面补零
+ for (int i = countToEight; i < 8; i++) {
+ stringBuilder.append("0");
+ }
+ }
+
+ // 创建 存储压缩后的 byte数组,huffmanCodeBytes[len]记录赫夫曼编码最后一个字节的有效位数
+ byte[] huffmanCodeBytes = new byte[len + 1];
+ huffmanCodeBytes[len] = countToEight;
+ int index = 0;
+ // 因为是每8位对应一个byte,所以步长 +8
+ for (int i = 0; i < stringBuilder.length(); i += 8) {
+ String strByte;
+ strByte = stringBuilder.substring(i, i + 8);
+ // 将strByte 转成一个byte,放入到 huffmanCodeBytes
+ huffmanCodeBytes[index] = (byte) Integer.parseInt(strByte, 2);
+ index++;
+ }
+ return huffmanCodeBytes;
+ }
+
+ // 将 byte 转换为对应的字符串
+ private String byteToBitString(byte b) {
+ int temp = b;
+ // 如果是正数我们需要将高位补零
+ temp |= 0x100;
+ // 转换为二进制字符串,正数:高位补 0 即可,然后截取低八位即可;负数直接截取低八位即可
+ // 负数在计算机内存储的是补码,补码转原码:先 -1 ,再取反
+ String binaryStr = Integer.toBinaryString(temp);
+ return binaryStr.substring(binaryStr.length() - 8);
+ }
+
+}
+
+class Node implements Comparable<Node> {
+ Byte data;
+ int weight;
+ Node left;
+ Node right;
+
+ public Node(Byte data, int weight) {
+ this.data = data;
+ this.weight = weight;
+ }
+
+ @Override
+ public int compareTo(Node o) {
+ // 从小到大排序
+ return this.weight - o.weight;
+ }
+
+ public String toString() {
+ return "Node [data = " + data + " weight=" + weight + "]";
+ }
+}
+
分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)……
+分治法的设计思想是:将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。
+使用分治算饭解决汉诺塔问题
+++汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。
+
public class HanoiTowerDemo {
+ public static void main(String[] args) {
+ HanoiTower hanoiTower = new HanoiTower();
+ hanoiTower.hanoiTower(3, 'A', 'B', 'C');
+ }
+}
+
+class HanoiTower {
+
+ public void hanoiTower(int n, char a, char b, char c) {
+ if (n <= 0) {
+ return;
+ }
+ if (n == 1) {
+ System.out.println(a + "->" + c);
+ return;
+ }
+ // 将a塔上面除了底盘外的所有盘移动到b塔
+ hanoiTower(n - 1, a, c, b);
+
+ // 将a塔遗留的底盘移动到c塔
+ System.out.println(a + "->" + c);
+
+ // 将b塔上面的所有盘移动到c塔
+ hanoiTower(n - 1, b, a, c);
+ }
+
+}
+
动态规划(Dynamic Programming)算法的核心思想是:将大问题划分为小问题进行解决,从而一步步获取最优解的处理算法。
+动态规划算法与分治算法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。 ( 即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解 )
+关于动态规划最经典的问题当属背包问题。
+++背包问题主要是指一个给定容量的背包、若干具有一定价值和重量的物品,如何选择物品放入背包使物品的价值最大。其中又分01背包和完全背包(完全背包指的是:每种物品都有无限件可用)这里的问题属于01背包,即每个物品最多放一个。而无限背包可以转化为01背包。
++ +
++ + + +物品 +重量 +价格 ++ +吉他(G) +1 +1500 ++ +音响(S) +4 +3000 ++ + +电脑(L) +3 +2000 +
public class KnapsackProblemDemo {
+ public static void main(String[] args) {
+ KnapsackProblem knapsackProblem = new KnapsackProblem();
+ System.out.println(knapsackProblem.knapsackProblem());
+ }
+}
+
+class KnapsackProblem {
+
+ public int knapsackProblem() {
+ // 物品的重量
+ int[] w = {1, 4, 3};
+ // 物品的价值
+ int[] val = {1500, 3000, 2000};
+ // 背包的容量
+ int m = 4;
+ // 物品的个数
+ int n = val.length;
+ // 物品规划表
+ int[][] v = new int[n + 1][m + 1];
+
+ // 将v[][] 第一列和第一行重置为0
+ for (int i = 0; i < v.length; i++) {
+ v[i][0] = 0;
+ }
+ for (int i = 0; i < v[0].length; i++) {
+ v[0][i] = 0;
+ }
+
+ // 处理 生成物品价格表
+ for (int i = 1; i < v.length; i++) {
+ for (int j = 1; j < v[0].length; j++) {
+ // 如果当前商品的重量 是否能写入当前表格中
+ if (w[i - 1] > j) {
+ v[i][j] = v[i - 1][j];
+ } else {
+ v[i][j] = Math.max(v[i - 1][j], val[i - 1] + v[i - 1][j - w[i - 1]]);
+ }
+ }
+ }
+
+ // 处理完后 v[][] 表中数值最大的就是最后的结果
+ int max = 0;
+ for (int[] ints : v) {
+ System.out.println(Arrays.toString(ints));
+ for (int anInt : ints) {
+ if (anInt > max) {
+ max = anInt;
+ }
+ }
+ }
+ return max;
+ }
+}
+
++KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)。
+
常规算法匹配字符串
+ +从主串的起始位置(或指定位置)开始与模式串的第一个字符比较,若相等,则继续逐个比较后续字符;否则从主串的下一个字符再重新和模式串的字符比较。依次类推,直到模式串成功匹配,返回主串中第一次出现模式串字符的位置,或者模式串匹配不成功,这里约定返回-1。
+KMP算法匹配字符串
+ +主要就是改进了暴力匹配中i回溯的操作,KMP算法中当一趟匹配过程中出现字符比较不等时,不直接回溯i,而是利用已经得到的“部分匹配”的结果将模式串向右移动(j-next[j-1])的距离。
+public class KMPDemo {
+ public static void main(String[] args) {
+ KMP kmp = new KMP();
+ String str1 = "BBC ABCDAB ABCDABCDABDE";
+ String str2 = "ABCDABD";
+ int[] next = kmp.getMatchTab(str2);
+ System.out.println(Arrays.toString(next));
+ System.out.println(kmp.kmpSearch(str1, str2, next));
+ }
+}
+
+class KMP {
+
+ // 获取KMP 部分匹配表
+ public int[] getMatchTab(String dest) {
+ int[] result = new int[dest.length()];
+ // 部分匹配表第一个值始终为0
+ result[0] = 0;
+ for (int i = 1, j = 0; i < result.length; i++) {
+ // KMP 核心(特点,公式)
+ while (j > 0 && dest.charAt(i) != dest.charAt(j)) {
+ j = result[j - 1];
+ }
+ if (dest.charAt(j) == dest.charAt(i)) {
+ j++;
+ }
+ result[i] = j;
+ }
+ return result;
+ }
+
+ /**
+ * KMP查找算法
+ *
+ * @param str1 原字符串
+ * @param str2 子字符串
+ * @param next 部分匹配表
+ * @return 匹配到字符串的第一个索引位置
+ */
+ public int kmpSearch(String str1, String str2, int[] next) {
+ for (int i = 0, j = 0; i < str1.length(); i++) {
+ while (j > 0 && str1.charAt(i) != str2.charAt(j)) {
+ j = next[j - 1];
+ }
+ if (str1.charAt(i) == str2.charAt(j)) {
+ j++;
+ }
+ if (j == str2.length()) {
+ return i - j + 1;
+ }
+ }
+ return -1;
+ }
+
+}
+
贪心算法又称贪婪算法,是指在对问题进行求解时,在每一步选择中都采取最好或者最优(即最有利)的选择,从而希望能够导致结果是最好或者最优的算法。贪婪算法所得到的结果不一定是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果。
+举例,假设存在下面需要付费的广播台,以及广播台信号可以覆盖的地区。 如何选择最少的广播台,让所有的地区都可以接收到信号。
+广播台 | +覆盖地区 | +
---|---|
K1 | +“北京”, “上海”, “天津” | +
K2 | +“广州”, “北京”, “深圳” | +
K3 | +“成都”, “上海”, “杭州” | +
K4 | +“上海”, “天津” | +
K5 | +“杭州”, “大连” | +
public class GreedyAlgorithmDemo {
+ public static void main(String[] args) {
+ HashMap<String, HashSet<String>> broadcasts = new HashMap<>();
+ HashSet<String> hashSet1 = new HashSet<>();
+ hashSet1.add("北京");
+ hashSet1.add("上海");
+ hashSet1.add("天津");
+
+ HashSet<String> hashSet2 = new HashSet<>();
+ hashSet2.add("广州");
+ hashSet2.add("北京");
+ hashSet2.add("深圳");
+
+ HashSet<String> hashSet3 = new HashSet<>();
+ hashSet3.add("成都");
+ hashSet3.add("上海");
+ hashSet3.add("杭州");
+
+ HashSet<String> hashSet4 = new HashSet<>();
+ hashSet4.add("上海");
+ hashSet4.add("天津");
+
+ HashSet<String> hashSet5 = new HashSet<>();
+ hashSet5.add("杭州");
+ hashSet5.add("大连");
+
+ broadcasts.put("K1", hashSet1);
+ broadcasts.put("K2", hashSet2);
+ broadcasts.put("K3", hashSet3);
+ broadcasts.put("K4", hashSet4);
+ broadcasts.put("K5", hashSet5);
+
+ // allAreas 存放所有的地区
+ HashSet<String> allAreas = new HashSet<>();
+ for (Map.Entry<String, HashSet<String>> broadcast : broadcasts.entrySet()) {
+ allAreas.addAll(broadcast.getValue());
+ }
+
+ System.out.println(new GreedyAlgorithm().getRadioByGreedyAlgorithm(allAreas, broadcasts));
+ }
+}
+
+class GreedyAlgorithm {
+
+ public List<String> getRadioByGreedyAlgorithm(HashSet<String> allAreas, HashMap<String, HashSet<String>> broadcasts) {
+ // 存放选择的电台
+ ArrayList<String> selects = new ArrayList<>();
+
+ // 存放每次选择最优的电台
+ String maxKey = null;
+
+ // 临时集合 从 broadcasts 中选出能覆盖的电台,即存放 allAreas 与 broadcasts 的交集
+ HashSet<String> tmpSet = new HashSet<>();
+
+ while (allAreas.size() > 0) {
+ // 每次需要清空
+ maxKey = null;
+
+ for (String key : broadcasts.keySet()) {
+ tmpSet.clear();
+ tmpSet.addAll(broadcasts.get(key));
+
+ // 计算覆盖的电台 并赋值给tmpSet
+ tmpSet.retainAll(allAreas);
+
+ // 此处进行比较 体现贪心算法
+ if (tmpSet.size() > 0 && (maxKey == null || tmpSet.size() > broadcasts.get(maxKey).size())) {
+ maxKey = key;
+ }
+ }
+
+ // 每进行一次循环最后需要移除选中的maxKey对应的电台城市
+ if (maxKey != null) {
+ selects.add(maxKey);
+ allAreas.removeAll(broadcasts.get(maxKey));
+ }
+ }
+ return selects;
+ }
+
+}
+
普里姆算法(Prim算法),图论中的一种算法,可在加权连通图里搜索最小生成树。意即由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点,且其所有边的权值之和亦为最小。
+++最小生成树:给定一个带权的无向连通图,如何选取一棵生成树,使树上所有边上权的总和为最小,这叫最小生成树。简称MST。 +求最小生成树的算法主要是普里姆算法和克鲁斯卡尔算法。
++
+- 普里姆算法:O(n^2),适合稠密图(边多的图)
+- 克鲁斯卡尔算法:O,适合稀疏图(边少的图)
+
普利姆(Prim)算法求最小生成树,也就是在包含n个顶点的连通图中,找出只有(n-1)条边包含所有n个顶点的连通子图,也就是所谓的极小连通子图。
+ +public class PrimAlgorithmDemo {
+ public static void main(String[] args) {
+ char[] data = new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G'};
+ int vertex = data.length;
+ //邻接矩阵的关系使用二维数组表示,10000这个大数,表示两个点不联通
+ int[][] weight = new int[][]{
+ {10000, 5, 7, 10000, 10000, 10000, 2},
+ {5, 10000, 10000, 9, 10000, 10000, 3},
+ {7, 10000, 10000, 10000, 8, 10000, 10000},
+ {10000, 9, 10000, 10000, 10000, 4, 10000},
+ {10000, 10000, 8, 10000, 10000, 5, 4},
+ {10000, 10000, 10000, 4, 5, 10000, 6},
+ {2, 3, 10000, 10000, 4, 6, 10000}
+ };
+
+ MGraph graph = new MGraph(vertex);
+ PrimAlgorithm minTree = new PrimAlgorithm();
+ graph.create(graph, vertex, data, weight);
+ graph.show(graph);
+ minTree.prim(graph, 0);
+ }
+}
+
+class PrimAlgorithm {
+
+ /**
+ * 最小生成树问题 prim算法
+ *
+ * @param graph 图对象
+ * @param vertex 开始的顶点
+ */
+ public void prim(MGraph graph, int vertex) {
+ int i = 0, j = 0;
+ int row = -1, column = -1;
+ // 存放已经访问过的顶点
+ int[] visited = new int[graph.vertex];
+ // 用1表示两点之间已经连接, 0表示未连接
+ visited[vertex] = 1;
+ int minWeight = 10000;
+
+ for (int k = 1; k < graph.vertex; k++){
+ // 比较两点之间的权值,每次都获取最小的权值
+ for (i = 0; i < graph.vertex; i++) {
+ for(j = 0; j < graph.vertex; j++){
+ if (visited[i] == 1 && visited[j] == 0 && graph.weight[i][j] < minWeight){
+ minWeight = graph.weight[i][j];
+ row = i;
+ column = j;
+ }
+ }
+ }
+ System.out.println("边<" + graph.data[row] + "," + graph.data[column] + "> 权值:" + minWeight);
+ // 将顶点标记为已经访问过
+ visited[column] = 1;
+
+ // 每次比较完后需要将minWeight重置
+ minWeight = 10000;
+ }
+
+ }
+
+}
+
+class MGraph {
+ int vertex;
+ char[] data;
+ int[][] weight;
+
+ public MGraph(int vertex) {
+ this.vertex = vertex;
+ data = new char[vertex];
+ weight = new int[vertex][vertex];
+ }
+
+ /**
+ * 创建图的邻接矩阵
+ *
+ * @param graph 图对象
+ * @param vertex 图对应的顶点个数
+ * @param data 图的各个顶点的值
+ * @param weight 图的邻接矩阵
+ */
+ public void create(MGraph graph, int vertex, char[] data, int[][] weight) {
+ int i, j;
+ for (i = 0; i < vertex; i++) {
+ graph.data[i] = data[i];
+ for (j = 0; j < vertex; j++) {
+ graph.weight[i][j] = weight[i][j];
+ }
+ }
+ }
+
+ // 显示图的邻接矩阵
+ public void show(MGraph graph) {
+ for (int[] link : graph.weight) {
+ System.out.println(Arrays.toString(link));
+ }
+ }
+}
+
克鲁斯卡尔算法是求连通网的最小生成树的另一种方法。基本思想是, 将所有边按照权值的大小进行升序排序,然后从小到大一一判断,条件为:如果这个边不会与之前选择的所有边组成回路,就可以作为最小生成树的一部分;反之,舍去。 直到具有 n 个顶点的连通网筛选出来 n-1(n为顶点个数) 条边为止。
+++ +判断是否构成回路: 当每次需要将一条边添加到最小生成树时,判断该边的两个顶点终点是否相同,相同就会构成回路。
+关于终点的说明:就是将所有顶点按照从小到大的顺序排列好之后;某个顶点的终点就是与它连通的最大顶点。 就是将所有顶点按照从小到大的顺序排列好之后;某个顶点的终点就是与它连通的最大顶点。
+举例
++
+- 首先ABCDEFG这7个顶点,在顶点集合中是按照顺序存放的;
+- 第一次选择的是EF,毫无疑问这一条边的终点是F;
+- 第二次选择的CD的终点D;
+- 第三次选择的DE,终点是F,因为此时D和E相连,D又和F相连,所以D的终点是F。而且,因为C和D是相连的,D和E相连,E和F也是相连的,所以C的终点此时变成了F。也就是说,当选择了EF、CD、DE这三条边后,C、D、E的终点都是F。当然F的终点也是F,因为F还没和后面的哪个顶点连接。
+- 本来接下来应该选择CE的,但是由于C和E的终点都是F,所以就会形成回路;
+
public class KruskalCaseDemo {
+
+ //使用 INF 表示两个顶点不能连通
+ private static final int INF = Integer.MAX_VALUE;
+
+ public static void main(String[] args) {
+ char[] vertexs = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
+ int matrix[][] = {
+ /*A*//*B*//*C*//*D*//*E*//*F*//*G*/
+ /*A*/ {0, 12, INF, INF, INF, 16, 14},
+ /*B*/ {12, 0, 10, INF, INF, 7, INF},
+ /*C*/ {INF, 10, 0, 3, 5, 6, INF},
+ /*D*/ {INF, INF, 3, 0, 4, INF, INF},
+ /*E*/ {INF, INF, 5, 4, 0, 2, 8},
+ /*F*/ {16, 7, 6, INF, 2, 0, 9},
+ /*G*/ {14, INF, INF, INF, 8, 9, 0}};
+
+ KruskalCase kruskalCase = new KruskalCase(vertexs, matrix);
+ kruskalCase.print();
+ kruskalCase.kruskal();
+ }
+
+}
+
+class KruskalCase {
+ //使用 INF 表示两个顶点不能连通
+ private static final int INF = Integer.MAX_VALUE;
+ private int edgeNum; //边的个数
+ private char[] vertexs; //顶点数组
+ private int[][] matrix; //邻接矩阵
+
+ // 构造器
+ public KruskalCase(char[] vertexs, int[][] matrix) {
+ // 初始化顶点数和边的个数
+ int vlen = vertexs.length;
+
+ // 初始化顶点, 复制拷贝的方式
+ this.vertexs = new char[vlen];
+ for (int i = 0; i < vertexs.length; i++) {
+ this.vertexs[i] = vertexs[i];
+ }
+
+ // 初始化边, 使用的是复制拷贝的方式
+ this.matrix = new int[vlen][vlen];
+ for (int i = 0; i < vlen; i++) {
+ for (int j = 0; j < vlen; j++) {
+ this.matrix[i][j] = matrix[i][j];
+ }
+ }
+ // 统计边的条数
+ for (int i = 0; i < vlen; i++) {
+ for (int j = i + 1; j < vlen; j++) {
+ if (this.matrix[i][j] != INF) {
+ edgeNum++;
+ }
+ }
+ }
+ }
+
+ public void print() {
+ System.out.println("邻接矩阵为: \n");
+ for (int i = 0; i < vertexs.length; i++) {
+ for (int j = 0; j < vertexs.length; j++) {
+ System.out.printf("%12d", matrix[i][j]);
+ }
+ System.out.println();
+ }
+ }
+
+ /**
+ * 功能:对边进行排序处理, 冒泡排序
+ *
+ * @param edges 边的集合
+ */
+ private void sortEdges(EdgeData[] edges) {
+ for (int i = 0; i < edges.length - 1; i++) {
+ for (int j = 0; j < edges.length - 1 - i; j++) {
+ if (edges[j].weight > edges[j + 1].weight) {
+ EdgeData tmp = edges[j];
+ edges[j] = edges[j + 1];
+ edges[j + 1] = tmp;
+ }
+ }
+ }
+ }
+
+ /**
+ * @param ch 顶点的值,比如'A','B'
+ * @return 返回ch顶点对应的下标,如果找不到,返回-1
+ */
+ private int getPosition(char ch) {
+ for (int i = 0; i < vertexs.length; i++) {
+ if (vertexs[i] == ch) {
+ return i;
+ }
+ }
+ // 找不到,返回-1
+ return -1;
+ }
+
+ /**
+ * 功能: 获取图中边,放到EData[] 数组中,后面我们需要遍历该数组
+ * 是通过matrix 邻接矩阵来获取
+ * EData[] 形式 [['A','B', 12], ['B','F',7], .....]
+ */
+ private EdgeData[] getEdges() {
+ int index = 0;
+ EdgeData[] edges = new EdgeData[edgeNum];
+ for (int i = 0; i < vertexs.length; i++) {
+ for (int j = i + 1; j < vertexs.length; j++) {
+ if (matrix[i][j] != INF) {
+ edges[index++] = new EdgeData(vertexs[i], vertexs[j], matrix[i][j]);
+ }
+ }
+ }
+ return edges;
+ }
+
+ /**
+ * 功能: 获取下标为i的顶点的终点, 用于后面判断两个顶点的终点是否相同
+ *
+ * @param ends : 数组就是记录了各个顶点对应的终点是哪个,ends 数组是在遍历过程中,逐步形成
+ * @param i : 表示传入的顶点对应的下标
+ * @return 返回的就是 下标为i的这个顶点对应的终点的下标, 一会回头还有来理解
+ */
+ private int getEnd(int[] ends, int i) { // i = 4 [0,0,0,0,5,0,0,0,0,0,0,0]
+ while (ends[i] != 0) {
+ i = ends[i];
+ }
+ return i;
+ }
+
+ public void kruskal() {
+ int index = 0;
+ // 用于保存"已有最小生成树" 中的每个顶点在最小生成树中的终点
+ int[] ends = new int[vertexs.length];
+ // 创建结果数组, 保存最后的最小生成树
+ EdgeData[] rets = new EdgeData[edgeNum];
+
+ // 获取图中 所有的边的集合 , 一共有12边
+ EdgeData[] edges = getEdges();
+
+ // 按照边的权值大小进行排序(从小到大)
+ sortEdges(edges);
+
+ // 遍历edges 数组,将边添加到最小生成树中时,判断是准备加入的边否形成了回路,如果没有,就加入 rets, 否则不能加入
+ for (int i = 0; i < edgeNum; i++) {
+ // 获取到第i条边的第一个顶点(起点)
+ int point1 = getPosition(edges[i].start);
+ // 获取到第i条边的第2个顶点
+ int point2 = getPosition(edges[i].end);
+ // 获取p1这个顶点在已有最小生成树中的终点
+ int endPointOfPoint1 = getEnd(ends, point1);
+ // 获取p2这个顶点在已有最小生成树中的终点
+ int endPointOfPoint2 = getEnd(ends, point2);
+
+ // 克鲁斯卡尔核心点:判断是否构成回路
+ if (endPointOfPoint1 != endPointOfPoint2) {
+ // 假设没有构成回路
+ // 将该边上终点上的值赋值给起点位置的值
+ ends[endPointOfPoint1] = endPointOfPoint2;
+ // 将该边保存起来
+ rets[index++] = edges[i];
+ }
+ }
+ System.out.println("最小生成树为");
+ for (int i = 0; i < index; i++) {
+ System.out.println(rets[i]);
+ }
+ }
+
+}
+
+class EdgeData {
+ char start; // 边的一个点
+ char end; // 边的另外一个点
+ int weight; // 边的权值
+
+ public EdgeData(char start, char end, int weight) {
+ this.start = start;
+ this.end = end;
+ this.weight = weight;
+ }
+
+ // 重写toString, 便于输出边信息
+ @Override
+ public String toString() {
+ return "EData [<" + start + ", " + end + ">= " + weight + "]";
+ }
+}
+
迪杰斯特拉算法是典型最短路径算法,用于计算一个结点到其他结点的最短路径。迪杰斯特拉算法是基于贪心思想,从起始位置触发,每次寻找与起点位置距离且未访问过的顶点,以该顶点作为中间结点,更新从起点到其他顶点的距离,直到全部顶点都作为了中间结点,并完成了路径更新,算法结束。 它的主要特点是以起始点为中心向外层层扩展,即图的广度优先搜索思想,直到扩展到终点为止。
+视频讲解:bilibili
+ +public class DijkstraAlgorithmDemo {
+ public static void main(String[] args) {
+ char[] vertex = { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };
+ int[][] matrix = new int[vertex.length][vertex.length];
+ // 表示不可以连接
+ final int N = 65535;
+ matrix[0] = new int[] { N, 5, 7, N, N, N, 2 };
+ matrix[1] = new int[] { 5, N, N, 9, N, N, 3 };
+ matrix[2] = new int[] { 7, N, N, N, 8, N, N };
+ matrix[3] = new int[] { N, 9, N, N, N, 4, N };
+ matrix[4] = new int[] { N, N, 8, N, N, 5, 4 };
+ matrix[5] = new int[] { N, N, N, 4, 5, N, 6 };
+ matrix[6] = new int[] { 2, 3, N, N, 4, 6, N };
+ Graph graph = new Graph(vertex, matrix);
+ graph.showGraph();
+ graph.dsj(6);
+ }
+}
+
+class Graph {
+ private char[] vertex;
+ private int[][] matrix;
+ private VisitedVertex vv;
+
+ // 构造器
+ public Graph(char[] vertex, int[][] matrix) {
+ this.vertex = vertex;
+ this.matrix = matrix;
+ }
+
+ // 显示结果
+ public void showDijkstra() {
+ vv.showArrays();
+ }
+
+ // 显示图
+ public void showGraph() {
+ for (int[] link : matrix) {
+ for (int i : link) {
+ System.out.printf("%8d", i);
+ }
+ System.out.println();
+ }
+ }
+
+ /**
+ * 迪杰斯特拉算法实现
+ * @param index 表示出发顶点对应的下标
+ */
+ public void dsj(int index) {
+ vv = new VisitedVertex(vertex.length, index);
+ update(index);
+ vv.showArrays();
+ for (int j = 1; j < vertex.length; j++) {
+ index = vv.findNextStartPoint();
+ update(index);
+ vv.showArrays();
+ }
+ }
+
+ // 更新index下标顶点到周围顶点的距离和周围顶点的前驱顶点,
+ private void update(int index) {
+ int len = 0;
+ // 根据遍历我们的邻接矩阵的 matrix[index]行
+ for (int j = 0; j < matrix[index].length; j++) {
+ // len 含义是 : 出发顶点到index顶点的距离 + 从index顶点到j顶点的距离的和
+ len = vv.getDis(index) + matrix[index][j];
+ // 如果j顶点没有被访问过,并且 len 小于出发顶点到j顶点的距离,就需要更新
+ if (!vv.isVisited(j) && len < vv.getDis(j)) {
+ vv.updatePre(j, index);
+ vv.updateDis(j, len);
+ }
+ }
+ }
+}
+
+class VisitedVertex {
+ public int[] alreadyArr;
+ public int[] preVisited;
+ public int[] dis;
+
+ /**
+ *
+ * @param length :表示顶点的个数
+ * @param index: 出发顶点对应的下标, 比如G顶点,下标就是6
+ */
+ public VisitedVertex(int length, int index) {
+ this.alreadyArr = new int[length];
+ this.preVisited = new int[length];
+ this.dis = new int[length];
+ // 初始化 dis数组
+ Arrays.fill(dis, 65535);
+ this.dis[index] = 0;
+ this.alreadyArr[index] = 1;
+
+ }
+
+ /**
+ * 功能: 判断index顶点是否被访问过
+ * @return 如果访问过,就返回true, 否则访问false
+ */
+ public boolean isVisited(int index) {
+ return alreadyArr[index] == 1;
+ }
+
+ /**
+ * 功能: 更新出发顶点到index顶点的距离
+ */
+ public void updateDis(int index, int len) {
+ dis[index] = len;
+ }
+
+ /**
+ * 功能: 更新pre这个顶点的前驱顶点为index顶点
+ */
+ public void updatePre(int pre, int index) {
+ preVisited[pre] = index;
+ }
+
+ /**
+ * 功能:返回出发顶点到index顶点的距离
+ */
+ public int getDis(int index) {
+ return dis[index];
+ }
+
+
+ /**
+ * 继续选择并返回新的访问顶点, 比如这里的G 完后,就是 A点作为新的访问顶点(注意不是出发顶点)
+ */
+ public int findNextStartPoint() {
+ int min = 65535, index = 0;
+ for (int i = 0; i < alreadyArr.length; i++) {
+ if (alreadyArr[i] == 0 && dis[i] < min) {
+ min = dis[i];
+ index = i;
+ }
+ }
+ // 更新 index 顶点被访问过
+ alreadyArr[index] = 1;
+ return index;
+ }
+
+ //显示最后的结果
+ public void showArrays() {
+ System.out.println("核心数组的值如下:");
+ for (int i : alreadyArr) {
+ System.out.print(i + " ");
+ }
+ System.out.println();
+ for (int i : dis) {
+ System.out.print(i + " ");
+ }
+ System.out.println();
+ for (int i : preVisited) {
+ System.out.print(i + " ");
+ }
+ System.out.println();
+
+ char[] vertex = { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };
+ int count = 0;
+ for (int i : dis) {
+ if (i != 65535) {
+ System.out.print(vertex[count] + "(" + i + ") ");
+ } else {
+ System.out.print("N ");
+ }
+ count++;
+ }
+ System.out.println();
+ System.out.println();
+ }
+
+}
+
弗洛伊德算法又称为插点法,是一种利用动态规划的思想寻找给定的加权图中多源点之间最短路径的算法,与迪杰斯特拉算法类似。
+++迪杰斯特拉算法对比弗洛伊德算法: +迪杰斯特拉算法通过选定的被访问顶点,求出从出发访问顶点到其他顶点的最短路径;弗洛伊德算法中每一个顶点都是出发访问点,所以需要将每一个顶点看做被访问顶点,求出从每一个顶点到其他顶点的最短路径。
++
+- 弗洛伊德算法计算图中各个顶点之间的最短路径
+- 迪杰斯特拉算法用于计算图中某一个顶点到其他顶点的最短路径
+
public class FloydAlgorithmDemo {
+ public static void main(String[] args) {
+ char[] vertex = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
+ int[][] matrix = new int[vertex.length][vertex.length];
+ final int N = 65535;
+ matrix[0] = new int[]{0, 5, 7, N, N, N, 2};
+ matrix[1] = new int[]{5, 0, N, 9, N, N, 3};
+ matrix[2] = new int[]{7, N, 0, N, 8, N, N};
+ matrix[3] = new int[]{N, 9, N, 0, N, 4, N};
+ matrix[4] = new int[]{N, N, 8, N, 0, 5, 4};
+ matrix[5] = new int[]{N, N, N, 4, 5, 0, 6};
+ matrix[6] = new int[]{2, 3, N, N, 4, 6, 0};
+
+ FloydGraph graph = new FloydGraph(vertex, vertex.length, matrix);
+ graph.floyd();
+ graph.show();
+ }
+}
+
+class FloydGraph {
+ private char[] vertex;
+ private int[][] pre;
+ private int[][] dis;
+
+ public FloydGraph(char[] vertex, int length, int[][] dis) {
+ this.vertex = vertex;
+ this.dis = dis;
+ this.pre = new int[length][length];
+ for (int i = 0; i < length; i++) {
+ Arrays.fill(pre[i], i);
+ }
+ }
+
+ public void show() {
+ for (int k = 0; k < dis.length; k++) {
+ // 先将pre数组输出的一行
+ for (int i = 0; i < dis.length; i++) {
+ System.out.print(vertex[pre[k][i]] + " ");
+ }
+ System.out.println();
+ // 输出dis数组的一行数据
+ for (int i = 0; i < dis.length; i++) {
+ System.out.print("(" + vertex[k] + "到" + vertex[i] + "的最短路径是" + dis[k][i] + ") ");
+ }
+ System.out.println();
+ System.out.println();
+ }
+ }
+
+ public void floyd(){
+ int len;
+ for (int m = 0; m < dis.length; m++) {
+ for (int a = 0; a < dis.length; a++) {
+ for (int b = 0; b < dis.length; b++) {
+ len = dis[a][m] + dis[m][b];
+ if (len < dis[a][b]){
+ dis[a][b] = len;
+ pre[a][b] = pre[m][b];
+ }
+ }
+ }
+ }
+ }
+
+}
+
马踏棋盘算法也被称为骑士周游问题。将马随机放在国际象棋的8×8棋盘0~7的某个方格中,马按走棋规则(马走日字)进行移动。要求每个方格只进入一次,走遍棋盘上全部64个方格。
+马踏棋盘问题实际上是图的深度优先搜索(DFS)的应用。
+public class HouseChessBoardDemo {
+ public static void main(String[] args) {
+ HouseChessBoard houseChessBoard = new HouseChessBoard(7, 7, 2, 4);
+ houseChessBoard.showChessBoard();
+ }
+}
+
+class HouseChessBoard {
+
+ /**
+ * 表示棋盘的列
+ */
+ private int x;
+ /**
+ * 表示棋盘的行
+ */
+ private int y;
+ /**
+ * 创建一个数组,标记棋盘的各个位置是否被访问过,true表示已经访问过
+ */
+ private boolean visited[];
+ /**
+ * 使用一个属性,标记是否棋盘的所有位置都被访问 如果为true,表示成功
+ */
+ private boolean finished;
+
+ private int[][] chessboard;
+
+ public HouseChessBoard(int x, int y, int row, int column) {
+ this.x = x;
+ this.y = y;
+ this.visited = new boolean[x * y];
+ this.chessboard = new int[x][y];
+ traversalChess(this.chessboard, row-1, column-1, 1);
+ }
+
+ public void showChessBoard(){
+ for (int[] ints : this.chessboard) {
+ for (int anInt : ints) {
+ System.out.print(anInt + "\t");
+ }
+ System.out.println();
+ }
+ }
+
+ public void traversalChess(int[][] chessboard, int row, int column, int step) {
+ chessboard[row][column] = step;
+
+ // 将当前位置标记为已经访问过
+ visited[row * x + column] = true;
+
+ // 获取当前位置的下一个可走通的位置的集合
+ ArrayList<Point> nextPos = getNext(new Point(column, row));
+
+ sort(nextPos);
+
+ while (nextPos.size() > 0) {
+ // 获取当前可走通的位置
+ Point current = nextPos.remove(0);
+ // 判断当前该点是否被访问过,如果没有被访问过则继续向下访问
+ if (!visited[current.y * x + current.x]) {
+ traversalChess(chessboard, current.y, current.x, step + 1);
+ }
+ }
+
+ // 当遍历完可走的位置集合后,如果发现该路不通,则进行回溯,否则标记为完成
+ if (step < x * y && !finished) {
+ chessboard[row][column] = 0;
+ visited[row * x + column] = false;
+ } else {
+ finished = true;
+ }
+ }
+
+ // 将可走通路根据回溯次数进行从小到大排序
+ public void sort(ArrayList<Point> points){
+ points.sort((o1, o2) -> {
+ int next1 = getNext(o1).size();
+ int next2 = getNext(o2).size();
+ return next1 - next2;
+ });
+ }
+
+
+
+ // 获取下一个可走的位置
+ public ArrayList<Point> getNext(Point current) {
+ ArrayList<Point> ps = new ArrayList<Point>();
+ Point p1 = new Point();
+ // 表示马儿可以走5这个位置
+ if ((p1.x = current.x - 2) >= 0 && (p1.y = current.y - 1) >= 0) {
+ ps.add(new Point(p1));
+ }
+ // 判断马儿可以走6这个位置
+ if ((p1.x = current.x - 1) >= 0 && (p1.y = current.y - 2) >= 0) {
+ ps.add(new Point(p1));
+ }
+ // 判断马儿可以走7这个位置
+ if ((p1.x = current.x + 1) < x && (p1.y = current.y - 2) >= 0) {
+ ps.add(new Point(p1));
+ }
+ // 判断马儿可以走0这个位置
+ if ((p1.x = current.x + 2) < x && (p1.y = current.y - 1) >= 0) {
+ ps.add(new Point(p1));
+ }
+ // 判断马儿可以走1这个位置
+ if ((p1.x = current.x + 2) < x && (p1.y = current.y + 1) < y) {
+ ps.add(new Point(p1));
+ }
+ // 判断马儿可以走2这个位置
+ if ((p1.x = current.x + 1) < x && (p1.y = current.y + 2) < y) {
+ ps.add(new Point(p1));
+ }
+ // 判断马儿可以走3这个位置
+ if ((p1.x = current.x - 1) >= 0 && (p1.y = current.y + 2) < y) {
+ ps.add(new Point(p1));
+ }
+ // 判断马儿可以走4这个位置
+ if ((p1.x = current.x - 2) >= 0 && (p1.y = current.y + 1) < y) {
+ ps.add(new Point(p1));
+ }
+ return ps;
+ }
+
+}
+
+ +
+ + + + + +工欲善其事必先利其器,一个好的开发工具,能极大提高开发效率.
+一些实用的插件,能提高开发速度
+代码调试热部署插件,使用需要花钱; 破解教程供参考.
+启动完成需要改动代码调试,编译(快捷键: ctrl+b)一下即完成热部署,非常方便.
+ +中文语言包,对英文不太好的人很友好,根据使用习惯自行添加.
+ +现在几乎用mybatis-plus / mybatis-plus-join 取代了mybatis,所以该插件根据需要安装吧
+功能:
+该插件可替代mybatis-generator生成代码,且支持支持导入导出模板,由于集成到IDEA中使用更加方便,配置好模板(Velocity模板引擎)即可生成. 使用文档: https://gitee.com/makejava/EasyCode/wikis/pages
+ + +一款比较好用的翻译插件,可以使用快捷键 Ctrl+Shift+X 替换单词,从此妈妈再也不用担心变量方法命名的问题了.
+ +自动填充调用参数,一些方法的参数非常多,可以用这个插件提高效率,根据需要下载
+ +该插件适用于 Java 和 JavaScript 的 AI 更好地完成代码,与之相关的国产有一个AiXcoder Code Completer 都挺不错的.
+ +可以使你写的代码不至于太烂
+++ +对于Java代码规范,业界有统一的标准,不少公司对此都有一定的要求。但是即便如此,庞大的Java使用者由于经验很水平的限制,未必有规范编码的意识,而且即便经验丰富的老Java程序员也无法做到时刻将规范牢记于心。所以对于代码规范扫描工具,一经问世就广受青睐,阿里巴巴出品的Alibaba Java Coding Guidelines(阿里巴巴Java代码规约扫描,以下简称为AJCG)插件便是其中之一.
+
公司用Yapi作为前后端项目文档,那么使用该插件可以快速导入到yapi中,操作详情查看文档: https://easyyapi.com/documents/index.html
+ +一键生成一个对象的所有set,get方法,可赋默认值,支持链式调用
+ +该插件最主要作用就算规范git提交信息,方便统一管理生成release note,当然也可以在项目根目录建立模板文件(git config commit.template
)这种方式来进行规范,请根据具体使用场景来
GitToolBox是的git增强工具,能够帮你开始查看当前代码的提交记录。比如什么时间、谁提交的。对于快速查看代码提交记录是一款不错的工具
+可以当前编辑行的后面显示git记录,不想看可以取消,当然如果你觉得碍眼,可以不下载.请根据使用习惯来进行下载
+ +鼠标选中日志中打印的mybatis日志,右键选择 Sql Params Setter 自动将参数拼接到sql语句里,并复制到剪切板上.
+ +当你在IDEA里面使用鼠标的时候,如果这个鼠标操作是能够用快捷键替代的,那么Key Promoter X会弹出一个提示框,告知你这个鼠标操作可以用什么快捷键替代。对于想完全使用快捷键在IDEA的,这个插件就很有用。
+ +Maven是个很好用的依赖管理工具,但是再好的东西也不是完美的。Maven的依赖机制会导致Jar包的冲突。举个例子,现在你的项目中,使用了两个Jar包,分别是A和B。现在A需要依赖另一个Jar包C,B也需要依赖C。但是A依赖的C的版本是1.0,B依赖的C的版本是2.0。这时候,Maven会将这1.0的C和2.0的C都下载到你的项目中,这样你的项目中就存在了不同版本的C,这时Maven会依据依赖路径最短优先原则,来决定使用哪个版本的Jar包,而另一个无用的Jar包则未被使用,这就是所谓的依赖冲突。
+在大多数时候,依赖冲突可能并不会对系统造成什么异常,因为Maven始终选择了一个Jar包来使用。但是,不排除在某些特定条件下,会出现类似找不到类的异常,所以,只要存在依赖冲突,在我看来,最好还是解决掉,不要给系统留下隐患。
+解决依赖冲突的方法,就是使用Maven提供的
Maven Helper插件可以帮助我们分析依赖关系,从而解决依赖冲突。
+ +不同括号不同颜色,能增加代码可读性
+ +能将json转java对象,按住alt + s然后进行配置转换
+ +配置一些常用代码字母缩写,在输入简写时可以出现你预定义的固定模式的代码,使得开发效率大大提高,同时也可以增加个性化。例如: 输入 sout
会出现 System.out.println();
++本人Idea设置(windows版)供参考 下载
+
IDEA windows 版本常用快捷键如下:
+快捷键 | +介绍 | +
---|---|
Ctrl+Shift+V | +粘贴板列表 | +
Ctrl+G | +转到行&列 | +
Ctrl+F | +在当前文件进行文本查找 | +
Ctrl+Y | +删除光标所在行或删除选中的行 | +
Shift+Shift | +随处搜索,常用查找接口 | +
Ctrl+Shift+F | +按照文本的内容查找,行内搜索 | +
Ctrl+D | +复制当前行 | +
Alt+Enter | +代码提示补全 | +
Ctrl+Tab | +切换文件 | +
Alt+Insert | +代码自动生成 | +
Ctrl+Shift+L | +格式化代码 | +
Ctrl+Shift+R | +全局重命名 | +
Alt+鼠标左键选中 | +修改多行 | +
Ctrl+鼠标左键点击 | +快速找到成员变量的出处 | +
Ctrl+B | +编译(配合热部署插件使用) | +
Ctrl+Shift+F10 | +运行快捷键 | +
除此之外,根据自己的使用习惯,可以用[Key Promoter X](#Key Promoter X)插件来配置你自己的快捷键.
+在这个地方可以自己设置快捷键,如果你之前用的是eclipse,那么可以使用eclipse映射的快捷键,大大降低了学习成本 +
+一般写完接口,我们会使用Postman等其他测试接口工具来发起请求,看符不符合自己的预期. 这里不是在介绍Postman,而是介绍IDEA中的一个插件,它也能做到Postman的功能,而且由于集成到了idea中使开发效率大大增加.
+HTTP Client
是 IDEA 自带的一款简洁轻量级的接口调用插件,通过它,我们能在 IDEA 上开发、调试、测试Restful Web
服务.有了它 Postman 可以扔掉了
+ +
+配置maven项目骨架(模板),可以快速开发,可自定义项目模板,参考教程,maven骨架下载地址
+++Maven骨架简单的来说就是一种模型 (结构),Maven根据我们的不同的项目和需求,提供了不同的模型,这样就不需要我们自己建模型了。举个简单的例子:就比如我们要做一套普通的楼房,我们使用Maven就不需要我们自己打地基,直接把使用Maven打好的地基就可以了。同时种类的楼房(写字楼,商场,套房,别墅) 就有不同的地基,因此,Maven就有很多种模型。
+
配置maven骨架 +
+设置自动导入包,清除无用的包,使代码更加整洁
+ +在开发一些功能时需要的某些类库 https://www.21doc.net/ 这个网站做了一个导航供参考
++ +
+ + + + + +Docker 属于 Linux 容器的一种封装,提供简单易用的容器使用接口。它是目前流行的 Linux 容器解决方案。 +Docker 将应用程序与该程序的依赖,打包在一个文件里面。运行这个文件,就会生成一个虚拟容器。程序在这个虚拟容器里运行,就好像在真实的物理机上运行一样。有了 Docker,就不用担心环境问题。 +总体来说,Docker 的接口相当简单,用户可以方便地创建和使用容器,把自己的应用放入容器。 +容器还可以进行版本管理、复制、分享、修改,就像管理普通的代码一样。
+提供一次性的环境。比如,本地测试他人的软件、持续集成的时候提供单元测试和构建的环境。 +提供弹性的云服务。因为 Docker 容器可以随开随关,很适合动态扩容和缩容。 +组建微服务架构。通过多个容器,一台机器可以跑多个服务,因此在本机就可以模拟出微服务架构。
+以linux上安装为例
+1.安装依赖,docker依赖于系统的一些必要的工具
+yum install -y yum-utils device-mapper-persistent-data lvm2
+
2.添加软件源, 阿里云镜像(在阿里云镜像站上面可以找到docker-ce的软件源,使用国内的源速度比较快)
+yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
+
yum clean allyum makecache fastyum -y install docker-ce
+
4.启动服务
+ #service 命令的用法
+$ sudo service docker start
+
+#systemctl 命令的用法
+$ sudo systemctl start docker
+
5.查看安装版
+docker version
+
6.测试,检查 docker 是否正确安装并运行 hello-world 镜像
+docker run hello-world
+
7.Docker 需要用户具有 sudo 权限,为了避免每次命令都输入sudo,可以把用户加入 Docker 用户组
+sudo usermod -aG docker $USER
+
docker架构分为三部分客户端,宿主机,注册中心
+ +例如:输入 docker run mysql:5.6
命令,docker程序会从docker_host去找对应的镜像,
+如果mysql不存在则会去从register下载,下载完成后docker会自动给该镜像分配一个contains,mysql就会运行起来
以安装运行nginx
为例
// docker run 包括下载镜像(pull),创建容器(create),运行容器(start) 可用dock -h查看帮助
+// --rm 表明这是一个临时的容器,关闭的话会自动删除
+// --name 容器名称
+// -p 外部服务器端口映射docker容器端口
+docker run --rm --name myNginx -p 80:80 nginx:版本
+
+// 查看容器日志
+docker logs myNginx
+
+// 进入容器
+docker exec -it myNginx bash
+
// 查看当前运行的镜像
+docker ps
+
+//查看所有镜像
+docker ps -a
+
+//停止容器
+docker stop 容器名称
+
+// 删除镜像
+docker rmi 镜像名称
+
+//下载镜像 若不指定版本为最新版本
+docker pull 镜像:版本
+
+//查看当前本地仓库的镜像
+docker images
+
+//查看远程仓库镜像
+docker search 镜像名
+
+ +
+ + + + + +Elasticsearch,简称为 ES, ES是一个开源的高扩展的分布式全文搜索引擎, 是整个 ElasticStack 技术栈的核心。它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上百台服务器,处理 PB 级别的数据。
+++Elastic Stack, 包括 Elasticsearch、 Kibana、 Beats 和 Logstash(也称为 ELK Stack)。能够安全可靠地获取任何来源、任何格式的数据,然后实时地对数据进行搜索、分析和可视化。
+
Elasticsearch 是面向文档型数据库,一条数据在这里就是一个文档。 Elasticsearch 里存储文档数据和关系型数据库 MySQL 存储数据的概念进行一个类比,如图: +
+ES 里的 Index 可以看做一个库,而 Types 相当于表, Documents 则相当于表的行。这里 Types 的概念已经被逐渐弱化, Elasticsearch 6.X 中,一个 index 下已经只能包含一个type, Elasticsearch 7.X 中, Type 的概念已经被删除了。
+官网下载地址: https://www.elastic.co/cn/downloads/past-releases
+发送put请求
+http://127.0.0.1:9200/_indexname
+
发送get请求,查询单条
+http://127.0.0.1:9200/_indexname
+
查询所有索引,发送get请求
+http://127.0.0.1:9200/_cat/indices
+
发送delete请求
+http://127.0.0.1:9200/_indexname
+
发送post请求,第一次为创建,再一次发送为修改
+http://127.0.0.1:9200/_indexname/_docname/idstr
+
请求参数
+{
+ "title":"华为手机",
+ "category":"小米",
+ "images":"http://www.gulixueyuan.com/xm.jpg",
+ "price":3999.00
+}
+
发送post请求
+http://127.0.0.1:9200/_indexname/_update/idstr
+
请求参数
+{
+ "doc": {
+ "title":"小米手机",
+ "category":"小米"
+ }
+}
+
发送get请求,查询单条
+http://127.0.0.1:9200/_indexname/_docname/idstr
+
发送delete请求
+http://127.0.0.1:9200/_indexname/_docname/idstr
+
发送get请求,能看到全部数据
+http://127.0.0.1:9200/_indexname/_search
+
url带参查询,发get请求
+http://127.0.0.1:9200/_indexname/_search?q=category:小米
+
请求体带参查询,发送get请求
+http://127.0.0.1:9200/_indexname/_search
+
查询category是华为和小米的,price大于2000,只显示title,显示第一页,每页显示两个,根据price降序
+{
+ "query": {
+ "bool": {
+ "should": [{
+ "match": {
+ "category": "小米"
+ }
+ },
+ {
+ "match": {
+ "category": "华为"
+ }
+ }]
+ },
+ "filter": {
+ "range": {
+ "price": {
+ "gt": 2000
+ }
+ }
+ }
+ },
+ "_source": ["title"],
+ "from": 0,
+ "size": 2,
+ "sort": {
+ "price": {
+ "order": "desc"
+ }
+ }
+}
+
<dependency>
+ <groupId>org.elasticsearch.client</groupId>
+ <artifactId>elasticsearch-rest-high-level-client</artifactId>
+ <version>7.5.2</version>
+ </dependency>
+ <dependency>
+ <groupId>org.elasticsearch.client</groupId>
+ <artifactId>elasticsearch-rest-client</artifactId>
+ <version>7.5.2</version>
+ </dependency>
+ <dependency>
+ <groupId>org.elasticsearch</groupId>
+ <artifactId>elasticsearch</artifactId>
+ <version>7.5.2</version>
+ <exclusions>
+ <exclusion>
+ <groupId>org.elasticsearch.client</groupId>
+ <artifactId>elasticsearch-rest-client</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.elasticsearch</groupId>
+ <artifactId>elasticsearch</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
demo:
+ data:
+ elasticsearch:
+ cluster-name: elasticsearch
+ cluster-nodes: [127.0.0.1:1001,127.0.0.1:1002,127.0.0.1:1003]
+ index:
+ number-of-replicas: 0
+ number-of-shards: 3
+
/**
+ * ElasticsearchAutoConfiguration
+ *
+ * @since 2019-09-15 22:59
+ */
+@Configuration
+@RequiredArgsConstructor(onConstructor_ = @Autowired)
+@EnableConfigurationProperties(ElasticsearchProperties.class)
+public class ElasticsearchAutoConfiguration {
+
+ private final ElasticsearchProperties elasticsearchProperties;
+
+ private List<HttpHost> httpHosts = new ArrayList<>();
+
+ @Bean
+ @ConditionalOnMissingBean
+ public RestHighLevelClient restHighLevelClient() {
+
+ List<String> clusterNodes = elasticsearchProperties.getClusterNodes();
+ clusterNodes.forEach(node -> {
+ try {
+ String[] parts = StringUtils.split(node, ":");
+ Assert.notNull(parts, "Must defined");
+ Assert.state(parts.length == 2, "Must be defined as 'host:port'");
+ httpHosts.add(new HttpHost(parts[0], Integer.parseInt(parts[1]), elasticsearchProperties.getSchema()));
+ } catch (Exception e) {
+ throw new IllegalStateException("Invalid ES nodes " + "property '" + node + "'", e);
+ }
+ });
+ RestClientBuilder builder = RestClient.builder(httpHosts.toArray(new HttpHost[0]));
+
+ return getRestHighLevelClient(builder, elasticsearchProperties);
+ }
+
+
+ /**
+ * get restHistLevelClient
+ *
+ * @param builder RestClientBuilder
+ * @param elasticsearchProperties elasticsearch default properties
+ * @return {@link org.elasticsearch.client.RestHighLevelClient}
+ * @author fxbin
+ */
+ private static RestHighLevelClient getRestHighLevelClient(RestClientBuilder builder, ElasticsearchProperties elasticsearchProperties) {
+
+ // Callback used the default {@link RequestConfig} being set to the {@link CloseableHttpClient}
+ builder.setRequestConfigCallback(requestConfigBuilder -> {
+ requestConfigBuilder.setConnectTimeout(elasticsearchProperties.getConnectTimeout());
+ requestConfigBuilder.setSocketTimeout(elasticsearchProperties.getSocketTimeout());
+ requestConfigBuilder.setConnectionRequestTimeout(elasticsearchProperties.getConnectionRequestTimeout());
+ return requestConfigBuilder;
+ });
+
+ // Callback used to customize the {@link CloseableHttpClient} instance used by a {@link RestClient} instance.
+ builder.setHttpClientConfigCallback(httpClientBuilder -> {
+ httpClientBuilder.setMaxConnTotal(elasticsearchProperties.getMaxConnectTotal());
+ httpClientBuilder.setMaxConnPerRoute(elasticsearchProperties.getMaxConnectPerRoute());
+ return httpClientBuilder;
+ });
+
+ // Callback used the basic credential auth
+ ElasticsearchProperties.Account account = elasticsearchProperties.getAccount();
+ if (!StringUtils.isEmpty(account.getUsername()) && !StringUtils.isEmpty(account.getUsername())) {
+ final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
+
+ credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(account.getUsername(), account.getPassword()));
+ }
+ return new RestHighLevelClient(builder);
+ }
+
+}
+
/**
+ * ElasticsearchProperties
+ *
+ * @version v1.0
+ * @since 2019-09-15 22:58
+ */
+@Data
+@Builder
+@Component
+@NoArgsConstructor
+@AllArgsConstructor
+@ConfigurationProperties(prefix = "demo.data.elasticsearch")
+public class ElasticsearchProperties {
+
+ /**
+ * 请求协议
+ */
+ private String schema = "http";
+
+ /**
+ * 集群名称
+ */
+ private String clusterName = "elasticsearch";
+
+ /**
+ * 集群节点
+ */
+ @NotNull(message = "集群节点不允许为空")
+ private List<String> clusterNodes = new ArrayList<>();
+
+ /**
+ * 连接超时时间(毫秒)
+ */
+ private Integer connectTimeout = 1000;
+
+ /**
+ * socket 超时时间
+ */
+ private Integer socketTimeout = 30000;
+
+ /**
+ * 连接请求超时时间
+ */
+ private Integer connectionRequestTimeout = 500;
+
+ /**
+ * 每个路由的最大连接数量
+ */
+ private Integer maxConnectPerRoute = 10;
+
+ /**
+ * 最大连接总数量
+ */
+ private Integer maxConnectTotal = 30;
+
+ /**
+ * 索引配置信息
+ */
+ private Index index = new Index();
+
+ /**
+ * 认证账户
+ */
+ private Account account = new Account();
+
+ /**
+ * 索引配置信息
+ */
+ @Data
+ public static class Index {
+
+ /**
+ * 分片数量
+ */
+ private Integer numberOfShards = 3;
+
+ /**
+ * 副本数量
+ */
+ private Integer numberOfReplicas = 2;
+
+ }
+
+ /**
+ * 认证账户
+ */
+ @Data
+ public static class Account {
+
+ /**
+ * 认证用户
+ */
+ private String username;
+
+ /**
+ * 认证密码
+ */
+ private String password;
+
+ }
+
+}
+
/**
+ * ElasticsearchConstant
+ *
+ * @version v1.0
+ * @since 2019-09-15 23:03
+ */
+public interface ElasticsearchConstant {
+
+ /**
+ * 索引名称
+ */
+ String INDEX_NAME = "person";
+
+
+ /**
+ * 文档名称(字段名称)
+ */
+ String COLUMN_NAME_1 = "column_1";
+ String COLUMN_NAME_2 = "column_2";
+ String COLUMN_NAME_3 = "column_3";
+ String COLUMN_NAME_4 = "column_4";
+ String COLUMN_NAME_5 = "column_5";
+
+ /**
+ * 高亮标签
+ */
+ String TAG_HIGH_LIGHT_START = "<label style='color:red'>";
+ String TAG_HIGH_LIGHT_END = "</label>";
+
+}
+
/**
+ * Person
+ *
+ * @version v1.0
+ * @since 2019-09-15 23:04
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Person implements Serializable {
+
+ private static final long serialVersionUID = 8510634155374943623L;
+
+ /**
+ * 主键
+ */
+ private Long id;
+
+ /**
+ * 名字
+ */
+ private String name;
+
+ /**
+ * 国家
+ */
+ private String country;
+
+ /**
+ * 年龄
+ */
+ private Integer age;
+
+ /**
+ * 生日
+ */
+ private Date birthday;
+
+ /**
+ * 介绍
+ */
+ private String remark;
+
+}
+
/**
+ * @author: whitepure
+ * @date: 2023/1/6 11:25
+ * @description: SearchPageHelper
+ */
+@Data
+@Accessors(chain = true)
+public class SearchPageHelper<E> {
+
+ private Long current;
+
+ private Long pageSize;
+
+ private Long total;
+
+ private List<E> records;
+
+}
+
/**
+ * PersonService
+ *
+ * @version v1.0
+ * @since 2019-09-15 23:07
+ */
+public interface PersonService {
+
+ /**
+ * create Index
+ *
+ * @param index elasticsearch index name
+ * @author fxbin
+ */
+ void createIndex(String index);
+
+ /**
+ * delete Index
+ *
+ * @param index elasticsearch index name
+ * @author fxbin
+ */
+ void deleteIndex(String index);
+
+ /**
+ * insert document source
+ *
+ * @param index elasticsearch index name
+ * @param list data source
+ * @author fxbin
+ */
+ void insert(String index, List<Person> list);
+
+ /**
+ * update document source
+ *
+ * @param index elasticsearch index name
+ * @param list data source
+ * @author fxbin
+ */
+ void update(String index, List<Person> list);
+
+ /**
+ * delete document source
+ *
+ * @param person delete data source and allow null object
+ * @author fxbin
+ */
+ void delete(String index, @Nullable Person person);
+
+ /**
+ * search all doc records
+ *
+ * @param index elasticsearch index name
+ * @return person list
+ * @author fxbin
+ */
+ List<Person> searchList(String index);
+
+
+ /**
+ * 分页查询
+ *
+ * @param searchRequest search condition
+ * @return search list
+ */
+ SearchPageHelper<Person> searchPage(SearchRequest searchRequest);
+
+}
+
/**
+ * BaseElasticsearchService
+ *
+ * @version 1.0v
+ * @since 2019-09-16 15:44
+ */
+@Slf4j
+public abstract class BaseElasticsearchService {
+
+ @Resource
+ protected RestHighLevelClient client;
+
+ @Resource
+ private ElasticsearchProperties elasticsearchProperties;
+
+ protected static final RequestOptions COMMON_OPTIONS;
+
+ static {
+ RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
+
+ // 默认缓冲限制为100MB,此处修改为30MB。
+ builder.setHttpAsyncResponseConsumerFactory(new HttpAsyncResponseConsumerFactory.HeapBufferedResponseConsumerFactory(30 * 1024 * 1024));
+ COMMON_OPTIONS = builder.build();
+ }
+
+ /**
+ * create elasticsearch index (asyc)
+ *
+ * @param index elasticsearch index
+ * @author fxbin
+ */
+ protected void createIndexRequest(String index) {
+ try {
+ CreateIndexRequest request = new CreateIndexRequest(index);
+ // Settings for this index
+ request.settings(Settings.builder().put("index.number_of_shards", elasticsearchProperties.getIndex().getNumberOfShards()).put("index.number_of_replicas", elasticsearchProperties.getIndex().getNumberOfReplicas()));
+
+ CreateIndexResponse createIndexResponse = client.indices().create(request, COMMON_OPTIONS);
+
+ log.info(" whether all of the nodes have acknowledged the request : {}", createIndexResponse.isAcknowledged());
+ log.info(" Indicates whether the requisite number of shard copies were started for each shard in the index before timing out :{}", createIndexResponse.isShardsAcknowledged());
+ } catch (IOException e) {
+ throw new ElasticsearchException("创建索引 {" + index + "} 失败");
+ }
+ }
+
+ /**
+ * delete elasticsearch index
+ *
+ * @param index elasticsearch index name
+ * @author fxbin
+ */
+ protected void deleteIndexRequest(String index) {
+ DeleteIndexRequest deleteIndexRequest = buildDeleteIndexRequest(index);
+ try {
+ client.indices().delete(deleteIndexRequest, COMMON_OPTIONS);
+ } catch (IOException e) {
+ throw new ElasticsearchException("删除索引 {" + index + "} 失败");
+ }
+ }
+
+ /**
+ * build DeleteIndexRequest
+ *
+ * @param index elasticsearch index name
+ * @author fxbin
+ */
+ private static DeleteIndexRequest buildDeleteIndexRequest(String index) {
+ return new DeleteIndexRequest(index);
+ }
+
+ /**
+ * build IndexRequest
+ *
+ * @param index elasticsearch index name
+ * @param id request object id
+ * @param object request object
+ * @return {@link org.elasticsearch.action.index.IndexRequest}
+ * @author fxbin
+ */
+ protected static IndexRequest buildIndexRequest(String index, String id, Object object) {
+ return new IndexRequest(index).id(id).source(BeanUtil.beanToMap(object), XContentType.JSON);
+ }
+
+ /**
+ * exec updateRequest
+ *
+ * @param index elasticsearch index name
+ * @param id Document id
+ * @param object request object
+ * @author fxbin
+ */
+ protected void updateRequest(String index, String id, Object object) {
+ try {
+ UpdateRequest updateRequest = new UpdateRequest(index, id).doc(BeanUtil.beanToMap(object), XContentType.JSON);
+ client.update(updateRequest, COMMON_OPTIONS);
+ } catch (IOException e) {
+ throw new ElasticsearchException("更新索引 {" + index + "} 数据 {" + object + "} 失败");
+ }
+ }
+
+ /**
+ * exec deleteRequest
+ *
+ * @param index elasticsearch index name
+ * @param id Document id
+ * @author fxbin
+ */
+ protected void deleteRequest(String index, String id) {
+ try {
+ DeleteRequest deleteRequest = new DeleteRequest(index, id);
+ client.delete(deleteRequest, COMMON_OPTIONS);
+ } catch (IOException e) {
+ throw new ElasticsearchException("删除索引 {" + index + "} 数据id {" + id + "} 失败");
+ }
+ }
+
+ /**
+ * 查询全部
+ *
+ * @param indices elasticsearch 索引名称
+ * @return {@link SearchResponse}
+ */
+ protected SearchResponse search(String... indices) {
+ SearchRequest searchRequest = new SearchRequest(indices);
+ SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
+ searchSourceBuilder.query(QueryBuilders.matchAllQuery());
+ searchRequest.source(searchSourceBuilder);
+ return search(searchRequest);
+ }
+
+
+ /**
+ * 查询全部
+ *
+ * @param searchRequest 查询条件
+ * @return {@link SearchResponse}
+ */
+ protected SearchResponse search(SearchRequest searchRequest) {
+ SearchResponse searchResponse;
+ try {
+ searchResponse = client.search(searchRequest, COMMON_OPTIONS);
+ } catch (IOException e) {
+ throw new org.elasticsearch.ElasticsearchException("查询索引 %s 失败" , e, Arrays.toString(Arrays.stream(searchRequest.indices()).toArray()));
+ }
+ return searchResponse;
+ }
+}
+
/**
+ * PersonServiceImpl
+ *
+ * @version v1.0
+ * @since 2019-09-15 23:08
+ */
+@Service
+public class PersonServiceImpl extends BaseElasticsearchService implements PersonService {
+
+ @Override
+ public void createIndex(String index) {
+ createIndexRequest(index);
+ }
+
+ @Override
+ public void deleteIndex(String index) {
+ deleteIndexRequest(index);
+ }
+
+ @SneakyThrows
+ @Override
+ public void insert(String index, List<Person> list) {
+ for (Person person : list) {
+ IndexRequest request = buildIndexRequest(index, String.valueOf(person.getId()), person);
+ client.index(request, COMMON_OPTIONS);
+ }
+ }
+
+ @Override
+ public void update(String index, List<Person> list) {
+ list.forEach(person -> updateRequest(index, String.valueOf(person.getId()), person));
+ }
+
+ @Override
+ public void delete(String index, Person person) {
+ if (ObjectUtils.isEmpty(person)) {
+ // 如果person 对象为空,则删除全量
+ searchList(index).forEach(p -> {
+ deleteRequest(index, String.valueOf(p.getId()));
+ });
+ }
+ deleteRequest(index, String.valueOf(person.getId()));
+ }
+
+ @Override
+ public List<Person> searchList(String index) {
+ return toSearchList(search(index));
+ }
+
+
+ @Override
+ public SearchPageHelper<Person> searchPage(SearchRequest searchRequest) {
+ SearchResponse searchResponse = search(searchRequest);
+ TotalHits totalHits = searchResponse.getHits().getTotalHits();
+ return new SearchPageHelper<Person>()
+ .setTotal(totalHits == null ? 0 : totalHits.value)
+ .setRecords(toSearchList(searchResponse))
+ .setPageSize(20L);
+ }
+
+
+ private List<Person> toSearchList(SearchResponse searchResponse) {
+ SearchHit[] hits = searchResponse.getHits().getHits();
+ List<Person> searchList = new ArrayList<>();
+
+ Arrays.stream(hits).forEach(hit -> {
+ Map<String, Object> sourceAsMap = hit.getSourceAsMap();
+ // 处理高亮数据
+ Map<String, HighlightField> highlightFields = hit.getHighlightFields();
+ highlightFields.forEach((k, v) -> {
+ if (v != null && v.getFragments().length > 0) {
+ sourceAsMap.put(k, StrUtil.strip(Arrays.toString(v.getFragments()), "[]"));
+ }
+ }
+ );
+ Person search = BeanUtil.mapToBean(sourceAsMap, Person.class, true);
+ searchList.add(search);
+ });
+ return searchList;
+ }
+
+}
+
@RunWith(SpringRunner.class)
+@SpringBootTest
+public class ElasticsearchApplicationTests {
+
+ @Autowired
+ private PersonService personService;
+
+ /**
+ * 测试删除索引
+ */
+ @Test
+ public void deleteIndexTest() {
+ personService.deleteIndex(ElasticsearchConstant.INDEX_NAME);
+ }
+
+ /**
+ * 测试创建索引
+ */
+ @Test
+ public void createIndexTest() {
+ personService.createIndex(ElasticsearchConstant.INDEX_NAME);
+ }
+
+ /**
+ * 测试新增
+ */
+ @Test
+ public void insertTest() {
+ List<Person> list = new ArrayList<>();
+ list.add(Person.builder().age(11).birthday(new Date()).country("CN").id(1L).name("哈哈").remark("test1").build());
+ list.add(Person.builder().age(22).birthday(new Date()).country("US").id(2L).name("hiahia").remark("test2").build());
+ list.add(Person.builder().age(33).birthday(new Date()).country("ID").id(3L).name("呵呵").remark("test3").build());
+
+ personService.insert(ElasticsearchConstant.INDEX_NAME, list);
+ }
+
+ /**
+ * 测试更新
+ */
+ @Test
+ public void updateTest() {
+ Person person = Person.builder().age(33).birthday(new Date()).country("ID_update").id(3L).name("呵呵update").remark("test3_update").build();
+ List<Person> list = new ArrayList<>();
+ list.add(person);
+ personService.update(ElasticsearchConstant.INDEX_NAME, list);
+ }
+
+ /**
+ * 测试删除
+ */
+ @Test
+ public void deleteTest() {
+ personService.delete(ElasticsearchConstant.INDEX_NAME, Person.builder().id(1L).build());
+ }
+
+ /**
+ * 测试查询
+ */
+ @Test
+ public void searchListTest() {
+ List<Person> personList = personService.searchList(ElasticsearchConstant.INDEX_NAME);
+ System.out.println(personList);
+ }
+
+
+ /**
+ * 测试分页查询
+ */
+ @Test
+ public void searchPageTest(){
+ int current = 1;
+ int pageSize = 20;
+ int maxCurrent = 500;
+
+ // 此处最大页数设置为500 为es浅分页; 如需深度分页需更换分页方法并移除此条件
+ if (current > maxCurrent) {
+ return;
+ }
+
+ // 构造查询条件
+ SearchRequest searchRequest = new SearchRequest(ElasticsearchConstant.INDEX_NAME);
+ searchRequest.source(new SearchSourceBuilder()
+ .trackTotalHits(true)
+ // 查询条件
+ .query(
+ QueryBuilders.boolQuery()
+ // and
+ .must(QueryBuilders.termQuery(ElasticsearchConstant.COLUMN_NAME_2, true))
+ // or
+ .must(
+ QueryBuilders.boolQuery()
+ .should(QueryBuilders.matchQuery(ElasticsearchConstant.COLUMN_NAME_1, 1))
+ .should(QueryBuilders.matchQuery(ElasticsearchConstant.COLUMN_NAME_2, 2))
+ .should(QueryBuilders.matchQuery(ElasticsearchConstant.COLUMN_NAME_3, 3))
+ )
+ )
+ // 分页
+ .from((current - 1) * pageSize)
+ .size(pageSize)
+ // 相关度排序: SortBuilders.scoreSort()
+ .sort(
+ // 字段排序 可根据时间
+ SortBuilders
+ .fieldSort(ElasticsearchConstant.COLUMN_NAME_2)
+ .order(SortOrder.DESC)
+ )
+ // 高亮字段
+ .highlighter(new HighlightBuilder()
+ .requireFieldMatch(true)
+ .preTags(ElasticsearchConstant.TAG_HIGH_LIGHT_START)
+ .field(ElasticsearchConstant.COLUMN_NAME_1)
+ .field(ElasticsearchConstant.COLUMN_NAME_2)
+ .field(ElasticsearchConstant.COLUMN_NAME_3)
+ .postTags(ElasticsearchConstant.TAG_HIGH_LIGHT_END)
+ ));
+
+ SearchPageHelper<Person> personSearchPageHelper = personService.searchPage(searchRequest);
+ System.out.println(personSearchPageHelper);
+ }
+
+}
+
一个运行中的 Elasticsearch 实例称为一个节点,而集群是由一个或者多个拥有相同 cluster.name 配置的节点组成, 它们共同承担数据和负载的压力。 当有节点加入集群中或者从集群中移除节点时,集群将会重新平均分布所有的数据。
+当一个节点被选举成为主节点时, 它将负责管理集群范围内的所有变更,例如增加、 删除索引,或者增加、删除节点等。 而主节点并不需要涉及到文档级别的变更和搜索等操作,所以当集群只拥有一个主节点的情况下,即使流量的增加它也不会成为瓶颈。 任何节点都可以成为主节点。我们的示例集群就只有一个节点,所以它同时也成为了主节点。
+作为用户,我们可以将请求发送到集群中的任何节点 ,包括主节点。 每个节点都知道任意文档所处的位置,并且能够将我们的请求直接转发到存储我们所需文档的节点。 无论我们将请求发送到哪个节点,它都能负责从各个包含我们所需文档的节点收集回数据,并将最终结果返回给客户端。
+创建 elasticsearch-cluster 文件夹,在内部复制三个 elasticsearch 服务。 +
+修改节点配置; config/elasticsearch.yml 文件
+#集群名称,节点之间要保持一致
+cluster.name: my-elasticsearch
+#节点名称,集群内要唯一
+node.name: node-1001
+node.master: true
+node.data: true
+#ip 地址
+network.host: localhost
+#http 端口
+http.port: 1001
+#tcp 监听端口
+transport.tcp.port: 9301
+#discovery.seed_hosts: ["localhost:9301", "localhost:9302","localhost:9303"]
+#discovery.zen.fd.ping_timeout: 1m
+#discovery.zen.fd.ping_retries: 5
+#集群内的可以被选为主节点的节点列表
+#cluster.initial_master_nodes: ["node-1", "node-2","node-3"]
+#跨域配置
+#action.destructive_requires_name: true
+http.cors.enabled: true
+http.cors.allow-origin: "*"
+
#集群名称,节点之间要保持一致
+ cluster.name: my-elasticsearch
+ #节点名称,集群内要唯一
+ node.name: node-1002
+ node.master: true
+ node.data: true
+ #ip 地址
+ network.host: localhost
+ #http 端口
+ http.port: 1002
+ #tcp 监听端口
+ transport.tcp.port: 9302
+ discovery.seed_hosts: ["localhost:9301"]
+ discovery.zen.fd.ping_timeout: 1m
+ discovery.zen.fd.ping_retries: 5
+ #集群内的可以被选为主节点的节点列表
+ #cluster.initial_master_nodes: ["node-1", "node-2","node-3"]
+ #跨域配置
+ #action.destructive_requires_name: true
+ http.cors.enabled: true
+ http.cors.allow-origin: "*"
+
#集群名称,节点之间要保持一致
+cluster.name: my-elasticsearch
+#节点名称,集群内要唯一
+node.name: node-1003
+node.master: true
+node.data: true
+#ip 地址
+network.host: localhost
+#http 端口
+http.port: 1003
+#tcp 监听端口
+transport.tcp.port: 9303
+#候选主节点的地址,在开启服务后可以被选为主节点
+discovery.seed_hosts: ["localhost:9301", "localhost:9302"]
+discovery.zen.fd.ping_timeout: 1m
+discovery.zen.fd.ping_retries: 5
+#集群内的可以被选为主节点的节点列表
+#cluster.initial_master_nodes: ["node-1", "node-2","node-3"]
+#跨域配置
+#action.destructive_requires_name: true
+http.cors.enabled: true
+http.cors.allow-origin: "*"
+
启动集群; 点击 bin\elasticsearch.bat +
+如果启动不起来可能原因是分配内存不足,需要修改 config\jvm.options 文件中的内存属性
+启动之后使用ES可视化工具查看,可使用elasticsearch-head,ElasticHD
+ElasticSearch 设计的理念就是分布式搜索引擎,底层其实还是基于 lucene 的。核心思想就是在多台机器上启动多个 ES 进程实例,组成了一个 ES 集群。
+ES分布式架构实际上就是对index的拆分,将index拆分成多个分片(shard),将分片分别放到不同的ES上实现集群部署.
+ +分片优点:
+分片的数据实际上是有多个备份存在的,会存在一个主分片,还有几个副本分片. 当写入数据的时候先写入主分片,然后并行将数据同步到副本分片上;当读数据的时候会获取到所有分片,负载均衡轮询读取.
+当某个节点宕机了,还有其他分片副本保存在其他的机器上,从而实现了高可用. 如果是非主节点宕机了,那么会由主节点,让那个宕机节点上的主分片的数据转移到其他机器上的副本数据。接着你要是修复了那个宕机机器,重启了之后,主节点会控制将缺失的副本数据分配过去,同步后续修改的数据之类的,让集群恢复正常. 如果是主节点宕机,那么会重新选举一个节点为主节点.
+在一个网络环境里,失败随时都可能发生,在某个分片/节点不知怎么的就处于离线状态,或者由于任何原因消失了,这种情况下,有一个故障转移机制是非常有用并且是强烈推荐的。为此目的,Elasticsearch 允许你创建分片的一份或多份拷贝,这些拷贝叫做复制分片。
+当集群中只有一个节点在运行时,意味着会有一个单点故障问题——没有冗余。 幸运的是,我们只需再启动一个节点即可防止数据丢失。当你在同一台机器上启动了第二个节点时,只要它和第一个节点有同样的 cluster.name 配置,它就会自动发现集群并加入到其中。
+ES最好部署3个以上的节点,并且配置仲裁数大于一半节点,防止master选举的脑裂问题。
+++关于master的选举: +主要是由ZenDiscovery模块负责,包含Ping(节点之间通过这个RPC来发现彼此)和Unicast(单播模块包含-一个主机列表以控制哪些节点需要ping通)这两部分. +首先对所有可以成为master的节点(可以配置)根据nodeId排序,每次选举每个节点都把自己所知道节点排一次序,然后选出第一个,暂且认为它是master节点 +如果对某个节点的投票数达到一定的值(可以成为master节点数n/2+1)并且该节点自己也选举自己,那这个节点就是master。否则重新选举一直到满足上述条件
+
++ES在主节点上产生分歧,产生多个主节点,从而使集群分裂,使得集群处于异常状态。这个现象叫做脑裂。脑裂问题其实就是同一个集群的不同节点对于整个集群的状态有不同的理解,导致操作错乱,类似于精神分裂。
+
当写入一个文档的时候,文档会被存储到一个主分片中。 Elasticsearch 集群如何知道一个文档应该存放到哪个分片中呢?
+Elasticsearch 集群路由计算公式:
+shard = hash(routing) % number_of_primary_shards
+
routing 是一个可变值,默认是文档的 _id ,也可以设置成一个自定义的值。 routing 通过hash 函数生成一个数字,然后这个数字再除以 number_of_primary_shards (主分片的数量)后得到余数 。这个分布在 0 到 number_of_primary_shards-1 之间的余数,就是我们所寻求的文档所在分片的位置。
+这也就是创建索引的时候主分片的数量永远也不会改变的原因,如果数量变化了,那么所有之前路由的值都会无效,文档也再也找不到了.
+用户可以访问任何一个节点获取数据,因为存放的规则一致(副本和主分片存放的数据一致),这个节点称之为协调节点.如果当前节点访问量较大可能被转到其他节点上,所以当发送请求的时候,为了扩展负载,更好的做法是轮询集群中所有的节点.
+在客户端收到成功响应时,文档变更已经在主分片和所有副本分片执行完成,变更是安全的。有一些可选的请求参数允许您影响这个过程,可能以数据安全为代价提升性能。
+设置 consistency 参数值会影响写入操作.consistency 参数的值可以设为:
+当consistency值设置为quorum时,如果没有足够的副本分片Elasticsearch 会等待.默认情况下,它最多等待 1 分钟,可以使用timeout参数使它更早终止.
+在处理读取请求时,协调结点在每次请求的时候都会通过轮询所有的副本分片来达到负载均衡。在文档被检索时,已经保存的数据可能已经存在于主分片上但是还没有复制到副本分片。 在这种情况下,副本分片可能会报告文档不存在,但是主分片可能成功返回文档。 一旦索引请求成功返回给用户,文档在主分片和副本分片都是可用的。
+在步骤4中,主分片把更改转发到副本分片时, 它不会转发更新请求。 相反,它转发完整文档的新版本。请记住,这些更改将会异步转发到副本分片,并且不能保证它们以发送它们相同的顺序到达。 如果 Elasticsearch 仅转发更改请求,则可能以错误的顺序应用更改,导致得到损坏的文档。
+分片是Elasticsearch最小的工作单元。传统的数据库每个字段存储单个值,但这对全文检索并不够。文本字段中的每个单词需要被搜索,对数据库意味着需要单个字段有索引多值的能力。最好的支持是一个字段多个值需求的数据结构是倒排索引。
+Elasticsearch 使用一种称为 倒排索引 的结构,它适用于快速的全文搜索。一个倒排索引由文档中所有不重复词的列表构成,对于其中每个词,有一个包含它的文档列表。 倒排索引(Inverted Index)也叫反向索引,有反向索引必有正向索引。通俗地来讲,正向索引是通过key找value,反向索引则是通过value找key。
+倒排索引示例:
+| value | key |
+|----------|-------------------|
+| my name is zhangsan | 1001 |
+
| key | value|
+|---------|------|
+| name | 1001 |
+| zhang | 1001 |
+| zhangsan| 1001 |
+
倒排索引搜索过程: 查询单词是否在词典中,如果不在搜索结束,如果在词典中需要查询单词在倒排列表中的指针,获取单词对应的文档ID,根据文档ID查询时哪一条数据
+词条: 索引中最小的存储和查询单元 +词典: 词条的集合;一般用hash表或B+tree存储 +倒排表: 记录了出现过某个单词的所有文档的文档列表及单词在该文档中出现的位置信息,每条记录称为一个倒排项.根据倒排列表,即可获知哪些文档包含某个单词.
+倒排索引总是和分词分不开的,中文分词和英文分词是不一样的,所以就需要分析器.
+分析器的主要功能是将一块文本分成适合于倒排索引的独立词条,分析器组成:
+Elasticsearch附带了可以直接使用的预包装的分析器:
+常用中文分词器: ik分词器, 将解压后的后的文件夹放入 ES 根目录下的 plugins 目录下,重启 ES 即可使用.
+早期的全文检索会为整个文档集合建立一个很大的倒排索引并将其写入到磁盘。 被写入的索引不可变化,一旦新的索引就绪,旧的就会被其替换.如果你需要让一个新的文档可被搜索,你需要重建整个索引。这要么对一个索引所能包含的数据量造成了很大的限制,要么对索引可被更新的频率造成了很大的限制。
+如何在保留不变性的前提下实现倒排索引的更新?
+用更多的索引。通过增加新的补充索引来反映新的修改,而不是直接重写整个倒排索引。每一个倒排索引都会被轮流查询到,从最早的开始查询完后再对结果进行合并。
+当一个文档被删除时,它实际上只是在文件中被标记删除。一个被标记删除的文档仍然可以被查询匹配到,但它会在最终结果被返回前从结果集中过滤掉。 文档更新也是类似的操作方式:当一个文档被更新时,旧版本文档被标记删除,文档的新版本被索引到一个新的段中。可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就已经被移除。 +当一个查询被触发,所有已知的段按顺序被查询。词项统计会对所有段的结果进行聚合,此时会将标记删除的数据真正的删除.
+Elasticsearch 的主要功能就是搜索,但是Elasticsearch的搜索功能不是实时的,而是近实时的,主要原因在于ES搜索是分段搜索.
+ES中的每一段就是一个倒排索引,最新的数据更新会体现在最新的段中,而最新的段落盘之后ES才能进行搜索,所以磁盘性能极大影响了ES软件的搜索.ES的主要作用就是快速准确的获取想要的数据,所以降低处理数据的延迟就显得尤为重要.
+ES近实时搜索实现: +
+Elasticsearch 在数据量很大的情况下(数十亿级别)如何提高查询效率?
+分片和副本的设计为 ES 提供了支持分布式和故障转移的特性,但并不意味着分片和副本是可以无限分配的。而且索引的分片完成分配后由于索引的路由机制,我们是不能重新修改分片数的.否则将无法找到对应的数据.
+往 ES 里写的数据,实际上都写到磁盘文件里去了,查询的时候,操作系统会将磁盘文件里的数据自动缓存到 filesystem cache 里面去;ES 的搜索引擎严重依赖于底层的 filesystem cache ,你如果给 filesystem cache 更多的内存,尽量让内存可以容纳所有的 idx segment file 索引数据文件,那么你搜索的时候就基本都是走内存的,性能会非常高。
+++案例: 某个公司 ES 节点有 3 台机器,每台机器看起来内存很多,64G,总内存就是 64 * 3 = 192G 。每台机器给 ES jvm heap 是 32G ,那么剩下来留给 filesystem cache 的就是每台机器才 32G ,总共集群里给 filesystem cache 的就是 32 * 3 = 96G 内存。而此时,整个磁盘上索引数据文件,在 3 台机器上一共占用了 1T 的磁盘容量,ES 数据量是 1T ,那么每台机器的数据量是 300G 。这样性能好吗? filesystem cache 的内存才 100G,十分之一的数据可以放内存,其他的都在磁盘,然后你执行搜索操作,大部分操作都是走磁盘,性能肯定差。
+
归根结底,你要让 ES 性能要好,最佳的情况下,就是你的机器的内存,至少可以容纳你的总数据量的一半。
+根据生产环境实践经验,最佳的情况下,是仅仅在 ES 中就存少量的数据,就是你要用来搜索的那些索引,如果内存留给 filesystem cache 的是 100G,那么你就将索引数据控制在 100G 以内,这样的话,你的数据几乎全部走内存来搜索,性能非常之高,一般可以在 1 秒以内。
+比如说你现在有一行数据。 id,name,age …. 30 个字段。但是你现在搜索,只需要根据 id,name,age 三个字段来搜索。如果你傻乎乎往 ES 里写入一行数据所有的字段,就会导致说 90% 的数据是不用来搜索的,结果硬是占据了 ES 机器上的 filesystem cache 的空间,单条数据的数据量越大,就会导致 filesystem cahce 能缓存的数据就越少。其实,仅仅写入 ES 中要用来检索的少数几个字段就可以了,比如说就写入 ES id,name,age 三个字段,然后你可以把其他的字段数据存在 mysql/hbase 里,我们一般是建议用 ES + hbase 这么一个架构。
+写入 ES 的数据最好小于等于,或者是略微大于 ES 的 filesystem cache 的内存容量。然后你从 ES 检索可能就花费 20ms,然后再根据 ES 返回的 id 去 hbase 里查询,查 20 条数据,可能也就耗费个 30ms,可能你原来那么玩儿,1T 数据都放 es,会每次查询都是 5~10s,现在可能性能就会很高,每次查询就是 50ms。
+假如说,哪怕是你就按照上述的方案去做了,ES 集群中每个机器写入的数据量还是超过了 filesystem cache 一倍,比如说你写入一台机器 60G 数据,结果 filesystem cache 就 30G,还是有 30G 数据留在了磁盘上。
+其实可以做数据预热。
+举个例子,拿微博来说,你可以把一些大 V,平时看的人很多的数据,你自己提前后台搞个系统,每隔一会儿,自己的后台系统去搜索一下热数据,刷到 filesystem cache 里去,后面用户实际上来看这个热数据的时候,他们就是直接从内存里搜索了,很快。
+或者是电商,你可以将平时查看最多的一些商品,比如说 iphone 8,热数据提前后台搞个程序,每隔 1 分钟自己主动访问一次,刷到 filesystem cache 里去。
+对于那些你觉得比较热的、经常会有人访问的数据,最好做一个专门的缓存预热子系统,就是对热数据每隔一段时间,就提前访问一下,让数据进入 filesystem cache 里面去。这样下次别人访问的时候,性能一定会好很多。
+ES 可以做类似于 mysql 的水平拆分,就是说将大量的访问很少、频率很低的数据,单独写一个索引,然后将访问很频繁的热数据单独写一个索引。最好是将冷数据写入一个索引中,然后热数据写入另外一个索引中,这样可以确保热数据在被预热之后,尽量都让他们留在 filesystem os cache 里,别让冷数据给冲刷掉。
+假设你有 6 台机器,2 个索引,一个放冷数据,一个放热数据,每个索引 3 个 shard。3 台机器放热数据 index,另外 3 台机器放冷数据 index。然后这样的话,你大量的时间是在访问热数据 index,热数据可能就占总数据量的 10%,此时数据量很少,几乎全都保留在 filesystem cache 里面了,就可以确保热数据的访问性能是很高的。但是对于冷数据而言,是在别的 index 里的,跟热数据 index 不在相同的机器上,大家互相之间都没什么联系了。如果有人访问冷数据,可能大量数据是在磁盘上的,此时性能差点,就 10% 的人去访问冷数据,90% 的人在访问热数据,也无所谓了。
+对于 MySQL,我们经常有一些复杂的关联查询。在 ES 里该怎么玩儿,ES 里面的复杂的关联查询尽量别用,一旦用了性能一般都不太好。
+最好是先在 Java 系统里就完成关联,将关联好的数据直接写入 ES 中。搜索的时候,就不需要利用 ES 的搜索语法来完成 join 之类的关联搜索了。
+document 模型设计是非常重要的,很多操作,不要在搜索的时候才想去执行各种复杂的乱七八糟的操作。ES 能支持的操作就那么多,不要考虑用 ES 做一些它不好操作的事情。如果真的有那种操作,尽量在 document 模型设计的时候,写入的时候就完成。另外对于一些太复杂的操作,比如 join/nested/parent-child 搜索都要尽量避免,性能都很差的。
++ +
+ + + + + +收录Linux常用命令,以下命令来自https://www.bilibili.com/video/BV14A411378a
+常用命令 | +作用 | +
---|---|
shutdown -h now | +即刻关机 | +
shutdown -h 10 | +10分钟后关机 | +
shutdown -h 11:00 | +11:00关机 | +
shutdown -h +10 | +预定时间关机(10分钟后) | +
shutdown -c | +取消指定时间关机 | +
shutdown -r now | +重启 | +
shutdown -r 10 | +10分钟之后重启 | +
shutdown -r 11:00 | +定时重启 | +
reboot | +重启 | +
init 6 | +重启 | +
init 0 | +⽴刻关机 | +
telinit 0 | +关机 | +
poweroff | +⽴刻关机 | +
halt | +关机 | +
sync | +buff数据同步到磁盘 | +
logout | +退出登录Shell | +
常用命令 | +作用 | +
---|---|
uname -a | +查看内核/OS/CPU信息 | +
uname -r | +查看内核版本 | +
uname -m | +查看处理器架构 | +
arch | +查看处理器架构 | +
hostname | +查看计算机名 | +
who | +显示当前登录系统的⽤户 | +
who am i | +显示登录时的⽤户名 | +
whoami | +显示当前⽤户名 | +
cat /proc/version | +查看linux版本信息 | +
cat /proc/cpuinfo | +查看CPU信息 | +
cat /proc/interrupts | +查看中断 | +
cat /proc/loadavg | +查看系统负载 | +
uptime | +查看系统运⾏时间、⽤户数、负载 | +
env | +查看系统的环境变量 | +
lsusb -tv | +查看系统USB设备信息 | +
lspci -tv | +查看系统PCI设备信息 | +
lsmod | +查看已加载的系统模块 | +
grep MemTotal /proc/meminfo | +查看内存总量 | +
grep MemFree /proc/meminfo | +查看空闲内存量 | +
free -m | +查看内存⽤量和交换区⽤量 | +
date | +显示系统⽇期时间 | +
cal 2021 | +显示2021⽇历表 | +
top | +动态显示cpu/内存/进程等情况 | +
vmstat 1 20 | +每1秒采⼀次系统状态,采20次 | +
iostat | +查看io读写/cpu使⽤情况 | +
查看io读写/cpu使⽤情况 | +查询cpu使⽤情况(1秒⼀次,共10次) | +
sar -d 1 10 | +查询磁盘性能 | +
常用命令 | +作用 | +
---|---|
fdisk -l | +查看所有磁盘分区 | +
swapon -s | +查看所有交换分区 | +
df -h | +查看磁盘使⽤情况及挂载点 | +
df -hl | +同上 | +
du -sh /dir | +查看指定某个⽬录的⼤⼩ | +
du -sk * | sort -rn | +从⾼到低依次显示⽂件和⽬录⼤⼩ | +
mount /dev/hda2 /mnt/hda2 | +挂载hda2盘 | +
mount -t ntfs /dev/sdc1 /mnt/usbhd1 | +指定⽂件系统类型挂载(如ntfs) | +
mount -o loop xxx.iso /mnt/cdrom | +挂 载 iso ⽂ 件 | +
umount -v /dev/sda1 | +通过设备名卸载 | +
umount -v /mnt/mymnt | +通过挂载点卸载 | +
fuser -km /mnt/hda1 | +强制卸载(慎⽤) | +
常用命令 | +作用 | +
---|---|
useradd codesheep | +创建⽤户 | +
userdel -r codesheep | +删除⽤户 | +
usermod -g group_name user_name | +修改⽤户的组 | +
usermod -aG group_name user_name | +将⽤户添加到组 | +
usermod -s /bin/ksh -d /home/codepig –g dev codesheep | +修改⽤户codesheep的登录Shell、主⽬录以及⽤户组 | +
groups test | +查看test⽤户所在的组 | +
groupadd group_name | +创建⽤户组 | +
groupdel group_name | +删除⽤户组 | +
groupmod -n new_name old_name | +重命名⽤户组 | +
su - user_name | +su - user_name | +
passwd | +修改⼝令 | +
passwd codesheep | +修改某⽤户的⼝令 | +
w | +查看活动⽤户 | +
id codesheep | +查看指定⽤户codesheep信息 | +
last | +查看⽤户登录⽇志 | +
crontab -l | +查看当前⽤户的计划任务 | +
cut -d: -f1 /etc/passwd | +查看系统所有⽤户 | +
cut -d: -f1 /etc/group | +查看系统所有组 | +
常用命令 | +作用 | +
---|---|
ifconfig | +查看⽹络接⼝属性 | +
ifconfig eth0 | +查看某⽹卡的配置 | +
route -n | +查看路由表 | +
netstat -lntp | +查看所有监听端⼝ | +
netstat -antp | +查看已经建⽴的TCP连接 | +
netstat -lutp | +查看TCP/UDP的状态信息 | +
ifup eth0 | +启⽤eth0⽹络设备 | +
ifdown eth0 | +禁⽤eth0⽹络设备 | +
iptables -L | +查看iptables规则 | +
ifconfig eth0 192.168.1.1 netmask 255.255.255.0 | +配置ip地址 | +
dhclient eth0 | +以dhcp模式启⽤eth0 | +
route add -net 0/0 gw Gateway_IP | +配置默认⽹关 | +
route add -net 192.168.0.0 netmask 255.255.0.0 gw 192.168.1.1 | +配置静态路由到达⽹络'192.168.0.0/16' | +
route del 0/0 gw Gateway_IP | +删除静态路由 | +
hostname | +查看主机名 | +
host www.baidu.com | +解析主机名 | +
nslookup www.baidu.com | +查询DNS记录,查看域名解析是否正常 | +
ps -ef | +查看所有进程 | +
ps -ef | grep codesheep | +过滤出你需要的进程 | +
kill -s name | +kill指定名称的进程 | +
kill -s pid | +kill指定pid的进程 | +
top | +实时显示进程状态 | +
vmstat 1 20 | +每1秒采⼀次系统状态,采20次 | +
iostat | +iostat | +
sar -u 1 10 | +查询cpu使⽤情况(1秒⼀次,共10次) | +
sar -d 1 10 | +查询磁盘性能 | +
常用命令 | +作用 | +
---|---|
chkconfig –list | +列出系统服务 | +
service <服务名> status | +查看某个服务 | +
service <服务名> start | +启动某个服务 | +
service <服务名> stop | +终⽌某个服务 | +
service <服务名> restart | +重启某个服务 | +
systemctl status <服务名> | +查看某个服务 | +
systemctl start <服务名> | +启动某个服务 | +
systemctl stop <服务名> | +终⽌某个服务 | +
systemctl restart <服务名> | +重启某个服务 | +
systemctl enable <服务名> | +关闭⾃启动 | +
systemctl disable <服务名> | +关闭⾃启动 | +
常用命令 | +作用 | +
---|---|
cd <⽬录名> | +进⼊某个⽬录 | +
cd .. | +回上级⽬录 | +
cd ../.. | +回上两级⽬录 | +
cd | +进个⼈主⽬录 | +
cd - | +回上⼀步所在⽬录 | +
pwd | +显示当前路径 | +
ls | +查看⽂件⽬录列表 | +
ls -F | +查看⽬录中内容(显示是⽂件还是⽬录) | +
ls -l | +查看⽂件和⽬录的详情列表 | +
ls -a | +查看隐藏⽂件 | +
ls -lh | +查看⽂件和⽬录的详情列表(增强⽂件⼤⼩易读性) | +
ls -lSr | +查看⽂件和⽬录列表(以⽂件⼤⼩升序查看) | +
tree | +查看⽂件和⽬录的树形结构 | +
mkdir <⽬录名> | +创建⽬录 | +
mkdir dir1 dir2 | +同时创建两个⽬录 | +
mkdir -p /tmp/dir1/dir2 | +创建⽬录树 | +
rm -f file1 | +删除’file1’⽂件 | +
rmdir dir1 | +删除’dir1’⽬录 | +
rm -rf dir1 | +删除’dir1’⽬录和其内容 | +
rm -rf dir1 dir2 | +同时删除两个⽬录及其内容 | +
mv old_dir new_dir | +重命名/移动⽬录 | +
cp file1 file2 | +复制⽂件 | +
cp dir/* . | +复制某⽬录下的所有⽂件⾄当前⽬录 | +
cp -a dir1 dir2 | +复制⽬录 | +
cp -a /tmp/dir1 . | +复制⼀个⽬录⾄当前⽬录 | +
ln -s file1 link1 | +创建指向⽂件/⽬录的软链接 | +
ln file1 lnk1 | +创建指向⽂件/⽬录的物理链接 | +
find / -name file1 | +从跟⽬录开始搜索⽂件/⽬录 | +
find / -user user1 | +搜索⽤户user1的⽂件/⽬录 | +
find /dir -name *.bin | +在⽬录/dir中搜带有.bin后缀的⽂件 | +
locate <关键词> | +快速定位⽂件 | +
locate *.mp4 | +寻找.mp4结尾的⽂件 | +
whereis <关键词> | +显示某⼆进制⽂件/可执⾏⽂件的路径 | +
which <关键词> | +查找系统⽬录下某的⼆进制⽂件 | +
chmod ugo+rwx dir1 | +设置⽬录所有者(u)、群组(g)及其他⼈(o)的读(r)写(w)执⾏(x)权限 | +
chmod go-rwx dir1 | +移除群组(g)与其他⼈(o)对⽬录的读写执⾏权限 | +
chown user1 file1 | +改变⽂件的所有者属性 | +
chown -R user1 dir1 | +改变⽬录的所有者属性 | +
chgrp group1 file1 | +改变⽂件群组 | +
chown user1:group1 file1 | +改变⽂件的所有⼈和群组 | +
常用命令 | +作用 | +
---|---|
cat file1 | +查看⽂件内容 | +
cat -n file1 | +查看内容并标示⾏数 | +
tac file1 | +从最后⼀⾏开始反看⽂件内容 | +
more file1 | +more file1 | +
less file1 | +类似more命令,但允许反向操作 | +
head -2 file1 | +查看⽂件前两⾏ | +
tail -2 file1 | +查看⽂件后两⾏ | +
tail -f /log/msg | +实时查看添加到⽂件中的内容 | +
grep codesheep hello.txt | +在⽂件hello.txt中查找关键词codesheep | +
grep ^sheep hello.txt | +在⽂件hello.txt中查找以sheep开头的内容 | +
grep [0-9] hello.txt | +选择hello.txt⽂件中所有包含数字的⾏ | +
sed ’s/s1/s2/g’ hello.txt | +将hello.txt⽂件中的s1替换成s2 | +
sed ‘/^$/d’ hello.txt | +从hello.txt⽂件中删除所有空⽩⾏ | +
sed ‘/ *#/d; /^$/d’ hello.txt | +从hello.txt⽂件中删除所有注释和空⽩⾏ | +
sed -e ‘1d’ hello.txt | +从⽂件hello.txt 中排除第⼀⾏ | +
sed -n ‘/s1/p’ hello.txt | +查看只包含关键词"s1"的⾏ | +
sed -e ’s/ *$//’ hello.txt | +删除每⼀⾏最后的空⽩字符 | +
sed -e ’s/s1//g’ hello.txt | +从⽂档中只删除词汇s1并保留剩余全部 | +
sed -n ‘1,5p;5q’ hello.txt | +查看从第⼀⾏到第5⾏内容 | +
sed -n ‘5p;5q’ hello.txt | +查看第5⾏ | +
paste file1 file2 | +合并两个⽂件或两栏的内容 | +
paste -d ‘+’ file1 file2 | +合并两个⽂件或两栏的内容,中间⽤"+“区分 | +
sort file1 file2 | +排序两个⽂件的内容 | +
comm -1 file1 file2 | +⽐较两个⽂件的内容(去除’file1’所含内容) | +
comm -2 file1 file2 | +⽐较两个⽂件的内容(去除’file2’所含内容 | +
comm -3 file1 file2 | +⽐较两个⽂件的内容(去除两⽂件共有部分) | +
常用命令 | +作用 | +
---|---|
zip xxx.zip file | +压缩⾄zip包 | +
zip -r xxx.zip file1 file2 dir1 | +将多个⽂件+⽬录压成zip包 | +
unzip xxx.zip | +解压zip包 | +
tar -cvf xxx.tar file | +创建⾮压缩tar包 | +
tar -cvf xxx.tar file1 file2 dir1 | +将多个⽂件+⽬录打tar包 | +
tar -tf xxx.tar | +查看tar包的内容 | +
tar -xvf xxx.tar | +解压tar包 | +
tar -xvf xxx.tar -C /dir | +将tar包解压⾄指定⽬录 | +
tar -cvfj xxx.tar.bz2 dir | +创建bz2压缩包 | +
tar -jxvf xxx.tar.bz2 | +解压bz2压缩包 | +
tar -cvfz xxx.tar.gz dir | +创建gzip压缩包 | +
tar -zxvf xxx.tar.gz | +解压gzip压缩包 | +
bunzip2 xxx.bz2 | +解压bz2压缩包 | +
bzip2 filename | +压缩⽂件 | +
gunzip xxx.gz | +解压gzip压缩包 | +
gzip filename | +压缩⽂件 | +
gzip -9 filename | +最⼤程度压缩 | +
常用命令 | +作用 | +
---|---|
rpm -qa | +查看已安装的rpm包 | +
rpm -q pkg_name | +查询某个rpm包 | +
rpm -q –whatprovides xxx | +显示xxx功能是由哪个包提供的 | +
rpm -q –whatrequires xxx | +显示xxx功能被哪个程序包依赖的 | +
rpm -q –changelog xxx | +显示xxx包的更改记录 | +
rpm -qi pkg_name | +查看⼀个包的详细信息 | +
rpm -qd pkg_name | +查询⼀个包所提供的⽂档 | +
rpm -qc pkg_name | +查看已安装rpm包提供的配置⽂件 | +
rpm -ql pkg_name | +查看⼀个包安装了哪些⽂件 | +
rpm -qf filename | +查看某个⽂件属于哪个包 | +
rpm -qR pkg_name | +查询包的依赖关系 | +
rpm -ivh xxx.rpm | +安装rpm包 | +
rpm -ivh –test xxx.rpm | +测试安装rpm包 | +
rpm -ivh –nodeps xxx.rpm | +安装rpm包时忽略依赖关系 | +
rpm -e xxx | +卸载程序包 | +
rpm -Fvh pkg_name | +升级确定已安装的rpm包 | +
rpm -Uvh pkg_name | +升级rpm包(若未安装则会安装) | +
rpm -V pkg_name | +RPM包详细信息校验 | +
常用命令 | +作用 | +
---|---|
yum repolist enabled | +显示可⽤的源仓库 | +
yum search pkg_name | +搜索软件包 | +
yum install pkg_name | +下载并安装软件包 | +
yum install –downloadonly pkg_name | +只下载不安装 | +
yum list | +显示所有程序包 | +
yum list installed | +查看当前系统已安装包 | +
yum list updates | +查看可以更新的包列表 | +
yum check-update | +查看可升级的软件包 | +
yum update | +更新所有软件包 | +
yum update pkg_name | +升级指定软件包 | +
yum deplist pkg_name | +列出软件包依赖关系 | +
yum remove pkg_name | +删除软件包 | +
yum clean all | +清除缓存 | +
yum clean packages | +清除缓存的软件包 | +
yum clean headers | +清除缓存的header | +
常用命令 | +作用 | +
---|---|
dpkg -c xxx.deb | +列出deb包的内容 | +
dpkg -i xxx.deb | +安装/更新deb包 | +
dpkg -r pkg_name | +移除deb包 | +
dpkg -P pkg_name | +移除deb包(不保留配置) | +
dpkg -l | +查看系统中已安装deb包 | +
dpkg -l pkg_name | +显示包的⼤致信息 | +
dpkg -L pkg_name | +查看deb包安装的⽂件 | +
dpkg -s pkg_name | +查看包的详细信息 | +
dpkg –unpack xxx.deb | +解开deb包的内容 | +
常用命令 | +作用 | +
---|---|
apt-cache search pkg_name | +搜索程序包 | +
apt-cache show pkg_name | +获取包的概览信息 | +
apt-get install pkg_name | +安装/升级软件包 | +
apt-get purge pkg_name | +卸载软件(包括配置) | +
apt-get remove pkg_name | +卸载软件(不包括配置) | +
apt-get update | +更新包索引信息 | +
apt-get upgrade | +更新已安装软件包 | +
apt-get clean | +清理缓存 | +
参考文章:
+jps查询系统内所有HotSpot进程,它位于java的bin目录下。
+命令 | +含义 | +
---|---|
jps | +输出当前运行主类名称,进程ID | +
jps -q | +只列出进程ID | +
jps -l | +输出当前运行主类的全称,进程ID | +
jps -v | +输出虚拟机进程启动时JVM 参数 |
+
jstat是JDK自带的一个轻量级小工具。全称“Java Virtual Machine statistics monitoring tool”,和jps一样,都在bin目录下。
+命令 | +含义 | +
---|---|
jstat -gc vmid 1000 10 | +查看进程pid 的GC 信息,每1000毫秒 输出一次,输出10次 |
+
jstat -gccause vmid 1000 10 | +查看进程pid 的GC 发生的原因,每一秒(1000毫秒)输出一次,输出10次 |
+
jstat -class vmid | +查看pid 的加载类信息 |
+
jstat -gcutil vmid | +对java 垃圾回收信息的统计 |
+
jstat -gcnew vmid | +显示新生代GC 的情况 |
+
jstat -gcold vmid | +显示老年代GC 的情况 |
+
jinfo查看虚拟机参数信息,也可用于调整虚拟机配置参数。我们通过jinfo --help
能看到相应的参数。
命令 | +含义 | +
---|---|
jinfo pid | +输出关于pid 的一堆相关信息 |
+
jinfo -flags pid | +查看当前进程曾经赋过值的一些参数 | +
jinfo -flag name pid | +查看指定进程的JVM 参数名称的参数的值 |
+
jinfo -flag [+-]name pid | +开启或者关闭指定进程对应名称的JVM 参数 |
+
jinfo -sysprops pid | +来输出当前 JVM 进行的全部的系统属性 |
+
当使用jinfo进行修改对应进程JVM参数时,有一定的局限性。并不是所有的参数都支持修改,只有参数被标记为manageable的参数才可以被实时修改。
+可以使用命令查看被标记为manageable的参数:java -XX:+PrintFlagsFinal -version | grep manageable
jmap全称:Java Memory Map,主要用于打印指定Java进程(或核心文件、远程调试服务器)的共享对象内存映射或堆内存细节。jmap以生成 java程序的dump文件, 也可以查看堆内对象示例的统计信息、查看ClassLoader 的信息以及 finalizer 队列。
+jmap命令可以获得运行中的JVM的堆的快照,从而可以离线分析堆,以检查内存泄漏,检查一些严重影响性能的大对象的创建,检查系统中什么对象最多,各种对象所占内存的大小等等。可以使用jmap生成Heap Dump。
+命令 | +含义 | +
---|---|
jmap -heap pid | +输出整个堆详细信息,包括GC的使用、堆的配置信息,以及内存的使用信息 | +
jmap -histo:live pid | +输出堆中对象的相关统计信息;第一列是序号,第二列是对象个数,第三列是对象大小byte ,第四列是class name |
+
jmap -finalizerinfo pid | +输出等待终结的对象信息 | +
jmap -clstats pid | +输出类加载器信息 | +
jmap -dump:[live],format=b,file=filename.hprof pid | +把进程堆内存使用情况生成到堆转储dump 文件中,live 子选项是可选的,假如指定live 选项,那么只输出活的对象到文件。dump 文件主要作用,如果发生溢出可以使用dump 文件分析是哪些数据导致的 |
+
++Heap Dump又叫堆转储文件,指一个java进程在某一个时间点的内存快照文件。Heap Dump在触发内存快照的时候会保存以下信息:
++
+- 所有的对象
+- 所有的class
+- GC Roots
+- 本地方法栈和本地变量
+通常在写Dump文件前会触发一次Full GC,所以Heap Dump文件里保存的对象都是Full GC后保留的对象信息。 +由于生成dump文件比较耗时,所以请耐心等待,尤其是大内存镜像生成的dump文件,则需要更长的时间来完成。
+
可以通过参数配置当发生OOM时自动生成dump文件:-XX:+HeapDumpOnOutOfMemeryError -XX:+HeapDumpPath=<filename.hprof>
,当然此种方式获取dump文件较大,如果想要获取dump文件较小可以手动获取dump文件并指定只获取存活的对象。
JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump文件,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看。在此要注意,一般不会直接在服务器上进行分析,因为jhat是一个耗时并且耗费硬件资源的过程,一般把服务器生成的dump文件复制到本地或其他机器上进行分析。
+注意,jhat在jdk9中已经移除,官方对贱使用visualvm来配置jmap进行分析。
+命令 | +含义 | +
---|---|
jhat -port 9998 /tmp/dump.dat | +配合jmap 命令使用,查看导出的/tmp/dump.dat 文件,端口为9998;注意如果dump 文件太大,可能需要加上-J-Xmx512m 这种参数指定最大堆内存,即jhat -J-Xmx512m -port 9998 /tmp/dump.dat |
+
jhat -baseline dump2.phrof dump1.phrof | +对比dump2.phrof 与dump1.phrof 文件 |
+
jhat heapDump | +分析dump 文件,默认端口为7000 |
+
jstack,全称JVM Stack Trace栈空间追踪,用于生成虚拟机指定进程当前线程快照;主要分析堆栈空间,也就是分析线程的情况,可以分析出死锁问题,以及cpu100%的问题。jstack可以定位到线程堆栈,根据堆栈信息我们可以定位到具体代码,所以它在JVM性能调优中使用得非常多。
+++jstack主要用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因, +如线程间死锁、死循环、请求外部资源导致的长时间等待等。
+
命令 | +含义 | +
---|---|
jstack pid | +打印出所有的线程,包括用户自己启动的线程和JVM 后台线程 |
+
jstack 13324 >1.txt | +将13324进程中线程信息写入到1.txt 文件中 |
+
jstack 21711|grep 54ee | +在进程21711中查找线程ID为54ee(16进制)的信息 | +
jstack -l pid | +除了堆栈信息外-l 参数会显示线程锁的附加信息 |
+
除了可以使用jstack打印栈的信息,在java层面也可以使用Thread.getAllStackTraces()
方法获取堆栈信息。
在JDK1.7之后,新增了一个命令行工具jcmd。
+它是一个多功能的工具,可以实现前面除了jstat之外的所有功能。例如,导出dump文件、查看线程信息、导出线程信息、执行GC,JVM运行时间等。
+jcmd拥有jmap的大部分功能,并且在官方网站上也推荐使用jcmd代替jmap。
+命令 | +含义 | +
---|---|
jcmd -l | +列出所有JVM 的进程 |
+
jcmd pid help | +针对指定进程罗列出可执行的命令 | +
jcmd pid <具体命令> | +显示指定进程的指令命令的数据 | +
JConsole 是一个内置 Java 性能分析器,可以从命令行(直接输入jconsole)或在 GUI shell (jdk\bin下打开)中运行。
+它用于对JVM中内存,线程和类等的监控。这款工具的好处在于,占用系统资源少,而且结合Jstat,可以有效监控到java内存的变动情况,以及引起变动的原因。在项目追踪内存泄露问题时,很实用。
+ + + +visual vm 是一个功能强大的多合一故障诊断和性能监控的可视化工具。它集成了多个JDK命令行工具,使用visual vm可用于显示虚拟机进程及进程的配置和环境信息,监视应用程序的CPU、GC、堆、方法区及线程的信息等,甚至代替jconsole。
+在JDK7,visual vm便作为JDK的一部分发布,在JDK的bin目录下,即:它完全免费。此外,visual vm也可以作为独立软件进行安装。
+主要功能:
+visual vm 支持插件扩展,可以在visual vm上安装插件,也可以将visual vm安装在idea上:
+ + +visual vm可以生成dump文件,生成的dump文件是临时的,如果想要保留该文件需要右键另存为即可:
+ + +如果堆文件数据较大,排查起来很困难,可以使用OQL语句进行筛选。
+++OQL:全称,Object Query Language 类似于SQL查询的一种语言,OQL使用SQL语法,可以在堆中进行对象的筛选。
+基本语法:
+select <JavaScript expression to select> +[ from (instanceof) <class name> <identifier> +( where <JavaScript boolean expression to filter> ) ] +
1.class name是java类的完全限定名 +2.instanceof表示也查询某一个类的子类 +3.from和where子句都是可选的 +4.可以使用obj.field_name语法访问Java字段
+例如
+-- 查询长度大于等于100的字符串 +select s from java.lang.String s where s.value.length >= 100 + +-- 显示所有File对象的文件路径 +select file.path.value.toString() from java.io.File file + +-- 显示由给定id字符串标识的Class的实例 +select o from instanceof 0x741012748 o +
visual vm也可以将两个dump文件进行比较:
+ +visual vm不但可以生成堆的dump文件,也可以对线程dump:
+ +MAT全称,Memory Analyzer Tool 是一款功能强大的Java堆内存分析器。可以用于查找内存泄漏以及查看内存消耗情况。
+MAT是eclipse开发的,不仅可以单独使用,还可以作为插件嵌入在eclipse中使用。是一款免费的性能分析工具,使用起来很方便。
+MAT的主要功能就是分析dump文件。分析dump最终目的是为了找出内存泄漏的疑点,防止内存泄漏。
+JVM内存包含信息:
+常见获取dump文件方式:
+MAT介绍
+导入dump文件:
+在生成可疑泄漏报告后,会在对应的堆转储文件目录下生成一个zip文件。
+ + + + + +MAT最主要的功能是分析dump文件,其中比较重要的功能就是histogram(直方图)和dominator tree(支配树)
+直方图
+ +如上图所示:(浅堆<= 深堆 <= 实际大小)
+支配树对象图
+ +支配树概念源自图论。它体现了对象实例之间的支配关系。在对象的引用图中,所有指向对象B的路径都要经过对象A,则认为对象A支配对象B。如果对象A是离对象B最近的一个支配对象,则认为对象A为对象B的直接支配者。
+支配树是基于对象间的引用图建立的,它有以下性质:
+分配树能直观的体现对象能否被回收的情况,如图所示,左为对象的引用图,右为对象的支配图。
+官方:
+理想的情况下,一个Java程序使用JVM的默认设置也可以运行得很好,所以一般来说,没有必要设置任何JVM参数。然而,由于一些性能问题,我们需要设置合理的JVM参数。
+可以通过java -XX:+PrintFlagsInitial
命令查看JVM所有参数。
常用参数:
+参数 | +含义 | +描述 | +
---|---|---|
-Xms | +堆初始值 | +Xmx和Xms设置为老年代存活对象的3-4倍,即FullGC之后的老年代内存占用的3-4倍 | +
-Xmx | +堆最大值 | +为了防止自动扩容降低性能,建议将-Xms和-Xmx的值设置为相同值 | +
-XX:MaxHeapFreeRatio | +最大堆内存使用率 | +默认70,当超过该比例会进行扩容堆,Xms=Xmx时该参数无效 | +
-XX:MinHeapFreeRatio | +最小堆内存使用率 | +默认40,当低于该比例会缩减堆,Xms=Xmx时该参数无效 | +
-Xmn | +年轻代内存最大值 | +年轻代设置的越大,老年代区域就会减少。一般不允许年轻代比老年代还大,因为要考虑GC时最坏情况,所有对象都晋升到老年代。建议设置为老年代存活对象的1-1.5倍,最大可以设置为-Xmx/2 。考虑性能,一般会通过参数 -XX:NewSize 设置年轻代初始大小。如果知道了年轻代初始分配的对象大小,可以节省新生代自动扩展的消耗。 | +
-XX:SurvivorRatio | +年轻代中两个Survivor区和Eden区大小比率 | +例如: -XX:SurvivorRatio=10 表示伊甸园区是幸存者其中一个区大小的10倍,所以,伊甸园区占新生代大小的10/12, 幸存区From和幸存区To 每个占新生代的1/12 | +
-XX:NewRatio | +年轻生代和老年代的比率 | +例如:-XX:NewRatio=3 指定老年代/新生代为3/1. 老年代占堆大小的 3/4 ,新生代占 1/4 。如果针对新生代,同时定义绝对值和相对值,绝对值将起作用,建议将年轻代的大小为整个堆的3/8左右。 | +
-XX:+HeapDumpOnOutOfMemoryError | +让JVM在发生内存溢出时自动的生成堆内存快照 | +可以通过-XX:HeapDumpPath=path参数将生成的快照放到指定路径下 | +
-XX:OnOutOfMemoryError | +当内存溢发生时可以执行一些指令 | +比如发个E-mail通知管理员或者执行一些清理工作,执行脚本 | +
-XX:ThreadStackSize | +每个线程栈最大值 | +栈设置太大,会导致线程创建减少,栈设置小,会导致深入不够,深度的递归会导致栈溢出,建议栈深度设置在3000-5000k。 | +
-XX:MetaspaceSize | +初始化的元空间大小 | +如果元空间大小达到了这个值,就会触发Full GC为了避免频繁的Full GC,建议将- XX:MetaspaceSize设置较大值。如果释放了空间之后,元空间还是不足,那么就会自动增加MetaspaceSize的大小 | +
-XX:MaxMetaspaceSize | +元空间最大值 | +默认情况下,元空间最大的大小是系统内存的大小,元空间一直扩大,虚拟机可能会消耗完所有的可用系统内存。 | +
JVM优化是到最后不得已才采用的手段,对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数。
+何时调优:
+调优原则:
+调优思路:
+#!/bin/sh
+
+#非特殊应用下面内存分配已经够用
+HEAP_MEMORY=1024M
+METASPACE_SIZE=256M
+
+SERVER_HOME="$( cd "$( dirname "$0" )" && pwd )"
+APP_NAME=${@: -1}
+
+#使用说明,用来提示输入参数
+help() {
+ echo "Usage: start.sh {start|stop|restart|status|help} APP_NAME.jar" >&2
+ echo "Examples:"
+ echo " sh start.sh start APP_NAME.jar"
+ echo " sh start.sh stop APP_NAME.jar"
+ echo " sh start.sh start -Heap 1024M -MetaspaceSize 256M APP_NAME.jar"
+}
+
+#检查程序是否在运行
+is_exist() {
+ pid=`ps -ef | grep ${SERVER_HOME} | grep ${APP_NAME} | grep -v grep | awk '{print $2}' `
+ #如果不存在返回1,存在返回0
+ if [ -z "${pid}" ]; then
+ return 1
+ else
+ return 0
+ fi
+}
+
+#启动方法
+start() {
+ is_exist
+ if [ $? -eq "0" ]; then
+ echo "${APP_NAME} is already running. pid=${pid} ."
+ else
+ echo "${APP_NAME} running..."
+ JAVA_OPTS="-server -XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError"
+ JAVA_OPTS="${JAVA_OPTS} -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false"
+ #JAVA_OPTS="${JAVA_OPTS} -Djava.rmi.server.hostname=${LOCAL_IP} -Dcom.sun.management.jmxremote.port=${JMX_PORT} -Dcom.sun.management.jmxremote.rmi.port=${JMX_PORT}"
+
+ shift
+ ARGS=($*)
+ for ((i=0; i<${#ARGS[@]}; i++)); do
+ case "${ARGS[$i]}" in
+ -D*) JAVA_OPTS="${JAVA_OPTS} ${ARGS[$i]}" ;;
+ -Heap*) HEAP_MEMORY="${ARGS[$i+1]}" ;;
+ -MetaspaceSize*) METASPACE_SIZE="${ARGS[$i+1]}" ;;
+ esac
+ done
+
+ JAVA_OPTS="${JAVA_OPTS} -Xms${HEAP_MEMORY} -Xmx${HEAP_MEMORY} -XX:MaxMetaspaceSize=${METASPACE_SIZE} -XX:MetaspaceSize=${METASPACE_SIZE}"
+ #生产环境加上下面这个配置 服务启动的时候真实的分配物理内存给jvm
+ #JAVA_OPTS="${JAVA_OPTS} -XX:+AlwaysPreTouch"
+ JAVA_OPTS="${JAVA_OPTS} -Duser.dir=${SERVER_HOME}"
+ #下面两段根据需要酌情配置
+ #JAVA_OPTS="${JAVA_OPTS} -Xloggc:${APP_NAME}.gc.log"
+ #JAVA_OPTS="${JAVA_OPTS} -Dapp.name=${SERVER_NAME} -Dlogging.config=${SERVER_HOME}/logback-spring.xml -Dspring.profiles.active=dev"
+ echo "jvm args: ${JAVA_OPTS}"
+ java ${JAVA_OPTS} -jar ${APP_NAME} >/dev/null 2>&1 &
+ fi
+}
+
+#停止方法
+stop() {
+ is_exist
+ if [ $? -eq "0" ]; then
+ echo "${APP_NAME} is stopping..."
+ kill -9 $pid
+ else
+ echo "${APP_NAME} is not running"
+ fi
+}
+
+#输出运行状态
+status() {
+ is_exist
+ if [ $? -eq "0" ]; then
+ echo "${APP_NAME} is running. Pid is ${pid}"
+ else
+ echo "${APP_NAME} is not running."
+ fi
+}
+
+#根据输入参数,选择执行对应方法,不输入则执行使用说明
+case "$1" in
+ "start")
+ start $@;
+ ;;
+ "stop")
+ stop $@;
+ ;;
+ "status")
+ status $@;
+ ;;
+ "restart")
+ stop $@;
+ start $@;
+ ;;
+ *)
+ help
+ ;;
+esac
+
一般在生产环境排查程序故障,都会查看日志什么的,但是有些故障日志是看不出来的,就比如:CPU使用过高。
+那应该怎么办呢?我们需要结合linux命令和JDK相关命令来排查程序故障。
+步骤:
+ps -mp 进程ID -o THREAD,tid,time
命令可以找到有问题的线程ID;
+++ps -mp 进程ID -o THREAD,tid,time 说明: +-m:显示所有线程 +-p:pid进程使用CPU的时间 +-o:该参数后是用户自定义参数
+
printf "%x\n" 线程ID
,当然也可以使用工具从10进制转16进制。
+printf "%x\n" 16
+
jstack 进程ID | grep 16进制线程ID -A50
,就能看到有问题的代码。与CPU使用过高同样的,内存如果占用过大,查看程序日志也看不出来。
+步骤:
+ps p进程ID -L -o pcpu,pmem,pid,tid,time,tname,cmd
,记下使用内存异常的记下线程ID;jstack -l 进程ID > 文件名
,写入文件后将文件中的线程ID转换为16进制,在文件中搜索16进制线程ID即可;死锁经常表现为程序的停顿,或者不再响应用户的请求。从操作系统上观察,对应进程的CPU占用率为零,很快会从top或prstat的输出中消失。
+死锁示例代码:
+public class MainTest {
+
+ public static void main(String[] args) {
+ String lockA = "lockA";
+ String lockB = "lockB";
+ new Thread(new ThreadHolderLock(lockA,lockB),"线程AAA").start();
+ new Thread(new ThreadHolderLock(lockB,lockA),"线程BBB").start();
+ }
+}
+
+class ThreadHolderLock implements Runnable{
+
+ private String lockA;
+ private String lockB;
+
+ public ThreadHolderLock(String lockA, String lockB){
+ this.lockA = lockA;
+ this.lockB = lockB;
+ }
+
+ @Override
+ public void run() {
+ synchronized (lockA){
+ System.out.println(Thread.currentThread().getName() + "\t 持有锁 "+ lockA+", 尝试获得"+ lockB);
+
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ synchronized (lockB){
+ System.out.println(Thread.currentThread().getName() + "\t 持有锁 "+ lockB+", 尝试获得"+ lockA);
+ }
+ }
+ }
+}
+
步骤:
+jps -l
命令找到程序进程;jstack pid
命令打印堆栈信息;上面死锁示例代码使用jstack pid
后的一些信息:
Found one Java-level deadlock:
+=============================
+"线程BBB":
+ waiting to lock monitor 0x00007feb0d80b018 (object 0x000000076af2d588, a java.lang.String),
+ which is held by "线程AAA"
+"线程AAA":
+ waiting to lock monitor 0x00007feb0d80d8a8 (object 0x000000076af2d5c0, a java.lang.String),
+ which is held by "线程BBB"
+
+Java stack information for the threads listed above:
+===================================================
+"线程BBB":
+ at com.github.springcloud.service.ThreadHolderLock.run(MainTest.java:35)
+ - waiting to lock <0x000000076af2d588> (a java.lang.String)
+ - locked <0x000000076af2d5c0> (a java.lang.String)
+ at java.lang.Thread.run(Thread.java:748)
+"线程AAA":
+ at com.github.springcloud.service.ThreadHolderLock.run(MainTest.java:35)
+ - waiting to lock <0x000000076af2d5c0> (a java.lang.String)
+ - locked <0x000000076af2d588> (a java.lang.String)
+ at java.lang.Thread.run(Thread.java:748)
+
+Found 1 deadlock.
+
Java虚拟机是使用引用计数法和可达性分析来判断对象是否可回收,本质是判断一个对象是否还被引用,如果没有引用则回收。在开发的过程中,由于代码的实现不同就会出现很多种内存泄漏问题,让gc误以为此对象还在引用中,无法回收,造成内存泄漏。
+当内存泄露时,如果不是JVM参数中的内存分配太小了,那么从根本上解决Java内存泄露的唯一方法就是修改程序。
+内存泄露主要原因:
+内存泄漏排查:
+使用MAT找到内存泄漏的代码思路:
+ ++ +
+ + + + + ++ +
+ + + + + +++bug的起源: +1945年,一只小飞蛾钻进了计算机电路里,导致系统无法工作,一位名叫格蕾丝·赫柏的人把飞蛾拍死在工作日志上,写道:就是这个 bug(虫子),害我们今天的工作无法完成——于是,bug一词成了电脑系统程序的专业术语,形容那些系统中的缺陷或问题。
+
bug一词,是指“故障”、“缺陷”。了解软件开发的朋友都非常熟悉,程序员和测试人员更不用说,在工作中会常遇到。 作为一名开发人员,项目出现bug是避免不了的。无论你是一名初入职场的小白,还是拥有经验丰富的大佬,只要经常写代码,梳理业务逻辑,很难免不出bug。正所谓常在河边走,哪能不湿鞋。记得以前经常听人说,如果你没有把系统搞宕机过,就不是一名合格的CTO,成为一名出色的开发人员,经验都是一个一个积累起来的。
+所谓的bug指的是生产环境发现的问题;那么只要线上不发现问题,那就不是bug。之所以这么讲是因为一旦影响程序的正常使用便会影响公司的利益,公司利益被影响你自己的利益也会被影响;所以为了自己写程序的时候尽量多测试,将可能会应对到的情况想得全一点,减少bug的出现。
+程序员能为了减少bug的出现能做的可以从代码方面下手:
+除此之外,一方面可以借助一些工具来进行bug的规避,比如sonar,可以发现一些潜在的错误和问题。另一方面我们要不断学习和提高自己的编程技能,以提高代码质量和减少错误的发生。
+尽管这样bug这种东西还是不可避免的,我们只能减少bug而不能消除bug,没有人保证自己的程序一定是没有问题的。减少bug的出现只能多测多验证,哪怕单元测试通过都不能非常有效减少bug,因为受到写单元测试的人的思维角度限制,导致单元测试的片面性。
+关于bug的种类,最容易出现的bug是逻辑上的bug,如复杂庞大一点软件如果不是所有地方都熟悉就写代码是比较容易遗漏一些特殊情况的。除了逻辑上的bug外在开发中还有框架中存在的bug,但是这种bug是我们避免不了的。如2021年爆发的log4j堪称史诗级的bug。 +没什么好的办法可以提前避免掉,就多用一些稳定的框架吧,有apache就用apache,没有就优先使用fork,start高的,从概率上减少。
+一个人的力量毕竟有限必要的时候可以叫上同事,进行code review。
+因为bug是不可避免的,所以解决bug能力就显得尤为重要,程序员归根到底拼的是解决问题的能力。
+bug的解决思路:
+亲自复现问题,关注第一现场,确定是必现还是偶现; 区分是人的问题还是环境的问题;如果是人的问题,那是配置参数的问题还是代码逻辑的问题。如果是配置参数的问题,则通过对比正常运行的配置参数发现问题。 +如果是代码逻辑的问题,则通过commit的历史二分查找缩小出现问题的逻辑范围; 如果是机器的问题,确定是单机问题还是集群问题; 如果是单机问题,则替换机器,如果是集群问题则考虑升级硬件设备。
++ +
+ + + + + +我们为什么要遵守规范来编码?
+是因为通常在编码过程中我们不只自己进行开发,通常需要一个团队来进行,开发好之后还需要维护,所以编码规范就显的尤为重要。
+代码维护时间比较长,那么保证代码可读性就显得很重要。作为一个程序员,咱们得有点追求和信仰。推荐《阿里巴巴Java开发手册》。
+内存泄漏是指不使用的对象持续占有内存使得内存得不到释放,从而造成内存空间的浪费。
+内存泄露导致问题:
+内存泄露是很严重的问题,在出现内存泄露的情况下,想要解决是肯定要修改代码的,所以在编写代码的时候要避免出现内存泄露。
+大多数内存泄露的原因是,长生命周期的对象引用了短生命周期的对象。例如,A对象引用B对象,A对象的生命周期(t1-t4)比B对象的生命周期(t2-t3)长的多。当B对象没有被应用程序使用之后,A对象仍然在引用着B对象。这样,垃圾回收器就没办法将B对象从内存中移除,从而导致内存泄露问题。
+一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏
+public class UsingRandom {
+ private String msg;
+ public void receiveMsg(){
+ readFromNet();//从网络中接受数据保存到msg中
+ saveDB();//把msg保存到数据库中
+ }
+}
+
解决办法,将msg定义在receiveMsg方法中或将msg重置为null。
+public class UsingRandom {
+ private String msg;
+ public void receiveMsg(){
+ readFromNet();//从网络中接受数据保存到msg中
+ saveDB();//把msg保存到数据库中
+ msg = null;
+ }
+}
+
HashMap、LinkedList 等集合类,如果这些集合是静态的并且向集合中添加了对象,这些对象就算不再使用,也不会被GC主动回收的,它们的生命周期与JVM程序一致,容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。
+public class MyTest {
+ static Map<String, User> map = new HashMap<>();
+ public static void main(String[] args) throws InterruptedException {
+ User user = new User();
+ map.put("01",user);
+ }
+}
+
解决办法是将使用完后的集合和对象重置为null,或将集合替换成弱引用集合(只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存)。
+ static Map<String, User> map = new HashMap<>();
+ public static void main(String[] args) throws InterruptedException {
+
+ User user = new User();
+ map.put("01",user);
+
+ user = null;
+ map = null;
+
+ System.gc();
+ Thread.sleep(1000);
+ }
+
static Map<String, User> map = new WeakHashMap<>();
+ public static void main(String[] args) throws InterruptedException {
+ User user = new User();
+ map.put("01",user);
+ }
+
非静态内部类,自动生成的构造方法,默认的参数是外部类的类型,因此使用非内部内部类的时候会保留一个外部类的引用。如果换成静态内部类则不会生成默认的构造方法。
+public class MyClass {
+
+
+ public static void main(String[] args) throws Throwable {
+
+ }
+
+ public class A{
+ public void methed1(){
+
+ }
+ }
+
+ public static class B{
+ public void methed1(){
+
+ }
+ }
+}
+
如果一个外部类的实例对象的方法返回了一个内部类的实例对象。这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄漏。
+Java容器LinkedList移除元素方法核心源码:
+
+//删除指定节点并返回被删除的元素值
+E unlink(Node<E> x) {
+ //获取当前值和前后节点
+ final E element = x.item;
+ final Node<E> next = x.next;
+ final Node<E> 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; // help gc
+ size--;
+ modCount++;
+ return element;
+}
+
除了修改节点间的关联关系,我们还要做的就是赋值为null的操作,不管GC何时会开始清理,我们都应及时的将无用的对象标记为可被清理的对象。
+public E pop(){
+ if(size == 0)
+ return null;
+ else{
+ E e = (E) elementData[--size];
+ elementData[size] = null; // help gc
+ return e;
+ }
+}
+
因为改变了对象属性的值相当于改变了改对象的hash值,删除的时候是根据对象的hash值来删除的,删除对象的时候找不到对应的hash值,所以不能删除,最终导致内存泄露。
+public class ChangeHashCode {
+ public static void main(String[] args) {
+ HashSet set = new HashSet();
+ Person p1 = new Person(1001, "AA");
+ Person p2 = new Person(1002, "BB");
+
+ set.add(p1);
+ set.add(p2);
+
+ p1.name = "CC"; // 导致了内存的泄漏
+ set.remove(p1); // 对象哈希值发生变化,检索不到,导致删除失败
+
+ System.out.println(set);
+
+ set.add(new Person(1001, "CC"));
+ System.out.println(set);
+
+ set.add(new Person(1001, "AA"));
+ System.out.println(set);
+ }
+}
+
+class Person {
+ int id;
+ String name;
+
+ public Person(int id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Person)) return false;
+
+ Person person = (Person) o;
+
+ if (id != person.id) return false;
+ return name != null ? name.equals(person.name) : person.name == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = id;
+ result = 31 * result + (name != null ? name.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "Person{" +
+ "id=" + id +
+ ", name='" + name + '\'' +
+ '}';
+ }
+}
+
如数据库连接、网络连接和IO连接等。当不再使用时,需要调用close方法来释放连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。否则,如果在连接过程中,对一些对象不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。
+public static void main(String[] args) {
+ try{
+ Connection conn =null;
+ Class.forName("com.mysql.jdbc.Driver");
+ conn =DriverManager.getConnection("url","","");
+ Statement stmt =conn.createStatement();
+ ResultSet rs =stmt.executeQuery("....");
+ } catch(Exception e){
+ } finally {
+ // 1.关闭结果集 Statement
+ // 2.关闭声明的对象 ResultSet
+ // 3.关闭连接 Connection
+ }
+}
+
编写代码最终要的原则就是要具有扩展性,如果没有扩展性那么代码维护起来会非常麻烦。
+public class MyTest {
+ public static void main(String[] args) {
+ Object obj = new Object();
+ Assert.notNull(obj,"对象不能为空");
+ Assert.isTrue(obj != null,"对象不能为空");
+ ArrayList<Object> list = new ArrayList<>();
+ Assert.notEmpty(list,"list不能为空");
+ }
+}
+
public class MyTest {
+ public static void main(String[] args) {
+ Object obj = new Object();
+ System.out.println("是否不为空:" + Optional.of(obj).isPresent());
+
+ User user = new User();
+ user.setId(1);
+ user.setName("ls");
+ Optional<User> zs = Optional.of(user).filter(u -> u.getName().equals("zs"));
+ System.out.println(zs.get());
+ zs.ifPresent(item ->{
+ System.out.println("对象不等于空,做的一系列操作");
+ });
+ User user1 = Optional.of(zs.orElse(new User())).get();
+ System.out.println(user1);
+ }
+
+
+ static class User {
+ private int id;
+ private String name;
+
+ public String getName() {
+ return name;
+ }
+ public int getId() {
+ return id;
+ }
+ public void setName(String name) {
+ this.name = name;
+ }
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ @Override
+ public String toString() {
+ return "User{" +
+ "id=" + id +
+ ", name='" + name + '\'' +
+ '}';
+ }
+ }
+}
+
自定义链式调用,对Optional扩展:
+ +@Data
+public class User {
+
+ private String name;
+
+ private String gender;
+
+ private School school;
+
+ @Data
+ public static class School {
+
+ private String scName;
+
+ private String adress;
+ }
+}
+
/**
+* @author Axin
+* @since 2020-09-10
+* @summary 链式调用 bean 中 value 的方法
+*/
+public final class OptionalBean<T> {
+
+ private static final OptionalBean<?> EMPTY = new OptionalBean<>();
+
+ private final T value;
+
+ private OptionalBean() {
+ this.value = null;
+ }
+
+ /**
+ * 空值会抛出空指针
+ * @param value
+ */
+ private OptionalBean(T value) {
+ this.value = Objects.requireNonNull(value);
+ }
+
+ /**
+ * 包装一个不能为空的 bean
+ * @param value
+ * @param <T>
+ * @return
+ */
+ public static <T> OptionalBean<T> of(T value) {
+ return new OptionalBean<>(value);
+ }
+
+ /**
+ * 包装一个可能为空的 bean
+ * @param value
+ * @param <T>
+ * @return
+ */
+ public static <T> OptionalBean<T> ofNullable(T value) {
+ return value == null ? empty() : of(value);
+ }
+
+ /**
+ * 取出具体的值
+ * @param fn
+ * @param <R>
+ * @return
+ */
+ public T get() {
+ return Objects.isNull(value) ? null : value;
+ }
+
+ /**
+ * 取出一个可能为空的对象
+ * @param fn
+ * @param <R>
+ * @return
+ */
+ public <R> OptionalBean<R> getBean(Function<? super T, ? extends R> fn) {
+ return Objects.isNull(value) ? OptionalBean.empty() : OptionalBean.ofNullable(fn.apply(value));
+ }
+
+ /**
+ * 如果目标值为空 获取一个默认值
+ * @param other
+ * @return
+ */
+ public T orElse(T other) {
+ return value != null ? value : other;
+ }
+
+ /**
+ * 如果目标值为空 通过lambda表达式获取一个值
+ * @param other
+ * @return
+ */
+ public T orElseGet(Supplier<? extends T> other) {
+ return value != null ? value : other.get();
+ }
+
+ /**
+ * 如果目标值为空 抛出一个异常
+ * @param exceptionSupplier
+ * @param <X>
+ * @return
+ * @throws X
+ */
+ public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
+ if (value != null) {
+ return value;
+ } else {
+ throw exceptionSupplier.get();
+ }
+ }
+
+ public boolean isPresent() {
+ return value != null;
+ }
+
+ public void ifPresent(Consumer<? super T> consumer) {
+ if (value != null)
+ consumer.accept(value);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(value);
+ }
+
+ /**
+ * 空值常量
+ * @param <T>
+ * @return
+ */
+ public static<T> OptionalBean<T> empty() {
+ @SuppressWarnings("unchecked")
+ OptionalBean<T> none = (OptionalBean<T>) EMPTY;
+ return none;
+ }
+
+}
+
public static void main(String[] args) {
+
+ User axin = new User();
+ User.School school = new User.School();
+ axin.setName("hello");
+
+ // 1. 基本调用
+ String value1 = OptionalBean.ofNullable(axin)
+ .getBean(User::getSchool)
+ .getBean(User.School::getAdress).get();
+ System.out.println(value1);
+
+ // 2. 扩展的 isPresent方法 用法与 Optional 一样
+ boolean present = OptionalBean.ofNullable(axin)
+ .getBean(User::getSchool)
+ .getBean(User.School::getAdress).isPresent();
+ System.out.println(present);
+
+
+ // 3. 扩展的 ifPresent 方法
+ OptionalBean.ofNullable(axin)
+ .getBean(User::getSchool)
+ .getBean(User.School::getAdress)
+ .ifPresent(adress -> System.out.println(String.format("地址存在:%s", adress)));
+
+ // 4. 扩展的 orElse
+ String value2 = OptionalBean.ofNullable(axin)
+ .getBean(User::getSchool)
+ .getBean(User.School::getAdress).orElse("家里蹲");
+
+ System.out.println(value2);
+
+ // 5. 扩展的 orElseThrow
+ try {
+ String value3 = OptionalBean.ofNullable(axin)
+ .getBean(User::getSchool)
+ .getBean(User.School::getAdress).orElseThrow(() -> new RuntimeException("空指针了"));
+ } catch (Exception e) {
+ System.out.println(e.getMessage());
+ }
+}
+
思路:定义一个注解,将需要校验的参数对象都标注该注解,利用SpringAOP,拦截该注解,将其中标注的参数取出,最后通过BeanValidator进行校验。
+所需依赖:
+<dependency>
+ <groupId>org.hibernate</groupId>
+ <artifactId>hibernate-validator</artifactId>
+</dependency>
+
/**
+ * facade接口注解, 用于统一对facade进行参数校验及异常捕获
+ * @author whitepure
+ */
+@Target({ElementType.METHOD,ElementType.PARAMETER})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Facade {
+
+}
+
@Slf4j
+@Aspect
+@Component
+public class FacadeAspect {
+
+
+ @Around("@annotation(com.spring.example.annotation.Facade)")
+ public Object facade(ProceedingJoinPoint pjp) throws Exception {
+
+ // 获取,执行目标方法
+ Method method = ((MethodSignature) pjp.getSignature()).getMethod();
+
+ Object[] args = pjp.getArgs();
+
+ log.info("获取@Facede注解参数列表,参数: {}", args);
+
+ // 参数类型
+ Class<?> returnType = ((MethodSignature) pjp.getSignature()).getMethod().getReturnType();
+
+ //循环遍历所有参数,进行参数校验
+ for (Object parameter : args) {
+ try {
+ BeanValidator.validateObject(parameter);
+ } catch (ValidationException e) {
+ return getFailedResponse(returnType, e);
+ }
+ }
+
+ try {
+ // 目标方法执行
+ return pjp.proceed();
+ } catch (Throwable throwable) {
+ // 返回通用失败响应
+ return getFailedResponse(returnType, throwable);
+ }
+ }
+
+ /**
+ * 定义并返回一个通用的失败响应
+ */
+ private Object getFailedResponse(Class<?> returnType, Throwable throwable)
+ throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
+
+ //如果返回值的类型为BaseResponse 的子类,则创建一个通用的失败响应
+ if (returnType.getDeclaredConstructor().newInstance() instanceof ApiResponse) {
+ ApiResponse response = (ApiResponse) returnType.getDeclaredConstructor().newInstance();
+ String message = throwable.getMessage();
+ log.error("校验bean异常:", throwable);
+ response.setMessage(message);
+ response.setCode(Status.ERROR.getCode());
+ return response;
+ }
+ log.error("failed to getFailedResponse , returnType ({}) is not instanceof BaseResponse", returnType);
+ return null;
+ }
+
+}
+
public class BeanValidator {
+
+
+ private static final Validator validator = Validation
+ .byProvider(HibernateValidator.class)
+ .configure().failFast(true)
+ .buildValidatorFactory().getValidator();
+
+ /**
+ * 校验对象
+ *
+ * @param object object
+ * @param groups groups
+ */
+ public static void validateObject(Object object, Class<?>... groups) throws ValidationException {
+ Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
+ if (constraintViolations.stream().findFirst().isPresent()) {
+ throw new ValidationException(constraintViolations.stream().findFirst().get().getMessage());
+ }
+ }
+
+
+ /**
+ * 校验对象bean
+ *
+ * @param t bean
+ * @param groups 校验组
+ * @return ValidResult
+ */
+ public static <T> ValidResult validateBean(T t, Class<?>... groups) {
+ ValidResult result = new ValidResult();
+ Set<ConstraintViolation<T>> violationSet = validator.validate(t, groups);
+ boolean hasError = violationSet != null && violationSet.size() > 0;
+ result.setHasErrors(hasError);
+ if (hasError) {
+ for (ConstraintViolation<T> violation : violationSet) {
+ result.addError(violation.getPropertyPath().toString(), violation.getMessage());
+ }
+ }
+ return result;
+ }
+
+ /**
+ * 校验bean的某一个属性
+ *
+ * @param obj bean
+ * @param propertyName 属性名称
+ * @return ValidResult
+ */
+ public static <T> ValidResult validateProperty(T obj, String propertyName) {
+ ValidResult result = new ValidResult();
+ Set<ConstraintViolation<T>> violationSet = validator.validateProperty(obj, propertyName);
+ boolean hasError = violationSet != null && violationSet.size() > 0;
+ result.setHasErrors(hasError);
+ if (hasError) {
+ for (ConstraintViolation<T> violation : violationSet) {
+ result.addError(propertyName, violation.getMessage());
+ }
+ }
+ return result;
+ }
+
+ /**
+ * 校验结果类
+ */
+ @Data
+ public static class ValidResult {
+
+ /**
+ * 是否有错误
+ */
+ private boolean hasErrors;
+
+ /**
+ * 错误信息
+ */
+ private List<ErrorMessage> errors;
+
+ public ValidResult() {
+ this.errors = new ArrayList<>();
+ }
+
+ public boolean hasErrors() {
+ return hasErrors;
+ }
+
+ public void setHasErrors(boolean hasErrors) {
+ this.hasErrors = hasErrors;
+ }
+
+ /**
+ * 获取所有验证信息
+ *
+ * @return 集合形式
+ */
+ public List<ErrorMessage> getAllErrors() {
+ return errors;
+ }
+
+ /**
+ * 获取所有验证信息
+ *
+ * @return 字符串形式
+ */
+ public String getErrors() {
+ StringBuilder sb = new StringBuilder();
+ for (ErrorMessage error : errors) {
+ sb.append(error.getPropertyPath()).append(":").append(error.getMessage()).append(" ");
+ }
+ return sb.toString();
+ }
+
+ public void addError(String propertyName, String message) {
+ this.errors.add(new ErrorMessage(propertyName, message));
+ }
+ }
+
+ @Data
+ public static class ErrorMessage {
+
+ private String propertyPath;
+
+ private String message;
+
+ public ErrorMessage() {
+ }
+
+ public ErrorMessage(String propertyPath, String message) {
+ this.propertyPath = propertyPath;
+ this.message = message;
+ }
+ }
+
+}
+
替换if..else并不会降低代码的复杂度,相反比较少见的写法可能会增加认知负荷,从而进一步增加了复杂度。之所以要替换过多的if..else是为了对代码进行解耦合,方便扩展代码,最终方便对代码的维护。
+以下有几种常见的方法来替换过多的if..else。
+public class MyTest {
+ public static void main(String[] args) {
+ String val = "FOUR";
+ condition(val);
+ System.out.println("--------------");
+ newCondition(val);
+ }
+
+ public static void condition(String val) {
+ if ("ONE".equals(val)) {
+ System.out.println("val:" + 1111111);
+ } else if ("TWO".equals(val)) {
+ System.out.println("val:" + 2222222);
+ } else if ("THREE".equals(val)) {
+ System.out.println("val:" + 3333333);
+ } else {
+ System.out.println("val:" + val);
+ }
+ }
+
+ public static void newCondition(String val) {
+ ConditionEnum conditionEnum;
+ try {
+ conditionEnum = ConditionEnum.valueOf(val);
+ } catch (IllegalArgumentException e) {
+ System.out.println("val:" + val);
+ return;
+ }
+ exec(conditionEnum);
+ }
+
+ public static void exec(ConditionEnum conditionEnum) {
+ conditionEnum.context();
+ }
+
+ enum ConditionEnum {
+ ONE {
+ @Override
+ public void context() {
+ System.out.println("val:" + 1111111);
+ }
+ },
+ TWO {
+ @Override
+ public void context() {
+ System.out.println("val:" + 2222222);
+ }
+ },
+ THREE {
+ @Override
+ public void context() {
+ System.out.println("val:" + 3333333);
+ }
+ };
+
+ public abstract void context();
+ }
+
+}
+
也可用枚举替换成这样:
+ public static void newCondition(String val) {
+ ConditionEnum condition = ConditionEnum.getCondition(val);
+ System.out.println("val:" + (condition == null ? val : condition.value));
+ }
+
+ enum ConditionEnum {
+ ONE("ONE",1111111),
+ TWO("TWO",2222222),
+ THREE("THREE",3333333)
+
+ ;
+
+ private String key;
+ private Integer value;
+
+ ConditionEnum(String key,Integer value){
+ this.key = key;
+ this.value = value;
+ }
+
+ public static ConditionEnum getCondition(String key){
+ return Arrays.stream(ConditionEnum.values()).filter(x -> Objects.equals(x.key, key)).findFirst().orElse(null);
+ }
+ }
+
public class MyTest {
+ public static void main(String[] args) {
+ String val = "1";
+ condition(val);
+ System.out.println("--------------");
+ newCondition(val);
+ }
+ public static void condition(String val) {
+ if ("1".equals(val)){
+ System.out.println("val:" + 1111111);
+ }else if ("2".equals(val)){
+ System.out.println("val:" + 2222222);
+ }else if ("3".equals(val)){
+ System.out.println("val:" + 3333333);
+ }else {
+ System.out.println("val:" + val);
+ }
+ }
+ public static void newCondition(String val){
+ Map<String, Function<?,?>> map = new HashMap<>();
+ map.put("1",(action) -> 1111111);
+ map.put("2",(action) -> 2222222);
+ map.put("3",(action) -> 3333333);
+ System.out.println("val:" + (map.get(val) != null ? map.get(val).apply(null) : val));
+ }
+
+}
+
public class MyTest {
+ public static void main(String[] args) {
+ String val = "FOUR";
+ condition(val);
+ System.out.println("--------------");
+ newCondition(val);
+ }
+
+ public static void condition(String val) {
+ if ("ONE".equals(val)) {
+ System.out.println("val:" + 1111111);
+ } else if ("TWO".equals(val)) {
+ System.out.println("val:" + 2222222);
+ } else if ("THREE".equals(val)) {
+ System.out.println("val:" + 3333333);
+ } else {
+ System.out.println("val:" + val);
+ }
+ }
+
+ public static void newCondition(String val) {
+ switch (val) {
+ case "ONE":
+ System.out.println("val:" + 1111111);
+ break;
+ case "TWO":
+ System.out.println("val:" + 2222222);
+ break;
+ case "THREE":
+ System.out.println("val:" + 3333333);
+ break;
+ default:
+ System.out.println("val:" + val);
+ }
+ }
+}
+
public class MyTest {
+ public static void main(String[] args) {
+ String val = "222";
+ condition(val);
+ System.out.println("--------------");
+ newCondition(val);
+ }
+
+ public static void condition(String val) {
+ if ("ONE".equals(val)) {
+ System.out.println("val:" + 1111111);
+ } else if ("TWO".equals(val)) {
+ System.out.println("val:" + 2222222);
+ } else if ("THREE".equals(val)) {
+ System.out.println("val:" + 3333333);
+ } else {
+ System.out.println("val:" + val);
+ }
+ }
+
+ public static void newCondition(String val) {
+ if ("ONE".equals(val)) {
+ System.out.println("val:" + 1111111);
+ return;
+ }
+ if ("TWO".equals(val)) {
+ System.out.println("val:" + 2222222);
+ return;
+ }
+ if ("THREE".equals(val)) {
+ System.out.println("val:" + 3333333);
+ return;
+ }
+ System.out.println("val:" + val);
+ }
+}
+
public class MyTest {
+ public static void main(String[] args) {
+ String val = "1";
+ condition(val);
+ System.out.println("--------------");
+ newCondition(val);
+ }
+
+ public static void condition(String val) {
+ if ("ONE".equals(val)) {
+ System.out.println("val:" + 1111111);
+ } else if ("TWO".equals(val)) {
+ System.out.println("val:" + 2222222);
+ } else if ("THREE".equals(val)) {
+ System.out.println("val:" + 3333333);
+ } else {
+ System.out.println("val:" + val);
+ }
+ }
+
+ public static void newCondition(String val) {
+ One one = new One();
+ Two two = new Two();
+ Three three = new Three();
+
+ // 设置调用链,可设置成死循环
+ one.setAbstractHandler(two);
+ two.setAbstractHandler(three);
+
+ // 执行
+ one.exec(val);
+ }
+}
+
+abstract class AbstractHandler {
+
+ protected AbstractHandler abstractHandler;
+
+ protected void setAbstractHandler(AbstractHandler abstractHandler) {
+ this.abstractHandler = abstractHandler;
+ }
+
+ protected abstract void exec(String val);
+
+}
+
+class One extends AbstractHandler {
+
+ @Override
+ protected void exec(String val) {
+ if (!val.equals("ONE")){
+ abstractHandler.exec(val);
+ return;
+ }
+ System.out.println("val:" + 1111111);
+ }
+}
+
+class Two extends AbstractHandler {
+
+ @Override
+ protected void exec(String val) {
+ if (!val.equals("TWO")){
+ abstractHandler.exec(val);
+ return;
+ }
+ System.out.println("val:" + 2222222);
+ }
+}
+
+class Three extends AbstractHandler {
+
+ @Override
+ protected void exec(String val) {
+ System.out.println(val.equals("THREE") ? "val:" + 3333333 : "val:" + val);
+ }
+}
+
public class MyTest {
+ public static void main(String[] args) {
+ String val = "ONE";
+ condition(val);
+ System.out.println("--------------");
+ newCondition(val);
+ }
+
+ public static void condition(String val) {
+ if ("ONE".equals(val)) {
+ System.out.println("val:" + 1111111);
+ } else if ("TWO".equals(val)) {
+ System.out.println("val:" + 2222222);
+ } else if ("THREE".equals(val)) {
+ System.out.println("val:" + 3333333);
+ } else {
+ System.out.println("val:" + val);
+ }
+ }
+
+ public static void newCondition(String val) {
+ if (!Objects.equals(val, "ONE") && !Objects.equals(val, "TWO") && !Objects.equals(val, "THREE")) {
+ System.out.println("val:" + val);
+ return;
+ }
+ List<ConditionTemplate> list = new ArrayList<>(Arrays.asList(new One(), new Two(), new Three()));
+ for (ConditionTemplate item : list) {
+ item.template(val);
+ }
+ }
+
+}
+
+abstract class ConditionTemplate {
+
+ public void template(String val){
+ if (supportIns(val)){
+ support();
+ }
+ }
+
+ public abstract void support();
+
+ public abstract boolean supportIns(String val);
+
+}
+
+class One extends ConditionTemplate {
+ @Override
+ public void support() {
+ System.out.println("val:" + 1111111);
+ }
+ @Override
+ public boolean supportIns(String val) {
+ return "ONE".equals(val);
+ }
+}
+
+class Two extends ConditionTemplate {
+ @Override
+ public void support() {
+ System.out.println("val:" + 2222222);
+ }
+ @Override
+ public boolean supportIns(String val) {
+ return "TWO".equals(val);
+ }
+}
+
+class Three extends ConditionTemplate {
+ @Override
+ public void support() {
+ System.out.println("val:" + 3333333);
+ }
+ @Override
+ public boolean supportIns(String val) {
+ return "THREE".equals(val);
+ }
+}
+
public class MyTest {
+ public static void main(String[] args) {
+ String val = "ONE";
+ condition(val);
+ System.out.println("--------------");
+ newCondition(val);
+ }
+
+ public static void condition(String val) {
+ if ("ONE".equals(val)) {
+ System.out.println("val:" + 1111111);
+ } else if ("TWO".equals(val)) {
+ System.out.println("val:" + 2222222);
+ } else if ("THREE".equals(val)) {
+ System.out.println("val:" + 3333333);
+ } else {
+ System.out.println("val:" + val);
+ }
+ }
+
+ public static void newCondition(String val) {
+ Map<String, ConditionFactory> operationMap = new HashMap<>();
+ operationMap.put("ONE",new One());
+ operationMap.put("TWO",new Two());
+ operationMap.put("THREE",new Three());
+ if (operationMap.get(val) == null) {
+ System.out.println("val:" + val);
+ }else {
+ operationMap.get(val).printCondition();
+ }
+ }
+
+}
+
+interface ConditionFactory{
+ void printCondition();
+}
+
+class One implements ConditionFactory {
+ @Override
+ public void printCondition() {
+ System.out.println("val:" + 1111111);
+ }
+}
+
+class Two implements ConditionFactory {
+ @Override
+ public void printCondition() {
+ System.out.println("val:" + 2222222);
+ }
+}
+
+class Three implements ConditionFactory {
+ @Override
+ public void printCondition() {
+ System.out.println("val:" + 3333333);
+ }
+}
+
@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface PayCode {
+
+ String value();
+ String name();
+}
+
@PayCode(value = "alia", name = "支付宝支付")
+@Service
+public class AliaPay implements IPay {
+
+ @Override
+ public void pay() {
+ System.out.println("===发起支付宝支付===");
+ }
+}
+
+
+@PayCode(value = "weixin", name = "微信支付")
+@Service
+public class WeixinPay implements IPay {
+
+ @Override
+ public void pay() {
+ System.out.println("===发起微信支付===");
+ }
+}
+
+
+@PayCode(value = "jingdong", name = "京东支付")
+@Service
+public class JingDongPay implements IPay {
+
+ @Override
+ public void pay() {
+ System.out.println("===发起京东支付===");
+ }
+}
+
@Service
+public class PayService2 implements ApplicationListener<ContextRefreshedEvent> {
+
+ private static Map<String, IPay> payMap = null;
+
+ @Override
+ public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
+ ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext();
+ Map<String, Object> beansWithAnnotation = applicationContext.getBeansWithAnnotation(PayCode.class);
+
+ if (beansWithAnnotation != null) {
+ payMap = new HashMap<>();
+ beansWithAnnotation.forEach((key, value) ->{
+ String bizType = value.getClass().getAnnotation(PayCode.class).value();
+ payMap.put(bizType, (IPay) value);
+ });
+ }
+ }
+
+ public void pay(String code) {
+ payMap.get(code).pay();
+ }
+}
+
+ +
+ + + + + +与外部系统交互、本系统模块之间流程,比较好用的画圈软件draw .io或在线的process on
+从DDD角度界限上下文、ER图、评审表结构设计是否合理,表的关联关系是否合理、是否创建索引、是否大数据量表考虑放到分片库以及分片字段设计
+++无状态服务:客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份。服务端不保存任何客户端请求者信息 +有状态服务: 即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如 tomcat 中的 session
+
设计预案减少损失
++ +
+ + + + + +即 Queries Per Second的缩写,每秒能处理查询数目。是一台服务器每秒能够相应的查询次数,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。
+即 Transactions Per Second的缩写,每秒处理的事务数目。一个事务是指一个客户机向服务器发送请求然后服务器做出反应的过程。客户机在发送请求时开始计时,收到服务器响应后结束计时,以此来计算使用的时间和完成的事务个数,最终利用这些信息作出的评估分。
+即 Requests Per Second的缩写,每秒能处理的请求数目。等效于QPS。
+即 Page view,页面浏览量;用户每一次对网站中的每个页面访问均被记录1次。用户对同一页面的多次刷新,访问量累计。
+即 Unique visitor,独立访客;通过客户端的cookies实现。即同一页面,客户端多次点击只计算一次,访问量不累计。
+即 Response time 响应时间,处理一次请求所需要的平均处理时间。
+即 Return on Investment,也就是投资回报率(投入产出比),它是一个投资术语。ROI 对于工作而言,主要体现在:绩效晋升、技术能力。
+系统的吞吐量与请求对CPU的消耗、外部接口、IO等等紧密关联。单个请求对CPU消耗越高,外部系统接口、IO速度越慢,系统吞吐能力越低,反之越高。
+Code Review 翻译成中文是代码评审。Code Review 是一种通过复查代码提高代码质量的过程,通过这个机制我们可以对代码、测试过程和注释进行检查。
+重构的目的是使软件更容易被理解和修改。可以在软件内部做很多修改,但必须对软件可观察的外部行为只造成很小的变化,甚至不造成变化。
+全称Content Delivery Network,即内容分发网络。通过将内容缓存在终端用户附近,使用户可就近取得所需内容,解决 Internet网络拥挤的状况,提高用户访问网站的响应速度。
+CND加速主要是加速静态资源,如网站上面上传的图片、媒体,以及引入的一些Js、css等文件。 +CDN是只对网站的某一个具体的域名加速。如果同一个网站有多个域名,则访问加入CDN的域名获得加速效果,访问未加入CDN的域名,或者直接访问IP地址,则无法获得CDN效果。
+即Domain Name System,域名系统,是将域名解析为IP地址的系统。
++ +
+ + + + + +capacity
: 容量,默认16;loadFactor
: 负载因子,表示HashMap满的程度,默认值为0.75f,也就是说默认情况下,当HashMap中元素个数达到了容量的3/4的时候就会进行自动扩容;threshold
: 阈值;阈值 = 容量 * 负载因子
。默认12;在JDK1.8时,如果存储Map中数组元素对应的索引的每个链表超过8,就将单向链表转化为红黑树;当红黑树的节点少于6个的时候又开始使用链表。
+当有发生大量的hash冲突时,因为链表遍历效率很慢,为了提升查询的效率,所以使用了红黑树的数据结构。
+JDK文档注释:
+++Because TreeNodes are about twice the size of regular nodes, we use them only when bins contain enough nodes to warrant use (see TREEIFY_THRESHOLD). +And when they become too small (due to removal or resizing) they are converted back to plain bins.
+
单个 TreeNode 需要占用的空间大约是普通 Node 的两倍,所以只有当包含足够多的 Nodes 时才会转成 TreeNodes,而是否足够多就是由 TREEIFY_THRESHOLD 的值(默认值8)决定的。而当桶中节点数由于移除或者 resize 变少后,又会变回普通的链表的形式,以便节省空间,这个阈值是 UNTREEIFY_THRESHOLD(默认值6)。
+JDK1.8HashMap文档注释:
+++如果 hashCode 分布良好,也就是 hash 计算的结果离散好的话,那么红黑树这种形式是很少会被用到的,因为各个值都均匀分布,很少出现链表很长的情况。 +在理想情况下,链表长度符合泊松分布,各个长度的命中概率依次递减,当长度为 8 的时候,概率仅为 0.00000006。这是一个小于千万分之一的概率,通常我们的 Map 里面是不会存储这么多的数据的,所以通常情况下,并不会发生从链表向红黑树的转换。
+
HashMap是通过hash算法,来判断对象应该放在哪个桶里面的;JDK 并不能阻止我们用户实现自己的哈希算法,如果我们故意把哈希算法变得不均匀,那么每次存放对象很容易造成hash冲突。
+链表长度超过 8 就转为红黑树的设计,更多的是为了防止用户自己实现了不好的哈希算法时导致链表过长,从而导致查询效率低,而此时转为红黑树更多的是一种保底策略,用来保证极端情况下查询的效率。红黑树的引入保证了在大量hash冲突的情况下,HashMap还具有良好的查询性能。
+为了防止出现节点个数频繁在一个相同的数值来回切换。
+举个极端例子,现在单链表的节点个数是9,开始变成红黑树,然后红黑树节点个数又变成8,就又得变成单链表,然后节点个数又变成9,就又得变成红黑树,这样的情况消耗严重浪费。因此干脆错开两个阈值的大小,使得变成红黑树后“不那么容易”就需要变回单链表,同样,使得变成单链表后,“不那么容易”就需要变回红黑树。
+不一定,在进行树化之前会进行判断(n = tab.length) < MIN_TREEIFY_CAPACITY)
是否需要扩容,如果表中数组元素小于这个阈值(默认是64),就会进行扩容。 因为扩容不仅能增加表中的容量,还能缩短单链表的节点数,从而更长远的解决链表遍历慢问题。
/**
+ * Replaces all linked nodes in bin at index for given hash unless
+ * table is too small, in which case resizes instead.
+ */
+ final void treeifyBin(Node<K,V>[] tab, int hash) {
+ int n, index; Node<K,V> e;
+ if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
+ resize();
+ else if ((e = tab[index = (n - 1) & hash]) != null) {
+ TreeNode<K,V> hd = null, tl = null;
+ do {
+ TreeNode<K,V> p = replacementTreeNode(e, null);
+ if (tl == null)
+ hd = p;
+ else {
+ p.prev = tl;
+ tl.next = p;
+ }
+ tl = p;
+ } while ((e = e.next) != null);
+ if ((tab[index] = hd) != null)
+ hd.treeify(tab);
+ }
+ }
+
HashMap中的负载因子这个值现在在JDK的源码中默认是0.75:
+/**
+ * The load factor used when none specified in constructor.
+ */
+static final float DEFAULT_LOAD_FACTOR = 0.75f;
+
在JDK的官方文档中解释如下:
+++As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. +Higher values decrease the space overhead but increase the lookup cost (reflected in most of the operations of the HashMap class, including get and put). +The expected number of entries in the map and its load factor should be taken into account when setting its initial capacity, so as to minimize the number of rehash operations. +If the initial capacity is greater than the maximum number of entries divided by the load factor, no rehash operations will ever occur.
+
大意:一般来说,默认的负载因子(0.75)在时间和空间成本之间提供了很好的权衡。更高的值减少了空间开销,但增加了查找成本(反映在HashMap类的大多数操作中,包括get和put)。在设置映射的初始容量时,应该考虑映射中预期的条目数及其负载因子,以便最小化重哈希操作的数量。如果初始容量大于最大条目数除以负载因子,则不会发生重新散列操作。
+负载因子和hashmap中的扩容有关,当hashmap中的元素大于临界值(threshold = loadFactor * capacity
)就会扩容。所以负载因子的大小决定了什么时机扩容,扩容又影响到了hash碰撞的频率。所以设置一个合理的负载因子可以有效的避免hash碰撞。
设置为0.75的其他解释:
+log(2)
的时候比较合理;实际大小是16。其容量为不小于指定容量的2的幂数。
+为什么容量始终是2的N次方?
+为了减少Hash碰撞,尽量使Hash算法的结果均匀分布。
+当使用put方法时,到底存入HashMap中的那个数组中?这时是通过hash算法决定的,如果某一个数组中的链表过长旧会影响查询的效率;那么为了避免出现hash碰撞,让hash尽可能的散列分布,就需要在hash算法上做文章。
+JDK1.7通过逻辑与运算,来判断这个元素该进入哪个数组;在下面的代码中length的长度始终为不小于指定容量的2的幂数。
+static int indexFor(int h, int length) {
+ return h & (length - 1);
+}
+
为了更好的理解举个例子:假设h=2或h=3,length=15,进行与运算,最终逻辑与运算后的结果是一致的,因为最终结果是一致的所以就发生了hash碰撞,这种问题多了以后会造成容器中的元素分布不均匀,都分配在同一个数组上,在查询的时候就减慢了查询的效率,另一方面也造成空间的浪费。
+-- 2转换为2进制与15-1进行&运算
+ 0000 0010
+& 0000 1110
+————————————
+ 0000 1110
+
+-- 3转换为2进制与15-1进行&运算
+ 0000 0011
+& 0000 1110
+————————————
+ 0000 1110
+
为了避免上面length=15
这类问题出现,所以集合的容量采用必须是2的N次幂这种方式,因为2的N次幂的结果减一转换为二进制后都是以...1111
结尾的,所以在进行逻辑与运算时碰撞几率小。
在JDK1.8中,在putVal()
方法中通过i = (n - 1) & hash
来计算key的散列地址:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
+ boolean evict) {
+ // 此处省略了代码
+ // i = (n - 1) & hash]
+ if ((p = tab[i = (n - 1) & hash]) == null)
+
+ tab[i] = newNode(hash, key, value, null);
+
+
+ else {
+ // 省略了代码
+ }
+}
+
++这里的 “&” 等同于 %",但是"%“运算的速度并没有”&“的操作速度快;”&“操作能代替”%“运算,必须满足一定的条件,也就是
+a%b=a&(b-1)
仅当b是2的n次方的时候方能成立。
容器容量怎么保持始终为2的N次方?
+HashMap
的tableSizeFor()
方法做了处理,能保证n永远都是2次幂。
如果用户制定了初始容量,那么HashMap会计算出比该数大的第一个2的幂作为初始容量;另外就是在扩容的时候,也是进行成倍的扩容,即4变成8,8变成16。
+/**
+ * Returns a power of two size for the given target capacity.
+ */
+static final int tableSizeFor(int cap) {
+
+ // 假设n=17
+ // n = 00010001 - 00010000 = 00010000 = 16
+ int n = cap - 1;
+
+ // n = (00010000 | 00001000) = 00011000 = 24
+ n |= n >>> 1;
+
+ // n = (00011000 | 00000110) = 00011110 = 30
+ n |= n >>> 2;
+
+ // n = (00011110 | 00000001) = 00011111 = 31
+ n |= n >>> 4;
+
+ // n = (00011111 | 00000000) = 00011111 = 31
+ n |= n >>> 8;
+
+ // n = (00011111 | 00000000) = 00011111 = 31
+ n |= n >>> 16;
+
+ // n = 00011111 = 31,MAXIMUM_CAPACITY:Integer的最大长度
+ // (31 < 0) ? 1 : (31 >= Integer的最大长度) ? Integer的最大长度 : 31 + 1 ;
+ // 即最终返回 32 = 2 的 (n=5)次方
+ return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
+}
+
发现上面在进行>>>
操作时会将cap的二进制值变为最高位后边全是1,00010001 -> 00011111
这个算法就导致了任意传入一个数值,会将该数字变为它的2倍减1,因为任何尾数全为1的在加1都为2的倍数。
至于开头减1,是因为如果给定的n已经是2的次幂,但是不进行减1操作的话,那么得到的值就是大于给定值的最小2的次幂值,例如传入4就会返回8。
+为什么最大右移到16位,因为可以得到的最大值是32个1,这个是int类型存储变量的最大值,在往后就没意义了。
+没有找到相关解释,推断这应该就是个经验值,既然一定要设置一个默认的2^n 作为初始值,那么就需要在效率和内存使用上做一个权衡。这个值既不能太小,也不能太大。太小了就有可能频繁发生扩容,影响效率。太大了又浪费空间,不划算。所以,16就作为一个经验值被采用了。
+关于默认容量的定义:
+/**
+ * The default initial capacity - MUST be a power of two.
+ */
+static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
+
故意把16写成1 << 4
这种形式,就是提醒开发者,这个地方要是2的次幂。
当我们使用HashMap(int initialCapacity)
来初始化容量的时候,HashMap
并不会使用我们传进来的initialCapacity
直接作为初始容量。JDK会默认帮我们计算一个相对合理的值当做初始容量。所谓合理值,其实是找到第一个比用户传入的值大的2的幂。
如果创建hashMap初始化容量设置为7,那么JDK通过计算会创建一个初始化为8的hashMap。当hashMap中的元素到0.75 * 8 = 6
就会进行扩容,这明显是我们不希望看到的。
参考JDK8中putAll
方法中的实现:
(int) ((float) expectedSize / 0.75F + 1.0F);
+
通过expectedSize / 0.75F + 1.0F
计算,7/0.75 + 1 = 10
,10经过JDK处理之后,会被设置成16,这就大大的减少了扩容的几率。
当我们明确知道HashMap中元素的个数的时候,把默认容量设置成expectedSize / 0.75F + 1.0F
是一个在性能上相对好的选择,但是,同时也会牺牲些内存。
这个算法在guava中有实现,开发的时候,可以直接通过Maps类创建一个HashMap:
+Map<String, String> map = Maps.newHashMapWithExpectedSize(7);
+
public static <K, V> HashMap<K, V> newHashMapWithExpectedSize(int expectedSize) {
+ return new HashMap(capacity(expectedSize));
+}
+
+static int capacity(int expectedSize) {
+ if (expectedSize < 3) {
+ CollectPreconditions.checkNonnegative(expectedSize, "expectedSize");
+ return expectedSize + 1;
+ } else {
+ return expectedSize < 1073741824 ? (int)((float)expectedSize / 0.75F + 1.0F) : 2147483647;
+ }
+}
+
随着HashMap中的元素增加,Hash碰撞导致获取元素方法的效率就会越来越低,为了保证获取元素方法的效率,所以针对HashMap中的数组进行扩容。扩容数组的方式只能再去开辟一个新的数组,并把之前的元素转移到新数组上。
+++PS 如何能避免哈希碰撞?
++
+- 容量太小。容量小,碰撞的概率就高了。狼多肉少,就会发生争抢。
+- hash算法不够好。算法不合理,就可能都分到同一个或几个桶中。分配不均,也会发生争抢。
+
HashMap的扩容条件就是当HashMap中的元素个数(size)超过临界值(threshold)时就会自动扩容。在HashMap中,threshold = loadFactor * capacity
。默认情况下负载因子为0.75,理解为当容器中元素到容器的3/4时就会扩容。
if (++size > threshold)
+ resize();
+
HashMap的容量是有上限的,必须小于1<<30
,即1073741824
。如果容量超出了这个数,则不再增长,且阈值会被设置为Integer.MAX_VALUE
:
// Java8
+if (oldCap >= MAXIMUM_CAPACITY) {
+ threshold = Integer.MAX_VALUE;
+ return oldTab;
+}
+// Java7
+if (oldCapacity == MAXIMUM_CAPACITY) {
+ threshold = Integer.MAX_VALUE;
+ return;
+}
+
新容量 = 旧容量 * 2
新阈值 = 新容量 * 负载因子
void addEntry(int hash, K key, V value, int bucketIndex) {
+ //size:The number of key-value mappings contained in this map.
+ //threshold:The next size value at which to resize (capacity * load factor)
+ //数组扩容条件:1.已经存在的key-value mappings的个数大于等于阈值
+ // 2.底层数组的bucketIndex坐标处不等于null
+ if ((size >= threshold) && (null != table[bucketIndex])) {
+ resize(2 * table.length);//扩容之后,数组长度变了
+ hash = (null != key) ? hash(key) : 0;//为什么要再次计算一下hash值呢?
+ bucketIndex = indexFor(hash, table.length);//扩容之后,数组长度变了,在数组的下标跟数组长度有关,得重算。
+ }
+ createEntry(hash, key, value, bucketIndex);
+}
+
void resize(int newCapacity) { //传入新的容量
+ Entry[] oldTable = table; //引用扩容前的Entry数组
+ int oldCapacity = oldTable.length;
+ if (oldCapacity == MAXIMUM_CAPACITY) { //扩容前的数组大小如果已经达到最大(2^30)了
+ threshold = Integer.MAX_VALUE; //修改阈值为int的最大值(2^31-1),这样以后就不会扩容了
+ return;
+ }
+
+ Entry[] newTable = new Entry[newCapacity]; //初始化一个新的Entry数组
+ transfer(newTable); //!!将数据转移到新的Entry数组里
+ table = newTable; //HashMap的table属性引用新的Entry数组
+ threshold = (int) (newCapacity * loadFactor);//修改阈值
+}
+
通过transfer方法将旧数组上的元素转移到扩容后的新数组上
+void transfer(Entry[] newTable) {
+ Entry[] src = table; //src引用了旧的Entry数组
+ int newCapacity = newTable.length;
+ for (int j = 0; j < src.length; j++) { //遍历旧的Entry数组
+ Entry<K, V> e = src[j]; //取得旧Entry数组的每个元素
+ if (e != null) {
+ src[j] = null;//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象)
+ do {
+ Entry<K, V> next = e.next;
+ int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置
+ e.next = newTable[i]; //标记[1]
+ newTable[i] = e; //将元素放在数组上
+ e = next; //访问下一个Entry链上的元素
+ } while (e != null);
+ }
+ }
+}
+
容量变为原来的2倍,阈值也变为原来的2倍。容量和阈值都变为原来的2倍时,负载因子还是不变。
+在1.8时做了一些优化,文档注释写的很清楚:“元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置”。也就是对比1.7的迁移到新的数组上省去了重新计算hash值的时间。
+这里的"2次幂的位置"是指长度为原来数组元素的两倍的位置;举个例子,现在容量为16,要扩容到32,要将之前的元素迁移过去,要根据hash值来判断迁移过去的位置;假设元素A:hash值:0101 0101;根据代码h & (length - 1)
可得元素A & 15
、元素A & 31
扩容之前的位置:
+ 0101 0101
+& 0000 1111
+————————————
+ 0000 0101
+
+扩容之后的位置:
+ 0101 0101
+& 0001 1111
+————————————
+ 0001 0101
+
发现规律:扩容前的hash值和扩容后的hash值,如果元素A二进制形式第三位如果是0,扩容之后就还是原来的位置,如果是1扩容后就是原来的位置加16,而16就是扩容的大小。
+ /**
+ * Initializes or doubles table size. If null, allocates in
+ * accord with initial capacity target held in field threshold.
+ * Otherwise, because we are using power-of-two expansion, the
+ * elements from each bin must either stay at same index, or move
+ * with a power of two offset in the new table.
+ *
+ * @return the table
+ */
+ final Node<K,V>[] resize() {
+ Node<K,V>[] 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;
+ }
+ 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 { // zero initial threshold signifies using defaults
+ newCap = DEFAULT_INITIAL_CAPACITY;
+ newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
+ }
+ 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<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
+ table = newTab;
+ if (oldTab != null) {
+ for (int j = 0; j < oldCap; ++j) {
+ Node<K,V> 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<K,V>)e).split(this, newTab, j, oldCap);
+ else { // preserve order
+ Node<K,V> loHead = null, loTail = null;
+ Node<K,V> hiHead = null, hiTail = null;
+ Node<K,V> next;
+ do {
+ next = e.next;
+ if ((e.hash & oldCap) == 0) {
+ if (loTail == null)
+ loHead = e;
+ else
+ loTail.next = e;
+ loTail = e;
+ }
+ else {
+ if (hiTail == null)
+ hiHead = e;
+ else
+ hiTail.next = e;
+ hiTail = e;
+ }
+ } while ((e = next) != null);
+ if (loTail != null) {
+ loTail.next = null;
+ newTab[j] = loHead;
+ }
+ if (hiTail != null) {
+ hiTail.next = null;
+ newTab[j + oldCap] = hiHead;
+ }
+ }
+ }
+ }
+ }
+ return newTab;
+ }
+
+ +
+ + + + + +线上接口很慢,线上生产问题,我们绝对不能马虎放过抱着侥幸心理,必须要找到根本原因及时处理,防止下次留下更大的坑.大致思路要定位接口问题,然后具体问题具体分析,讨论不同解决方案.
+要快速定位接口哪一个环节比较慢,性能瓶颈在哪里,可以使用应用性能监控工具(APM)定位问题。常见工具: skywalking、pinpoint、cat、zipkin。
+如果应用程序没有接入APM,可以在生产环境装一下arthas,利用trace接口方法和火焰图,大概能分析是那一块比较慢,定位能力稍微有点粗糙。亦可以利用程序中的告警日志定位问题。
+如果是数据库sql慢,可以使用执行计划去分析一下,常见sql慢的几种情况:
++ +
+ + + + + +指多个线程按照申请锁的顺序来获取锁类似排队打饭 先来后到
+指在多线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取到锁,在高并发的情况下,有可能造成优先级反转或者饥饿现象
+并发包ReentrantLock
的创建可以指定构造函数的boolean类型来得到公平锁或者非公平锁 默认是非公平锁,synchronized
也是非公平锁.
+源码如下:
/**
+ * Creates an instance of {@code ReentrantLock}.
+ * This is equivalent to using {@code ReentrantLock(false)}.
+ */
+ public ReentrantLock() {
+ sync = new NonfairSync();
+ }
+
+ /**
+ * Creates an instance of {@code ReentrantLock} with the
+ * given fairness policy.
+ *
+ * @param fair {@code true} if this lock should use a fair ordering policy
+ */
+ public ReentrantLock(boolean fair) {
+ sync = fair ? new FairSync() : new NonfairSync();
+ }
+
指同一个线程外层函数获得锁之后,内层仍然能获取到该锁,在同一个线程在外层方法获取锁的时候,在进入内层方法或会自动获取该锁. +可重入锁最大的作用就是避免死锁
+所谓不可重入锁,即若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞
+举个栗子: 当你进入你家时门外会有锁,进入房间后厨房卫生间都可以随便进出,这个叫可重入锁.当你进入房间时,发现厨房,卫生间都有上锁.这个叫不可重入锁
+以下的代码证明 synchronized
和 ReentrantLock
都是可重入锁
public class SynchronziedDemo {
+
+ private synchronized void print() {
+ doAdd();
+ }
+ private synchronized void doAdd() {
+ System.out.println("doAdd...");
+ }
+
+ public static void main(String[] args) {
+ SynchronziedDemo synchronziedDemo = new SynchronziedDemo();
+ synchronziedDemo.print(); // doAdd...
+ }
+}
+
public class ReentrantLockDemo {
+ private Lock lock = new ReentrantLock();
+
+ private void print() {
+ lock.lock();
+ doAdd();
+ lock.unlock();
+ }
+
+ private void doAdd() {
+ // doAdd 方法中加两次锁和解两次锁也可以,不会报错
+ // 如果少了一个unlock()也不会报错,会形成死锁
+ lock.lock();
+ lock.lock();
+ System.out.println("doAdd...");
+ lock.unlock();
+ lock.unlock();
+ }
+
+ public static void main(String[] args) {
+ ReentrantLockDemo reentrantLockDemo = new ReentrantLockDemo();
+ reentrantLockDemo.print();
+ }
+}
+
自旋锁(spin lock)是一种非阻塞锁,也就是说,如果某线程需要获取锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗CPU的时间,不停的试图获取锁.这样的好处是减少线程上线文切换的消耗,缺点就是循环会消耗 CPU.原理是CAS原理
+public class SpinLock {
+ private AtomicReference<Thread> atomicReference = new AtomicReference<>();
+ private void lock () {
+ System.out.println(Thread.currentThread() + " coming...");
+ while (!atomicReference.compareAndSet(null, Thread.currentThread())) {
+ // loop
+ }
+ }
+
+ private void unlock() {
+ Thread thread = Thread.currentThread();
+ atomicReference.compareAndSet(thread, null);
+ System.out.println(thread + " unlock...");
+ }
+
+ public static void main(String[] args) throws InterruptedException {
+ SpinLock spinLock = new SpinLock();
+ new Thread(() -> {
+ spinLock.lock();
+ try {
+ Thread.sleep(3000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ System.out.println("hahaha");
+ spinLock.unlock();
+
+ }).start();
+
+ Thread.sleep(1);
+
+ new Thread(() -> {
+ spinLock.lock();
+ System.out.println("hehehe");
+ spinLock.unlock();
+ }).start();
+ }
+}
+
Thread[Thread-0,5,main] coming...
+Thread[Thread-1,5,main] coming...
+hahaha
+Thread[Thread-0,5,main] unlock...
+hehehe
+Thread[Thread-1,5,main] unlock...
+
指该锁一次只能被一个线程独占,所持有.对于synchronized
和ReentrantLock
而言都是独占锁
该锁可以被多个线程持有.对于 ReentrantLock
和 synchronized
都是独占锁;对与 ReentrantReadWriteLock
其读锁是共享锁而写锁是独占锁。
+读锁的共享可保证并发读是非常高效的,读写、写读和写写的过程是互斥的.
读写锁案例
+`ReentrantReadWriteLock 能保证读写、写读和写写的过程是互斥的时候是独享的,读读的时候是共享的
+class MyCache {
+
+ private volatile Map<String, Object> map = new HashMap<>();
+
+ private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
+
+ public void put(String key, Object value) {
+ rwLock.writeLock().lock();
+ try {
+ System.out.println("开始 写入 ..." + key);
+ map.put(key, value);
+ System.out.println("写入完成 ...");
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ rwLock.writeLock().unlock();
+ }
+
+ }
+
+ public Object get(String key) {
+ Object obj = null;
+ rwLock.writeLock().lock();
+ try {
+ System.out.println("开始读取 ..." + key);
+ obj = map.get(key);
+ System.out.println("读取完成 ..." + obj);
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ rwLock.writeLock().unlock();
+ }
+ return obj;
+ }
+
+}
+
+public class Test {
+
+ public static void main(String[] args) {
+ MyCache myCache = new MyCache();
+ for (int i = 0; i < 10; i++) {
+ int finalI = i;
+ new Thread(() -> {
+ myCache.put(finalI + "", finalI + "");
+ }, String.valueOf(i)).start();
+ }
+
+ System.out.println("---------------");
+
+ for (int i = 0; i < 10; i++) {
+ int finalI = i;
+ new Thread(() -> {
+ myCache.get(finalI + "");
+ }, String.valueOf(i)).start();
+ }
+ }
+}
+
开始 写入 ...0
+写入完成 ...
+开始 写入 ...1
+写入完成 ...
+开始 写入 ...2
+写入完成 ...
+开始 写入 ...3
+写入完成 ...
+开始 写入 ...4
+写入完成 ...
+开始 写入 ...6
+写入完成 ...
+开始 写入 ...5
+写入完成 ...
+开始 写入 ...7
+写入完成 ...
+开始读取 ...0
+读取完成 ...0
+开始读取 ...3
+读取完成 ...3
+开始读取 ...4
+读取完成 ...4
+开始读取 ...5
+读取完成 ...5
+开始读取 ...7
+读取完成 ...7
+开始读取 ...9
+读取完成 ...null
+开始 写入 ...8
+写入完成 ...
+开始读取 ...1
+读取完成 ...1
+开始读取 ...2
+读取完成 ...2
+开始读取 ...6
+读取完成 ...6
+开始读取 ...8
+读取完成 ...8
+开始 写入 ...9
+写入完成 ...
+
+ +
+ + + + + +MQ 即 messagequeue 消息队列,是分布式系统的重要组件,主要解决异步消息,应用解耦,消峰等问题。从而实现高可用,高性能,可伸缩和最终一致性的架构。使用较多的MQ有:activeMQ,rabbitMQ,kafka,metaMQ。
+特性 | +ActiveMQ | +RabbitMQ | +RocketMQ | +Kafka | +
---|---|---|---|---|
单机吞吐量 | +万级,比 RocketMQ、Kafka 低一个数量级 | +同 ActiveMQ | +10 万级,支撑高吞吐 | +10 万级,高吞吐,一般配合大数据类的系统来进行实时数据计算、日志采集等场景 | +
topic 数量对吞吐量的影响 | ++ | + | topic 可以达到几百/几千的级别,吞吐量会有较小幅度的下降,这是 RocketMQ 的一大优势,在同等机器下,可以支撑大量的 topic | +topic 从几十到几百个时候,吞吐量会大幅度下降,在同等机器下,Kafka 尽量保证 topic 数量不要过多,如果要支撑大规模的 topic,需要增加更多的机器资源 | +
时效性 | +ms 级 | +微秒级,这是 RabbitMQ 的一大特点,延迟最低 | +ms 级 | +延迟在 ms 级以内 | +
可用性 | +高,基于主从架构实现高可用 | +同 ActiveMQ | +非常高,分布式架构 | +非常高,分布式,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 | +
消息可靠性 | +有较低的概率丢失数据 | +基本不丢 | +经过参数优化配置,可以做到 0 丢失 | +同 RocketMQ | +
功能支持 | +MQ 领域的功能极其完备 | +基于 erlang 开发,并发能力很强,性能极好,延时很低 | +MQ 功能较为完善,还是分布式的,扩展性好 | +功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用 | +
一般的业务系统要引入 MQ,最早大家都用 ActiveMQ,但是现在确实大家用的不多了,没经过大规模吞吐量场景的验证,社区也不是很活跃,所以大家还是算了吧,我个人不推荐用这个了。
+后来大家开始用 RabbitMQ,但是确实 erlang 语言阻止了大量的 Java 工程师去深入研究和掌控它,对公司而言,几乎处于不可控的状态,但是确实人家是开源的,比较稳定的支持,活跃度也高。
+不过现在确实越来越多的公司会去用 RocketMQ,确实很不错,毕竟是阿里出品,但社区可能有突然黄掉的风险,目前 RocketMQ 已捐给 Apache,但 GitHub 上的活跃度其实不算高,对自己公司技术实力有绝对自信的,推荐用 RocketMQ,否则回去老老实实用 RabbitMQ 吧,人家有活跃的开源社区,绝对不会黄。
+所以中小型公司,技术实力较为一般,技术挑战不是特别高,用 RabbitMQ 是不错的选择;大型公司,基础架构研发实力较强,用 RocketMQ 是很好的选择。
+如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。
+JMS即JavaMessageService,Java消息服务应用程序接口,是一个Java平台中关于面向消息中间件的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。
+JMS是基于JVM的消息代理规范,ActiveMQ、HornetMQ等是JMS的实现。
+Java消息服务是一个与具体平台无关的API,绝大多数MOM提供商都对JMS提供支持。我们可以简单的理解:两个应用程序之间需要进行通信,我们使用一个JMS服务,进行中间的转发,通过JMS的使用,我们可以解除两个程序之间的耦合。
+消息发送者发送消息,消息代理将其放入消息队列中,消息接受者从队列中获取消息,消息读取后被移除消息队列。每个消息都被发送到一个特定的队列,接收者从队列中获取消息。队列保留着消息,直到它们被消费或超时。
+ +虽然可能有多个客户端在队列中侦听消息,但只有一个可以读取到消息,之后消息将不存在,其他消费者将无法读取。也就是说消息队列只有唯一一个发送者和接受者,但是并不能说只有一个接收者。
+特点:
+发布者将消息发送到主题Topic中,多个订阅者订阅这个主题,订阅者不断的去轮询监听消息队列中的消息,那么就会在消息到达的同时接收消息。
+ +特点:
+kafka是一个分布式的基于发布/订阅模式的消息队列,主要应用于大数据实时处理领域。
+消费者组的作用为了提高消费能力,即提高并发。
+解耦合是消息队列作用之一,当消费者宕机后,再次启动的时候会继续消费消息,而不是从头消费消息。因为这个特性所以消费者会保存一些消费的进度信息,被称为offset,保存的位置在kafka0.9之前保存在zookpeer当中,在此之后保存在kafka本地。即最终kafka会将消息保存在本地磁盘中,默认保留168个小时,即7天。
+kafka中消息是以topic进行分类的,producer生产消息,consumer消费消息,都是面向topic的。topic是逻辑上的概念,而partition是物理上的概念,每个partition对应于一个log文件,该log文件中存储的就是producer生产的数据。
+Producer生产的数据会被不断追加到该log文件末端,且每条数据都有自己的offset。consumer组中的每个consumer,都会实时记录自己消费到了哪个offset,以便出错恢复时,从上次的位置继续消费。
+消费者可以选择随时倒退或跳至所需的主题偏移量并阅读所有后续消息。
+参考文章:
+ + +由于生产者生产的消息会不断追加到log文件末尾,为防止log文件过大导致数据定位效率低下,kafka采取了分片和索引机制,将每个partition分为多个segment。 +每个segment对应两个文件:“.index”文件和“.log”文件。这些文件位于一个文件夹下,该文件夹的命名规则为:topic名称+分区序号。例如,first这个topic有三个分区,则其对应的文件夹为 first-0,first-1,first-2。其中,每个segment中的日志数据文件大小均相等。
+++该日志数据文件的大小可以通过在kafka Broker的config/server.properties配置文件的中的“log.segment.bytes”进行设置,默认为1G大小(1073741824字节),在顺序写入消息时如果超出该设定的阈值,将会创建一组新的日志数据和索引文件。
+
“.index”文件存储大量的索引信息,“.log”文件存储大量的数据,索引文件中的元数据指向对应数据文件中message的物理偏移地址。其中文件的命名是以第一个数据的偏移量来命名的。
+kafka如何通过index文件快速找到log文件中的数据?
+根据指定的偏移量,使用二分法查询定位出该偏移量对应的消息所在的分段索引文件和日志数据文件。然后通过二分查找法,继续查找出小于等于指定偏移量的最大偏移量,同时也得出了对应的position即实际物理位置。
+根据该物理位置在分段的日志数据文件中顺序扫描查找偏移量与指定偏移量相等的消息。由于index文件中的每条对应log文件中存储内容大小都相同,所以想要找到指定的消息,只需要用index文件中的该条的大小加上该条的偏移量即可得出log文件中指定消息的位置。
+ +分区的原因:
+为保证 producer 发送的数据,能可靠的发送到指定的 topic,topic 的每个 partition 收到 producer 发送的数据后,都需要向 producer 发送 ack(acknowledgement 确认收到),如果 producer 收到 ack,就会进行下一轮的发送,否则重新发送数据。
+ +方案 | +优点 | +缺点 | +
---|---|---|
半数以上完成同步,就发 送 ack | +延迟低 | +选举新的 leader 时,容忍 n 台 节点的故障,需要 2n+1 个副本 | +
全部完成同步,才发送ack | +选举新的 leader 时,容忍 n 台 节点的故障,需要 n+1 个副 本 | +延迟高 | +
++理解 2n+1: +半数以上完成同步才可以发ACK,如果挂了n台有副本的服务器,那么就需要有另外n台正常发送(这样正常发送的刚好是总数(挂的和没挂的)的一半(n(挂的)+n(正常的)=2n)),因为是半数以上所以2n+1.(所以总数2n+1的时候最多只能容忍n台有故障)
+即,如果挂了n台有副本的服务器,那么存在副本的服务器的总和为 2n+1
+
kafka选择了第二种方案,原因如下: +1.同样为了容忍 n 台节点的故障,第一种方案需要 2n+1 个副本,而第二种方案只需要 n+1 个副本,而 kafka 的每个分区都有大量的数据,第一种方案会造成大量数据的冗余; +2.虽然第二种方案的网络延迟会比较高,但网络延迟对 kafka 的影响较小;
+采用第二种方案之后,设想以下情景: leader 收到数据,所有 follower 都开始同步数据,但有一个 follower,因为某种故障,迟迟不能与 leader 进行同步,那 leader 就要一直等下去,直到它完成同步,才能发送 ack。这个问题怎么解决呢?
+Leader 维护了一个动态的 in-sync replica set 即ISR。
+当和 leader 保持同步的 follower 集合。当 ISR 中的 follower 完成数据的同步之后,就会给 leader 发送 ack。如果 follower长时间未向leader同步数据,则该follower将被踢出ISR,该时间阈值由replica.lag.time.max.ms参数设定。 Leader 发生故障之后,就会从 ISR 中选举新的 leader。
+++kafka是通过消息条数差值(replica.lag.time.max.messages) 加 通信时间长短(同步时间replica.lag.time.max.ms) 两个条件来选副本进ISR,在高版本中不再关注副本的消息条数最大条件。
+
为何会去掉消息条数差值参数?
+因为kafka一般是按batch批量发数据到leader, 如果批量条数12条,replica.lag.time.max.messages参数设置是10条,那么当一个批次消息发到kafka leader,此时,ISR中就要踢掉所有的follower,很快follower同步完所有数据后,follower又要被加入到ISR,而且要加入到zookeeper中频繁操作,所以去除掉该条件。
+LEO:(Log End offset)每个副本的最后一个offset;
+HW:(High Watermark)高水位,指的是消费者能见到的最大的 offset, ISR 队列中最小的 LEO;
+follower 故障:follower 发生故障后会被临时踢出 ISR,待该 follower 恢复后, follower 会读取本地磁盘记录的上次的 HW,并将 log 文件高于 HW 的部分截取掉,从 HW 开始向 leader 进行同步。等该 follower 的 LEO 大于等于该 Partition 的 HW,即 follower 追上 leader 之后,就可以重新加入 ISR 了。
+leader 故障:leader 发生故障之后,会从 ISR 中选出一个新的 leader,之后,为保证多个副本之间的数据一致性, 其余的 follower 会先将各自的 log 文件高于 HW 的部分截掉,然后从新的 leader同步数据;如果少于 leader 中的数据则会从 leader 中进行同步。
+对于某些不太重要的数据,对数据的可靠性要求不是很高,能够容忍数据的少量丢失,所以没必要等 ISR 中的 follower 全部接收成功。kafka为生产者提供了三种ack可靠性级别配置:
+将服务器的 ACK 级别设置为-1,可以保证 Producer 到 Server 之间不会丢失数据,即 At Least Once 语义,至少发送一次;相对的,将服务器 ACK 级别设置为 0,可以保证生产者每条消息只会被发送一次,即 At Most Once 语义,至多发送一次。
+至少发送一次可以保证数据不丢失,但是不能保证数据不重复;相对的,至多发送一次可以保证数据不重复,但是不能保证数据不丢失。 但是,对于一些非常重要的信息,比如说交易数据,下游数据消费者要求数据既不重复也不丢失,即 Exactly Once 语义,准确发送一次。
+在0.11 版本的 Kafka,引入了一项重大特性:幂等性。所谓的幂等性就是指生产者不论向 Server 发送多少次重复数据, Server 端都只会持久化一条。幂等性结合 At Least Once 语义,就构成了 Kafka 的 Exactly Once 语义。
+要启用幂等性,只需要将 Producer 的参数中 enable.idempotence
设置为 true 即可。
Kafka的幂等性实现其实就是将原来下游需要做的去重放在了数据上游。开启幂等性的 Producer 在初始化的时候会被分配一个 PID,发往同一 Partition 的消息会附带 Sequence Number。而Broker 端会对<PID, Partition, SeqNumber>做缓存,当具有相同主键的消息提交时, Broker 只会持久化一条。但是 PID 重启就会变化,同时不同的 Partition 也具有不同主键,所以幂等性无法保证跨分区跨会话的 Exactly Once。
+kafka的生产者发送消息采用的是异步发送的方式。在消息发送的过程中,涉及到了两个线程——main 线程和 Sender 线程,以及一个线程共享变量——RecordAccumulator。
+ +在生产者发送消息时,main线程将消息发送给 RecordAccumulator,当数据积累到 batch.size 之后,sender线程才会不断从 RecordAccumulator 中拉取消息发送到 kafka的broker;如果数据迟迟未达到 batch.size,sender 线程等待 linger.time 之后就会发送数据。
+一个消费者组中有多个消费者,一个主题有多个分区,所以必然会涉及到分区的分配问题,即确定哪个个分区由哪个消费者来消费。
+消费分配策略:
+轮询就不必说了,就是把分区按照hash排序,然后分配。range按范围分配,先将所有的分区放到一起然后排序,按照平均分配的方式计算每个消费者会得到多少个分区,如果没有除尽,则会将多出来的分区依次计算到前面几个消费者。 +比如这里是三个分区和两个消费者,那么每个消费者至少会得到1个分区,而3除以2后还余1,那么就会将多余的部分依次算到前面几个消费者,也就是这里的1会分配给第一个消费者,
+如果按照Range分区方式进行分配,其本质上是依次遍历每个topic,然后将这些topic的分区按照其所订阅的消费者数量进行平均的范围分配。这种方式从计算原理上就会导致排序在前面的消费者分配到更多的分区,从而导致各个消费者的压力不均衡。
+消费者在消费的时候,需要维护一个offset,用于记录消费的位置,当offset提交时会有两个问题:重复消费和漏消费:
+这里就需要注意offset的提交方式,offset默认是自动提交,当然这会造成消费的不准确。offset提交方式:
+建议将offset保存在数据库中,使当前业务与offset提交绑定起来,这样可以一定程度避免重复消费问题,重复消费的问题,一方面需要消息中间件来进行保证。另一方面需要自己的处理逻辑来保证消息的幂等性。
+kafka从0.11版本开始引入了事务支持。事务可以保证kafka在Exactly Once语义的基础上,生产和消费可以跨分区和会话,要么全部成功,要么全部失败。
+++为了管理 Transaction, Kafka 引入了一个新的组件 Transaction Coordinator。 Producer 就是通过和 Transaction Coordinator 交互获得 Transaction ID 对应的任务状态。 Transaction Coordinator 还负责将事务所有写入 Kafka 的一个内部 Topic,这样即使整个服务重启,由于事务状态得到保存,进行中的事务状态可以得到恢复,从而继续进行。
+
如果线上遇到大量消息积压,那就是线上故障了.最可能是消费者出现故障
+一般这个时候,只能临时紧急扩容了:
+如果没有消费者没有出现问题,而出现了消费积压的情况可以参考以下思路:
++ +
+ + + + + +++摘自《重构:改善既有代码的设计》
++
+- 重构(名词形式): 对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
+- 重构(动词形式): 使用一些列重构手法,在不改变软件可观察行为的前提下,调整其结构。
+重构的目的是使软件更容易被理解和修改。可以在软件内部做很多修改,但必须对软件可观察的外部行为只造成很小的变化,甚至不造成变化。与之形成对比的是性能优化,和重构一样,性能优化通常不会改变组件的行为,只会改变其内部结构。但是两者的出发点不同:性能优化往往使代码较难理解,但为了得到所需的性能你不得不这么做。
+重构不会改变软件可观察的行为,重构之后的软件功能一往如此。
+
重构代码更像是整理代码,重构可以重构但是不要改变代码原本的功能,需要修改其内部结构,在不改变软件可观测行为的前提下,调整代码结构,当然这种修改肯定是有利的一面。提高软件的可理解性,降低变更成本。
+首先需要明确的一点是,重构是一种经济适用性行为,而非道德使然,重构只有一个目的,就是让我们更快更好的开发代码,为什么需要重构,是因为现有的代码不能够提高开发效率,有时不但不能提高,还会降低很多很多。
+其次重构是保证代码质量的一个极其有效的手段,不至于让代码腐化到无可救药的地步。项目在演进,代码不停地在堆砌。如果没有人为代码的质量负责任,代码总是会往越来越混乱的方向演进。当混乱到一定程度之后,量变引起质变,项目的维护成本已经高过重新开发一套新代码的成本,想要再去重构,已经没有人能做到了。
+当我们遇到这段代码实在是太恶心了,花了很长时间才看懂,并且代码非常僵硬,而正好这个需求需要改动到这里,代码真的就像一坨屎。而最后是怎么处理的,我们通常是又给它加了一坨。
+我们为什么会这么去做?因为重构会减慢当前任务速度,所以保持最快速度。
+为什么这么做能干得又多又快?因为他将成本放到了未来。软件工程最大的成本在于维护,我们每一次代码的改动,都应该是对历史代码的一次整理,而非单一的功能堆积。 +这样虽然能赢得现在,但终将失去未来,而这个失败的未来或许需要全团队与他一起买单。 这或许就是我们重构的最根本原因。
+优秀的代码或架构不是一开始就能完全设计好的,就像优秀的公司和产品也都是迭代出来的。所以当我们做一个需求一定要回过头整理代码,保证代码的质量。傻瓜都能写出计算机可以理解的代码。唯有能写出人类容易理解的代码的,才是优秀的程序员。
+除此之外重构对程序员本身的意义:
+因为重构是一种经济实用性行为,所以重构的时候需要考虑成本,仅对必要的代码进行重构,某个工作行为如果重复三次就可以认为未来也会存在重复,因此通过重构使得下次工作更加高效,这是一种务实的作法。
+举例,在添加一个新功能时,看之前的旧代码,如果有一些公共的功能可以提取出来,而这个功能,你新开发的功能又恰好用的上,那么重构这个功能可以帮助你梳理、理解代码,程序如果如此迭代下去,长久下来会提高团队的开发效率。
+在《重构:改善既有代码的设计》一书中提到持续重构的理念。重构本来就不是一件应该特别拨出时间来做的事,重构应该随时随地的进行。重构不一定是需要大规模的展开的任务,重构应该是不断持续进行的,将任务拆解为多个具有完备性的任务,每周完成一个,每个任务的上线都不会引起问题,并使项目变得更好,这是一种持续重构的精神态度,是高效能程序员最应该具有的工作习惯。
+不要代码烂到一定程度之后才去重构。当代码真的烂到出现开发效率低,招了很多人,天天加班,出活却不多,线上 bug 频发,领导发飙,中层束手无策,工程师抱怨不断,查找 bug 困难的时候,基本上重构也无法解决问题了。
+我们要正确地看待代码质量和重构这件事情。技术在更新、需求在变化、人员在流动,代码质量总会在下降,代码总会存在不完美,重构就会持续在进行。应当时刻具有持续重构意识,才能避免开发初期就过度设计,避免代码维护的过程中质量的下降。
+通读代码,分析现状,找到可重构代码。列出重构计划,确定重构目标,不要盲目的重构,明确的描述出重构后能达到的预期是什么。重构计划中必须给出测试验证方案,保证代码的可测试性,保证重构前与重构后软件的行为一致。 +将重构任务当作项目来管理,对指定任务的人明确的排期和进度同步。这是重构大体上基本的步骤。
+在重构项目时,我们可以借鉴分治思想,将重构任务拆分成每周都能见到效果的小任务,形成正反馈,定期开会同步进度,不断加强团队的重构意识。
+下面是重构代码的一些常见问题和重构的一些建议
+见注释命名
+++所谓复杂性,就是任何使得软件难于理解和修改的因素。
+
模糊性与依赖性是引起复杂性的2个主要因素,模糊性产生了最直接的复杂度,让我们很难读懂代码真正想表达的含义,无法读懂这些代码,也就意味着我们更难去改变它。而依赖性又导致了复杂性不断传递,不断外溢的复杂性最终导致系统的无限腐化,一旦代码变成意大利面条,几乎不可能修复,成本将成指数倍增长。
+我们可以找到很多因素导致系统腐化的原因:
+除了上述内容外,还可以想到很多理由。但我们发现他们好像有一个共同的指向点 - 软件工程师,似乎所有复杂的源头就是软件工程师的不合格导致,所以其实一些罪恶的根因是我们自己?
+++软件复杂才是常态,不复杂才不正常。软件的复杂性是固有的,包括问题域的复杂性、管理开发过程的困难性、通过软件可能实现的灵活性与刻画离散系统行为的问题,这4个方面来分析了软件的发展一定伴随着复杂,这是软件工程这本科学所必然伴随的一个特性。 +所以所有的软件架构万变不离其宗,都在致力解决软件的复杂性。
+
++世间万物都需要额外的能量和秩序来维持自身,无一例外。没有外部力量的注入事物就会逐渐崩溃,这是世间万物的规律,而非我们哪里做得不对。所以我们才需要对系统进行维护甚至重构,降低系统的复杂度来保障系统的正常运行及降低维护的成本。
+
过度设计就是增加复杂度的一种。
+在初学编程的时候,只管埋头写程序,浑浑噩噩的进行开发。然而很快便发现事先做好设计可以节省返工的成本。许多人把设计看作软件开发的关键环节,而把编程看成是机械式的劳动。他们认为设计就像画工程图纸而编码就像施工。 +因此需要把更更多得精力放在预先设计上,以免日后修改。
+但是当过分的考虑程序未来所要面对的需求时,将陷入过度设计的陷阱,为了当下用不上的能力,而使程序变得复杂,这些设计很可能是现在未来都不需要的。
+过度设计是一种负面的设计,所以要合理的进行设计,避免过度设计,把握好设计的尺度,适度设计。下面是避免过度设计的几点建议:
+要明确一点,设计是产品实现用户需求之间的一种手段,不是最终的目的,不能为了设计而设计,添加一些乱七八糟的功能,那样产品的用户体验也是很差的。在设计中应当确保应用程序的核心功能得到优先关注,避免过度边缘化。
+这是重构所面临最多的借口,是自己也是团队的借口。
+为此必须要明确重构是经济行为而不是一种道德行为,重构使得开发效率变得更高,因此仅对必要的代码进行重构,某个工作行为如果重复三次就可以认为未来也会存在重复,因此通过重构使得下次工作更加高效,这是一种务实的作法,而重构不一定是需要大规模的展开的任务,重构应该是不断持续进行的,将任务拆解为多个具有完备性的任务,每周完成一个,每个任务的上线都不会引起问题,并使项目变得更好,这是一种持续重构的精神态度,是高效能程序员最应该具有的工作习惯。
+如果你在给项目添加新的特性,发现当前的代码不能高效的完成这个任务,并且同样的任务出现三次以上,那么这时你应该先重构,再开发新特性。
++ +
+ + + + + +参考文章:
+Redis(Remote Dictionary Server) Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API 的非关系型数据库。
+简而言之,Redis是一个可基于内存亦可持久化的日志型、Key-Value非关系型数据库。
+非关系型数据库,简称NoSql,是Not Only SQL 的缩写,是对不同于传统的关系型数据库的数据库管理系统的统称,泛指非关系型的数据库。 +NoSql 不依赖业务逻辑方式存储,而以简单的key-value模式存储。因此大大的增加了数据库的扩展能力。
+NoSql特点:
+NoSql适用场景:
+NoSql不适用场景:
+传统数据库遵循 ACID 规则。而 Nosql 一般为分布式,而分布式一般遵循 CAP 定理。
+Redis 默认16个数据库,类似数组下标从0开始,初始默认使用0号库。可使用命令 select <dbid>
来切换数据库。如: select 8
。
Redis是单线程+多路IO复用技术
+多路复用: +指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行,比如使用线程池。
+Redis与原子性: +所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。 +在单线程中,能够在单条指令中完成的操作都可以认为是"原子操作",因为中断只能发生于指令之间;在多线程中,不能被其它进程或线程打断的操作就叫原子操作; +Redis命令的原子性主要得益于Redis的单线程机制。
+Redis使用场景:
+Redis特点:
+Redis优点:
+Redis缺点:
+++如果你使用docker可以先启动Redis容器再用
+docker exec -it <容器ID> redis-cli
命令进入redis客户端。
基础命令:
+keys *
exists <key>
type <key>
del <key>
unlink <key>
expire key <time>
ttl <key>
select <dbid>
dbsize
flushdb
flushall
更多命令详见:
+ +Redis 可以存储键和不同类型的值之间的映射。键的类型只能为字符串,值常见有五种数据类型:字符串、列表、集合、散列表、有序集合,Redis后续的更新中又新添加了位图、地理位置等数据类型。
+名称 | +使用场景 | +
---|---|
string-字符串 | +作为常规的key-value缓存应用 | +
hash-哈希 | +主要用来存储对象信息 | +
list-列表 | +缓存一些列表数据:关注列表、粉丝列表等 | +
set-集合 | +去重;提供了求交集、并集、差集等操作,可以用来做共同关注、共同好友 | +
sorted set-有序集合 | +用来做排行榜 | +
bitmaps-位图 | +可以用来统计状态,如日活是否浏览过某个东西 | +
hyperloglog-基数统计 | +用来做统计独立IP数、搜索记录数 | +
geospatial-地理位置 | +可以用来做附近的人、地图的一些推送接口 | +
String是Redis最基本的类型,一个key对应一个value。String类型是二进制安全的。意味着Redis的string可以包含任何数据。比如jpg图片或者序列化的对象。String类型是Redis最基本的数据类型,一个Redis中字符串value最多可以是512M。
+ +String的数据结构为简单动态字符串(Simple Dynamic String,缩写SDS)。是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配.
+如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。
+常用命令:
+set <key> <value>
get <key>
append <key> <value>
strlen <key>
setnx <key> <value>
incr <key>
decr <key>
incrby/decrby <key><步长>
Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边),单键多值。它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。
+ +List的数据结构为快速链表quickList。首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。当数据量比较多的时候才会改成quicklist。
+常用命令:
+lpush/rpush <key><value1><value2><value3>
lpop/rpop <key>
rpoplpush <key1><key2>
0 -1
表示获取所有:lrange <key><start><stop>
llen <key>
linsert <key> before <value><newvalue>
lrem <key><n><value>
lset <key><index><value>
Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动去重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。
+Redis的Set是string类型的无序集合。它底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是O(1)。
+++复杂度O(1):数据增加,查找数据的时间不变。
+
Set数据结构是dict字典,字典是用哈希表实现的。在Java中HashSet的内部实现使用的是HashMap,只不过所有的value都指向同一个对象。Redis的set结构也是一样,它的内部也使用hash结构,所有的value都指向同一个内部值。
+常用命令:
+sadd <key><value1><value2>
smembers <key>
sismember <key><value>
scard<key>
srem <key><value1><value2>
spop <key>
srandmember <key><n>
smove <source Key><destination Key><value>
sinter <key1><key2>
sunion <key1><key2>
sdiff <key1><key2>
Redis hash 是一个键值对集合。Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。类似Java里面的Map<String,Object>。
+Hash类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。当field-value长度较短且个数较少时,使用ziplist,否则使用hashtable。
+常用命令:
+hset <key><field><value>
hget <key1><field>
hmset <key1><field1><value1><field2><value2>...
hexists<key1><field>
hkeys <key>
hvals <key>
hincrby <key><field><increment>
hsetnx <key><field><value>
sorted set 有序集合也称为 zset,Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。不同之处是有序集合的每个成员都关联了一个评分(score),这个评分被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复了 。
+因为元素是有序的, 所以你也可以很快的根据评分(score)或者次序(position)来获取一个范围的元素。 +访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。
+SortedSet(zset)是Redis提供的一个非常特别的数据结构,一方面它等价于Java的数据结构Map<String, Double>,可以给每一个元素value赋予一个权重score,另一方面它又类似于TreeSet,内部的元素会按照权重score进行排序,可以得到每个元素的名次,还可以通过score的范围来获取元素的列表。
+zset底层使用了两个数据结构:
+++跳跃表:
+
+有序集合在生活中比较常见,例如根据成绩对学生排名,根据得分对玩家排名等。对于有序集合的底层实现,可以用数组、平衡树、链表等。数组不便元素的插入、删除;平衡树或红黑树虽然效率高但结构复杂;链表查询需要遍历所有效率低。Redis采用的是跳跃表。跳跃表效率堪比红黑树,实现远比红黑树简单。 +
+举例:对比有序链表和跳跃表,从链表中查询出51有序链表: 查找值为51的元素,需要从第一个元素开始依次查找、比较才能找到;共需要6次比较。 +
+跳跃表:从第2层开始,1节点比51节点小,向后比较;21节点比51节点小,继续向后比较,后面就是NULL了,所以从21节点向下到第1层;在第1层,41节点比51节点小,继续向后,61节点比51节点大,所以从41向下;在第0层,51节点为要查找的节点,节点被找到,共查找4次。 +
+可以看出跳跃表比有序链表效率要高。
+
常用命令:
+zadd <key><score1><value1><score2><value2>...
zrange <key><start><stop> [WITHSCORES]
zrangebyscore key minmax [withscores] [limit offset count]
zincrby <key><increment><value>
zrem <key><value>
zcount <key><min><max>
zrank <key><value>
Bitmaps 并不是一种数据结构,实际上它就是字符串,但是可以对字符串的位进行操作。
+++bit(位)简介:
+
+现代计算机用二进制(位) 作为信息的基础单位, 1个字节等于8位, 例如“abc”字符串是由3个字节组成, 但实际在计算机存储时将其用二进制表示, “abc”分别对应的ASCII码分别是97、 98、 99, 对应的二进制分别是01100001、 01100010和01100011。
Bitmaps单独提供了一套命令, 所以在Redis中使用Bitmaps和使用字符串的方法不太相同。 可以把Bitmaps想象成一个以位为单位的数组, 数组的每个单元只能存储0和1, 数组的下标在Bitmaps中叫做偏移量。
+ +常用命令:
+setbit <key><offset><value>
getbit <key><offset>
bitcount <key>[start end]
虽然使用位操作能够极大提高内存使用效率,但也并非总是如此,合理地使用操作位能才能够有效地提高内存使用率和开发效率。
+假设网站有1亿用户, 每天独立访问的用户有5千万, 如果每天用集合类型和Bitmaps分别存储活跃用户可以得到表:
+Set和Bitmaps存储一天活跃用户对比:
++++
+- Set每个用户id占用空间:假设用户id用的是long存储占8字节,所以Set用户id占用空间是8*8bit=64bit。
+- Bitmaps需要存储的用户量:因为Bitmaps 并不是一种数据结构,实际上是字符串,所以在存储的时候需要存储全部的用户,此处为1亿。
+
数据类型 | +每个用户id占用空间 | +需要存储的用户量 | +全部内存量 | +
---|---|---|---|
Set | +64bit | +50000000 | +64位*50000000 = 400MB | +
Bitmaps | +1bit | +100000000 | +1位*100000000 = 12.5MB | +
但Bitmaps并不是万金油,假如该网站每天的独立访问用户很少,例如只有10万,这时候使用Bitmaps就不太合适了,两者的对比如下表所示:
+数据类型 | +每个用户id占用空间 | +需要存储的用户量 | +全部内存量 | +
---|---|---|---|
Set | +64位 | +100000 | +64位*100000 = 800KB | +
Bitmaps | +1位 | +100000000 | +1位*100000000 = 12.5MB | +
Redis 在 2.8.9 版本添加了 HyperLogLog 结构。Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
+++HyperLogLog中的基数:比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 不重复元素为5个,5就是基数。 基数估计就是在误差可接受的范围内,快速计算基数。
+
在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
+但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
+常用命令:
+pfadd <key>< element> [element ...]
pfcount<key> [key ...]
pfmerge <destkey><sourcekey> [sourcekey ...]
GEO,Geographic,地理信息的缩写。该类型,就是元素的2维坐标,在地图上就是经纬度。redis基于该类型,提供了经纬度设置、查询、范围查询、距离查询、经纬度Hash等常见操作。
+常用命令:
+geoadd <key>< longitude><latitude><member> [longitude latitude member...]
++两极无法直接添加,一般会下载城市数据,直接通过 Java 程序一次性导入。有效的经度从 -180 度到 180 度。有效的纬度从 -85.05112878 度到 85.05112878 度。当坐标位置超出指定范围时,该命令将会返回一个错误。已经添加的数据,是无法再次往里面添加的。
+
geopos <key><member> [member...]
geodist <key><member1><member2> [m|km|ft|mi ]
georadius <key><longitude><latitude><radius><m|km|ft|mi> [withcoord]
Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。Redis 客户端可以订阅任意数量的频道。
+客户端可以订阅频道:
+ +给这个频道发布消息后,消息就会发送给订阅的客户端:
+ +实现Redis发布订阅模式:
+打开两个Redis客户端;
+在其中一个客户端输入:subscribe <channel>
,channel为订阅的频道名称:
+
在另外一个客户端输入:publish <channel> <message>
,channel为订阅的频道名称,message为推送的消息,返回的1是订阅者数量:
+
Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
+Redis事务的主要作用就是串联多个命令防止别的命令插队。
+Redis事务操作相关命令:Multi、Exec、discard
+ +从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。组队的过程中可以通过discard来放弃组队。 +可以将exec命令理解为提交操作,discard理解为回滚操作。
+multi,exec组队成功,提交成功举例:
+127.0.0.1:6379> multi
+OK
+127.0.0.1:6379(TX)> set k1 v1
+QUEUED
+127.0.0.1:6379(TX)> set k2 v2
+QUEUED
+127.0.0.1:6379(TX)> set k3 v3
+QUEUED
+127.0.0.1:6379(TX)> exec
+1) OK
+2) OK
+3) OK
+
multi,discard组队失败举例:
+127.0.0.1:6379> multi
+OK
+127.0.0.1:6379(TX)> set k1 v1
+QUEUED
+127.0.0.1:6379(TX)> set k2 v2
+QUEUED
+127.0.0.1:6379(TX)> set k3 v3
+QUEUED
+127.0.0.1:6379(TX)> discard
+OK
+
如果在组队过程中执行某个命令失败了,则认为组队失败整个队列都会被取消:
+ +127.0.0.1:6379> multi
+OK
+127.0.0.1:6379(TX)> set k1 v1
+QUEUED
+127.0.0.1:6379(TX)> set k2
+(error) ERR wrong number of arguments for 'set' command
+127.0.0.1:6379(TX)> exec
+(error) EXECABORT Transaction discarded because of previous errors.
+
如果执行阶段某个命令出了错,则只有报错的命令不会被执行,而其他的命令都会执行且不会回滚:
+ +127.0.0.1:6379> multi
+OK
+127.0.0.1:6379(TX)> set k1 v1
+QUEUED
+127.0.0.1:6379(TX)> incr k1
+QUEUED
+127.0.0.1:6379(TX)> set k2 v2
+QUEUED
+127.0.0.1:6379(TX)> exec
+1) OK
+2) (error) ERR value is not an integer or out of range
+3) OK
+
为什么要使用事务?
+想想一个场景:有很多人有你的账户,同时去参加抢购。一个请求想给金额减8000、一个请求想给金额减5000、一个请求想给金额减1000。
+如果不加事务,有可能一个线程在减8000后,还没有写入数据库,此时另一个线程执行减5000操作写入数据库,然后再将减8000的操作写入数据库,就会造成数据异常。针对于高并发这种情况下我们就需要用事务来控制,可以用加锁来处理。
+在Redis中可以使用悲观锁、乐观锁的思想来处理。
+悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
+悲观锁实现:在Redis中没有悲观锁,但是可以通过调用lua脚本来实现。
+++Lua 是一个小巧的脚本语言,Lua脚本可以很容易的被C/C++ 代码调用,也可以反过来调用C/C++的函数,Lua并没有提供强大的库,一个完整的Lua解释器不过200k,所以Lua不适合作为开发独立应用程序的语言,而是作为嵌入式脚本语言。 +很多应用程序、游戏使用LUA作为自己的嵌入式脚本语言,以此来实现可配置性、可扩展性。
+
LUA脚本在Redis中的优势:
+乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。
+乐观锁实现:在Redis中如果涉及到了操作数据需要用事务控制的情况可以用 WATCH key … 命令来监控,可以监控多个key。如果在事务执行之前这个key被其他命令所改动,那么事务将被打断。
+ +UNWATCH 取消 WATCH 命令对所有 key 的监视。如果在执行 WATCH 命令之后,EXEC 命令或DISCARD命令先被执行了的话,那么就不需要再执行UNWATCH了。
+参考文章:
+因为Redis操作的是纯内存所以性能极高,常用来做缓存,来存放一些热点数据。
+用户第一次访问数据库中的某些数据。整个过程会比较慢,因为是从硬盘上读取的。如果将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快,但随之而来的也会存在一些问题。
+Redis中的key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会压到数据源,从而可能压垮数据源。简单理解为,避开缓存,疯狂请求数据库里没有的数据.从而造成服务器宕机。
+解决方法
+ @Resource
+ private RedisTemplate<Long, String> redisTemplate;
+
+ @Resource
+ private TestMapper testMapper;
+
+ public Test findById(Long id) {
+ String testStr = redisTemplate.opsForValue().get(id);
+ //判断缓存是否存在,是否为空对象
+ if (StringUtils.isEmpty(testStr)) {
+ // 这里的锁,使用的时候注意锁的力度,这里建议换成分布式锁,这里做演示
+ synchronized (TestServiceImpl.class){
+ testStr = redisTemplate.opsForValue().get(id);
+ if (StringUtils.isEmpty(testStr)) {
+ Test test = testMapper.findById(id);
+ if(test == null){
+ //构建一个空对象
+ test= new Test();
+ }
+ testStr = JSON.toJSONString(test);
+ // 存入Redis中,下次再次访问就不访问数据库
+ redisTemplate.opsForValue().set(id, testStr);
+ }
+ }
+ }
+ Test test = JSON.parseObject(testStr, Test.class);
+ //空对象处理
+ if(test.getId() == null){
+ return null;
+ }
+ return JSON.parseObject(testStr, Test.class);
+ }
+
@Resource
+ private TestMapper testMapper;
+
+ @Resource
+ private RedisTemplate<Long, String> redisTemplate;
+
+ private static BloomFilter<Long> bloomFilter = BloomFilter.create(Funnels.longFunnel(), 1000000000L);
+
+ @Override
+ public Test findById(Long id) {
+ String testStr = redisTemplate.opsForValue().get(id);
+ if (StringUtils.isEmpty(testStr)) {
+ //校验是否在布隆过滤器中
+ if(bloomFilter.mightContain(id)){
+ return null;
+ }
+ // 这里的锁,使用的时候注意锁的力度,这里建议换成分布式锁,这里做演示
+ synchronized (TestServiceImpl.class){
+ testStr = redisTemplate.opsForValue().get(id);
+ if (StringUtils.isEmpty(testStr) ) {
+ if(bloomFilter.mightContain(id)){
+ return null;
+ }
+ Test test = testMapper.findById(id);
+ if(test == null){
+ //放入布隆过滤器中
+ bloomFilter.put(id);
+ return null;
+ }
+ testStr = JSON.toJSONString(test);
+ redisTemplate.opsForValue().set(id, testStr);
+ }
+ }
+ }
+ return JSON.parseObject(testStr, Test.class);
+ }
+
key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端数据库加载数据并回设到缓存,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。这个时候大并发的请求可能会瞬间把后端数据源压垮。
+解决方法
+ @Resource
+ private RedisTemplate<String,Long> template;
+
+ @Resource
+ private TestMapper testMapper;
+
+ public Long findById(Long id){
+ Long value = template.opsForValue().get(id);
+
+ if (StringUtils.isEmpty(value)){
+ String key = id + ":nx";
+ // 使用 redis setnx 命令如果设置为 1 则代表成功
+ if (template.opsForValue().setIfAbsent(key, 1L, 3 * 60, TimeUnit.SECONDS)){
+ value = testMapper.findById(id);
+ template.opsForValue().set(id.toString(),value,30 * 60);
+ template.delete(key);
+ }else {
+ try {
+ // 睡眠50ms后重试
+ Thread.sleep(50);
+ value = template.opsForValue().get(id);
+ } catch (InterruptedException e) {
+ Thread.interrupted();
+ }
+ }
+ return value;
+ }else {
+ return value;
+ }
+ }
+
key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端数据源加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端数据源压垮,简单理解为,在某一个时间段,缓存key大面积失效,集中过期.可能会导致服务器宕机。
+解决方法
+ @Resource
+ private RedisTemplate<String,Long> template;
+
+ public void setKeys(){
+ template.opsForValue().set("k1",1L,30 * 60 + (new Random().nextInt(9999)));
+ template.opsForValue().set("k2",2L,30 * 60 + (new Random().nextInt(9999)));
+ }
+
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。Master以写为主,Slave以读为主。
+主从复制的作用:
+info replication
查看主从相关信息;slaveof <ip> <port>
命令将该服务器设置为从服务器;info replication
查看主从相关信息,查看是否生效;全量复制:用于初次复制或其他无法进行部分复制的情况,将主节点中的所有数据都发送给从节点,是一个非常重型的操作。
+部分复制:用于网络中断等情况后的复制,只将中断期间主节点执行的写命令发送给从节点,与全量复制相比更加高效。需要注意的是,如果网络中断时间过长,导致主节点没有能够完整地保存中断期间执行的写命令,则无法进行部分复制,仍使用全量复制。
+特点:
+特点:
+可以使用命令slaveof <ip> <port>
将该服务器设置为某服务器的从机。
当一个master宕机后,后面的slave可以立刻升为master,其后面的slave不用做任何修改,手动执行命令slaveof no one
将从机变为主机。可以使用哨兵模式让"反客为主"模式变为自动。
反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。当主机挂掉,从机选举中产生新的主机,当之前的主机再次启动,会变为从机。
+sentinel.conf
文件:+++
+- sentinel:哨兵模式
+- monitor:监控
+- mymaster:主服务器别名
+- 127.0.0.1 6379:ip端口
+- 1:至少有多少个哨兵同意迁移的数量 +
+
+sentinel monitor mymaster 127.0.0.1 6379 1
redis-sentinel ./sentinel.conf
启动哨兵由于所有的写操作都是先在Master上操作,然后同步更新到Slave上,所以从Master同步到Slave机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,Slave机器数量的增加也会使这个问题更加严重。
+++优先级在redis.conf中默认:slave-priority 100,值越小优先级越高。
+
++偏移量是指获得原主机数据最全的。
+
++每个redis实例启动后都会随机生成一个40位的runid,这里也就是随机选择。
+
Redis 集群是 Redis 提供的分布式数据库方案,集群通过分片来实现数据共享,并提供复制和故障转移。
+Redis集群模式是哨兵模式的一种拓展,在没有Redis 集群的时候,人们使用哨兵模式,所有的数据都存在 master 上面,master 的压力越来越大,垂直扩容再多的 salve 已经不能分担 master 的压力的,因为所有的写操作集中都集中在 master 上。所以人们就想到了水平扩容,就是搭建多个 master 节点。客户端进行分片,手动的控制访问其中某个节点。但是这几个节点之间的数据是不共享的。并且如果增加一个节点,需要手动的将数据进行迁移,维护起来很麻烦。 +另外,主从模式,薪火相传模式,主机宕机,导致ip地址发生变化,应用程序中配置需要修改对应的主机地址、端口等信息。所以才产生了 Redis 集群。
+Redis集群是无中心化集群,即每个结点都是入口。Redis 集群通过分区来提供一定程度的可用性;即使集群中有一部分节点失效或者无法进行通讯,集群也可以继续处理命令请求。
+Redis集群故障恢复:
+cluster-require-full-coverage
相关;如果为yes则可以继续使用,为no,那么,该插槽数据全都不能使用,也无法存储;Redis集群优点:
+Redis集群缺点:
+Redis 集群没有使用一致性hash, 而是引入了 哈希槽的概念. Redis cluster 有固定的 16384 个 hash slot,对每个 key 计算 CRC16 值,然后对 16384 取模,可以获取来决定放置哪个槽.
+集群的每个节点负责一部分hash槽,举个例子,比如当前集群有3个节点,那么:
+这种结构很容易添加或者删除节点. 比如如果我想新添加个节点D, 我需要从节点 A, B, C中得部分槽到D上. 如果我想移除节点A,需要将A中的槽移到B和C节点上,然后将没有任何槽的A节点从集群中移除即可. 由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态.
+redis.conf
配置文件,修改前建议备份,修改之后启动redis服务;
+++redis.conf: +// 引入原始配置文件 +include /home/bigdata/redis.conf +// 端口 +port 6379 +// pid文件名称 +pidfile “/var/run/redis_6379.pid” +// rdb文件名称 +dbfilename “dump6379.rdb” +// 日志文件 +logfile “/home/bigdata/redis_cluster/redis_err_6379.log” +// 启用集群模式 +cluster-enabled yes +// 设定节点配置文件名,启动后会自动生成 +cluster-config-file nodes-6379.conf +// 设定节点失联时间,超过该时间(毫秒),集群自动进行主从切换 +cluster-node-timeout 15000
+
nodes-xxxx.conf
文件都正常生成;cd /opt/redis-6.2.1/src
进入redis的src目录,执行:
+++此处不要用127.0.0.1,请用真实IP地址 +redis-cli –cluster create –cluster-replicas 1 192.168.11.101:6379 192.168.11.101:6380 192.168.11.101:6381 192.168.11.101:6389 192.168.11.101:6390 192.168.11.101:6391
+
redis-cli -c -p<port>
登陆redis集群;cluster nodes
查看集群信息;set k1 v1
+->Redirected to slot [12706] located at 192.168.137.3:6381
+OK
+
++这里的slot是插槽: +一个 Redis 集群包含 16384 个插槽(hash slot),数据库中的每个键都属于这 16384 个插槽的其中一个;集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽, 其中 CRC16(key) 语句用于计算键 key 的 CRC16 校验和。集群中的每个节点负责处理一部分插槽。 +在redis客户端每次录入、查询键值,redis都会计算出该key应该送往的插槽。
+举个例子,如果一个集群可以有主节点,其中:节点 A 负责处理 0 号至 5460 号插槽;节点 B 负责处理 5461 号至 10922 号插槽;节点 C 负责处理 10923 号至 16383 号插槽。
+
mset k1 v1 k2 v2 k3 v3
+(error) CORSSSLOT Keys in request don't hash to the same slot
+
mset k1{cust} v1 k2{cust} v2 k3{cust} v3
+
cluster getkeysinslot <slot> <count>
cluster keyslot <key>
cluster countkeysinslot <slot>
在代码里面我们常用ReetrantLock、synchronized
保证线程安全。通过上面的锁,在某个时刻只能保证一个线程执行锁作用域内的代码。
类似这样:
+public class MainTest {
+
+ private static final ReentrantLock lock = new ReentrantLock();
+
+ public static void main(String[] args) {
+ lock.lock();
+ try {
+ System.out.println("hello world");
+ }finally {
+ lock.unlock();
+ }
+ }
+}
+
但是,当项目采用分布式部署方式之后,再使用ReetrantLock、synchronized
就不能保证数据的准确性,可能会出现严重bug。
+
举个例子,当很多个请求过来的时候,会先经过Nginx
,然后Nginx
再根据算法分发请求,到哪些服务器的程序上。
+此时商品的库存为一件,有两个请求,到达不同服务器上的不同程序的相同代码,先后执行了查询SQL,查出来的数据是相同的,然后依次执行库存减一操作,此时库存会变成-1件。这就造成了超卖问题。
针对超卖问题,我们可以使用Redis分布式锁来解决。当然也有其他分布式锁,这里不做介绍。
+使用Redis,setnx
:
++SETNX 是SET IF NOT EXISTS的简写.日常命令格式是SETNX key value,如果 key不存在,则SETNX成功返回1,如果这个key已经存在了,则返回0。
+
@Autowired
+ private RedisTemplate redisTemplate;
+
+ // 保证value值唯一,这里是伪代码
+ final String value = "";
+ final String REDIS_LOCK = "redis_lock_demo";
+ public void context(){
+ try {
+ Boolean flag = redisTemplate.opsForValue().setIfAbsent(REDIS_LOCK,value);
+ if (!flag) {
+ System.out.println("抢锁失败!");
+ }
+ String redisKey = redisTemplate.opsForvalue().get("redis_key");
+ int num0 = redisKey == null ? 0 : Integer.parseInt(redisKey);
+
+ if (num0 <= 0){
+ System.out.println("商品已售完!");
+ return;
+ }
+
+ // 卖出商品,存入Redis中
+ int num1 = num0 - 1;
+ redisTemplate.opsForvalue().set("redis_key",num1);
+ }finally {
+ redisTemplate.delete(REDIS_LOCK);
+ }
+ }
+
需要注意的是redisTemplate.delete()
方法要加在finally
中是为了程序出现异常不释放锁。
但是这种写法会有一个问题,如果Redis服务器宕机了,或Redis服务被其他人kill掉了,此时恰好没有执行finally
中的代码,就会造成Redis中永远都会存在这把锁,不会释放。
针对上面Redis宕机的问题,我们可以对这个key加一个过期时间,来解决:
+ @Autowired
+ private RedisTemplate redisTemplate;
+
+ // 保证value值唯一,这里是伪代码
+ final String value = "";
+ final String REDIS_LOCK = "redis_lock_demo";
+ public void context(){
+ try {
+ // 加锁
+ Boolean flag = redisTemplate.opsForValue().setIfAbsent(REDIS_LOCK,value);
+ // 设置过期时间,假设为10s
+ redisTemplate.expire(REDIS_LOCK,10, TimeUnit.SECONDS);
+
+ if (!flag) {
+ System.out.println("抢锁失败!");
+ }
+ String redisKey = redisTemplate.opsForvalue().get("redis_key");
+ int num0 = redisKey == null ? 0 : Integer.parseInt(redisKey);
+
+ if (num0 <= 0){
+ System.out.println("商品已售完!");
+ return;
+ }
+
+ // 卖出商品,存入Redis中
+ int num1 = num0 - 1;
+ redisTemplate.opsForvalue().set("redis_key",num1);
+ }finally {
+ redisTemplate.delete(REDIS_LOCK);
+ }
+ }
+
上面的代码虽然解决了Redis宕机的问题,但是也带来了一个新的问题:设置过期时间和加锁并不再一行,即是非原子操作。
+举个例子,如果执行完setnx
加锁,正要执行expire
设置过期时间时,进程要重启维护了,那么这个锁就“长生不老”了,别的线程永远获取不到锁了。
针对上面加锁和设置过期时间的问题,我们可以使用Redis提供的一个方法,使其具备原子性:
+ @Autowired
+ private RedisTemplate redisTemplate;
+
+ // 保证value值唯一,这里是伪代码
+ final String value = "";
+ final String REDIS_LOCK = "redis_lock_demo";
+ public void context(){
+ try {
+ Boolean flag = redisTemplate.opsForValue().setIfAbsent(REDIS_LOCK,value, 10, TimeUnit.SECONDS);
+
+ if (!flag) {
+ System.out.println("抢锁失败!");
+ }
+ String redisKey = redisTemplate.opsForvalue().get("redis_key");
+ int num0 = redisKey == null ? 0 : Integer.parseInt(redisKey);
+
+ if (num0 <= 0){
+ System.out.println("商品已售完!");
+ return;
+ }
+
+ // 卖出商品,存入Redis中
+ int num1 = num0 - 1;
+ redisTemplate.opsForvalue().set("redis_key",num1);
+ }finally {
+ redisTemplate.delete(REDIS_LOCK);
+ }
+ }
+
加过期时间释放锁的这种方式会带来另一个问题,某个线程加锁,然后执行业务代码,业务代码执行的时间超过了限定时间,此时Redis会释放锁,然后第二个请求就进来了,此时第一个线程业务代码执行完毕,执行释放锁步骤。这就造成误删除其他线程的锁。
+简单说就是,张冠李戴,当前线程删除了其他线程的锁。
+针对方式三带来的问题,需要加一个判断,来避免误删除其他线程的锁:
+ @Autowired
+ private RedisTemplate redisTemplate;
+
+ final String REDIS_LOCK = "redis_lock_demo";
+ // 保证value值唯一,这里是伪代码
+ final String value = "";
+ public void context(){
+ try {
+ Boolean flag = redisTemplate.opsForValue().setIfAbsent(REDIS_LOCK,value, 10, TimeUnit.SECONDS);
+
+ if (!flag) {
+ System.out.println("抢锁失败!");
+ }
+ String redisKey = redisTemplate.opsForvalue().get("redis_key");
+ int num0 = redisKey == null ? 0 : Integer.parseInt(redisKey);
+
+ if (num0 <= 0){
+ System.out.println("商品已售完!");
+ return;
+ }
+
+ // 卖出商品,存入Redis中
+ int num1 = num0 - 1;
+ redisTemplate.opsForvalue().set("redis_key",num1);
+ }finally {
+ // 判断是否是当前线程,如果是当前线程则允许释放锁
+ if (redisTemplate.opsForvalue().get(REDIS_LOCK).equalsIgnoreCase(value)){
+ redisTemplate.delete(REDIS_LOCK);
+ }
+ }
+ }
+
实际上这种方式判断和删除的操作不是原子的,不是原子性的就会出现问题。即该锁没有保存持有者的唯一标识,可能被别的客户端解锁。
+针对方式四的问题,Redis官网有推荐的解决方法,即,使用Lua脚本:
+if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then
+ redis.call('expire',KEYS[1],ARGV[2])
+else
+ return 0
+end;
+
@Autowired
+ private RedisTemplate redisTemplate;
+
+ final String REDIS_LOCK = "redis_lock_demo";
+ // 保证value值唯一,这里是伪代码
+ final String value = "";
+ public void context(){
+ try {
+ Boolean flag = redisTemplate.opsForValue().setIfAbsent(REDIS_LOCK,value, 10, TimeUnit.SECONDS);
+
+ if (!flag) {
+ System.out.println("抢锁失败!");
+ }
+ String redisKey = redisTemplate.opsForvalue().get("redis_key");
+ int num0 = redisKey == null ? 0 : Integer.parseInt(redisKey);
+
+ if (num0 <= 0){
+ System.out.println("商品已售完!");
+ return;
+ }
+
+ // 卖出商品,存入Redis中
+ int num1 = num0 - 1;
+ redisTemplate.opsForvalue().set("redis_key",num1);
+ }finally {
+ // 伪代码
+ JRedis = jedis = JRedisUtils.getJRedis();
+
+ String lua_scripts =
+ "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then\n" +
+ " redis.call('expire',KEYS[1],ARGV[2])\n" +
+ "else\n" +
+ " return 0\n" +
+ "end;";
+ Object result = jedis.eval(lua_scripts, Collections.singletonList(REDIS_LOCK), Collections.singletonList(value))
+ if (result.equals("1")) {
+ System.out.println("删除key成功");
+ }
+
+ if (jedis != null) {
+ jedis.close();
+ }
+
+ if (redisTemplate.opsForvalue().get(REDIS_LOCK).equalsIgnoreCase(value)){
+ redisTemplate.delete(REDIS_LOCK);
+ }
+ }
+ }
+
除了用这中方式,也可以用Redis事务来处理方式四带来的问题。
+对于上面的解决方法,其实并没有真正的解决缓存续期的问题,还是会带来能存在锁过期释放,业务没执行完的问题。
+参考文章:
+ +针对缓存续期的问题,我们可以开一个守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。
+Redisson
框架解决了这个问题:
@Autowired
+ private RedisTemplate redisTemplate;
+
+ @Autowired
+ private RedissonClient redisson;
+
+ final String REDIS_LOCK = "redis_lock_demo";
+ // 保证value值唯一,这里是伪代码
+ final String value = "";
+ public void context(){
+ RLock lock = redisson.getLock(REDIS_LOCK);
+ try {
+ lock.lock(REDIS_LOCK);
+
+ String redisKey = redisTemplate.opsForvalue().get("redis_key");
+ int num0 = redisKey == null ? 0 : Integer.parseInt(redisKey);
+
+ if (num0 <= 0){
+ System.out.println("商品已售完!");
+ return;
+ }
+
+ // 卖出商品,存入Redis中
+ int num1 = num0 - 1;
+ redisTemplate.opsForvalue().set("redis_key",num1);
+ }finally {
+ lock.unlock();
+ }
+ }
+
Redisson
大致工作原理:只要线程一加锁成功,就会启动一个watch dog
看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程一还持有锁,那么就会不断的延长锁key的生存时间。
+因此,Redisson
解决了锁过期释放,业务没执行完问题。
看似完美的解决方案,但是在高并发下可能也会出现下面的异常:
+Caused by: java.lang.IllegalMonitorStateException: attempt to unlock lock, not locked by current thread by node id: 32caba49-5799-491b-aa7b-47d789dbca93 thread-id: 1
+
异常出现的原因,加锁和解锁的线程不是同一个。
+针对上面的异常,需要判断当前线程是否持有锁,如果还持有,则释放,如果未持有,则说明已被释放:
+ @Autowired
+ private RedisTemplate redisTemplate;
+
+ @Autowired
+ private RedissonClient redisson;
+
+ final String REDIS_LOCK = "redis_lock_demo";
+ // 保证value值唯一,这里是伪代码
+ final String value = "";
+ public void context(){
+ RLock lock = redisson.getLock(REDIS_LOCK);
+ try {
+ lock.lock(REDIS_LOCK);
+
+ String redisKey = redisTemplate.opsForvalue().get("redis_key");
+ int num0 = redisKey == null ? 0 : Integer.parseInt(redisKey);
+
+ if (num0 <= 0){
+ System.out.println("商品已售完!");
+ return;
+ }
+
+ // 卖出商品,存入Redis中
+ int num1 = num0 - 1;
+ redisTemplate.opsForvalue().set("redis_key",num1);
+ }finally {
+ // 查询当前线程是否持有此锁
+ if (lock.isLocked() && lock.isHeldByCurrentThread()) {
+ lock.unlock();
+ }
+ }
+ }
+
这样写,程序的健壮性会更好,代码会更加严谨。
+长期把Redis做缓存用,总有一天Redis内存总会满的。有没有相关这个问题,Redis内存满了会怎么样?
+在redis.conf
中把Redis内存设置为1个字节,做一个测试:
// 默认单位就是字节
+maxmemory 1
+
设置完之后重启为了确保测试的准确性,重启一下Redis,之后在用下面的命令,向Redis中存入键值对,模拟Redis打满的情况:
+set k1 v1
+
执行完后会看到下面的信息:
+(error) OOM command not allowed when used memory > 'maxmemory'.
+
大意:OOM,当当前内存大于最大内存时,这个命令不允许被执行。
+是的,Redis也会出现OOM,正因如此,我们才要避免这种情况发生,正常情况下,不考虑极端业务,Redis不是MySql数据库,不能什么都往里边写,一般情况下Redis只存放热点数据。
+Redis默认最大内存是全部的内存,我们在实际配置的时候,一般配实际服务器内存的3/4也就足够了。
+正因为Redis内存打满后报OOM,为了避免出现该情况所以要设置Redis的删除策略。思考一个问题,一个键到了过期时间之后是不是马上就从内存中被删除的?
+当然不是的,那过期之后到底什么时候被删除?是个什么操作?
+Redis提供了三种删除策略:
+定时删除,即用时间换空间;它对于内存来说是友好的,定时清理出干净的空间,但是对于CPU来说并不是友好的,程序需要维护一个定时器,这就会占用CPU资源。
+惰性的删除,即用空间换时间;它对于CPU来说是友好的,CPU不需要维护其它额外的操作,但是对于内存来说是不友好的,因为要是有些key一直没有被访问到,就会一直占用着内存。
+定期删除,是上面两种方案的折中方案,它每隔一段时间删除过期的key,也就是根据具体的业务,合理的取一个时间定期的删除key。
+若果在数据量很大的情况下,定时删除时,key从来没有被检查到过;惰性删除时,key从来没有被使用过,这样就会造成内存泄漏,大量的key堆积在内存中,导致Redis内存空间紧张。
+所以我们必须有一个兜底方案,即Redis的内存淘汰策略。
+在 Redis 4.0
版本之前有 6 种策略,4.0 增加了 2种,主要新增了 LFU
算法。下图为 Redis 6.2.0
版本的配置文件:
淘汰策略默认,使用noeviction
,意思是不再驱逐的,即等着内存被打满。
The default is:
+maxmemory-policy noeviction
+
策略名称 | +描述 | +
---|---|
noeviction (默认策略) |
+不会驱逐任何key,即内存满了就报错。 | +
allkeys-lru |
+所有key都是使用LRU算法进行淘汰。 | +
volatile-lru |
+所有设置了过期时间的key使用LRU算法进行淘汰。 | +
allkeys-random |
+所有的key使用随机淘汰的方式进行淘汰。 | +
volatile-random |
+所有设置了过期时间的key使用随机淘汰的方式进行淘汰。 | +
volatile-ttl |
+所有设置了过期时间的key根据过期时间进行淘汰,越早过期就越快被淘汰。 | +
假如在Redis中的数据有一部分是热点数据,而剩下的数据是冷门数据,或者我们不太清楚我们应用的缓存访问分布状况,这时可以使用allkeys-lru
。
可以在redis.conf
配置文件中配置:
maxmemory-policy allkeys-lru // 淘汰策略名字
+
当然也可以动态的配置,在Redis运行时修改:
+// 设置内存淘汰策略
+config set maxmemory-policy allkeys-lru
+
+// 查看内存淘汰策略
+config get maxmemory-policy
+
LRU是,Least Recently Used
的缩写,即最近最少使用,是一种常用的页面置换算法。
++页面置换算法: +进程运行时,若其访问的页面不在内存而需将其调入,但内存已无空闲空间时,就需要从内存中调出一页程序或数据,送入磁盘的对换区,其中选择调出页面的算法就称为页面置换算法。
+
这个算法的思想就是: 如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。所以,当指定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。
+明白了思想之后,要实现LRU算法,首先要确定数据结构,再确定实现思路。如果对算法有要求,查询和插入的时间复杂度都是O(1)
,可以选用链表+哈希的结构来存储:
链表+哈希,我们不难想到JDK中的LinkedHashMap
,在LinkedHashMap
文档注释中找到关于LRU算法的相关描述:
++A special {@link #LinkedHashMap(int,float,boolean) constructor} is provided to create a linked hash map whose order of iteration is the order in which its entries were last accessed,from least-recently accessed to most-recently (access-order). This kind of map is well-suited to building LRU caches.
+
大意:{@link #LinkedHashMap(int,float,boolean)}
提供了一个特殊的构造器来创建一个链表散列映射,其迭代顺序为其条目最后访问的顺序,从最近最少访问到最近最近(access-order
)。这种映射非常适合构建LRU缓存。
参照LinkedHashMap
实现LRU算法:
public class MainTest {
+
+ public static void main(String[] args) {
+ LRUDemo<Integer,String> list0 = new LRUDemo<>(3,true);
+ System.out.println("-------------accessOrder等于true-------------");
+ context(list0);
+ System.out.println("-------------accessOrder等于false-------------");
+ LRUDemo<Integer,String> list1 = new LRUDemo<>(3,false);
+ context(list1);
+ }
+
+ public static void context(LRUDemo<Integer,String> list){
+ list.put(1,"a");
+ list.put(2,"b");
+ list.put(3,"c");
+ System.out.println(list.keySet());
+
+ list.put(4,"d");
+ System.out.println(list.keySet());
+ System.out.println();
+
+ list.put(3,"123");
+ System.out.println(list.keySet());
+
+ list.put(3,"1234");
+ System.out.println(list.keySet());
+
+ list.put(3,"12345");
+ System.out.println(list.keySet());
+ System.out.println();
+
+ list.put(5,"123456");
+ System.out.println(list.keySet());
+ }
+}
+
+class LRUDemo<K,V> extends LinkedHashMap<K,V>{
+
+ private int capacity;
+
+ public LRUDemo(int capacity, boolean accessOrder) {
+ /**
+ * accessOrder the ordering mode -
+ * <tt>true</tt> for access-order 存取顺序:如果存贮集合中有相同的元素,再次插入时先删除在插入
+ * <tt>false</tt> for insertion-order 插入顺序:不会因为集合中有相同元素,再次插入该元素就会打乱位置
+ */
+ super(capacity,0.75f,accessOrder);
+ this.capacity = capacity;
+ }
+
+ @Override
+ protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
+ return super.size() > capacity;
+ }
+}
+
除了可以参照LinkedHashMap
,也可以自己手动实现:
public class MainTest {
+
+ public static void main(String[] args) {
+ LRUDemo<Integer,String> list0 = new LRUDemo<>(3);
+ context(list0);
+ }
+
+ public static void context(LRUDemo<Integer,String> list){
+ list.put(1,"a");
+ list.put(2,"b");
+ list.put(3,"c");
+ System.out.println(list.getMap().keySet());
+
+ list.put(4,"d");
+ System.out.println(list.getMap().keySet());
+ System.out.println();
+
+ list.put(3,"123");
+ System.out.println(list.getMap().keySet());
+
+ list.put(3,"1234");
+ System.out.println(list.getMap().keySet());
+
+ list.put(3,"12345");
+ System.out.println(list.getMap().keySet());
+ System.out.println();
+
+ list.put(5,"123456");
+ System.out.println(list.getMap().keySet());
+ }
+}
+
+class LRUDemo<K, V> {
+
+ static class Node<K,V>{
+ K key;
+ V value;
+ Node<K,V> prev;
+ Node<K, V> next;
+
+ public Node(){
+ prev = next = null;
+ }
+
+ public Node(K key,V value){
+ this.key = key;
+ this.value = value;
+ }
+ }
+
+ static class DoubleLinkedList<K,V>{
+ Node<K,V> head;
+ Node<K, V> tail;
+
+ public DoubleLinkedList(){
+ head = new Node<>();
+ tail = new Node<>();
+
+ // 如果变成尾插法,需要调换头、尾指针的指向
+ head.next = tail;
+ tail.prev = head;
+ }
+
+ public void putHead(Node<K, V> node){
+ node.next = head.next;
+ node.prev = head; // 将新结点插到头部
+ head.next.prev = node;
+ head.next = node;
+ }
+
+ public void remove(Node<K, V> node){
+ node.prev.next = node.next;
+ node.next.prev = node.prev;
+ node.prev = node.next = null;
+ }
+
+ public Node<K,V> getLastNode(){
+ return tail.prev;
+ }
+ }
+
+ private int capacity;
+ private Map<K, Node<K,V>> map;
+ private DoubleLinkedList<K,V> doubleLinkedList;
+
+ public Map<K, Node<K, V>> getMap() {
+ return map;
+ }
+
+ public LRUDemo(int capacity){
+ this.capacity = capacity;
+ this.map = new HashMap<>();
+ doubleLinkedList = new DoubleLinkedList<>();
+ }
+
+ public void put(K key,V val){
+ if (key == null || val == null){
+ return;
+ }
+ // 如果集合中key已经存在,则先删除
+ if (map.containsKey(key)){
+ Node<K, V> node = map.get(key);
+ node.value = val;
+ map.put(key,node);
+
+ // 刷新node
+ doubleLinkedList.remove(node);
+ doubleLinkedList.putHead(node);
+ }else {
+ // 删除最少使用的key
+ if (map.size() == capacity){
+ Node<K, V> lastNode = doubleLinkedList.getLastNode();
+ doubleLinkedList.remove(lastNode);
+ map.remove(lastNode.key);
+ }
+ Node<K, V> newNode = new Node<>(key,val);
+ map.put(key,newNode);
+ doubleLinkedList.putHead(newNode);
+ }
+ }
+
+ public V get(K key){
+ if (!map.containsKey(key)){
+ return null;
+ }
+ Node<K, V> node = map.get(key);
+
+ // 刷新结点位置,将该key移动到队列头部
+ doubleLinkedList.remove(node);
+ doubleLinkedList.putHead(node);
+ return node.value;
+ }
+}
+
什么是Redis持久化? 持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失。Redis 提供了两种持久化方式:RDB(默认) 和AOF。
+其实 RDB 和 AOF 两种方式也可以同时使用,在这种情况下,则会优先采用 AOF 方式来进行数据恢复,这是因为 AOF 方式的数据恢复完整度更高。如果你没有数据持久化的需求,也完全可以关闭 RDB 和 AOF 方式,这样的话,redis 将变成一个纯内存数据库,就像 memcache
一样。
RDB (Redis DataBase)方式,是将 redis 某一时刻的数据持久化到磁盘中,是一种快照式的持久化方法。
+dbfilename dump.rdb
+
dir ./
+
save 3600 1
+save 30 10
+save 60 10000
+
stop-writes-on-bgsave-error yes
+
rdbcompression yes
+
rdbchecksum yes
+
redis 在进行数据持久化的过程中,会先将数据写入到一个临时文件中,待持久化过程都结束了,才会用这个临时文件替换上次持久化好的文件。正是这种特性,让我们可以随时来进行备份,因为快照文件总是完整可用的。对于 RDB 方式,redis 会单独创建(fork)一个子进程来进行持久化,而主进程是不会进行任何 IO 操作的,这样就确保了 redis 极高的性能。
+++ +fork:
++
+- 在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,Linux中引入了“写时复制技术”;
+- 一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程;
+
如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB 方式要比 AOF 方式更加的高效。虽然 RDB 有不少优点,但它的缺点也是不容忽视的:丢失数据风险较大,fork进程在保存rdb文件时会先复制旧文件,如果文件较大则耗时较多。如果你对数据的完整性非常敏感,那么 RDB 方式就不太适合你,因为即使你每 5 分钟都持久化一次,当 redis 故障时,仍然会有近 5 分钟的数据丢失。所以,redis 还提供了另一种持久化方式,那就是 AOF。
+AOF,英文是 Append Only File,即只允许追加不允许改写的文件。如前面介绍的,AOF 方式是将执行过的写指令记录下来,在数据恢复时按照从前到后的顺序再将指令都执行一遍。
+在redis中AOF默认时不开启的:
+appendonly no
+
AOF文件名称,文件路径与rdb保持一致:
+appendfilename "appendonly.aof"
+
AOF同步频率设置:默认的 AOF 持久化策略是每秒钟 fsync 一次(fsync 是指把缓存中的写指令记录到磁盘中),因为在这种情况下,redis 仍然可以保持很好的处理性能,即使 redis 故障,也只会丢失最近 1 秒钟的数据。
+appendfsync always
+
appendfsync everysec
+
appendfsync no
+
Rewrite压缩:AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制,当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。举个例子或许更形象,假如我们调用了 100 次 INCR 指令,在 AOF 文件中就要存储 100 条指令,但这明显是很低效的,完全可以把这 100 条指令合并成一条 SET 指令,这就是重写机制的原理。在进行 AOF 重写时,仍然是采用先写临时文件,全部完成后再替换的流程,所以断电、磁盘满等问题都不会影响 AOF 文件的可用性。
+auto-aof-rewrite-percentage
+
auto-aof-rewrite-min-size
+
++例如:文件达到70MB开始重写,降到50MB,下次什么时候开始重写? +系统载入时或者上次重写完毕时,Redis会记录此时AOF大小,设为base_size,如果Redis的AOF文件当前大小>= base_size +base_size*100% (默认)且当前大小>=64mb(默认)的情况下,Redis会对AOF进行重写。
+当前base_size为50MB,根据公式:base_size +base_size*100% = 100MB
+
如果在追加日志时,恰好遇到磁盘空间满或断电等情况导致日志写入不完整,也没有关系,可以进行恢复:
+redis-check-aof --fix AOP文件名称
+
AOF 方式的一个好处,我们通过一个“场景再现”来说明。某同学在操作 redis 时,不小心执行了 FLUSHALL,导致 redis 内存中的数据全部被清空了,这是很悲剧的事情。不过这也不是世界末日,只要 redis 配置了 AOF 持久化方式,且 AOF 文件还没有被重写(rewrite),我们就可以用最快的速度暂停 redis 并编辑 AOF 文件,将最后一行的 FLUSHALL 命令删除,然后重启 redis,就可以恢复 redis 的所有数据到 FLUSHALL 之前的状态了。这就是 AOF 持久化方式的好处之一。但是如果 AOF 文件已经被重写了,那就无法通过这种方法来恢复数据了。
+虽然优点多多,但 AOF 方式也同样存在缺陷,比如在同样数据规模的情况下,AOF 文件要比 RDB 文件的体积大。而且 AOF 方式的恢复速度也要慢于 RDB 方式。
+官方推荐两个都启用,如果对数据不敏感,可以选单独用RDB,如果对数据不敏感,可以选单独用RDB,如果只是做纯内存缓存,可以都不用。
++ | RDB | +AOF | +
---|---|---|
定义 | +在指定的时间间隔能对你的数据进行快照存储 | +记录每次对服务器的写操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾 | +
优先级 | +低 | +高 | +
数据完整性 | +易丢失 | +不易丢失 | +
恢复数据速度 | +较快 | +较慢 | +
数据文件损坏 | +不能修复 | +可使用命令进行修复 | +
数据文件大小 | +较小 | +较大,但是可压缩 | +
所谓的大key问题是某个key的value比较大,所以本质上是大value问题.因为key往往是程序可以自行设置的,value往往不受程序控制,因此可能导致value很大。
+因为Redis是单线程的,单线程中请求任务的处理是串行的,前面完不成,后面处理不了,同时也导致分布式架构中内存数据和CPU的不平衡.所以大key问题最典型的就是阻塞线程,并发量下降,导致客户端超时,服务端业务成功率下降。
+大key问题一般是由于业务方案设计不合理,没有预见value的动态增长问题产生的.一直往value塞数据,没有删除机制,迟早要爆炸或数据没有合理做分片,将大key变成小key. 在线上一般通过集成Redis可视化工具,来发现和定位大key问题.
+解决大key问题,根据大key的实际用途可以分为两种情况:可删除和不可删除。如果发现某些大key并非热key就可以在DB中查询使用,则可以在Redis中删掉.
+删除
+不可删除
+你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题.
+一般来说,如果允许缓存可以稍微的跟数据库偶尔有不一致的情况,也就是说如果你的系统不是严格要求 “缓存+数据库” 必须保持一致性的话,最好不要做这个方案,即:读请求和写请求串行化,串到一个内存队列里去。
+串行化可以保证一定不会出现不一致的情况,但是它也会导致系统的吞吐量大幅度降低,用比正常情况下多几倍的机器去支撑线上的一个请求。
+最经典的Redis使用方案:
+先更新数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致
+解决思路:
+++延迟双删的主要思路为异步重试,一般会把重试的步骤放在MQ中进行不断尝试
+
并发,数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后数据变更的程序完成了数据库的修改
+++这种场景一般只会在数据在并发的进行读写的时候,才可能会出现这种问题,如果说你的并发量很低的话,特别是读并发很低,每天访问量就 1 万次,那么很少的情况下,会出现刚才描述的那种不一致的场景
+
解决思路: 当第二个请求进来的时候,第一个请求删除了缓存,正要去修改数据库,此时把第二个请求加入到一个队列中,让其等待第一个请求执行完毕,在从队列中获取第二个请求,让其执行.
+该解决方案,最大的风险点在于说,可能数据更新很频繁,导致队列中积压了大量更新操作在里面,然后读请求会发生大量的超时,最后导致大量的请求直接走数据库。 如果一个内存队列中可能积压的更新操作特别多,那么你就要加机器,让每个机器上部署的服务实例处理更少的数据,那么每个内存队列中积压的更新操作就会越少.
+粗略测算,如果一秒有 500 的写操作,如果分成 5 个时间片,每 200ms 就 100 个写操作,放到 20 个内存队列中,每个内存队列,可能就积压 5 个写操作。每个写操作性能测试后,一般是在 20ms 左右就完成,那么针对每个内存队列的数据的读请求,也就最多 hang 一会儿,200ms 以内肯定能返回了.
++ +
+ + + + + +++In short, the microservice architectural style is an approach to developing a single application as a suite of small services, +each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API。 +These services are built around business capabilities and independently deployable by fully automated deployment machinery。 +There is a bare minimum of centralized management of these services, which may be written in different programming languages and use different data storage technologies。 ——James Lewis and Martin Fowler (2014)
+
大意:简而言之,微服务体系结构风格是一种将单个应用程序开发为一套小型服务的方法,每个服务运行在自己的进程中,并与轻量级机制(通常是HTTP资源API)通信。 +这些服务是围绕业务功能构建的,可以通过全自动部署机制进行独立部署。对这些服务的集中管理是最低限度的,这些服务可能用不同的编程语言编写,并使用不同的数据存储技术。
+微服务是一种架构风格,将服务围绕业务功能拆分,一个应用拆分为一组小型服务,每个服务运行在自己的进程内,也就是可独立部署和升级,服务之间使用轻量级HTTP交互,可以由全自动部署机制独立部署,去中心化,服务自治。服务可以使用不同的语言、不同的存储技术。
+说起微服务就不得不说微服务之前的单体应用,有对比才能看的出微服务对比之前的单体应用到底强在哪?为什么我们要抛弃单体应用?
+微服务虽然好处多多,但是缺点就是服务众多,不好管理,这也就是为什么要治理微服务,治理成本高,不利于维护系统,项目成本会升高,所以项目架构设计时应综合考量。
+建议设计的原则是业务驱动、设计保障、演进式迭代、保守治疗的方式。搞不清楚,有争议的地方先尽量不要拆,如果确实要拆,要经过业务分析后慎重设计,把真正相对独立的部分拆分出来,可以借鉴 DDD 的方式。拆了以后要观察微服务的接口是否稳定,针对业务需求的变更微服务的模块是否可以保持相对稳定,是否可以独立演进。
+++DDD: Domain-driven design的简称,译为领域驱动设计,是一种通过将实现连接到持续进化的模型来满足复杂需求的软件开发方法。 +
+
+核心思想:DDD其实是面向对象方法论的一个升华。无外乎是通过划分领域(聚合根、实体、值对象)、领域行为封装到领域对象(充血模式)、内外交互封装到防腐层、职责封装到对应的模块和分层,从而实现了高内聚低耦合。 +链接:https://blog.csdn.net/qq_31960623/article/details/119840131
重写代码,但是不要大规模重写代码,重写代码听起来很好但风险较大,如果大规模重写代码可能会导致程序出现各种各样的bug;(当你承担重建一套全新基于微服务的应用程序不需要使用时候,可以采用重写这种方法)
+逐步迁移单体应用的功能,独立出来形成新的微服务,同时需要与旧的单体应用集成,这可以保证系统的正常运行,单体式应用在整个架构中比例逐渐下降直到消失或者成为微服务架构一部分;
+虽然在逐步的迁移功能,但是也会有源源不断的需求需要开发,此时该停止让单体式应用继续变大,也就是说当开发新功能时不应该为旧单体应用添加新代码,应该是将新功能开发成独立微服务;
+一个巨大的复杂单体应用由成十上百个模块构成,每个都是被抽取对象。决定第一个被抽取模块一般都是挑战,一般最好是从最容易抽取的模块开始,这会让开发者积累足够经验,这些经验可以为后续模块化工作带来巨大好处。
+转换模块成为微服务一般很耗费时间,一般可以根据获益程度来排序,一般从经常变化模块开始会获益最大。一旦转换一个模块为微服务,就可以将其开发部署成独立模块,从而加速开发进程。比如,抽取一些消耗内存资源较大的代码,将其弄成一个服务,然后可以将其部署在大内存主机上。同样的,将对计算资源很敏感的算法应用抽取出来也是非常有益的,这种服务可以被部署在有很多 CPU 的主机上。 +有三种策略可以考虑:将新功能以微服务方式实现;将表现层与业务数据访问层分离;将现存模块抽取变成微服务。
+首先要将抽离的相关功能,封装成相关几个方法,几个类,几个包中;定义好模块和单体应用之间的几个大概的接口,从程序内部开始调用; 一旦完成相关的接口,也就将此模块转换成独立微服务。为了实现,必须写代码使得单体应用和微服务之间通过使用进程间通信机制的API来交换信息;服务和单体应用整合的API代码就成为了容灾层;
+然后,将抽离的功能,转化为独立的服务进行部署,将其整合成一个微服务基础框架; 每抽取一个服务,就朝着微服务方向前进一步,随着时间推移,单体应用将会越来越简单,用户就可以增加更多独立的微服务。
+摘自《微服务设计》:
+++在进行城市交通规划之前首先要做的第一个事情是收集信息,要能够知道这个城市发生了什么,所以在各个路口需要安装采集探头,记录车来车往的信息。有了信息以后就需要对信息进行分析了,那么就需要可视化的图形界面,能够一眼就看出什么地方出了问题,通往哪个工厂的路坏了。发现了问题就要解决问题了,限制一下拥堵路段的流量,把去往一个公园的车辆导向到另外一个类似的公园。最后,如果把城市作为一个国家来考虑,那么每个进入这个城市的车辆都需要进行检查,看看有没有携带违禁品,最后给这些不熟悉道路的外地车规划路线。通过上面这个思考的过程,我们发现要对一个城市进行治理的时候,第一要采集信息,然后要能够对采集的信息进行监控和分析,最后根据分析的结果采取对应的治理策略。另外从整体安全的角度考虑还需要一个守门人
+
我们也用同样的思路来思考服务治理,网关就是整个整体的守门人,日志采集,追踪工具,服务注册发现都是用来采集信息的,然后需要监控平台来展现这些采集的信息,并进行监控和分析。 +最后根据分析的结果采取治理策略,有的服务快撑不住了要限流,有的服务坏了要熔断,并且还能够及时的调整这些服务的配置。
+ +随着微服务模式的使用,服务之间的调用带来的问题有很多,例如:数据一致性、网络波动、缓存、事务等问题,针对这一系列的问题就要有对应的框架来解决,目前主流一站式微服务解决方案有Spring Cloud
、Spring Cloud Alibaba
。
Spring Cloud
它并不是一个框架,而是很多个框架。它是分布式微服务架构的一站式解决方案,是多种微服务架构落地技术的集合体,俗称微服务全家桶。
在服务注册与发现中,有一个注册中心。当服务器启动的时候,会把当前自己服务器的信息比如服务地址通讯地址等以别名方式注册到注册中心上。另一方消费者服务提供者,以该别名的方式去注册中心上获取到实际的服务通讯地址,然后再实现本地RPC调用。RPC远程调用框架核心设计思想:在于注册中心,因为使用注册中心管理每个服务与服务之间的一个依赖关系。在任何RPC远程框架中,都会有一个注册中心存放服务地址相关信息。
+参考文章:
+ +++Eureka is a REST (Representational State Transfer) based service that is primarily used in the AWS cloud for locating services for the purpose of load balancing and failover of middle-tier servers。 We call this service, the Eureka Server。 Eureka also comes with a Java-based client component,the Eureka Client, which makes interactions with the service much easier。 The client also has a built-in load balancer that does basic round-robin load balancing。 —https://github.com/Netflix/eureka
+
大意:Eureka是一个REST (Representational State Transfer)服务,它主要用于AWS云,用于定位服务,以实现中间层服务器的负载平衡和故障转移,我们称此服务为Eureka服务器。Eureka也有一个基于java的客户端组件,Eureka客户端,这使得与服务的交互更加容易,同时客户端也有一个内置的负载平衡器,它执行基本的循环负载均衡。
+Eureka采用了Client / Server 的设计架构,提供了完整的服务注册和服务发现,可以和 Spring Cloud 无缝集成。Eureka Sever作为服务注册功能的服务器,它是服务注册中心。而系统中的其他微服务,使用Eureka的客户端连接到 Eureka Server并维持心跳连接。这样系统的维护人员就可以通过Eureka Server来监控系统中各个微服务是否正常运行。
+ +Eureka的自我保护机制:
+默认情况下,如果Eureka Server在一定时间内(默认90秒)没有接收到某个微服务实例的心跳,Eureka Server将会移除该实例。但是当网络分区故障发生时,微服务与Eureka Server之间无法正常通信,而微服务本身是正常运行的,此时不应该移除这个微服务,所以引入了自我保护机制。
+++自我保护模式正是一种针对网络异常波动的安全保护措施,使用自我保护模式能使Eureka集群更加的健壮、稳定的运行。
+
如果在15分钟内超过85%的客户端节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,Eureka Server自动进入自我保护机制:
+简而言之,某时刻某一个微服务不可用了,Eureka不会立刻清理,依旧会对该微服务的信息进行保存。所以Eureka属于AP。
+由于分布式架构中应用程序依赖关系可能非常多,每个依赖关系在某些时候将不可避免地失败,针对服务调用失败,为了缩小调用失败的影响,引入了服务熔断降级的思想。
+多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”。
+服务熔断是应对系统服务雪崩的一种保险措施,给出的一种特殊降级措施。而服务降级则是更加宽泛的概念,主要是对系统整体资源的合理分配以应对压力。服务熔断可看作特殊降级。
+Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
+++“断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
+
“断路器"思想:https://martinfowler.com/bliki/CircuitBreaker.html
+ +HystrixCommand
或HystrixObserableCommand
对象。HystrixCommand
实现了下面前两种执行方式:
+execute
:同步执行,从依赖的服务返回一个单一的结果对象或是在发生错误的时候抛出异常。queue
:异步执行,直接返回一个Future对象,其中包含了服务执行结束时要返回的单一结果对象。HystrixObservableCommand
实现了后两种执行方式:
+obseve()
:返回Observable对象,它代表了操作的多个结果,它是一个Hot Observable,不论“事件源”是否有“订阅者”,都会在创建后对事件进行发布,所以对于Hot Observable的每一个“订阅者”都有可能是从“事件源”的中途开始的,并可能只是看到了整个操作的局部过程。toObservable()
:同样会返回Observable对象,也代表了操作的多个结果,但它返回的是一个Cold Observable,没有“订间者”的时候并不会发布事件,而是进行等待,直到有“订阅者"之后才发布事件,所以对于Cold Observable 的订阅者,它可以保证从一开始看到整个操作的全部过程。HystrixCommand.run()
:返回一个单一的结果,或者抛出异常。HystrixObservableCommand.construct()
:返回一个Observable对象来发射多个结果,或通过onError发送错误通知。HystrixObsevableCommand.construct()
或HytrixCommand.run()
抛出异常的时候。参考文章:
+网关的角色是作为一个 API 架构,用来保护、增强和控制对于 API 服务的访问。API 网关是一个处于应用程序或服务(提供 REST API 接口服务)之前的系统,用来管理授权、访问控制和流量限制等,这样 REST API 接口服务就被 API 网关保护起来,对所有的调用者透明。因此,隐藏在 API 网关后面的业务系统就可以专注于创建和管理服务,而不用去处理这些策略性的基础设施。
+网关作用:
+Spring Cloud Gateway是Spring官方基于Spring 5.0,Spring Boot 2.0和Project Reactor等技术开发的网关,Spring Cloud Gateway旨在为微服务架构提供一种简单而有效的统一的API路由管理方式。Spring Cloud Gateway作为Spring Cloud生态系中的网关,目标是替代ZUUL,其不仅提供统一的路由方式,并且基于Filter链的方式提供了网关基本的功能,例如:安全,监控/埋点,和限流等。
+核心组件:
+java.util.function。Predicate
,开发人员可以匹配HTTP请求中的所有内容,例如请求头或请求参数,如果请求与断言相匹配则进行路由;工作流程: +
+客户端向Spring Cloud Gateway发出请求。然后在Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到GatewayWeb Handler,Handler再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。
+过滤器分为前置过滤器和后置过滤器,可以在发送代理请求之前或之后执行业务逻辑。
+参考文章:
+服务调用可以说是微服务中最关键的,如果没有服务调用不会出现熔断降级、也不会出现服务负载、服务注册,分布式的服务治理可以说是围绕服务调用展开的。
+常见的服务之间的调用方式有两种:
+现在热门的Rest风格,就可以通过HTTP协议来实现,如果公司全部采用Java技术栈,那么使用Dubbo作为微服务框架是一个不错的选择;如果公司的技术栈多样化,而且你更青睐Spring家族,那么SpringCloud搭建微服务是不二之选。
+++Feign is a declarative web service client。 It makes writing web service clients easier。 To use Feign create an interface and annotate it。 It has pluggable annotation support including Feign annotations and JAX-RS annotations。 Feign also supports pluggable encoders and decoders。 Spring Cloud adds support for Spring MVC annotations and for using the same HttpMessageConverters used by default in Spring Web。 Spring Cloud integrates Ribbon and Eureka, as well as Spring Cloud LoadBalancer to provide a load-balanced http client when using Feign。
+
大意:Feign是一个声明式WebService客户端。使用Feign能让编写Web Service客户端更加简单。它的使用方法是定义一个服务接口然后在上面添加注解。Feign也支持可拔插式的编码器和解码器。Spring Cloud对Feign进行了封装,使其支持了Spring MVC标准注解和HttpMessageConverters。Feign可以与Eureka和Ribbon组合使用以支持负载均衡。
+Feign和OpenFeign:
+通过使用OpenFeign
可以使代码变得更加简洁,减轻程序员负担:
// 伪代码
+ @Component
+ // 括号中,为在注册中心注册的服务名称
+ @FeignClient("CLOUD-PAYMENT-SERVICE")
+ public interface PaymentFeignService {
+
+ @GetMapping("/payment/get/{id}")
+ CommonResult<Payment> getPayment(@PathVariable("id") Long id);
+
+ @PostMapping("/payment/timeout")
+ String feignTimeoutTest();
+ }
+
openFeign工作原理:
+ +@FeignClient
注册FactoryBean
到IOC容器, 最终产生了一个虚假的实现类代理;++负载均衡,英文名称为Load Balance,其含义就是指将工作任务进行平衡、分摊到多个操作单元上进行运行,例如FTP服务器、Web服务器、企业核心应用服务器和其它主要任务服务器等,从而协同完成工作任务。
+
当前服务与服务之间进行相互调用时,在分布式架构下应用都是集群部署,所以这个时候就需要进行服务负载,即将收到的请求分摊到对应的服务器上,从而达到系统的高可用。
+常见负载均衡算法:
+Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具,是一个内置软件负载平衡器的进程间通信(远程过程调用)库。主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon客户端组件提供一系列完善的配置项,如连接超时、重试等。
+本地负载与服务端负载:
+Ribbon其实是一个软负载均衡的客户端组件,它可以和其他所需请求的客户端结合使用,例如与Eureka结合:
+ +消费方和服务方在注册中心注册服务,当消费方发起请求时,Ribbon会去注册中心寻找请求的服务名,即服务方集群,Ribbon默认负载算法会根据接口第几次请求 % 服务器集群总数量
算出实际消费方服务器的位置,每次服务重启动后rest接口计数从1开始。
模拟Ribbon默认负载均衡算法:
+public interface ILoadBalance {
+ ServiceInstance instance(List<ServiceInstance> instances);
+}
+
@Component
+public class MyLoadBalance implements ILoadBalance{
+
+ /**
+ * 轮询索引
+ */
+ private final AtomicInteger index = new AtomicInteger(0);
+
+ /**
+ * 负载均衡算法:rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标,每次服务重启动后rest接口计数从1开始。
+ * @param instances 服务器集群数量
+ * @return 实际服务器的下标
+ */
+ @Override
+ public ServiceInstance instance(List<ServiceInstance> instances) {
+ return instances.get(incrementAndGet() % instances.size());
+ }
+
+ public final int incrementAndGet() {
+ int current = 0;
+ int next = 0;
+ do {
+ current = index.get();
+ // 当最大数量超过 Integer。MAX_VALUE 归0
+ next = current >= 2147483647 ? 0 : current + 1;
+ }while (!index.compareAndSet(current,next));
+ return next;
+ }
+}
+
@Resource
+ private RestTemplate restTemplate;
+
+ @Resource
+ private DiscoveryClient discoveryClient;
+
+ @Resource
+ private ILoadBalance iLoadBalance;
+
+ @GetMapping("/myLoadBalance")
+ public String myLoadBalanceTest() {
+
+ List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
+ ServiceInstance instance = iLoadBalance.instance(instances);
+
+ URI uri = instance.getUri();
+ String finalUri = String.format("%s/%s", uri, PaymentConstant。PAYMENT_GETPORT_API);
+
+ log.info("自定义负载均衡,请求地址:{}", finalUri);
+
+ return restTemplate.getForObject(finalUri, String.class);
+ }
+
在分布式微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。由于每个服务都需要必要的配置信息才能运行,所以一套集中式的、动态的配置管理设施是必不可少的。
+配置中心应运而生。配置中心,顾名思义,就是来统一管理项目中所有配置的系统。对于单机版,我们称之为配置文件;对于分布式集群系统,我们称之为配置中心。
+配置中心作用:
+参考文章:
+SpringCloud Config为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为各个不同微服务应用的所有环境提供了一个中心化的外部配置。
+ +SpringCloud Config
是配置中心的一种,它分为服务端和客户端两部分:
使用SpringCloud Config
非常简单,需要在服务端和客户端分别进行改造:
@EnableConfigServer
注解,就大功告成了; ## http://127.0.0.1:3344/master/config-dev.yml
+ 1. /{label}/{application}-{profile}.yml
+
+ ## http://127.0.0.1:3344/config-dev.yml
+ 2. /{application}-{profile}.yml
+
+ ## http://127.0.0.1:3344/config/dev/master
+ 3. /{application}/{profile}[/{label}]
+
application.yml
文件改为bootstrap.yml
,再将主启动类加上@EnableEurekaClient
注解;
+++当配置客户端启动时,它绑定到配置服务器(通过spring.cloud.config.uri引导配置属性),并使用远程属性源初始化Spring Environment。 +这种行为的最终结果是,所有想要使用Config Server的客户端应用程序都需要bootstrap.yml或一个环境变量
+applicaiton.yml是用户级的资源配置项,而bootstrap.yml是系统级的,优先级更加高,在加载配置时优先加载bootstrap.yml
+
当将SpringCloud Config
客户端服务端都配置好之后,修改配置时会发现修改的配置文件不能实时生效;针对这个问题,可以将服务重启或者调用actuator
的刷新接口使其生效,使用之前需要引入actuator
的依赖:
# 使用SpringCloud Config修改完配置后,调用刷新接口使客户端配置生效
+ curl -X POST "http://localhost:3355/actuator/refresh"
+
为了避免手动的调用刷新接口,可以使用SpringCloud Bus
配合SpringCloud Config
实现配置的动态刷新。
长期以来 Java 的开发一直让人所诟病:项目开发复杂度极其高、项目的维护非常困难;即便使用了大量的开发框架,发现我们的开发也没少多少。为了解决让开发更佳简单,项目更容易管理,SpringBoot诞生了。
+Spring Boot是一个广泛用来构建Java微服务的框架,它基于Spring依赖注入框架来进行工作。
+官网地址:https://spring.io/projects/spring-boot
++++
SpringBoot
是由Pivotal
团队提供的全新框架,其设计目的是用来简化新Spring
应用的初始搭建以及开发过程。 +该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。SpringBoot
提供了一种新的编程范式,可以更加快速便捷地开发Spring
项目,在开发过程当中可以专注于应用程序本身的功能开发,而无需在Spring
配置上花太大的工夫。
SpringBoot
基于 Sring4
进行设计,继承了原有 Spring
框架的优秀基因。
+SpringBoot
准确的说并不是一个框架,而是一些类库的集合。
+maven
或者 gradle
项目导入相应依赖即可使用 SpringBoot
,而无需自行管理这些类库的版本。
特点:
+Spring
项目:
+SpringBoot
可以以 jar 包的形式独立运行,运行一个 SpringBoot
项目只需通过 java–jar xx.jar
来运行。Servlet
容器:
+SpringBoot
可选择内嵌 Tomcat
、Jetty
或者 Undertow
,这样我们无须以 war
包形式部署项目。starter
简化 Maven
配置:
+Spring
提供了一系列的 starter
pom 来简化 Maven
的依赖加载,例如,当你使用了spring-boot-starter-web
时,会自动加入依赖包。Spring
:
+SpringBoot
会根据在类路径中的 jar 包、类,为 jar 包里的类自动配置 Bean,这样会极大地减少我们要使用的配置。当然,SpringBoot
只是考虑了大多数的开发场景,并不是所有的场景,若在实际开发中我们需要自动配置 Bean
,而 SpringBoot
没有提供支持,则可以自定义自动配置。SpringBoot
提供基于 http、ssh、telnet
对运行时的项目进行监控。SpringBoot
的神奇的不是借助于代码生成来实现的,而是通过条件注解来实现的,这是 Spring 4.x
提供的新特性。Spring 4.x
提倡使用 Java 配置和注解配置组合,而 SpringBoot
不需要任何 xml 配置即可实现 Spring
的所有配置。+ +
+ + + + + +原文地址:https://www.jianshu.com/p/0f967298a5d7
+语法糖(Syntactic Sugar
),也称糖衣语法,是由英国计算机学家 Peter.J.Landin
发明的一个术语,指在计算机语言中添加的某种语法,
+这种语法对语言的功能并没有影响,但是更方便程序员使用。简而言之,语法糖让程序更加简洁,有更高的可读性。
++在编程领域,除了语法糖,还有语法盐和语法糖精的说法,篇幅有限这里不做扩展了。 +我们所熟知的编程语言中几乎都有语法糖。作者认为,语法糖的多少是评判一个语言够不够牛逼的标准之一。
+
很多人说Java是一个“低糖语言”,其实从Java 7开始Java语言层面上一直在添加各种糖, +主要是在“Project Coin”项目下研发。尽管现在Java有人还是认为现在的Java是低糖,未来还会持续向着“高糖”的方向发展。
+前面提到过,语法糖的存在主要是方便开发人员使用。但其实,Java虚拟机并不支持这些语法糖。这些语法糖在编译阶段就会被还原成简单的基础语法结构,这个过程就是解语法糖。
+说到编译,大家肯定都知道,Java语言中,javac命令可以将后缀名为.java的源文件编译为后缀名为.class的可以运行于Java虚拟机的字节码。
+如果你去看com.sun.tools.javac.main.JavaCompiler
的源码,你会发现在compile()
中有一个步骤就是调用desugar()
,这个方法就是负责解语法糖的实现的。
Java 中最常用的语法糖主要有泛型、变长参数、条件编译、自动拆装箱、内部类等。本文主要来分析下这些语法糖背后的原理。一步一步剥去糖衣,看看其本质。
+前面提到过,从Java 7 开始,Java语言中的语法糖在逐渐丰富,其中一个比较重要的就是Java 7中switch开始支持String。
+在开始coding之前先科普下,Java中的swith自身原本就支持基本类型。比如int、char等。
+对于int类型,直接进行数值的比较。对于char类型则是比较其ascii码。
+所以,对于编译器来说,switch中其实只能使用整型,任何类型的比较都要转换成整型。比如byte。short,char
(ackii码是整型)以及int。
那么接下来看下switch对String得支持,有以下代码:
+public class switchDemoString {
+ public static void main(String[] args) {
+ String str = "world";
+ switch (str) {
+ case "hello":
+ System.out.println("hello");
+ break;
+ case "world":
+ System.out.println("world");
+ break;
+ default:
+ break;
+ }
+ }
+}
+
反编译后内容如下:
+public class switchDemoString
+{
+ public switchDemoString()
+ {
+ }
+ public static void main(String args[])
+ {
+ String str = "world";
+ String s;
+ switch((s = str).hashCode())
+ {
+ default:
+ break;
+ case 99162322:
+ if(s.equals("hello"))
+ System.out.println("hello");
+ break;
+ case 113318802:
+ if(s.equals("world"))
+ System.out.println("world");
+ break;
+ }
+ }
+}
+
看到这个代码,你知道原来字符串的switch是通过equals()和hashCode()方法来实现的。还好hashCode()方法返回的是int,而不是long。
+++仔细看下可以发现,进行switch的实际是哈希值,然后通过使用equals方法比较进行安全检查,这个检查是必要的, +因为哈希可能会发生碰撞。因此它的性能是不如使用枚举进行switch或者使用纯整数常量,但这也不是很差。
+
我们都知道,很多语言都是支持泛型的,但是很多人不知道的是,不同的编译器对于泛型的处理方式是不同的。
+通常情况下,一个编译器处理泛型有两种方式:Code specialization
和Code sharing
。
C++和C#是使用Code specialization
的处理机制,而Java使用的是Code sharing
的机制。
++Code sharing方式为每个泛型类型创建唯一的字节码表示,并且将该泛型类型的实例都映射到这个唯一的字节码表示上。将多种泛型类形实例映射到唯一的字节码表示是通过类型擦除(type erasue)实现的。 +也就是说,对于Java虚拟机来说,他根本不认识Map<String, String> map这样的语法。需要在编译阶段通过类型擦除的方式进行解语法糖。
+
类型擦除的主要过程如下:
+以下代码:
+Map<String, String> map = new HashMap<String, String>();
+map.put("name", "suxiansheng");
+map.put("wechat", "java");
+map.put("blog", "https://www.jianshu.com/u/94111742c97c");
+
解语法糖之后会变成:
+Map map = new HashMap();
+map.put("name", "suxiansheng");
+map.put("wechat", "Java");
+map.put("blog", "https://www.jianshu.com/u/94111742c97c");
+
以下代码:
+public static <A extends Comparable<A>> A max(Collection<A> xs) {
+ Iterator<A> xi = xs.iterator();
+ A w = xi.next();
+ while (xi.hasNext()) {
+ A x = xi.next();
+ if (w.compareTo(x) < 0)
+ w = x;
+ }
+ return w;
+}
+
类型擦除后会变成:
+ public static Comparable max(Collection xs){
+ Iterator xi = xs.iterator();
+ Comparable w = (Comparable)xi.next();
+ while(xi.hasNext())
+ {
+ Comparable x = (Comparable)xi.next();
+ if(w.compareTo(x) < 0)
+ w = x;
+ }
+ return w;
+}
+
虚拟机中没有泛型,只有普通类和普通方法,所有泛型类的类型参数在编译时都会被擦除,泛型类并没有自己独有的Class类对象。
+比如并不存在List<String>.class
或是List<Integer>.class
,而只有List.class
。
自动装箱就是Java自动将原始类型值转换成对应的对象,比如将int的变量转换成Integer对象,这个过程叫做装箱,反之将Integer对象转换成int类型值,这个过程叫做拆箱。参考:一文读懂什么是Java中的自动拆装箱
+因为这里的装箱和拆箱是自动进行的非人为转换,所以就称作为自动装箱和拆箱。
+原始类型byte, short, char, int, long, float, double,boolean
对应的封装类为Byte, Short, Character, Integer, Long, Float, Double, Boolean
。
先来看个自动装箱的代码:
+ public static void main(String[] args) {
+ int i = 10;
+ Integer n = i;
+}
+
反编译后代码如下:
+public static void main(String args[])
+{
+ int i = 10;
+ Integer n = Integer.valueOf(i);
+}
+
再来看个自动拆箱的代码:
+public static void main(String[] args) {
+
+ Integer i = 10;
+ int n = i;
+}
+
反编译后代码如下:
+public static void main(String args[])
+{
+ Integer i = Integer.valueOf(10);
+ int n = i.intValue();
+}
+
从反编译得到内容可以看出,在装箱的时候自动调用的是Integer的valueOf(int)方法。 +而在拆箱的时候自动调用的是Integer的intValue方法。 +所以,装箱过程是通过调用包装器的valueOf方法实现的,而拆箱过程是通过调用包装器的 xxxValue方法实现的。
+可变参数(variable arguments)是在Java 1.5中引入的一个特性。它允许一个方法把任意数量的值作为参数。
+看下以下可变参数代码,其中print方法接收可变参数:
+public static void main(String[] args)
+ {
+ print("java", "123", "456", "789");
+ }
+
+public static void print(String... strs)
+{
+ for (int i = 0; i < strs.length; i++)
+ {
+ System.out.println(strs[i]);
+ }
+}
+
反编译后代码:
+public static void main(String args[])
+{
+ print(new String[] {
+ "java", "123", "456", "789"
+ });
+}
+
+public static transient void print(String strs[])
+{
+ for(int i = 0; i < strs.length; i++)
+ System.out.println(strs[i]);
+
+}
+
从反编译后代码可以看出,可变参数在被使用的时候,他首先会创建一个数组,数组的长度就是调用该方法是传递的实参的个数, +然后再把参数值全部放到这个数组当中,然后再把这个数组作为参数传递到被调用的方法中。
+Java SE5提供了一种新的类型-Java的枚举类型,关键字enum可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的程序组件使用,这是一种非常有用的功能。
+要想看源码,首先得有一个类吧,那么枚举类型到底是什么类呢?是enum吗?
+答案很明显不是,enum就和class一样,只是一个关键字,他并不是一个类。
+那么枚举是由什么类维护的呢,我们简单的写一个枚举:
+public enum t {
+ SPRING,SUMMER;
+}
+
然后我们使用反编译,看看这段代码到底是怎么实现的,反编译后代码内容如下:
+public final class T extends Enum
+{
+ private T(String s, int i)
+ {
+ super(s, i);
+ }
+ public static T[] values()
+ {
+ T at[];
+ int i;
+ T at1[];
+ System.arraycopy(at = ENUM$VALUES, 0, at1 = new T[i = at.length], 0, i);
+ return at1;
+ }
+
+ public static T valueOf(String s)
+ {
+ return (T)Enum.valueOf(demo/T, s);
+ }
+
+ public static final T SPRING;
+ public static final T SUMMER;
+ private static final T ENUM$VALUES[];
+ static
+ {
+ SPRING = new T("SPRING", 0);
+ SUMMER = new T("SUMMER", 1);
+ ENUM$VALUES = (new T[] {
+ SPRING, SUMMER
+ });
+ }
+}
+
通过反编译后代码我们可以看到,public final class T extends Enum
,说明,
+该类是继承了Enum类的,同时final关键字告诉我们,这个类也是不能被继承的。
当我们使用enmu来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承。
+内部类又称为嵌套类,可以把内部类理解为外部类的一个普通成员。
+内部类之所以也是语法糖,是因为它仅仅是一个编译时的概念。
+outer.java
里面定义了一个内部类inner,一旦编译成功,就会生成两个完全不同的.class文件了,
+分别是outer.class
和outer$inner.class
。所以内部类的名字完全可以和它的外部类名字相同。
public class OutterClass {
+ private String userName;
+
+ public String getUserName() {
+ return userName;
+ }
+
+ public void setUserName(String userName) {
+ this.userName = userName;
+ }
+
+ public static void main(String[] args) {
+
+ }
+
+ class InnerClass{
+ private String name;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+ }
+}
+
以上代码编译后会生成两个class文件:OutterClass$InnerClass.class 、OutterClass.class
。
当我们尝试使用jad对OutterClass.class
文件进行反编译的时候,命令行会打印以下内容:
Parsing OutterClass.class...
+Parsing inner class OutterClass$InnerClass.class...
+Generating OutterClass.jad
+
他会把两个文件全部进行反编译,然后一起生成一个OutterClass.jad
文件。文件内容如下:
public class OutterClass
+{
+ class InnerClass
+ {
+ public String getName()
+ {
+ return name;
+ }
+ public void setName(String name)
+ {
+ this.name = name;
+ }
+ private String name;
+ final OutterClass this$0;
+
+ InnerClass()
+ {
+ this.this$0 = OutterClass.this;
+ super();
+ }
+ }
+
+ public OutterClass()
+ {
+ }
+ public String getUserName()
+ {
+ return userName;
+ }
+ public void setUserName(String userName){
+ this.userName = userName;
+ }
+ public static void main(String args1[])
+ {
+ }
+ private String userName;
+}
+
七 、条件编译
+—般情况下,程序中的每一行代码都要参加编译。 +但有时候出于对程序代码优化的考虑,希望只对其中一部分内容进行编译,此时就需要在程序中加上条件,让编译器只对满足条件的代码进行编译,将不满足条件的代码舍弃,这就是条件编译。
+如在C或CPP中,可以通过预处理语句来实现条件编译。其实在Java中也可实现条件编译。我们先来看一段代码:
+public class ConditionalCompilation {
+ public static void main(String[] args) {
+ final boolean DEBUG = true;
+ if(DEBUG) {
+ System.out.println("Java, DEBUG!");
+ }
+
+ final boolean ONLINE = false;
+
+ if(ONLINE){
+ System.out.println("Java, ONLINE!");
+ }
+ }
+}
+
反编译后代码如下:
+public class ConditionalCompilation
+{
+
+ public ConditionalCompilation()
+ {
+ }
+
+ public static void main(String args[])
+ {
+ boolean DEBUG = true;
+ System.out.println("Java, DEBUG!");
+ boolean ONLINE = false;
+ }
+}
+
首先,我们发现,在反编译后的代码中没有System.out.println("Hello, ONLINE!");
,这其实就是条件编译。
当if(ONLINE)为false的时候,编译器就没有对其内的代码进行编译。 +所以,Java语法的条件编译,是通过判断条件为常量的if语句实现的。根据if判断条件的真假,编译器直接把分支为false的代码块消除。 +通过该方式实现的条件编译,必须在方法体内实现,而无法在正整个Java类的结构或者类的属性上进行条件编译。
+这与C/C++的条件编译相比,确实更有局限性。在Java语言设计之初并没有引入条件编译的功能,虽有局限,但是总比没有更强。
+在Java中,assert关键字是从JAVA SE 1.4 引入的,为了避免和老版本的Java代码中使用了assert关键字导致错误, +Java在执行的时候默认是不启动断言检查的(这个时候,所有的断言语句都将忽略!)。
+如果要开启断言检查,则需要用开关-enableassertions
或-ea
来开启。
看一段包含断言的代码:
+public class AssertTest {
+ public static void main(String args[]) {
+ int a = 1;
+ int b = 1;
+ assert a == b;
+ System.out.println("Java");
+ assert a != b : "suxiansheng";
+ System.out.println("博客:https://www.jianshu.com/u/94111742c97c");
+ }
+}
+
反编译后代码如下:
+public class AssertTest {
+ public AssertTest()
+ {
+ }
+ public static void main(String args[])
+{
+ int a = 1;
+ int b = 1;
+ if(!$assertionsDisabled && a != b)
+ throw new AssertionError();
+ System.out.println("\u516C\u4F17\u53F7\uFF1AJava");
+ if(!$assertionsDisabled && a == b)
+ {
+ throw new AssertionError("Java");
+ } else
+ {
+ System.out.println("\u535A\u5BA2\uFF1Awww.jianshu.com/u/94111742c97c");
+ return;
+ }
+}
+
+static final boolean $assertionsDisabled = !com/hollis/suguar/AssertTest.desiredAssertionStatus();
+
+}
+
很明显,反编译之后的代码要比我们自己的代码复杂的多。所以,使用了assert这个语法糖我们节省了很多代码。
+其实断言的底层实现就是if语言,如果断言结果为true,则什么都不做,程序继续执行,如果断言结果为false,则程序抛出AssertError来打断程序的执行。
+在java 7中,数值字面量,不管是整数还是浮点数,都允许在数字之间插入任意多个下划线。这些下划线不会对字面量的数值产生影响,目的就是方便阅读。
+比如:
+public class Test {
+ public static void main(String... args) {
+ int i = 10_000;
+ System.out.println(i);
+ }
+}
+
反编译后:
+public class Test
+{
+ public static void main(String[] args)
+ {
+ int i = 10000;
+ System.out.println(i);
+ }
+}
+
反编译后就是把删除了。也就是说编译器并不认识在数字字面量中的,需要在编译阶段把他去掉。
+增强for循环(for-each)相信大家都不陌生,日常开发经常会用到的,他会比for循环要少写很多代码,那么这个语法糖背后是如何实现的呢?
+public static void main(String... args) {
+ String[] strs = {"suxiansehng", "Java", "博客:www.jianshu.com/u/94111742c97c"};
+ for (String s : strs) {
+ System.out.println(s);
+ }
+ List<String> strList = ImmutableList.of("suxiansheng", "java", "博客:www.jianshu.com/u/94111742c97c");
+ for (String s : strList) {
+ System.out.println(s);
+ }
+}
+
反编译后代码如下:
+public static transient void main(String args[])
+{
+ String strs[] = {
+ "suxiansheng", "\u516C\u4F17\u53F7\uFF1AJava", "\u535A\u5BA2\uFF1Awww.jianshu.com/u/94111742c97c"
+ };
+ String args1[] = strs;
+ int i = args1.length;
+ for(int j = 0; j < i; j++)
+ {
+ String s = args1[j];
+ System.out.println(s);
+ }
+
+ List strList = ImmutableList.of("suxiansheng", "\u516C\u4F17\u53F7\uFF1AJava", "\u535A\u5BA2\uFF1Awww.jianshu.com/u/94111742c97c");
+ String s;
+ for(Iterator iterator = strList.iterator(); iterator.hasNext(); System.out.println(s))
+ s = (String)iterator.next();
+
+}
+
代码很简单,for-each的实现原理其实就是使用了普通的for循环和迭代器。
+Java里,对于文件操作IO流、数据库连接等开销非常昂贵的资源,用完之后必须及时通过close方法将其关闭,否则资源会一直处于打开状态,可能会导致内存泄露等问题。
+关闭资源的常用方式就是在finally块里是释放,即调用close方法。比如,我们经常会写这样的代码:
+public static void main(String[] args) {
+ BufferedReader br = null;
+ try {
+ String line;
+ br = new BufferedReader(new FileReader("d:\\hollischuang.xml"));
+ while ((line = br.readLine()) != null) {
+ System.out.println(line);
+ }
+ } catch (IOException e) {
+ // handle exception
+ } finally {
+ try {
+ if (br != null) {
+ br.close();
+ }
+ } catch (IOException ex) {
+ // handle exception
+ }
+ }
+}
+
从Java 7开始,jdk提供了一种更好的方式关闭资源,使用try-with-resources
语句,改写一下上面的代码,效果如下:
public static void main(String... args) {
+ try (BufferedReader br = new BufferedReader(new FileReader("d:\\ hollischuang.xml"))) {
+ String line;
+ while ((line = br.readLine()) != null) {
+ System.out.println(line);
+ }
+ } catch (IOException e) {
+ // handle exception
+ }
+}
+
看,这简直是一大福音啊,虽然我之前一般使用IOUtils去关闭流,并不会使用在finally中写很多代码的方式,但是这种新的语法糖看上去好像优雅很多呢。
+反编译以上代码,看下他的背后原理:
+public static transient void main(String args[])
+ {
+ BufferedReader br;
+ Throwable throwable;
+ br = new BufferedReader(new FileReader("d:\\ hollischuang.xml"));
+ throwable = null;
+ String line;
+ try
+ {
+ while((line = br.readLine()) != null)
+ System.out.println(line);
+ }
+ catch(Throwable throwable2)
+ {
+ throwable = throwable2;
+ throw throwable2;
+ }
+ if(br != null)
+ if(throwable != null)
+ try
+ {
+ br.close();
+ }
+ catch(Throwable throwable1)
+ {
+ throwable.addSuppressed(throwable1);
+ }
+ else
+ br.close();
+ break MISSING_BLOCK_LABEL_113;
+ Exception exception;
+ exception;
+ if(br != null)
+ if(throwable != null)
+ try
+ {
+ br.close();
+ }
+ catch(Throwable throwable3)
+ {
+ throwable.addSuppressed(throwable3);
+ }
+ else
+ br.close();
+ throw exception;
+ IOException ioexception;
+ ioexception;
+ }
+}
+
其实背后的原理也很简单,那些我们没有做的关闭资源的操作,编译器都帮我们做了。
+所以,再次印证了,语法糖的作用就是方便程序员的使用,但最终还是要转成编译器认识的语言。
+关于lambda表达式,有人可能会有质疑,因为网上有人说他并不是语法糖。其实我想纠正下这个说法。
+Labmda表达式不是匿名内部类的语法糖,但是他也是一个语法糖。实现方式其实是依赖了几个JVM底层提供的lambda相关api。
+先来看一个简单的lambda表达式。遍历一个list:
+public static void main(String... args) {
+ List<String> strList = ImmutableList.of("suxiansheng", "Java", "博客:www.jianshu.com/u/94111742c97c");
+
+ strList.forEach( s -> { System.out.println(s); } );
+}
+
为啥说他并不是内部类的语法糖呢,前面讲内部类我们说过,内部类在编译之后会有两个class文件,但是,包含lambda表达式的类编译后只有一个文件。
+反编译后代码如下:
+public static /* varargs */ void main(String ... args) {
+ ImmutableList strList = ImmutableList.of((Object)"Java", (Object)"\u516c\u4f17\u53f7\uff1aJava", (Object)"\u535a\u5ba2\uff1awww.jianshu.com/u/94111742c97c");
+ strList.forEach((Consumer<String>)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)V, lambda$main$0(java.lang.String ), (Ljava/lang/String;)V)());
+}
+
+private static /* synthetic */ void lambda$main$0(String s) {
+ System.out.println(s);
+}
+
可以看到,在forEach方法中,其实是调用了java.lang.invoke.LambdaMetafactory#metafactory
方法,
+该方法的第四个参数implMethod指定了方法实现。可以看到这里其实是调用了一个lambda0方法进行了输出。
再来看一个稍微复杂一点的,先对List进行过滤,然后再输出:
+public static void main(String... args) {
+ List<String> strList = ImmutableList.of("suxiansheng", "Java, "博客:www.jianshu.com/u/94111742c97c");
+
+ List HollisList = strList.stream().filter(string -> string.contains("Java")).collect(Collectors.toList());
+
+ HollisList.forEach( s -> { System.out.println(s); } );
+}
+
反编译后代码如下:
+public static /* varargs */ void main(String ... args) {
+ ImmutableList strList = ImmutableList.of((Object)"Java", (Object)"\u516c\u4f17\u53f7\uff1aJava", (Object)"\u535a\u5ba2\uff1awww.jianshu.com/u/94111742c97c");
+ List<Object> HollisList = strList.stream().filter((Predicate<String>)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)Z, lambda$main$0(java.lang.String ), (Ljava/lang/String;)Z)()).collect(Collectors.toList());
+ HollisList.forEach((Consumer<Object>)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)V, lambda$main$1(java.lang.Object ), (Ljava/lang/Object;)V)());
+}
+
+private static /* synthetic */ void lambda$main$1(Object s) {
+ System.out.println(s);
+}
+
+private static /* synthetic */ boolean lambda$main$0(String string) {
+ return string.contains("Hollis");
+}
+
两个lambda表达式分别调用了lambda1和lambda0两个方法。
+所以,lambda表达式的实现其实是依赖了一些底层的api,在编译阶段,编译器会把lambda表达式进行解糖,转换成调用内部api的方式。
+public class GenericTypes {
+
+ public static void method(List<String> list) {
+ System.out.println("invoke method(List<String> list)");
+ }
+
+ public static void method(List<Integer> list) {
+ System.out.println("invoke method(List<Integer> list)");
+ }
+}
+
上面这段代码,有两个重载的函数,因为他们的参数类型不同,一个是List另一个是List, +但是,这段代码是编译通不过的。因为我们前面讲过,参数List和List编译之后都被擦除了,变成了一样的原生类型List,擦除动作导致这两个方法的特征签名变得一模一样。
+泛型的类型参数不能用在Java异常处理的catch语句中。因为异常处理是由JVM在运行时刻来进行的。由于类型信息被擦除,JVM是无法区分两个异常类型MyException
泛型——当泛型内包含静态变量
+public class StaticTest{
+ public static void main(String[] args){
+ GT<Integer> gti = new GT<Integer>();
+ gti.var=1;
+ GT<String> gts = new GT<String>();
+ gts.var=2;
+ System.out.println(gti.var);
+ }
+}
+class GT<T>{
+ public static int var=0;
+ public void nothing(T x){}
+}
+
以上代码输出结果为:2!由于经过类型擦除,所有的泛型类实例都关联到同一份字节码上,泛型类的所有静态变量是共享的。
+public static void main(String[] args) {
+ Integer a = 1000;
+ Integer b = 1000;
+ Integer c = 100;
+ Integer d = 100;
+ System.out.println("a == b is " + (a == b));
+ System.out.println(("c == d is " + (c == d)));
+}
+
输出结果:
+a == b is false
+c == d is true
+
在Java 5中,在Integer的操作上引入了一个新功能来节省内存和提高性能。整型对象通过使用相同的对象引用实现了缓存和重用。
+++适用于整数值区间
+-128 至 +127
。只适用于自动装箱。使用构造函数创建对象不适用。
for (Student stu : students) {
+ if (stu.getId() == 2)
+ students.remove(stu);
+}
+
会抛出ConcurrentModificationException
异常。
Iterator是工作在一个独立的线程中,并且拥有一个 mutex 锁。
+Iterator被创建之后会建立一个指向原来对象的单链索引表,当原来的对象数量发生变化时,这个索引表的内容不会同步改变,所以当索引指针往后移动的时候就找不到要迭代的对象,
+所以按照 fail-fast
原则 Iterator 会马上抛出java.util.ConcurrentModificationException
异常。
所以 Iterator 在工作的时候是不允许被迭代的对象被改变的。但你可以使用 Iterator 本身的方法remove()来删除对象,Iterator.remove()
方法会在删除当前迭代对象的同时维护索引的一致性。
+ +
+ + + + + +ArrayList线程不安全示例:
+ public static void main(String[] args) {
+ ArrayList<String> arrayList = new ArrayList<>();
+ for(int i=0; i< 3; i++) {
+ new Thread(() -> {
+ arrayList.add(UUID.randomUUID().toString());
+ System.out.println(arrayList);
+ },String.valueOf(i)).start();
+ }
+ }
+
// ConcurrentModificationException 同步修改异常
+Exception in thread "8" java.util.ConcurrentModificationException
+
+[null, 2041b613-8068-4ddd-9d01-305f5680d377]
+[null, 2041b613-8068-4ddd-9d01-305f5680d377, b3e0296d-e263-4632-a023-4267cdec5e25]
+[null, 2041b613-8068-4ddd-9d01-305f5680d377]
+
原因分析:
+当某个线程正在执行 add()
方法时,被某个线程打断,添加到一半被打断,没有被添加完
解决方案:
+Vector
来代替ArrayList
,Vector
是线程安全的ArrayList
,但是由于,并发量太小,被淘汰Collections.synchronizedArrayList(new ArrayList<>())
来创建ArrayList
.使用Collections
工具类来创建ArrayList
的思路是,在ArrayList
的外边套了一个外壳,来使ArrayList
线程安全new CopyOnWriteArrayList()
来保证ArrayList线程安全CopyWriteArrayList
字面意思就是在写的时候复制,思想就是读写分离的思想
以下是CopyOnWriteArrayList
的add()
方法源码
/** The array, accessed only via getArray/setArray. */
+ private transient volatile Object[] array;
+
+/** The lock protecting all mutators */
+ final transient ReentrantLock lock = new ReentrantLock();
+
+ /**
+ * Gets the array. Non-private so as to also be accessible
+ * from CopyOnWriteArraySet class.
+ */
+ final Object[] getArray() {
+ return array;
+ }
+
+/**
+ * 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();
+ }
+ }
+
因为在源码里面加了ReentrantLock
所以保证了某个线程在写的时候不会被打断,
+可以看到源码开始先是复制了一份数组(因为同一时刻只有一个线程写,其余的线程会读),在复制的数组上边进行写操作,写好以后在返回true
.
+这样写的就把读写进行了分离.写好以后因为array
加了volatile
关键字,所以该数组是对于其他的线程是可见的,就会读取到最新的值.
HashSet
和ArrayList
类似,也是线程不安全的集合类,具体证明HashSet
线程不安全的代码,请参考ArrayList
线程不安全的示例.
+因为与ArrayList类似,都属于一类问题,也会报ConcurrentModificationException
异常.
解决方案
+Collections.synchronizedSet(new HashSet<>())
使用集合工具类解决new CopyOnWriteArraySet<>()
来保证集合线程安全 private final CopyOnWriteArrayList<E> al;
+
+ /**
+ * Creates an empty set.
+ */
+ public CopyOnWriteArraySet() {
+ al = new CopyOnWriteArrayList<E>();
+ }
+
底层是CopyOnWriteArrayList
HashMap
也是线程不安全的集合类
解决方案
+Collections.synchronizedMap(new HashMap<>())
使用集合工具类new ConcurrentHashMap<>()
来保证线程安全在JDK1.7中ConcurrentHashMap采用了数组+Segment+分段锁的方式实现。
+Segment(分段锁) +ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表,同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
+内部结构 +ConcurrentHashMap使用分段锁技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,能够实现真正的并发访问。如下图是ConcurrentHashMap的内部结构图: +
+从上面的结构我们可以了解到,ConcurrentHashMap
定位一个元素的过程需要进行两次Hash操作。
第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部。
+该结构的优劣势
+JDK8中ConcurrentHashMap
参考了JDK8 HashMap的实现,采用了数组+链表+红黑树的实现方式来设计,内部大量采用CAS操作.如果不了解CAS请移步CAS原理
+JDK8中彻底放弃了Segment转而采用的是Node,其设计思想也不再是JDK1.7中的分段锁思想。
Node:保存key,value及key的hash值的数据结构。其中value和next都用volatile修饰,保证并发的可见性。 +Java8 ConcurrentHashMap结构基本上和Java8的HashMap一样,不过保证线程安全性。
+在JDK8中ConcurrentHashMap的结构,由于引入了红黑树,使得ConcurrentHashMap的实现非常复杂,我们都知道,红黑树是一种性能非常好的二叉查找树,其查找性能为O(logN),但是其实现过程也非常复杂,而且可读性也非常差,DougLea的思维能力确实不是一般人能比的,早期完全采用链表结构时Map的查找时间复杂度为O(N),JDK8中ConcurrentHashMap在链表的长度大于某个阈值的时候会将链表转换成红黑树进一步提高其查找性能。
+
+其实可以看出JDK1.8版本的ConcurrentHashMap
的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树。
CurrentHashMapJDK1.7,JDK1.8前后对比
++ +
+ + + + + +什么是事务?举个例子:你去超市买东西,“一手交钱,一手交货"就是一个事务的例子,交钱和交货必须同时成功,事务才算成功,其中有一个环节失败,事务将会撤销所有已成功的活动。
+所以事务可以看作是一次重大的活动,它由不同的小活动组成,这些活动要么全部成功,要么全部失败。
+在计算机系统中,更多的是通过关系型数据库来控制事务,这是利用数据库本身的事务特性来实现的,因此叫做数据库事务,由于应用程序主要靠关系型数据库来控制事务,而数据库通常和应用在同一个服务器上,所以基于关系型数据库的事务又叫做本地事务。
+数据库事务的四大特性:ACID:
+数据库事务在实现时将会将一次事务涉及的所有操作全部纳入到一个不可分割的执行单元,该执行单元中的所有操作要么都成功,要么都失败,只要其中任一操作执行失败,都将会导致事务回滚。
+随着互联网的发展,软件系统由原来的单体应用转变为分布式应用。分布式系统会把一个应用系统拆分为多个可独立部署的程序,因此需要服务与服务之间的远程协作才能完成事务操作,这种分布式系统环境下由不同服务之间通过网络协作完成的事务被称为分布式事务。 +例如:用户注册送积分事务、创建订单减少库存、银行转账事务。
+本地事务与分布式事务理解:
+1.本地事务:
+begin transation;
+// 1.本地数据库操作:张三减少金额
+// 2.本地数据库操作:李四增加金额
+commit transation;
+
2.分布式事务:
+begin transation;
+// 1.本地数据库操作:张三减少金额
+// 2.远程调用:李四增加金额
+commit transation;
+
在2.分布式事务
中如果李四增加金额成功,但是由于网络原因,远程调用并没有返回,此时本地事务提交失败就会回滚张三减少金额的操作,此时张三李四的金额就不一致了。
因此在分布式架构的基础上,传统数据库事务就无法使用了,张三和李四的账户不在同一个数据库中甚至不在一个系统中,实现转账事务需要通过远程调用,由于网络的问题就会导致分布式事务的问题。
+最典型的是微服务架构,微服务之间通过远程调用完成事务操作。比如:订单微服务和库存微服务,下单的同时订单微服务请求库存微服务减少库存。简而言之,就是跨JVM进程产生的分布式事务。
+单体系统访问多个数据库实例,就会产生分布式事务。比如:用户和订单信息分别存储在两个MySQL中,用户管理系统删除用户信息,需要分别删除用户信息及用户的订单信息,由于数据分布在不同的数据库,就需要通过不同的数据库连接去操作数据,此时产生分布式事务。
+多个微服务访问同一个数据库实例。比如:订单微服务和库存微服务即使访问同一个数据库也会产生分布式事务,原因是跨JVM进程,两个微服务持有了不同的数据库连接进行数据库操作,此时产生分布式事务。
+分布式事务之所以叫做分布式事务,是因为提供服务的各个结点分布在不同的机器上,相互之间通过网络交互。不能因为有网络问题就导致整个系统无法提供服务,网络因素成了分布式事务的考量标准之一。因此,分布式事务需要更进一步的理论支持。
+CAP是Consistency、Availability、Partition tolerance三个词语的缩写,分别表示一致性、可用性、分区容忍性。
+为了方便对CAP理论的理解,结合电商系统中的一些业务场景来理解CAP;如下图是商品信息管理的执行流程:
+ +整体执行流程:
+C-Consistency: +一致性是指写操作后的读操作可以读取到最新的数据状态,当数据分布在多个结点上,从任意结点读取到的数据都是最新状态。
+上图中,商品的信息的读写要满足一致性就是要实现如下目标:
+这里一致性指的就是主从数据库数据的一致性。
+如何实现一致性:
+分布式系统一致性的特点:
+A-Availability: +可用性是指任何事务操作都可以获得响应结果,且不会出现响应超时或响应错误。
+上图中,商品信息的读取满足可用性就是要实现如下目标:
+如何实现可用性:
+分布式系统可用性的特点:
+P-Partition tolerance: +分布式系统的各个结点部署在不同的子网,这就是网络分区,不可避免的会出现由于网络问题而导致结点之间的通信失败,此时仍可以对外提供服务,这就是分区容忍性。
+上图中,商品信息的读写满足分区容忍性就是要实现如下目标:
+如何实现分区容忍性:
+分布式分区容忍性特点:
+在所有分布式事务场景中不会同时具备CAP三个特征,因为在具备了P的前提下C和A是不能共存的。
+例如,下图满足了P分区容忍性
+ +上图分区容忍性含义:
+在分区容忍性存在的前提下,一致性和可用性存在矛盾:
+在分布式的环境下,一致性和可用性只能存在一种,即AP、CP。
+如果不在分布式的环境下,一致性和可用性其实是不矛盾的:
+CAP是一个已经被证实的理论:一个分布式系统最多只能同时满足一致性、可用性、分区容忍性中的两种。它可以作为我们进行架构设计、技术选型的考量标准。对于大型互联网应用场景来说,结点众多、部署分散,而且现在的集群规模越来越大,所以结点故障、网络故障是常态,而且要保证服务可用性达到N个9(99.99..%),并要达到良好的响应性能来提高用户的体验,因此一般都会做出以下选择:保证可用性和分区容忍性即AP,舍弃C,强一致性,保证最终一致性。
+++理解强一致性与最终一致性: +CAP理论说明一个分布式系统最多能同时满足一致性、可用性、分区容忍性这三项中的两项;其中AP在实际应用中比较多,AP舍弃一致性,保证可用性和分区容忍性,但是在实际生产中很多场景都要实现一致性,比如前边的例子,主数据库向从数据库同步数据,即使不要一致性,但是最终也要将数据同步成功来保证数据一致,这种一致性和CAP中的一致性不同,CAP中的一致性要求在任何时间查询每个结点数据都必须保证一致,它强调的是一致性; +但是最终一致性是允许可以在一段时间内每个结点数据不一致,但是经过一段时间后每个结点的数据必须一致,它强调的是数据的最终一致性。
+
BASE是Basically Available(基本可用)、Soft state(软状态)、Eventually consistent(最终一致性) 这三个短语的缩写。 +BASE理论是对于CAP中AP的一个扩展,通过牺牲强一致性来获得可用性,当出现故障允许的部分不可用但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终达到一致的状态。满足BASE理论的事务,我们称之为柔性事务。
+针对不同的分布式场景常见的解决方案有2PC、TCC、可靠消息最终一致性、最大努力通知这几种。
+2PC即两阶段提交协议,是将整个事务流程分为两个阶段:准备阶段(Prepare)、提交阶段(Commit),2是指两个阶段,P是准备阶段,C是提交阶段。
+举例,张三,李四聚餐,饭店老板要求先买单才能出票。张三李四都不愿请客,只能AA。只有张三和李四都付款,老板才能出票安排就餐。
+例子中形成了一个事务,若张三或李四其中一人拒绝付款,或者钱不够,老板都不会出票,并且把已收的钱退回。
+整个事务过程由事务管理器和参与者组成,老板就是事务管理器,张三、李四就是事务参与者,事务管理器负责抉择整个分布式事务的提交和回滚,事务参与者负责自己的本地事务提交和回滚。
+在计算机中部分关系型数据库如oracle、mysql支持两阶段提交协议:
+++Undo日志是记录修改前的数据,用于数据库回滚;Redo日志是记录修改后的数据,用于提交事务后写入数据文件。
+
++注意:必须在最后释放锁资源
+
成功情况: +
+失败情况: +
+2PC具体解决方案.
+2PC的传统方案是在数据库层面实现的,如:oracle、mysql都支持2PC协议,为了统一标准减少行业内不必要的对接成本,需要制定标准化的处理模型即接口标准,国际开放标准组织OpenGroup定义了分布式处理模型DTP(Distributed Transaction Processing Reference Model
)。
DTP模型定义如下角色:
+Application Program
即应用程序,可以理解为使用分布式事务的程序。Resource Manager
即资源管理器,可理解为事务的参与者,一般情况是指一个数据库实例,通过资源管理器对该数据库进行控制,资源管理器控制这分支事务。Transacition Manager
即事务管理器,负责协调事务和事务管理,它控制着全局的事务,管理事务的生命周期,并协调各个资源管理器。++全局事务是指分布式处理事务环境中,需要操作多个数据库共同完成一个动作,这个工作即是一个全局事务。
+
以新用户注册送积分为例: +
+执行流程如下:
+应用程序持有数据库和积分库两个数据源。
+应用程序通过事务管理器通知用户库的资源管理器新增用户,同时也通知积分库的资源管理器为该用户增加积分,资源管理器此时并未提交事务,此时用户和积分资源锁定。
+事务管理器收到执行回复,只要有一方失败则分别向其他方发起事务回滚,回滚完毕,资源释放锁;或事务管理器收到执行回复,全部成功,此时向所有资源管理器发起提交事务,提交完毕,资源释放。
+DTP模型定义TM和RM之间通讯的接口规范叫XA,简单理解为数据库提供的2PC接口协议,基于数据库的XA协议来实现2PC又称为XA方案。
+以上三个角色之间的交互方式如下:
+整个2PC的事务管理流程涉及到三个角色:AP、RM、TM。AP指的是使用2PC分布式事务的应用程序;RM指的是资源管理器,它控制这分支事务;TM指的是事务管理器,它控制着全局事务。
+在准备阶段RM执行实际的业务操作,但是不提交事务,资源锁定;在提交阶段TM会接受RM在准备阶段的执行回复,只要任何一个RM执行失败,TM会通知所有的RM进行回滚操作,否则TM将会通知RM提交事务。提交阶段结束释放资源。
+XA方案存在的问题:
+Seata是由阿里中间件团队发起的开源项目Fescar,后更名为Seata,它是一个开源的分布式事务框架。
+传统2PC的问题在Seata中的到了解决,它通过对本地关系型数据库的分支事务的协调来驱动完成全局事务,是工作在应用层的中间件。主要优点是性能较好,且不长时间占用连接资源,它以高效并对业务0入侵的方式解决微服务场景下面临的分布式事务问题,目前提供AT模式(即2PC)和TCC模式的分布式事务解决方案。
+Seata的设计思想:
+Seata的设计目标其一是对业务零侵入,因此从业务无侵入的2PC入手,在传统方案2PC的基础上演进,并解决2PC方案面临的问题。
+Seata把一个分布式的事务理解为一个包含了若干分支事务的全局事务。全局事务的职责是协调其下管辖的分支事务达成一致,要么一起成功提交事务,要么一起回滚失败。此外,通常分支事务本身就是一个关系型数据库的本地事务。
+ +与传统2PC的模型类似,Seata定义了3个组件来协调分布式事务的处理过程:
+ +Transaction Manager
事务管理器,需要嵌入(jar包)应用程序中工作,负责开启一个全局事务,并最终向TC发起全局提交或全局回滚的指令。Resource Manager
资源管理器,控制分支事务,负责分支事务注册,状态回报,并接收事务调节器的指令,驱动分支事务的提交和回滚。Transaction Coordinator
事务协调器,它是独立的中间件,需要独立运行部署,它维护全局事务的运行状态,接收TM指令发起全局事务的提交与回滚,负责与RM通信协调各个分支事务的提交回滚。以新用户注册送积分举例: +
+具体执行流程:
+++注意:在执行注册分支事务的时候,这里事务已经提交了,提交后可以释放资源,从而提升程序性能。
+
Seata实现2PC与传统2PC的差别:
+架构方面,传统2PC方案的RM实际上是在数据库层,RM本质上就是数据库自身,通过XA协议实现,而Seata的RM则是以jar包的形式作为中间件层部署在应用程序这一侧的。
+两阶段提交方面,传统2PC无论第二阶段的决策是commit还是rollback,事务性资源的锁都要保持到第二阶段才释放。而Seata的做法是在第一阶段就将本地事务提交,这样可以省去第二阶段持有锁的时间,提高整体效率。
+Seata原理简介:
+++两阶段提交协议的演变:
++
+- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
+- 二阶段: +
++
+- 提交异步化,非常快速地完成。
+- 回滚通过一阶段的回滚日志进行反向补偿。
+
一阶段:seata会拦截,解析SQL语义,找到SQL要更新的数据,在业务员数据更新前,将其保存为before image,随后执行业务SQL,在业务数据更新后,将其保存为after image,插入UNDO LOG回滚日志;提交前,向 TC 注册分支生成行锁。 +随后本地事务提交,将本地事务提交的结果上报给 TC,由TC协调。
+ +二阶段-提交:执行提交操作,说明SQL执行顺利,因业务SQL在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
+ +二阶段-回滚:要执行回滚操作,说明SQL执行不顺利,Seata需要回滚一阶段已经执行的业务SQL还原数据。回滚方式是用before image还原数据,但是在还原前还要校验脏写,对比数据库当前业务数据和after image。 +对比数据库当前业务数据和after image。
+ +针对不同的分布式场景常见的解决方案有2PC、TCC、可靠消息最终一致性、最大努力通知这几种。
+TCC是Try、Confirm、Cancel三个词语的缩写,TCC要求每个分支事务实现三个操作:预处理Try、确认Confirm、撤销Cancel。 +Try操作做业务检查及资源预留,Confirm做业务确认操作,Cancel实现一个与Try相反的操作,即回滚操作。TM首先发起所有分支事务的try操作,任何一个分支事务的try操作执行失败,TM将会发起所有分支事务的Cancel操作,若try操作全部成功,TM将发起所有分支事务的Confirm操作,其中Confirm或Cancel操作失败,TM会重试。
+ +分布式事务执行失败情况:
+ +TCC的三个阶段:
+TM事务管理器:
+TM发起全局事务时生成全局事务记录,全局事务ID贯穿整个分布式事务调用链,用来记录事务上下文,追踪和记录状态,由于Confirm和Cancel失败需要进行重试或人工处理,因此实现为幂等,幂等性是指同一个操作无论请求多少次,其结果都相同。
+Hmily是一个高性能分布式事务TCC开源框架。基于Java语言来开发(JDK1.8),支持Dubbo,Spring Cloud等RPC框架进行分布式事务。 +目前支持以下特性:
+Hmily利用AOP对参与分布式事务的本地方法与远程方法进行拦截处理,通过多方拦截,事务参与者能透明的调用到另一方的Try、Confirm、Cancel方法; +传递事务上下文;并记录事务日志,酌情进行补偿,重试等。
+Hmily是一个轻量级的TCC事务框架不需要部署独立的事务协调服务,但需要提供一个数据库来进行日志存储。
+Hmily实现的TCC服务与普通的服务一样,只需要暴露一个接口,也就是它的Try业务。Confirm/Cancel业务逻辑,只是因为全局事务提交/回滚的需要才提供的,因此Confirm/Cancel业务只需要被Hmily TCC事务框架发现即可,不需要被调用它的其他业务服务所感知。
+++要实现TCC协议,必须要实现三个方法:Try、Confirm、Cancel,其中最关键的方法是Try方法,Try方法是一个事务的起点。三个方法会由不同的线程来分别调用。
+
TCC需要注意三种异常处理:空回滚、幂等、悬挂:
+空回滚:在没有调用TCC资源Try方法的情况下,调用了二阶段的Cancel方法,Cancel方法需要识别出这是一个空回滚,然后直接返回成功。
+
+出现原因是当一个分支事务所在的服务器宕机或网络异常,分支事务调用记录为失败,这个时候其实是没有执行Try阶段,当故障恢复后,分布式事务进行回滚则会调用二阶段的Cancel方法,从而形成空回滚。
+
+解决思路关键就是要标识这个空回滚,就是要知道try阶段是否已经执行了,如果执行了那就是正常回滚;如果没执行,那就是空回滚。可以在try阶段执行完毕后向表里插入一条记录进行标识,如果记录存在则try阶段执行了,如果不存在try阶段则未执行。
幂等:为了保证TCC二阶段提交重试机制不会引发数据不一致,要求TCC二阶段Try、Confirm、Cancel接口保证幂等性,这样不会重复使用或示释放资源。如果幂等控制没有做好,很有可能导致数据不一致等严重问题。
+悬挂:悬挂就是对于一个分布式事务,其二阶段Cancel接口对比Try接口先执行。
+出现原因是在RPC调用分支事务时,先注册分支事务,在执行RPC调用,如果此时RPC调用的网络发生错误,RPC超时后,TM就会通知RM回滚该分布式事务,可能回滚完成后,RPC请求才到达,然后执行,而一个try方法预留的业务资源,只有该分布式事务才能使用,该分布式事务第一阶段预留的业务资源也就在也没有人能够处理,对于这种情况就称为悬挂,即业务资源预留后没办法处理。
+针对不同的分布式场景常见的解决方案有2PC、TCC、可靠消息最终一致性、最大努力通知这几种。
+可靠消息最终一致性是指当事务发起方执行完本地事务后并发出一条消息,事务参与方(消息消费者)一定能接收到消息并成功处理事务,此方案强调的是只要消息发给事务参与方最终事务要达到一致。
+此方案利用消息中间件完成,事务发起方将消息发送给消息中间件,事务参与方从消息中间件接收消息,事务发起方和消息中间件之间,事务参与方和消息中间件之间都是通过网络进行通信,由于网络通信的不稳定会导致分布式事务问题。
+ +因此可靠消息最终一致性方案要解决几个问题:
+本地消息表该方案最初是eBay提出,此方案的核心是通过本地事务保证数据业务操作和消息的一致性,然后通过定时任务将消息发送至消息中间件,待确认消息发送给消费方成功后在将消息删除。
+以用户注册送积分举例:用户服务负责添加用户,积分服务负责添加积分
+ +执行流程:
+如何保证将消息发送给消息队列?
+如何保证消费者一定能消费到消息?
+由于消息会重复投递,积分服务的增加积分操作功能要实现幂等性。
+RocketMQ是阿里巴巴的分布式消息中间件,于2012年开源并在2017年正式称为apache的项目。ApacheRocketMQ4.3之后的版本正式支持事务消息,并为分布式事务提供了便利性支持。
+RocketMQ事务消息设计则主要是为了解决生产者端的消息发送与本地事务执行的原子性问题,RockerMQ设计中broker与生产者端的双向通信能力,使得broker天生可以作为一个事务协调者存在;RockerMQ的高可用机制以及可靠消息设计原则则为事务消息在系统发生异常时依然能够保证达成事务的最终一致性。
+ +执行流程:
+注意:发起方与MQ之间的网络出现问题,但此时MQ里的消息不会一直存着;MQ服务会定回查消费者方本地事务状态(实现MQ事务回查接口),如果事务已经提交了消费者仍然可以接收到该消息,如果没有提交MQ则丢弃该消息。
+针对不同的分布式场景常见的解决方案有2PC、TCC、可靠消息最终一致性、最大努力通知这几种。
+顾名思义,发起通知方通过一定机制最大努力的将业务处理结果通知到接收方。
+举个例子:
+ +账户系统调用充值系统接口,充值系统完成处理后向账户账户接口发起充值结果通知,如果通知失败则充值充值系统则按策略进行重复通知;账户系统接收到充值系统的通知后修改充值状态。如果账户系统未接受到通知,账户系统会主动调用充值系统接口进行结果查询。
+最大努力通知特点:
+最大努力通知与可靠消息一致性不同点
+通过对最大努力通知的理解,采用MQ的ack机制可以实现最大努力通知。
+执行流程:
+方案1中接收通知方MQ接口,即接收通知方监听MQ,此方案主要应用于应用与内部应用之间的通知。
+交互流程:
+方案2中由于通知程序与MQ接口,通知程序监听MQ,收到MQ消息后由通知程序通过互联网接口协议调用接收通知方。此方案主要应用于外部应用之间的通知。
+2PC最大的诟病是一个阻塞协议。RM在执行分支事务后需要等待MT决定,此时服务会堵塞并锁定资源,由于其堵塞机制和最差时间复杂度较高,因此这种设计不能适应随着事务涉及的服务数量增多而扩展的需要,很难用于并发较高以及事务生命周期较长的分布式服务中。
+如果拿TCC的事务处理流程与2PC做比较,2PC通常都是在跨库的DB层面,而TCC则在应用层面的处理,需要通过业务逻辑来实现。这种分布式事务的实现方式优势在于,可以让应用自己定义数据库操作的粒度,使得降低锁冲突、提高吞吐量。不足之处是对应用的侵入性非常强,业务逻辑的每个分支都要实现try、confirm、cancel三个操作;此外实现难度也比较大,需要按照网络状态、系统故障的等不同失败原因进行不同回滚策略。
+可靠消息最终一致性适合执行周期长且实时性要求不高的业务场景。引入消息机制后,同步的事务操作变为基于消息执行的异步操作,避免了分布式事务中同步堵塞操作的影响,并实现了两个服务之间的解耦。
+最大努力通知是分布式事务中要求最低的一种,适用于一些最终一致性时间敏感度低的业务;允许发起通知方处理失败,在接收通知方收到失败通知后积极进行失败处理,无论发起通知方如何处理结果都不会影响到接收方的后续处理;发起通知方需要提供查询执行情况的接口,用于接收通知方校对结果。
++ | 2PC | +TCC | +可靠消息最终一致性 | +最大努力通知 | +
---|---|---|---|---|
一致性 | +强一致性 | +最终一致性 | +最终一致性 | +最终一致性 | +
吞吐量 | +低 | +中 | +高 | +高 | +
实现复杂度 | +易 | +难 | +中 | +易 | +
+ +
+ + + + + +以下内容摘自百度百科:
+ +网络协议指的是计算机网络中互相通信的对等实体之间交换信息时所必须遵守的规则的集合。
+为了使不同计算机厂家生产的计算机能够相互通信,以便在更大的范围内建立计算机网络,国际标准化组织(ISO)在1978年提出了“开放系统互联参考模型”,即著名的OSI/RM模型(Open System Interconnection/Reference Model)。
+国际标准化组织ISO 于1981年正式推荐了一个网络系统结构—-七层参考模型,叫做开放系统互连模型(Open System Interconnection,OSI)。由于这个标准模型的建立,使得各种计算机网络向它靠拢,大大推动了网络通信的发展。
+它将计算机网络体系结构的通信协议划分为七层,自下而上依次为:
+物理层(Physics Layer):物理层是OSI的第一层,它虽然处于最底层,却是整个开放系统的基础。物理层为设备之间的数据通信提供传输媒体及互连设备,为数据传输提供可靠的环境。以太网 · 调制解调器 · 电力线通信(PLC) · SONET/SDH · G.709 · 光导纤维 · 同轴电缆 · 双绞线等属于物理层;
+数据链路层(Data Link Layer):数据链路可以粗略地理解为数据通道。物理层要为终端设备间的数据通信提供传输媒体及其连接.媒体是长期的,连接是有生存期的.在连接生存期内,收发两端可以进行不等的一次或多次数据通信.每次通信都要经过建立通信联络和拆除通信联络两过程.这种建立起来的数据收发关系就叫作数据链路。Wi-Fi(IEEE 802.11) · WiMAX(IEEE 802.16) ·ATM · DTM · 令牌环 · 以太网 ·FDDI · 帧中继 · GPRS · EVDO ·HSPA · HDLC · PPP · L2TP ·PPTP · ISDN·STP · CSMA/CD等;
+网络层(Network Layer):这层对端到端的包传输进行定义,它定义了能够标识所有结点的逻辑地址,还定义了路由实现的方式和学习的方式。为了适应最大传输单元长度小于包长度的传输介质,网络层还定义了如何将一个包分解成更小的包的分段方法。IP (IPv4 · IPv6) · ICMP· ICMPv6·IGMP ·IS-IS · IPsec · ARP · RARP · RIP等属于网络层;
+传输层(Transport Layer):TCP · UDP · TLS · DCCP · SCTP · RSVP · OSPF 等;
+会话层(Session Layer):它定义了如何开始、控制和结束一个会话,包括对多个双向消息的控制和管理,以便在只完成连续消息的一部分时可以通知应用,从而使表示层看到的数据是连续的,在某些情况下,如果表示层收到了所有的数据,则用数据代表表示层。示例:RPC,SQL等;
+表示层(Presentation Layer):这一层的主要功能是定义数据格式及加密。例如,FTP允许你选择以二进制或ASCII格式传输。如果选择二进制,那么发送方和接收方不改变文件的内容。如果选择ASCII格式,发送方将把文本从发送方的字符集转换成标准的ASCII后发送数据。在接收方将标准的ASCII转换成接收方计算机的字符集。示例:加密,ASCII等;
+应用层(Application Layer):与其它计算机进行通讯的一个应用,它是对应应用程序的通信服务的。DHCP ·DNS · FTP · Gopher · HTTP· IMAP4 · IRC · NNTP · XMPP ·POP3 · SIP · SMTP ·SNMP · SSH ·TELNET · RPC · RTCP · RTP ·RTSP· SDP · SOAP · GTP · STUN · NTP· SSDP · BGP 等属于应用层协议;
+网络协议有很多种,具体选择哪一种协议则要看情况而定。Internet上的计算机使用的是TCP/IP协议。
+TCP/IP 协议是一个协议簇,包括很多协议。命名为 TCP/IP 协议的原因是 TCP 和 IP 这两个协议非常重要,应用很广。
+TCP/IP是因特网的正式网络协议,是一组在许多独立主机系统之间提供互联功能的协议,规范因特网上所有计算机互联时的传输、解释、执行、互操作,解决计算机系统的互联、互通、操作性,是被公认的网络通信协议的国际工业标准。TCP/IP是分组交换协议,信息被分成多个分组在网上传输,到达接收方后再把这些分组重新组合成原来的信息。除TCP/IP外,常用的网络协议还有PPP、SLIP等。
+++ +TCP/IP(Transport Control Protocol/Internet Protocol,传输控制协议/Internet协议)的历史应当追溯到Internet的前身—ARPAnet时代。为了实现不同网络之间的互连,美国国防部于1977年到1979年间制定了TCP/IP体系结构和协议。TCP/IP是由一组具有专业用途的多个子协议组合而成的,这些子协议包括TCP、IP、UDP、ARP、ICMP等。TCP/IP凭借其实现成本低、在多平台间通信安全可靠以及可路由性等优势迅速发展,并成为Internet中的标准协议。在上世纪90年代,TCP/IP已经成为局域网中的首选协议,在最新的操作系统(如Windows7、Windows XP、Windows Server2003等)中已经将TCP/IP作为其默认安装的通信协议。
+
TCP 和 UDP 还是HTTP都是 TCP/IP 协议簇里的一员。UDP、TCP处于 OSI 的传输层,而http协议是在tcp/ip协议模型上应用层的一种传输协议。
+TCP是Transmission Control Protocol的简称,中文名是传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议。
+特点:
+因为TCP协议是可靠性协议,即接收方收到的数据是完整,有序,无差错的,所以建立连接需要三次握手。
+UDP 是User Datagram Protocol的简称,中文名是用户数据报协议,是OSI参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。
+特点:
+HTTP 协议是 Hyper Text Transfer Protocol的缩写,超文本传输协议,是一个基于请求与响应,无状态的,应用层的协议,常基于TCP/IP协议传输数据,互联网上应用最为广泛的一种网络协议,所有的WWW文件都必须遵守这个标准。设计HTTP的初衷是为了提供一种发布和接收HTML页面的方法。
+特点:
+HTTPS 是Hyper Text Transfer Protocol over SecureSocket Layer的简称,是以安全为目标的 HTTP 通道,在HTTP的基础上通过传输加密和身份认证保证了传输过程的安全性。HTTPS 在HTTP 的基础下加入SSL,HTTPS 的安全基础是 SSL,因此加密的详细内容就需要 SSL。
+特点:
+Socket 也称作"套接字",用于描述 IP 地址和端口,是一个通信链的句柄,是应用层与传输层之间的桥梁。网络应用程序位于应用层,TCP 和 UDP 属于传输层协议,在应用层和传输层之间,就可以使用 Socket 来进行连接。 +即Socket 是传输层供给应用层的编程接口。
+拆包和粘包是在socket编程中经常出现的情况,在socket通讯过程中,如果通讯的一端一次性连续发送多条数据包,tcp协议会将多个数据包打包成一个tcp报文发送出去,这就是所谓的粘包。而如果通讯的一端发送的数据包超过一次tcp报文所能传输的最大值时,就会将一个数据包拆成多个最大tcp长度的tcp报文分开传输,这就叫做拆包。
+对于粘包的情况,要对粘在一起的包进行拆包。对于拆包的情况,要对被拆开的包进行粘包,即将一个被拆开的完整应用包再组合成一个完整包。比较通用的做法就是每次发送一个应用数据包前在前面加上四个字节的包长度值,指明这个应用包的真实长度。
++ +
+ + + + + +++Nginx (“engine x”)是一个高性能的HTTP和反向代理服务器,特点是占有内存少,并发能力强,事实上Nginx的并发能力确实在同类型的网页服务器中表现较好. +Nginx专为性能优化而开发,性能是其最重要的考量,实现上非常注重效率,能经受高负载的考验,有报告表明能支持高达50000个并发连接数。
+
Nginx 启动特别容易, 并且几乎可以做到7*24不间断运行,即使运行数个月也不需要重新启动. 你还能够不间断服务的情况下进行软件版本的升级。
+Nginx 静态处理性能比 Apache 高 3倍以上,Apache 对 PHP 支持比较简单, Nginx 需要配合其他后端来使用 ,Apache 的组件比 Nginx 多。
+Nginx的优势是处理静态请求,cpu内存使用率低,apache适合处理动态请求, 所以现在一般前端用nginx作为反向代理抗住压力,apache作为后端处理动态请求。
+apache是同步多进程模型,一个连接对应一个进程;nginx是异步的,多个连接(万级别)可以对应一个进程。所以在高连接并发的情况下,Nginx是Apache服务器不错的替代品。
+正向代理: +在客户端(浏览器)配置代理服务器,通过代理服务器进行互联网访问. 例如:在国内想通过网络翻墙,直接访问外网,访问失败, 这个时候就要通过代理服务器,我们访问代理服务器,让代理服务器访问外网,这样就能顺利翻墙。
+反向代理: +其实客户端对代理是无感知的,因为客户端不需要任何配置就可以访问,我们只需要将请求发送到反向代理服务器,由反向代理服务器去选择目标服务器获取数据后,在返回给客户端,此时反向代理服务器和目标服务器对外就是一个服务器,暴露的是代理服务器地址,隐藏了真实服务器IP地址。
+由于访问量的增加,单个服务器承受不了并发,我们增加服务器的数量,然后将请求分发到各个服务器上, 将原先请求集中到单个服务器上的情况改为将请求分发到多个服务器上,将负载分发到不同的服务器,也就是我们所说的负载均衡。 +
+为了加快网站的解析速度,降低原来单个服务器的压力,可以把动态页面和静态页面由不同的服务器来解析。
+ +如未安装docker移步安装 docker
+搜寻 nginx 镜像
+docker search nginx
+
拉取 nginx 镜像
+docker pull nginx
+
查看本地已经安装的镜像,是否有我们刚拉取的 nginx 镜像
+docker images
+
在本地创建对应 docekr 上 nginx 的配置文件和目录,方便管理。
+~/Documents/config/nginx
是我本地存放 nginx 的配置路径。路径哪里方便放哪里。
在上述路径下创建 conf.d
、www
文件夹
mkdir conf.d www
+
创建 nginx 临时容器,用于拷贝所需配置文件
+docker run --name tmp-nginx-container -d nginx
+
拷贝 nginx 配置文件
+docker cp tmp-nginx-container:/etc/nginx/nginx.conf ~/Documents/config/nginx/nginx.conf
+
拷贝站点配置文件
+docker cp tmp-nginx-container:/etc/nginx/conf.d/default.conf ~/Documents/config/nginx/conf.d/default.conf
+
删除 nginx 临时容器
+docker rm -f tmp-nginx-container
+
创建 nginx 容器,并映射 nginx 配置文件、站点配置文件目录和网站根目录;-v 是挂载的意思,将宿主机的文件映射到容器中
+docker run --name nginx -p 80:80 -v ~/Documents/config/nginx/nginx.conf:/etc/nginx/nginx.conf -v ~/Documents/config/nginx/conf.d:/etc/nginx/conf.d -v ~/Documents/config/nginx/www:/www -d nginx
+
拷贝 default.conf
为 test.conf
,并修改test.conf
中 server_name
文件
server {
+ listen 80;
+ listen [::]:80;
+ server_name www.test.com;
+
+ #charset koi8-r;
+ #access_log /var/log/nginx/host.access.log main;
+
+ location / {
+ root /www/test;
+ index index.html index.htm;
+ }
+
+ #error_page 404 /404.html;
+
+ # redirect server error pages to the static page /50x.html
+ #
+ error_page 500 502 503 504 /50x.html;
+ location = /50x.html {
+ root /usr/share/nginx/html;
+ }
+ # ...
+}
+
修改本地 /etc/hosts
文件;浏览器解析域名会先从 hosts 文件进行解析,如果没有的话,会从网络上进行解析。
127.0.0.1 www.test.com
+
在本地 ~/Documents/config/nginx/www
下新建 test 目录,并编写一个 index.html
测试文件
+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+</head>
+
+<body>
+ <h1>测试Nginx</h1>
+</body>
+
+</html>
+
重启 nginx
+docker restart nginx
+
浏览器访问http://www.test.com +
+######Nginx配置文件nginx.conf中文详解#####
+
+#定义Nginx运行的用户和用户组
+user www www;
+
+#nginx进程数,建议设置为等于CPU总核心数。
+worker_processes 8;
+
+#全局错误日志定义类型,[ debug | info | notice | warn | error | crit ]
+error_log /usr/local/nginx/logs/error.log info;
+
+#进程pid文件
+pid /usr/local/nginx/logs/nginx.pid;
+
+#指定进程可以打开的最大描述符:数目
+#工作模式与连接数上限
+#这个指令是指当一个nginx进程打开的最多文件描述符数目,理论值应该是最多打开文件数(ulimit -n)与nginx进程数相除,但是nginx分配请求并不是那么均匀,所以最好与ulimit -n 的值保持一致。
+#现在在linux 2.6内核下开启文件打开数为65535,worker_rlimit_nofile就相应应该填写65535。
+#这是因为nginx调度时分配请求到进程并不是那么的均衡,所以假如填写10240,总并发量达到3-4万时就有进程可能超过10240了,这时会返回502错误。
+worker_rlimit_nofile 65535;
+
+
+events
+{
+ #参考事件模型,use [ kqueue | rtsig | epoll | /dev/poll | select | poll ]; epoll模型
+ #是Linux 2.6以上版本内核中的高性能网络I/O模型,linux建议epoll,如果跑在FreeBSD上面,就用kqueue模型。
+ #补充说明:
+ #与apache相类,nginx针对不同的操作系统,有不同的事件模型
+ #A)标准事件模型
+ #Select、poll属于标准事件模型,如果当前系统不存在更有效的方法,nginx会选择select或poll
+ #B)高效事件模型
+ #Kqueue:使用于FreeBSD 4.1+, OpenBSD 2.9+, NetBSD 2.0 和 MacOS X.使用双处理器的MacOS X系统使用kqueue可能会造成内核崩溃。
+ #Epoll:使用于Linux内核2.6版本及以后的系统。
+ #/dev/poll:使用于Solaris 7 11/99+,HP/UX 11.22+ (eventport),IRIX 6.5.15+ 和 Tru64 UNIX 5.1A+。
+ #Eventport:使用于Solaris 10。 为了防止出现内核崩溃的问题, 有必要安装安全补丁。
+ use epoll;
+
+ #单个进程最大连接数(最大连接数=连接数*进程数)
+ #根据硬件调整,和前面工作进程配合起来用,尽量大,但是别把cpu跑到100%就行。每个进程允许的最多连接数,理论上每台nginx服务器的最大连接数为。
+ worker_connections 65535;
+
+ #keepalive超时时间。
+ keepalive_timeout 60;
+
+ #客户端请求头部的缓冲区大小。这个可以根据你的系统分页大小来设置,一般一个请求头的大小不会超过1k,不过由于一般系统分页都要大于1k,所以这里设置为分页大小。
+ #分页大小可以用命令getconf PAGESIZE 取得。
+ #[root@web001 ~]# getconf PAGESIZE
+ #4096
+ #但也有client_header_buffer_size超过4k的情况,但是client_header_buffer_size该值必须设置为“系统分页大小”的整倍数。
+ client_header_buffer_size 4k;
+
+ #这个将为打开文件指定缓存,默认是没有启用的,max指定缓存数量,建议和打开文件数一致,inactive是指经过多长时间文件没被请求后删除缓存。
+ open_file_cache max=65535 inactive=60s;
+
+ #这个是指多长时间检查一次缓存的有效信息。
+ #语法:open_file_cache_valid time 默认值:open_file_cache_valid 60 使用字段:http, server, location 这个指令指定了何时需要检查open_file_cache中缓存项目的有效信息.
+ open_file_cache_valid 80s;
+
+ #open_file_cache指令中的inactive参数时间内文件的最少使用次数,如果超过这个数字,文件描述符一直是在缓存中打开的,如上例,如果有一个文件在inactive时间内一次没被使用,它将被移除。
+ #语法:open_file_cache_min_uses number 默认值:open_file_cache_min_uses 1 使用字段:http, server, location 这个指令指定了在open_file_cache指令无效的参数中一定的时间范围内可以使用的最小文件数,如果使用更大的值,文件描述符在cache中总是打开状态.
+ open_file_cache_min_uses 1;
+
+ #语法:open_file_cache_errors on | off 默认值:open_file_cache_errors off 使用字段:http, server, location 这个指令指定是否在搜索一个文件是记录cache错误.
+ open_file_cache_errors on;
+}
+
+
+
+#设定http服务器,利用它的反向代理功能提供负载均衡支持
+http
+{
+ #文件扩展名与文件类型映射表
+ include mime.types;
+
+ #默认文件类型
+ default_type application/octet-stream;
+
+ #默认编码
+ #charset utf-8;
+
+ #服务器名字的hash表大小
+ #保存服务器名字的hash表是由指令server_names_hash_max_size 和server_names_hash_bucket_size所控制的。参数hash bucket size总是等于hash表的大小,并且是一路处理器缓存大小的倍数。在减少了在内存中的存取次数后,使在处理器中加速查找hash表键值成为可能。如果hash bucket size等于一路处理器缓存的大小,那么在查找键的时候,最坏的情况下在内存中查找的次数为2。第一次是确定存储单元的地址,第二次是在存储单元中查找键 值。因此,如果Nginx给出需要增大hash max size 或 hash bucket size的提示,那么首要的是增大前一个参数的大小.
+ server_names_hash_bucket_size 128;
+
+ #客户端请求头部的缓冲区大小。这个可以根据你的系统分页大小来设置,一般一个请求的头部大小不会超过1k,不过由于一般系统分页都要大于1k,所以这里设置为分页大小。分页大小可以用命令getconf PAGESIZE取得。
+ client_header_buffer_size 32k;
+
+ #客户请求头缓冲大小。nginx默认会用client_header_buffer_size这个buffer来读取header值,如果header过大,它会使用large_client_header_buffers来读取。
+ large_client_header_buffers 4 64k;
+
+ #设定通过nginx上传文件的大小
+ client_max_body_size 8m;
+
+ #开启高效文件传输模式,sendfile指令指定nginx是否调用sendfile函数来输出文件,对于普通应用设为 on,如果用来进行下载等应用磁盘IO重负载应用,可设置为off,以平衡磁盘与网络I/O处理速度,降低系统的负载。注意:如果图片显示不正常把这个改成off。
+ #sendfile指令指定 nginx 是否调用sendfile 函数(zero copy 方式)来输出文件,对于普通应用,必须设为on。如果用来进行下载等应用磁盘IO重负载应用,可设置为off,以平衡磁盘与网络IO处理速度,降低系统uptime。
+ sendfile on;
+
+ #开启目录列表访问,合适下载服务器,默认关闭。
+ autoindex on;
+
+ #此选项允许或禁止使用socke的TCP_CORK的选项,此选项仅在使用sendfile的时候使用
+ tcp_nopush on;
+
+ tcp_nodelay on;
+
+ #长连接超时时间,单位是秒
+ keepalive_timeout 120;
+
+ #FastCGI相关参数是为了改善网站的性能:减少资源占用,提高访问速度。下面参数看字面意思都能理解。
+ fastcgi_connect_timeout 300;
+ fastcgi_send_timeout 300;
+ fastcgi_read_timeout 300;
+ fastcgi_buffer_size 64k;
+ fastcgi_buffers 4 64k;
+ fastcgi_busy_buffers_size 128k;
+ fastcgi_temp_file_write_size 128k;
+
+ #gzip模块设置
+ gzip on; #开启gzip压缩输出
+ gzip_min_length 1k; #最小压缩文件大小
+ gzip_buffers 4 16k; #压缩缓冲区
+ gzip_http_version 1.0; #压缩版本(默认1.1,前端如果是squid2.5请使用1.0)
+ gzip_comp_level 2; #压缩等级
+ gzip_types text/plain application/x-javascript text/css application/xml; #压缩类型,默认就已经包含textml,所以下面就不用再写了,写上去也不会有问题,但是会有一个warn。
+ gzip_vary on;
+
+ #开启限制IP连接数的时候需要使用
+ #limit_zone crawler $binary_remote_addr 10m;
+
+
+
+ #负载均衡配置
+ upstream piao.jd.com {
+
+ #upstream的负载均衡,weight是权重,可以根据机器配置定义权重。weigth参数表示权值,权值越高被分配到的几率越大。
+ server 192.168.80.121:80 weight=3;
+ server 192.168.80.122:80 weight=2;
+ server 192.168.80.123:80 weight=3;
+
+ #nginx的upstream目前支持4种方式的分配
+ #1、轮询(默认)
+ #每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。
+ #2、weight
+ #指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。
+ #例如:
+ #upstream bakend {
+ # server 192.168.0.14 weight=10;
+ # server 192.168.0.15 weight=10;
+ #}
+ #2、ip_hash
+ #每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题。
+ #例如:
+ #upstream bakend {
+ # ip_hash;
+ # server 192.168.0.14:88;
+ # server 192.168.0.15:80;
+ #}
+ #3、fair(第三方)
+ #按后端服务器的响应时间来分配请求,响应时间短的优先分配。
+ #upstream backend {
+ # server server1;
+ # server server2;
+ # fair;
+ #}
+ #4、url_hash(第三方)
+ #按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,后端服务器为缓存时比较有效。
+ #例:在upstream中加入hash语句,server语句中不能写入weight等其他的参数,hash_method是使用的hash算法
+ #upstream backend {
+ # server squid1:3128;
+ # server squid2:3128;
+ # hash $request_uri;
+ # hash_method crc32;
+ #}
+
+ #tips:
+ #upstream bakend{#定义负载均衡设备的Ip及设备状态}{
+ # ip_hash;
+ # server 127.0.0.1:9090 down;
+ # server 127.0.0.1:8080 weight=2;
+ # server 127.0.0.1:6060;
+ # server 127.0.0.1:7070 backup;
+ #}
+ #在需要使用负载均衡的server中增加 proxy_pass http://bakend/;
+
+ #每个设备的状态设置为:
+ #1.down表示单前的server暂时不参与负载
+ #2.weight为weight越大,负载的权重就越大。
+ #3.max_fails:允许请求失败的次数默认为1.当超过最大次数时,返回proxy_next_upstream模块定义的错误
+ #4.fail_timeout:max_fails次失败后,暂停的时间。
+ #5.backup: 其它所有的非backup机器down或者忙的时候,请求backup机器。所以这台机器压力会最轻。
+
+ #nginx支持同时设置多组的负载均衡,用来给不用的server来使用。
+ #client_body_in_file_only设置为On 可以讲client post过来的数据记录到文件中用来做debug
+ #client_body_temp_path设置记录文件的目录 可以设置最多3层目录
+ #location对URL进行匹配.可以进行重定向或者进行新的代理 负载均衡
+ }
+
+
+
+ #虚拟主机的配置
+ server
+ {
+ #监听端口
+ listen 80;
+
+ #域名可以有多个,用空格隔开
+ server_name www.jd.com jd.com;
+ index index.html index.htm index.php;
+ root /data/www/jd;
+
+ #fastcgi解析php
+ location ~ .*.(php|php5)?$
+ {
+#此处有两种方式去和php-fpm交互,一种是9000端口,另一种是使用socket连接
+ fastcgi_pass 127.0.0.1:9000;
+ fastcgi_index index.php;
+ include fastcgi.conf;
+ }
+
+ #图片缓存时间设置
+ location ~ .*.(gif|jpg|jpeg|png|bmp|swf)$
+ {
+ expires 10d;
+ }
+
+ #JS和CSS缓存时间设置
+ location ~ .*.(js|css)?$
+ {
+ expires 1h;
+ }
+
+ #日志格式设定
+ #$remote_addr与$http_x_forwarded_for用以记录客户端的ip地址;
+ #$remote_user:用来记录客户端用户名称;
+ #$time_local: 用来记录访问时间与时区;
+ #$request: 用来记录请求的url与http协议;
+ #$status: 用来记录请求状态;成功是200,
+ #$body_bytes_sent :记录发送给客户端文件主体内容大小;
+ #$http_referer:用来记录从那个页面链接访问过来的;
+ #$http_user_agent:记录客户浏览器的相关信息;
+ #通常web服务器放在反向代理的后面,这样就不能获取到客户的IP地址了,通过$remote_add拿到的IP地址是反向代理服务器的iP地址。反向代理服务器在转发请求的http头信息中,可以增加x_forwarded_for信息,用以记录原有客户端的IP地址和原来客户端的请求的服务器地址。
+ log_format access '$remote_addr - $remote_user [$time_local] "$request" '
+ '$status $body_bytes_sent "$http_referer" '
+ '"$http_user_agent" $http_x_forwarded_for';
+
+ #定义本虚拟主机的访问日志
+ access_log /usr/local/nginx/logs/host.access.log main;
+ access_log /usr/local/nginx/logs/host.access.404.log log404;
+
+ #对 "/" 启用反向代理
+ location / {
+ proxy_pass http://127.0.0.1:88;
+ proxy_redirect off;
+ proxy_set_header X-Real-IP $remote_addr;
+
+ #后端的Web服务器可以通过X-Forwarded-For获取用户真实IP
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+
+ #以下是一些反向代理的配置,可选。
+ proxy_set_header Host $host;
+
+ #允许客户端请求的最大单文件字节数
+ client_max_body_size 10m;
+
+ #缓冲区代理缓冲用户端请求的最大字节数,
+ #如果把它设置为比较大的数值,例如256k,那么,无论使用firefox还是IE浏览器,来提交任意小于256k的图片,都很正常。如果注释该指令,使用默认的client_body_buffer_size设置,也就是操作系统页面大小的两倍,8k或者16k,问题就出现了。
+ #无论使用firefox4.0还是IE8.0,提交一个比较大,200k左右的图片,都返回500 Internal Server Error错误
+ client_body_buffer_size 128k;
+
+ #表示使nginx阻止HTTP应答代码为400或者更高的应答。
+ proxy_intercept_errors on;
+
+ #后端服务器连接的超时时间_发起握手等候响应超时时间
+ #nginx跟后端服务器连接超时时间(代理连接超时)
+ proxy_connect_timeout 90;
+
+ #后端服务器数据回传时间(代理发送超时)
+ #后端服务器数据回传时间_就是在规定时间之内后端服务器必须传完所有的数据
+ proxy_send_timeout 90;
+
+ #连接成功后,后端服务器响应时间(代理接收超时)
+ #连接成功后_等候后端服务器响应时间_其实已经进入后端的排队之中等候处理(也可以说是后端服务器处理请求的时间)
+ proxy_read_timeout 90;
+
+ #设置代理服务器(nginx)保存用户头信息的缓冲区大小
+ #设置从被代理服务器读取的第一部分应答的缓冲区大小,通常情况下这部分应答中包含一个小的应答头,默认情况下这个值的大小为指令proxy_buffers中指定的一个缓冲区的大小,不过可以将其设置为更小
+ proxy_buffer_size 4k;
+
+ #proxy_buffers缓冲区,网页平均在32k以下的设置
+ #设置用于读取应答(来自被代理服务器)的缓冲区数目和大小,默认情况也为分页大小,根据操作系统的不同可能是4k或者8k
+ proxy_buffers 4 32k;
+
+ #高负荷下缓冲大小(proxy_buffers*2)
+ proxy_busy_buffers_size 64k;
+
+ #设置在写入proxy_temp_path时数据的大小,预防一个工作进程在传递文件时阻塞太长
+ #设定缓存文件夹大小,大于这个值,将从upstream服务器传
+ proxy_temp_file_write_size 64k;
+ }
+
+
+ #设定查看Nginx状态的地址
+ location /NginxStatus {
+ stub_status on;
+ access_log on;
+ auth_basic "NginxStatus";
+ auth_basic_user_file confpasswd;
+ #htpasswd文件的内容可以用apache提供的htpasswd工具来产生。
+ }
+
+ #本地动静分离反向代理配置
+ #所有jsp的页面均交由tomcat或resin处理
+ location ~ .(jsp|jspx|do)?$ {
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_pass http://127.0.0.1:8080;
+ }
+
+ #所有静态文件由nginx直接读取不经过tomcat或resin
+ location ~ .*.(htm|html|gif|jpg|jpeg|png|bmp|swf|ioc|rar|zip|txt|flv|mid|doc|ppt|
+ pdf|xls|mp3|wma)$
+ {
+ expires 15d;
+ }
+
+ location ~ .*.(js|css)?$
+ {
+ expires 1h;
+ }
+ }
+}
+######Nginx配置文件nginx.conf中文详解#####
+
+ +
+ + + + + +<dependencies>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-web</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-test</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.projectlombok</groupId>
+ <artifactId>lombok</artifactId>
+ <optional>true</optional>
+ </dependency>
+
+ <dependency>
+ <groupId>cn.hutool</groupId>
+ <artifactId>hutool-all</artifactId>
+ <version>5.8.11</version>
+ </dependency>
+
+ <dependency>
+ <groupId>com.alipay.sdk</groupId>
+ <artifactId>alipay-sdk-java</artifactId>
+ <version>4.9.9</version>
+ </dependency>
+
+ <dependency>
+ <groupId>com.github.binarywang</groupId>
+ <artifactId>weixin-java-pay</artifactId>
+ <version>4.5.0</version>
+ </dependency>
+
+</dependencies>
+
server:
+ port: 8080
+
+pay:
+ wechat:
+ #微信公众号或者小程序等的appid
+ appId: ""
+ #微信支付商户号
+ mchId: ""
+ #微信支付商户密钥
+ mchKey: ""
+ #服务商模式下的子商户公众账号ID
+ subAppId:
+ #服务商模式下的子商户号
+ subMchId:
+ # p12证书的位置,可以指定绝对路径,也可以指定类路径(以classpath:开头)
+ keyPath:
+ alipay:
+ # 应用ID,您的APPID,收款账号既是您的APPID对应支付宝账号
+ appId: ""
+ # 商户私钥,您的PKCS8格式RSA2私钥
+ merchantPrivateKey: ""
+ # 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。
+ alipayPublicKey: ""
+ # 服务器异步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
+ notifyUrl: "http://localhost:8080/ali/paymentNotify"
+ # 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
+ returnUrl: "http://localhost:8080/ali/paymentNotify"
+ # 签名方式
+ signType: "RSA2"
+ # 字符编码格式
+ charset: "utf-8"
+ # 格式
+ format: "json"
+ # 支付宝网关
+ gatewayUrl: "https://openapi.alipaydev.com/gateway.do"
+
public interface PayService {
+
+
+ /**
+ * 获取支付平台类型
+ *
+ * @return 支付平台类型
+ */
+ PayPlatformTypeEnum getPayPlatformType();
+
+
+ /**
+ * 支付时可能存在的行为; 支付订单 查询订单 生成支付二维码 退款等
+ *
+ * @param orderInfo 订单信息
+ * @return any
+ */
+ <T extends CommonPayRequest> Object apply(T orderInfo);
+
+}
+
public interface PayRefundAble extends PayService{
+}
+
public interface PayOrderQueryAble extends PayService{
+}
+
public interface PayOrderCloseAble extends PayService{
+}
+
public interface PaymentAble extends PayService{
+}
+
@Slf4j
+public abstract class AbstractPayService<Q extends CommonPayRequest,S> implements PayService{
+
+
+ @Override
+ @SneakyThrows
+ public Object apply(CommonPayRequest payRequest) {
+ Q orderInfo = (Q) payRequest;
+
+ // 前置处理
+ log.debug("开始执行前置处理.");
+ if (!preprocessing(orderInfo)) {
+ throw new PayException("支付前置处理失败,不能继续执行");
+ }
+ log.debug("前置处理执行完毕");
+
+ // 执行
+ log.info("开始执行,请求参数 {}", JSON.toJSONString(orderInfo));
+ S result = processing(orderInfo);
+ log.info("执行完毕,响应参数 {}", JSON.toJSONString(result));
+
+ // 后置处理
+ log.debug("开始执行后置处理.");
+ postprocessing(orderInfo);
+ log.debug("后置处理执行完毕.");
+ return result;
+ }
+
+
+
+
+ /**
+ * 前置处理
+ *
+ * @param orderInfo 订单信息
+ * @return true 通过 ; false 不通过
+ */
+ protected boolean preprocessing(Q orderInfo){
+ return true;
+ }
+
+
+ /**
+ * 核心处理方法
+ *
+ * @param orderInfo 订单信息
+ * @return 支付结果
+ */
+ protected abstract S processing(Q orderInfo) throws PayException;
+
+
+ /**
+ * 后置处理
+ *
+ * @param orderInfo 订单信息
+ */
+ protected void postprocessing(Q orderInfo){
+ }
+
+
+}
+
public interface CommonPayRequest {
+
+
+ /**
+ * 获取平台类型
+ */
+ PayPlatformTypeEnum getPayTypeEnum();
+
+
+}
+
public enum PayPlatformTypeEnum {
+
+ // 支付平台
+ ALIPAY,
+ WECHAT,
+
+}
+
public interface PayFacade {
+
+
+ /**
+ * 支付调用接口(指定操作传入class,传入请求参数)
+ *
+ * @param <T> 支付操作类型 {@linkplain PayService}
+ * @param orderInfo 订单信息
+ */
+ <T extends PayService> Object execute(Class<T> payClass, CommonPayRequest orderInfo);
+}
+
@Slf4j
+@Component
+public class PayFacadeImpl implements PayFacade {
+
+
+ @Override
+ public <T extends PayService> Object execute(Class<T> payClass, CommonPayRequest orderInfo){
+ Optional<T> payItem = new ArrayList<>(SpringUtil.getBeansOfType(payClass).values())
+ .stream()
+ .filter(item -> item.getPayPlatformType().equals(orderInfo.getPayTypeEnum()))
+ .findFirst();
+ log.info("支付行为[{}],调用平台类型[{}]",payClass.getSimpleName(),orderInfo.getPayTypeEnum());
+
+ @SuppressWarnings("unchecked")
+ Optional<PayService> payService = (Optional<PayService>) payItem;
+ if (payService.isPresent()) {
+ return payService.get().apply(orderInfo);
+ }
+ throw new IllegalArgumentException("未获取到支付平台类型");
+ }
+
+
+}
+
public class PayException extends RuntimeException{
+
+
+ private String msg;
+
+ public PayException(String message) {
+ super(message);
+ this.msg = message;
+ }
+
+ public PayException(String message, Throwable cause) {
+ super(message, cause);
+ this.msg = message;
+ }
+
+}
+
@Service
+public class AlipayRefundService extends AbstractPayService<AlipayRefundRequest,AlipayTradeRefundResponse> implements PayRefundAble {
+
+ @Autowired
+ private AlipayClient alipayClient;
+
+ @Override
+ protected AlipayTradeRefundResponse processing(AlipayRefundRequest orderInfo) throws PayException {
+ AlipayTradeRefundRequest alipayRequest = new AlipayTradeRefundRequest ();
+ alipayRequest.setBizContent(JSON.toJSONString(orderInfo));
+ AlipayTradeRefundResponse response = null;
+ try {
+ response = alipayClient.execute(alipayRequest);
+ } catch (AlipayApiException e) {
+ throw new PayException(e.getMessage(),e);
+ }
+ if (response.isSuccess()) {
+ // 退款成功
+ return response;
+ }
+ return response;
+ }
+
+
+ @Override
+ public PayPlatformTypeEnum getPayPlatformType() {
+ return PayPlatformTypeEnum.ALIPAY;
+ }
+
+
+}
+
@Slf4j
+@Service
+public class AlipayPayOrderQueryService extends AbstractPayService<AlipayQueryOrderRequest,AlipayTradeQueryResponse> implements PayOrderQueryAble {
+
+
+ @Autowired
+ private AlipayClient alipayClient;
+
+
+ @Override
+ protected AlipayTradeQueryResponse processing(AlipayQueryOrderRequest orderInfo) throws PayException {
+ AlipayTradeQueryRequest alipayRequest = new AlipayTradeQueryRequest();
+ alipayRequest.setBizContent(JSON.toJSONString(orderInfo));
+ AlipayTradeQueryResponse response = null;
+ try {
+ response = alipayClient.execute(alipayRequest);
+ } catch (AlipayApiException e) {
+ throw new PayException(e.getMessage(),e);
+ }
+ if (response.isSuccess()) {
+ // 支付成功
+ return response;
+ }
+ return response;
+ }
+
+
+ @Override
+ public PayPlatformTypeEnum getPayPlatformType() {
+ return PayPlatformTypeEnum.ALIPAY;
+ }
+}
+
@Slf4j
+@Service
+public class AlipayPaymentService extends AbstractPayService<AlipayPaymentRequest, AlipayTradePagePayResponse> implements PaymentAble {
+
+
+ @Autowired
+ private AlipayClient alipayClient;
+
+ @Autowired
+ private AlipayProperties aliPayProperties;
+
+
+ protected AlipayTradePagePayRequest buildUnifiedOrderRequest(AlipayPaymentRequest alipayPaymentRequest) {
+ AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
+ alipayRequest.setReturnUrl(aliPayProperties.getReturnUrl());
+ alipayRequest.setNotifyUrl(aliPayProperties.getNotifyUrl());
+ alipayRequest.setBizContent(JSON.toJSONString(alipayPaymentRequest));
+ return alipayRequest;
+ }
+
+
+ @Override
+ protected AlipayTradePagePayResponse processing(AlipayPaymentRequest alipayPaymentRequest) throws PayException {
+ try {
+ return alipayClient.pageExecute(buildUnifiedOrderRequest(alipayPaymentRequest));
+ } catch (AlipayApiException e) {
+ throw new PayException(e.getMessage(), e);
+ }
+ }
+
+
+ @Override
+ public PayPlatformTypeEnum getPayPlatformType() {
+ return PayPlatformTypeEnum.ALIPAY;
+ }
+
+
+}
+
@Service
+public class AlipayOrderCloseService extends AbstractPayService<AlipayOrderCloseRequest,AlipayTradeCloseResponse> implements PayOrderCloseAble {
+
+
+ @Autowired
+ private AlipayClient alipayClient;
+
+
+ @Override
+ protected AlipayTradeCloseResponse processing(AlipayOrderCloseRequest orderInfo) throws PayException {
+ AlipayTradeCloseRequest alipayRequest = new AlipayTradeCloseRequest();
+ alipayRequest.setBizContent(JSON.toJSONString(orderInfo));
+ AlipayTradeCloseResponse response = null;
+ try {
+ response = alipayClient.execute(alipayRequest);
+ } catch (AlipayApiException e) {
+ throw new PayException(e.getMessage(),e);
+ }
+ if (response.isSuccess()) {
+ // 关闭成功
+ return response;
+ }
+ return response;
+ }
+
+
+
+ @Override
+ public PayPlatformTypeEnum getPayPlatformType() {
+ return PayPlatformTypeEnum.ALIPAY;
+ }
+
+
+}
+
@Service
+public class AliApp extends AlipayPaymentService {
+
+ @Override
+ protected AlipayTradePagePayRequest buildUnifiedOrderRequest(AlipayPaymentRequest alipayPaymentRequest) {
+ alipayPaymentRequest.setProduct_code("QUICK_MSECURITY_PAY");
+ return super.buildUnifiedOrderRequest(alipayPaymentRequest);
+ }
+
+
+}
+
@Service
+public class AliH5 extends AlipayPaymentService {
+
+ @Override
+ protected AlipayTradePagePayRequest buildUnifiedOrderRequest(AlipayPaymentRequest alipayPaymentRequest) {
+ alipayPaymentRequest.setProduct_code("QUICK_WAP_WAY");
+ return super.buildUnifiedOrderRequest(alipayPaymentRequest);
+ }
+
+
+}
+
@Service
+public class AliPc extends AlipayPaymentService {
+
+ @Override
+ protected AlipayTradePagePayRequest buildUnifiedOrderRequest(AlipayPaymentRequest alipayPaymentRequest) {
+ alipayPaymentRequest.setProduct_code("FAST_INSTANT_TRADE_PAY");
+ return super.buildUnifiedOrderRequest(alipayPaymentRequest);
+ }
+
+}
+
@Data
+@Accessors(chain = true)
+public class AlipayOrderCloseRequest implements CommonPayRequest {
+
+ /**
+ * 该交易在支付宝系统中的交易流水号
+ */
+ private String trade_no;
+
+ /**
+ * 订单支付时传入的商户订单号,和支付宝交易号不能同时为空。 trade_no,out_trade_no如果同时存在优先取trade_no
+ */
+ private String out_trade_no;
+
+
+ @Override
+ public PayPlatformTypeEnum getPayTypeEnum() {
+ return PayPlatformTypeEnum.ALIPAY;
+
+ }
+
+}
+
@Data
+@Accessors(chain = true)
+public class AlipayPaymentRequest implements CommonPayRequest {
+
+ /**
+ * 商户订单号
+ */
+ private String out_trade_no;
+
+ /**
+ * 订单名称
+ */
+ private String subject;
+
+ /**
+ * 付款金额
+ */
+ private String total_amount;
+
+ /**
+ * 商品描述
+ */
+ private String body;
+
+ /**
+ * 超时时间参数
+ */
+ private String timeout_express = "60m";
+
+ /**
+ * 产品编号
+ */
+ private String product_code;
+
+ @Override
+ public PayPlatformTypeEnum getPayTypeEnum() {
+ return PayPlatformTypeEnum.ALIPAY;
+ }
+
+}
+
@Data
+@Accessors(chain = true)
+public class AlipayQueryOrderRequest implements CommonPayRequest {
+
+
+ /**
+ * 商户订单号;
+ */
+ private String out_trade_no;
+
+
+ @Override
+ public PayPlatformTypeEnum getPayTypeEnum() {
+ return PayPlatformTypeEnum.ALIPAY;
+ }
+
+
+}
+
@Data
+@Accessors(chain = true)
+public class AlipayRefundRequest implements CommonPayRequest {
+
+ /**
+ * 支付宝交易号
+ */
+ private String trade_no;
+
+ /**
+ * 商户订单号
+ */
+ private String out_trade_no;
+
+ /**
+ * 退款请求号
+ */
+ private String out_request_no;
+
+ /**
+ * 退款金额
+ */
+ private String refund_amount;
+
+
+ @Override
+ public PayPlatformTypeEnum getPayTypeEnum() {
+ return PayPlatformTypeEnum.ALIPAY;
+ }
+
+}
+
@Configuration
+@EnableConfigurationProperties(AlipayProperties.class)
+@AllArgsConstructor
+public class AlipayConfiguration {
+
+ private AlipayProperties aliPayProperties;
+
+
+ @Bean
+ public AlipayClient alipayClient() {
+ return new DefaultAlipayClient(
+ aliPayProperties.getGatewayUrl(),
+ aliPayProperties.getAppId(),
+ aliPayProperties.getMerchantPrivateKey(),
+ aliPayProperties.getFormat(),
+ aliPayProperties.getCharset(),
+ aliPayProperties.getAlipayPublicKey(),
+ aliPayProperties.getSignType()
+ );
+ }
+
+
+}
+
@Data
+@ConfigurationProperties(prefix = "pay.alipay")
+public class AlipayProperties {
+
+ /**
+ * 应用ID,您的APPID,收款账号既是您的APPID对应支付宝账号
+ */
+ private String appId;
+
+ /**
+ * 商户私钥,您的PKCS8格式RSA2私钥
+ */
+ private String merchantPrivateKey;
+
+ /**
+ * 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。
+ */
+ private String alipayPublicKey;
+
+ /**
+ * 服务器异步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
+ */
+ private String notifyUrl;
+
+ /**
+ * 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
+ */
+ private String returnUrl;
+
+ /**
+ * 签名方式
+ */
+ private String signType;
+
+ /**
+ * 字符编码格式
+ */
+ private String charset;
+
+ /**
+ * 格式
+ */
+ private String format;
+
+ /**
+ * 支付宝网关
+ */
+ private String gatewayUrl;
+
+
+}
+
@Service
+public class WxpayRefundService extends AbstractPayService implements PayRefundAble {
+
+ @Autowired
+ protected WxPayService wxPayService;
+
+
+ @Override
+ protected Object processing(CommonPayRequest orderInfo) throws PayException {
+ WxRefundRequest wxRefundRequest = (WxRefundRequest) orderInfo;
+ WxPayRefundRequest refundRequest = new WxPayRefundRequest();
+ refundRequest.setOutRefundNo(wxRefundRequest.getOutRefundNo());
+ refundRequest.setRefundAccount(wxRefundRequest.getRefundAccount());
+ refundRequest.setRefundDesc(wxRefundRequest.getRefundDesc());
+ refundRequest.setRefundFee(wxRefundRequest.getRefundFee());
+ refundRequest.setNotifyUrl(wxRefundRequest.getNotifyUrl());
+ refundRequest.setRefundFeeType(wxRefundRequest.getRefundFeeType());
+ refundRequest.setTransactionId(wxRefundRequest.getTransactionId());
+ refundRequest.setOutRefundNo(wxRefundRequest.getOutTradeNo());
+ refundRequest.setTotalFee(wxRefundRequest.getTotalFee());
+
+ try {
+ return wxPayService.refund(refundRequest);
+ } catch (WxPayException e) {
+ throw new PayException(e.getMessage(),e);
+ }
+ }
+
+
+
+
+ @Override
+ public PayPlatformTypeEnum getPayPlatformType() {
+ return PayPlatformTypeEnum.WECHAT;
+ }
+
+
+}
+
@Slf4j
+@Service
+public class WxpayPaymentService extends AbstractPayService implements PaymentAble {
+
+
+ @Autowired
+ protected WxPayService wxPayService;
+
+
+ @Override
+ public PayPlatformTypeEnum getPayPlatformType() {
+ return PayPlatformTypeEnum.WECHAT;
+ }
+
+
+ protected WxPayUnifiedOrderRequest buildUnifiedOrderRequest(WxpayPaymentRequest wxpayOrder) {
+ // 微信统一下单请求对象
+ WxPayUnifiedOrderRequest request = new WxPayUnifiedOrderRequest();
+ request.setOutTradeNo(wxpayOrder.getOut_trade_no());
+ request.setBody(wxpayOrder.getBody());
+ request.setFeeType(wxpayOrder.getFee_type());
+ request.setTotalFee(Integer.valueOf(wxpayOrder.getTotal_fee()));
+ request.setSpbillCreateIp(wxpayOrder.getSpbill_create_ip());
+ request.setNotifyUrl(wxpayOrder.getNotify_url());
+ request.setProductId(String.valueOf(System.currentTimeMillis()));
+ request.setTimeExpire(wxpayOrder.getTime_expire());
+ request.setTimeStart(wxpayOrder.getTime_start());
+ request.setProfitSharing("Y");
+ return request;
+ }
+
+
+ @Override
+ protected Object processing(CommonPayRequest orderInfo) throws PayException {
+ try {
+ return wxPayService.unifiedOrder(buildUnifiedOrderRequest((WxpayPaymentRequest) orderInfo));
+ } catch (WxPayException e) {
+ throw new PayException(e.getMessage(),e);
+ }
+ }
+
+
+}
+
@Service
+public class WxpayOrderQueryService extends AbstractPayService implements PayOrderQueryAble {
+
+ @Autowired
+ private WxPayService wxPayService;
+
+ @Override
+ public PayPlatformTypeEnum getPayPlatformType() {
+ return PayPlatformTypeEnum.WECHAT;
+ }
+
+
+ @Override
+ protected Object processing(CommonPayRequest orderInfo) throws PayException {
+ WxOrderQueryRequest wxQueryOrder = (WxOrderQueryRequest) orderInfo;
+ WxPayOrderQueryRequest req = new WxPayOrderQueryRequest();
+ req.setSubMchId(wxQueryOrder.getSubMchId());
+ req.setSubAppId(wxQueryOrder.getSubMchAppId());
+ req.setOutTradeNo(wxQueryOrder.getPayOrderId());
+ WxPayOrderQueryResult result;
+ try {
+ result = wxPayService.queryOrder(req);
+ } catch (WxPayException e) {
+ throw new PayException("微信订单查询异常", e);
+ }
+ // 根据状态判断
+ if ("SUCCESS".equals(result.getTradeState())) {
+ // 支付成功
+ return result;
+ } else if ("USERPAYING".equals(result.getTradeState())) {
+ // 支付中,等待用户输入密码
+ return result;
+ } else if ("CLOSED".equals(result.getTradeState()) || "REVOKED".equals(result.getTradeState()) || "PAYERROR".equals(result.getTradeState())) {
+ // CLOSED—已关闭, REVOKED—已撤销(刷卡支付), PAYERROR--支付失败(其他原因,如银行返回失败) 支付失败
+ return result;
+ } else {
+ // unknow
+ return result;
+ }
+ }
+
+
+}
+
@Service
+public class WxpayOrderCloseService extends AbstractPayService implements PayOrderCloseAble {
+
+
+ @Autowired
+ private WxPayService wxPayService;
+
+ @Override
+ protected Object processing(CommonPayRequest orderInfo) throws PayException {
+ WxpayOrderCloseRequest wxOrderInfo = (WxpayOrderCloseRequest) orderInfo;
+ WxPayOrderCloseRequest wxPayRequest = new WxPayOrderCloseRequest();
+ wxPayRequest.setOutTradeNo(wxOrderInfo.getOut_trade_no());
+ WxPayOrderCloseResult result = null;
+ try {
+ result = wxPayService.closeOrder(wxPayRequest);
+ } catch (WxPayException e) {
+ throw new PayException(e.getMessage(), e);
+ }
+ return result;
+ }
+
+
+ @Override
+ public PayPlatformTypeEnum getPayPlatformType() {
+ return PayPlatformTypeEnum.WECHAT;
+ }
+
+}
+
@Service
+public class WxApp extends WxpayPaymentService {
+
+
+ @Override
+ protected WxPayUnifiedOrderRequest buildUnifiedOrderRequest(WxpayPaymentRequest wxpayOrder) {
+ WxPayUnifiedOrderRequest wxPayUnifiedOrderRequest = super.buildUnifiedOrderRequest(wxpayOrder);
+ wxPayUnifiedOrderRequest.setTradeType(WxPayConstants.TradeType.APP);
+ wxPayUnifiedOrderRequest.setOpenid(wxpayOrder.getOpenid());
+ return wxPayUnifiedOrderRequest;
+ }
+
+
+}
+
@Service
+public class WxH5 extends WxpayPaymentService {
+
+
+
+ @Override
+ protected WxPayUnifiedOrderRequest buildUnifiedOrderRequest(WxpayPaymentRequest wxpayOrder) {
+ WxPayUnifiedOrderRequest wxPayUnifiedOrderRequest = super.buildUnifiedOrderRequest(wxpayOrder);
+ wxPayUnifiedOrderRequest.setTradeType(WxPayConstants.TradeType.MWEB);
+ wxPayUnifiedOrderRequest.setOpenid(wxpayOrder.getOpenid());
+ return wxPayUnifiedOrderRequest;
+ }
+
+
+}
+
@Service
+public class WxJsapi extends WxpayPaymentService {
+
+
+
+ @Override
+ protected WxPayUnifiedOrderRequest buildUnifiedOrderRequest(WxpayPaymentRequest wxpayOrder) {
+ WxPayUnifiedOrderRequest wxPayUnifiedOrderRequest = super.buildUnifiedOrderRequest(wxpayOrder);
+ wxPayUnifiedOrderRequest.setTradeType(WxPayConstants.TradeType.JSAPI);
+ wxPayUnifiedOrderRequest.setOpenid(wxpayOrder.getOpenid());
+ return wxPayUnifiedOrderRequest;
+ }
+
+
+}
+
@Service
+public class WxNative extends WxpayPaymentService {
+
+ @Override
+ protected WxPayUnifiedOrderRequest buildUnifiedOrderRequest(WxpayPaymentRequest wxpayOrder) {
+ WxPayUnifiedOrderRequest wxPayUnifiedOrderRequest = super.buildUnifiedOrderRequest(wxpayOrder);
+ wxPayUnifiedOrderRequest.setTradeType(WxPayConstants.TradeType.NATIVE);
+ return wxPayUnifiedOrderRequest;
+ }
+
+}
+
@Data
+@Accessors(chain = true)
+public class WxOrderQueryRequest implements CommonPayRequest {
+
+ /**
+ * 支付订单号
+ */
+ private String payOrderId;
+
+
+ // 特约商户传入
+ /** 子商户ID **/
+ private String subMchId;
+
+ /** 子账户appID **/
+ private String subMchAppId;
+
+
+ @Override
+ public PayPlatformTypeEnum getPayTypeEnum() {
+ return PayPlatformTypeEnum.WECHAT;
+ }
+
+}
+
@Data
+@Accessors(chain = true)
+public class WxpayOrderCloseRequest implements CommonPayRequest {
+
+
+ /**
+ * 商户订单号
+ */
+ private String out_trade_no;
+
+
+ @Override
+ public PayPlatformTypeEnum getPayTypeEnum() {
+ return PayPlatformTypeEnum.WECHAT;
+ }
+
+
+}
+
@Data
+@Accessors(chain = true)
+public class WxpayPaymentRequest implements CommonPayRequest {
+
+
+ /**
+ * 公众账号appid
+ */
+ private String appid;
+
+
+ /**
+ * 商户号
+ */
+ private String mch_id;
+
+ /**
+ * 商品描述
+ */
+ private String body;
+
+
+ /**
+ * 附加数据; 在查询API和支付通知中原样返回,可作为自定义参数使用。
+ */
+ private String attach;
+
+ /**
+ * 商户订单号; 要求32个字符内(最少6个字符),只能是数字、大小写字母_-|*且在同一个商户号下唯一
+ */
+ private String out_trade_no;
+
+ /**
+ * 总金额(分)
+ */
+ private String total_fee;
+
+ /**
+ * 默认人民币:CNY
+ */
+ private String fee_type;
+
+
+ /**
+ * 交易起始时间 格式为yyyyMMddHHmmss
+ */
+ private String time_start;
+
+
+ /**
+ * 交易结束时间 格式为yyyyMMddHHmmss
+ */
+ private String time_expire;
+
+ /**
+ * 交易类型;
+ * JSAPI -JSAPI支付
+ * NATIVE -Native支付
+ * APP -APP支付
+ */
+ private String trade_type;
+
+
+ /**
+ * 商品ID; trade_type=NATIVE时,此参数必传
+ */
+ private String product_id;
+
+ /**
+ * 用户标识; trade_type=JSAPI时(即JSAPI支付),此参数必传
+ */
+ private String openid;
+
+ /**
+ * 是否分账 不传默认不分账
+ * <p>
+ * Y-是,需要分账
+ * N-否,不分账
+ */
+ private String profit_sharing;
+
+ /**
+ * 回调地址
+ */
+ private String notify_url;
+
+ /**
+ * 支持IPV4和IPV6两种格式的IP地址。用户的客户端IP
+ */
+ private String spbill_create_ip;
+
+ /**
+ * 随机字符串; 长度要求在32位以内
+ */
+ private String nonce_str;
+
+ @Override
+ public PayPlatformTypeEnum getPayTypeEnum() {
+ return PayPlatformTypeEnum.WECHAT;
+ }
+
+
+}
+
@Data
+@Accessors(chain = true)
+public class WxRefundRequest implements CommonPayRequest {
+
+ /**
+ * 微信支付订单号
+ */
+ private String transactionId;
+ /**
+ * 商户订单号
+ */
+ private String outTradeNo;
+
+ /**
+ * 商户退款单号
+ */
+ private String outRefundNo;
+ /**
+ * 订单金额
+ */
+ private Integer totalFee;
+ /**
+ * 退款金额
+ */
+ private Integer refundFee;
+ /**
+ * 退款货币种类
+ */
+ private String refundFeeType;
+ /**
+ * 退款资金来源
+ */
+ private String refundAccount;
+ /**
+ * 退款原因
+ */
+ private String refundDesc;
+ /**
+ * 退款结果通知url
+ */
+ private String notifyUrl;
+
+ @Override
+ public PayPlatformTypeEnum getPayTypeEnum() {
+ return PayPlatformTypeEnum.WECHAT;
+ }
+}
+
@Configuration
+@ConditionalOnClass(WxPayService.class)
+@EnableConfigurationProperties(WxpayProperties.class)
+@AllArgsConstructor
+public class WxpayConfiguration {
+
+ private WxpayProperties properties;
+
+ @Bean
+ @ConditionalOnMissingBean
+ public WxPayService wxService() {
+ WxPayConfig payConfig = new WxPayConfig();
+ payConfig.setAppId(StringUtils.trimToNull(this.properties.getAppId()));
+ payConfig.setMchId(StringUtils.trimToNull(this.properties.getMchId()));
+ payConfig.setMchKey(StringUtils.trimToNull(this.properties.getMchKey()));
+ payConfig.setSubAppId(StringUtils.trimToNull(this.properties.getSubAppId()));
+ payConfig.setSubMchId(StringUtils.trimToNull(this.properties.getSubMchId()));
+ payConfig.setKeyPath(StringUtils.trimToNull(this.properties.getKeyPath()));
+ // 可以指定是否使用沙箱环境
+ payConfig.setUseSandboxEnv(false);
+ WxPayService wxPayService = new WxPayServiceImpl();
+ wxPayService.setConfig(payConfig);
+ return wxPayService;
+ }
+
+}
+
@Data
+@ConfigurationProperties(prefix = "pay.wechat")
+public class WxpayProperties {
+ /**
+ * 设置微信公众号或者小程序等的appid
+ */
+ private String appId;
+
+ /**
+ * 微信支付商户号
+ */
+ private String mchId;
+
+ /**
+ * 微信支付商户密钥
+ */
+ private String mchKey;
+
+ /**
+ * 服务商模式下的子商户公众账号ID,普通模式请不要配置,请在配置文件中将对应项删除
+ */
+ private String subAppId;
+
+ /**
+ * 服务商模式下的子商户号,普通模式请不要配置,最好是请在配置文件中将对应项删除
+ */
+ private String subMchId;
+
+ /**
+ * apiclient_cert.p12文件的绝对路径,或者如果放在项目中,请以classpath:开头指定
+ */
+ private String keyPath;
+
+ /**
+ * 支付回调地址
+ */
+ private String payNotifyUrl;
+
+ /**
+ * 退款回调地址
+ */
+ private String refundNotifyUrl;
+
+}
+
@RestController
+@RequestMapping("/example/alipay")
+@AllArgsConstructor
+public class AlipayExampleController {
+
+ private AlipayClient alipayClient;
+
+
+ /**
+ * 统一下单
+ * 接口地址: https://opendocs.alipay.com/open/59da99d0_alipay.trade.page.pay
+ *
+ * @param request 请求对象
+ * @return 返回 {@link com.alipay.api.AlipayResponse}包下的类对象
+ */
+ @SneakyThrows
+ @PostMapping("/unifiedOrder")
+ public AlipayTradePagePayResponse unifiedOrder(@RequestBody AlipayTradePagePayRequest request) {
+ return alipayClient.pageExecute(request);
+ }
+
+
+ /**
+ * 交易查询
+ * 接口地址: https://opendocs.alipay.com/open/bff76748_alipay.trade.query
+ *
+ * @param request 请求对象
+ * @return 返回 {@link com.alipay.api.AlipayResponse}包下的类对象
+ */
+ @SneakyThrows
+ @PostMapping("/queryOrder")
+ public AlipayTradeQueryResponse queryOrder(@RequestBody AlipayTradeQueryRequest request) {
+ return alipayClient.pageExecute(request);
+ }
+
+ /**
+ * 交易退款
+ * 接口地址: https://opendocs.alipay.com/open/357441a2_alipay.trade.fastpay
+ *
+ * @param request 请求对象
+ * @return 返回 {@link com.alipay.api.AlipayResponse}包下的类对象
+ */
+ @SneakyThrows
+ @PostMapping("/refund")
+ public AlipayTradeRefundResponse refund(@RequestBody AlipayTradeRefundRequest request) {
+ return alipayClient.pageExecute(request);
+ }
+
+ /**
+ * 退款查询
+ * 接口地址: https://opendocs.alipay.com/open/357441a2_alipay.trade.fastpay.refund.query
+ *
+ * @param request 请求对象
+ * @return 返回 {@link com.alipay.api.AlipayResponse}包下的类对象
+ */
+ @SneakyThrows
+ @PostMapping("/refundQuery")
+ public AlipayTradeFastpayRefundQueryResponse refundQuery(@RequestBody AlipayTradeFastpayRefundQueryRequest request) {
+ return alipayClient.pageExecute(request);
+ }
+
+
+ /**
+ * 交易关闭
+ * 接口地址: https://opendocs.alipay.com/open/8dc9ebb3_alipay.trade.close
+ *
+ * @param request 请求对象
+ * @return 返回 {@link com.alipay.api.AlipayResponse}包下的类对象
+ */
+ @SneakyThrows
+ @PostMapping("/closeOrder")
+ public AlipayTradeCloseResponse closeOrder(@RequestBody AlipayTradeCloseRequest request) {
+ return alipayClient.pageExecute(request);
+ }
+
+
+}
+
@RestController
+@RequestMapping("/example/wxpay")
+@AllArgsConstructor
+public class WxpayExampleController {
+
+ private WxPayService wxService;
+
+ /**
+ * <pre>
+ * 查询订单(详见https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_2)
+ * 该接口提供所有微信支付订单的查询,商户可以通过查询订单接口主动查询订单状态,完成下一步的业务逻辑。
+ * 需要调用查询接口的情况:
+ * ◆ 当商户后台、网络、服务器等出现异常,商户系统最终未接收到支付通知;
+ * ◆ 调用支付接口后,返回系统错误或未知交易状态情况;
+ * ◆ 调用被扫支付API,返回USERPAYING的状态;
+ * ◆ 调用关单或撤销接口API之前,需确认支付状态;
+ * 接口地址:https://api.mch.weixin.qq.com/pay/orderquery
+ * </pre>
+ *
+ * @param transactionId 微信订单号
+ * @param outTradeNo 商户系统内部的订单号,当没提供transactionId时需要传这个。
+ */
+ @GetMapping("/queryOrder")
+ public WxPayOrderQueryResult queryOrder(@RequestParam(required = false) String transactionId,
+ @RequestParam(required = false) String outTradeNo)
+ throws WxPayException {
+ return this.wxService.queryOrder(transactionId, outTradeNo);
+ }
+
+ @PostMapping("/queryOrder")
+ public WxPayOrderQueryResult queryOrder(@RequestBody WxPayOrderQueryRequest wxPayOrderQueryRequest) throws WxPayException {
+ return this.wxService.queryOrder(wxPayOrderQueryRequest);
+ }
+
+ /**
+ * <pre>
+ * 关闭订单
+ * 应用场景
+ * 以下情况需要调用关单接口:
+ * 1. 商户订单支付失败需要生成新单号重新发起支付,要对原订单号调用关单,避免重复支付;
+ * 2. 系统下单后,用户支付超时,系统退出不再受理,避免用户继续,请调用关单接口。
+ * 注意:订单生成后不能马上调用关单接口,最短调用时间间隔为5分钟。
+ * 接口地址:https://api.mch.weixin.qq.com/pay/closeorder
+ * 是否需要证书: 不需要。
+ * </pre>
+ *
+ * @param outTradeNo 商户系统内部的订单号
+ */
+ @GetMapping("/closeOrder/{outTradeNo}")
+ public WxPayOrderCloseResult closeOrder(@PathVariable String outTradeNo) throws WxPayException {
+ return this.wxService.closeOrder(outTradeNo);
+ }
+
+ @PostMapping("/closeOrder")
+ public WxPayOrderCloseResult closeOrder(@RequestBody WxPayOrderCloseRequest wxPayOrderCloseRequest) throws WxPayException {
+ return this.wxService.closeOrder(wxPayOrderCloseRequest);
+ }
+
+ /**
+ * 调用统一下单接口,并组装生成支付所需参数对象.
+ *
+ * @param request 统一下单请求参数
+ * @param <T> 请使用{@link com.github.binarywang.wxpay.bean.order}包下的类
+ * @return 返回 {@link com.github.binarywang.wxpay.bean.order}包下的类对象
+ */
+ @PostMapping("/createOrder")
+ public <T> T createOrder(@RequestBody WxPayUnifiedOrderRequest request) throws WxPayException {
+ return this.wxService.createOrder(request);
+ }
+
+ /**
+ * 统一下单(详见https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1)
+ * 在发起微信支付前,需要调用统一下单接口,获取"预支付交易会话标识"
+ * 接口地址:https://api.mch.weixin.qq.com/pay/unifiedorder
+ *
+ * @param request 请求对象,注意一些参数如appid、mchid等不用设置,方法内会自动从配置对象中获取到(前提是对应配置中已经设置)
+ */
+ @PostMapping("/unifiedOrder")
+ public WxPayUnifiedOrderResult unifiedOrder(@RequestBody WxPayUnifiedOrderRequest request) throws WxPayException {
+ return this.wxService.unifiedOrder(request);
+ }
+
+ /**
+ * <pre>
+ * 微信支付-申请退款
+ * 详见 https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_4
+ * 接口链接:https://api.mch.weixin.qq.com/secapi/pay/refund
+ * </pre>
+ *
+ * @param request 请求对象
+ * @return 退款操作结果
+ */
+ @PostMapping("/refund")
+ public WxPayRefundResult refund(@RequestBody WxPayRefundRequest request) throws WxPayException {
+ return this.wxService.refund(request);
+ }
+
+ /**
+ * <pre>
+ * 微信支付-查询退款
+ * 应用场景:
+ * 提交退款申请后,通过调用该接口查询退款状态。退款有一定延时,用零钱支付的退款20分钟内到账,
+ * 银行卡支付的退款3个工作日后重新查询退款状态。
+ * 详见 https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_5
+ * 接口链接:https://api.mch.weixin.qq.com/pay/refundquery
+ * </pre>
+ * 以下四个参数四选一
+ *
+ * @param transactionId 微信订单号
+ * @param outTradeNo 商户订单号
+ * @param outRefundNo 商户退款单号
+ * @param refundId 微信退款单号
+ * @return 退款信息
+ */
+ @GetMapping("/refundQuery")
+ public WxPayRefundQueryResult refundQuery(@RequestParam(required = false) String transactionId,
+ @RequestParam(required = false) String outTradeNo,
+ @RequestParam(required = false) String outRefundNo,
+ @RequestParam(required = false) String refundId)
+ throws WxPayException {
+ return this.wxService.refundQuery(transactionId, outTradeNo, outRefundNo, refundId);
+ }
+
+ /**
+ * 退款查询
+ * @param wxPayRefundQueryRequest 请求对象
+ * @return 退款信息
+ * @throws WxPayException
+ */
+ @PostMapping("/refundQuery")
+ public WxPayRefundQueryResult refundQuery(@RequestBody WxPayRefundQueryRequest wxPayRefundQueryRequest) throws WxPayException {
+ return this.wxService.refundQuery(wxPayRefundQueryRequest);
+ }
+
+ /**
+ * 支付回调通知处理
+ */
+ @PostMapping("/notify/order")
+ public String parseOrderNotifyResult(@RequestBody String xmlData) throws WxPayException {
+ final WxPayOrderNotifyResult notifyResult = this.wxService.parseOrderNotifyResult(xmlData);
+ // TODO 根据自己业务场景需要构造返回对象
+ return WxPayNotifyResponse.success("成功");
+ }
+
+ /**
+ * 退款回调通知处理
+ * @param xmlData
+ */
+ @PostMapping("/notify/refund")
+ public String parseRefundNotifyResult(@RequestBody String xmlData) throws WxPayException {
+ final WxPayRefundNotifyResult result = this.wxService.parseRefundNotifyResult(xmlData);
+ // TODO 根据自己业务场景需要构造返回对象
+ return WxPayNotifyResponse.success("成功");
+ }
+
+ /**
+ * 扫码支付回调通知处理
+ */
+ @PostMapping("/notify/scanpay")
+ public String parseScanPayNotifyResult(String xmlData) throws WxPayException {
+ final WxScanPayNotifyResult result = this.wxService.parseScanPayNotifyResult(xmlData);
+ // TODO 根据自己业务场景需要构造返回对象
+ return WxPayNotifyResponse.success("成功");
+ }
+
+
+ /**
+ * <pre>
+ * 扫码支付模式一生成二维码的方法
+ * 二维码中的内容为链接,形式为:
+ * weixin://wxpay/bizpayurl?sign=XXXXX&appid=XXXXX&mch_id=XXXXX&product_id=XXXXXX&time_stamp=XXXXXX&nonce_str=XXXXX
+ * 其中XXXXX为商户需要填写的内容,商户将该链接生成二维码,如需要打印发布二维码,需要采用此格式。商户可调用第三方库生成二维码图片。
+ * 文档详见: https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_4
+ * </pre>
+ *
+ * @param productId 产品Id
+ * @param logoFile 商户logo图片的文件对象,可以为空
+ * @param sideLength 要生成的二维码的边长,如果为空,则取默认值400
+ * @return 生成的二维码的字节数组
+ */
+ public byte[] createScanPayQrcodeMode1(String productId, File logoFile, Integer sideLength) {
+ return this.wxService.createScanPayQrcodeMode1(productId, logoFile, sideLength);
+ }
+
+ /**
+ * <pre>
+ * 扫码支付模式一生成二维码的方法
+ * 二维码中的内容为链接,形式为:
+ * weixin://wxpay/bizpayurl?sign=XXXXX&appid=XXXXX&mch_id=XXXXX&product_id=XXXXXX&time_stamp=XXXXXX&nonce_str=XXXXX
+ * 其中XXXXX为商户需要填写的内容,商户将该链接生成二维码,如需要打印发布二维码,需要采用此格式。商户可调用第三方库生成二维码图片。
+ * 文档详见: https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_4
+ * </pre>
+ *
+ * @param productId 产品Id
+ * @return 生成的二维码URL连接
+ */
+
@Slf4j
+@RestController
+@RequestMapping("/example/iPay")
+public class PayTestController {
+
+ @Autowired
+ private PayFacade payFacade;
+
+
+ @GetMapping("/alipayUnifiedOrder")
+ public String alipayUnifiedOrder() {
+ payFacade.execute(PaymentAble.class,
+ new AlipayPaymentRequest()
+ .setBody("商品描述")
+ .setSubject("支付宝测试商品")
+ .setTotal_amount("0.1")
+ .setOut_trade_no("2023009999999")
+ );
+ return "支付宝-支付-交易成功";
+ }
+
+
+ @RequestMapping("/alipayUnifiedOrderNotify")
+ public String alipayUnifiedOrderNotify() {
+ log.info("支付宝-支付回调-交易成功");
+ return "交易成功!";
+ }
+
+
+ @GetMapping("/wxUnifiedOrder")
+ public String wxUnifiedOrder() {
+ payFacade.execute(PaymentAble.class,
+ new WxpayPaymentRequest()
+ .setBody("微信测试商品")
+ .setTotal_fee("0.1")
+ .setOut_trade_no("2023009999999")
+ );
+ return "微信-支付-交易成功";
+ }
+
+
+ @RequestMapping("/wxUnifiedOrderNotify")
+ public String wxUnifiedOrderNotify() {
+ log.info("微信-支付回调-交易成功");
+ return "交易成功!";
+ }
+
+}
+
+ +
+ + + + + +<dependencies>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-web</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.springframework.plugin</groupId>
+ <artifactId>spring-plugin-core</artifactId>
+ <version>${spring.plugin.core.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-test</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.projectlombok</groupId>
+ <artifactId>lombok</artifactId>
+ <optional>true</optional>
+ </dependency>
+
+ <dependency>
+ <groupId>cn.hutool</groupId>
+ <artifactId>hutool-all</artifactId>
+ <version>${hutool.version}</version>
+ </dependency>
+</dependencies>
+
public interface EventContext {
+
+
+ /**
+ * 是否继续调用链
+ */
+ boolean continueChain();
+
+
+ /**
+ * 获取当前过滤器选择器
+ */
+ FilterSelector getFilterSelector();
+
+}
+
public interface BizType {
+
+
+ /**
+ * 获取业务类型码值
+ */
+ Integer getCode();
+
+ /**
+ * 业务类型名称
+ *
+ */
+ String getName();
+
+}
+
public abstract class AbstractEventContext implements EventContext{
+
+
+ private final BizType businessType;
+ private final FilterSelector filterSelector;
+
+
+ protected AbstractEventContext(BizType businessType, FilterSelector filterSelector) {
+ this.businessType = businessType;
+ this.filterSelector = filterSelector;
+ }
+
+
+ @Override
+ public boolean continueChain() {
+ return true;
+ }
+
+ @Override
+ public FilterSelector getFilterSelector() {
+ return filterSelector;
+ }
+
+}
+
public interface EventFilter<T extends EventContext> {
+
+ /**
+ * 过滤逻辑封装点
+ *
+ * @param context 上下文对象
+ * @param chain 调用链
+ */
+ void doFilter(T context, EventFilterChain<T> chain);
+
+}
+
public abstract class AbstractEventFilter<T extends EventContext> implements EventFilter<T> {
+
+ @Override
+ public void doFilter(T context, EventFilterChain<T> chain) {
+ // 执行
+ if (context.getFilterSelector().matchFilter(this.getClass().getSimpleName())) {
+ handler(context);
+ }
+
+ // 是否继续执行调用链
+ if (context.continueChain()) {
+ chain.nextHandler(context);
+ }
+ }
+
+
+ /**
+ * 执行器
+ *
+ * @param context 上下文对象
+ */
+ protected abstract void handler(T context);
+
+}
+
public interface EventFilterChain<T extends EventContext> {
+
+
+ /**
+ * 执行当前过滤器
+ *
+ * @param context 上下文对象
+ */
+ void handler(T context);
+
+
+ /**
+ * 跳过当前过滤器 执行下一个执行过滤器
+ *
+ * @param context 上下文对象
+ */
+ void nextHandler(T context);
+
+}
+
@Slf4j
+@Component
+public class FilterChainPipeline<F extends EventFilter>{
+
+
+ private DefaultEventFilterChain<EventContext> last;
+
+
+ public FilterChainPipeline<F> append(F filter){
+ last = new DefaultEventFilterChain<>(last, filter);
+ return this;
+ }
+
+
+ public FilterChainPipeline<F> append(String description, F filter){
+ log.debug("过滤器调用链管道开始设置 {} 过滤器",description);
+ last = new DefaultEventFilterChain<>(last, filter);
+ return this;
+ }
+
+
+ public DefaultEventFilterChain<EventContext> getFilterChain() {
+ return this.last;
+ }
+
+}
+
public class DefaultEventFilterChain<T extends EventContext> implements EventFilterChain<T> {
+
+ private final EventFilterChain<T> next;
+ private final EventFilter<T> filter;
+
+
+ public DefaultEventFilterChain(EventFilterChain<T> next, EventFilter<T> filter) {
+ this.next = next;
+ this.filter = filter;
+ }
+
+
+ @Override
+ public void handler(T context) {
+ filter.doFilter(context,this);
+ }
+
+ @Override
+ public void nextHandler(T context) {
+ if (next != null) {
+ next.handler(context);
+ }
+ }
+
+}
+
public interface FilterSelector {
+
+
+ /**
+ * 匹配过滤器
+ *
+ * @param currentFilterName 过滤器名称
+ * @return true 匹配成功
+ */
+ boolean matchFilter(String currentFilterName);
+
+ /**
+ * 获取当前所有过滤器名称
+ *
+ * @return 过滤器名称
+ */
+ List<String> getAllFilterNames();
+}
+
public class DefaultFilterSelector implements FilterSelector{
+
+ @Setter
+ private List<String> filterNames = CollUtil.newArrayList();
+
+ @Override
+ public boolean matchFilter(String currentFilterName) {
+ return filterNames.stream().anyMatch(s -> Objects.equals(s,currentFilterName));
+ }
+
+
+ @Override
+ public List<String> getAllFilterNames() {
+ return filterNames;
+ }
+
+}
+
@SpringBootApplication
+@EnablePluginRegistries(value = {Business1PostPlugin.class, Business2PostPlugin.class})
+public class PipelineApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(PipelineApplication.class, args);
+ }
+}
+
@RestController
+@RequestMapping("/pipelineTest")
+public class PipelineController {
+
+ @Autowired
+ private Business1Service business1PipelineTestService;
+
+ @Autowired
+ private Business2Service business2PipelineTestService;
+
+
+ @GetMapping("/business1")
+ public void business1(){
+ PipelineRequestVo pipelineTestRequest = new PipelineRequestVo();
+ pipelineTestRequest.setUuid("business1-1110-1111231afsas-123adss");
+ pipelineTestRequest.setBusinessCode("business1");
+ pipelineTestRequest.setModel2(new Business1Model2());
+ pipelineTestRequest.setModel1(new Business1Model1());
+ business1PipelineTestService.doService(pipelineTestRequest);
+ }
+
+
+ @GetMapping("/business2")
+ public void business2(){
+ PipelineRequestVo pipelineTestRequest = new PipelineRequestVo();
+ pipelineTestRequest.setUuid("business2-1110-1111231afsas-123adss");
+ pipelineTestRequest.setBusinessCode("business2");
+ pipelineTestRequest.setModel3(new Business2Model1());
+ pipelineTestRequest.setModel4(new Business2Model2());
+ business2PipelineTestService.doService(pipelineTestRequest);
+ }
+
+}
+
@Data
+public class PipelineRequestVo {
+
+
+ private String uuid;
+
+ private String businessCode;
+
+ /**
+ * 在自定义的filter中处理
+ */
+ @Setter
+ @Getter
+ private Business1Model1 model1;
+
+ /**
+ * 在自定义的filter中处理
+ */
+ @Setter
+ @Getter
+ private Business1Model2 model2;
+
+
+ /**
+ * 在自定义的filter中处理
+ */
+ @Setter
+ @Getter
+ private Business2Model1 model3;
+
+ /**
+ * 在自定义的filter中处理
+ */
+ @Setter
+ @Getter
+ private Business2Model2 model4;
+
+}
+
@Getter
+@AllArgsConstructor
+public enum BusinessTypeEnum implements BizType {
+
+ BUSINESS_1(1,"业务1"),
+ BUSINESS_2(2,"业务2"),
+ BUSINESS_3(3,"业务3"),
+ ;
+
+
+
+ private Integer code;
+ private String name;
+
+}
+
public interface Business1Service {
+
+ void doService(PipelineRequestVo pipelineTestRequest);
+}
+
@Slf4j
+@Service
+public class Business1ServiceImpl implements Business1Service {
+
+ @Qualifier("business1PipelineSelectorFactory")
+ @Autowired
+ private PipelineSelectorFactory business1PipelineSelectorFactory;
+
+ @Autowired
+ private FilterChainPipeline<Business1PipelineFilter> filterChainPipeline;
+
+ @Autowired
+ private PluginRegistry<Business1PostPlugin, Business1Model1> business1PostPlugin;
+
+
+
+ @Override
+ public void doService(PipelineRequestVo pipelineTestRequest) {
+ log.info("===============business1开始===============");
+ // 处理器参数
+ log.info("===============开始获取FilterSelector===============");
+ FilterSelector filterSelector = business1PipelineSelectorFactory.getFilterSelector(pipelineTestRequest);
+ Business1Context pipelineEventContext = new Business1Context(BusinessTypeEnum.BUSINESS_1, filterSelector);
+ log.info("获取FilterSelector完成: {}",filterSelector.getAllFilterNames());
+ log.info("===============获取FilterSelector完成===============");
+
+ // 处理
+ log.info("===============开始执行过滤器===============");
+ pipelineEventContext.setPipelineTestRequest(pipelineTestRequest);
+ pipelineEventContext.setModel2(pipelineTestRequest.getModel2());
+ pipelineEventContext.setModel1(pipelineTestRequest.getModel1());
+ filterChainPipeline.getFilterChain().handler(pipelineEventContext);
+ log.info("===============执行过滤器完成===============");
+
+ // 处理后获取值
+ log.info("===============开始执行后置处理器===============");
+ Business1Model2 model2 = pipelineEventContext.getModel2();
+ Business1Model1 model1 = pipelineEventContext.getModel1();
+ PipelineRequestVo pipelineTestRequest1 = pipelineEventContext.getPipelineTestRequest();
+ business1PostPlugin.getPluginsFor(model1)
+ .forEach(handler -> handler.postProcessing(model1));
+ log.info("===============执行后置处理器完成===============");
+
+ log.info("===============business1结束===============");
+
+ }
+
+}
+
public class Business1Context extends AbstractEventContext {
+
+ /**
+ * 在自定义的filter中处理
+ */
+ @Setter
+ @Getter
+ private Business1Model1 model1;
+
+ /**
+ * 在自定义的filter中处理
+ */
+ @Setter
+ @Getter
+ private Business1Model2 model2;
+
+ /**
+ * 在自定义的filter中处理
+ */
+ @Setter
+ @Getter
+ private PipelineRequestVo pipelineTestRequest;
+
+
+ public Business1Context(BizType businessType, FilterSelector filterSelector) {
+ super(businessType, filterSelector);
+ }
+
+ @Override
+ public boolean continueChain() {
+ return true;
+ }
+
+}
+
@Data
+public class Business1Model1 {
+
+ private Integer id;
+
+ private String name1;
+
+ private String name2;
+
+ private String name3;
+
+}
+
@Data
+public class Business1Model2 {
+
+ private Integer id;
+
+ private String name;
+
+ private String desc;
+
+ private String age;
+
+}
+
public interface Business1PipelineFilter extends EventFilter<Business1Context> {
+
+ int order();
+}
+
@Slf4j
+@Component
+public class Business1Filter1 extends AbstractEventFilter<Business1Context> implements Business1PipelineFilter {
+
+ @Override
+ public void handler(Business1Context context) {
+ // 模拟操作数据库 等业务操作 可以利用门面模式进行解耦
+ Business1Model1 model1 = context.getModel1();
+ model1.setName1("张三");
+ model1.setName2("李四");
+ model1.setName3("王五");
+ model1.setId(1);
+
+ Business1Model2 model2 = context.getModel2();
+ model2.setId(2);
+ model2.setDesc("");
+ model2.setAge("18");
+ model2.setName("小白");
+
+ log.info("Filter1执行完毕...");
+
+ // 存入新的值到上下文对象中 下个处理器继续处理
+ context.setModel1(model1);
+ context.setModel2(model2);
+ }
+
+ @Override
+ public int order() {
+ return 1;
+ }
+}
+
@Slf4j
+@Component
+public class Business1Filter2 extends AbstractEventFilter<Business1Context> implements Business1PipelineFilter {
+
+ @Override
+ public void handler(Business1Context context) {
+ // 模拟操作数据库 等业务操作 可以利用门面模式进行解耦
+ Business1Model1 model1 = context.getModel1();
+ model1.setName1(model1.getName1() + "-------------");
+ model1.setName2(model1.getName2() + "-------------");
+ model1.setName3(model1.getName3() + "-------------");
+ model1.setId(100);
+
+ log.info("Filter2执行完毕...");
+ // 存入新的值到上下文对象中 下个处理器继续处理
+ context.setModel1(model1);
+ context.setModel2(context.getModel2());
+ }
+
+ @Override
+ public int order() {
+ return 2;
+ }
+}
+
public interface Business1PostPlugin extends Plugin<Business1Model1> {
+
+
+ /**
+ * 后置处理
+ *
+ * @param model 处理参数
+ */
+ void postProcessing(Business1Model1 model);
+
+}
+
@Slf4j
+@Component
+public class Business1ServicePluginImpl implements Business1PostPlugin {
+
+
+ @Override
+ public boolean supports(Business1Model1 pipelineEventContext) {
+ return true;
+ }
+
+
+ @Override
+ public void postProcessing(Business1Model1 model) {
+ log.info("===>{}",model.getId());
+ }
+
+}
+
@Slf4j
+@Component
+public class Business1ServicePluginImpl2 implements Business1PostPlugin {
+
+ @Override
+ public boolean supports(Business1Model1 model) {
+ return true;
+ }
+
+
+ @Override
+ public void postProcessing(Business1Model1 model) {
+ log.info("===>{}",model.getId());
+ }
+
+}
+
public interface Business2Service {
+
+ void doService(PipelineRequestVo pipelineTestRequest);
+}
+
@Slf4j
+@Service
+public class Business2ServiceImpl implements Business2Service {
+
+ @Qualifier("business2PipelineSelectorFactory")
+ @Autowired
+ private PipelineSelectorFactory business2PipelineSelectorFactory;
+
+ @Autowired
+ private FilterChainPipeline<Business2PipelineFilter> filterChainPipeline;
+
+ @Autowired
+ private PluginRegistry<Business2PostPlugin, Business2Model1> business2PostPlugin;
+
+
+
+ @Override
+ public void doService(PipelineRequestVo pipelineTestRequest) {
+ log.info("===============business2开始===============");
+ // 处理器参数
+ log.info("===============开始获取FilterSelector===============");
+ FilterSelector filterSelector = business2PipelineSelectorFactory.getFilterSelector(pipelineTestRequest);
+ Business2Context pipelineEventContext = new Business2Context(BusinessTypeEnum.BUSINESS_2, filterSelector);
+ log.info("获取FilterSelector完成: {}",filterSelector.getAllFilterNames());
+ log.info("===============获取FilterSelector完成===============");
+
+ // 处理
+ log.info("===============开始执行过滤器===============");
+ pipelineEventContext.setPipelineTestRequest(pipelineTestRequest);
+ pipelineEventContext.setModel2(pipelineTestRequest.getModel4());
+ pipelineEventContext.setModel1(pipelineTestRequest.getModel3());
+ filterChainPipeline.getFilterChain().handler(pipelineEventContext);
+ log.info("===============执行过滤器完成===============");
+
+ // 处理后获取值
+ log.info("===============开始执行后置处理器===============");
+ Business2Model2 model2 = pipelineEventContext.getModel2();
+ Business2Model1 model1 = pipelineEventContext.getModel1();
+ PipelineRequestVo pipelineTestRequest1 = pipelineEventContext.getPipelineTestRequest();
+ business2PostPlugin.getPluginsFor(model1)
+ .forEach(handler -> handler.postProcessing(model1));
+ log.info("===============执行后置处理器完成===============");
+ log.info("===============business2结束===============");
+ }
+
+}
+
public class Business2Context extends AbstractEventContext {
+
+ /**
+ * 在自定义的filter中处理
+ */
+ @Setter
+ @Getter
+ private Business2Model1 model1;
+
+ /**
+ * 在自定义的filter中处理
+ */
+ @Setter
+ @Getter
+ private Business2Model2 model2;
+
+ /**
+ * 在自定义的filter中处理
+ */
+ @Setter
+ @Getter
+ private PipelineRequestVo pipelineTestRequest;
+
+
+ public Business2Context(BizType businessType, FilterSelector filterSelector) {
+ super(businessType, filterSelector);
+ }
+
+ @Override
+ public boolean continueChain() {
+ return true;
+ }
+
+}
+
@Data
+public class Business2Model1 {
+
+ private Integer id;
+
+ private String name1;
+
+ private String name2;
+
+ private String name3;
+
+}
+
@Data
+public class Business2Model2 {
+
+ private Integer id;
+
+ private String name;
+
+ private String desc;
+
+ private String age;
+
+}
+
public interface Business2PipelineFilter extends EventFilter<Business2Context> {
+
+ int order();
+}
+
@Slf4j
+@Component
+public class Business2Filter1 extends AbstractEventFilter<Business2Context> implements Business2PipelineFilter {
+
+ @Override
+ public void handler(Business2Context context) {
+ // 模拟操作数据库 等业务操作 可以利用门面模式进行解耦
+ Business2Model1 model1 = context.getModel1();
+ model1.setName1("张三");
+ model1.setName2("李四");
+ model1.setName3("王五");
+ model1.setId(1);
+
+ Business2Model2 model2 = context.getModel2();
+ model2.setId(2);
+ model2.setDesc("");
+ model2.setAge("18");
+ model2.setName("小白");
+
+ log.info("Filter1执行完毕...");
+
+ // 存入新的值到上下文对象中 下个处理器继续处理
+ context.setModel1(model1);
+ context.setModel2(model2);
+ }
+
+ @Override
+ public int order() {
+ return 1;
+ }
+}
+
@Slf4j
+@Component
+public class Business2Filter2 extends AbstractEventFilter<Business2Context> implements Business2PipelineFilter {
+
+ @Override
+ public void handler(Business2Context context) {
+ // 模拟操作数据库 等业务操作 可以利用门面模式进行解耦
+ Business2Model1 model1 = context.getModel1();
+ model1.setName1(model1.getName1() + "-------------");
+ model1.setName2(model1.getName2() + "-------------");
+ model1.setName3(model1.getName3() + "-------------");
+ model1.setId(100);
+
+ log.info("Filter2执行完毕...");
+ // 存入新的值到上下文对象中 下个处理器继续处理
+ context.setModel1(model1);
+ context.setModel2(context.getModel2());
+ }
+
+ @Override
+ public int order() {
+ return 2;
+ }
+}
+
public interface Business2PostPlugin extends Plugin<Business2Model1> {
+
+
+ /**
+ * 后置处理
+ *
+ * @param model 处理参数
+ */
+ void postProcessing(Business2Model1 model);
+
+}
+
@Slf4j
+@Component
+public class Business2ServicePluginImpl implements Business2PostPlugin {
+
+
+ @Override
+ public boolean supports(Business2Model1 pipelineEventContext) {
+ return true;
+ }
+
+
+ @Override
+ public void postProcessing(Business2Model1 model) {
+ log.info("===>{}",model.getId());
+ }
+
+}
+
@Slf4j
+@Component
+public class Business2ServicePluginImpl2 implements Business2PostPlugin {
+
+
+ @Override
+ public boolean supports(Business2Model1 model) {
+ return true;
+ }
+
+
+ @Override
+ public void postProcessing(Business2Model1 model) {
+ log.info("===>{}",model.getId());
+ }
+
+}
+
@ConfigurationProperties(prefix = "test")
+@Component
+@Data
+public class FilterConfigProperties {
+
+ private Map<String, List<String>> configs;
+
+
+ public Map<String, List<String>> getConfigs() {
+ if (configs == null) {
+ configs = MapUtil.newHashMap(16);
+ }
+ return configs;
+ }
+
+}
+
@Component
+@RequiredArgsConstructor
+public class PipelineFilterConfig {
+
+ private final List<Business1PipelineFilter> business1PipelineFilter;
+ private final FilterChainPipeline<Business1PipelineFilter> business1FilterChainPipeline;
+
+ private final List<Business2PipelineFilter> business2PipelineFilter;
+ private final FilterChainPipeline<Business2PipelineFilter> business2FilterChainPipeline;
+
+ private final FilterConfigProperties filterConfigProperties;
+
+
+ @Bean
+ public FilterChainPipeline<Business1PipelineFilter> business1ChargePipeline() {
+ Map<String, List<String>> configs = filterConfigProperties.getConfigs();
+ if (business1PipelineFilter.isEmpty() || configs.isEmpty()){
+ return business1FilterChainPipeline;
+ }
+
+ Set<Map.Entry<String, List<String>>> filtersName = configs.entrySet();
+ long distinctCount = filtersName.stream().distinct().count();
+ if (distinctCount > business1PipelineFilter.size()) {
+ throw new IllegalArgumentException("设置的过滤器数量大于实际过滤器数量");
+ }
+
+ business1PipelineFilter
+ .stream()
+ .sorted(Comparator.comparing(Business1PipelineFilter::order))
+ .forEach(business1FilterChainPipeline::append)
+ ;
+ return business1FilterChainPipeline;
+ }
+
+
+ @Bean
+ public FilterChainPipeline<Business2PipelineFilter> business2ChargePipeline() {
+ Map<String, List<String>> configs = filterConfigProperties.getConfigs();
+ if (business2PipelineFilter.isEmpty() || configs.isEmpty()){
+ return business2FilterChainPipeline;
+ }
+
+ Set<Map.Entry<String, List<String>>> filtersName = configs.entrySet();
+ long distinctCount = filtersName.stream().distinct().count();
+ if (distinctCount > business2PipelineFilter.size()) {
+ throw new IllegalArgumentException("设置的过滤器数量大于实际过滤器数量");
+ }
+
+ business2PipelineFilter
+ .stream()
+ .sorted(Comparator.comparing(Business2PipelineFilter::order))
+ .forEach(business2FilterChainPipeline::append)
+ ;
+ return business2FilterChainPipeline;
+ }
+
+}
+
public interface PipelineSelectorFactory {
+ FilterSelector getFilterSelector(PipelineRequestVo request);
+}
+
@Component("business1PipelineSelectorFactory")
+public class Business1PipelineSelectorFactory implements PipelineSelectorFactory {
+
+ @Autowired
+ private FilterConfigProperties filterConfigProperties;
+
+ @Override
+ public FilterSelector getFilterSelector(PipelineRequestVo request) {
+ String businessCode = request.getBusinessCode();
+ DefaultFilterSelector defaultFilterSelector = new DefaultFilterSelector();
+ if (businessCode.equals("business1")){
+ defaultFilterSelector.setFilterNames(filterConfigProperties.getConfigs().getOrDefault(businessCode, Collections.unmodifiableList(new ArrayList<>())));
+ }
+ return defaultFilterSelector;
+ }
+}
+
@Component("business2PipelineSelectorFactory")
+public class Business2PipelineSelectorFactory implements PipelineSelectorFactory {
+
+ @Autowired
+ private FilterConfigProperties filterConfigProperties;
+
+ @Override
+ public FilterSelector getFilterSelector(PipelineRequestVo request) {
+ String businessCode = request.getBusinessCode();
+ DefaultFilterSelector defaultFilterSelector = new DefaultFilterSelector();
+ if (businessCode.equals("business2")){
+ defaultFilterSelector.setFilterNames(filterConfigProperties.getConfigs().getOrDefault(businessCode, Collections.unmodifiableList(new ArrayList<>())));
+ }
+ return defaultFilterSelector;
+ }
+
+}
+
server:
+ port: 8080
+
+test:
+ configs:
+ business1:
+ - Business1Filter1
+ - Business1Filter2
+ business2:
+ - Business2Filter1
+ - Business2Filter2
+
+ +
+ + + + + +代码结构
+ +<dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-security</artifactId>
+</dependency>
+
+<dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-web</artifactId>
+</dependency>
+
+<dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-quartz</artifactId>
+</dependency>
+
+<dependency>
+ <groupId>org.projectlombok</groupId>
+ <artifactId>lombok</artifactId>
+</dependency>
+
+<dependency>
+ <groupId>cn.hutool</groupId>
+ <artifactId>hutool-all</artifactId>
+</dependency>
+
-- ----------------------------
+-- 定时任务调度表
+-- ----------------------------
+drop table if exists sys_job;
+create table sys_job (
+ job_id bigint(20) not null auto_increment comment '任务ID',
+ job_name varchar(64) default '' comment '任务名称',
+ job_group varchar(64) default 'DEFAULT' comment '任务组名',
+ invoke_target varchar(500) not null comment '调用目标字符串',
+ cron_expression varchar(255) default '' comment 'cron执行表达式',
+ misfire_policy varchar(20) default '3' comment '计划执行错误策略(1立即执行 2执行一次 3放弃执行)',
+ concurrent char(1) default '1' comment '是否并发执行(0允许 1禁止)',
+ status char(1) default '0' comment '状态(0正常 1暂停)',
+ create_by varchar(64) default '' comment '创建者',
+ create_time datetime comment '创建时间',
+ update_by varchar(64) default '' comment '更新者',
+ update_time datetime comment '更新时间',
+ remark varchar(500) default '' comment '备注信息',
+ primary key (job_id, job_name, job_group)
+) engine=innodb auto_increment=100 comment = '定时任务调度表';
+
+
+-- ----------------------------
+-- 定时任务调度日志表
+-- ----------------------------
+drop table if exists sys_job_log;
+create table sys_job_log (
+ job_log_id bigint(20) not null auto_increment comment '任务日志ID',
+ job_name varchar(64) not null comment '任务名称',
+ job_group varchar(64) not null comment '任务组名',
+ invoke_target varchar(500) not null comment '调用目标字符串',
+ job_message varchar(500) comment '日志信息',
+ status char(1) default '0' comment '执行状态(0正常 1失败)',
+ exception_info varchar(2000) default '' comment '异常信息',
+ create_time datetime comment '创建时间',
+ primary key (job_log_id)
+) engine=innodb comment = '定时任务调度日志表';
+
/**
+ * @author: whitepure
+ * @date: 2023/7/20 13:35
+ * @description: JobIndexController
+ */
+@Controller
+public class JobManagerController {
+
+ @RequestMapping({"/","/index"})
+ public String index(){
+ return "index.html";
+ }
+
+}
+
@RestController
+@RequestMapping("/main")
+public class SysJobController {
+ @Autowired
+ private ISysJobService jobService;
+
+ /**
+ * 查询定时任务列表
+ */
+ @GetMapping("/list")
+ public R<List<SysJob>> list(SysJob sysJob) {
+ return R.ok(jobService.selectJobList(sysJob));
+ }
+
+ /**
+ * 获取定时任务详细信息
+ */
+ @GetMapping(value = "/getInfo")
+ public R<SysJob> getInfo(Long jobId) {
+ return R.ok(jobService.selectJobById(jobId));
+ }
+
+ /**
+ * 新增定时任务
+ */
+ @PostMapping("/add")
+ public R<Boolean> add(@RequestBody SysJob job) throws SchedulerException, TaskException {
+ if (!CronUtils.isValid(job.getCronExpression())) {
+ return R.failed("新增任务'" + job.getJobName() + "'失败,Cron表达式不正确");
+ } else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), ScheduleConstants.LOOKUP_RMI)) {
+ return R.failed("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi'调用");
+ } else if (StrUtil.containsAnyIgnoreCase(job.getInvokeTarget(), new String[]{ScheduleConstants.LOOKUP_LDAP, ScheduleConstants.LOOKUP_LDAPS})) {
+ return R.failed("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap(s)'调用");
+ } else if (StrUtil.containsAnyIgnoreCase(job.getInvokeTarget(), new String[]{ScheduleConstants.HTTP, ScheduleConstants.HTTPS})) {
+ return R.failed("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)'调用");
+ } else if (StrUtil.containsAnyIgnoreCase(job.getInvokeTarget(), ScheduleConstants.JOB_ERROR_STR)) {
+ return R.failed("新增任务'" + job.getJobName() + "'失败,目标字符串存在违规");
+ } else if (!ScheduleUtils.whiteList(job.getInvokeTarget())) {
+ return R.failed("新增任务'" + job.getJobName() + "'失败,目标字符串不在白名单内");
+ }
+ return R.ok(jobService.insertJob(job) > 0);
+ }
+
+ /**
+ * 修改定时任务
+ */
+ @PostMapping("/edit")
+ public R<Boolean> edit(@RequestBody SysJob job) throws SchedulerException, TaskException {
+ if (!CronUtils.isValid(job.getCronExpression())) {
+ return R.failed("修改任务'" + job.getJobName() + "'失败,Cron表达式不正确");
+ } else if (StrUtil.containsIgnoreCase(job.getInvokeTarget(), ScheduleConstants.LOOKUP_RMI)) {
+ return R.failed("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi'调用");
+ } else if (StrUtil.containsAnyIgnoreCase(job.getInvokeTarget(), new String[]{ScheduleConstants.LOOKUP_LDAP, ScheduleConstants.LOOKUP_LDAPS})) {
+ return R.failed("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap(s)'调用");
+ } else if (StrUtil.containsAnyIgnoreCase(job.getInvokeTarget(), new String[]{ScheduleConstants.HTTP, ScheduleConstants.HTTPS})) {
+ return R.failed("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)'调用");
+ } else if (StrUtil.containsAnyIgnoreCase(job.getInvokeTarget(), ScheduleConstants.JOB_ERROR_STR)) {
+ return R.failed("修改任务'" + job.getJobName() + "'失败,目标字符串存在违规");
+ } else if (!ScheduleUtils.whiteList(job.getInvokeTarget())) {
+ return R.failed("修改任务'" + job.getJobName() + "'失败,目标字符串不在白名单内");
+ }
+ return R.ok(jobService.updateJob(job) > 0);
+ }
+
+ /**
+ * 定时任务状态修改
+ */
+ @PostMapping("/changeStatus")
+ public R<Boolean> changeStatus(@RequestBody SysJob job) throws SchedulerException {
+ SysJob newJob = jobService.selectJobById(job.getJobId());
+ newJob.setStatus(job.getStatus());
+ return R.ok(jobService.changeStatus(newJob) > 0);
+ }
+
+ /**
+ * 定时任务立即执行一次
+ */
+ @GetMapping("/run")
+ public R<Boolean> run(Long jobId) throws SchedulerException {
+ boolean result = jobService.run(jobId);
+ return result ? R.ok(true): R.failed("任务不存在或已过期!");
+ }
+
+ /**
+ * 删除定时任务
+ */
+ @GetMapping("/remove")
+ public R<Boolean> remove(Long jobId) throws SchedulerException {
+ jobService.deleteJobByIds(new Long[]{jobId});
+ return R.ok(true);
+ }
+}
+
@RestController
+@RequestMapping("/jobLog")
+public class SysJobLogController {
+ @Autowired
+ private ISysJobLogService jobLogService;
+
+ /**
+ * 查询定时任务调度日志列表
+ */
+ @GetMapping("/list")
+ public R<List<SysJobLog>> list(SysJobLog sysJobLog) {
+ return R.ok(jobLogService.selectJobLogList(sysJobLog));
+ }
+
+
+ /**
+ * 根据调度编号获取详细信息
+ */
+ @GetMapping(value = "/getInfo")
+ public R<SysJobLog> getInfo(Long logId) {
+ return R.ok(jobLogService.selectJobLogById(logId));
+ }
+
+
+ /**
+ * 删除定时任务调度日志
+ */
+ @PostMapping("/{jobLogIds}")
+ public R<Boolean> remove(@PathVariable Long[] jobLogIds) {
+ return R.ok(jobLogService.deleteJobLogByIds(jobLogIds) > 0);
+ }
+
+ /**
+ * 清空定时任务调度日志
+ */
+ @GetMapping("/clean")
+ public R<Boolean> clean() {
+ jobLogService.cleanJobLog();
+ return R.ok(true);
+ }
+
+}
+
@Slf4j
+@RestControllerAdvice("com.chenglian.scheduled")
+public class JobExceptionHandler {
+
+
+ @ExceptionHandler(Exception.class)
+ public R<Boolean> globalHandler(Exception exception) {
+ log.error("定时任务程序发生异常.", exception);
+ return R.failed(exception.getMessage());
+ }
+
+
+}
+
public class TaskException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ private final Code code;
+
+ public TaskException(String msg, Code code) {
+ this(msg, code, null);
+ }
+
+ public TaskException(String msg, Code code, Exception nestedEx) {
+ super(msg, nestedEx);
+ this.code = code;
+ }
+
+ public Code getCode() {
+ return code;
+ }
+
+ public enum Code {
+ TASK_EXISTS, NO_TASK_EXISTS, TASK_ALREADY_STARTED, UNKNOWN, CONFIG_ERROR, TASK_NODE_NOT_AVAILABLE
+ }
+}
+
public interface IErrorCode {
+ long getCode();
+
+ String getMsg();
+}
+
@Getter
+@ToString
+public enum ApiErrorCode implements IErrorCode {
+ FAILED(-1L, "操作失败"),
+ SUCCESS(0L, "执行成功");
+
+ private final long code;
+ private final String msg;
+
+ ApiErrorCode(final long code, final String msg) {
+ this.code = code;
+ this.msg = msg;
+ }
+
+ public static ApiErrorCode fromCode(long code) {
+ ApiErrorCode[] ecs = values();
+ ApiErrorCode[] var3 = ecs;
+ int var4 = ecs.length;
+
+ for(int var5 = 0; var5 < var4; ++var5) {
+ ApiErrorCode ec = var3[var5];
+ if (ec.getCode() == code) {
+ return ec;
+ }
+ }
+
+ return SUCCESS;
+ }
+}
+
@Data
+@EqualsAndHashCode
+public class R<T> implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+ private long code;
+ private T data;
+ private String msg;
+
+ public R() {
+ }
+
+ public R(IErrorCode errorCode) {
+ errorCode = (IErrorCode) Optional.ofNullable(errorCode).orElse(ApiErrorCode.FAILED);
+ this.code = errorCode.getCode();
+ this.msg = errorCode.getMsg();
+ }
+
+ public static <T> R<T> ok(T data) {
+ ApiErrorCode aec = ApiErrorCode.SUCCESS;
+ if (data instanceof Boolean && Boolean.FALSE.equals(data)) {
+ aec = ApiErrorCode.FAILED;
+ }
+
+ return restResult(data, aec);
+ }
+
+ public static <T> R<T> failed(String msg) {
+ return restResult(null, ApiErrorCode.FAILED.getCode(), msg);
+ }
+
+ public static <T> R<T> failed(IErrorCode errorCode) {
+ return restResult(null, errorCode);
+ }
+
+ public static <T> R<T> restResult(T data, IErrorCode errorCode) {
+ return restResult(data, errorCode.getCode(), errorCode.getMsg());
+ }
+
+ private static <T> R<T> restResult(T data, long code, String msg) {
+ R<T> apiResult = new R();
+ apiResult.setCode(code);
+ apiResult.setData(data);
+ apiResult.setMsg(msg);
+ return apiResult;
+ }
+
+ public boolean ok() {
+ return ApiErrorCode.SUCCESS.getCode() == this.code;
+ }
+}
+
public interface ISysJobLogService {
+ /**
+ * 获取quartz调度器日志的计划任务
+ *
+ * @param jobLog 调度日志信息
+ * @return 调度任务日志集合
+ */
+ List<SysJobLog> selectJobLogList(SysJobLog jobLog);
+
+ /**
+ * 通过调度任务日志ID查询调度信息
+ *
+ * @param jobLogId 调度任务日志ID
+ * @return 调度任务日志对象信息
+ */
+ SysJobLog selectJobLogById(Long jobLogId);
+
+ /**
+ * 新增任务日志
+ *
+ * @param jobLog 调度日志信息
+ */
+ void addJobLog(SysJobLog jobLog);
+
+ /**
+ * 批量删除调度日志信息
+ *
+ * @param logIds 需要删除的日志ID
+ * @return 结果
+ */
+ int deleteJobLogByIds(Long[] logIds);
+
+ /**
+ * 删除任务日志
+ *
+ * @param jobId 调度日志ID
+ * @return 结果
+ */
+ int deleteJobLogById(Long jobId);
+
+ /**
+ * 清空任务日志
+ */
+ void cleanJobLog();
+}
+
public interface ISysJobService {
+ /**
+ * 获取quartz调度器的计划任务
+ *
+ * @param job 调度信息
+ * @return 调度任务集合
+ */
+ List<SysJob> selectJobList(SysJob job);
+
+ /**
+ * 通过调度任务ID查询调度信息
+ *
+ * @param jobId 调度任务ID
+ * @return 调度任务对象信息
+ */
+ SysJob selectJobById(Long jobId);
+
+ /**
+ * 暂停任务
+ *
+ * @param job 调度信息
+ * @return 结果
+ */
+ int pauseJob(SysJob job) throws SchedulerException;
+
+ /**
+ * 恢复任务
+ *
+ * @param job 调度信息
+ * @return 结果
+ */
+ int resumeJob(SysJob job) throws SchedulerException;
+
+ /**
+ * 删除任务后,所对应的trigger也将被删除
+ *
+ * @param job 调度信息
+ * @return 结果
+ */
+ int deleteJob(SysJob job) throws SchedulerException;
+
+ /**
+ * 批量删除调度信息
+ *
+ * @param jobIds 需要删除的任务ID
+ * @return 结果
+ */
+ void deleteJobByIds(Long[] jobIds) throws SchedulerException;
+
+ /**
+ * 任务调度状态修改
+ *
+ * @param job 调度信息
+ * @return 结果
+ */
+ int changeStatus(SysJob job) throws SchedulerException;
+
+ /**
+ * 立即运行任务
+ *
+ * @param jobId 调度任务ID
+ * @return 结果
+ */
+ boolean run(Long jobId) throws SchedulerException;
+
+ /**
+ * 新增任务
+ *
+ * @param job 调度信息
+ * @return 结果
+ */
+ int insertJob(SysJob job) throws SchedulerException, TaskException;
+
+ /**
+ * 更新任务
+ *
+ * @param job 调度信息
+ * @return 结果
+ */
+ int updateJob(SysJob job) throws SchedulerException, TaskException, TaskException;
+
+ /**
+ * 校验cron表达式是否有效
+ *
+ * @param cronExpression 表达式
+ * @return 结果
+ */
+ boolean checkCronExpressionIsValid(String cronExpression);
+}
+
public abstract class AbstractQuartzJob implements Job {
+ private static final Logger log = LoggerFactory.getLogger(AbstractQuartzJob.class);
+
+ /**
+ * 线程本地变量
+ */
+ private static final ThreadLocal<Date> threadLocal = new ThreadLocal<>();
+
+ @Override
+ public void execute(JobExecutionContext context) {
+ SysJob sysJob = new SysJob();
+ BeanUtil.copyProperties(context.getMergedJobDataMap().get(ScheduleConstants.TASK_PROPERTIES),sysJob);
+ try {
+ before(context, sysJob);
+ if (sysJob.getJobId() != null) {
+ doExecute(context, sysJob);
+ }
+ after(context, sysJob, null);
+ } catch (Exception e) {
+ log.error("任务执行异常 - :", e);
+ after(context, sysJob, e);
+ }
+ }
+
+ /**
+ * 执行前
+ *
+ * @param context 工作执行上下文对象
+ * @param sysJob 系统计划任务
+ */
+ protected void before(JobExecutionContext context, SysJob sysJob) {
+ threadLocal.set(new Date());
+ }
+
+ /**
+ * 执行后
+ *
+ * @param context 工作执行上下文对象
+ * @param sysJob 系统计划任务
+ */
+ protected void after(JobExecutionContext context, SysJob sysJob, Exception e) {
+ Date startTime = threadLocal.get();
+ threadLocal.remove();
+
+ final SysJobLog sysJobLog = new SysJobLog();
+ sysJobLog.setJobName(sysJob.getJobName());
+ sysJobLog.setJobGroup(sysJob.getJobGroup());
+ sysJobLog.setInvokeTarget(sysJob.getInvokeTarget());
+ sysJobLog.setStartTime(startTime);
+ sysJobLog.setStopTime(new Date());
+ long runMs = sysJobLog.getStopTime().getTime() - sysJobLog.getStartTime().getTime();
+ sysJobLog.setJobMessage(sysJobLog.getJobName() + " 总共耗时:" + runMs + "毫秒");
+ if (e != null) {
+ sysJobLog.setStatus(ScheduleConstants.FAIL);
+ String errorMsg = StringUtils.substring(getExceptionMessage(e), 0, 2000);
+ sysJobLog.setExceptionInfo(errorMsg);
+ } else {
+ sysJobLog.setStatus(ScheduleConstants.SUCCESS);
+ }
+
+ // 写入数据库当中
+ SpringUtil.getBean(ISysJobLogService.class).addJobLog(sysJobLog);
+ }
+
+
+ public static String getExceptionMessage(Throwable e) {
+ StringWriter sw = new StringWriter();
+ e.printStackTrace(new PrintWriter(sw, true));
+ return sw.toString();
+ }
+
+ /**
+ * 执行方法,由子类重载
+ *
+ * @param context 工作执行上下文对象
+ * @param sysJob 系统计划任务
+ * @throws Exception 执行过程中的异常
+ */
+ protected abstract void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception;
+}
+
public class CronUtils {
+ /**
+ * 返回一个布尔值代表一个给定的Cron表达式的有效性
+ *
+ * @param cronExpression Cron表达式
+ * @return boolean 表达式是否有效
+ */
+ public static boolean isValid(String cronExpression) {
+ return CronExpression.isValidExpression(cronExpression);
+ }
+
+ /**
+ * 返回一个字符串值,表示该消息无效Cron表达式给出有效性
+ *
+ * @param cronExpression Cron表达式
+ * @return String 无效时返回表达式错误描述,如果有效返回null
+ */
+ public static String getInvalidMessage(String cronExpression) {
+ try {
+ new CronExpression(cronExpression);
+ return null;
+ } catch (ParseException pe) {
+ return pe.getMessage();
+ }
+ }
+
+ /**
+ * 返回下一个执行时间根据给定的Cron表达式
+ *
+ * @param cronExpression Cron表达式
+ * @return Date 下次Cron表达式执行时间
+ */
+ public static Date getNextExecution(String cronExpression) {
+ try {
+ CronExpression cron = new CronExpression(cronExpression);
+ return cron.getNextValidTimeAfter(new Date(System.currentTimeMillis()));
+ } catch (ParseException e) {
+ throw new IllegalArgumentException(e.getMessage());
+ }
+ }
+}
+
public class JobInvokeUtil {
+ /**
+ * 执行方法
+ *
+ * @param sysJob 系统任务
+ */
+ public static void invokeMethod(SysJob sysJob) throws Exception {
+ String invokeTarget = sysJob.getInvokeTarget();
+ String beanName = getBeanName(invokeTarget);
+ String methodName = getMethodName(invokeTarget);
+ List<Object[]> methodParams = getMethodParams(invokeTarget);
+
+ if (!isValidClassName(beanName)) {
+ Object bean = SpringUtil.getBean(beanName);
+ invokeMethod(bean, methodName, methodParams);
+ } else {
+ Object bean = Class.forName(beanName).getDeclaredConstructor().newInstance();
+ invokeMethod(bean, methodName, methodParams);
+ }
+ }
+
+ /**
+ * 调用任务方法
+ *
+ * @param bean 目标对象
+ * @param methodName 方法名称
+ * @param methodParams 方法参数
+ */
+ private static void invokeMethod(Object bean, String methodName, List<Object[]> methodParams)
+ throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException,
+ InvocationTargetException {
+ if (ObjectUtil.isNotEmpty(methodParams) && !methodParams.isEmpty()) {
+ Method method = bean.getClass().getMethod(methodName, getMethodParamsType(methodParams));
+ method.invoke(bean, getMethodParamsValue(methodParams));
+ } else {
+ Method method = bean.getClass().getMethod(methodName);
+ method.invoke(bean);
+ }
+ }
+
+ /**
+ * 校验是否为为class包名
+ *
+ * @param invokeTarget 名称
+ * @return true是 false否
+ */
+ public static boolean isValidClassName(String invokeTarget) {
+ return StringUtils.countMatches(invokeTarget, ".") > 1;
+ }
+
+ /**
+ * 获取bean名称
+ *
+ * @param invokeTarget 目标字符串
+ * @return bean名称
+ */
+ public static String getBeanName(String invokeTarget) {
+ String beanName = StringUtils.substringBefore(invokeTarget, "(");
+ return StringUtils.substringBeforeLast(beanName, ".");
+ }
+
+ /**
+ * 获取bean方法
+ *
+ * @param invokeTarget 目标字符串
+ * @return method方法
+ */
+ public static String getMethodName(String invokeTarget) {
+ String methodName = StringUtils.substringBefore(invokeTarget, "(");
+ return StringUtils.substringAfterLast(methodName, ".");
+ }
+
+ /**
+ * 获取method方法参数相关列表
+ *
+ * @param invokeTarget 目标字符串
+ * @return method方法相关参数列表
+ */
+ public static List<Object[]> getMethodParams(String invokeTarget) {
+ String methodStr = StringUtils.substringBetween(invokeTarget, "(", ")");
+ if (StringUtils.isEmpty(methodStr)) {
+ return null;
+ }
+ String[] methodParams = methodStr.split(",(?=([^\"']*[\"'][^\"']*[\"'])*[^\"']*$)");
+ List<Object[]> classs = new LinkedList<>();
+ for (int i = 0; i < methodParams.length; i++) {
+ String str = StringUtils.trimToEmpty(methodParams[i]);
+ // String字符串类型,以'或"开头
+ if (StringUtils.startsWithAny(str, "'", "\"")) {
+ classs.add(new Object[]{StringUtils.substring(str, 1, str.length() - 1), String.class});
+ }
+ // boolean布尔类型,等于true或者false
+ else if ("true".equalsIgnoreCase(str) || "false".equalsIgnoreCase(str)) {
+ classs.add(new Object[]{Boolean.valueOf(str), Boolean.class});
+ }
+ // long长整形,以L结尾
+ else if (StringUtils.endsWith(str, "L")) {
+ classs.add(new Object[]{Long.valueOf(StringUtils.substring(str, 0, str.length() - 1)), Long.class});
+ }
+ // double浮点类型,以D结尾
+ else if (StringUtils.endsWith(str, "D")) {
+ classs.add(new Object[]{Double.valueOf(StringUtils.substring(str, 0, str.length() - 1)), Double.class});
+ }
+ // 其他类型归类为整形
+ else {
+ classs.add(new Object[]{Integer.valueOf(str), Integer.class});
+ }
+ }
+ return classs;
+ }
+
+ /**
+ * 获取参数类型
+ *
+ * @param methodParams 参数相关列表
+ * @return 参数类型列表
+ */
+ public static Class<?>[] getMethodParamsType(List<Object[]> methodParams) {
+ Class<?>[] classs = new Class<?>[methodParams.size()];
+ int index = 0;
+ for (Object[] os : methodParams) {
+ classs[index] = (Class<?>) os[1];
+ index++;
+ }
+ return classs;
+ }
+
+ /**
+ * 获取参数值
+ *
+ * @param methodParams 参数相关列表
+ * @return 参数值列表
+ */
+ public static Object[] getMethodParamsValue(List<Object[]> methodParams) {
+ Object[] classs = new Object[methodParams.size()];
+ int index = 0;
+ for (Object[] os : methodParams) {
+ classs[index] = os[0];
+ index++;
+ }
+ return classs;
+ }
+}
+
@DisallowConcurrentExecution
+public class QuartzDisallowConcurrentExecution extends AbstractQuartzJob {
+ @Override
+ protected void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception {
+ JobInvokeUtil.invokeMethod(sysJob);
+ }
+}
+
public class QuartzJobExecution extends AbstractQuartzJob {
+ @Override
+ protected void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception {
+ JobInvokeUtil.invokeMethod(sysJob);
+ }
+}
+
public class ScheduleConstants {
+ public static final String TASK_CLASS_NAME = "TASK_CLASS_NAME";
+
+ /**
+ * 执行目标key
+ */
+ public static final String TASK_PROPERTIES = "TASK_PROPERTIES";
+
+ /**
+ * 默认
+ */
+ public static final String MISFIRE_DEFAULT = "0";
+
+ /**
+ * 立即触发执行
+ */
+ public static final String MISFIRE_IGNORE_MISFIRES = "1";
+
+ /**
+ * 触发一次执行
+ */
+ public static final String MISFIRE_FIRE_AND_PROCEED = "2";
+
+ /**
+ * 不触发立即执行
+ */
+ public static final String MISFIRE_DO_NOTHING = "3";
+
+ public enum Status {
+ /**
+ * 正常
+ */
+ NORMAL("0"),
+ /**
+ * 暂停
+ */
+ PAUSE("1");
+
+ private final String value;
+
+ Status(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+ }
+
+
+
+
+
+ /**
+ * http请求
+ */
+ public static final String HTTP = "http://";
+
+ /**
+ * https请求
+ */
+ public static final String HTTPS = "https://";
+
+ /**
+ * 通用成功标识
+ */
+ public static final String SUCCESS = "0";
+
+ /**
+ * 通用失败标识
+ */
+ public static final String FAIL = "1";
+
+
+ /**
+ * RMI 远程方法调用
+ */
+ public static final String LOOKUP_RMI = "rmi:";
+
+ /**
+ * LDAP 远程方法调用
+ */
+ public static final String LOOKUP_LDAP = "ldap:";
+
+ /**
+ * LDAPS 远程方法调用
+ */
+ public static final String LOOKUP_LDAPS = "ldaps:";
+
+ /**
+ * 定时任务白名单配置(仅允许访问的包名,如其他需要可以自行添加)
+ */
+ public static final String[] JOB_WHITELIST_STR = {"com.chenglian"};
+
+ /**
+ * 定时任务违规的字符
+ */
+ public static final String[] JOB_ERROR_STR = {"java.net.URL", "javax.naming.InitialContext", "org.yaml.snakeyaml",
+ "org.springframework", "org.apache", "com.chenglian.common.utils.file", "com.chenglian.common.config"};
+
+
+}
+
public class ScheduleUtils {
+ /**
+ * 得到quartz任务类
+ *
+ * @param sysJob 执行计划
+ * @return 具体执行任务类
+ */
+ private static Class<? extends Job> getQuartzJobClass(SysJob sysJob) {
+ boolean isConcurrent = "0".equals(sysJob.getConcurrent());
+ return isConcurrent ? QuartzJobExecution.class : QuartzDisallowConcurrentExecution.class;
+ }
+
+ /**
+ * 构建任务触发对象
+ */
+ public static TriggerKey getTriggerKey(Long jobId, String jobGroup) {
+ return TriggerKey.triggerKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup);
+ }
+
+ /**
+ * 构建任务键对象
+ */
+ public static JobKey getJobKey(Long jobId, String jobGroup) {
+ return JobKey.jobKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup);
+ }
+
+ /**
+ * 创建定时任务
+ */
+ public static void createScheduleJob(Scheduler scheduler, SysJob job) throws SchedulerException, TaskException {
+ Class<? extends Job> jobClass = getQuartzJobClass(job);
+ // 构建job信息
+ Long jobId = job.getJobId();
+ String jobGroup = job.getJobGroup();
+ JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(getJobKey(jobId, jobGroup)).build();
+
+ // 表达式调度构建器
+ CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression());
+ cronScheduleBuilder = handleCronScheduleMisfirePolicy(job, cronScheduleBuilder);
+
+ // 按新的cronExpression表达式构建一个新的trigger
+ CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(getTriggerKey(jobId, jobGroup))
+ .withSchedule(cronScheduleBuilder).build();
+
+ // 放入参数,运行时的方法可以获取
+ jobDetail.getJobDataMap().put(ScheduleConstants.TASK_PROPERTIES, job);
+
+ // 判断是否存在
+ if (scheduler.checkExists(getJobKey(jobId, jobGroup))) {
+ // 防止创建时存在数据问题 先移除,然后在执行创建操作
+ scheduler.deleteJob(getJobKey(jobId, jobGroup));
+ }
+
+ // 判断任务是否过期
+ if (Objects.nonNull(CronUtils.getNextExecution(job.getCronExpression()))) {
+ // 执行调度任务
+ scheduler.scheduleJob(jobDetail, trigger);
+ }
+
+ // 暂停任务
+ if (job.getStatus().equals(ScheduleConstants.Status.PAUSE.getValue())) {
+ scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup));
+ }
+ }
+
+ /**
+ * 设置定时任务策略
+ */
+ public static CronScheduleBuilder handleCronScheduleMisfirePolicy(SysJob job, CronScheduleBuilder cb)
+ throws TaskException {
+ switch (job.getMisfirePolicy()) {
+ case ScheduleConstants.MISFIRE_DEFAULT:
+ return cb;
+ case ScheduleConstants.MISFIRE_IGNORE_MISFIRES:
+ return cb.withMisfireHandlingInstructionIgnoreMisfires();
+ case ScheduleConstants.MISFIRE_FIRE_AND_PROCEED:
+ return cb.withMisfireHandlingInstructionFireAndProceed();
+ case ScheduleConstants.MISFIRE_DO_NOTHING:
+ return cb.withMisfireHandlingInstructionDoNothing();
+ default:
+ throw new TaskException("The task misfire policy '" + job.getMisfirePolicy()
+ + "' cannot be used in cron schedule tasks", TaskException.Code.CONFIG_ERROR);
+ }
+ }
+
+ /**
+ * 检查包名是否为白名单配置
+ *
+ * @param invokeTarget 目标字符串
+ * @return 结果
+ */
+ public static boolean whiteList(String invokeTarget) {
+ String packageName = StringUtils.substringBefore(invokeTarget, "(");
+ int count = StringUtils.countMatches(packageName, ".");
+ if (count > 1) {
+ return CharSequenceUtil.containsAnyIgnoreCase(invokeTarget, ScheduleConstants.JOB_WHITELIST_STR);
+ }
+ Object obj = SpringUtil.getBean(StringUtils.split(invokeTarget, ".")[0]);
+ String beanPackageName = obj.getClass().getPackage().getName();
+ return CharSequenceUtil.containsAnyIgnoreCase(beanPackageName, ScheduleConstants.JOB_WHITELIST_STR)
+ && !CharSequenceUtil.containsAnyIgnoreCase(beanPackageName, ScheduleConstants.JOB_ERROR_STR);
+ }
+}
+
+ +
+ + + + + +在日常的接口开发中,为了防止非法参数对业务造成影响,经常需要对接口的参数做校验,最简单就是用if条件语句来判断,但是随着参数越来越多,业务越来越复杂,判断参数代码语句显得尤为冗长.
+或者有些程序会将if封装起来,例如spring中的assert类,进行判断,但归根结底还是靠代码对接口参数一个一个校验,这样太繁琐了,而且如果参数太多代码可读性比较差.
+@Slf4j
+@RequestMapping("/Chapter1")
+@RestController
+public class ValidatedCaseController1 {
+
+
+ @PostMapping("/case1")
+ public R<Boolean> case1(UserEntity1 userEntity){
+ String username = userEntity.getUsername();
+ if (CharSequenceUtil.isBlank(username)){
+ return R.failed("用户名不能为空");
+ }
+ return R.ok(true);
+ }
+
+
+ @PostMapping("/case2")
+ public R<Boolean> case2(UserEntity1 userEntity){
+ String username = userEntity.getUsername();
+ Assert.notNull(username,"用户名不能为空");
+ return R.ok(true);
+ }
+
+}
+
包括引入依赖,创建controller,创建校验实体,错误异常处理.
+从springboot-2.3开始,校验包被独立成了一个starter组件,所以需要引入validation和web,而springboot-2.3之前的版本只需要引入 web 依赖就可以了.
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-web</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-validation</artifactId>
+ </dependency>
+
/**
+ * 常用校验注解
+ * JSR提供的校验注解:
+ * @Null 被注释的元素必须为 null
+ * @NotNull 被注释的元素必须不为 null
+ * @AssertTrue 被注释的元素必须为 true
+ * @AssertFalse 被注释的元素必须为 false
+ * @Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
+ * @Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
+ * @DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
+ * @DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
+ * @Size(max=, min=) 被注释的元素的大小必须在指定的范围内
+ * @Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
+ * @Past 被注释的元素必须是一个过去的日期
+ * @Future 被注释的元素必须是一个将来的日期
+ * @Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式
+ *
+ */
+@Data
+public class UserEntity1 {
+
+ @NotNull(message = "id不能为空")
+ private Integer id;
+
+ @NotBlank(message = "用户名不能为空")
+ private String username;
+
+ @Size(max = 10,min = 7,message = "密码位数必须在7-10位")
+ @NotBlank(message = "密码不能为空")
+ private String password;
+
+ @NotBlank(message = "企业名称不能为空")
+ private String enterpriseName;
+
+ @Null
+ private Integer contactId;
+
+ @Null
+ private BigDecimal money;
+
+ @Email(message = "电子邮箱不能为空")
+ private String email;
+
+ @NotBlank(message = "手机号不能为空")
+ private String mobile;
+
+}
+
@Slf4j
+@RequestMapping("/Chapter1")
+@RestController
+public class ValidatedCaseController1 {
+
+
+ @PostMapping("/case3")
+ public R<Boolean> case3(@RequestBody @Validated UserEntity1 userEntity) {
+ log.info("userEntity=>{}", userEntity);
+ return R.ok(true);
+ }
+
+}
+
发起请求测试接口
+POST http://localhost:8080/Chapter1/case3
+Content-Type: application/json
+
+{
+ "id": "1",
+ "username": "111",
+ "password": "123",
+ "enterpriseName": "1112",
+ "contactId": "",
+ "money": "",
+ "email": "",
+ "mobile": "123123"
+}
+
@Slf4j
+@RestControllerAdvice(basePackages = {"com.whitepure.demo.validated"})
+public class ValidatedExceptionAdvice {
+
+
+ /**
+ * json格式参数校验异常
+ */
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ public R<String> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
+ log.error(e.getMessage(), e);
+ return R.failed(Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage());
+ }
+
+
+ @ExceptionHandler(Exception.class)
+ public R<String> handleRuntimeException(RuntimeException e, HttpServletRequest request) {
+ String requestUri = request.getRequestURI();
+ log.error("请求地址'{}',发生未知异常.", requestUri, e);
+ return R.failed(e.getMessage().length() > 1000 ? "系统错误" : e.getMessage());
+ }
+
+}
+
包括自定义校验注解,validated分组校验,对象嵌套校验,表单格式校验.
+@Data
+public class UserEntity2 {
+
+ @NotNull(message = "id不能为空",groups = ValidGroup.Crud.Update.class)
+ @Null(groups = ValidGroup.Crud.Create.class,message = "id添加时必须为null")
+ private Integer id;
+
+ @NotBlank(message = "用户名不能为空",groups = {ValidGroup.Crud.Update.class, ValidGroup.Crud.Create.class})
+ private String username;
+
+ @Password(groups = {ValidGroup.Crud.Update.class, ValidGroup.Crud.Create.class})
+ private String password;
+
+ @NotBlank(message = "企业名称不能为空")
+ @Chinese(message = "企业名称只能为中文")
+ private String enterpriseName;
+
+ @Null(groups = {ValidGroup.Crud.Update.class, ValidGroup.Crud.Create.class})
+ private Integer contactId;
+
+ @NotNull(message = "日期不能为null")
+ @Past(message = "当前日期必须是过期的一个日期",groups = {ValidGroup.Crud.Update.class, ValidGroup.Crud.Create.class})
+ private Date yesterday;
+
+ @NotNull(message = "展示不能为null")
+ @AssertTrue(message = "展示字段必须为true",groups = {ValidGroup.Crud.Update.class, ValidGroup.Crud.Create.class})
+ private Boolean isShow;
+
+ @Email(message = "电子邮箱不合法",groups = {ValidGroup.Crud.Update.class, ValidGroup.Crud.Create.class})
+ @NotBlank(message = "电子邮箱不能为空")
+ private String email;
+
+ @Mobile(groups = {ValidGroup.Crud.Update.class, ValidGroup.Crud.Create.class})
+ private String mobile;
+
+ @Size(max = 3, min = 1, message = "图片为1-3张")
+ @NotNull(message = "图片不能为空")
+ private List<String> imgs;
+
+ @Valid
+ @NotNull(groups = {ValidGroup.Crud.Create.class, ValidGroup.Crud.Update.class}, message = "工作对象不能为空")
+ private Job job;
+
+ @Data
+ public static class Job {
+
+ @NotNull(groups = {ValidGroup.Crud.Update.class}, message = "工作id不能为空")
+ @Min(value = 1, groups = ValidGroup.Crud.Update.class)
+ private Long jobId;
+
+ @NotNull(groups = {ValidGroup.Crud.Create.class, ValidGroup.Crud.Update.class}, message = "工作名称不能为空")
+ @Length(min = 2, max = 10, groups = {ValidGroup.Crud.Create.class, ValidGroup.Crud.Update.class},message = "工作名称.2-10位之间")
+ private String jobName;
+
+ @NotNull(groups = {ValidGroup.Crud.Create.class, ValidGroup.Crud.Update.class}, message = "工作职务不能为空")
+ @Length(min = 2, max = 10, groups = {ValidGroup.Crud.Create.class, ValidGroup.Crud.Update.class},message = "工作职务.2-10位之间")
+ private String position;
+ }
+
+}
+
@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.FIELD,ElementType.PARAMETER})
+@Constraint(validatedBy = ChineseValidator.class)
+public @interface Chinese {
+
+ String[] value() default {};
+
+ String message() default "请输入中文";
+
+ Class<?>[] groups() default {};
+
+ Class<? extends Payload>[] payload() default {};
+
+ String regexp() default ".*";
+
+}
+
+
+public class ChineseValidator implements ConstraintValidator<Chinese, String> {
+
+ @SneakyThrows
+ @Override
+ public boolean isValid(String value, ConstraintValidatorContext context) {
+ if (value == null || StringPool.EMPTY.equals(value)){
+ return false;
+ }
+ return Pattern.compile(PatternPool.CHINESE_PATTERN, Pattern.CASE_INSENSITIVE).matcher(value).matches();
+ }
+
+}
+
@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.FIELD,ElementType.PARAMETER})
+@Constraint(validatedBy = MobileValidator.class)
+public @interface Mobile {
+
+ String[] value() default {};
+
+ String message() default "手机号不合法";
+
+ Class<?>[] groups() default {};
+
+ Class<? extends Payload>[] payload() default {};
+
+ String regexp() default ".*";
+
+}
+
+
+public class MobileValidator implements ConstraintValidator<Mobile, String> {
+
+ @SneakyThrows
+ @Override
+ public boolean isValid(String value, ConstraintValidatorContext context) {
+ if (value == null || StringPool.EMPTY.equals(value)){
+ return false;
+ }
+ return Pattern.compile(PatternPool.MOBILE_PATTERN, Pattern.CASE_INSENSITIVE).matcher(value).matches();
+ }
+
+}
+
@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.FIELD,ElementType.PARAMETER})
+@Constraint(validatedBy = PasswordValidated.class)
+public @interface Password {
+
+ String[] value() default {};
+
+ String message() default "密码不合法.以字母开头,长度在6~18之间,只能包含字符、数字和下划线。";
+
+ Class<?>[] groups() default {};
+
+ Class<? extends Payload>[] payload() default {};
+
+ String regexp() default ".*";
+
+}
+
+public class PasswordValidated implements ConstraintValidator<Password, String> {
+
+ @Override
+ public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
+ if (value == null || StringPool.EMPTY.equals(value)){
+ return false;
+ }
+ return Pattern.compile(PatternPool.PASSWORD_PATTERN, Pattern.CASE_INSENSITIVE).matcher(value).matches();
+ }
+
+}
+
public interface PatternPool {
+
+ String MOBILE_PATTERN = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(16[5,6])|(17[0-8])|(18[0-9])|(19[1、5、8、9]))\\d{8}$";
+
+ String CHINESE_PATTERN = "^[\\u4e00-\\u9fa5]{0,}$";
+
+ String PASSWORD_PATTERN = "^[a-zA-Z]\\w{5,17}$";
+}
+
public interface ValidGroup extends Default {
+
+ interface Crud extends ValidGroup{
+ interface Create extends Crud{
+
+ }
+
+ interface Update extends Crud{
+
+ }
+
+ interface Query extends Crud{
+
+ }
+
+ interface Delete extends Crud{
+
+ }
+ }
+}
+
@Slf4j
+@RequestMapping("/Chapter2")
+@RestController
+public class ValidatedCaseController2 {
+
+ @PostMapping("/case1")
+ public R<Boolean> case1(@RequestBody @Validated(ValidGroup.Crud.Create.class) UserEntity2 userEntity) {
+ log.info("userEntity=>{}", userEntity);
+ return R.ok(true);
+ }
+
+
+ @PostMapping("/case2")
+ public R<Boolean> case2(@RequestBody @Validated(ValidGroup.Crud.Update.class) UserEntity2 userEntity) {
+ log.info("userEntity=>{}", userEntity);
+ return R.ok(true);
+ }
+
+}
+
发起请求测试接口
+POST http://localhost:8080/Chapter2/case1
+Content-Type: application/json
+
+{
+ "id": "",
+ "username": "111",
+ "password": "a1231123121",
+ "enterpriseName": "企业名称",
+ "contactId": "",
+ "email": "11123@qq.com",
+ "yesterday": "2023-06-30",
+ "isShow": "true",
+ "mobile": "18830281211"
+}
+
表单格式参数校验
+@Slf4j
+@RequestMapping("/Chapter3")
+@RestController
+@Validated
+public class ValidatedCaseController3 {
+
+
+ @PostMapping("/case1")
+ public R<Boolean> case1(
+ @NotBlank(message = "用户名称不能为空") String username,
+ @NotBlank(message = "密码不能为空") String password,
+ @NotNull(message = "联系人id不能为空") Integer cid
+ ) {
+ log.info("username=>{}", username);
+ return R.ok(true);
+ }
+
+}
+
发起请求测试接口
+POST http://localhost:8080/Chapter3/case1
+
@Slf4j
+@RestControllerAdvice(basePackages = {"com.whitepure.demo.validated"})
+public class ValidatedExceptionAdvice {
+
+ /**
+ * json格式参数校验异常
+ */
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ public R<String> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
+ log.error(e.getMessage(), e);
+ return R.failed(Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage());
+ }
+
+ /**
+ * 表单格式参数验证异常
+ */
+ @ExceptionHandler(ConstraintViolationException.class)
+ public R<String> handleMethodArgumentNotValidException(ConstraintViolationException e) {
+ log.error(e.getMessage(), e);
+ return R.failed(e.getMessage());
+ }
+
+ @ExceptionHandler(Exception.class)
+ public R<String> handleRuntimeException(RuntimeException e, HttpServletRequest request) {
+ String requestUri = request.getRequestURI();
+ log.error("请求地址'{}',发生未知异常.", requestUri, e);
+ return R.failed(e.getMessage().length() > 1000 ? "系统错误" : e.getMessage());
+ }
+
+}
+
+ +
+ + + + + +主要分为:连接层,服务层,引擎层,存储层。
+客户端执行一条select命令的流程如下:
+ +连接层:最上层是一些客户端和连接服务,包含本地sock通信和大多数基于客户端/服务端工具实现的类似于tcplip的通信。主要完成一些类似于连接处理、授权认证、及相关的安全方案。在该层上引入了线程池的概念,为通过认证安全接入的客户端提供线程。同样在该层上可以实现基于SSL的安全链接。服务器也会为安全接入的每个客户端验证它所具有的操作权限。
+服务层:第二层架构主要完成大多少的核心服务功能,如SQL接口,并完成缓存的查询,SQL的分析和优化及部分内置函数的执行。所有跨存储引擎的功能也在这一层实现,如过程、函数等。在该层,服务器会解析查询并创建相应的内部解析树,并对其完成相应的优化如确定查询表的顺序是否利用索引等,最后生成相应的执行操作。如果是select语句,服务器还会查询内部的缓存。如果缓存空间足够大,这样在解决大量读操作的环境中能够很好的提升系统的性能。
+引擎层:存储引擎层,存储引擎真正的负责了MySQL中数据的存储和提取,服务器通过API与存储引擎进行通信。不同的存储引擎具有的功能不同这样我们可以根据自己的实际需要进行选取,后面介绍MyISAM和InnoDB。
+存储层:数据存储层,主要是将数据存储在运行于裸设备的文件系统之上,并完成与存储引擎的交互。
+其中比较重要的就要说存储引擎了,MySQL默认使用的存储引擎是InnoDB,可以使用相关命令来查看:
+ show engines;
+
如果查看当前使用的存储引擎可以使用命令:
+ show variables like '%storage_engine%';
+
在创建数据库的时候可以指定存储引擎:
+ CREATE TABLE tbl_emp (
+ id INT(11) NOT NULL AUTO_INCREMENT,
+ NAME VARCHAR(20) DEFAULT NULL,
+ deptId INT(11) DEFAULT NULL,
+ PRIMARY KEY (id)
+ )ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
+
参考文章:
+为什么要进行优化?
+总之一句话,数据库出现性能瓶颈。
+想要进行SQL优化首先要弄懂SQL编写过程和SQL执行过程是存在区别的:
+ select distinct .. from .. join .. on .. where .. group by .. having .. order by .. limit ..
+
from .. on .. join .. where .. group by .. having .. select distinct .. order by .. limit ..
+
之所以SQL的编写过程会和执行过程有区别,是因为Mysql中有专门负责优化SELECT语句的优化器模块。
+++MySQL Query Optimizer:MySQL查询优化器,主要功能:通过计算分析系统中收集到的统计信息,为客户端请求的Query提供MySQL认为最优的执行计划。
+
当客户端向MySQL请求一条Query,命令解析器模块完成请求分类,区别出是SELECT并转发给MySQL Query Optimizer时,MySQL Query Optimizer首先会对整条Query进行优化,处理掉一些常量表达式的预算直接换算成常量值。 +并对Query中的查询条件进行简化和转换,如去掉一些无用或显而易见的条件、结构调整等。然后分析Query 中的 Hint信息(如果有),看显示Hint信息是否可以完全确定该Query的执行计划。如果没有Hint 或Hint信息还不足以完全确定执行计划,则会读取所涉及对象的统计信息,根据Query进行写相应的计算分析,然后再得出最后的执行计划。
+++Hint: 简单来说就是在某些特定的场景下人工协助MySQL优化器的工作,使其生成最优的执行计划。一般来说,优化器的执行计划都是最优化的,不过在某些特定场景下,执行计划可能不是最优化。
+
数据库优化方案很多,主要分为两大类:软件层面、硬件层面。
+软件层面包括:SQL 调优、表结构优化、读写分离、数据库集群、分库分表等;硬件层面主要是增加机器性能。对于后端程序员来说最主要的就是软件层面优化索引和分库分表了。
+当数据库存储出现瓶颈的时候,就需要进行分库分表了。
+分表主要是为了减少单张表的大小,解决单表数据量带来的性能问题。当单表数据增量过快,业界流传是超过500万的数据量就要考虑分表了。当然500万只是一个经验值,大家可以根据实际情况做出决策。
+分表有几个维度,一是水平切分和垂直切分,二是单库内分表和多库内分表:
+水平拆分:基于数据划分,表结构相同,数据不同。例如,现有一个业务表,新建业务表2,业务表与业务表2中字段一致,将ID为奇数的存储在业务表中,偶数的存放在业务表2中;
+垂直拆分:基于表或字段划分,表结构不同。例如,将业务表中不常用的字段name和age存放到业务表2中;
+单库内拆:先分库,将不同数据库中的表拆分为了多个子表,多个子表存在于同一数据库中; +
+多库拆分: +
+在一个数据库中将一张表拆分为几个子表在一定程度上可以解决单表查询性能的问题,但是也会遇到一个问题:单数据库存储瓶颈。所以在业界用的更多的还是将子表拆分到多个数据库中。
+分库分表问题:
+MySQL官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构。可以得到索引的本质:索引是数据结构。相当于一本书的目录或者字典。
+比如,要查“mysql”这个单词,我们肯定需要定位到m字母,然后从下往下找到y字母,再找到剩下的sql;如果没有索引,那么你可能需要逐个逐个寻找。你可以简单理解为,索引维护的就是一组排好序的数据。
+我们平常所说的索引,如果没有特别指明,都是指B树结构组织的索引。当然,除了B+树这种类型的索引之外,还有哈稀索引等。
+总结:索引,这种数据结构就是以空间换取时间。
+索引的优势:
+索引的劣势:
+创建索引:
+ CREATE [UNIQUE] INDEX indexName ON mytable(columnName(length));
+ ALTER mytable ADD [UNIQUE] INDEX [indexName] ON (columnName(length));
+
删除索引:
+ DROP INDEX [indexName] ON [tableName];
+
修改索引:
+-- 该语句添加一个主键,这意味着索引值必须是唯一的,且不能为NULL
+ ALTER TABLE [tableName] ADD PRIMARY KEY (column_list);
+
+-- 这条语句创建索引的值必须是唯一的(除了NULL外,NULL可能会出现多次)
+ ALTER TABLE [tableName] ADD UNIQUE [indexName] (column_list);
+
+-- 添加普通索引,索引值可出现多次
+ ALTER TABLE [tableName] ADD INDEX [indexName] (column_list);
+
+-- 该语句指定了索引为FULLTEXT,用于全文索引
+ ALTER TABLE [tableName] ADD FULLTEXT [indexName] (column_list);
+
查看索引:
+ SHOW INDEX FROM [tableName];
+
varchar(5)
和varchar(200)
的字段建立索引,则优先考虑为varchar(5)
的建立;如果非要为varchar(200)
的字段建立索引那么可以这样:
+ -- 为varchar(200)的字段建立索引,将其长度设置为 20
+ create index tbl_001 on dual(address(20));
+
++惯用的百分比界线是”30%”。匹配的数据量超过一定限制的时候查询器会放弃使用索引,这也是索引失效的场景之一。
+
使用explain关键字可以模拟优化器执行SQL查询语句,从而知道MySQL是如何处理你的SQL语句的。分析你的查询语句或是表结构的性能瓶颈。
+++explain不止适用于select也适用于delete、insert、replace和update语句。
+
当explain可解释语句一起使用时,MySQL会显示来自优化器的关于语句执行计划的信息。也就是说,MySQL解释了它将如何处理语句,包括有关表如何连接以及按顺序连接的信息。
+在MySQL中执行任意一条SQL前加上explain关键字,即可看到:
+ +----+-------------+----------+------+---------------+------+---------+------+------+-------+
+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+ +----+-------------+----------+------+---------------+------+---------+------+------+-------+
+
table是显示这一行的数据是关于哪张表的。
+id是select查询的序列号,包含一组数字,表示查询中执行select子句或操作表的顺序。
+++表的执行顺序与表中数据息息相关,举个例子:表A:3条数据 表B:4条数据 表C:6条数据
+假设在执行某条SQL的情况下表A、B、C的id值相同,假设执行顺序:表A、B、C。在给表B添加4条数据后,再次执行前面的这条SQL发现执行顺序变为:表A、C、B。 +
+
+这是因为中间结果会影响表的执行顺序: +3 * 4 * 2 = 12 * 2 = 24 +3 * 2 * 4 = 6 * 4 = 24 +虽然最后结果一样,但是中间过程不一样。中间过程越小占用的空间越小,所以在ID值相同的情况下数据小的表优先查询。
select_type是查询的类型,主要是用于区别普通查询、联合查询、子查询等的复杂查询。
+查询类型:
+ explain select name from (select * from table1) t1;
+
explain select name from (select * from table1 union select * from table2) t1;
+
type是索引类型,是较为重要的一个指标,结果值从最好到最坏依次是:
+ system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index >ALL
+
实际工作中可能用不到这么多,一般只要记住以下就行了:
+ system > const > eq_ref > ref > range > index > ALL
+
其中system、const只是理想情况,实际能达到ref、range。
+常见索引类型:
+ -- 衍生表:(select * from tbl_001) 只有一条记录;id为主键索引
+ explain select * from (select * from tbl_001) where id = 1;
+
-- tbl_001表只有一条记录;id为主键索引或唯一键索引
+ explain select * from tbl_001 where id = 1;
+
-- tbl_001.tid为索引列;且tbl_001与tbl_002表中数据记录一一对应
+ select * from tbl_001,tbl_002 where tbl_001.tid = tbl_002.id
+
-- name 为索引列;tbl_001 表中 name 值为 zs 的有多个
+ explain select * from tbl_001 where name = 'zs';
+
-- tid 为索引列;
+ explain select * from tbl_001 where tid <=3;
+
-- name为索引列;
+ explain select name from tbl_001;
+
-- name不是索引列;
+ explain select name from tbl_001;
+
possible_keys是可能用到的索引,是一种预测,不准确;key是实际用到的索引,如果为null则没有用到索引。
+ -- name为索引列
+ explain select name from tbl_001;
+ +----+-------------+---------+-------+---------------+----------+---------+------+------+-------------+
+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+ +----+-------------+---------+-------+---------------+----------+---------+------+------+-------------+
+ | 1 | SIMPLE | tbl_001 | index | NULL | idx_name | 83 | NULL | 4 | Using index |
+ +----+-------------+---------+-------+---------------+----------+---------+------+------+-------------+
+
key_len索引使用的长度,可通过索引列长度,例如:name vachar(20)
,来判断复合索引到底有没有使用。
-- name为索引列;可以为null;varchar类型;utf8字符集;
+ explain select name from tbl_001 name = 'zs';
+ +----+-------------+---------+-------+---------------+----------+---------+------+------+-------------+
+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+ +----+-------------+---------+-------+---------------+----------+---------+------+------+-------------+
+ | 1 | SIMPLE | tbl_001 | index | NULL | idx_name | 63 | NULL | 4 | Using index |
+ +----+-------------+---------+-------+---------------+----------+---------+------+------+-------------+
+
其中,如果索引字段可以为null,MySQL会用一个字节进行标识;varchar为可变长度,MySQL用两个字节来标识。
+++utf8:一个字符三个字节; +gbk:一个字符两个字节; +latin:一个字符一个字节;
+
举例,字段name vachar(20)
,不能为null,utf8编码,执行执行计划name列使用到了索引那么key_len等于20*3+1+2=63
。
ref指明当前表所引用的字段。
+ -- 其中b.d可以为常量,常量用 const 标识
+ select ... where a.c = b.d;
+
-- tid 为索引列
+ explain select * from tbl_001 t1,tbl_002 t2 where t1.tid = t2.id and t1.name = 'zs';
+ +----+-------------+-------+--------+------------------+----------+---------+-----------------+------+-------------+
+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+ +----+-------------+-------+--------+------------------+----------+---------+-----------------+------+-------------+
+ | 1 | SIMPLE | t1 | ref | idx_name,idx_tid | idx_name | 83 | const | 1 | Using where |
+ | 1 | SIMPLE | t2 | eq_ref | PRIMARY | PRIMARY | 4 | sql_demo.t1.tid | 1 | NULL |
+ +----+-------------+-------+--------+------------------+----------+---------+-----------------+------+-------------+
+
rows表示查询行数,实际通过索引查询到的个数。
+ mysql> select * from tbl_001 t1,tbl_002 t2 where t1.tid = t2.id and t1.name = 'ls';
+ +----+------+------+----+------+-------+-------+
+ | id | name | tid | id | name | name1 | name2 |
+ +----+------+------+----+------+-------+-------+
+ | 2 | ls | 2 | 2 | ls | ls1 | ls2 |
+ | 4 | ls | 1 | 1 | zs | zs1 | zs2 |
+ +----+------+------+----+------+-------+-------+
+
+ mysql> explain select * from tbl_001 t1,tbl_002 t2 where t1.tid = t2.id and t1.name = 'ls';
+ +----+-------------+-------+-------+------------------+------------+---------+----------------+------+-------------+
+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+ +----+-------------+-------+-------+------------------+------------+---------+----------------+------+-------------+
+ | 1 | SIMPLE | t2 | index | PRIMARY | index_name | 249 | NULL | 2 | Using index |
+ | 1 | SIMPLE | t1 | ref | idx_name,idx_tid | idx_tid | 5 | sql_demo.t2.id | 1 | Using where |
+ +----+-------------+-------+-------+------------------+------------+---------+----------------+------+-------------+
+
Extra包含不适合在其他列中显示但十分重要的额外信息。
+常见值:
+ -- name 为单值索引
+ explain select * from tbl_001 where name = 'zs' order by tid;
+ +----+-------------+---------+------+---------------+----------+---------+-------+------+----------------+
+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+ +----+-------------+---------+------+---------------+----------+---------+-------+------+----------------+
+ | 1 | SIMPLE | tbl_001 | ref | idx_name | idx_name | 83 | const | 1 | Using filesort |
+ +----+-------------+---------+------+---------------+----------+---------+-------+------+----------------+
+
-- name,tid 为复合索引
+ explain select * from tbl_001 where name = 'zs' order by tid;
+ +----+-------------+---------+------+---------------+-----------+---------+-------+------+-------------+
+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+ +----+-------------+---------+------+---------------+-----------+---------+-------+------+-------------+
+ | 1 | SIMPLE | tbl_001 | ref | idx_trans | idx_trans | 83 | const | 1 | Using index |
+ +----+-------------+---------+------+---------------+-----------+---------+-------+------+-------------+
+
++跨列使用指,where后边的字段和order by后边的字段拼接起来,看是否满足索引的顺序,如不满足则为跨列,会出现Using filsort。
+
-- name、tid、cid 为复合索引
+ explain select name from tbl_001 where name = 'zs' group by cid;
+ +----+-------------+---------+------+---------------+---------+---------+-------+------+------------------------------+
+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+ +----+-------------+---------+------+---------------+---------+---------+-------+------+------------------------------+
+ | 1 | SIMPLE | tbl_001 | ref | idx_ntc | idx_ntc | 83 | const | 2 | Using index; Using temporary |
+ +----+-------------+---------+------+---------------+---------+---------+-------+------+------------------------------+
+
++覆盖索引(Covering Index),一说为索引覆盖: 查询的数据列从索引文件中就能够取得,不必读取原数据行,MySQL可以利用索引返回select列表中的字段,而不必根据索引再次读取数据文件,换句话说查询到的列全部都在索引列中就是索引覆盖。 +如果要使用覆盖索引,一定要注意select列表中只取出需要的列,不可select*,因为如果将所有字段一起做索引会导致索引文件过大,查询性能下降。
+
-- name、tid、cid 为复合索引
+ explain select name,tid,cid from tbl_001 where name = 'zs';
+ +----+-------------+---------+------+---------------+---------+---------+-------+------+-------------+
+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+ +----+-------------+---------+------+---------------+---------+---------+-------+------+-------------+
+ | 1 | SIMPLE | tbl_001 | ref | idx_ntc | idx_ntc | 83 | const | 2 | Using index |
+ +----+-------------+---------+------+---------------+---------+---------+-------+------+-------------+
+
-- name 为单值索引,此时索引中不包含其他字段,想要查询其他字段就必须回表查询
+ explain select name,tid,cid from tbl_001 where name = 'zs';
+ +----+-------------+---------+------+---------------+---------+---------+-------+------+-------------+
+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+ +----+-------------+---------+------+---------------+---------+---------+-------+------+-------------+
+ | 1 | SIMPLE | tbl_001 | ref | idx_name | idx_name | 83 | const | 2 | Using where |
+ +----+-------------+---------+------+---------------+---------+---------+-------+------+-------------+
+
explain select * from tbl_001 where name = 'zs' and name = 'ls';
+ +----+-------------+-------+------+---------------+------+---------+------+------+------------------+
+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+ +----+-------------+-------+------+---------------+------+---------+------+------+------------------+
+ | 1 | SIMPLE | NULL | NULL | NULL | NULL | NULL | NULL | NULL | Impossible WHERE |
+ +----+-------------+-------+------+---------------+------+---------+------+------+------------------+
+
-- 准备测试表、测试数据
+ create table book_2021(
+ bid int(4) primary key,
+ name varchar(20) not null,
+ authorid int(4) not null,
+ publicid int(4) not null,
+ typeid int(4) not null
+ );
+ insert into book_2021 values(1,'java',1,1,2);
+ insert into book_2021 values(2,'php',4,1,2);
+ insert into book_2021 values(3,'c',1,2,2);
+ insert into book_2021 values(4,'c#',3,1,2);
+ insert into book_2021 values(5,'c++',3,1,2);
+
需要优化的SQL:
+ -- SQL原型,优化该SQL
+ select bid from book_2021 where typeid in(2,3) and authorid=1 order by typeid desc;
+
执行计划分析该SQL,发现该SQL存在Using filesort
,type为ALL,需要进行SQL优化。
explain select bid from book_2021 where typeid in(2,3) and authorid=1 order by typeid desc;
+ +----+-------------+-----------+------+---------------+------+---------+------+------+-----------------------------+
+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+ +----+-------------+-----------+------+---------------+------+---------+------+------+-----------------------------+
+ | 1 | SIMPLE | book_2021 | ALL | NULL | NULL | NULL | NULL | 5 | Using where; Using filesort |
+ +----+-------------+-----------+------+---------------+------+---------+------+------+-----------------------------+
+
typeid、authorid、bid
;
+ -- sql执行顺序
+ from .. on .. join .. where .. group by .. having .. select distinct .. order by .. limit ..
+
-- 建立索引
+ alter table book_2021 add index idx_tab(typeid,authorid,bid);
+
-- 优化后的SQL
+ select bid from book_2021 where authorid=1 and typeid in(2,3) order by typeid desc;
+
++温馨提示:索引一旦升级优化,需要删除索引防止干扰
+
-- 删除 idx_tab 索引
+ drop index idx_tab on book_2021;
+
+ -- 给表 book_2021 添加索引 idx_atb
+ alter table book_2021 add index idx_atb(authorid,typeid,bid);
+
最终优化后的SQL执行过程:
+ explain select bid from book_2021 where authorid=1 and typeid in(2,3) order by typeid desc;
+ +----+-------------+-----------+------------+-------+---------------+---------+---------+------+------+----------+-----------------------------------------------+
+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+ +----+-------------+-----------+------------+-------+---------------+---------+---------+------+------+----------+-----------------------------------------------+
+ | 1 | SIMPLE | book_2021 | NULL | range | idx_atb | idx_atb | 8 | NULL | 3 | 100.00 | Using where; Backward index scan; Using index |
+ +----+-------------+-----------+------------+-------+---------------+---------+---------+------+------+----------+-----------------------------------------------+
+
单表优化总结:
+ -- 准备测试表、测试数据
+ create table teacher2(
+ tid int(4) primary key,
+ cid int(4) not null
+ );
+
+ insert into teacher2 values(1,2);
+ insert into teacher2 values(2,1);
+ insert into teacher2 values(3,3);
+
+ create table course2(
+ cid int(4) not null,
+ cname varchar(20)
+ );
+
+ insert into course2 values(1,'java');
+ insert into course2 values(2,'phython');
+ insert into course2 values(3,'kotlin');
+
需要优化的SQL:
+ -- SQL原型,优化该SQL
+ select * from teacher2 t left outer join course2 c on t.cid = c.cid where c.cname = 'java';
+
执行过程分析该SQL,发现进行了全表扫描。
+ explain select * from teacher2 t left outer join course2 c on t.cid = c.cid where c.cname = 'java';
+ +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+
+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+ +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+
+ | 1 | SIMPLE | c | NULL | ALL | NULL | NULL | NULL | NULL | 3 | 33.33 | Using where |
+ | 1 | SIMPLE | t | NULL | ALL | NULL | NULL | NULL | NULL | 3 | 33.33 | Using where; Using join buffer (hash join) |
+ +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+
+
优化思路:
+t.cid、c.name
字段加索引;++小表驱动大表:假设 小表10条数据;大表300条数据;让大小表做嵌套循环,无论10是外层循环还是300是外层循环,结果都是3000次,但是为了减少表连接创建的次数,应该将10,即小表放在外层循环这样效率会更高。
+小表驱动大表原因: +现有两个表A与B ,表A有200条数据,表B有20万条数据;按照循环的概念举个例子: +小表驱动大表 > A驱动表,B被驱动表 +
+for(200条){ for(20万条){ ... } }
+大表驱动小表 > B驱动表,A被驱动表 +for(20万条){ for(200条){ ... } }
+总结: +如果小的循环在外层,对于表连接来说就只连接200次 ; +如果大的循环在外层,则需要进行20万次表连接,从而浪费资源,增加消耗 ; +小表驱动大表的主要目的是通过减少表连接创建的次数,加快查询速度。
-- 给表teacher2、course2 添加索引
+ alter table teacher2 add index idx_teacher2_cid(cid);
+ alter table course2 add index idx_course2_cname(cname);
+
最终优化后的SQL:
+ -- 添加索引后
+ explain select * from teacher2 t left outer join course2 c on t.cid = c.cid where c.cname = 'java';
+ +----+-------------+-------+------------+------+-------------------+-------------------+---------+----------------+------+----------+-------------+
+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+ +----+-------------+-------+------------+------+-------------------+-------------------+---------+----------------+------+----------+-------------+
+ | 1 | SIMPLE | c | NULL | ref | idx_course2_cname | idx_course2_cname | 83 | const | 1 | 100.00 | Using where |
+ | 1 | SIMPLE | t | NULL | ref | idx_teacher2_cid | idx_teacher2_cid | 4 | sql_demo.c.cid | 1 | 100.00 | Using index |
+ +----+-------------+-------+------------+------+-------------------+-------------------+---------+----------------+------+----------+-------------+
+
多表优化总结:
+++跨列使用指,where后边的字段和order by后边的字段拼接起来,看是否满足索引的顺序,如不满足则为跨列,会出现Using filsort。
+
-- age列为索引,但此时SQL语句age列存在计算,age索引列失效;
+ select * from tbl_001 where age*3 = '10';
+
+ -- name为索引列,此时在该索引列上进行函数计算,name索引列失效;
+ select * from tbl_001 tbl_001 left(name,4)='July';
+
+ -- typeid为索引列,typeid为int类型,此时发生隐式类型转换,typeid索引列失效;
+ select * from tbl_001 c typeid = '1';
+
++大部分情况下如果复合索引使用上面的运算符索引会失效,但是MySQL在服务层存在SQL优化阶段,所以复合索引即使使用了不等于、is null、is not null等操作,还是可能会存在索引生效的情况。
+
-- name、tid、cid 为复合索引
+ select name,tid,cid from tbl_001 where name = 'zs';
+
-- name 为索引列,此处索引失效
+ select * from tbl_002 where name like '%x%';
+
+ -- 如果必须要使用%开头,可以使用覆盖索引挽救一下
+ select name from tbl_002 where name like '%x%';
+
-- authorid、typeid为复合索引,此处or的左右两边索引全部失效
+ select * from book_2021 where authorid = 1 or typeid = 1;
+
-- 不要使用*,把*替换成具体需要的列
+ select * from book_2021;
+
两者可以互相替代,如果主查询的数据集大,使用in的效率高一点;如果子查询的数据集大,使用exist效率高一点;
+ -- 假设此处主查询数据集较大,使用in
+ select * from tbl_001 where tid in (select id from tbl_001);
+
+ -- 假设此处子查询数据集较大,使用exist
+ select tid from tbl_001 where exists (select * from tbl_001);
+
++exists语法:将主查询的结果,放到子查询结果中进行判断,看子查询中是否有数据,如果有数据则保留数据并返回;没有则返回空。
+
order by 是比较常用的一个关键字,基本上查询出来的数据都要进行排序,否则就太乱了。使用order by的时候常常伴随着Using filesort的发生,Using filesort在底层有两种算法,根据IO的次数分为:
+ -- 设置读取文件缓冲区(buffer)的大小,单位字节
+ set max_length_for_sort_data = 1024;
+
如果需要排序的列的总大小超过了设置的max_length_for_sort_data
那么MySQL底层会自动从单路排序切换到双路排序。
保证全部索引排序字段的一致性,都是升序或都是降序,不要部分升序部分降序。
+在MySQL中可以通过抓去慢SQL日志来进行SQL排查,慢SQL日志超过响应时间的阈值默认10秒,便会记录该SQL;慢SQL日志默认是关闭的,建议在开发调优时打开,最终部署上线时关闭。
+通过SQL来查看记录慢SQL日志是否开启:
+ -- 查看变量 slow_query_log,记录慢查询日志默认关闭
+ show variables like '%slow_query_log%';
+ +---------------------+----------------------------------------------+
+ | Variable_name | Value |
+ +---------------------+----------------------------------------------+
+ | slow_query_log | OFF |
+ | slow_query_log_file | /usr/local/mysql/data/MacBook-Pro-2-slow.log |
+ +---------------------+----------------------------------------------+
+
可以通过SQL来进行开启记录慢SQL日志:
+ -- 在内存中开启慢SQL记录日志; 此种方式在 mysql 服务重启后失效
+ set global slow_query_log=1;
+
+ -- 查看是否开启慢SQL日志记录
+ show variables like '%slow_query_log%';
+ +---------------------+----------------------------------------------+
+ | Variable_name | Value |
+ +---------------------+----------------------------------------------+
+ | slow_query_log | ON |
+ | slow_query_log_file | /usr/local/mysql/data/MacBook-Pro-2-slow.log |
+ +---------------------+----------------------------------------------+
+
-- 在MySQL my.cnf 配置文件中追加配置;重启MySQL服务后也生效,即永久开启
+ slow_query_log=1
+ slow_query_log_file=/usr/local/mysql/data/MacBook-Pro-2-slow.log
+
查看慢SQL记录日志阈值:
+ -- 查看慢SQL记录日志阈值,默认10秒
+ show variables like '%long_query_time%';
+ +-----------------+-----------+
+ | Variable_name | Value |
+ +-----------------+-----------+
+ | long_query_time | 10.000000 |
+ +-----------------+-----------+
+
修改慢SQL记录日志阈值:
+ -- 在内存中修改设置慢查询SQL记录日志阈值为5秒;注意设置后不会立即生效,需要重新登陆mysql客户端才会生效
+ set global long_query_time = 5;
+
+ -- 设置之后重新登陆mysql使用命令查看慢SQL查询日志阈值
+ show variables like '%long_query_time%';
+ +-----------------+-----------+
+ | Variable_name | Value |
+ +-----------------+-----------+
+ | long_query_time | 5.000000 |
+ +-----------------+-----------+
+
-- 在MySQL配置文件my.cnf中修改慢查询SQL记录日志阈值,设置为3秒
+ long_query_time=3
+
-- 模拟慢查询SQL
+ select sleep(5);
+
+ -- 查看是否开启慢SQL日志记录
+ show variables like '%slow_query_log%';
+ +---------------------+----------------------------------------------+
+ | Variable_name | Value |
+ +---------------------+----------------------------------------------+
+ | slow_query_log | ON |
+ | slow_query_log_file | /usr/local/mysql/data/MacBook-Pro-2-slow.log |
+ +---------------------+----------------------------------------------+
+
cat /usr/local/mysql/data/MacBook-Pro-2-slow.log
执行命令:
+ MacBook-Pro-2:~ root# cat /usr/local/mysql/data/MacBook-Pro-2-slow.log
+ /usr/local/mysql/bin/mysqld, Version: 8.0.22 (MySQL Community Server - GPL). started with:
+ Tcp port: 3306 Unix socket: /tmp/mysql.sock
+ Time Id Command Argument
+ # Time: 2021-09-17T06:46:20.655775Z
+ # User@Host: root[root] @ localhost [] Id: 15
+ # Query_time: 4.003891 Lock_time: 0.000000 Rows_sent: 1 Rows_examined: 1
+ use sql_demo;
+ SET timestamp=1631861176;
+ select sleep(4);
+
mysqldumpslow
工具来查看,可以通过一些过滤条件快速找到需要定位的慢SQL;
+++语法:mysqldumpslow 各种参数 慢查询日志文件路径 +-s:排序方式 +-r:逆序反转 +-l:锁定时间,不要从总时间中减去锁定时间 +-g:在文件中查找考虑包含此字符串的 +更多指令解析查看 mysqldumpslow –help
+
-- 获取返回记录最多的3条SQL
+ mysqldumpslow -s r -t 3 /usr/local/mysql/data/MacBook-Pro-2-slow.log
+
+ -- 获取访问次数最多的3条SQL
+ mysqldumpslow -s r -t 3 /usr/local/mysql/data/MacBook-Pro-2-slow.log
+
+ -- 按照时间排序,前10条包含 left join 的查询SQL
+ mysqldumpslow -s t -t 10 -g "left join" /usr/local/mysql/data/MacBook-Pro-2-slow.log
+
模拟数据;
+ -- 创建表
+ create database testdata;
+ use testdata;
+
+ create table dept
+ (
+ dno int(5) primary key default 0,
+ dname varchar(20) not null default '',
+ loc varchar(30) default ''
+ ) engine=innodb default charset=utf8;
+
+ create table emp
+ (
+ eid int(5) primary key,
+ ename varchar(20) not null default '',
+ job varchar(20) not null default '',
+ deptno int(5) not null default 0
+ )engine=innodb default charset=utf8;
+
-- 创建存储过程:获取随机字符串
+ set global log_bin_trust_function_creators=1;
+ use testdata;
+ delimiter $
+ create function randstring(n int) returns varchar(255)
+ begin
+
+ declare all_str varchar(100) default 'abcdefghijklmnopqrestuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
+ declare return_str varchar(255) default '';
+ declare i int default 0;
+ while i<n
+ do
+ set return_str=concat(return_str, substring(all_str, FLOOR(1+rand()*52), 1));
+ set i=i+1;
+ end while;
+ return return_str;
+ end $
+
-- 创建存储函数:插入随机整数
+ use testdata;
+ create function ran_num() returns int(5)
+ begin
+
+ declare i int default 0;
+ set i=floor(rand()*100);
+ return i;
+
+ end$
+
-- 创建存储过程:向emp表插入数据
+ create procedure insert_emp(in eid_start int(10), in data_times int(10))
+ begin
+ declare i int default 0;
+ set autocommit =0;
+
+ repeat
+ insert into emp values(eid_start+i, randstring(5), 'other', ran_num());
+ set i=i+1;
+ until i=data_times
+ end repeat;
+
+ commit;
+
+ end $
+
-- 创建存储过程:向dept表插入数据
+ create procedure insert_dept(in dno_start int(10), in data_times int(10))
+ begin
+ declare i int default 0;
+ set autocommit =0;
+
+ repeat
+ insert into dept values(dno_start+i, randstring(6), randstring(8));
+ set i=i+1;
+ until i=data_times
+ end repeat;
+
+ commit;
+
+ end $
+
-- 向emp、dept表中插入数据
+ delimiter ;
+ call insert_emp(1000, 800000);
+ call insert_dept(10, 30);
+
-- 验证插入数据量
+ select count(1) from emp;
+
使用show profile
进行sql分析;
-- 查看 profiling 是否开启,默认是关闭的
+ show variables like 'profiling';
+ +---------------+-------+
+ | Variable_name | Value |
+ +---------------+-------+
+ | profiling | OFF |
+ +---------------+-------+
+
+ -- 如果没开启将其开启
+ set profiling=on;
+
执行测试SQL,任意执行均可供之后SQL分析;
+select * from emp;
+
+select * from emp group by eid order by eid;
+
+select * from emp group by eid limit 150000;
+
-- 执行命令,查看结果
+ show profiles;
+ +----------+------------+--------------------------------------------------+
+ | Query_ID | Duration | Query |
+ +----------+------------+--------------------------------------------------+
+ | 2 | 0.00300900 | show tables |
+ | 3 | 0.01485700 | desc emp |
+ | 4 | 0.00191200 | select * from emp group by eid%10 limit 150000 |
+ | 5 | 0.25516300 | select * from emp |
+ | 6 | 0.00026400 | select * from emp group by eid%10 limit 150000 |
+ | 7 | 0.00019600 | select eid from emp group by eid%10 limit 150000 |
+ | 8 | 0.03907500 | select eid from emp group by eid limit 150000 |
+ | 9 | 0.06499100 | select * from emp group by eid limit 150000 |
+ | 10 | 0.00031500 | select * from emp group by eid%20 order by 5 |
+ | 11 | 0.00009100 | select * from emp group by eid%20 order by |
+ | 12 | 0.00012400 | select * from emp group by eid order by |
+ | 13 | 0.25195300 | select * from emp group by eid order by eid |
+ | 14 | 0.00196200 | show variables like 'profiling' |
+ | 15 | 0.26208900 | select * from emp group by eid order by eid |
+ +----------+------------+--------------------------------------------------+
+
使用命令show profile cpu,block io for query 上一步前面执行 show profiles 的 Query_ID ;
诊断SQL;
++参数备注,不区分大小写: +all:显示所有的开销信息; +block io:显示块lO相关开销; +context switches:上下文切换相关开销; +cpu:显示CPU相关开销信息; +ipc:显示发送和接收相关开销信息; +memory:显示内存相关开销信息; +page faults:显示页面错误相关开销信息; +source:显示和Source_function,Source_file,Source_line相关的开销信息; +swaps:显示交换次数相关开销的信息;
+
show profile cpu,block io for query 3;
+ +----------------------------+----------+----------+------------+--------------+---------------+
+ | Status | Duration | CPU_user | CPU_system | Block_ops_in | Block_ops_out |
+ +----------------------------+----------+----------+------------+--------------+---------------+
+ | starting | 0.004292 | 0.000268 | 0.000941 | 0 | 0 |
+ | checking permissions | 0.000026 | 0.000012 | 0.000013 | 0 | 0 |
+ | checking permissions | 0.000007 | 0.000005 | 0.000003 | 0 | 0 |
+ | Opening tables | 0.003883 | 0.000865 | 0.000880 | 0 | 0 |
+ | init | 0.000022 | 0.000013 | 0.000008 | 0 | 0 |
+ | System lock | 0.000013 | 0.000012 | 0.000002 | 0 | 0 |
+ | optimizing | 0.000994 | 0.000078 | 0.000155 | 0 | 0 |
+ | statistics | 0.000233 | 0.000222 | 0.000011 | 0 | 0 |
+ | preparing | 0.000046 | 0.000043 | 0.000003 | 0 | 0 |
+ | Creating tmp table | 0.000075 | 0.000073 | 0.000002 | 0 | 0 |
+ | executing | 0.001532 | 0.000141 | 0.000266 | 0 | 0 |
+ | checking permissions | 0.000050 | 0.000044 | 0.000005 | 0 | 0 |
+ | checking permissions | 0.000014 | 0.000012 | 0.000002 | 0 | 0 |
+ | checking permissions | 0.001539 | 0.000102 | 0.000356 | 0 | 0 |
+ | checking permissions | 0.000044 | 0.000037 | 0.000007 | 0 | 0 |
+ | checking permissions | 0.000019 | 0.000016 | 0.000002 | 0 | 0 |
+ | checking permissions | 0.001250 | 0.000089 | 0.000318 | 0 | 0 |
+ | end | 0.000015 | 0.000007 | 0.000008 | 0 | 0 |
+ | query end | 0.000006 | 0.000004 | 0.000001 | 0 | 0 |
+ | waiting for handler commit | 0.000041 | 0.000040 | 0.000002 | 0 | 0 |
+ | removing tmp table | 0.000012 | 0.000010 | 0.000001 | 0 | 0 |
+ | waiting for handler commit | 0.000010 | 0.000009 | 0.000002 | 0 | 0 |
+ | closing tables | 0.000019 | 0.000018 | 0.000001 | 0 | 0 |
+ | freeing items | 0.000648 | 0.000081 | 0.000152 | 0 | 0 |
+ | cleaning up | 0.000067 | 0.000047 | 0.000021 | 0 | 0 |
+ +----------------------------+----------+----------+------------+--------------+---------------+
+
不要在生产环境开启这个功能。
+在配置MySQL文件my.cnf
设置全局查询日志:
-- 开启全局查询日志
+general_log=1
+
+-- 记录日志文件的路径
+general_log_file=/path/logfile
+
+-- 设置输出格式为 FILE
+log_output=FILE
+
+-- 通过cat命令直接查看;如果为空则需要造数
+cat /var/lib/mysql/bigdata01.log;
+
在mysql客户端设置全局查询日志:
+-- 查看是否开启全局查询日志
+show variables like '%general_log%';
+
+-- 开启全局查询日志
+set global general_log=1;
+
+-- 设置输出格式为 TABLE
+set global log_output='TABLE';
+
+-- 可在mysql库里的geneial_log表查看;如果为空则需要造数
+select * from mysql.general_log;
+
在数据库中,除传统的计算资源(如CPU、RAM、I/O等)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂。 +简而言之,锁机制是解决因资源共享,而造成的并发问题。
+MySQL中的锁按照对数据操作的类型分为:读锁即共享锁,写锁即互斥锁;按照操作粒度来分,分为表锁、行锁,页锁。
+测试数据:
+ create table mylock (
+ id int not null primary key auto_increment,
+ name varchar(20) default ''
+ ) engine myisam;
+
+ insert into mylock(name) values('a');
+ insert into mylock(name) values('b');
+ insert into mylock(name) values('c');
+ insert into mylock(name) values('d');
+ insert into mylock(name) values('e');
+
常用命令:
+ -- 加锁
+ lock table 表名1 read/write, 表名2 read/write, 其他;
+
+ -- 解锁
+ unlock tables;
+
+ -- 查看表上是否加锁;在In_use列,0代表没有加锁,1代表加锁
+ show open tables;
+
+ -- 分析表锁
+ -- Table_locks_immediate:产生表级锁定的次数,表示可以立即获取锁的查询次数,每立即获取锁值加1
+ -- Table_locks_waited:出现表级锁定争用而发生等待的次数(不能立即获取锁的次数,每等待一次锁值加1),此值高则说明存在着较严重的表级锁争用情况
+ show status like 'table_locks%';
+
给表添加读锁及测试读操作:
+ + +给表添加读锁及测试写操作:
+ + +给某个表添加读锁之后,所有会话都能对这个表进行读操作,但是当前会话不能对该表进行写操作,对其他表进行读、写操作;其他会话能需要等持有锁的会话释放该表的锁后,才能进行写操作,期间将会一直处于等待状态,可以对其他表进行读写操作。
+给表添加写锁及测试读操作:
+ + +给表添加写锁及测试写操作:
+ + +给某个表添加写锁之后,当前会话可以对该表进行写操作、读操作,其他会话要想对该表进行读写操作需要等持有锁的会话释放锁,否则期间将会一直等待该锁释放;但是当前持有锁的会话是不能对其他表进行读写操作,其他会话能够对其他表进行读写操作。
+对于表锁MyISAM在执行查询语句前,会自动给涉及的所有表加读锁,在执行增删改操作前,会自动给涉及的表加写锁。
+简而言之,就是读锁会阻塞写,但是不会堵塞读。而写锁则会把读和写都堵塞。
+测试数据:
+ CREATE TABLE test_innodb_lock (a INT(11),b VARCHAR(16))ENGINE=INNODB;
+
+ INSERT INTO test_innodb_lock VALUES(1,'b2');
+ INSERT INTO test_innodb_lock VALUES(3,'3');
+ INSERT INTO test_innodb_lock VALUES(4, '4000');
+ INSERT INTO test_innodb_lock VALUES(5,'5000');
+ INSERT INTO test_innodb_lock VALUES(6, '6000');
+ INSERT INTO test_innodb_lock VALUES(7,'7000');
+ INSERT INTO test_innodb_lock VALUES(8, '8000');
+ INSERT INTO test_innodb_lock VALUES(9,'9000');
+ INSERT INTO test_innodb_lock VALUES(1,'b1');
+
+ CREATE INDEX test_innodb_a_ind ON test_innodb_lock(a);
+ CREATE INDEX test_innodb_lock_b_ind ON test_innodb_lock(b);
+
+
常用命令:
+ -- 分析行锁定:
+ -- 尤其是当等待次数很高,而且每次等待时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待,然后根据分析结果着手指定优化计划。
+ -- Innodb_row_lock_current_waits:当前正在等待锁定的数量;
+ -- Innodb_row_lock_time:从系统启动到现在锁定总时间长度;
+ -- Innodb_row_lock_time_avg:每次等待所花平均时间;
+ -- Innodb_row_lock_time_max:从系统启动到现在等待最常的一次所花的时间;
+ -- Innodb_row_lock_waits:系统启动后到现在总共等待的次数;
+ -- show status like 'innodb_row_lock%';
+
由于行锁与事务相关,所以在测试之前需要关闭MySQL的自动提交:
+ -- 关闭自动提交;0关闭,1打开
+ set autocommit = 0;
+
测试行锁读写操作:
+ + +当前会话当关闭MySQL自动提交后,修改表中的某一行数据,未提交前其他会话是不可见该修改的数据的;如果当前会话和修改某一行数据其他会话也修改该行数据则其他会话会一直等待,直到持有锁的会话commit后才会执行。 +如果当前会话和其他会话操作同一张表的不同行数据时,则相互不影响。
+使用行锁时注意,无索引行或索引失效都会导致行锁变为表锁。
+间隙锁:
+当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁,对于键值在条件范围内但并不存在的记录,叫做“间隙”。 +InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁。
+因为Query执行过程中通过过范围查找的话,他会锁定整个范围内所有的索引键值,即使这个键值并不存在。
+间隙锁有一个比较致命的弱点,就是当锁定一个范围键值之后,即使某些不存在的键值也会被无辜的锁定,而造成在锁定的时候无法插入锁定键值范围内的任何数据。在某些场景下这可能会对性能造成很大的危害。
+如何锁定一行?
+在查询语句使用for update
关键字:
select * from test_innodb_lock where a=8 for update;
+
Innodb存储引擎由于实现了行级锁定,虽然在锁定机制的实现方面所带来的性能损耗可能比表级锁定会要更高一些,但是在整体并发处理能力方面要远远优于MyISAM的表级锁定的。当系统并发量较高的时候,Innodb的整体性能和MylISAM相比就会有比较明显的优势了。 +但是,Innodb的行级锁定同样也有其脆弱的一面,当我们使用不当的时候,可能会让Innodb的整体性能表现不仅不能比MyISAM高,甚至可能会更差。
+分库分表是两回事儿,可别搞混了,可能是光分库不分表,也可能是光分表不分库,都有可能。分库分表一定是为了支撑高并发、数据量大两个问题的。
+分库分表场景: +假设你现在在一个小公司,注册用户就 20 万,每天活跃用户就 1 万,每天单表数据量就 1000,然后高峰期每秒钟并发请求最多就 10 个。随着公司业务发展,过了几年,注册用户数达到了 5000 万,每天活跃用户数 200 万,每天单表数据量 50 万条,数据库磁盘容量不断消耗掉,高峰期并发达到惊人的 5000~8000,这时候你会发现你得系统已经支撑不住了已经挂掉了。 其实在挂掉之前就应该考虑一个数据量得增长,考虑分库分表。
+一句话概括,随着业务的发展,数据库会遇到瓶颈,单表得数据量太大,会极大影响你的 sql 执行的性能,到了后面你的 sql 可能就跑的很慢了。所以为了维持功能的正常使用不得不分库分表。
+分表: +分表就是把一个表的数据放到多个表中,然后查询的时候你就查一个表。比如按照用户 id 来分表,将一个用户的数据就放在一个表中。然后操作的时候你对一个用户就操作那个表就好了。这样可以控制每个表的数据量在可控的范围内,比如每个表就固定在 200 万以内。
+分库: +分库是啥意思?就是你一个库一般我们经验而言,最多支撑到并发 2000,一定要扩容了,而且一个健康的单库并发值你最好保持在每秒 1000 左右,不要太大。那么你可以将一个库的数据拆分到多个库中,访问的时候就访问一个库好了。
+Cobar: +阿里 b2b 团队开发和开源的,属于 proxy 层方案,就是介于应用服务器和数据库服务器之间。应用程序通过 JDBC 驱动访问 Cobar 集群,Cobar 根据 SQL 和分库规则对 SQL 做分解,然后分发到 MySQL 集群不同的数据库实例上执行。早些年还可以用,但是最近几年都没更新了,基本没啥人用,差不多算是被抛弃的状态吧。而且不支持读写分离、存储过程、跨库 join 和分页等操作。
+TDDL: +淘宝团队开发的,属于 client 层方案。支持基本的 crud 语法和读写分离,但不支持 join、多表查询等语法。目前使用的也不多,因为还依赖淘宝的 diamond 配置管理系统。
+Atlas: +360 开源的,属于 proxy 层方案,以前是有一些公司在用的,但是确实有一个很大的问题就是社区最新的维护都在 5 年前了。所以,现在用的公司基本也很少了。
+Sharding-jdbc: +当当开源的,属于 client 层方案,是 ShardingSphere 的 client 层方案, ShardingSphere 还提供 proxy 层的方案 Sharding-Proxy。确实之前用的还比较多一些,因为 SQL 语法支持也比较多,没有太多限制,而且截至 2019.4,已经推出到了 4.0.0-RC1 版本,支持分库分表、读写分离、分布式 id 生成、柔性事务(最大努力送达型事务、TCC 事务)。而且确实之前使用的公司会比较多一些(这个在官网有登记使用的公司,可以看到从 2017 年一直到现在,是有不少公司在用的),目前社区也还一直在开发和维护,还算是比较活跃,个人认为算是一个现在也可以选择的方案。
+Mycat: +基于 Cobar 改造的,属于 proxy 层方案,支持的功能非常完善,而且目前应该是非常火的而且不断流行的数据库中间件,社区很活跃,也有一些公司开始在用了。但是确实相比于 Sharding jdbc 来说,年轻一些,经历的锤炼少一些。
+综上,现在其实建议考量的,就是 Sharding-jdbc 和 Mycat,这两个都可以去考虑使用。无论分库还是分表,上面说的那些数据库中间件都是可以支持的。就是基本上那些中间件可以做到你分库分表之后,中间件可以根据你指定的某个字段值,比如说 userid,自动路由到对应的库上去,然后再自动路由到对应的表里去。
+Sharding-jdbc 这种 client 层方案的优点在于不用部署,运维成本低,不需要代理层的二次转发请求,性能很高,但是如果遇到升级啥的需要各个系统都重新升级版本再发布,各个系统都需要耦合 Sharding-jdbc 的依赖;
+Mycat 这种 proxy 层方案的缺点在于需要部署,自己运维一套中间件,运维成本高,但是好处在于对于各个项目是透明的,如果遇到升级之类的都是自己中间件那里搞就行了。
+通常来说,这两个方案其实都可以选用,但是建议中小型公司选用 Sharding-jdbc,client 层方案轻便,而且维护成本低,不需要额外增派人手,而且中小型公司系统复杂度会低一些,项目也没那么多;但是中大型公司最好还是选用 Mycat 这类 proxy 层方案,因为可能大公司系统和项目非常多,团队很大,人员充足,那么最好是专门弄个人来研究和维护 Mycat,然后大量项目直接透明使用即可。
+拆分方案一般分为两种一种是水平拆分,一种是垂直拆分。
+水平拆分的意思,就是把一个表的数据给弄到多个库的多个表里去,但是每个库的表结构都一样,只不过每个库表放的数据是不同的,所有库表的数据加起来就是全部数据。水平拆分的意义,就是将数据均匀放更多的库里,然后用多个库来扛更高的并发,还有就是用多个库的存储容量来进行扩容。
+垂直拆分的意思,就是把一个有很多字段的表给拆分成多个表,或者是多个库上去。每个库表的结构都不一样,每个库表都包含部分字段。一般来说,会将较少的访问频率很高的字段放到一个表里去,然后将较多的访问频率很低的字段放到另外一个表里去。因为数据库是有缓存的,你访问频率高的行字段越少,就可以在缓存里缓存更多的行,性能就越好。这个一般在表层面做的较多一些。
+除了垂直和水平拆分外,还有比较流行的分库分表方式,一种是按照range(范围)来分,就是每个库一段连续的数据,这个一般是按比如时间范围来的,但是这种一般较少用,因为很容易产生热点问题,大量的流量都打在最新的数据上了;另一种是按照某个字段的hash值,一下均匀分散。
+range:好处在于扩容的时候很简单,因为你只要预备好,给每个月都准备一个库就可以了,到了一个新的月份的时候,自然而然,就会写新的库了;缺点,但是大部分的请求,都是访问最新的数据。实际生产用 range,要看场景。
+hash:好处在于可以平均分配每个库的数据量和请求压力;坏处在于说扩容起来比较麻烦,会有一个数据迁移的过程,之前的数据需要重新计算 hash 值重新分配到不同的库或表。
+现在有一个未分库分表的系统,未来要分库分表,如何设计才可以让系统从未分库分表动态切换到分库分表上?
+方案一:停机迁移方案
+方案二:双写迁移方案
+分库分表之后你必然要面对的一个问题,就是 id 咋生成?因为要是分成多个表之后,每个表都是从 1 开始累加,那肯定不对啊,需要一个全局唯一的 id 来支持。
+分库分表之后需要的id条件: 自增(最好),唯一
+往一个库的一个表里插入一条没什么业务含义的数据,获取一个数据库自增的一个 id,拿到这个 id 之后再往对应的分库分表里去写入。缺点是高并发存在瓶颈;优点是简单方便;适用于并发不高,但是数据量太大导致的分库分表扩容,可能每秒最高并发最多就几百,那么就走单独的一个库和表生成自增主键即可。
+基于对数据库自增的改进,原理就是利用redis的incr命令实现ID的原子性自增。不依赖于数据库,灵活方便,且性能优于数据库,但是如果系统中没有Redis,还需要引入新的组件,会增加系统复杂度。
+UUID一般不会作为主键使用,因为UUID 太长了、占用空间大,作为主键性能太差了,且UUID 不具有有序性,会导致 B+ 树索引在写的时候有过多的随机写操作,总之就是无序的性能开销大;适合于随机生成个什么文件名、编号之类的。
+一般如果用这个方案,是将当前时间跟很多其他的业务字段拼接起来,作为一个 id,如果业务上你觉得可以接受,那么也是可以的。你可以将别的业务字段值跟当前时间拼接起来,组成一个全局唯一的编号。
+算法思路是是把一个 64 位的 long 型的 id,1 个 bit 是不用的,用其中的 41 bits 作为毫秒数,用 10 bits 作为工作机器 id,12 bits 作为序列号。
+雪花算法相对来说还是比较靠谱的,毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的,能达到百万计QPS。但是雪花算法强依赖时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。
+为了规避雪花算法的缺点,一些国内的大厂做了改进,像美团的Leaf,百度的uid-generator,都是基于雪花算法来实现的。
+所以你要真是搞分布式 id 生成,如果是高并发啥的,那么用这个应该性能比较好,一般每秒几万并发的场景,也足够用了。
++ +
+ + + + + +线程状态共包含6种,6中状态又可以互相的转换。
+ +start()
方法。该状态的线程位于可运行线程池中,等待被线程调度选中并分配cpu使用权 。++补充: +睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。
++
+- 调用
+Thread.sleep()
方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。- 调用
+Object.wait()
方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。- 阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁,而等待是主动的,通过调用
+Thread.sleep()
和Object.wait()
等方法进入等待。
在Java中,创建一个线程,有且仅有一种方式: 创建一个Thread
类实例,并调用它的start
方法。
通过继承Thread
类,重写run()
方法来创建线程。
public class MainTest {
+ public static void main(String[] args) {
+ ThreadDemo thread1 = new ThreadDemo();
+ thread1.start();
+ }
+}
+class ThreadDemo extends Thread {
+ @Override
+ public void run() {
+ System.out.printf("通过继承Thread类的方式创建线程,线程 %s 启动",Thread.currentThread().getName());
+ }
+}
+
实现 Runnale
接口,将它作为 target
参数传递给 Thread
类构造函数的方式创建线程。
public class MainTest {
+ public static void main(String[] args) {
+ new Thread(() -> {
+ System.out.printf("通过实现Runnable接口的方式,重写run方法创建线程;线程 %s 启动",Thread.currentThread().getName());
+ }).start();
+ }
+}
+
通过实现 Callable
接口,来创建一个带有返回值的线程。
在Callable执行完之前的这段时间,主线程可以先去做一些其他的事情,事情都做完之后,再获取Callable的返回结果。可以通过isDone()来判断子线程是否执行完。
+public class MainTest {
+ public static void main(String[] args) throws ExecutionException, InterruptedException {
+ FutureTask<String> futureTask = new FutureTask<>(() -> {
+ System.out.printf("通过实现Callable接口的方式,重写call方法创建线程;线程 %s 启动", Thread.currentThread().getName());
+ System.out.println();
+ Thread.sleep(10000);
+ return "我是call方法返回值";
+ });
+ new Thread(futureTask).start();
+
+ System.out.println("主线程工作中 ...");
+ String callRet = null;
+ while (callRet == null){
+ if(futureTask.isDone()){
+ callRet = futureTask.get();
+ }
+ System.out.println("主线程继续工作 ...");
+ }
+ System.out.println("获取call方法返回值:"+ callRet);
+ }
+}
+
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。
+它的主要特点为:线程复用,控制最大并发数,管理线程。
+优点:
+通过Executors
线程池工具类来使用:
Executors.newSingleThreadExecutor()
:创建只有一个线程的线程池Executors.newFixedThreadPool(int)
:创建固定线程的线程池Executors.newCachedThreadPool()
:创建一个可缓存的线程池,线程数量随着处理业务数量变化这三种常用创建线程池的方式,底层代码都是用ThreadPoolExecutor
创建的。
Executors.newSingleThreadExecutor()
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。newSingleThreadExecutor
将 corePoolSize
和 maximumPoolSize
都设置为1,它使用的 LinkedBlockingQueue
。源代码
+ public static ExecutorService newSingleThreadExecutor() {
+ return new FinalizableDelegatedExecutorService
+ (new ThreadPoolExecutor(1, 1,
+ 0L, TimeUnit.MILLISECONDS,
+ new LinkedBlockingQueue<Runnable>()));
+ }
+
代码演示
+public class MainTest {
+ public static void main(String[] args) {
+ ExecutorService executor1 = null;
+ try {
+ executor1 = Executors.newSingleThreadExecutor();
+ for (int i = 1; i <= 10; i++) {
+ executor1.execute(() -> {
+ System.out.println(Thread.currentThread().getName() + "执行了");
+ });
+ }
+ } finally {
+ executor1.shutdown();
+ }
+ }
+}
+
Executors.newFixedThreadPool(int)
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待newFixedThreadPool
创建的线程池 corePoolSize
和 maximumPoolSize
值是相等的,它使用的 LinkedBlockingQueue
。源代码
+ public static ExecutorService newFixedThreadPool(int nThreads) {
+ return new ThreadPoolExecutor(nThreads, nThreads,
+ 0L, TimeUnit.MILLISECONDS,
+ new LinkedBlockingQueue<Runnable>());
+ }
+
代码演示
+public class MainTest {
+ public static void main(String[] args) {
+ ExecutorService executor1 = null;
+ try {
+ executor1 = Executors.newFixedThreadPool(10);
+ for (int i = 1; i <= 10; i++) {
+ executor1.execute(() -> {
+ System.out.println(Thread.currentThread().getName() + "执行了");
+ });
+ }
+ } finally {
+ executor1.shutdown();
+ }
+ }
+}
+
Executors.newCachedThreadPool()
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。newCachedThreadPool
将 corePoolSize
设置为0,将 maximumPoolSize
设置为 Integer.MAX_VALUE
,使用的 SynchronousQueue
,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。源代码
+ public static ExecutorService newCachedThreadPool() {
+ return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
+ 60L, TimeUnit.SECONDS,
+ new SynchronousQueue<Runnable>());
+ }
+
代码演示
+public class MainTest {
+ public static void main(String[] args) {
+ ExecutorService executor1 = null;
+ try {
+ executor1 = Executors.newCachedThreadPool();
+ for (int i = 1; i <= 10; i++) {
+ executor1.execute(() -> {
+ System.out.println(Thread.currentThread().getName() + "执行了");
+ });
+ }
+ } finally {
+ executor1.shutdown();
+ }
+ }
+}
+
public ThreadPoolExecutor(int corePoolSize,
+ int maximumPoolSize,
+ long keepAliveTime,
+ TimeUnit unit,
+ BlockingQueue<Runnable> workQueue,
+ ThreadFactory threadFactory,
+ RejectedExecutionHandler handler) {
+ // ...
+ }
+
corePoolSize
: 线程池中的常驻核心线程数,可理解为初始化线程数maximumPoolSize
:线程池能够容纳同时执行的最大线程数,此值必须大于等于1threadFactory
:线程工厂;表示生成线程池中工作线程的线程工厂,用于创建线程,一般用默认的即可workQueue
:任务队列;随着业务量的增多,线程开始慢慢处理不过来,这时候需要放到任务队列中去等待线程处理rejectedExecutionHandler
:拒绝策略;如果业务越来越多,线程池首先会扩容,扩容后发现还是处理不过来,任务队列已经满了,这时候拒绝接收新的请求keepAliveTime
:多余的空闲线程的存活时间;如果线程池扩容后,能处理过来,而且数据量并没有那么大,用最初的线程数量就能处理过来,剩下的线程被叫做空闲线程unit
:多余的空闲线程的存活时间的单位在创建了线程池后,等待提交过来的任务请求;
+当调用execute
方法添加一个请求任务时,线程池会做如下判断:
corePoolSize
,那么马上创建线程运行该任务corePoolSize
,那么该任务会被放入任务队列maximumPoolSize
,那么要创建非核心线程立刻运行这个任务(扩容)maximumPoolSize
,那么线程池会启动饱和拒绝策略来执行corePoolSize
,那么这个线程就被停掉,所以线程池的所有任务完成后它最终会收缩到 corePoolSize
的大小在线程池中,如果任务队列满了并且正在运行的线程个数大于等于允许运行的最大线程数,那么线程池会启动拒绝策略来执行,具体分为下列四种:
+AbortPolicy
: 默认拒绝策略;直接抛出java.util.concurrent.RejectedExecutionException
异常,阻止系统的正常运行;CallerRunsPolicy
:调用这运行,一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量;DiscardOldestPolicy
:抛弃队列中等待最久的任务,然后把当前任务加入到队列中;DiscardPolicy
:直接丢弃任务,不给予任何处理也不会抛出异常;如果允许任务丢失,这是一种最好的解决方案;在实际开发中用哪个线程池?
+上面的三种一个都不用,我们生产上只能使用自定义的。
+Executors
中JDK已经给你提供了,为什么不用?
以下内容摘自《阿里巴巴开发手册》
+++【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。 +说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。 如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。 +【强制】线程池不允许使用
+Executors
去创建,而是通过ThreadPoolExecutor
的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
++说明:
+Executors
返回的线程池对象的弊端如下: +1)FixedThreadPool
和SingleThreadPool
: 允许的请求队列长度为Integer.MAX_VALUE
,可能会堆积大量的请求,从而导致 OOM。 +2)CachedThreadPool
: 允许的创建线程数量为Integer.MAX_VALUE
,可能会创建大量的线程,从而导致 OOM。
自定义线程池代码演示
+public class MainTest {
+ public static void main(String[] args) {
+ ExecutorService executor1 = null;
+ try {
+ executor1 = new ThreadPoolExecutor(
+ 2,
+ 5,
+ 1L,
+ TimeUnit.SECONDS,
+ new LinkedBlockingQueue<>(3),
+ Executors.defaultThreadFactory(),
+ new ThreadPoolExecutor.CallerRunsPolicy());
+ for (int i = 1; i <= 20; i++) {
+ executor1.execute(() -> {
+ System.out.println(Thread.currentThread().getName() + "执行了");
+ });
+ }
+ } finally {
+ executor1.shutdown();
+ }
+ }
+}
+
合理配置线程池参数,可以分为以下两种情况
+CPU密集型:CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行;
+CPU密集型任务配置尽可能少的线程数量:参考公式:(CPU核数+1)
IO密集型:即该任务需要大量的IO,即大量的阻塞;
+在IO密集型任务中使用多线程可以大大的加速程序运行,故需要多配置线程数:参考公式:CPU核数/ (1-阻塞系数) 阻塞系数在0.8~0.9之间
代码演示
+public class MainTest {
+ public static void main(String[] args) {
+ ExecutorService executor1 = null;
+ try {
+ // 获取cpu核心数
+ int coreNum = Runtime.getRuntime().availableProcessors();
+ /*
+ * 1. IO密集型: CPU核数/ (1-阻塞系数) 阻塞系数在0.8~0.9之间
+ * 2. CPU密集型: CPU核数+1
+ */
+// int maximumPoolSize = coreNum + 1;
+ int maximumPoolSize = (int) (coreNum / (1 - 0.9));
+ System.out.println("当前线程池最大允许存放:" + maximumPoolSize + "个线程");
+ executor1 = new ThreadPoolExecutor(
+ 2,
+ maximumPoolSize,
+ 1L,
+ TimeUnit.SECONDS,
+ new LinkedBlockingQueue<>(3),
+ Executors.defaultThreadFactory(),
+ new ThreadPoolExecutor.CallerRunsPolicy());
+ for (int i = 1; i <= 20; i++) {
+ executor1.execute(() -> {
+ System.out.println(Thread.currentThread().getName() + "执行了");
+ });
+ }
+ } finally {
+ executor1.shutdown();
+ }
+ }
+}
+
+ +
+ + + + + +fastdfs-client-java-1.27.jar:点击下载
+ <dependencies>
+
+ <!-- fastdfs -->
+ <dependency>
+ <groupId>org.csource</groupId>
+ <artifactId>fastdfs-client-java</artifactId>
+ <version>1.27</version>
+ <systemPath>${project.basedir}/lib/fastdfs-client-java-1.27.jar</systemPath>
+ <scope>system</scope>
+ </dependency>
+
+ <!--aliyun oss 依赖-->
+ <dependency>
+ <groupId>com.aliyun.oss</groupId>
+ <artifactId>aliyun-sdk-oss</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-web</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-test</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.projectlombok</groupId>
+ <artifactId>lombok</artifactId>
+ <optional>true</optional>
+ </dependency>
+
+ <dependency>
+ <groupId>cn.hutool</groupId>
+ <artifactId>hutool-all</artifactId>
+ <version>5.8.11</version>
+ </dependency>
+
+ <dependency>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ <version>2.11.0</version>
+ </dependency>
+
+ </dependencies>
+
server:
+ port: 80
+
public interface FileManagement {
+
+
+ /**
+ * 设置下一个bean的对象
+ *
+ * @param nextFileManagement 下一个bean对象
+ */
+ void setNext(FileManagement nextFileManagement);
+
+}
+
@Component
+public class FileBeanManagement {
+
+
+ @Bean(name = "uploadChain")
+ public AbstractUploadManagement uploadChain() {
+ return buildChain(AbstractUploadManagement.class, true);
+ }
+
+
+ @Bean(name = "downloadChain")
+ public AbstractDownloadManagement downloadChain() {
+ return buildChain(AbstractDownloadManagement.class, true);
+ }
+
+
+ /**
+ * 设置文件管理bean调用链(环状链)
+ *
+ * @param type 文件管理类型
+ * @param isRing 是否形成环形链表
+ * @param <T> extends FileManagement 指定类型
+ * @return bean
+ */
+ private <T extends FileManagement> T buildChain(Class<T> type, boolean isRing) {
+ List<T> beans = new ArrayList<>(SpringUtil.getBeansOfType(type).values());
+
+ // 设置调用链
+ for (int index = 0; index < beans.size(); index++) {
+ // 判断是否到最后一个bean 如果到了最后一个bean将第一个bean设置为尾部的下一个节点 形成环状结构
+ beans.get(index).setNext(index + 1 >= beans.size() && isRing ? beans.get(0) : beans.get(index + 1));
+ }
+ return beans.get(0);
+ }
+
+
+}
+
@Slf4j
+public abstract class AbstractUploadManagement implements FileManagement {
+
+ protected AbstractUploadManagement self;
+
+ @Override
+ public void setNext(FileManagement fileManagement) {
+ this.self = (AbstractUploadManagement)fileManagement;
+ }
+
+ protected List<String> allowUploadFiles(ArchetypeFileConfig fileConfig) {
+ List<String> allowFiles = fileConfig.getAllowFiles();
+ return allowFiles.isEmpty() ? Arrays.asList(FileConstant.DEFAULT_ALLOWED_EXTENSION) : allowFiles;
+ }
+
+ /**
+ * 判断是否是当前平台处理
+ *
+ * @param platformType 指定平台类型 {@linkplain ArchetypeFilePlatformType}
+ * @return true 为当前平台处理
+ */
+ protected abstract boolean isCurrentPlatform(ArchetypeFilePlatformType platformType);
+
+ /**
+ * 校验允许上传的文件格式; 默认{@code FileTypeUtils.DEFAULT_ALLOWED_EXTENSION}
+ *
+ * @param fileName 文件名称
+ * @return true 允许上传
+ */
+ private boolean checkAllowUploadFiles(String fileName,ArchetypeFileConfig fileConfig) {
+ if (fileName == null) {
+ return false;
+ }
+ List<String> allowUploadFiles = allowUploadFiles(fileConfig);
+ for (String allowUploadFile : allowUploadFiles) {
+ if (fileName.toLowerCase().endsWith(allowUploadFile)) {
+ return true;
+ }
+ }
+ log.info("当前上传文件仅支持 [{}] 格式", Arrays.toString(allowUploadFiles.toArray()));
+ return false;
+ }
+
+ /**
+ * 校验允许上传的文件大小
+ *
+ * @param bytes 字节数量
+ * @return true 允许上传
+ */
+ protected boolean checkSingleAllowUploadSize(byte[] bytes,ArchetypeFileConfig fileConfig){
+ return bytes.length <= fileConfig.getAllowFileSize();
+ }
+
+ /**
+ * 前置处理 预留方法
+ *
+ * @param multipartFile 上传文件对象
+ * @return true 允许上传
+ */
+ protected abstract boolean preProcessing(MultipartFile multipartFile,ArchetypeFileConfig fileConfig);
+
+ /**
+ * 上传文件
+ *
+ * @param multipartFile 上传文件对象
+ * @return 上传成功后的路径
+ */
+ protected abstract String upload(MultipartFile multipartFile,ArchetypeFileConfig fileConfig);
+
+ /**
+ * 后置处理 预留方法
+ *
+ * @param fileUri 上传成功后的路径
+ * @return 文件上传路径
+ */
+ protected abstract String postProcessing(String fileUri);
+
+
+ /**
+ * 多上传文件模板方法
+ *
+ * @param fileConfig 文件上传配置
+ * @param multipartFile 上传文件对象
+ * @return 上传成功后的文件uri集合
+ */
+ @SneakyThrows
+ public final List<String> uploadTemplate(MultipartFile[] multipartFile, ArchetypeFileConfig fileConfig) {
+ if (multipartFile == null || multipartFile.length == 0) {
+ return Collections.emptyList();
+ }
+ ArchetypeFilePlatformType filePlatformType = fileConfig.getFilePlatformType();
+
+ // 检索上传文件平台类型
+ if (!self.isCurrentPlatform(filePlatformType)) {
+ return self.uploadTemplate(multipartFile,fileConfig);
+ }
+
+ // 多文件上传需要加循环
+ List<String> fileUriList = new ArrayList<>(multipartFile.length);
+ for (MultipartFile file : multipartFile) {
+ if (!self.beforeCheck(file,fileConfig)) {
+ break;
+ }
+ fileUriList.add(self.postProcessing(self.upload(file,fileConfig)));
+ }
+ log.info("上传文件=> 平台类型:[{}]\t 上传路径:[{}]", filePlatformType, Arrays.toString(fileUriList.toArray()));
+ return fileUriList;
+ }
+
+
+ /**
+ * 上传文件前置校验
+ *
+ * @param multipartFile 上传的文件
+ * @return true校验成功允许上传
+ */
+ @SneakyThrows
+ private boolean beforeCheck(MultipartFile multipartFile,ArchetypeFileConfig fileConfig) {
+ return !multipartFile.isEmpty()
+ && (multipartFile.getSize() != 0)
+ && self.checkAllowUploadFiles(multipartFile.getOriginalFilename(),fileConfig)
+ && self.checkSingleAllowUploadSize(multipartFile.getBytes(),fileConfig)
+ && self.preProcessing(multipartFile,fileConfig)
+ ;
+ }
+
+}
+
@Slf4j
+public abstract class AbstractDownloadManagement implements FileManagement {
+
+ private AbstractDownloadManagement self;
+
+ @Override
+ public void setNext(FileManagement fileManagement) {
+ this.self = (AbstractDownloadManagement)fileManagement;
+ }
+
+ /**
+ * 前置处理
+ *
+ * @param uri 下载文件的地址
+ * @return true-允许执行后置操作
+ */
+ protected abstract boolean preProcessing(String uri);
+
+ /**
+ * 下载文件
+ *
+ * @param uri 下载文件地址
+ * @return 下载到的文件
+ */
+ protected abstract File download(String uri);
+
+ /**
+ * 后置处理
+ *
+ * @param downloadFile 下载的文件
+ * @return 经过后置处理下载后的文件
+ */
+ protected abstract File postProcessing(File downloadFile);
+
+ /**
+ * 判断是否是当前平台处理
+ *
+ * @param platformType 指定平台类型 {@linkplain ArchetypeFilePlatformType}
+ * @return true 为当前平台处理
+ */
+ protected abstract boolean isCurrentPlatform(ArchetypeFilePlatformType platformType);
+
+
+ /**
+ * 文件下载模板方法
+ *
+ * @param platformType 平台类型
+ * @param uri 文件地址,支持本地和http地址
+ * @return file对象
+ */
+ public final File downloadTemplate(ArchetypeFilePlatformType platformType, String uri) {
+ // 检索上传文件平台类型
+ if (!self.isCurrentPlatform(platformType)) {
+ return self.downloadTemplate(platformType, uri);
+ }
+
+ // 前置下载校验
+ if (!self.preProcessing(uri)) {
+ return new File(CharSequenceUtil.EMPTY);
+ }
+ // 下载文件到指定路径
+ return self.postProcessing(self.download(uri));
+ }
+
+
+ @SneakyThrows
+ protected File download(ArchetypeFilePlatformType platformType, String uri, String downloadPath, InputStream inputStream, long fileLength) {
+ log.info("下载文件平台类型: {} 文件大小为:{} MB", platformType, new DecimalFormat("0.00").format(fileLength / (float) (1024 * 1024)));
+ // 获取文件名称
+ String[] split = uri.split(platformType.getFileSeparator());
+ String fullFileName = split[split.length - 1];
+ String[] fileNameWithSuffix = fullFileName.split("\\.");
+
+ // 指定存放位置(有需求可以自定义)
+ String path = String.format("%s%s%s_%s.%s", downloadPath, File.separatorChar, fileNameWithSuffix[0], DateUtil.format(new Date(), "yyyyMMddHHmmss"), fileNameWithSuffix[1]);
+ File file = new File(path);
+ // 校验文件夹目录是否存在,不存在就创建一个目录
+ if (!file.getParentFile().exists()) {
+ file.getParentFile().mkdirs();
+ }
+
+ // 写入文件
+ @Cleanup OutputStream out = Files.newOutputStream(file.toPath());
+ @Cleanup BufferedInputStream bin = new BufferedInputStream(inputStream);
+ int size = 0;
+ int len = 0;
+ byte[] buf = new byte[2048];
+ while ((size = bin.read(buf)) != -1) {
+ len += size;
+ out.write(buf, 0, size);
+ if (log.isDebugEnabled()) {
+ log.debug("下载文件 {} 进度 =>{}%", fullFileName, len * 100L / fileLength);
+ }
+ }
+ log.info("{} 文件下载成功!", path);
+ return file;
+ }
+
+
+
+
+ @SneakyThrows
+ protected File defaultHttpDownload(ArchetypeFilePlatformType platformType, String uri, String downloadPath) {
+ // 建立 http 下载连接 建立链接从请求中获取数据
+ HttpURLConnection httpUrlConnection = (HttpURLConnection) new URL(uri).openConnection();
+ httpUrlConnection.setConnectTimeout(10 * 1000);
+ httpUrlConnection.setRequestMethod("GET");
+ httpUrlConnection.setRequestProperty("Charset", "UTF-8");
+ httpUrlConnection.connect();
+ return download(platformType, uri, downloadPath, httpUrlConnection.getInputStream(), httpUrlConnection.getContentLength());
+ }
+
+
+}
+
public interface ArchetypeFileConfig {
+
+
+ /**
+ * 文件平台类型
+ */
+ ArchetypeFilePlatformType getFilePlatformType();
+
+ /**
+ * 允许上传的文件类型
+ */
+ List<String> getAllowFiles();
+
+ /**
+ * 允许上传的文件大小; 单位字节
+ */
+ Long getAllowFileSize();
+
+
+}
+
@AllArgsConstructor
+public enum ArchetypeFilePlatformType {
+
+ // 上传下载文件平台类型
+ LOCAL("\\\\"),
+ ALIYUN("/"),
+ TENCEN_CLOUD("/"),
+ FAST_DFS("/")
+
+ ;
+
+ /**
+ * 下载文件分隔符
+ */
+ @Getter
+ private String fileSeparator;
+
+
+
+}
+
public interface FileConstant {
+
+ String[] IMAGE_EXTENSION = {"bmp", "gif", "jpg", "jpeg", "png"};
+
+ String[] FLASH_EXTENSION = {"swf", "flv"};
+
+ String[] MEDIA_EXTENSION = {"swf", "flv", "mp3", "wav", "wma", "wmv", "mid", "avi", "mpg",
+ "asf", "rm", "rmvb"};
+
+ String[] VIDEO_EXTENSION = {"mp4", "avi", "rmvb"};
+
+ String[] DEFAULT_ALLOWED_EXTENSION = {
+ // 图片
+ "bmp", "gif", "jpg", "jpeg", "png",
+ // word excel powerpoint
+ "doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt",
+ // 压缩文件
+ "rar", "zip", "gz", "bz2",
+ // 视频格式
+ "mp4", "avi", "rmvb",
+ // pdf
+ "pdf"};
+
+}
+
public interface FileFacade {
+
+
+ /**
+ * 上传文件
+ *
+ * @param fileConfig 文件上传配置
+ * @param multipartFile 上传的文件
+ * @return 上传后的文件地址
+ */
+ default List<String> upload(MultipartFile[] multipartFile, ArchetypeFileConfig fileConfig) {
+ return upload(multipartFile, fileConfig, new UploadFileExtensions() {
+ @Override
+ public boolean preProcessing(MultipartFile[] multipartFile) {
+ return true;
+ }
+
+ @Override
+ public List<String> postProcessing(MultipartFile[] multipartFile, List<String> uploadUriList) {
+ return uploadUriList;
+ }
+ });
+ }
+
+
+ /**
+ * 上传文件
+ *
+ * @param fileConfig 文件上传配置
+ * @param multipartFile 上传的文件
+ * @return 上传后的文件地址
+ */
+ default List<String> upload(ArchetypeFileConfig fileConfig, MultipartFile[] multipartFile) {
+ return upload(multipartFile, fileConfig);
+ }
+
+
+ /**
+ * 上传文件
+ *
+ * @param fileConfig 文件上传配置
+ * @param multipartFile 上传的文件
+ * @param fileExtensions 文件扩展接口; 可在文件上传之前,上传之后进行操作
+ * @return 上传后的文件地址
+ */
+ List<String> upload(MultipartFile[] multipartFile, ArchetypeFileConfig fileConfig, UploadFileExtensions fileExtensions);
+
+
+ /**
+ * 下载文件
+ *
+ * @param platformType 指定平台类型{@linkplain ArchetypeFilePlatformType}
+ * @param uri 文件地址; 支持本地,http
+ * @return 下载的文件对象
+ */
+ default File download(@NotNull ArchetypeFilePlatformType platformType, @NotNull String uri) {
+ return download(platformType, uri, new DownloadFileExtensions() {
+ @Override
+ public boolean preProcessing(String uri) {
+ return true;
+ }
+
+ @Override
+ public File postProcessing(String uri, File downloadFile) {
+ return downloadFile;
+ }
+ });
+ }
+
+
+ /**
+ * 下载文件
+ *
+ * @param platformType 指定平台类型{@linkplain ArchetypeFilePlatformType}
+ * @param uri 文件地址; 支持本地,http
+ * @param fileExtensions 下载文件扩展接口;可在下载上传之前,下载之后进行操作
+ * @return 下载的文件对象
+ */
+ File download(ArchetypeFilePlatformType platformType, String uri, DownloadFileExtensions fileExtensions);
+
+}
+
@Component
+public class FileManagementBridge implements FileFacade {
+
+
+ @Resource(name = "uploadChain")
+ private AbstractUploadManagement uploadChain;
+
+
+ @Resource(name = "downloadChain")
+ private AbstractDownloadManagement downloadChain;
+
+
+ @Override
+ public List<String> upload(MultipartFile[] multipartFile, ArchetypeFileConfig fileConfig, UploadFileExtensions fileExtensions) {
+ if (ObjectUtil.isAllEmpty(fileConfig.getFilePlatformType(), multipartFile)) {
+ throw new IllegalArgumentException(String.format("upload file need param (platformType:[%s] multipartFile:[%s] maybe is empty.", fileConfig.getFilePlatformType(), Arrays.toString(multipartFile)));
+ }
+ if (!fileExtensions.preProcessing(multipartFile)) {
+ return Collections.emptyList();
+ }
+ return fileExtensions.postProcessing(multipartFile, uploadChain.uploadTemplate(multipartFile, fileConfig));
+ }
+
+
+ @Override
+ public File download(ArchetypeFilePlatformType platformType, String uri, DownloadFileExtensions fileExtensions) {
+ if (ObjectUtil.isAllEmpty(platformType, uri)) {
+ throw new IllegalArgumentException(String.format("download file need param ( platformType:[%s] uri:[%s] ) maybe is empty.", platformType, uri));
+ }
+ if (!fileExtensions.preProcessing(uri)) {
+ return new File(CharSequenceUtil.EMPTY);
+ }
+ return fileExtensions.postProcessing(uri, downloadChain.downloadTemplate(platformType, uri));
+ }
+
+
+}
+
public interface DownloadFileExtensions {
+
+
+ /**
+ * 下载文件-前置处理
+ *
+ * @param uri 下载的uri
+ * @return true-允许下载;false-不允许下载
+ */
+ boolean preProcessing(String uri);
+
+
+ /**
+ * 下载文件-后置处理
+ *
+ * @param uri 下载的uri
+ * @param downloadFile 下载的文件
+ * @return 经过后置处理的下载的文件
+ */
+ File postProcessing(String uri, File downloadFile);
+
+}
+
public interface UploadFileExtensions {
+
+
+ /**
+ * 上传文件-前置处理
+ *
+ * @param multipartFile 上传的文件对象
+ * @return true-能继续处理
+ */
+ boolean preProcessing(MultipartFile[] multipartFile);
+
+
+ /**
+ * 上传文件-后置处理
+ *
+ * @param uploadUriList 上传文件返回的uri地址集合
+ * @return 经过后置处理返回的uri地址集合
+ */
+ List<String> postProcessing(MultipartFile[] multipartFile,List<String> uploadUriList);
+
+}
+
@Slf4j
+@Data
+@Configuration(value = "aliyunFileConfig")
+@Accessors(chain = true)
+public class AliyunFileConfig implements ArchetypeFileConfig {
+
+
+ /**
+ * 填写Bucket所在地域对应的Endpoint,可在创建好的Bucket概况页查看
+ */
+ private String endpoint;
+
+ /**
+ * 阿里云账号AccessKey里所对应的AccessKey ID
+ */
+ private String accessKeyId;
+
+ /**
+ * 阿里云账号AccessKey里所对应的AccessKey Secret
+ */
+ private String accessKeySecret;
+
+ /**
+ * OSS对象存储空间名
+ */
+ private String bucketName;
+ /**
+ * 允许上传的文件类型
+ */
+ private List<String> allowFiles;
+
+ /**
+ * 允许上传的文件大小; 单位字节
+ */
+ private Long allowFileSize;
+
+
+ @Override
+ public ArchetypeFilePlatformType getFilePlatformType() {
+ return ArchetypeFilePlatformType.ALIYUN;
+ }
+}
+
@Component("archetypeAliyunDownload")
+@RequiredArgsConstructor
+public class ArchetypeAliyunDownload extends AbstractDownloadManagement {
+
+
+ @Override
+ protected boolean preProcessing(String uri) {
+ return false;
+ }
+
+ @Override
+ protected File download(String uri) {
+ return null;
+ }
+
+ @Override
+ protected File postProcessing(File downloadFile) {
+ return null;
+ }
+
+ @Override
+ protected boolean isCurrentPlatform(ArchetypeFilePlatformType platformType) {
+ return false;
+ }
+}
+
@Component("archetypeAliyunUpload")
+@RequiredArgsConstructor
+public class ArchetypeAliyunUpload extends AbstractUploadManagement {
+
+
+ @Override
+ protected boolean isCurrentPlatform(ArchetypeFilePlatformType platformType) {
+ return ArchetypeFilePlatformType.ALIYUN.equals(platformType);
+ }
+
+
+ @Override
+ protected boolean preProcessing(MultipartFile multipartFile, ArchetypeFileConfig fileConfig) {
+ return true;
+ }
+
+
+ @SneakyThrows
+ @Override
+ protected String upload(MultipartFile multipartFile,ArchetypeFileConfig fileConfig) {
+ AliyunFileConfig aliyunFileConfig = ((AliyunFileConfig) fileConfig);
+ OSS ossClient = new OSSClientBuilder().build(
+ aliyunFileConfig.getEndpoint(),
+ aliyunFileConfig.getAccessKeyId(),
+ aliyunFileConfig.getAccessKeySecret()
+ );
+ // 上传文件流
+ try (InputStream inputStream = multipartFile.getInputStream()) {
+ String fileName = multipartFile.getOriginalFilename();
+ fileName = UUID.randomUUID().toString().replaceAll("-", "") + fileName;
+
+ // 按照当前日期,创建文件夹,上传到创建文件夹里面 20220315/xx.jpg
+ fileName = DateUtil.format(DateUtil.date(), "yyyyMMdd") + ArchetypeFilePlatformType.ALIYUN.getFileSeparator() + fileName;
+ ossClient.putObject(aliyunFileConfig.getBucketName(), fileName, inputStream);
+ ossClient.shutdown();
+
+ return String.format("https://%s.%s/%s", aliyunFileConfig.getBucketName(), aliyunFileConfig.getEndpoint(), fileName);
+ }
+ }
+
+
+ @Override
+ protected String postProcessing(String fileUri) {
+ return fileUri;
+ }
+}
+
@Data
+@Accessors(chain = true)
+@Configuration(value = "fastDfsConfig")
+public class FastDfsConfig implements ArchetypeFileConfig {
+
+
+ /**
+ * 编码
+ */
+ private String charset;
+ /**
+ * 连接超时事件
+ */
+ private String connectTimeoutInSecond;
+ /**
+ * 网络超时时间
+ */
+ private String networkTimeoutInSeconds;
+ /**
+ * track端口
+ */
+ private String httpTrackerHttpPort;
+ /**
+ * http令牌
+ */
+ private String httpAntiStealToken;
+ /**
+ * 服务器地址
+ */
+ private String trackerServers;
+
+ private String storageServiceIp;
+ private Integer storageServicePort;
+ private Integer storageServicePathIndex;
+ /**
+ * 允许上传的文件类型
+ */
+ private List<String> allowFiles;
+
+ /**
+ * 允许上传的文件大小; 单位字节
+ */
+ private Long allowFileSize;
+
+ @Override
+ public ArchetypeFilePlatformType getFilePlatformType() {
+ return ArchetypeFilePlatformType.FAST_DFS;
+ }
+
+}
+
@Component("archetypeFastDfsDownload")
+@DependsOn({"fastDfsConfig"})
+@RequiredArgsConstructor
+public class ArchetypeFastDfsDownload extends AbstractDownloadManagement {
+ @Override
+ protected boolean preProcessing(String uri) {
+ return false;
+ }
+
+ @Override
+ protected File download(String uri) {
+ return null;
+ }
+
+ @Override
+ protected File postProcessing(File downloadFile) {
+ return null;
+ }
+
+ @Override
+ protected boolean isCurrentPlatform(ArchetypeFilePlatformType platformType) {
+ return false;
+ }
+}
+
@Slf4j
+@Component("archetypeFastDfsUpload")
+@RequiredArgsConstructor
+public class ArchetypeFastDfsUpload extends AbstractUploadManagement {
+
+
+ @Override
+ protected boolean isCurrentPlatform(ArchetypeFilePlatformType platformType) {
+ return ArchetypeFilePlatformType.FAST_DFS.equals(platformType);
+ }
+
+
+ @Override
+ protected boolean preProcessing(MultipartFile multipartFile, ArchetypeFileConfig fileConfig) {
+ return true;
+ }
+
+
+ @SneakyThrows
+ @Override
+ protected String upload(MultipartFile multipartFile,ArchetypeFileConfig fileConfig) {
+ FastDfsConfig fastDfsConfig = (FastDfsConfig) fileConfig;
+ init(fastDfsConfig);
+
+ String originalFilename = multipartFile.getOriginalFilename();
+ String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")+1);
+
+ StorageClient storageClient = new StorageClient(null, new StorageServer(fastDfsConfig.getStorageServiceIp(), fastDfsConfig.getStorageServicePort(), fastDfsConfig.getStorageServicePathIndex()));
+ String[] strings = storageClient.upload_file(multipartFile.getBytes(), suffix, null);
+ return String.format("http://%s:%s/%s",fastDfsConfig.getStorageServiceIp(),fastDfsConfig.getHttpTrackerHttpPort(),StrUtil.join("/",strings));
+ }
+
+ @SneakyThrows
+ private void init(FastDfsConfig fastDfsConfig){
+ // 先从容器获取如果没有在加载 fixme
+
+ // 文件服务器客户端初始化
+ Properties properties = new Properties();
+ properties.setProperty("fastdfs.connect_timeout_in_seconds", fastDfsConfig.getConnectTimeoutInSecond());
+ properties.setProperty("fastdfs.network_timeout_in_seconds", fastDfsConfig.getNetworkTimeoutInSeconds());
+ properties.setProperty("fastdfs.charset", fastDfsConfig.getCharset());
+ properties.setProperty("fastdfs.http_tracker_http_port", fastDfsConfig.getHttpTrackerHttpPort());
+ properties.setProperty("fastdfs.http_anti_steal_token", fastDfsConfig.getHttpAntiStealToken());
+ properties.setProperty("fastdfs.tracker_servers", fastDfsConfig.getTrackerServers());
+ ClientGlobal.initByProperties(properties);
+ log.info("fastdfs客户端初始化成功");
+ }
+
+
+
+ @Override
+ protected String postProcessing(String fileUri) {
+ return fileUri;
+ }
+
+}
+
@Data
+@Configuration(value = "localFileConfig")
+@Accessors(chain = true)
+public class LocalFileConfig implements ArchetypeFileConfig {
+
+ /**
+ * 允许上传的文件类型
+ */
+ private List<String> allowFiles;
+
+ /**
+ * 允许上传的文件大小; 单位字节
+ */
+ private Long allowFileSize;
+
+ /**
+ * 上传的路径
+ */
+ private String uploadPath;
+
+ /**
+ * 下载文件到指定路径
+ */
+ private String downloadPath;
+
+ @Override
+ public ArchetypeFilePlatformType getFilePlatformType() {
+ return ArchetypeFilePlatformType.LOCAL;
+ }
+
+
+}
+
@DependsOn({"localFileConfig"})
+@Component("archetypeLocalDownload")
+@RequiredArgsConstructor
+public class ArchetypeLocalDownload extends AbstractDownloadManagement {
+
+ private final LocalFileConfig localFileConfig;
+
+ @Override
+ protected boolean preProcessing(String uri) {
+ return true;
+ }
+
+ @SneakyThrows
+ @Override
+ protected File download(String uri) {
+ return download(ArchetypeFilePlatformType.LOCAL, uri, localFileConfig.getDownloadPath(), Files.newInputStream(Paths.get(uri)), new File(uri).length());
+ }
+
+ @Override
+ protected File postProcessing(File downloadFile) {
+ return downloadFile;
+ }
+
+ @Override
+ protected boolean isCurrentPlatform(ArchetypeFilePlatformType platformType) {
+ return ArchetypeFilePlatformType.LOCAL.equals(platformType);
+ }
+
+}
+
@Component("archetypeLocalUpload")
+@RequiredArgsConstructor
+public class ArchetypeLocalUpload extends AbstractUploadManagement {
+
+
+ @Override
+ protected boolean isCurrentPlatform(ArchetypeFilePlatformType platformType) {
+ return ArchetypeFilePlatformType.LOCAL.equals(platformType);
+ }
+
+
+ @Override
+ protected boolean preProcessing(MultipartFile multipartFile,ArchetypeFileConfig fileConfig) {
+ return true;
+ }
+
+
+ @SneakyThrows
+ @Override
+ protected String upload(MultipartFile file, ArchetypeFileConfig fileConfig) {
+ LocalFileConfig localFileConfig = (LocalFileConfig) fileConfig;
+ String fileName = extractFilename(file);
+ String absPath = getAbsoluteFile(localFileConfig.getUploadPath(), fileName).getAbsolutePath();
+ file.transferTo(Paths.get(absPath));
+ return String.format("%s/%s", localFileConfig.getUploadPath(), fileName);
+ }
+
+
+ /**
+ * 编码文件名
+ */
+ private String extractFilename(MultipartFile file) {
+ return CharSequenceUtil.format("{}/{}_{}.{}", DateUtil.format(new Date(), "yyyyMMdd"),
+ FilenameUtils.getBaseName(file.getOriginalFilename()), IdUtil.getSnowflakeNextIdStr(), FilenameUtils.getExtension(file.getOriginalFilename()));
+ }
+
+
+ private File getAbsoluteFile(String uploadDir, String fileName) {
+ File desc = new File(uploadDir + File.separator + fileName);
+ if (!desc.exists() && !desc.getParentFile().exists()) {
+ desc.getParentFile().mkdirs();
+ }
+ return desc;
+ }
+
+
+ @Override
+ protected String postProcessing(String fileUri) {
+ return fileUri;
+ }
+
+}
+
@Slf4j
+@RunWith(SpringRunner.class)
+@SpringBootTest(classes = FileApplication.class)
+public class FileTest {
+
+
+ @Autowired
+ private FileFacade fileFacade;
+
+
+ /**
+ * aliyun:
+ * allowFiles: [ ".png",".txt" ]
+ * allowFileSize: 10240
+ * endpoint: oss-cn-shanghai.aliyuncs.com
+ * accessKeyId:
+ * accessKeySecret:
+ * bucketName: shanghai-bucketnametest
+ */
+ ArchetypeFileConfig aliyunFileConfig = new AliyunFileConfig()
+ .setAllowFiles(Arrays.asList(".png", ".txt"))
+ .setAllowFileSize(10240L)
+ .setEndpoint("oss-cn-shanghai.aliyuncs.com")
+ .setAccessKeyId("")
+ .setAccessKeySecret("")
+ .setBucketName("shanghai-bucketnametest");
+
+ /**
+ * local:
+ * allowFiles: [".png",".txt"]
+ * allowFileSize: 10240
+ * uploadPath: "C:/Users/Administrator/Downloads"
+ * downloadPath: "C:/Users/Administrator/Downloads"
+ */
+ ArchetypeFileConfig localFileConfig = new LocalFileConfig()
+ .setAllowFiles(Arrays.asList(".png", ".txt"))
+ .setAllowFileSize(10240L)
+ .setUploadPath("C:\\Users\\Administrator\\Downloads")
+ .setDownloadPath("C:\\Users\\Administrator\\Downloads");
+
+
+ /**
+ * fastdfs:
+ * allowFiles: [ ".png",".txt" ]
+ * allowFileSize: 10240
+ * charset: UTF-8
+ * connectTimeoutInSecond: 10
+ * networkTimeoutInSeconds: 30
+ * httpTrackerHttpPort: 8888
+ * httpAntiStealToken: no
+ * trackerServers: 172.16.1.199:22122
+ * fastdfsUrl: http://172.16.1.199:8888
+ * storageServiceIp: 172.16.1.199
+ * storageServicePort: 23000
+ * storageServicePathIndex: 0
+ */
+ ArchetypeFileConfig fastDfsConfig = new FastDfsConfig()
+ .setAllowFiles(Arrays.asList(".png", ".txt"))
+ .setAllowFileSize(10240L)
+ .setCharset("UTF-8")
+ .setConnectTimeoutInSecond("10")
+ .setNetworkTimeoutInSeconds("30")
+ .setHttpTrackerHttpPort("8888")
+ .setHttpAntiStealToken("no")
+ .setTrackerServers("172.16.1.199:22122")
+ .setStorageServiceIp("172.16.1.199")
+ .setStorageServicePort(23000)
+ .setStorageServicePathIndex(0)
+ ;
+
+ @Test
+ public void uploadTest() {
+ MultipartFile file = new MockMultipartFile(
+ "file",
+ "hello11.txt",
+ MediaType.TEXT_PLAIN_VALUE,
+ "Hello, World!111111111111111111111111111".getBytes()
+ );
+ List<String> uriLs = fileFacade.upload(new MultipartFile[]{file}, aliyunFileConfig);
+ System.out.println("=>" + uriLs);
+ }
+
+
+ @Test
+ public void downloadTest() {
+ fileFacade.download(ArchetypeFilePlatformType.LOCAL, "C:\\Users\\Administrator\\Downloads\\20230114\\hello_1614199174685622272.txt");
+ }
+
+}
+
+ +
+ + + + + +参考资料:
+ +组件是可复用的 Vue 实例,且带有一个名字. 组件的出现是为了拆分vue实例的代码量,能够让我们以不同的组件,来划分不同的功能模块,将来我们需要什么样的功能,就可以去调用对应的组件即可.
+参考资料:
+路由概念:
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <script src="./lib/vue.min.js" type="text/javascript"></script>
+ <title>Document</title>
+</head>
+<body>
+
+ <!-- 使用 vue 框架 开发大大简化了开发,提高了开发效率 代码 使得前端程序员只关心页面逻辑 -->
+ <div class="app">
+ <p>{{msg}}</p>
+ </div>
+ <script>
+ var vm = new Vue({
+ el: '.app',
+ data: {
+ msg: 'helloWord'
+ }
+ })
+ </script>
+</body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+ <style>
+ [v-cloak] {
+ display: none;
+ }
+ </style>
+</head>
+<body>
+ <!--v-cloak 和 v-text 可以避免 由于网速过慢vue 的闪烁问题
+ "v-bind:" : 等同于 ":"
+ v-html : 转义渲染页面
+ v-on:"事件名字"="函数名字" 绑定事件
+ -->
+ <div id="app">
+ <div v-cloak>{{msg}}</div>
+ <div v-text="msg"></div>
+ <div v-html="msg2"></div>
+ <input type="button" value="按钮" :title="mytitle" v-on:click="show">
+ </div>
+ <script src="./lib/vue.min.js" type="text/javascript"></script>
+ <script>
+ var vm = new Vue({
+ el: '#app',
+ data: {
+ msg: '123',
+ mytitle: '我是一个按钮!',
+ msg2: '<h1>我是一个h1标签,.....</h1>',
+
+ },
+ methods:{
+ show:function(){
+ alert("1234");
+ }
+ }
+ })
+ </script>
+</body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+ <script src="./lib/vue.min.js" type="text/javascript"></script>
+</head>
+<body>
+ <div id="app">
+ <button @click="lang">start</button>
+ <button @click="stop">end</button>
+ <div>{{ msg }}</div>
+ </div>
+ <script>
+ var vm = new Vue({
+ el: '#app',
+ data: {
+ msg: '猥琐发育别浪~~~',
+ time: null,
+ },
+ methods: {
+ //如果调用vue 本身的属性 用this
+ lang(){
+ //如果定时器不等于null 说明开了定时器 就不能再开一个定时器了
+ if(this.time != null)return;
+ //开启定时器
+ this.time = setInterval(() => {
+ //截取字符串
+ var start = this.msg.substring(0,1);
+ var end = this.msg.substring(1);
+ //拼接成新的字符串
+ this.msg = end + start;
+
+ },400)
+ },
+ stop(){
+ //清除定时器
+ clearInterval(this.time);
+ //把time重新赋值为null
+ this.time = null;
+ }
+ }
+ })
+ </script>
+</body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+ <script src="./lib/vue.min.js" type="text/javascript"></script>
+ <style>
+ .outDiv{
+ height: 300px;
+ background-color: red;
+ }
+ .innerDiv{
+ height: 100px;
+ background-color: aqua;
+ }
+ </style>
+</head>
+<body>
+ <div id="app">
+ <!-- 1.stop修饰符 组织冒泡事件
+ 2.prevent 修饰符 阻止默认事件
+ 3.once 修饰符 once 事件只触发一次
+ 4.capture 从外往里执行事件 实现捕获触发事件的机制
+ 5.self 只有点击当前元素的时候才触发的处理函数
+
+ self 和 stop 都会阻止冒泡 区别:
+ .self 只会阻止一层冒泡 stop 阻止所有冒泡
+ -->
+ <div class="outDiv" @click.self.capture="div1Handler">
+ <div class="innerDiv" @click.self="div2Handler">
+ <button @click.stop="btn">戳我</button>
+ <a href="https:www.baidu.com" @click.prevent="baidu">我是百度</a>
+ </div>
+ </div>
+ </div>
+ <script>
+ var vm = new Vue({
+ el: '#app',
+ data: {
+
+ },
+ methods:{
+ div1Handler(){
+ console.log("div1Handler事件");
+ },
+ div2Handler(){
+ console.log("div2Handler事件");
+ },
+ btn(){
+ console.log("点击了按钮");
+ },
+ baidu(){
+ console.log("点击了超链接!");
+ }
+ }
+ })
+ </script>
+</body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+ <script src="./lib/vue.min.js" type="text/javascript"></script>
+</head>
+<body>
+ <!-- v-model 实现双向数据绑定
+ 1.只对表单元素生效
+ 2. 使用 v-model 可以实现 表单元素和model中的数据 双向绑定
+ v-bind 只能从 M -> V 无法实现数据双向绑定
+ -->
+ <div id="app">
+ <div><h1>{{msg}}</h1></div>
+ <input type="text" style="width: 100%;" v-model="msg">
+ </div>
+ <script>
+ var vm = new Vue({
+ el: '#app',
+ data:{
+ msg: 'Hello Word',
+ },
+ methods:{
+
+ }
+ })
+ </script>
+</body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+ <script src="./lib/vue.min.js"></script>
+</head>
+
+<body>
+ <div id="app">
+ <input type="text" v-model="n1">
+ <select v-model="opt">
+ <option value="+">+</option>
+ <option value="-">-</option>
+ <option value="*">*</option>
+ <option value="/">/</option>
+ </select>
+ <input type="text" v-model="n2">
+ <button @click="calcu">=</button>
+ <input type="text" v-model="result">
+ </div>
+ <script>
+ var vm = new Vue({
+ el: '#app',
+ data: {
+ n1: 0,
+ n2: 0,
+ opt: '+',
+ result: 0,
+ },
+ methods: {
+ calcu() {
+ // switch (this.opt) {
+ // case '+': this.result = parseInt(this.n1) + parseInt(this.n2)
+ // break;
+ // case '-': this.result = parseInt(this.n1) - parseInt(this.n2)
+ // break;
+ // case '*': this.result = parseInt(this.n1) * parseInt(this.n2)
+ // break;
+ // case '/': this.result = parseInt(this.n1) / parseInt(this.n2)
+ // break;
+ // }
+ this.result = eval(parseInt(this.n1) + this.opt + parseInt(this.n2));
+ }
+ }
+ })
+ </script>
+</body>
+
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+ <script src="./lib/vue.min.js"></script>
+ <style>
+ .red{
+ color: red;
+ }
+ .italic{
+ font-style: italic;
+ }
+ .thin{
+ font-weight: 100;
+ }
+ </style>
+</head>
+<body>
+ <div id="app">
+ <div>
+ <!--
+ vue 的class 选择器 可以通过v-band来绑定
+ :class="" 可以放数组 可以放对象
+ 可以通过三目运算符 来进行运算
+ -->
+ <h1 :class="[flag?'thin':'red']">这是一个h1标签,大到你无法想象!</h1>
+ <!-- <h1 :class="['red']">这是一个h1标签,大到你无法想象!</h1> -->
+ <!-- <h1 :class="classObj">这是一个h1标签,大到你无法想象!</h1> -->
+ </div>
+ </div>
+ <script>
+ var vm = new Vue({
+ el: '#app',
+ data: {
+ flag: true,
+ classObj:{red: true,italic: true,thin: true},
+ },
+ methods:{
+
+ }
+ })
+ </script>
+</body>
+
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+ <script src="./lib/vue.min.js"></script>
+</head>
+<body>
+ <div id="app">
+ <!--
+ vue 中的内联样式
+ :style="" 可以写对象
+ 可以写多个对象
+ 可以写数组 和 外联样式 class 一样
+ -->
+ <h1 :style="[styleObj,styleObj2]">我是h1, 我为自己代言</h1>
+ </div>
+ <script>
+ var vm = new Vue({
+ el: '#app',
+ data:{
+ //样式名称有横杠 必须用单引号
+ styleObj: {color: 'red','font-weight': 100},
+ styleObj2:{'font-style': 'italic'}
+ },
+ methods:{
+
+ }
+ })
+ </script>
+</body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+ <script src="./lib/vue.min.js"></script>
+</head>
+<body>
+
+ <div id="app">
+ <!-- v-for 循环索引从 1 开始 -->
+ <p v-for="count in 10">这是第{{count}}次循环</p>
+ </div>
+
+ <script>
+ var vm = new Vue({
+ el:'#app',
+ data:{
+
+ }
+ })
+ </script>
+</body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+ <script src="./lib/vue.min.js"></script>
+</head>
+<body>
+
+ <div id="app">
+ <p v-for="(val, key) in user">值:{{val}} 键:{{key}}</p>
+ </div>
+
+ <script>
+ var vm = new Vue({
+ el:'#app',
+ data:{
+ user:{
+ name: '张三',
+ sex: '男',
+ age: '19',
+ }
+ },
+ methods:{
+
+ }
+ })
+ </script>
+</body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+ <script src="./lib/vue.min.js"></script>
+</head>
+<body>
+
+
+ <div id="app">
+ <p v-for="item in list">list中的元素:{{item}}</p>
+ </div>
+
+ <script>
+ var vm = new Vue({
+ el: '#app',
+ data:{
+ list:[1,2,,3,4,5,6]
+ },
+ methods:{
+
+ }
+ })
+ </script>
+</body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+ <script src="./lib/vue.min.js"></script>
+</head>
+<body>
+ <div id="app">
+ <label>
+ id:<input type="text" v-model="id">
+ </label>
+ <label>
+ Name: <input type="text" v-model="name">
+ </label>
+ <input type="button" value="添加" @click="add">
+ <!--
+ v-for 循环的时候 key 的值只能为 String或number 不能是对象或者其他的
+ 使用key的时候 必须用v-bind: 绑定
+ 在组件中 如果使用v-for有问题 但又必须使用 v-for 要指明key的值
+ 不指明的话 默认是按索引寻找的
+ -->
+ <p v-for="item in list" :key="item.id">
+ id:{{item.id}}-----name:{{item.name}}
+ </p>
+ </div>
+ <script>
+ var vm = new Vue({
+ el: '#app',
+ data:{
+ id: '',
+ name: '',
+ list:[
+ {id: 1, name: '张三'},
+ {id: 2, name: '李四'},
+ {id: 3, name: 'abc'},
+ {id: 4, name: '王五'},
+ {id: 5, name: '赵六'},
+ ],
+ },
+ methods:{
+ add(){
+ this.list.push({ id: this.id,name: this.name});
+ }
+ }
+ })
+ </script>
+</body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+ <script src="./lib/vue.min.js"></script>
+</head>
+<body>
+
+ <div id="app">
+ <input type="button" value="戳我" @click="show">
+ <h1 v-show="flag">这是用v-show控制的元素.</h1>
+ <h1 v-if="flag">这是用v-if控制的元素.</h1>
+ <!--
+ v-show 和 v-if 区别:
+ v-show: 是用style:display:none; 来控制元素
+ v-if: 是控制dom 删除或者添加元素
+ -->
+ </div>
+
+ <script>
+ var vm = new Vue({
+ el: '#app',
+ data:{
+ flag: false,
+ },
+ methods:{
+ show(){
+ this.flag = !this.flag;
+ }
+ }
+ })
+ </script>
+</body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+ <script src="./lib/vue.min.js"></script>
+ <link rel="stylesheet" href="./lib/bootstrap.css">
+</head>
+
+<body>
+ <div id="app">
+
+ <div class="panel panel-primary">
+ <div class="panel-heading">
+ <h3 class="panel-title">添加品牌</h3>
+ </div>
+ <div class="panel-body form-inline">
+ <label>
+ ID
+ <input type="text" class="form-control" v-model="id">
+ </label>
+ <label>
+ Name
+ <!--
+
+ -->
+ <input type="text" class="form-control" v-model="name" @keyup.space="add">
+ </label>
+ <label>
+ <!-- 方法带括号 可以传参数 -->
+ <input type="button" value="添加" class="btn btn-primary" @click="add()">
+ </label>
+ <label>
+ 搜索名称关键字
+ <!--
+ v-color=" 'blue' "
+ 如果不加单引号 会认为 blue 是 date里边的对象 会从date里边去找
+ 加了就是字符串
+ -->
+ <input type="text" class="form-control" v-model="keywords" v-focus v-color="'blue'">
+ </label>
+ </div>
+ </div>
+
+ <table class="table table-bordered table-hover table-striped">
+ <thead>
+ <tr>
+ <th>ID</th>
+ <th>Name</th>
+ <th>Time</th>
+ <th>Operation</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="item in search(keywords)" :key="item.id">
+ <td>{{ item.id }}</td>
+ <td>{{ item.name }}</td>
+ <td>{{ item.time | dateFormat()}}</td>
+ <td><a href="" @click.prevent="del(item.id)">删除</a></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <script>
+ //自定义全局指令 使用该指令 进入该页面 获取焦点
+ //定义指令的时候不用加 v-... 但是调用的时候 要加 v-...
+ Vue.directive('focus', {
+ //钩子函数
+ //在这三个函数中 第一个参数永远都是 el 表示 被绑定了的那个元素
+ //el 为 原声的 DOM 对象
+ bind: function(el){//当指令绑定到元素上的时候 会执行该函数 只执行1次
+ //bind 函数常用来 控制样式
+ },
+ inserted: function(el){//当元素插入到 DOM 的时候会执行该函数 只执行1次
+ //当控制 js 行为的时候 最好放在inserted中 防止不执行该行为
+ el.focus()
+ },
+ updated: function(el){// 当VVOde 节点更新的时候 会执行该函数 可执行多次
+
+ }
+ });
+
+ Vue.directive('color',{
+ bind(el,binding){
+ el.style.color = binding.value
+ }
+ });
+
+
+ //自定义键盘修饰符
+ Vue.config.keyCodes.space = 32
+ // Vue.config.devtools = true
+ //定义全局过滤器 格式化时间
+ Vue.filter('dateFormat', function (dateStr) {
+ // var date = new Date(dateStr)
+
+ // //padStart 可以在前边补0 第一个参数为总长度 第二个为 补的东西
+ // var year = date.getFullYear()
+ // var month = (date.getMonth() + 1).toString().padStart(2, 0)
+ // var day = date.getDate().toString().padStart(2, 0)
+ // var hour = date.getHours().toString().padStart(2, 0)
+ // var minutes = date.getMinutes().toString().padStart(2, 0)
+ // var seconds = date.getSeconds().toString().padStart(2, 0)
+
+ // return year + '-' + month + '-' + day + ' ' + hour + ':' + minutes + ':' + seconds
+ })
+
+
+ // vue 入口
+ var vm = new Vue({
+ el: "#app",
+ data: {
+ list: [
+ { id: 1, name: '奔驰', time: new Date() },
+ { id: 2, name: '奔奔', time: new Date() },
+ { id: 3, name: '奔奔x', time: new Date() },
+ { id: 4, name: '宝马', time: new Date() },
+ { id: 5, name: '宝马x', time: new Date() },
+ { id: 6, name: '宝宝', time: new Date() },
+ ],
+ id: '',
+ name: '',
+ keywords: ''
+ },
+ methods: {
+ add() {//添加方法
+ var car = ({
+ id: this.id,
+ name: this.name,
+ time: new Date()
+ })
+ this.list.push(car)
+ this.id = this.name = ''
+ },
+ del(id) {//删除方法
+
+ //some 循环 根据指定的条件进行判断 如果返回true 则终止循环
+ this.list.some((item, i) => {
+ if (item.id == id) {
+ this.list.splice(i, 1)
+ return true;
+ }
+ });
+
+ //findIndex 方法直接查找索引 根据索引删除
+
+ // var index = this.list.findIndex((item) => {
+ // if(item.id == id){
+ // return true;
+ // }
+ // })
+ // this.list.splice(index, 1)
+ },
+ search(keywords) {//搜索关键字
+ // var newList = []
+ // this.list.forEach(item => {
+ // //判断搜索的名称 包不包含 该关键字 包含不等于-1
+ // if(item.name.indexOf(keywords) != -1){
+ // newList.push(item)
+ // }
+ // });
+ // return newList
+
+ //另一种实现方式 es6 新增
+ return this.list.filter(item => {
+ if (item.name.includes(keywords)) {
+ return item
+ }
+ });
+ }
+ },
+ filters: {//自定义过滤器
+ //如果 全局的过滤器 和自定义的过滤器名称相同 则优先调用自定义的过滤器
+ //就近原则
+ dateFormat: function (dateStr) {
+ var date = new Date(dateStr)
+
+ //padStart 可以在前边补0 第一个参数为总长度 第二个为 补的东西
+ var year = date.getFullYear()
+ var month = (date.getMonth() + 1).toString().padStart(2, 0)
+ var day = date.getDate().toString().padStart(2, 0)
+ var hour = date.getHours().toString().padStart(2, 0)
+ var minutes = date.getMinutes().toString().padStart(2, 0)
+ var seconds = date.getSeconds().toString().padStart(2, 0)
+
+ return year + '-' + month + '-' + day + ' ' + hour + ':' + minutes + ':' + seconds
+ }
+ },
+ //定义私有指令
+ //和自定义过滤器相似 就近原则
+ directives:{
+ // 'color':{
+ // bind:function(el,binding){
+ // el.style.color = 'pink'
+ // },
+ // inserted:function(el,binding){
+
+ // }
+ // }
+ //简写指令
+ //等同于 在 bind 和 updated 这两个钩子函数中各写了一份
+ 'fontsize':function(el,binding){
+ el.style.color = parseInt(binding.value) + 'px'
+ }
+ }
+ })
+ </script>
+</body>
+
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+ <script src="./lib/vue.min.js"></script>
+</head>
+<body>
+ <div id="app">
+ <!--
+ 调用过滤器
+ 必须在差值表达式或v-bind中使用
+ 调用格式: {{msg | 过滤器名称 | 过滤器名称}}
+ 可以调用多个过滤器 可以传多个参数 但注意过滤器的第一个参数是 要处理的数据 function(data)
+ -->
+ <p>{{ msg | msgFormat('昨天') | test() }}</p>
+ </div>
+ <script>
+ //自定义过滤器
+ Vue.filter( 'msgFormat' , function(msg,arg1){
+ return msg.replace(/今天/g , arg1)
+ })
+
+ Vue.filter('test' , function(data){
+ return data + "123"
+ })
+
+ var vm = new Vue({
+ el: '#app',
+ data: {
+ msg: '今天是2019年6月2日,星期日,天气多云,今天和昨天一样,今天.....',
+ },
+ methods:{
+
+ }
+ })
+ </script>
+</body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+ <script src="./lib/vue.min.js"></script>
+ <!-- vue-resource.js 依赖于 vue.js 导入的时候 注意顺序 -->
+ <script src="./lib/vue-resource.js"></script>
+</head>
+<body>
+ <div id="app">
+ <input value="get方式" @click="getInfo" type="button">
+ <input value="post方式" @click="postInfo" type="button">
+ <input value="jsonp方式" @click="jsonpInfo" type="button">
+ <p>{{message}}</p>
+ </div>
+ <script>
+
+ // 设置全局的根域名
+ // 如果配置了全局的根域名,则在每次单独发起调用http的请求的时候,请求的url应以相对路径开头 前边不能带"/",否则不会启用根路径作拼接
+ Vue.http.options.root = "http://jsonplaceholder.typicode.com"
+
+ //设置全局的 emulateJson 选项
+ Vue.http.options.emulateJSON = true
+
+ var vm = new Vue({
+ el: '#app',
+ data:{
+ message:"这里显示控制台的输出"
+ },
+ methods:{
+ getInfo(){
+ this.$http.get('users').then(function(result){
+ console.log(result.body)
+ this.message = result.body
+ })
+ },
+ postInfo(){
+ //post 默认表单格式
+ // 第三个参数为: emulateJSON : true 表单格式 相当于设置 application/x-www-form-urlencoded
+ //第二个参数为: options 可选参数
+ this.$http.post('users',{},{emulateJSON: true}).then(result =>{
+ console.log(result.body)
+ this.message = result.body
+ })
+ },
+ jsonpInfo(){
+ this.$http.jsonp('users').then(result => {
+ console.log(result.body)
+ this.message = result.body
+ })
+ }
+
+ }
+ })
+ </script>
+</body>
+
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+ <script src="./lib/vue.min.js"></script>
+ <link rel="stylesheet" href="./lib/bootstrap.css">
+</head>
+
+<body>
+ <div id="app">
+ <div class="panel-body form-inline">
+ ID: <input type="text" v-model="id" class="form-control">
+ Name: <input type="text" v-model="name" class="form-control">
+ Age:<input type="text" v-model="age" class="form-control" @keydown="doAdd($event)">
+ <input type="button" value="添加" class="btn btn-primary" @click="add()">
+ </div>
+ <table class="table table-bordered table-hover table-striped">
+ <thead>
+ <tr>
+ <th>ID</th>
+ <th>姓名</th>
+ <th>年龄</th>
+ <th>时间</th>
+ <th>操作</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="(item,key) in list" :key="item.id">
+ <td>{{item.id}}</td>
+ <td>{{ item.name }}</td>
+ <td>{{ item.age }}</td>
+ <td>{{ item.time}}</td>
+ <td><a href="" @click.prevent="del(item.id)">删除</a></td>
+ </tr>
+ </tbody>
+ </table>
+
+ </div>
+ <script>
+
+ // 自定义键盘码
+ Vue.config.keyCodes.f3 = 113
+
+ var vm = new Vue({
+ el: '#app',
+ data: {
+ id: '',
+ name: '',
+ age: '',
+ list: [{
+ id: '1',
+ name: '张三',
+ age: '18',
+ time: new Date()
+ }]
+ },
+ methods: {
+ add() {
+ var user = ({
+ id: this.id,
+ name: this.name,
+ age: this.age,
+ time: new Date()
+ })
+ this.list.push(user)
+ this.id = this.name = this.age = ''
+ },
+ doAdd(e) {
+ if (e.keyCode == 13)
+ this.add()
+ },
+ del(id) {
+ this.list.some(
+ (item, i) => {
+ if (item.id == id) {
+ this.list.splice(i, 1)
+ return true;
+ }
+ }
+ )
+ },
+ },
+
+ })
+ </script>
+</body>
+
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+ <script src="./lib/vue.min.js"></script>
+ <link rel="stylesheet" href="./lib/animate.css">
+ <!-- <link rel="stylesheet" href="https://raw.githubusercontent.com/daneden/animate.css/master/animate.css"> -->
+</head>
+
+<body>
+ <div id="app">
+ <!-- 使用 :duration="{enter:300,leave:300}" 来设置出场离场的时间 -->
+ <input type="button" value="animate基本使用" @click="flag = !flag">
+ <transition enter-active-class="bounceIn" leave-active-class="bounceOut" :duration="{enter:300,leave:300}">
+ <h4 v-if="flag" class="animated">animate的基本使用</h4>
+ </transition>
+ </div>
+
+</body>
+
+<script>
+ var vm = new Vue({
+ el: "#app",
+ data: {
+ flag: false
+ }
+ })
+</script>
+
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+ <script src="./lib/vue.min.js"></script>
+ <link rel="stylesheet" href="./lib/animate.css">
+ <style>
+ .boll {
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background-color: red;
+ }
+ </style>
+</head>
+
+<body>
+ <div id="app">
+ <input @click="flag = !flag" type="button" value="加入购物车">
+ <!-- -->
+ <transition @before-enter="beforeEnter" @enter="enter" @after-enter="afterEnter">
+ <div class="boll" v-if="flag"></div>
+ </transition>
+ </div>
+</body>
+<script>
+ var vm = new Vue({
+ el: "#app",
+ data: {
+ flag: false
+ },
+ methods: {
+ // 动画执行钩子函数第一个参数: el 表示 JavaScript原生的dom元素 ,可理解为通过 document.getElememtById得到的dom对象
+ // 动画入场前
+ beforeEnter(el) {
+ // 设置小球的初始位置
+ el.style.transform = "translate(0, 0)"
+ },
+ //表示动画开始之后的样式,这里可以设置小球完成动画之后的结束状态
+ enter(el,done) {
+ // offsetTop没有实际意义 可以理解为offsetTop 强制会刷新动画
+ el.offsetTop
+ // 设置动画过渡时间
+ el.style.transition = "all 1s ease"
+ //设置小球结束位置
+ el.style.transform = "translate(150px,450px)"
+ // done() 相当于afterEnter函数的调用
+ done()
+ },
+ afterEnter(el) {
+ this.flag = !this.flag
+ }
+ },
+ })
+</script>
+
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>animate动画效果</title>
+ <script src="./lib/vue.min.js"></script>
+ <link rel="stylesheet" href="./lib/animate.css">
+ <style>
+ li {
+ margin: 10px;
+ border: 1px #ccc solid;
+ font-size: 12px;
+ width: 100%;
+ padding: 10px;
+ list-style: none;
+ }
+
+ li:hover {
+ background-color: hotpink;
+ transition: all .6s ease;
+ }
+
+ .v-enter,
+ .v-leave-to {
+ opacity: 0;
+ transform: translateY(80px);
+ }
+
+ .v-enter-active,
+ .v-leave-active {
+ transition: all 0.6s ease;
+ }
+
+ .v-move {
+ transition: all .6s ease;
+ }
+
+ .v-leave-active {
+ position: absolute;
+ }
+ </style>
+</head>
+
+<body>
+ <div id="app">
+ <div>
+ ID: <input type="text" v-model="id">
+ 姓名: <input type="text" v-model="name">
+ <input type="button" value="添加" @click="add()">
+ </div>
+
+ <!-- 在实现列表过渡的时候,如果过渡的元素是通过v-for循环渲染出来的,不能使用transition包裹,需要使用transitionGroup -->
+ <!--使用 transition-group v-for必须指明 :key 属性 -->
+ <!-- appear实现入场效果 tag 外围标签渲染ul标签 如果不指定外围标签渲染为span元素-->
+ <transition-group appear tag="ul">
+ <li v-for="(item,index) in list" :key="item.id" @click="del(index)" title="点我删除哦~">
+ ID:{{item.id}}-----姓名:{{ item.name}}
+ </li>
+ </transition-group>
+
+
+
+ </div>
+</body>
+<script>
+ var vm = new Vue({
+ el: "#app",
+ data: {
+ id: '',
+ name: '',
+ list: [
+ { id: 1, name: "张三" },
+ { id: 2, name: "李四" },
+ { id: 3, name: "王五" },
+ { id: 4, name: "赵六" },
+ { id: 5, name: "孙七" },
+ ]
+ },
+ methods: {
+ //添加功能
+ add() {
+ this.list.push({ id: this.id, name: this.name })
+ this.id = this.name = ''
+ },
+ //删除功能
+ del(index) {
+ this.list.splice(index, 1)
+ }
+ },
+ })
+</script>
+
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+ <script src="./lib/vue.min.js"></script>
+</head>
+
+<body>
+ <div id="app">
+ <!-- 组件使用 直接使用标签即可 -->
+ <!-- 第一种方式创建组件使用 -->
+ <test1></test1>
+ <!-- 第二种方式创建组件使用 -->
+ <test2></test2>
+ <!-- 第三种方式创建组件使用 -->
+ <test3></test3>
+ <!-- 私有组件 -->
+ <personalcom></personalcom>
+ <!-- 组件中的data测试 -->
+ <test4></test4>
+ </div>
+
+ <!-- 第三种方式创建组件 详情看JavaScript-->
+ <template id="testTemplate">
+ <div>
+ <h4>这是第三种创建组件的方式</h4>
+ </div>
+ </template>
+
+ <!-- 定义私有组件 -->
+ <template id="personal">
+ <div>
+ <h4>这是定义的私有组件</h4>
+ </div>
+ </template>
+
+ <template id="testData">
+ <div>
+ <h4>组件中data测试---{{msg}}</h4>
+ </div>
+ </template>
+</body>
+<script>
+
+ //第一种方式创建组件 通过 Vue.extend来创建全局的组件模板
+ //给组件起名字 初始化组件 注意组件命名.如果组件名称使用驼峰命名,则在引用组件的时候需要把大写字母改成小写字母,同时两个单词之间用 '-' 连接
+ Vue.component('test1', Vue.extend({
+ // template 指定组件要展示的HTML结构
+ template: "<h4>这是第一种方式创建组件</h4>"
+ }))
+
+ //第二种创建组件的方式 直接通过component创建组件
+ // 注意: 无论通过那种方式创建组件 组件的template 模板内容,必须有且只能有唯一的一个根元素
+ Vue.component('test2', ({
+ template: "<h4>这是创建组件的第二种方式</h4>"
+ }));
+
+ //第三种方式创建组件 在 #app 外 创建
+ Vue.component('test3', {
+ template: '#testTemplate'
+ });
+
+ //组件中data测试
+ // 组件中可以存在data, data必须为一个方法, data必须返回一个对象,组件中data的用法和vue中的用法一样
+ Vue.component('test4', {
+ template: '#testData',
+ data: () => {
+ return {
+ msg: "组件中data的数据"
+ }
+ }
+ })
+
+
+ var vm = new Vue({
+ el: "#app",
+ data: {
+
+ },
+ methods: {
+
+ },
+ //私有组件
+ components: {
+ personalcom: {
+ template: "#personal"
+ }
+ },
+ });
+</script>
+
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+ <script src="./lib/vue.min.js"></script>
+</head>
+
+
+<body>
+ <div id="app">
+ <input @click="flag=true" type="button" value="login">
+ <input @click='flag=false' type="button" value="register">
+ <login v-if='flag'></login>
+ <register v-else='flag'></register>
+ </div>
+
+ <!-- 登录组件测试 -->
+ <template id="login">
+ <div>
+ <h3>登录组件</h3>
+ </div>
+ </template>
+
+ <!-- 注册组件测试 -->
+ <template id="register">
+ <div>
+ <h3>注册组件</h3>
+ </div>
+ </template>
+</body>
+<script>
+ var vm = new Vue({
+ el: "#app",
+ data: {
+ flag:false
+ },
+ components: {
+ login: {
+ template: '#login'
+ },
+ register: {
+ template: '#register'
+ }
+ }
+ })
+</script>
+
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+ <script src="./lib/vue.min.js"></script>
+</head>
+
+
+<body>
+ <div id="app">
+ <a href="" @click.prevent='componentName="login"'>显示登录组件</a>
+ <a href="" @click.prevent='componentName="register"'>显示注册组件</a>
+ <component :is="componentName"></component>
+ </div>
+
+ <!-- 登录组件测试 -->
+ <template id="login">
+ <div>
+ <h3>登录组件</h3>
+ </div>
+ </template>
+
+ <!-- 注册组件测试 -->
+ <template id="register">
+ <div>
+ <h3>注册组件</h3>
+ </div>
+ </template>
+</body>
+<script>
+ var vm = new Vue({
+ el: "#app",
+ data: {
+ componentName:'login'
+ },
+ components: {
+ login: {
+ template: '#login'
+ },
+ register: {
+ template: '#register'
+ }
+ }
+ })
+</script>
+
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+ <script src="./lib/vue.min.js"></script>
+ <style>
+
+ .v-enter,
+ .v-leave-to{
+ opacity: 0;
+ transform: translateX(100px);
+ }
+
+ .v-enter-active,
+ .v-leave-active{
+ transition: all 0.5s ease;
+ }
+ </style>
+</head>
+
+
+<body>
+ <div id="app">
+ <a href="" @click.prevent='componentName="login"'>显示登录组件</a>
+ <a href="" @click.prevent='componentName="register"'>显示注册组件</a>
+
+ <!-- 通过mode 属性,设置组件切换时候的模式 -->
+ <transition mode='out-in'>
+ <component :is="componentName"></component>
+ </transition>
+ </div>
+
+ <!-- 登录组件测试 -->
+ <template id="login">
+ <div>
+ <h3>登录组件</h3>
+ </div>
+ </template>
+
+ <!-- 注册组件测试 -->
+ <template id="register">
+ <div>
+ <h3>注册组件</h3>
+ </div>
+ </template>
+</body>
+<script>
+ var vm = new Vue({
+ el: "#app",
+ data: {
+ componentName:'login'
+ },
+ components: {
+ login: {
+ template: '#login'
+ },
+ register: {
+ template: '#register'
+ }
+ }
+ })
+</script>
+
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+ <script src="./lib/vue.min.js"></script>
+</head>
+
+<body>
+ <div id="app">
+ <p>父子组件传值测试</p>
+ <!-- 父组件可以在引用子组件的时候,通过v-bind绑定的形式,把需要传递给子组件的数据,传递到子组件的内部,共子组件使用 -->
+ <test v-bind:parentmsg="msg"></test>
+ </div>
+ <template id="test">
+ <div>
+ <h4>这是子组件-----{{parentmsg}}</h4>
+ <input type="button" value="点我修改值" @click="hello">
+ </div>
+ </template>
+</body>
+<script>
+ var vm = new Vue({
+ el: '#app',
+ data: {
+ msg: '父组件的值'
+ },
+ components: {
+ test: {
+ template: '#test',
+ //定义在props里边的数据都是只读数据 不可修改
+ //把父组件传递过来的parentmsg属性,现在props数组里边定义一下,这样才能使用这个数据
+ props: ['parentmsg'],
+ //定义在data里边的数据 并不是通过父组件传递过来的 而是私有的
+ //data里边的数据都是可读可写的
+ data() {
+ return {
+ commsg: '这是组件data里边的数据'
+ }
+ },
+
+ methods: {
+ hello() {
+ this.parentmsg = "修改后的值~"
+ }
+ },
+ },
+ }
+ });
+</script>
+
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+ <script src="./lib/vue.min.js"></script>
+</head>
+<body>
+ <div id="app">
+ <com1 @func='show'></com1>
+ </div>
+ <template id="template1">
+ <div>
+ <h4>这是子组件</h4>
+ <input type="button" value="点我" @click='com1show'>
+ </div>
+ </template>
+</body>
+<script>
+ var com1 = {
+ template: '#template1',
+ data() {
+ return {
+ msg: { name: 'test', age: 22 }
+ }
+ },
+ methods: {
+ com1show() {
+ // emit 触发调用父元素中绑定的方法
+ this.$emit('func', this.msg)
+ }
+ },
+ }
+
+ var vm = new Vue({
+ el: '#app',
+ components: {
+ com1
+ },
+ methods: {
+ show(data) {
+ console.log(data)
+ }
+ },
+ });
+</script>
+
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+ <script src="./lib/vue.min.js"></script>
+ <link href="./lib/bootstrap.css" rel="stylesheet">
+ </script>
+</head>
+
+<body>
+ <div id="app">
+ <comment @func="loadComments"></comment>
+
+ <ul class="list-group">
+ <li class="list-group-item" v-for="item in list" :key="item.id">
+ <span class="badge">评论人:{{ item.name }}</span>
+ {{ item.content }}
+ </li>
+ </ul>
+
+ </div>
+
+ <template id="templateComment">
+ <div class="">
+ 评论人:<input type="text" class="form-control" v-model='name'>
+ 评论内容:<textarea class="form-control" v-model='content'></textarea>
+ <input type="button" value="发布评论" class="btn btn-primary" @click="postComment">
+ </div>
+ </template>
+
+</body>
+<script>
+
+ //用户评论组件
+ var cmts = {
+ data() {
+ return {
+ name: '',
+ content: ''
+ }
+ },
+ template: '#templateComment',
+ methods: {
+ postComment() {
+ //创建评论对象
+ var comment = {
+ id: Date.now(),
+ name: this.name,
+ content: this.content
+ }
+
+ //从localstorage获取数据
+ var list = JSON.parse(localStorage.getItem('cmts') || '[]')
+ //添加到list
+ list.unshift(comment)
+ //存到localstorage
+ localStorage.setItem('cmts',JSON.stringify(list))
+ //清空input
+ this.name = this.content = ''
+
+ this.$emit('func')
+ }
+ },
+
+ }
+
+ var vm = new Vue({
+ el: '#app',
+ data: {
+ list: [
+ { id: Date.now(), name: '测试1', content: "这是一条评论!" },
+ { id: Date.now(), name: '测试2', content: "23333333!" },
+ { id: Date.now(), name: '测试3', content: "helloworld!" }
+ ]
+ },
+ created() {
+ //自动刷新评论列表
+ this.loadComments()
+ },
+ methods: {
+ loadComments() {
+ this.list = JSON.parse(localStorage.getItem('cmts') || '[]')
+ }
+ },
+ components: {
+ 'comment': cmts
+ }
+ })
+</script>
+
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+ <script src="./lib/vue.min.js"></script>
+</head>
+<body>
+ <div id="app">
+ <templ ref="templ"></templ>
+ <input type="button" value="点我" @click="func">
+ <h4 ref="myh4">测试</h4>
+ </div>
+ <template id="templ">
+ <div>
+ <h4>ref获取组件测试,请点击按钮后,按F12检查</h4>
+ </div>
+ </template>
+</body>
+<script>
+
+ var templ = {
+ template:'#templ',
+ data() {
+ return {
+ msg:"这是组件中的msg"
+ }
+ },
+ }
+
+ var vm = new Vue({
+ el:'#app',
+ data:{
+
+ },
+ methods: {
+ func(){
+ console.log(this.$refs.templ.msg)
+ console.log("这是获取页面中的dom==>"+this.$refs.myh4.innerText)
+ }
+ },
+ components:{
+ templ
+ }
+ })
+</script>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+ <script src="./lib/vue.min.js"></script>
+ <script src="./lib/vue-router.js"></script>
+ <style>
+ /* 设置router-link的样式
+ 也可以设置默认的 router-link-exact-active 类的样式
+ */
+ .myActive {
+ color: red;
+ font-size: 30px;
+ font-weight: 500;
+ }
+
+ .v-enter,
+ .v-leave-to {
+ opacity: 0;
+ transform: translateX(150px);
+ }
+
+ .v-enter-active,
+ .v-leave-active {
+ transition: all .5s ease;
+ }
+ </style>
+</head>
+
+<body>
+ <div id="app">
+ <!-- <a href="#/login">登录</a>
+ <a href="#/register">注册</a> -->
+ <!-- 会默认渲染a标签 -->
+ <router-link to="/login">登录[routerlink]</router-link>
+ <router-link to="/register">注册[routerlink]</router-link>
+ <!-- 这是vue-router提供的组件,专门用来当做占位符,将来路由规则匹配到的组件,就会展示到这个router-view中 -->
+ <transition mode='out-in'>
+ <router-view></router-view>
+ </transition>
+
+ </div>
+</body>
+<script>
+
+ var login = {
+ template: '<h2>登录组件</h2>'
+ }
+
+ var register = {
+ template: '<h2>注册组件</h2>'
+ }
+
+
+ //创建路由对象
+ const routerobj = new VueRouter({
+ //路由的匹配规则
+ // 每个路由规则都是一个对象 这个规则对象身上必须有两个属性
+ //属性1: path,表示监听那个路由的连接
+ // 属性2: component: 表示如果路由匹配到的path,则展示对应的component
+ routes: [
+ //重定向 根路径 到登录组件
+ { path: '/', redirect: login },
+ { path: '/login', component: login },
+ { path: '/register', component: register }
+ ],
+
+ //修改router-link样式
+ linkActiveClass: 'myActive'
+ })
+ new Vue({
+ el: '#app',
+ //将路由对象挂载到vue上
+ router: routerobj
+ })
+</script>
+
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+ <script src="./lib/vue.min.js"></script>
+ <script src="./lib/vue-router.js"></script>
+ <style>
+ /* 设置router-link的样式
+ 也可以设置默认的 router-link-exact-active 类的样式
+ */
+ .myActive {
+ color: red;
+ font-size: 30px;
+ font-weight: 500;
+ }
+
+ .v-enter,
+ .v-leave-to {
+ opacity: 0;
+ transform: translateX(150px);
+ }
+
+ .v-enter-active,
+ .v-leave-active {
+ transition: all .5s ease;
+ }
+ </style>
+</head>
+
+<body>
+ <div id="app">
+ <!-- <a href="#/login">登录</a>
+ <a href="#/register">注册</a> -->
+ <!-- 会默认渲染a标签 -->
+ <router-link to="/login?id=99&name=zs">登录[routerlink]</router-link>
+ <router-link to="/register/20/ls">注册[routerlink]</router-link>
+ <!-- 这是vue-router提供的组件,专门用来当做占位符,将来路由规则匹配到的组件,就会展示到这个router-view中 -->
+ <transition mode='out-in'>
+ <router-view></router-view>
+ </transition>
+
+ </div>
+</body>
+<script>
+
+ var login = {
+ //第一种方式显示参数
+ template: '<h2>登录组件 id:{{$route.query.id}} 姓名:{{$route.query.name}}</h2>',
+ created() {
+ console.log(this.$route)
+ },
+ }
+
+ var register = {
+ //第二种方式显示参数
+ template: '<h2>注册组件 年龄:{{$route.params.age}} 姓名:{{$route.params.name}}</h2>'
+ }
+
+ var index = {
+ template: '<h2>index首页</h2>'
+ }
+
+ //创建路由对象
+ const routerobj = new VueRouter({
+ //路由的匹配规则
+ // 每个路由规则都是一个对象 这个规则对象身上必须有两个属性
+ //属性1: path,表示监听那个路由的连接
+ // 属性2: component: 表示如果路由匹配到的path,则展示对应的component
+ routes: [
+ //重定向 根路径 到登录组件 注意重定向路径要写字符串
+ { path: '/', redirect:'/index' },
+ { path: '/index', component: index },
+ { path: '/login', component: login },
+ { path: '/register/:age/:name', component: register }
+ ],
+
+ //修改router-link样式
+ linkActiveClass: 'myActive'
+ })
+ new Vue({
+ el: '#app',
+ //将路由对象挂载到vue上
+ router: routerobj
+ })
+</script>
+
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+ <script src="./lib/vue.min.js"></script>
+ <script src="./lib/vue-router.js"></script>
+</head>
+
+<body>
+ <div id="app">
+ <router-link to="/account">跳转到account组件</router-link>
+ <router-view></router-view>
+ </div>
+
+ <template id="templ">
+ <div>
+ <h2>account组件</h2>
+ <router-link to="/account/login">登录</router-link>
+ <router-link to="/account/register">注册</router-link>
+
+ <router-view></router-view>
+ </div>
+ </template>
+</body>
+<script>
+
+ var account = {
+ template: '#templ'
+ }
+
+ var login = {
+ template: '<h3>登录组件</h3>'
+ }
+
+ var register = {
+ template: '<h3>注册组件</h3>'
+ }
+
+ const router = new VueRouter({
+ routes: [
+ {
+ path: '/account',
+ component: account,
+ // 子路由的嵌套
+ children: [
+ {
+
+ path: 'register', component: register
+ },
+ {
+ path: 'login', component: login,
+ }
+ ]
+ },
+ ]
+ })
+
+ new Vue({
+ el: '#app',
+ router
+ })
+</script>
+
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+ <script src="./lib/vue.min.js"></script>
+ <script src="./lib/vue-router.js"></script>
+ <style>
+ html,body{
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+ }
+ h1 {
+ margin: 0;
+ padding: 0;
+ }
+
+ .header {
+ height: 100px;
+ background-color: blanchedalmond;
+ }
+ .container{
+ display: flex;
+ height: 653px;
+ }
+
+ .left {
+ flex: 2;
+ background-color: chocolate;
+ }
+ .main{
+ flex: 8;
+ background-color: darkgoldenrod;
+ }
+ </style>
+</head>
+
+<body>
+ <div id="app">
+ <router-view></router-view>
+ <div class="container">
+ <router-view name='left'></router-view>
+ <router-view name='main'></router-view>
+ </div>
+
+ </div>
+</body>
+<script>
+
+ var header = {
+ template: '<h1 class="header">头部组件<h1>'
+ }
+ var leftBox = {
+ template: '<h1 class="left">左边导航组件<h1>'
+ }
+ var main = {
+ template: '<h1 class="main">中间内容组件<h1>'
+ }
+
+ const router = new VueRouter({
+ routes: [
+ {
+ //设置路由匹配规则
+ path: '/', components: {
+ 'default': header,
+ 'left': leftBox,
+ 'main': main
+ }
+ }
+ ]
+ })
+
+ new Vue({
+ el: '#app',
+ router
+ })
+</script>
+
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+ <script src="./lib/vue.min.js"></script>
+ <script src="./lib/vue-router.js"></script>
+</head>
+
+<body>
+ <div id="app">
+ <router-link to="/login">登录[routerlink]</router-link>
+ <router-link to="/register">注册[routerlink]</router-link>
+ <router-view></route-view>
+ </div>
+</body>
+<script>
+
+ var login = {
+ template: '<h3>登录组件</h3>'
+ }
+
+ var register = {
+ template: '<h3>注册组件</h3>'
+ }
+
+ const router = new VueRouter({
+ routes: [
+ {
+ path: '/login', component: login
+ },
+
+ {
+ path: '/register', component: register
+ }
+ ]
+ })
+
+ new Vue({
+ el: "#app",
+ router,
+ //watch 监控
+ watch: {
+ '$route.path': (newVal, oldVal) => {
+ // console.log("路由的newVal==>"+newVal)
+ // console.log("路由的oldVal==>"+oldVal)
+ if(newVal === '/login' )
+ console.log("登录组件")
+ if(newVal === '/register' )
+ console.log("注册组件")
+ }
+ }
+ })
+</script>
+
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Document</title>
+ <script src="./lib/vue.min.js"></script>
+</head>
+<body>
+ <div id="app">
+ <input type="text" v-model='firstName'>+
+ <input type="text" v-model='lastName'>=
+ <input type="text" v-model='fullName'>
+ </div>
+</body>
+<script>
+ new Vue({
+ el:'#app',
+ data:{
+ firstName:'',
+ lastName:'',
+ },
+ computed: {
+ // 计算的结果会被缓存起来
+ //计算属性在引用的时候一定不要加 () 直接把他当做普通的属性去调用
+ // 只要这个计算属性的function内部的data发生了变化,就会立即重新计算这个属性的值
+ fullName(){
+ return this.lastName +'----'+ this.firstName
+ }
+ },
+ })
+</script>
+</html>
+
+ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + +Java语言提供了八种基本类型。六种数值类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型. 俗称4类8种
+这里只介绍称4类8种.实际上,JAVA中还存在另外一种基本类型 void,它也有对应的包装类java.lang.Void
,不过我们无法直接对它们进行操作.
++一个Byte(字节)等于8个bit(位),bit是最小的单位,1B(字节)=8bit(位).
+
+一般情况下,一个汉字是两个字节,英文与数字是一个字节
byte/8
+-128(-2^7)
127(2^7-1)
byte a = 100,byte b = -50
short/16
+-32768(-2^15)
32767(2^15 - 1)
short s = 1000,short r = -20000
int/32
+-2,147,483,648(-2^31)
2,147,483,647(2^31 - 1)
int a = 100000, int b = -200000
long/64
+-9,223,372,036,854,775,808(-2^63)
9,223,372,036,854,775,807(2^63 -1)
0L
long a = 100000L,Long b = -200000L
float/32
+1.4E-45
,最大值3.4028235E38
0.0f
float f = 234.5f
double/64
+4.9E-324
,最大值1.7976931348623157E308
0.0d
double d = 123.4
Float
和Double
的最小值和最大值都是以科学记数法的形式输出的,结尾的"E+数字"表示E之前的数字要乘以10的多少次方.比如3.14E3
就是3.14 × 10^3 =3140
,3.14E-3
就是 3.14 x 10^-3 =0.00314
.
\u0000
(即为 0)\uffff
(即为65、535)\u0000
char letter = 'A'
false
boolean one = true
boolean 只有两个值:true、false
,可以使用 1 bit 来存储,但是具体大小没有明确规定。JVM 会在编译时期将 boolean 类型的数据转换为 int,使用 1 来表示 true,0 表示 false。JVM 支持 boolean 数组,但是是通过读写 byte 数组来实现的。
整型、实型(常量)、字符型数据可以混合运算。运算中,不同类型的数据先转化为同一类型,然后进行运算。
+转换从低级到高级。
+byte,short,char,int,long,float,double
+
数据类型转换必须满足如下规则:
+不能对boolean类型进行类型转换。
+不能把对象类型转换成不相关类的对象。
+在把容量大的类型转换为容量小的类型时必须使用强制类型转换。
+浮点数到整数的转换是通过舍弃小数得到,而不是四舍五入
+(int)23.7 == 23;
+(int)-45.89f == -45
+
转换过程中可能导致溢出或损失精度,在运算时要避免该问题.例如:
+// 因为 byte 类型是 8 位,最大值为127,所以当 int 强制转换为 byte 类型时,值 128 时候就会导致溢出
+int i =128;
+byte b = (byte)i;
+
自动类型转换必须满足转换前的数据类型的位数要低于转换后的数据类型. 即可以 long l = 100;
而不可以int l = 100L;
public class Test{
+ public static void main(String[] args){
+ char c1='a';//定义一个char类型
+ int i1 = c1;//char自动类型转换为int
+ System.out.println("char自动类型转换为int后的值等于"+i1);
+ char c2 = 'A';//定义一个char类型
+ int i2 = c2+1;//char 类型和 int 类型计算
+ System.out.println("char类型和int计算后的值等于"+i2);
+ }
+}
+// 结果:
+// char自动类型转换为int后的值等于97
+// char类型和int计算后的值等于66
+
Java 不能隐式执行向下转型,因为这会使得精度降低。
+1.1
字面量属于 double
类型,不能直接将 1.1
直接赋值给 float
变量,因为这是向下转型。
float f = 1.1;
+
1.1f
字面量才是 float 类型。
float f = 1.1f;
+
因为字面量 1 是 int 类型,它比 short 类型精度要高,因此不能隐式地将 int 类型下转型为 short 类型。
+short s1 = 1;
+s1 = s1 + 1;
+
但是使用 += 或者 ++ 运算符可以执行隐式类型转换。
+s1 += 1;
+s1++;
+
上面的语句相当于将 s1 + 1
的计算结果进行了向下转型:
//强制类型转换
+s1 = (short) (s1 + 1);
+
简而言之,不能够直接的将精度高的类型 直接的赋值给精度低的类型.如: float f = 1.1; short s = 1;
如果想要赋值可用Java的隐式类型转换 如:float f+=1.1; s += 1;
由大到小需要强制转换,由小到大不需要强转. 顺序:byte , short , char , int ,long,float,double
byte b=1; int a = b;//由小到大
+int c = 1;
+byte d = (byte) c;//由大到小
+
基本数据类型 | +包装类 | +
---|---|
byte | +Byte | +
boolean | +Boolean | +
short | +Short | +
char | +Character | +
int | +Integer | +
long | +Long | +
float | +Float | +
double | +Double | +
在这八个类名中,除了 Integer
和 Character
类以后,其它六个类的类名和基本数据类型一致,只是类名的第一个字母大写即可.
Integer、Long、Byte、Double、Float、Short
都是抽象类Number
的子类.
因为 Java 是一种面向对象语言,很多地方都需要使用对象而不是基本数据类型。比如,在集合类中,我们是无法将 int 、double
等类型放进去的。因为集合的容器要求元素是 Object
类型.所以才有了对应基本类型分包装类型.
基本类型都有对应的包装类型,基本类型与其对应的包装类型之间的赋值使用自动装箱与拆箱完成。
+以Integer int
为例
Integer x = 2; // 装箱 调用了 Integer.valueOf(2)
+int y = x; // 拆箱 调用了 X.intValue()
+
自动装箱: 就是将基本数据类型自动转换成对应的包装类.
+自动拆箱:就是将包装类自动转换成对应的基本数据类型.
+ Integer i = 10; //自动装箱
+ int b = i; //自动拆箱
+
反编译得
+ public static void main(String[]args){
+ Integer integer=Integer.valueOf(1);
+ int i=integer.intValue();
+ }
+
int
的自动装箱都是通过 Integer.valueOf()
方法来实现的,Integer 的自动拆箱都是通过 integer.intValue
来实现的
++自动装箱都是通过包装类的
+valueOf()
方法来实现的.自动拆箱都是通过包装类对象的xxxValue()
来实现的。
比较
+包装对象的数值比较,不能简单的使用==
,虽然 -128 到 127 之间的数字可以,但是这个范围之外还是需要使用 equals
方法进行比较.
NPE
+因为有自动拆箱的机制,如果初始的包装类型对象为null
,那么在自动拆箱的时候的就会报NullPointerException
,在使用时需要格外注意.
内存浪费 +如果一个 for 循环中有大量拆装箱操作,会浪费很多资源
+案例: new Integer(123)
与 Integer.valueOf(123)
的区别在于:
new Integer(123)
每次都会新建一个对象;Integer.valueOf(123)
会使用缓存池中的对象,多次调用会取得同一个对象的引用Integer x = new Integer(123);
+Integer y = new Integer(123);
+System.out.println(x == y); // false
+Integer z = Integer.valueOf(123);
+Integer k = Integer.valueOf(123);
+System.out.println(z == k); // true
+
valueOf()
方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容
public static Integer valueOf(int i) {
+ if (i >= IntegerCache.low && i <= IntegerCache.high)
+ return IntegerCache.cache[i + (-IntegerCache.low)];
+ return new Integer(i);
+}
+
在 Java 8 中,Integer 缓存池的大小默认为 -128~127
+static final int low = -128;
+static final int high;
+static final Integer cache[];
+
+static {
+ // high value may be configured by property
+ int h = 127;
+ String integerCacheHighPropValue =
+ sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
+ if (integerCacheHighPropValue != null) {
+ try {
+ int i = parseInt(integerCacheHighPropValue);
+ i = Math.max(i, 127);
+ // Maximum array size is Integer.MAX_VALUE
+ h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
+ } catch( NumberFormatException nfe) {
+ // If the property cannot be parsed into an int, ignore it.
+ }
+ }
+ high = h;
+
+ cache = new Integer[(high - low) + 1];
+ int j = low;
+ for(int k = 0; k < cache.length; k++)
+ cache[k] = new Integer(j++);
+
+ // range [-128, 127] must be interned (JLS7 5.1.7)
+ assert IntegerCache.high >= 127;
+}
+
编译器会在自动装箱过程调用 valueOf()
方法,因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建,那么就会引用相同的对象。
Integer m = 123;
+Integer n = 123;
+System.out.println(m == n); // true
+
Integer,int
在 -127~128
之前是不会创建新的对象的,即
Integer a = new Integer(12);
+ int b = 12;
+ System.out.println(a==b);//true
+
Integer
和int
自动装箱拆箱会通过valueOf()
方法实现,当这个数在-127~128之间直接从缓存里边取,不会重新new对象
基本类型对应的缓冲池如下:
+boolean values true and false
all byte values
short values between -128 and 127
int values between -128 and 127
char in the range \u0000 to \u007F
在使用这些基本类型对应的包装类型时,如果该数值范围在缓冲池范围内,就可以直接使用缓冲池中的对象。==两种浮点数类型的包装类Float,Double并没有实现常量池技术。==
+++在 JDK 1.8 所有的数值类缓冲池中,Integer 的缓冲池
+IntegerCache
很特殊,这个缓冲池的下界是 - 128,上界默认是 127,但是这个上界是可调的:
在启动JVM的时候,通过-XX:AutoBoxCacheMax=<size>
来指定这个缓冲池的大小,该选项在 JVM 初始化的时候会设定一个名为java.lang.IntegerCache.high
系统属性,然后IntegerCache
初始化的时候就会读取该系统属性来决定上界。
引用类型指向一个对象(类似于C的指针),指向对象的变量是引用变量
+所有引用类型的默认值都是null
基本数据类型在被创建时,在栈中直接划分一块内存,将数值直接存入栈中,引用数据类型再被创建时,先在堆中开辟内存创建存放值,然后引用到栈中的是在堆中的地址值
+传递参数的时候不同.基本数据类型是值传递,引用数据类型是引用传递.
+String
类是引用类型,不是基本类型double
类型,如果float
加后缀F(不区分大小写).如果long
类型加后缀L(不区分大小写)BigDecimal
类型案例: 常量,变量和字面量
+int a = 10; //a为变量,10为字面量
+final int b = 10; //b为常量,10为字面量
+static c = "Hello World"; //c为变量,HelloWorld为字面量
+
int a = 10
;这里的a是左值,10为右值.字面量
+++在计算机科学中,字面量(literal)是用于表达源代码中一个固定值的表示法(notation)。几乎所有计算机编程语言都具有对基本值的字面量表示,诸如:整数、浮点数以及字符串;而有很多也对布尔类型和字符类型的值也支持字面量表示;还有一些甚至对枚举类型的元素以及像数组、记录和对象等复合类型的值也支持字面量表示法。
+
Java常量
+++JAVA常量就是在程序运行过程中一直不会改变的量的量称为常量.常量在整个程序中只能被赋值一次.
+
Java常量简单理解为final
修饰的变量
在 Java 中使用final
关键字来修饰常量,声明方式和变量类似
要声明一个常量,第一需要制定数据类型,第二需要通过final
关键字进行限定格式:
+final 数据类型 常量名称[=值]
常量在程序运行时是不能被修改的(final作用).所以在定义常量时就需要对该常量进行初始化.为了与变量区别,常量取名一般都用大写字符
+final double PI = 3.1415927;
+
final
关键字表示最终的,它可以修改很多元素,修饰变量就变成了常量.之后会详细说明final
关键字
public class HelloWorld {
+ // 静态常量
+ public static final double PI = 3.14;
+ // 声明成员常量
+ final int y = 10;
+ public static void main(String[] args) {
+ // 声明局部常量
+ final double x = 3.3;
+ }
+}
+
一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述符信息外, +还包含一项信息就是常量池表(Constant Pool Table),包括各种字面量和对类型、域和方法的符号引用。
+常量池中有什么?
+常量池、可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。
+在Java中,大致上可以分为三种常量池.分别是字符串常量池、Class常量池(静态常量池)和运行时常量池.
+我们常说的常量池,就是指方法区中的运行时常量池。
+++class文件全名称为Java class文件,主要在平台无关性和网络移动性方面使Java更适合网络。它在平台无关性方面的任务是:为Java程序提供独立于底层主机平台的二进制形式的服务。(会产生字节码)
+
++有了字节码,无论是哪种平台(如Windows、Linux等),只要安装了虚拟机,都可以直接运行字节码。目前Java虚拟机已经可以支持很多除Java语言以外的语言了,如Groovy、JRuby、Jython、Scala等。之所以可以支持,就是因为这些语言也可以被编译成字节码。而虚拟机并不关心字节码是有哪种语言编译而来的。
+
将下面代码通过javac
命令编译
public class HelloWorld {
+ public static void main(String[] args) {
+ String s = "123";
+ }
+}
+
生成.class
文件.vi
命令查看
Êþº¾^@^@^@4^@^Q
+^@^D^@^M^H^@^N^G^@^O^G^@^P^A^@^F<init>^A^@^C()V^A^@^DCode^A^@^OLineNumberTable^A^@^Dmain^A^@^V([Ljava/lang/String;)V^A^@
+SourceFile^A^@^OHelloWorld.java^L^@^E^@^F^A^@^C123^A^@7com/example/springboot/example/security/util/HelloWorld^A^@^Pjava/lang/Object^@!^@^C^@^D^@^@^@^@^@^B^@^A^@^E^@^F^@^A^@^G^@^@^@^]^@^A^@^A^@^@^@^E*·^@^A±^@^@^@^A^@^H^@^@^@^F^@^A^@^@^@^C^@ ^@ ^@
+^@^A^@^G^@^@^@ ^@^A^@^B^@^@^@^D^R^BL±^@^@^@^A^@^H^@^@^@
+^@^B^@^@^@^E^@^C^@^F^@^A^@^K^@^@^@^B^@^L
+
++使用16进制打开class文件:使用 vim xxx.class ,然后在交互模式下,输入:%!xxd 即可。
+
00000000: cafe babe 0000 0034 0011 0a00 0400 0d08 .......4........
+00000010: 000e 0700 0f07 0010 0100 063c 696e 6974 ...........<init
+00000020: 3e01 0003 2829 5601 0004 436f 6465 0100 >...()V...Code..
+00000030: 0f4c 696e 654e 756d 6265 7254 6162 6c65 .LineNumberTable
+00000040: 0100 046d 6169 6e01 0016 285b 4c6a 6176 ...main...([Ljav
+00000050: 612f 6c61 6e67 2f53 7472 696e 673b 2956 a/lang/String;)V
+00000060: 0100 0a53 6f75 7263 6546 696c 6501 000f ...SourceFile...
+00000070: 4865 6c6c 6f57 6f72 6c64 2e6a 6176 610c HelloWorld.java.
+00000080: 0005 0006 0100 0331 3233 0100 3763 6f6d .......123..7com
+00000090: 2f65 7861 6d70 6c65 2f73 7072 696e 6762 /example/springb
+000000a0: 6f6f 742f 6578 616d 706c 652f 7365 6375 oot/example/secu
+000000b0: 7269 7479 2f75 7469 6c2f 4865 6c6c 6f57 rity/util/HelloW
+000000c0: 6f72 6c64 0100 106a 6176 612f 6c61 6e67 orld...java/lang
+
++HelloWorld.class文件中的前八个字母是cafe babe,这就是Class文件的魔数(Java中的”魔数”)
+
cafe babe 0000 0034 0011 0a00 0400 0d08
+ 魔数 此版本号 主版本号 常量池计数器 常量池计数区
+
++我们需要知道的是,在Class文件的4个字节的魔数后面的分别是4个字节的Class文件的版本号(第5、6个字节是次版本号,第7、8个字节是主版本号,我生成的Class文件的版本号是52,这时Java 8对应的版本。也就是说,这个版本的字节码,在JDK 1.8以下的版本中无法运行)在版本号后面的,就是Class常量池入口了.
+
Class常量池中主要存放两大类常量:字面量和符号引用。
+++PS 字面量前面已经记录过了,这里来记录下符号引用的概念.
+
符号引用
+符号引用是编译原理中的概念,是相对于直接引用来说的。主要包括了以下三类常量: 类和接口的全限定名 字段的名称和描述符 方法的名称和描述符.
+++符号引用 :符号引用以一组符号来描述所引用的目标。符号引用可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,符号引用和虚拟机的布局无关。
+
在编译的时候每个java类都会被编译成一个class文件,但在编译的时候虚拟机并不知道所引用类的地址,所以就用符号引用来代替, +而在这个解析阶段就是为了把这个符号引用转化成为真正的地址的阶段。
+++解析阶段: +Java类从加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括,加载 ,验证 , 准备 , 解析 , 初始化 , 卸载 ,总共七个阶段。其中验证 ,准备 , 解析 统称为连接。 +而在解析阶段会有一步将常量池当中二进制数据当中的符号引用转化为直接引用的过程。
+
在Java编译阶段,由.java
文件会生成.class
文件.Class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,用于存放编译器生成的各种字面量和符号引用。
由于不同的Class文件中包含的常量的个数是不固定的,所以在Class文件的常量池入口处会设置两个字节的常量池容量计数器,记录了常量池中常量的个数。
+Class常量池可以理解为是Class文件中的资源仓库。
+class常量池中保存了各种常量。而这些常量都是开发者定义出来,需要在程序的运行期使用的。
+++Java代码在进行Javac编译的时候,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。
+
Class是用来保存常量的一个媒介场所,并且是一个中间场所。在JVM真的运行时,需要把常量池中的常量加载到内存中.
+++运行时常量池是每一个类或接口的常量池的运行时表示形式.
+它包括了若干种不同的常量:从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。运行时常量池扮演了类似传统语言中符号表的角色,不过它存储数据范围比通常意义上的符号表要更为广泛。
+每一个运行时常量池都分配在 Java 虚拟机的方法区之中,在类和接口被加载到虚拟机后,对应的运行时常量池就被创建出来。
+
简单说来就是JVM在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中。 +我们常说的常量池,就是指方法区中的运行时常量池。
+运行时常量池类似于传统编程语言中的符号表,但是它所包含的数据却比符号表要更加丰富一些。
+当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会抛outOfMemoryError异常。
+在不同版本的JDK中,运行时常量池所处的位置也不一样.以HotSpot
为例:
+JDK1.7之前方法区位于永久代.由于一些原因在JDK1.8时彻底祛除了永久代,用元空间代替.
++根据JVM规范,JVM内存共分为虚拟机栈,堆,方法区,程序计数器,本地方法栈五个部分.
+
运行时常量池存放在JVM内存模型中的方法区中。
+++PS 方法区:
++
+- 被所有方法线程共享的一块内存区域.
+- 用于存储已经被虚拟机加载的类信息,常量,静态变量等.
+- 这个区域的内存回收目标主要针对常量池的回收和堆类型的卸载.
+
运行时常量池是方法区的一部分.class
文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,
+用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放.
运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。 +此时不再是常量池中的符号地址了,这里换为真实地址。
+运行时常量池内容包含了Class常量池中的常量和字符串常量池中的内容. +运行时常量池,相对于Class文件常量池的另一重要特征是:具备动态性。
+在JVM中,为了减少相同的字符串的重复创建,为了达到节省内存的目的。会单独开辟一块内存,用于保存字符串常量,这个内存区域被叫做字符串常量池.
+字符串常量池保存着所有字符串字面量,这些字面量在编译时期就确定.
+不同版本JDK内存分配情况:
+字符串常量池为什么要调整位置?
+JDK7中将字符串常量池放到了堆空间中。因为对永久代的回收效率很低,只有在Full GC的时候才会触发。
+Full GC 是老年代的空间不足、永久代不足时才会触发。
+这就导致字符串常量池回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。
+所以JDK7之后将字符串常量池放到堆里,能及时回收内存,避免出现OOM
错误。
简单说来:
+OOM
(OutOfMemoryError
)错误。如何证明Java8中的字符串常量池方到了堆中?
+代码演示:
+public class MainTest {
+ // 虚拟机参数: -Xmx6m -Xms6m -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
+ public static void main(String[] args) {
+ HashSet<String> hashSet = new HashSet<>();
+ short i = 0;
+ while (true) {
+ hashSet.add(String.valueOf(i++).intern());
+ }
+ }
+}
+
抛出异常证明Java8中的字符串常量池方到了堆中
+Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
+
intern是一个native方法,调用的是底层C的方法
+ /**
+ * Returns a canonical representation for the string object.
+ * <p>
+ * A pool of strings, initially empty, is maintained privately by the
+ * class {@code String}.
+ * <p>
+ * When the intern method is invoked, if the pool already contains a
+ * string equal to this {@code String} object as determined by
+ * the {@link #equals(Object)} method, then the string from the pool is
+ * returned. Otherwise, this {@code String} object is added to the
+ * pool and a reference to this {@code String} object is returned.
+ * <p>
+ * It follows that for any two strings {@code s} and {@code t},
+ * {@code s.intern() == t.intern()} is {@code true}
+ * if and only if {@code s.equals(t)} is {@code true}.
+ * <p>
+ * All literal strings and string-valued constant expressions are
+ * interned. String literals are defined in section 3.10.5 of the
+ * <cite>The Java™ Language Specification</cite>.
+ */
+ public native String intern();
+
intern
方法文档注释大意是:字符串池最初是空的,由String类私有地维护。在调用intern方法时,如果池中已经包含了由equals(object)方法确定的与该字符串对象相等的字符串,则返回池中的字符串。
+否则,该字符串对象将被添加到池中,并返回对该字符串对象的引用。
当一个字符串调用 intern()
方法时,如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals()
方法进行确定),那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用.
下面示例中,s1 和 s2 采用 new String()
的方式新建了两个不同字符串,而 s3 和 s4 是通过 s1.intern()
方法取得一个字符串引用。intern()
首先把 s1 引用的字符串放到 String Pool 中,然后返回这个字符串引用。因此 s3 和 s4 引用的是同一个字符串。
public class MainTest {
+ public static void main(String[] args) {
+ String s1 = new String("aaa");
+ String s2 = new String("aaa");
+ System.out.println(s1 == s2); // false
+ String s3 = s1.intern();
+ String s4 = s1.intern();
+ System.out.println(s3 == s4); // true
+ }
+}
+
上面的示例说明了可以使用 String的intern()
方法在程序运行过程中将字符串添加到字符串常量池中。
空间效率测试
+对于程序中大量使用存在的字符串时,尤其存在很多已经重复的字符串时,使用intern()
方法能够节省内存空间。
public class MainTest {
+ static final int MAX_COUNT = 1000 * 10000;
+ static final String[] arr = new String[MAX_COUNT];
+
+ public static void main(String[] args) {
+ Integer [] data = new Integer[]{1,2,3,4,5,6,7,8,9,10};
+ long start = System.currentTimeMillis();
+ for (int i = 0; i < MAX_COUNT; i++) {
+// arr[i] = new String(String.valueOf(data[i%data.length])); // 花费的时间为:3074
+ arr[i] = new String(String.valueOf(data[i%data.length])).intern(); // 花费的时间为:1196
+ }
+ long end = System.currentTimeMillis();
+ System.out.println("花费的时间为:" + (end - start));
+
+ try {
+ Thread.sleep(1000000);
+ } catch (Exception e) {
+ e.getStackTrace();
+ }
+ System.gc();
+ }
+}
+
不使用intern
方法
+
使用intern
方法后
+
String常量池常考的一个问题就是new String("abc")
会创建几个对象?
答案: 两个字符串对象(前提是 String常量池中还没有 “abc” 字符串对象).
+以下是 JDK8 中 String 构造函数的源码,文档注释大意是:
+++初始化新创建的String对象,使其表示与实参相同的字符序列;换句话说,新创建的字符串是实参字符串的副本。 +除非需要显式复制形参的值,否则没有必要使用这个构造函数,因为字符串是不可变的。
+
/**
+ * Initializes a newly created {@code String} object so that it represents
+ * the same sequence of characters as the argument; in other words, the
+ * newly created string is a copy of the argument string. Unless an
+ * explicit copy of {@code original} is needed, use of this constructor is
+ * unnecessary since Strings are immutable.
+ *
+ */
+public String(String original) {
+ this.value = original.value;
+ this.hash = original.hash;
+}
+
字节码分析:
+创建一个测试类,其 main 方法中使用这种方式来创建字符串对象。
+public class MainTest {
+ public static void main(String[] args) {
+ String s = new String("abc");
+ }
+}
+
使用 javap -verbose
进行反编译,得到以下内容:
// ...
+Constant pool:
+// ...
+ #2 = Class #18 // java/lang/String
+ #3 = String #19 // abc
+// ...
+ #18 = Utf8 java/lang/String
+ #19 = Utf8 abc
+// ...
+
+ public static void main(java.lang.String[]);
+ descriptor: ([Ljava/lang/String;)V
+ flags: ACC_PUBLIC, ACC_STATIC
+ Code:
+ stack=3, locals=2, args_size=1
+ 0: new #2 // class java/lang/String
+ 3: dup
+ 4: ldc #3 // String abc
+ 6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
+ 9: astore_1
+// ...
+
在 Constant Pool
中,#19 存储这字符串字面量 "abc"
,#3 是 String Pool 的字符串对象,它指向 #19
这个字符串字面量。
+在 main
方法中,0:
行使用 new #2
在堆中创建一个字符串对象,并且使用 ldc #3
将 String Pool 中的字符串对象作为 String 构造函数的参数。
+所以能看到使用new String()
的方式创建字符串是创建两个对象。
使用new String("a") + new String("b")
会创建几个对象?
答案:会创建6个对象:
+new StringBuilder()
对象;new String("a")
会从堆空间创建一个对象;new String("b")
会从堆空间创建一个对象;new StringBuilder().append()
添加完字符串后,会通过StringBuilder.toString()
中会创建一个 new String("ab")
;
+但是调用toString()
方法,不会在常量池中生成ab字符串;也可跟上面的代码一样从字节码角度进行分析,这里不做过多分析。
+扩展:在JDK1.6/JDK1.7中下列代码执行结果不同
+public class MainTest {
+ public static void main(String[] args) {
+ String s3 = new String("1") + new String("1"); // ==> new String("11"); 并不会在字符串常量池创建 "11"
+ // 在Jdk7及之后,JVM为了节省空间进行优化,在字符串常量池中的对象只保存了堆中对象的地址,而并不是创建了一个真正的对象;
+ // "11"不在字符串常量池中 ,把"11"放入字符串常量池中 因为堆中已经存在对象,JDK6在字符串池中保存的是一个真正的对象 JDK7保存的是一个引用地址
+ s3.intern();
+ // 获取字符串常量池的引用
+ String s4 = "11";
+ System.out.println(s3 == s4); // JDK1.6: false JDK1.7: true
+ }
+}
+
原因是因为字符串常量池在JDK1.7的时候被放在了堆中,而JDK1.6被放在了永久区中;
+JDK1.6创建对象被放在了永久区中的字符串常量池,并非堆中,JVM无法对其进行优化;
+在JDK1.7及之后,new String("1")
会在堆中创建一个对象并且会在字符串常量池中创建一个对象;
+因为在堆中已经创建了对象,JVM为了节省空间,在字符串常量池中的对象只保留了引用堆中对象的地址,而并不是创建了一个真正的对象;
JDK1.6中,将这个字符串对象尝试放入串池。
+JDK1.7起,将这个字符串对象尝试放入串池。
+字符串常量池是不会存储相同内容的字符串的。
+字符串常量池其实是一个固定大小的Hashtable
;
+如果放进字符串常量池的string非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用string.intern时性能会大幅下降。
使用VM参数-XX:StringTablesize
可设置字符串常量池的的长度;
不同JDK版本字符串常量池的的长度:
+-XX:StringTablesize
设置没有要求,可随意设置;-XX:StringTablesize
设置没有要求,可随意设置;可以配置不同版本的JRE,运行下列代码,然后使用 jsp(获取程序进程ID) -> jinfo -flag StringTableSize 进程ID
命令进行查看stringTableSize
public class MainTest {
+ public static void main(String[] args) {
+ try {
+ Thread.sleep(1000000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+}
+
一个Java源文件中的类、接口,编译后产生一个字节码文件。 +而Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池,这个字节码包含了指向常量池的引用。 +在动态链接的时候会用到运行时常量池。
+比如:如下的代码:
+public class SimpleClass {
+ public void sayHello() {
+ System.out.println("hello");
+ }
+}
+
虽然上述代码生成的class文件只有194字节,但是里面却使用了String、System、PrintStream及Object等结构。 +这里的代码量其实很少了,如果代码多的话,引用的结构将会更多,这样就需要用到常量池了。
+常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享. +避免了相同内容的字符串的创建,节省了内存,省去了创建相同字符串的时间,同时提升了性能.
+String是Java中一个比较基础的类.广泛应用 在 Java 编程中,在Java中字符串属于对象,Java 提供了 String 类来创建和操作字符串.
+String被声明为final
,因此它不可被继承(当然Integer等包装类也不能被继承);
String实现了Serializable接口:表示字符串是支持序列化的。实现了Comparable接口:表示string可以比较大小;
+在 Java 8 中,String 内部使用 char 数组存储数据。
+public final class String
+ implements java.io.Serializable, Comparable<String>, CharSequence {
+ /** The value is used for character storage. */
+ private final char value[];
+}
+
在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 coder 来标识使用了哪种编码。
+public final class String
+ implements java.io.Serializable, Comparable<String>, CharSequence {
+ /** The value is used for character storage. */
+ private final byte[] value;
+
+ /** The identifier of the encoding used to encode the bytes in {@code value}. */
+ private final byte coder;
+}
+
那么为什么要改变内存存储结构呢? +官方的解释:
+++The current implementation of the String class stores characters in a char array, using two bytes (sixteen bits) for each character. +Data gathered from many different applications indicates that strings are a major component of heap usage and, +moreover, that most String objects contain only Latin-1 characters. Such characters require only one byte of storage, +hence half of the space in the internal char arrays of such String objects is going unused.
+We propose to change the internal representation of the String class from a UTF-16 char array to a byte array plus an encoding-flag field. +The new String class will store characters encoded either as ISO-8859-1/Latin-1 (one byte per character), +or as UTF-16 (two bytes per character), based upon the contents of the string. +The encoding flag will indicate which encoding is used.
+
大意:String类的当前实现将字符存储在char数组中,每个字符使用两个字节(16位)。 +从许多不同的应用程序收集的数据表明,字符串是堆使用的主要组成部分,而且,大多数字符串对象只包含拉丁字符。 +这些字符只需要一个字节的存储空间,因此这些字符串对象的内部char数组中有一半的空间将不会使用。
+我们建议将String类的内部表示从UTF-16字符数组更改为一个字节数组加上一个编码标志字段。 +新的String类将根据字符串的内容存储编码为ISO-8859-1/Latin-1(每个字符一个字节)或UTF-16(每个字符两个字节)的字符。 +编码标志将指示所使用的编码。
+简单说来就是用char数组存,有些数据占不了两个字节,所以为了节约资源改成byte数组存放; +如果存放的不是拉丁文则需要占两个字节,所以还需要加上编码标记。
+基于String的数据结构,StringBuffer
和StringBuilder
也同样做了修改
value数组被声明为final
,这意味着value数组初始化之后就不能再引用其它数组。
+并且 String 内部没有改变 value 数组的方法,因此可以保证String
不可变;
+String是Java中一个不可变的类,所以他一旦一个string对象在内存(堆)中被创建出来,他就无法被修改。
下面代码说明String是不可变的:
+public class MainTest {
+ String str = new String("good");
+ char [] ch = {'t','e','s','t'};
+
+ public void change(String str, char ch []) {
+ str = "test ok";
+ System.out.println( "chanage方法里str=" + str); // chanage方法里str=test ok
+ ch[0] = 'b';
+ }
+
+ public static void main(String[] args) {
+ MainTest ex = new MainTest();
+ ex.change(ex.str, ex.ch);
+ System.out.println(ex.str); // good
+ System.out.println(ex.ch);// best
+ }
+}
+
特别要注意的是,String类的所有方法都没有改变字符串本身的值,都是返回了一个新的对象。字符串不可变的根本原因应是处于安全性考虑。
+不可变性优点:
+因为String
的hash
值经常被使用,像Set、Map
结构中的 key 值也需要用到HashCode
来保证唯一性和一致性,因此不可变的 HashCode
才是安全可靠的
字符串常量池的基础就是字符串的不可变性,如果字符串是可变的,那想一想,常量池就没必要存在了。
+如果一个 String
对象已经被创建过了,那么就会从String常量池中取得引用。只有
String`是不可变的,才可能使用 String常量池。
String
经常作为参数,String
不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果String
是可变的,那么在网络连接过程中,String
被改变,改变String
对象的那一方以为现在连接的是其它主机,而实际情况却不一定是.
+实际项目中会用到,比如数据库连接串、账号、密码等字符串,只有不可变的连接串、用户名和密码才能保证安全性.
字符串在 Java 中的使用频率可谓高之又高,那在高并发的情况下不可变性也使得对字符串的读写操作不用考虑多线程竞争的情况. +String不可变性天生具备线程安全,可以在多个线程中安全地使用.
+// String 直接创建
+String str1 = "123";
+// String 对象创建
+String str2 = new String("123");
+// 引用创建
+String str3 = str1;
+
格式化创建字符串
+String str = String.format("浮点型变量: " +
+ "%f, 整型变量: " +
+ " %d, 字符串变量: " +
+ " %s", 1.0f, 1, "1");
+System.out.println(str);
+
通过字面量的方式(区别于new)给一个字符串赋值,存储在String常量池中,而 new 创建的字符串对象在堆上.
+因为String类是不可变的.所以所谓字符串拼接,都是重新生成了一个新的字符串.
+以下是字符串的几种拼接的方式
+// 原字符串
+String str1 = "123";
+
+//1. concat 方法连接字符串
+String str2 = "123".concat("456");
+System.out.println(str2);
+
+//2. 用 "+" 连接字符串
+// 在编译时会编译成 String str4 = "123456";
+String str3 = "123" + "456";
+// 会调用StringBuilder.append方法
+String str4 = str3 + "789";
+System.out.println(str3);
+System.out.println(str4);
+
+//3. 用JDK内置处理字符串类 Stringbuilder, StringBuffer
+StringBuilder builder = new StringBuilder();
+StringBuffer buffer = new StringBuffer();
+String str5 = builder.append(str).append("456").toString();
+String str6 = buffer.append(str).append("456").toString();
+System.out.println(str5);
+System.out.println(str6);
+
+//4. Java8新增String.join方法连接字符串
+List<String> list = new ArrayList<>();
+list.add("1");
+list.add("2");
+list.add("3");
+String str7 = String.join("-", list);
+String str8 = String.join("~", new String[]{"1","2","3"}r);
+System.out.println(str7);
+System.out.println(str8);
+
+//5. 用第三方处理字符串工具类. 例如: apache.commons.
+String str9 = StringUtils.join(new String[]{str, "456", "789"});
+
+// Java8提供的String.join方法和`apache.commons`方法相似.
+// 主要作用是将数组或集合以某拼接符拼接到一起形成新的字符串
+
拼接字符串最简单的方式就是直接使用符号+来拼接. + 是Java提供的一个语法糖.
+++语法糖:语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性。
+
public static void main(String[] args) {
+ String str = "12" + "3";
+ }
+
两个都为编译期常量,编译器会进行常量折叠变为String str = "123";
如果拼接符号的前后出现了变量,则相当于在堆空间中new String()
(这里的堆指的是除了字符串常量池外的区域),具体的内容为拼接的结果.
public static void main(String[] args) {
+ String str1 = "123";
+ String str2 = "456";
+ String str3 = str1 + str2;
+ System.out.println(str3);
+ }
+
打开文件所在位置,用javap -verbose
命令进行反编译.
// ...
+ public static void main(java.lang.String[]);
+ descriptor: ([Ljava/lang/String;)V
+ flags: ACC_PUBLIC, ACC_STATIC
+ Code:
+ stack=2, locals=4, args_size=1
+ 0: ldc #2 // String 123
+ 2: astore_1
+ 3: ldc #3 // String 456
+ 5: astore_2
+ 6: new #4 // class java/lang/StringBuilder
+ 9: dup
+ 10: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
+ 13: aload_1
+ 14: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
+ 17: aload_2
+ 18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
+ 21: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
+ 24: astore_3
+ 25: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
+ 28: aload_3
+ 29: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
+ 32: return
+ // ...
+
可以看到在字符串拼接过程中,是将String转成了StringBuilder
后,使用其append
方法进行拼接字符串处理的.最后在去调用StringBuilder
的toString
方法进行返回
++在JDK5之后,使用的是StringBuilder,在JDK5之前使用的是StringBuffer
+
但是如果使用final修饰String变量时,在VM编译过程中,仍然被编译器优化;
+public class MainTest {
+ public static void main(String[] args) {
+ final String a = "1";
+ final String b = "2";
+ String ab = a + b; // 在class文件中会优化为: String ab = 12;
+ String ab0 = "12";
+ System.out.println(ab == ab0);
+ }
+}
+
在开发中,能够使用final的时候,建议使用上。
+源码
+ public String concat(String str) {
+ // 获取字符串长度
+ int otherLen = str.length();
+ // 判空
+ if (otherLen == 0) {
+ return this;
+ }
+ // 获取已有字符串长度
+ int len = value.length;
+ // 创建新的字符数组.
+ // 长度为:已有字符串+待拼接字符长度.
+ // 将两个字符串的值复制到新的字符数组
+ char buf[] = Arrays.copyOf(value, len + otherLen);
+ str.getChars(buf, len);
+ // 创建新的字符串对象
+ return new String(buf, true);
+ }
+
Stringbuilder
和StringBuffer
共同的父类:
abstract class AbstractStringBuilder implements Appendable, CharSequence {
+ /**
+ * The value is used for character storage.
+ */
+ char[] value;
+
+ /**
+ * The count is the number of characters used.
+ */
+ int count;
+
+ // ...
+}
+
用于存贮的value
并没有用final
修饰,说明是可以修改的.还有一个属性字段count
,用来保存数组中已经使用的字符个数.
append方法
+StringBuilder.append
方法
@Override
+ public StringBuilder append(String str) {
+ super.append(str);
+ return this;
+ }
+
StringBuffer.append
方法
@Override
+ public synchronized StringBuffer append(String str) {
+ toStringCache = null;
+ super.append(str);
+ return this;
+ }
+
由此可以看出StringBuilder
和StringBuffer
原理是相似的,最大的区别就是StringBuffer
是线程安全的.原因是用了synchronized
修饰.
append
方法原理的在父类中.需要注意的是,如果append
方法append(null)
会直接拼接字符串"null”
public AbstractStringBuilder append(String str) {
+ // 判空
+ if (str == null)
+ /**
+ * final char[] value = this.value;
+ * value[c++] = 'n';
+ * value[c++] = 'u';
+ * value[c++] = 'l';
+ * value[c++] = 'l';
+ */
+ // 如果是'null' 则返回 字符串 "null"
+ return appendNull();
+ // 获取字符长度
+ int len = str.length();
+ /** if (count + len - value.length > 0) {
+ // 拷贝字符到内部的字符数组中.如果字符数组长度不够,会进行扩展
+ value = Arrays.copyOf(value,newCapacity(count+len));
+ }
+ **/
+ ensureCapacityInternal(count + len);
+ // 复制数组
+ // System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
+ str.getChars(0, len, value, count);
+ // 拼接字符长度
+ count += len;
+ return this;
+ }
+
与 String 类不同的是,StringBuffer
和 StringBuilder
类的对象能够被多次的修改,并且不产生新的未使用对象。
如果你需要一个可修改的字符串,应该使用StringBuffer
或者 StringBuilder
。但是会有大量时间浪费在垃圾回收上,因为每次试图修改都有新的String
对象被创建出来.
时间比较(短->长):
+StringBuilder
<StringBuffer
<concat
<+
<StringUtils.join
使用注意:
+如果在并发场景中进行字符串拼接的话,要使用StringBuffer
来代替StringBuilder
.
如果不是在循环体中进行字符串拼接的话,直接使用+就好了,如果在循环体内使用"+“拼接字符串对象会在每一次循环都会创建StringBuilder
对象,导致程序效率降低.
+如下代码:
public static void method1(int highLevel) {
+ String src = "";
+ for (int i = 0; i < highLevel; i++) {
+ src += "a"; // 每次循环都会创建一个StringBuilder对象
+ }
+ }
+
+ public static void method2(int highLevel) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < highLevel; i++) {
+ sb.append("a");
+ }
+ }
+
通过StringBuilder
的append()
方式添加字符串的效率,要远远高于String
的字符串拼接方法.
在实际开发中还可以进行优化,StringBuilder
的空参构造器,默认的字符串容量是16,如果需要存放的数据过多,容量就会进行扩容,我们可以设置默认初始化更大的长度,来减少扩容的次数。
如果我们能够确定,前前后后需要添加的字符串不高于某个限定值,那么建议使用构造器创建一个阈值的长度。
+基于Java8整理.如果查看其他方法请参照Java8API官方文档 java.lang.String
对字符串进行截取.返回一个新的字符串,它是此字符串的一个子字符串.
+源码
+ public String substring(int beginIndex) {
+ // 判空
+ if (beginIndex < 0) {
+ throw new StringIndexOutOfBoundsException(beginIndex);
+ }
+ // 需要截取的长度不能超过源字符的长度
+ int subLen = value.length - beginIndex;
+ if (subLen < 0) {
+ throw new StringIndexOutOfBoundsException(subLen);
+ }
+ // 如果传入的长度不等于被截字符串的长度 则创建新的字符串
+ return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
+ }
+
+ public String substring(int beginIndex, int endIndex) {
+ if (beginIndex < 0) {
+ throw new StringIndexOutOfBoundsException(beginIndex);
+ }
+ if (endIndex > value.length) {
+ throw new StringIndexOutOfBoundsException(endIndex);
+ }
+ int subLen = endIndex - beginIndex;
+ if (subLen < 0) {
+ throw new StringIndexOutOfBoundsException(subLen);
+ }
+ return ((beginIndex == 0) && (endIndex == value.length)) ? this
+ : new String(value, beginIndex, subLen);
+ }
+
使用
+ public static void main(String[] args) {
+ String str = "123456";
+ String substring = str.substring(2);
+ // 3456
+ System.out.println(substring);
+ // 索引从1开始截取字符串
+ String substring2 = str.substring(2,4);
+ // 34
+ System.out.println(substring2);
+ }
+
替换字符串.返回一个新的字符串,它是通过用 newChar
替换此字符串中出现的所有 oldChar
得到的.
源码
+ public String replace(char oldChar, char newChar) {
+ // 校验: 需要进行替换新旧字符不能相同
+ if (oldChar != newChar) {
+ // 获取源字符串中的长度
+ int len = value.length;
+ int i = -1;
+ // 获取源字符串
+ char[] val = value; /* avoid getfield opcode */
+ // 判断源字符串是否存在 需要替换的旧字符
+ while (++i < len) {
+ if (val[i] == oldChar) {
+ break;
+ }
+ }
+ if (i < len) {
+ // 创建新的字符数组 用于替换后保存新字符串
+ char buf[] = new char[len];
+ for (int j = 0; j < i; j++) {
+ // 将旧字符存入到新字符数组
+ buf[j] = val[j];
+ }
+ while (i < len) {
+ char c = val[i];
+ // 替换字符
+ buf[i] = (c == oldChar) ? newChar : c;
+ i++;
+ }
+ // 创建新字符串对象
+ return new String(buf, true);
+ }
+ }
+ return this;
+ }
+
使用
+ public static void main(String[] args) {
+ String str = "123456";
+ // 223456
+ System.out.println(str.replace('1', '2'));
+ }
+
使用给定的 replacement
替换此字符串所有匹配给定的正则表达式的子字符串.
源码
+ public String replaceAll(String regex, String replacement) {
+ return Pattern.compile(regex).matcher(this).replaceAll(replacement);
+ }
+
+
// complile 解析正则表达式 获得 Pattern对象
+ public static Pattern compile(String regex) {
+ return new Pattern(regex, 0);
+ }
+
+ //matcher 获取匹配器对象
+ public Matcher matcher(CharSequence input) {
+ if (!compiled) {
+ synchronized(this) {
+ if (!compiled)
+ compile();
+ }
+ }
+ Matcher m = new Matcher(this, input);
+ return m;
+ }
+
+ // replaceAll 进行字符串替换
+ public String replaceAll(String replacement) {
+ // 对当前Matcher类进行重置,即对其中记录匹配结果的开始和结束位置索引,以及分组信息重置
+ reset();
+ // 执行第一次搜索
+ boolean result = find();
+ // 第一次搜索匹配成功
+ if (result) {
+ // 用于记录最终的替换结果字符串
+ StringBuffer sb = new StringBuffer();
+ do {
+ // 重点:用于将从上一次匹配子字符串的下一个索引位置开始,到当前匹配的子字符串的结束索引位置的所有字符 append到字符串sb中
+ appendReplacement(sb, replacement);
+ result = find();
+ } while (result);
+ // 将从最后一次匹配子字符串的下一个索引位置,到字符串的结尾的所有字符append到字符串sb中
+ appendTail(sb);
+ return sb.toString();
+ }
+ return text.toString();
+ }
+
重点看一下replaceAll
中调用的appendReplacement
方法
public Matcher appendReplacement(StringBuffer sb, String replacement) {
+
+ // ...
+
+ // 用于跟踪 replacement 字符串的索引
+ int cursor = 0;
+
+ // 对当前匹配到子字符串替换后的结果字符串
+ StringBuffer result = new StringBuffer();
+
+ // 遍历 replacement字符串
+ while (cursor < replacement.length()) {
+
+ char nextChar = replacement.charAt(cursor);
+
+ if (nextChar == '\\') {
+ // 重点1:当字符为'\'时,跳过,并获取其后面的字符,追加到result
+ cursor++;
+ nextChar = replacement.charAt(cursor);
+ result.append(nextChar);
+ cursor++;
+ } else if (nextChar == '$') {
+
+ // 重点2:当字符为$时,跳过,并获取其后面的数值,并以此如果$后面第一个不为数字则抛异常,
+ // Skip past $
+ cursor++;
+
+ // The first number is always a group
+ int refNum = (int)replacement.charAt(cursor) - '0';
+
+ // 此处代码用于计算$符号后的数值,数值结果赋予 refNum
+ // ...
+
+ // group(refNum) 用于获取正则表达式第refNum个分组表示的字符串,不详说了
+ if (group(refNum) != null)
+ // 追加到result
+ result.append(group(refNum));
+ } else {
+
+ // 当前字符不为\ 或 $ 则直接追加到result
+ result.append(nextChar);
+ cursor++;
+ }
+ }
+
+ // 将从上一次匹配的子字符串的结尾索引,到当前匹配的第一个字符串索引的字符串追加到sb
+ // lastAppendPosition参数为上一次执行appendReplacement方法最后追加的字符在原始字符串中的索引位置。
+ // first 参数为当前待替换的子字符串的首个字符在原始字符串中的索引位置
+ sb.append(getSubSequence(lastAppendPosition, first));
+
+ // 将当前配置子字符串替换后的结果字符串追加到sb
+ sb.append(result.toString());
+
+ // 更新lastAppendPosition,供下一个匹配执行appendReplacement方法使用
+ lastAppendPosition = last;
+
+ /* sb中追加了当前匹配的子字符串与前一次匹配子字符串中间的字符,以及当前匹配子字符串被替换后的字符串
+ */
+ return this;
+ }
+
replaceAll
中第二个参数replacement
中,\ 有转义的作用, $ 用于获取分组匹配的当前子字符串 因为引入了 $ 符的分组功能,所以为了解决能输出 $ 字符,故引入 \ 转义功能.
使用
+ public static void main(String[] args) {
+ String str = "111111";
+ // 222222
+ System.out.println(str.replaceAll("1", "2"));
+ }
+
该方法作用是将对象转成String类型.
+源码
+ public static String valueOf(Object obj) {
+ return (obj == null) ? "null" : obj.toString();
+ }
+
使用
+ public static void main(String[] args) {
+ Integer integer = 11111;
+ String str = String.valueOf(integer);
+ // 11111
+ System.out.println(str);
+ }
+
翻阅String源码在String源码中发现有定义字符串长度的构造函数
+ // count 就是 字符串定义长度
+ public String(char value[], int offset, int count) {
+ if (offset < 0) {
+ throw new StringIndexOutOfBoundsException(offset);
+ }
+ if (count <= 0) {
+ if (count < 0) {
+ throw new StringIndexOutOfBoundsException(count);
+ }
+ if (offset <= value.length) {
+ this.value = "".value;
+ return;
+ }
+ }
+ // Note: offset or count might be near -1>>>1.
+ if (offset > value.length - count) {
+ throw new StringIndexOutOfBoundsException(offset + count);
+ }
+ this.value = Arrays.copyOfRange(value, offset, offset+count);
+ }
+
通过源码可以看到int
的最大长度就是String
的支持的最大长度.
public static void main(String[] args) {
+ // 2,147,483,648 = 2^31 - 1
+ System.out.println(Integer.MAX_VALUE);
+ }
+
注意new String(char value[], int offset, int count)
是运行时String
支持的最大长度.
在String
编译声明期间,用javac
编译 长度为2^31 -1
的字符串.
public static void main(String[] args) {
+ // 长度: 2^31 -1
+ String str = "1111 ... ";
+ System.out.println(str);
+ }
+
java: 常量字符串过长
+
在Gen
类中相关报错信息源码
private void checkStringConstant(DiagnosticPosition var1, Object var2) {
+ if (this.nerrs == 0 && var2 != null && var2 instanceof String && ((String)var2).length() >= 65535) {
+ this.log.error(var1, "limit.string", new Object[0]);
+ ++this.nerrs;
+ }
+}
+
可以看到源码中如果String
长度大于等于65535会导致编译失败
在编译期的时候,字面量要进字符串常量池.所以要遵守《Java®虚拟机规范》(Java8)中对String常量池的描述.
+CONSTANT_String_info
用于表示 java.lang.String
类型的常量对象结构体
CONSTANT_String_info
格式如下:
CONSTANT_String_info {
+ u1 tag;
+ u2 string_index;
+}
+
++tag
+
+结构CONSTANT_String_info
的标签项的值为CONSTANT_String(8)
++string_index
+
+string_index
项的值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info
结构,表示一组Unicode
码点序列,这组 Unicode 码点序列最终会被初始化为一个 下Unicode
对象
CONSTANT_Utf8_info
是一个CONSTANT_Utf8
类型的常量池数据项,它存储的是一个常量字符串。常量池中的所有字面量几乎都是通过CONSTANT_Utf8_info
描述的。CONSTANT_Utf8_info
的定义如下:
ONSTANT_Utf8_info {
+ u1 tag;
+ u2 length;
+ u1 bytes[length];
+}
+
length
则指明了 bytes
数组的长度,其类型为u2
通过查阅《JVM规范》发现u2
表示两个字节的无符号数,那么1个字节有8位,2个字节就有16位。16位无符号数可表示的最大值位2^16 - 1 = 65535
。也就是说,Class
文件中常量池的格式规定了,其字符串常量的长度不能超过65535。
关于编译器字符串最大长度65534的问题
+++如果一个方法的Java虚拟机代码长度正好是65535字节,并且以一个1字节长的指令结束,那么该指令不能被异常处理程序保护。编译器作者可以通过将任何方法、实例初始化方法或静态初始化器(任何代码数组的大小)生成的Java虚拟机代码的最大大小限制为65534字节来解决这个问题
+
简单来说
+2^31 -1
String
JVM常量池规范String字符串在声明时最大为 65535,但是为了修复Java
的遗留问题改为65534在程序开发中,需要注意如果你用String
变量接收Base64图片或音频视频需要注意不要超过在程序运行时字符串的最大阈值.
因为全世界有很多编程人员,有很多语言,不同的国家使用不同的语言,如果说没有一套统一的编码规则,这么多语言混在一起,很容易出现乱码现象,本着既方便又节约内存的理念大家基本都是用utf-8
码来编写程序.
++Unicode(中文:万国码、国际码、统一码、单一码)是计算机科学领域里的一项业界标准。它对世界上大部分的文字系统进行了整理、编码,使得计算机可以用更为简单的方式来呈现和处理文字.
+
++Unicode伴随着通用字符集的标准而发展,同时也以书本的形式对外发表。Unicode至今仍在不断增修,每个新版本都加入更多新的字符。目前最新的版本为2018年6月5日公布的11.0.0,已经收录超过13万个字符(第十万个字符在2005年获采纳)。Unicode涵盖的数据除了视觉上的字形、编码方法、标准的字符编码外,还包含了字符特性,如大小写字母。
+
Unicode
是一种编码规范,是为解决全球字符通用编码而设计的,而UTF-8 UTF-16
等是这种规范的一种实现.Unicode
是字符集而UTF-8
是编码规则。
Java内部采用Unicode
编码规范,也就是支持多语言的,具体采用的UTF-16
编码方式.
不管程序过程中用到了GBK
还是ISO8859-1
等格式,在存储与传递的过程中实际传递的都是Unicode
编码的数据,要想接收到的值不出现乱码,就要保证传过去的时候用的是X编码,接收的时候也用X编码来转换接收
编码时格式和解码时格式不一致.
+string
在文件里面底层保存形式是二进制,底层用byte[]
数组存储(Java9. Java8是用char
数组储存).byte[]
数组里面的内容可以按照不同的编码格式存放.在读取字符串的时候,也可以按照不同的解码格式存放.这样就造成了乱码.
简单理解为
+在编码(字符串到字节)的时候是用一种编码;而在解码(从字节到字符串)的时候用另一种编码;所以导致乱码问题.所以想要避免乱码问题最简单的办法就是从始至终,都用同一种字符格式
+String
类有两种比较常用的操作编码方式
// 注意处理异常
+ public static void main(String[] args) throws UnsupportedEncodingException {
+ // 本地使用的是 utf-8 的编码
+ String str = "你好";
+ byte[] bytes = str.getBytes("utf-8");
+ // 你好
+ System.out.println(new String(bytes));
+ String string = new String(str.getBytes(), "utf-8");
+ // 你好
+ System.out.println(string);
+ }
+
该方法会根据指定的decode
编码返回某字符串在该编码下的byte
数组表示
源码
+ public byte[] getBytes(String charsetName)
+ throws UnsupportedEncodingException {
+ if (charsetName == null) throw new NullPointerException();
+ return StringCoding.encode(charsetName, value, 0, value.length);
+ }
+
StringCoding.encode
方法
// len: 当前字符串长度
+static byte[] encode(String charsetName, char[] ca, int off, int len)
+ throws UnsupportedEncodingException
+ {
+ StringEncoder se = deref(encoder);
+ // 如果为空 默认ISO-8859-1
+ String csn = (charsetName == null) ? "ISO-8859-1" : charsetName;
+ //
+ if ((se == null) || !(csn.equals(se.requestedCharsetName())
+ || csn.equals(se.charsetName()))) {
+ se = null;
+ try {
+ // 根据编码获取 Charset对象
+ Charset cs = lookupCharset(csn);
+ if (cs != null)
+ se = new StringEncoder(cs, csn);
+ } catch (IllegalCharsetNameException x) {}
+ if (se == null)
+ throw new UnsupportedEncodingException (csn);
+ set(encoder, se);
+ }
+ return se.encode(ca, off, len);
+ }
+
该方法为字节数组构造
+char[]
数组是以unicode
码来存储的,String
和char
为内存形式.byte
是网络传输或存储的序列化形式.可以通过charset
来解码指定的byte
数组,将其解码成unicode
的char[]
数组,构造String
.
源码
+ public String(byte bytes[], String charsetName)
+ throws UnsupportedEncodingException {
+ this(bytes, 0, bytes.length, charsetName);
+ }
+
public String(byte bytes[], int offset, int length, String charsetName)
+ throws UnsupportedEncodingException {
+ if (charsetName == null)
+ throw new NullPointerException("charsetName");
+ checkBounds(bytes, offset, length);
+ this.value = StringCoding.decode(charsetName, bytes, offset, length);
+ }
+
static char[] decode(String charsetName, byte[] ba, int off, int len)
+ throws UnsupportedEncodingException
+ {
+ StringDecoder sd = deref(decoder);
+ String csn = (charsetName == null) ? "ISO-8859-1" : charsetName;
+ if ((sd == null) || !(csn.equals(sd.requestedCharsetName())
+ || csn.equals(sd.charsetName()))) {
+ sd = null;
+ try {
+ Charset cs = lookupCharset(csn);
+ if (cs != null)
+ sd = new StringDecoder(cs, csn);
+ } catch (IllegalCharsetNameException x) {}
+ if (sd == null)
+ throw new UnsupportedEncodingException(csn);
+ set(decoder, sd);
+ }
+ return sd.decode(ba, off, len);
+ }
+
+ +
+ + + + + +Throwable
可以用来表示任何可以作为异常抛出的类,分为两种:Error
和 Exception
。
其中 Error
用来表示Java程序无法处理的错误;这类错误一般与硬件有关,与程序本身无关,通常由系统进行处理,程序本身无法捕获和处理。是不可控制的。
Exception
分为两种:运行时异常和检查型异常。
try...catch...
语句捕获并进行处理,并且可以从异常中恢复;
+public void test() throws MyException{}
+
catch
,必须得进行捕获,否则编译不过去。Java认为检查性异常都可以被处理,所以必须显示的处理 checked
异常。
+常见的检查性异常有IOException
,SqlException
。
+当我们希望我们的⽅法调⽤者, 明确的处理⼀些特殊情况的时候, 就应该使⽤受检异常。ArithmeticException
,此时程序崩溃并且无法恢复。
+public void test() {
+ int a = 1;
+ int b = a/0;
+}
+
Exception
表⽰程序需要捕捉、 需要处理的常,是由与程序设计的不完善⽽出现的问题,程序必须处理的问题。
++异常和错误的区别:异常(Exception)能被程序本身可以处理,错误(Error)是无法处理。
+
在 Java 中可以自定义异常。编写自己的异常类时需要注意:
+Throwable
的子类。Exception
类。RuntimeException
类。大多数情况下都会继承RuntimeException
,自定义运行时异常。
public class MainTest {
+
+ void context() throws TestException {
+ throw new TestException();
+ }
+
+ void context2() {
+ throw new Test2Exception();
+ }
+}
+// TestException 为受检异常 程序必须要处理否则编译不通过
+class TestException extends Exception {
+}
+
+// Test2Exception为运行时异常 可以不进行处理
+class Test2Exception extends RuntimeException{
+}
+
++异常链是Java中⾮常流⾏的异常处理概念, 是指在进⾏⼀个异常处理时抛出了另外⼀个异常, 由此产⽣了⼀个异常链条。
+
如果因为因为异常你决定抛出⼀个新的异常, 你⼀定要包含原有的异常, 这样, 处理程序才可以通过getCause()
和initCause()
⽅法来访问异常最终的根源。
public class MainTest {
+ public static void main(String[] args) {
+ try {
+ int a = 1;
+ int b = a / 0;
+ } catch (ArithmeticException e) {
+ throw new RuntimeException("除以零异常", e);
+ }
+ }
+}
+
在此示例中,当捕获到ArithmeticException
时,将创建一个新的RuntimeException
异常,并附加原始的异常原因,并将异常链抛出到下一个更高级别的异常处理程序。
异常的处理⽅式有两种:
+不要丢弃异常,捕获异常后需要进行相关处理。如果用户觉得不能很好地处理该异常,就让它继续传播,传到别的地方去处理,或者把一个低级的异常转换成应用级的异常,重新抛出。
+千万不能捕获了之后什么也不做。 或者只是使⽤e.printStacktrace
。如果是练习这样写也就算了,但是在正式的环境上不能这样做。正式环境请使用日志记录。
++写完代码后请一定要检查下,代码中千万不要有printStackTrace()。因为printStackTrace()只会在控制台上输出错误的堆栈信息,他只适合于用来代码调试。
+
catch
语句应该指定具体的异常类型。不能把不该捕获的异常也捕获了;如果finally
里面也会抛出异常,也一样需要使用try..catch
处理。
在Java中如果需要处理异常,必须先对异常通过try..catch..
进行捕获,然后再对异常情况进行处理。
try{
+ // 程序代码
+}catch(异常类型1 异常的变量名1){
+ // 程序代码
+}
+
一个 try
对应多个 catch
,进行多重捕获。
可以在 try
语句后面添加任意数量的 catch
块。如果发生异常,异常被抛给第一个 catch
块。如果不匹配,它会被传递给第二个 catch
块。
+如此,直到异常被捕获或者通过所有的 catch
块。
try{
+ // 程序代码
+}catch(异常类型1 异常的变量名1){
+ // 程序代码
+}catch(异常类型2 异常的变量名2){
+ // 程序代码
+}catch(异常类型3 异常的变量名3){
+ // 程序代码
+}
+
在JDK7之后,可以将 catch
语句块折叠,这意味着可以将多个不同类型的异常合并处理。
try{
+ // 程序代码
+} catch (异常类型1 | 异常类型2 | 异常类型3 e) {
+ // 程序代码
+}
+
try..catch..
通常连用用来捕获异常;try..catch..finally..
也可以连用。
try{
+ // 程序代码
+ return a;
+}catch(异常类型2 异常的变量名2){
+ // 程序代码
+ return b;
+}finally{
+ // 程序代码
+ return c;
+}
+
对于try..catch..finally..
语句块中执行顺序的解释
根据JVM规范:
+try
语句块里边有返回值则返回 try
语句块里边的;try
语句块和 finally
语句块都有 return
,则忽略 try
语句块里边的使用 finally
语句块里边的 return
;finally
语句块是在 try
语句块或者 catch
语句块中的 return
语句之前执行的;finally
代码块中的代码总会被执行;如果方法有返回值,切忌不要再 finally
中使用 return
,这样会使得程序结构变得混乱。
++finally语句块什么时候不执行 ?
+如果当一个线程在执行 try 语句块或者 catch 语句块时被打断(interrupted)或者被终止(killed)或退出虚拟机(
+System.exit(0)
),与其相对应的 finally 语句块可能不会执行。 +还有更极端的情况,就是在线程运行 try 语句块或者 catch 语句块时,突然死机或者断电,finally 语句块肯定不会执行了。
JVM先会把try
或者 catch
代码块中的返回值保留,再来执行 finally
代码块中的语句,等到 finally
代码块执行完毕之后,在把之前保留的返回值给返回出去。
+这条规则(保留返回值),只适用于 return
和 throw
语句,不适用于 break
和 continue
语句,因为它们根本就没有返回值。
public class MyTest {
+
+ public static void main(String[] args) {
+ // main 代码块中的执行结果为:1
+ System.out.println("main 代码块中的执行结果为:" + myMethod());
+ }
+
+ public static int myMethod() {
+ int i = 1;
+ try {
+ System.out.println("try 代码块被执行!");
+ return i;
+ } finally {
+ ++i;
+ System.out.println("finally 代码块被执行!");
+ System.out.println("finally 代码块中的i = " + i);
+ }
+
+ }
+
+}
+
try
语句块不止可以与 catch
连用,也可以与 finally
连用,但是 catch
不能与 finally
连用。
try{
+ // 程序代码
+} finally{
+ // 程序代码
+}
+
由于..finally..
语句块中的代码一般情况下一定会执行,所以经常用来关闭资源。
代码演示
+public class MainTest {
+ public static void main(String[] args) {
+ InputStream in = null;
+ try {
+ in = new FileInputStream("awsl");
+ in.read();
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ if (in != null) {
+ try {
+ in.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+}
+
自从JDK7之后,支持try-with-resources
的写法,这种写法对比之前更清晰、明了:
public class MainTest {
+ public static void main(String[] args) {
+ try (InputStream in = new FileInputStream("awsl")) {
+ in.read();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+}
+
这种写法其实是Java语法糖。
+++Java语法糖
+1.什么是语法糖? +语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。
+2.能够带来的好处 +语法糖让程序更加简洁,有更高的可读性
+3.有哪些语法糖? +1.自动拆箱、装箱 +2.泛型擦除 +3.不定长参数 +4.迭代器 +5.枚举 +6.switch支持枚举和字符串 +7.内部类 +8.try-with-resources +9.lambda
+
处理异常的方法,除了可以捕获异常,也可以将其丢给调用者进行处理。
+throws
用在方法上声明异常,子类继承的时候要继承该异常或者该异常的子类,不处理异常,谁调用该方法谁处理异常;
+throws
抛出异常时,它的调用者也要申明抛出异常或者捕获,不然编译报错。
+public static void main(String[] args) throws Exception {}
+
throw
用于方法内部,抛出的是异常对象。调用者可以不申明或不捕获(这是非常不负责任的方式)但编译器不会报错。
+throw new RuntimeException("这是运行中的异常");
+
throws
表示出现异常的一种可能性,告诉调用者这个方法是危险的,并不一定会发生这些异常;throw
则是抛出了异常,执行throw
则一定抛出了某种异常对象。
+两者都是消极处理异常的方式(这里的消极并不是说这种方式不好),只是抛出或者可能抛出异常,但是不会由方法去处理异常,真正的处理异常由此方法的上层调用处理。
参考文章:
+ +要谨慎地使用异常,异常捕获的代价非常高昂,异常使用过多会严重影响程序的性能。
+如果在程序中能够用if语句和boolean
变量来进行逻辑判断,那么尽量减少异常的使用,从而避免不必要的异常捕获和处理。
千万不要使用空的catch
块:
try{
+ // ...
+}catch(IOException e){
+ // ...
+}
+
在捕获了异常之后什么都不做,相当于忽略了这个异常。空的catch
块意味着你在程序中隐藏了错误和异常,并且很可能导致程序出现不可控的执行结果。
+如果你非常肯定捕获到的异常不会以任何方式对程序造成影响,最好用日志将该异常进行记录,以便日后方便更新和维护。
请不要在catch
块中吞掉异常:
catch (NoSuchMethodException e) {
+ return null;
+}
+
不要不处理异常,而返回null
,这样异常就会被吞掉,无法获取到任何失败信息,会给日后的问题排查带来巨大困难。
public void foo() throws Exception { //错误做法
+}
+
一定要尽量避免上面的代码,因为他的调用者完全不知道错误的原因到底是什么。
+在方法声明中,可以由方法抛出一些特定受检异常。如果有多个,那就分别抛出多个,这样这个方法的使用者才会分别针对每个异常做特定的处理,从而避免发生故障。
+public void foo() throws SpecificException1, SpecificException2 {
+//正确做法
+}
+
同样的在捕获异常时,也要注意,尽量捕获特定的子类,而不是直接捕获Exception
类。
try {
+ someMethod();
+}
+catch (Exception e) {
+ log.error("method has failed", e);
+}
+
上面代码,最大的问题就是,如果someMethod()
的开发者在里面新增了一个特定的异常,并且预期是调用方能够特殊的对他进行处理。
但是调用者直接catch了Exception
类,就会导致永远无法知道someMethod
的具体变化细节。这久可能导致在运行的过程中在某一个时间点程序崩溃。
更不要去捕获Throwable
类。因为Java中的Error
也可以是Throwable
的子类。但是Error
是Java虚拟机本身无法控制的。Java虚拟机甚至可能不会在出现任何错误时请求用户的catch
子句。
try {
+ someMethod();
+}
+catch (Throwable e) {
+ log.error("method has failed", e);
+}
+
OutOfMemoryError
和StackOverflowError
便是典型的例子,它们都是由于一些超出应用处理范围的情况导致的。
通常情况下,在捕获异常的时候抛出异常,需要注意的是,要始终在自定义异常中,覆盖原有的异常,从而构成一条异常链,这样堆栈跟踪就不会丢失:
+catch (NoSuchMethodException e) {
+ throw new MyServiceException("Some information: " + e.getMessage()); //错误做法
+}
+
上面的命令可能会丢失掉主异常的堆栈跟踪。正确的方法是:
+catch (NoSuchMethodException e) {
+ throw new MyServiceException("Some information: " , e); //正确做法
+}
+
需要注意的是,可以记录异常或抛出异常,但不要同时做:
+catch (NoSuchMethodException e) {
+ log.error("Some information", e);
+ throw e;
+}
+
抛出和日志记录可能会在日志文件中产生多个日志消息,这就会导致同一个问题,却在日志中有很多不同的错误信息,使得开发人员陷入混乱。
+一旦你决定抛出异常,你就要决定抛出抛出检查异常还是非检查异常。
+检查异常导致了太多的try…catch
代码,可能有很多检查异常对开发人员来说是无法合理地进行处理的,比如:SQLException
,而开发人员却不得不去进行try…catch
,这样就会导致经常出现这样一种情况:逻辑代码只有很少的几行,而进行异常捕获和处理的代码却有很多行。
+这样不仅导致逻辑代码阅读起来晦涩难懂,而且降低了程序的性能。
建议尽量避免检查异常的使用,如果确实该异常情况出现很的普遍,需要提醒调用者注意处理的话,就使用检查异常;否则使用非检查异常。 +因此,在一般情况下,尽量将检查异常转变为非检查异常交给上层处理。
+try {
+ someMethod(); //抛出 exceptionOne
+}finally{
+ cleanUp(); //如果在这里再抛出一个异常,那么try中的exception将会丢失
+}
+
在上面的例子中,如果someMethod()
抛出一个异常,并且在finally
块中,cleanUp()
也抛出一个异常,那么初始的exception
(正确的错误异常)将永远丢失。
但是,如果你不想处理someMethod()
中的异常,但是仍然需要做一些清理工作,那么在finally
块中进行清理。不要使用catch
块。
+ +
+ + + + + +Java IO通过数据流、序列化和文件系统提供系统输入和输出。
+++IO,即 in 和 out,也就是输入和输出,指应用程序和外部设备之间的数据传递,常见的外部设备包括文件、管道、网络连接。
+
传统的 IO 是通过流技术来处理的。
+++流(Stream),是一个抽象的概念,是指一连串的数据(字符或字节),是以先进先出的方式发送信息的通道。 +代表任何有能力产出数据的数据源对象或者是有能力接受数据的接收端对象。
+
流的作用就是为数据源和目的地建立一个输送通道
+一般来说关于流的特性有下面几点:
+RandomAccessFile
除外)根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作。
+主要的分类方式有以下3种:
+此输入、输出是相对于我们写的代码程序而言。
+字节流和字符流的用法几乎完成全一样,区别在于字节流和字符流所操作的数据单元不同,字节流操作的单元是数据单元是8位的字节,字符流操作的是数据单元为16位的字符。
++++
+- 字符流的由来
+Java中字符是采用Unicode标准,一个字符是16位,即一个字符使用两个字节来表示。 +为此,JAVA中引入了处理字符的流。因为数据编码的不同,而有了对字符进行高效操作的流对象。本质其实就是基于字节流读取时,去查了指定的码表。
++
+- 为什么要有字符流?
+Java中字符是采用Unicode标准,Unicode 编码中,一个英文为一个字节,一个中文为两个字节。 +如果使用字节流处理中文,如果一次读写一个字符对应的字节数就不会有问题,一旦将一个字符对应的字节分裂开来,就会出现乱码了。
+
+++
+- 字节流一般用来处理图像、视频、音频、
+PPT、Word
等类型的文件。字符流一般用于处理纯文本类型的文件,如TXT文件等,但不能处理图像视频等非文本文件。 +用一句话说就是:字节流可以处理一切文件,而字符流只能处理纯文本文件。- 字节流本身没有缓冲区,缓冲字节流相对于字节流,效率提升非常高。而字符流本身就带有缓冲区,缓冲字符流相对于字符流效率提升就不是那么大了。
+
按功能不同分为 节点流、处理流:
+FileInputStream
.BufferedReader
。
+处理流的构造方法总是要带一个其他的流对象做参数。一个流对象经过其他流的多次包装,处理流是对节点流的封装,最终的数据处理还是由节点流完成的。
+看上面的几个分类,可能会感觉到有些混乱,那什么时候用字节流,什么时候该用输出流呢?
+++1、首先自己要知道是选择输入流还是输出流,这就要根据自己的情况而定,如果你想从程序写东西到别的地方,那么就选择输出流,反之用输入流 +2、然后考虑你传输数据时,是选择使用字节流传输还是字符流,也就是每次传1个字节还是2个字节,有中文肯定就选择字符流了。 +3、前面两步就可以选出一个合适的节点流了,比如字节输入流inputStream,如果要在此基础上增强功能,那么就在处理流中选择一个合适的即可。
+
如果我们想要操作流首先,那不得不首先说一下File类。
+Java中的File类以抽象的方式代表文件名和目录路径名。该类主要用于文件和目录的创建、文件的查找和文件的删除等。
+File对象代表磁盘中实际存在的文件和目录。
+使用 File 类,查找文件、删除文件、创建文件的代码演示
+ public static void main(String args[]) throws IOException {
+ String dirname = "/home/dir";
+ File file = new File(dirname);
+
+ // 判断目录或文件存在
+ if (!file.exists()) {
+ System.out.println(dirname + " 该路径或文件不存在");
+ System.out.println("开始创建该文件或目录....");
+ // 不存在则创建
+ if (file.createNewFile()) {
+ System.out.println(dirname + " 创建成功");
+ }
+ }
+
+ // 判断是否为目录
+ if (file.isDirectory()) {
+ System.out.println("目录: " + dirname);
+ String s[] = file.list();
+ for (int i = 0; i < s.length; i++) {
+ File f = new File(dirname + "/" + s[i]);
+ if (f.isDirectory()) {
+ System.out.println(s[i] + " 是文件夹");
+ } else {
+ System.out.println(s[i] + " 是文件");
+ }
+ }
+ } else {
+ System.out.println(dirname + " 不是一个目录");
+ System.out.println("开始删除文件 ...");
+ // 删除文件
+ if (file.delete()) {
+ System.out.println(dirname + "删除成功");
+ }
+ }
+
+ }
+
操作byte类型数据,主要操作类是OutputStream、InputStream
的子类;不用缓冲区,直接对文件本身操作。
以下代码就是用FileInputStream、FileOutputStream
操作字节流
public static void main(String[] args) throws IOException {
+ File file = new File("./test.txt");
+ write(file);
+ System.out.println(read(file));
+ }
+
+ // 用字节流写入
+ public static void write(File file) throws IOException {
+ OutputStream os = new FileOutputStream(file, true);
+ // 要写入的字符串
+ String string = "awslawslawslawslawslawslawsl";
+ // 写入文件
+ os.write(string.getBytes());
+ // 关闭流
+ os.close();
+ }
+
+ // 用字节流读取
+ public static String read(File file) throws IOException {
+ InputStream in = new FileInputStream(file);
+ // 一次性取多少个字节
+ byte[] bytes = new byte[1024];
+ // 用来接收读取的字节数组
+ StringBuilder sb = new StringBuilder();
+ // 读取到的字节数组长度,为-1时表示没有数据
+ int length = 0;
+ // 循环取数据
+ while ((length = in.read(bytes)) != -1) {
+ // 将读取的内容转换成字符串
+ sb.append(new String(bytes, 0, length));
+ }
+ // 关闭流
+ in.close();
+ return sb.toString();
+ }
+
缓冲字节流是为高效率而设计的,真正的读写操作还是靠FileOutputStream
和FileInputStream
public static void main(String[] args) throws IOException {
+ File file = new File("test.txt");
+ write(file);
+ System.out.println(read(file));
+ }
+
+ public static void write(File file) throws IOException {
+ // 缓冲字节流,提高了效率
+ BufferedOutputStream bis = new BufferedOutputStream(new FileOutputStream(file, true));
+ // 要写入的字符串
+ String string = "awslawslawslawslawslawslawsl";
+ // 写入文件
+ bis.write(string.getBytes());
+ // 关闭流
+ bis.close();
+ }
+
+ public static String read(File file) throws IOException {
+ BufferedInputStream fis = new BufferedInputStream(new FileInputStream(file));
+ // 一次性取多少个字节
+ byte[] bytes = new byte[1024];
+ // 用来接收读取的字节数组
+ StringBuilder sb = new StringBuilder();
+ // 读取到的字节数组长度,为-1时表示没有数据
+ int length = 0;
+ // 循环取数据
+ while ((length = fis.read(bytes)) != -1) {
+ // 将读取的内容转换成字符串
+ sb.append(new String(bytes, 0, length));
+ }
+ // 关闭流
+ fis.close();
+ return sb.toString();
+ }
+
操作字符类型数据,主要操作类是Reader、Writer
的子类;使用缓冲区缓冲字符,不关闭流就不会输出任何内容。
字符流适用于文本文件的读写,OutputStreamWriter
类其实也是借助 FileOutputStream
类实现的
以下代码就是用InputStreamReader、OutputStreamWriter
操作字节流.
public static void main(String[] args) throws IOException {
+ File file = new File("test.txt");
+ write(file);
+ System.out.println(read(file));
+ }
+
+ public static void write(File file) throws IOException {
+ // OutputStreamWriter可以显示指定字符集,否则使用默认字符集
+ OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(file, true), "UTF-8");
+ // 要写入的字符串
+ String string = "awslawslawslawslawslawslawslawsl";
+ osw.write(string);
+ osw.close();
+ }
+
+ public static String read(File file) throws IOException {
+ InputStreamReader isr = new InputStreamReader(new FileInputStream(file), "UTF-8");
+ // 字符数组:一次读取多少个字符
+ char[] chars = new char[1024];
+ // 每次读取的字符数组先append到StringBuilder中
+ StringBuilder sb = new StringBuilder();
+ // 读取到的字符数组长度,为-1时表示没有数据
+ int length;
+ // 循环取数据
+ while ((length = isr.read(chars)) != -1) {
+ // 将读取的内容转换成字符串
+ sb.append(chars, 0, length);
+ }
+ // 关闭流
+ isr.close();
+ return sb.toString();
+ }
+
字符缓冲流
+public static void main(String[] args) throws IOException {
+ File file = new File("test.txt");
+ write(file);
+ System.out.println(read(file));
+ }
+
+ public static void write(File file) throws IOException {
+ // FileWriter可以大幅度简化代码
+ BufferedWriter bw = new BufferedWriter(new FileWriter(file, true));
+ // 要写入的字符串
+ String string = "awslawslawslawslawslawslawslawsl";
+ bw.write(string);
+ bw.close();
+ }
+
+ public static String read(File file) throws IOException {
+ BufferedReader br = new BufferedReader(new FileReader(file));
+ // 用来接收读取的字节数组
+ StringBuilder sb = new StringBuilder();
+ // 按行读数据
+ String line;
+ // 循环取数据
+ while ((line = br.readLine()) != null) {
+ // 将读取的内容转换成字符串
+ sb.append(line);
+ }
+ // 关闭流
+ br.close();
+ return sb.toString();
+ }
+
字节流和字符流间的转换
+OutputStreamWriter
是字符流通向字节流的桥梁 public static void main(String[] args) throws IOException {
+ File f = new File("test.txt");
+
+ // OutputStreamWriter 是字符流通向字节流的桥梁,创建了一个字符流通向字节流的对象
+ OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(f),"UTF-8");
+
+ osw.write("我是字符流转换成字节流输出的");
+ osw.close();
+
+ }
+
InputStreamReader
是字节流通向字符流的桥梁 public static void main(String[] args) throws IOException {
+
+ File f = new File("test.txt");
+
+ InputStreamReader inr = new InputStreamReader(new FileInputStream(f),"UTF-8");
+
+ char[] buf = new char[1024];
+
+ int len = inr.read(buf);
+ System.out.println(new String(buf,0,len));
+
+ inr.close();
+
+ }
+
序列化是将对象的状态信息转换为可存储或传输的形式的过程(一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型)。 +是一种数据的持久化手段。一般广泛应用于网络传输,RMI和RPC等场景中。 +一般是以字节码或XML格式传输。而字节码或XML编码格式可以还原为完全相等的对象。
+将序列化对象写入文件之后,可以从文件中读取出来,这个相反的过程称为反序列化。
+序列化机制允许将实现序列化的Java对象转换位字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以达到以后恢复成原来的对象。 +序列化机制使得对象可以脱离程序的运行而独立存在。
+对象序列化机制是Java语言内建的一种对象持久化方式,通过对象序列化,可以把对象的状态保存为字节数组, +并且可以在有需要的时候将这个字节数组通过反序列化的方式再转换成对象。对象序列化可以很容易的在JVM中的活动对象和字节流之间进行转换。
+由于序列化整个过程都是 Java 虚拟机独立的,也就是说,在一个平台上序列化的对象可以在另一个完全不同的平台上反序列化该对象。
+在Java中,对象的序列化与反序列化被广泛应用到RMI(远程方法调用)及网络传输中。
+使用Java对象序列化,在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装成对象。 +必须注意地是,对象序列化保存的是对象的"状态",即它的成员变量。所以,对象序列化不会关注类中的静态变量。
+如果需要将某个对象保存到磁盘上或者通过网络传输,那么这个类应该实现Serializable
接口或者Externalizable
接口。
++类的可序列化性是通过实现
+java.io.Serializable
接口的类来启用的。没有实现此接口的类的任何状态都不会被序列化或反序列化。 +可序列化类的所有子类型本身都是可序列化的。序列化接口没有方法或字段,只用于标识可序列化的语义。为了允许非序列化类的子类型被序列化,子类型可以承担保存和恢复超类型的公共、受保护和(如果可以访问)包字段的状态的责任。 +只有当它所继承的类有一个可访问的无参数构造函数来初始化类的状态时,子类型才可以承担这种责任。如果不是这种情况,则声明一个类可序列化是错误的。 +该错误将在运行时检测到。
+当试图对一个对象进行序列化的时候,如果遇到不支持 Serializable 接口的对象。在此情况下,将抛出
+NotSerializableException
。并标识非serializable
对象的类。
实现Serializable
序列化反序列对象化代码演示
public class MainTest {
+ public static void main(String[] args) {
+// serialUser();
+ System.out.println("----------反序列化对象----------");
+ unSerialUser();
+ }
+
+ private static void serialUser (){
+ User user = new User();
+ user.setName("Jane");
+ user.setAge("100");
+ System.out.println(user);
+ try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./user.txt"));) {
+ oos.writeObject(user);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private static void unSerialUser() {
+ File file = new File("./user.txt");
+ try(ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) {
+ User newUser = (User) ois.readObject();
+ System.out.println(newUser);
+ } catch (IOException | ClassNotFoundException e) {
+ e.printStackTrace();
+ }
+ }
+
+}
+
+class User implements Serializable {
+ private String name;
+ private String age;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getAge() {
+ return age;
+ }
+
+ public void setAge(String age) {
+ this.age = age;
+ }
+
+ @Override
+ public String toString() {
+ return "User{" +
+ "name='" + name + '\'' +
+ ", age='" + age + '\'' +
+ '}';
+ }
+}
+
++Externalizable继承了Serializable,该接口中定义了两个抽象方法:
+writeExternal
()与readExternal()
。 +当使用Externalizable接口来进行序列化与反序列化的时候需要开发人员重写writeExternal()
与readExternal()
方法。 +由于上面的代码中,并没有在这两个方法中定义序列化实现细节,所以输出的内容为空。还有一点值得注意:在使用
+Externalizable
进行序列化的时候,在读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。 +所以,实现Externalizable
接口的类必须要提供一个public的无参的构造器。
如果User类中没有无参数的构造函数,在反序列化时会抛出异常:
+java.io.InvalidClassException: content.posts.rookie.User; no valid constructor
实现Externalizable
序列化反序列对象化代码演示
public class MainTest {
+ public static void main(String[] args) {
+// serialUser();
+ System.out.println("----------反序列化对象----------");
+ unSerialUser();
+ }
+
+ private static void serialUser () {
+ User user = new User();
+ user.setName("Jane");
+ user.setAge("100");
+ System.out.println(user);
+ // /将对象序列化到文件
+ try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./user.txt"));) {
+ oos.writeObject(user);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private static void unSerialUser() {
+ File file = new File("./user.txt");
+ try(ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) {
+ User newUser = (User) ois.readObject();
+ System.out.println(newUser);
+ } catch (IOException | ClassNotFoundException e) {
+ e.printStackTrace();
+ }
+ }
+
+}
+
+class User implements Externalizable {
+
+ public User() {
+ }
+
+ private String name;
+ private String age;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getAge() {
+ return age;
+ }
+
+ public void setAge(String age) {
+ this.age = age;
+ }
+
+ @Override
+ public String toString() {
+ return "User{" +
+ "name='" + name + '\'' +
+ ", age='" + age + '\'' +
+ '}';
+ }
+
+ @Override
+ public void writeExternal(ObjectOutput out) throws IOException {
+ out.writeObject(name);
+ out.writeObject(age);
+ }
+
+ @Override
+ public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
+ name = (String) in.readObject();
+ age = (String) in.readObject();
+ }
+}
+
对于一个类中的某些字段如果不需要序列化,就需要加上transient
关键字。
++被
+transient
修饰的成员变量,在序列化的时候其值会被忽略,在被反序列化后,transient
变量的值被设为初始值, +如 int 型的是 0,对象型的是 null。
private transient String name;
+
此时name字段将不会被序列化;当然如果一个变量被static修饰,他也不会被序列化。
+虚拟机是否允许反序列化, 不仅取决于类路径和功能代码是否⼀致, ⼀个⾮常重要的⼀点是两个类的序列化 ID 是否⼀致, 即serialVersionUID
要求⼀致。
因为⽂件存储中的内容可能被篡改,为了保证数据的安全: 在进⾏反序列化时, JVM会把传来的字节流中的serialVersionUID
与本地相应实体类的serialVersionUID
进⾏⽐较, 如果相同就认为是⼀致的, 可以进⾏反序列化;
+否则就会出现序列化版本不⼀致的异常, 即是InvalidCastException
。
以下内容来自Serializable
接口注释
++If a serializable class does not explicitly declare a serialVersionUID, +then the serialization runtime will calculate a default +serialVersionUID value for that class based on various aspects of the class, +as described in the Java(TM) Object Serialization Specification. +However, it is strongly recommended that all serializable classes explicitly declare serialVersionUID values, +since the default serialVersionUID computation is highly sensitive to class details that may vary depending on compiler implementations, +and can thus result in unexpectedInvalidClassExceptions during deserialization.
+
当实现java.io.Serializable
接口的类没有显式地定义⼀个serialVersionUID
变量时候,Java序列化机制会根据编译的Class⾃动⽣成⼀个serialVersionUID
作序列化版本⽐较⽤,
+这种情况下,如果Class⽂件没有发⽣变化,就算再编译多次, serialVersionUID
也不会变化的。
+但是,如果发⽣了变化,那么这个⽂件对应的serialVersionUID
也就会发⽣变化。
Java强烈建议用户自定义一个serialVersionUID
,因为默认的serialVersinUID
对于class的细节非常敏感,
+反序列化时可能会导致InvalidClassException
这个异常。
代码演示序列化、反序列化加上serialVersionUID
private static final long serialVersionUID = 1L;
+
public class MainTest {
+ public static void main(String[] args) {
+ System.out.println("----------序列化对象----------");
+ serialUser();
+ System.out.println("----------反序列化对象----------");
+ unSerialUser();
+ }
+
+ private static void serialUser (){
+ User user = new User();
+ user.setName("Jane");
+ user.setAge("100");
+ System.out.println(user);
+ try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./user.txt"));) {
+ oos.writeObject(user);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private static void unSerialUser() {
+ File file = new File("./user.txt");
+ try(ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) {
+ User newUser = (User) ois.readObject();
+ System.out.println(newUser);
+ } catch (IOException | ClassNotFoundException e) {
+ e.printStackTrace();
+ }
+ }
+
+}
+
+class User implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ private String name;
+ private String age;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getAge() {
+ return age;
+ }
+
+ public void setAge(String age) {
+ this.age = age;
+ }
+
+ @Override
+ public String toString() {
+ return "User{" +
+ "name='" + name + '\'' +
+ ", age='" + age + '\'' +
+ '}';
+ }
+}
+
java.io.InvalidClassException: co.test.User; local class incompatible: stream classdesc serialVersionUID = -1643371274357194431, local class serialVersionUID = 1
+
代码调用链:
+ObjectInputStream.readObject -> readObject0 -> readOrdinaryObject -> readClassDesc -> readNonProxyDesc -> ObjectStreamClass.initNonProxy
+
在initNonProxy
中 ,关键代码如下:
void initNonProxy(ObjectStreamClass model,
+ Class<?> cl,
+ ClassNotFoundException resolveEx,
+ ObjectStreamClass superDesc)
+ throws InvalidClassException
+ {
+ long suid = Long.valueOf(model.getSerialVersionUID());
+ ObjectStreamClass osc = null;
+ if (cl != null) {
+ osc = lookup(cl, true);
+ if (osc.isProxy) {
+ throw new InvalidClassException(
+ "cannot bind non-proxy descriptor to a proxy class");
+ }
+ if (model.isEnum != osc.isEnum) {
+ throw new InvalidClassException(model.isEnum ?
+ "cannot bind enum descriptor to a non-enum class" :
+ "cannot bind non-enum descriptor to an enum class");
+ }
+
+ // ========== 判断反序列化 serializableUID 是否一致 ========== start//
+ if (model.serializable == osc.serializable &&
+ !cl.isArray() &&
+ suid != osc.getSerialVersionUID()) {
+ throw new InvalidClassException(osc.name,
+ "local class incompatible: " +
+ "stream classdesc serialVersionUID = " + suid +
+ ", local class serialVersionUID = " +
+ osc.getSerialVersionUID());
+ }
+ // ========== 判断反序列化 serializableUID 是否一致 ========== end//
+
+ if (!classNamesEqual(model.name, osc.name)) {
+ throw new InvalidClassException(osc.name,
+ "local class name incompatible with stream class " +
+ "name \"" + model.name + "\"");
+ }
+
+ // ...
+
+
getSerialVersionUID
方法:
public long getSerialVersionUID() {
+ // REMIND: synchronize instead of relying on volatile?
+ if (suid == null) {
+ suid = AccessController.doPrivileged(
+ new PrivilegedAction<Long>() {
+ public Long run() {
+ return computeDefaultSUID(cl);
+ }
+ }
+ );
+ }
+ return suid.longValue();
+}
+
在没有定义serialVersionUID
的时候,会调用computeDefaultSUID
方法,生成一个默认的serialVersionUID
。
serialVersionUID
有两种显示的生成方式:
private static final long serialVersionUID = 1L;
private static final long serialVersionUID = xxxxL;
第二种方式可通过编译器进行配置: +
+ +通过在被序列化的类中增加 writeObject 和 readObject 方法来实现。
+在java.util.ArrayList
中我们能找到答案:
public class ArrayList<E> extends AbstractList<E>
+ implements List<E>, RandomAccess, Cloneable, java.io.Serializable
+{
+ private static final long serialVersionUID = 8683452581122892189L;
+ transient Object[] elementData; // non-private to simplify nested class access
+ private int size;
+}
+
ArrayList实现了java.io.Serializable
接口,那么我们就可以对它进行序列化及反序列化。
+因为elementData
是 transient
的,所以这个成员变量不会被序列化而保留下来.
ArrayList底层是通过数组实现的。 +那么数组elementData其实就是用来保存列表中的元素的。通过该属性的声明方式我们知道,他是无法通过序列化持久化下来的。 +那么为什么却通过序列化和反序列化把List中的元素保留下来了呢?
+public static void main(String[] args) throws IOException, ClassNotFoundException {
+ List<String> stringList = new ArrayList<String>();
+ stringList.add("hello");
+ stringList.add("world");
+ stringList.add("hollis");
+ stringList.add("chuang");
+ System.out.println("init StringList" + stringList);
+ ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("stringlist"));
+ objectOutputStream.writeObject(stringList);
+
+ IOUtils.close(objectOutputStream);
+ File file = new File("stringlist");
+ ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
+ List<String> newStringList = (List<String>)objectInputStream.readObject();
+ IOUtils.close(objectInputStream);
+ if(file.exists()){
+ file.delete();
+ }
+ System.out.println("new StringList" + newStringList);
+ }
+//init StringList[hello, world, hollis, chuang]
+//new StringList[hello, world, hollis, chuang]
+
++在序列化过程中,如果被序列化的类中定义了
+writeObject
和readObject
方法,虚拟机会试图调用对象类里的writeObject
和readObject
方法,进行用户自定义的序列化和反序列化。如果没有这样的方法,则默认调用是
+ObjectOutputStream
的defaultWriteObject
方法以及ObjectInputStream
的defaultReadObject
方法。用户自定义的
+writeObject
和readObject
方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。 +对象的序列化过程通过ObjectOutputStream
和ObjectInputputStream
来实现的.
ArrayList实际上是动态数组,每次在放满以后自动增长设定的长度值,如果数组自动增长长度设为100, +而实际只放了一个元素,那就会序列化99个null元素。为了保证在序列化的时候不会将这么多null同时进行序列化, +ArrayList把元素数组设置为transient。
+为了防止一个包含大量空对象的数组被序列化,为了优化存储,所以,ArrayList使用transient来声明elementData。 +但是,作为一个集合,在序列化过程中还必须保证其中的元素可以被持久化下来, +所以,通过重写writeObject 和 readObject方法的方式把其中的元素保留下来。
+writeObject
方法把 elementData
数组中的元素遍历的保存到输出流(ObjectOutputStream
)中。readObject
方法从输入流(ObjectInputStream
)中读出对象并保存赋值到 elementData
数组中。writeObject
和 readObject
方法,那么这两个方法是怎么被调用的呢?在使用 ObjectOutputStream
的 writeObject
方法和 ObjectInputStream
的 readObject
方法时,会通过反射的方式调用。
ObjectOutputStream中writeObject
的调用栈:
++writeObject —> writeObject0 —>writeOrdinaryObject—>writeSerialData—>invokeWriteObject
+
++调用表示的
+serializable
类的writeObject
方法。 +如果类描述符不与类相关联,或者该类是可外部化、不可序列化的,或者没有定义writeObject
, +则抛出UnsupportedOperationException
。类定义的writeObject方法,如果没有则为null
+
invokeWriteObject
方法
/**
+ * Invokes the writeObject method of the represented serializable class.
+ * Throws UnsupportedOperationException if this class descriptor is not
+ * associated with a class, or if the class is externalizable,
+ * non-serializable or does not define writeObject.
+ */
+ void invokeWriteObject(Object obj, ObjectOutputStream out)
+ throws IOException, UnsupportedOperationException
+ {
+ requireInitialized();
+ if (writeObjectMethod != null) {
+ try {
+
+ // ========== 调用writeObject 方法 start========== //
+ writeObjectMethod.invoke(obj, new Object[]{ out });
+ // ========== 调用writeObject 方法 end========== //
+
+ } catch (InvocationTargetException ex) {
+ Throwable th = ex.getTargetException();
+ if (th instanceof IOException) {
+ throw (IOException) th;
+ } else {
+ throwMiscException(th);
+ }
+ } catch (IllegalAccessException ex) {
+ // should not occur, as access checks have been suppressed
+ throw new InternalError(ex);
+ }
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+
/** class-defined writeObject method, or null if none */
+ private Method writeObjectMethod;
+
Serializable
接口就能保证对象序列化?ObjectOutputStream中writeObject
的调用栈:
++writeObject —> writeObject0 —>writeOrdinaryObject—>writeSerialData—>invokeWriteObject
+
writeObject0
方法
/**
+ * Underlying writeObject/writeUnshared implementation.
+ */
+ private void writeObject0(Object obj, boolean unshared)
+ throws IOException
+ {
+ boolean oldMode = bout.setBlockDataMode(false);
+ depth++;
+ try {
+ // ...
+
+ // remaining cases
+ if (obj instanceof String) {
+ writeString((String) obj, unshared);
+ } else if (cl.isArray()) {
+ writeArray(obj, desc, unshared);
+ } else if (obj instanceof Enum) {
+ writeEnum((Enum<?>) obj, desc, unshared);
+ // =============================
+ } else if (obj instanceof Serializable) {
+ writeOrdinaryObject(obj, desc, unshared);
+ } else {
+ if (extendedDebugInfo) {
+ throw new NotSerializableException(
+ cl.getName() + "\n" + debugInfoStack.toString());
+ } else {
+ throw new NotSerializableException(cl.getName());
+ }
+ }
+ // =============================
+ } finally {
+ // ...
+ }
+ }
+
在进行序列化操作时,会判断要被序列化的类是否是 String、Enum、Array
和 Serializable
类型,
+如果不是则直接抛出 NotSerializableException
。
为什么序列化可以破坏单例了?
+序列化会通过反射调用无参数的构造方法创建一个新的对象。
+public class MainTest {
+ public static void main(String[] args) throws Exception {
+ String path = "/Users/whitepure/github/iblog/blog-site/content/posts/rookie/singleton.txt";
+
+ //Write Obj to file
+ ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(path));
+ oos.writeObject(Singleton.getSingleton());
+
+ //Read Obj from file
+ File file = new File(path);
+ ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
+ Singleton newInstance = (Singleton) ois.readObject();
+
+ //判断是否是同一个对象
+ System.out.println(newInstance == Singleton.getSingleton());
+ }
+
+}
+
+class Singleton implements Serializable {
+ private static final long serialVersionUID = 6377402142849822126L;
+
+ private volatile static Singleton singleton;
+
+ private Singleton() {
+ }
+
+ public static Singleton getSingleton() {
+ if (singleton == null) {
+ synchronized (MainTest.class) {
+ if (singleton == null) {
+ singleton = new Singleton();
+ }
+ }
+ }
+ return singleton;
+ }
+}
+
输出结果为false,对Singleton
的序列化与反序列化得到的对象是一个新的对象,这就破坏了 Singleton
的单例性。
对象的序列化过程通过 ObjectOutputStream
和 ObjectInputputStream
来实现的
ObjectInputStream
中 readObject
的调用栈:
++readObject —> readObject0 —> readOrdinary —> checkResolve
+
readOrdinaryObject
方法
++读取并返回"ordinary"(即,不是字符串,类,ObjectStreamClass,数组,或枚举常量)对象,如果对象的类是不可解析的,则为null(在这种情况下,ClassNotFoundException将与对象的句柄相关联)。 +设置passHandle为对象的赋值句柄。
+
/**
+ * Reads and returns "ordinary" (i.e., not a String, Class,
+ * ObjectStreamClass, array, or enum constant) object, or null if object's
+ * class is unresolvable (in which case a ClassNotFoundException will be
+ * associated with object's handle). Sets passHandle to object's assigned
+ * handle.
+ */
+ private Object readOrdinaryObject(boolean unshared)
+ throws IOException
+ {
+
+ // ...
+
+ Object obj;
+ try {
+ // `desc.isInstantiable()`: 如果一个 `serializable/externalizable` 的类可以在运行时被实例化,那么该方法就返回true
+ // `desc.newInstance`:该方法通过反射的方式调用无参构造方法新建一个对象
+ obj = desc.isInstantiable() ? desc.newInstance() : null;
+ } catch (Exception ex) {
+ throw (IOException) new InvalidClassException(
+ desc.forClass().getName(),
+ "unable to create instance").initCause(ex);
+ }
+
+ // ...
+
+ // hasReadResolveMethod:如果实现了serializable 或者 externalizable接口的类中包含readResolve则返回true
+ if (obj != null &&
+ handles.lookupException(passHandle) == null &&
+ desc.hasReadResolveMethod())
+ {
+ // invokeReadResolve:通过反射的方式调用要被反序列化的类的readResolve方法。
+ Object rep = desc.invokeReadResolve(obj);
+ if (unshared && rep.getClass().isArray()) {
+ rep = cloneArray(rep);
+ }
+ // ...
+ }
+
+ return obj;
+ }
+
在 Singleton
中定义 readResolve
方法,并在该方法中指定要返回的对象的生成策略,就可以防止单例被破坏。
class Singleton implements Serializable {
+ private static final long serialVersionUID = 6377402142849822126L;
+
+ private volatile static Singleton singleton;
+
+ private Singleton() {
+ }
+
+ public static Singleton getSingleton() {
+ if (singleton == null) {
+ synchronized (MainTest.class) {
+ if (singleton == null) {
+ singleton = new Singleton();
+ }
+ }
+ }
+ return singleton;
+ }
+
+ public Object readResolve() {
+ return singleton;
+ }
+}
+
IO模型共有5种:阻塞IO、非阻塞IO、信号驱动IO、IO多路转接、异步IO。其中,前四个被称为同步IO。
+BIO(Blocking IO):最传统的一种IO模型,即在读写数据过程中会发生阻塞现象。
+当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。 +当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block状态。
+特点:
+因为一个请求IO会阻塞进程,不能充分利用cpu资源,所以,得为每请求分配一个处理进程(线程)以及时响应,系统开销大;不适用并发量大的应用。
+NIO(NoBlocking IO):当用户线程发起一个read操作后,并不需要等待,而是马上就得到了一个结果。 +如果结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。 +一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。
+特点:
+在非阻塞IO模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU。
+IO复用模型: 一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作。 +在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket读写事件进行时,才会使用IO资源,所以它大大减少了资源占用。
+Java NIO实际上就是多路复用IO。
+通过selector.select()
查询每个通道是否有到达事件,如果没有事件,则一直阻塞在那里,因此这种方式会导致用户线程的阻塞。所以,多路复用IO比较适合连接数比较多的情况。
+
特点:
+++多路复用IO为何比非阻塞IO模型的效率高? +因为在非阻塞IO中,不断地询问socket状态是通过用户线程去进行的,而在多路复用IO中,轮询每个socket状态是内核在进行的,这个效率要比用户线程要高的多。
+
多路复用IO模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件逐一进行响应。 +因此对于多路复用IO模型来说,一旦事件响应体很大,那么就会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询。
+当用户线程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户线程会继续执行不阻塞, +当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。
+进程预先告知内核,使得当某个socket有事件发生时,系统内核使用信号通知相关进程。
+特点:
+AIO(Async IO):当用户线程发起IO操作后,立刻就可以开始去做其它的事。 +另一方面,从内核的角度,当它收到一个IO请求之后,它会立刻返回给用户线程,说明IO请求已经成功发起了,因此不会对用户线程产生任何阻塞。 +然后,内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它IO操作完成了。
+用户线程完全不需要知道实际的整个IO操作是如何进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示IO操作已经完成,可以直接去使用数据了。
+在异步IO模型中,IO操作的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完成,然后发送一个信号告知用户线程操作已完成。
+用户线程中不需要再次调用IO函数进行具体的读写。 +这点是和信号驱动模型有所不同的,在信号驱动模型中,当用户线程接收到信号表示数据已经就绪,然后需要用户线程调用IO函数进行实际的读写操作; +而在异步IO模型中,收到信号表示IO操作已经完成,不需要再在用户线程中调用IO函数进行实际的读写操作。
+特点:
+Java NIO
解释为: New IO
或 Non Blocking IO
是从J ava 1.4 版本开始引入的一个新的IO API,可以替代标准的Java IO API。
+NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。
与传统IO的区别:
+区别 | +IO | +NIO | +
---|---|---|
传输方式 | +面向流,通过流传输 | +面向缓冲区,通过缓冲区传输 | +
是否阻塞 | +阻塞IO | +非阻塞IO | +
其他 | +无 | +选择器,可以解决阻塞问题 | +
通道负责传输,缓冲区负责存储。
+若需要使用 NIO ,需要获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。
+缓冲区:在java NIO 中负者数据的存储。缓冲区底层实现是数组。用于存储不同类型的数据。 +根据数据类型的不同(boolean 除外),有以下 Buffer 常用子类:
+在父类抽象类Buffer中存在四个核心属性:
+大小关系:0<=mark<=position<=limit<=capacity
存储数据:
+flip(): 切换为读取数据模式
+读取数据:
+clear(): 清空缓冲区;但是缓冲区中的数据依然存在,只是将position、limit 的值回归到初始值
+position、limit 数值变化:
+代码演示
+public class MainTest {
+ public static void main(String[] args) {
+ String str = "abcde";
+ ByteBuffer allocate = ByteBuffer.allocate(1024);
+ allocate.put(str.getBytes());
+ System.out.println("========向ByteBuffer添加数据========");
+ System.out.println(str);
+ System.out.println("capacity: "+ allocate.capacity());
+ System.out.println("limit: " + allocate.limit());
+ System.out.println("position: " + allocate.position());
+
+ allocate.flip();
+ System.out.println("========切换为读取数据模式========");
+ System.out.println("capacity: "+ allocate.capacity());
+ System.out.println("limit: " + allocate.limit());
+ System.out.println("position: " + allocate.position());
+
+ byte[] bytes = str.getBytes();
+ allocate.get(bytes);
+ System.out.println("========从ByteBuffer取出数据========");
+ System.out.println(new String(bytes, 0, bytes.length));
+ System.out.println("capacity: "+ allocate.capacity());
+ System.out.println("limit: " + allocate.limit());
+ System.out.println("position: " + allocate.position());
+
+ allocate.rewind();
+ System.out.println("========切换重新读取数据模式========");
+ System.out.println("capacity: "+ allocate.capacity());
+ System.out.println("limit: " + allocate.limit());
+ System.out.println("position: " + allocate.position());
+
+ allocate.clear();
+ System.out.println("========清空缓冲区========");
+ System.out.println("capacity: "+ allocate.capacity());
+ System.out.println("limit: " + allocate.limit());
+ System.out.println("position: " + allocate.position());
+ System.out.println("再来读取数据:" + (char)allocate.get());
+ }
+}
+
mark方法: 记录当前position位置。可以通过 reset() 恢复到 mark 的位置。
+代码演示
+public class MainTest {
+ public static void main(String[] args) {
+ String str = "abcde";
+ ByteBuffer allocate = ByteBuffer.allocate(1024);
+
+ allocate.put(str.getBytes());
+
+ allocate.flip();
+
+ byte[] bytes = str.getBytes();
+ allocate.get(bytes, 0, 2);
+ System.out.println("========从ByteBuffer取出数据========");
+ System.out.println(new String(bytes, 0, 2));
+ System.out.println("capacity: " + allocate.capacity());
+ System.out.println("limit: " + allocate.limit());
+ System.out.println("position: " + allocate.position());
+
+ allocate.mark();
+ System.out.println("========记录当前 `position` 的位置========");
+ System.out.println("capacity: " + allocate.capacity());
+ System.out.println("limit: " + allocate.limit());
+ System.out.println("position: " + allocate.position());
+
+ allocate.get(bytes, 2, 2);
+ System.out.println("========从ByteBuffer再次取出数据========");
+ System.out.println(new String(bytes, 2, 2));
+ System.out.println("capacity: " + allocate.capacity());
+ System.out.println("limit: " + allocate.limit());
+ System.out.println("position: " + allocate.position());
+
+ allocate.reset();
+ System.out.println("========恢复之前被标记的位置========");
+ System.out.println("capacity: " + allocate.capacity());
+ System.out.println("limit: " + allocate.limit());
+ System.out.println("position: " + allocate.position());
+ }
+}
+
直接缓冲区:通过 allocateDirect()
方法分配直接缓冲区,将缓冲区建立在物理内存中。可以提高读写效率。
非直接缓冲区:通过 allocate()
方法分配缓冲区,将缓冲区建立在JVM的内存中。
allocate()
方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区 。
+直接缓冲区的内容可以驻留在常规的垃圾回收堆之外.
public class MainTest {
+ public static void main(String[] args) {
+ ByteBuffer allocate = ByteBuffer.allocate(1024);
+ ByteBuffer direct = ByteBuffer.allocateDirect(1024);
+
+ if (direct.isDirect()){
+ System.out.println("allocateDirect 是直接缓冲区");
+ }
+ if (!allocate.isDirect()){
+ System.out.println("allocate 是非直接缓冲区");
+ }
+ }
+}
+
通道(Channel):用于源节点与目标节点的连接。在 java NIO
中负责缓冲区中数据的传输。Channel本身不存储数据,需要配合缓冲区进行数据传输。
在操作系统中,通道是一种通过执行通道程序管理I/O操作的控制器,它使主机(CPU和内存)与I/O操作之间达到更高的并行程度。 +需要进行I/O操作时,CPU只需启动通道,然后可以继续执行自身程序,通道则执行通道程序,管理与实现I/O操作。
+通道的主要实现类:
+FileChannel
:用于读取、写入、映射和操作文件的通道。SocketChannel
:通过 TCP 读写网络中的数据。ServerSocketChannel
:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel
。DatagramChannel
:通过 UDP 读写网络中的数据通道。Java 针对支持通道的类提供了 getChannel()
方法
使用 非直接缓冲区 完成对文件的读写
+public class MainTest {
+ /**
+ * 使用非直接缓冲区完成读写操作
+ * @param args args
+ */
+ public static void main(String[] args) {
+ long start = System.currentTimeMillis();
+ try (
+ // 获取通道
+ FileChannel inChannel = new FileInputStream("1.jpg").getChannel();
+ FileChannel outChannel = new FileOutputStream("2.jpg").getChannel();
+ ) {
+ // 分配指定大小的缓冲区
+ ByteBuffer buf = ByteBuffer.allocate(1024);
+
+ // 将通道中的数据存入缓冲区中
+ while (inChannel.read(buf) != -1) {
+
+ // 切换读取数据的模式
+ buf.flip();
+
+ // 将缓冲区中的数据写入通道中
+ outChannel.write(buf);
+
+ // 清空缓冲区
+ buf.clear();
+ }
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ long end = System.currentTimeMillis();
+ System.out.println("耗费的时间为:" + (end - start));
+ }
+ }
+}
+
使用 直接缓冲区 完成对文件的读写
+public class MainTest {
+ /**
+ * 使用直接缓冲区完成文件的读写
+ *
+ * @param args args
+ */
+ public static void main(String[] args) {
+ long start = System.currentTimeMillis();
+ try (
+ FileChannel inChannel = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ);
+ FileChannel outChannel = FileChannel.open(Paths.get("2.jpg"), StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE);
+ ) {
+
+ //内存映射文件
+ MappedByteBuffer inMappedBuf = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
+ MappedByteBuffer outMappedBuf = outChannel.map(FileChannel.MapMode.READ_WRITE, 0, inChannel.size());
+
+ //直接对缓冲区进行数据的读写操作
+ byte[] dst = new byte[inMappedBuf.limit()];
+ inMappedBuf.get(dst);
+ outMappedBuf.put(dst);
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ long end = System.currentTimeMillis();
+ System.out.println("耗费的时间为:" + (end - start));
+ }
+ }
+}
+
使用 通道 完成对文件的读写
+public class MainTest {
+ /**
+ * 使用通道完成读写操作
+ *
+ * @param args args
+ */
+ public static void main(String[] args) {
+ long start = System.currentTimeMillis();
+ try (
+ // 获取通道
+ FileChannel inChannel = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ);
+ FileChannel outChannel = FileChannel.open(Paths.get("2.jpg"), StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE);
+ ) {
+
+ //内存映射文件
+ MappedByteBuffer inMappedBuf = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
+ MappedByteBuffer outMappedBuf = outChannel.map(FileChannel.MapMode.READ_WRITE, 0, inChannel.size());
+
+ //直接对缓冲区进行数据的读写操作
+ byte[] dst = new byte[inMappedBuf.limit()];
+ inMappedBuf.get(dst);
+ outMappedBuf.put(dst);
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ long end = System.currentTimeMillis();
+ System.out.println("耗费的时间为:" + (end - start));
+ }
+ }
+}
+
分散读取和聚集写入:
+public class MainTest {
+ /**
+ * 分散和聚集
+ *
+ * @param args args
+ */
+ public static void main(String[] args) {
+ long start = System.currentTimeMillis();
+ try (
+ // 分散读取通道
+ FileChannel channel1 = new RandomAccessFile("1.txt", "rw").getChannel();
+ // 聚集写入通道
+ FileChannel channel2 = new RandomAccessFile("2.txt", "rw").getChannel();
+ ) {
+ // 分配指定大小的缓冲区
+ ByteBuffer buf1 = ByteBuffer.allocate(100);
+ ByteBuffer buf2 = ByteBuffer.allocate(1024);
+
+ // 分散读取
+ ByteBuffer[] bufs = {buf1, buf2};
+ channel1.read(bufs);
+
+ for (ByteBuffer byteBuffer : bufs) {
+ byteBuffer.flip();
+ }
+
+ System.out.println(new String(bufs[0].array(), 0, bufs[0].limit()));
+ System.out.println("--------------------");
+ System.out.println(new String(bufs[1].array(), 0, bufs[1].limit()));
+
+ // 聚集写入
+ channel2.write(bufs);
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ long end = System.currentTimeMillis();
+ System.out.println("耗费的时间为:" + (end - start));
+ }
+ }
+}
+
解码与编码:
+public class MainTest {
+ /**
+ * 编码与解码
+ *
+ * @param args args
+ */
+ public static void main(String[] args) {
+ Charset cs1 = Charset.forName("GBK");
+
+ //获取编码器
+ CharsetEncoder ce = cs1.newEncoder();
+
+ //获取解码器
+ CharsetDecoder cd = cs1.newDecoder();
+
+ CharBuffer cBuf = CharBuffer.allocate(1024);
+ cBuf.put("阿伟死了");
+ cBuf.flip();
+
+ ByteBuffer bBuf;
+ try {
+ //编码
+ bBuf = ce.encode(cBuf);
+ for (int i = 0; i < 8; i++) {
+ System.out.println(bBuf.get());
+ }
+ bBuf.flip();
+
+ //解码
+ CharBuffer cBuf2 = cd.decode(bBuf);
+ System.out.println(cBuf2.toString());
+
+ } catch (CharacterCodingException e) {
+ e.printStackTrace();
+ }
+ }
+}
+
传统的 IO 流都是阻塞式的。
+就是说,当一个线程调用 read()
或 write()
时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。
+因此,在完成网络通信进行 IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量客户端时,性能急剧下降。
阻塞式IO,代码演示
+class Client {
+ public static void main(String[] args) throws IOException {
+ System.out.println("启动客户端 ...");
+ client();
+ }
+
+ /**
+ * 阻塞NIO 客户端
+ */
+ public static void client() throws IOException {
+ SocketChannel sChannel=SocketChannel.open(new InetSocketAddress("127.0.0.1",9898));
+ FileChannel inChannel=FileChannel.open(Paths.get("/Users/whitepure/Desktop/1.txt"), StandardOpenOption.READ);
+ ByteBuffer buf=ByteBuffer.allocate(1024);
+
+ while(inChannel.read(buf)!=-1){
+ buf.flip();
+ sChannel.write(buf);
+ buf.clear();
+ }
+
+ //关闭发送通道,表明发送完毕
+ sChannel.shutdownOutput();
+
+ //接收服务端的反馈
+ int len=0;
+ while((len=sChannel.read(buf))!=-1){
+ buf.flip();
+ System.out.println(new String(buf.array(),0,len));
+ buf.clear();
+ }
+ inChannel.close();
+ sChannel.close();
+ }
+}
+
+class Server {
+ public static void main(String[] args) throws IOException {
+ System.out.println("启动服务端 ...");
+ server();
+ }
+ /**
+ * 阻塞IO 服务器方
+ */
+ public static void server() throws IOException{
+ ServerSocketChannel ssChannel=ServerSocketChannel.open();
+ FileChannel outChannel=FileChannel.open(Paths.get("/Users/whitepure/Desktop/2.txt"), StandardOpenOption.WRITE,StandardOpenOption.CREATE);
+ ssChannel.bind(new InetSocketAddress(9898));
+ SocketChannel sChannel=ssChannel.accept();
+ ByteBuffer buf=ByteBuffer.allocate(1024);
+
+ while(sChannel.read(buf)!=-1){
+ buf.flip();
+ outChannel.write(buf);
+ buf.clear();
+ }
+
+ //发送反馈给客户端
+ buf.put("服务端接收数据成功".getBytes());
+ buf.flip();
+ sChannel.write(buf);
+
+ sChannel.close();
+ outChannel.close();
+ ssChannel.close();
+ }
+}
+
Java NIO 是非阻塞模式的。 +当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。 +因此,NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。
+非阻塞式IO,代码演示
+class Client {
+ public static void main(String[] args) throws IOException {
+ System.out.println("启动客户端 ... 等待输入 ...");
+ client();
+ }
+
+ /**
+ * 非阻塞NIO 客户端
+ */
+ public static void client() throws IOException {
+ SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
+ ByteBuffer buf = ByteBuffer.allocate(1024);
+
+ Scanner scanner = new Scanner(System.in);
+
+ // 发送数据到服务方
+ while (scanner.hasNextLine()) {
+ buf.put((LocalDate.now() + "\n" + scanner.next()).getBytes());
+ buf.flip();
+ sChannel.write(buf);
+ buf.clear();
+ }
+ sChannel.close();
+ }
+}
+
+class Server {
+ public static void main(String[] args) throws IOException {
+ System.out.println("启动服务端 ... 等待客户端请求 ...");
+ server();
+ }
+
+ /**
+ * 非阻塞IO 服务器方
+ */
+ public static void server() throws IOException {
+ ServerSocketChannel ssChannel = ServerSocketChannel.open();
+
+ // 切换为非阻塞模式
+ ssChannel.configureBlocking(false);
+ // 绑定链接
+ ssChannel.bind(new InetSocketAddress(9898));
+
+ // 获取选择器
+ Selector selector = Selector.open();
+
+ /**
+ * 将通道注册到选择器上,并且指定“监听接收事件”
+ * 使用 SelectionKey 的四个常量 表示
+ *
+ * 读 : SelectionKey.OP_READ (1)
+ * 写 : SelectionKey.OP_WRITE (4)
+ * 连接 : SelectionKey.OP_CONNECT (8)
+ * 接收 : SelectionKey.OP_ACCEPT (16)
+ *
+ * 若注册时不止监听一个事件,则可以使用“位或”操作符连接。
+ */
+ SelectionKey selectionKey = ssChannel.register(selector, SelectionKey.OP_ACCEPT);
+
+ // 轮巡获取 注册器上的接收事件
+ while (selector.select() > 0) {
+ // 获取当前选择器中所有注册的“选择键(已就绪的监听事件)”
+ Iterator<SelectionKey> it = selector.selectedKeys().iterator();
+
+ while (it.hasNext()) {
+ // 获取准备“就绪”的事件
+ SelectionKey sk = it.next();
+
+ // 判断具体是什么时间准备就绪
+ if (sk.isAcceptable()) {
+ // 若“接收就绪”,获取客户端连接
+ SocketChannel sChannel = ssChannel.accept();
+
+ // 切换非阻塞模式
+ sChannel.configureBlocking(false);
+
+ // 将该通道注册到选择器上
+ sChannel.register(selector, SelectionKey.OP_READ);
+ } else if (sk.isReadable()) {
+ // 获取当前选择器上“读就绪”状态的通道
+ SocketChannel sChannel = (SocketChannel) sk.channel();
+ // 读取数据
+ ByteBuffer buf = ByteBuffer.allocate(1024);
+ int len = 0;
+ while ((len = sChannel.read(buf)) > 0) {
+ buf.flip();
+ System.out.println(new String(buf.array(), 0, len));
+ buf.clear();
+ }
+ }
+
+ // 移除注册的选择键,否则会一直轮巡获取
+ it.remove();
+ }
+ }
+ }
+}
+
Java NIO 管道是2个线程之间的单向数据连接。Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。
+代码演示
+public class MainTest {
+ public static void main(String[] args) throws IOException {
+ //1.获取管道
+ Pipe pipe= Pipe.open();
+
+ //2.将缓冲区中的数据写入管道
+ ByteBuffer buf= ByteBuffer.allocate(1024);
+ Pipe.SinkChannel sinkChannel=pipe.sink();
+ buf.put("通过单向管道发送数据".getBytes());
+ buf.flip();
+ sinkChannel.write(buf);
+
+ //3.读取缓冲区中的数据
+ Pipe.SourceChannel sourceChannel=pipe.source();
+ buf.flip();
+ int len=sourceChannel.read(buf);
+ System.out.println(new String(buf.array(),0,len));
+
+ sourceChannel.close();
+ sinkChannel.close();
+ }
+}
+
+ ++
目前存在的线程模型有:
+Netty 主要基于主从 Reactor 多线程模型做了一定的改进。
+采用阻塞 IO 模式获取输入的数据,每个连接都需要独立的线程完成数据的输入,业务处理,数据返回。 +当并发数很大,就会创建大量的线程,占用很大系统资源,连接创建后,如果当前线程暂时没有数据可读,该线程会阻塞在 read 操作,造成线程资源浪费。
+基于 I/O 复用模型,多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象等待,无需阻塞等待所有连接。 当某个连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。
+ +为了避免浪费可以创建一个线程池,当客户端发起请求时,通过DispatcherHandler
进行分发请求处理到线程池,线程池中在使用具体的线程进行事件处理。 服务器端程序处理传入的多个请求,并将它们同步分派到相应的处理线程,因此 Reactor 模式也叫 Dispatcher 模式。
Reactor 模式使用 IO 复用监听事件,收到事件后,分发给某个线程(进程),这点就是网络服务器高并发处理关键。
+步骤:
+Read → 业务处理 → Send
的完整业务流程缺点:
+优点:
+使用场景:
+步骤:
+缺点:
+优点:
+Reactor 主线程可以对应多个 Reactor 子线程,即 MainRecator 可以关联多个 SubReactor,从而解决了Reactor 在单线程中运行,高并发场景下容易成为性能瓶颈。 +步骤:
+优点:
+缺点:
+这种模型在许多项目中广泛使用,包括 Nginx 主从 Reactor 多进程模型,Memcached 主从多线程,Netty 主从多线程模型的支持。
+BossGroup
专门负责接收客户端的连接,WorkerGroup
专门负责网络的读写;BossGroup
和 WorkerGroup
类型都是 NioEventLoopGroup
NioEventLoopGroup
相当于一个事件循环组,这个组中含有多个事件循环,每一个事件循环是 NioEventLoop
,每个 NioEventLoop
都有一个 Selector
,用于监听绑定在其上的 socket
的网络通讯NioEventLoopGroup
可指定多个 NioEventLoop
BossNioEventLoop
循环执行的步骤
+accept
事件,与 client 建立连接,生成 NioSocketChannel
,并将其注册到某个 workerNioEventLoop
上的 Selector
runAllTasks
WorkerNioEventLoop
循环执行的步骤
+read,write
事件read,write
事件,在对应 NioSocketChannel
处理runAllTasks
WorkerNioEventLoop
处理业务时,会使用 pipeline
(管道),pipeline
中包含了 channel
(通道),即通过 pipeline
可以获取到对应通道,管道中维护了很多的处理器1.导入依赖
+<dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-all</artifactId>
+ <version>4.1.36.Final</version>
+</dependency>
+<dependency>
+ <groupId>org.projectlombok</groupId>
+ <artifactId>lombok</artifactId>
+</dependency>
+
2.服务器端代码演示
+/**
+ * netty 服务端测试
+ */
+public class MainTestServer {
+ public static void main(String[] args) {
+ // 启动器, 负责组装netty组件 启动服务器
+ new ServerBootstrap()
+ // BossEventLoop WorkEventLoop 每个 EventLoop 就是 一个选择器 + 一个线程
+ .group(new NioEventLoopGroup())
+ // 选择服务器 ServerSocketChannel 具体实现
+ .channel(NioServerSocketChannel.class)
+ // 决定了 workEventLoop 能做那些操作
+ .childHandler(
+ // 建立连接后会被调用; 作用: 初始化 + 添加其他的 handler
+ new ChannelInitializer<NioSocketChannel>() {
+ // 当客户端请求发过来时 才会调用
+ @Override
+ protected void initChannel(NioSocketChannel channel) throws Exception {
+ channel.pipeline().addLast(new StringDecoder());
+ // 自定义handler
+ channel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
+ @Override
+ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+ System.out.println("服务器端接收数据:" + msg);
+ }
+ });
+ }
+ })
+ // 绑定监听端口
+ .bind(8090);
+ }
+}
+
3.客户端代码演示
+/**
+ * netty 客户端测试
+ */
+public class MainTestClient {
+ public static void main(String[] args) throws InterruptedException {
+ new Bootstrap()
+ .group(new NioEventLoopGroup())
+ .channel(NioSocketChannel.class)
+ .handler(new ChannelInitializer<NioSocketChannel>() {
+ // 初始化 在与服务器建立链接的时候 调用
+ @Override
+ protected void initChannel(NioSocketChannel channel) throws Exception {
+ // 添加编码器 只有当向服务端发送请求数据时 才会执行
+ channel.pipeline().addLast(new StringEncoder());
+ }
+ })
+ .connect(new InetSocketAddress("127.0.0.1", 8090))
+ //阻塞方法,直到与服务器端连接建立
+ .sync()
+ .channel()
+ // 向服务器端发送数据
+ .writeAndFlush("hello word");
+ }
+}
+
客户端
+/**
+ * netty 客户端测试
+ */
+public class MainTestClient {
+ public static void main(String[] args) throws InterruptedException {
+ new Bootstrap()
+ .group(new NioEventLoopGroup())
+ .channel(NioSocketChannel.class)
+ .handler(new ChannelInitializer<NioSocketChannel>() {
+
+ // 初始化 在与服务器建立链接的时候 调用
+ @Override
+ protected void initChannel(NioSocketChannel channel) throws Exception {
+ // 添加编码器 只有当向服务端发送请求数据时 才会执行
+ channel.pipeline().addLast("decoder", new StringDecoder());
+ channel.pipeline().addLast("encoder", new StringEncoder());
+ channel.pipeline().addLast(new ChannelInboundHandlerAdapter(){
+ @Override
+ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+ System.out.println("服务端响应数据:" + msg);
+ }
+
+ @Override
+ public void channelActive(ChannelHandlerContext ctx) throws Exception {
+ System.out.println("客户端Active .....");
+ }
+
+ /**
+ * 客户端异常时触发
+ */
+ @Override
+ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+ cause.printStackTrace();
+ ctx.close();
+ }
+
+ });
+ }
+
+ })
+ .connect(new InetSocketAddress("127.0.0.1", 8090))
+ //阻塞方法,直到与服务器端连接建立
+ .sync()
+ .channel()
+ // 向服务器端发送数据
+ .writeAndFlush("hello word");
+ }
+}
+
服务端
+/**
+ * netty 服务端测试
+ */
+public class MainTestServer {
+ public static void main(String[] args) {
+ //new 一个主线程组
+ EventLoopGroup bossGroup = new NioEventLoopGroup(1);
+ //new 一个工作线程组
+ EventLoopGroup workGroup = new NioEventLoopGroup(200);
+ // 启动器, 负责组装netty组件 启动服务器
+ try {
+ ServerBootstrap serverBootstrap = new ServerBootstrap()
+ // BossEventLoop WorkEventLoop 每个 EventLoop 就是 一个选择器 + 一个线程
+ .group(bossGroup, workGroup)
+ // 选择服务器 ServerSocketChannel 具体实现
+ .channel(NioServerSocketChannel.class)
+ //设置队列大小
+ .option(ChannelOption.SO_BACKLOG, 1024)
+ // 两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文
+ .childOption(ChannelOption.SO_KEEPALIVE, true)
+ // 决定了 workEventLoop 能做那些操作
+ .childHandler(
+ // 建立连接后会被调用; 作用: 初始化 + 添加其他的 handler
+ new ChannelInitializer<NioSocketChannel>() {
+ // 当客户端请求发过来时 才会调用
+ @Override
+ protected void initChannel(NioSocketChannel channel) throws Exception {
+ channel.pipeline().addLast("decoder", new StringDecoder(CharsetUtil.UTF_8));
+ channel.pipeline().addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));
+ // 自定义handler
+ channel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
+
+ /**
+ * 客户端连接会触发
+ */
+ @Override
+ public void channelActive(ChannelHandlerContext ctx) throws Exception {
+ System.out.println("服务端 Active......");
+ }
+
+ /**
+ * 客户端发消息会触发
+ */
+ @Override
+ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+ System.out.println("服务器收到消息: " + msg.toString());
+ // 比如这里我们有一个非常耗时长的业务-> 应该一步执行 -> 提交该对应的channel
+ // 将任务放在 taskQueue 中
+ ctx.channel().eventLoop().execute(() -> {
+ try {
+ Thread.sleep(10 * 1000);
+ ctx.writeAndFlush("业务1处理完成");
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ });
+ // 放在TaskQueue 中的任务是由一个线程来进行处理的 所以执行完上一个任务才会执行下一个任务 时间是累加的
+ ctx.channel().eventLoop().execute(() -> {
+ try {
+ Thread.sleep(20 * 1000);
+ ctx.writeAndFlush("业务2处理完成");
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ });
+ // 用户自定义定时任务 -》 该任务是提交到 scheduleTaskQueue中
+
+ ctx.channel().eventLoop().schedule(new Runnable() {
+ @Override
+ public void run() {
+
+ try {
+ Thread.sleep(5 * 1000);
+ ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵4", CharsetUtil.UTF_8));
+ System.out.println("channel code=" + ctx.channel().hashCode());
+ } catch (Exception ex) {
+ System.out.println("发生异常" + ex.getMessage());
+ }
+ }
+ }, 5, TimeUnit.SECONDS);
+ }
+
+ /**
+ * 给客户端发送消息
+ */
+ @Override
+ public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
+ ctx.writeAndFlush("over");
+ }
+
+ /**
+ * 发生异常触发
+ */
+ @Override
+ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+ cause.printStackTrace();
+ ctx.close();
+ }
+
+
+ });
+ }
+ });
+ // 绑定监听端口
+ ChannelFuture future = serverBootstrap.bind(8090).sync();
+ // 对关闭通道进行监听
+ future.channel().closeFuture().sync();
+ } catch (InterruptedException e) {
+
+ } finally {
+ //关闭主线程组
+ bossGroup.shutdownGracefully();
+ //关闭工作线程组
+ workGroup.shutdownGracefully();
+ }
+ }
+}
+
异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的组件在完成后,通过状态、通知和回调来通知调用者。
+Netty
中的 I/O 操作是异步的,包括 Bind、Write、Connect
等操作会简单的返回一个 ChannelFuture
。 调用者并不能立刻获得结果,而是通过 Future-Listener
机制,用户可以方便的主动获取或者通过通知机制获得 IO 操作结果。
当 Future
对象刚刚创建时,处于非完成状态,调用者可以通过返回的 ChannelFuture
来获取操作执行的状态,注册监听函数来执行完成后的操作。
常见有如下操作:
+isDone
方法来判断当前操作是否完成;isSuccess
方法来判断已完成的当前操作是否成功;getCause
方法来获取已完成的当前操作失败的原因;isCancelled
方法来判断已完成的当前操作是否被取消;addListener
方法来注册监听器,当操作已完成(isDone方法返回完成),将会通知指定的监听器;如果 Future
对象已完成,则通知指定的监听器//绑定一个端口并且同步,生成了一个ChannelFuture对象
+//启动服务器(并绑定端口)
+ChannelFuture cf = bootstrap.bind(6668).sync();
+//给cf注册监听器,监控我们关心的事件
+cf.addListener(new ChannelFutureListener() {
+ @Override
+ public void operationComplete (ChannelFuture future) throws Exception {
+ if (cf.isSuccess()) {
+ System.out.println("监听端口6668成功");
+ } else {
+ System.out.println("监听端口6668失败");
+ }
+ }
+});
+
服务端
+public class MainTestServer {
+ public static void main(String[] args) {
+ //new 一个主线程组
+ EventLoopGroup bossGroup = new NioEventLoopGroup(1);
+ //new 一个工作线程组
+ EventLoopGroup workGroup = new NioEventLoopGroup(200);
+ // 启动器, 负责组装netty组件 启动服务器
+ try {
+ ServerBootstrap serverBootstrap = new ServerBootstrap()
+ // BossEventLoop WorkEventLoop 每个 EventLoop 就是 一个选择器 + 一个线程
+ .group(bossGroup, workGroup)
+ // 选择服务器 ServerSocketChannel 具体实现
+ .channel(NioServerSocketChannel.class)
+ //设置队列大小
+ .option(ChannelOption.SO_BACKLOG, 1024)
+ // 两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文
+ .childOption(ChannelOption.SO_KEEPALIVE, true)
+ // 决定了 workEventLoop 能做那些操作
+ .childHandler(
+ // 建立连接后会被调用; 作用: 初始化 + 添加其他的 handler
+ new ChannelInitializer<NioSocketChannel>() {
+ // 当客户端请求发过来时 才会调用
+ @Override
+ protected void initChannel(NioSocketChannel channel) throws Exception {
+ channel.pipeline().addLast("MyHttpServerCodec", new HttpServerCodec());
+ // 自定义handler
+ channel.pipeline().addLast(new SimpleChannelInboundHandler<HttpObject>() {
+ @Override
+ protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
+ System.out.println("对应的channel=" + ctx.channel() + " pipeline=" + ctx.pipeline() + " 通过pipeline获取channel" + ctx.pipeline().channel());
+
+ System.out.println("当前ctx的handler=" + ctx.handler());
+
+ //判断 msg 是不是 httprequest请求
+ if (msg instanceof HttpRequest) {
+
+ System.out.println("ctx 类型=" + ctx.getClass());
+
+ System.out.println("pipeline hashcode" + ctx.pipeline().hashCode() + " TestHttpServerHandler hash=" + this.hashCode());
+
+ System.out.println("msg 类型=" + msg.getClass());
+ System.out.println("客户端地址" + ctx.channel().remoteAddress());
+
+ //获取到
+ HttpRequest httpRequest = (HttpRequest) msg;
+ //获取uri, 过滤指定的资源
+ URI uri = new URI(httpRequest.uri());
+ if ("/favicon.ico".equals(uri.getPath())) {
+ System.out.println("请求了 favicon.ico, 不做响应");
+ return;
+ }
+ //回复信息给浏览器 [http协议]
+
+ ByteBuf content = Unpooled.copiedBuffer("hello, 我是服务器", CharsetUtil.UTF_8);
+
+ //构造一个http的相应,即 httpresponse
+ FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content);
+
+ response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
+ response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
+
+ //将构建好 response返回
+ ctx.writeAndFlush(response);
+ }
+ }
+ });
+ }
+ });
+ // 绑定监听端口
+ ChannelFuture future = serverBootstrap.bind(8090).sync();
+ future.addListener( future1 -> {
+ if (future.isSuccess()) {
+ System.out.println("监听端口8090成功");
+ }else{
+ System.out.println("监听端口8090失败");
+ }
+ });
+ // 对关闭通道进行监听
+ future.channel().closeFuture().sync();
+ } catch (InterruptedException e) {
+
+ } finally {
+ //关闭主线程组
+ bossGroup.shutdownGracefully();
+ //关闭工作线程组
+ workGroup.shutdownGracefully();
+ }
+ }
+}
+
TCP 是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的 socket,因此,发送端为了将多个发给接收端的包,更有效的发给对方,使用了优化方法(Nagle 算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样做虽然提高了效率,但是接收端就难于分辨出完整的数据包了,由于 TCP 无消息保护边界,需要在接收端处理消息边界问题,也就是我们所说的粘包、拆包问题。
+拆包和粘包是在socket编程中经常出现的情况,在socket通讯过程中,如果通讯的一端一次性连续发送多条数据包,tcp协议会将多个数据包打包成一个tcp报文发送出去,这就是所谓的粘包。而如果通讯的一端发送的数据包超过一次tcp报文所能传输的最大值时,就会将一个数据包拆成多个最大tcp长度的tcp报文分开传输,这就叫做拆包。
+对于粘包的情况,要对粘在一起的包进行拆包。对于拆包的情况,要对被拆开的包进行粘包,即将一个被拆开的完整应用包再组合成一个完整包。比较通用的做法就是每次发送一个应用数据包前在前面加上四个字节的包长度值,指明这个应用包的真实长度。
+使用netty解决拆包、粘包问题代码示例:
+客户端代码
+@SpringBootApplication
+public class NettyClientApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(NettyClientApplication.class, args);
+ }
+}
+
+@Slf4j
+@Component
+public class StartNetty implements CommandLineRunner {
+
+ private final NettyClient nettyClient;
+
+ public StartNetty(NettyClient nettyClient) {
+ this.nettyClient = nettyClient;
+ }
+
+ @Override
+ public void run(String... args) throws Exception {
+ log.info("启动netty客户端 ...");
+ nettyClient.start();
+ }
+}
+
+@Slf4j
+@Component
+public class NettyClient {
+ /**
+ * Netty客户端启动
+ */
+ public void start() {
+ EventLoopGroup group = new NioEventLoopGroup();
+ Bootstrap bootstrap = new Bootstrap()
+ .group(group)
+ //该参数的作用就是禁止使用Nagle算法,使用于小数据即时传输
+ .option(ChannelOption.TCP_NODELAY, true)
+ .channel(NioSocketChannel.class)
+ .handler(new NettyClientInitializer());
+ try {
+ ChannelFuture future = bootstrap.connect("127.0.0.1", 9000).sync();
+ log.info("客户端成功....");
+ //发送消息
+ future.channel().writeAndFlush("客户端请求数据");
+ // 等待连接被关闭
+ future.channel().closeFuture().sync();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } finally {
+ group.shutdownGracefully();
+ }
+ }
+}
+
+
+
+public class NettyClientInitializer extends ChannelInitializer<SocketChannel> {
+ @Override
+ protected void initChannel(SocketChannel socketChannel) throws Exception {
+ socketChannel.pipeline().addLast("decoder", new MyMessageDecoder());
+ socketChannel.pipeline().addLast("encoder", new MyMessageEncoder());
+ socketChannel.pipeline().addLast(new NettyClientHandler());
+ }
+}
+
+
+@Slf4j
+public class NettyClientHandler extends ChannelInboundHandlerAdapter {
+ @Override
+ public void channelActive(ChannelHandlerContext ctx) throws Exception {
+ log.info("客户端Active .....");
+ // 模拟tcp粘包
+ for (int i = 0; i < 5; i++) {
+ String mes = "今天天气冷,吃火锅";
+ byte[] content = mes.getBytes(Charset.forName("utf-8"));
+ int length = mes.getBytes(Charset.forName("utf-8")).length;
+
+ // 解决tcp粘包问题
+ MessageProtocol messageProtocol = new MessageProtocol();
+ messageProtocol.setLen(length);
+ messageProtocol.setContent(content);
+ ctx.writeAndFlush(messageProtocol);
+ }
+ }
+
+ /**
+ * 收到服务端的消息
+ */
+ @Override
+ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+ log.info("客户端收到消息: {}", msg.toString());
+ MessageProtocol mp = (MessageProtocol)msg;
+ int len = mp.getLen();
+ byte[] content = mp.getContent();
+
+ System.out.println("客户端接收到消息如下");
+ System.out.println("长度=" + len);
+ System.out.println("内容=" + new String(content, StandardCharsets.UTF_8));
+
+
+ }
+
+ /**
+ * 客户端异常时触发
+ */
+ @Override
+ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+ cause.printStackTrace();
+ ctx.close();
+ }
+}
+
服务端代码
+@SpringBootApplication
+public class NettyServerApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(NettyServerApplication.class, args);
+ }
+}
+
+@Slf4j
+@Component
+public class StartNetty implements CommandLineRunner {
+
+ private final NettyServer nettyServer;
+
+ public StartNetty(NettyServer nettyServer) {
+ this.nettyServer = nettyServer;
+ }
+
+ /**
+ * 启动netty 让netty随着项目一起启动
+ */
+ @Override
+ public void run(String... args) throws Exception {
+ log.info("netty 服务端启动 ...");
+ nettyServer.start(new InetSocketAddress("127.0.0.1", 9000));
+ }
+}
+
+@Slf4j
+@Component
+public class NettyServer {
+
+ /**
+ * Netty服务启动
+ */
+ public void start(InetSocketAddress socketAddress) {
+ //new 一个主线程组
+ EventLoopGroup bossGroup = new NioEventLoopGroup(1);
+ //new 一个工作线程组
+ EventLoopGroup workGroup = new NioEventLoopGroup(200);
+
+ ServerBootstrap bootstrap = new ServerBootstrap()
+ .group(bossGroup, workGroup)
+ .channel(NioServerSocketChannel.class)
+ .childHandler(new ServerChannelInitializer())
+ .localAddress(socketAddress)
+ //设置队列大小
+ .option(ChannelOption.SO_BACKLOG, 1024)
+ // 两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文
+ .childOption(ChannelOption.SO_KEEPALIVE, true);
+ //绑定端口,开始接收进来的连接
+ try {
+ // 绑定端口 生成一个ChannelFuture 对象 启动服务器
+ ChannelFuture future = bootstrap.bind(socketAddress).sync();
+ log.info("服务器启动开始监听端口: {}", socketAddress.getPort());
+ // 对关闭通道进行监听
+ future.channel().closeFuture().sync();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } finally {
+ //关闭主线程组
+ bossGroup.shutdownGracefully();
+ //关闭工作线程组
+ workGroup.shutdownGracefully();
+ }
+ }
+}
+
+public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> {
+
+ @Override
+ protected void initChannel(SocketChannel socketChannel) throws Exception {
+ socketChannel.pipeline().addLast("decoder", new MyMessageDecoder());
+ socketChannel.pipeline().addLast("encoder", new MyMessageEncoder());
+ socketChannel.pipeline().addLast(new NettyServerHandler());
+ }
+}
+
+@Slf4j
+public class NettyServerHandler extends ChannelInboundHandlerAdapter {
+ /**
+ * 客户端连接会触发
+ */
+ @Override
+ public void channelActive(ChannelHandlerContext ctx) throws Exception {
+ log.info("服务端 Active......");
+ }
+
+ /**
+ * 客户端发消息会触发
+ */
+ @Override
+ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+ log.info("服务器收到消息: {}", msg.toString());
+ MessageProtocol mp = (MessageProtocol)msg;
+ int len = mp.getLen();
+ byte[] content = mp.getContent();
+
+ System.out.println();
+ System.out.println();
+ System.out.println();
+ System.out.println("服务器接收到信息如下");
+ System.out.println("长度=" + len);
+ System.out.println("内容=" + new String(content, Charset.forName("utf-8")));
+
+ //回复消息
+ String responseContent = UUID.randomUUID().toString();
+ int responseLen = responseContent.getBytes("utf-8").length;
+ byte[] responseContent2 = responseContent.getBytes("utf-8");
+ //构建一个协议包
+ MessageProtocol messageProtocol = new MessageProtocol();
+ messageProtocol.setLen(responseLen);
+ messageProtocol.setContent(responseContent2);
+
+ ctx.writeAndFlush(messageProtocol);
+ }
+
+ /**
+ * 给客户端发送消息
+ */
+ @Override
+ public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
+ ctx.writeAndFlush("hello client");
+ }
+
+ /**
+ * 发生异常触发
+ */
+ @Override
+ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+ cause.printStackTrace();
+ ctx.close();
+ }
+}
+
公共代码部分
+public class MessageProtocol {
+
+ private int len;
+
+ private byte[] content;
+
+ public int getLen() {
+ return len;
+ }
+
+ public void setLen(int len) {
+ this.len = len;
+ }
+
+ public byte[] getContent() {
+ return content;
+ }
+
+ public void setContent(byte[] content) {
+ this.content = content;
+ }
+
+ @Override
+ public String toString() {
+ return "MessageProtocol{" +
+ "len=" + len +
+ ", content=" + new String(content) +
+ '}';
+ }
+}
+
+public class MyMessageDecoder extends ReplayingDecoder<Void> {
+
+ @Override
+ protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
+ //需要将得到二进制字节码-> MessageProtocol 数据包(对象)
+ int length = in.readInt();
+ byte[] content = new byte[length];
+ in.readBytes(content);
+
+ //封装成 MessageProtocol 对象,放入 out, 传递下一个handler业务处理
+ MessageProtocol messageProtocol = new MessageProtocol();
+ messageProtocol.setLen(length);
+ messageProtocol.setContent(content);
+ out.add(messageProtocol);
+ }
+}
+
+public class MyMessageEncoder extends MessageToByteEncoder<MessageProtocol> {
+
+ @Override
+ protected void encode(ChannelHandlerContext ctx, MessageProtocol msg, ByteBuf out) throws Exception {
+ System.out.println("MyMessageEncoder encode 方法被调用");
+ out.writeInt(msg.getLen());
+ out.writeBytes(msg.getContent());
+ }
+}
+
+ +
+ + + + + +Java中的集合主要包括 Collection
和 Map
两种,Collection
存储着对象的集合,而 Map
存储着键值对(两个对象)的映射表。
如果你看过ArrayList类源码,就知道ArrayList底层是通过数组来存储元素的,所以如果严格来说,数组也算集合的一种。
+Java中提供的数组是用来存储固定大小的同类型元素,所以Java数组就是同类数据元素的集合。
+数组是引用数据类型,如果使用了没有开辟空间的数组,则一定会出现NullPointerException
异常信息。所以数组本质上也是Java对象,能够向下或者向上转型,能使用instanceof
关键字。
// 数组的父类也是Object,可以将a向上转型到Object
+int[] a = new int[8];
+Object obj = a ;
+
+// 可以进行向下转型
+int[] b = (int[])obj;
+
+// 可以用instanceof关键字进行类型判定
+if(obj instanceof int[]){
+}
+
+++
void method_name(int ... value)
方法中变参就是当数组处理的,参数为定参的编译后就是数组。一个方法只能有一个变参,即使是不同的类型也不行,变参参数只能在形参列表的末尾,如果传入的是数组,则只能传一个。
数组优点:
+数组缺点:
+数组的优点是效率高,但为此,所付出的代价就是数组对象的大小被固定。这也使得在工作中,数组并不实用。所以我们应该优选容器,而不是数组。只有在已证明性能成为问题的时候,并且确定切换到数组对性能提高有帮助时,才应该将项目重构为使用数组。
+由于数组没有提供任何的封装,所有对元素的操作,都是通过自定义的方法实现的,对数组元素的操作比较麻烦,好在Java自带了一些API供开发者调用。
+ int[] array1 = { 1,2,3,4,5 };
+ int[] array2 = new int[10];
+ int[] array3 = new int[]{ 1,2,3,4,5 };
+
需要注意的是[],写在数组名称的前后都可以,但是推荐第一种写法:
+ int[] array1 = { 1,2,3,4,5 };
+ int array2[] = { 1,2,3,4,5 };
+
for (int i = 0; i < array1.length; i++) {
+ System.out.println(array1[i]);
+ }
+
// 最简单方法,利用 hashSet 集合去重
+Set<Integer> set2 = new HashSet<Integer>();
+for (int i = 0; i < arr11.length; i++) {
+ set2.add(arr11[i]);
+}
+
// 数组转成set集合
+Set<String> set = new HashSet<String>(Arrays.asList(array2));
+
+// 数组转list
+List<String > list2 = Arrays.asList(array);
+
// 原生方法 或 8种排序算法
+ Arrays.sort(arr);
+
// 待复制的数组
+int[] arr = {1, 2, 3, 4};
+
+// 指定新数组的长度
+int[] arr2 = Arrays.copyOf(arr, 10);
+
+// 只复制从索引[1]到索引[3]之间的元素(不包括索引[3]的元素)
+int[] arr3 = Arrays.copyOfRange(arr, 1, 3);
+
在List接口实现类中,最常用的就是ArrayList,ArrayList 类是一个可以动态修改的数组,与普通数组的区别就是它是没有固定大小的限制,可以添加或删除元素。
+ArrayList 继承了 AbstractList ,并实现了 List、RandomAccess, Cloneable 接口:
+public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess,Cloneable,Serializable
+
Random是随机的意思,Access是访问的意思,合起来就是随机访问的意思。
+RandomAccess接口是一个标记接口,用来标记实现的List集合具备快速随机访问的能力。所有的List实现都支持随机访问的,只是基于基本结构的不同,实现的速度不同罢了。
+当一个List拥有快速访问功能时,其遍历方法采用随机访问速度最快,而没有快速随机访问的List采用顺序访问的速度最快。如果集合中的数据量过大需要遍历时,此时需要格外注意,因为不同的遍历方式会影响很大,可以使用instanceof
关键字来判断该类有没有RandomAccess标记:
// 假设 list 数据量非常大,推荐写法
+List<Object> list = ...;
+
+if(list instanceof RandomAccess){
+ // 随机访问
+ for (int i = 0;i< list.size();i++) {
+ System.out.println(list.get(i));
+ }
+}else {
+ // 顺序访问
+ for(Object obj: list) {
+ System.out.println(obj);
+ }
+}
+
在List中ArrayList被RandomAccess接口标记,而LinkedList没有被RandomAccess接口标记,所以ArrayList适合随机访问,LinkedList适合顺序访问。
+Cloneable接口是Java开发中常用的一个接口之一,它是一个标记接口。
+如果一个想要拷贝一个对象,就需要重写Object中的clone方法并让其实现Cloneable接口,如果只重写clone方法,不实现Cloneable接口就会报CloneNotSupportedException异常。
+JDK中clone方法源码:
+protected native Object clone() throws CloneNotSupportedException;
+
应当注意的是,clone()
方法并不是 Cloneable
接口的方法,而是 Object
的一个 protected
方法。Cloneable
接口只是规定,如果一个类没有实现 Cloneable
接口又调用了 clone()
方法,就会抛出 CloneNotSupportedException
。
换言之,clone方法规定了想要拷贝对象,就需要实现Cloneable方法,clone方法让Cloneable接口变得有意义。
+clone
,并指向被复制过的新对象。如果一个被复制的属性都是基本类型,那么只需要实现当前类的cloneable
机制就可以了,此为浅拷贝。
如果被复制对象的属性包含其他实体类对象引用,那么这些实体类对象都需要实现cloneable
接口并覆盖clone()
方法。
浅拷贝:
+public class ShallowCloneExample implements Cloneable {
+
+ private int[] arr;
+
+ public ShallowCloneExample() {
+ arr = new int[10];
+ for (int i = 0; i < arr.length; i++) {
+ arr[i] = i;
+ }
+ }
+
+ public void set(int index, int value) {
+ arr[index] = value;
+ }
+
+ public int get(int index) {
+ return arr[index];
+ }
+
+ @Override
+ protected ShallowCloneExample clone() throws CloneNotSupportedException {
+ return (ShallowCloneExample) super.clone();
+ }
+}
+
ShallowCloneExample e1 = new ShallowCloneExample();
+ShallowCloneExample e2 = null;
+try {
+ e2 = e1.clone();
+} catch (CloneNotSupportedException e) {
+ e.printStackTrace();
+}
+e1.set(2, 222);
+System.out.println(e2.get(2)); // 222
+
深拷贝:
+public class DeepCloneExample implements Cloneable {
+
+ private int[] arr;
+
+ public DeepCloneExample() {
+ arr = new int[10];
+ for (int i = 0; i < arr.length; i++) {
+ arr[i] = i;
+ }
+ }
+
+ public void set(int index, int value) {
+ arr[index] = value;
+ }
+
+ public int get(int index) {
+ return arr[index];
+ }
+
+ @Override
+ protected DeepCloneExample clone() throws CloneNotSupportedException {
+ DeepCloneExample result = (DeepCloneExample) super.clone();
+ result.arr = new int[arr.length];
+ for (int i = 0; i < arr.length; i++) {
+ result.arr[i] = arr[i];
+ }
+ return result;
+ }
+}
+
DeepCloneExample e1 = new DeepCloneExample();
+DeepCloneExample e2 = null;
+try {
+ e2 = e1.clone();
+} catch (CloneNotSupportedException e) {
+ e.printStackTrace();
+}
+e1.set(2, 222);
+System.out.println(e2.get(2)); // 2
+
clone方法调用栈:
+clone
+ -> Object.clone
+ -> Arrays.copyOf(T[] original, int newLength)
+ -> Arrays.copyOf(U[] original, int newLength, Class<? extends T[]> newType)
+
文档注释大意:返回这个ArrayList实例的浅拷贝(元素本身不会被复制)。
+public class ArrayList implements Cloneable {
+
+ transient Object[] elementData;
+
+ /**
+ * Returns a shallow copy of this <tt>ArrayList</tt> instance. (The
+ * elements themselves are not copied.)
+ *
+ * @return a clone of this <tt>ArrayList</tt> instance
+ */
+ public Object clone() {
+ try {
+ // 调用Object类的clone方法
+ ArrayList<?> v = (ArrayList<?>) super.clone();
+
+ // 将集合中的元素进行拷贝
+ v.elementData = Arrays.copyOf(elementData, size);
+ v.modCount = 0;
+ return v;
+ } catch (CloneNotSupportedException e) {
+ // this shouldn't happen, since we are Cloneable
+ throw new InternalError(e);
+ }
+ }
+}
+
public class Arrays{
+ public static <T> T[] copyOf(T[] original, int newLength) {
+ return (T[]) copyOf(original, newLength, original.getClass());
+ }
+
+ public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
+ @SuppressWarnings("unchecked")
+ T[] copy = ((Object)newType == (Object)Object[].class)
+ ? (T[]) new Object[newLength]
+ : (T[]) Array.newInstance(newType.getComponentType(), newLength);
+ System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));
+ return copy;
+ }
+}
+
ArrayList中clone方法底层是调用父类的clone方法,父类没有重写clone方法所以调用的是Object类的clone方法。
+在ArrayList中核心方法最终调用Arrays.copyOf
方法,不论怎样都会创建一个Object数组。
+++
Arrays.newInstance(Class<?> componentType,int length)
方法作用,创建具有指定组件类型和长度的新数组。
最终使用System.arraycopy
方法将之前的旧数组中的元素拷贝到新创建的数组中,然后赋值给ArrayList.elementData
对象并返回。
因为ArrayList底层使用数组保存数据的,而数组一旦被创建就不能改变大小,但是ArrayList的长度是可以改变的,所以可以通过ArrayList类中的add方法找到数组扩容方法。
+add方法调用栈:
+add
+ -> ensureCapacityInternal()
+ -> calculateCapacity()
+ -> ensureExplicitCapacity()
+ -> grow()
+
private void grow(int minCapacity) {
+ // overflow-conscious code
+ int oldCapacity = elementData.length;
+ int newCapacity = oldCapacity + (oldCapacity >> 1);
+ if (newCapacity - minCapacity < 0)
+ newCapacity = minCapacity;
+ 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);
+ }
+
ArrayList容量:如果没有指定容量创建数组,默认会创建一个长度为10的数组用来保存元素,之后通过:
+ int newCapacity = oldCapacity + (oldCapacity >> 1);
+
每次扩容都是原容量的1.5倍。
++++
>>
,右移几位就是相当于除以2的几次幂 +<<
,左移几位就是相当于乘以2的几次幂
最后通过Arrays.copyOf方法将之前的数组中元素,全部移到新创建的数组上。
+由于频繁的扩容数组会对性能产生影响,如果在ArrayList中要存储很大的数据,就需要在ArrayList的有参构造中指定数组的长度:
+List<String> list = new ArrayList(1000000);
+
需要注意的是创建指定长度的ArrayList,在没有add之前ArrayList中的数组已经初始化了,但是List的大小没变,因为List的大小是由size决定的。
+ArrayList与LinkedList性能比较是一道经典的面试题,ArrayList查找快,增删慢;而LinkedList增删快,查找慢。
+造成这种原因是因为底层的数据结构不一样,ArrayList底层是数组,而数组的中的元素内存分配都是连续的,并且数组中的元素只能存放一种,这就造成了数组中的元素地址是有规律的,数组中查找元素快速的原因正是利用了这一特点。
+++查询方式为: 首地址+(元素长度*下标) +例如:new int arr[5]; arr数组的地址假设为0x1000,arr[0] ~ arr[5] 地址可看作为 0x1000 + i * 4。
+
而LinkedList在Java中的底层结构是对象,每一个对象结点中都保存了下一个结点的位置形成的链表结构,由于LinkedList元素的地址是不连续的,所以没办法按照数组那样去查找,所以就比较慢。
+由于数组一旦分配了大小就不能改变,所以ArrayList在进行添加操作时会创建新的数组,如果要添加到ArrayList中的指定的位置,是通过System.arraycopy方法将数组进行复制,新的数组会将待插入的指定位置空余出来,最后在将元素添加到集合中。
+在进行删除操作时是通过System.arraycopy方法,将待删除元素后面剩余元素复制到待删除元素的位置。当ArrayList里有大量数据时,这时候去频繁插入或删除元素会触发底层数组频繁拷贝,效率不高,还会造成内存空间的浪费。
+LinkedList在进行添加,删除操作时,会用二分查找法找到将要添加或删除的元素,之后再设置对象的下一个结点来进行添加或删除操作。
+++二分查找法:也称为折半查找法,是一种适用于大量数据查找的方法,但是要求数据必须的排好序的,每次以中间的值进行比较,根据比较的结果可以直接舍去一半的值,直至全部找完(可能会找不到)或者找到数据为止。
+此处LinkedList会比较查找的元素是距离头结点比较近,还是尾结点比较近,距离哪边较近则从哪边开始查找。
+
ArrayList,获取元素效率非常的高,时间复杂度是O(1),而查找,插入和删除元素效率似乎不太高,时间复杂度为O(n)。
+LinkedList,正与ArrayList相反,获取第几个元素依次遍历复杂度O(n),添加到末尾复杂度O(1),添加到指定位置复杂度O(n),删除元素,直接指针指向操作复杂度O(1)。
+注意,ArrayList的增删不一定比LinkedList效率低,但是ArrayList查找效率一定比LinkedList高,如果在List靠近末尾的地方插入,那么ArrayList只需要移动较少的数据,而LinkedList则需要一直查找到列表尾部,反而耗费较多时间,这时ArrayList就比LinkedList要快。
+使用场景:
+众所周知,ArrayList是线程不安全的:
+public class MainTest {
+ // 如果没有报错,需要多试几次
+ public static void main(String[] args) {
+ ArrayList<String> arrayList = new ArrayList<>();
+ for(int i=0; i< 10; i++) {
+ new Thread(() -> {
+ arrayList.add(UUID.randomUUID().toString());
+ System.out.println(arrayList);
+ },String.valueOf(i)).start();
+ }
+ }
+}
+
为避免偶然事件,请重复多试几次上面的代码,很大情况会出现ConcurrentModificationException
“同步修改异常”:
java.util.ConcurrentModificationException
+
出现该异常的原因是,当某个线程正在执行 add()
方法时,被某个线程打断,添加到一半被打断,没有被添加完。
保证ArrayList线程安全有以下几种方法:
+Vector
来代替 ArrayList
,Vector
是线程安全的 ArrayList
,但是由于底层是加了synchronized
,性能略差不推荐使用;
+List list = new Vector();
+list.add(UUID.randomUUID().toString());
+
Collections.synchronizedArrayList()
来创建 ArrayList
;使用 Collections
工具类来创建 ArrayList
的思路是,在 ArrayList
的外边套了一个synchronized
外壳,来使 ArrayList
线程安全;
+List list = Collections.synchronizedArrayList();
+list.add(UUID.randomUUID().toString());
+
CopyOnWriteArrayList()
来保证 ArrayList
线程安全;CopyWriteArrayList
字面意思就是在写的时候复制,主要思想就是读写分离的思想。CopyWriteArrayList
之所以线程安全的原因是在源码里面使用ReentrantLock
,所以保证了某个线程在写的时候不会被打断;
+CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
+list.add(UUID.randomUUID().toString());
+
队列是一种经常使用的集合。Queue实际上是实现了一个先进先出(FIFO:First In First Out)的有序列表。它和List的区别在于,List可以在任意位置添加和删除元素,而Queue只有两个操作:
+常见实现:
+Queue实现通常不允许插入null元素,尽管一些实现,如LinkedList,不禁止插入null元素。即使在允许它的实现中,null也不应插入Queue中,因为poll方法也使用null作为特殊返回值,用来表示队列不包含任何元素。
+++poll(): 检索并删除此队列的头部,如果此队列为空,则返回null +peek(): 检索但不删除此队列的头部,如果此队列为空,则返回null
+
相关操作:
+总的来说hashMap底层将key-value(键值对)当成一个整体来处理,hashMap底层采用一个 Entry 数组保存所有的键值对,当存储一个entry对象时,会根据key的hash算法来决定存放在数组中的位置,在根据equals方法来确定在链表中的位置,读取一个entry对象,先根据hash算法确定在数组中的位置,再根据equals来获取该值,equals和equals在hashMap中就像一个坐标一样,来确定hashMap中的值。
+capacity
: 容量,默认16;loadFactor
: 负载因子,表示HashMap满的程度,默认值为0.75f,也就是说默认情况下,当HashMap中元素个数达到了容量的3/4的时候就会进行自动扩容;threshold
: 阈值;阈值 = 容量 * 负载因子
。默认12;在JDK1.8时,如果存储Map中数组元素对应的索引的每个链表超过8,就将单向链表转化为红黑树;当红黑树的节点少于6个的时候又开始使用链表。
+当有发生大量的hash冲突时,因为链表遍历效率很慢,为了提升查询的效率,所以使用了红黑树的数据结构。
+JDK文档注释:
+++Because TreeNodes are about twice the size of regular nodes, we use them only when bins contain enough nodes to warrant use (see TREEIFY_THRESHOLD). +And when they become too small (due to removal or resizing) they are converted back to plain bins.
+
单个 TreeNode 需要占用的空间大约是普通 Node 的两倍,所以只有当包含足够多的 Nodes 时才会转成 TreeNodes,而是否足够多就是由 TREEIFY_THRESHOLD 的值(默认值8)决定的。而当桶中节点数由于移除或者 resize 变少后,又会变回普通的链表的形式,以便节省空间,这个阈值是 UNTREEIFY_THRESHOLD(默认值6)。
+JDK1.8HashMap文档注释:
+++如果 hashCode 分布良好,也就是 hash 计算的结果离散好的话,那么红黑树这种形式是很少会被用到的,因为各个值都均匀分布,很少出现链表很长的情况。 +在理想情况下,链表长度符合泊松分布,各个长度的命中概率依次递减,当长度为 8 的时候,概率仅为 0.00000006。这是一个小于千万分之一的概率,通常我们的 Map 里面是不会存储这么多的数据的,所以通常情况下,并不会发生从链表向红黑树的转换。
+
HashMap是通过hash算法,来判断对象应该放在哪个桶里面的;JDK 并不能阻止我们用户实现自己的哈希算法,如果我们故意把哈希算法变得不均匀,那么每次存放对象很容易造成hash冲突。
+链表长度超过 8 就转为红黑树的设计,更多的是为了防止用户自己实现了不好的哈希算法时导致链表过长,从而导致查询效率低,而此时转为红黑树更多的是一种保底策略,用来保证极端情况下查询的效率。红黑树的引入保证了在大量hash冲突的情况下,HashMap还具有良好的查询性能。
+为了防止出现节点个数频繁在一个相同的数值来回切换。
+举个极端例子,现在单链表的节点个数是9,开始变成红黑树,然后红黑树节点个数又变成8,就又得变成单链表,然后节点个数又变成9,就又得变成红黑树,这样的情况消耗严重浪费。因此干脆错开两个阈值的大小,使得变成红黑树后“不那么容易”就需要变回单链表,同样,使得变成单链表后,“不那么容易”就需要变回红黑树。
+不一定,在进行树化之前会进行判断(n = tab.length) < MIN_TREEIFY_CAPACITY)
是否需要扩容,如果表中数组元素小于这个阈值(默认是64),就会进行扩容。 因为扩容不仅能增加表中的容量,还能缩短单链表的节点数,从而更长远的解决链表遍历慢问题。
/**
+ * Replaces all linked nodes in bin at index for given hash unless
+ * table is too small, in which case resizes instead.
+ */
+ final void treeifyBin(Node<K,V>[] tab, int hash) {
+ int n, index; Node<K,V> e;
+ if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
+ resize();
+ else if ((e = tab[index = (n - 1) & hash]) != null) {
+ TreeNode<K,V> hd = null, tl = null;
+ do {
+ TreeNode<K,V> p = replacementTreeNode(e, null);
+ if (tl == null)
+ hd = p;
+ else {
+ p.prev = tl;
+ tl.next = p;
+ }
+ tl = p;
+ } while ((e = e.next) != null);
+ if ((tab[index] = hd) != null)
+ hd.treeify(tab);
+ }
+ }
+
HashMap中的负载因子这个值现在在JDK的源码中默认是0.75:
+/**
+ * The load factor used when none specified in constructor.
+ */
+static final float DEFAULT_LOAD_FACTOR = 0.75f;
+
在JDK的官方文档中解释如下:
+++As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. +Higher values decrease the space overhead but increase the lookup cost (reflected in most of the operations of the HashMap class, including get and put). +The expected number of entries in the map and its load factor should be taken into account when setting its initial capacity, so as to minimize the number of rehash operations. +If the initial capacity is greater than the maximum number of entries divided by the load factor, no rehash operations will ever occur.
+
大意:一般来说,默认的负载因子(0.75)在时间和空间成本之间提供了很好的权衡。更高的值减少了空间开销,但增加了查找成本(反映在HashMap类的大多数操作中,包括get和put)。在设置映射的初始容量时,应该考虑映射中预期的条目数及其负载因子,以便最小化重哈希操作的数量。如果初始容量大于最大条目数除以负载因子,则不会发生重新散列操作。
+负载因子和hashmap中的扩容有关,当hashmap中的元素大于临界值(threshold = loadFactor * capacity
)就会扩容。所以负载因子的大小决定了什么时机扩容,扩容又影响到了hash碰撞的频率。所以设置一个合理的负载因子可以有效的避免hash碰撞。
设置为0.75的其他解释:
+log(2)
的时候比较合理;实际大小是16。其容量为不小于指定容量的2的幂数。
+为什么容量始终是2的N次方?
+为了减少Hash碰撞,尽量使Hash算法的结果均匀分布。
+当使用put方法时,到底存入HashMap中的那个数组中?这时是通过hash算法决定的,如果某一个数组中的链表过长旧会影响查询的效率;那么为了避免出现hash碰撞,让hash尽可能的散列分布,就需要在hash算法上做文章。
+JDK1.7通过逻辑与运算,来判断这个元素该进入哪个数组;在下面的代码中length的长度始终为不小于指定容量的2的幂数。
+static int indexFor(int h, int length) {
+ return h & (length - 1);
+}
+
为了更好的理解举个例子:假设h=2或h=3,length=15,进行与运算,最终逻辑与运算后的结果是一致的,因为最终结果是一致的所以就发生了hash碰撞,这种问题多了以后会造成容器中的元素分布不均匀,都分配在同一个数组上,在查询的时候就减慢了查询的效率,另一方面也造成空间的浪费。
+-- 2转换为2进制与15-1进行&运算
+ 0000 0010
+& 0000 1110
+————————————
+ 0000 1110
+
+-- 3转换为2进制与15-1进行&运算
+ 0000 0011
+& 0000 1110
+————————————
+ 0000 1110
+
为了避免上面length=15
这类问题出现,所以集合的容量采用必须是2的N次幂这种方式,因为2的N次幂的结果减一转换为二进制后都是以...1111
结尾的,所以在进行逻辑与运算时碰撞几率小。
在JDK1.8中,在putVal()
方法中通过i = (n - 1) & hash
来计算key的散列地址:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
+ boolean evict) {
+ // 此处省略了代码
+ // i = (n - 1) & hash]
+ if ((p = tab[i = (n - 1) & hash]) == null)
+
+ tab[i] = newNode(hash, key, value, null);
+
+
+ else {
+ // 省略了代码
+ }
+}
+
++这里的 “&” 等同于 %",但是"%“运算的速度并没有”&“的操作速度快;”&“操作能代替”%“运算,必须满足一定的条件,也就是
+a%b=a&(b-1)
仅当b是2的n次方的时候方能成立。
容器容量怎么保持始终为2的N次方?
+HashMap
的tableSizeFor()
方法做了处理,能保证n永远都是2次幂。
如果用户制定了初始容量,那么HashMap会计算出比该数大的第一个2的幂作为初始容量;另外就是在扩容的时候,也是进行成倍的扩容,即4变成8,8变成16。
+/**
+ * Returns a power of two size for the given target capacity.
+ */
+static final int tableSizeFor(int cap) {
+
+ // 假设n=17
+ // n = 00010001 - 00010000 = 00010000 = 16
+ int n = cap - 1;
+
+ // n = (00010000 | 00001000) = 00011000 = 24
+ n |= n >>> 1;
+
+ // n = (00011000 | 00000110) = 00011110 = 30
+ n |= n >>> 2;
+
+ // n = (00011110 | 00000001) = 00011111 = 31
+ n |= n >>> 4;
+
+ // n = (00011111 | 00000000) = 00011111 = 31
+ n |= n >>> 8;
+
+ // n = (00011111 | 00000000) = 00011111 = 31
+ n |= n >>> 16;
+
+ // n = 00011111 = 31,MAXIMUM_CAPACITY:Integer的最大长度
+ // (31 < 0) ? 1 : (31 >= Integer的最大长度) ? Integer的最大长度 : 31 + 1 ;
+ // 即最终返回 32 = 2 的 (n=5)次方
+ return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
+}
+
发现上面在进行>>>
操作时会将cap的二进制值变为最高位后边全是1,00010001 -> 00011111
这个算法就导致了任意传入一个数值,会将该数字变为它的2倍减1,因为任何尾数全为1的在加1都为2的倍数。
至于开头减1,是因为如果给定的n已经是2的次幂,但是不进行减1操作的话,那么得到的值就是大于给定值的最小2的次幂值,例如传入4就会返回8。
+为什么最大右移到16位,因为可以得到的最大值是32个1,这个是int类型存储变量的最大值,在往后就没意义了。
+没有找到相关解释,推断这应该就是个经验值,既然一定要设置一个默认的2^n 作为初始值,那么就需要在效率和内存使用上做一个权衡。这个值既不能太小,也不能太大。太小了就有可能频繁发生扩容,影响效率。太大了又浪费空间,不划算。所以,16就作为一个经验值被采用了。
+关于默认容量的定义:
+/**
+ * The default initial capacity - MUST be a power of two.
+ */
+static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
+
故意把16写成1 << 4
这种形式,就是提醒开发者,这个地方要是2的次幂。
当我们使用HashMap(int initialCapacity)
来初始化容量的时候,HashMap
并不会使用我们传进来的initialCapacity
直接作为初始容量。JDK会默认帮我们计算一个相对合理的值当做初始容量。所谓合理值,其实是找到第一个比用户传入的值大的2的幂。
如果创建hashMap初始化容量设置为7,那么JDK通过计算会创建一个初始化为8的hashMap。当hashMap中的元素到0.75 * 8 = 6
就会进行扩容,这明显是我们不希望看到的。
参考JDK8中putAll
方法中的实现:
(int) ((float) expectedSize / 0.75F + 1.0F);
+
通过expectedSize / 0.75F + 1.0F
计算,7/0.75 + 1 = 10
,10经过JDK处理之后,会被设置成16,这就大大的减少了扩容的几率。
当我们明确知道HashMap中元素的个数的时候,把默认容量设置成expectedSize / 0.75F + 1.0F
是一个在性能上相对好的选择,但是,同时也会牺牲些内存。
这个算法在guava中有实现,开发的时候,可以直接通过Maps类创建一个HashMap:
+Map<String, String> map = Maps.newHashMapWithExpectedSize(7);
+
public static <K, V> HashMap<K, V> newHashMapWithExpectedSize(int expectedSize) {
+ return new HashMap(capacity(expectedSize));
+}
+
+static int capacity(int expectedSize) {
+ if (expectedSize < 3) {
+ CollectPreconditions.checkNonnegative(expectedSize, "expectedSize");
+ return expectedSize + 1;
+ } else {
+ return expectedSize < 1073741824 ? (int)((float)expectedSize / 0.75F + 1.0F) : 2147483647;
+ }
+}
+
随着HashMap中的元素增加,Hash碰撞导致获取元素方法的效率就会越来越低,为了保证获取元素方法的效率,所以针对HashMap中的数组进行扩容。扩容数组的方式只能再去开辟一个新的数组,并把之前的元素转移到新数组上。
+++PS 如何能避免哈希碰撞?
++
+- 容量太小。容量小,碰撞的概率就高了。狼多肉少,就会发生争抢。
+- hash算法不够好。算法不合理,就可能都分到同一个或几个桶中。分配不均,也会发生争抢。
+
HashMap的扩容条件就是当HashMap中的元素个数(size)超过临界值(threshold)时就会自动扩容。在HashMap中,threshold = loadFactor * capacity
。默认情况下负载因子为0.75,理解为当容器中元素到容器的3/4时就会扩容。
if (++size > threshold)
+ resize();
+
HashMap的容量是有上限的,必须小于1<<30
,即1073741824
。如果容量超出了这个数,则不再增长,且阈值会被设置为Integer.MAX_VALUE
:
// Java8
+if (oldCap >= MAXIMUM_CAPACITY) {
+ threshold = Integer.MAX_VALUE;
+ return oldTab;
+}
+// Java7
+if (oldCapacity == MAXIMUM_CAPACITY) {
+ threshold = Integer.MAX_VALUE;
+ return;
+}
+
新容量 = 旧容量 * 2
新阈值 = 新容量 * 负载因子
void addEntry(int hash, K key, V value, int bucketIndex) {
+ //size:The number of key-value mappings contained in this map.
+ //threshold:The next size value at which to resize (capacity * load factor)
+ //数组扩容条件:1.已经存在的key-value mappings的个数大于等于阈值
+ // 2.底层数组的bucketIndex坐标处不等于null
+ if ((size >= threshold) && (null != table[bucketIndex])) {
+ resize(2 * table.length);//扩容之后,数组长度变了
+ hash = (null != key) ? hash(key) : 0;//为什么要再次计算一下hash值呢?
+ bucketIndex = indexFor(hash, table.length);//扩容之后,数组长度变了,在数组的下标跟数组长度有关,得重算。
+ }
+ createEntry(hash, key, value, bucketIndex);
+}
+
void resize(int newCapacity) { //传入新的容量
+ Entry[] oldTable = table; //引用扩容前的Entry数组
+ int oldCapacity = oldTable.length;
+ if (oldCapacity == MAXIMUM_CAPACITY) { //扩容前的数组大小如果已经达到最大(2^30)了
+ threshold = Integer.MAX_VALUE; //修改阈值为int的最大值(2^31-1),这样以后就不会扩容了
+ return;
+ }
+
+ Entry[] newTable = new Entry[newCapacity]; //初始化一个新的Entry数组
+ transfer(newTable); //!!将数据转移到新的Entry数组里
+ table = newTable; //HashMap的table属性引用新的Entry数组
+ threshold = (int) (newCapacity * loadFactor);//修改阈值
+}
+
通过transfer方法将旧数组上的元素转移到扩容后的新数组上
+void transfer(Entry[] newTable) {
+ Entry[] src = table; //src引用了旧的Entry数组
+ int newCapacity = newTable.length;
+ for (int j = 0; j < src.length; j++) { //遍历旧的Entry数组
+ Entry<K, V> e = src[j]; //取得旧Entry数组的每个元素
+ if (e != null) {
+ src[j] = null;//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象)
+ do {
+ Entry<K, V> next = e.next;
+ int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置
+ e.next = newTable[i]; //标记[1]
+ newTable[i] = e; //将元素放在数组上
+ e = next; //访问下一个Entry链上的元素
+ } while (e != null);
+ }
+ }
+}
+
容量变为原来的2倍,阈值也变为原来的2倍。容量和阈值都变为原来的2倍时,负载因子还是不变。
+在1.8时做了一些优化,文档注释写的很清楚:“元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置”。也就是对比1.7的迁移到新的数组上省去了重新计算hash值的时间。
+这里的"2次幂的位置"是指长度为原来数组元素的两倍的位置;举个例子,现在容量为16,要扩容到32,要将之前的元素迁移过去,要根据hash值来判断迁移过去的位置;假设元素A:hash值:0101 0101;根据代码h & (length - 1)
可得元素A & 15
、元素A & 31
扩容之前的位置:
+ 0101 0101
+& 0000 1111
+————————————
+ 0000 0101
+
+扩容之后的位置:
+ 0101 0101
+& 0001 1111
+————————————
+ 0001 0101
+
发现规律:扩容前的hash值和扩容后的hash值,如果元素A二进制形式第三位如果是0,扩容之后就还是原来的位置,如果是1扩容后就是原来的位置加16,而16就是扩容的大小。
+ /**
+ * Initializes or doubles table size. If null, allocates in
+ * accord with initial capacity target held in field threshold.
+ * Otherwise, because we are using power-of-two expansion, the
+ * elements from each bin must either stay at same index, or move
+ * with a power of two offset in the new table.
+ *
+ * @return the table
+ */
+ final Node<K,V>[] resize() {
+ Node<K,V>[] 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;
+ }
+ 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 { // zero initial threshold signifies using defaults
+ newCap = DEFAULT_INITIAL_CAPACITY;
+ newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
+ }
+ 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<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
+ table = newTab;
+ if (oldTab != null) {
+ for (int j = 0; j < oldCap; ++j) {
+ Node<K,V> 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<K,V>)e).split(this, newTab, j, oldCap);
+ else { // preserve order
+ Node<K,V> loHead = null, loTail = null;
+ Node<K,V> hiHead = null, hiTail = null;
+ Node<K,V> next;
+ do {
+ next = e.next;
+ if ((e.hash & oldCap) == 0) {
+ if (loTail == null)
+ loHead = e;
+ else
+ loTail.next = e;
+ loTail = e;
+ }
+ else {
+ if (hiTail == null)
+ hiHead = e;
+ else
+ hiTail.next = e;
+ hiTail = e;
+ }
+ } while ((e = next) != null);
+ if (loTail != null) {
+ loTail.next = null;
+ newTab[j] = loHead;
+ }
+ if (hiTail != null) {
+ hiTail.next = null;
+ newTab[j + oldCap] = hiHead;
+ }
+ }
+ }
+ }
+ }
+ return newTab;
+ }
+
+ +
+ + + + + +++进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。例如,一个正在运行的程序的实例就是一个进程。
+
++线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。 +一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
+
进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。 +一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。
+Java 程序是多线程程序,每启动一个Java程序至少我们知道的都会包含一个主线程和一个垃圾回收线程。 +而且启动的时候,每条线程可以并行执行不同的任务。
+ ++ | 单线程 | +多线程 | +
---|---|---|
单CPU | +串行 | +并发 | +
多CPU | +串行 | +并行 | +
无论并行、并发,都可以有多个线程执行,如果是多个线程抢占一个CPU就成了并发,多个CPU同时执行多个线程就是并行。
+对于单CPU的计算机来说,同一时间是只能干一件事儿的,如果是单线程就是串行;如果是多个线程就是并发。 +而对于多CPU的计算机说,同一时间能干多个事,如果多个CPU同时执行多个线程就是并行;如果一个CPU同时执行多个线程就是并行。
+并行与并发区别图解 +
+并发是两个队列交替使用一台咖啡机,并行是两个队列同时使用两台咖啡机; +如果串行,一个队列使用一台咖啡机,那么哪怕前面那个人便秘了去厕所呆半天,后面的人也只能死等着他回来才能去接咖啡,这效率无疑是最低的。
+同步、异步一般是相对与方法来说的。
+只有多线程环境下才会异步调用方法,换言之异步调用方法则需要单独创建一个线程。
++ | 单线程 | +多线程 | +
---|---|---|
同步 | +只能同步 | +可以同步 | +
异步 | +不能异步 | +可以异步 | +
++PS 线程中的同步(synchronized)机制 +在多线程环境下,一旦一个方法或一段代码被synchronized修饰,也就意味着被同步了; +在synchronized修饰的作用域中,某一段时间内只允许一个线程进行操作数据,如果有多个线程需要操作则要排队等待。
+
++守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。
+
守护线程也称“服务线程”,在没有用户线程可服务时会自动离开。因为主要是服务其他线程所以在程序中的优先级比较低。
+举例:垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的线程,程序就不会再产生垃圾,垃圾回收器也就无事可做; +所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
+守护线程在程序中的操作演示
+public class MainTest {
+ public static void main(String[] args) {
+ int i = 0;
+ while (true) {
+ Thread daemon = new Thread(() -> {
+ System.out.println("启动线程--->" + Thread.currentThread().getName());
+ });
+ daemon.setDaemon(i==3);
+ daemon.start();
+ boolean isDaemon = daemon.isDaemon();
+ System.out.println("当前线程是否是守护线程:" + isDaemon);
+ if (isDaemon) {
+ break;
+ }
+ i++;
+ }
+
+ }
+}
+
线程状态共包含6种,6中状态又可以互相的转换。 +
+start()
方法。该状态的线程位于可运行线程池中,等待被线程调度选中并分配cpu使用权 。++睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。
++
+- 调用
+Thread.sleep()
方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。- 调用
+Object.wait()
方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。- 阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁,而等待是主动的,通过调用
+Thread.sleep()
和Object.wait()
等方法进入等待。
sleep
和wait
方法区别:
sleep()
属于Thread
类,wait()
属于Object
类;sleep()
和wait()
都会抛出InterruptedException
异常,这个异常属于checkedException
不可避免;sleep()
不会释放锁,会使线程堵塞,而调用wait()
会释放锁,让线程进入等待状态,用 notify()、notifyall()
可以唤醒,或者等待时间到了;
+这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify()
或者 notifyAll()
来唤醒挂起的线程,造成死锁。wait()
必须在同步synchronized
块里使用,sleep()
可以在任意地方使用;其他方法:
+join()
使当前线程停下来等待,直至另一个调用join
的线程终止,值得注意的是:线程的在被激活后不一定马上就运行.而是进入到可运行线程的队列中;yield()
是停止当前线程,让同等优先权的线程运行。如果没有同等优先权的线程,那么yield()
将不会起作用;notify()、notifyall()
需要搭配wait
方法使用,前提条件必须要在synchronized
代码块里面,因为需要依赖monitor
对象;在Java中,创建一个线程,有且仅有一种方式: 创建一个Thread
类实例,并调用它的start
方法。
通过继承Thread
类,重写run()
方法来创建线程。
public class MainTest {
+ public static void main(String[] args) {
+ ThreadDemo thread1 = new ThreadDemo();
+ thread1.start();
+ }
+}
+class ThreadDemo extends Thread {
+ @Override
+ public void run() {
+ System.out.printf("通过继承Thread类的方式创建线程,线程 %s 启动",Thread.currentThread().getName());
+ }
+}
+
实现 Runnale
接口,将它作为 target
参数传递给 Thread
类构造函数的方式创建线程。
public class MainTest {
+ public static void main(String[] args) {
+ new Thread(() -> {
+ System.out.printf("通过实现Runnable接口的方式,重写run方法创建线程;线程 %s 启动",Thread.currentThread().getName());
+ }).start();
+ }
+}
+
通过实现 Callable
接口,来创建一个带有返回值的线程。
在Callable
执行完之前的这段时间,主线程可以先去做一些其他的事情,事情都做完之后,再获取 Callable
的返回结果。可以通过isDone()
来判断子线程是否执行完。
public class MainTest {
+ public static void main(String[] args) throws ExecutionException, InterruptedException {
+ FutureTask<String> futureTask = new FutureTask<>(() -> {
+ System.out.printf("通过实现Callable接口的方式,重写call方法创建线程;线程 %s 启动", Thread.currentThread().getName());
+ System.out.println();
+ Thread.sleep(10000);
+ return "我是call方法返回值";
+ });
+ new Thread(futureTask).start();
+
+ System.out.println("主线程工作中 ...");
+ String callRet = null;
+ while (callRet == null){
+ if(futureTask.isDone()){
+ callRet = futureTask.get();
+ }
+ System.out.println("主线程继续工作 ...");
+ }
+ System.out.println("获取call方法返回值:"+ callRet);
+ }
+}
+
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。
+它的主要特点为:线程复用,控制最大并发数,管理线程。
+优点:
+通过Executors
线程池工具类来使用:
Executors.newSingleThreadExecutor()
:创建只有一个线程的线程池Executors.newFixedThreadPool(int)
:创建固定线程的线程池Executors.newCachedThreadPool()
:创建一个可缓存的线程池,线程数量随着处理业务数量变化这三种常用创建线程池的方式,底层代码都是用ThreadPoolExecutor
创建的。
Executors.newSingleThreadExecutor()
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。newSingleThreadExecutor
将 corePoolSize
和 maximumPoolSize
都设置为1,它使用的 LinkedBlockingQueue
。源代码
+ public static ExecutorService newSingleThreadExecutor() {
+ return new FinalizableDelegatedExecutorService
+ (new ThreadPoolExecutor(1, 1,
+ 0L, TimeUnit.MILLISECONDS,
+ new LinkedBlockingQueue<Runnable>()));
+ }
+
代码演示
+public class MainTest {
+ public static void main(String[] args) {
+ ExecutorService executor1 = null;
+ try {
+ executor1 = Executors.newSingleThreadExecutor();
+ for (int i = 1; i <= 10; i++) {
+ executor1.execute(() -> {
+ System.out.println(Thread.currentThread().getName() + "执行了");
+ });
+ }
+ } finally {
+ executor1.shutdown();
+ }
+ }
+}
+
Executors.newFixedThreadPool(int)
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待newFixedThreadPool
创建的线程池 corePoolSize
和 maximumPoolSize
值是相等的,它使用的 LinkedBlockingQueue
。源代码
+ public static ExecutorService newFixedThreadPool(int nThreads) {
+ return new ThreadPoolExecutor(nThreads, nThreads,
+ 0L, TimeUnit.MILLISECONDS,
+ new LinkedBlockingQueue<Runnable>());
+ }
+
代码演示
+public class MainTest {
+ public static void main(String[] args) {
+ ExecutorService executor1 = null;
+ try {
+ executor1 = Executors.newFixedThreadPool(10);
+ for (int i = 1; i <= 10; i++) {
+ executor1.execute(() -> {
+ System.out.println(Thread.currentThread().getName() + "执行了");
+ });
+ }
+ } finally {
+ executor1.shutdown();
+ }
+ }
+}
+
Executors.newCachedThreadPool()
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。newCachedThreadPool
将 corePoolSize
设置为0,将 maximumPoolSize
设置为 Integer.MAX_VALUE
,使用的 SynchronousQueue
,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。源代码
+ public static ExecutorService newCachedThreadPool() {
+ return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
+ 60L, TimeUnit.SECONDS,
+ new SynchronousQueue<Runnable>());
+ }
+
代码演示
+public class MainTest {
+ public static void main(String[] args) {
+ ExecutorService executor1 = null;
+ try {
+ executor1 = Executors.newCachedThreadPool();
+ for (int i = 1; i <= 10; i++) {
+ executor1.execute(() -> {
+ System.out.println(Thread.currentThread().getName() + "执行了");
+ });
+ }
+ } finally {
+ executor1.shutdown();
+ }
+ }
+}
+
阻塞队列,顾名思义,首先它是一个队列:
+ +一个阻塞队列在数据结构中所起的作用:
+blockQueue
作为线程容器、阻塞队列,多用于生产者、消费者的关系模式中,保障并发编程线程同步,线程池中被用于当作存储任务的队列,还可以保证线程执行的有序性.fifo先进先出
在多线程领域:所谓阻塞,在某些情况下会挂起线程(即线程阻塞),一旦条件满足,被挂起的线程优惠被自动唤醒
+我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为BlockingQueue
都一手给你包办好了
+在concurrent
包 发布以前,在多线程环境下,我们每个程序员都必须自己去控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度.
ArrayBlockingQueue
: 由数组结构组成的有界阻塞队列.LinkedBlockingDeque
: 由链表结构组成的有界(但大小默认值Integer>MAX_VALUE
大约21亿)阻塞队列.PriorityBlockingQueue
:支持优先级排序的无界阻塞队列.DelayQueue
: 使用优先级队列实现的延迟无界阻塞队列.SynchronousQueue
:不存储元素的阻塞队列,也即是单个元素的队列.LinkedTransferQueue
:由链表结构组成的无界阻塞队列.LinkedBlockingDeque
:由了解结构组成的双向阻塞队列.add()
:相对列里边添加元素,返回值了类型boolean
,当超出队列大小时会抛出异常java.lang.IllegalStateException: Queue full
remove
:清除元素,默认清除队列最上边的元素,可指定元素进行清除,如果清除一个不存在的元素会报异常java.util.NoSuchElementException
element
:查看队首元素,检查队列为不为空public static void arrayBlockDemo() {
+ // 与ArrayList类似,但需要设置队列大小
+ ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
+ System.out.println(queue.add("c"));
+ System.out.println(queue.add("b"));
+ System.out.println(queue.add("a"));
+ // 当add第四个元素到队列时会抛异常
+ queue.add("f");
+ //查看队首元素,检查队列为不为空
+ System.out.println(queue.element());
+ System.out.println(queue.remove());
+ System.out.println(queue.remove());
+ System.out.println(queue.remove());
+ // 如果多清除一个不存在的元素会报异常
+ System.out.println(queue.remove());
+ }
+
offer
:与add()
类似,但如果添加失败,不会报异常.会返回false
poll
:与remove
类似,如果没有元素可取,不会报异常,会返回null
peek
:与element
类似public static void arrayBlockDemo2(){
+ ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
+ System.out.println(queue.offer("1"));
+ System.out.println(queue.offer("2"));
+ System.out.println(queue.offer("3"));
+ // 不会抛异常
+ System.out.println(queue.offer("4"));
+ System.out.println(queue.peek());
+ System.out.println(queue.poll());
+ System.out.println(queue.poll());
+ System.out.println(queue.poll());
+ System.out.println(queue.poll());
+ }
+
put
:当阻塞队列满时,生产者继续往队列里面put元素,队列会一直阻塞直到put数据or响应中断退出take
:获取并移除此队列头元素,若没有元素则一直阻塞.当阻塞队列空时,消费者试图从队列take元素,队列会一直阻塞消费者线程直到队列可用.当阻塞队列满时,队列会阻塞生产者线程一定时间,超过后限时后生产者线程就会退出SynchronousQueue
,实际上它不是一个真正的队列,因为它不会为队列中元素维护存储空间。与其他队列不同的是,它维护一组线程,这些线程在等待着把元素加入或移出队列.
SynchronousQueue
支持支持生产者和消费者等待的公平性策略。默认情况下,不能保证生产消费的顺序。
+如果是公平锁的话可以保证当前第一个队首的线程是等待时间最长的线程,这时可以视SynchronousQueue
为一个FIFO队列
public class SynchronousQueueDemo {
+
+ public static void main(String[] args) {
+ SynchronousQueue<Integer> synchronousQueue = new SynchronousQueue<>();
+ new Thread(() -> {
+ try {
+ synchronousQueue.put(1);
+ Thread.sleep(3000);
+ synchronousQueue.put(2);
+ Thread.sleep(3000);
+ synchronousQueue.put(3);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }).start();
+
+ new Thread(() -> {
+ try {
+ Integer val = synchronousQueue.take();
+ System.out.println(val);
+ Integer val2 = synchronousQueue.take();
+ System.out.println(val2);
+ Integer val3 = synchronousQueue.take();
+ System.out.println(val3);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }).start();
+ }
+}
+
使用场景:
+ public ThreadPoolExecutor(int corePoolSize,
+ int maximumPoolSize,
+ long keepAliveTime,
+ TimeUnit unit,
+ BlockingQueue<Runnable> workQueue,
+ ThreadFactory threadFactory,
+ RejectedExecutionHandler handler) {
+ // ...
+ }
+
corePoolSize
: 线程池中的常驻核心线程数,可理解为初始化线程数maximumPoolSize
:线程池能够容纳同时执行的最大线程数,此值必须大于等于1threadFactory
:线程工厂;表示生成线程池中工作线程的线程工厂,用于创建线程,一般用默认的即可workQueue
:任务队列;随着业务量的增多,线程开始慢慢处理不过来,这时候需要放到任务队列中去等待线程处理rejectedExecutionHandler
:拒绝策略;如果业务越来越多,线程池首先会扩容,扩容后发现还是处理不过来,任务队列已经满了,这时候拒绝接收新的请求keepAliveTime
:多余的空闲线程的存活时间;如果线程池扩容后,能处理过来,而且数据量并没有那么大,用最初的线程数量就能处理过来,剩下的线程被叫做空闲线程unit
:多余的空闲线程的存活时间的单位在创建了线程池后,等待提交过来的任务请求;
+当调用execute
方法添加一个请求任务时,线程池会做如下判断:
corePoolSize
,那么马上创建线程运行该任务corePoolSize
,那么该任务会被放入任务队列maximumPoolSize
,那么要创建非核心线程立刻运行这个任务(扩容)maximumPoolSize
,那么线程池会启动饱和拒绝策略来执行corePoolSize
,那么这个线程就被停掉,所以线程池的所有任务完成后它最终会收缩到 corePoolSize
的大小在线程池中,如果任务队列满了并且正在运行的线程个数大于等于允许运行的最大线程数,那么线程池会启动拒绝策略来执行,具体分为下列四种:
+AbortPolicy
: 默认拒绝策略;直接抛出java.util.concurrent.RejectedExecutionException
异常,阻止系统的正常运行;CallerRunsPolicy
:调用这运行,一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量;DiscardOldestPolicy
:抛弃队列中等待最久的任务,然后把当前任务加入到队列中;DiscardPolicy
:直接丢弃任务,不给予任何处理也不会抛出异常;如果允许任务丢失,这是一种最好的解决方案;在实际开发中用哪个线程池?
+上面的三种一个都不用,我们生产上只能使用自定义的。
+Executors
中JDK已经给你提供了,为什么不用?
以下内容摘自《阿里巴巴开发手册》
+++【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。 +说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。 如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。 +【强制】线程池不允许使用
+Executors
去创建,而是通过ThreadPoolExecutor
的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
++说明:
+Executors
返回的线程池对象的弊端如下: +1)FixedThreadPool
和SingleThreadPool
: 允许的请求队列长度为Integer.MAX_VALUE
,可能会堆积大量的请求,从而导致 OOM。 +2)CachedThreadPool
: 允许的创建线程数量为Integer.MAX_VALUE
,可能会创建大量的线程,从而导致 OOM。
自定义线程池代码演示:
+public class MainTest {
+ public static void main(String[] args) {
+ ExecutorService executor1 = null;
+ try {
+ executor1 = new ThreadPoolExecutor(
+ 2,
+ 5,
+ 1L,
+ TimeUnit.SECONDS,
+ new LinkedBlockingQueue<>(3),
+ Executors.defaultThreadFactory(),
+ new ThreadPoolExecutor.CallerRunsPolicy());
+ for (int i = 1; i <= 20; i++) {
+ executor1.execute(() -> {
+ System.out.println(Thread.currentThread().getName() + "执行了");
+ });
+ }
+ } finally {
+ executor1.shutdown();
+ }
+ }
+}
+
SpringBoot异步配置,自定义线程池代码演示:
+@EnableAsync
+@Configuration
+public class AsyncConfig {
+
+ /**
+ * 线程空闲存活的时间 单位: TimeUnit.SECONDS
+ */
+ public static final int KEEP_ALIVE_TIME = 60 * 60;
+ /**
+ * CPU 核心数量
+ */
+ private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
+ /**
+ * 核心线程数量
+ */
+ public static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
+ /**
+ * 线程池最大容纳线程数量
+ * IO密集型:即存在大量堵塞; 公式: CPU核心数量 / 1- 阻塞系数 (阻塞系统在 0.8~0.9 之间)
+ * CPU密集型: 需要大量运算,没有堵塞或很少有; 公式:CPU核心数量 + 1
+ */
+ public static final int IO_MAXIMUM_POOL_SIZE = (int) (CPU_COUNT / (1 - 0.9));
+ public static final int CPU_MAXIMUM_POOL_SIZE = CPU_COUNT + 2;
+
+ /**
+ * 执行写入请求时的线程池
+ *
+ * @return 线程池
+ */
+ @Bean(name = "iSaveTaskThreadPool")
+ public Executor iSaveTaskThreadPool() {
+ return getThreadPoolTaskExecutor("iSaveTaskThreadPool-",IO_MAXIMUM_POOL_SIZE,100000,new ThreadPoolExecutor.CallerRunsPolicy());
+ }
+
+ /**
+ * 执行读请求时的线程池
+ *
+ * @return 线程池
+ */
+ @Bean(name = "iQueryThreadPool")
+ public Executor iQueryThreadPool() {
+ return getThreadPoolTaskExecutor("iQueryThreadPool-",CPU_MAXIMUM_POOL_SIZE,10000,new ThreadPoolExecutor.CallerRunsPolicy());
+ }
+
+ /**
+ * 创建一个线程池对象
+ * @param threadNamePrefix 线程名称
+ * @param queueCapacity 堵塞队列长度
+ * @param refusePolicy 拒绝策略
+ */
+ private ThreadPoolTaskExecutor getThreadPoolTaskExecutor(String threadNamePrefix,int maxPoolSize,int queueCapacity,ThreadPoolExecutor.CallerRunsPolicy refusePolicy) {
+ ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
+ taskExecutor.setCorePoolSize(CORE_POOL_SIZE);
+ taskExecutor.setMaxPoolSize(maxPoolSize);
+ taskExecutor.setKeepAliveSeconds(KEEP_ALIVE_TIME);
+ taskExecutor.setThreadNamePrefix(threadNamePrefix);
+ // 拒绝策略; 既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量
+ taskExecutor.setRejectedExecutionHandler(refusePolicy);
+ // 阻塞队列 长度
+ taskExecutor.setQueueCapacity(queueCapacity);
+ taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
+ taskExecutor.setAwaitTerminationSeconds(60);
+ taskExecutor.initialize();
+ return taskExecutor;
+ }
+
+}
+
合理配置线程池参数,可以分为以下两种情况
+CPU密集型:CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行;
+CPU密集型任务配置尽可能少的线程数量:参考公式:(CPU核数+1)
IO密集型:即该任务需要大量的IO,即大量的阻塞;
+在IO密集型任务中使用多线程可以大大的加速程序运行,故需要多配置线程数:参考公式:CPU核数/ (1-阻塞系数) 阻塞系数在0.8~0.9之间
代码演示
+public class MainTest {
+ public static void main(String[] args) {
+ ExecutorService executor1 = null;
+ try {
+ // 获取cpu核心数
+ int coreNum = Runtime.getRuntime().availableProcessors();
+ /*
+ * 1. IO密集型: CPU核数/ (1-阻塞系数) 阻塞系数在0.8~0.9之间
+ * 2. CPU密集型: CPU核数+1
+ */
+// int maximumPoolSize = coreNum + 1;
+ int maximumPoolSize = (int) (coreNum / (1 - 0.9));
+ System.out.println("当前线程池最大允许存放:" + maximumPoolSize + "个线程");
+ executor1 = new ThreadPoolExecutor(
+ 2,
+ maximumPoolSize,
+ 1L,
+ TimeUnit.SECONDS,
+ new LinkedBlockingQueue<>(3),
+ Executors.defaultThreadFactory(),
+ new ThreadPoolExecutor.CallerRunsPolicy());
+ for (int i = 1; i <= 20; i++) {
+ executor1.execute(() -> {
+ System.out.println(Thread.currentThread().getName() + "执行了");
+ });
+ }
+ } finally {
+ executor1.shutdown();
+ }
+ }
+}
+
参考文章:
+在Java中根据锁的特性来区分可以分为很多,在程序中"锁"的作用无非就是保证线程安全,线程安全的目的就是保证程序正常执行。
+在Java中具体"锁"的实现,无非就三种:使用synchronized
关键字、调用juc.locks
包下相关接口、使用CAS
思想。
公平锁:多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。
+非公平锁:多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。
+相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。 +当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。
+在Java中公平锁和非公平锁的实现为ReentrantLock
、synchronized
。
+其中synchronized
是非公平锁;ReentrantLock
默认是非公平锁,但是可以指定ReentrantLock
的构造函数创建公平锁。
/**
+ * Creates an instance of {@code ReentrantLock}.
+ * This is equivalent to using {@code ReentrantLock(false)}.
+ */
+public ReentrantLock() {
+ sync = new NonfairSync();
+}
+
+/**
+ * Creates an instance of {@code ReentrantLock} with the
+ * given fairness policy.
+ *
+ * @param fair {@code true} if this lock should use a fair ordering policy
+ */
+public ReentrantLock(boolean fair) {
+ sync = fair ? new FairSync() : new NonfairSync();
+}
+
可重入锁:在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class)不会因为之前已经获取过还没释放而阻塞。 +所以可重入锁又叫做递归锁,就是因为能获取到加锁方法里面的加锁方法的锁。
+不可重入锁:所谓不可重入锁,就是与可冲入锁作用相悖;即当前线程执行某个方法已经获取了该锁,那么在该方法中尝试再次获取加锁的方法时,就会获取不到被阻塞。
+++举个栗子: 当你进入你家时门外会有锁,进入房间后厨房卫生间都可以随便进出,这个叫可重入锁; +当你进入房间时,发现厨房,卫生间都有上锁.这个叫不可重入锁。
+
在Java中ReentrantLock
和synchronized
都是可重入锁。
在Java中,对于 ReentrantLock
和 synchronized
都是独占锁;对与 ReentrantReadWriteLock
其读锁是共享锁而写锁是独占锁。
+读锁的共享可保证并发读是非常高效的。
乐观锁与悲观锁是一种广义上的概念,可以理解为一种标准类似于Java中的接口。
+对于多线程并发操作,加了悲观锁的线程认为每一次修改数据时都会有其他线程来跟它一起修改数据,所以在修改数据之前先会加锁,确保其他线程不会修改该数据。 +由于悲观锁在修改数据前先加锁的特性,能保证写操作时数据正确,所以悲观锁更适合写多读少的场景。
+乐观锁则与悲观锁相反,每一次修改数据时,都认为没有其他线程来跟它一起修改,所以在修改数据之前不会去添加锁,如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作。 +由于乐观锁是一种无锁操作,所以在使用乐观锁的场景中读的性能会大幅度提升,适合读多写少。
+ +在Java中悲观锁的实现有:synchronized
、Lock
实现类,乐观锁的实现有CAS
。
当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态,直到获取到某个锁。
+自旋锁本身是有缺点的,它不能代替阻塞。如果锁被占用的时间很长,那么自旋的线程只会白白浪费处理器资源,带来性能上的浪费。
+自旋锁的实现原理是CAS,例如AtomicInteger
中getAndUpdate()
方法
public final int getAndUpdate(IntUnaryOperator updateFunction) {
+ int prev, next;
+ do {
+ prev = get();
+ next = updateFunction.applyAsInt(prev);
+ } while (!compareAndSet(prev, next));
+ return prev;
+ }
+
源码中的do-while
循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。
为什么要使用自旋锁?
+在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。 +简单来说就是,避免切换线程带来的开销。
+自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。
+反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。
+所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin
来更改)没有成功获得锁,就应当挂起线程。
自旋锁在JDK 1.4中引入,默认关闭,但是可以使用-XX:+UseSpinning
开开启;在JDK1.6中默认开启,同时自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin
来调整。
如果通过参数-XX:PreBlockSpin
来调整自旋锁的自旋次数,会带来诸多不便。
+假如将参数调整为10,但是系统很多线程都是等你刚刚退出的时候就释放了锁(假如多自旋一两次就可以获取锁)。于是JDK1.6引入适应性自旋锁。
适应性自旋锁是对自旋的升级、优化,自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态决定。
+如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。 +如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
+引入偏向锁的目的是在没有多线程竞争的前提下,进一步减少线程同步的性能消耗。
+++《深入理解Java虚拟机》对偏向锁的解释: +
+Hotspot
的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入 +了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程 ID,以后该线程在进入和退出同步块时不需要花费 +CAS操作来加锁和解锁,而只需简单的测试一下对象头的MarkWord
里是否存储着指向当前线程的偏向锁,如果测试成功,表示线程已经获得了锁, +如果测试失败,则需要再测试下MarkWord
中偏向锁的标识是否设置成 1(表示当前是偏向锁),如果没有设置,则使用 CAS 竞争锁,如果设置了, +则尝试使用 CAS 将对象头的偏向锁指向当前线程。
之所以叫偏向锁是因为偏向于第一个获取到他的线程,如果在程序执行中该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。 +但是如果线程间存在锁竞争,会带来额外的锁撤销(CAS)的消耗。
+个人理解,偏向锁就是对锁的标志位做了一个缓存,在没有多线程竞争的前提下,这样做会大幅度提升程序性能。
+重量级锁:传统的重量级锁,使用的是系统互斥量实现的;重量级锁会导致线程堵塞; +轻量级锁:相对于重量级锁而言的;他的出现并不是代替重量级锁,而是在没有多线程竞争的前提下,减少系统互斥量操作产生的性能消耗;是重量级锁的优化。
+在Java中轻量级锁的经典实现是CAS中的自旋锁,所以优点缺点就很明显了。
+所以适合,追求响应时间,同步块执行速度非常快的场景。
+重量级锁优缺点:
+适合追求吞吐量、同步块执行时间较长也就是线程竞争激烈的场景。
+轻量级锁不是在任何情况下都比重量级锁快的,要看同步块执行期间有没有多个线程抢占资源的情况。 +如果有,那么轻量级线程要承担 CAS + 互斥锁的性能消耗,就会比重量锁执行的更慢。
+顾名思义,就是可以中断的锁。
+如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
+在Java中synchronized
就是不可中断锁,Lock
是可中断锁。
++在编程中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。 +每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
+
互斥锁:在访问共享资源之前对对象进行加锁操作,在访问完成之后进行解锁操作。 +加锁后,任何其他试图再次加锁的线程会被阻塞,直到当前线程解锁其他线程才能访问公共资源。
+如果解锁时有一个以上的线程阻塞,那么所有该锁上的线程都变为就绪状态,第一个变为就绪状态的线程又执行加锁操作,其他的线程又会进入等待。 +在这种方式下,只有一个线程能够访问被互斥锁保护的资源。
+在Java里最基本的互斥手段就是使用synchronized
关键字、ReentrantLock
。
++中提供一把互斥锁
+mutex
也称之为互斥量。每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。 +要注意,同一时刻,只能有一个线程持有该锁。 +所以,互斥锁实质上是操作系统提供的一把“建议锁”(又称“协同锁”),建议程序中有多线程访问共享资源的时候使用该机制。但,并没有强制限定。 +因此,即使有了mutex
,如果有线程不按规则来访问数据,依然会造成数据混乱。
死锁并不是Java程序中的"锁",而是程序中出现的一种问题。之所以放到锁这个标题下,是为了方便类比,就类似谐音梗吧。
+++死锁通常被定义为:如果一个进程集合中的每个进程都在等待只能由此集合中的其他进程才能引发的事件,而无限期陷入僵持的局面称为死锁。
+
举个例子,当线程A持有锁a并尝试获取锁b,线程B持有锁b并尝试获取锁a时,就会出现死锁。简单来说,死锁问题的产生是由两个或者以上线程并行执行的时候,争夺资源而互相等待造成的。
+ public class MainTest {
+
+ public static void main(String[] args) {
+ String lockA = "lockA";
+ String lockB = "lockB";
+ new Thread(new ThreadHolderLock(lockA,lockB),"线程AAA").start();
+ new Thread(new ThreadHolderLock(lockB,lockA),"线程BBB").start();
+ }
+ }
+
+ class ThreadHolderLock implements Runnable{
+
+ private String lockA;
+ private String lockB;
+
+ public ThreadHolderLock(String lockA, String lockB){
+ this.lockA = lockA;
+ this.lockB = lockB;
+ }
+
+ @Override
+ public void run() {
+ synchronized (lockA){
+ System.out.println(Thread.currentThread().getName() + "\t 持有锁 "+ lockA+", 尝试获得"+ lockB);
+
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ synchronized (lockB){
+ System.out.println(Thread.currentThread().getName() + "\t 持有锁 "+ lockB+", 尝试获得"+ lockA);
+ }
+ }
+ }
+ }
+
参考文章:
+ +想要如何避免死锁,就要弄清楚死锁出现的原因,造成死锁必须达成的4个条件:
+避免死锁的产生就只需要破环其中一个条件就可以,最常见的并且可行的就是使用资源有序分配法,来破循环等待条件。
+++资源有序分配法: 线程 A 和 线程 B 获取资源的顺序要一样,当线程 A 先尝试获取资源 A,然后尝试获取资源 B 的时候,线程 B 同样也是先尝试获取资源 A,然后尝试获取资源 B。也就是说,线程 A 和 线程 B 总是以相同的顺序申请自己想要的资源。
+
也可以通过银行家算法来动态避免死锁问题。 +
+++银行家算法:一个避免死锁(Deadlock)的著名算法,是由艾兹格·迪杰斯特拉在1965年为T.H.E系统设计的一种避免死锁产生的算法。它以银行借贷系统的分配策略为基础,判断并保证系统的安全运行。 +
+
+在银行中,客户申请贷款的数量是有限的,每个客户在第一次申请贷款时要声明完成该项目所需的最大资金量,在满足所有贷款要求时,客户应及时归还。银行家在客户申请的贷款数量不超过自己拥有的最大值时,都应尽量满足客户的需要。通过判断借贷是否安全,然后决定借不借。 +
+举例,现有公司B、公司A、公司T,想要从银行分别贷款70亿、40亿、50亿,假设银行只有100亿供放贷,如果借不到企业最大需求的钱,钱将不会归还,怎么才能合理的放贷?+ +
++ + + +公司 +最大需求 +已借走 +最多还借 ++ +B +70 +20 +50 ++ +A +40 +10 +30 ++ + +T +50 +30 +20 +此时公司B、A、T已经从银行借走60亿,银行还剩40亿。此时银行可放贷金额组合:
++
+- 借给公司B10亿、公司A10亿、公司T20亿,等待公司T还钱再将10亿借给公司A,等待公司A还钱,再将钱借给公司B;
+- 借给公司T20亿,等公司T还钱再将钱借给公司A,等待公司A还钱再将钱借给公司B;
+- 借给公司A10亿,等待公司A还钱再将钱借给公司T,公司T还钱再将钱借给公司B;
+
线程不安全,在多线程并发的环境中,多个线程共同操作同一个数据,如果最后数据的值和期待值不一样,这时候就出现了线程不安全问题。
+线程安全,就是在多线程并发中,多个线程共同操作同一个数据,在Java中最常用的就是加锁; +当一个线程修改某个数据的时候,其他线程不能进行访问直到该线程操作该数据结束释放锁,其他线程才可以继续操作该数据。
+一个对象是否安全取决于它是否被多个线程访问,要使对象线程安全,那么需要采用同步的机制来协同对对象可变状态的访问。
+在保证线程安全之前要先弄明白线程安全的三大特性,即原子性、可见性、有序性。
+++关于有序性肯定会有人有疑问,程序执行的顺序难道不是从上到下按照顺序来执行吗?
+在多线程环境下,Java语句可能会不按照顺序执行,所以要注意数据的依赖性。计算机在执行程序时,为了提高性能,编译器和处理器常常会做指令重排,一把分为以下两种:
++
+- 单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。处理器在进行重新排序是必须要考虑指令之间的数据依赖;
+- 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程使用的变量能否保持一致性是无法确定的,结果无法预测;
+
多线程并发出问题的根本原因:
+++为了保证并发编程中可以满足原子性、可见性及有序性。内存模型定义了共享内存系统中多线程程序读写操作行为的规范。 +通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。它与处理器有关、与缓存有关、与并发有关、与编译器也有关。 +他解决了CPU多级缓存、处理器优化、指令重排等导致的内存访问问题,保证了并发场景下的一致性、原子性和有序性。
+
内存模型解决并发问题主要采用两种方式:限制处理器优化和使用内存屏障。
+ +Java内存模型,即JMM(Java Memory Model)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范通过规范定制了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。 +屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。
+JMM就作用于工作内存和主存之间数据同步过程。他规定了如何做数据同步以及什么时候做数据同步。
+ +那么在Java中是如何保证原子性、可见性及有序性的呢?
+原子性:在Java中,为了保证原子性,提供了两个高级的字节码指令 monitorenter
和 monitorexit
。对应的就是Java中的关键字 synchronized
,在Java中只要被synchronized
修饰就能保证原子性。
可见性:在Java中,为了保证线程间的可见性,可以使用volatile
、synchronized
、final
来修饰。
有序性:在Java中,可以使用 synchronized
和 volatile
来保证多线程之间操作的有序性。其中,volatile
关键字会禁止编译器指令重排,来保证;synchronized
关键字保证同一时刻只允许一条线程操作,而不能禁止指令重排,指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序性,从而保证了有序性。
volatile
通常被比喻成轻量级的锁,也是Java并发编程中比较重要的一个关键字。
volatile
特点:
在Java中volatile
是一个变量修饰符,只能用来修饰变量。
volatile
典型的使用就是单例模式中的DCL双重检查锁。
/**
+多线程下的单例模式 DCL(double check lock)
+**/
+class SingletonDemo {
+
+ // volatile 此处作用 禁止指令重排
+ public static volatile SingletonDemo singleton = null;
+
+ private SingletonDemo() {
+ }
+
+ public static SingletonDemo getInstance() {
+ if (singleton == null) {
+ synchronized (SingletonDemo.class) {
+ if (singleton == null) {
+ singleton = new SingletonDemo();
+ }
+ }
+ }
+ return singleton;
+ }
+
+}
+
为什么在此处要使用volatile
修饰singleton
?
多线程下的DCL单例模式,如果不加 volatile
修饰不是绝对安全的,因为在创建对象的时候JVM底层会进行三个步骤:
其中步骤2和步骤3是没有数据依赖关系的,而且无论重排前还是重排后的程序执行结果在单线程中并没有改变,因此这种重排优化是允许的。
+所以有可能先执行步骤3在执行步骤2,导致分配的对象不为 null
,但对象没有被初始化;
所以当一个线程获取对象不为 null
时,由于对象未必已经完成初始化,会存在线程不安全的风险。
《深入理解JVM》中对 volatile
的描述:
++一旦一个共享变量(类的成员变量、类的静态成员变量)被
+volatile
修饰之后,那么就具备了两层语义:+
+- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的;
+- 禁止进行指令重排序,即有序性;
++
volatile
只提供了保证访问该变量时,每次都是从内存中读取最新值,并不会使用寄存器缓存该值——每次都会从内存中读取。 +而对该变量的修改,volatile
并不提供原子性(线程不安全)的保证;由于及时更新,很可能导致另一线程访问最新变量值,无法跳出循环的情况,多线程下计数器必须使用锁保护.
将上面的代码用javap -v SingletonDemo.class >test.txt
命令执行,将反编译后的字节码指令写入到test文件中,可以看到ACC_VOLATILE
public static volatile content.posts.rookie.SingletonDemo singleton;
+ descriptor: Lcontent/posts/rookie/SingletonDemo;
+ flags: ACC_PUBLIC, ACC_STATIC, ACC_VOLATILE
+
volatile
在字节码层面,就是使用访问标志:ACC_VOLATILE
来表示,供后续操作此变量时判断访问标志是否为 ACC_VOLATILE
,来决定是否遵循 volatile
的语义处理。
可以从openjdk8
中找到对应的源码文件
路径:openjdk8/hotspot/src/share/vm/interpreter/bytecodeInterpreter.cpp
+
重点是cache->is_volatile()
方法,调用栈
bytecodeInterpreter.cpp>is_volatile()
+==> accessFlags.hpp>is_volatile
+==> bytecodeInterpreter.cpprelease_byte_field_put
+==> oop.inline.hpp>(oopDesc::byte_field_acquire、oopDesc::release_byte_field_put)
+==> orderAccess.hpp
+>> orderAccess_linux_x86.inline.hpp.OrderAccess::release_store
+
最终调用了OrderAccess::release_store
inline void OrderAccess::release_store(volatile jbyte* p, jbyte v) { *p = v; }
+inline void OrderAccess::release_store(volatile jshort* p, jshort v) { *p = v; }
+
可以从上面看到,到C++的实现层面,又使用C++中的 volatile
关键字,用来修饰变量,通常用于建立语言级别的内存屏障memory barrier
。
+在《C++ Programming Language》一书中对 volatile
修饰词的解释:
++A volatile specifier is a hint to a compiler that an object may change its value in ways not specified by the language so that aggressive optimizations must be avoided.
+
volatile
修饰的类型变量表示可以被某些编译器未知的因素更改;volatile
变量时,避免激进的优化; 系统总是重新从内存读取数据,即使它前面的指令刚从内存中读取被缓存,防止出现未知更改和主内存中不一致。其在64位系统的实现orderAccess_linux_x86.inline.hpp.OrderAccess::release_store
inline void OrderAccess::fence() {
+ if (os::is_MP()) {
+ // always use locked addl since mfence is sometimes expensive
+#ifdef AMD64
+ __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
+#else
+ __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
+#endif
+ }
+}
+
代码lock; addl $0,0(%%rsp)
就是lock前缀;
++lock前缀,会保证某个处理器对共享内存的独占使用。 +它将本处理器缓存写入内存,该写入操作会引起其他处理器或内核对应的缓存失效。 +通过独占内存、使其他处理器缓存失效,达到了“指令重排序无法越过内存屏障”的作用。
+
对于 volatile
修饰的变量,当对 volatile
修饰的变量进行写操作的时候,JVM会向处理器发送一条带有 lock
前缀的指令,将这个缓存中的变量回写到系统主存中。
+但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现 缓存一致性协议
++缓存一致性协议: 每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。
+
为了提高CPU处理器的执行速度,在处理器和内存之间增加了多级缓存来提升。但是由于引入了多级缓存,就存在缓存数据不一致问题。
+ +所以,如果一个变量被 volatile
所修饰的话,在每次数据变化之后,其值都会被强制刷入主存。而其他处理器的缓存由于遵守了缓存一致性协议,也会把这个变量的值从主存加载到自己的缓存中。这就保证了一个 volatile
在并发编程中,其值在多个缓存中是可见的。
各个线程对主内存中共享变量的操作,都是各个线程各自拷贝到自己的工作内存操作后再写回主内存中的。 +这就可能存在一个线程AAA修改了共享变量X的值还未写回主内存中时 ,另外一个线程BBB又对内存中的一个共享变量X进行操作,但此时A线程工作内存中的共享比那里X对线程B来说并不不可见。 +这种工作内存与主内存同步延迟现象就造成了可见性问题。
+这种变量的可见性问题可以用volatile
来解决。volatile
的作用简单来说就是当一个线程修改了数据,并且写回主物理内存,其他线程都会得到通知获取最新的数据。
volatile
可见性,代码演示
public class MainTest {
+ public static void main(String[] args) {
+ A a = new A();
+ // thread1
+ new Thread(() -> {
+ System.out.println(Thread.currentThread().getName() + " is come in");
+ try {
+ // 模拟执行其他业务
+ Thread.sleep(3);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ // 用该线程改变A类中 number 变量的值
+ a.numberTo100();
+ }, "thread1").start();
+
+ // 如果number 等于0,则其他线程会一直等待 则证明 volatile 没有保证变量的可见性;相反则保证了变量的可见性
+ while (a.number == 0) {
+ }
+ System.out.println(Thread.currentThread().getName() + " thread is over");
+ }
+}
+class A {
+ // 注意: 此时变量要加 volatile 关键字修饰; 可以去掉 volatile 来进行对比测试
+ volatile int number = 0;
+
+ public void numberTo100() {
+ System.out.println(Thread.currentThread().getName() + " update number");
+ this.number = 100;
+ }
+}
+
volatile
原子性,代码演示
public class MainTest {
+
+ public static void main(String[] args) {
+ A a = new A();
+ /**
+ * 创建20个线程 每个线程让 number++ 1000次;
+ * number 变量用 volatile 修饰
+ * 如果 volatile 保证变量的原子性,则最后结果为 20 * 1000,反之则不保证。
+ * 当然不排除偶然事件,建议反复多试几次。
+ */
+ for (int i = 0; i < 20; i++) {
+ new Thread(() -> {
+ for (int j = 0; j < 1000; j++) {
+ a.addPlusplus();
+ }
+ }, String.valueOf(i)).start();
+ }
+ // 如果当前存活线程大于 2 个(包括main线程) 礼让线程继续执行上边的线程
+ while (Thread.activeCount() > 2) {
+ Thread.yield();
+ }
+ System.out.println(Thread.currentThread().getName() + " Thread is over\t" + a.number);
+
+ }
+
+}
+
+class A {
+ volatile int number = 0;
+
+ public void addPlusplus() {
+ this.number++;
+ }
+}
+
不保证原子性的原因
+由于各个线程之间都是复制主内存的数据到自己的工作空间里边修改数据,CPU的轮询反复切换线程,会导致数据丢失。 +即某个线程修改了数据,准备回主内存,此时CPU切换到另一个线程修改了数据,并且写回到了主内存,此时其他的线程不知道主内存的数据已经被更改,还会执行将之前从主内存复制的数据修改后的,写到主内存,这就导致了数据被覆盖,丢失。
+解决
+如果要解决原子性的问题,在Java中只能控制线程,在修改的时候不能被中断,即加锁。
+上面的例子可以使用CAS的实现AtomicInteger
来解决。
public class MainTest {
+
+ public static void main(String[] args) {
+ A a = new A();
+ /**
+ * 创建20个线程 每个线程让 number++ 1000次;
+ * number 变量用 volatile 修饰
+ * 如果 volatile 保证变量的原子性,则最后结果为 20 * 1000,反之则不保证。
+ * 当然不排除偶然事件,建议反复多试几次。
+ */
+ for (int i = 0; i < 20; i++) {
+ new Thread(() -> {
+ for (int j = 0; j < 1000; j++) {
+ a.addPlusplus();
+ }
+ }, String.valueOf(i)).start();
+ }
+ // 如果当前存活线程大于 2 个(包括main线程) 礼让线程继续执行上边的线程
+ while (Thread.activeCount() > 2) {
+ Thread.yield();
+ }
+ System.out.println(Thread.currentThread().getName() + " Thread is over\t" + a.number);
+
+ }
+
+}
+
+class A {
+
+ int number = 0;
+
+ /**
+ * 如果要解决原子性的问题可以用synchronized 关键字(这种太浪费性能)
+ * 可用JUC下的 AtomicInteger 来解决
+ **/
+ AtomicInteger atomicInteger = new AtomicInteger(number);
+
+ public void addPlusplus() {
+ number = atomicInteger.incrementAndGet();
+ }
+}
+
对于AtomicInteger.incrementAndGet
方法来说,原理就是volatile
+ do...while()
+ CAS
;
AtomicInteger.incrementAndGet
源码
public final int incrementAndGet() {
+ return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
+}
+//=========================
+public final int getAndAddInt(Object var1, long var2, int var4) {
+ int var5;
+ do {
+ var5 = this.getIntVolatile(var1, var2);
+ } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
+
+ return var5;
+}
+
用volatile
修饰该变量,保证该变量被某个线程修改时,保证其他线程中的这个变量的可见性;
+在多线程环境下,CPU轮流切换线程执行,有可能某个线程修改了数据,准备回主内存,此时CPU切换到另一个线程修改了数据,并且写回到了主内存,此时就导致数据的不准确;
+do...while()
+ CAS
的作用就是,当某个线程工作内存中的值与主内存中的值,如果不相同就会一直while
循环下去,之所以用do..while
是考虑到做自增操作。
有序性,指的就是代码按照顺序执行,这个就是对比指令重排来说的;计算机在执行程序时,为了提高性能,编译器和处理器常常会做指令重排。
+在上面的使用案例中的代码,DCL就是一个使用禁止指令重排的案例。
+volatile
禁止指令重排原因
由于编译器和处理器都能执行指令重排的优化,如果在指令键加入一条内存屏障(Memory barrier
),就会告诉编译器和CPU不管什么指令都不能和这条加入Memory barrier
指令键重新排序,也就是说通过内存屏障禁止在内存屏障前后的指令重新排序优化。
+内存屏障的另一个作用就是强制刷出各种CPU缓存数据,因此任何CPU上的线程都能读取到这些数据的最新值,即可见性。
CAS全称为Compare and Swap
被译为比较并交换。是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。
+java.util.concurrent.atomic
并发包下的所有原子类都是基于 CAS
来实现的。
以 AtomicInteger
原子整型类为例,来看一下CAS实现原理。
public class MainTest {
+ public static void main(String[] args) {
+ new AtomicInteger().compareAndSet(1,2);
+ }
+}
+
以上面的代码为例,调用栈如下:
+compareAndSet --> unsafe.compareAndSwapInt ---> unsafe.compareAndSwapInt --> (C++) cmpxchg
+
AtomicInteger
内部方法都是基于 Unsafe
类实现的。
public final boolean compareAndSet(int expect, int update) {
+ return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
+}
+
参数:
+this
: Unsafe
对象本身,需要通过这个类来获取 value
的内存偏移地址;valueOffset
: value
变量的内存偏移地址;expect
: 期望更新的值;update
: 要更新的最新值;偏移量valueOffset
// 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;
+
Unsafe
是CAS的核心类,Java无法直接访问底层操作系统,而是通过 native
方法来访问。不过尽管如此,JVM还是开了一个后门,JDK中有一个类 Unsafe
,它提供了硬件级别的原子操作。valueOffset
表示的是变量值在内存中的偏移地址,因为 Unsafe
就是根据内存偏移地址获取数据的原值的。value
是用 volatile
修饰的,保证了多线程之间看到的 value
值是同一份。继续向底层深入,就会看到Unsafe
类中的一些方法,同时也是CAS的核心方法:
public final class Unsafe {
+
+ // ...
+
+ public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
+
+ public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
+
+ public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
+
+ // ...
+}
+
上面的三个方法的原理,可以对应去查看 openjdk
的 hotspot
源码:src/share/vm/prims/unsafe.cpp
#define FN_PTR(f) CAST_FROM_FN_PTR(void*, &f)
+
+{CC"compareAndSwapObject", CC"("OBJ"J"OBJ""OBJ")Z", FN_PTR(Unsafe_CompareAndSwapObject)},
+
+{CC"compareAndSwapInt", CC"("OBJ"J""I""I"")Z", FN_PTR(Unsafe_CompareAndSwapInt)},
+
+{CC"compareAndSwapLong", CC"("OBJ"J""J""J"")Z", FN_PTR(Unsafe_CompareAndSwapLong)},
+
最终在 hotspot
源码实现中都会调用统一的 cmpxchg
函数,/src/share/vm/runtime/Atomic.cpp
jbyte Atomic::cmpxchg(jbyte exchange_value, volatile jbyte*dest, jbyte compare_value) {
+ assert (sizeof(jbyte) == 1,"assumption.");
+ uintptr_t dest_addr = (uintptr_t) dest;
+ uintptr_t offset = dest_addr % sizeof(jint);
+ volatile jint*dest_int = ( volatile jint*)(dest_addr - offset);
+ // 对象当前值
+ jint cur = *dest_int;
+ // 当前值cur的地址
+ jbyte * cur_as_bytes = (jbyte *) ( & cur);
+ // new_val地址
+ jint new_val = cur;
+ jbyte * new_val_as_bytes = (jbyte *) ( & new_val);
+ // new_val存exchange_value,后面修改则直接从new_val中取值
+ new_val_as_bytes[offset] = exchange_value;
+ // 比较当前值与期望值,如果相同则更新,不同则直接返回
+ while (cur_as_bytes[offset] == compare_value) {
+ // 调用汇编指令cmpxchg执行CAS操作,期望值为cur,更新值为new_val
+ jint res = cmpxchg(new_val, dest_int, cur);
+ if (res == cur) break;
+ cur = res;
+ new_val = cur;
+ new_val_as_bytes[offset] = exchange_value;
+ }
+ // 返回当前值
+ return cur_as_bytes[offset];
+}
+
从上述源码可以看出CAS的原理就是调用了汇编指令 cmpxchg
,最终其实也就调用了CPU的某些指令。
CAS作用也一目了然,在多线程环境中,就是比较当前线程工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较,直到主内存和当前线程工作内存中的值一致为止。
+例如代码:
+public final int getAndAddInt(Object var1, long var2, int var4) {
+ int var5;
+ do {
+ var5 = this.getIntVolatile(var1, var2);
+ } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
+ return var5;
+}
+
从源码可以看出,CAS是通过Unsafe
调用CPU指令,当CPU中某个处理器对缓存中的共享变量进行了操作,其他处理器会有个嗅探机制,将其他处理器的该共享变量的缓存失效,待其他线程读取时会重新从主内存中读取最新的数据,基于 MESI
缓存一致性协议来实现的。
简述,就是通过CPU的缓存一致性协议来保证线程之间的数据一致性的。
+++ +CPU 处理器速度远远大于在主内存中的,为了解决速度差异,在他们之间架设了多级缓存,如 L1、L2、L3 级别的缓存,这些缓存离CPU越近就越快,将频繁操作的数据缓存到这里,加快访问速度。
+
CAS的作用是比较并交换,就是先拿这个期望值,与主内存的值比较,判断主内存中该位置是否存在期望值,
+如果存在,则改为新的值,这个修改的过程是具有原子性的.
+因为CAS是cpu并发源语,并发源语体现在Java sun.misc.Unsafa
类上.
+调用Unsafe类中的CAS方法,JVM会帮我们实现CAS汇编指令。这是一种完全依赖于硬件的功能,通过他实现了原子操作。
+由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成数据不一致问题。
++PS Unsafe类 +CAS其实是调用了
+Unsafe
类的方法Unsafa
类是CAS核心类,由于Java方法无法直接访问底层系统,需要通过本地(native
)方法来访问,Unsafe
相当于一个后门,基于该类可以直接操作特定内存数据。 +Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针(内存地址)一样直接操作内存,因此Java中CAS操作的执行依赖于Unsafe类的方法。 +Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务。
因为是采用自旋锁的方式来实现所以,自然有自旋锁的缺点,循环时间长开销大,例如:getAndAddInt
方法执行,有个do while
循环,如果CAS失败,一直会进行尝试,如果CAS长时间不成功,可能会给CPU带来很大的开销。
public final int getAndAddInt(Object var1, long var2, int var4) {
+ int var5;
+ do {
+ var5 = this.getIntVolatile(var1, var2);
+ } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
+ return var5;
+}
+
只能保证一个共享变量的原子操作,对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
+但是Java从1.5开始JDK提供了AtomicReference
类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。
ABA问题示例代码:
+public class MainTest {
+ static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
+ public static void main(String[] args) {
+ new Thread(() -> {
+ // 先改到101在改回来,CAS会认为value没有被修改过
+ atomicReference.compareAndSet(100, 101);
+ atomicReference.compareAndSet(101, 100);
+ }, "Thread 1").start();
+
+ new Thread(() -> {
+ try {
+ //保证线程1完成一次ABA操作
+ TimeUnit.SECONDS.sleep(1);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
+ }, "Thread 2").start();
+ try {
+ TimeUnit.SECONDS.sleep(2);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+}
+
CAS算法实现一个重要前提是,需要去除内存中某个时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
+比如,线程1从内存位置V取出A,线程2同时也从内存取出A,并且线程2进行一些操作将值改为B,然后线程2又将V位置数据改成A,这时候线程1进行CAS操作发现内存中的值依然时A,然后线程1操作成功。 +尽管线程1的CAS操作成功,但是不代表这个过程没有问题。
+简单说,如果一个线程改了一个值,最后又改回到初始值了,这时候CAS会认为它没有被修改过。简而言之就是只比较结果,不比较过程。
+ABA问题解决
+利用 AtomicReference
类进行原子引用
public class AtomicRefrenceDemo {
+ public static void main(String[] args) {
+ User z3 = new User("张三", 22);
+ User l4 = new User("李四", 23);
+ AtomicReference<User> atomicReference = new AtomicReference<>();
+ atomicReference.set(z3);
+ System.out.println(atomicReference.compareAndSet(z3, l4) + "\t" + atomicReference.get().toString());
+ System.out.println(atomicReference.compareAndSet(z3, l4) + "\t" + atomicReference.get().toString());
+ }
+}
+
+@Getter
+@ToString
+@AllArgsConstructor
+class User {
+ String userName;
+ int age;
+}
+
// 输出结果
+true User(userName=李四, age=23)
+false User(userName=李四, age=23)
+
使用时间戳的原子引用AtomicStampedReference
修改版本号。主要是在对象中额外再增加一个标记来标识对象是否有过变更
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
+
+public static void main(String[] args) {
+ new Thread(() -> {
+ int stamp = atomicStampedReference.getStamp();
+ System.out.println(Thread.currentThread().getName() + "\t第1次版本号" + stamp);
+ try {
+ TimeUnit.SECONDS.sleep(2);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
+ System.out.println(Thread.currentThread().getName() + "\t第2次版本号" + atomicStampedReference.getStamp());
+ atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
+ System.out.println(Thread.currentThread().getName() + "\t第3次版本号" + atomicStampedReference.getStamp());
+ }, "Thread 3").start();
+
+ new Thread(() -> {
+ int stamp = atomicStampedReference.getStamp();
+ System.out.println(Thread.currentThread().getName() + "\t第1次版本号" + stamp);
+ try {
+ TimeUnit.SECONDS.sleep(4);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
+
+ System.out.println(Thread.currentThread().getName() + "\t修改是否成功" + result + "\t当前最新实际版本号:" + atomicStampedReference.getStamp());
+ System.out.println(Thread.currentThread().getName() + "\t当前最新实际值:" + atomicStampedReference.getReference());
+ }, "Thread 4").start();
+}
+
Thread 3 第1次版本号1
+Thread 4 第1次版本号1
+Thread 3 第2次版本号2
+Thread 3 第3次版本号3
+Thread 4 修改是否成功false 当前最新实际版本号:3
+Thread 4 当前最新实际值:100
+
参考文章:
+java.util.concurrent.locks
包下常用的类与接口是jdk1.5
后新增的。lock
的出现是为了弥补synchronized
关键字解决不了的一些问题。
例如:当一个代码块被synchronized
修饰了,一个线程获取了对应的锁,并执行该代码块时,其他线程只能一直等待,等待获取锁的线程释放锁,如果这个线程因为某些原因被堵塞了,没有释放锁,那么其他线程只能一直等待下去。导致效率很低。
因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。
+lock
与synchronized
最大的区别就是lock
能够手动控制锁,而synchronized
是JVM控制的。所以lock
更加灵活。lock
锁的粒度要优于synchronized
。
+在实际使用中,自然是能够替代synchronized
关键字的。
在实际使用过程中,lock
也是比较简单的。
+Lock
和ReadWriteLock
是两大锁的根接口,Lock
代表实现类是ReentrantLock
(可重入锁),ReadWriteLock
(读写锁)的代表实现类是ReentrantReadWriteLock
。
lock()
+lock()
方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。
+如果采用Lock
,必须主动去释放锁,并且在发生异常时,不会自动释放锁。
+因此,一般来说,使用Lock
必须在try…catch…
块中进行,并且将释放锁的操作放在finally
块中进行,以保证锁一定被被释放,防止死锁的发生。
Lock l = ...;
+ l.lock();
+ try {
+ // access the resource protected by this lock
+ } finally {
+ l.unlock();
+ }
+
trylock()
+尝试获取锁,如果锁可用则返回true,不可用则返回false。也就是说,这个方法无论如何都会立即返回,在拿不到锁时不会一直在那等待。
++++
tryLock(long time, TimeUnit unit)
方法和tryLock()
方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false,同时可以响应中断。 +如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
Lock lock = ...;
+ if (lock.tryLock()) {
+ try {
+ // manipulate protected state
+ } finally {
+ lock.unlock();
+ }
+ } else {
+ // perform alternative actions
+ }
+
lockInterruptibly()
+当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。
+例如,当两个线程同时通过lock.lockInterruptibly()
想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()
能够中断线程B的等待过程。
注意,当一个线程获取了锁之后,是不会被interrupt()
方法中断的。因为interrupt()
方法只能中断阻塞过程中的线程而不能中断正在运行过程中的线程。因此,当通过 lockInterruptibly()
方法获取某个锁时,如果不能获取到,那么只有进行等待的情况下,才可以响应中断的。
+与 synchronized
相比,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。
由于lockInterruptibly()
的声明中抛出了异常,所以lock.lockInterruptibly()
必须放在try块中或者在调用lockInterruptibly()
的方法外声明抛出 InterruptedException
public void method() throws InterruptedException {
+ lock.lockInterruptibly();
+ try {
+ //.....
+ }
+ finally {
+ lock.unlock();
+ }
+}
+
newCondition
+Lock
接口提供了方法Condition newCondition();
,Condition
也是一个接口,可以理解为synchronized
锁的监视器的概念;
+对于synchronized
是借助于锁与监视器,从而进行线程的同步与通信协作;而Lock
接口也提供了synchronized
的语意,对于监视器的概念,则借助于Condition
。
在lock
中可以定义多个Condition
,也就是一个锁,可以对应多个监视器,可以更加细粒度的进行同步协作的处理。
该接口有两个方法:
+//返回用于读取操作的锁
+Lock readLock()
+//返回用于写入操作的锁
+Lock writeLock()
+
ReadWriteLock
管理一组锁,一个是只读的锁,一个是写锁。共享锁与独占锁的实现是读写锁。
+Java并发库中ReetrantReadWriteLock
实现了ReadWriteLock
接口并添加了可重入的特性。
对于ReetrantReadWriteLock
其读锁是共享锁而写锁是独占锁,读锁的共享可保证并发读是非常高效的;
+需要注意的是,读写、写读和写写的过程是互斥的,只有读读不是互斥的。
读写锁使用示例
+public class MainTest {
+
+ public static void main(String[] args) {
+ MyCache myCache = new MyCache();
+ for (int i = 0; i < 10; i++) {
+ int finalI = i;
+ new Thread(() -> {
+ myCache.put(finalI + "", finalI + "");
+ }, String.valueOf(i)).start();
+ }
+
+ System.out.println("---------------");
+
+ for (int i = 0; i < 10; i++) {
+ int finalI = i;
+ new Thread(() -> {
+ myCache.get(finalI + "");
+ }, String.valueOf(i)).start();
+ }
+ }
+}
+
+class MyCache {
+
+ private volatile Map<String, Object> map = new HashMap<>();
+
+ private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
+
+ // 写操作
+ public void put(String key, Object value) {
+ rwLock.writeLock().lock();
+ try {
+ System.out.println("开始 写入 ..." + key);
+ map.put(key, value);
+ System.out.println("写入完成 ...");
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ rwLock.writeLock().unlock();
+ }
+
+ }
+
+ // 读操作
+ public Object get(String key) {
+ Object obj = null;
+ rwLock.readLock().lock();
+ try {
+ System.out.println("开始读取 ..." + key);
+ obj = map.get(key);
+ System.out.println("读取完成 ..." + obj);
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ rwLock.readLock().unlock();
+ }
+ return obj;
+ }
+
+}
+
LockSupport
是java.util.concurrent.locks
包下的一个工具类。
+LockSupport
类使用了一种名为Permit
(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit
只有两个值1和零,默认是零。
官网解释:LockSupport
是用来创建锁和其他同步类的基本线程阻塞原语;
其中有两个重要的方法,通过park()
和unpark()
方法来实现阻塞和唤醒线程的操作;可以理解为wait()
和notify
的加强版。
传统等待唤醒机制:
+Object
中的wait()
方法让线程等待,使用Object
中的notify
方法唤醒线程Condition
的await()
方法让线程等待,使用signal()
方法唤醒线程传统等待唤醒机制的弊端:
+wait
和notify
/await()
和signal()
方法必须要在同步块或同步方法里且成对出现使用,如果没有在synchronized
代码块使用则抛出java.lang.IllegalMonitorStateException
;wait
/await()
后notify
/signal()
,如果先notify
后wait
会出现另一个线程一直处于等待状态;LockSupport
对比传统等待唤醒机制,能够解决上面的弊端:
public class MainTest {
+ public static void main(String[] args) {
+
+ Thread t1=new Thread(()->{
+// try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) {e.printStackTrace();}
+ System.out.println(Thread.currentThread().getName()+"\t"+"coming....");
+ LockSupport.park();
+ /*
+ 如果这里有两个LockSupport.park(),因为permit的值为1,上一行已经使用了permit
+ 所以下一行被注释的打开会导致程序处于一直等待的状态
+ * */
+ //LockSupport.park();
+ System.out.println(Thread.currentThread().getName()+"\t"+"被B唤醒了");
+ },"A");
+ t1.start();
+
+ Thread t2=new Thread(()->{
+ System.out.println(Thread.currentThread().getName()+"\t"+"唤醒A线程");
+ //有两个LockSupport.unpark(t1),由于permit的值最大为1,所以只能给park一个通行证
+ LockSupport.unpark(t1);
+ //LockSupport.unpark(t1);
+ },"B");
+ t2.start();
+ }
+}
+
LockSupport
原理是调用的Unsafe
中的native
代码。以unpark、park
为例:
public static void unpark(Thread thread) {
+ if (thread != null)
+ UNSAFE.unpark(thread);
+ }
+
+ public static void park(Object blocker) {
+ Thread t = Thread.currentThread();
+ setBlocker(t, blocker);
+ UNSAFE.park(false, 0L);
+ setBlocker(t, null);
+ }
+
理解:
+park
方法时,如果有凭证则会直接消耗这张凭证然后退出;如果没有凭证就必须堵塞等待凭证可用;unpark
方法则相反,调用该方法会增加一个凭证,连续调用两次unpark()
和调用一次一样,只会增加一个凭证。为什么可以先唤醒线程后阻塞线程?
+因为unpark
获得了一个凭证,之后再调用park
方法,就可以名正言顺的凭证消费,所以不会阻塞。
为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
+因为凭证的数量最多为1,连续调用两次unpark
和调用一次unpark
效果一样,只会增加一个凭证;
+而调用两次park却需要消费两个凭证,证不够,不能放行。
AQS是指java.util.concurrent.locks
包下的一个抽象类AbstractQueuedSynchronizer
译为:抽象的队列同步器。
在JUC包下,能够看到有许多类都继承了AQS,例如,ReentrantLock 、CountDownLatch 、 ReentrantReadWriteLock 、 Semaphore
;
+所以AQS是JUC内容中重要的基础。
++同步、同步器? +同步,面向锁的使用者,定义了程序员和锁交互的使用层API; +同步器,面向锁的实现者,统一规范,实现锁 自定义等待唤醒机制等等;
+
AQS是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的CLH(FIFO)
队列的变种来完成资源获取线程的排队工作;
+将每条将要去抢占资源的线程封装成一个Node
节点来实现锁的分配,有一个int
类变量表示持有锁的状态,通过CAS完成对status
值的修改。
在多线程并发环境下,使用lock加锁,当处在加锁与解锁之间的代码,只能有一个线程来执行;这时候其他线程不能够获取锁,如果不处理线程就会造成了堵塞;
+在AQS框架中,会将暂时获取不到锁的线程加入到队列里,这个队列就是AQS的抽象表现。它会将这些线程封装成队列的结点,通过CAS、自旋以及LockSuport.park()
的方式,维护state
变量的状态,使并发达到同步的效果。
AQS中的队列,是指CLH队列(Craig, Landin, and Hagerste[三个人名组成])锁队列的变体,是一个双向队列。
+队列中的元素即Node
结点,每个Node
中包含:头结点、尾结点、等待状态、存放的线程等;Node
遵循从尾部入队,从头部出队的规则,即先进先出原则。
++详细可查看 java.util.concurrent.locks; 包下 AbstractQueuedSynchronizer 类。
+
AQS可以理解为一个框架,因为它定义了一些JUC包下常用"锁"的标准。
+AQS简单来说,包含一个status
和一个队列;status
保存线程持有锁的状态,用于判断该线程获没获取到锁,没获取到锁就去队列中排队。
+队列是由Node
结点构成,每个Node
结点里面主要包含一个waitStatus
和保存的线程。
ReentrantLock
译为,可重入锁,它的原理用到了AQS。
++AQS里面有个变量叫State,它的值有3种状态:没占用是0,占用了是1,大于1是可重入锁 +如果A、B两个线程进来了以后,请问这个总共有多少个Node节点?答案是3个,其中队列的第一个是傀儡节点(哨兵节点)
+
ReentrantLock
原理说简单一点,就是加锁解锁的过程。
在多线程并发环境下,某个线程持有锁,将state
由0设置为1,如果在有其他线程再次进入,线程则会经过一系列判断,然后构建Node结点,最终形成双向链表结构。
+最后在执行LockSupport.park()
方法,将等待的线程挂起,如果当前持有锁的线程释放了锁,则将state
变量设置为0,调用LockSpoort.unpark()
方法指定唤醒等待队列中的某个线程。
ReentrantLock
使用
public class AQSDemo {
+ public static void main(String[] args) {
+ ReentrantLock lock = new ReentrantLock();
+ new Thread(() -> {
+ lock.lock();
+ try{
+ System.out.println("-----A thread come in");
+
+ try { TimeUnit.MINUTES.sleep(20); }catch (Exception e) {e.printStackTrace();}
+ }finally {
+ lock.unlock();
+ }
+ },"A").start();
+
+ new Thread(() -> {
+ lock.lock();
+ try{
+ System.out.println("-----B thread come in");
+ }finally {
+ lock.unlock();
+ }
+ },"B").start();
+
+ new Thread(() -> {
+ lock.lock();
+ try{
+ System.out.println("-----C thread come in");
+ }finally {
+ lock.unlock();
+ }
+ },"C").start();
+ }
+}
+
ReentrantLock加锁
+ReentrantLock
原理用到了AQS,而AQS包括一个线程队列和一个state变量;所以ReentrantLock
加锁过程,可以简单理解为state
变量的变化。
+如果在多线程并发的环境下,还要有其他线程被保存到AQS的队列中。
+加锁过程,如图所示:
+
ReentrantLock
加锁,有两种形式,默认是非公平锁,但可以通过构造方法来指定为公平锁。
public static void main(String[] args) {
+ ReentrantLock reentrantLock = new ReentrantLock(true);
+ }
+ //⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇
+ /**
+ * Creates an instance of {@code ReentrantLock} with the
+ * given fairness policy.
+ *
+ * @param fair {@code true} if this lock should use a fair ordering policy
+ */
+ public ReentrantLock(boolean fair) {
+ sync = fair ? new FairSync() : new NonfairSync();
+ }
+
无论是公平锁还是非公平锁,由于用到了AQS框架,所以底层实现的逻辑大致是差不多的,ReentrantLock
加锁方法调用栈:
lock() --> acquire() --> tryAcquire() --> addWaiter() --> acquireQueued() --> selfInterrupt()
+
虽然大致逻辑差不多,但是区别总是有的,总的来说非公平锁比非公平锁在代码里面多了几行判断;
+// ===========重写 lock 方法对比===========
+ // 公平锁
+ final void lock() {
+ acquire(1);
+ }
+
+ // 非公平锁
+ final void lock() {
+ if (compareAndSetState(0, 1))
+ setExclusiveOwnerThread(Thread.currentThread());
+ else
+ acquire(1);
+ }
+
// ===========重写 tryAcquire 方法对比===========
+
+ // 公平锁
+ protected final boolean tryAcquire(int acquires) {
+ final Thread current = Thread.currentThread();
+ int c = getState();
+ if (c == 0) {
+ 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;
+ }
+
+ // 非公平锁
+ protected final boolean tryAcquire(int acquires) {
+ return nonfairTryAcquire(acquires);
+ }
+
+ 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;
+ }
+
在重写的tryAcquire
方法里,公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()
;
该方法作用:保证等待队列中的线程按照从头到尾的顺序排队获取锁。 +举个例子,目前队列中有两个线程A、B,线程A,在线程B的前面;在当前线程释放锁的时候,线程B获取到了锁,该方法会判断当前头结点的下一个结点中存放的线程跟当前线程相不相同;
+在这里头结点的下一个结点存放的线程是傀儡结点线程为null
,而当前线程是线程B,所以返回true
,回到上一个方法true
取反就是false
,则获取锁失败。
public final boolean hasQueuedPredecessors() {
+ // The correctness of this depends on head being initialized
+ // before tail and on head.next being accurate if the current
+ // thread is first in queue.
+ Node t = tail; // Read fields in reverse initialization order
+ Node h = head;
+ Node s;
+ return h != t &&
+ ((s = h.next) == null || s.thread != Thread.currentThread());
+ }
+
在执行完tryAcquire
方法之后就会执行addWaiter
方法。
addWaiter
方法作用;当第一次将等待的线程添加到队列时,先会调用enq方法;如果不是第一次调用,即尾结点不为空,队列中已经有了其他线程结点,则会直接将当前线程的前结点指向尾结点,即队列中最后一个线程结点;
+然后用CAS将前一个结点的下一个结点指向当前结点,最后返回添加到队列中的结点。
private Node addWaiter(Node mode) {
+ Node node = new Node(Thread.currentThread(), mode);
+ // Try the fast path of enq; backup to full enq on failure
+ Node pred = tail;
+ if (pred != null) {
+ node.prev = pred;
+ if (compareAndSetTail(pred, node)) {
+ pred.next = node;
+ return node;
+ }
+ }
+ enq(node);
+ return node;
+ }
+
enq方法作用是,将等待获取锁的线程封装成Node结点,并将Node结点串联起来,形成双向链表结构;简而言之就是将线程添加到等待队列中去。
+该方法运用自旋机制,如果添加的结点为第一个结点,则会在第一个实际结点之前,先生成一个“傀儡结点”; +头结点指向指向傀儡结点,傀儡结点的后结点则指向添加的第一个结点;添加的第一个结点的前结点指向傀儡结点,尾结点指向实际结点。然后将处理好的实际结点返回。
+ private Node enq(final Node node) {
+ for (;;) {
+ Node t = tail;
+ if (t == null) { // Must initialize
+ if (compareAndSetHead(new Node()))
+ tail = head;
+ } else {
+ node.prev = t;
+ if (compareAndSetTail(t, node)) {
+ t.next = node;
+ return t;
+ }
+ }
+ }
+ }
+
之后在执行acquireQueued
方法。该方法用到了自旋机制;首先先判断当前结点是否为头结点,如果是头结点,就让头结点中的线程尝试获取锁,之后执行异常结点的操作。
+如果不是头结点,就会尝试让当前线程挂起,直到持有锁的线程释放锁,唤醒等待的线程,之后再去尝试获取锁。
final boolean acquireQueued(final Node node, int arg) {
+ boolean failed = true;
+ try {
+ boolean interrupted = false;
+ for (;;) {
+ final Node p = node.predecessor();
+ if (p == head && tryAcquire(arg)) {
+ setHead(node);
+ p.next = null; // help GC
+ failed = false;
+ return interrupted;
+ }
+ if (shouldParkAfterFailedAcquire(p, node) &&
+ parkAndCheckInterrupt())
+ interrupted = true;
+ }
+ } finally {
+ if (failed)
+ cancelAcquire(node);
+ }
+ }
+
如果不是头结点,则会执行shouldParkAfterFailedAcquire
方法:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
+ int ws = pred.waitStatus;
+ if (ws == Node.SIGNAL)
+ return true;
+ if (ws > 0) {
+ do {
+ node.prev = pred = pred.prev;
+ } while (pred.waitStatus > 0);
+ pred.next = node;
+ } else {
+ compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
+ }
+ return false;
+ }
+
执行该方法,首先判断上一个结点的waitStatus
;
+如果该队列只有一个结点,则上一个结点为头结点,此时头结点的waitStatus=0
,经过该方法会将上一个结点的waitStatus
通过CAS,设置为-1;
+因为最外部是一个自旋机制,会一直循环,当第二次进入该方法,则会直接返回true。返回true,则意味着当前线程将进入堵塞状态,会执行parkAndCheckInterrupt()
方法。
private final boolean parkAndCheckInterrupt() {
+ LockSupport.park(this);
+ return Thread.interrupted();
+ }
+
调用LockSupport.park()
方法让线程挂起,直到持有锁的线程将它们唤醒。
ReentrantLock解锁 +
+ReentrantLock
释放锁调用栈:
unlock() --> release() --> tryRelease() --> unparkSuccessor()
+
release
方法,如果tryRelease
方法返回true,则判队列中的头结点中的waitStatus
,如果不等于0则,执行unparkSuccessor
方法,按唤醒队列中等待的线程。
核心就是调用tryRelease
方法和unparkSuccessor
方法:
public final boolean release(int arg) {
+ if (tryRelease(arg)) {
+ Node h = head;
+ if (h != null && h.waitStatus != 0)
+ unparkSuccessor(h);
+ return true;
+ }
+ return false;
+ }
+
tryRelease
方法作用是尝试释放锁;首先获取当前持有锁线程的state
,并使其减1;
+如果减一后的state
值等于0,则认为该线程马上要释放锁,将当前持有锁的线程为null,将0设置为state
的新值,返回true。
protected final boolean tryRelease(int releases) {
+ int c = getState() - releases;
+ if (Thread.currentThread() != getExclusiveOwnerThread())
+ throw new IllegalMonitorStateException();
+ boolean free = false;
+ if (c == 0) {
+ free = true;
+ setExclusiveOwnerThread(null);
+ }
+ setState(c);
+ return free;
+ }
+
由于之前加锁等待队列中是自旋机制,由于持有锁的线程唤醒队列中排队的线程,队列中的线程则会尝试再次获取锁。
+首先,将头结点从前向后移动一个结点,随后清空该结点的线程对象、该结点的前结点、后结点,即将该结点设置成新的傀儡结点(哨兵结点),最后结束循环。
+private void unparkSuccessor(Node node) {
+ int ws = node.waitStatus;
+ if (ws < 0)
+ compareAndSetWaitStatus(node, ws, 0);
+
+ Node s = node.next;
+ if (s == null || s.waitStatus > 0) {
+ s = null;
+ for (Node t = tail; t != null && t != node; t = t.prev)
+ if (t.waitStatus <= 0)
+ s = t;
+ }
+ if (s != null)
+ LockSupport.unpark(s.thread);
+}
+
总结
+ReentrantLock
在采用非公平锁构造时,首先检查锁状态,如果锁可用,直接通过CAS设置成持有状态,且把当前线程设置为锁的拥有者。
+如果当前锁已经被持有,那么接下来进行可重入检查,如果可重入,需要为锁状态加上请求数。如果不属于上面两种情况,那么说明锁是被其他线程持有,
+当前线程应该放入等待队列。
在放入等待队列的过程中,首先要检查队列是否为空队列,如果为空队列,需要创建虚拟的头节点,然后把对当前线程封装的节点加入到队列尾部。 +由于设置尾部节点采用了CAS,为了保证尾节点能够设置成功,这里采用了无限循环的方式,直到设置成功为止。
+在完成放入等待队列任务后,则需要维护节点的状态,以及及时清除处于Cancel
状态的节点,以帮助垃圾收集器及时回收。
+如果当前节点之前的节点的等待状态小于1,说明当前节点之前的线程处于等待状态(挂起),那么当前节点的线程也应处于等待状态(挂起)。
+挂起的工作是由 LockSupport
类支持的,LockSupport
通过JNI调用本地操作系统来完成挂起的任务。
+在当前等待的线程,被唤起后,检查中断状态,如果处于中断状态,那么需要中断当前线程。
count down latch
直译为:倒计时门闩,也可以叫做闭锁。
++门闩,汉语词汇。拼音:mén shuān 释义:指门关上后,插在门内使门推不开的滑动插销。
+
CountDownLatch
JDK文档注释:
++A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.
+
大意:一种同步辅助工具,允许一个或多个线程等待,直到在其他线程中执行的一组操作完成。
+举个例子,晚上教室关门,要同学都离开之后,再关门:
+public class MainTest {
+ public static void main(String[] args) throws InterruptedException {
+ CountDownLatch countDownLatch = new CountDownLatch(7);
+ for (int i = 0; i < 7; i++){
+ new Thread(() -> {
+ System.out.println("同学"+Thread.currentThread().getName() + "\t 离开");
+ countDownLatch.countDown();
+ },String.valueOf(i)).start();
+ }
+ countDownLatch.await();
+ System.out.println("关门...");
+ }
+}
+
再比如,跑步比赛,裁判的发令枪一响,参赛者就开始跑步:
+public class MainTest {
+ public static void main(String[] args) throws InterruptedException {
+ CountDownLatch countDownLatch = new CountDownLatch(1);
+ for (int i = 0; i < 5; i++) {
+ new Thread(() -> {
+ try {
+ //准备完毕……运动员都阻塞在这,等待号令
+ countDownLatch.await();
+ String parter = "【" + Thread.currentThread().getName() + "】";
+ System.out.println(parter + "开始执行……");
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }).start();
+ }
+ Thread.sleep(2000);// 裁判准备发令
+ countDownLatch.countDown();// 发令枪:执行发令
+ }
+}
+
CountDownLatch
是通过一个计数器来实现的,计数器的初始值为线程的数量;
+可以通过CountDownLatch
的构造函数,可以指定,不能小于0:
public CountDownLatch(int count) {
+ if (count < 0) throw new IllegalArgumentException("count < 0");
+ this.sync = new Sync(count);
+ }
+
每次调用countDown()
方法可以让计数器减1,底层是AQS框架,这里就不写了。
+调用了await()
进行阻塞等待的线程,当计数器减到0后,再执行await()
之后的代码。
参考文章:
+Cyclic Barrier
直译为:循环屏障,是Java中关于线程的计数器,也可以叫它栅栏。
它与CountDownLatch
的作用是相反的,CountDownLatch
是定义一个次数,然后减,直到减到0,在去执行一些任务;
+而CyclicBarrier
是定义一个上限次数,然后从零开始加,直到加到定义的上限次数,在去执行一些任务。
++CyclicBarrier与CountDownLatch作用是相反的,CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置,可以使用多次,所以CyclicBarrier能够处理更为复杂的场景。
+
CyclicBarrier
JDK文档注释:
++A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point. CyclicBarriers are useful in programs involving a fixed sized party of threads that must occasionally wait for each other. The barrier is called cyclic because it can be re-used after the waiting threads are released.
+
大意:一种同步辅助工具,允许一组线程相互等待到达一个共同的障碍点。cyclicbarrier
在包含固定大小的线程组的程序中非常有用,这些线程必须偶尔相互等待。
+这个屏障被称为cyclic·
,因为它可以在等待的线程被释放后被重用。
它要做的事情是,让一组线程达到一个屏障(同步点)时被阻塞,直到最后一个线程达到屏障时,所有被屏障拦截的线程才会继续干活线程进入屏障通过CyclicBarrier.await()
方法。
简单说就是让一组线程相互等待,当达到一个共同点时,所有之前等待的线程再继续执行,且 CyclicBarrier
功能可重复使用。
例如,凑齐七颗龙珠召唤神龙:
+public class MainTest {
+ public static void main(String[] args) {
+ CyclicBarrier cyclicBarrier = new CyclicBarrier(7,() -> {
+ System.out.println("凑齐七颗龙珠,召唤神龙!");
+ });
+ for (int i = 1; i <= 7;i++){
+ new Thread(() -> {
+ System.out.println("拿到"+Thread.currentThread().getName() + "星龙珠");
+ try {
+ cyclicBarrier.await();
+ } catch (InterruptedException | BrokenBarrierException e) {
+ e.printStackTrace();
+ }
+ },String.valueOf(i)).start();
+ }
+ }
+}
+
CyclicBarrier
原理简单说明:
CyclicBarrier
是基于 ReentrantLock 实现的,其底层也是基于 AQS 的。
在 CyclicBarrier
类的内部有一个计数器 count
,当 count
不为 0 时,每个线程在到达屏障点会先调用 await
方法将自己阻塞,此时计数器会减 1,直到计数器减为 0 的时候,所有因调用 await
方法而被阻塞的线程就会被唤醒继续执行。
+当 count
计数器变成 0 之后,就会进入下一轮阻塞,此时 parties
(parties
是在 new CyclicBarrier(parties)
时设置的值)会将它的值赋值给 count
从而实现复用。
CyclicBarrier
内部使用了ReentrantLock
和Condition
两个类。它有两个构造函数:
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;
+}
+
调用await
方法的线程告诉CyclicBarrier
已经到达同步点,然后当前线程被阻塞。
+直到达到定义上限个数的线程都到达了屏障;
参与线程调用了await
方法,CyclicBarrier
同样提供带超时时间的await
和不带超时时间的await
方法:
public int await() throws InterruptedException, BrokenBarrierException {
+ try {
+ // 不超时等待
+ return dowait(false, 0L);
+ } catch (TimeoutException toe) {
+ throw new Error(toe); // cannot happen
+ }
+}
+
+public int await(long timeout, TimeUnit unit)
+ throws InterruptedException,
+ BrokenBarrierException,
+ TimeoutException {
+ return dowait(true, unit.toNanos(timeout));
+}
+
这两个方法最终都会调用dowait(boolean, long)
方法,它也是CyclicBarrier
的核心方法:
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()) {
+ // 将损坏状态设置为true
+ // 并通知其他阻塞在此栅栏上的线程
+ breakBarrier();
+ throw new InterruptedException();
+ }
+
+ // 获取下标
+ int index = --count;
+ // 如果是 0,说明最后一个线程调用了该方法
+ if (index == 0) { // tripped
+ boolean ranAction = false;
+ try {
+ final Runnable command = barrierCommand;
+ // 执行栅栏任务
+ if (command != null)
+ command.run();
+ ranAction = true;
+ // 更新一代,将count重置,将generation重置
+ // 唤醒之前等待的线程
+ nextGeneration();
+ return 0;
+ } finally {
+ // 如果执行栅栏任务的时候失败了,就将损坏状态设置为true
+ 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 {
+ // 上面条件不满足,说明这个线程不是这代的
+ // 就不会影响当前这代栅栏的执行,所以,就打个中断标记
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ // 当有任何一个线程中断了,就会调用breakBarrier方法
+ // 就会唤醒其他的线程,其他线程醒来后,也要抛出异常
+ if (g.broken)
+ throw new BrokenBarrierException();
+
+ // g != generation表示正常换代了,返回当前线程所在栅栏的下标
+ // 如果 g == generation,说明还没有换代,那为什么会醒了?
+ // 因为一个线程可以使用多个栅栏,当别的栅栏唤醒了这个线程,就会走到这里,所以需要判断是否是当前代。
+ // 正是因为这个原因,才需要generation来保证正确。
+ if (g != generation)
+ return index;
+
+ // 如果有时间限制,且时间小于等于0,销毁栅栏并抛出异常
+ if (timed && nanos <= 0L) {
+ breakBarrier();
+ throw new TimeoutException();
+ }
+ }
+ } finally {
+ // 释放独占锁
+ lock.unlock();
+ }
+}
+
dowait
方法作用,如果该线程不是最后一个调用await
方法的线程,则它会一直处于等待状态,除非发生以下情况:
index == 0
;CyclicBarrier的reset()
方法。该方法会将屏障重置为初始状态;参考文章:
+Semaphore
译为信号量,有时被称为信号灯。可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。
++信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数量的控制。
+
Semaphore
JDK文档注释:
++A counting semaphore. Conceptually, a semaphore maintains a set of permits. Each {@link #acquire} blocks if necessary until a permit is available, and then takes it. Each {@link #release} adds a permit, potentially releasing a blocking acquirer.
+
大意:计数信号量。从概念上讲,信号量维护一组许可。如果需要,每个{@link #acquire}
块,直到有一个许可可用,然后获取它。
+每个{@link #release}
添加一个许可,可能释放一个阻塞的获取者。
+但是,没有实际的permit对象被使用;{@code Semaphore}
只保留可用数量的计数,并相应地执行操作。
简单理解,使用acquire
方法获取一个令牌(许可),进入堵塞状态,使用release
方法则释放一个令牌(许可)唤醒一个堵塞的线程。
举个例子,抢车位,九辆车抢三个车位,车位满了之后只有等里面的车离开停车场外面的车才可以进入:
+public class MainTest {
+ public static void main(String[] args) {
+
+ Semaphore semaphore = new Semaphore(3);
+
+ for (int i = 1; i <= 9; i++) {
+ new Thread(() -> {
+ try {
+ semaphore.acquire();
+ System.out.println("第" + Thread.currentThread().getName() + "辆车,抢到车位");
+ Thread.sleep(2000);
+ System.out.println("停车结束.");
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }finally {
+ semaphore.release();
+ }
+ }, String.valueOf(i)).start();
+ }
+
+ }
+}
+
Semaphore
有两个构造方法,可以通过其中一个构造方法来指定锁的类型,是公平锁还是非公平锁:
// 设置令牌(许可)数量
+ public Semaphore(int permits) {
+ sync = new NonfairSync(permits);
+ }
+
+ // 可以设置锁的类型,是否是公平锁
+ public Semaphore(int permits, boolean fair) {
+ sync = fair ? new FairSync(permits) : new NonfairSync(permits);
+ }
+
Semaphore
的底层也用到了AQS。
Semaphore
是用来保护一个或者多个共享资源的访问,Semaphore
内部维护了一个计数器,其值为可以访问的共享资源的个数。
+一个线程要访问共享资源,先获得信号量,如果信号量的计数器值大于1,意味着有共享资源可以访问,则使其计数器值减去1,再访问共享资源。
如果计数器值为0,线程进入休眠。当某个线程使用完共享资源后,释放信号量,并将信号量内部的计数器加1,之前进入休眠的线程将被唤醒并再次试图获得信号量。
+当调用semaphore.acquire()
方法时:
state
,获取一个令牌则修改为state=state-1
;state<0
,则代表令牌数量不足,此时会创建一个Node
节点加入阻塞队列,挂起当前线程;state>=0
,则代表获取令牌成功;当调用semaphore.release()
方法时:
state
修改为state=state+1
的过程;state=state-1
的操作,如果state>=0
则获取令牌成功,否则重新进入阻塞队列,挂起线程;synchronized
是Java提供的关键字,可译为同步。可用来给对象、方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。
+++
synchronized
关键字在需要原子性、可见性和有序性这三种特性的时候都可以作为其中一种解决方案,看起来是“万能”的。的确,大部分并发控制操作都能使用synchronized
来完成。
修饰的对象 | +作用范围 | +作用对象 | +
---|---|---|
同步一个实例方法 | +整个实例方法 | +调用此方法的对象 | +
同步一个静态方法 | +整个静态方法 | +此类的所有对象 | +
同步代码块-对象 | +整个代码块 | +调用此代码块的对象 | +
同步代码块-类 | +整个代码块 | +此类的所有对象 | +
代码演示
+public class MainTest {
+
+ //共享资源
+ static int i = 0;
+
+ public static void main(String[] args) throws InterruptedException {
+ MainTest mainTest = new MainTest();
+ Thread thread1 = new Thread(() -> {
+ for (int j = 0; j < 1000000; j++) {
+ mainTest.increase();
+ }
+ }, "线程1");
+ Thread thread2 = new Thread(() -> {
+ for (int j = 0; j < 1000000; j++) {
+ mainTest.increase();
+ }
+ }, "线程2");
+
+ thread1.start();
+ thread2.start();
+
+ // join方法的作用是调用线程等待该线程完成后,才能继续用下运行。
+ thread1.join();
+ thread2.join();
+ System.out.println(i);
+ }
+
+ public synchronized void increase() {
+ i++;
+ }
+
+ // 通过是否使用synchronized来体会
+// public void increase() {
+// i++;
+// }
+}
+
对于上面的代码如果加上synchronized
最后输出的结果为2000000;
+如果没有加,最后的结果很大程度上是小于2000000的,当然不排除偶然情况,所以这里不是肯定句。
由此可见,当某个线程运行到这个方法时,都要检查有没有其它线程正在用这个方法(或者该类的其他同步方法),有的话要等待正在使用 synchronized
方法的线程运行完这个方法后再运行此线程,没有的话,锁定调用者,然后直接运行。
当 synchronized
作用于静态方法时,其锁就是当前类的class对象锁。由于静态成员不专属于任何一个实例对象,是类成员,因此通过class对象锁可以控制静态 成员的并发操作。
需要注意的是如果一个线程A调用一个实例对象的非static synchronized
方法,而线程B需要调用这个实例对象所属类的静态 synchronized
方法,是允许的,不会发生互斥现象;
+因为访问静态 synchronized
方法占用的锁是当前类的class对象,而访问非静态 synchronized
方法占用的锁是当前实例对象锁。
代码演示
+public class MainTest {
+
+ //共享资源
+ static int i = 0;
+
+ public static void main(String[] args) throws InterruptedException {
+ MainTest mainTest = new MainTest();
+ Thread thread1 = new Thread(() -> {
+ for (int j = 0; j < 1000000; j++) {
+// increase();
+ mainTest.increaseNoneStatic();
+ }
+ }, "线程1");
+ Thread thread2 = new Thread(() -> {
+ for (int j = 0; j < 1000000; j++) {
+// increase();
+// mainTest.increaseNoneStatic();
+ }
+ }, "线程2");
+
+ thread1.start();
+ thread2.start();
+
+ // join方法的作用是调用线程等待该线程完成后,才能继续用下运行。
+ thread1.join();
+ thread2.join();
+ System.out.println(i);
+ }
+
+ // static修饰 锁住的是类对象
+ public static synchronized void increase() {
+ i++;
+ }
+
+ // 无static修饰 锁住的是调用该方法的 当前对象
+ public synchronized void increaseNoneStatic() {
+ i++;
+ }
+}
+
同步一个静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁。也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员(static表明这是该类的一个静态资源,不管new了多少个对象,只有一份,所以对该类的所有对象都加了锁)。
+所以如果一个线程A调用一个实例对象的非静态synchronized
方法,而线程B需要调用这个实例对象所属类的静态synchronized
方法,是允许的,不会发生互斥现象,因为访问静态synchronized
方法占用的锁是当前类的锁,而访问非静态synchronized
方法占用的锁是当前实例对象锁。
在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,这样做就有点浪费; +此时我们可以使用同步代码块的方式对需要同步的代码进行包裹。
+代码演示
+public class MainTest {
+
+ //共享资源
+ static int i = 0;
+
+ public static void main(String[] args) throws InterruptedException {
+ MainTest mainTest = new MainTest();
+ Thread thread1 = new Thread(() -> {
+ for (int j = 0; j < 1000000; j++) {
+ mainTest.increase();
+ }
+ }, "线程1");
+ Thread thread2 = new Thread(() -> {
+ for (int j = 0; j < 1000000; j++) {
+ mainTest.increase();
+ }
+ }, "线程2");
+
+ thread1.start();
+ thread2.start();
+
+ // join方法的作用是调用线程等待该线程完成后,才能继续用下运行。
+ thread1.join();
+ thread2.join();
+ System.out.println(i);
+ }
+
+ public void increase() {
+ synchronized (this){
+ i++;
+ }
+ }
+
+// public void increase() {
+// i++;
+// }
+
+}
+
除了使用synchronized (this)
锁定,当然静态方法是没有this对象的;也可以使用class
对象,和程序中创建的一些对象来做为锁。
// class类对象锁
+synchronized(MainTest.class){
+ // ...
+}
+
+//
+
当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的对象来充当锁;
+private byte[] lock = new byte[0];
+public void method(){
+ synchronized(lock) {
+ // .....
+ }
+}
+
零长度的byte
数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]
对象只需3条操作码,而Object lock = new Object()
则需要7行操作码。
当一个线程访问对象的一个synchronized(this)
同步代码块时,另一个线程仍然可以访问该对象中的非synchronized(this)
同步代码块。
public class MainTest {
+ public static void main(String[] args) {
+ Counter counter = new Counter();
+ Thread thread1 = new Thread(counter, "A");
+ Thread thread2 = new Thread(counter, "B");
+ thread1.start();
+ thread2.start();
+ }
+}
+
+class Counter implements Runnable{
+ private int count;
+
+ public Counter() {
+ count = 0;
+ }
+
+ public void countAdd() {
+ synchronized(this) {
+ for (int i = 0; i < 5; i ++) {
+ try {
+ System.out.println(Thread.currentThread().getName() + ":" + (count++));
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ //非synchronized代码块,未对count进行读写操作,所以可以不用synchronized
+ public void printCount() {
+ for (int i = 0; i < 5; i ++) {
+ try {
+ System.out.println(Thread.currentThread().getName() + " count:" + count);
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @Override
+ public void run() {
+ String threadName = Thread.currentThread().getName();
+ if (threadName.equals("A")) {
+ countAdd();
+ } else if (threadName.equals("B")) {
+ printCount();
+ }
+ }
+}
+
参考文章:
+阅读前建议先了解Java对象头。
+如果你对对象头有了解,你就知道在Java中synchronized
锁对象时,其实就是改变对象中的对象头的markword
的锁的标志位来实现的。
通过上面的使用,可以体会到被synchronized
修饰的代码块及方法,在同一时间,只能被单个线程访问。
用javap -v MainTest.class
命令反编译下面代码,我们就能了解到JVM对synchronized
是怎么处理的了。
public class MainTest {
+
+ synchronized void demo01() {
+ System.out.println("demo 01");
+ }
+
+ void demo02() {
+ synchronized (MainTest.class) {
+ System.out.println("demo 02");
+ }
+ }
+
+}
+
synchronized void demo01();
+ descriptor: ()V
+ flags: ACC_SYNCHRONIZED
+ Code:
+ stack=2, locals=1, args_size=1
+ 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
+ 3: ldc #3 // String demo 01
+ 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
+ 8: return
+// ...
+void demo02();
+ descriptor: ()V
+ flags:
+ Code:
+ stack=2, locals=3, args_size=1
+ 0: ldc #5 // class content/posts/rookie/MainTest
+ 2: dup
+ 3: astore_1
+ 4: monitorenter
+ 5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
+ 8: ldc #6 // String demo 02
+ 10: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
+ 13: aload_1
+ 14: monitorexit
+ 15: goto 23
+ 18: astore_2
+ 19: aload_1
+ 20: monitorexit
+ 21: aload_2
+ 22: athrow
+ 23: return
+// ...
+
通过反编译后代码可以看出:
+ACC_SYNCHRONIZED
标记符来实现同步;monitorenter
、monitorexit
两个指令来实现同步;其中同步代码块,有两个monitorexit
指令的原因是,为了保证抛异常的情况下也能释放锁,所以javac
为同步代码块添加了一个隐式的try-finally
,在finally
中会调用monitorexit
命令释放锁。
官方文档中关于同步方法和同步代码块的实现原理描述
+++方法级的同步是隐式的。同步方法的常量池中会有一个
+ACC_SYNCHRONIZED
标志。当某个线程要访问某个方法的时候,会检查是否有ACC_SYNCHRONIZED
,如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。
++同步代码块使用
+monitorenter
和monitorexit
两个指令实现。可以把执行monitorenter
指令理解为加锁,执行monitorexit
理解为释放锁。 每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为0,当一个线程获得锁(执行monitorenter
)后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行monitorexit
指令)的时候,计数器再自减。当计数器为0的时候。锁将被释放,其他线程便可以获得锁。
其实无论是ACC_SYNCHRONIZED
还是monitorenter
、monitorexit
都是基于Monitor
实现的,每一个锁都对应一个monitor
对象;
+在Java虚拟机(HotSpot)中,Monitor
是基于C++实现的,由ObjectMonitor
实现。
在/hotspot/src/share/vm/runtime/objectMonitor.hpp
中有ObjectMonitor
的实现
// initialize the monitor, exception the semaphore, all other fields
+// are simple integers or pointers
+ObjectMonitor() {
+ _header = NULL;
+ _count = 0; //记录个数
+ _waiters = 0,
+ _recursions = 0;
+ _object = NULL;
+ _owner = NULL;
+ _WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
+ _WaitSetLock = 0 ;
+ _Responsible = NULL ;
+ _succ = NULL ;
+ _cxq = NULL ;
+ FreeNext = NULL ;
+ _EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
+ _SpinFreq = 0 ;
+ _SpinClock = 0 ;
+ OwnerIsThread = 0 ;
+ }
+
_owner
:指向持有ObjectMonitor
对象的线程_WaitSet
:存放处于wait
状态的线程队列_EntryList
:存放处于等待锁block
状态的线程队列_recursions
:锁的重入次数_count
:用来记录该线程获取锁的次数当多个线程同时访问一段同步代码时,首先会进入_EntryList
队列中,当某个线程获取到对象的monitor
后进入_Owner
区域并把monitor
中的_owner
变量设置为当前线程,同时monitor
中的计数器_count
加1。即获得对象锁。
若此时持有monitor
的线程调用wait()
方法,将释放当前对象持有的monitor
,_owner
变量恢复为null
,_count
自减1,同时该线程进入_WaitSet
集合中等待被唤醒。若当前线程执行完毕也将释放monitor
并复位变量的值,以便其他线程进入获取monitor
。
由此看来,monitor
对象存在于每个Java对象的对象头中(存储的是指针),synchronized
锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因。
ObjectMonitor
中其他方法:
bool try_enter (TRAPS) ;
+ void enter(TRAPS);
+ void exit(bool not_suspended, TRAPS);
+ void wait(jlong millis, bool interruptable, TRAPS);
+ void notify(TRAPS);
+ void notifyAll(TRAPS);
+
sychronized
加锁的时候,会调用objectMonitor
的enter
方法,解锁的时候会调用exit
方法。
+在JDK1.6之前,synchronized
的实现才会直接调用 ObjectMonitor
的enter
和exit
,这种锁被称之为重量级锁。
++早期的
+synchronized
效率低的原因: +Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统的帮忙,监视器锁monitor
是依赖于底层的操作系统的Mutex Lock
来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态。因此状态转换需要花费很多的处理器时间。 +对于代码简单的同步块(如被synchronized
修饰的get
、set
方法)状态转换消耗的时间有可能比用户代码执行的时间还要长,所以说synchronized
是java语言中一个重量级的操作。也是为什么早期的synchronized
效率低的原因。
所以,在JDK1.6中出现对锁进行了很多的优化,进而出现轻量级锁,偏向锁,锁消除,适应性自旋锁,锁粗化。
+参考文章:
+在JDK1.6之前,使用synchronized
被称作重量级锁,重量级锁的实现是基于底层操作系统的mutex
互斥原语的,这个开销是很大的。所以在JDK1.6时JVM对synchronized
做了优化。
对象头中markword
锁状态的表示:
+++
+- +
biased_lock
:0lock
: 01: 表示无锁状态- +
biased_lock
:1lock
: 01: 表示偏向锁状态- +
lock
: 00: 表示轻量级锁状态- +
lock
: 10: 表示重量级锁状态- +
lock
: 11: 表示被垃圾回收器标记的状态
对象的锁状态,可以分为4种,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。
+其中这几个锁只有重量级锁是需要使用操作系统底层mutex
互斥原语来实现,其他的锁都是使用对象头来实现的。
+随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁。
锁升级过程:
+markword
锁的标志位0,偏向锁的标志位为1;例如:刚被创建出来的对象;markword
的结构变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,直接可以获取锁。
+省去了大量有关锁申请的操作,从而也就提供程序的性能。MarkWord
中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。所以开销是很大;无锁状态升级为偏向锁:
+一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。
+偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将对象头中的 ThreadID
改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。
偏向锁升级为轻量级锁: +一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象的偏向状态; +这时表明在这个对象上已经存在竞争了,JVM会检查原来持有该对象锁的线程是否依然存活,如果不存活,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁。 +如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。
+轻量级锁升级为重量级锁: +轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 +但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。
+在所有的锁都启用的情况下线程进入临界区时会先去获取偏向锁,如果已经存在偏向锁了,则会尝试获取轻量级锁,启用自旋锁,如果自旋也没有获取到锁,则使用重量级锁,将没有获取到锁的线程阻塞挂起,直到持有锁的线程执行完同步块唤醒他们;
+偏向锁是在无锁争用的情况下使用的,也就是同步代码块在当前线程没有执行完之前,没有其它线程会执行该同步块,一旦有了第二个线程的争用,偏向锁就会升级为轻量级锁,如果轻量级锁自旋到达阈值后,没有获取到锁,就会升级为重量级锁;
+锁可以升级,但是不可以降级。
+++PS:有的观点认为 Java 不会进行锁降级。 +实际上,锁降级确实是会发生的,当 JVM 进入安全点(
+SafePoint
)的时候,会检查是否有闲置的Monitor
,然后试图进行降级。
在 HotSpot
虚拟机中是有锁降级的,但是仅仅只发生在 STW 的时候,只有垃圾回收线程能够观测到它,也就是说,在我们正常使用的过程中是不会发生锁降级的,只有在 GC 的时候才会降级。
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
+Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。
+不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。所以,就可能出现线程1改了某个变量的值,但是线程2不可见的情况。
+被synchronized
修饰的代码,在开始执行时会加锁,执行完成后会进行解锁。而为了保证可见性,有一条规则是这样的:对一个变量解锁之前,必须先把此变量同步回主存中。这样解锁后,后续线程就可以访问到被修改后的值。
所以,synchronized
关键字锁住的对象,其值是具有可见性的.
原子性是指一个操作是不可中断的,要全部执行完成,要不就都不执行。
+线程是CPU调度的基本单位。CPU有时间片的概念,会根据不同的调度算法进行线程调度。当一个线程获得时间片之后开始执行,在时间片耗尽之后,就会失去CPU使用权。所以在多线程场景下,由于时间片在线程间轮换,就会发生原子性问题。
+在Java中,为了保证原子性,提供了两个高级的字节码指令monitorenter
和monitorexit
。
+这两个字节码指令,在Java中对应的关键字就是synchronized
。
通过下monitorexit
和monitorexit
指令,可以保证被synchronized
修饰的代码在同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到。因此,在Java中可以使用synchronized
来保证方法和代码块内的操作是原子性的。
++例如: 线程1在执行
+monitorenter
指令的时候,会对Monitor进行加锁,加锁后其他线程无法获得锁,除非线程1主动解锁。 +即使在执行过程中,由于某种原因,比如CPU时间片用完,线程1放弃了CPU,但是,他并没有进行解锁。 +而由于synchronized
的锁是可重入的,下一个时间片还是只能被他自己获取到,还是会继续执行代码。直到所有代码执行完。这就保证了原子性。
有序性即程序执行的顺序按照代码的先后顺序执行。
+除了引入了时间片以外,由于处理器优化和指令重排等,CPU还可能对输入代码进行乱序执行,比如load->add->save
有可能被优化成load->save->add
这就是可能存在有序性问题。
这里需要注意的是,synchronized
是无法禁止指令重排和处理器优化的。也就是说,synchronized
无法避免上述提到的问题。
那么,为什么还说synchronized
也提供了有序性保证呢?
++如果在本线程内观察,所有操作都是天然有序的。如果在一个线程中观察另一个线程,所有操作都是无序的。
+
以上这句话也是,但是怎么理解呢?简单扩展一下,这其实和as-if-serial
语义有关。
as-if-serial
语义的意思指:不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果都不能被改变。
+编译器和处理器无论如何优化,都必须遵守as-if-serial
语义。
这里不对as-if-serial
语义详细展开了,简单说就是as-if-serial
语义保证了单线程中,指令重排是有一定的限制的,而只要编译器和处理器都遵守了这个语义,那么就可以认为单线程程序是按照顺序执行的。
+当然,实际上还是有重排的,只不过我们无须关心这种重排的干扰。
所以呢,由于synchronized
修饰的代码,同一时间只能被同一线程访问。那么也就是单线程执行的。所以,可以保证其有序性。
Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问;
+第一个是 JVM 实现的 synchronized
,而另一个是 JDK 实现的 ReentrantLock
。
比较 | +synchronized | +ReentrantLock | +
---|---|---|
锁的实现 | +JVM 实现,监视器模式 | +JDK实现,依赖AQS | +
性能 | +新版本 Java 对 synchronized 进行锁的升级 | +synchronized 与 ReentrantLock 大致相同 | +
等待可中断 | +不可中断 | +可中断 | +
公平锁 | +非公平锁 | +默认非公平锁,也可以是公平锁 | +
锁绑定多个条件 | +不能绑定 | +可以同时绑定多个 Condition 对象 | +
可重入 | +可重入锁 | +可重入锁 | +
释放锁 | +自动释放锁 | +调用 unlock() 释放锁 | +
等待唤醒 | +搭配wait()、notify或notifyAll()使用 | +搭配await()/singal()使用 | +
synchronized
与 ReentrantLock
最直观的区别就是,在使用ReentrantLock
的时候需要调用unlock
方法释放锁,所以为了保证一定释放,通常都是和 try~finally
配合使用的。
除非需要使用 ReentrantLock
的高级功能,否则优先使用 synchronized
。
+这是因为 synchronized
是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock
不是所有的 JDK 版本都支持。
+并且使用 synchronized
不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。
参考文章:
+ThreadLocal
文档注释:
++This class provides thread-local variables. These variables differ from +their normal counterparts in that each thread that accesses one (via its +{@code get} or {@code set} method) has its own, independently initialized +copy of the variable.
+
大意:这个类提供线程局部变量。这些变量与普通变量的不同之处在于,每个访问它们的线程(通过其get方法或set方法)都有自己的独立初始化的变量副本。
+如文档注释所说,ThraedLocal
为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
++从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。
+
说白了ThreadLocal
就是存放线程的局部变量的。
在JDK5.0中,ThreadLocal
已经支持泛型,该类的类名已经变为ThreadLocal<T>
。API方法也相应进行了调整,新版本的API方法分别是void set(T value)、T get()
以及T initialValue()
。
++关于Object和T的区别:Object是个基类,是个真实存在的类;T是个占位符,表示某个具体的类,仅在编译器有效,最终会被擦除用Object代替。
+
主要方法:
+// 返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。
+// 如果有人心急则吃不了热豆腐,在还没有set的情况下,调用get则返回null。
+protected T initialValue()
+
+// 该方法返回当前线程所对应的线程局部变量
+public T get()
+
+// 设置当前线程的线程局部变量的值
+public void set(T value)
+
+// 将当前线程局部变量的值删除,目的是为了减少内存的占用
+public void remove()
+
ThreadLocal
里设置的值,只有当前线程自己看得见:
public class MainTest {
+
+ private static ThreadLocal<Integer> localInt = new ThreadLocal<>();
+
+ public static void main(String[] args) {
+ localInt.set(100);
+
+ new Thread(() -> {
+ localInt.set(200);
+ System.out.println("-----thead1-----");
+ System.out.println(context0());
+ System.out.println(context1());
+ System.out.println(context2());
+ },"thread1").start();
+
+ System.out.println("-----main-----");
+ System.out.println(context0());
+ System.out.println(context1());
+ System.out.println(context2());
+ }
+
+
+ static int context0() {
+ return localInt.get();
+ }
+
+ static int context1(){
+ return localInt.get();
+ }
+
+ static int context2(){
+ return localInt.get();
+ }
+}
+
由于ThreadLocal
里设置的值,只有当前线程自己看得见,这意味着你不可能通过其他线程为它初始化值。
+为了弥补这一点,ThreadLocal
提供了一个withInitial()
方法统一初始化所有线程的ThreadLocal
的值:
public class MainTest {
+
+ private static final ThreadLocal<Integer> localInt = ThreadLocal.withInitial(() -> 300);
+
+ public static void main(String[] args) {
+
+ new Thread(() -> {
+ System.out.println("-----thead1-----");
+ System.out.println(context0());
+ System.out.println(context1());
+ System.out.println(context2());
+ },"thread1").start();
+
+ System.out.println("-----main-----");
+ System.out.println(context0());
+ System.out.println(context1());
+ System.out.println(context2());
+ }
+
+
+ static int context0() {
+ return localInt.get();
+ }
+
+ static int context1(){
+ return localInt.get();
+ }
+
+ static int context2(){
+ return localInt.get();
+ }
+
+}
+
通过上面的代码,可以发现ThreadLocal
是跨越几个方法的。为了在几个函数之间共用一个变量,所以才出现:线程变量,这种变量在Java中就是ThreadLocal
变量。
ThreadLocal
是跨函数的,虽然全局变量也是跨函数的,但是跨所有的函数,而且不是动态的。跨哪些函数是由线程来定的,所以更灵活。
总之,ThreadLocal
类是修饰变量的,是在控制它的作用域,是为了增加变量的种类而已,这才是ThreadLocal
类诞生的初衷,它的初衷可不是解决线程冲突的。
ThreadLocal
类是修饰变量的,重点是在控制变量的作用域,初衷可不是为了解决线程并发和线程冲突的,而是为了让变量的种类变的更多更丰富,方便人们使用罢了。
+很多开发语言在语言级别都提供这种作用域的变量类型。
++要保证线程安全,并不一定就是要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段。 +如果一个方法本来就不涉及共享数据,那它自然就无需任何同步措施去保证正确性。
+
总之,线程安全,并不一定就是要进行同步,ThreadLocal
目的是线程安全,但不是同步手段。
ThreadLocal
和线程同步机制都可以解决多线程中共享变量的访问冲突问题。
在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。
+而ThreadLocal
则从另一个角度来解决多线程的并发访问。ThreadLocal
会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。
+ThreadLocal
提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进 ThreadLocal
。
虽然ThreadLocal
能够保证多线程访问数据安全,但是由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比不使用 ThreadLocal
要大。
对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal
采用了“以空间换时间”的方式。
+前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
public T get() {
+ // 获取当前线程
+ Thread t = Thread.currentThread();
+ // 每个线程 都有一个自己的ThreadLocalMap,
+ // ThreadLocalMap里就保存着所有的ThreadLocal变量
+ ThreadLocalMap map = getMap(t);
+ if (map != null) {
+ //ThreadLocalMap的key就是当前ThreadLocal对象实例,
+ //多个ThreadLocal变量都是放在这个map中的
+ ThreadLocalMap.Entry e = map.getEntry(this);
+ if (e != null) {
+ @SuppressWarnings("unchecked")
+ //从map里取出来的值就是我们需要的这个ThreadLocal变量
+ T result = (T)e.value;
+ return result;
+ }
+ }
+ // 如果map没有初始化,那么在这里初始化一下
+ return setInitialValue();
+ }
+
+
+ public void set(T value) {
+ // 获取当前线程
+ Thread t = Thread.currentThread();
+ // 每个线程 都有一个自己的ThreadLocalMap
+ // ThreadLocalMap 里就保存着所有的ThreadLocal变量
+ ThreadLocalMap map = getMap(t);
+ if (map != null)
+ // 向map里添加值
+ map.set(this, value);
+ else
+ // map为null,创建一个 ThreadLocalMap
+ createMap(t, value);
+ }
+
+
+ // 全局定义的localMap
+ ThreadLocal.ThreadLocalMap threadLocals = null;
+
+ // 获取当前线程所持有的localMap
+ ThreadLocalMap getMap(Thread t) {
+ return t.threadLocals;
+ }
+
+ // 创建,初始化 localMap
+ void createMap(Thread t, T firstValue) {
+ t.threadLocals = new ThreadLocalMap(this, firstValue);
+ }
+
ThreadLocal
,get()、set()
源码中可以看出,所谓的ThreadLocal
变量就是保存在每个线程的map中的。这个map就是Thread
对象中的threadLocals
字段。
ThreadLocal.ThreadLocalMap threadLocals = null;
+
首先,在每个线程 Thread
内部有一个 ThreadLocal
.ThreadLocalMap
类型的成员变量 threadLocals
,这个 threadLocals
就是用来存储实际的变量副本的,键值为当前 ThreadLocal
变量,value为变量副本,即T类型的变量。
初始时,在Thread里面,threadLocals
为空,当通过ThreadLocal
变量调用get()
方法或者set()
方法,就会对Thread
类中的threadLocals
进行初始化,并且以当前ThreadLoca
变量为键值,以ThreadLocal
要保存的副本变量为value
,存到threadLocals
。
ThreadLocal.ThreadLocalMap
是一个比较特殊的Map,它的每个Entry的key
都是一个弱引用:
static class Entry extends WeakReference<ThreadLocal<?>> {
+ /** The value associated with this ThreadLocal. */
+ Object value;
+ //key就是一个弱引用
+ Entry(ThreadLocal<?> k, Object v) {
+ super(k);
+ value = v;
+ }
+}
+
这样设计的好处是,如果这个变量不再被其他对象使用时,可以自动回收这个ThreadLoca
l对象,避免可能的内存泄露。
虽然ThreadLocalMap
中的key是弱引用,当不存在外部强引用的时候,就会自动被回收,但是Entry
中的value
依然是强引用。这个value
的引用链条如下:
Thrad --> ThreadLocalMap --> Entry --> value
+
只有当Thread
被回收时,这个value
才有被回收的机会,否则,只要线程不退出,value
总是会存在一个强引用。
+但是,要求每个Thread都会退出,是一个极其苛刻的要求,对于线程池来说,大部分线程会一直存在在系统的整个生命周期内,那样的话,就会造成value对象出现泄漏的可能。
+处理的方法是,在ThreadLocalMap
进行set()
,get()
,remove()
的时候,都会进行清理:
以remove()
方法为例:
// public remove
+ public void remove() {
+ ThreadLocalMap m = getMap(Thread.currentThread());
+ if (m != null)
+ m.remove(this);
+ }
+
+// private remove
+private void remove(ThreadLocal<?> key) {
+ Entry[] tab = table;
+ int len = tab.length;
+ int i = key.threadLocalHashCode & (len-1);
+ for (Entry e = tab[i];
+ e != null;
+ e = tab[i = nextIndex(i, len)]) {
+ if (e.get() == key) {
+ e.clear();
+ expungeStaleEntry(i);
+ return;
+ }
+ }
+}
+// 核心方法
+private int expungeStaleEntry(int staleSlot) {
+ Entry[] tab = table;
+ int len = tab.length;
+
+ // expunge entry at staleSlot
+ tab[staleSlot].value = null;
+ tab[staleSlot] = null;
+ size--;
+
+ // Rehash until we encounter null
+ Entry e;
+ int i;
+ for (i = nextIndex(staleSlot, len);
+ (e = tab[i]) != null;
+ i = nextIndex(i, len)) {
+ ThreadLocal<?> k = e.get();
+ if (k == null) {
+ // 将 value 赋值为 null; help gc
+ e.value = null;
+ tab[i] = null;
+ size--;
+ } else {
+ int h = k.threadLocalHashCode & (len - 1);
+ if (h != i) {
+ tab[i] = null;
+
+ // Unlike Knuth 6.4 Algorithm R, we must scan until
+ // null because multiple entries could have been stale.
+ while (tab[h] != null)
+ h = nextIndex(h, len);
+ tab[h] = e;
+ }
+ }
+ }
+ return i;
+}
+
虽然ThreadLocal
为了避免内存泄露,花了一番大心思,但是并不能100%保证不发生内存泄漏。
比如,你的get()方法总是访问固定几个一直存在的ThreadLocal
,那么清理动作就不会执行,如果你没有机会调用set()
和remove()
,那么这个内存泄漏依然会发生。
+所以,当你不需要这个ThreadLoca
变量时,主动调用remove()
,这样是能够避免内存泄漏的。
线程不安全 | +线程不安全解决方案 | +
---|---|
ArrayList | +使用Vector、Collections.synchronizedArrayList、CopyOnWriteArrayList | +
HashSet | +使用Collections.synchronizedSet、CopyOnWriteArraySet | +
HashMap | +使用HashTable、Collections.synchronizedMap、ConcurrentHashMap | +
ArrayList
线程不安全代码演示
public class MainTest {
+ public static void main(String[] args) {
+ ArrayList<String> arrayList = new ArrayList<>();
+ for(int i=0; i< 10; i++) {
+ new Thread(() -> {
+ arrayList.add(UUID.randomUUID().toString());
+ System.out.println(arrayList);
+ },String.valueOf(i)).start();
+ }
+ }
+}
+
为避免偶然事件,请重复多试几次上面的代码,很大情况会出现ConcurrentModificationException
“同步修改异常”
java.util.ConcurrentModificationException
+
出现该异常的原因是,当某个线程正在执行 add()
方法时,被某个线程打断,添加到一半被打断,没有被添加完。
Vector
来代替 ArrayList
,Vector
是线程安全的 ArrayList
,但是由于,并发量太小,被淘汰;Collections.synchronizedArrayList()
来创建 ArrayList
;使用 Collections
工具类来创建 ArrayList
的思路是,在 ArrayList
的外边套了一个synchronized
外壳,来使 ArrayList
线程安全;CopyOnWriteArrayList()
来保证 ArrayList
线程安全;下面详细说明CopyOnWriteArrayList()
;使用CopyOnWriteArrayList
演示代码
public class MainTest {
+ public static void main(String[] args) {
+ CopyOnWriteArrayList<String> arrayList = new CopyOnWriteArrayList<>();
+ for(int i=0; i< 10; i++) {
+ new Thread(() -> {
+ arrayList.add(UUID.randomUUID().toString());
+ System.out.println(arrayList);
+ },String.valueOf(i)).start();
+ }
+ }
+}
+
CopyWriteArrayList
字面意思就是在写的时候复制,思想就是读写分离的思想。以下是 CopyOnWriteArrayList
的 add()
方法源码
/** The array, accessed only via getArray/setArray. */
+ private transient volatile Object[] array;
+
+/** The lock protecting all mutators */
+ final transient ReentrantLock lock = new ReentrantLock();
+
+ /**
+ * Gets the array. Non-private so as to also be accessible
+ * from CopyOnWriteArraySet class.
+ */
+ final Object[] getArray() {
+ return array;
+ }
+
+/**
+ * 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();
+ }
+ }
+
CopyWriteArrayList
之所以线程安全的原因是在源码里面使用 ReentrantLock
,所以保证了某个线程在写的时候不会被打断;
+可以看到源码开始先是复制了一份数组(因为同一时刻只有一个线程写,其余的线程会读),在复制的数组上边进行写操作,写好以后在返回 true
。
+这样写的就把读写进行了分离.写好以后因为 array
加了 volatile
关键字,所以该数组是对于其他的线程是可见的,就会读取到最新的值.
HashSet
和 ArrayList
类似,也是线程不安全的集合类。代码演示线程不安全示例,与ArrayList
类似
public class MainTest {
+ public static void main(String[] args) {
+ HashSet<String> set = new HashSet<>();
+ for(int i=0; i< 10; i++) {
+ new Thread(() -> {
+ set.add(UUID.randomUUID().toString());
+ System.out.println(set);
+ },String.valueOf(i)).start();
+ }
+ }
+}
+
也会报 java.util.ConcurrentModificationException
异常。
参照ArrayList
解决方案,HashSet
有两种解决方案:
Collections.synchronizedSet()
使用集合工具类解决;CopyOnWriteArraySet()
来保证集合线程安全;使用 CopyOnWriteArraySet()
代码演示
public class MainTest {
+ public static void main(String[] args) {
+ CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<>();
+ for(int i=0; i< 10; i++) {
+ new Thread(() -> {
+ set.add(UUID.randomUUID().toString());
+ System.out.println(set);
+ },String.valueOf(i)).start();
+ }
+ }
+}
+
CopyOnWriteArraySet
底层调用的就是CopyOnWriteArrayList
。
private final CopyOnWriteArrayList<E> al;
+/**
+ * Creates an empty set.
+ */
+public CopyOnWriteArraySet() {
+ al = new CopyOnWriteArrayList<E>();
+}
+
HashMap
也是线程不安全的集合类;
+在多线程环境下使用同样会出现java.util.ConcurrentModificationException
。
public class MainTest {
+ public static void main(String[] args) {
+ HashMap<String,Object> map = new HashMap<>();
+ for(int i=0; i< 10; i++) {
+ new Thread(() -> {
+ map.put(UUID.randomUUID().toString(),Thread.currentThread().getName());
+ System.out.println(map);
+ },String.valueOf(i)).start();
+ }
+ }
+}
+
再多线程环境下HashMap
不仅会出现ConcurrentModificationException
问题;
+更严重的是,当多个线程中的 HashMap
同时扩容时,再使用put方法添加元素,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,CPU飙升到100%。
解决方案:
+HashTable
来保证线程安全;Collections.synchronizedMap()
使用集合工具类;ConcurrentHashMap<>()
来保证线程安全;上面的HashTable
、Collections.synchronizedMap()
因为性能的原因,在多线程环境下很少使用,一般都会使用ConcurrentHashMap<>()
。
HashTable
性能低的原因,就是直接加了synchronized
修饰;
+当使用put方法时,通过hash算法判断应该分配到哪一个数组上,如果分配到同一个数组上,即发生hash冲突,这个时候加锁是没问题的;但是一旦不发生hash冲突,再去加锁,性能就不太好了。
可理解为HashTable
性能不好的原因就是锁的粒度太粗了。
HashTable
put方法源码
public synchronized V put(K key, V value) {
+ // Make sure the value is not null
+ if (value == null) {
+ throw new NullPointerException();
+ }
+
+ // Makes sure the key is not already in the hashtable.
+ Entry<?,?> tab[] = table;
+ int hash = key.hashCode();
+ int index = (hash & 0x7FFFFFFF) % tab.length;
+ @SuppressWarnings("unchecked")
+ Entry<K,V> entry = (Entry<K,V>)tab[index];
+ for(; entry != null ; entry = entry.next) {
+ if ((entry.hash == hash) && entry.key.equals(key)) {
+ V old = entry.value;
+ entry.value = value;
+ return old;
+ }
+ }
+
+ addEntry(hash, key, value, index);
+ return null;
+ }
+
ConcurrentHashMap
原理简单理解为:HashMap
+ 分段锁。
因为HashMap
在jdk1.7与jdk1.8结构上做了调整,所以ConcurrentHashMap
在jdk1.7与jdk1.8结构上也有所不同。
在阅读之前建议掌握HashMap
基本原理、CAS、synchronized
、lock以及对多线程并发有一定了解。
JDK1.7采用segment
的分段锁机制实现线程安全,其中segment
类继承自ReentrantLock
。用ReentrantLock
、CAS来保证线程安全。
jdk1.7的ConcurrentHashMap
结构:
segment
: 每一个segment
数组就相当于一个HashMap
;HashEntry
: 等同于HashMap
中Entry
,用于存放K,V键值对;ConcurrentHashMap
存放的值;jdk1.7ConcurrentHashMap
之所以能够保证线程安全,主要原因是在每个segment
数组上加了锁,俗称分段锁,细化了锁的粒度。
jdk1.7ConcurrentHashMap.put
方法源码
public V put(K key, V value) {
+ Segment<K,V> s;
+ if (value == null)
+ throw new NullPointerException();
+ int hash = hash(key.hashCode());
+ int j = (hash >>> segmentShift) & segmentMask;
+ if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
+ (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
+ s = ensureSegment(j);
+ return s.put(key, hash, value, false);
+ }
+
首先判空,计算hash值,计算put进来的元素分配到哪个segment
数组上,判断当前segments
数组上的元素是否为空,如果为空就会使用ensureSegment
方法创建segment
对象;
+最后调用Segment.put
方法,存放到对应的节点中。
Segment.ensureSegment
方法源码
/**
+ * Returns the segment for the given index, creating it and
+ * recording in segment table (via CAS) if not already present.
+ *
+ * @param k the index
+ * @return the segment
+ */
+private Segment<K,V> ensureSegment(int k) {
+ final Segment<K,V>[] ss = this.segments;
+ long u = (k << SSHIFT) + SBASE; // raw offset
+ Segment<K,V> seg;
+ if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
+ Segment<K,V> proto = ss[0]; // use segment 0 as prototype
+ int cap = proto.table.length;
+ float lf = proto.loadFactor;
+ int threshold = (int)(cap * lf);
+ HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
+ if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
+ == null) { // recheck
+ Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
+ while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
+ == null) {
+ if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
+ break;
+ }
+ }
+ }
+ return seg;
+ }
+
通过文档注释可以看到ensureSegment
方法作用
++返回指定索引的segment对象,通过CAS判断,如果还没有则创建它并记录在segment表中。
+
当多个线程同时执行该方法,同时通过ensureSegment
方法创建segment
对象时,只有一个线程能够创建成功;
+其中创建的新segment
对象中的加载因子、存放位置、扩容阈值与segment[0]
元素保持一致。这样做性能更高,因为不用在计算了。
为了保证线程安全,在ensureSegment
方法中用Unsafe
类中的一些方法做了三次判断,其中最后一次也就是该方法保证线程安全的关键,用到了CAS操作;
当多个线程并发执行下面的代码,先执行CAS的线程,判断segment
数组中某个位置是空的,然后就把这个线程自己创建的segment
数组赋值给seg,即seg = s
;然后break
跳出循环;
+后执行的线程会再次判断seg是否为空,因先执行的线程已经seg = s
不为空了,所以循环条件不成立,也就不再执行了。
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
+ == null) {
+ if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
+ break;
+}
+
Segment.put
方法源码;为了保证线程安全,执行put方法要保证要加到锁,如果没加到锁就会执行scanAndLockForPut
方法;
+这个方法就会保证一定要加到锁;
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
+ HashEntry<K,V> node = tryLock() ? null :
+ scanAndLockForPut(key, hash, value);
+ // ... 插入节点操作 最后释放锁
+}
+
scanAndLockForPut
方法的主要作用就是加锁,如果没有获取锁,就会一致遍历segment
数组,直到遍历到最后一个元素;
+每次遍历完都会尝试获取锁,如果还是获取不到锁,就会重试,最大次数为MAX_SCAN_RETRIES
在CPU多核下为64次,如果大于64次就会强制加锁。
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
+ HashEntry<K,V> first = entryForHash(this, hash);
+ HashEntry<K,V> e = first;
+ HashEntry<K,V> node = null;
+ int retries = -1; // negative while locating node
+ while (!tryLock()) {
+ HashEntry<K,V> f; // to recheck first below
+ if (retries < 0) {
+ if (e == null) {
+ if (node == null) // speculatively create node
+ node = new HashEntry<K,V>(hash, key, value, null);
+ retries = 0;
+ }
+ else if (key.equals(e.key))
+ retries = 0;
+ else
+ e = e.next;
+ }
+ else if (++retries > MAX_SCAN_RETRIES) {
+ lock();
+ break;
+ }
+ else if ((retries & 1) == 0 &&
+ (f = entryForHash(this, hash)) != first) {
+ e = first = f; // re-traverse if entry changed
+ retries = -1;
+ }
+ }
+ return node;
+}
+
+static final int MAX_SCAN_RETRIES =
+ Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
+
JDK1.8的实现已经摒弃了 Segment
的概念,而是直接用 Node数组+链表/红黑树
的数据结构来实现,并发控制使用 synchronized
和CAS来操作,整个看起来就像是优化过且线程安全的HashMap
;
+虽然在JDK1.8中还能看到 Segment
的数据结构,但是已经简化了属性,只是为了兼容旧版本。
JDK1.8中彻底放弃了Segment
转而采用的是Node
,其设计思想也不再是JDK1.7中的分段锁思想;
+JDK1.8版本的ConcurrentHashMap
的数据结构已经接近HashMap
,相对而言,ConcurrentHashMap
只是增加了同步操作来控制并发。
相关概念:
+sizeCtl
:默认为0,用来控制table
的初始化和扩容操作;用volatile
修饰,保证了其可见性;JDK1.8ConcurrentHashMap.put
方法源码;
final V putVal(K key, V value, boolean onlyIfAbsent) {
+ if (key == null || value == null) throw new NullPointerException();
+ int hash = spread(key.hashCode());
+ int binCount = 0;
+ for (Node<K,V>[] tab = table;;) {
+ Node<K,V> f; int n, i, fh;
+ if (tab == null || (n = tab.length) == 0)
+ tab = initTable();
+ else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
+ if (casTabAt(tab, i, null,
+ new Node<K,V>(hash, key, value, null)))
+ break; // no lock when adding to empty bin
+ }
+ else if ((fh = f.hash) == MOVED)
+ tab = helpTransfer(tab, f);
+ else {
+ V oldVal = null;
+ synchronized (f) {
+ if (tabAt(tab, i) == f) {
+ if (fh >= 0) {
+ binCount = 1;
+ for (Node<K,V> 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<K,V> pred = e;
+ if ((e = e.next) == null) {
+ pred.next = new Node<K,V>(hash, key,
+ value, null);
+ break;
+ }
+ }
+ }
+ else if (f instanceof TreeBin) {
+ Node<K,V> p;
+ binCount = 2;
+ if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
+ value)) != null) {
+ oldVal = p.val;
+ if (!onlyIfAbsent)
+ p.val = value;
+ }
+ }
+ }
+ }
+ if (binCount != 0) {
+ if (binCount >= TREEIFY_THRESHOLD)
+ treeifyBin(tab, i);
+ if (oldVal != null)
+ return oldVal;
+ break;
+ }
+ }
+ }
+ addCount(1L, binCount);
+ return null;
+}
+
首先调用Node.initTable()
方法,初始化table;sizeCtl
默认为0,如果ConcurrentHashMap
实例化时有传参数,sizeCtl
会是一个2的幂次方的值。
+所以执行第一次put方法时操作的线程会执行Unsafe.compareAndSwapInt
方法修改sizeCtl=-1
,只有一个线程能够修改成功,其它线程通过Thread.yield()
礼让线程让出CPU时间片,等待table
初始化完成。
private final Node<K,V>[] initTable() {
+ Node<K,V>[] tab; int sc;
+ while ((tab = table) == null || tab.length == 0) {
+ if ((sc = sizeCtl) < 0)
+ Thread.yield(); // lost initialization race; just spin
+ else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
+ try {
+ if ((tab = table) == null || tab.length == 0) {
+ int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
+ @SuppressWarnings("unchecked")
+ Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
+ table = tab = nt;
+ sc = n - (n >>> 2);
+ }
+ } finally {
+ sizeCtl = sc;
+ }
+ break;
+ }
+ }
+ return tab;
+}
+
调用put方法,通过hash算法计算,将要存放数组中的位置(n - 1) & hash
,如果该节点为空就通过CAS判断,创建一个Node放到该位置上。
int hash = spread(key.hashCode());
+
+// hash算法,计算存放在map中的位置;要保证尽可能的均匀分散,避免hash冲突
+static final int HASH_BITS = 0x7fffffff;
+static final int spread(int h) {
+ // 等同于: key.hashCode() ^ (key.hashCode() >>> 16) & 0x7fffffff
+ return (h ^ (h >>> 16)) & HASH_BITS;
+}
+
如果该位置不为空就会继续判断当前线程的ConcurrentHashMap
是否进行扩容
// MOVED = -1
+if ((fh = f.hash) == MOVED)
+tab = helpTransfer(tab, f);
+
插入之前,再次利用tabAt(tab, i) == f
判断,防止被其它线程修改;
+之后就会对这个将要添加到该位置的元素加锁,判断是链表还是树节点,做不同的操作;
f.hash >= 0
,说明f是链表结构的头结点,遍历链表,如果找到对应的node
节点,则修改value
,否则在链表尾部加入节点。TreeBin
类型节点,说明f是红黑树根节点,则在树结构上遍历元素,更新或增加节点。binCount >= TREEIFY_THRESHOLD(默认是8)
,则把链表转化为红黑树结构。V oldVal = null;
+synchronized (f) {
+ if (tabAt(tab, i) == f) {
+ if (fh >= 0) {
+ binCount = 1;
+ for (Node<K,V> 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<K,V> pred = e;
+ if ((e = e.next) == null) {
+ pred.next = new Node<K,V>(hash, key,
+ value, null);
+ break;
+ }
+ }
+ }
+ else if (f instanceof TreeBin) {
+ Node<K,V> p;
+ binCount = 2;
+ if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
+ value)) != null) {
+ oldVal = p.val;
+ if (!onlyIfAbsent)
+ p.val = value;
+ }
+ }
+ }
+}
+if (binCount != 0) {
+ if (binCount >= TREEIFY_THRESHOLD)
+ treeifyBin(tab, i);
+ if (oldVal != null)
+ return oldVal;
+ break;
+}
+
最后则进行扩容操作
+//相当于size++
+addCount(1L, binCount);
+
private final void addCount(long x, int check) {
+ CounterCell[] as; long b, s;
+ if ((as = counterCells) != null ||
+ !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
+ CounterCell a; long v; int m;
+ boolean uncontended = true;
+ if (as == null || (m = as.length - 1) < 0 ||
+ (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
+ !(uncontended =
+ U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
+ fullAddCount(x, uncontended);
+ return;
+ }
+ if (check <= 1)
+ return;
+ s = sumCount();
+ }
+ if (check >= 0) {
+ Node<K,V>[] tab, nt; int n, sc;
+ while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
+ (n = tab.length) < MAXIMUM_CAPACITY) {
+ int rs = resizeStamp(n);
+ if (sc < 0) {
+ if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
+ sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
+ transferIndex <= 0)
+ break;
+ if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
+ transfer(tab, nt);
+ }
+ else if (U.compareAndSwapInt(this, SIZECTL, sc,
+ (rs << RESIZE_STAMP_SHIFT) + 2))
+ transfer(tab, null);
+ s = sumCount();
+ }
+ }
+}
+
节点从table
移动到nextTable
,大体思想是遍历、复制的过程。
+通过Unsafe.compareAndSwapInt
修改sizeCtl
值,保证只有一个线程能够初始化nextTable
,扩容后的数组长度为原来的两倍,但是容量是原来的1.5。
tabAt
方法获得i位置的元素f,初始化一个forwardNode
实例fwd。f == null
,则在table
中的i位置放入fwd,这个过程是采用Unsafe.compareAndSwapObjectf
方法实现的,实现了节点的并发移动。nextTable
的i和i+n的位置上,移动完成,采用Unsafe.putObjectVolatile
方法给table
原位置赋值fwd。TreeBin
节点,也做一个反序处理,并判断是否需要untreeify
,把处理的结果分别放在nextTable的i和i+n的位置上,移动完成,同样采用Unsafe.putObjectVolatile
方法给table
原位置赋值fwd。+ +
+ + + + + +++面向对象是一种编程思想,包括三大特性和六大原则,其中,三大特性指的是封装、继承和多态;六大原则指的是单一职责原则、开放封闭原则、迪米特原则、里氏替换原则、依赖倒置原则以及接口隔离原则,其中,单一职责原则是指一个类应该是一组相关性很高的函数和数据的封装,这是为了提高程序的内聚性,而其他五个原则是通过抽象来实现的,目的是为了降低程序的耦合性以及提高可扩展性。
+
面向对象简称OO(object-oriented)是相对面向过程(procedure-oriented)来说的,是一种编程思想.Java就是一门面向对象的语言.
+面向对象编程简称OOP(Object-oriented programming),是将事务高度抽象化的编程模式. +面向对象编程是以功能来划分问题的,将问题分解成一个一个步骤,对每个步骤进行相应的抽象,形成对应对象,通过不同对象之间的调用,组合成某个功能解决问题.
+++PS: 面向过程编程简称POP(Procedural oriented programming),面向过程是以过程为中心的编程思想.是自顶而下的编程.
+
举个栗子: 下五子棋
+
+面向过程 {
+
+ 1.开始游戏
+
+ 2.黑子先走
+
+ 3.绘制画面
+
+ 4.判断输赢
+
+ 5.轮到白子
+
+ 6.绘制画面
+
+ 7.判断输赢
+
+ 8.返回到 黑子先走
+
+}
+
面向对象 {
+
+ 1.创建黑棋,白棋
+
+ 2.创建棋盘
+
+ 3.创建规则
+
+ 4.赋予每个对象相关属性和指定行为
+
+ 5.各个功能之间互相调用
+}
+
面向对象是模型化的,你只需抽象出几个类,进行封装成各个功能,通过不同对象之间的调用来解决问题.而面向过程需要把问题分解为几个步骤,每个步骤用对应的函数调用即可.面向过程是具体化的,流程化的,解决一个问题,需要你一步一步的分析,一步一步的实现.
+面向对象的底层其实还是面向过程,把面向过程抽象成类,然后进行封装,方便我们我们使用,就是面向对象了.
+简而言之,用面向过程的方法写出来的程序是一份蛋炒饭,而用面向对象写出来的程序是一份盖浇饭(就是在一碗白米饭上面浇上一份盖菜,你喜欢什么菜,你就浇上什么菜). +通过例子可以看出面向对象更重视不重复造轮子,即创建一次,重复使用.
+面向对象
++++
+- +
+优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护。
+- +
+缺点:性能比面向过程低
+
面向过程
++++
+- 优点:流程化使得编程任务明确,在开发之前基本考虑了实现方式和最终结果,具体步骤清楚,便于节点分析; 效率高,面向过程强调代码的短小精悍,善于结合数据结构来开发高效率的程序。
+- 缺点:没有面向对象易维护、易复用、易扩展
+
抽象会使复杂的问题更加简单化,面向对象更符合人类的思维,而面向过程则是机器的思想.
+设计原则的目的是为了让程序达到高内聚、低耦合,提高可扩展性的目的,其实现手段是面向对象的三大特性:封装、继承以及多态。
+设计原则名称 | +核心思想 | +
---|---|
单一职责原则 | +一个类只负责一个功能领域中的相应职责 | +
开放封闭原则 | +软件实体应对扩展开放,而对修改关闭 | +
依赖倒转原则 | +抽象不应该依赖于细节,细节应该依赖于抽象 | +
里氏替换原则 | +所有引用基类对象的地方能够透明地使用其子类的对象 | +
接口隔离原则 | +使用多个专门的接口,而不使用单一的总接口 | +
合成复用原则 | +尽量使用对象组合,而不是继承来达到复用的目的 | +
迪米特法则 | +一个软件实体应当尽可能少地与其他实体发生相互作用 | +
++其核心思想为:一个类,最好只做一件事,只有一个引起它的变化。单一职责原则可降低类的复杂度,提高代码可读性,可维护性,降低变更风险. 单一职责原则可以看做是低耦合、高内聚在面向对象原则上的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。职责过多,可能引起它变化的原因就越多,这将导致职责依赖,相互之间就产生影响,从而大大损伤其内聚性和耦合度。通常意义下的单一职责,就是指只有一种单一功能,不要为类实现过多的功能点,以保证实体只有一个引起它变化的原因。 专注,是一个人优良的品质;同样的,单一也是一个类的优良设计。交杂不清的职责将使得代码看起来特别别扭牵一发而动全身,有失美感和必然导致丑陋的系统错误风险。
+
代码实现
+public class MainTest {
+ public static void main(String[] args) {
+ Vehicle vehicle = new Vehicle();
+ vehicle.running("汽车");
+ // 飞机不是在路上行驶
+ vehicle.running("飞机");
+ }
+}
+
+/**
+ * 在run方法中违反了单一职责原则
+ * 解决方法根据不同的交通工具,分解成不同的类即可
+ */
+class Vehicle{
+ public void running(String name) {
+ System.out.println(name + "在路上行驶 ....");
+ }
+}
+
// 解决
+public class MainTest {
+ public static void main(String[] args) {
+ Driving driving = new Driving();
+ driving.running("汽车");
+ Flight flight = new Flight();
+ flight.running("飞机");
+ }
+}
+
+class Driving {
+ public void running(String name) {
+ System.out.println(name + "在路上行驶 ....");
+ }
+}
+
+class Flight {
+ public void running(String name) {
+ System.out.println(name + "在空中飞行 ....");
+ }
+}
+
通常情况下,我们应当遵循单一职责原则,只要逻辑足够简单,才可以在代码里边违反单一职责原则;只要类中方法数量足够少,可以在方法级别保持单一职责原则.
+public class MainTest {
+ public static void main(String[] args) {
+ Vehicle2 vehicle2 = new Vehicle2();
+ vehicle2.driving("汽车");
+ vehicle2.flight("飞机");
+ }
+}
+/*
+ * 改进
+ *↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
+ */
+
+class Vehicle2 {
+ public void driving(String name) {
+ System.out.println(name + "在路上行驶 ....");
+ }
+ public void flight(String name) {
+ System.out.println(name + "在空中飞行 ....");
+ }
+}
+
++软件实体应该是可扩展的,而不可修改的。也就是,对(提供方)扩展开放,对(使用方)修改封闭的。 开放封闭原则主要体现在两个方面
++
+- 对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
+- 对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对其进行任何尝试的修改。
+实现开放封闭原则的核心思想就是对抽象编程,而不对具体编程,因为抽象相对稳定。让类依赖于固定的抽象,所以修改就是封闭的;而通过面向对象的继承和多态机制,又可以实现对抽象类的继承,通过覆写其方法来改变固有行为,实现新的拓展方法,所以就是开放的。 “需求总是变化”没有不变的软件,所以就需要用封闭开放原则来封闭变化满足需求,同时还能保持软件内部的封装体系稳定,不被需求的变化影响。编程中遵循其他原则,以及使用其他设计模式的目的就是为了遵循开闭原则.
+
当软件需要变化时,尽量使用扩展的软件实体的方式行为来实现变化,而不是通过修改已有的代码来实现变化.
+代码实现
+
+public class MainTest {
+ public static void main(String[] args) {
+ Mother mother = new Mother();
+
+ Son son = new Son();
+ Daughter daughter = new Daughter();
+
+ // 注入子类对象 如果扩展需要其他类 换成其他对象即可
+ mother.setAbstractFather(son);
+ mother.display();
+ }
+}
+
+abstract class AbstractFather {
+
+ protected abstract void display();
+
+}
+class Son extends AbstractFather{
+ @Override
+ protected void display() {
+ System.out.println("son class ...");
+ }
+}
+class Daughter extends AbstractFather{
+
+ @Override
+ protected void display() {
+ System.out.println("daughter class ...");
+ }
+}
+
+class Mother {
+
+ private AbstractFather abstractFather;
+
+ public void setAbstractFather(AbstractFather abstractFather) {
+ this.abstractFather = abstractFather;
+ }
+
+ public void display() {
+ abstractFather.display();
+ }
+
+}
+
++该原则依赖于抽象。具体而言就是高层模块不依赖于底层模块,二者都同依赖于抽象;抽象不依赖于具体,具体依赖于抽象。 我们知道,依赖一定会存在于类与类、模块与模块之间。当两个模块之间存在紧密的耦合关系时,最好的方法就是分离接口和实现:在依赖之间定义一个抽象的接口使得高层模块调用接口,而底层模块实现接口的定义,以此来有效控制耦合关系,达到依赖于抽象的设计目标。 抽象的稳定性决定了系统的稳定性,因为抽象是不变的,依赖于抽象是面向对象设计的精髓,也是依赖倒置原则的核心。 依赖于抽象是一个通用的原则,而某些时候依赖于细节则是在所难免的,必须权衡在抽象和具体之间的取舍,方法不是一层不变的。依赖于抽象,就是对接口编程,不要对实现编程。
+
代码实现
+
+public class MainTest {
+
+ public static void main(String[] args) {
+ Computer computer = new Computer();
+ // 对接口编程,不要对实现编程
+ // 如果没有接口 则代码很难实现扩展
+ Disk disk = new CustomDisk();
+ Memory memory = new CustomMemory();
+
+ computer.setDisk(disk);
+ computer.setMemory(memory);
+ computer.run();
+ }
+
+}
+
+interface Disk{
+ void diskMethod();
+}
+
+interface Memory{
+ void memoryMethod();
+}
+
+class CustomDisk implements Disk{
+
+ @Override
+ public void diskMethod() {
+ System.out.println("i am disk ...");
+ }
+}
+
+class CustomMemory implements Memory{
+
+ @Override
+ public void memoryMethod() {
+ System.out.println("i am memory ...");
+ }
+}
+
+class Computer {
+
+ private Memory memory;
+
+ private Disk disk;
+
+ public void setDisk(Disk disk) {
+ this.disk = disk;
+ }
+
+ public void setMemory(Memory memory) {
+ this.memory = memory;
+ }
+
+ public Disk getDisk() {
+ return disk;
+ }
+
+ public Memory getMemory() {
+ return memory;
+ }
+ public void run() {
+ System.out.println(" computer is running ...");
+ memory.memoryMethod();
+ disk.diskMethod();
+ }
+}
+
++使用多个小的专门的接口,而不要使用一个大的总接口。 具体而言,接口隔离原则体现在:接口应该是内聚的,应该避免“胖”接口。一个类对另外一个类的依赖应该建立在最小的接口上,不要强迫依赖不用的方法,这是一种接口污染。 接口有效地将细节和抽象隔离,体现了对抽象编程的一切好处,接口隔离强调接口的单一性。而胖接口存在明显的弊端,会导致实现的类型必须完全实现接口的所有方法、属性等;而某些时候,实现类型并非需要所有的接口定义,在设计上这是“浪费”,而且在实施上这会带来潜在的问题,对胖接口的修改将导致一连串的客户端程序需要修改,有时候这是一种灾难。在这种情况下,将胖接口分解为多个特点的定制化方法,使得客户端仅仅依赖于它们的实际调用的方法,从而解除了客户端不会依赖于它们不用的方法。
+分离的手段主要有以下两种:
++
+- 委托分离,通过增加一个新的类型来委托客户的请求,隔离客户和接口的直接依赖,但是会增加系统的开销。
+- 多重继承分离,通过接口多继承来实现客户的需求,这种方式是较好的。
+
代码实现
+public class MainTest {
+ public static void main(String[] args) {
+ FuncImpl func = new FuncImpl();
+ func.func1();
+ func.func2();
+ func.func3();
+ }
+}
+
+interface Function1{
+ void func1();
+ // 如果将接口中的方法都写在一个接口就会造成实现该接口就要重写该接口所有方法。
+ // 当然Java 8 接口可以有实现,降低了维护成本,解了决该问题;
+ // 但是我们还是应当遵循该原则,使得接口看起来更加清晰
+ // void func2();
+ // void func3();
+}
+interface Function2 {
+ void func2();
+}
+interface Function3 {
+ void func3();
+}
+
+class FuncImpl implements Function1,Function2,Function3{
+
+ @Override
+ public void func1() {
+ System.out.println("i am function1 impl");
+ }
+
+ @Override
+ public void func2() {
+ System.out.println("i am function2 impl");
+ }
+
+ @Override
+ public void func3() {
+ System.out.println("i am function3 impl");
+ }
+}
+
++子类必须能够替换其基类。 这一思想体现为对继承机制的约束规范,只有子类能够替换基类时,才能保证系统在运行期内识别子类,这是保证继承复用的基础。在父类和子类的具体行为中,必须严格把握继承层次中的关系和特征,将基类替换为子类,程序的行为不会发生任何变化。同时,这一约束反过来则是不成立的,子类可以替换基类,但是基类不一定能替换子类。 里氏替换原则,主要着眼于对抽象和多态建立在继承的基础上,因此只有遵循了里氏替换原则,才能保证继承复用是可靠地。
+实现的方法是面向接口编程:将公共部分抽象为基类接口或抽象类,通过
+Extract Abstract Class
,在子类中通过覆写父类的方法实现新的方式支持同样的职责。 里氏替换原则是关于继承机制的设计原则,违反了里氏替换原则就必然导致违反开放封闭原则。 里氏替换原则能够保证系统具有良好的拓展性,同时实现基于多态的抽象机制,能够减少代码冗余,避免运行期的类型判别。
简单来说就是子类可以扩展父类的功能,但是尽量不要重写父类的功能.如果通过重写父类方法来完成新的功能,这样写起来虽然简单,但整个体系的可复用性会非常差,特别是运用多态比较频繁时,程序运行出错的概率会非常大.
+代码实现
+
+public class MainTest {
+ public static void main(String[] args) {
+ Rectangle rectangle = new Rectangle();
+ rectangle.setWidth(20);
+ rectangle.setHeight(10);
+ resize(rectangle);
+ print(rectangle);
+ System.out.println("=======================");
+ // 因为 Square类 重写了父类set的方法导致调用时出错
+ Rectangle square = new Square();
+ square.setWidth(10);
+ resize(square);
+ print(square);
+ }
+ public static void resize(Rectangle rectangle){
+ while (rectangle.getWidth() >= rectangle.getHeight()){
+ rectangle.setHeight(rectangle.getHeight() + 1);
+ }
+ }
+
+ public static void print(Rectangle rectangle){
+ System.out.println(rectangle.getWidth());
+ System.out.println(rectangle.getHeight());
+ }
+}
+
+class Square extends Rectangle{
+ private Integer width;
+ private Integer height;
+
+ @Override
+ public void setWidth(Integer width) {
+ super.setWidth(width);
+ super.setHeight(width);
+ }
+
+ @Override
+ public void setHeight(Integer height) {
+ super.setWidth(height);
+ super.setHeight(height);
+ }
+}
+class Rectangle {
+ private Integer width;
+ private Integer height;
+
+ public void setWidth(Integer width) {
+ this.width = width;
+ }
+
+ public void setHeight(Integer height) {
+ this.height = height;
+ }
+
+ public Integer getWidth() {
+ return width;
+ }
+
+ public Integer getHeight() {
+ return height;
+ }
+}
+
++尽量使用对象组合,而不是继承来达到复用的目的。 在面向对象设计中,可以通过两种方法在不同的环境中复用已有的设计和实现,即通过组合/聚合关系或通过继承,但首先应该考虑使用组合/聚合,组合/聚合可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少;其次才考虑继承,在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。
+
代码实现
+详见继承与组合
+++迪米特法则又叫最少知识原则,就是说一个对象应当对其他对象有尽可能少的了解。 +其核心思想是: 降低类之间的耦合.如果类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大.一个对象应该对其他对象有最少的了解。 通俗地讲,一个类应该对自己需要耦合或调用的类知道得最少,你(被耦合或调用的类)的内部是如何复杂都和我没关系,那是你的事情,我就知道你提供的
+public
方法,我就调用这么多,其他的一概不关心.迪米特法则其根本思想,是强调了类之间的松耦合。类之间的耦合越弱,越有利于复用,一个处在弱耦合的类被修改,不会对有关系的类造成搏击,也就是说,信息的隐藏促进了软件的复用。
迪米特法则还有个更简单的定义:只与直接的朋友通信
+朋友定义:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。 耦合的方式很多,依赖,关联,组合,聚合等。其中,我们称出现成员变量,方法参数,方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友。也就是说,陌生的类最好不要以局部变量的形式出现在类的内部。
+代码实现
+public class MainTest {
+ public static void main(String[] args) {
+ //创建了一个 SchoolManager 对象
+ SchoolManager schoolManager = new SchoolManager();
+ // SchoolManager直接朋友: CollegeManager (方法参数) Employee(返回值)
+ // CollegeEmployee以局部变量的形式出现在SchoolManager类中 所以违反了迪米特法则
+ schoolManager.printAllEmployee(new CollegeManager());
+ }
+}
+
+
+class Employee {
+ private String id;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+}
+
+
+class CollegeEmployee {
+ private String id;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+}
+
+class CollegeManager {
+ public List<CollegeEmployee> getAllEmployee() {
+ List<CollegeEmployee> list = new ArrayList<>();
+ for (int i = 0; i < 10; i++) {
+ CollegeEmployee emp = new CollegeEmployee();
+ emp.setId("学院员工 id= " + i);
+ list.add(emp);
+ }
+ return list;
+ }
+}
+
+class SchoolManager {
+
+ public List<Employee> getAllEmployee() {
+ List<Employee> list = new ArrayList<>();
+
+ for (int i = 0; i < 5; i++) {
+ //这里我们增加了 5 个员工到
+ Employee emp = new Employee();
+ emp.setId("学校总部员工 id= " + i);
+ list.add(emp);
+ }
+ return list;
+ }
+
+ void printAllEmployee(CollegeManager sub) {
+ //获取到学院员工
+ List<CollegeEmployee> list1 = sub.getAllEmployee();
+ System.out.println("------------学院员工------------");
+ for (CollegeEmployee e : list1) {
+ System.out.println(e.getId());
+ }
+ //获取到学校总部员工
+ List<Employee> list2 = this.getAllEmployee();
+ System.out.println("------------学校总部员工------------");
+ for (Employee e : list2) {
+ System.out.println(e.getId());
+ }
+ }
+}
+
+class CollegeManager {
+ public List<CollegeEmployee> getAllEmployee() {
+ List<CollegeEmployee> list = new ArrayList<>();
+ for (int i = 0; i < 10; i++) {
+ CollegeEmployee emp = new CollegeEmployee();
+ emp.setId("学院员工 id= " + i);
+ list.add(emp);
+ }
+ return list;
+ }
+ // 修改后
+ public void printAllEmployee() {
+ List<CollegeEmployee> list1 = this.getAllEmployee();
+ System.out.println("------------学院员工------------");
+ for (CollegeEmployee e : list1) {
+ System.out.println(e.getId());
+ }
+ }
+}
+
+class SchoolManager {
+
+ public List<Employee> getAllEmployee() {
+ List<Employee> list = new ArrayList<>();
+
+ for (int i = 0; i < 5; i++) {
+ //这里我们增加了 5 个员工到
+ Employee emp = new Employee();
+ emp.setId("学校总部员工 id= " + i);
+ list.add(emp);
+ }
+ return list;
+ }
+
+ void printAllEmployee(CollegeManager sub) {
+ //获取到学院员工
+ // 修改后
+ sub.printAllEmployee();
+
+ //获取到学校总部员工
+ List<Employee> list2 = this.getAllEmployee();
+ System.out.println("------------学校总部员工------------");
+ for (Employee e : list2) {
+ System.out.println(e.getId());
+ }
+ }
+}
+
封装是面向对象方法的重要原则,就是把对象的属性和操作(或服务)结合为一个独立的整体,并尽可能隐藏对象的内部实现细节.简单的说,一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分。
+封装的目的是增强安全性和简化编程,使用者不必了解具体的实现细节,而只是要通过外部接口,以特定的访问权限来使用类的成员.
+Java的封装可以通过修改属性的可见性限制对属性的访问来体现.
+Java 中有三个访问权限修饰符:private、protected 以及 public,如果不加访问修饰符,表示包级可见(default)。
+修饰符 | +当前类 | +同一包下 | +其他包的子类 | +不同包的子类 | +其他包 | +
---|---|---|---|---|---|
public | +Y | +Y | +Y | +Y | +Y | +
protected | +Y | +Y | +Y | +Y/N | +N | +
default | +Y | +Y | +Y | +N | +N | +
private | +Y | +N | +N | +N | +N | +
这四种访问权限的控制符能够控制类中成员的可见性.当然需要满足在不使用Java反射的情况下.
+注意
+protected
用于修饰成员,表示在继承体系中成员对于子类可见.如果不存在继承关系则不能访问protected
修饰的实例.设计良好的模块会隐藏所有的实现细节,把它的 API 与它的实现清晰地隔离开来。模块之间只通过它们的 API 进行通信,一个模块不需要知道其他模块的内部工作情况,这个概念被称为信息隐藏或封装。因此访问权限应当尽可能地使每个类或者成员不被外界访问。
+如果子类的方法重写了父类的方法,那么子类中该方法的访问级别不允许低于父类的访问级别。这是为了确保可以使用父类实例的地方都可以使用子类实例,也就是确保满足里氏替换原则。
+某个类的字段决不能是公有的,因为这么做的话就失去了对这个字段修改行为的控制,其他类可以对其随意修改。
+例如下面的例子中,AccessExample
拥有id公有字段,如果在某个时刻,我们想要使用int
存储id
字段,那么就需要修改所有类中的代码。
public class AccessExample {
+ public String id;
+ // public int id;
+}
+
可以使用公有的 getter
和 setter
方法来替换公有字段,这样的话就可以控制对字段的修改行为。实现了封装
public class AccessExample {
+
+ private int id;
+
+ public String getId() {
+ return id + "";
+ }
+
+ public void setId(String id) {
+ this.id = Integer.valueOf(id);
+ }
+}
+
但是也有例外,如果是包级私有的类或者私有的嵌套类,那么直接暴露成员不会有特别大的影响。
+public class AccessWithInnerClassExample {
+
+ private class InnerClass {
+ int x;
+ }
+
+ private InnerClass innerClass;
+
+ public AccessWithInnerClassExample() {
+ innerClass = new InnerClass();
+ }
+
+ public int getValue() {
+ return innerClass.x; // 直接访问
+ }
+}
+
继承可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类”。继承的过程,就是从一般到特殊的过程.要实现继承,可以通过“继承”(Inheritance)和“组合”(Composition)来实现. 继承概念的实现方式有两类:实现继承与接口继承。实现继承是指直接使用基类的属性和方法而无需额外编码的能力;接口继承是指仅使用属性和方法的名称、但是子类必须提供实现的能力.
+++继承: 如果多个类的某个部分的功能相同,那么可以抽象出一个类来,把相同的部分放到父类中,让他们继承这个类
+实现:如果多个类处理的目标是一样的,但是处理的方法方式不同,那么就定义一个接口,也就是一个标准,让他们的实现这个接口,各自实现自己具体的处理方法来处理那个目标
+
继承的根本原因是因为要复用,而实现的根本原因是需要定义一个标准.
+继承是实现复用代码的重要手段,但是继承会破坏封装.组合也是代码复用的重要方式,可以提供良好的封装性.
+// 继承
+
+public class MainTest {
+
+ public static void main(String[] args) {
+ B b = new B();
+ b.test();
+ }
+}
+
+class A {
+
+ protected int i;
+
+ protected void test() {
+ System.out.println("I am super class ... ");
+ }
+
+}
+
+class B extends A{
+
+ // 调用父类成员
+ public void t() {
+ System.out.println(i);
+ }
+
+}
+
通过以上代码,可以发现,子类可以访问父类的成员变量方法,并且通过重写可以改变父类方法实现.从而破坏了封装性.
+++在继承结构中,父类的内部细节对于子类是可见的。所以我们通常也可以说通过继承的代码复用是一种白盒式代码复用。(如果基类的实现发生改变,那么派生类的实现也将随之改变。这样就导致了子类行为的不可预知性;)
+
为了保证父类有良好的封装性,不会对子类随意更改,设计父类时应遵循以下原则:
++++
+- 尽量隐藏父类的内部数据.尽量把所有父类的所有成员变量都用
+private
修饰,不要让子类直接访问父类的成员.- 不要让子类随意的修改访问父类的方法.父类中那些仅为辅助其他的工具方法,应该使用private修饰,让子类无法访问该方法;如果父类中的方法需要被外部类调用,则需以public修饰,但又不希望重写父类方法可以使用final来修饰方法;但如果希望父类某个方法被重写,但又不希望其他类访问自由,可以使用protected修饰.
+- 尽量不要在父类构造器中调用将要被子类重写的方法.
+
继承是类与类或者接口与接口之间最常见的关系,继承是一种is-a
关系。
组合是把旧类对象作为新类对象的成员变量组合进来,用以实现新类的功能.
+
+public class MainTest {
+
+ public static void main(String[] args) {
+ B b = new B(new A());
+ b.test();
+ }
+}
+
+class A {
+
+ protected int i;
+
+ protected void test() {
+ System.out.println("I am super class ... ");
+ }
+
+}
+
+class B {
+
+ private final A a;
+
+ public B(A a) {
+ this.a = a;
+ }
+ public void test() {
+ // 复用 A 类提供的 test 方法
+ a.test();
+ }
+}
+
++组合是通过对现有的对象进行拼装(组合)产生新的、更复杂的功能。因为在对象之间,各自的内部细节是不可见的,所以我们也说这种方式的代码复用是黑盒式代码复用。(因为组合中一般都定义一个类型,所以在编译期根本不知道具体会调用哪个实现类的方法)
+
组合强调的是整体与部分、拥有的关系,即has-a
的关系.
++继承,在写代码的时候就要指名具体继承哪个类,所以,在编译期就确定了关系。(从基类继承来的实现是无法在运行期动态改变的,因此降低了应用的灵活性。)
+
++组合,在写代码的时候可以采用面向接口编程。所以,类的组合关系一般在运行期确定。
+
组合 | +继承 | +
---|---|
优点:不破坏封装,整体类与局部类之间松耦合,彼此相对独立 | +缺点:破坏封装,子类与父类之间紧密耦合,子类依赖于父类的实现,子类缺乏独立性 | +
优点:具有较好的可扩展性 | +缺点:支持扩展,但是往往以增加系统结构的复杂度为代价 | +
优点:支持动态组合。在运行时,整体对象可以选择不同类型的局部对象 | +缺点:不支持动态继承。在运行时,子类无法选择不同的父类 | +
优点:整体类可以对局部类进行包装,封装局部类的接口,提供新的接口 | +缺点:子类不能改变父类的接口 | +
缺点:整体类不能自动获得和局部类同样的接口 | +优点:子类能自动继承父类的接口 | +
缺点:创建整体类的对象时,需要创建所有局部类的对象 | +优点:创建子类的对象时,无须创建父类的对象 | +
经过以上比较,可以得出结论: 组合比继承更加灵活.所以在写代码如果这个功能组合和继承都能够完成,那么应该优先选择组合. +但是继承在一些场景还是要优先于组合的.
++++
+- 继承要慎用,其使用场合仅限于你确信使用该技术有效的情况。一个判断方法是,问一问自己是否需要从新类向基类进行向上转型。如果是必须的,则继承是必要的。反之则应该好好考虑是否需要继承。
+- 只有当子类真正是超类的子类型时,才适合用继承。换句话说,对于两个类A和B,只有当两者之间确实存在is-a关系的时候,类B才应该继承类A。
+
super()
函数访问父类的构造函数,从而委托父类完成一些初始化的工作。应该注意到,子类一定会调用父类的构造函数来完成初始化工作,一般是调用父类的默认构造函数,如果子类需要调用父类其它构造函数,那么就可以使用super函数。super
关键字来引用父类的方法实现。public class SuperExample {
+
+ protected int x;
+ protected int y;
+
+ public SuperExample(int x, int y) {
+ this.x = x;
+ this.y = y;
+ }
+
+ public void func() {
+ System.out.println("SuperExample.func()");
+ }
+}
+public class SuperExtendExample extends SuperExample {
+
+ private int z;
+
+ public SuperExtendExample(int x, int y, int z) {
+ super(x, y);
+ this.z = z;
+ }
+
+ @Override
+ public void func() {
+ super.func();
+ System.out.println("SuperExtendExample.func()");
+ }
+}
+SuperExample e = new SuperExtendExample(1, 2, 3);
+e.func();
+SuperExample.func()
+SuperExtendExample.func()
+
抽象类和接口也是Java继承体系中的重要组成部分.抽象类是用来捕捉子类的通用特性的,而接口则是抽象方法的集合;抽象类不能被实例化,只能被用作子类的超类,是被用来创建继承层级里子类的模板,而接口只是一种形式,接口自身不能做任何事情。
+抽象类和抽象方法都使用abstract
关键字进行声明。如果一个类中包含抽象方法,那么这个类必须声明为抽象类。
抽象类和普通类最大的区别是,抽象类不能被实例化,需要继承抽象类才能实例化其子类。
+public abstract class AbstractClassExample {
+
+ protected int x;
+ private int y;
+
+ public abstract void func1();
+
+ public void func2() {
+ System.out.println("func2");
+ }
+}
+public class AbstractExtendClassExample extends AbstractClassExample {
+ @Override
+ public void func1() {
+ System.out.println("func1");
+ }
+}
+
+// 实例化抽象类
+// AbstractClassExample ac1 = new AbstractClassExample();
+// 实例化抽象类子类
+// AbstractClassExample ac2 = new AbstractExtendClassExample();
+// ac2.func1();
+
接口是抽象类的延伸,在 Java 8 之前,它可以看成是一个完全抽象的类,也就是说它不能有任何的方法实现。
+从 Java 8 开始,接口也可以拥有默认的方法实现,这是因为不支持默认方法的接口的维护成本太高了。在 Java 8 之前,如果一个接口想要添加新的方法,那么要修改所有实现了该接口的类,现在不用修改所有实现该接口的类.
+接口的成员(字段 + 方法)默认都是public
的,并且不允许定义为private
或者 protected
。
接口的字段默认都是用static
和final
修饰的.
public interface InterfaceExample {
+
+ void func1();
+
+ default void func2(){
+ System.out.println("func2");
+ }
+
+ int x = 123;
+ // int y; // Variable 'y' might not have been initialized
+ public int z = 0; // Modifier 'public' is redundant for interface fields
+ // private int k = 0; // Modifier 'private' not allowed here
+ // protected int l = 0; // Modifier 'protected' not allowed here
+ // private void fun3(); // Modifier 'private' not allowed here
+}
+public class InterfaceImplementExample implements InterfaceExample {
+ @Override
+ public void func1() {
+ System.out.println("func1");
+ }
+}
+
+// InterfaceExample ie1 = new InterfaceExample(); // 'InterfaceExample' is abstract; cannot be instantiated
+InterfaceExample ie2 = new InterfaceImplementExample();
+ie2.func1();//func1
+System.out.println(InterfaceExample.x);//123
+
Java的接口可以多继承
+interface Action extends Serializable,AutoCloseable {
+ // to do ...
+}
+
从设计层面上看,抽象类提供了一种is-a
关系,那么就必须满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 like-a
关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 is-a
关系。
+抽象类是一种自下而上的思想,而接口是一种自上而下的思想。抽象类是将多个类的公共特点聚合到同一个类中,然后实现父类的方法;接口更像是对类的一种约束,其他类调用实现某接口的类。
从语法角度来看,一个类可以实现多个接口,但是不能继承多个抽象类;接口的字段只能是static
和final
类型的,而抽象类的字段没有这种限制,接口的成员只能是public
的,而抽象类的成员可以有多种访问权限。
使用接口
+Compareable
接口中的 compareTo()
方法;使用抽象类
+public
。在很多情况下,接口优先于抽象类。因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。
+多态即多种表现形态;同一个行为具有多个不同表现形式或形态的能力.
+多态存在的前提
+Parent p = new Child();
简单说来: 父类引用指向子类对象,调用方法时会调用子类的实现,而不是父类的实现,称为多态.
+class Parent {
+
+ void contextLoads(){
+ System.out.println("i am Parent ... ");
+ }
+}
+class Child extends Parent {
+ @Override
+ void contextLoads(){
+ System.out.println("i am Child ... ");
+ }
+}
+class mainTest{
+ public static void main(String[] args) {
+ Parent child = new Child();
+ // i am Child ...
+ child.contextLoads();
+ }
+}
+
优点
+缺点
+不能使用子类特有的方法和属性.在编写代码期间使用多态调用方法或属性时,编译工具首先会检查父类中是否有该方法和属性,如果没有,则会编译报错。
+class Parent {
+ void contextLoads(){
+ System.out.println("i am Parent ... ");
+ }
+}
+class Child extends Parent {
+
+ String c = "child";
+
+ @Override
+ void contextLoads(){
+ System.out.println("i am Child ... ");
+ }
+ void test() {
+ System.out.println("i am test method ...");
+ }
+}
+class mainTest{
+ public static void main(String[] args) {
+ Parent child = new Child();
+ // 编译报错: 无法解析 'Parent' 中的方法 'test'
+ child.test();
+ // 编译报错: 不能解决符号 'c'
+ child.c;
+ }
+}
+
重写(Override)存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法。
+++为了满足里式替换原则,重写有以下三个限制:
++
+- 子类方法的访问权限必须大于等于父类方法;
+- 子类方法的返回类型必须是父类方法返回类型或为其子类型。
+- 子类方法抛出的异常类型必须是父类抛出异常类型或为其子类型。
+
使用@Override
注解,可以让编译器帮忙检查是否满足上面的三个限制条件。
下面的示例中,SubClass
为 SuperClass
的子类,SubClass
重写了 SuperClass
的 func()
方法。其中:
public
,大于父类的protected
。ArrayList
,是父类返回类型List
的子类。Exception
,是父类抛出异常 Throwable
的子类。@Override
注解,从而让编译器自动检查是否满足限制条件。class SuperClass {
+ protected List<Integer> func() throws Throwable {
+ return new ArrayList<>();
+ }
+}
+
+class SubClass extends SuperClass {
+ @Override
+ public ArrayList<Integer> func() throws Exception {
+ return new ArrayList<>();
+ }
+}
+
在调用一个方法时,先从本类中查找看是否有对应的方法,如果没有查找到再到父类中查看,看是否有继承来的方法。否则就要对参数进行转型,转成父类之后看是否有对应的方法。总的来说,方法调用的优先级为:
+this.func(this)
super.func(this)
this.func(super)
super.func(super)
/*继承关系
+ A
+ |
+ B
+ |
+ C
+ |
+ D
+ */
+
+
+class A {
+
+ public void show(A obj) {
+ System.out.println("A.show(A)");
+ }
+
+ public void show(C obj) {
+ System.out.println("A.show(C)");
+ }
+}
+
+class B extends A {
+
+ @Override
+ public void show(A obj) {
+ System.out.println("B.show(A)");
+ }
+}
+
+class C extends B {
+}
+
+class D extends C {
+}
+class mainTest{
+ public static void main(String[] args) {
+
+ A a = new A();
+ B b = new B();
+ C c = new C();
+ D d = new D();
+
+ // 在 A 中存在 show(A obj),直接调用
+ a.show(a); // A.show(A)
+ // 在 A 中不存在 show(B obj),将 B 转型成其父类 A
+ a.show(b); // A.show(A)
+ // 在 B 中存在从 A 继承来的 show(C obj),直接调用
+ b.show(c); // A.show(C)
+ // 在 B 中不存在 show(D obj),但是存在从 A 继承来的 show(C obj),将 D 转型成其父类 C
+ b.show(d); // A.show(C)
+
+ // 引用的还是 B 对象,所以 ba 和 b 的调用结果一样
+ A ba = new B();
+ ba.show(c); // A.show(C)
+ ba.show(d); // A.show(C)
+ }
+}
+
重载(overload)是在一个类里面,方法名字相同,但是参数类型、个数、顺序至少有一个不同。返回类型可以相同也可以不同。应该注意的是,返回值不同,其它都相同不算是重载。
+每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。 最常用的地方就是构造器的重载。
+++重载规则:
++
+- 被重载的方法必须改变参数列表(参数个数或类型不一样);
+- 被重载的方法可以改变返回类型;
+- 被重载的方法可以改变访问修饰符;
+- 被重载的方法可以声明新的或更广的检查异常;
+- 方法能够在同一个类中或者在一个子类中被重载。
+- 无法以返回值类型作为重载函数的区分标准。
+
public class Overloading {
+ public int test(){
+ System.out.println("test1");
+ return 1;
+ }
+
+ public void test(int a){
+ System.out.println("test2");
+ }
+
+ //以下两个参数类型顺序不同
+ public String test(int a,String s){
+ System.out.println("test3");
+ return "returntest3";
+ }
+
+ public String test(String s,int a){
+ System.out.println("test4");
+ return "returntest4";
+ }
+
+ public static void main(String[] args){
+ Overloading o = new Overloading();
+ System.out.println(o.test());
+ o.test(1);
+ System.out.println(o.test(1,"test3"));
+ System.out.println(o.test("test4",1));
+ }
+}
+
方法的重写(Override)和重载(Overload)是Java多态性的不同表现,重写是父类与子类之间多态性的一种表现,重载可以理解成多态的具体表现形式
++++
+- 方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载(Overloading)。
+- 方法重写是在子类存在方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也一样的方法,就称为重写(Overriding)。
+- 方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。
+
++设计模式是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结,使用设计模式是为了可重用代码、让代码更容易被他人理解并且保证代码可靠性。
+虽然GoF设计模式只有23个,但是它们各具特色,每个模式都为某一个可重复的设计问题提供了一套解决方案。根据它们的用途,设计模式可分为创建型,结构型和行为型三种,其中创建型模式主要用于描述如何创建对象,结构型模式主要用于描述如何实现类或对象的组合,行为型模式主要用于描述类或对象怎样交互以及怎样分配职责;在GoF23种设计模式中包含5种创建型设计模式、7种结构型设计模式和11种行为型设计模式。此外,根据某个模式主要是用于处理类之间的关系还是对象之间的关系,设计模式还可以分为类模式和对象模式。
+
设计模式是一套被反复使用的、多数人知晓的、经过分类编目、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性、高内聚低耦合。
++ ++
设计模式类型 | +设计模式名称 | +介绍 | +学习难度 | +使用频率 | +
---|---|---|---|---|
创建型模式(6种) | +单例模式 | +保证一个类仅有一个对象,并提供一个访问它的全局访问点。 | +★☆☆☆☆ | +★★★★☆ | +
简单工厂模式 | +定义一个工厂类,它可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类。 | +★★☆☆☆ | +★★★★★ | +|
工厂方法模式 | +定义一个用于创建对象的接口,让子类决定将哪一个类实例化。 | +★★☆☆☆ | +★★★★★ | +|
抽象工厂模式 | +提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们的具体类。 | +★★★★☆ | +★★★★★ | +|
原型模式 | +使用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。 | +★★★☆☆ | +★★★☆☆ | +|
建造者模式 | +将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。 | +★★★★☆ | +★★☆☆☆ | +|
结构型模式(7种) | +适配器模式 | +将一个类的接口转换成客户希望的另一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。 | +★★☆☆☆ | +★★★★☆ | +
桥接模式 | +将抽象部分与它的实现部分分离,使他们都可以独立地变化。 | +★★★☆☆ | +★★★☆☆ | +|
组合模式 | +组合多个对象形成树形结构以表示具有“整体—部分”关系的层次结构。组合模式对单个对象和组合对象的使用具有一致性。 | +★★★☆☆ | +★★★★☆ | +|
装饰模式 | +动态地给一个对象增加一些额外的职责,就增加对象功能来说,装饰模式比生成子类实现更为灵活。 | +★★★☆☆ | +★★★☆☆ | +|
外观模式 | +为子系统中的一组接口提供一个统一的入口。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。 | +★☆☆☆☆ | +★★★★★ | +|
享元模式 | +运用共享技术有效地支持大量细粒度的对象。 | +★★★★☆ | +★☆☆☆☆ | +|
代理模式 | +为其他对象提供一个代理以控制对这个对象的访问。 | +★★★☆☆ | +★★★★☆ | +|
行为模式(11种) + | 职责链模式 | +为解除请求的发送者和接收者之间的耦合,而使多个对象都有机会处理这个请求。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它。 | +★★★☆☆ | +★★☆☆☆ | +
命令模式 | +将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可取消的操作。 | +★★★☆☆ | +★★★★☆ | +|
解释器模式 | +定义一个语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。 | +★★★★★ | +★☆☆☆☆ | +|
迭代器模式 | +提供一种方法顺序访问一个聚合对象中各个元素,而又不需暴露该对象的内部表示。 | +★★★☆☆ | +★★★★★ | +|
中介者模式 | +用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显示地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。 | +★★★☆☆ | +★★☆☆☆ | +|
备忘录模式 | +在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保持该状态,这样以后就可以将该对象恢复到保存的状态。 | +★★☆☆☆ | +★★☆☆☆ | +|
观察者模式 | +定义对象间的一种一对多的依赖关系,以便当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动刷新。 | +★★★☆☆ | +★★★★★ | +|
状态模式 | +允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它所属的类。 | +★★★☆☆ | +★★★☆☆ | +|
策略模式 | +定义一系列的算法,把它们一个个封装起来,并且使他们可相互替换。本模式使得算法的变化可以独立于使用它的客户。 | +★☆☆☆☆ | +★★★★☆ | +|
模板方法模式 | +定义一个操作中的算法的骨架,而将一些步骤延迟到子类。 | +★★☆☆☆ | +★★★☆☆ | +|
访问者模式 | +表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素类别的前提下定义作用于这些元素的新操作。 | +★★★★☆ | +★☆☆☆☆ | +
++单例模式:确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象创建型模式。
+
单例模式设计就是采用一定的方法保证在整个程序中,对某个类只能存在一个对象的实例,并且该类只提供一个取得其对象实例的方法.
+++单例模式作用:
++
+- 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如网站首页页面缓存).
+- 避免对资源的多重占用(比如写文件操作).
+
单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数据库或文件的对象(比如数据源、session
工厂等)
单例模式的6种写法.
+写法名称 | +优点 | +缺点 | +
---|---|---|
饿汉式 | +线程安全,写法简单 | +不懒加载,可能造成浪费 | +
懒汉式(线程不安全) | +懒加载 | +线程不安全 | +
懒汉式(线程安全) | +线程安全,懒加载 | +效率很低,反序列化破坏单例 | +
双重校验锁 | +线程安全,懒加载 | +反序列化破坏单例 | +
静态内部类式 | +线程安全,懒加载 | +反序列化破坏单例 | +
枚举式 | +防止反射攻击,反序列化创建对象,写法简单 | +不能传参,继承其他类 | +
class Singleton {
+
+ private Singleton() {}
+
+ private static final Singleton instance = new Singleton();
+
+ public static Singleton getInstance() {
+ return instance;
+ }
+}
+
这种写法比较简单,就是在类加载的时候就完成实例化。避免了线程同步问题。但是在类装载的时候就完成实例化,没有达到懒加载的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费.
+class Singleton {
+ private Singleton() {
+ }
+
+ private static Singleton instance;
+
+ public static Singleton getInstance() {
+ if (instance == null) {
+ instance = new Singleton();
+ }
+ return instance;
+ }
+}
+
起到了懒加载的效果,但是只能在单线程下使用。如果在多线程下,一个线程进入了if (singleton == null)
判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式.
class Singleton {
+
+ private static Singleton instance;
+
+ private Singleton() {
+ }
+
+ public static synchronized Singleton getInstance() {
+ if (instance == null) {
+ instance = new Singleton();
+ }
+ return instance;
+ }
+}
+
虽然解决了线程安全问题但是效率太低了,每个线程在想获得类的实例时候,执行getInstance()
方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接 return
就行了。
class Singleton {
+
+ /**
+ * volatile在这作用: 禁止JVM指令重排
+ */
+ private static volatile Singleton instance;
+
+ private Singleton() {
+ }
+
+ public static Singleton getInstance() {
+ if (instance == null) {
+ synchronized (Singleton.class) {
+ if (instance == null) {
+ instance = new Singleton();
+ }
+ }
+ }
+ return instance;
+ }
+}
+
Double-Check
概念是多线程开发中常使用到的,如代码中所示,我们进行了两次if (singleton == null)
检查,这样就可以保证线程安全了。这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null)
,直接return
实例化对象,也避免的反复进行方法同步.
class Singleton {
+
+ private Singleton() {}
+
+ private static class InnerClass {
+ private static final Singleton instance = new Singleton();
+ }
+ public static Singleton getInstance() {
+ return InnerClass.instance;
+ }
+}
+
++这种方式同样利用了
+classloder
的机制来保证初始化instance
时只有一个线程,它跟饿汉式不同的是(很细微的差别):饿汉式是只要Singleton
类被装载了,那么instance
就会被实例化(没有达到lazy loading
效果),而这种方式是Singleton
类被装载了,instance
不一定被初始化。因为SingletonHolder
类没有被主动使用,只有显示通过调用getInstance
方法时,才会显示装载SingletonHolder
类,从而实例化instance
。想象一下,如果实例化instance
很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton
类加载时就实例化,因为我不能确保Singleton
类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance
显然是不合适的。这个时候,这种方式相比饿汉式更加合理。
enum Singleton {
+ INSTAMCE;
+
+ Singleton() {
+ }
+}
+
这种方式是《Effective Java》作者Josh Bloch
提倡的方式.借助 JDK1.5 中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
上述单例模式6种写法除了,枚举式单例外其他5种写法都存在序列化问题.序列化可以破坏单例.原因是序列化会通过反射调用无参数的构造方法创建一个新的对象.
+要想防止序列化对单例的破坏,只要在单例类中定义readResolve
方法就可以解决该问题.原因是反序列化时,会通过反射的方式调用要被反序列化的类的readResolve
方法
+主要在Singleton
类中定义readResolve
方法,并在该方法中指定要返回的对象的生成策略,就可以防止单例被破坏。
以DCL
为例,在该单例类中插入readResolve
方法。
public class Singleton implements Serializable{
+
+ private volatile static Singleton singleton;
+
+ private Singleton (){}
+
+ public static Singleton getSingleton() {
+ if (singleton == null) {
+ synchronized (Singleton.class) {
+ if (singleton == null) {
+ singleton = new Singleton();
+ }
+ }
+ }
+ return singleton;
+ }
+
+ private Object readResolve() {
+ return singleton;
+ }
+}
+
工厂模式是将实例化的对象代码提取出来,放到一个类中统一管理和维护,达到和主项目的依赖关系的解耦,从而提高项目的扩展和维护性.创建对象实例时,不要直接new
类而是把这个new
类的动作放在一个工厂的方法中,并返回.不要让类继承具体的类,而是继承抽象类或者实现接口.
详情查看以下三种工厂设计模式.
+++简单工厂模式:定义一个工厂类,它可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类。因为在简单工厂模式中用于创建实例的方法是静态方法,因此简单工厂模式又被称为静态工厂方法模式,它属于类创建型模式。
+
public class SimpleFactoryDemo {
+ public static void main(String[] args) {
+ SimpleFactory.getTest(1);
+ }
+}
+
+class SimpleFactoryImpl1 implements SimpleFactoryInterface {
+
+ @Override
+ public void test() {
+ System.out.println("i am simpleFactory 1 ...");
+ }
+}
+class SimpleFactoryImpl2 implements SimpleFactoryInterface {
+
+ @Override
+ public void test() {
+ System.out.println("i am simpleFactory 2 ...");
+ }
+}
+
+class SimpleFactory {
+ public static void getTest(int n) {
+ switch (n) {
+ case 1:
+ SimpleFactoryImpl1 simpleFactory = new SimpleFactoryImpl1();
+ simpleFactory.test();
+ break;
+ case 2:
+ SimpleFactoryImpl2 simpleFactoryImpl2 = new SimpleFactoryImpl2();
+ simpleFactoryImpl2.test();
+ break;
+ default:
+ }
+ }
+}
+
简单工厂模式总结:
+简单工厂模式适用于创建的对象比较少,业务逻辑不太复杂的情景
+++工厂方法模式:定义一个用于创建对象的接口,让子类决定将哪一个类实例化。工厂方法模式让一个类的实例化延迟到其子类。工厂方法模式又简称为工厂模式,又可称作虚拟构造器模式或多态工厂模式。工厂方法模式是一种类创建型模式。
+
public class FactoryMethodDemo {
+ public static void main(String[] args) {
+ FoodFactory f = new ColdRiceNoodleFactory();
+ f.getFood().eat();
+ // 扩展需要增加产品及相应产品工厂并实现相关接口
+ }
+}
+
+interface Food {
+ void eat();
+}
+interface FoodFactory {
+ Food getFood();
+}
+
+class RiceNoodle implements Food{
+
+ @Override
+ public void eat() {
+ System.out.println("eat rice noodle ...");
+ }
+}
+class RiceNoodleFactory implements FoodFactory{
+
+ @Override
+ public Food getFood() {
+ return new RiceNoodle();
+ }
+}
+
+class ColdRiceNoodle implements Food {
+
+ @Override
+ public void eat() {
+ System.out.println("eat cold rice noodle ...");
+ }
+}
+class ColdRiceNoodleFactory implements FoodFactory {
+
+ @Override
+ public Food getFood() {
+ return new ColdRiceNoodle();
+ }
+}
+
工厂方法模式是简单工厂模式的延伸,它继承了简单工厂模式的优点,同时还弥补了简单工厂模式的不足. +使用工厂方法模式扩展时,无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改其他的具体工厂和具体产品,而只要添加一个具体工厂和具体产品就可以了,这样,系统的可扩展性也就变得非常好,完全符合“开闭原则”。
+但是在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。
+++抽象工厂模式:提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。抽象工厂模式又称为Kit模式,它是一种对象创建型模式。
+
interface Food {
+ void eat();
+}
+
+interface FoodFactory {
+ ColdRiceNoodle getColdRiceNoodle();
+ RiceNoodle getRiceNoodle();
+}
+class RiceNoodle implements Food{
+
+ @Override
+ public void eat() {
+ System.out.println("eating rice noodle");
+ }
+}
+class ColdRiceNoodle implements Food{
+
+ @Override
+ public void eat() {
+ System.out.println("eating cold rice noodle");
+ }
+}
+class RiceNoodleFactory implements FoodFactory {
+
+
+ @Override
+ public ColdRiceNoodle getColdRiceNoodle() {
+ return new ColdRiceNoodle();
+ }
+
+ @Override
+ public RiceNoodle getRiceNoodle() {
+ return new RiceNoodle();
+ }
+}
+
+class ColdRiceNoodleFactory implements FoodFactory {
+
+ @Override
+ public ColdRiceNoodle getColdRiceNoodle() {
+ return new ColdRiceNoodle();
+ }
+
+ @Override
+ public RiceNoodle getRiceNoodle() {
+ return new RiceNoodle();
+ }
+}
+
抽象工厂模式是工厂方法模式的进一步延伸,仍然具有工厂方法和简单工厂的优点.抽象工厂模式隔离了具体类的生成,使得客户并不需要知道什么被创建。由于这种隔离,更换一个具体工厂就变得相对容易,所有的具体工厂都实现了抽象工厂中定义的那些公共接口,因此只需改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为.
+但是抽象工厂也存在一些缺点,增加新的产品等级结构麻烦,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,这显然会带来较大的不便,违背了“开闭原则”。
+在Java中通过new
关键字创建的对象是非常繁琐的,在我们需要大量对象的情况下,原型模式就是我们可以考虑实现的方式.
++原型模式:使用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。原型模式是一种对象创建型模式
+
原型模式我们也称为克隆模式,即一个某个对象为原型克隆出来一个一模一样的对象,该对象的属性和原型对象一模一样。而且对于原型对象没有任何影响。原型模式的克隆方式有两种:浅克隆和深度克隆;浅克隆和深克隆的主要区别在于是否支持引用类型的成员变量的复制.
+在浅克隆中,当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制.
+代码实现
+public class ShallowClone {
+ public static void main(String[] args) {
+ CloneHuman cloneHuman = new CloneHuman("黑色","大眼睛","高鼻梁","大嘴巴",new Date(123231231231L));
+ for (int i = 0; i < 20; i++) {
+ try {
+ CloneHuman clone = (CloneHuman)cloneHuman.clone();
+ System.out.printf("头发:%s,眼睛:%s,鼻子:%s,嘴巴:%s,生日:%s",clone.getHair(),clone.getEye(),clone.getNodes(),clone.getMouse(),clone.getBirth());
+ System.out.println();
+ System.out.println("浅克隆,引用类型地址比较:" + (cloneHuman.getBirth() == clone.getBirth()));
+ } catch (CloneNotSupportedException e) {
+ System.out.println(e.getMessage());
+ }
+ }
+ }
+}
+
+class CloneHuman implements Cloneable {
+
+ private String hair;
+
+ private String eye;
+
+ private String nodes;
+
+ private String mouse;
+
+ private Date birth;
+
+ public String getHair() {
+ return hair;
+ }
+
+ public void setHair(String hair) {
+ this.hair = hair;
+ }
+
+ public String getEye() {
+ return eye;
+ }
+
+ public void setEye(String eye) {
+ this.eye = eye;
+ }
+
+ public String getNodes() {
+ return nodes;
+ }
+
+ public void setNodes(String nodes) {
+ this.nodes = nodes;
+ }
+
+ public String getMouse() {
+ return mouse;
+ }
+
+ public void setMouse(String mouse) {
+ this.mouse = mouse;
+ }
+
+ public CloneHuman(String hair, String eye, String nodes, String mouse,Date brith) {
+ this.hair = hair;
+ this.eye = eye;
+ this.nodes = nodes;
+ this.mouse = mouse;
+ this.birth = brith;
+ }
+
+ @Override
+ protected Object clone() throws CloneNotSupportedException {
+ return super.clone();
+ }
+
+ public Date getBirth() {
+ return birth;
+ }
+
+ public void setBirth(Date birth) {
+ this.birth = birth;
+ }
+}
+
++PS Java语言提供的
+Cloneable
接口和Serializable
接口的代码非常简单,它们都是空接口,这种空接口也称为标识接口,标识接口中没有任何方法的定义,其作用是告诉JRE这些接口的实现类是否具有某个功能,如是否支持克隆、是否支持序列化等。
应该注意的是,clone()
方法并不是Cloneable
接口的方法,而是Object
的一个protected
方法。Cloneable
接口只是规定,如果一个类没有实现Cloneable
接口又调用了clone()
方法,就会抛出 CloneNotSupportedException
。
在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制. +深克隆有两种实现方式,第一种是在浅克隆的基础上实现,第二种是通过序列化和反序列化实现
+在浅克隆的基础上实现
+public class DeepClone {
+ public static void main(String[] args) {
+ CloneHuman cloneHuman = new CloneHuman("黑色","大眼睛","高鼻梁","大嘴巴",new Date(123231231231L));
+ for (int i = 0; i < 20; i++) {
+ try {
+ CloneHuman clone = (CloneHuman)cloneHuman.clone();
+ System.out.printf("头发:%s,眼睛:%s,鼻子:%s,嘴巴:%s,生日:%s",clone.getHair(),clone.getEye(),clone.getNodes(),clone.getMouse(),clone.getBirth());
+ System.out.println();
+ System.out.println("深克隆,引用类型地址比较:" + (cloneHuman.getBirth() == clone.getBirth()));
+ } catch (CloneNotSupportedException e) {
+ System.out.println(e.getMessage());
+ }
+ }
+ }
+}
+class CloneHuman implements Cloneable {
+
+ private String hair;
+
+ private String eye;
+
+ private String nodes;
+
+ private String mouse;
+
+ private Date birth;
+
+ public String getHair() {
+ return hair;
+ }
+
+ public void setHair(String hair) {
+ this.hair = hair;
+ }
+
+ public String getEye() {
+ return eye;
+ }
+
+ public void setEye(String eye) {
+ this.eye = eye;
+ }
+
+ public String getNodes() {
+ return nodes;
+ }
+
+ public void setNodes(String nodes) {
+ this.nodes = nodes;
+ }
+
+ public String getMouse() {
+ return mouse;
+ }
+
+ public void setMouse(String mouse) {
+ this.mouse = mouse;
+ }
+
+ public CloneHuman(String hair, String eye, String nodes, String mouse,Date brith) {
+ this.hair = hair;
+ this.eye = eye;
+ this.nodes = nodes;
+ this.mouse = mouse;
+ this.birth = brith;
+ }
+
+ @Override
+ protected Object clone() throws CloneNotSupportedException {
+ CloneHuman human = (CloneHuman)super.clone();
+ human.birth = (Date)this.birth.clone();
+ return human;
+ }
+
+ public Date getBirth() {
+ return birth;
+ }
+
+ public void setBirth(Date birth) {
+ this.birth = birth;
+ }
+}
+
序列化反序列化实现深克隆
+public class DeepClone2 {
+ public static void main(String[] args) throws IOException, ClassNotFoundException {
+ CloneHuman2 cloneHuman1 = new CloneHuman2("黑色","大眼睛","高鼻梁","大嘴巴",new Date(123231231231L));
+
+ // 使用序列化和反序列化实现深克隆
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ ObjectOutputStream oos = new ObjectOutputStream(bos);
+ oos.writeObject(cloneHuman1);
+ byte[] bytes = bos.toByteArray();
+
+ ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
+ ObjectInputStream ois = new ObjectInputStream(bis);
+
+ // 克隆好的对象
+ CloneHuman2 cloneHuman2 = (CloneHuman2) ois.readObject();
+ System.out.println("深克隆,引用类型地址比较:" + (cloneHuman1.getBirth() == cloneHuman2.getBirth()));
+
+ }
+}
+class CloneHuman2 implements Cloneable, Serializable {
+
+ private String hair;
+
+ private String eye;
+
+ private String nodes;
+
+ private String mouse;
+
+ private Date birth;
+
+ public String getHair() {
+ return hair;
+ }
+
+ public void setHair(String hair) {
+ this.hair = hair;
+ }
+
+ public String getEye() {
+ return eye;
+ }
+
+ public void setEye(String eye) {
+ this.eye = eye;
+ }
+
+ public String getNodes() {
+ return nodes;
+ }
+
+ public void setNodes(String nodes) {
+ this.nodes = nodes;
+ }
+
+ public String getMouse() {
+ return mouse;
+ }
+
+ public void setMouse(String mouse) {
+ this.mouse = mouse;
+ }
+
+ public CloneHuman2(String hair, String eye, String nodes, String mouse,Date brith) {
+ this.hair = hair;
+ this.eye = eye;
+ this.nodes = nodes;
+ this.mouse = mouse;
+ this.birth = brith;
+ }
+
+ @Override
+ protected Object clone() throws CloneNotSupportedException {
+ CloneHuman2 human = (CloneHuman2)super.clone();
+ human.birth = (Date)this.birth.clone();
+ return human;
+ }
+
+ public Date getBirth() {
+ return birth;
+ }
+
+ public void setBirth(Date birth) {
+ this.birth = birth;
+ }
+}
+
原型模式作为一种快速创建大量相同或相似对象的方式,在软件开发中应用较为广泛,很多软件提供的复制和粘贴操作就是原型模式的典型应用.
+通过clone
的方式在获取大量对象的时候性能开销基本没有什么影响,而new
的方式随着实例的对象越来越多,性能会急剧下降,所以原型模式是一种比较重要的获取实例的方式.
优点
+缺点
+使用场景
+原型模式很少单独出现,一般是和工厂方法模式一起出现,通过clone
的方法创建一个对象,然后由工厂方法提供给调用者。
+spring中bean的创建实际就是两种:单例模式和原型模式。
++建造者模式:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。建造者模式是一种对象创建型模式。
+
建造者模式是较为复杂的创建型模式,它将客户端与包含多个组成部分(或部件)的复杂对象的创建过程分离,客户端无须知道复杂对象的内部组成部分与装配方式,只需要知道所需建造者的类型即可。它关注如何一步一步创建一个的复杂对象,不同的具体建造者定义了不同的创建过程,且具体建造者相互独立,增加新的建造者非常方便,无须修改已有代码,系统具有较好的扩展性。
+代码实现
+public class MainTest {
+ public static void main(String[] args) {
+
+ Director director = new Director();
+ Builder commonBuilder = new CommonRole();
+
+ director.construct(commonBuilder);
+ Role commonRole = commonBuilder.getRole();
+ System.out.println(commonRole);
+ }
+}
+
+class Role {
+ private String head;
+ private String body;
+ private String foot;
+ private String sp;
+ private String hp;
+ private String name;
+
+ public void setSp(String sp) {
+ this.sp = sp;
+ }
+
+ public String getSp() {
+ return sp;
+ }
+
+ public void setHp(String hp) {
+ this.hp = hp;
+ }
+
+ public String getHp() {
+ return hp;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String toString() {
+ return "Role{" +
+ "head='" + head + '\'' +
+ ", body='" + body + '\'' +
+ ", foot='" + foot + '\'' +
+ ", sp='" + sp + '\'' +
+ ", hp='" + hp + '\'' +
+ ", name='" + name + '\'' +
+ '}';
+ }
+}
+
+abstract class Builder {
+
+ public abstract void builderHead();
+
+ public abstract void builderBody();
+
+ public abstract void builderFoot();
+
+ public abstract void builderSp();
+
+ public abstract void builderHp();
+
+ public abstract void builderName();
+
+ public Role getRole() {
+ return new Role();
+ }
+}
+
+class CommonRole extends Builder {
+
+ private Role role = new Role();
+
+ @Override
+ public void builderHead() {
+ System.out.println("building head .....");
+ }
+
+ @Override
+ public void builderBody() {
+ System.out.println("building body .....");
+ }
+
+ @Override
+ public void builderFoot() {
+ System.out.println("building foot .....");
+ }
+
+ @Override
+ public void builderSp() {
+ role.setSp("100");
+ }
+
+ @Override
+ public void builderHp() {
+ role.setHp("100");
+ }
+
+ @Override
+ public void builderName() {
+ role.setName("lucy");
+ }
+
+ @Override
+ public Role getRole() {
+ return role;
+ }
+}
+class Director {
+
+ public void construct(Builder builder) {
+ builder.builderHead();
+ builder.builderBody();
+ builder.builderFoot();
+ builder.builderHp();
+ builder.builderSp();
+ builder.builderName();
+ }
+}
+
优点
+缺点
+使用场景
+++适配器模式:将一个接口转换成客户希望的另一个接口(指广义的接口,它可以表示一个方法或者方法的集合),使接口不兼容的那些类可以一起工作,其别名为包装器。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。
+
在适配器模式中,我们通过增加一个新的适配器类来解决接口不兼容的问题,使得原本没有任何关系的类可以协同工作。根据适配器类与适配者类的关系不同,适配器模式可分为对象适配器和类适配器两种,在对象适配器模式中,适配器与适配者之间是关联关系;在类适配器模式中,适配器与适配者之间是继承(或实现)关系。
+由于在Java中不支持多重继承,而且有破坏封装之嫌。所以提倡多用组合少用继承,在实际开发中推荐使用对象适配器模式。
+代码实现
+public class MainTest {
+ public static void main(String[] args) {
+ new RedHat(new Linux()).useInputMethod();
+ System.out.println("==============");
+
+ new Win10(new Windows()).useInputMethod();
+ System.out.println("==============");
+
+ Adapter adapter = new Adapter(new Windows());
+ new RedHat(adapter).useInputMethod();
+ }
+}
+
+interface LinuxSoftware {
+ void inputMethod();
+}
+
+interface WindowsSoftware {
+ void inputMethod();
+}
+
+class Linux implements LinuxSoftware {
+
+ @Override
+ public void inputMethod() {
+ System.out.println("linux 系统输入法 ...");
+ }
+}
+
+class Windows implements WindowsSoftware {
+
+ @Override
+ public void inputMethod() {
+ System.out.println("windows 系统输入法 ...");
+ }
+}
+
+class RedHat {
+ private LinuxSoftware linuxSoftware;
+
+ public RedHat(LinuxSoftware linuxSoftware) {
+ this.linuxSoftware = linuxSoftware;
+ }
+
+ public void useInputMethod() {
+ System.out.println("开始使用 redHat 系统输入法 ...");
+ linuxSoftware.inputMethod();
+ System.out.println("结束使用 redHat 系统输入法 ...");
+ }
+}
+
+class Win10 {
+ private WindowsSoftware windowsSoftware;
+
+ public Win10(WindowsSoftware windowsSoftware) {
+ this.windowsSoftware = windowsSoftware;
+ }
+
+ public void useInputMethod() {
+ System.out.println("开始使用 win10 系统输入法 ...");
+ windowsSoftware.inputMethod();
+ System.out.println("结束使用 win10 系统输入法 ...");
+ }
+}
+
+// 在 Linux 系统上使用 windows 输入法
+class Adapter implements LinuxSoftware{
+
+ private WindowsSoftware windowsSoftware;
+
+ public Adapter(WindowsSoftware windowsSoftware) {
+ this.windowsSoftware = windowsSoftware;
+ }
+
+ @Override
+ public void inputMethod() {
+ windowsSoftware.inputMethod();
+ }
+}
+
类适配器模式和对象适配器模式最大的区别在于适配器和适配者之间的关系不同,对象适配器模式中适配器和适配者之间是关联关系,而类适配器模式中适配器和适配者是继承关系。
+代码实现
+public class MainTest {
+ public static void main(String[] args) {
+ new RedHat(new Linux()).useInputMethod();
+ System.out.println("==============");
+
+ new Win10(new Windows()).useInputMethod();
+ System.out.println("==============");
+
+ Adapter adapter = new Adapter();
+ new RedHat(adapter).useInputMethod();
+ }
+}
+
+interface LinuxSoftware {
+ void inputMethod();
+}
+
+interface WindowsSoftware {
+ void inputMethod();
+}
+
+class Linux implements LinuxSoftware {
+
+ @Override
+ public void inputMethod() {
+ System.out.println("linux 系统输入法 ...");
+ }
+}
+
+class Windows implements WindowsSoftware {
+
+ @Override
+ public void inputMethod() {
+ System.out.println("windows 系统输入法 ...");
+ }
+}
+
+class RedHat {
+ private LinuxSoftware linuxSoftware;
+
+ public RedHat(LinuxSoftware linuxSoftware) {
+ this.linuxSoftware = linuxSoftware;
+ }
+
+ public void useInputMethod() {
+ System.out.println("开始使用 redHat 系统输入法 ...");
+ linuxSoftware.inputMethod();
+ System.out.println("结束使用 redHat 系统输入法 ...");
+ }
+}
+
+class Win10 {
+ private WindowsSoftware windowsSoftware;
+
+ public Win10(WindowsSoftware windowsSoftware) {
+ this.windowsSoftware = windowsSoftware;
+ }
+
+ public void useInputMethod() {
+ System.out.println("开始使用 win10 系统输入法 ...");
+ windowsSoftware.inputMethod();
+ System.out.println("结束使用 win10 系统输入法 ...");
+ }
+}
+
+// 在 Linux 系统上使用 windows 输入法
+class Adapter extends Windows implements LinuxSoftware{
+ @Override
+ public void inputMethod() {
+ super.inputMethod();
+ }
+}
+
适配器模式将现有接口转化为客户类所期望的接口,实现了对现有类的复用,它是一种使用频率非常高的设计模式,在软件开发中得以广泛应用,在Spring等开源框架、驱动程序设计(如JDBC中的数据库驱动程序)中也使用了适配器模式。
+优点
+缺点
+使用场景
+系统需要使用现有的类,而这些类的接口不符合系统的需要;想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类一起工作。
+++桥接模式:将实现与抽象放在两个不同的类层次中,使两个层次可以独立改变。它是一种对象结构型模式,又称为柄体模式或接口模式。
+
桥接模式是一种很实用的结构型设计模式,如果软件系统中某个类存在两个独立变化的维度,通过该模式可以将这两个维度分离出来,使两者可以独立扩展,让系统更加符合“单一职责原则”。
+例如像手机制造:内存是一个公司生产,芯片是另一个公司生产,而品牌又是另一个公司。我们需要什么样子的手机就把相应的芯片、内存组装起来。桥接模式就是把两个不同维度的东西桥接起来。
+代码实现
+public class MainTest {
+ public static void main(String[] args) {
+ Phone_A phone_a = new Phone_A();
+ phone_a.setAbstractChip(new Chip_A());
+ phone_a.setAbstractMemory(new Memory_A());
+ phone_a.finished();
+ System.out.println("=================");
+ Phone_B phone_b = new Phone_B();
+ phone_b.setAbstractChip(new Chip_B());
+ phone_b.setAbstractMemory(new Memory_A());
+ phone_b.finished();
+ }
+}
+abstract class AbstractMemory {
+ protected abstract void size();
+}
+
+abstract class AbstractChip {
+ protected abstract void type();
+}
+abstract class AbstractPhone {
+ protected AbstractMemory abstractMemory;
+ protected AbstractChip abstractChip;
+
+ public void setAbstractChip(AbstractChip abstractChip) {
+ this.abstractChip = abstractChip;
+ }
+
+ public void setAbstractMemory(AbstractMemory abstractMemory) {
+ this.abstractMemory = abstractMemory;
+ }
+
+ protected abstract void finished();
+}
+
+class Memory_A extends AbstractMemory{
+
+ @Override
+ protected void size() {
+ System.out.println("create 6G of memory ...");
+ }
+}
+class Memory_B extends AbstractMemory{
+
+ @Override
+ protected void size() {
+ System.out.println("create 8G of memory ...");
+ }
+}
+
+class Chip_A extends AbstractChip {
+
+ @Override
+ protected void type() {
+ System.out.println("snapdragon 888 chip ...");
+ }
+}
+
+class Chip_B extends AbstractChip {
+
+ @Override
+ protected void type() {
+ System.out.println("A14 chip ...");
+ }
+}
+
+class Phone_A extends AbstractPhone{
+
+ @Override
+ protected void finished() {
+ abstractMemory.size();
+ abstractChip.type();
+ System.out.println("phone_a assembly completed ...");
+ }
+}
+
+class Phone_B extends AbstractPhone{
+
+ @Override
+ protected void finished() {
+ abstractMemory.size();
+ abstractChip.type();
+ System.out.println("phone_b assembly completed ...");
+ }
+}
+
桥接模式是设计Java虚拟机和实现JDBC等驱动程序的核心模式之一,应用较为广泛。在软件开发中如果一个类或一个系统有多个变化维度时,都可以尝试使用桥接模式对其进行设计。桥接模式为多维度变化的系统提供了一套完整的解决方案,并且降低了系统的复杂度。
+优点
+缺点
+使用场景
+++组合模式:组合多个对象形成树形结构以表示具有“整体—部分”关系的层次结构。组合模式对单个对象和组合对象的使用具有一致性,组合模式又可以称为“整体—部分”模式,它是一种对象结构型模式。
+
组合模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。
+代码实现
+public class MainTest {
+ public static void main(String[] args) {
+ Tree tree = new Tree();
+ tree.add(new LeftNode());
+ tree.add(new RightNode());
+ tree.implmethod();
+ }
+}
+
+abstract class AbstractTree {
+ protected abstract void add(AbstractTree node);
+
+ protected abstract void remove(AbstractTree node);
+
+ protected abstract void implmethod();
+}
+
+class LeftNode extends AbstractTree {
+
+ @Override
+ protected void add(AbstractTree node) {
+ System.out.println("Exception: the method is not supported. ");
+ }
+
+ @Override
+ protected void remove(AbstractTree node) {
+ System.out.println("Exception: the method is not supported. ");
+ }
+
+ @Override
+ protected void implmethod() {
+ System.out.println("left node method ...");
+ }
+}
+
+class RightNode extends AbstractTree {
+
+ @Override
+ protected void add(AbstractTree node) {
+ System.out.println("Exception: the method is not supported. ");
+ }
+
+ @Override
+ protected void remove(AbstractTree node) {
+ System.out.println("Exception: the method is not supported. ");
+ }
+
+ @Override
+ protected void implmethod() {
+ System.out.println("right node method ...");
+ }
+}
+
+class Tree extends AbstractTree {
+
+ private ArrayList<AbstractTree> treeList = new ArrayList<>();
+
+
+ @Override
+ protected void add(AbstractTree node) {
+ treeList.add(node);
+ }
+
+ @Override
+ protected void remove(AbstractTree node) {
+ treeList.remove(node);
+ }
+
+ @Override
+ protected void implmethod() {
+ System.out.println("tree node");
+ for (AbstractTree node : treeList) {
+ node.implmethod();
+ }
+ }
+}
+
组合模式使用面向对象的思想来实现树形结构的构建与处理,描述了如何将容器对象和叶子对象进行递归组合,实现简单,灵活性好。由于在软件开发中存在大量的树形结构,因此组合模式是一种使用频率较高的结构型设计模式。在XML解析、组织结构树处理、文件系统设计等领域,组合模式都得到了广泛应用。
+优点
+缺点
+使用场景
+++装饰模式:动态地给一个对象增加一些额外的职责,就增加对象功能来说,装饰模式比生成子类实现更为灵活。装饰模式是一种对象结构型模式。
+
代码实现
+public class MainTest {
+ public static void main(String[] args) {
+ Drink coffee = new ShortBlock();
+ System.out.println("订单价格:"+ coffee.cost());
+ System.out.println("订单描述:" + coffee.getDesc());
+ System.out.println("=====================");
+ coffee = new Chocolate(coffee);
+ System.out.println("订单价格:"+ coffee.cost());
+ System.out.println("订单描述:" + coffee.getDesc());
+ System.out.println("=====================");
+ coffee = new Milk(coffee);
+ System.out.println("订单价格:"+ coffee.cost());
+ System.out.println("订单描述:" + coffee.getDesc());
+ }
+}
+
+abstract class Drink {
+
+ public String desc;
+
+ private double price = 0.0;
+
+ public void setDesc(String desc) {
+ this.desc = desc;
+ }
+
+ public String getDesc() {
+ return desc;
+ }
+
+ public void setPrice(double price) {
+ this.price = price;
+ }
+
+ public double getPrice() {
+ return price;
+ }
+
+ protected abstract double cost();
+}
+
+class ShortBlock extends Drink{
+
+ public ShortBlock() {
+ super.setPrice(3.0);
+ super.setDesc("ShortBlock");
+ }
+
+ @Override
+ protected double cost() {
+ return super.getPrice();
+ }
+}
+
+class LongBlock extends Drink {
+
+ public LongBlock() {
+ super.setPrice(5.0);
+ super.setDesc("LongBlock");
+ }
+
+ @Override
+ protected double cost() {
+ return super.getPrice();
+ }
+}
+
+class CoffeeShop extends Drink {
+
+ private final Drink coffee;
+
+ protected CoffeeShop(Drink coffee) {
+ this.coffee = coffee;
+ }
+
+ @Override
+ protected double cost() {
+ return super.getPrice() + coffee.cost();
+ }
+
+ @Override
+ public String getDesc() {
+ return desc + " " + getPrice() +" && "+ coffee.getDesc();
+ }
+}
+
+class Milk extends CoffeeShop {
+
+ public Milk(Drink seasoning) {
+ super(seasoning);
+ super.setDesc("Milk");
+ super.setPrice(6);
+ }
+
+}
+
+class Chocolate extends CoffeeShop {
+
+ public Chocolate(Drink seasoning) {
+ super(seasoning);
+ super.setDesc("Chocolate");
+ super.setPrice(4);
+ }
+
+}
+
装饰模式降低了系统的耦合度,可以动态增加或删除对象的职责,并使得需要装饰的具体构件类和具体装饰类可以独立变化,以便增加新的具体构件类和具体装饰类。在软件开发中,装饰模式应用较为广泛,例如在JavaIO中的输入流和输出流的设计、javax.swing包中一些图形界面构件功能的增强等地方都运用了装饰模式。
+**装饰者模式主要解决:**一般的,我们为了扩展一个类经常使用继承方式实现,由于继承为类引入静态特征,并且随着扩展功能的增多,子类会很膨胀。
+优点
+缺点
+使用场景
+这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。可代替继承。
+++外观模式:为子系统中的一组接口提供一个统一的入口。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
+
外观模式又称为门面模式,它是一种对象结构型模式。外观模式是迪米特法则的一种具体实现,通过引入一个新的外观角色可以降低原有系统的复杂度,同时降低客户类与子系统的耦合度。
+代码实现
+public class MainTest {
+ public static void main(String[] args) {
+ Facade facade = new Facade();
+ facade.aTest();
+ }
+}
+class Facade {
+ private final A a;
+ private final B b;
+ private final C c;
+ private final D d;
+
+ public Facade() {
+ this.a = A.getInstance();
+ this.b = B.getInstance();
+ this.c = C.getInstance();
+ this.d = D.getInstance();
+ }
+
+ public void aTest() {
+ a.aTest();
+ b.aTest();
+ c.aTest();
+ d.aTest();
+ }
+}
+
+/**
+ * A调用B类,A调用C类,B调用C类,D调用B类,D调用C类
+ */
+class A {
+ private final static A instance = new A();
+ private A(){}
+ public static A getInstance() {
+ return instance;
+ }
+ public void aTest() {
+ B.getInstance().aTest();
+ C.getInstance().aTest();
+ System.out.println("A class test method ...");
+ }
+}
+
+class B {
+ private final static B instance = new B();
+ private B(){}
+ public static B getInstance() {
+ return instance;
+ }
+ public void aTest() {
+ C.getInstance().aTest();
+ System.out.println("B class test method ...");
+ }
+}
+
+class C {
+ private final static C instance = new C();
+ private C(){}
+ public static C getInstance() {
+ return instance;
+ }
+ public void aTest() {
+ System.out.println("C class test method ...");
+ }
+}
+
+class D {
+ private final static D instance = new D();
+ private D(){}
+ public static D getInstance() {
+ return instance;
+ }
+ public void aTest() {
+ B.getInstance().aTest();
+ C.getInstance().aTest();
+ System.out.println("D class test method ...");
+ }
+}
+
外观模式的主要目的在于降低系统的复杂程度,在面向对象软件系统中,类与类之间的关系越多,不能表示系统设计得越好,反而表示系统中类之间的耦合度太大,这样的系统在维护和修改时都缺乏灵活性,因为一个类的改动会导致多个类发生变化,而外观模式的引入在很大程度上降低了类与类之间的耦合关系。引入外观模式之后,增加新的子系统或者移除子系统都非常方便,客户类无须进行修改(或者极少的修改),只需要在外观类中增加或移除对子系统的引用即可。从这一点来说,外观模式在一定程度上并不符合开放封闭原则,增加新的子系统需要对原有系统进行一定的修改,虽然这个修改工作量不大。
+优点
+缺点
+使用场景
+++享元模式:运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式,它是一种对象结构型模式。
+
代码实现
+public class MainTest {
+ public static void main(String[] args) {
+ WebSiteFactory webSiteFactory = new WebSiteFactory();
+
+ webSiteFactory.getWebSite("新闻").use(new User("lucy"));
+
+ webSiteFactory.getWebSite("新闻").use(new User("jane"));
+
+ webSiteFactory.getWebSite("博客").use(new User("jack"));
+
+ webSiteFactory.getWebSite("博客").use(new User("maik"));
+
+ webSiteFactory.getWebSite("博客").use(new User("seven"));
+
+ System.out.println("网站类型共:" + webSiteFactory.countWebSiteType());
+ }
+}
+
+abstract class WebSite{
+
+ private String name;
+
+ public abstract void use(User user);
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+}
+
+class ConcreateWebSite extends WebSite {
+
+ public ConcreateWebSite(String name) {
+ super.setName(name);
+ }
+
+ @Override
+ public void use(User user) {
+ System.out.println("网站类型名称:" + super.getName() +"\t 网站用户:" + user.getUsername());
+ }
+}
+
+class User {
+
+ private String username;
+
+ public User(String username) {
+ this.username = username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+}
+
+class WebSiteFactory{
+
+ private HashMap<String, WebSite> pool = new HashMap<>();
+
+ public WebSite getWebSite(String webSiteName){
+ if (!pool.containsKey(webSiteName)) {
+ pool.put(webSiteName, new ConcreateWebSite(webSiteName));
+ }
+ return pool.get(webSiteName);
+ }
+
+ public Integer countWebSiteType() {
+ return pool.size();
+ }
+
+}
+
当系统中存在大量相同或者相似的对象时,享元模式是一种较好的解决方案,它通过共享技术实现相同或相似的细粒度对象的复用,从而节约了内存空间,提高了系统性能。相比其他结构型设计模式,享元模式的使用频率并不算太高,但是作为一种以“节约内存,提高性能”为出发点的设计模式,它在软件开发中还是得到了一定程度的应用。例如:String、Java的池技术等。
+享元模式主要解决在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建。
+优点
+缺点
+使用场景
+ 在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。
+++代理模式:给某一个对象提供一个代理或占位符,并由代理对象来控制对原对象的访问。
+
代理模式是为一个对象提供一个替身,以控制对这个对象的访问。即通过代理对象访问目标对象.这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。被代理的对象可以是远程对象、创建开销大的对象或需要安全控制的对象
+代理模式有不同的形式, 主要有三种 静态代理、动态代理 (JDK
代理、Cglib
代理)。
public class MainTest {
+ public static void main(String[] args) {
+ ProxyImpl proxy = new ProxyImpl();
+ StaticProxy staticProxy = new StaticProxy(proxy);
+ staticProxy.test();
+ }
+}
+
+interface ProxyInterface {
+ void test();
+}
+
+class ProxyImpl implements ProxyInterface{
+
+ @Override
+ public void test() {
+ System.out.println("helloWorld");
+ }
+
+}
+
+
+class StaticProxy implements ProxyInterface{
+
+ private ProxyImpl target;
+
+ public StaticProxy (ProxyImpl target){
+ this.target=target;
+ }
+
+
+ @Override
+ public void test() {
+ System.out.println("静态代理之前...");
+ target.test();
+ System.out.println("静态代理之后...");
+ }
+}
+
静态代理能在不修改目标对象的功能前提下,能通过代理对象对目标进行扩展。但是因为代理对象需要与目标对象实现相同的接口,所以会有很多代理类一旦接口增加方法后,目标对象与代理对象都需要维护。
+动态代理,代理对象不需要实现接口,但是目标对象需要实现接口,否则不能实现动态代理。代理对象的生产是利用JDK的API,动态的在内存中构建代理对象。
+public class MainTest {
+ public static void main(String[] args) {
+ ProxyImpl proxy = new ProxyImpl();
+ ProxyInterface jdkProxyInterface = (ProxyInterface)new JDKProxy().bind(proxy);
+ jdkProxyInterface.test();
+ }
+}
+interface ProxyInterface {
+ void test();
+}
+
+class ProxyImpl implements ProxyInterface{
+
+ @Override
+ public void test() {
+ System.out.println("helloWorld");
+ }
+
+}
+
+class JDKProxy {
+ //通用类型,表示被代理的真实对象
+ private Object target;
+
+ public Object bind(Object target){
+ this.target=target;
+ //生成代理类(与被代理对象实现相同接口的兄弟类)
+ return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), (proxy, method, args) -> {
+ Object res;
+ System.out.println("JDK动态代理前");
+ res=method.invoke(target, args);
+ System.out.println("JDK动态代理后");
+ return res;
+ });
+ }
+
+}
+
Cglib
代理也叫作子类代理,它是在内存中构建一个子类对象从而实现对目标对象功能扩展。
+Cglib
是一个强大的高性能的代码生成包,它可以在运行期扩展 java 类与实现 java 接口.它广泛的被许多 AOP 的框架使用,例如 Spring AOP,实现方法拦截。
以下测试代码需要导入Cglib和asm相关jar包。
+asm-3.3.1.jar
+cglib-2.2.jar
+
PS:使用`CGLib`实现动态代理时出现了下面这个异常
+Exception in thread "main" java.lang.IncompatibleClassChangeError:
+class net.sf.cglib.core.DebuggingClassWriter has interface org.objectweb.asm.ClassVisitor as super class
+
+++
+- +
+原因: +cglib.jar包含asm.jar包。报错内容是
+ClassVisitor
的父类不相容。详细:原因分析- +
+解决: +测试时用
+cglib2.2.jar
和asm3.3.1.jar
版本,解决jar包冲突问题。
public class MainTest {
+ public static void main(String[] args) {
+ Target target = new Target();
+ Target bind = (Target)new CGLibProxy().bind(target);
+ bind.test();
+ }
+}
+
+
+class Target{
+
+ public void test() {
+ System.out.println("helloWorld");
+ }
+
+}
+
+class CGLibProxy implements MethodInterceptor {
+
+ private Object target;
+
+ public Object bind(Object target) {
+ this.target = target;
+ Enhancer enhancer = new Enhancer();
+ enhancer.setSuperclass(target.getClass());
+ enhancer.setCallback(this);
+ return enhancer.create();
+ }
+
+ @Override
+ public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
+ Object res;
+ System.out.println("CGLib动态代理前");
+ res=method.invoke(target, args);
+ System.out.println("CGLib动态代理后");
+ return res;
+ }
+
+}
+
代理模式是常用的结构型设计模式之一,它为对象的间接访问提供了一个解决方案,可以对对象的访问进行控制。代理模式主要解决:在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。
+优点
+缺点
+使用场景
+++职责链模式:避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。职责链模式是一种对象行为型模式。
+
代码实现
+public class MainTest {
+ public static void main(String[] args) {
+ // 产品
+ RequestEntity test = new RequestEntity("test", 2000);
+
+ // 指定职责链
+ BeforeHandler before = new BeforeHandler("before");
+ AfterHandler after = new AfterHandler("after");
+ PostHandler post = new PostHandler("post");
+
+ // 形成链状闭环 要确保会被责任链中的组件处理 否则会一直循环下去 ,当然也可以选择不闭环
+ before.setHandler(after);
+ after.setHandler(post);
+ post.setHandler(before);
+
+ after.handleRequest(test);
+ }
+
+}
+
+class RequestEntity {
+
+ public String name;
+
+ public Integer grade;
+
+ public RequestEntity(String name,Integer grade) {
+ this.name = name;
+ this.grade = grade;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public float getGrade() {
+ return grade;
+ }
+}
+
+abstract class Handler {
+
+ // 下一个引用
+ protected Handler handler;
+
+ protected String name;
+
+ public Handler(String name) {
+ this.name = name;
+ }
+
+ public void setHandler(Handler handler) {
+ this.handler = handler;
+ }
+
+ public abstract void handleRequest(RequestEntity requestEntity);
+
+}
+
+class BeforeHandler extends Handler {
+
+ public BeforeHandler(String name){
+ super(name);
+ }
+
+
+ @Override
+ public void handleRequest(RequestEntity requestEntity) {
+ if (requestEntity.getGrade() < 1000) {
+ System.out.println("分数为:" + requestEntity.getGrade() + ",被" + this.name + "处理 ");
+ }else {
+ handler.handleRequest(requestEntity);
+ }
+ }
+}
+
+class AfterHandler extends Handler {
+
+ public AfterHandler(String name){
+ super(name);
+ }
+
+ @Override
+ public void handleRequest(RequestEntity requestEntity) {
+ if (requestEntity.getGrade() <= 2000) {
+ System.out.println("分数为:" + requestEntity.getGrade() + ",被" + this.name + "处理 ");
+ }else {
+ handler.handleRequest(requestEntity);
+ }
+ }
+}
+
+class PostHandler extends Handler {
+
+ public PostHandler(String name){
+ super(name);
+ }
+
+ @Override
+ public void handleRequest(RequestEntity requestEntity) {
+ if (requestEntity.getGrade() > 3000) {
+ System.out.println("分数为:" + requestEntity.getGrade() + ",被" + this.name + "处理 ");
+ }else {
+ handler.handleRequest(requestEntity);
+ }
+ }
+}
+
职责链模式通过建立一条链来组织请求的处理者,请求将沿着链进行传递,请求发送者无须知道请求在何时、何处以及如何被处理,实现了请求发送者与处理者的解耦。在软件开发中,如果遇到有多个对象可以处理同一请求时可以应用职责链模式,例如在Web应用开发中创建一个过滤器(Filter)链来对请求数据进行过滤,在工作流系统中实现公文的分级审批等等,使用职责链模式可以较好地解决此类问题。
+优点
+缺点
+ setNext()
方法中判断是否已经超过阀值,超过则不允许该链建立,避免出现超长链无意识地破坏系统性能。使用场景
+++命令模式:将一个请求封装为一个对象,从而让我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式,其别名为动作模式或事务模式。
+
命令模式可以将请求发送者和接收者完全解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求。
+代码实现
+public class MainTest {
+ public static void main(String[] args) {
+ LightReceiver lightReceiver = new LightReceiver();
+
+ LightOnCommand lightOnCommand = new LightOnCommand(lightReceiver);
+ LightOffCommand lightOffCommand = new LightOffCommand(lightReceiver);
+ RemoteCommand remoteCommand = new RemoteCommand();
+ // 测试
+ remoteCommand.setCommand(0,lightOnCommand,lightOffCommand);
+ // 按下开灯按钮
+ System.out.println("按下开灯按钮 -------");
+ remoteCommand.onButtonPushed(0);
+ // 按下关灯按钮
+ System.out.println("按下关灯按钮 -------");
+ remoteCommand.offButtonPushed(0);
+ // 按下撤销按钮
+ System.out.println("按下撤销按钮 ---------");
+ remoteCommand.undoButtonPushed(0);
+ }
+}
+
+interface Command{
+
+ /**
+ * 执行命令
+ */
+ void exec();
+
+ /**
+ * 撤销命令
+ */
+ void undo();
+
+}
+
+class LightReceiver{
+
+ public void on(){
+ System.out.println("开灯 ...");
+ }
+
+ public void off() {
+ System.out.println("关灯 ...");
+ }
+
+
+}
+
+class LightOnCommand implements Command{
+
+ private LightReceiver lightReceiver;
+
+ public LightOnCommand(LightReceiver lightReceiver) {
+ super();
+ this.lightReceiver = lightReceiver;
+ }
+
+ @Override
+ public void exec() {
+ lightReceiver.on();
+ }
+
+ @Override
+ public void undo() {
+ lightReceiver.off();
+ }
+}
+
+class LightOffCommand implements Command {
+
+ private LightReceiver lightReceiver;
+
+ public LightOffCommand(LightReceiver lightReceiver) {
+ super();
+ this.lightReceiver = lightReceiver;
+ }
+
+ @Override
+ public void exec() {
+ lightReceiver.off();
+ }
+
+ @Override
+ public void undo() {
+ lightReceiver.on();
+ }
+}
+
+/**
+ * 空执行 默认命令实现类
+ */
+class NoCommand implements Command {
+
+ @Override
+ public void exec() {
+ System.out.println("默认命令执行");
+ }
+
+ @Override
+ public void undo() {
+ System.out.println("默认撤销方法");
+ }
+}
+
+class RemoteCommand {
+
+ // 存放开关命令
+ private Command onCommands[];
+
+ private Command offCommands[];
+
+ // 存放撤销命令
+ private Command undoCommands[];
+
+ public RemoteCommand() {
+ undoCommands = new Command[5];
+ onCommands = new Command[5];
+ offCommands = new Command[5];
+
+ // 默认空命令
+ for (int i = 0; i < 5; i++) {
+ onCommands[i] = new NoCommand();
+ offCommands[i] = new NoCommand();
+ }
+ }
+
+ // 设置命令
+ public void setCommand(int no, Command onCommand, Command offCommand) {
+ onCommands[no] = onCommand;
+ offCommands[no] = offCommand;
+ }
+
+ // 按下开的按钮
+ public void onButtonPushed(int no) {
+ onCommands[no].exec();
+ // 记录撤销操作
+ undoCommands[no] = onCommands[no];
+ }
+
+ // 按下关闭的按钮
+ public void offButtonPushed(int no) {
+ offCommands[no].exec();
+ // 记录撤销操作
+ undoCommands[no] = offCommands[no];
+ }
+
+ // 按下撤销按钮
+ public void undoButtonPushed(int no) {
+ undoCommands[no].undo();
+ }
+
+
+}
+
命令模式是一种使用频率非常高的设计模式,它可以将请求发送者与接收者解耦,请求发送者通过命令对象来间接引用请求接收者,使得系统具有更好的灵活性和可扩展性。在基于GUI的软件开发,无论是在电脑桌面应用还是在移动应用中,命令模式都得到了广泛的应用。
+在软件系统中,行为请求者与行为实现者通常是一种紧耦合的关系,但某些场合,比如需要对行为进行记录、撤销或重做、事务等处理时,这种无法抵御变化的紧耦合的设计就不太合适。
+优点
+缺点
+使用场景
+++解释器模式:定义一个语言的文法,并且建立一个解释器来解释该语言中的句子,这里的“语言”是指使用规定格式和语法的代码。解释器模式是一种类行为型模式。
+
代码实现
+public class MainTest {
+ public static void main(String[] args) throws IOException {
+ String expStr = getExpStr();
+ HashMap<String, Integer> var = getValue(expStr);
+ Calculator calculator = new Calculator(expStr);
+ System.out.println("运算结果:" + expStr + "=" + calculator.run(var));
+ }
+
+ // 获得表达式
+ public static String getExpStr() throws IOException {
+ System.out.print("请输入表达式:");
+ return (new BufferedReader(new InputStreamReader(System.in))).readLine();
+ }
+
+ // 获得值映射
+ public static HashMap<String, Integer> getValue(String expStr) throws IOException {
+ HashMap<String, Integer> map = new HashMap<>();
+
+ for (char ch : expStr.toCharArray()) {
+ if (ch != '+' && ch != '-') {
+ if (!map.containsKey(String.valueOf(ch))) {
+ System.out.print("请输入" + String.valueOf(ch) + "的值:");
+ String in = (new BufferedReader(new InputStreamReader(System.in))).readLine();
+ map.put(String.valueOf(ch), Integer.valueOf(in));
+ }
+ }
+ }
+ return map;
+ }
+
+}
+
+abstract class AbstractExpression {
+
+ /**
+ * 表达式解释器
+ */
+ public abstract int interpreter(HashMap<String, Integer> var);
+
+}
+
+/**
+ * 终结表达式
+ */
+class VarExpression extends AbstractExpression {
+
+ private String key;
+
+ public VarExpression(String key) {
+ this.key = key;
+ }
+
+ @Override
+ public int interpreter(HashMap<String, Integer> map) {
+ return map.get(key);
+ }
+}
+
+/**
+ * 非终结表达式
+ */
+class SymbolExpression extends AbstractExpression {
+
+ protected AbstractExpression left;
+
+ protected AbstractExpression right;
+
+ SymbolExpression(AbstractExpression left, AbstractExpression right) {
+ this.left = left;
+ this.right = right;
+ }
+
+
+ // 因为 SymbolExpression 是让其子类来实现,因此 interpreter 是一个默认实现
+ @Override
+ public int interpreter(HashMap<String, Integer> var) {
+ return 0;
+ }
+
+}
+
+/**
+ * 减法
+ */
+class SubExpression extends SymbolExpression {
+
+ public SubExpression(AbstractExpression left, AbstractExpression right) {
+ super(left, right);
+ }
+
+ @Override
+ public int interpreter(HashMap<String, Integer> var) {
+ return super.left.interpreter(var) - super.right.interpreter(var);
+ }
+
+}
+
+/**
+ * 加法
+ */
+class AddExpression extends SymbolExpression {
+
+ public AddExpression(AbstractExpression left, AbstractExpression right) {
+ super(left, right);
+ }
+
+ @Override
+ public int interpreter(HashMap<String, Integer> var) {
+ return super.left.interpreter(var) + super.right.interpreter(var);
+ }
+
+}
+
+/**
+ * 计算器 调用加减法
+ */
+class Calculator {
+
+ private AbstractExpression expression;
+
+ public Calculator(AbstractExpression expression) {
+ this.expression = expression;
+ }
+
+ public Calculator(String expStr) {
+ // 安排运算先后顺序
+ Stack<AbstractExpression> stack = new Stack<>();
+ // 表达式拆分成字符数组
+ char[] charArray = expStr.toCharArray();
+
+
+ AbstractExpression left = null;
+ AbstractExpression right = null;
+ //遍历我们的字符数组, 即遍历 [a, +, b]
+ //针对不同的情况,做处理
+ for (int i = 0; i < charArray.length; i++) {
+ switch (charArray[i]) {
+ case '+':
+ // 从 stack 取 出 left => "a"
+ left = stack.pop();
+ // 取出右表达式 "b"
+ right = new VarExpression(String.valueOf(charArray[++i]));
+ stack.push(new AddExpression(left, right));
+ // 然后根据得到 left 和 right 构建 AddExpresson 加入 stack
+ break;
+ case '-':
+ left = stack.pop();
+ right = new VarExpression(String.valueOf(charArray[++i]));
+ stack.push(new SubExpression(left, right));
+ break;
+ default:
+ //如果是一个 Var 就创建要给 VarExpression 对象,并 push 到 stack
+ stack.push(new VarExpression(String.valueOf(charArray[i])));
+
+ break;
+ }
+ }
+ //当遍历完整个 charArray 数组后,stack 就得到最后
+ this.expression = stack.pop();
+ }
+
+
+ public int run(HashMap<String, Integer> var) {
+ //最后将表达式 a+b 和 var = {a=10,b=20}
+ //然后传递给 expression 的 interpreter 进行解释执行
+ return this.expression.interpreter(var);
+ }
+
+}
+
解释器模式为自定义语言的设计和实现提供了一种解决方案,它用于定义一组文法规则并通过这组文法规则来解释语言中的句子。虽然解释器模式的使用频率不是特别高,但是它在正则表达式、XML文档解释等领域还是得到了广泛使用。与解释器模式类似,目前还诞生了很多基于抽象语法树的源代码处理工具,例如Eclipse中的Eclipse AST,它可以用于表示Java语言的语法结构,用户可以通过扩展其功能,创建自己的文法规则。
+优点
+缺点
+使用场景
+++迭代器模式:提供一种方法来访问聚合对象,而不用暴露这个对象的内部表示,其别名为游标。迭代器模式是一种对象行为型模式。
+
迭代器模式的重要用途就是帮助我们遍历容器。迭代器模式,提供一种遍历集合元素的统一接口,用一致的方法遍历集合元素,不需要知道集合对象的底层表示,即:不暴露其内部的结构。在迭代器模式结构中包含聚合和迭代器两个层次结构,考虑到系统的灵活性和可扩展性,在迭代器模式中应用了工厂方法模式.
+代码实现
+public class MainTest {
+ public static void main(String[] args) {
+ Bread bread = new Bread();
+ bread.add("面粉");
+ bread.add("黄油");
+ bread.add("白糖");
+ bread.add("鸡蛋");
+ Iterator iterator = bread.getIterator();
+ while (iterator.hasNext()) {
+ System.out.println(iterator.next());
+ }
+ }
+}
+
+class FoodIterator implements Iterator {
+
+ String[] foods;
+ int position = 0;
+
+ public FoodIterator(String[] foods) {
+ this.foods = foods;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return position != foods.length;
+ }
+
+ @Override
+ public Object next() {
+ String food = foods[position];
+ position += 1;
+ return food;
+ }
+
+}
+
+interface Food {
+
+ void add(String name);
+
+ Iterator getIterator();
+}
+
+class Bread implements Food{
+ private String[] foods = new String[4];
+ private int position = 0;
+
+ @Override
+ public void add(String name) {
+ foods[position] = name;
+ position += 1;
+ }
+
+ @Override
+ public Iterator getIterator() {
+ return new FoodIterator(this.foods);
+ }
+}
+
++迭代器模式是一种使用频率非常高的设计模式,通过引入迭代器可以将数据的遍历功能从聚合对象中分离出来,聚合对象只负责存储数据,而遍历数据由迭代器来完成。由于很多编程语言的类库都已经实现了迭代器模式,因此在实际开发中,我们只需要直接使用Java、C#等语言已定义好的迭代器即可,迭代器已经成为我们操作聚合对象的基本工具之一。
+
迭代器的使用现在非常广泛,因为Java中提供了java.util.Iterator
。而且Java中的很多容器(Collection、Set
)也都提供了对迭代器的支持。
优点
+缺点
+使用场景
+++中介者模式:用一个中介对象(中介者)来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。中介者模式又称为调停者模式,它是一种对象行为型模式。
+
如果在一个系统中对象之间存在多对多的相互关系,我们可以将对象之间的一些交互行为从各个对象中分离出来,并集中封装在一个中介者对象中,并由该中介者进行统一协调,这样对象之间多对多的复杂关系就转化为相对简单的一对多关系。通过引入中介者来简化对象之间的复杂交互,中介者模式是“迪米特法则”的一个典型应用。
+代码实现
+public class MainTest {
+ public static void main(String[] args) {
+ //创建一个中介者对象
+ Mediator mediator = new ConcreteMediator();
+
+ //创建 Alarm 并且加入到ConcreteMediator 对象的 HashMap
+ Alarm alarm = new Alarm(mediator, "alarm");
+
+ //创建了 CoffeeMachine 对象,并且加入到 ConcreteMediator 对象的 HashMap
+ CoffeeMachine coffeeMachine = new CoffeeMachine(mediator, "coffeeMachine");
+
+ //创建 tV , 并 且加入到 ConcreteMediator 对象的 HashMap
+ TV tV = new TV(mediator, "TV");
+
+ //让闹钟发出消息 依次调用
+ alarm.sendAlarm(0);
+ coffeeMachine.finishCoffee();
+ alarm.sendAlarm(1);
+ tV.startTv();
+ }
+
+}
+
+/**
+ * 中介者
+ */
+abstract class Mediator {
+
+ public abstract void register(String colleagueName, Colleague colleague);
+
+ public abstract void getMessage(int stateChange, String name);
+}
+
+class ConcreteMediator extends Mediator {
+
+ /**
+ * 集合,放入所有的同事对象
+ */
+ private HashMap<String, Colleague> colleagueMap;
+ private HashMap<String, String> interMap;
+
+ public ConcreteMediator() {
+ colleagueMap = new HashMap<>();
+ interMap = new HashMap<>();
+ }
+
+
+ @Override
+ public void register(String colleagueName, Colleague colleague) {
+ if (colleague instanceof Alarm) {
+ interMap.put("Alarm", colleagueName);
+ } else if (colleague instanceof CoffeeMachine) {
+ interMap.put("CoffeeMachine", colleagueName);
+ } else {
+ System.out.println("........");
+ }
+ }
+
+ @Override
+ public void getMessage(int stateChange, String colleagueName) {
+ if (colleagueMap.get(colleagueName) instanceof Alarm) {
+ if (stateChange == 0) {
+ ((CoffeeMachine) (colleagueMap.get(interMap
+ .get("CoffeeMachine")))).startCoffee();
+ ((TV) (colleagueMap.get(interMap.get("TV")))).startTv();
+ } else if (stateChange == 1) {
+ ((TV) (colleagueMap.get(interMap.get("TV")))).stopTv();
+ }
+
+ } else if (colleagueMap.get(colleagueName) instanceof TV) {
+ //如果 TV 发现消息
+ }
+ }
+}
+
+/**
+ * 抽象同事类
+ */
+abstract class Colleague {
+
+ private final Mediator mediator;
+ public String name;
+
+ public Colleague(Mediator mediator, String name) {
+
+
+ this.mediator = mediator;
+ this.name = name;
+
+ }
+
+
+ public Mediator getMediator() {
+ return this.mediator;
+ }
+
+
+ public abstract void sendMessage(int stateChange);
+}
+
+class Alarm extends Colleague {
+
+ public Alarm(Mediator mediator, String name) {
+ super(mediator, name);
+ //在创建 Alarm 同事对象时,将自己放入到 ConcreteMediator 对象中[集合]
+ mediator.register(name, this);
+ }
+
+ public void sendAlarm(int stateChange) {
+ this.sendMessage(stateChange);
+ }
+
+ @Override
+ public void sendMessage(int stateChange) {
+ // 调用的中介者对象的 getMessage 方法
+ this.getMediator().getMessage(stateChange, this.name);
+ }
+
+}
+
+class TV extends Colleague {
+
+
+ public TV(Mediator mediator, String name) {
+ super(mediator, name);
+ mediator.register(name, this);
+ }
+
+
+ @Override
+ public void sendMessage(int stateChange) {
+ this.getMediator().getMessage(stateChange, this.name);
+ }
+
+
+ public void startTv() {
+ System.out.println("It's time to StartTv!");
+ }
+
+
+ public void stopTv() {
+ System.out.println("StopTv!");
+ }
+}
+
+
+class CoffeeMachine extends Colleague {
+
+ public CoffeeMachine(Mediator mediator, String name) {
+ super(mediator, name);
+ mediator.register(name, this);
+ }
+
+
+ @Override
+ public void sendMessage(int stateChange) {
+ this.getMediator().getMessage(stateChange, this.name);
+ }
+
+
+ public void startCoffee() {
+ System.out.println("It's time to startcoffee!");
+ }
+
+
+ public void finishCoffee() {
+ System.out.println("After 5 minutes!");
+ System.out.println("Coffee is ok!");
+ sendMessage(0);
+ }
+}
+
中介者模式将一个网状的系统结构变成一个以中介者对象为中心的星形结构,在这个星型结构中,使用中介者对象与其他对象的一对多关系来取代原有对象之间的多对多关系。中介者模式在事件驱动类软件中应用较为广泛,特别是基于GUI(图形用户界面)的应用软件,此外,在类与类之间存在错综复杂的关联关系的系统中,中介者模式都能得到较好的应用。
+优点
+缺点
+使用场景
+主要解决:对象与对象之间存在大量的关联关系,这样势必会导致系统的结构变得很复杂,同时若一个对象发生改变,我们也需要跟踪与之相关联的对象,同时做出相应的处理。
+应当注意:不应当在职责混乱的时候使用。
+++备忘录模式:在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。它是一种对象行为型模式,其别名为Token。
+
在设计备忘录类时需要考虑其封装性,除了Originator
类,不允许其他类来调用备忘录类Memento
的构造函数与相关方法,如果不考虑封装性,允许其他类调用构造方法,将导致在备忘录中保存的历史状态发生改变,通过撤销操作所恢复的状态就不再是真实的历史状态,备忘录模式也就失去了本身的意义。
所谓备忘录模式就是在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。
+代码实现
+public class MainTest {
+ public static void main(String[] args) {
+ Originator originator = new Originator();
+ CareTaker careTaker = new CareTaker();
+ // 保存状态
+ careTaker.saveMemento(originator.saveState(" 状态#1 "));
+ careTaker.saveMemento(originator.saveState(" 状态#2 "));
+ careTaker.saveMemento(originator.saveState(" 状态#3 "));
+
+ System.out.println("目前保存的状态为:" + originator.getState());
+
+ System.out.println("开始恢复以前的状态 ....");
+ originator.recover(careTaker.recover(0));
+
+ System.out.println("恢复之后的状态为:" + originator.getState());
+
+ }
+}
+
+
+class Originator{
+
+ private String state;
+
+
+ public String getState() {
+ return state;
+ }
+
+ public Memento saveState(String state) {
+ this.state = state;
+ return new Memento(state);
+ }
+
+ public void recover(Memento memento) {
+ this.state = memento.getState();
+ }
+}
+
+/**
+ * 备忘录对象 保存对象信息
+ */
+class Memento{
+
+ /**
+ * 需要保存状态
+ */
+ private final String state;
+
+ public Memento(String state) {
+ this.state = state;
+ }
+
+ public String getState() {
+ return state;
+ }
+
+}
+
+/**
+ * 管理备忘录对象
+ */
+class CareTaker {
+
+ public ArrayList<Memento> mementos = new ArrayList<>();
+
+ public Memento recover(int index) {
+ return mementos.get(index);
+ }
+
+ public void saveMemento(Memento memento) {
+ mementos.add(memento);
+ }
+
+}
+
备忘录模式在很多软件的使用过程中普遍存在,但是在应用软件开发中,它的使用频率并不太高,因为现在很多基于窗体和浏览器的应用软件并没有提供撤销操作。如果需要为软件提供撤销功能,备忘录模式无疑是一种很好的解决方案。在一些字处理软件、图像编辑软件、数据库管理系统等软件中备忘录模式都得到了很好的应用。
+为了符合迪米特原则,还要增加一个管理备忘录的类(CareTaker
);为了节约内存,可使用原型模式+备忘录模式。
优点
+缺点
+使用场景
+很多时候我们总是需要记录一个对象的内部状态,这样做的目的就是为了允许用户取消不确定或者错误的操作,能够恢复到他原先的状态,使得他有"后悔药"可吃。
+++观察者模式:定义对象之间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。观察者模式的别名包括发布-订阅模式、模型-视图模式、源-监听器模式或从属者模式。观察者模式是一种对象行为型模式。
+
观察者模式描述了如何建立对象与对象之间的依赖关系,以及如何构造满足这种需求的系统。观察者模式包含观察目标和观察者两类对象,一个目标可以有任意数目的与之相依赖的观察者,一旦观察目标的状态发生改变,所有的观察者都将得到通知。作为对这个通知的响应,每个观察者都将监视观察目标的状态以使其状态与目标状态同步,这种交互也称为发布-订阅(Publish-Subscribe)。观察目标是通知的发布者,它发出通知时并不需要知道谁是它的观察者,可以有任意数目的观察者订阅它并接收通知。
+代码实现
+public class MainTest {
+ public static void main(String[] args) {
+ WeatherData weatherData = new WeatherData();
+ CurrentCondition currentCondition = new CurrentCondition();
+ BaiduSite baiduSite = new BaiduSite();
+
+ // 注册观察者
+ weatherData.registerObserver(currentCondition);
+ weatherData.registerObserver(baiduSite);
+ // 设置数据 一旦数据变化 所有的观察者都会变化
+ weatherData.setWeatherData(10f, 20f);
+
+ // 移除注册观察者
+ weatherData.removeObserver(baiduSite);
+
+ // 唤醒所有已经注册的观察者
+ weatherData.notifyObservers();
+ }
+}
+
+interface Subject {
+
+ void registerObserver(Observer observer);
+
+ void removeObserver(Observer observer);
+
+ void notifyObservers();
+}
+
+/**
+ * 观察者
+ */
+interface Observer {
+
+ void update(float temperature, float humidity);
+}
+
+class WeatherData implements Subject{
+
+ private ArrayList<Observer> observers = new ArrayList<>();
+
+ private float temperature;
+
+ private float humidity;
+
+ public void setWeatherData(float humidity, float temperature) {
+ this.humidity = humidity;
+ this.temperature = temperature;
+ }
+
+
+ @Override
+ public void registerObserver(Observer observer) {
+ observers.add(observer);
+ }
+
+ @Override
+ public void removeObserver(Observer observer) {
+ if (observers.contains(observer)) {
+ observers.remove(observer);
+ }
+ }
+
+ @Override
+ public void notifyObservers() {
+ // 唤醒所有的观察者
+ for (Observer observer : observers) {
+ observer.update(temperature,humidity);
+ }
+ }
+}
+
+class CurrentCondition implements Observer{
+
+ private float temperature;
+ private float humidity;
+
+ @Override
+ public void update(float temperature, float humidity) {
+ this.temperature = temperature;
+ this.humidity = humidity;
+ displayed();
+ }
+
+ void displayed() {
+ System.out.println("===当前天气情况===");
+ System.out.println("当前湿度:" + this.temperature);
+ System.out.println("当前温度:" + this.humidity);
+ }
+}
+
+class BaiduSite implements Observer{
+
+ private float temperature;
+ private float humidity;
+
+ @Override
+ public void update(float temperature, float humidity) {
+ this.temperature = temperature;
+ this.humidity = humidity;
+ displayed();
+ }
+
+ void displayed() {
+ System.out.println("===当前百度网站天气情况===");
+ System.out.println("当前湿度:" + this.temperature);
+ System.out.println("当前温度:" + this.humidity);
+ }
+}
+
观察者模式是一种使用频率非常高的设计模式,无论是移动应用、Web应用或者桌面应用,观察者模式几乎无处不在,它为实现对象之间的联动提供了一套完整的解决方案,凡是涉及到一对一或者一对多的对象交互场景都可以使用观察者模式。观察者模式广泛应用于各种编程语言的GUI事件处理的实现,在基于事件的XML解析技术(如SAX2)以及Web事件处理中也都使用了观察者模式。
+优点
+缺点
+使用场景
+一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。
+注意事项:
+++状态模式:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。其别名为状态对象,状态模式是一种对象行为型模式。
+
状态模式用于解决系统中复杂对象的状态转换以及不同状态下行为的封装问题。当系统中某个对象存在多个状态,这些状态之间可以进行转换,而且对象在不同状态下行为不相同时可以使用状态模式。状态模式将一个对象的状态从该对象中分离出来,封装到专门的状态类中,使得对象状态可以灵活变化,对于客户端而言,无须关心对象状态的转换以及对象所处的当前状态,无论对于何种状态的对象,客户端都可以一致处理。
+代码实现
+public class MainTest {
+ public static void main(String[] args) {
+ // 创建活动对象,奖品有 1 个奖品
+ RaffleActivity activity = new RaffleActivity(1);
+
+ // 我们连续抽 300 次奖
+ for (int i = 0; i < 30; i++) {
+ System.out.println("--------第" + (i + 1) + "次抽奖----------");
+ // 参加抽奖,第一步点击扣除积分
+ activity.debuctMoney();
+
+ // 第二步抽奖
+ activity.raffle();
+ }
+ }
+}
+
+
+abstract class State {
+
+
+ // 扣除积分 - 50
+ public abstract void deductMoney();
+
+ // 是否抽中奖品
+ public abstract boolean raffle();
+
+ // 发放奖品
+ public abstract void dispensePrize();
+}
+
+
+class RaffleActivity {
+
+ // state 表示活动当前的状态,是变化
+ State state = null;
+
+ // 奖品数量
+ int count = 0;
+
+ // 四个属性,表示四种状态
+ State noRafflleState = new NoRaffleState(this);
+ State canRaffleState = new CanRaffleState(this);
+
+ State dispenseState = new DispenseState(this);
+ State dispensOutState = new DispenseOutState(this);
+
+ //构造器
+ //1. 初始化当前的状态为 noRafflleState(即不能抽奖的状态)
+ //2. 初始化奖品的数量
+ public RaffleActivity(int count) {
+ this.state = getNoRafflleState();
+ this.count = count;
+ }
+
+ //扣分, 调用当前状态的 deductMoney
+ public void debuctMoney() {
+ state.deductMoney();
+ }
+
+ //抽奖
+ public void raffle() {
+ // 如果当前的状态是抽奖成功
+ if (state.raffle()) {
+ //领取奖品
+ state.dispensePrize();
+ }
+
+
+ }
+
+
+ public State getState() {
+ return state;
+ }
+
+
+ public void setState(State state) {
+ this.state = state;
+ }
+
+ //这里请大家注意,每领取一次奖品,count--
+ public int getCount() {
+ int curCount = count;
+ count--;
+ return curCount;
+ }
+
+
+ public void setCount(int count) {
+ this.count = count;
+ }
+
+ public State getNoRafflleState() {
+ return noRafflleState;
+ }
+
+
+ public void setNoRafflleState(State noRafflleState) {
+ this.noRafflleState = noRafflleState;
+ }
+
+
+ public State getCanRaffleState() {
+ return canRaffleState;
+ }
+
+
+ public void setCanRaffleState(State canRaffleState) {
+ this.canRaffleState = canRaffleState;
+ }
+
+
+ public State getDispenseState() {
+ return dispenseState;
+ }
+
+
+ public void setDispenseState(State dispenseState) {
+ this.dispenseState = dispenseState;
+ }
+
+ public State getDispensOutState() {
+ return dispensOutState;
+
+ }
+
+
+ public void setDispensOutState(State dispensOutState) {
+ this.dispensOutState = dispensOutState;
+ }
+}
+
+
+class DispenseOutState extends State {
+
+ // 初始化时传入活动引用
+ RaffleActivity activity;
+
+
+ public DispenseOutState(RaffleActivity activity) {
+ this.activity = activity;
+ }
+
+ @Override
+ public void deductMoney() {
+ System.out.println("奖品发送完了,请下次再参加");
+ }
+
+
+ @Override
+ public boolean raffle() {
+ System.out.println("奖品发送完了,请下次再参加");
+ return false;
+ }
+
+
+ @Override
+ public void dispensePrize() {
+ System.out.println("奖品发送完了,请下次再参加");
+ }
+}
+
+class DispenseState extends State {
+
+ // 初始化时传入活动引用,发放奖品后改变其状态
+ RaffleActivity activity;
+
+
+ public DispenseState(RaffleActivity activity) {
+ this.activity = activity;
+ }
+
+
+ @Override
+ public void deductMoney() {
+ System.out.println("不能扣除积分");
+ }
+
+
+ @Override
+ public boolean raffle() {
+ System.out.println("不能抽奖");
+ return false;
+ }
+
+ //发放奖品
+ @Override
+ public void dispensePrize() {
+ if (activity.getCount() > 0) {
+ System.out.println("恭喜中奖了");
+ // 改变状态为不能抽奖
+ activity.setState(activity.getNoRafflleState());
+ } else {
+ System.out.println("很遗憾,奖品发送完了");
+ // 改变状态为奖品发送完毕, 后面我们就不可以抽奖
+ activity.setState(activity.getDispensOutState());
+ //System.out.println("抽奖活动结束");
+ //System.exit(0);
+ }
+
+ }
+}
+
+class NoRaffleState extends State {
+
+ // 初始化时传入活动引用,扣除积分后改变其状态
+ RaffleActivity activity;
+
+
+ public NoRaffleState(RaffleActivity activity) {
+ this.activity = activity;
+ }
+
+ // 当前状态可以扣积分 , 扣除后,将状态设置成可以抽奖状态
+ @Override
+ public void deductMoney() {
+ System.out.println("扣除 50 积分成功,您可以抽奖了");
+ activity.setState(activity.getCanRaffleState());
+ }
+
+ // 当前状态不能抽奖
+ @Override
+ public boolean raffle() {
+ System.out.println("扣了积分才能抽奖喔!");
+ return false;
+ }
+
+ // 当前状态不能发奖品
+ @Override
+ public void dispensePrize() {
+ System.out.println("不能发放奖品");
+ }
+}
+
+
+class CanRaffleState extends State {
+
+ RaffleActivity activity;
+
+ public CanRaffleState(RaffleActivity activity) {
+ this.activity = activity;
+ }
+
+ @Override
+ public void deductMoney() {
+ System.out.println("已经扣取过了积分");
+ }
+
+ //可以抽奖, 抽完奖后,根据实际情况,改成新的状态
+ @Override
+ public boolean raffle() {
+ System.out.println("正在抽奖,请稍等!");
+ Random r = new Random();
+ int num = r.nextInt(10);
+ // 10%中奖机会
+ if (num == 0) {
+ // 改变活动状态为发放奖品
+ activity.setState(activity.getDispenseState());
+ return true;
+ } else {
+ System.out.println("很遗憾没有抽中奖品!");
+ // 改变状态为不能抽奖
+ activity.setState(activity.getNoRafflleState());
+ return false;
+ }
+ }
+
+ // 不能发放奖品
+ @Override
+ public void dispensePrize() {
+ System.out.println("没中奖,不能发放奖品");
+ }
+}
+
状态模式将一个对象在不同状态下的不同行为封装在一个个状态类中,通过设置不同的状态对象可以让环境对象拥有不同的行为,而状态转换的细节对于客户端而言是透明的,方便了客户端的使用。在实际开发中,状态模式具有较高的使用频率,在工作流和游戏开发中状态模式都得到了广泛的应用,例如公文状态的转换、游戏中角色的升级等。
+优点
+缺点
+使用场景
+状态模式主要解决对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为。在代码中包含大量与对象状态有关的条件语句时应该考虑使用状态模式。应当注意的是,在行为受状态约束的时候使用状态模式,状态应该不超过 5 个,太多则导致程序结构、代码混乱。
+++策略模式:定义一系列算法类,将每一个算法封装起来,并让它们可以相互替换,策略模式让算法独立于使用它的客户而变化,也称为政策模式。策略模式是一种对象行为型模式。
+
策略模式的主要目的是将算法的定义与使用分开,也就是将算法的行为和环境分开,将算法的定义放在专门的策略类中,每一个策略类封装了一种实现算法,使用算法的环境类针对抽象策略类进行编程,符合“依赖倒转原则”。在出现新的算法时,只需要增加一个新的实现了抽象策略类的具体策略类即可。
+代码实现
+public class MainTest {
+ public static void main(String[] args) {
+ Bird bird = new Bird();
+ bird.fly();
+
+ Duck duck = new Duck();
+ duck.fly();
+
+ Dog dog = new Dog();
+ dog.fly();
+
+ System.out.println("变为会飞 :");
+ dog.setFlyStrategy(new GoodFlyStrategy());
+ dog.fly();
+ }
+}
+
+abstract class AbstractStrategy {
+
+ protected FlyStrategy flyStrategy;
+
+ public void setFlyStrategy(FlyStrategy flyStrategy) {
+ this.flyStrategy = flyStrategy;
+ }
+
+ public abstract void fly();
+
+}
+
+class Bird extends AbstractStrategy{
+
+ public Bird() {
+ System.out.print("小鸟");
+ flyStrategy = new GoodFlyStrategy();
+ }
+
+ @Override
+ public void fly() {
+ flyStrategy.fly();
+ }
+}
+
+class Duck extends AbstractStrategy {
+
+ public Duck() {
+ System.out.print("鸭子");
+ flyStrategy = new BadFlyStrategy();
+ }
+
+ @Override
+ public void fly() {
+ flyStrategy.fly();
+ }
+}
+
+class Dog extends AbstractStrategy {
+
+ public Dog() {
+ System.out.print("狗");
+ flyStrategy = new NoFlyStrategy();
+ }
+
+ @Override
+ public void fly() {
+ flyStrategy.fly();
+ }
+}
+
+
+
+interface FlyStrategy {
+
+ void fly();
+}
+
+class GoodFlyStrategy implements FlyStrategy {
+
+ @Override
+ public void fly() {
+ System.out.println("擅长飞翔 ...");
+ }
+}
+
+class BadFlyStrategy implements FlyStrategy {
+
+ @Override
+ public void fly() {
+ System.out.println("不擅长飞翔 ...");
+ }
+}
+
+class NoFlyStrategy implements FlyStrategy {
+ @Override
+ public void fly() {
+ System.out.println("不会飞 ...");
+ }
+}
+
策略模式用于算法的自由切换和扩展,它是应用较为广泛的设计模式之一。策略模式对应于解决某一问题的一个算法族,允许用户从该算法族中任选一个算法来解决某一问题,同时可以方便地更换算法或者增加新的算法。只要涉及到算法的封装、复用和切换都可以考虑使用策略模式。
+优点
+缺点
+如果一个系统的策略多于四个,就需要考虑使用混合模式,解决策略类膨胀的问题。
+使用场景
+一个系统有许多许多类,而区分它们的只是他们直接的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
+如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。
+++模板方法模式:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。模板方法模式是一种基于继承的代码复用技术,它是一种类行为型模式。
+
代码实现
+public class MainTest {
+ public static void main(String[] args) {
+ System.out.println("=====红豆豆浆=====");
+ RedBean redBean = new RedBean();
+ redBean.template();
+
+ System.out.println("=====花生豆浆=====");
+ Peanut peanut = new Peanut();
+ peanut.template();
+
+ System.out.println("=====豆浆=====");
+ None none = new None();
+ none.template();
+ }
+}
+
+abstract class SoyaMilk {
+
+ final void template(){
+ filterMaterial();
+ soak();
+ if (isAppended()) {
+ add();
+ }
+ over();
+ }
+
+ void filterMaterial() {
+ System.out.println("第一步:筛选材料");
+ }
+
+ void soak() {
+ System.out.println("第二步:浸泡");
+ }
+
+ abstract void add();
+
+ void over(){
+ System.out.println("第四步:打豆浆");
+ }
+
+ /**
+ * 钩子方法
+ * @return
+ */
+ boolean isAppended(){
+ return true;
+ }
+
+}
+
+class Peanut extends SoyaMilk{
+
+
+ @Override
+ void add() {
+ System.out.println("第三步:加入花生");
+ }
+}
+
+class RedBean extends SoyaMilk {
+
+
+ @Override
+ void add() {
+ System.out.println("第三步:加入红豆");
+ }
+}
+
+class None extends SoyaMilk {
+
+ @Override
+ void add() {
+
+ }
+
+ @Override
+ boolean isAppended() {
+ return false;
+ }
+
+}
+
模板方法模式是基于继承的代码复用技术,它体现了面向对象的诸多重要思想,是一种使用较为频繁的模式。模板方法模式广泛应用于框架设计中,以确保通过父类来控制处理流程的逻辑顺序(如框架的初始化,测试流程的设置等)。
+优点
+缺点
+使用场景
+++访问者模式:提供一个作用于某对象结构中的各元素的操作表示,它使我们可以在不改变各元素的类的前提下定义作用于这些元素的新操作。访问者模式是一种对象行为型模式。
+
代码实现
+public class MainTest {
+ public static void main(String[] args) {
+ ObjectStructure objectStructure = new ObjectStructure();
+ objectStructure.attach(new Man());
+ objectStructure.attach(new WoMan());
+ objectStructure.attach(new Man());
+ objectStructure.attach(new WoMan());
+
+ // 显示成功的评价
+ Success success = new Success();
+ objectStructure.display(success);
+
+ System.out.println("==================");
+
+ // 显示失败的评价
+ Fail fail = new Fail();
+ objectStructure.display(fail);
+ }
+}
+
+abstract class Action {
+
+ protected abstract void getManResult(Man man);
+
+ protected abstract void getWomanResult(WoMan woman );
+
+}
+
+class Success extends Action{
+
+ @Override
+ protected void getManResult(Man man) {
+ System.out.println("男人觉得很赞~");
+ }
+
+ @Override
+ protected void getWomanResult(WoMan woman) {
+ System.out.println("女人觉得很赞~");
+ }
+}
+
+class Fail extends Action{
+
+ @Override
+ protected void getManResult(Man man) {
+ System.out.println("男人觉得很失败~");
+ }
+
+ @Override
+ protected void getWomanResult(WoMan woman) {
+ System.out.println("女人觉得很失败~");
+ }
+}
+
+
+abstract class Person {
+ abstract void accpet(Action action);
+}
+
+class WoMan extends Person{
+
+ @Override
+ void accpet(Action action) {
+ action.getWomanResult(this);
+ }
+}
+
+class Man extends Person{
+
+ @Override
+ void accpet(Action action) {
+ action.getManResult(this);
+ }
+}
+
+class ObjectStructure {
+
+ ArrayList<Person> people = new ArrayList<>();
+
+ public void attach(Person person) {
+ people.add(person);
+ }
+
+ public void detach(Person person) {
+ people.remove(person);
+ }
+
+ public void display(Action acion) {
+ people.forEach(item -> {
+ item.accpet(acion);
+ });
+ }
+
+}
+
由于访问者模式的使用条件较为苛刻,本身结构也较为复杂,因此在实际应用中使用频率不是特别高。当系统中存在一个较为复杂的对象结构,且不同访问者对其所采取的操作也不相同时,可以考虑使用访问者模式进行设计。在XML文档解析、编译器的设计、复杂集合对象的处理等领域访问者模式得到了一定的应用。
+优点
+缺点
+ +使用场景
+需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,使用访问者模式将这些封装到类中。访问者可以对功能进行统一,可以做报表、UI、拦截器与过滤器。
++ +
+ + + + + +Object 类位于 java.lang
包中,编译时会自动导入,我们创建一个类时,如果没有明确继承一个父类,那么它就会自动继承Object
,成为Object
的子类。
Object
类可以显示继承,也可以隐式继承,效果都是一样的。
class A extends Object{
+ // to do
+}
+
+class A {
+ // to do
+}
+
Java Object
类是所有类的父类,也就是说 Java 的所有类都继承了Object
,子类可以使用Object
的所有方法。
方法名称 | +方法作用 | +
---|---|
equals | +比较两个对象是否相同 | +
hashCode | +获取对象的哈希值 | +
toString | +返回对象的字符串表示形式 | +
clone | +创建并返回一个对象的拷贝 | +
finalize | +当垃圾收集确定不再有对对象的引用时,由垃圾收集器在对象上调用 | +
getClass | +获取对象运行时的类 | +
notify | +唤醒在该对象上等待的某个线程 | +
notifyAll | +唤醒在该对象上等待的所有线程 | +
wait | +让当前线程进入等待(阻塞)状态。直到其他线程调用此对象的notify() 方法或notifyAll() 方法。 |
+
Object
类中的equals()
方法作用是比较两个对象,是判断两个对象引用指向的是同一个对象,即比较两个对象的内存地址是否相等。
Object
类中的equals()
源码如下
public boolean equals(Object obj) {
+ return (this == obj);
+ }
+
在Java规范中,equals()
方法的使用存在如下特性:
x.equals(x); // true
x.equals(y) == y.equals(x); // true
if (x.equals(y) && y.equals(z)) => x.equals(z); // true;
x.equals(y) == x.equals(y); // true
多次调用equals()
方法结果不变null
的比较:x.equals(null); // false;
对任何不是null
的对象x调用 x.equals(null)
结果都为false
==
判断两个值是否相等,基本类型没有 equals()
方法。==
判断两个变量是否引用同一个对象,而 equals()
判断引用的对象是否等价。Integer x = new Integer(1);
+Integer y = new Integer(1);
+System.out.println(x.equals(y)); // true
+System.out.println(x == y); // false
+
equals()
作用是判断两个对象是否相等,但一般有两种情况:
equals
方法,则相当于通过 ==
来比较这两个对象的地址;equals
方法,一般我们通过equals()
来比较两个对象的内容是否相等,相等则返回true;equals()
在不重写的情况下与 ==
作用一样都是比较的内存中的地址.但是equals()
可以重写。
重写equals
方法一般思路:
Object
对象进行转型;public class EqualExample {
+
+ private int x;
+ private int y;
+ private int z;
+
+ public EqualExample(int x, int y, int z) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ EqualExample that = (EqualExample) o;
+
+ if (x != that.x) return false;
+ if (y != that.y) return false;
+ return z == that.z;
+ }
+}
+
在Java中hashcode
方法是Object
类的native
方法,返回值为int类型,根据一定的规则将与对象相关的信息(比如对象的存储地址,对象的字段等)映射成一个数值,这个数值称作为hash值(散列值)。
+++
hashCode
通用约定:+
+- 若
+x.equals(y)
返回true ,则x.hashCode()==y.hashCode()
,其逆命题不一定成立。- 尽量使 hashCode 方法返回的散列码总体上呈均匀分布,可以提高哈希表的性能。
+- 程序运行时,若对象的
+equals
方法中使用的字段没有改变,则在程序结束前,多次调用hashCode
方法都应返回相同的散列码;程序结束后再执行时则没有此要求。
hashCode
方法源码:
public native int hashCode();
+
根据这个方法的声明可知,该方法返回一个int
类型的数值,并且是本地方法,因此在Object
类中并没有给出具体的实现。
对于包含容器类型的程序设计语言来说,基本上都会涉及到hashCode
。在Java中也一样,hashCode
方法的主要作用是为了配合基于散列的集合一起正常运行,这样的散列集合包括HashSet、HashMap
以及HashTable
。
在集合中已经存在上万条数据或者更多的数据场景下向集合中插入对象时,如何判别在集合中是否已经存在该对象了?
+如果采用equals
方法去逐一比较,效率必然是一个问题。此时hashCode
方法的优点就体现出来了。因为两个不同的对象可能会有相同的hashCode
值,所有不能通过hashCode
值来判断两个对象是否相等,但是可以直接根据hashcode
值判断两个对象不等,如果两个对象的hashCode
值不等,则必定是两个不同的对象。
+当集合要添加新的对象时,先调用这个对象的hashCode
方法,得到对应的hashcode
值,如果存放的hash
值中没有该hashcode
值,它就可以直接存进去,不用再进行任何比较了;如果存在该hashcode
值,就调用它的equals
方法与新元素进行比较,相同的话就不存了,不相同就去存。
++需要额外注意的是: +
+设计hashCode()
时最重要的因素就是,无论何时,对同一个对象调用hashCode()
都应该产生同样的值。如果在将一个对象用
+put()
添加进HashMap
时产生一个hashCdoe
值,而用get()
取出时却产生了另一个hashCode
值,那么就无法获取该对象了。 +所以如果你的hashCode
方法依赖于对象中易变的数据,就要当心了,因为此数据发生变化时,hashCode()
方法就会生成一个不同的散列码,从而获取不到该对象。所以在重写
+hashCode
方法和equals
方法的时候,如果对象中的数据易变,则最好在equals
方法和hashCode
方法中不要依赖于该字段。
如下代码
+public class MainTest {
+ public static void main(String[] args) {
+ Person p1 = new Person("lucy", 22);
+ // 85134311
+ System.out.println(p1.hashCode());
+ HashMap<Person, Integer> hashMap = new HashMap<>();
+ hashMap.put(p1, 1);
+ p1.setAge(13);
+ // null
+ System.out.println(hashMap.get(p1));
+ }
+}
+
+class Person {
+ private String name;
+ private int age;
+
+ public Person(String name, int age) {
+ this.name = name;
+ this.age = age;
+ }
+
+ public void setAge(int age) {
+ this.age = age;
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode() * 37 + age;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return this.name.equals(((Person) obj).name) && this.age == ((Person) obj).age;
+ }
+}
+
hashCode()
返回散列值,而equals()
是用来判断两个对象是否等价。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价。
equals()
地址比较是通过对象的哈希值来比较的。hash
值是由hashCode
方法产生的,hashCode
属于Object
类的本地方法,默认使用==
比较两个对象,如果equals()
相等,hashcode
一定相等,如果hashcode
相等,equals
不一定相等。
所以在覆盖 equals()
方法时应当总是覆盖hashCode()
方法,保证等价的两个对象散列值也相等。
下面的代码中,新建了两个等价的对象,并将它们添加到HashSet
中。我们希望将这两个对象当成一样的,只在集合中添加一个对象,但是因为EqualExample
没有实现hashCode()
方法,因此这两个对象的散列值是不同的,最终导致集合添加了两个等价的对象。
public class MainTest {
+ public static void main(String[] args) {
+ EqualExample e1 = new EqualExample(1, 1, 1);
+ EqualExample e2 = new EqualExample(1, 1, 1);
+ // true
+ System.out.println(e1.equals(e2));
+ HashSet<EqualExample> set = new HashSet<>();
+ set.add(e1);
+ set.add(e2);
+ // 2
+ System.out.println(set.size());
+ }
+}
+
所以在覆盖 equals()
方法时应当总是覆盖hashCode()
方法,保证等价的两个对象散列值也相等。
重写hashCode
方法规则:
result
的int类型的常量中true
为1,false
则为0byte、char、short
和int
类型,需要强制转为int的值(int)(f^(f>>32))
Float.floatToIntBits(f)
Double.doubleToLongBits(f)
,再按照long的方法进行计算hashCode
方法(假设其hashCode
满足你的需求)result = result * 31 + c
,返回result
《Effective Java》的作者推荐使用基于17和31的散列码的算法:
+@Override
+public int hashCode() {
+ int result = 17;
+ result = 31 * result + x;
+ result = 31 * result + y;
+ result = 31 * result + z;
+ return result;
+}
+
Java 7新增的Objects
类提供了计算hashCode
的通用方法,可以很简洁实现hashCode
方法:
@Override
+public int hashCode() {
+ return Objects.hash(name,age);
+}
+
toString
方法是Object
类里定义的,返回只类型是String
默认返回类名和它的引用地址:ToStringExample@4554617c
这种形式,其中@后面的数值为散列码的无符号十六进制表示。
Object
类toString
源代码如下:
public String toString() {
+ return getClass().getName() + "@" + Integer.toHexString(hashCode());
+ }
+
当我们打印一个对象的引用时,实际是默认调用这个对象的toString()
方法,当打印的对象所在类没有重写Object
中的toString()
方法时,默认调用的是Object
类中toString()
方法.返回此对象所在的类及对应的堆空间对象实体的首地址值。
public class MainTest {
+ public static void main(String[] args) {
+ // object.ToStringDemo@511d50c0
+ System.out.println(new ToStringDemo());
+ }
+}
+class ToStringDemo {
+ private String name;
+}
+
当我们打印对象所在类重写了toString()
,调用的就是已经重写了的toString()
方法,一般重写是将类对象的属性信息返回。
public class MainTest {
+ public static void main(String[] args) {
+ ToStringDemo toStringDemo = new ToStringDemo();
+ toStringDemo.setName("lucy");
+ // ToStringDemo{name='lucy'}
+ System.out.println(toStringDemo);
+ }
+}
+class ToStringDemo {
+ private String name;
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ return "ToStringDemo{" +
+ "name='" + name + '\'' +
+ '}';
+ }
+}
+
在进行String类与其他类型的连接操作时,自动调用toString()
方法:
public class MainTest {
+ public static void main(String[] args) {
+ Date time = new Date();
+ System.out.println("time = " + time);//相当于下一行代码
+ System.out.println("time = " + time.toString());
+ }
+}
+
在实际应用中,可以根据需要在用户自定义类型中重写toString()
方法:
public class MainTest {
+ public static void main(String[] args) {
+ // null 没有toString()方法 *会报错*
+ Object var0 = null;
+ System.out.println(var0.toString());
+
+ // 布尔型数据true和false返回对应的'true'和'false'
+ Boolean var1 = false;
+ Boolean var2 = true;
+ System.out.println(var1.toString());
+ System.out.println(var2.toString());
+
+ // 字符串类型原值返回
+ String var3 = "string";
+ System.out.println(var3.toString());
+
+ // 正浮点数及NaN、Infinity加引号返回
+ Double var4 = 1.23d;
+ System.out.println(var4.toString());
+ Double nan = Double.NaN;
+ System.out.println(nan.toString());
+ Double negativeInfinity = Double.NEGATIVE_INFINITY;
+ Double positiveInfinity = Double.POSITIVE_INFINITY;
+ System.out.println(negativeInfinity.toString());
+ System.out.println(positiveInfinity.toString());
+
+ // 负浮点数或加'+'号的正浮点数直接跟上.toString(),相当于先运行toString()方法,再添加正负号,转换为数字
+ Double var5 = -1.23d;
+ Double var6 = +1.23d;
+ System.out.println(var5.toString());
+ System.out.println(var6.toString());
+ }
+}
+
基本数据类型转换为String
类型就是调用了对应包装类的toString()
方法:
int i = 10;
+System.out.println("i=" + i);
+
在Java中可以使用clone
方法来创建对象:
protected native Object clone() throws CloneNotSupportedException;
+
如何对对象进行克隆:
+Cloneable
接口,这是一个标记接口,自身没有方法clone()
方法,可见性提升为public
clone()
是 Object
的 protected
方法,它不是被 public
修饰;一个类不显式的去重写clone()
,其它类就不能直接去调用该类实例的 clone()
方法:
public class CloneExample {
+ private int a;
+ private int b;
+}
+CloneExample e1 = new CloneExample();
+// CloneExample e2 = e1.clone(); // 'clone()' has protected access in 'java.lang.Object'
+
重写clone()
方法得到以下实现:
public class CloneExample {
+ private int a;
+ private int b;
+
+ @Override
+ public CloneExample clone() throws CloneNotSupportedException {
+ return (CloneExample)super.clone();
+ }
+}
+
CloneExample e1 = new CloneExample();
+try {
+ CloneExample e2 = e1.clone();
+} catch (CloneNotSupportedException e) {
+ e.printStackTrace();
+}
+
以上抛出了 java.lang.CloneNotSupportedException: CloneExample
,这是因为 CloneExample
没有实现 Cloneable
接口。
应该注意的是,clone()
方法并不是 Cloneable
接口的方法,而是 Object
的一个 protected
方法。
Cloneable
接口只是规定,如果一个类没有实现 Cloneable
接口又调用了 clone()
方法,就会抛出 CloneNotSupportedException
。
public class CloneExample implements Cloneable {
+ private int a;
+ private int b;
+
+ @Override
+ public Object clone() throws CloneNotSupportedException {
+ return super.clone();
+ }
+}
+
clone
,并指向被复制过的新对象。如果一个被复制的属性都是基本类型,那么只需要实现当前类的cloneable
机制就可以了,此为浅拷贝。
如果被复制对象的属性包含其他实体类对象引用,那么这些实体类对象都需要实现cloneable
接口并覆盖clone()
方法。
public class ShallowCloneExample implements Cloneable {
+
+ private int[] arr;
+
+ public ShallowCloneExample() {
+ arr = new int[10];
+ for (int i = 0; i < arr.length; i++) {
+ arr[i] = i;
+ }
+ }
+
+ public void set(int index, int value) {
+ arr[index] = value;
+ }
+
+ public int get(int index) {
+ return arr[index];
+ }
+
+ @Override
+ protected ShallowCloneExample clone() throws CloneNotSupportedException {
+ return (ShallowCloneExample) super.clone();
+ }
+}
+
ShallowCloneExample e1 = new ShallowCloneExample();
+ShallowCloneExample e2 = null;
+try {
+ e2 = e1.clone();
+} catch (CloneNotSupportedException e) {
+ e.printStackTrace();
+}
+e1.set(2, 222);
+System.out.println(e2.get(2)); // 222
+
public class DeepCloneExample implements Cloneable {
+
+ private int[] arr;
+
+ public DeepCloneExample() {
+ arr = new int[10];
+ for (int i = 0; i < arr.length; i++) {
+ arr[i] = i;
+ }
+ }
+
+ public void set(int index, int value) {
+ arr[index] = value;
+ }
+
+ public int get(int index) {
+ return arr[index];
+ }
+
+ @Override
+ protected DeepCloneExample clone() throws CloneNotSupportedException {
+ DeepCloneExample result = (DeepCloneExample) super.clone();
+ result.arr = new int[arr.length];
+ for (int i = 0; i < arr.length; i++) {
+ result.arr[i] = arr[i];
+ }
+ return result;
+ }
+}
+
DeepCloneExample e1 = new DeepCloneExample();
+DeepCloneExample e2 = null;
+try {
+ e2 = e1.clone();
+} catch (CloneNotSupportedException e) {
+ e.printStackTrace();
+}
+e1.set(2, 222);
+System.out.println(e2.get(2)); // 2
+
使用 clone()
方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。《Effective Java》 书上讲到,最好不要去使用 clone()
,可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。
public class CloneConstructorExample {
+
+ private int[] arr;
+
+ public CloneConstructorExample() {
+ arr = new int[10];
+ for (int i = 0; i < arr.length; i++) {
+ arr[i] = i;
+ }
+ }
+
+ public CloneConstructorExample(CloneConstructorExample original) {
+ arr = new int[original.arr.length];
+ for (int i = 0; i < original.arr.length; i++) {
+ arr[i] = original.arr[i];
+ }
+ }
+
+ public void set(int index, int value) {
+ arr[index] = value;
+ }
+
+ public int get(int index) {
+ return arr[index];
+ }
+}
+
CloneConstructorExample e1 = new CloneConstructorExample();
+CloneConstructorExample e2 = new CloneConstructorExample(e1);
+e1.set(2, 222);
+System.out.println(e2.get(2)); // 2
+
finalize()
方法是Java提供的对象终止机制,允许开发人员提供对象被销毁之前的自定义处理逻辑。当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()
方法。
finalize()
方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。
文档注释大意:当GC确定不再有对对象的引用时,由垃圾收集器在对象上调用。子类重写finalize
方法来释放系统资源或执行其他清理。
/**
+ * Called by the garbage collector on an object when garbage collection
+ * determines that there are no more references to the object.
+ * A subclass overrides the {@code finalize} method to dispose of
+ * system resources or to perform other cleanup.
+ */
+ protected void finalize() throws Throwable { }
+
简而言之,finalize
方法是与Java中的垃圾回收器有关系。即:当一个对象变成一个垃圾对象的时候,如果此对象的内存被回收,那么就会调用该类中定义的finalize
方法。
当一个对象可被回收时,就需要执行该对象的 finalize()
方法,那么就有可能在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize()
方法自救,后面回收时不会再调用该方法。
永远不要主动调用某个对象的finalize
方法应该交给垃圾回收机制调用的原因:
finalize
方法时时可能会导致对象复活;finalize
方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finalize
方法将没有执行机会;因为优先级比较低,即使主动调用该方法,也不会因此就直接进行回收;finalize
方法会严重影响GC的性能;由于finalize
方法的存在,虚拟机中的对象一般可能处于三种状态:
如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用了。一般来说,此对象需要被回收。但事实上,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段。一个无法触及的对象有可能在某一个条件下“复活”自己,如果这样,那么对它的回收就是不合理的,为此,虚拟机中定义了的对象可能的三种状态:
+finalize
中复活;对象被复活,对象在finalize
方法中被重新使用;finalize
方法被调用,并且没有复活,那么就会进入不可触及状态;对象死亡,对象没有被使用;只有在对象不可触及时才可以被回收。不可触及的对象不可能被复活,因为finalize()
只会被调用一次。
finalize
对象终止机制判定一个对象能否被回收过程:
判定一个对象是否可回收,至少要经历两次标记过程:
+finalize
方法
+finalize
方法,或者finalize
方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,对象被判定为不可触及的。finalize
方法,且还未执行过,那么会被插入到F-Queue
队列中,由一个虚拟机自动创建的、低优先级的Finalizer
线程触发其finalize
方法执行。finalize
方法是对象逃脱死亡的最后机会,稍后GC会对F-Queue
队列中的对象进行第二次标记。如果对象在finalize
方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,该对象会被移出“即将回收”集合。之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize
方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的finalize
方法只会被调用一次。代码演示对象能否被回收:
+public class MainTest {
+
+ public static MainTest var;
+
+ /**
+ * 此方法只能被调用一次
+ * 可对该方法进行注释,来测试finalize方法是否能复活对象
+ */
+ @Override
+ protected void finalize() throws Throwable {
+ System.out.println("调用当前类重写的finalize()方法");
+ // 复活对象 让当前带回收对象重新与引用链中的对象建立联系
+ var = this;
+ }
+
+ public static void main(String[] args) throws InterruptedException {
+ var = new MainTest();
+ var = null;
+ System.gc();
+ System.out.println("-----------------第一次gc操作------------");
+ // 因为Finalizer线程的优先级比较低,暂停2秒,以等待它
+ Thread.sleep(2000);
+ if (var == null) {
+ System.out.println("对象已经死了");
+ // 如果第一次对象就死亡了 就终止
+ return;
+ } else {
+ System.out.println("对象还活着");
+ }
+
+ System.out.println("-----------------第二次gc操作------------");
+ var = null;
+ System.gc();
+ // 下面代码和上面代码是一样的,但是 对象却自救失败了
+ Thread.sleep(2000);
+ if (var == null) {
+ System.out.println("对象已经死了");
+ } else {
+ System.out.println("对象还活着");
+ }
+ }
+
+}
+
/**
+ * Returns the runtime class of this {@code Object}. The returned
+ * {@code Class} object is the object that is locked by {@code
+ * static synchronized} methods of the represented class.
+ */
+ public final native Class<?> getClass();
+
大意:返回这个对象的运行时类。返回的Class
对象是被表示类的static synchronized
方法锁定的对象。
getClass
方法返回对象运行时的类。返回的类型是Class
类型的对象。可以通过这个Class
对象来创建调用这个方法的对象和执行一些其他操作,这便是反射的入口。
除了可以使用getClass
来获取反射入口外,还有一种方法与getClass()
方法极为相似:获取对象的.class
属性。
二者区别:
+.class
其实是在java运行时就加载进去的,可以说是编译时期就决定好的getClass()
是运行程序时动态加载的之所以把这三个方法放在一起,是因为他们是搭配使用的。
+wait
方法的作用是让当前对象上的线程进入等待状态,同时wait()
也会让当前线程释放它所持有的锁。直到其他线程调用此对象的notify()
方法或 notifyAll()
方法,当前对象上线程被唤醒进入就绪状态。notify()
和notifyAll()
的作用,则是唤醒当前对象上的等待线程;notify()
是(随机)唤醒当前对象上单个线程,而notifyAll()
是唤醒当前对象上所有的线程。wait(long timeout)
方法让当前对象上线程处于等待(阻塞)状态,直到其他线程调用此对象的notify()
方法或notifyAll()
方法,或者超过指定的时间量,当前线程被唤醒进入就绪状态。需要注意的是wait
方法与sleep
方法,很多人分不清他俩。
sleep
和wait
方法异同点:
sleep()
属于Thread
类,wait()
属于Object
类;sleep()
和wait()
都会抛出InterruptedException
异常,这个异常属于checkedException
不可避免;sleep()
不会释放锁,会使线程堵塞,而调用wait()
会释放锁,让线程进入等待状态,用 notify()、notifyall()
可以唤醒,或者等待时间到了; 这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify()
或者 notifyAll()
来唤醒挂起的线程,造成死锁。wait()
必须在同步synchronized
块里使用,sleep()
可以在任意地方使用;其中"wait()
必须在同步synchronized
块里使用",使其不止wait
方法,notify、notifyAll
也和wait
方法一样,必须在synchronized
块里使用,为什么呢?
synchronized
修饰,使用了wait
方法而没有设置等待时间,也没有调用唤醒方法或者唤醒方法调用的时机不对,这个线程将会永远的堵塞下去。wait()、notify、notifyAll
方法调用的时候要释放锁,你都没给它加锁,他怎么释放锁,所以如果没在synchronized
块中调用wait()、notify、notifyAll
方法是肯定抛异常的。+ +
+ + + + + +运算符指明对操作数的运算方式。组成表达式的Java操作符有很多种。运算符按照其要求的操作数数目来分,可以有单目运算符、双目运算符和三目运算符,它们分别对应于1个、2个、3个操作数。
+运算符按其功能来分:有算术运算符、赋值运算符、关系运算符、逻辑运算符、位运算符和其他运算符.
+运算符 | +名称 | +用法描述 | +备注 | +
---|---|---|---|
+ | +加法 | +相加运算符两侧的值 | ++ |
- | +减法 | +左操作数减去右操作数 | ++ |
* | +乘法 | +相乘操作符两侧的值 | ++ |
/ | +除法 | +左操作数除以右操作数 | +右操作数不能为0 | +
% | +取余(模) | +(左操作数除以右操作数)的余数 | ++ |
++ | +自增 | +操作数的值增加1 | ++ |
-- |
+自减 | +操作数的值减少1 | ++ |
基本的+、-、*、/
就不写了.记录一下剩下的算数运算符。
基本用法
+ public static void main(String[] args) {
+ System.out.println(0%5); // 0
+ System.out.println(1%5); // 1
+ System.out.println(2%5); // 2
+ System.out.println(3%5); // 3
+ System.out.println(4%5); // 4
+ System.out.println(5%5); // 0
+ System.out.println("------------");
+ System.out.println(6%5); // 1
+ System.out.println(7%5); // 2
+ System.out.println(8%5); // 3
+ System.out.println(9%5); // 4
+ System.out.println(9%1); // 0
+ }
+
如果被除数小于除数,那取模的结果就是被除数.如果被除数等于除数,结果是0,如果除数是1,结果是0.
+取余应用
+当使用随机数生成器产生的结果时,取余运算(%)可将结果限制在上限为操作数最大值减1的范围.
+例如:n是随机数,那么n%10
就是0~9
中的一个数.无论n是多大的数,n%10
只能是0~9
之间的一个数,其中10就是操作数.可以利用这一特性,可以在数据库分库,分表等.
基本用法
+ public static void main(String[] args) {
+ int i = 1;
+ System.out.println(i++); // 1
+ System.out.println(i--); // 2
+ }
+
需要特别留意的是++
和--
运算符可以前置、后置,都是合法的语句,如a++
和++a
都是合法的,上面这两种写法其最终的结果都是是变量a的值加1了,但是它们之间也是有区别的,其区别是:表达是++a
会先将a的值自增1,然后在使用变量a。而表达式a++是先使用了a的值,然后再让a的值自增1。也就是说在一些表达式,使用a++
和使用++a
得到的结果时不一样的。
public static void main(String[] args) {
+ int i = 1;
+ System.out.println(i++);// 1
+ System.out.println(++i);// 3
+ }
+
自增(++
)和自减(--
)两个运算符只能作用于变量,而不能作用于表达式.
public static void main(String[] args) {
+ int j = 0, i = 1;
+ // 编译报错
+ System.out.println((j+i)++);
+ }
+
运算符 | +名称 | +用法描述 | +
---|---|---|
= | +赋值 | +将右操作数的值赋给左侧操作数 | +
+= | +加等于 | +把左操作数和右操作数相加赋值给左操作数 | +
-= | +减等于 | +把左操作数和右操作数相减赋值给左操作数 | +
*= | +乘等于 | +把左操作数和右操作数相乘赋值给左操作数 | +
/= | +除等于 | +把左操作数和右操作数相除赋值给左操作数 | +
%= | +模等于 | +把左操作数和右操作数取模后赋值给左操作数 | +
<<= |
+左位移等于 | +把左操作数和右操作数进行左移运算后赋值给左操作数 | +
>>= |
+右位移等于 | +把左操作数和右操作数进行右移运算后赋值给左操作数 | +
&= | +按位与等于 | +把左操作数和右操作数进行按位与运算后赋值给左操作数 | +
|= | +按位或等于 | +把左操作数和右操作数进行按位或运算后赋值给左操作数 | +
^= | +异或等于 | +把左操作数和右操作数进行按位异或运算后赋值给左操作数 | +
关系运算符也称为比较运算符.
+用于测试两个操作数之间的关系.使用关系运算符表达式的最终结果为boolean
型,也就是其结果只有两个true
和false
.
运算符 | +名称 | +用法描述 | +
---|---|---|
== | +双等号 | +检查两个操作数的值是否相等,如果相等则条件为真. | +
!= | +不等号 | +检查两个操作数的值是否相等,如果值不相等则条件为真. | +
> | +大于 | +检查左操作数的值是否大于右操作数的值,如果是那么条件为真. | +
< | +小于 | +检查左操作数的值是否小于右操作数的值,如果是那么条件为真. | +
>= | +大于等于 | +检查左操作数的值是否大于或等于右操作数的值,如果是那么条件为真. | +
<= | +小于等于 | +检查左操作数的值是否小于或等于右操作数的值,如果是那么条件为真. | +
运算符 | +名称 | +用法描述 | +
---|---|---|
&& | +逻辑与 | +当且仅当两个操作数都为真,条件才为真. | +
|| | +逻辑或 | +如果两个操作数任何一个为真,条件为真. | +
! | +逻辑非 | +用来反转操作数的逻辑状态.如果条件为true,通过逻辑非将得到false. | +
&&
运算符,运算顺序是从左到右计算,运算规则是如果两个操作数都是真,则返回true
,否则返回false
.但是当判定到第一个操作数为false
时,其结果必定为false
,这时候就不再会判定第二个操作数了.
public static void main(String[] args) {
+ int i = 1, j = 2;
+ boolean flag = (i++ == 2) && (++j == 3);
+ // flag的值: false,i的值:2,j的值:2
+ System.out.printf("flag的值: %s,i的值:%s,j的值:%s", flag, i, j);
+ }
+
位运算符在追求代码效率和编写底层应用时,使用的比较多;在企业Java开发一般用到的较少.
+因为位运算符是以bit
运算单位的.所以想要要弄明白位运算符,就要先弄明白2进制的表示方法.
位运算符只能对整数型(int,long,short,byte
)和字符型数据(char
)进行操作.
+++
>>
,右移几位就是相当于除以2的几次幂 +<<
,左移几位就是相当于乘以2的几次幂 +%
,当b为2的n次方时,有如下替换公式:a % b = a & (b-1)
运算符 | +名称 | +用法描述 | +
---|---|---|
& | +按位与 | +如果相对应位都是1,则结果为1,否则为0 | +
| | +按位或 | +如果相对应位都是 0,则结果为 0,否则为 1 | +
^ | +按位异或 | +如果相对应位值相同,则结果为0,否则为1 | +
~ | +按位取反 | +翻转操作数的每一位,即0变成1,1变成0. | +
<< |
+左移 | +左操作数按位左移右操作数指定的位数. | +
>> |
+右移 | +左操作数按位右移右操作数指定的位数. | +
>>> |
+无符号右移 | +左操作数的值按右操作数指定的位数右移,移动得到的空位以零填充. | +
基本用法
+ public static void main(String[] args) {
+ int i = 1, j = 2;
+ System.out.println(i&j);//0
+ System.out.println(i|j);//3
+ System.out.println(i^j);//3
+ System.out.println(~(j+i));//-4
+ System.out.println(i<<j);//4
+ System.out.println(i>>j);//0
+ System.out.println(i>>>j);//0
+ }
+
解析:
+以下示例用十进制的1和2进行运算.1用二进制表示为0000 0001
,2用二进制表示为0000 0010
10进制2进制互相转换怎么转就不讲了.不懂的小伙伴可自行查看链接.
&:如果相对应位都是1,则结果为1,否则为0
+0000 0001
+0000 0010
+——————————
+0000 0000
+
|:如果相对应位都是 0,则结果为 0,否则为 1
+0000 0001
+0000 0010
+——————————
+0000 0011
+
^:如果相对应位值相同,则结果为0,否则为1
+0000 0001
+0000 0010
+——————————
+0000 0011
+
~:翻转操作数的每一位,即0变成1,1变成0.
+++十进制负数转换为二进制,就是将其相反数(正数)的补码的每一位变反(1变0,0变1)最后将变完了的数值加1,就完成了负数的补码运算.这样就变成了二进制.二进制转十进制负数相反.
+
0000 0011
+——————————
+1111 1100
+
<<
:左操作数按位左移右操作数指定的位数.
1左移两位
+0000 0001
+——————————
+0000 0100
+
>>
:左操作数按位右移右操作数指定的位数.
正数右移高位补0,负数右移高位补1
+1右移两位
+0000 0001
+——————————
+0000 0000
+
负一右移两位
+1111 1111
+——————————
+1111 1111
+
>>>
:左操作数的值按右操作数指定的位数右移,移动得到的空位以零填充.
无符号右移,无论是正数还是负数,高位通通补0
+1无符号右移两位
+0000 0001
+——————————
+0000 0000
+
负一无符号右移两位
+1111 1111
+——————————
+0011 1111
+
该运算符有3个操作数,其功能是对表达式条件进行判断,根据判断结果是true
或者false
两种情况赋予赋予不同的返回值.
+该运算符的主要是决定哪个值应该赋值给变量.
在某些情况下,可以发现三目运算符和if...else
作用是相同的.
public static void main(String[] args) {
+ int var1 = 1;
+ int var2 = 0;
+ var1 = var2 > 1 ? 1: 2;
+
+ // 等价于if...else以下代码
+ if (var2 > 1) {
+ var1 = 1;
+ }else {
+ var1 = 2;
+ }
+ }
+
三目运算符的原理到底是什么,与if...else
到底哪有啥不同?要探究这个原因,需要看JVM是对三目运算符怎么处理的.
用javap -verbose
命令,分别把if...else
和三目运算符相关代码进行反编译
三目运算符
+ public static void main(String[] args) {
+ int var1 = 1;
+ int var2 = 0;
+ // 如果var2大于1 则var1等于1 否则等于2
+ var1 = var2 > 1 ? 1: 2;
+ }
+
// ...
+{
+ // ...
+ public static void main(java.lang.String[]);
+ descriptor: ([Ljava/lang/String;)V
+ flags: ACC_PUBLIC, ACC_STATIC
+ Code:
+ stack=2, locals=3, args_size=1
+ 0: iconst_1 // 向栈里添加int类型 1
+ 1: istore_1 // 将int 1 存储到局部变量
+ 2: iconst_0 // 向栈里添加int类型 0
+ 3: istore_2 // 将int 2 存储到局部变量
+ 4: iload_2 // 从局部变量加载int 2
+ 5: iconst_1
+ 6: if_icmple 13 // 将1,3步骤栈中的变量弹出 进行比较
+ 9: iconst_1 // 比较成功 存储该值到栈中
+ 10: goto 14 // 改变地址
+ 13: iconst_2
+ 14: istore_1
+ 15: return
+ LineNumberTable:
+ line 6: 0
+ line 7: 2
+ line 8: 4
+ line 9: 15
+ LocalVariableTable:
+ Start Length Slot Name Signature
+ 0 16 0 args [Ljava/lang/String;
+ 2 14 1 var1 I
+ 4 12 2 var2 I
+ StackMapTable: number_of_entries = 2
+ frame_type = 253 /* append */
+ offset_delta = 13
+ locals = [ int, int ]
+ frame_type = 64 /* same_locals_1_stack_item */
+ stack = [ int ]
+}
+// ...
+
if...else
public static void main(String[] args) {
+ int var1 = 1;
+ int var2 = 0;
+ if (var2 > 1){
+ var1 = 1;
+ } else {
+ var1 = 2;
+ }
+ }
+
// ...
+ public static void main(java.lang.String[]);
+ descriptor: ([Ljava/lang/String;)V
+ flags: ACC_PUBLIC, ACC_STATIC
+ Code:
+ stack=2, locals=3, args_size=1
+ 0: iconst_1
+ 1: istore_1
+ 2: iconst_0
+ 3: istore_2
+ 4: iload_2
+ 5: iconst_1
+ 6: if_icmple 14
+ 9: iconst_1
+ 10: istore_1
+ 11: goto 16
+ 14: iconst_2
+ 15: istore_1
+ 16: return
+ LineNumberTable:
+ line 6: 0
+ line 7: 2
+ line 8: 4
+ line 9: 9
+ line 11: 14
+ line 13: 16
+ LocalVariableTable:
+ Start Length Slot Name Signature
+ 0 17 0 args [Ljava/lang/String;
+ 2 15 1 var1 I
+ 4 13 2 var2 I
+ StackMapTable: number_of_entries = 2
+ frame_type = 253 /* append */
+ offset_delta = 14
+ locals = [ int, int ]
+ frame_type = 1 /* same */
+}
+// ...
+
通过查阅《Java虚拟机指令集》,可以看到JVM对上边的代码进行了什么处理.
+通过比较两次反编译后的代码可以得出一个结论:三目运算符,省去了一步赋值操作 所以实际开发中三目运算符的运算效率略高于if..else
.
+但是对于实际开发来说三目运算符的可读性相对不如if...else
代码,所以在常见的编码中,三目运算符更倾向于简单的if...else
语句的替代.
interfaceof
是一个双目运算符,该关键字的作用是判断左边的对象是不是右边类的实例,并返回一个boolean
值
基本用法
+ public static void main(String[] args) {
+ String str1 = "123";
+ String str = (str1 instanceof Object) ? "123" : "456";
+ }
+
Java 语言中大部分运算符也是从左向右结合的,只有单目运算符、赋值运算符和三目运算符例外,是从右向左运算的.
+Java 语言中运算符的优先级共分为 14 级,其中 1 级最高,14 级最低.在同一个表达式中运算符优先级高的先执行.
+++算术运算符>比较运算符>赋值运算符>逻辑运算符>三元运算符
+
+可使用以下口诀记住: +单目乘除为关系,逻辑三目后赋值.
运算符优先级表
+优先级 | +运算符 | +结合性 | +
---|---|---|
1 | +()、[] | +从左向右 | +
2 | +!、+、-、~、++、– | +从右向左 | +
3 | +*、/、% | +从左向右 | +
4 | ++、- | +从左向右 | +
5 | +«、»、»> | +从左向 | +
6 | +<、<=、>、>=、instanceof | +从左向右 | +
7 | +==、!= | +从左向右 | +
8 | +& | +从左向右 | +
10 | +| | +从左向右 | +
11 | +&& | +从左向右 | +
12 | +|| | +从左向右 | +
13 | +?: | +从右向左 | +
14 | +=、+=、-=、*=、/=、&=、|=、^=、~=、«=、»=、»>= | +从右向左 | +
建议
+表达式是由运算符和运算对象组成的,单独的一个运算对象(常量/变量)也可以叫做表达式.
+变量的赋值与计算都离不开表达式,表达式的运算依赖于变量、常量和运算符.
+在Java中表达式通常是以分号结尾的一段代码.
+float f1 = 1.1f;
+float f2 = 1.2f;
+float f3 = f1 + f2;
+
++正则表达式,又称规则表达式。(英语:Regular Expression,在代码中常简写为regex、regexp或RE),计算机科学的一个概念。正则表达式通常被用来检索、替换那些符合某个模式(规则)的文本。在众多语言中都可以支持正则表达式,如
+Perl、PHP、Java、Python、Ruby
等.
正则表达式并不仅限于某一种语言,但是在每种语言中有细微的差别.
+例如:
+++在其他语言中,
+\\
表示:我想要在正则表达式中插入一个普通的(字面上的)反斜杠.
+在 Java 中,\\
表示:我要插入一个正则表达式的反斜线,所以其后的字符具有特殊的意义。
+所以,在其他的语言中,一个反斜杠\
就足以具有转义的作用,而在 Java 中正则表达式中则需要有两个反斜杠才能被解析为其他语言中的转义作用。也可以简单的理解在 Java 的正则表达式中,两个\\
代表其他语言中的一个 \,这也就是为什么表示一位数字的正则表达式是\\d
,而表示一个普通的反斜杠是\\\\
。
正则表达式是由普通字符(如英文字母)以及特殊字符(也称为元字符)组成的文字模式.
+例如:
+String str = "abc^123/?[1,2]";
+
在Java语言中主要是使用正则表达式处理字符串.Java从jdk1.4开始提供了一个包java.util.regex
来处理正则表达式.
在Java开发中主要使用java.util.regex
包中Pattern
类,Matcher
类来处理字符串.
+详情参照文档《Java™平台
+标准Ed. 8》,《菜鸟教程》Java正则表达式 ,《JavaSchool》Java正则表达式 这些文档里面有详细的教程,所以这里不作过多介绍了.
使用正则表达式示例
+ public static void main(String[] args) {
+ // 表示匹配以字母a为开头的单词
+ String regx = "\\ba\\w*\\b";
+ // 将给定的正则表达式编译到具有给定标志的模式中
+ Pattern pattern = Pattern.compile(regx);
+ // 创建匹配给定输入与此模式的匹配器
+ Matcher matcher = pattern.matcher("abcdab cccabcd aaacd");
+ int index = 0;
+ // 循环 查找与上边正则匹配的字符序列
+ while (matcher.find()) {
+ // 返回 由以前匹配操作 所匹配的 输入子序列
+ String res = matcher.group();
+ System.out.println(index + ":" + res);
+ index++;
+ }
+ }
+
0:abcdab
+1:aaacd
+
总的来说正则表达式是对字符串操作的一种逻辑公式,用事先定义好的一些特定字符、及这些特定字符的组合,组成一个"规则字符串",这个"规则字符串"用来表达对字符串的一种过滤逻辑.
+正则表达式的灵活性、逻辑性和功能性非常的强.可以迅速地用极简单的方式达到字符串的复杂控制.但是对于刚接触的人来说,比较晦涩难懂.
+描述 | +正则表达式 | +
---|---|
是否为数字 | +^[0-9]*$ |
+
是否为n位数数字 | +^\d{n}$ |
+
是否为m-n位的数字 | +^\d{m,n}$ |
+
是否输入至少n位的数字 | +^/d{n,}$" |
+
是否为整数 | +^-?/d+$ |
+
是否为负整数 | +^-[0-9]*[1-9][0-9]*$ |
+
是否为正整数 | +^[0-9]*[1-9][0-9]*$ |
+
是否为汉字 | +^[\u4e00-\u9fa5]{0,}$ |
+
是否为邮箱 | +^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$ |
+
是否为域名 | +[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(/.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+/.? |
+
是否为URL | +^http://([\w-]+\.)+[\w-]+(/[\w-./?%&=]*)?$ |
+
是否为手机号码 | +`^(13[0-9] | +
是否为固话号码 | +`^($$\d{3,4}-) | +
是否为身份证号码 | +`^([0-9]){7,18}(x | +
是否以字母开头,长度在6~18之间,只能包含字母、数字和下划线 | +^[a-zA-Z]\w{5,17}$ |
+
是否必须包含大小写字母和数字的组合,不能使用特殊字符,长度在8-10之间 | +^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,10}$ |
+
是否为腾讯QQ号码 | +[1-9][0-9]{4,} (QQ号从10000开始) |
+
是否为中国邮政编码 | +[1-9]\d{5}(?!\d) |
+
是否为ip地址 | +`((25[0-5] | +
if
语句是最基本的控制语句,它只有在If(exception)
为true
的时候才会执行特定的代码.
public static void main(String[] args) {
+ boolean var1 = true;
+ if (var1){
+ System.out.println("hello world ...");
+ }
+ }
+
if
语句后面可以跟else
语句.当If(exception)
为false
时,else
语句体将被执行.
public static void main(String[] args) {
+ boolean var1 = false;
+ if (var1){
+ System.out.println("if ...");
+ }else{
+ System.out.println("else ...");
+ }
+ }
+
if
语句后面可以跟 else if…else
语句
public class HelloWorld {
+ public static void main(String[] args) {
+ String var = "123";
+ if ("123".equals(var)){
+ System.out.println("123 ...");
+ }else if("234".equals(var)){
+ System.out.println("234 ...");
+ }else if ("345".equals(var)){
+ System.out.println("345 ...");
+ }else{
+ System.out.println("...");
+ }
+ }
+
+}
+
switch
语句是在项目开发中是比较常用的.可以和if...else
起到相同的作用,但是用switch
代码可读性更高.
switch case
语句判断一个变量与一系列值中某个值是否相等,每个值称为一个分支.
switch
语句可以包含一个default
分支. default
在没有case
语句的值和变量值相等的时候执行.default
分支不需要break
语句。
public static void main(String[] args) {
+ String s = "123";
+ switch (s) {
+ case "123":
+ System.out.println("123");
+ break;
+ case "456":
+ System.out.println("456");
+ break;
+ default :
+ System.out.println("over ...");
+ }
+ }
+
switch
语句中的变量类型可以是: byte,short,int,char
或者 enum
从 Java 7 开始,可以在switch
条件判断语句中使用String
对象.
switch 不支持 long,是因为 switch
的设计初衷是对那些只有少数的几个值进行等值判断,如果值过于复杂,那么还是用 if
比较合适。
long x = 111;
+ switch (x) {
+ case 111:
+ System.out.println(111);
+ break;
+ case 222:
+ System.out.println(222);
+ break;
+}
+
当变量的值与case
语句的值相等时,那么case
语句之后的语句开始执行,直到break
语句出现才会跳出switch
语句.如果没有break
语句出现,程序会继续执行下一条case
语句,直到出现break
语句.
public static void main(String[] args) {
+ int i = 100;
+ switch (i){
+ case 100:
+ System.out.println("first case ...");
+ case 200:
+ System.out.println("second case ...");
+ break;
+ }
+ }
+
for循环执行的次数是在执行前就确定的.
+for语句提供了一种紧凑的方法来遍历一系列值。程序员经常将其称为“for循环”,因为它反复循环直到满足特定条件。for语句的一般形式可以表示为:
+ public static void main(String[] args) {
+ for (int i = 0; i < 100; i++) {
+ System.out.println(i);
+ }
+ }
+
for循环的三个表达式是可选的;可以这样创建一个无限循环:
+/ /无限循环
+for(;;){
+
+ // to do ...
+}
+
foreach
语句是java5的新特征之一,在遍历数组、集合方面,为开发人员提供了极大的方便.
foreach
循环的效率大概是普通for循环效率的一半 ,但在项目开发中如果只是少量的循环,可以忽略foreach
带来的影响.
public static void main(String[] args) {
+ String[] arr = {"1", "2", "3"};
+ ArrayList<String> list = new ArrayList<>(Arrays.asList(arr));
+ for (String s : list) {
+ System.out.println(s);
+ }
+ for (String s : arr) {
+ System.out.println(s);
+ }
+ }
+
分支语句break
,continue
和return
continue
语句用来结束当前循环,并进入下一次循环,即仅仅这一次循环结束了,不是所有循环结束了,后边的循环依旧进行.
public static void main(String[] args) {
+ String[] arr = {"1", "2", "3"};
+ for (String s : arr) {
+ // 如果s等于1,则结束本次循环 执行下次循环
+ if ("1".equals(s)) {
+ continue;
+ }
+ System.out.println("...");
+ }
+ }
+
break
语句作用是跳出循环.break
主要用在循环语句或者switch
语句中
public class HelloWorld {
+ public static void main(String[] args) {
+ String[] arr = {"1", "2", "3"};
+ for (String s : arr) {
+ // s等于2,结束for循环
+ if ("2".equals(s)) {
+ break;
+ }
+ System.out.println("...");
+ }
+ }
+
+}
+
如果存在多层循环,要注意break
只能跳出一层循环.
public static void main(String[] args) {
+ String[] arr = {"1", "2", "3"};
+ for (int i = 0; i < 10; i++) {
+ for (String s : arr) {
+ if ("2".equals(s)) {
+ break;
+ }
+ System.out.println("...");
+ }
+ System.out.println("外层 for ...");
+ }
+ }
+
如果存在多层循环,可以用以下方式,当然也可以用break
跳两次循环
public static void main(String[] args) {
+ String[] arr = {"1", "2", "3"};
+
+ test:
+ for (int i = 0; i < 10; i++) {
+ for (String s : arr) {
+ if ("2".equals(s)) {
+ break test;
+ }
+ System.out.println("...");
+ }
+ System.out.println("外层 for ...");
+ }
+ }
+
continue
也可以用这种方式
public static void main(String[] args) {
+ String[] arr = {"1", "2", "3"};
+
+ test:
+ for (int i = 0; i < 10; i++) {
+ for (String s : arr) {
+ if ("1".equals(s)) {
+ continue test;
+ }
+ }
+ System.out.println("外层 for ...");
+ }
+ }
+
return
语句表示从当前方法退出,控制流返回到调用方法的地方。
上面continue
的示例,用return
也能达到一样的效果
public static void main(String[] args) {
+ String[] arr = {"1", "2", "3"};
+
+ for (int i = 0; i < 10; i++) {
+ for (String s : arr) {
+ if ("1".equals(s)) {
+ return;
+ }
+ }
+ System.out.println("外层 for ...");
+ }
+ }
+
return
语句有两种形式:一种返回值,另一种不返回值。要返回一个值,只需将值(或计算该值的表达式)放在return
关键字之后.
像这样
+return ++count;
+
while
语句对表达式进行计算,表达式必须返回一个布尔值。如果表达式的计算结果为true
,while
语句将执行while
块中的语句。while
语句继续测试表达式并执行其块,直到表达式的计算结果为false
.
public static void main(String[] args) {
+ int i = 0;
+ while(++i >= 1){
+ i--;
+ }
+ }
+
死循环
+ public static void main(String[] args) {
+ while (true) {
+ }
+ }
+
do…while
循环和while
循环相似,不同的是do…while
循环至少会执行一次.
public static void main(String[] args) {
+ int i = 0;
+ do{
+ i++;
+ System.out.println(" ... ");
+ }while (--i == 1);
+ }
+
注意:布尔表达式在循环体的后面,所以语句块在检测布尔表达式之前已经执行了.如果布尔表达式的值为true
,则语句块一直执行,直到布尔表达式的值为false
++形式参数:是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传入的参数。
+
++实际参数:在调用有参函数时,主调函数和被调函数之间有数据传递关系。在主调函数中调用一个函数时,函数名后面括号中的参数称为“实际参数”。
+
举个例子:
+public class HelloWorld {
+ public static void main(String[] args) {
+ // 实参
+ test("world");
+ }
+
+ // 形参
+ public static void test(String param){
+ System.out.println("hello " + param);
+ }
+}
+
++值传递: 是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
+
++引用传递: 是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
+
… | +值传递 | +引用传递 | +
---|---|---|
区别 | +传参时会创建副本 | +传参数不创建副本 | +
描述 | +方法中无法修改原始对象 | +方法中可以修改原始对象 | +
很多人理解Java中,传递的参数如果是普通类型,那就是值传递,如果是对象,那就是引用传递.这个理解是错误的.
+举个例子:
+如果在方法中改变对象的字段值会改变原对象该字段值,因为改变的是同一个地址指向的内容。
+class PassByValueExample {
+ public static void main(String[] args) {
+ Dog dog = new Dog("A");
+ func(dog);
+ System.out.println(dog.getName()); // B
+ }
+
+ private static void func(Dog dog) {
+ dog.setName("B");//dog.name="B";
+ }
+}
+
然后有人就说Java传递对象类型参数是引用传递.这一点在官方《Java™教程》中有相关的描述.
+++Passing Reference Data Type Arguments. +Reference data type parameters, such as objects, are also passed into methods by value. This means that when the method returns, the passed-in reference still references the same object as before. However, the values of the object’s fields can be changed in the method, if they have the proper access level.
+
传递引用数据类型参数。 +引用数据类型参数(如对象)也按值传递给方法。这意味着,当方法返回时,传入的引用仍然引用与之前相同的对象。但是,如果对象的字段具有适当的访问级别,则可以在方法中更改它们的值。
+++例如,考虑任意类中的一个移动Circle对象的方法:
+
public void moveCircle(Circle circle, int deltaX, int deltaY) {
+ // code to move origin of circle to x+deltaX, y+deltaY
+ circle.setX(circle.getX() + deltaX);
+ circle.setY(circle.getY() + deltaY);
+
+ // code to assign a new reference to circle
+ circle = new Circle(0, 0);
+}
+
++使用以下参数调用该方法:
+
moveCircle(myCircle, 23, 56)
+
++在这个方法中,circle最初指的是
+myCircle
。该方法将circle
引用的对象(即myCircle
)的x坐标和y坐标分别更改了23和56。当方法返回时,这些更改将持续存在。然后circle
被赋予一个新的circle
对象x = y = 0的引用。但是,这种重新分配不具有持久性,因为引用是按值传入的,不能更改。在方法中,circle指向的对象已经改变,但是,当方法返回时,myCircle
仍然引用与方法调用之前相同的circle
对象。
值传递和引用传递最大的区别是传递的过程中有没有复制出一个副本来,如果是传递副本,那就是值传递,否则就是引用传递。 +在Java中,其实是通过值传递实现的参数传递,只不过对于Java对象的传递,传递的内容是对象的引用。 +所以说 Java的参数是以值传递的形式传入方法中,而不是引用传递.
+Java对象的传递,是通过复制的方式把引用关系传递了,如果我们没有改引用关系,而是找到引用的地址,把里面的内容改了,是会对调用方有影响的,因为大家指向的是同一个共享对象。
+以下代码中Dog
类中的dog
是一个指针,存储的是对象的地址.
public class Dog {
+
+ String name;
+
+ Dog(String name) {
+ this.name = name;
+ }
+
+ String getName() {
+ return this.name;
+ }
+
+ void setName(String name) {
+ this.name = name;
+ }
+
+ String getObjectAddress() {
+ return super.toString();
+ }
+}
+public class PassByValueExample {
+ public static void main(String[] args) {
+ Dog dog = new Dog("A");
+ // Dog@4554617c
+ System.out.println(dog.getObjectAddress());
+ func(dog);
+ // Dog@4554617c
+ System.out.println(dog.getObjectAddress());
+ // A
+ System.out.println(dog.getName());
+ }
+
+ private static void func(Dog dog) {
+ // Dog@4554617c
+ System.out.println(dog.getObjectAddress());
+ //重新new生成新的对象
+ dog = new Dog("B");
+ // Dog@74a14482
+ System.out.println(dog.getObjectAddress());
+ // B
+ System.out.println(dog.getName());
+ }
+}
+
在Java中,使用浮点类型进行计算会造成精度丢失
+例如:
+ public static void main(String[] args) {
+ // 0.20000005
+ System.out.println(1.2f - 1);
+ // 0.19999999999999996
+ System.out.println(1.2d - 1);
+ }
+
那么为什么会浮点类型会存在精度精度丢失问题呢?
+因为Java的浮点类型在计算机中是用二进制来存储的,也就是小数在转二进制的时候出现了精度丢失.
+PS: 小数如何转二进制
+++将该数字乘以2,取出整数部分作为二进制表示的第1位;然后再将小数部分乘以2,将得到的整数部分作为二进制表示的第2位;以此类推,直到小数部分为0.
+
+特殊情况: 小数部分出现循环,无法停止,则用有限的二进制位无法准确表示一个小数,这也是在编程语言中表示小数会出现误差的原因.
例如: 0.1 转二进制
+ 0.1 转2进制
+
+ 0.1 x 2 = 0.2 取整数位 0
+ 0.2 x 2 = 0.4 取整数位 0
+ 0.4 x 2 = 0.8 取整数位 0
+ 0.8 x 2 = 1.6 取整数位 1
+ 0.6 x 2 = 1.2 取整数位 1
+ 0.2 x 2 = 0.4 取整数位 0
+ 0.4 x 2 = 0.8 取整数位 0
+ 0.8 x 2 = 1.6 取整数位 1
+ 0.6 x 2 = 1.2 取整数位 1
+
+ ........无限循环
+
因为计算机中存储一个浮点类型所用的位数是有限的,所以遇到无限循环的小数,只能选择在某个精度进行保存.
+由于计算机中保存的小数其实是十进制的小数的近似值,并不是准确值,所以,千万不要在代码中使用浮点数来表示金额等重要的指标。
+++Java在
+java.math
包中提供的API类BigDecimal
,用来对超过16位有效位的数进行精确的运算。双精度浮点型变量double可以处理16位有效数。在实际应用中,需要对更大或者更小的数进行运算和处理。float
和double
只能用来做科学计算或者是工程计算,在商业计算中要用java.math.BigDecimal
。BigDecimal
所创建的是对象,我们不能使用传统的+、-、*、/
等算术运算符直接对其对象进行数学运算,而必须调用其相对应的方法。方法中的参数也必须是BigDecimal
的对象。构造器是类的特殊方法,专门用来创建对象,特别是带有参数的对象.
Java中提供了大数字(超过16位有效位)的操作类,即java.math.BinInteger
类和java.math.BigDecimal
类,用于高精度计算.
其中BigInteger
类是针对大整数的处理类,而BigDecimal
类则是针对大小数的处理类.BigDecimal
类的实现用到了 BigDecimal
BigInteger类,不同的是BigDecimal
加入了小数的概念.
之所以要用BigDecimal
,是因为十进制的小数在转化成二进制浮点数时会精度丢失.
BigDecimal
类创建的是对象,不能使用传统的+、-、*、/
等算术运算符直接对其进行数学运算,而必须调用其对应的方法.方法的参数也必须是BigDecimal
类型的对象.
public static void main(String[] args) {
+ BigDecimal num1 = new BigDecimal("2");
+ BigDecimal num2 = new BigDecimal("1");
+ BigDecimal num3;
+
+ num3 = num1.add(num2);
+ System.out.printf("num1 + num2 = %s\n",num3);
+
+ num3 = num1.subtract(num2);
+ System.out.printf("num1 - num2 = %s\n",num3);
+
+ num3 = num1.multiply(num2);
+ System.out.printf("num1 * num2 = %s\n",num3);
+
+ num3 = num1.divide(num2);
+ System.out.printf("num1 / num2 = %s\n",num3);
+
+ // 绝对值
+ System.out.printf("|num1 / num2| = %s\n",num3.abs());
+ }
+
BigDecimal
基本用法如上所示,重点记录一下除法.
在使用除法的时候如果两个数字,除不尽.而又没有设置精确小数位和舍入模式,就会报错.
+ public static void main(String[] args) {
+ BigDecimal num1 = new BigDecimal("1");
+ BigDecimal num2 = new BigDecimal("3");
+ BigDecimal num3;
+
+ num3 = num1.divide(num2);
+ System.out.printf("num1 / num2 = %s\n",num3);
+ }
+
Exception in thread "main" java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
+
为了防止报错,我们可以这样写
+ public static void main(String[] args) {
+ BigDecimal num1 = new BigDecimal("1");
+ BigDecimal num2 = new BigDecimal("3");
+ BigDecimal num3;
+
+ num3 = num1.divide(num2,3, BigDecimal.ROUND_UP);
+ // 打印结果: num1 / num2 = 0.334
+ System.out.printf("num1 / num2 = %s\n",num3);
+ }
+
BigDecimal
类divide
方法
// divisor: 除数; scale: 精确小数位; roundingMode: 舍入模式
+public BigDecimal divide(BigDecimal divisor, int scale, int roundingMode)
+
舍入模式,如果不指定默认采用四舍五入方式.
+/**
+ * Rounding mode to round away from zero. Always increments the
+ * digit prior to a nonzero discarded fraction. Note that this rounding
+ * mode never decreases the magnitude of the calculated value.
+ * 向远离0的方向舍入 如2.35 --> 2.4
+ */
+ public final static int ROUND_UP = 0;
+
+ /**
+ * Rounding mode to round towards zero. Never increments the digit
+ * prior to a discarded fraction (i.e., truncates). Note that this
+ * rounding mode never increases the magnitude of the calculated value.
+ * 向零方向舍入 直接删除多余小数位 如2.35 --> 2.3
+ */
+ public final static int ROUND_DOWN = 1;
+
+ /**
+ * Rounding mode to round towards positive infinity. If the
+ * {@code BigDecimal} is positive, behaves as for
+ * {@code ROUND_UP}; if negative, behaves as for
+ * {@code ROUND_DOWN}. Note that this rounding mode never
+ * decreases the calculated value.
+ * 向正无穷方向舍入
+ */
+ public final static int ROUND_CEILING = 2;
+
+ /**
+ * Rounding mode to round towards negative infinity. If the
+ * {@code BigDecimal} is positive, behave as for
+ * {@code ROUND_DOWN}; if negative, behave as for
+ * {@code ROUND_UP}. Note that this rounding mode never
+ * increases the calculated value.
+ * 向负无穷方向舍入
+ */
+ public final static int ROUND_FLOOR = 3;
+
+ /**
+ * Rounding mode to round towards {@literal "nearest neighbor"}
+ * unless both neighbors are equidistant, in which case round up.
+ * Behaves as for {@code ROUND_UP} if the discarded fraction is
+ * ≥ 0.5; otherwise, behaves as for {@code ROUND_DOWN}. Note
+ * that this is the rounding mode that most of us were taught in
+ * grade school.
+ * 向(距离)最近的一边舍入,除非两边(的距离)是相等,如果是这样,向上舍入, 1.55保留一位小数结果为1.6
+ */
+ public final static int ROUND_HALF_UP = 4;
+
+ /**
+ * Rounding mode to round towards {@literal "nearest neighbor"}
+ * unless both neighbors are equidistant, in which case round
+ * down. Behaves as for {@code ROUND_UP} if the discarded
+ * fraction is {@literal >} 0.5; otherwise, behaves as for
+ * {@code ROUND_DOWN}.
+ * 向(距离)最近的一边舍入,除非两边(的距离)是相等,如果是这样,向下舍入, 例如1.55 保留一位小数结果为1.5
+ */
+ public final static int ROUND_HALF_DOWN = 5;
+
+ /**
+ * Rounding mode to round towards the {@literal "nearest neighbor"}
+ * unless both neighbors are equidistant, in which case, round
+ * towards the even neighbor. Behaves as for
+ * {@code ROUND_HALF_UP} if the digit to the left of the
+ * discarded fraction is odd; behaves as for
+ * {@code ROUND_HALF_DOWN} if it's even. Note that this is the
+ * rounding mode that minimizes cumulative error when applied
+ * repeatedly over a sequence of calculations.
+ * 向(距离)最近的一边舍入,除非两边(的距离)是相等,如果是这样,如果保留位数是奇数,使用ROUND_HALF_UP ,如果是偶数,使用ROUND_HALF_DOWN
+ */
+ public final static int ROUND_HALF_EVEN = 6;
+
+ /**
+ * Rounding mode to assert that the requested operation has an exact
+ * result, hence no rounding is necessary. If this rounding mode is
+ * specified on an operation that yields an inexact result, an
+ * {@code ArithmeticException} is thrown.
+ * 计算结果是精确的,不需要舍入模式
+ */
+ public final static int ROUND_UNNECESSARY = 7;
+
所以我们在使用BigDecimal
除法的时候,最好要指定精确小数位和舍入模式.
BigDecimal
一共有两种方法可以进行初始化赋值
valueOf
方法初始化赋值valueOf(double val)
底层也是调用String
构造
public static BigDecimal valueOf(double val) {
+ // Reminder: a zero double returns '0.0', so we cannot fastpath
+ // to use the constant ZERO. This might be important enough to
+ // justify a factory approach, a cache, or a few private
+ // constants, later.
+ return new BigDecimal(Double.toString(val));
+ }
+
valueOf(long val)
public static BigDecimal valueOf(long val) {
+ // 判断在不在 缓存常用的小BigDecimal值中
+ if (val >= 0 && val < zeroThroughTen.length)
+ return zeroThroughTen[(int)val];
+ else if (val != INFLATED)
+ return new BigDecimal(null, val, 0, 0);
+ // 会调用BigInteger中 valueOf(long val)涉及到BigDecimal原理
+ return new BigDecimal(INFLATED_BIGINT, val, 0, 0);
+ }
+
重点记录一下double类型构造器初始化赋值
+在使用BigDecimal
中参数为double
类型的构造器时,发现存储结果并不准确.
public static void main(String[] args) {
+ // 打印结果: 0.200000000000000011102230246251565404236316680908203125
+ System.out.println(new BigDecimal(0.2));
+ }
+
发现Java API 中有相关的描述
+++The results of this constructor can be somewhat unpredictable. +One might assume that writing {@code new BigDecimal(0.1)} in +Java creates a {@code BigDecimal} which is exactly equal to +0.1 (an unscaled value of 1, with a scale of 1), but it is +actually equal to +0.1000000000000000055511151231257827021181583404541015625. +This is because 0.1 cannot be represented exactly as a +{@code double} (or, for that matter, as a binary fraction of +any finite length). Thus, the value that is being passed +in to the constructor is not exactly equal to 0.1, +appearances notwithstanding.
+
+The {@code String} constructor, on the other hand, is perfectly predictable: writing {@code new BigDecimal(“0.1”)} creates a {@code BigDecimal} which is exactly equal to +0.1, as one would expect. Therefore, it is generally +recommended that the {@linkplain #BigDecimal(String) +String constructor} be used in preference to this one.
大概意思是说 用double
作为参数的构造函数,无法精确构造一个BigDecimal
对象,需要自己指定一个上下文的环境,也就是指定精确位。
而利用String对象作为参数传入的构造函数能精确的构造出一个BigDecimal
对象.
public static void main(String[] args) {
+ // 0.2
+ System.out.println(new BigDecimal("0.2"));
+ }
+
所以要想获得精确的结果,要使用BigDecimal
的字符串构造函数,不要使用double
参数的构造函数.
PS: 另外说一下BigDecimal
转换其他类型.BigDecimal
类提供了intValue,byteValue,shortValue
…将BigDecimal
对象转换成对应的值.
public static void main(String[] args) {
+ BigDecimal bigDecimal = new BigDecimal("1.2");
+ System.out.println(bigDecimal.byteValue());
+ System.out.println(bigDecimal.shortValue());
+ System.out.println(bigDecimal.intValue());
+ System.out.println(bigDecimal.floatValue());
+ System.out.println(bigDecimal.doubleValue());
+ System.out.println(bigDecimal.longValue());
+ }
+
用BigDecimal
的equals来进行比较
public static void main(String[] args) {
+ BigDecimal num1 = new BigDecimal("1.10");
+ BigDecimal num2 = new BigDecimal("1.1");
+ BigDecimal num3 = new BigDecimal("1.1");
+ // false
+ System.out.println(num1.equals(num2));
+ // true
+ System.out.println(num2.equals(num3));
+ }
+
我们可以看到,用BigDecimal
的equals
方法进行比较,我们可以看到1.10
和1.1
数值是相等的,而equals
方法返回的却是false
.
为了查清原因,看一下BigDecimal
中重写的equals
的源码
/**
+ * Compares this {@code BigDecimal} with the specified
+ * {@code Object} for equality. Unlike {@link
+ * #compareTo(BigDecimal) compareTo}, this method considers two
+ * {@code BigDecimal} objects equal only if they are equal in
+ * value and scale (thus 2.0 is not equal to 2.00 when compared by
+ * this method).
+ *
+ * @param x {@code Object} to which this {@code BigDecimal} is
+ * to be compared.
+ * @return {@code true} if and only if the specified {@code Object} is a
+ * {@code BigDecimal} whose value and scale are equal to this
+ * {@code BigDecimal}'s.
+ * @see #compareTo(java.math.BigDecimal)
+ * @see #hashCode
+ */
+ @Override
+ public boolean equals(Object x) {
+ if (!(x instanceof BigDecimal))
+ return false;
+ BigDecimal xDec = (BigDecimal) x;
+ if (x == this)
+ return true;
+ if (scale != xDec.scale)
+ return false;
+ long s = this.intCompact;
+ long xs = xDec.intCompact;
+ if (s != INFLATED) {
+ if (xs == INFLATED)
+ xs = compactValFor(xDec.intVal);
+ return xs == s;
+ } else if (xs != INFLATED)
+ return xs == compactValFor(this.intVal);
+
+ return this.inflated().equals(xDec.inflated());
+ }
+
源码API注释中有写.
+++objects equal only if they are equal in value and scale. thus 2.0 is not equal to 2.00 when compared by this method
+
大概意思: 对象只有在value
和scale
相等时才相等.因此,用本方法比较时,2.0不等于2.00.
PS: BigDecimal
对象中scale
字段属性
/**
+ * The scale of this BigDecimal, as returned by {@link #scale}.
+ *
+ * @serial
+ * @see #scale
+ */
+ private final int scale; // Note: this may have any value, so
+ // calculations must be done in longs
+
scale
指的是小数点后面的位数.
BigDecimal
可以通过setScale
来提高精度,只要新设的值比原来的大.
public static void main(String[] args) {
+ BigDecimal num1 = new BigDecimal("1.12345");
+ num1.setScale(8);
+ System.out.println(num1);
+ }
+
+
setScale
方法返回一个BigDecimal
对象,它的scale
是指定的值,并且它的值在数字上大于等于这个BigDecimal
对象的值。
public BigDecimal setScale(int newScale);
+
如果不是,则抛出arithmex exception
.
例如: 在上面的示例中,如果设置setScale(4)
就会报错
public static void main(String[] args) {
+ BigDecimal num1 = new BigDecimal("1.12345");
+ num1.setScale(4);
+ System.out.println(num1);
+ }
+
java.lang.ArithmeticException: Rounding necessary
+
BigDecimal
也可以通过setScale
来降低精度.因为新设的值比原来的小,所以必须保证原来数值的该位小数点后面都是0,只有这样才可以设比原来小的精度。
例如
+ public static void main(String[] args) {
+ BigDecimal num1 = new BigDecimal("1.1234500000");
+ num1.setScale(5);
+ // 1.1234500000
+ System.out.println(num1);
+ }
+
所以不要用equals
方法来比较BigDecimal
对象,因为它的equals方法会比较scale
,如果scale
不一样,它会返回false.
比较大小建议使用BigDecimal
类中重写的compareTo
方法进行比较
public static void main(String[] args) {
+ BigDecimal a = new BigDecimal("1.10");
+ BigDecimal b = new BigDecimal("1.1");
+
+ if(a.compareTo(b) == -1){
+ System.out.println("a小于b");
+ }
+
+ if(a.compareTo(b) == 0){
+ System.out.println("a等于b");
+ }
+
+ if(a.compareTo(b) == 1){
+ System.out.println("a大于b");
+ }
+
+ if(a.compareTo(b) > -1){
+ System.out.println("a大于等于b");
+ }
+
+ if(a.compareTo(b) < 1){
+ System.out.println("a小于等于b");
+ }
+ }
+
为什么BigDecimal
能够保证精度?
因为十进制整数在转化成二进制数时不会有精度问题,那么把十进制小数扩大N倍让它在整数的维度上进行计算,并保留相应的精度信息.所以就能保证精度了.
+注意事项
+1.在需要精确的小数计算时再使用BigDecimal
,BigDecimal
的性能比double
和float
差,在处理庞大,复杂的运算时尤为明显.故一般精度的计算没必要使用BigDecimal
.
尽量使用参数类型为String的构造函数.如果处理double
类型数据,可使用BigDecimal.valueOf(double val)
BigDecimal
都是不可变的的,在进行每一次四则运算时,都会产生一个新的对象,所以在做加减乘除运算时要记得要保存操作后的值.
java.lang.Math
,该类和Java中的运算息息相关.Math
类被final
修饰.构造方法是私有的.Math
类中大部分方法都被public static
修饰.
public static void main(String[] args) {
+ // 计算平方根
+ System.out.printf("4的平方根: %s\n",Math.sqrt(4));
+
+ //计算立方根
+ System.out.printf("8的立方根: %s\n",Math.cbrt(8));
+
+ // 计算n的m次方
+ System.out.printf("2的3次方: %s\n",Math.pow(2,3));
+
+ // 计算最大值
+ System.out.printf("1和2中最大值: %s\n",Math.max(1,2));
+
+ // 计算最小值
+ System.out.printf("1和2中最小值: %s\n",Math.min(1,2));
+
+ // 求绝对值
+ System.out.printf("-1的绝对值: %s\n",Math.abs(-1));
+
+ // 向上取整
+ System.out.printf("1.2向上取整: %s\n",Math.ceil(1.2));
+
+ // 向下取整
+ System.out.printf("1.2向下取整: %s\n",Math.floor(1.2));
+
+ // [0,1)区间的随机数
+ System.out.printf("[0,1)区间的随机数: %s\n",Math.random());
+
+ // 返回与参数值最接近的 double值
+ double rint = Math.rint(1.5);
+ System.out.printf("rint方法: %s\n",rint);
+
+ // 四舍五入 float时返回int值,double时返回long值
+ long round = Math.round(1.5);
+ int round1 = Math.round(1.5f);
+ System.out.printf("round方法: 1.5四舍五入: %s\n",round);
+ }
+
4的平方根: 2.0
+8的立方根: 2.0
+2的3次方: 8.0
+1和2中最大值: 2
+1和2中最小值: 1
+-1的绝对值: 1
+1.2向上取整: 2.0
+1.2向下取整: 1.0
+[0,1)区间的随机数: 8.293290468311953E-4
+rint方法: 1.5四舍五入: 2.0
+round方法: 1.5四舍五入: 2
+
其中sqrt
方法,cbrt
方法,pow
方法用native
关键字修饰,是用其他语言实现的.这里不做过多分析.
// 计算最大值
+ System.out.printf("1和2中最大值: %s\n",Math.max(1,2));
+
++返回两个
+{@code int}
值中较大的一个。也就是说,结果是参数更接近{@link Integer#MAX_VALUE}
的值。如果参数具有相同的值,则结果为相同的值。
源码
+ public static int max(int a, int b) {
+ return (a >= b) ? a : b;
+ }
+
// 计算最小值
+ System.out.printf("1和2中最小值: %s\n",Math.min(1,2));
+
++返回两个
+{@code int}
值中较小的一个。也就是说,参数的结果更接近{@link Integer#MIN_VALUE}
的值。如果参数具有相同的值,则结果为相同的值。
源码
+ public static int min(int a, int b) {
+ return (a <= b) ? a : b;
+ }
+
// 求绝对值
+ System.out.printf("-1的绝对值: %s\n",Math.abs(-1));
+
++返回
+{@code int}
值的绝对值。如果实参不是负数,则返回实参。如果参数为负数,则返回参数的否定值。
源码
+ public static int abs(int a) {
+ return (a < 0) ? -a : a;
+ }
+
ceil
方法的功能是向上取整。ceil
译为“天花板”,顾名思义就是对操作数取顶,Math.ceil(a)
就是取大于a的最小整数。需要注意的是它的返回值不是int
类型,而是double
类型.
// 向上取整
+ System.out.printf("1.2向上取整: %s\n",Math.ceil(1.2));
+
floor
方法的功能是向下取整。floor
译为“地板”,顾名思义是对操作数取底。Math.floor(a)
,就会取小于a的最大整数。它的返回值类型与ceil
一致,也是double
类型。
// 向下取整
+ System.out.printf("1.2向下取整: %s\n",Math.floor(1.2));
+
由于ceil
方法和floor
方法逻辑是一样的,区别只是传入的参数不同,所以下面主要分析一个方法.
以ceil
方法为例
++返回大于或等于参数的最小(最接近负无穷){@code double}值,并且等于一个数学整数。
+
+特殊情况:
{@code Math.ceil(x)}
的值正好是{@code -Math.floor(-x)}
的值。源码
+ public static double ceil(double a) {
+ // default impl. delegates to StrictMath
+ return StrictMath.ceil(a);
+ }
+ /**
+ * |
+ * |
+ * |
+ * ↓
+ */
+ // StrictMath.ceil
+ public static double ceil(double a) {
+ return floorOrCeil(a, -0.0, 1.0, 1.0);
+ }
+ /**
+ * |
+ * |
+ * |
+ * ↓
+ */
+ /*
+ *内部方法共享floor和ceil之间的逻辑。
+ *
+ * @param a 被floored或ceiled的值
+ * @param negativeBoundary (- 1,0)中的值的结果
+ * @param positiveBoundary (0,1)中的值的结果
+ * @param increment 当参数是非整数时要添加的值
+ */
+ private static double floorOrCeil(double a,
+ double negativeBoundary,
+ double positiveBoundary,
+ double sign) {
+ /**
+ * 返回用于{@code double}表示的无偏指数。
+ *
+ * 特殊情况:
+ *
+ * 如果参数为NaN或infinite,则结果为{@link Double#MAX_EXPONENT} + 1。
+ * 如果参数为零或低于标准,则结果为{@link Double#MIN_EXPONENT} -1。
+ */
+ // 将浮点数或双精度数转换为浮点表示形式.该方法从表示中返回指数部分
+ int exponent = Math.getExponent(a);
+
+ if (exponent < 0) {
+ /*
+ * 参数的绝对值小于1
+ * floorOrceil(-0.0) => -0.0
+ * floorOrceil(+0.0) => +0.0
+ */
+ return ((a == 0.0) ? a :
+ ( (a < 0.0) ? negativeBoundary : positiveBoundary) );
+ } else if (exponent >= 52) {
+ /*
+ * 无穷,NaN,或者一个很大的值.但一定是整数
+ */
+ return a;
+ }
+ // 否则,参数要么是一个已经异或运算的整数值
+ // 必须四舍五入为1
+ assert exponent >= 0 && exponent <= 51;
+
+ long doppel = Double.doubleToRawLongBits(a);
+ long mask = DoubleConsts.SIGNIF_BIT_MASK >> exponent;
+
+ if ( (mask & doppel) == 0L )
+ return a; // integral value
+ else {
+ double result = Double.longBitsToDouble(doppel & (~mask));
+ if (sign*a > 0.0)
+ result = result + sign;
+ return result;
+ }
+ }
+
+
返回一个大于0的double
类型数据,该值大于等于0.0且小于1.0,返回的是一个伪随机选择数,在该范围内(几乎)均匀分布.
// [0,1)区间的随机数
+ System.out.printf("[0,1)区间的随机数: %s\n",Math.random());
+
API中random
方法描述
++返回一个大于或等于
+{@code 0.0}
且小于{@code 1.0}
的{@code double}
值,带有正号。返回值是在该范围内(近似)均匀分布的伪随机选择的。
+当这个方法第一次被调用时,它会创建一个新的伪随机数生成器,与表达式{@code new java.util.Random()}
完全一样。 +此方法被正确同步以允许多个线程正确使用。但是,如果许多线程需要以非常快的速度生成伪随机数,那么每个线程拥有自己的伪随机数生成器就可以减少争用。
API中nextDouble
方法中描述
++返回该随机数生成器序列中在
+{@code 0.0}
和{@code 1.0}
之间均匀分布的{@code double}
值的下一个伪随机值。 +{@code nextDouble}
的一般约定是,从{@code 0.0d}
(包括)到{@code 1.0d}
(排除)范围内(近似地)统一选择一个{@code double}
值,伪随机生成并返回。
源码
+ public static double random() {
+ return RandomNumberGeneratorHolder.randomNumberGenerator.nextDouble();
+ }
+ /**
+ * |
+ * |
+ * |
+ * ↓
+ */
+ // Random.nextDouble
+ public double nextDouble() {
+ return (((long)(next(26)) << 27) + next(27)) * DOUBLE_UNIT;
+ }
+
通过源码可以看到,Math.random
底层是调用了Random
类中nextDouble
方法.
也就是说以下打印语句执行代码相同
+ // [0,1)区间的随机数
+ System.out.printf("[0,1)区间的随机数: %s\n",Math.random());
+ System.out.printf("[0,1)区间的随机数: %s\n",new Random().nextDouble());
+
Random类
+++该类的一个实例用于生成伪随机数流。该类使用48位种子,并使用线性同余公式进行修改。(参见Donald Knuth, 计算机编程的艺术,第二卷,第3.2.1节。)
+
++如果用相同的种子创建了
+{@code Random}
的两个实例,并且对每个实例进行了相同的方法调用序列,那么它们将生成并返回相同的数字序列。为了保证这个属性,为类{@code Random}
指定了特定的算法。为了保证Java代码的绝对可移植性,Java实现必须对{@code Random}
类使用这里显示的所有算法。然而,{@code Random}
类的子类约定了所有的方法。
Random
类位于java.util
包下,此类的实例用于生成伪随机数流。之所以称之为伪随机,是因为真正意义上的随机数(或者称为随机事件)在某次产生过程中是按照实验过程表现的分布概率随机产生的,其结果是不可预测,不可见的。而计算机中的随机函数是按照一定的算法模拟产生的,其结果是确定的,可见的。我们认为这样产生的数据不是真正意义上的随机数,因而称之为伪随机。
该类提供了两种构造方法.
+无参构造底层调用的也是有参构造.将System.nanoTime()
作为参数传递。即如果使用无参构造,默认的seed值为System.nanoTime()
。
public Random() {
+ this(seedUniquifier() ^ System.nanoTime());
+ }
+
public Random(long seed) {
+ if (getClass() == Random.class)
+ this.seed = new AtomicLong(initialScramble(seed));
+ else {
+ // subclass might have overriden setSeed
+ this.seed = new AtomicLong();
+ setSeed(seed);
+ }
+ }
+
要注意的是用有参构造创建Random
对象,如果随机种子相同,不管执行多少次,最后结果都是相同的.
例如
+ public static void main(String[] args) {
+ Random random = new Random(1);
+ // 第一次执行程序打印结果: 随机数: -1155869325
+ // 第二次执行程序打印结果: 随机数: -1155869325
+ System.out.printf("随机数: %s\n",random.nextInt());
+ }
+
随机数应用举例
+生成100个不重复的随机数,1~100的范围
+ public static void main(String[] args) {
+ int[] nums=new int[100];
+ boolean[] flag=new boolean[101];
+ Random random=new Random();
+ for (int i = 0; i < nums.length; i++) {
+ int num=random.nextInt(100)+1;
+ while (flag[num]) {
+ num=random.nextInt(100)+1;
+ }
+ flag[num]=true;
+ nums[i]=num;
+ }
+ Arrays.sort(nums);
+ System.out.println(Arrays.toString(nums));
+ System.out.println(Arrays.toString(flag));
+ }
+
++返回与参数值最接近的
+{@code double}
值,该值等于一个数学整数。如果两个数学整数{@code double
}值相同,则结果为偶数整数值.
+特殊情况:
+如果参数值已经等于一个数学整数,则结果与参数相同。
+如果参数是NaN或无穷大或正零或负零,则结果与参数相同
// 返回与参数值最接近的 double值
+ double rint = Math.rint(1.5);
+ System.out.printf("rint方法: %s\n",rint);
+
若存在两个这样的数,则返回其中的偶数值
+例如
+ public static void main(String[] args) {
+ double rint = Math.rint(100.5);
+ double rint2 = Math.rint(101.5);
+ // 100.0
+ System.out.printf(" %s\n",rint);
+ // 102.0
+ System.out.printf(" %s\n",rint2);
+ }
+
++round 表示"四舍五入",算法为
+Math.floor(x+0.5)
,即将原来的数字加上 0.5 后再向下取整,所以Math.round(11.5)
的结果为 12,Math.round(-11.5)
的结果为 -11。
// 四舍五入 float时返回int值,double时返回long值
+ long round = Math.round(1.5);
+ int round1 = Math.round(1.5f);
+ System.out.printf("round方法: 1.5四舍五入: %s\n",round);
+
++返回与实参最接近的
+{@code int}
,并四舍五入到正无穷。 +特殊情况:
@code Integer.MIN_VALUE}
,结果等于{@code Integer.MIN_VALUE}
的值。{@code Integer.MAX_VALUE}
,结果等于{@code Integer.MAX_VALUE}
的值.++PS: NAN +NaN表示非数值,例如:0.0/0结果为NAN,负数的平方根结果也为NAN.
+
public static void main(String[] args) {
+ // 四舍五入 float时返回int值,double时返回long值
+ int round1 = Math.round(0);
+ long round2 = Math.round(Double.NaN);
+ int round3 = Math.round(2147483648123L);
+ int round4 = Math.round(-2147483647123L);
+ System.out.printf("0四舍五入: %s\n", round1);
+ System.out.printf("Double.NaN=[%s],四舍五入: %s\n", Double.NaN, round2);
+ System.out.printf("Integer.MAX_VALUE=[%s] + 1 四舍五入: %s\n", Integer.MAX_VALUE, round3);
+ System.out.printf("Integer.MIN_VALUE=[%s] - 1 四舍五入: %s\n", Integer.MIN_VALUE, round4);
+ }
+
0四舍五入: 0
+Double.NaN=[NaN],四舍五入: 0
+Integer.MAX_VALUE=[2147483647] + 1 四舍五入: 2147483647
+Integer.MIN_VALUE=[-2147483648] - 1 四舍五入: -2147483648
+
+ +
+ + + + + +在运行状态中,对于任意一个实体类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。
+++反射是Java语言的一个特性,它允许程序在运行时来进行自我检查并且对内部的成员进行操作.
+
通过反射,我们可以在运行时获得程序或程序集中每一个类型的成员和成员的信息。程序中一般的对象的类型都是在编译期就确定下来的,而 Java 反射机制可以动态地创建对象并调用其属性,这样的对象的类型在编译期是未知的。所以我们可以通过反射机制直接创建对象,即使这个对象的类型在编译期是未知的.
+反射的核心是JVM在运行时才动态加载类或调用方法/访问属性,它不需要事先(写代码的时候或编译期)知道运行对象是谁。
+注解我们经常会遇到,如:@Override, @Deprecated ...
是否思考过注解是怎样工作的呢? 自定义一个注解体会一下注解是怎样工作的.
+创建注解: MyAnnotation
import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface MyAnnotation {
+ String value() default "";
+}
+
使用注解: MyAnnotationTest
public class MyAnnotationTest {
+
+ @MyAnnotation("123")
+ public void test(String str){
+ System.out.println("invoke test ...param: "+ str);
+ }
+
+ public void t2(){
+ System.out.println("I am t2 ...");
+ }
+
+}
+
实现注解@MyAnnotation
import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+public class AnnotationInvoke {
+
+
+ public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
+ // 获取使用@MyAnnotation注解的类,这里举例子 就直接写了,如果想要实现的话可以参照spring扫描包
+ Class<MyAnnotationTest> clazz = MyAnnotationTest.class;
+ Method[] methods = clazz.getDeclaredMethods();
+ for (Method method : methods) {
+ //判断该类是否使用了 @MyAnnotation注解
+ MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);
+ if (annotation != null) {
+ // 可以进行一系列操作 ...
+ // 获取该方法上 @MyAnnotation 注解的值
+ System.out.println(annotation.value());
+ // 执行test方法
+ if (method.getName().equals("test")) {
+ method.invoke(clazz.newInstance(), "hello ...");
+ }
+ }
+ }
+ }
+}
+
执行main
方法,结果
123
+invoke test ...param: hello ...
+
经典案例: 用枚举实现单例设计模式,防止反射破坏单例
+public enum EnumSingleton {
+
+ INSTANCE;
+
+ public EnumSingleton getInstance(){
+ return INSTANCE;
+ }
+
+ public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
+ EnumSingleton singleton1=EnumSingleton.INSTANCE;
+ EnumSingleton singleton2=EnumSingleton.INSTANCE;
+ System.out.println("正常情况下,实例化两个实例是否相同:"+(singleton1==singleton2));
+ Constructor<EnumSingleton> constructor= null;
+ constructor = EnumSingleton.class.getDeclaredConstructor();
+ constructor.setAccessible(true);
+ EnumSingleton singleton3= null;
+ singleton3 = constructor.newInstance();
+ System.out.println(singleton1+"\n"+singleton2+"\n"+singleton3);
+ System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:"+(singleton1==singleton3));
+ }
+
+
+}
+
结果
+正常情况下,实例化两个实例是否相同:true
+Exception in thread "main" java.lang.NoSuchMethodException
+
原因
+Constructor
类中 newInstance
方法 不能通过反射来创建对象
@CallerSensitive
+ public T newInstance(Object ... initargs)
+ throws InstantiationException, IllegalAccessException,
+ IllegalArgumentException, InvocationTargetException
+ {
+ if (!override) {
+ if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
+ Class<?> caller = Reflection.getCallerClass();
+ checkAccess(caller, clazz, null, modifiers);
+ }
+ }
+ if ((clazz.getModifiers() & Modifier.ENUM) != 0)
+ throw new IllegalArgumentException("Cannot reflectively create enum objects");
+ ConstructorAccessor ca = constructorAccessor; // read volatile
+ if (ca == null) {
+ ca = acquireConstructorAccessor();
+ }
+ @SuppressWarnings("unchecked")
+ T inst = (T) ca.newInstance(initargs);
+ return inst;
+ }
+
枚举类无法通过反射来创建对象,原因是newInstance
方法加了判断如果是枚举类就抛出异常throw new IllegalArgumentException("Cannot reflectively create enum objects");
除了不能创建枚举类的对象外,反射还是能够调用枚举类的方法的
+public enum EnumSingleton {
+
+ public EnumSingleton getInstance(){
+ return INSTANCE;
+ }
+
+ public void getTst(){
+ System.out.println("enum method ...");
+ }
+
+ public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException, ClassNotFoundException {
+ Class<?> aClass = Class.forName("com.test.EnumSingleton");
+ Method getInstance = aClass.getMethod("getTst");
+ // 枚举对应的class没有newInstance方法,会报NoSuchMethodException,应该使用getEnumConstants方法
+ Object[] oo = aClass.getEnumConstants();
+ getInstance.invoke(oo[0]);
+ Method getTst = aClass.getMethod("getTst");
+ getTst.invoke(oo[0]);
+ }
+}
+
结果
+enum getInstance ...
+enum method ...
+
反射会擦除泛型,因为泛型只在编译期间生效.而反射是在Java程序运行期间生效。
+public static void main(String[] args) throws Exception {
+
+ ArrayList<Integer> list = new ArrayList<Integer>();
+
+ list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer
+
+ list.getClass().getMethod("add", Object.class).invoke(list, "string");
+
+ for (int i = 0; i < list.size(); i++)
+ System.out.println(list.get(i));// 1 string
+ //list.forEach(System.out::print); //语法详情 见Java8 Lambda表达式
+ }
+
反射最重要的用途就是开发各种通用框架。
+以 spring 的 IOC 框架为例:
+在Java中,只要给定类的名字,那么就可以通过反射机制来获得类的所有信息.
+使用反射会有异常出现.注意处理异常
+Class类 和 java.lang.reflect
一起对反射提供了支持,java.lang.reflect
类库主要包含了以下三个类:
get()
和set()
方法读取和修改Field对象关联的字段;invoke()
方法调用与 Method
对象关联的方法;Constructor
的 newInstance()
创建新的对象;在程序运行时,反射可以获取Java类中所有的属性,下边举几个经常用的栗子
+在操作反射前我们要先了解一些Class类
+++Java的Class类是java反射机制的基础,通过Class类我们可以获得关于一个类的相关信息.
+
+虚拟机为每种类型管理一个独一无二的Class对象。也就是说,每个类(型)都有一个Class对象。运行程序时,Java虚拟机(JVM)首先检查是否所要加载的类对应的Class对象是否已经加载。如果没有加载,JVM就会根据类名查找.class文件,并将其Class对象载入
private Class(ClassLoader loader) {
+ classLoader = loader;
+}
+
class类的构造器时私有的,只有JVM可以创建Class的对象,因此不可以像普通类一样new一个Class对象, 但是却可以通过已有的类得到一个Class对象,共有三种方式
+在运行时获取 class 的对象.
+Class.forName("包名+类名");
+例如 连接Oracle数据库加载JDBC驱动// 注意此种方式请写全类名(包名+类名)
+String driver = "oracle.jdbc.driver.OracleDriver";
+Class.forName(driver);
+
类名.class
Class<?> clazz = int.class;
+Class<?> classInt = Integer.TYPE;
+
getClass()
方法. 实例.getClass()
StringBuilder str = new StringBuilder("123");
+Class<?> clazz = str.getClass();
+
反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。
+创建MainTest
类,A类,创建一个B类和C接口.让A类去继承B类.A类实现C接口,MainTest
为主类,A类,B类为内部类
public class MainTest {
+
+ class B{
+ private void privateMethodB(){
+ System.out.println("private method B ...");
+ }
+
+ void defaultMethodB(){
+ System.out.println("default method B... ");
+ }
+
+ protected void protectedMethodB(){
+ System.out.println("protected method B...");
+ }
+
+ public void publicMethodB(){
+ System.out.println("public method B...");
+ }
+ }
+
+ interface C{}
+
+ class A extends B implements C{
+
+ private void privateMethod(){
+ System.out.println("private method ...");
+ }
+
+ void defaultMethod(){
+ System.out.println("default method ... ");
+ }
+
+ protected void protectedMethod(){
+ System.out.println("protected method ...");
+ }
+
+ public void publicMethod(){
+ System.out.println("public method ...");
+ }
+
+ }
+}
+
getDeclaredMethods
方法返回类或接口声明的所有方法,包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法。
+执行下边代码 @Test
+ public void contextLoad() throws ClassNotFoundException {
+ Class<?> clazz = Class.forName("com.test.MainTest$A");
+ Method[] methods = clazz.getDeclaredMethods();
+ for (Method method : methods) {
+ System.out.println(method);
+ }
+ }
+
打印结果
+public void com.test.MainTest$A.publicMethod()
+private void com.test.MainTest$A.privateMethod()
+void com.test.MainTest$A.defaultMethod()
+protected void com.test.MainTest$A.protectedMethod()
+
getMethods
方法返回某个类的所有公用public
方法,包括其继承类的公用方法. @Test
+ public void contextLoad() throws ClassNotFoundException {
+ Class<?> clazz = Class.forName("com.test.MainTest$A");
+ Method[] methods = clazz.getMethods();
+ for (Method method : methods) {
+ System.out.println(method);
+ }
+ }
+
打印结果
+public void com.test.MainTest$A.publicMethod()
+public void com.test.MainTest$B.publicMethodB()
+public final void java.lang.Object.wait() throws java.lang.InterruptedException
+public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
+public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
+public boolean java.lang.Object.equals(java.lang.Object)
+public java.lang.String java.lang.Object.toString()
+public native int java.lang.Object.hashCode()
+public final native java.lang.Class java.lang.Object.getClass()
+public final native void java.lang.Object.notify()
+public final native void java.lang.Object.notifyAll()
+// 接口也被打印了
+public default void com.test.MainTest$C.interfaceC()
+
getMethod
方法返回一个特定的方法,其中第一个参数为方法名称,后面的参数为方法的参数对应Class
的对象.只能获取到public
修饰的方法.能获取到接口和父类的public
修饰的方法 @Test
+ public void contextLoad() throws ClassNotFoundException, NoSuchMethodException {
+ Class<?> clazz = Class.forName("com.test.MainTest$A");
+ System.out.println(clazz.getMethod("publicMethod"));
+// System.out.println(clazz.getMethod("defaultMethod"));
+// System.out.println(clazz.getMethod("protectedMethod"));
+// System.out.println(clazz.getMethod("privateMethod"));
+ System.out.println(clazz.getMethod("publicMethodB"));
+ System.out.println(clazz.getMethod("interfaceC"));
+ }
+
打印结果
+public void com.test.MainTest$A.publicMethod()
+public void com.test.MainTest$B.publicMethodB()
+public default void com.test.MainTest$C.interfaceC()
+
getFiled
:访问公有的成员变量getDeclaredField
:所有已声明的成员变量,但不能得到其父类的成员变量getFileds
和 getDeclaredFields
方法用法同上(参照 获取方法).public static void testArray() throws ClassNotFoundException {
+ Class<?> cls = Class.forName("java.lang.String");
+ Object array = Array.newInstance(cls,25);
+ //往数组里添加内容
+ Array.set(array,0,"hello");
+ Array.set(array,1,"Java");
+ Array.set(array,2,"fuck");
+ Array.set(array,3,"Scala");
+ Array.set(array,4,"Clojure");
+ //获取某一项的内容
+ System.out.println(Array.get(array,3));
+ }
+
getConstructor
方法得到Constructor
类的一个实例,而Constructor
类有一个newInstance
方法可以创建一个对象实例,创建之前要确保该类存在构造无参构造器 @Test
+ public void contextLoad() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException {
+ Class<?> clazz = Class.forName("com.test.MainTest");
+ System.out.println(clazz.newInstance());
+ }
+
打印结果
+com.test.MainTest@65e579dc
+
在MainTest
加入该方法
public void T(){
+ System.out.println("t method invoke ... ");
+ }
+
执行
+ public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
+ Class<?> outerClass = Class.forName("com.MainTest");
+ outerClass.getMethod("T").invoke(outerClass.newInstance());
+ }
+
结果
+t method invoke ...
+
内部类调用方法方式
+ public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
+ Class<?> outerClass = Class.forName("com.MainTest");
+ Class<?> innerClass = Class.forName("com.MainTest$A");
+ Method method = innerClass.getDeclaredMethod("publicMethod");
+ Object o = innerClass.getDeclaredConstructors()[0].newInstance(outerClass.newInstance());
+ method.invoke(o);
+ }
+
执行结果
+public method ...
+
当内部类私有化(private class InnerClass
)时,也可以调用,这里就不列举了
通过反射机制我们可以获得类的各种内容,进行反编译。对于JAVA这种先编译再运行的语言来说,反射机制可以使代码更加灵活,更加容易实现面向对象.
+反射功能虽然强大,但不应任意使用.如果一个功能可以不用反射完成,那么最好就不用。通过反射访问代码时,应牢记以下注意事项
+private
字段和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此可能会随着平台的升级而改变行为.+ +
+ + + + + +垃圾收集,不是Java语言的伴生产物。早在1960年,第一门开始使用内存动态分配和垃圾收集技术的Lisp语言诞生。
+垃圾收集机制是Java的招牌能力,极大地提高了开发效率。 +如今,垃圾收集几乎成为现代语言的标配,即使经过如此长时间的发展,Java的垃圾收集机制仍然在不断的演进中,不同大小的设备、不同特征的应用场景,对垃圾收集提出了新的挑战,这当然也是面试的热点。
+++垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。
+
如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用程序的结束,被保留的空间无法被其它对象使用,甚至可能导致内存溢出。
+对于高级语言来说,一个基本认知是如果不进行垃圾回收,内存迟早都会被消耗完,因为不断地分配内存空间而不进行回收,就好像不停地生产生活垃圾而从来不打扫一样。
+除了释放没用的对象,垃圾回收也可以清除内存里的记录碎片。碎片整理将所占用的堆内存移到堆的一端,以便JVM将整理出的内存分配给新的对象。
+随着应用程序所应付的业务越来越庞大、复杂,用户越来越多,没有GC就不能保证应用程序的正常进行。而经常造成STW的GC又跟不上实际的需求,所以才会不断地尝试对GC进行优化。
+在早期的C/C++时代,垃圾回收基本上是手工进行的。
+开发人员可以使用 new
关键字进行内存申请,并使用 delete
关键字进行内存释放。比如以下代码:
MibBridge *pBridge= new cmBaseGroupBridge();
+//如果注册失败,使用Delete释放该对象所占内存区域
+if(pBridge->Register (kDestroy) != NO ERROR)
+ delete pBridge;
+
这种方式可以灵活控制内存释放的时间,但是会给开发人员带来频繁申请和释放内存的管理负担。
+倘若有一处内存区间由于程序员编码的问题忘记被回收,那么就会产生内存泄漏; +垃圾对象永远无法被清除,随着系统运行时间的不断增长,垃圾对象所耗内存可能持续上升,直到出现内存溢出并造成应用程序崩溃。
+有了垃圾回收机制后,上述代码极有可能变成这样
+MibBridge *pBridge=new cmBaseGroupBridge();
+pBridge->Register(kDestroy);
+
现在,除了Java以外,C#、Python、Ruby等语言都使用了自动垃圾回收的思想,也是未来发展趋势,可以说这种自动化的内存分配和来及回收方式已经成为了线代开发语言必备的标准。
+自动内存管理,无需开发人员手动参与内存的分配与回收,这样降低内存泄漏和内存溢出的风险; +没有垃圾回收器,java也会和c++一样,各种悬垂指针,野指针,泄露问题让你头疼不已。
+自动内存管理机制,将程序员从繁重的内存管理中释放出来,可以更专心地专注于业务开发.
+对于Java开发人员而言,自动内存管理就像是一个黑匣子,如果过度依赖于“自动”,那么这将会是一场灾难,最严重的就会弱化Java开发人员在程序出现内存溢出时定位问题和解决问题的能力。
+此时,了解JVM的自动内存分配和内存回收原理就显得非常重要,只有在真正了解JVM是如何管理内存后,我们才能够在遇见outOfMemoryError
时,快速地根据错误异常日志定位问题和解决问题。
当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就必须对这些“自动化”的技术实施必要的监控和调节。
+垃圾收集器主要对 方法区 、堆中的垃圾收集 +
+垃圾收集器可以对年轻代回收,也可以对老年代回收,甚至是全栈和方法区的回收; +其中,Java堆是垃圾收集器的工作重点。
+从次数上讲:频繁收集新生代 > 较少收集老年代 » 基本不收集方法区
+在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。 +只有被标记为己经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段。
+在运行程序中,当一个对象已经不再被任何存活的对象引用时,就可以就可以判定该对象已经死亡了; +判定对象是否存活在有两种算法,应用技术算法、可达性分析算法。
+对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。 +在堆中判定新生代中的幸存者区是否可以进老年代,会有一个年龄计数器,这里用的就是引用计数算法。
+缺点:
+循环引用
+当p的指针断开的时候,内部的引用形成一个循环,从而造成内存泄漏。
+ +Java并没有选择引用计数算法,是因为其存在一个基本的难题,也就是很难处理循环引用关系。 +虽然引用计数算法存在循环引用的问题,但是很多语言的资源回收选择,例如:因人工智能而更加火热的Python,它更是同时支持引用计数和垃圾收集机制; +具体哪种最优是要看场景的,业界有大规模实践中仅保留引用计数机制,以提高吞吐量的尝试。
+Python如何解决循环引用?
+weakref
,weakref
是Python提供的标准库,旨在解决循环引用。可达性分析算法是以根对象集合(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。 +所谓根集合"GCRoots”就是一组必须活跃的引用,即有在栈中有指针指向堆中的地址; +可达性分析算法:也可以称为 根搜索算法、追踪性垃圾收集。
+相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
+在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象; +使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链; +如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。 +
+对象集合(GC Roots)可以是哪些?
+synchronized
持有的对象;例如:JMXBean、JVMTI
中注册的回调、本地代码缓存等除了堆空间产生对象的一些结构外,比如:虚拟机栈、本地方法栈、方法区、字符串常量池等地方对堆空间的对象的引用,都可以作为GC Roots进行可达性分析。 +
+++如何判定是否为
+GC root
? +由于Root采用栈方式存放变量和指针,所以如果一个指针,保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root
使用注意
+如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。 +简单来说就是执行这个算法的时候,要停止程序标记对象,不能一边改变对象的引用一边判定对象是不是垃圾。
+这点也是导致GC进行时必须stop The World
的一个重要原因。即使是号称几乎不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的。
当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。 +目前在JVM中比较常见的三种垃圾收集算法是:
+标记清除算法是一种非常基础和常见的垃圾收集算法,该算法被J.McCarthy等人在1960年提出并并应用于Lisp
语言。
执行过程
+当堆中的有效内存空间被耗尽的时候,就会停止整个程序(stop the world),然后进行两项工作,第一项则是标记,第二项则是清除;
+标记的是可达对象,不是垃圾对象;清除回收的是垃圾对象。
+++什么是清除? +上面所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。 +下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放覆盖原有的地址。
+
缺点
+为了解决标记-清除算法在垃圾收集效率方面的缺陷,M.L.Minsky于1963年发表了著名的论文:“使用双存储区的Lisp语言垃圾收集器(CA LISP Garbage Collector Algorithm Using Serial Secondary Storage
)”。
+M.L.Minsky在该论文中描述的算法被人们称为复制算法,它也被M.L.Minsky本人成功地引入到了Lisp语言的一个实现版本中。
执行过程
+将分配内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中存活对象复制到未被使用的内存块中去, +之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
+缺点
+优点 +解决了标记清除算法带来的问题;
+复制算法最坏情况,如果系统中的垃圾对象很少,复制算法需要复制的存活对象数量就会很多,那么大部分对象从一个区域移动到另一个区域,GCRoots需要改变了对象的地址,加大了维护成本; +所以复制算法适合,系统中的垃圾对象很多,可复制的存活的对象很少的情况。利用这个特点,在新生代中的幸存者区里面就用到了复制算法的思想。
+复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。 +如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。标记整理算法应运而生。
+标记一清除算法的确可以应用在老年代中,但是该算法不仅执行效率低下,而且在执行完内存回收后还会产生内存碎片,所以JvM的设计者需要在此基础之上进行改进。
+1970年前后,G.L.Steele、C.J.Chene
和D.s.Wise
等研究者发布标记-压缩算法。在许多现代的垃圾收集器中,人们都使用了标记-压缩算法或其改进版本。
执行过程
+与标记清理算法相比,多了一个步骤"压缩(整理)",也就是移动对象的步骤; +是否移动回收后的存活对象是一项优缺点并存的风险决策。标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。 +如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。
+优点
+缺点
+标记-整理算法相对来说更平滑一些,但是效率上不尽如人意,它比复制算法多了一个标记的阶段,比标记-清除多了一个整理内存的阶段。
++ | 标记清除算法 | +标记整理算法 | +复制算法 | +
---|---|---|---|
时间开销 | +中等 | +最慢 | +最快 | +
空间开销 | +少(会堆积碎片) | +少(不堆积碎片) | +通常需要存活对象的两倍空间(不堆积碎片) | +
移动对象 | +否 | +是 | +是 | +
这些算法中,并没有一种算法可以完全替代其他算法,它们都具有自己独特的优势和特点。分代收集算法思想应运而生。
+分代收集算法,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。 +一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。
+在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象、线程、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。 +但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如:string对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。
+分代的思想被现有的虚拟机广泛使用。几乎所有的垃圾回收器都区分新生代和老年代。 +在HotSpot中,基于分代的概念,GC所使用的内存回收算法必须结合年轻代和老年代各自的特点。
+年轻代:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。 +适合复制算法;复制算法内存利用率不高的问题,通过Hotspot中的两个幸存者区的设计得到缓解。
+老年代:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。 +不适合复制算法,一般是由标记-清除或者是标记-清除与标记-整理的混合实现。
+在垃圾回收过程中,应用软件将处于一种stop the World的状态。 +在STW状态下,应用程序所有的线程都会挂起,暂停一切正常的工作,等待垃圾回收的完成。 +如果垃圾回收时间过长,应用程序会被挂起很久,将严重影响用户体验或者系统的稳定性。 +为了解决这个问题,即对实时垃圾收集算法的研究直接导致了增量收集算法的诞生。
+如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。 +每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。
+总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法。 +增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作
+使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。 +但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
+一般来说,在相同条件下,堆空间越大,一次GC时所需要的时间就越长,有关GC产生的停顿也越长。 +为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。
+分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间。 +每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。
++ +
+ + + + + +垃圾收集器没有在规范中进行过多的规定,可以由不同的厂商、不同版本的JVM来实现。
+由于JDK的版本处于高速迭代过程中,因此Java发展至今已经衍生了众多的GC版本。
+++Java不同版本新特性学习思路: +语法层面:Lambda表达式、switch、自动拆箱装箱、enum +API层面:Stream API、新的日期时间、Optional、String、集合框架 +底层优化:JVM优化、GC的变化、元空间、静态域、字符串常量池位置变化
+
从不同角度分析垃圾收集器,可以将GC分为不同的类型。
+在单核cpu的硬件情况下,该收集器会在工作时冻结所有应用程序线程,这使它在所有目的和用途上都无法在服务器环境中使用。
+ +在停止用户线程之后,多条GC线程并行进行垃圾回收。和串行回收相反,并行收集可以运用多个CPU同时执行垃圾回收,因此提升了应用的吞吐量。
+ +不会出现STW现象,指多条垃圾收集线程同时进行工作,GC线程和用户线程同时运行,不会出现STW现象。 +
+会出现STW现象,一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束。
+吞吐量、暂停时间、内存占用 这三者共同构成一个“不可能三角”。 +这三项里,暂停时间的重要性日益凸显。因为随着硬件发展,内存占用多些越来越能容忍,硬件性能的提升也有助于降低收集器运行时对应用程序的影响,即提高了吞吐量。 +而内存的扩大,对延迟反而带来负面效果。
+一款优秀的收集器通常最多同时满足其中的两项。 简单来说,主要抓住两点:
+吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值。吞吐量=运行用户代码时间 /(运行用户代码时间+垃圾收集时间)
在注重吞吐量的这种情况下,应用程序能容忍较高的暂停时间,因此,高吞吐量的应用程序有更长的时间基准,快速响应是不必考虑的。 +吞吐量优先,意味着在单位时间内,STW的时间最短。
+暂停时间是指一个时间段内应用程序线程暂停,让GC线程执行的状态。
+例如:GC期间100毫秒的暂停时间意味着在这100毫秒期间内没有应用程序线程是活动的。暂停时间优先,意味着尽可能让单次STW的时间最短。
+高吞吐量较好因为这会让应用程序的最终用户感觉只有应用程序线程在做“生产性”工作。直觉上,吞吐量越高程序运行越快。
+低暂停时间较好因为从最终用户的角度来看不管是GC还是其他原因导致一个应用被挂起始终是不好的。 +这取决于应用程序的类型,有时候甚至短暂的200毫秒暂停都可能打断终端用户体验。 +因此,具有低的较大暂停时间是非常重要的,特别是对于一个交互式应用程序。
+不幸的是”高吞吐量”和”低暂停时间”是相互矛盾的。
+因为如果选择以吞吐量优先,那么必然需要降低内存回收的执行频率,但是这样会导致GC需要更长的暂停时间来执行内存回收。 +相反的,如果选择以低延迟优先为原则,那么为了降低每次执行内存回收时的暂停时间,也只能频繁地执行内存回收,但这又引起了年轻代内存的缩减和导致程序吞吐量的下降。
+所以,在设计或使用GC算法时,我们必须确定我们的目标:一个GC算法只可能针对两个目标之一即只专注于较大吞吐量或最小暂停时间,或尝试找到一个二者的折中。
+垃圾收集机制是Java的招牌能力,极大地提高了开发效率。
+垃圾收集器是和JVM一脉相承的,它是和JVM进行搭配使用,在不同的使用场景对应的收集器也是有区别。
+有了虚拟机,就一定需要收集垃圾的机制,这就是Garbage Collection
,对应的产品我们称为Garbage Collector
。
经典GC
+Parallel GC
和 Concurrent Mark Sweep GC
跟随JDK1.4.2一起发布·Parallel GC
在JDK6之后成为HotSpot默认GC。高性能GC
+Epsilon
垃圾回收器,又被称为 “No-Op(无操作)“ 回收器。同时,引入ZGC:可伸缩的低延迟垃圾回收器(Experimental)Shenandoah GC
:低停顿时间的GC(Experimental)。·2019年9月,JDK13发布。增强ZGC,自动返回未用堆内存给操作系统。Serial
、Serial old
ParNew
、Parallel Scavenge
、Parallel old
CMS
、G1
Serial
、ParNew
、Parallel Scavenge
Serial old
、Parallel old
、CMS
G1
Serial/Serial old
Serial/CMS
ParNew/Serial old
、ParNew/CMS、Parallel
Scavenge/Serial 0ld
Parallel Scavenge/Parallel old
G1
Serial old
和CMS
之间的连线表示,Serial old
作为CMS出现"Concurrent Mode Failure"失败的后备预案Serial + CMS
、ParNew + Serial old
这两个组合声明为废弃;并在JDK9中完全取消了这些组合的支持Parallel Scavenge
和Serial old
组合++PS 为什么要有很多垃圾回收器? +因为垃圾回收器没有最好的实现,想要STW时间段的就需要吞吐量少一点;所以我们选择的只是对具体应用最合适的收集器。 +针对不同的场景,提供不同的垃圾收集器,来提高垃圾收集的性能。
+
使用虚拟机参数-XX:+PrintCommandLineFlags
:查看命令行相关参数
运行下列程序
+public class MainTest {
+ public static void main(String[] args) {
+ try {
+ Thread.sleep(100000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+}
+
输出结果
+-XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
+
使用命令行指令:jinfo -flag 垃圾回收器参数 进程ID +
+Serial GC
由于弊端较大,只有放在单核CPU上才能充分发挥其作用,由于现在都是多核CPU已经不用串行收集器了,所以以下内容了解即可。
对于交互较强的应用而言,这种垃圾收集器是不能接受的。一般在Java web应用程序中是不会采用串行垃圾收集器的。
+Serial GC
(串行垃圾回收回器)是最基本、历史最悠久的垃圾收集器了。JDK1.3之前回收新生代唯一的选择。
Serial GC
作为HotSpot中client模式下的默认新生代垃圾收集器;
+Serial GC
采用复制算法、串行回收和"stop-the-world"机制的方式执行内存回收;
除了年轻代之外,Serial GC
还提供用于执行老年代垃圾收集的Serial old GC
;Serial old GC
是运行在Client模式下默认的老年代的垃圾回收器;
+同样也采用了串行回收和"stop the world"机制,只不过内存回收算法使用的是标记-压缩算法。
Serial GC
是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束
Serial GC
优点, 简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial GC
由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
+是运行在client模式下的虚拟机是个不错的选择。
运行任意程序,设置虚拟机参数如下;当设置使用Serial GC
时,新生代和老年代都会使用串行收集器。
-XX:+PrintCommandLineFlags -XX:+UseSerialGC
+
输出
+-XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseSerialGC
+
+ +
+ + + + + +getInstance
的静态类方法,XXXFactory
的静态方法;Class
的newInstance
方法:在JDK9里面被标记为过时的方法,因为只能调用空参构造器;Constructor
的newInstance(XXX)
:反射的方式,可以调用空参的,或者带参的构造器;clone()
:不调用任何的构造器,要求当前的类需要实现Cloneable
接口中的clone
接口;Socket
的网络传输;Objenesis
;创建对象代码演示及字节码指令
+public class MainTest {
+ public static void main(String[] args) {
+ Object obj = new Object();
+ }
+}
+
创建对象步骤:
+判断对象对应的类是否加载、链接、初始化
+虚拟机遇到一条new
指令,首先去检查这个指令的参数能否在 Metaspace
的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载,解析和初始化。(即判断类元信息是否存在)。
如果没有,那么在双亲委派模式下,使用当前类加载器以 ClassLoader + 包名 + 类名
为key进行查找对应的 .class
文件,如果没有找到文件,则抛出 ClassNotFoundException
异常,如果找到,则进行类加载,并生成对应的Class对象。
为对象分配内存
+首先计算对象占用空间的大小,接着在堆中划分一块内存给新对象。 +如果实例成员变量是引用类型,仅分配引用变量空间即可,即4个字节大小.
+如果内存规整:指针碰撞;如果内存不规整:虚拟表需要维护一个列表,即空闲列表分配
+++指针碰撞: +所有用过的内存在一边,空闲的内存放另外一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针指向空闲那边挪动一段与对象大小相等的距离罢了。 +如果垃圾收集器选择的是Serial ,ParNew这种基于压缩算法的,虚拟机采用这种分配方式。一般使用带Compact(整理)过程的收集器时,使用指针碰撞。
+
++空闲列表分配: +虚拟机维护了一个列表,记录上那些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。 +这种分配方式成为了 “空闲列表(Free List)”。
+
选择哪种分配方式由Java堆是否规整所决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
+处理并发问题
+-XX:+UseTLAB
参数来设置,在Java8是默认开启的初始化分配到的内存
+就是给对象属性赋值的操作
+给所有属性设置默认值,保证对象实例字段在不赋值时可以直接使用。
+设置对象的对象头
+将对象的所属类(即类的元数据信息)、对象的 HashCode
和对象的GC信息、锁信息等数据存储在对象的对象头中。这个过程的具体设置方式取决于JVM实现。
执行init方法进行初始化
+初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。
+因此一般来说(由字节码中跟随 invokespecial
指令所决定),new指令之后会接着执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完成创建出来。
引入依赖
+<!-- 引入查看对象布局的依赖 -->
+<dependency>
+ <groupId>org.openjdk.jol</groupId>
+ <artifactId>jol-core</artifactId>
+ <version>0.9</version>
+</dependency>
+
测试代码
+public class MainTest {
+ public static void main(String[] args) {
+ //这个对象里面是空的什么都没有
+ T t = new T();
+ System.out.println(ClassLayout.parseInstance(t).toPrintable());
+ }
+}
+class T{}
+
测试结果
+ OFFSET SIZE TYPE DESCRIPTION VALUE
+ 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
+ 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
+ 8 4 (object header) a1 2c 01 20 (10100001 00101100 00000001 00100000) (536947873)
+ 12 4 (loss due to the next object alignment)
+Instance size: 16 bytes
+Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
+
在64位JVM下,通过测试的结果我们可以看到Java对象的布局对象头占12byte,因为Jvm规定内存分配的字节必须是8的倍数,否则无法分配内存,所以就出现了,4byte的对齐数据。
+通过测试发现,Java的对象布局包括:
+官方文档对于对象头的解释
+++object header +Common structure at the beginning of every GC-managed heap object. +(Every oop points to an object header.) Includes fundamental information about the heap object’s layout, type, GC state, synchronization state, and identity hash code. +Consists of two words. In arrays it is immediately followed by a length field. +Note that both Java objects and VM-internal objects have a common object header format.
+
大意:每个GC管理堆对象开始处的公共结构。(每个oop都指向一个对象头。)包括关于堆对象布局、类型、GC状态、同步状态和标识散列代码的基本信息。 +由两个词组成。在数组中,它后面紧跟着一个长度字段。注意,Java对象和vm内部对象都有一个共同的对象头格式。
+其中上面的两个词指的是
+++mark word +The first word of every object header. Usually a set of bitfields including synchronization state and identity hash code. +May also be a pointer (with characteristic low bit encoding) to synchronization related information. +During GC, may contain GC state bits.
+
大意:每个对象头文件的第一个字。通常是一组位域,包括同步状态和身份散列码。也可以是一个用于同步相关信息的指针(具有特征的低位编码)。在GC期间,可能包含GC状态位。
+++klass pointer +The second word of every object header. Points to another object (a metaobject) which describes the layout and behavior of the original object. +For Java objects, the “klass” contains a C++ style “vtable”.
+
大意:每个对象头文件的第二个字。指向另一个对象(元对象),它描述了原始对象的布局和行为。对于Java对象,“klass”包含一个c++风格的“vtable”。
+简单来说,对象头包含了包括两部分,分别是标志词(mark word)和类型指针(klass pointer);如果是数组,还需要记录数组的长度。
+++类型指针,是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
+
引用上面在64位虚拟机下的测试结果,对象头中的这些二进制数字代表什么呢?
+ OFFSET SIZE TYPE DESCRIPTION VALUE
+ 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
+ 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
+ 8 4 (object header) a1 2c 01 20 (10100001 00101100 00000001 00100000) (536947873)
+
在openjdk8-master
中markOop.hpp
,有对mark word
的描述
// 32 bits:
+// --------
+// hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
+// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
+// size:32 ------------------------------------------>| (CMS free block)
+// PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
+//
+// 64 bits:
+// --------
+// unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
+// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
+// PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
+// size:64 ----------------------------------------------------->| (CMS free block)
+
mark word
,从前到后的顺序,25个位表示hash值,4位表示GC分代的年龄,1位偏向锁的信息,2位锁的状态;mark word
,从前到后的顺序,25个未使用,31个位表示hash值,未使用1位,4位表示GC分代的年龄,1位偏向锁的信息,2位锁的状态;64位虚拟机下对象头共12个字节(96bit),mark word
占8字节(64位),所以klass pointer
占4字节(32位);
++有些资料上显示:
+mark word
占8字节(64位),klass pointer
也占8字节(64位); +其实这种说法也正确,因为虚拟机默认开启了指针压缩,所以默认情况下klass pointer
占4字节(32位)。
那么mark word
64位中存放什么呢?
从上面可以看出,标志词(mark word)主要存放:
+当然mark word
会根据对象的不同状态存放的也不相同;
++对象的状态
++
+- 无状态:对象刚被new出来
+- 偏向锁状态: 一个线程持有对象
+- 轻量级锁状态
+- 重量级锁状态
+- 被垃圾回收器标记的状态
+
目前32位虚拟机已经几乎没人用了,所以只介绍64位JVM
+对象状态 | +对象头-markword | +
---|---|
无锁状态状态 | +unused:25|hash:31| unused:1 |age:4 | biased_lock:1|lock:2 | +
偏向锁状态 | +JavaThread:54| epoch:2| unused:1 | age:4| biased_lock:1|lock:2 | +
轻量级锁状态 | +ptr_to_lock_record:62(指向栈中锁记录的指针)|lock:2 | +
重量级锁状态 | +Ptr_to_heavyweight_monitor:62(指向重量级锁的指针)|lock:2 | +
被GC标记状态 | +lock:2 | +
对象的状态是5种,但是在markword
中表示对象状态的lock
却是2bit,2bit最多能表示4种状态,那么对象的5种状态是怎么表示的?
++2bit 排列组合:00、11、01、10,最多四种
+
对象锁的状态是联合用biased_lock: 1
和 lock: 2
表示的:
biased_lock
:0 lock
: 01: 表示无锁状态biased_lock
:1 lock
: 01: 表示偏向锁状态lock
: 00: 表示轻量级锁状态lock
: 10: 表示重量级锁状态lock
: 11: 表示被垃圾回收器标记的状态markword
中存储的内容:
lock
:锁标志位;区分锁状态,11时表示对象待GC回收状态, 只有最后2位锁标识(11)有效。biased_lock
:是否偏向锁;由于正常锁和偏向锁的锁标识都是 01,没办法区分,这里引入一位的偏向锁标识位。age
:分代年龄;表示对象被GC的次数,当该次数到达阈值的时候,对象就会转移到老年代。hash
: 对象的 hashcode
;运行期间调用System.identityHashCode()
来计算,延迟计算,并把结果赋值到这里。当对象加锁后,计算的结果31位不够表示,在偏向锁,轻量锁,重量锁,hashcode
会被转移到Monitor
中。JavaThread
:偏向锁的线程ID,偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID。 在后面的操作中,就无需再进行尝试获取锁的动作。epoch
:时间戳,代表偏向锁的有效性;偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。ptr_to_lock_record
:轻量级锁状态下,指向栈中锁记录的指针。当锁获取是无竞争的时,JVM使用原子操作而不是OS互斥。这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM通过CAS操作在对象的标题字中设置指向锁记录的指针。ptr_to_heavyweight_monitor
:重量级锁状态下,指向对象监视器Monitor
的指针。如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到Monitor以管理等待的线程。在重量级锁定的情况下,JVM在对象的ptr_to_heavyweight_monitor
设置指向Monitor
的指针。mark word
在对象的不同状态下会有不同的表现形式,主要有三种状态,无锁状态、加锁状态、GC标记状态。
那么可以理解为Java当中的取锁其实可以理解是给对象上锁,也就是改变对象头中markword>lock
的状态,如果上锁成功则进入同步代码块。
+但是Java当中的锁有分为很多种,从上面可以看出大体分为偏向锁、轻量锁、重量锁三种锁状态.
独立于方法之外的变量,在类里定义的变量无 static
修饰;包括从父类继承下来的变量和本身拥有的字段。
一些规则:
+CompactFields
设置为true,子类的窄变量可能插入到父类的间隙;不是必须的,也没有特别的含义,仅仅起到占位符的作用。 +因为Jvm规定内存分配的字节必须是8的倍数,否则无法分配内存.如果对象头占得字节 + 实例变量占得字节刚好为8的倍数,对齐填充数据则不存在。
+JVM是如何通过栈帧中的对象引用访问到其内部的对象实例呢? +
+hotspot使用的是直接访问。因为句柄访问开辟了句柄池,所以直接访问相较于句柄访问效率稍高一点。
+句柄访问是在栈的局部变量表中,记录的对象的引用,然后在堆空间中开辟了一块空间,也就是句柄池。指向的是方法区中的对象类型数据。
+优点:reference
中存储稳定句柄地址,对象被移动(垃圾收集时移动对象很普遍)时只会改变句柄中实例数据指针即可,reference
本身不需要被修改
直接指针是局部变量表中的引用,直接指向堆中的实例,在对象实例中有类型指针,指向的是方法区中的对象类型数据。
+ +finalize()
方法是Java提供的对象终止机制,允许开发人员提供对象被销毁之前的自定义处理逻辑。
+当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()
方法。
finalize()
方法允许在子类中被重写,用于在对象被回收时进行资源释放。
+通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。
文档注释大意:当GC确定不再有对对象的引用时,由垃圾收集器在对象上调用。子类重写finalize
方法来释放系统资源或执行其他清理。
/**
+ * Called by the garbage collector on an object when garbage collection
+ * determines that there are no more references to the object.
+ * A subclass overrides the {@code finalize} method to dispose of
+ * system resources or to perform other cleanup.
+ */
+ protected void finalize() throws Throwable { }
+
简而言之,finalize
方法是与Java中的垃圾回收器有关系。即:当一个对象变成一个垃圾对象的时候,如果此对象的内存被回收,那么就会调用该类中定义的finalize
方法。
当一个对象可被回收时,就需要执行该对象的 finalize()
方法,那么就有可能在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize()
方法自救,后面回收时不会再调用该方法。
永远不要主动调用某个对象的finalize
方法应该交给垃圾回收机制调用的原因:
finalize
方法时时可能会导致对象复活;finalize
方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finalize
方法将没有执行机会;因为优先级比较低,即使主动调用该方法,也不会因此就直接进行回收;finalize
方法会严重影响GC的性能;由于finalize
方法的存在,虚拟机中的对象一般可能处于三种状态:
如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用了。一般来说,此对象需要被回收。 +但事实上,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段。一个无法触及的对象有可能在某一个条件下“复活”自己,如果这样,那么对它的回收就是不合理的,为此,虚拟机中定义了的对象可能的三种状态:
+finalize
中复活;对象被复活,对象在finalize
方法中被重新使用;finalize
方法被调用,并且没有复活,那么就会进入不可触及状态;对象死亡,对象没有被使用;只有在对象不可触及时才可以被回收。不可触及的对象不可能被复活,因为finalize()
只会被调用一次。
finalize
对象终止机制判定一个对象能否被回收过程:
判定一个对象是否可回收,至少要经历两次标记过程:
+finalize
方法
+finalize
方法,或者finalize
方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,对象被判定为不可触及的。finalize
方法,且还未执行过,那么会被插入到F-Queue
队列中,由一个虚拟机自动创建的、低优先级的Finalizer
线程触发其finalize
方法执行。finalize
方法是对象逃脱死亡的最后机会,稍后GC会对F-Queue
队列中的对象进行第二次标记。如果对象在finalize
方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,该对象会被移出“即将回收”集合。
+之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize
方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的finalize
方法只会被调用一次。代码演示对象能否被回收:
+public class MainTest {
+
+ public static MainTest var;
+
+ /**
+ * 此方法只能被调用一次
+ * 可对该方法进行注释,来测试finalize方法是否能复活对象
+ */
+ @Override
+ protected void finalize() throws Throwable {
+ System.out.println("调用当前类重写的finalize()方法");
+ // 复活对象 让当前带回收对象重新与引用链中的对象建立联系
+ var = this;
+ }
+
+ public static void main(String[] args) throws InterruptedException {
+ var = new MainTest();
+ var = null;
+ System.gc();
+ System.out.println("-----------------第一次gc操作------------");
+ // 因为Finalizer线程的优先级比较低,暂停2秒,以等待它
+ Thread.sleep(2000);
+ if (var == null) {
+ System.out.println("对象已经死了");
+ // 如果第一次对象就死亡了 就终止
+ return;
+ } else {
+ System.out.println("对象还活着");
+ }
+
+ System.out.println("-----------------第二次gc操作------------");
+ var = null;
+ System.gc();
+ // 下面代码和上面代码是一样的,但是 对象却自救失败了
+ Thread.sleep(2000);
+ if (var == null) {
+ System.out.println("对象已经死了");
+ } else {
+ System.out.println("对象还活着");
+ }
+ }
+
+}
+
+ +
+ + + + + +内存溢出(Out Of Memory,简称OOM)是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。 +官方文档中对内存溢出的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。
+由于GC一直在发展,所有一般情况下,除非应用程序占用的内存增长速度非常快,造成垃圾回收已经跟不上内存消耗的速度,否则不太容易出现OOM的情况。
+引起内存溢出的原因:
+OOM异常信息变化:
+java.lang.OutOfMemoryError:PermGen space
java.lang.OutofMemoryError:Metaspace
在抛出OutOfMemoryError
之前,通常垃圾收集器会被触发,尽其所能去清理出空间。当然,不是在任何情况下垃圾收集器都会被触发的:
+比如,我们去分配一个超大对象,类似一个超大数组超过堆的最大值,JVM可以判断出垃圾收集并不能解决这个问题,所以直接抛出OutOfMemoryError
。
严格来说,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏。 +但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致00M,也可以叫做宽泛意义上的“内存泄漏”。
+Java使用可达性分析算法来标记垃圾,最上面的数据不可达,就是需要被回收的。 +后期有一些对象不用了,按道理应该断开引用,但是存在一些链没有断开,从而导致没有办法被回收。从而造成内存泄漏。
+ +内存泄漏与内存溢出的关系
+尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现outOfMemory
异常,导致程序崩溃。
++买房子:80平的房子,但是有10平是公摊的面积,我们是无法使用这10平的空间,这就是所谓的内存泄漏
+
单例模式;单例的生命周期和应用程序是一样长的,所以单例程序中,如果单例对象持有对外部对象的引用的话,而外部对象引用却不再使用,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。
+提供close
的资源未关闭导致内存泄漏;数据库连接,网络连接和IO连接必须手动,否则是不能被回收的。
Stop-the-world
直译为:停止一切,简称STW,指的是垃圾回收发生过程中,会产生应用程序的停顿。
+停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉。
STW事件和采用哪款GC无关,因为所有的GC都有这个事件。任何垃圾回收器都不能完全避免Stop-the-world
情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间。
为什么垃圾回收时要STW?
+在垃圾回收标记阶段,JVM使用可达性分析算法进行标记那些对象是垃圾,如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证。 +所以在垃圾回收的时候要STW,分析工作必须在一个能确保一致性的快照中进行。
+STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。 +被STW中断的应用程序线程会在完成GC之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样,所以我们需要减少STW的发生。
+在默认情况下,通过System.gc()
者Runtime.getRuntime().gc()
的调用,会显式触发FullGC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。
源码调用了Runtime.getRuntime().gc();
大意为:
+++运行垃圾收集器。 +调用GC方法意味着Java虚拟机要努力回收未使用的对象,以便使它们当前占用的内存能够快速重用。当控制从方法调用中返回时,Java虚拟机已经尽了最大努力从所有丢弃的对象中回收空间。 +调用
+System.gc()
有效地等同于调用:Runtime.getRuntime().gc()
/**
+ * Runs the garbage collector.
+ * <p>
+ * Calling the <code>gc</code> method suggests that the Java Virtual
+ * Machine expend effort toward recycling unused objects in order to
+ * make the memory they currently occupy available for quick reuse.
+ * When control returns from the method call, the Java Virtual
+ * Machine has made a best effort to reclaim space from all discarded
+ * objects.
+ * <p>
+ * The call <code>System.gc()</code> is effectively equivalent to the
+ * call:
+ * <blockquote><pre>
+ * Runtime.getRuntime().gc()
+ * </pre></blockquote>
+ *
+ * @see java.lang.Runtime#gc()
+ */
+ public static void gc() {
+ Runtime.getRuntime().gc();
+ }
+
调用System.gc();
无法保证对垃圾收集器的调用;一般情况下,垃圾回收应该是自动进行的,无须手动触发。
代码演示是否触发GC
+// 在线程不忙的情况下,GC几乎都会执行都会调用finalize()方法 多试几次(15~30)
+public class MainTest {
+ public static void main(String[] args) {
+ new MainTest();
+
+ // 建议垃圾回收器执行垃圾收集行为 不会立即执行
+ System.gc();
+
+ // 调用System.gc();后再调用System.runFinalization(); 会强制调用失去引用对象的 finalize() 方法
+// System.runFinalization();
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ super.finalize();
+ System.out.println("finalize 被调用 ...");
+ }
+}
+
++PS: 并行、并发、串行
++
+- 并行:前提是在多核CPU或多个CPU条件下,多个线程同时被多个CPU执行,同时执行的线程并不会抢占CPU资源。
+- 串行:前提是在单核CPU条件下,单线程程序执行,不能同时执行,也不能去切换执行。也就是在同一时间段只能做一件事。
+- 并发:前提是多线程条件下,多个线程抢占一个CPU资源,多个线程被交替执行。因为CPU运算速度很快,所以用户感觉不到线程切换的卡顿。
+
无论并行、并发,都可以有多个线程执行,如果是多个线程抢占一个CPU就成了并发,多个CPU同时执行多个线程就是并行。
+对于单CPU的计算机来说,同一时间是只能干一件事儿的,如果是单线程线程就是串行;如果是多个线程就是并发。 +而对于多CPU的计算机说,同一时间能干多个事,如果多个CPU同时执行多个线程就是并行;如果一个CPU同时执行多个线程就是并行。
+并行垃圾回收:在停止用户线程之后,多条GC线程并行进行垃圾回收,此时用户线程仍处于等待状态,出现STW现象。
+并发垃圾回收:指多条垃圾收集线程同时进行工作,GC线程和用户线程同时运行,不会出现STW现象。
+串行垃圾回收:在同一时间段内只允许有一个CPU用于执行垃圾回收操作,该收集器会在工作时冻结所有应用程序线程,这使它在所有目的和用途上都无法在服务器环境中使用。
+程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为安全点。
+安全点的选择很重要,如果太少可能导致GC等待的时间太长,如果太频繁可能导致运行时的性能问题。 +大部分指令的执行时间都非常短暂,通常会根据“是否具有让程序长时间执行的特征”为标准。比如:选择一些执行时间较长的指令作为安全点。
+如何在GC发生时,检查所有线程都跑到最近的安全点停顿下来呢?
+安全点机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的安全点。
+但是,程序“不执行”的时候呢?例如线程处于sleep
状态或Blocked
状态,这时候线程无法响应JVM的中断请求,“走”到安全点去中断挂起,JVM也不太可能等待线程被唤醒。
+对于这种情况,就需要安全区域来解决。
安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始Gc都是安全的。 +可以把安全点看做是被扩展了的安全区域。
+在JDK1.2版之后,Java对引用的概念进行了扩充,将引用分为:
+这4种引用强度依次逐渐减弱。除强引用外,其他3种引用均可以在java.lang.ref
包中找到它们的身影。
+强引用为JVM内部实现。其他三类引用类型全部继承自Reference
父类。
上述引用垃圾回收的前提条件是:对象都是可触及的(可达性分析结果为可达),如果对象不可触及就直接被垃圾回收器回收了。
+在Java程序中,最常见的引用类型是强引用,普通系统99%以上都是强引用,也就是我们最常见的普通对象引用,也是默认的引用类型。 +当在Java语言中使用new操作符创建一个新的对象,并将其赋值给一个变量的时候,这个变量就成为指向该对象的一个强引用。
+强引用测试
+// 强引用测试
+public class MainTest {
+ public static void main(String[] args) {
+ StringBuffer var0 = new StringBuffer("hello world");
+ StringBuffer var1 = var0;
+
+ var0 = null;
+ System.gc();
+ try {
+ Thread.sleep(3000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ System.out.println(var1.toString());
+ }
+}
+
强引用所指向的对象在任何时候都不会被系统回收,虚拟机会抛出OOM异常,也不会回收强引用所指向对象; +所以强引用是导致内存泄漏的主要原因。
+软引用是一种比强引用生命周期稍弱的一种引用类型。在JVM内存充足的情况下,软引用并不会被垃圾回收器回收,只有在JVM内存不足的情况下,才会被垃圾回收器回收。
+软引用是用来描述一些还有用,但非必需的对象。 +只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。
+++这里的第一次回收是指不可达的对象
+
所以软引用一般用来实现一些内存敏感的缓存,只要内存空间足够,对象就会保持不被回收掉。 +比如:高速缓存就有用到软引用。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
+软引用测试
+/**
+ * 软引用测试
+ *
+ * 虚拟机参数:
+ * -Xms10m
+ * -Xmx10m
+ * -XX:+PrintGCDetails
+ */
+public class MainTest {
+ public static void main(String[] args) {
+ //SoftReference<User> softReference = new SoftReference<>(new User("hello"));
+ // 上面的一行代码等价于下面的三行代码
+ User user = new User("hello");
+ SoftReference<User> softReference = new SoftReference<User>(user);
+ // 一定要销毁强引用对象 否则创建软引用对象将毫无意义
+ user = null;
+ System.out.println("创建大对象之前:" + softReference.get());
+ try{
+ // 模拟堆内存资源紧张 看软引用对象是否会被回收
+ byte[] bytes = new byte[1024 * 1024 *7];
+ }catch (Throwable e) {
+ e.printStackTrace();
+ }finally {
+ System.out.println("创建大对象之后:" + softReference.get());
+ }
+ }
+}
+
+class User {
+ private String name;
+
+ public User(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ return "User{" +
+ "name='" + name + '\'' +
+ '}';
+ }
+}
+
弱引用也是用来描述那些非必需对象,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。
+在系统GC时,只要发现弱引用,不管系统堆空间使用是否充足,都会回收掉只被弱引用关联的对象。 +由于垃圾回收器的线程通常优先级很低,因此,并不一定能很快地发现持有弱引用的对象。在这种情况下,弱引用对象可以存在较长的时间。
+弱引用和软引用一样,在构造弱引用时,也可以指定一个引用队列,当弱引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况。
+软引用、弱引用都非常适合来保存那些可有可无的缓存数据。如果这么做,当系统内存不足时,这些缓存数据会被回收,不会导致内存溢出。 +而当内存资源充足时,这些缓存数据又可以存在相当长的时间,从而起到加速系统的作用。
+弱引用测试
+/**
+ * 弱引用测试
+ */
+public class MainTest {
+ public static void main(String[] args) {
+ WeakReference<User> weakReference = new WeakReference<>(new User("hello"));
+ System.out.println("建议GC之前:" + weakReference.get());
+ System.gc();
+ System.out.println("建议GC之后:" + weakReference.get());
+ }
+}
+
+class User {
+ private String name;
+
+ public User(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ return "User{" +
+ "name='" + name + '\'' +
+ '}';
+ }
+}
+
虚引用也称为“幽灵引用”或者“幻影引用”,是所有引用类型中最弱的一个。
+一个对象是否有虚引用的存在,完全不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它和没有引用几乎是一样的,随时都可能被垃圾回收器回收。
+它不能单独使用,也无法通过虚引用来获取被引用的对象。当试图通过虚引用的get()
方法取得对象时,总是null
;
为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。比如:能在这个对象被收集器回收时收到一个系统通知。 +虚引用必须和引用队列一起使用。 虚引用在创建时必须提供一个引用队列作为参数。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况。 +由于虚引用可以跟踪对象的回收时间,因此,也可以将一些资源释放操作放置在虚引用中执行和记录。
+虚引用测试
+/**
+ * 虚引用测试
+ */
+public class MainTest {
+ // 当前类对象的声明
+ public static MainTest obj;
+ // 引用队列
+ static ReferenceQueue<MainTest> phantomQueue = null;
+
+ @Override
+ protected void finalize() throws Throwable {
+ super.finalize();
+ System.out.println("调用当前类的finalize方法");
+ obj = this;
+ }
+
+ public static void main(String[] args) {
+ Thread thread = new Thread(() -> {
+ while(true) {
+ if (phantomQueue != null) {
+ PhantomReference<MainTest> objt = null;
+ try {
+ objt = (PhantomReference<MainTest>) phantomQueue.remove();
+ } catch (Exception e) {
+ e.getStackTrace();
+ }
+ if (objt != null) {
+ System.out.println("追踪垃圾回收过程:PhantomReferenceTest实例被GC了");
+ }
+ }
+ }
+ }, "t1");
+ thread.setDaemon(true);
+ thread.start();
+
+ phantomQueue = new ReferenceQueue<>();
+ obj = new MainTest();
+ // 构造了PhantomReferenceTest对象的虚引用,并指定了引用队列
+ PhantomReference<MainTest> phantomReference = new PhantomReference<>(obj, phantomQueue);
+ try {
+ System.out.println(phantomReference.get());
+ // 去除强引用
+ obj = null;
+ // 第一次进行GC,由于对象可复活,GC无法回收该对象
+ System.out.println("第一次GC操作");
+ System.gc();
+ Thread.sleep(1000);
+ if (obj == null) {
+ System.out.println("obj 是 null");
+ } else {
+ System.out.println("obj 不是 null");
+ }
+ System.out.println("第二次GC操作");
+ obj = null;
+ System.gc();
+ Thread.sleep(1000);
+ if (obj == null) {
+ System.out.println("obj 是 null");
+ } else {
+ System.out.println("obj 不是 null");
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+
+ }
+ }
+}
+
+ +
+ + + + + +在Java中,类加载器把一个类装入JVM中,要经过以下步骤: 加载、验证、准备、解析和初始化。其中验证,准备,解析统称为连接。 这5个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段发生在初始化阶段之后。
+ +类加载器只负责class文件的加载,至于它是否可以运行,则由执行引擎(Execution Engine)决定。
+被加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量。
+类加载过程流程图:
+ + +++加载阶段是类加载过程的第一个阶段。在这个阶段,JVM 的主要目的是将字节码从各个位置(网络、磁盘等)转化为二进制字节流加载到内存中,接着会为这个类在 JVM 的方法区创建一个对应的 Class 对象,这个 Class 对象就是这个类各种数据的访问入口。
+
类的加载指的是将类的.class
文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class
对象,用来封装类在方法区内的数据结构.简单来说,加载指的是把class字节码文件从各个来源通过类加载器装载入内存中。
类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class
文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError
错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。
JVM不是一开始就把所有的类都加载进内存中,而是只有第一次遇到某个需要运行的类时才会加载,且只加载一次。
+JVM在类加载阶段作用:
+java.lang.Class
对象作为方法区的这个类的各种数据访问入口.JVM会在该阶段对二进制字节流进行校验,只有符合JVM字节码规范的才能被 JVM 正确执行。
+大致都会完成以下四个阶段的验证
+为静态变量在方法取分配内存,并设置默认初始值。将在方法区中进行分配.
+举个例子:
+public String var1 = "var1";
+public static String var2 = "var2";
+public static final String var3 = "var3";
+
变量var1
不会被分配内存,但是var2
会被分配.var2
会被分配初始值为null
而不是’var2'.
这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false
等),而不是被在Java代码中被显式地赋予的值。
实例变量会在对象实例化时随着对象一块分配在Java堆中。
+这里不包含final
修饰的static
,因为final
在编译的时候就已经分配了.也就是说var3
被分配的值为’var3'
虚拟机将常量池中的符号引用替换为直接引用.
+符号引用
+符号引用是编译原理中的概念,是相对于直接引用来说的。主要包括了以下三类常量: 类和接口的全限定名 字段的名称和描述符 方法的名称和描述符.
+++符号引用 :符号引用以一组符号来描述所引用的目标。符号引用可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,符号引用和虚拟机的布局无关。
+
在编译的时候每个java类都会被编译成一个class文件,但在编译的时候虚拟机并不知道所引用类的地址,所以就用符号引用来代替,而在这个解析阶段就是为了把这个符号引用转化成为真正的地址的阶段。
+直接引用
+直接引用和虚拟机的布局是相关的,不同的虚拟机对于相同的符号引用所翻译出来的直接引用一般是不同的。 +如果有了直接引用,那么直接引用的目标一定被加载到了内存中。 +直接引用通过对符号引用进行解析,找到引用的实际内存地址。
+解析操作往往会伴随者JVM在执行完初始化之后再执行。 +解析动作主要针对接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等。
+初始化阶段就是执行类构造器<clinit>()
的过程。
+++
<clinit>()
可理解为类中的方法。
此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
+也就是说,当我们代码中包含static变量的时候,就会有<clinit>()
方法
在准备阶段,静态变量已经被赋过默认初始值,而在初始化阶段,静态变量将被赋值为代码期望赋的值.
+举个例子
+public static String var2 = "var2";
+
在准备阶段变量var2
的值为null
,在初始化阶段赋值为’var2'.
在Java中对类变量进行初始值设定有两种方式:
+new
一个对象需要初始化final
修饰的字段,在编译时就被放入静态常量池的字段除外.)Class.forName("");
对类反射调用的时候,该类需要初始化main()
方法),需要初始化若该类具有父类,JVM会保证子类的
初始化顺序
+父类静态域 --> 子类静态域 --> 父类成员初始化 -->父类构造块 --->父类构造方法 -->子类成员初始化 -->子类构造块 -->子类构造方法
+
一些初始化规则:
++++
+- 类从顶至底的顺序初始化,所以声明在顶部的字段的早于底部的字段初始化
+- 超类早于子类和衍生类的初始化
+- 如果类的初始化是由于访问静态域而触发,那么只有声明静态域的类才被初始化,而不会触发超类的初始化或者子类的
+- 初始化即使静态域被子类或子接口或者它的实现类所引用。
+- 接口初始化不会导致父接口的初始化。
+- 静态域的初始化是在类的静态初始化期间,非静态域的初始化时在类的实例创建期间。这意味这静态域初始化在非静态域之前。
+- 非静态域通过构造器初始化,子类在做任何初始化之前构造器会隐含地调用父类的构造器,他保证了非静态或实例变量(父类)初始化早于子类。
+- 虚拟机必须保证一个类的
+<clinit>()
方法在多线程下被同步加锁。也就是说类只能被初始化一次。
JVM设计者把类加载阶段中的"通过’类全名’来获取定义此类的二进制字节流" 这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。
+从虚拟机的角度来说,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader
),该类加载器使用C++语言实现(这里仅限于Hotspot
,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),属于虚拟机自身的一部分。
+另外一种就是自定义类加载器,这些类加载器是由Java语言实现,独立于JVM外部,并且全部继承自抽象类java.lang.ClassLoader
。
public class ClassloaderTest {
+ public static void main(String[] args) {
+ // 获取系统类加载器
+ ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
+ // sun.misc.Launcher$AppClassLoader@18b4aac2
+ System.out.println(systemClassLoader);
+
+ // 获取其上层的:扩展类加载器
+ ClassLoader extClassLoader = systemClassLoader.getParent();
+ // sun.misc.Launcher$ExtClassLoader@5cad8086
+ System.out.println(extClassLoader);
+
+ // 试图获取 启动类加载器
+ ClassLoader bootstrapClassLoader = extClassLoader.getParent();
+ // null 不能获取到启动类加载器
+ System.out.println(bootstrapClassLoader);
+
+ // 获取自定义加载器
+ ClassLoader classLoader = ClassloaderTest.class.getClassLoader();
+ // sun.misc.Launcher$AppClassLoader@18b4aac2
+ System.out.println(classLoader);
+
+ // 获取String类型的加载器
+ // Java 核心包都是用启动类加载器加载的
+ ClassLoader classLoader1 = String.class.getClassLoader();
+ // null
+ System.out.println(classLoader1);
+ }
+}
+
可以看出 启动类加载器无法直接通过代码获取,同时目前用户代码所使用的加载器为系统类加载器。同时我们通过获取String类型的加载器,发现是null, +那么说明String类型是通过根加载器进行加载的,也就是说Java的核心类库都是使用根加载器进行加载的。
+获取启动类加载器加载的路径
+public class ClassloaderTest {
+ public static void main(String[] args) {
+ System.out.println("*********启动类加载器加载的路径************");
+ // 获取BootstrapClassLoader 能够加载的API的路径
+ URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
+ for (URL url : urls) {
+ System.out.println(url.toExternalForm());
+ }
+
+ // 从上面路径中,随意选择一个类,来看看他的类加载器是什么:得到的是null,说明是 根加载器
+ ClassLoader classLoader = Provider.class.getClassLoader();
+ System.out.println(classLoader);
+
+ }
+}
+
JAVAHOME/jre/1ib/rt.jar、resources.jar
或sun.boot.class.path
路径下的内容),用于提供JVM自身需要的类。java.lang.ClassLoader
,没有父加载器。sun.misc.Launcher$ExtClassLoader
实现。ClassLoader
类。java.ext.dirs
系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/1ib/ext
子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。sun.misc.LaunchersAppClassLoader
实现。classpath
或系统属性java.class.path
指定路径下的类库。classLoader#getSystemclassLoader()
方法可以获取到该类加载器。++PS 为什么会有自定义类加载器?
++
+- 一方面是由于java代码很容易被反编译,如果需要对自己的代码加密的话,可以对编译后的代码进行加密,然后再通过实现自己的自定义类加载器进行解密,最后再加载。
+- 另一方面也有可能从非标准的来源加载代码,比如从网络来源,那就需要自己实现一个类加载器,从指定源进行加载。
+自定义加载器使用场景
++
+- 隔离加载类
+- 修改类加载的方式
+- 扩展加载源
+- 防止源码泄漏
+
若要实现自定义类加载器,只需要继承java.lang.ClassLoader
类.按需重写相关方法即可.
findClass
方法loadClass
方法++在JDK1.2之前,类加载尚未引入双亲委派模式,因此实现自定义类加载器时常常重写
+loadClass
方法,提供双亲委派逻辑,从JDK1.2之后,双亲委派模式已经被引入到类加载体系中,自定义类加载器时不需要在自己写双亲委派的逻辑,因此不鼓励重写loadClass
方法,而推荐重写findClass
方法。在Java中,任意一个类都需要由加载它的类加载器和这个类本身一同确定其在java虚拟机中的唯一性,即比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提之下才有意义,否则,即使这两个类来源于同一个Class类文件,只要加载它的类加载器不相同,那么这两个类必定不相等(这里的相等包括代表类的Class对象的
+equals()
方法、isAssignableFrom()
方法、isInstance()
方法和instanceof
关键字的结果)。
重写findClass
方法
准备一个class文件,编译后放到C盘根目录下
+public class People {
+ private String name;
+ public String getName() {
+ return name;
+ }
+ public void setName(String name) {
+ this.name = name;
+ }
+}
+
自定义类加载器,继承ClassLoader
重写findClass
方法(其中defineClass
方法可以把二进制流字节组成的文件转换为一个java.lang.Class
)
public class MyClassLoader extends ClassLoader {
+
+ public MyClassLoader(){}
+
+ public MyClassLoader(ClassLoader parent)
+ {
+ super(parent);
+ }
+
+ protected Class<?> findClass(String name) throws ClassNotFoundException {
+ File file = new File("C:/People.class");
+ try{
+ byte[] bytes = getClassBytes(file);
+ //defineClass方法可以把二进制流字节组成的文件转换为一个java.lang.Class
+ Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
+ return c;
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+
+ return super.findClass(name);
+ }
+
+ private byte[] getClassBytes(File file) throws Exception {
+ // 这里要读入.class的字节,因此要使用字节流
+ FileInputStream fis = new FileInputStream(file);
+ FileChannel fc = fis.getChannel();
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ WritableByteChannel wbc = Channels.newChannel(baos);
+ ByteBuffer by = ByteBuffer.allocate(1024);
+
+ while (true){
+ int i = fc.read(by);
+ if (i == 0 || i == -1)
+ break;
+ by.flip();
+ wbc.write(by);
+ by.clear();
+ }
+ fis.close();
+ return baos.toByteArray();
+ }
+}
+
这种层次关系称为类加载器的双亲委派模型。 我们把每一层上面的类加载器叫做当前层类加载器的父加载器,当然,它们之间的父子关系并不是通过继承关系来实现的,而是使用组合关系来复用父加载器中的代码。 +该模型在JDK1.2期间被引入并广泛应用于之后几乎所有的Java程序中,但它并不是一个强制性的约束模型,而是Java设计者们推荐给开发者的一种类的加载器实现方式。
+双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上。 +因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
+在rt.jar
包中的java.lang.ClassLoader
类中,我们可以查看类加载实现过程loadClass
方法的代码,具体源码如下:
protected Class<?> loadClass(String name, boolean resolve)
+ throws ClassNotFoundException
+ {
+ synchronized (getClassLoadingLock(name)) {
+ // First, check if the class has already been loaded
+ // 首先检查该name指定的class是否有被加载
+ Class<?> c = findLoadedClass(name);
+ if (c == null) {
+ long t0 = System.nanoTime();
+ try {
+ if (parent != null) {
+ // 如果parent不为null,则调用parent的loadClass进行加载
+ c = parent.loadClass(name, false);
+ } else {
+ // parent为null,则调用BootstrapClassLoader进行加载
+ c = findBootstrapClassOrNull(name);
+ }
+ } catch (ClassNotFoundException e) {
+ // 如果从非空父类加载器中找不到类,则抛出ClassNotFoundException
+ }
+
+ if (c == null) {
+ // 如果仍然无法加载成功,则调用自身的findClass进行加载
+ long t1 = System.nanoTime();
+ c = findClass(name);
+
+ // 这是定义类加载器;记录统计数据
+ sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
+ sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
+ sun.misc.PerfCounter.getFindClasses().increment();
+ }
+ }
+ if (resolve) {
+ resolveClass(c);
+ }
+ return c;
+ }
+ }
+
根据代码以及代码中的注释可以很清楚地了解整个过程:
+先检查是否已经被加载过,如果没有则调用父加载器的loadClass()
方法,如果父加载器为空则默认使用启动类加载器作为父加载器。
+如果父类加载器加载失败,则先抛出ClassNotFoundException
,然后再调用自己的findClass()
方法进行加载。
使用这种模型来组织类加载器之间的关系的好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。
+例如java.lang.Object
类,无论哪个类加载器去加载该类,最终都是由启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。
+否则的话,如果不使用该模型的话,如果用户自定义一个java.lang.Object
类且存放在classpath
中,那么系统中将会出现多个Object类,应用程序也会变得很混乱。
+如果我们自定义一个rt.jar
中已有类的同名Java类,会发现JVM可以正常编译,但该类永远无法被加载运行。
自定义string类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class), +报错信息说没有main方法,就是因为加载的是rt.jar包中的string类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
+在JVM中表示两个class对象是否为同一个类存在两个必要条件:
+换句话说,在JvM中,即使这两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载, +但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的。
+JVM必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。 +当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。
+Java程序对类的使用方式分为:王动使用和被动使用。 主动使用,又分为七种情况:
+除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。
++ +
+ + + + + +直接内存不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。 +直接内存是在Java堆外的、直接向系统申请的内存区间。
+操作直接内存演示代码:
+public class MainTest {
+ public static void main(String[] args) {
+ ByteBuffer allocate = ByteBuffer.allocate(1024 * 1024 * 1024);
+
+ System.out.println("直接内存分配完成 ...");
+ Scanner scanner = new Scanner(System.in);
+ scanner.next();
+
+ System.out.println("直接内存开始释放");
+ System.gc();
+
+ scanner.next();
+ System.out.println("退出!");
+ }
+}
+
使用NIO,通过存在堆中的直接内存操作本地内存
++
+通常,访问直接内存的速度会优于Java堆。即读写性能高。因此出于性能考虑,读写频繁的场合可能会考虑使用直接内存。 +Java的NIO库允许Java程序使用直接内存,用于数据缓冲区。
+直接内存也存在 OutOfMemoryError
异常:OutOfMemoryError: Direct buffer memory
public class MainTest {
+ private static final int BUFFER = 1024 * 1024 * 20;
+ public static void main(String[] args) {
+ ArrayList<ByteBuffer> list = new ArrayList<>();
+ int count = 0;
+ try {
+ while(true){
+ ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
+ list.add(byteBuffer);
+ count++;
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ } finally {
+ System.out.println(count);
+ }
+ }
+}
+
由于直接内存在Java堆外,因此它的大小不会直接受限于-Xmx
指定的最大堆大小,但是系统内存是有限的,
+Java堆和直接内存的总和依然受限于操作系统能给出的最大内存。
直接内存缺点:
+直接内存大小可以通过 MaxDirectMemorySize
设置,如果不指定,默认与堆的最大值-Xmx
参数值一致.
+ +
+ + + + + +执行引擎是Java虚拟机核心的组成部分之一,属于JVM的下层,里面包括 解释器、及时编译器、垃圾回收器。
+ +“虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力, +其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的,而虚拟机的执行引擎则是由软件自行实现的, +因此可以不受物理条件制约地定制指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式。
+JVM的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令, +它内部包含的仅仅只是一些能够被JVM所识别的字节码指令、符号表,以及其他辅助信息。 +那么,如果想要让一个Java程序运行起来,执行引擎的任务就是将字节码指令 解释/编译 为对应平台上的本地机器指令才可以。 +简单来说,JVM中的执行引擎充当了将高级语言翻译为机器语言的译者。
+所有的Java虚拟机的执行引擎输入,输出都是一致的:输入的是字节码二进制流,处理过程是字节码解析执行的等效过程,输出的是执行过程。
+执行引擎负责将字节码指令翻译为cpu执行的命令。在执行过程中究竟执行什么样的字节码指令完全依赖于PC寄存器; +每当执行完一项指令操作后,PC寄存器就会更新下一条需要被执行的指令地址; +当然方法在执行的过程中,执行引擎有可能会通过存储在局部变量表中的对象引用准确定位到存储在Java堆区中的对象实例信息,以及通过对象头中的元数据指针定位到目标对象的类型信息。
+ +大部分的程序代码转换成物理机的目标代码或虚拟机能执行的指令集之前,都需要经过图中的各个步骤:
+++ +1.前面橙色部分是生成字节码文件的过程,和JVM无关 +2.后面蓝色和绿色才是JVM需要考虑的过程
+
现在JVM在执行Java代码的时候,会将解释执行与编译执行二者结合起来进行。如今Java采用的是解释和编译混合的模式。
+执行引擎获取到,由 javac
将源码编译成字节码文件.class
之后,然后在运行的时候通过解释器 interpreter
转换成最终的机器码。
+另外JVM平台支持一种叫作即时编译的技术。即时编译的目的是避免函数被解释执行,而是将整个函数体编译成为机器码,这种方式可以使执行效率大幅度提升。
+
++为什么说Java是半解释半编译型语言? +最初的Java语言只有解释器,所以定位为“解释执行”还是比较准确的:先编译成字节码,再对字节码逐行用解释器解释执行; +后来Java也发展出来可以直接生成本地代码的编译器:JVM执行引擎中解释器和即时编译器共存的。故叫做半解释半编译型。
+
解释器(Interpreter):Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。
+解释器真正意义上所承担的角色就是一个运行时“翻译者”,将字节码文件中的内容“翻译”为对应平台的本地机器指令执行。 +当一条字节码指令被解释执行完成后,接着再根据PC寄存器中记录的下一条需要被执行的字节码指令执行解释操作。
+即时编译器(Just In Time Compiler):就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言。
+由于解释器在设计和实现上非常简单,因此除了Java语言之外,还有许多高级语言同样也是基于解释器执行的,
+比如Python、 Perl、Ruby
等。但是在今天,基于解释器执行已经沦落为低效的代名词,并且时常被一些C/C+ +程序员所调侃。
为了解决这个问题,JVM平台支持一种叫作即时编译的技术。 +即时编译的目的是避免函数被解释执行,而是将整个函数体编译成为机器码,每次函数执行时,只执行编译后的机器码即可,这种方式可以使执行效率大幅度提升。 +不过无论如何,基于解释器的执行模式仍然为中间语言的发展做出了不可磨灭的贡献。
+既然即时编译器执行效率比解释器执行效率高,那为什么还需要再使用解释器?
+当程序启动后,解释器可以马上发挥作用,省去编译的时间,立即执行。(解释器响应速度比即时编译器速度快)编译器要想发挥作用,把代码编译成本地代码,需要一定的执行时间。 +但编译为本地代码后,即时编译器执行效率高。
+在此模式下,当Java虚拟器启动时,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成后再执行,这样可以省去许多不必要的编译时间。 +随着时间的推移,编译器发挥作用,把越来越多的代码编译成本地代码,获得更高的执行效率。
+同时,解释执行在编译器进行激进优化不成立的时候,作为编译器的“逃生门”。
++++
JRockit
虚拟机是砍掉了解释器,也就是只采及时编译器。那是因为JRockit
只部署在服务器上, +一般已经有时间让他进行指令编译的过程了,对于响应来说要求不高,等及时编译器的编译完成后,就会提供更好的性能. +尽管JRockit
VM中程序的执行性能会非常高效,但程序在启动时必然需要花费更长的时间来进行编译。 +对于服务端应用来说,启动时间并非是关注重点,但对于那些看中启动时间的应用场景而言,或许就需要采用解释器与即时编译器并存的架构来换取一个平衡点。
JIT的编译器还分为了两种,分别是C1和C2,在HotSpot VM中内嵌有两个JIT编译器,分别为Client Compiler和Server Compiler; +但大多数情况下我们简称为C1编译器 和 C2编译器。开发人员可以通过如下命令显式指定Java虚拟机在运行时到底使用哪一种即时编译器。
+client:指定Java虚拟机运行在Client模式下,并使用C1编译器; +C1编译器会对字节码进行简单和可靠的优化,耗时短。以达到更快的编译速度。
+server:指定Java虚拟机运行在server模式下,并使用C2编译器; +C2进行耗时较长的优化,以及激进优化。但优化的代码执行效率更高。
+++选择Java HotSpot Server 虚拟机。64位版本的JDK只支持 Server VM,因此在这种情况下,该选项是隐式的。
+
在不同的编译器上有不同的优化策略,C1编译器上主要有方法内联,去虚拟化、冗余消除:
+C2的优化主要是在全局层面,逃逸分析是优化的基础。 +基于逃逸分析在C2上有如下几种优化:
+synchronized
;分层编译策略:程序解释执行(不开启性能监控)可以触发C1编译,将字节码编译成机器码,可以进行简单优化; +也可以加上性能监控,C2编译会根据性能监控信息进行激进优化。
+在Java7版本之后,一旦开发人员在程序中显式指定命令“-server"时,默认将会开启分层编译策略,由C1编译器和C2编译器相互协作共同来执行编译任务。
+总的来说,C2编译器启动时长比C1慢,系统稳定执行以后,C2编译器执行速度远快于C1编译器
+++关于编译器: +前端编译器:把 .java 文件转变成 .class 文件的过程; +后端编译器:把 .class 文件转变为 机器指令的过程;
+
是否需要启动即时编译器将字节码转换为机器指令,则需要根据代码的调用频率而定。 +一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体都可以被称之为“热点代码”,即时编译器在运行时会针对那些被频繁调用的热点代码做出深度优化,将其直接编译为本地的机器指令,以此来提升程序的性能。 +由于这种编译方式发生在方法的执行过程中,因此被称之为栈上替换,或简称为OSR(On Stack Replacement)编译。
+一个方法究竟要被调用多少次,或者一个循环体究竟需要执行多少次循环才可以达到这个标准? +必然需要一个明确的阈值,JIT编译器才会将这些“热点代码”编译为本地机器指令执行。这里主要依靠热点探测功能。
+目前HotSpot VM所采用的热点探测方式是基于计数器的热点探测。 +HotSpot 将会为每一个方法都建立2个不同类型的计数器,分别为方法调用计数器和回边计数器。
+这个计数器就用于统计方法被调用的次数,它的默认阈值在Client模式下是1500次,在Server模式下是10000次。超过这个阈值,就会触发即时编译。
+这个阀值可以通过虚拟机参数 -XX:CompileThreshold
来设定。
当一个方法被调用时,会先检查该方法是否存在被即时编译器编译过的版本,如果存在,则优先使用编译后的本地代码来执行。 +如果不存在已被编译过的版本,则将此方法的调用计数器值加1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阀值; +如果已超过阈值,那么将会向即时编译器提交一个该方法的代码编译请求;否则就通过解释器执行。
+它的作用是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”(Back Edge)。 +跟方法调用计数器搭配使用,如何两者相加总和超过计数器的阀值,那么就会除法即时编译器。 +显然,建立回边计数器统计的目的就是为了触发栈上替换编译。
+如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,可理解为一段时间之内方法被调用的次数。 +当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器就会被减少一半, +这个过程称为方法调用计数器热度的衰减,而这段时间就称为此方法统计的半衰周期。
+可以使用-XX:CounterHalfLifeTime
参数设置半衰周期的时间,单位是秒。
进行热度衰减的动作是在虚拟机进行垃圾收集时顺便进行的,可以使用虚拟机参数 -XX:-UseCounterDecay
来关闭热度衰减,让方法计数器统计方法调用的绝对次数;
+这样,只要系统运行时间足够长,绝大部分方法都会被编译成本地代码。
缺省情况下HotSpot VM是采用解释器与即时编译器并存的架构,当然开发人员可以根据具体的应用场景,通过命令显式地为Java虚拟机指定在运行时到底是完全采用解释器执行,还是完全采用即时编译器执行。
+-Xint
:完全采用解释器模式执行程序;-Xcomp
:完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行;-Xmixed
:采用解释器+即时编译器的混合模式共同执行程序。JDK9 引入了静态提前编译器(Ahead of Time Compiler)。
+Java 9引入了实验性AOT编译工具AOTC。它借助了Graal编译器,将所输入的Java类文件转换为机器码,并存放至生成的动态共享库之中。
+++静态提前编译器: 直接把 .java 文件编译成机器指令的过程。 +.java -> .class -> (使用jaotc) -> .so
+
所谓AOT编译,是与即时编译相对立的一个概念。 +即时编译指的是在程序的运行过程中,将字节码转换为可在硬件上直接运行的机器码,并部署至托管环境中的过程。 +而AOT编译指的则是,在程序运行之前,便将字节码转换为机器码的过程。
+优点:
+缺点:
+自JDK10起,HotSpot又加入了一个全新的及时编译器:Graal编译器; +编译效果短短几年时间就追评了G2编译器,未来可期。
+特点:
+++二进制编码 –> 指令、指令集 –> 汇编语言 –> 高级语言 –> ?
+
用二进制编码方式表示的指令,叫做机器指令码。最开始,人们就用它采编写程序,这就是机器语言。 +机器语言虽然能够被计算机理解和接受,但和人们的语言差别太大,不易被人们理解和记忆,并且用它编程容易出差错。
+由于机器码是有0和1组成的二进制序列,可读性实在太差,于是人们发明了指令。 +指令就是把机器码中特定的0和1序列,简化成对应的指令(一般为英文简写,如mov,inc等),可读性稍好。
+不同的硬件平台,各自支持的指令,是有差别的。因此每个平台所支持的指令,称之为对应平台的指令集。
+由于指令的可读性还是太差,于是人们又发明了汇编语言。 +由于计算机只认识指令码,所以用汇编语言编写的程序还必须翻译成机器指令码,计算机才能识别和执行。
+为了使计算机用户编程序更容易些,后来就出现了各种高级计算机语言。 +高级语言比机器语言、汇编语言更接近人的语言当计算机执行高级语言编写的程序时,仍然需要把程序解释和编译成机器的指令码。
+高级语言也不是直接翻译成机器指令,而是翻译成汇编语言,在由汇编语言翻译成机器指令;当然也可以先翻译为字节码,在由字节码翻译为机器指令。
+++字节码: +字节码是一种中间状态(中间码)的二进制代码(文件),它比机器码更抽象,需要直译器转译后才能成为机器码。 +字节码主要为了实现特定软件运行和软件环境、与硬件环境无关。 +实现方式:编译器将源码编译成字节码,特定平台上的虚拟机器将字节码转译为可以直接执行的指令。
+
+ +
+ + + + + +Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 +另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。
+ +运行时数据区域包括
+其中:方法区、堆为线程共享;程序计数寄存器、虚拟机栈、本地方法栈 为线程私有。
+堆针对一个JVM进程来说是唯一的,也就是一个进程只有一个JVM,但是进程包含多个线程,他们是共享同一堆空间的。 +一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
+Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。堆内存的大小是可以调节的。
+++-Xms10m:最小堆内存
+-Xmx10m:最大堆内存
+
《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。 +所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer,TLAB)。
+《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。
+++The heap is the run-time data area from which memory for all class instances and arrays is allocated
+
“几乎”所有的对象实例都在这里分配内存。—从实际使用角度看的。因为还有一些对象是在栈上分配的。
+数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。
+在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
+堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。 +
+Java 7及之前堆内存逻辑上分为三部分:新生区+养老区+永久区
+Java 8及之后堆内存逻辑上分为三部分:新生区+养老区+元空间
+堆空间内部结构,JDK1.8之前从 永久代 替换成 元空间。
+Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,可以通过选项"-Xmx"和"-Xms"来进行设置。
+++ +“-Xms"用于表示堆区的起始内存,等价于 -XX:InitialHeapSize +“-Xmx"则用于表示堆区的最大内存,等价于 -XX:MaxHeapSize
+
默认情况下,初始堆内存大小:物理电脑内存大小/64;最大堆内存大小:物理电脑内存大小/4。
+++在生产环境和开发环境,通常会将-Xms和-Xmx两个参数配置相同的值, +其目的是为了能够在 Java 垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
+
使用代码查看
+public class MainTest {
+ public static void main(String[] args) {
+ // 返回Java虚拟机中的堆内存总量
+ long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
+ // 返回Java虚拟机试图使用的最大堆内存
+ long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
+ System.out.println("-Xms:" + initialMemory + "M");
+ System.out.println("-Xmx:" + maxMemory + "M");
+ }
+}
+
程序启动加入-XX:+PrintGCDetails
参数
++ +jps -> jstat -gc 进程ID
+
一旦堆区中的内存大小超过“-Xmx"所指定的最大内存时,将会抛出outOfMemoryError
异常。
代码实现
+public class MainTest {
+ public static void main(String[] args) {
+ ArrayList<Object> list = new ArrayList<>();
+ while (true){
+// try {
+// Thread.sleep(1000000);
+// } catch (InterruptedException e) {
+// e.printStackTrace();
+// }
+ list.add(new Picture(new Random().nextInt(1024 * 1024)));
+ }
+ }
+}
+
+class Picture {
+ private int data;
+ public Picture(int data) {
+ this.data = data;
+ }
+}
+
+++
jvisual
工具在 jdk /bin/jvisualvm.exe +亦可在编译器 下载jvisual插件
出现OOM错误后,可以通过 VisualVM 这个工具查看具体是什么参数造成的 +
+存储在JVM中的Java对象,按照生命周期可以被划分为两类:
+一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速,生命周期短的,及时回收; +另外一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一致。
+Java堆区进一步细分的话,可以划分为年轻代和老年代。 +其中年轻代又可以划分为Eden区、Survivor0区和 Survivor1 区(有时也叫做from区、to区)。 +
+++没有明确规定,to 区是 Survivor1;这两个区域是不断进行交换的;是从一个区到另外一个区
+
测试用的代码,用于测试以下JVM参数
+public class MainTest {
+ public static void main(String[] args) {
+ System.out.println("start ...");
+ try {
+ Thread.sleep(1000000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+}
+
该参数是配置新生代与老年代在堆结构的占比。
+默认情况下,新生代:老年代 - > 1 : 2
+当发现在整个项目中,生命周期长的对象偏多,那么就可以通过调整年轻代与老年代的比例,来进行调优。
+该命令是调整eden区与survivor区比例。这个参数一般使用默认值就可以了。
+在HotSpot中,Eden空间和另外两个survivor空间缺省所占的比例是8:1:1, +当然开发人员可以通过选项“-XX:SurvivorRatio”调整这个空间比例。比如:-XX:SurvivorRatio=8。
+++PS 在实际开发中使用hotspot虚拟机,默认情况下不是8:1:1; +是因为虚拟机有一个自适应内存分配策略,可以通过
+-XX:-UseAdaptiveSizePolicy
关闭再来进行查看. +
几乎所有的Java对象都是在Eden区被new出来的。绝大部分的Java对象的销毁都在新生代进行了。(有些大的对象在Eden区无法存储时候,将直接进入老年代)
+IBM公司的专门研究表明,新生代中80%的对象都是“朝生夕死”的。
+可以使用选项"-Xmn"设置新生代最大内存大小。
+++ +PS 当 -Xmn 参数与 -XX:NewRatio 设置的值发生冲突时,会以 -Xmn 设置的具体值为准。
+
为新对象分配内存是一件非常严谨和复杂的任务,JM的设计者们不仅需要考虑内存如何分配、在哪里分配等问题, +并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中产生内存碎片。
+对象分配内存步骤: +
+可以用 -XX:MaxTenuringThreshold=N 进行设置幸存者区到老年代的GC扫描次数,默认15次。
+++PS 如果幸存者区满了? +如果Survivor区满了后,将会触发一些特殊的规则,也就是可能直接晋升老年代。 +需要特别注意,在Eden区满了的时候,才会触发MinorGC,而幸存者区满了后,不会触发MinorGC操作
+
代码演示对象分配过程
+// -Xms600m -Xmx600m
+public class HeapInstanceTest {
+ byte [] buffer = new byte[new Random().nextInt(1024 * 200)];
+ public static void main(String[] args) throws InterruptedException {
+ ArrayList<HeapInstanceTest> list = new ArrayList<>();
+ while (true) {
+ list.add(new HeapInstanceTest());
+ Thread.sleep(10);
+ }
+ }
+}
+
打开VisualVM
图形化界面,通过VisualGC
进行动态化查看
+
++总结:
++
+- 针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to区
+- 关于垃圾回收:频繁在新生区收集,很少在老年代收集,几乎不再永久代和元空间进行收集
+- 新生代采用复制算法的目的:是为了减少内碎片
+
JVM的调优的一个环节,也就是垃圾收集,我们需要尽量的避免垃圾回收,因为在垃圾回收的过程中,容易出现 STW 的问题; +而 Major GC 和 Full GC出现 STW 的时间,是Minor GC的10倍以上。
+++STW: Java中 Stop-The-World 机制简称 STW ,是在执行垃圾收集算法时,Java 应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。 +Java中一种全局暂停现象,全局停顿,所有 Java 代码停止,native 代码可以执行,但不能与 JVM 交互;这些现象多半是由于 GC 引起。
+
JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。
+针对Hotspot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(FullGC)。
+当年轻代空间不足时,就会触发MinorGC,这里的年轻代满指的是Eden代满,Survivor满了不会引发Minor GC。每次Minor GC会清理年轻代的垃圾。 +因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
+Minor GC会引发STW,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。
+发生在老年代的GC,对象从老年代消失时,我们说 “Major GC” 或 “Full GC” 发生了。
+出现了MajorGc,经常会伴随至少一次的Minor GC,但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行MajorGC的策略选择过程。
+也就是在老年代空间不足时,会先尝试触发MinorGc。 +如果之后空间还不足,则触发Major GC,Major GC的速度一般会比MinorGc慢10倍以上,STW的时间更长,如果Major GC后,内存还不足,就报OOM了。
+触发Full GC执行的情况有如下五种:
+System.gc()
时,系统建议执行Full GC,但是不必然执行.测试GC代码
+public class MainTest {
+ public static void main(String[] args) {
+ int i = 0;
+ try {
+ List<String> list = new ArrayList<>();
+ String a = "awsl";
+ while(true) {
+ list.add(a);
+ a = a + a;
+ i++;
+ }
+ }catch (Exception e) {
+ e.getStackTrace();
+ }
+ }
+}
+
加入如下虚拟机参数
+-Xms10m -Xmx10m -XX:+PrintGCDetails
+
GC 日志
+[GC (Allocation Failure) [PSYoungGen: 1933K->496K(2560K)] 1933K->736K(9728K), 0.0009799 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
+[GC (Allocation Failure) [PSYoungGen: 2476K->480K(2560K)] 2716K->1464K(9728K), 0.0014628 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
+[Full GC (Ergonomics) [PSYoungGen: 2156K->0K(2560K)] [ParOldGen: 7128K->4559K(7168K)] 9284K->4559K(9728K), [Metaspace: 3029K->3029K(1056768K)], 0.0033635 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
+[GC (Allocation Failure) [PSYoungGen: 56K->128K(2560K)] 6663K->6735K(9728K), 0.0009897 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
+[Full GC (Ergonomics) [PSYoungGen: 128K->0K(2560K)] [ParOldGen: 6607K->6509K(7168K)] 6735K->6509K(9728K), [Metaspace: 3047K->3047K(1056768K)], 0.0040646 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
+[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 6509K->6509K(9728K), 0.0006842 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
+[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 6509K->6491K(7168K)] 6509K->6491K(9728K), [Metaspace: 3047K->3047K(1056768K)], 0.0039890 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
+Heap
+ PSYoungGen total 2560K, used 111K [0x00000007bfd00000, 0x00000007c0000000, 0x00000007c0000000)
+ eden space 2048K, 5% used [0x00000007bfd00000,0x00000007bfd1bf38,0x00000007bff00000)
+ from space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
+ to space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
+ ParOldGen total 7168K, used 6491K [0x00000007bf600000, 0x00000007bfd00000, 0x00000007bfd00000)
+ object space 7168K, 90% used [0x00000007bf600000,0x00000007bfc56f18,0x00000007bfd00000)
+ Metaspace used 3093K, capacity 4496K, committed 4864K, reserved 1056768K
+ class space used 338K, capacity 388K, committed 512K, reserved 1048576K
+Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
+
为什么要把Java堆分代?不分代就不能正常工作了吗?经研究,不同对象的生命周期不同。70%-99% 的对象是临时对象。
+不分代完全可以,分代的唯一理由就是优化GC性能。 +如果没有分代,那所有的对象都在一块,GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。比较耗费性能。 +而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。
+如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到survivor空间中,并将对象年龄设为1。 +对象在survivor区中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,其实每个JVM、每个GC都有所不同)时,就会被晋升到老年代。
+++PS: 对象晋升老年代的年龄阀值,可以通过选项
+-XX:MaxTenuringThreshold
来设置
针对不同年龄段的对象分配原则:
+优先分配到Eden。 +但是开发中比较长的字符串或者数组,会直接存在老年代。因为新创建的对象都是朝生夕死的,所以这个大对象可能也很快被回收,由于老年代触发Major GC的次数比 Minor GC要更少,因此可能回收起来就会比较慢
+大对象直接分配到老年代。 +尽量避免程序中出现过多的大对象
+长期存活的对象分配到老年代。
+动态对象年龄判断。
+如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到 MaxTenuringThreshold
中要求的年龄。
空间分配担保。
+就是经过Minor GC后,所有的对象都存活,因为Survivor比较小,所以就需要将Survivor无法容纳的对象,存放到老年代中。通过-XX:HandlePromotionFailure
参数来调节。
++堆空间都是共享的么? +不是,因为还有 TLAB 这个概念,在堆中划分出一块区域,为每个线程所独占,以此来保证线程安全。
+
TLAB全称:Thread Local Allocation Buffer
译为:线程本地分配缓冲区。
因为堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据, +由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的。 +为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。 +使用锁又会影响性能,TLAB应运而生。 +多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。
+ +从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。
+默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,当然我们可以通过选项-XX:TLABWasteTargetPercent
设置TLAB空间所占用Eden空间的百分比大小。
对象首先是通过TLAB开辟空间,如果不能放入,那么需要通过Eden来进行分配。
+尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选。
+可以通过选项-XX:UseTLAB
设置是否开启TLAB空间,默认是开启的。
+一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。
-XX:+PrintFlagsInitial:查看所有的参数的默认初始值
+-XX:+PrintFlagsFinal:查看所有的参数的最终值(可能会存在修改,不再是初始值)
+-Xms:初始堆空间内存(默认为物理内存的1/64)
+-Xmx:最大堆空间内存(默认为物理内存的1/4)
+-Xmn:设置新生代的大小。(初始值及最大值)
+-XX:NewRatio:配置新生代与老年代在堆结构的占比(默认是2)
+-XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例(默认是8)
+-XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄((默认是15)
+-XX:+PrintGCDetails:输出详细的GC处理日志
+-XX:+PrintGC - verbose:gc 打印gc简要信息
+-XX:HandlePromotionFailure:是否设置空间分配担保(默认true)
+++在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。 +如果大于,则此次Minor GC是安全的。 +如果小于,则虚拟机会查看-xx:HandlePromotionFailure设置值是否允担保失败。 +如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。 +如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的; +如果小于,则改为进行一次FullGC。 +如果HandlePromotionFailure=false,则改为进行一次Full Gc。 +在JDK7之后,HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略,观察openJDK中的源码变化,虽然源码中还定义了HandlePromotionFailure参数,但是在代码中已经不会再使用它。 +JDK7之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行FullGC。
+简而言之,在JDK7之后 -XX:HandlePromotionFailure=true 默认为true,且不会受到分配担保策略。
+
++随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。
+
在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。 +但是,有一种特殊情况,那就是如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。 +这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。
+还有基于openJDK深度定制的TaoBaoVM,其中创新的GCIH(GC invisible heap)技术实现off-heap: +将生命周期较长的Java对象从heap中移至heap外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的。
+逃逸分析是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。 +通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。 +逃逸分析的基本行为就是分析对象动态作用域:
+没有发生逃逸出方法的对象,则可以分配到栈上,随着方法执行的结束,栈空间就被移除,每个栈里面包含了很多栈帧,也就是发生逃逸分析
+public static StringBuffer createStringBuffer(String s1, String s2) {
+ StringBuffer sb = new StringBuffer();
+ sb.append(s1);
+ sb.append(s2);
+ return sb;
+}
+
如果想要StringBuffer sb对象不发生逃逸方法,则发生逃逸分析,可以这样写
+public static String createStringBuffer(String s1, String s2) {
+ StringBuffer sb = new StringBuffer();
+ sb.append(s1);
+ sb.append(s2);
+ return sb.toString();
+}
+
++如何快速的判断是否发生了逃逸分析,看new的对象实体是否在方法外被调用。
+
在JDK 1.7 版本之后,HotSpot中默认就已经开启了逃逸分析
+如果使用的是较早的版本,则可以通过:
+在开发中能使用局部变量的,就不要使用在方法外定义。
+将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会发生逃逸,对象可能是栈上分配的候选,而不是堆上分配。
+JIT即时编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配。 +分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须进行垃圾回收了。
+代码演示
+/**
+ * 通过代码来演示,逃逸分析前,逃逸分析后的变化情况
+ * 逃逸分析前虚拟机参数: -Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
+ * 逃逸分析后虚拟机参数: -Xmx1G -Xms1G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails
+ */
+public class MainTest {
+ public static void main(String[] args) throws InterruptedException {
+ long start = System.currentTimeMillis();
+ for (int i = 0; i < 100000000; i++) {
+ alloc();
+ }
+ long end = System.currentTimeMillis();
+ System.out.println("花费的时间为:" + (end - start) + " ms");
+
+ // 为了方便查看堆内存中对象个数,线程sleep
+ Thread.sleep(10000000);
+ }
+
+ private static void alloc() {
+ // 未发生逃逸
+ User user = new User();
+ }
+}
+class User {
+ private String name;
+ private String age;
+ private String gender;
+ private String phone;
+}
+
逃逸分析之前
+花费的时间为:881 ms
+
逃逸分析之后
+花费的时间为:5 ms
+
如果一个对象被发现只有一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
+线程同步的代价是相当高的,同步的后果是降低并发性和性能。
+在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。 +如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除。
+代码演示
+public void func() {
+ Object obj = new Object();
+ synchronized(obj) {
+ System.out.println(obj);
+ }
+}
+
当多个线程同时进来,每个线程都会重新new Object()
不会发生线程安全问题;还有obj对象的生命周期只在func()方法中,并不会被其他线程所访问到.
+所以在JIT编译阶段就会被优化掉,提高效率。
public void func() {
+ Object obj = new Object();
+ System.out.println(obj);
+}
+
标量是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。 +相对的,那些还可以分解的数据叫做聚合量(Aggregate),Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。
+在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化, +就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。
+有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
+代码演示
+public static void main(String args[]) {
+ alloc();
+}
+class Point {
+ private int x;
+ private int y;
+}
+private static void alloc() {
+ Point point = new Point(1,2);
+ System.out.println("point.x" + point.x + ";point.y" + point.y);
+}
+
经过标量替换后
+private static void alloc() {
+ int x = 1;
+ int y = 2;
+ System.out.println("point.x = " + x + "; point.y=" + y);
+}
+
这样做的好处是可以大大减少堆内存的占用。因为一旦不需要创建对象了,那么就不再需要分配堆内存了。 标量替换为栈上分配提供了很好的基础。
+关于逃逸分析的论文在1999年就已经发表了,但直到JDK1.6才有实现,而且这项技术到如今也并不是十分成熟。
+其根本原因就是无法保证逃逸分析的性能消耗一定能高于他的消耗。 +虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程。 +一个极端的例子,就是经过逃逸分析之后,发现没有一个对象是不逃逸的。那这个逃逸分析的过程就白白浪费掉了。
+虽然这项技术并不十分成熟,但是它也是即时编译器优化技术中一个十分重要的手段。 +注意到有一些观点,认为通过逃逸分析,JVM会在栈上分配那些不会逃逸的对象,这在理论上是可行的,但是取决于JvM设计者的选择。 +oracle Hotspot JVM中并未这么做,这一点在逃逸分析相关的文档里已经说明,所以可以明确所有的对象实例都是创建在堆上。
+目前很多书籍还是基于JDK7以前的版本,JDK已经发生了很大变化,intern字符串的缓存和静态变量曾经都被分配在永久代上,而永久代已经被元数据区取代。 +但是,intern字符串缓存和静态变量并不是被转移到元数据区,而是直接在堆上分配,所以这一点同样符合前面一点的结论:对象实例都是分配在堆上。
+年轻代是对象的诞生、成长、消亡的区域,一个对象在这里产生、应用,最后被垃圾回收器收集、结束生命。
+老年代放置长生命周期的对象,通常都是从survivor区域筛选拷贝过来的Java对象。 +当然,也有特殊情况,我们知道普通的对象会被分配在TLAB上;如果对象较大,JVM会试图直接分配在Eden其他位置上; +如果对象太大,完全无法在新生代找到足够长的连续空闲空间,JVM就会直接分配到老年代。当GC只发生在年轻代中,回收年轻代对象的行为被称为MinorGC。
+当GC发生在老年代时则被称为MajorGc或者FullGC。一般的,MinorGc的发生频率要比MajorGC高很多,即老年代中垃圾回收发生的频率将大大低于年轻代。
++ +
+ + + + + +Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 +另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。
+ +运行时数据区域包括
+其中:方法区、堆为线程共享;程序计数寄存器、虚拟机栈、本地方法栈 为线程私有。
+++尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。 +”但对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。
+
方法区看作是一块独立于Java堆的内存空间。下图说明了栈、堆、方法区的交互关系 +
+java.lang.OuOfMemoryError:PermGen space
或者java.lang.OutOfMemoryError:Metaspace
在JDK7及以前,习惯上把方法区,称为永久代。JDK8开始,使用元空间取代了永久代。 +JDK 1.8后,元空间存放在直接内存中。
+元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存。
+永久代、元空间二者并不只是名字变了,内部结构也调整了。 +根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出OOM异常。
+方法区的大小不必是固定的,JVM可以根据应用的需要动态调整。
+通过参数-XX:Permsize=size
来设置永久代初始分配空间。默认值是20.75M
+通过参数-XX:MaxPermsize=size
来设定永久代最大可分配空间。32位机器默认是64M,64位机器模式是82M
+当JVM加载的类信息容量超过了这个值,会报异常OuOfMemoryError:PermGen space
。
元空间大小可以使用参数 -XX:MetaspaceSize=size
和 -XX:MaxMetaspaceSize=size
来指定。
默认值依赖于平台。windows下,-XX:MetaspaceSize
是21M,-XX:MaxMetaspaceSize
的值是-1,由于直接存放在直接内存中所以没有限制。
与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。
+如果元数据区发生溢出,虚拟机一样会抛出异常OutOfMemoryError:Metaspace
。
-XX:MetaspaceSize=size
设置初始的元空间大小。对于一个64位的服务器端JVM来说,其默认的-XX:MetaspaceSize
值为21MB。
+这就是初始的高水位线,一旦触及这个水位线,Ful1GC将会被触发并卸载没用的类即这些类对应的类加载器不再存活然后这个高水位线将会重置。
+新的高水位线的值取决于GC后释放了多少元空间。
+如果释放的空间不足,那么在不超过MaxMetaspaceSize
时,适当提高该值。如果释放空间过多,则适当降低该值。
如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。
+通过垃圾回收器的日志可以观察到Ful1GC多次调用。
+为了避免频繁地GC,建议将-XX:MetaspaceSize=size
设置为一个相对较高的值。
JDK8 方法区/元空间 OOM代码演示
+设置虚拟机参数
+-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
+
public class MainTest extends ClassLoader{
+ public static void main(String[] args) {
+ MainTest mainTest = new MainTest();
+ int count = 0;
+ try {
+ for (int i = 0; i < 1000; i++) {
+
+ ClassWriter classWriter = new ClassWriter(0);
+ classWriter.visit(Opcodes.V1_8,Opcodes.ACC_PUBLIC ,"Class" + i,null,"java/lang/Object",null);
+
+ byte[] bytes = classWriter.toByteArray();
+ mainTest.defineClass("Class" + i, bytes, 0, bytes.length);
+ count ++;
+ }
+ }finally {
+ System.out.println(count);
+ }
+ }
+}
+
如何解决OOM?
+要解决ooM异常或heap space的异常,一般的手段是首先通过内存映像分析工具(如Ec1ipse Memory Analyzer)对dump出来的堆转储快照进行分析, +重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow).
+如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。 +于是就能找到泄漏对象是通过怎样的路径与GCRoots相关联并导致垃圾收集器无法自动回收它们的。 +掌握了泄漏对象的类型信息,以及GCRoots引用链的信息,就可以比较准确地定位出泄漏代码的位置。
+++内存泄漏: 有大量的引用指向某些对象,但是这些对象以后不会使用了,但是因为它们还和GC ROOT有关联,所以导致以后这些对象也不会被回收,这就是内存泄漏的问题
+
++方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
+
展示方法区内部结构,演示代码
+public class MainTest {
+
+ private String string = "awsl";
+
+ public static void context() {
+ try {
+ int a = 0;
+ int b = 20/a;
+ }catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ private String context2() {
+ return string;
+ }
+
+ public static void main(String[] args) {
+ new MainTest().context2();
+ context();
+ }
+
+}
+
编译该类,找到该类的class文件,打开终端执行javap -v MainTest.class > test.txt
命令,在当前目录会生成test.txt
文件.
+打开即可查看反编译后的字节码信息。
对每个加载的类型(类class、接口interface、枚举enum、注解annotation),Jvm必须在方法区中存储以下类型信息:
+// ...
+public class content.posts.jvm.MainTest
+ minor version: 0
+ major version: 52
+ flags: ACC_PUBLIC, ACC_SUPER
+//...
+
JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。
+域的相关信息包括:域名称、域类型、域修饰符(public,private,protected,static,final,volatile,transient的某个子集)
+// ...
+Constant pool:
+ #1 = Methodref #10.#35 // java/lang/Object."<init>":()V
+ #2 = String #36 // awsl
+ #3 = Fieldref #6.#37 // content/posts/jvm/MainTest.string:Ljava/lang/String;
+
+// ...
+
JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:
+public,private,protected,static,final,synchronized,native,abstract
的一个子集)bytecodes
)、操作数栈、局部变量表及大小(abstract和native
方法除外)abstract和native
方法除外)每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
+ public static void context();
+ descriptor: ()V
+ flags: ACC_PUBLIC, ACC_STATIC
+ Code:
+ stack=2, locals=2, args_size=0
+ 0: iconst_0
+ 1: istore_0
+ 2: bipush 20
+ 4: iload_0
+ 5: idiv
+ 6: istore_1
+ 7: goto 15
+ 10: astore_0
+ 11: aload_0
+ 12: invokevirtual #5 // Method java/lang/Exception.printStackTrace:()V
+ 15: return
+ // 异常表
+ Exception table:
+ from to target type
+ 0 7 10 Class java/lang/Exception
+ // 代码字节码指令行号对照表
+ LineNumberTable:
+ line 11: 0
+ line 12: 2
+ line 15: 7
+ line 13: 10
+ line 14: 11
+ line 16: 15
+ // 局部变量表
+ LocalVariableTable:
+ Start Length Slot Name Signature
+ 2 5 0 a I
+ 11 4 0 e Ljava/lang/Exception;
+ StackMapTable: number_of_entries = 2
+ frame_type = 74 /* same_locals_1_stack_item */
+ stack = [ class java/lang/Exception ]
+ frame_type = 4 /* same */
+
静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分。
+类变量被类的所有实例共享,即使没有类实例时,也可以访问它:
+public class MainTest {
+
+ public static void main(String[] args) {
+ Test test = null;
+ // 相当于 Test.hello();
+ test.hello();
+ // Test.count;
+ System.out.println(test.count);
+ }
+}
+
+class Test {
+ public static int count = 1;
+ public static final int number = 2;
+
+ public static void hello() {
+ System.out.println("hello!");
+ }
+}
+
全局常量就是使用 static final 进行修饰
+被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。
+代码如下
+public class MethodAreaDemo {
+ public static void main(String args[]) {
+ int x = 500;
+ int y = 100;
+ int a = x / y;
+ int b = 50;
+ System.out.println(a+b);
+ }
+}
+
反编译后该方法字节码指令
+// ....
+ // 栈的最大深度为3 局部变量表长度为5 参数长度为1
+ stack=3, locals=5, args_size=1
+ // 将500压入操作数栈
+ 0: sipush 500
+ // 在局部变量表里存放 500
+ 3: istore_1
+ // 将100压入栈
+ 4: bipush 100
+ // 在局部变量表里存放 100
+ 6: istore_2
+ // 将 500 从局部变量表里边取出,并压入操作数栈
+ 7: iload_1
+ // 将 100 从局部变量表里边取出,并压入操作数栈
+ 8: iload_2
+ // 调用 CPU 执行除法 500/100=5
+ 9: idiv
+ // 存储在局部变量表里
+ 10: istore_3
+ // 将50压入栈中
+ 11: bipush 50
+ // 将 50 存储在局部变量表中
+ 13: istore 4
+ // 获取 #2 地址上类或接口字段的值并将其推入操作数栈
+ 15: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
+ // 将 5 从本地变量表取出放到栈中
+ 18: iload_3
+ // 将50从本地变量表,压入栈
+ 19: iload 4
+ // 调用cpu执行加法 5 + 50=55
+ 21: iadd
+ // 虚方法调用 #3 中的方法
+ // JVM会根据这个方法的描述,创建新的栈桢,方法的参数从操作数栈中弹出来,压入虚拟机栈,然后虚拟机会开始执行虚拟机栈上最上面的栈桢
+ 22: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
+ // void 类型返回 main方法执行结束
+ 25: return
+// ...
+
只有Hotspot
才有永久代(方法区具体实现)。BEA JRockit、IBMJ9
等来说,是不存在永久代的概念的。
Hotspot中方法区的变化
+JDK版本 | +方法区变化 | +
---|---|
JDK1.6及以前 | +有永久代,字符串常量池、静态变量存储在永久代上 | +
JDK1.7 | +有永久代,但已经逐步 “去永久代”,字符串常量池,静态变量移除,保存在堆中 | +
JDK1.8 | +无永久代,类型信息,字段,方法,常量保存在本地内存的元空间,但字符串常量池、静态变量仍然在堆中 | +
根据官方的解释:替代是JRockit
和HotSpot
融合后的结果,因为JRockit
没有永久代,所以hotspot
用元空间替代了永久代。
由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间,这项改动是很有必要的,原因有:
+因为永久代设置空间大小是很难确定的。 +在某些场景下,如果动态加载类过多,容易产生方法区的OOM。 +比如某个实际Web工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误。 +而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。 因此,默认情况下,元空间的大小仅受本地内存限制。
+对永久代进行调优是很困难的。 +因为FullGC的花费时间是MinorGC的10倍,所以我们可以降低GC的频率,尽量不让方法区执行GC来提高效率。 +方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不在使用的类型。
+JDK7中将StringTable
放到了堆空间中。因为对永久代的回收效率很低,只有在Full GC的时候才会触发。
++Full GC 是老年代的空间不足、永久代不足时才会触发。
+
这就导致StringTable
回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。
+所以JDK7之后将字符串常量池放到堆里,能及时回收内存,避免出现错误。
有些人认为JVM的方法区(如 Hotspot 虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。 +《Java虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。 +事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如JDK11时期的ZGC收集器就不支持类卸载)。
+一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。 +以前sun公司的Bug列表中,曾出现过的若干个严重的Bug就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄漏。
+方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。
+方法区内运行时常量池之中主要存放的两大类常量:字面量和符号引用。
+HotSpot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。
+判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。 +需要同时满足下面三个条件:
+该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。 +加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如osGi、JSP的重加载等,否则通常是很难达成的。
+该类对应的java.lang.C1ass
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
加载该类的类加载器已经被回收,这个条件除非是经过精心设计可替换类加载器的场景,如JSP、OSGi,否则通常很难达成。
+Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会回收。
+关于是否要对类型进行回收,HotSpot虚拟机提供了-Xnoclassgc
参数进行控制,还可以使用-verbose:class
以及 -XX:+TraceClass-Loading、-XX:+TraceClassUnLoading
查看类加载和卸载信息。
在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及OSGi这类频繁自定义类加载器的场景中, +通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。
+符号引用是编译原理中的概念,是相对于直接引用来说的。主要包括了以下三类常量:
+符号引用 :符号引用以一组符号来描述所引用的目标。 +符号引用可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,符号引用和虚拟机的布局无关。
+在编译的时候每个java类都会被编译成一个class文件,但在编译的时候虚拟机并不知道所引用类的地址,所以就用符号引用来代替, +而在这个解析阶段就是为了把这个符号引用转化成为真正的地址的阶段。
+在计算机科学中,字面量(literal)是用于表达源代码中一个固定值的表示法(notation)。 +几乎所有计算机编程语言都具有对基本值的字面量表示,诸如:整数、浮点数以及字符串; +而有很多也对布尔类型和字符类型的值也支持字面量表示; +还有一些甚至对枚举类型的元素以及像数组、记录和对象等复合类型的值也支持字面量表示法。
++ +
+ + + + + +简单地讲,一个Native Methodt是一个Java调用非Java代码的接囗。 +一个Native Method是这样一个Java方法:该方法的实现由非Java语言实现,比如C。 +这个特征并非Java所特有,很多其它的编程语言都有这一机制,比如在C++中,你可以用extern “c” 告知c++编译器去调用一个c的函数。
+在定义一个native method时,并不提供实现体(有些像定义一个Java interface),因为其实现体是由非java语言在外面实现的。
+本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序。 +
+需要注意的是:标识符native可以与其它java标识符连用,但是abstract除外。
+public class IhaveNatives {
+ public native void Native1(int x);
+ native static public long Native2();
+ native synchronized private float Native3(Object o);
+ native void Natives(int[] ary) throws Exception;
+}
+
Java使用起来非常方便,然而有些层次的任务用Java实现起来不容易,或者我们对程序的效率很在意时,问题就来了。
+有时Java应用需要与Java外面的环境交互,这是本地方法存在的主要原因。你可以想想Java需要与一些底层系统,如操作系统或某些硬件交换信息时的情况。 +本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解Java应用之外的繁琐的细节。
+JVM支持着Java语言本身和运行时库,它是Java程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成。 +然而不管怎样,它毕竟不是一个完整的系统,它经常依赖于一底层系统的支持。这些底层系统常常是强大的操作系统。 +通过使用本地方法,我们得以用Java实现了jre的与底层系统的交互,甚至JVM的一些部分就是用c写的。 +还有,如果我们要使用一些Java语言本身没有提供封装的操作系统的特性时,我们也需要使用本地方法。
+Sun的解释器是用C实现的,这使得它能像一些普通的C一样与外部交互。jre大部分是用Java实现的,它也通过一些本地方法与外界交互。 +例如:类java.lang.Thread的setPriority()方法是用Java实现的,但是它实现调用的是该类里的本地方法setPriorityo()。 +这个本地方法是用C实现的,并被植入JVM内部,在Windows 95的平台上,这个本地方法最终将调用Win32 setPriority()ApI。 +这是一个本地方法的具体实现由JVM直接提供,更多的情况是本地方法由外部的动态链接库提供,然后被JVM调用
++ +
+ + + + + +Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 +另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。
+ +运行时数据区域包括
+其中:方法区、堆为线程共享;程序计数寄存器、虚拟机栈、本地方法栈 为线程私有。
+Java虚拟机栈于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。
+本地方法栈,也是线程私有的。
+允许被实现成固定或者是可动态扩展的内存大小。(在内存溢出方面是相同的)
+如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个stackOverflowError
异常。
+如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虚拟机将会抛出一个OutOfMemoryError
异常。
+本地方法是使用C语言实现的。
它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库。 +
+当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。
+本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。
++ +
+ + + + + +Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 +另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。
+ +运行时数据区域包括
+其中:方法区、堆为线程共享;程序计数寄存器、虚拟机栈、本地方法栈 为线程私有。
+JVM中的程序计数寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。 +CPU只有把数据装载到寄存器才能够运行。 +这里,并非是广义上所指的物理寄存器,或许将其翻译为PC计数器(或指令计数器)会更加贴切(也称为程序钩子),并且也不容易引起一些不必要的误会。 +JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。
+特点
+outOfMemoryError
情况的区域。程序计数器中既不存在GC又不存在OOM,所以不存在垃圾回收问题。
+作用
+由于Java的多线程是通过线程轮流切换完成的,一个线程没有执行完时就需要一个东西记录它执行到哪了,下次抢占到了CPU资源时再从这开始, +这个东西就是程序计数器,正是因为这样,所以它也是“线程私有”的内存。
+代码演示
+public class MainTest {
+ public static void main(String[] args) {
+ int i = 10;
+ int j = 20;
+ int k = i + j;
+
+ String str = "abc";
+ System.out.println(str);
+ System.out.println(k);
+ }
+}
+
通过javap -verbose MainTest.class
命令反编译.class
文件,得到如下
// ...
+ public static void main(java.lang.String[]);
+ descriptor: ([Ljava/lang/String;)V
+ flags: ACC_PUBLIC, ACC_STATIC
+ Code:
+ stack=2, locals=5, args_size=1
+ 0: bipush 10
+ 2: istore_1
+ 3: bipush 20
+ 5: istore_2
+ 6: iload_1
+ 7: iload_2
+ 8: iadd
+ 9: istore_3
+ 10: ldc #2 // String abc
+ 12: astore 4
+ 14: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
+ 17: aload 4
+ 19: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
+ 22: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
+ 25: iload_3
+ 26: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
+ 29: return
+// ...
+
通过PC寄存器,我们就可以知道当前程序执行到哪一步了。 +
+使用PC寄存器存储字节码指令地址有什么用呢?
+因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。 +JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
+PC寄存器为什么被设定为私有的?
+我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复, +如何保证分毫无差呢?为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器, +这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。
+由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。
+这样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响。
++ +
+ + + + + +Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 +另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。
+ +运行时数据区域包括
+其中:方法区、堆为线程共享;程序计数寄存器、虚拟机栈、本地方法栈 为线程私有。
+Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈, +其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用。
+生命周期和线程一致,也就是线程结束了,该虚拟机栈也销毁了
+主管Java程序的运行,它保存方法的局部变量(8中基本数据类型及对象的引用地址)、部分结果,并参与方法的调用和返回。
+栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。JVM直接对Java栈的操作只有两个
+对于栈来说不存在垃圾回收问题(栈存在溢出的情况:OOM异常)
+++PS 栈与堆 +1.首先栈是运行时的单位,而堆是存储的单位 +2.栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放,放哪里
+
与程序计数器一样,Java的虚拟机栈也是线程私有的,虚拟机栈描述的是Java的方法执行的内存模型, +方法每个执行在同时的创建都会一个栈桢用于存储局部变量表,操作数栈,动态链接,方法出口等信息。
+Java 虚拟机规范允许Java栈的大小是动态的或者是固定不变的。
+如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。
+如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError
异常。
public class MainTest {
+ private static int count = 1;
+ public static void main(String[] args) {
+ System.out.println(count++);
+ main(args);
+ }
+}
+
抛出异常栈内存不足
+// ...
+9377***
+Exception in thread "main" java.lang.StackOverflowError
+// ...
+
在使用递归的情况下,如果线程请求的栈的深度超过虚拟机所允许栈的深度就会抛出StackOverflowError
;
+但是大部分虚拟机栈的深度都可以动态扩展,HotSpot中使用 Xss 可以设置栈的深度,如果扩展时无法请求到足够的内存就会抛出OutOfMemoryError
。
可以设置栈的内存大小,使用参数 -Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。
+-Xss256m
+-Xss256k
+
每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在。 +在这个线程上正在执行的每个方法都各自对应一个栈颜(Stack Frame)。 +栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
+JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”或“后进先出”原则。
+在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的, +这个栈帧被称为当前栈帧(Current Frame),与当前栈帧相对应的方法就是当前方法(Current Method),定义这个方法的类就是当前类(Current Class)。
+执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
+如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。 +
+代码演示
+public class MainTest {
+
+ public static void main(String[] args) {
+ method01();
+ }
+
+ private static int method01() {
+ System.out.println("方法1的开始");
+ int i = method02();
+ System.out.println("方法1的结束");
+ return i;
+ }
+
+ private static int method02() {
+ System.out.println("方法2的开始");
+ int i = method03();;
+ System.out.println("方法2的结束");
+ return i;
+ }
+ private static int method03() {
+ System.out.println("方法3的开始");
+ int i = 30;
+ System.out.println("方法3的结束");
+ return i;
+ }
+
+}
+
不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。
+如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
+Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。
+每个栈帧中存储着:
+每个线程下的栈都是私有的,因此每个线程都有自己各自的栈,并且每个栈里面都有很多栈帧,栈帧的大小主要由 局部变量表 和 操作数栈 决定的。
+局部变量表:Local Variables,被称之为局部变量数组或本地变量表。
+定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型。
+由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题。
+局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables
数据项中。
+在方法运行期间是不会改变局部变量表的大小的。
+
+方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。 +对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。 +进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。
+局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。 +当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
+局部变量表,最基本的存储单元是Slot(变量槽)局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量。
+参数值的存放总是在局部变量数组的 index0 开始,到数组长度-1的索引结束。
+在局部变量表里,32位以内的类型只占用一个slot,64位的类型(1ong和double)占用两个slot。
+byte、short、char 在存储前被转换为int,boolean也被转换为int,0表示false,非0表示true。 1ong和double则占据两个slot。 +JVM会为局部变量表中的每一个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值
+当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个slot上
+如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。(比如:访问1ong或doub1e类型变量)
+如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的s1ot处,其余的参数按照参数表顺序继续排列。
+ +栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变就很有可能会复用过期局部变量的槽位, +从而达到节省资源的目的。
+代码演示
+ public void test() {
+ int a = 0;
+ {
+ int b = 0;
+ b = a + 1;
+ }
+ int c = a + 1;
+ }
+
变量的分类:
+参数表分配完毕之后,再根据方法体内定义的变量的顺序和作用域分配。
+我们知道类变量表有两次初始化的机会,第一次是在“准备阶段”,执行系统初始化,对类变量设置零值,另一次则是在“初始化”阶段,赋予程序员在代码中定义的初始值。 +和类变量初始化不同的是,局部变量表不存在系统初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用。
+在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。 +局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
+每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出的 操作数栈,也可以称之为 表达式栈。
+++PS: 栈为抽象数据结构,不是真实存在的。一般可以用数组或者链表来实现。这里是假定是用数组实现栈结构。
+
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和 出栈(pop)。
+举例
+ +操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
+++PS:这个时候操作数栈是有长度的,数组一旦创建,那么就是不可变的
+
操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。
+每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为maxstack
的值。
栈中的任何一个元素都可以是任意的Java数据类型
+操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问; +如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
+操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。 +另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
+每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。 +比如:invokedynamic指令。
++
+在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(symbolic Reference)保存在class文件的常量池里。 +比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的, +那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
+++为什么需要运行时常量池?
+因为在不同的方法,都可能调用常量或者方法,所以只需要存储一份即可,节省了空间。常量池的作用:就是为了提供一些符号和常量,便于指令的识别
+
存放调用该方法的pc寄存器的值。 当一个方法开始执行后,只有两种方式可以退出:
+无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时, +调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。 +而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
+执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口;
+在方法执行过程中遇到异常(Exception),并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,简称异常完成出口。
+一个方法在正常调用完成之后,究竟需要使用哪一个返回指令,还需要根据方法返回值的实际数据类型而定。
+++在字节码指令中,返回指令包含ireturn(当返回值是boolean,byte,char,short和int类型时使用),lreturn(Long类型),freturn(Float类型),dreturn(Double类型),areturn。 +另外还有一个return指令声明为void的方法,实例初始化方法,类和接口的初始化方法使用。
+
方法执行过程中,抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码 + +本质上,方法的退出就是当前栈帧出栈的过程。 +此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。
+正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。
+栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如:对程序调试提供支持的信息。
+基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令, +这同时也就意味着将需要更多的指令分派(instruction dispatch)次数和内存读/写次数。
+由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。 +为了解决这个问题,HotSpot JVM的设计者们提出了栈顶缓存(Tos,Top-of-Stack Cashing)技术, +将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。
+++寄存器:指令更少,执行速度快
+
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关。
+静态链接
+当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期克制,且运行期保持不变时,这种情况下降调用方法的符号引用转换为直接引用的过程称之为静态链接
+动态链接
+如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用的方法的符号转换为直接引用,由于这种引用转换过程具备动态性,因此也被称之为动态链接。
+对应的方法的绑定机制为:早期绑定(Early Binding)和晚期绑定(Late Binding)。 +绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。
+早期绑定
+早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。
+晚期绑定
+如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。
+Java中任何一个普通的方法其实都具备虚函数的特征,它们相当于C++语言中的虚函数(C++中则需要使用关键字virtual来显式定义)。 +如果在Java程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字final来标记这个方法。
+虚拟机中提供了以下几条方法调用指令
+普通调用指令
+invokestatic
:调用静态方法,解析阶段确定唯一方法版本invokespecial
:调用方法、私有及父类方法,解析阶段确定唯一方法版本invokevirtual
:调用所有虚方法invokeinterface
:调用接口方法动态调用指令
+invokedynamic
:动态解析出需要调用的方法,然后执行JVM字节码指令集一直比较稳定,一直到Java7中才增加了一个invokedynamic指令,这是Java为了实现动态类型语言】支持而做的一种改进。
+但是在Java7中并没有提供直接生成invokedynamic指令的方法,需要借助ASM这种底层字节码工具来产生invokedynamic指令。 +直到Java8的Lambda表达式的出现,invokedynamic指令的生成,在Java中才有了直接的生成方式。
+Java7中增加的动态语言类型支持的本质是对Java虚拟机规范的修改,而不是对Java语言规则的修改,这一块相对来讲比较复杂, +增加了虚拟机中的方法调用,最直接的受益者就是运行在Java平台的动态语言的编译器。 +
+前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic
指令则支持由用户确定方法版本。
+其中invokestatic
指令和invokespecial
指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法。
Java 语言中方法重写的本质:
+java.1ang.I1legalAccessError
异常。
+否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。java.1ang.AbstractMethodError
异常。++PS: IllegalAccessError 程序试图访问或修改一个属性或调用一个方法,这个属性或方法,你没有权限访问。一般的,这个会引起编译器异常。这个错误如果发生在运行时,就说明一个类发生了不兼容的改变。
+
在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就可能影响到执行效率。因此,为了提高性能,JVM采用在类的方法区建立一个虚方法表 (virtual method table)(非虚方法不会出现在表中)来实现。使用索引表来代替查找。
+每个类中都有一个虚方法表,表中存放着各个方法的实际入口。
+虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕。
+ +如果类中重写了方法,那么调用的时候,就会直接在虚方法表中查找,否则将会直接连接到Object的方法中。
+通过 -Xss设置栈的大小;使用递归调用同一个方法;
+不能保证。一定时间内降低了OOM概率,但是不能避免OOM;
+不是,因为整个空间是有限的,会挤占其它的线程空间。
+不会;因为栈结构,出栈就相当于垃圾回收了。
+运行时数据区,是否存在Error和GC
+运行时数据区 | +是否存在Error | +是否存在GC | +
---|---|---|
程序计数器 | +否 | +否 | +
虚拟机栈 | +是 | +否 | +
本地方法栈 | +是 | +否 | +
方法区 | +是(OOM) | +是 | +
堆 | +是 | +是 | +
++PS 何为线程安全? +如果只有一个线程才可以操作此数据,则必是线程安全的 +如果有多个线程操作,则此数据是共享数据,如果不考虑共享机制,则为线程不安全
+
如果对象是在内部产生,并在内部消亡,没有返回到外部,那么它就是线程安全的,反之则是线程不安全的。
+public class StringBuilderTest {
+
+ // s1的声明方式是线程安全的
+ public static void method01() {
+ // 线程内部创建的,属于局部变量
+ StringBuilder s1 = new StringBuilder();
+ s1.append("a");
+ s1.append("b");
+ }
+
+ // 这个也是线程不安全的,因为有返回值,有可能被其它的程序所调用
+ public static StringBuilder method04() {
+ StringBuilder stringBuilder = new StringBuilder();
+ stringBuilder.append("a");
+ stringBuilder.append("b");
+ return stringBuilder;
+ }
+
+ // stringBuilder 是线程不安全的,操作的是共享数据
+ public static void method02(StringBuilder stringBuilder) {
+ stringBuilder.append("a");
+ stringBuilder.append("b");
+ }
+
+
+ /**
+ * 同时并发的执行,会出现线程不安全的问题
+ */
+ public static void method03() {
+ StringBuilder stringBuilder = new StringBuilder();
+ new Thread(() -> {
+ stringBuilder.append("a");
+ stringBuilder.append("b");
+ }, "t1").start();
+
+ method02(stringBuilder);
+ }
+
+ // StringBuilder是线程安全的,但是String也可能线程不安全的
+ public static String method05() {
+ StringBuilder stringBuilder = new StringBuilder();
+ stringBuilder.append("a");
+ stringBuilder.append("b");
+ return stringBuilder.toString();
+ }
+}
+
+ +
+ + + + + +大部分Java开发人员,除了会在项目中使用到与Java平台相关的各种高精尖技术,对于Java技术的核心Java虚拟机了解甚少。 +一些有一定工作经验的开发人员,打心眼儿里觉得SSM、微服务等上层技术才是重点,基础技术并不重要, +这其实是一种本末倒置的“病态”。 +如果我们把核心类库的API比做数学公式的话,那么Java虚拟机的知识就好比公式的推导过程。
+++不要以钱为你的最终目标,当你把技术做到位的时候,你会发现你的薪资待遇、社会地位,都会自然而然的提升上来。不要本末倒置、急功近利。
+
垃圾收集机制为我们打理了很多繁琐的工作,大大提高了开发的效率, +但是,垃圾收集也不是万能的,懂得JVM内部的内存结构、工作机制,是设计高扩展性应用和诊断运行时问题的基础,也是Java工程师进阶的必备能力。
+所谓虚拟机(Virtual Machine),就是一台虚拟的计算机。它是一款软件,用来执行一系列虚拟计算机指令。 +大体上,虚拟机可以分为系统虚拟机和程序虚拟机。
+HotSpot VM是目前市面上高性能虚拟机的代表作之一。下图就是 HotSport 虚拟机结构图 +
+Java代码通过编译器生成字节码文件,字节码通过Java虚拟机跟操作系统交互。 +
+Java编译器输入的指令流基本上是一种 基于栈的指令集架构 ,另外一种指令集架构则是 基于寄存器的指令集架构 。 +Hotsport 是基于栈的指令集架构。
+基于栈式架构的特点
+基于寄存器架构的特点
+启动 –> 执行 –> 退出
+虚拟机的启动
+虚拟机的执行
+虚拟机的退出
+Hotspot VM、JRockit、J9 是目前主要流行的Java虚拟机。所有虚拟机的原则:一次编译,到处运行。
+具体JVM的内存结构,其实取决于其实现,不同厂商的JVM,或者同一厂商发布的不同版本,都有可能存在一定差异。 +主要以oracle HotSpot VM为默认虚拟机。
+早在1996 Java1.0 的时候,Sun 公司发布了了第一款名为 Sun Classic VM 的Java虚拟机,他同时也是世界上第一款商用的Java虚拟机, +JDK1.4 的时候完全被淘汰。这款虚拟机只提供解释器。
+++Java虚拟机分为两类执行引擎
++
+- 解释型:一行一行执行代码,执行效率慢,类似于javascript、python这类解释型的编程语言
+- 及时编译型:将字节码中的热点代码编译成机器码,并且将机器码缓存到方法区的代码缓存区。
+
这款虚拟机只能使用纯解释器方式来执行Java代码,如果要使用即时编译器那 就必须进行外挂, +但是假如外挂了即时编译器的话,即时编译器就会完全接管虚拟机的执行系统,解释器便不能再工作了。 +因此这个阶段的虚拟机 虽然用了即时编译器输出本地代码, +其执行效率也和传统的 C/C++ 程序有很大差距,“Java语言很慢”的 印象就是在这阶段开始在用户心中树立起来的。
+Hotspot 虚拟机内置了 Sun Classic 虚拟机。
+Sun的虚拟机团队努力去解决Classic虚拟机所面临的各种问题,提升运行效率, +在JDK 1.2时,曾在 Solaris 平台上发布过一款名为 Exact VM 的虚拟机, +它的编译执行系统已经具备现代高性能虚拟机雏形,如热点探测、两级即时编译器、编译器与解释器混合工作模式等。
+Hotspot VM 是目前使用范围最广的Java虚拟机。Hotspot 并非由Sun公司开发,而是由一家名为“Longview Technologies”的小公司设计的;
+HotSpot VM既继承了Sun之前两款商用虚拟机的优点(如前面提到的准确式内存管理),也有许多自己新的技术优势, +如它名称中的HotSpot指的就是它的热点代码探测技术,HotSpot VM的热点代码探测能力可以通过执行计数器找出最具有编译价值的代码,然后通知JIT编译器以方法为单位进行编译。 +如果一个方法被频繁调用,或方法中有效循环次数很多,将会分别触发标准编译和OSR(栈上替换)编译动作。 +通过编译器与解释器恰当地协同工作,可以在最优化的程序响应时间与最佳执行性能中取得平衡,而且无须等待本地代码输出才能执行程序, 即时编译的时间压力也相对减小, +这样有助于引入更多的代码优化技术,输出质量更高的本地代码。
+JRockit 专注服务端的应用,内部不包含解析器的实现,全部代码都靠即时编译器编译后执行。
+++大量的行业基准测试显示,JRockit JVM是世界上最快的JVM。 JRockit面向延迟敏感型应用的解决方案JRockit Real Time提供以毫秒或微秒计的JVM响应时间, +适合财务前端办公、军事指挥与控制和电信网络的需要。使用JRockit产品,客户已经体验到了显著的性能提高(一些超过了70% )和硬件成本的减少(达50%)。
+
全面的Java运行时解决方案
+IBM的J9全称:IBM Technology for Java Virtual Machine,简称IT4J,内部代号J9。
+J9的市场定位与HotSpot接近,服务器端、桌面应用、嵌入式等多用途VM。
+J9是目前由影响力的三大商业虚拟机之一,2017年IBM发布了开源J9 VM,命名为OpenJ9,交给Eclipse基金会管理,也称Eclipse OpenJ9。
+++IBM J9直至今天仍旧非常活跃,IBM J9虚拟机的职责分离与模块化做得比HotSpot更优秀, +由J9 虚拟机中抽象封装出来的核心组件库(包括垃圾收集器、即时编译器、诊断监控子系统等)就单独构 成了IBM OMR项目, +可以在其他语言平台如Ruby、Python中快速组装成相应的功能。从2016年起, IBM逐步将OMR项目和J9虚拟机进行开源, +完全开源后便将它们捐献给了Eclipse基金会管理,并重新 命名为Eclipse OMR和OpenJ9。 +如果为了学习虚拟机技术而去阅读源码,更加模块化的OpenJ9代码 其实是比HotSpot更好的选择。 +如果为了使用Java虚拟机时多一种选择,那可以通过AdoptOpenJDK来 获得采用OpenJ9搭配上OpenJDK其他类库组成的完整JDK。
+
Oracle 在Java Me 产品线上的两款虚拟机: CDC/CLDC Hotspot VM。目前移动领域地位的尴尬,智能手机被IOS和android二分天下。
+KVM 简单、轻量,高度可移植性,在面向更低端的设备上还维持着自己的一片市场。
+前面三大“高性能Java虚拟机”使用在通用硬件平台上这里Azu1VW和BEALiquid VM是与特定硬件平台绑定、软硬件配合的专有虚拟机I
+高性能Java虚拟机中的战斗机。 +Azul VM是Azu1Systems公司在HotSpot基础上进行大量改进,运行于Azul Systems公司的专有硬件Vega系统上的ava虚拟机。
+每个Azu1VM实例都可以管理至少数十个CPU和数百GB内存的硬件资源,并提供在巨大内存范围内实现可控的GC时间的垃圾收集器、专有硬件优化的线程调度等优秀特性。
+2010年,AzulSystems公司开始从硬件转向软件,发布了自己的zing JVM,可以在通用x86平台上提供接近于Vega系统的特性。
+Apache也曾经推出过与JDK1.5和JDK1.6兼容的Java运行平台Apache Harmony。
+它是IElf和Inte1联合开发的开源JVM,受到同样开源的openJDK的压制,Sun坚决不让Harmony获得JCP认证,最终于2011年退役,IBM转而参与OpenJDK
+虽然目前并没有Apache Harmony被大规模商用的案例,但是它的Java类库代码吸纳进了Android SDK。
+微软为了在IE3浏览器中支持Java Applets,开发了Microsoft JVM。
+只能在window平台下运行。但确是当时Windows下性能最好的Java VM。
+1997年,sun以侵犯商标、不正当竞争罪名指控微软成功,赔了sun很多钱。微软windowsXPSP3中抹掉了其VM。 +现在windows上安装的jdk都是HotSpot.
+由AliJVM团队发布。阿里,国内使用Java最强大的公司,覆盖云计算、金融、物流、电商等众多领域,需要解决高并发、高可用、分布式的复合问题。有大量的开源产品。
+基于openJDK开发了自己的定制版本AlibabaJDK,简称AJDK。是整个阿里Java体系的基石。
+基于openJDK Hotspot VM发布的国内第一个优化、深度定制且开源的高性能服务器版Java虚拟机。
+创新的GCIH(GCinvisible heap)技术实现了off-heap,即将生命周期较长的Java对象从heap中移到heap之外,并且Gc不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升Gc的回收效率的目的。 +GCIH中的对象还能够在多个Java虚拟机进程中实现共享 +使用crc32指令实现JvM intrinsic降低JNI的调用开销 +PMU hardware的Java profiling tool和诊断协助功能 +针对大数据场景的ZenGc +taobao vm应用在阿里产品上性能高,硬件严重依赖inte1的cpu,损失了兼容性,但提高了性能
+目前已经在淘宝、天猫上线,把oracle官方JvM版本全部替换了。
+谷歌开发的,应用于Android系统,并在Android2.2中提供了JIT,发展迅猛。
+Dalvik y只能称作虚拟机,而不能称作“Java虚拟机”,它没有遵循 Java虚拟机规范
+不能直接执行Java的Class文件
+基于寄存器架构,不是jvm的栈架构。
+执行的是编译以后的dex(Dalvik Executable)文件。执行效率比较高。
+它执行的dex(Dalvik Executable)文件可以通过class文件转化而来,使用Java语法编写应用程序,可以直接使用大部分的Java API等。 +Android 5.0使用支持提前编译(Ahead of Time Compilation,AoT)的ART VM替换Dalvik VM。
+2018年4月,oracle Labs公开了GraalvM,号称 “Run Programs Faster Anywhere”,勃勃野心。 +与1995年Java的”write once,run anywhere"遥相呼应。
+GraalVM在HotSpot VM基础上增强而成的跨语言全栈虚拟机,可以作为“任何语言” 的运行平台使用。语言包括:Java、Scala、Groovy、Kotlin;C、C++、Javascript、Ruby、Python、R等
+支持不同语言中混用对方的接口和对象,支持这些语言使用已经编写好的本地库文件
+工作原理是将这些语言的源代码或源代码编译后的中间格式,通过解释器转换为能被Graal VM接受的中间表示。Graal VM提供Truffle工具集快速构建面向一种新语言的解释器。在运行时还能进行即时编译优化,获得比原生编译器更优秀的执行效率。
+如果说HotSpot有一天真的被取代,Graalvm希望最大。 但是Java的软件生态没有丝毫变化。
++ +
+ + + + + ++ +
+ + + + + +面试官您好,我叫XXX,来自XXX,毕业于XXXX,在大学期间参与老师组织的"XXX"项目建设,获得一些Java框架的使用经验。有良好的编程习惯和扎实的Java基础,可以从容应对用Java框架编写分布式项目、使用Vue编写页面的场景。
+
+毕业后,就职于北京某金融领域公司,参与XXXX从零到一的搭建,通过一段时间,自学了Vue框架、Java设计模式。
+在和同事共同开发的过程中,得到了领导的赏识,将部分模块完全交付给我。后端主要通过Springboot、SSM 框架构建、前端通过Vue构建;
+后来因为项目的需求又通过一段时间学习了用`gitlab-runner`流水线发版的技能和一些新的技术;
+在项目中的这一年的时间,每天996的工作,尤其是在项目上线前一个月左右,每天顶着巨大的压力工作到半夜,这对于刚步入职场的新人来说,无疑是一种挑战,也使我明白了这个行业的艰辛.
+
+因此,我在使用Vue、Java搭建项目上有相关的经验;在工作方面有不错的抗压能力和学习能力;这个经验和工作能力应该能为公司的日后持续发展,做出贡献。
+
+项目上线后又自学了JVM等相关知识,通过不断对Java的学习,也使我对Java这门语言有了新的认识;
+到今年,我参与了XXXX的建设,通过引入Java设计模式对代码设计进行优化,使其更符合面向对象编程;通过用netty框架替代了直接使用socket,提高了系统的并发和响应速度,使系统更加健壮;
+
+相信通过我的工作能力,能帮助公司项目中的程序更上一层楼。
+
在说离职的原因的时候不要过多的表露自己的负面情绪,即使你有理由也不要把大把的时间花到这个上面,因为面试的时间是非常宝贵的,你不要让面试官抓到它不找你的理由,而是要把握机会,展示自己的闪光点。
+在说离职原因时要注意:
+离职是分两种情况,一种是主动离职,另一种是被辞退,说的时候不要撒谎,万一公司有被调的,入职会受影响。下面按照这两种情况分别来说明:
+在过往的两三年当中,我在这个职位上成长非常多,也非常感激这个机遇,我学到了XXX方向的知识和XXX方向的技能,但是在晋升空间上确实有一定的瓶颈。
+放眼望去,我的前辈们,他们在自己的职位上呆了有三四年的时间了,在这段时间里都没有动过,我理解大部分的职位都是有很多重复性的,但是对于我这个职位来说确实学习空间和成长空间太小了。
+所以我希望能看看外面的机会,能够给自己带来更多的成长空间。
+
公司的整个利润有所下滑,组织架构有改变,不是我一个人被辞退,是我和其他员工都被辞退,有大规模的裁员。
+但是我也是很感激这两年的学习机遇的,我在XXX方面和XXX方面都有很多成长,那么我也相信我的一些成长能够给公司带来很多价值。
+
大致思路:自己工作能力+事例+对日后的工作有什么好处。 +结合具体的事件来说,显得优点更加真实、更加容易让被人相信你,当然前提是你可以适当包装,但是不能撒谎。
+我有很强的自驱力和自学能力,在之前的工作中自学了Vue、设计模式、JVM等Java相关技术;
+如果在未来工作中遇到不会的,也会去努力的去学习;包括向前辈学习、书本学习、自己去搜集资料,而不断的去提升自己;我的这个能力相信会对公司未来发展做出贡献。
+
缺点不能说的太真诚、直白;例如:我的缺点是我很懒,不细心、爱嫉妒别人、不爱团队合作 … +缺点也不能说的太过委婉,不能让面试官觉得你在自作聪明、不诚实;例如:我最大的缺点就是过于追求完美;不要做出非常明显的将自己的缺点转化为优点的答案。
+要避免雷区蹦迪:如果你面试的工作非常需要这个能力,而你却说我的这个能力不行;如果这样说,这是对你应聘这个岗位是非常不利的。 +所以在说的时候要避免说这样的缺点,可以谈一些这个岗位不需要的能力,或者说比较不在意的。 +在说缺点的时候可以穿插着优点,先说优点再说缺点。
+1.我是一个很理性的人,在混乱的时候,我能迅速的理清头绪,梳理出多种方案思路;然后再去探讨问题;
+但同时因为这一点,如果没有任何方案的情况下,共同头脑风暴,在这个时候我很难去提出自己的一些观点和看法。
+我也在慢慢的努力去改进,因为有时候只有打破这个规则,才能有一些创新性的想法,同时我相信我这个分析能力也能够帮助团队,去更好的达成目标。
+
+2. 我的工作经验相较于其他前辈比较少,我会在工作中多多学习来弥补我的经验不足。
+
应该反问一句你问的是男对象还是女对象?(/doge)
+hr问你问题的目的无非就三个:
+其实有对象也不一定是坏事,没有对象也不一定是好事;如果你面试的是需要沟通能力比较强的岗位那么你有对象可能就是优势,如果没有对象可能给对方留下你这个人沟通能力不行的印象。
+如果你回答有对象,可能hr会顺着感情稳不稳定,什么时候结婚,什么时候生孩子的思路问下去,毕竟这些问题和企业付出的成本有很大的关系。
+如果你的感情不稳定,比如和对象分居,吵架的时候总想去对方的城市去哄她(他),这就肯定会影响到工作。一般女生被问道这个问题的可能行会比较大,毕竟女生可以生育,生育需要放产假,需要企业付出比较大的成本。可以这么回答
+我和对象的感情比较稳定,我们两个达成了共识,就是先打拼好事业,有好的物质基础在考虑.
+
如果你回答没有对象,hr可能会顺着什么时候准备找对象,结婚的思路问下去。可以这么回答
+最近一些时间没有找对象的心思,我会先全身心投入工作,提升自己的专业水平,争取在2-3年间更上一层楼
+
HR其实并不关心你的职业规划,甚至她自己的职业规划都不清楚,那么为什么要问这个问题?
+因为HR关心你的其他几件事:
+所谓的稳定性,是指你在没有想清楚或者没有详细了解一个岗位的综合情况下,就来应聘这个岗位,这就意味这你的稳定性是不佳的; 你可能会干3个月、5个月,感觉跟你想象中的不太一样你就会离职。 所以你在选择一份工作之前你要详细了解它的发展前景,尽量将我想公司长期发展的观点表达出来。
+岗位发展前景和职业规划的匹配度,比如:我希望两年就当上经理,但是公司的晋升比较缓慢,可能公司3年一次晋升或者我们现在的经理5年10年才熬到这个位置, 那你想两年就晋升,可能想是不大的。
+如果HR了解到你的职业规划,那意味着你可能未来的2、3年都会在这条路上沉淀一直下去,这种沉淀一方面代表了稳定性,另一方面也代表了在工作的这个方向有足够的能力。
+在说职业规划的时候有三个注意的点:
+我去年才毕业,说实话太长远的职业规划还没有去做,但是短期三年之内,我希望能在Java开发这个岗位深耕。
+为什么呢?
+1. 我很喜欢Java开发这个行业,用写的程序可以应用到实际生活中去,可以帮助到人们;
+2. 我在平时的时候也会看一些开源项目的源代码,并且在Github有自己的小项目;
+3. 我之前工作做的还是比较不错的,也间接证明了我比较适合这个岗位;
+
+因此,我还是希望未来在这个方向继续发展下去。如果公司认可我的能力的话,比如在工作一两年后,如果公司确实需要我会其他语言的开发或者需要我学习其他新的技术,我也是很乐意的。
+
如果前面面试谈的不错的话,面试是有戏的,这个时候就可以谈薪资了。
+一般来说HR问你的期望薪资会根据你上一家公司的薪资来进行判断,也就是判断你的能力价值;这个时候不要最好说谎,因为一般的公司都会去做背景调查,被发现说谎的话,就不用我说了。
+虽然我们都说钱很重要,但经常又在谈薪的时候,因为不知道怎么开口就麻痹地说,钱也不是唯一的,然后就放弃了好好谈薪。
+把谈谈薪的人分为两类人,一类是谈薪资过于激进派,一类是怂怂派,即不知道怎么合理提出薪资诉求。
+激进派存在的问题:
+怂怂派存在的问题:
+自己是很认可公司的,同时也很开心能有机会加入,同时薪酬也是一个重要的考虑维度,自己在这方面也有一些诉求
+
我的预期是相较于现有的工资水平,有一定合理比例的上浮,也想了解一下公司对我的想法。
+
+能否先了解一下工资的薪酬结构,比如基础薪资和绩效奖金的构成、绩效奖金的发放标准、公司有没有年终奖、项目奖金,公司有没有期权、五险一金缴纳方式具体是怎么样的呢、公司有没有大小周,周末上班的计薪方式是什么样的。
+
当对方非要问工资预期的时候,先给到方向性的回复,而不是具体的数字,争取让对方先报数,比如hr在介绍完薪资结构之后会再次问我们,了解了公司的薪酬结构你现在是什么想法呢?这个是否可以回复说
+我希望总体上相较于上一份工作,有一定合理比例的涨幅,也想听听公司和我目前聊下来对我的提议
+
此时会出现两种情况,第一种直接反馈一个数字,第二种对方仍然不给你数字,并追问你你希望的涨幅是多少呢,你预计的薪资数字是多少呢
+这个距离我的预期还是有一定的差距,看能否给到xxx呢
+
如果对方能够给到那就愉快的接受,如果不能给到,那个可以基于这个底线,以及offer的其他情况,做一个判断,然后和对方有进一步的沟通。在谈的过程中找好参照物,帮助在谈薪资的过程中找到更有力的条款,我们需要论据来说明这个论据是合理的。
+论据可以从这几个方面来找:
+在整个沟通过程中,如果期望薪资,对方的条件没有达到我们预期的时候,我们都要记得礼貌而坚定的表达自己的想法。并且有理有具的给予说明。面试是一个双向选择的过程,不要过于卑微亦不要过于高傲自大。正确的认清自己的价值,合理的谈论薪酬。
+我能够接受加班,但是不赞成加班。
+有两种情况加班,我个人认为是没有问题的:
+1. 我个人能力不够,不能够按时完成上级交给我的任务,需要通过加班来完成;
+2. 公司遇到重要且紧急的事情或者突发事件需要马上来处理的;
+
+这两种情况下的加班我认为是必要的,对公司很重要且需要长期加班的项目,我希望公司能够平衡我个人的付出与收入之间的关系;
+对于不是特别重要或不是很紧急的项目加班,我个人不是很赞成。
+
+那我们公司的加班情况,您能介绍一下吗?
+
通过HR的回答,可以了解入职之后的工作强度如何。那些强制加班或强制996的公司你能否接受就看你自己了,如果你有能力有选择,那么你可以避开这些公司。
+这个问题看上去是面试官在问我们选择行业、选择公司、选择岗位,事实上面试官在意的是为什么要选择我们。
+先来弄清这个问题的本质,理解面试官的目的,面试官问你的求职动机,通常是想从三个维度来判断你是否适合这家公司:
+回答思路:
+Java工程师这个岗位吸引我的地方是自己可以去动手设计程序,并且通过自己做产品,来满足用户的需求,影响很多人。而咱公司的产品,我认为它解决了XX问题,创造了价值,这一点是我非常认可的。
+
+我理解作为一个程序员需要有较强的逻辑思维能力、良好的编码能力、团队协作能力以及学习能力,我在之前的岗位上也都具备这些能力,以及有一些项目经验;同时,我逻辑分析能力不错,过往在解决问题时,都知道怎么样用合理的去拆解和定位问题,最终找到机会点。
+
这个问题,一般面试官都会放到最后来问,一般是面试官出于对面试者的尊重,客气一下,又或者真的想给面试者一个了解公司的机会; +无论哪种情况下,这个问题我们都应该让它有价值,不要问了公司情况之后自己也没有面试上,那样的话也没啥意义。 +当然,如果你自己有底线的话,对公司有一定要求,某一个点不能接受,就像我吃菜时不吃辣椒一样,接受不了,这样的话要提早说。
+如果你面试的是中、大公司,你是第一次面试,还会有第二次、第三次面试等,先不要问你关心的问题,因为大概率上问了也没啥用。 +这时候你可以说,我对贵公司有一定的了解,贵公司是从事什么什么行业的,主要是做什么产品、技术的等。 +如果有机会的话,我很期待能加入贵公司;希望您能给机会进行下一次面试,到时候我想再详细的了解贵公司的情况; +说一些,表达我很想去这个公司,我是对公司有一点了解的,我是在这个方面下过功夫,把你的态度表达出来。
+如果你面试的是小公司,这个公司只面试一次,公司决定要你的话,大概率会在这个问题之前给出结果; +如果在这之前面试官没有表达出来这层意思,那么面试也就八成凉凉了,那自然问题问不问也就没啥用了; +如果公司决定要你了,你就要问一些,你自己比较关心的问题。
+1. 咱公司平常的项目中会用到的技术栈?
+2. 公司的项目开发流程?敏捷开发?开会是否频繁?
+3. 公司出差是否频繁、出差地点?
+4. 您对我有什么好的建议吗?
+5. 上班的环境、上班的位置、晋升机制、交通补贴、吃饭补贴、是否提供住宿、员工福利、加班补助 ...
+6. 格局大一些,把面试官问你的问题,反问一遍
+
辞职的时候不要给公司埋雷,当然也不要被公司薅最后一把羊毛;正所谓害人之心不可有,防人之心不可无。
+如果你在一个公司实在是工作不下去了,请不要忍气吞声,也不要不管三七二十一的直接撂挑子走人;前者是对自己的不负责任,后者是对他人的不负责任。 +作为一个"人",我们既要对自己负责,又要对他人负责,当然这需要掌握一些辞职小技巧。下面将从这几个方面展开来说:
+这个问题其实对于不同的人来说,有不同的答案,总的来说就是对公司某些方面不满意,甚至怨恨。
+其实,大部分人辞职的主要原因就是为了涨工资;俗话说:
+月薪100w:公司就是我活着的意义,公司方向就是我信仰的方向。
+月薪80w:公司是我家,老板是爸妈。
+月薪50w:我与公司共存亡,务必对我耍流氓。
+月薪30w:千万不要因为我是娇花而怜惜我。
+月薪10w:修福报算什么,看我给你修一个新世纪福音战士。
+月薪8w:老板你今天想喝苹果汁、葡萄汁、西瓜汁,还是我这个小比灾汁。
+月薪5w:加班那是家常便饭,我直接不需要下班。
+月薪3w:老板说的都是对的,错的是这个世界。
+月薪2w:老板说啥就是啥,只要给钱就是好老板。
+月薪1w:老板时不时脑子有坑,我在背后说他点坏话。
+月薪8k:人在屋檐下,不得不低头,生活就是这么苦涩。
+月薪5k:大家都是碳基生物,老板你凭什么这么跳。
+月薪2k老板脸上那不是嘴,而是括约肌,说话就像放屁一样。
+月薪1k:老板祖坟冒烟,一行白鹭上青天。
+月薪500:老板何不乘风起,扶摇直上九万里。
+工资扣到0:老板我是你爹。吃我大XX,哦不对这样不礼貌对孩子怎么能说脏话呢,应该说老板我是您爹。
+
所以就有些人想辞职,但又不是真的想辞职,只是借机想提高一下自己的工资。奉劝有这些想法的兄弟姐妹们,这么做之前先想一下自己的能力,想一下自己在公司中的地位; +如果老板真的把你开了呢?请三思而后行,最好的涨工资方法就是攒足了劲儿然后跳槽。
+还有一部分人,因为一些事或人就是在公司中呆不下去了,看啥啥都烦,浑身不刺挠,恨不得上午提辞职下午就走。这种情况下建议用一些个人的原因,如:身体不舒服、家里有事,同时把这些事情所需要的时间说的长一点,事情说的严重一些,让老板无法回绝你,让它感觉你是很焦急的样子。例如:
+老板,我家里有点着急的事,需要我立刻就走回去处理,短的来说可能需要一两个月,长的来说可能需要三五个月,半年这样。
+
这种情况下,如果你的业务能力相当出色,老板一般是会拒绝你离职的,会说一些话来挽留你:
+身体不舒服,那休息一段时间怎么样啊?家里有事先去处理吧,等回来的时候在看。
+
相反如果你的业务能力一般或较差,可替代性非常强,老板应该很快就能同意。
+除了前两种比较极端的情况,大部分人都是不着急离职,但是又决定了我要离职,典型的骑驴找马。这种情况下可以用职业发展的这个原因:
+我在公司三年了,我非常喜欢公司的氛围,工作内容跟我自己想的也很一致,但是现在我想对我下一阶段的规划,有一个重新的定位。
+
+现在我是做金融的,那么接下来我可能会更多想尝试互联网这个行业。我知道咱公司现在还没有做这方面的打算,所以经过我慎重的考虑,我今天正式跟您提出离职。
+
+同时我也会交接好下面的工作,您放心,希望您能够理解。
+
上述第三种情况,注意事项:
+其实除了这些离职原因外,还有一些其他的原因,如:工作的地点离我太远了、工作内容不喜欢、对企业氛围不满意。 不建议说这些理由原因是,以上这些理由多多少少都带有一些不满和借口,都离职了,给对方留下个好的印象,没准之后还能碰见,路走宽点。
+应该什么时候辞职,取决于你想什么时候辞职,如果你想要离职需要提前打招呼,针对于转正的员工,公司让你交接工作一个月是合情合法合理的,如果你想一个月之内离职,需要你的态度好点。
+针对于这种情况下,来谈谈辞职时机的重要性:
+这时候辞职已经提好了,可能会有一段时间需要你交接工作,那么这段时间我们要注意几个问题:
+当你成功做完上面几个事情之后,请不要删掉某些同事的微信,甚至跟某些同事直接翻脸,发生不愉快的事情,这样做是非常不好的;这并不是非的让你维护某些人际关系,因为你会在职场后期你会发现自己的圈子并不大。 +不是说非要你去维护人际关系,那至少请不要闹僵了。
+包括可以逢年过节的时候,可以去发一个微信、或者去关心一下。人脉关系这件事情,如果你现在没有时间去维持,那么你现在可以先保持。
++ +
+ + + + + +系统使用springboot,spring,springmvc,mybatisplus,mysql,redis,Tomcat进行开发,采用进行docker部署 +主要模块功能:
+该系统全部都由本人完成,包括项目文档编写,接口设计,数据库,设计页面设计
+项目周期:2019.2~2019.10
+在校期间 参与远程直流供电系统,后管系统建设。系统基于springboot进行开发、前后端分离。主要分为参数模块,设备模块等。 主要负责 设备模块及主要模块开发。
+项目周期:2019.11~至今
+新系统基于电子验印系统原型进行升级、改造.采用前后端分离。后端基于springboot 开发,前端基于Vue.js进行开发。新电子验印系统集成到客户端。 主要负责电子验印系统前端开发、后管系统开发、接口功能开发(柜面接口服务等) 、外系统对接(集中作业系统,影像平台系统,银企对账系统等)等及相关维护,通过easyOps发布流水线持续发版更新等。
+2017.09 ~ 2020.06
就读于河北软件学院 专业为软件技术 学历为专科+ +
+ + + + + ++ 本人有严谨的工作态度与高质量意识;能查阅各种开发技术手册,具有独立解决问题的能力。具备扎实的Java基础和三年开发经验,有良好的编程风格,独立熟练使用Spring全家桶等常用类库开发Java服务端程序、对SQL能够进行分析调优、对Java服务端程序故障能独立排查。工作责任心强,具有一定的承压能力。 +
+Java
基础,熟练使用Java
集合、Java IO
、多线程、反射等技术;Spring
、Spring Boot
、Netty
、Kafka
、Redis
,Elasticsearch
等开源框架;Spring Cloud
、Spring CloudAlibaba
体系;Spring
、Netty
框架,研究过核心源码,较为熟练借鉴框架中的设计,具备一定框架定制开发能力;SQL
、视图及存储过程,熟练使用索引和执行计划进行数据库调优;公司主要做“产业电商官网”模式,旗下运营:“中国耐材之窗网”、“冶金炉料网” 、“中国陶瓷官网”、“中国物流官网”四大行业电商平台,在职期间主要负责维护“中国耐材之窗网”和建设耐材部门相关项目。
+公司主要为金融行业提供计算机软件服务,在职期间主要负责建设验印系统和票据影像交换系统。
+Spring Boot、Netty
+框架与其他系统进行交互。Spring Boot
+等主流框架进行开发,前端基于Vue.js进行开发,采用分布式部署。2017.09 ~ 2020.06
就读于河北软件学院 专业为软件技术 学历为专科+ +
+ + + + + ++ 本人有严谨的工作态度与高质量意识;能查阅各种开发技术手册,具有独立解决问题的能力。具备扎实的Java基础和四年开发经验,有良好的编程风格,独立熟练使用Spring全家桶等常用类库开发Java服务端程序、对Java服务端程序故障能独立排查。工作责任心强,具有一定的承压能力。 +
+Java
基础,熟练使用Java
集合、Java IO
、多线程、反射等技术;熟悉java核心的集合框架;Spring Cloud
、Springcloud Alibaba
体系,对分布式微服务,旧服务改造,服务划分、服务治理、服务分层都有深入理解,有线上项目经验;Spring
、Netty
框架,研究过核心源码,较为熟练借鉴框架中的设计,具备一定框架定制开发能力;SQL
、视图及存储过程,熟练使用索引和执行计划进行数据库调优;公司主要做“产业电商官网”模式,旗下运营:“中国耐材之窗网”、“冶金炉料网” 、“中国陶瓷官网”、“中国物流官网”四大行业电商平台,在职期间主要负责维护“中国耐材之窗网”正常运行和建设耐材部门相关项目。
+简介:网站主要展示一些耐火材料行业相关的资讯信息,至今共为其迭代80多个版本。
+技术栈:Springboot、Springcloud alibaba、 Nacos、Mybatisplus、Redis、Elasticsearch、xxljob
工作职责:
+工作业绩:
+公司主要为金融行业提供计算机软件服务,在职期间主要负责建设验印系统和票据影像交换系统。
+简介:票据影像交换系统,即交换银行与银行之间的凭证对其进行验印。盛京银行-票据影像交换系统,基于旧系统改造。
+技术栈:Springboot、Netty、Springcloud alibaba、 Nacos、Mybatisplus、Redis
工作职责:
+简介:电子验印系统,即通过该程序可以核对凭证上印章与银行预留印章是否匹配。贵州银行-新电子验印系统,基于验印系统原型进行改造,推翻原型所有代码进行重建。新电子验印系统采用前后端分离设计,将前端部分集成到客户端中;后端基于Spring Boot
等主流框架进行开发,前端基于Vue.js进行开发,采用分布式部署。
技术栈:Springboot、Springcloud、Redis、Kafka、Mybatisplus、Vue、Hystrix
工作职责:
+2017.09 ~ 2020.06
就读于河北软件学院 专业为软件技术 学历为专科+ +
+ + + + + +Spring Web MVC是建立在Servlet API上的原始Web框架,从一开始就包含在Spring框架中。正式名称 “Spring Web MVC “来自其源模块的名称(spring-webmvc),但它更常被称为 “Spring MVC”。
+SpringMVC是基于Spring的,是Spring中的一个模块,专门用来做web开发使用的。
+SpringMVC也是一个容器,使用IoC核心技术,管理界面层中的控制器对象。SpringMVC的底层就是servlet,以servlet为核心,接收请求、处理请求,显示处理结果给用户。在此之前这个功能是由Servlet来实现的,现在使用SpringMVC来代替Servlet行驶控制器的角色和功能。 其核心Servlet是:DispatcherServlet。
+更多详情查看官方资料这里不在赘述。
+MVC模式是一种将应用程序分为三个主要部分的架构模式,分别是模型(Model)、视图(View)和控制器(Controller)。模型负责处理数据,视图负责展示数据,控制器负责协调模型和视图之间的交互。
+在没有使用SpringMVC之前都是使用Servlet在做Web开发。但是使用Servlet开发在接收请求参数,数据共享,页面跳转等操作相对比较复杂。servlet是java进行web开发的标准,既然springMVC是对servlet的封装,那么很显然SpringMVC底层就是Servlet,SpringMVC就是对Servlet进行深层次的封装。
+SpringMVC 是一种基于 Spring 框架的 MVC 设计模型,它是 Spring 框架的一个子项目。SpringMVC 的核心思想是将 MVC 设计模式应用于 Spring 框架,实现了请求-响应模式,将业务逻辑、数据、显示分离,提高了部分代码的复用性,降低了各个模块间的耦合性。
+流程描述:
+DispatcherServlet
;DispatcherServlet
收到请求调用 HandlerMapping
处理器映射器;DispatcherServlet
;DispatcherServlet
调用 HandlerAdapter
处理器适配器;HandlerAdapter
经过适配调用具体的处理器(Controller
,也叫后端控制器);Controller
执行完成返回 ModelAndView
;HandlerAdapter
将 controller执行结果
ModelAndView
返回给 DispatcherServlet
;DispatcherServlet
将 ModelAndView
传给 ViewReslover
视图解析器;ViewReslover
解析后返回具体 View
;DispatcherServlet
根据 View 进行渲染视图(即将模型数据填充至视图中);DispatcherServlet
响应用户; <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-web</artifactId>
+ </dependency>
+
@Controller
+@RequestMapping("/hello")
+public class HelloController{
+
+ @RequestMapping(method = RequestMethod.GET)
+ public String printHello(ModelMap model) {
+ model.addAttribute("message", "Hello Spring MVC Framework!");
+ return "hello";
+ }
+
+}
+
与Spring Web MVC并行,Spring Framework 5.0引入了一个响应式Web框架,其名称为 “Spring WebFlux”。与Spring MVC不同,它不需要Servlet API,完全异步和非阻塞,又被叫做响应式WebClient。
+Spring WebFlux 模块将默认的 web 服务器改为 Netty,所以具有Netty的特点,是完全非阻塞式的。通过少量的容器线程就可以支撑大量的并发访问,有一种池化思想在里面,所以 Spring WebFlux 可以有效提升系统的吞吐量和伸缩性但是并不能使接口的响应时间缩短。
+Spring WebFlux 是一个异步非阻塞式的 Web 框架,所以,它特别适合应用在 IO 密集型的服务中,比如微服务网关这样的应用中。
+更多信息请查看官方资料这里不在赘述。
+Spring WebFlux 底层实现依赖 reactor 和 netty。Spring 做的就是通过抽象和封装,把 reactor 的能力通过 Controller 来使用。
+ +请求执行的流程大致和SpringMVC差不多,SpringMVC核心控制器是DispatcherServlet,SpringWebFlux核心处理器是DispatcherHandler:
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-webflux</artifactId>
+ </dependency>
+
@RestController
+@RequestMapping("/webflux")
+public class HelloController {
+
+ @GetMapping("/hello")
+ public Mono<String> hello() {
+ return Mono.just("Hello Spring Webflux");
+ }
+
+ @GetMapping("/posts")
+ public Flux<Post> posts() {
+ WebClient webClient = WebClient.create();
+ Flux<Post> postFlux = webClient.get().uri("http://jsonplaceholder.typicode.com/posts").retrieve().bodyToFlux(Post.class);
+ return postFlux;
+ }
+
+}
+
在 WebFlux 中 Mono 和 Flux 均能充当响应式编程中发布者的角色:
+Spring WebFlux 并不是 Spring MVC 的替代方案,两者可以混合使用。都可以使用 Spring MVC 注解,如 @Controller、@RequestMapping等。均可以使用 Tomcat, Jetty, Undertow Servlet 容器。
+Spring WebFlux相比较Spring MVC最大的优势在于它是异步非堵塞的框架,可以让我们在不扩充硬件资源的前提下,提升系统的吞吐量和伸缩性;但是由于是异步非堵塞的框架,对于开发人员来说调试起来不太友好。 +而 Spring MVC 是同步阻塞的,如果你目前在 Spring MVC 框架中大量使用异步方案,那么,WebFlux 才是你想要的,否则,使用 Spring MVC 才是你的首选。
+官方建议:
+当我们引入一门新技术到原有的项目中,我们需要评估究竟为我们带来多少益处,同时还要评估为了这些益处所要付出的学习和改造成本,然后衡量收益,如果收益大值得则值得尝试。不能为了装逼而装逼,为了技术而技术。
++ +
+ + + + + +Spring是一个轻量级的Java开源框架,为了解决企业应用开发的复杂性而创建的。Spring的核心是控制反转(IOC)和面向切面(AOP)。
+简单来说,Spring是一个分层的JavaSE/EE 一站式轻量级开源框架。在每一层都提供支持。
+Spring是一个轻量级的框架,简化我们的开发,里面重点包含两个模块分别是IOC和AOP。
+Spring虽然把它当成框架来使用,但其本质是一个容器,即IOC容器,里面最核心是如何创建对象和管理对象,里面包含了Bean的生命周期和Spring的一些扩展点,包含对AOP的应用。 +除此之外,Spring真正的强大之处在于其生态,它包含了Spring Framework、Spring Boot、Spring Cloud等一些列框架,极大提高了开发效率。
+参考:https://blog.csdn.net/scjava/article/details/109587619
+ +核心方法AbstractApplicationContext#refresh()
public void refresh() throws BeansException, IllegalStateException {
+ synchronized (this.startupShutdownMonitor) {
+ // Prepare this context for refreshing.
+ prepareRefresh();
+
+ // Tell the subclass to refresh the internal bean factory.
+ ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
+
+ // Prepare the bean factory for use in this context.
+ prepareBeanFactory(beanFactory);
+
+ try {
+ // Allows post-processing of the bean factory in context subclasses.
+ postProcessBeanFactory(beanFactory);
+
+ // Invoke factory processors registered as beans in the context.
+ invokeBeanFactoryPostProcessors(beanFactory);
+
+ // Register bean processors that intercept bean creation.
+ registerBeanPostProcessors(beanFactory);
+
+ // Initialize message source for this context.
+ initMessageSource();
+
+ // Initialize event multicaster for this context.
+ initApplicationEventMulticaster();
+
+ // Initialize other special beans in specific context subclasses.
+ onRefresh();
+
+ // Check for listener beans and register them.
+ registerListeners();
+
+ // Instantiate all remaining (non-lazy-init) singletons.
+ finishBeanFactoryInitialization(beanFactory);
+
+ // Last step: publish corresponding event.
+ finishRefresh();
+ }
+
+ catch (BeansException ex) {
+ // ...
+ }
+
+ finally {
+ // ...
+ }
+ }
+}
+
Spring循环依赖调用流程:
+在BeanA中注入BeanB,BeanB中注入BeanA,在BeanA创建的过程中,会先判断容器中A是否存在,如果不存在会先初始化BeanA,然后给BeanA赋值,此时会给BeanA里的BeanB属性赋值,在赋值之前会将创建BeanA的流程放到三级缓存中(三级缓存为Map结构,key为String,value为函数式接口); 由于BeanA里面包含BeanB,所以接下来给BeanB执行创建流程,判断容器中是否存在BeanB,给属性B赋值,此时会给BeanB里的BeanA属性赋值。
+在判断容器中是否存在该Bean时,查找顺序为:一级缓存->二级缓存->三级缓存,经历过上面的步骤后,此时三级缓存中A和B都有值(为BeanA、B的创建流程),不需要再进行初始化操作,然后将会执行BeanA的创建流程并将其放入二级缓存中并删除三级缓存中的值,但是此时BeanA中的BeanB还未赋值进行完全的初始化, +BeanA已经创建,此时会将BeanA赋值给BeanB中的A属性,至此BeanB已经完全赋值,然后将完全赋值的BeanB放入一级缓存中并删除三级缓存中的值,由于BeanB已经完全赋值,此时将其赋值给BeanA,将BeanA放入一级缓存并删除二级缓存,至此循环依赖问题解决。
+Spring循环依赖大致调用思路:
+官网地址:https://spring.io/projects/spring-boot
++++
SpringBoot
是由Pivotal
团队提供的全新框架,其设计目的是用来简化新Spring
应用的初始搭建以及开发过程。 +该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。SpringBoot
提供了一种新的编程范式,可以更加快速便捷地开发Spring
项目,在开发过程当中可以专注于应用程序本身的功能开发,而无需在Spring
配置上花太大的工夫。
SpringBoot
基于 Sring4
进行设计,继承了原有 Spring
框架的优秀基因。SpringBoot
准确的说并不是一个框架,而是一些类库的集合。maven
或者 gradle
项目导入相应依赖即可使用 SpringBoot
,而无需自行管理这些类库的版本。
特点:
+Spring
项目:
+SpringBoot
可以以 jar 包的形式独立运行,运行一个 SpringBoot
项目只需通过 java–jar xx.jar
来运行。Servlet
容器:
+SpringBoot
可选择内嵌 Tomcat
、Jetty
或者 Undertow
,这样我们无须以 war
包形式部署项目。starter
简化 Maven
配置:
+Spring
提供了一系列的 starter
pom 来简化 Maven
的依赖加载,例如,当你使用了spring-boot-starter-web
时,会自动加入依赖包。Spring
:
+SpringBoot
会根据在类路径中的 jar 包、类,为 jar 包里的类自动配置 Bean,这样会极大地减少我们要使用的配置。当然,SpringBoot
只是考虑了大多数的开发场景,并不是所有的场景,若在实际开发中我们需要自动配置 Bean
,而 SpringBoot
没有提供支持,则可以自定义自动配置。SpringBoot
提供基于 http、ssh、telnet
对运行时的项目进行监控。SpringBoot
的神奇的不是借助于代码生成来实现的,而是通过条件注解来实现的,这是 Spring 4.x
提供的新特性。Spring 4.x
提倡使用 Java 配置和注解配置组合,而 SpringBoot
不需要任何 xml 配置即可实现 Spring
的所有配置。@SpringBootApplication
这个注解通常标注在启动类上:
@SpringBootApplication
+public class SpringBootExampleApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootExampleApplication.class, args);
+ }
+}
+
@SpringBootApplication
是一个复合注解,即由其他注解构成。核心注解是@SpringBootConfiguration
和@EnableAutoConfiguration
@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+@SpringBootConfiguration
+@EnableAutoConfiguration
+@ComponentScan(
+ excludeFilters = {@Filter(
+ type = FilterType.CUSTOM,
+ classes = {TypeExcludeFilter.class}
+), @Filter(
+ type = FilterType.CUSTOM,
+ classes = {AutoConfigurationExcludeFilter.class}
+)}
+)
+public @interface SpringBootApplication{
+}
+
@SpringBootConfiguration
核心注解是@Configuration
代表自己是一个Spring
的配置类
@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Configuration
+public @interface SpringBootConfiguration {
+}
+
@Configuration
底层实现就是一个Component
++指示带注释的类是一个“组件”。 +在使用基于注释的配置和类路径扫描时,这些类被视为自动检测的候选类。
+
/**
+ * Indicates that an annotated class is a "component".
+ * Such classes are considered as candidates for auto-detection
+ * when using annotation-based configuration and classpath scanning.
+ *
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Indexed
+public @interface Component
+
核心注解是@AutoConfigurationPackage
和@Import({AutoConfigurationImportSelector.class})
@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+@AutoConfigurationPackage
+@Import({AutoConfigurationImportSelector.class})
+public @interface EnableAutoConfiguration {
+}
+
@AutoConfigurationPackage
注解核心是引入了一个@Import(AutoConfigurationPackages.Registrar.class)
配置类,该类实现了ImportBeanDefinitionRegistrar
接口
/**
+ * {@link ImportBeanDefinitionRegistrar} to store the base package from the importing
+ * configuration.
+ */
+ static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
+ @Override
+ public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
+ register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
+ }
+ @Override
+ public Set<Object> determineImports(AnnotationMetadata metadata) {
+ return Collections.singleton(new PackageImports(metadata));
+ }
+
+ }
+
++这里可以打断点自己看一下
+
@AutoConfigurationPackage
这个注解本身的含义就是将主配置类(@SpringBootApplication
标注的类)所在的包下面所有的组件都扫描到 spring
容器中。
AutoConfigurationImportSelector
核心代码如下
/**
+ * Return the auto-configuration class names that should be considered. By default
+ * this method will load candidates using {@link SpringFactoriesLoader} with
+ * {@link #getSpringFactoriesLoaderFactoryClass()}.
+ * @param metadata the source metadata
+ * @param attributes the {@link #getAttributes(AnnotationMetadata) annotation
+ * attributes}
+ * @return a list of candidate configurations
+ */
+ protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
+ List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
+ getBeanClassLoader());
+ Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
+ + "are using a custom packaging, make sure that file is correct.");
+ return configurations;
+ }
+
+ /**
+ * Return the class used by {@link SpringFactoriesLoader} to load configuration
+ * candidates.
+ * @return the factory class
+ */
+ protected Class<?> getSpringFactoriesLoaderFactoryClass() {
+ return EnableAutoConfiguration.class;
+ }
+ protected ClassLoader getBeanClassLoader() {
+ return this.beanClassLoader;
+ }
+
getSpringFactoriesLoaderFactoryClass
方法返回EnableAutoConfiguration.class
目的就是为了将启动类所需的所有资源导入。
在getCandidateConfigurations
中有如下代码
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
+
大意:在META-INF/spring.factories
中没有发现自动配置类。如果您使用的是自定义打包,请确保该文件是正确的。
+
spring.factories
包含了很多类,但不是全部都加载的,在某些类里面,是有一个条件@ConditionalOnXXX
注解,只有当这个注解上的条件满足才会加载。
例如:SpringApplicationAdminJmxAutoConfiguration
@Configuration(proxyBeanMethods = false)
+@AutoConfigureAfter(JmxAutoConfiguration.class)
+@ConditionalOnProperty(prefix = "spring.application.admin", value = "enabled", havingValue = "true",
+ matchIfMissing = false)
+public class SpringApplicationAdminJmxAutoConfiguration
+
当 Springboot
启动的时候,会执行AutoConfigurationImportSelector
这个类中的getCandidateConfigurations
方法,这个方法会帮我们加载META-INF/spring.factories
文件里面的当@ConditionXXX
注解条件满足的类。
提到Bean的自动装配就不得不说一下Spring的核心IOC,IOC全称为Inversion of Control,中文译为控制反转,是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。
+IOC的一个重点是在系统运行中,动态的向某个对象提供它所需要的其他对象。这一点是通过DI(Dependency Injection,依赖注入)来实现的。
+所谓IOC,对于spring框架来说,就是由spring来负责控制对象的生命周期和对象间的关系。IOC负责创建对象,把它们连接在一起,配置它们,并管理他们的整个生命周期从创建到销毁,所以可将IOC理解为一个大容器。IOC使用依赖注入(DI)来管理组成一个应用程序的组件。这些对象被称为 Spring Beans。
+一般在代码里面由这些注解体现:
+@Component
+@Service
+@Repository
+@Controller
+@Autowired
+@Resource
+@Inject
+
Spring
利用依赖注入(DI),完成对IOC容器中各个组件的依赖关系赋值。
Spring
提供三种装配方式:
此处详细介绍基于注解的自动装配
+自动装配 | +来源 | +支持@Primary | +springboot支持属性 | +
---|---|---|---|
@Autowired | +Springboot原生 | +支持 | +boolean required | +
@Resource | +JSR-250,JDK自带 | +不支持 | +String name | +
@Inject | +JSR-330,需要导入javax.inject | +支持 | +无其他属性 | +
可以放在构造器、参数、方法、属性上
+源码如下:
+@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Autowired {
+
+ /**
+ * Declares whether the annotated dependency is required.
+ * <p>Defaults to {@code true}.
+ */
+ boolean required() default true;
+
+}
+
使用@Autowired
注解通常将其加载属性上或者构造器上,让其自动注入;默认是按照类型去容器中寻找对应的组件,例如:
+public class SpringBootExampleApplication {
+
+
+ public static void main(String[] args) {
+ AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(TestConfig.class);
+
+ TestService testService = annotationConfigApplicationContext.getBean(TestService.class);
+ // TestService 实例=====>TestService(testDao=TestDao(name=default))
+ System.out.println("TestService 实例=====>" +testService);
+
+ }
+
+}
+
+
+// 扫描的包名称
+@ComponentScan({"com.example.springboot.example.task"})
+@Configuration
+class TestConfig{
+
+}
+
+
+@ToString
+@Service
+class TestService {
+
+ @Autowired
+ TestDao testDao;
+
+}
+
+@ToString
+@Repository
+class TestDao{
+
+ @Getter
+ @Setter
+ private String name = "default";
+
+}
+
如果容器中有多个组件的名称相同,可以通过@Qualifier
来进行选择注入;
public class SpringBootExampleApplication {
+
+
+ public static void main(String[] args) {
+ AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(TestConfig.class);
+
+ TestService testService = annotationConfigApplicationContext.getBean(TestService.class);
+ // TestService 实例=====>TestService(testDao=TestDao(name=我是默认的TestDao))
+ System.out.println("TestService 实例=====>" +testService);
+
+ }
+
+}
+
+
+@ComponentScan({"com.example.springboot.example.task"})
+@Configuration
+class TestConfig{
+
+ @Bean(name = "testDao2")
+ public TestDao testDao(){
+ TestDao testDao = new TestDao();
+ testDao.setName("我是testDao2");
+ return testDao;
+ }
+}
+
+@ToString
+@Service
+class TestService {
+
+ @Autowired
+ @Qualifier("testDao")
+ TestDao testDao;
+
+}
+
+@ToString
+@Repository
+class TestDao{
+
+ @Getter
+ @Setter
+ private String name = "我是默认的TestDao";
+
+}
+
除了使用@Qualifier
来进行选择注入外,也可以使用@Primary
来设置 bean 的优先级,默认情况下指定让哪个 bean 优先注入;
@Primary
注解是在没有明确指定的情况下,默认使用的 bean,如果你明确用@Qualifier
指定,则会使用@Qualifier
指定的bean;
+确保测试结果准确,在使用@Primary
时,将@Qualifier
去掉。
public class SpringBootExampleTaskApplication {
+
+
+ public static void main(String[] args) {
+ AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(TestConfig.class);
+
+ TestService testService = annotationConfigApplicationContext.getBean(TestService.class);
+// TestService 实例=====>TestService(testDao=TestDao(name=我是testDao2))
+ System.out.println("TestService 实例=====>" +testService);
+
+ }
+
+}
+
+
+@ComponentScan({"com.example.springboot.example.task"})
+@Configuration
+class TestConfig{
+
+ @Primary
+ @Bean(name = "testDao2")
+ public TestDao testDao(){
+ TestDao testDao = new TestDao();
+ testDao.setName("我是testDao2");
+ return testDao;
+ }
+}
+
+@ToString
+@Service
+class TestService {
+
+ @Autowired
+ TestDao testDao;
+
+}
+
+@ToString
+@Repository
+class TestDao{
+
+ @Getter
+ @Setter
+ private String name = "我是默认的TestDao";
+
+}
+
如果使用@Autowired
在容器中没有对应的组件名称,默认情况下会报错。
nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException
+
如果没有找到对应的 bean 不报错,可以通过@Autowired(required = false)
来进行设置
@ToString
+@Service
+class TestService {
+
+ // TestService 实例=====>TestService(testDao=null)
+
+ @Autowired(required = false)
+ TestDao testDao;
+
+}
+
@Autowired
注解不仅可以标注在属性上,也可以标注在方法上,当标注在方法上时,Spring容器创建当前对象,就会调用该方法完成赋值,方法使用的参数从IOC容器中获取。
通过测试打印对象的地址可以看到,方法中的参数确实是从IOC容器中获取的。
+public class SpringBootExampleTaskApplication {
+
+
+ public static void main(String[] args) {
+ AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(TestConfig.class);
+
+ TestService testService = annotationConfigApplicationContext.getBean(TestService.class);
+ System.out.println("TestService 中的实例=====>" +testService);
+
+ TestDao testDao = annotationConfigApplicationContext.getBean(TestDao.class);
+ System.out.println("TestDao 中的实例=====>" +testDao);
+
+ // TestService 中的实例=====>TestService(testDao=com.example.springboot.example.task.TestDao@5fe94a96)
+ //TestDao 中的实例=====>com.example.springboot.example.task.TestDao@5fe94a96
+
+
+ }
+
+}
+
+
+@ComponentScan({"com.example.springboot.example.task"})
+@Configuration
+class TestConfig{
+}
+
+@ToString
+@Service
+class TestService {
+
+
+ TestDao testDao;
+
+ @Autowired
+ public void setTestDao(TestDao testDao) {
+ this.testDao = testDao;
+ }
+}
+
+
+@Repository
+class TestDao{
+
+ private String name = "我是默认的TestDao";
+
+}
+
也可以加在参数上,与加在方法上类似也是从IOC容器中获取该对象。
+@ToString
+@Service
+class TestService {
+
+
+ TestDao testDao;
+
+ public void setTestDao(@Autowired TestDao testDao) {
+ this.testDao = testDao;
+ }
+}
+
在Spring
创建对象的时候会默认调用组件的无参构造方法,如果只有一个有参构造,如果想要创建对象,则必须调用该有参构造;
+所以当一个组件只有一个有参构造时,则可以不用写@Autowired
注解。
@ToString
+@Service
+class TestService {
+
+
+ TestDao testDao;
+
+ // @Autowired
+ public TestService(TestDao testDao) {
+ this.testDao = testDao;
+ }
+
+}
+
除了通过构造方法的方式实例化组件,也可以通过用bean标注的形式,来实例化容器中的组件。
+public class SpringBootExampleTaskApplication {
+
+
+ public static void main(String[] args) {
+ AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(TestConfig.class);
+
+ TestService testService = annotationConfigApplicationContext.getBean(TestService.class);
+ System.out.println("TestService 中的实例=====>" +testService);
+
+ TestDao testDao = annotationConfigApplicationContext.getBean(TestDao.class);
+ System.out.println("TestDao 中的实例=====>" +testDao);
+
+ TestDao1 testDao1 = annotationConfigApplicationContext.getBean(TestDao1.class);
+ System.out.println("TestDao1 中的实例=====>" +testDao);
+
+ // TestService 中的实例=====>TestService(testDao=com.example.springboot.example.task.TestDao@639c2c1d)
+ //TestDao 中的实例=====>com.example.springboot.example.task.TestDao@639c2c1d
+ //TestDao1 中的实例=====>com.example.springboot.example.task.TestDao@639c2c1d
+
+ }
+
+}
+
+
+@ComponentScan({"com.example.springboot.example.task"})
+@Configuration
+class TestConfig{
+
+ @Bean
+ public TestDao1 testDao1(TestDao testDao){
+ TestDao1 testDao1 = new TestDao1();
+ testDao1.setTestDao(testDao);
+ return testDao1;
+ }
+}
+
+@ToString
+@Service
+class TestService {
+
+
+ TestDao testDao;
+
+ @Autowired
+ public TestService(TestDao testDao) {
+ this.testDao = testDao;
+ }
+
+}
+
+
+@Component
+class TestDao{
+}
+
+class TestDao1{
+ @Setter
+ TestDao testDao;
+}
+
/
+ * @see AutowiredAnnotationBeanPostProcessor
+ */
+@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Autowired{}
+
在@Autowired
注解文档注释上面,可以看到与之息息相关的一个类AutowiredAnnotationBeanPostProcessor
,即@Autowired
后置处理器;
+可以看到该类实现了MergedBeanDefinitionPostProcessor
接口,在postProcessMergedBeanDefinition
方法上打一个断点,就可以看到@Autowired
的调用栈。
@Autowired
注解调用栈:
AbstractApplicationContext.refresh(容器初始化)
+---> registerBeanPostProcessors (注册AutowiredAnnotationBeanPostProcessor)
+---> finishBeanFactoryInitialization
+---> AbstractAutowireCapableBeanFactory.doCreateBean
+---> AbstractAutowireCapableBeanFactory.applyMergedBeanDefinitionPostProcessors
+---> MergedBeanDefinitionPostProcessor.postProcessMergedBeanDefinition
+---> AutowiredAnnotationBeanPostProcessor.findAutowiringMetadata
+
核心调用:
+postProcessMergedBeanDefinition`->`findAutowiringMetadata`->`buildAutowiringMetadata
+
相关源码:
+@Override
+public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
+ // 调用 findAutowiringMetadata
+ InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null);
+ metadata.checkConfigMembers(beanDefinition);
+}
+
+private InjectionMetadata findAutowiringMetadata(String beanName, Class<?> clazz, @Nullable PropertyValues pvs) {
+ // Fall back to class name as cache key, for backwards compatibility with custom callers.
+ String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());
+ // Quick check on the concurrent map first, with minimal locking.
+ InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey);
+ if (InjectionMetadata.needsRefresh(metadata, clazz)) {
+ synchronized (this.injectionMetadataCache) {
+ metadata = this.injectionMetadataCache.get(cacheKey);
+ if (InjectionMetadata.needsRefresh(metadata, clazz)) {
+ if (metadata != null) {
+ metadata.clear(pvs);
+ }
+ // 调用buildAutowiringMetadata
+ metadata = buildAutowiringMetadata(clazz);
+ this.injectionMetadataCache.put(cacheKey, metadata);
+ }
+ }
+ }
+ return metadata;
+ }
+
+
+
+private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {
+ LinkedList<InjectionMetadata.InjectedElement> elements = new LinkedList<>();
+ Class<?> targetClass = clazz;//需要处理的目标类
+
+ do {
+ final LinkedList<InjectionMetadata.InjectedElement> currElements = new LinkedList<>();
+
+ /*通过反射获取该类所有的字段,并遍历每一个字段,并通过方法findAutowiredAnnotation遍历每一个字段的所用注解,并如果用autowired修饰了,则返回auotowired相关属性*/
+
+ ReflectionUtils.doWithLocalFields(targetClass, field -> {
+ AnnotationAttributes ann = findAutowiredAnnotation(field);
+ if (ann != null) {//校验autowired注解是否用在了static方法上
+ if (Modifier.isStatic(field.getModifiers())) {
+ if (logger.isWarnEnabled()) {
+ logger.warn("Autowired annotation is not supported on static fields: " + field);
+ }
+ return;
+ }//判断是否指定了required
+ boolean required = determineRequiredStatus(ann);
+ currElements.add(new AutowiredFieldElement(field, required));
+ }
+ });
+ //和上面一样的逻辑,但是是通过反射处理类的method
+ ReflectionUtils.doWithLocalMethods(targetClass, method -> {
+ Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
+ if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
+ return;
+ }
+ AnnotationAttributes ann = findAutowiredAnnotation(bridgedMethod);
+ if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
+ if (Modifier.isStatic(method.getModifiers())) {
+ if (logger.isWarnEnabled()) {
+ logger.warn("Autowired annotation is not supported on static methods: " + method);
+ }
+ return;
+ }
+ if (method.getParameterCount() == 0) {
+ if (logger.isWarnEnabled()) {
+ logger.warn("Autowired annotation should only be used on methods with parameters: " +
+ method);
+ }
+ }
+ boolean required = determineRequiredStatus(ann);
+ PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
+ currElements.add(new AutowiredMethodElement(method, required, pd));
+ }
+ });
+ //用@Autowired修饰的注解可能不止一个,因此都加在currElements这个容器里面,一起处理
+ elements.addAll(0, currElements);
+ targetClass = targetClass.getSuperclass();
+ }
+ while (targetClass != null && targetClass != Object.class);
+
+ return new InjectionMetadata(clazz, elements);
+ }
+
当Spring
容器启动时,AutowiredAnnotationBeanPostProcessor
组件会被注册到容器中,然后扫描代码,如果带有 @Autowired
注解,则将依赖注入信息封装到 InjectionMetadata
中。
最后创建 bean
,即实例化对象和调用初始化方法,会调用各种 XXXBeanPostProcessor
对 bean
初始化,其中包括AutowiredAnnotationBeanPostProcessor
,它负责将相关的依赖注入到容器中。
Spring 自动装配除了@Autowired
注解外,也支持JSR-250中的@Resource
和JSR-330中的@Inject
注解,来进行自动装配;
public class SpringBootExampleTaskApplication {
+
+
+ public static void main(String[] args) {
+ AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(TestConfig.class);
+
+ TestService testService = annotationConfigApplicationContext.getBean(TestService.class);
+// TestService 实例=====>TestService(testDao=TestDao(name=我是默认的TestDao))
+ System.out.println("TestService 实例=====>" +testService);
+
+ }
+
+}
+
+
+@ComponentScan({"com.example.springboot.example.task"})
+@Configuration
+class TestConfig{
+
+ @Bean(name = "testDao2")
+ public TestDao testDao(){
+ TestDao testDao = new TestDao();
+ testDao.setName("我是testDao2");
+ return testDao;
+ }
+}
+
+@ToString
+@Service
+class TestService {
+
+
+ @Resource
+ TestDao testDao;
+
+}
+
+
+@ToString
+@Repository
+class TestDao{
+
+ @Getter
+ @Setter
+ private String name = "我是默认的TestDao";
+
+}
+
使用@Inject
注解需要导入:
<dependency>
+ <groupId>javax.inject</groupId>
+ <artifactId>javax.inject</artifactId>
+ <version>1</version>
+</dependency>
+
@ToString
+@Service
+class TestService {
+
+ // TestService 实例=====>TestService(testDao=TestDao(name=我是默认的TestDao))
+ @Inject
+ TestDao testDao;
+
+}
+
通过实现Aware
接口的子接口,来使用Spring的底层的组件。Aware
接口类似于回调方法的形式在 Spring 加载的时候将我们自定以的组件加载。
/**
+ * A marker superinterface indicating that a bean is eligible to be notified by the
+ * Spring container of a particular framework object through a callback-style method.
+ * The actual method signature is determined by individual subinterfaces but should
+ * typically consist of just one void-returning method that accepts a single argument.
+ */
+public interface Aware {
+
+}
+
使用测试
+@Component
+class TestService implements ApplicationContextAware, EmbeddedValueResolverAware, BeanFactoryAware {
+
+ public TestService() {
+ }
+
+ ApplicationContext applicationContext;
+
+
+ @Override
+ public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
+ System.out.println("获取实例名称===>" + beanFactory.getBean("TestService"));
+ }
+
+ @Override
+ public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+ this.applicationContext = applicationContext;
+ System.out.println("获取容器对象===> "+ applicationContext);
+ }
+
+ @Override
+ public void setEmbeddedValueResolver(StringValueResolver resolver) {
+ System.out.println(resolver.resolveStringValue("我是${os.name},今年${10*2.1}岁"));
+ }
+}
+
关于这些Aware
都是使用AwareProcessor
进行处理的,比如:ApplicationContextAwareProcessor
就是处理ApplicationContextAware
接口的。
{jdbc.username}
此处就可以进行替换操作;Bean
的生命周期,即Bean
的 实例化->初始化->使用->销毁
的过程。
我们可以使用 xml 配置的方式来指定,bean
在初始化、销毁的时候调用对应的方法:
<bean id="getDemoEntity" class="com.my.demo" init-method="init" destroy-method="destroy" />
+
也可以使用注解的方式,来调用bean在初始化、销毁的时候调用对应的方法:
+public class MainTest {
+ public static void main(String[] args) {
+ // 获取Spring IOC容器
+ AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(DemoConfiguration.class);
+ System.out.println("容器初始化完成...");
+
+ annotationConfigApplicationContext.close();
+ System.out.println("容器销毁了...");
+ }
+}
+
+@Configuration
+class DemoConfiguration {
+ @Bean(initMethod = "init", destroyMethod = "destroy")
+ public DemoEntity getDemoEntity() {
+ return new DemoEntity();
+ }
+}
+
+class DemoEntity {
+ public DemoEntity(){
+ System.out.println("调用了构造器...");
+ }
+
+ public void init(){
+ System.out.println("调用了初始化方法...");
+ }
+
+ public void destroy(){
+ System.out.println("调用了销毁方法...");
+ }
+}
+
需要注意的是,上面演示的是单实例 bean
,如果是多实例 bean
,初始化和销毁会不一样。
单实例 bean
:
多实例 bean
:
bean
;多实例注解代码:
+@Scope("prototype")
+@Bean(initMethod="init",destroyMethod="destroy")
+public Test test(){}
+
通过让Bean
实现 InitializingBean
(定义初始化逻辑)和实现DisposableBean
(销毁逻辑)实现初始化bean
和销毁bean
:
public class MainTest {
+ public static void main(String[] args) {
+ // 获取Spring IOC容器
+ AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(DemoEntity.class);
+ System.out.println("容器初始化完成...");
+
+ annotationConfigApplicationContext.close();
+ System.out.println("容器销毁了...");
+ }
+}
+
+@Component
+class DemoEntity implements InitializingBean, DisposableBean {
+ public DemoEntity(){
+ System.out.println("调用了构造器...");
+ }
+
+ @Override
+ public void destroy(){
+ System.out.println("调用了销毁方法...");
+ }
+
+ @Override
+ public void afterPropertiesSet() throws Exception {
+ System.out.println("调用了初始化方法...");
+ }
+}
+
Java提供了对应的注解,也可以调用Bean
的初始化方法和销毁方法:
@PostConstruct
标注该注解的方法,在bean
创建完成并且属性赋值完成 来执行初始化方法;@PreDestroy
, 在容器销毁bean
之前通知我们进行bean
的清理工作;这两个注解不是spring
的注解是JSR250
JDK带的注解。
public class MainTest {
+ public static void main(String[] args) {
+ // 获取Spring IOC容器
+ AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(DemoEntity.class);
+ System.out.println("容器初始化完成...");
+
+ annotationConfigApplicationContext.close();
+ System.out.println("容器销毁了...");
+ }
+}
+
+@Component
+class DemoEntity {
+ public DemoEntity(){
+ System.out.println("调用了构造器...");
+ }
+
+ // 销毁之前调用
+ @PreDestroy
+ public void destroy(){
+ System.out.println("调用了销毁方法...");
+ }
+
+ // 对象创建并赋值之后调用
+ @PostConstruct
+ public void init() {
+ System.out.println("调用了初始化方法...");
+ }
+}
+
除了上面的几种方法,也可以使用BeanPostProcessor
,Bean
的后置处理器,在初始化前后进行处理工作。
postProcessBeforeInitialization
:会在初始化完成之前调用
+postProcessAfterInitialization
:会在初始化完成之后调用
public class MainTest {
+ public static void main(String[] args) {
+ // 获取Spring IOC容器
+ AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(DemoConfiguration.class);
+ System.out.println("容器初始化完成...");
+
+ annotationConfigApplicationContext.close();
+ System.out.println("容器销毁了...");
+ }
+}
+
+@Configuration
+class DemoConfiguration implements BeanPostProcessor {
+
+ @Bean(initMethod = "init", destroyMethod = "destroy")
+ public DemoEntity getDemoEntity(){
+ return new DemoEntity();
+ }
+
+ @Override
+ public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
+ System.out.println("调用了 postProcessBeforeInitialization");
+ return bean;
+ }
+
+ @Override
+ public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
+ System.out.println("调用了 postProcessAfterInitialization");
+ return bean;
+ }
+}
+
+@Component
+class DemoEntity {
+ public DemoEntity(){
+ System.out.println("调用了构造器...");
+ }
+
+ public void destroy(){
+ System.out.println("调用了销毁方法...");
+ }
+
+ public void init() {
+ System.out.println("调用了初始化方法...");
+ }
+}
+
调用顺序:
+++创建对象 –> postProcessBeforeInitialization –> 初始化 –> postProcessAfterInitialization –> 销毁
+
通过打断点,可以看到,在创建bean
的时候会,会调用AbstractAutowireCapableBeanFactory
类的doCreateBean
方法,这也是创建bean
的核心方法。
try {
+ populateBean(beanName, mbd, instanceWrapper);
+ exposedObject = initializeBean(beanName, exposedObject, mbd);
+ }
+
+ // ======= initializeBean =======
+ if (mbd == null || !mbd.isSynthetic()) {
+ wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
+ }
+
+ try {
+ invokeInitMethods(beanName, wrappedBean, mbd);
+ }
+ catch (Throwable ex) {
+ throw new BeanCreationException(
+ (mbd != null ? mbd.getResourceDescription() : null),
+ beanName, "Invocation of init method failed", ex);
+ }
+ if (mbd == null || !mbd.isSynthetic()) {
+ wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
+ }
+
调用栈大致如下:
+populateBean()
+{
+ applyBeanPostProcessorsBeforeInitialization() -> invokeInitMethods() -> applyBeanPostProcessorsAfterInitialization()
+}
+
在初始化之前调用populateBean()
方法,给bean
进行属性赋值,之后在调用applyBeanPostProcessorsBeforeInitialization
方法;
applyBeanPostProcessorsBeforeInitialization
源码:
public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName)
+ throws BeansException {
+
+ Object result = existingBean;
+ for (BeanPostProcessor processor : getBeanPostProcessors()) {
+ Object current = processor.postProcessBeforeInitialization(result, beanName);
+ if (current == null) {
+ return result;
+ }
+ result = current;
+ }
+ return result;
+ }
+
该方法作用,遍历容器中所有的BeanPostProcessor
挨个执行postProcessBeforeInitialization
方法,一旦返回null
,将不会执行后面bean
的postProcessBeforeInitialization
方法。
之后在调用invokeInitMethods
方法,进行bean
的初始化,最后在执行applyBeanPostProcessorsAfterInitialization
方法,执行一些初始化之后的工作。
AOP全称:Aspect-Oriented Programming
,译为面向切面编程 。AOP可以说是对OOP的补充和完善。在程序原有的纵向执行流程中,针对某一个或某些方法添加通知(方法),形成横切面的过程就叫做面向切面编程。
实现AOP的技术,主要分为两大类: 一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“切面”,从而使得编译器可以在编译期间织入有关“切面”的代码,属于静态代理。
+作用:
+动态代理,可以说是AOP的核心了。在Spring
中主要使用了两种动态代理:
JDK的动态代理时基于Java 的反射机制来实现的,是Java 原生的一种代理方式。他的实现原理就是让代理类和被代理类实现同一接口,代理类持有目标对象来达到方法拦截的作用。
+通过接口的方式有两个弊端一个就是必须保证被代理类有接口,另一个就是如果相对被代理类的方法进行代理拦截,那么就要保证这些方法都要在接口中声明。接口继承的是java.lang.reflect.InvocationHandler
。
CGLib 动态代理使用的 ASM 这个非常强大的 Java 字节码生成框架来生成class
,基于继承的实现动态代理,可以直接通过 super 关键字来调用被代理类的方法.子类可以调用父类的方法,不要求有接口。
使用AOP大致可以分为三步:
+@Aspect
注解标注切面类。Spring
何时何地的运行:
+@Before
:前置通知,在目标方法运行之前执行;@After
: 后置通知,在目标方法运行之后执行,无论方法是否出现异常都会执行;@Around
: 环绕通知,通过joinPoint.proceed()
方法手动控制目标方法的执行;@AfterThrowing
: 异常通知,在目标方法出现异常之后执行;@AfterReturning
: 返回通知,在目标方法返回之后执行;@EnableAspectJAutoProxy
开启基于注解的AOP模式。public class MainTest {
+ public static void main(String[] args) {
+ // 获取Spring IOC容器
+ AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(DemoConfiguration.class);
+
+ DemoEntity demoEntity = annotationConfigApplicationContext.getBean(DemoEntity.class);
+ demoEntity.myAspectTest("123");
+
+ annotationConfigApplicationContext.close();
+ }
+}
+
+@EnableAspectJAutoProxy
+@Configuration
+class DemoConfiguration{
+
+ @Bean
+ public DemoEntity getDemoEntity(){
+ return new DemoEntity();
+ }
+
+ @Bean
+ public DemoAspect gerDemoAspect(){
+ return new DemoAspect();
+ }
+
+}
+
+@Aspect
+class DemoAspect {
+
+ @Pointcut("execution(* com.lilian.ticket.image.exchange.DemoEntity.myAspectTest(..))")
+ public void pointer() {}
+
+ @Before("pointer()")
+ public void beforeTest(JoinPoint joinPoint) {
+ System.out.println("调用了AOP,前置通知");
+ Object[] args = joinPoint.getArgs();
+ System.out.println("前置通知:目标方法参数:[" + args[0] + "]");
+ }
+
+ @After("pointer()")
+ public void afterTest(JoinPoint joinPoint){
+ System.out.println("调用了AOP,后置通知");
+ Object[] args = joinPoint.getArgs();
+ System.out.println("后置通知:目标方法参数:[" + args[0] + "]");
+ }
+
+ @Around("pointer()")
+ public Object aroundTest(ProceedingJoinPoint joinPoint) {
+ System.out.println("===调用了AOP,环绕通知===");
+ System.out.println("环绕通知目标方法执行前");
+ Object[] args = joinPoint.getArgs();
+ System.out.println("环绕通知:目标方法参数:[" + args[0] + "]");
+ Object proceed = null;
+ try {
+ proceed = joinPoint.proceed();
+ } catch (Throwable throwable) {
+ throwable.printStackTrace();
+ }
+ System.out.println("环绕通知目标方法执行后\n");
+ return proceed;
+ }
+
+ @AfterThrowing(pointcut="pointer()", throwing="ex")
+ public void afterThrowingTest(JoinPoint joinPoint, Exception ex) {
+ System.out.println("异常通知==>["+ex.getMessage()+"]\n");
+ }
+
+ @AfterReturning("pointer()")
+ public void afterReturnTest(JoinPoint joinPoint){
+ Object[] args = joinPoint.getArgs();
+ System.out.println("有返回值的后置通知:目标方法参数:[" + args[0] + "]");
+ }
+
+}
+
+class DemoEntity {
+
+ public String myAspectTest(String name) {
+ System.out.println("调用了 myAspectTest 方法;\t name=[" + name + "]");
+ // 当name传入null时,模拟异常
+ name.split("123");
+ return name;
+ }
+}
+
要想AOP起作用,就要加@EnableAspectJAutoProxy
注解,所以AOP的原理可以从@EnableAspectJAutoProxy
入手研究。
它是一个复合注解,启动的时候,给容器中导入了一个AspectJAutoProxyRegistrar
组件:
@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Import(AspectJAutoProxyRegistrar.class)
+public @interface EnableAspectJAutoProxy {}
+
发现该类实现了ImportBeanDefinitionRegistrar
接口,而该接口的作用是给容器中注册bean
的;所以AspectJAutoProxyRegistrar
作用是,添加自定义组件给容器中注册bean
。
class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {
+
+ /**
+ * Register, escalate, and configure the AspectJ auto proxy creator based on the value
+ * of the @{@link EnableAspectJAutoProxy#proxyTargetClass()} attribute on the importing
+ * {@code @Configuration} class.
+ */
+ @Override
+ public void registerBeanDefinitions(
+ AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
+
+ // 注册了 AnnotationAwareAspectJAutoProxyCreator 组件
+ AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);
+
+ AnnotationAttributes enableAspectJAutoProxy =
+ AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
+ // 获取 @EnableAspectJAutoProxy 中的属性,做一些工作
+ if (enableAspectJAutoProxy != null) {
+ if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
+ AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
+ }
+ if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
+ AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
+ }
+ }
+ }
+}
+
AspectJAutoProxyRegistrar
组件何时注册?
通过对下面代码打断点
+AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);
+
可以看到该方法是给容器中注册了一个AnnotationAwareAspectJAutoProxyCreator
组件,实际上是注册AnnotationAwareAspectJAutoProxyCreator
组件。
可以看出@EnableAspectJAutoProxy
注解最主要的作用实际上就是通过@Import
注解把AnnotationAwareAspectJAutoProxyCreator
这个对象注入到spring
容器中。
现在只要把AnnotationAwareAspectJAutoProxyCreator
组件何时注册搞懂,AspectJAutoProxyRegistrar
组件何时注册也就明白了。
AnnotationAwareAspectJAutoProxyCreator
继承关系:
AnnotationAwareAspectJAutoProxyCreator
+ extends AspectJAwareAdvisorAutoProxyCreator
+ extends AbstractAdvisorAutoProxyCreator
+ extends AbstractAutoProxyCreator
+ extends ProxyProcessorSupport implements SmartInstantiationAwareBeanPostProcessor,BeanFactoryAware
+ extends ProxyConfig implements Ordered, BeanClassLoaderAware, AopInfrastructureBean
+
可以看到其中的一个父类AbstractAutoProxyCreator
这个父类实现了SmartInstantiationAwareBeanPostProcessor
接口,该接口是一个后置处理器接口;同样实现了BeanFactoryAware
接口,这意味着,该类可以通过接口中的方法进行自动装配BeanFactory
。
这两个接口的在AOP体系中具体的实现方法:
+1.AbstractAutoProxyCreator
+BeanFactoryAware重写:
+- AbstractAutoProxyCreator.setBeanFactory
+
+SmartInstantiationAwareBeanPostProcessor重写:
+- AbstractAutoProxyCreator.postProcessBeforeInstantiation
+- AbstractAutoProxyCreator.postProcessAfterInitialization
+
+2.AbstractAdvisorAutoProxyCreator
+BeanFactoryAware重写:
+- AbstractAdvisorAutoProxyCreator.setBeanFactory -> initBeanFactory
+
+3. AnnotationAwareAspectJAutoProxyCreator
+BeanFactoryAware重写:
+- AnnotationAwareAspectJAutoProxyCreator.initBeanFactory
+
在上面的任何方法搭上断点即可看到类似下面的方法调用栈:
+AnnotationConfigApplicationContext.AnnotationConfigApplicationContext()
+ ->AbstractApplicationContext.refresh() //刷新容器,给容器初始化bean
+ ->AbstractApplicationContext.finishBeanFactoryInitialization()
+ ->DefaultListableBeanFactory.preInstantiateSingletons()
+ ->AbstractBeanFactory.getBean()
+ ->AbstractBeanFactory.doGetBean()
+ ->DefaultSingletonBeanRegistry.getSingleton()
+ ->AbstractBeanFactory.createBean()
+ ->AbstractAutowireCapableBeanFactory.resolveBeforeInstantiation()
+ ->AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInstantiation()
+ ->AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInstantiation()
+ ->调用AOP相关的后置处理器
+
其中 AbstractApplicationContext.refresh()
方法,调用了 registerBeanPostProcessors()
方法 ,它是用来注册后置处理器,以拦截 bean
的创建。也是在这个方法中完成了对 AnnotationAwareAspectJAutoProxyCreator
的注册。
+在下面详细的展开。
注册完 BeanPostProcessor
后,还调用了方法 finishBeanFactoryInitialization()
,完成 BeanFactory
初始化工作,并创建剩下的单实例 bean
。
@Override
+public void refresh() throws BeansException, IllegalStateException {
+
+ // .....
+
+ // Register bean processors that intercept bean creation.
+ registerBeanPostProcessors(beanFactory);
+
+ // .....
+
+ // Instantiate all remaining (non-lazy-init) singletons.
+ finishBeanFactoryInitialization(beanFactory);
+
+ // .....
+
+}
+
registerBeanPostProcessors
方法中注册了所有的BeanPostProcessor
;注册顺序是:
PriorityOrdered
接口的BeanPostProcessor
;Ordered
接口的 BeanPostProcessor
;BeanPostProcessor
,也就是没有实现优先级接口的 BeanPostProcessor
;Spring
内部 BeanPostProcessor
;由于AnnotationAwareAspectJAutoProxyCreator
类间接实现了Ordered
接口。所以它是在注册实现Ordered
接口的BeanPostProcessor
中完成注册。
注册时会调用AbstractBeanFactory.getBean() -> AbstractBeanFactory.doGetBean()
创建bean
。
doGetBean()
方法作用:
bean
:createBeanInstance()
;bean
中的属性赋值:populateBean()
;bean
:initializeBean()
;初始化bean
时,initializeBean
方法会调用BeanPostProcessor
和BeanFactory
以及Aware
接口的相关方法。这也是BeanPostProcessor
发挥初始化bean
的原理。
protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) {
+
+ // ...
+
+ invokeAwareMethods(beanName, bean); //处理Aware接口的方法回调
+
+ Object wrappedBean = bean;
+ if (mbd == null || !mbd.isSynthetic()) {
+ // 执行后置处理器的postProcessBeforeInitialization方法
+ wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
+ }
+ try {
+ // 执行自定义的初始化方法,也就是在这执行 setBeanFactory方法
+ invokeInitMethods(beanName, wrappedBean, mbd);
+ }
+
+ // ...
+
+ if (mbd == null || !mbd.isSynthetic()) {
+ // 执行后置处理器的postProcessAfterInitialization方法
+ wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
+ }
+ return wrappedBean;
+}
+
+// ...invokeAwareMethods方法简要 ...
+private void invokeAwareMethods(String beanName, Object bean) {
+ if (bean instanceof Aware) {
+ if (bean instanceof BeanFactoryAware) {
+ ((BeanFactoryAware) bean).setBeanFactory(AbstractAutowireCapableBeanFactory.this);
+ }
+ }
+}
+
initializeBean
作用:
Aware
接口的方法回调:invokeAwareMethods()
;postProcessBeforeInitialization()
方法;invokeInitMethods()
;postProcessAfterInitialization()
方法;initializeBean
方法执行成功,AnnotationAwareAspectJAutoProxyCreator
组件才会注册和初始化成功。
除了弄懂AnnotationAwareAspectJAutoProxyCreator
组件何时注册,也需要知道它什么时候被调用,这就涉及到finishBeanFactoryInitialization
方法。
继续看方法的调用:
+AnnotationConfigApplicationContext.AnnotationConfigApplicationContext()
+ ->AbstractApplicationContext.refresh() // 刷新容器,给容器初始化bean
+ ->AbstractApplicationContext.finishBeanFactoryInitialization() // 从这继续
+ ->DefaultListableBeanFactory.preInstantiateSingletons()
+ ->AbstractBeanFactory.getBean()
+ ->AbstractBeanFactory.doGetBean()
+ ->DefaultSingletonBeanRegistry.getSingleton()
+ ->AbstractBeanFactory.createBean()
+ ->AbstractAutowireCapableBeanFactory.resolveBeforeInstantiation()
+ ->AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInstantiation()
+ ->AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInstantiation()
+ ->调用AOP相关的后置处理器
+
finishBeanFactoryInitialization
源码简要:
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
+
+ // ...
+
+ // 注释大意: 实例化所有剩余的(非lazy-init)单例。
+ // Instantiate all remaining (non-lazy-init) singletons.
+ beanFactory.preInstantiateSingletons(); // 断点停在这里
+}
+
finishBeanFactoryInitialization
方法也需要注册Bean
。它会调用 preInstantiateSingletons()
方法遍历获取容器中所有的 Bean
,实例化所有剩余的非懒加载初始化单例 Bean
。
preInstantiateSingletons()
方法源码简要:
@Override
+ public void preInstantiateSingletons() throws BeansException {
+
+ // Iterate over a copy to allow for init methods which in turn register new bean definitions.
+ // While this may not be part of the regular factory bootstrap, it does otherwise work fine.
+ List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);
+
+ // Trigger initialization of all non-lazy singleton beans...
+ for (String beanName : beanNames) {
+ RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
+ // 获取,非抽象、单例、非懒加载Bean
+ if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
+ // 是否 是FactoryBean类型
+ if (isFactoryBean(beanName)) {
+ // ...
+ }
+ else {
+ getBean(beanName); // 断点停在这
+ }
+ }
+ }
+
+ // ...
+ }
+
preInstantiateSingletons()
调用 getBean()
方法,获取Bean
实例,执行过程getBean()->doGetBean()->getSingleton()->createBean()
,又回到了上面注册Bean
的步骤。
这里要注意createBean()
方法中的resolveBeforeInstantiation()
方法,这里可以理解为缓存Bean
,如果被创建了就拿来直接用,如果没有则创建Bean
。
protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
+ throws BeanCreationException {
+
+ // ...
+
+ try {
+ // 注释大意:给 BeanPostProcessors 一个返回代理而不是目标bean实例的机会。
+ // Give BeanPostProcessors a chance to return a proxy instead of the target bean instance.
+ Object bean = resolveBeforeInstantiation(beanName, mbdToUse); // 断点停在这里
+ if (bean != null) {
+ return bean;
+ }
+ }
+
+ // ...
+
+ try {
+ Object beanInstance = doCreateBean(beanName, mbdToUse, args);
+ if (logger.isTraceEnabled()) {
+ logger.trace("Finished creating instance of bean '" + beanName + "'");
+ }
+ return beanInstance;
+ }
+
+ // ...
+}
+
resolveBeforeInstantiation()
、applyBeanPostProcessorsBeforeInstantiation()
方法源码:
protected Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd) {
+ Object bean = null;
+ if (!Boolean.FALSE.equals(mbd.beforeInstantiationResolved)) {
+ // Make sure bean class is actually resolved at this point.
+ if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
+ Class<?> targetType = determineTargetType(beanName, mbd);
+ if (targetType != null) {
+ // 调用 applyBeanPostProcessorsBeforeInstantiation 方法
+ bean = applyBeanPostProcessorsBeforeInstantiation(targetType, beanName); // 断点停在这
+ if (bean != null) {
+ bean = applyBeanPostProcessorsAfterInitialization(bean, beanName);
+ }
+ }
+ }
+ mbd.beforeInstantiationResolved = (bean != null);
+ }
+ return bean;
+}
+
+// ... 上面代码调用的方法 ...
+
+protected Object applyBeanPostProcessorsBeforeInstantiation(Class<?> beanClass, String beanName) {
+ // 遍历所有的 BeanPostProcessor
+ for (BeanPostProcessor bp : getBeanPostProcessors()) {
+
+ // //如果是 InstantiationAwareBeanPostProcessor 类型
+ if (bp instanceof InstantiationAwareBeanPostProcessor) {
+ InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
+
+ // 调用 postProcessBeforeInstantiation 方法
+ Object result = ibp.postProcessBeforeInstantiation(beanClass, beanName); // 断点停在这
+ if (result != null) {
+ return result;
+ }
+ }
+ }
+ return null;
+}
+
到了这里在回过头来看一下AnnotationAwareAspectJAutoProxyCreator
组件实现的SmartInstantiationAwareBeanPostProcessor
接口,继承关系:
SmartInstantiationAwareBeanPostProcessor
+ ->extends InstantiationAwareBeanPostProcessor
+ ->extends BeanPostProcessor
+
到这就跟前边对上了,AOP相关的后置处理器也就是在这被调用的。
+回头在看上面的createBean()
方法,刚才看到的是resolveBeforeInstantiation()
方法的调用栈,所以从层次结构上看AnnotationAwareAspectJAutoProxyCreator
组件的调用是在创建 Bean
实例之前先尝试用后置处理器返回对象的。
Spring 为事务管理提供了丰富的功能支持。Spring 事务管理分为编码式和声明式的两种方式。编程式事务指的是通过编码方式实现事务;声明式事务基于 AOP,将具体业务逻辑与事务处理解耦。声明式事务管理使业务代码逻辑不受污染, 因此在实际使用中声明式事务用的比较多。
+事务隔离级别指的是一个事务对数据的修改与另一个并行的事务的隔离程度,当多个事务同时访问相同数据时,如果没有采取必要的隔离机制,就可能发生以下问题:
+问题 | +描述 | +
---|---|
脏读 | +一个事务读到另一个事务未提交的更新数据。比如银行取钱,事务A开启事务,此时切换到事务B,事务B开启事务–>取走100元,此时切换回事务A,事务A读取的肯定是数据库里面的原始数据,因为事务B取走了100块钱,并没有提交,数据库里面的账务余额肯定还是原始余额,这就是脏读。 | +
幻读 | +是指当事务不是独立执行时发生的一种现象。如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。 同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象 发生了幻觉一样。 | +
不可重复读 | +在一个事务里面的操作中发现了未被操作的数据。 比方说在同一个事务中先后执行两条一模一样的select语句,期间在此次事务中没有执行过任何DDL语句,但先后得到的结果不一致,这就是不可重复读。 | +
Spring支持的隔离级别:
+隔离级别 | +描述 | +
---|---|
DEFAULT | +使用数据库本身使用的隔离级别。ORACLE(读已提交) MySQL(可重复读) | +
READ_UNCOMITTED | +读未提交(脏读)最低的隔离级别,一切皆有可能。 | +
READ_COMMITED | +读已提交,ORACLE默认隔离级别,有幻读以及不可重复读风险。 | +
REPEATABLE_READ | +可重复读,解决不可重复读的隔离级别,但还是有幻读风险。 | +
SERLALIZABLE | +串行化,所有事务请求串行执行,最高的事务隔离级别,不管多少事务,挨个运行完一个事务的所有子事务之后才可以执行另外一个事务里面的所有子事务,这样就解决了脏读、不可重复读和幻读的问题了。 | +
不是事务隔离级别设置得越高越好,事务隔离级别设置得越高,意味着势必要花手段去加锁用以保证事务的正确性,那么效率就要降低,因此实际开发中往往要在效率和并发正确性之间做一个取舍,一般情况下会设置为READ_COMMITED,此时避免了脏读,并发性也还不错,之后再通过一些别的手段去解决不可重复读和幻读的问题就好了。
+Spring建议的是使用DEFAULT,即数据库本身的隔离级别,配置好数据库本身的隔离级别,无论在哪个框架中读写数据都不用操心了。
+事务传播行为指当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。
+Spring定义了七种传播行为:
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+@Documented
+public @interface Transactional {
+ @AliasFor("transactionManager")
+ String value() default "";
+
+ @AliasFor("value")
+ String transactionManager() default "";
+
+ // 事务的传播行为
+ Propagation propagation() default Propagation.REQUIRED;
+
+ // 事务的隔离级别
+ Isolation isolation() default Isolation.DEFAULT;
+
+ // 超时时间
+ // 事务需要在一定时间内提交,如不提交则进行回滚
+ // 默认-1,设置时间以秒单位进行计算
+ int timeout() default -1;
+
+ // 是否只读
+ // 读:查询操作;写:添加、修改、删除操作
+ // 默认值false,表示可以进行读、写操作
+ // 设置true后 只能查询
+ boolean readOnly() default false;
+
+ // 回滚
+ // 设置出现哪些异常进行回滚
+ Class<? extends Throwable>[] rollbackFor() default {};
+
+ String[] rollbackForClassName() default {};
+
+ // 不回滚
+ // 设置出现哪些异常不进行回滚
+ Class<? extends Throwable>[] noRollbackFor() default {};
+
+ String[] noRollbackForClassName() default {};
+}
+
利用Spring Aop实现的。 当一个方法使用了@Transactional注解,在运行时,JVM为该Bean创建一个代理对象,并且在调用该方法的时候进行使用TransactionInterceptor拦截,在方法执行之前会开启一个事务,然后执行方法的逻辑。 方法执行成功,则提交事务。如果执行方法中出现异常,则回滚事务。
++ +
+ + + + + +国内下载网站: http://get.daocloud.io 不推荐下载docker版本太旧了
+官网下载: https://docs.docker.com/get-started/#download-and-install-docker
+或用homebrew
进行下载安装
brew install --cask --appdir=/Applications docker
+
由于网速原因,可以配置一下国内的镜像加速器
+阿里云获取镜像地址: https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors +登陆后,左侧菜单选中镜像加速器就可以看到你的专属地址了; +
+配置docker镜像地址 +
+{
+ "debug": true,
+ "experimental": false,
+ "registry-mirrors": [
+ "https://docker.mirrors.ustc.edu.cn"
+ ]
+}
+
在终端执行docker info
命令
+
出现上图所示,docker镜像加速器配置成功
+参考菜鸟教程
+HelloController.class
+/**
+ * <p>
+ * Hello Controller
+ * </p>
+ */
+@RestController
+@RequestMapping
+public class HelloController {
+ @GetMapping
+ public String hello() {
+ return "Hello,From Docker";
+ }
+}
+
application.yml
+server:
+ port: 8080
+ servlet:
+ context-path: /demo
+
首先创建一个名字叫Dockerfile
的文件,路径任意
+
# 基础镜像
+FROM openjdk:8-jdk-alpine
+
+# 作者信息
+MAINTAINER "whitepure"
+
+# 添加一个存储空间 其效果是在主机 /var/lib/docker 目录下创建了一个临时文件,并链接到容器的/tmp
+VOLUME /tmp
+
+# 暴露8080端口
+EXPOSE 8080
+
+# 添加变量,获取target下的jar包; 如果使用dockerfile-maven-plugin,则会自动替换这里的变量内容
+ARG JAR_FILE=target/demo-docker.jar
+
+# 往容器中添加jar包
+ADD ${JAR_FILE} app.jar
+
+# 启动镜像自动运行程序
+ENTRYPOINT ["java","-Djava.security.egd=file:/dev/urandom","-jar","/app.jar"]
+
在 pom 文件加入 docker 插件
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+ <java.version>1.8</java.version>
+ <dockerfile-version>1.4.9</dockerfile-version>
+ </properties>
+
+ <build>
+ <finalName>demo-docker</finalName>
+ <plugins>
+ <plugin>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-maven-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <groupId>com.spotify</groupId>
+ <artifactId>dockerfile-maven-plugin</artifactId>
+ <version>${dockerfile-version}</version>
+ <configuration>
+ <repository>${project.build.finalName}</repository>
+ <tag>${project.version}</tag>
+ <buildArgs>
+ <JAR_FILE>target/${project.build.finalName}.jar</JAR_FILE>
+ </buildArgs>
+ </configuration>
+<!-- <executions>-->
+<!-- <!–设置运行 mvn package 的时候自动执行docker build–>-->
+<!-- <execution>-->
+<!-- <id>default</id>-->
+<!-- <phase>package</phase>-->
+<!-- <goals>-->
+<!-- <goal>build</goal>-->
+<!-- </goals>-->
+<!-- </execution>-->
+<!-- </executions>-->
+ </plugin>
+ </plugins>
+ </build>
+
使用 maven 打包 +
+前往 Dockerfile 目录,打开命令行执行;
+++执行 docker build 命令,docker就会根据 Dockerfile 里你定义好的命令进行构建新的镜像; +-t代表要构建的镜像的 tag ,.代表当前目录,也就是 Dockerfile 所在的目录
+
docker build -t demo-docker .
+
查看docker镜像列表
+docker images
+
运行该镜像
+++使用镜像 demo-docker ,将容器的 8080 端口映射到主机的 9090 端口
+
docker run -d -p 9090:8080 demo-docker
+
访问http://localhost:9090/demo +
+如果要停止docker镜像,首先获取镜像id,然后在用stop命令停止运行镜像
+++docker ps -a: 显示所有的容器,包括未运行的
+
whitepure@MacBook-Pro demo-docker % docker ps -a
+CONTAINER ID IMAGE ... NAMES
+421fcff1be87 demo-docker ... affectionate_nightingale
+whitepure@MacBook-Pro demo-docker % docker stop 421fcff1be87
+421fcff1be87
+
如果要删除镜像,也需要先获取镜像id,在用rm命令进行删除
+whitepure@MacBook-Pro demo-docker % docker ps -a
+CONTAINER ID IMAGE ... NAMES
+421fcff1be87 demo-docker ... affectionate_nightingale
+whitepure@MacBook-Pro demo-docker % docker rm 421fcff1be87
+421fcff1be87
+
+ +
+ + + + + +要注意导入依赖的版本和安装elasticsearch
的版本与springboot
的兼容问题
本例用elasticsearch-6.5.3
和springboot-2.1.0.RELEASE
版本
docker pull elasticsearch:6.5.3
+
docker run -d -p 9200:9200 -p 9300:9300 --name elasticsearch-6.5.3 elasticsearch:6.5.3
+
docker exec -it elasticsearch-6.5.3 /bin/bash
+
/bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.5.3/elasticsearch-analysis-ik-6.5.3.zip
+
vi ./config/elasticsearch.yml
cluster.name: "docker-cluster"
+network.host: 0.0.0.0
+
+# minimum_master_nodes need to be explicitly set when bound on a public IP
+# set to 1 to allow single node clusters
+# Details: https://github.com/elastic/elasticsearch/pull/17288
+discovery.zen.minimum_master_nodes: 1
+
+# just for elasticsearch-head plugin
+http.cors.enabled: true
+http.cors.allow-origin: "*"
+
exit
+
docker stop elasticsearch-6.5.3
+
docker start elasticsearch-6.5.3
+
<dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
+</dependency>
+
EsConsts 常量池
+public interface EsConsts {
+ /**
+ * 索引名称
+ */
+ String INDEX_NAME = "person";
+
+ /**
+ * 类型名称
+ */
+ String TYPE_NAME = "person";
+}
+
创建Javabean
+++@Document 注解主要声明索引名、类型名、分片数量和备份数量
+@Field 注解主要声明字段对应ES的类型
+
/** 注意(坑点): ES 6以后不允许一个索引下有多个类型,只允许一个索引下有一个类型
+ * @Document:
+ *
+ * String indexName(); //索引库的名称,个人建议以项目的名称命名
+ *
+ * String type() default ""; //类型,建议以实体的名称命名
+ *
+ * short shards() default 5; //默认分区数
+ *
+ * short replicas() default 1; //每个分区默认的备份数
+ *
+ * String refreshInterval() default "1s"; //刷新间隔
+ *
+ * String indexStoreType() default "fs"; //索引文件存储类型
+ */
+@Document(indexName = EsConsts.INDEX_NAME, type = EsConsts.TYPE_NAME, shards = 1, replicas = 0)
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class Person {
+ /**
+ * 主键
+ */
+ @Id
+ private Long id;
+
+ /**
+ * 名字
+ */
+ @Field(type = FieldType.Keyword)
+ private String name;
+
+ /**
+ * 国家
+ */
+ @Field(type = FieldType.Keyword)
+ private String country;
+
+ /**
+ * 年龄
+ */
+ @Field(type = FieldType.Integer)
+ private Integer age;
+
+ /**
+ * 生日
+ */
+ @Field(type = FieldType.Date)
+ private Date birthday;
+
+ /**
+ * 介绍
+ */
+ @Field(type = FieldType.Text, analyzer = "ik_smart")
+ private String remark;
+}
+
PersonRepository
+public interface PersonRepository extends ElasticsearchRepository<Person, Long> {
+
+ /**
+ * 根据年龄区间查询
+ *
+ * @param min 最小值
+ * @param max 最大值
+ * @return 满足条件的用户列表
+ */
+ List<Person> findByAgeBetween(Integer min, Integer max);
+}
+
application.yml配置
+spring:
+ data:
+ elasticsearch:
+ cluster-name: docker-cluster
+ cluster-nodes: localhost:9300
+
@RunWith(SpringRunner.class)
+@SpringBootTest
+public class SpringBootDemoElasticsearchApplicationTests {
+
+ @Test
+ public void contextLoads() {
+ }
+
+}
+
@Slf4j
+public class PersonRepositoryTest extends SpringBootDemoElasticsearchApplicationTests {
+ @Autowired
+ private PersonRepository repo;
+
+ /**
+ * 测试新增
+ */
+ @Test
+ public void save() {
+ Person person = new Person(1L, "刘备", "蜀国", 18, DateUtil.parse("1990-01-02 03:04:05"), "刘备(161年-223年6月10日),即汉昭烈帝(221年-223年在位),又称先主,字玄德,东汉末年幽州涿郡涿县(今河北省涿州市)人,西汉中山靖王刘胜之后,三国时期蜀汉开国皇帝、政治家。\n刘备少年时拜卢植为师;早年颠沛流离,备尝艰辛,投靠过多个诸侯,曾参与镇压黄巾起义。先后率军救援北海相孔融、徐州牧陶谦等。陶谦病亡后,将徐州让与刘备。赤壁之战时,刘备与孙权联盟击败曹操,趁势夺取荆州。而后进取益州。于章武元年(221年)在成都称帝,国号汉,史称蜀或蜀汉。《三国志》评刘备的机权干略不及曹操,但其弘毅宽厚,知人待士,百折不挠,终成帝业。刘备也称自己做事“每与操反,事乃成尔”。\n章武三年(223年),刘备病逝于白帝城,终年六十三岁,谥号昭烈皇帝,庙号烈祖,葬惠陵。后世有众多文艺作品以其为主角,在成都武侯祠有昭烈庙为纪念。");
+ Person save = repo.save(person);
+ log.info("【save】= {}", save);
+ }
+
+ /**
+ * 测试批量新增
+ */
+ @Test
+ public void saveList() {
+ List<Person> personList = Lists.newArrayList();
+ personList.add(new Person(2L, "曹操", "魏国", 20, DateUtil.parse("1988-01-02 03:04:05"), "曹操(155年-220年3月15日),字孟德,一名吉利,小字阿瞒,沛国谯县(今安徽亳州)人。东汉末年杰出的政治家、军事家、文学家、书法家,三国中曹魏政权的奠基人。\n曹操曾担任东汉丞相,后加封魏王,奠定了曹魏立国的基础。去世后谥号为武王。其子曹丕称帝后,追尊为武皇帝,庙号太祖。\n东汉末年,天下大乱,曹操以汉天子的名义征讨四方,对内消灭二袁、吕布、刘表、马超、韩遂等割据势力,对外降服南匈奴、乌桓、鲜卑等,统一了中国北方,并实行一系列政策恢复经济生产和社会秩序,扩大屯田、兴修水利、奖励农桑、重视手工业、安置流亡人口、实行“租调制”,从而使中原社会渐趋稳定、经济出现转机。黄河流域在曹操统治下,政治渐见清明,经济逐步恢复,阶级压迫稍有减轻,社会风气有所好转。曹操在汉朝的名义下所采取的一些措施具有积极作用。\n曹操军事上精通兵法,重贤爱才,为此不惜一切代价将看中的潜能分子收于麾下;生活上善诗歌,抒发自己的政治抱负,并反映汉末人民的苦难生活,气魄雄伟,慷慨悲凉;散文亦清峻整洁,开启并繁荣了建安文学,给后人留下了宝贵的精神财富,鲁迅评价其为“改造文章的祖师”。同时曹操也擅长书法,唐朝张怀瓘在《书断》将曹操的章草评为“妙品”。"));
+ personList.add(new Person(3L, "孙权", "吴国", 19, DateUtil.parse("1989-01-02 03:04:05"), "孙权(182年-252年5月21日),字仲谋,吴郡富春(今浙江杭州富阳区)人。三国时代孙吴的建立者(229年-252年在位)。\n孙权的父亲孙坚和兄长孙策,在东汉末年群雄割据中打下了江东基业。建安五年(200年),孙策遇刺身亡,孙权继之掌事,成为一方诸侯。建安十三年(208年),与刘备建立孙刘联盟,并于赤壁之战中击败曹操,奠定三国鼎立的基础。建安二十四年(219年),孙权派吕蒙成功袭取刘备的荆州,使领土面积大大增加。\n黄武元年(222年),孙权被魏文帝曹丕册封为吴王,建立吴国。同年,在夷陵之战中大败刘备。黄龙元年(229年),在武昌正式称帝,国号吴,不久后迁都建业。孙权称帝后,设置农官,实行屯田,设置郡县,并继续剿抚山越,促进了江南经济的发展。在此基础上,他又多次派人出海。黄龙二年(230年),孙权派卫温、诸葛直抵达夷州。\n孙权晚年在继承人问题上反复无常,引致群下党争,朝局不稳。太元元年(252年)病逝,享年七十一岁,在位二十四年,谥号大皇帝,庙号太祖,葬于蒋陵。\n孙权亦善书,唐代张怀瓘在《书估》中将其书法列为第三等。"));
+ personList.add(new Person(4L, "诸葛亮", "蜀国", 16, DateUtil.parse("1992-01-02 03:04:05"), "诸葛亮(181年-234年10月8日),字孔明,号卧龙,徐州琅琊阳都(今山东临沂市沂南县)人,三国时期蜀国丞相,杰出的政治家、军事家、外交家、文学家、书法家、发明家。\n早年随叔父诸葛玄到荆州,诸葛玄死后,诸葛亮就在襄阳隆中隐居。后刘备三顾茅庐请出诸葛亮,联孙抗曹,于赤壁之战大败曹军。形成三国鼎足之势,又夺占荆州。建安十六年(211年),攻取益州。继又击败曹军,夺得汉中。蜀章武元年(221年),刘备在成都建立蜀汉政权,诸葛亮被任命为丞相,主持朝政。蜀后主刘禅继位,诸葛亮被封为武乡侯,领益州牧。勤勉谨慎,大小政事必亲自处理,赏罚严明;与东吴联盟,改善和西南各族的关系;实行屯田政策,加强战备。前后六次北伐中原,多以粮尽无功。终因积劳成疾,于蜀建兴十二年(234年)病逝于五丈原(今陕西宝鸡岐山境内),享年54岁。刘禅追封其为忠武侯,后世常以武侯尊称诸葛亮。东晋政权因其军事才能特追封他为武兴王。\n诸葛亮散文代表作有《出师表》《诫子书》等。曾发明木牛流马、孔明灯等,并改造连弩,叫做诸葛连弩,可一弩十矢俱发。诸葛亮一生“鞠躬尽瘁、死而后已”,是中国传统文化中忠臣与智者的代表人物。"));
+ Iterable<Person> people = repo.saveAll(personList);
+ log.info("【people】= {}", people);
+ }
+
+ /**
+ * 测试更新
+ */
+ @Test
+ public void update() {
+ repo.findById(1L).ifPresent(person -> {
+ person.setRemark(person.getRemark() + "\n更新更新更新更新更新");
+ Person save = repo.save(person);
+ log.info("【save】= {}", save);
+ });
+ }
+
+ /**
+ * 测试删除
+ */
+ @Test
+ public void delete() {
+ // 主键删除
+ repo.deleteById(1L);
+
+ // 对象删除
+ repo.findById(2L).ifPresent(person -> repo.delete(person));
+
+ // 批量删除
+ repo.deleteAll(repo.findAll());
+ }
+
+ /**
+ * 测试普通查询,按生日倒序
+ */
+ @Test
+ public void select() {
+ repo.findAll(Sort.by(Sort.Direction.DESC, "birthday")).
+ forEach(person -> log.info("{} 生日: {}", person.getName(), DateUtil.formatDateTime(person.getBirthday())));
+ }
+
+ /**
+ * 自定义查询,根据年龄范围查询
+ */
+ @Test
+ public void customSelectRangeOfAge() {
+ repo.findByAgeBetween(18, 19).forEach(person -> log.info("{} 年龄: {}", person.getName(), person.getAge()));
+ }
+
+ /**
+ * 高级查询
+ */
+ @Test
+ public void advanceSelect() {
+ // QueryBuilders 提供了很多静态方法,可以实现大部分查询条件的封装
+ MatchQueryBuilder queryBuilder = QueryBuilders.matchQuery("name", "孙权");
+ log.info("【queryBuilder】= {}", queryBuilder.toString());
+
+ repo.search(queryBuilder).forEach(person -> log.info("【person】= {}", person));
+ }
+
+ /**
+ * 自定义高级查询
+ */
+ @Test
+ public void customAdvanceSelect() {
+ // 构造查询条件
+ NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
+ // 添加基本的分词条件
+ queryBuilder.withQuery(QueryBuilders.matchQuery("remark", "东汉"));
+ // 排序条件
+ queryBuilder.withSort(SortBuilders.fieldSort("age").order(SortOrder.DESC));
+ // 分页条件
+ queryBuilder.withPageable(PageRequest.of(0, 2));
+ Page<Person> people = repo.search(queryBuilder.build());
+ log.info("【people】总条数 = {}", people.getTotalElements());
+ log.info("【people】总页数 = {}", people.getTotalPages());
+ people.forEach(person -> log.info("【person】= {},年龄 = {}", person.getName(), person.getAge()));
+ }
+
+ /**
+ * 测试聚合,测试平均年龄
+ */
+ @Test
+ public void agg() {
+ // 构造查询条件
+ NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
+ // 不查询任何结果
+ queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{""}, null));
+
+ // 平均年龄
+ queryBuilder.addAggregation(AggregationBuilders.avg("avg").field("age"));
+
+ log.info("【queryBuilder】= {}", JSONUtil.toJsonStr(queryBuilder.build()));
+
+ AggregatedPage<Person> people = (AggregatedPage<Person>) repo.search(queryBuilder.build());
+ double avgAge = ((InternalAvg) people.getAggregation("avg")).getValue();
+ log.info("【avgAge】= {}", avgAge);
+ }
+
+ /**
+ * 测试高级聚合查询,每个国家的人有几个,每个国家的平均年龄是多少
+ */
+ @Test
+ public void advanceAgg() {
+ // 构造查询条件
+ NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
+ // 不查询任何结果
+ queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{""}, null));
+
+ // 1. 添加一个新的聚合,聚合类型为terms,聚合名称为country,聚合字段为age
+ queryBuilder.addAggregation(AggregationBuilders.terms("country").field("country")
+ // 2. 在国家聚合桶内进行嵌套聚合,求平均年龄
+ .subAggregation(AggregationBuilders.avg("avg").field("age")));
+
+ log.info("【queryBuilder】= {}", JSONUtil.toJsonStr(queryBuilder.build()));
+
+ // 3. 查询
+ AggregatedPage<Person> people = (AggregatedPage<Person>) repo.search(queryBuilder.build());
+
+ // 4. 解析
+ // 4.1. 从结果中取出名为 country 的那个聚合,因为是利用String类型字段来进行的term聚合,所以结果要强转为StringTerm类型
+ StringTerms country = (StringTerms) people.getAggregation("country");
+ // 4.2. 获取桶
+ List<StringTerms.Bucket> buckets = country.getBuckets();
+ for (StringTerms.Bucket bucket : buckets) {
+ // 4.3. 获取桶中的key,即国家名称 4.4. 获取桶中的文档数量
+ log.info("{} 总共有 {} 人", bucket.getKeyAsString(), bucket.getDocCount());
+ // 4.5. 获取子聚合结果:
+ InternalAvg avg = (InternalAvg) bucket.getAggregations().asMap().get("avg");
+ log.info("平均年龄:{}", avg);
+ }
+ }
+
+}
+
+ +
+ + + + + +Kafka是一种分布式的,基于发布/订阅的消息系统。主要特点如下:
+kafka依赖Java和zookeeper,安装前请先安装Java和zookeeper。
+安装zookeeper
+brew install zookeeper
+
安装kafka
+brew install kafka
+
假定你是按照上边的方法安装的kafka,接下来启动kafka
+因为kafka依赖zk,所以首先要启动zookeeper
+cd /usr/local/Cellar/zookeeper/xxx/bin
xxx是zookeeper的版本zkServer start
,停止命令为 zkServer stop
启动之前必须修改kafka的配置文件,否则后面的启动会失败
+vim /usr/local/etc/kafka/server.properties
listeners=PLAINTEXT://localhost:9092
其中有一行,默认被注释掉了,打开修改即可最后启动kafka
+cd /usr/local/Cellar/kafka/xxx/bin
xxx是kafka的版本kafka-server-start /usr/local/etc/kafka/server.properties &
到此为止kafka启动完成
+kafka-topics --create --zookeeper localhost:2181
--replication-factor 1 --partitions 1 --topic test
kafka-topics --list --zookeeper localhost:2181
该命令会列出所有的主题kafka-console-producer --topic test --broker-list localhost:9092
kafka-console-consumer --bootstrap-server localhost:9092 -topic test
在生产者终端输入信息,在消费者终端就能看的见
+ <dependency>
+ <groupId>org.springframework.kafka</groupId>
+ <artifactId>spring-kafka</artifactId>
+ </dependency>
+
server:
+ port: 8080
+ servlet:
+ context-path: /demo
+spring:
+ kafka:
+ bootstrap-servers: localhost:9092
+ producer:
+ retries: 0
+ batch-size: 16384
+ buffer-memory: 33554432
+ key-serializer: org.apache.kafka.common.serialization.StringSerializer
+ value-serializer: org.apache.kafka.common.serialization.StringSerializer
+ consumer:
+ group-id: spring-boot-demo
+ # 手动提交
+ enable-auto-commit: false
+ auto-offset-reset: latest
+ key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
+ value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
+ properties:
+ session.timeout.ms: 60000
+ listener:
+ log-container-config: false
+ concurrency: 5
+ # 手动提交
+ ack-mode: manual_immediate
+
public interface KafkaConsts {
+ /**
+ * 默认分区大小
+ */
+ Integer DEFAULT_PARTITION_NUM = 3;
+
+ /**
+ * Topic 名称
+ */
+ String TOPIC_TEST = "test";
+}
+
@Configuration
+@EnableConfigurationProperties({KafkaProperties.class})
+@EnableKafka
+@AllArgsConstructor
+public class KafkaConfig {
+ private final KafkaProperties kafkaProperties;
+
+ @Bean
+ public KafkaTemplate<String, String> kafkaTemplate() {
+ return new KafkaTemplate<>(producerFactory());
+ }
+
+ @Bean
+ public ProducerFactory<String, String> producerFactory() {
+ return new DefaultKafkaProducerFactory<>(kafkaProperties.buildProducerProperties());
+ }
+
+ @Bean
+ public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() {
+ ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
+ factory.setConsumerFactory(consumerFactory());
+ factory.setConcurrency(KafkaConsts.DEFAULT_PARTITION_NUM);
+ factory.setBatchListener(true);
+ factory.getContainerProperties().setPollTimeout(3000);
+ return factory;
+ }
+
+ @Bean
+ public ConsumerFactory<String, String> consumerFactory() {
+ return new DefaultKafkaConsumerFactory<>(kafkaProperties.buildConsumerProperties());
+ }
+
+ @Bean("ackContainerFactory")
+ public ConcurrentKafkaListenerContainerFactory<String, String> ackContainerFactory() {
+ ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
+ factory.setConsumerFactory(consumerFactory());
+ factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);
+ factory.setConcurrency(KafkaConsts.DEFAULT_PARTITION_NUM);
+ return factory;
+ }
+
+}
+
@Component
+@Slf4j
+public class MessageHandler {
+
+ @KafkaListener(topics = KafkaConsts.TOPIC_TEST, containerFactory = "ackContainerFactory")
+ public void handleMessage(ConsumerRecord record, Acknowledgment acknowledgment) {
+ try {
+ String message = (String) record.value();
+ log.info("收到消息: {}", message);
+ } catch (Exception e) {
+ log.error(e.getMessage(), e);
+ } finally {
+ // 手动提交 offset
+ acknowledgment.acknowledge();
+ }
+ }
+}
+
测试之前请确保kafka已启动
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class SpringBootDemoMqKafkaApplicationTests {
+ @Autowired
+ private KafkaTemplate<String, String> kafkaTemplate;
+
+ /**
+ * 测试发送消息
+ */
+ @Test
+ public void testSend() {
+ kafkaTemplate.send(KafkaConsts.TOPIC_TEST, "hello,kafka...");
+ }
+
+}
+
+ +
+ + + + + +一键傻瓜试安装即可,官网写的很清楚这里不在赘述 http://nacos.io/zh-cn/docs/v2/quickstart/quick-start.html
+将模式改为单机模式
+ +启动成功
+ +server:
+ port: 8001
+
+config:
+ info: "config info for dev from nacos config center"
+
server:
+ port: 3333
+
+config:
+ info: "config info for test from nacos config center"
+
user:
+ name: zs1112222
+ age: 10
+ address: 测试地址
+
整合nacos配置中心,注册中心,完整项目地址 gitee地址
+<parent>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-parent</artifactId>
+ <version>2.2.2.RELEASE</version>
+</parent>
+
+<dependencies>
+ <dependency>
+ <groupId>org.projectlombok</groupId>
+ <artifactId>lombok</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-web</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-test</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.alibaba.cloud</groupId>
+ <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
+ <version>2.2.2.RELEASE</version>
+ </dependency>
+ <dependency>
+ <groupId>com.alibaba.cloud</groupId>
+ <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
+ <version>2.2.2.RELEASE</version>
+ </dependency>
+</dependencies>
+
@Data
+@Configuration
+@ConfigurationProperties(prefix = "user")
+public class UserConfig {
+
+ private String name;
+
+ private Integer age;
+
+ private String address;
+
+}
+
@RestController
+public class BeanAutoRefreshConfigExample {
+
+ @Autowired
+ private UserConfig userConfig;
+
+ @GetMapping("/user/hello")
+ public String hello(){
+ return userConfig.getName() + userConfig.getAge() + userConfig.getAddress();
+ }
+
+}
+
@RestController
+@RefreshScope
+public class ValueAnnotationExample {
+
+ @Value("${config.info}")
+ private String configInfo;
+
+ @GetMapping("/config/info")
+ public String getConfigInfo() {
+ return configInfo;
+ }
+
+}
+
@SpringBootApplication
+public class DemoApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(DemoApplication.class, args);
+ }
+
+}
+
spring:
+ profiles:
+ # 指定环境 切换环境
+ active: dev
+ application:
+ name: demo
+ cloud:
+ # nacos server dataId
+ # ${spring.application.name)}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
+ nacos:
+ # Nacos服务注册中心
+ discovery:
+ serverAddr: @serverAddr@
+ group: DEMO_GROUP
+ namespace: 25af15f3-ae79-41c3-847d-960adb953185
+ username: @username@
+ password: @password@
+ # Nacos作为配置中心
+ config:
+ server-addr: @serverAddr@
+ file-extension: yaml
+ group: DEMO_GROUP
+ namespace: 25af15f3-ae79-41c3-847d-960adb953185
+ username: @username@
+ password: @password@
+ # 加载多配置
+ extension-configs:
+ - data-id: user.yaml
+ group: DEMO_GROUP
+ refresh: true
+
有时候一些老项目或者一些写法会遇到静态的配置,这时候可以利用Java的反射特性来刷新静态变量.
+大致原理为: 监听nacos配置改动,通过nacos改动确定改动的配置,进而缩小更新范围,通过反射更新变量.
+<!-- https://mvnrepository.com/artifact/com.purgeteam/dynamic-config-spring-boot-starter -->
+<dependency>
+ <groupId>com.purgeteam</groupId>
+ <artifactId>dynamic-config-spring-boot-starter</artifactId>
+ <version>0.1.1.RELEASE</version>
+</dependency>
+<dependency>
+ <groupId>org.projectlombok</groupId>
+ <artifactId>lombok</artifactId>
+</dependency>
+
@NacosRefreshStaticField
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface NacosRefreshStaticField {
+
+ String configPrefix() default "";
+
+}
+
NacosListener
+@Slf4j
+@Component
+@EnableDynamicConfigEvent
+public class NacosListener implements ApplicationListener<ActionConfigEvent> {
+
+ @Autowired
+ private ApplicationContext applicationContext;
+
+ @SneakyThrows
+ @Override
+ public void onApplicationEvent(ActionConfigEvent environment) {
+ Map<String, HashMap> map = environment.getPropertyMap();
+ for (Map.Entry<String, HashMap> entry : map.entrySet()) {
+ String key = entry.getKey();
+ Map changeMap = entry.getValue();
+ String before = String.valueOf(changeMap.get("before"));
+ String after = String.valueOf(changeMap.get("after"));
+ log.info("配置[key:{}]被改变,改变前before:{},改变后after:{}",key,before,after);
+
+ String[] configNameArr = key.split("\\.");
+ String configPrefix = configNameArr[0];
+ String configRealVal = configNameArr[configNameArr.length-1];
+
+ AtomicReference<Class<?>> curClazz = new AtomicReference<>();
+ Map<String, Object> refreshStaticFieldBeanMap = applicationContext.getBeansWithAnnotation(NacosRefreshStaticField.class);
+ for (Map.Entry<String, Object> mapEntry : refreshStaticFieldBeanMap.entrySet()) {
+ String beanName = mapEntry.getKey();
+ if (ObjectUtil.isEmpty(beanName)) {
+ continue;
+ }
+
+ String fullClassName = refreshStaticFieldBeanMap.get(beanName).toString().split("@")[0];
+ Class<?> refreshStaticFieldClass;
+ try {
+ refreshStaticFieldClass = Class.forName(fullClassName);
+ } catch (ClassNotFoundException e) {
+ throw new ClassNotFoundException("监听nacos刷新当前静态类属性,未找到当前类",e);
+ }
+ NacosRefreshStaticField refreshStaticConfig = refreshStaticFieldClass.getAnnotation(NacosRefreshStaticField.class);
+ if (Objects.nonNull(refreshStaticConfig) && refreshStaticConfig.configPrefix().equalsIgnoreCase(configPrefix)) {
+ curClazz.set(refreshStaticFieldClass);
+ }
+ }
+ Class<?> aClass = curClazz.get();
+ if (Objects.isNull(aClass)) {
+ return;
+ }
+
+ // 利用反射动态更新 静态变量
+ Field[] declaredFields = aClass.getDeclaredFields();
+ for (Field declaredField : declaredFields) {
+ if (declaredField.getName().equalsIgnoreCase(configRealVal)) {
+ log.info("刷新当前配置 更新当前类[{}] 静态属性 [{}]",aClass.getSimpleName(),declaredField.getName());
+ declaredField.setAccessible(true);
+ declaredField.set(null,after);
+ }
+ }
+
+ }
+
+ }
+}
+
CommonWebConfig
+@Data
+@Component
+@ConfigurationProperties(prefix = "common")
+@RefreshScope
+public class CommonWebConfig {
+
+ private String apiUrl;
+
+}
+
使用
+@Component
+@NacosRefreshStaticField(configPrefix="common")
+public class ExampleComponent {
+ public static String apiUrl = SpringUtil.getBean(CommonWebConfig.class).getApiUrl();
+}
+
+ +
+ + + + + +redis是开源的一个高性能的 key-value 数据库。
+注意: Java bean 要序列化
+参考菜鸟教程
+<dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-data-redis</artifactId>
+ <version>2.1.3.RELEASE</version>
+</dependency>
+
@Configuration
+public class RedisConfig {
+
+ //用于解决注解操作redis 序列话的问题
+ @Bean(name = "myCacheManager")
+ public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
+
+ RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
+ RedisSerializer<Object> jsonSerializer = new GenericJackson2JsonRedisSerializer();
+ RedisSerializationContext.SerializationPair<Object> pair = RedisSerializationContext.SerializationPair
+ .fromSerializer(jsonSerializer);
+ RedisCacheConfiguration defaultCacheConfig= RedisCacheConfiguration.defaultCacheConfig()
+ .serializeValuesWith(pair);
+ defaultCacheConfig.entryTtl(Duration.ofMinutes(30));
+ return new RedisCacheManager(redisCacheWriter, defaultCacheConfig);
+ }
+
/**
+ * 解决用redisTemplate操作的序列化的问题
+ *
+ * @param factory RedisConnectionFactory
+ * @return redisTemplate
+ */
+ @Bean
+ @ConditionalOnMissingBean(name = "redisTemplate")
+ public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
+ // 配置redisTemplate
+ RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
+ redisTemplate.setConnectionFactory(factory);
+ // key序列化
+ redisTemplate.setKeySerializer(STRING_SERIALIZER);
+ // value序列化
+ redisTemplate.setValueSerializer(JACKSON__SERIALIZER);
+ // Hash key序列化
+ redisTemplate.setHashKeySerializer(STRING_SERIALIZER);
+ // Hash value序列化
+ redisTemplate.setHashValueSerializer(JACKSON__SERIALIZER);
+ redisTemplate.afterPropertiesSet();
+ return redisTemplate;
+ }
+
+}
+
@EnableCaching //允许注解操作缓存
+@SpringBootApplication
+public class RedisExampleApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(RedisExampleApplication.class, args);
+ }
+}
+
#redis 缓存配置
+redis:
+ database: 0
+ host: @ip
+ port: 6379
+ timeout: 8000
+ #如果没有可不写
+ password:
+ jedis:
+ pool:
+ #连接池最大连接数量
+ max-active: 10
+ #连接池最大堵塞时间
+ max-wait: -1
+ #连接池最小空闲连接
+ min-idle: 0
+ #连接池最大空闲连接
+ max-idle: 8
+
@Cacheable
作用将方法的运行结果进行缓存,以后要相同的数据,直接从缓存中获取.
+cacheNames 和 key 都必须填,如果不填 key ,默认的 key 是当前的方法名,更新缓存时会因为方法名不同而更新失败.
属性
+key 也可以动态设置为方法的参数(支持EL)
+@Cacheable(cacheNames = "xxx", key = "#openid")
+public Response detail(@RequestParam("openid") String openid){
+ //to do sthing
+}
+
@Cacheable(cacheNames = "xxx", key = "xxx",keyGenerator = "xxx", CacheManager = "xxx", condition = "xxx", unless = "xxx", sync = "xxx")
+
既调用方法又更新缓存修改了数据库某个数据. +更新缓存,先调用目标方法将目标方法缓存起来,更新时要注意,要和查询时的key相同,否则缓存不会更新,属性和 @Cacheable 相同
+@CachePut(cacheNames = "xxx", key = "xxx",keyGenerator = "xxx", CacheManager = "xxx", condition = "xxx", unless = "xxx", sync = "xxx")
+
该注解作用是删除缓存,需要指定 key
+@CacheEvict(cacheNames = "xxx", key = "xxx", allEntries = "xxx", beforeInvocation="xxx")
+
@CacheConfig
可指定公共key的生成策略
// 公共的cacheNames (value) 可以统一写在类上面 这样就不用每缓存一个就起名字了
+// 公共的CacheManager
+@CacheConfig(cacheNames = "xxx")
+public class RedisExampleController {
+
+ @Caching(cacheable = {@Cacheable}, put = {@CachePut})
+ public Response<Map<String, Object>> test(){
+ //to do
+ }
+}
+
// 查询数据时 先去更新数据 在放到缓存中
+@Caching(cacheable = {@Cacheable}, put = {@CachePut})
+@Caching(cacheable = {@Cacheable}, put = {@CachePut}, evict = {@CacheEvict})
+
@Autowired
+private RedisTemplate redisTemplate;
+
+ @Test
+public void demo(){
+
+ //常见redis类型数据操作 set zset 未列出
+
+ //string 类型数据
+ redisTemplate.opsForValue().set("test","123");
+ redisTemplate.opsForValue().get("test") // 输出结果为123
+
+ //list 数据类型
+ redisTemplate.opsForList().rightPushAll("list",new String[]{"1","2","3"});//从右边插入元素
+ redisTemplate.opsForList().range("list",0,-1);//获取所有元素
+ redisTemplate.opsForList().index("listRight",1);//获取下标为2的元素
+ redisTemplate.opsForList().rightPush("listRiht","1");//从右边插入 也可从左边插
+ redisTemplate.opsForList().leftPop("list");//从左边弹出元素 元素弹出将不存在
+
+ //hash
+ redisTemplate.opsForHash().hasKey("redisHash","111");//判断该hash key 是否存在
+ redisTemplate.opsForHash().put("redisHash","name","111");//存放 hash 数据
+ redisTemplate.opsForHash().keys("redisHash");//获取该key对应的hash值
+ redisTemplate.opsForHash().get("redisHash","age");//给定key 获取 hash 值
+
+
+}
+
+ +
+ + + + + +本文中所涉及的程序均为Java开发,如果您想要直接使用这些工具需要提前配置Java环境。所涉及到的程序均提供完整代码,如果您有兴趣可以尝试运行。
+使用java -jar
命令启动
某些程序功能并不是很完善,但是也可以凑合着用,写这些程序的主要目的是为了方便理解一些常用软件的实现逻辑。
+生成6到20位的随机强密码,可指定密码长度、密码内容。点击下载
+ +/**
+ * 随机生成字符抽象类
+ */
+public abstract class AbstractRandomChar {
+
+ protected static final String LOW_STR = "abcdefghijklmnopqrstuvwxyz";
+ protected static final String UPPER_STR = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ protected static final String SPECIAL_STR = "~!@#$%^&*()_+/|-=[]{};:'<>?.";
+ protected static final String NUM_STR = "0123456789";
+
+
+ /**
+ * 根据字符串获取随机获取字符
+ *
+ * @param str 指定字符串
+ * @return 返回一个随机字符
+ */
+ protected char getRandomChar(String str) {
+ SecureRandom random = new SecureRandom();
+ return str.charAt(random.nextInt(str.length()));
+ }
+
+ /**
+ * 获取随机字符,根据不同的子类不同的实现。现有类型,随机数字、随机大些字母、随机小写字母、随机特殊字符
+ *
+ * @return 随机字符
+ */
+ protected abstract char getRandomChar();
+
+}
+
public class RandomLowChar extends AbstractRandomChar {
+
+ /**
+ * 随机获取小写字符
+ * @return 随机小写字符
+ */
+ @Override
+ public char getRandomChar() {
+ return getRandomChar(LOW_STR);
+ }
+}
+
public class RandomNumChar extends AbstractRandomChar {
+
+ /**
+ * 获取随机获取数字字符
+ * @return 随机获取数字字符
+ */
+ @Override
+ public char getRandomChar() {
+ return getRandomChar(NUM_STR);
+ }
+}
+
public class RandomSpecialChar extends AbstractRandomChar {
+
+ /**
+ * 获取随机获取特殊字符
+ * @return 随机大写字符
+ */
+ @Override
+ public char getRandomChar() {
+ return getRandomChar(SPECIAL_STR);
+ }
+}
+
public class RandomUpperChar extends AbstractRandomChar {
+
+ /**
+ * 随机获取大写字符
+ * @return 随机大写字符
+ */
+ @Override
+ public char getRandomChar() {
+ return getRandomChar(UPPER_STR);
+ }
+}
+
/**
+ * 生成随机密码
+ */
+public class GenRandomPwd {
+
+ /**
+ * 保存 AbstractRandomChar 对象,用于随机数生成
+ */
+ private final List<AbstractRandomChar> randomCharList = new ArrayList<>();
+
+ public GenRandomPwd(boolean genNumCharPwd, boolean genLowCharPwd, boolean genUpperCharPwd, boolean genSpecialCharPwd) {
+
+ if (genNumCharPwd) {
+ randomCharList.add(new RandomNumChar());
+ }
+
+ if (genLowCharPwd) {
+ randomCharList.add(new RandomLowChar());
+ }
+
+ if (genUpperCharPwd) {
+ randomCharList.add(new RandomUpperChar());
+ }
+
+ if (genSpecialCharPwd) {
+ randomCharList.add(new RandomSpecialChar());
+ }
+
+ // 默认都是false的情况下 指定只用数字生成随机数字
+ if (randomCharList.isEmpty()) {
+ randomCharList.add(new RandomNumChar());
+ }
+ }
+
+
+ public String getRandomPwd(int length) {
+ // 密码最大长度
+ int maxPwdLength = 25;
+ // 密码最小长度
+ int minPwdLength = 6;
+ if (length > maxPwdLength || length < minPwdLength) {
+ System.out.printf("密码长度在%d~%d位之间", minPwdLength, maxPwdLength);
+ return "";
+ }
+ // 创建集合将随机生成的字符放入到集合中
+ List<Character> list = new ArrayList<>(length);
+
+ // 产生随机数用于随机调用生成字符的函数
+ int randomListSize = randomCharList.size();
+ for (int i = 0; i < length; i++) {
+ int randomCharNum = new SecureRandom().nextInt(randomListSize);
+ // 随机从randomCharList中获取一个字符,并加入到list中
+ list.add(randomCharList.get(randomCharNum).getRandomChar());
+ }
+
+ // 将选好的list打乱顺序重新排序
+ Collections.shuffle(list);
+
+ // 将char类型转string字符串
+ StringBuilder result = new StringBuilder(list.size());
+ for (Character c : list) {
+ result.append(c);
+ }
+ return result.toString();
+ }
+
+}
+
public class PwdFrame {
+
+ public void init() {
+ Frame f = new Frame("强密码生成器");
+ String pwdNumContent = "123";
+ String pwdLowContent = "abc";
+ String pwdUpperContent = "ABC";
+ String pwdSpecialContent = "!@#";
+
+ // 一些组件
+ TextField tf = new TextField(22);
+ f.setBounds(600, 250, 280, 140);
+ JButton genPwdBtn = new JButton("生成");
+ JLabel lenLabel = new JLabel("长度", JLabel.LEFT);
+ JLabel contentLabel = new JLabel("内容", JLabel.LEFT);
+ JPanel panel1 = new JPanel(new FlowLayout());
+ JPanel panel2 = new JPanel(new FlowLayout());
+
+ // 密码长度单选框
+ ButtonGroup lenGroup = new ButtonGroup();
+ JRadioButton len1 = new JRadioButton("6", true);
+ JRadioButton len2 = new JRadioButton("8");
+ JRadioButton len3 = new JRadioButton("14");
+ JRadioButton len4 = new JRadioButton("16");
+ JRadioButton len5 = new JRadioButton("20");
+ lenGroup.add(len1);
+ lenGroup.add(len2);
+ lenGroup.add(len3);
+ lenGroup.add(len4);
+ lenGroup.add(len5);
+
+ // 密码内容复选框
+ JRadioButton content1 = new JRadioButton(pwdNumContent, true);
+ JRadioButton content2 = new JRadioButton(pwdLowContent);
+ JRadioButton content3 = new JRadioButton(pwdUpperContent);
+ JRadioButton content4 = new JRadioButton(pwdSpecialContent);
+
+ // 将组件添加到 panel
+ panel1.add(lenLabel);
+ panel1.add(len1);
+ panel1.add(len2);
+ panel1.add(len3);
+ panel1.add(len4);
+ panel1.add(len5);
+
+ panel2.add(contentLabel);
+ panel2.add(content1);
+ panel2.add(content2);
+ panel2.add(content3);
+ panel2.add(content4);
+
+
+ // 设置按钮功能
+ genPwdBtn.addActionListener(e -> {
+ // 获取密码长度单选框的值
+ String checkBoxValLength = getCheckBoxVal(panel1);
+
+ // 获取密码内容单选框的值
+ String checkBoxValContent = getCheckBoxVal(panel2);
+ boolean numPwd = checkBoxValContent.contains(pwdNumContent);
+ boolean lowPwd = checkBoxValContent.contains(pwdLowContent);
+ boolean upperPwd = checkBoxValContent.contains(pwdUpperContent);
+ boolean specialPwd = checkBoxValContent.contains(pwdSpecialContent);
+
+ // 生成强密码
+ GenRandomPwd genRandomPwd = new GenRandomPwd(numPwd, lowPwd, upperPwd, specialPwd);
+ tf.setText(genRandomPwd.getRandomPwd(Integer.parseInt(checkBoxValLength)));
+
+ // 获取光标,使光标一直在文本框内
+ tf.requestFocus();
+ });
+
+ f.add(tf);
+ f.add(genPwdBtn);
+ f.add(panel1);
+ f.add(panel2);
+ f.setResizable(false);
+
+ //设置窗体布局模式为流式布局
+ f.setLayout(new FlowLayout());
+
+ //关闭窗口
+ f.addWindowListener(new WindowAdapter() {
+ @Override
+ public void windowClosing(WindowEvent e) {
+ System.exit(0);
+ }
+ });
+ f.setVisible(true);
+ }
+
+
+ /**
+ * 获取单选框、复选框的值
+ *
+ * @param panel panel对象
+ * @return 单选框的值
+ */
+ public String getCheckBoxVal(JPanel panel) {
+ StringBuilder info = new StringBuilder();
+ for (Component c : panel.getComponents()) {
+ if (c instanceof JRadioButton && ((JRadioButton) c).isSelected()) {
+ // 按空格拆分获取复选框的值
+ info.append(((JRadioButton) c).getText());
+ }
+ }
+ return info.toString();
+ }
+
+}
+
public class AutoPwdMainStarter {
+ public static void main(String[] args) {
+ new PwdFrame().init();
+ }
+}
+
截图小工具,支持复制到粘贴板、一次性截取多张图片。点击下载
+ + +/**
+ * 截屏小工具 参考:https://blog.csdn.net/Code__rookie/article/details/103509851 有改动
+ * @author whitepure
+ */
+public class CaptureScreen extends JFrame implements ActionListener {
+ private JButton start, cancel;
+ private JPanel c;
+ private BufferedImage get;
+ private final JTabbedPane jtp;
+ private int index;
+
+ public CaptureScreen() {
+ super("屏幕截取");
+ try {
+ UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+ } catch (Exception exe) {
+ System.out.println("截图异常");
+ exe.printStackTrace();
+ }
+ initWindow();
+ jtp = new JTabbedPane(JTabbedPane.TOP, JTabbedPane.SCROLL_TAB_LAYOUT);
+ }
+
+
+ /**
+ * 初始化窗口
+ */
+ private void initWindow() {
+ start = new JButton("开始截取");
+ cancel = new JButton("退出");
+ start.addActionListener(this);
+ cancel.addActionListener(this);
+
+ JPanel panel = new JPanel();
+ JPanel all = new JPanel();
+
+ c = new JPanel(new BorderLayout());
+ panel.add(start);
+ panel.add(cancel);
+
+ all.add(panel);
+ this.getContentPane().add(c, BorderLayout.CENTER);
+ this.getContentPane().add(all, BorderLayout.SOUTH);
+ setFrameSizeDefault();
+ this.setVisible(true);
+ this.setAlwaysOnTop(true);
+ this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ this.setResizable(false);
+ }
+
+ /**
+ * 设置窗口size 位置
+ */
+ private void setFrameSizeWhenExitsImg() {
+ this.setSize(720, 620);
+ this.setLocation(400,150);
+ }
+
+ /**
+ * 设置窗口size 位置
+ */
+ private void setFrameSizeDefault() {
+ this.setSize(180, 80);
+ this.setLocation(600,300);
+ }
+
+ private void updates() {
+ this.setVisible(true);
+ if (get == null) {
+ return;
+ }
+ // 如果索引是0,则表示一张图片都没有被加入过,则要清除当前的东西,重新把tabpane放进来
+ if (index == 0) {
+ c.removeAll();
+ c.add(jtp, BorderLayout.CENTER);
+ }
+ PicPanel pic = new PicPanel(get);
+ jtp.addTab("图片" + (++index), pic);
+ jtp.setSelectedComponent(pic);
+ SwingUtilities.updateComponentTreeUI(c);
+ }
+
+
+ /**
+ * 点击开始截屏执行
+ */
+ private void doStart() {
+ // 点击截屏后隐藏主界面并 sleep 500ms 彻底隐藏主界面
+ this.setVisible(false);
+ try {
+ Thread.sleep(500);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ Dimension dimension = Toolkit.getDefaultToolkit().getScreenSize();
+ Rectangle rec = new Rectangle(0, 0, dimension.width, dimension.height);
+ BufferedImage bi = null;
+ try {
+ bi = new Robot().createScreenCapture(rec);
+ } catch (AWTException e) {
+ e.printStackTrace();
+ }
+ JFrame jf = new JFrame();
+ // 自定义的截屏时窗口对象,调整大小然后开始截图
+ CutScreen temp = new CutScreen(jf, bi, dimension.width, dimension.height);
+ jf.getContentPane().add(temp, BorderLayout.CENTER);
+
+ jf.setUndecorated(true);
+ jf.setSize(dimension);
+ jf.setVisible(true);
+ jf.setAlwaysOnTop(true);
+
+ // 设置当前窗口大小
+ setFrameSizeWhenExitsImg();
+ }
+
+ /**
+ * 公用的处理保存图片的方法
+ */
+ public void doSave(BufferedImage get) {
+ try {
+ if (get == null) {
+ JOptionPane.showMessageDialog(this
+ , "图片不能为空!!", "错误", JOptionPane.ERROR_MESSAGE);
+ return;
+ }
+ JFileChooser jfc = new JFileChooser(".");
+ jfc.addChoosableFileFilter(new gifFilter());
+ jfc.addChoosableFileFilter(new bmpFilter());
+ jfc.addChoosableFileFilter(new jpgFilter());
+ jfc.addChoosableFileFilter(new pngFilter());
+ int i = jfc.showSaveDialog(this);
+ if (i == JFileChooser.APPROVE_OPTION) {
+ File file = jfc.getSelectedFile();
+ String about = "PNG";
+ String ext = file.toString().toLowerCase();
+ javax.swing.filechooser.FileFilter ff = jfc.getFileFilter();
+ if (ff instanceof jpgFilter) {
+ if (!ext.endsWith(".jpg")) {
+ String ns = ext + ".jpg";
+ file = new File(ns);
+ about = "JPG";
+ }
+ } else if (ff instanceof pngFilter) {
+ if (!ext.endsWith(".png")) {
+ String ns = ext + ".png";
+ file = new File(ns);
+ about = "PNG";
+ }
+ } else if (ff instanceof bmpFilter) {
+ if (!ext.endsWith(".bmp")) {
+ String ns = ext + ".bmp";
+ file = new File(ns);
+ about = "BMP";
+ }
+ } else if (ff instanceof gifFilter) {
+ if (!ext.endsWith(".gif")) {
+ String ns = ext + ".gif";
+ file = new File(ns);
+ about = "GIF";
+ }
+ }
+ if (ImageIO.write(get, about, file)) {
+ JOptionPane.showMessageDialog(this, "保存成功!");
+ } else {
+ JOptionPane.showMessageDialog(this, "保存失败!");
+ }
+ }
+ } catch (Exception exe) {
+ exe.printStackTrace();
+ }
+ }
+
+ /**
+ * 公共的处理把当前的图片加入剪帖板的方法
+ */
+ public void doCopy(final BufferedImage image) {
+ try {
+ if (get == null) {
+ JOptionPane.showMessageDialog(this
+ , "图片不能为空!!", "错误", JOptionPane.ERROR_MESSAGE);
+ return;
+ }
+
+ Transferable trans = new Transferable() {
+ @Override
+ public DataFlavor[] getTransferDataFlavors() {
+ return new DataFlavor[]{DataFlavor.imageFlavor};
+ }
+
+ @Override
+ public boolean isDataFlavorSupported(DataFlavor flavor) {
+ return DataFlavor.imageFlavor.equals(flavor);
+ }
+
+ @Override
+ public Object getTransferData(DataFlavor flavor)
+ throws UnsupportedFlavorException, IOException {
+ if (isDataFlavorSupported(flavor)) {
+ return image;
+ }
+ throw new UnsupportedFlavorException(flavor);
+ }
+ };
+
+ Toolkit.getDefaultToolkit().getSystemClipboard().setContents(trans, null);
+ JOptionPane.showMessageDialog(this, "已复制到系统粘帖板!!");
+ } catch (Exception exe) {
+ exe.printStackTrace();
+ JOptionPane.showMessageDialog(this
+ , "复制到系统粘帖板出错!!", "错误", JOptionPane.ERROR_MESSAGE);
+ }
+ }
+
+ /**
+ * 处理关闭事件
+ * @param c 窗口对象
+ */
+ private void doClose(Component c) {
+ jtp.remove(c);
+ if (jtp.getTabCount() == 0){
+ // 将当前截屏的窗口重置为初始化状态
+ setFrameSizeDefault();
+ }
+ c = null;
+ System.gc();
+ }
+
+ /**
+ * 判断当前按钮发生的事件
+ *
+ * @param ae 事件对象
+ */
+ @Override
+ public void actionPerformed(ActionEvent ae) {
+ Object source = ae.getSource();
+ if (source == start) {
+ doStart();
+ return;
+ }
+ if (source == cancel) {
+ System.exit(0);
+ return;
+ }
+ try {
+ UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
+ SwingUtilities.updateComponentTreeUI(this);
+ } catch (Exception exe) {
+ exe.printStackTrace();
+ }
+ }
+
+
+ /**
+ * 截图后预览截图的panel
+ */
+ private class PicPanel extends JPanel implements ActionListener {
+ JButton save;
+ JButton copy;
+ JButton close;
+ BufferedImage get;
+
+ public PicPanel(BufferedImage get) {
+ super(new BorderLayout());
+ this.get = get;
+ initPanel();
+ }
+
+ /**
+ * 初始化
+ */
+ private void initPanel() {
+ save = new JButton("保存");
+ copy = new JButton("复制到剪帖板");
+ close = new JButton("删除");
+
+ JPanel buttonPanel = new JPanel();
+ buttonPanel.add(copy);
+ buttonPanel.add(save);
+ buttonPanel.add(close);
+ JLabel icon = new JLabel(new ImageIcon(get));
+ this.add(new JScrollPane(icon), BorderLayout.CENTER);
+ this.add(buttonPanel, BorderLayout.SOUTH);
+
+ save.addActionListener(this);
+ copy.addActionListener(this);
+ close.addActionListener(this);
+ }
+
+ /**
+ * 判断当前按钮点击发生的事件
+ *
+ * @param e 事件对象
+ */
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ Object source = e.getSource();
+ if (source == save) {
+ doSave(get);
+ } else if (source == copy) {
+ doCopy(get);
+ } else if (source == close) {
+ get = null;
+ doClose(this);
+ }
+ }
+ }
+
+ // 保存BMP格式的过滤器
+ private class bmpFilter extends javax.swing.filechooser.FileFilter {
+ public bmpFilter() {
+ }
+
+ @Override
+ public boolean accept(File file) {
+ if (file.toString().toLowerCase().endsWith(".bmp") ||
+ file.isDirectory()) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public String getDescription() {
+ return "*.BMP(BMP图像)";
+ }
+ }
+
+ // 保存JPG格式的过滤器
+ private class jpgFilter extends javax.swing.filechooser.FileFilter {
+ public jpgFilter() {
+ }
+
+ @Override
+ public boolean accept(File file) {
+ if (file.toString().toLowerCase().endsWith(".jpg") ||
+ file.isDirectory()) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public String getDescription() {
+ return "*.JPG(JPG图像)";
+ }
+ }
+
+ // 保存GIF格式的过滤器
+ private class gifFilter extends javax.swing.filechooser.FileFilter {
+ public gifFilter() {
+ }
+
+ @Override
+ public boolean accept(File file) {
+ return file.toString().toLowerCase().endsWith(".gif") ||
+ file.isDirectory();
+ }
+
+ @Override
+ public String getDescription() {
+ return "*.GIF(GIF图像)";
+ }
+ }
+
+ // 保存PNG格式的过滤器
+ private class pngFilter extends javax.swing.filechooser.FileFilter {
+ @Override
+ public boolean accept(File file) {
+ return file.toString().toLowerCase().endsWith(".png") ||
+ file.isDirectory();
+ }
+
+ @Override
+ public String getDescription() {
+ return "*.PNG(PNG图像)";
+ }
+ }
+
+ /**
+ * 显示当前的屏幕图像
+ */
+ private class CutScreen extends JPanel implements MouseListener, MouseMotionListener {
+ public static final int START_X = 1;
+ public static final int START_Y = 2;
+ public static final int END_X = 3;
+ public static final int END_Y = 4;
+ private final BufferedImage bi;
+ private final int width;
+ private final int height;
+ private final JFrame jf;
+ //表示一般情况下的鼠标状态(十字线)
+ private final Cursor cs = new Cursor(Cursor.CROSSHAIR_CURSOR);
+ private int startX, startY, endX, endY, tempX, tempY;
+ //表示选中的区域
+ private Rectangle select = new Rectangle(0, 0, 0, 0);
+ // 表示当前的编辑状态
+ private States current = States.DEFAULT;
+ //表示八个编辑点的区域
+ private Rectangle[] rec;
+ //当前被选中的X和Y,只有这两个需要改变
+ private int currentX, currentY;
+ //当前鼠标移的地点
+ private Point p = new Point();
+ //是否显示提示.如果鼠标左键一按,则提示就不再显示了
+ private boolean showTip = true;
+
+ public CutScreen(JFrame jf, BufferedImage bi, int width, int height) {
+ this.jf = jf;
+ this.bi = bi;
+ this.width = width;
+ this.height = height;
+ this.addMouseListener(this);
+ this.addMouseMotionListener(this);
+ initRecs();
+ }
+
+ private void initRecs() {
+ rec = new Rectangle[8];
+ for (int i = 0; i < rec.length; i++) {
+ rec[i] = new Rectangle();
+ }
+ }
+
+ @Override
+ public void paintComponent(Graphics g) {
+ g.drawImage(bi, 0, 0, width, height, this);
+ g.setColor(Color.RED);
+ g.drawLine(startX, startY, endX, startY);
+ g.drawLine(startX, endY, endX, endY);
+ g.drawLine(startX, startY, startX, endY);
+ g.drawLine(endX, startY, endX, endY);
+
+ int x = Math.min(startX, endX);
+ int y = Math.min(startY, endY);
+ select = new Rectangle(x, y, Math.abs(endX - startX), Math.abs(endY - startY));
+ int x1 = (startX + endX) / 2;
+ int y1 = (startY + endY) / 2;
+
+ g.fillRect(x1 - 2, startY - 2, 5, 5);
+ g.fillRect(x1 - 2, endY - 2, 5, 5);
+ g.fillRect(startX - 2, y1 - 2, 5, 5);
+ g.fillRect(endX - 2, y1 - 2, 5, 5);
+ g.fillRect(startX - 2, startY - 2, 5, 5);
+ g.fillRect(startX - 2, endY - 2, 5, 5);
+ g.fillRect(endX - 2, startY - 2, 5, 5);
+ g.fillRect(endX - 2, endY - 2, 5, 5);
+
+ rec[0] = new Rectangle(x - 5, y - 5, 10, 10);
+ rec[1] = new Rectangle(x1 - 5, y - 5, 10, 10);
+ rec[2] = new Rectangle((Math.max(startX, endX)) - 5, y - 5, 10, 10);
+ rec[3] = new Rectangle((Math.max(startX, endX)) - 5, y1 - 5, 10, 10);
+ rec[4] = new Rectangle((Math.max(startX, endX)) - 5, (Math.max(startY, endY)) - 5, 10, 10);
+ rec[5] = new Rectangle(x1 - 5, (Math.max(startY, endY)) - 5, 10, 10);
+ rec[6] = new Rectangle(x - 5, (Math.max(startY, endY)) - 5, 10, 10);
+ rec[7] = new Rectangle(x - 5, y1 - 5, 10, 10);
+
+ if (showTip) {
+ g.setColor(Color.CYAN);
+ g.fillRect(p.x, p.y, 235, 20);
+ g.setColor(Color.RED);
+ g.drawRect(p.x, p.y, 235, 20);
+ g.setColor(Color.BLACK);
+ g.drawString("按住鼠标不放选择截图,双击完成截图", p.x + 10, p.y + 15);
+ }
+ }
+
+ /**
+ * 根据东南西北等八个方向决定选中的要修改的X和Y的座标
+ *
+ * @param state 状态
+ */
+ private void initSelect(States state) {
+ switch (state) {
+ case EAST:
+ currentX = (endX > startX ? END_X : START_X);
+ currentY = 0;
+ break;
+ case WEST:
+ currentX = (endX > startX ? START_X : END_X);
+ currentY = 0;
+ break;
+ case NORTH:
+ currentX = 0;
+ currentY = (startY > endY ? END_Y : START_Y);
+ break;
+ case SOUTH:
+ currentX = 0;
+ currentY = (startY > endY ? START_Y : END_Y);
+ break;
+ case NORTH_EAST:
+ currentY = (startY > endY ? END_Y : START_Y);
+ currentX = (endX > startX ? END_X : START_X);
+ break;
+ case NORTH_WEST:
+ currentY = (startY > endY ? END_Y : START_Y);
+ currentX = (endX > startX ? START_X : END_X);
+ break;
+ case SOUTH_EAST:
+ currentY = (startY > endY ? START_Y : END_Y);
+ currentX = (endX > startX ? END_X : START_X);
+ break;
+ case SOUTH_WEST:
+ currentY = (startY > endY ? START_Y : END_Y);
+ currentX = (endX > startX ? START_X : END_X);
+ break;
+ case DEFAULT:
+ default:
+ currentX = 0;
+ currentY = 0;
+ break;
+ }
+ }
+
+ /**
+ * 鼠标移动对象
+ *
+ * @param me 事件对象
+ */
+ @Override
+ public void mouseMoved(MouseEvent me) {
+ doMouseMoved(me);
+ initSelect(current);
+ if (showTip) {
+ p = me.getPoint();
+ repaint();
+ }
+ }
+
+ /**
+ * 处理鼠标移动,是为了每次都能初始化一下所要选择的区域
+ *
+ * @param me 鼠标事件对象
+ */
+ private void doMouseMoved(MouseEvent me) {
+ if (select.contains(me.getPoint())) {
+ this.setCursor(new Cursor(Cursor.MOVE_CURSOR));
+ current = States.MOVE;
+ } else {
+ States[] st = States.values();
+ for (int i = 0; i < rec.length; i++) {
+ if (rec[i].contains(me.getPoint())) {
+ current = st[i];
+ this.setCursor(st[i].getCursor());
+ return;
+ }
+ }
+ this.setCursor(cs);
+ current = States.DEFAULT;
+ }
+
+ }
+
+ @Override
+ public void mouseExited(MouseEvent me) {
+ }
+
+ @Override
+ public void mouseEntered(MouseEvent me) {
+ }
+
+ /**
+ * 鼠标拖拽对象
+ *
+ * @param me 事件对象
+ */
+ @Override
+ public void mouseDragged(MouseEvent me) {
+ int x = me.getX();
+ int y = me.getY();
+ // 分别处理一系列的(光标)状态(枚举值)
+ if (current == States.MOVE) {
+ startX += (x - tempX);
+ startY += (y - tempY);
+ endX += (x - tempX);
+ endY += (y - tempY);
+ tempX = x;
+ tempY = y;
+ } else if (current == States.EAST || current == States.WEST) {
+ if (currentX == START_X) {
+ startX += (x - tempX);
+ tempX = x;
+ } else {
+ endX += (x - tempX);
+ tempX = x;
+ }
+ } else if (current == States.NORTH || current == States.SOUTH) {
+ if (currentY == START_Y) {
+ startY += (y - tempY);
+ tempY = y;
+ } else {
+ endY += (y - tempY);
+ tempY = y;
+ }
+ } else if (current == States.NORTH_EAST || current == States.SOUTH_EAST || current == States.SOUTH_WEST) {
+ if (currentY == START_Y) {
+ startY += (y - tempY);
+ tempY = y;
+ } else {
+ endY += (y - tempY);
+ tempY = y;
+ }
+ if (currentX == START_X) {
+ startX += (x - tempX);
+ tempX = x;
+ } else {
+ endX += (x - tempX);
+ tempX = x;
+ }
+ } else {
+ startX = tempX;
+ startY = tempY;
+ endX = me.getX();
+ endY = me.getY();
+ }
+ this.repaint();
+ }
+
+
+ /**
+ * 鼠标按压事件
+ *
+ * @param me 事件对象
+ */
+ @Override
+ public void mousePressed(MouseEvent me) {
+ showTip = false;
+ tempX = me.getX();
+ tempY = me.getY();
+ }
+
+
+ /**
+ * 鼠标释放事件
+ *
+ * @param me 事件对象
+ */
+ @Override
+ public void mouseReleased(MouseEvent me) {
+ // 鼠标右键
+ if (me.isPopupTrigger()) {
+ if (current == States.MOVE) {
+ showTip = true;
+ p = me.getPoint();
+ startX = 0;
+ startY = 0;
+ endX = 0;
+ endY = 0;
+ repaint();
+ } else {
+ jf.dispose();
+ updates();
+ }
+ }
+ }
+
+
+ /**
+ * 鼠标点击事件
+ *
+ * @param me 事件
+ */
+ @Override
+ public void mouseClicked(MouseEvent me) {
+ // 双击左键触发事件
+ if (me.getClickCount() == 2 && select.contains(me.getPoint())) {
+ if (select.x + select.width < this.getWidth() && select.y + select.height < this.getHeight()) {
+ get = bi.getSubimage(select.x, select.y, select.width, select.height);
+ jf.dispose();
+ updates();
+ return;
+ }
+
+ int wid = select.width, het = select.height;
+ if (select.x + select.width >= this.getWidth()) {
+ wid = this.getWidth() - select.x;
+ }
+ if (select.y + select.height >= this.getHeight()) {
+ het = this.getHeight() - select.y;
+ }
+ get = bi.getSubimage(select.x, select.y, wid, het);
+ jf.dispose();
+ updates();
+ }
+ }
+
+ }
+
+}
+
/**
+ * 截屏方向状态 东西南北
+ * @author whitepure
+ */
+public enum States {
+
+ NORTH_WEST(new Cursor(Cursor.NW_RESIZE_CURSOR)),
+
+ NORTH(new Cursor(Cursor.N_RESIZE_CURSOR)),
+
+ NORTH_EAST(new Cursor(Cursor.NE_RESIZE_CURSOR)),
+
+ EAST(new Cursor(Cursor.E_RESIZE_CURSOR)),
+
+ SOUTH_EAST(new Cursor(Cursor.SE_RESIZE_CURSOR)),
+
+ SOUTH(new Cursor(Cursor.S_RESIZE_CURSOR)),
+
+ SOUTH_WEST(new Cursor(Cursor.SW_RESIZE_CURSOR)),
+
+ WEST(new Cursor(Cursor.W_RESIZE_CURSOR)),
+
+ MOVE(new Cursor(Cursor.MOVE_CURSOR)),
+
+ DEFAULT(new Cursor(Cursor.DEFAULT_CURSOR));
+
+ private Cursor cs;
+
+ States(Cursor cs) {
+ this.cs = cs;
+ }
+
+ public Cursor getCursor() {
+ return cs;
+ }
+}
+
public class CutScreenMainStarter {
+
+ public static void main(String[] args) {
+ SwingUtilities.invokeLater(CaptureScreen::new);
+ }
+}
+
输入文字,生成二维码。点击下载
+ +需要依赖第三方类库:点击下载
+/**
+ * 创建二维码
+ *
+ */
+public class QRCode {
+
+
+ /**
+ * 生成二维码图像
+ *
+ * @param context 二维码内容
+ * @param size 二维码尺寸
+ * @return 二维码图像
+ */
+ public BufferedImage createPassword(String context, int size) {
+ BufferedImage buffer;
+ Qrcode qrCodeHandler = new Qrcode();
+ qrCodeHandler.setQrcodeErrorCorrect('M');
+ qrCodeHandler.setQrcodeEncodeMode('B');
+ qrCodeHandler.setQrcodeVersion(size);
+ byte[] contextBytes = context.getBytes(StandardCharsets.UTF_8);
+ boolean[][] codeOut = qrCodeHandler.calQrcode(contextBytes);
+
+ // 图像的尺寸 都使用一个值生成一个正方形图案
+ int imgSize = 67 + 12 * (size - 1);
+ buffer = new BufferedImage(imgSize, imgSize, 1);
+ Graphics2D gs = buffer.createGraphics();
+ gs.setColor(Color.BLACK);
+ gs.setBackground(Color.white);
+ gs.clearRect(0, 0, imgSize, imgSize);
+ int pixOff = 2;
+
+ for (int i = 0; i < codeOut.length; ++i) {
+ for (int j = 0; j < codeOut.length; ++j) {
+ if (codeOut[i][j]) {
+ gs.fillRect(j * 3 + pixOff, i * 3 + pixOff, 3, 3);
+ }
+ }
+ }
+ return buffer;
+ }
+
+}
+
public class QRCodeFrame {
+
+ public QRCodeFrame(){
+ init();
+ }
+
+
+ private void init(){
+ // 一些组件
+ JFrame frame = new JFrame("生成二维码");
+ JTextField input = new JTextField(13);
+ JPanel panel = new JPanel();
+ JPanel imgPanel = new JPanel();
+ JButton btn = new JButton("生成");
+ Border lineBorder = BorderFactory.createLineBorder(Color.gray, 1);
+ // 获取光标,使光标一直在文本框内
+ input.requestFocus();
+ input.setBorder(lineBorder);
+
+ panel.add(input);
+ panel.add(btn);
+
+ // 点击按钮执行
+ btn.addActionListener(e -> {
+ String text = input.getText();
+ if (text == null || "".equals(text)){
+ return;
+ }
+ // 刷新imgPanel
+ imgPanel.removeAll();
+ imgPanel.repaint();
+
+ QRCode qrCode = new QRCode();
+ BufferedImage qrCodeImg = qrCode.createPassword(text, 11);
+
+ ImageIcon imageIcon = new ImageIcon(qrCodeImg);
+ JLabel label = new JLabel(imageIcon);
+ imgPanel.add(label, BorderLayout.CENTER);
+ // 刷新imgPanel
+ imgPanel.updateUI();
+ });
+
+ frame.getContentPane().add(panel, BorderLayout.NORTH);
+ frame.getContentPane().add(imgPanel, BorderLayout.SOUTH);
+ frame.setLayout(new FlowLayout(FlowLayout.CENTER));
+ frame.setBounds(600,250,300,280);
+ frame.setVisible(true);
+ frame.setAlwaysOnTop(true);
+ frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ frame.setResizable(false);
+ }
+
+}
+
public class QRCodeMainStarter {
+ public static void main(String[] args) {
+ new QRCodeFrame();
+ }
+}
+
将英文翻译为中文,仅支持单词、短语翻译,程序默认加载自带的英语字典,如果您想要修改字典可在源代码中进行配置或在程序运行时指定字典路径。点击下载
+ +/**
+ * 参考:https://www.jb51.net/article/163917.htm 有改动
+ * <p>
+ * 暂时可翻译单词,需要手动录入词典;暂时不能翻译英文句子需要写一些语法判断,比如动词后面跟名词,倒装句结构等等。
+ *
+ * @author whitepure
+ */
+public class EnglishTranslation {
+
+ private Properties pps;
+
+ public EnglishTranslation(String dictPath) {
+ loadDict(dictPath);
+ }
+
+
+ /**
+ * 加载词典
+ *
+ * @param dictPath 词典路径
+ */
+ public void loadDict(String dictPath) {
+ if (dictPath == null || "".equals(dictPath) || !(new File(dictPath).exists())) {
+ System.out.println("加载词典文件不存在");
+ System.exit(0);
+ return;
+ }
+ pps = new Properties();
+ // 以字符载入时没有乱码,以字节载入时出现了乱码
+ try (FileReader fis = new FileReader(dictPath)) {
+ pps.load(fis);
+ } catch (Exception ex) {
+ ex.printStackTrace(System.out);
+ System.out.println("载入词库时出错");
+ }
+ }
+
+ /**
+ * 翻译将待翻译的去词典中去找然后在返回
+ *
+ * @param data 待翻译的数据
+ * @return 翻译后的数据
+ */
+ public String translation(byte[] data) {
+ String srcTxt = new String(data);
+ String dstTxt = srcTxt;
+ String delim = " ,.!?%$*()\n\t";
+ StringTokenizer st = new StringTokenizer(srcTxt, delim, false);
+ String sub, lowerSub, newSub;
+ while (st.hasMoreTokens()) {
+ // 获取待翻译的单词
+ sub = st.nextToken();
+ // 将单词转化为小写
+ lowerSub = sub.toLowerCase();
+ // 从词典中寻找中文对应的单词
+ newSub = pps.getProperty(lowerSub);
+ if (newSub != null) {
+ // 只替换第一个,即只替换了当前的字符串,否则容易造成翻译错误,如 china 翻译为 ch我na
+ dstTxt = dstTxt.replaceFirst(sub, newSub);
+ }
+ }
+ return dstTxt.replaceAll(" ", "");
+ }
+
+}
+
/**
+ * GUI翻译组件
+ *
+ * @author whitepure
+ */
+public class TranslationFrame {
+
+ private final EnglishTranslation englishTranslation;
+
+ public TranslationFrame(String dictPath){
+ englishTranslation = new EnglishTranslation(dictPath);
+ init();
+ }
+
+
+ /**
+ * 初始化gui组件
+ */
+ private void init(){
+ // 一些组件
+ JFrame frame = new JFrame("英语翻译");
+ JTextField input = new JTextField(20);
+ JPanel panel = new JPanel();
+ JButton btn = new JButton("翻译");
+ JTextArea textArea = new JTextArea(4,27);
+ Border lineBorder = BorderFactory.createLineBorder(Color.gray, 1);
+ textArea.setBorder(lineBorder);
+ // 获取光标,使光标一直在文本框内
+ input.requestFocus();
+ input.setBorder(lineBorder);
+
+ panel.add(input);
+ panel.add(btn);
+ frame.getContentPane().add(panel, BorderLayout.NORTH);
+ frame.add(textArea);
+ // 点击翻译按钮执行
+ btn.addActionListener(e -> {
+ String text = input.getText();
+ if (text == null || "".equals(text)){
+ return;
+ }
+ String translationText = englishTranslation.translation(text.getBytes());
+ System.out.printf("%s 译为 %s \n",text,translationText);
+ textArea.setText(translationText);
+ });
+
+ frame.setLayout(new FlowLayout(FlowLayout.CENTER));
+ frame.setBounds(600,250,355,160);
+ frame.setVisible(true);
+ frame.setAlwaysOnTop(true);
+ frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ frame.setResizable(false);
+ }
+
+}
+
public class TranslationMainStarter {
+ public static void main(String[] args) {
+ Scanner scanner = new Scanner(System.in);
+ String path = null;
+ System.out.println("请输入字典的全路径,例如:/Users/whitepure/IdeaProjects/gadget/out/production/english-translation/io/github/whitepure/dict.txt");
+ if(scanner.hasNext()){
+ String dictPath = scanner.next();
+ System.out.println("键盘输入的内容是:"+ dictPath);
+ path = dictPath;
+ }
+ if (path == null || "".equals(path)){
+ path = new TranslationMainStarter().getDefaultLoadPath();
+ }
+ new TranslationFrame(path);
+ }
+
+ /**
+ * 加载词典文件加载到内存
+ *
+ * @return 词典路径
+ */
+ private String getDefaultLoadPath() {
+ final String dictPath = new File(Objects.requireNonNull(this.getClass().getResource("")).getPath())
+ + System.getProperty("file.separator")
+ + "dict.txt";
+ System.out.println("当前词典加载路径:" + dictPath);
+ return dictPath;
+ }
+}
+
字典文件dict.txt
+i=我
+me=我
+myself=我
+he=他
+him=他
+she=她
+it=它
+they=它们
+us=我们
+our=我们
+we=我们
+her=她的
+his=他的
+them=他们
+you=你
+thee=你
+thou=你
+your=你的
+my=我的
+and=并且
+hello=你好
+world=世界
+love=爱
+china=中国
+chinese=中国人
+
按住alt(macOS用户按住option)鼠标滑动即可获取当前位置颜色。点击下载
+ +public class ExtracterFrame {
+
+
+ public ExtracterFrame() {
+ init();
+ }
+
+
+ /**
+ * 初始化
+ */
+ private void init() {
+ // 窗口组件
+ JFrame frame = new JFrame("提色器");
+ JPanel rgbPanel = new JPanel();
+ JPanel colorPanel = new JPanel();
+
+ // rgb 组件
+ JLabel labelR = new JLabel("R:");
+ JLabel labelG = new JLabel("G:");
+ JLabel labelB = new JLabel("B:");
+ JTextField txtR = new JTextField(3);
+ JTextField txtG = new JTextField(3);
+ JTextField txtB = new JTextField(3);
+
+ rgbPanel.add(labelR);
+ rgbPanel.add(txtR);
+ rgbPanel.add(labelG);
+ rgbPanel.add(txtG);
+ rgbPanel.add(labelB);
+ rgbPanel.add(txtB);
+
+ // 展示颜色组件
+ JLabel labelHex = new JLabel("16进制:");
+ JTextField txtHex = new JTextField(8);
+ JTextField colorTxt = new JTextField(5);
+ colorTxt.setBackground(Color.BLACK);
+
+ colorPanel.add(labelHex);
+ colorPanel.add(txtHex);
+ colorPanel.add(colorTxt);
+
+
+ class ExtractKeyListener implements KeyListener {
+ @Override
+ public void keyTyped(KeyEvent e) {
+ }
+
+ @Override
+ public void keyPressed(KeyEvent e) {
+ }
+
+ @Override
+ public void keyReleased(KeyEvent e) {
+ if (e.getKeyCode() == 18) {
+ Robot robot;
+ try {
+ robot = new Robot();
+ } catch (AWTException exception) {
+ System.out.println("提取颜色失败");
+ exception.printStackTrace();
+ return;
+ }
+ Point point = MouseInfo.getPointerInfo().getLocation();
+ Color pixelColor = robot.getPixelColor(point.x, point.y);
+
+ int red = pixelColor.getRed();
+ int green = pixelColor.getGreen();
+ int blue = pixelColor.getBlue();
+
+ txtR.setText(String.valueOf(red));
+ txtG.setText(String.valueOf(green));
+ txtB.setText(String.valueOf(blue));
+
+ colorTxt.setBackground(pixelColor);
+ txtHex.setText("#" + Integer.toHexString(red) + Integer.toHexString(green) + Integer.toHexString(blue));
+ }
+ }
+ }
+
+
+ // 监听键盘事件
+ ExtractKeyListener extractKeyListener = new ExtractKeyListener();
+ txtR.addKeyListener(extractKeyListener);
+ txtG.addKeyListener(extractKeyListener);
+ txtB.addKeyListener(extractKeyListener);
+ colorTxt.addKeyListener(extractKeyListener);
+
+
+ // 添加panel
+ frame.getContentPane().add(rgbPanel, BorderLayout.NORTH);
+ frame.getContentPane().add(colorPanel, BorderLayout.SOUTH);
+
+ frame.setLayout(new FlowLayout(FlowLayout.CENTER));
+ frame.setBounds(600, 250, 250, 120);
+ frame.setVisible(true);
+ frame.setAlwaysOnTop(true);
+ frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ frame.setResizable(false);
+ }
+
+}
+
public class ExtractColorMainStarter {
+ public static void main(String[] args) {
+ new ExtracterFrame();
+ }
+}
+
加载图片,输入水印内容,在加载图片的右下会生成红色水印。点击下载
+ +public class ImgWatermarking {
+
+
+ /**
+ * 给图片添加水印文字、可设置水印文字的旋转角度
+ *
+ * @param logoText 水印文本
+ * @param srcImgPath 原图片路径
+ * @param degree 旋转角度,如果不旋转设置为 null
+ */
+ public BufferedImage createImgWatermarking(
+ String logoText,
+ String srcImgPath,
+ Integer degree,
+ Color color,
+ Font font,
+ float alpha
+ ) {
+ alpha = alpha == 0 ? 0.5f : alpha;
+ font = font == null ? new Font("微软雅黑", Font.PLAIN, 35) : font;
+ color = color == null ? Color.red : color;
+
+ System.out.println("生成图片=》图片路径:" + srcImgPath + " 水印内容=》" + logoText);
+
+ Image srcImg;
+ try {
+ srcImg = ImageIO.read(new File(srcImgPath));
+ } catch (IOException e) {
+ System.out.println("读取文件失败");
+ e.printStackTrace();
+ return null;
+ }
+ int width = srcImg.getWidth(null);
+ int height = srcImg.getHeight(null);
+ BufferedImage buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+
+ // 拿到画笔对象
+ Graphics2D g = buffImg.createGraphics();
+
+ // 设置水印
+ g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
+ g.drawImage(srcImg.getScaledInstance(width, height, Image.SCALE_SMOOTH), 0, 0, null);
+
+ // 设置水印旋转
+ if (null != degree) {
+ g.rotate(Math.toRadians(degree), (buffImg.getWidth() >> 1), (buffImg.getHeight() >> 1));
+ }
+
+ // 设置水印
+ g.setColor(color);
+ g.setFont(font);
+ g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha));
+ int x = width - logoText.length() * font.getSize();
+ int y = height - font.getSize() * 2;
+ g.drawString(logoText, Math.max(x, 0), Math.max(y, 0));
+ g.dispose();
+ return buffImg;
+ }
+
+}
+
/**
+ * @author whitepure
+ */
+public class WatermarkingFrame {
+
+ public WatermarkingFrame() {
+ init();
+ }
+
+ /**
+ * 初始化
+ */
+ private void init() {
+ // 一些组件
+ JFrame frame = new JFrame("生成水印");
+ JTextField input = new JTextField(13);
+ JPanel panel = new JPanel();
+ JPanel imgPanel = new JPanel();
+ JButton createBtn = new JButton("生成");
+ JButton loadImgBtn = new JButton("加载图片");
+ JButton saveAs = new JButton("另存为");
+ Border lineBorder = BorderFactory.createLineBorder(Color.gray, 1);
+ // 获取光标,使光标一直在文本框内
+ input.requestFocus();
+ input.setBorder(lineBorder);
+
+ panel.add(input);
+ panel.add(saveAs);
+ panel.add(createBtn);
+ panel.add(loadImgBtn);
+
+ // 图片路径
+ String[] imgPath = new String[1];
+ // 用于另存为的图片
+ final BufferedImage[] waterMarkingImg = new BufferedImage[1];
+ // 屏幕的尺寸
+ Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
+
+ // 点击按钮加载图片
+ loadImgBtn.addActionListener(e -> {
+ JFileChooser jfc = new JFileChooser(FileSystemView.getFileSystemView().getHomeDirectory());
+ jfc.setDialogTitle("选择图片");
+ jfc.setAcceptAllFileFilterUsed(false);
+ FileNameExtensionFilter filter = new FileNameExtensionFilter("PNG and JPG images", "png", "jpg");
+ jfc.addChoosableFileFilter(filter);
+
+ int returnValue = jfc.showOpenDialog(null);
+ // 获取图片路径
+ if (returnValue == JFileChooser.APPROVE_OPTION) {
+ String path = jfc.getSelectedFile().getPath();
+ imgPath[0] = path;
+ }
+
+ String path = jfc.getSelectedFile().getPath();
+ File sourceImage = new File(path);
+ BufferedImage image;
+ try {
+ image = ImageIO.read(sourceImage);
+ } catch (IOException ex) {
+ System.out.println("图片加载失败");
+ ex.printStackTrace();
+ JOptionPane.showMessageDialog(panel, "图片加载失败," + ex.getMessage(), "提示", JOptionPane.PLAIN_MESSAGE);
+ return;
+ }
+ // 刷新imgPanel
+ imgPanel.removeAll();
+ imgPanel.repaint();
+
+ // 将图片放入到数组中,用于另存为保存该图片
+ waterMarkingImg[0] = image;
+ int width = image.getWidth(null);
+ int height = image.getHeight(null);
+
+ ImageIcon imageIcon = new ImageIcon(image);
+ imageIcon.setImage(imageIcon.getImage().getScaledInstance(width >> 1, height >> 1, Image.SCALE_DEFAULT));
+ JLabel label = new JLabel(imageIcon);
+
+ imgPanel.add(label, BorderLayout.CENTER);
+ // 刷新imgPanel
+ imgPanel.updateUI();
+ frame.setBounds(((screenSize.width - (width >> 1)) >> 1) , ((screenSize.height - (height >> 1)) >> 2), Math.max((width >> 1), 450), (height >> 1) + 90);
+ });
+
+ // 点击按钮生成水印
+ createBtn.addActionListener(e -> {
+ String text = input.getText();
+ if (text == null || "".equals(text)) {
+ System.out.println("水印内容为空");
+ JOptionPane.showMessageDialog(panel, "水印内容为空", "提示", JOptionPane.WARNING_MESSAGE);
+ return;
+ }
+ if (imgPath[0] == null) {
+ JOptionPane.showMessageDialog(panel, "未加载图片", "提示", JOptionPane.WARNING_MESSAGE);
+ System.out.println("未加载图片");
+ return;
+ }
+ BufferedImage imgWatermarking = new ImgWatermarking().createImgWatermarking(text, imgPath[0], null, null, null, 0);
+
+ // 刷新imgPanel
+ imgPanel.removeAll();
+ imgPanel.repaint();
+
+ // 保存图片用于另存为
+ waterMarkingImg[0] = imgWatermarking;
+ int width = imgWatermarking.getWidth(null);
+ int height = imgWatermarking.getHeight(null);
+
+ ImageIcon imageIcon = new ImageIcon(imgWatermarking);
+ imageIcon.setImage(imageIcon.getImage().getScaledInstance(width >> 1, height >> 1, Image.SCALE_DEFAULT));
+ JLabel label = new JLabel(imageIcon);
+
+ imgPanel.add(label, BorderLayout.CENTER);
+ // 刷新imgPanel
+ imgPanel.updateUI();
+ });
+
+ // 点击另存为
+ saveAs.addActionListener(e -> {
+ BufferedImage image = waterMarkingImg[0];
+ if (image == null) {
+ JOptionPane.showMessageDialog(panel, "未加载图片", "提示", JOptionPane.WARNING_MESSAGE);
+ return;
+ }
+
+ FileDialog fd = new FileDialog(frame, "另存为", FileDialog.SAVE);
+ fd.setVisible(true);
+ // 获取路径
+ String directory = fd.getDirectory();
+ // 获取文件名
+ String fileName = fd.getFile();
+ try (OutputStream out = new FileOutputStream(directory + fileName);) {
+ // 将图片输出到文件
+ ImageIO.write(image, "jpg", out);
+ } catch (IOException ex) {
+ System.out.println("另存为图片失败");
+ ex.printStackTrace();
+ JOptionPane.showMessageDialog(panel, "另存为图片失败," + ex.getMessage(), "提示", JOptionPane.PLAIN_MESSAGE);
+ }
+ });
+
+ frame.getContentPane().add(panel, BorderLayout.NORTH);
+ frame.getContentPane().add(imgPanel, BorderLayout.SOUTH);
+ frame.setLayout(new FlowLayout(FlowLayout.CENTER));
+ frame.setBounds(screenSize.width >> 1, screenSize.height >> 2, 450, 80);
+ frame.setVisible(true);
+ frame.setAlwaysOnTop(true);
+ frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ frame.setResizable(false);
+ }
+
+}
+
public class ImgWatermarkingMainStarter {
+ public static void main(String[] args) {
+ new WatermarkingFrame();
+ }
+}
+
采用huffman算法对文件进行无损压缩,目前仅支持压缩单个文件,压缩好的文件以.huf
后缀结尾,会与被压缩文件放在相同目录下。点击下载
public abstract class HuffmanCode {
+
+
+ /**
+ * 存放 huffman 编码表
+ */
+ private final Map<Byte, String> huffmanCodes = new HashMap<>();
+
+
+ public Map<Byte, String> getHuffmanCodesTab() {
+ return huffmanCodes;
+ }
+
+ /**
+ * 将文件解码或将文件压缩
+ *
+ * @param zipFile 原文件
+ * @param dstFile 目标文件
+ */
+ public abstract void zipOrUnZip(String zipFile, String dstFile);
+
+ /**
+ * 生成 huffman 编码 压缩
+ *
+ * @param bytes 将传入的文件转成字节传入
+ * @return 将传入字节转换为huffman压缩后的字节数组
+ */
+ public byte[] encode(byte[] bytes) {
+ List<Node> nodes = buildHuffmanNodes(bytes);
+ Node huffmanTreeRoot = buildHuffmanTree(nodes);
+ Map<Byte, String> huffmanCodes = buildHuffmanCodeTab(huffmanTreeRoot);
+ return zip(bytes, huffmanCodes);
+ }
+
+ /**
+ * 将 huffman编码解码,解压缩
+ *
+ * @param huffmanCodes huffman编码表
+ * @param huffmanBytes 待解码的huffman编码(byte数组类型)
+ * @return 解码后的
+ */
+ public byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
+ StringBuilder stringBuilder = new StringBuilder();
+
+ // 将byte数组转成二进制的字符串
+ for (int i = 0; i < huffmanBytes.length - 1; i++) {
+ byte b = huffmanBytes[i];
+ String strToAppend = byteToBitString(b);
+ // 判断是不是最后一个字节
+ boolean isLastByte = (i == huffmanBytes.length - 2);
+ if (isLastByte) {
+ // 得到最后一个字节的有效位数
+ byte validBits = huffmanBytes[huffmanBytes.length - 1];
+ strToAppend = strToAppend.substring(0, validBits);
+ }
+ stringBuilder.append(strToAppend);
+ }
+
+ // 将huffman编码key,val反转
+ Map<String, Byte> map = new HashMap<>();
+ huffmanCodes.forEach((key, value) -> map.put(value, key));
+
+ // 创建要给集合,存放byte
+ List<Byte> list = new ArrayList<>();
+ for (int i = 0; i < stringBuilder.length(); ) {
+ int count = 1;
+ boolean flag = true;
+ Byte b = null;
+
+ // 根据huffman编码表来匹配Huffman编码
+ while (flag) {
+ String key = stringBuilder.substring(i, i + count);
+ b = map.get(key);
+ if (b == null) {
+ // 没有匹配到
+ count++;
+ } else {
+ // 匹配到
+ flag = false;
+ }
+ }
+ list.add(b);
+ i += count;
+ }
+ byte[] b = new byte[list.size()];
+ IntStream.range(0, b.length).forEach(i -> b[i] = list.get(i));
+ return b;
+ }
+
+ /**
+ * 计算传入字节数组中每个字符出现的次数
+ * 在Node对象中添加对应的值
+ *
+ * @param bytes 传入字节数组
+ * @return list
+ */
+ private List<Node> buildHuffmanNodes(byte[] bytes) {
+ ArrayList<Node> nodes = new ArrayList<>();
+ // 利用map记录集合中元素出现的次数
+ Map<Byte, Integer> counts = new HashMap<>();
+ for (byte b : bytes) {
+ counts.merge(b, 1, Integer::sum);
+ }
+ // 把每一个键值对转成一个Node 对象,并加入到nodes集合
+ counts.forEach((key, value) -> nodes.add(new Node(key, value)));
+ return nodes;
+ }
+
+ /**
+ * 构建Huffman树
+ *
+ * @param nodes 传入统计好的 每个字符出现的次数,即{@method buildHuffmanNodes}方法执行完毕
+ * @return 构建好Huffman的根结点
+ */
+ private Node buildHuffmanTree(List<Node> nodes) {
+ while (nodes.size() > 1) {
+ // 排序, 从小到大
+ Collections.sort(nodes);
+ // 取出第一颗最小的二叉树
+ Node leftNode = nodes.get(0);
+ // 取出第二颗最小的二叉树
+ Node rightNode = nodes.get(1);
+ // 创建一颗新的二叉树,它的根节点 没有data, 只有权值
+ Node parent = new Node(null, leftNode.weight + rightNode.weight);
+ parent.left = leftNode;
+ parent.right = rightNode;
+
+ // 将已经处理的两颗二叉树从nodes删除
+ nodes.remove(leftNode);
+ nodes.remove(rightNode);
+ // 将新的二叉树,加入到nodes
+ nodes.add(parent);
+ }
+ // nodes 最后的结点,就是赫夫曼树的根结点
+ return nodes.get(0);
+ }
+
+ /**
+ * 构建huffman编码表
+ *
+ * @param root huffman root结点
+ * @return 返回一个Huffman编码表
+ */
+ private Map<Byte, String> buildHuffmanCodeTab(Node root) {
+ if (root == null) {
+ return null;
+ }
+ // 处理root的左子树
+ buildHuffmanCodeTab(root.left, "0", new StringBuilder());
+ // 处理root的右子树
+ buildHuffmanCodeTab(root.right, "1", new StringBuilder());
+ return huffmanCodes;
+ }
+
+ private void buildHuffmanCodeTab(Node node, String code, StringBuilder stringBuilder) {
+ StringBuilder curNodeCode = new StringBuilder(stringBuilder);
+ curNodeCode.append(code);
+ if (node == null) {
+ return;
+ }
+ // 判断当前node 是叶子结点还是非叶子结点,如果 node.data == null 为非叶子结点
+ if (node.data == null) {
+ // 向左递归
+ buildHuffmanCodeTab(node.left, "0", curNodeCode);
+ // 向右递归
+ buildHuffmanCodeTab(node.right, "1", curNodeCode);
+ } else {
+ // 表示找到某个叶子结点的最后
+ huffmanCodes.put(node.data, curNodeCode.toString());
+ }
+ }
+
+ // 压缩传入字节(将传入字符串转成字节类型)将待压缩字节转换为字节数组
+ private byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
+ // 利用 huffmanCodes 将 bytes 转成 赫夫曼编码对应的字符串
+ StringBuilder stringBuilder = new StringBuilder();
+ // 遍历bytes 数组
+ for (byte b : bytes) {
+ stringBuilder.append(huffmanCodes.get(b));
+ }
+
+ // 统计返回 byte[] huffmanCodeBytes 长度
+ int len;
+ // 等同于 int len = (stringBuilder.length() + 7) / 8;
+ byte countToEight = (byte) (stringBuilder.length() & 7);
+ if (countToEight == 0) {
+ len = stringBuilder.length() >> 3;
+ } else {
+ len = (stringBuilder.length() >> 3) + 1;
+ // 后面补零
+ for (int i = countToEight; i < 8; i++) {
+ stringBuilder.append("0");
+ }
+ }
+
+ // 创建 存储压缩后的 byte数组,huffmanCodeBytes[len]记录赫夫曼编码最后一个字节的有效位数
+ byte[] huffmanCodeBytes = new byte[len + 1];
+ huffmanCodeBytes[len] = countToEight;
+ int index = 0;
+ // 因为是每8位对应一个byte,所以步长 +8
+ for (int i = 0; i < stringBuilder.length(); i += 8) {
+ String strByte;
+ strByte = stringBuilder.substring(i, i + 8);
+ // 将strByte 转成一个byte,放入到 huffmanCodeBytes
+ huffmanCodeBytes[index] = (byte) Integer.parseInt(strByte, 2);
+ index++;
+ }
+ return huffmanCodeBytes;
+ }
+
+ /**
+ * 将 byte 转换为对应的字符串 解码时要用
+ *
+ * @param b 待处理的字节
+ * @return 将字节转换成二进制字符串
+ */
+ private String byteToBitString(byte b) {
+ int temp = b;
+ // 如果是正数我们需要将高位补零
+ temp |= 0x100;
+ // 转换为二进制字符串,正数:高位补 0 即可,然后截取低八位即可;负数直接截取低八位即可
+ // 负数在计算机内存储的是补码,补码转原码:先 -1 ,再取反
+ String binaryStr = Integer.toBinaryString(temp);
+ return binaryStr.substring(binaryStr.length() - 8);
+ }
+
+
+ /**
+ * huffman 树的结点
+ */
+ public static class Node implements Comparable<Node> {
+ Byte data;
+ int weight;
+ Node left;
+ Node right;
+
+ public Node(Byte data, int weight) {
+ this.data = data;
+ this.weight = weight;
+ }
+
+ @Override
+ public int compareTo(Node o) {
+ // 从小到大排序
+ return this.weight - o.weight;
+ }
+
+ @Override
+ public String toString() {
+ return "Node [data = " + data + " weight=" + weight + "]";
+ }
+ }
+}
+
public class HuffmanUnZip extends HuffmanCode {
+
+
+ @Override
+ public void zipOrUnZip(String zipFile, String dstFile) {
+ // 定义文件输入流
+ InputStream is = null;
+ // 定义一个对象输入流
+ ObjectInputStream ois = null;
+ // 定义文件的输出流
+ OutputStream os = null;
+ try {
+ // 创建文件输入流
+ is = new FileInputStream(zipFile);
+ // 创建一个和 is关联的对象输入流
+ ois = new ObjectInputStream(is);
+ // 读取byte数组 huffmanBytes
+ byte[] huffmanBytes = (byte[]) ois.readObject();
+ // 读取赫夫曼编码表
+ Map<Byte, String> huffmanCodes = (Map<Byte, String>) ois.readObject();
+ // 解码
+ byte[] bytes = decode(huffmanCodes, huffmanBytes);
+ // 将bytes 数组写入到目标文件
+ os = new FileOutputStream(dstFile);
+ // 写数据到 dstFile 文件
+ os.write(bytes);
+ } catch (Exception e) {
+ // TODO: handle exception
+ System.out.println(e.getMessage());
+ } finally {
+ try {
+ if (os != null) {
+ os.close();
+ }
+ if (ois != null) {
+ ois.close();
+ }
+ if (is != null) {
+ is.close();
+ }
+ } catch (Exception e2) {
+ // TODO: handle exception
+ System.out.println(e2.getMessage());
+ }
+
+ }
+ }
+}
+
public class HuffmanZip extends HuffmanCode{
+
+ @Override
+ public void zipOrUnZip(String srcFile, String dstFile){
+ // 创建输出流
+ OutputStream os = null;
+ ObjectOutputStream oos = null;
+ // 创建文件的输入流
+ FileInputStream is = null;
+ try {
+ // 创建文件的输入流
+ is = new FileInputStream(srcFile);
+ // 创建一个和源文件大小一样的byte[]
+ byte[] b = new byte[is.available()];
+ // 读取文件
+ is.read(b);
+ // 直接对源文件压缩
+ byte[] huffmanBytes = encode(b);
+ // 创建文件的输出流, 存放压缩文件
+ os = new FileOutputStream(dstFile);
+ // 创建一个和文件输出流关联的ObjectOutputStream
+ oos = new ObjectOutputStream(os);
+ // 把 赫夫曼编码后的字节数组写入压缩文件
+ oos.writeObject(huffmanBytes);
+ // 这里我们以对象流的方式写入 赫夫曼编码,是为了以后我们恢复源文件时使用
+ // 注意一定要把赫夫曼编码 写入压缩文件
+ oos.writeObject(getHuffmanCodesTab());
+
+ } catch (Exception e) {
+ throw new RuntimeException("使用huffman编码压缩异常!", e);
+ } finally {
+ try {
+ if (is != null) {
+ is.close();
+ }
+ if (oos != null) {
+ oos.close();
+ }
+ if (os != null) {
+ os.close();
+ }
+ } catch (Exception e) {
+ // TODO: handle exception
+ System.out.println(e.getMessage());
+ }
+ }
+ }
+
+}
+
public class HuffmanFrame {
+
+ /**
+ * 当前屏幕的宽
+ */
+ private final int WINDOW_WIDTH;
+
+ /**
+ * 当前屏幕的高
+ */
+ private final int WINDOW_HEIGHT;
+
+ public HuffmanFrame() {
+ Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
+ WINDOW_WIDTH = screenSize.width;
+ WINDOW_HEIGHT = screenSize.height;
+ }
+
+
+ /**
+ * 初始化huffman窗口
+ *
+ * @param width 宽
+ * @param height 高
+ */
+ public void init(int width, int height) {
+ Frame frame = new Frame();
+ frame.setTitle("huffman压缩");
+ // 设置窗口可见
+ frame.setVisible(true);
+ // 禁止调整窗口大小
+ frame.setResizable(false);
+ // 设置窗口大小
+ frame.setSize(width, height);
+ // 设置窗口出现在屏幕的位置
+ frame.setLocation((WINDOW_WIDTH - width) >> 1, (WINDOW_HEIGHT - height) >> 2);
+ FlowLayout flowLayout = new FlowLayout();
+ //设置对齐方式
+ flowLayout.setAlignment(FlowLayout.CENTER);
+ // 设置流式布局
+ frame.setLayout(flowLayout);
+ // 设置按钮
+ Button zipBtn = new Button("压缩");
+ Button unZipBtn = new Button("解压缩");
+ frame.add(zipBtn, BorderLayout.CENTER);
+ frame.add(unZipBtn, BorderLayout.CENTER);
+ listenerBtnOfFile(zipBtn, new HuffmanZip(),frame);
+ listenerBtnOfFile(unZipBtn, new HuffmanUnZip(),frame);
+ //关闭窗口
+ frame.addWindowListener(new WindowAdapter() {
+ @Override
+ public void windowClosing(WindowEvent e) {
+ System.exit(0);
+ }
+ });
+ }
+
+
+ /**
+ * 监听点击按钮触发事件,解压缩或压缩
+ *
+ * @param btn 按钮对象
+ * @param huffmanCode 压缩或解压缩对象
+ */
+ private void listenerBtnOfFile(Button btn, HuffmanCode huffmanCode,Frame frame) {
+ btn.addActionListener(e -> {
+ FileDialog openDialog = new FileDialog(frame, "打开文件", FileDialog.LOAD);
+ openDialog.setVisible(true);
+
+ String dirName = openDialog.getDirectory();
+ String fileName = openDialog.getFile();
+ String src = dirName + fileName;
+ if (dirName == null || fileName == null) {
+ return;
+ }
+
+ // 压缩或解压缩后 弹框提示
+ JDialog jDialog = new JDialog();
+ jDialog.setSize(200, 200);
+ jDialog.setLocation((WINDOW_WIDTH - 200) / 2, (WINDOW_HEIGHT - 200) / 3);
+ jDialog.setVisible(true);
+ jDialog.setTitle("提示");
+ jDialog.setLayout(new FlowLayout());
+ JLabel jLabel = new JLabel();
+ String suffix = ".huf";
+ String dist;
+
+ // 如果文件包含了后缀则需要解压缩文件 不给原文件添加后缀
+ if (src.contains(suffix)) {
+ dist = src.substring(0, src.length() - 4);
+ } else {
+ // 如果不包含压缩后缀 && 传递解压缩类型则无法解压
+ if (huffmanCode instanceof HuffmanUnZip) {
+ jLabel.setText("不包含" + suffix + "无法解压!");
+ jDialog.add(jLabel);
+ return;
+ }
+ dist = src + suffix;
+ }
+ System.out.printf("原文件:%s \t 目标文件:%s \n", src, dist);
+
+ try {
+ huffmanCode.zipOrUnZip(src, dist);
+ jLabel.setText("操作成功!");
+ } catch (Exception exception) {
+ jLabel.setText("操作异常!" + exception.getMessage());
+ }
+ jDialog.add(jLabel);
+ });
+ }
+
+}
+
public class HuffmanMainStarter {
+ public static void main(String[] args) {
+ new HuffmanFrame().init(200,70);
+ }
+}
+
+ +
+ + + + + +<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>card</title>
+ <style>
+ body,html{
+ width: 100%;
+ height: 100%;
+ }
+ body{
+ display: flex;/*弹性盒模型*/
+ justify-content: center;/*水平对齐 盒子位于中心*/
+ align-items: center;/*竖直对齐 居中对齐*/
+ background-color: yellow;
+ perspective: 1000px;/*景深:眼到屏幕的距离*/
+ }
+ body,h1,p{
+ margin: 0;
+ }
+ .card{
+ width: 520px;
+ height: 350px;
+ border-radius: 15px;
+ background: linear-gradient(#020333 70%,#fff 75%);/*渐变色*/
+ transform:rotateX(0deg);
+ transform-style: preserve-3d;
+ animation: move 1.5s;
+ }
+ .box{
+ width: 520px;
+ height: 350px;
+ font-family: Rockwell;
+ transform-style: preserve-3d;
+ transform: translateZ(88px);
+ box-shadow: 0 0 30px #000 inset;
+ }
+ @keyframes move {
+ 0%{
+ transform: scale(0);
+ }
+ 30%{
+ transform: scale(1.2);
+ }
+ 40%{
+ transform: scale(0.85);
+ }
+ 50%{
+ transform: scale(1.15);
+ }
+ 60%{
+ transform: scale(0.9);
+ }
+ 70%{
+ transform: scale(1.1);
+ }
+ 80%{
+ transform: scale(0.95);
+ }
+ 90%{
+ transform: scale(1.05);
+ }
+ 100%{
+ transform: scale(1);
+ }
+ }
+ h1{
+
+ color: #fff;
+ height: 60%;
+ font-size: 46px;
+ text-align: center;
+ line-height: 210px;
+ }
+ p{
+
+ color: #a9467d;
+ height: 40%;
+ font-size: 24px;
+ text-align: center;
+ line-height: 140px;
+
+ }
+
+ </style>
+ <script src="jquery-3.3.1.js"></script>
+
+</head>
+<body>
+<div class="card">
+ <div class="box">
+ <h1>Happy Birthday
+ <span style="color: #FF0000;font-size: 50px">XXX</span>
+ </h1>
+ <p>To my friend,
+ 爱你的XXX爸爸!
+ </p>
+ </div>
+</div>
+<script>
+ var card = $(".card");
+ $(document).on("mousemove",function (e) {
+ var ax = ($(window).innerWidth() / 2 - e.pageX)/20;
+ var ay = ($(window).innerHeight() / 2 - e.pageY)/10;
+ card.attr("style","transform: rotateY("+ ax +"deg)rotateX("+ay +"deg) translateZ(7em)");
+ });
+</script>
+</body>
+</html>
+
+ +
+ + + + + +<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>滑块拖拽</title>
+</head>
+<style>
+ body {
+ margin: 0;
+ padding: 0;
+ user-select: none;
+ }
+
+ .content {
+ position: relative;
+ width: 300px;
+ height: 40px;
+ margin: 50px auto;
+ background-color: #E8E8EB;
+ text-align: center;
+ line-height: 40px;
+ }
+
+ .rect {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+
+ }
+
+ .rect .bg {
+ position: absolute;
+ left: 0;
+ top: 0;
+ z-index: 1;
+ width: 0;
+ height: 100%;
+ background: rgba(122,194,60,.4);
+ }
+
+ .rect .move {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ width: 45px;
+ height: 40px;
+ position: absolute;
+ top: 0;
+ left: 0;
+ background-color: #fff;
+ border: 1px solid #cccccc;
+ }
+ .rect span{
+
+ }
+</style>
+<body>
+<div class="content">
+ <div class="rect">
+ <div class="bg"></div>
+ <span>滑块拖动验证</span>
+ <div class="move">
+ <img src="images/right.png" alt="图片拖动">
+ </div>
+ </div>
+</div>
+<script>
+ var oMove = document.querySelector(".move"),
+ oBg = document.querySelector(".bg"),
+ oRect = document.querySelector(".rect"),
+ oImage = document.querySelector("img"),
+ oSpan = document.querySelector("span"),
+ _X = 0;
+ oMove.onmousedown = function (e) {
+
+ var startX = e.clientX;
+ document.onmousemove = function (e) {
+ var endX = e.clientX;
+ _X = endX - startX;
+ if (_X < 0) {
+ _X = 0;
+ }
+ if (_X > 255) {
+ _X = 255;
+ }
+ oBg.style.width = oMove.style.left = _X + "px";
+
+ if (_X >= 255) {
+ oRect.style.color = "black";
+ oImage.src = "images/left.png";
+ oSpan.innerHTML = "验证成功";
+ oBg.style.width = oMove.style.left = 255 + "px";
+ }
+ }
+ };
+ document.onmouseup = function () {
+ document.onmousemove = null;
+ if (_X < 255) {
+ oBg.style.width = oMove.style.left = 0;
+ }
+
+ }
+</script>
+
+</body>
+</html>
+
+ +
+ + + + + +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+<title>五子棋</title>
+<meta name="viewport" content="device-width; initial-scale=1.0;" />
+<style>
+#c1 {
+ display: block;
+ margin: 60px auto;
+ box-shadow: 1px 1px 5px #000000;
+}
+</style>
+<script src="js/index.js"></script>
+
+</head>
+
+<body>
+<canvas id="c1" width="450px" height="450px"></canvas>
+</body>
+</html>
+
window.onload = function(){
+ var oC = document.getElementById('c1');
+ var oGc = oC.getContext('2d');
+
+ var over = false;
+
+ oGc.strokeStyle = "#bfbfbf";
+
+ //绘制棋盘
+ for(var i=0;i<15;i++){
+ oGc.moveTo(15+i*30,15);
+ oGc.lineTo(15+i*30,435);
+ oGc.stroke();
+ oGc.moveTo(15,15+i*30);
+ oGc.lineTo(435,15+i*30);
+ oGc.stroke();
+ }
+
+
+ /* AI难点解析
+ 赢法数组:记录了五子棋说有的赢法,三维数组
+ 每一种赢法的统计数组,一维数组
+ 如何判断胜负
+ 计算机落子规则*/
+
+ //赢法数组
+ var wins = [];
+
+ for(var i=0;i<15;i++){
+ wins[i] = [];
+ for(var j=0;j<15;j++){
+ wins[i][j] = [];
+ }
+ }
+
+ var count = 0;
+ for(var i=0;i<15;i++){
+ for(var j=0;j<11;j++){
+ //i=0 j=0
+ //wins[0][0][0] = true;
+ //wins[0][1][0] = true;
+ //wins[0][2][0] = true;
+ //wins[0][3][0] = true;
+ //wins[0][4][0] = true;
+
+ //wins[0][1][1] = true;
+ //wins[0][2][1] = true;
+ //wins[0][3][1] = true;
+ //wins[0][4][1] = true;
+ //wins[0][5][1] = true;
+ for(var k=0;k<5;k++){
+ wins[i][j+k][count] = true;
+ }
+ count++;
+ }
+ }
+ for(var i=0;i<15;i++){
+ for(var j=0;j<11;j++){
+ for(var k=0;k<5;k++){
+ wins[j+k][i][count] = true;
+ }
+ count++;
+ }
+ }
+ for(var i=0;i<11;i++){
+ for(var j=0;j<11;j++){
+ for(var k=0;k<5;k++){
+ wins[i+k][j+k][count] = true;
+ }
+ count++;
+ }
+ }
+ for(var i=0;i<11;i++){
+ for(var j=14;j>3;j--){
+ for(var k=0;k<5;k++){
+ wins[i+k][j-k][count] = true;
+ }
+ count++;
+ }
+ }
+
+ var myWin = [];
+ var computerWin = [];
+
+ for(var i=0;i<count;i++){
+ myWin[i] = 0;
+ computerWin[i] = 0;
+ }
+
+ function oneStep(i,j,me){
+ oGc.beginPath();
+ oGc.arc(15+i*30,15+j*30,13,0,2*Math.PI);
+ oGc.closePath();
+ var gradient = oGc.createRadialGradient(15+i*30+2,15+j*30+2,13,15+i*30+2,15+j*30+2,0);
+ if(me){
+ gradient.addColorStop(0,"#0A0A0A");
+ gradient.addColorStop(1,"#636766");
+ }else{
+ gradient.addColorStop(0,"#D1D1D1");
+ gradient.addColorStop(1,"#F9F9F9");
+ }
+
+ oGc.fillStyle = gradient;
+ oGc.fill();
+
+ };
+
+ var me = true;
+ var chessBoard = [];
+ for(var i=0;i<15;i++){
+ chessBoard[i] = [];
+ for(var j=0;j<15;j++){
+ chessBoard[i][j] = 0;
+ }
+ };
+
+ oC.onclick = function(ev){
+ if(!me){return;}
+ if(over){return;}
+
+ var x = ev.offsetX;
+ var y = ev.offsetY;
+ var i = Math.floor(x/30);
+ var j = Math.floor(y/30);
+
+ if(chessBoard[i][j] == 0){
+ oneStep(i,j,me);
+ chessBoard[i][j] = 1;
+
+ }
+
+ for(var k=0;k<count;k++){
+ if(wins[i][j][k]){
+ myWin[k]++;
+ computerWin[k] = 6;
+ if(myWin[k] == 5){
+ window.alert("恭喜你,获得了胜利!");
+ over = true;
+ }
+ }
+ }
+
+ if(!over){
+ computerAI();
+ me = !me;
+ }
+
+ }
+
+ function computerAI(){
+ var myScore = [];
+ var computerScore = [];
+ var iMax = 0;
+ var u =0;
+ var v= 0;
+
+ for(var i=0;i<15;i++){
+ myScore[i] = [];
+ computerScore[i] = [];
+ for(var j=0;j<15;j++){
+ myScore[i][j] = 0;
+ computerScore[i][j] = 0;
+ }
+ }
+
+ for(var i=0;i<15;i++){
+ for(var j=0;j<15;j++){
+ if(chessBoard[i][j] == 0){
+ for(var k=0;k<count;k++){
+ if(wins[i][j][k]){
+ if(myWin[k] == 1){
+ myScore[i][j]+=200;
+ }else if(myWin[k] == 2){
+ myScore[i][j]+=400;
+ }else if(myWin[k] == 3){
+ myScore[i][j]+=2000;
+ }else if(myWin[k] == 4){
+ myScore[i][j]+=10000;
+ }
+
+ if(computerWin[k] == 1){
+ computerScore[i][j]+=400;
+ }else if(computerWin[k] == 2){
+ computerScore[i][j]+=800;
+ }else if(computerWin[k] == 3){
+ computerScore[i][j]+=2200;
+ }else if(computerWin[k] == 4){
+ computerScore[i][j]+=20000;
+ }
+ }
+ }
+
+ if(myScore[i][j]>iMax){
+ iMax = myScore[i][j];
+ u = i;
+ v = j;
+ }else if(myScore[i][j]==iMax){
+ if(computerScore[i][j]>computerScore[u][v]){
+ u = i;
+ v = j;
+ }
+ }
+
+ if(computerScore[i][j]>iMax){
+ iMax = computerScore[i][j];
+ u = i;
+ v = j;
+ }else if(computerScore[i][j]==iMax){
+ if(myScore[i][j]>myScore[u][v]){
+ u = i;
+ v = j;
+ }
+ }
+ }
+ }
+ }
+
+ oneStep(u,v,false);
+ chessBoard[u][v] = 2;
+
+ for(var k=0;k<count;k++){
+ if(wins[u][v][k]){
+ computerWin[k]++;
+ myWin[k] = 6;
+ if(computerWin[k] == 5){
+ window.alert('YOU LOST!');
+ over = true;
+ }
+ }
+ }
+
+ console.log(iMax);
+ if(!over){
+ me = !me;
+ }
+
+ };
+
+
+};
+
+ +
+ + + + + +<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <meta name="Generator" content="EditPlus®">
+ <meta name="Author" content="">
+ <meta name="Keywords" content="">
+ <meta name="Description" content="">
+ <title>懒加载技术</title>
+ <style>
+ *{
+ margin: 0;
+ padding:0;
+ }
+ body{
+ background: rgb(0,0,0);
+ }
+ .box{
+ overflow: hidden;
+ width: 948px;
+ background-color: #7c7c7c;
+ margin: 50px auto;
+ -webkit-border-radius: 10px;
+ -moz-border-radius: 10px;
+ border-radius: 10px;
+
+ }
+ .box img{
+ float: left;
+ display: block;
+ width: 300px;
+ height: 150px;
+ margin: 6px;
+ border: 2px solid grey;
+ -webkit-border-radius: 10px;
+ -moz-border-radius: 10px;
+ border-radius: 10px;
+ }
+ </style>
+ <script src="js/lazyload.js"></script>
+
+ </head>
+ <body>
+ <div class="box">
+ <img src="images/1.jpg"/>
+ <img src="images/2.jpg"/>
+ <img src="images/3.jpg"/>
+ <img src="images/4.jpg"/>
+ <img src="images/5.jpg"/>
+ <img src="images/6.jpg"/>
+ <img src="images/7.jpg"/>
+ <img src="images/8.jpg"/>
+ <img src="images/9.jpg"/>
+ <img src="images/10.jpg"/>
+ <img src="images/11.jpg"/>
+ <img src="images/12.jpg"/>
+ <img src="images/13.jpg"/>
+ <img src="images/14.jpg"/>
+ <img src="images/15.jpg"/>
+ <img src="images/16.jpg"/>
+ <img src="images/17.jpg"/>
+ <img src="images/18.jpg"/>
+ <img src="images/19.jpg"/>
+ <img src="images/20.jpg"/>
+ <img src="images/1.jpg"/>
+ <img src="images/2.jpg"/>
+ <img src="images/3.jpg"/>
+ <img src="images/4.jpg"/>
+ <img src="images/5.jpg"/>
+ <img src="images/6.jpg"/>
+ <img src="images/7.jpg"/>
+ <img src="images/8.jpg"/>
+ <img src="images/9.jpg"/>
+ <img src="images/10.jpg"/>
+ <img src="images/11.jpg"/>
+ <img src="images/12.jpg"/>
+ <img src="images/13.jpg"/>
+ <img src="images/14.jpg"/>
+ <img src="images/15.jpg"/>
+ <img src="images/16.jpg"/>
+ <img src="images/17.jpg"/>
+ <img src="images/18.jpg"/>
+ <img src="images/19.jpg"/>
+ <img src="images/20.jpg"/>
+ <img src="images/1.jpg"/>
+ <img src="images/2.jpg"/>
+ <img src="images/3.jpg"/>
+ <img src="images/4.jpg"/>
+ <img src="images/5.jpg"/>
+ <img src="images/6.jpg"/>
+ <img src="images/7.jpg"/>
+ <img src="images/8.jpg"/>
+ <img src="images/9.jpg"/>
+ <img src="images/10.jpg"/>
+ <img src="images/11.jpg"/>
+ <img src="images/12.jpg"/>
+ <img src="images/13.jpg"/>
+ <img src="images/14.jpg"/>
+ <img src="images/15.jpg"/>
+ <img src="images/16.jpg"/>
+ <img src="images/17.jpg"/>
+ <img src="images/18.jpg"/>
+ <img src="images/19.jpg"/>
+ <img src="images/20.jpg"/>
+ </div>
+ <script src="js/jquery-1.11.1.min.js"></script>
+ <script src="js/lazyload.js"></script>
+ <script>
+ $("img").lazyload({
+ placeholder : "images/loading.gif", //用图片提前占位
+ // placeholder,值为某一图片路径.此图片用来占据将要加载的图片的位置,待图片加载时,占位图则会隐藏
+ effect: "fadeIn", // 载入使用何种效果
+ // effect(特效),值有show(直接显示),fadeIn(淡入),slideDown(下拉)等,常用fadeIn
+ threshold: -150, // 提前开始加载
+ // threshold,值为数字,代表页面高度.如设置为200,表示滚动条在离目标位置还有200的高度时就开始加载图片,可以做到不让用户察觉
+ //event: 'click', // 事件触发时才加载
+ // event,值有click(点击),mouseover(鼠标划过),sporty(运动的),foobar(…).可以实现鼠标莫过或点击图片才开始加载,后两个值未测试…
+ //container: $("#container"), // 对某容器中的图片实现效果
+ // container,值为某容器.lazyload默认在拉动浏览器滚动条时生效,这个参数可以让你在拉动某DIV的滚动条时依次加载其中的图片
+ //failurelimit : 10 // 图片排序混乱时
+ // failurelimit,值为数字.lazyload默认在找到第一张不在可见区域里的图片时则不再继续加载,但当HTML容器混乱的时候可能出现可见区域内图片并没加载出来的情况,failurelimit意在加载N张可见区域外的图片,以避免出现这个问题.
+});
+ </script>
+ </body>
+</html>
+
(function($){
+ $.fn.lazyload = function(options){
+ var settings = {
+ threshold: 0,
+ failurelimit: 0,
+ event: "scroll",
+ effect: "show",
+ container: window
+ };
+ if(options){
+ $.extend(settings, options);
+ }
+ var elements = this;
+ if("scroll" == settings.event){
+ $(settings.container).bind("scroll", function(event){
+ var counter = 0;
+ elements.each(function(){
+ if($.abovethetop(this, settings) || $.leftofbegin(this, settings)){
+ } else if(!$.belowthefold(this, settings) && !$.rightoffold(this, settings)){
+ $(this).trigger("appear");
+ } else {
+ if(counter++ > settings.failurelimit){
+ return false;
+ }
+ }
+ });
+ var temp = $.grep(elements, function(element){
+ return !element.loaded;
+ });
+ elements = $(temp);
+ });
+ }
+ this.each(function(){
+ var self = this;
+ if(undefined == $(self).attr("original")){
+ $(self).attr("original", $(self).attr("src"));
+ }
+ if("scroll" != settings.event || undefined == $(self).attr("src") || settings.placeholder == $(self).attr("src") || ($.abovethetop(self, settings) || $.leftofbegin(self, settings) || $.belowthefold(self, settings) || $.rightoffold(self, settings))){
+ if(settings.placeholder){
+ $(self).attr("src", settings.placeholder);
+ } else {
+ $(self).removeAttr("src");
+ }
+ self.loaded = false;
+ } else {
+ self.loaded = true;
+ }
+ $(self).one("appear", function(){
+ if(!this.loaded){
+ $("<img />").bind("load", function(){
+ $(self).hide().attr("src", $(self).attr("original"))[settings.effect](settings.effectspeed);
+ self.loaded = true;
+ }).attr("src", $(self).attr("original"));
+ }
+ });
+ if("scroll" != settings.event){
+ $(self).bind(settings.event, function(event){
+ if(!self.loaded){
+ $(self).trigger("appear");
+ }
+ });
+ }
+ });
+ $(settings.container).trigger(settings.event);
+ return this;
+ };
+ $.belowthefold = function(element, settings){
+ if(settings.container === undefined || settings.container === window){
+ var fold = $(window).height() + $(window).scrollTop();
+ } else {
+ var fold = $(settings.container).offset().top + $(settings.container).height();
+ }
+ return fold <= $(element).offset().top - settings.threshold;
+ };
+ $.rightoffold = function(element, settings){
+ if(settings.container === undefined || settings.container === window){
+ var fold = $(window).width() + $(window).scrollLeft();
+ } else {
+ var fold = $(settings.container).offset().left + $(settings.container).width();
+ }
+ return fold <= $(element).offset().left - settings.threshold;
+ };
+ $.abovethetop = function(element, settings){
+ if(settings.container === undefined || settings.container === window){
+ var fold = $(window).scrollTop();
+ } else {
+ var fold = $(settings.container).offset().top;
+ }
+ return fold >= $(element).offset().top + settings.threshold + $(element).height();
+ };
+ $.leftofbegin = function(element, settings){
+ if(settings.container === undefined || settings.container === window){
+ var fold = $(window).scrollLeft();
+ } else {
+ var fold = $(settings.container).offset().left;
+ }
+ return fold >= $(element).offset().left + settings.threshold + $(element).width();
+ };
+ $.extend($.expr[':'], {
+ "below-the-fold": "$.belowthefold(a, {threshold : 0, container: window})",
+ "above-the-fold": "!$.belowthefold(a, {threshold : 0, container: window})",
+ "right-of-fold": "$.rightoffold(a, {threshold : 0, container: window})",
+ "left-of-fold": "!$.rightoffold(a, {threshold : 0, container: window})"
+ });
+})(jQuery);
+
+ +
+ + + + + +<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>love</title>
+ <style>
+ *{
+ margin: 0;
+ padding: 0;
+ }
+ body{
+ background-color: #000;
+ background-size: cover;
+ overflow-y: hidden;
+ }
+ .love{
+ width: 400px;
+ height: 400px;
+ /*background-color: #7c7c7c;*/
+ margin: 130px auto;
+ animation: move 1s infinite alternate;
+ }
+ @keyframes move {
+ 100%{
+ transform: scale(1.5);
+ }
+ }
+ .left{
+ float: left;
+ width: 150px;
+ height: 250px;
+ background-color: #FF0000;
+ border-radius: 75px 75px 0 5px;
+ -webkit-transform: rotate(-45deg);
+ -moz-transform: rotate(-45deg);
+ -ms-transform: rotate(-45deg);
+ -o-transform: rotate(-45deg);
+ transform: rotate(-45deg);
+ margin-left: 85px;
+ box-shadow: 0 0 20px #FF0000;
+ animation: shadow 1s infinite alternate;
+ }
+ @keyframes shadow {
+ 100%{
+ box-shadow: 0 0 100px #FF0000;
+ }
+ }
+ .right{
+ float: left;
+ width: 150px;
+ height: 250px;
+ background-color: #FF0000;
+ border-radius: 75px 75px 5px 0;
+ -webkit-transform: rotate(45deg);
+ -moz-transform: rotate(45deg);
+ -ms-transform: rotate(45deg);
+ -o-transform: rotate(45deg);
+ transform: rotate(45deg);
+ margin-left: -78px;
+ box-shadow: 0 0 10px #FF0000;
+ animation: shadow 1s infinite alternate;
+ }
+ p{
+ color: #FF0000;
+ box-shadow: 0 0 100px #FF0000;
+ text-align: center;
+ font-size: 80px;
+ margin-top: -110px;
+ }
+ .snowfall-flakes:before,.snowfall-flakes:after{
+ content:'';
+ position:absolute;/*绝对定位 参考物 一般是父元素*/
+ width:10px;
+ height:16px;
+ background-color:red;
+ border-radius:50px 50px 0 0;/*圆角 左上 右上 右下 左下*/
+ transform:rotate(-45deg);/*css3新增 transform变换 rotate旋转 */
+ }
+
+
+ /*在元素之后添加内容*/
+ .snowfall-flakes:after{
+ left:5px;
+ transform:rotate(45deg);/*css3新增 transform变换 rotate旋转 */
+ }
+ </style>
+</head>
+<body>
+
+<div class="love">
+ <div class="left"></div>
+ <div class="right"></div>
+</div>
+<script src="http://libs.baidu.com/jquery/2.1.4/jquery.min.js"></script>
+<script>
+ (function () {
+ var lastTime = 0;
+ var vendors = ['webkit', 'moz'];
+ for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
+ window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
+ window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] || window[vendors[x] + 'CancelRequestAnimationFrame'];
+ }
+ if (!window.requestAnimationFrame)
+ window.requestAnimationFrame = function (callback, element) {
+ var currTime = new Date().getTime();
+ var timeToCall = Math.max(0, 16 - (currTime - lastTime));
+ var id = window.setTimeout(function () {
+ callback(currTime + timeToCall);
+ }, timeToCall);
+ lastTime = currTime + timeToCall;
+ return id;
+ };
+ if (!window.cancelAnimationFrame)
+ window.cancelAnimationFrame = function (id) {
+ clearTimeout(id);
+ };
+ }());
+ (function ($) {
+ $.snowfall = function (element, options) {
+ var defaults = {
+ flakeCount: 35,
+ flakeColor: 'transparent',
+ flakePosition: 'absolute',
+ flakeIndex: 999999,
+ minSize: 1,
+ maxSize: 2,
+ minSpeed: 1,
+ maxSpeed: 5,
+ round: false,
+ shadow: false,
+ collection: false,
+ collectionHeight: 40,
+ deviceorientation: false
+ }, options = $.extend(defaults, options), random = function random(min, max) {
+ return Math.round(min + Math.random() * (max - min));
+ };
+ $(element).data("snowfall", this);
+
+ function Flake(_x, _y, _size, _speed, _id) {
+ this.id = _id;
+ this.x = _x;
+ this.y = _y;
+ this.size = _size;
+ this.speed = _speed;
+ this.step = 0;
+ this.stepSize = random(1, 10) / 100;
+ if (options.collection) {
+ this.target = canvasCollection[random(0, canvasCollection.length - 1)];
+ }
+ var flakeMarkup = null;
+ if (options.image) {
+ flakeMarkup = $(document.createElement("div"));
+ /*flakeMarkup[0].src=options.图片;*/
+ } else {
+ flakeMarkup = $(document.createElement("div"));
+ flakeMarkup.css({'background': options.flakeColor});
+ }
+ flakeMarkup.attr({'class': 'snowfall-flakes', 'id': 'flake-' + this.id}).css({
+ 'width': this.size,
+ 'height': this.size,
+ 'position': options.flakePosition,
+ 'top': this.y,
+ 'left': this.x,
+ 'fontSize': 0,
+ 'zIndex': options.flakeIndex
+ });
+ if ($(element).get(0).tagName === $(document).get(0).tagName) {
+ $('body').append(flakeMarkup);
+ element = $('body');
+ } else {
+ $(element).append(flakeMarkup);
+ }
+ this.element = document.getElementById('flake-' + this.id);
+ this.update = function () {
+ this.y += this.speed;
+ if (this.y > (elHeight) - (this.size + 6)) {
+ this.reset();
+ }
+ this.element.style.top = this.y + 'px';
+ this.element.style.left = this.x + 'px';
+ this.step += this.stepSize;
+ if (doRatio === false) {
+ this.x += Math.cos(this.step);
+ } else {
+ this.x += (doRatio + Math.cos(this.step));
+ }
+ if (options.collection) {
+ if (this.x > this.target.x && this.x < this.target.width + this.target.x && this.y > this.target.y && this.y < this.target.height + this.target.y) {
+ var ctx = this.target.element.getContext("2d"), curX = this.x - this.target.x,
+ curY = this.y - this.target.y, colData = this.target.colData;
+ if (colData[parseInt(curX)][parseInt(curY + this.speed + this.size)] !== undefined || curY + this.speed + this.size > this.target.height) {
+ if (curY + this.speed + this.size > this.target.height) {
+ while (curY + this.speed + this.size > this.target.height && this.speed > 0) {
+ this.speed *= .5;
+ }
+ ctx.fillStyle = "#fff";
+ if (colData[parseInt(curX)][parseInt(curY + this.speed + this.size)] == undefined) {
+ colData[parseInt(curX)][parseInt(curY + this.speed + this.size)] = 1;
+ ctx.fillRect(curX, (curY) + this.speed + this.size, this.size, this.size);
+ } else {
+ colData[parseInt(curX)][parseInt(curY + this.speed)] = 1;
+ ctx.fillRect(curX, curY + this.speed, this.size, this.size);
+ }
+ this.reset();
+ } else {
+ this.speed = 1;
+ this.stepSize = 0;
+ if (parseInt(curX) + 1 < this.target.width && colData[parseInt(curX) + 1][parseInt(curY) + 1] == undefined) {
+ this.x++;
+ } else if (parseInt(curX) - 1 > 0 && colData[parseInt(curX) - 1][parseInt(curY) + 1] == undefined) {
+ this.x--;
+ } else {
+ ctx.fillStyle = "#fff";
+ ctx.fillRect(curX, curY, this.size, this.size);
+ colData[parseInt(curX)][parseInt(curY)] = 1;
+ this.reset();
+ }
+ }
+ }
+ }
+ }
+ if (this.x > (elWidth) - widthOffset || this.x < widthOffset) {
+ this.reset();
+ }
+ }
+ this.reset = function () {
+ this.y = 0;
+ this.x = random(widthOffset, elWidth - widthOffset);
+ this.stepSize = random(1, 10) / 100;
+ this.size = random((options.minSize * 100), (options.maxSize * 100)) / 100;
+ this.speed = random(options.minSpeed, options.maxSpeed);
+ }
+ }//素材家园 - www.sucaijiayuan.com
+ var flakes = [], flakeId = 0, i = 0, elHeight = $(element).height(), elWidth = $(element).width(),
+ widthOffset = 0, snowTimeout = 0;
+ if (options.collection !== false) {
+ var testElem = document.createElement('canvas');
+ if (!!(testElem.getContext && testElem.getContext('2d'))) {
+ var canvasCollection = [], elements = $(options.collection),
+ collectionHeight = options.collectionHeight;
+ for (var i = 0; i < elements.length; i++) {
+ var bounds = elements[i].getBoundingClientRect(), canvas = document.createElement('canvas'),
+ collisionData = [];
+ if (bounds.top - collectionHeight > 0) {
+ document.body.appendChild(canvas);
+ canvas.style.position = options.flakePosition;
+ canvas.height = collectionHeight;
+ canvas.width = bounds.width;
+ canvas.style.left = bounds.left + 'px';
+ canvas.style.top = bounds.top - collectionHeight + 'px';
+ for (var w = 0; w < bounds.width; w++) {
+ collisionData[w] = [];
+ }
+ canvasCollection.push({
+ element: canvas,
+ x: bounds.left,
+ y: bounds.top - collectionHeight,
+ width: bounds.width,
+ height: collectionHeight,
+ colData: collisionData
+ });
+ }
+ }
+ } else {
+ options.collection = false;
+ }
+ }
+ if ($(element).get(0).tagName === $(document).get(0).tagName) {
+ widthOffset = 25;
+ }
+ $(window).bind("resize", function () {
+ elHeight = $(element)[0].clientHeight;
+ elWidth = $(element)[0].offsetWidth;
+ });
+ for (i = 0; i < options.flakeCount; i += 1) {
+ flakeId = flakes.length;
+ flakes.push(new Flake(random(widthOffset, elWidth - widthOffset), random(0, elHeight), random((options.minSize * 100), (options.maxSize * 100)) / 100, random(options.minSpeed, options.maxSpeed), flakeId));
+ }
+ if (options.round) {
+ $('.snowfall-flakes').css({
+ '-moz-border-radius': options.maxSize,
+ '-webkit-border-radius': options.maxSize,
+ 'border-radius': options.maxSize
+ });
+ }
+ if (options.shadow) {
+ $('.snowfall-flakes').css({
+ '-moz-box-shadow': '1px 1px 1px #555',
+ '-webkit-box-shadow': '1px 1px 1px #555',
+ 'box-shadow': '1px 1px 1px #555'
+ });
+ }
+ var doRatio = false;
+ if (options.deviceorientation) {
+ $(window).bind('deviceorientation', function (event) {
+ doRatio = event.originalEvent.gamma * 0.1;
+ });
+ }
+
+ function snow() {
+ for (i = 0; i < flakes.length; i += 1) {
+ flakes[i].update();
+ }
+ snowTimeout = requestAnimationFrame(function () {
+ snow()
+ });
+ }
+
+ snow();
+ this.clear = function () {
+ $(element).children('.snowfall-flakes').remove();
+ flakes = [];
+ cancelAnimationFrame(snowTimeout);
+ }
+ };
+ $.fn.snowfall = function (options) {
+ if (typeof(options) == "object" || options == undefined) {
+ return this.each(function (i) {
+ (new $.snowfall(this, options));
+ });
+ } else if (typeof(options) == "string") {
+ return this.each(function (i) {
+ var snow = $(this).data('snowfall');
+ if (snow) {
+ snow.clear();
+ }
+ });
+ }
+ };
+ })(jQuery);
+</script>
+<script>
+ $(document).snowfall({
+ flakeCount:200//规定爱心的数量
+ })
+</script>
+</body>
+</html>
+
+ +
+ + + + + +<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>折纸导航栏</title>
+</head>
+<style>
+ *{
+ margin: 0;
+ padding: 0;
+ }
+ .content{
+ position: relative;
+ width: 400px;
+ height: 30px;
+ margin: 50px auto;
+ /*-webkit-perspective: 1000px;
+ -moz-perspective: 1000px;
+ -ms-perspective: 1000px;*/
+ perspective: 1000px;/*景深相当于眼睛距离元素的位置距离*/
+ }
+ .content .open{
+ transform: rotateX(0);
+ animation: open 1s linear;
+ }
+ @keyframes open {
+ 0%{
+ transform: rotateX(-90deg);
+ }
+ 20%{
+ transform:rotateX(30deg);
+ }
+ 40%{
+ transform:rotateX(-60deg);
+ }
+ 60%{
+ transform:rotateX(60deg);
+ }
+ 80%{
+ transform:rotateX(-30deg);
+ }
+ 100%{
+ transform:rotateX(0);
+ }
+ }
+ .content .close{
+ transform: rotateX(-120deg);
+ animation: close 1s ease;
+ }
+ @keyframes close {
+ 0%{
+ transform: rotateX(0);
+ }
+ 100%{
+ transform: rotateX(-90deg);
+ }
+ }
+ .content div{
+ position: absolute;
+ left: 0;
+ top: 30px;
+ width: 100%;
+ transform-style: preserve-3d;
+ transform-origin: top;
+ transform: rotateX(-90deg);
+ }
+ .content div span{
+ display: block;
+ height: 28px;
+ line-height: 30px;
+ text-align: center;
+ background: rgb(153,102,102);
+ }
+ input{
+ position: absolute;
+ left: 0;
+ top: 0;
+ z-index: 999;
+ width: 400px;
+ height: 30px;
+ border: 0;
+ background-color: #c74;
+ }
+</style>
+<body>
+<div class="content">
+ <input type="button" value="打开" id="btn"/>
+ <div>
+ <span>导航1</span>
+ <div>
+ <span>导航2</span>
+ <div>
+ <span>导航3</span>
+ <div>
+ <span>导航4</span>
+ <div>
+ <span>导航5</span>
+ <div>
+ <span>导航6</span>
+ <div>
+ <span>导航7</span>
+ <div>
+ <span>导航8</span>
+ <div>
+ <span>导航9</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+<script>
+ var oBtn = document.getElementById('btn');
+ var oCon = document.getElementsByClassName('content')[0];
+ var oDiv = oCon.getElementsByTagName('div');
+ var time = null;
+ var i = 0;
+ var mark = true;
+ oBtn.onclick = function () {
+ if (mark){
+ i = 0;
+ timer=setInterval( function () {
+ if (i == oDiv.length -1)
+ {
+ clearInterval(timer);
+ }
+ oDiv[i].className = 'open';
+ i++;
+ },200)
+ oBtn.value = '关闭';
+ }
+ else
+ {
+ i = oDiv.length -1;
+ timer=setInterval( function () {
+ if (i == 0)
+ {
+ clearInterval(timer);
+ }
+ oDiv[i].className = 'close';
+ i--;
+ },100)
+ oBtn.value = '打开';
+ }
+ mark = !mark;
+ }
+</script>
+</body>
+</html>
+
+ +
+ + + + + +<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>
+ rain
+ </title>
+
+ <style>
+ html {
+ width: 100%;
+ }
+
+ body {
+ width: 100%;
+ margin: 0;
+ padding: 0;
+ background-color: #000;
+ }
+
+ .rain {
+ display: block;
+ }
+
+ embed {
+ display: block;
+ }
+ </style>
+</head>
+<body>
+<!--
+ 2、使用hidden="true"表示隐藏音乐播放按钮,相反使用hidden="false"表示开启音乐播放按钮。
+ 3、使用autostart="true" 表示是打开网页加载完后自动播放。
+ 4、使用loop="true"表示 循环播放 如仅想播放一次则为:loop="false"
+ -->
+<embed src="music/恋如雨止.mp3" hidden="true" autostart="true" loop="false">
+
+<canvas class="rain"></canvas>
+<!--
+canvas雨滴逻辑思路:
+1 画什么 -- 雨滴
+ 雨滴 是由许多个小矩形拼接成的 加上 遮盖层 让每个矩形透明度依次递减 就能看到此效果
+ 首先画一个 然后再去复制这个雨滴
+2 怎么画让雨滴动起来
+ 定义随机数让雨滴的 起始位置 长 宽 下落速度 绽放速度 不同
+ 然后开启定时器让雨滴动起来
+3 需要什么东西
+ 需要画笔: 实心画笔 空心画笔
+ 创建雨滴的模板
+ 盛放雨滴的容器
+-->
+<script>
+ var oCanvas = document.querySelector(".rain");//获取元素rain
+ var w, h;//定义变量w ,h
+ var aRain = [];//用来存放新生成的雨滴
+
+ ~~function () {//自执行函数:不用调用自己执行 把canvas和整个屏幕无缝贴合
+ window.onresize = arguments.callee;//随着浏览器的变化宽和高都变化
+ w = window.innerWidth;//获取浏览器的宽
+ h = window.innerHeight;//获取浏览器的高
+ oCanvas.width = w;//把值赋给canvas
+ oCanvas.height = h;//把值赋给canvas
+ }();
+
+ function random(min, max) {//定义随机函数
+ return Math.random() * (max - min) + min;//返回想要的任意的值
+ }
+
+ var canCon = oCanvas.getContext("2d");//创建一个2d画笔
+
+ Rain.prototype = {//this 指代当前函数
+ init: function () {//雨滴的基本参数
+ this.x = random(0, w);//雨滴横坐标
+ this.y = 0;//纵坐标
+ this.w = random(1.6, 3);//雨滴的 宽度 1.6px 到 2.5px
+ this.h = random(8, 15);//雨滴的 长度8px 到 15px
+ this.color = "#3ff";//颜色
+ this.vy = random(1.2, 2.3);//下降速度
+ this.vr = random(0.5, 1.5);//绽放速度
+ this.ground = random(0.8 * h, 0.9 * h);//雨滴绽放的位置
+ this.r = this.w / 2;//圆的半径
+ },
+ draw: function () {//把基本参数变化成雨滴效果
+ if (this.y < this.ground)//如果雨滴y坐标小于绽放位置,就一直下落
+ {
+ canCon.beginPath();//抬笔
+ canCon.fillStyle = this.color;//用画笔蘸颜色画雨滴
+ canCon.fillRect(this.x, this.y, this.w, this.h);//画矩形区域左上角的坐标 矩形的宽 高
+ } else//绽放的圆形区域
+ {
+ canCon.beginPath();//抬笔 雨滴停止下落
+ canCon.strokeStyle = "rgba(50,250,250,0.96)";//空心画笔 rgba() rgb是颜色 a是透明度
+ canCon.arc(this.x, this.y, this.r, 0, Math.PI * 2);//画圆形 圆心坐标 半径 画多少(弧度制)
+ canCon.stroke();//下笔作画
+ }
+
+ },
+ move: function () {//移动函数:设置怎么移动
+
+ if (this.y < this.ground)//如果雨滴的y坐标小于定义的地面高度 则雨滴往下移动
+ {
+ this.y += this.vy;//让每个雨滴下降速度不同
+ } else//如果大于 就绽放
+ {
+ if (this.r < 100)//绽放半径
+ {
+ this.r += this.vr;//让每个雨滴绽放速度不同
+ } else {
+ this.init();//循环雨滴:重新设置雨滴,重头再来 循环
+ }
+
+ }
+
+ this.draw();//调用draw函数
+ }
+ };
+
+
+ function Rain() {
+ }//雨滴的模板
+ //创建num个雨滴
+ function createRain(num) {//参数为创建多少雨滴
+ for (let i = 0; i < num; i++)//let 为立执行
+ {
+ setTimeout(function () {//创建num个雨滴的定时器
+ var rain = new Rain();//创建雨滴对象 创建什么样的雨滴 : 调用init函数 draw函数
+ rain.init();
+ rain.draw();
+ aRain.push(rain);//把雨滴放在aRain里边, 目的是 循环他
+ }, 200 * i);//时间200*i目的是为了让每个雨滴下落时间不同 单位为毫秒
+
+ }
+ }
+
+
+ setInterval(function () {//下落运动的定时器
+ canCon.fillStyle = "rgba(0,0,0,0.04)";//遮盖层,透明度递减,让画的矩形看起来更像雨滴
+ canCon.fillRect(0, 0, w, h);//画矩形
+ for (var item of aRain)//遍历num个雨滴
+ {
+ item.move();//把遍历后的雨滴都执行move函数 运动起来
+ }
+ }, 1000 / 90);//单位为毫秒
+
+
+ createRain(70);//方法入口 下多少雨滴
+
+</script>
+</body>
+</html>
+
+ +
+ + + + + +<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>snow</title>
+</head>
+<style>
+ html {
+ width: 100%;
+ }
+
+ body {
+ margin: 0;
+ padding: 0;
+ overflow-y: hidden;
+ width: 100%;
+ }
+
+ .header {
+ width: 100%;
+ height: 315px;
+ background: url("images/header-bg.png") repeat;
+
+ }
+
+ .snow {
+ position: relative;
+ height: inherit;
+ width: 960px;
+ background: url("images/con-bg.png") no-repeat 0 204px,
+ url("images/snow-bg.png") no-repeat 0 0;;
+ margin: 0 auto;
+ animation: auto 10s linear infinite;
+ }
+
+ /* 下雪动画
+
+ 插入两个背景图片*/
+ @keyframes auto {
+ from {
+ background: url("images/con-bg.png") no-repeat 0 204px,
+ url("images/snow-bg.png") repeat 0 0;
+ }
+ to {
+ background: url("images/con-bg.png") no-repeat 0 204px,
+ url("images/snow-bg.png") repeat 0 1000px;
+ }
+ }
+
+ tree, snow {
+ position: absolute;
+ }
+
+ tree {
+ width: 112px;
+ height: 137px;
+ background: url("images/tree.png");
+ }
+
+ snow {
+ left: 410px;
+ top: 210px;
+ width: 115px;
+ height: 103px;
+ background: url("images/ice.png");
+ animation: play 3s;
+ }
+
+ @keyframes play {
+ from {
+ transform: translate(0, -500px);
+ }
+ to {
+ transform: translate(0, 0);
+ }
+ }
+
+ tree:nth-child(1) {
+ left: 35px;
+ top: 169px;
+ animation: play 1s;
+ }
+
+ tree:nth-child(2) {
+ left: 200px;
+ top: 180px;
+ animation: play 1.9s;
+ }
+
+ tree:nth-child(3) {
+ left: 350px;
+ top: 125px;
+ animation: play 2.2s;
+ }
+
+ tree:nth-child(4) {
+ left: 515px;
+ top: 150px;
+ animation: play 1s;
+ }
+
+ tree:nth-child(5) {
+ left: 680px;
+ top: 170px;
+ animation: play 2s;
+ }
+
+ tree:nth-child(6) {
+ left: 805px;
+ top: 125px;
+ animation: play 1.7s;
+ }
+
+ /* 文字部分 start*/
+ .content {
+ position: relative;
+ width: inherit;
+ opacity: 0.7;
+ background-attachment:fixed;
+ background: url("images/snow.jpg") no-repeat center/cover;
+ height: 650px;
+ background-color: #ffffff;
+ padding-top: 60px;
+ }
+
+ .content .text {
+ padding-left: 250px;
+ }
+
+ .content .text p {
+ margin: 0;
+ font-size: 20px;
+ font-family: "微软雅黑 Light"; /* 这里可以改文字的字体*/
+ font-weight: bold;
+ color: #000000;
+ }
+
+ .content .box {
+ position: absolute;
+ top: 0;
+ width: inherit;
+ height: 500px;
+ float: left;
+ background-color: #ffffff;
+ }
+
+
+</style>
+<body>
+<div class="header">
+ <div class="snow">
+ <!-- 自定义标签 <tree> -->
+ <tree></tree>
+ <tree></tree>
+ <tree></tree>
+ <tree></tree>
+ <tree></tree>
+ <tree></tree>
+ <snow></snow>
+ </div>
+</div>
+<div class="content">
+ <div class="box"></div>
+ <div class="text">
+ <h1 style="color: red ">这里写主题哦!</h1>
+ <p>文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!</p>
+ <p>文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!</p>
+ <p>文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!</p>
+ <p>文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!</p>
+ <p>文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!</p>
+ <p>文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!</p>
+ <p>文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!</p>
+ <p>文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!</p>
+ <p>文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!</p>
+ <p>文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!</p>
+
+ </div>
+
+</div>
+
+<script>
+ /**
+ * 如何使文字实现渐变出现效果
+ *
+ */
+ var oContent = document.getElementsByClassName("content")[0],
+ oText = document.getElementsByClassName("text")[0],
+ oMove = document.getElementsByClassName("box")[0],
+ oP = document.getElementsByTagName("p")
+ ;
+
+ (function startMove() {
+ var timer = null;
+ clearInterval(timer);
+ timer = setInterval(function () {
+ var speed = 5;
+ if (oMove.offsetTop >= oContent.offsetHeight) {
+ clearInterval(timer);
+ }
+ else {
+ oMove.style.top = oMove.offsetTop + speed + 'px';
+
+ }
+ }, 30);
+
+ })()
+
+
+</script>
+
+</body>
+</html>
+
+ +
+ + + + + +<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>换肤特效</title>
+ <style type="text/css">
+ body {
+ margin: 0;
+ background-image: url("images/1.jpg");
+ background-size: cover;
+ }
+
+ ul {
+ margin: 0;
+ padding: 0;
+ list-style-type: none;
+ }
+
+ .bg-list {
+ display: none;
+ margin: 0;
+ width: 100%;
+ height: 200px;
+ background: rgba(0, 0, 0, 0.5);
+ }
+
+ .img-wrap {
+ height: 200px;
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+ }
+
+ .tab-btn {
+ background-image: url("images/upseek.png");
+ height: 50px;
+ width: 50px;
+ position: fixed;
+ top: 0;
+ right: 0;
+ }
+
+ .tab-btn:hover {
+ background-position-y: -63.6px;
+ }
+ </style>
+
+
+</head>
+<body>
+<div class="bg-list">
+ <ul class="img-wrap">
+ <li class="img-item" data-src="images/1.jpg">
+ <img src="images/1-1.jpg" width="160px"/>
+ </li>
+ <li class="img-item" data-src="images/2.jpg">
+ <img src="images/2-2.jpg" width="160px"/>
+ </li>
+ <li class="img-item" data-src="images/3.jpg">
+ <img src="images/3-3.jpg" width="160px"/>
+ </li>
+ <li class="img-item" data-src="images/4.jpg">
+ <img src="images/4-4.jpg" width="160px"/>
+ </li>
+ <li class="img-item" data-src="images/5.jpg">
+ <img src="images/5-5.jpg" width="160px"/>
+ </li>
+ <li class="img-item" data-src="images/6.jpg">
+ <img src="images/6-6.jpg" width="160px"/>
+ </li>
+ </ul>
+</div>
+<div class="tab-btn"></div>
+<script src="js\jquery.js" type="text/javascript"></script>
+<script type="text/javascript">
+ $(".tab-btn").click(function () {
+ $(".bg-list").slideToggle();
+ });
+ $(".img-item").click(function () {
+ var src = $(this).attr("data-src");
+ $("body").css({
+ "background-image": "url(" + src + ")"
+ })
+ });
+</script>
+</body>
+</html>
+
+ +
+ + + + + +本人在进入公司起,期间一直对自己要求严谨,遵守公司的相应制度. 在过去的一个月时间里,我参与了贵州银行的电子验印系统的开发,一直努力完成和完善分配给我的任务,在这一个月发现了自身还有很多的不足,所以抱着虚心学习的态度,学习公司的开发流程,了解公司的产品架构,主要技术,主动和同事沟通,学习经验,希望能快速融入公司,能够全心的投入工作.
+试用期完成的工作有限,主要负责验印系统的统一门开发,学习了一些新技术,因为自己在经验上不足,对于技术的学习和掌握还不够深入,发现问题的能力还不够,所以拖慢了自己的开发进度,简单列一些. 通过开发的过程中,学习并掌握了vue框架的使用,学习到了Oracle数据库的使用….. 使我认识到了一个称职的开发人员应当具备良好的语言表达能力,较强的逻辑能力,灵活的处理应变能力,有效的对外联系能力. 在参与项目的开发过程中,发现很多看似简单的工作,其实里面有很多技巧
+今后,我会多注意在这些方面的学习和积累,努力做好开发人员的本职工作,注重工作态度,把自己的工作做好做扎实,为项目的开发及公司的发展贡献自己的一份力量.在工作的这段时间里.我得到了同事的帮助,经常与我交流,指出技术上的问题,传授了很多开发经验,在生活上也给与快了我很大的帮助,使得我很快就适应了这里的生活.
+整个工作学习过程中,我认为自己工作比较认真负责.具有较强的责任心和进取心,能完成领导交付的工作,但也存在着许多缺点与不足,对工作的专业性还不够,业务经验不够丰富,对于发现问题的处理还不是很全面,我会在以后的工作中不断实践和总结,并积极学习新知识,弥补自身不足,来提高自己的综合素质. 总之,认真的回顾了这段时间的工作,发现了一些不足之处,这都是我在接下的工作中需要完善的.同时,也会尽最大努力的学习和积累经验,逐步发展成一个全面的技术开发人员,更好的完成工作.
+以上是我对2019年的工作总结及2020年工作计划,可能还不是很成熟,希望领导指正.展望2020年,我会更加努力,认真负责的去对待工作,相信自己会完成新的任务,能迎接新的挑战。为公司的发展做出自己的贡献.
++ +
+ + + + + +在职期间,我主要负责耐材项目的开发与维护,共迭代171个版本。通过与团队成员的紧密合作,我们按时完成了项目中的需求。在这段时间里,我不断提升自己的专业技能和知识,增强了自己的专业能力。我始终认为团队合作是成功的关键。在工作中,我积极与同事沟通交流,共同解决问题,促进了团队的凝聚力。
+在过去,我发现自己在与部分同事沟通时存在障碍。为了解决这一问题,我主动与他们进行深入交流,明确工作目标和期望,并加强了团队间的协作和沟通。
+由于工作任务的繁重,我曾面临较大的工作压力。为了释放压力,我学会了合理安排时间,优化工作流程,并积极参与团队活动,放松身心。
+我认为我始终保持积极的工作态度,认真对待每一个任务,尽最大努力去完成。认真履行工作职责,完成了各项工作任务,取得了一定的成绩。具体如下:
+回顾过去一年,我认为自己在工作中展现出了较强的责任心和团队精神。但在时间管理和应对突发事件方面,仍有待提高。
++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + ++ +
+ + + + + +代码实现 代码结构 pom <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> </dependency> 库表结构 -- ---------------------------- -- 定时任务调度表 -- ---------------------------- drop table if exists sys_job; create table sys_job ( job_id bigint(20) not null auto_increment comment '任务ID', job_name varchar(64) default '' comment '任务名称', job_group varchar(64) default 'DEFAULT' comment '任务组名', invoke_target varchar(500) not null comment......
+基础知识 网络知识 HTTP DNS 域名 云服务 网络安全 HTTPS CORS 网络渗透 OWASP HTML CSS JavaScript JQuery Ajax ES6-ES11 综合应用 工程化体系 代码规范 CSS预处理器 Less Sass PostCSS Node Promise Axios 工具 包管理工具 Npm Yarn 打包工具 Webpack Parcel 代码格式化工具 ESLint Prettier 调试工具 Chrome IETest Postman 版本管理工具 Git GitLab GitHub 部署发布工具 Jenkins CICD 主流技术 TypeScript Vue React Angular 综合应用 静态......
nacos nacos下载 下载地址 一键傻瓜试安装即可,官网写的很清楚这里不在赘述 http://nacos.io/zh-cn/docs/v2/quickstart/quick-start.html nacos启动 将模式改为单机模式 启动成功 nacos相关配置 demo-dev.yaml server: port: 8001 config: info: "config info for dev from nacos config center" demo-test.yaml server: port: 3333 config: info: "config info for test from nacos config center" user.yaml user: name: zs1112222 age: 10 address: 测试地址 代码 整合nacos配置中心,注册......
+在职期间,我主要负责耐材项目的开发与维护,共迭代171个版本。通过与团队成员的紧密合作,我们按时完成了项目中的需求。在这段时间里,我不断提升自己的专业技能和知识,增强了自己的专业能力。我始终认为团队合作是成功的关键。在工作中,我积极与同事沟......
结构 pom.xml fastdfs-client-java-1.27.jar:点击下载 <dependencies> <!-- fastdfs --> <dependency> <groupId>org.csource</groupId> <artifactId>fastdfs-client-java</artifactId> <version>1.27</version> <systemPath>${project.basedir}/lib/fastdfs-client-java-1.27.jar</systemPath> <scope>system</scope> </dependency> <!--aliyun oss 依赖--> <dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.11</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.11.0</version> </dependency> </dependencies> application.yml server: port: 80 公共部分 FileManagement public interface FileManagement { /** * 设置下一个bean的对象 * * @param nextFileManagement 下一个......
+自我介绍 1998 · 李济芝 河北唐山 15176733539 m15176733539@163.com 本人有严谨的工作态度与高质量意识;能查阅各种开发技术手册,具有独立解决问题的能力。具备扎实的Java基础和四年开发经验,有良好的编程风格,独立熟练使用Spring全家桶等常用类库开发Java服务端程序、对Jav......
结构 pom.xml <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.11</version> </dependency> <dependency> <groupId>com.alipay.sdk</groupId> <artifactId>alipay-sdk-java</artifactId> <version>4.9.9</version> </dependency> <dependency> <groupId>com.github.binarywang</groupId> <artifactId>weixin-java-pay</artifactId> <version>4.5.0</version> </dependency> </dependencies> application.yml server: port: 8080 pay: wechat: #微信公众号或者小程序等的appid appId: "" #微信支付商户号 mchId: "" #微信支付商户密钥 mchKey: "" #服务商模式下的子商户公众账号ID subAppId: #服务商模式下的子商户号 subMchId: # p12证书的位......
+代码实现 代码结构 pom <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> </dependency> 库表结构 -- ---------------------------- -- 定时任务调度表 -- ---------------------------- drop table if exists sys_job; create table sys_job ( job_id bigint(20) not null auto_increment comment '任务ID', job_name varchar(64) default '' comment '任务名称', job_group varchar(64) default 'DEFAULT' comment '任务组名', invoke_target varchar(500) not null comment......