├── .gitignore ├── Computer_Networks ├── 1.md ├── UDP&TCP.md └── images │ ├── 2018-01-09-12-26-04.png │ ├── 2018-01-09-16-20-45.png │ ├── 2018-01-09-16-37-31.png │ └── 2018-01-09-16-42-03.png ├── ConcurrentHashMap源码分析(jdk1.8).md ├── Distributed_Systems(MIT) ├── MapReduce.md └── pic │ └── 2017-11-10-1.png ├── HashMap源码解析(jdk8).md ├── JavaEE └── 浅析Web容器.md ├── Java_Concurrency_in_Practice ├── 原子变量与非阻塞同步机制.md ├── 基础构件模块.md ├── 对象的共享.md └── 对象的组合.md ├── README.md ├── Redis设计原理 ├── Redis持久化与复制.md └── Redis集群.md ├── ReentrantLock源码解析(jdk8).md ├── Spring揭秘 └── 1.md ├── Storm ├── pic │ └── 2018-3-2.1.jpg └── storm-grouping.md ├── ThinkInJava ├── Java容器.md └── Java并发.md ├── ThreadPoolExecutor源码解析(jdk8版).md ├── 分布式系统 └── Raft.md ├── 双重检查锁定漏洞分析笔记.md ├── 深入理解Java虚拟机 ├── Java内存区域划分.md ├── Java内存模型.md ├── jvm垃圾收集与内存回收策略.md ├── jvm类加载机制.md ├── pic │ ├── 2017-11-05-1.png │ ├── 2017-11-05-2.png │ └── 2017-12-13-1.png └── 锁优化.md ├── 设计模式 ├── images │ ├── 20130712102729562.png │ ├── 20130713162941328.png │ ├── 20130713163800203.png │ ├── 944365-24c6bf44da1b79ad.png │ └── 944365-c736416f78a5b2d5.png ├── 单例模式.md ├── 工厂方法模式.md ├── 建造者模式.md ├── 抽象工厂模式.md ├── 模板方法.md ├── 策略模式.md ├── 简单工厂模式.md └── 适配器模式.md ├── 面向对象设计六大原则.md └── 面经 ├── 1.md ├── 2.md └── huya.md /.gitignore: -------------------------------------------------------------------------------- 1 | 深入理解Java虚拟机读书笔记/.vscode/settings.json 2 | -------------------------------------------------------------------------------- /Computer_Networks/1.md: -------------------------------------------------------------------------------- 1 | # 网络层 2 | 3 | ### 网络层提供的两种服务: 4 | 5 | 1. 虚电路服务: 两台计算机进行通讯前,应当先建立连接,预留双方通讯所需的一切网络资源,然后沿着这条虚电路发送分组 6 | 7 | 2. 数据报服务: 网络在发送分组时不需要先建立连接,每一个分组(IP数据报)独立发送,与其前后的分组无关。网络层不提供服务质量的承诺 8 | 9 | + 互联网在设计上采用了数据报服务
10 | 网络层向上只提供简单灵活的、无连接的、尽最大努力交付的数据报服务。 11 | 12 | + 好处: 网络造价大大减低,运行方式灵活,能够适应多种应用。 13 | 14 | ### 网际协议IP 15 | 16 | + 与IP协议配套使用的三个协议: 17 | 18 | 1.地址解析协议ARP(Address Resolve Protocol) 19 | 20 | 2.网际控制报文协议ICMP(Internet Control Message Protocol) 21 | 22 | 3.网际组管理协议IGMP(Internet Group Management Protocol) 23 | 24 | + 将网络互相连接起来的中间设备: 25 | 26 | 1.物理层使用的中间设备: 转发器(repeater) 27 | 28 | 2.数据链路层使用的中间设备: 网桥/桥接器(bridge) 29 | 30 | 3.网络层使用的中间设备: 路由器(router) 31 | 32 | 4.网络层之上使用的中间设备: 网关(gateway) 33 | 34 | + 网络互联一般指用路由器进行网络互联和路由选择
35 | 路由器其实就是一台专用计算机,用来在互联网中进行路由选择 36 | 37 | #### 虚拟互联网络: 38 | 39 | 互联起来的物理网络的异构性本来是客观存在的,但是我们利用IP协议就可以使这些性能各异的网络在网络层上看起来好像是一个统一的网络.
40 | 如果在这种覆盖全球的IP网的上层使用TCP协议,那么就是现在的互联网 41 | 42 | 43 | + 分组交换的存储转发: 44 | 45 | 主机H1先查找自己的路由表,看目的主机是否就在本网络上,如是就`直接交付`,如果不是, -------------------------------------------------------------------------------- /Computer_Networks/UDP&TCP.md: -------------------------------------------------------------------------------- 1 | # TCP/IP 2 | 3 | + 复用: 发送方不同的应用进程可以使用同一个运输层协议传送数据 4 | 5 | + 分用: 接收方的运输层在剥去报文的首部后能够把这些数据正确交付目的应用进层 6 | 7 | + 协议端口: 应用层的各种协议进程与运输实体进行层间交互的一种地址.
8 | TCP/IP的运输层用一个词6位的端口号来标志一个端口.端口号只有本地意义,它只是为了标志本计算机应用层中的各个进程和运输层交互时的层间接口 9 | (协议端口是软件端口, 交换机或路由器上的端口是硬件端口) 10 | 11 | + 运输层的端口号分为两大类: 12 | 13 | 1. 服务端使用的端口号: 14 | 15 | a. 熟知端口号(系统端口号): 数值为0~1023,例如FTP(23) TELNET(23) SMTP(25) DNS(53) TFPT(69) HTTP(80) SSH(22) HTTPS(443) 16 | 17 | b. 登记端口号: 1024~49151,没有熟知的应用程序使用,使用需登记. 18 | 19 | 2. 客户端使用的端口号: 20 | 21 | 数值为49152~65535,这类端口号仅在客户端进程运行时才动态选择. 22 | 23 | 24 | ## UDP 用户数据报 25 | 26 | + 主要特点: 27 | 28 | 1. 无连接: 发送数据前不需要建立连接,减少了开销和发送前的时延. 29 | 30 | 2. 尽最大努力交付: 不保证可靠交付,不用维护连接状态表. 31 | 32 | 3. 面向报文: 应用层交下来的报文,UDP直接添加首部就直接交付,不拆分也不合并 33 | 34 | 4. 没有拥塞控制: 网络拥塞不会使源主机降低发送速率.(因为无连接直接发送) 35 | 36 | 5. UDP支持一对一,多对多,多对一和一对多的交互通信. 37 | 38 | 6. UDP首部开销小: 只有个字节 39 | 40 | + UDP首部: 41 | 42 | + UDP有两个字段: 首部字段和数据字段. 43 | 44 | + 首部字段: 45 | 只有8个字节,四个字段组成: 源端口、目的端口、长度(UDP用户数据报长度)、检验和(检测UDP用户数据报传输过程中是否有错) 46 | 47 | + 伪首部: 计算检验和时临时添加在数据报前面,用来得到临时数据报的12个字节 48 | 49 | + 检验和计算算法: 临时数据报检验和字段设为0,若临时数据报不是偶数个字节,则在末尾补零,求16位和取反码得到检验和。 50 | 51 | ## TCP 传输控制 52 | 53 | + 主要特点: 54 | 55 | 1. 面向连接: 应用程序在使用TCP协议之前必须先建立TCP连接 56 | 57 | 2. 点对点通信: 每条TCP连接只能有两个端点 58 | 59 | 3. 可靠交付: 通过TCP连接传送的数据,无差错、不丢失、不重复,并且按序到达 60 | 61 | 4. 双全工通信: 双方的应用进程在任何时候都能发送数据。TCP连接的两端都设有发送缓存和接收缓存,用来临时存放双向通信的工具,TCP在合适的时候发送缓存里的数据,上层应用在合适的时候读取缓存里的数据 62 | 63 | 5. 面向字节流: TCP中的流指流入进程或从进程流出的字节序列。
64 | TCP把应用程序交下来的`数据块`看成是一连串的`无结构的字节流`。TCP不知道所传送的字节流的含义,不保证接收方应用程序所收到的数据块和发送方的数据块具有对应大小关系。但发出跟收到的字节流完全一样。

65 | 66 | TCP根据对方给出的窗口值和当前网络的拥塞程度来决定一个报文段应包含多少个字节(UCP报文长度由应用进程给出)。可以划短一些或积累长一些再构成报文段发送。 67 | 68 | ### TCP连接 69 | 70 | + TCP连接有两个端点,该端点叫`套接字(socket)`或`插口`,端口号拼接到IP地址就构成`套接字`:
71 | 72 | 套接字socket = (IP地址:端口号) 73 | 74 | + 每一条TCP连接唯一的被通信两端的两个端点所确定: 75 | 76 | TCP连接 ::= {socket1, socket2} = {(IP1:PORT1), (IP2:port2)} 77 | 78 | ### 可靠传输工作原理 79 | 80 | 1. 停止等待协议: 每发送完一个分组就停止发送,等待对方的确认 81 | 82 | + 为了提高传输效率,发送方可以使用流水线传输,连续发送多个分组,不必每发完一个就停顿等待确认 83 | 84 | 2. 自动重传请求ARQ: 重传请求是自动进行的,接收方不需要请求发送方重传某个出错的分组 85 | 86 | 3. 连续ARQ协议: 87 | 88 | 发送方每收到一个确认,就把发送窗口向前滑动一个分组的位置。
89 | 接收方采用`累积确认`的方式,接收方在收到几个分组后对按序到达的最后一个分组发送确认,表示收到这个分组为止的所有分组 90 | 91 | + 累计确认的优点: 容易实现,确认丢失也不必重传
92 | + 累计确认的缺点: 不能向发送方反映出接收方已经正确收到的所有分组信息 93 | 94 | 95 | ### TCP报文段首部格式 96 | 97 | 1. 源端口和目的端口: 各占两个字节 98 | 2. 序号: 占4个字节(0~2^32-1).TCP中传输的每一个字节都按顺序编号,该字段填报文段所发送数据的第一个字节的序号 99 | 3. 确认号: 占4个字节.期望收到对方下一个报文段的第一个数据字节的序号 100 | 4. 数据偏移: 占4位.表示TCP报文首部的长度 101 | 5. 保留: 占6位.默认为0 102 | 6. 窗口: 占2字节(0~2^16-1).窗口字段指出允许对方发送的数据量。这个数值经常变动 103 | 104 | 以下为6个控制位 105 | 106 | --- 107 | 7. 紧急 URG 108 | 109 | 当URG=1时,表明此报文段中有紧急数据,系统会优先传送。 110 | 111 | 8. 确认 ACK 112 | 113 | ACK=1时确认字段有效,等于0时无效(TCP规定所有传送的报文段都必须把ACK置1)。 114 | 115 | 9. 推送 PSH 116 | 117 | 当接收方收到PSH设为1的报文段,会尽快的交付接收应用程序且立即响应,而不等缓存填满。 118 | 119 | 10. 复位 RST 120 | 121 | RST为1时,表明TCP连接中出现严重差错,必须释放当前连接并重新建立。 122 | 123 | 11. 同步 SYN 124 | 125 | 在连接建立时用来同步序号。
126 | SYN=1,ACK=0时是连接请求报文段,
127 | SYN=1,ACK=1时是同意连接的响应报文段。 128 | 129 | 12. 终止 FIN 130 | 131 | 用来释放一个连接。
132 | FIN=1时表明此报文段的发送方已发送完毕,要求释放连接。 133 | --- 134 | 135 | 13. 检验和: 占2字节,跟UDP计算过程一样 136 | 137 | 14. 紧急指针: 占2字节。 138 | 139 | 15. 选项: 最长可达40字节的可变字段 140 | 141 | ### 可靠传输工作原理 142 | 143 | 1. 停止等待协议 144 | 145 | 停止等待协议: 每发送完一个分组就停止发送,等待对方的确认。在收到确认后再发送下一个分组
146 | 全双工通信的双方即是发送方也是接收方。 147 | 148 | + 无差错情况: 149 | 150 | A 发送分组 M1,发完就暂停发送,等待 B 的确认 (ACK)。B 收到了 M1 后向A发送 ACK。A 在收到了对 M1 的确认后,就再发送下一个分组 M2。 151 | 152 | + 出现差错: 153 | 154 | + 可能的情况: 155 | 156 | 1. B 接收 M1 时检测出了差错,就丢弃 M1,其他什么也不做(不通知 A 收到有差错的分组)。 157 | 2. M1 在传输过程中丢失了,这时 B 当然什么都不知道,也什么都不做。 158 | 159 | + 解决方法: `超时重传` 160 | 161 | A 为每一个已发送的分组都设置了一个超时计时器。
162 | A 只要在超时计时器到期之前收到了相应的确认,就撤销该超时计时器,继续发送下一个分组 M2 。 163 | 164 | + 确认丢失和确认迟到: 165 | 166 | 1. 确认丢失导致收到重传的分组: 167 | 168 | 1) 丢弃重复的分组 169 | 170 | 2) 发送确认 171 | 172 | 2. 确认迟到导致收到重复分组: 173 | 174 | 1) 丢弃重复分组 175 | 176 | 2) 发送确认 177 | 178 | 3) 确认接收方会收到重复确认,直接丢弃 179 | 180 | 2. 连续ARQ协议 181 | 182 | + 发送方维持一个`发送窗口`,位于发送窗口内的分组都可连续发送出去而不需要等待对方的确认,提高信道利用率。发送方每收到一个确认,就把发送窗口向前滑动该确认分组的位置 183 | 184 | + 接收方采用累积确认的方式,不必对收到的分组逐个发送确认,而是对按序到达的最后一个分组发送确认,表示到目前为止的所有分组都已正确收到了。 185 | 186 | + Go-Back-N: 如果发送了5个分组,中间的第三个分组丢失了,需要重传后面三个分组。 187 | 188 | ### TCP可靠传输实现 189 | 190 | 1. 以字节为单位的滑动窗口 191 | 192 | + TCP的滑动窗口以字节为单位.
193 | 194 | + A收到B发来的确认报文段: 窗口20字节,确认号31
195 | 表明B期望收到的下一个序号是31,A的发送窗口应为31~50 196 | 197 | + 发送缓存: 发送方的应用进程把字节流(已经送出但未收到确认和准备发送的数据,通常比发送窗口大)写入TCP的发送缓存 198 | 199 | + 接收缓存: 接收方的应用进程从TCP的接收缓存中读取字节流。(按序到达的但未被应用程序读取的数据、未按序到达的数据) 200 | 201 | + 强调三点: 202 | 203 | 1) A的发送窗口不总是和B的接收窗口一样大(时间滞后) 204 | 205 | 2) TCP标准未规定对不按序到达的数据应如何处理。通常先临时存放在接收窗口中,等字节流中缺少的字节收到后,再交付上层应用程序 206 | 207 | 3) TCP要求接收方必须有累积确认的功能,这样可以减少传输开销 208 | 209 | 2. 超时重传的时间选择 210 | 211 | + 超时重传时间太短: 引起很多报文段的不必要重传,使网络负荷增大 212 | 213 | + 超时重传时间过长: 网络的空闲时间增大,减低传输效率 214 | 215 | + TCP采用自适应算法,记录一个报文的发出时间和相应的确认时间,这两个时间的时间差就是报文段的往返时间RTT。进行加权平均得到加权平均往返时间RTTs: 216 | 217 | 新的RTTs = (1-a)×旧的RTTs×新的RTT 218 | 219 | 3. 选择确认SACK 220 | 221 | 确认选择SACK用于只传送缺少的数据而不重传已经正确到达接收方的数据。
222 | 大多数实现还是重传所有未被确认的数据块。 223 | 224 | ### TCP流量控制 225 | 226 | 1. 利用滑动窗口实现流量控制 227 | 228 | + 流量控制: 让发送方的发送速率不要太快,使得接收方来得及接收。 229 | 230 | + A 向 B 发送数据。在连接建立时,B 告诉 A: 231 | “我的接收窗口 rwnd = 400(字节)”。
232 | 发送方的发送窗口不能超过接收方给出的接收窗口的数值。 233 | 234 | + TCP为每一个连接设有一个`持续计时器`。只要TCP连接的一方收到对方的零窗口通知,就启动持续计时器,计时器设置的时间到期,就发送一个零窗口的`探测报文段`(1字节),对方就在确认这个探测报文段时给出现在的窗口值。
235 | 若仍为0,则重设持续计时器。
236 | 若不为0,那么打破僵局。 237 | 238 | 2. TCP的传输效率 239 | 240 | + 控制TCP报文发送时机的三种机制: 241 | 242 | 1) TCP维持一个变量,它等于最大报文段长度MSS。只要缓存中存放数据达到MSS字节时就组装成一个TCP报文段发送出去 243 | 244 | 2) 发送方应用进程指明要求发送报文段 245 | 246 | 3) 发送方计时器时限到了,把当前已有缓存装入报文段(不超过MSS)发送 247 | 248 | + 发送方糊涂综合征: 249 | 250 | + 发送方TCP每次接收到一字节数据后就发送出去 251 | 252 | 解决: Nagle算法
253 | 发送方就把第一个数据字节先发送出去,把后面到达的数据字节都缓存起来。当发送方收到对第一个数据字符的确认后,再把发送缓存中的所有数据组装成一个报文段发送出去,同时继续对随后到达的数据进行缓存。 254 | 255 | + 接收方糊涂综合征: 256 | 257 | + 接收方的 TCP 缓冲区已满。接收方的应用进程以交互方式每次只读取一个字节,于是接收方又发送窗口大小为一个字节的更新报文,发送方应邀发送一个字节的数据。 258 | 259 | 解决: 让接收方等待一段时间,使得或者接收缓存已有足够空间容纳一个最长的报文段,或者等到接收缓存已有一半空闲的空间。只要出现这两种情况之一,接收方就发出确认报文,并向发送方通知当前的窗口大小。 260 | 261 | ### TCP拥塞控制 262 | 263 | 1. 拥塞控制一般原理 264 | 265 | + 拥塞: 对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏的情况: 266 | 267 | ∑对资源需求 > 可用资源 268 | 269 | + 拥塞控制与流量控制的区别: 270 | 271 | 拥塞控制是防止过多的数据注入到网络,使其中的路由器或链路不致于过载.
272 | 拥塞控制是一个全局性过程,涉及到所有的主机,路由器,以及与降低网络传输性可能有关的所有因素。

273 | 相反,流量控制是点对点通信量的控制,是端到端的问题。流量控制所要做的就是抑制发送端发送数据的速率,以便使接收端来得及接收。 274 | 275 | + 宏观上两种拥塞控制方法: 276 | 277 | 1. 开环控制: 在设计网络时事先将有关发生拥塞的因素考虑周到,力求网络在工作时不产生拥塞。 278 | 279 | 2. 闭环控制: 基于反馈环路的概念: 280 | 281 | 1) 检测网络系统以便检测到拥塞在何时、何处发生 282 | 283 | 2) 将拥塞发生的心思传送到可采取行动的地方 284 | 285 | 3) 调整网络系统的运行以解决出现的问题 286 | 287 | + 监测网络拥塞的指标: 288 | 289 | + 缺少缓存空间而被丢弃的分组的百分数 290 | + 平均队列长度 291 | + 超时重传的分组数 292 | + 平均分组时延 293 | + 分组时延的标准差,等待 294 | 295 | 2. 拥塞控制方法 296 | 297 | + TCP采用基于窗口的方法进行拥塞控制(属于闭环控制) 298 | 299 | + TCP发送方维持一个拥塞窗口CWND。
300 | 拥塞窗口的大小取决于网络的拥塞程度,并动态变化。
301 | 发送端利用拥塞窗口根据网络的拥塞情况调整发送的数据量。
302 | 发送窗口大小不仅取决于接收方公告的接收窗口,还取决于网络的拥塞状况。
303 | 304 | 真正的发送窗口值 = Min(公告窗口值,拥塞窗口值) 305 | 306 | + 拥塞的判断: 307 | 308 | 1. 重传定时器超时 309 | 310 | 2. 收到三个相同的ACK 311 | 312 | + 四种TCP控制算法: 313 | 314 | 1. 慢开始: 315 | 316 | + 用来确定网络的负载能力 317 | + 算法思路: 由小到大逐渐增加(×2)拥塞窗口数值 318 | + 慢开始门限ssthresh: 防止拥塞窗口cwnd增长过大引起网络拥塞 319 | + cwnd控制方法: 每收到一个对新报文的确认后,把拥塞窗口最多增加一个SMSS的数值(N 是原先未被确认的、但现在被刚收到的确认报文段所确认的字节数): 320 | 321 | 拥塞窗口cwnd每次增加量 = min(N, SMSS) 322 | 323 | + 慢开始门限 ssthresh 的用法如下: 324 | + 当 cwnd < ssthresh 时,使用慢开始算法。 325 | + 当 cwnd > ssthresh 时,停止使用慢开始算法而改用拥塞避免算法。 326 | + 当 cwnd = ssthresh 时,既可使用慢开始算法,也可使用拥塞避免算法 327 | 328 | 2. 拥塞避免: 329 | 330 | + 思路:让cwnd缓慢增大,每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍,使拥塞窗口 cwnd 按线性规律缓慢增长。 331 | + 无论在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(重传超时): 332 | 333 | ssthresh = max(cwnd/2,2) 334 | cwnd = 1 335 | 执行慢开始算法 336 | (减少主机发送到网络中的分组数,使得发生拥塞的路由器有足够时间把队列中积压的分组处理完毕) 337 | 338 | 3. 快重传 339 | 340 | + 快重传FR (Fast Retransmission) 算法可以让发送方尽早知道发生了个别报文段的丢失。 341 | 342 | + 快重传算法要求接收方不要等待自己发送数据时才进行捎带确认,而是要立即发送确认,即使收到了失序的报文段也要立即发出对已收到的报文段的重复确认 343 | 344 | + 发送方只要一连收到三个重复确认,就知道接收方确实没有收到报文段,因而应当立即进行重传(即“快重传”),这样就不会出现超时,发送方也不就会误认为出现了网络拥塞 345 | 346 | + 快重传并非取消重传计时器,而是在某些情况下可更早地重传丢失的报文段。 347 | 348 | 4. 快速恢复 349 | 350 | + 当发送端收到连续三个重复的确认时,由于发送方现在认为网络很可能没有发生拥塞,因此现在不执行慢开始算法,而是执行快恢复算法 351 | 352 | + 控制方法: 353 | 354 | 1) 慢开始门限 ssthresh = 当前拥塞窗口 cwnd / 2。 355 | 2) 新拥塞窗口 cwnd = 慢开始门限 ssthresh。 356 | 3) 开始执行拥塞避免算法,使拥塞窗口缓慢地线性增大。 357 | 358 | ![](images/2018-01-09-12-26-04.png) 359 | 360 | ### TCP的运输连接管理 361 | 362 | + 运输连接有三个阶段: 363 | 364 | 1. 连接建立 365 | 2. 数据传送 366 | 3. 连接释放 367 | 368 | + 运输连接的管理就是使运输连接的建立和释放都能正常地运行 369 | 370 | + 客户-服务器方式: 371 | 372 | + TCP连接的建立采用客户-服务器方式 373 | + 主动发起建立的应用进程叫做客户(client) 374 | + 被动等待连接建立的应用进程叫做服务器(server) 375 | 376 | 377 | 1. TCP连接建立 378 | 379 | + TCP的连接建立: 采用三报文握手: 380 | 381 | 1. A的TCP向B发出连接请求报文段,其首部中的同步位SYN=1,并选择序号seq=x,表明传送数据时的第一个数据字节的序号是x。 382 | 383 | 2. B的TCP收到连接请求报文段后,如果同一,就发回确认:
384 | B在确认报文段中使SYN=1,使ACK=1,确认号ack=x+1,自己选择的序号seq=y. 385 | 386 | 3. A收到B的确认报文后向B给出确认,ACK=1, 确认号ack=y+1。A的TCP通知上层应用程序TCP连接已经建立 387 | 388 | 4. B收到A的确认报文后,也通知上层应用程序TCP连接已建立 389 | 390 | ![](images/2018-01-09-16-20-45.png) 391 | 392 | 2. TCP连接释放 393 | 394 | + TCP的连接释放: 采用四报文分手: 395 | 396 | 1. A的应用程序进程向其TCP发出连接释放报文段,并停止发送数据,主动关闭TCP连接。
397 | A把连接释放报文段首部的FIN=1,序号seq=u,等待B的确认。 398 | 399 | 2. B收到A的连接释放报文后发出确认。确认号ack=u+1,报文段序号seq=v。
400 | TCP服务器进程通知高层应用进程释放从A到B这个方向的连接。
401 | 此时TCP连接处于半关闭状态,若B发送数据,A仍要接收。 402 | 403 | 3. 若B已经没有要向A发送的数据,其应用程序进程就通知TCP释放连接。FIN=1,ACK=1,ack=u+1,序号seq=w 404 | 405 | 4. A收到后发送确认。
406 | ACK=1,确认号码ack=w+1,序号seq=u+1。等待2MSL后释放从B到A这个方向的连接 407 | 408 | + A必须等待2MSL(MSL最长报文寿命)的时间: 409 | 410 | 1. 为了保证A发送的最后一个ACK报文能到达B 411 | 2. 防止“已失效的连接请求报文段”出现在本连接中,影响下一个新的连接。 412 | 413 | ![](images/2018-01-09-16-37-31.png) 414 | 415 | 416 | 3. TCP的有限状态机 417 | 418 | ![](images/2018-01-09-16-42-03.png) -------------------------------------------------------------------------------- /Computer_Networks/images/2018-01-09-12-26-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wususu/Notes/c566a18c29c64943f6043b7580536693ed68f13d/Computer_Networks/images/2018-01-09-12-26-04.png -------------------------------------------------------------------------------- /Computer_Networks/images/2018-01-09-16-20-45.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wususu/Notes/c566a18c29c64943f6043b7580536693ed68f13d/Computer_Networks/images/2018-01-09-16-20-45.png -------------------------------------------------------------------------------- /Computer_Networks/images/2018-01-09-16-37-31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wususu/Notes/c566a18c29c64943f6043b7580536693ed68f13d/Computer_Networks/images/2018-01-09-16-37-31.png -------------------------------------------------------------------------------- /Computer_Networks/images/2018-01-09-16-42-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wususu/Notes/c566a18c29c64943f6043b7580536693ed68f13d/Computer_Networks/images/2018-01-09-16-42-03.png -------------------------------------------------------------------------------- /ConcurrentHashMap源码分析(jdk1.8).md: -------------------------------------------------------------------------------- 1 | # ConcurrentHashMap源码分析(基于jdk1.8) 2 | 3 | # 简介 4 | 5 | 1.8版本的并发HashMap相较与1.7的版本,去掉了分段锁,每次操作都在hash table的一个桶位上进行cas或加锁操作,细化了加锁粒度,提高并发能力。初次之外,在哈希冲突处理方面更新了增到8转红黑树,减到6转链表。 6 | 7 | 8 | 9 | We do not want to waste the space required to associate a distinct lock object with each bin, so instead use the first node of a bin list itself as a lock. Locking support for these locks relies on builtin "synchronized" monitors. 10 | 11 | The main disadvantage of per-bin locks is that other update operations on other nodes in a bin list protected by the same lock can stall 12 | 13 | public class ConcurrentHashMap extends AbstractMap 14 | implements ConcurrentMap, Serializable { 15 | } 16 | 17 | ### 类属性 18 | 19 | + private static final int MAXIMUM_CAPACITY = 1 << 30; 20 | 21 | table桶数最大值,前两位用作控制标志 22 | 23 | 24 | + private static final int DEFAULT_CAPACITY = 16; 25 | 26 | table桶数初始化默认值,需为2的幂次方 27 | /** 28 | * The largest possible (non-power of two) array size. 29 | * Needed by toArray and related methods. 30 | */ 31 | + static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; 32 | 33 | 34 | + private static final float LOAD_FACTOR = 0.75f; 35 | 36 | 加载因子,扩容的阀值,可在构造方法定义 37 | 38 | 39 | + static final int TREEIFY_THRESHOLD = 8; 40 | 41 | 树化阀值1,当链表节点超过8允许转化为红黑树 42 | 43 | + static final int UNTREEIFY_THRESHOLD = 6; 44 | 45 | 链化阀值1,当树节点小于6则转化为链表 46 | 47 | + static final int MIN_TREEIFY_CAPACITY = 64; 48 | 49 | 树化阀值2,当数组桶树达到64以上才允许链表树化 50 | 51 | + private static final int MIN_TRANSFER_STRIDE = 16; 52 | 53 | 链化阀值2,与上类似 54 | 55 | /** 56 | * The number of bits used for generation stamp in sizeCtl. 57 | * Must be at least 6 for 32bit arrays. 58 | */ 59 | private static int RESIZE_STAMP_BITS = 16; 60 | 61 | /** 62 | * The maximum number of threads that can help resize. 63 | * Must fit in 32 - RESIZE_STAMP_BITS bits. 64 | */ 65 | private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1; 66 | 67 | /** 68 | * The bit shift for recording size stamp in sizeCtl. 69 | */ 70 | private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS; 71 | 72 | /* 73 | * Encodings for Node hash fields. See above for explanation. 74 | */ 75 | static final int MOVED = -1; // hash for forwarding nodes 76 | static final int TREEBIN = -2; // hash for roots of trees 77 | static final int RESERVED = -3; // hash for transient reservations 78 | static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash 79 | 80 | /** Number of CPUS, to place bounds on some sizings */ 81 | static final int NCPU = Runtime.getRuntime().availableProcessors(); 82 | 83 | 84 | ## 主要的内部类 85 | 86 | ### Node 87 | 88 | + final保证线程安全性 volatile保证线程间修改(update)的可见性 89 | 90 | static class Node implements Map.Entry { 91 | // 键值对的hash计算值 92 | final int hash; 93 | final K key; 94 | volatile V val; 95 | // 下一个节点,链表法解决冲突 96 | volatile Node next; 97 | } 98 | 99 | ## 对象属性 100 | 101 | + transient volatile Node[] table; 102 | 103 | 桶数组,数组长度为2的幂次方 104 | 105 | + private transient volatile Node[] nextTable; 106 | 107 | resize操作使用的桶数组 108 | 109 | /** 110 | * Base counter value, used mainly when there is no contention, 111 | * but also as a fallback during table initialization 112 | * races. Updated via CAS. 113 | */ 114 | private transient volatile long baseCount; 115 | 116 | /** 117 | * Table initialization and resizing control. When negative, the 118 | * table is being initialized or resized: -1 for initialization, 119 | * else -(1 + the number of active resizing threads). Otherwise, 120 | * when table is null, holds the initial table size to use upon 121 | * creation, or 0 for default. After initialization, holds the 122 | * next element count value upon which to resize the table. 123 | */ 124 | private transient volatile int sizeCtl; 125 | 126 | /** 127 | * The next table index (plus one) to split while resizing. 128 | */ 129 | private transient volatile int transferIndex; 130 | 131 | /** 132 | * Spinlock (locked via CAS) used when resizing and/or creating CounterCells. 133 | */ 134 | private transient volatile int cellsBusy; 135 | 136 | /** 137 | * Table of counter cells. When non-null, size is a power of 2. 138 | */ 139 | private transient volatile CounterCell[] counterCells; 140 | 141 | + private transient KeySetView keySet;
private transient ValuesView values;
private transient EntrySetView entrySet; 142 | 143 | 数据视图 144 | 145 | 146 | ## 主要的几个方法 147 | 148 | ### spread 149 | 150 | 再散列,通过哈希吗的高位与低位抑或,使K-V的分布更均匀。HASH_BITS为7fffffff,通过与其相与保障最高位为0,避免出现ffff0000^0000ffff=ffffffff发送数组越界的情况 151 | 152 | static final int spread(int h) { 153 | return (h ^ (h >>> 16)) & HASH_BITS; 154 | } 155 | 156 | 157 | ### get 158 | 159 | // 与HashMap不同,key不能为Null 160 | public V get(Object key) { 161 | Node[] tab; Node e, p; int n, eh; K ek; 162 | // spread通过高位参与运算,获取数组的桶位 163 | int h = spread(key.hashCode()); 164 | if ((tab = table) != null && (n = tab.length) > 0 && 165 | // 取出该桶位的第一个节点 166 | (e = tabAt(tab, (n - 1) & h)) != null) { 167 | // 若第一个节点就是当前key,直接返回第一个节点的val 168 | if ((eh = e.hash) == h) { 169 | if ((ek = e.key) == key || (ek != null && key.equals(ek))) 170 | return e.val; 171 | } 172 | // 在冲突解决的数据结构中查找节点并返回其val 173 | // eh<0 --> hash最高位为1,代表当前桶位冲突解决使用的是红黑树,find查找 174 | else if (eh < 0) 175 | return (p = e.find(h, key)) != null ? p.val : null; 176 | // eh<0 --> hash最高位为0,代表当前桶位冲突解决使用的是链表,直接遍历查询 177 | while ((e = e.next) != null) { 178 | if (e.hash == h && 179 | ((ek = e.key) == key || (ek != null && key.equals(ek)))) 180 | return e.val; 181 | } 182 | } 183 | return null; 184 | } 185 | 186 | ### put 187 | 188 | put实际实现为putVal,增加一个参数,判断是否覆盖原有的K-V 189 | 190 | Key和Value都不能为Null 191 | 192 | /** 193 | * Maps the specified key to the specified value in this table. 194 | * Neither the key nor the value can be null. 195 | * 196 | *

The value can be retrieved by calling the {@code get} method 197 | * with a key that is equal to the original key. 198 | */ 199 | public V put(K key, V value) { 200 | return putVal(key, value, false); 201 | } 202 | 203 | /** Implementation for put and putIfAbsent */ 204 | final V putVal(K key, V value, boolean onlyIfAbsent) { 205 | if (key == null || value == null) throw new NullPointerException(); 206 | int hash = spread(key.hashCode()); 207 | int binCount = 0; 208 | for (Node[] tab = table;;) { 209 | Node f; int n, i, fh; 210 | // 若桶数组仍未初始化,则先初始化数组 211 | if (tab == null || (n = tab.length) == 0) 212 | tab = initTable(); 213 | // 若该桶位无头节点,则通过CAS操作插入桶位,退出循环 214 | else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { 215 | if (casTabAt(tab, i, null, 216 | new Node(hash, key, value, null))) 217 | break; // no lock when adding to empty bin 218 | } 219 | // 查看该头节点状态,若为MOVED(当前正在resize操作),执行helpTransfer帮助resize 220 | else if ((fh = f.hash) == MOVED) 221 | tab = helpTransfer(tab, f); 222 | // 当前头节点已经存在或者CAS操作失败,对头结点synchronized加对象锁,执行冲突解决 223 | else { 224 | V oldVal = null; 225 | synchronized (f) { 226 | // 双重检查 227 | if (tabAt(tab, i) == f) { 228 | // 若是链表 229 | if (fh >= 0) { 230 | binCount = 1; 231 | for (Node e = f;; ++binCount) { 232 | K ek; 233 | if (e.hash == hash && 234 | ((ek = e.key) == key || 235 | (ek != null && key.equals(ek)))) { 236 | oldVal = e.val; 237 | // 发现插入Key已存在,根据onlyIfAbsent执行覆盖 238 | if (!onlyIfAbsent) 239 | e.val = value; 240 | break; 241 | } 242 | Node pred = e; 243 | // 没有相同的Key,接入链表尾部 244 | if ((e = e.next) == null) { 245 | pred.next = new Node(hash, key, 246 | value, null); 247 | break; 248 | } 249 | } 250 | } 251 | // 若是红黑树 252 | else if (f instanceof TreeBin) { 253 | Node p; 254 | binCount = 2; 255 | if ((p = ((TreeBin)f).putTreeVal(hash, key, 256 | value)) != null) { 257 | oldVal = p.val; 258 | if (!onlyIfAbsent) 259 | p.val = value; 260 | } 261 | } 262 | } 263 | } 264 | // bitCount为链插入后的长度,达到阀值就执行树化操作 265 | if (binCount != 0) { 266 | if (binCount >= TREEIFY_THRESHOLD) 267 | treeifyBin(tab, i); 268 | if (oldVal != null) 269 | return oldVal; 270 | break; 271 | } 272 | } 273 | } 274 | // K-V计数加一 275 | addCount(1L, binCount); 276 | return null; 277 | } 278 | 279 | ### size 280 | 281 | size操作返回的值来自于CounterCell[]中元素值之和,其中的元素改动又来自于addCount方法 282 | 283 | 可知CounterCell[]其中每个元素代表着桶数组中每个元素内的节点数 284 | 285 | /** 286 | * {@inheritDoc} 287 | */ 288 | public int size() { 289 | long n = sumCount(); 290 | return ((n < 0L) ? 0 : 291 | (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : 292 | (int)n); 293 | } 294 | 295 | @sun.misc.Contended static final class CounterCell { 296 | volatile long value; 297 | CounterCell(long x) { value = x; } 298 | } 299 | 300 | final long sumCount() { 301 | CounterCell[] as = counterCells; CounterCell a; 302 | long sum = baseCount; 303 | if (as != null) { 304 | for (int i = 0; i < as.length; ++i) { 305 | if ((a = as[i]) != null) 306 | sum += a.value; 307 | } 308 | } 309 | return sum; 310 | } 311 | 312 | ### resize 313 | 314 | -------------------------------------------------------------------------------- /Distributed_Systems(MIT)/MapReduce.md: -------------------------------------------------------------------------------- 1 | # MapReduce 2 | 3 | ### 介绍: 4 | 5 | MapReduce是一种编程模型,也是一种处理和生成大量数据的实现 6 | 7 | 用户定义一个`map`函数用来处理键值对并生成中间键值对,定义一个`reduce`函数将相同中间键的中间值合并起来. 8 | 9 | 10 | ### MapReduce隐藏的"痛苦"细节: 11 | 12 | + 跟踪哪些任务完成 13 | + 数据移动 14 | + 从故障中恢复 15 | 16 | ### MapReduce可以很好地扩展: 17 | 18 | N台电脑可以有N*x的吞吐量,`Maps()`是可以并行执行的,`Reduce()`也一样.所以可以通过加入更多的电脑来提高吞吐量. 19 | 20 | ### 可能影响性能的因素: 21 | 22 | + CPU 23 | + 内存 24 | + 网络 25 | + 硬盘 26 | 27 | 28 | ### 参考一个具体的实现: 29 | 30 | + 把Map调用的输入数据分割城M个数据片段的集合,输入的数据片段在多台机器上调用`Map`并行处理. 31 | + 通过分区函数(用户指定)把`Map调用`产生的中间`key`值分成R(用户指定)个不同分区(eg:hash(key) mod R),`Reduce`调用也是分布在多台机器上执行. 32 | 33 | 34 | ![MR实现](pic/2017-11-10-1.png) 35 | 36 | 37 | + `MapReduce`实现的操作流程: 38 | 39 | 1. 用户程序调用`MapReduce库`将输入文件分成M个数据片段(16~64MB),然后程序在集群中创建大量程序副本. 40 | 41 | 2. 程序副本中有个特殊的程序`master`,副本中其他程序都是`worker`.由`master`将一个`Map任务`或`Reduce任务`分配给一个空闲的`worker`. 42 | 43 | 3. 被分配`Map任务`的`worker`将读取相关输入数据片段,从数据片断分解出`key/value键值对`传递给用户自定义的`Map函数`,`Map函数`处理并生成`中间key/value键值对`,缓存在内存中. 44 | 45 | 4. `分区函数`把缓存中的`中间key/value键值对`分成R个区域,周期性写入本地硬盘,然后将本地硬盘上的储存位置回传给`master`,由`master`将存储位置传送给`Reduce worker`. 46 | 47 | 5. `Reduce worker`接收到`master`程序发来的数据存储位置后,使用`RPC`从对应主机硬盘上读取这些中间数据,通过`key`进行排序. 48 | 49 | 6. `Reduce worker`遍历排序后的中间键值对,将每个不同的key值及其`value值集合`传递给用户自定义的`Reduce函数`,把输出追加到所属分区的输出文件. 50 | 51 | 7. 所有的`worker`都将任务完成后,`master`唤醒用户程序,MapReduce的调用才返回. 52 | 53 | > 完成任务后,计算结果在R个输出文件中(对应每个`Reduce`任务尝试的一个输出文件).一般情况下用户不需要将R个输出文件合并成一个文件,而是用于另一个MapReduce的输入或者可以处理多个分割文件的分布式应用中使用. 54 | 55 | ### 如何做到负载平衡: 56 | 57 | + `Master`把新的任务分配给完成先前任务的`worker`,这样处理能力强的`worker`完成更多的任务. 58 | + 当`ReduceMap`操作快要接近完成的时候,`master`调度备用(backup)任务进程去执行那些处于处理状态中的任务(相当赛跑,看谁先完成). 59 | 60 | 61 | ### 怎样进行容错: 62 | 63 | + MapReduce要求`Map`和`Reduce`是输入确定性函数/pure function(即相同的输入产生相同的输出) 64 | 65 | + 不保存状态 66 | + 没隐藏通讯 67 | + 没有IO读写 68 | 69 | 70 | + worker故障: 71 | 72 | 1. `Master`会周期性的ping每个`worker`,在某个约定时间内没有响应就会判断该`worker`失效. 73 | 74 | 2. 这个`worker`已完成的`Map任务`(中间输出保存在故障机器上)和正在完成的任务将被标记为失效,安排给其他的`worker`执行. 75 | 76 | + master失效: 77 | 78 | `master`周期性将自身数据结构写入磁盘,建立一个检查点(checkpoint),若`master`任务失效了,就从最后一个检查点(checkpoint)开始启动另一个master进程. 79 | 80 | 81 | ### 什么应用程序MapReduce无法正常工作: 82 | 83 | + 不是所有东西符合 map/shuffle/reduce 模式: 84 | + 小数据,开销太大 85 | + 大数据的小更新 86 | + 不可预测的读取 87 | 88 | ### 总结: 89 | 90 | MapReduce使得大集群运算变得流行 91 | 92 | - 它不是最有效或灵活的。 93 | + 扩展性好 94 | + 易于编程实现 -- 故障和数据移动是隐藏的 95 | 96 | 97 | 98 | 99 | > 参考资料:
100 | > https://pdos.csail.mit.edu/6.824/notes/l01.txt
101 | > https://pdos.csail.mit.edu/6.824/papers/mapreduce.pdf -------------------------------------------------------------------------------- /Distributed_Systems(MIT)/pic/2017-11-10-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wususu/Notes/c566a18c29c64943f6043b7580536693ed68f13d/Distributed_Systems(MIT)/pic/2017-11-10-1.png -------------------------------------------------------------------------------- /HashMap源码解析(jdk8).md: -------------------------------------------------------------------------------- 1 | # HashMap源码解析(jdk8版) 2 | 3 | HashMap允许key为null,也允许value为null
4 | HashMap跟HashTable两个类是差不多的,除了HashTable是线程安全且不允许null值这一点外. 5 | 6 | 基本概念:HashMap底层是数组+链表(数组的每个值都是一条链表的头结点),1.8后加入了红黑树(当链表长度达到8就自动将该链表替换为红黑树),通过计算key的哈希码,在经过高位参与位运算计算得出键值对(将key和value包装起来的对象)所在的数组的下标,采用头插入法插入该位置的链表(若该位置是空的就直接插入) 7 | 8 | ## HashMap的相关域: 9 | 10 | 11 | static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 12 | 13 | static final int MAXIMUM_CAPACITY = 1 << 30; 14 | 15 | static final float DEFAULT_LOAD_FACTOR = 0.75f; 16 | 17 | static final int TREEIFY_THRESHOLD = 8; 18 | 19 | static final int UNTREEIFY_THRESHOLD = 6; 20 | 21 | static final int MIN_TREEIFY_CAPACITY = 64; 22 | 23 | + DEFAULT_INITIAL_CAPACITY: 默认底层数组的初始大小(2^4),可通过构造参数指定 24 | + MAXIMUM_CAPACITY: 数组的最大长度(2^31),超过将替换为此值 25 | + DEFAULT_LOAD_FACTOR: 默认负载因子,为0.75,可通过构造参数指定 26 | + TREEIFY_THRESHOLD: 链表转换为红黑树的阙值,当链表长度达到8自动转为红黑树进行存储(前提是数组长度大于等于64) 27 | + UNTREEIFY_THRESHOLD: 红黑树转为链表的阙值,当红黑树结点个数减小到6时,自动转为链表存储 28 | + MIN_TREEIFY_CAPACITY: 链表进行树化的前提条件,数组长度要达到64或一上,在这之前只能通过数组扩容来减少链表长度 29 | 30 | transient Node[] table; 31 | 32 | transient Set> entrySet; 33 | 34 | transient int size; 35 | 36 | transient int modCount; 37 | 38 | int threshold; 39 | 40 | final float loadFactor; 41 | 42 | + table: 底层数组,可动态扩容,数组长度为2的整数次方 43 | + size: 这个map中存放的键值对数目 44 | + modCount: 记录这个map数据结构发生改变的次数(发送插入删除或者链表与树相互转换的操作),由于fail-fast机制 45 | + threshold: 数组进行扩容的下个阙值(当前键值对数量达到这个值后进行`resize()`(扩容)操作)(threshold = capacity * load factor) 46 | + loadFactor: 实际的负载因子 47 | 48 | 49 | 50 | ## 哈希码计算方法`hash(Object key)` 51 | 52 | static final int hash(Object key) { 53 | int h; 54 | return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); 55 | } 56 | 57 | + 局部变量h存放hashCode()放回的初始哈希码,通过h右移16位与h异或(右移后,前16位为0,异或不改变h的前16位值)得到最终的哈希吗.
58 | 通过高位参与位运算可以减少数组长度较低时的哈希码冲突问题(取模时,高位变低位不变,冲突几率会变高) 59 | 60 | 61 | ## 链表结点的数据结构`Node` 62 | 63 | static class Node implements Map.Entry { 64 | final int hash; // 计算得到的hash码 65 | final K key; // 键对象 66 | V value; // 值对象 67 | Node next; // 下一个结点 68 | 方法略... 69 | } 70 | 71 | ## 树节点数据结构v`TreeNode` 72 | 73 | static final class TreeNode extends LinkedHashMap.Entry { 74 | TreeNode parent; // 父亲节点 75 | TreeNode left; // 左子树 76 | TreeNode right; //右子树 77 | TreeNode prev; // needed to unlink next upon deletion 78 | boolean red; // 红色还是黑色 79 | TreeNode(int hash, K key, V val, Node next) { 80 | super(hash, key, val, next); 81 | 方法略... 82 | } 83 | 84 | + 通过继承`LinkedHashMap.Entry`,实际上间接继承了链表的`Node` 85 | 86 | ## 获取value:get(Object key) 87 | 88 | public V get(Object key) { 89 | Node e; 90 | return (e = getNode(hash(key), key)) == null ? null : e.value; //1 91 | } 92 | 93 | final Node getNode(int hash, Object key) { 94 | Node[] tab; 95 | Node first, e; 96 | int n; 97 | K k; 98 | if ((tab = table) != null && (n = tab.length) > 0 && 99 | (first = tab[(n - 1) & hash]) != null) { //2 100 | if (first.hash == hash && // always check first node 101 | ((k = first.key) == key || (key != null && key.equals(k)))) //3 102 | return first; 103 | if ((e = first.next) != null) { //4 104 | if (first instanceof TreeNode) 105 | return ((TreeNode)first).getTreeNode(hash, key); 106 | do { 107 | if (e.hash == hash && 108 | ((k = e.key) == key || (key != null && key.equals(k)))) 109 | return e; 110 | } while ((e = e.next) != null); 111 | } 112 | } 113 | return null; 114 | } 115 | 116 | 1. 计算key的哈希码,传入getNode方法,放回Node对象或者null 117 | 2. 如果table为null,table是空的或者数组( (length-1)&hash )处的值为null,就返回null,否则进入3 118 | 3. 检查第一个结点,若是指定的key,直接返回该结点,否则进入4 119 | 4. 如果这个树/链表不止一个结点,先判断是树还是链表,再进行对应的结点查找,找到就返回,否则返回null. 120 | 121 | ## 增加键值对`put(K key, V value)` 122 | 123 | public V put(K key, V value) { 124 | return putVal(hash(key), key, value, false, true); 125 | } 126 | 127 | /** 128 | * Implements Map.put and related methods 129 | * 130 | * @param hash hash for key 131 | * @param key the key 132 | * @param value the value to put 133 | * @param onlyIfAbsent if true, don't change existing value 134 | * @param evict if false, the table is in creation mode. 135 | * @return previous value, or null if none 136 | */ 137 | final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 138 | boolean evict) { 139 | Node[] tab; 140 | Node p; 141 | int n, i; 142 | if ((tab = table) == null || (n = tab.length) == 0) //1 143 | n = (tab = resize()).length; 144 | if ((p = tab[i = (n - 1) & hash]) == null) //2 145 | tab[i] = newNode(hash, key, value, null); 146 | else { 147 | Node e; K k; 148 | if (p.hash == hash && 149 | ((k = p.key) == key || (key != null && key.equals(k)))) //3 150 | e = p; 151 | else if (p instanceof TreeNode) //4 152 | e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); 153 | else { //5 154 | for (int binCount = 0; ; ++binCount) { 155 | if ((e = p.next) == null) { 156 | p.next = newNode(hash, key, value, null); 157 | if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 158 | treeifyBin(tab, hash); 159 | break; 160 | } 161 | if (e.hash == hash && 162 | ((k = e.key) == key || (key != null && key.equals(k)))) 163 | break; 164 | p = e; 165 | } 166 | } 167 | if (e != null) { // existing mapping for key //6 168 | V oldValue = e.value; 169 | if (!onlyIfAbsent || oldValue == null) 170 | e.value = value; 171 | afterNodeAccess(e); 172 | return oldValue; 173 | } 174 | } //7 175 | ++modCount; 176 | if (++size > threshold) 177 | resize(); 178 | afterNodeInsertion(evict); 179 | return null; 180 | } 181 | 182 | 1. 如果数组为null或是空的,则`resize()`扩充容量 183 | 2. 通过hash计算并位运算取摸获得数组下标,若该位置是空的,新建链表结点直接填坑然后跳到7,否则进入3 184 | 3. 判断头结点的key跟要put进去的key是否同一个,是则将其引用赋给e,进入6,否则进入4 185 | 4. 判断头结点是不是树结点,是则执行`putTreeVal`,若树中已存在该key,则直接返回该键值对(赋给e),否则新建并插入结点并返回null,然后进入6.如果不是树节点则进入5 186 | 5. 在链表中遍历,如果不存在,就新建一个结点,然后是否达到树化的阙值,是就转化为树结构,之后跳到7.如果存在就把它的引用赋给e跳到6 187 | 6. 在搜索到当前map中存在相同key时候将该键值对赋给e,在这里进行值的覆盖,并返回旧值 188 | 7. 对改动进行计数,判断是否需要进行数组扩容,返回null 189 | 190 | 191 | ## 移除键值对`remove(Object key)` 192 | 193 | public V remove(Object key) { 194 | Node e; 195 | return (e = removeNode(hash(key), key, null, false, true)) == null ? 196 | null : e.value; 197 | } 198 | 199 | final Node removeNode(int hash, Object key, Object value, 200 | boolean matchValue, boolean movable) { 201 | Node[] tab; 202 | Node p; 203 | int n,index; 204 | if ((tab = table) != null && (n = tab.length) > 0 && //1 205 | (p = tab[index = (n - 1) & hash]) != null) { 206 | Node node = null, e; 207 | K k; 208 | V v; 209 | if (p.hash == hash && 210 | ((k = p.key) == key || (key != null && key.equals(k)))) //2 211 | node = p; 212 | else if ((e = p.next) != null) { //3 213 | if (p instanceof TreeNode) 214 | node = ((TreeNode)p).getTreeNode(hash, key); 215 | else { 216 | do { 217 | if (e.hash == hash && 218 | ((k = e.key) == key || 219 | (key != null && key.equals(k)))) { 220 | node = e; 221 | break; 222 | } 223 | p = e; 224 | } while ((e = e.next) != null); 225 | } 226 | } 227 | if (node != null && (!matchValue || (v = node.value) == value || //4 228 | (value != null && value.equals(v)))) { 229 | if (node instanceof TreeNode) 230 | ((TreeNode)node).removeTreeNode(this, tab, movable); 231 | else if (node == p) 232 | tab[index] = node.next; 233 | else 234 | p.next = node.next; 235 | ++modCount; 236 | --size; 237 | afterNodeRemoval(node); 238 | return node; 239 | } 240 | } 241 | return null; 242 | } 243 | 244 | 1. 判断底层数组是否为null或者是空的,是就直接返回null,否则2 245 | 2. 判断头结点是否就是要移除的键值对,是就赋给e,进入4,否则进入3 246 | 3. 判断是树还是链表并进行相应遍历,找到符合的键值对,并赋给e,进入4,若查无,返回null 247 | 4. 针对不同的存储结构进行相应的移除操作,并更新相关的计数值 248 | 249 | ## 链表的树化操作`treeifyBin(Node[] tab, int hash)` 250 | 251 | /** 252 | * Replaces all linked nodes in bin at index for given hash unless 253 | * table is too small, in which case resizes instead. 254 | */ 255 | final void treeifyBin(Node[] tab, int hash) { 256 | int n, index; Node e; 257 | if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) 258 | resize(); 259 | else if ((e = tab[index = (n - 1) & hash]) != null) { 260 | TreeNode hd = null, tl = null; 261 | do { 262 | TreeNode p = replacementTreeNode(e, null); 263 | if (tl == null) 264 | hd = p; 265 | else { 266 | p.prev = tl; 267 | tl.next = p; 268 | } 269 | tl = p; 270 | } while ((e = e.next) != null); 271 | if ((tab[index] = hd) != null) 272 | hd.treeify(tab); 273 | } 274 | } 275 | 276 | + 先将链表结点转化成树结点,构造成双向链表,在`treeify`进行红黑树的构造 277 | 278 | ## 扩容操作`resize` 279 | 280 | /** 281 | * Initializes or doubles table size. If null, allocates in 282 | * accord with initial capacity target held in field threshold. 283 | * Otherwise, because we are using power-of-two expansion, the 284 | * elements from each bin must either stay at same index, or move 285 | * with a power of two offset in the new table. 286 | * 287 | * @return the table 288 | */ 289 | final Node[] resize() { 290 | Node[] oldTab = table; 291 | int oldCap = (oldTab == null) ? 0 : oldTab.length; 292 | int oldThr = threshold; 293 | int newCap, newThr = 0; 294 | if (oldCap > 0) { 295 | if (oldCap >= MAXIMUM_CAPACITY) { // 1 296 | threshold = Integer.MAX_VALUE; 297 | return oldTab; 298 | } 299 | else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && // 2 300 | oldCap >= DEFAULT_INITIAL_CAPACITY) 301 | newThr = oldThr << 1; // double threshold 302 | } 303 | else if (oldThr > 0) // initial capacity was placed in threshold // 3 304 | newCap = oldThr; 305 | else { // zero initial threshold signifies using defaults // 4 306 | newCap = DEFAULT_INITIAL_CAPACITY; 307 | newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); 308 | } 309 | if (newThr == 0) { // 5 310 | float ft = (float)newCap * loadFactor; 311 | newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? 312 | (int)ft : Integer.MAX_VALUE); 313 | } 314 | threshold = newThr; // 6 315 | @SuppressWarnings({"rawtypes","unchecked"}) 316 | Node[] newTab = (Node[])new Node[newCap]; 317 | table = newTab; 318 | if (oldTab != null) { // 7 319 | for (int j = 0; j < oldCap; ++j) { 320 | Node e; 321 | if ((e = oldTab[j]) != null) { 322 | oldTab[j] = null; 323 | if (e.next == null) // 7-1 324 | newTab[e.hash & (newCap - 1)] = e; 325 | else if (e instanceof TreeNode) // 7-2 326 | ((TreeNode)e).split(this, newTab, j, oldCap); 327 | else { // preserve order // 7-3 328 | Node loHead = null, loTail = null; 329 | Node hiHead = null, hiTail = null; 330 | Node next; 331 | do { 332 | next = e.next; 333 | if ((e.hash & oldCap) == 0) { 334 | if (loTail == null) 335 | loHead = e; 336 | else 337 | loTail.next = e; 338 | loTail = e; 339 | } 340 | else { 341 | if (hiTail == null) 342 | hiHead = e; 343 | else 344 | hiTail.next = e; 345 | hiTail = e; 346 | } 347 | } while ((e = next) != null); 348 | if (loTail != null) { 349 | loTail.next = null; 350 | newTab[j] = loHead; 351 | } 352 | if (hiTail != null) { 353 | hiTail.next = null; 354 | newTab[j + oldCap] = hiHead; 355 | } 356 | } 357 | } 358 | } 359 | } 360 | return newTab; 361 | } 362 | 363 | 1. 若底层数组长度大于等于允许的最大值,将扩容阙值设为MAX_INT,直接不做任何操作,直接返回原数组 364 | 2. 如果底层数组长度是大于默认初始长度且当前长度*2小于允许的最大值,则将新的数组长度,扩容阙值都设为原来的两倍 365 | 3. 当前数组未初始化,且扩容阙值已经初始化(不为0),将新的数组长度设定为扩容阙值,跳到5 366 | 4. 当前数组与扩容阙值都未初始化,将新的数组长度和扩容阙值设为默认初始值 367 | 5. 根据新的数组长度值计算新的扩容阙值,如果新的数组长度值或者新的阙值大于数组长度的允许最大值,则将其替换为MAX_INT,反之保留 368 | 6. 将经过上述计算得到的新值进行更新(设置threshold为新值, 实例化一个新长度的底层数组) 369 | 7. 遍历数组的每个坑位,将老数组的值搬运到新的数组中 370 | 7-1. 若该坑位只有一个结点,直接搬运到新数组对应坑位,需要重新计算下标,因为新数组的长度已经改变 371 | 7-2. 若该坑位放的是树,则调用对应方法进行换坑 372 | 7-3. 若该坑位是是链表,遍历这条链表,根据其hash&旧数组长度是0还是1分为两组,一组在新数组下标不变,另一组是原来下标+旧数组长度
373 | 注: 因为每次扩容都是2扩容两倍,位运算时只增加一个高位(右数第oldCap个),按位与时,若键值对的右数第oldCap位是0则下标不会受扩容影响,若不是,则下标是原下标加上oldCap. 374 | 375 | 376 | > 以上分析为个人理解,欢迎指正! 377 | 378 | 379 | > 关于红黑树的实现与操作并没有深入代码层次解析,有兴趣可阅读 380 | [红黑树深入析及Java实现 381 | ](https://tech.meituan.com/redblack-tree.html) 382 | -------------------------------------------------------------------------------- /JavaEE/浅析Web容器.md: -------------------------------------------------------------------------------- 1 | # 浅析Web容器 2 | 3 | ## 容器介绍 4 | 5 | 容器这个概念是针对Servlet而言的,Servlet一般指的是Java Servlet
6 | Web容器用于管理Servlet,当服务器接收到一个指向Servlet的请求,就将其转发给Web容器
7 | 之后由Web容器向Servlet提供http请求(request),http响应(response),再通过调用service()方法(doPost或doGet)进行处理 8 | 9 | ## 容器的作用 10 | 11 | 1. 提供通讯支持: 12 | 13 | 利用容器提供的方法,可以简单的实现servlet与web服务器的对话。 14 | 否则就要自己建立server,监听端口,创建新的流等等一系列复杂的操作。(就是直接Socket编程了)而容器的存在就帮我们封装这一系列复杂的操作。使我们能够专注于servlet中的业务逻辑的实现。 15 | 16 | 2. 生命周期管理 17 | 容器负责servlet的整个生命周期。如何加载类,实例化和初始化servlet,调用servlet方法,并使servlet实例能够被垃圾回收。有了容器,我们就不用花精力去考虑这些资源管理垃圾回收之类的事情。 18 | 19 | 3. 多线程支持 20 | 容器会自动为接收的每个servlet请求创建一个新的java线程,servlet运行完之后,容器会自动结束这个线程。 21 | 22 | 4. 声明式安全 23 | 利用容器,可以使用xml部署描述文件来配置安全性,而不必将其硬编码到servlet中 24 | 25 | 5. jsp支持 26 | 容器可以jsp翻译成java文件(本质是Servlet) 27 | 28 | ## 容器处理请求流程 29 | 30 | 1. client请求一个指向Servlet的url 31 | 2. 容器识别出请求索要的是Servlet,新建出`httpServletRequest`和`httpServletResponse`两个对象 32 | 3. 容器根据请求的url和web.xml配置文件的映射关系找到对应的Servlet,为当前请求新建一个线程,并把`httpServletRequest`,`httpServletResponse`传递到Servlet对象中 33 | 4. Servlet对象通过调用`service()`方法调用`doGet()`或`doPost()`方法处理请求 34 | 5. 请求处理完成后将响应内容填入`httpServletResponse`对象中 35 | 6. 线程结束,容器将`httpServletResponse`对象转化为http响应,传回给client,销毁`httpServletRequest`和`httpServletResponse`对象 36 | 37 | 38 | > 内容引自[web开发中 web 容器的作用(如tomcat)-六尺帐篷](https://www.jianshu.com/p/99f34a91aefe) -------------------------------------------------------------------------------- /Java_Concurrency_in_Practice/原子变量与非阻塞同步机制.md: -------------------------------------------------------------------------------- 1 | # 原子变量与非阻塞同步机制 2 | -------------------------------------------------------------------------------- /Java_Concurrency_in_Practice/基础构件模块.md: -------------------------------------------------------------------------------- 1 | # 基础构建模块 2 | 3 | 4 | ## 一. 同步容器类 5 | 6 | + 同步容器类实现线程安全的方式是:
7 | 将他们的状态封装起来,并对每个公有方法都进行同步,使得每次只有一个线程能访问容器的状态. 8 | 9 | ### 同步容器类的问题 10 | 11 | + 同步容器类都是`线程安全`的,但在某些情况下可能需要额外的客户端加锁来保护复合操作. 12 | 13 | + 由于同步容器类要遵守`同步策略`(支持客户端加锁),因此可能会创建一些新的操作,只要我们知道应该使用哪一个锁,那么这些新操作就与容器的其他操作一样是`原子操作`. 14 | 15 | ### 迭代器与ConcurrentModificationException 16 | 17 | + 在设计同步容器类的迭代器时并没有考虑到并发修改的问题,它们表现出的行为是`"及时失败"(fail-fast)`的
18 | 这意味着,当发现容器在迭代过程中被修改时,就会抛出`迭代器与ConcurrentModificationException` 19 | 20 | + 避免出现`迭代器与ConcurrentModificationException`的方法: 21 | 22 | 1. 迭代过程持有容器的锁 23 | 2. "克隆"容器,并在副本上迭代(克隆过程仍需要对容器加锁) 24 | 25 | ### 隐藏迭代器 26 | 27 | + 标准容器的`toString`方法将`隐式`迭代容器,并在每个元素上调用`toString`来生成容器内容的格式化表示 28 | 29 | + 容器的`containsAll`,`hashCode`和`equals`等方法也会间接地执行迭代操作. 30 | 31 | > 正如封装对象的状态有助于维持`不变性条件`一样,封装对象的`同步机制`(同步代码)同样有助于确保实施`同步策略`(eg:用`synchronizedSet`包装`HashSet`) 32 | 33 | 34 | ## 二. 并发容器 35 | 36 | + 同步容器将所有对容器的状态访问都串行化,严重降低并发性,当多个线程访问锁时,吞吐量将严重降低. 37 | 38 | + 同步容器是`synchronizedXXX`这类,仅仅给容器提供同步,效率很低
39 | 并发容器是针对多个线程并发访问设计的,`ConcurrentMap`,`CopyOnWriteList`(遍历操作较多的情况下代理同步的`List`),`ConcurrentQueue`(传统先进先出队列)`BlockingQueue`(扩展`Queue`,提供可阻塞操作),`BlockingDeque`(可阻塞双端队列).. 40 | 41 | > 用并发容器来代替同步容器,可以极大地提高伸缩性并降低风险 42 | 43 | ### ConcurrentHashMap 44 | 45 | + `ConcurrentHashMap`使用一种粒度更细的`加锁机制`来实现更大程度的共享,这种机制称为`分段锁(Lock Strip)`
46 | 任意数量的读取线程可以并发地访问Map,并且一定数量的写入线程可以并发地修改Map
47 | `ConcurrentHashMap`带来的结果是,在并发访问的环境下实现更高的`吞吐量`,而在单线程环境中值损失非常小的性能 48 | 49 | + `ConcurrentHashMap`返回的迭代器具有弱一致性(Weakly Consistent) 50 | 51 | + 用`ConcurrentHashMap`代替同步Map能进一步提高代码的`可伸缩性`,只有当应用程序需要加锁Map进行`独占访问`时,才应放弃使用`ConcurrentHashMap` 52 | 53 | ### 额外的原子Map操作 54 | 55 | + "若没有则添加","若相等则移除"和"若相等则替换"等,都已经实现为`原子操作`并且在`ConcurrentMap`的接口中声明 56 | 57 | ### CopyOnWriteArrayList 58 | 59 | + `CopyOnWriteArrayList`用于替代同步`List`在某些情况下它提供了更好的并发性能,并且在迭代期间不需要对容器进行加锁或复制 60 | 61 | + 在每次修改时,它都会创建并重新发布一个`新的容器副本`,从而实现`可变性` 62 | 63 | + 仅当迭代操作远远多于修改操作时,才应该使用"写入时复制"容器 64 | 65 | ## 三. 阻塞队列和生——消模式 66 | 67 | + 阻塞队列提供了可阻塞的`put`和`take`方法,以及支持定时的`offer`和`poll`方法 68 | 69 | + "生产者"和"消费者"角色是相对的,某种环境中的消费者在另一种不同的环境中可能成为生产者 70 | 71 | > 在构建高可靠的应用程序时,有界队列是一种强大的资源管理工具:
72 | > 它们能抑制并防止产生过多的工作项,使应用程序在负荷过载的情况下变得更加强壮 73 | 74 | + 类库中包含了`BlockingQueue`(并发队列)的多种实现: 75 | 76 | + `LinkedBlockingQueue`和`ArrayBlockingQueue`是FIFO队列,比同步List拥有更好的并发性能 77 | 78 | + `PriorityBlockingQueue`是按优先级排序的队列 79 | 80 | + `SynchronousQueue`不是一个真正的队列,不会为元素维护储存空间,它只维护一组等待着把元素加入或移出队列的线程

81 | 实现了直接交付,从而降低了从`生产者`移动到`消费者`的延迟,当交付被接受时,它就知道消费者已经得到了任务

82 | 仅当有足够多的消费者,并且总有一个消费者准备好获取交付的工作时,才适合使用同步队列 83 | 84 | + 生产者——消费者模式能带来许多性能优势: 85 | 86 | `生产者`和`消费者`可以并发地执行.
87 | 如果一个是`I/O密集型`,另一个是`CPU密集型`,那么`并发执行`的`吞吐率`要高于`串行执行`的吞吐率.
88 | 如果`生产者`和`消费者`的并行度不同,将它们`紧密耦合`在一起会把整体并行度降低为二者中最下的并行度 89 | 90 | ### 串行线程封闭 91 | 92 | + `可变对象`,`生产者——消费者`与`阻塞队列`一起,促进了串行线程封闭,将对象所有权从`生产者`交付给`消费者`
93 | `封闭对象`只能由单个线程拥有,但可以通过安全地发布对象来"转移"所有权,在转移后,另一个线程独占该对象访问权限 94 | 95 | ### 双端队列与工作密取 96 | 97 | + `Deque`是一个`双端队列`,实现了在队列头和队列尾的高效插入和移除
98 | 具体实现包括`ArrayDeque`和`LinkedBlockingDeque` 99 | 100 | + 双端队列适用与`工作密取模式(Work Stealing)`
101 | 在`工作密取模式`中,每个`消费者`都有各自的`双端队列`.如果一个`消费者`完成了自己`双端队列`中的全部工作,那它就可以从其他`消费者`的`双端队列`末尾秘密地获取工作 102 | 103 | + 密取模式比传统的`生产者——消费者`模式具有更高的`可伸缩性`,因为工作者线程不会在单个共享的任务队列上发生竞争 104 | 105 | ## 四. 阻塞方法和中断方法 106 | 107 | + 线程可能会阻塞或暂停执行的原因: 等待I/O操作结束,等待获得一个锁,等待从`Thread.sleep`方法中醒来,或是等待另一个线程的计算结果

108 | 当线程阻塞时,它通常被挂起,并处于某种阻塞状态(BLOCKED,WAITING或TIMED_WAITING)

109 | 阻塞操作与执行时间长的差别在于,被阻塞的线程必须等待某个不受它控制的事件发生后才能继续执行(eg:等待I/O操作完成,等待某个锁变可用,等待外部计算结果) 110 | 111 | + 当某方法抛出`InterruptedException`时,表示该方法是阻塞方法,如果这个方法被中断,那么它将女里提前结束阻塞状态 112 | 113 | + 中断是一种`协作机制`,一个线程不能强制其他线程停止正在执行的操作而去执行其他的操作
114 | 当线程A中断线程B时,A仅仅要求B在执行到某个可以暂停的地方停止正在执行的操作,前提是B愿意停下来 115 | 116 | + 处理对中断的响应,有两种选择: 117 | 118 | 1. 传递`InterruptedException` 119 | 2. 恢复中断 120 | 121 | + 在出现`InterruptException`时最不应该做的是,捕获它但不作出任何响应 122 | 123 | 124 | ## 五. 同步工具类 125 | 126 | + 同步工具类可以是任何一个对象,只要它根据自身的状态来协调线程的控制流
127 | `阻塞队列`可以作为同步工具类,其他类型的同步工具还有`信号量(Semaphore)`,`栅栏(Barrier)`以及`闭锁(Latch)` 128 | 129 | + 所有同步工具类都包含一些特定的结构化属性: 130 | 131 | 1. 封装了一些状态(决定线程是继续执行还是等待) 132 | 2. 提供一些方法对状态进行操作 133 | 3. 另一些方法用于高效地等待同步工具进入到预期状态 134 | 135 | ### 闭锁 136 | 137 | + 闭锁相当于一扇门: 在闭锁到达结束状态之前,这扇门一直是关闭的,并且没有任何线程能通过,当到达结束状态时,这扇门就会打开并允许所有线程通过
138 | 当闭锁到达结束状态后,将不会再改变状态,因此这扇门将永远保持打开状态

139 | 闭锁可以用来确保某些活动直到其他活动都完成后才能执行 140 | 141 | + 闭锁状态包括: 142 | 143 | 1. 一个计数器,该计数器被初始化为一个正数,表示需要等待的事件数量 144 | 2. `countDown`方法递减计数器,表示有一个事件已经发生了 145 | 3. `await`方法等待计数器达到零,表示所有需要等待的事件都已经发生 146 | + 如果计数器的值非零(或者等待的线程中断/超时),那么`await`会一直阻塞到计数器为零 147 | 148 | ### FutureTask 149 | 150 | + `FutureTask`表示的计算是通过`Callable`来实现的,相当于一中可生成结果的`Runnable`
151 | 有以下三种状态: `等待运行(Waiting to Run`, `正在运行(Running)` 和 `运行完成(Completed)` 152 | 153 | + 通过提前启动计算,可以减少在等待结果时需要的时间 154 | 155 | + 在`get`方法抛出`ExecutionException`时,可能是以下三种情况之一:
156 | `Callable`抛出的受检查异常, `RuntimeException`, 以及`Error`
157 | (必须对每种情况进行单独处理) 158 | 159 | ### 信号量 160 | 161 | + `计数信号量(Counting Semaphore)`用来控制同时访问某个特点资源的操作数量,或者同时执行某个指定操作的数量. 162 | 163 | + `Semaphore`中管理着一组虚拟的许可,许可的初始数量可通过构造函数来指定
164 | 在执行操作时可以先获得许可,并在使用后`release`释放许可给信号量
165 | 如果没有许可,那么`acquire`将阻塞直到有许可 166 | 167 | + `Sequence`可以用于实现资源池,也可以将任何一种容器变成有界阻塞容器 168 | 169 | ### 栅栏 170 | 171 | + `栅栏`与`闭锁`的关键区别在于,所有线程必须都到达栅栏位置才能继续执行
172 | 173 | + `闭锁`用于等待事件,而`栅栏`用于等待其他线程 174 | 175 | + 如果对`await`的调用超时,或者`await`阻塞的线程被中断,那么栅栏就被认为是打破了,所有阻塞的`await`调用都终止并抛出`BrokenBarrierException` 176 | 177 | + 在模拟程序中通常需要使用栅栏(eg.等待所有玩家加载完毕开启游戏) 178 | 179 | + 另一种形式的栅栏是`Exchanger`,它是一种`两方(Two-Party)栅栏`,各方在栅栏位置上交换数据

180 | 当双方执行不对称时,`Exchanger`非常有用 181 | 182 | ## 六. 构件高效且可伸缩的结果缓存 183 | 184 | ### 分析一个简单的`HashMap`缓存 185 | 186 | * ### 代码一 187 | 188 | public interface Computable{ 189 | V compute(A arg) throws InterruptedException; 190 | } 191 | 192 | public class ExpensiveFunction implements Computable{ 193 | public BigInteger compute(String arg){ 194 | //长时间计算 195 | return new BigInteger(arg); 196 | } 197 | } 198 | 199 | public class Memoizerl implements Computable{ 200 | private final Map cache = new HashMap(); 201 | private final Computable c; 202 | 203 | public Memoizer1(Computable c){ 204 | this.c = c; 205 | } 206 | 207 | public synchronized V compute(A arg) throws InterruptedException{ 208 | V result = cache.get(arg); 209 | if(result == null){ 210 | result = c.compute(arg); 211 | cache.put(arg, result); 212 | } 213 | return result; 214 | } 215 | } 216 | 217 | + 对整个方法进行同步,能保证线程安全性.但会带来一个明显的`可伸缩性`问题:每次只有一个线程能执行该方法

218 | 如果有多个线程在排队等待还未计算出的结果,那么该方法的计算时间可能比没有缓存操作的计算时间还长 219 | 220 | * ### 代码二 221 | 222 | public class Memoizer2 implements Computable{ 223 | 224 | private final Map cache = new ConcurrentHashMap(); 225 | private final Computable c; 226 | 227 | public Memoizer2(Computable c){ 228 | this.c = c; 229 | } 230 | 231 | public V compute(A arg) throws InterruptedException{ 232 | V result = cache.get(arg); 233 | if(result == null){ // 线程B进入(在这之前A已经进入) 234 | result = c.compute(arg); // 线程A在计算 235 | cache.put(arg, result); 236 | } 237 | return result; 238 | } 239 | } 240 | 241 | + 代码二的问题在于:
242 | 如果某个线程启动了一个开销很大的计算,而其他线程并不知道这个计算正在进行,那么会产生重复计算. 243 | 244 | * ### 代码三 245 | 246 | public class Memoizer3 implements Computable{ 247 | 248 | private final Map> cache = new ConcurrentHashMap<>(); 249 | private final Computable c; 250 | 251 | public Memoizer3(Computable c){ 252 | this.c = c; 253 | } 254 | 255 | public V compute(final A arg) throws InterruptedException{ 256 | Future f = cache.get(arg); 257 | if(f == null){ // 非原子,多个线程可进入 258 | Callable eval = new Callable(){ 259 | public V call() throws InterruptedException{ 260 | return c.compute(arg); 261 | } 262 | }; 263 | FutureTask ft = new FutureTask(eval); 264 | f = ft; 265 | cache.put(arg, ft); 266 | ft.run(); 267 | } 268 | try{ 269 | return f.get(); 270 | }cache(ExcutionException e){ 271 | throw launderThrowable(e.getCause()); 272 | } 273 | } 274 | } 275 | 276 | + `compute`方法中的if代码块是`非原子`的"先检查再执行"操作,因此两个线程仍有可能在同一时间内调用`compute`来计算相同的值 277 | 278 | * ### 代码四(最终实现) 279 | 280 | public class Memoizer3 implements Computable{ 281 | private final Map> cache = new ConcurrentHashMap<>(); 282 | private final Computable c; 283 | 284 | public Memoizer3(Computable c){ 285 | this.c = c; 286 | } 287 | 288 | public V compute(final A arg) throws InterruptedException{ 289 | while(true){ 290 | Future f = cache.get(arg); 291 | if(f == null){ // 非原子,多个线程可进入 292 | Callable eval = new Callable(){ 293 | public V call() throws InterruptedException{ 294 | return c.compute(arg); 295 | } 296 | }; 297 | FutureTask ft = new FutureTask(eval); 298 | f = cache.putIfAbsent(arg, ft); //原子操作,在此限制其他线程 299 | if(f == null){ 300 | f = ft; 301 | ft.run(); 302 | } 303 | } 304 | try{ 305 | return f.get(); 306 | }cache(CancellationException e){ 307 | cache.remove(arg, f); //计算取消,移除Future 308 | }cache(ExcutionException e){ 309 | throw launderThrowable(e.getCause()); 310 | } 311 | } 312 | } 313 | } 314 | 315 | ### 最后奖励一个并发技巧清单:dog: 316 | 317 | > * 可变状态是至关重要的
318 | 所有的并发问题都可以归结为如何协调对并发状态的访问.可变状态越少,就越容易确保线程安全性
319 | > * 尽量将域声明为`final`型,除非需要它们是不可变的
320 | > * 不可变对象一定是线程安全的
321 | 不可变对象能极大地降低并发编程的复杂性.它们更为简单而且安全,可以任意共享而无须使用加锁或保护性复制等机制 322 | > * 封装有助于管理复杂性
323 | 在编写线程安全的程序时,虽然可以将所有数据都保存在全局变量中.
324 | 但将数据封装在对象中,更易于维持不变性条件: 将同步机制封装在对象中,更易于遵循同步策略 325 | > * 用锁来保护每个可变变量 326 | > * 当保护同一个不变性条件中的所有变量时,要使用同一个锁 327 | > * 在执行复操作期间,要持有锁 328 | > * 如果从多个线程中访问同一个变量时没有同步机制,那么程序会出现问题 329 | > * 不要故作聪明地推断出不需要使用同步 330 | > * 在设计过程中考虑线程安全,或者在文档中指出它不是线程安全的 331 | > * 将同步策略文档化 332 | 333 | ~~done! 洗澡睡觉 -------------------------------------------------------------------------------- /Java_Concurrency_in_Practice/对象的共享.md: -------------------------------------------------------------------------------- 1 | # 对象的共享 2 | 3 | 4 | 5 | ## 一. 可见性 6 | 7 | + 可见性指当一个线程修改了对象状态之后,其他线程能看到发生的状态变化. 8 | 9 | + 在没有同步的情况下,编译器,处理器及运行时等都可能对操作的执行顺序进行一些意想不到的调整. 10 | 11 | + 只要有数据在多个线程之间共享,就得使用正确的同步. 12 | 13 | 14 | ### 失效数据 15 | 16 | + 读取某一变量时,可能会得到的一个已经失效的值(旧值,默认值) 17 | 18 | + 对set方法和get方法进行同步(仅仅set同步是不够的) 19 | 20 | 21 | ### 非原子的64位操作 22 | 23 | + Java内存模型要求,变量的读取操作和写入操作都必须是原子操作,但对于非`volatile`类型的`long`和`double`变量,JVM允许将64位的读取操作或写操作分解为两个32位的操作.在多线程中可能导致高32位和低32位在不同线程中. 24 | 25 | + 在多线程中共享可变的`double`和`long`等类型,需要用`volatile`声明,或用锁保护起来. 26 | 27 | 28 | ### 加锁与可见性 29 | 30 | + 加锁的含义不仅仅局限于互斥行为,还包括内存可见性.为了确保所有线程都能看到共享变量的最新值,所有读写操作的线程都要在同一个锁上同步. 31 | 32 | 33 | ### Volatile变量 34 | 35 | + 把变量声明为`volatile`类型,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序.
`volatile变量`不会被缓存在寄存器或对其他处理器不可见的地方,因此读取`volatile`类型的变量总会返回最新写入的值. 36 | 37 | + #### volatile变量的三种正确使用方式: 38 | 1. 确保自身状态可见性. 39 | 2. 确保引用对象的状态可见性. 40 | 3. 标识一些重要的程序生命周期事件的发生(初始化或关闭) 41 | 42 | + ### 当满足以下所有条件时,才该使用`volatile`变量: 43 | 1. 对变量的写入操作不依赖变量的当前值,或者能确保只有单个线程更新变量值. 44 | 2. 该变量不会与其他状态变量一起纳入不变性条件中. 45 | 3. 访问变量时不需要加锁. 46 | 47 | + 加锁机制既可以确保`可见性`又可以确保`原子性`,而`volatile`变量只能确保`可见性`. 48 | 49 | 50 | 51 | ## 二. 发布与逸出 52 | 53 | + `发布(Publish)`: 对象能够在当前作用域之外的代码中使用.
`逸出(EScape)`: 当某个不该发布的对象被发布时,这种情况就称为逸出. 54 | 55 | + 发布内部状态可能会破坏封装性,并使得程序难以维持不变性条件. 56 | 57 | + 发布对象的方式: 58 | 59 | 1. 将对象的应用保存到一个共有的静态变量中: 60 | 61 | public static Set knownSerects; 62 | 63 | public void initialize(){ 64 | knownSecrets = new HashSet(); 65 | } 66 | 67 | 2. 间接发布其他对象: 68 | 69 | 将一个Secret对象添加到knownSecret中,这个对象也会被发布. 70 | 71 | 3. 发布一个对象时,该对象的非私有域中引用的所有对象都会被发布. 72 | 73 | 4. 发布一个`内部类`的实例: 74 | 75 | public class ThisEscape{ 76 | 77 | public ThisEscape(EventSource source){ 78 | source.registerListener( 79 | new EventListener(){ 80 | public void onEvent(Event e){ 81 | doSomething(e); 82 | } 83 | }); 84 | } 85 | } 86 | 87 | 当`ThisEscape`发布`EventListener`时,也隐含地发布了`ThisEscape`实例本身.因为内部类实例包含对外部类实例的隐含引用. 88 | 89 | ### 安全的对象构造过程: 90 | 91 | + 不正确的对象构造过程: 当从对象的构造函数中发布该对象时,只发布了一个尚未构造完成的对象(即使发布语句位于最后一行).
如果`this`引用在构造过程中逸出,那么这种对象就被认为是不正确构造. 92 | 93 | + `this`引用逸出: 94 | 1. 在构造函数中创建并启动一个线程. 95 | 2. 在构造函数中调用一个可改写的实例方法(既不是私有也不是终结方法). 96 | 97 | 98 | ## 三. 线程封闭 99 | 100 | + 一种避免使用同步的方式就是不共享数据,如果仅在单线程内访问数据,就不需要同步,这种技术被称为`线程封闭(Thread Confinement)`. 101 | 102 | ### Ad-hoc线程封闭 103 | 104 | + `Ad-hoc`线程封闭是指,维护线程封闭性的职责完全由程序实现来承担. 105 | 106 | + `Ad-hoc`线程封闭技术较为脆弱,应尽量少用. 107 | 108 | 109 | ### 栈封闭 110 | 111 | + 栈封闭是线程封闭的一中特例,在栈封闭中,只能通过局部变量才能访问对象. 112 | 113 | + 局部变量的固有属性之一就是封闭在执行线程中,为与执行线程的栈中,其他线程无法访问这个栈. 114 | 115 | ### ThreadLocal类 116 | 117 | + 维持线程封闭的更规范的方法是使用`ThreadLocal`,这个类能使线程中的某个值与保存值的对象关联起来. 118 | 119 | + `ThreadLocal`将特定于线程的值保存在`Thread`对象中,当线程终止后,这些值会作为垃圾回收. 120 | 121 | + `ThreadLocal`变量类似于全局变量,能降低代码的`可重用性`,并在类之间引入隐含的`耦合性`. 122 | 123 | 124 | 125 | ## 四. 不变性 126 | 127 | + 不可变对象: 如果某个对象在被创建后其状态不能被修改,那么这个对象就称为不可变对象. 128 | 129 | + 不可变对象一定是线程安全的 130 | 131 | + #### 当满足以下条件时,对象为不可变的: 132 | 133 | 1. 对象创建后其状态就不能修改. 134 | 2. 对象的所有域都是`final`类型. 135 | 3. 对象是正确创建的(创建期间`this`没有逸出) 136 | 137 | + 不可变对象的内部仍可以使用可变对象来管理他们的状态: 138 | 139 | // 可变对象的基础上构件不可变类 140 | public finall class ThreeStooges{ 141 | private final Set stooges = new HashSet(); 142 | 143 | public ThreeStooges(){ 144 | stooges.add("janke"); 145 | stooges.add("wususu"); 146 | } 147 | public boolean isStooge(String name){ 148 | return stooges.contains(name); 149 | } 150 | } 151 | 152 | 153 | ### Final域: 154 | 155 | + `final`类型的域是不可修改的.
`final`域能确保初始化过程的安全性,从而可以不受限制地访问不可变对象,并在共享这些对象时无须同步. 156 | 157 | > 除非需要更高的可见性,否则所有域都应声明为私有.
除非需要某个域是可变的,否则应将其声明为final. 158 | 159 | ### 使用Volatile类型来发布不可变对象 160 | 161 | + 对于访问和更新多个相关变量时出现的竞争条件问题,可以通过将这些变量全部保存在一个不可变对象中来消除.
162 | 当线程获得了不可变对象的引用后,就不必担心其他线程会修改对象的状态.
163 | 如果要更新这个变量,那么可以创建一个新的容器对象,其他使用原有对象的线程仍会看到对象处于一致的状态. 164 | 165 | // 不可变容器类 166 | class OneValeCache{ 167 | private final BigInteger lastNumber; 168 | private final BigInteger[] lastFactors; 169 | 170 | public OneValueCache(BigInteger i, BigInteger[] factors){ 171 | lastNumber = i; 172 | lastFactors = Arrays.copyOf(factors, factors.length); 173 | } 174 | 175 | public BigInteger[] getFactors(BigInteger i){ 176 | if(lastNumber == nul || !lastNumber.equals(i)) 177 | return null; 178 | else 179 | return Arrays.copyOf(lastFactors, lastFactors.length); 180 | } 181 | } 182 | 183 | public class VolatileCachedFactorizer implements Servlet{ 184 | private volatile OneValueCache cache = new OneValueCache(null, null); 185 | 186 | public void service(ServletRequest req, ServletResponse reso){ 187 | BigInteger i = extractFromRequest(req); 188 | BigInteger[] factors = cache.getFactors(i); 189 | if(factors == null){ 190 | factors = factor(i); 191 | cache = new OneValueCache(i, factors); 192 | } 193 | encodeIntoResponse(resp, factors); 194 | } 195 | } 196 | 197 | + 通过使用包含多个状态变量的容器对象来维持不变性条件,并使用一个volatile类型引用来确保可见性. 198 | 199 | 200 | ## 五. 安全发布 201 | 202 | ### 不可变对象与初始化安全性: 203 | 204 | + 初始化安全性保证: 205 | 206 | 1. 为了确保对象状态能呈现出一致的视图,必须使用同步. 207 | 2. 若不同步,必须满足不可变性的三个要求 208 | 209 | > 在"准备"阶段,如果存在被`final`修饰的域,则一开始就初始化为其指定的值(而不是默认值) 210 | 211 | + 如果`final`类型的域指向的是可变对象,那么在访问这些域所指向的对象的状态时仍需同步. 212 | 213 | ### 安全发布的常用模式: 214 | 215 | + 一个正确构造的对象可以通过以下方式来安全发布: 216 | 217 | 1. 在静态初始化函数中初始化一个对象引用.(静态初始化器) 218 | 2. 将对象的引用保存到`volatile`类型的域或`AtomicReference`对象中. 219 | 3. 将对象的引用保存到某个正确构造对象的`final`类型域中. 220 | 4. 将对象的引用保存到一个由锁保护的域中. 221 | 222 | +要发布一个对象,最简单安全的方式是使用静态的初始化器: 223 | 224 | public static Holder holder = new Holder(42); 225 | 226 | > 静态初始化器由JVM在类初始化阶段执行.由于JVM内部存在同步机制,所以这种方式初始化的任何对象都可以被安全发布. 227 | 228 | 229 | ### 事实不可变对象: 230 | 231 | + 对象在技术上来看是可变的,但其状态在发布后不会再改变,这种对象称为`事实不可变对象(Effectivelt Immutable Object)`.(例如,Date()) 232 | 233 | + 没有而外同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变对象. 234 | 235 | 236 | ### 可变对象: 237 | 238 | + 对象在构造后可以修改,安全发布只能确保"发布当时"的状态可见性. 239 | 240 | + 对于`可变对象`,不仅在发布时需要同步,每次在对象访问时都需要同步来确保后续修改操作的可见性. 241 | 242 | 243 | ### 安全地共享对象: 244 | 245 | 1. 线程关闭: 线程封闭的对象只能由一个线程拥有. 246 | 247 | 2. 只读共享: 在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它.(共享的只读对象包括`不可变对象`和`事实不可变对象`). 248 | 249 | 3. 线程安全共享: 线程安全的对象在其内部实现同步,多个线程可通过对象的共有接口来进行访问. 250 | 251 | 4. 保护对象: 被保护的对象只能通过持有特定的锁来访问.保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象. -------------------------------------------------------------------------------- /Java_Concurrency_in_Practice/对象的组合.md: -------------------------------------------------------------------------------- 1 | # 对象的组合 2 | 3 | ## 一. 设计线程安全的类 4 | 5 | + 将状态封装起来的程序比将所有状态保存在共有静态域的程序更容易验证线程安全性. 6 | 7 | > 设计线程安全类的过程中,需要包含以下三个基本要素: 8 | > 9 | > 1. 找出构成对象状态的所有变量 10 | > 2. 找出约束状态变量的`不变性条件` 11 | > 3. 建立对象状态的`并发访问管理策略` 12 | 13 | + 对于含有N个基本类型域的对象,其状态就是这些域构成的N元组 14 | 15 | + `同步策略(Synchronization Policy)`定义了如何在不违背对象`不变性条件`或`后验条件`的情况下对其状态的访问操作进行协同.同步策略规定了如何将`不可变性`,`线程封闭`与`加锁机制`等结合起来以维护线程的安全性,并且还规定了哪些变量由哪些锁来保护
要确保开发人员可以对这个类进行分析与维护,就必须将同步策略写为正式文档 16 | 17 | 18 | ### 收集同步需求 19 | 20 | + 要确保类的线程安全性,就需要确保它的`不变性条件`不会在并发访问的情况下被破坏,这就需要对其状态进行推断 21 | 22 | + 许多类中都定义了一些不`可变条件`,用于判断状态是否有效(eg:取值范围是否有效)
23 | 在操作中还会包含一些`后验条件`来判断`状态迁移`是否有效(eg:状态改变后的值是否合法) 24 | 25 | > 如果不了解对象的`不变性条件`与`后验条件`,那么就不能确保线程安全性.要满足在状态变量的有`效状态转换上`的各种约束条件,就需要借助与`原子性`与`封装性` 26 | 27 | ### 依赖状态的操作 28 | 29 | + 类的`不变性条件`与`后验条件`约束了在对象上有哪些状态和状态转换是有效的.在某些对象的方法中还包含一些基于状态的`先验条件`
30 | 如果在某个操作中包含基于状态的`先验条件`,那么这个操作就称为`依赖状态的操作`(eg: 空队列中不能移除元素) 31 | 32 | + 在并发程序中要一直等到`先验条件`为真,然后才执行该操作. 33 | 34 | + 要想实现某个等待`先验操作`为真才执行的操作,更简单的方法是通过现有库中的类 35 | 36 | ### 状态的所有权 37 | 38 | + 所有权与封装性总是相互关联的: 39 | 40 | 对象封装它拥有的状态,拥有它封装的状态所有权
41 | 状态变量的所有者将决定采用何种加锁协议来维持变量状态的完整性
42 | 43 | + 如果发布了某个可变对象的引用,那么就不再拥有对其独占的控制权,最多是`共享控制权` 44 | 45 | + 容器类通常表现出一种"所有权分离"的形式,容器类拥有其自身的状态,客户端代码则拥有容器中各个对象的状态. 46 | 47 | ## 二. 实例封闭 48 | 49 | + 封装简化了线程安全类的实现过程,它提供了一种`实例封闭机制(Instance Confinement)`
50 | 当一个对象被封装到另外一个对象中时,能够访问被封装对象的所有代码路径都是已知的.
51 | 与对象可由整个程序访问相比,更易于对代码进行分析 52 | 53 | > 将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁 54 | 55 | + 被封闭对象一定不能超出它们既定的作用域.对象可封闭在类的一个实例(对象私有成员),某个作用域(就不变量)或线程内. 56 | 57 | + 发布一个本该封闭的对象会破坏`封闭性`
58 | 发布其他对象(迭代器或内部类实例),也可能会间接地发布被封闭对象,造成其`逸出` 59 | 60 | > `封闭机制`更易于构造线程安全的类,因为当封闭类的状态时,在分析类的`线程安全性`时就无须检查整个程序 61 | 62 | ### Java监视器模式 63 | 64 | + 遵循`Java监视器模式`的对象会把对象的所有可变状态都封装起来,并由对象自己的内置锁来保护. 65 | 66 | + `Java监视器模式`仅仅是一种编写代码的约定,对于任何一种锁对象,只要自始至终都使用该锁对象,都可以用来保护对象的状态. 67 | 68 | //通过一个私有锁来保护状态 69 | public class PrivateLock{ 70 | private final Object myLock = new Object(); 71 | Widget widget; 72 | 73 | void someMethod(){ 74 | synchronized(myLock){ 75 | //访问或修改Widget的状态 76 | } 77 | } 78 | } 79 | 80 | 81 | 82 | + 使用私有锁对象的好处: 83 | 84 | 私有的锁对象可以将锁封装起来,使客户端代码无法得到锁,但客户端代码可以通过方法来访问锁,以便(正确或不正确)参与到它的同步策略中
85 | 如果客户端代码错误地获取到另一个对象的锁,那么可能产生`活跃性问题` 86 | 87 | 88 | ## 三. 线程安全性的委托 89 | 90 | + 在某些情况下,通过多个线程安全类组合而成的类是线程安全的
91 | 而在某些情况下,这仅仅是一个好的开端. 92 | 93 | + 不可变的值可以被自由地共享与发布 94 | 95 | ### 独立的状态变量 96 | 97 | + 可以将线程安全性`委托`给多个状态变量,只要这些变量是彼此独立的,即组合而成的类并不会在其包含的多个状态变量上增加任何不变性条件 98 | 99 | ### 委托失败 100 | 101 | + 如果组合对象的状态变量之间存在着某些不变性条件(复合操作),那么仅靠`委托`并不足以实现线程安全性
102 | 这种情况下,这个类必须提供自己的加锁机制以保证这些`复合操作`都是`原子操作`,除非整个`复合操作`都可以`委托`给状态变量 103 | 104 | > 如果一个类是由多个独立且线程安全的状态变量组成,并且在所有的操作中都不包含无效状态转换,那么可以将线程安全性`委托`给底层状态变量 105 | 106 | ### 发布底层的状态变量 107 | 108 | > 如果一个状态变量是线程安全的,并且没有任何不变性条件来约束它的值,在变量的操作上也不存在任何不允许的状态转换,那么就可以安全地发布这个变量 109 | 110 | ## 四. 现有的线程安全类中添加功能 111 | 112 | ### 1. 最安全的方法,修改原始的类 113 | 114 | ### 2. 扩展这个类,(假定设计这个类时考虑了可扩展性 115 | 116 | public class BetterVector extends Vector{ 117 | public synchronized boolean putIfAbsent(E x){ 118 | boolean absent = !contains(x); 119 | if(absent) 120 | add(x); 121 | return absent; 122 | } 123 | } 124 | 125 | + "扩展"方法比第一种(修改原始类)更加脆弱,因为`同步策略实现`被分不到多个单独维护的源代码文件中.
126 | 如果底层改变了`同步策略`并选择了不同的锁来保护它的状态变量,那么子类会被破坏,因为在`同步策略`改变后它无法再使用正确的锁来控制对基类状态的`并发访问`. 127 | 128 | ### 3. 客户端加锁机制: 129 | 130 | + 扩展类的功能(并不是扩展类本身,而是将扩展代码放入"辅助类"中) 131 | 132 | //通过客户端代码加锁 133 | public class ListHelper{ 134 | public List list = Collections.synchronizedList(new ArrayList()); 135 | 136 | public boolean putIfAbsent(E x){ 137 | synchronized(list){ 138 | boolean absent = !list.contains(x); 139 | if(absent) 140 | list.add(x); 141 | return absent; 142 | } 143 | } 144 | } 145 | 146 | + 通过添加一个原子操作来扩展类是脆弱的,因为它的加锁代码分不到多个类中.
147 | 然而,通过`客户端加锁`更加脆弱,因为它将一个类的加锁代码放到与这个类完全无关的其他类中. 148 | 149 | ### 4. 组合 150 | 151 | + 通过将将List对象的操作`委托`给底层List对象实例来实现List的操作,同时添加新的`原子操作`. 152 | 153 | public class ImprovedList implements List{ 154 | private final List list; 155 | 156 | public ImproveList(List list)[ 157 | this.list = list; 158 | ] 159 | 160 | public synchronized boolean putIfAbsent(T x){ 161 | boolean contains = list.contains(x); 162 | if(contains) 163 | list.add(x); 164 | return !contains; 165 | } 166 | 167 | public synchronized void clear(){ 168 | list.clear(); 169 | } 170 | //.... 按照类似操作委托List的其他方法 171 | } 172 | 173 | + 虽然额外的同步可能导致轻微的`性能损失`,但与模拟另一个对象的`加锁策略`相比,组合更为健壮. 174 | 175 | ## 五. 将同步策略文档化 176 | 177 | + 在维护线程安全时,文档是最强大的工具之一.
178 | 用户通过查阅文档来判断某个类是否线程安全的,而维护人员也可以通过查阅文档来理解其中的实现策略,避免在维护过程中破坏安全性. 179 | 180 | > 在文档中说明客户代码需要了解的线程安全性保证,以及代码维护人员需要了解的同步策略 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Notes 我的笔记: 2 | 3 | 4 | 5 | :octocat: 知识不总结,就会被大脑当垃圾清理 6 | 7 | :racehorse:求知之路必然不会一帆风顺,唯有以勤为刃,以习惯为马,劈懒斩愚,跨越舒适区,方能知众人所不知,晓众人所不晓。 8 | 9 | :walking:勤于思考的人,就算散步也是修行 10 | 11 | 12 | ## Java知识点 13 | 14 | ### 其他 15 | 16 | + [MapReduce](./Distributed_Systems(MIT)/MapReduce.md) 17 | + [双重检查锁定漏洞分析](双重检查锁定漏洞分析笔记.md) 18 | + [ThreadPoolExecutor源码解析(jdk8版)](ThreadPoolExecutor源码解析(jdk8版).md) 19 | + [HashMap源码解析(jdk8版)](HashMap源码解析(jdk8).md) 20 | + [ReentrantLock源码解析(jdk8版)](ReentrantLock源码解析(jdk8).md) 21 | + [面向对象设计六大原则](面向对象设计六大原则.md) 22 | + [Storm数据流分组策略](./Storm/storm-grouping.md) 23 | 24 | ### 小试分布式系统 25 | 26 | + [Raft算法](./分布式系统/Raft.md) 27 | 28 | ### Redis 29 | 30 | + [Redis持久化与复制](./Redis设计原理/Redis持久化与复制.md) 31 | + [Redis集群模式](./Redis设计原理/Redis集群.md) 32 | 33 | ### 深入理解Java虚拟机 34 | 35 | + [Java内存区域划分与对象新建过程](./深入理解Java虚拟机/Java内存区域划分.md) 36 | + [jvm垃圾收集机制与内存分配策略](./深入理解Java虚拟机/jvm垃圾收集与内存回收策略.md) 37 | + [jvm类加载机制](./深入理解Java虚拟机/jvm类加载机制.md) 38 | + [Java的内存模型](./深入理解Java虚拟机/Java内存模型.md) 39 | + [锁优化](./深入理解Java虚拟机/锁优化.md) 40 | 41 | ### Think In Java 42 | 43 | + [Java容器](./ThinkInJava/Java容器.md) 44 | + [Java并发](./ThinkInJava/Java并发.md) 45 | 46 | ### Java Concurrency in Practice 47 | 48 | + [对象的共享](./Java_Concurrency_in_Practice/对象的共享.md) 49 | + [对象的组合](./Java_Concurrency_in_Practice/对象的组合.md) 50 | + [基础构建模块](./Java_Concurrency_in_Practice/基础构件模块.md) 51 | 52 | ### JavaGC监控与优化 53 | 54 | + [垃圾回收机制](http://www.importnew.com/1993.html) 55 | + [垃圾回收机制的监控](http://www.importnew.com/2057.html) 56 | + [优化垃圾回收机制](http://www.importnew.com/3146.html) 57 | + [Apache的MaxClients参数详解及其在Tomcat执行FullGC时的影响](http://www.importnew.com/3151.html) 58 | 59 | ## JavaEE 60 | 61 | + [浅析Web容器](./JavaEE/浅析Web容器.md) 62 | 63 | ## 计算机网络 64 | 65 | + [UDP&TCP](./Computer_Networks/UDP&TCP.md) 66 | 67 | ## DB 68 | 69 | + [MySQL索引背后的数据结构及算法原理(干货)](http://blog.codinglabs.org/articles/theory-of-mysql-index.html) 70 | 71 | ## 设计模式 72 | 73 | 行为型模式 74 | 75 | --- 76 | 77 | + [模板方法](./设计模式/模板方法.md) 78 | + [策略模式](./设计模式/策略模式.md) 79 | 80 | 81 | 结构型模式 82 | 83 | --- 84 | + [适配器模式](./设计模式/适配器模式.md) 85 | 86 | 创建型模式 87 | 88 | --- 89 | 90 | + [简单工厂模式](./设计模式/简单工厂模式.md) 91 | + [工厂方法模式](./设计模式/工厂方法模式.md) 92 | + [抽象工厂模式](./设计模式/抽象工厂模式.md) 93 | + [建造者模式](./设计模式/建造者模式.md) 94 | + [单例模式](./设计模式/单例模式.md) 95 | 96 | ## 面经 97 | 98 | + [唯品会Java金融爬虫实习(offer)](./面经/1.md) 99 | + [腾讯春招实习-WEB前端(不要问我为什么是前端)](./面经/2.md) 100 | + [虎牙春招实习-JAVA研发大数据方向(offer)](./面经/huya.md) -------------------------------------------------------------------------------- /Redis设计原理/Redis持久化与复制.md: -------------------------------------------------------------------------------- 1 | # Redis持久化与复制 2 | 3 | ## Redis持久化 4 | 5 | ### 一.RDB持久化 6 | 7 |  将当前数据库状态(当前保存的键值对)做一个快照,生成一个经过压缩的二进制文件,通过该文件可还原到生成RDB文件时的数据库状态. 8 | 9 | 10 | ### 二.AOF持久化 11 | 通过保存Redis服务器所执行的写命令生成AOF文件来记录数据库状态 12 | 13 | + 实现方式: 14 | 15 | 1. 命令追加: 执行完一个写命令后,以协议格式将写命令追加到aof_buf缓冲区末尾 16 | 2. AOF文件的写入与同步: 17 | 18 | + 通过appendfsync的值决定写入与同步方式: 19 | 20 | 1. always: 服务器在每个事件循环都要将aof_buf缓冲区中所有内容写入AOF文件,并且同步AOF文件 21 | 2. everysec: 每个事件循环都要将aof_buf缓冲区中所有内容写入AOF文件,每隔一秒在子线程中对AOF文件进行一次同步 22 | 3. no: 每个事件循环都将aof_buf中的内容写入AOF文件,由操作系统控制何时同步 23 | 24 | 3. AOF重写: 通过创建一个简化写命令的AOF文件来代替现有AOF文件,解决AOF文件体积膨胀的问题 25 | 26 | 27 | ### 三.复制 28 | 29 | + Redis的复制功能分为同步(Sync)和命令传播(command propagate 30 | 31 | #### 1. 同步: 32 | 33 | 1. 从服务器向主服务器发送SYNC命令 34 | 35 | 2. 主服执行BFSAVE在后台生成一个RDB文件,并在缓冲区记录从此刻开始的所有写命令 36 | 37 | 3. 从服务器接收并载入这个RDB文件,将服务器更新到主服务器执行BGSAVE时的状态 38 | 39 | 4. 主服务器将缓冲区的写命令发送给从服,从服执行写命令 40 | 41 | * ### 新版复制功能: 同步分为完整重同步与部分重同步: 42 | 43 | + 完整重同步: 与旧版同步一样,用于处理初次复制的情况. 44 | + 部分重同步: 断线重连后,主服将连接断开期间执行的写命令发送给从服务器,从服务器接收并执行这些写命令,主从恢复一致状态. 45 | 46 | 47 | #### 2. 命令传播: 48 | 49 | 同步之后主从达到一致状态,当主服执行客户端的写命令时,主从状态不再一致. 50 | 51 | + 命令传播操作: 52 | 53 | 主服务器将自己执行的写命令发送给从服务器执行,主从服务器再次回到不一致状态 54 | 55 | -------------------------------------------------------------------------------- /Redis设计原理/Redis集群.md: -------------------------------------------------------------------------------- 1 | # Redis集群 2 | 3 | Sentinel 是HA(高可用)解决方案, Cluster与客户端分片是sharding(分布式数据库)方案 4 | 5 | ## 一. Sentinel模式 6 | 7 | + ### 什么是哨兵(Sentinel): 8 | 9 | 由一个或多个Sentinel实例组成的Sentinel系统可以监视(十秒发送一个INFO心跳)多个主服务器,以及这些主服务器属下的所有从服务器.
10 | 在被监视的主服务器进入下线状态时(因为一个主服务器负责本集群的一个Redis分片/一部分槽),哨兵们会从下线的主服务器属下的从服务器中挑选一个升级为新的主服务器.这个过程也叫故障转移操作. 11 | 12 | + Sentinel本质上是一个运行在特殊模式下的Redis服务器 13 | 14 | + ### 下线状态检测: 15 | + 主观下线: 当主服务器master连续down-after-milliseconds毫秒都向某个哨兵放回无效回复时,该哨兵就会将master标记为主观下线状态 16 | + 客观下线: 一个哨兵将主服务器判断为主观下线后,向其他同样检测这个服务器的其他哨兵发送询问,若收到的主观下线数量达到阀值,该哨兵就将主服务器判断为客观下线并发送广播. 17 | 18 | + ### 选举Sentinel领头(RAFT算法): 19 | + 当一个主服务器被判断为客观下线后,监视这个服务器的所有Sentinel会选举出一个领头,由领头对其进行故障转移操作. 20 | 21 | 1. 所有监视该主服务器的在线Sentinel都有被选举为领头的资格 22 | 23 | 2. 进行一轮选举后,无论成功与否,所有Sentinel的配置纪元都要自增
24 | 配置纪元为一个计数器,用于防止不同轮次选举相互影响 25 | 26 | 3. 所有Sentinel都有一次将除自己之外的某个Sentinel选为设为领头的机会. 27 | 28 | 4. 每个Sentinel都会要求其他Sentinel将自己设为局部领头. 29 | 30 | 5. 设置局部领头的规则为先到先得,最先向目标Sentinel发送请求的源Sentinel将会成为模板Sentinel的局部领头.后来的都会被拒绝. 31 | 32 | 6. 如果存在一个Sentinel被半数以上的Sentinel设为局部领头,则这个Sentinel将成为领头,否则在一段时间后重新进行下一轮选举,直到选出领头为止. 33 | 34 | 35 | + ### 哨兵对主服务器Server的故障转移操作: 36 | 37 | 1. 挑选Server的一个从服务器升级为新的主服务器 38 | 2. 将Server属下的从服务器成为新主服务器的从服务器 39 | 3. 监视已下线的Server,重新上线时设置为新主服务器的从服务器 40 | 41 | + ### 领头Sentinel怎么挑选新的主服务器: 42 | 43 | + 根据优先级高, 复制偏移量大(即保存的数据最新), 运行ID小, 依次过滤选出从服务器作为新的主服务器 44 | 45 | ## 二. Redis Cluster模式(服务端分片) 46 | 47 | 集群通过分片来进行数据共享,并提供复制和故障转移功能 48 | 49 | + ### 集群数据结构: 50 | 51 | #### 1. ClusterNode: 每个节点都会用一个clusterNode来记录自己的状态.
并为集群中的所有其他节点都创建一个相应的clusterNode结构来记录其他节点的状态. 52 | 53 | struct clusterNode{ 54 | // 创建节点的时间 55 | mstime_t ctime; 56 | 57 | // 节点名字 58 | char name[REDIS_CLUSTER_NAMELEN]; 59 | 60 | // 节点标识: 记录节点角色(主从)与状态(上下线) 61 | int flag; 62 | 63 | // 节点当前的配置纪元 64 | uint64_t configEpoch; 65 | 66 | // 节点 ip 67 | char ip[REDIS_IP_STR_LEN]; 68 | 69 | // 节点端口 70 | int port; 71 | 72 | // 二进制数组,用于记录节点处理的槽 73 | unsigned char slots[16384/8]; 74 | 75 | // 节点处理槽的数量 76 | int numsoltsl; 77 | 78 | ...... 79 | } 80 | 81 | #### 2. clusterState: 每个节点都保存着一个clusterState结构,记录当前节点的视角下,集群目前所处的状态: 82 | 83 | typeof struct clusterState { 84 | 85 | // 指向当前节点 86 | clusterNode *myself; 87 | 88 | // 集群当前的配置纪元 89 | uint64_t currentEpoch; 90 | 91 | // 集群当前状态: 上下线 92 | int state; 93 | 94 | // 集群中处理着槽的节点数量 95 | int size; 96 | 97 | // 集群所有节点名单 98 | // key: 节点名 99 | // value: clusterNode 100 | dict *nodes; 101 | 102 | // 数组的元素用于指向clsuterNode结构的指针 103 | // 代表该槽已经指派给该结构所代表的节点 104 | clusterNode *slots[16384]; 105 | 106 | ... 107 | } 108 | 109 | + ### 集群添加节点: 110 | 111 | #### 节点A将节点B添加到A当前所在集群的握手流程: 112 | 113 | 1. A节点为B节点创建一个clusterNode结构,并将其添加到自己的clusterState.nodes字典中 114 | 2. 节点A根据客户端命令的IP与端口号,向节点B发送MEET消息 115 | 3. 节点B收到后为节点A创建一个clusterNode结构,添加到自己的clusterState.nodes中,并返回PONG消息 116 | 4. 节点A收到PONG后发送PING消息,握手完成 117 | 118 | + ### 槽的分派: 119 | 120 | + 集群的整个数据库被分为16384个槽(solt),数据库中的每个键都属于这16384个槽中的一个,集群中的每个节点可以处理0或最多16384个槽. 121 | + 当每个槽都有节点在处理时,集群处于上线状态; 相反,如果有一个槽没得到处理,集群处于下线状态 122 | 123 | + ### 客户端与集群的交互: 124 | 125 | 对客户端来说,整个cluster被看做是一个整体,客户端可以连接任意一个节点进行操作,就像操作单一Redis实例一样,当客户端操作的key没有分配到该节点上时,Redis会返回转向指令,指向正确的节点. 126 | 127 | + ### 故障检查: 128 | 129 | + 集群中的每个节点都会定期地向集群中的其他节点发送PING消息,检测对方是否在线,如果没有在规定时间回复PONG消息,那么节点就会将其标记为疑似下线. 130 | + 集群中的每个节点都会通过相互发送消息的方式来交换集群中各个节点的状态信息: 在线, 疑似下线, 已经下线. 131 | + 如果有超过半数的处理槽的主节点将某个主节点X标记为疑似下线,则这个节点将被最后一个标记的节点标记为已下线,并发送广播 132 | 133 | + ### 故障转移: 134 | 135 | 如果某个节点发现自己的正在复制的主节点已经进入下线状态,开始故障转移操作: 136 | 1. 在该下线节点的从节点中选出一个节点称为新的主节点 137 | 2. 新主节点撤销下线节点指派的槽,将其指派给自己 138 | 3. 向集群发送PONG消息广播,告知自己已经称为主节点并处理哪些槽 139 | 140 | + ### 主节点选举操作(RAFT领头选举算法): 141 | 142 | 1. 集群的配置纪元是一个自增计数器,初始值为0,每经过一次选举,都会增一 143 | 2. 在一个纪元内,每个负责处理槽的主节点都有一次投票的机会,第一个向主节点要求投票的从节点将获得投票 144 | 3. 当从节点发现自己的主节点下线后会广播一条消息,要求所有具有投票权的节点向其投票. 145 | 4. 当一个节点收到超过一半的票数时,该节点升级为主节点.否则进入下一个纪元,重新开始投票,直至选出新主节点 146 | 147 | 148 | ## 三. Redis sharing(客户端分片, Jedis实现) 149 | 150 | + 采用一致性哈希算法(consistent hashing),将key和节点name同时hashing,然后进行映射匹配,采用的算法是MURMUR_HASH。采用一致性哈希而不是采用简单类似哈希求模映射的主要原因是当增加或减少节点时,不会产生由于重新匹配造成的rehashing。一致性哈希只影响相邻节点key分配,影响量小。 151 | 152 | + 为了避免一致性哈希只影响相邻节点造成节点分配压力,ShardedJedis会对每个Redis节点根据名字(Jedis会赋予缺省名字)会虚拟化出160个虚拟节点进行散列。根据权重weight,也可虚拟化出160倍数的虚拟节点。用虚拟节点做映射匹配,可以在增加或减少Redis节点时,key在各Redis节点移动再分配更均匀,而不是只有相邻节点受影响。 -------------------------------------------------------------------------------- /ReentrantLock源码解析(jdk8).md: -------------------------------------------------------------------------------- 1 | # ReentrantLock 源码解析 2 | 3 | ReentrantLock锁的实现基于AQS同步器: 4 | 5 | AQS维护一个volatile的int型state变量
6 | AQS有条双向队列存放阻塞的等待线程,并提供一系列判断和处理方法 7 | 8 | + state是独占的,还是共享的; 9 | + state被获取后,其他线程需要等待; 10 | + state被释放后,唤醒等待线程; 11 | 12 | 只有一个实例变量sync提供锁的功能,在构造函数中初始化: 13 | 14 | private final Sync sync; 15 | 16 | sync有两种实例对象:FairSync(公平锁)和NofairSync(非公平锁),二者及其抽象类Sync都是继承于AQS以及RentrantLock的内部类。 17 | 18 | ## 加锁 19 | 20 | ReentrantLock 通过lock()加锁: 21 | 22 | public void lock() { 23 | sync.lock(); 24 | } 25 | 26 | case 1. 如果这个锁没有被任何线程持有,立即锁定并设置count(持锁计数)为1,方法立即返回 27 | 28 | case 2. 如果当前线程已经持有这个锁,count+1,方法立即返回 29 | 30 | case 3. 如果这个锁被其他线程持有,那当前线程立即休眠停止调度,直到这个锁可以被获得(count被设为0) 31 | 32 | `lock()`方法内部是通过sync.lock()来实现加锁,即根据不同的加锁实现来执行不同的加锁策略(公平or非公平) 33 | 34 | FairSync和NofairSync内的lock()方法都调用了AQS的acquire(1)
35 | 36 | 非公平因为不用考虑顺序情况,在state为0(无锁状态)时可直接cas尝试抢占state,失败的话,执行acquire()尝试加锁,否则阻塞线程 37 | 38 | 公平锁执行acquire()尝试加锁 39 | 40 | // NofairLock 41 | final void lock() { 42 | // case 1 43 | if (compareAndSetState(0, 1)) 44 | setExclusiveOwnerThread(Thread.currentThread()); 45 | // case 2/3 46 | else 47 | acquire(1); 48 | } 49 | 50 | 51 | // FairLock 52 | final void lock() { 53 | //case 2/3 54 | acquire(1); 55 | } 56 | 57 | AQSDEacquire()方法中先调用实现类的tryAcquire()方法尝试获取锁,失败后将线程包装为Node并加入双端队列(addWriter),最后执行acquireQueued()挂起当前线程 58 | 59 | #### Node中分别有: 60 | 61 | + pre: 指向上一个等待线程Node 62 | + tail: 指向下一个等待线程Node 63 | + thread: 存放线程实例 64 | + waitStatus: 线程状态 65 | + SIGNAL=-1 等待被唤醒(前一个Node释放锁后就会通知当前结点) 66 | + CANCELLED=1 已经结束(在同步队列中等待超时或者被中断) 67 | + CONDITION=-2 条件状态(在等待队列中等待,注意:等待队列与同步队列不是同一个概念) 68 | + PROPAGATE=-3 共享模式下,同步状态会被传播(不用管,ReentrantLock用不到) 69 | 70 | 71 | // AQS 72 | public final void acquire(int arg) { 73 | if (!tryAcquire(arg) && 74 | acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 75 | selfInterrupt(); 76 | } 77 | 78 | 79 | + 公平锁的尝试加锁逻辑: 80 | 81 | 如果state还不是被占有状态(锁未被占有),且当前线程就是第一个等待锁的线程,则尝试获得锁(设置state),并将当前线程设为占有锁的线程 82 | 如果state不为0,锁已经被占有,判断是否被当前线程占有,是则state计数+1 83 | 84 | /** 85 | * Fair version of tryAcquire. Don't grant access unless 86 | * recursive call or no waiters or is first. 87 | */ 88 | protected final boolean tryAcquire(int acquires) { 89 | final Thread current = Thread.currentThread(); 90 | int c = getState(); 91 | if (c == 0) { 92 | if (!hasQueuedPredecessors() && 93 | compareAndSetState(0, acquires)) { 94 | setExclusiveOwnerThread(current); 95 | return true; 96 | } 97 | } 98 | else if (current == getExclusiveOwnerThread()) { 99 | int nextc = c + acquires; 100 | if (nextc < 0) 101 | throw new Error("Maximum lock count exceeded"); 102 | setState(nextc); 103 | return true; 104 | } 105 | return false; 106 | } 107 | 108 | 109 | + 非公平锁的尝试加锁逻辑: 110 | 111 | 跟公平锁加锁逻辑类似,只不过少了检查是否有等待当前锁的线程 112 | 113 | protected final boolean tryAcquire(int acquires) { 114 | return nonfairTryAcquire(acquires); 115 | } 116 | 117 | /** 118 | * Performs non-fair tryLock. tryAcquire is implemented in 119 | * subclasses, but both need nonfair try for trylock method. 120 | */ 121 | final boolean nonfairTryAcquire(int acquires) { 122 | final Thread current = Thread.currentThread(); 123 | int c = getState(); 124 | if (c == 0) { 125 | if (compareAndSetState(0, acquires)) { 126 | setExclusiveOwnerThread(current); 127 | return true; 128 | } 129 | } 130 | else if (current == getExclusiveOwnerThread()) { 131 | int nextc = c + acquires; 132 | if (nextc < 0) // overflow 133 | throw new Error("Maximum lock count exceeded"); 134 | setState(nextc); 135 | return true; 136 | } 137 | return false; 138 | } 139 | 140 | + acquireQueued() 用于管理同步队列中的等待线程,进行线程的挂起 141 | 142 | final boolean acquireQueued(final Node node, int arg) { 143 | boolean failed = true; 144 | try { 145 | boolean interrupted = false; 146 | // 注意:在自旋期间当前线程可能会被唤醒, 147 | for (;;) { 148 | // 获取该结点的前一个结点,若前一个结点是头结点,那么这个结点就可以开始尝试去争抢锁 149 | final Node p = node.predecessor(); 150 | if (p == head && tryAcquire(arg)) { 151 | 争抢成功(头结点线程被释放),把当前结点设为头结点,返回false 152 | setHead(node); 153 | p.next = null; // help GC 154 | failed = false; 155 | return interrupted; 156 | } 157 | // 判断当前线程是否需要挂起,若waitStatus(Node/线程状态)为 158 | `SIGNAL`挂起, 159 | `CANCELLED`在同步队列中移除当前结点 160 | 否则将其设为`SIGNAL`在下个自选周期中挂起线程 161 | if (shouldParkAfterFailedAcquire(p, node) && 162 | // 该方法挂起线程 163 | parkAndCheckInterrupt()) 164 | // 当被唤醒后获取当前线程interrput状态(判断是被唤醒还是中断操作),是则设interrupt为true,在外部方法中断当前线程 165 | interrupted = true; 166 | } 167 | } finally { 168 | if (failed) 169 | cancelAcquire(node); 170 | } 171 | } 172 | 173 | + 至此加锁结束 174 | 175 | ## 解锁 176 | 177 | + 公平锁与非公平锁使用的都是同一种加锁操作 178 | 179 | 调用AQS自带的release()方法: 180 | 181 | // RL 182 | public void unlock() { 183 | sync.release(1); 184 | } 185 | 186 | 使用ReentrantLock自定义的解锁逻辑解锁,解锁成功后获取当前等待的线程头,解除一个等待的Node线程 187 | 188 | // AQS 189 | public final boolean release(int arg) { 190 | if (tryRelease(arg)) { 191 | Node h = head; 192 | if (h != null && h.waitStatus != 0) 193 | unparkSuccessor(h); 194 | return true; 195 | } 196 | return false; 197 | } 198 | 199 | tryRelease通过state的大小判断当前解锁的线程是不是最后一个锁,是则释放当前锁的占有线程并返回true 200 | 201 | // RL 202 | protected final boolean tryRelease(int releases) { 203 | int c = getState() - releases; 204 | if (Thread.currentThread() != getExclusiveOwnerThread()) 205 | throw new IllegalMonitorStateException(); 206 | boolean free = false; 207 | if (c == 0) { 208 | free = true; 209 | setExclusiveOwnerThread(null); 210 | } 211 | setState(c); 212 | return free; 213 | } 214 | 215 | 这段代码作用是唤醒同步队列中第一个非CANCELLED状态的结点的阻塞线程.
216 | 故,即使使用非公平锁,一旦线程获取不到锁进入同步队列,之后的唤醒也是按顺序的,只是唤醒后不一定能拿到锁,需要跟新的线程竞争.
217 | 而公平锁按顺序唤醒,也按顺序获取锁 218 | 219 | // AQS 220 | private void unparkSuccessor(Node node) { 221 | /* 222 | * If status is negative (i.e., possibly needing signal) try 223 | * to clear in anticipation of signalling. It is OK if this 224 | * fails or if status is changed by waiting thread. 225 | */ 226 | int ws = node.waitStatus; 227 | if (ws < 0) 228 | compareAndSetWaitStatus(node, ws, 0); 229 | 230 | /* 231 | * Thread to unpark is held in successor, which is normally 232 | * just the next node. But if cancelled or apparently null, 233 | * traverse backwards from tail to find the actual 234 | * non-cancelled successor. 235 | */ 236 | Node s = node.next; 237 | if (s == null || s.waitStatus > 0) { 238 | s = null; 239 | for (Node t = tail; t != null && t != node; t = t.prev) 240 | if (t.waitStatus <= 0) 241 | s = t; 242 | } 243 | if (s != null) 244 | LockSupport.unpark(s.thread); 245 | } 246 | 247 | 248 | ## Condition原理 249 | 250 | 一个Condition有一个等待队列,一个AQS有一个同步队列,所以一个基于AQS的锁可以有多个等待队列 251 | 252 | newCondition()方法实例化且返回AQS的ConditionObject 253 | 254 | // ReentrantLock 255 | final ConditionObject newCondition() { 256 | return new ConditionObject(); 257 | } 258 | 259 | ConditionObject的await()方法, 挂起当前线程: 260 | 261 | // ConditionObject 262 | public final void await() throws InterruptedException { 263 | if (Thread.interrupted()) 264 | throw new InterruptedException(); 265 | // 将当前线程包装加入这个Condition的等待队列 266 | Node node = addConditionWaiter(); 267 | // 释放当前线程对锁的持有,唤醒后继线程 268 | int savedState = fullyRelease(node); 269 | int interruptMode = 0; 270 | // 当前线程是否在同步队列中,否说明线程是活跃状态,需要挂起 271 | while (!isOnSyncQueue(node)) { 272 | LockSupport.park(this); 273 | if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) 274 | break; 275 | } 276 | // 被唤醒后尝试获取锁 277 | if (acquireQueued(node, savedState) && interruptMode != THROW_IE) 278 | interruptMode = REINTERRUPT; 279 | // 清理等待队列中不为CONDITION状态(即CANCELLED)的结点 280 | if (node.nextWaiter != null) // clean up if cancelled 281 | unlinkCancelledWaiters(); 282 | if (interruptMode != 0) 283 | reportInterruptAfterWait(interruptMode); 284 | } 285 | 286 | ConditionObject的signal()方法, 唤醒一个线程: 287 | 288 | // 取出等待队列中第一个线程唤醒 289 | public final void signal() { 290 | if (!isHeldExclusively()) 291 | throw new IllegalMonitorStateException(); 292 | Node first = firstWaiter; 293 | if (first != null) 294 | doSignal(first); 295 | } 296 | 297 | // 将要唤醒的线程结点的后继设为null,即将其移出等待队列 298 | // transferForSignal(node)进行cas唤醒,若失败,则说明被其他线程唤醒了,重新取下一个线程进行唤醒 299 | // 若取到最后一个线程结点,就把尾结点指向设为null 300 | private void doSignal(Node first) { 301 | do { 302 | if ( (firstWaiter = first.nextWaiter) == null) 303 | lastWaiter = null; 304 | first.nextWaiter = null; 305 | } while (!transferForSignal(first) && 306 | (first = firstWaiter) != null); 307 | } 308 | 309 | // 唤醒一个线程 310 | final boolean transferForSignal(Node node) { 311 | /* 312 | * If cannot change waitStatus, the node has been cancelled. 313 | */ 314 | if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) 315 | return false; 316 | 317 | /* 318 | * Splice onto queue and try to set waitStatus of predecessor to 319 | * indicate that thread is (probably) waiting. If cancelled or 320 | * attempt to set waitStatus fails, wake up to resync (in which 321 | * case the waitStatus can be transiently and harmlessly wrong). 322 | */ 323 | // 将唤醒的结点加入同步队列末端,并放回其前继结点 324 | Node p = enq(node); 325 | int ws = p.waitStatus; 326 | // 若前继结点无法设为SIGNAL状态时,唤醒线程 327 | if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) 328 | LockSupport.unpark(node.thread); 329 | return true; 330 | } -------------------------------------------------------------------------------- /Spring揭秘/1.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wususu/Notes/c566a18c29c64943f6043b7580536693ed68f13d/Spring揭秘/1.md -------------------------------------------------------------------------------- /Storm/pic/2018-3-2.1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wususu/Notes/c566a18c29c64943f6043b7580536693ed68f13d/Storm/pic/2018-3-2.1.jpg -------------------------------------------------------------------------------- /Storm/storm-grouping.md: -------------------------------------------------------------------------------- 1 | ## 数据流组 2 | 3 | #### 1. Shuffle Grouping :随机分组,尽量均匀分布到下游Bolt中 4 | 5 | 将流分组定义为混排。这种混排分组意味着来自Spout的输入将混排,或随机分发给此Bolt中的任务。shuffle grouping对各个task的tuple分配的比较均匀。 6 | 7 | #### 2. Fields Grouping :按字段分组,按数据中field值进行分组;相同field值的Tuple被发送到相同的Task(一个Bolt内运行多个Task) 8 | 9 | 这种grouping机制保证相同field值的tuple会去同一个task,这对于WordCount来说非常关键,如果同一个单词不去同一个task,那么统计出来的单词次数就不对了。“if the stream is grouped by the “user-id” field, tuples with the same “user-id” will always go to the same task”. —— 小示例 10 | 11 | 12 | 1. emit发出的值第一次由哪个task实例处理是随机的,此后再次出现这个值,就固定由最初处理他的那个task实例再次处理,直到topology结束 13 | 14 | 2. 一个task实例可以处理多个emit发出的值 15 | 16 | 3. 和shuffle Grouping的区别就在于,当emit发出同样的值时,处理他的task是随机的 17 | 18 | 19 | #### 3. All grouping :广播 20 | 21 | 广播发送, 对于每一个tuple将会复制到每一个bolt中处理。 22 | 23 | #### 4. Global grouping :全局分组,Tuple被分配到一个Bolt中的一个Task,实现事务性的Topology。 24 | 25 | Stream中的所有的tuple都会发送给同一个bolt任务处理,所有的tuple将会发送给拥有最小task_id的bolt任务处理。 26 | 27 | ##### 5. None grouping :不分组 28 | 29 | 不关注并行处理负载均衡策略时使用该方式,目前等同于shuffle grouping,另外storm将会把bolt任务和他的上游提供数据的任务安排在同一个线程下。 30 | 31 | ##### 6. Direct grouping :直接分组 指定分组 32 | 33 | 由tuple的发射单元直接决定tuple将发射给那个bolt,一般情况下是由接收tuple的bolt决定接收哪个bolt发射的Tuple。 34 | 这是一种比较特别的分组方法,用这种分组意味着消息的发送者指定由消息接收者的哪个task处理这个消息。 35 | 只有被声明为Direct Stream的消息流可以声明这种分组方法。而且这种消息tuple必须使用emitDirect方法来发射。 36 | 消息处理者可以通过TopologyContext来获取处理它的消息的taskid (OutputCollector.emit方法也会返回taskid)。 37 | 38 | ![](pic/2018-3-2.1.jpg) 39 | -------------------------------------------------------------------------------- /ThinkInJava/Java容器.md: -------------------------------------------------------------------------------- 1 | # Java容器: 2 | 3 | > 本文只记录关键知识点,API自己看文档 4 | 5 | ## List: 6 | 7 | List承诺可以将元素维护在特定的序列当中,它在Collection的基础上添加了大量方法,使得可以在List中插入和移除元素.List的行为依据`equals()`的行为所变化. 8 | 9 | + 有两种类型的List: 10 | 11 | 1. ArrayList: 底层实现基于数组,擅长随机访问元素,但是在List中间插入移除元素代价比较大. 12 | 13 | 2. LinkedList: 底层实现基于双向链表,在List中插入删除和随机访问代价较低,随机访问较为逊色. 14 | 15 | + `LinkedList`中还添加了可以使其用作栈,队列或双端队列的方法: 16 | 17 | 1. `element()`, `getFirst()`和`peek()`都返回列表的头一个元素.如果List为空,则抛出`NoSuchElementException`.而`peek()`返回`null`. 18 | 2. `removeFirst()`,`remove()`和`poll()`移除并放回列表的头元素.如果为空,抛出`NNoSuchElementException`,而`poll()`返回`null`.` 19 | 20 | ## Set 21 | 22 | Set用于保存不重复的元素,元素必须定义`equals()`方法来确保对象的唯一性. 23 | 24 | + 三种类型的Set: 25 | 26 | 1. HashSet: 使用了散列函数,为快速查找而设计的Set,存入的元素必须定义`hashCode()`. 27 | 28 | 2. TreeSet: 保持次序的`Set`,底层是`红-黑树`.使用它可以从`Set`中提取有序的序列.元素必须实现`Comparable`接口.(可传入比较器或者覆盖`compareTo()`自定义排序). 29 | 30 | 3. LinkedHashSet: 具有`HashSet`的查询速度,且内部使用链表维护元素(插入)顺序,迭代遍历时,结果会按插入次序显示.同样元素要实现`hashCode()`. 31 | 32 | + BitSet: 33 | 34 | 高效地存储大量"开/关"信息.效率是指空间,访问时间比数组慢. 35 | 36 | 37 | ## Map 38 | 39 | Map可以将对象映射到其他对象进行存储.元素需要实现`equals()` 40 | 41 | + 六种类型的Map: 42 | 43 | 1. HashMap: 基于散列表实现(取代了Hashtable).插入和查询时"键值对"的开销是固定的.可以通过构造器设置`容量`和`负载因子`,以调整容器的性能. 44 | + `HashMap`使用了特殊的值,称作`散列码`,用来取代对键的缓慢搜索,`散列码`是"相对唯一"的,用来代表对象的int值,它通过对象的某些信息换算而成. 45 | 46 | 2. LinkedHashMap: 类似于`HashMap`但是迭代遍历时,取得的"键值对"是其插入次序,或者是最近最少使用次序(LRU).比`HashMap`慢一点,但迭代访问速度更快一点.底层用到了链表. 47 | 48 | 3. TreeMap: 基于红黑数实现,查看"键"或"键值对"时,它们会被排序(次序由`Comparable`或`Comparator`决定).遍历所得到的结果是经过排序的.可以通过`subMap()`返回一个子树. 49 | 50 | 4. WeakHashMap: 使用`弱键`映射,允许虚拟机自动清理键和值.如果除映射之外没有引用指向这个"键",则这个"键"会被回收. 51 | 52 | 5. ConcurrentHashMap: 一种线程安全的Map,它不涉及同步加锁. 53 | 54 | 6. IdentityHashMap: 使用`==`代替`equals()`对键进行比较的散列映射. 55 | 56 | + 散列与散列码: 57 | 58 | 1. 如果要用自己的类作为HashMap的键,就必须同时覆盖`hashCode()`和`equals()`.
59 | 2. `hashCode()`不需要总能返回唯一的标识码,但`equals()`必须严格判断两个对象异同.
60 | 3. `hashCode()`用来查找对象,`equals()`用来严格判断对象是否与表中的键相同.
61 | 62 | + `equals()`必须满足5个条件: 63 | 64 | 1. 子反性: 对任意x,x.equals(x)一定返回true. 65 | 2. 对称性: 对任意x和y, 如果x.equals(y)返回true,y.equals(x)也一定返回true. 66 | 3. 传递性: 对任意x,y,z,如果有x.equals(y)和y.equals(x)返回true.则x.equals(z)返回true. 67 | 4. 一致性: 任意x.equals(y)要是幂等的. 68 | 5. 任意不是null的x,x.equals(null)一定是false. 69 | 70 | + 散列的实现: 71 | 72 | 1. Map中用数组来保存键的信息. 73 | 74 | 2. 通过键对象的`hashCode()(散列函数)`生成`散列码`,作为数组的下标. 75 | 76 | 3. 数组中保存拥有散列码和该位置下标相同的"值"的`List`,查询时就通过`equals()`方法对`List`中的值进行线性查询. 77 | 78 | > 注: 新版的jdk是基于List和红黑数实现(数据少时用list,大时用树) 79 | 80 | 81 | ## Queue 82 | 83 | 可以将元素从队列的一段插入,并于另一端将他们抽取出来. 84 | 85 | + 在JavaSE5中有两种实现(排除并发的队列); 86 | 87 | 1. LinkedList. 88 | 89 | 2. PriorityQueue: 优先队列,排序顺序通过实现`Comparable`而进行控制. 90 | 91 | + 双端队列: 92 | 93 | java中没有显示用于双向队列的接口,但是`LinkedList`包含支持双端队列的方法,可以通过组合来进行创建. 94 | 95 | 96 | 97 | ## Stack 98 | 99 | 一般通过`LinkedList`实现. 100 | 101 | ## 迭代器 102 | 103 | 迭代器是一个对象,它的工作是遍历并选择序列中的对象,而不必知道或者关心该序列底层的结构.任何实现`Iterable`接口的类,其对象都可以用`foreach`语句遍历. 104 | 105 | + Java的`Iterator`只能单向移动,它能用来: 106 | 107 | 1. 使用方法`iterator()`要求容器返回一个`Iterator`,Iterator将准备好返回序列的第一个元素. 108 | 109 | 2. 使用`next()`获得序列的下一个yuans. 110 | 111 | 3. 使用`hasNext()`检查序列是否还有元素. 112 | 113 | 4. 使用`remove()`将迭代器新返回的元素删除. 114 | 115 | + ListIterator: 116 | 117 | `ListIterator`是一个更加强大的`Iterator`子类型,当是只能用于`List`类型. 118 | 119 | `ListIterator`可以双向移动,可以产生当前位置的前一个和后一个索引,并且可以通过`set()`方法替换它访问过的最后一个元素. 120 | 121 | 122 | ## 线程安全的容器: 123 | 124 | (有空补充...) -------------------------------------------------------------------------------- /ThinkInJava/Java并发.md: -------------------------------------------------------------------------------- 1 | # Java并发 2 | 3 | ## 背景知识 4 | 5 | 1. Java所使用的并发系统会共享`内存`和`I/O`等资源,因此编写多线程程序的基本困难在于协调不同线程所驱动的任务之间对这些`共享资源`的使用,使得这些任务不会被同时被多个任务访问. 6 | 7 | 2. Java线程的机制是`抢占式`的,这表示调度机制会`周期性`地中断线程,将上下文切换到另一个线程,从而为每个线程都提供`时间片`,使每个线程都会分配到数量合理的时间去驱动他的任务. 8 | 9 | 3. 线程机制是一种建立透明的,可扩展的程序方法,为机器添加一个CPU就能很容易的加快程序运行速度. 10 | 11 | 12 | 13 | ## 基本的线程机制: 14 | 15 | * ### 一些方法说明: 16 | 17 | - `Thread.yield()`: 对线程调度器的一种建议,表示可以切换给其他任务执行一段时间. 18 | - `Thread.sleep()`(旧方法)/`TimeUnit.MILLISECONDS.sleep()`: 使当前任务中止执行一段时间(线程阻塞). 19 | - `threadObj.join()`: 挂起当前线程,等待threadObj线程执行结束才恢复.(可被`interrupt()`中断) 20 | - `Thread.currentThread()`: 获得驱动该任务的`Thread`对象的引用. 21 | 22 | ### 建立一个线程的方式: 23 | 24 | + 建立实现`Runnable`接口的任务并传入`Thread`构造器. 25 | + 直接继承`Thread`. 26 | 27 | ### 使用Executor: 28 | 29 | #### - Executor(执行器)在客户端和任务执行之间提供了一个`中介层`,这个`中介层`将代替客户端执行任务,而无需程序员显示地管理线程的生命周期.
- `任务对象`知道如何运行具体的任务,而`ExecutorService`(具有服务`生命周期`的`Executor`)知道如何构建恰当的上下文来执行`Runnable`对象. 30 | 31 | > `Executor`其实就是线程对象管理池,代替我们管理线程的生命周期,`ExecutorService`提供各种`Executor`. 32 | 33 | * ### 几种不同的`Executor`: 34 | 35 | 1. `FixedThreadPool`: 使用有限(自己设定)的线程集来执行所提交的任务.(预先进行了线程的分配,驱动任务的时候就直接使用) 36 | 37 | 2. `CacheThreadPool`: 在程序执行的过程中创建与所需数量相同的线程,然后在Executor回收旧线程时停止创建新线程.(在程序运行的过程中进行线程分配再驱动任务运行) 38 | 39 | 3. `SingleThreadPool`: 就像是线程数量为1的`FixedThreadPool`. 40 | 41 | + 静态的`ExecutorService`创建方法可以接受一个`ThreadFactory对象`(用于定制线程`优先级`,`是否后台`,`名称`),`Executor`将用这个对象来创建进行. 42 | + 调用某个`ExecutorService`的`shutdownNow()`时,它会调用所有由它控制的线程的`interrupt()`. 43 | 44 | 45 | ### 从任务中产生返回值: 46 | 47 | + 实现`Callable`接口而不是`Runnable`接口,并且要使用`ExecutorService.submit()`方法调用. 48 | + `submit()`调用会产生`Future`对象,可以使用`isDone()`来查询任务是否完成,或者直接用`get()`获取结果,如果任务未完成,`get()`将阻塞直至结果就绪. 49 | 50 | 51 | ### 线程的优先级: 52 | 53 | + 线程的`优先级`将该线程的重要器传给`Executor`,`Executor`将倾向于让`优先权`最高的任务先执行. 54 | 55 | + JDK有10个优先级,但由于操作系统的优先级多样性.在调整优先级的时候只使用: `MAX_PRIORITY`,`NORM_PRIORITY`和`MIN_PRIORITY`三种级别. 56 | 57 | ### 后台(守护)线程: 58 | 59 | + 在程序运行的时候在后台提供一种通用服务的线程,并且这种线程不属于程序中不可或缺的部分.当所有`非后台线程`结束,会杀死进程中的所有`后台线程`. 60 | 61 | + 一个`后台线程`创建的任何`子线程`都将被自动设为`后台线程`. 62 | 63 | + `后台线程`存在不执行`finally`子句就退出的情况. 64 | 65 | 66 | ### 捕获异常: 67 | 68 | Java多线程程序中,所有线程都不允许抛出未经捕获的异常,所有的异常都要在自己线程内解决.(`run()`方法没有`throws exception`)
69 | 当是线程仍有可能抛出异常.当此类异常跑抛出时,线程就会终结,而对于主线程和其他线程完全不受影响,且完全感知不到某个线程抛出的异常(也是说完全无法catch到这个异常). 70 | 71 | + #### 当线程没有处理异常,我们要在线程代码边界之外(`run()方法之外`)处理这个异常的方法: 72 | 73 | `Thread`对象提供的`setUncaughtExceptionHandler`(Thread.UncaughtExceptionHandler eh)方法.通过该方法给某个`thread`设置一个`UncaughtExceptionHandler`,可以确保在该线程出现异常时能通过回调`UncaughtExceptionHandler`接口的`public void uncaughtException(Thread t, Throwable e)`方法来处理异常,这样的好处是可以在线程代码边界之外(Thread的run()方法之外),有一个地方能处理未捕获异常。 74 | + 但是要特别明确的是:虽然是在`回调方法`中处理异常,但这个`回调方法`在执行时依然还在抛出异常的这个线程中!另外还要特别说明一点:如果线程是通过线程池创建,线程异常发生时`UncaughtExceptionHandler`接口不一定会立即回调。 75 | 76 | 77 | 78 | 79 | ## 终结任务 80 | (待补充) 81 | 82 | 83 | ## 如何安全地共享资源 84 | 85 | ### 各种各样的锁: 86 | 87 | > 隐式锁: 88 | 89 | 1. #### 对象锁: 90 | 所有对象都自动含有单一的锁(monitor),在对象上调用任意`synchronized`方法的时候,对象都被加锁,此时其他该对象上其他`synchronized`方法只有等到前一个方法调用完毕并释放锁才能被调用. 91 | 92 | 2. #### 类锁: 93 | 每个类对象都有一个锁,所以`synchronized static`方法可以在类的范围内防止对`static`数据的并发访问. 94 | 95 | > 显式锁: 96 | 97 | 3. #### Lock对象: 98 | Java类库中的显式`互斥机制`,`Lock`对象必须被显式地`创建`,`锁定`和`释放`.与内建锁相比,代码缺乏优雅性,但对于某些问题的解决更加灵活. 99 | 100 | 4. #### ReentrantLock: 101 | `ReentrantLock`允许尝试着获取一个锁,如果其他人已经获取了这个锁,可以选择离开去执行其他事情,而不是一直等待锁的释放. 102 | 103 | + 一个任务可以多次获得对象的锁(锁的计数递增) 104 | 105 | 106 | ### 原子性与易变性: 107 | 108 | + `原子性`可以应用于除`long`,`double`以外的所有基本类型之上的 _"简单操作"_. 109 | 110 | + JVM将64位的读取写入当做两个分离的32位操作来执行,产生了在`读取`和`写入`操作中间发生上下文(线程)切换,导致不同任务看到不同结果的可能性(字撕裂). 111 | 112 | + `volatile`关键字确保可视性.(修饰的域的修改立即写入`主存`,不进行任何读写优化) 113 | 114 | + 同步也会导致锁释放前向主存中刷新. 115 | 116 | 117 | ### 原子类: 118 | 119 | + JavaSE5引入了`AutomicInteger`,`AutomicLong`,`AutomicReference`等特殊的原子性变量类,并提供原子性的更新操作. 120 | 121 | ### 临界区: 122 | 123 | #### 防止多个线程同时访问方法内部的部分代码,而不是整个方法,这部分代码叫做`临界区`(也称同步代码块) 124 | 125 | + `synchronized`创建临界区的方法: 126 | 127 | 将`synchronized`用来指定某个对象,此对象的锁被用来对花括号内的代码进行同步 128 | 129 | synchronized(obj){ 130 | // 这部分代码一次只能被一个线程访问 131 | } 132 | 133 | 134 | ### 线程本地存储: 135 | 136 | + 防止任务在共享资源上发生冲突的第二种方式是根除对变量的共享.
137 | `线程本地存储`是一种自动化机制,可以为使用相同变量的每个不同的线程都创建不同的存储.通过`ThreadLocal`实现. 138 | 139 | ## 线程之间的协作 140 | 141 | ### wait()与notifyAll(): 142 | 143 | + 有两种形式的`wait()`: 144 | 145 | 1. 接收毫秒数作为参数,在一段时期内暂停(阻塞): 146 | 1. 在这段时期内锁是释放的 147 | 2. 可以通过`notify()`,`notifyAll()`或者指令时间到期,恢复执行. 148 | 149 | 2. 不接受任何参数,这时线程将无限执行下去,直至收到`notify()`或`notifyAll()`消息 150 | 151 | + `wait()`,`notify()`和`notifyAll()`都是基类`Object`的一部分. 152 | + 只能在同步代码块里调用`wait()`,`notify()`和`notifyAll()`. 153 | + 在进行协作时,信号量可能会丢失,从而导致线程无限阻塞.(`notify()/notifyAll()`在`wait()`之前发生) 154 | 155 | ### notify()与notifyAll() 156 | 157 | + 使用`notify()`时,众多等待同个锁的任务中只有一个会唤醒,要保证唤醒的是恰当的任务. 158 | 159 | + `notifyAll()`只唤醒所有等待这个锁的任务. 160 | 161 | 162 | ### 显式的Lock和Condition对象: 163 | 164 | 通过在`Condition`上调用`await()`来挂起一个任务. 165 | 当外部条件变化,某个任务需要执行时,通过调用`signal()`唤醒一个任务或`signalAll()`唤醒在这个`Condition`上被其挂起的任务. 166 | 167 | + 与使用`notifyAll()`相比,`signalAll()`是更安全的方式:
`notifyAll()`唤醒所有在此对象上的等待`synchronized`锁的任务;
`signalAll()`唤醒被`Condition`挂起的任务,控制粒度更细. 168 | 169 | + 每个`lock()`的调用都必须紧跟一个`try-finally`子句,保证所有情况下都能释放锁. 170 | 171 | 172 | ### 生产者-消费者与队列: 173 | 174 | 更高的抽象级别解决线程协作,是使用`同步队列`,`同步队列`在任何时刻都只允许一个任务插入或移除元素. 175 | 176 | + `LinkedListBlockingQueue`: 无界同步队列. 177 | + `ArrayBlockingQueue`: 指定尺寸的同步队列. 178 | 179 | ### 任务间使用管道(阻塞队列)进行输入/输出: 180 | 181 | 通过输入/输出在线程间进行通讯,Java输入/输出类库中的对应物是`PipedWriter`类(允许任务向管道写)和`PipedReader`类(允许不同任务从同一管道读取).这个"管道"基本上是一个阻塞队列. 182 | 183 | 184 | ## 死锁 185 | 186 | 某个任务在等待另一个任务,后者又在等待其他的任务,这样一直下去,直到这个链条上的任务又在等待第一个任务释放锁.任务间循环相互等待,没有哪个线程能继续,称之为`死锁`. 187 | 188 | + #### 当一下四个条件全部满足时,`死锁`就会发生: 189 | 190 | 1. 互斥条件. 191 | 2. 至少有一个任务,它持有一个资源且正在等待获取一个被别的任务持有的资源. 192 | 2. 资源不能被抢占. 193 | 3. 有循环等待. 194 | 195 | + Java并没有提供语言层面上的支持来避免死锁.要防止`死锁`的发生,只需破坏上述条件之一. 196 | 197 | ## 新类库中的构件 198 | 199 | ### CountDownLatch: 200 | 201 | #### 用来同步一个或多个任务,强制他们等待由其他任务执行的一组操作的完成: 202 | 1. 向`CountDownLatch`对象设置一个初始计数值,任何在这个对象上调用`await()`的方法都将阻塞,直至计数值达到0. 203 | 2. 其他任务在结束其工作时,可以在该对象上调用`countDown()`来减小这个计数值. 204 | 205 | + `CountDownLatch`被设计为只能触发一次,计数值不能重置. 206 | 207 | ### CyclicBarrier: 208 | 209 | #### 用于使所有并行任务在栅栏处列队,然后一致地向前移动.与`CountDownLatch`类似,但`CyclicBarrier`可以多次重用. 210 | 211 | + 可以向`CyclicBarrier`提供一个Runnable的"栅栏动作",当计数值达到0时自动执行. 212 | 213 | ### DelayQueue: 214 | 215 | #### 一个无界的`BlockingQueue`,用于放置实现了`Delayed`接口的对象,里面的对象只能到期时才能从队列中取走.
这种队列是有序的,对象头的`到期时间`最长.如果没有任何`到期`,那就不会有任何头元素. 216 | 217 | ### PriorityBlockingQueue: 218 | 219 | #### 基础的具有`阻塞读取`操作的`优先级`队列. 220 | 221 | ### ScheduledExecutor: 222 | 223 | #### `ScheduleThreadPoolExecutor`提供了任务的定时执行功能,通过使用`schedule()`(运行一次)或者`scheduleAtFixedRate()`(每隔规则的时间重复执行),将`Runnable`对象设置为在将来的某个时刻执行. 224 | 225 | ### Semaphore: 226 | 227 | #### `Semaphore`(计数信号量)允许n个任务同时访问某个资源.(正常的锁在任何时刻只允许一个任务访问一项资源) 228 | 229 | ### Exchanger: 230 | 231 | #### `Exchanger`是在两个任务之间交换对象的`栅栏`.当任务进入`栅栏`前,任务各自拥有一个对象,离开栅栏时,他们拥有之前对方持有的对象. 232 | 233 | 234 | 235 | ## 性能调优: 236 | (待补充) 237 | -------------------------------------------------------------------------------- /ThreadPoolExecutor源码解析(jdk8版).md: -------------------------------------------------------------------------------- 1 | # ThreadPoolExecutor解析 2 | 3 | ## 相关域 4 | 5 | private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); 6 | private static final int COUNT_BITS = Integer.SIZE - 3; 7 | private static final int CAPACITY = (1 << COUNT_BITS) - 1; 8 | 9 | // runState is stored in the high-order bits 10 | private static final int RUNNING = -1 << COUNT_BITS; 11 | private static final int SHUTDOWN = 0 << COUNT_BITS; 12 | private static final int STOP = 1 << COUNT_BITS; 13 | private static final int TIDYING = 2 << COUNT_BITS; 14 | private static final int TERMINATED = 3 << COUNT_BITS; 15 | 16 | // Packing and unpacking ctl 17 | private static int runStateOf(int c) { return c & ~CAPACITY; } 18 | private static int workerCountOf(int c) { return c & CAPACITY; } 19 | private static int ctlOf(int rs, int wc) { return rs | wc; } 20 | 21 | 22 | + 线程池中实现了内部类Worker,实现了Runnable,扩展了AbstractQueuedSynchronizer(一个同步框架),其实例域封装了一个线程, 可以认为一个worker实例是一个工作者线程, 负责执行提交到线程池的任务. 23 | 24 | + ctl: 用于表示线程池状态和当前worker数量的包装属性(可以理解为记账本),其二进制前三位用于表示`runState`(线程池运行状态),后29位表示当任务数. 25 | 26 | CAPACITY: 000111111~1; 27 | ~CAPACITY: 111000000~0(用于解包装) 28 | 29 | `runStateOf(int c)`通过传入ctl获取当前运行状态
30 | `workerCountOf(int c)`通过传入ctl获取当前worker数
31 | `ctlOf(int rs, int wc)`通过传入运行状态和worker数算出ctl 32 | 33 | 34 | + RUNNING: 运行状态,此时接收并运行排队中的任务 35 | + SHUTDOWN: 不再接受新的任务,继续执行排队中的任务 36 | + STOP: 不再接收新的任务,也不执行排队中的任务,并且中断运行中的任务 37 | + TIDYING: 整理状态,所有任务都终止,当前worker数为0,即将运行`terminated()`方法(`terminated`交给子类实现,默认什么都不干) 38 | + TERMINATED: `terminated()`运行完毕 39 | 40 | 41 | private final BlockingQueue workQueue; 42 | 43 | private final ReentrantLock mainLock = new ReentrantLock(); 44 | 45 | private final HashSet workers = new HashSet(); 46 | 47 | 48 | + workQueue: 存储任务的阻塞队列,待执行任务在此排队 49 | + mainLock: 主锁, 用于控制线程池属性访问修改的同步 50 | + workers: 当前可用worker(线程)集合 51 | 52 | 53 | private int largestPoolSize; 54 | 55 | private long completedTaskCount; 56 | 57 | private volatile ThreadFactory threadFactory; 58 | 59 | private volatile RejectedExecutionHandler handler; 60 | 61 | private volatile long keepAliveTime; 62 | 63 | private volatile boolean allowCoreThreadTimeOut; 64 | 65 | private volatile int corePoolSize; 66 | 67 | private volatile int maximumPoolSize; 68 | 69 | 70 | + largestPoolSize: 记录线程池达到过的最高worker数量, 通过主锁访问修改 71 | + completedTaskCount: 完成的任务总数,只能在worker中更新,通过主锁读 72 | + threadFactory: 线程工厂 73 | + handler: 线程池饱和或关闭后的拒绝方案 74 | + keepAliveTime: 空闲线程的存活时间,超时则回收该线程(如果设置了`allowCoreThreadTimeOut`),也会回收核心线程 75 | + allowCoreThreadTimeOut: false,核心空闲线程超时不会被回收,反之被回收 76 | + corePoolSize: 核心线程数量 77 | + maximumPoolSize: 最高线程数量, 但是最高不超过CAPACITY的值 78 | 79 | 以上的基本类型实例域都可以算是线程池对象的记账本,记录当前线程池的“身体状态” 80 | 81 | ## 入口方法execute(Runnable command) 82 | 83 | /** 84 | * Executes the given task sometime in the future. The task 85 | * may execute in a new thread or in an existing pooled thread. 86 | * 87 | * If the task cannot be submitted for execution, either because this 88 | * executor has been shutdown or because its capacity has been reached, 89 | * the task is handled by the current {@code RejectedExecutionHandler}. 90 | * 91 | * @param command the task to execute 92 | * @throws RejectedExecutionException at discretion of 93 | * {@code RejectedExecutionHandler}, if the task 94 | * cannot be accepted for execution 95 | * @throws NullPointerException if {@code command} is null 96 | */ 97 | public void execute(Runnable command) { 98 | if (command == null) 99 | throw new NullPointerException(); 100 | /* 101 | * Proceed in 3 steps: 102 | * 103 | * 1. If fewer than corePoolSize threads are running, try to 104 | * start a new thread with the given command as its first 105 | * task. The call to addWorker atomically checks runState and 106 | * workerCount, and so prevents false alarms that would add 107 | * threads when it shouldn't, by returning false. 108 | * 109 | * 2. If a task can be successfully queued, then we still need 110 | * to double-check whether we should have added a thread 111 | * (because existing ones died since last checking) or that 112 | * the pool shut down since entry into this method. So we 113 | * recheck state and if necessary roll back the enqueuing if 114 | * stopped, or start a new thread if there are none. 115 | * 116 | * 3. If we cannot queue task, then we try to add a new 117 | * thread. If it fails, we know we are shut down or saturated 118 | * and so reject the task. 119 | */ 120 | int c = ctl.get(); 121 | if (workerCountOf(c) < corePoolSize) { // 第一步 122 | if (addWorker(command, true)) 123 | return; 124 | c = ctl.get(); 125 | } 126 | if (isRunning(c) && workQueue.offer(command)) { // 第二步 127 | int recheck = ctl.get(); 128 | if (! isRunning(recheck) && remove(command)) 129 | reject(command); 130 | else if (workerCountOf(recheck) == 0) 131 | addWorker(null, false); 132 | } 133 | else if (!addWorker(command, false)) // 第三步 134 | reject(command); // 第四步 135 | } 136 | 137 | 138 | 1. 当前运行的worker小于`corePoolSize`时,直接实例化一个新的worker运行此任务,否则进入第二步 139 | 2. 如果阻塞队列未满则进入阻塞队列排队等待worker执行,否则进入第三步 140 | 3. 如果当前worker数小于`maximumPoolSize`时,尝试实例化一个新的worker来执行此任务,否则进入第四步 141 | 4. 当worker数大于`maximumPoolSize`或线程池正处于关闭过程时,拒绝这个接收这个任务,由rejectHandler处理. 142 | 143 | + addWorker方法将会原子地检查`runState`和`workerCount`,防止多线程状态下中错误的添加worker 144 | + 第二部使用了双重检查,防止进入条件语句后线程池关闭 145 | 146 | 147 | ## 新建线程:addWorker(Runnable firstTask, boolean core) 148 | /* 149 | * Methods for creating, running and cleaning up after workers 150 | */ 151 | 152 | /** 153 | * Checks if a new worker can be added with respect to current 154 | * pool state and the given bound (either core or maximum). If so, 155 | * the worker count is adjusted accordingly, and, if possible, a 156 | * new worker is created and started, running firstTask as its 157 | * first task. This method returns false if the pool is stopped or 158 | * eligible to shut down. It also returns false if the thread 159 | * factory fails to create a thread when asked. If the thread 160 | * creation fails, either due to the thread factory returning 161 | * null, or due to an exception (typically OutOfMemoryError in 162 | * Thread.start()), we roll back cleanly. 163 | * 164 | * @param firstTask the task the new thread should run first (or 165 | * null if none). Workers are created with an initial first task 166 | * (in method execute()) to bypass queuing when there are fewer 167 | * than corePoolSize threads (in which case we always start one), 168 | * or when the queue is full (in which case we must bypass queue). 169 | * Initially idle threads are usually created via 170 | * prestartCoreThread or to replace other dying workers. 171 | * 172 | * @param core if true use corePoolSize as bound, else 173 | * maximumPoolSize. (A boolean indicator is used here rather than a 174 | * value to ensure reads of fresh values after checking other pool 175 | * state). 176 | * @return true if successful 177 | */ 178 | private boolean addWorker(Runnable firstTask, boolean core) { 179 | retry: 180 | for (;;) { 181 | int c = ctl.get(); 182 | int rs = runStateOf(c); 183 | 184 | // Check if queue empty only if necessary. 185 | if (rs >= SHUTDOWN && 186 | ! (rs == SHUTDOWN && 187 | firstTask == null && 188 | ! workQueue.isEmpty())) 189 | return false; 190 | 191 | for (;;) { 192 | int wc = workerCountOf(c); 193 | if (wc >= CAPACITY || 194 | wc >= (core ? corePoolSize : maximumPoolSize)) 195 | return false; 196 | if (compareAndIncrementWorkerCount(c)) 197 | break retry; 198 | c = ctl.get(); // Re-read ctl 199 | if (runStateOf(c) != rs) 200 | continue retry; 201 | // else CAS failed due to workerCount change; retry inner loop 202 | } 203 | } 204 | 205 | boolean workerStarted = false; 206 | boolean workerAdded = false; 207 | Worker w = null; 208 | try { 209 | w = new Worker(firstTask); 210 | final Thread t = w.thread; 211 | if (t != null) { 212 | final ReentrantLock mainLock = this.mainLock; 213 | mainLock.lock(); 214 | try { 215 | // Recheck while holding lock. 216 | // Back out on ThreadFactory failure or if 217 | // shut down before lock acquired. 218 | int rs = runStateOf(ctl.get()); 219 | 220 | if (rs < SHUTDOWN || 221 | (rs == SHUTDOWN && firstTask == null)) { 222 | if (t.isAlive()) // precheck that t is startable 223 | throw new IllegalThreadStateException(); 224 | workers.add(w); 225 | int s = workers.size(); 226 | if (s > largestPoolSize) 227 | largestPoolSize = s; 228 | workerAdded = true; 229 | } 230 | } finally { 231 | mainLock.unlock(); 232 | } 233 | if (workerAdded) { 234 | t.start(); // 执行任务 235 | workerStarted = true; 236 | } 237 | } 238 | } finally { 239 | if (! workerStarted) 240 | addWorkerFailed(w); 241 | } 242 | return workerStarted; 243 | } 244 | 245 | ### 方法执行操作: 246 | 检查线程池是否运行状态,添加是否超过核心工作者数与最大工作者数,都满足的话就添加(放入`workers`Set),启动新的worker并调整worker数目
247 | firstTask将作为这个worker的第一个运行任务 248 | 249 | ### 新建worker失败的情况: 250 | 251 | 1. 线程池是STOP状态或者有资格关闭(不知道什么意思,反正是非RUNNING状态就对了) 252 | 2. 线程工厂创建新线程失败(原因可能是工厂返回空值或抛出异常(可能是OutOfMemoryError)) 253 | 失败后方法将会回滚并返回false, `addWorkerFailed()`方法执行回滚动作 254 | 255 | 256 | 257 | ## 内部类Worker 258 | 259 | /** 260 | * Class Worker mainly maintains interrupt control state for 261 | * threads running tasks, along with other minor bookkeeping. 262 | * This class opportunistically extends AbstractQueuedSynchronizer 263 | * to simplify acquiring and releasing a lock surrounding each 264 | * task execution. This protects against interrupts that are 265 | * intended to wake up a worker thread waiting for a task from 266 | * instead interrupting a task being run. We implement a simple 267 | * non-reentrant mutual exclusion lock rather than use 268 | * ReentrantLock because we do not want worker tasks to be able to 269 | * reacquire the lock when they invoke pool control methods like 270 | * setCorePoolSize. Additionally, to suppress interrupts until 271 | * the thread actually starts running tasks, we initialize lock 272 | * state to a negative value, and clear it upon start (in 273 | * runWorker). 274 | */ 275 | private final class Worker 276 | extends AbstractQueuedSynchronizer 277 | implements Runnable 278 | { 279 | /** 280 | * This class will never be serialized, but we provide a 281 | * serialVersionUID to suppress a javac warning. 282 | */ 283 | private static final long serialVersionUID = 6138294804551838833L; 284 | 285 | /** Thread this worker is running in. Null if factory fails. */ 286 | final Thread thread; 287 | /** Initial task to run. Possibly null. */ 288 | Runnable firstTask; 289 | /** Per-thread task counter */ 290 | volatile long completedTasks; 291 | 292 | /** 293 | * Creates with given first task and thread from ThreadFactory. 294 | * @param firstTask the first task (null if none) 295 | */ 296 | Worker(Runnable firstTask) { 297 | setState(-1); // inhibit interrupts until runWorker 298 | this.firstTask = firstTask; 299 | this.thread = getThreadFactory().newThread(this); 300 | } 301 | 302 | /** Delegates main run loop to outer runWorker */ 303 | public void run() { // 线程调用start()方法时实际上是调用了runWorker方法 304 | runWorker(this); 305 | } 306 | 307 | // Lock methods 308 | // 309 | // The value 0 represents the unlocked state. 310 | // The value 1 represents the locked state. 311 | 312 | protected boolean isHeldExclusively() { 313 | return getState() != 0; 314 | } 315 | 316 | protected boolean tryAcquire(int unused) { 317 | if (compareAndSetState(0, 1)) { 318 | setExclusiveOwnerThread(Thread.currentThread()); 319 | return true; 320 | } 321 | return false; 322 | } 323 | 324 | protected boolean tryRelease(int unused) { 325 | setExclusiveOwnerThread(null); 326 | setState(0); 327 | return true; 328 | } 329 | 330 | public void lock() { acquire(1); } 331 | public boolean tryLock() { return tryAcquire(1); } 332 | public void unlock() { release(1); } 333 | public boolean isLocked() { return isHeldExclusively(); } 334 | 335 | void interruptIfStarted() { 336 | Thread t; 337 | if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) { 338 | try { 339 | t.interrupt(); 340 | } catch (SecurityException ignore) { 341 | } 342 | } 343 | } 344 | } 345 | 346 | 明显从字面就能看出worker就是执行我们任务的工人,实现`Runnable`,其本身代表一个任务 347 | 覆盖了run方法, 所以线程`addWorker()`中启动线程,实际上执行了`runWorker()`(接下来会继续展开) 348 | 349 | ### 主要的实例域: 350 | 351 | + thread: 用于执行任务的线程 352 | + firstTask: 运行的第一个任务(伴随worker实例创建的) 353 | + completedTasks: 这个worker完成的任务数量 354 | 355 | Worker还扩展了`AbstractQueuedSynchronizer`(一个用于构建锁和同步器的框架)且实现了父类的模板方法,实现任务执行前后的加锁与解锁 356 | 357 | 358 | ## 线程执行的方法runWorker(Worker w) 359 | 360 | /** 361 | * Main worker run loop. Repeatedly gets tasks from queue and 362 | * executes them, while coping with a number of issues: 363 | * 364 | * 1. We may start out with an initial task, in which case we 365 | * don't need to get the first one. Otherwise, as long as pool is 366 | * running, we get tasks from getTask. If it returns null then the 367 | * worker exits due to changed pool state or configuration 368 | * parameters. Other exits result from exception throws in 369 | * external code, in which case completedAbruptly holds, which 370 | * usually leads processWorkerExit to replace this thread. 371 | * 372 | * 2. Before running any task, the lock is acquired to prevent 373 | * other pool interrupts while the task is executing, and then we 374 | * ensure that unless pool is stopping, this thread does not have 375 | * its interrupt set. 376 | * 377 | * 3. Each task run is preceded by a call to beforeExecute, which 378 | * might throw an exception, in which case we cause thread to die 379 | * (breaking loop with completedAbruptly true) without processing 380 | * the task. 381 | * 382 | * 4. Assuming beforeExecute completes normally, we run the task, 383 | * gathering any of its thrown exceptions to send to afterExecute. 384 | * We separately handle RuntimeException, Error (both of which the 385 | * specs guarantee that we trap) and arbitrary Throwables. 386 | * Because we cannot rethrow Throwables within Runnable.run, we 387 | * wrap them within Errors on the way out (to the thread's 388 | * UncaughtExceptionHandler). Any thrown exception also 389 | * conservatively causes thread to die. 390 | * 391 | * 5. After task.run completes, we call afterExecute, which may 392 | * also throw an exception, which will also cause thread to 393 | * die. According to JLS Sec 14.20, this exception is the one that 394 | * will be in effect even if task.run throws. 395 | * 396 | * The net effect of the exception mechanics is that afterExecute 397 | * and the thread's UncaughtExceptionHandler have as accurate 398 | * information as we can provide about any problems encountered by 399 | * user code. 400 | * 401 | * @param w the worker 402 | */ 403 | final void runWorker(Worker w) { 404 | Thread wt = Thread.currentThread(); 405 | Runnable task = w.firstTask; 406 | w.firstTask = null; 407 | w.unlock(); // allow interrupts 408 | boolean completedAbruptly = true; 409 | try { 410 | while (task != null || (task = getTask()) != null) { 411 | w.lock(); 412 | // If pool is stopping, ensure thread is interrupted; 413 | // if not, ensure thread is not interrupted. This 414 | // requires a recheck in second case to deal with 415 | // shutdownNow race while clearing interrupt 416 | if ((runStateAtLeast(ctl.get(), STOP) || 417 | (Thread.interrupted() && 418 | runStateAtLeast(ctl.get(), STOP))) && 419 | !wt.isInterrupted()) 420 | wt.interrupt(); 421 | try { 422 | beforeExecute(wt, task); 423 | Throwable thrown = null; 424 | try { 425 | task.run(); 426 | } catch (RuntimeException x) { 427 | thrown = x; throw x; 428 | } catch (Error x) { 429 | thrown = x; throw x; 430 | } catch (Throwable x) { 431 | thrown = x; throw new Error(x); 432 | } finally { 433 | afterExecute(task, thrown); 434 | } 435 | } finally { 436 | task = null; 437 | w.completedTasks++; 438 | w.unlock(); 439 | } 440 | } 441 | completedAbruptly = false; 442 | } finally { 443 | processWorkerExit(w, completedAbruptly); 444 | } 445 | } 446 | 447 | + runWork()代表每个工作者线程的执行,当成功完成初始的任务后,只要线程池还是运行状态,就通过`getTask()`从任务等待队列中拿任务来执行,直到`getTask()`方法返回null(这个后面会详细说明), 会跳出循环,之后`processWorkerExit()`将为他“处理后事”,这个worker的生命周期就到此结束 448 | 449 | + 每个任务运行前后都会执行(beforeExecute)前置动作和(afterExecute)后置动作,不过实现留空了,应该是供子类扩展用的 450 | 451 | 452 | ## 获取任务getTask() 453 | 454 | 455 | /** 456 | * Performs blocking or timed wait for a task, depending on 457 | * current configuration settings, or returns null if this worker 458 | * must exit because of any of: 459 | * 1. There are more than maximumPoolSize workers (due to 460 | * a call to setMaximumPoolSize). 461 | * 2. The pool is stopped. 462 | * 3. The pool is shutdown and the queue is empty. 463 | * 4. This worker timed out waiting for a task, and timed-out 464 | * workers are subject to termination (that is, 465 | * {@code allowCoreThreadTimeOut || workerCount > corePoolSize}) 466 | * both before and after the timed wait, and if the queue is 467 | * non-empty, this worker is not the last thread in the pool. 468 | * 469 | * @return task, or null if the worker must exit, in which case 470 | * workerCount is decremented 471 | */ 472 | private Runnable getTask() { 473 | boolean timedOut = false; // Did the last poll() time out? 474 | 475 | for (;;) { 476 | int c = ctl.get(); 477 | int rs = runStateOf(c); 478 | 479 | // Check if queue empty only if necessary. 480 | if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) { 481 | decrementWorkerCount(); 482 | return null; 483 | } 484 | 485 | int wc = workerCountOf(c); 486 | 487 | // Are workers subject to culling? 488 | boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; 489 | 490 | if ((wc > maximumPoolSize || (timed && timedOut)) 491 | && (wc > 1 || workQueue.isEmpty())) { 492 | if (compareAndDecrementWorkerCount(c)) 493 | return null; 494 | continue; 495 | } 496 | 497 | try { 498 | Runnable r = timed ? 499 | workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : 500 | workQueue.take(); 501 | if (r != null) 502 | return r; 503 | timedOut = true; 504 | } catch (InterruptedException retry) { 505 | timedOut = false; 506 | } 507 | } 508 | } 509 | 510 | + 这个方法会根据配置提供阻塞or限时从任务队列中提取任务的操作 511 | 512 | + 返回null的几种情况(注释已经很明确了): 513 | 1. 当前worker数超过`maximumPoolSize`限定值 514 | 2. 线程池是STOP状态(stop不执行队列的任务) 515 | 3. 线程池塘是SHUTDOWN状态且任务队列是空的(shutdown会等待队列任务执行完毕) 516 | 4. worker等待一个任务超过时间限制 517 | 518 | 519 | ## Worker的“后事处理”:processWorkerExit(Worker w, boolean completedAbruptly) 520 | 521 | 522 | /** 523 | * Performs cleanup and bookkeeping for a dying worker. Called 524 | * only from worker threads. Unless completedAbruptly is set, 525 | * assumes that workerCount has already been adjusted to account 526 | * for exit. This method removes thread from worker set, and 527 | * possibly terminates the pool or replaces the worker if either 528 | * it exited due to user task exception or if fewer than 529 | * corePoolSize workers are running or queue is non-empty but 530 | * there are no workers. 531 | * 532 | * @param w the worker 533 | * @param completedAbruptly if the worker died due to user exception 534 | */ 535 | private void processWorkerExit(Worker w, boolean completedAbruptly) { 536 | if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted 537 | decrementWorkerCount(); 538 | 539 | final ReentrantLock mainLock = this.mainLock; 540 | mainLock.lock(); 541 | try { 542 | completedTaskCount += w.completedTasks; 543 | workers.remove(w); 544 | } finally { 545 | mainLock.unlock(); 546 | } 547 | 548 | tryTerminate(); 549 | 550 | int c = ctl.get(); 551 | if (runStateLessThan(c, STOP)) { 552 | if (!completedAbruptly) { 553 | int min = allowCoreThreadTimeOut ? 0 : corePoolSize; 554 | if (min == 0 && ! workQueue.isEmpty()) 555 | min = 1; 556 | if (workerCountOf(c) >= min) 557 | return; // replacement not needed 558 | } 559 | addWorker(null, false); 560 | } 561 | } 562 | 563 | + completedAbruptly:用于判断worker是因为`getTask()`返回null退出还是执行发生异常退出,前者不做任何处理(`getTask`已经做了),后者将调整下workerCount 564 | 565 | + 之后将登记这个worker的工作成果再将它“辞退”,这个worker对象的生命周期结束 566 | 567 | + 最后再判断,如果线程池还在运行状态且因为运行异常退出,就再增加一个增加一个新的worker(可以理解为workerA不能完成boss的需求,boss就把他辞退,然后聘请一个新的workerB)
568 | 如果线程池在运行状态且是因为`getTask()`返回null退出,就要根据是否设置了`allowCoreThreadTimeOut`(运行核心线程在空闲超时被回收)来判断是否要增加新的worker(这就好像工厂没有单做,就要辞退工人好减少亏损,如果厂长可以选择2种策略:1是全炒了,有新单再招新的工人;2是留下一部分工人以便有新单的时候能马上开工) 569 | 570 | + `tryTerminate()`这个方法是在每个`worker`生命周期结束后都检查线程池运行状态,在SHUTDOWN并且任务为空,或者STOP并且任务队列为空的情况下将线程池转为TERMINATED状态,线程池生命周期结束 571 | 572 | > 以上内容都是个人理解,如有不严谨或错漏之处,欢迎指正! 573 | 574 | 575 | 576 | ### 使用线程池的好处 577 | 578 | 1. 重复利用线程, 减少在创建和销毁线程的时间资源开销。 579 | 2. 提高响应速度, 新任务可以不需要等线程创建就可以立即行。 580 | 3. 提高线程的可管理性, 使用线程池对线程进行统一的分配和监控。 581 | 4. 如果不使用线程池, 有可能造成系统创建大量线程而导致消耗完系统内存 582 | 583 | ### 线程池的注意事项 584 | 585 | 虽然线程池是构建多线程应用程序的强大机制, 但使用它并不是没有风险: 586 | 587 | (1) 线程池的大小。 多线程应用并非线程越多越好, 需要根据系统运行的软硬件环境以及应用本身的特点决定线程池的大小。 一般来说, 如果代码结构合理的话, 线程数目与 CPU数量相适合即可。 如果线程运行时可能出现阻塞现象, 可相应增加池的大小; 如有必要可采用自适应算法来动态调整线程池的大小, 以提高 CPU 的有效利用率和系统的整体性能。 588 | 589 | (2) 并发错误。 多线程应用要特别注意并发错误, 要从逻辑上保证程序的正确性, 注意避免死锁现象的发生。 590 | 591 | <<<<<<< HEAD 592 | (3) 线程泄漏。 这是线程池应用中一个严重的问题, 当任务执行完毕而线程没能返回池中就会发生线程泄漏现象。 593 | ======= 594 | (3) 线程泄漏。 这是线程池应用中一个严重的问题, 当任务执行完毕而线程没能返回池中就会发生线程泄漏现象。 595 | >>>>>>> 01d1d023c73a164a67d43a6405dea468b9247753 596 | -------------------------------------------------------------------------------- /分布式系统/Raft.md: -------------------------------------------------------------------------------- 1 | # Raft协议 2 | 3 | 4 | ## Raft节点 5 | 6 | ### 一个Raft节点可以有三种状态: 7 | 8 | 1. Follower(跟随者) 9 | 2. Candidate(候选者) 10 | 3. Leader(领导者) 11 | 12 | ### Leader Election(领导选举) 13 | 14 | 1. 所有节点一开始都是Follower 15 | 2. 如果Follower与Leader失去连接(即在定时器时间内无法接收or回复Follwer的心跳包)则变为Candidate(初始化时通过随机定时器来避免产生多个Candidate), 一个Candidate将会要求其他节点向他投票 16 | 3. 如果一个Candidate节点获得超过节点总数一半的选票,则变为Leader,选举失败的节点变为Follower。 17 | 4. 如果没有一个Candidate节点获得超过半数的选票,则重新开始新的一轮选举 18 | 19 | + #### 选举过程有两种Timeout: 20 | 1. election timeout(选举超时): Follower等待(等待与Leader通信)的时间,若超时则转换为Candidate并开始一个新的选举任期. 21 |
Timeout时长为150ms~300ms不等,每个节点都是随机值. 22 | 2. heartbeat timeout(心跳超时): Leader与Follower心跳链接过程中有一个心跳超时,若Follower在超时时间内未收到Leader的心跳包,则自己转为Candidate发起选票请求;若收到Leader的心跳包。则重置time. 23 | 24 | 25 | ### Log Replication(日志复制) 26 | 27 | 1. 所有客户端对这个系统的请求都会先经过Leader,每个修改都会在Leader节点增加一条日志(log entry), 但不会提交该日志,所以不会改变Leader节点当前的数据状态。 28 | 2. 接下来,Leader将该条日志通过Leader的心跳包复制到他的Follower节点,Follower收到该日志后写入自己的日志文件中,向Leader返回一条类似ACK的答复。 29 | 3. 当Leader收到绝对大多数的Follower的写入答复后就在自己的节点上提交该条日志记录,并相应客户端结果(异步),然后在通过下次心跳包通知Follower节点该条记录已被提交,Follower节点也跟着一起提交该条日志 30 | 31 | ### Consitent in Network partitions(网络分区下保持一致性) 32 | 33 | Raft能够正确地处理网络分区(“脑裂”)问题 34 | 35 | + A~E五个结点,B是leader。如果发生“脑裂”,A、B成为一个子分区,C、D、E成为一个子分区。此时C、D、E会发生选举,选出C作为新term的leader。这样我们在两个子分区内就有了不同term的两个leader。这时如果有客户端写A时,因为B无法复制日志到大部分follower所以日志处于uncommitted未提交状态。而同时另一个客户端对C的写操作却能够正确完成,因为C是新的leader,它只知道D和E。 36 | 37 | + 当网络分区消除时,A,B能与所有节点发送心跳包,此时发现彼此term不一致,于是会以term最大的为Leader,另一个及其Follower会rollback在分区时uncommited的值成为其Follower。 (如果term比较小的那部分分区有超过总数一半的节点,已经commit,如何保持更新一致呢? 突然想到了答案: 如果trem比较小,不可能拥有超过总数一半的节点,因为节点过少无法提交) 38 | 39 | 40 | ## Raft特性: 41 | 42 | + 强领导者(Strong Leader):Raft 使用一种比其他算法更强的领导形式。例如,日志条目只从领导者发送向其他服务器。这样就简化了对日志复制的管理,使得 Raft 更易于理解。 43 | 44 | + 领导选取(Leader Selection):Raft 使用随机定时器来选取领导者。这种方式仅仅是在所有算法都需要实现的心跳机制上增加了一点变化,它使得在解决冲突时更简单和快速。 45 | 46 | + 成员变化(Membership Change):Raft 为了调整集群中成员关系使用了新的联合一致性(joint consensus)的方法,这种方法中大多数不同配置的机器在转换关系的时候会交迭(overlap)。这使得在配置改变的时候,集群能够继续操作。 47 | 48 | 49 | 50 | #### 相关文档 51 | > [Raft 一致性算法论文译文](http://www.infoq.com/cn/articles/raft-paper) 52 | 53 | > [Raf 动图](http://thesecretlivesofdata.com/raft/) 54 | 55 | >[Raft 为什么是更易理解的分布式一致性算法](https://www.cnblogs.com/mindwind/p/5231986.html) 56 | -------------------------------------------------------------------------------- /双重检查锁定漏洞分析笔记.md: -------------------------------------------------------------------------------- 1 | # 双重检查锁定漏洞分析 2 | 3 | > 双重检查锁定模式(也被称为"双重检查加锁优化","锁暗示") 是一种软件设计模式用来`减少并发系统中竞争和同步的开销`。双重检查锁定模式`首先验证锁定条件(第一次检查),只有通过锁定条件验证才真正的进行加锁逻辑并再次验证条件(第二次检查)`。

4 | 该模式在某些语言在某些硬件平台的实现可能是不安全的。有的时候,这一模式被看做是反模式。

5 | 它通常用于减少加锁开销,尤其是为多线程环境中的`单例模式`实现“`惰性初始化`”。惰性初始化的意思是直到第一次访问时才初始化它的值。

6 | ---- from [wikipedia](https://zh.wikipedia.org/wiki/%E5%8F%8C%E9%87%8D%E6%A3%80%E6%9F%A5%E9%94%81%E5%AE%9A%E6%A8%A1%E5%BC%8F#Java.E4.B8.AD.E7.9A.84.E4.BD.BF.E7.94.A8) 7 | 8 | 9 | ## 代码的演变: 10 | 11 | 1. 最开始的代码: 12 | 13 | + 在多线程环境下运行这份代码将会造成很多错误,最明显的问题是(其他问题在后面): 14 | 15 | 假设有线程1,2,当线程1运行到A进入B区将要新建实例时,(此时helper还未赋值)线程2也通过A的判断语句,进入B区新建实例.这样就造成了两个或多个helper被实例化. 16 | 17 | 18 | // Single threaded version 19 | class Foo { 20 | private Helper helper = null; 21 | public Helper getHelper() { 22 | if (helper == null) //A 23 | helper = new Helper(); //B 24 | return helper; //C 25 | } 26 | // other functions and members... 27 | } 28 | 29 | 30 | 2. 加锁代码: 31 | 32 | + 对`代码1`最简单的处理方式是对方法`加锁`进行同步处理: 33 | 34 | 35 | // Correct multithreaded version 36 | class Foo { 37 | private Helper helper = null; 38 | public synchronized Helper getHelper() { 39 | if (helper == null) 40 | helper = new Helper(); 41 | return helper; 42 | } 43 | // other functions and members... 44 | } 45 | 46 | 47 | 3. 双重检查锁定代码: 48 | 49 | + `代码2`当中每次获取helper都要进行加解锁操作,开销是很大的.而除了第一次实例化对象,其他时候都只是单纯放回helper对象,不需要同步操作,于是有了`代码3`: 50 | 51 | // Broken multithreaded version 52 | // "Double-Checked Locking" idiom 53 | class Foo { 54 | private Helper helper = null; 55 | public Helper getHelper() { 56 | if (helper == null) 57 | synchronized(this) { 58 | if (helper == null) 59 | helper = new Helper(); 60 | } 61 | return helper; 62 | } 63 | // other functions and members... 64 | } 65 | 66 | 67 | + 【我是重点】然而问题来了,这份代码在`编译器`和`共享内存多处理器`的优化(即重排序)下是无效的. 68 | 69 | + 为什么无效?有两个原因,第一个原因(第二个原因看`代码4`): 70 | 71 | 72 | > the writes that initialize the Helper object and the write to the helper field can be done or perceived out of order. Thus, a thread which invokes getHelper() could see a non-null reference to a helper object, but see the default values for fields of the helper object, rather than the values set in the constructor.

73 | 大致意思是`Helper对象的初始化`(这里特指``方法,注意区分`对象初始化`与`类初始化`)和`对象写入helper域`可以是无序的.因此,一个调用getHelper()的线程可以看到helper对象的非空引用,但是看到的`helper的字段是默认值`(零值),而不是在构造函数中设置的值。([参考对象创建过程](https://github.com/wususu/Notes/blob/master/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/Java%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F%E5%88%92%E5%88%86.md)) 74 | 75 | > singletons[i].reference = new Singleton();这段代码在`Symantec JIT编译`后如下:
76 | 显然,对象的`分配`发生在`对象初始化`之前,这在Java内存模型中是合法的. 77 | 78 | 79 | 0206106A mov eax,0F97E78h 80 | 0206106F call 01F6B210 ; 为单例分配空间 81 | ; 结果返回到eax 82 | 02061074 mov dword ptr [ebp],eax ; EBP 是 &singletons[i].reference 83 | ; 将未执行操作的对象存放到此处 84 | 02061077 mov ecx,dword ptr [eax] ; 大致是获取原始指针的操作 85 | 02061079 mov dword ptr [ecx],100h ; 接下来四行是 86 | 0206107F mov dword ptr [ecx+4],200h ; 单例的操作 87 | 02061086 mov dword ptr [ecx+8],400h 88 | 0206108D mov dword ptr [ecx+0Ch],0F84030h 89 | 90 | 91 | 92 | 4. "改进"版代码: 93 | 94 | + 基于上述问题,人们又给出了改进版的代码:
95 | 把对象实例化放入`内部的同步代码块`中,通过一个内部锁,认为这样可以强制`对象实例化`完成后才能将对象的引用存入helper域中. 96 | 97 | // (Still) Broken multithreaded version 98 | // "Double-Checked Locking" idiom 99 | class Foo { 100 | private Helper helper = null; 101 | public Helper getHelper() { 102 | if (helper == null) { 103 | Helper h; 104 | synchronized(this) { 105 | h = helper; 106 | if (h == null) 107 | synchronized (this) { 108 | h = new Helper(); 109 | } // release inner synchronization lock 110 | helper = h; 111 | } 112 | } 113 | return helper; 114 | } 115 | // other functions and members... 116 | } 117 | 118 | + 然而,这种想法也是错误的!!!
119 | 120 | `monitorexit`(弹出objectref,释放和objectref相关联的锁的操作码)的规则是: `monitorexit`操作码之前的操作必须在锁释放之前执行.
121 | 但并没有规定说明`monitorexit`之后的操作不能在锁释放之前执行.所以编译器移除`helper = h`是完全合理的.
122 | 许多处理器提供了这种`单向内存栅栏`的说明,强行改变它的语义为`双向内存栅栏`将会得到错误的运行结果. 123 | 124 | 125 | ## 真正的解决方案: 126 | 127 | ### 1. 如果你想要的单例是静态的,而不是对象属性: 128 | 129 | 将单例定义为一个单独的类中的`静态字段`.Java的语义保证字段不会被初始化,直到字段被引用,并且任何访问字段的线程都将看到所有的初始化该字段的值. 130 | 131 | // 1 132 | class HelperSingleton { 133 | static Helper singleton = new Helper(); 134 | } 135 | 136 | class Foo { 137 | public Helper getHelper() { 138 | return HelperSingleton.singleton; 139 | } 140 | } 141 | 142 | //2 143 | class Foo { 144 | 145 | private static class HelperSingleton { 146 | static Helper singleton = new Helper(); 147 | } 148 | 149 | public static Helper getHelper() { 150 | return HelperSingleton.singleton; 151 | } 152 | } 153 | 154 | ### 2. 单例对象属性: 155 | 156 | 1. 使用`线程本地存储`实现`双重检查锁定`的巧妙方法.
157 | 每个线程保留一个`线程本地标志`来确定该线程是否完成了所需的同步。但这取决于`LocalThread`的存取速度. 158 | 159 | 160 | class Foo { 161 | /** If perThreadInstance.get() returns a non-null value, this thread 162 | has done synchronization needed to see initialization 163 | of helper */ 164 | private final ThreadLocal perThreadInstance = new ThreadLocal(); 165 | private Helper helper = null; 166 | public Helper getHelper() { 167 | if (perThreadInstance.get() == null) createHelper(); 168 | return helper; 169 | } 170 | private final void createHelper() { 171 | synchronized(this) { 172 | if (helper == null) 173 | helper = new Helper(); 174 | } 175 | // Any non-null value would do as the argument here 176 | perThreadInstance.set(perThreadInstance); 177 | } 178 | } 179 | 180 | 181 | 2. 更好的方法: 使用`volatile`: 182 | 183 | 系统不允许`volatile写操作`与之前的`任何读写`操作重排序,不允许`volatile读操作`与之后的`任何读写`操作重排序.
184 | 通过声明实例属性是`volitile`的,保证(`对象初始化`在`实例分配`之前发生)双重检查锁定. 185 | 186 | 187 | // Works with acquire/release semantics for volatile 188 | // Broken under current semantics for volatile 189 | class Foo { 190 | private volatile Helper helper = null; 191 | public Helper getHelper() { 192 | if (helper == null) { 193 | synchronized(this) { 194 | if (helper == null) 195 | helper = new Helper(); 196 | } 197 | } 198 | return helper; 199 | } 200 | } 201 | 202 | 203 | + 如果Helper是一个不可变的对象,这样Helper的所有字段都是final的,那么双重检查锁定就可以工作,而不必使用volatile字段 204 | 对一个不可变对象(比如一个String或者一个Integer)的引用应该和int或者float类似。读取和写入对不可变对象的引用是原子操作。 205 | 206 | ## 说明 207 | 208 | 笔记内容来自:
209 | [The "Double-Checked Locking is Broken" Declaration](http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html) -------------------------------------------------------------------------------- /深入理解Java虚拟机/Java内存区域划分.md: -------------------------------------------------------------------------------- 1 | # Java内存区域划分 2 | 3 | ### 运行时数据区域: 4 | 5 | #### 线程隔离的数据区 6 | 7 | 1. *程序计数器(Program Counter Register):* 8 | 9 | `程序计数器`是一块较小的内存空间,可以看作是当前线程所执行的`字节码`的行号指示器.
由于Java虚拟机的多线程是通过线程轮流转换并分配处理器执行时间的方式来实现的,所以每条线程都需要有一个独立的程序计数器. 10 | 11 | 2. *Java虚拟机栈(Java Virtual Machine Stacks):* 12 | 13 | 线程私有,生命周期与线程相同.
14 | 15 | - *描述Java方法执行的内存模型:* 16 | 17 | 每个方法执行时都会创建一个栈帧,用于存放`局部变量表`,`操作数栈`,`动态链接`,`方法出口`等.
每个方法从调用到完成,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程. 18 | 19 | 20 | 3. *本地方法栈(Native Method Stack):* 21 | 22 | 本地`方法栈`与`Java虚拟机栈`类似,不过它是服务于虚拟机用到的`Native方法`.
有的虚拟机将`本地方法栈`与`虚拟机栈`合二为一(Sun HotSpot). 23 | 24 | 什么是`Native方法`?
25 | Native方法就是一个非java语言实现的方法 26 | 27 | 28 | ---- 29 | #### 所有线程共享的数据区 30 | 31 | 4. *Java堆(Java Heap):* 32 | 33 | `Java堆`是被所有线程共享的一块用于`存放对象实例`的内存区域,在虚拟机启动时创建.一般情况下,所有的`对象实例`和`数组`都要在堆上分配.
34 | 35 | 从垃圾回收的角度: 可以分为`新生代`和`老年代`
36 | 从内存分配的角度: 线程共享的堆可以划分出`多个线程私有`的`分配缓冲区`(TLAB). 37 | 38 | 39 | 5. *方法区(Method Area):* 40 | 41 | 用于存储已被虚拟机加载的`类信息`,`常量`(final),`静态变量`(static),`即时编译器编译后的代码`等数据.
42 | 这个区域的垃圾回收比较少,但并不是没有(主要针对`常量池的回收`和`类型的卸载`). 43 | 44 | - *运行时常量池(Runtime Constant Pool)*: 45 | 46 | *`常量池(Constant Pool Table)`*: Class文件中的一项描述信息,用于存放编译器生成的各种`字面量`和`符号引用`. 47 | 48 | `运行时常量池`是`方法区`的一部分.`Class文件`中的`常量池`信息会在类加载后存放在`运行时常量池中`.
49 | 50 | > 注意: 在虚拟机规范中,`方法区`是`堆`的一个`逻辑部分`,故`Java堆`和`方法区`同属于广义上的`堆`. 51 | 52 | --- 53 | ### 虚拟机对象(HotSpot): 54 | 55 | 56 | #### 对象的创建过程: 57 | 58 | > 注: 只针对普通Java对象,不包括数组和Class对象等. 59 | 60 | 1. 虚拟机遇到`new指令`时会检查这个指令的参数能否在常量池中定位到一个`类的符号引用` 61 | 62 | 2. 检查引用代表的类是否执行`类加载`过程(被`加载`,`解析`和`初始化`过),若没有就执行相应过程. 63 | 64 | 3. `类加载`检查通过后,虚拟机就为新生对象分配空间. 65 | 66 | - 并发情况下的线程安全解决方案: 67 | 68 | 1. 对分配内存空间的动作进行`同步处理`. 69 | 70 | 2. 哪个线程需要分配内存,就在自己的`本地内存分配缓冲(TLAB)`上分配.`TLAB`用完后才会`同步锁定分配新的TLAB`. 71 | 72 | 4. 分配完成之后,虚拟机将分配到的内存空间(包括实例属性)`初始化为零值`(可提前在TLAB分配时进行). 73 | 74 | 5. 执行``方法,按程序员的意愿初始化. 75 | 76 | 77 | #### 对象的内存布局: 78 | 79 | 对象在内存中可分为三个区域: `对象头(Header)`,`示例数据(Instance Data)`和`对齐补充(Padding)`. 80 | 81 | 1. 对象头: 82 | 83 | 对象头包含两部分信息: 84 | 85 | 1. `Mark Word`: 用于存放对象自身的`运行时数据`(`哈希码`,`GC分代年龄`,`锁状态标志`,`线程持有的锁`...). 86 | 2. `类型指针`: 指向该对象的`元数据类型(应该是Class对象)`的指针,虚拟机通过它来确定这个对象是哪个类的实例.(若是数组还有一块用于几率数组长度) 87 | 88 | 2. 示例数据: 89 | 90 | 对象真正存储的有效信息,也是程序代码中定义的各种类型的`字段内容`.无论是子类中定义的还是从父类继承的,都要记录起来. 91 | 92 | 93 | 3. 对齐填充: 94 | 95 | 对象的大小必须为`8字节的整数倍`,没有的话需要进行`对齐填充`来补全(类似空格占位). 96 | 97 | #### 对象的访问定位: 98 | 99 | Java程序通过栈上的`reference`引用来指向堆上的具体对象. 100 | 101 | - 对象引用的定位方式: 102 | 103 | 1. 句柄访问: `Java堆`上划分出一块内存作为`句柄池`,`reference`中储存的是对象的`句柄地址`.而句柄中包含了`对象实例数据地址`和`对象类型数据地址`. 104 | 105 | 优点: `reference`中储存的是稳定的`句柄地址`,在对象移动时(垃圾收集时移动很普遍)只会改变句柄池中的`实例数据指针`,`reference`本身不用修改. 106 | 107 | 2. 直接指针访问: `reference`中储存`对象地址`.而`对象地址`中包含了`对象实例数据`和`对象类型数据地址`. 108 | 109 | 优点: 速度更快,节省了一次指针定位的开销. 110 | -------------------------------------------------------------------------------- /深入理解Java虚拟机/Java内存模型.md: -------------------------------------------------------------------------------- 1 | # Java内存模型 2 | 3 | ## 一. 基础(硬件效率的一致性): 4 | 5 | 6 | ### 1. 计算机内存模型: 7 | 8 | 为了提高计算能力,减少计算机存储设备与处理器运算速度的差距,现代计算机系统在`内存`与`处理器`之间加入了一层读写速度与处理器运算速度尽可能接近的`高速缓存(Cache)`作为内存与处理器之间的缓冲.
9 | 将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存中,这样处理器就无须等待缓慢的内存读写. 10 | 11 | ![处理器,高速缓存,主内存间的关系](./pic/2017-11-05-1.png) 12 | 13 | 14 | + 这种模型虽然很好的解决了处理器与内存的速度矛盾,但却引入了`缓存一致性(Cahce Coherence)`的问题: 15 | 16 | 多处理器系统中,每个处理器都有自己的高速缓存,它们共享同一块`主存(Main Memory)`.当多个处理器的运算都涉及同一块主内存区域时,可能导致各自的缓存数据不一致. 17 | + 为了解决一致性问题,需要各个处理器访问缓存时都遵循`一致性协议`,读写时根据协议进行操作 18 | 19 | 20 | ### 2. 处理器指令重排序: 21 | 22 | 为了使处理器内部单元能被充分利用,处理器可能会对代码进行`乱序执行(Out-Out-Order Execution)优化`,处理器会在计算之后将乱序执行的结果重组,保证结果与顺序执行结果一致,但并不保证与输入代码的顺序一致.
23 | + Java虚拟机的即时编译器中也有类似的`指令重排序(Instruction Reorder)`优化. 24 | 25 | 26 | 27 | ## 二. Java内存模型: 28 | 29 | ### 1. 内存模型中的关系(可与计算机内存交互关系类比): 30 | 31 | 1. Java规定了所有的变量(`静态字段`,`实例字段`和`构成数组对象的元素`)都存储在`主内存`中. 32 | 33 | 2. 每条线程拥有自己的`工作内存`,其中保存了该线程使用到的`主内存副本拷贝`. 34 | 35 | 3. 线程对变量的所有操作(赋值,读取等)都必须在自己的工作内存中进行,而不能直接读写主内存中的变量.不同的线程之间也无法直接访问对方工作内存中的变量.
36 | 线程间的变量值传递均需要通过主内存来完成. 37 | 38 | ![线程,工作内存和主内存交互关系](./pic/2017-11-05-2.png) 39 | 40 | 41 | 56 | 57 | + `主内存`主要对应于`Java堆`中的`对象实例`数据部分,`工作内存`则对应于`虚拟机栈`中的部分区域.
58 | 59 | 从更低的层次上说,`主内存`直接对应于`物理硬件的内存`,而为了获取更好的运行速度,虚拟机(甚至是硬件系统本身的优化措施)可能会让`工作内存`优先存储于`寄存器`和`高速缓存`中. 60 | 61 | 62 | ### 2. 内存间交互操作: 63 | 64 | 1. Java内存模型定义了8种操作(原子,不可再分)来完成主内存与工作内存之间的具体交互细节: 65 | 66 | + lock(锁定): 作用于`主内存变量`,把一个变量标识为一条线程独占状态. 67 | 68 | + unlock(解锁): 作用于`主内存变量`,把处于锁状态的变量释放出来. 69 | 70 | + read(读取): 作用于`主内存变量`,把变量值从`主内存`传到线程的`工作内存`,以便`load`动作使用. 71 | 72 | + load(载入): 作用于`工作内存变量`,把`read`操作从`主内存`得到的变量放入`工作内存`的`变量副本`中. 73 | 74 | + use(使用): 作用于`工作内存变量`,把变量值传递给`执行引擎`.当虚拟机遇到需要使用变量值的字节码指令时会执行这个操作. 75 | 76 | + assign(赋值): 作用于`工作内存变量`,把`执行引擎`的值,赋给`工作内存变量`.虚拟机遇到赋值的字节码指令执行此操作. 77 | 78 | + store(储存): 作用于`工作内存变量`,把`工作内存变量`的值传送到`主内存`中. 79 | 80 | + write(写入): 作用于`工作内存变量`,把`store`操作从`工作内存`得到的变量的值放入`主内存变量`中. 81 | 82 | + 把变量从`主内存`复制到`工作内存`: 顺序执行`read`和`load`. 83 | + 把变量从`工作内存`同步回`主内存`: 顺序执行`store`和`write`.
84 | (顺序执行,不保证连续执行,可插入其他指令) 85 | 86 | 87 | 88 | 2. Java内存模型规定执行上必须满足以下规则: 89 | 90 | 1. 不允许`read`和`load`,`store`和`write`操作之一单独出现. 91 | 92 | 2. 不允许一个线程丢弃它的`assign`操作. 93 | 94 | 3. 不允许线程无原因的(未发送`assign`)把数据从`工作内存`同步回`主内存`中. 95 | 96 | 4. 新的变量只能在`主内存`中诞生. 97 | 98 | 5. 一个变量同一时刻只允许一个线程对其进行`lock`操作,`lock`可被同个线程重复执行. 99 | 100 | 6. 对一个变量进行`lock`操作,`工作内存`会清空此变量值,`执行引擎`使用这个变量之前,需重新执行`load`(主内存到工作内存)或者`assign`(执行引擎到工作内存)初始化变量值. 101 | 102 | 7. 不允许`unlock`被其他线程`lock`或未被`lock`的变量. 103 | 104 | 8. 对一个变量`unlock`之前,必须先同步(执行`store`,`write`)回`主内存`中. 105 | 106 | 107 | 108 | 3. volatile的特殊规则(有空另开一篇详细整理下): 109 | 110 | volatile是Java虚拟机提供的最轻量级同步机制. 111 | 112 | + 两种特性: 113 | 114 | 1. 可见性: 保证volatile修饰的变量对所有线程可见. 115 | 116 | + volatile变量只能保证可见性,只能再一下场景使用: 117 | 118 | 1. 运算结果不依赖当前值,或者能保证只有单一线程修改变量值. 119 | 120 | 2. 变量不需要与其他的状态变量共同参与不变约束. 121 | 122 | 2. 禁止重排序优化: 普通变量仅仅会保证该线程方法的执行过程中所依赖的赋值结果的地方都能获得正确结果.`volatile`能保证变量赋值操作顺序与代码中的执行一致. 123 | 124 | + 内存屏障(Memory Barrier/Memory Fence): 指`重排序`时不能把后面的指令重排序到内存屏障之前的位置. 125 | 126 | + volatile变量读操作性能消耗与普通变量几乎没差别,写操作开销比较大. 127 | 128 | + 假设T标识一个线程,V和W分别表示两个volatile型变量,在进行`read`,`load`,`use`,`assign`,`store`和`write`时需要满足如下规则: 129 | 130 | 1. 每次使用V前都必须从`主内存`刷新最新的值,保证能看见其他线程对变量V的最后修改. 131 | 132 | 2. `工作内存`中,每次修改V后都必须立刻同步回主内存中,保证其他线程可以看到自己对变量V的修改. 133 | 134 | 3. 要求volatile修饰的变量不会被指令重排序优化,保证代码`执行顺序`与`程序的顺序`相同. 135 | 136 | 4. 对于long,double型变量的特殊规则: 137 | 138 | + Java内存模型允许虚拟机将没被volatile修饰的64位数据(long和double)的读写操作划分为两次32位操作.即允许虚拟机不保证64位数据的`load`,`read`,`store`和`write`的原子性. 139 | 140 | 141 | ### 4. 原子性,可见性和有序性: 142 | 143 | 1. 原子性(Atomicity): 144 | 145 | + 基本数据类型(long和double除外)的读写是具备原子性的. 146 | 147 | + 更大范围的原子性保证:`synchronized`关键字,Java虚拟机提供`monitorenter`和`monitorexit`式使用`lock`和`unlock`操作. 148 | 149 | 2. 可见性(Visibility): 150 | 151 | + `volatile`保证了多线程操作时变量的可见性. 152 | 153 | + `sychronized`可见性:对一个变量进行`unlock`之前必须把此变量同步回主内存中. 154 | 155 | + `final`可见性: 被`final`修饰的字段在构造器中一旦初始化完成,且构造器没有把`this`引用传递出去,其他线程中就能看见`final`字段的值. 156 | 157 | 3. 有序性(Ordering): 158 | 159 | + Java提供`volatile`和`synchronized`保证线程之间操作的有序性: 160 | 161 | - `volatile`本身包含禁止指令重排序的语义. 162 | 163 | - `synchronized`变量在一个时刻只允许一条线程对其进行`lock`操作,决定同一个锁的两个代码块只能串行进入. 164 | 165 | 166 | ### 5. 先行发生原则(happens-before): 167 | 168 | + 先行发生原则: 判断数据是否存在竞争,线程是否安全的主要依据.如果操作A先行发生于操作B,在发生B操作之前,操作A产生的影响能被操作B观察到. 169 | 170 | + Java内存模型的先行发生关系: 171 | 172 | 1. 程序次序规则: 173 | 174 | 在一个线内,按照代码顺序,书写在前面的操作先行发生于写在后面的操作(注意区分`先行发生`与`先发生`). 175 | 176 | 2. 管程锁定规则: 177 | 178 | 一个`unlock`操作先行发生于后面对同一个锁的`lock`操作. 179 | 180 | 3. volatile变量规则: 181 | 182 | 对一个`volatile`变量的写操作先行发生于后面对这个变量的读操作. 183 | 184 | 4. 线程启动规则: 185 | 186 | `Thread`对象的start()方法先行发生于此线程的每个动作. 187 | 188 | 5. 线程终止规则: 189 | 190 | 线程中的所有操作都先行发生于对此线程的终止检测. 191 | 192 | 6. 线程中断规则: 193 | 194 | 对线程`interrupt()`方法的调用先行发生于被中断线程的代码检测中断事件发生. 195 | 196 | 7. 对象终结规则: 197 | 198 | 一个对象的初始化完成(构造函数执行结束),先行发生于它的`finalize()`方法的开始. 199 | 200 | 8. 传递性: 201 | 202 | 操作A先行发生于操作B,操作B先行发生于操作C,则操作A先行发生于操作C. 203 | 204 | 205 | 206 | -------------------------------------------------------------------------------- /深入理解Java虚拟机/jvm垃圾收集与内存回收策略.md: -------------------------------------------------------------------------------- 1 | # 垃圾收集器与内存分配 2 | 3 | ### 一.判断对象是否存活的算法: 4 | 5 | 1. *引用计数法:* 6 | 7 | 给对象添加一个引用计数器,每一个地方引用它时计数器加1;引用失效时计数器减1.为值为0的对象不可能再被使用. 8 | 9 | 缺点:
10 | 难以解决对象之间循环引用的问题(对象A引用对象B,对象B引用对象A,两个对象无实际意义) 11 | 12 | 2. *可达性分析算法:* 13 | 14 | 通过一些称为"GC_ROOTS"的对象作为起点,开始向下搜索引用它的对象,这个过程走过的路径即"引用链".当一个对象到"GC_ROOTS"没有任何引用链相连,证明此对象不存活.
15 | 16 | 可作为GC_ROORS的对象:
17 | 1) 虚拟机栈(栈帧中的本地变量表)中引用的对象
18 | 2) 方法区中静态属性引用的对象
19 | 3) 方法区中常量引用的对象
20 | 4) 本地方法栈中JNI(Native方法)引用的对象 21 | 22 | 23 | ### 二.四种引用方式: 24 | 25 | 1. *强引用(Strong Reference)*: 26 | 27 | 程序代码当中普遍存在的类似"Object obj = new Object()"这类引用. 28 | 只要强引用存在,该对象永远不会被垃圾收集器回收. 29 | 30 | 2. *软引用(Soft Reference):* 31 | 32 | SoftReference类实现,用于描述还有用但非必需的对象. 33 | 在系统将要发生内存溢出异常之前,会对这些软引用对象进行二次回收. 34 | 35 | 3. *弱引用(Weak Reference):* 36 | 37 | WeakReference类实现,也是用于描述非必须对象,强度比软引用更弱. 38 | 无论内存是否足够,弱引用对象都会在下一次垃圾收集发生时进行回收. 39 | 40 | 4. *虚引用(Phantom Reference):* 41 | 42 | PhantomReference类实现,虚引用无法影响对象的生存时间,唯一用处是在对象被回收时收到一个系统通知. 43 | 44 | 45 | ### 三.对象死亡的判定过程: 46 | 47 | + 一个对象真正被宣告死亡,至少经过两次标记过程: 48 | 49 | 1. 第一次标记:对象经过可达性分析判定不存活 50 | 51 | 2. 判断对象是否覆盖或者是否被调用过finalize方法.若未覆盖或已被调用则直接回收对象,否则将对象放入`F-Queue`队列中去触发finalize方法 52 | 53 | 3. 第二次标记: GC对`F-Queue`中的对象进行筛选,检查对象是否在finalize()中与引用链上的对象进行关联(自救),是则被移出回收集合,否则标记 54 | 55 | 56 | ### 四.垃圾收集的算法: 57 | 58 | 1. *标记 - 清除算法:* 59 | 60 | 标记阶段:标记出所有需要回收的对象 61 | 62 | 清除阶段:统一回收所有被标记的对象 63 | 64 | + 缺点:
65 | 1. 效率:标记和清除两个过程效率都不高. 66 | 2. 空间:标记清除过后会产生大量不连续的内存碎片,导致不发分配足够的连续内存给大对象而提前出发一次垃圾收集 67 | 68 | 69 | 2. *复制算法(用于新生代):* 70 | 71 | 将可用内存按容量分为大小相等的两块,每次只使用其中一块.当一块内存用完后,将存活对象复制到另一块上,然后一次性清理使用过的空间. 72 | 73 | + 优点:
74 | 实现简单,运行高效 75 | 76 | + 缺点:
77 | 内存缩小为一半,代价太高 78 | 79 | + 新生代的复制算法: 80 | 81 | 将新生代内存分为一个较大的Eden和两个较小的Survivor空间,每次使用Eden和一个Survivor.
82 | 当回收时将Eden和Survivor中存活的对象一次性复制到另一块Survivor中,清理刚用过的Eden和Survivor空间.
83 | 当Survivor中不足以存放上一次存活对象时,这些对象将直接通过`分配担保机制`进入老年代.
84 | 85 | 3. *标记 - 整理算法(用于老年代):* 86 | 87 | 标记阶段:标记出所有需要回收的对象 88 | 89 | 整理阶段:让所有存活的对象都向一端移动,然后直接清理端边界以外的内存 90 | 91 | 缺点:
92 | 整理过程耗时 93 | 94 | 95 | 4. *分代收集算法:* 96 | 97 | 把Java堆分为新生代和老年代,根据各个年代特点采用合适的收集算法.
98 | 99 | 新生代:每次GC都有大批对象死去,只有少量存活,故使用复制算法(实际上经过改进)
100 | 101 | 老年代:对象成活率高,使用标记-整理算法 102 | 103 | 104 | 105 | 106 | ### 五.内存的分配与回收策略(对象年龄判定): 107 | 108 | 新生代: Eden + Survivor*2
109 | Eden:Survivor = 8:1 110 | 111 | 112 | 1. *对象优先在Eden上分配:* 113 | 114 | 每次分配可用空间为Eden+Survivor,另一个Survivor在下次垃圾收集时用于存放复制的存活对象. 115 | 116 | 2. *长期存活的对象进入老年代:* 117 | 118 | 对象在Survivor区中每存活一次Minor GC,年龄就增加一岁,当年龄增加到一定程度(默认为15岁)就会从新生代晋升到老年代中 119 | 120 | 3. *对象年龄的动态判定:* 121 | 122 | 如果在Survivor空间中相同年龄所有对象所占空间大于Survivor空间的一半,则大于等于此年龄的对象可以直接晋升老年代. 123 | 124 | 125 | + *空间分配担保:* 126 | 127 | 老年代剩余空间 > 之前转入老年代的对象的平均大小 ---> 直接Major GC 128 | 129 | 老年代剩余空间 < 之前转入老年代的对象的平均大小 && 允许担保失败 ---> 直接Minor GC,不需要做Full GC 130 | 131 | 老年代剩余空间 < 之前转入老年代的对象的平均大小 && 不允许担保失败 ---> 触发Full GC 132 | 133 | 134 | -------------------------------------------------------------------------------- /深入理解Java虚拟机/jvm类加载机制.md: -------------------------------------------------------------------------------- 1 | # 虚拟机类加载机制 2 | 3 | ### 一.类加载时机: 4 | 5 | 1. 类从加载到虚拟机内存~卸载出内存的生命周期(这些阶段通常是交叉混合进行的): 6 | 7 | 加载 --> [验证 --> 准备 --> 解析](这部分统称为连接) --> 初始化 --> 使用 --> 卸载 8 | 9 | 2. *`主动引用`五种必须立即对类进行`初始化`的情况:* 10 | 11 | 1. 遇到new(new实例化对象),getstatic,putstatic(读取/修改类属性,final除外)或invokestatic(调用静态方法)这4条字节码指令时 12 | 2. 使用java.lang.reflect包的方法对类进行反射调用时 13 | 3. 初始化某个类,发现其父类未初始化时(对父类立即初始化) 14 | 4. 虚拟机启动时用户指定的执行主类(包含main()的) 15 | 5. JDK1.7动态语言支持的情况(略) 16 | 17 | 18 | 3. *除上述五种场景外所有的方式都不会触发初始化,称为`被动引用`:* 19 | 20 | 1. 通过子类引用父类的类属性,不会触发子类初始化: 21 | 对于静态字段,只有直接定义这个字段的类才会被初始化.通过子类引用父类中定义的类属性,只会触发父类的初始化. 22 | 2. 通过类数组定义来引用类,不会触发此类的初始化(SuperClass[] a=new SuperClass[10]): 23 | 类数组是由虚拟机自动生成,直接继承于Object的子类,只会触发`[SuperClass`的初始化. 24 | 3. 使用某一类的常量不会触发该定义类的初始化: 25 | 在编译阶段已经将该`定义类`(定义该常量的类)的常量存储到了`调用类`(调用此常量的类)的`常量池`中,以后的每一次引用实际上都转化为`调用类`对自身常量池的引用. 26 | 4. 一个接口在初始化时,并不要求父接口也全初始化,只有真正使用父接口(如引用接口常量)才会初始化. 27 | 28 | 29 | 30 | ### 二.类加载过程: 31 | 32 | 1. *加载:* 33 | 34 | + 加载过程: 35 | 36 | 1) 通过类的权限定名获取类的二进制字节流. 37 | 2) 将这个字节流代表的`静态存储结构`转化为方法区的运行时数据结构. 38 | 3) 在内存中生成代表这个类的Class对象,作为这个类在方法区的各种数据的访问入口. 39 | 40 | + 数组类本身不通过类加载器创建,而是由Java虚拟机直接创建的. 41 | 42 | 一个数组类的创建遵循以下规则: 43 | 44 | 1) 若数组的组件类型是引用类型,则遵循上述加载过程加载这个类型.这个数组将在加载该组件类型的加载器的类名称空间上被标识. 45 | 46 | 2) 若数组的组件类型为基本类型,则虚拟机把数组标记为与启动类加载器关联. 47 | 48 | 3) 数组可见性与组件类型可见性一致,基本类型默认为public. 49 | 50 | + 加载与连接交叉进行(开始时间固定先后). 51 | 52 | 2. *验证:* 53 | 54 | 目的:确保Class文件的字节流中的信息符合当前虚拟机要求,不会危害虚拟机安全. 55 | 56 | + 验证的四个阶段: 57 | 58 | 1) 文件格式验证: 59 | 60 | 验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理: 61 | a.是否以魔数0xCAFEBABY开头. 62 | b.主次版本号是否在当前虚拟机处理范围之内. 63 | c.是否支持常量池中的常量类型(检查常量tag标志). 64 | d.指向常量的各种索引值是否存在且符合类型. 65 | e.CONSTANT_Utf8_info型的常量中是否符合UTF8编码. 66 | f.Class文件中各个部分及文件本身是否有被删除或附加的信息. 67 | ... 68 | 69 | 2) 元数据验证: 70 | 71 | 对字节码描述的信息进行语义分析,保证其描述的信息符合Java语言规范的要求: 72 | a.是否有父类(除Object外都须有父类). 73 | b.是否继承了不允许被继承的类(final). 74 | c.如果不是抽象类,是否实现了父类或接口中的所有方法. 75 | d.类中字段,方法是否与父类产生矛盾 76 | ... 77 | 78 | 3) 字节码验证: 79 | 80 | 通过数据流和控制流分析,确定程序语义是合法符合逻辑的(最复杂的部分): 81 | a.保证操作数栈的数据类型与指令代码序列都能配合工作. 82 | b.保证跳转指令不会跳到方法体之外的字节码. 83 | c.保证方法体中的类型转换是有效的. 84 | .... 85 | 86 | 4) 符号引用验证: 87 | 88 | 对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验.发生在虚拟机将符号引用转化为直接引用的时候(在解析阶段发生): 89 | a.符号引用中通过字符串描述的权限定名是否能够找到对应类. 90 | b.指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段. 91 | c.符号引用中的类,字段,方法的访问性是否可被当前类访问. 92 | ... 93 | 94 | 通过-Xverify:none参数可关闭大部分类的验证措施,缩短加载时间. 95 | 96 | 97 | 3. *准备:* 98 | 99 | 正式为类分配内存并设置类变量初始值(数据类型的零值)的阶段,这些变量所使用的内存都将在方法区中进行分配.
100 | 这时候进行内存分配的仅包括`类变量`(static修饰的静态变量),而不包括实例变量(实例变量将在对象实例化时随着对象一起分配在Java堆上). 101 | 102 | 特殊情况: 如果是被final修饰,则一开始就初始化为其指定的值. 103 | 104 | 105 | 4. *解析:* 106 | 107 | 虚拟机将常量池内的符号引用替换为直接引用的过程. 108 | 109 | `符号引用`: 以一组符号来描述所引用的目标,符号可以是任何形式的字面量,能无歧义地定位到目标(未必在内存中).(与虚拟机实现的内存布局无关) 110 | 111 | `直接引用`: 可以是直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄.(和虚拟机实现的内存布局相关) 112 | 113 | + 解析动作: 114 | 115 | 1) 类或接口解析: 116 | 2) 字段解析 117 | 3) 类方法解析 118 | 4) 接口方法解析 119 | (下面三种与动态语言相关) 120 | 5) 方法类型解析 121 | 6) 方法句柄解析 122 | 7) 调用点限定符解析 123 | 124 | 125 | 126 | 127 | 5. *初始化:* 128 | 129 | 初始化阶段是根据程序员在程序中制定的主观计划区初始化类变量和其他资源,即执行类构造器()方法的过程. 130 | 131 | + \执行过程的特点和细节: 132 | 1) \()是由编译器自动收集类中所有`类变量的赋值动作`和`静态语句块(static{})中的语句`合并并而成的. 133 | 编译器收集的顺序是由语句在源文件中出现的顺序所决定的 134 | 注:静态语句块只能访问到定义静态语句块之前的变量;无法访问在它之后的变量,但却可以赋值. 135 | 136 | 137 | public class Test{ 138 | static{ 139 | i = 0; //赋值语句正常编译通过 140 | System.out.println(i); //编译器会提示非法向前引用 141 | } 142 | static int i = 1; 143 | } 144 | 145 | 146 | 2) 虚拟机会保证在子类\()方法执行之前,父类的\()方法已经执行完毕. 147 | 148 | 3) 由于父类的\()先执行,则意味着父类的静态语句块要优先于子类的变量赋值操作. 149 | 150 | 4) \()对于类或接口来说并不是必需的,若没有静态语句块,也没有类变量赋值操作,则可以不生成 151 | \()方法. 152 | 153 | 5) 接口与类不同,执行接口的\方法不需要先执行父接口的\()方法.只有使用到父接口中的 154 | 类变量时,父接口才会初始化(接口实现类初始化也不会执行接口的\()方法). 155 | 156 | 6) 虚拟机会保证一个类的\()方法在多线程的情况下能被正确的加锁,同步(如果\()中有 157 | 耗时很长的操作,可能导致多个线程阻塞). 158 | 159 | 160 | 161 | 162 | + 类加载器: 163 | 164 | 通过一个类的权限定名来获取描述此类的`二进制字节流`. 165 | 166 | 1. 类与类加载器: 167 | 168 | `类与加载它的类加载器`一起确立这个类在虚拟机中的唯一性. 169 | (来源于同一个class文件的两个类,被同一个虚拟机加载,只要加载的类加载器不同,那这两个类必定不相等) 170 | 171 | 2. 双亲委派模型: 172 | 173 | + Java虚拟机的角度存在两种类加载器: 174 | 175 | 1) `启动类加载器`Bootstrap ClassLoader(C++实现,虚拟机的一部分). 176 | 2) 其他类加载器(Java语言实现,继承抽象类ClassLoader) 177 | 178 | + 开发人员角度存在四种类加载器: 179 | 180 | 1) `启动类加载器`Bootstrap ClassLoader: 181 | 加载\lib目录下,或者被-Xbootclasspath参数指定的路径下的能被虚拟机识别的类库. 182 | 2) `扩展类加载器`Extension CLassLoader: 183 | 加载\lib\ext目录下,或者被java.ext.dirs变量指定的路径下的类库. 184 | 3) `应用程序类加载器`Application ClassLoader(默认类加载器): 185 | 一般也称为系统类加载器.加载用户类路径(ClassPath)上所指定的类库. 186 | 4) `自定义类加载器` 187 | 188 | + 双亲委派模型工作流程: 189 | 190 | 1) 一个类加载器收到类加载请求,先把请求`委派`给`父类加载器`,因此所有的请求都会传递到顶层的启动类加载器中. 191 | 2) 当父类加载器反馈自己无法完成这个加载请求(它的搜索范围内找不到这个类)时,子加载器才会`尝试自己去加载`. 192 | 193 | - 优点: 194 | 195 | Java类随着类加载器一起具备了一中带有优先级的层次关系.保证了Java类型体系中的基础行为(唯一性). 196 | 197 | 198 |  3. 打破双亲委派模型: 199 |  Tomcat 自定义了一套双亲委派模型,当应用使用Spring管理对象时,必然会打破双亲委派模型: 200 |  Spring使用线程上下文下载器委托下层加载器加载对象 201 | -------------------------------------------------------------------------------- /深入理解Java虚拟机/pic/2017-11-05-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wususu/Notes/c566a18c29c64943f6043b7580536693ed68f13d/深入理解Java虚拟机/pic/2017-11-05-1.png -------------------------------------------------------------------------------- /深入理解Java虚拟机/pic/2017-11-05-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wususu/Notes/c566a18c29c64943f6043b7580536693ed68f13d/深入理解Java虚拟机/pic/2017-11-05-2.png -------------------------------------------------------------------------------- /深入理解Java虚拟机/pic/2017-12-13-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wususu/Notes/c566a18c29c64943f6043b7580536693ed68f13d/深入理解Java虚拟机/pic/2017-12-13-1.png -------------------------------------------------------------------------------- /深入理解Java虚拟机/锁优化.md: -------------------------------------------------------------------------------- 1 | # 锁优化 2 | 3 | ## 虚拟机锁优化技术 4 | 5 | 1. 自旋锁 6 | 2. 自适应自旋锁 7 | 3. 锁消除 8 | 4. 锁粗化 9 | 5. 轻量级锁 10 | 6. 偏向锁 11 | 12 | ## 自旋锁与自适应自旋锁 13 | 14 | 在很多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得 15 | 16 | #### 自旋锁:让线程执行一个忙循环(自旋),如果自选超过了限定次数仍没有成功获得锁,就使用传统方式挂起线程。 17 | 18 | #### 自适应自旋锁: 自旋的时间不再固定,由前一次在同个锁上的自旋时间锁的拥有者的状态来决定。(“自己学习”) 19 | 20 | 21 | ## 锁消除 22 | 23 | #### 锁消除: 在虚拟机即时编译器运行时,对一些代码上要求同步,但是是被检测到不可能存在共享数据竞争的锁进行消除。 24 | 25 | 锁消除的主要判定依据来自于逃逸分析的数据支持,如果判断一段代码中,堆上的所有数据都不会逃逸出去被其他线程访问,那就可以把他们当做栈上数据对待,认为是线程私有的,不进行同步加锁。 26 | 27 | 28 | ## 锁粗化 29 | 30 | 连续对同一对象反复加锁和解锁,会导致不必要的性能消耗 31 | 32 | #### 锁粗化:如果虚拟机探测到有一串零碎的操作都对同一个对象加锁,将会把加锁的同步范围拓展(粗化)到整个操作序列的外部。 33 | 34 | 35 | ## 轻量级锁 36 | 37 | “轻量级”是相对于使用操作系统互斥量来实现的传统锁而言,传统的锁叫“重量级锁” 38 | 39 | 在没有多线程竞争的前提下,能减少传统的重量级锁使用产生的性能消耗。轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间多个线程访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。 40 | 41 | 42 | ## 偏向锁 43 | 44 | #### 偏向锁: 这个锁会偏向于第一个获得它的线程,在接下来的执行过程中,如果该锁没有被其他线程获取,则持有偏向锁的线程将永远不需要再进行同步 45 | 46 | 47 | ## 几种锁的对比 48 | 49 | ![](./pic/2017-12-13-1.png) 50 | -------------------------------------------------------------------------------- /设计模式/images/20130712102729562.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wususu/Notes/c566a18c29c64943f6043b7580536693ed68f13d/设计模式/images/20130712102729562.png -------------------------------------------------------------------------------- /设计模式/images/20130713162941328.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wususu/Notes/c566a18c29c64943f6043b7580536693ed68f13d/设计模式/images/20130713162941328.png -------------------------------------------------------------------------------- /设计模式/images/20130713163800203.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wususu/Notes/c566a18c29c64943f6043b7580536693ed68f13d/设计模式/images/20130713163800203.png -------------------------------------------------------------------------------- /设计模式/images/944365-24c6bf44da1b79ad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wususu/Notes/c566a18c29c64943f6043b7580536693ed68f13d/设计模式/images/944365-24c6bf44da1b79ad.png -------------------------------------------------------------------------------- /设计模式/images/944365-c736416f78a5b2d5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wususu/Notes/c566a18c29c64943f6043b7580536693ed68f13d/设计模式/images/944365-c736416f78a5b2d5.png -------------------------------------------------------------------------------- /设计模式/单例模式.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wususu/Notes/c566a18c29c64943f6043b7580536693ed68f13d/设计模式/单例模式.md -------------------------------------------------------------------------------- /设计模式/工厂方法模式.md: -------------------------------------------------------------------------------- 1 | # 工厂方法模式 2 | 3 | ## 简介 4 | 5 | 由于简单工厂模式中静态工厂方法通过所传入参数的不同来创建不同的产品,这必定要修改工厂类的源代码,将违背“开闭原则”,所以引入工厂方法模式实现增加新产品而不影响已有代码
6 | 7 | 在工厂方法模式中,工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象,这样做的目的是将产品类的实例化操作延迟到工厂子类中完成,即通过工厂子类来确定究竟应该实例化哪一个具体产品类。 8 | 9 | ## 结构 10 | 11 | ![结构图](images/20130712102729562.png) 12 | 13 | 1. Product(抽象产品): 14 | 15 | 定义产品的接口,工厂方法模式创建对象的超类型,即具体产品的公共父类 16 | 17 | 2. ConcreteProduct(具体产品): 18 | 19 | 实现了抽象产品接口由某种类型的具体工厂创建实例.具体产品和具体工厂之间一一对应 20 | 21 | 3. Factory(抽象工厂): 22 | 23 | 声明了工厂方法的方法接口.所有的具体工厂都要实现该接口 24 | 25 | 4. ConcreteFactory(具体工厂): 26 | 27 | 实现了抽象工厂中定义的工厂方法,可由客户端调用,新建并放回一个具体产品实例 28 | 29 | ## Java代码实现 30 | 31 | ## 优点 32 | 33 | 1. 在工厂方法模式中,工厂方法用来创建客户所需要的产品,同时还向客户隐藏了哪种具体产品类将被实例化这一细节,用户只需要关心所需产品对应的工厂,无须关心创建细节,甚至无须知道具体产品类的类名。 34 | 35 | 2. 基于工厂角色和产品角色的多态性设计是工厂方法模式的关键。它能够使工厂可以自主确定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部。工厂方法模式之所以又被称为多态工厂模式,是因为所有的具体工厂类都具有同一抽象父类。 36 | 37 | 3. 使用工厂方法模式的另一个优点是在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改其他的具体工厂和具体产品,而只要添加一个具体工厂和具体产品就可以了。这样,系统的可扩展性也就变得非常好,完全符合“开闭原则”。 38 | 39 | ## 缺点 40 | 41 | 1. 在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。 42 | 43 | 2. 由于考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度,且在实现时可能需要用到DOM、反射等技术,增加了系统的实现难度。 44 | 45 | ## 适用场景 46 | 47 | 1. 一个类不知道它所需要的对象的类:在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建;客户端需要知道创建具体产品的工厂类。 48 | 49 | 2. 一个类通过其子类来指定创建哪个对象:在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏代换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。 50 | 51 | 3. 将创建对象的任务委托给多个工厂子类中的某一个,客户端在使用时可以无须关心是哪一个工厂子类创建产品子类,需要时再动态指定,可将具体工厂类的类名存储在配置文件或数据库中。 52 | 53 | 参阅: 54 | > [工厂三兄弟之工厂方法模式](https://quanke.gitbooks.io/design-pattern-java/%E5%B7%A5%E5%8E%82%E6%96%B9%E6%B3%95%E6%A8%A1%E5%BC%8F-Factory%20Method%20Pattern.html) 55 | 56 | > [工厂方法模式(Factory Method Pattern)](http://design-patterns.readthedocs.io/zh_CN/latest/creational_patterns/factory_method.html) -------------------------------------------------------------------------------- /设计模式/建造者模式.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wususu/Notes/c566a18c29c64943f6043b7580536693ed68f13d/设计模式/建造者模式.md -------------------------------------------------------------------------------- /设计模式/抽象工厂模式.md: -------------------------------------------------------------------------------- 1 | # 抽象工厂模式 2 | 3 | ## 简介 4 | 5 | 抽象工厂模式(Abstract Factory Pattern):提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。抽象工厂模式又称为Kit模式,属于对象创建型模式。 6 | 7 | ![产品族与等级](images/20130713162941328.png) 8 | 9 | 抽象工厂模式与工厂方法模式最大的区别在于,工厂方法模式针对的是一个产品等级结构,而抽象工厂模式需要面对多个产品等级结构,一个工厂等级结构可以负责多个不同产品等级结构中的产品对象的创建。
10 | 当一个工厂等级结构可以创建出分属于不同产品等级结构的一个产品族中的所有对象时,抽象工厂模式比工厂方法模式更为简单、更有效率。
11 | 可以说,抽象工厂模式是工厂方法模式复杂情况下的改进方案. 12 | 13 | 14 | + 抽象工厂模式的特点: 15 | 16 | 增加新的产品族符合"开闭原则",在增加新的产品结构不满足"开闭原则" 17 | 18 | ## 结构 19 | 20 | ![结构图](images/20130713163800203.png) 21 | 22 | 1. AbstractFactory(抽象工厂): 23 | 24 | 声明了一组用于创建一族产品的方法,每一个方法对应一种产品. 25 | 26 | 2. ConcreteFactory(具体工厂): 27 | 28 | 实现了在抽象工厂中声明的创建具体产品的方法,生成一组具体产品,这些产品构成了一个产品族. 29 | 30 | 3. AbstractProduct(抽象产品): 31 | 32 | 为每种产品声明接口,在抽象产品中声明了产品所有的业务方法. 33 | 34 | 4. ConcreteProduct(具体产品): 35 | 36 | 定义具体工厂创建的具体产品对象,实现抽象产品接口中声明的业务方法. 37 | 38 | ## Java代码 39 | 40 | ## 优点 41 | 42 | 1. 抽象工厂模式隔离了具体类的生成,使得客户并不需要知道什么被创建。由于这种隔离,更换一个具体工厂就变得相对容易,所有的具体工厂都实现了抽象工厂中定义的那些公共接口,因此只需改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。 43 | 44 | 2. 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。 45 | 46 | 3. 增加新的产品族很方便,无须修改已有系统,符合“开闭原则”。 47 | 48 | ## 缺点 49 | 50 | 增加新的产品等级结构麻烦,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,这显然会带来较大的不便,违背了“开闭原则”。 51 | 52 | ## 适用场景 53 | 54 | 1. 一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有类型的工厂模式都是很重要的,用户无须关心对象的创建过程,将对象的创建和使用解耦. 55 | 56 | 2. 系统中有多于一个的产品族,而每次只使用其中某一产品族。可以通过配置文件等方式来使得用户可以动态改变产品族,也可以很方便地增加新的产品族. 57 | 58 | 3. 属于同一个产品族的产品将在一起使用,这一约束必须在系统的设计中体现出来。同一个产品族中的产品可以是没有任何关系的对象,但是它们都具有一些共同的约束,如同一操作系统下的按钮和文本框,按钮与文本框之间没有直接关系,但它们都是属于某一操作系统的,此时具有一个共同的约束条件:操作系统的类型. 59 | 60 | 4. 产品等级结构稳定,设计完成之后,不会向系统中增加新的产品等级结构或者删除已有的产品等级结构. 61 | 62 | 引自 63 | > [工厂三兄弟之抽象工厂模式](https://quanke.gitbooks.io/design-pattern-java/%E6%8A%BD%E8%B1%A1%E5%B7%A5%E5%8E%82%E6%A8%A1%E5%BC%8F-Abstract%20%20Factory%20Pattern.html) 64 | 65 | > [抽象工厂模式(Abstract Factory)](http://design-patterns.readthedocs.io/zh_CN/latest/creational_patterns/abstract_factory.html) -------------------------------------------------------------------------------- /设计模式/模板方法.md: -------------------------------------------------------------------------------- 1 | # 模板方法模式 2 | 3 | > 模板方法基于继承的代码复用,是一种类行为型模式 4 | 5 | ## 介绍 6 | 7 | 某些业务的实现需要多个流程,有些步骤是固定的,而有些是存在可变性的.(即: 步骤次序固定,存在公共代码)
8 | 9 | 为了提高代码复用性和系统灵活性,可以将相同的步骤实现代码放在父类中,而对于存在可变性的方法,就只在父类中做一个声明,具体实现等待子类来完成,不同的子类可以有不同的实现方式.
10 | 11 | 12 | ## 结构 13 | 14 | 1. 抽象父类: 15 | 16 | 类中定义了一系列基本方法对应每一个步骤,这些方法可以是抽象也可以是具体,子类可以重新定义,直接使用或实现这些方法.
17 | 实现一个模板方法(调用了其他方法),用于定义一个算法框架(整个流程). 18 | 19 | 2. 具体子类: 20 | 21 | 用于覆盖父类中的具体方法或者实现抽象方法. 22 | 23 | ## 基本方法 24 | 25 | + 基本方法是模板方法的组成成分 26 | 27 | 1. 抽象方法: 28 | 29 | 由抽象父类声明,具体子类实现 30 | 31 | 2. 具体方法: 32 | 33 | 由抽象类声明,子类覆盖或者直接继承使用 34 | 35 | 3. 钩子方法: 36 | 37 | 一个钩子方法由一个抽象类或具体类声明并实现,而其子类可能会加以扩展。通常在父类中给出的实现是一个空实现,并以该空实现作为方法的默认实现.
38 | 当然钩子方法也可以提供一个非空的默认实现.
39 | 40 | 钩子方法有两类: 41 | 42 | a. 与'实现步骤'挂钩,用于决定模板方法中的具体步骤,方法名一般为`iSXXX()`,对某个条件进行判断,以决定是否执行某步骤.
43 | b. 实现体为空,子类可以选择覆盖或继承该钩子方法 44 | 45 | ## Java代码实现: 46 | 47 | 48 | 49 | ## 优点 50 | 51 | 1. 父类中形式化地定义一个算法,而由它的子类来实现细节的处理,在子类实现详细的处理算法时并不会改变算法中步骤的执行次序 52 | 53 | 2. 提高代码的复用性 54 | 55 | 3. 利用面向对象的多态性,实现一种反向控制结构,通过子类覆盖父类的钩子方法来决定某一特定步骤是否需要执行 56 | 57 | 4. 通过子类来覆盖父类的基本方法,不同的子类可以提供基本方法的不同实现,更换和增加新的子类很方便,符合单一职责原则和开闭原则。 58 | 59 | 60 | ## 适用情景: 61 | 62 | (1) 对一些复杂的算法进行分割,将其算法中固定不变的部分设计为模板方法和父类具体方法,而一些可以改变的细节由其子类来实现。即:一次性实现一个算法的不变部分,并将可变的行为留给子类来实现。 63 | 64 | (2) 各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。 65 | 66 | (3) 需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制。 67 | -------------------------------------------------------------------------------- /设计模式/策略模式.md: -------------------------------------------------------------------------------- 1 | # 策略模式 2 | 3 | ## 介绍 4 | 5 | 策略模式是对算法的封装,把一系列的算法分别封装到对应的类中,并且这些类实现相同的接口,相互之间可以灵活地替换.(不修改原代码)
6 | 7 | ## 结构 8 | 9 | 1. Context(环境类): 10 | 11 | 使用算法的角色,它在解决某个问题(即实现某个方法)时可以采用多种策略。在环境类中维持一个对抽象策略类的引用实例,用于定义所采用的策略,在运行时调用具体策略类中的方法. 12 | 13 | 2. Strategy(抽象策略类): 14 | 15 | 为所支持的算法声明了抽象方法,是所有策略类的父类,它可以是抽象类或具体类,也可以是接口. 16 | 17 | 3. ConcreteStrategy(具体策略类): 18 | 19 | 实现了在抽象策略类中声明的算法,在运行时,具体策略类将覆盖在环境类中定义的抽象策略类对象,使用一种具体的算法实现某个业务处理. 20 | 21 | ## 与模板方法的区别 22 | 23 | 模板方法也是对不同算法的实现封装到不同的子类中,在开发过程中这两种方法也经常混用.
24 | 25 | 硬要区分的话还是有的: 26 | 27 | 1. 模板方法更注重代码的复用(一般抽象类需是Abstract Class),定义与实现耦合,策略模式更注重策略(不同子类)的选择(抽象类一般是Interface,当然也可以是Abstract Class)将定义与实现分离.
28 | 2. 模板方法模式的算法调用主体在抽象父类中(即`模板方法`的步骤组合),策略模式的算法调用主题在Context类中 29 | 30 | ## Java代码实现 31 | 32 | ## 适用情景 33 | 34 | 1. 一个系统需要动态地在几种算法中选择一种,那么可以将这些算法封装到一个个的具体算法类中,而这些具体算法类都是一个抽象算法类的子类,满足里氏替换原则. 35 | 36 | 2. 一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重条件选择语句来实现.此时,使用策略模式,把这些行为转移到相应的具体策略类里面,就可以避免使用难以维护的多重条件选择语句,使其满足单一职责原则. 37 | 38 | 3. 不希望客户端知道复杂的、与算法相关的数据结构,在具体策略类中封装算法与相关的数据结构,可以提高算法的保密性与安全性. 39 | 40 | 41 | ## 优点 42 | 43 | 1. 易于扩展,增加一个新的策略对策略模式来说非常容易,基本上可以在不改变原有代码的基础上进行扩展.满足“开闭原则”. 44 | 45 | 2. 具体策略类之间可以自由切换. 46 | 47 | 3. 避免使用落后的多重条件选择语句. 48 | 49 | 4. 提供了一种算法的复用机制,可将算法单独提取出来封装在抽象策略类中,不同的环境类可以方便地复用这些算法。 50 | 51 | ## 缺点 52 | 53 | 1. 客户端需要知道所有的具体策略类并自行选择. 54 | 55 | 2. 系统产生多个具体策略类,每个细小的改动豆浆增加一个新的具体策略类.
具体策略类过多也增加了维护的难度. 56 | 57 | > [算法的封装与切换——策略模式](https://quanke.gitbooks.io/design-pattern-java/%E7%AD%96%E7%95%A5%E6%A8%A1%E5%BC%8F-Strategy%20Pattern.html) 58 | > [23种设计模式(12):策略模式](http://blog.csdn.net/zhengzhb/article/details/7609670) -------------------------------------------------------------------------------- /设计模式/简单工厂模式.md: -------------------------------------------------------------------------------- 1 | # 简单工厂模式 2 | 3 | > 属于创建类型模式 4 | 5 | ## 简介 6 | 7 | 简单工厂模式(Simple Factory Pattern):又称为静态工厂方法(Static Factory Method)模式,它属于类创建型模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类or接口. 8 | 9 | ## 结构 10 | 11 | 1. Factory(工厂角色): 12 | 负责创建实例,提供静态工厂方法,返回抽象产品类型. 13 | 14 | 2. Product(抽象产品): 15 | 是创建的实例的父类,负责定义具体产品类型的公共外部接口. 16 | 17 | 3. ConcreteProduct(具体产品): 18 | 工厂模式的创建目标. 19 | 20 | ## Java代码实现 21 | 22 | ## 优点 23 | 24 | 1. 对象的创建职责与业务使用对象职责分离,使修改对象或业务时不用修改另一方的代码 25 | 26 | 2. 客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可,对于一些复杂的类名,通过简单工厂模式可以减少使用者的记忆量. 27 | 28 | 3. 通过引入配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品类,在一定程度上提高了系统的灵活性 29 | 30 | 4. 防止用来实例化一个类的数据和代码在多个类中到处都是,可以将有关创建的知识搬移到一个工厂类中 31 | 32 | 5. 可以提供一系列名字完全不同的工厂方法,每一个工厂方法对应类的一个构造函数,降低客户端调用同一类的不同构造方法出错概率. 33 | 34 | ## 缺点 35 | 36 | (1) 由于工厂类集中了所有产品的创建逻辑,职责过重,一旦不能正常工作,整个系统都要受到影响。 37 | 38 | (2) 使用简单工厂模式势必会增加系统中类的个数(引入了新的工厂类),增加了系统的复杂度和理解难度。 39 | 40 | (3) 系统扩展困难,一旦添加新产品就不得不修改工厂逻辑,在产品类型较多时,有可能造成工厂逻辑过于复杂,不利于系统的扩展和维护。 41 | 42 | (4) 简单工厂模式由于使用了静态工厂方法,造成工厂角色无法形成基于继承的等级结构。 43 | 44 | ## 适用情景 45 | 46 | 1. 工厂类负责创建的对象比较少,由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂. 47 | 48 | 2. 客户端只知道传入工厂类的参数,对于如何创建对象并不关心. 49 | 50 | 参阅: 51 | > [创建对象与使用对象——谈谈工厂的作用](http://blog.csdn.net/lovelion/article/details/7523392)
52 | > [简单工厂模式-Simple Factory Pattern](https://quanke.gitbooks.io/design-pattern-java/%E7%AE%80%E5%8D%95%E5%B7%A5%E5%8E%82%E6%A8%A1%E5%BC%8F-Simple%20Factory%20Pattern.html)
53 | > [简单工厂模式( Simple Factory Pattern )](http://design-patterns.readthedocs.io/zh_CN/latest/creational_patterns/simple_factory.html) -------------------------------------------------------------------------------- /设计模式/适配器模式.md: -------------------------------------------------------------------------------- 1 | # 适配器模式 2 | 3 | ## 简介 4 | 5 | 客户端可以通过目标类的接口访问它所提供的服务。有时,现有的类可以满足客户类的功能需要,但是它所提供的接口不一定是客户类所期望的,这可能是因为现有类中方法名与目标类中定义的方法名不一致等原因所导致的。 6 | 在这种情况下,现有的接口需要转化为客户类期望的接口,这样保证了对现有类的重用。 7 | 8 | + 两种适配器模式: 9 | 10 | 1. 类适配器: 把适配的类的API转换成为目标类的API,使用继承连接到适配类 11 | 12 | 2. 对象适配器: 也是把适配的类的API转换成为目标类的API,使用委派关系连接到适配类 13 | 14 | ## 结构 15 | 16 | ![类适配器](images/944365-24c6bf44da1b79ad.png) 17 | 18 | ![对象适配器](images/944365-c736416f78a5b2d5.png) 19 | 20 | Target:目标抽象类(适配目标) 21 | Adapter:适配器类(适配出来的最终类) 22 | Adaptee:适配者类(被适配的源类) 23 | Client:客户类(调用适配器的类) 24 | 25 | ## Java代码实现 26 | 27 | ## 优点 28 | 29 | 1. 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无须修改原有结构. 30 | 31 | 2. 增加了类的透明性和复用性,将具体的业务实现过程封装在适配者类中,对于客户端类而言是透明的,而且提高了适配者的复用性,同一个适配者类可以在多个不同的系统中复用. 32 | 33 | 3. 灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合“开闭原则”. 34 | 35 | ## 缺点 36 | 37 | 1. 对于Java、C#等不支持多重类继承的语言,一次最多只能适配一个适配者类,不能同时适配多个适配者. 38 | 39 | 2. 适配者类不能为最终类,如在Java中不能为final类,C#中不能为sealed类. 40 | 41 | 3. 在Java、C#等语言中,类适配器模式中的目标抽象类只能为接口,不能为类,其使用有一定的局限性. 42 | 43 | ## 适用场景 44 | 45 | 1. 系统需要使用一些现有的类,而这些类的接口(如方法名)不符合系统的需要,甚至没有这些类的源代码。+ 46 | 47 | 2. 想创建一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。 -------------------------------------------------------------------------------- /面向对象设计六大原则.md: -------------------------------------------------------------------------------- 1 | # 设计模式六大原则 2 | 3 | ### 一.[单一职责链原则](http://blog.csdn.net/zhengzhb/article/details/7278174) 4 | 5 | 不要存在多于一个导致类变更的原因(一个类只负责一项职责)
6 | (要在类的职责扩散到无法控制之前,立即重构) 7 | 8 | ### 二.[里氏替换原则](http://blog.csdn.net/zhengzhb/article/details/7281833) 9 | 10 | 定义1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。 11 | 12 | 定义2:所有引用基类的地方必须能透明地使用其子类的对象。 13 | 14 | ### 三.[依赖倒置原则](http://blog.csdn.net/zhengzhb/article/details/7289269) 15 | 16 | 高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。 17 | 18 | ### 四.[接口隔离原则](http://blog.csdn.net/zhengzhb/article/details/7296921) 19 | 20 | 客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。 21 | 22 | ### 五.[迪米特法则](http://blog.csdn.net/zhengzhb/article/details/7296930) 23 | 24 | 一个对象应该对其他对象保持最少的了解。 25 | 26 | ### 六.[开闭原则](http://blog.csdn.net/zhengzhb/article/details/7296944) 27 | 28 | 一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。 29 | -------------------------------------------------------------------------------- /面经/1.md: -------------------------------------------------------------------------------- 1 | # 唯品会Java金融爬虫实习面经(通过) 2 | 3 | ## 一面 4 | 5 | 一面是部门老大面试 6 | 7 | 1. 自我介绍 8 | 9 | 2. 看过什么书籍 10 | 11 | 《Think in Java》 《深入理解Java虚拟机》 《Java并发编程实战》 《大型互联网技术架构》等 12 | 13 | 3. 学校教过什么课程 14 | 15 | 照实回答 16 | 17 | 3. Jvm虚拟机这本书是全啃完了吗 18 | 19 | 没有,跳着看 20 | 21 | 4. 说说在爬虫中遇到哪些反爬 22 | 23 | 同一ip请求频繁会验证码反爬,通过接入第三方识别服务解决 24 | 25 | 5. 怎么把验证码发送到识别接口 26 | 27 | 主要是问如何定位该验证码,每一个验证码在Cookie中都有一个对应的序列号,post验证码识别后带上这个序列号就行了 28 | 29 | 6. Python爬虫与Java爬虫那个好 30 | 31 | Python,Python比Java更加短小精悍,现成的库更丰富 32 | 33 | 7. 用什么数据库,知不知道数据库索引和查询优化 34 | 35 | MySQL,了解数据库索引原理,没了解过查询优化。然后没有继续问了 36 | 37 | 8. Linux上的部署 38 | 39 | 9. 后面就是实习天数和时长了 40 | 41 | ## 二面 42 | 43 | 二面也是技术面,是个年轻的逗逼小哥哥,主要是根据简历里的项目和用过的技术提问 44 | 45 | 1. 打不打篮球(!-.-)听到这问题没反应过来 46 | 47 | 2. 说说模拟登录的流程 48 | 49 | 说了模拟正方教务系统的登录流程 50 | 51 | 3. WebSocket是短连接还是长连接 52 | 53 | 说了长连接,最后发现好像不太对 54 | 55 | 4. 短连接跟长连接有什么区别 56 | 57 | 长连接hold住同一tcp连接不断开,继续用这条通道传数据 58 | 59 | 5. Spring如何实现懒加载 60 | 61 | 知道这方式,但忘记了。 62 | 63 | xml方式: lazy-init="true" 64 | 注解: bean上加上@Lazy @AutoWire注入时使用beanFactory 65 | 66 | 6. Redis设置过期的参数是什么 67 | 68 | 没记过..最后问我过期的英文单词是啥,答:expired 69 | 70 | 7. 用什么数据库,基本的查询操作 71 | 72 | MySQL,照答 73 | 74 | 8. WebMagic模仿了Python哪个框架 75 | 76 | Scrapy,忘记单词了,我说是S开头的框架,算我过。。。 77 | 78 | 9. 使用Redis做过什么 79 | 80 | 存session,做token鉴权的存储层 81 | 82 | 10. 自己实现过的IP代理池原理 83 | 84 | 爬取免费IP,存入临时阻塞队列(说到阻塞队列就说够了,可能觉得我会就不用继续说)再针对要爬取的网站验证IP可用性,存入正式阻塞队列 85 | 86 | 最后小哥说留下我的简历~nice! 87 | 88 | 一开始准备了很久,以为会问我java容器,并发包,虚拟机的东西,发现都没有~ 89 | -------------------------------------------------------------------------------- /面经/2.md: -------------------------------------------------------------------------------- 1 | # 腾讯MIG春招 2 | 3 | + ### 投的是服务端不知道为什么就被叫去面前端,水到最后一轮技术面挂了 4 | 5 | ## 一面 6 | 7 | + 面试官很好,知道我前端不行,就都问后端的,整场面试一个半小时,气氛很轻松 8 | 9 | 1. web发展史 10 | 11 | 2. 单向链表与双向链表区别与适用场景 12 | 13 | 3. 手写双向链表 14 | 15 | 4. 分析快排原理 16 | 17 | 5. 项目: 18 | 19 | + 点对点通讯架构 20 | + 假设为多人聊天室,重新架构 21 | + 如何确保消息能送到每个群聊客户端 22 | + WebSocket与Http的区别,WebSocket如何复用tcp连接 23 | + 单点故障的解决方案 24 | 25 | 6. 进程与线程的区别,切换进程/线程,哪个更耗资源,为什么 26 | 27 | 7. 说说Java的线程模型与内存模型. 28 | 29 | 8. 身边人对自己的评价 30 | 31 | 9. 未来的发展规划 32 | 33 | ## 二面 34 | 35 | + 时长30分钟 36 | 37 | 1. 自我介绍 38 | 39 | 2. 聊团队开发 40 | 41 | 3. 讲一个前端自己做的项目(一般都是前后分离只做后端,说了一个很水的爬虫+web demo) 42 | 43 | 4. 文章去重解决 44 | 45 | 5. 有没有ui设计经验 46 | 47 | 6. 对开源框架的了解,tomcat的模型 48 | 49 | 7. io模型 50 | 51 | 8. 相对于同学自己的专业优势 -------------------------------------------------------------------------------- /面经/huya.md: -------------------------------------------------------------------------------- 1 | # 2018虎牙春招实习 2 | 3 | ## 一面 4 | 5 | 1. 讲述唯品会实习有代表性的项目,说了storm实时计算 6 | 7 | 2. storm的拓扑结构,集群架构,分组策略及特点 8 | 9 | 3. 统计虎牙各个直播间每五分钟的网页访问流量: 10 | 11 | 1. 各个url的时间窗口计数 12 | 2. 高并发,数据流大,不存redis直接内存计算 13 | 3. 使用离线修复做容灾 14 | 15 | 4. stom的可靠性怎么实现的 16 | 17 | 5. 手写冒泡排序 18 | 19 | 6. 一个大文件存放一系列url和该url的访问次数(同一url会多次出现){key:‘www.github.com’, value:2},内存不能容纳全部数据,累加求和求出访问量最大的10个url 20 | 21 | 7. tomcat架构 22 | 23 | 8. Filter的实现原理 24 | 25 | 9. JDBC用过吗(说用过一两次,没继续问下去) 26 | 27 | 10. 写出一个生产者消费者模型 28 | 29 | 11. 写网络爬虫一般的分析过程和反爬应对策略,顺便说了写爬虫时遇到的一个奇葩反爬 30 | 31 | 12. websocket原理,项目里怎么使用的 32 | 33 | 13. 怎么爬弹幕(这个没接触过,直接说不会,面试官就教我怎么爬) 34 | 35 | 14. synchronize实现原理 36 | 37 | 15. 问我在哪学到这些的,看过哪些书 38 | 39 | 16. java.util包下的类结构,HashMap原理,HashSet原理 40 | 41 | 17. Redis和MySQL原理的了解程度(扯到了Redis底层数据结构,InnoDB索引原理) 42 | 43 | 18. 一个情景题,考点是联合索引的什么情况下会用到索引,什么时候不会(奸诈地给我设坑,还好看过哈哈) 44 | 45 | 19. web,大数据和爬虫三个方向,更想做哪个。讲了一下他们大数据部门主要的工作和技术栈 46 | 47 | 48 | ## 二面 49 | 50 | 1. 一个Web请求经过哪些流程 51 | 52 | 2. 实习参与的爬虫项目 53 | 54 | 3. 写网络爬虫一般的分析过程和反爬应对策略 55 | 56 | 4. IP代理池怎么实现 57 | 58 | 5. 怎么识别一张图片。 59 | 60 | 6. 图片降噪是怎么实现的(直接说没研究过) 61 | 62 | 7. 有什么职业规划 63 | 64 | 65 | ## 三面 66 | 67 | 这个面试官贼好玩,带我去找地方面试的路上闲聊了很多,涉及到情感,两性,大学,青少年,潮汕人。。。 68 | 69 | 1. 问了一个自己完整实现的WEB项目 70 | 71 | 2. 用websocket的哪个协议 72 | 73 | 3. 服务有多种,客户端访问,怎么确定它需要的服务 74 | (不太理解问的什么,随便答了几个) 75 | 76 | 1. DNS域名解析 77 | 2. 一个节点维护一个注册了服务名和服务主机ip的map,做服务映射并转发 78 | 79 | 4. 实习参与的实时计算项目 80 | 81 | 5. storm计算中如何保障消息的有序性,顺便扯了遇到的消息乱序的问题 82 | 83 | 6. spark跟storm的区别,hadoop的原理(概念层面回答了一下) 84 | 85 | 接下来就是了解一些个人情况和Q&A 86 | 87 | 88 | ## HR面 89 | 90 | 1. 对之前面试的评价 91 | 92 | 2. 觉得面试官水平怎么样 93 | 94 | 3. 为什么要来虎牙 95 | 96 | 4. 讲了一下薪资待遇和转正时间,再问我到岗时间 97 | 98 | 5. Q&A 99 | 100 | ## 一些注意点: 101 | 102 | 1. 面试大都是围绕简历中的内容提问,简历应当多提到自己使用的技术和代表性实现(要有亮点) 103 | 104 | 2. 面试官有些问题会比较泛或者浅,这时候不要简单回答,要把问题延伸到自己有去研究过的方面,例如问我util包下的类结构,我再扯到hashmap,引导他问我hashmap原理,然后再顺便扯了一下hashmap的两个版本区别和hashset就是基于hashmap实现。 105 | 106 | 3. 先准备一两个代表性bug,提问项目的时候,顺便扯一下解决过程。体现出项目真实性和自己是真的参与项目而不是打酱油 107 | 108 | 4. 场景题:就算是看过的题也要故作沉思一番再回答 109 | 110 | 5. 知之为知之,不知为不知。大致了解但不清楚的话在回答前要先说好,例如:我不太了解这方面,但我可以说一下我的思路,可能不太对。 111 | 112 | --------------------------------------------------------------------------------