googo 邀请码 lbVOQjeS
googo 可以让你更好的浏览外文网站,比如meta、youtube,google等。 它之前的网址是googo.in,现在已经不用访问。是一款非常好用的科学上网工具,使用简单。 使用这个链接可以获取优惠:https://cn.googo.us/#/register?code=lbVOQjeS 或者使用这个邀请码:lbVOQjeS
googo 可以让你更好的浏览外文网站,比如meta、youtube,google等。 它之前的网址是googo.in,现在已经不用访问。是一款非常好用的科学上网工具,使用简单。 使用这个链接可以获取优惠:https://cn.googo.us/#/register?code=lbVOQjeS 或者使用这个邀请码:lbVOQjeS
第一章 并发编程的挑战第2章 InnoDB存储引擎第1章 Java的I/O演进之路第1章 Spring框架的由来第2章 Tomcat总体架构三、Paxos的工程实践序第1章 概述第6章 深入分析ClassLoader工作机制第8章 虚拟机字节码执行系统
第1章 Java的I/O演进之路 1.1 I/O基础入门 1.1.1 Linux网络I/O模型简介 UNIX提供了5种I/O模型: (1)阻塞I/O模型 (2)非阻塞I/O模型 (3)I/O复用模型 (4)信号驱动I/O模型 (5)异步I/O 1.1.2 I/O多路复用技术 把多个I/O阻塞复用到同一个select的阻塞上,从而是的系统在单线程的情况下可以同时处理多个客户端请求。比多线程有性能优势,节约资源。 支持I/O多路复用的系统调用select/pselect/poll/epoll。 epoll的优点: 支持一个进程打开的socket描述符(FD)不受限制(仅受限于操作系统的最大文件句柄数(内存))。 I/O效率不会随着FD数目的增加而线性下降。 使用mmap加速内核与用户空间的消息传递。 epoll的API更加简单。 1.2 Java的I/O演进 历史题,略。 第2章 NIO入门 2.1 传统的BIO编程 C/S模型,客户端发起连接请求,三次握手,通过Socket进行通信。 2.1.1 BIO通信模型图 一请求一应答模型:每次接收到连接请求都创建一个新的线程进行链路处理。处理完成后通过输出流返回应答给客户端,线程销毁。 该模型最大的问题就是:缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系。 2.1.2 同步阻塞式I/O创建的TimeServer源码分析 import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; /** * <p>Description: 同步阻塞式I/O创建的TimeServer</p> * * @author 李宏博 * @version xxx * @create 2019/8/14 17:58 */ public class TimeServer { /** * * @param args */ public static void main(String[] args) throws IOException { int port = 8080; if (args != null && args.length > 0) { try { port = Integer.valueOf(args[0]); } catch (NumberFormatException e) { e.printStackTrace(); } } ServerSocket server = null; try { server = new ServerSocket(port); System.out.println("服务端端口开启:" + port); Socket socket = null; while (true) { socket = server.accept(); new Thread(new TimeServerHandle(socket)).start(); } } finally { if (server != null) { System.out.println("服务端关闭"); server.close(); server = null; } } } } import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; /** * <p>Description: xx</p> * * @author 李宏博 * @version xxx * @create 2019/8/14 18:05 */ public class TimeServerHandle implements Runnable { private Socket socket; public TimeServerHandle(Socket socket) { this.socket = socket; } @Override public void run() { BufferedReader in = null; PrintWriter out = null; try { in = new BufferedReader(new InputStreamReader(this.socket.getInputStream())); out = new PrintWriter(this.socket.getOutputStream(),true); String currentTime = null; String body = null; while (true) { body = in.readLine(); if (body == null) { break; } System.out.println("接收到命令:" + body); currentTime = "time".equalsIgnoreCase(body) ? new java.util.Date(System.currentTimeMillis()).toString() : "命令错误"; out.println(currentTime); } } catch (Exception e) { if (in != null) { try { in.close(); } catch (IOException ex) { ex.printStackTrace(); } } if (out != null) { out.close(); out = null; } if (this.socket != null) { try { this.socket.close(); } catch (IOException ex) { ex.printStackTrace(); } finally { this.socket = null; } } } } } 2.1.3 同步阻塞式I/O创建的TimeClient源码分析 import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; import java.net.UnknownHostException; /** * <p>Description: 同步阻塞式I/O创建的TimeClient</p> * * @author 李宏博 * @version xxx * @create 2019/8/14 18:21 */ public class TimeClient { /** * * @param args */ public static void main(String[] args) { int port = 8080; if (args != null && args.length > 0) { try { port = Integer.valueOf(args[0]); } catch (NumberFormatException e) { e.printStackTrace(); } } Socket socket = null; BufferedReader in = null; PrintWriter out = null; try { socket = new Socket("127.0.0.1",port); in = new BufferedReader(new InputStreamReader(socket.getInputStream())); out = new PrintWriter(socket.getOutputStream(),true); out.println("time"); System.out.println("命令发送成功"); String resp = in.readLine(); System.out.println("现在时间:" + resp); } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (out != null) { out.close(); out = null; } if (in != null) { try { in.close(); } catch (IOException e) { e.printStackTrace(); } finally { in = null; } } if (socket != null) { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } finally { socket = null; } } } } } 2.2 伪异步I/O编程 引入线程池。 ...
第1章 Spring框架的由来 1.1 Spring之崛起 1.2 Spring框架概述 基于POJO(Plain Old Java Object,简单Java对象)的轻量级开发理念。 Spring总体架构: 1.3 Spring大观园 1.4 小结 第2章 Spring的IOC容器 2.1 我们的理念是:让别人为你服务 2.2 手语,呼喊,还是心有灵犀 2.2.1 构造方法注入 IoC Service Provider会检查被注入对象的构造方法,取得它所需要的依赖对象列表,进而为其注 入相应的对象。同一个对象是不可能被构造两次的,因此,被注入对象的构造乃至其整个生命周期, 应该是由IoC Service Provider来管理的。 2.2.2 setter方法注入 setter方法注入虽不像构造方法注入那样,让对象构造完成后即可使用,但相对来说更宽松一些, 可以在对象构造完成后再注入。 2.2.3 接口注入 对于前两种注入方式来说,接口注入没有那么简单明了。被注入对象如果想要IoC Service Provider为其注入依赖对象,就必须实现某个接口。这个接口提供一个方法,用来为其注入依赖对象。 IoC Service Provider最终通过这些接口来了解应该为被注入对象注入什么依赖对象。 示例: 2.2.4 三种注入方式的比较 接口注入。不提倡,带有侵入性 构造方法注入。这种注入方式的优点就是,对象在构造完成之后,即已进入就绪状态,可以马上使用。缺点就是,当依赖对象比较多的时候,构造方法的参数列表会比较长。而通过反射构造对象的时候,对相同类型的参数的处理会比较困难,维护和使用上也比较麻烦。而且在Java中,构造方法无法被继承,无法设置默认值。对于非必须的依赖处理,可能需要引入多个构造方法,而参数数量的变动可能造成维护上的不便。 setter方法注入。因为方法可以命名, 所以setter方法注入在描述性上要比构造方法注入好一些。另外, setter方法可以被继承,允许设置默认值,而且有良好的IDE支持。缺点当然就是对象无法在构造完成后马上进入就绪状态。 2.3 IOC的附加值
第1章 概述 互联网公司的分布式两个特点:规模大、成本低。 1.1 分布式存储概念 定义:分布式存储系统是大量普通PC服务器通过Internet互联,对外作为一个整体提供存储服务。 特性: 可扩展。集群,性能随规模线性增长。 低成本。有自动容错,自动负载均衡机制。所以可以构建在普通PC机器。 高性能。 易用。提供易用的对外接口。 主要挑战:数据、状态信息的持久化,要求在自动迁移、自动容错、并发读写的过程中保证数据的一致性。 主要技术来自于分布式系统和数据库: 数据分布:如何保证均匀?如何实现跨服务器读写? 一致性:如何复制?如何保证出现异常也一致? 容错:如何检测?出错如何迁移? 负载均衡:如何实现?如何做到迁移时不影响其他业务? 事务与并发控制:如何实现分布式事务?如何实现MVCC? 易用性:如何易用?如何方便运维? 压缩/解压缩:如何根据数据特点设计压缩/解压缩算法?如何平衡节省的存储空间和消耗的CPU资源浪费? 1.2 分布式存储分类 数据大致分为三类: 非结构化数据:包括所有格式的办公文档、文本、图片、图像、音频和视频信息等。 结构化数据:一般存储在关系数据库中,可用二位关系表结构来表示。 半结构化数据:与结构化的区别在于,半结构化数据的模式结构和内容混在一起,没有明显的区分,也不需要预先定义数据的模式结构。 存储系统分为四类: 分布式文件系统 图片、照片、视频等非结构化数据对象,以对象组织,对象之间没有关联,称为Blob(Binary Large Object,二进制大对象)数据。 总体上看:分布式文件系统存储三种类型的数据:Blob对象、定长块以及大文件。在系统层面,分布式文件系统内部按照数据块来组织数据,每个数据块的大小大致相同,每个数据块可以包含多个Blob对象或者定长块,一个大文件也可以拆分成多个数据块。 分布式键值系统 用于存储关系简单的半结构化数据,它只提供基于主键的CRUD功能,即根据主键创建、读取、更新或者删除一条键值记录。 与传统哈希表相似,但是支持将数据分布到多个存储节点。 分布式键值系统是分布式表格系统的一种简化实现,一般用作缓存。 分布式表格系统 用于存储较为复杂的半结构化数据。不仅仅支持简单的CRUD操作,而且支持扫面某个主键范围。 借鉴了许多关系型数据库的技术,例如支持某种程度上的事务,比如单行事务,某个实体组下的多行事务。 与分布式数据库先比,仅支持针对单张表格的操作,不支持复杂操作。 分布式数据库 一般从单机数据库扩展而来,用于存储结构化数据。 第2章 单机存储系统 单机存储引擎就是哈希表、B树等数据结构在机械磁盘、SSD等持久化介质上的实现。 2.1 硬件基础 2.1.1 CPU架构 经典的多CPU架构为对称多处理结构(Symmetric Multi-Processing,SMP),即在一个计算机上汇集了一组处理器,它们之间对称工作,无主次或从属关系,共享相同的物理内存及总线。 2.1.2 IO总线 存储系统的性能瓶颈一般在与IO。 2.1.3 网络拓扑 传统的数据中心网络拓扑,分三层。最下面是接入层,中间是汇聚层,上面是汇聚层。存在的问题:大量下层接入,导致同一个接入层下的服务器之间的带宽减小。 2.1.4 性能参数 2.1.5 存储层次架构 存储系统的性能主要包括两个维度:吞吐量以及访问延时。 2.2 单机存储引擎 2.2.1 哈希存储引擎 Bitcask是一个基于哈希表结构的键值寸尺系统,它仅支持追加操作(Append-only),即所有的写操作只追加而不修改老的数据。 在Bitcask系统中,每个文件有一定的大小限制,当文件增加到相应的大小时,就会产生一个新的文件,老的文件只读不写。在任意时刻,只有一个文件市可写的,用于数据追加,称为活跃数据文件。而其他已经达到大小限制的文件,称为老数据文件。 数据结构 一条一条写入,每条记录的数据项分别为:主键(key),value内容(value),主键长度(key_sz),value长度(value_sz),时间戳(timetamp)以及crc校验值。(删除不会删除旧的条目,而是将value设定为一个特殊的之作标识)。内存中的结构是hash表。 定期合并 为解决垃圾文件问题。将所有老数据文件中的数据扫描一遍生成一个新的数据文件。对用一个key的多个操作以保留最新的一个原则进行删除。 快速恢复 每次合并时,将内存中的哈希索引表转储到磁盘中,生成一个索引文件。这个索引文件只存储value的位置。 2.2.2 B树存储引擎 不仅支持随机读取,还支持范围扫描。 数据结构 InnoDB按照页面(Page)来组织数据,每个页面对用B+树的一个节点。 B+树的根节点是常驻内存的。修改操作首先需要记录提交日志,接着修改内存中的B+树。 缓冲区管理 LRU、LIRS 2.2.3 LSM树存储引擎 将对数据的修改增量保持在内存中,达到指定的大小先之后将这些修改操作批量写入磁盘,读取时需要合并磁盘中的历史数据和内存中最近的修改操作。
第2章 InnoDB存储引擎 事务安全。 2.1 InnoDB存储引擎概述 Mysql5.5开始是默认的表存储引擎(之前尽在Window下是)。第一个完整支持ACID事务的MySQL存储引擎。其特点是行锁设计、支持MVCC、支持外键、提供一致性非锁定读,同时被设计用来最有效地利用以及使用内存和CUP。 高性能、高可用、高扩展。 2.2 InnoDB存储引擎的版本 略 2.3 InnoDB体系结构 后台线程的主要作用是负责刷新内存池中的数据,保证缓冲池中的内存缓存的是最近的数据。 此外将已修改的数据文件刷新到磁盘文件,同时保证在数据库发生异常的情况下InnoDB能恢复到正常运行状态。 2.3.1 后台线程 多线程模型。不同的后台线程处理不同的任务。 Master Thread 将缓冲池的数据异步刷新到磁盘,保证数据一致性,包括脏页的刷新、合并插入缓冲(INSERT BUFFER)、UNDO页的回收。 IO Thread 大量使用AIO(Async IO),极大提高性能。IO Thread主要负责这些IO请求的回调处理。 第3章 文件 参数文件:告诉MySQL启动时查找文件的地址,指定初始化参数; 日志文件:错误日志文件、二进制文件日志文件、慢查询日志文件、查询日志文件; socket:当用UNIX域套接字方式进行连接时需要的文件 pid文件:MySQL实例的进程ID文件 MySQL表结构文件:用来存放MySQL表结构定义文件 存储引擎文件:每个存储引擎都会有自己的文件来保存各种数据,这些存储引擎真正存储了记录和索引等数据。 3.1 参数文件 MySQL实例也可以不需要参数文件,这是编译MySQL时指定的默认值。但是如果在默认的数据库目录下找不到mysql架构,则会启动失败。 3.1.1 什么是参数 数据库中的参数是键值对。MySQL中无类似Oracle中的隐藏参数。 3.1.2 参数类型 动态参数:运行时可修改 静态参数:在整个实例的生命周期都不能修改。 3.2 日志文件 日志文件记录了MySQL数据库的各种类型活动。 3.2.1 错误日志 对MySQL的启动、运行、关闭过程进行了记录。当出现MySQL数据库不能正常启动时,第一个必须查找的文件应该就是错误日志文件。 3.2.2 慢查询日志 帮助DBA定位可能存在问题的SOL语句,从而进行SQL语句层面的优化。设置一个阈值(最小精度是微秒),超过(必须是大于,等于不行)时将该SQL语句记录到慢查询日志中。 默认情况下并不启动慢查询日志。 如果没有使用索引也会被记录。 3.2.3 查询日志 记录了所有对MySQL数据库请求的信息,无论这些请求是否得到了正确的执行。 3.2.4 二进制文件 记录了对MySQL数据库执行更改的所有操作,但是不包括SELEC和SHOW这类操作。 作用: 恢复:某些数据的恢复需要二进制日志。 复制:主从复制 审计:通过审计二进制文件,判断是否有数据库进行注入的攻击。 二进制日志文件在默认情况下并没有启动,需要手动指定参数来启动。会影响性能,但仅仅1%。 缓冲写宕机问题,无缓冲写宕机问题 3.3 套接字文件 3.4 pid文件 3.5 表结构定义文件 记录该存储引擎对应的表结构 3.6 InnoDB存储引擎文件 InnoDB独有的文件 3.6.1 表空间文件 InnoDB采用将存储的数据按表空间进行存放的设计。 还可设置独立表空间,用户不用将所有数据都存放于默认的表空间中。 单独的表空间仅存储表的数据、索引和插入缓冲BITMAP等信息,其余的信息存放到默认表空间。 3.6.2 重做日志文件 宕机问题。 重做日志文件与二进制文件的区别: 记录范围,二进制日志会记录所有与MySQL数据库有关的日志记录,而InnoDB仅记录该存储引擎的事务日志。 记录内容,二进制日志记录具体操作(逻辑日志),而InnoDB的重做日志文件是关于每个Page的更改的物理情况 写入时间,二进制日志文件只在事务提交前进行一次写入,而重做日志文件在事务的进行过程中,一直会被写入。 3.7 小结 重做日志文件使得InnoDB存储引擎可以提供可靠的事务。 第4章 表 4.1 索引组织表 在InnoDB存储引擎中,表都是根据主键顺序组织存放。 在InnoDB存储引擎中,每张表都有个主键,如果没有显示定义,则按如下方式创建: 首先判断表中是否含有唯一非空索引(Unique NOT NULL),如果有,则该列即为主键。(按定义索引的顺序选择) 如果不符合上述条件,InnoDB存储引擎自动创建一个6字节大小的指针。 _rowid仅适用于单个列为主键的情况。 ...
第2章 Tomcat总体架构 系统设计及中间件设计时的参考:生命周期管理、可扩展的容器组件设计、类加载方式。 2.1 总体设计 如何设计一个应用服务器? 2.1.1 Server 最基本的功能:接收请求,业务处理,返回响应。 两个方法: start():启动服务器,打开Socket链接,监听端口,负责接收请求,处理及返回。 stop():停止服务器并释放网络资源。 作为嵌入在应用系统中的远程请求处理方案,且访问量低时可行。但作为应用服务器不可行。 2.1.2 Connector和Container 请求监听与请求处理放到一起扩展性差。 Connector负责监听,返回。 Container负责处理请求。 均分别拥有自己的start()和stop()方法来加载和释放自己维护的资源。 明显的缺陷:如何让Connector与Container对应?可以维护一个复杂关系映射,但是并不必需。Container设计足够灵活。 引入Service,负责维护多个Connector和一个Container。 在Tomcat中,Container是一个更加通用的概念。为了与Tomcat中的组件命名一致,所以重新命名为Engine,用以表示整个Servlet引擎。 Engine表示整个Servlet引擎。Server表示整个Servlet容器。 2.1.3 Container设计 应用服务器是用来部署并运行Web应用的,是一个运行环境,而不是独立的业务处理系统。因此需要在Engine容器中支持管理Web应用,当接收到Connector的处理请求时,Engine容器能够找到一个合适的Web应用来处理。 使用一个Context来表示一个Web应用,并且一个Engine可以包含多个Context。 虚拟主机,加入Host。一个Host可以包含多个Context。 Tomcat的设计中Engine可以包含Host也可以包含Context,这是由具体的Engine实现确定的。Tomcat提供的默认实现StandardEngine只能包含Host。 一个Web应用可以包含多个Servlet实例。在Tomcat中,Servlet定义被称为Wrapper。 “容器”的作用都是处理请求并返回响应数据。所以引入一个Container接口:addchild()添加子容器,backgroundProcess()实现文件变更的扫描。 2.1.4 Lifecycle 所有组件均存在启动、停止这两个生命周期方法,可在此基础上扩展生命周期管理的方法,即对于生命周期管理进行一次接口抽象。 将Server接口替换为Lifecycle接口: Init():初始化组件 start():启动组件 stop():停止组件 destory():销毁组件 addLifecycleListener:添加事件监听器(用于监听组件的状态变化) removeLifecycleListener:删除 Tomcat核心组件的默认实现均继承自LifecycleBeanBase抽象类,该类不但负责组件各个状态的转换和事件处理,还将组件自身注册为MBean,以便通过Tomcat的管理工具进行动态维护。 2.1.5 Pipeline和Valve 以上设计以保证核心架构的了可伸缩性和可扩展性。但是还要考虑各个组件的灵活性,使其同样可扩展。 责任链模式是一种比较好的选择。Tomcat即采用该模式来实现客户端请求的处理。在Tomcat中每个Container组件通过执行一个责任链来完成具体的请求处理。 Pipeline(管道)用于构造责任链,Valve(阀)代表责任链上的每个处理器。Pipeline中维护了一个基础的Valve(位于末端,最后执行)。 Tomcat的每个层级的容器(Engine、Host、Context、Wrapper)均有对应的基础Valve实现,同时维护一个Pipeline实例。即任何层级的容器都可以对请求处理进行可扩展。 2.1.6 Connector设计 基本功能: 监听服务器端口,读取来自客户端的请求。 将请求数据按照指定协议进行解析。 根据请求地址匹配正确的容器进行处理。 将响应返回客户端。 Tomcat支持多协议,默认支持HTTP和AJP。同时支持多种I/O方式,包括BIO(8.5之后移除)、NIO、APR。而且在Tomcat8之后新增了对NIO2和HTTP/2协议的支持。因此对协议和I/O进行抽象和建模时需要关注的重点。 在Tomcat中,ProtocolHandler表示一个协议处理器,其包含一个Endpoint(无此接口,仅有AbstractEndpoint抽象类)用于启动Socket监听,还包含一个Processor用于按照指定协议读取数据,并将请求交由容器处理。 在Connector启动时,Endpoint会启动线程来监听,并在接收到请求后调用Processor进行数据读取。 当Processor读取客户端请求后,需要按照请求地址映射到具体的容器进行处理,这个过程即为请求映射。由于Tomcat各个组件采用通用的生命周期管理,而且可以通过管理工具进行状态变更,因此请求映射除考虑映射规则的实现外,还要考虑容器组件的注册与销毁。 Tomcat通过Mapper和MapperListener两个类实现上述功能。前者用于维护容器映射信息,同时按照映射规则(Servlet规范)查找容器。后者实现了ContainerListener和LifecycleListener,用于在容器组件状态发生变更时,注册或者取消对应的容器映射信息。为了实现上述功能,MapperListener实现了Lifecycle接口,当其启动时(在Service启动时启动),会自动作为监听器注册到各个容器组件上,同时将已创建的容器注册到Mapper。 Tomcat通过适配器模式(Adapter)实现了Connector与Mapper、Container的解耦。实现自己的Adapter可以脱离Servlet容器又使用Tomcat链接器。 2.1.7 Excutor 并发问题的解决方案。采用线程池(默认采用JDK5的线程池,继承自Lifecycle,当作通用组件进行管理)对线程进行统一管理。 在Tomcat中Excutor由Service维护,因此同一个Service中的组件可以共享一个线程池。 如果没有定义任何线程池,相关组件(Endpoint)会自动创建线程池,此时线程池不再共享。 在Tomcat中,Endpoint会启动一组线程来监听Socket端口,当接收到客户端请求后,会创建请求处理对象,并交由线程池处理,由此支持并发处理客户端请求。 2.1.8 Bootstrap和Catalina 除开前面的核心组件外,还需要提供一套配置环境来支持系统的可配置性,便于通过修改配置来优化应用。 集群、安全等组件同样重要,但不属于通用概念。 Tomcat通过类Catalina提供了一个Shell程序,用于解析server.xml创建各种组件,同时,负责启动、停止应用服务器(只需要启动Tomcat顶层组件Server)。 Tomcat使用Digester解析XML文件,包括server.xml以及web.xml等。 最后,Tomcat提供了Bootstrap作为应用服务器启动入口。Bootstrap负责创建Catalina实例,根据执行参数调用Catalina相关方法完成针对应用服务器的操作(启动、停止)。 Bootstrap与Tomcat应用服务器完全松耦合(通过反射调用Catalina实例),它可以直接依赖JRE运行并为Tomcat应用服务器创建共享类加载器,用于构造Catalina实例以及整个Tomcat服务器。 上述是Tomcat标准的启动方式。但是Server及其子组件代表了应用服务器本身,那么我们可以不通过Bootstrap和Catalina来启动服务器。 Tomcat组件说明: 组件名称 说明 Server 表示整个Servlet容器,因此Tomcat运行环境中只有唯一一个Server实例 Service Service表示一个或者多个Connector的集合,这些Connector共享同一个Container来处理其请求。在同一个Tomcat实例内可以包含任意多个Service实例,它们彼此独立 Connector 即Tomcat链接器,用于监听并转化Socket请求,同时将读取的Socket请求交由Container处理,支持不同协议以及不同的I/O方式 Container Container表示能够执行客户端请求并返回响应的一类对象。在Tomcat中存在不同级别的容器:Engine、Host、Context、Warpper Engine Engine表示整个Servlet引擎。在Tomcat中,Engine为最高层级的容器对象。尽管Engine不是直接处理请求的容器,确实获取目标容器的入口 Host Hostz作为一类容器,表示Servlet容器中的虚拟机,与一个服务器的网络名有关,如域名等。客户端可以使用这个网络名连接服务器,这个名称必须在DNS服务器上注册。 Context Context作为一类容器,用于表示ServletContext,在Servlet规范中,一个ServletContext即表示一个独立的Web应用 Wrapper Wrapper作为一类容器,用于表示Web应用中动议的Servlet Executor 表示Tomcat组件可以共享的线程池 2.2 Tomcat启动 Tomcat默认实现在相关概念的基础上结合生命周期管理监听器完成了大量的启动工作。 ...
第6章 深入分析ClassLoader工作机制 三个作用: 将Class加载到JVM中; 审查每个类应该由谁加载(双亲委派); 将CLass字节码重新解析成JVM统一要求的格式。 6.1 ClassLoader类结构解析 defineClass方法:将byte字节流解析成JVM能够识别的Class对象。意味着不仅仅可以通过class文件实例化对象。调用此方法生成的Class对象还没有resolve,resolve会在对象真正实例化时才进行。 findClass方法:通过直接覆盖ClassLoader父类的findClass方法来实现类的加载规则,从而取得要加载的字节码。然后调用defineClass方法生成类的Class对象。如果想在类被加载到JVM的时候就被链接(Link),那么可以接着调用另外一个resolveClass方法。 如果想实现自己的ClassLader,一般都会继承URLClassLoader。 6.2 ClassLoader的等级加载机制 双亲委派。 (1)Bootstrap ClassLoader,主要加载JVM自身工作需要的类,完全由JVM自己控制。(既没有更高一级的父加载器,也没有子加载器)。 (2)ExtClassLoader,并不是JVM亲自实现,加载System.getProperty(“java.ext.dirs”)目录下的类。 (3)AppClassLoader,父类是ExtClassLoader。加载System.getProperty(“java.class.path”)目录下的类都可以被其加载。 实现自己的类加载器,都必须最终调用getSystemClassLoader()作为父加载器。而此方法获取到的就是AppClassLoader。 注意Bootstrap ClassLoader并不是如其他文章所说,而是其并无子类也无父类。ExtClassLoader并没有父类加载器。 ExtClassLoader和AppClassLoader都继承了URLClassloader类,而URLClassLoader又实现了抽象类ClassLoader,在创建Launcher对象时会首先创建ExtClassLoader,然后讲ExtClassLoader对象作为父加载器创建AppClassLoader对象。所以如果在Java应用中没有定义其他的ClassLoader,那么除了System.getProperty(“java.ext.dirs”)目录下的类是由ExtClassloader加载,其他类都是由AppClassLoader加载。 加载class文件到内存的两种方式:隐式,显式。 6.3 如何加载class文件 加载、验证、准备、解析、初始化。 第13章 Spring框架的设计理念与设计模式分析 13.1 Spring的骨骼架构 三个核心组件:Core、Context和Bean。 13.1.1 Spring的设计理念 最核心:Bean。(面向Bean编程) 解决了一个关键问题:把对象之间的依赖关系转而用配置文件来管理(依赖注入)。 Spring通过把对象包装在Bean中,从而达到管理这些对象及做一系列额外操作的目的。 这种设计策略完全类似于OOP的设计理念。构建一个数据结构,然后根据这个数据结构设计它的生存环境,并让它在这个环境中按照一定的规律不停地运动,在它们地不停运动中设计一个系列于环境或者与其他各地完成信息交换。(同时也是大多数框架地设计理念) 13.1.12 核心组件如何协同工作 Context负责发现每个Bean之间的关系,建立关系并且维护关系。所以Context就是一个Bean关系的集合,也叫Ioc容器。 Core就是发现、建立和维护每个Bean之间的关系所需要的一系列工具。(也就是一些Util) 13.2 核心组件详解 13.2.1 Bean组件 包:org.springframework.beans。这个包下的类主要解决三件事:Bean的定义、Bean的创建及对Bean的解析。(使用者只需关心创建) Spring是典型的工厂模式,工厂的继承层次关系图如下: 顶级接口BeanFactory有3个子接口:ListableBeanFactory、HierarchicalBeanFantory和AutowireCapableBeanFactory。 DefaultListableBeanFactory实现了所有的结构。 定义多接口的原因:每个接口有不同的使用场景,主要是为了区分Spring内部对象的传递和转化过程中,对对象的数据访问所作的限制。例如,ListableBeanFactory接口标识这些Bean是可列表的,HierarchicalBeanFactory表示这些Bean是有继承关系的,AutowireCapableBeanFactory接口定义Bean的自动装配规则。4个接口共同定义了Bean的集合、Bean之间的关系和Bean的行为。 Bean的定义主要有Beandefinition描述: Bean是配置文件信息中<bean/>节点信息的转化。Spring解析完成后,内部就是一个BeanDefinition对象。 Bean的解析过程过于繁琐,不赘述。 13.2.2 Context组件 ApplicationContext继承了BeanFactory。 ApplicationContext的子类主要包含两个方面: ConfigurableApplicationContext表示该Context是可修改的,也就是构建Context中,用户可以动态添加或修改已有的配置信息。 WebApplication,用于Web,可以直接访问Servletcontext。 ApplicationContext必须完成的事情: 标识一个应用环境 利用BeanFactory创建Bean对象 保存对象关系表 能够捕获各种事件 13.2.3 Core组件 其中有很多关键类,一个重要的组成部分就是定义了资源的访问方式。 Resource类相关:封装了各种可能的资源类型,也就是说对使用者来说屏蔽了文件类型的不同。通过继承InputStreamSource接口,在这个接口中有个getInputStream方法,返回InputStream类,所有资源都可以通过InputStream来获取,及屏蔽了资源的提供者。 Context把资源的加载、解析和描述工作委托给了ResourcePatternResolver类来完成。 13.2.4 Ioc容器如何工作 如何创建BeanFactory工厂 refresh方法。源码已阅就不贴了。步骤如下: (1)构建BeanFactory (2)注册可能感兴趣的事件 (3)创建Bean实例对象 (4)出发被监听的事件 如何创建Bean实例并构建Bean的关系网 详见源码。 Ioc容器的扩展点 BeanFactoryPostProcessor和BeanPostProcessor,分别在构建BeanFactory和构建Be’an对象时调用。还有就是InitPostProcessor和DisposableBean,它们分别在Bean实例创建和销毁时被调用。用户可以实现在这些接口中定义的方法,Spring会在适当的时候调用他们。还有一个是FactoryBean。(会扩展是精通Spring的第一步) Ioc容器如何为我所用 扩展点。通过扩展点来改变Spring的通用行为。(AOP是一个例子,可以作为参考) 13.3 Spring中AOP的特性详解 13.3.1 动态代理的实现原理 java.lang.reflect.Proxy。 重点看公有方法。 阅读源码部分略。 13.2.2 Spring AOP如何实现 13.4 设计模式解析之代理模式 给某一个对象创建一个代理对象,有代理对象控制对原对象的引用,而创建代理对象之后可以再调用时增加一些额外的操作。 ...
6.4字节码指令简介 Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。由于Java虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令都不包含操作数,而只有一个操作码。 6.4.1 字节码与数据类型 Java虚拟机的指令集中,大多数的指令都包含了其操作数所对应的数据类型信息。 如果每一种与数据类型相关的指令都支持Java虚拟机所有运行时数据类型的话,那指令的数量恐怕就会超出一个字节所能表示的数量范围了 前面的被系统吞了 之后再补上 第8章 虚拟机字节码执行系统 8.1概述 8.2 运行时栈帧结构 8.2.1 局部变量表 是一组变量存储空间,用于存放方法参数和方法内部定义的局部变量。 用于存放方法参数和方法内部定义的局部变量。 容量以变量槽(Variable Slot)为最小单位,虚拟机规范中并没有明确指明一个Slot应占用的内存空间大小,知识很有导向性地说道每个Slot都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据。 只要保证计时在64位虚拟机中使用了64位的物理内存空间去实现一个Slot,虚拟机仍要使用对齐和不败的手段让Slot在外观上看起来与32位虚拟机中的一致。 Java中占用32位以内的数据类型有boolean、byte、char、short、int、float、reference和returnAddress8种类型。(Java语言与Java虚拟机种的剧本数据类型是存在本质差别的)。reference类型表示对一个对象实例的引用。虚拟机规范没有指明长度和结构。但需要做到如下两点: 从引用中直接或间接地查找到对象在Java堆中的数据存放的起始地址索引; 引用中直接或简介地查找到对象所属数据类型在方法区中地存储的类型信息,否则无法实现Java语言规范中定义的语法约束。 returnAddress类型目前已经很少见了,为字节码指令jsr、jsr_w和ret服务的,指向一条字节码指令的地址,很古老的Java虚拟机曾经使用这几条指令来实现异常处理,现在已经由异常表替代。 Java中明确的64位的数据类型只有long和double两种。分割存储的做法与“long和double的非原子性协定”类似。 但在局部变量表中不会引起数据安全问题(线程私有)。 索引定位。访问32位数据类型的变量,索引n就代表了使用第n个Slot。64位则会同时使用n和n+1两个Slot。对于两个相邻的共同存放一个64位数据的两个Slot,不允许采用任何方式单独访问其中的某一个。 在方法执行时,如果执行的实例(非static),局部变量表中第0位索引的Slot默认时用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问到这个隐含的参数。 为了尽可能节省栈帧空间,局部变量表中的Slot是可以重用的,方法中定义的变量,其作用域并不一定会覆盖整个方法体。副作用:某些情况下会直接影响到GC。 实例,placeholder能否被回收的根本原因是:局部变量表中的Slot是否还存有关于placeholder数组对象的引用。 局部变量表是GC Roots的一部分。把不用的占用了大量内存的变量手动设置为null值。 但冲编码角度讲,以恰当的变量作用域来控制变量回收时间才是最优雅的解决方法。 ———————————待补充——————————— 局部变量不负初值会编译不通过。
本书特色 结合JDK源码介绍了Java并发框架、线程池的实现原理。 不仅仅局限于Java层面,更深入JVM,CPU。 结合线上应用,给出了一些并发编程实战技巧。 第一章 并发编程的挑战 如果希望通过多线程执行任务让程序运行得更快,会面临非常多得挑战,如上下文切换的问题、死锁的问题,以及受限于硬件和软件的资源限制问题。 1.1 上下文切换 单核处理器支持多线程执行代码,CPU通过给每个线程分配COU时间片来实现。 时间片非常短,所以CPU通过不停的切换线程执行,让我们感觉多个线程是同时执行的,几十毫秒ms。 CPU通过时间片分配算法来循环执行任务。任务从保存到加载的过程就是一次上下文切换。 上下文切换回影响多线程的执行速度。 1.1.1 多线程一定快吗 累加,并发执行的速度比串行慢是因为线程有创建和上下文切换的开销。 1.1.2 测试上下文切换次数和时长 Lmbech3可以测量上下文切换的时长。 cmstat可以测量上下文切换的次数。 CS(Content Switch)表示上下文切换的次数。 1.1.3 如何减少上下文切换 较少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程。 无所并发编程:多线程竞争锁时,回引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。 CAS算法。Java的Atomic包使用CAS算法来更新数据,而不需要加锁。 使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样回造成大量线程都处于等待状态。 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。 1.1.4 减少上下文切换实战 通过见扫线上大量WAITING的线程,来减少上下文切换次数。 1.2 死锁 避免死锁的几个常见方法: 避免一个线程同时获取多个锁。 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的的情况。 1.3 资源限制的挑战 (1)什么是资源限制 资源是指在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源。 硬件资源限制:带宽的上传/下载速读、硬盘读写速度和CPU的处理速度。 软件资源限制:数据库的连接数和scoket连接数。 (2)资源限制引发的问题 在并发编程中,将代码执行速度加快的原则是将代码中串行执行的部分变成并发执行,但是如果将某段串行的代码并发执行,因为受限于资源,仍然在串行执行,这时候程序不仅不会加快执行,反而会更慢,因为增加了上下文切换和资源调度的问题。 (3)如何解决资源限制的问题 对于硬件资源限制,可以考虑使用集群并行执行程序。既然单机的资源有限制,那么就让程序在多级上运行。比如使用ODPS、Hadoop或者自己搭建服务器集群,不同的机器吹不同的数据。可以通过“数据ID%机器数”,计算得到一个机器编号,然后由对应编号的机器处理这笔数据。 对于软件资源限制,可以考虑使用资源池将资源复用。比如使用连接池将数据库和Socket连接复用,或者调用对方webservice接口获取数据时,只建立一个连接。 (4)在资源限制情况下进行并发编程 如何在资源限制的情况下,让程序执行得更快呢?方法就是,根据不同得资源限制调整程序得并发度。 1.4 本章小结 本章介绍了在进行并发编程时会遇到的几个挑战,并给出了一些建议。 第二章 Java并发机制的底层实现原理 JVM执行字节码,最终需要转化为汇编指令在CPU上执行,Java中所使用得并发机制依赖于JVM得实现和CPU的指令。 2.1 volatile的应用 1.volatile的定义与实现原理 volatile是轻量级的synchronized,在多处理器开发中保证了共享变量的“可见性”。使用得当的话,比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。本文分析Intel处理器是如何实现volatile的。 CPU的术语定义: 术语 英文单词 术语描述 内存屏障 memory barriers 是一组处理器指令,用于事项对内存操作的顺序限制 缓冲行 cache line 缓存中可以分配的最小存储单位。处理器填写缓存线时会加载整个缓存线,需要使用多个主内存读周期 原子操作 atomic line 不可终端的一个或一系列操作 缓存行填充 cache line fill 当处理器试别到内存中读取操作数时可缓存的,处理器读取整个缓存行到适当的缓存(L1,L2,L3的或所有) 缓存命中 cache hit 如果进行高速缓存行填充操作的内存位置仍然是下次处理器访问的地址时,处理器从缓存中读取操作数,而不是从内存读取 写命中 write hit 当处理器将操作数写回到一个内存缓存的区域时,它首先回检查这个缓存的内存地址是否在缓存行中,如果存在一个有效的缓存行,则处理器将这个操作数写回到缓存,而不是写回到内存,这个操作被称为写命中 写缺失 write misses the cache 一个有效的缓存行被写入到不存在的内存区域 为了提高处理速度,处理器不直接个内存进行通信,二十先将系统内存的数据督导内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会像处理器发送一条Lock前缀的指令,将这个变量所在缓存的数据写回到系统内存。但是就算写回到内存,如过其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,再多处理器下,未了保证各个处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的换粗你一直,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,回重新从系统内存中把数据读到处理器缓存里。 Lock前缀的指令在多核处理器下会引发两件事情: 将当前处理器缓存行的数据写回到系统内存 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效 volatile的两条实现原则: ...