'事务-分布式事务'
传统事务
事务,即是通过锁控制并发访问。
锁粒度越大,越安全,效率越低;
锁粒度越小,并发高,越不安全。
并发场景:
读读、读写、写读、写写
什么是ACID
原子性(Atomicity)
整个事务,要么全部执行,要么全部不执行。有异常,回滚到执行前状态。
- 记录undo日志,能回滚到之前状态
- 不保证(不一致的)中间结果不被外部看到
一致性(Consistency)
不能破坏关系数据的完整性,以及业务逻辑的一致性。
- 加锁
- 保证中间结果不被外部看到
隔离性(Isolation)==以性能为理由,对一致性的破坏==
持久性(Durability)
事务完成后,改动被持久化存储。不会回滚。
- 磁盘损坏:RAID(保证两块磁盘数据一致,分布式)
- 内存 –> 磁盘:
- 批持久化
- 逐条持久化
- group committed
- 延时提交
- 攒够几个一起提交
读写锁
锁:
- 全局锁(串行化)
- 表级锁
- 数据锁
- 数据读写锁
- 读锁:读数据时加锁,可以并发读。但不能写。
- 写锁:不能并发读,也不能并发写。
数据库隔离级别
隔离级别的存在,提高并发,破坏了一致性。
read uncommitted
- 无读锁、有写锁————读读、读写、写读并行
read committed
- 有读锁、有写锁,读锁可升级为写锁—————读读、读写并行
read repeatable
- 有读锁、有写锁,读锁不可升级写锁—————读读并行
serializable
- 串行化(读时,阻止新数据插入)——————无并行
拓展的隔离级别:snapshot(以上为SQL92标准定义)
当前被映射回SQL92标准的read uncommitted 或者 read committed
- MVCC(当下各流行数据库均有使用)
- 无锁编程
多版本并发控制(MVCC - multi version concurrent control)
核心:copy on write
==优化“写读并发”场景==
- 用新的方式,实现传统意义上read uncommitted场景;同时保证可序列化的隔离级别
- 写多,读少,反而增加系统成本
- 适合读写比率高的场景
拓展(引出的问题):
实现复杂度高。
旧版本数据,什么时候删除。undo log/redo log超长。
常见问题
事务先后
给事务加ID、时间戳。标记事务先后。
故障恢复
业务属性不一致,回滚
需记录事务中,所有操作的反向操作。
数据库崩溃,恢复
先回滚未完成事务,完成后暴露对外访问。
死锁、死锁检测
碰撞检测(高效)
检查事务A请求的锁,被谁持有;持有锁的事务B,又在请求什么锁;该锁刚好被A持有,则判定为死锁。 回滚一个事务即可。
等锁超时
调优原则
- 减少锁的覆盖范围
- 增加锁上可并行的线程数
- 选择正确的锁类型
- 悲观锁
- 使线程到blocking状态(等待被唤醒)
- 导致频繁切换寄存器数据,缓存、cpu cache被清空。换成下一个线程的数据。
- 适用并发争抢严重场景
- 乐观锁
- 适用并发争抢不太严重场景(锁阻塞时间短、请求并发度不高,可以很快抢到锁)
- 悲观锁
'hadoop-spark全景图'
spring中beanFactory、factoryBean的解释
BeanFactory(bean工厂)
常见的spring-web配置方式中,xml配置解析就是使用XmlWebApplicationContxt解析配置文件,然后建立applicationContext(即:bean工厂)。
接口设计:通过层层封装,不断添加新特性、拓展。
FactoryBean(代理真实bean生成,简化bean定义)
以org.springframework.format.datetime.joda.DateTimeFormatterFactoryBean为例
spring检测到该类实现了org.springframework.beans.factory.FactoryBean,则不再调用反射生成实例。
而是,通过factoryBean的getObject()方法,获取要加载的bean。
springboot + springcloud 微服务
为什么说java反射慢
==反射通常包括几两步:==
通过类名、类加载器查找类定义
通过类定义创建实例、访问方法、字段
对于第一点,我们很容易想到办法
- 缓存类定义,也就是类名对应的Class
对于第二点,需要深究
常见耗时,通过Class访问时。大概是普通方式的1.5-3(不准确)倍
The flexibility achieved by reflection in Java is attained to a large extent by delaying the binding of names. That is, the binding of names (of methods, fields, and
so on) that is normally done by the compiler is delayed until runtime. This
delayed binding has a performance impactin the form of searches and checks
executed at runtime. As an example of the former, getMethodmust search the
inheritance hierarchy for the appropriate method object. An example of the latter is Method.invoke, which must check accessibility. This is not a negative statement; it is merely an observation of the trade-off involved in the use of reflection.
参考:
读《Chris Richardson 微服务系列》
阅读文章来自DaoCloud:http://blog.daocloud.io/microservices-1/
微服务架构概念解析
单体应用释义:
应用的核心是商业逻辑,它由定义服务、域对象和事件各模块来完成。各种适配器围绕核心与外部交互。适配器包括数据库访问组件、生成和 consume 信息的消息组件,以及提供 API 或者 UI 访问支持的 web 模块。
优点:
- 易于构建、部署
- 容易测试
- 横向拓展,负载均衡方便
缺点:
- 系统复杂:内部多模块紧密耦合,关联依赖复杂
- 运维困难:代码量巨大,没有人能理解整个应用,bug修复困难。任何修改都必须重启整个应用。
- 无法拓展:不能拆分部署,出现性能瓶颈往往只能增加服务器或集群节点。但是DB问题无法解决。
微服务释义:
一个微服务一般==完成某个特定的功能== ,比如订单管理、客户管理等。每个微服务都是一个微型应用,==有着自己六边形架构==,包括商业逻辑和各种接口。有的微服务通过暴露 API 被别的微服务或者应用客户端所用;有的微服务则通过网页 UI 实现。
与SOA区别:
- 不包含网络服务说明(WS-*)
- 不包含Enterprise Service Bus(ESB)
优点:
- scale cube的3D模型
- Y轴:应用分解,即将传统的单体应用分解为多个微服务应用
- X轴:负载均衡实现水平弹性拓展,但DB问题无法解决,引入3
- Z轴:DB弹性拓展,引入数据库拆分和Daas
- 引用分解后,单应用复杂度降低。每个服务都有一个RPC 或者消息驱动API,边界清晰。
- 单应用可以自由选择开发技术,不必受某些早起技术影响。甚至重构代码也相对简单。
- 服务单独部署,不需要协调其他服务部署。加快部署速度。
- 单服务对于性能消耗更明确,利于优化硬件资源配置。
缺点:
- 微服务应用是分布式系统,由此带来固有复杂性
- 通讯机制:RPC或消息传递
- 通讯速度慢
- 服务不可用
- 通讯超时
- 通讯机制:RPC或消息传递
- CAP原则:分布式事务一致性,业务实现复杂度提高
- 集成复杂:任何吃滴的分解都将带来集成的复杂度
- 部署:部署节点多,还涉及部署后的配置、拓展、监控问题
使用API网关构建微服务
客户端与微服务交互方式。
单体应用只需要发起一次请求,就能拿到所有需要的信息;
微服务需要发起多次请求,调用不同微服务提供的API。
发现问题:
- 客户端需求和每个微服务暴露的细粒度API不匹配
- 部分服务使用的协议对web并不友好
- 重构困难:系统拆分、合并,涉及外部改造。困难重重
使用API网关的架构:
API网关职责:请求路由、组合、协议转换
- 外部调用API,网关调用多个微服务并合并结果
- 处理内部服务,与外部调用协议不匹配问题
- 根据外部不同设备,设计不同API
优点:
- 封装了应用程序内部,简化外部调用
缺点:
- 增加了一个必须开发、部署、维护的高可用组件
- API网关变成了开发瓶颈
实现API网关
- 性能和可拓展性
- 构建在一个支持异步、I/O非阻塞的平台
- JVM上的NIO框架,例如:netty、vertx、spring reactor等
- node.js
- 另一种方法,使用nginx plus提供的成熟的、可拓展的、高性能web服务器和一个易于部署的、可配置、可编程的反向代理
- 身份验证
- 访问控制
- 负载均衡
- 缓存响应
- 提供程序可感知的健康检查、监控
- 构建在一个支持异步、I/O非阻塞的平台
- 使用响应式编程模型
- 独立请求
- 组合请求:代码复杂
- 响应式抽象概念的例子有 Scala 中的 Future、Java 8 中的 CompletableFuture 和 JavaScript 中的Promise,还有最初微软为 .NET 平台开发的 Reactive Extensions(RX)。
- 服务调用:多协议支持
- 异步,基于消息传递的机制==响应可能非即时==
- jms
- amqp
- 同步通信==由于等待而阻塞==
- http
- thrift
- 异步,基于消息传递的机制==响应可能非即时==
- 服务发现:应用程序服务的位置是动态分配的,而且,单个服务的一组实例也会随着自动扩展或升级而动态变化。
- 服务端发现
- 客户端发现
- 服务版本问题:
- 特别是RestAPI调用,由于json本身无Schema返回,更容易忽视对服务的版本控制
- 小版本变更,直接覆盖。升级受影响消费端
- 大版本升级,则视为增加一个服务;逐步迁移和替代旧版本服务
- 特别是RestAPI调用,由于json本身无Schema返回,更容易忽视对服务的版本控制
- 处理局部失败:Netfilix的服务解决方案
- 超时设置
- 断路器机制
- 流量控制
- 缓存数据或默认返回值
- 最终目的:减少对最终用户影响
进程间通信
一对一 | 一对多 | |
---|---|---|
同步 | 请求/响应 | |
异步 | 通知(客户端不期望服务端响应) | 发布/订阅(通知消息) |
异步 | 请求/异步响应(客户端不阻塞,默认响应不会立即到达) | 发布/异步响应(请求消息,等待从感兴趣的服务发回响应) |
REST包含以下四个层次:
- Level 0:本层级的 Web 服务只是使用 HTTP 作为传输方式,实际上只是远程方法调用(RPC)的一种具体形式。SOAP 和 XML-RPC 都属于此类。
- Level 1:Level 1 层级的 API 引入了资源的概念。要执行对资源的操作,客户端发出指定要执行的操作和任何参数的
POST
请求。 - Level 2:Level 2 层级的 API 使用 HTTP 语法来执行操作,譬如
GET
表示获取、POST
表示创建、PUT
表示更新。如有必要,请求参数和主体指定操作的参数。这能够让服务影响 web 基础设施服务,如缓存GET
请求。 - Level 3:Level 3 层级的 API 基于 HATEOAS(Hypertext As The Engine Of Application State)原则设计,基本思想是在由
GET
请求返回的资源信息中包含链接,这些链接能够执行该资源允许的操作。例如,客户端通过订单资源中包含的链接取消某一订单,GET
请求被发送去获取该订单。HATEOAS 的优点包括无需在客户端代码中写入硬链接的 URL。此外,由于资源信息中包含可允许操作的链接,客户端无需猜测在资源的当前状态下执行何种操作。
优点:
- HTTP 非常简单并且大家都很熟悉。
- 可以使用浏览器扩展(比如 Postman)或者 curl 之类的命令行来测试 API。
- 内置支持请求/响应模式的通信。
- HTTP 对防火墙友好。
- 不需要中间代理,简化了系统架构。
缺点:
- 只支持请求/响应模式交互。尽管可以使用 HTTP 通知,但是服务端必须一直发送 HTTP 响应。
- 由于客户端和服务端直接通信(没有代理或者缓冲机制),在交互期间必须都保持在线。
- 客户端必须知道每个服务实例的 URL。如前篇文章“API 网关”所述,这也是个烦人的问题。客户端必须使用服务实例发现机制。
Thrift
服务发现的可行方案以及实践案例
客户端发现模式:
Netflix OSS 是客户端发现模式的绝佳范例。Netflix Eureka 是一个服务注册表,为服务实例注册管理和查询可用实例提供了 REST API 接口。Netflix Ribbon 是 IPC 客户端,与 Eureka 一起实现对请求的负载均衡。我们会在后面深入讨论 Eureka。
优点:
- 除了服务注册,其他部分无需变动
- 能针对特定应用实现负载均衡
缺点:
- 客服端与服务注册绑定,要针对服务端用到的每个编程语言和框架,实现发现服务逻辑
服务端发现模式
HTTP 服务器与类似 NGINX PLUS 和 NGINX 这样的负载均衡起也能用作服务端的发现均衡器。Graham Jenson 的 Scalable Architecture DR CoN: Docker, Registrator, Consul, Consul Template and Nginx一文就描述如何使用 Consul Template 来动态配置 NGINX 反向代理。Consul Template 定期从 Consul Template 注册表中的配置数据中生成配置文件;文件发生更改即运行任意命令。在这篇文章中,Consul Template 生成 nginx.conf 文件,用于配置反向代理,然后运行命令,告诉 NGINX 重新加载配置文件。在更复杂的实现中,需要使用 HTTP API 或DNS 来动态配置 NGINX Plus。
———————–未完待续————————-
读《从paxos到zookeeper分布式一致性》
[TOC]
一致性协议
- question:为什么说两阶段提交时,参与者同步阻塞,等待其他参与者响应时,无法进行其他任何操作?
- res:
角色类型
- leader:提供读写
- follower:提供读,参与leader选举
- observer:提供读
数据节点
- 机器节点
- 数据节点-Znode
watcher
ACL(Access Control List)权限控制
zookeeper专门设计的ZAB协议
- 崩溃恢复:选举新的leader
- 触发条件:leader崩溃;或者leader与过半follower失去联系
- 选举新的leader,通知自己及集群内所有机器
- 选举出的leader必然具有最大事务ID
- follower同步leader数据,保证状态一致。同步完成的加入可用follower列表
- 消息广播:过半机器同意则提交
- 基于具有FIFO特性的TCP协议来进行网络通信,保证消息接受与发送的顺序性。
- leader为每个事务请求分配全局单调递增的唯一ID,保证顺序执行。
- leader为每个follower分配一个单独队列,存放请求。
- follower接到请求,先将其写入日志,再反馈给leader
- leader接收到超过半数响应,则通知提交事务
选举算法
实际应用
1.发布/订阅:配置中心
信息特性:
- 数据量通常比较小
- 数据内容再运行时会发生动态变化
- 集群中各机器共享,配置一致
两种模式:
- pull:客户端与ZK建立心跳响应
- push:在ZK注册watcher,接收节点事件更新
2. 负载均衡
3.命名服务:生成全局唯一序列号
4.分布式协调/通知
5.集群管理
6.master选举:集群中选出某一台机器,来做一件事
- 监控时间完成情况,通知其余机器
- master机器宕机,可立即重新选举master
7.分布式锁
排它锁
共享锁
步骤:==会造成羊群效应,不断获取所有子节点==
- 创建节点,获取/shared_lock节点下的所有子节点,并对改节点注册子节点变更的watcher监听。
- 确定自己的节点序号在所有子节点中的顺序
- 对于读请求:
- 如果没有比自己序号小的子节点,或者所有比自己小的子节点都是读请求。那么表名,获取共享锁成功。
- 如果比自己序号小的子节点中有写请求,那么就需要进入等待。
- 对于写请求:
- 如果自己不是序号最小的子节点,那么就需要进入等待。
- 接收到watcher通知后,重复步骤【1】。
改进的共享锁
8.分布式队列
FIFO
Barrier:分布式屏障
- 通过调用getData( )接口获取/queue_barrier节点的数据内容:10 即,达到10个调用
- 通过调用getChildren( )接口获取/queue_barrier节点下的所有子节点,即获取队列中所有元素,同时注册对子节点列表变更的watcher见监听。
- 统计子节点个数
- 如果子节点不足10个,继续等待
- 接收到watcher通知后,重复步骤2
Dubbo
使用zookeeper做为注册中心
Lock和ReentrantLock
Lock
特征:
- 提供无条件的、可轮询的、定时的、可中断的锁获取操作
- 显式的加锁、解锁
实现类:
- ReentrantLock
- ReentrantReadWriteLock.ReadLock
- ReentrantReadWriteLock.WriteLock
API:
1 | public abstract interface Lock{ |
Lock与synchronized比较
Lock | synchronized |
---|---|
使用灵活 | 使用简单 |
必须手动加锁、解锁 | 关键字搞定 |
锁代码块 | 锁是对象(this…) |
ReentrantLock
Lock的实现类,是一个互斥的同步器。竞争条件下,比synchronized实现具有更好的伸缩性。
公平锁
获取锁时判断,是否有前置节点在等待中,有则休眠,等待
非公平锁
释放锁
==图示两种锁策略==
JVM笔记
[TOC]
一 走近java
1.1 java技术体系发展趋势
二 自动内存管理机制
2.1 虚拟机区域划分,内存溢出常见原因
2.1.1 运行时数据区域
程序计数器
可以看作是当前线程所执行的字节码的行号指示器
线程私有
如果正在执行java方法,记录的是虚拟机字节码指令的地址;如果正在执行的是Native方法,即java程序调用C程序的方式,计数器值为空。
唯一不存在OutOfMemoryError的内存区域
虚拟机栈:服务于java方法
描述java方法执行的内存模型:每个方法在执行时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
方法调用到执行完成的过程,对应一个栈帧在虚拟机入栈到出栈的过程。
局部变量表:存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double),对象引用和returnAddress类型(指向了一条字节码指令的地址)。
- 局部变量表所需空间在编译器完成分配
- long和double占用2个局部变量空间(slot),其余只占用一个
- 运行期间不改变局部变量表大小
虚拟机栈不可动态拓展:线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError
虚拟机栈可动态拓展==大多可拓展==:拓展时无法申请到足够内存,抛出OutOfMemoryError
本地方法栈:服务于Native方法
类似虚拟机栈
java堆
存放实例对象
虚拟机启动时创建
线程共享
虚拟机内存中最大的一块
垃圾收集器管理的主要区域,又称“GC堆”
可动态拓展(-Xmx和-Xms控制)
没有内存完成实例分配,且堆无法再拓展,抛出OutOfMemoryError
方法区
存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
线程共享
JVM[^hotSpot] 使用永久带实现,有上限(-XX:MaxPermSize)
无法满足内存分配需求时,抛出OutOfMemoryError
运行时常量池
方法区的一部分,用于存放编译期生成的各种字面量和符号引用
符号引用+翻译出来得直接引用
可动态拓展,无法申请内存时,抛出OutOfMemoryError
直接内存
虚拟机之外的系统内存
NIO操作中,引入一种基于通道(channel)与缓冲区(buffer)的I/O方式,它可以使用Native函数库直接分配的堆外内存。然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。==避免了在java堆和Native堆中来回复制数据==,显著提高性能
容易被忽略的内存空间。当虚拟机各区域内存总和大于物理内存限制,或剩余直接内存过小,会导致动态拓展失败出现OutOfMemoryError
2.2 JDK提供的几种垃圾收集算法,验证自动内存分配及回收的主要规则
GC的三个目标:
- 哪些内存需要回收
- 什么时候回收
- 如何回收
2.2.1 对象是否死亡
- 引用计数法
- 有地方引用它,计数器值加1
- 引用失效,计数器减1
- 计数器为0对象不能再被使用
- 无法解决对象循环引用问题,内存泄漏
- 可达性分析算法
- 可作为GC Roots的对象包括
- 虚拟机栈(本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
- java引用概念的扩充
- 强引用
- 常见对象创建赋给变量属于此类,存在强引用就不会回收
- 软引用
- 有用但非必须
- 内存溢出前会尝试回收这部分空间
- 弱引用
- 非必须对象
- 只能生存到下一次垃圾收集发生前
- 虚引用
- 不对对象生存时间构成影响
- 目的:对象呗回收时,收到一个系统通知
- 强引用
- 回收方法区
- 可以回收,回收效率低
- 废弃常量
- 无用的类
- 该类所有实例已回收
- 该类的ClassLoader已回收
- 对应Class对象没有被任何地方引用,无法通过反射访问
- 常见大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGI这类频繁自定义ClassLoader场景。需要卸载类,保证永久带不溢出
2.2.2 垃圾收集算法
- 标记-清除算法(Mark-Sweep)
- 标记、清除过程效率不高
- 产生大量不连续内存碎片,导致之后分配较大对象时,无法找到足够的连续内存。提前触发另一次垃圾收集操作
- 复制算法
- 将内存分为相等的两块,当一块的内存用完,就将还存活的对象复制到另一块上面;然后再把使用过的内存一次清理掉
- 目的:提高效率;内存空间连续
- 缺点:只能使用一半内存空间,浪费
- 优化:将内存分割为一块较大的eden和两块较小的survivor。
- HotSpot虚拟机默认Eden : Survivor = 8 : 1,即只有10%的内存空间被“浪费”
- Survivor空间不够时,将使用老年代内存空间分担
- 标记-整理算法
- 目的:解决“复制算法”极端情况下,对象100%存活问题。
- 为老年代设计
- 类似“标记-清除”算法,只是清除后,存活对象往一边移动,空出另一侧
- 分代收集算法
- 划分新生代、老年代
- 新生代:每次都有大批对象死去,“复制算法”
- 老年代:对象存活率高、没有额外空间为其分担,“标记-清理” 或者 “标记-整理”
2.2.3 HotSpot的算法实现
- 每局根节点
- 可达性分析时,为保证 “一致性” 。所有java执行线程将暂停,又称“stop the world”
- 优化:用一组OopMap数据结构保存,哪些地方存放着对象引用
- 安全点
- 所有线程到达安全点才能开始GC
- 安全区域
- 针对sleep、blocked状态的线程
2.2.4 垃圾收集器
2.2.5 g1收集器
2.2.6 理解gc日志
2.2.7 垃圾收集器参数总结
2.2.8 内存分配与回收策略
- 对象优先在Eden分配
- 新生代 = Eden + Survivor
- 空间不够时,发起GC
- 打印内存回收日志,-XX:+PrintGCDetails
- 大对象直接 进入老年代
- 很长的字符串以及数组
- -XX:PretenureSizeThreshold,大于该阀值的对象直接进入老年代
- 长期存活的对象将进入老年代
- 为每个对象定义一个年龄(Age)计数器
- 对象在Eden出生,经过一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间,设置对象年龄为1。
- 对象在Survivor中每“熬过”一次Minor GC,年龄就加1。默认达到15,就会被晋升到老年代中。
- -XX:MaxTenuringThreshold设置几岁晋升老年代
- 动态对象年龄判定
- Survivor空间中,相同年龄的所有对象大小总和大于Survivor空间的一半,则年龄大于等于该年龄的对象就可以直接进入老年代
- 空间分配担保
- Minor GC之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象之和;如果成立,那么Minor GC是安全的;如果不成立,虚拟机查看参数(HandlePromotionFailure)设置是否允许担保失败。
- 如果允许担保失败,那么继续检查老年代最大可用连续空间是都大于历次晋升到老年代对象平均大小;如果大于,尝试一次Minor GC,失败Full GC;如果小于,直接Full GC
- 如果不允许担保失败,直接Full GC
2.3 虚拟机性能监控与故障处理工具
2.3.1 JDK命令行工具
- jps:jvm process status tool
- 显示指定系统内所有hotspot虚拟机进程
- jstat:jvm statistics monitoring
- 用于收集hotspot虚拟机各方面运行数据
- jinfo:configuration info for java
- 显示虚拟机配置信息
- jmap:memory map for java
- 生成虚拟机的内存转储快照(heapdump文件)
- jhat:jvm heap dump browser
- 用于分析heapdump文件,建立一个web服务,让用户通过浏览器查看分析结果
- jstack:stack trace for java
- 显示虚拟机的线程快照(threaddump文件)
2.4 故障处理、调优实战
三 虚拟机执行子系统
3.1 Class文件组成,各部分定义、数据结构、使用方法;演示储存于访问
3.2 类加载过程:加载、验证、准备、解析、初始化;类加载器工作原理及其对虚拟机的意义
比较两个类是否相等:
- 类限定名相同
- 类加载器相同
类加载器双亲委派模型:
- 从当前类加载器出发
- 委派给父类加载器加载(从bootstarp classloader开始寻找)
- 最后到当前类加载器,一旦找到就返回
破坏双亲委派模型:
- 兼容JDK1.2以前版本
- 线程上下文类加载器(thread context classloader)
- 创建线程时未设置,从父线程继承
- 应用程序全局范围未设置,则默认为application classloader
- 用于SPI,例如JNDI、JDBC等
- OSGI(模块化)
- 通过替换类加载器,实现可插拔
3.3 虚拟机在执行代码时如何找到正确的方法,如何执行方法内的字节码,以及执行代码时涉及的内存结构
3.4 实战 + 类加载及执行子系统案例
四 程序编译与代码优化
4.1 分析java语言中泛型、主动装箱和拆箱、条件编译等语法糖;实战演示如何使用插入式注解处理器实现一个程序命名规范的编译器插件
4.2 虚拟机热点探测方法、hotSpot即时编译、编译触发条件,以及如何从虚拟机外部观察和分析JIT编译的数据和结果。常见编译优化技术
五 高效并发
5.1 虚拟机内存模型结构及操作,以及原子性、可见性和有序性在java内存模型中的体现;先行发生原则的规律及使用,线程在java语言中是如何实现的
5.1.1 内存模型
- 处理器对输入代码,乱序执行优化
- 虚拟机编译时,指令重排序优化
主内存与工作内存
- 线程对变量的所有操作,都必须在工作内存中完成
- 不同线程,无法直接访问对方工作内存中的变量
内存间交互操作:原子的、不可再分的
主内存与工作内存
lock:作用于主内存,把一个变量标志位一条线程独占
unlock:作用于主内存,把一个变量解锁,其他线程可访问
read:作用于主内存,从主内存读到工作内存,便于随后load
load:作用于工作内存,从主内存得到的变量值放入工作内存副本
use:作用于工作内存,把工作内存中变量传递给执行引擎
assign:作用于工作内存,执行引擎的变量赋值给工作内存
store:作用于工作内存,把工作内存中变量传递给主内存,便于随后write
write:作用于主内存,把store操作传递的变量写入主内存
要完成变量复制,就要顺序执行(read、load)(store、write)操作。
然而,java内存模型只要求顺序执行,不是连续执行。所以,指令之间会插入其他指令。
内存交互规则
对于volatile型变量的特殊规则
- 保证可见性:每次读拿到的都是最新值
- 不保证原子性:写操作可能相互覆盖
- 禁止指令重排:设置内存屏障,后面指令不能排到屏障之前
- 使用场景:
- 运算结果不依赖当前值
- 能够保证只有单一的线程改变变量值
- 变量不需要与其他状态变量共同参与不变约束
- ==规则==:
- 保证read、load、use连续一起出现(每次使用变量都必须从主内存刷新)
- 保证assign、store、write连续一起出现(每次修改变量后立即同步回主内存)
- 禁止指令重排
原子性、可见性、有序性
- 原子性:操作中不会插入其他指令
- 可见性:一个线程修改了共享变量,其他线程立即知晓
- 有序性:本线程内有序;一个线程观察另一个线程表现为无序(指令重排,工作内存与主内存同步引起)。使用volatile和synchronized保证有序性
先行发生原则
- 程序次序规则:代码依次执行
- 管程锁定规则:同一个锁,unlock发生于lock之前
- volatile变量规则:volatile变量写操作,发生于后面读操作之前
- 线程启动规则:thread的start()方法,发生于线程每一个动作之前
- 线程终止规则:线程中所有操作,发生于对此线程终止检测之前
- 线程中断规则:线程interrupt()方法,发生于检测线程中断代码之前
- 对象终结规则:一个对象的初始化,发生于它的finalize()之前
- 传递性:A先于B,B先于C;则A先于C
5.2 线程安全、同步实现的方式以及虚拟机底层运作原理;虚拟机实现高效并发所采取的一系列锁优化措施
5.2.1 线程的实现:CPU调度的基本单位
- 使用内核线程实现
- 直接由操作系统内核支持的线程
- 每个内核线程可视为一个内核分身
- 内核通过调度器(scheduler)调度线程,分配到各个处理器
- 程序一般不直接 使用内核线程,而是使用内核线程的一种高级接口–轻量级进行(light weight process, LWP)
- 内核线程与轻量级进程之间 1 : 1的线程模型
- 局限性
- 线程操作都需要进行系统调度,代价较高:需要在用户态、内核态中来回切换
- 消耗内核资源(内核线程栈空间)
- 支持线程数有限
- 用户线程实现
- 完全建立在用户空间的线程库上
- 线程操作完全在用户态完成,低消耗
- 线程与用户线程之间 1:N
- 局限性
- 线程创建、切换、调度都需要用户程序自己处理
- 阻塞处理、多处理器跟线程映射 解决困难
- 程序复杂
- 用户线程+轻量级进程混合实现
- 线程创建等在用户线程
- 线程调度、处理器映射等在轻量级进程
- java线程的实现
- 基于操作系统原生线程模型实现
- 不同平台不一致
- 对于sun JDK来说,win,linux中均为一对一模型,即一条java线程映射到一条轻量级进程
5.2.2 java线程调度
- 协同式线程调度
- 抢占式线程调度(java使用的)
- 线程执行时间系统控制
- 不会一个线程导致整个进程阻塞
- 通过设置线程优先级,可以倾斜资源分配
5.2.3 状态转换
- new:新建,未启动
- runnable:正在执行、或等待系统分配资源
- waiting:无限期等待,直到被其他线程唤醒
- timed waiting:限期等待,一定时间后自动唤醒
- blocked:阻塞,等待锁释放
- terminated:线程终止状态
5.2.4 线程安全的实现方法
- 互斥同步:悲观锁
- 非阻塞同步:乐观锁(发现不一致,重试)
5.2.5 锁优化(JDK完成)
- 自旋锁与自适应自旋
- 场景:共享数据的锁只会持续很短时间,挂起、回复线程并不值得
- 并发请求同一个 锁,后一个线程为了占有处理器执行时间,让线程执行一个忙循环,称为“自旋锁”
- -XX:+UseSpinning参数开启,JDK1.6默认开启
- -XX:PreBlockSpin参数设置自旋次数,默认10次
- 自适应自旋
- 根据前一次在同一个锁上自旋时间及锁拥有者状态来决定
- 如果,同一个锁对象上,自旋等待刚刚获得成功,并且持有锁的线程正在运行中,那么判定这次自旋也很有可能成功
- 如果,某个锁,自旋很少获得过成功。那以后获取这个锁,很可能省略掉自旋过程。
- 锁消除
- 编译器检测到不可能存在共享数据竞争的锁进行消除
- 例如:方法内局部变量,使用了stringBuffer
- 锁粗化
- 扩大锁范围
- 例如:循环中不断加锁、解锁,会将锁范围扩大到循环外
- 轻量级锁
- 记录在对象(锁对象)头部
- 通过CAS操作,设置失败,且锁不归属当前线程,则升级为重量级锁
- 经验判断:绝大部分锁,在整个同步周期内,不保存在竞争。避免使用重量级锁,减少开销
- 偏向锁
- 消除数据在无竞争情况下的同步
- 相比于轻量级锁,连CAS都不做
- -XX:+UseBiasedLocking,默认开启
- 通过在对象头设置偏向锁标记,CAS记录第一个获取到锁的线程ID实现
- 一旦另外一个线程获取锁,则偏向锁宣告结束
参考信息
[^hotSpot]: JDK中JVM的默认实现,java -version查看.