大型网站系统与Java中间件开发(曾宪杰)-持续更新
2022-02-16 14:15:50 19 举报
AI智能生成
大型网站系统与Java中间件开发(曾宪杰)-持续更新
作者其他创作
大纲/内容
4、服务框架
网站功能持续丰富后的困境与应对
服务框架的设计与实现
应用从集中式走向分布式所遇到的问题
透过实例看服务框架原型
服务调用端的设计与实现
服务提供短的设计与实现
服务升级
实战中的优化
为服务化护航的服务治理
服务框架与ESB的对比
总结
5、数据访问层
数据库从单机到分布式的挑战和应对
从应用使用单机数据库开始
数据库垂直拆分/水平拆分的困难
单机变为多机后,事务如何处理
多机的Sequence问题和处理
应对多机的数据查询
数据库访问层的设计和实现
如何对外提供数据访问层的功能
按照数据层流程的顺序看数据层设计
独立部署的数据访问层实现方式
读写分离的挑战和应对
总结
6、消息中间件
消息中间件的价值
消息中间件的定义
透过实例看消息中间件对应用的解耦
互联网时代的消息中间件
如何解决消息发送的一致性
如何解决消息中间件和使用者的强依赖问题
消息模型对消息接收的影响
消息订阅者订阅消息的方式
保证消息可靠性的做法
订阅者视角的消息重复的产生和应对
消息投递的其他属性支持
保证顺序的消息队列的设计
Push和Pull方式的比对
8、构建大型网站的其他要素
加速静态内容访问速度的CDN
大型网站的存储支持
搜索系统
数据计算支撑
发布系统
应用监控系统
依赖管理系统
多机房问题分析
系统容量规划
内部私有云
后记
待定
1、分布式系统介绍
认识分布式系统
分布式系统定义
经典定义
1、组件分布在网络计算机中
2、组件之间仅仅通过消息传递来通信进行协调
分布式系统意义
为什么需要分布式系统呢?
升级单机处理能力的性价比越来越低
单机处理能力存在瓶颈
单机处理能力有哪些?
处理器CPU
内存
磁盘
网络
摩尔定律
当价格不变时,每个18个月,集成电路上可容纳的晶体管数目会增加一倍,性能也能提升一倍
摩尔定律告诉我们,随着时间转移,单位成本的支出所能购买的计算能力在提升。不过,如果把时间固定下来,所购买的处理器性能越高,所要付出的成本越高,性价比就越低。
出于稳定性和可用性的考虑
单机存在单点故障,虽然可以做容灾备份,而这些方案就会让你的单机系统演变成分布式系统了
分布式系统的基础知识
组成计算机的5大要素
冯诺依曼的EDVAC(电子离散变量自动计算机)
冯诺依曼计算机
输入设备
输出设备
运算器
控制器
存储器
内存
外存
图
子主题
线程与进程的执行模式
阿姆达尔定律
程序中可并行代码的比例决定你增加处理器(总核心数)所能带来的速度提升的上限,是否能达到这个上限,还取决于其他的因素
多线程的几种交互模式
互不通信的多线程模式
线程之间不需要处理共享的数据,也不需要进行动作协调,线程独立运行
图
基于共享容器(数据对象)协调的多线程模式
线程之间需要对共享的数据进行处理
图
案例
基于队列的生产者消费者
线程不安全的容器或者对象
通过加锁
如果读写比例很高
采用读写锁而不是互斥锁
Copy On Write
线程安全的容器或者对象
直接使用
思考如下代码是否有问题?
HashMap<String, Integer> map = new HashMap<String, Integer>();
public synchronized void add(String key){
Integer value = map.get(key);
if (value == null){
map.put(key, 1);
}else{
map.put(key, value + 1);
}
}
public synchronized void add(String key){
Integer value = map.get(key);
if (value == null){
map.put(key, 1);
}else{
map.put(key, value + 1);
}
}
Map<String, Integer> map = new ConcurrentHashMap<>();
public void add(String key){
Integer value = map.get(key);
if (value == null){
map.put(key, 1);
}else{
map.put(key, value + 1);
}
}
public void add(String key){
Integer value = map.get(key);
if (value == null){
map.put(key, 1);
}else{
map.put(key, value + 1);
}
}
通过事件协同的多线程模型
定义
除了并发访问的控制,线程间会存在着协调的需求,例如AB两个线程,B线程需要等到某个状态或事件发生后才能继续自己的工作,而这个状态改变或事件产生和A线程有关,这种场景下,就需要完场线程间的协调。
图
子主题
死锁
死锁是怎么发生的?
两个锁A和B,两个线程T1和T2,T1和T2的某段代码都需要获取A和B的两个锁。
伪代码
伪代码:T1代码:
A.lock();
B.lock();
T2代码:
B.lock();
A.lock();
A.lock();
B.lock();
T2代码:
B.lock();
A.lock();
这个时候,T1等不到B,T2等不到A,就发生死锁了
如何解决呢?
如何避免死锁
原子性的获取锁
调整对多个锁的获取顺序
伪代码
T1代码:
A.lock();
B.lock();
T2代码:
A.lock();
B.lock();
A.lock();
B.lock();
T2代码:
A.lock();
B.lock();
和前面相比,T2获取锁的顺序发生了变化,现在和T1一样,都是先获取A然后再获取B,这样就能避免死锁
一次性获取需要的全部锁
伪代码
伪代码:T1代码:
getLocks(A,B);
T2代码:
getLocks(A,B);
getLocks(A,B);
T2代码:
getLocks(A,B);
多进程间的几种交互模式
多进程交互的特点
进程之间内存空间是独立的,因此多进程之间通过内存共享,交互数据的方式和多个线程之间的方式就有所不同
多进程相比多线程,资源控制会更容易实现
多进程中的单个进程问题,不会造成整体不可用
多进程之间
网络通信基础知识
OSI与TCP/IP网络模型
经典的ISO的OSI七层模型
应用层Application
视图层Presentation
会话层Session
传输层Transport
网络层Network
数据链路层Data Link
物理层Physical
TCP/IP网络模型
应用层Application
传输层Transport
网络层Internetwork
网络接口层 Network Interface
网络IO的实现方式
TCP
BIO
定义
BIO即Blocking IO,采用阻塞的方式实现。
一个Socket套接字需要使用一个线程来进行处理。
建立连接,读数据,写数据都可能会阻塞。
工作方式&原理
图
子主题
优缺点
优点
简单易懂
缺点
阻塞,并发低
NIO
定义
NIO即NonBlocking IO,基于事件驱动的方式,采用的Reactor模式
工作方式&原理
Reactor模式
Reactor会管理所有的Handler,并且把出现的事件交给相应的Handler去处理。
在NIO的方式下,不是用单个线程去应对单个Socket套接字,而是统一通过Reactor对所有的客户端的Socket套接字的事件做处理,然后派发到不同的线程中。解决了BIO
图
子主题
https://www.processon.com/diagraming/61360e2763768906a2236432
优缺点
优点
相比于BIO,NIO的一个明显的好处是不需要为每个Socket套接字分配一个线程,而可以在一个线程中处理多个Socket套接字相关的工作。
AIO
定义
AIO即AsynchronousIO即异步IO。
AIO采用Proactor模式。
图
子主题
AIO与NIO的差别
AIO在进行读写操作时,只需要调用相对应的read/write方法,并且需要传入CompletionHandler(动作完成的处理器,即回调Callback);在动作完成后,会调用CompletionHandler,当然,不同的系统上会有一些细微差异,不同语言在SDK也会有所差别,但总体就是这样的工作方式。
NIO的通知是发生在动作之前,是在可读,可写的时候,Selector发现这些事件后调用Handler。
工作方式&原理
优缺点
高性能IO模型分析-Reactor模式和Proactor模式(二)
https://zhuanlan.zhihu.com/p/95662364
前言
在web服务中,处理web请求通常有两种体系结构
thread-based architecture(基于线程的架构)
通俗的说就是:多线程并发模式,一个连接一个线程,服务器每当收到客户端的一个请求, 便开启一个独立的线程来处理。
优点
这种模式一定程度上极大地提高了服务器的吞吐量,由于在不同线程中,之前的请求在read阻塞以后,不会影响到后续的请求。但是,仅适用于于并发量不大的场景,因为:
缺点
线程需要占用一定的内存资源
如果连接数太高,系统将无法承受
创建和销毁线程也需一定的代价
操作系统在切换线程也需要一定的开销
线程处理I/O,在等待输入或输出的这段时间处于空闲的状态,同样也会造成cpu资源的浪费
event-driven architecture(事件驱动模型)
这种方式会定义一系列的事件处理器来响应事件的发生,并且将服务端接受连接与对事件的处理分离。其中,事件是一种状态的改变。比如,tcp中socket的new incoming connection、ready for read、ready for write。
Reactor模式和Proactor模式都是是event-driven architecture(事件驱动模型)的实现方式
Reactor模式
定义
The reactor design pattern is an event handling pattern for handling service requests delivered concurrently to a service handler by one or more inputs. The service handler then demultiplexes the incoming requests and dispatches them synchronously to the associated request handlers
工作方式&原理
Reactor模式首先是事件驱动的,有一个或多个并发输入源,有一个Service Handler,有多个Request Handlers;
Service Handler会对输入的请求(Event)进行多路复用,并同步地将它们分发给相应的Request Handler。
图
子主题
Reactor模式也有三种不同的方式
Reactor模式-单线程模式
Java中的NIO模式的Selector网络通讯,其实就是一个简单的Reactor模型。可以说是单线程的Reactor模式
Reactor的单线程模式的单线程主要是针对于I/O操作而言,也就是所以的I/O的accept()、read()、write()以及connect()操作都在一个线程上完成的。
但在目前的单线程Reactor模式中,不仅I/O操作在该Reactor线程上,连非I/O的业务操作也在该线程上进行处理了,这可能会大大延迟I/O请求的响应。所以我们应该将非I/O的业务逻辑操作从Reactor线程上卸载,以此来加速Reactor线程对I/O请求的响应。
但在目前的单线程Reactor模式中,不仅I/O操作在该Reactor线程上,连非I/O的业务操作也在该线程上进行处理了,这可能会大大延迟I/O请求的响应。所以我们应该将非I/O的业务逻辑操作从Reactor线程上卸载,以此来加速Reactor线程对I/O请求的响应。
图
子主题
Reactor模式-工作者线程池模式
与单线程模式不同的是,添加了一个工作者线程池,并将非I/O操作从Reactor线程中移出转交给工作者线程池(Thread Pool)来执行。这样能够提高Reactor线程的I/O响应,不至于因为一些耗时的业务逻辑而延迟对后面I/O请求的处理。
在工作者线程池模式中,虽然非I/O操作交给了线程池来处理,但是所有的I/O操作依然由Reactor单线程执行,在高负载、高并发或大数据量的应用场景,依然较容易成为瓶颈。所以,对于Reactor的优化,又产生出下面的多线程模式。
图
子主题
Reactor模式-多线程模式
对于多个CPU的机器,为充分利用系统资源,将Reactor拆分为两部分:mainReactor和subReactor
mainReactor负责监听server socket,用来处理网络新连接的建立,将建立的socketChannel指定注册给subReactor,通常一个线程就可以处理 ;
subReactor维护自己的selector, 基于mainReactor 注册的socketChannel多路分离I/O读写事件,读写网络数据,通常使用多线程;
对非I/O的操作,依然转交给工作者线程池(Thread Pool)执行。
此种模型中,每个模块的工作更加专一,耦合度更低,性能和稳定性也大量的提升,支持的可并发客户端数量可达到上百万级别。关于此种模型的应用,目前有很多优秀的框架已经在应用了,比如mina和netty 等。Reactor模式-多线程模式下去掉工作者线程池(Thread Pool),则是Netty中NIO的默认模式。
mainReactor对应Netty中配置的BossGroup线程组,主要负责接受客户端连接的建立。一般只暴露一个服务端口,BossGroup线程组一般一个线程工作即可
subReactor对应Netty中配置的WorkerGroup线程组,BossGroup线程组接受并建立完客户端的连接后,将网络socket转交给WorkerGroup线程组,然后在WorkerGroup线程组内选择一个线程,进行I/O的处理。WorkerGroup线程组主要处理I/O,一般设置2*CPU核数个线程
subReactor维护自己的selector, 基于mainReactor 注册的socketChannel多路分离I/O读写事件,读写网络数据,通常使用多线程;
对非I/O的操作,依然转交给工作者线程池(Thread Pool)执行。
此种模型中,每个模块的工作更加专一,耦合度更低,性能和稳定性也大量的提升,支持的可并发客户端数量可达到上百万级别。关于此种模型的应用,目前有很多优秀的框架已经在应用了,比如mina和netty 等。Reactor模式-多线程模式下去掉工作者线程池(Thread Pool),则是Netty中NIO的默认模式。
subReactor对应Netty中配置的WorkerGroup线程组,BossGroup线程组接受并建立完客户端的连接后,将网络socket转交给WorkerGroup线程组,然后在WorkerGroup线程组内选择一个线程,进行I/O的处理。WorkerGroup线程组主要处理I/O,一般设置2*CPU核数个线程
图
子主题
Proactor模式
定义
流程与Reactor模式类似,区别在于proactor在IO ready事件触发后,完成IO操作再通知应用回调。虽然在linux平台还是基于epoll/select,但是内部实现了异步操作处理器(Asynchronous Operation Processor)以及异步事件分离器(Asynchronous Event Demultiplexer)将IO操作与应用回调隔离。经典应用例如boost asio异步IO库的结构和流程图如下:
工作方式&原理
图1
子主题
图2
子主题
Reactor模式中,用户线程通过向Reactor对象注册感兴趣的事件监听,然后事件触发时调用事件处理函数。而Proactor模式中,用户线程将AsynchronousOperation(读/写等)、Proactor以及操作完成时的CompletionHandler注册到AsynchronousOperationProcessor。
AsynchronousOperationProcessor使用Facade模式提供了一组异步操作API(读/写等)供用户使用,当用户线程调用异步API后,便继续执行自己的任务。AsynchronousOperationProcessor 会开启独立的内核线程执行异步操作,实现真正的异步。当异步IO操作完成时,AsynchronousOperationProcessor将用户线程与AsynchronousOperation一起注册的Proactor和CompletionHandler取出,然后将CompletionHandler与IO操作的结果数据一起转发给Proactor,Proactor负责回调每一个异步操作的事件完成处理函数handle_event。虽然Proactor模式中每个异步操作都可以绑定一个Proactor对象,但是一般在操作系统中,Proactor被实现为Singleton模式,以便于集中化分发操作完成事件。
图3
再再直观一点,其实就回到了五大模型-异步I/O模型的流程,就是下面这幅图:
子主题
Reactor模式和Proactor模式的总结对比
主动和被动
以主动写为例
Reactor
Reactor将handler放到select(),等待可写就绪,然后调用write()写入数据;写完数据后再处理后续逻辑;
Reactor模式是一种被动的处理,即有事件发生时被动处理。
Proactor
Proactor调用aoi_write后立刻返回,由内核负责写操作,写完后调用相应的回调函数处理后续逻辑
而Proator模式则是主动发起异步调用,然后循环检测完成事件。
实现
Reactor
Reactor实现了一个被动的事件分离和分发模型,服务等待请求事件的到来,再通过不受间断的同步处理事件,从而做出反应;
Proactor
Proactor实现了一个主动的事件分离和分发模型;这种设计允许多个任务并发的执行,从而提高吞吐量。
总结
涉及到文件I/O或耗时I/O可以使用Proactor模式,或使用多线程模拟实现异步I/O的方式。
优缺点
优点
Reactor
Reactor实现相对简单,对于链接多,但耗时短的处理场景高效;
操作系统可以在多个事件源上等待,并且避免了线程切换的性能开销和编程复杂性;
事件的串行化对应用是透明的,可以顺序的同步执行而不需要加锁;
事务分离:将与应用无关的多路复用、分配机制和与应用相关的回调函数分离开来。
Proactor
Proactor在理论上性能更高,能够处理耗时长的并发场景。为什么说在理论上?请自行搜索Netty 5.X版本废弃的原因。
缺点
Reactor
Reactor处理耗时长的操作会造成事件分发的阻塞,影响到后续事件的处理;
Proactor
Proactor实现逻辑复杂;依赖操作系统对异步的支持,目前实现了纯异步操作的操作系统少,实现优秀的如windows IOCP,但由于其windows系统用于服务器的局限性,目前应用范围较小;而Unix/Linux系统对纯异步的支持有限,应用事件驱动的主流还是通过select/epoll来实现。
适用场景
Reactor
同时接收多个服务请求,并且依次同步的处理它们的事件驱动程序;
Proactor
异步接收和同时处理多个服务请求的事件驱动程序。
UDP
如何把应用从单机扩展到分布式
分布式系统的难点
缺乏全局时钟
分布式系统中每个节点的时钟完全一致很难做到。
很多时候使用时钟,只需要取药先后顺序,而不是一定需要准确的时间。
这种时候,有一个单独的集群来做动作顺序的区分。
子主题
面对故障独立性
单机
如果是机器问题,OS问题或者程序自身的问题,基本程序整体不可用
分布式
整个系统的一部分有问题二其他部分是正常是经常出现的问题,称之为故障独立性。
应对和解决故障独立性的办法。
处理单点故障
某个角色和功能只有单机在支撑,称之为SPoF(Single Point of Failure)
如何处理?
单机变为集群
给单点做备份,能够在出现问题时进行恢复,并且尽量做到自动恢复,降低恢复需要的时间
降低单点故障的影响范围。
如服务拆分、数据拆分等降低故障影响范围,但是增加了故障出现的次数和时间,这种方式更多的是转移和交换,并没有真正解决问题帮助解决单点故障。
事务的挑战
事务的特性
ACID
分布式事务方案
两阶段提交(2PC)
最终一致
BASE
CAP
Paxos
Raft
ZAB
Gossip
2、大型网站及其架构演进过程
什么是大型网站
访问量
数据量
大型网站的架构演进
用Java技术和单机来构建的网站
技术单机构建的网站
从一个单机的交易网站说起
基于java技术单机构建的交易网站
单机负载告警,数据库和应用分离
应用和数据库分开的结构
应用服务器负载告警,如何让应用服务器走向集群
应用服务器集群
引入负载均衡社保的结构
解决应用服务器变为集群后的session问题
session
负载均衡、应用集群与session
方式一、session sticky
打个比方,web服务器使我们每次吃饭的发你单,会话数据就是吃饭用的碗筷。
要保证每次吃饭都用自己的碗筷,就要把餐具存到某一家,
并且每一个都去这家店吃饭,是个不错的主意。
打个比方,web服务器使我们每次吃饭的发你单,会话数据就是吃饭用的碗筷。
要保证每次吃饭都用自己的碗筷,就要把餐具存到某一家,
并且每一个都去这家店吃饭,是个不错的主意。
缺陷
一台服务宕机,这台机器会员数据丢失,用户需要重新登陆
会员标识是应用层的信息,同一个会员被负载均衡都保存到一个web服务器上,
就需要第七层的应用层的计息,这个解析比第四层的交换要打
就需要第七层的应用层的计息,这个解析比第四层的交换要打
负载均衡变成一个有状态的节点,要将会话保存到具体的web服务器的映射。
和无状态的节点相比,内存消耗更大,容灾方面更麻烦
和无状态的节点相比,内存消耗更大,容灾方面更麻烦
方式二、session replication
每次都去同一家饭店吃饭很快就吃腻了。
除了前面的每次是同一家饭店吃饭的方式外,我们还可以在每个店里
都存放一套自己的餐具,不就可以更加自由的选择饭点了吗?
每次都去同一家饭店吃饭很快就吃腻了。
除了前面的每次是同一家饭店吃饭的方式外,我们还可以在每个店里
都存放一套自己的餐具,不就可以更加自由的选择饭点了吗?
缺陷
同步session数据曹成了网络带宽的开销。
只要session数据有变化,就需要将数据同步到所有其他机器上,机器数越多,同步带来的网络带宽开销就越大。
只要session数据有变化,就需要将数据同步到所有其他机器上,机器数越多,同步带来的网络带宽开销就越大。
每台web服务器都要保存所有的session数据,如果整个集群的session数很多的话,
每台机器用于保存的session数据的内容占用会很严重。
每台机器用于保存的session数据的内容占用会很严重。
总结
该方案是应用容器来完成的,应用本身不关系这个事情。
不适合多集群机器数多的场景。如果只有几台机器,是可以的。
方式三、session数据集中存储
除了方式二的粗暴复制方式以外,还可以把session数据集中存储,只有一份数据,
然后不同的web服务器从同样的地方来获取session即可
除了方式二的粗暴复制方式以外,还可以把session数据集中存储,只有一份数据,
然后不同的web服务器从同样的地方来获取session即可
缺陷
读写session数据引入了网络操作,这相对于本机的数据读取来说,问题就在于存在时延和不稳定性,
不过我们的通信基本都发生在内网,问题不大。
不过我们的通信基本都发生在内网,问题不大。
如果集中存储session的机器或者集群有问题,就会影响我们的应用。
总结
相对于session replication,当web服务器数量比较大时,session数据比较多时,集中存储的优势非常明显了
方案四、cookie based
将session数据存在cookie中,然后再web服务器上从cookie中生成对应的session数据。
这就好比将自己的碗筷带在身上,这样我去哪家饭店吃饭就随意了。
将session数据存在cookie中,然后再web服务器上从cookie中生成对应的session数据。
这就好比将自己的碗筷带在身上,这样我去哪家饭店吃饭就随意了。
缺陷
cookie长度有限制。
cookie长度是有限制的,这回限制session数据的长度。
cookie长度是有限制的,这回限制session数据的长度。
安全性。
session数据本来都是服务端数据么该方案是让这些服务端数据到了外部网络以及客户端。
因此存在安全性问题。
虽然我们可以对写入cookie的session数据做加密,不过对于安全来说,物理上不能接触才是安全的。
session数据本来都是服务端数据么该方案是让这些服务端数据到了外部网络以及客户端。
因此存在安全性问题。
虽然我们可以对写入cookie的session数据做加密,不过对于安全来说,物理上不能接触才是安全的。
带宽消耗。
这里指的不是内部web服务器之间的带宽消耗,而是我们数据中心的整体外部带宽的消耗。
这里指的不是内部web服务器之间的带宽消耗,而是我们数据中心的整体外部带宽的消耗。
性能影响。
每次http请求和响应都带有session数据,对web服务器来说,在同样的处理情况下,响应的结果数据越小,
支持的并发请求就会越多。
每次http请求和响应都带有session数据,对web服务器来说,在同样的处理情况下,响应的结果数据越小,
支持的并发请求就会越多。
总结
不依赖外部的存储系统,也就不存在从外部系统获取,写入session数据的网络时延和不稳定性了
数据读写压力大,读写分离吧
加入读库后的架构
架构存在的问题
数据复制问题
应用对于数据源的选择问题
搜索引擎其实是一个读库
弥补关系型数据库的不足,引入分布式存储系统
什么情况要引入分布式存储系统?
分布式存储系统通过集群提供了一个高容量、高并发访问、数据冗余容灾的支持。
分布式存储系统有哪些?
分布式文件系统
解决小文件和大文件的存储问题
分布式key-value系统
提高高性能的半结构化的支持
分布式数据库
提供一个支持特大数据、高并发的数据库系统
引入分布式存储系统的结构
子主题
读写分离后,数据库又遇到瓶颈
读写分离带来的问题?
商品、交易、用户全在一个数据库中,主库遇到瓶颈
如何解决主库的瓶颈?
专库专用,数据垂直拆分
什么是数据垂直拆分?
就是把数据库中不同的业务数据拆分到不同的数据库中。
数据垂直拆分后的结构
子主题
垂直拆分带给我们的影响是什么?
应用需要配置多个数据源
坏处是增加了所需的配置
好处是每个数据库连接池的隔离。
不同业务的数据从原来的一个数据库拆分到多个数据库中
坏处
坏处是需要考虑如何处理原来单机中跨业务的事务。
解决办法
一种是使用分布式事务,但是其性能要明显低于之前的单机事务。
另一种是去掉事务或者不去追求强事务支持,但是坏处是这原来在单库中的可以使用的表关联的查询也就需要改变实现了。
好处
解决了把所有业务数据放在一个数据库中的压力问题。
并可以根据不同业务的特点进行更多优化。
垂直拆分后的单机遇到瓶颈
数据水平拆分
什么是水平拆分?
为什么垂直拆分后还要水平拆分?
水平拆分和读写分离的区别?
读写分离解决的是读压力大的问题,对于数据量大或者更新量大的情况病不起作用。
水平拆分和垂直拆分的区别?
垂直拆分是把不同的表拆到不同的数据库中。
水平拆分是把同一个表拆到不同的数据库中。
数据水平拆分带给我们的影响是什么?
认识消息中间件
3、构建java中间件
Java中间件的定义
定义
中间件不是最上层的应用,也不是底层的支撑系统。中间件起到桥梁的作用,是应用于应用的桥梁,是应用于服务的桥梁。
领域
远程过程调用和对象访问中间件
主要解决分布式环境下应用的互相访问问题,这也是支撑我们介绍应用服务化的基础。
消息中间件。
主要解决应用之间的消息传递,
数据访问中间件
主要解决应用访问数据库的共性问题的组件
构建Java中间件运行环境--JVM
跨平台的Java运行环境--JVM
Java虚拟机规范
Java虚拟机
Oracle Hotspot
垃圾回收与内存堆布局
内存布局
Oracle Hotspot
Young(新生代)
Eden
Survivor 1
Survivor 2
Tenured(年老代)
Perm(持久代)
jvm堆内存分布
JVM堆分布图
子主题
子主题
参数解释
-Xss:规定了每个线程虚拟机栈及堆栈的大小,一般情况下,256k是足够的,此配置将会影响此进程中并发线程数的大小。
-Xms:表示初始化JAVA堆的大小及该进程刚创建出来的时候,他的专属JAVA堆的大小,一旦对象容量超过了JAVA堆的初始容量,JAVA堆将会自动扩容到-Xmx大小。
-Xmx:表示java堆可以扩展到的最大值,在很多情况下,通常将-Xms和-Xmx设置成一样的,因为当堆不够用而发生扩容时,会发生内存抖动影响程序运行时的稳定性。
-Xms:表示初始化JAVA堆的大小及该进程刚创建出来的时候,他的专属JAVA堆的大小,一旦对象容量超过了JAVA堆的初始容量,JAVA堆将会自动扩容到-Xmx大小。
-Xmx:表示java堆可以扩展到的最大值,在很多情况下,通常将-Xms和-Xmx设置成一样的,因为当堆不够用而发生扩容时,会发生内存抖动影响程序运行时的稳定性。
内存回收算法
YGC
YGC有哪些算法
YGC算法(Minor GC)流程
子主题
此时如果新生的对象无法在 Eden 区创建(Eden 区无法容纳) 就会触发一次Young GC 此时会将 S0 区与Eden 区的对象一起进行可达性分析,找出活跃的对象,将它复制到 S1 区并且将S0区域和 Eden 区的对象给清空,这样那些不可达的对象进行清除,并且将S0 区 和 S1区交换。
但是这里会产生一个问题,Q:为啥会有两个 Survivor 区?
A: 因为假设设想一下只有一个 Survibor 区 那么就无法实现对于 S0 区的垃圾收集,以及分代年龄的提升。
A: 因为假设设想一下只有一个 Survibor 区 那么就无法实现对于 S0 区的垃圾收集,以及分代年龄的提升。
full GC
full GC有哪些算法
full GC算法流程
既然我们已经知道了 Minor GC 是在 Eden 区快满的情况下才会触发
Q:那么 Major GC 呢?
Q:那么 Major GC 呢?
1. 对于一个大对象,我们会首先在Eden 尝试创建,如果创建不了,就会触发Minor GC
2. 随后继续尝试在Eden区存放,发现仍然放不下
3. 尝试直接进入老年代,老年代也放不下
4. 触发 Major GC 清理老年代的空间
5. 放的下 成功
6. 放不下 OOM
子主题
子主题
注意::::发生在老年代的GC ,基本上发生了一次Major GC 就会发生一次 Minor GC。并且Major GC 的速度往往会比 Minor GC 慢 10 倍。
参见垃圾回收章节 https://www.processon.com/mindmap/613db80ee401fd7aedfddd7e
Java并发编程的类,接口和方法
线程池
线程池复用线程
固定大小线程池
定义
核心参数
优缺点
定时线程池
线程没有固定大小线程池
锁
java面试题之synchronized和lock有什么区别? https://www.cnblogs.com/hujinshui/p/9990173.html
存在层次
synchronize
java的关键字,在jvm层面上
Lock
是一个类
锁的释放
synchronize
1、以获取锁的线程执行完同步代码,释放锁
2、线程执行发生异常,jvm会让线程释放锁
2、线程执行发生异常,jvm会让线程释放锁
Lock
在finally中必须释放锁,不然容易造成线程死锁
锁的获取
synchronize
假设A线程获得锁,B线程等待,
如果A线程阻塞,B线程会一直等待
如果A线程阻塞,B线程会一直等待
Lock
分情况而定,lock有多个锁获取的方法,可以尝试获得锁,
线程可以不用功一直等待
线程可以不用功一直等待
锁状态
synchronize
无法判断
Lock
可以判断
性能
synchronize
少量同步
Lock
大量同步
java面试-synchronized与lock有什么区别? https://www.cnblogs.com/wjh123/p/11129548.html
原始构成
synchronized
synchronized是关键字,属于JVM层面,底层是由一对monitorenter和monitorexit指令实现的。
Lock
ReentrantLock是一个具体类,是API层面的锁。
使用方法:
synchronized
synchronized不需要用户手动释放锁,当synchronized代码块执行完成后,系统会自动让线程释放对锁的占用
Lock
ReentrantLock需要用户手动释放锁,若没有手动释放可能导致死锁现象。
等待是否可中断:
synchronized
synchronized不可中断,除非抛出异常或者正常运行完成
Lock
ReentrantLock可中断
加锁是否公平
synchronized
synchronized非公平锁
Lock
ReentrantLock两者都可以,默认是非公平锁。
tryLock方法
如果锁被其他线程持有,那么tryLock会立即返回
公平锁
优点
等待锁的线程不会饿死,但是整体效率会高一些
缺点
非公平锁
优点
整体效率会高一点,但是有些线程可能会饿死或者说很早就在等待锁,但是要等很久才会得到锁
缺点
锁绑定多个条件Condition
synchronized
synchronized没有。
Lock
ReentrantLock可用来分组唤醒需要唤醒的线程。而不是像synchronized要么随机唤醒一个线程,要么唤醒所有线程。
题目:多线程之间按顺序调用,实现A->B->C三个线程启动,要求如下:
* AA打印5次,BB打印10次,CC打印15次
* 紧接着
* AA打印5次,BB打印10次,CC打印15次
* 来10轮
* AA打印5次,BB打印10次,CC打印15次
* 紧接着
* AA打印5次,BB打印10次,CC打印15次
* 来10轮
package com.yonghui.galaxy.metadata.juc;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
*java面试-synchronized与lock有什么区别?
*
* https://www.cnblogs.com/wjh123/p/11129548.html
*
*
* 1、原始构成:
synchronized是关键字,属于JVM层面,底层是由一对monitorenter和monitorexit指令实现的。
ReentrantLock是一个具体类,是API层面的锁。
2、使用方法:
synchronized不需要用户手动释放锁,当synchronized代码块执行完成后,系统会自动让线程释放对锁的占用
ReentrantLock需要用户手动释放锁,若没有手动释放可能导致死锁现象。
3、等待是否可中断:
synchronized不可中断,除非抛出异常或者正常运行完成
ReentrantLock可中断
4、加锁是否公平:
synchronized非公平锁
ReentrantLock两者都可以,默认是非公平锁。
5、锁绑定多个条件Condition:
synchronized没有。
ReentrantLock可用来分组唤醒需要唤醒的线程。而不是像synchronized要么随机唤醒一个线程,要么唤醒所有线程。
题目:多线程之间按顺序调用,实现A->B->C三个线程启动,要求如下:
* AA打印5次,BB打印10次,CC打印15次
* 紧接着
* AA打印5次,BB打印10次,CC打印15次
* 来10轮
*/
public class TestLocknewCondition {
public static void main(String[] args) {
ShareResource shareResource = new ShareResource();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
shareResource.print5();
}
}, "AAA").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
shareResource.print10();
}
}, "BBB").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
shareResource.print15();
}
}, "CCC").start();
}
}
class ShareResource {
private int number = 1;
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
public void print5() {
lock.lock();
try {
while (number != 1) {
condition1.await();
}
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
number = 2;
// 唤醒等待condition2的线程
condition2.signal();// signal: 信号 ,指示
/**
* FIXME java.util.concurrent.locks.Condition#signal()
*唤醒一个正在等待的线程。
*
如果有任何线程在此条件下等待,则会选择一个线程进行唤醒。然后,该线程必须在从{@code wait}返回之前重新获取锁。
*
*实施注意事项</b>
*
*<p>调用此方法时,实现可能(并且通常确实)要求当前线程持有与此{@code Condition}关联的锁。您必须记录此先决条件以及未持有锁时所采取的任何操作。
通常,会引发{@link IllegalMonitorStateException}之类的异常。
*/
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print10() {
lock.lock();
try {
while (number != 2) {
condition2.await();
}
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
number = 3;
// 唤醒等待condition3的线程
condition3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print15() {
lock.lock();
try {
while (number != 3) {
condition3.await();
}
for (int i = 1; i <= 15; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
number = 1;
// 唤醒等待condition1的线程
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
*java面试-synchronized与lock有什么区别?
*
* https://www.cnblogs.com/wjh123/p/11129548.html
*
*
* 1、原始构成:
synchronized是关键字,属于JVM层面,底层是由一对monitorenter和monitorexit指令实现的。
ReentrantLock是一个具体类,是API层面的锁。
2、使用方法:
synchronized不需要用户手动释放锁,当synchronized代码块执行完成后,系统会自动让线程释放对锁的占用
ReentrantLock需要用户手动释放锁,若没有手动释放可能导致死锁现象。
3、等待是否可中断:
synchronized不可中断,除非抛出异常或者正常运行完成
ReentrantLock可中断
4、加锁是否公平:
synchronized非公平锁
ReentrantLock两者都可以,默认是非公平锁。
5、锁绑定多个条件Condition:
synchronized没有。
ReentrantLock可用来分组唤醒需要唤醒的线程。而不是像synchronized要么随机唤醒一个线程,要么唤醒所有线程。
题目:多线程之间按顺序调用,实现A->B->C三个线程启动,要求如下:
* AA打印5次,BB打印10次,CC打印15次
* 紧接着
* AA打印5次,BB打印10次,CC打印15次
* 来10轮
*/
public class TestLocknewCondition {
public static void main(String[] args) {
ShareResource shareResource = new ShareResource();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
shareResource.print5();
}
}, "AAA").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
shareResource.print10();
}
}, "BBB").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
shareResource.print15();
}
}, "CCC").start();
}
}
class ShareResource {
private int number = 1;
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
public void print5() {
lock.lock();
try {
while (number != 1) {
condition1.await();
}
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
number = 2;
// 唤醒等待condition2的线程
condition2.signal();// signal: 信号 ,指示
/**
* FIXME java.util.concurrent.locks.Condition#signal()
*唤醒一个正在等待的线程。
*
如果有任何线程在此条件下等待,则会选择一个线程进行唤醒。然后,该线程必须在从{@code wait}返回之前重新获取锁。
*
*实施注意事项</b>
*
*<p>调用此方法时,实现可能(并且通常确实)要求当前线程持有与此{@code Condition}关联的锁。您必须记录此先决条件以及未持有锁时所采取的任何操作。
通常,会引发{@link IllegalMonitorStateException}之类的异常。
*/
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print10() {
lock.lock();
try {
while (number != 2) {
condition2.await();
}
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
number = 3;
// 唤醒等待condition3的线程
condition3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print15() {
lock.lock();
try {
while (number != 3) {
condition3.await();
}
for (int i = 1; i <= 15; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
number = 1;
// 唤醒等待condition1的线程
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
可见性
volatile和synchronized区别面试题
https://blog.csdn.net/a347911/article/details/88379625
https://blog.csdn.net/a347911/article/details/88379625
使用关键字synchronized
Synchronized能够实现多线程的原子性(同步)和可见性。
JVM关于Synchronized的两条规定:
线程解锁前,必须把共享变量的最新值刷新到主内存中。
线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意:加锁和解锁需要同一把锁)。
Synchronized执行互斥代码的过程
获得互斥锁
清空工作内存
从主内存拷贝变量的最新副本到工作内存
执行代码
将更改后的共享变量的值刷新到主内存
释放互斥锁
使用关键字volatile
volatile可以保证变量的可见性,但是不能保证复合操作的原子性
volatile如何实现内存可见性?
深入来说:通过加入内存屏障和禁止重排序优化来实现的。
对volatile变量执行写操作时,会在写操作后加入一条store屏障指令。
对volatile变量执行读操作时,会在读操作后加入一条load屏障指令。
通俗地讲:volatile变量在每次被线程访问时,都强迫从主内存中重读该变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存,这样任何时刻,不同的线程总能看到该变量的最新值。
线程写volatile变量的过程:
写操作后加入一条store屏障指令。
写操作后加入一条store屏障指令。
改变线程工作内存中volatile变量副本的值。
将改变后的副本的值从工作内存刷新到主内存。
线程读volatile变量的过程:
会在读操作后加入一条load屏障指令。
会在读操作后加入一条load屏障指令。
从主内存中读取volatile变量的最新值到线程的工作内存中。
从工作内存中读取volatile变量的副本。
volatile不能保证volatile变量复合操作的原子性
对于下面的一段程序的使用volatile和synchronized
private int number = 0;
number++;//不是原子操作
1读取number的值
2将number的值加1
3写入最新的number的值
//加入synchronized,变为原子操作
synchronized(thhis){
number++;
}
//变为volatile变量,无法保证原子性
private volatile int number = 0;
————————————————
版权声明:本文为CSDN博主「我还是个少年」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/a347911/article/details/88379625
private int number = 0;
number++;//不是原子操作
1读取number的值
2将number的值加1
3写入最新的number的值
//加入synchronized,变为原子操作
synchronized(thhis){
number++;
}
//变为volatile变量,无法保证原子性
private volatile int number = 0;
————————————————
版权声明:本文为CSDN博主「我还是个少年」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/a347911/article/details/88379625
使用关键字volatile的适用场景
volatile变量可用于提供线程安全,但是只能应用于非常有限的一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。因此,单独使用 volatile 还不足以实现计数器、互斥锁或任何具有与多个变量相关的不变式(Invariants)的类(例如 “start <=end”)。
出于简易性或可伸缩性的考虑,您可能倾向于使用 volatile 变量而不是锁。当使用 volatile 变量而非锁时,某些习惯用法(idiom)更加易于编码和阅读。此外,volatile 变量不会像锁那样造成线程阻塞,因此也很少造成可伸缩性问题。在某些情况下,如果读操作远远大于写操作,volatile 变量还可以提供优于锁的性能优势。
————————————————
版权声明:本文为CSDN博主「我还是个少年」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/a347911/article/details/88379625
出于简易性或可伸缩性的考虑,您可能倾向于使用 volatile 变量而不是锁。当使用 volatile 变量而非锁时,某些习惯用法(idiom)更加易于编码和阅读。此外,volatile 变量不会像锁那样造成线程阻塞,因此也很少造成可伸缩性问题。在某些情况下,如果读操作远远大于写操作,volatile 变量还可以提供优于锁的性能优势。
————————————————
版权声明:本文为CSDN博主「我还是个少年」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/a347911/article/details/88379625
对变量的写入操作不依赖其当前值
第一个条件的限制使 volatile 变量不能用作线程安全计数器。虽然增量操作(x++)看上去类似一个单独操作,实际上它是一个由(读取-修改-写入)操作序列组成的组合操作,必须以原子方式执行,而 volatile 不能提供必须的原子特性。实现正确的操作需要使x 的值在操作期间保持不变,而 volatile 变量无法实现这点。(然而,如果只从单个线程写入,那么可以忽略第一个条件。)
————————————————
版权声明:本文为CSDN博主「我还是个少年」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/a347911/article/details/88379625
————————————————
版权声明:本文为CSDN博主「我还是个少年」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/a347911/article/details/88379625
该变量没有包含在具有其他变量的不变式中。
volatile比synchronized比较总结
volatile比synchronized更轻量级。
volatile没有synchronized使用的广泛。
volatile不需要加锁,比synchronized更轻量级,不会阻塞线程。
从内存可见性角度看,volatile读相当于加锁,volatile写相当于解锁。
synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性。
volatile本身不保证获取和设置操作的原子性,仅仅保持修改的可见性。但是java的内存模型保证声明为volatile的long和double变量的get和set操作是原子的。
volatile比synchronized的常见问题及解决方案
对64位(long、double)变量的读写可能不是原子操作
Java内存模型允许JVM将没有被volatile修饰的64位数据类型的读写操作划分为两次32位的读写操作来运行。
Java内存模型允许JVM将没有被volatile修饰的64位数据类型的读写操作划分为两次32位的读写操作来运行。
导致的问题
有可能会出现读取到半个变量的情况。
如何解决
加volatile关键字。
即使没有保证可见性的措施,很多时候共享变量依然能够在主内存和工作内存间得到及时的更新?
一般只有在短时间内高并发的情况下才会出现变量得不到及时更新的情况,因为CPU在执行时会很快地刷新缓存,所以一般情况下很难看到这种问题。慢了不就不会刷新了。CPU运算快的话,在分配的时间片内就能完成所有工作:工作内从1->主内存->工作内存2,这样一来就保证了数据的可见性。在这个过程中,假如线程没有在规定时间内完成工作,然后这个线程就释放CPU,分配给其它线程,该线程就需要等待CPU下次给该线程分配时间片,如果在这段时间内有别的线程访问共享变量,可见性就没法保证了。
————————————————
版权声明:本文为CSDN博主「我还是个少年」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/a347911/article/details/88379625
————————————————
版权声明:本文为CSDN博主「我还是个少年」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/a347911/article/details/88379625
定义
可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。
原子性
定义
原子是世界上的最小单位,具有不可分割性。synchronized块之间的操作就具备原子性。volatile关键字定义的变量就可以做到这一点,Java还有两个关键字能实现可见性,即synchronized和final。
Atomics
AtomicIntegerArray
AtomicStampedReference
Atomic原子类
基本类型
AtomicInteger
常用方法
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //加上给定的值,并返回之前的值
public final int addAndGet(int delta) //加上给定的值,并返回最终结果
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //加上给定的值,并返回之前的值
public final int addAndGet(int delta) //加上给定的值,并返回最终结果
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
@Test
public void AtomicIntegerT() {
AtomicInteger c = new AtomicInteger();
c.set(10);
System.out.println("初始设置的值 ==>" + c.get());
int andAdd = c.getAndAdd(10);
System.out.println("为原先的值加上10,并返回原先的值,原先的值是 ==> " + andAdd + "加上之后的值是 ==> " + c.get());
int finalVal = c.addAndGet(5);
System.out.println("加上5, 之后的值是 ==> " + finalVal);
int i = c.incrementAndGet();
System.out.println("++1,之后的值为 ==> " + i);
int result = c.updateAndGet(e -> e + 3);
System.out.println("可以使用函数式更新 + 3 计算后的结果为 ==> "+ result);
int res = c.accumulateAndGet(10, (x, y) -> x + y);
System.out.println("使用指定函数计算后的结果为 ==>" + res);
}
初始设置的值 ==>10
为原先的值加上10,并返回原先的值,原先的值是 ==> 10
加上之后的值是 ==> 20
加上5, 之后的值是 ==> 25
++1,之后的值为 ==> 26
可以使用函数式更新 + 3 计算后的结果为 ==> 29
使用指定函数计算后的结果为 ==>39
public void AtomicIntegerT() {
AtomicInteger c = new AtomicInteger();
c.set(10);
System.out.println("初始设置的值 ==>" + c.get());
int andAdd = c.getAndAdd(10);
System.out.println("为原先的值加上10,并返回原先的值,原先的值是 ==> " + andAdd + "加上之后的值是 ==> " + c.get());
int finalVal = c.addAndGet(5);
System.out.println("加上5, 之后的值是 ==> " + finalVal);
int i = c.incrementAndGet();
System.out.println("++1,之后的值为 ==> " + i);
int result = c.updateAndGet(e -> e + 3);
System.out.println("可以使用函数式更新 + 3 计算后的结果为 ==> "+ result);
int res = c.accumulateAndGet(10, (x, y) -> x + y);
System.out.println("使用指定函数计算后的结果为 ==>" + res);
}
初始设置的值 ==>10
为原先的值加上10,并返回原先的值,原先的值是 ==> 10
加上之后的值是 ==> 20
加上5, 之后的值是 ==> 25
++1,之后的值为 ==> 26
可以使用函数式更新 + 3 计算后的结果为 ==> 29
使用指定函数计算后的结果为 ==>39
getAndIncrement()方法的实现
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
//objectFieldOffset本地方法,用来拿到“原来的值”的内存地址。
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//value在内存中可见,JVM可以保证任何时刻任何线程总能拿到该变量的最新值
private volatile int value;
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
//openjdk1.8Unsafe类的源码:Unsafe.java
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
public native int getIntVolatile(Object var1, long var2);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
//objectFieldOffset本地方法,用来拿到“原来的值”的内存地址。
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//value在内存中可见,JVM可以保证任何时刻任何线程总能拿到该变量的最新值
private volatile int value;
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
//openjdk1.8Unsafe类的源码:Unsafe.java
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
public native int getIntVolatile(Object var1, long var2);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
Java的源码改动是有的,《Java并发编程的艺术》的内容也在此摘录一下,相对来说更好理解一些:
public final int getAddIncrement() {
for ( ; ; ) {
//先取得存储的值
int current = get();
//加1操作
int next = current + 1;
// CAS保证原子更新操作,如果输入的数值等于预期值,将值设置为输入的值
if (compareAndSet(current, next)) {
return current;
}
}
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
public final int getAddIncrement() {
for ( ; ; ) {
//先取得存储的值
int current = get();
//加1操作
int next = current + 1;
// CAS保证原子更新操作,如果输入的数值等于预期值,将值设置为输入的值
if (compareAndSet(current, next)) {
return current;
}
}
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
并发编程:并发操作原子类Atomic以及CAS的ABA问题
https://zhuanlan.zhihu.com/p/259624313
https://zhuanlan.zhihu.com/p/259624313
AtomicBoolean
AtomicLong
数组类型
AtomicIntegerArray:整型数组原子类
AtomicLongArray:长整型数组原子类
AtomicReferenceArray :引用类型数组原子类
AtomicLongArray:长整型数组原子类
AtomicReferenceArray :引用类型数组原子类
AtomicIntegerArray的常用方法
@Test
public void AtomicIntegerArrayT() {
int[] nums = {1, 2, 3, 4, 5};
AtomicIntegerArray c = new AtomicIntegerArray(nums);
for (int i = 0; i < nums.length; i++) {
System.out.print(c.get(i) + " ");
} System.out.println();
int finalVal = c.addAndGet(0, 10);
System.out.println("索引为 0 的值 加上 10 ==> " + finalVal);
int i = c.incrementAndGet(0);
System.out.println("索引为 0 的值 ++1,之后的值为 ==> " + i);
int result = c.updateAndGet(0, e -> e + 3);
System.out.println("可以使用函数式更新索引为0 的位置 + 3 计算后的结果为 ==> " + result);
int res = c.accumulateAndGet(0, 10, (x, y) -> x * y);
System.out.println("使用指定函数计算后的结果为 ==> " + res);
}
public void AtomicIntegerArrayT() {
int[] nums = {1, 2, 3, 4, 5};
AtomicIntegerArray c = new AtomicIntegerArray(nums);
for (int i = 0; i < nums.length; i++) {
System.out.print(c.get(i) + " ");
} System.out.println();
int finalVal = c.addAndGet(0, 10);
System.out.println("索引为 0 的值 加上 10 ==> " + finalVal);
int i = c.incrementAndGet(0);
System.out.println("索引为 0 的值 ++1,之后的值为 ==> " + i);
int result = c.updateAndGet(0, e -> e + 3);
System.out.println("可以使用函数式更新索引为0 的位置 + 3 计算后的结果为 ==> " + result);
int res = c.accumulateAndGet(0, 10, (x, y) -> x * y);
System.out.println("使用指定函数计算后的结果为 ==> " + res);
}
引用类型
基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用 引用类型原子类。
AtomicReference:引用类型原子类
AtomicMarkableReference:原子更新带有标记的引用类型,无法解决ABA问题,该类的标记更多用于表示引用值是否已逻辑删除。
AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
AtomicMarkableReference:原子更新带有标记的引用类型,无法解决ABA问题,该类的标记更多用于表示引用值是否已逻辑删除。
AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
AtomicReference常见方法的使用
@Test
public void AtomicReferenceT(){
AtomicReference<Person> ar = new AtomicReference<>();
Person p = new Person(18,"summer");
ar.set(p);
Person pp = new Person(50,"dan");
ar.compareAndSet(p, pp);// except = p update = pp
System.out.println(ar.get().getName());
System.out.println(ar.get().getAge());
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class Person{
int age;
String name;
}//dan
//50
public void AtomicReferenceT(){
AtomicReference<Person> ar = new AtomicReference<>();
Person p = new Person(18,"summer");
ar.set(p);
Person pp = new Person(50,"dan");
ar.compareAndSet(p, pp);// except = p update = pp
System.out.println(ar.get().getName());
System.out.println(ar.get().getAge());
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class Person{
int age;
String name;
}//dan
//50
对象的属性修改类型
如果需要原子更新某个类里的某个字段时,需要用到对象的属性修改类型原子类。
AtomicIntegerFieldUpdater:原子更新整型字段的更新器
AtomicLongFieldUpdater:原子更新长整型字段的更新器
AtomicReferenceFieldUpdater:原子更新引用类型里的字段
AtomicLongFieldUpdater:原子更新长整型字段的更新器
AtomicReferenceFieldUpdater:原子更新引用类型里的字段
要想原子地更新对象的属性需要两步。
因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。
更新的对象属性必须使用 public volatile 修饰符。
因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。
更新的对象属性必须使用 public volatile 修饰符。
AtomicIntegerFieldUpdater常用方法的使用
@Test
public void AtomicIntegerFieldUpdateTest(){
AtomicIntegerFieldUpdater<Person> a = AtomicIntegerFieldUpdater.newUpdater(Person.class,"age");
Person p = new Person(18,"summer");
System.out.println(a.getAndIncrement(p)); //18
System.out.println(a.get(p)); //19
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class Person{
public volatile int age;
private String name;
}
public void AtomicIntegerFieldUpdateTest(){
AtomicIntegerFieldUpdater<Person> a = AtomicIntegerFieldUpdater.newUpdater(Person.class,"age");
Person p = new Person(18,"summer");
System.out.println(a.getAndIncrement(p)); //18
System.out.println(a.get(p)); //19
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class Person{
public volatile int age;
private String name;
}
Java8新增的原子操作类
LongAdder
解决多线程下AtomicLong的性能问题
由于AtomicLong通过CAS提供非阻塞的原子性操作,性能已经很好,在高并发下大量线程竞争更新同一个原子量,但只有一个线程能够更新成功,这就造成大量的CPU资源浪费。
LongAdder 通过让多个线程去竞争多个Cell资源,来解决,在很高的并发情况下,线程操作的是Cell数组,并不是base,在cell元素不足时进行2倍扩容,在高并发下性能高于AtomicLong
LongAdder 通过让多个线程去竞争多个Cell资源,来解决,在很高的并发情况下,线程操作的是Cell数组,并不是base,在cell元素不足时进行2倍扩容,在高并发下性能高于AtomicLong
经典问题
CAS的ABA问题的产生
问题的定义
假设两个线程访问同一变量x。
第一个线程获取到了变量x的值A,然后执行自己的逻辑。
这段时间内,第二个线程也取到了变量x的值A,然后将变量x的值改为B,然后执行自己的逻辑,最后又把变量x的值变为A【还原】。
在这之后,第一个线程终于进行了变量x的操作,但此时变量x的值还是A,因为x的值没有变化,所以compareAndSet还是会成功执行。
先来看一个值变量产生的ABA问题,理解一下ABA问题产生的流程:
@SneakyThrows
@Testpublic void test1() { AtomicInteger atomicInteger = new AtomicInteger(10);
CountDownLatch countDownLatch = new CountDownLatch(2);
new Thread(() -> {
atomicInteger.compareAndSet(10, 11);
atomicInteger.compareAndSet(11,10);
System.out.println(Thread.currentThread().getName() + ":10->11->10");
countDownLatch.countDown(); }).start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
boolean isSuccess = atomicInteger.compareAndSet(10,12);
System.out.println("设置是否成功:" + isSuccess + ",设置的新值:" + atomicInteger.get());
} catch (InterruptedException e) {
e.printStackTrace(); }
countDownLatch.countDown(); }).start();
countDownLatch.await();
}//输出:线程2并没有发现初始值已经被修改
//Thread-0:10->11->10
//设置是否成功:true,设置的新值:12
第一个线程获取到了变量x的值A,然后执行自己的逻辑。
这段时间内,第二个线程也取到了变量x的值A,然后将变量x的值改为B,然后执行自己的逻辑,最后又把变量x的值变为A【还原】。
在这之后,第一个线程终于进行了变量x的操作,但此时变量x的值还是A,因为x的值没有变化,所以compareAndSet还是会成功执行。
先来看一个值变量产生的ABA问题,理解一下ABA问题产生的流程:
@SneakyThrows
@Testpublic void test1() { AtomicInteger atomicInteger = new AtomicInteger(10);
CountDownLatch countDownLatch = new CountDownLatch(2);
new Thread(() -> {
atomicInteger.compareAndSet(10, 11);
atomicInteger.compareAndSet(11,10);
System.out.println(Thread.currentThread().getName() + ":10->11->10");
countDownLatch.countDown(); }).start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
boolean isSuccess = atomicInteger.compareAndSet(10,12);
System.out.println("设置是否成功:" + isSuccess + ",设置的新值:" + atomicInteger.get());
} catch (InterruptedException e) {
e.printStackTrace(); }
countDownLatch.countDown(); }).start();
countDownLatch.await();
}//输出:线程2并没有发现初始值已经被修改
//Thread-0:10->11->10
//设置是否成功:true,设置的新值:12
ABA问题存在,但可能对值变量并不会造成结果上的影响,但是考虑一种特殊的情况:
线程1和线程2并发访问ConcurrentStack。
线程1执行出栈【预期结果是弹出B,A成为栈顶】,但在读取栈顶B之后,被线程2抢占。
线程2记录栈顶B,依次弹出B和A,再依次将C,D,B入栈,且保证B就是原栈顶记录的B。
之后轮到线程1,发现栈顶确实是期望的B,虽弹出B,但此时栈顶已经是D,就出现了错误。
线程1执行出栈【预期结果是弹出B,A成为栈顶】,但在读取栈顶B之后,被线程2抢占。
线程2记录栈顶B,依次弹出B和A,再依次将C,D,B入栈,且保证B就是原栈顶记录的B。
之后轮到线程1,发现栈顶确实是期望的B,虽弹出B,但此时栈顶已经是D,就出现了错误。
ABA的问题如何解决
AtomicStampedReference 原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
@SneakyThrows
@Testpublic void test2() {
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference(10,1);
CountDownLatch countDownLatch = new CountDownLatch(2);
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 第一次版本:" + atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(10, 11, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + " 第二次版本:" + atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(11, 10, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + " 第三次版本:" + atomicStampedReference.getStamp());
countDownLatch.countDown(); }).start(); new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 第一次版本:" + atomicStampedReference.getStamp());
try {
TimeUnit.SECONDS.sleep(2);
boolean isSuccess = atomicStampedReference.compareAndSet(10,12, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + " 修改是否成功:" + isSuccess + " 当前版本:" + atomicStampedReference.getStamp() + " 当前值:" + atomicStampedReference.getReference());
countDownLatch.countDown();
} catch (InterruptedException e) {
e.printStackTrace(); } }).start();
countDownLatch.await();
}//输出
//输出
Thread-0 第一次版本:1
Thread-0 第二次版本:2
Thread-0 第三次版本:3
Thread-1 第一次版本:3
Thread-1 修改是否成功:true 当前版本:4 当前值:12
@Testpublic void test2() {
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference(10,1);
CountDownLatch countDownLatch = new CountDownLatch(2);
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 第一次版本:" + atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(10, 11, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + " 第二次版本:" + atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(11, 10, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + " 第三次版本:" + atomicStampedReference.getStamp());
countDownLatch.countDown(); }).start(); new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 第一次版本:" + atomicStampedReference.getStamp());
try {
TimeUnit.SECONDS.sleep(2);
boolean isSuccess = atomicStampedReference.compareAndSet(10,12, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + " 修改是否成功:" + isSuccess + " 当前版本:" + atomicStampedReference.getStamp() + " 当前值:" + atomicStampedReference.getReference());
countDownLatch.countDown();
} catch (InterruptedException e) {
e.printStackTrace(); } }).start();
countDownLatch.await();
}//输出
//输出
Thread-0 第一次版本:1
Thread-0 第二次版本:2
Thread-0 第三次版本:3
Thread-1 第一次版本:3
Thread-1 修改是否成功:true 当前版本:4 当前值:12
而AtomicMarkableReference 通过标志位,标志位只有true和false,每次更新标志位的话,在第三次的时候,又会变得跟第一次一样,并不能解决ABA问题。
有序性
如果在本线程内观察,所有的操作都是有序的:如果在一个线程中观察另外一个线程,所有的线程操作都是无序的。前半句是指“线程内表现为串行的语义”,后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性
————————————————
版权声明:本文为CSDN博主「我还是个少年」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/a347911/article/details/88379625
————————————————
版权声明:本文为CSDN博主「我还是个少年」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/a347911/article/details/88379625
Java内存模型
JVM中存在一个主存区(Main Memory或Java Heap Memory),Java中所有变量都是存在主存中的,对于所有线程进行共享,而每个线程又存在自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作并非发生在主存区,而是发生在工作内存中,而线程之间是不能直接相互访问,变量在程序中的传递,是依赖主存来完成的。
————————————————
版权声明:本文为CSDN博主「我还是个少年」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/a347911/article/details/88379625
————————————————
版权声明:本文为CSDN博主「我还是个少年」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/a347911/article/details/88379625
动态代理
反射
网络通信实现选择
分布式系统的Java中间件
7、软负载中心与集中配置管理
认识软负载中心
软负载中心的结构
内容聚合功能的设计
解决服务上下线的感知
软负载中心的数据分发的特点和设计
数据分发和消息订阅的区别
提升数据分发性能需要注意的问题
针对服务化的特性支持
软负载数据分组
提供自动感知以外的上下线开关
维护管理路由规则
从单机到集群
数据统一管理方案
数据对等管理方案
集中配置管理中心
客户端实现和容灾策略
服务端实现和容灾策略
数据库策略
收藏
0 条评论
下一页