深入理解线程通信
你应该知道的 volatile 关键字
前言
不管是在面试还是实际开发中 volatile
都是一个应该掌握的技能。
首先来看看为什么会出现这个关键字。
内存可见性
由于 Java
内存模型(JMM
)规定,所有的变量都存放在主内存中,而每个线程都有着自己的工作内存(高速缓存)。
线程在工作时,需要将主内存中的数据拷贝到工作内存中。这样对数据的任何操作都是基于工作内存(效率提高),并且不能直接操作主内存以及其他线程工作内存中的数据,之后再将更新之后的数据刷新到主内存中。
这里所提到的主内存可以简单认为是堆内存,而工作内存则可以认为是栈内存。
如下图所示:
所以在并发运行时可能会出现线程 B 所读取到的数据是线程 A 更新之前的数据。
显然这肯定是会出问题的,因此 volatile
的作用出现了:
当一个变量被
volatile
修饰时,任何线程对它的写操作都会立即刷新到主内存中,并且会强制让缓存了该变量的线程中的数据清空,必须从主内存重新读取最新数据。
LinkedHashMap 底层分析
众所周知 HashMap 是一个无序的 Map
,因为每次根据 key
的 hashcode
映射到 Entry
数组上,所以遍历出来的顺序并不是写入的顺序。
因此 JDK 推出一个基于 HashMap
但具有顺序的 LinkedHashMap
来解决有排序需求的场景。
它的底层是继承于 HashMap
实现的,由一个双向链表所构成。
LinkedHashMap
的排序方式有两种:
- 根据写入顺序排序。
- 根据访问顺序排序。
其中根据访问顺序排序时,每次 get
都会将访问的值移动到链表末尾,这样重复操作就能的到一个按照访问顺序排序的链表。
ReentrantLock 实现原理
使用 synchronize
来做同步处理时,锁的获取和释放都是隐式的,实现的原理是通过编译后加上不同的机器指令来实现。
而 ReentrantLock
就是一个普通的类,它是基于 AQS(AbstractQueuedSynchronizer)
来实现的。
是一个重入锁:一个线程获得了锁之后仍然可以反复的加锁,不会出现自己阻塞自己的情况。
AQS
是Java
并发包里实现锁、同步的一个重要的基础框架。
锁类型
ReentrantLock 分为公平锁和非公平锁,可以通过构造方法来指定具体类型:
1 | //默认非公平锁 |
默认一般使用非公平锁,它的效率和吞吐量都比公平锁高的多(后面会分析具体原因)。
对象的创建与内存分配
创建对象
当 JVM
收到一个 new
指令时,会检查指令中的参数在常量池是否有这个符号的引用,还会检查该类是否已经被加载过了,如果没有的话则要进行一次类加载。
接着就是分配内存了,通常有两种方式:
- 指针碰撞
- 空闲列表
使用指针碰撞的前提是堆内存是完全工整的,用过的内存和没用的内存各在一边每次分配的时候只需要将指针向空闲内存一方移动一段和内存大小相等区域即可。
当堆中已经使用的内存和未使用的内存互相交错时,指针碰撞的方式就行不通了,这时就需要采用空闲列表的方式。虚拟机会维护一个空闲的列表,用于记录哪些内存是可以进行分配的,分配时直接从可用内存中直接分配即可。
堆中的内存是否工整是有垃圾收集器来决定的,如果带有压缩功能的垃圾收集器就是采用指针碰撞的方式来进行内存分配的。
synchronized 关键字原理
众所周知 synchronized
关键字是解决并发问题常用解决方案,有以下三种使用方式:
- 同步普通方法,锁的是当前对象。
- 同步静态方法,锁的是当前
Class
对象。 - 同步块,锁的是
()
中的对象。
实现原理:JVM
是通过进入、退出对象监视器( Monitor
)来实现对方法、同步块的同步的。
具体实现是在编译之后在同步方法调用前加入一个 monitor.enter
指令,在退出方法和异常处插入 monitor.exit
的指令。
其本质就是对一个对象监视器( Monitor
)进行获取,而这个获取过程具有排他性从而达到了同一时刻只能一个线程访问的目的。
而对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程 monitor.exit
之后才能尝试继续获取锁。
流程图如下:
一致性 Hash 算法分析
当我们在做数据库分库分表或者是分布式缓存时,不可避免的都会遇到一个问题:
如何将数据均匀的分散到各个节点中,并且尽量的在加减节点时能使受影响的数据最少。
Hash 取模
随机放置就不说了,会带来很多问题。通常最容易想到的方案就是 hash 取模
了。
可以将传入的 Key 按照 index = hash(key) % N
这样来计算出需要存放的节点。其中 hash 函数是一个将字符串转换为正整数的哈希映射方法,N 就是节点的数量。
这样可以满足数据的均匀分配,但是这个算法的容错性和扩展性都较差。
比如增加或删除了一个节点时,所有的 Key 都需要重新计算,显然这样成本较高,为此需要一个算法满足分布均匀同时也要有良好的容错性和拓展性。
sbc(六) Zuul GateWay 网关应用
前言
看过之前SBC系列的小伙伴应该都可以搭建一个高可用、分布式的微服务了。 目前的结构图应该如下所示:
各个微服务之间都不存在单点,并且都注册于 Eureka
,基于此进行服务的注册于发现,再通过 Ribbon
进行服务调用,并具有客户端负载功能。
一切看起来都比较美好,但这里却忘了一个重要的细节:
当我们需要对外提供服务时怎么处理?
这当然也能实现,无非就是将我们具体的微服务地址加端口暴露出去即可。
那又如何来实现负载呢?
简单!可以通过 Nginx F5
之类的工具进行负载。
但是如果系统庞大,服务拆分的足够多那又有谁来维护这些路由关系呢?
当然这是运维的活,不过这时候运维可能就要发飙了!
并且还有一系列的问题:
- 服务调用之间的一些鉴权、签名校验怎么做?
- 由于服务端地址较多,客户端请求难以维护。
针对于这一些问题 SpringCloud
全家桶自然也有对应的解决方案: Zuul
。
当我们系统整合 Zuul 网关之后架构图应该如下所示:
【译】你可以用GitHub做的12件 Cool 事情
原文链接
1 在 GitHub.com 编辑代码
我将从我认为大家都知道的一件事情开始(尽管我是直到一周前才知道)。
当你在 GitHub 查看文件时(任何文本文件,任何仓库中),右上角会有一个小铅笔图标,点击它就可以编辑文件了。完成之后点击 Propose file change 按钮 GitHub 将会自动帮你 fork 该项目并且创建一个 pull request
。
很厉害吧!他自动帮你 fork
了该 repo。
不再需要 fork
, pull
,本地编辑再 push
以及创建一个 PR
这样的流程了。
这非常适合修复编写代码中出现的拼写错误和修正一个不太理想的想法。
2 粘贴图片
你不仅仅受限于输入文本和描述问题,你知道你可以直接从粘贴板中粘贴图片吗?当你粘贴时,你会看到图片已经被上传了(毫无疑问被上传到云端)之后会变成 Markdown
语法来显示图片。
3 格式化代码
如果你想写一段代码,你可以三个反引号开始 —— 就像你在研究MarkDown
时所学到的 —— 之后 GitHub 会试着猜测你写的语言。
但如果你写了一些类似于 Vue, Typescript, JSX 这样的语言,你可以明确指定得到正确的高亮。
注意第一行中的
1 | ```jsx |