大纲
1.wait()与notify()实现一个简易的内存队列
2.wait()与notify()的底层原理
3.分布式存储系统NameNode机制介绍
4.分布式存储系统的edits log机制介绍
5.分布式存储系统的NameNode实现
6.分布式存储系统的创建目录功能的实现
7.edits log的全局txid机制和双缓冲机制实现
8.synchronized实现edits log分段加锁机制
9.wait()与notify()实现edits log批量刷磁盘
10.i++和AtomicInteger分别实现并发安全
11.AtomicInteger中的CAS无锁化原理
12.Atomic源码之仅限JDK使用的Unsafe类
13.Atomic源码之无限重复循环以及CAS操作
14.Atomic原子类基于CAS操作的三大问题
15.AtomicLong优化服务注册中心心跳计数器
16.LongAdder的分段CAS优化多线程自旋
17.LongAdder的分段CAS优化心跳计数器
18.服务注册中心的增量拉取机制
19.AtomicReference优化客户端缓存注册表
20.AtomicStampedReference解决ABA问题
21.AtomicLong多线程拉取注册表版本不错乱
1.wait()与notify()实现一个简易的内存队列
在多线程开发中,wait()和notify()/notifyAll()还是挺常见的。在分布式系统里经常会使用wait()和notifyAll()来进行线程通信,当某个线程处于阻塞等待状态时,其他线程可以进行通知并唤醒它。
如下代码向内存队列添加元素和获取元素时,都使用了MyQueue对象锁。当内存队列满或者空时,需要释放锁,才能让添加或者获取继续下去。其中wait()方法会释放锁,并让当前线程进入等待状态,而notify()方法和notifyAll()方法会唤醒等待获取锁的线程。所以wait()和notify()主要是用来控制线程的,当然也可认为用于线程通信。
public class MyQueue {
private final static int MAX_SIZE = 100;
private LinkedList<String> queue = new LinkedList<String>();
//向内存队列添加一个元素
public synchronized void offer(String element) {
try {
if (queue.size() == MAX_SIZE) {
//一个线程只要执行到这一步,就说明已经获取到锁
//但现在内存队列已经满了,所以可以让线程进入一个等待的状态,并释放锁
wait();
}
queue.addLast(element);
//唤醒当前在等待锁的线程
notifyAll();
} catch (Exception e) {
e.printStackTrace();
}
}
//获取内存队列的第一个元素
public synchronized String take() {
//别的线程可以通过take()方法从队列里获取数据
String element = null;
try {
if (queue.size() == 0) {
//释放锁,并让当前线程自己进行阻塞等待
//等待其他线程往内存队列放入数据后,通过notifyAll()来唤醒自己
wait();
}
element = queue.removeFirst();
//唤醒当前在等待锁的线程
notifyAll();
} catch (Exception e) {
e.printStackTrace();
}
return element;
}
}
2.wait()与notify()的底层原理
(1)获取重量级锁之前的锁膨胀
(2)ObjectMonitor对象的重要字段
(3)重量级锁的获取流程
(4)重量级锁的释放流程
(5)wait()与notify()的底层原理
(6)wait()与notify()在使用时的注意事项
(1)获取重量级锁之前的锁膨胀
如果线程在运行synchronized修饰的同步块代码时,发现锁状态是轻量级锁并且有其他线程抢占了锁资源,那么该线程就会触发锁膨胀升级到重量级锁。
在获取重量级锁之前会先实现锁膨胀,锁膨胀时首先会创建一个ObjectMonitor对象,然后把ObjectMonitor对象的指针保存到锁对象的Mark Word中。
重量级锁的实现是在ObjectMonitor中完成的,所以锁膨胀的意义就是构建一个ObjectMonitor对象。
(2)ObjectMonitor对象的重要字段
_owner:保存当前持有锁的线程
_cxq:没有获得锁的线程队列
_waitset:被wait()方法阻塞的线程队列
_recursions:锁被重入的次数
(3)重量级锁的获取流程
重量级锁的竞争都是在ObjectMonitor对象中完成的。首先判断当前线程是否是重入,如果是则重入次数+1。然后通过CAS自旋来判断ObjectMonitor中的_owner字段是否为空。如果为空,则表示重量级锁已经被释放,当前线程可以获得锁。如果不为空,就继续进行自适应自旋重试。最后如果通过自旋竞争锁失败,则把当前线程构建成一个ObjectWaiter结点,插入到ObjectMonitor的_cxq队列的队头,再调用park()方法阻塞当前线程。
(4)重量级锁的释放流程
首先把ObjectMonitor的_owner字段设置为null,然后从ObjectMonitor的_cxq队列中调用unpark()方法唤醒一个阻塞的线程。被唤醒的线程会重新竞争重量级锁,如果没抢到,则继续阻塞等待。因为synchronized是非公平锁,被唤醒的线程不一定能重新抢占到锁。
(5)wait()与notify()的底层原理
这与synchronized的原理(ObjectMonitor对象)相关,ObjectMonitor对象有一个_waitset队列和重入计数器。使用wait()和notify()时必须对同一个对象实例进行加synchronized锁。如果对象实例加锁,那么重入计数器 + 1。如果对象实例释放锁,那么重入计数器 - 1。
执行wait()方法时会释放锁 + 阻塞当前线程 + 把当前线程放入_waitset队列,执行notify()方法时会唤醒_waitset队列里的被阻塞的线程。
(6)wait()与notify()在使用时的注意事项
wait()与sleep()的区别:两者都会等待,前者释放锁,后者不释放锁。wait()必须要有其他线程调用notify()来唤醒它。wait(timeout)会阻塞一段时间,然后自己唤醒自己,继续争抢锁。wait()与notify()必须与synchornized一起,对同一个对象进行使用。notify()会唤醒阻塞状态的一个线程,notifyall()会唤醒阻塞状态的所有线程。
3.分布式存储系统NameNode机制介绍
(1)HDFS的DataNode和NameNode
(2)HDFS的NameNode架构简介
(1)HDFS的DataNode和NameNode
HDFS是Hadoop的分布式文件系统,它由很多机器组成。每台机器上运行一个DataNode进程,存储一部分数据。然后会有一台机器上运行一个NameNode进程,NameNode可以认为是负责管理整个HDFS集群的进程,NameNode里存储了HDFS集群的所有元数据。
(2)HDFS的NameNode架构简介
一.每次修改元数据都顺序追加edits log
二.如何避免edits log过大导致恢复过慢
三.NameNode主备高可用故障转移机制
一.每次修改元数据都顺序追加edits log
NameNode的核心功能是管理整个HDFS集群的元数据,比如文件目录树、权限设置、副本数设置等。
HDFS客户端每次上传文件时,都要维护NameNode的文件目录树。但是NameNode的文件目录树是在内存里的,万一NameNode宕机,内存里的文件目录树可能就会丢失。
所以每次修改内存,就顺序追加一条edits log(元数据操作日志)到磁盘文件。每次NameNode重启,就把edits log(元数据操作日志)读到内存恢复数据。
二.如何避免edits log过大导致恢复过慢
为了避免edits log(元数据操作日志)越来越大每次重启恢复过慢,于是引入了一个新的磁盘文件fsimage、一个JournalNodes集群、一个Active NameNode(主节点)、一个Standby NameNode(备节点)。
主节点每修改一条元数据都会生成一条edits log。每条edits log除了写到主节点外,还会写到JournalNodes集群。然后备节点会从JournalNodes集群拉取edits log到自己内存的文件目录树里,这样备节点的数据就可以跟主节点的数据保持一致了。
每隔一段时间备节点会把自己内存的文件目录树写一份到fsimage磁盘文件,这个也就是所谓的checkpoint检查点操作。然后备节点再把这个fsimage磁盘文件上传到到主节点,接着清空掉主节点上的旧的edits log文件(可能几十万行)。之后主节点继续处理修改元数据请求,那么可能只有几十行edits log日志了。
如果此时主节点重启,首先把备节点传过来的fsimage文件读到内存里,然后把新的edits log里少量的几十行操作日志重新恢复到内存中即可。
三.NameNode主备高可用故障转移机制
整个过程有两个NameNode:一是对外提供服务接收请求的主节点NameNode,二是同步主节点edits log + 定期执行checkpoint的备节点NameNode。
这两个NameNode内存里的元数据几乎一模一样。所以如果主节点挂了,可以马上切换到备节点对外提供服务,而这就是所谓的NameNode主备高可用故障转移机制了。
4.分布式存储系统的edits log机制介绍
(1)高并发请求下NameNode会遇到的问题
(2)通过双缓冲机制来提升写edits log的性能
(1)高并发请求下NameNode会遇到的问题
NameNode每修改一条元数据都要写一条edits log,这包括两个步骤:写入本地磁盘和通过网络传输给JournalNodes集群。
NameNode必须保证写入的每条edits log都有一个全局顺序递增的txid,这样才可以标识出一条edits log的先后顺序。
如果要保证每条edits log的txid都是递增的,那么就必须要加锁。每个线程修改完元数据准备写一条edits log时,按顺序排队获取锁,获取到锁之后才能生成一个递增的txid给要准备写的edits log。
但是如果每次在加锁的代码块里生成txid,然后写磁盘文件edits log,接着通过网络传输写入JournalNodes,那么性能就一定很低。所以每个线程写edits log时最好不要串行化排队来执行这3个操作:生成txid + 写磁盘 + 写JournalNode。
(2)通过双缓冲机制来提升写edits log的性能
为了避免线程写edits log时串行化排队去生成txid + 写磁盘 + 写JournalNode,可以考虑增加内存缓冲。首先将edits log写入到内存缓冲里,然后通过后台线程将内存中的edits log刷入磁盘 + 写入JournalNode。而且将edits log刷盘的过程中,其他线程依然可以将edits log写入内存缓冲。
如果针对同一块内存缓冲,同时有线程写入、同时有线程读取后刷入磁盘,那么是会存在并发读写问题的,因为不能并发读写一块共享内存数据。
所以HDFS采取了双缓冲机制来处理,也就是将一块内存缓冲分成两部分。其中一部分只用来写入,另一部分只用来读取进行刷盘。
5.分布式存储系统的NameNode实现
(1)NameNode的基本功能
(2)NameNode的核心启动类
(3)NameNode的RPC服务接口
(4)负责管理元数据的FSNamesystem
(5)负责管理文件目录树的FSDirectory
(6)负责管理edits log日志的FSEditlog
(1)NameNode的基本功能
如果NameNode执行命令创建一个目录,那么会做两件事情:一是在内存里的文件目录树中加入目录节点,二是在磁盘里写入一条edits log日志来记录本次元数据修改。
所以接下来要实现两个功能:一是在内存文件目录树中加入目录节点,二是写edits log到磁盘文件。
如下是NameNode的核心组件说明:
FSNamesystem类:作为NameNode里元数据操作的核心入口,负责管理所有的元数据的操作,会调用其他组件完成相关事情。
FSDirectory类:管理内存中的文件目录树。
FSEditLog类:写入edits log到磁盘文件里。
(2)NameNode的核心启动类
//NameNode核心启动类
public class NameNode {
//NameNode是否在运行
private volatile Boolean shouldRun;
//负责管理元数据的核心组件
private FSNamesystem namesystem;
//NameNode对外提供RPC接口的Server,可以响应请求
private NameNodeRpcServer rpcServer;
public NameNode() {
this.shouldRun = true;
}
//初始化NameNode
private void initialize() {
this.namesystem = new FSNamesystem();
this.rpcServer = new NameNodeRpcServer(this.namesystem);
this.rpcServer.start();
}
//让NameNode运行起来
private void run() {
try {
while(shouldRun) {
Thread.sleep(10000);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
NameNode namenode = new NameNode();
namenode.initialize();
namenode.run();
}
}
(3)NameNode的RPC服务接口
//NameNode的rpc服务的接口
public class NameNodeRpcServer {
//负责管理元数据的核心组件
private FSNamesystem namesystem;
public NameNodeRpcServer(FSNamesystem namesystem) {
this.namesystem = namesystem;
}
//创建目录
public Boolean mkdir(String path) throws Exception {
return this.namesystem.mkdir(path);
}
//启动这个rpc server
public void start() {
System.out.println("开始监听指定的rpc server的端口号,来接收请求");
}
}
(4)负责管理元数据的FSNamesystem
//负责管理元数据的核心组件
public class FSNamesystem {
//负责管理内存文件目录树的组件
private FSDirectory directory;
//负责管理edits log写入磁盘的组件
private FSEditlog editlog;
public FSNamesystem() {
this.directory = new FSDirectory();
this.editlog = new FSEditlog();
}
//创建目录
public Boolean mkdir(String path) throws Exception {
this.directory.mkdir(path);
this.editlog.logEdit("创建了一个目录:" + path);
return true;
}
}
(5)负责管理文件目录树的FSDirectory
//负责管理内存中的文件目录树的核心组件
public class FSDirectory {
//创建目录
public void mkdir(String path) {
}
}
(6)负责管理edits log日志的FSEditlog
//负责管理edits log日志的核心组件
public class FSEditlog {
//记录edits log日志
public void logEdit(String log) {
}
}
6.分布式存储系统的创建目录功能实现
在内存的文件目录树中创建一个目录节点的代码如下。内存里的文件目录树是会被多线程并发写的资源,所以创建目录的代码块必须要用synchronized保护起来。
//负责管理内存中的文件目录树的核心组件
public class FSDirectory {
//内存中的文件目录树
private INodeDirectory dirTree;
public FSDirectory() {
this.dirTree = new INodeDirectory("/");
}
//创建目录
public void mkdir(String path) {
//path = /usr/warehouse/hive
//首先判断'/'根目录下有没有一个'usr'目录
//如果有,那么再判断'/usr'目录下有没有一个'/warehouse'目录
//如果没有,那么就得先在'/usr'目录下创建一个'/warehosue'目录
//接着再在'/warehosue'目录下,创建'hive'这个目录节点
synchronized(dirTree) {
String[] pathes = path.split("/");
INodeDirectory parent = dirTree;
for (String splitedPath : pathes) {
if (splitedPath.trim().equals("")) {
continue;
}
INodeDirectory dir = findDirectory(parent, splitedPath);
if (dir != null) {
parent = dir;
continue;
}
INodeDirectory child = new INodeDirectory(splitedPath);
parent.addChild(child);
}
}
}
//对文件目录树递归查找目录
private INodeDirectory findDirectory(INodeDirectory dir, String path) {
if (dir.getChildren().size() == 0) {
return null;
}
INodeDirectory resultDir = null;
for (INode child : dir.getChildren()) {
if (child instanceof INodeDirectory) {
INodeDirectory childDir = (INodeDirectory) child;
if ((childDir.getPath().equals(path))) {
return childDir;
}
resultDir = findDirectory(childDir, path);
if (resultDir != null) {
return resultDir;
}
}
}
return null;
}
//代表的是文件目录树中的一个节点
private interface INode {
}
//代表文件目录树中的一个目录
public static class INodeDirectory implements INode {
private String path;
private List<INode> children;
public INodeDirectory(String path) {
this.path = path;
this.children = new LinkedList<INode>();
}
public void addChild(INode inode) {
this.children.add(inode);
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public List<INode> getChildren() {
return children;
}
public void setChildren(List<INode> children) {
this.children = children;
}
}
//代表文件目录树中的一个文件
public static class INodeFile implements INode {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
7.edits log的全局txid机制和双缓冲机制实现
全局txid机制 + 双缓冲机制的代码如下:
//负责管理edits log日志的核心组件
public class FSEditlog {
//当前递增到的txid的序号
private long txidSeq = 0L;
//内存双缓冲区
private DoubleBuffer editLogBuffer = new DoubleBuffer();
//记录edits log日志
public void logEdit(String content) {
//这里必须加锁
synchronized(this) {
//获取全局唯一递增的txid,代表了edits log的序号
txidSeq++;
long txid = txidSeq;
//构造一条edits log对象
EditLog log = new EditLog(txid, content);
//将edits log写入内存缓冲中,不是直接刷入磁盘文件
editLogBuffer.write(log);
}
}
//代表了一条edits log,内部类
private class EditLog {
long txid;
String content;
public EditLog(long txid, String content) {
this.txid = txid;
this.content = content;
}
}
//内存双缓冲,内部类
private class DoubleBuffer {
//专门用来承载线程写入edits log
LinkedList<EditLog> currentBuffer = new LinkedList<EditLog>();
//