JUC并发—4.wait和notify以及Atomic原理

大纲

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>();

        //
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值