├── Collection ├── 01. ArrayList源码解析(基于JDK1.8).md ├── 02. LinkedList源码解析(基于JDK1.8).md ├── 03. HashMap源码解析(基于JDK1.8).md ├── 04. HashTable源码解析(基于JDK1.8).md ├── 05. TreeMap源码解析(基于JDK1.8).md └── 06. ConcurrentHashMap源码解析(基于JDK1.8).md ├── Docker ├── 01. Docker简介以及安装.md ├── 02. Docker容器基础操作.md ├── 03. Docker容器高级操作.md ├── 04. Docker镜像操作.md ├── 05. Docker镜像仓库.md ├── 06. Dockerfile简介.md └── 07. Docker制作镜像.md ├── JVM ├── 01. Java 内存区域与内存溢出异常.md └── 02. Java 虚拟机的类加载机制.md ├── JavaBase ├── 01. Java 数据类型.md ├── 02. Java 关键字.md ├── 03. Java 的三大特性—封装、继承、多态.md ├── 04. 你真的了解 Java 中的 String 吗.md ├── 05. StringBuffer 和 StringBuilder 的区别.md ├── 06. Java反射详解.md ├── 07. 你对 Java 异常了解多少.md ├── 08. Java IO流.md ├── 09. Java 注解.md ├── 10. Java泛型.md ├── 11. Java枚举.md └── 12. Java 8 新特性.md ├── JavaThread ├── 01. Java 创建线程有哪些方式.md ├── 02. 线程有哪些状态,彼此之间如何切换.md ├── 03. 传统线程互斥技术 synchronized.md ├── 04. 死锁问题.md ├── 05. 传统线程同步通信技术.md ├── 06. 线程范围内共享数据.md ├── 07. 原子性操作类的使用.md ├── 08. ThreadLocal 类以及应用技巧.md ├── 09. 造成 HashMap 非线程安全的原因.md ├── 10. 多个线程间共享数据.md ├── 11. 线程并发库 Executors 的使用.md ├── 12. Callable 与 Future 的应用.md ├── 13. 线程锁 Lock 技术的应用.md ├── 14. 条件阻塞Condition的应用.md ├── 15. 线程同步工具Semaphore的使用.md ├── 16. 线程同步工具 CyclicBarrier 的使用.md ├── 17. 线程同步工具Exchanger的使用.md └── 18. 阻塞队列的使用.md ├── README.md ├── Spring ├── 01. Spring 概述.md ├── 02. Spring 依赖注入与装配Bean.md ├── 03. Spring Bean的作用域和生命周期.md ├── 04. Spring AOP:这可能是东半球最详细的一篇文章.md ├── 05. Spring 和数据库编程.md ├── 06. Spring 数据库编程在实际开发中的应用.md └── 07. 深入Spring数据库事务管理.md ├── SpringBoot ├── 00. 我们为什么要学习SpringBoot.md ├── 01. Spring Boot 开发环境搭建和项目启动.md ├── 02. Spring Boot返回Json数据及数据封装.md ├── 03. Spring Boot 使用slf4j进行日志记录.md ├── 04. Spring Boot 中的项目属性配置.md ├── 05. Spring Boot 中的MVC支持.md ├── 06. Spring Boot 集成 Swagger2 展现在线接口文档.md ├── 07. Spring Boot 集成Thymeleaf模板引擎.md ├── 08. Spring Boot 中的全局异常处理.md ├── 09. Spring Boot 中的切面AOP处理.md ├── 10. Spring Boot 集成MyBatis.md ├── 11. Spring Boot 事务配置管理.md ├── 12. Spring Boot 中使用监听器.md ├── 13. Spring Boot 中使用拦截器.md ├── 14. Spring Boot 中集成Redis.md ├── 15. Spring Boot 中集成ActiveMQ.md ├── 16. Spring Boot 中集成 Shiro.md ├── 17. Spring Boot 中集成Lucence.md └── 18. Spring Boot 搭建实际项目开发中的架构.md ├── SpringMVC ├── 01. 宏观上把握 SpringMVC 框架.md ├── 02. 走进 SpringMVC 的世界.md ├── 03. SpringMVC中 注解和非注解方式下的映射器和适配器总结.md ├── 04. Spring、MyBatis 和 SpringMVC 的整合(ssm).md ├── 05. SpringMVC 中的参数绑定总结.md ├── 06. SpringMVC 中的数据校验.md ├── 07. SpringMVC 中的统一异常处理.md ├── 08. SpringMVC 中实现文件上传.md ├── 09. SpringMVC 与前台的 json 数据交互.md ├── 10. SpringMVC 对 RESTfull 的支持.md └── 11. SpringMVC 中的拦截器.md ├── SpringSecurity ├── 01. 概述:SpringSecurity的前世今生.md ├── 02. Spring Security 基于表单登录的认证模式.md ├── 03. Spring Security 表单登录源码解析.md ├── 04. Spring Security 的验证码生成详解.md ├── 05. Spring Security 验证码登录流程讲解.md ├── 06. Spring Security 在前后端分离下的JSON交互方案.md └── 07. 扩展篇:Spring Security 过滤器链之 SecurityContext.md └── mq ├── 01. 快速了解 RabbitMQ .md ├── 02. RabbitMQ 模型详解.md ├── 03. RabbitMQ 在项目中配置.md ├── 04. RabbitMQ 高级用法.md ├── 05. 一起来了解下 SpringAmqp.md └── 06. RabbitMQ 可靠性投递.md /Docker/01. Docker简介以及安装.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | ---- 8 | 9 | ## Docker简介 10 | 11 | ![image](http://note.youdao.com/yws/public/resource/86feadd8e5ad608ae81a32c1ddc4fc0b/xmlnote/CECE657394264985BA564318838866A7/10151) 12 | Docker 是一个开源的应用容器引擎,基于 Go 语言 并遵从 Apache2.0 协议开源。 13 | Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。它包含容器引擎和Docker Hub注册服务器。 14 | 15 | - Docker容器引擎 16 | 17 | ``` 18 | 该引擎可以让开发者打包应用程序和依赖包到一个可移植的容器中,可以发布到任何流行的linux服务器。 19 | ``` 20 | 21 | - Docker Hub镜像服务器 22 | 23 | ``` 24 | 用户可以在该服务器上存储、管理、分享自己的镜像。利用Docker,可实现软件的一次配置、处处运行。 25 | ``` 26 | 27 | ### Docker优点 28 | 29 | > 一致的运行环境 30 | 31 | ``` 32 | Docker的镜像提供了除内核外完整的运行时环境,确保了应用运行环境一致性, 33 | 从而避免"这段代码在我机器上没问题啊!!!"的郁闷。 34 | ``` 35 | 36 | > 更快速的启动时间 37 | 38 | ``` 39 | 可以做到秒级、甚至毫秒级的启动时间。大大的节约了开发、测试、部署的时间。 40 | ``` 41 | 42 | > 隔离性 43 | 44 | ``` 45 | 避免公用的服务器,资源会容易受到其他用户的影响。 46 | ``` 47 | 48 | > 弹性伸缩,快速扩展 49 | 50 | ``` 51 | 善于处理集中爆发的服务器使用压力; 52 | ``` 53 | 54 | 55 | > 可移植性好 56 | 57 | ``` 58 | 可以很轻易的将在一个平台上运行的应用,迁移到另一个平台上,而不用担心运行环境的变化导致应用无法正常运行的情况。 59 | ``` 60 | 61 | > 持续交付和部署 62 | 63 | ``` 64 | 使用 Docker 可以通过定制应用镜像来实现持续集成、持续交付、部署。 65 | ``` 66 | 67 | > 资源利用最大化 68 | 69 | ``` 70 | Docker容器是完全使用沙箱机制,互不干扰。可以在一台机器运行多个容器即多个应用,从而达到资源的最大化利用,经济、高效的方案。 71 | ``` 72 | 73 | ## Docker安装 74 | 75 | ### linux(Centos、Redhat) 76 | 77 | 1. 安装 78 | 79 | ``` 80 | sudo yum install docker 81 | ``` 82 | 83 | ![install-docker](http://note.youdao.com/yws/public/resource/86feadd8e5ad608ae81a32c1ddc4fc0b/xmlnote/DFE662D5320F4FD48C8DFF8219462909/10270) 84 | 85 | 2. 校验 86 | 87 | ``` 88 | docker -v 89 | ``` 90 | 91 | ![check-dokcer](http://note.youdao.com/yws/public/resource/86feadd8e5ad608ae81a32c1ddc4fc0b/xmlnote/569C9A228C6D41BABCFFABE1A24C3932/10274) 92 | 93 | ### Windows 94 | 95 | **要利用 docker toolbox来安装,国内可以使用阿里云的镜像来下载,下载地址:http://mirrors.aliyun.com/docker-toolbox/windows/docker-toolbox/** 96 | 97 | 98 | 99 | > 安装比较简单,双击运行,点下一步即可,可以勾选自己需要的组件 100 | 101 | ![image](http://note.youdao.com/yws/public/resource/86feadd8e5ad608ae81a32c1ddc4fc0b/xmlnote/5F14773C2CEE44CD87758CC97D041AD0/10315) 102 | 103 | > docker toolbox 是一个工具集,它主要包含以下一些内容: 104 | 105 | - Docker CLI - 客户端,用来运行 docker 引擎创建镜像和容器。 106 | - Docker Machine - 可以让你在 Windows 的命令行中运行 docker 引擎命令。 107 | - Docker Compose - 用来运行 docker-compose 命令。 108 | - Kitematic - 这是 Docker 的 GUI 版本。 109 | - Docker QuickStart shell - 这是一个已经配置好Docker的命令行环境。 110 | - Oracle VM Virtualbox - 虚拟机。 111 | 112 | **安装完成,桌面会出现三个图标。如下:** 113 | 114 | ![image](http://note.youdao.com/yws/public/resource/86feadd8e5ad608ae81a32c1ddc4fc0b/xmlnote/269A3BC5A48C4D11B90FD132FCB9D135/10324) 115 | 116 | 点击 Docker QuickStart 图标来启动 Docker Toolbox 终端。 117 | 118 | **综上所述,简介了docker以及安装步骤,开启你的docker浪漫之旅吧!!!** 119 | 120 | ---- 121 | 122 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   123 | > 124 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   125 | 126 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) -------------------------------------------------------------------------------- /Docker/02. Docker容器基础操作.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | ---- 8 | 9 | # Docker容器基础操作 10 | 11 | **容器(container)是Docker镜像的运行实例,类似于可执行文件与进程的关系,Docker是容器引擎,相当于系统平台。** 12 | 13 | > 容器的生命周期 14 | 15 | ![image](http://note.youdao.com/yws/public/resource/86feadd8e5ad608ae81a32c1ddc4fc0b/xmlnote/820BF7EA957F47E28D6101ADE6E08FC4/10371) 16 | 17 | ## 容器的基础操作 18 | 19 | 以 `tomcat8.0` 为例: 20 | 21 | ```shell 22 | # 拉取tomcat8.0镜像 23 | [root@tudou tudou]# docker pull tomcat:8.0 24 | Using default tag: latest 25 | latest: Pulling from library/tomcat 26 | 57df1a1f1ad8: Pull complete 27 | 71e126169501: Pull complete 28 | 1af28a55c3f3: Pull complete 29 | 03f1c9932170: Pull complete 30 | 881ad7aafb13: Pull complete 31 | 9c0ffd4062f3: Pull complete 32 | bd62e479351a: Pull complete 33 | 48ee8bc64dbc: Pull complete 34 | 6daad3485ea7: Pull complete 35 | bc07a0199230: Pull complete 36 | Digest: sha256:c2b033c9cee06d6a3eb5a4d082935bbb8afee7478e97dcd6bc452bb6ab28da4b 37 | Status: Downloaded newer image for tomcat:8.0 38 | docker.io/library/tomcat:8.0 39 | 40 | # 查看本地镜像 41 | [root@tudou tudou]# docker images | grep tomcat 42 | tomcat 8.0 ef6a7c98d192 2 years ago 356MB 43 | ``` 44 | 45 | ## 运行容器 46 | 47 | ```shell 48 | docker run -d --name tudou-tomcat tomcat:8.0 49 | ``` 50 | 51 | - `-d` : 后台运行 52 | - `--name` : 指定容器的名称(tudou-tomcat)。 53 | - `tomcat:8.0` : 指定镜像名称以及tag(`可以理解为版本`)。 54 | - 执行之后控制台会输出一串id,这是 `容器ID`。 55 | - 执行`docker ps -a | grep tomcat` 即可看到名为`tudou-tomcat` 的容器存在。 56 | 57 | ```shell 58 | # 后台运行一个名为tudou-tomcat的容器 59 | [root@tudou tudou]# docker run -d --name tudou-tomcat tomcat:8.0 60 | 0fe0e4049abe892c4e80afa832eddd7d18b4c805ae2c4272ffdfab8b81df99f8 61 | 62 | # 查看容器 63 | [root@tudou tudou]# docker ps -a | grep tomcat 64 | # 容器ID 镜像名称 运行命令 创建时间 实时状态 端口 容器名称 65 | 0fe0e4049abe tomcat:8.0 "catalina.sh run" 43 seconds ago Up 42 seconds 8080/tcp tudou-tomcat 66 | ``` 67 | 68 | ## 停止容器 69 | 70 | ```shell 71 | docker stop idOrName 72 | # 或者 73 | docker kill idOrName 74 | ``` 75 | 76 | - `stop` : 针对容器的停止操作 77 | - `idOrName` : 容器ID或者名称(名称可能重复,通常采用 `容器ID`) 78 | - `docker kill `: 杀掉一个运行中的容器,结果和停止类似 79 | 80 | ```shell 81 | # 根据名称(tudou-tomcat)停止容器 82 | [root@tudou tudou]# docker stop tudou-tomcat 83 | tudou-tomcat 84 | 85 | # 根据ID(0fe0e4049abe)停止容器 86 | [root@tudou tudou]# docker stop 0fe0e4049abe 87 | 0fe0e4049abe 88 | 89 | # 上述两种均可停止,查看容器:状态变为 90 | [root@tudou tudou]# docker ps -a | grep tomcat 91 | # 容器ID 镜像名称 运行命令 创建时间 实时状态 容器名称 92 | 0fe0e4049abe tomcat:8.0 "catalina.sh run" 9 hours ago Exited (143) 49 seconds ago tudou-tomcat 93 | ``` 94 | 95 | ## 启动容器 96 | 97 | ```shell 98 | docker start idOrName 99 | ``` 100 | 101 | - `start` : 针对容器的启动操作 102 | - `idOrName` : 容器ID或者名称(名称可能重复,通常采用 `容器ID`) 103 | 104 | ```shell 105 | # 根据名称(tudou-tomcat)启动容器 106 | [root@tudou tudou]# docker start tudou-tomcat 107 | tudou-tomcat 108 | 109 | # 根据ID(0fe0e4049abe)启动容器 110 | [root@tudou tudou]# docker start 0fe0e4049abe 111 | 0fe0e4049abe 112 | 113 | # 上述两种均可启动,查看容器:状态变为 且刚启动3秒钟 114 | [root@tudou tudou]# docker ps -a | grep tomcat 115 | # 容器ID 镜像名称 运行命令 创建时间 实时状态 端口 容器名称 116 | 0fe0e4049abe tomcat:8.0 "catalina.sh run" 9 hours ago Up 3 seconds 8080/tcp tudou-tomcat 117 | ``` 118 | 119 | ## 重启容器 120 | 121 | ```shell 122 | docker restart idOrName 123 | ``` 124 | 125 | - `restart` : 针对容器的重启操作 126 | - `idOrName` : 容器ID或者名称(名称可能重复,通常采用 `容器ID`) 127 | 128 | ```shell 129 | # 根据名称(tudou-tomcat)停止容器 130 | [root@tudou tudou]# docker restart tudou-tomcat 131 | tudou-tomcat 132 | 133 | # 根据ID(0fe0e4049abe)停止容器 134 | [root@tudou tudou]# docker restart 0fe0e4049abe 135 | 0fe0e4049abe 136 | 137 | # 上述两种均可启动,查看容器:状态变为 且刚启动1秒钟 138 | [root@tudou tudou]# docker ps -a | grep tomcat 139 | # 容器ID 镜像名称 运行命令 创建时间 实时状态 端口 容器名称 140 | 0fe0e4049abe tomcat:8.0 "catalina.sh run" 9 hours ago Up 1 second 8080/tcp tudou-tomcat 141 | ``` 142 | 143 | ## 进入容器 144 | 145 | ```shell 146 | docker exec -it idOrName bash 147 | ``` 148 | 149 | - `-i`: 交互式操作。 150 | - `-t`: 终端。 151 | - `idOrName`: 容器ID或者名称。 152 | - `bash`:这里我们要进入容器里的shell控制台,因此是bash。 153 | - 进入之后要退出终端,直接输入 `exit`。 154 | - `docker attach ` :也可进入容器,但 `退出之后容器会停止`,因此建议使用 `docker exec` 方式。 155 | 156 | ```shell 157 | # 进入tudou-tomct容器 158 | [root@tudou tudou]# docker exec -it tudou-tomcat bash 159 | # 查看容器内当前目录下的文件 160 | root@0fe0e4049abe:/usr/local/tomcat# ls 161 | # tomcat下的文件 bin webapps ... 162 | LICENSE NOTICE RELEASE-NOTES RUNNING.txt bin conf include lib logs native-jni-lib temp webapps work 163 | # 使用 exit 退出容器, 回到宿主机shell控制台 164 | root@0fe0e4049abe:/usr/local/tomcat# exit 165 | exit 166 | [root@tudou tudou]# 167 | ``` 168 | 169 | ## 导出容器 170 | 171 | ```shell 172 | docker export idOrName > filename.tar 173 | ``` 174 | 175 | - `export` : 容器导出操作。 176 | - `idOrName` : 容器ID或者名称。 177 | - `filename.tar` : 导出的文件名称 178 | 179 | ```shell 180 | # 导出操作 181 | [root@tudou tudou]# docker export tudou-tomcat > tudou-tomcat.tar 182 | [root@tudou tudou]# 183 | # 查看导出的文件 tudou-tomcat.tar 184 | [root@tudou tudou]# ll 185 | total 346392 186 | drwxr-xr-x 4 root users 4096 Jun 9 22:21 es 187 | drwxr-xr-x 2 root users 4096 Jun 6 23:10 server 188 | drwxr-xr-x 3 root users 4096 Jun 7 20:50 tomcat-webapps 189 | # ----- I am here -------- 190 | -rw-r--r-- 1 root root 354687488 Sep 15 23:09 tudou-tomcat.tar 191 | [root@tudou tudou]# 192 | ``` 193 | 194 | ## 导入容器 195 | 196 | ```shell 197 | docker import filename.tar REPOSITORY:TAG 198 | ``` 199 | 200 | - `import` : 容器导出操作。 201 | - `filename.tar` : 导出的文件。 202 | - `REPOSITORY` : 镜像名称 203 | - `TAG` : 镜像tag(`可认为版本`) 204 | 205 | ```shell 206 | # 导入操作 207 | [root@tudou tudou]# docker import tudou-tomcat.tar test/tudou-tomcat:v1 208 | # 查看镜像 209 | [root@tudou tudou]# docker images 210 | # 镜像名称 版本 镜像ID 创建时间 镜像大小 211 | REPOSITORY TAG IMAGE ID CREATED SIZE 212 | test/tudou-tomcat v1 99692189792f 25 seconds ago 345MB 213 | ``` 214 | 215 | ## 删除容器 216 | 217 | ```shell 218 | docker rm idOrName 219 | ``` 220 | 221 | - `rm` :容器删除操作。 222 | - `idOrName` : 容器ID或者名称。 223 | - `rm -f` : 强制删除操作 224 | 225 | ```shell 226 | # 删除操作 227 | [root@tudou tudou]# docker rm -f tudou-tomcat 228 | tudou-tomcat 229 | 230 | # 查看容器 无果 231 | [root@iZ2zed6z27o4rzpwn4kwpqZ liqiao]# docker ps -a | grep tomcat 232 | [root@iZ2zed6z27o4rzpwn4kwpqZ liqiao]# 233 | ``` 234 | 235 | **综上所述,讲述了容器的最基本操作,希望对大家有所帮助!!!!** 236 | 237 | ---- 238 | 239 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   240 | > 241 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   242 | 243 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) -------------------------------------------------------------------------------- /Docker/07. Docker制作镜像.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | ---- 8 | 9 | ## 自定义制作镜像 10 | 11 | 我们学习了Dockerfile语法,那么如何应用Dockerfile制作自定义的镜像呢?那今天我们就来实战一下,以主流的微服务`Jar` 为例子,开启我们自定义制作镜像之旅。 12 | 13 | > 建立简单Springboot项目,并打包成jar 14 | 15 | 1. 简历demo项目(访问路径 /start/springboot) 16 | 17 | ![image](http://note.youdao.com/yws/public/resource/86feadd8e5ad608ae81a32c1ddc4fc0b/xmlnote/D340639AA16948FFB12679E4B87E8E84/11731) 18 | 19 | 2. 配置端口,以及利用maven打出jar包(`tudou-0.0.1-SNAPSHOT.jar`) 20 | 21 | ![image](http://note.youdao.com/yws/public/resource/86feadd8e5ad608ae81a32c1ddc4fc0b/xmlnote/20E02721AD0043F092068DE5C624CEB3/11734) 22 | 23 | 3.运行项目访问(端口为 `8081` ,访问路径 http://127.0.0.1:8081/start/springboot) 24 | 25 | ![image](http://note.youdao.com/yws/public/resource/86feadd8e5ad608ae81a32c1ddc4fc0b/xmlnote/B3EC0D5E805A482C80177F7E31D31A99/11870) 26 | 27 | 28 | > 制作镜像 29 | 30 | 1. 在装有`docker`的机器上,新建一个目录(`tudou`) 31 | 32 | ```shell 33 | # 创建一个目录(tudou) 34 | [root@tudou tudou]# mkdir tudou 35 | [root@tudou tudou]# cd tudou 36 | [root@tudou tudou]# pwd 37 | /home/tudou/tudou 38 | ``` 39 | 40 | 2. 上传`Jar`(`tudou-0.0.1-SNAPSHOT.jar`)到当前目录(`tudou`)下 41 | 42 | ```shell 43 | # 查看Jar(tudou-0.0.1-SNAPSHOT.jar) 44 | [root@tudou tudou]# pwd 45 | /home/tudou/tudou 46 | [root@tudou tudou]# ls 47 | tudou-0.0.1-SNAPSHOT.jar 48 | [root@tudou tudou]# 49 | ``` 50 | 51 | 3. 在当前目录下建立dockerfile文件 52 | 53 | ```shell 54 | # 查看dockerfile文件 55 | [root@tudou tudou]# ll 56 | total 15768 57 | -rw-r--r-- 1 root root 0 Oct 18 10:03 dockerfile 58 | -rw-r--r-- 1 root root 16143518 Oct 18 09:58 tudou-0.0.1-SNAPSHOT.jar 59 | ``` 60 | 61 | 4. 编写dockerfile 62 | 63 | ```shell 64 | # 查看dockerfile内容 65 | [root@tudou tudou]# cat dockerfile 66 | # 使用 openjdk:8-jdk-alpine环境 为基础镜像 67 | FROM openjdk:8-jdk-alpine 68 | # 镜像作者 69 | MAINTAINER tudou 70 | # 设置时区为 Asia/Shanghai 71 | RUN echo "Asia/Shanghai" > /etc/timezone 72 | # 增加jar 73 | ADD tudou-0.0.1-SNAPSHOT.jar /home/tudou/tudou.jar 74 | # 说明要暴露的端口 75 | EXPOSE 8081 76 | # 容器启动执行的命令。也可(CMD java -jar /home/tudou/tudou.jar) 77 | ENTRYPOINT ["java","-jar","/home/tudou/tudou.jar"] 78 | ``` 79 | 80 | * `FROM`: 使用 openjdk:8-jdk-alpine环境 为基础镜像,如果镜像不是本地的会从 DockerHub 进行下载,openjdk:8-jdk-alpine 镜像小,推荐使用 81 | * `MAINTAINER`: 镜像作者 82 | * `RUN`: 设置时区为 Asia/Shanghai 83 | * `ADD`: 拷贝jar(`tudou-0.0.1-SNAPSHOT.jar`)并且重命名`tudou.jar` 84 | * `EXPOSE`: 声明提供服务端口。帮助镜像使用者理解这个镜像服务的守护端口,且在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口。 85 | * `ENTRYPOINT`: 指定容器启动执行的命令 86 | 87 | 5. 制作镜像 88 | 89 | ```shell 90 | # 这里为了下一步推送所以名称带上了镜像中心地址:端口号 91 | # 若无须推送则可 docker build -t tudou:v1 . 92 | [root@tudou tudou]# docker build -t x.x.x.x:8080/tudou/tudou:v1 . 93 | Sending build context to Docker daemon 16.15MB 94 | Step 1/6 : FROM openjdk:8-jdk-alpine 95 | ---> a3562aa0b991 96 | Step 2/6 : MAINTAINER tudou 97 | ---> Running in 3136db0cd8e6 98 | Removing intermediate container 3136db0cd8e6 99 | ---> edc19b0100db 100 | Step 3/6 : RUN echo "Asia/Shanghai" > /etc/timezone 101 | ---> Running in 30d37bf618d7 102 | Removing intermediate container 30d37bf618d7 103 | ---> 3be349b3bbae 104 | Step 4/6 : ADD tudou-0.0.1-SNAPSHOT.jar /home/tudou/tudou.jar 105 | ---> 4053de23bdb5 106 | Step 5/6 : EXPOSE 8081 107 | ---> Running in 25db3b7f5827 108 | Removing intermediate container 25db3b7f5827 109 | ---> d2abf4dcf4b5 110 | Step 6/6 : ENTRYPOINT ["java","-jar","/home/tudou/tudou.jar"] 111 | ---> Running in 9165e0850ea1 112 | Removing intermediate container 9165e0850ea1 113 | ---> c33d1214c2aa 114 | Successfully built c33d1214c2aa 115 | Successfully tagged x.x.x.x:8080/tudou/tudou:v1 116 | [root@tudou tudou]# docker images | grep tudou 117 | x.x.x.x:8080/tudou/tudou v1 c33d1214c2aa 12 seconds ago 121MB 118 | x.x.x.x:8080/tudou/tomcat tudou ef6a7c98d192 2 years ago 356MB 119 | [root@tudou tudou]# 120 | ``` 121 | 122 | 6.推送到镜像中心(顺带复习镜像操作) 123 | 124 | ```shell 125 | [root@tudou tudou]# docker login x.x.x.x:8080 -u admin -p Harbor12345 126 | WARNING! Using --password via the CLI is insecure. Use --password-stdin. 127 | WARNING! Your password will be stored unencrypted in /root/.docker/config.json. 128 | Configure a credential helper to remove this warning. See 129 | https://docs.docker.com/engine/reference/commandline/login/#credentials-store 130 | 131 | Login Succeeded 132 | [root@tudou tudou]# docker push x.x.x.x:8080/tudou/tudou:v1 133 | The push refers to repository [x.x.x.x:8080/tudou/tudou] 134 | 78f839ad322c: Pushed 135 | 718b90d2941d: Pushed 136 | ceaf9e1ebef5: Pushed 137 | 9b9b7f3d56a0: Pushed 138 | f1b5933fe4b5: Pushed 139 | v1: digest: sha256:548796ed0dc545539b5d854812badc40cda2072cca748b17fc7a4658ec1aad19 size: 1366 140 | [root@tudou tudou]# 141 | ``` 142 | 7. 镜像中心查看 143 | 144 | - 查看项目 145 | 146 | ![image](http://note.youdao.com/yws/public/resource/86feadd8e5ad608ae81a32c1ddc4fc0b/xmlnote/0F40922ECBE14AC0B0427ADA8DBD1425/11826) 147 | 148 | - 查看镜像 149 | 150 | ![image](http://note.youdao.com/yws/public/resource/86feadd8e5ad608ae81a32c1ddc4fc0b/xmlnote/8E79011B53584624AF9C7C52D368C3D3/11828) 151 | 152 | **到此我们Jar包的镜像制作就讲述完毕!!!** 153 | 154 | > 用自己的镜像,创建容器 155 | 156 | 1. 运行容器 157 | 158 | ```shell 159 | # 映射jar端口8081到宿主机8080 160 | [root@tudou tudou]# docker run -it -d -p8080:8081/tcp --name tudou-jar x.x.x.x:8080/tudou/tudou:v1 161 | 330e3a8d85501d44c19c24e4d0f9dfd3cc88259ded522489057e6d72a454014d 162 | # 查看容器是否存在 163 | [root@tudou tudou]# docker ps -a | grep tudou-jar 164 | 330e3a8d8550 x.x.x.x:8080/tudou/tudou:v1 "java -jar /home/tud…" 2 minutes ago Up 2 minutes 0.0.0.0:8080->8081/tcp tudou-jar 165 | # 查看主机端口8080是否监听状态 166 | [root@tudou tudou]# netstat -ano | grep 8080 167 | tcp6 0 0 :::8080 :::* LISTEN off (0.00/0/0) 168 | ``` 169 | 170 | 2.进入容器,查看运行的进程 171 | 172 | ```shell 173 | # 进入容器 174 | [root@tudou tudou]# docker exec -it tudou-jar sh 175 | # 查看进程 176 | / # ps -ef | grep -v grep | grep jar 177 | 1 root 0:09 java -jar /home/tudou/tudou.jar 178 | / # 179 | ``` 180 | 181 | 2.校验正确性(访问http://x.x.x.x:8080/start/springboot) 182 | 183 | ![image](http://note.youdao.com/yws/public/resource/86feadd8e5ad608ae81a32c1ddc4fc0b/xmlnote/0595239972F94BD1B0DEF2660A875CE1/11856) 184 | 185 | **综上所述,我们讲述了镜像的诞生,从 `项目源代码` 到 `docker镜像` 再到 `容器` 的一系列操作,同时也回顾了 `docker镜像` 以及 `docker容器` 的一些操作,希望对大家有所帮助,你是否已经跃跃欲试了,那就```开启你制作镜像的旅程吧!!!```* 186 | 187 | ---- 188 | 189 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   190 | > 191 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   192 | 193 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) -------------------------------------------------------------------------------- /JVM/01. Java 内存区域与内存溢出异常.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | ---- 8 | 9 | 本文参考自《深入理解Java虚拟机》一书。主要总结一下java虚拟机内存的各个区域,以及这些区域的作用、服务对象以及其中可能产生的问题。 10 | 11 | ## 1. 运行时数据区域 12 | Java虚拟机在执行 Java 程序的过程中会把它说管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。具体如下图所示: 13 | 14 | ![运行时数据区域](https://img-blog.csdnimg.cn/20201204234657959.png) 15 | 16 | 结合这张图,下面逐个来分析一下每个数据区域的特点。 17 | 18 | ### 1.1 程序计数器(Program Counter Register) 19 | 20 | **程序计数器**是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。啥意思呢?我们知道,CPU的计算时间是以分片的方式给到每个线程的(换句话说,所谓的并行其实本质上还是串行),比如线程A执行到了一个地方,CPU将控制权给了线程B,那么线程A重新获得CPU的资源时,如何恢复到刚才执行的地方呢?这就是程序计数器要干的事了!它能帮助线程A找到刚刚执行的地方,从而继续刚刚的执行。 21 | 22 | 为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储。所以程序计数器是**线程私有**的。 23 | 24 | 另外,程序计数器是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。 25 | 26 | ### 1.2 Java虚拟机栈 27 | 28 | Java虚拟机栈所占有的内存空间也就是我们平时所说的“栈内存”,并且也是**线程私有**的,生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时,都会创建一个栈帧,用于存储局部变量表(基本数据类型,**对象的引用**和returnAddress类型)、操作数栈、动态链接、方法出口等信息。 29 | 30 | 局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。 31 | 32 | 每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。对于Java虚拟机栈,有两种异常情况: 33 | 34 | >1)如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
35 | >2)如果虚拟机栈在动态扩展时,无法申请到足够的内存,就会抛出OutOfMemoryError; 36 | 37 | ### 1.3 本地方法栈 38 | 39 | 本地方法栈和虚拟机栈所发挥的作用非常相似,它们之间的区别主要是,虚拟机栈是为虚拟机执行Java方法(也就是字节码)服务的,而本地方法栈则为虚拟机使用到的Native方法服务。 40 | 41 | 与虚拟机栈类似,本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。 42 | 43 | ### 1.4 Java堆 44 | 45 | Java堆是Java虚拟机所管理的内存中最大的一块。Java堆在主内存中,是**被所有线程共享**的一块内存区域,其随着JVM的创建而创建,**堆内存的唯一目的是存放对象实例和数组**。同时Java堆也是GC管理的主要区域。 46 | 47 | Java堆物理上不需要连续的内存,只要逻辑上连续即可。如果堆中没有内存完成实例分配,并且也无法再扩展时,将会抛出OutOfMemoryError异常。 48 | 49 | ### 1.5 方法区 50 | 51 | 方法去是**所有线程共享**的一个内存区域。用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区也有一个别名叫做Non-Heap(非堆),用于与Java堆区分。对于HotSpot虚拟机来说,方法区又习惯称为“永久代”(Permancent Generation),但这只是对于HotSpot虚拟机来说的,其他虚拟机的实现上并没有这个概念。相对而言,垃圾收集行为在这个区域比较少出现,但也并非不会来收集,**这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载上**。 52 | 53 | 根据Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError 异常。 54 | 55 | ### 1.6 运行时常量池 56 | 57 | 运行时常量池属于方法区。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量表,用于存放编译期生成的各种字面常量和符号引用,这部分内容将在**类加载后进入方法区**的运行时常量池中存放(JDK1.7开始,常量池已经被移到了堆内存中了)。也就是说,这部分内容,在编译时只是放入到了常量池信息中,到了加载时,才会放到运行时常量池中去。运行时常量池县归于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区的运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用的比较多的是String类的intern()方法。 58 | 59 | 当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常,常量池属于方法区,同样可能抛出OutOfMemoryError异常。 60 | 61 | 下面针对内存区域模型做一个小结。 62 | 63 | ## 2. java内存区域模型总结 64 | |内存区域|线程私有|主要作用|溢出异常| 65 | |:--:|:--:|:--|:--:| 66 | |程序计数器|是|记录当前线程执行的位置|无异常| 67 | |虚拟机栈|是|存储局部变量表(基本数据类型,**对象的引用**和returnAddress类型)、操作数栈、动态链接、方法出口等信息(java方法)|StackOverflowError和OutOfMemoryError| 68 | |本地方法栈|是|和虚拟机栈相似,区别本地方法栈为虚拟机使用到的Native方法服务|StackOverflowError和OutOfMemoryError| 69 | |堆|否|存放对象实例和数组|OutOfMemoryError| 70 | |方法区|否|用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据|OutOfMemoryError | 71 | 72 | ---- 73 | 74 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   75 | > 76 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   77 | 78 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 79 | 80 | ---- -------------------------------------------------------------------------------- /JVM/02. Java 虚拟机的类加载机制.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | ---- 8 | 9 | 这篇博文主要来总结一下java虚拟机加载一个类的过程,部分参考自《深入理解Java虚拟机》。 10 | 11 | ## 1. 一个经典的面试题 12 | 为了避免枯燥的解说,为了让读者在读完本文后能彻底理解类加载的过程,首先来看一段 Java 代码,我们从一个例子入手: 13 | 14 | ```java 15 | //ClassLoaderProcess.java文件 16 | class Singleton { 17 | private static Singleton singleton = new Singleton(); 18 | public static int count_1; 19 | public static int count_2 = 0; 20 | 21 | static { 22 | count_1++; 23 | count_2++; 24 | } 25 | 26 | private Singleton() { 27 | count_1++; 28 | count_2++; 29 | } 30 | 31 | public static Singleton getInstance() { 32 | return singleton; 33 | } 34 | } 35 | 36 | public class ClassLoaderProcess { 37 | 38 | public static void main(String[] args) { 39 | System.out.println(Singleton.count_1); 40 | System.out.println(Singleton.count_2); 41 | } 42 | } 43 | ``` 44 | Singleton是个单例模式的类,里面有两个静态变量,在静态代码块中对两个静态变量做自增运算,在私有构造方法中,再对这两个静态变量做自增运算,最后打印出来的结果是多少呢?先说一下正确答案不是2和2,而是2和1。我们带着这个问题去分析虚拟机是如何加载一个类的(如果对虚拟机加载类的过程已经很清楚了,就可以不用往下看了)。看完本文,相信你也会从虚拟机加载类的过程中来分析这段java代码了。 45 | 46 | ## 2. 虚拟机加载类的过程 47 | ### 2.1 类的生命周期 48 | ![类的加载过程](https://img-blog.csdnimg.cn/img_convert/ae91b8a134a525ec1283309ec5f7c34f.png) 49 | 50 | 上图表示一个类的生命周期图。类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、连接(验证、准备、解析)、初始化、使用和卸载7个阶段。其中加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的开始,而解析阶段则不一定:它再某些情况下可以在初始化阶段之后再开始,这是为了支持java语言的运行时绑定。下面来逐个分析一下类加载的各个过程。 51 | 52 | ### 2.2 加载 53 | 54 | 我们知道,程序要加载到内存中才可以执行,什么情况下需要开始类加载过程的第一阶段:加载呢?java虚拟机规范中并没有进行强制约定,这点可以交给虚拟机的具体实现来自由把握。 55 | 56 | 在加载阶段,java虚拟机需要完成以下3件事情: 57 | >1. 通过一个类的全限定名来获取定义此类的二进制字节流
58 | >2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
59 | >3. 在内存中(堆中)生成一个代表这个类的java.lang.Class对象,用来封装类在方法区里的数据结构,作为方法区中这个类的各种数据的访问入口 60 | 61 | 从这三个步骤中可以很明显的看出,我们可以通过这个Class来获取类的各种数据,它就像是一面镜子,可以反射出类的信息来,所以也就明白了在用反射的时候为什么要使用Class了。 62 | 63 | ### 2.3 验证 64 | 65 | 验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。 66 | 67 | 一般我们都是通过java文件编译生成的class文件,这是没有什么问题的,但是class文件并不一定要求用java源码编译而来,可以使用很多其它的途径,比如用十六进制编辑器直接编写来产生class文件。虚拟机如果不检查输入的字节流,对其完全信任的话,可能就会因为载入了有害的字节流而导致系统崩溃,所以验证是虚拟机的一项重要的工作。 68 | 69 | ### 2.4 准备 70 | 71 | 接下来就是连接的第二步:准备了。**准备阶段是正式为类变量分配内存并设置类变量的初始值的阶段**。这里有两个概念要搞清楚: 72 | >1. 类变量:即被static修饰的静态变量。
73 | >2. 初始值:指的是该数据类型所对应的“零值”
74 | 75 | 所以也就是说,**准备阶段是为静态变量分配内存,并且对其初始化为零值**。不包括静态代码块和实例变量,静态代码块在下面的初始化阶段执行,实例变量将会在对象实例化的时候随着对象一起分到到java堆中的。例如: 76 | 77 | ```java 78 | public static int value = 123; 79 | ``` 80 | 在准备阶段,value的值为0,并非123!当然咯,如果是boolean型数据,则为false。零值是针对具体类型来说的。 81 | 82 | ### 2.5 解析 83 | 84 | **解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程**,这个符号引用和直接引用有什么关联呢? 85 | >1. 符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局是无关的,引用的目标不一定已经加载到内存中。
86 | >2. 直接引用:指的是直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,引用的目标必定已经在内存中存在。 87 | 88 | ### 2.6 初始化 89 | 90 | 初始化是类加载过程的最后一步,在前面的过程中,除了第一步加载阶段用户可以通过指定自定义类加载器参与外,其余过程完全是由虚拟机自己主导控制的。到了初始化阶段,才真正开始执行类中定义的java程序代码了(或者说是字节码)。 91 | 92 | 由上面分析可知,在准备阶段,静态变量已经赋过一次值了,只不过是系统要求的初始值而已,而**在初始化阶段,为类的静态变量赋予程序中指定的初始值,还有执行静态代码块中的程序**。 93 | 94 | 关于类的初始化这个阶段,可以再分析的深入一点,刚刚说初始化的阶段是为类的静态变量赋实际值的阶段,我们也可以从另外的一个角度去表达:**初始化阶段是执行类构造器方法(注意:不是我们平时说的类的构造方法)的过程**,构造器方法是`()`方法,**它是由编译器自动收集类中所有的静态变量的赋值动作和静态代码块中的语句合并产生的**,所以也就清楚了,为啥初始化阶段也可以叫做类构造器方法执行的过程。 95 | 96 | 这里需要注意的是,编译器收集的顺序是由语句在程序中出现的顺序所决定的,静态代码块中只能访问到定义在静态代码块之前的变量,定义在它之后的变量,在前面的静态代码块中可以赋值,但是不能访问。可以举个例子: 97 | 98 | ```java 99 | public class Test { 100 | static { 101 | i = 0; //给变量赋值可以正常通过编译 102 | System.out.print(i); //但是不能访问,这句编译会提示非法向前引用 103 | } 104 | static int i = 1; 105 | ``` 106 | 107 | `()`方法与类的构造函数不同,它不需要显示的调用父类的构造器,虚拟机会保证在子类的`()`方法执行前,父类的`()`方法已经执行完毕,所以在虚拟机中第一个被执行的`()`方法的类肯定是java.lang.Object。 108 | 109 | 由于父类的`()`方法先执行,也就意味着父类中定义的静态代码块要优先于子类的静态变量赋值操作,看一个例子: 110 | 111 | ```java 112 | //演示虚拟机方法执行的过程 113 | public class CinitMethod { 114 | 115 | static class Parent{ 116 | public static int A = 1; 117 | static { 118 | A = 2; 119 | } 120 | } 121 | 122 | static class Sub extends Parent { 123 | public static int B = A; 124 | } 125 | 126 | public static void main(String[] args) { 127 | System.out.println(Sub.B); 128 | } 129 | } 130 | ``` 131 | 132 | 这段程序中,在准备阶段,先将A赋为0,B赋为0,在初始化阶段,先执行父类的`()`方法,所以会执行A=1;然后A=2,然后执行子类的`()`方法,执行B=A,所以打印出来是2。 133 | 134 | 虚拟机会保证一个类的`()`方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的`()`方法,其它线程都需要阻塞等待,直到活动线程执行完该方法。 135 | 136 | 到这里,一个类的加载过程就算完毕了,**类加载的最终产品是位于堆中的Class对象,封装了类在方法区内的数据结构,并向java程序员提供了访问方法区内数据结构的接口**。所以程序员就可以使用可以使用这个类去获取与该类相关的信息了。 137 | 138 | 要注意的是,这是类加载完毕了,跟类的对象是没有关系的,到目前只能使用类的静态变量和静态方法,类的对象需要我们去产生的,有了对象才能操作其中的普通成员变量和方法。 139 | 140 | 现在再去看文章开头的那段java代码应该很简单了 141 | 142 | >1. 在准备阶段,java虚拟机将Singleton赋为空,count_1和count_2赋为0(count_2赋为0不是程序中赋的0,是int的默认值)。
143 | >2. 在初始化阶段,java虚拟机按照顺序执行static代码,
144 | >首先实例化Singleton,执行构造方法中的代码,count_1和cout_2变成1;
145 | >然后按顺序执行static代码,count_1没有赋值,还是1,count_2被赋值为0;
146 | >最后执行静态代码块中的代码,count_1和count_2各自增1,所以count_1=2,count_2=1。 147 | 148 | 分析完毕。 149 | 150 | ---- 151 | 152 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   153 | > 154 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   155 | 156 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 157 | 158 | ---- -------------------------------------------------------------------------------- /JavaBase/01. Java 数据类型.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | ----- 8 | Java 数据类型有很多,本文主要从基本类型、包装类型、引用类型和缓存池四个方面来总结。 9 | 10 | 11 | ### 基本数据类型 12 | 13 | 基本数据类型有 byte、short、int、long、float、double、boolean、char,关于它们的分类,我画了个图。 14 | 15 | ![基本类型](https://img-blog.csdnimg.cn/20200601102739431.png) 16 | 17 | 接下来我主要从字节数、数据范围、默认值、以及用途等方面给大家总结成一个表格,一目了然。 18 | 19 | | 数据类型 | 字节数 | 位数 | 最小值 | 最大值 | 默认值 |用途| 20 | | :-------: | :------: | :------: | :------: | :----: | :------: |:---| 21 | | byte | 1 | 8 | -128 | 127 |0|byte 类型用在大型数组中节约空间,主要代替整数。因为 byte 变量占用的空间只有 int 类型的四分之一| 22 | | short | 2 | 16 | -32768 | 32767 |0|Short 数据类型也可以像 byte 那样节省空间。一个short变量是int型变量所占空间的二分之一| 23 | | int | 4 | 32 |-2^31 |2^31 - 1 |0|一般地整型变量默认为 int 类型| 24 | | long | 8 | 64 | -2^63 | 2^63-1 |0L|这种类型主要使用在需要比较大整数的系统上| 25 | | float | 4 | 32 | 1.4e-45 | 3.4e38 |0.0F|float 在储存大型浮点数组的时候可节省内存空间。浮点数不能用来表示精确的值,如货币| 26 | | double | 8 | 64 | 4.9e-324 | 1.8e308 |0.0D|浮点数的默认类型为double类型。double类型同样不能表示精确的值,如货币| 27 | | boolean | × | × | × | × |false|true和false| 28 | | char | 2 | 16 | \u0000 | \uffff ||char 数据类型可以储存任何字符| 29 | 30 | ### 包装数据类型 31 | 32 | 上面提到的基本类型都有对应的包装类型,为了方便读者查看,我也整了一个表格。 33 | 34 | | 基本类型 | 包装类型 | 最小值 | 最大值 | 35 | | :-------: | :------: | :------: | :----: | 36 | |byte|Byte|`Byte.MIN_VALUE`|`Byte.MAX_VALUE`| 37 | |short|Short|`Short.MIN_VALUE`|`Short.MAX_VALUE`| 38 | |int|Integer|`Integer.MIN_VALUE`|`Integer.MAX_VALUE`| 39 | |long|Long|`Long.MIN_VALUE`|`Long.MAX_VALUE`| 40 | |float|Float|`Float.MIN_VALUE`|`Float.MAX_VALUE`| 41 | |double|Double|`Double.MIN_VALUE`|`Double.MAX_VALUE`| 42 | |boolean|Boolean|`Boolean.MIN_VALUE`|`Boolean.MAX_VALUE`| 43 | |char|Char|`Char.MIN_VALUE`|`Char.MAX_VALUE`| 44 | 45 | 46 | 47 | ### 引用类型 48 | 49 | 在Java中,引用类型的变量非常类似于 C/C++ 的指针。引用类型指向一个对象,指向对象的变量是引用变量。这些变量在声明时被指定为一个特定的类型,比如 Student、Dog 等。变量一旦声明后,类型就不能被改变了。 50 | 51 | 对象、数组都是引用数据类型。所有引用类型的默认值都是null。一个引用变量可以用来引用任何与之兼容的类型。例如: 52 | 53 | ```java 54 | Dog dog = new Dog("旺财")。 55 | ``` 56 | 57 | 58 | 59 | ### 数据类型转换 60 | 61 | 包装类型和基本类型之间如何转化呢? 62 | 63 | ```java 64 | Integer x = 2; // 装箱 调用了 Integer.valueOf(2) 65 | int y = x; // 拆箱 调用了 X.intValue() 66 | ``` 67 | 基本类型之间如何转换呢?有两个点: 68 | 69 | > 1. 把大容量的类型转换为小容量的类型时必须使用强制类型转换。 70 | > 2. 把小容量的类型转换为大容量的类型可以自动转换。 71 | 72 | 比如: 73 | 74 | ```java 75 | int i =128; 76 | byte b = (byte)i; 77 | long c = i; 78 | ``` 79 | 80 | 81 | 82 | ### 缓存池 83 | 84 | 大家思考一个问题:`new Integer(123)` 与` Integer.valueOf(123)` 有什么区别? 85 | 86 | 有些人可能知道,有些人可能不知道。其实他们的区别很大。 87 | >1. new Integer(123) 每次都会新建一个对象; 88 | >2. Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。 89 | 90 | 我写个demo大家就知道了 91 | 92 | ```java 93 | Integer x = new Integer(123); 94 | Integer y = new Integer(123); 95 | System.out.println(x == y); // false 96 | Integer z = Integer.valueOf(123); 97 | Integer k = Integer.valueOf(123); 98 | System.out.println(z == k); // true 99 | ``` 100 | 101 | 编译器会在自动装箱过程调用`valueOf()`方法,因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建,那么就会引用相同的对象。如: 102 | ```java 103 | Integer m = 123; 104 | Integer n = 123; 105 | System.out.println(m == n); // true 106 | ``` 107 | `valueOf()`方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。我们看下源码就知道。 108 | 109 | ```java 110 | public static Integer valueOf(int i) { 111 | if (i >= IntegerCache.low && i <= IntegerCache.high) 112 | return IntegerCache.cache[i + (-IntegerCache.low)]; 113 | return new Integer(i); 114 | } 115 | ``` 116 | 根据数据类型的不一样,这个缓存池的上下限也不同,比如这个 Integer,就是 -128~127。不过这个上界是可调的,在启动 jvm 的时候,通过 -XX:AutoBoxCacheMax= 来指定这个缓冲池的大小,该选项在 JVM 初始化的时候会设定一个名为 java.lang.IntegerCache.high 系统属性,然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界。 117 | 118 | 参考自:[StackOverflow : Differences between new Integer(123), Integer.valueOf(123) and just 123](https://stackoverflow.com/questions/9030817/differences-between-new-integer123-integer-valueof123-and-just-123) 119 | 120 |   121 | 122 | OK,这篇文章就分享到这,Java 的数据类型虽然简单,但是里面还是有很多小细节值得我们玩味的,希望这篇文章能给大家带来一些帮助。 123 | 124 |   125 | 126 | ------ 127 |    128 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   129 | > 130 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   131 | 132 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 133 | -------------------------------------------------------------------------------- /JavaBase/03. Java 的三大特性—封装、继承、多态.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | ---- 8 | ## 封装 9 | 10 | ### public、private、protected修饰符 11 | 12 | 说到封装,Java 中有三个访问权限修饰符:private、protected 以及 public,如果不加访问修饰符,表示包级可见。我们先来看下这三个修饰符的作用。 13 | 14 | |修饰符|当前类|同包|子类|其他包| 15 | |:--:|--|--|--|--| 16 | |public|√|√|√|√| 17 | |protected|√|√|√|×| 18 | |default|√|√|×|×| 19 | |private|√|×|×|×| 20 | 21 | 设计良好的模块会隐藏所有的实现细节,把它的 API 与它的实现清晰地隔离开来。模块之间只通过它们的 API 进行通信,一个模块不需要知道其他模块的内部工作情况,这个概念被称为信息隐藏或封装。因此访问权限应当尽可能地使每个类或者成员不被外界访问。 22 | 23 | 如果子类的方法重写了父类的方法,那么子类中该方法的访问级别不允许低于父类的访问级别。这是为了确保可以使用父类实例的地方都可以使用子类实例去代替,也就是确保满足里氏替换原则。 24 | 25 | ### 规范 26 | 27 | 字段决不能是公有的,因为这么做的话就失去了对这个字段修改行为的控制,客户端可以对其随意修改。例如下面的例子中,AccessExample 拥有 id 公有字段,如果在某个时刻,我们想要使用 int 存储 id 字段,那么就需要修改所有的客户端代码。 28 | ```java 29 | public class AccessExample { 30 | public String id; 31 | } 32 | ``` 33 | 34 | 可以使用公有的 getter 和 setter 方法来替换公有字段,这样的话就可以控制对字段的修改行为。 35 | 36 | ```java 37 | public class AccessExample { 38 | 39 | private int id; 40 | 41 | public String getId() { 42 | return id + ""; 43 | } 44 | 45 | public void setId(String id) { 46 | this.id = Integer.valueOf(id); 47 | } 48 | } 49 | ``` 50 | ### 封装的好处 51 | 52 | 1. 提高数据的安全性。 53 | 2. 操作简单。 54 | 3. 隐藏了实现。 55 | 56 | ## 继承 57 | 58 | 继承是类与类的一种关系,是一种“is a”的关系。比如“狗”继承“动物”,这里动物类是狗类的父类或者基类,狗类是动物类的子类或者派生类。如下图所示: 59 | 60 | ![](https://images2015.cnblogs.com/blog/1189312/201707/1189312-20170701123011243-2128400556.png) 61 | 62 | 注:java中的继承是单继承,即一个类只有一个父类。 63 | 64 | ### 继承的初始化顺序 65 | 66 | 1. 初始化父类再初始化子类 67 | 2. 先执行初始化对象中属性,再执行构造方法中的初始化。 68 | 69 | 基于上面两点,我们就知道实例化一个子类,java程序的执行顺序是: 70 | >父类对象属性初始化---->父类对象构造方法---->子类对象属性初始化--->子类对象构造方法    71 | 72 | 继承这块大家都比较熟,我来举个例子: 73 | 74 | ### 继承示例 75 | Animal动物: 76 | ```java 77 | public class Animal { 78 | /**名称*/ 79 | public String name; 80 | /**颜色*/ 81 | public String color; 82 | 83 | /**显示信息*/ 84 | public void show(){ 85 | System.out.println("名称:"+name+",颜色:"+color); 86 | } 87 | } 88 | ``` 89 | Dog狗: 90 | ```java 91 | /**狗继承自动物,子类 is a 父类*/ 92 | public class Dog extends Animal { 93 | /**价格*/ 94 | public double price; 95 | } 96 | ``` 97 | dog并没有定义color属性,但在使用中可以调用,是因为dog继承了父类Animal,父类的非私有成员将被子类继承。如果再定义其它的动物类则无须再反复定义name,color与show方法。 98 | 99 | ## 多态 100 | 101 | 多态这块,我要跟大家好好唠叨唠叨,首先来看下概念: 102 | ### 多态概念 103 | **多态**:一个对象具备多种形态。 104 | ①父类的引用类型变量指向了子类的对象 105 | ②接口的引用类型变量指向了接口实现类的对象 106 | 107 | **多态的前提**:必须存在继承或者实现关系。 108 | ```java 109 | 动物 a = new 狗(); 110 | ``` 111 | ### 多态要注意的细节 112 | 113 | 1. 多态情况下,子父类存在同名的成员变量时,访问的是父类的成员变量。 114 | 2. 多态情况下,子父类存在同名的非静态的成员函数时,访问的是子类的成员函数。 115 | 3. 多态情况下,子父类存在同名的静态的成员函数时,访问的是父类的成员函数。 116 | 4. 多态情况下,不能访问子类特有的成员。 117 | 118 | **总结**:多态情况下,子父类存在同名的成员时,访问的都是父类的成员,除了在同名非静态函数时才是访问子类的。 119 | 120 | >注意:java编译器在编译的时候,会检查引用类型变量所属的类是否具备指定的成员,如果不具备马上编译报错。(编译看左边) 121 | 122 | ### 多态的应用 123 | 124 | 1. 多态用于形参类型的时候,可以接收更多类型的数据。 125 | 2. 多态用于返回值类型的时候,可以返回更多类型的数据。 126 | 127 | **多态的好处**: 提高了代码的拓 展性。 128 | 129 | 我们想想,平时写MVC三层模型的时候,service为什么要写接口呢?因为可能有个service会有多种不同的实现。service就是我们平时用多态最多的地方。 130 | 131 | ----- 132 | 133 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   134 | > 135 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   136 | 137 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) -------------------------------------------------------------------------------- /JavaBase/04. 你真的了解 Java 中的 String 吗.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | ----- 8 | 9 | ## 1. 看看源码 10 | 11 | 大家都知道, String 被声明为 final,因此它不可被继承。(Integer 等包装类也不能被继承)。我们先来看看 String 的源码。 12 | 13 | 在 Java 8 中,String 内部使用 char 数组存储数据。 14 | ```java 15 | public final class String 16 | implements java.io.Serializable, Comparable, CharSequence { 17 | /** The value is used for character storage. */ 18 | private final char value[]; 19 | } 20 | ``` 21 | 22 | 在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 coder 来标识使用了哪种编码。 23 | 24 | ```java 25 | public final class String 26 | implements java.io.Serializable, Comparable, CharSequence { 27 | /** The value is used for character storage. */ 28 | private final byte[] value; 29 | 30 | /** The identifier of the encoding used to encode the bytes in {@code value}. */ 31 | private final byte coder; 32 | } 33 | ``` 34 | value 数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变。 35 | 36 | ## 2. 不可变有什么好处呢 37 | 38 | ### 2.1 可以缓存 hash 值 39 | 40 | 因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。 41 | 42 | ### 2.2 String Pool 的使用 43 | 44 | 如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。 45 | 46 | ### 2.3 安全性 47 | 48 | String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 的那一方以为现在连接的是其它主机,而实际情况却不一定是。 49 | 50 | ### 2.4 线程安全 51 | 52 | String 不可变性天生具备线程安全,可以在多个线程中安全地使用。 53 | 54 | ## 3. 再来深入了解一下 String 55 | 56 | ### 3.1 "+" 连接符 57 | 58 | 字符串对象可以使用“+”连接其他对象,其中字符串连接是通过 StringBuilder(或 StringBuffer)类及其 append 方法实现的,对象转换为字符串是通过 toString 方法实现的。可以通过反编译验证一下: 59 | ```java 60 | /** 61 | * 测试代码 62 | */ 63 | public class Test { 64 | public static void main(String[] args) { 65 | int i = 10; 66 | String s = "abc"; 67 | System.out.println(s + i); 68 | } 69 | } 70 | 71 | /** 72 | * 反编译后 73 | */ 74 | public class Test { 75 | public static void main(String args[]) { //删除了默认构造函数和字节码 76 | byte byte0 = 10; 77 | String s = "abc"; 78 | System.out.println((new StringBuilder()).append(s).append(byte0).toString()); 79 | } 80 | } 81 | 82 | ``` 83 | 由上可以看出,Java中使用"+"连接字符串对象时,会创建一个StringBuilder()对象,并调用append()方法将数据拼接,最后调用toString()方法返回拼接好的字符串。那这个 "+" 的效率怎么样呢? 84 | 85 | ### 3.2 “+”连接符的效率 86 | 87 | 使用“+”连接符时,JVM会隐式创建StringBuilder对象,这种方式在大部分情况下并不会造成效率的损失,不过在进行大量循环拼接字符串时则需要注意。比如: 88 | 89 | ```java 90 | String s = "abc"; 91 | for (int i=0; i<10000; i++) { 92 | s += "abc"; 93 | } 94 | ``` 95 | 这样由于大量StringBuilder创建在堆内存中,肯定会造成效率的损失,所以在这种情况下建议在循环体外创建一个StringBuilder对象调用append()方法手动拼接(如上面例子如果使用手动拼接运行时间将缩小到1/200左右)。 96 | 97 | 与此之外还有一种特殊情况,也就是当"+"两端均为编译期确定的字符串常量时,编译器会进行相应的优化,直接将两个字符串常量拼接好,例如: 98 | ```java 99 | System.out.println("Hello" + "World"); 100 | 101 | /** 102 | * 反编译后 103 | */ 104 | System.out.println("HelloWorld"); 105 | ``` 106 | 107 | ## 4. 字符串常量 108 | 109 | JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化:使用字符串常量池。每当创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于String字符串的不可变性,**常量池中一定不存在两个相同的字符串**。 110 | 111 | ![](https://img-blog.csdnimg.cn/20200609003544858.png) 112 | 113 | 我们来看一个例子: 114 | 115 | ```java 116 | /** 117 | * 运行结果为true false 118 | */ 119 | String s1 = "abc"; 120 | String s2 = "abc"; 121 | String s3 = new String("abc"); 122 | System.out.println(s1 == s2); 123 | System.out.println(s1 == s3); 124 | ``` 125 | 由于常量池中不存在两个相同的对象,所以s1和s2都是指向JVM字符串常量池中的"AB"对象。new关键字一定会产生一个对象,并且这个对象存储在堆中。所以`String s3 = new String(“AB”);`产生了两个对象:保存在栈中的s3和保存堆中的String对象。 126 | 127 | String 就总结这么多,如果有问题,欢迎讨论。 128 | 129 | ---- 130 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   131 | > 132 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   133 | 134 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 135 | 136 | ----- 137 | -------------------------------------------------------------------------------- /JavaBase/05. StringBuffer 和 StringBuilder 的区别.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | ----- 8 | 9 | StringBuilder 和 StringBuffer 的问题已经是老生常谈了,但是为什么还有那么多面试官喜欢问这个问题呢?不知道,我觉得是面试官太low了……🤣🤣 10 | 11 | 不过我也来把这个知识点给大家总结一下,先说说他们的相同点。 12 | 13 | ## 1. 相同点 14 | 15 | 说到这单,我又忍不住要跟大家唠叨唠叨 String 了,我们先看下 String 的一个例子: 16 | ```java 17 | String a = "123"; 18 | a = "456"; 19 | // 打印出来的a为456 20 | System.out.println(a) 21 | ``` 22 | 不是说 String 是不可改变的吗?这怎么就变了呢?逗我吗?其实是没变的,原来那个 123 没变,相当于重新创建了一个 456,然后把 a 指向了这个 456,那个 123 会被回收掉。看看我给大家画的图: 23 | 24 | ![](https://img-blog.csdnimg.cn/20200612220539886.png) 25 | 26 | 而 StringBuffer 和 StringBuilder 就不一样了,他们是可变的,当一个对象被创建以后,通过`append()`、`insert()`、`reverse()`、`setCharAt()`、`setLength()`等方法可以改变这个字符串对象的字符序列。也来看个例子: 27 | 28 | ```java 29 | StringBuffer b = new StringBuffer("123"); 30 | b.append("456"); 31 | // b打印结果为:123456 32 | System.out.println(b); 33 | ``` 34 | ![](https://img-blog.csdnimg.cn/20200612221256392.png) 35 | 36 | OK,那现在我们知道了,StringBuffer 和 StringBuilder 有个共同点,那就是它们代表一个字符可变的字符串。那除此之外,还有没有其他相同点呢? 37 | 38 | 肯定是有的,比如: 39 | 1. StringBuilder 与 StringBuffer 有公共父类 AbstractStringBuilder (抽象类); 40 | 2. StringBuilder、StringBuffer 的方法都会调用 AbstractStringBuilder 中的公共方法,如`super.append(...)`。 41 | 42 | ## 2. 不同点 43 | 44 | StringBuffer 是线程安全的,StringBuilder 是非线程安全的。我们看源码都知道,StringBuffer 的方法上面都加了 synchronized 关键字,所以没有线程安全问题,但是性能会受影响,所以 StringBuilder 会比 StringBuffer 快,在单线程环境下,建议使用 StringBuilder。 45 | 46 | 另外还有个不同点是缓冲区。 47 | 48 | 先看下 StringBuilder 的`toString()`方法: 49 | ```java 50 | @Override 51 | public String toString() { 52 | // Create a copy, don't share the array 53 | return new String(value, 0, count); 54 | } 55 | ``` 56 | 57 | 再看下 StringBuffer 的`toString()`方法: 58 | 59 | ```java 60 | @Override 61 | public synchronized String toString() { 62 | if (toStringCache == null) { 63 | toStringCache = Arrays.copyOfRange(value, 0, count); 64 | } 65 | return new String(toStringCache, true); 66 | } 67 | 68 | ``` 69 | 可以看出:StringBuffer 每次获取 toString 都会直接使用缓存区的 toStringCache 值来构造一个字符串。而 StringBuilder 则每次都需要复制一次字符数组,再构造一个字符串。 70 | 71 | 所以, StringBuffer 对缓存区优化,不过 StringBuffer 的这个toString 方法仍然是同步的。 72 | 73 | OK,StringBuffer 和 StringBuilder 就总结这么多吧,如有问题,欢迎指正。 74 | 75 | ----- 76 | 77 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   78 | > 79 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   80 | 81 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 82 | 83 | ----- 84 | -------------------------------------------------------------------------------- /JavaBase/06. Java反射详解.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | ----- 8 | 反射是框架设计的灵魂要素! 9 | 10 | ## 1. 反射是什么? 11 | 所谓的反射就是java语言在运行时拥有的一种自观的能力,反射使您的程序代码能够得到装载到JVM中的类的内部信息,允许您执行程序时才得到需要类的内部信息,而不是在编写代码的时候就必须要知道所需类的内部信息;也可以通俗的将这种动态获取信息以及动态调用对象的方法称为Java的反射机制. 12 | 13 | 通过Java的反射机制,程序猿们可以更深入的控制程序的运行过程,如在程序运行时对用户输入的信息进行验证,还可以逆向控制程序的执行过程,这也使反射成为构建灵活的应用的主要工具。 14 | 15 | ## 2. 反射原理大解析 16 | ### 2.1 反射的常用类和函数 17 | Java反射机制的实现要借助于4个类: 18 | - Class 类对象 19 | - Constructor 类的构造器对象 20 | - Field 类的属性对象 21 | - Method 类的方法对象 22 | 23 | ### 2.2 Class 类包含的方法 24 | 通过这四个对象我们可以粗略的看到一个类的各个组成部分。其中最核心的就是Class类,它是实现反射的基础,Class类包含的方法主要有: 25 | 序号|名称|功能 26 | ---|:--|:-- 27 | 1|getName()| 获取此类型的全限定名 28 | 2|getSuperClass() | 得到此类型的直接超类的全限定名 29 | 3|isInterface() | 判断此类型是类类型还是接口类型 30 | 4|getTypeParamters() | 获取这个类型的访问修饰符 31 | 5|getInterfaces() | 获取任何直接超接口的全限定名的有序列表 32 | 6| getFields() | 获取字段信息 33 | 7| getMethods()| 获取方法信息 34 | 35 | ### 2.3 反射的主要方法 36 | 应用反射时我们最关心的一般是一个类的构造器、属性和方法,下面我们主要介绍Class类中针对这三个元素的方法: 37 | 38 | #### 2.3.1 得到构造器的方法 39 | 40 | 语法|功能 41 | ---|:-- 42 | Constructor getConstructor(Class[] params) | 获得使用特殊的参数类型的公共构造函数 43 | Constructor[] getConstructors() |获得类的所有公共构造函数 44 | Constructor getDeclaredConstructor(Class[] params)| 获得使用特定参数类型的构造函数(与接入级别无关) 45 | Constructor[] getDeclaredConstructors() | 获得类的所有构造函数(与接入级别无关) 46 | 47 | #### 2.3.2 获得字段信息的方法 48 | 49 | 语法|功能 50 | ---|:-- 51 | Field getField(String name) | 获得命名的公共字段 52 | Field[] getFields() | 获得类的所有公共字段 53 | Field getDeclaredField(String name) | 获得类声明的命名的字段 54 | Field[] getDeclaredFields() | 获得类声明的所有字段 55 | 56 | #### 2.3.3 获得方法信息的方法 57 | 58 | 语法|功能 59 | ---|:-- 60 | Method getMethod(String name, Class[] params)| 使用特定的参数类型,获得命名的公共方法 61 | Method[] getMethods() | 获得类的所有公共方法 62 | Method getDeclaredMethod(String name, Class[] params)| 使用特写的参数类型,获得类声明的命名的方法 63 | Method[] getDeclaredMethods() | 获得类声明的所有方法 64 | 65 | ### 2.4 反射实战的基本步骤 66 | ```java 67 | Class actionClass=Class.forName(“MyClass”); 68 | Object action=actionClass.newInstance(); 69 | Method method = actionClass.getMethod(“myMethod”,null); 70 | method.invoke(action,null); 71 | ``` 72 | 上面就是最常见的反射使用的例子,前两行实现了类的装载、链接和初始化(newInstance方法实际上也是使用反射调用了``方法),后两行实现了从class对象中获取到method对象然后执行反射调用。下面简单分析一下该代码的具体原理。 73 | 74 | #### 2.4.1 获得类的Class对象 75 | 76 | >通常有三种不同的方法: 77 | 1)Class c = Class.forName("java.lang.String") 78 | 2)Class c = MyClass.class 79 | 3)对于基本数据类型可以用Class c = int.class 或 Class c = Integer.TYPE这样的语句. 80 | 81 | 举个小栗子:先通过反射机制得到某个类的构造器,然后调用该构造器创建该类的一个实例 82 | 83 | PS:反射的原理之一其实就是动态的生成类似于上述的字节码,加载到jvm中运行。 84 | 85 | 设想一下,上面的代码中,如果想要实现`method.invoke(action,null)`调用action对象的`myMethod`方法,只需要实现这样一个Method类即可: 86 | 87 | ``` java 88 | Class Method{ 89 | 90 | public Object invoke(Object obj,Object[] param){ 91 | 92 | MyClass myClass=(MyClass)obj; 93 | 94 | return myClass.myMethod(); 95 | 96 | } 97 | 98 | } 99 | ``` 100 | 101 | #### 2.4.2 获取 Method 对象 102 | 103 | 首先来看一下Method对象是如何生成的: 104 | - 使用Method m =myclass.getMethod("myMethod")获得了一个Class对象 105 | - 接着对其进行判断,如果没有对应的cache,那么JVM就会为其创建一个并放入缓冲空间 106 | - 处理器再判断Cache中是否存在"myMethod" 107 | - 如果没有则返回NoSuchMethodException 108 | - 如果存在那么就Copy一份"myMethod"对象并返回 109 | 110 | 111 | 上面的Class对象是在加载类时由JVM构造的,JVM为每个类管理一个独一无二的Class对象,这份Class对象里维护着该类的所有Method,Field,Constructor的cache,这份cache也可以被称作根对象。每次getMethod获取到的Method对象都持有对根对象的引用,因为一些重量级的Method的成员变量(主要是MethodAccessor),我们不希望每次创建Method对象都要重新初始化,于是所有代表同一个方法的Method对象都共享着根对象的MethodAccessor,每一次创建都会调用根对象的copy方法复制一份: 112 | 113 | ``` java 114 | Method copy() { 115 | Method res = new Method(clazz, name, parameterTypes, returnType, 116 | 117 | exceptionTypes, modifiers, slot, signature, 118 | 119 | annotations, parameterAnnotations, annotationDefault); 120 | res.root = this; 121 | res.methodAccessor = methodAccessor; 122 | return res; 123 | } 124 | ``` 125 | #### 2.4.3 调用invoke()方法 126 | 获取到Method对象之后,调用invoke方法的流程如下: 127 | 128 | 129 | m.invoke(obj,param); 130 | 131 | - 首先调用MethodAccess.invoke 132 | - 如果该方法的累计调用次数<=15,会创建出NativeMethodAccessorImp 133 | - 如果该方法的累计调用次数>15,会由java代码创建出字节码组装而成的MethodAccessorImpl 134 | 135 | 我们可以看到,调用Method.invoke之后,会直接去调`MethodAccessor.invoke`。MethodAccessor就是上面提到的所有同名method共享的一个实例,由ReflectionFactory创建。创建机制采用了一种名为inflation的方式(JDK1.4之后):如果该方法的累计调用次数<=15,会创建出NativeMethodAccessorImpl,它的实现就是直接调用native方法实现反射;如果该方法的累计调用次数>15,会由java代码创建出字节码组装而成的MethodAccessorImpl。(是否采用inflation和15这个数字都可以在jvm参数中调整) 136 | 137 | 以调用`MyClass.myMethod(String s)`为例,生成出的MethodAccessorImpl字节码翻译成Java代码大致如下: 138 | ``` java 139 | public class GeneratedMethodAccessor1 extends MethodAccessorImpl { 140 | public Object invoke(Object obj, Object[] args) throws Exception { 141 | try { 142 | MyClass target = (MyClass) obj; 143 | String arg0 = (String) args[0]; 144 | target.myMethod(arg0); 145 | } catch (Throwable t) { 146 | throw new InvocationTargetException(t); 147 | } 148 | } 149 | } 150 | ``` 151 | 152 | 通过对java运行过程的详细分析,我们可以发现其中第1次和第16次调用是最耗时的(初始化NativeMethodAccessorImpl和字节码拼装MethodAccessorImpl)。初始化不可避免,因而native方式的初始化会更快,所以前几次的调用会采用native方法。 153 | 154 | 随着调用次数的增加,每次反射都使用JNI跨越native边界会对优化有阻碍作用,相对来说使用拼装出的字节码可以直接以Java调用的形式实现反射,发挥了JIT优化的作用,避免了JNI为了维护OopMap(HotSpot用来实现准确式GC的数据结构)进行封装/解封装的性能损耗。 155 | 156 | 在已经创建了MethodAccessor的情况下,使用Java版本的实现会比native版本更快。所以当调用次数到达一定次数(15次)后,会切换成Java实现的版本,来优化未来可能的更频繁的反射调用。 157 | 158 | ## 3. Java反射的应用(Hibernate框架) 159 | 160 | 前面我们已经知道,Java 反射机制提供了一种动态链接程序组件的多功能方法,它允许程序创建和控制任何类的对象(根据安全性限制)之前,无需提前硬编码目标类。这些特性使得反射特别适用于创建以非常普通的方式与对象协作的库。例如,反射经常在持续存储对象为数据库、XML或其它外部格式的框架中使用。下面就已Hibernate框架为例像大家阐述一下反射的重要意义。 161 | 162 | Hibernate是一个屏蔽了JDBC,实现了ORM的java框架,利用该框架我们可以抛弃掉繁琐的sql语句而是利用Hibernate中Session类的save()方法直接将某个类的对象存到数据库当中,也就是所涉及到sql语句的那些代码Hibernate帮我们做了。这时候就出现了一个问题,Hibernate怎样知道他要存的某个对象都有什么属性呢?这些属性都是什么类型呢?想一想,它在向数据库中存储该对象属性时的sql语句该怎么构造呢?OK,反射的作用此刻就体现出来了! 163 | 164 | 下面我们以一个例子来进行阐述,比如我们定义了一个User类,这个User类中有20个属性和这些属性的get和set方法,相应的在数据库中有一个User表,这个User表中对应着20个字段。假设我们从User表中提取了一条记录,现在需要将这条记录的20个字段的内容分别赋给一个User对象myUser的20个属性,而Hibernate框架在编译的时候并不知道这个User类,他无法直接调用myUser.getXXX或者myUser.setXXX方法,此时就用到了反射,具体处理过程如下: 165 | 166 | 1. 根据查询条件构造PreparedStament语句,该语句返回20个字段的值; 167 | 168 | 2. Hibernate通过读取配置文件得到User类的属性列表list(是一个String数组)以及这些属性的类型; 169 | 170 | 3. 创建myUser所属类的Class对象c;c = myUser.getClass(); 171 | 172 | 4. 构造一个for循环,循环的次数为list列表的长度; 173 | 174 | - 读取list[i]的值,然后构造对应该属性的set方法; 175 | 176 | - 判断list[i]的类型XXX,调用PreparedStament语句中的getXXX(i),进而得到i出字段的值; 177 | 178 | - 将4.2中得到的值作为4.1中得到的set方法的参数,这样就完成了一个字段像一个属性的赋值,如此循环直至程序运行结束; 179 | 180 | 如果没有反射难以想象实现这么复杂的功能将会有多么难! 181 | 182 | 183 | 话说回来,反射给我们带来便利的同时也有它自身的缺点,比如性能较低、安全性较低、过程比较复杂等等,感兴趣的读者也可以在实际工作中再深入研究哦! 184 | 185 | ---- 186 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   187 | > 188 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   189 | 190 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 191 | 192 | -------------------------------------------------------------------------------- /JavaBase/07. 你对 Java 异常了解多少.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | ----- 8 | 9 | ## 1. 异常的概念 10 | 11 | 如果某个方法不能按照正常的途径完成任务,就可以通过另一种路径退出方法。在这种情况下会抛出一个封装了错误信息的对象。此时,这个方法会立刻退出同时不返回任何值。另外,调用这个方法的其他代码也无法继续执行,异常处理机制会将代码执行交给异常处理器。 12 | 13 | ![Java异常概念](https://img-blog.csdnimg.cn/20200427134201871.png) 14 | 15 | ## 2. Java 中异常分为哪些种类 16 | 按照异常需要处理的时机分为编译时异常(也叫强制性异常)也叫CheckedException 和运行时异常(也叫非强制性异常)也叫RuntimeException。 17 | 18 | 只有java 语言提供了Checked 异常,Java 认为 Checked 异常都是可以被处理的异常,所以Java 程序必须显式处理Checked 异常。如果程序没有处理Checked 异常,该程序在编译时就会发生错误无法编译。这体现了Java 的设计哲学:没有完善错误处理的代码根本没有机会被执行。对Checked 异常处理方法有两种: 19 | 20 | 1. 当前方法知道如何处理该异常,则用try...catch 块来处理该异常。 21 | 2. 当前方法不知道如何处理,则在定义该方法是声明抛出该异常。 22 | 23 | 运行时异常只有当代码在运行时才发行的异常,编译时不需要try catch。Runtime 如除数是0 和数组下标越界等,其产生频繁,处理麻烦,若显示申明或者捕获将会对程序的可读性和运行效率影响很大。所以由系统自动检测并将它们交给缺省的异常处理程序。当然如果你有处理要求也可以显示捕获它们。 24 | 25 | 那么,调用下面的方法,会得到什么结果呢? 26 | 27 | ```java 28 | public int getNum(){ 29 | try { 30 | int a = 1/0; 31 | return 1; 32 | } catch (Exception e) { 33 | return 2; 34 | }finally{ 35 | return 3; 36 | } 37 | } 38 | ``` 39 | 代码在走到第3 行的时候遇到了一个MathException,这时第四行的代码就不会执行了,代码直接跳转到catch语句中,走到第6 行的时候,异常机制有这么一个原则如果在catch 中遇到了return 或者异常等能使该函数终止的话那么有finally 就必须先执行完finally 代码块里面的代码然后再返回值。因此代码又跳到第8 行,可惜第8 行是一个return 语句,那么这个时候方法就结束了,因此第6 行的返回结果就无法被真正返回。如果finally 仅仅是处理了一个释放资源的操作,那么该道题最终返回的结果就是2。因此上面返回值是3。 40 | 41 | ## 3. error 和exception 有什么区别? 42 | Error 类和Exception 类的父类都是Throwable 类,他们的区别如下。 43 | Error 类一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢出等。对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防,遇到这样的错误,建议让程序终止。 44 | Exception 类表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常。 45 | Exception 类又分为运行时异常(Runtime Exception)和受检查的异常(Checked Exception ),运行时异常;ArithmaticException,IllegalArgumentException,编译能通过,但是一运行就终止了,程序不会处理运行时异常,出现这类异常,程序会终止。而受检查的异常,要么用try。。。catch 捕获,要么用throws 字句声明抛出,交给它的父类处理,否则编译不会通过。 46 | 47 | ## 4. throw 和 throws 的区别是什么? 48 | Java 中的异常处理除了包括捕获异常和处理异常之外,还包括声明异常和拋出异常,可以通过 throws 关键字在方法上声明该方法要拋出的异常,或者在方法内部通过 throw 拋出异常对象。 49 | 50 | throws 关键字和 throw 关键字在使用上的几点区别如下: 51 | 52 | 1. throw 关键字用在方法内部,只能用于抛出一种异常,用来抛出方法或代码块中的异常,受查异常和非受查异常都可以被抛出。 53 | 2. throws 关键字用在方法声明上,可以抛出多个异常,用来标识该方法可能抛出的异常列表。一个方法用 throws 标识了可能抛出的异常列表,调用该方法的方法中必须包含可处理异常的代码,否则也要在方法签名中用 throws 关键字声明相应的异常。 54 | 55 | ## 5. Java 的异常处理机制 56 | Java 对异常进行了分类,不同类型的异常分别用不同的Java 类表示,所有异常的根类为`java.lang.Throwable`,Throwable 下面又派生了两个子类:Error 和Exception,Error 表示应用程序本身无法克服和恢复的一种严重问题。 57 | Exception 表示程序还能够克服和恢复的问题,其中又分为系统异常和普通异常,系统异常是软件本身缺陷所导致的问题,也就是软件开发人员考虑不周所导致的问题,软件使用者无法克服和恢复这种问题,但在这种问题下还可以让软件系统继续运行或者让软件死掉,例如,数组脚本越界(ArrayIndexOutOfBoundsException),空指针异常(NullPointerException)、类转换异常(ClassCastException);普通异常是运行环境的变化或异常所导致的问题,是用户能够克服的问题,例如,网络断线,硬盘空间不够,发生这样的异常后,程序不应该死掉。java 为系统异常和普通异常提供了不同的解决方案,编译器强制普通异常必须try..catch 处理或用throws 声明继续抛给上层调用方法处理,所以普通异常也称为checked 异常,而系统异常可以处理也可以不处理,所以,编译器不强制用try..catch 处理或用throws 声明,所以系统异常也称为unchecked 异常。 58 | 59 | ## 6. 请写出你最常见的5 个RuntimeException 60 | 61 | 这是面试过程中,很喜欢问的问题,下面列举几个常见的RuntimeException。 62 | 63 | 1)java.lang.NullPointerException 空指针异常;出现原因:调用了未经初始化的对象或者是不存在的对象。 64 | 2)java.lang.ClassNotFoundException 指定的类找不到;出现原因:类的名称和路径加载错误;通常都是程序试图通过字符串来加载某个类时可能引发异常。 65 | 3)java.lang.NumberFormatException 字符串转换为数字异常;出现原因:字符型数据中包含非数字型字符。 66 | 4)java.lang.IndexOutOfBoundsException 数组角标越界异常,常见于操作数组对象时发生。 67 | 5)java.lang.IllegalArgumentException 方法传递参数错误。 68 | 6)java.lang.ClassCastException 数据类型转换异常。 69 | 7)java.lang.NoClassDefFoundException 未找到类定义错误。 70 | 8)SQLException SQL 异常,常见于操作数据库时的SQL 语句错误。 71 | 9)java.lang.InstantiationException 实例化异常。 72 | 10)java.lang.NoSuchMethodException 方法不存在异常。 73 | 74 | ## 7. final、finally、finalize 的区别 75 | 1)final:用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,被其修饰的类不可继承。 76 | 2)finally:异常处理语句结构的一部分,表示总是执行。 77 | 3)finalize:Object 类的一个方法,在垃圾回收器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源回收,例如关闭文件等。该方法更像是一个对象生命周期的临终方法,当该方法被系统调用则代表该对象即将“死亡”,但是需要注意的是,我们主动行为上去调用该方法并不会导致该对象“死亡”,这是一个被动的方法(其实就是回调方法),不需要我们调用。 78 | 79 | ## 8. NoClassDefFoundError 和 ClassNotFoundException 区别 80 | NoClassDefFoundError 是一个 Error 类型的异常,是由 JVM 引起的,不应该尝试捕获这个异常。 81 | 82 | 引起该异常的原因是 JVM 或 ClassLoader 尝试加载某类时在内存中找不到该类的定义,该动作发生在运行期间,即编译时该类存在,但是在运行时却找不到了,可能是变异后被删除了等原因导致; 83 | 84 | ClassNotFoundException 是一个受查异常,需要显式地使用 try-catch 对其进行捕获和处理,或在方法签名中用 throws 关键字进行声明。当使用 Class.forName, ClassLoader.loadClass 或 ClassLoader.findSystemClass 动态加载类到内存的时候,通过传入的类路径参数没有找到该类,就会抛出该异常;另一种抛出该异常的可能原因是某个类已经由一个类加载器加载至内存中,另一个加载器又尝试去加载它。 85 | 86 | Java 异常 就总结这么多,如果有问题,欢迎讨论。 87 | 88 | ---- 89 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   90 | > 91 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   92 | 93 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 94 | 95 | ----- 96 | -------------------------------------------------------------------------------- /JavaThread/01. Java 创建线程有哪些方式.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | ---- 8 | 9 | 昨天有个小伙伴去阿里面试实习生岗位,面试官问他了一个老生常谈的问题:你说一说 Java 创建线程都有哪些方式? 10 | 11 | 这哥们心中窃喜,这个老生常谈的问题早已背的滚瓜烂熟,于是很流利的说了出来。 12 | 13 | Java 创建线程有两种方式: 14 | 15 | >1. 继承`Thread`类,并重写`run()`方法 16 | >2. 实现`Runnable`接口,覆盖接口中的`run()`方法,并把`Runnable`接口的实现扔给`Thread` 17 | 18 | 面试官:(拿出一张白纸)那你把这两种方式写一下吧! 19 | 20 | 小哥:(这有何难!)好~ 21 | 22 | ```java 23 | public static void main(String[] args) { 24 | // 第一种 25 | MyThread myThread = new MyThread(); 26 | myThread.start(); 27 | // 第二种 28 | new Thread(() -> System.out.println("自己实现的run-2")).start(); 29 | } 30 | 31 | public static class MyThread extends Thread { 32 | @Override 33 | public void run() { 34 | System.out.println("自己实现的run-1"); 35 | } 36 | } 37 | ``` 38 | 39 | 面试官:嗯,那除了这两种,还有其他创建线程的方法吗? 40 | 41 | 这些简单的问题难不倒这哥们,于是他想到了 Java5 之后的`Executors`,`Executors`工具类可以用来创建线程池。 42 | 43 | 小哥:`Executors`工具类是用来创建线程池的,这个线程池可以指定线程个数,也可以不指定,也可以指定定时器的线程池,它有如下常用的方法: 44 | 45 | >`newFixedThreadPool(int nThreads)`:创建固定数量的线程池 46 | >`newCachedThreadPool()`:创建缓存线程池 47 | >`newSingleThreadExecutor()`:创建单个线程 48 | >`newScheduledThreadPool(int corePoolSize)`:创建定时器线程池 49 | 50 | 面试官:嗯,OK,咱们还是针对你刚刚写的代码,我再问你个问题。 51 | 52 | 此时这哥们有种不祥的预感,是不是自己代码写的有点问题?或者要问我底层实现? 53 | 54 | 面试官:你写的两种创建线程的方式,都涉及到了`run()`方法,你了解过`Thread`里的`run()`方法具体是怎么实现的吗? 55 | 56 | 果然问到了源码了,这哥们之前看了点,所以不是很慌,回忆了一下,向面试官道来。 57 | 58 | 小哥:emm……`Thread` 中的`run()`方法里东西很少,就一个 if 判断: 59 | 60 | ```java 61 | @Override 62 | public void run() { 63 | if (target != null) { 64 | target.run(); 65 | } 66 | } 67 | ``` 68 | 69 | 有个`target`对象,会去判断该变量是否为空,非空的时候,去执行`target`对象中的`run()`方法,否则啥也不干。而这个`target`对象,就是我们说的`Runnable`: 70 | 71 | ```java 72 | /* What will be run. */ 73 | private Runnable target; 74 | ``` 75 | 76 | 面试官:嗯,那这个`Runnable`类你了解过吗? 77 | 小哥:了解过,这个`Runnable`类很简单,就一个抽象方法: 78 | 79 | ```java 80 | @FunctionalInterface 81 | public interface Runnable { 82 | public abstract void run(); 83 | } 84 | ``` 85 | 86 | 这个抽象方法也是`run()`!如果我们使用`Runnable`接口,就需要实现这个`run()`方法。由于这个`Runnable`类上面标了`@FunctionalInterface`注解,所以可以使用函数式编程。 87 | 88 | 那小哥接着说:(突然自信起来了)所以这就对应了刚才说的两种创建线程的方式,假如我用第一种方式:继承了`Thread`类,然后重写了`run()`方法,那么它就不会去执行上面这个默认的`run()`方法了(即不会去判断`target`),会执行我重写的`run()`方法逻辑。 89 | 90 | 假如我是用的第二种方式:实现`Runnable`接口的方式,那么它会执行默认的`run()`方法,然后判断`target`不为空,再去执行我在`Runnable`接口中实现的`run()`方法。 91 | 92 | 面试官:OK,可以,我再问你个问题。 93 | 94 | 小哥:(暗自窃喜) 95 | 96 | 面试官:那如果我既继承了`Thread`类,同时我又实现了`Runnable`接口,比如这样,最后会打印什么信息出来呢? 97 | 98 | 面试官边说边拿起刚刚这小哥写的代码,对它进行了简单的修改: 99 | 100 | ```java 101 | public static void main(String[] args) { 102 | new Thread(() -> System.out.println("runnable run")) { 103 | @Override 104 | public void run() { 105 | System.out.println("Thread run"); 106 | } 107 | }.start(); 108 | } 109 | ``` 110 | 111 | 这小哥,突然有点懵,好像从来没想过这个问题,一时没有什么思路,于是回答了个:会打印 “Thread run” 吧…… 112 | 113 | 面试官:答案是对的,但是为什么呢? 114 | 115 | 这小哥一时没想到原因,于是面试官让他回去可以思考思考,就继续下一个问题了。 116 | 117 | 亲爱的读者朋友,你们知道为什么吗?你们可以先思考一下。 118 | 119 | 其实这个答案很简单,我们来分析一下代码便知:其实是 new 了一个对象(子对象)继承了`Thread`对象(父对象),在子对象里重写了父类的`run()`方法;然后父对象里面扔了个`Runnable`进去,父对象中的`run()`方法就是最初那个带有 if 判断的`run()`方法。 120 | 121 | 好了,现在执行`start()`后,肯定先在子类中找`run()`方法,找到了,父类的`run()`方法自然就被干掉了,所以会打印出:Thread run。 122 | 123 | 如果我们现在假设子类中没有重写`run()`方法,那么必然要去父类找`run()`方法,父类的`run()`方法中就得判断是否有`Runnable`传进来,现在有一个,所以执行`Runnable`中的`run()`方法,那么就会打印:Runnable run 出来。 124 | 125 | 说白了,就是问了个 Java 语言本身的父子继承关系,会优先执行子类重写的方法而已,只是借这个场景,换了个提问的方式,面试者可能一时没反应过来,有点懵也是正常的,如果直接问,傻子都能回答的出来。 126 | 127 | 后记:通过这道简单的面试题,帮大家分析了一下在创建线程过程中的源码,可以看出来,面试过程中,面试官更加看重一些原理性的东西,而不是背一下方式就行了。同时也能看的出,面试大厂,需要做好充分的准备。另外,在面试的时候要冷静,可能有些问题并没有太难,回答不出来只是当时太紧张造成的。 128 | 129 | 这篇文章就写到这,最后祝大家都能面试成功。 130 | 131 | ---- 132 | 133 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   134 | > 135 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   136 | 137 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 138 | 139 | ---- -------------------------------------------------------------------------------- /JavaThread/02. 线程有哪些状态,彼此之间如何切换.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | ---- 8 | 9 | 是的,今天必须要把它给彻底根治了! 10 | 11 | 现在 Java 面试,基本上都会问到多线程,那么随之而来的线程状态,很多时候都会被问到,大部分人都知道线程的几种状态,但是让你整体全部串起来,做到面试时游刃有余,估计很多人记忆不是很清晰。 12 | 13 | 今天武哥就把这些全部给整了,下次面试官再问,就把这篇文章扔给他。 14 | 15 | ### 1. 开局一张图,其他全靠吹 16 | ![](https://img-blog.csdnimg.cn/2020041810160437.png) 17 | 本文的核心就在于这幅图。我用 PPT 画了好几个小时,应该是**全网最清新最好看的一张图**了吧(不接受反驳,谁反驳上去也是一jio,手动滑稽) 18 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200420161014675.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2Vzb25fMTU=,size_16,color_FFFFFF,t_70#pic_center) 19 | 20 | 好了,牛逼不能再吹了,我们根据上面这幅图来展开,我把这块涉及到的东西都跟大家理一遍,希望看过这篇文章的小伙伴,特别是初学者,后面不用再在这个问题上纠结了。 21 | 22 | 针对线程的状态,首先来看下官方的源码(除去了注释): 23 | ```java 24 | public enum State { 25 | NEW, 26 | RUNNABLE, 27 | BLOCKED, 28 | WAITING, 29 | TIMED_WAITING, 30 | TERMINATED; 31 | } 32 | ``` 33 | 所以从官方的角度,线程是有6中状态的,结合官方的注释,我首先把六种状态解释一下,然后具体讲解一下每个状态之间转换所涉及到的那些方法。把这些都理清楚了,这一块也就差不多了。 34 | 35 | ### 2. 线程的6种状态解释 36 | 37 | 结合上面的那张图一起看: 38 | 39 | **NEW**:线程被创建出来了,但是还没有`start()`。 40 | 41 | **RUNNABLE**:可运行状态,这个状态比较特殊,我在图中把这个状态拆分成了两部分:一部分是`READY`,顾名思义是准备状态,另一部分是`RUNNING`,即运行状态。 42 | 43 | 准备状态:只能说明你有资格运行,单只要调度程序没有调度到你,你就永远是准备状态。从图中可以看出,好几个状态都能切换到准备状态,至于切换的方法,我在下文详细给大家整理出来介绍。 44 | 45 | 运行状态:线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。说白了,就是线程跑起来了。 46 | 47 | **BLOCKED**:阻塞状态是线程阻塞在进入`synchronized`关键字修饰的方法或代码块(获取锁)时的状态。 48 | 49 | **WAITING**:等待状态的线程不会被分配 CPU 执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。 50 | 51 | **TIMED_WAITING**:超时等待状态的线程不会被分配 CPU 执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。这是和上面的 WAITING 状态的区别。 52 | 53 | **TERMINATED**:终止状态,顾名思义,线程执行结束了。 54 | 55 | ### 3. 线程状态之间的切换函数分析 56 | 57 | 从上面的图中可以看出,线程之间的状态切换,主要涉及到以下几个函数: 58 | 59 | `Thread.sleep`、`Thread.yeild`、`Object.wait`、`Thread.join`、`Object.notify/notifyAll`、`LockSupport.park/unpark/parkNanos/parkUtil` 60 | 61 | 接下来针对这几个函数,做一下简单的功能分析和对比,大家可以结合上面的那幅图。 62 | 63 | `Thread.sleep(time)`:当前线程调用此方法,顾名思义,就是让当前线程进入 TIMED_WAITING 状态,睡眠固定的时间(但是不释放对象锁),到点后自动唤醒,进入准备状态。主要作用是给其他线程执行机会。 64 | 65 | `Thread.yield()`:当前线程调用此方法,放弃获取的CPU时间片,但不释放锁资源,由运行状态变为准备状态,让OS再次选择线程。实际中无法保证`yield()`达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。它跟 `sleep`方法的区别在于不能指定暂停多少时间。 66 | 67 | `Object.wait()`:当前线程调用对象的`wait()`方法,当前线程释放对象锁,进入WAITING 状态。依靠`notify()/notifyAll()`来唤醒;而`wait(time)`方法的主要区别是进入 TIMED_WAITING 状态,到达时间后,自动唤醒。 68 | 69 | `Object.notify/notifyAll`:`notify()`唤醒在此对象监视器上等待的单个线程,选择是任意性的。`notifyAll()`唤醒在此对象监视器上等待的所有线程。 70 | 71 | `Thread.join()`:当前线程里调用其它线程 t 的`join()`方法,当前线程进入 WAITING 状态,当前线程不会释放已经持有的对象锁。线程 t 执行完毕,当前线程进入 RUNNABLE 状态。而`join(time)`方法的主要区别是当前线程进入 TIMED_WAITING 状态,到达时间后,进入 RUNNABLE 状态。 72 | 73 | `LockSupport.park()`:当前线程进入 WAITING 状态,需要通过`LockSupport.unpark(thread)`来唤醒。 74 | 75 | `LockSupport.parkNanos(nanos)/parkUntil(deadlines)`:当前线程进入 TIMED_WAITING 状态,需要通过``LockSupport.unpark(thread)`来唤醒。 76 | 77 | 相比与`wait`方法,`LockSupport`不需要获得锁就可以让线程进入 WAITING 或者 TIMED_WAITING 状态。 78 | 79 | 当然了,还有个 BLOCKED 状态,涉及到 `synchronized`关键字,由于这块也是面试经常会问到的,下一篇我会全面剖析一下`synchronized`。 80 | 81 | 关于线程的状态以及状态间切换所涉及到的函数,这篇文章就总结这么多。全篇围绕文章开头的那幅图,我觉得这块内容,**只要把那幅图记住,就基本没什么问题了**。如有疑问,欢迎讨论。 82 | 83 | ---- 84 | 85 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   86 | > 87 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   88 | 89 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 90 | 91 | ---- -------------------------------------------------------------------------------- /JavaThread/03. 传统线程互斥技术 synchronized.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | ---- 8 | 9 | 在多个线程同时操作相同资源的时候,就会遇到并发的问题,如银行转账啊、售票系统啊等。为了避免这些问题的出现,我们可以使用synchronized关键字来解决,下面针对synchronized常见的用法做一个总结。首先写一个存在并发问题的程序,如下: 10 | ```java 11 | public class TraditionalThreadSynchronized { 12 | 13 | public static void main(String[] args) { 14 | //在静态方法中不能new内部类的实例对象 15 | //private Outputer outputer = new Outputer(); 16 | new TraditionalThreadSynchronized().init(); 17 | } 18 | 19 | private void init() { 20 | final Outputer outputer = new Outputer(); 21 | //线程1打印:duoxiancheng 22 | new Thread(new Runnable() { 23 | @Override 24 | public void run() { 25 | while(true) { 26 | try { 27 | Thread.sleep(5); 28 | } catch (InterruptedException e) { 29 | // TODO Auto-generated catch block 30 | e.printStackTrace(); 31 | } 32 | outputer.output1("duoxiancheng"); 33 | } 34 | 35 | } 36 | }).start();; 37 | 38 | //线程2打印:eson15 39 | new Thread(new Runnable() { 40 | @Override 41 | public void run() { 42 | while(true) { 43 | try { 44 | Thread.sleep(5); 45 | } catch (InterruptedException e) { 46 | // TODO Auto-generated catch block 47 | e.printStackTrace(); 48 | } 49 | outputer.output1("eson15"); 50 | } 51 | 52 | } 53 | }).start();; 54 | } 55 | 56 | static class Outputer { 57 | //自定义一个字符串打印方法,一个个字符的打印 58 | public void output1(String name) { 59 | int len = name.length(); 60 | for(int i = 0; i < len; i++) { 61 | System.out.print(name.charAt(i)); 62 | } 63 | System.out.println(""); 64 | } 65 | } 66 | } 67 | ``` 68 | 在内部类Outputer中定义了一个打印字符串的方法,一个字符一个字符的打印,这样比较容易直观的看出并发问题,因为字符顺序打乱了就说明出现问题了。然后在init方法中开启两个线程,一个线程打印“duoxiancheng”,另一个线程打印“eson15”。看一下运行结果: 69 | >eson15duoxianche
70 | ng
71 | eson15
72 | duoxiancheng
73 | duoxiancheng
74 | eson15
75 | esduoxiancheng
76 | on15
77 | duoxiancheng
78 | 79 | 已经出现问题了,为了解决这个问题,可以使用synchronized同步代码块来对公共部分进行同步操作,但是需要给它一把锁,这把锁是一个对象,可以是任意一个对象,但是前提是,两个线程使用的必须是同一个对象锁才可以,这很好理解。那么下面在`output1()`方法中加入synchronized代码块: 80 | ```java 81 | static class Outputer { 82 | private String token = ""; //定义一个锁 83 | public void output1(String name) { 84 | synchronized(token) //任何一个对象都可以作为参数,但是该对象对于两个线程来说是同一个才行 85 | //如果用name就不行了,因为不同的线程进来name是不一样的,不是同一个name 86 | { 87 | int len = name.length(); 88 | for(int i = 0; i < len; i++) { 89 | System.out.print(name.charAt(i)); 90 | } 91 | System.out.println(""); 92 | } 93 | } 94 | } 95 | ``` 96 | 经过上面的改造,线程安全问题就基本解决了,但是还可以再往下引申,如果在方法上加synchronized关键字的话,那么这个同步锁是什么呢?我们在Outputer类中再写一个`output2()`方法: 97 | ```java 98 | static class Outputer { 99 | private String token = ""; //定义一个锁 100 | public void output1(String name) { 101 | synchronized(token) //任何一个对象都可以作为参数,但是该对象对于两个线程来说是同一个才行 102 | { 103 | int len = name.length(); 104 | for(int i = 0; i < len; i++) { 105 | System.out.print(name.charAt(i)); 106 | } 107 | System.out.println(""); 108 | } 109 | } 110 | 111 | public synchronized void output2(String name) { 112 | 113 | int len = name.length(); 114 | for(int i = 0; i < len; i++) { 115 | System.out.print(name.charAt(i)); 116 | } 117 | System.out.println(""); 118 | } 119 | } 120 | ``` 121 | 方法内部实现逻辑一模一样,唯一不同的就是synchronized加在了方法上,那么我们让init()方法中的两个线程中,一个调用`output1(String name)`方法,另一个调用`output2(String name)`方法,从结果中能看出,线程安全问题又出现了。产生问题的原因不难发现:现在两个方法都加了synchronized,但是两个线程在调用两个不同的方法还是出现了问题,也就是说,还是各玩各的……那么问题就出在这个锁上,说明两者并没有使用同一把锁! 122 | 123 | 如果我们把`output1()`方法中synchronized中的token改成this,再运行就没问题了,这说明一点:**synchronized关键字修饰方法的时候,同步锁是this,即等效于代码块**`synchronized(this) {...}`。 124 | 125 | 再继续往下引申,现在在Outputer类中再写一个静态方法`output3(String name)`,并且也让synchronized去修饰这个静态方法。 126 | ```java 127 | static class Outputer { 128 | private String token = ""; //定义一个锁 129 | public void output1(String name) { 130 | synchronized(token) //任何一个对象都可以作为参数,但是该对象对于两个线程来说是同一个才行 131 | { 132 | int len = name.length(); 133 | for(int i = 0; i < len; i++) { 134 | System.out.print(name.charAt(i)); 135 | } 136 | System.out.println(""); 137 | } 138 | } 139 | 140 | public static synchronized void output3(String name) { 141 | 142 | int len = name.length(); 143 | for(int i = 0; i < len; i++) { 144 | System.out.print(name.charAt(i)); 145 | } 146 | System.out.println(""); 147 | } 148 | } 149 | } 150 | ``` 151 | 152 | 然后在init()方法中一个线程调用`output1()`方法,另一个线程调用`output3()`方法。毫无疑问,肯定又会出现线程安全问题。但是如何解决呢?因为static方法在类加载的时候就加载了,所以这个锁应该是类的字节码对象。那么将token改为`Outputer.class`就解决问题了,这说明一点:**synchronized关键字修饰static方法的时候,同步锁是该类的字节码对象,即等效于代码块**`synchronized(classname.class) {...}`。 153 |    154 | 最后再总结一下: 155 | >- **同步代码块的锁是任意对象**。只要不同的线程都执行同一个同步代码块的时候,这个锁随便设。
156 | >- **同步函数的锁是固定的this**。当需要和同步函数中的逻辑实行同步的时候,代码块中的锁必须为this。
157 | >- **静态同步函数的锁是该函数所属类的字节码文件对象**。该对象可以用`this.getClass()`方法获取,也可以使用 `当前类名.class` 表示。 158 | 159 | ---- 160 | 161 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   162 | > 163 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   164 | 165 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 166 | 167 | ---- -------------------------------------------------------------------------------- /JavaThread/04. 死锁问题.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | ---- 8 | 9 | 我们知道,使用synchronized关键字可以有效的解决线程同步问题,但是如果不恰当的使用synchronized关键字的话也会出问题,即我们所说的死锁。死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。 10 | 11 | 下面写一个死锁的例子加深理解。先看程序,再来分析一下死锁产生的原因: 12 | 13 | ```java 14 | public class DeadLock { 15 | 16 | public static void main(String[] args) { 17 | Business business = new Business1(); 18 | //开启一个线程执行Business类中的functionA方法 19 | new Thread(new Runnable() { 20 | 21 | @Override 22 | public void run() { 23 | while(true) { 24 | business.functionA(); 25 | } 26 | } 27 | }).start(); 28 | 29 | //开启另一个线程执行Business类中的functionB方法 30 | new Thread(new Runnable() { 31 | 32 | @Override 33 | public void run() { 34 | while(true) { 35 | business.functionB(); 36 | } 37 | } 38 | }).start(); 39 | } 40 | 41 | } 42 | 43 | class Business { //定义两个锁,两个方法 44 | //定义两个锁 45 | public static final Object lock_a = new Object(); 46 | public static final Object lock_b = new Object(); 47 | 48 | public void functionA() { 49 | synchronized(lock_a) { 50 | System.out.println("---ThreadA---lock_a---"); 51 | synchronized(lock_b) { 52 | System.out.println("---ThreadA---lock_b---"); 53 | } 54 | } 55 | } 56 | 57 | public void functionB() { 58 | synchronized(lock_b) { 59 | System.out.println("---ThreadB---lock_b---"); 60 | synchronized(lock_a) { 61 | System.out.println("---ThreadB---lock_a---"); 62 | } 63 | } 64 | } 65 | } 66 | ``` 67 | 程序结构很清晰,没什么难度,先看一下程序的执行结果: 68 | >---ThreadA---lock_a---
69 | >---ThreadA---lock_b---
70 | >---ThreadA---lock_a---
71 | >---ThreadA---lock_b---
72 | >---ThreadA---lock_a---
73 | >---ThreadA---lock_b---
74 | >---ThreadA---lock_a---
75 | >---ThreadB---lock_b---
76 | 77 | 从执行结果来看,线程A跑着跑着,当线程B一跑,啪叽一下就挂了! 78 | 79 | 我们来分析一下原因:从上面的代码中可以看出,定义了一个类Business,该类中维护了两个锁和两个方法,每个方法都是synchronized连环套,并且使用的是不同的锁。好了,现在main方法中开启两个线程A和B,分别执行Business类中的两个方法。A优先执行,跑的很爽,当B线程也开始执行的时候,问题来了,从执行结果的最后两行来看,A线程进入了functionA方法中的第一个synchronized,拿到了lock_a锁,B线程进入了functionB中的第一个synchronized,拿到了lock_b锁,并且两者的锁都还没释放。接下来就是关键了:A线程进入第二个synchronized的时候,发现lock_b正在被B占用,那没办法,它只好被阻塞,等呗~同样地,B线程进入第二个synchronized的时候,发现lock_a正在被A占用,那没办法,它也只好被阻塞,等呗!好了,两个就这样互相等着,你不放,我也不放……死了…… 80 | 81 | 上面这个程序对于理解死锁很有帮助,因为结构很好,不过个人感觉这个死的还不过瘾,因为两个线程是实现了两个不同的Runnable接口,只不过调用了同一个类的两个方法而已,因为我把要同步的方法放到一个类中了。下面我把程序改一下,把要同步的代码放到一个Runnable中,让它一运行就挂掉…… 82 | 83 | ```java 84 | public class DeadLock { 85 | 86 | public static void main(String[] args) { 87 | 88 | //开启两个线程,分别扔两个自定义的Runnable进去 89 | new Thread(new MyRunnable(true)).start();; 90 | new Thread(new MyRunnable(false)).start();; 91 | 92 | } 93 | } 94 | 95 | class MyRunnable implements Runnable 96 | { 97 | private boolean flag; //用于判断,执行不同的同步代码块 98 | 99 | MyRunnable(boolean flag) { //构造方法 100 | this.flag = flag; 101 | } 102 | 103 | @Override 104 | public void run() 105 | { 106 | if(flag) 107 | { 108 | while(true){ 109 | synchronized(MyLock.lock_a) 110 | { 111 | System.out.println("--threadA---lock_a--"); 112 | synchronized(MyLock.lock_b) 113 | { 114 | System.out.println("--threadA---lock_b--"); 115 | } 116 | } 117 | } 118 | } 119 | else 120 | { 121 | while(true){ 122 | synchronized(MyLock.lock_b) 123 | { 124 | System.out.println("--threadB---lock_a--"); 125 | synchronized(MyLock.lock_a) 126 | { 127 | System.out.println("--threadB---lock_b--"); 128 | } 129 | } 130 | } 131 | } 132 | } 133 | } 134 | 135 | class MyLock //把两把锁放到一个类中定义,是为了两个线程使用的都是这两把锁 136 | { 137 | public static final Object lock_a = new Object(); 138 | public static final Object lock_b = new Object(); 139 | } 140 | ``` 141 | 这个死锁就厉害了,一运行,啪叽一下直接就挂掉了……看下运行结果: 142 | 143 | >--threadA---lock_a--
144 | >--threadB---lock_b--
145 | 146 | 以上是死锁的两个例子,都比较容易理解和记忆,主要是“设计模式”不太一样,第一种结构更加清晰,主函数中只要运行逻辑即可,关于同步的部分全扔到Business中,这个便于后期维护,我随便把Business扔到哪去执行都行,因为所有同步的东西都在它自己的类中,这种设计思想很好。第二种是把Runnable先定义好,通过构造方法传进来不同的boolean类型值决定执行run()方法中不同的部分,这种思路也很容易理解,这种死锁更厉害,两个线程直接执行相反的部分,直接挂掉,不给对方一点情面~? 147 | 148 | 如果面试的时候,面试官让你写个死锁,现在会了吗? 149 | 150 | ---- 151 | 152 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   153 | > 154 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   155 | 156 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 157 | 158 | ---- -------------------------------------------------------------------------------- /JavaThread/05. 传统线程同步通信技术.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | ---- 8 | 9 | 先看一个问题: 10 | >有两个线程,子线程先执行10次,然后主线程执行5次,然后再切换到子线程执行10,再主线程执行5次……如此往返执行50次。 11 | 12 | 看完这个问题,很明显要用到线程间的通信了, 先分析一下思路:首先肯定要有两个线程,然后每个线程中肯定有个50次的循环,因为每个线程都要往返执行任务50次,主线程的任务是执行5次,子线程的任务是执行10次。线程间通信技术主要用到`wait()`方法和`notify()`方法。`wait()`方法会导致当前线程等待,并释放所持有的锁,notify()方法表示唤醒在此对象监视器上等待的单个线程。下面来一步步完成这道线程间通信问题。 13 | 14 | 首先不考虑主线程和子线程之间的通信,先把各个线程所要执行的任务写好: 15 | 16 | ```java 17 | public class TraditionalThreadCommunication { 18 | 19 | public static void main(String[] args) { 20 | //开启一个子线程 21 | new Thread(new Runnable() { 22 | 23 | @Override 24 | public void run() { 25 | for(int i = 1; i <= 50; i ++) { 26 | 27 | synchronized (TraditionalThreadCommunication.class) { 28 | //子线程任务:执行10次 29 | for(int j = 1;j <= 10; j ++) { 30 | System.out.println("sub thread sequence of " + j + ", loop of " + i); 31 | } 32 | } 33 | } 34 | 35 | } 36 | }).start(); 37 | 38 | //main方法即主线程 39 | for(int i = 1; i <= 50; i ++) { 40 | 41 | synchronized (TraditionalThreadCommunication.class) { 42 | //主线程任务:执行5次 43 | for(int j = 1;j <= 5; j ++) { 44 | System.out.println("main thread sequence of " + j + ", loop of " + i); 45 | } 46 | } 47 | } 48 | } 49 | } 50 | ``` 51 | 如上,两个线程各有50次大循环,执行50次任务,子线程的任务是执行10次,主线程的任务是执行5次。为了保证两个线程间的同步问题,所以用了`synchronized`同步代码块,并使用了相同的锁:类的字节码对象。这样可以保证线程安全。但是这种设计不太好,就像我在上一节的死锁中写的一样,我们可以把线程任务放到一个类中,这种设计的模式更加结构化,而且把不同的线程任务放到同一个类中会很容易解决同步问题,因为在一个类中很容易使用同一把锁。所以把上面的程序修改一下: 52 | 53 | ```java 54 | public class TraditionalThreadCommunication { 55 | 56 | public static void main(String[] args) { 57 | Business bussiness = new Business(); //new一个线程任务处理类 58 | //开启一个子线程 59 | new Thread(new Runnable() { 60 | 61 | @Override 62 | public void run() { 63 | for(int i = 1; i <= 50; i ++) { 64 | bussiness.sub(i); 65 | } 66 | 67 | } 68 | }).start(); 69 | 70 | //main方法即主线程 71 | for(int i = 1; i <= 50; i ++) { 72 | bussiness.main(i); 73 | } 74 | } 75 | 76 | } 77 | //要用到的共同数据(包括同步锁)或共同的若干个方法应该归在同一个类身上,这种设计正好体现了高类聚和程序的健壮性。 78 | class Business { 79 | 80 | public synchronized void sub(int i) { 81 | 82 | for(int j = 1;j <= 10; j ++) { 83 | System.out.println("sub thread sequence of " + j + ", loop of " + i); 84 | } 85 | } 86 | 87 | public synchronized void main(int i) { 88 | 89 | for(int j = 1;j <= 5; j ++) { 90 | System.out.println("main thread sequence of " + j + ", loop of " + i); 91 | } 92 | } 93 | ``` 94 | 经过这样修改后,程序结构更加清晰了,也更加健壮了,只要在两个线程任务方法上加上`synchronized`关键字即可,用的都是this这把锁。但是现在两个线程之间还没有通信,执行的结果是主线程循环执行任务50次,然后子线程再循环执行任务50次,原因很简单,因为有`synchronized`同步。 95 | 96 | 下面继续完善程序,让两个线程之间完成题目中所描述的那样通信: 97 | 98 | ```java 99 | public class TraditionalThreadCommunication { 100 | 101 | public static void main(String[] args) { 102 | Business bussiness = new Business(); //new一个线程任务处理类 103 | //开启一个子线程 104 | new Thread(new Runnable() { 105 | 106 | @Override 107 | public void run() { 108 | for(int i = 1; i <= 50; i ++) { 109 | bussiness.sub(i); 110 | } 111 | 112 | } 113 | }).start(); 114 | 115 | //main方法即主线程 116 | for(int i = 1; i <= 50; i ++) { 117 | bussiness.main(i); 118 | } 119 | } 120 | 121 | } 122 | //要用到共同数据(包括同步锁)或共同的若干个方法应该归在同一个类身上,这种设计正好体现了高雷剧和程序的健壮性。 123 | class Business { 124 | private boolean bShouldSub = true; 125 | 126 | public synchronized void sub(int i) { 127 | while(!bShouldSub) { //如果不轮到自己执行,就睡 128 | try { 129 | this.wait(); //调用wait()方法的对象必须和synchronized锁对象一致,这里synchronized在方法上,所以用this 130 | } catch (InterruptedException e) { 131 | // TODO Auto-generated catch block 132 | e.printStackTrace(); 133 | } 134 | } 135 | for(int j = 1;j <= 10; j ++) { 136 | System.out.println("sub thread sequence of " + j + ", loop of " + i); 137 | } 138 | bShouldSub = false; //改变标记 139 | this.notify(); //唤醒正在等待的主线程 140 | } 141 | 142 | public synchronized void main(int i) { 143 | while(bShouldSub) { //如果不轮到自己执行,就睡 144 | try { 145 | this.wait(); 146 | } catch (InterruptedException e) { 147 | // TODO Auto-generated catch block 148 | e.printStackTrace(); 149 | } 150 | } 151 | for(int j = 1;j <= 5; j ++) { 152 | System.out.println("main thread sequence of " + j + ", loop of " + i); 153 | } 154 | bShouldSub = true; //改变标记 155 | this.notify(); //唤醒正在等待的子线程 156 | } 157 | } 158 | ``` 159 | 首先,先不说具体的程序实现,就从结构上来看,已经体会到了这种设计的好处了:主函数里不用修改任何东西,关于线程间同步和线程间通信的逻辑全都在Business类中,主函数中的不同线程只需要调用放在该类中对应的任务即可。体现了高类聚的好处。 160 | 161 | 再看一下具体的代码,首先定义一个boolean型变量来标识哪个线程该执行,当不是子线程执行的时候,它就睡,那么很自然主线程就执行了,执行完了,修改了bShouldSub并唤醒了子线程,子线程这时候再判断一下while不满足了,就不睡了,就执行子线程任务,同样地,刚刚主线程修改了bShouldSub后,第二次循环来执行主线程任务的时候,判断while满足就睡了,等待子线程来唤醒。这样逻辑就很清楚了,主线程和子线程你一下我一下轮流执行各自的任务,这种节奏共循环50次。 162 | 163 | 另外有个小小的说明:这里其实用if来判断也是可以的,但是为什么要用while呢?因为有时候线程会假醒(就好像人的梦游,明明正在睡,结果站起来了),如果用的是if的话,那么它假醒了后,就不会再返回去判断if了,那它就很自然的往下执行任务,好了,另一个线程正在执行呢,啪叽一下就与另一个线程之间相互影响了。但是如果是while的话就不一样了,就算线程假醒了,它还会判断一下while的,但是此时另一个线程在执行啊,bShouldSub并没有被修改,所以还是进到while里了,又被睡了!所以很安全,不会影响另一个线程!官方JDK文档中也是这么干的。 164 | 165 | 传统线程间通信就总结到这吧,欢迎拍砖…… 166 | 167 | ---- 168 | 169 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   170 | > 171 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   172 | 173 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 174 | 175 | ---- -------------------------------------------------------------------------------- /JavaThread/06. 线程范围内共享数据.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | ---- 8 | 9 | 假设现在有个公共的变量 data,有不同的线程都可以去操作它,如果在不同的线程对 data 操作完成后再去取这个 data,那么肯定会出现线程间的数据混乱问题,因为 A 线程在取 data 数据前可能 B 线程又对其进行了修改,下面写个程序来说明一下该问题: 10 | 11 | ```java 12 | public class ThreadScopeShareData { 13 | 14 | private static int data = 0;//公共的数据 15 | 16 | public static void main(String[] args) { 17 | for(int i = 0; i < 2; i ++) { //开启两个线程 18 | new Thread(new Runnable() { 19 | 20 | @Override 21 | public void run() { 22 | int temp = new Random().nextInt(); 23 | System.out.println(Thread.currentThread().getName() + " has put a data: " + temp); //打印出来为了看效果 24 | data = temp; //操作数据:赋新值 25 | 26 | new TestA().getData(); 27 | new TestB().getData(); 28 | } 29 | }).start(); 30 | } 31 | } 32 | 33 | static class TestA { 34 | public void getData() { 35 | System.out.println("A get data from " + Thread.currentThread().getName() + ": " + data);//取出公共数据data 36 | } 37 | } 38 | 39 | static class TestB { 40 | public void getData() { 41 | System.out.println("B get data from " + Thread.currentThread().getName() + ": " + data); 42 | } 43 | } 44 | } 45 | ``` 46 | 来看一下打印出来的结果: 47 | >Thread-0 has put a data: -1885917900
48 | Thread-1 has put a data: -1743455464
49 | A get data from Thread-0: -1743455464
50 | A get data from Thread-1: -1743455464
51 | B get data from Thread-1: -1743455464
52 | B get data from Thread-0: -1743455464
53 | 54 | 从结果中可以看出,两次对 data 赋的值确实不一样,但是两个线程最后打印出来的都是最后赋的那个值,说明 Thread-0 拿出的数据已经不对了,这就是线程间共享数据带来的问题。 55 | 56 | 当然,我们完全可以使用 synchronized 关键字将 `run()` 方法中的几行代码给套起来,这样每个线程各自执行完,打印出各自的信息,这是没问题的,确实可以解决上面的线程间共享数据问题。但是,这是以其他线程被阻塞为代价的,即 Thread-0 在执行的时候,Thread-1 就被阻塞了,必须等待 Thread-0 执行完了才能执行。 57 | 58 | 那么如果我想两个线程同时跑,并且互不影响各自取出的值,该怎么办呢?这也是本文所要总结的重点,解决该问题的思想是:虽然现在都在操作公共数据 data,但是不同的线程本身对这个 data 要维护一个副本,这个副本不是线程间所共享的,而是每个线程所独有的,所以不同线程中所维护的 data 是不一样的,最后取的时候,是哪个线程,我就从哪个线程中取该 data。 59 | 60 | 基于上面这个思路,我再把上面的程序做一修改,如下: 61 | 62 | ```java 63 | public class ThreadScopeShareData { 64 | 65 | private static int data = 0;//公共的数据 66 | //定义一个Map以键值对的方式存储每个线程和它对应的数据,即Thread:data 67 | private static Map threadData = Collections.synchronizedMap(new HashMap()); 68 | 69 | public static void main(String[] args) { 70 | for(int i = 0; i < 2; i ++) { 71 | new Thread(new Runnable() { 72 | 73 | @Override 74 | public void run() { 75 | int temp = new Random().nextInt(); 76 | System.out.println(Thread.currentThread().getName() + " has put a data: " + temp); //打印出来为了看效果 77 | threadData.put(Thread.currentThread(), temp); //向Map中存入本线程data数据的一个副本 78 | data = temp; //操作数据:赋新值 79 | new TestA().getData(); 80 | new TestB().getData(); 81 | } 82 | }).start(); 83 | } 84 | } 85 | 86 | static class TestA { 87 | public void getData() { 88 | System.out.println("A get data from " + Thread.currentThread().getName() + ": " 89 | + threadData.get(Thread.currentThread())); //取出各线程维护的那个副本 90 | } 91 | } 92 | 93 | static class TestB { 94 | public void getData() { 95 | System.out.println("B get data from " + Thread.currentThread().getName() + ": " 96 | + threadData.get(Thread.currentThread())); 97 | } 98 | } 99 | } 100 | ``` 101 | 上面程序中维护了一个 Map,键值对分别是线程和它的数据,那么在操作 data 的时候,先把各自的数据保存到这个 Map 中,这样每个线程保存的肯定不同,当再取的时候,根据当前线程对象作为 key 来取出对应的 data 副本,这样不同的线程之间就不会相互影响了。这个 HashMap 也需要包装一下,因为 HashMap 是非线程安全的,上面的程序中,不同的线程有对 HashMap 进行写操作,就有可能产生并发问题,所以也要包装一下。最后来看一下执行结果: 102 | >Thread-0 has put a data: 1817494992
103 | Thread-1 has put a data: -1189758355
104 | A get data from Thread-0: 1817494992
105 | A get data from Thread-1: -1189758355
106 | B get data from Thread-0: 1817494992
107 | B get data from Thread-1: -1189758355
108 | 109 | 就是线程范围内共享数据,即同一个线程里面这个数据是共享的,线程间是不共享的。 110 | 111 | 这让我联想到了学习数据库的时候用到的 ThreadLocal,操作数据库需要 connection,如果当前线程中有就拿当前线程中存的 connection,否则就新建一个放到当前线程中,这样就不会出现问题,因为每个线程本身共享了一个 connection,它不是线程间共享的。这也很好理解,这个 connection 肯定不能共享,假设 A 和 B 用户都拿到这个 connection 并开启了事务,现在 A 开始转账了,但是钱还没转好,B 转好了关闭了事务,那么A那边就出问题了。 112 | 113 | 线程范围内共享数据的问题就总结这么多吧~如果有问题,欢迎指正,我们一起进步! 114 | 115 | ---- 116 | 117 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   118 | > 119 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   120 | 121 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 122 | 123 | ---- -------------------------------------------------------------------------------- /JavaThread/07. 原子性操作类的使用.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | ---- 8 | 9 | 在 java5 以后,我们接触到了线程原子性操作,也就是在修改时我们只需要保证它的那个瞬间是安全的即可,经过相应的包装后可以再处理对象的并发修改,本文总结一下Atomic系列的类的使用方法,其中包含: 10 | 11 | | 类型 | Integer | Long | | 12 | | :--------: | :--------:| :--: | :--: | 13 | |基本类型|AtomicInteger|AtomicLong|AtomicBoolean| 14 | |数组类型|AtomicIntegerArray|AtomicLongArray|AtomicReferenceArray| 15 | |属性原子修改器|AtomicIntegerFieldUpdater|AtomicLongFieldUpdater|AtomicReferenceFieldUpdater| 16 | 17 | ### 1. 基本类型的使用 18 | 19 | 首先看一下AtomicInteger的使用,AtomicInteger主要是针对整数的修改的,看一下示例代码: 20 | ```java 21 | public class AtomicIntegerDemo { 22 | 23 | /** 24 | * 常见的方法列表 25 | * @see AtomicInteger#get() 直接返回值 26 | * @see AtomicInteger#getAndAdd(int) 增加指定的数据,返回变化前的数据 27 | * @see AtomicInteger#getAndDecrement() 减少1,返回减少前的数据 28 | * @see AtomicInteger#getAndIncrement() 增加1,返回增加前的数据 29 | * @see AtomicInteger#getAndSet(int) 设置指定的数据,返回设置前的数据 30 | * 31 | * @see AtomicInteger#addAndGet(int) 增加指定的数据后返回增加后的数据 32 | * @see AtomicInteger#decrementAndGet() 减少1,返回减少后的值 33 | * @see AtomicInteger#incrementAndGet() 增加1,返回增加后的值 34 | * @see AtomicInteger#lazySet(int) 仅仅当get时才会set 35 | * 36 | * @see AtomicInteger#compareAndSet(int, int) 尝试新增后对比,若增加成功则返回true否则返回false 37 | */ 38 | public final static AtomicInteger TEST_INTEGER = new AtomicInteger(1); 39 | 40 | public static void main(String []args) { 41 | 42 | for(int i = 0 ; i < 10 ; i++) { //开启10个线程 43 | 44 | new Thread() { 45 | public void run() { 46 | try { 47 | Thread.sleep(1000); 48 | } catch (InterruptedException e) { 49 | e.printStackTrace(); 50 | } 51 | int now = TEST_INTEGER.incrementAndGet(); //自增 52 | System.out.println(Thread.currentThread().getName() + " get value : " + now); 53 | } 54 | }.start(); 55 | } 56 | } 57 | } 58 | ``` 59 | 看一下结果: 60 | >Thread-3 get value : 4
61 | Thread-7 get value : 5
62 | Thread-9 get value : 9
63 | Thread-4 get value : 6
64 | Thread-0 get value : 3
65 | Thread-1 get value : 8
66 | Thread-5 get value : 11
67 | Thread-8 get value : 7
68 | Thread-2 get value : 10
69 | Thread-6 get value : 2
70 | 71 | 可以看出,10个线程之间是线程安全的,并没有冲突。也就是说,我们使用原子性操作类去操作基本类型int就可以解决线程安全问题,一个线程在操作的时候,会对其它线程进行排斥,不用我们手动去使用synchronized实现互斥操作了。AtomicLong和AtomicBoolean类似,就不举例子了。 72 | 73 | ### 2. 数组类型的使用 74 | 75 | 下面要开始说Atomic的数组用法,Atomic的数组要求不允许修改长度等,不像集合类那么丰富的操作,不过它可以让数组上每个元素的操作绝对安全的,也就是它细化的力度还是到数组上的元素,做了二次包装,虽然是数组类型的,但是最后还是操作数组中存的数,所以会了上面的基本类型的话,数组类型也很好理解。这里主要看一下AtomicIntegerArray的使用,其它的类似。 76 | ```java 77 | public class AtomicIntegerArrayTest { 78 | 79 | /** 80 | * 常见的方法列表 81 | * @see AtomicIntegerArray#addAndGet(int, int) 执行加法,第一个参数为数组的下标,第二个参数为增加的数量,返回增加后的结果 82 | * @see AtomicIntegerArray#compareAndSet(int, int, int) 对比修改,参数1:数组下标,参数2:原始值,参数3,修改目标值,修改成功返回true否则false 83 | * @see AtomicIntegerArray#decrementAndGet(int) 参数为数组下标,将数组对应数字减少1,返回减少后的数据 84 | * @see AtomicIntegerArray#incrementAndGet(int) 参数为数组下标,将数组对应数字增加1,返回增加后的数据 85 | * 86 | * @see AtomicIntegerArray#getAndAdd(int, int) 和addAndGet类似,区别是返回值是变化前的数据 87 | * @see AtomicIntegerArray#getAndDecrement(int) 和decrementAndGet类似,区别是返回变化前的数据 88 | * @see AtomicIntegerArray#getAndIncrement(int) 和incrementAndGet类似,区别是返回变化前的数据 89 | * @see AtomicIntegerArray#getAndSet(int, int) 将对应下标的数字设置为指定值,第二个参数为设置的值,返回是变化前的数据 90 | */ 91 | private final static AtomicIntegerArray ATOMIC_INTEGER_ARRAY = new AtomicIntegerArray(new int[]{1,2,3,4,5,6,7,8,9,10}); 92 | 93 | public static void main(String []args) throws InterruptedException { 94 | Thread []threads = new Thread[10]; 95 | for(int i = 0 ; i < 10 ; i++) { 96 | final int index = i; 97 | final int threadNum = i; 98 | threads[i] = new Thread() { 99 | public void run() { 100 | int result = ATOMIC_INTEGER_ARRAY.addAndGet(index, index + 1); 101 | System.out.println("线程编号为:" + threadNum + " , 对应的原始值为:" + (index + 1) + ",增加后的结果为:" + result); 102 | } 103 | }; 104 | threads[i].start(); 105 | } 106 | for(Thread thread : threads) { 107 | thread.join(); 108 | } 109 | System.out.println("=========================>\n执行已经完成,结果列表:"); 110 | for(int i = 0 ; i < ATOMIC_INTEGER_ARRAY.length() ; i++) { 111 | System.out.println(ATOMIC_INTEGER_ARRAY.get(i)); 112 | } 113 | } 114 | } 115 | ``` 116 | 运行结果是给每个数组元素加上相同的值,它们之间互不影响。 117 | 118 | ### 3. 作为类属性的使用 119 | 120 | 当某个数据类型是某个类中的一个属性的时候,然后我们要操作该数据,就需要使用属性原子修改器了,这里还是以Integer为例,即:AtomicIntegerFieldUpdater。示例代码如下: 121 | ```java 122 | public class AtomicIntegerFieldUpdaterTest { 123 | 124 | static class A { 125 | volatile int intValue = 100; 126 | } 127 | 128 | /** 129 | * 可以直接访问对应的变量,进行修改和处理 130 | * 条件:要在可访问的区域内,如果是private或挎包访问default类型以及非父亲类的protected均无法访问到 131 | * 其次访问对象不能是static类型的变量(因为在计算属性的偏移量的时候无法计算),也不能是final类型的变量(因为根本无法修改),必须是普通的成员变量 132 | * 133 | * 方法(说明上和AtomicInteger几乎一致,唯一的区别是第一个参数需要传入对象的引用) 134 | * @see AtomicIntegerFieldUpdater#addAndGet(Object, int) 135 | * @see AtomicIntegerFieldUpdater#compareAndSet(Object, int, int) 136 | * @see AtomicIntegerFieldUpdater#decrementAndGet(Object) 137 | * @see AtomicIntegerFieldUpdater#incrementAndGet(Object) 138 | * 139 | * @see AtomicIntegerFieldUpdater#getAndAdd(Object, int) 140 | * @see AtomicIntegerFieldUpdater#getAndDecrement(Object) 141 | * @see AtomicIntegerFieldUpdater#getAndIncrement(Object) 142 | * @see AtomicIntegerFieldUpdater#getAndSet(Object, int) 143 | */ 144 | public final static AtomicIntegerFieldUpdater ATOMIC_INTEGER_UPDATER = AtomicIntegerFieldUpdater.newUpdater(A.class, "intValue"); 145 | 146 | public static void main(String []args) { 147 | final A a = new A(); 148 | for(int i = 0 ; i < 10 ; i++) { 149 | 150 | new Thread() { 151 | public void run() { 152 | if(ATOMIC_INTEGER_UPDATER.compareAndSet(a, 100, 120)) { 153 | System.out.println(Thread.currentThread().getName() + " 对对应的值做了修改!"); 154 | } 155 | } 156 | }.start(); 157 | } 158 | } 159 | } 160 | ``` 161 | 可以看到,这里需要将类和类属性传进去才行,传进去后其实跟前面操作Integer没什么不同了,本质都一样的,运行一下,结果只有一个线程能对其进行修改。 162 | 163 | 线程的原子性操作类的使用就简单总结到这,其他的操作类原理都相似,可以参考 JDK 的文档,可以很容易写出相应的测试代码。 164 | 165 | 原子性操作类的使用就分享这么多,如有错误之处,欢迎指正,我们一同进步~ 166 | 167 | ---- 168 | 169 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   170 | > 171 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   172 | 173 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 174 | 175 | ---- -------------------------------------------------------------------------------- /JavaThread/08. ThreadLocal 类以及应用技巧.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | ---- 8 | 9 | 在上一节我们总结了一下,线程范围内的数据共享问题,即定义一个 Map,将当前线程名称和线程中的数据以键值对的形式存到 Map 中,然后在当前线程中使用数据的时候就可以根据当前线程名称从 Map 中拿到当前线程中的数据,这样就可以做到不同线程之间数据互不干扰。其实 ThreadLocal 类就是给我们提供了这个解决方法,所以我们完全可以用 ThreadLocal 来完成线程范围内数据的共享。 10 | 11 | ```java 12 | public class ThreadScopeShareData { 13 | //定义一个ThreadLocal 14 | private static ThreadLocal threadLocal = new ThreadLocal(); 15 | 16 | public static void main(String[] args) { 17 | for(int i = 0; i < 2; i ++) { 18 | new Thread(new Runnable() { 19 | 20 | @Override 21 | public void run() { 22 | int data = new Random().nextInt(); 23 | System.out.println(Thread.currentThread().getName() + " has put a data: " + data); 24 | threadLocal.set(data);//直接往threadLocal里面里面扔数据即可 25 | new TestA().getData(); 26 | new TestB().getData(); 27 | } 28 | }).start(); 29 | } 30 | } 31 | 32 | static class TestA { 33 | public void getData() { 34 | System.out.println("A get data from " + Thread.currentThread().getName() + ": " + threadLocal.get());//直接取,不用什么关键字,它直接从当前线程中取 35 | } 36 | } 37 | 38 | static class TestB { 39 | public void getData() { 40 | System.out.println("B get data from " + Thread.currentThread().getName() + ": " + threadLocal.get());//直接取,不用什么关键字,它直接从当前线程中取 41 | } 42 | } 43 | } 44 | ``` 45 | 结合第 6 节的代码,可以看出,其实 ThreadLocal 就相当于一个 Map,只不过我们不需要设定 key 了,它默认就是当前的 Thread,往里面放数据,直接 set 即可,取数据,直接 get 即可,很方便,就不用 Map 一个个存了, 46 | 47 | 但是问题来了,ThreadLocal 虽然存取方便,但是 `get()` 方法中根本没有参数,也就是说我们只能往 ThreadLocal 中放一个数据,多了就不行了,那么该如何解决这个问题呢? 48 | 49 | 很明显,ThreadLocal 是个容器,且只能存一下,那么如果有多个数据,我们可以定义一个类,把数据都封装到这个类中,然后扔到 ThreadLocal 中,用的时候取这个类,再从类中去我们想要的数据即可。 50 | 51 | 好,现在有两个线程,每个线程都要操作各自的数据,而且数据有两个:名字和年龄。根据上面的思路,写一个 demo,如下: 52 | 53 | ```java 54 | public class ThreadScopeShareData { 55 | 56 | private static ThreadLocal threadLocal = new ThreadLocal(); 57 | 58 | public static void main(String[] args) { 59 | for(int i = 0; i < 2; i ++) {//开启两个线程 60 | new Thread(new Runnable() { 61 | 62 | @Override 63 | public void run() { 64 | int data = new Random().nextInt(); 65 | System.out.println(Thread.currentThread().getName() + " has put a data: " + data); 66 | 67 | //每个线程中维护一个User,User中保存了name和age 68 | User user = new User(); 69 | user.setName("name" + data); 70 | user.setAge(data); 71 | threadLocal.set(user); //向当前线程中存入user对象 72 | 73 | new TestA().getData(); 74 | new TestB().getData(); 75 | } 76 | }).start(); 77 | } 78 | } 79 | 80 | static class TestA { 81 | public void getData() { 82 | 83 | User user = threadLocal.get();//从当前线程中取出user对象 84 | System.out.println("A get data from " + Thread.currentThread().getName() + ": " 85 | + user.getName() + "," + user.getAge()); 86 | } 87 | } 88 | 89 | static class TestB { 90 | public void getData() { 91 | 92 | User user = threadLocal.get();//从当前线程中取出user对象 93 | System.out.println("B get data from " + Thread.currentThread().getName() + ": " 94 | + user.getName() + "," + user.getAge()); 95 | 96 | } 97 | } 98 | 99 | } 100 | //定义一个User类来存储姓名和年龄 101 | class User { 102 | 103 | private String name; 104 | private int age; 105 | public String getName() { 106 | return name; 107 | } 108 | public void setName(String name) { 109 | this.name = name; 110 | } 111 | public int getAge() { 112 | return age; 113 | } 114 | public void setAge(int age) { 115 | this.age = age; 116 | } 117 | } 118 | ``` 119 | 这样进行一下封装就可以实现多个数据的存储了,但是上面这个程序是不太好的,原因很明显,在线程中,我要自己 new 一个对象,然后对其进行操作,最后还得把这个对象扔到当前线程中。这不太符合设计的思路,设计的思路应该是这样的,不能让用户自己去 new 啊,如果有个类似于 `getThreadInstance()` 的方法,用户想要从 ThreadLocal 中拿什么对象就用该对象去调用这个 `getThreadInstance()` 方法多好,这样拿到的永远都是本线程范围内的对象了。 120 | 121 | 这让我想到了学习 JDBC 的时候,从 ThreadLocal 中拿 connection 时的做法了,如果当前ThreadLocal中有就拿出来,没有就产生一个,这跟这里的需求是一样的,我想要一个User,那我应该用User去调用`getThreadLInstance()`方法获取本线程中的一个User对象,如果有就拿,如果没有就产生一个。完全一样的思路。这个设计跟单例的模式有点像,这里说有点像不是本质上像,是代码结构很像。先看一下简单的单例模式代码结构: 122 | 123 | ```java 124 | public class Singleton { 125 | private static Singleton instance = null; 126 | private Singleton() {//私有构造方法阻止外界new 127 | } 128 | public static synchronized Singleton getInstance() { //提供一个公共方法返回给外界一个单例的实例 129 | if (instance == null) { //如果没有实例 130 | instance = new Singleton(); //就新new一个 131 | } 132 | return instance; //返回该实例 133 | } 134 | } 135 | ``` 136 | 这是懒汉式单例模式的代码结构,我门完全可以效仿该思路去设计一个从当前线程中拿 User 的办法,所以将程序修改如下: 137 | 138 | ```java 139 | public class ThreadScopeShareData { 140 | //不需要在外面定义threadLocal了,放到User类中了 141 | // private static ThreadLocal threadLocal = new ThreadLocal(); 142 | 143 | public static void main(String[] args) { 144 | for(int i = 0; i < 2; i ++) { 145 | new Thread(new Runnable() { 146 | 147 | @Override 148 | public void run() { 149 | int data = new Random().nextInt(); 150 | System.out.println(Thread.currentThread().getName() + " has put a data: " + data); 151 | 152 | //这里直接用User去调用getThreadLocal这个静态方法获取本线程范围内的一个User对象 153 | //这里就优雅多了,我完全不用关心如何去拿该线程中的对象,如何把对象放到threadLocal中 154 | //我只要拿就行,而且拿出来的肯定就是当前线程中的对象,原因看下面User类中的设计 155 | User.getThreadInstance().setName("name" + data); 156 | User.getThreadInstance().setAge(data); 157 | 158 | new TestA().getData(); 159 | new TestB().getData(); 160 | } 161 | }).start(); 162 | } 163 | } 164 | 165 | static class TestA { 166 | public void getData() { 167 | //还是调用这个静态方法拿,因为刚刚已经拿过一次了,threadLocal中已经有了 168 | User user = User.getThreadInstance(); 169 | System.out.println("A get data from " + Thread.currentThread().getName() + ": " 170 | + user.getName() + "," + user.getAge()); 171 | } 172 | } 173 | 174 | static class TestB { 175 | public void getData() { 176 | 177 | User user = User.getThreadInstance(); 178 | System.out.println("A get data from " + Thread.currentThread().getName() + ": " 179 | + user.getName() + "," + user.getAge()); 180 | } 181 | } 182 | 183 | } 184 | 185 | class User { 186 | 187 | private User() {} 188 | 189 | private static ThreadLocal threadLocal = new ThreadLocal(); 190 | 191 | //注意,这不是单例,每个线程都可以new,所以不用synchronized, 192 | //但是每个threadLocal中是单例的,因为有了的话就不会再new了 193 | public static /*synchronized*/ User getThreadInstance() { 194 | User instance = threadLocal.get(); //先从当前threadLocal中拿 195 | if(instance == null) { 196 | instance = new User(); 197 | threadLocal.set(instance);//如果没有就新new一个放到threadLocal中 198 | } 199 | return instance; //向外返回该User 200 | } 201 | 202 | private String name; 203 | private int age; 204 | public String getName() { 205 | return name; 206 | } 207 | public void setName(String name) { 208 | this.name = name; 209 | } 210 | public int getAge() { 211 | return age; 212 | } 213 | public void setAge(int age) { 214 | this.age = age; 215 | } 216 | } 217 | ``` 218 | 经过这样的改造,代码就优雅多了,外界从来不要考虑如何去当前线程中拿数据,只要拿就行,拿出来的肯定就是当前线程中你想要的对象,因为在对象内部已经写好了这个静态方法了,而且拿出来操作完了后,也不需要再放到 threadLocal 中,因为它本来就在 threadLocal 中,这就封装的相当好了。 219 | 220 | ThreadLocal类的应用和使用技巧就总结这么多吧~如有问题,欢迎交流,我们一起进步! 221 | 222 | ---- 223 | 224 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   225 | > 226 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   227 | 228 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 229 | 230 | ---- -------------------------------------------------------------------------------- /JavaThread/09. 造成 HashMap 非线程安全的原因.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | ---- 8 | 9 | 在前面我的一篇总结线程范围内共享数据d 文章中提到,为了数据能在线程范围内使用,我用了 HashMap 来存储不同线程中的数据,key 为当前线程,value 为当前线程中的数据。我取的时候根据当前线程名从 HashMap 中取即可。 10 | 11 | 因为当初学习 HashMap 和 HashTable 源码的时候,知道 HashTable 是线程安全的,因为里面的方法使用了 synchronized 进行同步,但是 HashMap 没有,所以 HashMap 是非线程安全的。 12 | 13 | 在上面提到的例子中,我想反正不用修改 HashMap,只需要从中取值即可,所以不会有线程安全问题,但是我忽略了一个步骤:我得先把不同线程的数据存到 HashMap 中吧,这个存就可能出现问题,虽然我存的时候 key 使用了不同的线程名字,理论上来说是不会冲突的,但是这种设计或者思想本来就不够严谨。我后来仔细推敲了下,重新温习了下 HashMap 的源码,再加上网上查的一些资料,在这里总结一下 HashMap 到底什么时候可能出现线程安全问题。 14 | 15 | 我们知道 HashMap 底层是一个 Entry 数组,当发生 hash 冲突的时候,HashMap 是采用链表的方式来解决的,在对应的数组位置存放链表的头结点。对链表而言,新加入的节点会从头结点加入。javadoc 中有一段关于 HashMap 的描述: 16 | 17 | >此实现不是同步的。如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。(结构上的修改是指添加或删除一个或多个映射关系的任何操作;仅改变与实例已经包含的键关联的值不是结构上的修改。)这一般通过对自然封装该映射的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedMap 方法来“包装”该映射。最好在创建时完成这一操作,以防止对映射进行意外的非同步访问,如下所示: 18 | `Map m = Collections.synchronizedMap(new HashMap(...));` 19 | 20 | 可以看出,解决 HashMap 线程安全问题的方法很简单,下面我简单分析一下可能会出现线程问题的一些地方。 21 | 22 | ## 1. 向HashMap中插入数据的时候 23 | 24 | 在 HashMap 做 put 操作的时候会调用到以下的方法: 25 | 26 | ```java 27 | //向HashMap中添加Entry 28 | void addEntry(int hash, K key, V value, int bucketIndex) { 29 | if ((size >= threshold) && (null != table[bucketIndex])) { 30 | resize(2 * table.length); //扩容2倍 31 | hash = (null != key) ? hash(key) : 0; 32 | bucketIndex = indexFor(hash, table.length); 33 | } 34 | 35 | createEntry(hash, key, value, bucketIndex); 36 | } 37 | //创建一个Entry 38 | void createEntry(int hash, K key, V value, int bucketIndex) { 39 | Entry e = table[bucketIndex];//先把table中该位置原来的Entry保存 40 | //在table中该位置新建一个Entry,将原来的Entry挂到该Entry的next 41 | table[bucketIndex] = new Entry<>(hash, key, value, e); 42 | //所以table中的每个位置永远只保存一个最新加进来的Entry,其他Entry是一个挂一个,这样挂上去的 43 | size++; 44 | } 45 | ``` 46 | 现在假如 A 线程和 B 线程同时进入 addEntry,然后计算出了相同的哈希值对应了相同的数组位置,因为此时该位置还没数据,然后对同一个数组位置调用 createEntry,两个线程会同时得到现在的头结点,然后 A 写入新的头结点之后,B 也写入新的头结点,那 B 的写入操作就会覆盖A的写入操作造成 A 的写入操作丢失。 47 | 48 | ## 2. HashMap扩容的时候 49 | 50 | 还是上面那个 addEntry 方法中,有个扩容的操作,这个操作会新生成一个新的容量的数组,然后对原数组的所有键值对重新进行计算和写入新的数组,之后指向新生成的数组。来看一下扩容的源码: 51 | 52 | ```java 53 | //用新的容量来给table扩容 54 | void resize(int newCapacity) { 55 | Entry[] oldTable = table; //保存old table 56 | int oldCapacity = oldTable.length; //保存old capacity 57 | // 如果旧的容量已经是系统默认最大容量了,那么将阈值设置成整形的最大值,退出 58 | if (oldCapacity == MAXIMUM_CAPACITY) { 59 | threshold = Integer.MAX_VALUE; 60 | return; 61 | } 62 | 63 | //根据新的容量新建一个table 64 | Entry[] newTable = new Entry[newCapacity]; 65 | //将table转换成newTable 66 | transfer(newTable, initHashSeedAsNeeded(newCapacity)); 67 | table = newTable; 68 | //设置阈值 69 | threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); 70 | } 71 | ``` 72 | 那么问题来了,当多个线程同时进来,检测到总数量超过门限值的时候就会同时调用 resize操作,各自生成新的数组并 rehash 后赋给该 map 底层的数组 table,结果最终只有最后一个线程生成的新数组被赋给 table 变量,其他线程的均会丢失。而且当某些线程已经完成赋值而其他线程刚开始的时候,就会用已经被赋值的 table 作为原始数组,这样也会有问题。所以在扩容操作的时候也有可能会引起一些并发的问题。 73 | 74 | ## 3. 删除HashMap中数据的时候 75 | 76 | 删除键值对的源代码如下: 77 | ```java 78 | //根据指定的key删除Entry,返回对应的value 79 | public V remove(Object key) { 80 | Entry e = removeEntryForKey(key); 81 | return (e == null ? null : e.value); 82 | } 83 | 84 | //根据指定的key,删除Entry,并返回对应的value 85 | final Entry removeEntryForKey(Object key) { 86 | if (size == 0) { 87 | return null; 88 | } 89 | int hash = (key == null) ? 0 : hash(key); 90 | int i = indexFor(hash, table.length); 91 | Entry prev = table[i]; 92 | Entry e = prev; 93 | 94 | while (e != null) { 95 | Entry next = e.next; 96 | Object k; 97 | if (e.hash == hash && 98 | ((k = e.key) == key || (key != null && key.equals(k)))) { 99 | modCount++; 100 | size--; 101 | if (prev == e) //如果删除的是table中的第一项的引用 102 | table[i] = next;//直接将第一项中的next的引用存入table[i]中 103 | else 104 | prev.next = next; //否则将table[i]中当前Entry的前一个Entry中的next置为当前Entry的next 105 | e.recordRemoval(this); 106 | return e; 107 | } 108 | prev = e; 109 | e = next; 110 | } 111 | 112 | return e; 113 | } 114 | ``` 115 | 删除这一块可能会出现两种线程安全问题,第一种是一个线程判断得到了指定的数组位置i并进入了循环,此时,另一个线程也在同样的位置已经删掉了i位置的那个数据了,然后第一个线程那边就没了。但是删除的话,没了倒问题不大。 116 | 117 | 再看另一种情况,当多个线程同时操作同一个数组位置的时候,也都会先取得现在状态下该位置存储的头结点,然后各自去进行计算操作,之后再把结果写会到该数组位置去,其实写回的时候可能其他的线程已经就把这个位置给修改过了,就会覆盖其他线程的修改。 118 | 119 | 其他地方还有很多可能会出现线程安全问题,我就不一一列举了,总之 HashMap 是非线程安全的,在高并发的场合使用的话,要用 Collections.synchronizedMap 进行包装一下,或者直接使用 ConcurrentHashMap 都行。 120 | 121 | 关于 HashMap 的线程非安全性,就总结这么多,如有问题,欢迎交流,我们一同进步~ 122 | 123 | 124 | ---- 125 | 126 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   127 | > 128 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   129 | 130 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 131 | 132 | ---- -------------------------------------------------------------------------------- /JavaThread/10. 多个线程间共享数据.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | ---- 8 | 9 | 先看一个多线程间共享数据的问题: 10 | 11 | >设计四个线程,其中两个线程每次对data增加1,另外两个线程每次对data减少1。 12 | 13 | 从问题来看,很明显涉及到了线程间通数据的共享,四个线程共享一个 data,共同操作一个 data。我们先把上面这个问题放在一边,慢慢分析多个线程之间共享数据的一些情况,从最简单开始,分类分析完了后,到时候也好解决上面这个问题了。 14 | 15 | ## 1. 每个线程执行的任务相同 16 | 17 | 这是最简单的一种情况,比如卖票,几个线程共同操作记录票数的那个变量,任务都是使它减一。针对这种情况,我们只需要写一个类实现 Runnable 接口即可,在 `run()` 方法中对这个票进行减一,然后将这个 Runnable 扔给多个线程去执行,自然它们就操作同一个data了。看一下代码: 18 | ```java 19 | public class MultiThreadShareData { 20 | 21 | public static void main(String[] args) { 22 | 23 | ShareData task = new ShareData(); //一个类实现了Runnable接口 24 | 25 | for(int i = 0; i < 4; i ++) { //四个线程来卖票 26 | new Thread(task).start(); 27 | } 28 | 29 | } 30 | 31 | } 32 | 33 | class ShareData implements Runnable { 34 | 35 | private int data = 100; 36 | @Override 37 | public void run() { //卖票,每次一个线程进来,先判断票数是否大于0 38 | // while(data > 0) { 39 | synchronized(this) { 40 | if(data > 0) { 41 | System.out.println(Thread.currentThread().getName() + ": " + data); 42 | data--; 43 | } 44 | } 45 | // } 46 | } 47 | } 48 | ``` 49 | 这很好理解,也很容易实现,四个线程卖了4张票。运行结果为: 50 | >Thread-0: 100
51 | Thread-3: 99
52 | Thread-2: 98
53 | Thread-1: 97
54 | 55 | ## 2. 每个线程执行不同的任务 56 | 57 | 就如上面那个题目所描述的,两个线程执行 data 增,两个线程执行 data 减。针对这种情况,我们要实现两个 Runnable 了,因为很明显有两个不同的任务了,一个任务执行 data 增,另一个任务执行 data 减。为了便于维护,可以将两个任务方法放到一个类中,然后将 data 也放在这个类中,然后传到不同的 Runnabl e中,即可完成数据的共享。如下: 58 | ```java 59 | public class MultiThreadShareData { 60 | 61 | public static void main(String[] args) { 62 | 63 | ShareData task = new ShareData(); //公共数据和任务放在task中 64 | 65 | for(int i = 0; i < 2; i ++) { //开启两个线程增加data 66 | 67 | new Thread(new Runnable() { 68 | 69 | @Override 70 | public void run() { 71 | task.increment(); 72 | } 73 | }).start(); 74 | } 75 | 76 | for(int i = 0; i < 2; i ++) { //开启两个线程减少data 77 | 78 | new Thread(new Runnable() { 79 | 80 | @Override 81 | public void run() { 82 | task.decrement(); 83 | } 84 | }).start(); 85 | } 86 | } 87 | } 88 | 89 | 90 | class ShareData /*implements Runnable*/ { 91 | 92 | private int data = 0; 93 | public synchronized void increment() { //增加data 94 | System.out.println(Thread.currentThread().getName() + ": before : " + data); 95 | data++; 96 | System.out.println(Thread.currentThread().getName() + ": after : " + data); 97 | } 98 | 99 | public synchronized void decrement() { //减少data 100 | System.out.println(Thread.currentThread().getName() + ": before : " + data); 101 | data--; 102 | System.out.println(Thread.currentThread().getName() + ": after : " + data); 103 | } 104 | } 105 | ``` 106 | 看一下打印结果: 107 | >Thread-0: before : 0
108 | Thread-0: after : 1
109 | Thread-1: before : 1
110 | Thread-1: after : 2
111 | Thread-2: before : 2
112 | Thread-2: after : 1
113 | Thread-3: before : 1
114 | Thread-3: after : 0
115 | 116 | 这样写的好处是两个任务方法可以直接在方法名上进行同步操作,这种模式的好处在前面的博文中已经有说过了,封装的好。 117 | 118 | 最后总结一下,多个线程之间共享数据主要关注两点就行:一是什么任务?几个任务?二是几个线程?记住 一点:几个任务和几个线程是没有关系的!100个线程可以执行一个任务,也可以执行2个任务,3个任务…… 119 | 120 | 如果只有一个任务,那说明多个线程执行一个任务,我们只要实现一个 Runnable 接口,把公共 data 放进 Runnable,把任务放进去 `run()` 中即可(任务注意要同步),然后开启N个线程去执行这个任务即可;如果有M个任务,那我们新建一个专门执行任务的类,把公共的 data 放进类中,把任务作为类中的同步方法即可,然后开启N个线程,每个线程中扔一个 Runnable,按照要求执行任务类中的方法即可。 121 | 122 | 到这里,读者应该能体会到任务和线程的分离了,这种思想也算是面向对象的一种吧,思路很清晰。 123 | 124 | 多个线程之间共享数据就总结这么多,如有问题,欢迎交流,我们共同进步~ 125 | 126 | ---- 127 | 128 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   129 | > 130 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   131 | 132 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 133 | 134 | ---- -------------------------------------------------------------------------------- /JavaThread/11. 线程并发库 Executors 的使用.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | ---- 8 | 9 | ## 1. 线程池的概念 10 | 11 | 在 java5 之后,就有了线程池的功能了,在介绍线程池之前,先来简单看一下线程池的概念。假设我开了家咨询公司,那么每天会有很多人过来咨询问题,如果我一个个接待的话,必然有很多人要排队,这样效率就很差,我想解决这个问题,现在我雇几个客服,来了一个咨询的,我就分配一个客服去接待他,再来一个,我再分配个客服去接待……如果第一个客服接待完了,我就让她接待下一个咨询者,这样我雇的这些客服可以循环利用。这些客服就好比不同的线程,那么装这些线程的容器就称为线程池。 12 | 13 | ## 2. Executors 的使用 14 | 15 | Executors 工具类是用来创建线程池的,这个线程池可以指定线程个数,也可以不指定,也可以指定定时器的线程池,它有如下常用的方法: 16 | 17 | |方法名|作用| 18 | |:----:|:----:| 19 | |newFixedThreadPool(int nThreads)|创建固定数量的线程池| 20 | |newCachedThreadPool()|创建缓存的线程池| 21 | |newSingleThreadExecutor()|创建单个线程| 22 | |newScheduledThreadPool(int corePoolSize)|创建定时器线程池| 23 | 24 | ### 2.1 固定数量的线程池 25 | 26 | 先来看下 Executors 工具类的使用: 27 | 28 | ```java 29 | public class ThreadPool { 30 | //线程池的概念与Executors类的使用 31 | public static void main(String[] args) { 32 | //固定线程池:创建固定线程数去执行线程的任务,这里创建三个线程 33 | ExecutorService threadPool = Executors.newFixedThreadPool(3); 34 | 35 | for (int i = 1; i <= 10; i++) {//向池子里扔10个任务 36 | final int task = i; 37 | threadPool.execute(new Runnable() {//execute方法表示向池子中扔任务,任务即一个Runnable 38 | 39 | @Override 40 | public void run() { 41 | for (int j = 1; j <= 5; j++) { 42 | try { 43 | Thread.sleep(20); 44 | } catch (InterruptedException e) { 45 | // TODO Auto-generated catch block 46 | e.printStackTrace(); 47 | } 48 | System.out.println(Thread.currentThread().getName() 49 | + " looping of " + j + " for task of " + task); 50 | } 51 | } 52 | }); 53 | } 54 | System.out.println("all of 10 tasks have committed!"); 55 | threadPool.shutdown(); //执行完任务后关闭 56 | // threadPool.shutdownNow(); //立即关闭 57 | } 58 | } 59 | ``` 60 | 61 | 从代码中可以看出,有了 Executors 工具类,我们创建固定数量的线程数就方便了,这些线程都去做同样的任务。`threadPool.execute` 表示从池子里取出一个线程去执行任务,上面定义了三个线程,所以每次会取三个任务去让线程执行,其他任务等待,执行完后,再从池子里取三个任务执行,执行完,再取三个任务,最后一个任务三个线程有一个会抢到执行。所以定义了线程数量的话,每次会执行该数量的任务,因为一个线程一个任务,执行完再执行其他任务。 62 | 63 | 因为这个执行结果有点多,就不贴结果了,反正每次三个任务一执行,直到执行完10个任务为止。 64 | 65 | ### 2.2 缓存线程池 66 | 67 | 所谓缓存线程池,指的是线程数量不固定,一个任务来了,我开启一个线程为其服务,两个任务我就开启两个,N个任务我就开启N个线程为其服务。如果现在只剩1个任务了,那么一段时间后,就把多余的线程给干掉,保留一个线程为其服务。所以可以改写一下上面的代码: 68 | 69 | ```java 70 | public class ThreadPool { 71 | //线程池的概念与Executors类的使用 72 | public static void main(String[] args) { 73 | //缓存的线程池 74 | //自动根据任务数量来设定线程数去服务,多了就增加线程数,少了就减少线程数 75 | //这貌似跟一般情况相同,因为一般也是一个线程执行一个任务,但是这里的好处是:如果有个线程死了,它又会产生一个新的来执行任务 76 | ExecutorService threadPool = Executors.newCachedThreadPool(); 77 | for (int i = 1; i <= 10; i++) {//扔5个任务 78 | final int task = i; 79 | threadPool.execute(new Runnable() {//向池子中扔任务,任务即一个Runnable 80 | 81 | @Override 82 | public void run() { 83 | for (int j = 1; j <= 5; j++) { 84 | try { 85 | Thread.sleep(20); 86 | } catch (InterruptedException e) { 87 | // TODO Auto-generated catch block 88 | e.printStackTrace(); 89 | } 90 | System.out.println(Thread.currentThread().getName() 91 | + " looping of " + j + " for task of " + task); 92 | } 93 | } 94 | }); 95 | } 96 | System.out.println("all of 10 tasks have committed!"); 97 | threadPool.shutdown(); //执行完任务后关闭 98 | } 99 | } 100 | ``` 101 | 102 | 使用缓存线程池的时候,会自动根据任务数量来产生线程数,即线程跟着任务走。运行结果也不贴了,有点多。 103 | 104 | 那么创建单个线程池 `newSingleThreadExecutor()` 就写了,把上面那个方法改掉就行了,就只有一个线程去执行10个任务了,但是这跟我们平常直接new一个线程还有啥区别呢?它还有个好处就是,如果线程死了,它会自动再生一个,而我们自己new的就不会了。如果线程死了还要重新产生一个,也就是说要保证有一个线程在执行任务的话,那么 `newSingleThreadExecutor()` 是个很好的选择。 105 | 106 | ## 3. 线程池启动定时器 107 | 108 | 我们可以用静态方法 `newScheduledThreadPool(int corePoolSize)` 来定义一个定时器线程池,可以指定线程个数。然后再调用 schedule 方法,传进去一个 Runnable 和定时时长即可,见代码: 109 | 110 | ```java 111 | public class ThreadPool { 112 | 113 | public static void main(String[] args) { 114 | Executors.newScheduledThreadPool(3).schedule(new Runnable() { 115 | 116 | @Override 117 | public void run() { 118 | System.out.println(Thread.currentThread().getName() + " bombing"); 119 | } 120 | }, 2, TimeUnit.SECONDS); 121 | } 122 | } 123 | ``` 124 | 125 | 定义了三个线程,会有一个率先抢到任务执行在2秒后执行。这只是创建了一个任务,如果我们要使用这个线程池去执行多个任务咋办呢?schedule 中只能传入一个 Runnable,也就是说只能传入一个任务,解决办法跟上面那些程序一样,先拿到创建的线程池,再循环多次执行 schedule,每次都传进去一个任务即可: 126 | 127 | ```java 128 | public class ThreadPool { 129 | 130 | public static void main(String[] args) { 131 | //拿到定时器线程池 132 | ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(3); 133 | for(int i = 1; i <= 5; i ++) { //执行5次任务 134 | threadPool.schedule(new Runnable() { 135 | 136 | @Override 137 | public void run() { 138 | try { 139 | Thread.sleep(1000); 140 | } catch (InterruptedException e) { 141 | // TODO Auto-generated catch block 142 | e.printStackTrace(); 143 | } 144 | System.out.println(Thread.currentThread().getName() + " bombing"); 145 | } 146 | }, 2, TimeUnit.SECONDS); 147 | } 148 | } 149 | } 150 | ``` 151 | 因为线程池中只有3个线程,但是有5个任务,所以会先执行3个任务,剩下两个任务,随机2个线程执行,看下结果: 152 | >pool-1-thread-3 bombing
153 | pool-1-thread-2 bombing
154 | pool-1-thread-1 bombing
155 | pool-1-thread-2 bombing
156 | pool-1-thread-3 bombing
157 | 158 | 如果我想5秒后执行一个任务,然后每个2秒执行一次该怎么办呢?我们可以调用另一个方法 `scheduleAtFixedRate`,这个方法中传进去一个 Runnable,一个定时时间和每次重复执行的时间间隔,如下: 159 | ```java 160 | public class ThreadPool { 161 | 162 | public static void main(String[] args) { 163 | Executors.newScheduledThreadPool(3).scheduleAtFixedRate(new Runnable() { 164 | 165 | @Override 166 | public void run() { 167 | System.out.println(Thread.currentThread().getName() + " bombing"); 168 | } 169 | }, 5, 2, TimeUnit.SECONDS); 170 | } 171 | } 172 | ``` 173 | 这样就可以5秒后执行,并且以后每隔2秒执行一次了。这个方法有个瑕疵,就是无法设置指定时间点执行,官方JDK提供的解决办法是 `data.getTime()-System.currentTimeMillis()` 来获取相对时间,然后放到上面方法的第二个参数即可。 174 | 175 | 线程并发库 Executors 的使用就总结这么多吧,如有问题欢迎交流,我们共同进步。 176 | 177 | ---- 178 | 179 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   180 | > 181 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   182 | 183 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 184 | 185 | ---- -------------------------------------------------------------------------------- /JavaThread/12. Callable 与 Future 的应用.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | ---- 8 | 9 | 这篇文章主要来介绍下线程并发库中 Callable 和 Future 的使用。 10 | 11 | Callable 接口类似于 Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。但是 Runnable 不会返回结果,并且无法抛出经过检查的异常。而 Callable 可以返回一个结果,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值,下面来看一个简单的例子: 12 | ```java 13 | public class CallableAndFuture { 14 | 15 | public static void main(String[] args) { 16 | ExecutorService threadPool = Executors.newSingleThreadExecutor();// 创建一个线程即可 17 | Future future = threadPool.submit( 18 | new Callable() { 19 | 20 | @Override 21 | public String call() throws Exception { 22 | Thread.sleep(2000); 23 | return "hello"; 24 | } 25 | } 26 | ); 27 | System.out.println("等待结果:"); 28 | try { 29 | System.out.println("拿到结果:" + future.get()); 30 | } catch (Exception e) { 31 | // TODO Auto-generated catch block 32 | e.printStackTrace(); 33 | } 34 | } 35 | ``` 36 | 使用并发库创建一个线程,前面博文已经提到过,如果调用 execute 方法,要传进去一个 Runnable,现在我调用 submit 方法,就可以传进去一个 Callable 了,然后重写 call 方法,即可返回一个我们需要的数据,然后我们用 Future 来接收返回的数据,通过 `future.get()` 方法就可以取到。 37 | 38 | 那么问题来了,这样做不是更加麻烦?我还不如直接去调用一个方法,然后返回一个值,我立马就拿到了啊,干嘛搞得这么麻烦!说的是有道理的,但是 Callable 和 Future 这个组合的用处不在于此。假设有一个很耗时的返回值需要计算,并且这个返回值不是立刻需要的话,那么就可以使用这个组合,用另一个线程去计算返回值,而当前线程在使用这个返回值之前可以做其它的操作,等到需要这个返回值时,再通过 Future 得到,这样设计岂不是很好? 39 | 40 | 官方对 Future 有如下介绍: 41 | >Future 表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。计算完成后只能使用 get 方法来获取结果,如有必要,计算完成前可以阻塞此方法。取消则由 cancel 方法来执行。还提供了其他方法,以确定任务是正常完成还是被取消了。一旦计算完成,就不能再取消计算。 42 | 43 | 查看 JDK 文档,有个 cancel 方法是用来取消任务的,也就是说我们可以人为取消,这就比较灵活了,就像上面所说的,计算量比较大的时候,如果在不同情况下,是否需要去计算是不确定的,如果 xxx,就让它算出结果,如果 xxx,就不用计算了,那我们可以很灵活的使用这个 Future,另外,在取消前,也可以先判断是否已经执行完了。都有相关方法的。 44 |    45 | 上面是单个线程执行的情况,现在如果有多个线程都在执行,且有多个返回值,该怎么做呢?这种情况下,我们就要使用 `CompletionService` 接口了,看一下具体的代码: 46 | 47 | ```java 48 | public class CallableAndFuture { 49 | 50 | public static void main(String[] args) { 51 | 52 | ExecutorService threadPool = Executors.newCachedThreadPool();//定义一个缓存线程池 53 | CompletionService completionService = 54 | new ExecutorCompletionService(threadPool); //将线程池扔进去 55 | for(int i = 1; i <= 5; i ++) { 56 | final int seq = i; 57 | completionService.submit( //用里面装的线程去执行这些任务,每个线程都会返回一个数据 58 | new Callable () { 59 | 60 | @Override 61 | public Integer call() throws Exception { 62 | Thread.sleep(new Random().nextInt(5000)); 63 | return seq; 64 | } 65 | 66 | } 67 | ); 68 | } 69 | for(int i = 0; i < 5; i ++) { //执行完了后,再取出来 70 | try { 71 | System.out.print(completionService.take().get() + " "); 72 | } catch (Exception e) { 73 | // TODO Auto-generated catch block 74 | e.printStackTrace(); 75 | } 76 | } 77 | } 78 | } 79 | ``` 80 | 81 | 这种方式和上面的方式有个区别是,上面单个线程的时候,使用 ExecutorService 对象去执行 submit,多个线程的时候就把线程池扔到 `CompletionService` 中去,然后执行 submit,最后我们在调用 `take()` 和 `get()` 方法取出结果即可。使用起来比较方便,下面来看一下 JDK 文档对其的介绍: 82 | 83 | >将生产新的异步任务与使用已完成任务的结果分离开来的服务。生产者 submit 执行的任务。使用者 take 已完成的任务,并按照完成这些任务的顺序处理它们的结果。
84 | >通常,CompletionService 依赖于一个单独的 Executor 来实际执行任务,在这种情况下,CompletionService 只管理一个内部完成队列。 85 | 86 | 官方介绍跟上面写的程序是一致的,首先 take 获取已完成的任务,然后 get 将每个任务拿到。并且按照完成这些任务的顺序处理它们,也就是说,刚刚在执行的时候,哪个线程先执行的,就会先拿到该线程执行的结果。 87 | 88 | ok,Callable 和 Future 的使用就介绍这么多,如有问题,欢迎讨论,我们一起进步~ 89 | 90 | ---- 91 | 92 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   93 | > 94 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   95 | 96 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 97 | 98 | ---- -------------------------------------------------------------------------------- /JavaThread/15. 线程同步工具Semaphore的使用.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | ---- 8 | 9 | Semaphore 通常用于限制可以访问某些资源(物理或逻辑的)的线程数目,我们可以自己设定最大访问量。它有两个很常用的方法是 `acquire()` 和 `release()`,分别是获得许可和释放许可。 10 | 11 | 官方JDK上面对Semaphore的解释是这样子的 : 12 | 13 | >一个计数信号量。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 `acquire()`,然后再获取该许可。每个 `release()` 添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。拿到信号量的线程可以进入代码,否则就等待。通过 `acquire()` 和 `release()` 获取和释放访问许可。 14 | 15 | 我的解释是这样子的: 16 | 17 | >Semaphore 相当于一个厕所,我在造的时候可以想造几个坑就造几个坑,假如现在我就造了3个坑,现在有10个人想要来上厕所,那么每次就只能3个人上,谁最先抢到谁就进去,出来了一个人后,第4个人才能进去,这个就限制了上厕所的人数了,就这个道理。每个人上厕所之前都先 `acquire()` 一下,如果有坑,就可以进入,没有就被阻塞,在外面等;上完厕所后,会 `release()` 一下,释放一个坑出来,以保证下一个人 `acquire()` 的时候有坑。 18 | 19 | 我觉得我的解释比官方的要好…… 20 | 21 | ## 1. Semaphore基本使用 22 | 23 | Semaphore 在限制资源访问量的问题上用处很大,比如限制一个文件的并发访问次数等,它的原理很好理解。下面写一个 Semaphore 的示例代码: 24 | ```java 25 | public class SemaphoreTest { 26 | 27 | public static void main(String[] args) { 28 | 29 | ExecutorService service = Executors.newCachedThreadPool();//使用并发库,创建缓存的线程池 30 | final Semaphore sp = new Semaphore(3);//创建一个Semaphore信号量,并设置最大并发数为3 31 | 32 | //availablePermits() //用来获取当前可用的访问次数 33 | System.out.println("初始化:当前有" + (3 - sp.availablePermits() + "个并发")); 34 | 35 | //创建10个任务,上面的缓存线程池就会创建10个对应的线程去执行 36 | for (int index = 0; index < 10; index++) { 37 | final int NO = index; //记录第几个任务 38 | Runnable run = new Runnable() { //具体任务 39 | public void run() { 40 | try { 41 | sp.acquire(); // 获取许可 42 | System.out.println(Thread.currentThread().getName() 43 | + "获取许可" + "("+NO+")," + "剩余:" + sp.availablePermits()); 44 | Thread.sleep(1000); 45 | // 访问完后记得释放 ,否则在控制台只能打印3条记录,之后线程一直阻塞 46 | sp.release(); //释放许可 47 | System.out.println(Thread.currentThread().getName() 48 | + "释放许可" + "("+NO+")," + "剩余:" + sp.availablePermits()); 49 | } catch (InterruptedException e) { 50 | } 51 | } 52 | }; 53 | service.execute(run); //执行任务 54 | } 55 | service.shutdown(); //关闭线程池 56 | } 57 | } 58 | ``` 59 | 代码结构很容易理解,10个任务,每次最多3个线程去执行任务,其他线程被阻塞。可以通过打印信息来看线程的执行情况: 60 | 61 | >初始化:当前有0个并发
62 | pool-1-thread-1获取许可(0),剩余:1
63 | pool-1-thread-3获取许可(2),剩余:0
64 | pool-1-thread-2获取许可(1),剩余:1
65 | pool-1-thread-1释放许可(0),剩余:3
66 | pool-1-thread-4获取许可(3),剩余:1
67 | pool-1-thread-5获取许可(4),剩余:1
68 | pool-1-thread-2释放许可(1),剩余:3
69 | pool-1-thread-3释放许可(2),剩余:3
70 | pool-1-thread-6获取许可(5),剩余:0
71 | pool-1-thread-4释放许可(3),剩余:2
72 | pool-1-thread-9获取许可(8),剩余:0
73 | pool-1-thread-5释放许可(4),剩余:2
74 | pool-1-thread-6释放许可(5),剩余:2
75 | pool-1-thread-8获取许可(7),剩余:0
76 | pool-1-thread-7获取许可(6),剩余:2
77 | pool-1-thread-8释放许可(7),剩余:2
78 | pool-1-thread-10获取许可(9),剩余:2
79 | pool-1-thread-7释放许可(6),剩余:2
80 | pool-1-thread-9释放许可(8),剩余:2
81 | pool-1-thread-10释放许可(9),剩余:3
82 | 83 | 从结果中看,前三个为什么剩余的不是3,2,1呢?包括下面,每次释放的时候剩余的量好像也不对,其实是对的,只不过线程运行太快,前三个是这样子的:因为最大访问量是3,所以前三个在打印语句之前都执行完了 `aquire()` 方法了,或者有部分执行了,从上面的结果来看,线程1是第一个进去的,线程2第二个进去,然后线程1和2开始打印,所以只剩1个了,接下来线程3进来了,打印只剩0个了。后面释放的时候也是,打印前可能有不止一个释放了。 84 | 85 | ## 2. Semaphore同步问题 86 | 87 | 我从网上查了一下,有些人说 Semaphore 实现了同步功能,我觉得不对,因为我自己写了个测试代码试了,并不会自己解决并发问题,如果多个线程操作同一个数据,还是需要自己同步一下的。然后我查了一下官方 JDK 文档(要永远相信官方的文档),它里面是这样说的: 88 | 89 | >获得一项前,每个线程必须从信号量获取许可,从而保证可以使用该项。该线程结束后,将项返回到池中并将许可返回到该信号量,从而允许其他线程获取该项。注意,调用 `acquire()` 时无法保持同步锁,因为这会阻止将项返回到池中。信号量封装所需的同步,以限制对池的访问,这同维持该池本身一致性所需的同步是分开的。 90 | 91 | 这段官方的解释就很明确了,然后我就明白了网上有些人说的实现了同步的意思是信号量本身封装所需的同步,也就是说我拿到了一个,别人就无法拿到了,我释放了别人才能拿到(就跟我举的厕所的坑一样),但是我拿到了之后去操作公共数据的时候,针对这个数据操作的同步 Semaphore 就不管了,这就需要我们自己去同步了。下面写一个同步的测试代码: 92 | 93 | ```java 94 | public class SemaphoreTest2 { 95 | 96 | private static int data = 0; 97 | 98 | public static void main(String[] args) { 99 | ExecutorService service = Executors.newCachedThreadPool(); 100 | final Semaphore sp = new Semaphore(3); 101 | System.out.println("初始化:当前有" + (3 - sp.availablePermits() + "个并发")); 102 | 103 | // 10个任务 104 | for (int index = 0; index < 10; index++) { 105 | final int NO = index; 106 | Runnable run = new Runnable() { 107 | public void run() { 108 | try { 109 | // 获取许可 110 | sp.acquire(); 111 | System.out.println(Thread.currentThread().getName() 112 | + "获取许可" + "(" + NO + ")," + "剩余:" + sp.availablePermits()); 113 | //实现同步 114 | synchronized(SemaphoreTest2.class) { 115 | System.out.println(Thread.currentThread().getName() 116 | + "执行data自增前:data=" + data); 117 | data++; 118 | System.out.println(Thread.currentThread().getName() 119 | + "执行data自增后:data=" + data); 120 | } 121 | 122 | sp.release(); 123 | System.out.println(Thread.currentThread().getName() 124 | + "释放许可" + "(" + NO + ")," + "剩余:" + sp.availablePermits()); 125 | } catch (InterruptedException e) { 126 | } 127 | } 128 | }; 129 | service.execute(run); 130 | } 131 | service.shutdown(); 132 | } 133 | } 134 | ``` 135 | 看一下运行结果(部分): 136 | 137 | >初始化:当前有0个并发
138 | pool-1-thread-2获取许可(1),剩余:0
139 | pool-1-thread-2执行data自增前:data=0
140 | pool-1-thread-3获取许可(2),剩余:0
141 | pool-1-thread-1获取许可(0),剩余:0
142 | pool-1-thread-2执行data自增后:data=1
143 | pool-1-thread-3执行data自增前:data=1
144 | pool-1-thread-3执行data自增后:data=2
145 | pool-1-thread-1执行data自增前:data=2
146 | pool-1-thread-7获取许可(6),剩余:1
147 | pool-1-thread-3释放许可(2),剩余:2
148 | pool-1-thread-1执行data自增后:data=3
149 | 150 | 从结果中可以看出,每个线程在操作数据的前后,是不会受其他线程的影响的,但是其他线程可以获取许可,获取许可了之后就被阻塞在外面,等待当前线程操作完 data 才能去操作。当然也可以在当前线程操作 data 的时候,其他线程释放许可,因为这完全不冲突。 151 | 152 | 那如果把上面同步代码块去掉,再试试看会成什么乱七八糟的结果(部分): 153 | 154 | >初始化:当前有0个并发
155 | pool-1-thread-3获取许可(2),剩余:0
156 | pool-1-thread-2获取许可(1),剩余:0
157 | pool-1-thread-3执行data自增前:data=0
158 | pool-1-thread-2执行data自增前:data=0
159 | pool-1-thread-1获取许可(0),剩余:0
160 | pool-1-thread-3执行data自增后:data=1
161 | pool-1-thread-2执行data自增后:data=2
162 | pool-1-thread-7获取许可(6),剩余:0
163 | pool-1-thread-1执行data自增前:data=2
164 | pool-1-thread-8获取许可(7),剩余:0
165 | pool-1-thread-7执行data自增前:data=2
166 | pool-1-thread-2释放许可(1),剩余:1
167 | pool-1-thread-7执行data自增后:data=4
168 | 169 | 从结果中看,已经很明显了,线程2和3都进去了,然后初始 data 都是0,线程3自增了一下,打印出1是没问题的,但是线程2呢?也自增了一下,却打印出了2。也就是说,线程2在操作数据的前后,数据已经被线程3修改过了,再一次证明 Semaphere 并没有实现对共有数据的同步,在操作公共数据的时候,需要我们自己实现。 170 | 171 | Semaphere 中如果设置信号量为1的话,那就说明每次只能一个线程去操作任务,那这样的话也就不存在线程安全问题了,所以如果设置信号量为1的话,就可以去掉那个 synchronized,不过效率就不行了。 172 | 173 | Semaphere的使用就总结这么多吧!如有问题,欢迎讨论,我们一起进步! 174 | 175 | ---- 176 | 177 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   178 | > 179 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   180 | 181 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 182 | 183 | ---- -------------------------------------------------------------------------------- /JavaThread/16. 线程同步工具 CyclicBarrier 的使用.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | ---- 8 | 9 | 上一节中总结了 Semaphore 同步工具的使用,Semaphore 主要提供了一个记数信号量,允许最大线程数运行。CyclicBarrier 是另一个同步工具,这一节主要来总结一下 CyclicBarrier 的使用。先看一下官方的对 CyclicBarrier 的介绍: 10 | 11 | >一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier。 12 | 13 | 我的解释是这样的: 14 | 15 | >CyclicBarrier 可以使不同的线程彼此等待,在不同的线程都执行完了后,再执行下面的程序。比如A、B、C三个同学要去玩,大巴在校门口,A、B、C分别从各自的寝室出来,先后到达大巴处,先来的必须等待,直到三个同学都来了,大巴才能走。他们要玩两个地方M和N,到了M处,三个同学又各自去玩了,玩完后各自回到大巴上,先回来的必须等待,直到三个同学都到了,大巴才能到N处,这个大巴可以循环利用。这个大巴就是CyclicBarrier。 16 | 17 | CyclicBarrier 同步工具相对来说比较简单,因为功能很明确,下面写一个 CyclicBarrier 的示例代码: 18 | ```java 19 | public class CyclicBarrierTest { 20 | 21 | public static void main(String[] args) { 22 | ExecutorService service = Executors.newCachedThreadPool(); 23 | final CyclicBarrier cb = new CyclicBarrier(3); //设置要三个线程等待,都执行完了再往下执行 24 | 25 | System.out.println("初始化:当前有" + (cb.getNumberWaiting() + "个线程在等待")); 26 | 27 | //3个任务 28 | for (int i = 0; i < 3; i++) { 29 | 30 | Runnable run = new Runnable() { 31 | public void run() { 32 | try { 33 | Thread.sleep((long)(Math.random()*10000)); 34 | System.out.println(Thread.currentThread().getName() 35 | + "即将到达集合点1,当前已有" + (cb.getNumberWaiting()+1) + "个线程到达," 36 | + (cb.getNumberWaiting()==2?"都到齐了,去集合点2!":"正在等候……")); 37 | // 访问完后,释放 ,如果屏蔽下面的语句,则在控制台只能打印3条记录,之后线程一直阻塞 38 | cb.await(); //等待 39 | 40 | Thread.sleep((long)(Math.random()*10000)); 41 | System.out.println(Thread.currentThread().getName() 42 | + "即将到达集合点2,当前已有" + (cb.getNumberWaiting()+1) + "个线程到达," 43 | + (cb.getNumberWaiting()==2?"都到齐了,去集合点3!":"正在等候……")); 44 | cb.await(); 45 | 46 | Thread.sleep((long)(Math.random()*10000)); 47 | System.out.println(Thread.currentThread().getName() 48 | + "即将到达集合点3,当前已有" + (cb.getNumberWaiting()+1) + "个线程到达," 49 | + (cb.getNumberWaiting()==2?"都到齐了,执行完毕!":"正在等候……")); 50 | cb.await(); 51 | 52 | } catch (Exception e) { 53 | } 54 | } 55 | }; 56 | service.execute(run); //执行任务 57 | } 58 | 59 | service.shutdown(); //关闭线程 60 | } 61 | } 62 | ``` 63 | 从代码中可以看出,CyclicBarrier 的使用主要有两点,一是初始化,二是调用await()方法。这个 `await()` 方法也就是官方解释中的“公共屏障点”,到了这个点,所有线程都得等待,直到规定数量的线程全部到达才能往下执行。看一下运行效果: 64 | 65 | >初始化:当前有0个线程在等待
66 | pool-1-thread-3即将到达集合点1,当前已有1个线程到达,正在等候……
67 | pool-1-thread-2即将到达集合点1,当前已有2个线程到达,正在等候……
68 | pool-1-thread-1即将到达集合点1,当前已有3个线程到达,都到齐了,去集合点2!
69 | pool-1-thread-2即将到达集合点2,当前已有1个线程到达,正在等候……
70 | pool-1-thread-3即将到达集合点2,当前已有2个线程到达,正在等候……
71 | pool-1-thread-1即将到达集合点2,当前已有3个线程到达,都到齐了,去集合点3!
72 | pool-1-thread-3即将到达集合点3,当前已有1个线程到达,正在等候……
73 | pool-1-thread-1即将到达集合点3,当前已有2个线程到达,正在等候……
74 | pool-1-thread-2即将到达集合点3,当前已有3个线程到达,都到齐了,执行完毕!
75 | 76 | CyclicBarrier 的应用场合也很明显:在某种需求中,比如一个大型的任务,常常需要分配好多子任务去执行,只有当所有子任务都执行完成时候,才能执行主任务,这时候,就可以选择 CyclicBarrier 了。 77 | 78 | 可以再确切一点:假如我们需要统计全国的业务数据,其中各省的数据库是独立的,也就是说按省分库。并且统计的数据量很大,统计过程也比较慢。为了提高性能,快速计算。我们采取并发的方式,多个线程同时计算各省数据,最后再汇总统计,在这里 CyclicBarrier 就非常有用。 79 | 80 | 关于 CyclicBarrier 同步工具就总结这么多吧!如有问题,欢迎讨论,我们一起进步! 81 | 82 | ---- 83 | 84 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   85 | > 86 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   87 | 88 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 89 | 90 | ---- -------------------------------------------------------------------------------- /JavaThread/17. 线程同步工具Exchanger的使用.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | ---- 8 | 9 | 如果两个线程在运行过程中需要交换彼此的信息,比如一个数据或者使用的空间,就需要用到 Exchanger 这个类,Exchanger 为线程交换信息提供了非常方便的途径,它可以作为两个线程交换对象的同步点,只有当每个线程都在进入 `exchange ()` 方法并给出对象时,才能接受其他线程返回时给出的对象。 10 | 11 | 每次只能两个线程交换数据,如果有多个线程,也只有两个能交换数据。下面看个通俗的例子:一手交钱一首交货! 12 | 13 | ```java 14 | public class ExchangerTest { 15 | public static void main(String[] args) { 16 | ExecutorService service = Executors.newCachedThreadPool(); 17 | final Exchanger exchanger = new Exchanger(); //定义一个交换对象,用来交换数据 18 | 19 | //开启一个线程执行任务 20 | service.execute(new Runnable(){ 21 | 22 | @Override 23 | public void run() { 24 | try { 25 | String data1 = "海洛因"; 26 | System.out.println("线程" + Thread.currentThread().getName() 27 | + "正在把毒品" + data1 + "拿出来"); 28 | Thread.sleep((long)(Math.random()*10000)); 29 | 30 | //把要交换的数据传到exchange方法中,然后被阻塞,等待另一个线程与之交换。返回交换后的数据 31 | String data2 = (String)exchanger.exchange(data1); 32 | 33 | System.out.println("线程" + Thread.currentThread().getName() + 34 | "用海洛因换来了" + data2); 35 | }catch(Exception e){ 36 | } finally { 37 | service.shutdown(); 38 | System.out.println("交易完毕,拿着钱快跑!"); 39 | } 40 | } 41 | }); 42 | 43 | //开启另一个线程执行任务 44 | service.execute(new Runnable(){ 45 | 46 | @Override 47 | public void run() { 48 | try { 49 | String data1 = "300万"; 50 | System.out.println("线程" + Thread.currentThread().getName() + 51 | "正在把" + data1 +"拿出来"); 52 | Thread.sleep((long)(Math.random()*10000)); 53 | 54 | String data2 = (String)exchanger.exchange(data1); 55 | 56 | System.out.println("线程" + Thread.currentThread().getName() + 57 | "用300万弄到了" + data2); 58 | }catch(Exception e){ 59 | } finally { 60 | service.shutdown(); 61 | System.out.println("交易完毕,拿着海洛因快跑!"); 62 | } 63 | } 64 | }); 65 | } 66 | } 67 | ``` 68 | 69 | 从代码中我仿佛看到了两个人在交易毒品的场面……来看一下交易结果如何: 70 | 71 | >线程pool-1-thread-1正在把毒品海洛因拿出来
72 | 线程pool-1-thread-2正在把300万拿出来
73 | 线程pool-1-thread-2用300万弄到了海洛因
74 | 线程pool-1-thread-1用海洛因换来了300万
75 | 交易完毕,拿着海洛因快跑!
76 | 交易完毕,拿着钱快跑!
77 | 78 | 跑的倒是快,从运行结果来看,确实实现了数据的交换,这个只是交换一个基本类型的数据而已,它真正的用处不仅仅局限于此,比如我们可以交换一个对象,这就有用了,JDK官方提到了一个高级的应用: 79 | 80 | >使用 Exchanger 在线程间交换缓冲区,因此,在需要时,填充缓冲区的线程获取一个新腾空的缓冲区,并将填满的缓冲区传递给腾空缓冲区的线程。 81 | 82 | 这就得根据实际情况了,思想和上面的一样,实际中肯定要定义一个缓冲区的类,然后两个线程之间交换的就是这个缓冲区的类即可,至于类中如何实现,就得看实际情况了。 83 | 84 | Exchanger 的使用就总结这么多吧~如有问题,欢迎讨论,我们一起进步。 85 | 86 | ---- 87 | 88 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   89 | > 90 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   91 | 92 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 93 | 94 | ---- -------------------------------------------------------------------------------- /JavaThread/18. 阻塞队列的使用.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | ---- 8 | 9 | 在前面一篇文章:条件阻塞Condition的应用中提到了一个拔高的例子:利用 Condition 来实现阻塞队列。其实在 java 中,有个叫 `ArrayBlockingQueue` 的类提供了阻塞队列的功能,所以我们如果需要使用阻塞队列,完全没有必要自己去写。 10 | 11 | `ArrayBlockingQueue` 实现了 `BlockingQueue`,另外还有 `LinkedBlockingQueue` 和 `PriorityBlockingQueue`。`ArrayBlockingQueue` 主要是基于数组实现的,`LinkedBlockingQueue` 是基于链表实现的,它们都是先进先出;而 `PriorityBlockingQueue` 是基于优先级队列的。这篇文章主要用 `ArrayBlockingQueue` 举例,另外两个使用方式和 `ArrayBlockingQueue` 差不多,具体的可以参考官方文档。 12 | 13 | 使用 `ArrayBlockingQueue` 需要先指明缓存区的大小,指明之后就无法修改,试图向已满队列中放入元素会导致操作受阻塞;试图从空队列中提取元素将导致类似阻塞。下面使用 `ArrayBlockingQueue` 来实现类似于前面那篇博客中提到的一个功能: 14 | 15 | ```java 16 | public class BlockingQueueTest { 17 | 18 | public static void main(String[] args) { 19 | final BlockingQueue queue = new ArrayBlockingQueue(3); //缓冲区允许放3个数据 20 | 21 | for(int i = 0; i < 2; i ++) { 22 | new Thread() { //开启两个线程不停的往缓冲区存数据 23 | 24 | @Override 25 | public void run() { 26 | while(true) { 27 | try { 28 | Thread.sleep((long) (Math.random()*1000)); 29 | System.out.println(Thread.currentThread().getName() + "准备放数据" 30 | + (queue.size() == 3?"..队列已满,正在等待":"...")); 31 | queue.put(1); 32 | System.out.println(Thread.currentThread().getName() + "存入数据," 33 | + "队列目前有" + queue.size() + "个数据"); 34 | } catch (InterruptedException e) { 35 | // TODO Auto-generated catch block 36 | e.printStackTrace(); 37 | } 38 | } 39 | } 40 | 41 | }.start(); 42 | } 43 | 44 | new Thread() { //开启一个线程不停的从缓冲区取数据 45 | 46 | @Override 47 | public void run() { 48 | while(true) { 49 | try { 50 | Thread.sleep(1000); 51 | System.out.println(Thread.currentThread().getName() + "准备取数据" 52 | + (queue.size() == 0?"..队列已空,正在等待":"...")); 53 | queue.take(); 54 | System.out.println(Thread.currentThread().getName() + "取出数据," 55 | + "队列目前有" + queue.size() + "个数据"); 56 | } catch (InterruptedException e) { 57 | // TODO Auto-generated catch block 58 | e.printStackTrace(); 59 | } 60 | } 61 | } 62 | }.start(); 63 | } 64 | 65 | } 66 | ``` 67 | 程序的逻辑很简单,不难理解,下面看一下运行的结果: 68 | >Thread-0准备放数据...
69 | Thread-0存入数据,队列目前有1个数据
70 | Thread-1准备放数据...
71 | Thread-1存入数据,队列目前有2个数据
72 | Thread-2准备取数据...
73 | Thread-2取出数据,队列目前有1个数据
74 | Thread-0准备放数据...
75 | Thread-0存入数据,队列目前有2个数据
76 | Thread-1准备放数据...
77 | Thread-1存入数据,队列目前有3个数据
78 | Thread-2准备取数据...
79 | Thread-2取出数据,队列目前有2个数据
80 | Thread-0准备放数据...
81 | Thread-0存入数据,队列目前有3个数据
82 | Thread-1准备放数据..队列已满,正在等待
83 | Thread-0准备放数据..队列已满,正在等待
84 | Thread-2准备取数据...
85 | Thread-2取出数据,队列目前有2个数据
86 | Thread-1存入数据,队列目前有3个数据
87 | 88 | 所以使用阻塞队列就非常方便了,不用我们人为的去做判断了。 89 | 90 | 之前在文章里介绍过线程间通信可以使用 synchronized 和 wait、notify 组合,可以使用 Condition,其实我们也可以使用阻塞队列来实现线程间的通信,下面举个示例: 91 | 92 | ```java 93 | //阻塞队列实现线程间通信 94 | public class BlockingQueueCommunication { 95 | 96 | public static void main(String[] args) { 97 | Business bussiness = new Business(); 98 | 99 | new Thread(new Runnable() {// 开启一个子线程 100 | 101 | @Override 102 | public void run() { 103 | for (int i = 1; i <= 3; i++) { 104 | 105 | bussiness.sub(); 106 | } 107 | 108 | } 109 | }).start(); 110 | 111 | // main方法主线程 112 | for (int i = 1; i <= 3; i++) { 113 | 114 | bussiness.main(); 115 | } 116 | } 117 | } 118 | 119 | class Business { 120 | 121 | private int i = 0; 122 | 123 | BlockingQueue queue1 = new ArrayBlockingQueue(1); 124 | BlockingQueue queue2 = new ArrayBlockingQueue(1); 125 | 126 | { 127 | try { 128 | queue2.put(1); 129 | } catch (InterruptedException e) { 130 | // TODO Auto-generated catch block 131 | e.printStackTrace(); 132 | } 133 | } 134 | 135 | public void sub() { 136 | try { 137 | queue1.put(1); //如果主线程没执行完,则子线程缓冲区一直有数,子线程在这里被阻塞 138 | } catch (InterruptedException e) { 139 | // TODO Auto-generated catch block 140 | e.printStackTrace(); 141 | } 142 | 143 | System.out.println("子线程执行前i=" + i++); 144 | System.out.println("子线程执行后i=" + i); 145 | 146 | try { 147 | queue2.take(); //取出主线程中缓冲区的数,让主线程执行 148 | } catch (InterruptedException e) { 149 | // TODO Auto-generated catch block 150 | e.printStackTrace(); 151 | } 152 | } 153 | 154 | public void main() { 155 | try { 156 | queue2.put(1); //如果子线程没执行完,则主线程缓冲区一直有数,主线程在这里被阻塞 157 | } catch (InterruptedException e) { 158 | // TODO Auto-generated catch block 159 | e.printStackTrace(); 160 | } 161 | 162 | System.out.println("主线程执行前i=" + i++); 163 | System.out.println("主线程执行后i=" + i); 164 | 165 | try { 166 | queue1.take(); //取出子线程中缓冲区的数,让子线程执行 167 | } catch (InterruptedException e) { 168 | // TODO Auto-generated catch block 169 | e.printStackTrace(); 170 | } 171 | } 172 | } 173 | ``` 174 | 代码看起来有点长,但是逻辑很简单,就是主线程和子线程你一下我一下,轮流执行,执行的任务就是将公共数据i自增1,使用阻塞队列实现的,而且线程安全,因为一个线程执行的时候,另一个线程是被阻塞的。 175 | 176 | 设计思想是这样的,定义两个阻塞队列,分别只能放1个数,分别对应两个线程。那么我在静态代码块中先将主线程的队列塞满,然后我让两个线程在执行任务之前,先往各自的队列中塞一个数,那么肯定主线程肯定被阻塞,因为我之前已经在静态代码块中给主线程的队列塞过一个数了。然后子线程在执行完任务后,把主线程队列中的数取出,那么主线程就开始执行了,子线程此时被阻塞(因为刚刚它自己执行任务前塞了个数),主线程执行完拿出子线程队列中的数,这时候子线程又开始执行了。所以利用了阻塞队列会阻塞一个线程的办法来实现两个线程之间交替执行。 177 | 178 | 关于阻塞队列的使用,就总结这么多吧!如有问题,欢迎讨论,我们一起进步。 179 | 180 | ---- 181 | 182 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   183 | > 184 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   185 | 186 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 187 | 188 | ---- -------------------------------------------------------------------------------- /Spring/03. Spring Bean的作用域和生命周期.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,公众号不能不关注,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**,您的支持,是我们创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | ---- 8 | 9 | ##### 写在前面: 10 | 11 | 小伙伴儿们,大家好!上一篇我们学习了依赖注入和装配Bean的相关知识; 12 | 现在我们来学习一下Bean的作用域和生命周期,看看它在容器中是如何运作的。 13 | 14 | ##### 思维导图: 15 | 16 | ![image-20200927111145998](https://gitee.com/Huke-123/PicCloud/raw/master/20200927111146.png) 17 | 18 | ## 1、Bean的作用域 19 | 20 | 作用域在面向对象语言的定义是对象和变量的可见范围,在Spring中指的是当前配置创建的Bean相对于其他Bean的可见范围。 21 | 22 | scope属性用来进行Bean的作用域配置,可以配置6种类型的作用域,分别是`singleton`、`prototype`、`request`、`session`、`application`以及`globalsession`。其中,`singleton`和`prototype`是较为常见的类型,`scope`属性的配置方式很简单,示例如下: 23 | 24 | ```xml 25 | 26 | ``` 27 | 28 | ### 1.1,singleton——单例作用域 29 | 30 | Spring IoC容器只创建和维护一个该类型的Bean,并将这个实例存储到单例缓存中,针对该Bean的请求和引用,使用的都是同一个实例。从容器启动或者第一次调用实例化开始,只要容器没有退出或者销毁,该类型的单一实例就会一直存活。 31 | 32 | singleton作用域类似于设计模式中的单例模式。区别是单例模式基本上是一个类对应一个单例对象,而Spring可以同一个类进行多个单例Bean的配置,也就是一个类可以对应多个不同id的对象。 33 | 34 | singleton是最常使用的作用域,也是默认的类型。也就是说,如果没有设置Bean的scope属性,则默认就是单例作用域。 35 | 36 | 我们这里搞一个简单的测试: 37 | 创建一个简单的类AccountServiceImpl: 38 | 39 | ```java 40 | package com.java.service; 41 | 42 | public class AccountServiceImpl{ 43 | 44 | public AccountServiceImpl() { 45 | System.out.println("对象创建了"); 46 | } 47 | 48 | } 49 | 50 | ``` 51 | 52 | 配置文件bean.xml:(这里我们使用的作用域是singleton,单例的,默认值) 53 | 54 | ```xml 55 | 56 | 60 | 61 | 62 | 63 | ``` 64 | 65 | 测试类: 66 | 67 | ```java 68 | package com.java.test; 69 | 70 | import com.java.service.AccountService; 71 | import com.java.service.AccountServiceImpl; 72 | import org.springframework.context.ApplicationContext; 73 | import org.springframework.context.support.ClassPathXmlApplicationContext; 74 | 75 | public class Test { 76 | public static void main(String[] args) { 77 | //加载配置文件applicationContext.xml 78 | ApplicationContext ac=new ClassPathXmlApplicationContext("bean.xml"); 79 | //通过id获取bean,返回一个对象 80 | AccountServiceImpl as1=(AccountServiceImpl)ac.getBean("accountService"); 81 | AccountServiceImpl as2=(AccountServiceImpl)ac.getBean("accountService"); 82 | //调用方法 83 | System.out.println(as1==as2); 84 | } 85 | } 86 | 87 | ``` 88 | 89 | 运行结果: 90 | 91 | ![image-20200927092404569](https://gitee.com/Huke-123/PicCloud/raw/master/20200927092411.png) 92 | 93 | 说明singleton模式下对象只创建了一次,所以返回的都是相同的对象,结果也是true。 94 | 95 | ### 1.2,prototype——原型作用域 96 | 97 | 原型作用域的Bean在使用容器的getBean()方法获取的时候,每次得到的都是一个新的对象,作为依赖注入到其他Bean的时候会产生一个新的类对象。在代码层级来看,相当于每次使用都使用new的操作符创建一个新的对象。 98 | 99 | 需要注意的是,容器不负责原型作用域Bean实例的完整生命周期,在初始化或装配完该Bean的类对象之后,容器就不再对该对象进行管理,而需要由客户端对该对象进行管理,特别是如果该对象占用了一些昂贵的资源,就需要手动释放。此外,对于singleton类型的Bean,如果有配置对象的生命周期回调方法,则容器会根据配置进行调用,类似于singleton Bean使用后处理器释放会被Bean占用的资源,而prototype Bean即使配置了回调方法也不会调用。 100 | 101 | 我们还是照用上方的例子,修改一下配置文件: 102 | 配置文件bean.xml:(这里我们使用的作用域是singleton,多例的) 103 | 104 | ```xml 105 | 106 | 110 | 111 | 112 | 113 | ``` 114 | 115 | 运行结果: 116 | 117 | ![image-20200927093920176](https://gitee.com/Huke-123/PicCloud/raw/master/20200927093920.png) 118 | 119 | 可以看见创建了两次对象,调用Bean时都会返回一个新的实例,所以结果也是false,肯定是不同的。 120 | 121 | 1.3,request——请求作用域 122 | 123 | 针对每次HTTP请求,Spring都会去创建一个Bean实例。作用于web应用的请求范围。 124 | 125 | 1.4,session——会话作用域 126 | 127 | 使用于HTTP Session,同一个Session共享同一个Bean实例。 128 | 129 | 1.5,application——应用作用域 130 | 131 | 整个Web应用,也就是在ServletContext生命周期中使用一个Bean实例 132 | 133 | 1.6,globalsession——作用于集群环境的会话范围(全局会话范围),当不是集群环境时,它就是session。 134 | 135 | ## 2、Bean的生命周期 136 | 137 | ### 2.1,实例化和生命周期流程图 138 | 139 | Spring中bean的实例化过程,下面是一张经典的过程图: 140 | 141 | ![](https://gitee.com/Huke-123/PicCloud/raw/master/20200928104341.png) 142 | 143 | bean的生命周期流程图: 144 | 145 | ![](https://gitee.com/Huke-123/PicCloud/raw/master/20200928105418.png) 146 | 147 | 在Spring框架中,所有的Bean对象都有生命周期,是指Bean对象的创建,初始化,服务,销毁等这个过程,称为Bean对象的生命周期。 148 | 149 | 1. 实例化bean对象,Spring对bean进行实例化,Ioc容器通过获取BeanDefinition对象中的信息进行实例化,实例化对象被包装在BeanWrapper对象中; 150 | 2. 设置对象属性,Spring对bean进行依赖注入(DI),通过BeanWrapper提供的设置属性的接口完成属性依赖注入; 151 | 3. 如果bean实现了ApplicationContextAware接口,Spring将调用setApplicationContext(),将相关的实例注入给bean; 152 | 4. 执行我们自定义的方法,InitializingBean和init-method; 153 | 5. BeanPostProcessor,前置处理和后置处理; 154 | 6. 销毁destroy,如果bean实现了DisposableBean接口,Spring直接调用销毁方法destroy(); 155 | 156 | ### 2.2,Spring对单例和多例bean的管理 157 | 158 | 1,单例管理 159 | 160 | 当scope="singleton",默认情况下,会在启动容器时实例化,这中间就有个延迟过程,就是调用init-method属性的过程。这时候,只有在第一次获取bean时才会初始化bean,即第一次请求该bean时才初始化。 161 | 162 | 2,非单例对象 163 | 164 | 当scope="prototype",容器也会延迟初始化bean,Spring在第一次请求bean时才会初始化。在第一次请求每一个prototype的bean时,Spring容器都会调用其构造器创建这个对象,然后调用init-method属性值中所指定的方法。对象销毁的时候,Spring容器不会帮我们调用任何方法,因为是非单例,这个类型的对象有很多个,Spring容器一旦把这个对象交给你之后,就不再管理这个对象了。 165 | 166 | 可以发现:Spring容器可以管理singleton作用域下bean的生命周期,在此作用域下,Spring能够精确地知道bean何时被创建,何时初始化完成,以及何时被销毁。而对于prototype作用域的bean,Spring只负责创建,当容器创建了bean的实例后,bean的实例就交给了客户端的代码管理,Spring容器将不再跟踪其生命周期,并且不会管理那些被配置成prototype作用域的bean的生命周期。 167 | 168 | ## 小结: 169 | 170 | 这节我们学了关于Bean的作用域,最常用的两种作用范围,以及Bean的生命周期和实例化流程的相关知识点,这些对于了解Spring还是非常重要的。 171 | 172 | ---- 173 | 174 | > 文章可以白嫖,公众号不能不关注,手动滑稽🤣🤣   175 | > 176 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**,您的支持,是我们创作的持续动力!   177 | 178 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 179 | 180 | ---- -------------------------------------------------------------------------------- /SpringBoot/00. 我们为什么要学习SpringBoot.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,公众号不能不关注,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**,您的支持,是我们创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png) 6 | ---- 7 | 8 | ## 1. Spring Boot是什么 9 | 10 | 我们知道,从 2002 年开始,Spring 一直在飞速的发展,如今已经成为了在Java EE(Java Enterprise Edition)开发中真正意义上的标准,但是随着技术的发展,Java EE使用 Spring 逐渐变得笨重起来,大量的 XML 文件存在于项目之中。**繁琐的配置,整合第三方框架的配置问题,导致了开发和部署效率的降低**。 11 | 12 | 2012 年 10 月,Mike Youngstrom 在 Spring jira 中创建了一个功能请求,要求**在 Spring 框架中支持无容器 Web 应用程序体系结构**。他谈到了在主容器引导 spring 容器内配置 Web 容器服务。这是 jira 请求的摘录: 13 | 14 | > 我认为 Spring 的 Web 应用体系结构可以大大简化,如果它提供了从上到下利用 Spring 组件和配置模型的工具和参考体系结构。在简单的 `main()`方法引导的 Spring 容器内嵌入和统一这些常用Web 容器服务的配置。 15 | 16 | 这一要求促使了 2013 年初开始的 Spring Boot 项目的研发,到今天,Spring Boot 的版本已经到了 2.0.3 RELEASE。Spring Boot 并不是用来替代 Spring 的解决方案,而**是和 Spring 框架紧密结合用于提升 Spring 开发者体验的工具**。 17 | 18 | 它集成了大量常用的第三方库配置,Spring Boot应用中这些第三方库几乎可以是零配置的开箱即用(out-of-the-box),大部分的 Spring Boot 应用都只需要非常少量的配置代码(基于 Java 的配置),开发者能够更加专注于业务逻辑。 19 | 20 | ## 2. 为什么学习Spring Boot 21 | 22 | ### 2.1 从Spring官方来看 23 | 24 | 我们打开 Spring 的[官方网站](http://spring.io/),可以看到下图: 25 | 26 | ![Spring官网首图](https://images.gitbook.cn/98fda7f0-8656-11e8-8675-5537a701ae7d) 27 | 28 | 我们可以看到图中官方对 Spring Boot 的定位:*Build Anything*, Build任何东西。Spring Boot旨在尽可能快地启动和运行,并且只需最少的 Spring 前期配置。 同时我们也来看一下官方对后面两个的定位: 29 | 30 | SpringCloud:*Coordinate Anything*,协调任何事情; 31 | SpringCloud Data Flow:*Connect everything*,连接任何东西。 32 | 33 | 仔细品味一下,Spring 官网对 Spring Boot、SpringCloud 和 SpringCloud Data Flow三者定位的措辞非常有味道,同时也可以看出,Spring 官方对这三个技术非常重视,是现在以及今后学习的重点(SpringCloud 相关达人课课程届时也会上线)。 34 | 35 | ### 2.2 从Spring Boot的优点来看 36 | 37 | Spring Boot 有哪些优点?主要给我们解决了哪些问题呢?我们以下图来说明: 38 | 39 | ![Spring Boot的优点](https://images.gitbook.cn/e17b8c40-8656-11e8-8a91-d70bc2d847c5) 40 | 41 | #### 2.2.1 良好的基因 42 | 43 | Spring Boot 是伴随着 Spring 4.0 诞生的,从字面理解,Boot是引导的意思,因此 Spring Boot 旨在帮助开发者快速搭建 Spring 框架。Spring Boot 继承了原有 Spring 框架的优秀基因,使 Spring 在使用中更加方便快捷。 44 | 45 | ![Spring Boot与Spring](https://images.gitbook.cn/00078650-8657-11e8-87de-d910a3ee087e) 46 | 47 | #### 2.2.2 简化编码 48 | 49 | 举个例子,比如我们要创建一个 web 项目,使用 Spring 的朋友都知道,在使用 Spring 的时候,需要在 pom 文件中添加多个依赖,而 Spring Boot 则会帮助开发着快速启动一个 web 容器,在 Spring Boot 中,我们只需要在 pom 文件中添加如下一个 starter-web 依赖即可。 50 | 51 | ```xml 52 | 53 | org.springframework.boot 54 | spring-boot-starter-web 55 | 56 | ``` 57 | 58 | 我们点击进入该依赖后可以看到,Spring Boot 这个 starter-web 已经包含了多个依赖,包括之前在 Spring 工程中需要导入的依赖,我们看一下其中的一部分,如下: 59 | 60 | ```xml 61 | 62 | 63 | org.springframework 64 | spring-web 65 | 5.0.7.RELEASE 66 | compile 67 | 68 | 69 | org.springframework 70 | spring-webmvc 71 | 5.0.7.RELEASE 72 | compile 73 | 74 | ``` 75 | 由此可以看出,Spring Boot 大大简化了我们的编码,我们不用一个个导入依赖,直接一个依赖即可。 76 | 77 | #### 2.2.3 简化配置 78 | 79 | Spring 虽然使Java EE轻量级框架,但由于其繁琐的配置,一度被人认为是“配置地狱”。各种XML、Annotation配置会让人眼花缭乱,而且配置多的话,如果出错了也很难找出原因。Spring Boot更多的是采用 Java Config 的方式,对 Spring 进行配置。举个例子: 80 | 81 | 我新建一个类,但是我不用 `@Service`注解,也就是说,它是个普通的类,那么我们如何使它也成为一个 Bean 让 Spring 去管理呢?只需要`@Configuration` 和`@Bean`两个注解即可,如下: 82 | ```java 83 | public class TestService { 84 | public String sayHello () { 85 | return "Hello Spring Boot!"; 86 | } 87 | } 88 | ``` 89 | ```java 90 | import org.springframework.context.annotation.Bean; 91 | import org.springframework.context.annotation.Configuration; 92 | 93 | @Configuration 94 | public class JavaConfig { 95 | @Bean 96 | public TestService getTestService() { 97 | return new TestService(); 98 | } 99 | } 100 | ``` 101 | `@Configuration`表示该类是个配置类,`@Bean`表示该方法返回一个 Bean。这样就把`TestService`作为 Bean 让 Spring 去管理了,在其他地方,我们如果需要使用该 Bean,和原来一样,直接使用`@Resource`注解注入进来即可使用,非常方便。 102 | ```java 103 | @Resource 104 | private TestService testService; 105 | ``` 106 | 107 | 另外,部署配置方面,原来 Spring 有多个 xml 和 properties配置,在 Spring Boot 中只需要个 application.yml即可。 108 | 109 | #### 2.2.4 简化部署 110 | 111 | 在使用 Spring 时,项目部署时需要我们在服务器上部署 tomcat,然后把项目打成 war 包扔到 tomcat里,在使用 Spring Boot 后,我们不需要在服务器上去部署 tomcat,因为 Spring Boot 内嵌了 tomcat,我们只需要将项目打成 jar 包,使用 `java -jar xxx.jar`一键式启动项目。 112 | 113 | 另外,也降低对运行环境的基本要求,环境变量中有JDK即可。 114 | 115 | #### 2.2.5 简化监控 116 | 117 | 我们可以引入 spring-boot-start-actuator 依赖,直接使用 REST 方式来获取进程的运行期性能参数,从而达到监控的目的,比较方便。但是 Spring Boot 只是个微框架,没有提供相应的服务发现与注册的配套功能,没有外围监控集成方案,没有外围安全管理方案,所以在微服务架构中,还需要 Spring Cloud 来配合一起使用。 118 | 119 | ### 2.3 从未来发展的趋势来看 120 | 121 | 微服务是未来发展的趋势,项目会从传统架构慢慢转向微服务架构,因为微服务可以使不同的团队专注于更小范围的工作职责、使用独立的技术、更安全更频繁地部署。而 继承了 Spring 的优良特性,与 Spring 一脉相承,而且 支持各种REST API 的实现方式。Spring Boot 也是官方大力推荐的技术,可以看出,Spring Boot 是未来发展的一个大趋势。 122 | 123 | ## 3. 本节能学到什么 124 | 125 | 本节使用目前 Spring Boot 最新版本2.0.3 RELEASE,文章均为作者在实际项目中剥离出来的场景和demo,目标是带领学习者快速上手 Spring Boot,将 Spring Boot 相关技术点快速运用在微服务项目中。全篇分为两部分:基础篇和进阶篇。 126 | 127 | 基础篇主要介绍 Spring Boot 在项目中最常使用的一些功能点,旨在带领学习者快速掌握 Spring Boot 在开发时需要的知识点,能够把 Spring Boot 相关技术运用到实际项目架构中去。该部分以 Spring Boot 框架为主线,内容包括Json数据封装、日志记录、属性配置、MVC支持、在线文档、模板引擎、异常处理、AOP 处理、持久层集成等等。 128 | 129 | 进阶篇主要是介绍 Spring Boot 在项目中拔高一些的技术点,包括集成的一些组件,旨在带领学习者在项目中遇到具体的场景时能够快速集成,完成对应的功能。该部分以 Spring Boot 框架为主线,内容包括拦截器、监听器、缓存、安全认证、分词插件、消息队列等等。 130 | 131 | 认真读完该系列文章之后,学习者会快速了解并掌握 Spring Boot 在项目中最常用的技术点,作者课程的最后,会基于课程内容搭建一个 Spring Boot 项目的空架构,该架构也是从实际项目中剥离出来,学习者可以运用该架构于实际项目中,具备使用 Spring Boot 进行实际项目开发的能力。 132 | 133 | 课程所有源码提供免费下载:[下载地址](https://gitee.com/eson15/springboot_study)。 134 | 135 | ----- 136 | 137 | > 文章可以白嫖,公众号不能不关注,手动滑稽🤣🤣   138 | > 139 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**,您的支持,是我们创作的持续动力!   140 | 141 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png) -------------------------------------------------------------------------------- /SpringBoot/01. Spring Boot 开发环境搭建和项目启动.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,公众号不能不关注,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**,您的支持,是我们创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png) 6 | ---- 7 | 8 | [上一节](https://github.com/eson15/javaAll/blob/master/SpringBoot/%E6%88%91%E4%BB%AC%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E5%AD%A6%E4%B9%A0SpringBoot.md)对 SpringBoot 的特性做了一个介绍,本节主要对 **jdk 的配置、Spring Boot工程的构建和项目的启动、Spring Boot 项目工程的结构做一下讲解和分析**。 9 | 10 | ## 1. jdk 的配置 11 | 12 | 本课程是使用 IDEA 进行开发,在IDEA 中配置 jdk 的方式很简单,打开`File->Project Structure`,如下图所: 13 | 14 | ![IDEA中配置jdk](https://images.gitbook.cn/9befde80-8659-11e8-9b0d-95de449dc107) 15 | 16 | 1. 选择 SDKs 17 | 2. 在 JDK home path 中选择本地 jdk 的安装目录 18 | 3. 在 Name 中为 jdk 自定义名字 19 | 20 | 通过以上三步骤,即可导入本地安装的 jdk。如果是使用 STS 或者 eclipse 的朋友,可以通过两步骤添加: 21 | * `window->preference->java->Instralled JRES`来添加本地 jdk。 22 | * `window-->preference-->java-->Compiler`选择 jre,和 jdk 保持一致。 23 | 24 | ## 2. Spring Boot 工程的构建 25 | 26 | ### 2.1 IDEA 快速构建 27 | 28 | IDEA 中可以通过`File->New->Project`来快速构建 Spring Boot 工程。如下,选择 Spring Initializr,在 Project SDK 中选择刚刚我们导入的 jdk,点击 Next,到了项目的配置信息。 29 | 30 | * Group:填企业域名,本课程使用com.itcodai 31 | * Artifact:填项目名称,本课程中每一课的工程名以`course+课号`命令,这里使用 course01 32 | * Dependencies:可以添加我们项目中所需要的依赖信息,根据实际情况来添加,本课程只需要选择 Web 即可。 33 | 34 | ### 2.2 官方构建 35 | 36 | 第二种方式可以通过官方构建,步骤如下: 37 | * 访问 http://start.spring.io/。 38 | * 在页面上输入相应的 Spring Boot 版本、Group 和 Artifact 信息以及项目依赖,然后创建项目。 39 | * ![创建Spring Boot工程](https://images.gitbook.cn/014a1ba0-865b-11e8-956e-f528114b28bd) 40 | * 解压后,使用 IDEA 导入该 maven 工程:`File->New->Model from Existing Source`,然后选择解压后的项目文件夹即可。如果是使用 eclipse 的朋友,可以通过`Import->Existing Maven Projects->Next`,然后选择解压后的项目文件夹即可。 41 | 42 | ### 2.3 maven配置 43 | 44 | 创建了 Spring Boot 项目之后,需要进行 maven 配置。打开`File->settings`,搜索 maven,配置一下本地的 maven 信息。如下: 45 | 46 | ![maven配置](https://images.gitbook.cn/2ff7b930-865b-11e8-8675-5537a701ae7d) 47 | 48 | 在 Maven home directory 中选择本地 Maven 的安装路径;在 User settings file 中选择本地 Maven 的配置文件所在路径。在配置文件中,我们配置一下国内阿里的镜像,这样在下载 maven 依赖时,速度很快。 49 | ```xml 50 | 51 | nexus-aliyun 52 | * 53 | Nexus aliyun 54 | http://maven.aliyun.com/nexus/content/groups/public 55 | 56 | ``` 57 | 如果是使用 eclipse 的朋友,可以通过`window-->preference-->Maven-->User Settings`来配置,配置方式和上面一致。 58 | 59 | ### 2.4 编码配置 60 | 61 | 同样地,新建项目后,我们一般都需要配置编码,这点非常重要,很多初学者都会忘记这一步,所以要养成良好的习惯。 62 | 63 | IDEA 中,仍然是打开`File->settings`,搜索 encoding,配置一下本地的编码信息。如下: 64 | 65 | ![编码配置](https://images.gitbook.cn/57564e60-865b-11e8-8a91-d70bc2d847c5) 66 | 67 | 如果是使用 eclipse 的朋友,有两个地方需要设置一下编码: 68 | * window--> perferences-->General-->Workspace,将Text file encoding改成utf-8 69 | * window-->perferences-->General-->content types,选中Text,将Default encoding填入utf-8 70 | 71 | OK,编码设置完成即可启动项目工程了。 72 | 73 | ## 3. Spring Boot 项目工程结构 74 | 75 | Spring Boot 项目总共有三个模块,如下图所示: 76 | 77 | ![Spring Boot项目工程结构](https://images.gitbook.cn/8b32fa80-865b-11e8-9d13-03ea4b4d8504) 78 | 79 | * src/main/java路径:主要编写业务程序 80 | * src/main/resources路径:存放静态文件和配置文件 81 | * src/test/java路径:主要编写测试程序 82 | 83 | 默认情况下,如上图所示会创建一个启动类 Course01Application,该类上面有个`@SpringBootApplication`注解,该启动类中有个 main 方法,没错,Spring Boot 启动只要运行该 main 方法即可,非常方便。另外,Spring Boot 内部集成了 tomcat,不需要我们人为手动去配置 tomcat,开发者只需要关注具体的业务逻辑即可。 84 | 85 | 到此为止,Spring Boot 就启动成功了,为了比较清楚的看到效果,我们写一个 Controller 来测试一下,如下: 86 | ```java 87 | package com.itcodai.course01.controller; 88 | 89 | import org.springframework.web.bind.annotation.RequestMapping; 90 | import org.springframework.web.bind.annotation.RestController; 91 | 92 | @RestController 93 | @RequestMapping("/start") 94 | public class StartController { 95 | 96 | @RequestMapping("/springboot") 97 | public String startSpringBoot() { 98 | return "Welcome to the world of Spring Boot!"; 99 | } 100 | } 101 | ``` 102 | 重新运行 main 方法启动项目,在浏览器中输入 `localhost:8080/start/springboot`,如果看到 `“Welcome to the world of Spring Boot!”`,那么恭喜你项目启动成功!Spring Boot 就是这么简单方便!端口号默认是8080,如果想要修改,可以在 application.yml 文件中使用 `server.port` 来人为指定端口,如8001端口: 103 | ```xml 104 | server: 105 | port: 8001 106 | ``` 107 | 108 | ## 4. 总结 109 | 110 | 本节我们快速学习了如何在 IDEA 中导入 jdk,以及使用 IDEA 如何配置 maven 和编码,如何快速的创建和启动 Spring Boot 工程。IDEA 对 Spring Boot 的支持非常友好,建议大家使用 IDEA 进行 Spring Boot 的开发,从下一课开始,我们真正进入 Spring Boot 的学习中。 111 | 课程源代码下载地址:[戳我下载](https://gitee.com/eson15/springboot_study) 112 | 113 | ---- 114 | 115 | > 文章可以白嫖,公众号不能不关注,手动滑稽🤣🤣   116 | > 117 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**,您的支持,是我们创作的持续动力!   118 | 119 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png) -------------------------------------------------------------------------------- /SpringBoot/03. Spring Boot 使用slf4j进行日志记录.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,公众号不能不关注,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**,您的支持,是我们创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png) 6 | ---- 7 | 8 | 在开发中,我们经常使用 `System.out.println()` 来打印一些信息,但是这样不好,因为大量的使用 `System.out` 会增加资源的消耗。我们实际项目中使用的是 slf4j 的 logback 来输出日志,效率挺高的,Spring Boot 提供了一套日志系统,logback 是最优的选择。 9 | 10 | ## 1. slf4j 介绍 11 | 12 | 引用百度百科里的一段话: 13 | > SLF4J,即简单日志门面(Simple Logging Facade for Java),不是具体的日志解决方案,它只服务于各种各样的日志系统。按照官方的说法,SLF4J是一个用于日志系统的简单Facade,允许最终用户在部署其应用时使用其所希望的日志系统。 14 | 15 | 这段的大概意思是:你只需要按统一的方式写记录日志的代码,而无需关心日志是通过哪个日志系统,以什么风格输出的。因为它们取决于部署项目时绑定的日志系统。例如,在项目中使用了 slf4j 记录日志,并且绑定了 log4j(即导入相应的依赖),则日志会以 log4j 的风格输出;后期需要改为以 logback 的风格输出日志,只需要将 log4j 替换成 logback 即可,不用修改项目中的代码。这对于第三方组件的引入的不同日志系统来说几乎零学习成本,况且它的优点不仅仅这一个而已,还有简洁的占位符的使用和日志级别的判断。 16 | 17 | 正因为 sfl4j 有如此多的优点,阿里巴巴已经将 slf4j 作为他们的日志框架了。在《阿里巴巴Java开发手册(正式版)》中,日志规约一项第一条就强制要求使用 slf4j: 18 | > 1.【强制】应用中不可直接使用日志系统(Log4j、Logback)中的API,而应依赖使用日志框架SLF4J中的API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。 19 | 20 | “强制”两个字体现出了 slf4j 的优势,所以建议在实际项目中,使用 slf4j 作为自己的日志框架。使用 slf4j 记录日志非常简单,直接使用 LoggerFactory 创建即可。 21 | ```java 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | 25 | public class Test { 26 | private static final Logger logger = LoggerFactory.getLogger(Test.class); 27 | // …… 28 | } 29 | ``` 30 | 31 | ## 2. application.yml 中对日志的配置 32 | 33 | Spring Boot 对 slf4j 支持的很好,内部已经集成了 slf4j,一般我们在使用的时候,会对slf4j 做一下配置。`application.yml` 文件是 Spring Boot 中唯一一个需要配置的文件,一开始创建工程的时候是 `application.properties` 文件,个人比较细化用 yml 文件,因为 yml 文件的层次感特别好,看起来更直观,但是 yml 文件对格式要求比较高,比如英文冒号后面必须要有个空格,否则项目估计无法启动,而且也不报错。用 properties 还是 yml 视个人习惯而定,都可以。本课程使用 yml。 34 | 35 | 我们看一下 application.yml 文件中对日志的配置: 36 | ```xml 37 | logging: 38 | config: logback.xml 39 | level: 40 | com.itcodai.course03.dao: trace 41 | ``` 42 | `logging.config` 是用来指定项目启动的时候,读取哪个配置文件,这里指定的是日志配置文件是根路径下的 `logback.xml` 文件,关于日志的相关配置信息,都放在 `logback.xml` 文件中了。`logging.level` 是用来指定具体的 mapper 中日志的输出级别,上面的配置表示 `com.itcodai.course03.dao` 包下的所有 mapper 日志输出级别为 trace,会将操作数据库的 sql 打印出来,开发时设置成 trace 方便定位问题,在生产环境上,将这个日志级别再设置成 error 级别即可(本节课不讨论 mapper 层,在后面 Spring Boot 集成 MyBatis 时再详细讨论)。 43 | 44 | 常用的日志级别按照从高到低依次为:ERROR、WARN、INFO、DEBUG。 45 | 46 | ## 3. logback.xml 配置文件解析 47 | 48 | 在上面 `application.yml` 文件中,我们指定了日志配置文件 `logback.xml`,`logback.xml` 文件中主要用来做日志的相关配置。在 `logback.xml` 中,我们可以定义日志输出的格式、路径、控制台输出格式、文件大小、保存时长等等。下面来分析一下: 49 | 50 | ### 3.1 定义日志输出格式和存储路径 51 | 52 | ```xml 53 | 54 | 55 | 56 | 57 | ``` 58 | 我们来看一下这个定义的含义:首先定义一个格式,命名为 “LOG_PATTERN”,该格式中 `%date` 表示日期,`%thread` 表示线程名,`%-5level` 表示级别从左显示5个字符宽度,`%logger{36}` 表示 logger 名字最长36个字符,`%msg` 表示日志消息,`%n` 是换行符。 59 | 60 | 然后再定义一下名为 “FILE_PATH” 文件路径,日志都会存储在该路径下。`%i` 表示第 i 个文件,当日志文件达到指定大小时,会将日志生成到新的文件里,这里的 i 就是文件索引,日志文件允许的大小可以设置,下面会讲解。这里需要注意的是,不管是 windows 系统还是 Linux 系统,日志存储的路径必须要是绝对路径。 61 | 62 | ### 3.2 定义控制台输出 63 | 64 | ```xml 65 | 66 | 67 | 68 | 69 | ${LOG_PATTERN} 70 | 71 | 72 | 73 | ``` 74 | 使用 `` 节点设置个控制台输出(`class="ch.qos.logback.core.ConsoleAppender"`)的配置,定义为 “CONSOLE”。使用上面定义好的输出格式(LOG_PATTERN)来输出,使用 `${}` 引用进来即可。 75 | 76 | ### 3.3 定义日志文件的相关参数 77 | 78 | ```xml 79 | 80 | 81 | 82 | 83 | ${FILE_PATH} 84 | 85 | 15 86 | 87 | 88 | 10MB 89 | 90 | 91 | 92 | 93 | 94 | ${LOG_PATTERN} 95 | 96 | 97 | 98 | ``` 99 | 使用 `` 定义一个名为 “FILE” 的文件配置,主要是配置日志文件保存的时间、单个日志文件存储的大小、以及文件保存的路径和日志的输出格式。 100 | 101 | ### 3.4 定义日志输出级别 102 | 103 | ```xml 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | ``` 112 | 有了上面那些定义后,最后我们使用 `` 来定义一下项目中默认的日志输出级别,这里定义级别为 INFO,然后针对 INFO 级别的日志,使用 `` 引用上面定义好的控制台日志输出和日志文件的参数。这样 logback.xml 文件中的配置就设置完了。 113 | 114 | ## 4. 使用Logger在项目中打印日志 115 | 116 | 在代码中,我们一般使用 Logger 对象来打印出一些 log 信息,可以指定打印出的日志级别,也支持占位符,很方便。 117 | ```java 118 | import org.slf4j.Logger; 119 | import org.slf4j.LoggerFactory; 120 | import org.springframework.web.bind.annotation.RequestMapping; 121 | import org.springframework.web.bind.annotation.RestController; 122 | 123 | @RestController 124 | @RequestMapping("/test") 125 | public class TestController { 126 | 127 | private final static Logger logger = LoggerFactory.getLogger(TestController.class); 128 | 129 | @RequestMapping("/log") 130 | public String testLog() { 131 | logger.debug("=====测试日志debug级别打印===="); 132 | logger.info("======测试日志info级别打印====="); 133 | logger.error("=====测试日志error级别打印===="); 134 | logger.warn("======测试日志warn级别打印====="); 135 | 136 | // 可以使用占位符打印出一些参数信息 137 | String str1 = "武哥聊编程"; 138 | String str2 = "blog.csdn.net/eson_15"; 139 | logger.info("======武哥的公众号:{};武哥的CSDN博客:{}", str1, str2); 140 | 141 | return "success"; 142 | } 143 | } 144 | ``` 145 | 启动该项目,在浏览器中输入 `localhost:8080/test/log` 后可以看到控制台的日志记录: 146 | >1. ======测试日志info级别打印===== 147 | >2. =====测试日志error级别打印==== 148 | >3. ======测试日志warn级别打印===== 149 | >4. ======武哥的公众号:武哥聊编程;武哥的CSDN博客:blog.csdn.net/eson_15 150 | 151 | 因为 INFO 级别比 DEBUG 级别高,所以 debug 这条没有打印出来,如果将 logback.xml 中的日志级别设置成 DEBUG,那么四条语句都会打印出来,这个大家自己去测试了。同时可以打开 D:\logs\course03\ 目录,里面有刚刚项目启动,以后后面生成的所有日志记录。在项目部署后,我们大部分都是通过查看日志文件来定位问题。 152 | 153 | ## 5. 总结 154 | 155 | 本节课主要对 slf4j 做了一个简单的介绍,并且对 Spring Boot 中如何使用 slf4j 输出日志做了详细的说明,着重分析了 `logback.xml` 文件中对日志相关信息的配置,包括日志的不同级别。最后针对这些配置,在代码中使用 Logger 打印出一些进行测试。在实际项目中,这些日志都是排查问题的过程中非常重要的资料。 156 | 课程源代码下载地址:[戳我下载](https://gitee.com/eson15/springboot_study) 157 | 158 | ----- 159 | 160 | > 文章可以白嫖,公众号不能不关注,手动滑稽🤣🤣   161 | > 162 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**,您的支持,是我们创作的持续动力!   163 | 164 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java学习指南](https://img-blog.csdnimg.cn/20200608005630228.png) -------------------------------------------------------------------------------- /SpringBoot/04. Spring Boot 中的项目属性配置.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,公众号不能不关注,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**,您的支持,是我们创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java学习指南](https://img-blog.csdnimg.cn/20200608005630228.png) 6 | ---- 7 | 8 | 我们知道,在项目中,很多时候需要用到一些配置的信息,这些信息可能在测试环境和生产环境下会有不同的配置,后面根据实际业务情况有可能还会做修改,针对这种情况,我们不能将这些配置在代码中写死,最好就是写到配置文件中。比如可以把这些信息写到 `application.yml` 文件中。 9 | 10 | ## 1. 少量配置信息的情形 11 | 12 | 举个例子,在微服务架构中,最常见的就是某个服务需要调用其他服务来获取其提供的相关信息,那么在该服务的配置文件中需要配置被调用的服务地址,比如在当前服务里,我们需要调用订单微服务获取订单相关的信息,假设 订单服务的端口号是 8002,那我们可以做如下配置: 13 | ```xml 14 | server: 15 | port: 8001 16 | 17 | # 配置微服务的地址 18 | url: 19 | # 订单微服务的地址 20 | orderUrl: http://localhost:8002 21 | ``` 22 | 然后在业务代码中如何获取到这个配置的订单服务地址呢?我们可以使用 `@Value` 注解来解决。在对应的类中加上一个属性,在属性上使用 `@Value` 注解即可获取到配置文件中的配置信息,如下: 23 | ```java 24 | import org.slf4j.Logger; 25 | import org.slf4j.LoggerFactory; 26 | import org.springframework.beans.factory.annotation.Value; 27 | import org.springframework.web.bind.annotation.RequestMapping; 28 | import org.springframework.web.bind.annotation.RestController; 29 | 30 | @RestController 31 | @RequestMapping("/test") 32 | public class ConfigController { 33 | 34 | private static final Logger LOGGER = LoggerFactory.getLogger(ConfigController.class); 35 | 36 | @Value("${url.orderUrl}") 37 | private String orderUrl; 38 | 39 | @RequestMapping("/config") 40 | public String testConfig() { 41 | LOGGER.info("=====获取的订单服务地址为:{}", orderUrl); 42 | return "success"; 43 | } 44 | } 45 | ``` 46 | `@Value` 注解上通过 `${key}` 即可获取配置文件中和 key 对应的 value 值。我们启动一下项目,在浏览器中输入 `localhost:8080/test/config` 请求服务后,可以看到控制台会打印出订单服务的地址: 47 | 48 | ``` 49 | =====获取的订单服务地址为:http://localhost:8002 50 | ``` 51 | 52 | 说明我们成功获取到了配置文件中的订单微服务地址,在实际项目中也是这么用的,后面如果因为服务器部署的原因,需要修改某个服务的地址,那么只要在配置文件中修改即可。 53 | 54 | ## 2. 多个配置信息的情形 55 | 56 | 这里再引申一个问题,随着业务复杂度的增加,一个项目中可能会有越来越多的微服务,某个模块可能需要调用多个微服务获取不同的信息,那么就需要在配置文件中配置多个微服务的地址。可是,在需要调用这些微服务的代码中,如果这样一个个去使用 `@Value` 注解引入相应的微服务地址的话,太过于繁琐,也不科学。 57 | 58 | 所以,在实际项目中,业务繁琐,逻辑复杂的情况下,需要考虑封装一个或多个配置类。举个例子:假如在当前服务中,某个业务需要同时调用订单微服务、用户微服务和购物车微服务,分别获取订单、用户和购物车相关信息,然后对这些信息做一定的逻辑处理。那么在配置文件中,我们需要将这些微服务的地址都配置好: 59 | ```xml 60 | # 配置多个微服务的地址 61 | url: 62 | # 订单微服务的地址 63 | orderUrl: http://localhost:8002 64 | # 用户微服务的地址 65 | userUrl: http://localhost:8003 66 | # 购物车微服务的地址 67 | shoppingUrl: http://localhost:8004 68 | ``` 69 | 也许实际业务中,远远不止这三个微服务,甚至十几个都有可能。对于这种情况,我们可以先定义一个 `MicroServiceUrl` 类来专门保存微服务的 url,如下: 70 | 71 | ```java 72 | @Component 73 | @ConfigurationProperties(prefix = "url") 74 | public class MicroServiceUrl { 75 | 76 | private String orderUrl; 77 | private String userUrl; 78 | private String shoppingUrl; 79 | // 省去get和set方法 80 | } 81 | ``` 82 | 细心的朋友应该可以看到,使用 `@ConfigurationProperties` 注解并且使用 prefix 来指定一个前缀,然后该类中的属性名就是配置中去掉前缀后的名字,一一对应即可。即:前缀名 + 属性名就是配置文件中定义的 key。同时,该类上面需要加上 `@Component` 注解,把该类作为组件放到Spring容器中,让 Spring 去管理,我们使用的时候直接注入即可。 83 | 84 | 需要注意的是,使用 `@ConfigurationProperties` 注解需要导入它的依赖: 85 | ```xml 86 | 87 | org.springframework.boot 88 | spring-boot-configuration-processor 89 | true 90 | 91 | ``` 92 | 93 | OK,到此为止,我们将配置写好了,接下来写个 Controller 来测试一下。此时,不需要在代码中一个个引入这些微服务的 url 了,直接通过 `@Resource` 注解将刚刚写好配置类注入进来即可使用了,非常方便。如下: 94 | ```java 95 | @RestController 96 | @RequestMapping("/test") 97 | public class TestController { 98 | 99 | private static final Logger LOGGER = LoggerFactory.getLogger(TestController.class); 100 | 101 | @Resource 102 | private MicroServiceUrl microServiceUrl; 103 | 104 | @RequestMapping("/config") 105 | public String testConfig() { 106 | LOGGER.info("=====获取的订单服务地址为:{}", microServiceUrl.getOrderUrl()); 107 | LOGGER.info("=====获取的用户服务地址为:{}", microServiceUrl.getUserUrl()); 108 | LOGGER.info("=====获取的购物车服务地址为:{}", microServiceUrl.getShoppingUrl()); 109 | 110 | return "success"; 111 | } 112 | } 113 | ``` 114 | 115 | 再次启动项目,请求一下可以看到,控制台打印出如下信息,说明配置文件生效,同时正确获取配置文件内容: 116 | ``` 117 | =====获取的订单服务地址为:http://localhost:8002 118 | =====获取的订单服务地址为:http://localhost:8002 119 | =====获取的用户服务地址为:http://localhost:8003 120 | =====获取的购物车服务地址为:http://localhost:8004 121 | ``` 122 | 123 | ## 3. 指定项目配置文件 124 | 125 | 我们知道,在实际项目中,一般有两个环境:开发环境和生产环境。开发环境中的配置和生产环境中的配置往往不同,比如:环境、端口、数据库、相关地址等等。我们不可能在开发环境调试好之后,部署到生产环境后,又要将配置信息全部修改成生产环境上的配置,这样太麻烦,也不科学。 126 | 127 | 最好的解决方法就是开发环境和生产环境都有一套对用的配置信息,然后当我们在开发时,指定读取开发环境的配置,当我们将项目部署到服务器上之后,再指定去读取生产环境的配置。 128 | 129 | 我们新建两个配置文件: `application-dev.yml` 和 `application-pro.yml`,分别用来对开发环境和生产环境进行相关配置。这里为了方便,我们分别设置两个访问端口号,开发环境用 8001,生产环境用 8002. 130 | ```xml 131 | # 开发环境配置文件 132 | server: 133 | port: 8001 134 | ``` 135 | ```xml 136 | # 开发环境配置文件 137 | server: 138 | port: 8002 139 | ``` 140 | 然后在 `application.yml` 文件中指定读取哪个配置文件即可。比如我们在开发环境下,指定读取 `applicationn-dev.yml` 文件,如下: 141 | ```xml 142 | spring: 143 | profiles: 144 | active: 145 | - dev 146 | ``` 147 | 这样就可以在开发的时候,指定读取 `application-dev.yml` 文件,访问的时候使用 8001 端口,部署到服务器后,只需要将 `application.yml` 中指定的文件改成 `application-pro.yml` 即可,然后使用 8002 端口访问,非常方便。 148 | 149 | ## 4. 总结 150 | 151 | 本节课主要讲解了 Spring Boot 中如何在业务代码中读取相关配置,包括单一配置和多个配置项,在微服务中,这种情况非常常见,往往会有很多其他微服务需要调用,所以封装一个配置类来接收这些配置是个很好的处理方式。除此之外,例如数据库相关的连接参数等等,也可以放到一个配置类中,其他遇到类似的场景,都可以这么处理。最后介绍了开发环境和生产环境配置的快速切换方式,省去了项目部署时,诸多配置信息的修改。 152 | 课程源代码下载地址:[戳我下载](https://gitee.com/eson15/springboot_study) 153 | 154 | ---- 155 | 156 | > 文章可以白嫖,公众号不能不关注,手动滑稽🤣🤣   157 | > 158 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**,您的支持,是我们创作的持续动力!   159 | 160 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java学习指南](https://img-blog.csdnimg.cn/20200608005630228.png) -------------------------------------------------------------------------------- /SpringBoot/05. Spring Boot 中的MVC支持.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,公众号不能不关注,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**,您的支持,是我们创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png) 6 | ---- 7 | 8 | Spring Boot 的 MVC 支持主要来介绍实际项目中最常用的几个注解,包括 `@RestController`、 `@RequestMapping`、`@PathVariable`、`@RequestParam` 以及 `@RequestBody`。主要介绍这几个注解常用的使用方式和特点。 9 | 10 | ## 1. @RestController 11 | 12 | `@RestController` 是 Spring Boot 新增的一个注解,我们看一下该注解都包含了哪些东西。 13 | ```java 14 | @Target({ElementType.TYPE}) 15 | @Retention(RetentionPolicy.RUNTIME) 16 | @Documented 17 | @Controller 18 | @ResponseBody 19 | public @interface RestController { 20 | String value() default ""; 21 | } 22 | ``` 23 | 可以看出, `@RestController` 注解包含了原来的 `@Controller` 和 `@ResponseBody` 注解,使用过 Spring 的朋友对 `@Controller` 注解已经非常了解了,这里不再赘述, `@ResponseBody` 注解是将返回的数据结构转换为 Json 格式。所以 `@RestController` 可以看作是 `@Controller` 和 `@ResponseBody` 的结合体,相当于偷个懒,我们使用 `@RestController` 之后就不用再使用 `@Controller` 了。但是需要注意一个问题:如果是前后端分离,不用模板渲染的话,比如 Thymeleaf,这种情况下是可以直接使用`@RestController` 将数据以 json 格式传给前端,前端拿到之后解析;但如果不是前后端分离,需要使用模板来渲染的话,一般 Controller 中都会返回到具体的页面,那么此时就不能使用`@RestController`了,比如: 24 | ```java 25 | public String getUser() { 26 | return "user"; 27 | } 28 | ``` 29 | 其实是需要返回到 user.html 页面的,如果使用 `@RestController` 的话,会将 user 作为字符串返回的,所以这时候我们需要使用 `@Controller` 注解。这在下一节 Spring Boot 集成 Thymeleaf 模板引擎中会再说明。 30 | 31 | 32 | ## 2. @RequestMapping 33 | 34 | `@RequestMapping` 是一个用来处理请求地址映射的注解,它可以用于类上,也可以用于方法上。在类的级别上的注解会将一个特定请求或者请求模式映射到一个控制器之上,表示类中的所有响应请求的方法都是以该地址作为父路径;在方法的级别表示进一步指定到处理方法的映射关系。 35 | 36 | 该注解有6个属性,一般在项目中比较常用的有三个属性:value、method 和 produces。 37 | * value 属性:指定请求的实际地址,value 可以省略不写 38 | * method 属性:指定请求的类型,主要有 GET、PUT、POST、DELETE,默认为 GET 39 | * produces属性:指定返回内容类型,如 produces = "application/json; charset=UTF-8" 40 | 41 | `@RequestMapping` 注解比较简单,举个例子: 42 | ```java 43 | @RestController 44 | @RequestMapping(value = "/test", produces = "application/json; charset=UTF-8") 45 | public class TestController { 46 | 47 | @RequestMapping(value = "/get", method = RequestMethod.GET) 48 | public String testGet() { 49 | return "success"; 50 | } 51 | } 52 | ``` 53 | 这个很简单,启动项目在浏览器中输入 `localhost:8080/test/get` 测试一下即可。 54 | 55 | 针对四种不同的请求方式,是有相应注解的,不用每次在 `@RequestMapping` 注解中加 method 属性来指定,上面的 GET 方式请求可以直接使用 `@GetMapping("/get")` 注解,效果一样。相应地,PUT 方式、POST 方式和 DELETE 方式对应的注解分别为 `@PutMapping`、`@PostMapping` 和 `DeleteMapping`。 56 | 57 | ## 3. @PathVariable 58 | 59 | `@PathVariable` 注解主要是用来获取 url 参数,Spring Boot 支持 restfull 风格的 url,比如一个 GET 请求携带一个参数 id 过来,我们将 id 作为参数接收,可以使用 `@PathVariable` 注解。如下: 60 | ```java 61 | @GetMapping("/user/{id}") 62 | public String testPathVariable(@PathVariable Integer id) { 63 | System.out.println("获取到的id为:" + id); 64 | return "success"; 65 | } 66 | ``` 67 | 这里需要注意一个问题,如果想要 url 中占位符中的 id 值直接赋值到参数 id 中,需要保证 url 中的参数和方法接收参数一致,否则就无法接收。如果不一致的话,其实也可以解决,需要用 `@PathVariable` 中的 value 属性来指定对应关系。如下: 68 | ```java 69 | @RequestMapping("/user/{idd}") 70 | public String testPathVariable(@PathVariable(value = "idd") Integer id) { 71 | System.out.println("获取到的id为:" + id); 72 | return "success"; 73 | } 74 | ``` 75 | 对于访问的 url,占位符的位置可以在任何位置,不一定非要在最后,比如这样也行:`/xxx/{id}/user`。另外,url 也支持多个占位符,方法参数使用同样数量的参数来接收,原理和一个参数是一样的,例如: 76 | ```java 77 | @GetMapping("/user/{idd}/{name}") 78 | public String testPathVariable(@PathVariable(value = "idd") Integer id, @PathVariable String name) { 79 | System.out.println("获取到的id为:" + id); 80 | System.out.println("获取到的name为:" + name); 81 | return "success"; 82 | } 83 | ``` 84 | 运行项目,在浏览器中请求 `localhost:8080/test/user/2/zhangsan` 可以看到控制台输出如下信息: 85 | ``` 86 | 获取到的id为:2 87 | 获取到的name为:zhangsan 88 | ``` 89 | 所以支持多个参数的接收。同样地,如果 url 中的参数和方法中的参数名称不同的话,也需要使用 value 属性来绑定两个参数。 90 | 91 | 92 | ## 4. @RequestParam 93 | 94 | `@RequestParam` 注解顾名思义,也是获取请求参数的,上面我们介绍了 `@PathValiable` 注解也是获取请求参数的,那么 `@RequestParam` 和 `@PathVariable` 有什么不同呢?主要区别在于: `@PathValiable` 是从 url 模板中获取参数值, 即这种风格的 url:`http://localhost:8080/user/{id}` ;而 `@RequestParam` 是从 request 里面获取参数值,即这种风格的 url:`http://localhost:8080/user?id=1` 。我们使用该 url 带上参数 id 来测试一下如下代码: 95 | ```java 96 | @GetMapping("/user") 97 | public String testRequestParam(@RequestParam Integer id) { 98 | System.out.println("获取到的id为:" + id); 99 | return "success"; 100 | } 101 | ``` 102 | 可以正常从控制台打印出 id 信息。同样地,url 上面的参数和方法的参数需要一致,如果不一致,也需要使用 value 属性来说明,比如 url 为:`http://localhost:8080/user?idd=1` 103 | ```java 104 | @RequestMapping("/user") 105 | public String testRequestParam(@RequestParam(value = "idd", required = false) Integer id) { 106 | System.out.println("获取到的id为:" + id); 107 | return "success"; 108 | } 109 | ``` 110 | 除了 value 属性外,还有个两个属性比较常用: 111 | * required 属性:true 表示该参数必须要传,否则就会报 404 错误,false 表示可有可无。 112 | * defaultValue 属性:默认值,表示如果请求中没有同名参数时的默认值。 113 | 114 | 从 url 中可以看出,`@RequestParam` 注解用于 GET 请求上时,接收拼接在 url 中的参数。除此之外,该注解还可以用于 POST 请求,接收前端表单提交的参数,假如前端通过表单提交 username 和 password 两个参数,那我们可以使用 `@RequestParam` 来接收,用法和上面一样。 115 | ```java 116 | @PostMapping("/form1") 117 | public String testForm(@RequestParam String username, @RequestParam String password) { 118 | System.out.println("获取到的username为:" + username); 119 | System.out.println("获取到的password为:" + password); 120 | return "success"; 121 | } 122 | ``` 123 | 我们使用 postman 来模拟一下表单提交,测试一下接口: 124 | 125 | ![使用postman测试表单提交](https://img-blog.csdnimg.cn/20200215043749282.png) 126 | 127 | 那么问题来了,如果表单数据很多,我们不可能在后台方法中写上很多参数,每个参数还要 `@RequestParam` 注解。针对这种情况,我们需要封装一个实体类来接收这些参数,实体中的属性名和表单中的参数名一致即可。 128 | ```java 129 | public class User { 130 | private String username; 131 | private String password; 132 | // set get 133 | } 134 | ``` 135 | 使用实体接收的话,我们不能在前面加 `@RequestParam` 注解了,直接使用即可。 136 | ```java 137 | @PostMapping("/form2") 138 | public String testForm(User user) { 139 | System.out.println("获取到的username为:" + user.getUsername()); 140 | System.out.println("获取到的password为:" + user.getPassword()); 141 | return "success"; 142 | } 143 | ``` 144 | 使用 postman 再次测试一下表单提交,观察一下返回值和控制台打印出的日志即可。在实际项目中,一般都是封装一个实体类来接收表单数据,因为实际项目中表单数据一般都很多。 145 | 146 | 147 | ## 5. @RequestBody 148 | 149 | `@RequestBody` 注解用于接收前端传来的实体,接收参数也是对应的实体,比如前端通过 json 提交传来两个参数 username 和 password,此时我们需要在后端封装一个实体来接收。在传递的参数比较多的情况下,使用 `@RequestBody` 接收会非常方便。例如: 150 | ```java 151 | public class User { 152 | private String username; 153 | private String password; 154 | // set get 155 | } 156 | ``` 157 | ```java 158 | @PostMapping("/user") 159 | public String testRequestBody(@RequestBody User user) { 160 | System.out.println("获取到的username为:" + user.getUsername()); 161 | System.out.println("获取到的password为:" + user.getPassword()); 162 | return "success"; 163 | } 164 | ``` 165 | 我们使用 postman 工具来测试一下效果,打开 postman,然后输入请求地址和参数,参数我们用 json 来模拟,如下图所有,调用之后返回 success。 166 | 167 | ![使用Postman测试requestBody](https://img-blog.csdnimg.cn/20200215043853532.png) 168 | 169 | 同时看一下后台控制台输出的日志: 170 | ``` 171 | 获取到的username为:倪升武 172 | 获取到的password为:123456 173 | ``` 174 | 可以看出,`@RequestBody` 注解用于 POST 请求上,接收 json 实体参数。它和上面我们介绍的表单提交有点类似,只不过参数的格式不同,一个是 json 实体,一个是表单提交。在实际项目中根据具体场景和需要使用对应的注解即可。 175 | 176 | ## 6. 总结 177 | 178 | 本节课主要讲解了 Spring Boot 中对 MVC 的支持,分析了 `@RestController`、 `@RequestMapping`、`@PathVariable`、 `@RequestParam` 和 `@RequestBody` 四个注解的使用方式,由于 `@RestController` 中集成了 `@ResponseBody` 所以对返回 json 的注解不再赘述。以上四个注解是使用频率很高的注解,在所有的实际项目中基本都会遇到,要熟练掌握。 179 | 180 | 课程源代码下载地址:[戳我下载](https://gitee.com/eson15/springboot_study) 181 | 182 | ---- 183 | 184 | > 文章可以白嫖,公众号不能不关注,手动滑稽🤣🤣   185 | > 186 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**,您的支持,是我们创作的持续动力!   187 | 188 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png) -------------------------------------------------------------------------------- /SpringBoot/10. Spring Boot 集成MyBatis.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,公众号不能不关注,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**,您的支持,是我们创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png) 6 | ---- 7 | 8 | ## 1. MyBatis 介绍 9 | 10 | 大家都知道,MyBatis 框架是一个持久层框架,是 Apache 下的顶级项目。Mybatis 可以让开发者的主要精力放在 sql 上,通过 Mybatis 提供的映射方式,自由灵活的生成满足需要的 sql 语句。使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs 映射成数据库中的记录,在国内可谓是占据了半壁江山。本节课程主要通过两种方式来对 Spring Boot 集成 MyBatis 做一讲解。重点讲解一下基于注解的方式。因为实际项目中使用注解的方式更多一点,更简洁一点,省去了很多 xml 配置(这不是绝对的,有些项目组中可能也在使用 xml 的方式)。 11 | 12 | ## 2. MyBatis 的配置 13 | 14 | ## 2.1 依赖导入 15 | 16 | Spring Boot 集成 MyBatis,需要导入 `mybatis-spring-boot-starter` 和 mysql 的依赖,这里我们使用的版本时 1.3.2,如下: 17 | ```xml 18 | 19 | org.mybatis.spring.boot 20 | mybatis-spring-boot-starter 21 | 1.3.2 22 | 23 | 24 | mysql 25 | mysql-connector-java 26 | runtime 27 | 28 | ``` 29 | 我们点开 `mybatis-spring-boot-starter` 依赖,可以看到我们之前使用 Spring 时候熟悉的依赖,就像我在课程的一开始介绍的那样,Spring Boot 致力于简化编码,使用 starter 系列将相关依赖集成在一起,开发者不需要关注繁琐的配置,非常方便。 30 | ```xml 31 | 32 | 33 | org.mybatis 34 | mybatis 35 | 36 | 37 | org.mybatis 38 | mybatis-spring 39 | 40 | ``` 41 | 42 | ## 2.2 properties.yml配置 43 | 44 | 我们再来看一下,集成 MyBatis 时需要在 properties.yml 配置文件中做哪些基本配置呢? 45 | 46 | ```xml 47 | # 服务端口号 48 | server: 49 | port: 8080 50 | 51 | # 数据库地址 52 | datasource: 53 | url: localhost:3306/blog_test 54 | 55 | spring: 56 | datasource: # 数据库配置 57 | driver-class-name: com.mysql.jdbc.Driver 58 | url: jdbc:mysql://${datasource.url}?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&autoReconnect=true&failOverReadOnly=false&maxReconnects=10 59 | username: root 60 | password: 123456 61 | hikari: 62 | maximum-pool-size: 10 # 最大连接池数 63 | max-lifetime: 1770000 64 | 65 | mybatis: 66 | # 指定别名设置的包为所有entity 67 | type-aliases-package: com.itcodai.course10.entity 68 | configuration: 69 | map-underscore-to-camel-case: true # 驼峰命名规范 70 | mapper-locations: # mapper映射文件位置 71 | - classpath:mapper/*.xml 72 | ``` 73 | 我们来简单介绍一下上面的这些配置:关于数据库的相关配置,我就不详细的解说了,这点相信大家已经非常熟练了,配置一下用户名、密码、数据库连接等等,这里使用的连接池是 Spring Boot 自带的 hikari,感兴趣的朋友可以去百度或者谷歌搜一搜,了解一下。 74 | 75 | 这里说明一下 `map-underscore-to-camel-case: true`, 用来开启驼峰命名规范,这个比较好用,比如数据库中字段名为:`user_name`, 那么在实体类中可以定义属性为 `userName` (甚至可以写成 `username`,也能映射上),会自动匹配到驼峰属性,如果不这样配置的话,针对字段名和属性名不同的情况,会映射不到。 76 | 77 | ## 3. 基于 xml 的整合 78 | 79 | 使用原始的 xml 方式,需要新建 UserMapper.xml 文件,在上面的 application.yml 配置文件中,我们已经定义了 xml 文件的路径:`classpath:mapper/*.xml`,所以我们在 resources 目录下新建一个 mapper 文件夹,然后创建一个 UserMapper.xml 文件。 80 | ```xml 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 94 | 95 | ``` 96 | 这和整合 Spring 一样的,namespace 中指定的是对应的 Mapper, `` 中指定对应的实体类,即 User。然后在内部指定表的字段和实体的属性相对应即可。这里我们写一个根据用户名查询用户的 sql。 97 | 98 | 实体类中有 id,username 和 password,我不在这贴代码,大家可以下载源码查看。UserMapper.java 文件中写一个接口即可: 99 | ```java 100 | User getUserByName(String username); 101 | ``` 102 | 103 | 中间省略 service 的代码,我们写一个 Controller 来测试一下: 104 | ```java 105 | @RestController 106 | public class TestController { 107 | 108 | @Resource 109 | private UserService userService; 110 | 111 | @RequestMapping("/getUserByName/{name}") 112 | public User getUserByName(@PathVariable String name) { 113 | return userService.getUserByName(name); 114 | } 115 | } 116 | ``` 117 | 启动项目,在浏览器中输入:`http://localhost:8080/getUserByName/CSDN` 即可查询到数据库表中用户名为 CSDN 的用户信息(事先搞两个数据进去即可): 118 | ```json 119 | {"id":2,"username":"CSDN","password":"123456"} 120 | ``` 121 | 这里需要注意一下:Spring Boot 如何知道这个 Mapper 呢?一种方法是在上面的 mapper 层对应的类上面添加 `@Mapper` 注解即可,但是这种方法有个弊端,当我们有很多个 mapper 时,那么每一个类上面都得添加 `@Mapper` 注解。另一种比较简便的方法是在 Spring Boot 启动类上添加`@MaperScan` 注解,来扫描一个包下的所有 mapper。如下: 122 | ```java 123 | @SpringBootApplication 124 | @MapperScan("com.itcodai.course10.dao") 125 | public class Course10Application { 126 | 127 | public static void main(String[] args) { 128 | SpringApplication.run(Course10Application.class, args); 129 | } 130 | } 131 | ``` 132 | 这样的话,`com.itcodai.course10.dao` 包下的所有 mapper 都会被扫描到了。 133 | 134 | ## 4. 基于注解的整合 135 | 136 | 基于注解的整合就不需要 xml 配置文件了,MyBatis 主要提供了 `@Select`, `@Insert`, `@Update`, `Delete` 四个注解。这四个注解是用的非常多的,也很简单,注解后面跟上对应的 sql 语句即可,我们举个例子: 137 | ```java 138 | @Select("select * from user where id = #{id}") 139 | User getUser(Long id); 140 | ``` 141 | 这跟 xml 文件中写 sql 语句是一样的,这样就不需要 xml 文件了,但是有个问题,有人可能会问,如果是两个参数呢?如果是两个参数,我们需要使用 `@Param` 注解来指定每一个参数的对应关系,如下: 142 | ```java 143 | @Select("select * from user where id = #{id} and user_name=#{name}") 144 | User getUserByIdAndName(@Param("id") Long id, @Param("name") String username); 145 | ``` 146 | 可以看出,`@Param` 指定的参数应该要和 sql 中 `#{}` 取的参数名相同,不同则取不到。可以在 controller 中自行测试一下,接口都在源码中,文章中我就不贴测试代码和结果了。 147 | 148 | 有个问题需要注意一下,一般我们在设计表字段后,都会根据自动生成工具生成实体类,这样的话,基本上实体类是能和表字段对应上的,最起码也是驼峰对应的,由于在上面配置文件中开启了驼峰的配置,所以字段都是能对的上的。但是,万一有对不上的呢?我们也有解决办法,使用 `@Results` 注解来解决。 149 | ```java 150 | @Select("select * from user where id = #{id}") 151 | @Results({ 152 | @Result(property = "username", column = "user_name"), 153 | @Result(property = "password", column = "password") 154 | }) 155 | User getUser(Long id); 156 | ``` 157 | `@Results` 中的 `@Result` 注解是用来指定每一个属性和字段的对应关系,这样的话就可以解决上面说的这个问题了。 158 | 159 | 当然了,我们也可以 xml 和注解相结合使用,目前我们实际的项目中也是采用混用的方式,因为有时候 xml 方便,有时候注解方便,比如就上面这个问题来说,如果我们定义了上面的这个 UserMapper.xml,那么我们完全可以使用 `@ResultMap` 注解来替代 `@Results` 注解,如下: 160 | ```java 161 | @Select("select * from user where id = #{id}") 162 | @ResultMap("BaseResultMap") 163 | User getUser(Long id); 164 | ``` 165 | `@ResultMap` 注解中的值从哪来呢?对应的是 UserMapper.xml 文件中定义的 `` 时对应的 id 值: 166 | ```xml 167 | 168 | ``` 169 | 这种 xml 和注解结合着使用的情况也很常见,而且也减少了大量的代码,因为 xml 文件可以使用自动生成工具去生成,也不需要人为手动敲,所以这种使用方式也很常见。 170 | 171 | ## 5. 总结 172 | 173 | 本节课主要系统的讲解了 Spring Boot 集成 MyBatis 的过程,分为基于 xml 形式和基于注解的形式来讲解,通过实际配置手把手讲解了 Spring Boot 中 MyBatis 的使用方式,并针对注解方式,讲解了常见的问题已经解决方式,有很强的实战意义。在实际项目中,建议根据实际情况来确定使用哪种方式,一般 xml 和注解都在用。 174 | 175 | 课程源代码下载地址:[戳我下载](https://gitee.com/eson15/springboot_study) 176 | 177 | ---- 178 | > 文章可以白嫖,公众号不能不关注,手动滑稽🤣🤣   179 | > 180 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**,您的支持,是我们创作的持续动力!   181 | 182 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png) 183 | 184 | ---- -------------------------------------------------------------------------------- /SpringBoot/11. Spring Boot 事务配置管理.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,公众号不能不关注,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**,您的支持,是我们创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png) 6 | ---- 7 | 8 | ## 1. 事务相关 9 | 10 | 场景:我们在开发企业应用时,由于数据操作在顺序执行的过程中,线上可能有各种无法预知的问题,任何一步操作都有可能发生异常,异常则会导致后续的操作无法完成。此时由于业务逻辑并未正确的完成,所以在之前操作过数据库的动作并不可靠,需要在这种情况下进行数据的回滚。 11 | 12 | 事务的作用就是为了保证用户的每一个操作都是可靠的,事务中的每一步操作都必须成功执行,只要有发生异常就回退到事务开始未进行操作的状态。这很好理解,转账、购票等等,必须整个事件流程全部执行完才能人为该事件执行成功,不能转钱转到一半,系统死了,转账人钱没了,收款人钱还没到。 13 | 14 | 事务管理是 Spring Boot 框架中最为常用的功能之一,我们在实际应用开发时,基本上在 service 层处理业务逻辑的时候都要加上事务,当然了,有时候可能由于场景需要,也不用加事务(比如我们就要往一个表里插数据,相互没有影响,插多少是多少,不能因为某个数据挂了,把之前插的全部回滚)。 15 | 16 | ## 2. Spring Boot 事务配置 17 | 18 | ### 2.1 依赖导入 19 | 在 Spring Boot 中使用事务,需要导入 mysql 依赖: 20 | ```xml 21 | 22 | org.mybatis.spring.boot 23 | mybatis-spring-boot-starter 24 | 1.3.2 25 | 26 | ``` 27 | 导入了 mysql 依赖后,Spring Boot 会自动注入 DataSourceTransactionManager,我们不需要任何其他的配置就可以用 `@Transactional` 注解进行事务的使用。关于 mybatis 的配置,在上一节课中已经说明了,这里还是使用上一节课中的 mybatis 配置即可。 28 | 29 | ### 2.2 事务的测试 30 | 31 | 我们首先在数据库表中插入一条数据: 32 | |id|user_name|password| 33 | |:--:|:--:|:--:| 34 | |1|倪升武|123456| 35 | 36 | 然后我们写一个插入的 mapper: 37 | ```java 38 | public interface UserMapper { 39 | 40 | @Insert("insert into user (user_name, password) values (#{username}, #{password})") 41 | Integer insertUser(User user); 42 | } 43 | ``` 44 | OK,接下来我们来测试一下 Spring Boot 中的事务处理,在 service 层,我们手动抛出个异常来模拟实际中出现的异常,然后观察一下事务有没有回滚,如果数据库中没有新的记录,则说明事务回滚成功。 45 | ```java 46 | @Service 47 | public class UserServiceImpl implements UserService { 48 | 49 | @Resource 50 | private UserMapper userMapper; 51 | 52 | @Override 53 | @Transactional 54 | public void isertUser(User user) { 55 | // 插入用户信息 56 | userMapper.insertUser(user); 57 | // 手动抛出异常 58 | throw new RuntimeException(); 59 | } 60 | } 61 | ``` 62 | 我们来测试一下: 63 | ```java 64 | @RestController 65 | public class TestController { 66 | 67 | @Resource 68 | private UserService userService; 69 | 70 | @PostMapping("/adduser") 71 | public String addUser(@RequestBody User user) throws Exception { 72 | if (null != user) { 73 | userService.isertUser(user); 74 | return "success"; 75 | } else { 76 | return "false"; 77 | } 78 | } 79 | } 80 | ``` 81 | 我们使用 postman 调用一下该接口,因为在程序中抛出了个异常,会造成事务回滚,我们刷新一下数据库,并没有增加一条记录,说明事务生效了。事务很简单,我们平时在使用的时候,一般不会有多少问题,但是并不仅仅如此…… 82 | 83 | 84 | ## 3. 常见问题总结 85 | 86 | 从上面的内容中可以看出,Spring Boot 中使用事务非常简单,`@Transactional` 注解即可解决问题,说是这么说,但是在实际项目中,是有很多小坑在等着我们,这些小坑是我们在写代码的时候没有注意到,而且正常情况下不容易发现这些小坑,等项目写大了,某一天突然出问题了,排查问题非常困难,到时候肯定是抓瞎,需要费很大的精力去排查问题。 87 | 88 | 这一小节,我专门针对实际项目中经常出现的,和事务相关的细节做一下总结,希望读者在读完之后,能够落实到自己的项目中,能有所受益。 89 | 90 | ### 3.1 异常并没有被 ”捕获“ 到 91 | 92 | 首先要说的,就是异常并没有被 ”捕获“ 到,导致事务并没有回滚。我们在业务层代码中,也许已经考虑到了异常的存在,或者编辑器已经提示我们需要抛出异常,但是这里面有个需要注意的地方:并不是说我们把异常抛出来了,有异常了事务就会回滚,我们来看一个例子: 93 | ```java 94 | @Service 95 | public class UserServiceImpl implements UserService { 96 | 97 | @Resource 98 | private UserMapper userMapper; 99 | 100 | @Override 101 | @Transactional 102 | public void isertUser2(User user) throws Exception { 103 | // 插入用户信息 104 | userMapper.insertUser(user); 105 | // 手动抛出异常 106 | throw new SQLException("数据库异常"); 107 | } 108 | } 109 | ``` 110 | 我们看上面这个代码,其实并没有什么问题,手动抛出一个 `SQLException` 来模拟实际中操作数据库发生的异常,在这个方法中,既然抛出了异常,那么事务应该回滚,实际却不如此,读者可以使用我源码中 controller 的接口,通过 postman 测试一下,就会发现,仍然是可以插入一条用户数据的。 111 | 112 | 那么问题出在哪呢?因为 Spring Boot 默认的事务规则是遇到运行异常(RuntimeException)和程序错误(Error)才会回滚。比如上面我们的例子中抛出的 RuntimeException 就没有问题,但是抛出 SQLException 就无法回滚了。针对非检测异常,如果要进行事务回滚的话,可以在 `@Transactional` 注解中使用 `rollbackFor` 属性来指定异常,比如 `@Transactional(rollbackFor = Exception.class)`,这样就没有问题了,所以在实际项目中,一定要指定异常。 113 | 114 | ### 3.2 异常被 ”吃“ 掉 115 | 116 | 这个标题很搞笑,异常怎么会被吃掉呢?还是回归到现实项目中去,我们在处理异常时,有两种方式,要么抛出去,让上一层来捕获处理;要么把异常 try catch 掉,在异常出现的地方给处理掉。就因为有这中 try...catch,所以导致异常被 ”吃“ 掉,事务无法回滚。我们还是看上面那个例子,只不过简单修改一下代码: 117 | ```java 118 | @Service 119 | public class UserServiceImpl implements UserService { 120 | 121 | @Resource 122 | private UserMapper userMapper; 123 | 124 | @Override 125 | @Transactional(rollbackFor = Exception.class) 126 | public void isertUser3(User user) { 127 | try { 128 | // 插入用户信息 129 | userMapper.insertUser(user); 130 | // 手动抛出异常 131 | throw new SQLException("数据库异常"); 132 | } catch (Exception e) { 133 | // 异常处理逻辑 134 | } 135 | } 136 | } 137 | ``` 138 | 读者可以使用我源码中 controller 的接口,通过 postman 测试一下,就会发现,仍然是可以插入一条用户数据,说明事务并没有因为抛出异常而回滚。这个细节往往比上面那个坑更难以发现,因为我们的思维很容易导致 try...catch 代码的产生,一旦出现这种问题,往往排查起来比较费劲,所以我们平时在写代码时,一定要多思考,多注意这种细节,尽量避免给自己埋坑。 139 | 140 | 那这种怎么解决呢?直接往上抛,给上一层来处理即可,千万不要在事务中把异常自己 ”吃“ 掉。 141 | 142 | ### 3.3 事务的范围 143 | 144 | 事务范围这个东西比上面两个坑埋的更深!我之所以把这个也写上,是因为这是我之前在实际项目中遇到的,该场景在这个课程中我就不模拟了,我写一个 demo 让大家看一下,把这个坑记住即可,以后在写代码时,遇到并发问题,就会注意这个坑了,那么这节课也就有价值了。 145 | 146 | 我来写个 demo: 147 | ```java 148 | @Service 149 | public class UserServiceImpl implements UserService { 150 | 151 | @Resource 152 | private UserMapper userMapper; 153 | 154 | @Override 155 | @Transactional(rollbackFor = Exception.class) 156 | public synchronized void isertUser4(User user) { 157 | // 实际中的具体业务…… 158 | userMapper.insertUser(user); 159 | } 160 | } 161 | ``` 162 | 可以看到,因为要考虑并发问题,我在业务层代码的方法上加了个 synchronized 关键字。我举个实际的场景,比如一个数据库中,针对某个用户,只有一条记录,下一个插入动作过来,会先判断该数据库中有没有相同的用户,如果有就不插入,就更新,没有才插入,所以理论上,数据库中永远就一条同一用户信息,不会出现同一数据库中插入了两条相同用户的信息。 163 | 164 | 但是在压测时,就会出现上面的问题,数据库中确实有两条同一用户的信息,分析其原因,在于事务的范围和锁的范围问题。 165 | 166 | 从上面方法中可以看到,方法上是加了事务的,那么也就是说,在执行该方法开始时,事务启动,执行完了后,事务关闭。但是 synchronized 没有起作用,其实根本原因是因为事务的范围比锁的范围大。也就是说,在加锁的那部分代码执行完之后,锁释放掉了,但是事务还没结束,此时另一个线程进来了,事务没结束的话,第二个线程进来时,数据库的状态和第一个线程刚进来是一样的。即由于mysql Innodb引擎的默认隔离级别是可重复读(在同一个事务里,SELECT的结果是事务开始时时间点的状态),线程二事务开始的时候,线程一还没提交完成,导致读取的数据还没更新。第二个线程也做了插入动作,导致了脏数据。 167 | 168 | 这个问题可以避免,第一,把事务去掉即可(不推荐);第二,在调用该 service 的地方加锁,保证锁的范围比事务的范围大即可。 169 | 170 | ## 4. 总结 171 | 172 | 本章主要总结了 Spring Boot 中如何使用事务,只要使用 `@Transactional` 注解即可使用,非常简单方便。除此之外,重点总结了三个在实际项目中可能遇到的坑点,这非常有意义,因为事务这东西不出问题还好,出了问题比较难以排查,所以总结的这三点注意事项,希望能帮助到开发中的朋友。 173 | 174 | 课程源代码下载地址:[戳我下载](https://gitee.com/eson15/springboot_study) 175 | 176 | ---- 177 | 178 | > 文章可以白嫖,公众号不能不关注,手动滑稽🤣🤣   179 | > 180 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**,您的支持,是我们创作的持续动力!   181 | 182 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png) -------------------------------------------------------------------------------- /SpringMVC/01. 宏观上把握 SpringMVC 框架.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,公众号不能不关注,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**,您的支持,是我们创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | ---- 8 | 9 | springmvc 是一个基于mvc的web框架,是 spring 框架的一个模块,所以springmvc和spring无需通过中间整合层进行整合。我们先来看下spring的一个架构模型,看springmvc在spring框架中所处的位置: 10 | 11 | ![spring框架](https://img-blog.csdnimg.cn/img_convert/c4ea0218fab77b36718c760d15c5e097.png) 12 | 13 | 从图中可以看出,springmvc是spring的一个web框架,所以上图中描述的是spring web mvc,它和struts2的功能差不多,下面我们来深入到springmvc内部,看它的干了些啥,先看一个图: 14 | 15 | ![springmvc执行流程](https://img-blog.csdnimg.cn/img_convert/de6d2b213f112297298f3e223bf08f28.png) 16 | 17 | 这个图描述了springmvc的整个执行的流程,乍一看有点晕乎,待我一步步分析,最后弄个流程图出来就明白了。 18 | 19 | 结合上图,我描述一下springmvc的执行流程: 20 | >1. 向服务器发送Http request请求,请求被**前端控制器(DispatcherServlet)**捕获。 21 | >2. 前端控制器根据xml文件中的配置(或者注解)对请求的URL进行解析,得到请求资源标识符(URI)。然后根据该URI,调用**处理器映射器(HandlerMapping)**获得处理该请求的Handler以及Handler对应的拦截器,最后以 HandlerExecutionChain 对象的形式返回。 22 | >3. 前端控制器根据获得的Handler,选择一个合适的**处理器适配器(HandlerAdapter)**去执行该Handler。 23 | >4. 处理器适配器提取request中的模型数据,填充Handler入参,执行**处理器(Handler)**(也称之为Controller). 24 | >5. Handler(Controller)执行完成后,向处理器适配器返回一个**ModelAndView**对象,处理器适配器再向前端控制器返回该ModelAndView对象(ModelAndView只是一个逻辑视图)。 25 | >6. 根据返回的ModelAndView,前端控制器请求一个适合的**视图解析器(ViewResolver)**(必须是已经注册到Spring容器中的ViewResolver)去进行视图解析,然后视图解析器向前端控制器返回一个真正的视图View(html或者jsp)。 26 | >7. 前端控制器通过Model解析出ModelAndView中的参数进行解析,最终展现出完整的View并通过Http response返回给客户端。 27 | 28 | 上面描述了一下springmvc的执行流程,如果还是有点模糊的话,我用下面这个流程图来表示一下,也为了自己更好的理解整个执行流程: 29 | 30 | ```mermaid 31 | sequenceDiagram 32 | 用户->>DispatcherServlet:Http request请求 33 | Note right of DispatcherServlet:读取配置文件或注解 34 | Note right of DispatcherServlet:解析请求的url 35 | DispatcherServlet->>HandlerMapping:根据解析结果调用处理器映射器 36 | HandlerMapping->>DispatcherServlet:返回请求对应的Handler 37 | DispatcherServlet->>HandlerAdapter:选择一个合适的处理器适配器 38 | Note right of HandlerAdapter:提取request中的模型数据 39 | Note right of HandlerAdapter:填充Handler入参 40 | HandlerAdapter->>Handler:执行 41 | Handler->>HandlerAdapter:返回ModelAndView 42 | HandlerAdapter->>DispatcherServlet:返回ModelAndView 43 | DispatcherServlet->>ViewResolver:选择一个合适的视图解析器 44 | Note right of ViewResolver:解析ModelAndView 45 | ViewResolver->>DispatcherServlet:返回真正的视图View 46 | Note right of DispatcherServlet:通过Model解析出ModelAndView中的参数 47 | Note right of DispatcherServlet:解析参数,展现完整的view 48 | DispatcherServlet->>用户:Http response返回 49 | ``` 50 | 51 | 经过这么一分析,现在对springmvc的执行流程有了宏观上的了解了,从上面的分析可以看出,springmvc有几个主要的组件,下面结合我们编程,来分析一下这几个组件: 52 | >1. 前端控制器DispatcherServlet(**不需要程序员开发**)。 53 | 作用:接收请求,响应结果,相当于转发器,中央处理器。有了DispatcherServlet减少了其它组件之间的耦合度。 54 | 2. 处理器映射器HandlerMapping(**不需要程序员开发**)。 55 | 作用:根据请求的url查找Handler。 56 | >3. 处理器适配器HandlerAdapter(**不需要程序员开发**)。 57 | 作用:按照特定规则(HandlerAdapter要求的规则)去执行Handler。 58 | >4. 处理器Handler(**需要程序员开发**)。 59 | 注意:编写Handler时按照HandlerAdapter的要求去做,这样适配器才可以去正确执行Handler 60 | >5. 视图解析器ViewResolver(**不需要程序员开发**)。 61 | 作用:进行视图解析,根据逻辑视图名解析成真正的视图(view) 62 | >6. 视图View(**需要程序员开发jsp**)。 63 | View是一个接口,实现类支持不同的View类型(jsp、thymeleaf、pdf...) 64 | **【注】**:不需要程序员开发的,需要程序员自己做一下配置即可。 65 | 66 | 现在看来,其实真正需要程序员开发的就两大块:一个是Handler,一个是页面,这样的话,其实和struts2就差不多了,所以不要被上面那个一系列的流程给绕傻了。 67 | 68 | 整个springmvc的架构就总结到这吧,后面就要开始走进springmvc的世界了。 69 | 70 | ------ 71 | 72 | > 文章可以白嫖,公众号不能不关注,手动滑稽🤣🤣   73 | > 74 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**,您的支持,是我们创作的持续动力!   75 | 76 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 77 | 78 | --- 79 | 80 | -------------------------------------------------------------------------------- /SpringSecurity/01. 概述:SpringSecurity的前世今生.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | ---- 8 | 9 | 本篇文章为 ```Spring Security``` 系列文章的概述篇,主要介绍了```Spring Security```的前世今生、提供的主要功能、以及其实现原理的粗略探讨。 10 | 11 | ![过滤器链](https://img-blog.csdnimg.cn/20200913225028240.png) 12 | 13 | ## 前世今生 14 | 15 | Spring Security的前身并非称呼为Spring Security,而是叫Acegi Security;但这并不意味着它与Spring毫无关系,它仍然是为Spring提供安全支持的。 16 | 17 | Acegi Security搭上了Spring的便车,摇身一变成为Spring Security,但即便如此其还是继承了Acegi Security的臃肿繁琐的配置,学习成本相对还是十分的高。 18 | 19 | 直到有一天,Spring Boot横空出世,提出约定优于配置等理念,极大的简化了繁琐的配置;Spring Security也收益于此,一飞冲天。 20 | 21 | ## 功能介绍 22 | 23 | 安全这个话题绕不开 ```认证``` 和 ```授权``` 这两个方面,Spring Security也是如此,其作为一个权限管理框架,最核心的功能有: 24 | 25 | - 认证(你是谁?) 26 | 27 | - 授权(你能做什么?) 28 | 29 | - 攻击防护(防止伪造身份) 30 | 31 | Spring Security提供了很多的认证和授权方案,但作为一个优秀的开放框架,它的优点更在于“扩展性”,当其提供的认证或授权方案无法满足我们的需求时,我们可以自定义认证或授权逻辑。例如后续会讲解到的《认证(三):验证码认证登录模式》。 32 | 33 | ## 实现原理 34 | 35 | Spring Security底层是通过一组过滤器链来实现的,过滤器链上的每个 ```Filter``` 各司其职,但同时又相互关联;因此了解 ```Filter``` 的顺序就显得十分的重要,下面会介绍几个比较核心的```Filter```。 36 | 37 | ![过滤器链](https://note.youdao.com/yws/api/personal/file/7831A84E495D411DB71F97709042E35D?method=download&shareKey=85256209e68294593a6376aa52d2429a) 38 | 39 | 如图所示,一个请求在到达API之前会经过Spring Security的过滤器链进行校验,只有"合法规范"的请求才能被API所接收。 40 | 41 | 荧光绿的都属认证过滤器,我们可以配置其是否生效,也可以自定义认证过滤器添加到过滤器链上,可控性相对比较高;其他的深蓝色```ExceptionTranslationFilter```和橘色```FilterSecurityinterecptor```则没法随意的控制。 42 | 43 | 备注:为避免引入过多的概念,该过滤器链并非完整的链路图,只是截图了部分过滤器。 44 | 45 | ### 表单登录过滤器 46 | 47 | ```UsernamePasswordAuthenticationFilter``` 是表单登录过滤器,表单登录也是我们最常见的登录方式;对于表单登录过滤器来说,它的主要职责为: 48 | 49 | - 检查请求是否为登录请求&检查请求中是否含有自身需要的认证信息(username、password)。 50 | 51 | - 如果不满足则放行让下一个过滤器进行校验,如符合自身认证要求则进行认证。 52 | 53 | ### Basic认证过滤器 54 | 55 | ```BasicAuthenticationFilter``` 过滤器是用来处理```Http```请求中的```Basic Authorization```头部;当一个 ```Http```请求头(```Reqeust Header```)中包含```Authorization```,并且其值为 ```Basic xxx```的时候,```BasicAuthenticationFilter```就会生效。 56 | 57 | 主要职责: 58 | 59 | - 尝试解析 ```Http basic authorization```获取相应的认证信息(username、password)。 60 | 61 | - 如果不满足则放行让下一个过滤器进行校验,如符合则自身进行认证。 62 | 63 | ### 异常转换过滤器 64 | 65 | ```ExceptionTranslationFilter``` 是一个异常过滤器,用来处理 ```认证``` 或 ```授权```过程中抛出的异常,这个很好理解;正如我们自身项目会有异常处理一般,```Spring Security```也有对应的异常处理机制。 66 | 67 | 该过滤器的主要职责是对在 ```FilterChain``` 范围内抛出的```AccessDeniedException(授权异常基类)``` 和 ```AuthenticationException(认证异常基类)```,转换为对应的```HTTP```错误码返回或者返回对应的页面。 68 | 69 | ### FilterSecurityInterceptor过滤器 70 | 71 | ```FilterSecurityInterceptor```过滤器位于过滤器链上的最后一环,其主要负责的是:权限的校验;判断请求的用户是否有权限访问该API。保护web Uri 并且在访问被拒绝时抛出异常。 72 | 73 | ## 总结 74 | 75 | 本篇文章为 ```Spring Security``` 系列文章的概述篇,主要介绍了```Spring Security```的前世今生、提供的主要功能、以及其实现原理的粗略探讨。后面我继续跟大家深入讲解。 76 | 77 | ---- 78 | 79 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   80 | > 81 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   82 | 83 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 84 | 85 | ---- -------------------------------------------------------------------------------- /SpringSecurity/07. 扩展篇:Spring Security 过滤器链之 SecurityContext.md: -------------------------------------------------------------------------------- 1 | 2 | # 扩展篇:再探Spring Security过滤器链 3 | 4 | ![图1-1 思维导图](https://img-blog.csdnimg.cn/20201025234926937.png?x-oss-process=image) 5 | 6 | ## 再探过滤器链 7 | 8 | 在 (todo:换链接)[概述(一):SpringSecurity的前世今生](https://blog.csdn.net/weixin_46920376/article/details/108549688) 中我们曾提过Spring Security底层是通过一组过滤器链来实现的,当时简单的介绍了几个比较重要的过滤器;并且在后面的几篇认证文章中都有比较详细的讲解。 9 | 10 | 但前几篇的系列文章更多的是关注 ```认证```本身,对一些细枝末节没有额外的展开;而本篇文章的目的是再探 Spring Security 的过滤器链,完整的剖析全链路。 11 | 12 | 经过 (todo:换链接) [认证(一):基于表单登录的认证模式](https://blog.csdn.net/weixin_46920376/article/details/108561551) 的介绍,我们对认证的流程有了一个比较全面的认识;但是对于认证之后的事情呢?比如说:为什么我登录之后就不用再登录了呢?登录成功之后用户信息保存在哪里呢…… 别急,让我们先来看看完整的过滤器链以及认证链路全流程。 13 | 14 | ![图1-2 过滤器链](https://img-blog.csdnimg.cn/20201024151916654.png?x-oss-process=image) 15 | 16 | 从过滤器链图中我们发现,当我们在请求资源的时候,请求会先经过 ```SecurityContextPersistenceFilter```过滤器,从其名字中我们大概能猜测出它应该是与某些资源的持久化(Persistence)有关。 17 | 18 | ![图1-3认证链路全流程](https://img-blog.csdnimg.cn/20201024154742223.png?x-oss-process=image) 19 | 20 | 认证链路全流程图大致上可以分两部分来理解,一部分是荧光绿的认证流程(在之前的认证系列中已经有过比较详细的将讲解);而另一部分是认证后的响应流程。 21 | 22 | ## 揭秘之旅 23 | 24 | 结合两张图示来看,未知的接口/类主要有:```Authentication```、```SecurityContext```、 ```SecurityContextHolder```,```SecurityContextPersistenceFilter```、 那么就让我们一起来揭开它们神秘的面纱吧! 25 | 26 | ### Authenticaiton 27 | 28 | ```Authentication``` 接口之前曾有过介绍,在认证成功之前存储的是客户端传递的登录信息,认证成功之后存储的是用户信息。 29 | 30 | ```java 31 | public interface Authentication extends Principal, Serializable { 32 | /** 33 | * 权限相关信息 34 | * 因SecurityContext存储的Authentication对象的经过认证的,所以会带有权限信息 35 | */ 36 | Collection getAuthorities(); 37 | 38 | Object getCredentials(); 39 | 40 | Object getDetails(); 41 | 42 | Object getPrincipal(); 43 | 44 | /** 45 | * 是否认证 46 | * 认证前存储的是客户端传递的登录信息,认证后存储的是用户信息 47 | */ 48 | boolean isAuthenticated(); 49 | 50 | void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; 51 | } 52 | ``` 53 | 54 | ### SecurityContext 55 | 56 | ```SecurityContext``` 用以存储认证授权的相关信息(已认证的Authentication),换句话说就是存储 ```当前用户```的账号信息以及相关权限信息。 57 | 58 | ```java 59 | public interface SecurityContext extends Serializable { 60 | 61 | Authentication getAuthentication(); 62 | 63 | void setAuthentication(Authentication authentication); 64 | } 65 | ``` 66 | 67 | ### SecurityContextHolder 68 | 69 | 前面讲到 ```SecurityContext``` 存储的是 ```当前用户``` 的相关信息,那么如何判定```当前用户``` 是谁呢?实际上一个请求从开发到结束一般会是一个线程来处理,所以这段时间内这个 ```当前用户``` 是和当前的处理线程是一一对应的。 ```SecurityContextHolder``` 类的作用就是将 ```SecurityContext``` 和当前线程所绑定,将其存储到当前线程中(实现方式是利用 ```ThreadLocal```)。 70 | 71 | ```SecurityContextHolder``` 主要是用于框架内部使用,比如说利用它获取当前用户的 ```SecurityContext``` 进行请求检查以及访问控制等。 72 | 73 | 这时候可能有朋友会有疑问:“既然是与当前线程所绑定,那么当一个请求结束后,用户信息不就消失了吗?那不是每次都会需要重新认证?” 74 | 75 | 答案在 ```SecurityContextPersistenceFilter``` 中。 76 | 77 | ### SecurityContextPersistenceFilter 78 | 79 | ```SecurityContextPersistenceFilter``` 是过滤器链中的第一个过滤器,它的主要工作是:当请求来临的时候,它会从 ```HttpSession``` 中把对应用户的 ```SecurityContext``` 放入到 ```SecurityContextHolder``` 并且在所有拦截器都处理完毕之后,将含有当前用户信息的 ```SecurityContext``` 存入 ```HttpSession``` 中,并清除 ```SecurityContextHolder``` 中的引用(释放 ```ThreadLoacal```)。 80 | 81 | ```java 82 | public class SecurityContextPersistenceFilter extends GenericFilterBean { 83 | static final String FILTER_APPLIED = "__spring_security_scpf_applied"; 84 | private SecurityContextRepository repo; 85 | private boolean forceEagerSessionCreation; 86 | 87 | public SecurityContextPersistenceFilter() { 88 | this(new HttpSessionSecurityContextRepository()); 89 | } 90 | 91 | public SecurityContextPersistenceFilter(SecurityContextRepository repo) { 92 | this.forceEagerSessionCreation = false; 93 | this.repo = repo; 94 | } 95 | 96 | public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { 97 | HttpServletRequest request = (HttpServletRequest)req; 98 | HttpServletResponse response = (HttpServletResponse)res; 99 | if (request.getAttribute("__spring_security_scpf_applied") != null) { 100 | chain.doFilter(request, response); 101 | } else { 102 | boolean debug = this.logger.isDebugEnabled(); 103 | request.setAttribute("__spring_security_scpf_applied", Boolean.TRUE); 104 | if (this.forceEagerSessionCreation) { 105 | HttpSession session = request.getSession(); 106 | if (debug && session.isNew()) { 107 | this.logger.debug("Eagerly created session: " + session.getId()); 108 | } 109 | } 110 | 111 | HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response); 112 | // 从HttpSessionSecurityContextRepository中尝试获取SecurityContext对象,如果是还未登录的用户,则返回的是一个空的SecurityContext对象 113 | SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder); 114 | boolean var13 = false; 115 | 116 | try { 117 | var13 = true; 118 | // 将securityContext设置到 SecurityContextHolder中 119 | SecurityContextHolder.setContext(contextBeforeChainExecution); 120 | // 执行后续的拦截器链 121 | chain.doFilter(holder.getRequest(), holder.getResponse()); 122 | var13 = false; 123 | } finally { 124 | if (var13) { 125 | // 当后续的拦截器链执行完毕后,从SecurityContextHolder中取出 securityContext并做如下事情 126 | SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext(); 127 | // 1.回收SecurityContextHolder中的 securityContext引用(相当于释放ThreadLocal) 128 | SecurityContextHolder.clearContext(); 129 | // 2.将 securityContext信息存储到HttpSession中 130 | this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse()); 131 | request.removeAttribute("__spring_security_scpf_applied"); 132 | if (debug) { 133 | this.logger.debug("SecurityContextHolder now cleared, as request processing completed"); 134 | } 135 | 136 | } 137 | } 138 | 139 | SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext(); 140 | SecurityContextHolder.clearContext(); 141 | this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse()); 142 | request.removeAttribute("__spring_security_scpf_applied"); 143 | if (debug) { 144 | this.logger.debug("SecurityContextHolder now cleared, as request processing completed"); 145 | } 146 | 147 | } 148 | } 149 | 150 | public void setForceEagerSessionCreation(boolean forceEagerSessionCreation) { 151 | this.forceEagerSessionCreation = forceEagerSessionCreation; 152 | } 153 | } 154 | 155 | ``` 156 | 157 | ## 总结 158 | 159 | 至此,我们总算对认证的全链路有了一个比较完整的认识:当请求到达服务端,首先会经过 ```SecurityContextPersistenceFilter``` 拦截器,它会尝试从 ```HttpSession``` 中获取安全上下文 ```SecurityContext``` 并且将其设置到 ```SecurityContextHolder``` 中,然后等过滤器链上的所有过滤器都执行完后,再将 ```SecurityContextHolder``` 中的 安全上下文 ```SecurityContext``` 清除(请求走到了最后阶段),并且将 ```SecurityContext``` 设置到 ```HttpSession``` 中。 160 | 161 | ```HttpSession``` 存在于服务端,默认的过期时间是30分钟(当然这些都是可以设置的),有兴趣的朋友可以自行了解一番~ 162 | 163 | 本文为个人学习总结,如有错误之处请指出! 164 | 165 | 参考:[Spring Security 之 SecurityContext](https://www.cnblogs.com/longfurcat/p/10293819.html) 166 | -------------------------------------------------------------------------------- /mq/01. 快速了解 RabbitMQ .md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | ---- 8 | 9 | 10 | 11 | 12 | ## RabbitMQ是什么? 13 | 14 | RabbitMQ 是当前热门的消息队列中间件之一。 15 | 16 | ## RabbitMQ主要特点 17 | 18 | 根据定义(Message Queue)可知它是个具有**先进先出**特点的消息存储中间件,可以理解为存储消息的容器。生产者只需将消息放入该容器中,而无需关心消费者如何处理消息。同时消息可以由多个生产者存入中间件,也可以由多个消费者去获取并处理。同时生产者并不知道消费者何时处理消息,因此它还具有异步的特性。 19 | 20 | 总结一下主要特点: 21 | 22 | - 阻塞&有序:通过先进先出维护一个阻塞的有序队列 23 | - 中间件容器:在分布式环境下使用,用于多应用间通信 24 | - 异步:生产者将消息丢入容器即可,不再关心消费者何时处理 25 | 26 | ## 使用RabbitMQ主要目的 27 | 28 | 通过上述特点,便可以理解MQ常见的使用目的: 29 | 30 | - **解耦:**维护了生产者-> 中间件 和 中间件->消费者的关系,让生产者和消费者没有直接的调用关系,仅需关心各自的发/收的数据结构即可,达到程序解耦的目的。 31 | - **削峰:**当生产者收到大量请求时,生产消息丢给mq无需立即处理(立刻响应,慢慢处理)。且可以增加消费者数量,来分摊处理生产者生产的的消息。通过异步和负载均衡分流思路,来解决生产者需要及时处理大量请求的问题,以达到削峰的目的。 32 | 33 | ### 解耦例子: 34 | 35 | 如果某个功能需要A B C 三个步骤,并且得依次执行,并且 A B C 都要执行很久。正常情况下,我们需要判断A是否执行完,然后执行B,接着判断B是否执行完,然后再执行C。 36 | 37 | 如果用了MQ,那么A只需要立刻返回,告诉用户请求成功,处理完成后丢一个消息给MQ,那么B就可以根据MQ中消息执行下一步,同样,B执行完发送处理结果给MQ就行,C自然能拿到消息并继续执行自己的业务处理。如果中途B失败了,C自然不会收到消息,也就不会执行了。这样B不需要关心A是否成功,C也不需要关心B是否成功,只要判断MQ是否有消息,消息数据是否满足业务需要即可。 38 | 39 | 由于RabbitMQ 还具备有序、消息通信等特点,因此还可以用来解决一些业务处理顺序的问题,也可以作为分布式环境的数据传输/存储中间件,但是这么用的话,就会显得比较"骚"了,因为想要使用这些特点时候,都有更好的中间件产品可以选择。因此在使用MQ或者其他技术的时候,都应该尽量避免这种"骚操作",当然特殊问题特殊看待,在一些特定场景下能解决问题,"骚一下"也是情有可原的。 40 | 41 | 42 | 43 | 说到这可能会存在一些疑问: 44 | 45 | - 如果是阻塞队列,我为什么不用Java自带的BlockingQueue?为什么要用RabbitMQ? 46 | 47 | - 如果是为了异步,我为什么不直接`new Thread(()->{}).start()`? 48 | 49 | - 如果是为了通信,为什么不直接使用Http的工具,直接发HTTP请求? 50 | 51 | 这些疑问都没错。如果只是为了通过阻塞队列控制代码执行顺序,用BlockingQueue,这当然没问题,也最简单。 52 | 53 | 如果在单一场景下,仅仅为了异步,且系统本身不存在性能瓶颈,使用线程/线程池也没有任何问题。 54 | 55 | 如果仅仅为了通信,发一个json 字符串 或者其它字节流等信息,也确实没有必要引入RabbitMQ增加复杂度。 56 | 57 | 因此,我们使用mq 除了上述的解耦,削峰等主要目的外,还为了使用它异步,阻塞,通信,存储消息,灵活的生产/消费规则,而这些特点想要同时使用,让消息实现方便的网络传输、可靠的存储、灵活的投递,那我们用MQ自然是更合适,而不需要自己大量编码来维护消息队列的这些特性。 58 | 59 | 60 | 61 | ## RabbitMQ使用场景 62 | 63 | 上面分析了那么多概念,现在你能知道什么时候使用mq了吗? 64 | 65 | 下面举一个常见的场景方便大家理解: 66 | 67 | 系统需要做秒杀活动,当用户抢购商品时需要锁定库存,扣库存,生成订单。 68 | 69 | 如果立即处理这么多操作,系统肯定会很慢,而且请求量巨大的话,系统就会面临崩溃风险。这时候我们可以通过如下流程立即响应用户: 70 | 71 | 1. 在秒杀的系统中通过特定机制告诉用户抢购成功, 72 | 2. 将锁定库存消息发送到MQ,库存系统进行锁定和扣库存的操作,然后发送消息到MQ。 73 | 3. 订单系统发现扣库存操作成功,进行订单创建操作,完成后发送消息到MQ 74 | 75 | ![rabbitMQ秒杀场景](https://img-blog.csdnimg.cn/2020120213154150.png) 76 | 77 | 78 | 79 | 80 | 81 | 这里,用户在发起请求后,立刻就能知道自己是否抢购成功。我们通过引入MQ,将锁库存,生成订单,通知用户支付等流程异步执行。减少了大量用户请求时的压力。同时,可以通过增加库存系统,订单系统,支付系统来增加后续业务的处理能力。 82 | 83 | 84 | 85 | ## 使用RabbitMQ带来的问题 86 | 87 | 上面其实已经提到了一些MQ带来的问题: 88 | 89 | - 如果系统简单,引入mq会增加系统复杂度 90 | - 由于mq独立部署,在网络不稳定情况下,增加了系统故障的风险,因为业务流程依赖mq进行通信协作,一旦mq无法连接,将导致系统无法正常处理业务。 91 | 92 | 93 | 94 | ## 总结 95 | 96 | 任何技术都不能做到简单而全面,也无法做到有利无弊,因此在使用技术的时候需要考虑引入技术优势的同时带来的负面因素,后面会讲到一些关于mq使用中常见的问题,如:消息可靠性投递,以便更好地使用mq。 97 | 98 | ---- 99 | 100 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   101 | > 102 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   103 | 104 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) -------------------------------------------------------------------------------- /mq/04. RabbitMQ 高级用法.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | ---- 8 | 9 | 10 | 11 | 前面已经讲了如何在不同环境下集成rabbitMQ。这节基于springboot讲一下rabbitMQ中常见的高级用法,主要内容如下: 12 | 13 | - 交换机类型 14 | - 消息存活时间 15 | - 死信交换机/死信队列 16 | - 延时队列 17 | 18 | ## 交换机类型 19 | 20 | **Direct(直连交换机)** 21 | 交换机通过binding Key将消息精确投递至指定名称的队列 22 | 23 | 24 | 25 | **Topic(主题交换机)** 26 | 交换机通过两种通配符(* #)将消息投递至与bindingKey匹配的队列。 27 | 28 | 【 **.** 】用来分割多个词,"词"可以是不含特殊字符的任意字符串 29 | 30 | 【*】 匹配一个词 31 | 32 | 【#】 匹配0或多个词 33 | 34 | 35 | 36 | **Fanout(广播交换机)** 37 | 38 | 队列与该交换机绑定时,无需指定binding key,该交换机收到的消息将发送给所有与之绑定的队列。 39 | 40 | **Headers** 41 | 42 | 该类型交换机性能很差,很少使用,不多做介绍。 43 | 44 | ## 消息存活时间 Time To Live(TTL) 45 | 46 | 设置消息TTL有两种方式: 47 | 48 | - 设置队列TTL,发送至该队列的消息超过TTL时间无人消费会被删除 49 | - 单独设置消息TTL 50 | 51 | 如果同时设置了队列和消息的TTL 则时间短的生效(同时生效,时间短的先执行) 52 | 53 | ```java 54 | @Bean("ttlQueue") 55 | public Queue queue() { 56 | Map msgConfig = new HashMap(); 57 | // 设置队列TTL,该队列中的消息10s内未被消费,则会删除 58 | msgConfig.put("x-message-ttl", 10000); 59 | return new Queue("TTL_QUEUE", true, false, false, msgConfig); 60 | } 61 | 62 | 63 | //指定消息的TTL 64 | @Autowired 65 | private AmqpTemplate amqpTemplate; 66 | public void sendMqMsg() { 67 | MessageProperties messageProperties = new MessageProperties(); 68 | // 消息的过期时间,单位ms 69 | messageProperties.setExpiration("5000"); 70 | Message ttlMsg = new Message("这条消息5s后自动删除".getBytes(), messageProperties); 71 | amqpTemplate.send("TTL_EXCHANGE", "ttl.routingkey", ttlMsg); 72 | } 73 | ``` 74 | 75 | 76 | 77 | ## 死信队列 78 | 79 | 消息无法正常投递/消费则会进入死信队列,进入死信队列有如下几个场景: 80 | 81 | - 消息被消费者拒绝或未收到消费响应(ack),且未重新回到队列:(Reject || NotACK ) && !requeue 82 | 83 | - 消息过期(超过TTL时间未消费) 84 | - 消息超过目标队列数限制或消息长度超过队列配置的限制 85 | 86 | 87 | 88 | ### 死信队列用法 89 | 90 | - 使消息没有消费者而过期。 91 | 92 | ```java 93 | //交换机 94 | @Bean("someExchange") 95 | public DirectExchange exchange() { 96 | //这里是direct交换机 97 | return new DirectExchange("SOME_EXCHANGE", true, false, new HashMap<>()); 98 | } 99 | 100 | //队列 101 | @Bean("someQueue") 102 | public Queue queue() { 103 | Map map = new HashMap(); 104 | // 消息5s后成为死信 105 | map.put("x-message-ttl", 5000); 106 | // 指定死信交换机,队列中的消息变成死信后,进入死信交换机 107 | map.put("x-dead-letter-exchange", "DEAD_LETTER_EXCHANGE"); 108 | 109 | return new Queue("SOME_EXCHANGE", true, false, false, map); 110 | } 111 | 112 | //队列绑定至交换机 113 | @Bean 114 | public Binding binding(@Qualifier("someQueue") Queue queue, 115 | @Qualifier("someExchange") DirectExchange exchange) { 116 | //没有消费者的消息 117 | return BindingBuilder.bind(queue).to(exchange).with("noConsumer.routingKey"); 118 | } 119 | ``` 120 | 121 | - 直接声明死信队列和死信交换机,并绑定 122 | 123 | ```java 124 | @Bean("deadLetterExchange") 125 | public TopicExchange deadLetterExchange() { 126 | //这里是topic交换机 127 | return new TopicExchange("DEAD_LETTER_EXCHANGE", true, false, new HashMap<>()); 128 | } 129 | 130 | @Bean("deadLetterQueue") 131 | public Queue deadLetterQueue() { 132 | return new Queue("DEAD_LETTER_QUEUE", true, false, false, new HashMap<>()); 133 | } 134 | 135 | @Bean 136 | public Binding bindingDead(@Qualifier("deadLetterQueue") Queue queue, 137 | @Qualifier("deadLetterExchange")TopicExchange exchange) { 138 | // 无条件路由 139 | return BindingBuilder.bind(queue).to(exchange).with("#"); 140 | } 141 | ``` 142 | 143 | 这样消息就进入了死信队列,后续业务直接从死信队列取消息,处理“死信情况下的业务”即可 144 | 145 | 146 | 147 | ## 延时队列 148 | 149 | RabbitMQ并未直接提供延时队列功能,基于TTL 和死信队列的特点,rabbitMQ常被设计用作延时队列。即,处理一些“到特定时间执行”的业务。 150 | 151 | 常见的例子如: 152 | 153 | - 订单超时未支付自动取消 154 | - 淘宝发货后15天自动确认 155 | - 定时任务,到指定时间启动 156 | - 其它定时/延时执行的业务... ... 157 | 158 | 159 | 160 | 对于定时任务场景,常见做法如下: 161 | 162 | - 开线程不停扫描数据库或缓存,监听数据状态,发现数据改变时处理特定业务。 163 | - 通过消息TTL使得消息超时,进入死信队列,再监听死信队列进行处理 164 | - 利用rabbitmq-delayed-message-exchange 插件实现 165 | 166 | 167 | 168 | 这里主要讲一下TTL+DLX的延时队列,该方式实现死信队列有如下缺点: 169 | 170 | - 定的时间不会很准时,存在延迟执行 171 | - TTL只能对队列/消息进行设置,如果时间梯度多(每个业务具有不同的时间要求),设置太多消息的TTL,则可能导致消息阻塞;设置队列TTL,则需要创建很多队列(和交换机) ; 172 | 173 | ---- 174 | 175 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   176 | > 177 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   178 | 179 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 180 | 181 | -------------------------------------------------------------------------------- /mq/05. 一起来了解下 SpringAmqp.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | 8 | 9 | ---- 10 | 11 | 本节主要介绍spring amqp的使用及常见api的封装 12 | 13 | ## 前言 14 | 15 | 说到spring amqp 先看下spring amqp的依赖 16 | 17 | image-20201105005201304 18 | 19 | 可以发现,排除了context 和tx事务包以外,主要有client 和 amqp与rabbit mq相关。 20 | 21 | 其实amqp应该先讲,这样大家就会了解rabbitMQ的协议,了解mq的诞生和发展。 22 | 23 | 而我这里放在这么多节之后来讲,是防止在刚开始讲一些枯燥概念导致大家云里雾里效果不好。 24 | 25 | 26 | 27 | 现在我们有了前面几节的铺垫,大家对rabbitMQ api的基本使用,spring的集成 spring boot的集成 有了大致印象,虽然可能还是没有特别理解,不过这时候再讲amqp协议,大家不会“一脸懵逼”了。 28 | 29 | 30 | 31 | ## AMQP协议 32 | 33 | 引用`RabbitMQ实战指南`的介绍 34 | 35 | > AMQP 协议本身包括三层。 36 | > 37 | > - Module Layer: 位于协议最高层,主要定义了一些供客户端调用的命令,客户端可以利用这些命令实现自己的业务逻辑。例如,客户端可以使用Queue . Declare 命令声明一个队列或者使用Basic.Consume 订阅消费一个队列中的消息。 38 | > 39 | > - Session Layer: 位于中间层,主要负责将客户端的命令发送给服务器,再将服务端的应答返回给客户端,主要为客户端与服务器之间的通信提供可靠性同步机制和错误处理。 40 | > 41 | > - Transport Layer: 位于最底层,主要传输二进制数据流,提供帧的处理、信道复用、错误检测和数据表示等。 42 | > 43 | > AMQP 说到底还是一个通信协议,通信协议都会涉及报文交互,从low-level 举例来说,AMQP 本身是应用层的协议,其填充于TCP 协议层的数据部分。而从high-level 来说, AMQP是通过协议命令进行交互的。AMQP 协议可以看作一系列结构化命令的集合,这里的命令代表一种操作,类似于HTTP 中的方法(GET 、POST 、PUT 、DELETE 等) 。 44 | 45 | rabbitMQ 不仅支持AMQP协议,还支持STOMP 和 MQTT协议(这个在一些面试中,面试官考知识面广度可能会提到作为加分项) 46 | 47 | 说到这里还是很抽象,但是提到 `交换器、交换器类型、队列、绑定、路由键`这些前面demo中提到的概念,大家应该不陌生了,这些概念就是AMQP中的概念,也是大佬们为了优化消息通信过程而提出的一些模型概念。 48 | 49 | AMQP的主要模型架构就是 50 | 51 | > 生产者将消息发送给交换器,交换器和队列绑定。当生产者发送消息时所携带的RoutingKey 与绑定时的BindingKey 相匹配时,消息即被存入相应的队列之中。消费者可以订阅相应的队列来获取消息。 52 | 53 | 这样,大家对AMQP应该就有点“感觉”了吧。 54 | 55 | 这里再给大家搬运几个AMQP解释的比较通俗易懂的链接,感兴趣的可以深入了解,不感兴趣的直接跳过。 56 | 57 | CRUD程序员只要会api就行[手动狗头]: 58 | 59 | 官网:https://www.rabbitmq.com/tutorials/amqp-concepts.html 60 | 61 | 直译:https://www.cnblogs.com/xiaochengzi/p/6895126.html 62 | 63 | 大佬的深入解读:http://www.blogjava.net/qbna350816/archive/2016/08/12/431554.html 64 | 65 | 66 | 67 | 总结一句话:AMQP就是一个协议,是大佬们总结的一种编程模型概念的抽象,约定了通信过程和步骤。用来约束不同MQ中间件厂商,使得大家在使用不同的MQ中间件时,不会出现太大的不兼容情况。 68 | 69 | 而spring就是对这个协议做了简化封装,方便我们使用。 70 | 71 | 72 | 73 | ## Spring AMQP 核心 74 | 75 | ### 1. ConnectionFactory 76 | 77 | 从字面理解,它是一个用于创建连接的工厂类 78 | 79 | ### 2. RabbitAdmin 80 | 81 | 从字面理解,它是rabbitMQ的管理类,主要提供了交换机&队列的声明/绑定/删除功能等,也可以设置一些交换机/队列的初始化参数,如前面提到的的消息有有效期TTL(Time To Live) 82 | 83 | ### 3. Message 84 | 85 | 该类是spring 对消息的一个封装,核心是body 和messageProperties。body就是消息序列化后的字节数组,messageProperties就是消息的一些属性,如:消息的有效期 编码类型等 86 | 87 | ### 4. RabbitTemplate 88 | 89 | 该类是spring封装的rabbitMQ操作类,用来简化收发,有点类似于jdbcTemplate(方便大家快速编写jdbc代码,而不需要关注驱动加载,statement result等)。常用它来`创建连接->创建消息信道->收发消息->消息格式转换->关闭信道->关闭连接`等等操作。该类可以通过配置实例化多个,实现对不同连接相互独立的操作。 90 | 91 | 其中核心又包含ReturnCallBack和ConfirmCallBack。 92 | 93 | #### ConfirmCallBack 94 | 95 | 该功能用于设置消息确认操作,待消费者消费消息之后,告诉发送者`"我已消费消息"`。 96 | 97 | ```java 98 | rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback(){ 99 | public void confirm(CorrelationData correlationData, boolean ack, String cause) { 100 | if (ack) { 101 | System.out.println("消息确认成功"); 102 | } else { 103 | // nack 104 | System.out.println("消息确认失败"); 105 | } 106 | 107 | } 108 | }); 109 | ``` 110 | 111 | 112 | 113 | 114 | #### ReturnCallBack 115 | 116 | 该功能用于设置消息确认响应时的一些参数。 117 | 118 | ```java 119 | @Bean 120 | public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) { 121 | RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); 122 | rabbitTemplate.setMandatory(true); 123 | rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback(){ 124 | public void returnedMessage(Message message, 125 | int replyCode, 126 | String replyText, 127 | String exchange, 128 | String routingKey){ 129 | } 130 | }); 131 | return rabbitTemplate; 132 | } 133 | ``` 134 | 135 | 136 | 137 | 138 | ### 5. MessageListener 139 | 140 | 从字面理解,它是一个需要常开运行的监听者。主要用来监听队列动态,用于处理生产者发送来的消息,并通知/调用消费者进行处理。这个listener主要通过`MessageListenerContainer`来进行管理。 141 | 142 | MessageListenerContainer 143 | 144 | 该Container 可以管理Listener 的生命周期,可以对消费者和队列进行动态配置:动态添加移除队列、设置消费者的ConsumerTag、Arguments、并发、消费者数量、消息确认模式等等。 145 | 146 | 该container是单例的,且是线程安全的。而它生产/管理的的Listener是可以多个线程各不相同的。 147 | 148 | ```java 149 | @Bean 150 | public SimpleMessageListenerContainer messageContainer(ConnectionFactory connectionFactory) { 151 | SimpleMessageListenerContainer container = 152 | new SimpleMessageListenerContainer(connectionFactory); 153 | container.setQueues(getSecondQueue(), getThirdQueue()); //监听的队列 154 | container.setConcurrentConsumers(1); // 最小消费者数 155 | container.setMaxConcurrentConsumers(5); // 最大的消费者数量 156 | container.setDefaultRequeueRejected(false); //是否重回队列 157 | container.setAcknowledgeMode(AcknowledgeMode.AUTO); //签收模式 158 | container.setExposeListenerChannel(true); 159 | container.setConsumerTagStrategy(new ConsumerTagStrategy() { //消费端的标签策略 160 | @Override 161 | public String createConsumerTag(String queue) { 162 | return queue + "_" + UUID.randomUUID().toString(); 163 | } 164 | }); 165 | return container; 166 | } 167 | ``` 168 | 169 | 170 | 171 | ### 6. MessageConvertor 172 | 173 | 从字面理解,该接口是定义一个转换类。因为消息传输都是序列化的字节数组,而收到消息时肯定要还原为业务代码可以直接处理的文本消息。这里的字节数组(byte[]) 就是上面提到的message对象的body部分。 174 | 175 | 整体来说,消息的传输分为两个阶段,序列化和反序列化。 176 | 177 | 序列化就是把类似 `"Hi Tom"` 这样的文字消息,对照ASCII表(或其它字符集编码表)变为字节数组的过程。 178 | 179 | > 如下图 "Hi Tom",5个字母在ASCII表中分别为十进制的`72 105 84 111 109`,空白符是`32`我没有圈出来 180 | > 181 | > image-20201113103905178 182 | > 183 | > image-20201113104031019 184 | > 185 | > 这样一串文字就变成了一串数字,一串数字又可以用二进制表示,在传输过程中直接传输这些文字的二进制,其中每8位二进制作为一个字节,所以就有了上面说的字节数组了。 186 | 187 | 反序列化就是把上面的字节数组再变回`"Hi Tom"`这个过程。 188 | 189 | spring中自带了默认的转换器`SimpleMessageConverter`。 190 | 191 | rabbitMQ自带`Jackson2JsonMessageConverter`支持将对象转换为json。 192 | 193 | 我们也可以通过实现`MessageConverter`接口,并重写`toMessage()`和`fromMessage()`方法来实现消息的序列化(toMessage)和反序列化(fromMessage)。 194 | 195 | 196 | 197 | ## 结束语 198 | 199 | 至此spring的基础接口就讲完了。 200 | 201 | demo源码可以在https://github.com/teartao/rabbitmq-samples.git 的chapter-05找到。 202 | 203 | 参考文献: 204 | [1] RabbitMQ官网:https://www.rabbitmq.com/ 205 | [2] RabbitMQ实战指南 朱忠华 206 | 207 | ---- 208 | 209 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   210 | > 211 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   212 | 213 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 214 | 215 | -------------------------------------------------------------------------------- /mq/06. RabbitMQ 可靠性投递.md: -------------------------------------------------------------------------------- 1 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   2 | > 3 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   4 | 5 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) 6 | 7 | ---- 8 | 9 | 10 | 11 | 本节主要讨论一些复杂情况下的MQ问题。通过一些异常情况和方案来确保消息正确投递。 12 | 13 | ## MQ和DB操作的一致性问题 14 | 15 | 确保MQ消息和数据库一致是消息可靠性的要求之一。下面列出一些常见的异常场景进行分析。 16 | 17 | - 先投递消息再更新数据库 18 | - 先更新数据库再投递消息 19 | 20 | **先投递消息再更新数据库**:当消息投递成功而数据库连接失败时,操作mq回滚可能比较麻烦。因此建议**先操作数据库再发送mq**,利用数据库的事务来回滚数据。这样,先操作数据库暂不提交,等待mq发送成功再提交,可确保数据库和mq一致。一旦mq操作出现失败情况,而此时数据库尚未提交,直接根据mq失败的异常,通过数据库事务控制回滚即可。 21 | 22 | **拓展:**该思路依然适用于一些其它类似场景,如:redis缓存和数据库操作的问题,先操作数据库,再操作缓存,在缓存失败时对数据库进行回滚。 23 | 24 | 其实可以把mq redis 等(需要和数据库保持一致的)中间件都看做缓存,甚至文件的读写等IO操作都可以视作缓存。将数据库放在前面操作是利用了数据库事务回滚方便的特性,可以在后续操作出现异常时回滚,从而保证一致性。(可以继续引申出分布式事务的问题,这里暂不讨论)。 25 | 26 | 27 | 28 | ## MQ的确认机制 29 | 30 | 在mq消息发送过程中,可能存在网络或其它异常情况导致消息发送失败。RabbitMQ提供了确认机制。即:生产者发送了消息给MQ服务端之后,服务端会给生产者一个应答,只要生产者收到了应答,便证明服务端收到了消息。 31 | 32 | 确认机制有两种: 33 | 34 | - 事务模式 35 | - 确认模式 36 | 37 | **事务模式** 38 | 39 | 和数据库类似,在发送消息前开启事务,结束后提交事务。 40 | 41 | 只要提交成功便可以确保消息投递到了MQ服务端。 42 | 43 | 事务模式可以通过`channel.txSelect()`来开启,`channel.txCommit()`来提交。如果中途出现了错误,通过`channel.txRollback()`来回滚。springboot中可以通过`rabbitTemplate.setChannelTransacted(true)`来开启事务。 44 | 45 | 缺点:channel是阻塞的,未提交时无法发送下一条消息,会影响投递性能。(这一点和数据库也类似,没有提交之前,和事务相关的锁定操作都不允许执行),一般不建议使用该模式。 46 | 47 | **确认模式:** 48 | 49 | 确认模式又分了三种: 50 | 51 | - 普通确认模式:发一条确认一条;缺点:单条效率低。 52 | - 批量确认:发了多条一起确认;缺点:不同场景需求不同,无法准确估量一次确认的数量。 53 | - 异步确认:发送消息后,一个独立的确认线程(Listener)专门用来处理未确认的消息。[推荐] 54 | 55 | 异步原为`ConfirmListener` , RabbitTemplate对channel做了封装,具体用法如下: 56 | 57 | ```java 58 | rabbitTemplate.setConfirmCallback( 59 | new RabbitTemplate.ConfirmCallback() { 60 | @Override 61 | public void confirm(CorrelationData correlationData, 62 | boolean ack, String cause) { 63 | if (!ack) { 64 | System.out.println("发送消息失败:" + cause); 65 | throw new RuntimeException("发送异常:" + cause); 66 | } 67 | } 68 | } 69 | ); 70 | ``` 71 | 72 | 73 | 74 | ## 投递失败的消息处理 75 | 76 | 消息可能由于路由键设置错误,或队列配置错误等,无法投递成功。 77 | 78 | 常见处理方式有:重新投递;将消息路由到备份交换机。这里主要说下备份交换机的设置。 79 | 80 | `channel.basicPublish`有两个参数`mandatory`和`immediate` 81 | 82 | ```java 83 | //发送消息 84 | channel.basicPublish(EXCHANGE NAME, "", true, 85 | MessageProperties.PERSISTENT TEXT PLAIN, 86 | "mandatory Msg".getBytes()); 87 | 88 | //投递失败后返回 89 | channel.addReturnListener(new ReturnListener()( 90 | public void handleReturn ( int replyCode, String replyText,String exchange, 91 | String routingKey, AMQP.BasicProperties basicProperties , 92 | byte[] body) throws IOException{ 93 | String message = new String(body); 94 | System.out.println("返回: " + message); 95 | } 96 | }); 97 | ``` 98 | 99 | Spring AMQP中配置方式如下 100 | 101 | ```java 102 | rabbitTemplate.setMandatory(true); 103 | rabbitTemplate.setReturnCallback((RabbitTemplate.ReturnCallback) (message, replyCode, replyText, exchange, routingKey) -> { 104 | System.out.println("回发的消息:"); 105 | System.out.println("replyCode: " + replyCode); 106 | System.out.println("replyText: " + replyText); 107 | System.out.println("exchange: " + exchange); 108 | System.out.println("routingKey: " + routingKey); 109 | }); 110 | ``` 111 | 112 | 创建交换机时添加备份交换机。 113 | 114 | ```java 115 | Map arguments = new HashMap(); 116 | arguments.put("alternate-exchange","ALTERNATE_EXCHANGE"); // 指定交换机的备份交换机 117 | channel.exchangeDeclare("TEST_EXCHANGE","topic", false, false, false, arguments); 118 | ``` 119 | 120 | 121 | 122 | ## 未被消费的消息处理 123 | 124 | 如果消息在队列中长期未被消费,而此时出现了宕机等异常关闭的情况,数据便会丢失。有经验的同学肯定能想到应该将数据持久化到磁盘。RabbitMQ自然提供了该功能。配置方法如下: 125 | 126 | ```java 127 | // 设置消息参数为持久化 128 | MessageProperties messageProperties = new MessageProperties(); 129 | messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT); 130 | Message message = new Message("消息持久化".getBytes(), messageProperties); 131 | 132 | //设置队列持久化 133 | @Bean("persistentQueue") 134 | public Queue persistentQueue() { 135 | // queueName, durable, exclusive, autoDelete, Properties 136 | // 直接设置durable参数为true即可 137 | return new Queue("PERSISTENT_QUEUE", true, false, false, new HashMap<>()); 138 | } 139 | 140 | //设置交换机持久化 141 | @Bean("persistentExchange") 142 | public DirectExchange persistentExchange() { 143 | // exchangeName, durable, exclusive, autoDelete, Properties 144 | return new DirectExchange("PERSISTENT_EXCHANGE", true, false, new HashMap<>()); 145 | } 146 | ``` 147 | 148 | 149 | 150 | ## 消费过程中出现异常 151 | 152 | 消息已经正常投递,且被正常接收用于消费。而在消费过程中出现了异常,该消息未被正确处理时。 153 | 154 | 这种情况下,通常是按需重新发送给消费者进行消费。rabbitMQ提供了“消费者确认”机制,同前文提到的服务端确认类似,此处由消费者发送确认消息给服务端。设置方法如下: 155 | 156 | ```properties 157 | # springboot设置,参数值可选: 158 | # NONE:自动ACK 159 | # MANUAL:手动ACK 160 | # AUTO:无异常,则发送ack。 161 | spring.rabbitmq.listener.direct.acknowledge-mode=manual 162 | spring.rabbitmq.listener.simple.acknowledge-mode=manual 163 | ``` 164 | 165 | 166 | 167 | ## 消息补偿 168 | 169 | 假设上述ack配置后,由于业务处理较长或网络情况,服务端迟迟没有收到ack怎么办? 170 | 171 | 常见的是按业务需要设定/约定一个足够长的时间,当超过该时间进行一次重新投递。 172 | 173 | 174 | 175 | ## 幂等性 176 | 177 | 假设上述的重新投递只是慢,A消息在B重新投递后,依然正确发送了ack,则导致处理了两次同样的消息。因此可以对A B...重复发送的消息做校验/锁定/生成唯一ID等操作,避免重复处理相同消息。 178 | 179 | 180 | 181 | ## 集群 182 | 183 | 前面讲到了消息未被消费的情况,假设消息在持久化过程中出现了故障怎么办?常见做法是对RabbitMQ做集群处理。 184 | 185 | 186 | 187 | ## 总结 188 | 189 | 异常情况还有很多,这里只列出了一些常见的异常情况,并提供了一些相对简单成熟的处理思路。在具体生产环境中,根据业务的复杂程度,数据一致性、可靠性要求的不同,还有其它更多的复杂情况,就得根据实际需要做特定处理了,具体问题还得具体分析。 190 | 191 | ---- 192 | 193 | > 文章可以白嫖,老铁们顺便关注一下我的公众号,手动滑稽🤣🤣   194 | > 195 | > 欢迎大家关注:**武哥聊编程**、**Java开发宝典**、**Java秃头哥**,您的支持,是我创作的持续动力!   196 | 197 | ![武哥聊编程](https://img-blog.csdnimg.cn/202002150421550.jpg)![Java开发宝典](https://img-blog.csdnimg.cn/20200608005630228.png)![Java秃头哥](https://img-blog.csdnimg.cn/20201025170941235.png) --------------------------------------------------------------------------------